@synkro-sh/cli 1.6.84 → 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
@@ -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
  }
@@ -10516,6 +10540,7 @@ __export(install_exports, {
10516
10540
  getOrMintCloudToken: () => getOrMintCloudToken,
10517
10541
  installCommand: () => installCommand,
10518
10542
  parseArgs: () => parseArgs,
10543
+ readFullSynkroFile: () => readFullSynkroFile,
10519
10544
  reconcileDeployLocation: () => reconcileDeployLocation,
10520
10545
  reconcileHarness: () => reconcileHarness,
10521
10546
  recycleCloudContainer: () => recycleCloudContainer,
@@ -10844,7 +10869,7 @@ function writeConfigEnv(opts) {
10844
10869
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
10845
10870
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
10846
10871
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
10847
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.84")}`
10872
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.85")}`
10848
10873
  ];
10849
10874
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
10850
10875
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -11961,6 +11986,7 @@ function readFullSynkroFile() {
11961
11986
  ruleset: parsed.ruleset || "default",
11962
11987
  skills: Array.isArray(parsed.skills) ? parsed.skills.filter((s) => typeof s === "string" && s.endsWith(".md")) : [],
11963
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)])) : {},
11964
11990
  _repoRoot: root
11965
11991
  };
11966
11992
  } catch {
@@ -14425,6 +14451,177 @@ var init_import = __esm({
14425
14451
  }
14426
14452
  });
14427
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
+
14428
14625
  // cli/commands/lifecycle.ts
14429
14626
  var lifecycle_exports = {};
14430
14627
  __export(lifecycle_exports, {
@@ -14537,13 +14734,13 @@ var config_exports = {};
14537
14734
  __export(config_exports, {
14538
14735
  configCommand: () => configCommand
14539
14736
  });
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";
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";
14543
14740
  function readConfigEnv2() {
14544
- if (!existsSync18(CONFIG_PATH6)) return {};
14741
+ if (!existsSync20(CONFIG_PATH6)) return {};
14545
14742
  const out = {};
14546
- for (const line of readFileSync16(CONFIG_PATH6, "utf-8").split("\n")) {
14743
+ for (const line of readFileSync17(CONFIG_PATH6, "utf-8").split("\n")) {
14547
14744
  const t = line.trim();
14548
14745
  if (!t || t.startsWith("#")) continue;
14549
14746
  const eq = t.indexOf("=");
@@ -14552,11 +14749,11 @@ function readConfigEnv2() {
14552
14749
  return out;
14553
14750
  }
14554
14751
  function updateConfigValue(key, value) {
14555
- if (!existsSync18(CONFIG_PATH6)) {
14752
+ if (!existsSync20(CONFIG_PATH6)) {
14556
14753
  console.error("No config found. Run `synkro install` first.");
14557
14754
  process.exit(1);
14558
14755
  }
14559
- const lines = readFileSync16(CONFIG_PATH6, "utf-8").split("\n");
14756
+ const lines = readFileSync17(CONFIG_PATH6, "utf-8").split("\n");
14560
14757
  const pattern = new RegExp(`^${key}=`);
14561
14758
  let found = false;
14562
14759
  const updated = lines.map((line) => {
@@ -14567,7 +14764,7 @@ function updateConfigValue(key, value) {
14567
14764
  return line;
14568
14765
  });
14569
14766
  if (!found) updated.splice(updated.length - 1, 0, `${key}='${value}'`);
14570
- writeFileSync11(CONFIG_PATH6, updated.join("\n"), "utf-8");
14767
+ writeFileSync13(CONFIG_PATH6, updated.join("\n"), "utf-8");
14571
14768
  }
14572
14769
  async function reconcileContainer() {
14573
14770
  const cfg = readConfigEnv2();
@@ -14677,20 +14874,20 @@ var init_config = __esm({
14677
14874
  "cli/commands/config.ts"() {
14678
14875
  "use strict";
14679
14876
  init_stub();
14680
- SYNKRO_DIR7 = join18(homedir18(), ".synkro");
14681
- CONFIG_PATH6 = join18(SYNKRO_DIR7, "config.env");
14877
+ SYNKRO_DIR7 = join20(homedir19(), ".synkro");
14878
+ CONFIG_PATH6 = join20(SYNKRO_DIR7, "config.env");
14682
14879
  }
14683
14880
  });
14684
14881
 
14685
14882
  // cli/bootstrap.js
14686
- import { readFileSync as readFileSync17, existsSync as existsSync19 } from "fs";
14883
+ import { readFileSync as readFileSync18, existsSync as existsSync21 } from "fs";
14687
14884
  import { resolve as resolve3 } from "path";
14688
14885
  var envCandidates = [
14689
14886
  resolve3(process.env.HOME ?? "", ".synkro", "config.env")
14690
14887
  ];
14691
14888
  for (const envPath of envCandidates) {
14692
- if (!existsSync19(envPath)) continue;
14693
- const envContent = readFileSync17(envPath, "utf-8");
14889
+ if (!existsSync21(envPath)) continue;
14890
+ const envContent = readFileSync18(envPath, "utf-8");
14694
14891
  for (const line of envContent.split("\n")) {
14695
14892
  const trimmed = line.trim();
14696
14893
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -14705,7 +14902,7 @@ var args = process.argv.slice(2);
14705
14902
  var cmd = args[0] || "";
14706
14903
  var subArgs = args.slice(1);
14707
14904
  function printVersion() {
14708
- console.log("1.6.84");
14905
+ console.log("1.6.85");
14709
14906
  }
14710
14907
  function printHelp2() {
14711
14908
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
@@ -14784,6 +14981,11 @@ async function main() {
14784
14981
  await importCommand2();
14785
14982
  break;
14786
14983
  }
14984
+ case "sync": {
14985
+ const { syncCommand: syncCommand2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
14986
+ await syncCommand2(subArgs);
14987
+ break;
14988
+ }
14787
14989
  case "version":
14788
14990
  case "--version":
14789
14991
  case "-v": {