switchroom 0.13.48 → 0.13.50

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.
@@ -47249,6 +47249,20 @@ function dispatchTool(name, args) {
47249
47249
  parseMode = "json";
47250
47250
  break;
47251
47251
  }
47252
+ case "skill_clone_to_personal": {
47253
+ const a = args;
47254
+ if (!a.source || typeof a.source !== "string") {
47255
+ return errorText("skill_clone_to_personal: source is required");
47256
+ }
47257
+ const base = ["skill", "clone-to-personal", a.source];
47258
+ if (a.agent)
47259
+ base.push("--agent", a.agent);
47260
+ if (a.name)
47261
+ base.push("--name", a.name);
47262
+ cliArgs = base;
47263
+ parseMode = "json";
47264
+ break;
47265
+ }
47252
47266
  default:
47253
47267
  return errorText(`unknown tool: ${name}`);
47254
47268
  }
@@ -47488,6 +47502,26 @@ var init_server3 = __esm(() => {
47488
47502
  type: "object",
47489
47503
  properties: {}
47490
47504
  }
47505
+ },
47506
+ {
47507
+ name: "skill_clone_to_personal",
47508
+ 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.",
47509
+ inputSchema: {
47510
+ type: "object",
47511
+ required: ["source"],
47512
+ properties: {
47513
+ source: {
47514
+ type: "string",
47515
+ pattern: "^(shared|bundled):[a-z0-9][a-z0-9_-]{0,62}$",
47516
+ description: "Source skill: `shared:<name>` (operator pool) or `bundled:<name>` (shipped)."
47517
+ },
47518
+ name: {
47519
+ type: "string",
47520
+ pattern: "^[a-z0-9][a-z0-9_-]{0,62}$",
47521
+ description: "Optional destination slug (defaults to the source slug)."
47522
+ }
47523
+ }
47524
+ }
47491
47525
  }
47492
47526
  ];
47493
47527
  });
@@ -47913,8 +47947,8 @@ var {
47913
47947
  } = import__.default;
47914
47948
 
47915
47949
  // src/build-info.ts
47916
- var VERSION = "0.13.48";
47917
- var COMMIT_SHA = "576a7cef";
47950
+ var VERSION = "0.13.50";
47951
+ var COMMIT_SHA = "99e127b1";
47918
47952
 
47919
47953
  // src/cli/agent.ts
47920
47954
  init_source();
@@ -49955,6 +49989,16 @@ function buildSettingsHooksBlock(p) {
49955
49989
  timeout: 5
49956
49990
  }
49957
49991
  ]
49992
+ },
49993
+ {
49994
+ matcher: "^(Read|Edit|Write|MultiEdit|NotebookEdit|Bash)$",
49995
+ hooks: [
49996
+ {
49997
+ type: "command",
49998
+ command: wrap("hook:repo-context-pretool", `node "${join8(DOCKER_HOOKS_PATH, "repo-context-pretool.mjs")}"`),
49999
+ timeout: 5
50000
+ }
50001
+ ]
49958
50002
  }
49959
50003
  ] : [];
49960
50004
  const switchroomPostToolUse = useSwitchroomPlugin ? [
@@ -77596,6 +77640,87 @@ init_source();
77596
77640
  var PERSONAL_PREFIX = "personal-";
77597
77641
  var TRASH_DIRNAME = "skills-trash";
77598
77642
  var TRASH_TTL_MS = 24 * 60 * 60 * 1000;
77643
+ var PERSONAL_SKILLS_SUBPATH = "personal-skills";
77644
+ function resolveConfigSkillsDir(agent) {
77645
+ const override = process.env.SWITCHROOM_CONFIG_DIR;
77646
+ const candidate = override ? resolve45(override) : join69(homedir39(), ".switchroom-config");
77647
+ if (!existsSync76(candidate))
77648
+ return null;
77649
+ return join69(candidate, "agents", agent, PERSONAL_SKILLS_SUBPATH);
77650
+ }
77651
+ var MIRROR_PRIOR_TTL_MS = 24 * 60 * 60 * 1000;
77652
+ function sweepMirrorPriors(configSkillsRoot) {
77653
+ try {
77654
+ if (!existsSync76(configSkillsRoot))
77655
+ return;
77656
+ const now = Date.now();
77657
+ for (const ent of readdirSync30(configSkillsRoot)) {
77658
+ const m = /^\.(?:.+)-(?:prior|trash)-(\d+)$/.exec(ent);
77659
+ if (!m)
77660
+ continue;
77661
+ const ts = Number(m[1]);
77662
+ if (!Number.isFinite(ts))
77663
+ continue;
77664
+ if (now - ts < MIRROR_PRIOR_TTL_MS)
77665
+ continue;
77666
+ try {
77667
+ rmSync17(join69(configSkillsRoot, ent), { recursive: true, force: true });
77668
+ } catch {}
77669
+ }
77670
+ } catch {}
77671
+ }
77672
+ function mirrorToConfigRepo(agent, name, liveSkillDir) {
77673
+ const configSkillsRoot = resolveConfigSkillsDir(agent);
77674
+ if (!configSkillsRoot)
77675
+ return;
77676
+ const dest = join69(configSkillsRoot, name);
77677
+ try {
77678
+ if (liveSkillDir !== null) {
77679
+ try {
77680
+ const st = lstatSync9(liveSkillDir);
77681
+ if (st.isSymbolicLink()) {
77682
+ process.stderr.write(source_default.yellow(`warning: refusing to mirror ${liveSkillDir} \u2014 source is a symlink
77683
+ `));
77684
+ return;
77685
+ }
77686
+ } catch {}
77687
+ }
77688
+ if (liveSkillDir === null) {
77689
+ sweepMirrorPriors(configSkillsRoot);
77690
+ if (existsSync76(dest)) {
77691
+ const trash = join69(configSkillsRoot, `.${name}-trash-${Date.now()}`);
77692
+ renameSync17(dest, trash);
77693
+ }
77694
+ return;
77695
+ }
77696
+ mkdirSync41(configSkillsRoot, { recursive: true, mode: 493 });
77697
+ sweepMirrorPriors(configSkillsRoot);
77698
+ const staging = mkdtempSync6(join69(configSkillsRoot, `.${name}-staging-`));
77699
+ const walk2 = (src, dst) => {
77700
+ mkdirSync41(dst, { recursive: true, mode: 493 });
77701
+ for (const ent of readdirSync30(src, { withFileTypes: true })) {
77702
+ const s = join69(src, ent.name);
77703
+ const d = join69(dst, ent.name);
77704
+ if (ent.isSymbolicLink())
77705
+ continue;
77706
+ if (ent.isDirectory())
77707
+ walk2(s, d);
77708
+ else if (ent.isFile()) {
77709
+ writeFileSync35(d, readFileSync61(s));
77710
+ }
77711
+ }
77712
+ };
77713
+ walk2(liveSkillDir, staging);
77714
+ if (existsSync76(dest)) {
77715
+ const prior = join69(configSkillsRoot, `.${name}-prior-${Date.now()}`);
77716
+ renameSync17(dest, prior);
77717
+ }
77718
+ renameSync17(staging, dest);
77719
+ } catch (err2) {
77720
+ 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.
77721
+ `));
77722
+ }
77723
+ }
77599
77724
  function fail3(msg, exit = 2) {
77600
77725
  console.error(source_default.red(`error: ${msg}`));
77601
77726
  process.exit(exit);
@@ -77832,14 +77957,6 @@ function loadValidateWrite(agentsRoot, agent, name, files, ensureNew) {
77832
77957
  process.exit(3);
77833
77958
  }
77834
77959
  writePersonalSkill(target, files);
77835
- console.log(JSON.stringify({
77836
- ok: true,
77837
- action: ensureNew ? "init" : "edit",
77838
- agent,
77839
- name,
77840
- path: target,
77841
- files: Object.keys(files).length
77842
- }));
77843
77960
  }
77844
77961
  function loadFiles(opts) {
77845
77962
  if (opts.from === undefined) {
@@ -77863,6 +77980,16 @@ function initPersonalAction(name, opts) {
77863
77980
  const agentsRoot = resolveAgentsRoot(opts);
77864
77981
  const files = loadFiles(opts);
77865
77982
  loadValidateWrite(agentsRoot, agent, name, files, true);
77983
+ const skillDir = personalSkillDir(agentsRoot, agent, name);
77984
+ mirrorToConfigRepo(agent, name, skillDir);
77985
+ console.log(JSON.stringify({
77986
+ ok: true,
77987
+ action: "init",
77988
+ agent,
77989
+ name,
77990
+ path: skillDir,
77991
+ files: Object.keys(files).length
77992
+ }));
77866
77993
  appendAudit(agent, "skill.init_personal", { name, files: Object.keys(files).length }, 0);
77867
77994
  }
77868
77995
  function editPersonalAction(name, opts) {
@@ -77870,8 +77997,126 @@ function editPersonalAction(name, opts) {
77870
77997
  const agentsRoot = resolveAgentsRoot(opts);
77871
77998
  const files = loadFiles(opts);
77872
77999
  loadValidateWrite(agentsRoot, agent, name, files, false);
78000
+ const skillDir = personalSkillDir(agentsRoot, agent, name);
78001
+ mirrorToConfigRepo(agent, name, skillDir);
78002
+ console.log(JSON.stringify({
78003
+ ok: true,
78004
+ action: "edit",
78005
+ agent,
78006
+ name,
78007
+ path: skillDir,
78008
+ files: Object.keys(files).length
78009
+ }));
77873
78010
  appendAudit(agent, "skill.edit_personal", { name, files: Object.keys(files).length }, 0);
77874
78011
  }
78012
+ var CLONE_SOURCE_RE = /^(shared|bundled):([a-z0-9][a-z0-9_-]{0,62})$/;
78013
+ function defaultSharedRoot() {
78014
+ return join69(homedir39(), ".switchroom", "skills");
78015
+ }
78016
+ function defaultBundledRoot() {
78017
+ return join69(homedir39(), ".switchroom", "skills", "_bundled");
78018
+ }
78019
+ function resolveCloneSource(source, opts) {
78020
+ const m = CLONE_SOURCE_RE.exec(source);
78021
+ if (!m) {
78022
+ fail3(`source must be \`shared:<name>\` or \`bundled:<name>\` (got ${JSON.stringify(source)})`);
78023
+ }
78024
+ const tier = m[1];
78025
+ const slug = m[2];
78026
+ const root = tier === "bundled" ? opts.bundledRoot ?? defaultBundledRoot() : opts.sharedRoot ?? defaultSharedRoot();
78027
+ const dir = join69(root, slug);
78028
+ if (!existsSync76(dir)) {
78029
+ fail3(`clone source ${JSON.stringify(source)} not found at ${dir}; ` + `check \`switchroom skill search --tier ${tier}\``, 1);
78030
+ }
78031
+ const st = lstatSync9(dir);
78032
+ if (st.isSymbolicLink()) {
78033
+ fail3(`clone source ${JSON.stringify(source)} is a symlink at ${dir}; ` + `point clone at the canonical pool path instead`);
78034
+ }
78035
+ return { tier, slug, dir };
78036
+ }
78037
+ var CLONE_MAX_FILE_BYTES = 1024 * 1024;
78038
+ function readSourceFiles(dir) {
78039
+ const files = {};
78040
+ const walk2 = (sub) => {
78041
+ for (const ent of readdirSync30(sub, { withFileTypes: true })) {
78042
+ const full = join69(sub, ent.name);
78043
+ if (ent.isSymbolicLink()) {
78044
+ continue;
78045
+ }
78046
+ if (ent.isDirectory()) {
78047
+ walk2(full);
78048
+ continue;
78049
+ }
78050
+ if (ent.isFile()) {
78051
+ const rel = relative3(dir, full).replace(/\\/g, "/");
78052
+ try {
78053
+ const st = lstatSync9(full);
78054
+ if (st.size > CLONE_MAX_FILE_BYTES) {
78055
+ fail3(`clone source has oversized file ${rel} (${st.size} bytes > ${CLONE_MAX_FILE_BYTES}); ` + `refuse to read`, 3);
78056
+ }
78057
+ } catch {}
78058
+ files[rel] = readFileSync61(full, "utf-8");
78059
+ }
78060
+ }
78061
+ };
78062
+ walk2(dir);
78063
+ return files;
78064
+ }
78065
+ function rewriteSkillMdName(content, newName) {
78066
+ if (!content.startsWith(`---
78067
+ `) && !content.startsWith(`---\r
78068
+ `)) {
78069
+ return content;
78070
+ }
78071
+ const rest = content.slice(content.indexOf(`
78072
+ `) + 1);
78073
+ const endIdx = rest.indexOf(`
78074
+ ---`);
78075
+ if (endIdx < 0)
78076
+ return content;
78077
+ const fm = rest.slice(0, endIdx);
78078
+ const body = rest.slice(endIdx);
78079
+ const patched = fm.replace(/^(\s*name\s*:)[ \t]*\S.*$/m, `$1 ${newName}`);
78080
+ return `---
78081
+ ` + patched + body;
78082
+ }
78083
+ function clonePersonalAction(source, opts) {
78084
+ const agent = resolveAgent(opts);
78085
+ const agentsRoot = resolveAgentsRoot(opts);
78086
+ const src = resolveCloneSource(source, opts);
78087
+ const newName = opts.name ?? src.slug;
78088
+ if (!SKILL_SLUG_RE.test(newName)) {
78089
+ fail3(`destination name must match ${SKILL_SLUG_RE.source}: got ${JSON.stringify(newName)}`);
78090
+ }
78091
+ const files = readSourceFiles(src.dir);
78092
+ if (!files["SKILL.md"]) {
78093
+ fail3(`source ${JSON.stringify(source)} has no SKILL.md at ${src.dir}`);
78094
+ }
78095
+ if (newName !== src.slug) {
78096
+ files["SKILL.md"] = rewriteSkillMdName(files["SKILL.md"], newName);
78097
+ }
78098
+ loadValidateWrite(agentsRoot, agent, newName, files, true);
78099
+ const skillDir = personalSkillDir(agentsRoot, agent, newName);
78100
+ mirrorToConfigRepo(agent, newName, skillDir);
78101
+ console.log(JSON.stringify({
78102
+ ok: true,
78103
+ action: "clone_to_personal",
78104
+ agent,
78105
+ source,
78106
+ source_tier: src.tier,
78107
+ source_slug: src.slug,
78108
+ name: newName,
78109
+ path: skillDir,
78110
+ files: Object.keys(files).length
78111
+ }));
78112
+ appendAudit(agent, "skill.clone_to_personal", {
78113
+ source,
78114
+ source_tier: src.tier,
78115
+ source_slug: src.slug,
78116
+ name: newName,
78117
+ files: Object.keys(files).length
78118
+ }, 0);
78119
+ }
77875
78120
  function removePersonalAction(name, opts) {
77876
78121
  const agent = resolveAgent(opts);
77877
78122
  const agentsRoot = resolveAgentsRoot(opts);
@@ -77899,6 +78144,7 @@ function removePersonalAction(name, opts) {
77899
78144
  renameSync17(target, trashTarget);
77900
78145
  const now = new Date(ts);
77901
78146
  utimesSync(trashTarget, now, now);
78147
+ mirrorToConfigRepo(agent, name, null);
77902
78148
  console.log(JSON.stringify({
77903
78149
  ok: true,
77904
78150
  action: "remove",
@@ -77964,6 +78210,9 @@ function registerSkillPersonalCommands(program3) {
77964
78210
  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) => {
77965
78211
  listPersonalAction(opts);
77966
78212
  }));
78213
+ 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) => {
78214
+ clonePersonalAction(source, opts);
78215
+ }));
77967
78216
  }
77968
78217
 
77969
78218
  // src/cli/skill-search.ts
@@ -77978,10 +78227,10 @@ var AGENT_NAME_RE3 = /^[a-z][a-z0-9_-]{0,62}$/;
77978
78227
  function defaultAgentsRoot() {
77979
78228
  return resolve46(homedir40(), ".switchroom/agents");
77980
78229
  }
77981
- function defaultSharedRoot() {
78230
+ function defaultSharedRoot2() {
77982
78231
  return resolve46(homedir40(), ".switchroom/skills");
77983
78232
  }
77984
- function defaultBundledRoot() {
78233
+ function defaultBundledRoot2() {
77985
78234
  return resolve46(homedir40(), ".switchroom/skills/_bundled");
77986
78235
  }
77987
78236
  function readSkillFrontmatter(skillDir) {
@@ -78067,7 +78316,7 @@ function listPersonalSkills(agent, agentsRoot = defaultAgentsRoot()) {
78067
78316
  }
78068
78317
  return out;
78069
78318
  }
78070
- function listSharedSkills(sharedRoot = defaultSharedRoot()) {
78319
+ function listSharedSkills(sharedRoot = defaultSharedRoot2()) {
78071
78320
  if (!existsSync77(sharedRoot))
78072
78321
  return [];
78073
78322
  const out = [];
@@ -78105,7 +78354,7 @@ function listSharedSkills(sharedRoot = defaultSharedRoot()) {
78105
78354
  }
78106
78355
  return out;
78107
78356
  }
78108
- function listBundledSkills(bundledRoot = defaultBundledRoot()) {
78357
+ function listBundledSkills(bundledRoot = defaultBundledRoot2()) {
78109
78358
  if (!existsSync77(bundledRoot))
78110
78359
  return [];
78111
78360
  const out = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.13.48",
3
+ "version": "0.13.50",
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": {
@@ -48732,10 +48732,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
48732
48732
  }
48733
48733
 
48734
48734
  // ../src/build-info.ts
48735
- var VERSION = "0.13.48";
48736
- var COMMIT_SHA = "576a7cef";
48737
- var COMMIT_DATE = "2026-05-25T20:47:09Z";
48738
- var LATEST_PR = 1838;
48735
+ var VERSION = "0.13.50";
48736
+ var COMMIT_SHA = "99e127b1";
48737
+ var COMMIT_DATE = "2026-05-25T22:55:41Z";
48738
+ var LATEST_PR = 1845;
48739
48739
  var COMMITS_AHEAD_OF_TAG = 0;
48740
48740
 
48741
48741
  // gateway/boot-version.ts