@synkro-sh/cli 1.6.74 → 1.6.75

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
@@ -9852,7 +9852,10 @@ function captureClaudeSetupToken() {
9852
9852
  const bin = "script";
9853
9853
  const args2 = isMac ? ["-q", tmpFile, "claude", "setup-token"] : ["-qec", "claude setup-token", tmpFile];
9854
9854
  return new Promise((resolve4, reject) => {
9855
- const proc = nodeSpawn(bin, args2, { stdio: "inherit" });
9855
+ const proc = nodeSpawn(bin, args2, {
9856
+ stdio: "inherit",
9857
+ env: { ...process.env, FORCE_COLOR: "3", COLORTERM: "truecolor", TERM: "xterm-256color" }
9858
+ });
9856
9859
  proc.on("error", (err) => reject(new Error(`Failed to spawn claude setup-token: ${err.message}`)));
9857
9860
  proc.on("close", (code) => {
9858
9861
  let raw = "";
@@ -9870,27 +9873,48 @@ function captureClaudeSetupToken() {
9870
9873
  reject(new Error(`claude setup-token exited with code ${code}`));
9871
9874
  return;
9872
9875
  }
9873
- const stripToToken = (s) => s.replace(/\x1B\[[0-9;?]*[a-zA-Z]/g, "").replace(/\x1B\][^\x07]*\x07/g, "").replace(/[\s\x00-\x1F\x7F]/g, "");
9874
- const yellowRe = /\x1B\[38;2;255;193;7m([^\x1B]*)/g;
9875
- let yellow = "";
9876
- let m;
9877
- while ((m = yellowRe.exec(raw)) !== null) yellow += m[1];
9878
- const yToken = stripToToken(yellow);
9879
- let lineMatch = "";
9880
- const clean = raw.replace(/\x1B\[[0-9;?]*[a-zA-Z]/g, "").replace(/\x1B\][^\x07]*\x07/g, "");
9881
- const lines = clean.split("\n").map((l) => l.trim());
9882
- for (let i = 0; i < lines.length; i++) {
9883
- const idx = lines[i].indexOf("sk-ant-oat01-");
9884
- if (idx < 0) continue;
9885
- let tok = (lines[i].slice(idx).match(/^sk-ant-oat01-[A-Za-z0-9_-]*/) || [""])[0];
9886
- for (let j = i + 1; j < lines.length && /^[A-Za-z0-9_-]+$/.test(lines[j]); j++) tok += lines[j];
9887
- lineMatch = tok;
9888
- break;
9876
+ const TOKEN_YELLOW = "2;255;193;7";
9877
+ let fg = "default";
9878
+ let token = "";
9879
+ let i = 0;
9880
+ while (i < raw.length) {
9881
+ if (raw[i] === "\x1B") {
9882
+ const sgr = /^\x1B\[([0-9;?]*)m/.exec(raw.slice(i));
9883
+ if (sgr) {
9884
+ const codes = sgr[1].split(";").map((n) => Number(n) || 0);
9885
+ for (let k = 0; k < codes.length; k++) {
9886
+ const c = codes[k];
9887
+ if (c === 0 || c === 39) fg = "default";
9888
+ else if (c === 38 && codes[k + 1] === 2) {
9889
+ fg = `2;${codes[k + 2]};${codes[k + 3]};${codes[k + 4]}`;
9890
+ k += 4;
9891
+ } else if (c === 38 && codes[k + 1] === 5) {
9892
+ fg = `5;${codes[k + 2]}`;
9893
+ k += 2;
9894
+ } else if (c >= 30 && c <= 37 || c >= 90 && c <= 97) fg = `basic;${c}`;
9895
+ }
9896
+ i += sgr[0].length;
9897
+ continue;
9898
+ }
9899
+ const csi = /^\x1B\[[0-9;?]*[ -/]*[@-~]/.exec(raw.slice(i));
9900
+ const osc = /^\x1B\][^\x07]*(?:\x07|\x1B\\)/.exec(raw.slice(i));
9901
+ if (csi) {
9902
+ i += csi[0].length;
9903
+ continue;
9904
+ }
9905
+ if (osc) {
9906
+ i += osc[0].length;
9907
+ continue;
9908
+ }
9909
+ i += 1;
9910
+ continue;
9911
+ }
9912
+ const ch = raw[i];
9913
+ if (fg === TOKEN_YELLOW && !/[\s\x00-\x1F\x7F]/.test(ch)) token += ch;
9914
+ i += 1;
9889
9915
  }
9890
- const valid = (t) => /^sk-ant-oat01-[A-Za-z0-9_-]{40,}$/.test(t);
9891
- const token = valid(yToken) ? yToken : valid(lineMatch) ? lineMatch : "";
9892
9916
  if (!token) {
9893
- reject(new Error(`Could not capture a full token from claude setup-token output (file=${raw.length}b, yellow=${yellow.length}b, yToken=${yToken.length}b)`));
9917
+ reject(new Error(`Captured no yellow token text from claude setup-token output (raw=${raw.length}b \u2014 is the terminal emitting color?)`));
9894
9918
  return;
9895
9919
  }
9896
9920
  resolve4(token);
@@ -10421,9 +10445,9 @@ function writeHookScripts(mode = "stub") {
10421
10445
  const taskActivateIntentScriptPath = join10(HOOKS_DIR, "cc-task-activate-intent.ts");
10422
10446
  const mcpGateScriptPath = join10(HOOKS_DIR, "cc-mcp-gate.ts");
10423
10447
  if (mode === "stub") {
10424
- const stubCommonPath = join10(HOOKS_DIR, "_synkro-stub-common.ts");
10448
+ const stubCommonPath2 = join10(HOOKS_DIR, "_synkro-stub-common.ts");
10425
10449
  const stubFiles = [
10426
- [stubCommonPath, STUB_COMMON_TS],
10450
+ [stubCommonPath2, STUB_COMMON_TS],
10427
10451
  [bashScriptPath, STUB_BASH_JUDGE_TS],
10428
10452
  [bashFollowupScriptPath, STUB_BASH_FOLLOWUP_TS],
10429
10453
  [editPrecheckScriptPath, STUB_EDIT_PRECHECK_TS],
@@ -10494,6 +10518,9 @@ function writeHookScripts(mode = "stub") {
10494
10518
  writeFileSync8(mcpStdioProxyPath, MCP_STDIO_PROXY_SRC, "utf-8");
10495
10519
  writeFileSync8(installExtractCorePath, "/**\n * Deterministic install-command extraction \u2014 no LLM, no network.\n * Shared by the API pkg-scan route and the hook scripts (copied to ~/.synkro/hooks/).\n */\nimport { parse as shellParse } from 'shell-quote';\n\nexport interface DeterministicPkgRequest {\n name: string;\n version: string;\n ecosystem: string;\n}\n\ninterface RawInstall {\n ecosystem: string;\n name: string;\n versionSpec: string | null;\n source: 'registry' | 'git' | 'local' | 'url' | 'unknown';\n}\n\nconst SEPARATOR_OPS = new Set(['&&', '||', ';', '|', '&', '\\n']);\n\n/** Split a shell command into command segments (each an argv string array). */\nexport function segmentCommand(command: string): string[][] {\n let tokens: unknown[];\n try {\n tokens = shellParse(command) as unknown[];\n } catch {\n return [command.split(/\\s+/).filter(Boolean)];\n }\n const segments: string[][] = [];\n let current: string[] = [];\n for (const tok of tokens) {\n if (typeof tok === 'string') {\n current.push(tok);\n continue;\n }\n if (tok && typeof tok === 'object') {\n const op = (tok as { op?: string }).op;\n const pattern = (tok as { pattern?: string }).pattern;\n if (op && SEPARATOR_OPS.has(op)) {\n if (current.length) segments.push(current);\n current = [];\n } else if (typeof pattern === 'string') {\n current.push(pattern);\n }\n }\n }\n if (current.length) segments.push(current);\n return segments;\n}\n\nconst PM_TABLE: Record<string, { subs: Set<string>; ecosystem: string }> = {\n npm: { subs: new Set(['install', 'i', 'add', 'ci']), ecosystem: 'npm' },\n pnpm: { subs: new Set(['add', 'install', 'i', 'dlx']), ecosystem: 'npm' },\n yarn: { subs: new Set(['add', 'install']), ecosystem: 'npm' },\n bun: { subs: new Set(['add', 'install', 'i']), ecosystem: 'npm' },\n pip: { subs: new Set(['install']), ecosystem: 'PyPI' },\n pip3: { subs: new Set(['install']), ecosystem: 'PyPI' },\n cargo: { subs: new Set(['add', 'install']), ecosystem: 'crates.io' },\n go: { subs: new Set(['get', 'install']), ecosystem: 'Go' },\n gem: { subs: new Set(['install']), ecosystem: 'RubyGems' },\n composer: { subs: new Set(['require']), ecosystem: 'Packagist' },\n};\nconst SYSTEM_PMS = new Set(['apt', 'apt-get', 'apk', 'brew', 'dnf', 'yum', 'pacman']);\nconst SYSTEM_SUBS = new Set(['install', 'add']);\n\nconst WRAPPERS = new Set(['sudo', 'doas', 'command', 'env', 'xargs', 'nice', 'time']);\nconst VALUE_FLAGS = new Set([\n '--filter', '-F', '-C', '--dir', '--prefix', '--registry', '--tag', '--features',\n '-v', '--version', '--index-url', '--extra-index-url', '--target', '-t',\n]);\n\nfunction basename(p: string): string {\n const i = p.lastIndexOf('/');\n return i >= 0 ? p.slice(i + 1) : p;\n}\n\nfunction stripPrefixes(argv: string[]): string[] {\n let i = 0;\n while (i < argv.length) {\n const t = argv[i];\n if (WRAPPERS.has(basename(t).toLowerCase())) { i++; continue; }\n if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(t)) { i++; continue; }\n break;\n }\n return argv.slice(i);\n}\n\nfunction looksLikePath(tok: string): boolean {\n return tok === '.' || tok === '..' || /^\\.{0,2}\\//.test(tok) || tok.startsWith('~/') || tok.startsWith('file:');\n}\n\n/** Shell redirect fragments (e.g. `2>&1` \u2192 argv `2`, `1`) \u2014 not package names. */\nfunction isRedirectFragment(tok: string): boolean {\n if (/^\\d+$/.test(tok)) return true;\n if (/^[<>]|[<>]$/.test(tok)) return true;\n if (tok === '&' || tok === '|') return true;\n if (/^\\d*[<>]/.test(tok)) return true;\n return false;\n}\n\nfunction parsePackageToken(tok: string, ecosystem: string): RawInstall | null {\n if (/^(https?:)?\\/\\//.test(tok) || tok.startsWith('git+') || tok.startsWith('git:')) {\n return { ecosystem, name: tok, versionSpec: null, source: tok.includes('git') ? 'git' : 'url' };\n }\n if (looksLikePath(tok)) {\n return { ecosystem, name: basename(tok.replace(/\\/+$/, '')) || tok, versionSpec: null, source: 'local' };\n }\n if (ecosystem === 'PyPI') {\n const noExtras = tok.replace(/\\[[^\\]]*\\]/g, '');\n const m = noExtras.match(/^([A-Za-z0-9_.-]+)\\s*([=~!<>].*)?$/);\n if (!m) return null;\n return { ecosystem, name: m[1], versionSpec: m[2] ? m[2].trim() : null, source: 'registry' };\n }\n if (ecosystem === 'Packagist') {\n // composer uses vendor/package:version-constraint (e.g. monolog/monolog:1.0.0).\n // Split on the first ':' after the vendor/ slash; never on the '/'.\n const ci = tok.indexOf(':');\n if (ci > 0) return { ecosystem, name: tok.slice(0, ci), versionSpec: tok.slice(ci + 1) || null, source: 'registry' };\n return { ecosystem, name: tok, versionSpec: null, source: 'registry' };\n }\n const at = tok.lastIndexOf('@');\n if (at > 0) {\n return { ecosystem, name: tok.slice(0, at), versionSpec: tok.slice(at + 1) || null, source: 'registry' };\n }\n return { ecosystem, name: tok, versionSpec: null, source: 'registry' };\n}\n\n/** Deterministic extraction for a single command segment. */\nexport function extractSegment(rawArgv: string[]): RawInstall[] {\n let argv = stripPrefixes(rawArgv);\n if (argv.length < 2) return [];\n let bin = basename(argv[0]).toLowerCase();\n\n if (bin === 'uv' && argv[1] === 'pip') { argv = argv.slice(1); bin = 'pip'; }\n if ((bin === 'python' || bin === 'python3') && argv.includes('-m')) {\n const mi = argv.indexOf('-m');\n if (argv[mi + 1] === 'pip') { argv = ['pip', ...argv.slice(mi + 2)]; bin = 'pip'; }\n }\n\n const isSystem = SYSTEM_PMS.has(bin);\n const entry = PM_TABLE[bin];\n if (!entry && !isSystem) return [];\n const ecosystem = entry ? entry.ecosystem : 'system';\n const subs = entry ? entry.subs : SYSTEM_SUBS;\n\n let subIdx = -1;\n for (let i = 1; i < argv.length; i++) {\n if (subs.has(argv[i].toLowerCase())) { subIdx = i; break; }\n }\n if (subIdx === -1) return [];\n\n const installs: RawInstall[] = [];\n // gem pins the version with a separate `-v`/`--version` flag rather than an\n // inline spec; capture it and apply to the package(s) in this segment.\n let flagVersion: string | null = null;\n for (let i = subIdx + 1; i < argv.length; i++) {\n const tok = argv[i];\n if (isRedirectFragment(tok)) break;\n if (tok.startsWith('-')) {\n const lower = tok.toLowerCase();\n if (ecosystem === 'RubyGems' && (lower === '-v' || lower === '--version')) {\n flagVersion = (argv[i + 1] || '').replace(/^[v=\\s]+/, '') || null;\n i++;\n continue;\n }\n if (VALUE_FLAGS.has(tok)) i++;\n continue;\n }\n const parsed = parsePackageToken(tok, ecosystem);\n if (parsed) installs.push(parsed);\n }\n if (flagVersion) {\n for (const ins of installs) {\n if (ins.source === 'registry' && !ins.versionSpec) ins.versionSpec = flagVersion;\n }\n }\n return installs;\n}\n\nconst ECO_TO_OSV: Record<string, string | null> = {\n npm: 'npm',\n pypi: 'PyPI', PyPI: 'PyPI',\n cargo: 'crates.io', 'crates.io': 'crates.io',\n go: 'Go', Go: 'Go',\n rubygems: 'RubyGems', RubyGems: 'RubyGems',\n packagist: 'Packagist', Packagist: 'Packagist',\n maven: 'Maven', Maven: 'Maven',\n nuget: 'NuGet', NuGet: 'NuGet',\n apt: null, brew: null, system: null, other: null,\n};\n\nfunction normalizeName(name: string, osvEco: string): string {\n const n = name.trim();\n if (osvEco === 'npm') return n.toLowerCase();\n if (osvEco === 'PyPI') return n.toLowerCase().replace(/[-_.]+/g, '-');\n return n;\n}\n\nfunction concretePin(spec: string | null): string | null {\n if (!spec) return null;\n const c = spec.trim().replace(/^[v=\\s]+/, '');\n if (c.toLowerCase() === 'latest' || c === '') return null;\n if (/[\\^~><|*\\sx]/i.test(c)) return null;\n return /^\\d[\\w.\\-+]*$/.test(c) ? c : null;\n}\n\nconst PKG_JSON_DEP_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];\n\nfunction safeParseObject(text: string): Record<string, any> | null {\n try {\n const v = JSON.parse(text);\n return v && typeof v === 'object' && !Array.isArray(v) ? v : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Diff two package.json contents and return the registry packages that are\n * newly added or whose version spec changed in the new content. The caller\n * scans these against the vuln DB before letting the edit land \u2014 so a bare\n * `pnpm install` afterwards has nothing left to vet. Non-registry sources\n * (file:, link:, workspace:, git, http, relative paths) are skipped.\n */\nexport function extractPackageJsonDelta(oldText: string, newText: string): DeterministicPkgRequest[] {\n const newJson = safeParseObject(newText);\n if (!newJson) return [];\n const oldJson = safeParseObject(oldText) || {};\n\n const out = new Map<string, DeterministicPkgRequest>();\n for (const field of PKG_JSON_DEP_FIELDS) {\n const oldDeps = (oldJson[field] && typeof oldJson[field] === 'object') ? oldJson[field] : {};\n const newDeps = (newJson[field] && typeof newJson[field] === 'object') ? newJson[field] : {};\n for (const [rawName, version] of Object.entries(newDeps)) {\n if (typeof version !== 'string') continue;\n if (oldDeps[rawName] === version) continue;\n const spec = version.trim();\n if (\n spec.startsWith('file:') || spec.startsWith('link:') ||\n spec.startsWith('http') || spec.startsWith('git') ||\n spec.startsWith('workspace:') || spec.startsWith('catalog:') ||\n spec.startsWith('npm:') ||\n spec.startsWith('./') || spec.startsWith('../') ||\n spec === '' || spec === '*'\n ) continue;\n const name = rawName.toLowerCase();\n out.set(name, {\n name,\n version: concretePin(spec) ?? '*',\n ecosystem: 'npm',\n });\n }\n }\n return [...out.values()];\n}\n\n/**\n * Parse registry installs from a shell command without LLM/network.\n * Unpinned versions use '*' so OSV scans the full advisory history.\n */\nexport function extractDeterministicPkgRequests(command: string): DeterministicPkgRequest[] {\n const merged = new Map<string, DeterministicPkgRequest>();\n for (const r of segmentCommand(command).flatMap(extractSegment)) {\n if (r.source !== 'registry') continue;\n const osvEco = ECO_TO_OSV[r.ecosystem] ?? ECO_TO_OSV[r.ecosystem.toLowerCase()] ?? null;\n if (!osvEco) continue;\n const name = normalizeName(r.name, osvEco);\n if (!name) continue;\n const key = osvEco + '|' + name.toLowerCase();\n const version = concretePin(r.versionSpec) ?? '*';\n const prev = merged.get(key);\n if (!prev || (prev.version === '*' && version !== '*')) {\n merged.set(key, { name, version, ecosystem: osvEco });\n }\n }\n return [...merged.values()];\n}\n", "utf-8");
10496
10520
  writeFileSync8(taskActivateIntentScriptPath, STUB_TASK_ACTIVATE_INTENT_TS, "utf-8");
10521
+ const stubCommonPath = join10(HOOKS_DIR, "_synkro-stub-common.ts");
10522
+ writeFileSync8(stubCommonPath, STUB_COMMON_TS, "utf-8");
10523
+ chmodSync2(stubCommonPath, 493);
10497
10524
  writeFileSync8(mcpGateScriptPath, STUB_MCP_GATE_TS, "utf-8");
10498
10525
  chmodSync2(bashScriptPath, 493);
10499
10526
  chmodSync2(bashFollowupScriptPath, 493);
@@ -10516,10 +10543,6 @@ function writeHookScripts(mode = "stub") {
10516
10543
  chmodSync2(installExtractCorePath, 493);
10517
10544
  chmodSync2(taskActivateIntentScriptPath, 493);
10518
10545
  chmodSync2(mcpGateScriptPath, 493);
10519
- try {
10520
- unlinkSync4(join10(HOOKS_DIR, "_synkro-stub-common.ts"));
10521
- } catch {
10522
- }
10523
10546
  return {
10524
10547
  bashScript: bashScriptPath,
10525
10548
  bashFollowupScript: bashFollowupScriptPath,
@@ -10569,7 +10592,7 @@ function writeConfigEnv(opts) {
10569
10592
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
10570
10593
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
10571
10594
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
10572
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.74")}`
10595
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.75")}`
10573
10596
  ];
10574
10597
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
10575
10598
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -10632,6 +10655,24 @@ async function provisionCloudContainer(opts) {
10632
10655
  console.error(" Cloud needs a Claude setup-token. Run `claude setup-token` manually, then re-run install.");
10633
10656
  process.exit(1);
10634
10657
  }
10658
+ console.log(" Validating Claude token...");
10659
+ try {
10660
+ const out = execSync6('claude --print --output-format json "say ok"', {
10661
+ env: { ...process.env, CLAUDE_CODE_OAUTH_TOKEN: setupToken },
10662
+ encoding: "utf-8",
10663
+ timeout: 3e4,
10664
+ stdio: ["ignore", "pipe", "pipe"]
10665
+ });
10666
+ const result = JSON.parse(out);
10667
+ if (result.is_error) throw new Error(result.result || "auth failed");
10668
+ console.log(" \u2713 Claude token validated.\n");
10669
+ } catch (err) {
10670
+ const msg = err instanceof Error ? err.message : String(err);
10671
+ console.error(` \u2717 Claude token failed validation: ${msg}`);
10672
+ console.error(" The captured token can't authenticate (truncated capture, wrong account, or no API credits).");
10673
+ console.error(" Re-run install to re-authorize \u2014 nothing was stored.");
10674
+ process.exit(1);
10675
+ }
10635
10676
  const repo = detectGitRepo2() || void 0;
10636
10677
  const sf = readFullSynkroFile();
10637
10678
  const harness = [];
@@ -14221,7 +14262,7 @@ var args = process.argv.slice(2);
14221
14262
  var cmd = args[0] || "";
14222
14263
  var subArgs = args.slice(1);
14223
14264
  function printVersion() {
14224
- console.log("1.6.74");
14265
+ console.log("1.6.75");
14225
14266
  }
14226
14267
  function printHelp2() {
14227
14268
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents