@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.
- package/dist/src/cli/commands/onboard.d.ts +38 -0
- package/dist/src/cli/commands/onboard.d.ts.map +1 -1
- package/dist/src/cli/commands/onboard.js +176 -101
- package/dist/src/cli/commands/onboard.js.map +1 -1
- package/dist/src/cli/status/friend-header.d.ts +8 -1
- package/dist/src/cli/status/friend-header.d.ts.map +1 -1
- package/dist/src/cli/status/friend-header.js +93 -12
- package/dist/src/cli/status/friend-header.js.map +1 -1
- package/dist/src/cli/ui.d.ts +47 -0
- package/dist/src/cli/ui.d.ts.map +1 -0
- package/dist/src/cli/ui.js +166 -0
- package/dist/src/cli/ui.js.map +1 -0
- package/dist/src/jobs/handlers/dedupe-conversations.d.ts +25 -2
- package/dist/src/jobs/handlers/dedupe-conversations.d.ts.map +1 -1
- package/dist/src/jobs/handlers/dedupe-conversations.js +48 -9
- package/dist/src/jobs/handlers/dedupe-conversations.js.map +1 -1
- package/dist/src/jobs/handlers/ingest.d.ts.map +1 -1
- package/dist/src/jobs/handlers/ingest.js +8 -2
- package/dist/src/jobs/handlers/ingest.js.map +1 -1
- package/dist/src/mcp/server.d.ts.map +1 -1
- package/dist/src/mcp/server.js +43 -3
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/mcp/tools/context-pack.js +163 -25
- package/dist/src/mcp/tools/context-pack.js.map +1 -1
- package/dist/src/observability/onboarding-metric.d.ts +115 -0
- package/dist/src/observability/onboarding-metric.d.ts.map +1 -0
- package/dist/src/observability/onboarding-metric.js +344 -0
- package/dist/src/observability/onboarding-metric.js.map +1 -0
- package/dist/src/retrieval/context-pack.d.ts +37 -0
- package/dist/src/retrieval/context-pack.d.ts.map +1 -1
- package/dist/src/retrieval/context-pack.js +165 -1
- package/dist/src/retrieval/context-pack.js.map +1 -1
- package/dist/src/retrieval/current-truth.d.ts +326 -0
- package/dist/src/retrieval/current-truth.d.ts.map +1 -0
- package/dist/src/retrieval/current-truth.js +747 -0
- package/dist/src/retrieval/current-truth.js.map +1 -0
- package/dist/src/retrieval/git-state.d.ts +53 -0
- package/dist/src/retrieval/git-state.d.ts.map +1 -0
- package/dist/src/retrieval/git-state.js +174 -0
- package/dist/src/retrieval/git-state.js.map +1 -0
- package/dist/src/server/routes/friend-status.d.ts +63 -0
- package/dist/src/server/routes/friend-status.d.ts.map +1 -1
- package/dist/src/server/routes/friend-status.js +97 -0
- package/dist/src/server/routes/friend-status.js.map +1 -1
- 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":"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;
|
|
1
|
+
{"version":3,"file":"onboard.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/onboard.ts"],"names":[],"mappings":"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"}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
108
|
+
ui.step("ok", "Existing data", `carried over ${migration.migratedCount} item(s)`);
|
|
102
109
|
}
|
|
103
110
|
else {
|
|
104
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
148
|
+
ui.step("ok", "Chat history", `${claudeSessions} Claude Code · ${codexSessions} Codex CLI`);
|
|
130
149
|
// Step 5 — privacy + feedback opt-in.
|
|
131
|
-
|
|
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
|
-
|
|
154
|
+
ui.step("ok", "Feedback", `relay on (installation_id: ${feedback.installation_id})`);
|
|
137
155
|
}
|
|
138
156
|
else {
|
|
139
|
-
|
|
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 (
|
|
147
|
-
|
|
148
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
323
|
-
? "
|
|
324
|
-
: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
587
|
+
ui.step("skip", "Claude Code", "skipped (pass --with-claude-hook to install non-interactively)");
|
|
559
588
|
return;
|
|
560
589
|
}
|
|
561
590
|
else {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
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
|
-
|
|
612
|
+
ui.step("ok", "Claude Code", "connected · auto-recall hook added");
|
|
582
613
|
}
|
|
583
614
|
else if (outcome.hookEntryReplaced || outcome.scriptUpdated) {
|
|
584
|
-
|
|
615
|
+
ui.step("ok", "Claude Code", "updated");
|
|
585
616
|
}
|
|
586
617
|
else {
|
|
587
|
-
|
|
618
|
+
ui.step("ok", "Claude Code", "already up-to-date");
|
|
588
619
|
}
|
|
589
620
|
if (outcome.backupId) {
|
|
590
|
-
|
|
621
|
+
ui.note(`backup ID: ${outcome.backupId}`);
|
|
591
622
|
}
|
|
592
|
-
|
|
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
|
-
|
|
627
|
+
ui.step("fail", "Claude Code", err.message);
|
|
597
628
|
}
|
|
598
629
|
else {
|
|
599
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
759
|
-
"
|
|
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
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|
-
|
|
897
|
-
"
|
|
898
|
-
"
|
|
899
|
-
"
|
|
900
|
-
"
|
|
901
|
-
"
|
|
902
|
-
"
|
|
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) {
|