Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,19 @@ Temporary Items

# Environment and secrets
.env
**/.env
.env.local
**/.env.local
.env.*.local
**/.env.*.local
*.pem
*.key
credentials.json

# Testing and coverage
__pycache__/
**/__pycache__/
*.py[cod]
**/coverage/
**/test-results/
**/playwright-report/
Expand Down
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
inject-workspace-packages=true
Original file line number Diff line number Diff line change
Expand Up @@ -1748,7 +1748,7 @@ final class MenuBarAppController: NSObject, NSApplicationDelegate {
alert.alertStyle = .informational
alert.messageText = "Update Available: v\(targetVersion)"
alert.informativeText =
"Current version: v\(status.currentVersion)\nInstalling the update will restart Cued and migrate the local database if needed."
"Current version: v\(status.currentVersion)\nInstalling the update will restart Cued and initialize the local database schema if needed."
alert.addButton(withTitle: "Install and Restart")
alert.addButton(withTitle: "Later")
if status.releaseUrl != nil {
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
"check:ci-local": "sh scripts/check-ci-local.sh",
"monitor:upstreams": "node scripts/monitor-upstreams.mjs",
"smoke:auth-lifecycle": "tsx scripts/smoke-auth-lifecycle.ts",
"smoke:actions-local": "tsx scripts/smoke-actions-local.ts",
"smoke:actions-personal": "tsx scripts/smoke-actions-personal.ts",
"smoke:actions-sandbox": "tsx scripts/smoke-actions-sandbox.ts",
"smoke:actions-plugin-local": "tsx scripts/smoke-actions-plugin-local.ts",
"bootstrap:signal:macos": "bash scripts/fetch-signal-cli-macos.sh",
"check:native:macos": "swift build --package-path native/macos/CuedNative -c release",
"check:biome": "biome check .",
Expand Down
1 change: 1 addition & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions scripts/build-cued-daemon-app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ mkdir -p "$(dirname "$SLACK_HELPER_SOURCE")"
(cd "$ROOT_DIR/native/helpers/slack-go" && GOWORK=off go build -o "$SLACK_HELPER_SOURCE" .) >/dev/null
mkdir -p "$(dirname "$WHATSAPP_HELPER_SOURCE")"
(cd "$ROOT_DIR/native/helpers/whatsapp-go" && GOWORK=off go build -o "$WHATSAPP_HELPER_SOURCE" .) >/dev/null
npm_config_ignore_scripts=true pnpm --dir "$ROOT_DIR" --filter . deploy --legacy --prod "$DEPLOY_STAGING_DIR" >/dev/null
npm_config_ignore_scripts=true pnpm --dir "$ROOT_DIR" --filter . deploy --prod "$DEPLOY_STAGING_DIR" >/dev/null
copy_better_sqlite3_binary

rm -rf "$APP_BUNDLE"
Expand Down Expand Up @@ -285,7 +285,7 @@ cp "$BETTER_SQLITE3_BINDING_SOURCE" "$BETTER_SQLITE3_RUNTIME_DIR/build/Release/b

# Remove symlinks that escape the bundled runtime or no longer resolve after deploy.
"$NODE_PATH" "$RUNTIME_SYMLINK_PRUNER" "$RUNTIME_DIR" >/dev/null
# `pnpm deploy --legacy --prod` can still leave a handful of dangling package links behind.
# `pnpm deploy --prod` can still leave a handful of dangling package links behind.
find -L "$RUNTIME_DIR" -type l -exec rm -f {} +
rm -rf "$RUNTIME_DIR/node_modules/cued" "$RUNTIME_DIR/node_modules/@cued/app"
# The bundled CLI only needs compiled JS, package metadata, and production
Expand Down
221 changes: 221 additions & 0 deletions scripts/smoke-actions-local.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import { loadActionExecutor } from "../src/actions/executor-loader.js";
import { ActionDefinitionRegistry } from "../src/actions/registry.js";
import { openCuedDatabaseReadOnly } from "../src/db/database.js";

type ContactRow = {
id: string;
name: string | null;
};

type ConversationRow = {
id: string;
message_count: number;
};

function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}

const registry = ActionDefinitionRegistry.load();
const definitions = registry.list();
assert(definitions.length > 0, "Expected at least one action definition.");
for (const definition of definitions) {
assert(
loadActionExecutor(definition),
`Missing executor for ${definition.type}@${definition.version}`,
);
}

const db = openCuedDatabaseReadOnly();
try {
const contacts = db.executeReadOnlySql(`
SELECT id, name
FROM contacts
WHERE archived = 0
ORDER BY updated_at DESC, created_at DESC
LIMIT 2
`) as ContactRow[];

const memoryValidation =
contacts[0] != null
? registry.validatePayload("contact.memory.add", "1", {
contactId: contacts[0].id,
body: "Local action smoke validation only. Do not write.",
sourceKind: "smoke",
})
: null;
assert(
memoryValidation == null || memoryValidation.ok,
`contact.memory.add payload failed validation: ${memoryValidation?.errors.join("; ")}`,
);

const mergeValidation =
contacts.length >= 2
? registry.validatePayload("contact.merge", "1", {
primaryContactId: contacts[0]!.id,
secondaryContactId: contacts[1]!.id,
reason: "Local action smoke validation only. Do not write.",
})
: null;
assert(
mergeValidation == null || mergeValidation.ok,
`contact.merge payload failed validation: ${mergeValidation?.errors.join("; ")}`,
);

const followupValidation =
contacts[0] != null
? registry.validatePayload("contact.followup.recommend", "1", {
contactId: contacts[0].id,
reason: "Local action smoke validation only. Do not write.",
suggestedMessage: "Local action smoke validation only.",
evidence: { source: "smoke-actions-local" },
})
: null;
assert(
followupValidation == null || followupValidation.ok,
`contact.followup.recommend payload failed validation: ${followupValidation?.errors.join(
"; ",
)}`,
);

const enrichmentValidation =
contacts[0] != null
? registry.validatePayload("contact.enrichment.recommend", "1", {
contactId: contacts[0].id,
field: "profile_url",
value: "Local action smoke validation only. Do not write.",
sourceKind: "smoke",
evidence: { source: "smoke-actions-local" },
})
: null;
assert(
enrichmentValidation == null || enrichmentValidation.ok,
`contact.enrichment.recommend payload failed validation: ${enrichmentValidation?.errors.join(
"; ",
)}`,
);

const introductionValidation =
contacts.length >= 2
? registry.validatePayload("contact.introduction.recommend", "1", {
fromContactId: contacts[0]!.id,
toContactId: contacts[1]!.id,
reason: "Local action smoke validation only. Do not write.",
suggestedIntro: "Local action smoke validation only.",
evidence: { source: "smoke-actions-local" },
})
: null;
assert(
introductionValidation == null || introductionValidation.ok,
`contact.introduction.recommend payload failed validation: ${introductionValidation?.errors.join(
"; ",
)}`,
);

const messageDraftValidation =
contacts[0] != null
? registry.validatePayload("contact.message.draft", "1", {
contactId: contacts[0].id,
body: "Local action smoke validation only. Do not send.",
reason: "Local action smoke validation only. Do not write.",
channelHint: "smoke",
evidence: { source: "smoke-actions-local" },
})
: null;
assert(
messageDraftValidation == null || messageDraftValidation.ok,
`contact.message.draft payload failed validation: ${messageDraftValidation?.errors.join("; ")}`,
);

const aliases = db.listContactMergeAliases();
const conversations = db.executeReadOnlySql(`
SELECT c.id, COUNT(m.id) AS message_count
FROM conversations c
JOIN messages m ON m.conversation_id = c.id
WHERE c.is_active = 1
AND m.is_deleted = 0
GROUP BY c.id
ORDER BY MAX(m.sent_at) DESC
LIMIT 1
`) as ConversationRow[];
const summaryDraftValidation =
conversations[0] != null
? registry.validatePayload("conversation.summary.draft", "1", {
conversationId: conversations[0].id,
summary: "Local action smoke validation only. Do not write.",
reason: "Local action smoke validation only. Do not write.",
timeWindow: "recent",
evidence: {
source: "smoke-actions-local",
messageCount: conversations[0].message_count,
},
})
: null;
assert(
summaryDraftValidation == null || summaryDraftValidation.ok,
`conversation.summary.draft payload failed validation: ${summaryDraftValidation?.errors.join(
"; ",
)}`,
);
const conversationFollowupValidation =
conversations[0] != null
? registry.validatePayload("conversation.followup.recommend", "1", {
conversationId: conversations[0].id,
reason: "Local action smoke validation only. Do not write.",
suggestedNextStep: "Local action smoke validation only.",
evidence: {
source: "smoke-actions-local",
messageCount: conversations[0].message_count,
},
})
: null;
assert(
conversationFollowupValidation == null || conversationFollowupValidation.ok,
`conversation.followup.recommend payload failed validation: ${conversationFollowupValidation?.errors.join(
"; ",
)}`,
);
const hasActionsTable =
db.executeReadOnlySql(`
SELECT name
FROM sqlite_master
WHERE type = 'table'
AND name = 'actions'
LIMIT 1
`).length > 0;
const recentActions = hasActionsTable
? db.executeReadOnlySql(`
SELECT action_type, status, approval_status, execution_status, queued_at
FROM actions
ORDER BY queued_at DESC
LIMIT 5
`)
: [];

process.stdout.write(
`${JSON.stringify(
{
ok: true,
readonly: true,
definitions: definitions.map((definition) => ({
type: definition.type,
version: definition.version,
module: definition.module,
sourcePath: definition.sourcePath,
rebuildProjection: definition.postExecution.rebuildProjection,
})),
sampledContactCount: contacts.length,
sampledContactsHaveNames: contacts.map((contact) => Boolean(contact.name)),
sampledConversationCount: conversations.length,
mergeAliasCount: aliases.length,
recentActionCount: Array.isArray(recentActions) ? recentActions.length : 0,
},
null,
2,
)}\n`,
);
} finally {
db.close();
}
Loading
Loading