@synkro-sh/cli 1.6.84 → 1.6.86

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
@@ -1846,6 +1846,33 @@ export function ruleFilterText(action: string, userMessage?: string | null): str
1846
1846
 
1847
1847
  export async function filterRules(commandText: string, allRules: Rule[]): Promise<Rule[]> {
1848
1848
  if (allRules.length <= 3) return allRules;
1849
+
1850
+ // Cloud: the embedding index lives server-side (the container grade path can't
1851
+ // reach 127.0.0.1), so ask the gateway for the top-3 relevant rules \u2014 same
1852
+ // CF/BYOK bge-base vectors, same behavior as local. Without this, cloud grades
1853
+ // dumped EVERY rule into the prompt (9KB+), the dominant cause of grade
1854
+ // timeouts. Any failure falls back to all rules (status quo), never blocks.
1855
+ if (!isLocalStorageMode()) {
1856
+ const jwt = loadJwt();
1857
+ if (!jwt) return allRules;
1858
+ try {
1859
+ const resp = await fetch(GATEWAY_URL + '/api/v1/hook/filter-rules', {
1860
+ method: 'POST',
1861
+ headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
1862
+ body: JSON.stringify({ text: commandText, top_k: 3 }),
1863
+ signal: AbortSignal.timeout(2000),
1864
+ });
1865
+ if (!resp.ok) return allRules;
1866
+ const data = await resp.json() as { rules?: Array<Record<string, unknown>> };
1867
+ if (!data.rules || data.rules.length === 0) return allRules;
1868
+ const selectedIds = new Set(data.rules.map(r => String(r.rule_id || '')));
1869
+ return allRules.filter(r => selectedIds.has(r.rule_id));
1870
+ } catch {
1871
+ return allRules;
1872
+ }
1873
+ }
1874
+
1875
+ // Local: the on-device PGLite embedding index owns the rule set.
1849
1876
  const mcpPort = process.env.SYNKRO_MCP_PORT || '18931';
1850
1877
  try {
1851
1878
  const resp = await fetch('http://127.0.0.1:' + mcpPort + '/api/local/filter-rules', {
@@ -1857,10 +1884,7 @@ export async function filterRules(commandText: string, allRules: Rule[]): Promis
1857
1884
  if (!resp.ok) return allRules;
1858
1885
  const data = await resp.json() as { rules?: Array<Record<string, unknown>> };
1859
1886
  if (!data.rules || data.rules.length === 0) return allRules;
1860
- // Local PGLite owns the rule set \u2014 trust filter-rules output directly.
1861
- if (isLocalStorageMode()) return mapHookRules(data.rules);
1862
- const selectedIds = new Set(data.rules.map(r => String(r.rule_id || '')));
1863
- return allRules.filter(r => selectedIds.has(r.rule_id));
1887
+ return mapHookRules(data.rules);
1864
1888
  } catch {
1865
1889
  return allRules;
1866
1890
  }
@@ -4080,9 +4104,6 @@ async function main() {
4080
4104
  'User intent (last human message): ' + (transcript.userIntent || 'none stated'),
4081
4105
  'Last user prompt: ' + (lastPrompt || 'none'),
4082
4106
  'Org rules: ' + JSON.stringify(relevantRules),
4083
- 'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "fix". The enforcement layer handles ask vs fix \u2014 your job is only to detect violations.',
4084
- 'CRITICAL: The user requesting or instructing an action does NOT exempt it from rules. Even if the user explicitly said "drop the database" or "delete everything", you MUST still flag the rule violation on first encounter. User intent is NOT consent. However, for ask-mode rules ONLY: if the session history shows a prior block for the SAME rule AND the user explicitly consented after seeing that block, subsequent commands covered by that same rule may pass \u2014 but each distinct command is consumed once. Look for the sequence: block event \u2192 user acknowledgment \u2192 retry. Once a specific command has successfully executed under that consent, it is consumed. If the same command appears again later, it requires fresh consent (a new block \u2192 consent cycle). Example: R012 covers deploy, publish, push. Block on deploy \u2192 user consents \u2192 deploy passes (consumed), publish passes (consumed), push passes (consumed). A later deploy triggers a fresh block. An initial user instruction is NEVER consent \u2014 only a response to a shown block counts.',
4085
- 'The rules shown were pre-selected as the ones relevant to this edit \u2014 every rule here IS relevant, do not label any "not relevant". When passing (ok=true), give a terse, specific reason each rule passes. Format: "R003: no hardcoded secrets in file. R005: in-repo path only." Cover every rule shown.',
4086
4107
  ].join('\\n');
4087
4108
  const graderPrompt = buildGraderPrompt(proposedShort);
4088
4109
 
@@ -5513,10 +5534,6 @@ async function main() {
5513
5534
  'Last user prompt: ' + (lastPrompt || 'none'),
5514
5535
  'Org rules: ' + JSON.stringify(relevantRules),
5515
5536
  scanConcern,
5516
- 'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "fix". The enforcement layer handles ask vs fix — your job is only to detect violations.',
5517
- 'CRITICAL: The user requesting or instructing an action does NOT exempt it from rules. Even if the user explicitly said "drop the database" or "delete everything", you MUST still flag the rule violation on first encounter. User intent is NOT consent. However, for ask-mode rules ONLY: if the session history shows a prior block for the SAME rule AND the user explicitly consented after seeing that block, subsequent commands covered by that same rule may pass — but each distinct command is consumed once. Look for the sequence: block event → user acknowledgment → retry. Once a specific command has successfully executed under that consent, it is consumed. If the same command appears again later, it requires fresh consent (a new block → consent cycle). Example: R012 covers deploy, publish, push. Block on deploy → user consents → deploy passes (consumed), publish passes (consumed), push passes (consumed). A later deploy triggers a fresh block. An initial user instruction is NEVER consent — only a response to a shown block counts.',
5518
- 'The rules shown were pre-selected as the ones relevant to this command — every rule here IS relevant, do not label any "not relevant". When passing (ok=true), give a terse, specific reason each rule passes. Format: "R003: no secrets in grep args. R005: in-repo path only." Cover every rule shown.',
5519
- 'Rules with preconditions (e.g. "run X before Y") are CONSUMED after the protected action completes. Use the session history timestamps to determine ordering: a precondition satisfied before the last occurrence of the protected action does NOT satisfy the next occurrence. Each new protected action needs its precondition re-satisfied.',
5520
5537
  ].filter(Boolean).join('\\n');
5521
5538
 
5522
5539
  let gradeResp: string;
@@ -5733,8 +5750,6 @@ async function main() {
5733
5750
  'User intent (last human message): ' + (transcript.userIntent || 'none stated'),
5734
5751
  'Last user prompt: ' + (lastPrompt || 'none'),
5735
5752
  'Org rules: ' + JSON.stringify(relevantRules),
5736
- 'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "fix". The enforcement layer handles ask vs fix \u2014 your job is only to detect violations.',
5737
- 'CRITICAL: The user requesting or instructing an action does NOT exempt it from rules. Even if the user explicitly said "drop the database" or "delete everything", you MUST still flag the rule violation on first encounter. User intent is NOT consent. However, for ask-mode rules ONLY: if the session history shows a prior block for the SAME rule AND the user explicitly consented after seeing that block, subsequent commands covered by that same rule may pass \u2014 but each distinct command is consumed once. Look for the sequence: block event \u2192 user acknowledgment \u2192 retry. Once a specific command has successfully executed under that consent, it is consumed. If the same command appears again later, it requires fresh consent (a new block \u2192 consent cycle). Example: R012 covers deploy, publish, push. Block on deploy \u2192 user consents \u2192 deploy passes (consumed), publish passes (consumed), push passes (consumed). A later deploy triggers a fresh block. An initial user instruction is NEVER consent \u2014 only a response to a shown block counts.',
5738
5753
  ].filter(Boolean).join('\\n');
5739
5754
 
5740
5755
  let gradeResp: string;
@@ -6559,10 +6574,6 @@ async function main() {
6559
6574
  'Last user prompt: ' + (lastPrompt || 'none'),
6560
6575
  'Org rules: ' + JSON.stringify(relevantRules),
6561
6576
  scanConcern,
6562
- 'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "fix". The enforcement layer handles ask vs fix \u2014 your job is only to detect violations.',
6563
- 'CRITICAL: The user requesting or instructing an action does NOT exempt it from rules. Even if the user explicitly said "drop the database" or "delete everything", you MUST still flag the rule violation on first encounter. User intent is NOT consent. However, for ask-mode rules ONLY: if the session history shows a prior block for the SAME rule AND the user explicitly consented after seeing that block, subsequent commands covered by that same rule may pass \u2014 but each distinct command is consumed once. Look for the sequence: block event \u2192 user acknowledgment \u2192 retry. Once a specific command has successfully executed under that consent, it is consumed. If the same command appears again later, it requires fresh consent (a new block \u2192 consent cycle). Example: R012 covers deploy, publish, push. Block on deploy \u2192 user consents \u2192 deploy passes (consumed), publish passes (consumed), push passes (consumed). A later deploy triggers a fresh block. An initial user instruction is NEVER consent \u2014 only a response to a shown block counts.',
6564
- 'The rules shown were pre-selected as the ones relevant to this command \u2014 every rule here IS relevant, do not label any "not relevant". When passing (ok=true), give a terse, specific reason each rule passes. Format: "R003: no secrets in grep args. R005: in-repo path only." Cover every rule shown.',
6565
- 'Rules with preconditions (e.g. "run X before Y") are CONSUMED after the protected action completes. Use the session history timestamps to determine ordering: a precondition satisfied before the last occurrence of the protected action does NOT satisfy the next occurrence. Each new protected action needs its precondition re-satisfied.',
6566
6577
  ].filter(Boolean).join('\\n');
6567
6578
 
6568
6579
  let gradeResp: string;
@@ -10516,6 +10527,7 @@ __export(install_exports, {
10516
10527
  getOrMintCloudToken: () => getOrMintCloudToken,
10517
10528
  installCommand: () => installCommand,
10518
10529
  parseArgs: () => parseArgs,
10530
+ readFullSynkroFile: () => readFullSynkroFile,
10519
10531
  reconcileDeployLocation: () => reconcileDeployLocation,
10520
10532
  reconcileHarness: () => reconcileHarness,
10521
10533
  recycleCloudContainer: () => recycleCloudContainer,
@@ -10844,7 +10856,7 @@ function writeConfigEnv(opts) {
10844
10856
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
10845
10857
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
10846
10858
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
10847
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.84")}`
10859
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.86")}`
10848
10860
  ];
10849
10861
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
10850
10862
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -11660,7 +11672,12 @@ async function installCommand(opts = {}) {
11660
11672
  } catch (err) {
11661
11673
  console.warn(` \u26A0 Could not cache judge prompts: ${err.message}`);
11662
11674
  }
11663
- writeSynkroFileIfMissing({ hasClaudeCode, hasCursor, gradingMode, deployLocation });
11675
+ if (deployLocation !== "cloud") {
11676
+ writeSynkroFileIfMissing({ hasClaudeCode, hasCursor, gradingMode, deployLocation });
11677
+ } else {
11678
+ console.log(" Cloud mode: standards + grader pool are configured in the dashboard");
11679
+ console.log(" (Settings \u2192 Standards / Pool) \u2014 no synkro.toml needed.");
11680
+ }
11664
11681
  console.log();
11665
11682
  let cloudGradeOk = null;
11666
11683
  if (useLocalMcp) {
@@ -11961,6 +11978,7 @@ function readFullSynkroFile() {
11961
11978
  ruleset: parsed.ruleset || "default",
11962
11979
  skills: Array.isArray(parsed.skills) ? parsed.skills.filter((s) => typeof s === "string" && s.endsWith(".md")) : [],
11963
11980
  scanning: { cwe: parsed.scanning?.cwe !== false, cve: parsed.scanning?.cve !== false },
11981
+ standards: parsed.standards && typeof parsed.standards === "object" ? Object.fromEntries(Object.entries(parsed.standards).filter(([k, v]) => typeof v === "string" && k.includes("/")).map(([k, v]) => [k, String(v)])) : {},
11964
11982
  _repoRoot: root
11965
11983
  };
11966
11984
  } catch {
@@ -14425,6 +14443,182 @@ var init_import = __esm({
14425
14443
  }
14426
14444
  });
14427
14445
 
14446
+ // cli/installer/packVerify.ts
14447
+ import crypto from "crypto";
14448
+ function stableStringify(value) {
14449
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
14450
+ if (Array.isArray(value)) return "[" + value.map(stableStringify).join(",") + "]";
14451
+ const obj = value;
14452
+ const keys = Object.keys(obj).sort();
14453
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
14454
+ }
14455
+ function canonicalize(pack) {
14456
+ return stableStringify({
14457
+ rules: pack.rules ?? [],
14458
+ docs: pack.docs ?? []
14459
+ });
14460
+ }
14461
+ function computeDigest(canonical) {
14462
+ return "sha256:" + crypto.createHash("sha256").update(canonical, "utf8").digest("hex");
14463
+ }
14464
+ function verifySignature(digest, signatureB64, publicKeyPem) {
14465
+ try {
14466
+ const key = crypto.createPublicKey(publicKeyPem);
14467
+ return crypto.verify(null, Buffer.from(digest, "utf8"), key, Buffer.from(signatureB64, "base64"));
14468
+ } catch {
14469
+ return false;
14470
+ }
14471
+ }
14472
+ function verifyPack(pack, claimedDigest, signatureB64, publicKeyPem) {
14473
+ const recomputed = computeDigest(canonicalize(pack));
14474
+ if (recomputed !== claimedDigest) return false;
14475
+ return verifySignature(claimedDigest, signatureB64, publicKeyPem);
14476
+ }
14477
+ var init_packVerify = __esm({
14478
+ "cli/installer/packVerify.ts"() {
14479
+ "use strict";
14480
+ }
14481
+ });
14482
+
14483
+ // cli/installer/lockfile.ts
14484
+ import { existsSync as existsSync18, readFileSync as readFileSync16, writeFileSync as writeFileSync11 } from "fs";
14485
+ import { join as join18 } from "path";
14486
+ function lockPath(repoRoot) {
14487
+ return join18(repoRoot, LOCK_FILE);
14488
+ }
14489
+ function writeLockfile(repoRoot, entries) {
14490
+ const sorted = [...entries].sort((a, b) => a.ref.localeCompare(b.ref));
14491
+ const body = [
14492
+ "# synkro.lock \u2014 generated by `synkro sync`. Commit this file.",
14493
+ "# Pins each subscribed pack to its verified, attested digest.",
14494
+ "",
14495
+ ...sorted.flatMap((e) => [
14496
+ "[[pack]]",
14497
+ `ref = "${e.ref}"`,
14498
+ `version = "${e.version}"`,
14499
+ `digest = "${e.digest}"`,
14500
+ `signature = "${e.signature}"`,
14501
+ `signing_key_id = "${e.signingKeyId}"`,
14502
+ ""
14503
+ ])
14504
+ ].join("\n");
14505
+ writeFileSync11(lockPath(repoRoot), body, "utf-8");
14506
+ }
14507
+ var LOCK_FILE;
14508
+ var init_lockfile = __esm({
14509
+ "cli/installer/lockfile.ts"() {
14510
+ "use strict";
14511
+ LOCK_FILE = "synkro.lock";
14512
+ }
14513
+ });
14514
+
14515
+ // cli/commands/sync.ts
14516
+ var sync_exports = {};
14517
+ __export(sync_exports, {
14518
+ syncCommand: () => syncCommand
14519
+ });
14520
+ import { existsSync as existsSync19, mkdirSync as mkdirSync12, readdirSync as readdirSync6, rmSync as rmSync2, writeFileSync as writeFileSync12 } from "fs";
14521
+ import { homedir as homedir18 } from "os";
14522
+ import { join as join19 } from "path";
14523
+ function cacheKey(ref, version) {
14524
+ return ref.replace(/\//g, "__").replace(/[^\w.@-]/g, "_") + "@" + version + ".json";
14525
+ }
14526
+ async function syncCommand(_args = []) {
14527
+ if (process.env.SYNKRO_DEPLOY_LOCATION === "cloud") {
14528
+ console.log("Cloud mode: standards are applied org-wide from the dashboard");
14529
+ console.log("(Settings \u2192 Standards). No synkro.toml or `synkro sync` needed \u2014");
14530
+ console.log("packs enforce automatically wherever Synkro is installed.");
14531
+ return;
14532
+ }
14533
+ const sf = readFullSynkroFile();
14534
+ if (!sf) {
14535
+ console.error("No synkro.toml found in the repo root. Run `synkro install` first.");
14536
+ process.exitCode = 1;
14537
+ return;
14538
+ }
14539
+ const refs = Object.entries(sf.standards);
14540
+ if (refs.length === 0) {
14541
+ console.log("No [standards] subscriptions in synkro.toml \u2014 nothing to sync.");
14542
+ return;
14543
+ }
14544
+ await ensureValidToken();
14545
+ const token = getAccessToken();
14546
+ if (!token) {
14547
+ console.error("Not logged in. Run `synkro login` and try again.");
14548
+ process.exitCode = 1;
14549
+ return;
14550
+ }
14551
+ const gateway = (process.env.SYNKRO_GATEWAY_URL || "https://api.synkro.sh").replace(/\/$/, "");
14552
+ const cloud = process.env.SYNKRO_DEPLOY_LOCATION === "cloud";
14553
+ const cacheDir = join19(homedir18(), ".synkro", "cache", "packs");
14554
+ if (!cloud) mkdirSync12(cacheDir, { recursive: true });
14555
+ console.log(`Syncing ${refs.length} standard(s) from the registry\u2026`);
14556
+ const lock = [];
14557
+ const keptCacheFiles = /* @__PURE__ */ new Set();
14558
+ for (const [ref, version] of refs) {
14559
+ let url = `${gateway}/api/v1/registry/resolve?ref=${encodeURIComponent(ref)}`;
14560
+ if (version && version !== "latest") url += `&version=${encodeURIComponent(version)}`;
14561
+ let data;
14562
+ try {
14563
+ const resp = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
14564
+ if (!resp.ok) {
14565
+ console.error(` \u2717 ${ref}: resolve failed (${resp.status} ${resp.statusText})`);
14566
+ continue;
14567
+ }
14568
+ data = await resp.json();
14569
+ } catch (err) {
14570
+ console.error(` \u2717 ${ref}: ${err.message}`);
14571
+ continue;
14572
+ }
14573
+ const pack = { rules: data.rules ?? [], docs: data.docs ?? [], manifest: data.manifest ?? {} };
14574
+ if (!verifyPack(pack, data.digest, data.signature, data.publicKey)) {
14575
+ console.error(` \u2717 ${ref}: signature/digest verification FAILED \u2014 refusing to apply.`);
14576
+ continue;
14577
+ }
14578
+ lock.push({ ref, version: data.version, digest: data.digest, signature: data.signature, signingKeyId: data.signingKeyId });
14579
+ if (!cloud) {
14580
+ const fname = cacheKey(ref, data.version);
14581
+ keptCacheFiles.add(fname);
14582
+ writeFileSync12(join19(cacheDir, fname), JSON.stringify({
14583
+ ref,
14584
+ version: data.version,
14585
+ digest: data.digest,
14586
+ rules: pack.rules,
14587
+ docs: pack.docs,
14588
+ manifest: pack.manifest
14589
+ }), "utf-8");
14590
+ }
14591
+ const ruleCount = Array.isArray(pack.rules) ? pack.rules.length : 0;
14592
+ console.log(` \u2713 ${ref}:${data.version} \u2014 verified (${ruleCount} rule${ruleCount === 1 ? "" : "s"})`);
14593
+ }
14594
+ if (!cloud && existsSync19(cacheDir)) {
14595
+ for (const f of readdirSync6(cacheDir)) {
14596
+ if (f.endsWith(".json") && !keptCacheFiles.has(f)) {
14597
+ try {
14598
+ rmSync2(join19(cacheDir, f));
14599
+ } catch {
14600
+ }
14601
+ }
14602
+ }
14603
+ }
14604
+ if (lock.length > 0) {
14605
+ writeLockfile(sf._repoRoot, lock);
14606
+ console.log(`Wrote synkro.lock (${lock.length} pack${lock.length === 1 ? "" : "s"} pinned).`);
14607
+ } else {
14608
+ console.error("No packs synced successfully.");
14609
+ process.exitCode = 1;
14610
+ }
14611
+ }
14612
+ var init_sync = __esm({
14613
+ "cli/commands/sync.ts"() {
14614
+ "use strict";
14615
+ init_install();
14616
+ init_stub();
14617
+ init_packVerify();
14618
+ init_lockfile();
14619
+ }
14620
+ });
14621
+
14428
14622
  // cli/commands/lifecycle.ts
14429
14623
  var lifecycle_exports = {};
14430
14624
  __export(lifecycle_exports, {
@@ -14537,13 +14731,13 @@ var config_exports = {};
14537
14731
  __export(config_exports, {
14538
14732
  configCommand: () => configCommand
14539
14733
  });
14540
- import { readFileSync as readFileSync16, writeFileSync as writeFileSync11, existsSync as existsSync18 } from "fs";
14541
- import { join as join18 } from "path";
14542
- import { homedir as homedir18 } from "os";
14734
+ import { readFileSync as readFileSync17, writeFileSync as writeFileSync13, existsSync as existsSync20 } from "fs";
14735
+ import { join as join20 } from "path";
14736
+ import { homedir as homedir19 } from "os";
14543
14737
  function readConfigEnv2() {
14544
- if (!existsSync18(CONFIG_PATH6)) return {};
14738
+ if (!existsSync20(CONFIG_PATH6)) return {};
14545
14739
  const out = {};
14546
- for (const line of readFileSync16(CONFIG_PATH6, "utf-8").split("\n")) {
14740
+ for (const line of readFileSync17(CONFIG_PATH6, "utf-8").split("\n")) {
14547
14741
  const t = line.trim();
14548
14742
  if (!t || t.startsWith("#")) continue;
14549
14743
  const eq = t.indexOf("=");
@@ -14552,11 +14746,11 @@ function readConfigEnv2() {
14552
14746
  return out;
14553
14747
  }
14554
14748
  function updateConfigValue(key, value) {
14555
- if (!existsSync18(CONFIG_PATH6)) {
14749
+ if (!existsSync20(CONFIG_PATH6)) {
14556
14750
  console.error("No config found. Run `synkro install` first.");
14557
14751
  process.exit(1);
14558
14752
  }
14559
- const lines = readFileSync16(CONFIG_PATH6, "utf-8").split("\n");
14753
+ const lines = readFileSync17(CONFIG_PATH6, "utf-8").split("\n");
14560
14754
  const pattern = new RegExp(`^${key}=`);
14561
14755
  let found = false;
14562
14756
  const updated = lines.map((line) => {
@@ -14567,7 +14761,7 @@ function updateConfigValue(key, value) {
14567
14761
  return line;
14568
14762
  });
14569
14763
  if (!found) updated.splice(updated.length - 1, 0, `${key}='${value}'`);
14570
- writeFileSync11(CONFIG_PATH6, updated.join("\n"), "utf-8");
14764
+ writeFileSync13(CONFIG_PATH6, updated.join("\n"), "utf-8");
14571
14765
  }
14572
14766
  async function reconcileContainer() {
14573
14767
  const cfg = readConfigEnv2();
@@ -14677,20 +14871,20 @@ var init_config = __esm({
14677
14871
  "cli/commands/config.ts"() {
14678
14872
  "use strict";
14679
14873
  init_stub();
14680
- SYNKRO_DIR7 = join18(homedir18(), ".synkro");
14681
- CONFIG_PATH6 = join18(SYNKRO_DIR7, "config.env");
14874
+ SYNKRO_DIR7 = join20(homedir19(), ".synkro");
14875
+ CONFIG_PATH6 = join20(SYNKRO_DIR7, "config.env");
14682
14876
  }
14683
14877
  });
14684
14878
 
14685
14879
  // cli/bootstrap.js
14686
- import { readFileSync as readFileSync17, existsSync as existsSync19 } from "fs";
14880
+ import { readFileSync as readFileSync18, existsSync as existsSync21 } from "fs";
14687
14881
  import { resolve as resolve3 } from "path";
14688
14882
  var envCandidates = [
14689
14883
  resolve3(process.env.HOME ?? "", ".synkro", "config.env")
14690
14884
  ];
14691
14885
  for (const envPath of envCandidates) {
14692
- if (!existsSync19(envPath)) continue;
14693
- const envContent = readFileSync17(envPath, "utf-8");
14886
+ if (!existsSync21(envPath)) continue;
14887
+ const envContent = readFileSync18(envPath, "utf-8");
14694
14888
  for (const line of envContent.split("\n")) {
14695
14889
  const trimmed = line.trim();
14696
14890
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -14705,7 +14899,7 @@ var args = process.argv.slice(2);
14705
14899
  var cmd = args[0] || "";
14706
14900
  var subArgs = args.slice(1);
14707
14901
  function printVersion() {
14708
- console.log("1.6.84");
14902
+ console.log("1.6.86");
14709
14903
  }
14710
14904
  function printHelp2() {
14711
14905
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
@@ -14784,6 +14978,11 @@ async function main() {
14784
14978
  await importCommand2();
14785
14979
  break;
14786
14980
  }
14981
+ case "sync": {
14982
+ const { syncCommand: syncCommand2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
14983
+ await syncCommand2(subArgs);
14984
+ break;
14985
+ }
14787
14986
  case "version":
14788
14987
  case "--version":
14789
14988
  case "-v": {