@synkro-sh/cli 1.9.0 → 1.10.3

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/bootstrap.js CHANGED
@@ -147,7 +147,7 @@ function getIdentity() {
147
147
  if (cached2) return cached2;
148
148
  let cliVersion2 = "0.0.0";
149
149
  try {
150
- cliVersion2 = "1.9.0";
150
+ cliVersion2 = "1.10.3";
151
151
  } catch {
152
152
  }
153
153
  const creds = loadCredentialsIdentity();
@@ -740,11 +740,19 @@ async function insertEvents(sql, rows, mirroredIds) {
740
740
  async function drainToPglite() {
741
741
  const queued = readQueue().rows;
742
742
  if (queued.length === 0) return { ok: true, ingested: 0, pending: 0, reason: "no_pending" };
743
- const rows = queued.length > MIRROR_MAX_PER_DRAIN ? queued.slice(-MIRROR_MAX_PER_DRAIN) : queued;
743
+ const bounded = queued.length > MIRROR_MAX_PER_DRAIN ? queued.slice(-MIRROR_MAX_PER_DRAIN) : queued;
744
+ const candidates = bounded.filter((r) => !mirroredMemo.has(r.client_event_id));
745
+ if (candidates.length === 0) return { ok: true, ingested: 0, pending: queued.length, reason: "no_pending" };
744
746
  const sql = await connectDb();
745
747
  if (!sql) return { ok: false, ingested: 0, pending: queued.length, reason: "db_unavailable" };
746
748
  try {
747
- const ingested = await insertEvents(sql, rows);
749
+ const ids = candidates.map((r) => r.client_event_id);
750
+ const present = await sql`
751
+ SELECT client_event_id FROM telemetry_events WHERE client_event_id = ANY(${ids})`;
752
+ if (mirroredMemo.size > MIRRORED_MEMO_CAP) mirroredMemo.clear();
753
+ for (const row2 of present) mirroredMemo.add(row2.client_event_id);
754
+ const fresh = candidates.filter((r) => !mirroredMemo.has(r.client_event_id));
755
+ const ingested = fresh.length ? await insertEvents(sql, fresh, mirroredMemo) : 0;
748
756
  patchMetaCache({ last_drained_at: (/* @__PURE__ */ new Date()).toISOString() });
749
757
  return { ok: true, ingested, pending: queued.length };
750
758
  } catch (err) {
@@ -752,7 +760,7 @@ async function drainToPglite() {
752
760
  return { ok: false, ingested: 0, pending: queued.length, reason: "error", error: msg };
753
761
  }
754
762
  }
755
- var SYNKRO_DIR3, QUEUE_LOCK, PROCESSING_PATH, CHUNK, MIRROR_MAX_PER_DRAIN, STALE_LOCK_MS, MAX_QUEUE_ROWS;
763
+ var SYNKRO_DIR3, QUEUE_LOCK, PROCESSING_PATH, CHUNK, MIRROR_MAX_PER_DRAIN, STALE_LOCK_MS, MAX_QUEUE_ROWS, mirroredMemo, MIRRORED_MEMO_CAP;
756
764
  var init_drain = __esm({
757
765
  "cli/telemetry/drain.ts"() {
758
766
  "use strict";
@@ -766,6 +774,8 @@ var init_drain = __esm({
766
774
  MIRROR_MAX_PER_DRAIN = 5e3;
767
775
  STALE_LOCK_MS = 6e4;
768
776
  MAX_QUEUE_ROWS = 5e4;
777
+ mirroredMemo = /* @__PURE__ */ new Set();
778
+ MIRRORED_MEMO_CAP = 5e4;
769
779
  }
770
780
  });
771
781
 
@@ -1684,6 +1694,18 @@ function installCCHooks(settingsPath, config) {
1684
1694
  [SYNKRO_MARKER]: true
1685
1695
  });
1686
1696
  }
1697
+ if (config.mcpFollowupScriptPath) {
1698
+ settings.hooks.PostToolUse.push({
1699
+ matcher: "mcp__.*",
1700
+ hooks: [
1701
+ {
1702
+ type: "command",
1703
+ command: config.mcpFollowupScriptPath
1704
+ }
1705
+ ],
1706
+ [SYNKRO_MARKER]: true
1707
+ });
1708
+ }
1687
1709
  if (config.taskActivateIntentScriptPath) {
1688
1710
  settings.hooks.PreToolUse.push({
1689
1711
  matcher: "mcp__synkro[-_]guardrails__activate_standard",
@@ -1730,7 +1752,12 @@ function installCCHooks(settingsPath, config) {
1730
1752
  {
1731
1753
  type: "command",
1732
1754
  command: config.userPromptSubmitScriptPath,
1733
- timeout: 5
1755
+ // 15s, not 5: the stub is fire-and-forget (no server round-trip on the
1756
+ // interactive path), so the budget only covers LOCAL work — but the SCM
1757
+ // reconcile runs git twice, and on a large dirty checkout with worktrees
1758
+ // and file churn that alone can spike past 5s. A trip here discards the
1759
+ // whole hook output, so size the budget to the slow-git tail.
1760
+ timeout: 15
1734
1761
  }
1735
1762
  ],
1736
1763
  [SYNKRO_MARKER]: true
@@ -2893,12 +2920,12 @@ var init_skillParser = __esm({
2893
2920
  function stubHook(surface, optsLiteral) {
2894
2921
  return "#!/usr/bin/env bun\nimport { runStub } from './_synkro-stub-common.ts';\nrunStub(" + JSON.stringify(surface) + ", " + optsLiteral + ");\n";
2895
2922
  }
2896
- var STUB_COMMON_TS, STUB_EDIT_PRECHECK_TS, STUB_EDIT_FOLLOWUP_TS, STUB_CWE_PRECHECK_TS, STUB_CVE_PRECHECK_TS, STUB_BASH_JUDGE_TS, STUB_SKILL_JUDGE_TS, STUB_INSTALL_SCAN_TS, STUB_AGENT_JUDGE_TS, STUB_MCP_GATE_TS, STUB_PLAN_JUDGE_TS, STUB_STOP_SUMMARY_TS, STUB_SESSION_START_TS, STUB_TRANSCRIPT_SYNC_TS, STUB_CODEX_CWE_STOP_TS, STUB_CODEX_CVE_STOP_TS, STUB_SUBAGENT_START_TS, STUB_SUBAGENT_STOP_TS, STUB_USER_PROMPT_SUBMIT_TS, STUB_BASH_FOLLOWUP_TS, STUB_PROMPT_ROUTE_TS, STUB_TASK_ACTIVATE_INTENT_TS, STUB_CURSOR_BASH_JUDGE_TS, STUB_CURSOR_SKILL_JUDGE_TS, STUB_CURSOR_EDIT_CAPTURE_TS, STUB_CURSOR_AGENT_CAPTURE_TS;
2923
+ var STUB_COMMON_TS, STUB_EDIT_PRECHECK_TS, STUB_EDIT_FOLLOWUP_TS, STUB_CWE_PRECHECK_TS, STUB_CVE_PRECHECK_TS, STUB_BASH_JUDGE_TS, STUB_SKILL_JUDGE_TS, STUB_INSTALL_SCAN_TS, STUB_AGENT_JUDGE_TS, STUB_MCP_GATE_TS, STUB_MCP_FOLLOWUP_TS, STUB_PLAN_JUDGE_TS, STUB_STOP_SUMMARY_TS, STUB_SESSION_START_TS, STUB_TRANSCRIPT_SYNC_TS, STUB_CODEX_CWE_STOP_TS, STUB_CODEX_CVE_STOP_TS, STUB_SUBAGENT_START_TS, STUB_SUBAGENT_STOP_TS, STUB_USER_PROMPT_SUBMIT_TS, STUB_BASH_FOLLOWUP_TS, STUB_PROMPT_ROUTE_TS, STUB_TASK_ACTIVATE_INTENT_TS, STUB_CURSOR_BASH_JUDGE_TS, STUB_CURSOR_SKILL_JUDGE_TS, STUB_CURSOR_EDIT_CAPTURE_TS, STUB_CURSOR_AGENT_CAPTURE_TS;
2897
2924
  var init_hookScriptsTs = __esm({
2898
2925
  "cli/installer/hookScriptsTs.ts"() {
2899
2926
  "use strict";
2900
2927
  STUB_COMMON_TS = String.raw`import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync, appendFileSync, statSync, realpathSync, openSync, readSync, closeSync, unlinkSync } from 'node:fs';
2901
- import { execFileSync, execSync } from 'node:child_process';
2928
+ import { execFileSync, execSync, spawn } from 'node:child_process';
2902
2929
  import { homedir } from 'node:os';
2903
2930
  import { basename, dirname, join, resolve, relative, isAbsolute } from 'node:path';
2904
2931
  import { randomUUID, createHash } from 'node:crypto';
@@ -3674,6 +3701,26 @@ function taskScmWorkspaceInstruction(harness: string, sessionId: string, workspa
3674
3701
  + 'The Codex Environment panel must show that worktree and branch before retrying the blocked action. ' + requirement;
3675
3702
  }
3676
3703
 
3704
+ // CC validates hookSpecificOutput.hookEventName against the event that actually fired.
3705
+ // Server responses and the SCM-context injection historically stamped 'PreToolUse'
3706
+ // unconditionally; on any other surface CC rejects the whole output ("expected
3707
+ // 'UserPromptSubmit' but got 'PreToolUse'") and the hook's work is discarded. The
3708
+ // payload's hook_event_name is the ground truth, so re-stamp at the last mile.
3709
+ // Only CC-shaped responses carry hookSpecificOutput, so no harness check is needed.
3710
+ function withActualHookEvent(responseText: string, payload: any): string {
3711
+ const actual = String((payload && (payload.hook_event_name || payload.hookEventName)) || '');
3712
+ if (!actual) return responseText;
3713
+ try {
3714
+ const response = JSON.parse(responseText || '{}');
3715
+ const hso = response && response.hookSpecificOutput;
3716
+ if (hso && typeof hso === 'object' && hso.hookEventName !== actual) {
3717
+ hso.hookEventName = actual;
3718
+ return JSON.stringify(response);
3719
+ }
3720
+ } catch { /* non-JSON hook output - leave untouched */ }
3721
+ return responseText;
3722
+ }
3723
+
3677
3724
  function withTaskScmContext(responseText: string, harness: string, context: string): string {
3678
3725
  if (!context) return responseText;
3679
3726
  try {
@@ -4592,12 +4639,30 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
4592
4639
  // Dropping an occasional prompt tick is harmless; stalling the user is not.
4593
4640
  const attempts = (opts.telemetry && surface !== 'prompt-submit') ? 3 : 1;
4594
4641
  let text = '';
4595
- for (let i = 0; i < attempts; i++) {
4642
+ if (surface === 'prompt-submit' && process.env.SYNKRO_PROMPT_SUBMIT_SYNC !== '1') {
4643
+ // Fire-and-forget: prompt-submit's reply carries no verdict (telemetry-class,
4644
+ // response ignored) and the SCM workspace context is computed host-side, so
4645
+ // the server round-trip must never hold the user's KEYSTROKE hostage to DB
4646
+ // tail latency — a busy single-threaded PGLite stretch was tripping the 5s
4647
+ // hook budget on every prompt. A detached child delivers the envelope with
4648
+ // retries the interactive path could never afford; delivery reliability goes
4649
+ // UP versus the old single attempt racing a 5s clock.
4596
4650
  try {
4597
- const resp = await fetch(url, { method: 'POST', headers, body, signal: AbortSignal.timeout(timeoutMs) });
4598
- if (resp.ok) { text = (await resp.text()).trim(); break; }
4599
- } catch (e) { /* connection refused / timeout → retry below */ }
4600
- if (i < attempts - 1) await new Promise((r) => setTimeout(r, 500 * (i + 1)));
4651
+ const spoolDir = join(HOME, '.synkro', 'prompt-spool');
4652
+ mkdirSync(spoolDir, { recursive: true });
4653
+ const spoolPath = join(spoolDir, randomUUID() + '.json');
4654
+ writeFileSync(spoolPath, body);
4655
+ const child = spawn(process.execPath, [import.meta.path, '--deliver-spool', spoolPath], { detached: true, stdio: 'ignore' });
4656
+ child.unref();
4657
+ } catch { /* best-effort - the prompt must never block on delivery */ }
4658
+ } else {
4659
+ for (let i = 0; i < attempts; i++) {
4660
+ try {
4661
+ const resp = await fetch(url, { method: 'POST', headers, body, signal: AbortSignal.timeout(timeoutMs) });
4662
+ if (resp.ok) { text = (await resp.text()).trim(); break; }
4663
+ } catch (e) { /* connection refused / timeout → retry below */ }
4664
+ if (i < attempts - 1) await new Promise((r) => setTimeout(r, 500 * (i + 1)));
4665
+ }
4601
4666
  }
4602
4667
  const rawResponseText = text || failOpen(harness);
4603
4668
  // A completion-report sync can arm the push after the pre-scan reconciliation ran.
@@ -4606,15 +4671,19 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
4606
4671
  const contractResponseText = opts.stopContract && harness === 'codex'
4607
4672
  ? codexStopResponse(rawResponseText)
4608
4673
  : rawResponseText;
4609
- // Only the first hook of this tool call carries the workspace context; the other
4610
- // surfaces of the same call would otherwise repeat it verbatim 2-4 times.
4611
- let scmContext = scm.context && firstHookForToolCall(sessionId, payload) ? scm.context : '';
4674
+ // The informational workspace banner rides prompt-submit ONLY. Tool-call and stop
4675
+ // surfaces stay quiet: a pending workspace question already blocks the next
4676
+ // substantive tool with taskScmBlockResponse (which carries the full instructions
4677
+ // itself), and the server still receives scm.context via the envelope's
4678
+ // taskWorkspaceContext on every surface — so nothing enforcement- or data-bearing
4679
+ // is lost by silencing the per-tool-call and per-stop repeats.
4680
+ let scmContext = scm.context && surface === 'prompt-submit' && firstHookForToolCall(sessionId, payload) ? scm.context : '';
4612
4681
  // Say plainly that work is continuing outside the task worktree by the user's choice,
4613
4682
  // so the state is never mistaken for a binding that silently failed.
4614
4683
  if (scmContext && scmTaskId && taskWorkspaceStayRecorded(scmTaskId)) {
4615
4684
  scmContext += ' workspace=staying-here-by-user-choice';
4616
4685
  }
4617
- const responseText = withTaskScmContext(contractResponseText, harness, scmContext);
4686
+ const responseText = withActualHookEvent(withTaskScmContext(contractResponseText, harness, scmContext), payload);
4618
4687
  out(responseText);
4619
4688
  emitStubTelemetry(surface, harness, telemPayload, responseText, Date.now() - startedAt, telemCwd, telemSessionId);
4620
4689
  } catch (err) {
@@ -4847,6 +4916,31 @@ function emitStubTelemetry(
4847
4916
  }, opts);
4848
4917
  }
4849
4918
  }
4919
+
4920
+ // ─── Detached prompt-submit delivery child ──────────────────────────────────
4921
+ // The interactive parent spools the envelope and exits immediately; this child
4922
+ // (spawned detached from runStub's prompt-submit path) delivers it with retries
4923
+ // and a budget the 5s hook clock could never allow. Runs only when this file is
4924
+ // executed directly with --deliver-spool; a plain import never reaches it.
4925
+ async function deliverSpooledEnvelope(path: string): Promise<void> {
4926
+ let body = '';
4927
+ try { body = readFileSync(path, 'utf-8'); } catch { return; }
4928
+ const url = 'http://127.0.0.1:' + PORT + '/api/scan/prompt-submit';
4929
+ const headers = { 'Content-Type': 'application/json', Authorization: 'Bearer ' + loadMcpJwt() };
4930
+ for (let i = 0; i < 3; i++) {
4931
+ try {
4932
+ const resp = await fetch(url, { method: 'POST', headers, body, signal: AbortSignal.timeout(20000) });
4933
+ if (resp.ok) break;
4934
+ } catch { /* server busy or restarting - retry below */ }
4935
+ if (i < 2) await new Promise((r) => setTimeout(r, 2000 * (i + 1)));
4936
+ }
4937
+ try { unlinkSync(path); } catch { /* already collected */ }
4938
+ }
4939
+
4940
+ if (import.meta.main && process.argv[2] === '--deliver-spool' && process.argv[3]) {
4941
+ await deliverSpooledEnvelope(process.argv[3]);
4942
+ process.exit(0);
4943
+ }
4850
4944
  `;
4851
4945
  STUB_EDIT_PRECHECK_TS = stubHook("edit-precheck", "{ needsFile: true, needsTranscript: true }");
4852
4946
  STUB_EDIT_FOLLOWUP_TS = stubHook("edit-followup", "{ needsFile: true, needsTranscript: true, postEdit: true }");
@@ -4857,6 +4951,7 @@ function emitStubTelemetry(
4857
4951
  STUB_INSTALL_SCAN_TS = stubHook("install-scan", "{ needsTranscript: true }");
4858
4952
  STUB_AGENT_JUDGE_TS = stubHook("agent-judge", "{ needsTranscript: true }");
4859
4953
  STUB_MCP_GATE_TS = stubHook("mcp-gate", "{ needsTranscript: true }");
4954
+ STUB_MCP_FOLLOWUP_TS = stubHook("mcp-followup", "{ telemetry: true }");
4860
4955
  STUB_PLAN_JUDGE_TS = stubHook("plan-judge", "{ needsPlan: true }");
4861
4956
  STUB_STOP_SUMMARY_TS = stubHook("stop-summary", "{ needsTranscript: true, fullTranscript: true }");
4862
4957
  STUB_SESSION_START_TS = stubHook("session-start", "{ telemetry: true }");
@@ -7745,7 +7840,7 @@ var init_dockerInstall = __esm({
7745
7840
  HOST_PGLITE_PORT = parseInt(process.env.SYNKRO_HOST_PGLITE_PORT || "15433", 10);
7746
7841
  CONTAINER_NAME = resolveContainerName();
7747
7842
  defaultImageVersion = () => {
7748
- if (true) return "1.9.0";
7843
+ if (true) return "1.10.3";
7749
7844
  try {
7750
7845
  const pkg = JSON.parse(readFileSync17(new URL("../../package.json", import.meta.url), "utf8"));
7751
7846
  if (pkg.version) return pkg.version;
@@ -8705,6 +8800,7 @@ function writeHookScripts() {
8705
8800
  const mcpStdioProxyPath = join19(HOOKS_DIR, "mcp-stdio-proxy.ts");
8706
8801
  const taskActivateIntentScriptPath = join19(HOOKS_DIR, "cc-task-activate-intent.ts");
8707
8802
  const mcpGateScriptPath = join19(HOOKS_DIR, "cc-mcp-gate.ts");
8803
+ const mcpFollowupScriptPath = join19(HOOKS_DIR, "cc-mcp-followup.ts");
8708
8804
  const stubCommonPath = join19(HOOKS_DIR, "_synkro-stub-common.ts");
8709
8805
  const stubFiles = [
8710
8806
  [stubCommonPath, STUB_COMMON_TS],
@@ -8730,6 +8826,7 @@ function writeHookScripts() {
8730
8826
  [installScanScriptPath, STUB_INSTALL_SCAN_TS],
8731
8827
  [taskActivateIntentScriptPath, STUB_TASK_ACTIVATE_INTENT_TS],
8732
8828
  [mcpGateScriptPath, STUB_MCP_GATE_TS],
8829
+ [mcpFollowupScriptPath, STUB_MCP_FOLLOWUP_TS],
8733
8830
  [cursorBashJudgePath, STUB_CURSOR_BASH_JUDGE_TS],
8734
8831
  [cursorEditCapturePath, STUB_CURSOR_EDIT_CAPTURE_TS],
8735
8832
  [cursorAgentCapturePath, STUB_CURSOR_AGENT_CAPTURE_TS]
@@ -8770,7 +8867,8 @@ function writeHookScripts() {
8770
8867
  cursorEditCaptureScript: cursorEditCapturePath,
8771
8868
  cursorAgentCaptureScript: cursorAgentCapturePath,
8772
8869
  taskActivateIntentScript: taskActivateIntentScriptPath,
8773
- mcpGateScript: mcpGateScriptPath
8870
+ mcpGateScript: mcpGateScriptPath,
8871
+ mcpFollowupScript: mcpFollowupScriptPath
8774
8872
  };
8775
8873
  }
8776
8874
  function sanitizeConfigValue(raw, maxLen = 256) {
@@ -8802,7 +8900,7 @@ function writeConfigEnv(opts) {
8802
8900
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle2(credsPath)}`,
8803
8901
  `SYNKRO_TIER=${shellQuoteSingle2(safeTier)}`,
8804
8902
  `SYNKRO_INFERENCE=${shellQuoteSingle2(safeInference)}`,
8805
- `SYNKRO_VERSION=${shellQuoteSingle2("1.9.0")}`
8903
+ `SYNKRO_VERSION=${shellQuoteSingle2("1.10.3")}`
8806
8904
  ];
8807
8905
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle2(safeSynkroBin)}`);
8808
8906
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle2(safeUserId)}`);
@@ -9549,7 +9647,7 @@ async function installCommand(opts = {}) {
9549
9647
  await setTelemetryState({ enabled: true, remoteFlushEnabled: telemetryConsent });
9550
9648
  emit("install", {
9551
9649
  phase: "started",
9552
- cli_version_to: "1.9.0",
9650
+ cli_version_to: "1.10.3",
9553
9651
  agents_detected: agents.map((a) => a.kind),
9554
9652
  with_github: false,
9555
9653
  with_local_cc: false,
@@ -9593,6 +9691,7 @@ async function installCommand(opts = {}) {
9593
9691
  installScanScriptPath: scripts.installScanScript,
9594
9692
  taskActivateIntentScriptPath: scripts.taskActivateIntentScript,
9595
9693
  mcpGateScriptPath: scripts.mcpGateScript,
9694
+ mcpFollowupScriptPath: scripts.mcpFollowupScript,
9596
9695
  skipTranscriptSync: !transcriptCC
9597
9696
  });
9598
9697
  console.log(`Configured ${agent.name} hooks at ${agent.settingsPath}`);
@@ -16272,7 +16371,7 @@ function pseudonymousHostnameHash(installationId, host) {
16272
16371
  }
16273
16372
  function cliVersion() {
16274
16373
  try {
16275
- return "1.9.0";
16374
+ return "1.10.3";
16276
16375
  } catch {
16277
16376
  return "0.0.0";
16278
16377
  }
@@ -17041,7 +17140,7 @@ var subArgs = args.slice(1);
17041
17140
  var isDetachedChild = process.env.SYNKRO_TELEMETRY_DETACHED === "1";
17042
17141
  var FLUSH_SKIP = /* @__PURE__ */ new Set(["grade", "inventory-sync", "version", "--version", "-v", "help", "--help", "-h", ""]);
17043
17142
  function printVersion() {
17044
- console.log("1.9.0");
17143
+ console.log("1.10.3");
17045
17144
  }
17046
17145
  function printHelp2() {
17047
17146
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents