@synkro-sh/cli 1.7.8 → 1.7.18

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
@@ -11311,6 +11311,7 @@ var init_setupGithub = __esm({
11311
11311
  var install_exports = {};
11312
11312
  __export(install_exports, {
11313
11313
  detectGitRepo: () => detectGitRepo2,
11314
+ discoverAndIngestSkills: () => discoverAndIngestSkills,
11314
11315
  getOrMintCloudToken: () => getOrMintCloudToken,
11315
11316
  installCommand: () => installCommand,
11316
11317
  parseArgs: () => parseArgs,
@@ -11326,6 +11327,7 @@ import { homedir as homedir10 } from "os";
11326
11327
  import { join as join10 } from "path";
11327
11328
  import { execSync as execSync6 } from "child_process";
11328
11329
  import { createInterface as createInterface3 } from "readline";
11330
+ import { createHash } from "crypto";
11329
11331
  function resolvePersistedHookMode() {
11330
11332
  if (process.env.SYNKRO_HOOK_MODE === "full") return "full";
11331
11333
  if (process.env.SYNKRO_HOOK_MODE === "stub") return "stub";
@@ -11643,7 +11645,7 @@ function writeConfigEnv(opts) {
11643
11645
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
11644
11646
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
11645
11647
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
11646
- `SYNKRO_VERSION=${shellQuoteSingle("1.7.8")}`
11648
+ `SYNKRO_VERSION=${shellQuoteSingle("1.7.18")}`
11647
11649
  ];
11648
11650
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
11649
11651
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -12537,6 +12539,7 @@ async function installCommand(opts = {}) {
12537
12539
  if (workersUp) {
12538
12540
  console.log(" \u2713 workers ready");
12539
12541
  await syncSkillFiles();
12542
+ await discoverAndIngestSkills();
12540
12543
  } else {
12541
12544
  console.warn(" \u26A0 workers did not register within 30s \u2014 skill sync skipped");
12542
12545
  }
@@ -12859,6 +12862,34 @@ function reconcileHarness() {
12859
12862
  if (providers.length === 0) providers.push("claude_code");
12860
12863
  return splitWorkers(total, providers);
12861
12864
  }
12865
+ async function ingestSkillTasks(tasks, mcpPort) {
12866
+ const summary = { ingested: 0, rules: 0, rejected: [] };
12867
+ for (const { source, content } of tasks) {
12868
+ try {
12869
+ const resp = await fetch(`http://127.0.0.1:${mcpPort}/api/local/skills/sync`, {
12870
+ method: "POST",
12871
+ headers: { "Content-Type": "application/json" },
12872
+ body: JSON.stringify({ source, content }),
12873
+ signal: AbortSignal.timeout(65e3)
12874
+ });
12875
+ if (!resp.ok) throw new Error(`${resp.status}`);
12876
+ const r = await resp.json();
12877
+ if (r.status === "rejected") {
12878
+ const reason = (r.reasons || []).slice(0, 2).join("; ") || r.message || "blocked by security vetting";
12879
+ summary.rejected.push({ source, reason });
12880
+ console.log(` \u26D4 skill ${source}: BLOCKED \u2014 ${reason}`);
12881
+ } else {
12882
+ const created = r.created || 0;
12883
+ summary.ingested++;
12884
+ summary.rules += created;
12885
+ console.log(created > 0 ? ` \u2713 skill ${source}: ${created} rule${created === 1 ? "" : "s"} \u2192 its own pack` : ` \u2713 skill ${source}: ${r.message || "up to date"}`);
12886
+ }
12887
+ } catch (e) {
12888
+ console.warn(` \u26A0 skill ${source}: sync failed (${e instanceof Error ? e.message : String(e)})`);
12889
+ }
12890
+ }
12891
+ return summary;
12892
+ }
12862
12893
  async function syncSkillFiles() {
12863
12894
  const sf = readFullSynkroFile();
12864
12895
  if (!sf || sf.skills.length === 0) return;
@@ -12878,20 +12909,128 @@ async function syncSkillFiles() {
12878
12909
  }).filter(Boolean);
12879
12910
  if (tasks.length === 0) return;
12880
12911
  console.log(` indexing ${tasks.length} skill file${tasks.length === 1 ? "" : "s"} \u2014 comparing against existing ruleset (this may take a few seconds)...`);
12881
- for (const { source, content } of tasks) {
12912
+ await ingestSkillTasks(tasks, mcpPort);
12913
+ }
12914
+ function discoverSkillFiles(repoRoot, excludeHashes) {
12915
+ const roots = [];
12916
+ const add = (p) => {
12882
12917
  try {
12883
- const resp = await fetch(`http://127.0.0.1:${mcpPort}/api/local/skills/sync`, {
12884
- method: "POST",
12885
- headers: { "Content-Type": "application/json" },
12886
- body: JSON.stringify({ source, content }),
12887
- signal: AbortSignal.timeout(65e3)
12888
- });
12889
- if (!resp.ok) throw new Error(`${resp.status}`);
12890
- const { created, message } = await resp.json();
12891
- console.log(created > 0 ? ` \u2713 skill ${source}: ${created} new rule${created === 1 ? "" : "s"} added to index` : ` \u2713 skill ${source}: ${message || "up to date"} \u2014 index unchanged`);
12892
- } catch (e) {
12893
- console.warn(` \u26A0 skill ${source}: sync failed (${e instanceof Error ? e.message : String(e)})`);
12918
+ if (p && existsSync11(p) && statSync(p).isDirectory()) roots.push(p);
12919
+ } catch {
12920
+ }
12921
+ };
12922
+ add(join10(homedir10(), ".claude", "skills"));
12923
+ add(join10(homedir10(), ".agents", "skills"));
12924
+ if (repoRoot) {
12925
+ add(join10(repoRoot, ".claude", "skills"));
12926
+ add(join10(repoRoot, ".agents", "skills"));
12927
+ }
12928
+ const out = [];
12929
+ const seen = /* @__PURE__ */ new Set();
12930
+ const MAX = 200;
12931
+ const consider = (file, name) => {
12932
+ if (out.length >= MAX) return;
12933
+ let content = "";
12934
+ try {
12935
+ const st = statSync(file);
12936
+ if (!st.isFile() || st.size > 2e5) return;
12937
+ content = readFileSync10(file, "utf-8");
12938
+ } catch {
12939
+ return;
12940
+ }
12941
+ if (!content.trim()) return;
12942
+ const hash = createHash("sha256").update(content).digest("hex");
12943
+ if (seen.has(hash) || excludeHashes.has(hash)) return;
12944
+ seen.add(hash);
12945
+ out.push({ source: `skill:${name}`, content, name, hash });
12946
+ };
12947
+ for (const root of roots) {
12948
+ let entries = [];
12949
+ try {
12950
+ entries = readdirSync3(root);
12951
+ } catch {
12952
+ continue;
12953
+ }
12954
+ for (const entry of entries) {
12955
+ if (entry.startsWith(".")) continue;
12956
+ const full = join10(root, entry);
12957
+ let st;
12958
+ try {
12959
+ st = statSync(full);
12960
+ } catch {
12961
+ continue;
12962
+ }
12963
+ if (st.isDirectory()) {
12964
+ const skillMd = join10(full, "SKILL.md");
12965
+ if (existsSync11(skillMd)) consider(skillMd, entry);
12966
+ } else if (/\.mdx?$/i.test(entry) && entry.toUpperCase() !== "README.MD") {
12967
+ consider(full, entry.replace(/\.mdx?$/i, ""));
12968
+ }
12969
+ }
12970
+ }
12971
+ return out;
12972
+ }
12973
+ function discoverySetHash(found) {
12974
+ return createHash("sha256").update(found.map((f) => f.hash).sort().join(",")).digest("hex");
12975
+ }
12976
+ async function promptSkillDiscovery(found) {
12977
+ if (!process.stdin.isTTY || found.length === 0) return [];
12978
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
12979
+ const ask3 = (q) => new Promise((res) => rl.question(q, (a) => res(a.trim())));
12980
+ try {
12981
+ console.log(`
12982
+ Found ${found.length} skill${found.length === 1 ? "" : "s"} in your Claude Code / skills.sh folders:`);
12983
+ found.forEach((s, i) => console.log(` ${i + 1}. ${s.name}`));
12984
+ console.log("Ingest into Synkro as enforceable rules? Each is security-vetted first \u2014 anything");
12985
+ console.log("malicious (prompt injection, guardrail-weakening, secret exfil) is blocked and reported.");
12986
+ const a = (await ask3("Ingest [all] / type numbers (e.g. 1,3) / [skip]: ")).toLowerCase();
12987
+ if (a === "skip" || a === "n" || a === "no" || a === "s") return [];
12988
+ if (a === "" || a === "all" || a === "y" || a === "yes" || a === "a") return found.map((_, i) => i);
12989
+ const picks = a.split(/[\s,]+/).map((x) => parseInt(x, 10) - 1).filter((i) => Number.isInteger(i) && i >= 0 && i < found.length);
12990
+ return [...new Set(picks)];
12991
+ } finally {
12992
+ rl.close();
12993
+ }
12994
+ }
12995
+ async function discoverAndIngestSkills() {
12996
+ try {
12997
+ const sf = readFullSynkroFile();
12998
+ const repoRoot = sf?._repoRoot || detectGitRepo2();
12999
+ const excludeHashes = /* @__PURE__ */ new Set();
13000
+ if (sf?.skills?.length) {
13001
+ for (const fp of resolveSkillPaths(sf.skills, sf._repoRoot)) {
13002
+ try {
13003
+ excludeHashes.add(createHash("sha256").update(readFileSync10(fp, "utf-8")).digest("hex"));
13004
+ } catch {
13005
+ }
13006
+ }
12894
13007
  }
13008
+ const found = discoverSkillFiles(repoRoot, excludeHashes);
13009
+ if (found.length === 0) return;
13010
+ const setHash = discoverySetHash(found);
13011
+ let prev = "";
13012
+ try {
13013
+ prev = readFileSync10(SKILLS_DISCOVERED_PATH, "utf-8").trim();
13014
+ } catch {
13015
+ }
13016
+ if (prev === setHash) return;
13017
+ const picks = await promptSkillDiscovery(found);
13018
+ try {
13019
+ writeFileSync8(SKILLS_DISCOVERED_PATH, setHash);
13020
+ } catch {
13021
+ }
13022
+ if (picks.length === 0) {
13023
+ console.log(" Skipped \u2014 you can ingest skills anytime from the Synkro dashboard or via the MCP tool.");
13024
+ return;
13025
+ }
13026
+ const mcpPort = process.env.SYNKRO_MCP_PORT || "18931";
13027
+ console.log(` Ingesting ${picks.length} skill${picks.length === 1 ? "" : "s"} (security-vetting each first)\u2026`);
13028
+ const summary = await ingestSkillTasks(picks.map((i) => ({ source: found[i].source, content: found[i].content })), mcpPort);
13029
+ const parts = [`${summary.ingested} ingested (${summary.rules} rule${summary.rules === 1 ? "" : "s"})`];
13030
+ if (summary.rejected.length) parts.push(`${summary.rejected.length} blocked`);
13031
+ console.log(` Skills: ${parts.join(", ")} \u2014 each in its own toggleable pack.`);
13032
+ } catch (e) {
13033
+ console.warn(` \u26A0 skill discovery skipped (${e instanceof Error ? e.message : String(e)})`);
12895
13034
  }
12896
13035
  }
12897
13036
  function detectGitRepo2() {
@@ -13205,7 +13344,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
13205
13344
  }
13206
13345
  return { sessions: totalSessions, messages: totalMessages };
13207
13346
  }
13208
- var SYNKRO_DIR5, HOOKS_DIR, BIN_DIR, CONFIG_PATH2, MCP_STDIO_PROXY_SRC, OFFSETS_DIR, CLOUD_JWT_PATH;
13347
+ var SYNKRO_DIR5, HOOKS_DIR, BIN_DIR, CONFIG_PATH2, MCP_STDIO_PROXY_SRC, OFFSETS_DIR, CLOUD_JWT_PATH, SKILLS_DISCOVERED_PATH;
13209
13348
  var init_install = __esm({
13210
13349
  "cli/commands/install.ts"() {
13211
13350
  "use strict";
@@ -13340,6 +13479,7 @@ rl.on('line', async (line) => {
13340
13479
  `;
13341
13480
  OFFSETS_DIR = join10(SYNKRO_DIR5, ".transcript-offsets");
13342
13481
  CLOUD_JWT_PATH = join10(SYNKRO_DIR5, ".cloud-jwt");
13482
+ SKILLS_DISCOVERED_PATH = join10(SYNKRO_DIR5, ".skills-discovered");
13343
13483
  }
13344
13484
  });
13345
13485
 
@@ -15706,7 +15846,7 @@ var args = process.argv.slice(2);
15706
15846
  var cmd = args[0] || "";
15707
15847
  var subArgs = args.slice(1);
15708
15848
  function printVersion() {
15709
- console.log("1.7.8");
15849
+ console.log("1.7.18");
15710
15850
  }
15711
15851
  function printHelp2() {
15712
15852
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents