@useshifu/coding-harness 0.3.29 → 0.3.31

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.
@@ -19,6 +19,7 @@ const HARNESS_NAMES = {
19
19
  codex: "Codex",
20
20
  claude_code: "Claude Code",
21
21
  opencode: "OpenCode",
22
+ cursor: "Cursor",
22
23
  };
23
24
  const TOKEN_PATTERN = /^cs_sk_[a-f0-9]{16}_[a-f0-9]{64}$/;
24
25
  const SENSITIVE_PATTERNS = ["-----begin", "authorization", "sk-", "sk_", "ghp_", "gho_", "ghu_", "ghs_", "ghr_", "xox", "github_pat_", "glpat-", "ignore previous instructions", "ignore all previous instructions", "system prompt", "@", "://", "`", "/"];
@@ -46,7 +47,7 @@ const MAX_TURNS_PER_SEGMENT = 12;
46
47
  const MAX_TITLE_LENGTH = 160;
47
48
  const MAX_WORK_DETAIL_LENGTH = 600;
48
49
  const CONFIG_VERSION = 2;
49
- const RUNNER_VERSION = "0.3.29";
50
+ const RUNNER_VERSION = "0.3.31";
50
51
  const REDACTION_VERSION = 5;
51
52
  const LEGACY_REDACTION_VERSION = 4;
52
53
  const MAX_PREPARATIONS_PER_RUN = 4;
@@ -180,8 +181,9 @@ function openCodeDatabaseQuery(dbPath, query) {
180
181
  if (tables.length !== 2) return read(query);
181
182
  // V2 stores typed messages, ordered by sequence, instead of separate parts.
182
183
  // Read-only CTEs retain the existing adapter contract without changing its database.
184
+ const hasDirectory = read("PRAGMA table_info(session_v2)").some((column) => column.name === "directory");
183
185
  const projection = `WITH
184
- session AS (SELECT id, time_created, time_updated FROM session_v2),
186
+ session AS (SELECT id, time_created, time_updated, ${hasDirectory ? "directory" : "NULL AS directory"} FROM session_v2),
185
187
  message AS (
186
188
  SELECT id, session_id, seq, time_created, json_object(
187
189
  'role', type, 'finish', CASE WHEN json_extract(data, '$.time.completed') IS NOT NULL THEN json_extract(data, '$.finish') END,
@@ -211,7 +213,12 @@ function opencodeSessions(rows) {
211
213
  return [...sessions.values()];
212
214
  }
213
215
 
216
+ function cursorOptions() {
217
+ return { journalRoot: path.join(configRoot(), "cursor-capture") };
218
+ }
219
+
214
220
  function localSessions(harness) {
221
+ if (harness === "cursor") return require("./cursor-sessions.js").localSessions(cursorOptions());
215
222
  if (harness === "codex") return sessionFiles(codexSessionsRoot()).map(codexSession).filter(Boolean);
216
223
  if (harness === "claude_code") return sessionFiles(claudeSessionsRoot()).map(claudeSession).filter(Boolean);
217
224
  if (harness === "opencode") return opencodeSessions(opencodeQuery("SELECT s.id AS sessionRef, s.time_created AS startedAt, s.time_updated AS updatedAt, m.time_created AS turnAt FROM session s JOIN message m ON m.session_id = s.id WHERE json_extract(m.data, '$.role') = 'user' ORDER BY s.time_updated, m.time_created"));
@@ -219,7 +226,7 @@ function localSessions(harness) {
219
226
  }
220
227
 
221
228
  function requireHarness(value) {
222
- if (!Object.hasOwn(HARNESS_NAMES, value)) throw new Error("Choose --harness codex, claude_code, or opencode.");
229
+ if (!Object.hasOwn(HARNESS_NAMES, value)) throw new Error("Choose --harness codex, claude_code, opencode, or cursor.");
223
230
  return value;
224
231
  }
225
232
 
@@ -244,12 +251,15 @@ function validateSyncOptions(command, start = 3) {
244
251
  "sync-files": common,
245
252
  "scheduled-sync": common,
246
253
  pending: common,
254
+ "retry-preparation": [...common, "--session-ref", "--from-turn"],
247
255
  approve: common,
248
256
  install: common,
249
257
  connect: [...common, ...setup, ...syncSettings, "--api-url"],
250
258
  "config-update": [...common, ...setup, ...syncSettings],
251
259
  "mcp-setup": [...common, ...setup, "--mcp-url", "--api-url"],
252
260
  "mcp-headers": common,
261
+ "mcp-bridge": common,
262
+ "cursor-hook": common,
253
263
  };
254
264
  const flags = {
255
265
  connect: ["--interactive", "--non-interactive"], "mcp-setup": ["--interactive", "--non-interactive"], "config-update": ["--interactive", "--non-interactive"],
@@ -296,6 +306,7 @@ function writeJSON(file, value) {
296
306
  }
297
307
 
298
308
  function readConfig(harness) {
309
+ if (fs.existsSync(path.join(configRoot(), `${harness}-connection-transition.json`))) throw new Error("An interrupted connection replacement is held locally. Run connect again with the selected connection before syncing.");
299
310
  const config = readJSON(configPath(harness), undefined);
300
311
  if (!config || typeof config.token !== "string" || typeof config.apiUrl !== "string") {
301
312
  throw new Error(`No ${HARNESS_NAMES[harness]} connection is configured. Run connect first.`);
@@ -305,6 +316,7 @@ function readConfig(harness) {
305
316
  }
306
317
 
307
318
  function environmentSessionRef(harness) {
319
+ if (harness === "cursor") return process.env.SHIFU_CURSOR_SESSION_REF;
308
320
  return harness === "codex" ? process.env.CODEX_THREAD_ID || process.env.CODEX_SESSION_ID :
309
321
  harness === "claude_code" ? process.env.CLAUDE_CODE_SESSION_ID : process.env.OPENCODE_SESSION_ID;
310
322
  }
@@ -614,6 +626,11 @@ function opencodeReview(sessionRef, fromTurn, toTurn) {
614
626
  }
615
627
 
616
628
  function reviewSegment(harness, sessionRef, fromTurn, toTurn) {
629
+ if (harness === "cursor") {
630
+ const records = sourceRecords(harness, sessionRef);
631
+ const { turnCount, sources } = require("./cursor-sessions.js").sources({ sessionRef, fromTurn, toTurn }, records);
632
+ return { sessionRef, turnCount, notes: sources.filter((source) => source.role === "assistant").map((source) => ({ turn: source.turn, note: source.content })) };
633
+ }
617
634
  if (harness === "opencode") return opencodeReview(sessionRef, fromTurn, toTurn);
618
635
  const selectedFile = sessionFile(harness, sessionRef);
619
636
  return harness === "codex" ? codexReview(selectedFile, fromTurn, toTurn) : claudeReview(selectedFile, fromTurn, toTurn);
@@ -630,6 +647,7 @@ function reviewSourcePacket(harness, sessionRef, fromTurn, toTurn, scheduled = f
630
647
  }
631
648
 
632
649
  function sourceRecords(harness, sessionRef) {
650
+ if (harness === "cursor") return require("./cursor-sessions.js").sourceRecords(sessionRef, cursorOptions());
633
651
  if (harness === "opencode") {
634
652
  return opencodeQuery(`SELECT m.id AS messageId, m.time_created AS createdAt, json_extract(m.data, '$.role') AS role, json_extract(m.data, '$.finish') AS finish, json_extract(m.data, '$.error') AS error, json_extract(m.data, '$.storageVersion') AS storageVersion, p.data AS part FROM message m LEFT JOIN part p ON p.message_id = m.id WHERE m.session_id = ${JSON.stringify(sessionRef)} ORDER BY m.time_created, p.id`);
635
653
  }
@@ -1078,7 +1096,11 @@ function canonicalThinking(value) {
1078
1096
 
1079
1097
  function localProfileTurns(harness, segment) {
1080
1098
  const turns = [];
1081
- if (harness === "opencode") {
1099
+ if (harness === "cursor") {
1100
+ const records = sourceRecords(harness, segment.sessionRef);
1101
+ const sources = require("./cursor-sessions.js").sources({ ...segment, fromTurn: 1 }, records).sources;
1102
+ for (const source of sources.filter((source) => source.role === "user")) turns.push({ turn: source.turn, text: source.content });
1103
+ } else if (harness === "opencode") {
1082
1104
  const rows = opencodeQuery(`SELECT m.id AS messageId, json_extract(m.data, '$.role') AS role, json_extract(m.data, '$.modelID') AS model, p.data AS part FROM message m LEFT JOIN part p ON p.message_id = m.id WHERE m.session_id = ${JSON.stringify(segment.sessionRef)} ORDER BY m.time_created, p.id`);
1083
1105
  let lastMessage;
1084
1106
  let turn = 0;
@@ -1186,8 +1208,8 @@ function selectedSessionSegments(harness, { hours = 24, turns = MAX_TURNS_PER_SE
1186
1208
  }
1187
1209
  return [...sessions.values()]
1188
1210
  .sort((left, right) => count === undefined
1189
- ? left.updatedAt.localeCompare(right.updatedAt)
1190
- : right.updatedAt.localeCompare(left.updatedAt) || left.sessionRef.localeCompare(right.sessionRef))
1211
+ ? String(left.updatedAt || "").localeCompare(String(right.updatedAt || ""))
1212
+ : String(right.updatedAt || "").localeCompare(String(left.updatedAt || "")) || left.sessionRef.localeCompare(right.sessionRef))
1191
1213
  .slice(0, count ?? Infinity)
1192
1214
  .map(({ turnTimes, ...session }) => {
1193
1215
  const checkpoint = state.sessions[session.sessionRef]?.lastSyncedTurn || 0;
@@ -1213,7 +1235,7 @@ function pendingSegments(harness, state = readState(harness)) {
1213
1235
  const segments = [];
1214
1236
  for (const session of localSessions(harness)
1215
1237
  .filter((candidate) => !excluded.has(candidate.sessionRef))
1216
- .sort((left, right) => left.updatedAt.localeCompare(right.updatedAt))) {
1238
+ .sort((left, right) => String(left.updatedAt || "").localeCompare(String(right.updatedAt || "")))) {
1217
1239
  let fromTurn = (state.sessions[session.sessionRef]?.lastSyncedTurn || 0) + 1;
1218
1240
  if (fromTurn > session.turnCount || !initialHistoryEligibility(session, fromTurn, config).eligible) continue;
1219
1241
  const completedTo = require("./source-review.js").completedSourceTurn({ harness }, sourceRecords(harness, session.sessionRef));
@@ -1273,7 +1295,7 @@ function copyRunner() {
1273
1295
  const installedDependency = path.join(configRoot(), "node_modules", name);
1274
1296
  if (!sameFile(dependency, installedDependency)) copyDirectory(dependency, installedDependency);
1275
1297
  }
1276
- for (const name of ["source-review.js", "v5-payload.js", "native-preparation.js", "opencode-v2-account.js", "context-preferences.js", "setup-options.js", "initial-history.js", "onboarding-ui.js"]) {
1298
+ for (const name of ["source-review.js", "v5-payload.js", "native-preparation.js", "opencode-v2-account.js", "context-preferences.js", "setup-options.js", "initial-history.js", "onboarding-ui.js", "continuity-preparation.js", "continuity-state.js", "cursor-config.js", "cursor-mcp.js", "cursor-sessions.js", "cursor-native.js"]) {
1277
1299
  const source = path.join(__dirname, name);
1278
1300
  const destination = path.join(configRoot(), name);
1279
1301
  if (!sameFile(source, destination)) {
@@ -1333,6 +1355,7 @@ function removeCodexHooks() {
1333
1355
  }
1334
1356
 
1335
1357
  function installDestination(harness) {
1358
+ if (harness === "cursor") return path.join(require("./cursor-config.js").cursorHome(), "skills", "shifu-sync");
1336
1359
  if (harness === "codex") return path.join(codexHome(), "skills", "shifu-sync");
1337
1360
  if (harness === "claude_code") return path.join(claudeHome(), "skills", "shifu-sync");
1338
1361
  return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "opencode", "skills", "shifu-sync");
@@ -1382,6 +1405,7 @@ function install(harness, quiet = false, preferences = readJSON(configPath(harne
1382
1405
  fs.appendFileSync(syncFile, `\n## ${HARNESS_NAMES[harness]} context connection\n\nAfter connecting ${HARNESS_NAMES[harness]}, run \`"${process.execPath}" "${installedRunnerPath()}" mcp-setup --harness ${harness}\` to configure Shifu context and memory tools with the saved key. Use \`--mcp-url\` when the user supplied a branch endpoint. If the endpoint is not deployed, report that limitation while leaving sync connected. Reload ${HARNESS_NAMES[harness]} or start a new chat after successful setup. Follow the installed shifu-context skill for context and memory use.\n`);
1383
1406
  if (!quiet) refreshManagedContextInstructions(harness, preferences);
1384
1407
  }
1408
+ if (harness === "cursor") require("./cursor-config.js").configureHooks(installedRunnerPath(), path.resolve(configRoot()));
1385
1409
  const removedHooks = harness === "codex" && removeCodexHooks();
1386
1410
  if (!quiet) {
1387
1411
  printWelcome({ harness, upgraded });
@@ -1557,7 +1581,7 @@ async function connect(harness, readKey = readSecret, choices) {
1557
1581
  const token = (await readKey()).trim();
1558
1582
  if (!TOKEN_PATTERN.test(token)) throw new Error("The connection key is invalid.");
1559
1583
  const apiUrl = requireSafeApiUrl(option("--api-url") || existing?.apiUrl || process.env.SHIFU_API_URL || DEFAULT_API_URL);
1560
- await validateConnection(apiUrl, token, harness);
1584
+ const connectionInfo = await validateConnection(apiUrl, token, harness);
1561
1585
  const savedPolicy = existing && normalizedPolicy(existing);
1562
1586
  const currentPolicy = savedPolicy?.onboardingComplete ? savedPolicy : { intervalHours: DEFAULT_INTERVAL_HOURS, approvalMode: "automatic", excludedSessionRefs: savedPolicy?.excludedSessionRefs || [] };
1563
1587
  const selected = await chooseSyncPolicy(currentPolicy);
@@ -1570,15 +1594,17 @@ async function connect(harness, readKey = readSecret, choices) {
1570
1594
  install(harness, true, choices.contextPreferences);
1571
1595
  const scheduleFile = scheduleEnabled ? installSchedule(harness) : undefined;
1572
1596
  if (!scheduleEnabled && savedPolicy?.scheduleEnabled) removeSchedule(harness);
1573
- writeJSON(configPath(harness), { ...existing, ...choices, version: CONFIG_VERSION, apiUrl, token, harness, syncPolicy, inputFolder });
1574
- refreshManagedContextInstructions(harness, choices.contextPreferences);
1575
- if (refreshContextKey) writePrivateText(openCodeTokenPath(), token);
1576
- if (existing?.token && existing.token !== token) {
1577
- for (const file of [statePath(harness), pendingPath(harness)]) {
1578
- try { fs.unlinkSync(file); } catch (error) { if (error?.code !== "ENOENT") throw error; }
1579
- }
1580
- }
1581
- scheduleNextRun(harness, syncPolicy.intervalHours);
1597
+ const connection = { ...existing, ...choices, version: CONFIG_VERSION, apiUrl, token, harness, syncPolicy, inputFolder, capabilities: connectionInfo.capabilities || {} };
1598
+ const committed = await withSyncLock(harness, async () => {
1599
+ if (JSON.stringify(readJSON(configPath(harness), undefined)) !== JSON.stringify(existing)) throw new Error("The connection changed during setup. Run connect again.");
1600
+ require("./continuity-state.js").commitConnection(configRoot(), harness, existing, connection);
1601
+ refreshManagedContextInstructions(harness, choices.contextPreferences);
1602
+ if (refreshContextKey) writePrivateText(openCodeTokenPath(), token);
1603
+ const continuity = continuityState(harness, connection);
1604
+ continuity.load(); continuity.dictionary();
1605
+ scheduleNextRun(harness, syncPolicy.intervalHours);
1606
+ });
1607
+ if (committed?.skipped) throw new Error("Another Shifu sync is running. Retry connect after it finishes; the connection was not replaced.");
1582
1608
  console.error(`${HARNESS_NAMES[harness]} is connected. ${scheduleEnabled ? `It will sync every ${syncPolicy.intervalHours} hours with ${syncPolicy.approvalMode} approval.` : "The sync schedule is off."}`);
1583
1609
  console.error(`Shifu sync folder: ${inputFolder}. Place .txt, .md, .rtf, or .docx files there. Only grounded contributions will become claims.`);
1584
1610
  if (conflicts.length) console.error(`Existing Shifu shortcut(s) were left unchanged: ${conflicts.join(", ")}.`);
@@ -1938,6 +1964,7 @@ function readContextInstructionFile(file) {
1938
1964
  }
1939
1965
 
1940
1966
  function contextInstructionFile(harness) {
1967
+ if (harness === "cursor") return path.join(configRoot(), "cursor-context.md");
1941
1968
  if (harness === "codex") {
1942
1969
  const override = path.join(codexHome(), "AGENTS.override.md");
1943
1970
  if (readContextInstructionFile(override).original.trim()) return override;
@@ -2021,7 +2048,7 @@ async function setupMcp(harness) {
2021
2048
  printSetupIntro();
2022
2049
  const choices = await onboardingOptions(readJSON(configPath(harness), undefined));
2023
2050
  const instructions = prepareContextInstructions(harness, choices.contextPreferences);
2024
- const helper = harness === "opencode" ? undefined : helperCommand(harness);
2051
+ const helper = ["opencode", "cursor"].includes(harness) ? undefined : helperCommand(harness);
2025
2052
  if (fs.existsSync(configPath(harness)) && !fs.lstatSync(configPath(harness)).isFile()) {
2026
2053
  throw new Error(`The saved ${HARNESS_NAMES[harness]} connection must be a regular private file, not a link.`);
2027
2054
  }
@@ -2038,7 +2065,7 @@ async function setupMcp(harness) {
2038
2065
  if (!TOKEN_PATTERN.test(config.token)) throw new Error(`The saved ${HARNESS_NAMES[harness]} connection key is invalid. Run connect again.`);
2039
2066
  await validateConnection(config.apiUrl, config.token, harness);
2040
2067
  const tools = await verifyMcp(mcpUrl, config.token);
2041
- const prepared = harness === "codex" ? prepareCodexMcpConfig(mcpUrl, helper) : harness === "opencode" ? prepareOpenCodeMcpConfig(mcpUrl) : prepareClaudeMcpConfig(mcpUrl, helper);
2068
+ const prepared = harness === "cursor" ? require("./cursor-config.js").prepareMcp(path.resolve(installedRunnerPath()), path.resolve(configRoot())) : harness === "codex" ? prepareCodexMcpConfig(mcpUrl, helper) : harness === "opencode" ? prepareOpenCodeMcpConfig(mcpUrl) : prepareClaudeMcpConfig(mcpUrl, helper);
2042
2069
  if (!unchangedMcpConfig(prepared)) {
2043
2070
  throw new Error(`${HARNESS_NAMES[harness]} configuration changed during setup. Run mcp-setup again to preserve those changes.`);
2044
2071
  }
@@ -2068,6 +2095,11 @@ function disconnect(harness) {
2068
2095
  if (fs.existsSync(lockPath(harness))) throw new Error("A Shifu sync is running. Try disconnecting again after it finishes.");
2069
2096
  if (harness === "opencode" && (config.mcpUrl || fs.existsSync(openCodeTokenPath()))) removeOpenCodeMcpConfig(config.mcpConfigMajor);
2070
2097
  if (harness === "claude_code" && config.mcpUrl) removeClaudeMcpConfig(config.mcpHeadersHelper || helperCommand("claude_code"));
2098
+ if (harness === "cursor") {
2099
+ const cursor = require("./cursor-config.js");
2100
+ cursor.removeMcp(path.resolve(installedRunnerPath()), path.resolve(configRoot()));
2101
+ cursor.configureHooks(installedRunnerPath(), path.resolve(configRoot()), true);
2102
+ }
2071
2103
  removeSchedule(harness);
2072
2104
  for (const file of [configPath(harness), statePath(harness), pendingPath(harness)]) {
2073
2105
  try { fs.unlinkSync(file); } catch (error) { if (error?.code !== "ENOENT") throw error; }
@@ -2217,10 +2249,11 @@ function retryDelay(response, attempt) {
2217
2249
  return Math.min(base * (2 ** attempt), 30_000);
2218
2250
  }
2219
2251
 
2220
- async function postSync(config, input) {
2252
+ async function postSync(config, input, beforeAttempt, onPermanentFailure) {
2221
2253
  const body = JSON.stringify(input.force ? { ...input, forceRequestId: randomUUID() } : input);
2222
2254
  let lastError;
2223
2255
  for (let attempt = 0; attempt < MAX_RETRIES; attempt += 1) {
2256
+ beforeAttempt?.();
2224
2257
  let response;
2225
2258
  try {
2226
2259
  response = await fetch(`${config.apiUrl}/v1/connectors/coding-sessions/syncs`, {
@@ -2237,7 +2270,7 @@ async function postSync(config, input) {
2237
2270
  const payload = await response.json().catch(() => undefined);
2238
2271
  if (response.ok) return payload?.data;
2239
2272
  lastError = new Error(payload?.error?.message || `Shifu rejected the sync (${response.status}).`);
2240
- if (response.status !== 429 && response.status < 500) throw lastError;
2273
+ if (response.status !== 429 && response.status < 500) { onPermanentFailure?.(); throw lastError; }
2241
2274
  if (attempt + 1 < MAX_RETRIES) await sleep(retryDelay(response, attempt));
2242
2275
  }
2243
2276
  throw lastError;
@@ -2446,20 +2479,27 @@ async function sendSync(harness, input, config = readConfig(harness)) {
2446
2479
  validateSync(input, harness);
2447
2480
  const state = readState(harness);
2448
2481
  const checkpoint = state.sessions[input.sessionRef];
2482
+ const store = continuityState(harness, config);
2483
+ const frozen = store.find(input);
2449
2484
  const expected = (checkpoint?.lastSyncedTurn || 0) + 1;
2450
2485
  const force = input.force || Boolean(input.supersedesSyncId);
2451
- if (!force && input.fromTurn !== expected) throw new Error(`This local checkpoint expects turn ${expected}. Review the pending segment before retrying.`);
2486
+ if (!force && !frozen && input.fromTurn !== expected) throw new Error(`This local checkpoint expects turn ${expected}. Review the pending segment before retrying.`);
2452
2487
  if (force && reviewSegment(harness, input.sessionRef, input.fromTurn, input.toTurn).turnCount < input.toTurn) throw new Error("The original local turns must still exist before force syncing this range.");
2453
- if (!force) input = queuePayload(harness, input);
2488
+ if (!force && (!frozen || input.toTurn > (checkpoint?.lastSyncedTurn || 0))) input = queuePayload(harness, input);
2454
2489
  let profile;
2455
2490
  if (!force) {
2456
2491
  try { profile = profileForSegment(harness, input); }
2457
2492
  catch { console.error("AI Profile source is unavailable; the prepared activity payload remains retryable."); }
2458
2493
  }
2459
- const receipt = await postSync(config, input);
2460
- if (!validSyncReceipt(receipt, input)) {
2461
- throw new Error("Shifu returned a checkpoint that does not match this reviewed segment. Local state was not advanced.");
2494
+ let receipt;
2495
+ try {
2496
+ receipt = frozen?.receipt || await postSync(config, input, frozen ? () => store.reserveBase(input) : undefined, frozen ? () => store.update(frozen.key, (entry) => { entry.baseStatus = "needs_review"; entry.reason = "base_rejected"; }) : undefined);
2497
+ if (!validSyncReceipt(receipt, input)) throw new Error("Shifu returned a checkpoint that does not match this reviewed segment. Local state was not advanced.");
2498
+ } catch (error) {
2499
+ if (frozen && store.find(input).baseAttempts >= 10) store.update(frozen.key, (entry) => { entry.baseStatus = "needs_review"; entry.reason = "base_transport_budget"; });
2500
+ throw error;
2462
2501
  }
2502
+ if (frozen) store.bind(input, receipt);
2463
2503
  if (profile) {
2464
2504
  state.profilePending ||= {};
2465
2505
  state.profilePending[`${profile.sessionRef}:${profile.fromTurn}:${profile.toTurn}`] = profile;
@@ -2568,27 +2608,108 @@ async function reconcilePreparedSession(harness, sessionRef, config) {
2568
2608
  saveState(harness, state);
2569
2609
  }
2570
2610
 
2611
+ function continuityState(harness, config = readConfig(harness)) {
2612
+ return require("./continuity-state.js").continuityStore(configRoot(), harness, config);
2613
+ }
2614
+
2615
+ function preparationPacket(harness, segment, config) {
2616
+ if (normalizedPolicy(config).excludedSessionRefs.includes(segment.sessionRef)) throw new Error("This session is excluded from preparation.");
2617
+ if (!initialHistoryEligibility(segment, segment.fromTurn, config).eligible) throw new Error("This range is outside the saved initial-history selection.");
2618
+ const records = sourceRecords(harness, segment.sessionRef);
2619
+ let minimumTurn = Math.max(1, segment.fromTurn - 6);
2620
+ for (let turn = segment.fromTurn - 1; turn >= minimumTurn; turn -= 1) {
2621
+ if (!initialHistoryEligibility(segment, turn, config).eligible) { minimumTurn = turn + 1; break; }
2622
+ }
2623
+ const packet = require("./source-review.js").continuitySourcePacket({ harness, sessionRef: segment.sessionRef, fromTurn: segment.fromTurn, toTurn: segment.toTurn }, records, minimumTurn);
2624
+ let workspace;
2625
+ if (harness === "codex") workspace = records.find((record) => record.type === "session_meta")?.payload?.cwd;
2626
+ else if (harness === "claude_code") workspace = records.find((record) => typeof record.cwd === "string")?.cwd;
2627
+ else if (harness === "cursor") workspace = records.find((record) => typeof record.workspace === "string")?.workspace;
2628
+ else {
2629
+ try { workspace = opencodeQuery(`SELECT directory FROM session WHERE id = ${JSON.stringify(segment.sessionRef)}`)[0]?.directory; }
2630
+ catch { /* No trusted directory means a session-specific scope, never the scheduler workspace. */ }
2631
+ }
2632
+ return { packet, workspace: createHash("sha256").update(typeof workspace === "string" && workspace ? `workspace:${workspace}` : `${harness}:session:${segment.sessionRef}`).digest("hex") };
2633
+ }
2634
+
2571
2635
  async function prepareV5Payload(harness, segment, prepare = require("./native-preparation.js").prepareNative) {
2572
- const packet = reviewSourcePacket(harness, segment.sessionRef, segment.fromTurn, segment.toTurn, true);
2636
+ const config = readConfig(harness);
2637
+ const store = continuityState(harness, config);
2638
+ const prior = store.load().attempts[`${segment.sessionRef}:${segment.fromTurn}`];
2639
+ if (prior?.payloadKey) {
2640
+ const frozen = store.load().outbox.find((entry) => entry.key === prior.payloadKey);
2641
+ if (!frozen) throw new Error("Frozen preparation is unavailable; continuity needs review.");
2642
+ return frozen.payload;
2643
+ }
2644
+ if (prior?.toTurn) segment = { ...segment, toTurn: prior.toTurn };
2645
+ const { packet, workspace } = preparationPacket(harness, segment, config);
2646
+ const dictionary = store.dictionary();
2647
+ dictionary.scopes[workspace] ||= { scopeRef: `s_${randomUUID().replaceAll("-", "")}`, entities: {} };
2648
+ store.saveDictionary(dictionary);
2649
+ const relevantBindings = () => {
2650
+ const content = [...packet.sources, ...packet.referenceSources].map((source) => source.content).join("\n").toLocaleLowerCase();
2651
+ return Object.values(dictionary.scopes[workspace].entities).filter((entry) => entry.createdBy !== `${packet.sessionRef}:${packet.fromTurn}` && content.includes(entry.mention.toLocaleLowerCase()))
2652
+ .map(({ key, mention, kind }) => ({ bindingRef: key, mention, kind }));
2653
+ };
2654
+ packet.entityBindings = relevantBindings();
2655
+ while (packet.referenceSources.length && Buffer.byteLength(JSON.stringify(packet)) + 1 > 256 * 1024) {
2656
+ const oldest = packet.referenceSources[0].turn;
2657
+ packet.referenceSources = packet.referenceSources.filter((source) => source.turn > oldest);
2658
+ packet.referenceCoverage.limited = true;
2659
+ packet.referenceCoverage.droppedTurns += 1;
2660
+ packet.referenceCoverage.fromTurn = packet.referenceSources[0]?.turn || packet.fromTurn;
2661
+ packet.entityBindings = relevantBindings();
2662
+ }
2663
+ if (Buffer.byteLength(JSON.stringify(packet)) + 1 > 256 * 1024) throw new Error("The selected source packet exceeds 128 messages or 256 KiB. Nothing was truncated or synced.");
2664
+ if (packet.entityBindings.length > 32) throw new Error("Relevant private entity bindings exceed the bounded packet; this range needs review.");
2665
+ const bindings = { workspace, scopeRef: dictionary.scopes[workspace].scopeRef, catalog: require("./continuity-preparation.js").CATALOG };
2573
2666
  const deadline = Date.now() + 180_000;
2574
2667
  let validationFeedback;
2575
2668
  for (let attempt = 0; attempt < 2; attempt += 1) {
2576
2669
  const remainingMs = deadline - Date.now();
2577
2670
  if (remainingMs <= 0) throw new Error("Native preparation timed out; the source remains pending.");
2578
- const output = await prepare(harness, packet, { validationFeedback, timeoutMs: remainingMs });
2671
+ const reservation = store.reservation(packet, bindings);
2672
+ let output;
2579
2673
  try {
2580
- const payload = require("./v5-payload.js").preparedPayload(packet, output);
2581
- validateSync(payload, harness);
2582
- return payload;
2674
+ output = await prepare(harness, packet, { validationFeedback, timeoutMs: remainingMs, continuity: true });
2675
+ const candidateDictionary = structuredClone(dictionary);
2676
+ const result = require("./continuity-preparation.js").bindContinuity(packet, output, candidateDictionary, workspace);
2677
+ validateSync(result.payload, harness);
2678
+ if (result.assessment.disposition === "no_safe_context" && reservation.attempt < 2 && packet.sources.length) throw new Error("Recheck this proposed empty result against the original sources; preserve meaningful context or confirm a legitimate neutral empty result.");
2679
+ store.saveDictionary(candidateDictionary);
2680
+ return store.freeze(packet, result).payload;
2583
2681
  } catch (error) {
2584
- if (attempt === 1) throw error;
2585
- // Only local validator diagnostics enter the isolated correction prompt.
2586
- // Failed model output is never queued, echoed, or sent to Shifu.
2682
+ store.failure(packet);
2683
+ if (reservation.attempt >= 2 || output === undefined) throw error;
2587
2684
  validationFeedback = preparationValidationFeedback(error, output);
2588
2685
  }
2589
2686
  }
2590
2687
  }
2591
2688
 
2689
+ async function retryPreparation(harness) {
2690
+ const sessionRef = option("--session-ref");
2691
+ const fromTurn = numberOption("--from-turn");
2692
+ if (!sessionRefIsSafe(sessionRef) || !Number.isSafeInteger(fromTurn) || fromTurn < 1) throw new Error("Choose the held session and original from-turn explicitly.");
2693
+ const result = await withSyncLock(harness, async () => {
2694
+ const store = continuityState(harness);
2695
+ const state = store.load();
2696
+ const identity = `${sessionRef}:${fromTurn}`;
2697
+ const held = state.attempts[identity];
2698
+ if (!held || held.payloadKey || state.outbox.some((entry) => entry.sessionRef === sessionRef && entry.fromTurn === fromTurn) || readPendingQueue(harness).segments.some((input) => input.sessionRef === sessionRef && input.toTurn >= fromTurn)) {
2699
+ throw new Error("Only an unqueued preparation hold can be retried; frozen payloads keep their exact transport path.");
2700
+ }
2701
+ if ((readState(harness).sessions[sessionRef]?.lastSyncedTurn || 0) >= fromTurn) throw new Error("Accepted ranges require separately authorized historical recovery.");
2702
+ const session = localSessions(harness).find((item) => item.sessionRef === sessionRef);
2703
+ if (!session) throw new Error("The original session is unavailable; the hold was preserved.");
2704
+ held.attempts = 0; held.status = "unresolved"; delete held.reason;
2705
+ store.save(state);
2706
+ const payload = await prepareV5Payload(harness, { ...session, fromTurn, toTurn: held.toTurn });
2707
+ queuePayload(harness, payload);
2708
+ console.error("The selected preparation is queued for its existing approval and transport policy.");
2709
+ });
2710
+ if (result?.skipped) throw new Error("Another sync is running; the hold was preserved.");
2711
+ }
2712
+
2592
2713
  function preparationValidationFeedback(error, output) {
2593
2714
  const fields = [["title", output?.title, 160], ["summary", output?.summary, 1100]];
2594
2715
  for (const name of ["decisions", "verification"]) {
@@ -2631,6 +2752,7 @@ function preparationState(harness, queue) {
2631
2752
  }
2632
2753
 
2633
2754
  async function preparePendingQueue(harness, config, blockedSessions = new Set()) {
2755
+ restoreContinuityQueue(harness, config);
2634
2756
  let queue = prunePendingQueue(harness);
2635
2757
  const previousFailures = new Set((queue.preparationErrors || []).map((error) => error.sessionRef));
2636
2758
  const errors = [];
@@ -2680,8 +2802,13 @@ async function preparePendingQueue(harness, config, blockedSessions = new Set())
2680
2802
  async function drainPendingQueue(harness, config, blockedSessions) {
2681
2803
  let synced = 0;
2682
2804
  let failures = 0;
2683
- for (const input of eligibleQueuedSegments(readPendingQueue(harness), config)) {
2684
- if (blockedSessions.has(input.sessionRef)) continue;
2805
+ const store = continuityState(harness, config);
2806
+ const entries = store.load().outbox.filter((entry) => entry.baseStatus !== "needs_review" && (entry.baseStatus !== "accepted" || entry.toTurn > (readState(harness).sessions[entry.sessionRef]?.lastSyncedTurn || 0)));
2807
+ const queued = eligibleQueuedSegments(readPendingQueue(harness), config);
2808
+ const excluded = new Set(normalizedPolicy(config).excludedSessionRefs);
2809
+ const inputs = [...queued, ...entries.filter((entry) => !excluded.has(entry.sessionRef) && !queued.some((input) => JSON.stringify(input) === JSON.stringify(entry.payload))).map((entry) => entry.payload)];
2810
+ for (const input of inputs) {
2811
+ if (blockedSessions.has(input.sessionRef) || store.find(input)?.baseStatus === "needs_review") continue;
2685
2812
  try { await sendSync(harness, input, config); synced += 1; }
2686
2813
  catch (error) {
2687
2814
  failures += 1;
@@ -2692,11 +2819,35 @@ async function drainPendingQueue(harness, config, blockedSessions) {
2692
2819
  return { synced, failures };
2693
2820
  }
2694
2821
 
2822
+ async function flushContinuityAttachments(harness, config) {
2823
+ const store = continuityState(harness, config);
2824
+ if (!store.load().outbox.some((entry) => entry.baseStatus === "accepted" && !["attached", "not_applicable", "needs_review"].includes(entry.continuityStatus))) return;
2825
+ let capable = false;
2826
+ try {
2827
+ const connection = await validateConnection(config.apiUrl, config.token, harness);
2828
+ capable = connection.capabilities?.continuityAttachment === 1;
2829
+ } catch { /* Base receipts remain valid while capability discovery is unavailable. */ }
2830
+ await require("./continuity-state.js").flushContinuity(store, config, capable);
2831
+ }
2832
+
2833
+ function restoreContinuityQueue(harness, config) {
2834
+ const queue = readPendingQueue(harness);
2835
+ const state = readState(harness);
2836
+ for (const entry of continuityState(harness, config).load().outbox) {
2837
+ if (entry.toTurn > (state.sessions[entry.sessionRef]?.lastSyncedTurn || 0) && entry.baseStatus !== "needs_review" && !queue.segments.some((payload) => JSON.stringify(payload) === JSON.stringify(entry.payload))) queuePayload(harness, entry.payload);
2838
+ }
2839
+ }
2840
+
2695
2841
  function eligibleQueuedSegments(queue, config) {
2696
2842
  const excluded = new Set(normalizedPolicy(config).excludedSessionRefs);
2697
2843
  return queue.segments.filter((input) => !excluded.has(input.sessionRef));
2698
2844
  }
2699
2845
 
2846
+ function continuityRecoveries(harness, config, queued) {
2847
+ const excluded = new Set(normalizedPolicy(config).excludedSessionRefs);
2848
+ return continuityState(harness, config).load().outbox.filter((entry) => entry.baseStatus === "awaiting_receipt" && !excluded.has(entry.sessionRef) && !queued.some((payload) => JSON.stringify(payload) === JSON.stringify(entry.payload))).map((entry) => entry.payload);
2849
+ }
2850
+
2700
2851
  function approvalNotificationInvocation(harness, count, sessionRef) {
2701
2852
  const prompt = `Shifu sync approval: ${count} redacted segment${count === 1 ? " is" : "s are"} waiting locally. Tell the user to ask you to show the pending Shifu sync and approve it once. Do not inspect files, run tools, or sync anything in this session.`;
2702
2853
  if (harness === "codex") {
@@ -2726,6 +2877,12 @@ function notificationSessionRef(output) {
2726
2877
  }
2727
2878
 
2728
2879
  function notifyManualApproval(harness, count, existingSessionRef) {
2880
+ if (harness === "cursor") {
2881
+ const message = `${count} redacted work summaries are waiting. Ask Cursor to show pending Shifu sync and approve it once.`;
2882
+ if (process.platform === "darwin") execFileSync("osascript", ["-e", `display notification ${JSON.stringify(message)} with title "Shifu sync approval"`], { stdio: "ignore", timeout: 10_000 });
2883
+ else execFileSync("notify-send", ["Shifu sync approval", message], { stdio: "ignore", timeout: 10_000 });
2884
+ return "thread_cursor_approval";
2885
+ }
2729
2886
  const invocation = approvalNotificationInvocation(harness, count, existingSessionRef);
2730
2887
  const { resolveNativeBinary, nativeEnvironment } = require("./native-preparation.js");
2731
2888
  const before = existingSessionRef ? new Set() : new Set(localSessions(harness).map((session) => session.sessionRef));
@@ -2740,7 +2897,7 @@ function notifyManualApproval(harness, count, existingSessionRef) {
2740
2897
  if (existingSessionRef) return existingSessionRef;
2741
2898
  const created = notificationSessionRef(output) || localSessions(harness)
2742
2899
  .filter((session) => !before.has(session.sessionRef))
2743
- .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0]?.sessionRef;
2900
+ .sort((left, right) => String(right.updatedAt || "").localeCompare(String(left.updatedAt || "")))[0]?.sessionRef;
2744
2901
  if (!created) throw new Error("The approval session was created but its session reference could not be recorded safely.");
2745
2902
  return created;
2746
2903
  }
@@ -2748,7 +2905,9 @@ function notifyManualApproval(harness, count, existingSessionRef) {
2748
2905
  async function showPending(harness) {
2749
2906
  const config = readConfig(harness);
2750
2907
  const result = await withSyncLock(harness, async () => {
2751
- console.log(JSON.stringify(await preparePendingQueue(harness, config), null, 2));
2908
+ const queue = await preparePendingQueue(harness, config);
2909
+ queue.segments = [...eligibleQueuedSegments(queue, config), ...continuityRecoveries(harness, config, queue.segments)];
2910
+ console.log(JSON.stringify(queue, null, 2));
2752
2911
  });
2753
2912
  if (result?.skipped) throw new Error("Another Shifu sync is already preparing this queue. Retry later.");
2754
2913
  }
@@ -2756,18 +2915,21 @@ async function showPending(harness) {
2756
2915
  async function approvePending(harness) {
2757
2916
  const config = readConfig(harness);
2758
2917
  const result = await withSyncLock(harness, async () => {
2918
+ restoreContinuityQueue(harness, config);
2759
2919
  const queue = prunePendingQueue(harness);
2760
2920
  queue.segments = eligibleQueuedSegments(queue, config);
2761
- if (queue.segments.length === 0) {
2921
+ const recoveries = continuityRecoveries(harness, config, queue.segments);
2922
+ if (queue.segments.length === 0 && recoveries.length === 0) {
2762
2923
  console.error("There is no prepared Shifu activity to approve. Run pending to prepare the next ranges.");
2763
2924
  return;
2764
2925
  }
2765
- console.error(`\nThese ${queue.segments.length} redacted segment(s) will be sent to Shifu:\n${JSON.stringify(queue.segments, null, 2)}`);
2926
+ console.error(`\nThese ${queue.segments.length + recoveries.length} redacted segment(s) will be sent to Shifu:\n${JSON.stringify([...queue.segments, ...recoveries], null, 2)}`);
2766
2927
  if (!hasFlag("--approved") && !(await confirm("Approve all pending segments"))) {
2767
2928
  console.error("Nothing was sent. The prepared queue is unchanged.");
2768
2929
  return;
2769
2930
  }
2770
2931
  const sent = await drainPendingQueue(harness, config, new Set());
2932
+ await flushContinuityAttachments(harness, config);
2771
2933
  if (sent.failures) throw new Error("Some approved segments could not be acknowledged. Their exact payloads remain pending for retry.");
2772
2934
  });
2773
2935
  if (result?.skipped) throw new Error("Another Shifu sync is already running. Nothing was sent; retry later.");
@@ -2800,9 +2962,12 @@ async function scheduledSync(harness) {
2800
2962
  }
2801
2963
  }
2802
2964
  await flushPendingProfiles(harness, config);
2965
+ restoreContinuityQueue(harness, config);
2966
+ await flushContinuityAttachments(harness, config);
2803
2967
  if (policy.approvalMode === "manual") {
2804
2968
  const queue = await preparePendingQueue(harness, config);
2805
- const eligible = eligibleQueuedSegments(queue, config);
2969
+ const queued = eligibleQueuedSegments(queue, config);
2970
+ const eligible = [...queued, ...continuityRecoveries(harness, config, queued)];
2806
2971
  const pendingFingerprint = createHash("sha256").update(eligible.map((segment) => `${segment.sessionRef}:${segment.fromTurn}:${segment.toTurn}`).join("\n")).digest("hex");
2807
2972
  const changed = state.schedule?.pendingFingerprint !== pendingFingerprint;
2808
2973
  let approvalSessionRef = state.schedule?.approvalSessionRef;
@@ -2860,6 +3025,7 @@ async function scheduledSync(harness) {
2860
3025
  nextRunAt: Date.now() + (failures || failedFiles || pendingProfiles || remaining ? SCHEDULER_TICK_MINUTES / 60 : policy.intervalHours) * 3_600_000,
2861
3026
  };
2862
3027
  }
3028
+ await flushContinuityAttachments(harness, config);
2863
3029
  const latest = readState(harness);
2864
3030
  latest.schedule = state.schedule;
2865
3031
  saveState(harness, latest);
@@ -2890,7 +3056,7 @@ function status(harness) {
2890
3056
  if (!eligibility.eligible) initialHistoryBlocked.push({ sessionRef: session.sessionRef, reason: eligibility.reason, cutoffAt: eligibility.cutoffAt });
2891
3057
  }
2892
3058
  }
2893
- console.log(JSON.stringify({ harness, runnerVersion: RUNNER_VERSION, apiUrl: config.apiUrl, inputFolder: folder, inputFolderError, contextPreferences: config.contextPreferences === undefined ? null : normalizeContextPreferences(config.contextPreferences), historicalContext: true, initialBackfill: config.initialBackfill || null, initialHistoryBlocked, syncPolicy: normalizedPolicy(config), schedule: state.schedule || null, pendingSegments: pending.segments.length, completedFiles: entries.filter((entry) => typeof entry === "string").length, pendingFiles: entries.filter((entry) => typeof entry === "object" && entry?.jobId).length, unsupportedFiles, sessions: state.sessions }, null, 2));
3059
+ console.log(JSON.stringify({ continuity: continuityState(harness, config).summary(), harness, runnerVersion: RUNNER_VERSION, apiUrl: config.apiUrl, inputFolder: folder, inputFolderError, contextPreferences: config.contextPreferences === undefined ? null : normalizeContextPreferences(config.contextPreferences), historicalContext: true, initialBackfill: config.initialBackfill || null, initialHistoryBlocked, syncPolicy: normalizedPolicy(config), schedule: state.schedule || null, pendingSegments: pending.segments.length, completedFiles: entries.filter((entry) => typeof entry === "string").length, pendingFiles: entries.filter((entry) => typeof entry === "object" && entry?.jobId).length, unsupportedFiles, sessions: state.sessions }, null, 2));
2894
3060
  }
2895
3061
 
2896
3062
  async function syncFilesNow(harness) {
@@ -3025,7 +3191,7 @@ async function review(harness) {
3025
3191
  const state = readState(harness);
3026
3192
  let fromTurn = (state.sessions[sessionRef]?.lastSyncedTurn || 0) + 1;
3027
3193
  const selected = localSessions(harness).filter((session) => session.sessionRef === sessionRef)
3028
- .sort((left, right) => right.turnCount - left.turnCount || right.updatedAt.localeCompare(left.updatedAt))[0];
3194
+ .sort((left, right) => right.turnCount - left.turnCount || String(right.updatedAt || "").localeCompare(String(left.updatedAt || "")))[0];
3029
3195
  if (!selected) throw new Error(`No local ${HARNESS_NAMES[harness]} session matches this reference.`);
3030
3196
  const hours = syncWindowHours();
3031
3197
  const turns = numberOption("--turns", MAX_TURNS_PER_SEGMENT);
@@ -3104,6 +3270,25 @@ async function updateRecommendedPackage(harness, { currentVersion = RUNNER_VERSI
3104
3270
  }
3105
3271
  }
3106
3272
 
3273
+ async function cursorHook() {
3274
+ if (process.env.SHIFU_CURSOR_PREPARATION === "1" || !fs.existsSync(configPath("cursor"))) return console.log("{}");
3275
+ let input = "";
3276
+ process.stdin.setEncoding("utf8");
3277
+ for await (const chunk of process.stdin) {
3278
+ input += chunk.toString("utf8");
3279
+ if (Buffer.byteLength(input) > 1024 * 1024) throw new Error("Cursor hook input exceeded 1 MB. Nothing was captured.");
3280
+ }
3281
+ const payload = JSON.parse(input);
3282
+ if (payload.hook_event_name === "sessionStart") {
3283
+ const file = contextInstructionFile("cursor");
3284
+ const instruction = fs.existsSync(file) ? readContextInstructionFile(file).original : "";
3285
+ const sessionRef = require("./cursor-sessions.js").sessionRefForConversation(payload.conversation_id || payload.session_id);
3286
+ return console.log(JSON.stringify({ env: { SHIFU_CURSOR_SESSION_REF: sessionRef }, ...(instruction ? { additional_context: instruction } : {}) }));
3287
+ }
3288
+ require("./cursor-sessions.js").captureHook(payload, cursorOptions());
3289
+ console.log("{}");
3290
+ }
3291
+
3107
3292
  async function main() {
3108
3293
  if (option("--config-root")) process.env.SHIFU_CONFIG_ROOT = path.resolve(option("--config-root"));
3109
3294
  const command = process.argv[2];
@@ -3120,6 +3305,11 @@ async function main() {
3120
3305
  });
3121
3306
  return;
3122
3307
  }
3308
+ if (command === "cursor-hook" && harness === "cursor") return cursorHook();
3309
+ if (command === "mcp-bridge" && harness === "cursor") {
3310
+ const config = readConfig(harness);
3311
+ return require("./cursor-mcp.js").runBridge({ url: requireSafeApiUrl(config.mcpUrl || `${config.apiUrl}/mcp`), credentials: () => mcpHeaders(harness) });
3312
+ }
3123
3313
  if (command === "install") return install(harness);
3124
3314
  if (command === "connect") return connect(harness);
3125
3315
  if (command === "mcp-setup") return setupMcp(harness);
@@ -3130,21 +3320,21 @@ async function main() {
3130
3320
  if (command === "sync-files") return syncFilesNow(harness);
3131
3321
  if (command === "scheduled-sync") return scheduledSync(harness);
3132
3322
  if (command === "pending") return showPending(harness);
3323
+ if (command === "retry-preparation") return retryPreparation(harness);
3133
3324
  if (command === "approve") return approvePending(harness);
3134
3325
  if (command === "status") return status(harness);
3135
3326
  if (command === "sessions" || command === "discover" || (command === "session" && (subCommand === "discover" || subCommand === "list" || !subCommand || subCommand.startsWith("-")))) return sessions(harness);
3136
3327
  if (command === "review") return review(harness);
3137
3328
  if (command === "review-source") return console.log(JSON.stringify(reviewSourcePacket(harness, option("--session-ref"), numberOption("--from-turn"), numberOption("--to-turn")), null, 2));
3138
- throw new Error("Use install, connect, mcp-setup, mcp-headers, disconnect, config update, status, sessions, review, review-source, sync, sync-files, pending, approve, or scheduled-sync.");
3329
+ throw new Error("Use install, connect, mcp-setup, mcp-headers, disconnect, config update, status, sessions, review, review-source, sync, sync-files, pending, retry-preparation, approve, or scheduled-sync.");
3139
3330
  }
3140
3331
 
3141
3332
  if (require.main === module) {
3142
3333
  main()
3143
- .then(() => process.exit(0))
3144
3334
  .catch((error) => {
3145
3335
  console.error(error.message);
3146
- process.exit(1);
3336
+ process.exitCode = 1;
3147
3337
  });
3148
3338
  }
3149
3339
 
3150
- module.exports = { setupMcp, olderPackageVersion, updateRecommendedPackage, installPackageVersion, approvalNotificationInvocation, claudeReview, claudeSession, codexReview, codexSession, configPath, configuredSyncPolicy, connect, defaultInputFolder, disconnect, finalAssistantNote, install, installDestination, localSessions, normalizedPolicy, notificationSessionRef, opencodeQuery, opencodeReview, opencodeSessions, payloadForSegment, pendingSegments, prepareV5Payload, prepareInputFolder, profileForSegment, readSecret, removeHooks, requireSafeApiUrl, reviewCandidates, reviewSourcePacket, schedulerArtifact, statePath, syncWorkFiles, textIsSafe, unsyncedCodexSessions, validateAIProfileObservation, validateConnection, validateInputFolder, validSyncReceipt, validateApprovalMode, validateIntervalHours, validateSync, workFiles };
3340
+ module.exports = { setupMcp, olderPackageVersion, updateRecommendedPackage, installPackageVersion, approvalNotificationInvocation, claudeReview, claudeSession, codexReview, codexSession, configPath, configuredSyncPolicy, connect, defaultInputFolder, disconnect, finalAssistantNote, install, installDestination, localSessions, normalizedPolicy, notificationSessionRef, opencodeQuery, opencodeReview, opencodeSessions, payloadForSegment, pendingSegments, prepareV5Payload, preparationPacket, continuityState, flushContinuityAttachments, prepareInputFolder, profileForSegment, readSecret, removeHooks, requireSafeApiUrl, reviewCandidates, reviewSourcePacket, schedulerArtifact, statePath, syncWorkFiles, textIsSafe, unsyncedCodexSessions, validateAIProfileObservation, validateConnection, validateInputFolder, validSyncReceipt, validateApprovalMode, validateIntervalHours, validateSync, workFiles };