switchroom 0.13.49 → 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.49";
47917
- var COMMIT_SHA = "d8226915";
47950
+ var VERSION = "0.13.50";
47951
+ var COMMIT_SHA = "99e127b1";
47918
47952
 
47919
47953
  // src/cli/agent.ts
47920
47954
  init_source();
@@ -77606,6 +77640,87 @@ init_source();
77606
77640
  var PERSONAL_PREFIX = "personal-";
77607
77641
  var TRASH_DIRNAME = "skills-trash";
77608
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
+ }
77609
77724
  function fail3(msg, exit = 2) {
77610
77725
  console.error(source_default.red(`error: ${msg}`));
77611
77726
  process.exit(exit);
@@ -77842,14 +77957,6 @@ function loadValidateWrite(agentsRoot, agent, name, files, ensureNew) {
77842
77957
  process.exit(3);
77843
77958
  }
77844
77959
  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
77960
  }
77854
77961
  function loadFiles(opts) {
77855
77962
  if (opts.from === undefined) {
@@ -77873,6 +77980,16 @@ function initPersonalAction(name, opts) {
77873
77980
  const agentsRoot = resolveAgentsRoot(opts);
77874
77981
  const files = loadFiles(opts);
77875
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
+ }));
77876
77993
  appendAudit(agent, "skill.init_personal", { name, files: Object.keys(files).length }, 0);
77877
77994
  }
77878
77995
  function editPersonalAction(name, opts) {
@@ -77880,8 +77997,126 @@ function editPersonalAction(name, opts) {
77880
77997
  const agentsRoot = resolveAgentsRoot(opts);
77881
77998
  const files = loadFiles(opts);
77882
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
+ }));
77883
78010
  appendAudit(agent, "skill.edit_personal", { name, files: Object.keys(files).length }, 0);
77884
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
+ }
77885
78120
  function removePersonalAction(name, opts) {
77886
78121
  const agent = resolveAgent(opts);
77887
78122
  const agentsRoot = resolveAgentsRoot(opts);
@@ -77909,6 +78144,7 @@ function removePersonalAction(name, opts) {
77909
78144
  renameSync17(target, trashTarget);
77910
78145
  const now = new Date(ts);
77911
78146
  utimesSync(trashTarget, now, now);
78147
+ mirrorToConfigRepo(agent, name, null);
77912
78148
  console.log(JSON.stringify({
77913
78149
  ok: true,
77914
78150
  action: "remove",
@@ -77974,6 +78210,9 @@ function registerSkillPersonalCommands(program3) {
77974
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) => {
77975
78211
  listPersonalAction(opts);
77976
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
+ }));
77977
78216
  }
77978
78217
 
77979
78218
  // src/cli/skill-search.ts
@@ -77988,10 +78227,10 @@ var AGENT_NAME_RE3 = /^[a-z][a-z0-9_-]{0,62}$/;
77988
78227
  function defaultAgentsRoot() {
77989
78228
  return resolve46(homedir40(), ".switchroom/agents");
77990
78229
  }
77991
- function defaultSharedRoot() {
78230
+ function defaultSharedRoot2() {
77992
78231
  return resolve46(homedir40(), ".switchroom/skills");
77993
78232
  }
77994
- function defaultBundledRoot() {
78233
+ function defaultBundledRoot2() {
77995
78234
  return resolve46(homedir40(), ".switchroom/skills/_bundled");
77996
78235
  }
77997
78236
  function readSkillFrontmatter(skillDir) {
@@ -78077,7 +78316,7 @@ function listPersonalSkills(agent, agentsRoot = defaultAgentsRoot()) {
78077
78316
  }
78078
78317
  return out;
78079
78318
  }
78080
- function listSharedSkills(sharedRoot = defaultSharedRoot()) {
78319
+ function listSharedSkills(sharedRoot = defaultSharedRoot2()) {
78081
78320
  if (!existsSync77(sharedRoot))
78082
78321
  return [];
78083
78322
  const out = [];
@@ -78115,7 +78354,7 @@ function listSharedSkills(sharedRoot = defaultSharedRoot()) {
78115
78354
  }
78116
78355
  return out;
78117
78356
  }
78118
- function listBundledSkills(bundledRoot = defaultBundledRoot()) {
78357
+ function listBundledSkills(bundledRoot = defaultBundledRoot2()) {
78119
78358
  if (!existsSync77(bundledRoot))
78120
78359
  return [];
78121
78360
  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.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.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.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