@getrift/rift 0.1.0-beta.2 → 0.1.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -9
- package/dist/src/auth/keychain.d.ts +9 -0
- package/dist/src/auth/keychain.d.ts.map +1 -1
- package/dist/src/auth/keychain.js +37 -0
- package/dist/src/auth/keychain.js.map +1 -1
- package/dist/src/capture/auto-capture.d.ts +7 -0
- package/dist/src/capture/auto-capture.d.ts.map +1 -1
- package/dist/src/capture/auto-capture.js +82 -15
- package/dist/src/capture/auto-capture.js.map +1 -1
- package/dist/src/capture/auto-repair.d.ts +110 -0
- package/dist/src/capture/auto-repair.d.ts.map +1 -0
- package/dist/src/capture/auto-repair.js +269 -0
- package/dist/src/capture/auto-repair.js.map +1 -0
- package/dist/src/capture/codex-cli-triage-provider.d.ts.map +1 -1
- package/dist/src/capture/codex-cli-triage-provider.js +4 -3
- package/dist/src/capture/codex-cli-triage-provider.js.map +1 -1
- package/dist/src/capture/observability.d.ts +42 -0
- package/dist/src/capture/observability.d.ts.map +1 -1
- package/dist/src/capture/observability.js +45 -4
- package/dist/src/capture/observability.js.map +1 -1
- package/dist/src/capture/recover-quarantine.d.ts +260 -0
- package/dist/src/capture/recover-quarantine.d.ts.map +1 -0
- package/dist/src/capture/recover-quarantine.js +522 -0
- package/dist/src/capture/recover-quarantine.js.map +1 -0
- package/dist/src/cli/commands/backfill.d.ts.map +1 -1
- package/dist/src/cli/commands/backfill.js +5 -2
- package/dist/src/cli/commands/backfill.js.map +1 -1
- package/dist/src/cli/commands/capture-recover.d.ts +40 -0
- package/dist/src/cli/commands/capture-recover.d.ts.map +1 -0
- package/dist/src/cli/commands/capture-recover.js +184 -0
- package/dist/src/cli/commands/capture-recover.js.map +1 -0
- package/dist/src/cli/commands/capture.d.ts.map +1 -1
- package/dist/src/cli/commands/capture.js +96 -5
- package/dist/src/cli/commands/capture.js.map +1 -1
- package/dist/src/cli/commands/doctor.d.ts +6 -0
- package/dist/src/cli/commands/doctor.d.ts.map +1 -0
- package/dist/src/cli/commands/doctor.js +242 -0
- package/dist/src/cli/commands/doctor.js.map +1 -0
- package/dist/src/cli/commands/feedback.d.ts +12 -0
- package/dist/src/cli/commands/feedback.d.ts.map +1 -1
- package/dist/src/cli/commands/feedback.js +93 -4
- package/dist/src/cli/commands/feedback.js.map +1 -1
- package/dist/src/cli/commands/mcp-install.js +5 -2
- package/dist/src/cli/commands/mcp-install.js.map +1 -1
- package/dist/src/cli/commands/menubar.d.ts +80 -0
- package/dist/src/cli/commands/menubar.d.ts.map +1 -0
- package/dist/src/cli/commands/menubar.js +388 -0
- package/dist/src/cli/commands/menubar.js.map +1 -0
- package/dist/src/cli/commands/onboard.d.ts +143 -5
- package/dist/src/cli/commands/onboard.d.ts.map +1 -1
- package/dist/src/cli/commands/onboard.js +844 -188
- package/dist/src/cli/commands/onboard.js.map +1 -1
- package/dist/src/cli/commands/rebuild.d.ts.map +1 -1
- package/dist/src/cli/commands/rebuild.js +6 -3
- package/dist/src/cli/commands/rebuild.js.map +1 -1
- package/dist/src/cli/commands/reconcile.d.ts.map +1 -1
- package/dist/src/cli/commands/reconcile.js +12 -0
- package/dist/src/cli/commands/reconcile.js.map +1 -1
- package/dist/src/cli/commands/review.d.ts.map +1 -1
- package/dist/src/cli/commands/review.js +22 -7
- package/dist/src/cli/commands/review.js.map +1 -1
- package/dist/src/cli/commands/search.d.ts +2 -0
- package/dist/src/cli/commands/search.d.ts.map +1 -1
- package/dist/src/cli/commands/search.js +34 -4
- package/dist/src/cli/commands/search.js.map +1 -1
- package/dist/src/cli/commands/status.d.ts +9 -7
- package/dist/src/cli/commands/status.d.ts.map +1 -1
- package/dist/src/cli/commands/status.js +117 -12
- package/dist/src/cli/commands/status.js.map +1 -1
- package/dist/src/cli/commands/token-issue.d.ts.map +1 -1
- package/dist/src/cli/commands/token-issue.js +9 -1
- package/dist/src/cli/commands/token-issue.js.map +1 -1
- package/dist/src/cli/commands/triage.d.ts.map +1 -1
- package/dist/src/cli/commands/triage.js +7 -5
- package/dist/src/cli/commands/triage.js.map +1 -1
- package/dist/src/cli/commands/update.d.ts +80 -0
- package/dist/src/cli/commands/update.d.ts.map +1 -0
- package/dist/src/cli/commands/update.js +390 -0
- package/dist/src/cli/commands/update.js.map +1 -0
- package/dist/src/cli/default-config-path.d.ts +15 -0
- package/dist/src/cli/default-config-path.d.ts.map +1 -0
- package/dist/src/cli/default-config-path.js +27 -0
- package/dist/src/cli/default-config-path.js.map +1 -0
- package/dist/src/cli/feedback/feedback-config.d.ts +46 -0
- package/dist/src/cli/feedback/feedback-config.d.ts.map +1 -1
- package/dist/src/cli/feedback/feedback-config.js +130 -4
- package/dist/src/cli/feedback/feedback-config.js.map +1 -1
- package/dist/src/cli/feedback/feedback-history.d.ts +7 -0
- package/dist/src/cli/feedback/feedback-history.d.ts.map +1 -1
- package/dist/src/cli/feedback/feedback-history.js +39 -9
- package/dist/src/cli/feedback/feedback-history.js.map +1 -1
- package/dist/src/cli/feedback/feedback-payload.d.ts +22 -1
- package/dist/src/cli/feedback/feedback-payload.d.ts.map +1 -1
- package/dist/src/cli/feedback/feedback-payload.js.map +1 -1
- package/dist/src/cli/feedback/feedback-relay.d.ts +2 -2
- package/dist/src/cli/feedback/feedback-relay.d.ts.map +1 -1
- package/dist/src/cli/feedback/feedback-relay.js.map +1 -1
- package/dist/src/cli/feedback/invite.d.ts +17 -0
- package/dist/src/cli/feedback/invite.d.ts.map +1 -0
- package/dist/src/cli/feedback/invite.js +67 -0
- package/dist/src/cli/feedback/invite.js.map +1 -0
- package/dist/src/cli/feedback/relay-secret-store.d.ts +32 -0
- package/dist/src/cli/feedback/relay-secret-store.d.ts.map +1 -0
- package/dist/src/cli/feedback/relay-secret-store.js +137 -0
- package/dist/src/cli/feedback/relay-secret-store.js.map +1 -0
- package/dist/src/cli/http-client.d.ts +93 -1
- package/dist/src/cli/http-client.d.ts.map +1 -1
- package/dist/src/cli/http-client.js +254 -6
- package/dist/src/cli/http-client.js.map +1 -1
- package/dist/src/cli/index.d.ts.map +1 -1
- package/dist/src/cli/index.js +29 -6
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/cli/postinstall-menubar.d.ts +22 -0
- package/dist/src/cli/postinstall-menubar.d.ts.map +1 -0
- package/dist/src/cli/postinstall-menubar.js +53 -0
- package/dist/src/cli/postinstall-menubar.js.map +1 -0
- package/dist/src/cli/status/friend-header.d.ts +16 -1
- package/dist/src/cli/status/friend-header.d.ts.map +1 -1
- package/dist/src/cli/status/friend-header.js +354 -26
- package/dist/src/cli/status/friend-header.js.map +1 -1
- package/dist/src/cli/status/local-signals.d.ts +18 -0
- package/dist/src/cli/status/local-signals.d.ts.map +1 -1
- package/dist/src/cli/status/local-signals.js +29 -0
- package/dist/src/cli/status/local-signals.js.map +1 -1
- package/dist/src/cli/ui.d.ts +47 -0
- package/dist/src/cli/ui.d.ts.map +1 -0
- package/dist/src/cli/ui.js +166 -0
- package/dist/src/cli/ui.js.map +1 -0
- package/dist/src/config/schema.d.ts +79 -0
- package/dist/src/config/schema.d.ts.map +1 -1
- package/dist/src/config/schema.js +44 -0
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/diagnostics/codex-preflight.d.ts +33 -0
- package/dist/src/diagnostics/codex-preflight.d.ts.map +1 -0
- package/dist/src/diagnostics/codex-preflight.js +75 -0
- package/dist/src/diagnostics/codex-preflight.js.map +1 -0
- package/dist/src/diagnostics/doctor.d.ts +114 -0
- package/dist/src/diagnostics/doctor.d.ts.map +1 -0
- package/dist/src/diagnostics/doctor.js +352 -0
- package/dist/src/diagnostics/doctor.js.map +1 -0
- package/dist/src/diagnostics/notify.d.ts +90 -0
- package/dist/src/diagnostics/notify.d.ts.map +1 -0
- package/dist/src/diagnostics/notify.js +177 -0
- package/dist/src/diagnostics/notify.js.map +1 -0
- package/dist/src/diagnostics/repair-prompt.d.ts +49 -0
- package/dist/src/diagnostics/repair-prompt.d.ts.map +1 -0
- package/dist/src/diagnostics/repair-prompt.js +223 -0
- package/dist/src/diagnostics/repair-prompt.js.map +1 -0
- package/dist/src/ingestion/inbox-core/conversation-fingerprint.d.ts +2 -0
- package/dist/src/ingestion/inbox-core/conversation-fingerprint.d.ts.map +1 -0
- package/dist/src/ingestion/inbox-core/conversation-fingerprint.js +27 -0
- package/dist/src/ingestion/inbox-core/conversation-fingerprint.js.map +1 -0
- package/dist/src/ingestion/inbox-core/conversation-key.d.ts +2 -0
- package/dist/src/ingestion/inbox-core/conversation-key.d.ts.map +1 -0
- package/dist/src/ingestion/inbox-core/conversation-key.js +31 -0
- package/dist/src/ingestion/inbox-core/conversation-key.js.map +1 -0
- package/dist/src/ingestion/inbox-core/extensions.d.ts +3 -0
- package/dist/src/ingestion/inbox-core/extensions.d.ts.map +1 -0
- package/dist/src/ingestion/inbox-core/extensions.js +16 -0
- package/dist/src/ingestion/inbox-core/extensions.js.map +1 -0
- package/dist/src/ingestion/inbox-core/idempotency.d.ts +2 -0
- package/dist/src/ingestion/inbox-core/idempotency.d.ts.map +1 -0
- package/dist/src/ingestion/inbox-core/idempotency.js +22 -0
- package/dist/src/ingestion/inbox-core/idempotency.js.map +1 -0
- package/dist/src/ingestion/inbox-core/index.d.ts +20 -0
- package/dist/src/ingestion/inbox-core/index.d.ts.map +1 -0
- package/dist/src/ingestion/inbox-core/index.js +20 -0
- package/dist/src/ingestion/inbox-core/index.js.map +1 -0
- package/dist/src/ingestion/inbox-core/source-detection.d.ts +2 -0
- package/dist/src/ingestion/inbox-core/source-detection.d.ts.map +1 -0
- package/dist/src/ingestion/inbox-core/source-detection.js +23 -0
- package/dist/src/ingestion/inbox-core/source-detection.js.map +1 -0
- package/dist/src/ingestion/inbox-core/source-sniffer.d.ts +11 -0
- package/dist/src/ingestion/inbox-core/source-sniffer.d.ts.map +1 -0
- package/dist/src/ingestion/inbox-core/source-sniffer.js +69 -0
- package/dist/src/ingestion/inbox-core/source-sniffer.js.map +1 -0
- package/dist/src/ingestion/inbox-core/zip-sniffer.d.ts +70 -0
- package/dist/src/ingestion/inbox-core/zip-sniffer.d.ts.map +1 -0
- package/dist/src/ingestion/inbox-core/zip-sniffer.js +161 -0
- package/dist/src/ingestion/inbox-core/zip-sniffer.js.map +1 -0
- package/dist/src/ingestion/inbox-watcher.d.ts.map +1 -1
- package/dist/src/ingestion/inbox-watcher.js +34 -50
- package/dist/src/ingestion/inbox-watcher.js.map +1 -1
- package/dist/src/ingestion/indexer.d.ts +7 -0
- package/dist/src/ingestion/indexer.d.ts.map +1 -1
- package/dist/src/ingestion/indexer.js +36 -2
- package/dist/src/ingestion/indexer.js.map +1 -1
- package/dist/src/ingestion/metadata-extraction.d.ts +8 -5
- package/dist/src/ingestion/metadata-extraction.d.ts.map +1 -1
- package/dist/src/ingestion/metadata-extraction.js +24 -5
- package/dist/src/ingestion/metadata-extraction.js.map +1 -1
- package/dist/src/ingestion/skip-quarantine.d.ts +10 -0
- package/dist/src/ingestion/skip-quarantine.d.ts.map +1 -0
- package/dist/src/ingestion/skip-quarantine.js +35 -0
- package/dist/src/ingestion/skip-quarantine.js.map +1 -0
- package/dist/src/jobs/handlers/compact.d.ts.map +1 -1
- package/dist/src/jobs/handlers/compact.js +30 -4
- package/dist/src/jobs/handlers/compact.js.map +1 -1
- package/dist/src/jobs/handlers/dedupe-conversations.d.ts +134 -0
- package/dist/src/jobs/handlers/dedupe-conversations.d.ts.map +1 -0
- package/dist/src/jobs/handlers/dedupe-conversations.js +371 -0
- package/dist/src/jobs/handlers/dedupe-conversations.js.map +1 -0
- package/dist/src/jobs/handlers/ingest.d.ts.map +1 -1
- package/dist/src/jobs/handlers/ingest.js +295 -41
- package/dist/src/jobs/handlers/ingest.js.map +1 -1
- package/dist/src/jobs/handlers/reconcile.d.ts +28 -0
- package/dist/src/jobs/handlers/reconcile.d.ts.map +1 -1
- package/dist/src/jobs/handlers/reconcile.js +145 -19
- package/dist/src/jobs/handlers/reconcile.js.map +1 -1
- package/dist/src/jobs/handlers/reindex.d.ts.map +1 -1
- package/dist/src/jobs/handlers/reindex.js +13 -2
- package/dist/src/jobs/handlers/reindex.js.map +1 -1
- package/dist/src/jobs/handlers/save.d.ts.map +1 -1
- package/dist/src/jobs/handlers/save.js +57 -3
- package/dist/src/jobs/handlers/save.js.map +1 -1
- package/dist/src/jobs/queue.d.ts +51 -1
- package/dist/src/jobs/queue.d.ts.map +1 -1
- package/dist/src/jobs/queue.js +466 -26
- package/dist/src/jobs/queue.js.map +1 -1
- package/dist/src/jobs/worker-entry.d.ts.map +1 -1
- package/dist/src/jobs/worker-entry.js +35 -7
- package/dist/src/jobs/worker-entry.js.map +1 -1
- package/dist/src/jobs/worker-process.d.ts +11 -0
- package/dist/src/jobs/worker-process.d.ts.map +1 -1
- package/dist/src/jobs/worker-process.js +37 -4
- package/dist/src/jobs/worker-process.js.map +1 -1
- package/dist/src/main.js +199 -46
- package/dist/src/main.js.map +1 -1
- package/dist/src/mcp/errors.d.ts.map +1 -1
- package/dist/src/mcp/errors.js +20 -1
- package/dist/src/mcp/errors.js.map +1 -1
- package/dist/src/mcp/server.d.ts.map +1 -1
- package/dist/src/mcp/server.js +43 -3
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/mcp/tools/context-pack.d.ts.map +1 -1
- package/dist/src/mcp/tools/context-pack.js +164 -23
- package/dist/src/mcp/tools/context-pack.js.map +1 -1
- package/dist/src/mcp/tools/search.d.ts +6 -2
- package/dist/src/mcp/tools/search.d.ts.map +1 -1
- package/dist/src/mcp/tools/search.js +35 -4
- package/dist/src/mcp/tools/search.js.map +1 -1
- package/dist/src/observability/embedding-events.d.ts +52 -0
- package/dist/src/observability/embedding-events.d.ts.map +1 -0
- package/dist/src/observability/embedding-events.js +149 -0
- package/dist/src/observability/embedding-events.js.map +1 -0
- package/dist/src/observability/index-events.d.ts +70 -0
- package/dist/src/observability/index-events.d.ts.map +1 -0
- package/dist/src/observability/index-events.js +148 -0
- package/dist/src/observability/index-events.js.map +1 -0
- package/dist/src/observability/onboarding-metric.d.ts +131 -0
- package/dist/src/observability/onboarding-metric.d.ts.map +1 -0
- package/dist/src/observability/onboarding-metric.js +351 -0
- package/dist/src/observability/onboarding-metric.js.map +1 -0
- package/dist/src/observability/tool-usage-stats.d.ts +77 -4
- package/dist/src/observability/tool-usage-stats.d.ts.map +1 -1
- package/dist/src/observability/tool-usage-stats.js +112 -32
- package/dist/src/observability/tool-usage-stats.js.map +1 -1
- package/dist/src/observability/tool-usage.d.ts +100 -7
- package/dist/src/observability/tool-usage.d.ts.map +1 -1
- package/dist/src/observability/tool-usage.js +196 -33
- package/dist/src/observability/tool-usage.js.map +1 -1
- package/dist/src/observability/version-check.d.ts +71 -0
- package/dist/src/observability/version-check.d.ts.map +1 -0
- package/dist/src/observability/version-check.js +198 -0
- package/dist/src/observability/version-check.js.map +1 -0
- package/dist/src/providers/basic-metadata-extraction.d.ts +60 -0
- package/dist/src/providers/basic-metadata-extraction.d.ts.map +1 -0
- package/dist/src/providers/basic-metadata-extraction.js +114 -0
- package/dist/src/providers/basic-metadata-extraction.js.map +1 -0
- package/dist/src/providers/codex-cli-metadata-extraction.d.ts +1 -0
- package/dist/src/providers/codex-cli-metadata-extraction.d.ts.map +1 -1
- package/dist/src/providers/codex-cli-metadata-extraction.js +6 -2
- package/dist/src/providers/codex-cli-metadata-extraction.js.map +1 -1
- package/dist/src/providers/codex-cli-model.d.ts +61 -0
- package/dist/src/providers/codex-cli-model.d.ts.map +1 -0
- package/dist/src/providers/codex-cli-model.js +194 -0
- package/dist/src/providers/codex-cli-model.js.map +1 -0
- package/dist/src/providers/codex-cli-runner.d.ts +39 -0
- package/dist/src/providers/codex-cli-runner.d.ts.map +1 -1
- package/dist/src/providers/codex-cli-runner.js +234 -48
- package/dist/src/providers/codex-cli-runner.js.map +1 -1
- package/dist/src/providers/conversation-generation.d.ts.map +1 -1
- package/dist/src/providers/conversation-generation.js +43 -6
- package/dist/src/providers/conversation-generation.js.map +1 -1
- package/dist/src/providers/ollama-embed.d.ts +2 -1
- package/dist/src/providers/ollama-embed.d.ts.map +1 -1
- package/dist/src/providers/ollama-embed.js +1 -0
- package/dist/src/providers/ollama-embed.js.map +1 -1
- package/dist/src/providers/openai-metadata-extraction.d.ts +3 -3
- package/dist/src/providers/openai-metadata-extraction.d.ts.map +1 -1
- package/dist/src/providers/openai-metadata-extraction.js +18 -3
- package/dist/src/providers/openai-metadata-extraction.js.map +1 -1
- package/dist/src/providers/placeholder-embed.d.ts +56 -0
- package/dist/src/providers/placeholder-embed.d.ts.map +1 -0
- package/dist/src/providers/placeholder-embed.js +64 -0
- package/dist/src/providers/placeholder-embed.js.map +1 -0
- package/dist/src/providers/stub.d.ts +2 -0
- package/dist/src/providers/stub.d.ts.map +1 -1
- package/dist/src/providers/stub.js +2 -0
- package/dist/src/providers/stub.js.map +1 -1
- package/dist/src/providers/types.d.ts +11 -0
- package/dist/src/providers/types.d.ts.map +1 -1
- package/dist/src/providers/voyage.d.ts +2 -1
- package/dist/src/providers/voyage.d.ts.map +1 -1
- package/dist/src/providers/voyage.js +1 -0
- package/dist/src/providers/voyage.js.map +1 -1
- package/dist/src/retrieval/compact.d.ts +116 -2
- package/dist/src/retrieval/compact.d.ts.map +1 -1
- package/dist/src/retrieval/compact.js +158 -5
- package/dist/src/retrieval/compact.js.map +1 -1
- package/dist/src/retrieval/context-pack.d.ts +114 -0
- package/dist/src/retrieval/context-pack.d.ts.map +1 -1
- package/dist/src/retrieval/context-pack.js +292 -8
- package/dist/src/retrieval/context-pack.js.map +1 -1
- package/dist/src/retrieval/current-truth.d.ts +360 -0
- package/dist/src/retrieval/current-truth.d.ts.map +1 -0
- package/dist/src/retrieval/current-truth.js +766 -0
- package/dist/src/retrieval/current-truth.js.map +1 -0
- package/dist/src/retrieval/git-state.d.ts +53 -0
- package/dist/src/retrieval/git-state.d.ts.map +1 -0
- package/dist/src/retrieval/git-state.js +174 -0
- package/dist/src/retrieval/git-state.js.map +1 -0
- package/dist/src/retrieval/lexical.d.ts.map +1 -1
- package/dist/src/retrieval/lexical.js +19 -3
- package/dist/src/retrieval/lexical.js.map +1 -1
- package/dist/src/retrieval/locator-boost.d.ts +37 -0
- package/dist/src/retrieval/locator-boost.d.ts.map +1 -0
- package/dist/src/retrieval/locator-boost.js +129 -0
- package/dist/src/retrieval/locator-boost.js.map +1 -0
- package/dist/src/retrieval/report-demotion.d.ts +46 -0
- package/dist/src/retrieval/report-demotion.d.ts.map +1 -0
- package/dist/src/retrieval/report-demotion.js +169 -0
- package/dist/src/retrieval/report-demotion.js.map +1 -0
- package/dist/src/retrieval/vector.d.ts.map +1 -1
- package/dist/src/retrieval/vector.js +11 -2
- package/dist/src/retrieval/vector.js.map +1 -1
- package/dist/src/server/app.d.ts.map +1 -1
- package/dist/src/server/app.js +92 -11
- package/dist/src/server/app.js.map +1 -1
- package/dist/src/server/routes/compact.d.ts.map +1 -1
- package/dist/src/server/routes/compact.js +4 -1
- package/dist/src/server/routes/compact.js.map +1 -1
- package/dist/src/server/routes/context.d.ts +1 -1
- package/dist/src/server/routes/context.d.ts.map +1 -1
- package/dist/src/server/routes/context.js +2 -1
- package/dist/src/server/routes/context.js.map +1 -1
- package/dist/src/server/routes/conversations-search.d.ts.map +1 -1
- package/dist/src/server/routes/conversations-search.js +28 -3
- package/dist/src/server/routes/conversations-search.js.map +1 -1
- package/dist/src/server/routes/enqueue.d.ts +11 -0
- package/dist/src/server/routes/enqueue.d.ts.map +1 -0
- package/dist/src/server/routes/enqueue.js +17 -0
- package/dist/src/server/routes/enqueue.js.map +1 -0
- package/dist/src/server/routes/friend-status.d.ts +339 -3
- package/dist/src/server/routes/friend-status.d.ts.map +1 -1
- package/dist/src/server/routes/friend-status.js +447 -13
- package/dist/src/server/routes/friend-status.js.map +1 -1
- package/dist/src/server/routes/ingest.d.ts.map +1 -1
- package/dist/src/server/routes/ingest.js +5 -2
- package/dist/src/server/routes/ingest.js.map +1 -1
- package/dist/src/server/routes/mcp-usage.d.ts +5 -4
- package/dist/src/server/routes/mcp-usage.d.ts.map +1 -1
- package/dist/src/server/routes/mcp-usage.js.map +1 -1
- package/dist/src/server/routes/reconcile.d.ts.map +1 -1
- package/dist/src/server/routes/reconcile.js +20 -1
- package/dist/src/server/routes/reconcile.js.map +1 -1
- package/dist/src/server/routes/reindex.d.ts.map +1 -1
- package/dist/src/server/routes/reindex.js +4 -1
- package/dist/src/server/routes/reindex.js.map +1 -1
- package/dist/src/server/routes/save.d.ts.map +1 -1
- package/dist/src/server/routes/save.js +4 -1
- package/dist/src/server/routes/save.js.map +1 -1
- package/dist/src/server/routes/search.d.ts +1 -1
- package/dist/src/server/routes/search.d.ts.map +1 -1
- package/dist/src/server/routes/search.js +253 -29
- package/dist/src/server/routes/search.js.map +1 -1
- package/dist/src/server/routes/triage.d.ts.map +1 -1
- package/dist/src/server/routes/triage.js +4 -1
- package/dist/src/server/routes/triage.js.map +1 -1
- package/dist/src/storage/rebuild.d.ts +35 -1
- package/dist/src/storage/rebuild.d.ts.map +1 -1
- package/dist/src/storage/rebuild.js +288 -64
- package/dist/src/storage/rebuild.js.map +1 -1
- package/dist/src/storage/tables.d.ts +29 -0
- package/dist/src/storage/tables.d.ts.map +1 -1
- package/dist/src/storage/tables.js +32 -1
- package/dist/src/storage/tables.js.map +1 -1
- package/operator/swiftbar/render-menu.py +524 -0
- package/operator/swiftbar/rift.10s.sh +176 -0
- package/package.json +9 -3
|
@@ -24,7 +24,7 @@ import fs from "node:fs";
|
|
|
24
24
|
import os from "node:os";
|
|
25
25
|
import path from "node:path";
|
|
26
26
|
import readline from "node:readline";
|
|
27
|
-
import { Command } from "commander";
|
|
27
|
+
import { Command, Option } from "commander";
|
|
28
28
|
import { CliError, createHttpClient, readToken, resolveBaseUrl, } from "../http-client.js";
|
|
29
29
|
import { issueToken } from "../token.js";
|
|
30
30
|
import { loadConfig } from "../../config/loader.js";
|
|
@@ -35,13 +35,30 @@ import { validateVoyageKey } from "../../onboarding/voyage-validate.js";
|
|
|
35
35
|
import { writeEnvFile } from "../../onboarding/env-file.js";
|
|
36
36
|
import { kickstartDaemon, waitForHealth, } from "../../onboarding/daemon-control.js";
|
|
37
37
|
import { runAutoCapture, resolveCodexCaptureDeps, } from "../../capture/auto-capture.js";
|
|
38
|
+
import { appendAutoCaptureRunRecord, buildAutoCaptureRunRecord, } from "../../capture/observability.js";
|
|
39
|
+
import { DEFAULT_AUTO_CAPTURE_SOURCES } from "../../capture/sources.js";
|
|
38
40
|
import { CodexCliTriageProvider } from "../../capture/codex-cli-triage-provider.js";
|
|
39
41
|
import { discoverClaudeCodeSessions } from "../../ingestion/parsers/claude-code-jsonl.js";
|
|
40
42
|
import { discoverCodexSessions } from "../../ingestion/parsers/codex-jsonl.js";
|
|
43
|
+
import { sniffInboxSource } from "../../ingestion/inbox-core/source-sniffer.js";
|
|
41
44
|
import { writeHookConfig } from "../hooks-writers/index.js";
|
|
42
45
|
import { HooksParseFailedError } from "../hooks-writers/index.js";
|
|
43
46
|
import { pollJob } from "../job-poller.js";
|
|
44
47
|
import { isJobFailure } from "../output.js";
|
|
48
|
+
import * as ui from "../ui.js";
|
|
49
|
+
import { getBuildInfo } from "../../server/build-info.js";
|
|
50
|
+
import { postFeedback } from "../feedback/feedback-relay.js";
|
|
51
|
+
import { parseInvite } from "../feedback/invite.js";
|
|
52
|
+
import { macRelaySecretStore, } from "../feedback/relay-secret-store.js";
|
|
53
|
+
import { LockedKeychainError } from "../../auth/keychain.js";
|
|
54
|
+
/**
|
|
55
|
+
* Resolve a `--no-<flag>` to a single boolean ("yes, skip this thing").
|
|
56
|
+
* Commander populates the affirmative key with `false`; programmatic
|
|
57
|
+
* callers may pass the `no*` key with `true`. Either form wins.
|
|
58
|
+
*/
|
|
59
|
+
export function isOff(opts, affirmative, negative) {
|
|
60
|
+
return opts[affirmative] === false || opts[negative] === true;
|
|
61
|
+
}
|
|
45
62
|
const SUPPORTED_INGEST_EXTENSIONS = new Set([".json", ".zip"]);
|
|
46
63
|
const DEFAULT_DATA_DIR = path.join(os.homedir(), "Library", "Application Support", "Rift", "data");
|
|
47
64
|
// Inline import is intentionally narrow until Slice 3 lands the real
|
|
@@ -52,16 +69,31 @@ export function makeOnboardCommand() {
|
|
|
52
69
|
return new Command("onboard")
|
|
53
70
|
.description("First-run wizard: validate Voyage key, capture, import, recall test")
|
|
54
71
|
.option("--voyage-key <key>", "Voyage API key (skips paste prompt)")
|
|
72
|
+
.option("--no-voyage-key", "Finish without a Voyage key — keyword-only (lexical) search; add a key later")
|
|
55
73
|
.option("--voyage-label <label>", "Operator-supplied display label for the Voyage project")
|
|
74
|
+
.option("--invite <code>", "Accept a feedback invite from Clem (or omit it to paste when asked)")
|
|
56
75
|
.option("--enable-feedback-relay <url>", "Opt into the Rift feedback relay non-interactively (URL required)")
|
|
76
|
+
.addOption(
|
|
77
|
+
// Hidden: back-compat operator flag. The secret never appears in
|
|
78
|
+
// user-facing help; friends use --invite (or the interactive paste).
|
|
79
|
+
new Option("--relay-secret <secret>", "Back-compat HMAC secret paired with --enable-feedback-relay (stored in the Keychain)").hideHelp())
|
|
80
|
+
.addOption(
|
|
81
|
+
// Hidden: dev/operator escape hatch for unsigned local receivers.
|
|
82
|
+
new Option("--allow-unsigned-feedback-relay", "Dev-only: allow --enable-feedback-relay without a secret (receiver rejects in prod)")
|
|
83
|
+
.hideHelp()
|
|
84
|
+
.default(false))
|
|
57
85
|
.option("--no-feedback-relay", "Decline the feedback relay (local-only)")
|
|
86
|
+
.option("--email <address>", "Opt into beta updates + feedback non-interactively (skips the prompt)")
|
|
58
87
|
.option("--import-export <path>", "Import an export inline (.json or .zip)")
|
|
59
88
|
.option("--no-import-export", "Skip the import-now prompt")
|
|
60
89
|
.option("--reconfigure-voyage", "Recovery flow: replace the Voyage key only", false)
|
|
61
90
|
.option("--yes", "Accept all defaults (non-interactive)", false)
|
|
62
91
|
.option("--skip-capture", "Skip the post-setup capture pass (test-only)", false)
|
|
92
|
+
.option("--no-codex-capture", "Skip the Codex CLI preflight + disable the capture pass for this run")
|
|
63
93
|
.option("--with-claude-hook", "Install the Rift policy hook into Claude Code without prompting", false)
|
|
64
94
|
.option("--no-claude-hook", "Skip the Claude Code policy-hook prompt entirely")
|
|
95
|
+
.option("--enable-codex-enrichment", "Opt into Codex AI metadata enrichment (default: AI-free import + keyword search)", false)
|
|
96
|
+
.option("--enable-capture", "Opt into scheduled chat capture + its Codex preflight (default: capture off, zero Codex calls)", false)
|
|
65
97
|
.action(async (opts, cmd) => {
|
|
66
98
|
const globalOpts = cmd.optsWithGlobals();
|
|
67
99
|
try {
|
|
@@ -78,7 +110,9 @@ export function makeOnboardCommand() {
|
|
|
78
110
|
}
|
|
79
111
|
});
|
|
80
112
|
}
|
|
81
|
-
|
|
113
|
+
// Exported for orchestrator-level tests (e.g. the capture-without-key gate);
|
|
114
|
+
// `makeOnboardCommand().action` is the only production caller.
|
|
115
|
+
export async function runOnboard(opts, globalOpts) {
|
|
82
116
|
const rl = readline.createInterface({
|
|
83
117
|
input: process.stdin,
|
|
84
118
|
output: process.stdout,
|
|
@@ -89,105 +123,236 @@ async function runOnboard(opts, globalOpts) {
|
|
|
89
123
|
await reconfigureVoyageFlow(opts, globalOpts, rl);
|
|
90
124
|
return;
|
|
91
125
|
}
|
|
92
|
-
|
|
93
|
-
say("Rift — first-run setup");
|
|
94
|
-
say("─────────────────────");
|
|
126
|
+
ui.banner();
|
|
95
127
|
// Step 1 — legacy migration (idempotent).
|
|
96
128
|
const dataDir = await ensureConfigAndDataDir(globalOpts.config, opts, rl);
|
|
97
|
-
say("");
|
|
98
|
-
say("Running legacy migration check…");
|
|
99
129
|
const migration = await runLegacyMigration({ dataDir });
|
|
100
130
|
if (migration.migratedCount > 0) {
|
|
101
|
-
|
|
131
|
+
ui.step("ok", "Existing data", `carried over ${migration.migratedCount} item(s)`);
|
|
102
132
|
}
|
|
103
133
|
else {
|
|
104
|
-
|
|
134
|
+
ui.step("ok", "Existing data", "nothing to carry over");
|
|
105
135
|
}
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
136
|
+
// Privacy contract — shown BEFORE the first outbound choice (the
|
|
137
|
+
// Voyage key, whose snippets leave the machine for embedding). A beta
|
|
138
|
+
// user should understand what stays local and what leaves before they
|
|
139
|
+
// paste anything.
|
|
140
|
+
ui.step("info", "Privacy", "");
|
|
141
|
+
sayPrivacyContract();
|
|
142
|
+
// Step 2 — Voyage key + validate + persist env. The daemon kickstart +
|
|
143
|
+
// smoke is deferred to Step 2e (after the opt-in writes below) so the
|
|
144
|
+
// respawned daemon boots with the final config.json.
|
|
145
|
+
const last4 = await collectAndPersistVoyageKey(opts, rl);
|
|
110
146
|
// Step 2b — sanitize + persist optional --voyage-label to config.json
|
|
111
147
|
// (backup first). Invalid labels are dropped without echoing the raw
|
|
112
148
|
// value, so a key-shaped, path-shaped, or legacy-name-shaped label
|
|
113
149
|
// can never leak through stdout or config.json.
|
|
114
150
|
const safeLabel = applyVoyageLabel(opts.voyageLabel, globalOpts.config, say);
|
|
115
|
-
// Step
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
151
|
+
// Step 2c — optional opt-in to Codex AI metadata enrichment. Default is
|
|
152
|
+
// AI-free import + keyword search (zero Codex calls). Only persisted when
|
|
153
|
+
// the user explicitly passed --enable-codex-enrichment.
|
|
154
|
+
applyCodexEnrichmentOptIn(opts.enableCodexEnrichment, globalOpts.config, say);
|
|
155
|
+
// Step 2d — optional opt-in to scheduled chat capture. Default off, so a
|
|
156
|
+
// fresh install makes zero Codex calls. Auto-capture embeds each saved
|
|
157
|
+
// conversation, so it requires a Voyage key; opting in without one would
|
|
158
|
+
// persist `capture.enabled = true` that the daemon can't honor (it would
|
|
159
|
+
// wake hourly, find no embedding provider, and save nothing). So we only
|
|
160
|
+
// persist the opt-in when a key was provided, and warn otherwise.
|
|
161
|
+
// `captureEnabled` is the single source of truth for the capture lane:
|
|
162
|
+
// requested AND a key is present. It gates BOTH the persistence here and
|
|
163
|
+
// the Codex preflight in Step 3 — refusing the opt-in must also skip the
|
|
164
|
+
// preflight's Codex triage call, or `--enable-capture` without a key would
|
|
165
|
+
// still leak a Codex call.
|
|
166
|
+
const captureRequested = opts.enableCapture === true;
|
|
167
|
+
const captureEnabled = captureRequested && last4 !== null;
|
|
168
|
+
if (captureRequested && last4 === null) {
|
|
169
|
+
ui.note("--enable-capture ignored: capture embeds saved conversations and needs a Voyage key. Re-run `rift onboard` with a key to enable it.");
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
applyCaptureOptIn(opts.enableCapture, globalOpts.config, say);
|
|
173
|
+
}
|
|
174
|
+
// Step 2e — (re)start the daemon AFTER all config writes so it boots with
|
|
175
|
+
// the final config.json. The daemon builds its capture loop and metadata
|
|
176
|
+
// extractor once at boot (src/main.ts), so persisting the opt-ins above
|
|
177
|
+
// BEFORE this kickstart is what makes --enable-capture /
|
|
178
|
+
// --enable-codex-enrichment take effect immediately instead of only after
|
|
179
|
+
// a later restart. Key path only: the smoke asserts voyage_key_present,
|
|
180
|
+
// and the keyword-only path has no daemon smoke to run (the daemon was
|
|
181
|
+
// bootstrapped by install.sh and picks up config on its next start).
|
|
182
|
+
// `not_configured` / `agent_not_loaded` stay recoverable — the daemon
|
|
183
|
+
// simply isn't running yet on a fresh-Mac install before install.sh
|
|
184
|
+
// bootstraps the plist.
|
|
185
|
+
if (last4 !== null) {
|
|
186
|
+
const refresh = await daemonRefreshFlow(globalOpts);
|
|
187
|
+
if (!refresh.ok &&
|
|
188
|
+
refresh.kind !== "not_configured" &&
|
|
189
|
+
refresh.kind !== "agent_not_loaded") {
|
|
190
|
+
throw new CliError(`Voyage smoke failed after daemon kickstart: ${refresh.reason}`, "server_error");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Step 3 — Codex CLI preflight (capture lane only).
|
|
194
|
+
//
|
|
195
|
+
// Capture is OFF by default (trust-first): a fresh install makes zero
|
|
196
|
+
// Codex calls. The preflight below performs a real Codex triage call,
|
|
197
|
+
// so it ONLY runs when capture is actually enabled — i.e. `--enable-capture`
|
|
198
|
+
// AND a Voyage key (see `captureEnabled` above). Without the flag, or with
|
|
199
|
+
// the flag but no key, the capture lane — and its Codex preflight — is
|
|
200
|
+
// skipped entirely, and import + keyword search below still work with no
|
|
201
|
+
// Codex dependency.
|
|
202
|
+
//
|
|
203
|
+
// When capture IS enabled, a failed preflight is a warning, not fatal —
|
|
204
|
+
// onboarding continues and the capture pass is skipped for this run.
|
|
205
|
+
// `--no-codex-capture` skips the preflight even when capture was enabled
|
|
206
|
+
// (e.g. enable the daemon schedule but skip the one-shot).
|
|
207
|
+
let captureDisabled = false;
|
|
208
|
+
// `codexUnavailable` is set ONLY when capture was enabled and the live
|
|
209
|
+
// Codex preflight actually failed — i.e. scheduled capture is configured
|
|
210
|
+
// on but cannot run until Codex is fixed. `--no-codex-capture` is NOT this
|
|
211
|
+
// case: it only skips the onboarding one-shot, leaving the daemon's
|
|
212
|
+
// scheduled capture to run normally. Keeping the two apart is what lets the
|
|
213
|
+
// import-prompt and summary copy stay honest instead of either over- or
|
|
214
|
+
// under-promising automatic capture.
|
|
215
|
+
let codexUnavailable = false;
|
|
216
|
+
if (!captureEnabled) {
|
|
217
|
+
ui.step("skip", "Chat access", captureRequested
|
|
218
|
+
? "capture needs a Voyage key — skipped"
|
|
219
|
+
: "capture off (default) · enable with --enable-capture");
|
|
220
|
+
captureDisabled = true;
|
|
221
|
+
}
|
|
222
|
+
else if (isOff(opts, "codexCapture", "noCodexCapture")) {
|
|
223
|
+
ui.step("skip", "Chat access", "skipped (--no-codex-capture) · auto-import off");
|
|
224
|
+
captureDisabled = true;
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
const codexSpin = new ui.Spinner("Chat access").start();
|
|
228
|
+
const codexOk = await codexPreflight();
|
|
229
|
+
if (codexOk) {
|
|
230
|
+
codexSpin.succeed("Chat access", "ready · Codex CLI");
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
codexSpin.fail("Chat access", "not ready · Codex CLI — auto-import off");
|
|
234
|
+
ui.note("To enable later: run `codex login`, then re-run `rift onboard`.");
|
|
235
|
+
captureDisabled = true;
|
|
236
|
+
codexUnavailable = true;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Will the daemon's scheduled auto-capture actually run going forward?
|
|
240
|
+
// Configured on (requested + Voyage key) AND Codex available. This is the
|
|
241
|
+
// signal the import prompt and the summary line must use — NOT bare
|
|
242
|
+
// `captureEnabled`, which is only "configured on" and stays true even after
|
|
243
|
+
// a failed Codex preflight already told the user auto-import is off.
|
|
244
|
+
const autoCaptureWillRun = captureEnabled && !codexUnavailable;
|
|
125
245
|
// Step 4 — discover sessions.
|
|
126
|
-
say("");
|
|
127
246
|
const claudeSessions = safeDiscover(() => discoverClaudeCodeSessions(path.join(os.homedir(), ".claude")));
|
|
128
247
|
const codexSessions = safeDiscover(() => discoverCodexSessions());
|
|
129
|
-
|
|
130
|
-
// Step 5 —
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (feedback.enabled) {
|
|
136
|
-
|
|
248
|
+
ui.step("ok", "Chat history", `${claudeSessions} Claude Code · ${codexSessions} Codex CLI`);
|
|
249
|
+
// Step 5 — beta opt-in (stay connected: news, pricing, feedback).
|
|
250
|
+
const feedback = await collectFeedbackPreference(opts, dataDir, rl);
|
|
251
|
+
if (feedback.email) {
|
|
252
|
+
ui.step("ok", "Stay connected", `${feedback.email} — opted in (news + feedback)`);
|
|
253
|
+
}
|
|
254
|
+
else if (feedback.enabled) {
|
|
255
|
+
ui.step("ok", "Stay connected", `feedback relay on (installation_id: ${feedback.installation_id})`);
|
|
137
256
|
}
|
|
138
257
|
else {
|
|
139
|
-
|
|
258
|
+
ui.step("ok", "Stay connected", "skipped — nothing shared, feedback stays local");
|
|
140
259
|
}
|
|
141
260
|
// Step 5b — optional Claude Code policy hook.
|
|
142
|
-
say("");
|
|
143
261
|
await maybeInstallClaudeCodeHook(opts, rl);
|
|
144
262
|
// Step 6 + 7 — watermark current sessions + run one capture pass.
|
|
145
263
|
let captureSaved = 0;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
264
|
+
let captureRan = false;
|
|
265
|
+
if (opts.skipCapture) {
|
|
266
|
+
ui.step("skip", "Chat import", "skipped (--skip-capture)");
|
|
267
|
+
}
|
|
268
|
+
else if (last4 === null) {
|
|
269
|
+
// Auto-capture saves embed each conversation, so it needs a real
|
|
270
|
+
// embedding provider. In keyword-only mode it is skipped; import +
|
|
271
|
+
// search below still work.
|
|
272
|
+
ui.step("skip", "Chat import", "skipped (no embedding key — capture needs one)");
|
|
273
|
+
}
|
|
274
|
+
else if (captureDisabled) {
|
|
275
|
+
// Capture lane is off — either the default (no --enable-capture) or a
|
|
276
|
+
// failed/declined Codex preflight. The "Chat access" step above already
|
|
277
|
+
// printed the specific reason.
|
|
278
|
+
ui.step("skip", "Chat import", "skipped (capture not enabled)");
|
|
151
279
|
}
|
|
152
280
|
else {
|
|
153
|
-
|
|
281
|
+
captureRan = true;
|
|
282
|
+
const captureResult = await runFirstCapturePass(globalOpts.config, dataDir);
|
|
283
|
+
captureSaved = captureResult.saved;
|
|
284
|
+
// Per A-1.1/C-1.3: a token-issuance failure during capture-pass
|
|
285
|
+
// is fatal. The wizard cannot meaningfully continue without auth
|
|
286
|
+
// (import + recall both need a token), and silently advancing
|
|
287
|
+
// past it was the original bug.
|
|
288
|
+
if (captureResult.fatal) {
|
|
289
|
+
rl.close();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
154
292
|
}
|
|
155
293
|
// Step 8 — optional export import.
|
|
156
|
-
say("");
|
|
157
294
|
let importSucceeded = false;
|
|
158
|
-
|
|
159
|
-
|
|
295
|
+
let importAttempted = false;
|
|
296
|
+
if (isOff(opts, "importExport", "noImportExport")) {
|
|
297
|
+
ui.step("skip", "File import", "skipped (--no-import-export)");
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
const importPath = await collectImportPath(opts, rl, {
|
|
301
|
+
willRun: autoCaptureWillRun,
|
|
302
|
+
configured: captureEnabled,
|
|
303
|
+
});
|
|
160
304
|
if (importPath) {
|
|
305
|
+
importAttempted = true;
|
|
161
306
|
const outcome = await runImport(importPath, globalOpts.config);
|
|
162
307
|
if (outcome.kind === "imported" || outcome.kind === "duplicate") {
|
|
163
308
|
importSucceeded = true;
|
|
164
309
|
}
|
|
165
310
|
}
|
|
166
311
|
else {
|
|
167
|
-
|
|
312
|
+
ui.step("skip", "File import", "none — run `rift import <path> --source <name>` later");
|
|
168
313
|
}
|
|
169
314
|
}
|
|
170
|
-
else {
|
|
171
|
-
say("Skipping import (--no-import-export).");
|
|
172
|
-
}
|
|
173
315
|
// Step 9 — first-recall verification.
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
say("");
|
|
316
|
+
// A recall check only makes sense once this run actually landed user data.
|
|
317
|
+
// When nothing was imported or captured (the default, deliberate path), we
|
|
318
|
+
// skip the search check rather than start a spinner and FAIL it — failing a
|
|
319
|
+
// check the user opted out of is what made a clean setup feel broken.
|
|
179
320
|
const ingestedAny = captureSaved > 0 || importSucceeded;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
321
|
+
const ingestAttempted = captureRan || importAttempted;
|
|
322
|
+
let recall;
|
|
323
|
+
if (!ingestedAny) {
|
|
324
|
+
ui.step("skip", "Search check", "nothing imported yet — skipped");
|
|
325
|
+
recall = { ok: false, reason: "nothing imported yet" };
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
const recallSpin = new ui.Spinner("Search check").start();
|
|
329
|
+
recall = await firstRecallCheck(globalOpts.config);
|
|
330
|
+
if (recall.ok && recall.hits > 0) {
|
|
331
|
+
recallSpin.succeed("Search check", `found ${recall.hits} result(s)`);
|
|
332
|
+
}
|
|
333
|
+
else if (recall.ok) {
|
|
334
|
+
recallSpin.warn("Search check", "no results yet — index is fresh, try a query shortly");
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
recallSpin.fail("Search check", recall.reason);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
184
340
|
// Step 10 — next-action card.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
341
|
+
const card = decideOnboardOutcome({ ingestedAny, recall, ingestAttempted });
|
|
342
|
+
card.push(ui.pc.dim(last4 !== null
|
|
343
|
+
? `Voyage: key valid (last 4 …${last4})${safeLabel ? ` · label ${safeLabel}` : ""}`
|
|
344
|
+
: "Search: keyword-only (no embedding key) — add one later for semantic search"));
|
|
345
|
+
// One-line "what's on/off" so the friend leaves knowing whether Rift keeps
|
|
346
|
+
// itself fed. Three honest states: ready (configured + Codex available),
|
|
347
|
+
// configured-but-not-ready (Codex failed — don't claim it's running), and
|
|
348
|
+
// off (the default). Capture is off by default (zero-Codex trust default).
|
|
349
|
+
card.push(ui.pc.dim(autoCaptureWillRun
|
|
350
|
+
? "Auto-capture: on — new chats are captured on a schedule."
|
|
351
|
+
: captureEnabled
|
|
352
|
+
? "Auto-capture: configured on, but not ready — Codex CLI isn't available (run `codex login`)."
|
|
353
|
+
: "Auto-capture: off — enable with: rift onboard --enable-capture"));
|
|
354
|
+
ui.box(card);
|
|
355
|
+
ui.line("");
|
|
191
356
|
}
|
|
192
357
|
finally {
|
|
193
358
|
rl.close();
|
|
@@ -196,10 +361,11 @@ async function runOnboard(opts, globalOpts) {
|
|
|
196
361
|
// ----- Step 1: config.json + data dir -----
|
|
197
362
|
async function ensureConfigAndDataDir(configPath, opts, rl) {
|
|
198
363
|
const absoluteConfig = path.resolve(configPath);
|
|
364
|
+
const shownConfig = absoluteConfig.replace(os.homedir(), "~");
|
|
199
365
|
if (fs.existsSync(absoluteConfig)) {
|
|
200
366
|
const config = loadConfig(absoluteConfig);
|
|
201
|
-
|
|
202
|
-
|
|
367
|
+
ui.step("ok", "Settings", shownConfig);
|
|
368
|
+
ui.note(`data dir: ${config.data_paths.data_dir}`);
|
|
203
369
|
return config.data_paths.data_dir;
|
|
204
370
|
}
|
|
205
371
|
const defaultDataDir = DEFAULT_DATA_DIR;
|
|
@@ -222,15 +388,27 @@ async function ensureConfigAndDataDir(configPath, opts, rl) {
|
|
|
222
388
|
embedding: { provider: "voyage", model: "voyage-3-lite" },
|
|
223
389
|
data_paths: { data_dir: dataDir, jobs_dir: path.join(dataDir, "..", "jobs") },
|
|
224
390
|
rate_limit: { window_ms: 60_000, max_requests: 100 },
|
|
225
|
-
|
|
391
|
+
// Trust-first default: scheduled chat capture is OFF. The capture lane
|
|
392
|
+
// runs a Codex CLI triage call, so a fresh install sends no conversation
|
|
393
|
+
// content off-device, runs no Voyage embedding, and makes no Codex model
|
|
394
|
+
// call until the user opts in with `--enable-capture` (persisted below).
|
|
395
|
+
// (The daemon does make a metadata-only npm version check — no content, no
|
|
396
|
+
// key, no machine info.) The daemon gates auto-capture on `capture.enabled`.
|
|
397
|
+
capture: { enabled: false, interval_seconds: 3600 },
|
|
398
|
+
// Trust-first default: AI metadata enrichment is OFF. Import + keyword
|
|
399
|
+
// search run AI-free (BasicMetadataExtractor) and make no Codex calls,
|
|
400
|
+
// even on a machine with Codex installed. `--enable-codex-enrichment`
|
|
401
|
+
// (persisted below) flips this on. The `embedding` block above is a
|
|
402
|
+
// routing target only — without a Voyage key, search stays lexical.
|
|
403
|
+
enrichment: { ai_metadata: false },
|
|
226
404
|
};
|
|
227
405
|
fs.mkdirSync(path.dirname(absoluteConfig), { recursive: true });
|
|
228
406
|
fs.writeFileSync(absoluteConfig, JSON.stringify(config, null, 2) + "\n", {
|
|
229
407
|
encoding: "utf8",
|
|
230
408
|
mode: 0o644,
|
|
231
409
|
});
|
|
232
|
-
|
|
233
|
-
|
|
410
|
+
ui.step("ok", "Settings", `wrote default · ${shownConfig}`);
|
|
411
|
+
ui.note(`data dir: ${dataDir}`);
|
|
234
412
|
// Trigger ensureDirectories.
|
|
235
413
|
loadConfig(absoluteConfig);
|
|
236
414
|
return dataDir;
|
|
@@ -300,45 +478,135 @@ export function persistVoyageLabel(configPath, label) {
|
|
|
300
478
|
});
|
|
301
479
|
fs.renameSync(tmp, absolute);
|
|
302
480
|
}
|
|
481
|
+
/**
|
|
482
|
+
* Persist the Codex AI-metadata-enrichment opt-in into config.json. Default
|
|
483
|
+
* onboarding leaves `enrichment.ai_metadata = false` (AI-free import + keyword
|
|
484
|
+
* search, zero Codex calls). When the user passes `--enable-codex-enrichment`,
|
|
485
|
+
* this flips it to true so the daemon/worker select the Codex-backed extractor.
|
|
486
|
+
* Backs the file up first, mirroring `persistVoyageLabel`.
|
|
487
|
+
*
|
|
488
|
+
* Exported for orchestrator-level tests; `runOnboard` is the only production
|
|
489
|
+
* caller. Returns true when the flag was set and persisted, false otherwise.
|
|
490
|
+
*/
|
|
491
|
+
export function applyCodexEnrichmentOptIn(enable, configPath, emit) {
|
|
492
|
+
if (enable !== true)
|
|
493
|
+
return false;
|
|
494
|
+
const absolute = path.resolve(configPath);
|
|
495
|
+
if (!fs.existsSync(absolute)) {
|
|
496
|
+
throw new CliError(`Cannot persist enrichment opt-in: config not found at ${absolute}`, "validation");
|
|
497
|
+
}
|
|
498
|
+
const raw = fs.readFileSync(absolute, "utf8");
|
|
499
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
500
|
+
fs.writeFileSync(`${absolute}.bak.${stamp}`, raw, { encoding: "utf8", mode: 0o600 });
|
|
501
|
+
const parsed = JSON.parse(raw);
|
|
502
|
+
const enrichment = parsed["enrichment"] ?? {};
|
|
503
|
+
enrichment["ai_metadata"] = true;
|
|
504
|
+
parsed["enrichment"] = enrichment;
|
|
505
|
+
const tmp = `${absolute}.tmp.${process.pid}`;
|
|
506
|
+
fs.writeFileSync(tmp, JSON.stringify(parsed, null, 2) + "\n", {
|
|
507
|
+
encoding: "utf8",
|
|
508
|
+
mode: 0o644,
|
|
509
|
+
});
|
|
510
|
+
fs.renameSync(tmp, absolute);
|
|
511
|
+
emit("Codex AI metadata enrichment enabled (enrichment.ai_metadata = true).");
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Persist the scheduled-capture opt-in. Default install writes
|
|
516
|
+
* `capture.enabled = false` (trust-first: zero Codex calls on a fresh
|
|
517
|
+
* machine). When the user passes `--enable-capture`, this flips it to true
|
|
518
|
+
* so the daemon runs auto-capture on its interval. Backs the file up first,
|
|
519
|
+
* mirroring `applyCodexEnrichmentOptIn`.
|
|
520
|
+
*
|
|
521
|
+
* Exported for orchestrator-level tests; `runOnboard` is the only production
|
|
522
|
+
* caller. Returns true when the flag was set and persisted, false otherwise.
|
|
523
|
+
*/
|
|
524
|
+
export function applyCaptureOptIn(enable, configPath, emit) {
|
|
525
|
+
if (enable !== true)
|
|
526
|
+
return false;
|
|
527
|
+
const absolute = path.resolve(configPath);
|
|
528
|
+
if (!fs.existsSync(absolute)) {
|
|
529
|
+
throw new CliError(`Cannot persist capture opt-in: config not found at ${absolute}`, "validation");
|
|
530
|
+
}
|
|
531
|
+
const raw = fs.readFileSync(absolute, "utf8");
|
|
532
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
533
|
+
fs.writeFileSync(`${absolute}.bak.${stamp}`, raw, { encoding: "utf8", mode: 0o600 });
|
|
534
|
+
const parsed = JSON.parse(raw);
|
|
535
|
+
const capture = parsed["capture"] ?? {};
|
|
536
|
+
capture["enabled"] = true;
|
|
537
|
+
parsed["capture"] = capture;
|
|
538
|
+
const tmp = `${absolute}.tmp.${process.pid}`;
|
|
539
|
+
fs.writeFileSync(tmp, JSON.stringify(parsed, null, 2) + "\n", {
|
|
540
|
+
encoding: "utf8",
|
|
541
|
+
mode: 0o644,
|
|
542
|
+
});
|
|
543
|
+
fs.renameSync(tmp, absolute);
|
|
544
|
+
emit("Scheduled chat capture enabled (capture.enabled = true).");
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
303
547
|
// ----- Step 2: Voyage key flow -----
|
|
304
|
-
async function collectAndPersistVoyageKey(opts,
|
|
548
|
+
async function collectAndPersistVoyageKey(opts, rl) {
|
|
305
549
|
const key = await collectVoyageKey(opts, rl);
|
|
306
|
-
|
|
550
|
+
if (!key) {
|
|
551
|
+
// No key → keyword-only (lexical) mode. Not a failure; import + search
|
|
552
|
+
// still work. Skip validation, the env write, and the Voyage smoke.
|
|
553
|
+
ui.step("info", "Search index", "no key — keyword search (semantic off)");
|
|
554
|
+
ui.note("Add semantic search later: run `rift onboard` and paste a Voyage key.");
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
const validateSpin = new ui.Spinner("Search index").start();
|
|
307
558
|
const validation = await validateVoyageKey({ apiKey: key });
|
|
308
559
|
if (!validation.ok) {
|
|
560
|
+
validateSpin.fail("Search index", "validation failed");
|
|
309
561
|
throw new CliError(`Voyage validation failed: ${validation.reason}`, "validation");
|
|
310
562
|
}
|
|
311
|
-
|
|
563
|
+
validateSpin.succeed("Search index", `connected · Voyage · ····${validation.last4}`);
|
|
312
564
|
const envPath = defaultRiftEnvPath();
|
|
313
565
|
const writeResult = writeEnvFile({ filePath: envPath, key: "VOYAGE_API_KEY", value: key });
|
|
314
|
-
|
|
315
|
-
? "
|
|
316
|
-
: "
|
|
566
|
+
ui.note(writeResult.backedUp
|
|
567
|
+
? "wrote ~/.rift.env (existing file backed up)"
|
|
568
|
+
: "wrote ~/.rift.env (mode 0600)");
|
|
317
569
|
// Refresh process.env so any subsequent in-process Voyage call sees it.
|
|
318
570
|
loadRiftEnv({ filePath: envPath });
|
|
319
|
-
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
571
|
+
// NOTE: the daemon kickstart + smoke deliberately does NOT happen here.
|
|
572
|
+
// It runs in `runOnboard` AFTER the opt-in writes (label/enrichment/
|
|
573
|
+
// capture) land in config.json, so the respawned daemon boots with the
|
|
574
|
+
// final config. The daemon builds its capture loop and metadata extractor
|
|
575
|
+
// once at boot (src/main.ts), so kickstarting before those writes would
|
|
576
|
+
// leave `--enable-capture` / `--enable-codex-enrichment` inert until a
|
|
577
|
+
// later restart.
|
|
327
578
|
return validation.last4;
|
|
328
579
|
}
|
|
329
|
-
|
|
330
|
-
|
|
580
|
+
/**
|
|
581
|
+
* Resolve the Voyage key, or `""` to mean "finish without a key" (keyword-only
|
|
582
|
+
* lexical mode). The key is no longer mandatory: a friend can import and search
|
|
583
|
+
* first, then add a key later for semantic ranking. Returns `""` on explicit
|
|
584
|
+
* opt-out (`--no-voyage-key`), on `--yes` with no key available, or when the
|
|
585
|
+
* interactive prompt is left blank.
|
|
586
|
+
*/
|
|
587
|
+
export async function collectVoyageKey(opts, rl) {
|
|
588
|
+
if (opts.voyageKey === false || opts.noVoyageKey === true)
|
|
589
|
+
return "";
|
|
590
|
+
if (typeof opts.voyageKey === "string" && opts.voyageKey.trim()) {
|
|
331
591
|
return opts.voyageKey.trim();
|
|
592
|
+
}
|
|
332
593
|
if (process.env["VOYAGE_API_KEY"])
|
|
333
594
|
return process.env["VOYAGE_API_KEY"].trim();
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
595
|
+
// Non-interactive with no key supplied: finish in lexical mode rather than
|
|
596
|
+
// fail. The friend can run `rift onboard` again with a key anytime.
|
|
597
|
+
if (opts.yes)
|
|
598
|
+
return "";
|
|
599
|
+
// Explain the key BEFORE asking for it — a beta user should know what
|
|
600
|
+
// they are pasting and why, not be confronted with a bare prompt.
|
|
601
|
+
ui.detail([
|
|
602
|
+
"Search index key (optional)",
|
|
603
|
+
"• What: paste the Voyage key Clem sent you (it won't echo as you type).",
|
|
604
|
+
"• Why: Voyage adds meaning-based (semantic) search on top of keyword search.",
|
|
605
|
+
"• Skip: press Enter to finish now with keyword search — add a key later anytime.",
|
|
606
|
+
"• Privacy: the key is stored locally and only sent to Voyage when Rift calls Voyage; never sent to Clem.",
|
|
607
|
+
].join("\n"));
|
|
608
|
+
const answer = (await ask(rl, "Paste your Voyage API key (or press Enter to skip): ")).trim();
|
|
609
|
+
return answer; // "" → finish in keyword-only mode
|
|
342
610
|
}
|
|
343
611
|
/**
|
|
344
612
|
* Kickstart, wait for /health, confirm the respawned daemon process has
|
|
@@ -351,54 +619,126 @@ async function collectVoyageKey(opts, rl) {
|
|
|
351
619
|
async function daemonRefreshFlow(globalOpts) {
|
|
352
620
|
const baseUrl = safeResolveBaseUrl(globalOpts.config);
|
|
353
621
|
if (!baseUrl) {
|
|
354
|
-
|
|
622
|
+
ui.step("skip", "Rift service", "not configured yet — bootstrap via install.sh, then re-run");
|
|
355
623
|
return { ok: false, reason: "daemon not configured", kind: "not_configured" };
|
|
356
624
|
}
|
|
625
|
+
const daemonSpin = new ui.Spinner("Rift service").start();
|
|
357
626
|
const kick = await kickstartDaemon();
|
|
358
627
|
if (kick.status === "agent_not_loaded") {
|
|
359
|
-
|
|
628
|
+
daemonSpin.fail("Rift service", "launchd agent not loaded");
|
|
629
|
+
ui.note(kick.hint);
|
|
360
630
|
return { ok: false, reason: kick.hint ?? "agent not loaded", kind: "agent_not_loaded" };
|
|
361
631
|
}
|
|
362
632
|
if (kick.status === "failed") {
|
|
633
|
+
daemonSpin.fail("Rift service", "kickstart failed");
|
|
363
634
|
return {
|
|
364
635
|
ok: false,
|
|
365
636
|
reason: kick.hint ?? "kickstart failed",
|
|
366
637
|
kind: "kickstart_failed",
|
|
367
638
|
};
|
|
368
639
|
}
|
|
369
|
-
say("Daemon kickstarted.");
|
|
370
640
|
const health = await waitForHealth({ baseUrl });
|
|
371
641
|
if (!health.ok) {
|
|
642
|
+
daemonSpin.fail("Rift service", "health check failed");
|
|
372
643
|
return { ok: false, reason: health.reason, kind: "health_failed" };
|
|
373
644
|
}
|
|
374
|
-
say(`Daemon healthy (uptime ${health.uptimeSeconds}s).`);
|
|
375
645
|
if (!health.voyageKeyPresent) {
|
|
646
|
+
daemonSpin.fail("Rift service", "voyage_key_present=false after kickstart");
|
|
376
647
|
return {
|
|
377
648
|
ok: false,
|
|
378
649
|
reason: "Daemon /health reports voyage_key_present=false after kickstart.",
|
|
379
650
|
kind: "smoke_failed",
|
|
380
651
|
};
|
|
381
652
|
}
|
|
382
|
-
|
|
653
|
+
daemonSpin.succeed("Rift service", `healthy · daemon · uptime ${health.uptimeSeconds}s`);
|
|
383
654
|
return { ok: true, kickstarted: true };
|
|
384
655
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
656
|
+
export function classifyTokenFailure(err) {
|
|
657
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
658
|
+
const lower = message.toLowerCase();
|
|
659
|
+
// A-1.2: the macOS Keychain prints the same string whether the lock
|
|
660
|
+
// hits a read or a write. Catch on substring; don't try to inspect
|
|
661
|
+
// SecKeychain error codes from JS.
|
|
662
|
+
//
|
|
663
|
+
// Two shapes can land here:
|
|
664
|
+
// 1. The raw `security` error from a write path (`issueToken`),
|
|
665
|
+
// whose message contains "User interaction is not allowed."
|
|
666
|
+
// 2. The friendly `CliError` shape that `readToken` now throws
|
|
667
|
+
// (post-beta.7) in place of the raw error. Its message starts
|
|
668
|
+
// with "Login Keychain is locked" and embeds the
|
|
669
|
+
// `security unlock-keychain` recovery command verbatim.
|
|
670
|
+
// Both must classify as `keychain_locked` so the typed
|
|
671
|
+
// recovery branch in `runFirstCapturePass` fires correctly.
|
|
672
|
+
if (lower.includes("user interaction is not allowed") ||
|
|
673
|
+
lower.includes("login keychain is locked") ||
|
|
674
|
+
lower.includes("security unlock-keychain")) {
|
|
675
|
+
return { reason: "keychain_locked", message };
|
|
676
|
+
}
|
|
677
|
+
return { reason: "other", message };
|
|
678
|
+
}
|
|
679
|
+
export async function ensureToken(configPath) {
|
|
680
|
+
// A-1.2: readToken is inside the try — a locked-keychain *read* must
|
|
681
|
+
// be classified the same as a locked-keychain *write*.
|
|
682
|
+
try {
|
|
683
|
+
const existing = await readToken();
|
|
684
|
+
if (existing)
|
|
685
|
+
return { ok: true, token: existing, source: "existing" };
|
|
686
|
+
}
|
|
687
|
+
catch (err) {
|
|
688
|
+
const { reason, message } = classifyTokenFailure(err);
|
|
689
|
+
return { ok: false, reason, message, hint: describeTokenFailure(message) };
|
|
690
|
+
}
|
|
390
691
|
try {
|
|
391
692
|
const { token } = await issueToken(path.resolve(configPath));
|
|
392
693
|
// Mirror to ~/.rift/token so subsequent CLI calls don't depend on Keychain.
|
|
393
694
|
const tokenPath = path.join(os.homedir(), ".rift", "token");
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
695
|
+
try {
|
|
696
|
+
fs.mkdirSync(path.dirname(tokenPath), { recursive: true });
|
|
697
|
+
fs.writeFileSync(tokenPath, token + "\n", {
|
|
698
|
+
encoding: "utf8",
|
|
699
|
+
mode: 0o600,
|
|
700
|
+
});
|
|
701
|
+
fs.chmodSync(tokenPath, 0o600);
|
|
702
|
+
}
|
|
703
|
+
catch (writeErr) {
|
|
704
|
+
const reason = writeErr instanceof Error ? writeErr.message : String(writeErr);
|
|
705
|
+
say(`Token issued but mirror to ${tokenPath} failed: ${reason}. Subsequent CLI calls will rely on Keychain only.`);
|
|
706
|
+
}
|
|
707
|
+
return { ok: true, token, source: "issued" };
|
|
398
708
|
}
|
|
399
|
-
catch {
|
|
400
|
-
|
|
709
|
+
catch (err) {
|
|
710
|
+
const { reason, message } = classifyTokenFailure(err);
|
|
711
|
+
return { ok: false, reason, message, hint: describeTokenFailure(message) };
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
export function describeTokenFailure(reason) {
|
|
715
|
+
const lower = reason.toLowerCase();
|
|
716
|
+
// Two SSH-locked shapes can land here — check both before the generic
|
|
717
|
+
// Keychain branch so the GUI-only "Keychain Access → File → Unlock"
|
|
718
|
+
// copy never replaces the SSH-specific `security unlock-keychain`
|
|
719
|
+
// command: (1) the raw `security` error from the write path, and
|
|
720
|
+
// (2) the friendly CliError that readToken now emits post-beta.7,
|
|
721
|
+
// whose message starts with "Login Keychain is locked" and embeds
|
|
722
|
+
// the unlock command verbatim.
|
|
723
|
+
if (lower.includes("user interaction is not allowed") ||
|
|
724
|
+
lower.includes("login keychain is locked") ||
|
|
725
|
+
lower.includes("security unlock-keychain")) {
|
|
726
|
+
return "the login Keychain is locked (typical over SSH). Run `security unlock-keychain ~/Library/Keychains/login.keychain-db` (it will prompt for your Mac login password), then re-run `rift onboard`. For local Terminal installs this should not happen.";
|
|
727
|
+
}
|
|
728
|
+
if (lower.includes("keychain") || lower.includes("security:")) {
|
|
729
|
+
return "unlock the login Keychain (Keychain Access → File → Unlock) and re-run, or run from a Terminal session with GUI access (not over SSH).";
|
|
730
|
+
}
|
|
731
|
+
if (lower.includes("eacces") || lower.includes("permission denied")) {
|
|
732
|
+
return "check that `~/.rift/` is writable by your user, then re-run `rift onboard`.";
|
|
733
|
+
}
|
|
734
|
+
if (lower.includes("enoent") ||
|
|
735
|
+
lower.includes("no such file") ||
|
|
736
|
+
lower.includes("daemon") ||
|
|
737
|
+
lower.includes("pid file") ||
|
|
738
|
+
lower.includes("not running")) {
|
|
739
|
+
return "start the daemon with `launchctl kickstart -k gui/$UID/com.getrift.daemon`, then re-run `rift onboard`.";
|
|
401
740
|
}
|
|
741
|
+
return "re-run `rift onboard`. If the failure repeats, run `rift feedback --kind=broke --with-status` to ship a diagnostic bundle.";
|
|
402
742
|
}
|
|
403
743
|
// ----- Step 3: Codex preflight -----
|
|
404
744
|
async function codexPreflight() {
|
|
@@ -413,33 +753,125 @@ async function codexPreflight() {
|
|
|
413
753
|
return false;
|
|
414
754
|
}
|
|
415
755
|
}
|
|
416
|
-
|
|
417
|
-
|
|
756
|
+
/**
|
|
757
|
+
* Conservative email check — enough to catch obvious typos at the prompt;
|
|
758
|
+
* the relay endpoint can re-validate. Returns the normalized (trimmed,
|
|
759
|
+
* lowercased) address, or undefined if it doesn't look like an email.
|
|
760
|
+
*/
|
|
761
|
+
export function normalizeEmail(raw) {
|
|
762
|
+
const s = raw.trim().toLowerCase();
|
|
763
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s) ? s : undefined;
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Bundled "stay connected" opt-in. One yes/no that, on yes, collects an
|
|
767
|
+
* email used for product news, pricing, a leaving-beta heads-up, and a
|
|
768
|
+
* feedback channel. Opt-in only: `--yes` alone never subscribes (consent
|
|
769
|
+
* must be active), `--no-feedback-relay` declines, and `--email` subscribes
|
|
770
|
+
* non-interactively. The relay URL stays operator-only infra; when none is
|
|
771
|
+
* configured the email is captured locally (sidecar + beta-signups.jsonl)
|
|
772
|
+
* and reaches the operator once an endpoint is wired.
|
|
773
|
+
*/
|
|
774
|
+
export async function collectFeedbackPreference(opts, dataDir, rl, store = macRelaySecretStore) {
|
|
775
|
+
const declined = isOff(opts, "feedbackRelay", "noFeedbackRelay");
|
|
776
|
+
// Relay URL + signing secret come from EITHER an invite code (preferred:
|
|
777
|
+
// nothing to paste by hand) OR the back-compat operator flags. The secret
|
|
778
|
+
// never lands in the sidecar — it goes to the Keychain below.
|
|
418
779
|
let url;
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
780
|
+
let secret;
|
|
781
|
+
if (!declined) {
|
|
782
|
+
if (opts.invite) {
|
|
783
|
+
const invite = parseInvite(opts.invite); // throws a friendly CliError if bad
|
|
784
|
+
url = invite.url;
|
|
785
|
+
secret = invite.secret;
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
url = opts.enableFeedbackRelay;
|
|
789
|
+
secret = url ? opts.relaySecret : undefined;
|
|
790
|
+
}
|
|
422
791
|
}
|
|
423
|
-
|
|
424
|
-
|
|
792
|
+
// Friend-grade interactive path: when no relay flags were passed, offer to
|
|
793
|
+
// paste an invite so the bearer code never lands in shell history. Skipped
|
|
794
|
+
// off a TTY and under --yes (automation). A bad paste warns and skips — it
|
|
795
|
+
// never aborts onboarding.
|
|
796
|
+
if (!declined &&
|
|
797
|
+
!url &&
|
|
798
|
+
!opts.invite &&
|
|
799
|
+
!opts.enableFeedbackRelay &&
|
|
800
|
+
!opts.yes &&
|
|
801
|
+
process.stdin.isTTY) {
|
|
802
|
+
// Most friends will NOT have an invite code — make that the obvious,
|
|
803
|
+
// pressure-free default so the bare prompt doesn't read as "something is
|
|
804
|
+
// missing". An invite only turns on the direct feedback relay; the email
|
|
805
|
+
// ask below (and `rift feedback`) work without it.
|
|
806
|
+
ui.detail([
|
|
807
|
+
"Feedback channel (optional)",
|
|
808
|
+
"• Have an invite code from Clem? Paste it to turn on the direct feedback relay.",
|
|
809
|
+
"• No code? Just press Enter — this is optional, and most people skip it.",
|
|
810
|
+
"• Either way you can send feedback any time with `rift feedback`.",
|
|
811
|
+
].join("\n"));
|
|
812
|
+
const pasted = (await ask(rl, "Invite code from Clem (or press Enter to skip): ")).trim();
|
|
813
|
+
if (pasted) {
|
|
814
|
+
try {
|
|
815
|
+
const invite = parseInvite(pasted);
|
|
816
|
+
url = invite.url;
|
|
817
|
+
secret = invite.secret;
|
|
818
|
+
}
|
|
819
|
+
catch {
|
|
820
|
+
say(" That invite code is not valid — skipping feedback setup.");
|
|
821
|
+
}
|
|
822
|
+
}
|
|
425
823
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
824
|
+
// Configuring a URL without a signing secret leaves the install "looks
|
|
825
|
+
// configured, silently dead" — the receiver requires a valid signature.
|
|
826
|
+
// An invite always carries one; the flag path must pair a secret or
|
|
827
|
+
// knowingly opt into unsigned mode (dev/local receivers only).
|
|
828
|
+
if (url && !secret && !opts.allowUnsignedFeedbackRelay) {
|
|
829
|
+
throw new CliError("Relay needs a signing secret. Use an invite (rift onboard --invite <code>), " +
|
|
830
|
+
"or pair --enable-feedback-relay with --relay-secret " +
|
|
831
|
+
"(or --allow-unsigned-feedback-relay for a dev receiver).", "validation");
|
|
832
|
+
}
|
|
833
|
+
// Secure the secret BEFORE enabling relay: a write failure must not leave an
|
|
834
|
+
// enabled config that can't sign. (3a's `rift feedback setup` does the same.)
|
|
835
|
+
if (secret) {
|
|
836
|
+
try {
|
|
837
|
+
await store.write(secret);
|
|
838
|
+
}
|
|
839
|
+
catch (err) {
|
|
840
|
+
if (err instanceof LockedKeychainError) {
|
|
841
|
+
throw new CliError("Your macOS Keychain is locked. Unlock it and retry: " +
|
|
842
|
+
"security unlock-keychain ~/Library/Keychains/login.keychain-db", "validation");
|
|
437
843
|
}
|
|
844
|
+
throw err;
|
|
438
845
|
}
|
|
439
846
|
}
|
|
440
|
-
|
|
441
|
-
|
|
847
|
+
let email;
|
|
848
|
+
if (!declined) {
|
|
849
|
+
if (opts.email) {
|
|
850
|
+
email = normalizeEmail(opts.email);
|
|
851
|
+
if (!email) {
|
|
852
|
+
throw new CliError(`--email "${opts.email}" is not a valid email address.`, "validation");
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
else if (!opts.yes && process.stdin.isTTY) {
|
|
856
|
+
email = await promptStayConnected(rl);
|
|
857
|
+
}
|
|
858
|
+
// --yes / non-TTY with no --email: do not auto-subscribe.
|
|
859
|
+
}
|
|
860
|
+
const optedIn = email !== undefined || url !== undefined;
|
|
861
|
+
const cfg = optedIn
|
|
862
|
+
? {
|
|
863
|
+
enabled: true,
|
|
864
|
+
installation_id: crypto.randomUUID(),
|
|
865
|
+
...(email ? { email } : {}),
|
|
866
|
+
...(url ? { url } : {}),
|
|
867
|
+
// url && !secret only happens under --allow-unsigned-feedback-relay
|
|
868
|
+
// (the guard above throws otherwise): mark it an intentional unsigned
|
|
869
|
+
// relay so maybeRelay may POST without a secret for THIS install only.
|
|
870
|
+
...(url && !secret ? { unsigned: true } : {}),
|
|
871
|
+
}
|
|
442
872
|
: { enabled: false };
|
|
873
|
+
// The sidecar holds only non-secret fields; the secret is already in the
|
|
874
|
+
// Keychain (above).
|
|
443
875
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
444
876
|
const sidecar = path.join(dataDir, "feedback-config.json");
|
|
445
877
|
fs.writeFileSync(sidecar, JSON.stringify(cfg, null, 2) + "\n", {
|
|
@@ -447,8 +879,91 @@ async function collectFeedbackPreference(opts, rl, dataDir) {
|
|
|
447
879
|
mode: 0o600,
|
|
448
880
|
});
|
|
449
881
|
fs.chmodSync(sidecar, 0o600);
|
|
882
|
+
if (cfg.email && cfg.installation_id) {
|
|
883
|
+
// Sign the signup POST with the in-hand secret (no Keychain round-trip).
|
|
884
|
+
await emitBetaSignup(cfg.email, cfg.installation_id, cfg.url, secret, dataDir);
|
|
885
|
+
}
|
|
450
886
|
return cfg;
|
|
451
887
|
}
|
|
888
|
+
/**
|
|
889
|
+
* Interactive bundled opt-in. Returns the chosen email, or undefined if the
|
|
890
|
+
* user declines or skips. Re-prompts up to 3× on a malformed address; an
|
|
891
|
+
* empty line at any point means "skip".
|
|
892
|
+
*/
|
|
893
|
+
async function promptStayConnected(rl) {
|
|
894
|
+
ui.detail([
|
|
895
|
+
"Stay in the loop (optional)",
|
|
896
|
+
"• Get product news, pricing once we set it, and a heads-up when Rift leaves beta.",
|
|
897
|
+
"• Gives you a direct line to send feedback any time via `rift feedback`.",
|
|
898
|
+
"• Opt-in. Stored locally, shared only with Clem — never sold, never spammed.",
|
|
899
|
+
].join("\n"));
|
|
900
|
+
const yn = (await ask(rl, "Share your email to stay connected? [y/N] ")).trim().toLowerCase();
|
|
901
|
+
if (yn !== "y" && yn !== "yes")
|
|
902
|
+
return undefined;
|
|
903
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
904
|
+
const raw = (await ask(rl, "Your email (Enter to skip): ")).trim();
|
|
905
|
+
if (raw.length === 0)
|
|
906
|
+
return undefined;
|
|
907
|
+
const norm = normalizeEmail(raw);
|
|
908
|
+
if (norm)
|
|
909
|
+
return norm;
|
|
910
|
+
say(" That doesn't look like an email address — try again, or press Enter to skip.");
|
|
911
|
+
}
|
|
912
|
+
return undefined;
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Record a one-time beta signup. The local JSONL row is canonical (never
|
|
916
|
+
* lost); the relay POST is best-effort and only fires when an endpoint URL
|
|
917
|
+
* is configured. Failures are swallowed — onboarding must never break on a
|
|
918
|
+
* signup hiccup.
|
|
919
|
+
*/
|
|
920
|
+
async function emitBetaSignup(email, installationId, url, hmacSecret, dataDir) {
|
|
921
|
+
const build = getBuildInfo();
|
|
922
|
+
const payload = {
|
|
923
|
+
ts: new Date().toISOString(),
|
|
924
|
+
event: "beta_signup",
|
|
925
|
+
email,
|
|
926
|
+
installation_id: installationId,
|
|
927
|
+
version: build.version,
|
|
928
|
+
commit: build.commit,
|
|
929
|
+
node: build.node,
|
|
930
|
+
};
|
|
931
|
+
// Audit invariant: every byte that reaches Clem must exist locally first.
|
|
932
|
+
// If the canonical row can't be written, we must NOT relay — otherwise a
|
|
933
|
+
// signup could reach Clem with no local record to audit against.
|
|
934
|
+
let wroteLocal = false;
|
|
935
|
+
try {
|
|
936
|
+
const dir = path.join(dataDir, "observability");
|
|
937
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
938
|
+
const file = path.join(dir, "beta-signups.jsonl");
|
|
939
|
+
fs.appendFileSync(file, `${JSON.stringify(payload)}\n`, {
|
|
940
|
+
encoding: "utf8",
|
|
941
|
+
mode: 0o600,
|
|
942
|
+
});
|
|
943
|
+
wroteLocal = true; // the canonical row is on disk; chmod below is cosmetic
|
|
944
|
+
try {
|
|
945
|
+
fs.chmodSync(file, 0o600);
|
|
946
|
+
}
|
|
947
|
+
catch {
|
|
948
|
+
// best-effort permission fix — does not affect the audit invariant
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
catch {
|
|
952
|
+
// Local write failed — don't break onboarding over a signup row, and
|
|
953
|
+
// (below) don't relay something we couldn't record locally.
|
|
954
|
+
}
|
|
955
|
+
if (url && wroteLocal) {
|
|
956
|
+
try {
|
|
957
|
+
await postFeedback(payload, {
|
|
958
|
+
url,
|
|
959
|
+
...(hmacSecret ? { hmacSecret } : {}),
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
catch {
|
|
963
|
+
// Best-effort; the canonical row is already on disk.
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
452
967
|
// ----- Step 5b: Claude Code policy hook (opt-in) -----
|
|
453
968
|
/**
|
|
454
969
|
* Offer to install the Rift policy hook into Claude Code's settings.
|
|
@@ -463,7 +978,7 @@ async function collectFeedbackPreference(opts, rl, dataDir) {
|
|
|
463
978
|
* guardrail, not a correctness requirement.
|
|
464
979
|
*/
|
|
465
980
|
async function maybeInstallClaudeCodeHook(opts, rl) {
|
|
466
|
-
if (opts
|
|
981
|
+
if (isOff(opts, "claudeHook", "noClaudeHook")) {
|
|
467
982
|
return;
|
|
468
983
|
}
|
|
469
984
|
const homeDir = os.homedir();
|
|
@@ -478,20 +993,22 @@ async function maybeInstallClaudeCodeHook(opts, rl) {
|
|
|
478
993
|
}
|
|
479
994
|
else if (opts.yes) {
|
|
480
995
|
// Non-interactive default: skip. Operator must opt in with --with-claude-hook.
|
|
481
|
-
|
|
996
|
+
ui.step("skip", "Claude Code", "skipped (pass --with-claude-hook to install non-interactively)");
|
|
482
997
|
return;
|
|
483
998
|
}
|
|
484
999
|
else {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
1000
|
+
ui.note([
|
|
1001
|
+
"Claude Code detected.",
|
|
1002
|
+
"Optional: install a PreToolUse policy hook so Rift retrieval starts with rift_context_pack",
|
|
1003
|
+
"(prevents broad rift_search dumps from burning context). Disable any time with RIFT_POLICY_DISABLED=1.",
|
|
1004
|
+
].join("\n"));
|
|
1005
|
+
const answer = (await ask(rl, " Install Claude Code policy hook? [y/N]: ", "n"))
|
|
489
1006
|
.trim()
|
|
490
1007
|
.toLowerCase();
|
|
491
1008
|
install = answer === "y" || answer === "yes";
|
|
492
1009
|
}
|
|
493
1010
|
if (!install) {
|
|
494
|
-
|
|
1011
|
+
ui.step("skip", "Claude Code", "not installed");
|
|
495
1012
|
return;
|
|
496
1013
|
}
|
|
497
1014
|
try {
|
|
@@ -501,25 +1018,25 @@ async function maybeInstallClaudeCodeHook(opts, rl) {
|
|
|
501
1018
|
dryRun: false,
|
|
502
1019
|
});
|
|
503
1020
|
if (outcome.hookEntryAdded) {
|
|
504
|
-
|
|
1021
|
+
ui.step("ok", "Claude Code", "connected · auto-recall hook added");
|
|
505
1022
|
}
|
|
506
1023
|
else if (outcome.hookEntryReplaced || outcome.scriptUpdated) {
|
|
507
|
-
|
|
1024
|
+
ui.step("ok", "Claude Code", "updated");
|
|
508
1025
|
}
|
|
509
1026
|
else {
|
|
510
|
-
|
|
1027
|
+
ui.step("ok", "Claude Code", "already up-to-date");
|
|
511
1028
|
}
|
|
512
1029
|
if (outcome.backupId) {
|
|
513
|
-
|
|
1030
|
+
ui.note(`backup ID: ${outcome.backupId}`);
|
|
514
1031
|
}
|
|
515
|
-
|
|
1032
|
+
ui.note("(Restart Claude Code or open /hooks once for the new entry to load.)");
|
|
516
1033
|
}
|
|
517
1034
|
catch (err) {
|
|
518
1035
|
if (err instanceof HooksParseFailedError) {
|
|
519
|
-
|
|
1036
|
+
ui.step("fail", "Claude Code", err.message);
|
|
520
1037
|
}
|
|
521
1038
|
else {
|
|
522
|
-
|
|
1039
|
+
ui.step("fail", "Claude Code", `install failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
523
1040
|
}
|
|
524
1041
|
}
|
|
525
1042
|
}
|
|
@@ -527,26 +1044,59 @@ async function maybeInstallClaudeCodeHook(opts, rl) {
|
|
|
527
1044
|
export async function runFirstCapturePass(configPath, dataDir) {
|
|
528
1045
|
const baseUrl = safeResolveBaseUrl(configPath);
|
|
529
1046
|
if (!baseUrl) {
|
|
530
|
-
|
|
1047
|
+
ui.step("skip", "Chat import", "daemon not reachable — skipped");
|
|
531
1048
|
return { saved: 0, reviewed: 0 };
|
|
532
1049
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
1050
|
+
// Per A-1.1/C-1.3: token failure here is fatal. The wizard cannot
|
|
1051
|
+
// proceed without a token (capture, import, recall all need auth).
|
|
1052
|
+
// The previous "skipping" path silently broke onboarding.
|
|
1053
|
+
const tokenResult = await ensureToken(configPath);
|
|
1054
|
+
if (!tokenResult.ok) {
|
|
1055
|
+
say("");
|
|
1056
|
+
if (tokenResult.reason === "keychain_locked") {
|
|
1057
|
+
say("Could not issue an auth token: the macOS login keychain is locked.");
|
|
1058
|
+
say("Run this, then re-run `rift onboard`:");
|
|
1059
|
+
say(" security unlock-keychain ~/Library/Keychains/login.keychain-db");
|
|
1060
|
+
say("Note: the wizard will restart from the top. If your Voyage key is in");
|
|
1061
|
+
say(" VOYAGE_API_KEY it won't re-prompt; otherwise you'll re-paste it.");
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
// Per C-1.4 (round-6 fix): the "other" branch must spell out the
|
|
1065
|
+
// manual recovery so the user knows the exit path is `rift token
|
|
1066
|
+
// issue` then re-running onboard. Generic hints alone aren't
|
|
1067
|
+
// enough.
|
|
1068
|
+
say(`Could not issue an auth token: ${tokenResult.message}`);
|
|
1069
|
+
say(`Recover: ${tokenResult.hint}`);
|
|
1070
|
+
say("If the failure persists, run `rift token issue` manually, then re-run `rift onboard`.");
|
|
1071
|
+
}
|
|
1072
|
+
process.exitCode = 1;
|
|
1073
|
+
return { saved: 0, reviewed: 0, fatal: true };
|
|
537
1074
|
}
|
|
1075
|
+
const token = tokenResult.token;
|
|
538
1076
|
const client = createHttpClient({ baseUrl, token });
|
|
539
1077
|
let codexCaptureDeps = {};
|
|
1078
|
+
let enabledSources = DEFAULT_AUTO_CAPTURE_SOURCES;
|
|
540
1079
|
try {
|
|
541
1080
|
const { loadConfig } = await import("../../config/loader.js");
|
|
542
|
-
|
|
1081
|
+
const cfg = loadConfig(configPath);
|
|
1082
|
+
codexCaptureDeps = resolveCodexCaptureDeps(cfg);
|
|
1083
|
+
enabledSources = cfg.capture.sources;
|
|
543
1084
|
}
|
|
544
1085
|
catch {
|
|
545
1086
|
codexCaptureDeps = {};
|
|
546
1087
|
}
|
|
1088
|
+
const startedAt = new Date();
|
|
1089
|
+
const captureSpin = new ui.Spinner("Chat import").start();
|
|
547
1090
|
try {
|
|
548
1091
|
const report = await runAutoCapture({
|
|
549
1092
|
dataDir,
|
|
1093
|
+
// Capture exactly the sources config enables — the same set the
|
|
1094
|
+
// scheduled daemon loop uses (buildDaemonAutoCaptureDeps passes
|
|
1095
|
+
// cfg.capture.sources) and the same set recorded in the ledger row
|
|
1096
|
+
// below. Omitting this falls back to DEFAULT_AUTO_CAPTURE_SOURCES, which
|
|
1097
|
+
// would let onboarding capture sources it never records — a status
|
|
1098
|
+
// ledger that says one thing while capture did another.
|
|
1099
|
+
sources: enabledSources,
|
|
550
1100
|
...codexCaptureDeps,
|
|
551
1101
|
saveFn: async (saveOpts) => {
|
|
552
1102
|
const { data } = await client.post("/save", {
|
|
@@ -569,21 +1119,82 @@ export async function runFirstCapturePass(configPath, dataDir) {
|
|
|
569
1119
|
}
|
|
570
1120
|
},
|
|
571
1121
|
});
|
|
572
|
-
|
|
1122
|
+
// Record this onboard run in the health ledger (best-effort). Without
|
|
1123
|
+
// it, a friend who finishes onboarding and immediately runs `rift
|
|
1124
|
+
// status`/`rift doctor` sees "no runs yet" — the exact stale-health
|
|
1125
|
+
// trust break this ledger exists to prevent, at the moment it matters
|
|
1126
|
+
// most. Tagged `onboard` so it sets freshness without ever speaking for
|
|
1127
|
+
// the scheduled daemon loop.
|
|
1128
|
+
try {
|
|
1129
|
+
await appendAutoCaptureRunRecord(dataDir, buildAutoCaptureRunRecord({
|
|
1130
|
+
timestamp: startedAt.toISOString(),
|
|
1131
|
+
enabledSources,
|
|
1132
|
+
durationMs: Date.now() - startedAt.getTime(),
|
|
1133
|
+
report,
|
|
1134
|
+
trigger: "onboard",
|
|
1135
|
+
}));
|
|
1136
|
+
}
|
|
1137
|
+
catch (observabilityErr) {
|
|
1138
|
+
process.stderr.write(`Onboard capture run-record append failed: ${observabilityErr instanceof Error
|
|
1139
|
+
? observabilityErr.message
|
|
1140
|
+
: String(observabilityErr)}\n`);
|
|
1141
|
+
}
|
|
1142
|
+
const base = `${report.saved} added · ${report.review} to review`;
|
|
1143
|
+
// A preflight failure (Codex health probe) carries 0 per-session errors,
|
|
1144
|
+
// so an `errors > 0` check alone would `succeed()` on a run where triage
|
|
1145
|
+
// never ran — telling a friend everything's fine when it isn't. Treat a
|
|
1146
|
+
// failed probe as a warning too, and surface why.
|
|
1147
|
+
if (!report.preflight_ok) {
|
|
1148
|
+
captureSpin.warn("Chat import", `${base} · Codex preflight failed${report.preflight_error ? ` (${report.preflight_error})` : ""}`);
|
|
1149
|
+
}
|
|
1150
|
+
else if (report.errors > 0) {
|
|
1151
|
+
captureSpin.warn("Chat import", `${base} · ${report.errors} error(s)`);
|
|
1152
|
+
}
|
|
1153
|
+
else {
|
|
1154
|
+
captureSpin.succeed("Chat import", base);
|
|
1155
|
+
}
|
|
573
1156
|
return { saved: report.saved, reviewed: report.review };
|
|
574
1157
|
}
|
|
575
1158
|
catch (err) {
|
|
576
|
-
|
|
1159
|
+
captureSpin.fail("Chat import", `error (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
577
1160
|
return { saved: 0, reviewed: 0 };
|
|
578
1161
|
}
|
|
579
1162
|
}
|
|
580
1163
|
// ----- Step 8: import -----
|
|
581
|
-
async function collectImportPath(opts, rl) {
|
|
582
|
-
|
|
1164
|
+
async function collectImportPath(opts, rl, capture) {
|
|
1165
|
+
// opts.importExport is `string | false | undefined` — Commander writes
|
|
1166
|
+
// `false` when `--no-import-export` was passed, but the caller already
|
|
1167
|
+
// short-circuits in that case. Only a non-empty string is a real path.
|
|
1168
|
+
if (typeof opts.importExport === "string" && opts.importExport.length > 0) {
|
|
583
1169
|
return opts.importExport;
|
|
1170
|
+
}
|
|
584
1171
|
if (opts.yes)
|
|
585
1172
|
return null;
|
|
586
|
-
|
|
1173
|
+
// Explain what a path is, where to find the export, why it's optional,
|
|
1174
|
+
// and that ChatGPT is the only inline source — BEFORE the prompt. A beta
|
|
1175
|
+
// user who has never exported a chat archive shouldn't hit a bare
|
|
1176
|
+
// "Drop a path" prompt. The source-sniffer below still fail-fasts a
|
|
1177
|
+
// non-ChatGPT export, but the friend learns the boundary earlier here.
|
|
1178
|
+
//
|
|
1179
|
+
// The "skipping is fine" reassurance MUST match what will actually happen.
|
|
1180
|
+
// Auto-capture is OFF by default, AND can be configured-on-but-not-running
|
|
1181
|
+
// when the Codex preflight failed — promising "new chats get captured
|
|
1182
|
+
// automatically" in either case is the exact trust break this flow removes.
|
|
1183
|
+
// Only promise it when capture will genuinely run; otherwise point at the
|
|
1184
|
+
// real next step (fix Codex if configured, or enable it if off).
|
|
1185
|
+
const whySkip = capture.willRun
|
|
1186
|
+
? "skipping is fine — new chats get captured automatically."
|
|
1187
|
+
: capture.configured
|
|
1188
|
+
? "skipping is fine — import later with `rift import` (auto-capture is on but won't run until Codex is ready — see above)."
|
|
1189
|
+
: "skipping is fine — import later with `rift import`, or turn on auto-capture with `rift onboard --enable-capture`.";
|
|
1190
|
+
ui.detail([
|
|
1191
|
+
"Import past chats (optional)",
|
|
1192
|
+
"• What: the file path to a ChatGPT data export (a .zip or .json on your Mac), e.g. ~/Downloads/chatgpt-export.zip.",
|
|
1193
|
+
"• Where: chatgpt.com → Settings → Data controls → Export data, then unzip nothing — just drop the downloaded file's path here.",
|
|
1194
|
+
`• Why: this seeds Rift with your existing ChatGPT history so search works on day one; ${whySkip}`,
|
|
1195
|
+
"• Other tools: Claude/Grok/Gemini exports aren't imported here. Run them later with: rift import <path> --source claude_web|grok_web|gemini_web",
|
|
1196
|
+
].join("\n"));
|
|
1197
|
+
const answer = (await ask(rl, "Path to a ChatGPT export to import now (or `skip`): ", "skip")).trim();
|
|
587
1198
|
if (!answer || answer.toLowerCase() === "skip")
|
|
588
1199
|
return null;
|
|
589
1200
|
return answer;
|
|
@@ -606,18 +1217,27 @@ async function runImport(filePath, configPath) {
|
|
|
606
1217
|
return { kind: "skipped", reason: "unsupported extension" };
|
|
607
1218
|
}
|
|
608
1219
|
const baseUrl = safeResolveBaseUrl(configPath);
|
|
609
|
-
const
|
|
610
|
-
if (!baseUrl || !
|
|
1220
|
+
const tokenResult = await ensureToken(configPath);
|
|
1221
|
+
if (!baseUrl || !tokenResult.ok) {
|
|
611
1222
|
say("Daemon not reachable — cannot import.");
|
|
612
1223
|
return { kind: "skipped", reason: "daemon not reachable" };
|
|
613
1224
|
}
|
|
614
|
-
const client = createHttpClient({ baseUrl, token });
|
|
1225
|
+
const client = createHttpClient({ baseUrl, token: tokenResult.token });
|
|
615
1226
|
const buf = fs.readFileSync(absolute);
|
|
1227
|
+
// Fail-fast for non-ChatGPT exports: route them to `rift import` with
|
|
1228
|
+
// the right `--source` flag instead of silently feeding them through
|
|
1229
|
+
// the ChatGPT parser. Returns null on "looks like ChatGPT or unknown,"
|
|
1230
|
+
// a concrete provider otherwise.
|
|
1231
|
+
const sniffed = sniffInboxSource(path.basename(absolute), buf);
|
|
1232
|
+
if (sniffed) {
|
|
1233
|
+
say(`Detected a ${sniffed} export — the inline importer is ChatGPT-only.`);
|
|
1234
|
+
say(`Run: rift import "${absolute}" --source ${sniffed}`);
|
|
1235
|
+
return { kind: "skipped", reason: `non-chatgpt export detected: ${sniffed}` };
|
|
1236
|
+
}
|
|
616
1237
|
const form = new FormData();
|
|
617
1238
|
form.append("source", ONBOARD_INLINE_IMPORT_SOURCE);
|
|
618
1239
|
form.append("file", new Blob([buf]), path.basename(absolute));
|
|
619
1240
|
say(`Importing ${path.basename(absolute)} as ${ONBOARD_INLINE_IMPORT_SOURCE}…`);
|
|
620
|
-
say("(For Claude/Grok/Gemini exports, run: rift import <path> --source <name>)");
|
|
621
1241
|
const { data } = await client.postMultipart("/ingest", form);
|
|
622
1242
|
const resp = data;
|
|
623
1243
|
if (resp.duplicate) {
|
|
@@ -634,14 +1254,22 @@ async function runImport(filePath, configPath) {
|
|
|
634
1254
|
return { kind: "imported", jobId: resp.job_id };
|
|
635
1255
|
}
|
|
636
1256
|
/**
|
|
637
|
-
* Pure outcome decision for the next-action card.
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
* user data, not a leftover probe.
|
|
1257
|
+
* Pure outcome decision for the next-action card.
|
|
1258
|
+
*
|
|
1259
|
+
* "Complete + verified" needs both: this run ingested user data AND a recall
|
|
1260
|
+
* query returned a hit (smoke is non-indexing — see daemonRefreshFlow — so a
|
|
1261
|
+
* hit is real user data, not a leftover probe).
|
|
1262
|
+
*
|
|
1263
|
+
* But an EMPTY archive is not a failure. By default capture is OFF (zero-Codex
|
|
1264
|
+
* trust default) and import is optional, so a friend who simply presses Enter
|
|
1265
|
+
* past the import prompt has finished a valid setup — they just have nothing to
|
|
1266
|
+
* recall yet. That case (`ingestAttempted === false`) is reported as DONE, not
|
|
1267
|
+
* "incomplete". We only flag a retry when an ingest was actually attempted and
|
|
1268
|
+
* still added nothing.
|
|
642
1269
|
*/
|
|
643
1270
|
export function decideOnboardOutcome(input) {
|
|
644
1271
|
const { ingestedAny, recall } = input;
|
|
1272
|
+
const ingestAttempted = input.ingestAttempted ?? false;
|
|
645
1273
|
if (ingestedAny && recall.ok && recall.hits > 0) {
|
|
646
1274
|
return [
|
|
647
1275
|
`Setup complete — recall returned ${recall.hits} result(s) from your data.`,
|
|
@@ -649,16 +1277,27 @@ export function decideOnboardOutcome(input) {
|
|
|
649
1277
|
];
|
|
650
1278
|
}
|
|
651
1279
|
if (!ingestedAny) {
|
|
1280
|
+
if (!ingestAttempted) {
|
|
1281
|
+
// Deliberate empty setup — a finished, valid state, not a failure.
|
|
1282
|
+
return [
|
|
1283
|
+
"Setup complete — nothing imported yet.",
|
|
1284
|
+
"Import later: rift import <path> --source <name>",
|
|
1285
|
+
"Next: rift mcp install --client=claude-desktop",
|
|
1286
|
+
];
|
|
1287
|
+
}
|
|
1288
|
+
// An ingest ran but landed nothing — honest, calm retry guidance.
|
|
652
1289
|
return [
|
|
653
|
-
"Setup
|
|
654
|
-
"
|
|
655
|
-
"
|
|
1290
|
+
"Setup ready — but capture/import added nothing this run.",
|
|
1291
|
+
"Try: rift import <path> --source <name>",
|
|
1292
|
+
"If it repeats: rift feedback --kind=broke --with-status",
|
|
656
1293
|
];
|
|
657
1294
|
}
|
|
658
1295
|
if (recall.ok) {
|
|
659
1296
|
return [
|
|
660
|
-
"Setup
|
|
661
|
-
"
|
|
1297
|
+
"Setup ready — capture+import succeeded but the generic recall query returned 0 hits.",
|
|
1298
|
+
"Indexing may still be in flight, or this query just didn't match your archive yet.",
|
|
1299
|
+
"Try: rift search \"<a topic you remember discussing>\"",
|
|
1300
|
+
"If repeated tries still return nothing, run: rift feedback --kind=broke --with-status",
|
|
662
1301
|
];
|
|
663
1302
|
}
|
|
664
1303
|
return [
|
|
@@ -669,28 +1308,44 @@ export function decideOnboardOutcome(input) {
|
|
|
669
1308
|
// ----- Step 9: first-recall verification -----
|
|
670
1309
|
async function firstRecallCheck(configPath) {
|
|
671
1310
|
const baseUrl = safeResolveBaseUrl(configPath);
|
|
672
|
-
const
|
|
673
|
-
if (!baseUrl || !
|
|
1311
|
+
const tokenResult = await ensureToken(configPath);
|
|
1312
|
+
if (!baseUrl || !tokenResult.ok) {
|
|
674
1313
|
return { ok: false, reason: "daemon not reachable" };
|
|
675
1314
|
}
|
|
676
|
-
const client = createHttpClient({ baseUrl, token });
|
|
1315
|
+
const client = createHttpClient({ baseUrl, token: tokenResult.token });
|
|
677
1316
|
// Generic, content-bearing query — likely to land on captured CLI
|
|
678
1317
|
// sessions or imported chat exports without targeting any onboarding
|
|
679
1318
|
// marker. The smoke is non-indexing (see daemonRefreshFlow), so any
|
|
680
1319
|
// hit returned here is real user data, not a leftover probe.
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
1320
|
+
//
|
|
1321
|
+
// Small unusual imports (e.g. one short Claude project export) can
|
|
1322
|
+
// return 0 hits for this generic query even when the data was
|
|
1323
|
+
// indexed correctly. Retry briefly so the post-onboard nudge isn't a
|
|
1324
|
+
// false alarm on slow embedders / sparse archives.
|
|
1325
|
+
const RECALL_RETRY_DELAYS_MS = [0, 5_000];
|
|
1326
|
+
let lastErr = null;
|
|
1327
|
+
for (const delay of RECALL_RETRY_DELAYS_MS) {
|
|
1328
|
+
if (delay > 0)
|
|
1329
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
1330
|
+
try {
|
|
1331
|
+
const { data } = await client.post("/search", {
|
|
1332
|
+
query: "recent conversation",
|
|
1333
|
+
scope: "all",
|
|
1334
|
+
top_k: 10,
|
|
1335
|
+
});
|
|
1336
|
+
const body = data;
|
|
1337
|
+
const hits = (body.results?.length ?? body.hits?.length ?? 0);
|
|
1338
|
+
if (hits > 0)
|
|
1339
|
+
return { ok: true, hits };
|
|
1340
|
+
lastErr = null;
|
|
1341
|
+
}
|
|
1342
|
+
catch (err) {
|
|
1343
|
+
lastErr = err instanceof Error ? err.message : String(err);
|
|
1344
|
+
}
|
|
693
1345
|
}
|
|
1346
|
+
if (lastErr)
|
|
1347
|
+
return { ok: false, reason: lastErr };
|
|
1348
|
+
return { ok: true, hits: 0 };
|
|
694
1349
|
}
|
|
695
1350
|
// ----- Slice 6: --reconfigure-voyage -----
|
|
696
1351
|
/**
|
|
@@ -795,13 +1450,14 @@ function say(line) {
|
|
|
795
1450
|
process.stdout.write(line + "\n");
|
|
796
1451
|
}
|
|
797
1452
|
function sayPrivacyContract() {
|
|
798
|
-
|
|
799
|
-
"
|
|
800
|
-
"
|
|
801
|
-
"
|
|
802
|
-
"
|
|
803
|
-
"
|
|
804
|
-
"
|
|
1453
|
+
ui.detail([
|
|
1454
|
+
"• Conversation content stays local (LanceDB + raw transcripts).",
|
|
1455
|
+
"• Content snippets leave the machine for embedding only (Voyage AI).",
|
|
1456
|
+
"• The Voyage key sits in ~/.rift.env (mode 0600), is sent only to Voyage,",
|
|
1457
|
+
" and is never logged or sent to Clem.",
|
|
1458
|
+
"• Feedback is stored locally as JSONL. Relay is opt-in: explicit notes only,",
|
|
1459
|
+
" plus daemon health booleans — no paths, no content, no key bytes.",
|
|
1460
|
+
"• Full contract: https://getrift.dev/privacy",
|
|
805
1461
|
].join("\n"));
|
|
806
1462
|
}
|
|
807
1463
|
function safeDiscover(fn) {
|