spacecode 2.5.4 → 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
@@ -4949,6 +4949,187 @@ var init_command_file = __esm({
4949
4949
  }
4950
4950
  });
4951
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
+
4952
5133
  // ../agent/src/import/claude-sources.ts
4953
5134
  function isEnoent(err) {
4954
5135
  return typeof err === "object" && err !== null && err.code === "ENOENT";
@@ -4987,6 +5168,7 @@ async function readClaudeAssets({ fs: fs6, home, projectDirs }) {
4987
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");
4988
5169
  }
4989
5170
  await readCommands(fs6, home, report);
5171
+ await readSkills(fs6, home, report);
4990
5172
  for (const dir of projectDirs) {
4991
5173
  const memDir = `${home}/.claude/projects/${claudeProjectSlug(dir)}/memory`;
4992
5174
  let entries;
@@ -5006,7 +5188,7 @@ async function readClaudeAssets({ fs: fs6, home, projectDirs }) {
5006
5188
  if (text === null) continue;
5007
5189
  const base = entry === "MEMORY.md" ? "index" : entry.slice(0, -3).replace(/[^A-Za-z0-9._-]/g, "_");
5008
5190
  const name = `${key}.${base}`;
5009
- if (!NAME_RE.test(name)) {
5191
+ if (!NAME_RE3.test(name)) {
5010
5192
  report.errors.push({ path: path12, message: `cannot derive a valid asset name from "${entry}"` });
5011
5193
  continue;
5012
5194
  }
@@ -5063,14 +5245,84 @@ async function readCommands(fs6, home, report) {
5063
5245
  report.assets.push({ kind: "command", name, description: parseCommandFile(text).description, content: text, source: path12 });
5064
5246
  }
5065
5247
  }
5066
- var MAX_ASSET_BYTES, NAME_RE, COMMAND_NAME_RE;
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;
5067
5315
  var init_claude_sources = __esm({
5068
5316
  "../agent/src/import/claude-sources.ts"() {
5069
5317
  "use strict";
5070
5318
  init_command_file();
5319
+ init_skill_file();
5320
+ init_skill_source();
5071
5321
  MAX_ASSET_BYTES = 256 * 1024;
5072
- NAME_RE = /^[A-Za-z0-9._-]{1,100}$/;
5322
+ NAME_RE3 = /^[A-Za-z0-9._-]{1,100}$/;
5073
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;
5074
5326
  }
5075
5327
  });
5076
5328
 
@@ -5120,6 +5372,16 @@ function createProfileClient({ baseUrl, apiKey, fetch: doFetch, timeoutMs = 8e3
5120
5372
  const res = await call("POST", "/adapt", { body: { sections }, timeoutMs: ADAPT_TIMEOUT_MS });
5121
5373
  return await res.json();
5122
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
+ },
5123
5385
  async put(kind, name, input) {
5124
5386
  const res = await call("PUT", `/assets/${enc(kind)}/${enc(name)}`, {
5125
5387
  body: { content: input.content, description: input.description ?? "", base_version: input.baseVersion }
@@ -5302,46 +5564,6 @@ var init_instruction_sections = __esm({
5302
5564
  }
5303
5565
  });
5304
5566
 
5305
- // ../agent/src/import/tool-names.ts
5306
- function translateToolNames(text) {
5307
- const counts = /* @__PURE__ */ new Map();
5308
- const names = Object.keys(TOOL_NAME_MAP).join("|");
5309
- const re3 = new RegExp("`(" + names + ")(\\([^`)]*\\))?`", "g");
5310
- const out = text.replace(re3, (_m, name, args) => {
5311
- counts.set(name, (counts.get(name) ?? 0) + 1);
5312
- return "`" + TOOL_NAME_MAP[name] + (args ?? "") + "`";
5313
- });
5314
- return { text: out, replaced: [...counts].map(([from, count]) => ({ from, to: TOOL_NAME_MAP[from], count })) };
5315
- }
5316
- function findUnsupported(text) {
5317
- return UNSUPPORTED.filter((u3) => u3.re.test(text)).map((u3) => u3.label);
5318
- }
5319
- var TOOL_NAME_MAP, UNSUPPORTED;
5320
- var init_tool_names = __esm({
5321
- "../agent/src/import/tool-names.ts"() {
5322
- "use strict";
5323
- TOOL_NAME_MAP = {
5324
- Read: "read_file",
5325
- Write: "write_file",
5326
- Edit: "edit_file",
5327
- MultiEdit: "edit_file",
5328
- Glob: "list_directory",
5329
- Grep: "search",
5330
- Bash: "bash",
5331
- TodoWrite: "todo_write"
5332
- };
5333
- UNSUPPORTED = [
5334
- { label: "hooks", re: /\bhooks?\b.*\.(sh|json)|\bPreToolUse\b|\bPostToolUse\b|\bSessionStart\b|\bUserPromptSubmit\b/i },
5335
- { label: "ToolSearch / deferred tools", re: /\bToolSearch\b|deferred tools?/i },
5336
- { label: "Claude Code's Task/subagent tools", re: /\bsubagents?\b|\bAgent tool\b|`Task`|subagent_type/i },
5337
- { label: "Claude Code skills / slash commands", re: /\bSkill tool\b|invoke (?:the )?\w+ skill|\/ship\b|\/handoff\b/i },
5338
- { label: "MCP tool names (mcp__*)", re: /\bmcp__[A-Za-z0-9_-]+/ },
5339
- { label: "WebFetch/WebSearch/NotebookEdit tools", re: /`(?:WebFetch|WebSearch|NotebookEdit)`/ },
5340
- { label: "plan mode / worktree tools", re: /\bEnterPlanMode\b|\bExitPlanMode\b|\bEnterWorktree\b/ }
5341
- ];
5342
- }
5343
- });
5344
-
5345
5567
  // ../agent/src/import/coverage.ts
5346
5568
  function extractRuleLines(text) {
5347
5569
  const out = [];
@@ -5850,6 +6072,37 @@ var init_command_registry = __esm({
5850
6072
  }
5851
6073
  });
5852
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
+
5853
6106
  // ../agent/src/index.ts
5854
6107
  var init_src = __esm({
5855
6108
  "../agent/src/index.ts"() {
@@ -5894,6 +6147,10 @@ var init_src = __esm({
5894
6147
  init_command_file();
5895
6148
  init_command_registry();
5896
6149
  init_claude_sources();
6150
+ init_skill_file();
6151
+ init_skill_prompt();
6152
+ init_skill_tool();
6153
+ init_skill_source();
5897
6154
  }
5898
6155
  });
5899
6156
 
@@ -5934,14 +6191,225 @@ var init_api_key = __esm({
5934
6191
  }
5935
6192
  });
5936
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
+
5937
6405
  // src/profile-runtime.ts
5938
- import { join as join2 } from "node:path";
6406
+ import { join as join4 } from "node:path";
5939
6407
  function backendUrl(env3) {
5940
6408
  return env3.SPACECODE_BACKEND_URL?.trim() || env3.SPACECODE_API_URL?.trim() || DEFAULT_BACKEND_URL;
5941
6409
  }
5942
6410
  function fileCache(deps) {
5943
- const dir = join2(deps.env.SPACECODE_CONFIG_DIR?.trim() || join2(deps.homedir(), ".spacecode"));
5944
- 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");
5945
6413
  return {
5946
6414
  async read() {
5947
6415
  let raw;
@@ -5987,8 +6455,15 @@ async function prepareProfileUnsafe(deps) {
5987
6455
  const root = await deps.gitRoot?.() ?? null;
5988
6456
  const keys = [.../* @__PURE__ */ new Set([projectKey(deps.cwd), ...root ? [projectKey(root)] : []])];
5989
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
+ });
5990
6465
  const result = await loadProfile({ client, cache: fileCache(deps), projectKeys: keys });
5991
- 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 };
5992
6467
  const key = result.profile.projectKey;
5993
6468
  const hasMemory = result.profile.memoryIndex !== null && key !== null;
5994
6469
  const hasSections = result.profile.instructions?.includes("## More instructions (on demand)") ?? false;
@@ -5997,10 +6472,13 @@ async function prepareProfileUnsafe(deps) {
5997
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" : ""}`
5998
6473
  ];
5999
6474
  if (result.error) lines2.push(`profile warning: ${result.error}`);
6475
+ lines2.push(...skills.statusLines);
6000
6476
  return {
6001
- promptSection: section2.text,
6002
- 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],
6003
6480
  handlers: {
6481
+ ...skills.handlers,
6004
6482
  ...hasMemory ? { memory_read: createMemoryReadTool(client, key) } : {},
6005
6483
  ...hasSections ? { instructions_read: createInstructionsReadTool(client) } : {}
6006
6484
  },
@@ -6013,6 +6491,8 @@ var init_profile_runtime = __esm({
6013
6491
  "use strict";
6014
6492
  init_src();
6015
6493
  init_api_key();
6494
+ init_skills_loader();
6495
+ init_src();
6016
6496
  DEFAULT_BACKEND_URL = "https://backend.spacecode.app";
6017
6497
  EMPTY = (statusLines) => ({ promptSection: "", tools: [], handlers: {}, statusLines });
6018
6498
  }
@@ -41385,7 +41865,7 @@ var require_core = __commonJS({
41385
41865
  return match && match.index === 0;
41386
41866
  }
41387
41867
  var BACKREF_RE = /\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;
41388
- function join8(regexps, separator = "|") {
41868
+ function join9(regexps, separator = "|") {
41389
41869
  let numCaptures = 0;
41390
41870
  return regexps.map((regex2) => {
41391
41871
  numCaptures += 1;
@@ -41689,7 +42169,7 @@ var require_core = __commonJS({
41689
42169
  this.exec = () => null;
41690
42170
  }
41691
42171
  const terminators = this.regexes.map((el) => el[1]);
41692
- this.matcherRe = langRe(join8(terminators), true);
42172
+ this.matcherRe = langRe(join9(terminators), true);
41693
42173
  this.lastIndex = 0;
41694
42174
  }
41695
42175
  /** @param {string} s */
@@ -99195,7 +99675,7 @@ var init_credential_filter = __esm({
99195
99675
  });
99196
99676
 
99197
99677
  // src/session/session-store.ts
99198
- 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";
99199
99679
  import { randomBytes } from "node:crypto";
99200
99680
  import path3 from "node:path";
99201
99681
  function redactCredentials(value) {
@@ -99284,11 +99764,11 @@ var init_session_store = __esm({
99284
99764
  }
99285
99765
  const tmp = `${file}.${process.pid}.tmp`;
99286
99766
  try {
99287
- await mkdir(this.dir, { recursive: true, mode: 448 });
99288
- await chmod(this.dir, 448);
99289
- await writeFile(tmp, body, { mode: 384 });
99290
- await chmod(tmp, 384);
99291
- 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);
99292
99772
  } catch (err) {
99293
99773
  throw new SessionError(file, `Could not save session: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
99294
99774
  }
@@ -99301,7 +99781,7 @@ var init_session_store = __esm({
99301
99781
  if (size > MAX_SESSION_BYTES) {
99302
99782
  throw new SessionError(file, `Session is ${size} bytes, over the ${MAX_SESSION_BYTES}-byte cap; not loaded`);
99303
99783
  }
99304
- text = await readFile(file, "utf8");
99784
+ text = await readFile2(file, "utf8");
99305
99785
  } catch (err) {
99306
99786
  if (err instanceof SessionError) throw err;
99307
99787
  throw new SessionError(file, `Could not read session: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
@@ -99314,7 +99794,7 @@ var init_session_store = __esm({
99314
99794
  async list() {
99315
99795
  let names;
99316
99796
  try {
99317
- names = await readdir(this.dir);
99797
+ names = await readdir2(this.dir);
99318
99798
  } catch (err) {
99319
99799
  if (err.code === "ENOENT") return { sessions: [], errors: [] };
99320
99800
  throw new SessionError(this.dir, `Could not list sessions: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
@@ -100492,6 +100972,7 @@ var init_slash_commands = __esm({
100492
100972
  { name: "model", description: "List gateway models; /model <n|id> switches for this session" },
100493
100973
  { name: "turns", description: "Browse earlier turns and open their steps (shortcut: ctrl+o)" },
100494
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>" },
100495
100976
  { name: "tools", description: "List the tools available to the agent" },
100496
100977
  { name: "exit", description: "Quit the REPL (alias: /quit)" },
100497
100978
  { name: "quit", description: "Quit the REPL" }
@@ -100508,7 +100989,7 @@ function validateSubmission(text, extra = []) {
100508
100989
  function withCursor(row, col) {
100509
100990
  return `${row.slice(0, col)}\x1B[7m${row[col] ?? " "}\x1B[27m${row.slice(col + 1)}`;
100510
100991
  }
100511
- function PromptEditor({ onSubmit, disabled, history, listFiles: listFiles2, onTab, locked = false, promptGlyph: promptGlyph2 = ">", onEmptyEnter, unicode = true, fullWidth = false, extraCommands = [] }) {
100992
+ function PromptEditor({ onSubmit, disabled, history, listFiles: listFiles3, onTab, locked = false, promptGlyph: promptGlyph2 = ">", onEmptyEnter, unicode = true, fullWidth = false, extraCommands = [] }) {
100512
100993
  const { exit } = use_app_default();
100513
100994
  const { columns } = useTerminalSize();
100514
100995
  const { isRawModeSupported } = use_stdin_default();
@@ -100544,15 +101025,15 @@ function PromptEditor({ onSubmit, disabled, history, listFiles: listFiles2, onTa
100544
101025
  const mention = shell ? null : activeMention(buf.text, buf.cursor);
100545
101026
  const mentionOpen = mention !== null;
100546
101027
  (0, import_react29.useEffect)(() => {
100547
- if (!mentionOpen || !listFiles2 || filesRef.current !== null || loadingFiles.current) return;
101028
+ if (!mentionOpen || !listFiles3 || filesRef.current !== null || loadingFiles.current) return;
100548
101029
  loadingFiles.current = true;
100549
- listFiles2().then((f) => {
101030
+ listFiles3().then((f) => {
100550
101031
  filesRef.current = f;
100551
101032
  setFiles(f);
100552
101033
  }).catch((err) => fail("Could not list files", err)).finally(() => {
100553
101034
  loadingFiles.current = false;
100554
101035
  });
100555
- }, [mentionOpen, listFiles2, fail]);
101036
+ }, [mentionOpen, listFiles3, fail]);
100556
101037
  const chooseSel = (n) => {
100557
101038
  selRef.current = n;
100558
101039
  setSel(n);
@@ -100787,24 +101268,24 @@ var init_history_store = __esm({
100787
101268
  });
100788
101269
 
100789
101270
  // src/editor/history-file-io.ts
100790
- 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";
100791
101272
  import path4 from "node:path";
100792
101273
  function createFileHistoryIO(file) {
100793
101274
  return {
100794
101275
  async read() {
100795
101276
  try {
100796
- return await readFile2(file, "utf8");
101277
+ return await readFile3(file, "utf8");
100797
101278
  } catch (err) {
100798
101279
  if (err.code === "ENOENT") return null;
100799
101280
  throw err;
100800
101281
  }
100801
101282
  },
100802
101283
  async write(content) {
100803
- await mkdir2(path4.dirname(file), { recursive: true, mode: 448 });
101284
+ await mkdir3(path4.dirname(file), { recursive: true, mode: 448 });
100804
101285
  const tmp = `${file}.${process.pid}.tmp`;
100805
- await writeFile2(tmp, content, { mode: 384 });
100806
- await chmod2(tmp, 384);
100807
- await rename2(tmp, file);
101286
+ await writeFile3(tmp, content, { mode: 384 });
101287
+ await chmod3(tmp, 384);
101288
+ await rename3(tmp, file);
100808
101289
  }
100809
101290
  };
100810
101291
  }
@@ -101013,7 +101494,7 @@ var init_shell_turn = __esm({
101013
101494
  });
101014
101495
 
101015
101496
  // src/editor/file-list.ts
101016
- async function listFiles(readDir, opts = {}) {
101497
+ async function listFiles2(readDir, opts = {}) {
101017
101498
  const max = opts.maxFiles ?? DEFAULT_MAX_LISTED_FILES;
101018
101499
  const files = [];
101019
101500
  const queue = [""];
@@ -101240,18 +101721,18 @@ var init_mention_resolver = __esm({
101240
101721
  });
101241
101722
 
101242
101723
  // src/editor/node-mention-fs.ts
101243
- 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";
101244
101725
  import path6 from "node:path";
101245
101726
  function isInside(root, target) {
101246
101727
  return target === root || target.startsWith(root.endsWith(path6.sep) ? root : root + path6.sep);
101247
101728
  }
101248
101729
  function createNodeMentionFS(cwd2) {
101249
101730
  let rootPromise;
101250
- const root = () => rootPromise ??= realpath(cwd2);
101731
+ const root = () => rootPromise ??= realpath2(cwd2);
101251
101732
  async function confined(abs) {
101252
101733
  let real;
101253
101734
  try {
101254
- real = await realpath(abs);
101735
+ real = await realpath2(abs);
101255
101736
  } catch (err) {
101256
101737
  if (err.code === "ENOENT") return null;
101257
101738
  throw err;
@@ -101287,11 +101768,11 @@ var init_node_mention_fs = __esm({
101287
101768
  });
101288
101769
 
101289
101770
  // src/editor/node-read-dir.ts
101290
- import { readdir as readdir2 } from "node:fs/promises";
101771
+ import { readdir as readdir3 } from "node:fs/promises";
101291
101772
  import path7 from "node:path";
101292
101773
  function createNodeReadDir(cwd2) {
101293
101774
  return async (rel) => {
101294
- const entries = await readdir2(path7.join(cwd2, rel), { withFileTypes: true });
101775
+ const entries = await readdir3(path7.join(cwd2, rel), { withFileTypes: true });
101295
101776
  return entries.map((e) => ({ name: e.name, isFile: e.isFile(), isDirectory: e.isDirectory() }));
101296
101777
  };
101297
101778
  }
@@ -101302,11 +101783,11 @@ var init_node_read_dir = __esm({
101302
101783
  });
101303
101784
 
101304
101785
  // src/editor/node-mentions.ts
101305
- import { readFile as readFile3 } from "node:fs/promises";
101786
+ import { readFile as readFile4 } from "node:fs/promises";
101306
101787
  import path8 from "node:path";
101307
101788
  async function readGitignore(cwd2) {
101308
101789
  try {
101309
- return createGitignoreMatcher(await readFile3(path8.join(cwd2, ".gitignore"), "utf8"));
101790
+ return createGitignoreMatcher(await readFile4(path8.join(cwd2, ".gitignore"), "utf8"));
101310
101791
  } catch (err) {
101311
101792
  if (err.code === "ENOENT") return () => false;
101312
101793
  throw err;
@@ -101320,7 +101801,7 @@ function createNodeMentions(cwd2) {
101320
101801
  return formatMentionPrompt(text, result);
101321
101802
  },
101322
101803
  async list() {
101323
- return listFiles(createNodeReadDir(cwd2), { ignored: await readGitignore(cwd2) });
101804
+ return listFiles2(createNodeReadDir(cwd2), { ignored: await readGitignore(cwd2) });
101324
101805
  }
101325
101806
  };
101326
101807
  }
@@ -101336,89 +101817,48 @@ var init_node_mentions = __esm({
101336
101817
  }
101337
101818
  });
101338
101819
 
101339
- // src/import-gate.ts
101340
- import { createHash, randomUUID } from "node:crypto";
101341
- import { chmod as chmod3, mkdir as mkdir3, readFile as readFile4, readdir as readdir3, realpath as realpath2, rename as rename3, writeFile as writeFile3 } from "node:fs/promises";
101342
- import { dirname as dirname3, join as join4 } from "node:path";
101343
- async function readOrNull(path12) {
101344
- try {
101345
- return await readFile4(path12, "utf8");
101346
- } catch (err) {
101347
- if (err.code === "ENOENT") return null;
101348
- throw err;
101349
- }
101350
- }
101351
- async function writeTextAtomic(path12, text) {
101352
- await mkdir3(dirname3(path12), { recursive: true });
101353
- const tmp = `${path12}.${process.pid}.${randomUUID()}.tmp`;
101354
- await writeFile3(tmp, text, { mode: 384 });
101355
- await chmod3(tmp, 384);
101356
- await rename3(tmp, path12);
101357
- }
101358
- async function readRepoSources(cwd2) {
101359
- const files = {};
101360
- for (const name of REPO_SOURCE_FILES) {
101361
- const text = await readOrNull(join4(cwd2, name));
101362
- if (text !== null) files[name] = text;
101363
- }
101364
- let entries = [];
101365
- try {
101366
- entries = await readdir3(join4(cwd2, ".claude", "commands"));
101367
- } catch (err) {
101368
- if (err.code !== "ENOENT" && err.code !== "ENOTDIR") throw err;
101369
- }
101370
- for (const e of entries.filter((x2) => x2.endsWith(".md")).sort()) {
101371
- const text = await readOrNull(join4(cwd2, ".claude", "commands", e));
101372
- if (text !== null) files[`.claude/commands/${e}`] = text;
101373
- }
101374
- return files;
101375
- }
101376
- function nodeTrustFs() {
101377
- return { readText: readOrNull, writeTextAtomic, realPath: (p) => realpath2(p) };
101378
- }
101379
- function createNodeTrustStore(configDir) {
101380
- return new TrustStore({ fs: nodeTrustFs(), path: join4(configDir, "trust.json"), hash: sha256 });
101381
- }
101382
- async function importGateStatus(deps) {
101383
- const lines2 = [];
101384
- const configPath = join4(deps.configDir, "imports.json");
101385
- try {
101386
- const loaded = parseImportsConfig(await readOrNull(configPath), configPath);
101387
- if (loaded.error) lines2.push(`imports: ${loaded.error} (every category off)`);
101388
- else {
101389
- const on = IMPORT_CATEGORIES.filter((c) => loaded.config.categories[c]);
101390
- lines2.push(on.length ? `imports: on for ${on.join(", ")}` : "imports: every category off (default)");
101391
- }
101392
- } catch (err) {
101393
- lines2.push(`imports: not read (${err instanceof Error ? err.message : String(err)}); every category off`);
101394
- }
101395
- try {
101396
- const files = await readRepoSources(deps.cwd);
101397
- const found = Object.keys(files);
101398
- if (found.length === 0) return lines2;
101399
- const status = await createNodeTrustStore(deps.configDir).repoStatus(deps.cwd, files);
101400
- const names = found.join(", ");
101401
- if (status === "trusted" || status === "session") lines2.push(`repo sources: ${names} (trusted${status === "session" ? " for this session" : ""})`);
101402
- else if (status === "never") lines2.push(`repo sources: ${names} NOT loaded (marked never trust)`);
101403
- else lines2.push(`repo sources: ${names} NOT loaded (untrusted${deps.interactive ? "" : ", non-interactive run"})`);
101404
- } catch (err) {
101405
- 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.`;
101406
101849
  }
101407
- return lines2;
101850
+ return "Usage: /skills | /skills show <name> [file] | /skills delete <name>";
101408
101851
  }
101409
- var REPO_SOURCE_FILES, sha256;
101410
- var init_import_gate = __esm({
101411
- "src/import-gate.ts"() {
101852
+ var init_skills_command = __esm({
101853
+ "src/skills-command.ts"() {
101412
101854
  "use strict";
101413
101855
  init_src();
101414
- REPO_SOURCE_FILES = [".mcp.json", ".claude/settings.json", ".claude/settings.local.json", "CLAUDE.local.md"];
101415
- sha256 = (text) => createHash("sha256").update(text).digest("hex");
101416
101856
  }
101417
101857
  });
101418
101858
 
101419
101859
  // src/permission-rules-loader.ts
101420
101860
  import { readFile as readFile5 } from "node:fs/promises";
101421
- import { join as join5 } from "node:path";
101861
+ import { join as join6 } from "node:path";
101422
101862
  async function readOrNull2(path12) {
101423
101863
  try {
101424
101864
  return await readFile5(path12, "utf8");
@@ -101429,7 +101869,7 @@ async function readOrNull2(path12) {
101429
101869
  }
101430
101870
  async function loadPermissionRules(deps) {
101431
101871
  const none = (line) => ({ rules: void 0, defaultMode: void 0, statusLines: [line] });
101432
- const configPath = join5(deps.configDir, "imports.json");
101872
+ const configPath = join6(deps.configDir, "imports.json");
101433
101873
  try {
101434
101874
  const loaded = parseImportsConfig(await readOrNull2(configPath), configPath);
101435
101875
  if (loaded.error) return none("permission rules: off (imports.json unreadable)");
@@ -101438,13 +101878,13 @@ async function loadPermissionRules(deps) {
101438
101878
  return none(`permission rules: off (imports.json not read: ${err instanceof Error ? err.message : String(err)})`);
101439
101879
  }
101440
101880
  const lines2 = [];
101441
- const files = [{ scope: "user", path: join5(deps.homeDir, ".claude", "settings.json") }];
101881
+ const files = [{ scope: "user", path: join6(deps.homeDir, ".claude", "settings.json") }];
101442
101882
  try {
101443
101883
  const sources2 = await readRepoSources(deps.cwd);
101444
101884
  if (sources2[".claude/settings.json"] !== void 0 || sources2[".claude/settings.local.json"] !== void 0) {
101445
101885
  const status = await createNodeTrustStore(deps.configDir).repoStatus(deps.cwd, sources2);
101446
101886
  if (status === "trusted" || status === "session") {
101447
- 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") });
101448
101888
  } else lines2.push(`permission rules: repo settings NOT loaded (${status === "never" ? "marked never trust" : "untrusted repo"})`);
101449
101889
  }
101450
101890
  } catch (err) {
@@ -101486,7 +101926,7 @@ var init_permission_rules_loader = __esm({
101486
101926
  });
101487
101927
 
101488
101928
  // src/command-loader.ts
101489
- import { join as join6 } from "node:path";
101929
+ import { join as join7 } from "node:path";
101490
101930
  async function userCommands(deps, lines2) {
101491
101931
  try {
101492
101932
  const apiKey = await resolveApiKey({ env: deps.env, readFile: deps.readFile, homedir: deps.homedir });
@@ -101541,7 +101981,7 @@ async function repoCommands(deps, lines2) {
101541
101981
  }
101542
101982
  }
101543
101983
  async function loadCommands(deps) {
101544
- const configPath = join6(deps.configDir, "imports.json");
101984
+ const configPath = join7(deps.configDir, "imports.json");
101545
101985
  try {
101546
101986
  const loaded = parseImportsConfig(await deps.readFile(configPath).catch((err) => {
101547
101987
  if (err.code === "ENOENT") return null;
@@ -101576,7 +102016,7 @@ var init_command_loader = __esm({
101576
102016
  // src/useReplController.ts
101577
102017
  import { readFile as readFile6, writeFile as writeFile4, mkdir as mkdir4 } from "node:fs/promises";
101578
102018
  import { homedir as homedir2 } from "node:os";
101579
- import { join as join7 } from "node:path";
102019
+ import { join as join8 } from "node:path";
101580
102020
  import { execFile as execFile3 } from "node:child_process";
101581
102021
  function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mentionsOpt, sessions, resumeLatest: resumeLatest2, onViewTurns, onViewResume, isEscOwned }) {
101582
102022
  const { exit } = use_app_default();
@@ -101623,7 +102063,7 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
101623
102063
  (0, import_react30.useEffect)(() => {
101624
102064
  let live = true;
101625
102065
  loadCommands({
101626
- configDir: process.env.SPACECODE_CONFIG_DIR?.trim() || join7(homedir2(), ".spacecode"),
102066
+ configDir: process.env.SPACECODE_CONFIG_DIR?.trim() || join8(homedir2(), ".spacecode"),
101627
102067
  cwd: process.cwd(),
101628
102068
  env: process.env,
101629
102069
  readFile: (p) => readFile6(p, "utf-8"),
@@ -101954,6 +102394,13 @@ ${commandsLoaded.statusLines.map((l) => ` ${l}`).join("\n")}`.trim() : `Importe
101954
102394
  ${list2.map((c) => ` /${c.name} (${c.scope})${c.description ? ` ${c.description}` : ""}`).join("\n")}`);
101955
102395
  return;
101956
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
+ }
101957
102404
  case "tools": {
101958
102405
  const tools = toolsetRef.current?.tools ?? createLocalToolset({ cwd: process.cwd(), permissions, onTodos: setTodos }).tools;
101959
102406
  notice(`Tools:
@@ -102016,7 +102463,7 @@ ${tools.map((t) => ` ${t.name}`).join("\n")}`);
102016
102463
  cwd: process.cwd(),
102017
102464
  gitRoot: gitTopLevel,
102018
102465
  importGate: async () => {
102019
- const configDir = process.env.SPACECODE_CONFIG_DIR?.trim() || join7(homedir2(), ".spacecode");
102466
+ const configDir = process.env.SPACECODE_CONFIG_DIR?.trim() || join8(homedir2(), ".spacecode");
102020
102467
  const gate = await importGateStatus({ configDir, cwd: process.cwd(), interactive: Boolean(process.stdin.isTTY) });
102021
102468
  const loaded = await loadPermissionRules({ configDir, homeDir: homedir2(), cwd: process.cwd() });
102022
102469
  rulesRef.current = loaded.rules;
@@ -102149,6 +102596,8 @@ var init_useReplController = __esm({
102149
102596
  init_node_mentions();
102150
102597
  init_api_key();
102151
102598
  init_profile_runtime();
102599
+ init_skills_command();
102600
+ init_src();
102152
102601
  init_import_gate();
102153
102602
  init_permission_rules_loader();
102154
102603
  init_command_loader();