spacecode 2.5.3 → 2.5.4

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,30 @@ 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
+
4928
4952
  // ../agent/src/import/claude-sources.ts
4929
4953
  function isEnoent(err) {
4930
4954
  return typeof err === "object" && err !== null && err.code === "ENOENT";
@@ -4962,6 +4986,7 @@ async function readClaudeAssets({ fs: fs6, home, projectDirs }) {
4962
4986
  report.assets.push({ kind: "instructions", name: "global", description: "Global CLAUDE.md", content: globalText, source: globalPath });
4963
4987
  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
4988
  }
4989
+ await readCommands(fs6, home, report);
4965
4990
  for (const dir of projectDirs) {
4966
4991
  const memDir = `${home}/.claude/projects/${claudeProjectSlug(dir)}/memory`;
4967
4992
  let entries;
@@ -5003,11 +5028,49 @@ async function readClaudeAssets({ fs: fs6, home, projectDirs }) {
5003
5028
  report.notes.push("No Claude Code memory directory found for this project");
5004
5029
  return report;
5005
5030
  }
5006
- var NAME_RE;
5031
+ async function readCommands(fs6, home, report) {
5032
+ const dir = `${home}/.claude/commands`;
5033
+ let entries;
5034
+ try {
5035
+ entries = await fs6.readdir(dir);
5036
+ } catch (err) {
5037
+ if (!isEnoent(err)) report.errors.push({ path: dir, message: err instanceof Error ? err.message : String(err) });
5038
+ return;
5039
+ }
5040
+ const seen = /* @__PURE__ */ new Set();
5041
+ for (const entry of [...entries].sort()) {
5042
+ const path12 = `${dir}/${entry}`;
5043
+ if (!entry.endsWith(".md")) {
5044
+ if (!entry.startsWith(".")) report.notes.push(`Skipped ${path12}: only top-level .md command files are imported (subdirectories are not supported)`);
5045
+ continue;
5046
+ }
5047
+ const name = entry.slice(0, -3);
5048
+ if (!COMMAND_NAME_RE.test(name)) {
5049
+ report.errors.push({ path: path12, message: `command name "${name}" must start with a letter and use only letters, digits, - and _` });
5050
+ continue;
5051
+ }
5052
+ if (seen.has(name.toLowerCase())) {
5053
+ report.errors.push({ path: path12, message: `command name collision on "${name}" (names are case-insensitive)` });
5054
+ continue;
5055
+ }
5056
+ const text = await readOptional(fs6, path12, report);
5057
+ if (text === null) continue;
5058
+ if (Buffer.byteLength(text, "utf8") > MAX_ASSET_BYTES) {
5059
+ report.errors.push({ path: path12, message: `skipped: ${Buffer.byteLength(text, "utf8")} bytes is over the ${MAX_ASSET_BYTES} byte asset limit` });
5060
+ continue;
5061
+ }
5062
+ seen.add(name.toLowerCase());
5063
+ report.assets.push({ kind: "command", name, description: parseCommandFile(text).description, content: text, source: path12 });
5064
+ }
5065
+ }
5066
+ var MAX_ASSET_BYTES, NAME_RE, COMMAND_NAME_RE;
5007
5067
  var init_claude_sources = __esm({
5008
5068
  "../agent/src/import/claude-sources.ts"() {
5009
5069
  "use strict";
5070
+ init_command_file();
5071
+ MAX_ASSET_BYTES = 256 * 1024;
5010
5072
  NAME_RE = /^[A-Za-z0-9._-]{1,100}$/;
5073
+ COMMAND_NAME_RE = /^[A-Za-z][A-Za-z0-9_-]*$/;
5011
5074
  }
5012
5075
  });
5013
5076
 
@@ -5719,6 +5782,74 @@ var init_permission_rules = __esm({
5719
5782
  }
5720
5783
  });
5721
5784
 
5785
+ // ../agent/src/import/command-args.ts
5786
+ function parseWords(input) {
5787
+ const words = [];
5788
+ let current = "";
5789
+ let quote = null;
5790
+ let has = false;
5791
+ for (const ch of input) {
5792
+ if (quote) {
5793
+ if (ch === quote) quote = null;
5794
+ else current += ch;
5795
+ } else if (ch === '"' || ch === "'") {
5796
+ quote = ch;
5797
+ has = true;
5798
+ } else if (/\s/.test(ch)) {
5799
+ if (has) words.push(current);
5800
+ current = "";
5801
+ has = false;
5802
+ } else {
5803
+ current += ch;
5804
+ has = true;
5805
+ }
5806
+ }
5807
+ if (has) words.push(current);
5808
+ return words;
5809
+ }
5810
+ function expandCommand(body, args) {
5811
+ const trimmed = args.trim();
5812
+ const words = parseWords(trimmed);
5813
+ return body.replace(/\$ARGUMENTS|\$[1-9]/g, (m2) => m2 === "$ARGUMENTS" ? trimmed : words[Number(m2[1]) - 1] ?? "");
5814
+ }
5815
+ var init_command_args = __esm({
5816
+ "../agent/src/import/command-args.ts"() {
5817
+ "use strict";
5818
+ }
5819
+ });
5820
+
5821
+ // ../agent/src/import/command-registry.ts
5822
+ function buildCommandRegistry(input) {
5823
+ const builtins = new Set(input.builtins.map((b2) => b2.toLowerCase()));
5824
+ const commands = [];
5825
+ const skipped = [];
5826
+ const shadowed = [];
5827
+ const accepted = /* @__PURE__ */ new Map();
5828
+ const lists = [["repo", input.repo], ["user", input.user]];
5829
+ for (const [scope, defs] of lists) {
5830
+ for (const def of defs) {
5831
+ const name = def.name.toLowerCase();
5832
+ if (!VALID.test(def.name)) skipped.push({ name: def.name, scope, reason: "invalid name" });
5833
+ else if (builtins.has(name)) skipped.push({ name, scope, reason: `collides with built-in /${name}` });
5834
+ else if (accepted.get(name) === scope) skipped.push({ name, scope, reason: "duplicate name" });
5835
+ else if (accepted.get(name) === "repo") shadowed.push(name);
5836
+ else {
5837
+ accepted.set(name, scope);
5838
+ commands.push({ ...def, name, scope });
5839
+ }
5840
+ }
5841
+ }
5842
+ commands.sort((a, b2) => a.name < b2.name ? -1 : a.name > b2.name ? 1 : 0);
5843
+ return { commands, skipped, shadowed };
5844
+ }
5845
+ var VALID;
5846
+ var init_command_registry = __esm({
5847
+ "../agent/src/import/command-registry.ts"() {
5848
+ "use strict";
5849
+ VALID = /^[A-Za-z][A-Za-z0-9_-]*$/;
5850
+ }
5851
+ });
5852
+
5722
5853
  // ../agent/src/index.ts
5723
5854
  var init_src = __esm({
5724
5855
  "../agent/src/index.ts"() {
@@ -5759,6 +5890,10 @@ var init_src = __esm({
5759
5890
  init_trust();
5760
5891
  init_imports_config();
5761
5892
  init_permission_rules();
5893
+ init_command_args();
5894
+ init_command_file();
5895
+ init_command_registry();
5896
+ init_claude_sources();
5762
5897
  }
5763
5898
  });
5764
5899
 
@@ -6027,6 +6162,13 @@ ${USAGE}`);
6027
6162
  return 1;
6028
6163
  }
6029
6164
  const client = createProfileClient({ baseUrl: backendUrl(deps.env), apiKey, fetch: deps.fetch });
6165
+ const TENANT_ASSET_CAP = 200;
6166
+ let assetCount = 0;
6167
+ try {
6168
+ assetCount = (await client.index())?.assets.length ?? 0;
6169
+ } catch (err) {
6170
+ deps.log(` note: could not read the profile asset count (${err instanceof Error ? err.message : String(err)}); the server still enforces the cap`);
6171
+ }
6030
6172
  let failed = report.errors.length > 0;
6031
6173
  let imported = 0;
6032
6174
  let skipped = 0;
@@ -6049,6 +6191,11 @@ ${USAGE}`);
6049
6191
  deps.log(` unchanged ${a.kind}/${a.name}`);
6050
6192
  continue;
6051
6193
  }
6194
+ if (!existing && assetCount >= TENANT_ASSET_CAP) {
6195
+ failed = true;
6196
+ deps.log(` SKIPPED ${a.kind}/${a.name}: the profile already holds ${assetCount} of ${TENANT_ASSET_CAP} allowed assets`);
6197
+ continue;
6198
+ }
6052
6199
  if (existing && !overwrite) {
6053
6200
  failed = true;
6054
6201
  deps.log(` DIFFERS ${a.kind}/${a.name}: the profile already has a different version; re-run with --overwrite to replace it`);
@@ -6056,6 +6203,7 @@ ${USAGE}`);
6056
6203
  }
6057
6204
  await client.put(a.kind, a.name, { content: a.content, description: a.description, baseVersion: existing?.version ?? 0 });
6058
6205
  imported++;
6206
+ if (!existing) assetCount++;
6059
6207
  deps.log(` imported ${a.kind}/${a.name}`);
6060
6208
  } catch (err) {
6061
6209
  failed = true;
@@ -39350,12 +39498,12 @@ var require_utils = __commonJS({
39350
39498
  return str;
39351
39499
  }
39352
39500
  var codeCache = {};
39353
- function addToCodeCache(name, on, off) {
39501
+ function addToCodeCache(name, on, off2) {
39354
39502
  on = "\x1B[" + on + "m";
39355
- off = "\x1B[" + off + "m";
39503
+ off2 = "\x1B[" + off2 + "m";
39356
39504
  codeCache[on] = { set: name, to: true };
39357
- codeCache[off] = { set: name, to: false };
39358
- codeCache[name] = { on, off };
39505
+ codeCache[off2] = { set: name, to: false };
39506
+ codeCache[name] = { on, off: off2 };
39359
39507
  }
39360
39508
  addToCodeCache("bold", 1, 22);
39361
39509
  addToCodeCache("italics", 3, 23);
@@ -41237,7 +41385,7 @@ var require_core = __commonJS({
41237
41385
  return match && match.index === 0;
41238
41386
  }
41239
41387
  var BACKREF_RE = /\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;
41240
- function join7(regexps, separator = "|") {
41388
+ function join8(regexps, separator = "|") {
41241
41389
  let numCaptures = 0;
41242
41390
  return regexps.map((regex2) => {
41243
41391
  numCaptures += 1;
@@ -41541,7 +41689,7 @@ var require_core = __commonJS({
41541
41689
  this.exec = () => null;
41542
41690
  }
41543
41691
  const terminators = this.regexes.map((el) => el[1]);
41544
- this.matcherRe = langRe(join7(terminators), true);
41692
+ this.matcherRe = langRe(join8(terminators), true);
41545
41693
  this.lastIndex = 0;
41546
41694
  }
41547
41695
  /** @param {string} s */
@@ -100343,6 +100491,7 @@ var init_slash_commands = __esm({
100343
100491
  { name: "compact", description: "Summarize the conversation to free up context" },
100344
100492
  { name: "model", description: "List gateway models; /model <n|id> switches for this session" },
100345
100493
  { name: "turns", description: "Browse earlier turns and open their steps (shortcut: ctrl+o)" },
100494
+ { name: "commands", description: "List the slash commands imported from Claude Code" },
100346
100495
  { name: "tools", description: "List the tools available to the agent" },
100347
100496
  { name: "exit", description: "Quit the REPL (alias: /quit)" },
100348
100497
  { name: "quit", description: "Quit the REPL" }
@@ -100351,15 +100500,15 @@ var init_slash_commands = __esm({
100351
100500
  });
100352
100501
 
100353
100502
  // src/components/PromptEditor.tsx
100354
- function validateSubmission(text) {
100503
+ function validateSubmission(text, extra = []) {
100355
100504
  const slash = parseSlashCommand(text);
100356
- if (slash && !isKnownSlashCommand(slash.name)) return `Unknown command: /${slash.name}. Type /help for a list.`;
100505
+ if (slash && !isKnownSlashCommand(slash.name) && !extra.some((c) => c.name === slash.name)) return `Unknown command: /${slash.name}. Type /help for a list.`;
100357
100506
  return null;
100358
100507
  }
100359
100508
  function withCursor(row, col) {
100360
100509
  return `${row.slice(0, col)}\x1B[7m${row[col] ?? " "}\x1B[27m${row.slice(col + 1)}`;
100361
100510
  }
100362
- function PromptEditor({ onSubmit, disabled, history, listFiles: listFiles2, onTab, locked = false, promptGlyph: promptGlyph2 = ">", onEmptyEnter, unicode = true, fullWidth = false }) {
100511
+ function PromptEditor({ onSubmit, disabled, history, listFiles: listFiles2, onTab, locked = false, promptGlyph: promptGlyph2 = ">", onEmptyEnter, unicode = true, fullWidth = false, extraCommands = [] }) {
100363
100512
  const { exit } = use_app_default();
100364
100513
  const { columns } = useTerminalSize();
100365
100514
  const { isRawModeSupported } = use_stdin_default();
@@ -100462,7 +100611,7 @@ function PromptEditor({ onSubmit, disabled, history, listFiles: listFiles2, onTa
100462
100611
  else update(applyEdit(cur, { type: "delete" }));
100463
100612
  return;
100464
100613
  case "tab": {
100465
- const completed = completeSlash(cur.text, slashMatches(cur.text, SLASH_COMMANDS));
100614
+ const completed = completeSlash(cur.text, slashMatches(cur.text, [...SLASH_COMMANDS, ...extraCommands]));
100466
100615
  if (completed) update(createBuffer(completed));
100467
100616
  else onTab?.();
100468
100617
  return;
@@ -100495,7 +100644,7 @@ function PromptEditor({ onSubmit, disabled, history, listFiles: listFiles2, onTa
100495
100644
  return;
100496
100645
  }
100497
100646
  const text = inShell ? `!${typed}` : typed;
100498
- const problem = inShell ? null : validateSubmission(text);
100647
+ const problem = inShell ? null : validateSubmission(text, extraCommands);
100499
100648
  if (problem) {
100500
100649
  setHint(problem);
100501
100650
  return;
@@ -100515,7 +100664,7 @@ function PromptEditor({ onSubmit, disabled, history, listFiles: listFiles2, onTa
100515
100664
  }
100516
100665
  const rows = lines(buf);
100517
100666
  const { line, col } = lineCol(buf);
100518
- const matches = shell ? [] : slashMatches(buf.text, SLASH_COMMANDS);
100667
+ const matches = shell ? [] : slashMatches(buf.text, [...SLASH_COMMANDS, ...extraCommands]);
100519
100668
  const picks = mention && files ? rankPaths(files, mention.query, PICKER_ROWS) : [];
100520
100669
  const pickAt = Math.min(sel, Math.max(picks.length - 1, 0));
100521
100670
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(Box_default, { flexDirection: "column", children: [
@@ -101189,7 +101338,7 @@ var init_node_mentions = __esm({
101189
101338
 
101190
101339
  // src/import-gate.ts
101191
101340
  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";
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";
101193
101342
  import { dirname as dirname3, join as join4 } from "node:path";
101194
101343
  async function readOrNull(path12) {
101195
101344
  try {
@@ -101206,6 +101355,24 @@ async function writeTextAtomic(path12, text) {
101206
101355
  await chmod3(tmp, 384);
101207
101356
  await rename3(tmp, path12);
101208
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
+ }
101209
101376
  function nodeTrustFs() {
101210
101377
  return { readText: readOrNull, writeTextAtomic, realPath: (p) => realpath2(p) };
101211
101378
  }
@@ -101226,11 +101393,7 @@ async function importGateStatus(deps) {
101226
101393
  lines2.push(`imports: not read (${err instanceof Error ? err.message : String(err)}); every category off`);
101227
101394
  }
101228
101395
  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
- }
101396
+ const files = await readRepoSources(deps.cwd);
101234
101397
  const found = Object.keys(files);
101235
101398
  if (found.length === 0) return lines2;
101236
101399
  const status = await createNodeTrustStore(deps.configDir).repoStatus(deps.cwd, files);
@@ -101277,11 +101440,7 @@ async function loadPermissionRules(deps) {
101277
101440
  const lines2 = [];
101278
101441
  const files = [{ scope: "user", path: join5(deps.homeDir, ".claude", "settings.json") }];
101279
101442
  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
- }
101443
+ const sources2 = await readRepoSources(deps.cwd);
101285
101444
  if (sources2[".claude/settings.json"] !== void 0 || sources2[".claude/settings.local.json"] !== void 0) {
101286
101445
  const status = await createNodeTrustStore(deps.configDir).repoStatus(deps.cwd, sources2);
101287
101446
  if (status === "trusted" || status === "session") {
@@ -101326,10 +101485,98 @@ var init_permission_rules_loader = __esm({
101326
101485
  }
101327
101486
  });
101328
101487
 
101488
+ // src/command-loader.ts
101489
+ import { join as join6 } from "node:path";
101490
+ async function userCommands(deps, lines2) {
101491
+ try {
101492
+ const apiKey = await resolveApiKey({ env: deps.env, readFile: deps.readFile, homedir: deps.homedir });
101493
+ if (!apiKey) {
101494
+ lines2.push("commands: user commands NOT loaded (not signed in)");
101495
+ return [];
101496
+ }
101497
+ const client = createProfileClient({ baseUrl: backendUrl(deps.env), apiKey, fetch: deps.fetch, timeoutMs: 3e3 });
101498
+ const index = await client.index();
101499
+ const entries = (index?.assets ?? []).filter((a) => a.kind === "command");
101500
+ const defs = [];
101501
+ await Promise.all(
101502
+ entries.map(async (e) => {
101503
+ try {
101504
+ const asset = await client.get("command", e.name);
101505
+ if (asset) defs.push({ name: e.name, ...parseCommandFile(asset.content) });
101506
+ } catch (err) {
101507
+ lines2.push(`commands: ${e.name} NOT loaded (${err instanceof Error ? err.message : String(err)})`);
101508
+ }
101509
+ })
101510
+ );
101511
+ return defs;
101512
+ } catch (err) {
101513
+ lines2.push(`commands: user commands NOT loaded (${err instanceof Error ? err.message : String(err)})`);
101514
+ return [];
101515
+ }
101516
+ }
101517
+ async function repoCommands(deps, lines2) {
101518
+ try {
101519
+ const sources = await readRepoSources(deps.cwd);
101520
+ const names = Object.keys(sources).filter((k2) => k2.startsWith(".claude/commands/"));
101521
+ if (names.length === 0) return [];
101522
+ const status = await createNodeTrustStore(deps.configDir).repoStatus(deps.cwd, sources);
101523
+ if (status !== "trusted" && status !== "session") {
101524
+ lines2.push(`commands: ${names.length} repo command(s) NOT loaded (${status === "never" ? "marked never trust" : "untrusted repo"})`);
101525
+ return [];
101526
+ }
101527
+ const defs = [];
101528
+ for (const key of names) {
101529
+ const text = sources[key];
101530
+ const name = key.slice(".claude/commands/".length, -3);
101531
+ if (Buffer.byteLength(text, "utf8") > MAX_ASSET_BYTES) {
101532
+ lines2.push(`commands: repo command ${name} skipped (over ${MAX_ASSET_BYTES} bytes)`);
101533
+ continue;
101534
+ }
101535
+ defs.push({ name, ...parseCommandFile(text) });
101536
+ }
101537
+ return defs;
101538
+ } catch (err) {
101539
+ lines2.push(`commands: repo commands NOT loaded (${err instanceof Error ? err.message : String(err)})`);
101540
+ return [];
101541
+ }
101542
+ }
101543
+ async function loadCommands(deps) {
101544
+ const configPath = join6(deps.configDir, "imports.json");
101545
+ try {
101546
+ const loaded = parseImportsConfig(await deps.readFile(configPath).catch((err) => {
101547
+ if (err.code === "ENOENT") return null;
101548
+ throw err;
101549
+ }), configPath);
101550
+ if (loaded.error) return off("commands: off (imports.json unreadable)");
101551
+ if (!loaded.config.categories.commands) return off("commands: off (imports.json has commands off)");
101552
+ } catch (err) {
101553
+ return off(`commands: off (imports.json not read: ${err instanceof Error ? err.message : String(err)})`);
101554
+ }
101555
+ const lines2 = [];
101556
+ const [user, repo] = await Promise.all([userCommands(deps, lines2), repoCommands(deps, lines2)]);
101557
+ const reg = buildCommandRegistry({ builtins: deps.builtins, user, repo });
101558
+ const nUser = reg.commands.filter((c) => c.scope === "user").length;
101559
+ const nRepo = reg.commands.length - nUser;
101560
+ lines2.unshift(`commands: ${nUser} user, ${nRepo} repo${reg.shadowed.length ? `; repo shadows user: ${reg.shadowed.map((n) => "/" + n).join(", ")}` : ""}`);
101561
+ for (const s of reg.skipped) lines2.push(`commands: skipped [${s.scope}] /${s.name} (${s.reason})`);
101562
+ return { commands: reg.commands, statusLines: lines2 };
101563
+ }
101564
+ var off;
101565
+ var init_command_loader = __esm({
101566
+ "src/command-loader.ts"() {
101567
+ "use strict";
101568
+ init_src();
101569
+ init_api_key();
101570
+ init_import_gate();
101571
+ init_profile_runtime();
101572
+ off = (line) => ({ commands: [], statusLines: [line] });
101573
+ }
101574
+ });
101575
+
101329
101576
  // src/useReplController.ts
101330
101577
  import { readFile as readFile6, writeFile as writeFile4, mkdir as mkdir4 } from "node:fs/promises";
101331
101578
  import { homedir as homedir2 } from "node:os";
101332
- import { join as join6 } from "node:path";
101579
+ import { join as join7 } from "node:path";
101333
101580
  import { execFile as execFile3 } from "node:child_process";
101334
101581
  function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mentionsOpt, sessions, resumeLatest: resumeLatest2, onViewTurns, onViewResume, isEscOwned }) {
101335
101582
  const { exit } = use_app_default();
@@ -101372,6 +101619,27 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
101372
101619
  const pendingRef = (0, import_react30.useRef)(null);
101373
101620
  const toolsetRef = (0, import_react30.useRef)(null);
101374
101621
  const rulesRef = (0, import_react30.useRef)(void 0);
101622
+ const [commandsLoaded, setCommandsLoaded] = (0, import_react30.useState)({ commands: [], statusLines: [] });
101623
+ (0, import_react30.useEffect)(() => {
101624
+ let live = true;
101625
+ loadCommands({
101626
+ configDir: process.env.SPACECODE_CONFIG_DIR?.trim() || join7(homedir2(), ".spacecode"),
101627
+ cwd: process.cwd(),
101628
+ env: process.env,
101629
+ readFile: (p) => readFile6(p, "utf-8"),
101630
+ homedir: homedir2,
101631
+ fetch,
101632
+ builtins: SLASH_COMMANDS.map((c) => c.name)
101633
+ }).then((r) => {
101634
+ if (live) setCommandsLoaded(r);
101635
+ }, (err) => {
101636
+ if (live) setCommandsLoaded({ commands: [], statusLines: [`commands: NOT loaded (${err instanceof Error ? err.message : String(err)})`] });
101637
+ });
101638
+ return () => {
101639
+ live = false;
101640
+ };
101641
+ }, []);
101642
+ const commandEntries = (0, import_react30.useMemo)(() => commandsLoaded.commands.map((c) => ({ name: c.name, description: c.description })), [commandsLoaded]);
101375
101643
  (0, import_react30.useEffect)(() => {
101376
101644
  sessionTokensRef.current = sessionTokens;
101377
101645
  }, [sessionTokens]);
@@ -101494,8 +101762,15 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
101494
101762
  }
101495
101763
  })();
101496
101764
  }, [resumeLatest2, sessions, projectRoot, notice, applySession]);
101497
- const submit = (0, import_react30.useCallback)(async (prompt) => {
101498
- const shellCommand = shellCommandOf(prompt);
101765
+ const submit = (0, import_react30.useCallback)(async (rawPrompt) => {
101766
+ const typed = parseSlashCommand(rawPrompt);
101767
+ const custom = typed && !isKnownSlashCommand(typed.name) ? commandsLoaded.commands.find((c) => c.name === typed.name) : void 0;
101768
+ const prompt = custom ? expandCommand(custom.body, typed.args) : rawPrompt;
101769
+ if (custom && prompt.trim() === "") {
101770
+ notice(`/${custom.name} expands to an empty prompt.`);
101771
+ return;
101772
+ }
101773
+ const shellCommand = custom ? null : shellCommandOf(prompt);
101499
101774
  if (shellCommand !== null) {
101500
101775
  if (shellCommand === "") {
101501
101776
  notice("Type a command after !");
@@ -101529,7 +101804,7 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
101529
101804
  }
101530
101805
  return;
101531
101806
  }
101532
- const slash = parseSlashCommand(prompt);
101807
+ const slash = custom ? null : parseSlashCommand(prompt);
101533
101808
  if (slash && isKnownSlashCommand(slash.name)) {
101534
101809
  switch (slash.name) {
101535
101810
  case "exit":
@@ -101567,7 +101842,7 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
101567
101842
  maxContextTokens: DEFAULT_MAX_CONTEXT_TOKENS,
101568
101843
  tokens: sessionTokensRef.current,
101569
101844
  costUsd: estimateCostUsd(sessionTokensRef.current.input, sessionTokensRef.current.output),
101570
- profileLines: profileRef.current?.statusLines ?? ["profile: not loaded yet (loads on your first message)"]
101845
+ profileLines: [...profileRef.current?.statusLines ?? ["profile: not loaded yet (loads on your first message)"], ...commandsLoaded.statusLines]
101571
101846
  }));
101572
101847
  return;
101573
101848
  case "resume": {
@@ -101672,6 +101947,13 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
101672
101947
  if (onViewTurns) onViewTurns();
101673
101948
  else notice("The turn viewer is not available here.");
101674
101949
  return;
101950
+ case "commands": {
101951
+ const list2 = commandsLoaded.commands;
101952
+ notice(list2.length === 0 ? `No imported commands.
101953
+ ${commandsLoaded.statusLines.map((l) => ` ${l}`).join("\n")}`.trim() : `Imported commands:
101954
+ ${list2.map((c) => ` /${c.name} (${c.scope})${c.description ? ` ${c.description}` : ""}`).join("\n")}`);
101955
+ return;
101956
+ }
101675
101957
  case "tools": {
101676
101958
  const tools = toolsetRef.current?.tools ?? createLocalToolset({ cwd: process.cwd(), permissions, onTodos: setTodos }).tools;
101677
101959
  notice(`Tools:
@@ -101734,7 +102016,7 @@ ${tools.map((t) => ` ${t.name}`).join("\n")}`);
101734
102016
  cwd: process.cwd(),
101735
102017
  gitRoot: gitTopLevel,
101736
102018
  importGate: async () => {
101737
- const configDir = process.env.SPACECODE_CONFIG_DIR?.trim() || join6(homedir2(), ".spacecode");
102019
+ const configDir = process.env.SPACECODE_CONFIG_DIR?.trim() || join7(homedir2(), ".spacecode");
101738
102020
  const gate = await importGateStatus({ configDir, cwd: process.cwd(), interactive: Boolean(process.stdin.isTTY) });
101739
102021
  const loaded = await loadPermissionRules({ configDir, homeDir: homedir2(), cwd: process.cwd() });
101740
102022
  rulesRef.current = loaded.rules;
@@ -101806,7 +102088,7 @@ ${tools.map((t) => ` ${t.name}`).join("\n")}`);
101806
102088
  abortRef.current = null;
101807
102089
  setIsRunning(false);
101808
102090
  }
101809
- }, [repoName2, history, exit, permissions, notice, refuseAllConfirms, commitTurn, mentions, createToolset, sessions, projectRoot, persist, applySession, model, onViewTurns, onViewResume, setTodos]);
102091
+ }, [commandsLoaded, repoName2, history, exit, permissions, notice, refuseAllConfirms, commitTurn, mentions, createToolset, sessions, projectRoot, persist, applySession, model, onViewTurns, onViewResume, setTodos]);
101810
102092
  const approxTokens = (0, import_react30.useMemo)(
101811
102093
  () => dropDiscardedEvents(events).reduce((n, e) => n + (e.type === "text" ? estimateTokens(e.content) : e.type === "text_delta" ? estimateTokens(e.text) : 0), 0),
101812
102094
  [events]
@@ -101845,7 +102127,7 @@ ${tools.map((t) => ` ${t.name}`).join("\n")}`);
101845
102127
  }),
101846
102128
  [sessions, applySession]
101847
102129
  );
101848
- return { turns, events, isRunning, livePrompt, turnTokens, notices, pending, mode, todos, stats, submit, retry, mentions, replayed, resume };
102130
+ return { turns, events, isRunning, livePrompt, turnTokens, notices, pending, mode, todos, stats, submit, retry, mentions, replayed, resume, commandEntries };
101849
102131
  }
101850
102132
  var import_react30, gitTopLevel, apiUrl;
101851
102133
  var init_useReplController = __esm({
@@ -101869,6 +102151,7 @@ var init_useReplController = __esm({
101869
102151
  init_profile_runtime();
101870
102152
  init_import_gate();
101871
102153
  init_permission_rules_loader();
102154
+ init_command_loader();
101872
102155
  init_slash_commands();
101873
102156
  init_useEventAccumulator();
101874
102157
  gitTopLevel = () => new Promise((resolve2) => {
@@ -102169,7 +102452,7 @@ function App2({ repoName: repoName2, onboardingMode: onboardingMode2, launchCoun
102169
102452
  const { isRawModeSupported } = use_stdin_default();
102170
102453
  const [resumeFound, setResumeFound] = (0, import_react34.useState)(null);
102171
102454
  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 });
102455
+ 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
102456
  const { columns, rows } = useTerminalSize();
102174
102457
  const viewing = viewingRaw && !pending;
102175
102458
  const picking = resumeFound !== null && !pending;
@@ -102264,7 +102547,7 @@ function App2({ repoName: repoName2, onboardingMode: onboardingMode2, launchCoun
102264
102547
  ] }),
102265
102548
  isRunning && !pending && stats.elapsedMs !== null && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Text, { ...paint("secondary"), children: formatThinking(stats.elapsedMs, stats.approxTokens, unicode) }),
102266
102549
  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) }),
102550
+ /* @__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
102551
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Text, { ...footer.level === "critical" ? paint("delete") : footer.level === "warn" ? paint("needsYou") : paint("secondary"), children: footer.text }),
102269
102552
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Text, { ...paint("secondary"), children: formatHints({ running: isRunning, asking: Boolean(pending), columns }, activeGlyphs().sep) })
102270
102553
  ] })
@@ -102310,7 +102593,7 @@ function App2({ repoName: repoName2, onboardingMode: onboardingMode2, launchCoun
102310
102593
  pending && !waitingStep && promptNode,
102311
102594
  isRunning && !pending && stats.elapsedMs !== null && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Text, { ...paint("secondary"), children: formatThinking(stats.elapsedMs, stats.approxTokens, activeGlyphs().rail === "\u2502") }),
102312
102595
  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) }),
102596
+ /* @__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
102597
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Text, { ...footer.level === "critical" ? paint("delete") : footer.level === "warn" ? paint("needsYou") : paint("secondary"), children: footer.text }),
102315
102598
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Text, { ...paint("secondary"), children: formatHints({ running: isRunning, asking: Boolean(pending), columns }, activeGlyphs().sep) })
102316
102599
  ] })
@@ -102453,7 +102736,7 @@ function clearOnResize(stdout) {
102453
102736
  var import_jsx_runtime14 = __toESM(require_jsx_runtime(), 1);
102454
102737
  if (process.argv[2] === "import-claude") {
102455
102738
  const { runImportClaude: runImportClaude2 } = await Promise.resolve().then(() => (init_import_claude(), import_claude_exports));
102456
- const { readFile: readFile7, readdir: readdir3 } = await import("node:fs/promises");
102739
+ const { readFile: readFile7, readdir: readdir4 } = await import("node:fs/promises");
102457
102740
  const { execFile: execFile4 } = await import("node:child_process");
102458
102741
  const gitRoot = () => new Promise((resolve2) => {
102459
102742
  execFile4("git", ["rev-parse", "--show-toplevel"], { cwd: process.cwd() }, (err, stdout) => resolve2(err ? null : stdout.trim() || null));
@@ -102461,7 +102744,7 @@ if (process.argv[2] === "import-claude") {
102461
102744
  const code = await runImportClaude2({
102462
102745
  argv: process.argv.slice(2),
102463
102746
  env: process.env,
102464
- fs: { readFile: (p) => readFile7(p, "utf-8"), readdir: (p) => readdir3(p) },
102747
+ fs: { readFile: (p) => readFile7(p, "utf-8"), readdir: (p) => readdir4(p) },
102465
102748
  readFile: (p) => readFile7(p, "utf-8"),
102466
102749
  homedir: os5.homedir,
102467
102750
  cwd: process.cwd(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spacecode",
3
- "version": "2.5.3",
3
+ "version": "2.5.4",
4
4
  "description": "SpaceCode - launch cloud dev environments with AI coding agents",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",