switchroom 0.13.44 → 0.13.45

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.
@@ -39600,7 +39600,7 @@ class Protocol {
39600
39600
  return;
39601
39601
  }
39602
39602
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1000;
39603
- await new Promise((resolve44) => setTimeout(resolve44, pollInterval));
39603
+ await new Promise((resolve47) => setTimeout(resolve47, pollInterval));
39604
39604
  options?.signal?.throwIfAborted();
39605
39605
  }
39606
39606
  } catch (error2) {
@@ -39612,7 +39612,7 @@ class Protocol {
39612
39612
  }
39613
39613
  request(request, resultSchema, options) {
39614
39614
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
39615
- return new Promise((resolve44, reject) => {
39615
+ return new Promise((resolve47, reject) => {
39616
39616
  const earlyReject = (error2) => {
39617
39617
  reject(error2);
39618
39618
  };
@@ -39690,7 +39690,7 @@ class Protocol {
39690
39690
  if (!parseResult.success) {
39691
39691
  reject(parseResult.error);
39692
39692
  } else {
39693
- resolve44(parseResult.data);
39693
+ resolve47(parseResult.data);
39694
39694
  }
39695
39695
  } catch (error2) {
39696
39696
  reject(error2);
@@ -39881,12 +39881,12 @@ class Protocol {
39881
39881
  interval = task.pollInterval;
39882
39882
  }
39883
39883
  } catch {}
39884
- return new Promise((resolve44, reject) => {
39884
+ return new Promise((resolve47, reject) => {
39885
39885
  if (signal.aborted) {
39886
39886
  reject(new McpError(ErrorCode2.InvalidRequest, "Request cancelled"));
39887
39887
  return;
39888
39888
  }
39889
- const timeoutId = setTimeout(resolve44, interval);
39889
+ const timeoutId = setTimeout(resolve47, interval);
39890
39890
  signal.addEventListener("abort", () => {
39891
39891
  clearTimeout(timeoutId);
39892
39892
  reject(new McpError(ErrorCode2.InvalidRequest, "Request cancelled"));
@@ -42871,7 +42871,7 @@ var require_compile = __commonJS((exports2) => {
42871
42871
  const schOrFunc = root.refs[ref];
42872
42872
  if (schOrFunc)
42873
42873
  return schOrFunc;
42874
- let _sch = resolve44.call(this, root, ref);
42874
+ let _sch = resolve47.call(this, root, ref);
42875
42875
  if (_sch === undefined) {
42876
42876
  const schema = (_a = root.localRefs) === null || _a === undefined ? undefined : _a[ref];
42877
42877
  const { schemaId } = this.opts;
@@ -42898,7 +42898,7 @@ var require_compile = __commonJS((exports2) => {
42898
42898
  function sameSchemaEnv(s1, s2) {
42899
42899
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
42900
42900
  }
42901
- function resolve44(root, ref) {
42901
+ function resolve47(root, ref) {
42902
42902
  let sch;
42903
42903
  while (typeof (sch = this.refs[ref]) == "string")
42904
42904
  ref = sch;
@@ -43428,55 +43428,55 @@ var require_fast_uri = __commonJS((exports2, module) => {
43428
43428
  }
43429
43429
  return uri;
43430
43430
  }
43431
- function resolve44(baseURI, relativeURI, options) {
43431
+ function resolve47(baseURI, relativeURI, options) {
43432
43432
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
43433
43433
  const resolved = resolveComponent(parse6(baseURI, schemelessOptions), parse6(relativeURI, schemelessOptions), schemelessOptions, true);
43434
43434
  schemelessOptions.skipEscape = true;
43435
43435
  return serialize(resolved, schemelessOptions);
43436
43436
  }
43437
- function resolveComponent(base, relative2, options, skipNormalization) {
43437
+ function resolveComponent(base, relative4, options, skipNormalization) {
43438
43438
  const target = {};
43439
43439
  if (!skipNormalization) {
43440
43440
  base = parse6(serialize(base, options), options);
43441
- relative2 = parse6(serialize(relative2, options), options);
43441
+ relative4 = parse6(serialize(relative4, options), options);
43442
43442
  }
43443
43443
  options = options || {};
43444
- if (!options.tolerant && relative2.scheme) {
43445
- target.scheme = relative2.scheme;
43446
- target.userinfo = relative2.userinfo;
43447
- target.host = relative2.host;
43448
- target.port = relative2.port;
43449
- target.path = removeDotSegments(relative2.path || "");
43450
- target.query = relative2.query;
43444
+ if (!options.tolerant && relative4.scheme) {
43445
+ target.scheme = relative4.scheme;
43446
+ target.userinfo = relative4.userinfo;
43447
+ target.host = relative4.host;
43448
+ target.port = relative4.port;
43449
+ target.path = removeDotSegments(relative4.path || "");
43450
+ target.query = relative4.query;
43451
43451
  } else {
43452
- if (relative2.userinfo !== undefined || relative2.host !== undefined || relative2.port !== undefined) {
43453
- target.userinfo = relative2.userinfo;
43454
- target.host = relative2.host;
43455
- target.port = relative2.port;
43456
- target.path = removeDotSegments(relative2.path || "");
43457
- target.query = relative2.query;
43452
+ if (relative4.userinfo !== undefined || relative4.host !== undefined || relative4.port !== undefined) {
43453
+ target.userinfo = relative4.userinfo;
43454
+ target.host = relative4.host;
43455
+ target.port = relative4.port;
43456
+ target.path = removeDotSegments(relative4.path || "");
43457
+ target.query = relative4.query;
43458
43458
  } else {
43459
- if (!relative2.path) {
43459
+ if (!relative4.path) {
43460
43460
  target.path = base.path;
43461
- if (relative2.query !== undefined) {
43462
- target.query = relative2.query;
43461
+ if (relative4.query !== undefined) {
43462
+ target.query = relative4.query;
43463
43463
  } else {
43464
43464
  target.query = base.query;
43465
43465
  }
43466
43466
  } else {
43467
- if (relative2.path[0] === "/") {
43468
- target.path = removeDotSegments(relative2.path);
43467
+ if (relative4.path[0] === "/") {
43468
+ target.path = removeDotSegments(relative4.path);
43469
43469
  } else {
43470
43470
  if ((base.userinfo !== undefined || base.host !== undefined || base.port !== undefined) && !base.path) {
43471
- target.path = "/" + relative2.path;
43471
+ target.path = "/" + relative4.path;
43472
43472
  } else if (!base.path) {
43473
- target.path = relative2.path;
43473
+ target.path = relative4.path;
43474
43474
  } else {
43475
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative2.path;
43475
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative4.path;
43476
43476
  }
43477
43477
  target.path = removeDotSegments(target.path);
43478
43478
  }
43479
- target.query = relative2.query;
43479
+ target.query = relative4.query;
43480
43480
  }
43481
43481
  target.userinfo = base.userinfo;
43482
43482
  target.host = base.host;
@@ -43484,7 +43484,7 @@ var require_fast_uri = __commonJS((exports2, module) => {
43484
43484
  }
43485
43485
  target.scheme = base.scheme;
43486
43486
  }
43487
- target.fragment = relative2.fragment;
43487
+ target.fragment = relative4.fragment;
43488
43488
  return target;
43489
43489
  }
43490
43490
  function equal(uriA, uriB, options) {
@@ -43656,7 +43656,7 @@ var require_fast_uri = __commonJS((exports2, module) => {
43656
43656
  var fastUri = {
43657
43657
  SCHEMES,
43658
43658
  normalize,
43659
- resolve: resolve44,
43659
+ resolve: resolve47,
43660
43660
  resolveComponent,
43661
43661
  equal,
43662
43662
  serialize,
@@ -47039,12 +47039,12 @@ class StdioServerTransport {
47039
47039
  this.onclose?.();
47040
47040
  }
47041
47041
  send(message) {
47042
- return new Promise((resolve44) => {
47042
+ return new Promise((resolve47) => {
47043
47043
  const json = serializeMessage(message);
47044
47044
  if (this._stdout.write(json)) {
47045
- resolve44();
47045
+ resolve47();
47046
47046
  } else {
47047
- this._stdout.once("drain", resolve44);
47047
+ this._stdout.once("drain", resolve47);
47048
47048
  }
47049
47049
  });
47050
47050
  }
@@ -47061,12 +47061,13 @@ __export(exports_server, {
47061
47061
  dispatchTool: () => dispatchTool,
47062
47062
  TOOLS: () => TOOLS
47063
47063
  });
47064
- import { spawnSync as spawnSync10 } from "node:child_process";
47065
- function execCli(args) {
47066
- const r = spawnSync10(CLI_BIN, args, {
47064
+ import { spawnSync as spawnSync12 } from "node:child_process";
47065
+ function execCli(args, stdin) {
47066
+ const r = spawnSync12(CLI_BIN, args, {
47067
47067
  encoding: "utf-8",
47068
47068
  env: process.env,
47069
- timeout: 15000
47069
+ timeout: 15000,
47070
+ ...stdin !== undefined ? { input: stdin } : {}
47070
47071
  });
47071
47072
  return {
47072
47073
  stdout: r.stdout ?? "",
@@ -47094,6 +47095,7 @@ function buildArgs(base, a) {
47094
47095
  function dispatchTool(name, args) {
47095
47096
  let cliArgs;
47096
47097
  let parseMode;
47098
+ let stdinJson;
47097
47099
  switch (name) {
47098
47100
  case "config_get":
47099
47101
  cliArgs = buildArgs(["config", "get"], args);
@@ -47182,10 +47184,64 @@ function dispatchTool(name, args) {
47182
47184
  parseMode = "json";
47183
47185
  break;
47184
47186
  }
47187
+ case "skill_init_personal":
47188
+ case "skill_edit_personal": {
47189
+ const a = args;
47190
+ if (!a.name || typeof a.name !== "string") {
47191
+ return errorText(`${name}: name is required`);
47192
+ }
47193
+ if (!a.files || typeof a.files !== "object" || Array.isArray(a.files)) {
47194
+ return errorText(`${name}: files object is required`);
47195
+ }
47196
+ const verb = name === "skill_init_personal" ? "init-personal" : "edit-personal";
47197
+ const base = ["skill", verb, a.name];
47198
+ if (a.agent)
47199
+ base.push("--agent", a.agent);
47200
+ cliArgs = base;
47201
+ parseMode = "json";
47202
+ stdinJson = JSON.stringify(a.files);
47203
+ break;
47204
+ }
47205
+ case "skill_remove_personal": {
47206
+ const a = args;
47207
+ if (!a.name) {
47208
+ return errorText("skill_remove_personal: name is required");
47209
+ }
47210
+ const base = ["skill", "remove-personal", a.name];
47211
+ if (a.agent)
47212
+ base.push("--agent", a.agent);
47213
+ cliArgs = base;
47214
+ parseMode = "json";
47215
+ break;
47216
+ }
47217
+ case "skill_list_personal": {
47218
+ const a = args;
47219
+ const base = ["skill", "list-personal"];
47220
+ if (a.agent)
47221
+ base.push("--agent", a.agent);
47222
+ cliArgs = base;
47223
+ parseMode = "json";
47224
+ break;
47225
+ }
47226
+ case "skill_search": {
47227
+ const a = args;
47228
+ const base = ["skill", "search"];
47229
+ if (a.agent)
47230
+ base.push("--agent", a.agent);
47231
+ if (a.query)
47232
+ base.push("--query", a.query);
47233
+ if (a.tier)
47234
+ base.push("--tier", a.tier);
47235
+ if (typeof a.limit === "number")
47236
+ base.push("--limit", String(a.limit));
47237
+ cliArgs = base;
47238
+ parseMode = "json";
47239
+ break;
47240
+ }
47185
47241
  default:
47186
47242
  return errorText(`unknown tool: ${name}`);
47187
47243
  }
47188
- const r = execCli(cliArgs);
47244
+ const r = execCli(cliArgs, stdinJson);
47189
47245
  if (r.status !== 0) {
47190
47246
  return errorText(`CLI exit ${r.status}: ${r.stderr.trim() || r.stdout.trim()}`);
47191
47247
  }
@@ -47324,6 +47380,28 @@ var init_server3 = __esm(() => {
47324
47380
  }
47325
47381
  }
47326
47382
  },
47383
+ {
47384
+ name: "skill_search",
47385
+ description: "Enumerate skills the calling agent can see, across three tiers: " + "`personal` (own .claude/skills/personal-* workspace), `shared` " + "(operator-curated pool at ~/.switchroom/skills/), `bundled` " + "(shipped with switchroom). Read-only \u2014 no approval, no write, no " + "fleet exposure. Returns SKILL.md frontmatter + path + size + " + "mtime per match, with stable sort (personal \u2192 shared \u2192 bundled, " + "then name). Use this BEFORE authoring a new skill \u2014 odds are it " + "exists already, possibly in bundled.",
47386
+ inputSchema: {
47387
+ type: "object",
47388
+ properties: {
47389
+ query: {
47390
+ type: "string",
47391
+ description: "Case-insensitive substring match against name, description, jtbd."
47392
+ },
47393
+ tier: {
47394
+ type: "string",
47395
+ enum: ["personal", "shared", "bundled", "any"],
47396
+ description: "Filter to one tier; default `any`."
47397
+ },
47398
+ limit: {
47399
+ type: "number",
47400
+ description: "Cap result count (default 50, max 500)."
47401
+ }
47402
+ }
47403
+ }
47404
+ },
47327
47405
  {
47328
47406
  name: "skill_remove",
47329
47407
  description: "Remove an overlay-installed skill by slug. The agent's " + "`.claude/skills/<name>` symlink is removed on next reconcile. " + "Does NOT affect operator-installed skills listed directly in " + "switchroom.yaml \u2014 those are removed by the operator only.",
@@ -47338,6 +47416,67 @@ var init_server3 = __esm(() => {
47338
47416
  }
47339
47417
  }
47340
47418
  }
47419
+ },
47420
+ {
47421
+ name: "skill_init_personal",
47422
+ description: "Create a personal skill in this agent's writable workspace " + "(`<agentDir>/.claude/skills/personal-<name>/`). Multi-file " + "payload via `files: {path: content}`. Validates against the " + "same gates an operator-driven `switchroom skill apply` would " + "\u2014 name regex, path allowlist, SKILL.md frontmatter, bundle " + "size, banned-headless-phrase content scan, bash -n / py_compile. " + "No operator approval \u2014 agent owns its workspace.",
47423
+ inputSchema: {
47424
+ type: "object",
47425
+ required: ["name", "files"],
47426
+ properties: {
47427
+ name: {
47428
+ type: "string",
47429
+ pattern: "^[a-z0-9][a-z0-9_-]{0,62}$",
47430
+ description: "Skill slug (becomes the dir name `personal-<slug>`)."
47431
+ },
47432
+ files: {
47433
+ type: "object",
47434
+ description: "Multi-file payload \u2014 keys are relative paths under the skill " + "dir (must match the SKILL.md allowlist: SKILL.md, README.md, " + "scripts/*.{sh,py}, assets/..., reference/*.md). Values are " + "the file contents (strings)."
47435
+ }
47436
+ }
47437
+ }
47438
+ },
47439
+ {
47440
+ name: "skill_edit_personal",
47441
+ description: "Overwrite an existing personal skill. Same gates as " + "skill_init_personal. Fails if the skill doesn't already exist " + "(use skill_init_personal first).",
47442
+ inputSchema: {
47443
+ type: "object",
47444
+ required: ["name", "files"],
47445
+ properties: {
47446
+ name: {
47447
+ type: "string",
47448
+ pattern: "^[a-z0-9][a-z0-9_-]{0,62}$",
47449
+ description: "Skill slug to overwrite."
47450
+ },
47451
+ files: {
47452
+ type: "object",
47453
+ description: "Multi-file payload (see skill_init_personal)."
47454
+ }
47455
+ }
47456
+ }
47457
+ },
47458
+ {
47459
+ name: "skill_remove_personal",
47460
+ description: "Soft-remove a personal skill. The dir moves to " + "`<agentDir>/.claude/skills-trash/<name>-<unix-ts>/` for 24h " + "recovery. Lazy sweep on next personal-skill op deletes " + "entries older than 24h.",
47461
+ inputSchema: {
47462
+ type: "object",
47463
+ required: ["name"],
47464
+ properties: {
47465
+ name: {
47466
+ type: "string",
47467
+ pattern: "^[a-z0-9][a-z0-9_-]{0,62}$",
47468
+ description: "Skill slug to remove."
47469
+ }
47470
+ }
47471
+ }
47472
+ },
47473
+ {
47474
+ name: "skill_list_personal",
47475
+ description: "List personal skills owned by this agent. Returns name, on-disk " + "path, file count, and total bytes per skill. Read-only.",
47476
+ inputSchema: {
47477
+ type: "object",
47478
+ properties: {}
47479
+ }
47341
47480
  }
47342
47481
  ];
47343
47482
  });
@@ -47352,7 +47491,7 @@ __export(exports_server2, {
47352
47491
  TOOLS: () => TOOLS2
47353
47492
  });
47354
47493
  import { randomBytes as randomBytes14 } from "node:crypto";
47355
- import { existsSync as existsSync75, readFileSync as readFileSync60 } from "node:fs";
47494
+ import { existsSync as existsSync78, readFileSync as readFileSync63 } from "node:fs";
47356
47495
  function selfSocketPath() {
47357
47496
  return `/run/switchroom/hostd/${SELF_AGENT}/sock`;
47358
47497
  }
@@ -47367,7 +47506,7 @@ async function dispatchTool2(name, args) {
47367
47506
  return errorText2("hostd MCP: SWITCHROOM_AGENT_NAME env var is not set \u2014 cannot " + "determine which per-agent socket to talk to.");
47368
47507
  }
47369
47508
  const sockPath = selfSocketPath();
47370
- if (!existsSync75(sockPath)) {
47509
+ if (!existsSync78(sockPath)) {
47371
47510
  return errorText2(`hostd MCP: socket not bound at ${sockPath}. The host-control ` + `daemon is either not installed (run \`switchroom hostd install\`) ` + `or this agent isn't admin-flagged in switchroom.yaml. RFC C ` + `bind-mounts the per-agent socket only when host_control.enabled ` + `is true AND the agent has admin: true.`);
47372
47511
  }
47373
47512
  let req;
@@ -47519,18 +47658,18 @@ function resolveAuditLogPath() {
47519
47658
  if (process.env.HOSTD_AUDIT_LOG_PATH)
47520
47659
  return process.env.HOSTD_AUDIT_LOG_PATH;
47521
47660
  const bindMounted = "/host-home/.switchroom/host-control-audit.log";
47522
- if (existsSync75(bindMounted))
47661
+ if (existsSync78(bindMounted))
47523
47662
  return bindMounted;
47524
47663
  return defaultAuditLogPath2();
47525
47664
  }
47526
47665
  function getLastUpdateApplyStatus() {
47527
47666
  const path8 = resolveAuditLogPath();
47528
- if (!existsSync75(path8)) {
47667
+ if (!existsSync78(path8)) {
47529
47668
  return errorText2(`get_status: audit log not found at ${path8}. No update_apply has run yet?`);
47530
47669
  }
47531
47670
  let raw;
47532
47671
  try {
47533
- raw = readFileSync60(path8, "utf-8");
47672
+ raw = readFileSync63(path8, "utf-8");
47534
47673
  } catch (err2) {
47535
47674
  return errorText2(`get_status: failed to read audit log at ${path8}: ${err2.message}`);
47536
47675
  }
@@ -47763,8 +47902,8 @@ var {
47763
47902
  } = import__.default;
47764
47903
 
47765
47904
  // src/build-info.ts
47766
- var VERSION = "0.13.44";
47767
- var COMMIT_SHA = "fa99d4de";
47905
+ var VERSION = "0.13.45";
47906
+ var COMMIT_SHA = "e6c3e655";
47768
47907
 
47769
47908
  // src/cli/agent.ts
47770
47909
  init_source();
@@ -75660,7 +75799,7 @@ function registerAgentConfigCommands(program3) {
75660
75799
  process.exit(1);
75661
75800
  }
75662
75801
  }));
75663
- const skill = program3.command("skill").description("Read-only access to an agent's skill list");
75802
+ const skill = program3.commands.find((c) => c.name() === "skill") ?? program3.command("skill").description("Read-only access to an agent's skill list");
75664
75803
  skill.command("list").description("List the agent's configured skills as JSON").option("--agent <name>", "Target agent (defaults to $SWITCHROOM_AGENT_NAME)").action(withConfigError(async (opts) => {
75665
75804
  let agent;
75666
75805
  try {
@@ -76887,6 +77026,1223 @@ function registerAgentConfigSkillWriteCommands(program3) {
76887
77026
  });
76888
77027
  }
76889
77028
 
77029
+ // src/cli/skill.ts
77030
+ import {
77031
+ closeSync as closeSync15,
77032
+ existsSync as existsSync75,
77033
+ lstatSync as lstatSync8,
77034
+ mkdirSync as mkdirSync40,
77035
+ mkdtempSync as mkdtempSync5,
77036
+ openSync as openSync15,
77037
+ readFileSync as readFileSync60,
77038
+ readdirSync as readdirSync29,
77039
+ realpathSync as realpathSync7,
77040
+ renameSync as renameSync16,
77041
+ rmSync as rmSync16,
77042
+ statSync as statSync29,
77043
+ writeFileSync as writeFileSync34
77044
+ } from "node:fs";
77045
+ import { tmpdir as tmpdir4, homedir as homedir38 } from "node:os";
77046
+ import { dirname as dirname20, join as join68, relative as relative2, resolve as resolve44 } from "node:path";
77047
+ import { spawnSync as spawnSync10 } from "node:child_process";
77048
+
77049
+ // src/cli/skill-common.ts
77050
+ var import_yaml17 = __toESM(require_dist(), 1);
77051
+ var MAX_FILE_BYTES = 256 * 1024;
77052
+ var MAX_SKILL_BYTES = 2 * 1024 * 1024;
77053
+ var MAX_FILES_PER_SKILL = 50;
77054
+ var MAX_PATH_DEPTH = 3;
77055
+ var MAX_DESCRIPTION_LEN = 1024;
77056
+ function authorExitCodeFor(code) {
77057
+ switch (code) {
77058
+ case "E_SKILL_ALREADY_EXISTS":
77059
+ return 13;
77060
+ case "E_SKILL_NOT_FOUND":
77061
+ return 14;
77062
+ case "E_SKILL_FILE_TOO_LARGE":
77063
+ case "E_SKILL_BUNDLE_TOO_LARGE":
77064
+ return 15;
77065
+ case "E_SKILL_INVALID_NAME":
77066
+ case "E_SKILL_INVALID_PATH":
77067
+ case "E_SKILL_INVALID_FRONTMATTER":
77068
+ case "E_SKILL_SCOPE_DENIED":
77069
+ return 9;
77070
+ case "E_AGENT_PIN_REQUIRED":
77071
+ return 16;
77072
+ case "E_SKILL_GLOBAL_MOUNT_UNCONFIGURED":
77073
+ return 17;
77074
+ case "E_SKILL_OPERATOR_OWNED":
77075
+ return 18;
77076
+ }
77077
+ }
77078
+ function authorErr(code, message) {
77079
+ return { ok: false, code, message, exit: authorExitCodeFor(code) };
77080
+ }
77081
+ function validateRelPath(p) {
77082
+ if (typeof p !== "string" || p.length === 0)
77083
+ return false;
77084
+ if (p.includes("\x00"))
77085
+ return false;
77086
+ if (p.startsWith("/") || p.startsWith("\\"))
77087
+ return false;
77088
+ if (/^[a-zA-Z]:[\\/]/.test(p))
77089
+ return false;
77090
+ const norm = p.replace(/\\/g, "/");
77091
+ const parts = norm.split("/");
77092
+ if (parts.length > MAX_PATH_DEPTH)
77093
+ return false;
77094
+ for (const seg of parts) {
77095
+ if (seg === "" || seg === "." || seg === "..")
77096
+ return false;
77097
+ if (seg.startsWith("."))
77098
+ return false;
77099
+ }
77100
+ if (parts.length === 1) {
77101
+ return parts[0] === "SKILL.md" || parts[0] === "README.md";
77102
+ }
77103
+ if (parts.length === 2) {
77104
+ const [dir, file] = parts;
77105
+ if (dir === "scripts")
77106
+ return /^[A-Za-z0-9_.-]+\.(sh|py)$/.test(file);
77107
+ if (dir === "assets")
77108
+ return /^[A-Za-z0-9_.-]+$/.test(file);
77109
+ if (dir === "reference")
77110
+ return /^[A-Za-z0-9_.-]+\.md$/.test(file);
77111
+ return false;
77112
+ }
77113
+ if (parts.length === 3) {
77114
+ if (parts[0] !== "assets")
77115
+ return false;
77116
+ if (!/^[A-Za-z0-9_.-]+$/.test(parts[1]))
77117
+ return false;
77118
+ if (!/^[A-Za-z0-9_.-]+$/.test(parts[2]))
77119
+ return false;
77120
+ return true;
77121
+ }
77122
+ return false;
77123
+ }
77124
+ function validateSkillMd(content, expectedName) {
77125
+ if (typeof content !== "string" || content.length === 0) {
77126
+ return authorErr("E_SKILL_INVALID_FRONTMATTER", "SKILL.md is empty");
77127
+ }
77128
+ if (!content.startsWith(`---
77129
+ `) && !content.startsWith(`---\r
77130
+ `)) {
77131
+ return authorErr("E_SKILL_INVALID_FRONTMATTER", "SKILL.md must begin with YAML frontmatter delimited by `---`");
77132
+ }
77133
+ const rest = content.slice(content.indexOf(`
77134
+ `) + 1);
77135
+ const endIdx = rest.indexOf(`
77136
+ ---`);
77137
+ if (endIdx < 0) {
77138
+ return authorErr("E_SKILL_INVALID_FRONTMATTER", "SKILL.md frontmatter has no closing `---`");
77139
+ }
77140
+ const fmText = rest.slice(0, endIdx);
77141
+ const seen = new Set;
77142
+ for (const line of fmText.split(/\r?\n/)) {
77143
+ const m = /^([A-Za-z0-9_-]+)\s*:/.exec(line);
77144
+ if (!m)
77145
+ continue;
77146
+ const key = m[1];
77147
+ if (seen.has(key)) {
77148
+ return authorErr("E_SKILL_INVALID_FRONTMATTER", `duplicate frontmatter key: ${key}`);
77149
+ }
77150
+ seen.add(key);
77151
+ }
77152
+ let parsed;
77153
+ try {
77154
+ parsed = import_yaml17.parse(fmText);
77155
+ } catch (e) {
77156
+ return authorErr("E_SKILL_INVALID_FRONTMATTER", `frontmatter is not valid YAML: ${e.message}`);
77157
+ }
77158
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
77159
+ return authorErr("E_SKILL_INVALID_FRONTMATTER", "frontmatter must be a YAML mapping");
77160
+ }
77161
+ const fm = parsed;
77162
+ if (fm.name !== expectedName) {
77163
+ return authorErr("E_SKILL_INVALID_FRONTMATTER", `frontmatter name=${JSON.stringify(fm.name)} must equal the skill slug ${JSON.stringify(expectedName)}`);
77164
+ }
77165
+ const desc = fm.description;
77166
+ if (typeof desc !== "string" || desc.length < 1 || desc.length > MAX_DESCRIPTION_LEN) {
77167
+ return authorErr("E_SKILL_INVALID_FRONTMATTER", `frontmatter description must be a string 1..${MAX_DESCRIPTION_LEN} chars`);
77168
+ }
77169
+ return { ok: true, frontmatter: fm };
77170
+ }
77171
+ var BANNED_PHRASE = "claude" + " -p";
77172
+ var CLAUDE_P_LITERAL_RE = /\bclaude\s+-p\b/m;
77173
+ function scanForClaudeP(content) {
77174
+ const normalised = content.replace(/\\\r?\n\s*/g, " ");
77175
+ return CLAUDE_P_LITERAL_RE.test(normalised);
77176
+ }
77177
+ var SH_SCRIPT_RE = /^scripts\/[A-Za-z0-9_.-]+\.sh$/;
77178
+ var PY_SCRIPT_RE = /^scripts\/[A-Za-z0-9_.-]+\.py$/;
77179
+ var SKILL_SLUG_RE = /^[a-z0-9][a-z0-9_-]{0,62}$/;
77180
+ function validateSkillBundle(name, files) {
77181
+ const errors2 = [];
77182
+ if (!SKILL_SLUG_RE.test(name)) {
77183
+ errors2.push(`skill name must match ${SKILL_SLUG_RE.source}: got ${JSON.stringify(name)}`);
77184
+ }
77185
+ for (const path8 of Object.keys(files)) {
77186
+ if (!validateRelPath(path8)) {
77187
+ errors2.push(`disallowed file path: ${JSON.stringify(path8)}`);
77188
+ }
77189
+ }
77190
+ const skillMd = files["SKILL.md"];
77191
+ if (skillMd === undefined) {
77192
+ errors2.push("payload is missing SKILL.md (required)");
77193
+ } else {
77194
+ const r = validateSkillMd(skillMd, name);
77195
+ if (!r.ok) {
77196
+ errors2.push(`SKILL.md validation failed: ${r.message}`);
77197
+ }
77198
+ }
77199
+ const fileCount = Object.keys(files).length;
77200
+ if (fileCount > MAX_FILES_PER_SKILL) {
77201
+ errors2.push(`bundle has ${fileCount} files; max is ${MAX_FILES_PER_SKILL}`);
77202
+ }
77203
+ let totalBytes = 0;
77204
+ for (const [path8, content] of Object.entries(files)) {
77205
+ const bytes = Buffer.byteLength(content, "utf-8");
77206
+ if (bytes > MAX_FILE_BYTES) {
77207
+ errors2.push(`${path8} is ${bytes} bytes; max per file is ${MAX_FILE_BYTES}`);
77208
+ }
77209
+ totalBytes += bytes;
77210
+ }
77211
+ if (totalBytes > MAX_SKILL_BYTES) {
77212
+ errors2.push(`bundle total ${totalBytes} bytes; max per skill is ${MAX_SKILL_BYTES}`);
77213
+ }
77214
+ for (const [path8, content] of Object.entries(files)) {
77215
+ if (SH_SCRIPT_RE.test(path8) || PY_SCRIPT_RE.test(path8)) {
77216
+ if (scanForClaudeP(content)) {
77217
+ errors2.push(`${path8} contains the banned ${BANNED_PHRASE} invocation \u2014 ` + `programmatic usage under Anthropic 2026-06-15 policy. ` + `Route via inject_inbound IPC into the live session instead.`);
77218
+ }
77219
+ }
77220
+ }
77221
+ return { ok: errors2.length === 0, errors: errors2 };
77222
+ }
77223
+
77224
+ // src/cli/skill.ts
77225
+ init_loader();
77226
+ init_helpers();
77227
+ init_source();
77228
+ var SKILL_NAME_RE = /^[a-z0-9][a-z0-9_-]{0,62}$/;
77229
+ var SH_SCRIPT_RE2 = /^scripts\/[A-Za-z0-9_.-]+\.sh$/;
77230
+ var PY_SCRIPT_RE2 = /^scripts\/[A-Za-z0-9_.-]+\.py$/;
77231
+ var BANNED_PHRASE2 = "claude" + " -p";
77232
+ var CLAUDE_P_LITERAL_RE2 = /\bclaude\s+-p\b/m;
77233
+ function scanForClaudeP2(content) {
77234
+ const normalised = content.replace(/\\\r?\n\s*/g, " ");
77235
+ return CLAUDE_P_LITERAL_RE2.test(normalised);
77236
+ }
77237
+ function resolveSkillsPoolDir2(override) {
77238
+ const raw = override ?? "~/.switchroom/skills";
77239
+ if (raw.startsWith("~/")) {
77240
+ return join68(homedir38(), raw.slice(2));
77241
+ }
77242
+ if (raw === "~")
77243
+ return homedir38();
77244
+ return resolve44(raw);
77245
+ }
77246
+ function readStdinSync() {
77247
+ const chunks = [];
77248
+ const buf = Buffer.alloc(64 * 1024);
77249
+ while (true) {
77250
+ let n = 0;
77251
+ try {
77252
+ const fs5 = __require("node:fs");
77253
+ n = fs5.readSync(0, buf, 0, buf.length, null);
77254
+ } catch (e) {
77255
+ const err2 = e;
77256
+ if (err2.code === "EAGAIN")
77257
+ continue;
77258
+ break;
77259
+ }
77260
+ if (n <= 0)
77261
+ break;
77262
+ chunks.push(Buffer.from(buf.subarray(0, n)));
77263
+ }
77264
+ return Buffer.concat(chunks).toString("utf-8");
77265
+ }
77266
+ function isTarballPath(p) {
77267
+ const lower = p.toLowerCase();
77268
+ return lower.endsWith(".tar") || lower.endsWith(".tar.gz") || lower.endsWith(".tgz");
77269
+ }
77270
+ function loadFromDir(dir) {
77271
+ const abs = realpathSync7(dir);
77272
+ if (!statSync29(abs).isDirectory()) {
77273
+ fail2(`--from path is not a directory: ${dir}`);
77274
+ }
77275
+ const files = {};
77276
+ const walk2 = (sub) => {
77277
+ const entries = readdirSync29(sub, { withFileTypes: true });
77278
+ for (const ent of entries) {
77279
+ const full = join68(sub, ent.name);
77280
+ const rel = relative2(abs, full);
77281
+ if (ent.isSymbolicLink()) {
77282
+ fail2(`refusing to read symlink inside --from dir: ${rel}`);
77283
+ }
77284
+ if (ent.isDirectory()) {
77285
+ walk2(full);
77286
+ continue;
77287
+ }
77288
+ if (ent.isFile()) {
77289
+ const buf = readFileSync60(full);
77290
+ files[rel.replace(/\\/g, "/")] = buf.toString("utf-8");
77291
+ }
77292
+ }
77293
+ };
77294
+ walk2(abs);
77295
+ return files;
77296
+ }
77297
+ function loadFromTarball(tarPath) {
77298
+ const isGz = tarPath.endsWith(".gz") || tarPath.endsWith(".tgz");
77299
+ const listFlags = isGz ? ["-tzf"] : ["-tf"];
77300
+ const list2 = spawnSync10("tar", [...listFlags, tarPath], {
77301
+ encoding: "utf-8",
77302
+ stdio: ["ignore", "pipe", "pipe"]
77303
+ });
77304
+ if (list2.status !== 0) {
77305
+ fail2(`tar list failed (exit ${list2.status}): ${(list2.stderr ?? "").trim()}`);
77306
+ }
77307
+ const entries = (list2.stdout ?? "").split(`
77308
+ `).map((s) => s.trim()).filter((s) => s.length > 0 && !s.endsWith("/"));
77309
+ for (const entry of entries) {
77310
+ if (!validateRelPath(entry)) {
77311
+ fail2(`tarball contains disallowed path: ${JSON.stringify(entry)} \u2014 ` + `refusing to extract before any file is written`);
77312
+ }
77313
+ }
77314
+ const staging = mkdtempSync5(join68(tmpdir4(), "skill-apply-extract-"));
77315
+ try {
77316
+ const flags = isGz ? ["-xzf"] : ["-xf"];
77317
+ const r = spawnSync10("tar", [
77318
+ ...flags,
77319
+ tarPath,
77320
+ "-C",
77321
+ staging,
77322
+ "--no-same-owner",
77323
+ "--no-same-permissions",
77324
+ "--no-absolute-names"
77325
+ ], { stdio: "pipe" });
77326
+ if (r.status !== 0) {
77327
+ const stderr = r.stderr?.toString("utf-8") ?? "(no stderr)";
77328
+ fail2(`tar extraction failed (exit ${r.status}): ${stderr.trim()}`);
77329
+ }
77330
+ return loadFromDir(staging);
77331
+ } finally {
77332
+ try {
77333
+ rmSync16(staging, { recursive: true, force: true });
77334
+ } catch {}
77335
+ }
77336
+ }
77337
+ function loadSingleFile(filePath) {
77338
+ const content = readFileSync60(filePath, "utf-8");
77339
+ return { "SKILL.md": content };
77340
+ }
77341
+ function loadFromStdin() {
77342
+ const content = readStdinSync();
77343
+ if (content.length === 0) {
77344
+ fail2("no content on stdin; either pipe SKILL.md content in or pass --from");
77345
+ }
77346
+ return { "SKILL.md": content };
77347
+ }
77348
+ function validatePayload(name, files) {
77349
+ const errors2 = [];
77350
+ if (!SKILL_NAME_RE.test(name)) {
77351
+ errors2.push(`skill name must match ${SKILL_NAME_RE.source}: got ${JSON.stringify(name)}`);
77352
+ }
77353
+ for (const path8 of Object.keys(files)) {
77354
+ if (!validateRelPath(path8)) {
77355
+ errors2.push(`disallowed file path: ${JSON.stringify(path8)}`);
77356
+ }
77357
+ }
77358
+ const skillMd = files["SKILL.md"];
77359
+ if (skillMd === undefined) {
77360
+ errors2.push("payload is missing SKILL.md (required)");
77361
+ } else {
77362
+ const r = validateSkillMd(skillMd, name);
77363
+ if (!r.ok) {
77364
+ errors2.push(`SKILL.md validation failed: ${r.message}`);
77365
+ }
77366
+ }
77367
+ const fileCount = Object.keys(files).length;
77368
+ if (fileCount > MAX_FILES_PER_SKILL) {
77369
+ errors2.push(`bundle has ${fileCount} files; max is ${MAX_FILES_PER_SKILL}`);
77370
+ }
77371
+ let totalBytes = 0;
77372
+ for (const [path8, content] of Object.entries(files)) {
77373
+ const bytes = Buffer.byteLength(content, "utf-8");
77374
+ if (bytes > MAX_FILE_BYTES) {
77375
+ errors2.push(`${path8} is ${bytes} bytes; max per file is ${MAX_FILE_BYTES}`);
77376
+ }
77377
+ totalBytes += bytes;
77378
+ }
77379
+ if (totalBytes > MAX_SKILL_BYTES) {
77380
+ errors2.push(`bundle total ${totalBytes} bytes; max per skill is ${MAX_SKILL_BYTES}`);
77381
+ }
77382
+ for (const [path8, content] of Object.entries(files)) {
77383
+ if (SH_SCRIPT_RE2.test(path8) || PY_SCRIPT_RE2.test(path8)) {
77384
+ if (scanForClaudeP2(content)) {
77385
+ errors2.push(`${path8} contains the banned ${BANNED_PHRASE2} invocation \u2014 ` + `programmatic usage under Anthropic 2026-06-15 policy. ` + `Route via inject_inbound IPC into the live session instead.`);
77386
+ }
77387
+ }
77388
+ }
77389
+ if (errors2.length === 0) {
77390
+ for (const [path8, content] of Object.entries(files)) {
77391
+ if (SH_SCRIPT_RE2.test(path8)) {
77392
+ const r = spawnSync10("bash", ["-n"], {
77393
+ input: content,
77394
+ encoding: "utf-8"
77395
+ });
77396
+ if (r.status !== 0) {
77397
+ errors2.push(`${path8} fails \`bash -n\` syntax check: ${(r.stderr ?? "").trim()}`);
77398
+ }
77399
+ } else if (PY_SCRIPT_RE2.test(path8)) {
77400
+ const tmp = mkdtempSync5(join68(tmpdir4(), "skill-apply-py-"));
77401
+ const tmpPy = join68(tmp, "check.py");
77402
+ try {
77403
+ writeFileSync34(tmpPy, content);
77404
+ const r = spawnSync10("python3", ["-m", "py_compile", tmpPy], {
77405
+ encoding: "utf-8"
77406
+ });
77407
+ if (r.status !== 0) {
77408
+ errors2.push(`${path8} fails \`python3 -m py_compile\` syntax check: ${(r.stderr ?? "").trim()}`);
77409
+ }
77410
+ } finally {
77411
+ rmSync16(tmp, { recursive: true, force: true });
77412
+ }
77413
+ }
77414
+ }
77415
+ }
77416
+ return { ok: errors2.length === 0, errors: errors2 };
77417
+ }
77418
+ function diffSummary(currentDir, files) {
77419
+ const lines = [];
77420
+ const currentFiles = {};
77421
+ if (existsSync75(currentDir)) {
77422
+ const walk2 = (sub) => {
77423
+ for (const ent of readdirSync29(sub, { withFileTypes: true })) {
77424
+ const full = join68(sub, ent.name);
77425
+ const rel = relative2(currentDir, full);
77426
+ if (ent.isDirectory()) {
77427
+ walk2(full);
77428
+ } else if (ent.isFile()) {
77429
+ currentFiles[rel.replace(/\\/g, "/")] = readFileSync60(full, "utf-8");
77430
+ }
77431
+ }
77432
+ };
77433
+ walk2(currentDir);
77434
+ }
77435
+ const allPaths = new Set([
77436
+ ...Object.keys(currentFiles),
77437
+ ...Object.keys(files)
77438
+ ]);
77439
+ for (const path8 of [...allPaths].sort()) {
77440
+ const before = currentFiles[path8];
77441
+ const after = files[path8];
77442
+ if (before === undefined && after !== undefined) {
77443
+ lines.push(source_default.green(` + ${path8} (${Buffer.byteLength(after, "utf-8")} bytes)`));
77444
+ } else if (before !== undefined && after === undefined) {
77445
+ lines.push(source_default.red(` - ${path8} (was ${Buffer.byteLength(before, "utf-8")} bytes)`));
77446
+ } else if (before !== after) {
77447
+ lines.push(source_default.yellow(` ~ ${path8} (${Buffer.byteLength(before, "utf-8")} \u2192 ${Buffer.byteLength(after, "utf-8")} bytes)`));
77448
+ }
77449
+ }
77450
+ if (lines.length === 0) {
77451
+ return " (no changes \u2014 payload matches current pool content)";
77452
+ }
77453
+ return lines.join(`
77454
+ `);
77455
+ }
77456
+ function writePayload(poolDir, name, files) {
77457
+ if (!existsSync75(poolDir)) {
77458
+ mkdirSync40(poolDir, { recursive: true, mode: 493 });
77459
+ }
77460
+ const target = join68(poolDir, name);
77461
+ let targetIsSymlink = false;
77462
+ try {
77463
+ const st = lstatSync8(target);
77464
+ if (st.isSymbolicLink()) {
77465
+ targetIsSymlink = true;
77466
+ }
77467
+ } catch {}
77468
+ if (targetIsSymlink) {
77469
+ fail2(`refusing to overwrite symlink at ${target}; investigate manually`);
77470
+ }
77471
+ const staging = mkdtempSync5(join68(poolDir, `.skill-apply-stage-${name}-`));
77472
+ let oldRename = null;
77473
+ try {
77474
+ for (const [path8, content] of Object.entries(files)) {
77475
+ const full = join68(staging, path8);
77476
+ mkdirSync40(dirname20(full), { recursive: true, mode: 493 });
77477
+ const fd = openSync15(full, "wx");
77478
+ try {
77479
+ writeFileSync34(fd, content);
77480
+ } finally {
77481
+ closeSync15(fd);
77482
+ }
77483
+ if (SH_SCRIPT_RE2.test(path8) || PY_SCRIPT_RE2.test(path8)) {
77484
+ const fs5 = __require("node:fs");
77485
+ fs5.chmodSync(full, 493);
77486
+ }
77487
+ }
77488
+ let targetExists = false;
77489
+ try {
77490
+ lstatSync8(target);
77491
+ targetExists = true;
77492
+ } catch {}
77493
+ if (targetExists) {
77494
+ oldRename = `${target}.skill-apply-old-${Date.now()}`;
77495
+ renameSync16(target, oldRename);
77496
+ }
77497
+ renameSync16(staging, target);
77498
+ if (oldRename) {
77499
+ rmSync16(oldRename, { recursive: true, force: true });
77500
+ oldRename = null;
77501
+ }
77502
+ } catch (err2) {
77503
+ try {
77504
+ rmSync16(staging, { recursive: true, force: true });
77505
+ } catch {}
77506
+ if (oldRename && existsSync75(oldRename)) {
77507
+ try {
77508
+ if (existsSync75(target)) {
77509
+ rmSync16(target, { recursive: true, force: true });
77510
+ }
77511
+ renameSync16(oldRename, target);
77512
+ } catch {}
77513
+ }
77514
+ throw err2;
77515
+ }
77516
+ }
77517
+ function fail2(msg) {
77518
+ console.error(source_default.red(`error: ${msg}`));
77519
+ process.exit(2);
77520
+ }
77521
+ function registerSkillCommand(program3) {
77522
+ const skill = program3.commands.find((c) => c.name() === "skill") ?? program3.command("skill").description("Skill pool management.");
77523
+ skill.command("apply <name>").description("Apply (write or update) a skill in the global pool. Reads SKILL.md " + "content from stdin by default, or a file/dir/tarball via --from. " + "Validates against the same gates an agent-proposed publish would " + "(name regex, path allowlist, SKILL.md frontmatter, bundle size, " + "bash -n / py_compile script checks, banned-headless-phrase scan).").option("--from <path>", "Source path. Single .md \u2192 SKILL.md content; directory \u2192 multi-file; " + ".tar / .tar.gz / .tgz \u2192 extracted multi-file. Omit to read SKILL.md " + "from stdin.").option("--dry-run", "Validate + show diff vs current pool content. No writes.", false).action(withConfigError(async (name, opts) => {
77524
+ let files;
77525
+ if (opts.from === undefined) {
77526
+ files = loadFromStdin();
77527
+ } else {
77528
+ const fromPath = resolve44(opts.from);
77529
+ if (!existsSync75(fromPath)) {
77530
+ fail2(`--from path does not exist: ${opts.from}`);
77531
+ }
77532
+ const st = statSync29(fromPath);
77533
+ if (st.isDirectory()) {
77534
+ files = loadFromDir(fromPath);
77535
+ } else if (isTarballPath(fromPath)) {
77536
+ files = loadFromTarball(fromPath);
77537
+ } else if (fromPath.endsWith(".md")) {
77538
+ files = loadSingleFile(fromPath);
77539
+ } else {
77540
+ fail2(`--from must be a directory, a .tar/.tar.gz/.tgz tarball, or a ` + `.md file. Got: ${opts.from}`);
77541
+ }
77542
+ }
77543
+ const v = validatePayload(name, files);
77544
+ if (!v.ok) {
77545
+ console.error(source_default.red("Validation failed:"));
77546
+ for (const e of v.errors) {
77547
+ console.error(source_default.red(` - ${e}`));
77548
+ }
77549
+ process.exit(3);
77550
+ }
77551
+ const config = loadConfig();
77552
+ const poolDir = resolveSkillsPoolDir2(config.switchroom?.skills_dir);
77553
+ const currentDir = join68(poolDir, name);
77554
+ console.log(source_default.bold(`Skill: ${name}`) + source_default.gray(` (${Object.keys(files).length} files, ${sumBytes(files)} bytes)`));
77555
+ console.log(source_default.bold("Diff vs current pool content:"));
77556
+ console.log(diffSummary(currentDir, files));
77557
+ if (opts.dryRun) {
77558
+ console.log(source_default.gray(`
77559
+ (--dry-run; no writes performed)`));
77560
+ return;
77561
+ }
77562
+ writePayload(poolDir, name, files);
77563
+ console.log(source_default.green(`
77564
+ \u2713 Wrote ${name} to ${currentDir}`));
77565
+ const applyBin = process.argv[1] ?? "switchroom";
77566
+ console.log(source_default.gray(`Running \`switchroom apply --non-interactive\`...`));
77567
+ const r = spawnSync10(process.argv0, [applyBin, "apply", "--non-interactive"], { stdio: "inherit" });
77568
+ if (r.status !== 0) {
77569
+ console.error(source_default.yellow(`(warning: \`switchroom apply\` exited ${r.status} \u2014 skill is ` + `in the pool but symlinks may not be refreshed. Re-run manually.)`));
77570
+ }
77571
+ }));
77572
+ }
77573
+ function sumBytes(files) {
77574
+ let n = 0;
77575
+ for (const c of Object.values(files)) {
77576
+ n += Buffer.byteLength(c, "utf-8");
77577
+ }
77578
+ return n;
77579
+ }
77580
+
77581
+ // src/cli/skill-personal.ts
77582
+ import {
77583
+ closeSync as closeSync16,
77584
+ existsSync as existsSync76,
77585
+ lstatSync as lstatSync9,
77586
+ mkdirSync as mkdirSync41,
77587
+ mkdtempSync as mkdtempSync6,
77588
+ openSync as openSync16,
77589
+ readFileSync as readFileSync61,
77590
+ readdirSync as readdirSync30,
77591
+ renameSync as renameSync17,
77592
+ rmSync as rmSync17,
77593
+ statSync as statSync30,
77594
+ utimesSync,
77595
+ writeFileSync as writeFileSync35
77596
+ } from "node:fs";
77597
+ import { dirname as dirname21, join as join69, relative as relative3, resolve as resolve45 } from "node:path";
77598
+ import { homedir as homedir39, tmpdir as tmpdir5 } from "node:os";
77599
+ import { spawnSync as spawnSync11 } from "node:child_process";
77600
+ init_helpers();
77601
+ init_source();
77602
+ var PERSONAL_PREFIX = "personal-";
77603
+ var TRASH_DIRNAME = "skills-trash";
77604
+ var TRASH_TTL_MS = 24 * 60 * 60 * 1000;
77605
+ function fail3(msg, exit = 2) {
77606
+ console.error(source_default.red(`error: ${msg}`));
77607
+ process.exit(exit);
77608
+ }
77609
+ function resolveAgent(opts) {
77610
+ const fromEnv = process.env.SWITCHROOM_AGENT_NAME;
77611
+ const agent = opts.agent ?? fromEnv;
77612
+ if (!agent) {
77613
+ fail3("agent name required: pass --agent <name>, or set SWITCHROOM_AGENT_NAME " + "in the calling environment (set by switchroom for in-container agents).");
77614
+ }
77615
+ if (!SKILL_SLUG_RE.test(agent)) {
77616
+ fail3(`agent name has invalid shape: ${JSON.stringify(agent)}`);
77617
+ }
77618
+ return agent;
77619
+ }
77620
+ function resolveAgentsRoot(opts) {
77621
+ if (opts.root)
77622
+ return resolve45(opts.root);
77623
+ return join69(homedir39(), ".switchroom", "agents");
77624
+ }
77625
+ function personalSkillDir(agentsRoot, agent, name) {
77626
+ return join69(agentsRoot, agent, ".claude", "skills", PERSONAL_PREFIX + name);
77627
+ }
77628
+ function trashDir(agentsRoot, agent) {
77629
+ return join69(agentsRoot, agent, ".claude", TRASH_DIRNAME);
77630
+ }
77631
+ function readStdinSync2() {
77632
+ const chunks = [];
77633
+ const buf = Buffer.alloc(64 * 1024);
77634
+ while (true) {
77635
+ let n = 0;
77636
+ try {
77637
+ const fs5 = __require("node:fs");
77638
+ n = fs5.readSync(0, buf, 0, buf.length, null);
77639
+ } catch (e) {
77640
+ const err2 = e;
77641
+ if (err2.code === "EAGAIN")
77642
+ continue;
77643
+ break;
77644
+ }
77645
+ if (n <= 0)
77646
+ break;
77647
+ chunks.push(Buffer.from(buf.subarray(0, n)));
77648
+ }
77649
+ return Buffer.concat(chunks).toString("utf-8");
77650
+ }
77651
+ function loadFromDir2(dir) {
77652
+ const abs = resolve45(dir);
77653
+ if (!statSync30(abs).isDirectory()) {
77654
+ fail3(`--from path is not a directory: ${dir}`);
77655
+ }
77656
+ const files = {};
77657
+ const walk2 = (sub) => {
77658
+ for (const ent of readdirSync30(sub, { withFileTypes: true })) {
77659
+ const full = join69(sub, ent.name);
77660
+ if (ent.isSymbolicLink()) {
77661
+ fail3(`refusing to read symlink in --from dir: ${relative3(abs, full)}`);
77662
+ }
77663
+ if (ent.isDirectory()) {
77664
+ walk2(full);
77665
+ continue;
77666
+ }
77667
+ if (ent.isFile()) {
77668
+ const rel = relative3(abs, full).replace(/\\/g, "/");
77669
+ files[rel] = readFileSync61(full, "utf-8");
77670
+ }
77671
+ }
77672
+ };
77673
+ walk2(abs);
77674
+ return files;
77675
+ }
77676
+ function loadFromStdin2() {
77677
+ const raw = readStdinSync2();
77678
+ if (raw.length === 0) {
77679
+ fail3("no content on stdin; pipe a SKILL.md or JSON file-map");
77680
+ }
77681
+ const trimmed = raw.trimStart();
77682
+ if (trimmed.startsWith("{")) {
77683
+ let parsed;
77684
+ try {
77685
+ parsed = JSON.parse(raw);
77686
+ } catch (err2) {
77687
+ fail3(`stdin starts with '{' but is not valid JSON: ${err2.message}`);
77688
+ }
77689
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
77690
+ fail3("stdin JSON must be an object of {path: content, ...}");
77691
+ }
77692
+ const files = {};
77693
+ for (const [k, v] of Object.entries(parsed)) {
77694
+ if (typeof v !== "string") {
77695
+ fail3(`stdin JSON: value for ${JSON.stringify(k)} must be a string`);
77696
+ }
77697
+ files[k] = v;
77698
+ }
77699
+ return files;
77700
+ }
77701
+ return { "SKILL.md": raw };
77702
+ }
77703
+ function behavioralValidate(files) {
77704
+ const errors2 = [];
77705
+ for (const [path8, content] of Object.entries(files)) {
77706
+ if (SH_SCRIPT_RE.test(path8)) {
77707
+ const r = spawnSync11("bash", ["-n"], { input: content, encoding: "utf-8" });
77708
+ if (r.status !== 0) {
77709
+ errors2.push(`${path8} fails \`bash -n\`: ${(r.stderr ?? "").trim()}`);
77710
+ }
77711
+ } else if (PY_SCRIPT_RE.test(path8)) {
77712
+ const tmp = mkdtempSync6(join69(tmpdir5(), "skill-personal-py-"));
77713
+ const tmpPy = join69(tmp, "check.py");
77714
+ try {
77715
+ writeFileSync35(tmpPy, content);
77716
+ const r = spawnSync11("python3", ["-m", "py_compile", tmpPy], {
77717
+ encoding: "utf-8"
77718
+ });
77719
+ if (r.status !== 0) {
77720
+ errors2.push(`${path8} fails \`python3 -m py_compile\`: ${(r.stderr ?? "").trim()}`);
77721
+ }
77722
+ } finally {
77723
+ rmSync17(tmp, { recursive: true, force: true });
77724
+ }
77725
+ }
77726
+ }
77727
+ return errors2;
77728
+ }
77729
+ function sweepTrash(agentsRoot, agent) {
77730
+ const trash = trashDir(agentsRoot, agent);
77731
+ if (!existsSync76(trash))
77732
+ return;
77733
+ const now = Date.now();
77734
+ for (const ent of readdirSync30(trash, { withFileTypes: true })) {
77735
+ if (!ent.isDirectory())
77736
+ continue;
77737
+ const entPath = join69(trash, ent.name);
77738
+ try {
77739
+ const st = statSync30(entPath);
77740
+ if (now - st.mtimeMs > TRASH_TTL_MS) {
77741
+ rmSync17(entPath, { recursive: true, force: true });
77742
+ }
77743
+ } catch {}
77744
+ }
77745
+ }
77746
+ function writePersonalSkill(targetDir, files) {
77747
+ let targetIsSymlink = false;
77748
+ try {
77749
+ const st = lstatSync9(targetDir);
77750
+ if (st.isSymbolicLink()) {
77751
+ targetIsSymlink = true;
77752
+ }
77753
+ } catch {}
77754
+ if (targetIsSymlink) {
77755
+ fail3(`refusing to overwrite symlink at ${targetDir}; investigate manually`);
77756
+ }
77757
+ mkdirSync41(dirname21(targetDir), { recursive: true, mode: 493 });
77758
+ const staging = mkdtempSync6(join69(dirname21(targetDir), `.skill-personal-stage-`));
77759
+ let oldRename = null;
77760
+ try {
77761
+ for (const [path8, content] of Object.entries(files)) {
77762
+ const full = join69(staging, path8);
77763
+ mkdirSync41(dirname21(full), { recursive: true, mode: 493 });
77764
+ const fd = openSync16(full, "wx");
77765
+ try {
77766
+ writeFileSync35(fd, content);
77767
+ } finally {
77768
+ closeSync16(fd);
77769
+ }
77770
+ if (SH_SCRIPT_RE.test(path8) || PY_SCRIPT_RE.test(path8)) {
77771
+ const fs5 = __require("node:fs");
77772
+ fs5.chmodSync(full, 493);
77773
+ }
77774
+ }
77775
+ let targetExists = false;
77776
+ try {
77777
+ lstatSync9(targetDir);
77778
+ targetExists = true;
77779
+ } catch {}
77780
+ if (targetExists) {
77781
+ oldRename = `${targetDir}.personal-old-${Date.now()}`;
77782
+ renameSync17(targetDir, oldRename);
77783
+ }
77784
+ renameSync17(staging, targetDir);
77785
+ if (oldRename) {
77786
+ rmSync17(oldRename, { recursive: true, force: true });
77787
+ oldRename = null;
77788
+ }
77789
+ } catch (err2) {
77790
+ try {
77791
+ rmSync17(staging, { recursive: true, force: true });
77792
+ } catch {}
77793
+ if (oldRename && existsSync76(oldRename)) {
77794
+ try {
77795
+ if (existsSync76(targetDir)) {
77796
+ rmSync17(targetDir, { recursive: true, force: true });
77797
+ }
77798
+ renameSync17(oldRename, targetDir);
77799
+ } catch {}
77800
+ }
77801
+ throw err2;
77802
+ }
77803
+ }
77804
+ function loadValidateWrite(agentsRoot, agent, name, files, ensureNew) {
77805
+ sweepTrash(agentsRoot, agent);
77806
+ if (!SKILL_SLUG_RE.test(name)) {
77807
+ fail3(`skill name must match ${SKILL_SLUG_RE.source}: got ${JSON.stringify(name)}`);
77808
+ }
77809
+ const target = personalSkillDir(agentsRoot, agent, name);
77810
+ const exists = (() => {
77811
+ try {
77812
+ lstatSync9(target);
77813
+ return true;
77814
+ } catch {
77815
+ return false;
77816
+ }
77817
+ })();
77818
+ if (ensureNew && exists) {
77819
+ fail3(`personal skill ${JSON.stringify(name)} already exists for agent ${JSON.stringify(agent)}. ` + `Use \`skill edit-personal\` to overwrite, or \`skill remove-personal\` first.`, 9);
77820
+ }
77821
+ if (!ensureNew && !exists) {
77822
+ fail3(`personal skill ${JSON.stringify(name)} does not exist for agent ${JSON.stringify(agent)}. ` + `Use \`skill init-personal\` to create it.`, 9);
77823
+ }
77824
+ const v = validateSkillBundle(name, files);
77825
+ if (!v.ok) {
77826
+ console.error(source_default.red("Validation failed:"));
77827
+ for (const e of v.errors) {
77828
+ console.error(source_default.red(` - ${e}`));
77829
+ }
77830
+ process.exit(3);
77831
+ }
77832
+ const behavioral = behavioralValidate(files);
77833
+ if (behavioral.length > 0) {
77834
+ console.error(source_default.red("Behavioural validation failed:"));
77835
+ for (const e of behavioral) {
77836
+ console.error(source_default.red(` - ${e}`));
77837
+ }
77838
+ process.exit(3);
77839
+ }
77840
+ writePersonalSkill(target, files);
77841
+ console.log(JSON.stringify({
77842
+ ok: true,
77843
+ action: ensureNew ? "init" : "edit",
77844
+ agent,
77845
+ name,
77846
+ path: target,
77847
+ files: Object.keys(files).length
77848
+ }));
77849
+ }
77850
+ function loadFiles(opts) {
77851
+ if (opts.from === undefined) {
77852
+ return loadFromStdin2();
77853
+ }
77854
+ const p = resolve45(opts.from);
77855
+ if (!existsSync76(p)) {
77856
+ fail3(`--from path does not exist: ${opts.from}`);
77857
+ }
77858
+ const st = statSync30(p);
77859
+ if (st.isDirectory()) {
77860
+ return loadFromDir2(p);
77861
+ }
77862
+ if (p.endsWith(".md")) {
77863
+ return { "SKILL.md": readFileSync61(p, "utf-8") };
77864
+ }
77865
+ fail3(`--from must be a directory or a .md file. Got: ${opts.from}`);
77866
+ }
77867
+ function initPersonalAction(name, opts) {
77868
+ const agent = resolveAgent(opts);
77869
+ const agentsRoot = resolveAgentsRoot(opts);
77870
+ const files = loadFiles(opts);
77871
+ loadValidateWrite(agentsRoot, agent, name, files, true);
77872
+ }
77873
+ function editPersonalAction(name, opts) {
77874
+ const agent = resolveAgent(opts);
77875
+ const agentsRoot = resolveAgentsRoot(opts);
77876
+ const files = loadFiles(opts);
77877
+ loadValidateWrite(agentsRoot, agent, name, files, false);
77878
+ }
77879
+ function removePersonalAction(name, opts) {
77880
+ const agent = resolveAgent(opts);
77881
+ const agentsRoot = resolveAgentsRoot(opts);
77882
+ sweepTrash(agentsRoot, agent);
77883
+ if (!SKILL_SLUG_RE.test(name)) {
77884
+ fail3(`skill name must match ${SKILL_SLUG_RE.source}: got ${JSON.stringify(name)}`);
77885
+ }
77886
+ const target = personalSkillDir(agentsRoot, agent, name);
77887
+ try {
77888
+ const st = lstatSync9(target);
77889
+ if (st.isSymbolicLink()) {
77890
+ fail3(`refusing to remove symlink at ${target}; investigate manually`);
77891
+ }
77892
+ } catch (err2) {
77893
+ const e = err2;
77894
+ if (e.code === "ENOENT") {
77895
+ fail3(`personal skill ${JSON.stringify(name)} does not exist for agent ${JSON.stringify(agent)}`, 1);
77896
+ }
77897
+ throw err2;
77898
+ }
77899
+ const trashRoot = trashDir(agentsRoot, agent);
77900
+ mkdirSync41(trashRoot, { recursive: true, mode: 493 });
77901
+ const ts = Date.now();
77902
+ const trashTarget = join69(trashRoot, `${name}-${ts}`);
77903
+ renameSync17(target, trashTarget);
77904
+ const now = new Date(ts);
77905
+ utimesSync(trashTarget, now, now);
77906
+ console.log(JSON.stringify({
77907
+ ok: true,
77908
+ action: "remove",
77909
+ agent,
77910
+ name,
77911
+ trash_path: trashTarget,
77912
+ recoverable_until: new Date(ts + TRASH_TTL_MS).toISOString()
77913
+ }));
77914
+ }
77915
+ function listPersonalAction(opts) {
77916
+ const agent = resolveAgent(opts);
77917
+ const agentsRoot = resolveAgentsRoot(opts);
77918
+ sweepTrash(agentsRoot, agent);
77919
+ const skillsDir = join69(agentsRoot, agent, ".claude", "skills");
77920
+ const personal = [];
77921
+ if (existsSync76(skillsDir)) {
77922
+ for (const ent of readdirSync30(skillsDir, { withFileTypes: true })) {
77923
+ if (!ent.isDirectory())
77924
+ continue;
77925
+ if (!ent.name.startsWith(PERSONAL_PREFIX))
77926
+ continue;
77927
+ const skillName = ent.name.slice(PERSONAL_PREFIX.length);
77928
+ const skillPath = join69(skillsDir, ent.name);
77929
+ let fileCount = 0;
77930
+ let totalBytes = 0;
77931
+ const walk2 = (sub) => {
77932
+ for (const e of readdirSync30(sub, { withFileTypes: true })) {
77933
+ if (e.isFile()) {
77934
+ fileCount += 1;
77935
+ try {
77936
+ totalBytes += statSync30(join69(sub, e.name)).size;
77937
+ } catch {}
77938
+ } else if (e.isDirectory()) {
77939
+ walk2(join69(sub, e.name));
77940
+ }
77941
+ }
77942
+ };
77943
+ try {
77944
+ walk2(skillPath);
77945
+ } catch {}
77946
+ personal.push({
77947
+ name: skillName,
77948
+ path: skillPath,
77949
+ files: fileCount,
77950
+ size_bytes: totalBytes
77951
+ });
77952
+ }
77953
+ }
77954
+ console.log(JSON.stringify({ ok: true, agent, personal }, null, 2));
77955
+ }
77956
+ function registerSkillPersonalCommands(program3) {
77957
+ const parent = program3.commands.find((c) => c.name() === "skill") ?? program3.command("skill").description("Skill pool management.");
77958
+ parent.command("init-personal <name>").description("Create a personal skill in this agent's writable workspace " + "(<agentDir>/.claude/skills/personal-<name>/). Reads SKILL.md from " + "stdin, or multi-file JSON {path: content} from stdin, or a single " + "SKILL.md file via --from, or a directory via --from. No operator " + "approval \u2014 agent's own workspace.").option("--agent <name>", "Agent name (defaults to $SWITCHROOM_AGENT_NAME)").option("--from <path>", "Source path: .md file or directory").option("--root <path>", "Test-only override for agents-root dir").action(withConfigError(async (name, opts) => {
77959
+ initPersonalAction(name, opts);
77960
+ }));
77961
+ parent.command("edit-personal <name>").description("Overwrite an existing personal skill. Same input modes as " + "init-personal. Fails if the skill doesn't already exist.").option("--agent <name>", "Agent name (defaults to $SWITCHROOM_AGENT_NAME)").option("--from <path>", "Source path: .md file or directory").option("--root <path>", "Test-only override for agents-root dir").action(withConfigError(async (name, opts) => {
77962
+ editPersonalAction(name, opts);
77963
+ }));
77964
+ parent.command("remove-personal <name>").description("Soft-remove a personal skill. Moves the dir to " + "<agentDir>/.claude/skills-trash/<name>-<unix-ts>/ for 24h " + "recoverability. Lazy sweep on next op deletes entries older " + "than 24h.").option("--agent <name>", "Agent name (defaults to $SWITCHROOM_AGENT_NAME)").option("--root <path>", "Test-only override for agents-root dir").action(withConfigError(async (name, opts) => {
77965
+ removePersonalAction(name, opts);
77966
+ }));
77967
+ parent.command("list-personal").description("List personal skills owned by this agent. JSON output by default.").option("--agent <name>", "Agent name (defaults to $SWITCHROOM_AGENT_NAME)").option("--root <path>", "Test-only override for agents-root dir").action(withConfigError(async (opts) => {
77968
+ listPersonalAction(opts);
77969
+ }));
77970
+ }
77971
+
77972
+ // src/cli/skill-search.ts
77973
+ init_helpers();
77974
+ var import_yaml18 = __toESM(require_dist(), 1);
77975
+ import { existsSync as existsSync77, readdirSync as readdirSync31, readFileSync as readFileSync62, statSync as statSync31 } from "node:fs";
77976
+ import { homedir as homedir40 } from "node:os";
77977
+ import { join as join70, resolve as resolve46 } from "node:path";
77978
+ var PERSONAL_PREFIX2 = "personal-";
77979
+ var BUNDLED_SUBDIR = "_bundled";
77980
+ var AGENT_NAME_RE3 = /^[a-z][a-z0-9_-]{0,62}$/;
77981
+ function defaultAgentsRoot() {
77982
+ return resolve46(homedir40(), ".switchroom/agents");
77983
+ }
77984
+ function defaultSharedRoot() {
77985
+ return resolve46(homedir40(), ".switchroom/skills");
77986
+ }
77987
+ function defaultBundledRoot() {
77988
+ return resolve46(homedir40(), ".switchroom/skills/_bundled");
77989
+ }
77990
+ function readSkillFrontmatter(skillDir) {
77991
+ const mdPath = join70(skillDir, "SKILL.md");
77992
+ if (!existsSync77(mdPath))
77993
+ return null;
77994
+ let content;
77995
+ try {
77996
+ content = readFileSync62(mdPath, "utf-8");
77997
+ } catch {
77998
+ return null;
77999
+ }
78000
+ if (!content.startsWith(`---
78001
+ `) && !content.startsWith(`---\r
78002
+ `)) {
78003
+ return { error: "no leading `---` frontmatter delimiter" };
78004
+ }
78005
+ const rest = content.slice(content.indexOf(`
78006
+ `) + 1);
78007
+ const endIdx = rest.indexOf(`
78008
+ ---`);
78009
+ if (endIdx < 0)
78010
+ return { error: "no closing `---` delimiter" };
78011
+ const fmText = rest.slice(0, endIdx);
78012
+ let parsed;
78013
+ try {
78014
+ parsed = import_yaml18.parse(fmText);
78015
+ } catch (e) {
78016
+ return { error: `yaml parse: ${e.message}` };
78017
+ }
78018
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
78019
+ return { error: "frontmatter is not a mapping" };
78020
+ }
78021
+ return { fm: parsed };
78022
+ }
78023
+ function statSkillMd(skillDir) {
78024
+ const mdPath = join70(skillDir, "SKILL.md");
78025
+ try {
78026
+ const st = statSync31(mdPath);
78027
+ return { size: st.size, mtime: st.mtime.toISOString() };
78028
+ } catch {
78029
+ return null;
78030
+ }
78031
+ }
78032
+ function listPersonalSkills(agent, agentsRoot = defaultAgentsRoot()) {
78033
+ if (!AGENT_NAME_RE3.test(agent))
78034
+ return [];
78035
+ const skillsDir = join70(agentsRoot, agent, ".claude/skills");
78036
+ if (!existsSync77(skillsDir))
78037
+ return [];
78038
+ const out = [];
78039
+ let entries;
78040
+ try {
78041
+ entries = readdirSync31(skillsDir);
78042
+ } catch {
78043
+ return [];
78044
+ }
78045
+ for (const ent of entries) {
78046
+ if (!ent.startsWith(PERSONAL_PREFIX2))
78047
+ continue;
78048
+ const dirPath = join70(skillsDir, ent);
78049
+ try {
78050
+ if (!statSync31(dirPath).isDirectory())
78051
+ continue;
78052
+ } catch {
78053
+ continue;
78054
+ }
78055
+ const name = ent.slice(PERSONAL_PREFIX2.length);
78056
+ const fmr = readSkillFrontmatter(dirPath);
78057
+ const stat3 = statSkillMd(dirPath);
78058
+ if (!stat3)
78059
+ continue;
78060
+ out.push({
78061
+ name,
78062
+ tier: "personal",
78063
+ path: dirPath,
78064
+ size: stat3.size,
78065
+ mtime: stat3.mtime,
78066
+ frontmatter: fmr?.fm ?? {},
78067
+ error: fmr?.error,
78068
+ agent
78069
+ });
78070
+ }
78071
+ return out;
78072
+ }
78073
+ function listSharedSkills(sharedRoot = defaultSharedRoot()) {
78074
+ if (!existsSync77(sharedRoot))
78075
+ return [];
78076
+ const out = [];
78077
+ let entries;
78078
+ try {
78079
+ entries = readdirSync31(sharedRoot);
78080
+ } catch {
78081
+ return [];
78082
+ }
78083
+ for (const ent of entries) {
78084
+ if (ent === BUNDLED_SUBDIR)
78085
+ continue;
78086
+ if (ent.startsWith("."))
78087
+ continue;
78088
+ const dirPath = join70(sharedRoot, ent);
78089
+ try {
78090
+ if (!statSync31(dirPath).isDirectory())
78091
+ continue;
78092
+ } catch {
78093
+ continue;
78094
+ }
78095
+ const fmr = readSkillFrontmatter(dirPath);
78096
+ const stat3 = statSkillMd(dirPath);
78097
+ if (!stat3)
78098
+ continue;
78099
+ out.push({
78100
+ name: ent,
78101
+ tier: "shared",
78102
+ path: dirPath,
78103
+ size: stat3.size,
78104
+ mtime: stat3.mtime,
78105
+ frontmatter: fmr?.fm ?? {},
78106
+ error: fmr?.error
78107
+ });
78108
+ }
78109
+ return out;
78110
+ }
78111
+ function listBundledSkills(bundledRoot = defaultBundledRoot()) {
78112
+ if (!existsSync77(bundledRoot))
78113
+ return [];
78114
+ const out = [];
78115
+ let entries;
78116
+ try {
78117
+ entries = readdirSync31(bundledRoot);
78118
+ } catch {
78119
+ return [];
78120
+ }
78121
+ for (const ent of entries) {
78122
+ if (ent.startsWith("."))
78123
+ continue;
78124
+ const dirPath = join70(bundledRoot, ent);
78125
+ try {
78126
+ if (!statSync31(dirPath).isDirectory())
78127
+ continue;
78128
+ } catch {
78129
+ continue;
78130
+ }
78131
+ const fmr = readSkillFrontmatter(dirPath);
78132
+ const stat3 = statSkillMd(dirPath);
78133
+ if (!stat3)
78134
+ continue;
78135
+ out.push({
78136
+ name: ent,
78137
+ tier: "bundled",
78138
+ path: dirPath,
78139
+ size: stat3.size,
78140
+ mtime: stat3.mtime,
78141
+ frontmatter: fmr?.fm ?? {},
78142
+ error: fmr?.error
78143
+ });
78144
+ }
78145
+ return out;
78146
+ }
78147
+ function matchesQuery(entry, q) {
78148
+ const needle = q.toLowerCase();
78149
+ if (entry.name.toLowerCase().includes(needle))
78150
+ return true;
78151
+ const desc = entry.frontmatter["description"];
78152
+ if (typeof desc === "string" && desc.toLowerCase().includes(needle)) {
78153
+ return true;
78154
+ }
78155
+ const jtbd = entry.frontmatter["jtbd"];
78156
+ if (typeof jtbd === "string" && jtbd.toLowerCase().includes(needle)) {
78157
+ return true;
78158
+ }
78159
+ return false;
78160
+ }
78161
+ function searchSkills(opts) {
78162
+ const tier = opts.tier ?? "any";
78163
+ const wanted = tier === "any" ? ["personal", "shared", "bundled"] : [tier];
78164
+ let results = [];
78165
+ if (wanted.includes("personal")) {
78166
+ if (opts.agent) {
78167
+ results.push(...listPersonalSkills(opts.agent, opts.agentsRoot));
78168
+ }
78169
+ }
78170
+ if (wanted.includes("shared")) {
78171
+ results.push(...listSharedSkills(opts.sharedRoot));
78172
+ }
78173
+ if (wanted.includes("bundled")) {
78174
+ results.push(...listBundledSkills(opts.bundledRoot));
78175
+ }
78176
+ if (opts.query && opts.query.length > 0) {
78177
+ results = results.filter((e) => matchesQuery(e, opts.query));
78178
+ }
78179
+ const tierOrder = {
78180
+ personal: 0,
78181
+ shared: 1,
78182
+ bundled: 2
78183
+ };
78184
+ results.sort((a, b) => {
78185
+ const t = tierOrder[a.tier] - tierOrder[b.tier];
78186
+ if (t !== 0)
78187
+ return t;
78188
+ return a.name.localeCompare(b.name);
78189
+ });
78190
+ const limit = Math.max(1, Math.min(opts.limit ?? 50, 500));
78191
+ return results.slice(0, limit);
78192
+ }
78193
+ function searchAction(opts) {
78194
+ let tier = "any";
78195
+ if (opts.tier) {
78196
+ if (opts.tier !== "personal" && opts.tier !== "shared" && opts.tier !== "bundled" && opts.tier !== "any") {
78197
+ process.stderr.write(`error: --tier must be one of personal|shared|bundled|any (got ${opts.tier})
78198
+ `);
78199
+ process.exit(2);
78200
+ }
78201
+ tier = opts.tier;
78202
+ }
78203
+ let limit;
78204
+ if (opts.limit !== undefined) {
78205
+ const n = Number(opts.limit);
78206
+ if (!Number.isFinite(n) || n < 1) {
78207
+ process.stderr.write(`error: --limit must be a positive integer
78208
+ `);
78209
+ process.exit(2);
78210
+ }
78211
+ limit = Math.floor(n);
78212
+ }
78213
+ const results = searchSkills({
78214
+ agent: opts.agent,
78215
+ query: opts.query,
78216
+ tier,
78217
+ limit,
78218
+ agentsRoot: opts.root,
78219
+ sharedRoot: opts.sharedRoot,
78220
+ bundledRoot: opts.bundledRoot
78221
+ });
78222
+ if (opts.json !== false) {
78223
+ console.log(JSON.stringify(results, null, 2));
78224
+ return;
78225
+ }
78226
+ if (results.length === 0) {
78227
+ console.log("(no skills matched)");
78228
+ return;
78229
+ }
78230
+ for (const r of results) {
78231
+ const desc = typeof r.frontmatter["description"] === "string" ? r.frontmatter["description"].slice(0, 80) : r.error ? `<broken: ${r.error}>` : "";
78232
+ const where = r.agent ? `${r.tier}/${r.agent}` : r.tier;
78233
+ console.log(`${r.name.padEnd(40)} ${where.padEnd(16)} ${desc}`);
78234
+ }
78235
+ }
78236
+ function registerSkillSearchCommand(program3) {
78237
+ let skill = program3.commands.find((c) => c.name() === "skill");
78238
+ if (!skill) {
78239
+ skill = program3.command("skill").description("Manage switchroom skills.");
78240
+ }
78241
+ skill.command("search").description("Enumerate skills across personal (per-agent), shared (operator pool), " + "and bundled (shipped) tiers. Read-only. Returns JSON by default " + "(stable shape for MCP callers); use --no-json for a human table.").option("-a, --agent <name>", "Caller agent (required for personal tier).").option("-q, --query <text>", "Case-insensitive substring match against name, description, jtbd.").option("-t, --tier <tier>", "Filter to one tier: personal | shared | bundled | any (default any).").option("-l, --limit <n>", "Cap result count (default 50, max 500).").option("--no-json", "Render as human table instead of JSON.").addOption(new Option("--root <path>").hideHelp()).addOption(new Option("--shared-root <path>").hideHelp()).addOption(new Option("--bundled-root <path>").hideHelp()).action(withConfigError(async (opts) => {
78242
+ searchAction(opts);
78243
+ }));
78244
+ }
78245
+
76890
78246
  // src/cli/mcp-agent-config.ts
76891
78247
  function registerAgentConfigMcpCommand(program3) {
76892
78248
  const mcp = program3.commands.find((c) => c.name() === "mcp") ?? program3.command("mcp").description("MCP server entry points");
@@ -76908,10 +78264,10 @@ function registerHostdMcpCommand(program3) {
76908
78264
  // src/cli/hostd.ts
76909
78265
  init_source();
76910
78266
  init_helpers();
76911
- import { existsSync as existsSync76, mkdirSync as mkdirSync40, readdirSync as readdirSync29, readFileSync as readFileSync61, writeFileSync as writeFileSync34, statSync as statSync29, copyFileSync as copyFileSync12 } from "node:fs";
76912
- import { homedir as homedir38 } from "node:os";
76913
- import { join as join68 } from "node:path";
76914
- import { spawnSync as spawnSync11 } from "node:child_process";
78267
+ import { existsSync as existsSync79, mkdirSync as mkdirSync42, readdirSync as readdirSync32, readFileSync as readFileSync64, writeFileSync as writeFileSync36, statSync as statSync32, copyFileSync as copyFileSync12 } from "node:fs";
78268
+ import { homedir as homedir41 } from "node:os";
78269
+ import { join as join71 } from "node:path";
78270
+ import { spawnSync as spawnSync13 } from "node:child_process";
76915
78271
  init_audit_reader();
76916
78272
  var DEFAULT_IMAGE_TAG = "latest";
76917
78273
  var HOSTD_COMPOSE_PROJECT = "switchroom-hostd";
@@ -77001,14 +78357,14 @@ networks:
77001
78357
  `;
77002
78358
  }
77003
78359
  function hostdDir() {
77004
- return join68(homedir38(), ".switchroom", "hostd");
78360
+ return join71(homedir41(), ".switchroom", "hostd");
77005
78361
  }
77006
78362
  function hostdComposePath() {
77007
- return join68(hostdDir(), "docker-compose.yml");
78363
+ return join71(hostdDir(), "docker-compose.yml");
77008
78364
  }
77009
78365
  function backupExistingCompose() {
77010
78366
  const p = hostdComposePath();
77011
- if (!existsSync76(p))
78367
+ if (!existsSync79(p))
77012
78368
  return null;
77013
78369
  const ts = new Date().toISOString().replace(/[:.]/g, "-");
77014
78370
  const bak = `${p}.bak-${ts}`;
@@ -77016,7 +78372,7 @@ function backupExistingCompose() {
77016
78372
  return bak;
77017
78373
  }
77018
78374
  function runDocker(args) {
77019
- const r = spawnSync11("docker", args, { encoding: "utf8" });
78375
+ const r = spawnSync13("docker", args, { encoding: "utf8" });
77020
78376
  return {
77021
78377
  ok: r.status === 0,
77022
78378
  stdout: r.stdout ?? "",
@@ -77041,9 +78397,9 @@ async function doInstall(opts, program3) {
77041
78397
  }
77042
78398
  const dir = hostdDir();
77043
78399
  const composePath = hostdComposePath();
77044
- mkdirSync40(dir, { recursive: true });
78400
+ mkdirSync42(dir, { recursive: true });
77045
78401
  const yaml = renderHostdComposeFile({
77046
- hostHome: homedir38(),
78402
+ hostHome: homedir41(),
77047
78403
  imageTag: opts.tag ?? DEFAULT_IMAGE_TAG,
77048
78404
  operatorUid: resolveOperatorUid()
77049
78405
  });
@@ -77056,7 +78412,7 @@ async function doInstall(opts, program3) {
77056
78412
  const bak = backupExistingCompose();
77057
78413
  if (bak)
77058
78414
  console.log(source_default.dim(` Backed up existing compose to ${bak}`));
77059
- writeFileSync34(composePath, yaml, "utf8");
78415
+ writeFileSync36(composePath, yaml, "utf8");
77060
78416
  console.log(source_default.green(` \u2713 Wrote ${composePath}`));
77061
78417
  console.log(source_default.dim(` admin agents: ${adminAgents.length === 0 ? "(none)" : adminAgents.join(", ")}`));
77062
78418
  console.log(source_default.dim(` Pulling ghcr.io/switchroom/switchroom-hostd:${opts.tag ?? DEFAULT_IMAGE_TAG}\u2026`));
@@ -77085,7 +78441,7 @@ function doStatus() {
77085
78441
  const composeYml = hostdComposePath();
77086
78442
  console.log(source_default.bold("switchroom-hostd"));
77087
78443
  console.log("");
77088
- if (!existsSync76(composeYml)) {
78444
+ if (!existsSync79(composeYml)) {
77089
78445
  console.log(source_default.yellow(" compose: not installed"));
77090
78446
  console.log(source_default.dim(" run `switchroom hostd install` to set up."));
77091
78447
  return;
@@ -77106,15 +78462,15 @@ function doStatus() {
77106
78462
  } else {
77107
78463
  console.log(source_default.green(` container: ${ps.stdout.trim()}`));
77108
78464
  }
77109
- if (existsSync76(dir)) {
78465
+ if (existsSync79(dir)) {
77110
78466
  const entries = [];
77111
78467
  try {
77112
- for (const name of readdirSync29(dir)) {
78468
+ for (const name of readdirSync32(dir)) {
77113
78469
  if (name === "docker-compose.yml" || name.startsWith("docker-compose.yml."))
77114
78470
  continue;
77115
- const sockPath = join68(dir, name, "sock");
77116
- if (existsSync76(sockPath)) {
77117
- const st = statSync29(sockPath);
78471
+ const sockPath = join71(dir, name, "sock");
78472
+ if (existsSync79(sockPath)) {
78473
+ const st = statSync32(sockPath);
77118
78474
  if ((st.mode & 61440) === 49152) {
77119
78475
  entries.push(`${name} \u2192 ${sockPath}`);
77120
78476
  }
@@ -77132,7 +78488,7 @@ function doStatus() {
77132
78488
  }
77133
78489
  function doUninstall() {
77134
78490
  const composeYml = hostdComposePath();
77135
- if (!existsSync76(composeYml)) {
78491
+ if (!existsSync79(composeYml)) {
77136
78492
  console.log(source_default.yellow(" No hostd install detected (no compose file at this path)."));
77137
78493
  return;
77138
78494
  }
@@ -77156,12 +78512,12 @@ function registerHostdCommand(program3) {
77156
78512
  hostd.command("uninstall").description("Stop the hostd container. Leaves the compose file in place for re-install.").action(() => doUninstall());
77157
78513
  hostd.command("audit").description("Tail and filter the hostd audit log (privileged-verb call history)").option("--tail <n>", "Number of matching entries to show (default: 50)", "50").option("--agent <name>", "Filter to a specific caller agent").option("--op <verb>", "Filter to a specific hostd verb (e.g. update_apply, agent_restart)").option("--error", "Show only failed (error/denied) entries").option("--verbose", "Show the captured stderr / error tail under each failed row").option("--path <file>", "Override audit log path (for debugging)").action((opts) => {
77158
78514
  const logPath = opts.path ?? defaultAuditLogPath2();
77159
- if (!existsSync76(logPath)) {
78515
+ if (!existsSync79(logPath)) {
77160
78516
  console.error(source_default.yellow(`Audit log not found at ${logPath}.`) + source_default.gray(`
77161
78517
  The log is created when hostd handles its first privileged-verb request.`));
77162
78518
  return;
77163
78519
  }
77164
- const raw = readFileSync61(logPath, "utf-8");
78520
+ const raw = readFileSync64(logPath, "utf-8");
77165
78521
  const limit = Math.max(1, parseInt(opts.tail ?? "50", 10) || 50);
77166
78522
  const filters = {
77167
78523
  agent: opts.agent,
@@ -77238,6 +78594,9 @@ registerStatusAskCommand(program3);
77238
78594
  registerAgentConfigCommands(program3);
77239
78595
  registerAgentConfigWriteCommands(program3);
77240
78596
  registerAgentConfigSkillWriteCommands(program3);
78597
+ registerSkillCommand(program3);
78598
+ registerSkillPersonalCommands(program3);
78599
+ registerSkillSearchCommand(program3);
77241
78600
  registerAgentConfigMcpCommand(program3);
77242
78601
  registerHostdMcpCommand(program3);
77243
78602
  registerHostdCommand(program3);