switchroom 0.13.49 → 0.13.51

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.
@@ -23263,6 +23263,12 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
23263
23263
  mkdirSync8(`${hostHomeForChecks}/.switchroom/agents/${a.name}/schedule.d`, { recursive: true });
23264
23264
  } catch {}
23265
23265
  lines.push(` - ${homePrefix}/.switchroom/audit/${a.name}:${homePrefix}/.switchroom/audit/${a.name}:rw`);
23266
+ if (existsSync12(`${hostHomeForChecks}/.switchroom-config`)) {
23267
+ try {
23268
+ mkdirSync8(`${hostHomeForChecks}/.switchroom-config/agents/${a.name}/personal-skills`, { recursive: true });
23269
+ } catch {}
23270
+ lines.push(` - ${homePrefix}/.switchroom-config/agents/${a.name}/personal-skills:${homePrefix}/.switchroom-config/agents/${a.name}/personal-skills:rw`);
23271
+ }
23266
23272
  if (bundledSkillsPoolDir && existsSync12(bundledSkillsPoolDir) && !bundledSkillsPoolDir.startsWith(`${hostHomeForChecks}/.switchroom/skills`)) {
23267
23273
  lines.push(` - ${bundledSkillsPoolDir}:${bundledSkillsPoolDir}:ro`);
23268
23274
  }
@@ -47249,6 +47255,20 @@ function dispatchTool(name, args) {
47249
47255
  parseMode = "json";
47250
47256
  break;
47251
47257
  }
47258
+ case "skill_clone_to_personal": {
47259
+ const a = args;
47260
+ if (!a.source || typeof a.source !== "string") {
47261
+ return errorText("skill_clone_to_personal: source is required");
47262
+ }
47263
+ const base = ["skill", "clone-to-personal", a.source];
47264
+ if (a.agent)
47265
+ base.push("--agent", a.agent);
47266
+ if (a.name)
47267
+ base.push("--name", a.name);
47268
+ cliArgs = base;
47269
+ parseMode = "json";
47270
+ break;
47271
+ }
47252
47272
  default:
47253
47273
  return errorText(`unknown tool: ${name}`);
47254
47274
  }
@@ -47488,6 +47508,26 @@ var init_server3 = __esm(() => {
47488
47508
  type: "object",
47489
47509
  properties: {}
47490
47510
  }
47511
+ },
47512
+ {
47513
+ name: "skill_clone_to_personal",
47514
+ description: "Fork a shared or bundled skill into this agent's writable workspace. " + "Use when you find a defect or gap in a skill you depend on and want " + "to fix it yourself \u2014 the upstream source is untouched, your fork is " + "yours to edit via `skill_edit_personal`. Source format: " + "`shared:<name>` or `bundled:<name>`. Same validation gates as " + "init/edit. Pre-existing personal-<name> is refused (use edit or " + "remove first). No operator approval.",
47515
+ inputSchema: {
47516
+ type: "object",
47517
+ required: ["source"],
47518
+ properties: {
47519
+ source: {
47520
+ type: "string",
47521
+ pattern: "^(shared|bundled):[a-z0-9][a-z0-9_-]{0,62}$",
47522
+ description: "Source skill: `shared:<name>` (operator pool) or `bundled:<name>` (shipped)."
47523
+ },
47524
+ name: {
47525
+ type: "string",
47526
+ pattern: "^[a-z0-9][a-z0-9_-]{0,62}$",
47527
+ description: "Optional destination slug (defaults to the source slug)."
47528
+ }
47529
+ }
47530
+ }
47491
47531
  }
47492
47532
  ];
47493
47533
  });
@@ -47913,8 +47953,8 @@ var {
47913
47953
  } = import__.default;
47914
47954
 
47915
47955
  // src/build-info.ts
47916
- var VERSION = "0.13.49";
47917
- var COMMIT_SHA = "d8226915";
47956
+ var VERSION = "0.13.51";
47957
+ var COMMIT_SHA = "9494f463";
47918
47958
 
47919
47959
  // src/cli/agent.ts
47920
47960
  init_source();
@@ -48486,6 +48526,7 @@ function alignAgentUid(name, agentDir, uid, opts = {}) {
48486
48526
  const writeOut = opts.writeOut ?? ((s) => process.stdout.write(s));
48487
48527
  const logsDir = join8(homedir4(), ".switchroom", "logs", name);
48488
48528
  const auditDir = join8(homedir4(), ".switchroom", "audit", name);
48529
+ const configMirrorDir = join8(homedir4(), ".switchroom-config", "agents", name, "personal-skills");
48489
48530
  const paths = [];
48490
48531
  if (existsSync11(agentDir))
48491
48532
  paths.push(agentDir);
@@ -48493,6 +48534,8 @@ function alignAgentUid(name, agentDir, uid, opts = {}) {
48493
48534
  paths.push(logsDir);
48494
48535
  if (existsSync11(auditDir))
48495
48536
  paths.push(auditDir);
48537
+ if (existsSync11(configMirrorDir))
48538
+ paths.push(configMirrorDir);
48496
48539
  if (paths.length === 0)
48497
48540
  return { chowned: false, paths: [] };
48498
48541
  const priors = [];
@@ -74728,6 +74771,9 @@ async function ensureHostMountSources(config) {
74728
74771
  dirs.push(join62(home2, ".switchroom", "logs", name));
74729
74772
  dirs.push(join62(home2, ".claude", "projects", name));
74730
74773
  dirs.push(join62(home2, ".switchroom", "audit", name));
74774
+ if (existsSync68(join62(home2, ".switchroom-config"))) {
74775
+ dirs.push(join62(home2, ".switchroom-config", "agents", name, "personal-skills"));
74776
+ }
74731
74777
  }
74732
74778
  for (const dir of dirs) {
74733
74779
  await mkdir(dir, { recursive: true });
@@ -77606,6 +77652,87 @@ init_source();
77606
77652
  var PERSONAL_PREFIX = "personal-";
77607
77653
  var TRASH_DIRNAME = "skills-trash";
77608
77654
  var TRASH_TTL_MS = 24 * 60 * 60 * 1000;
77655
+ var PERSONAL_SKILLS_SUBPATH = "personal-skills";
77656
+ function resolveConfigSkillsDir(agent) {
77657
+ const override = process.env.SWITCHROOM_CONFIG_DIR;
77658
+ const candidate = override ? resolve45(override) : join69(homedir39(), ".switchroom-config");
77659
+ if (!existsSync76(candidate))
77660
+ return null;
77661
+ return join69(candidate, "agents", agent, PERSONAL_SKILLS_SUBPATH);
77662
+ }
77663
+ var MIRROR_PRIOR_TTL_MS = 24 * 60 * 60 * 1000;
77664
+ function sweepMirrorPriors(configSkillsRoot) {
77665
+ try {
77666
+ if (!existsSync76(configSkillsRoot))
77667
+ return;
77668
+ const now = Date.now();
77669
+ for (const ent of readdirSync30(configSkillsRoot)) {
77670
+ const m = /^\.(?:.+)-(?:prior|trash)-(\d+)$/.exec(ent);
77671
+ if (!m)
77672
+ continue;
77673
+ const ts = Number(m[1]);
77674
+ if (!Number.isFinite(ts))
77675
+ continue;
77676
+ if (now - ts < MIRROR_PRIOR_TTL_MS)
77677
+ continue;
77678
+ try {
77679
+ rmSync17(join69(configSkillsRoot, ent), { recursive: true, force: true });
77680
+ } catch {}
77681
+ }
77682
+ } catch {}
77683
+ }
77684
+ function mirrorToConfigRepo(agent, name, liveSkillDir) {
77685
+ const configSkillsRoot = resolveConfigSkillsDir(agent);
77686
+ if (!configSkillsRoot)
77687
+ return;
77688
+ const dest = join69(configSkillsRoot, name);
77689
+ try {
77690
+ if (liveSkillDir !== null) {
77691
+ try {
77692
+ const st = lstatSync9(liveSkillDir);
77693
+ if (st.isSymbolicLink()) {
77694
+ process.stderr.write(source_default.yellow(`warning: refusing to mirror ${liveSkillDir} \u2014 source is a symlink
77695
+ `));
77696
+ return;
77697
+ }
77698
+ } catch {}
77699
+ }
77700
+ if (liveSkillDir === null) {
77701
+ sweepMirrorPriors(configSkillsRoot);
77702
+ if (existsSync76(dest)) {
77703
+ const trash = join69(configSkillsRoot, `.${name}-trash-${Date.now()}`);
77704
+ renameSync17(dest, trash);
77705
+ }
77706
+ return;
77707
+ }
77708
+ mkdirSync41(configSkillsRoot, { recursive: true, mode: 493 });
77709
+ sweepMirrorPriors(configSkillsRoot);
77710
+ const staging = mkdtempSync6(join69(configSkillsRoot, `.${name}-staging-`));
77711
+ const walk2 = (src, dst) => {
77712
+ mkdirSync41(dst, { recursive: true, mode: 493 });
77713
+ for (const ent of readdirSync30(src, { withFileTypes: true })) {
77714
+ const s = join69(src, ent.name);
77715
+ const d = join69(dst, ent.name);
77716
+ if (ent.isSymbolicLink())
77717
+ continue;
77718
+ if (ent.isDirectory())
77719
+ walk2(s, d);
77720
+ else if (ent.isFile()) {
77721
+ writeFileSync35(d, readFileSync61(s));
77722
+ }
77723
+ }
77724
+ };
77725
+ walk2(liveSkillDir, staging);
77726
+ if (existsSync76(dest)) {
77727
+ const prior = join69(configSkillsRoot, `.${name}-prior-${Date.now()}`);
77728
+ renameSync17(dest, prior);
77729
+ }
77730
+ renameSync17(staging, dest);
77731
+ } catch (err2) {
77732
+ process.stderr.write(source_default.yellow(`warning: mirror to ${dest} failed (${err2.message ?? err2}); ` + `live copy still works, but this skill is not version-controlled until next successful sync.
77733
+ `));
77734
+ }
77735
+ }
77609
77736
  function fail3(msg, exit = 2) {
77610
77737
  console.error(source_default.red(`error: ${msg}`));
77611
77738
  process.exit(exit);
@@ -77842,14 +77969,6 @@ function loadValidateWrite(agentsRoot, agent, name, files, ensureNew) {
77842
77969
  process.exit(3);
77843
77970
  }
77844
77971
  writePersonalSkill(target, files);
77845
- console.log(JSON.stringify({
77846
- ok: true,
77847
- action: ensureNew ? "init" : "edit",
77848
- agent,
77849
- name,
77850
- path: target,
77851
- files: Object.keys(files).length
77852
- }));
77853
77972
  }
77854
77973
  function loadFiles(opts) {
77855
77974
  if (opts.from === undefined) {
@@ -77873,6 +77992,16 @@ function initPersonalAction(name, opts) {
77873
77992
  const agentsRoot = resolveAgentsRoot(opts);
77874
77993
  const files = loadFiles(opts);
77875
77994
  loadValidateWrite(agentsRoot, agent, name, files, true);
77995
+ const skillDir = personalSkillDir(agentsRoot, agent, name);
77996
+ mirrorToConfigRepo(agent, name, skillDir);
77997
+ console.log(JSON.stringify({
77998
+ ok: true,
77999
+ action: "init",
78000
+ agent,
78001
+ name,
78002
+ path: skillDir,
78003
+ files: Object.keys(files).length
78004
+ }));
77876
78005
  appendAudit(agent, "skill.init_personal", { name, files: Object.keys(files).length }, 0);
77877
78006
  }
77878
78007
  function editPersonalAction(name, opts) {
@@ -77880,8 +78009,137 @@ function editPersonalAction(name, opts) {
77880
78009
  const agentsRoot = resolveAgentsRoot(opts);
77881
78010
  const files = loadFiles(opts);
77882
78011
  loadValidateWrite(agentsRoot, agent, name, files, false);
78012
+ const skillDir = personalSkillDir(agentsRoot, agent, name);
78013
+ mirrorToConfigRepo(agent, name, skillDir);
78014
+ console.log(JSON.stringify({
78015
+ ok: true,
78016
+ action: "edit",
78017
+ agent,
78018
+ name,
78019
+ path: skillDir,
78020
+ files: Object.keys(files).length
78021
+ }));
77883
78022
  appendAudit(agent, "skill.edit_personal", { name, files: Object.keys(files).length }, 0);
77884
78023
  }
78024
+ var CLONE_SOURCE_RE = /^(shared|bundled):([a-z0-9][a-z0-9_-]{0,62})$/;
78025
+ function defaultSharedRoot() {
78026
+ return join69(homedir39(), ".switchroom", "skills");
78027
+ }
78028
+ function defaultBundledRoot() {
78029
+ return join69(homedir39(), ".switchroom", "skills", "_bundled");
78030
+ }
78031
+ function resolveCloneSource(source, opts) {
78032
+ const m = CLONE_SOURCE_RE.exec(source);
78033
+ if (!m) {
78034
+ fail3(`source must be \`shared:<name>\` or \`bundled:<name>\` (got ${JSON.stringify(source)})`);
78035
+ }
78036
+ const tier = m[1];
78037
+ const slug = m[2];
78038
+ const root = tier === "bundled" ? opts.bundledRoot ?? defaultBundledRoot() : opts.sharedRoot ?? defaultSharedRoot();
78039
+ const dir = join69(root, slug);
78040
+ if (!existsSync76(dir)) {
78041
+ fail3(`clone source ${JSON.stringify(source)} not found at ${dir}; ` + `check \`switchroom skill search --tier ${tier}\``, 1);
78042
+ }
78043
+ const st = lstatSync9(dir);
78044
+ if (st.isSymbolicLink()) {
78045
+ fail3(`clone source ${JSON.stringify(source)} is a symlink at ${dir}; ` + `point clone at the canonical pool path instead`);
78046
+ }
78047
+ return { tier, slug, dir };
78048
+ }
78049
+ var CLONE_MAX_FILE_BYTES = 1024 * 1024;
78050
+ function readSourceFiles(dir) {
78051
+ const files = {};
78052
+ const skipped = [];
78053
+ const walk2 = (sub) => {
78054
+ for (const ent of readdirSync30(sub, { withFileTypes: true })) {
78055
+ const full = join69(sub, ent.name);
78056
+ if (ent.isSymbolicLink()) {
78057
+ continue;
78058
+ }
78059
+ if (ent.isDirectory()) {
78060
+ walk2(full);
78061
+ continue;
78062
+ }
78063
+ if (ent.isFile()) {
78064
+ const rel = relative3(dir, full).replace(/\\/g, "/");
78065
+ if (!validateRelPath(rel)) {
78066
+ skipped.push(rel);
78067
+ continue;
78068
+ }
78069
+ try {
78070
+ const st = lstatSync9(full);
78071
+ if (st.size > CLONE_MAX_FILE_BYTES) {
78072
+ fail3(`clone source has oversized file ${rel} (${st.size} bytes > ${CLONE_MAX_FILE_BYTES}); ` + `refuse to read`, 3);
78073
+ }
78074
+ } catch {}
78075
+ files[rel] = readFileSync61(full, "utf-8");
78076
+ }
78077
+ }
78078
+ };
78079
+ walk2(dir);
78080
+ return { files, skipped };
78081
+ }
78082
+ function rewriteSkillMdName(content, newName) {
78083
+ if (!content.startsWith(`---
78084
+ `) && !content.startsWith(`---\r
78085
+ `)) {
78086
+ return content;
78087
+ }
78088
+ const rest = content.slice(content.indexOf(`
78089
+ `) + 1);
78090
+ const endIdx = rest.indexOf(`
78091
+ ---`);
78092
+ if (endIdx < 0)
78093
+ return content;
78094
+ const fm = rest.slice(0, endIdx);
78095
+ const body = rest.slice(endIdx);
78096
+ const patched = fm.replace(/^(\s*name\s*:)[ \t]*\S.*$/m, `$1 ${newName}`);
78097
+ return `---
78098
+ ` + patched + body;
78099
+ }
78100
+ function clonePersonalAction(source, opts) {
78101
+ const agent = resolveAgent(opts);
78102
+ const agentsRoot = resolveAgentsRoot(opts);
78103
+ const src = resolveCloneSource(source, opts);
78104
+ const newName = opts.name ?? src.slug;
78105
+ if (!SKILL_SLUG_RE.test(newName)) {
78106
+ fail3(`destination name must match ${SKILL_SLUG_RE.source}: got ${JSON.stringify(newName)}`);
78107
+ }
78108
+ const { files, skipped } = readSourceFiles(src.dir);
78109
+ if (!files["SKILL.md"]) {
78110
+ fail3(`source ${JSON.stringify(source)} has no SKILL.md at ${src.dir}`);
78111
+ }
78112
+ if (newName !== src.slug) {
78113
+ files["SKILL.md"] = rewriteSkillMdName(files["SKILL.md"], newName);
78114
+ }
78115
+ if (skipped.length > 0) {
78116
+ process.stderr.write(source_default.yellow(`note: skipped ${skipped.length} non-allowlisted path${skipped.length === 1 ? "" : "s"} from source: ${skipped.join(", ")}
78117
+ `));
78118
+ }
78119
+ loadValidateWrite(agentsRoot, agent, newName, files, true);
78120
+ const skillDir = personalSkillDir(agentsRoot, agent, newName);
78121
+ mirrorToConfigRepo(agent, newName, skillDir);
78122
+ console.log(JSON.stringify({
78123
+ ok: true,
78124
+ action: "clone_to_personal",
78125
+ agent,
78126
+ source,
78127
+ source_tier: src.tier,
78128
+ source_slug: src.slug,
78129
+ name: newName,
78130
+ path: skillDir,
78131
+ files: Object.keys(files).length,
78132
+ skipped
78133
+ }));
78134
+ appendAudit(agent, "skill.clone_to_personal", {
78135
+ source,
78136
+ source_tier: src.tier,
78137
+ source_slug: src.slug,
78138
+ name: newName,
78139
+ files: Object.keys(files).length,
78140
+ skipped_count: skipped.length
78141
+ }, 0);
78142
+ }
77885
78143
  function removePersonalAction(name, opts) {
77886
78144
  const agent = resolveAgent(opts);
77887
78145
  const agentsRoot = resolveAgentsRoot(opts);
@@ -77909,6 +78167,7 @@ function removePersonalAction(name, opts) {
77909
78167
  renameSync17(target, trashTarget);
77910
78168
  const now = new Date(ts);
77911
78169
  utimesSync(trashTarget, now, now);
78170
+ mirrorToConfigRepo(agent, name, null);
77912
78171
  console.log(JSON.stringify({
77913
78172
  ok: true,
77914
78173
  action: "remove",
@@ -77974,6 +78233,9 @@ function registerSkillPersonalCommands(program3) {
77974
78233
  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) => {
77975
78234
  listPersonalAction(opts);
77976
78235
  }));
78236
+ parent.command("clone-to-personal <source>").description("Fork a shared or bundled skill into this agent's writable workspace. " + "Source format: `shared:<name>` or `bundled:<name>`. The personal copy " + "becomes mutable via edit-personal; the upstream source is untouched. " + "Use --name to give the fork a different slug. No operator approval \u2014 " + "agent's own workspace.").option("--agent <name>", "Agent name (defaults to $SWITCHROOM_AGENT_NAME)").option("--name <slug>", "Override destination slug (default: source slug)").addOption(new Option("--root <path>").hideHelp()).addOption(new Option("--shared-root <path>").hideHelp()).addOption(new Option("--bundled-root <path>").hideHelp()).action(withConfigError(async (source, opts) => {
78237
+ clonePersonalAction(source, opts);
78238
+ }));
77977
78239
  }
77978
78240
 
77979
78241
  // src/cli/skill-search.ts
@@ -77988,10 +78250,10 @@ var AGENT_NAME_RE3 = /^[a-z][a-z0-9_-]{0,62}$/;
77988
78250
  function defaultAgentsRoot() {
77989
78251
  return resolve46(homedir40(), ".switchroom/agents");
77990
78252
  }
77991
- function defaultSharedRoot() {
78253
+ function defaultSharedRoot2() {
77992
78254
  return resolve46(homedir40(), ".switchroom/skills");
77993
78255
  }
77994
- function defaultBundledRoot() {
78256
+ function defaultBundledRoot2() {
77995
78257
  return resolve46(homedir40(), ".switchroom/skills/_bundled");
77996
78258
  }
77997
78259
  function readSkillFrontmatter(skillDir) {
@@ -78077,7 +78339,7 @@ function listPersonalSkills(agent, agentsRoot = defaultAgentsRoot()) {
78077
78339
  }
78078
78340
  return out;
78079
78341
  }
78080
- function listSharedSkills(sharedRoot = defaultSharedRoot()) {
78342
+ function listSharedSkills(sharedRoot = defaultSharedRoot2()) {
78081
78343
  if (!existsSync77(sharedRoot))
78082
78344
  return [];
78083
78345
  const out = [];
@@ -78115,7 +78377,7 @@ function listSharedSkills(sharedRoot = defaultSharedRoot()) {
78115
78377
  }
78116
78378
  return out;
78117
78379
  }
78118
- function listBundledSkills(bundledRoot = defaultBundledRoot()) {
78380
+ function listBundledSkills(bundledRoot = defaultBundledRoot2()) {
78119
78381
  if (!existsSync77(bundledRoot))
78120
78382
  return [];
78121
78383
  const out = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.13.49",
3
+ "version": "0.13.51",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -159,6 +159,19 @@ fi
159
159
  if [ ! -e "$HOME/.switchroom" ] || [ -L "$HOME/.switchroom" ]; then
160
160
  ln -sfn {{{hostHomeQ}}}/.switchroom "$HOME/.switchroom" 2>/dev/null || true
161
161
  fi
162
+ # Host ~/.switchroom-config symlink (#1846). Same shape as the
163
+ # ~/.switchroom link above, for the operator's git-tracked config repo.
164
+ # The compose generator bind-mounts a per-agent slice at
165
+ # <host-home>/.switchroom-config/agents/<self>/personal-skills/ (when
166
+ # the operator has opted in). Without this symlink, the CLI's
167
+ # resolveConfigSkillsDir would call existsSync("$HOME/.switchroom-config")
168
+ # and miss the bind-mounted path entirely → mirror silently no-ops.
169
+ # The link lands ONLY when the host slice is actually mounted (the dir
170
+ # the bind-mount targets) — leaving a dangling link when the operator
171
+ # hasn't opted in is fine, existsSync resolves false through it.
172
+ if [ ! -e "$HOME/.switchroom-config" ] || [ -L "$HOME/.switchroom-config" ]; then
173
+ ln -sfn {{{hostHomeQ}}}/.switchroom-config "$HOME/.switchroom-config" 2>/dev/null || true
174
+ fi
162
175
  {{/if}}
163
176
 
164
177
  export NVM_DIR="$HOME/.nvm"
@@ -48732,10 +48732,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
48732
48732
  }
48733
48733
 
48734
48734
  // ../src/build-info.ts
48735
- var VERSION = "0.13.49";
48736
- var COMMIT_SHA = "d8226915";
48737
- var COMMIT_DATE = "2026-05-25T21:14:02Z";
48738
- var LATEST_PR = 1840;
48735
+ var VERSION = "0.13.51";
48736
+ var COMMIT_SHA = "9494f463";
48737
+ var COMMIT_DATE = "2026-05-25T23:39:25Z";
48738
+ var LATEST_PR = 1861;
48739
48739
  var COMMITS_AHEAD_OF_TAG = 0;
48740
48740
 
48741
48741
  // gateway/boot-version.ts