@getrift/rift 0.1.0-beta.17 → 0.1.0-beta.19
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/capture/auto-repair.d.ts +110 -0
- package/dist/src/capture/auto-repair.d.ts.map +1 -0
- package/dist/src/capture/auto-repair.js +269 -0
- package/dist/src/capture/auto-repair.js.map +1 -0
- package/dist/src/capture/codex-cli-triage-provider.d.ts.map +1 -1
- package/dist/src/capture/codex-cli-triage-provider.js +4 -3
- package/dist/src/capture/codex-cli-triage-provider.js.map +1 -1
- package/dist/src/capture/recover-quarantine.d.ts +11 -0
- package/dist/src/capture/recover-quarantine.d.ts.map +1 -1
- package/dist/src/capture/recover-quarantine.js +30 -4
- package/dist/src/capture/recover-quarantine.js.map +1 -1
- package/dist/src/cli/commands/capture-recover.d.ts.map +1 -1
- package/dist/src/cli/commands/capture-recover.js +7 -0
- package/dist/src/cli/commands/capture-recover.js.map +1 -1
- package/dist/src/cli/commands/capture.d.ts.map +1 -1
- package/dist/src/cli/commands/capture.js +48 -1
- package/dist/src/cli/commands/capture.js.map +1 -1
- package/dist/src/cli/commands/feedback.d.ts +12 -0
- package/dist/src/cli/commands/feedback.d.ts.map +1 -1
- package/dist/src/cli/commands/feedback.js +87 -2
- package/dist/src/cli/commands/feedback.js.map +1 -1
- package/dist/src/cli/commands/onboard.d.ts +68 -1
- package/dist/src/cli/commands/onboard.d.ts.map +1 -1
- package/dist/src/cli/commands/onboard.js +415 -54
- package/dist/src/cli/commands/onboard.js.map +1 -1
- package/dist/src/cli/commands/search.d.ts +2 -0
- package/dist/src/cli/commands/search.d.ts.map +1 -1
- package/dist/src/cli/commands/search.js +6 -0
- package/dist/src/cli/commands/search.js.map +1 -1
- package/dist/src/cli/feedback/feedback-config.d.ts +46 -0
- package/dist/src/cli/feedback/feedback-config.d.ts.map +1 -1
- package/dist/src/cli/feedback/feedback-config.js +130 -4
- package/dist/src/cli/feedback/feedback-config.js.map +1 -1
- package/dist/src/cli/feedback/feedback-history.d.ts +7 -0
- package/dist/src/cli/feedback/feedback-history.d.ts.map +1 -1
- package/dist/src/cli/feedback/feedback-history.js +39 -9
- package/dist/src/cli/feedback/feedback-history.js.map +1 -1
- package/dist/src/cli/feedback/feedback-payload.d.ts +22 -1
- package/dist/src/cli/feedback/feedback-payload.d.ts.map +1 -1
- package/dist/src/cli/feedback/feedback-payload.js.map +1 -1
- package/dist/src/cli/feedback/feedback-relay.d.ts +2 -2
- package/dist/src/cli/feedback/feedback-relay.d.ts.map +1 -1
- package/dist/src/cli/feedback/feedback-relay.js.map +1 -1
- package/dist/src/cli/feedback/invite.d.ts +17 -0
- package/dist/src/cli/feedback/invite.d.ts.map +1 -0
- package/dist/src/cli/feedback/invite.js +67 -0
- package/dist/src/cli/feedback/invite.js.map +1 -0
- package/dist/src/cli/feedback/relay-secret-store.d.ts +32 -0
- package/dist/src/cli/feedback/relay-secret-store.d.ts.map +1 -0
- package/dist/src/cli/feedback/relay-secret-store.js +137 -0
- package/dist/src/cli/feedback/relay-secret-store.js.map +1 -0
- package/dist/src/cli/http-client.d.ts +17 -4
- package/dist/src/cli/http-client.d.ts.map +1 -1
- package/dist/src/cli/http-client.js +18 -7
- package/dist/src/cli/http-client.js.map +1 -1
- package/dist/src/cli/status/friend-header.d.ts.map +1 -1
- package/dist/src/cli/status/friend-header.js +75 -23
- package/dist/src/cli/status/friend-header.js.map +1 -1
- package/dist/src/config/schema.d.ts +79 -0
- package/dist/src/config/schema.d.ts.map +1 -1
- package/dist/src/config/schema.js +44 -0
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/diagnostics/codex-preflight.d.ts +33 -0
- package/dist/src/diagnostics/codex-preflight.d.ts.map +1 -0
- package/dist/src/diagnostics/codex-preflight.js +63 -0
- package/dist/src/diagnostics/codex-preflight.js.map +1 -0
- package/dist/src/diagnostics/doctor.d.ts +1 -1
- package/dist/src/diagnostics/doctor.d.ts.map +1 -1
- package/dist/src/diagnostics/doctor.js +69 -20
- package/dist/src/diagnostics/doctor.js.map +1 -1
- package/dist/src/diagnostics/repair-prompt.d.ts.map +1 -1
- package/dist/src/diagnostics/repair-prompt.js +10 -1
- package/dist/src/diagnostics/repair-prompt.js.map +1 -1
- package/dist/src/jobs/handlers/compact.js +4 -0
- package/dist/src/jobs/handlers/compact.js.map +1 -1
- package/dist/src/jobs/handlers/ingest.d.ts.map +1 -1
- package/dist/src/jobs/handlers/ingest.js +53 -3
- package/dist/src/jobs/handlers/ingest.js.map +1 -1
- package/dist/src/jobs/handlers/reconcile.d.ts.map +1 -1
- package/dist/src/jobs/handlers/reconcile.js +7 -0
- package/dist/src/jobs/handlers/reconcile.js.map +1 -1
- package/dist/src/jobs/handlers/save.d.ts.map +1 -1
- package/dist/src/jobs/handlers/save.js +10 -0
- package/dist/src/jobs/handlers/save.js.map +1 -1
- package/dist/src/jobs/worker-entry.d.ts.map +1 -1
- package/dist/src/jobs/worker-entry.js +33 -7
- package/dist/src/jobs/worker-entry.js.map +1 -1
- package/dist/src/jobs/worker-process.d.ts +11 -0
- package/dist/src/jobs/worker-process.d.ts.map +1 -1
- package/dist/src/jobs/worker-process.js +37 -4
- package/dist/src/jobs/worker-process.js.map +1 -1
- package/dist/src/main.js +122 -42
- package/dist/src/main.js.map +1 -1
- package/dist/src/mcp/tools/search.d.ts.map +1 -1
- package/dist/src/mcp/tools/search.js +1 -0
- package/dist/src/mcp/tools/search.js.map +1 -1
- package/dist/src/observability/onboarding-metric.d.ts +16 -0
- package/dist/src/observability/onboarding-metric.d.ts.map +1 -1
- package/dist/src/observability/onboarding-metric.js +8 -1
- package/dist/src/observability/onboarding-metric.js.map +1 -1
- package/dist/src/providers/basic-metadata-extraction.d.ts +60 -0
- package/dist/src/providers/basic-metadata-extraction.d.ts.map +1 -0
- package/dist/src/providers/basic-metadata-extraction.js +114 -0
- package/dist/src/providers/basic-metadata-extraction.js.map +1 -0
- package/dist/src/providers/codex-cli-metadata-extraction.d.ts +1 -0
- package/dist/src/providers/codex-cli-metadata-extraction.d.ts.map +1 -1
- package/dist/src/providers/codex-cli-metadata-extraction.js +6 -2
- package/dist/src/providers/codex-cli-metadata-extraction.js.map +1 -1
- package/dist/src/providers/codex-cli-model.d.ts +61 -0
- package/dist/src/providers/codex-cli-model.d.ts.map +1 -0
- package/dist/src/providers/codex-cli-model.js +194 -0
- package/dist/src/providers/codex-cli-model.js.map +1 -0
- package/dist/src/providers/codex-cli-runner.d.ts +25 -0
- package/dist/src/providers/codex-cli-runner.d.ts.map +1 -1
- package/dist/src/providers/codex-cli-runner.js +78 -12
- package/dist/src/providers/codex-cli-runner.js.map +1 -1
- package/dist/src/providers/conversation-generation.d.ts.map +1 -1
- package/dist/src/providers/conversation-generation.js +43 -6
- package/dist/src/providers/conversation-generation.js.map +1 -1
- package/dist/src/providers/placeholder-embed.d.ts +56 -0
- package/dist/src/providers/placeholder-embed.d.ts.map +1 -0
- package/dist/src/providers/placeholder-embed.js +64 -0
- package/dist/src/providers/placeholder-embed.js.map +1 -0
- package/dist/src/retrieval/compact.d.ts +1 -0
- package/dist/src/retrieval/compact.d.ts.map +1 -1
- package/dist/src/retrieval/compact.js +4 -0
- package/dist/src/retrieval/compact.js.map +1 -1
- package/dist/src/retrieval/lexical.d.ts.map +1 -1
- package/dist/src/retrieval/lexical.js +19 -3
- package/dist/src/retrieval/lexical.js.map +1 -1
- package/dist/src/retrieval/vector.d.ts.map +1 -1
- package/dist/src/retrieval/vector.js +11 -2
- package/dist/src/retrieval/vector.js.map +1 -1
- package/dist/src/server/app.d.ts.map +1 -1
- package/dist/src/server/app.js +26 -17
- package/dist/src/server/app.js.map +1 -1
- package/dist/src/server/routes/conversations-search.d.ts.map +1 -1
- package/dist/src/server/routes/conversations-search.js +28 -3
- package/dist/src/server/routes/conversations-search.js.map +1 -1
- package/dist/src/server/routes/friend-status.d.ts +40 -0
- package/dist/src/server/routes/friend-status.d.ts.map +1 -1
- package/dist/src/server/routes/friend-status.js +52 -11
- package/dist/src/server/routes/friend-status.js.map +1 -1
- package/dist/src/server/routes/ingest.js +1 -1
- package/dist/src/server/routes/ingest.js.map +1 -1
- package/dist/src/server/routes/search.d.ts.map +1 -1
- package/dist/src/server/routes/search.js +65 -4
- package/dist/src/server/routes/search.js.map +1 -1
- package/dist/src/storage/rebuild.d.ts.map +1 -1
- package/dist/src/storage/rebuild.js +7 -0
- package/dist/src/storage/rebuild.js.map +1 -1
- package/dist/src/storage/tables.d.ts +20 -0
- package/dist/src/storage/tables.d.ts.map +1 -1
- package/dist/src/storage/tables.js +22 -12
- package/dist/src/storage/tables.js.map +1 -1
- package/package.json +1 -1
|
@@ -24,7 +24,7 @@ import fs from "node:fs";
|
|
|
24
24
|
import os from "node:os";
|
|
25
25
|
import path from "node:path";
|
|
26
26
|
import readline from "node:readline";
|
|
27
|
-
import { Command } from "commander";
|
|
27
|
+
import { Command, Option } from "commander";
|
|
28
28
|
import { CliError, createHttpClient, readToken, resolveBaseUrl, } from "../http-client.js";
|
|
29
29
|
import { issueToken } from "../token.js";
|
|
30
30
|
import { loadConfig } from "../../config/loader.js";
|
|
@@ -46,6 +46,11 @@ import { HooksParseFailedError } from "../hooks-writers/index.js";
|
|
|
46
46
|
import { pollJob } from "../job-poller.js";
|
|
47
47
|
import { isJobFailure } from "../output.js";
|
|
48
48
|
import * as ui from "../ui.js";
|
|
49
|
+
import { getBuildInfo } from "../../server/build-info.js";
|
|
50
|
+
import { postFeedback } from "../feedback/feedback-relay.js";
|
|
51
|
+
import { parseInvite } from "../feedback/invite.js";
|
|
52
|
+
import { macRelaySecretStore, } from "../feedback/relay-secret-store.js";
|
|
53
|
+
import { LockedKeychainError } from "../../auth/keychain.js";
|
|
49
54
|
/**
|
|
50
55
|
* Resolve a `--no-<flag>` to a single boolean ("yes, skip this thing").
|
|
51
56
|
* Commander populates the affirmative key with `false`; programmatic
|
|
@@ -64,9 +69,21 @@ export function makeOnboardCommand() {
|
|
|
64
69
|
return new Command("onboard")
|
|
65
70
|
.description("First-run wizard: validate Voyage key, capture, import, recall test")
|
|
66
71
|
.option("--voyage-key <key>", "Voyage API key (skips paste prompt)")
|
|
72
|
+
.option("--no-voyage-key", "Finish without a Voyage key — keyword-only (lexical) search; add a key later")
|
|
67
73
|
.option("--voyage-label <label>", "Operator-supplied display label for the Voyage project")
|
|
74
|
+
.option("--invite <code>", "Accept a feedback invite from Clem (or omit it to paste when asked)")
|
|
68
75
|
.option("--enable-feedback-relay <url>", "Opt into the Rift feedback relay non-interactively (URL required)")
|
|
76
|
+
.addOption(
|
|
77
|
+
// Hidden: back-compat operator flag. The secret never appears in
|
|
78
|
+
// user-facing help; friends use --invite (or the interactive paste).
|
|
79
|
+
new Option("--relay-secret <secret>", "Back-compat HMAC secret paired with --enable-feedback-relay (stored in the Keychain)").hideHelp())
|
|
80
|
+
.addOption(
|
|
81
|
+
// Hidden: dev/operator escape hatch for unsigned local receivers.
|
|
82
|
+
new Option("--allow-unsigned-feedback-relay", "Dev-only: allow --enable-feedback-relay without a secret (receiver rejects in prod)")
|
|
83
|
+
.hideHelp()
|
|
84
|
+
.default(false))
|
|
69
85
|
.option("--no-feedback-relay", "Decline the feedback relay (local-only)")
|
|
86
|
+
.option("--email <address>", "Opt into beta updates + feedback non-interactively (skips the prompt)")
|
|
70
87
|
.option("--import-export <path>", "Import an export inline (.json or .zip)")
|
|
71
88
|
.option("--no-import-export", "Skip the import-now prompt")
|
|
72
89
|
.option("--reconfigure-voyage", "Recovery flow: replace the Voyage key only", false)
|
|
@@ -75,6 +92,8 @@ export function makeOnboardCommand() {
|
|
|
75
92
|
.option("--no-codex-capture", "Skip the Codex CLI preflight + disable the capture pass for this run")
|
|
76
93
|
.option("--with-claude-hook", "Install the Rift policy hook into Claude Code without prompting", false)
|
|
77
94
|
.option("--no-claude-hook", "Skip the Claude Code policy-hook prompt entirely")
|
|
95
|
+
.option("--enable-codex-enrichment", "Opt into Codex AI metadata enrichment (default: AI-free import + keyword search)", false)
|
|
96
|
+
.option("--enable-capture", "Opt into scheduled chat capture + its Codex preflight (default: capture off, zero Codex calls)", false)
|
|
78
97
|
.action(async (opts, cmd) => {
|
|
79
98
|
const globalOpts = cmd.optsWithGlobals();
|
|
80
99
|
try {
|
|
@@ -91,7 +110,9 @@ export function makeOnboardCommand() {
|
|
|
91
110
|
}
|
|
92
111
|
});
|
|
93
112
|
}
|
|
94
|
-
|
|
113
|
+
// Exported for orchestrator-level tests (e.g. the capture-without-key gate);
|
|
114
|
+
// `makeOnboardCommand().action` is the only production caller.
|
|
115
|
+
export async function runOnboard(opts, globalOpts) {
|
|
95
116
|
const rl = readline.createInterface({
|
|
96
117
|
input: process.stdin,
|
|
97
118
|
output: process.stdout,
|
|
@@ -118,23 +139,79 @@ async function runOnboard(opts, globalOpts) {
|
|
|
118
139
|
// paste anything.
|
|
119
140
|
ui.step("info", "Privacy", "");
|
|
120
141
|
sayPrivacyContract();
|
|
121
|
-
// Step 2 — Voyage key + validate + persist
|
|
122
|
-
|
|
142
|
+
// Step 2 — Voyage key + validate + persist env. The daemon kickstart +
|
|
143
|
+
// smoke is deferred to Step 2e (after the opt-in writes below) so the
|
|
144
|
+
// respawned daemon boots with the final config.json.
|
|
145
|
+
const last4 = await collectAndPersistVoyageKey(opts, rl);
|
|
123
146
|
// Step 2b — sanitize + persist optional --voyage-label to config.json
|
|
124
147
|
// (backup first). Invalid labels are dropped without echoing the raw
|
|
125
148
|
// value, so a key-shaped, path-shaped, or legacy-name-shaped label
|
|
126
149
|
// can never leak through stdout or config.json.
|
|
127
150
|
const safeLabel = applyVoyageLabel(opts.voyageLabel, globalOpts.config, say);
|
|
128
|
-
// Step
|
|
151
|
+
// Step 2c — optional opt-in to Codex AI metadata enrichment. Default is
|
|
152
|
+
// AI-free import + keyword search (zero Codex calls). Only persisted when
|
|
153
|
+
// the user explicitly passed --enable-codex-enrichment.
|
|
154
|
+
applyCodexEnrichmentOptIn(opts.enableCodexEnrichment, globalOpts.config, say);
|
|
155
|
+
// Step 2d — optional opt-in to scheduled chat capture. Default off, so a
|
|
156
|
+
// fresh install makes zero Codex calls. Auto-capture embeds each saved
|
|
157
|
+
// conversation, so it requires a Voyage key; opting in without one would
|
|
158
|
+
// persist `capture.enabled = true` that the daemon can't honor (it would
|
|
159
|
+
// wake hourly, find no embedding provider, and save nothing). So we only
|
|
160
|
+
// persist the opt-in when a key was provided, and warn otherwise.
|
|
161
|
+
// `captureEnabled` is the single source of truth for the capture lane:
|
|
162
|
+
// requested AND a key is present. It gates BOTH the persistence here and
|
|
163
|
+
// the Codex preflight in Step 3 — refusing the opt-in must also skip the
|
|
164
|
+
// preflight's Codex triage call, or `--enable-capture` without a key would
|
|
165
|
+
// still leak a Codex call.
|
|
166
|
+
const captureRequested = opts.enableCapture === true;
|
|
167
|
+
const captureEnabled = captureRequested && last4 !== null;
|
|
168
|
+
if (captureRequested && last4 === null) {
|
|
169
|
+
ui.note("--enable-capture ignored: capture embeds saved conversations and needs a Voyage key. Re-run `rift onboard` with a key to enable it.");
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
applyCaptureOptIn(opts.enableCapture, globalOpts.config, say);
|
|
173
|
+
}
|
|
174
|
+
// Step 2e — (re)start the daemon AFTER all config writes so it boots with
|
|
175
|
+
// the final config.json. The daemon builds its capture loop and metadata
|
|
176
|
+
// extractor once at boot (src/main.ts), so persisting the opt-ins above
|
|
177
|
+
// BEFORE this kickstart is what makes --enable-capture /
|
|
178
|
+
// --enable-codex-enrichment take effect immediately instead of only after
|
|
179
|
+
// a later restart. Key path only: the smoke asserts voyage_key_present,
|
|
180
|
+
// and the keyword-only path has no daemon smoke to run (the daemon was
|
|
181
|
+
// bootstrapped by install.sh and picks up config on its next start).
|
|
182
|
+
// `not_configured` / `agent_not_loaded` stay recoverable — the daemon
|
|
183
|
+
// simply isn't running yet on a fresh-Mac install before install.sh
|
|
184
|
+
// bootstraps the plist.
|
|
185
|
+
if (last4 !== null) {
|
|
186
|
+
const refresh = await daemonRefreshFlow(globalOpts);
|
|
187
|
+
if (!refresh.ok &&
|
|
188
|
+
refresh.kind !== "not_configured" &&
|
|
189
|
+
refresh.kind !== "agent_not_loaded") {
|
|
190
|
+
throw new CliError(`Voyage smoke failed after daemon kickstart: ${refresh.reason}`, "server_error");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Step 3 — Codex CLI preflight (capture lane only).
|
|
194
|
+
//
|
|
195
|
+
// Capture is OFF by default (trust-first): a fresh install makes zero
|
|
196
|
+
// Codex calls. The preflight below performs a real Codex triage call,
|
|
197
|
+
// so it ONLY runs when capture is actually enabled — i.e. `--enable-capture`
|
|
198
|
+
// AND a Voyage key (see `captureEnabled` above). Without the flag, or with
|
|
199
|
+
// the flag but no key, the capture lane — and its Codex preflight — is
|
|
200
|
+
// skipped entirely, and import + keyword search below still work with no
|
|
201
|
+
// Codex dependency.
|
|
129
202
|
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
// onboarding continues and the capture pass is skipped for this
|
|
135
|
-
// run. `--no-codex-capture` skips the preflight entirely.
|
|
203
|
+
// When capture IS enabled, a failed preflight is a warning, not fatal —
|
|
204
|
+
// onboarding continues and the capture pass is skipped for this run.
|
|
205
|
+
// `--no-codex-capture` skips the preflight even when capture was enabled
|
|
206
|
+
// (e.g. enable the daemon schedule but skip the one-shot).
|
|
136
207
|
let captureDisabled = false;
|
|
137
|
-
if (
|
|
208
|
+
if (!captureEnabled) {
|
|
209
|
+
ui.step("skip", "Chat access", captureRequested
|
|
210
|
+
? "capture needs a Voyage key — skipped"
|
|
211
|
+
: "capture off (default) · enable with --enable-capture");
|
|
212
|
+
captureDisabled = true;
|
|
213
|
+
}
|
|
214
|
+
else if (isOff(opts, "codexCapture", "noCodexCapture")) {
|
|
138
215
|
ui.step("skip", "Chat access", "skipped (--no-codex-capture) · auto-import off");
|
|
139
216
|
captureDisabled = true;
|
|
140
217
|
}
|
|
@@ -154,13 +231,16 @@ async function runOnboard(opts, globalOpts) {
|
|
|
154
231
|
const claudeSessions = safeDiscover(() => discoverClaudeCodeSessions(path.join(os.homedir(), ".claude")));
|
|
155
232
|
const codexSessions = safeDiscover(() => discoverCodexSessions());
|
|
156
233
|
ui.step("ok", "Chat history", `${claudeSessions} Claude Code · ${codexSessions} Codex CLI`);
|
|
157
|
-
// Step 5 —
|
|
158
|
-
const feedback = await collectFeedbackPreference(opts, dataDir);
|
|
159
|
-
if (feedback.
|
|
160
|
-
ui.step("ok", "
|
|
234
|
+
// Step 5 — beta opt-in (stay connected: news, pricing, feedback).
|
|
235
|
+
const feedback = await collectFeedbackPreference(opts, dataDir, rl);
|
|
236
|
+
if (feedback.email) {
|
|
237
|
+
ui.step("ok", "Stay connected", `${feedback.email} — opted in (news + feedback)`);
|
|
238
|
+
}
|
|
239
|
+
else if (feedback.enabled) {
|
|
240
|
+
ui.step("ok", "Stay connected", `feedback relay on (installation_id: ${feedback.installation_id})`);
|
|
161
241
|
}
|
|
162
242
|
else {
|
|
163
|
-
ui.step("ok", "
|
|
243
|
+
ui.step("ok", "Stay connected", "skipped — nothing shared, feedback stays local");
|
|
164
244
|
}
|
|
165
245
|
// Step 5b — optional Claude Code policy hook.
|
|
166
246
|
await maybeInstallClaudeCodeHook(opts, rl);
|
|
@@ -169,8 +249,17 @@ async function runOnboard(opts, globalOpts) {
|
|
|
169
249
|
if (opts.skipCapture) {
|
|
170
250
|
ui.step("skip", "Chat import", "skipped (--skip-capture)");
|
|
171
251
|
}
|
|
252
|
+
else if (last4 === null) {
|
|
253
|
+
// Auto-capture saves embed each conversation, so it needs a real
|
|
254
|
+
// embedding provider. In keyword-only mode it is skipped; import +
|
|
255
|
+
// search below still work.
|
|
256
|
+
ui.step("skip", "Chat import", "skipped (no embedding key — capture needs one)");
|
|
257
|
+
}
|
|
172
258
|
else if (captureDisabled) {
|
|
173
|
-
|
|
259
|
+
// Capture lane is off — either the default (no --enable-capture) or a
|
|
260
|
+
// failed/declined Codex preflight. The "Chat access" step above already
|
|
261
|
+
// printed the specific reason.
|
|
262
|
+
ui.step("skip", "Chat import", "skipped (capture not enabled)");
|
|
174
263
|
}
|
|
175
264
|
else {
|
|
176
265
|
const captureResult = await runFirstCapturePass(globalOpts.config, dataDir);
|
|
@@ -223,7 +312,9 @@ async function runOnboard(opts, globalOpts) {
|
|
|
223
312
|
}
|
|
224
313
|
// Step 10 — next-action card.
|
|
225
314
|
const card = decideOnboardOutcome({ ingestedAny, recall });
|
|
226
|
-
card.push(ui.pc.dim(
|
|
315
|
+
card.push(ui.pc.dim(last4 !== null
|
|
316
|
+
? `Voyage: key valid (last 4 …${last4})${safeLabel ? ` · label ${safeLabel}` : ""}`
|
|
317
|
+
: "Search: keyword-only (no embedding key) — add one later for semantic search"));
|
|
227
318
|
ui.box(card);
|
|
228
319
|
ui.line("");
|
|
229
320
|
}
|
|
@@ -261,7 +352,19 @@ async function ensureConfigAndDataDir(configPath, opts, rl) {
|
|
|
261
352
|
embedding: { provider: "voyage", model: "voyage-3-lite" },
|
|
262
353
|
data_paths: { data_dir: dataDir, jobs_dir: path.join(dataDir, "..", "jobs") },
|
|
263
354
|
rate_limit: { window_ms: 60_000, max_requests: 100 },
|
|
264
|
-
|
|
355
|
+
// Trust-first default: scheduled chat capture is OFF. The capture lane
|
|
356
|
+
// runs a Codex CLI triage call, so a fresh install sends no conversation
|
|
357
|
+
// content off-device, runs no Voyage embedding, and makes no Codex model
|
|
358
|
+
// call until the user opts in with `--enable-capture` (persisted below).
|
|
359
|
+
// (The daemon does make a metadata-only npm version check — no content, no
|
|
360
|
+
// key, no machine info.) The daemon gates auto-capture on `capture.enabled`.
|
|
361
|
+
capture: { enabled: false, interval_seconds: 3600 },
|
|
362
|
+
// Trust-first default: AI metadata enrichment is OFF. Import + keyword
|
|
363
|
+
// search run AI-free (BasicMetadataExtractor) and make no Codex calls,
|
|
364
|
+
// even on a machine with Codex installed. `--enable-codex-enrichment`
|
|
365
|
+
// (persisted below) flips this on. The `embedding` block above is a
|
|
366
|
+
// routing target only — without a Voyage key, search stays lexical.
|
|
367
|
+
enrichment: { ai_metadata: false },
|
|
265
368
|
};
|
|
266
369
|
fs.mkdirSync(path.dirname(absoluteConfig), { recursive: true });
|
|
267
370
|
fs.writeFileSync(absoluteConfig, JSON.stringify(config, null, 2) + "\n", {
|
|
@@ -339,9 +442,82 @@ export function persistVoyageLabel(configPath, label) {
|
|
|
339
442
|
});
|
|
340
443
|
fs.renameSync(tmp, absolute);
|
|
341
444
|
}
|
|
445
|
+
/**
|
|
446
|
+
* Persist the Codex AI-metadata-enrichment opt-in into config.json. Default
|
|
447
|
+
* onboarding leaves `enrichment.ai_metadata = false` (AI-free import + keyword
|
|
448
|
+
* search, zero Codex calls). When the user passes `--enable-codex-enrichment`,
|
|
449
|
+
* this flips it to true so the daemon/worker select the Codex-backed extractor.
|
|
450
|
+
* Backs the file up first, mirroring `persistVoyageLabel`.
|
|
451
|
+
*
|
|
452
|
+
* Exported for orchestrator-level tests; `runOnboard` is the only production
|
|
453
|
+
* caller. Returns true when the flag was set and persisted, false otherwise.
|
|
454
|
+
*/
|
|
455
|
+
export function applyCodexEnrichmentOptIn(enable, configPath, emit) {
|
|
456
|
+
if (enable !== true)
|
|
457
|
+
return false;
|
|
458
|
+
const absolute = path.resolve(configPath);
|
|
459
|
+
if (!fs.existsSync(absolute)) {
|
|
460
|
+
throw new CliError(`Cannot persist enrichment opt-in: config not found at ${absolute}`, "validation");
|
|
461
|
+
}
|
|
462
|
+
const raw = fs.readFileSync(absolute, "utf8");
|
|
463
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
464
|
+
fs.writeFileSync(`${absolute}.bak.${stamp}`, raw, { encoding: "utf8", mode: 0o600 });
|
|
465
|
+
const parsed = JSON.parse(raw);
|
|
466
|
+
const enrichment = parsed["enrichment"] ?? {};
|
|
467
|
+
enrichment["ai_metadata"] = true;
|
|
468
|
+
parsed["enrichment"] = enrichment;
|
|
469
|
+
const tmp = `${absolute}.tmp.${process.pid}`;
|
|
470
|
+
fs.writeFileSync(tmp, JSON.stringify(parsed, null, 2) + "\n", {
|
|
471
|
+
encoding: "utf8",
|
|
472
|
+
mode: 0o644,
|
|
473
|
+
});
|
|
474
|
+
fs.renameSync(tmp, absolute);
|
|
475
|
+
emit("Codex AI metadata enrichment enabled (enrichment.ai_metadata = true).");
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Persist the scheduled-capture opt-in. Default install writes
|
|
480
|
+
* `capture.enabled = false` (trust-first: zero Codex calls on a fresh
|
|
481
|
+
* machine). When the user passes `--enable-capture`, this flips it to true
|
|
482
|
+
* so the daemon runs auto-capture on its interval. Backs the file up first,
|
|
483
|
+
* mirroring `applyCodexEnrichmentOptIn`.
|
|
484
|
+
*
|
|
485
|
+
* Exported for orchestrator-level tests; `runOnboard` is the only production
|
|
486
|
+
* caller. Returns true when the flag was set and persisted, false otherwise.
|
|
487
|
+
*/
|
|
488
|
+
export function applyCaptureOptIn(enable, configPath, emit) {
|
|
489
|
+
if (enable !== true)
|
|
490
|
+
return false;
|
|
491
|
+
const absolute = path.resolve(configPath);
|
|
492
|
+
if (!fs.existsSync(absolute)) {
|
|
493
|
+
throw new CliError(`Cannot persist capture opt-in: config not found at ${absolute}`, "validation");
|
|
494
|
+
}
|
|
495
|
+
const raw = fs.readFileSync(absolute, "utf8");
|
|
496
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
497
|
+
fs.writeFileSync(`${absolute}.bak.${stamp}`, raw, { encoding: "utf8", mode: 0o600 });
|
|
498
|
+
const parsed = JSON.parse(raw);
|
|
499
|
+
const capture = parsed["capture"] ?? {};
|
|
500
|
+
capture["enabled"] = true;
|
|
501
|
+
parsed["capture"] = capture;
|
|
502
|
+
const tmp = `${absolute}.tmp.${process.pid}`;
|
|
503
|
+
fs.writeFileSync(tmp, JSON.stringify(parsed, null, 2) + "\n", {
|
|
504
|
+
encoding: "utf8",
|
|
505
|
+
mode: 0o644,
|
|
506
|
+
});
|
|
507
|
+
fs.renameSync(tmp, absolute);
|
|
508
|
+
emit("Scheduled chat capture enabled (capture.enabled = true).");
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
342
511
|
// ----- Step 2: Voyage key flow -----
|
|
343
|
-
async function collectAndPersistVoyageKey(opts,
|
|
512
|
+
async function collectAndPersistVoyageKey(opts, rl) {
|
|
344
513
|
const key = await collectVoyageKey(opts, rl);
|
|
514
|
+
if (!key) {
|
|
515
|
+
// No key → keyword-only (lexical) mode. Not a failure; import + search
|
|
516
|
+
// still work. Skip validation, the env write, and the Voyage smoke.
|
|
517
|
+
ui.step("info", "Search index", "no key — keyword search (semantic off)");
|
|
518
|
+
ui.note("Add semantic search later: run `rift onboard` and paste a Voyage key.");
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
345
521
|
const validateSpin = new ui.Spinner("Search index").start();
|
|
346
522
|
const validation = await validateVoyageKey({ apiKey: key });
|
|
347
523
|
if (!validation.ok) {
|
|
@@ -356,37 +532,45 @@ async function collectAndPersistVoyageKey(opts, globalOpts, rl) {
|
|
|
356
532
|
: "wrote ~/.rift.env (mode 0600)");
|
|
357
533
|
// Refresh process.env so any subsequent in-process Voyage call sees it.
|
|
358
534
|
loadRiftEnv({ filePath: envPath });
|
|
359
|
-
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
535
|
+
// NOTE: the daemon kickstart + smoke deliberately does NOT happen here.
|
|
536
|
+
// It runs in `runOnboard` AFTER the opt-in writes (label/enrichment/
|
|
537
|
+
// capture) land in config.json, so the respawned daemon boots with the
|
|
538
|
+
// final config. The daemon builds its capture loop and metadata extractor
|
|
539
|
+
// once at boot (src/main.ts), so kickstarting before those writes would
|
|
540
|
+
// leave `--enable-capture` / `--enable-codex-enrichment` inert until a
|
|
541
|
+
// later restart.
|
|
367
542
|
return validation.last4;
|
|
368
543
|
}
|
|
369
|
-
|
|
370
|
-
|
|
544
|
+
/**
|
|
545
|
+
* Resolve the Voyage key, or `""` to mean "finish without a key" (keyword-only
|
|
546
|
+
* lexical mode). The key is no longer mandatory: a friend can import and search
|
|
547
|
+
* first, then add a key later for semantic ranking. Returns `""` on explicit
|
|
548
|
+
* opt-out (`--no-voyage-key`), on `--yes` with no key available, or when the
|
|
549
|
+
* interactive prompt is left blank.
|
|
550
|
+
*/
|
|
551
|
+
export async function collectVoyageKey(opts, rl) {
|
|
552
|
+
if (opts.voyageKey === false || opts.noVoyageKey === true)
|
|
553
|
+
return "";
|
|
554
|
+
if (typeof opts.voyageKey === "string" && opts.voyageKey.trim()) {
|
|
371
555
|
return opts.voyageKey.trim();
|
|
556
|
+
}
|
|
372
557
|
if (process.env["VOYAGE_API_KEY"])
|
|
373
558
|
return process.env["VOYAGE_API_KEY"].trim();
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
559
|
+
// Non-interactive with no key supplied: finish in lexical mode rather than
|
|
560
|
+
// fail. The friend can run `rift onboard` again with a key anytime.
|
|
561
|
+
if (opts.yes)
|
|
562
|
+
return "";
|
|
377
563
|
// Explain the key BEFORE asking for it — a beta user should know what
|
|
378
564
|
// they are pasting and why, not be confronted with a bare prompt.
|
|
379
565
|
ui.detail([
|
|
380
|
-
"Search index key",
|
|
566
|
+
"Search index key (optional)",
|
|
381
567
|
"• What: paste the Voyage key Clem sent you (it won't echo as you type).",
|
|
382
|
-
"• Why:
|
|
568
|
+
"• Why: Voyage adds meaning-based (semantic) search on top of keyword search.",
|
|
569
|
+
"• Skip: press Enter to finish now with keyword search — add a key later anytime.",
|
|
383
570
|
"• Privacy: the key is stored locally and only sent to Voyage when Rift calls Voyage; never sent to Clem.",
|
|
384
571
|
].join("\n"));
|
|
385
|
-
const answer = (await ask(rl, "Paste your Voyage API key: ")).trim();
|
|
386
|
-
|
|
387
|
-
throw new CliError("Voyage key is required.", "validation");
|
|
388
|
-
}
|
|
389
|
-
return answer;
|
|
572
|
+
const answer = (await ask(rl, "Paste your Voyage API key (or press Enter to skip): ")).trim();
|
|
573
|
+
return answer; // "" → finish in keyword-only mode
|
|
390
574
|
}
|
|
391
575
|
/**
|
|
392
576
|
* Kickstart, wait for /health, confirm the respawned daemon process has
|
|
@@ -533,21 +717,115 @@ async function codexPreflight() {
|
|
|
533
717
|
return false;
|
|
534
718
|
}
|
|
535
719
|
}
|
|
536
|
-
|
|
537
|
-
|
|
720
|
+
/**
|
|
721
|
+
* Conservative email check — enough to catch obvious typos at the prompt;
|
|
722
|
+
* the relay endpoint can re-validate. Returns the normalized (trimmed,
|
|
723
|
+
* lowercased) address, or undefined if it doesn't look like an email.
|
|
724
|
+
*/
|
|
725
|
+
export function normalizeEmail(raw) {
|
|
726
|
+
const s = raw.trim().toLowerCase();
|
|
727
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s) ? s : undefined;
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Bundled "stay connected" opt-in. One yes/no that, on yes, collects an
|
|
731
|
+
* email used for product news, pricing, a leaving-beta heads-up, and a
|
|
732
|
+
* feedback channel. Opt-in only: `--yes` alone never subscribes (consent
|
|
733
|
+
* must be active), `--no-feedback-relay` declines, and `--email` subscribes
|
|
734
|
+
* non-interactively. The relay URL stays operator-only infra; when none is
|
|
735
|
+
* configured the email is captured locally (sidecar + beta-signups.jsonl)
|
|
736
|
+
* and reaches the operator once an endpoint is wired.
|
|
737
|
+
*/
|
|
738
|
+
export async function collectFeedbackPreference(opts, dataDir, rl, store = macRelaySecretStore) {
|
|
739
|
+
const declined = isOff(opts, "feedbackRelay", "noFeedbackRelay");
|
|
740
|
+
// Relay URL + signing secret come from EITHER an invite code (preferred:
|
|
741
|
+
// nothing to paste by hand) OR the back-compat operator flags. The secret
|
|
742
|
+
// never lands in the sidecar — it goes to the Keychain below.
|
|
538
743
|
let url;
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
744
|
+
let secret;
|
|
745
|
+
if (!declined) {
|
|
746
|
+
if (opts.invite) {
|
|
747
|
+
const invite = parseInvite(opts.invite); // throws a friendly CliError if bad
|
|
748
|
+
url = invite.url;
|
|
749
|
+
secret = invite.secret;
|
|
750
|
+
}
|
|
751
|
+
else {
|
|
752
|
+
url = opts.enableFeedbackRelay;
|
|
753
|
+
secret = url ? opts.relaySecret : undefined;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
// Friend-grade interactive path: when no relay flags were passed, offer to
|
|
757
|
+
// paste an invite so the bearer code never lands in shell history. Skipped
|
|
758
|
+
// off a TTY and under --yes (automation). A bad paste warns and skips — it
|
|
759
|
+
// never aborts onboarding.
|
|
760
|
+
if (!declined &&
|
|
761
|
+
!url &&
|
|
762
|
+
!opts.invite &&
|
|
763
|
+
!opts.enableFeedbackRelay &&
|
|
764
|
+
!opts.yes &&
|
|
765
|
+
process.stdin.isTTY) {
|
|
766
|
+
const pasted = (await ask(rl, "Paste a Rift invite code to enable feedback (Enter to skip): ")).trim();
|
|
767
|
+
if (pasted) {
|
|
768
|
+
try {
|
|
769
|
+
const invite = parseInvite(pasted);
|
|
770
|
+
url = invite.url;
|
|
771
|
+
secret = invite.secret;
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
say(" That invite code is not valid — skipping feedback setup.");
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
// Configuring a URL without a signing secret leaves the install "looks
|
|
779
|
+
// configured, silently dead" — the receiver requires a valid signature.
|
|
780
|
+
// An invite always carries one; the flag path must pair a secret or
|
|
781
|
+
// knowingly opt into unsigned mode (dev/local receivers only).
|
|
782
|
+
if (url && !secret && !opts.allowUnsignedFeedbackRelay) {
|
|
783
|
+
throw new CliError("Relay needs a signing secret. Use an invite (rift onboard --invite <code>), " +
|
|
784
|
+
"or pair --enable-feedback-relay with --relay-secret " +
|
|
785
|
+
"(or --allow-unsigned-feedback-relay for a dev receiver).", "validation");
|
|
786
|
+
}
|
|
787
|
+
// Secure the secret BEFORE enabling relay: a write failure must not leave an
|
|
788
|
+
// enabled config that can't sign. (3a's `rift feedback setup` does the same.)
|
|
789
|
+
if (secret) {
|
|
790
|
+
try {
|
|
791
|
+
await store.write(secret);
|
|
792
|
+
}
|
|
793
|
+
catch (err) {
|
|
794
|
+
if (err instanceof LockedKeychainError) {
|
|
795
|
+
throw new CliError("Your macOS Keychain is locked. Unlock it and retry: " +
|
|
796
|
+
"security unlock-keychain ~/Library/Keychains/login.keychain-db", "validation");
|
|
797
|
+
}
|
|
798
|
+
throw err;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
let email;
|
|
802
|
+
if (!declined) {
|
|
803
|
+
if (opts.email) {
|
|
804
|
+
email = normalizeEmail(opts.email);
|
|
805
|
+
if (!email) {
|
|
806
|
+
throw new CliError(`--email "${opts.email}" is not a valid email address.`, "validation");
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
else if (!opts.yes && process.stdin.isTTY) {
|
|
810
|
+
email = await promptStayConnected(rl);
|
|
811
|
+
}
|
|
812
|
+
// --yes / non-TTY with no --email: do not auto-subscribe.
|
|
813
|
+
}
|
|
814
|
+
const optedIn = email !== undefined || url !== undefined;
|
|
815
|
+
const cfg = optedIn
|
|
816
|
+
? {
|
|
817
|
+
enabled: true,
|
|
818
|
+
installation_id: crypto.randomUUID(),
|
|
819
|
+
...(email ? { email } : {}),
|
|
820
|
+
...(url ? { url } : {}),
|
|
821
|
+
// url && !secret only happens under --allow-unsigned-feedback-relay
|
|
822
|
+
// (the guard above throws otherwise): mark it an intentional unsigned
|
|
823
|
+
// relay so maybeRelay may POST without a secret for THIS install only.
|
|
824
|
+
...(url && !secret ? { unsigned: true } : {}),
|
|
825
|
+
}
|
|
550
826
|
: { enabled: false };
|
|
827
|
+
// The sidecar holds only non-secret fields; the secret is already in the
|
|
828
|
+
// Keychain (above).
|
|
551
829
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
552
830
|
const sidecar = path.join(dataDir, "feedback-config.json");
|
|
553
831
|
fs.writeFileSync(sidecar, JSON.stringify(cfg, null, 2) + "\n", {
|
|
@@ -555,8 +833,91 @@ async function collectFeedbackPreference(opts, dataDir) {
|
|
|
555
833
|
mode: 0o600,
|
|
556
834
|
});
|
|
557
835
|
fs.chmodSync(sidecar, 0o600);
|
|
836
|
+
if (cfg.email && cfg.installation_id) {
|
|
837
|
+
// Sign the signup POST with the in-hand secret (no Keychain round-trip).
|
|
838
|
+
await emitBetaSignup(cfg.email, cfg.installation_id, cfg.url, secret, dataDir);
|
|
839
|
+
}
|
|
558
840
|
return cfg;
|
|
559
841
|
}
|
|
842
|
+
/**
|
|
843
|
+
* Interactive bundled opt-in. Returns the chosen email, or undefined if the
|
|
844
|
+
* user declines or skips. Re-prompts up to 3× on a malformed address; an
|
|
845
|
+
* empty line at any point means "skip".
|
|
846
|
+
*/
|
|
847
|
+
async function promptStayConnected(rl) {
|
|
848
|
+
ui.detail([
|
|
849
|
+
"Stay in the loop (optional)",
|
|
850
|
+
"• Get product news, pricing once we set it, and a heads-up when Rift leaves beta.",
|
|
851
|
+
"• Gives you a direct line to send feedback any time via `rift feedback`.",
|
|
852
|
+
"• Opt-in. Stored locally, shared only with Clem — never sold, never spammed.",
|
|
853
|
+
].join("\n"));
|
|
854
|
+
const yn = (await ask(rl, "Share your email to stay connected? [y/N] ")).trim().toLowerCase();
|
|
855
|
+
if (yn !== "y" && yn !== "yes")
|
|
856
|
+
return undefined;
|
|
857
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
858
|
+
const raw = (await ask(rl, "Your email (Enter to skip): ")).trim();
|
|
859
|
+
if (raw.length === 0)
|
|
860
|
+
return undefined;
|
|
861
|
+
const norm = normalizeEmail(raw);
|
|
862
|
+
if (norm)
|
|
863
|
+
return norm;
|
|
864
|
+
say(" That doesn't look like an email address — try again, or press Enter to skip.");
|
|
865
|
+
}
|
|
866
|
+
return undefined;
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Record a one-time beta signup. The local JSONL row is canonical (never
|
|
870
|
+
* lost); the relay POST is best-effort and only fires when an endpoint URL
|
|
871
|
+
* is configured. Failures are swallowed — onboarding must never break on a
|
|
872
|
+
* signup hiccup.
|
|
873
|
+
*/
|
|
874
|
+
async function emitBetaSignup(email, installationId, url, hmacSecret, dataDir) {
|
|
875
|
+
const build = getBuildInfo();
|
|
876
|
+
const payload = {
|
|
877
|
+
ts: new Date().toISOString(),
|
|
878
|
+
event: "beta_signup",
|
|
879
|
+
email,
|
|
880
|
+
installation_id: installationId,
|
|
881
|
+
version: build.version,
|
|
882
|
+
commit: build.commit,
|
|
883
|
+
node: build.node,
|
|
884
|
+
};
|
|
885
|
+
// Audit invariant: every byte that reaches Clem must exist locally first.
|
|
886
|
+
// If the canonical row can't be written, we must NOT relay — otherwise a
|
|
887
|
+
// signup could reach Clem with no local record to audit against.
|
|
888
|
+
let wroteLocal = false;
|
|
889
|
+
try {
|
|
890
|
+
const dir = path.join(dataDir, "observability");
|
|
891
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
892
|
+
const file = path.join(dir, "beta-signups.jsonl");
|
|
893
|
+
fs.appendFileSync(file, `${JSON.stringify(payload)}\n`, {
|
|
894
|
+
encoding: "utf8",
|
|
895
|
+
mode: 0o600,
|
|
896
|
+
});
|
|
897
|
+
wroteLocal = true; // the canonical row is on disk; chmod below is cosmetic
|
|
898
|
+
try {
|
|
899
|
+
fs.chmodSync(file, 0o600);
|
|
900
|
+
}
|
|
901
|
+
catch {
|
|
902
|
+
// best-effort permission fix — does not affect the audit invariant
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
catch {
|
|
906
|
+
// Local write failed — don't break onboarding over a signup row, and
|
|
907
|
+
// (below) don't relay something we couldn't record locally.
|
|
908
|
+
}
|
|
909
|
+
if (url && wroteLocal) {
|
|
910
|
+
try {
|
|
911
|
+
await postFeedback(payload, {
|
|
912
|
+
url,
|
|
913
|
+
...(hmacSecret ? { hmacSecret } : {}),
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
catch {
|
|
917
|
+
// Best-effort; the canonical row is already on disk.
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
560
921
|
// ----- Step 5b: Claude Code policy hook (opt-in) -----
|
|
561
922
|
/**
|
|
562
923
|
* Offer to install the Rift policy hook into Claude Code's settings.
|