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.
package/dist/cli/switchroom.js
CHANGED
|
@@ -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((
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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,
|
|
43437
|
+
function resolveComponent(base, relative4, options, skipNormalization) {
|
|
43438
43438
|
const target = {};
|
|
43439
43439
|
if (!skipNormalization) {
|
|
43440
43440
|
base = parse6(serialize(base, options), options);
|
|
43441
|
-
|
|
43441
|
+
relative4 = parse6(serialize(relative4, options), options);
|
|
43442
43442
|
}
|
|
43443
43443
|
options = options || {};
|
|
43444
|
-
if (!options.tolerant &&
|
|
43445
|
-
target.scheme =
|
|
43446
|
-
target.userinfo =
|
|
43447
|
-
target.host =
|
|
43448
|
-
target.port =
|
|
43449
|
-
target.path = removeDotSegments(
|
|
43450
|
-
target.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 (
|
|
43453
|
-
target.userinfo =
|
|
43454
|
-
target.host =
|
|
43455
|
-
target.port =
|
|
43456
|
-
target.path = removeDotSegments(
|
|
43457
|
-
target.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 (!
|
|
43459
|
+
if (!relative4.path) {
|
|
43460
43460
|
target.path = base.path;
|
|
43461
|
-
if (
|
|
43462
|
-
target.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 (
|
|
43468
|
-
target.path = removeDotSegments(
|
|
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 = "/" +
|
|
43471
|
+
target.path = "/" + relative4.path;
|
|
43472
43472
|
} else if (!base.path) {
|
|
43473
|
-
target.path =
|
|
43473
|
+
target.path = relative4.path;
|
|
43474
43474
|
} else {
|
|
43475
|
-
target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) +
|
|
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 =
|
|
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 =
|
|
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:
|
|
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((
|
|
47042
|
+
return new Promise((resolve47) => {
|
|
47043
47043
|
const json = serializeMessage(message);
|
|
47044
47044
|
if (this._stdout.write(json)) {
|
|
47045
|
-
|
|
47045
|
+
resolve47();
|
|
47046
47046
|
} else {
|
|
47047
|
-
this._stdout.once("drain",
|
|
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
|
|
47065
|
-
function execCli(args) {
|
|
47066
|
-
const r =
|
|
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
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
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 =
|
|
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.
|
|
47767
|
-
var COMMIT_SHA = "
|
|
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
|
|
76912
|
-
import { homedir as
|
|
76913
|
-
import { join as
|
|
76914
|
-
import { spawnSync as
|
|
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
|
|
78360
|
+
return join71(homedir41(), ".switchroom", "hostd");
|
|
77005
78361
|
}
|
|
77006
78362
|
function hostdComposePath() {
|
|
77007
|
-
return
|
|
78363
|
+
return join71(hostdDir(), "docker-compose.yml");
|
|
77008
78364
|
}
|
|
77009
78365
|
function backupExistingCompose() {
|
|
77010
78366
|
const p = hostdComposePath();
|
|
77011
|
-
if (!
|
|
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 =
|
|
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
|
-
|
|
78400
|
+
mkdirSync42(dir, { recursive: true });
|
|
77045
78401
|
const yaml = renderHostdComposeFile({
|
|
77046
|
-
hostHome:
|
|
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
|
-
|
|
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 (!
|
|
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 (
|
|
78465
|
+
if (existsSync79(dir)) {
|
|
77110
78466
|
const entries = [];
|
|
77111
78467
|
try {
|
|
77112
|
-
for (const name of
|
|
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 =
|
|
77116
|
-
if (
|
|
77117
|
-
const st =
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
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);
|