spacecode 2.5.3 → 2.5.5

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/repl.mjs CHANGED
@@ -4925,6 +4925,211 @@ var init_summarizer = __esm({
4925
4925
  }
4926
4926
  });
4927
4927
 
4928
+ // ../agent/src/import/command-file.ts
4929
+ function parseCommandFile(text) {
4930
+ const lines2 = text.replace(/\r\n/g, "\n").split("\n");
4931
+ const close = lines2[0] === "---" ? lines2.indexOf("---", 1) : -1;
4932
+ if (close > 0) {
4933
+ const line = lines2.slice(1, close).find((l) => /^description\s*:/.test(l));
4934
+ const description = line ? unquote(line.slice(line.indexOf(":") + 1)).slice(0, 200) : "";
4935
+ return { description, body: lines2.slice(close + 1).join("\n").replace(/^\n+/, "") };
4936
+ }
4937
+ const normalized = lines2.join("\n");
4938
+ const first = lines2.find((l) => l.trim() !== "");
4939
+ return { description: first ? first.trim().slice(0, 80) : "", body: normalized };
4940
+ }
4941
+ var unquote;
4942
+ var init_command_file = __esm({
4943
+ "../agent/src/import/command-file.ts"() {
4944
+ "use strict";
4945
+ unquote = (v2) => {
4946
+ const t = v2.trim();
4947
+ return t.length >= 2 && (t[0] === '"' || t[0] === "'") && t[t.length - 1] === t[0] ? t.slice(1, -1) : t;
4948
+ };
4949
+ }
4950
+ });
4951
+
4952
+ // ../agent/src/import/skill-file.ts
4953
+ function parseSkillFile(text) {
4954
+ const lines2 = text.replace(/\r\n/g, "\n").split("\n");
4955
+ if (lines2[0] !== "---") return { ok: false, error: "missing frontmatter" };
4956
+ const endIdx = lines2.indexOf("---", 1);
4957
+ if (endIdx === -1) return { ok: false, error: "missing frontmatter" };
4958
+ const front = lines2.slice(1, endIdx);
4959
+ let name;
4960
+ let description;
4961
+ const blockIndicators = /* @__PURE__ */ new Set([">", "|", ">-", "|-"]);
4962
+ for (let i = 0; i < front.length; i++) {
4963
+ const raw = front[i];
4964
+ if (!raw.trim() || raw.trim().startsWith("#") || /^\s/.test(raw)) continue;
4965
+ if (!raw.includes(":")) return { ok: false, error: `malformed frontmatter line: ${raw}` };
4966
+ const [rawKey, ...rest] = raw.split(":");
4967
+ const key = rawKey.trim();
4968
+ let value = rest.join(":").trim();
4969
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
4970
+ if (key === "description" && blockIndicators.has(value)) {
4971
+ const block = [];
4972
+ let j2 = i + 1;
4973
+ while (j2 < front.length && /^\s/.test(front[j2])) block.push(front[j2++].replace(/^\s+/, ""));
4974
+ description = block.join(" ").trim();
4975
+ i = j2 - 1;
4976
+ continue;
4977
+ }
4978
+ if (key === "name") name = value;
4979
+ else if (key === "description") description = value;
4980
+ }
4981
+ if (!name || name.trim() === "") return { ok: false, error: "missing name" };
4982
+ if (!description || description.trim() === "") return { ok: false, error: "missing description" };
4983
+ return { ok: true, name: name.trim(), description: description.trim(), body: lines2.slice(endIdx + 1).join("\n").replace(/^(?:[ \t]*\n)*/, "") };
4984
+ }
4985
+ var init_skill_file = __esm({
4986
+ "../agent/src/import/skill-file.ts"() {
4987
+ "use strict";
4988
+ }
4989
+ });
4990
+
4991
+ // ../agent/src/import/tool-names.ts
4992
+ function translateToolNames(text) {
4993
+ const counts = /* @__PURE__ */ new Map();
4994
+ const names = Object.keys(TOOL_NAME_MAP).join("|");
4995
+ const re3 = new RegExp("`(" + names + ")(\\([^`)]*\\))?`", "g");
4996
+ const out = text.replace(re3, (_m, name, args) => {
4997
+ counts.set(name, (counts.get(name) ?? 0) + 1);
4998
+ return "`" + TOOL_NAME_MAP[name] + (args ?? "") + "`";
4999
+ });
5000
+ return { text: out, replaced: [...counts].map(([from, count]) => ({ from, to: TOOL_NAME_MAP[from], count })) };
5001
+ }
5002
+ function findUnsupported(text) {
5003
+ return UNSUPPORTED.filter((u3) => u3.re.test(text)).map((u3) => u3.label);
5004
+ }
5005
+ var TOOL_NAME_MAP, UNSUPPORTED;
5006
+ var init_tool_names = __esm({
5007
+ "../agent/src/import/tool-names.ts"() {
5008
+ "use strict";
5009
+ TOOL_NAME_MAP = {
5010
+ Read: "read_file",
5011
+ Write: "write_file",
5012
+ Edit: "edit_file",
5013
+ MultiEdit: "edit_file",
5014
+ Glob: "list_directory",
5015
+ Grep: "search",
5016
+ Bash: "bash",
5017
+ TodoWrite: "todo_write"
5018
+ };
5019
+ UNSUPPORTED = [
5020
+ { label: "hooks", re: /\bhooks?\b.*\.(sh|json)|\bPreToolUse\b|\bPostToolUse\b|\bSessionStart\b|\bUserPromptSubmit\b/i },
5021
+ { label: "ToolSearch / deferred tools", re: /\bToolSearch\b|deferred tools?/i },
5022
+ { label: "Claude Code's Task/subagent tools", re: /\bsubagents?\b|\bAgent tool\b|`Task`|subagent_type/i },
5023
+ { label: "Claude Code skills / slash commands", re: /\bSkill tool\b|invoke (?:the )?\w+ skill|\/ship\b|\/handoff\b/i },
5024
+ { label: "MCP tool names (mcp__*)", re: /\bmcp__[A-Za-z0-9_-]+/ },
5025
+ { label: "WebFetch/WebSearch/NotebookEdit tools", re: /`(?:WebFetch|WebSearch|NotebookEdit)`/ },
5026
+ { label: "plan mode / worktree tools", re: /\bEnterPlanMode\b|\bExitPlanMode\b|\bEnterWorktree\b/ }
5027
+ ];
5028
+ }
5029
+ });
5030
+
5031
+ // ../agent/src/import/skill-tool.ts
5032
+ function safeSkillPath(rel) {
5033
+ if (rel === "" || rel.length > 200 || rel.startsWith("/") || rel.includes("\\") || rel.includes("\0")) return null;
5034
+ const parts = rel.split("/");
5035
+ return parts.every((p) => p !== "" && p !== "." && p !== "..") ? parts.join("/") : null;
5036
+ }
5037
+ function formatSkillBody(name, body, scripts) {
5038
+ const parsed = parseSkillFile(body);
5039
+ const t = translateToolNames(parsed.ok ? parsed.body : body);
5040
+ const unsupported = findUnsupported(t.text);
5041
+ const head = [`# Skill: ${name}`];
5042
+ if (t.replaced.length) head.push(`Tool names translated: ${t.replaced.map((r) => `${r.from} -> ${r.to}`).join(", ")}.`);
5043
+ if (unsupported.length) head.push(`Not supported here (skip those parts): ${unsupported.join("; ")}.`);
5044
+ if (scripts) head.push("This skill ships scripts: they are not runnable until permitted (they run through `bash` and its permission rules).");
5045
+ return `${head.join("\n")}
5046
+
5047
+ ${t.text}`;
5048
+ }
5049
+ function createSkillTool(source) {
5050
+ return async (input) => {
5051
+ const name = typeof input.name === "string" ? input.name.trim() : "";
5052
+ if (!NAME_RE.test(name)) return { output: 'skill needs a "name" from the skills list', is_error: true };
5053
+ try {
5054
+ if (typeof input.file === "string" && input.file !== "") {
5055
+ const rel = safeSkillPath(input.file);
5056
+ if (!rel) return { output: `skill file path "${input.file}" must be relative to the skill, with no ".." segments`, is_error: true };
5057
+ const text = await source.file(name, rel);
5058
+ return text === null ? { output: `Skill "${name}" has no file "${rel}".`, is_error: true } : { output: text };
5059
+ }
5060
+ const body = await source.body(name);
5061
+ if (body === null) return { output: `No skill named "${name}". Use a name from the skills list.`, is_error: true };
5062
+ const scripts = (await source.list()).find((s) => s.name === name)?.scripts ?? false;
5063
+ return { output: formatSkillBody(name, body, scripts) };
5064
+ } catch (err) {
5065
+ return { output: `skill failed: ${err instanceof Error ? err.message : String(err)}`, is_error: true };
5066
+ }
5067
+ };
5068
+ }
5069
+ var SKILL_TOOL, NAME_RE;
5070
+ var init_skill_tool = __esm({
5071
+ "../agent/src/import/skill-tool.ts"() {
5072
+ "use strict";
5073
+ init_tool_names();
5074
+ init_skill_file();
5075
+ SKILL_TOOL = {
5076
+ name: "skill",
5077
+ description: 'Load an imported skill by name (listed under "Skills (imported)" in your instructions) and follow it. Pass `file` to read a supporting file that ships with the skill (a path relative to the skill, e.g. "reference.md"). Skill text is written for Claude Code: tool names in it are translated, and lines under "Not supported here" name features that do not exist in this REPL.',
5078
+ input_schema: {
5079
+ type: "object",
5080
+ properties: {
5081
+ name: { type: "string", description: "The skill name exactly as listed." },
5082
+ file: { type: "string", description: 'Optional path of a sibling file inside the skill, e.g. "scripts/run.sh" or "reference.md".' }
5083
+ },
5084
+ required: ["name"]
5085
+ }
5086
+ };
5087
+ NAME_RE = /^[A-Za-z0-9_-]{1,64}$/;
5088
+ }
5089
+ });
5090
+
5091
+ // ../agent/src/import/skill-source.ts
5092
+ function siblingAssetName(skill, rel) {
5093
+ return `${skill}.${rel.replace(/[^A-Za-z0-9_-]/g, "_")}`.slice(0, 100);
5094
+ }
5095
+ function isScriptPath(rel) {
5096
+ return SCRIPT_RE.test(rel);
5097
+ }
5098
+ function createProfileSkillSource(client) {
5099
+ const entries = async () => ((await client.index())?.assets ?? []).filter((a) => a.kind === "skill");
5100
+ return {
5101
+ async list() {
5102
+ const all = await entries();
5103
+ return all.filter((a) => NAME_RE2.test(a.name)).map((a) => ({
5104
+ name: a.name,
5105
+ description: a.description,
5106
+ scope: "user",
5107
+ scripts: all.some((s) => s.name.startsWith(`${a.name}.`) && s.description.startsWith(SIBLING_PREFIX) && isScriptPath(s.description.slice(SIBLING_PREFIX.length)))
5108
+ }));
5109
+ },
5110
+ async body(name) {
5111
+ return (await client.get("skill", name))?.content ?? null;
5112
+ },
5113
+ async file(name, rel) {
5114
+ const safe = safeSkillPath(rel);
5115
+ if (!safe) return null;
5116
+ const all = await entries();
5117
+ const hit = all.find((a) => a.name.startsWith(`${name}.`) && a.description === `${SIBLING_PREFIX}${safe}`);
5118
+ return hit ? (await client.get("skill", hit.name))?.content ?? null : null;
5119
+ }
5120
+ };
5121
+ }
5122
+ var SIBLING_PREFIX, NAME_RE2, SCRIPT_RE;
5123
+ var init_skill_source = __esm({
5124
+ "../agent/src/import/skill-source.ts"() {
5125
+ "use strict";
5126
+ init_skill_tool();
5127
+ SIBLING_PREFIX = "file:";
5128
+ NAME_RE2 = /^[A-Za-z0-9_-]{1,64}$/;
5129
+ SCRIPT_RE = /^(scripts\/|.*\.(sh|py|js|mjs|ts|rb|pl)$)/;
5130
+ }
5131
+ });
5132
+
4928
5133
  // ../agent/src/import/claude-sources.ts
4929
5134
  function isEnoent(err) {
4930
5135
  return typeof err === "object" && err !== null && err.code === "ENOENT";
@@ -4962,6 +5167,8 @@ async function readClaudeAssets({ fs: fs6, home, projectDirs }) {
4962
5167
  report.assets.push({ kind: "instructions", name: "global", description: "Global CLAUDE.md", content: globalText, source: globalPath });
4963
5168
  if (/^@\S+/m.test(globalText)) report.notes.push("CLAUDE.md contains @path includes; they are kept as plain text and not expanded in v1");
4964
5169
  }
5170
+ await readCommands(fs6, home, report);
5171
+ await readSkills(fs6, home, report);
4965
5172
  for (const dir of projectDirs) {
4966
5173
  const memDir = `${home}/.claude/projects/${claudeProjectSlug(dir)}/memory`;
4967
5174
  let entries;
@@ -4981,7 +5188,7 @@ async function readClaudeAssets({ fs: fs6, home, projectDirs }) {
4981
5188
  if (text === null) continue;
4982
5189
  const base = entry === "MEMORY.md" ? "index" : entry.slice(0, -3).replace(/[^A-Za-z0-9._-]/g, "_");
4983
5190
  const name = `${key}.${base}`;
4984
- if (!NAME_RE.test(name)) {
5191
+ if (!NAME_RE3.test(name)) {
4985
5192
  report.errors.push({ path: path12, message: `cannot derive a valid asset name from "${entry}"` });
4986
5193
  continue;
4987
5194
  }
@@ -5003,11 +5210,119 @@ async function readClaudeAssets({ fs: fs6, home, projectDirs }) {
5003
5210
  report.notes.push("No Claude Code memory directory found for this project");
5004
5211
  return report;
5005
5212
  }
5006
- var NAME_RE;
5213
+ async function readCommands(fs6, home, report) {
5214
+ const dir = `${home}/.claude/commands`;
5215
+ let entries;
5216
+ try {
5217
+ entries = await fs6.readdir(dir);
5218
+ } catch (err) {
5219
+ if (!isEnoent(err)) report.errors.push({ path: dir, message: err instanceof Error ? err.message : String(err) });
5220
+ return;
5221
+ }
5222
+ const seen = /* @__PURE__ */ new Set();
5223
+ for (const entry of [...entries].sort()) {
5224
+ const path12 = `${dir}/${entry}`;
5225
+ if (!entry.endsWith(".md")) {
5226
+ if (!entry.startsWith(".")) report.notes.push(`Skipped ${path12}: only top-level .md command files are imported (subdirectories are not supported)`);
5227
+ continue;
5228
+ }
5229
+ const name = entry.slice(0, -3);
5230
+ if (!COMMAND_NAME_RE.test(name)) {
5231
+ report.errors.push({ path: path12, message: `command name "${name}" must start with a letter and use only letters, digits, - and _` });
5232
+ continue;
5233
+ }
5234
+ if (seen.has(name.toLowerCase())) {
5235
+ report.errors.push({ path: path12, message: `command name collision on "${name}" (names are case-insensitive)` });
5236
+ continue;
5237
+ }
5238
+ const text = await readOptional(fs6, path12, report);
5239
+ if (text === null) continue;
5240
+ if (Buffer.byteLength(text, "utf8") > MAX_ASSET_BYTES) {
5241
+ report.errors.push({ path: path12, message: `skipped: ${Buffer.byteLength(text, "utf8")} bytes is over the ${MAX_ASSET_BYTES} byte asset limit` });
5242
+ continue;
5243
+ }
5244
+ seen.add(name.toLowerCase());
5245
+ report.assets.push({ kind: "command", name, description: parseCommandFile(text).description, content: text, source: path12 });
5246
+ }
5247
+ }
5248
+ async function listFiles(fs6, dir, rel, out, report) {
5249
+ let entries;
5250
+ try {
5251
+ entries = await fs6.readdir(dir);
5252
+ } catch (err) {
5253
+ const code = err.code;
5254
+ if (code === "ENOTDIR") out.push(rel);
5255
+ else if (code !== "ENOENT") report.errors.push({ path: dir, message: err instanceof Error ? err.message : String(err) });
5256
+ return;
5257
+ }
5258
+ for (const e of [...entries].sort()) {
5259
+ if (e.startsWith(".")) continue;
5260
+ await listFiles(fs6, `${dir}/${e}`, rel === "" ? e : `${rel}/${e}`, out, report);
5261
+ }
5262
+ }
5263
+ async function readSkills(fs6, home, report) {
5264
+ const root = `${home}/.claude/skills`;
5265
+ let dirs;
5266
+ try {
5267
+ dirs = await fs6.readdir(root);
5268
+ } catch (err) {
5269
+ if (!isEnoent(err)) report.errors.push({ path: root, message: err instanceof Error ? err.message : String(err) });
5270
+ return;
5271
+ }
5272
+ const seen = /* @__PURE__ */ new Set();
5273
+ for (const dir of [...dirs].sort()) {
5274
+ if (dir.startsWith(".")) continue;
5275
+ const skillPath = `${root}/${dir}/SKILL.md`;
5276
+ const text = await readOptional(fs6, skillPath, report);
5277
+ if (text === null) continue;
5278
+ const parsed = parseSkillFile(text);
5279
+ if (!parsed.ok) {
5280
+ report.errors.push({ path: skillPath, message: `malformed skill: ${parsed.error}` });
5281
+ continue;
5282
+ }
5283
+ if (!SKILL_NAME_RE.test(parsed.name) || seen.has(parsed.name.toLowerCase())) {
5284
+ report.errors.push({ path: skillPath, message: seen.has(parsed.name.toLowerCase()) ? `skill name collision on "${parsed.name}"` : `skill name "${parsed.name}" must use only letters, digits, - and _` });
5285
+ continue;
5286
+ }
5287
+ const bytes = Buffer.byteLength(text, "utf8");
5288
+ if (bytes > MAX_ASSET_BYTES) {
5289
+ report.errors.push({ path: skillPath, message: `skill skipped: SKILL.md is ${bytes} bytes, over the ${MAX_ASSET_BYTES} byte asset limit` });
5290
+ continue;
5291
+ }
5292
+ seen.add(parsed.name.toLowerCase());
5293
+ const files = [];
5294
+ await listFiles(fs6, `${root}/${dir}`, "", files, report);
5295
+ const siblings = [];
5296
+ const slugs = /* @__PURE__ */ new Set();
5297
+ for (const rel of files.filter((f) => f !== "SKILL.md")) {
5298
+ const path12 = `${root}/${dir}/${rel}`;
5299
+ const content = await readOptional(fs6, path12, report);
5300
+ if (content === null) continue;
5301
+ const size = Buffer.byteLength(content, "utf8");
5302
+ const name = siblingAssetName(parsed.name, rel);
5303
+ if (size > MAX_ASSET_BYTES || slugs.has(name) || siblings.length >= MAX_SKILL_FILES || content.includes("\0")) {
5304
+ report.notes.push(`Skill ${parsed.name}: ${rel} not imported (${content.includes("\0") ? "binary file" : size > MAX_ASSET_BYTES ? `${size} bytes, over the ${MAX_ASSET_BYTES} byte limit` : slugs.has(name) ? "name collision" : `more than ${MAX_SKILL_FILES} files`}); the skill still loads without it`);
5305
+ continue;
5306
+ }
5307
+ slugs.add(name);
5308
+ siblings.push({ kind: "skill", name, description: `${SIBLING_PREFIX}${rel}`, content, source: path12 });
5309
+ }
5310
+ if (siblings.some((s) => isScriptPath(s.description.slice(SIBLING_PREFIX.length)))) report.notes.push(`Skill ${parsed.name}: ships scripts, not runnable until permitted (they run through bash and its permission rules)`);
5311
+ report.assets.push({ kind: "skill", name: parsed.name, description: parsed.description, content: text, source: skillPath }, ...siblings);
5312
+ }
5313
+ }
5314
+ var MAX_ASSET_BYTES, NAME_RE3, COMMAND_NAME_RE, SKILL_NAME_RE, MAX_SKILL_FILES;
5007
5315
  var init_claude_sources = __esm({
5008
5316
  "../agent/src/import/claude-sources.ts"() {
5009
5317
  "use strict";
5010
- NAME_RE = /^[A-Za-z0-9._-]{1,100}$/;
5318
+ init_command_file();
5319
+ init_skill_file();
5320
+ init_skill_source();
5321
+ MAX_ASSET_BYTES = 256 * 1024;
5322
+ NAME_RE3 = /^[A-Za-z0-9._-]{1,100}$/;
5323
+ COMMAND_NAME_RE = /^[A-Za-z][A-Za-z0-9_-]*$/;
5324
+ SKILL_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/;
5325
+ MAX_SKILL_FILES = 40;
5011
5326
  }
5012
5327
  });
5013
5328
 
@@ -5057,6 +5372,16 @@ function createProfileClient({ baseUrl, apiKey, fetch: doFetch, timeoutMs = 8e3
5057
5372
  const res = await call("POST", "/adapt", { body: { sections }, timeoutMs: ADAPT_TIMEOUT_MS });
5058
5373
  return await res.json();
5059
5374
  },
5375
+ /** Deletes one asset; false when it did not exist. */
5376
+ async delete(kind, name) {
5377
+ try {
5378
+ await call("DELETE", `/assets/${enc(kind)}/${enc(name)}`);
5379
+ return true;
5380
+ } catch (err) {
5381
+ if (err instanceof ProfileApiError && err.status === 404) return false;
5382
+ throw err;
5383
+ }
5384
+ },
5060
5385
  async put(kind, name, input) {
5061
5386
  const res = await call("PUT", `/assets/${enc(kind)}/${enc(name)}`, {
5062
5387
  body: { content: input.content, description: input.description ?? "", base_version: input.baseVersion }
@@ -5239,46 +5564,6 @@ var init_instruction_sections = __esm({
5239
5564
  }
5240
5565
  });
5241
5566
 
5242
- // ../agent/src/import/tool-names.ts
5243
- function translateToolNames(text) {
5244
- const counts = /* @__PURE__ */ new Map();
5245
- const names = Object.keys(TOOL_NAME_MAP).join("|");
5246
- const re3 = new RegExp("`(" + names + ")(\\([^`)]*\\))?`", "g");
5247
- const out = text.replace(re3, (_m, name, args) => {
5248
- counts.set(name, (counts.get(name) ?? 0) + 1);
5249
- return "`" + TOOL_NAME_MAP[name] + (args ?? "") + "`";
5250
- });
5251
- return { text: out, replaced: [...counts].map(([from, count]) => ({ from, to: TOOL_NAME_MAP[from], count })) };
5252
- }
5253
- function findUnsupported(text) {
5254
- return UNSUPPORTED.filter((u3) => u3.re.test(text)).map((u3) => u3.label);
5255
- }
5256
- var TOOL_NAME_MAP, UNSUPPORTED;
5257
- var init_tool_names = __esm({
5258
- "../agent/src/import/tool-names.ts"() {
5259
- "use strict";
5260
- TOOL_NAME_MAP = {
5261
- Read: "read_file",
5262
- Write: "write_file",
5263
- Edit: "edit_file",
5264
- MultiEdit: "edit_file",
5265
- Glob: "list_directory",
5266
- Grep: "search",
5267
- Bash: "bash",
5268
- TodoWrite: "todo_write"
5269
- };
5270
- UNSUPPORTED = [
5271
- { label: "hooks", re: /\bhooks?\b.*\.(sh|json)|\bPreToolUse\b|\bPostToolUse\b|\bSessionStart\b|\bUserPromptSubmit\b/i },
5272
- { label: "ToolSearch / deferred tools", re: /\bToolSearch\b|deferred tools?/i },
5273
- { label: "Claude Code's Task/subagent tools", re: /\bsubagents?\b|\bAgent tool\b|`Task`|subagent_type/i },
5274
- { label: "Claude Code skills / slash commands", re: /\bSkill tool\b|invoke (?:the )?\w+ skill|\/ship\b|\/handoff\b/i },
5275
- { label: "MCP tool names (mcp__*)", re: /\bmcp__[A-Za-z0-9_-]+/ },
5276
- { label: "WebFetch/WebSearch/NotebookEdit tools", re: /`(?:WebFetch|WebSearch|NotebookEdit)`/ },
5277
- { label: "plan mode / worktree tools", re: /\bEnterPlanMode\b|\bExitPlanMode\b|\bEnterWorktree\b/ }
5278
- ];
5279
- }
5280
- });
5281
-
5282
5567
  // ../agent/src/import/coverage.ts
5283
5568
  function extractRuleLines(text) {
5284
5569
  const out = [];
@@ -5719,6 +6004,105 @@ var init_permission_rules = __esm({
5719
6004
  }
5720
6005
  });
5721
6006
 
6007
+ // ../agent/src/import/command-args.ts
6008
+ function parseWords(input) {
6009
+ const words = [];
6010
+ let current = "";
6011
+ let quote = null;
6012
+ let has = false;
6013
+ for (const ch of input) {
6014
+ if (quote) {
6015
+ if (ch === quote) quote = null;
6016
+ else current += ch;
6017
+ } else if (ch === '"' || ch === "'") {
6018
+ quote = ch;
6019
+ has = true;
6020
+ } else if (/\s/.test(ch)) {
6021
+ if (has) words.push(current);
6022
+ current = "";
6023
+ has = false;
6024
+ } else {
6025
+ current += ch;
6026
+ has = true;
6027
+ }
6028
+ }
6029
+ if (has) words.push(current);
6030
+ return words;
6031
+ }
6032
+ function expandCommand(body, args) {
6033
+ const trimmed = args.trim();
6034
+ const words = parseWords(trimmed);
6035
+ return body.replace(/\$ARGUMENTS|\$[1-9]/g, (m2) => m2 === "$ARGUMENTS" ? trimmed : words[Number(m2[1]) - 1] ?? "");
6036
+ }
6037
+ var init_command_args = __esm({
6038
+ "../agent/src/import/command-args.ts"() {
6039
+ "use strict";
6040
+ }
6041
+ });
6042
+
6043
+ // ../agent/src/import/command-registry.ts
6044
+ function buildCommandRegistry(input) {
6045
+ const builtins = new Set(input.builtins.map((b2) => b2.toLowerCase()));
6046
+ const commands = [];
6047
+ const skipped = [];
6048
+ const shadowed = [];
6049
+ const accepted = /* @__PURE__ */ new Map();
6050
+ const lists = [["repo", input.repo], ["user", input.user]];
6051
+ for (const [scope, defs] of lists) {
6052
+ for (const def of defs) {
6053
+ const name = def.name.toLowerCase();
6054
+ if (!VALID.test(def.name)) skipped.push({ name: def.name, scope, reason: "invalid name" });
6055
+ else if (builtins.has(name)) skipped.push({ name, scope, reason: `collides with built-in /${name}` });
6056
+ else if (accepted.get(name) === scope) skipped.push({ name, scope, reason: "duplicate name" });
6057
+ else if (accepted.get(name) === "repo") shadowed.push(name);
6058
+ else {
6059
+ accepted.set(name, scope);
6060
+ commands.push({ ...def, name, scope });
6061
+ }
6062
+ }
6063
+ }
6064
+ commands.sort((a, b2) => a.name < b2.name ? -1 : a.name > b2.name ? 1 : 0);
6065
+ return { commands, skipped, shadowed };
6066
+ }
6067
+ var VALID;
6068
+ var init_command_registry = __esm({
6069
+ "../agent/src/import/command-registry.ts"() {
6070
+ "use strict";
6071
+ VALID = /^[A-Za-z][A-Za-z0-9_-]*$/;
6072
+ }
6073
+ });
6074
+
6075
+ // ../agent/src/import/skill-prompt.ts
6076
+ function buildSkillsSection(skills, budget = SKILLS_BUDGET_CHARS) {
6077
+ if (skills.length === 0) return { text: "", trimmed: [], omitted: [] };
6078
+ const header = "\n## Skills (imported)\nCall the `skill` tool with a skill name to load its full instructions before doing work it covers.\n";
6079
+ const clean = skills.map((s) => ({ name: s.name, full: s.description.replace(/\s+/g, " ").trim() }));
6080
+ const descs = new Map(clean.map((s) => [s.name, s.full]));
6081
+ const line = (n) => `- ${n}: ${descs.get(n)}
6082
+ `;
6083
+ const size = (names2) => header.length + names2.reduce((a, n) => a + line(n).length, 0);
6084
+ let names = clean.map((s) => s.name);
6085
+ const trimmed = /* @__PURE__ */ new Set();
6086
+ while (size(names) > budget) {
6087
+ const longest = [...names].sort((a, b2) => descs.get(b2).length - descs.get(a).length)[0];
6088
+ const d = descs.get(longest);
6089
+ if (d.length <= MIN_DESC) break;
6090
+ descs.set(longest, `${d.slice(0, Math.max(MIN_DESC, Math.floor(d.length * 0.7)) - 1)}\u2026`);
6091
+ trimmed.add(longest);
6092
+ }
6093
+ const omitted = [];
6094
+ while (size(names) > budget && names.length > 0) omitted.unshift(names.pop());
6095
+ return { text: names.length ? header + names.map(line).join("") : "", trimmed: [...trimmed], omitted };
6096
+ }
6097
+ var SKILLS_BUDGET_CHARS, MIN_DESC;
6098
+ var init_skill_prompt = __esm({
6099
+ "../agent/src/import/skill-prompt.ts"() {
6100
+ "use strict";
6101
+ SKILLS_BUDGET_CHARS = 4e3;
6102
+ MIN_DESC = 20;
6103
+ }
6104
+ });
6105
+
5722
6106
  // ../agent/src/index.ts
5723
6107
  var init_src = __esm({
5724
6108
  "../agent/src/index.ts"() {
@@ -5759,6 +6143,14 @@ var init_src = __esm({
5759
6143
  init_trust();
5760
6144
  init_imports_config();
5761
6145
  init_permission_rules();
6146
+ init_command_args();
6147
+ init_command_file();
6148
+ init_command_registry();
6149
+ init_claude_sources();
6150
+ init_skill_file();
6151
+ init_skill_prompt();
6152
+ init_skill_tool();
6153
+ init_skill_source();
5762
6154
  }
5763
6155
  });
5764
6156
 
@@ -5799,14 +6191,225 @@ var init_api_key = __esm({
5799
6191
  }
5800
6192
  });
5801
6193
 
6194
+ // src/import-gate.ts
6195
+ import { createHash, randomUUID } from "node:crypto";
6196
+ import { chmod, mkdir, readFile, readdir, realpath, rename, writeFile } from "node:fs/promises";
6197
+ import { dirname as dirname3, join as join2 } from "node:path";
6198
+ async function readOrNull(path12) {
6199
+ try {
6200
+ return await readFile(path12, "utf8");
6201
+ } catch (err) {
6202
+ if (err.code === "ENOENT") return null;
6203
+ throw err;
6204
+ }
6205
+ }
6206
+ async function writeTextAtomic(path12, text) {
6207
+ await mkdir(dirname3(path12), { recursive: true });
6208
+ const tmp = `${path12}.${process.pid}.${randomUUID()}.tmp`;
6209
+ await writeFile(tmp, text, { mode: 384 });
6210
+ await chmod(tmp, 384);
6211
+ await rename(tmp, path12);
6212
+ }
6213
+ async function readRepoSources(cwd2) {
6214
+ const files = {};
6215
+ for (const name of REPO_SOURCE_FILES) {
6216
+ const text = await readOrNull(join2(cwd2, name));
6217
+ if (text !== null) files[name] = text;
6218
+ }
6219
+ let entries = [];
6220
+ try {
6221
+ entries = await readdir(join2(cwd2, ".claude", "commands"));
6222
+ } catch (err) {
6223
+ if (err.code !== "ENOENT" && err.code !== "ENOTDIR") throw err;
6224
+ }
6225
+ for (const e of entries.filter((x2) => x2.endsWith(".md")).sort()) {
6226
+ const text = await readOrNull(join2(cwd2, ".claude", "commands", e));
6227
+ if (text !== null) files[`.claude/commands/${e}`] = text;
6228
+ }
6229
+ await readRepoSkillFiles(cwd2, files);
6230
+ return files;
6231
+ }
6232
+ async function readRepoSkillFiles(cwd2, files) {
6233
+ const root = join2(cwd2, ".claude", "skills");
6234
+ let count = 0;
6235
+ const walk = async (dir, rel) => {
6236
+ let entries;
6237
+ try {
6238
+ entries = await readdir(dir, { withFileTypes: true });
6239
+ } catch (err) {
6240
+ const code = err.code;
6241
+ if (code === "ENOENT" || code === "ENOTDIR") return;
6242
+ throw err;
6243
+ }
6244
+ for (const e of entries.sort((a, b2) => a.name < b2.name ? -1 : 1)) {
6245
+ if (e.name.startsWith(".") || e.isSymbolicLink()) continue;
6246
+ const key = `${rel}/${e.name}`;
6247
+ if (e.isDirectory()) await walk(join2(dir, e.name), key);
6248
+ else if (e.isFile() && count < MAX_REPO_SKILL_FILES) {
6249
+ const text = await readOrNull(join2(dir, e.name));
6250
+ if (text !== null && !text.includes("\0") && Buffer.byteLength(text, "utf8") <= MAX_REPO_SKILL_BYTES) {
6251
+ files[key] = text;
6252
+ count++;
6253
+ }
6254
+ }
6255
+ }
6256
+ };
6257
+ await walk(root, ".claude/skills");
6258
+ }
6259
+ function nodeTrustFs() {
6260
+ return { readText: readOrNull, writeTextAtomic, realPath: (p) => realpath(p) };
6261
+ }
6262
+ function createNodeTrustStore(configDir) {
6263
+ return new TrustStore({ fs: nodeTrustFs(), path: join2(configDir, "trust.json"), hash: sha256 });
6264
+ }
6265
+ async function importGateStatus(deps) {
6266
+ const lines2 = [];
6267
+ const configPath = join2(deps.configDir, "imports.json");
6268
+ try {
6269
+ const loaded = parseImportsConfig(await readOrNull(configPath), configPath);
6270
+ if (loaded.error) lines2.push(`imports: ${loaded.error} (every category off)`);
6271
+ else {
6272
+ const on = IMPORT_CATEGORIES.filter((c) => loaded.config.categories[c]);
6273
+ lines2.push(on.length ? `imports: on for ${on.join(", ")}` : "imports: every category off (default)");
6274
+ }
6275
+ } catch (err) {
6276
+ lines2.push(`imports: not read (${err instanceof Error ? err.message : String(err)}); every category off`);
6277
+ }
6278
+ try {
6279
+ const files = await readRepoSources(deps.cwd);
6280
+ const found = Object.keys(files);
6281
+ if (found.length === 0) return lines2;
6282
+ const status = await createNodeTrustStore(deps.configDir).repoStatus(deps.cwd, files);
6283
+ const names = found.join(", ");
6284
+ if (status === "trusted" || status === "session") lines2.push(`repo sources: ${names} (trusted${status === "session" ? " for this session" : ""})`);
6285
+ else if (status === "never") lines2.push(`repo sources: ${names} NOT loaded (marked never trust)`);
6286
+ else lines2.push(`repo sources: ${names} NOT loaded (untrusted${deps.interactive ? "" : ", non-interactive run"})`);
6287
+ } catch (err) {
6288
+ lines2.push(`repo sources: NOT loaded (trust check failed: ${err instanceof Error ? err.message : String(err)})`);
6289
+ }
6290
+ return lines2;
6291
+ }
6292
+ var REPO_SOURCE_FILES, MAX_REPO_SKILL_FILES, MAX_REPO_SKILL_BYTES, sha256;
6293
+ var init_import_gate = __esm({
6294
+ "src/import-gate.ts"() {
6295
+ "use strict";
6296
+ init_src();
6297
+ REPO_SOURCE_FILES = [".mcp.json", ".claude/settings.json", ".claude/settings.local.json", "CLAUDE.local.md"];
6298
+ MAX_REPO_SKILL_FILES = 200;
6299
+ MAX_REPO_SKILL_BYTES = 256 * 1024;
6300
+ sha256 = (text) => createHash("sha256").update(text).digest("hex");
6301
+ }
6302
+ });
6303
+
6304
+ // src/skills-loader.ts
6305
+ import { join as join3 } from "node:path";
6306
+ function repoSkillSource(sources, parse, problems) {
6307
+ const dirs = /* @__PURE__ */ new Map();
6308
+ for (const key of Object.keys(sources).filter((k2) => k2.startsWith(REPO_PREFIX) && k2.endsWith("/SKILL.md") && k2.split("/").length === 4)) {
6309
+ const parsed = parse(sources[key]);
6310
+ if (!parsed.ok) problems.push(`${key}: malformed skill (${parsed.error})`);
6311
+ else if (dirs.has(parsed.name)) problems.push(`${key}: duplicate skill name "${parsed.name}"`);
6312
+ else dirs.set(parsed.name, { dir: key.split("/")[2], description: parsed.description });
6313
+ }
6314
+ const prefix = (name) => `${REPO_PREFIX}${dirs.get(name)?.dir}/`;
6315
+ return {
6316
+ async list() {
6317
+ return [...dirs].map(([name, d]) => ({
6318
+ name,
6319
+ description: d.description,
6320
+ scope: "repo",
6321
+ scripts: Object.keys(sources).some((k2) => k2.startsWith(prefix(name)) && SCRIPT_RE2.test(k2.slice(prefix(name).length)))
6322
+ }));
6323
+ },
6324
+ async body(name) {
6325
+ return dirs.has(name) ? sources[`${prefix(name)}SKILL.md`] ?? null : null;
6326
+ },
6327
+ async file(name, rel) {
6328
+ const safe = safeSkillPath(rel);
6329
+ return dirs.has(name) && safe ? sources[`${prefix(name)}${safe}`] ?? null : null;
6330
+ }
6331
+ };
6332
+ }
6333
+ function mergeSkillSources(repo, user) {
6334
+ return {
6335
+ async list() {
6336
+ const r = await repo?.list() ?? [];
6337
+ const names = new Set(r.map((s) => s.name));
6338
+ return [...r, ...(await user?.list() ?? []).filter((s) => !names.has(s.name))];
6339
+ },
6340
+ async body(name) {
6341
+ return await repo?.body(name) ?? null ?? await user?.body(name) ?? null;
6342
+ },
6343
+ async file(name, rel) {
6344
+ if (await repo?.body(name) != null) return repo.file(name, rel);
6345
+ return await user?.file(name, rel) ?? null;
6346
+ }
6347
+ };
6348
+ }
6349
+ async function loadSkills(deps) {
6350
+ const configPath = join3(deps.configDir, "imports.json");
6351
+ try {
6352
+ const loaded = parseImportsConfig(await deps.readFile(configPath).catch((err) => {
6353
+ if (err.code === "ENOENT") return null;
6354
+ throw err;
6355
+ }), configPath);
6356
+ if (loaded.error) return NONE("skills: off (imports.json unreadable)");
6357
+ if (!loaded.config.categories.skills) return NONE("skills: off (imports.json has skills off)");
6358
+ } catch (err) {
6359
+ return NONE(`skills: off (imports.json not read: ${err instanceof Error ? err.message : String(err)})`);
6360
+ }
6361
+ const lines2 = [];
6362
+ const problems = [];
6363
+ let repo = null;
6364
+ try {
6365
+ const sources = await readRepoSources(deps.cwd);
6366
+ if (Object.keys(sources).some((k2) => k2.startsWith(REPO_PREFIX))) {
6367
+ const status = await createNodeTrustStore(deps.configDir).repoStatus(deps.cwd, sources);
6368
+ if (status === "trusted" || status === "session") repo = repoSkillSource(sources, deps.parse, problems);
6369
+ else lines2.push(`skills: repo skills NOT loaded (${status === "never" ? "marked never trust" : "untrusted repo"})`);
6370
+ }
6371
+ } catch (err) {
6372
+ lines2.push(`skills: repo skills NOT loaded (${err instanceof Error ? err.message : String(err)})`);
6373
+ }
6374
+ let user = null;
6375
+ if (deps.client) {
6376
+ const source2 = createProfileSkillSource(deps.client);
6377
+ try {
6378
+ await source2.list();
6379
+ user = source2;
6380
+ } catch (err) {
6381
+ lines2.push(`skills: user skills NOT loaded (${err instanceof Error ? err.message : String(err)})`);
6382
+ }
6383
+ } else lines2.push("skills: user skills NOT loaded (not signed in)");
6384
+ const source = mergeSkillSources(repo, user);
6385
+ const all = await source.list();
6386
+ const shadowed = repo && user ? (await user.list()).filter((u3) => all.some((a) => a.name === u3.name && a.scope === "repo")).map((u3) => u3.name) : [];
6387
+ for (const p of problems) lines2.push(`skills: ${p}`);
6388
+ const section2 = buildSkillsSection(all);
6389
+ lines2.unshift(`skills: ${all.filter((s) => s.scope === "user").length} user, ${all.filter((s) => s.scope === "repo").length} repo${shadowed.length ? `; repo shadows user: ${shadowed.join(", ")}` : ""}${section2.trimmed.length ? `; descriptions trimmed: ${section2.trimmed.join(", ")}` : ""}${section2.omitted.length ? `; NOT listed (over the 4k prompt budget): ${section2.omitted.join(", ")}` : ""}`);
6390
+ if (all.length === 0) return { ...NONE(lines2[0]), statusLines: lines2, source };
6391
+ return { promptSection: section2.text, tools: [SKILL_TOOL], handlers: { skill: createSkillTool(source) }, statusLines: lines2, source };
6392
+ }
6393
+ var NONE, REPO_PREFIX, SCRIPT_RE2;
6394
+ var init_skills_loader = __esm({
6395
+ "src/skills-loader.ts"() {
6396
+ "use strict";
6397
+ init_src();
6398
+ init_import_gate();
6399
+ NONE = (line) => ({ promptSection: "", tools: [], handlers: {}, statusLines: [line], source: null });
6400
+ REPO_PREFIX = ".claude/skills/";
6401
+ SCRIPT_RE2 = /^(scripts\/|.*\.(sh|py|js|mjs|ts|rb|pl)$)/;
6402
+ }
6403
+ });
6404
+
5802
6405
  // src/profile-runtime.ts
5803
- import { join as join2 } from "node:path";
6406
+ import { join as join4 } from "node:path";
5804
6407
  function backendUrl(env3) {
5805
6408
  return env3.SPACECODE_BACKEND_URL?.trim() || env3.SPACECODE_API_URL?.trim() || DEFAULT_BACKEND_URL;
5806
6409
  }
5807
6410
  function fileCache(deps) {
5808
- const dir = join2(deps.env.SPACECODE_CONFIG_DIR?.trim() || join2(deps.homedir(), ".spacecode"));
5809
- const file = join2(dir, "profile-cache.json");
6411
+ const dir = join4(deps.env.SPACECODE_CONFIG_DIR?.trim() || join4(deps.homedir(), ".spacecode"));
6412
+ const file = join4(dir, "profile-cache.json");
5810
6413
  return {
5811
6414
  async read() {
5812
6415
  let raw;
@@ -5852,8 +6455,15 @@ async function prepareProfileUnsafe(deps) {
5852
6455
  const root = await deps.gitRoot?.() ?? null;
5853
6456
  const keys = [.../* @__PURE__ */ new Set([projectKey(deps.cwd), ...root ? [projectKey(root)] : []])];
5854
6457
  const client = createProfileClient({ baseUrl: backendUrl(deps.env), apiKey, fetch: deps.fetch, timeoutMs: 3e3 });
6458
+ const skills = await loadSkills({
6459
+ configDir: deps.env.SPACECODE_CONFIG_DIR?.trim() || join4(deps.homedir(), ".spacecode"),
6460
+ cwd: deps.cwd,
6461
+ readFile: deps.readFile,
6462
+ client,
6463
+ parse: parseSkillFile
6464
+ });
5855
6465
  const result = await loadProfile({ client, cache: fileCache(deps), projectKeys: keys });
5856
- if (!result.profile) return EMPTY([`profile: not loaded (${result.error ?? "unknown error"})`]);
6466
+ if (!result.profile) return { ...EMPTY([`profile: not loaded (${result.error ?? "unknown error"})`, ...skills.statusLines]), promptSection: skills.promptSection, tools: skills.tools, handlers: skills.handlers, skills: skills.source };
5857
6467
  const key = result.profile.projectKey;
5858
6468
  const hasMemory = result.profile.memoryIndex !== null && key !== null;
5859
6469
  const hasSections = result.profile.instructions?.includes("## More instructions (on demand)") ?? false;
@@ -5862,10 +6472,13 @@ async function prepareProfileUnsafe(deps) {
5862
6472
  `profile: v${result.profile.version} ${result.source === "cache" ? "(STALE: served from cache)" : "(live)"}${result.profile.instructions ? ", instructions" : ""}${hasSections ? ", adapted (on-demand sections)" : ""}${hasMemory ? `, memory "${key}"` : ""}${section2.truncated ? ", TRUNCATED to fit the prompt budget" : ""}`
5863
6473
  ];
5864
6474
  if (result.error) lines2.push(`profile warning: ${result.error}`);
6475
+ lines2.push(...skills.statusLines);
5865
6476
  return {
5866
- promptSection: section2.text,
5867
- tools: [...hasMemory ? [MEMORY_READ_TOOL] : [], ...hasSections ? [INSTRUCTIONS_READ_TOOL] : []],
6477
+ promptSection: section2.text + skills.promptSection,
6478
+ skills: skills.source,
6479
+ tools: [...hasMemory ? [MEMORY_READ_TOOL] : [], ...hasSections ? [INSTRUCTIONS_READ_TOOL] : [], ...skills.tools],
5868
6480
  handlers: {
6481
+ ...skills.handlers,
5869
6482
  ...hasMemory ? { memory_read: createMemoryReadTool(client, key) } : {},
5870
6483
  ...hasSections ? { instructions_read: createInstructionsReadTool(client) } : {}
5871
6484
  },
@@ -5878,6 +6491,8 @@ var init_profile_runtime = __esm({
5878
6491
  "use strict";
5879
6492
  init_src();
5880
6493
  init_api_key();
6494
+ init_skills_loader();
6495
+ init_src();
5881
6496
  DEFAULT_BACKEND_URL = "https://backend.spacecode.app";
5882
6497
  EMPTY = (statusLines) => ({ promptSection: "", tools: [], handlers: {}, statusLines });
5883
6498
  }
@@ -6027,6 +6642,13 @@ ${USAGE}`);
6027
6642
  return 1;
6028
6643
  }
6029
6644
  const client = createProfileClient({ baseUrl: backendUrl(deps.env), apiKey, fetch: deps.fetch });
6645
+ const TENANT_ASSET_CAP = 200;
6646
+ let assetCount = 0;
6647
+ try {
6648
+ assetCount = (await client.index())?.assets.length ?? 0;
6649
+ } catch (err) {
6650
+ deps.log(` note: could not read the profile asset count (${err instanceof Error ? err.message : String(err)}); the server still enforces the cap`);
6651
+ }
6030
6652
  let failed = report.errors.length > 0;
6031
6653
  let imported = 0;
6032
6654
  let skipped = 0;
@@ -6049,6 +6671,11 @@ ${USAGE}`);
6049
6671
  deps.log(` unchanged ${a.kind}/${a.name}`);
6050
6672
  continue;
6051
6673
  }
6674
+ if (!existing && assetCount >= TENANT_ASSET_CAP) {
6675
+ failed = true;
6676
+ deps.log(` SKIPPED ${a.kind}/${a.name}: the profile already holds ${assetCount} of ${TENANT_ASSET_CAP} allowed assets`);
6677
+ continue;
6678
+ }
6052
6679
  if (existing && !overwrite) {
6053
6680
  failed = true;
6054
6681
  deps.log(` DIFFERS ${a.kind}/${a.name}: the profile already has a different version; re-run with --overwrite to replace it`);
@@ -6056,6 +6683,7 @@ ${USAGE}`);
6056
6683
  }
6057
6684
  await client.put(a.kind, a.name, { content: a.content, description: a.description, baseVersion: existing?.version ?? 0 });
6058
6685
  imported++;
6686
+ if (!existing) assetCount++;
6059
6687
  deps.log(` imported ${a.kind}/${a.name}`);
6060
6688
  } catch (err) {
6061
6689
  failed = true;
@@ -39350,12 +39978,12 @@ var require_utils = __commonJS({
39350
39978
  return str;
39351
39979
  }
39352
39980
  var codeCache = {};
39353
- function addToCodeCache(name, on, off) {
39981
+ function addToCodeCache(name, on, off2) {
39354
39982
  on = "\x1B[" + on + "m";
39355
- off = "\x1B[" + off + "m";
39983
+ off2 = "\x1B[" + off2 + "m";
39356
39984
  codeCache[on] = { set: name, to: true };
39357
- codeCache[off] = { set: name, to: false };
39358
- codeCache[name] = { on, off };
39985
+ codeCache[off2] = { set: name, to: false };
39986
+ codeCache[name] = { on, off: off2 };
39359
39987
  }
39360
39988
  addToCodeCache("bold", 1, 22);
39361
39989
  addToCodeCache("italics", 3, 23);
@@ -41237,7 +41865,7 @@ var require_core = __commonJS({
41237
41865
  return match && match.index === 0;
41238
41866
  }
41239
41867
  var BACKREF_RE = /\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;
41240
- function join7(regexps, separator = "|") {
41868
+ function join9(regexps, separator = "|") {
41241
41869
  let numCaptures = 0;
41242
41870
  return regexps.map((regex2) => {
41243
41871
  numCaptures += 1;
@@ -41541,7 +42169,7 @@ var require_core = __commonJS({
41541
42169
  this.exec = () => null;
41542
42170
  }
41543
42171
  const terminators = this.regexes.map((el) => el[1]);
41544
- this.matcherRe = langRe(join7(terminators), true);
42172
+ this.matcherRe = langRe(join9(terminators), true);
41545
42173
  this.lastIndex = 0;
41546
42174
  }
41547
42175
  /** @param {string} s */
@@ -99047,7 +99675,7 @@ var init_credential_filter = __esm({
99047
99675
  });
99048
99676
 
99049
99677
  // src/session/session-store.ts
99050
- import { chmod, mkdir, open, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
99678
+ import { chmod as chmod2, mkdir as mkdir2, open, readdir as readdir2, readFile as readFile2, rename as rename2, rm, stat, writeFile as writeFile2 } from "node:fs/promises";
99051
99679
  import { randomBytes } from "node:crypto";
99052
99680
  import path3 from "node:path";
99053
99681
  function redactCredentials(value) {
@@ -99136,11 +99764,11 @@ var init_session_store = __esm({
99136
99764
  }
99137
99765
  const tmp = `${file}.${process.pid}.tmp`;
99138
99766
  try {
99139
- await mkdir(this.dir, { recursive: true, mode: 448 });
99140
- await chmod(this.dir, 448);
99141
- await writeFile(tmp, body, { mode: 384 });
99142
- await chmod(tmp, 384);
99143
- await rename(tmp, file);
99767
+ await mkdir2(this.dir, { recursive: true, mode: 448 });
99768
+ await chmod2(this.dir, 448);
99769
+ await writeFile2(tmp, body, { mode: 384 });
99770
+ await chmod2(tmp, 384);
99771
+ await rename2(tmp, file);
99144
99772
  } catch (err) {
99145
99773
  throw new SessionError(file, `Could not save session: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
99146
99774
  }
@@ -99153,7 +99781,7 @@ var init_session_store = __esm({
99153
99781
  if (size > MAX_SESSION_BYTES) {
99154
99782
  throw new SessionError(file, `Session is ${size} bytes, over the ${MAX_SESSION_BYTES}-byte cap; not loaded`);
99155
99783
  }
99156
- text = await readFile(file, "utf8");
99784
+ text = await readFile2(file, "utf8");
99157
99785
  } catch (err) {
99158
99786
  if (err instanceof SessionError) throw err;
99159
99787
  throw new SessionError(file, `Could not read session: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
@@ -99166,7 +99794,7 @@ var init_session_store = __esm({
99166
99794
  async list() {
99167
99795
  let names;
99168
99796
  try {
99169
- names = await readdir(this.dir);
99797
+ names = await readdir2(this.dir);
99170
99798
  } catch (err) {
99171
99799
  if (err.code === "ENOENT") return { sessions: [], errors: [] };
99172
99800
  throw new SessionError(this.dir, `Could not list sessions: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
@@ -100343,6 +100971,8 @@ var init_slash_commands = __esm({
100343
100971
  { name: "compact", description: "Summarize the conversation to free up context" },
100344
100972
  { name: "model", description: "List gateway models; /model <n|id> switches for this session" },
100345
100973
  { name: "turns", description: "Browse earlier turns and open their steps (shortcut: ctrl+o)" },
100974
+ { name: "commands", description: "List the slash commands imported from Claude Code" },
100975
+ { name: "skills", description: "List imported skills; /skills show <name> [file], /skills delete <name>" },
100346
100976
  { name: "tools", description: "List the tools available to the agent" },
100347
100977
  { name: "exit", description: "Quit the REPL (alias: /quit)" },
100348
100978
  { name: "quit", description: "Quit the REPL" }
@@ -100351,15 +100981,15 @@ var init_slash_commands = __esm({
100351
100981
  });
100352
100982
 
100353
100983
  // src/components/PromptEditor.tsx
100354
- function validateSubmission(text) {
100984
+ function validateSubmission(text, extra = []) {
100355
100985
  const slash = parseSlashCommand(text);
100356
- if (slash && !isKnownSlashCommand(slash.name)) return `Unknown command: /${slash.name}. Type /help for a list.`;
100986
+ if (slash && !isKnownSlashCommand(slash.name) && !extra.some((c) => c.name === slash.name)) return `Unknown command: /${slash.name}. Type /help for a list.`;
100357
100987
  return null;
100358
100988
  }
100359
100989
  function withCursor(row, col) {
100360
100990
  return `${row.slice(0, col)}\x1B[7m${row[col] ?? " "}\x1B[27m${row.slice(col + 1)}`;
100361
100991
  }
100362
- function PromptEditor({ onSubmit, disabled, history, listFiles: listFiles2, onTab, locked = false, promptGlyph: promptGlyph2 = ">", onEmptyEnter, unicode = true, fullWidth = false }) {
100992
+ function PromptEditor({ onSubmit, disabled, history, listFiles: listFiles3, onTab, locked = false, promptGlyph: promptGlyph2 = ">", onEmptyEnter, unicode = true, fullWidth = false, extraCommands = [] }) {
100363
100993
  const { exit } = use_app_default();
100364
100994
  const { columns } = useTerminalSize();
100365
100995
  const { isRawModeSupported } = use_stdin_default();
@@ -100395,15 +101025,15 @@ function PromptEditor({ onSubmit, disabled, history, listFiles: listFiles2, onTa
100395
101025
  const mention = shell ? null : activeMention(buf.text, buf.cursor);
100396
101026
  const mentionOpen = mention !== null;
100397
101027
  (0, import_react29.useEffect)(() => {
100398
- if (!mentionOpen || !listFiles2 || filesRef.current !== null || loadingFiles.current) return;
101028
+ if (!mentionOpen || !listFiles3 || filesRef.current !== null || loadingFiles.current) return;
100399
101029
  loadingFiles.current = true;
100400
- listFiles2().then((f) => {
101030
+ listFiles3().then((f) => {
100401
101031
  filesRef.current = f;
100402
101032
  setFiles(f);
100403
101033
  }).catch((err) => fail("Could not list files", err)).finally(() => {
100404
101034
  loadingFiles.current = false;
100405
101035
  });
100406
- }, [mentionOpen, listFiles2, fail]);
101036
+ }, [mentionOpen, listFiles3, fail]);
100407
101037
  const chooseSel = (n) => {
100408
101038
  selRef.current = n;
100409
101039
  setSel(n);
@@ -100462,7 +101092,7 @@ function PromptEditor({ onSubmit, disabled, history, listFiles: listFiles2, onTa
100462
101092
  else update(applyEdit(cur, { type: "delete" }));
100463
101093
  return;
100464
101094
  case "tab": {
100465
- const completed = completeSlash(cur.text, slashMatches(cur.text, SLASH_COMMANDS));
101095
+ const completed = completeSlash(cur.text, slashMatches(cur.text, [...SLASH_COMMANDS, ...extraCommands]));
100466
101096
  if (completed) update(createBuffer(completed));
100467
101097
  else onTab?.();
100468
101098
  return;
@@ -100495,7 +101125,7 @@ function PromptEditor({ onSubmit, disabled, history, listFiles: listFiles2, onTa
100495
101125
  return;
100496
101126
  }
100497
101127
  const text = inShell ? `!${typed}` : typed;
100498
- const problem = inShell ? null : validateSubmission(text);
101128
+ const problem = inShell ? null : validateSubmission(text, extraCommands);
100499
101129
  if (problem) {
100500
101130
  setHint(problem);
100501
101131
  return;
@@ -100515,7 +101145,7 @@ function PromptEditor({ onSubmit, disabled, history, listFiles: listFiles2, onTa
100515
101145
  }
100516
101146
  const rows = lines(buf);
100517
101147
  const { line, col } = lineCol(buf);
100518
- const matches = shell ? [] : slashMatches(buf.text, SLASH_COMMANDS);
101148
+ const matches = shell ? [] : slashMatches(buf.text, [...SLASH_COMMANDS, ...extraCommands]);
100519
101149
  const picks = mention && files ? rankPaths(files, mention.query, PICKER_ROWS) : [];
100520
101150
  const pickAt = Math.min(sel, Math.max(picks.length - 1, 0));
100521
101151
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(Box_default, { flexDirection: "column", children: [
@@ -100638,24 +101268,24 @@ var init_history_store = __esm({
100638
101268
  });
100639
101269
 
100640
101270
  // src/editor/history-file-io.ts
100641
- import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
101271
+ import { chmod as chmod3, mkdir as mkdir3, readFile as readFile3, rename as rename3, writeFile as writeFile3 } from "node:fs/promises";
100642
101272
  import path4 from "node:path";
100643
101273
  function createFileHistoryIO(file) {
100644
101274
  return {
100645
101275
  async read() {
100646
101276
  try {
100647
- return await readFile2(file, "utf8");
101277
+ return await readFile3(file, "utf8");
100648
101278
  } catch (err) {
100649
101279
  if (err.code === "ENOENT") return null;
100650
101280
  throw err;
100651
101281
  }
100652
101282
  },
100653
101283
  async write(content) {
100654
- await mkdir2(path4.dirname(file), { recursive: true, mode: 448 });
101284
+ await mkdir3(path4.dirname(file), { recursive: true, mode: 448 });
100655
101285
  const tmp = `${file}.${process.pid}.tmp`;
100656
- await writeFile2(tmp, content, { mode: 384 });
100657
- await chmod2(tmp, 384);
100658
- await rename2(tmp, file);
101286
+ await writeFile3(tmp, content, { mode: 384 });
101287
+ await chmod3(tmp, 384);
101288
+ await rename3(tmp, file);
100659
101289
  }
100660
101290
  };
100661
101291
  }
@@ -100864,7 +101494,7 @@ var init_shell_turn = __esm({
100864
101494
  });
100865
101495
 
100866
101496
  // src/editor/file-list.ts
100867
- async function listFiles(readDir, opts = {}) {
101497
+ async function listFiles2(readDir, opts = {}) {
100868
101498
  const max = opts.maxFiles ?? DEFAULT_MAX_LISTED_FILES;
100869
101499
  const files = [];
100870
101500
  const queue = [""];
@@ -101091,18 +101721,18 @@ var init_mention_resolver = __esm({
101091
101721
  });
101092
101722
 
101093
101723
  // src/editor/node-mention-fs.ts
101094
- import { open as open2, realpath, stat as stat2 } from "node:fs/promises";
101724
+ import { open as open2, realpath as realpath2, stat as stat2 } from "node:fs/promises";
101095
101725
  import path6 from "node:path";
101096
101726
  function isInside(root, target) {
101097
101727
  return target === root || target.startsWith(root.endsWith(path6.sep) ? root : root + path6.sep);
101098
101728
  }
101099
101729
  function createNodeMentionFS(cwd2) {
101100
101730
  let rootPromise;
101101
- const root = () => rootPromise ??= realpath(cwd2);
101731
+ const root = () => rootPromise ??= realpath2(cwd2);
101102
101732
  async function confined(abs) {
101103
101733
  let real;
101104
101734
  try {
101105
- real = await realpath(abs);
101735
+ real = await realpath2(abs);
101106
101736
  } catch (err) {
101107
101737
  if (err.code === "ENOENT") return null;
101108
101738
  throw err;
@@ -101138,11 +101768,11 @@ var init_node_mention_fs = __esm({
101138
101768
  });
101139
101769
 
101140
101770
  // src/editor/node-read-dir.ts
101141
- import { readdir as readdir2 } from "node:fs/promises";
101771
+ import { readdir as readdir3 } from "node:fs/promises";
101142
101772
  import path7 from "node:path";
101143
101773
  function createNodeReadDir(cwd2) {
101144
101774
  return async (rel) => {
101145
- const entries = await readdir2(path7.join(cwd2, rel), { withFileTypes: true });
101775
+ const entries = await readdir3(path7.join(cwd2, rel), { withFileTypes: true });
101146
101776
  return entries.map((e) => ({ name: e.name, isFile: e.isFile(), isDirectory: e.isDirectory() }));
101147
101777
  };
101148
101778
  }
@@ -101153,11 +101783,11 @@ var init_node_read_dir = __esm({
101153
101783
  });
101154
101784
 
101155
101785
  // src/editor/node-mentions.ts
101156
- import { readFile as readFile3 } from "node:fs/promises";
101786
+ import { readFile as readFile4 } from "node:fs/promises";
101157
101787
  import path8 from "node:path";
101158
101788
  async function readGitignore(cwd2) {
101159
101789
  try {
101160
- return createGitignoreMatcher(await readFile3(path8.join(cwd2, ".gitignore"), "utf8"));
101790
+ return createGitignoreMatcher(await readFile4(path8.join(cwd2, ".gitignore"), "utf8"));
101161
101791
  } catch (err) {
101162
101792
  if (err.code === "ENOENT") return () => false;
101163
101793
  throw err;
@@ -101171,7 +101801,7 @@ function createNodeMentions(cwd2) {
101171
101801
  return formatMentionPrompt(text, result);
101172
101802
  },
101173
101803
  async list() {
101174
- return listFiles(createNodeReadDir(cwd2), { ignored: await readGitignore(cwd2) });
101804
+ return listFiles2(createNodeReadDir(cwd2), { ignored: await readGitignore(cwd2) });
101175
101805
  }
101176
101806
  };
101177
101807
  }
@@ -101187,75 +101817,48 @@ var init_node_mentions = __esm({
101187
101817
  }
101188
101818
  });
101189
101819
 
101190
- // src/import-gate.ts
101191
- import { createHash, randomUUID } from "node:crypto";
101192
- import { chmod as chmod3, mkdir as mkdir3, readFile as readFile4, realpath as realpath2, rename as rename3, writeFile as writeFile3 } from "node:fs/promises";
101193
- import { dirname as dirname3, join as join4 } from "node:path";
101194
- async function readOrNull(path12) {
101195
- try {
101196
- return await readFile4(path12, "utf8");
101197
- } catch (err) {
101198
- if (err.code === "ENOENT") return null;
101199
- throw err;
101200
- }
101201
- }
101202
- async function writeTextAtomic(path12, text) {
101203
- await mkdir3(dirname3(path12), { recursive: true });
101204
- const tmp = `${path12}.${process.pid}.${randomUUID()}.tmp`;
101205
- await writeFile3(tmp, text, { mode: 384 });
101206
- await chmod3(tmp, 384);
101207
- await rename3(tmp, path12);
101208
- }
101209
- function nodeTrustFs() {
101210
- return { readText: readOrNull, writeTextAtomic, realPath: (p) => realpath2(p) };
101211
- }
101212
- function createNodeTrustStore(configDir) {
101213
- return new TrustStore({ fs: nodeTrustFs(), path: join4(configDir, "trust.json"), hash: sha256 });
101214
- }
101215
- async function importGateStatus(deps) {
101216
- const lines2 = [];
101217
- const configPath = join4(deps.configDir, "imports.json");
101218
- try {
101219
- const loaded = parseImportsConfig(await readOrNull(configPath), configPath);
101220
- if (loaded.error) lines2.push(`imports: ${loaded.error} (every category off)`);
101221
- else {
101222
- const on = IMPORT_CATEGORIES.filter((c) => loaded.config.categories[c]);
101223
- lines2.push(on.length ? `imports: on for ${on.join(", ")}` : "imports: every category off (default)");
101224
- }
101225
- } catch (err) {
101226
- lines2.push(`imports: not read (${err instanceof Error ? err.message : String(err)}); every category off`);
101227
- }
101228
- try {
101229
- const files = {};
101230
- for (const name of REPO_SOURCE_FILES) {
101231
- const text = await readOrNull(join4(deps.cwd, name));
101232
- if (text !== null) files[name] = text;
101233
- }
101234
- const found = Object.keys(files);
101235
- if (found.length === 0) return lines2;
101236
- const status = await createNodeTrustStore(deps.configDir).repoStatus(deps.cwd, files);
101237
- const names = found.join(", ");
101238
- if (status === "trusted" || status === "session") lines2.push(`repo sources: ${names} (trusted${status === "session" ? " for this session" : ""})`);
101239
- else if (status === "never") lines2.push(`repo sources: ${names} NOT loaded (marked never trust)`);
101240
- else lines2.push(`repo sources: ${names} NOT loaded (untrusted${deps.interactive ? "" : ", non-interactive run"})`);
101241
- } catch (err) {
101242
- lines2.push(`repo sources: NOT loaded (trust check failed: ${err instanceof Error ? err.message : String(err)})`);
101820
+ // src/skills-command.ts
101821
+ async function runSkillsCommand(args, deps) {
101822
+ const [sub, name, file] = args.trim().split(/\s+/);
101823
+ if (!deps.source) return `Skills are not available.
101824
+ ${deps.statusLines.map((l) => ` ${l}`).join("\n")}`;
101825
+ const all = await deps.source.list();
101826
+ if (!sub) {
101827
+ if (all.length === 0) return "No imported skills.";
101828
+ return `Imported skills:
101829
+ ${all.map((s) => ` ${s.name} (${s.scope}${s.scripts ? ", ships scripts: not runnable until permitted" : ""}) ${s.description}`).join("\n")}
101830
+
101831
+ Skills are loaded on demand with the skill tool; bodies are not in the prompt. /skills show <name> [file], /skills delete <name>.`;
101832
+ }
101833
+ if (sub === "show") {
101834
+ if (!name) return "Usage: /skills show <name> [file]";
101835
+ const text = file ? await deps.source.file(name, file) : await deps.source.body(name);
101836
+ return text ?? `No ${file ? `file "${file}" in skill` : "skill named"} "${name}".`;
101837
+ }
101838
+ if (sub === "delete") {
101839
+ if (!name) return "Usage: /skills delete <name>";
101840
+ const skill = all.find((s) => s.name === name);
101841
+ if (!skill) return `No skill named "${name}".`;
101842
+ if (skill.scope === "repo") return `"${name}" is a repo skill (${".claude/skills"}); delete it from the repo, not the profile.`;
101843
+ if (!deps.client) return "Not signed in: cannot delete from the profile.";
101844
+ const assets = ((await deps.client.index())?.assets ?? []).filter(
101845
+ (a) => a.kind === "skill" && (a.name === name || a.name.startsWith(`${name}.`) && a.description.startsWith(SIBLING_PREFIX))
101846
+ );
101847
+ for (const a of assets) await deps.client.delete("skill", a.name);
101848
+ return `Deleted skill "${name}" and ${assets.length - 1} file(s) from the profile. Re-running import-claude would import it again.`;
101243
101849
  }
101244
- return lines2;
101850
+ return "Usage: /skills | /skills show <name> [file] | /skills delete <name>";
101245
101851
  }
101246
- var REPO_SOURCE_FILES, sha256;
101247
- var init_import_gate = __esm({
101248
- "src/import-gate.ts"() {
101852
+ var init_skills_command = __esm({
101853
+ "src/skills-command.ts"() {
101249
101854
  "use strict";
101250
101855
  init_src();
101251
- REPO_SOURCE_FILES = [".mcp.json", ".claude/settings.json", ".claude/settings.local.json", "CLAUDE.local.md"];
101252
- sha256 = (text) => createHash("sha256").update(text).digest("hex");
101253
101856
  }
101254
101857
  });
101255
101858
 
101256
101859
  // src/permission-rules-loader.ts
101257
101860
  import { readFile as readFile5 } from "node:fs/promises";
101258
- import { join as join5 } from "node:path";
101861
+ import { join as join6 } from "node:path";
101259
101862
  async function readOrNull2(path12) {
101260
101863
  try {
101261
101864
  return await readFile5(path12, "utf8");
@@ -101266,7 +101869,7 @@ async function readOrNull2(path12) {
101266
101869
  }
101267
101870
  async function loadPermissionRules(deps) {
101268
101871
  const none = (line) => ({ rules: void 0, defaultMode: void 0, statusLines: [line] });
101269
- const configPath = join5(deps.configDir, "imports.json");
101872
+ const configPath = join6(deps.configDir, "imports.json");
101270
101873
  try {
101271
101874
  const loaded = parseImportsConfig(await readOrNull2(configPath), configPath);
101272
101875
  if (loaded.error) return none("permission rules: off (imports.json unreadable)");
@@ -101275,17 +101878,13 @@ async function loadPermissionRules(deps) {
101275
101878
  return none(`permission rules: off (imports.json not read: ${err instanceof Error ? err.message : String(err)})`);
101276
101879
  }
101277
101880
  const lines2 = [];
101278
- const files = [{ scope: "user", path: join5(deps.homeDir, ".claude", "settings.json") }];
101881
+ const files = [{ scope: "user", path: join6(deps.homeDir, ".claude", "settings.json") }];
101279
101882
  try {
101280
- const sources2 = {};
101281
- for (const name of REPO_SOURCE_FILES) {
101282
- const text = await readOrNull2(join5(deps.cwd, name));
101283
- if (text !== null) sources2[name] = text;
101284
- }
101883
+ const sources2 = await readRepoSources(deps.cwd);
101285
101884
  if (sources2[".claude/settings.json"] !== void 0 || sources2[".claude/settings.local.json"] !== void 0) {
101286
101885
  const status = await createNodeTrustStore(deps.configDir).repoStatus(deps.cwd, sources2);
101287
101886
  if (status === "trusted" || status === "session") {
101288
- files.push({ scope: "repo", path: join5(deps.cwd, ".claude", "settings.json") }, { scope: "local", path: join5(deps.cwd, ".claude", "settings.local.json") });
101887
+ files.push({ scope: "repo", path: join6(deps.cwd, ".claude", "settings.json") }, { scope: "local", path: join6(deps.cwd, ".claude", "settings.local.json") });
101289
101888
  } else lines2.push(`permission rules: repo settings NOT loaded (${status === "never" ? "marked never trust" : "untrusted repo"})`);
101290
101889
  }
101291
101890
  } catch (err) {
@@ -101326,10 +101925,98 @@ var init_permission_rules_loader = __esm({
101326
101925
  }
101327
101926
  });
101328
101927
 
101928
+ // src/command-loader.ts
101929
+ import { join as join7 } from "node:path";
101930
+ async function userCommands(deps, lines2) {
101931
+ try {
101932
+ const apiKey = await resolveApiKey({ env: deps.env, readFile: deps.readFile, homedir: deps.homedir });
101933
+ if (!apiKey) {
101934
+ lines2.push("commands: user commands NOT loaded (not signed in)");
101935
+ return [];
101936
+ }
101937
+ const client = createProfileClient({ baseUrl: backendUrl(deps.env), apiKey, fetch: deps.fetch, timeoutMs: 3e3 });
101938
+ const index = await client.index();
101939
+ const entries = (index?.assets ?? []).filter((a) => a.kind === "command");
101940
+ const defs = [];
101941
+ await Promise.all(
101942
+ entries.map(async (e) => {
101943
+ try {
101944
+ const asset = await client.get("command", e.name);
101945
+ if (asset) defs.push({ name: e.name, ...parseCommandFile(asset.content) });
101946
+ } catch (err) {
101947
+ lines2.push(`commands: ${e.name} NOT loaded (${err instanceof Error ? err.message : String(err)})`);
101948
+ }
101949
+ })
101950
+ );
101951
+ return defs;
101952
+ } catch (err) {
101953
+ lines2.push(`commands: user commands NOT loaded (${err instanceof Error ? err.message : String(err)})`);
101954
+ return [];
101955
+ }
101956
+ }
101957
+ async function repoCommands(deps, lines2) {
101958
+ try {
101959
+ const sources = await readRepoSources(deps.cwd);
101960
+ const names = Object.keys(sources).filter((k2) => k2.startsWith(".claude/commands/"));
101961
+ if (names.length === 0) return [];
101962
+ const status = await createNodeTrustStore(deps.configDir).repoStatus(deps.cwd, sources);
101963
+ if (status !== "trusted" && status !== "session") {
101964
+ lines2.push(`commands: ${names.length} repo command(s) NOT loaded (${status === "never" ? "marked never trust" : "untrusted repo"})`);
101965
+ return [];
101966
+ }
101967
+ const defs = [];
101968
+ for (const key of names) {
101969
+ const text = sources[key];
101970
+ const name = key.slice(".claude/commands/".length, -3);
101971
+ if (Buffer.byteLength(text, "utf8") > MAX_ASSET_BYTES) {
101972
+ lines2.push(`commands: repo command ${name} skipped (over ${MAX_ASSET_BYTES} bytes)`);
101973
+ continue;
101974
+ }
101975
+ defs.push({ name, ...parseCommandFile(text) });
101976
+ }
101977
+ return defs;
101978
+ } catch (err) {
101979
+ lines2.push(`commands: repo commands NOT loaded (${err instanceof Error ? err.message : String(err)})`);
101980
+ return [];
101981
+ }
101982
+ }
101983
+ async function loadCommands(deps) {
101984
+ const configPath = join7(deps.configDir, "imports.json");
101985
+ try {
101986
+ const loaded = parseImportsConfig(await deps.readFile(configPath).catch((err) => {
101987
+ if (err.code === "ENOENT") return null;
101988
+ throw err;
101989
+ }), configPath);
101990
+ if (loaded.error) return off("commands: off (imports.json unreadable)");
101991
+ if (!loaded.config.categories.commands) return off("commands: off (imports.json has commands off)");
101992
+ } catch (err) {
101993
+ return off(`commands: off (imports.json not read: ${err instanceof Error ? err.message : String(err)})`);
101994
+ }
101995
+ const lines2 = [];
101996
+ const [user, repo] = await Promise.all([userCommands(deps, lines2), repoCommands(deps, lines2)]);
101997
+ const reg = buildCommandRegistry({ builtins: deps.builtins, user, repo });
101998
+ const nUser = reg.commands.filter((c) => c.scope === "user").length;
101999
+ const nRepo = reg.commands.length - nUser;
102000
+ lines2.unshift(`commands: ${nUser} user, ${nRepo} repo${reg.shadowed.length ? `; repo shadows user: ${reg.shadowed.map((n) => "/" + n).join(", ")}` : ""}`);
102001
+ for (const s of reg.skipped) lines2.push(`commands: skipped [${s.scope}] /${s.name} (${s.reason})`);
102002
+ return { commands: reg.commands, statusLines: lines2 };
102003
+ }
102004
+ var off;
102005
+ var init_command_loader = __esm({
102006
+ "src/command-loader.ts"() {
102007
+ "use strict";
102008
+ init_src();
102009
+ init_api_key();
102010
+ init_import_gate();
102011
+ init_profile_runtime();
102012
+ off = (line) => ({ commands: [], statusLines: [line] });
102013
+ }
102014
+ });
102015
+
101329
102016
  // src/useReplController.ts
101330
102017
  import { readFile as readFile6, writeFile as writeFile4, mkdir as mkdir4 } from "node:fs/promises";
101331
102018
  import { homedir as homedir2 } from "node:os";
101332
- import { join as join6 } from "node:path";
102019
+ import { join as join8 } from "node:path";
101333
102020
  import { execFile as execFile3 } from "node:child_process";
101334
102021
  function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mentionsOpt, sessions, resumeLatest: resumeLatest2, onViewTurns, onViewResume, isEscOwned }) {
101335
102022
  const { exit } = use_app_default();
@@ -101372,6 +102059,27 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
101372
102059
  const pendingRef = (0, import_react30.useRef)(null);
101373
102060
  const toolsetRef = (0, import_react30.useRef)(null);
101374
102061
  const rulesRef = (0, import_react30.useRef)(void 0);
102062
+ const [commandsLoaded, setCommandsLoaded] = (0, import_react30.useState)({ commands: [], statusLines: [] });
102063
+ (0, import_react30.useEffect)(() => {
102064
+ let live = true;
102065
+ loadCommands({
102066
+ configDir: process.env.SPACECODE_CONFIG_DIR?.trim() || join8(homedir2(), ".spacecode"),
102067
+ cwd: process.cwd(),
102068
+ env: process.env,
102069
+ readFile: (p) => readFile6(p, "utf-8"),
102070
+ homedir: homedir2,
102071
+ fetch,
102072
+ builtins: SLASH_COMMANDS.map((c) => c.name)
102073
+ }).then((r) => {
102074
+ if (live) setCommandsLoaded(r);
102075
+ }, (err) => {
102076
+ if (live) setCommandsLoaded({ commands: [], statusLines: [`commands: NOT loaded (${err instanceof Error ? err.message : String(err)})`] });
102077
+ });
102078
+ return () => {
102079
+ live = false;
102080
+ };
102081
+ }, []);
102082
+ const commandEntries = (0, import_react30.useMemo)(() => commandsLoaded.commands.map((c) => ({ name: c.name, description: c.description })), [commandsLoaded]);
101375
102083
  (0, import_react30.useEffect)(() => {
101376
102084
  sessionTokensRef.current = sessionTokens;
101377
102085
  }, [sessionTokens]);
@@ -101494,8 +102202,15 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
101494
102202
  }
101495
102203
  })();
101496
102204
  }, [resumeLatest2, sessions, projectRoot, notice, applySession]);
101497
- const submit = (0, import_react30.useCallback)(async (prompt) => {
101498
- const shellCommand = shellCommandOf(prompt);
102205
+ const submit = (0, import_react30.useCallback)(async (rawPrompt) => {
102206
+ const typed = parseSlashCommand(rawPrompt);
102207
+ const custom = typed && !isKnownSlashCommand(typed.name) ? commandsLoaded.commands.find((c) => c.name === typed.name) : void 0;
102208
+ const prompt = custom ? expandCommand(custom.body, typed.args) : rawPrompt;
102209
+ if (custom && prompt.trim() === "") {
102210
+ notice(`/${custom.name} expands to an empty prompt.`);
102211
+ return;
102212
+ }
102213
+ const shellCommand = custom ? null : shellCommandOf(prompt);
101499
102214
  if (shellCommand !== null) {
101500
102215
  if (shellCommand === "") {
101501
102216
  notice("Type a command after !");
@@ -101529,7 +102244,7 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
101529
102244
  }
101530
102245
  return;
101531
102246
  }
101532
- const slash = parseSlashCommand(prompt);
102247
+ const slash = custom ? null : parseSlashCommand(prompt);
101533
102248
  if (slash && isKnownSlashCommand(slash.name)) {
101534
102249
  switch (slash.name) {
101535
102250
  case "exit":
@@ -101567,7 +102282,7 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
101567
102282
  maxContextTokens: DEFAULT_MAX_CONTEXT_TOKENS,
101568
102283
  tokens: sessionTokensRef.current,
101569
102284
  costUsd: estimateCostUsd(sessionTokensRef.current.input, sessionTokensRef.current.output),
101570
- profileLines: profileRef.current?.statusLines ?? ["profile: not loaded yet (loads on your first message)"]
102285
+ profileLines: [...profileRef.current?.statusLines ?? ["profile: not loaded yet (loads on your first message)"], ...commandsLoaded.statusLines]
101571
102286
  }));
101572
102287
  return;
101573
102288
  case "resume": {
@@ -101672,6 +102387,20 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
101672
102387
  if (onViewTurns) onViewTurns();
101673
102388
  else notice("The turn viewer is not available here.");
101674
102389
  return;
102390
+ case "commands": {
102391
+ const list2 = commandsLoaded.commands;
102392
+ notice(list2.length === 0 ? `No imported commands.
102393
+ ${commandsLoaded.statusLines.map((l) => ` ${l}`).join("\n")}`.trim() : `Imported commands:
102394
+ ${list2.map((c) => ` /${c.name} (${c.scope})${c.description ? ` ${c.description}` : ""}`).join("\n")}`);
102395
+ return;
102396
+ }
102397
+ case "skills": {
102398
+ const state = profileRef.current;
102399
+ const apiKey2 = await resolveApiKey({ env: process.env, readFile: (p) => readFile6(p, "utf-8"), homedir: homedir2 }).catch(() => null);
102400
+ const client = apiKey2 ? createProfileClient({ baseUrl: backendUrl(process.env), apiKey: apiKey2, fetch }) : null;
102401
+ notice(await runSkillsCommand(slash.args, { source: state?.skills ?? null, client, statusLines: state?.statusLines.filter((l) => l.startsWith("skills")) ?? ["skills: not loaded yet (loads on your first message)"] }));
102402
+ return;
102403
+ }
101675
102404
  case "tools": {
101676
102405
  const tools = toolsetRef.current?.tools ?? createLocalToolset({ cwd: process.cwd(), permissions, onTodos: setTodos }).tools;
101677
102406
  notice(`Tools:
@@ -101734,7 +102463,7 @@ ${tools.map((t) => ` ${t.name}`).join("\n")}`);
101734
102463
  cwd: process.cwd(),
101735
102464
  gitRoot: gitTopLevel,
101736
102465
  importGate: async () => {
101737
- const configDir = process.env.SPACECODE_CONFIG_DIR?.trim() || join6(homedir2(), ".spacecode");
102466
+ const configDir = process.env.SPACECODE_CONFIG_DIR?.trim() || join8(homedir2(), ".spacecode");
101738
102467
  const gate = await importGateStatus({ configDir, cwd: process.cwd(), interactive: Boolean(process.stdin.isTTY) });
101739
102468
  const loaded = await loadPermissionRules({ configDir, homeDir: homedir2(), cwd: process.cwd() });
101740
102469
  rulesRef.current = loaded.rules;
@@ -101806,7 +102535,7 @@ ${tools.map((t) => ` ${t.name}`).join("\n")}`);
101806
102535
  abortRef.current = null;
101807
102536
  setIsRunning(false);
101808
102537
  }
101809
- }, [repoName2, history, exit, permissions, notice, refuseAllConfirms, commitTurn, mentions, createToolset, sessions, projectRoot, persist, applySession, model, onViewTurns, onViewResume, setTodos]);
102538
+ }, [commandsLoaded, repoName2, history, exit, permissions, notice, refuseAllConfirms, commitTurn, mentions, createToolset, sessions, projectRoot, persist, applySession, model, onViewTurns, onViewResume, setTodos]);
101810
102539
  const approxTokens = (0, import_react30.useMemo)(
101811
102540
  () => dropDiscardedEvents(events).reduce((n, e) => n + (e.type === "text" ? estimateTokens(e.content) : e.type === "text_delta" ? estimateTokens(e.text) : 0), 0),
101812
102541
  [events]
@@ -101845,7 +102574,7 @@ ${tools.map((t) => ` ${t.name}`).join("\n")}`);
101845
102574
  }),
101846
102575
  [sessions, applySession]
101847
102576
  );
101848
- return { turns, events, isRunning, livePrompt, turnTokens, notices, pending, mode, todos, stats, submit, retry, mentions, replayed, resume };
102577
+ return { turns, events, isRunning, livePrompt, turnTokens, notices, pending, mode, todos, stats, submit, retry, mentions, replayed, resume, commandEntries };
101849
102578
  }
101850
102579
  var import_react30, gitTopLevel, apiUrl;
101851
102580
  var init_useReplController = __esm({
@@ -101867,8 +102596,11 @@ var init_useReplController = __esm({
101867
102596
  init_node_mentions();
101868
102597
  init_api_key();
101869
102598
  init_profile_runtime();
102599
+ init_skills_command();
102600
+ init_src();
101870
102601
  init_import_gate();
101871
102602
  init_permission_rules_loader();
102603
+ init_command_loader();
101872
102604
  init_slash_commands();
101873
102605
  init_useEventAccumulator();
101874
102606
  gitTopLevel = () => new Promise((resolve2) => {
@@ -102169,7 +102901,7 @@ function App2({ repoName: repoName2, onboardingMode: onboardingMode2, launchCoun
102169
102901
  const { isRawModeSupported } = use_stdin_default();
102170
102902
  const [resumeFound, setResumeFound] = (0, import_react34.useState)(null);
102171
102903
  const openResume = (0, import_react34.useCallback)((found) => setResumeFound(found), []);
102172
- const { turns, events, isRunning, livePrompt, notices, pending, mode, todos, stats, submit, retry, mentions, replayed, resume } = useReplController({ repoName: repoName2, yolo: yolo2, sessions: sessionStore, resumeLatest: resumeLatest2, onViewTurns: openViewer, onViewResume: isRawModeSupported && interactivePicker ? openResume : void 0, isEscOwned });
102904
+ const { turns, events, isRunning, livePrompt, notices, pending, mode, todos, stats, submit, retry, mentions, replayed, resume, commandEntries } = useReplController({ repoName: repoName2, yolo: yolo2, sessions: sessionStore, resumeLatest: resumeLatest2, onViewTurns: openViewer, onViewResume: isRawModeSupported && interactivePicker ? openResume : void 0, isEscOwned });
102173
102905
  const { columns, rows } = useTerminalSize();
102174
102906
  const viewing = viewingRaw && !pending;
102175
102907
  const picking = resumeFound !== null && !pending;
@@ -102264,7 +102996,7 @@ function App2({ repoName: repoName2, onboardingMode: onboardingMode2, launchCoun
102264
102996
  ] }),
102265
102997
  isRunning && !pending && stats.elapsedMs !== null && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Text, { ...paint("secondary"), children: formatThinking(stats.elapsedMs, stats.approxTokens, unicode) }),
102266
102998
  focusing && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Text, { ...paint("secondary"), children: unicode ? "step focus: \u2191\u2193 or tab move \xB7 enter open/close \xB7 esc back to the prompt" : "step focus: up/down or tab move - enter open/close - esc back to the prompt" }),
102267
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(PromptEditor, { onEmptyEnter: isRunning ? void 0 : retry, promptGlyph: promptGlyph(mode, unicode), unicode, fullWidth: true, onSubmit: submit, disabled: isRunning, history: promptHistory, listFiles: mentions.list, onTab: enterFocus, locked: focusing || viewing || picking || Boolean(pending) }),
102999
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(PromptEditor, { extraCommands: commandEntries, onEmptyEnter: isRunning ? void 0 : retry, promptGlyph: promptGlyph(mode, unicode), unicode, fullWidth: true, onSubmit: submit, disabled: isRunning, history: promptHistory, listFiles: mentions.list, onTab: enterFocus, locked: focusing || viewing || picking || Boolean(pending) }),
102268
103000
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Text, { ...footer.level === "critical" ? paint("delete") : footer.level === "warn" ? paint("needsYou") : paint("secondary"), children: footer.text }),
102269
103001
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Text, { ...paint("secondary"), children: formatHints({ running: isRunning, asking: Boolean(pending), columns }, activeGlyphs().sep) })
102270
103002
  ] })
@@ -102310,7 +103042,7 @@ function App2({ repoName: repoName2, onboardingMode: onboardingMode2, launchCoun
102310
103042
  pending && !waitingStep && promptNode,
102311
103043
  isRunning && !pending && stats.elapsedMs !== null && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Text, { ...paint("secondary"), children: formatThinking(stats.elapsedMs, stats.approxTokens, activeGlyphs().rail === "\u2502") }),
102312
103044
  focusing && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Text, { ...paint("secondary"), children: activeGlyphs().rail === "\u2502" ? "step focus: \u2191\u2193 or tab move \xB7 enter open/close \xB7 esc back to the prompt" : "step focus: up/down or tab move - enter open/close - esc back to the prompt" }),
102313
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(PromptEditor, { onEmptyEnter: isRunning ? void 0 : retry, promptGlyph: promptGlyph(mode, activeGlyphs().rail === "\u2502"), unicode: activeGlyphs().rail === "\u2502", onSubmit: submit, disabled: isRunning, history: promptHistory, listFiles: mentions.list, onTab: enterFocus, locked: focusing || viewing || picking || Boolean(pending) }),
103045
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(PromptEditor, { extraCommands: commandEntries, onEmptyEnter: isRunning ? void 0 : retry, promptGlyph: promptGlyph(mode, activeGlyphs().rail === "\u2502"), unicode: activeGlyphs().rail === "\u2502", onSubmit: submit, disabled: isRunning, history: promptHistory, listFiles: mentions.list, onTab: enterFocus, locked: focusing || viewing || picking || Boolean(pending) }),
102314
103046
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Text, { ...footer.level === "critical" ? paint("delete") : footer.level === "warn" ? paint("needsYou") : paint("secondary"), children: footer.text }),
102315
103047
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Text, { ...paint("secondary"), children: formatHints({ running: isRunning, asking: Boolean(pending), columns }, activeGlyphs().sep) })
102316
103048
  ] })
@@ -102453,7 +103185,7 @@ function clearOnResize(stdout) {
102453
103185
  var import_jsx_runtime14 = __toESM(require_jsx_runtime(), 1);
102454
103186
  if (process.argv[2] === "import-claude") {
102455
103187
  const { runImportClaude: runImportClaude2 } = await Promise.resolve().then(() => (init_import_claude(), import_claude_exports));
102456
- const { readFile: readFile7, readdir: readdir3 } = await import("node:fs/promises");
103188
+ const { readFile: readFile7, readdir: readdir4 } = await import("node:fs/promises");
102457
103189
  const { execFile: execFile4 } = await import("node:child_process");
102458
103190
  const gitRoot = () => new Promise((resolve2) => {
102459
103191
  execFile4("git", ["rev-parse", "--show-toplevel"], { cwd: process.cwd() }, (err, stdout) => resolve2(err ? null : stdout.trim() || null));
@@ -102461,7 +103193,7 @@ if (process.argv[2] === "import-claude") {
102461
103193
  const code = await runImportClaude2({
102462
103194
  argv: process.argv.slice(2),
102463
103195
  env: process.env,
102464
- fs: { readFile: (p) => readFile7(p, "utf-8"), readdir: (p) => readdir3(p) },
103196
+ fs: { readFile: (p) => readFile7(p, "utf-8"), readdir: (p) => readdir4(p) },
102465
103197
  readFile: (p) => readFile7(p, "utf-8"),
102466
103198
  homedir: os5.homedir,
102467
103199
  cwd: process.cwd(),