@getrift/rift 0.1.0-beta.12 → 0.1.0-beta.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -9
- package/dist/src/cli/commands/doctor.d.ts +6 -0
- package/dist/src/cli/commands/doctor.d.ts.map +1 -0
- package/dist/src/cli/commands/doctor.js +183 -0
- package/dist/src/cli/commands/doctor.js.map +1 -0
- package/dist/src/cli/commands/menubar.d.ts +30 -0
- package/dist/src/cli/commands/menubar.d.ts.map +1 -0
- package/dist/src/cli/commands/menubar.js +180 -0
- package/dist/src/cli/commands/menubar.js.map +1 -0
- 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 +203 -121
- package/dist/src/cli/commands/onboard.js.map +1 -1
- package/dist/src/cli/commands/status.d.ts +9 -7
- package/dist/src/cli/commands/status.d.ts.map +1 -1
- package/dist/src/cli/commands/status.js +29 -10
- package/dist/src/cli/commands/status.js.map +1 -1
- package/dist/src/cli/commands/update.d.ts +3 -0
- package/dist/src/cli/commands/update.d.ts.map +1 -1
- package/dist/src/cli/commands/update.js +19 -0
- package/dist/src/cli/commands/update.js.map +1 -1
- package/dist/src/cli/index.d.ts.map +1 -1
- package/dist/src/cli/index.js +4 -0
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/cli/postinstall-menubar.d.ts +22 -0
- package/dist/src/cli/postinstall-menubar.d.ts.map +1 -0
- package/dist/src/cli/postinstall-menubar.js +39 -0
- package/dist/src/cli/postinstall-menubar.js.map +1 -0
- 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/diagnostics/doctor.d.ts +106 -0
- package/dist/src/diagnostics/doctor.d.ts.map +1 -0
- package/dist/src/diagnostics/doctor.js +251 -0
- package/dist/src/diagnostics/doctor.js.map +1 -0
- package/dist/src/diagnostics/notify.d.ts +90 -0
- package/dist/src/diagnostics/notify.d.ts.map +1 -0
- package/dist/src/diagnostics/notify.js +177 -0
- package/dist/src/diagnostics/notify.js.map +1 -0
- package/dist/src/diagnostics/repair-prompt.d.ts +49 -0
- package/dist/src/diagnostics/repair-prompt.d.ts.map +1 -0
- package/dist/src/diagnostics/repair-prompt.js +198 -0
- package/dist/src/diagnostics/repair-prompt.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/main.js +43 -4
- package/dist/src/main.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/observability/version-check.d.ts +1 -0
- package/dist/src/observability/version-check.d.ts.map +1 -1
- package/dist/src/observability/version-check.js +2 -1
- package/dist/src/observability/version-check.js.map +1 -1
- 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/operator/swiftbar/render-menu.py +444 -0
- package/operator/swiftbar/rift.10s.sh +147 -0
- package/package.json +4 -1
|
@@ -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,23 @@ 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
|
}
|
|
113
|
+
// Privacy contract — shown BEFORE the first outbound choice (the
|
|
114
|
+
// Voyage key, whose snippets leave the machine for embedding). A beta
|
|
115
|
+
// user should understand what stays local and what leaves before they
|
|
116
|
+
// paste anything.
|
|
117
|
+
ui.step("info", "Privacy", "");
|
|
118
|
+
sayPrivacyContract();
|
|
106
119
|
// Step 2 — Voyage key + validate + persist + kickstart + smoke.
|
|
107
|
-
say("");
|
|
108
|
-
say("Voyage API key");
|
|
109
120
|
const last4 = await collectAndPersistVoyageKey(opts, globalOpts, rl);
|
|
110
121
|
// Step 2b — sanitize + persist optional --voyage-label to config.json
|
|
111
122
|
// (backup first). Invalid labels are dropped without echoing the raw
|
|
@@ -113,39 +124,53 @@ async function runOnboard(opts, globalOpts) {
|
|
|
113
124
|
// can never leak through stdout or config.json.
|
|
114
125
|
const safeLabel = applyVoyageLabel(opts.voyageLabel, globalOpts.config, say);
|
|
115
126
|
// Step 3 — Codex CLI preflight.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
//
|
|
128
|
+
// Codex CLI is only required by the auto-capture lane. Friends who
|
|
129
|
+
// use only Claude Desktop / Claude Code (and import via the inbox
|
|
130
|
+
// or `rift import`) should not be blocked by a missing/expired
|
|
131
|
+
// Codex auth. So a failed preflight is a warning, not fatal —
|
|
132
|
+
// onboarding continues and the capture pass is skipped for this
|
|
133
|
+
// run. `--no-codex-capture` skips the preflight entirely.
|
|
134
|
+
let captureDisabled = false;
|
|
135
|
+
if (isOff(opts, "codexCapture", "noCodexCapture")) {
|
|
136
|
+
ui.step("skip", "Chat access", "skipped (--no-codex-capture) · auto-import off");
|
|
137
|
+
captureDisabled = true;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
const codexSpin = new ui.Spinner("Chat access").start();
|
|
141
|
+
const codexOk = await codexPreflight();
|
|
142
|
+
if (codexOk) {
|
|
143
|
+
codexSpin.succeed("Chat access", "ready · Codex CLI");
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
codexSpin.fail("Chat access", "not ready · Codex CLI — auto-import off");
|
|
147
|
+
ui.note("To enable later: run `codex login`, then re-run `rift onboard`.");
|
|
148
|
+
captureDisabled = true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
125
151
|
// Step 4 — discover sessions.
|
|
126
|
-
say("");
|
|
127
152
|
const claudeSessions = safeDiscover(() => discoverClaudeCodeSessions(path.join(os.homedir(), ".claude")));
|
|
128
153
|
const codexSessions = safeDiscover(() => discoverCodexSessions());
|
|
129
|
-
|
|
130
|
-
// Step 5 — privacy
|
|
131
|
-
|
|
132
|
-
say("Privacy");
|
|
133
|
-
sayPrivacyContract();
|
|
134
|
-
const feedback = await collectFeedbackPreference(opts, rl, dataDir);
|
|
154
|
+
ui.step("ok", "Chat history", `${claudeSessions} Claude Code · ${codexSessions} Codex CLI`);
|
|
155
|
+
// Step 5 — feedback preference (privacy contract already shown above).
|
|
156
|
+
const feedback = await collectFeedbackPreference(opts, dataDir);
|
|
135
157
|
if (feedback.enabled) {
|
|
136
|
-
|
|
158
|
+
ui.step("ok", "Feedback", `relay on (installation_id: ${feedback.installation_id})`);
|
|
137
159
|
}
|
|
138
160
|
else {
|
|
139
|
-
|
|
161
|
+
ui.step("ok", "Feedback", "relay off — stays in local JSONL");
|
|
140
162
|
}
|
|
141
163
|
// Step 5b — optional Claude Code policy hook.
|
|
142
|
-
say("");
|
|
143
164
|
await maybeInstallClaudeCodeHook(opts, rl);
|
|
144
165
|
// Step 6 + 7 — watermark current sessions + run one capture pass.
|
|
145
166
|
let captureSaved = 0;
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
167
|
+
if (opts.skipCapture) {
|
|
168
|
+
ui.step("skip", "Chat import", "skipped (--skip-capture)");
|
|
169
|
+
}
|
|
170
|
+
else if (captureDisabled) {
|
|
171
|
+
ui.step("skip", "Chat import", "skipped (Codex CLI not ready)");
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
149
174
|
const captureResult = await runFirstCapturePass(globalOpts.config, dataDir);
|
|
150
175
|
captureSaved = captureResult.saved;
|
|
151
176
|
// Per A-1.1/C-1.3: a token-issuance failure during capture-pass
|
|
@@ -157,13 +182,12 @@ async function runOnboard(opts, globalOpts) {
|
|
|
157
182
|
return;
|
|
158
183
|
}
|
|
159
184
|
}
|
|
160
|
-
else {
|
|
161
|
-
say("Skipping capture pass (--skip-capture).");
|
|
162
|
-
}
|
|
163
185
|
// Step 8 — optional export import.
|
|
164
|
-
say("");
|
|
165
186
|
let importSucceeded = false;
|
|
166
|
-
if (
|
|
187
|
+
if (isOff(opts, "importExport", "noImportExport")) {
|
|
188
|
+
ui.step("skip", "File import", "skipped (--no-import-export)");
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
167
191
|
const importPath = await collectImportPath(opts, rl);
|
|
168
192
|
if (importPath) {
|
|
169
193
|
const outcome = await runImport(importPath, globalOpts.config);
|
|
@@ -172,30 +196,34 @@ async function runOnboard(opts, globalOpts) {
|
|
|
172
196
|
}
|
|
173
197
|
}
|
|
174
198
|
else {
|
|
175
|
-
|
|
199
|
+
ui.step("skip", "File import", "none — run `rift import <path> --source <name>` later");
|
|
176
200
|
}
|
|
177
201
|
}
|
|
178
|
-
else {
|
|
179
|
-
say("Skipping import (--no-import-export).");
|
|
180
|
-
}
|
|
181
202
|
// Step 9 — first-recall verification.
|
|
182
203
|
// Onboarding declares "complete" only when (a) capture or import landed
|
|
183
204
|
// user data this run, AND (b) a recall query against the daemon returns
|
|
184
205
|
// at least one hit. Without (a), there is nothing to recall — calling
|
|
185
206
|
// it "complete" because /search returned 0 rows would be misleading.
|
|
186
|
-
say("");
|
|
187
207
|
const ingestedAny = captureSaved > 0 || importSucceeded;
|
|
188
|
-
|
|
208
|
+
const recallSpin = new ui.Spinner("Search check").start();
|
|
189
209
|
const recall = ingestedAny
|
|
190
210
|
? await firstRecallCheck(globalOpts.config)
|
|
191
211
|
: { ok: false, reason: "no user data was captured or imported during onboarding" };
|
|
212
|
+
if (recall.ok && recall.hits > 0) {
|
|
213
|
+
recallSpin.succeed("Search check", `found ${recall.hits} result(s)`);
|
|
214
|
+
}
|
|
215
|
+
else if (recall.ok) {
|
|
216
|
+
// 0 hits is NOT success — decideOnboardOutcome treats it as incomplete.
|
|
217
|
+
recallSpin.warn("Search check", "no results yet — index is fresh, try a query shortly");
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
recallSpin.fail("Search check", recall.reason);
|
|
221
|
+
}
|
|
192
222
|
// Step 10 — next-action card.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
say(`Voyage: key valid (last 4 …${last4})${safeLabel ? ` · label ${safeLabel}` : ""}`);
|
|
198
|
-
say("");
|
|
223
|
+
const card = decideOnboardOutcome({ ingestedAny, recall });
|
|
224
|
+
card.push(ui.pc.dim(`Voyage: key valid (last 4 …${last4})${safeLabel ? ` · label ${safeLabel}` : ""}`));
|
|
225
|
+
ui.box(card);
|
|
226
|
+
ui.line("");
|
|
199
227
|
}
|
|
200
228
|
finally {
|
|
201
229
|
rl.close();
|
|
@@ -204,10 +232,11 @@ async function runOnboard(opts, globalOpts) {
|
|
|
204
232
|
// ----- Step 1: config.json + data dir -----
|
|
205
233
|
async function ensureConfigAndDataDir(configPath, opts, rl) {
|
|
206
234
|
const absoluteConfig = path.resolve(configPath);
|
|
235
|
+
const shownConfig = absoluteConfig.replace(os.homedir(), "~");
|
|
207
236
|
if (fs.existsSync(absoluteConfig)) {
|
|
208
237
|
const config = loadConfig(absoluteConfig);
|
|
209
|
-
|
|
210
|
-
|
|
238
|
+
ui.step("ok", "Settings", shownConfig);
|
|
239
|
+
ui.note(`data dir: ${config.data_paths.data_dir}`);
|
|
211
240
|
return config.data_paths.data_dir;
|
|
212
241
|
}
|
|
213
242
|
const defaultDataDir = DEFAULT_DATA_DIR;
|
|
@@ -237,8 +266,8 @@ async function ensureConfigAndDataDir(configPath, opts, rl) {
|
|
|
237
266
|
encoding: "utf8",
|
|
238
267
|
mode: 0o644,
|
|
239
268
|
});
|
|
240
|
-
|
|
241
|
-
|
|
269
|
+
ui.step("ok", "Settings", `wrote default · ${shownConfig}`);
|
|
270
|
+
ui.note(`data dir: ${dataDir}`);
|
|
242
271
|
// Trigger ensureDirectories.
|
|
243
272
|
loadConfig(absoluteConfig);
|
|
244
273
|
return dataDir;
|
|
@@ -311,17 +340,18 @@ export function persistVoyageLabel(configPath, label) {
|
|
|
311
340
|
// ----- Step 2: Voyage key flow -----
|
|
312
341
|
async function collectAndPersistVoyageKey(opts, globalOpts, rl) {
|
|
313
342
|
const key = await collectVoyageKey(opts, rl);
|
|
314
|
-
|
|
343
|
+
const validateSpin = new ui.Spinner("Search index").start();
|
|
315
344
|
const validation = await validateVoyageKey({ apiKey: key });
|
|
316
345
|
if (!validation.ok) {
|
|
346
|
+
validateSpin.fail("Search index", "validation failed");
|
|
317
347
|
throw new CliError(`Voyage validation failed: ${validation.reason}`, "validation");
|
|
318
348
|
}
|
|
319
|
-
|
|
349
|
+
validateSpin.succeed("Search index", `connected · Voyage · ····${validation.last4}`);
|
|
320
350
|
const envPath = defaultRiftEnvPath();
|
|
321
351
|
const writeResult = writeEnvFile({ filePath: envPath, key: "VOYAGE_API_KEY", value: key });
|
|
322
|
-
|
|
323
|
-
? "
|
|
324
|
-
: "
|
|
352
|
+
ui.note(writeResult.backedUp
|
|
353
|
+
? "wrote ~/.rift.env (existing file backed up)"
|
|
354
|
+
: "wrote ~/.rift.env (mode 0600)");
|
|
325
355
|
// Refresh process.env so any subsequent in-process Voyage call sees it.
|
|
326
356
|
loadRiftEnv({ filePath: envPath });
|
|
327
357
|
const refresh = await daemonRefreshFlow(globalOpts);
|
|
@@ -342,6 +372,14 @@ async function collectVoyageKey(opts, rl) {
|
|
|
342
372
|
if (opts.yes) {
|
|
343
373
|
throw new CliError("--yes given but no Voyage key found (use --voyage-key or set VOYAGE_API_KEY).", "validation");
|
|
344
374
|
}
|
|
375
|
+
// Explain the key BEFORE asking for it — a beta user should know what
|
|
376
|
+
// they are pasting and why, not be confronted with a bare prompt.
|
|
377
|
+
ui.detail([
|
|
378
|
+
"Search index key",
|
|
379
|
+
"• What: paste the Voyage key Clem sent you (it won't echo as you type).",
|
|
380
|
+
"• Why: Rift uses Voyage to make your archive searchable by meaning.",
|
|
381
|
+
"• Privacy: the key is stored locally and only sent to Voyage when Rift calls Voyage; never sent to Clem.",
|
|
382
|
+
].join("\n"));
|
|
345
383
|
const answer = (await ask(rl, "Paste your Voyage API key: ")).trim();
|
|
346
384
|
if (answer.length === 0) {
|
|
347
385
|
throw new CliError("Voyage key is required.", "validation");
|
|
@@ -359,35 +397,38 @@ async function collectVoyageKey(opts, rl) {
|
|
|
359
397
|
async function daemonRefreshFlow(globalOpts) {
|
|
360
398
|
const baseUrl = safeResolveBaseUrl(globalOpts.config);
|
|
361
399
|
if (!baseUrl) {
|
|
362
|
-
|
|
400
|
+
ui.step("skip", "Rift service", "not configured yet — bootstrap via install.sh, then re-run");
|
|
363
401
|
return { ok: false, reason: "daemon not configured", kind: "not_configured" };
|
|
364
402
|
}
|
|
403
|
+
const daemonSpin = new ui.Spinner("Rift service").start();
|
|
365
404
|
const kick = await kickstartDaemon();
|
|
366
405
|
if (kick.status === "agent_not_loaded") {
|
|
367
|
-
|
|
406
|
+
daemonSpin.fail("Rift service", "launchd agent not loaded");
|
|
407
|
+
ui.note(kick.hint);
|
|
368
408
|
return { ok: false, reason: kick.hint ?? "agent not loaded", kind: "agent_not_loaded" };
|
|
369
409
|
}
|
|
370
410
|
if (kick.status === "failed") {
|
|
411
|
+
daemonSpin.fail("Rift service", "kickstart failed");
|
|
371
412
|
return {
|
|
372
413
|
ok: false,
|
|
373
414
|
reason: kick.hint ?? "kickstart failed",
|
|
374
415
|
kind: "kickstart_failed",
|
|
375
416
|
};
|
|
376
417
|
}
|
|
377
|
-
say("Daemon kickstarted.");
|
|
378
418
|
const health = await waitForHealth({ baseUrl });
|
|
379
419
|
if (!health.ok) {
|
|
420
|
+
daemonSpin.fail("Rift service", "health check failed");
|
|
380
421
|
return { ok: false, reason: health.reason, kind: "health_failed" };
|
|
381
422
|
}
|
|
382
|
-
say(`Daemon healthy (uptime ${health.uptimeSeconds}s).`);
|
|
383
423
|
if (!health.voyageKeyPresent) {
|
|
424
|
+
daemonSpin.fail("Rift service", "voyage_key_present=false after kickstart");
|
|
384
425
|
return {
|
|
385
426
|
ok: false,
|
|
386
427
|
reason: "Daemon /health reports voyage_key_present=false after kickstart.",
|
|
387
428
|
kind: "smoke_failed",
|
|
388
429
|
};
|
|
389
430
|
}
|
|
390
|
-
|
|
431
|
+
daemonSpin.succeed("Rift service", `healthy · daemon · uptime ${health.uptimeSeconds}s`);
|
|
391
432
|
return { ok: true, kickstarted: true };
|
|
392
433
|
}
|
|
393
434
|
export function classifyTokenFailure(err) {
|
|
@@ -490,30 +531,18 @@ async function codexPreflight() {
|
|
|
490
531
|
return false;
|
|
491
532
|
}
|
|
492
533
|
}
|
|
493
|
-
async function collectFeedbackPreference(opts,
|
|
534
|
+
async function collectFeedbackPreference(opts, dataDir) {
|
|
494
535
|
let enabled = false;
|
|
495
536
|
let url;
|
|
537
|
+
// Default onboarding keeps feedback fully local (JSONL). There is no
|
|
538
|
+
// bundled hosted relay endpoint, so we never ask a beta user to paste a
|
|
539
|
+
// "Relay URL:" — that field is operator-only infrastructure. The relay
|
|
540
|
+
// is reachable solely via the advanced, non-interactive
|
|
541
|
+
// `--enable-feedback-relay <url>` flag.
|
|
496
542
|
if (opts.enableFeedbackRelay) {
|
|
497
543
|
enabled = true;
|
|
498
544
|
url = opts.enableFeedbackRelay;
|
|
499
545
|
}
|
|
500
|
-
else if (opts.noFeedbackRelay || opts.yes) {
|
|
501
|
-
enabled = false;
|
|
502
|
-
}
|
|
503
|
-
else {
|
|
504
|
-
const answer = (await ask(rl, "Opt into the Rift feedback relay? [y/N]: ", "n"))
|
|
505
|
-
.trim()
|
|
506
|
-
.toLowerCase();
|
|
507
|
-
enabled = answer === "y" || answer === "yes";
|
|
508
|
-
if (enabled) {
|
|
509
|
-
url = (await ask(rl, "Relay URL: ")).trim();
|
|
510
|
-
if (!url) {
|
|
511
|
-
say("No URL supplied — keeping relay off.");
|
|
512
|
-
enabled = false;
|
|
513
|
-
url = undefined;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
546
|
const cfg = enabled && url
|
|
518
547
|
? { enabled: true, url, installation_id: crypto.randomUUID() }
|
|
519
548
|
: { enabled: false };
|
|
@@ -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,43 @@ 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
|
+
// Explain what a path is, where to find the export, why it's optional,
|
|
727
|
+
// and that ChatGPT is the only inline source — BEFORE the prompt. A beta
|
|
728
|
+
// user who has never exported a chat archive shouldn't hit a bare
|
|
729
|
+
// "Drop a path" prompt. The source-sniffer below still fail-fasts a
|
|
730
|
+
// non-ChatGPT export, but the friend learns the boundary earlier here.
|
|
731
|
+
ui.detail([
|
|
732
|
+
"Import past chats (optional)",
|
|
733
|
+
"• What: the file path to a ChatGPT data export (a .zip or .json on your Mac), e.g. ~/Downloads/chatgpt-export.zip.",
|
|
734
|
+
"• Where: chatgpt.com → Settings → Data controls → Export data, then unzip nothing — just drop the downloaded file's path here.",
|
|
735
|
+
"• Why: this seeds Rift with your existing ChatGPT history so search works on day one; skipping is fine — new chats get captured automatically.",
|
|
736
|
+
"• Other tools: Claude/Grok/Gemini exports aren't imported here. Run them later with: rift import <path> --source claude_web|grok_web|gemini_web",
|
|
737
|
+
].join("\n"));
|
|
738
|
+
const answer = (await ask(rl, "Path to a ChatGPT export to import now (or `skip`): ", "skip")).trim();
|
|
685
739
|
if (!answer || answer.toLowerCase() === "skip")
|
|
686
740
|
return null;
|
|
687
741
|
return answer;
|
|
@@ -711,11 +765,20 @@ async function runImport(filePath, configPath) {
|
|
|
711
765
|
}
|
|
712
766
|
const client = createHttpClient({ baseUrl, token: tokenResult.token });
|
|
713
767
|
const buf = fs.readFileSync(absolute);
|
|
768
|
+
// Fail-fast for non-ChatGPT exports: route them to `rift import` with
|
|
769
|
+
// the right `--source` flag instead of silently feeding them through
|
|
770
|
+
// the ChatGPT parser. Returns null on "looks like ChatGPT or unknown,"
|
|
771
|
+
// a concrete provider otherwise.
|
|
772
|
+
const sniffed = sniffInboxSource(path.basename(absolute), buf);
|
|
773
|
+
if (sniffed) {
|
|
774
|
+
say(`Detected a ${sniffed} export — the inline importer is ChatGPT-only.`);
|
|
775
|
+
say(`Run: rift import "${absolute}" --source ${sniffed}`);
|
|
776
|
+
return { kind: "skipped", reason: `non-chatgpt export detected: ${sniffed}` };
|
|
777
|
+
}
|
|
714
778
|
const form = new FormData();
|
|
715
779
|
form.append("source", ONBOARD_INLINE_IMPORT_SOURCE);
|
|
716
780
|
form.append("file", new Blob([buf]), path.basename(absolute));
|
|
717
781
|
say(`Importing ${path.basename(absolute)} as ${ONBOARD_INLINE_IMPORT_SOURCE}…`);
|
|
718
|
-
say("(For Claude/Grok/Gemini exports, run: rift import <path> --source <name>)");
|
|
719
782
|
const { data } = await client.postMultipart("/ingest", form);
|
|
720
783
|
const resp = data;
|
|
721
784
|
if (resp.duplicate) {
|
|
@@ -755,8 +818,10 @@ export function decideOnboardOutcome(input) {
|
|
|
755
818
|
}
|
|
756
819
|
if (recall.ok) {
|
|
757
820
|
return [
|
|
758
|
-
"Setup
|
|
759
|
-
"
|
|
821
|
+
"Setup ready — capture+import succeeded but the generic recall query returned 0 hits.",
|
|
822
|
+
"Indexing may still be in flight, or this query just didn't match your archive yet.",
|
|
823
|
+
"Try: rift search \"<a topic you remember discussing>\"",
|
|
824
|
+
"If repeated tries still return nothing, run: rift feedback --kind=broke --with-status",
|
|
760
825
|
];
|
|
761
826
|
}
|
|
762
827
|
return [
|
|
@@ -776,19 +841,35 @@ async function firstRecallCheck(configPath) {
|
|
|
776
841
|
// sessions or imported chat exports without targeting any onboarding
|
|
777
842
|
// marker. The smoke is non-indexing (see daemonRefreshFlow), so any
|
|
778
843
|
// hit returned here is real user data, not a leftover probe.
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
844
|
+
//
|
|
845
|
+
// Small unusual imports (e.g. one short Claude project export) can
|
|
846
|
+
// return 0 hits for this generic query even when the data was
|
|
847
|
+
// indexed correctly. Retry briefly so the post-onboard nudge isn't a
|
|
848
|
+
// false alarm on slow embedders / sparse archives.
|
|
849
|
+
const RECALL_RETRY_DELAYS_MS = [0, 5_000];
|
|
850
|
+
let lastErr = null;
|
|
851
|
+
for (const delay of RECALL_RETRY_DELAYS_MS) {
|
|
852
|
+
if (delay > 0)
|
|
853
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
854
|
+
try {
|
|
855
|
+
const { data } = await client.post("/search", {
|
|
856
|
+
query: "recent conversation",
|
|
857
|
+
scope: "all",
|
|
858
|
+
top_k: 10,
|
|
859
|
+
});
|
|
860
|
+
const body = data;
|
|
861
|
+
const hits = (body.results?.length ?? body.hits?.length ?? 0);
|
|
862
|
+
if (hits > 0)
|
|
863
|
+
return { ok: true, hits };
|
|
864
|
+
lastErr = null;
|
|
865
|
+
}
|
|
866
|
+
catch (err) {
|
|
867
|
+
lastErr = err instanceof Error ? err.message : String(err);
|
|
868
|
+
}
|
|
791
869
|
}
|
|
870
|
+
if (lastErr)
|
|
871
|
+
return { ok: false, reason: lastErr };
|
|
872
|
+
return { ok: true, hits: 0 };
|
|
792
873
|
}
|
|
793
874
|
// ----- Slice 6: --reconfigure-voyage -----
|
|
794
875
|
/**
|
|
@@ -893,13 +974,14 @@ function say(line) {
|
|
|
893
974
|
process.stdout.write(line + "\n");
|
|
894
975
|
}
|
|
895
976
|
function sayPrivacyContract() {
|
|
896
|
-
|
|
897
|
-
"
|
|
898
|
-
"
|
|
899
|
-
"
|
|
900
|
-
"
|
|
901
|
-
"
|
|
902
|
-
"
|
|
977
|
+
ui.detail([
|
|
978
|
+
"• Conversation content stays local (LanceDB + raw transcripts).",
|
|
979
|
+
"• Content snippets leave the machine for embedding only (Voyage AI).",
|
|
980
|
+
"• The Voyage key sits in ~/.rift.env (mode 0600), is sent only to Voyage,",
|
|
981
|
+
" and is never logged or sent to Clem.",
|
|
982
|
+
"• Feedback is stored locally as JSONL. Relay is opt-in: explicit notes only,",
|
|
983
|
+
" plus daemon health booleans — no paths, no content, no key bytes.",
|
|
984
|
+
"• Full contract: https://getrift.dev/privacy",
|
|
903
985
|
].join("\n"));
|
|
904
986
|
}
|
|
905
987
|
function safeDiscover(fn) {
|