@synkro-sh/cli 1.6.83 → 1.6.85

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
@@ -1570,7 +1570,12 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
1570
1570
  .filter((e: any) => e && typeof e.path === 'string')
1571
1571
  .map((e: any) => ({ path: e.path, cwe_id: e.cwe_id || '' }));
1572
1572
  }
1573
- return config;
1573
+ // Trust the local result as authoritative ONLY when it actually carried a
1574
+ // policy (or we're in local storage mode, where no cloud fallback exists).
1575
+ // A transient empty local read (policy:null) must NOT short-circuit the
1576
+ // gateway fallback \u2014 otherwise the grade runs ruleless and the tag flaps to
1577
+ // "[\u2026:all:\u2026] no org rules to evaluate" on a repo that DOES have rules.
1578
+ if (policy || isLocalStorageMode()) return config;
1574
1579
  }
1575
1580
  } catch {}
1576
1581
 
@@ -1841,6 +1846,33 @@ export function ruleFilterText(action: string, userMessage?: string | null): str
1841
1846
 
1842
1847
  export async function filterRules(commandText: string, allRules: Rule[]): Promise<Rule[]> {
1843
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.
1844
1876
  const mcpPort = process.env.SYNKRO_MCP_PORT || '18931';
1845
1877
  try {
1846
1878
  const resp = await fetch('http://127.0.0.1:' + mcpPort + '/api/local/filter-rules', {
@@ -1852,10 +1884,7 @@ export async function filterRules(commandText: string, allRules: Rule[]): Promis
1852
1884
  if (!resp.ok) return allRules;
1853
1885
  const data = await resp.json() as { rules?: Array<Record<string, unknown>> };
1854
1886
  if (!data.rules || data.rules.length === 0) return allRules;
1855
- // Local PGLite owns the rule set \u2014 trust filter-rules output directly.
1856
- if (isLocalStorageMode()) return mapHookRules(data.rules);
1857
- const selectedIds = new Set(data.rules.map(r => String(r.rule_id || '')));
1858
- return allRules.filter(r => selectedIds.has(r.rule_id));
1887
+ return mapHookRules(data.rules);
1859
1888
  } catch {
1860
1889
  return allRules;
1861
1890
  }
@@ -3027,7 +3056,7 @@ export async function syncConversationTranscript(
3027
3056
  const b = c as Record<string, unknown>;
3028
3057
  return {
3029
3058
  name: b.name,
3030
- input: JSON.stringify(b.input || b.arguments || {}).slice(0, 500),
3059
+ input: JSON.stringify(b.input || b.arguments || {}).slice(0, 20000),
3031
3060
  id: b.id,
3032
3061
  };
3033
3062
  });
@@ -10511,6 +10540,7 @@ __export(install_exports, {
10511
10540
  getOrMintCloudToken: () => getOrMintCloudToken,
10512
10541
  installCommand: () => installCommand,
10513
10542
  parseArgs: () => parseArgs,
10543
+ readFullSynkroFile: () => readFullSynkroFile,
10514
10544
  reconcileDeployLocation: () => reconcileDeployLocation,
10515
10545
  reconcileHarness: () => reconcileHarness,
10516
10546
  recycleCloudContainer: () => recycleCloudContainer,
@@ -10839,7 +10869,7 @@ function writeConfigEnv(opts) {
10839
10869
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
10840
10870
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
10841
10871
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
10842
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.83")}`
10872
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.85")}`
10843
10873
  ];
10844
10874
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
10845
10875
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -11956,6 +11986,7 @@ function readFullSynkroFile() {
11956
11986
  ruleset: parsed.ruleset || "default",
11957
11987
  skills: Array.isArray(parsed.skills) ? parsed.skills.filter((s) => typeof s === "string" && s.endsWith(".md")) : [],
11958
11988
  scanning: { cwe: parsed.scanning?.cwe !== false, cve: parsed.scanning?.cve !== false },
11989
+ 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)])) : {},
11959
11990
  _repoRoot: root
11960
11991
  };
11961
11992
  } catch {
@@ -14299,7 +14330,7 @@ function parseSession(filePath, sessionId) {
14299
14330
  const msgObj = e.message;
14300
14331
  const text = extractText(msgObj?.content ?? e.content);
14301
14332
  const blocks = kind === "assistant" && Array.isArray(msgObj?.content) ? msgObj.content : [];
14302
- const toolCalls = blocks.filter((b) => b?.type === "tool_use").map((b) => ({ name: b.name, input: JSON.stringify(b.input || {}).slice(0, 500), id: b.id }));
14333
+ const toolCalls = blocks.filter((b) => b?.type === "tool_use").map((b) => ({ name: b.name, input: JSON.stringify(b.input || {}).slice(0, 2e4), id: b.id }));
14303
14334
  if (!text.trim() && toolCalls.length === 0) continue;
14304
14335
  const msg = {
14305
14336
  message_index: i,
@@ -14420,6 +14451,177 @@ var init_import = __esm({
14420
14451
  }
14421
14452
  });
14422
14453
 
14454
+ // cli/installer/packVerify.ts
14455
+ import crypto from "crypto";
14456
+ function stableStringify(value) {
14457
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
14458
+ if (Array.isArray(value)) return "[" + value.map(stableStringify).join(",") + "]";
14459
+ const obj = value;
14460
+ const keys = Object.keys(obj).sort();
14461
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
14462
+ }
14463
+ function canonicalize(pack) {
14464
+ return stableStringify({
14465
+ rules: pack.rules ?? [],
14466
+ docs: pack.docs ?? [],
14467
+ manifest: pack.manifest ?? {}
14468
+ });
14469
+ }
14470
+ function computeDigest(canonical) {
14471
+ return "sha256:" + crypto.createHash("sha256").update(canonical, "utf8").digest("hex");
14472
+ }
14473
+ function verifySignature(digest, signatureB64, publicKeyPem) {
14474
+ try {
14475
+ const key = crypto.createPublicKey(publicKeyPem);
14476
+ return crypto.verify(null, Buffer.from(digest, "utf8"), key, Buffer.from(signatureB64, "base64"));
14477
+ } catch {
14478
+ return false;
14479
+ }
14480
+ }
14481
+ function verifyPack(pack, claimedDigest, signatureB64, publicKeyPem) {
14482
+ const recomputed = computeDigest(canonicalize(pack));
14483
+ if (recomputed !== claimedDigest) return false;
14484
+ return verifySignature(claimedDigest, signatureB64, publicKeyPem);
14485
+ }
14486
+ var init_packVerify = __esm({
14487
+ "cli/installer/packVerify.ts"() {
14488
+ "use strict";
14489
+ }
14490
+ });
14491
+
14492
+ // cli/installer/lockfile.ts
14493
+ import { existsSync as existsSync18, readFileSync as readFileSync16, writeFileSync as writeFileSync11 } from "fs";
14494
+ import { join as join18 } from "path";
14495
+ function lockPath(repoRoot) {
14496
+ return join18(repoRoot, LOCK_FILE);
14497
+ }
14498
+ function writeLockfile(repoRoot, entries) {
14499
+ const sorted = [...entries].sort((a, b) => a.ref.localeCompare(b.ref));
14500
+ const body = [
14501
+ "# synkro.lock \u2014 generated by `synkro sync`. Commit this file.",
14502
+ "# Pins each subscribed pack to its verified, attested digest.",
14503
+ "",
14504
+ ...sorted.flatMap((e) => [
14505
+ "[[pack]]",
14506
+ `ref = "${e.ref}"`,
14507
+ `version = "${e.version}"`,
14508
+ `digest = "${e.digest}"`,
14509
+ `signature = "${e.signature}"`,
14510
+ `signing_key_id = "${e.signingKeyId}"`,
14511
+ ""
14512
+ ])
14513
+ ].join("\n");
14514
+ writeFileSync11(lockPath(repoRoot), body, "utf-8");
14515
+ }
14516
+ var LOCK_FILE;
14517
+ var init_lockfile = __esm({
14518
+ "cli/installer/lockfile.ts"() {
14519
+ "use strict";
14520
+ LOCK_FILE = "synkro.lock";
14521
+ }
14522
+ });
14523
+
14524
+ // cli/commands/sync.ts
14525
+ var sync_exports = {};
14526
+ __export(sync_exports, {
14527
+ syncCommand: () => syncCommand
14528
+ });
14529
+ import { existsSync as existsSync19, mkdirSync as mkdirSync12, readdirSync as readdirSync6, rmSync as rmSync2, writeFileSync as writeFileSync12 } from "fs";
14530
+ import { homedir as homedir18 } from "os";
14531
+ import { join as join19 } from "path";
14532
+ function cacheKey(ref, version) {
14533
+ return ref.replace(/\//g, "__").replace(/[^\w.@-]/g, "_") + "@" + version + ".json";
14534
+ }
14535
+ async function syncCommand(_args = []) {
14536
+ const sf = readFullSynkroFile();
14537
+ if (!sf) {
14538
+ console.error("No synkro.toml found in the repo root. Run `synkro install` first.");
14539
+ process.exitCode = 1;
14540
+ return;
14541
+ }
14542
+ const refs = Object.entries(sf.standards);
14543
+ if (refs.length === 0) {
14544
+ console.log("No [standards] subscriptions in synkro.toml \u2014 nothing to sync.");
14545
+ return;
14546
+ }
14547
+ await ensureValidToken();
14548
+ const token = getAccessToken();
14549
+ if (!token) {
14550
+ console.error("Not logged in. Run `synkro login` and try again.");
14551
+ process.exitCode = 1;
14552
+ return;
14553
+ }
14554
+ const gateway = (process.env.SYNKRO_GATEWAY_URL || "https://api.synkro.sh").replace(/\/$/, "");
14555
+ const cloud = process.env.SYNKRO_DEPLOY_LOCATION === "cloud";
14556
+ const cacheDir = join19(homedir18(), ".synkro", "cache", "packs");
14557
+ if (!cloud) mkdirSync12(cacheDir, { recursive: true });
14558
+ console.log(`Syncing ${refs.length} standard(s) from the registry\u2026`);
14559
+ const lock = [];
14560
+ const keptCacheFiles = /* @__PURE__ */ new Set();
14561
+ for (const [ref, version] of refs) {
14562
+ let url = `${gateway}/api/v1/registry/resolve?ref=${encodeURIComponent(ref)}`;
14563
+ if (version && version !== "latest") url += `&version=${encodeURIComponent(version)}`;
14564
+ let data;
14565
+ try {
14566
+ const resp = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
14567
+ if (!resp.ok) {
14568
+ console.error(` \u2717 ${ref}: resolve failed (${resp.status} ${resp.statusText})`);
14569
+ continue;
14570
+ }
14571
+ data = await resp.json();
14572
+ } catch (err) {
14573
+ console.error(` \u2717 ${ref}: ${err.message}`);
14574
+ continue;
14575
+ }
14576
+ const pack = { rules: data.rules ?? [], docs: data.docs ?? [], manifest: data.manifest ?? {} };
14577
+ if (!verifyPack(pack, data.digest, data.signature, data.publicKey)) {
14578
+ console.error(` \u2717 ${ref}: signature/digest verification FAILED \u2014 refusing to apply.`);
14579
+ continue;
14580
+ }
14581
+ lock.push({ ref, version: data.version, digest: data.digest, signature: data.signature, signingKeyId: data.signingKeyId });
14582
+ if (!cloud) {
14583
+ const fname = cacheKey(ref, data.version);
14584
+ keptCacheFiles.add(fname);
14585
+ writeFileSync12(join19(cacheDir, fname), JSON.stringify({
14586
+ ref,
14587
+ version: data.version,
14588
+ digest: data.digest,
14589
+ rules: pack.rules,
14590
+ docs: pack.docs,
14591
+ manifest: pack.manifest
14592
+ }), "utf-8");
14593
+ }
14594
+ const ruleCount = Array.isArray(pack.rules) ? pack.rules.length : 0;
14595
+ console.log(` \u2713 ${ref}:${data.version} \u2014 verified (${ruleCount} rule${ruleCount === 1 ? "" : "s"})`);
14596
+ }
14597
+ if (!cloud && existsSync19(cacheDir)) {
14598
+ for (const f of readdirSync6(cacheDir)) {
14599
+ if (f.endsWith(".json") && !keptCacheFiles.has(f)) {
14600
+ try {
14601
+ rmSync2(join19(cacheDir, f));
14602
+ } catch {
14603
+ }
14604
+ }
14605
+ }
14606
+ }
14607
+ if (lock.length > 0) {
14608
+ writeLockfile(sf._repoRoot, lock);
14609
+ console.log(`Wrote synkro.lock (${lock.length} pack${lock.length === 1 ? "" : "s"} pinned).`);
14610
+ } else {
14611
+ console.error("No packs synced successfully.");
14612
+ process.exitCode = 1;
14613
+ }
14614
+ }
14615
+ var init_sync = __esm({
14616
+ "cli/commands/sync.ts"() {
14617
+ "use strict";
14618
+ init_install();
14619
+ init_stub();
14620
+ init_packVerify();
14621
+ init_lockfile();
14622
+ }
14623
+ });
14624
+
14423
14625
  // cli/commands/lifecycle.ts
14424
14626
  var lifecycle_exports = {};
14425
14627
  __export(lifecycle_exports, {
@@ -14532,13 +14734,13 @@ var config_exports = {};
14532
14734
  __export(config_exports, {
14533
14735
  configCommand: () => configCommand
14534
14736
  });
14535
- import { readFileSync as readFileSync16, writeFileSync as writeFileSync11, existsSync as existsSync18 } from "fs";
14536
- import { join as join18 } from "path";
14537
- import { homedir as homedir18 } from "os";
14737
+ import { readFileSync as readFileSync17, writeFileSync as writeFileSync13, existsSync as existsSync20 } from "fs";
14738
+ import { join as join20 } from "path";
14739
+ import { homedir as homedir19 } from "os";
14538
14740
  function readConfigEnv2() {
14539
- if (!existsSync18(CONFIG_PATH6)) return {};
14741
+ if (!existsSync20(CONFIG_PATH6)) return {};
14540
14742
  const out = {};
14541
- for (const line of readFileSync16(CONFIG_PATH6, "utf-8").split("\n")) {
14743
+ for (const line of readFileSync17(CONFIG_PATH6, "utf-8").split("\n")) {
14542
14744
  const t = line.trim();
14543
14745
  if (!t || t.startsWith("#")) continue;
14544
14746
  const eq = t.indexOf("=");
@@ -14547,11 +14749,11 @@ function readConfigEnv2() {
14547
14749
  return out;
14548
14750
  }
14549
14751
  function updateConfigValue(key, value) {
14550
- if (!existsSync18(CONFIG_PATH6)) {
14752
+ if (!existsSync20(CONFIG_PATH6)) {
14551
14753
  console.error("No config found. Run `synkro install` first.");
14552
14754
  process.exit(1);
14553
14755
  }
14554
- const lines = readFileSync16(CONFIG_PATH6, "utf-8").split("\n");
14756
+ const lines = readFileSync17(CONFIG_PATH6, "utf-8").split("\n");
14555
14757
  const pattern = new RegExp(`^${key}=`);
14556
14758
  let found = false;
14557
14759
  const updated = lines.map((line) => {
@@ -14562,7 +14764,7 @@ function updateConfigValue(key, value) {
14562
14764
  return line;
14563
14765
  });
14564
14766
  if (!found) updated.splice(updated.length - 1, 0, `${key}='${value}'`);
14565
- writeFileSync11(CONFIG_PATH6, updated.join("\n"), "utf-8");
14767
+ writeFileSync13(CONFIG_PATH6, updated.join("\n"), "utf-8");
14566
14768
  }
14567
14769
  async function reconcileContainer() {
14568
14770
  const cfg = readConfigEnv2();
@@ -14672,20 +14874,20 @@ var init_config = __esm({
14672
14874
  "cli/commands/config.ts"() {
14673
14875
  "use strict";
14674
14876
  init_stub();
14675
- SYNKRO_DIR7 = join18(homedir18(), ".synkro");
14676
- CONFIG_PATH6 = join18(SYNKRO_DIR7, "config.env");
14877
+ SYNKRO_DIR7 = join20(homedir19(), ".synkro");
14878
+ CONFIG_PATH6 = join20(SYNKRO_DIR7, "config.env");
14677
14879
  }
14678
14880
  });
14679
14881
 
14680
14882
  // cli/bootstrap.js
14681
- import { readFileSync as readFileSync17, existsSync as existsSync19 } from "fs";
14883
+ import { readFileSync as readFileSync18, existsSync as existsSync21 } from "fs";
14682
14884
  import { resolve as resolve3 } from "path";
14683
14885
  var envCandidates = [
14684
14886
  resolve3(process.env.HOME ?? "", ".synkro", "config.env")
14685
14887
  ];
14686
14888
  for (const envPath of envCandidates) {
14687
- if (!existsSync19(envPath)) continue;
14688
- const envContent = readFileSync17(envPath, "utf-8");
14889
+ if (!existsSync21(envPath)) continue;
14890
+ const envContent = readFileSync18(envPath, "utf-8");
14689
14891
  for (const line of envContent.split("\n")) {
14690
14892
  const trimmed = line.trim();
14691
14893
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -14700,7 +14902,7 @@ var args = process.argv.slice(2);
14700
14902
  var cmd = args[0] || "";
14701
14903
  var subArgs = args.slice(1);
14702
14904
  function printVersion() {
14703
- console.log("1.6.83");
14905
+ console.log("1.6.85");
14704
14906
  }
14705
14907
  function printHelp2() {
14706
14908
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
@@ -14779,6 +14981,11 @@ async function main() {
14779
14981
  await importCommand2();
14780
14982
  break;
14781
14983
  }
14984
+ case "sync": {
14985
+ const { syncCommand: syncCommand2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
14986
+ await syncCommand2(subArgs);
14987
+ break;
14988
+ }
14782
14989
  case "version":
14783
14990
  case "--version":
14784
14991
  case "-v": {