@getrift/rift 0.1.0-beta.2 → 0.1.0-beta.20

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