@getrift/rift 0.1.0-beta.12 → 0.1.0-beta.13

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 (45) hide show
  1. package/dist/src/cli/commands/onboard.d.ts +38 -0
  2. package/dist/src/cli/commands/onboard.d.ts.map +1 -1
  3. package/dist/src/cli/commands/onboard.js +176 -101
  4. package/dist/src/cli/commands/onboard.js.map +1 -1
  5. package/dist/src/cli/status/friend-header.d.ts +8 -1
  6. package/dist/src/cli/status/friend-header.d.ts.map +1 -1
  7. package/dist/src/cli/status/friend-header.js +93 -12
  8. package/dist/src/cli/status/friend-header.js.map +1 -1
  9. package/dist/src/cli/ui.d.ts +47 -0
  10. package/dist/src/cli/ui.d.ts.map +1 -0
  11. package/dist/src/cli/ui.js +166 -0
  12. package/dist/src/cli/ui.js.map +1 -0
  13. package/dist/src/jobs/handlers/dedupe-conversations.d.ts +25 -2
  14. package/dist/src/jobs/handlers/dedupe-conversations.d.ts.map +1 -1
  15. package/dist/src/jobs/handlers/dedupe-conversations.js +48 -9
  16. package/dist/src/jobs/handlers/dedupe-conversations.js.map +1 -1
  17. package/dist/src/jobs/handlers/ingest.d.ts.map +1 -1
  18. package/dist/src/jobs/handlers/ingest.js +8 -2
  19. package/dist/src/jobs/handlers/ingest.js.map +1 -1
  20. package/dist/src/mcp/server.d.ts.map +1 -1
  21. package/dist/src/mcp/server.js +43 -3
  22. package/dist/src/mcp/server.js.map +1 -1
  23. package/dist/src/mcp/tools/context-pack.js +163 -25
  24. package/dist/src/mcp/tools/context-pack.js.map +1 -1
  25. package/dist/src/observability/onboarding-metric.d.ts +115 -0
  26. package/dist/src/observability/onboarding-metric.d.ts.map +1 -0
  27. package/dist/src/observability/onboarding-metric.js +344 -0
  28. package/dist/src/observability/onboarding-metric.js.map +1 -0
  29. package/dist/src/retrieval/context-pack.d.ts +37 -0
  30. package/dist/src/retrieval/context-pack.d.ts.map +1 -1
  31. package/dist/src/retrieval/context-pack.js +165 -1
  32. package/dist/src/retrieval/context-pack.js.map +1 -1
  33. package/dist/src/retrieval/current-truth.d.ts +326 -0
  34. package/dist/src/retrieval/current-truth.d.ts.map +1 -0
  35. package/dist/src/retrieval/current-truth.js +747 -0
  36. package/dist/src/retrieval/current-truth.js.map +1 -0
  37. package/dist/src/retrieval/git-state.d.ts +53 -0
  38. package/dist/src/retrieval/git-state.d.ts.map +1 -0
  39. package/dist/src/retrieval/git-state.js +174 -0
  40. package/dist/src/retrieval/git-state.js.map +1 -0
  41. package/dist/src/server/routes/friend-status.d.ts +63 -0
  42. package/dist/src/server/routes/friend-status.d.ts.map +1 -1
  43. package/dist/src/server/routes/friend-status.js +97 -0
  44. package/dist/src/server/routes/friend-status.js.map +1 -1
  45. package/package.json +2 -1
@@ -2,6 +2,44 @@ import { Command } from "commander";
2
2
  import { loadRiftEnv } from "../../runtime/rift-env.js";
3
3
  import { validateVoyageKey } from "../../onboarding/voyage-validate.js";
4
4
  import { writeEnvFile } from "../../onboarding/env-file.js";
5
+ /**
6
+ * Commander.js note on `--no-<flag>` shape.
7
+ *
8
+ * `.option("--no-foo", ...)` sets `opts.foo = false` on the parsed
9
+ * options, NOT `opts.noFoo = true`. Two consequences:
10
+ * 1. We accept BOTH shapes in this interface: the affirmative key
11
+ * (`codexCapture: boolean`, `importExport: string | boolean`, …)
12
+ * is what Commander populates; the `no*` keys exist purely so
13
+ * tests that drive `runOnboard` programmatically can keep passing
14
+ * `{ noCodexCapture: true }` without going through the parser.
15
+ * 2. Every read goes through `isOff(opts, "<affirmative>", "<no*>")`
16
+ * so a real CLI invocation and a programmatic one resolve to the
17
+ * same truth. Reading only `opts.no*` (the pre-2026-05-19 bug)
18
+ * silently ignored the CLI form.
19
+ */
20
+ interface OnboardOpts {
21
+ voyageKey?: string;
22
+ voyageLabel?: string;
23
+ enableFeedbackRelay?: string;
24
+ feedbackRelay?: boolean;
25
+ noFeedbackRelay?: boolean;
26
+ importExport?: string | boolean;
27
+ noImportExport?: boolean;
28
+ reconfigureVoyage?: boolean;
29
+ yes?: boolean;
30
+ skipCapture?: boolean;
31
+ withClaudeHook?: boolean;
32
+ claudeHook?: boolean;
33
+ noClaudeHook?: boolean;
34
+ codexCapture?: boolean;
35
+ noCodexCapture?: boolean;
36
+ }
37
+ /**
38
+ * Resolve a `--no-<flag>` to a single boolean ("yes, skip this thing").
39
+ * Commander populates the affirmative key with `false`; programmatic
40
+ * callers may pass the `no*` key with `true`. Either form wins.
41
+ */
42
+ export declare function isOff<K extends keyof OnboardOpts, N extends keyof OnboardOpts>(opts: OnboardOpts, affirmative: K, negative: N): boolean;
5
43
  export declare function makeOnboardCommand(): Command;
6
44
  /**
7
45
  * Sanitize a raw `--voyage-label`, persist it when valid, and return the
@@ -1 +1 @@
1
- {"version":3,"file":"onboard.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/onboard.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"onboard.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/onboard.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AASpC,OAAO,EAEL,WAAW,EACZ,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AACxE,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAoB5D;;;;;;;;;;;;;;GAcG;AACH,UAAU,WAAW;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,CAAC;IAKvB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,CAAC,SAAS,MAAM,WAAW,EAAE,CAAC,SAAS,MAAM,WAAW,EAC5E,IAAI,EAAE,WAAW,EACjB,WAAW,EAAE,CAAC,EACd,QAAQ,EAAE,CAAC,GACV,OAAO,CAET;AAqBD,wBAAgB,kBAAkB,IAAI,OAAO,CA0C5C;AA2MD;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAC3B,MAAM,GAAG,IAAI,CAgBf;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAmC1E;AA8DD,KAAK,oBAAoB,GACrB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,GAClC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,gBAAgB,GAAG,kBAAkB,GAAG,kBAAkB,GAAG,eAAe,GAAG,cAAc,CAAA;CAAE,CAAC;AAqDvI;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GACzB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,UAAU,GAAG,QAAQ,CAAA;CAAE,GAC1D;IACE,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEN,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG;IAClD,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC;IACpC,OAAO,EAAE,MAAM,CAAC;CACjB,CAwBA;AAED,wBAAsB,WAAW,CAC/B,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,iBAAiB,CAAC,CAkC5B;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAgC3D;AA8JD,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CA4F/D;AA2GD,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC1B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAElC;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAC1C,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,YAAY,CAAC;CACtB,GAAG,MAAM,EAAE,CA2BX;AAqED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAC7C,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,iBAAiB,CAAC;IACpC,6DAA6D;IAC7D,KAAK,CAAC,EAAE,OAAO,YAAY,CAAC;IAC5B,wDAAwD;IACxD,OAAO,CAAC,EAAE,OAAO,WAAW,CAAC;IAC7B,0CAA0C;IAC1C,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/B;AAED;;;;;;;;;GASG;AACH,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,IAAI,CAAC,CAgCf;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,iBAAiB,EAC9B,oBAAoB,EAAE,MAAM,GAAG,SAAS,GACvC,IAAI,CAyBN"}
@@ -38,10 +38,20 @@ import { runAutoCapture, resolveCodexCaptureDeps, } from "../../capture/auto-cap
38
38
  import { CodexCliTriageProvider } from "../../capture/codex-cli-triage-provider.js";
39
39
  import { discoverClaudeCodeSessions } from "../../ingestion/parsers/claude-code-jsonl.js";
40
40
  import { discoverCodexSessions } from "../../ingestion/parsers/codex-jsonl.js";
41
+ import { sniffInboxSource } from "../../ingestion/inbox-core/source-sniffer.js";
41
42
  import { writeHookConfig } from "../hooks-writers/index.js";
42
43
  import { HooksParseFailedError } from "../hooks-writers/index.js";
43
44
  import { pollJob } from "../job-poller.js";
44
45
  import { isJobFailure } from "../output.js";
46
+ import * as ui from "../ui.js";
47
+ /**
48
+ * Resolve a `--no-<flag>` to a single boolean ("yes, skip this thing").
49
+ * Commander populates the affirmative key with `false`; programmatic
50
+ * callers may pass the `no*` key with `true`. Either form wins.
51
+ */
52
+ export function isOff(opts, affirmative, negative) {
53
+ return opts[affirmative] === false || opts[negative] === true;
54
+ }
45
55
  const SUPPORTED_INGEST_EXTENSIONS = new Set([".json", ".zip"]);
46
56
  const DEFAULT_DATA_DIR = path.join(os.homedir(), "Library", "Application Support", "Rift", "data");
47
57
  // Inline import is intentionally narrow until Slice 3 lands the real
@@ -60,6 +70,7 @@ export function makeOnboardCommand() {
60
70
  .option("--reconfigure-voyage", "Recovery flow: replace the Voyage key only", false)
61
71
  .option("--yes", "Accept all defaults (non-interactive)", false)
62
72
  .option("--skip-capture", "Skip the post-setup capture pass (test-only)", false)
73
+ .option("--no-codex-capture", "Skip the Codex CLI preflight + disable the capture pass for this run")
63
74
  .option("--with-claude-hook", "Install the Rift policy hook into Claude Code without prompting", false)
64
75
  .option("--no-claude-hook", "Skip the Claude Code policy-hook prompt entirely")
65
76
  .action(async (opts, cmd) => {
@@ -89,23 +100,17 @@ async function runOnboard(opts, globalOpts) {
89
100
  await reconfigureVoyageFlow(opts, globalOpts, rl);
90
101
  return;
91
102
  }
92
- say("");
93
- say("Rift — first-run setup");
94
- say("─────────────────────");
103
+ ui.banner();
95
104
  // Step 1 — legacy migration (idempotent).
96
105
  const dataDir = await ensureConfigAndDataDir(globalOpts.config, opts, rl);
97
- say("");
98
- say("Running legacy migration check…");
99
106
  const migration = await runLegacyMigration({ dataDir });
100
107
  if (migration.migratedCount > 0) {
101
- say(`Migrated ${migration.migratedCount} legacy artifact(s).`);
108
+ ui.step("ok", "Existing data", `carried over ${migration.migratedCount} item(s)`);
102
109
  }
103
110
  else {
104
- say("No legacy artifacts found.");
111
+ ui.step("ok", "Existing data", "nothing to carry over");
105
112
  }
106
113
  // Step 2 — Voyage key + validate + persist + kickstart + smoke.
107
- say("");
108
- say("Voyage API key");
109
114
  const last4 = await collectAndPersistVoyageKey(opts, globalOpts, rl);
110
115
  // Step 2b — sanitize + persist optional --voyage-label to config.json
111
116
  // (backup first). Invalid labels are dropped without echoing the raw
@@ -113,39 +118,55 @@ async function runOnboard(opts, globalOpts) {
113
118
  // can never leak through stdout or config.json.
114
119
  const safeLabel = applyVoyageLabel(opts.voyageLabel, globalOpts.config, say);
115
120
  // 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.");
121
+ //
122
+ // Codex CLI is only required by the auto-capture lane. Friends who
123
+ // use only Claude Desktop / Claude Code (and import via the inbox
124
+ // or `rift import`) should not be blocked by a missing/expired
125
+ // Codex auth. So a failed preflight is a warning, not fatal
126
+ // onboarding continues and the capture pass is skipped for this
127
+ // run. `--no-codex-capture` skips the preflight entirely.
128
+ let captureDisabled = false;
129
+ if (isOff(opts, "codexCapture", "noCodexCapture")) {
130
+ ui.step("skip", "Chat access", "skipped (--no-codex-capture) · auto-import off");
131
+ captureDisabled = true;
132
+ }
133
+ else {
134
+ const codexSpin = new ui.Spinner("Chat access").start();
135
+ const codexOk = await codexPreflight();
136
+ if (codexOk) {
137
+ codexSpin.succeed("Chat access", "ready · Codex CLI");
138
+ }
139
+ else {
140
+ codexSpin.fail("Chat access", "not ready · Codex CLI — auto-import off");
141
+ ui.note("To enable later: run `codex login`, then re-run `rift onboard`.");
142
+ captureDisabled = true;
143
+ }
144
+ }
125
145
  // Step 4 — discover sessions.
126
- say("");
127
146
  const claudeSessions = safeDiscover(() => discoverClaudeCodeSessions(path.join(os.homedir(), ".claude")));
128
147
  const codexSessions = safeDiscover(() => discoverCodexSessions());
129
- say(`Found ${claudeSessions} Claude Code session(s) and ${codexSessions} Codex CLI session(s).`);
148
+ ui.step("ok", "Chat history", `${claudeSessions} Claude Code · ${codexSessions} Codex CLI`);
130
149
  // Step 5 — privacy + feedback opt-in.
131
- say("");
132
- say("Privacy");
150
+ ui.step("info", "Privacy", "");
133
151
  sayPrivacyContract();
134
152
  const feedback = await collectFeedbackPreference(opts, rl, dataDir);
135
153
  if (feedback.enabled) {
136
- say(`Feedback relay enabled (installation_id: ${feedback.installation_id}).`);
154
+ ui.step("ok", "Feedback", `relay on (installation_id: ${feedback.installation_id})`);
137
155
  }
138
156
  else {
139
- say("Feedback relay off — feedback stays in local JSONL only.");
157
+ ui.step("ok", "Feedback", "relay off — stays in local JSONL");
140
158
  }
141
159
  // Step 5b — optional Claude Code policy hook.
142
- say("");
143
160
  await maybeInstallClaudeCodeHook(opts, rl);
144
161
  // Step 6 + 7 — watermark current sessions + run one capture pass.
145
162
  let captureSaved = 0;
146
- if (!opts.skipCapture) {
147
- say("");
148
- say("Running first capture pass (watermark + scan)…");
163
+ if (opts.skipCapture) {
164
+ ui.step("skip", "Chat import", "skipped (--skip-capture)");
165
+ }
166
+ else if (captureDisabled) {
167
+ ui.step("skip", "Chat import", "skipped (Codex CLI not ready)");
168
+ }
169
+ else {
149
170
  const captureResult = await runFirstCapturePass(globalOpts.config, dataDir);
150
171
  captureSaved = captureResult.saved;
151
172
  // Per A-1.1/C-1.3: a token-issuance failure during capture-pass
@@ -157,13 +178,12 @@ async function runOnboard(opts, globalOpts) {
157
178
  return;
158
179
  }
159
180
  }
160
- else {
161
- say("Skipping capture pass (--skip-capture).");
162
- }
163
181
  // Step 8 — optional export import.
164
- say("");
165
182
  let importSucceeded = false;
166
- if (!opts.noImportExport) {
183
+ if (isOff(opts, "importExport", "noImportExport")) {
184
+ ui.step("skip", "File import", "skipped (--no-import-export)");
185
+ }
186
+ else {
167
187
  const importPath = await collectImportPath(opts, rl);
168
188
  if (importPath) {
169
189
  const outcome = await runImport(importPath, globalOpts.config);
@@ -172,30 +192,34 @@ async function runOnboard(opts, globalOpts) {
172
192
  }
173
193
  }
174
194
  else {
175
- say("Skipping import. Run rift import <path> --source <name> later.");
195
+ ui.step("skip", "File import", "none — run `rift import <path> --source <name>` later");
176
196
  }
177
197
  }
178
- else {
179
- say("Skipping import (--no-import-export).");
180
- }
181
198
  // Step 9 — first-recall verification.
182
199
  // Onboarding declares "complete" only when (a) capture or import landed
183
200
  // user data this run, AND (b) a recall query against the daemon returns
184
201
  // at least one hit. Without (a), there is nothing to recall — calling
185
202
  // it "complete" because /search returned 0 rows would be misleading.
186
- say("");
187
203
  const ingestedAny = captureSaved > 0 || importSucceeded;
188
- say(`First-recall sanity check (capture saved ${captureSaved} · import ${importSucceeded ? "ok" : "none"})…`);
204
+ const recallSpin = new ui.Spinner("Search check").start();
189
205
  const recall = ingestedAny
190
206
  ? await firstRecallCheck(globalOpts.config)
191
207
  : { ok: false, reason: "no user data was captured or imported during onboarding" };
208
+ if (recall.ok && recall.hits > 0) {
209
+ recallSpin.succeed("Search check", `found ${recall.hits} result(s)`);
210
+ }
211
+ else if (recall.ok) {
212
+ // 0 hits is NOT success — decideOnboardOutcome treats it as incomplete.
213
+ recallSpin.warn("Search check", "no results yet — index is fresh, try a query shortly");
214
+ }
215
+ else {
216
+ recallSpin.fail("Search check", recall.reason);
217
+ }
192
218
  // Step 10 — next-action card.
193
- say("");
194
- say("─────────────────────");
195
- for (const line of decideOnboardOutcome({ ingestedAny, recall }))
196
- say(line);
197
- say(`Voyage: key valid (last 4 …${last4})${safeLabel ? ` · label ${safeLabel}` : ""}`);
198
- say("");
219
+ const card = decideOnboardOutcome({ ingestedAny, recall });
220
+ card.push(ui.pc.dim(`Voyage: key valid (last 4 …${last4})${safeLabel ? ` · label ${safeLabel}` : ""}`));
221
+ ui.box(card);
222
+ ui.line("");
199
223
  }
200
224
  finally {
201
225
  rl.close();
@@ -204,10 +228,11 @@ async function runOnboard(opts, globalOpts) {
204
228
  // ----- Step 1: config.json + data dir -----
205
229
  async function ensureConfigAndDataDir(configPath, opts, rl) {
206
230
  const absoluteConfig = path.resolve(configPath);
231
+ const shownConfig = absoluteConfig.replace(os.homedir(), "~");
207
232
  if (fs.existsSync(absoluteConfig)) {
208
233
  const config = loadConfig(absoluteConfig);
209
- say(`Using existing config: ${absoluteConfig}`);
210
- say(`Data dir: ${config.data_paths.data_dir}`);
234
+ ui.step("ok", "Settings", shownConfig);
235
+ ui.note(`data dir: ${config.data_paths.data_dir}`);
211
236
  return config.data_paths.data_dir;
212
237
  }
213
238
  const defaultDataDir = DEFAULT_DATA_DIR;
@@ -237,8 +262,8 @@ async function ensureConfigAndDataDir(configPath, opts, rl) {
237
262
  encoding: "utf8",
238
263
  mode: 0o644,
239
264
  });
240
- say(`Wrote default config: ${absoluteConfig}`);
241
- say(`Data dir: ${dataDir}`);
265
+ ui.step("ok", "Settings", `wrote default · ${shownConfig}`);
266
+ ui.note(`data dir: ${dataDir}`);
242
267
  // Trigger ensureDirectories.
243
268
  loadConfig(absoluteConfig);
244
269
  return dataDir;
@@ -311,17 +336,18 @@ export function persistVoyageLabel(configPath, label) {
311
336
  // ----- Step 2: Voyage key flow -----
312
337
  async function collectAndPersistVoyageKey(opts, globalOpts, rl) {
313
338
  const key = await collectVoyageKey(opts, rl);
314
- say("Validating with Voyage API…");
339
+ const validateSpin = new ui.Spinner("Search index").start();
315
340
  const validation = await validateVoyageKey({ apiKey: key });
316
341
  if (!validation.ok) {
342
+ validateSpin.fail("Search index", "validation failed");
317
343
  throw new CliError(`Voyage validation failed: ${validation.reason}`, "validation");
318
344
  }
319
- say(`Voyage key valid (last 4 …${validation.last4}).`);
345
+ validateSpin.succeed("Search index", `connected · Voyage · ····${validation.last4}`);
320
346
  const envPath = defaultRiftEnvPath();
321
347
  const writeResult = writeEnvFile({ filePath: envPath, key: "VOYAGE_API_KEY", value: key });
322
- say(writeResult.backedUp
323
- ? "Wrote ~/.rift.env (existing file backed up)."
324
- : "Wrote ~/.rift.env (mode 0600).");
348
+ ui.note(writeResult.backedUp
349
+ ? "wrote ~/.rift.env (existing file backed up)"
350
+ : "wrote ~/.rift.env (mode 0600)");
325
351
  // Refresh process.env so any subsequent in-process Voyage call sees it.
326
352
  loadRiftEnv({ filePath: envPath });
327
353
  const refresh = await daemonRefreshFlow(globalOpts);
@@ -359,35 +385,38 @@ async function collectVoyageKey(opts, rl) {
359
385
  async function daemonRefreshFlow(globalOpts) {
360
386
  const baseUrl = safeResolveBaseUrl(globalOpts.config);
361
387
  if (!baseUrl) {
362
- say("Daemon not configured yet — skipping kickstart. Bootstrap via install.sh, then re-run rift onboard.");
388
+ ui.step("skip", "Rift service", "not configured yet — bootstrap via install.sh, then re-run");
363
389
  return { ok: false, reason: "daemon not configured", kind: "not_configured" };
364
390
  }
391
+ const daemonSpin = new ui.Spinner("Rift service").start();
365
392
  const kick = await kickstartDaemon();
366
393
  if (kick.status === "agent_not_loaded") {
367
- say(kick.hint);
394
+ daemonSpin.fail("Rift service", "launchd agent not loaded");
395
+ ui.note(kick.hint);
368
396
  return { ok: false, reason: kick.hint ?? "agent not loaded", kind: "agent_not_loaded" };
369
397
  }
370
398
  if (kick.status === "failed") {
399
+ daemonSpin.fail("Rift service", "kickstart failed");
371
400
  return {
372
401
  ok: false,
373
402
  reason: kick.hint ?? "kickstart failed",
374
403
  kind: "kickstart_failed",
375
404
  };
376
405
  }
377
- say("Daemon kickstarted.");
378
406
  const health = await waitForHealth({ baseUrl });
379
407
  if (!health.ok) {
408
+ daemonSpin.fail("Rift service", "health check failed");
380
409
  return { ok: false, reason: health.reason, kind: "health_failed" };
381
410
  }
382
- say(`Daemon healthy (uptime ${health.uptimeSeconds}s).`);
383
411
  if (!health.voyageKeyPresent) {
412
+ daemonSpin.fail("Rift service", "voyage_key_present=false after kickstart");
384
413
  return {
385
414
  ok: false,
386
415
  reason: "Daemon /health reports voyage_key_present=false after kickstart.",
387
416
  kind: "smoke_failed",
388
417
  };
389
418
  }
390
- say("Cloud embedding live (daemon loaded VOYAGE_API_KEY).");
419
+ daemonSpin.succeed("Rift service", `healthy · daemon · uptime ${health.uptimeSeconds}s`);
391
420
  return { ok: true, kickstarted: true };
392
421
  }
393
422
  export function classifyTokenFailure(err) {
@@ -497,7 +526,7 @@ async function collectFeedbackPreference(opts, rl, dataDir) {
497
526
  enabled = true;
498
527
  url = opts.enableFeedbackRelay;
499
528
  }
500
- else if (opts.noFeedbackRelay || opts.yes) {
529
+ else if (isOff(opts, "feedbackRelay", "noFeedbackRelay") || opts.yes) {
501
530
  enabled = false;
502
531
  }
503
532
  else {
@@ -540,7 +569,7 @@ async function collectFeedbackPreference(opts, rl, dataDir) {
540
569
  * guardrail, not a correctness requirement.
541
570
  */
542
571
  async function maybeInstallClaudeCodeHook(opts, rl) {
543
- if (opts.noClaudeHook) {
572
+ if (isOff(opts, "claudeHook", "noClaudeHook")) {
544
573
  return;
545
574
  }
546
575
  const homeDir = os.homedir();
@@ -555,20 +584,22 @@ async function maybeInstallClaudeCodeHook(opts, rl) {
555
584
  }
556
585
  else if (opts.yes) {
557
586
  // Non-interactive default: skip. Operator must opt in with --with-claude-hook.
558
- say("Claude Code detected — skipping the policy-hook prompt (pass --with-claude-hook to install non-interactively).");
587
+ ui.step("skip", "Claude Code", "skipped (pass --with-claude-hook to install non-interactively)");
559
588
  return;
560
589
  }
561
590
  else {
562
- say("Claude Code detected.");
563
- say("Optional: install a PreToolUse policy hook so Rift retrieval starts with rift_context_pack");
564
- say("(prevents broad rift_search dumps from burning context). Disable any time with RIFT_POLICY_DISABLED=1.");
565
- const answer = (await ask(rl, "Install Claude Code policy hook? [y/N]: ", "n"))
591
+ ui.note([
592
+ "Claude Code detected.",
593
+ "Optional: install a PreToolUse policy hook so Rift retrieval starts with rift_context_pack",
594
+ "(prevents broad rift_search dumps from burning context). Disable any time with RIFT_POLICY_DISABLED=1.",
595
+ ].join("\n"));
596
+ const answer = (await ask(rl, " Install Claude Code policy hook? [y/N]: ", "n"))
566
597
  .trim()
567
598
  .toLowerCase();
568
599
  install = answer === "y" || answer === "yes";
569
600
  }
570
601
  if (!install) {
571
- say("Skipping Claude Code policy hook.");
602
+ ui.step("skip", "Claude Code", "not installed");
572
603
  return;
573
604
  }
574
605
  try {
@@ -578,25 +609,25 @@ async function maybeInstallClaudeCodeHook(opts, rl) {
578
609
  dryRun: false,
579
610
  });
580
611
  if (outcome.hookEntryAdded) {
581
- say("Installed Rift policy hook into Claude Code (PreToolUse entry added).");
612
+ ui.step("ok", "Claude Code", "connected · auto-recall hook added");
582
613
  }
583
614
  else if (outcome.hookEntryReplaced || outcome.scriptUpdated) {
584
- say("Updated Rift policy hook in Claude Code.");
615
+ ui.step("ok", "Claude Code", "updated");
585
616
  }
586
617
  else {
587
- say("Rift policy hook already up-to-date in Claude Code.");
618
+ ui.step("ok", "Claude Code", "already up-to-date");
588
619
  }
589
620
  if (outcome.backupId) {
590
- say(` Backup ID: ${outcome.backupId}`);
621
+ ui.note(`backup ID: ${outcome.backupId}`);
591
622
  }
592
- say(" (Restart Claude Code or open /hooks once for the new entry to load.)");
623
+ ui.note("(Restart Claude Code or open /hooks once for the new entry to load.)");
593
624
  }
594
625
  catch (err) {
595
626
  if (err instanceof HooksParseFailedError) {
596
- say(err.message);
627
+ ui.step("fail", "Claude Code", err.message);
597
628
  }
598
629
  else {
599
- say(`Claude Code hook install failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
630
+ ui.step("fail", "Claude Code", `install failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
600
631
  }
601
632
  }
602
633
  }
@@ -604,7 +635,7 @@ async function maybeInstallClaudeCodeHook(opts, rl) {
604
635
  export async function runFirstCapturePass(configPath, dataDir) {
605
636
  const baseUrl = safeResolveBaseUrl(configPath);
606
637
  if (!baseUrl) {
607
- say("Daemon not reachable — skipping capture pass.");
638
+ ui.step("skip", "Chat import", "daemon not reachable — skipped");
608
639
  return { saved: 0, reviewed: 0 };
609
640
  }
610
641
  // Per A-1.1/C-1.3: token failure here is fatal. The wizard cannot
@@ -642,6 +673,7 @@ export async function runFirstCapturePass(configPath, dataDir) {
642
673
  catch {
643
674
  codexCaptureDeps = {};
644
675
  }
676
+ const captureSpin = new ui.Spinner("Chat import").start();
645
677
  try {
646
678
  const report = await runAutoCapture({
647
679
  dataDir,
@@ -667,21 +699,37 @@ export async function runFirstCapturePass(configPath, dataDir) {
667
699
  }
668
700
  },
669
701
  });
670
- say(`Capture: discovered ${report.total_discovered}, new ${report.new_conversations}, saved ${report.saved}, review ${report.review}, errors ${report.errors}.`);
702
+ const base = `${report.saved} added · ${report.review} to review`;
703
+ if (report.errors > 0) {
704
+ captureSpin.warn("Chat import", `${base} · ${report.errors} error(s)`);
705
+ }
706
+ else {
707
+ captureSpin.succeed("Chat import", base);
708
+ }
671
709
  return { saved: report.saved, reviewed: report.review };
672
710
  }
673
711
  catch (err) {
674
- say(`Capture pass error (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
712
+ captureSpin.fail("Chat import", `error (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
675
713
  return { saved: 0, reviewed: 0 };
676
714
  }
677
715
  }
678
716
  // ----- Step 8: import -----
679
717
  async function collectImportPath(opts, rl) {
680
- if (opts.importExport)
718
+ // opts.importExport is `string | false | undefined` — Commander writes
719
+ // `false` when `--no-import-export` was passed, but the caller already
720
+ // short-circuits in that case. Only a non-empty string is a real path.
721
+ if (typeof opts.importExport === "string" && opts.importExport.length > 0) {
681
722
  return opts.importExport;
723
+ }
682
724
  if (opts.yes)
683
725
  return null;
684
- const answer = (await ask(rl, "Drop a path to a ChatGPT/Claude/Grok/Gemini export to import now (or `skip`): ", "skip")).trim();
726
+ // Inline import is ChatGPT-only. Surface this BEFORE the prompt so a
727
+ // friend doesn't paste a Claude/Grok/Gemini path then get told to
728
+ // re-run elsewhere — the source-sniffer below also fail-fasts those,
729
+ // but the friend learns earlier this way.
730
+ say("Inline import here is ChatGPT-only. For Claude/Grok/Gemini exports, run:");
731
+ say(" rift import <path> --source claude_web|grok_web|gemini_web");
732
+ const answer = (await ask(rl, "Drop a path to a ChatGPT export to import now (or `skip`): ", "skip")).trim();
685
733
  if (!answer || answer.toLowerCase() === "skip")
686
734
  return null;
687
735
  return answer;
@@ -711,11 +759,20 @@ async function runImport(filePath, configPath) {
711
759
  }
712
760
  const client = createHttpClient({ baseUrl, token: tokenResult.token });
713
761
  const buf = fs.readFileSync(absolute);
762
+ // Fail-fast for non-ChatGPT exports: route them to `rift import` with
763
+ // the right `--source` flag instead of silently feeding them through
764
+ // the ChatGPT parser. Returns null on "looks like ChatGPT or unknown,"
765
+ // a concrete provider otherwise.
766
+ const sniffed = sniffInboxSource(path.basename(absolute), buf);
767
+ if (sniffed) {
768
+ say(`Detected a ${sniffed} export — the inline importer is ChatGPT-only.`);
769
+ say(`Run: rift import "${absolute}" --source ${sniffed}`);
770
+ return { kind: "skipped", reason: `non-chatgpt export detected: ${sniffed}` };
771
+ }
714
772
  const form = new FormData();
715
773
  form.append("source", ONBOARD_INLINE_IMPORT_SOURCE);
716
774
  form.append("file", new Blob([buf]), path.basename(absolute));
717
775
  say(`Importing ${path.basename(absolute)} as ${ONBOARD_INLINE_IMPORT_SOURCE}…`);
718
- say("(For Claude/Grok/Gemini exports, run: rift import <path> --source <name>)");
719
776
  const { data } = await client.postMultipart("/ingest", form);
720
777
  const resp = data;
721
778
  if (resp.duplicate) {
@@ -755,8 +812,10 @@ export function decideOnboardOutcome(input) {
755
812
  }
756
813
  if (recall.ok) {
757
814
  return [
758
- "Setup partially complete — capture+import succeeded but search returned 0 hits.",
759
- "Next: rift feedback --kind=broke --with-status \"first-recall returned 0\"",
815
+ "Setup ready — capture+import succeeded but the generic recall query returned 0 hits.",
816
+ "Indexing may still be in flight, or this query just didn't match your archive yet.",
817
+ "Try: rift search \"<a topic you remember discussing>\"",
818
+ "If repeated tries still return nothing, run: rift feedback --kind=broke --with-status",
760
819
  ];
761
820
  }
762
821
  return [
@@ -776,19 +835,35 @@ async function firstRecallCheck(configPath) {
776
835
  // sessions or imported chat exports without targeting any onboarding
777
836
  // marker. The smoke is non-indexing (see daemonRefreshFlow), so any
778
837
  // hit returned here is real user data, not a leftover probe.
779
- try {
780
- const { data } = await client.post("/search", {
781
- query: "recent conversation",
782
- scope: "all",
783
- top_k: 10,
784
- });
785
- const body = data;
786
- const hits = (body.results?.length ?? body.hits?.length ?? 0);
787
- return { ok: true, hits };
788
- }
789
- catch (err) {
790
- return { ok: false, reason: err instanceof Error ? err.message : String(err) };
838
+ //
839
+ // Small unusual imports (e.g. one short Claude project export) can
840
+ // return 0 hits for this generic query even when the data was
841
+ // indexed correctly. Retry briefly so the post-onboard nudge isn't a
842
+ // false alarm on slow embedders / sparse archives.
843
+ const RECALL_RETRY_DELAYS_MS = [0, 5_000];
844
+ let lastErr = null;
845
+ for (const delay of RECALL_RETRY_DELAYS_MS) {
846
+ if (delay > 0)
847
+ await new Promise((r) => setTimeout(r, delay));
848
+ try {
849
+ const { data } = await client.post("/search", {
850
+ query: "recent conversation",
851
+ scope: "all",
852
+ top_k: 10,
853
+ });
854
+ const body = data;
855
+ const hits = (body.results?.length ?? body.hits?.length ?? 0);
856
+ if (hits > 0)
857
+ return { ok: true, hits };
858
+ lastErr = null;
859
+ }
860
+ catch (err) {
861
+ lastErr = err instanceof Error ? err.message : String(err);
862
+ }
791
863
  }
864
+ if (lastErr)
865
+ return { ok: false, reason: lastErr };
866
+ return { ok: true, hits: 0 };
792
867
  }
793
868
  // ----- Slice 6: --reconfigure-voyage -----
794
869
  /**
@@ -893,13 +968,13 @@ function say(line) {
893
968
  process.stdout.write(line + "\n");
894
969
  }
895
970
  function sayPrivacyContract() {
896
- say([
897
- " • Conversation content stays local (LanceDB + raw transcripts).",
898
- " • Content snippets leave the machine for embedding only (Voyage AI).",
899
- " • The Voyage key sits in ~/.rift.env (mode 0600). Never logged, never sent to Clem.",
900
- " • Feedback is stored locally as JSONL. Relay is opt-in: explicit notes only,",
901
- " plus daemon health booleans — no paths, no content, no key bytes.",
902
- " • Full contract: docs/feedback/PRIVACY.md",
971
+ ui.detail([
972
+ "• Conversation content stays local (LanceDB + raw transcripts).",
973
+ "• Content snippets leave the machine for embedding only (Voyage AI).",
974
+ "• The Voyage key sits in ~/.rift.env (mode 0600). Never logged, never sent to Clem.",
975
+ "• Feedback is stored locally as JSONL. Relay is opt-in: explicit notes only,",
976
+ " plus daemon health booleans — no paths, no content, no key bytes.",
977
+ "• Full contract: https://getrift.dev/privacy",
903
978
  ].join("\n"));
904
979
  }
905
980
  function safeDiscover(fn) {