spacecode 2.5.2 → 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
@@ -4580,9 +4580,22 @@ function guardHost(inner, opts) {
4580
4580
  await log(kind, subject, d, false);
4581
4581
  throw new PolicyViolation(d.rule, `Blocked by policy (${d.rule}): ${d.reason}`);
4582
4582
  }
4583
+ const verdict = opts.rules?.evaluate(kind === "exec" ? "bash" : kind, subject);
4584
+ const ruleTag = verdict?.rule ? `${verdict.rule} [${verdict.scope}]` : "";
4585
+ if (verdict?.effect === "deny") {
4586
+ d = { action: "deny", rule: "rule-deny", reason: `denied by imported rule ${ruleTag}` };
4587
+ await log(kind, subject, d, false);
4588
+ throw new PolicyViolation(d.rule, `Blocked by policy (${d.rule}): ${d.reason}`);
4589
+ }
4583
4590
  if (kind !== "exec" && opts.permissions?.mode === "default") {
4584
4591
  d = { action: "confirm", rule: "confirm-edit", reason: "modifies a file" };
4585
4592
  }
4593
+ if (verdict?.effect === "ask") {
4594
+ d = { action: "confirm", rule: "rule-ask", reason: `imported rule ${ruleTag} asks first${opts.confirm ? "" : "; no way to ask (headless), so refused"}` };
4595
+ } else if (verdict?.effect === "allow" && d.action === "confirm") {
4596
+ await log(kind, subject, { action: "allow", rule: "rule-allow", reason: `allowed by imported rule ${ruleTag} (would be ${d.rule})` }, true);
4597
+ return;
4598
+ }
4586
4599
  if (d.action === "confirm" && kind === "exec" && opts.permissions?.allowsCommand(subject)) {
4587
4600
  await log(kind, subject, { action: "allow", rule: "allow-session", reason: "approved earlier this session" }, true);
4588
4601
  return;
@@ -4606,7 +4619,15 @@ function guardHost(inner, opts) {
4606
4619
  return {
4607
4620
  kind: inner.kind,
4608
4621
  files: {
4609
- readFile: (p) => inner.files.readFile(p),
4622
+ readFile: async (p) => {
4623
+ const v2 = opts.rules?.evaluate("read_file", p.replace(/^\.\//, ""));
4624
+ if (v2?.effect === "deny") {
4625
+ const d = { action: "deny", rule: "rule-deny", reason: `denied by imported rule ${v2.rule} [${v2.scope}]` };
4626
+ await log("read_file", p, d, false);
4627
+ throw new PolicyViolation(d.rule, `Blocked by policy (${d.rule}): ${d.reason}`);
4628
+ }
4629
+ return inner.files.readFile(p);
4630
+ },
4610
4631
  listDirectory: (p) => inner.files.listDirectory(p),
4611
4632
  writeFile: async (p, c) => {
4612
4633
  await gate("write_file", p, allow, { oldText: "", newText: c });
@@ -4904,6 +4925,30 @@ var init_summarizer = __esm({
4904
4925
  }
4905
4926
  });
4906
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
+
4907
4952
  // ../agent/src/import/claude-sources.ts
4908
4953
  function isEnoent(err) {
4909
4954
  return typeof err === "object" && err !== null && err.code === "ENOENT";
@@ -4941,6 +4986,7 @@ async function readClaudeAssets({ fs: fs6, home, projectDirs }) {
4941
4986
  report.assets.push({ kind: "instructions", name: "global", description: "Global CLAUDE.md", content: globalText, source: globalPath });
4942
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");
4943
4988
  }
4989
+ await readCommands(fs6, home, report);
4944
4990
  for (const dir of projectDirs) {
4945
4991
  const memDir = `${home}/.claude/projects/${claudeProjectSlug(dir)}/memory`;
4946
4992
  let entries;
@@ -4982,11 +5028,49 @@ async function readClaudeAssets({ fs: fs6, home, projectDirs }) {
4982
5028
  report.notes.push("No Claude Code memory directory found for this project");
4983
5029
  return report;
4984
5030
  }
4985
- 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;
4986
5067
  var init_claude_sources = __esm({
4987
5068
  "../agent/src/import/claude-sources.ts"() {
4988
5069
  "use strict";
5070
+ init_command_file();
5071
+ MAX_ASSET_BYTES = 256 * 1024;
4989
5072
  NAME_RE = /^[A-Za-z0-9._-]{1,100}$/;
5073
+ COMMAND_NAME_RE = /^[A-Za-z][A-Za-z0-9_-]*$/;
4990
5074
  }
4991
5075
  });
4992
5076
 
@@ -5572,6 +5656,200 @@ var init_imports_config = __esm({
5572
5656
  }
5573
5657
  });
5574
5658
 
5659
+ // ../agent/src/import/permission-rules.ts
5660
+ function splitCommand(cmd) {
5661
+ const segments2 = [];
5662
+ let cur = "";
5663
+ let quote = null;
5664
+ let compound = false;
5665
+ const flush = () => {
5666
+ const s = cur.trim();
5667
+ if (s) segments2.push(s);
5668
+ cur = "";
5669
+ };
5670
+ for (let i = 0; i < cmd.length; i++) {
5671
+ const ch = cmd[i];
5672
+ if (quote) {
5673
+ cur += ch;
5674
+ if (ch === quote) quote = null;
5675
+ } else if (ch === '"' || ch === "'") {
5676
+ quote = ch;
5677
+ cur += ch;
5678
+ } else if (";&|\n`()<>".includes(ch) || ch === "$" && cmd[i + 1] === "(") {
5679
+ compound = true;
5680
+ flush();
5681
+ } else {
5682
+ cur += ch;
5683
+ }
5684
+ }
5685
+ if (quote) return { compound: true, segments: [cmd.trim()] };
5686
+ flush();
5687
+ return compound ? { compound, segments: segments2 } : { compound, segments: [cmd.trim()] };
5688
+ }
5689
+ function matchesBash(pattern, cmd, effect) {
5690
+ const { compound, segments: segments2 } = splitCommand(cmd);
5691
+ if (compound && effect === "allow") return false;
5692
+ const prefix = pattern.endsWith(":*") ? pattern.slice(0, -2).trim() : null;
5693
+ const hit = (s) => prefix === null ? s === pattern.trim() : s === prefix || s.startsWith(prefix) && /\s/.test(s[prefix.length] ?? "");
5694
+ return segments2.some(hit);
5695
+ }
5696
+ function globToRegExp(glob) {
5697
+ let re3 = "";
5698
+ for (let i = 0; i < glob.length; i++) {
5699
+ const ch = glob[i];
5700
+ if (ch === "*") {
5701
+ if (glob[i + 1] === "*") {
5702
+ re3 += ".*";
5703
+ i++;
5704
+ if (glob[i + 1] === "/") i++;
5705
+ } else re3 += "[^/]*";
5706
+ } else if (ch === "?") re3 += "[^/]";
5707
+ else re3 += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
5708
+ }
5709
+ return new RegExp("^" + re3 + "$");
5710
+ }
5711
+ function compile(text, effect, scope) {
5712
+ const m2 = /^([A-Za-z0-9_-]+)(?:\((.*)\))?$/s.exec(text.trim());
5713
+ if (!m2) return "malformed rule";
5714
+ const [, name, rawArg] = m2;
5715
+ const arg = rawArg === void 0 || rawArg.trim() === "" ? null : rawArg.trim();
5716
+ const tools = name.startsWith("mcp__") ? [name] : TOOL_MAP[name];
5717
+ if (!tools) return `unsupported tool ${name}`;
5718
+ if (arg === null) return { effect, scope, text, tools, arg, test: () => true };
5719
+ if (name === "Bash") return { effect, scope, text, tools, arg, test: (s, e) => matchesBash(arg, s, e) };
5720
+ if (name.startsWith("mcp__")) return "argument patterns are not supported for MCP tools";
5721
+ const re3 = globToRegExp(arg);
5722
+ return { effect, scope, text, tools, arg, test: (s) => re3.test(s) };
5723
+ }
5724
+ function buildRuleSet(sources) {
5725
+ const rules = [];
5726
+ const skipped = [];
5727
+ const counts = {};
5728
+ for (const s of ["user", "repo", "local"]) counts[s] = { allow: 0, deny: 0, ask: 0 };
5729
+ for (const src of sources) {
5730
+ for (const effect of ["deny", "ask", "allow"]) {
5731
+ for (const text of src[effect] ?? []) {
5732
+ const c = typeof text === "string" ? compile(text, effect, src.scope) : "malformed rule";
5733
+ if (typeof c === "string") skipped.push({ text: String(text), scope: src.scope, reason: c });
5734
+ else {
5735
+ rules.push(c);
5736
+ counts[src.scope][effect]++;
5737
+ }
5738
+ }
5739
+ }
5740
+ }
5741
+ const frozen = Object.freeze(rules);
5742
+ return {
5743
+ skipped,
5744
+ counts,
5745
+ evaluate(tool, subject) {
5746
+ for (const effect of ["deny", "ask", "allow"]) {
5747
+ const r = frozen.find((c) => c.effect === effect && c.tools.includes(tool) && c.test(subject, effect));
5748
+ if (r) return { effect, rule: r.text, scope: r.scope };
5749
+ }
5750
+ return { effect: "none" };
5751
+ }
5752
+ };
5753
+ }
5754
+ function defaultModeFor(mode) {
5755
+ switch (mode) {
5756
+ case void 0:
5757
+ case "default":
5758
+ return { mode: "default" };
5759
+ case "acceptEdits":
5760
+ return { mode: "accept-edits" };
5761
+ case "plan":
5762
+ return { mode: "plan" };
5763
+ case "bypassPermissions":
5764
+ return { mode: "yolo" };
5765
+ default:
5766
+ return { mode: null, reason: `unmappable defaultMode "${mode}"` };
5767
+ }
5768
+ }
5769
+ var TOOL_MAP;
5770
+ var init_permission_rules = __esm({
5771
+ "../agent/src/import/permission-rules.ts"() {
5772
+ "use strict";
5773
+ TOOL_MAP = {
5774
+ Bash: ["bash"],
5775
+ Read: ["read_file"],
5776
+ Edit: ["edit_file", "write_file"],
5777
+ Write: ["edit_file", "write_file"],
5778
+ MultiEdit: ["edit_file", "write_file"],
5779
+ Glob: ["list_directory"],
5780
+ Grep: ["search"]
5781
+ };
5782
+ }
5783
+ });
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
+
5575
5853
  // ../agent/src/index.ts
5576
5854
  var init_src = __esm({
5577
5855
  "../agent/src/index.ts"() {
@@ -5611,13 +5889,18 @@ var init_src = __esm({
5611
5889
  init_instructions_tool();
5612
5890
  init_trust();
5613
5891
  init_imports_config();
5892
+ init_permission_rules();
5893
+ init_command_args();
5894
+ init_command_file();
5895
+ init_command_registry();
5896
+ init_claude_sources();
5614
5897
  }
5615
5898
  });
5616
5899
 
5617
5900
  // src/api-key.ts
5618
5901
  import { join } from "node:path";
5619
5902
  async function resolveApiKey(deps) {
5620
- const { env: env3, readFile: readFile6, homedir: homedir3 } = deps;
5903
+ const { env: env3, readFile: readFile7, homedir: homedir3 } = deps;
5621
5904
  for (const name of ["SPACECODE_API_KEY", "ANTHROPIC_API_KEY"]) {
5622
5905
  const value = env3[name]?.trim();
5623
5906
  if (value) return value;
@@ -5626,7 +5909,7 @@ async function resolveApiKey(deps) {
5626
5909
  const file = join(dir, "auth.json");
5627
5910
  let raw;
5628
5911
  try {
5629
- raw = await readFile6(file);
5912
+ raw = await readFile7(file);
5630
5913
  } catch (err) {
5631
5914
  if (err.code === "ENOENT") return null;
5632
5915
  throw new Error(`Cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
@@ -5879,6 +6162,13 @@ ${USAGE}`);
5879
6162
  return 1;
5880
6163
  }
5881
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
+ }
5882
6172
  let failed = report.errors.length > 0;
5883
6173
  let imported = 0;
5884
6174
  let skipped = 0;
@@ -5901,6 +6191,11 @@ ${USAGE}`);
5901
6191
  deps.log(` unchanged ${a.kind}/${a.name}`);
5902
6192
  continue;
5903
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
+ }
5904
6199
  if (existing && !overwrite) {
5905
6200
  failed = true;
5906
6201
  deps.log(` DIFFERS ${a.kind}/${a.name}: the profile already has a different version; re-run with --overwrite to replace it`);
@@ -5908,6 +6203,7 @@ ${USAGE}`);
5908
6203
  }
5909
6204
  await client.put(a.kind, a.name, { content: a.content, description: a.description, baseVersion: existing?.version ?? 0 });
5910
6205
  imported++;
6206
+ if (!existing) assetCount++;
5911
6207
  deps.log(` imported ${a.kind}/${a.name}`);
5912
6208
  } catch (err) {
5913
6209
  failed = true;
@@ -34655,7 +34951,7 @@ var init_source = __esm({
34655
34951
  object.level = options.level === void 0 ? colorLevel : options.level;
34656
34952
  };
34657
34953
  chalkFactory = (options) => {
34658
- const chalk2 = (...strings) => strings.join(" ");
34954
+ const chalk2 = (...strings2) => strings2.join(" ");
34659
34955
  applyOptions(chalk2, options);
34660
34956
  Object.setPrototypeOf(chalk2, createChalk.prototype);
34661
34957
  return chalk2;
@@ -39202,12 +39498,12 @@ var require_utils = __commonJS({
39202
39498
  return str;
39203
39499
  }
39204
39500
  var codeCache = {};
39205
- function addToCodeCache(name, on, off) {
39501
+ function addToCodeCache(name, on, off2) {
39206
39502
  on = "\x1B[" + on + "m";
39207
- off = "\x1B[" + off + "m";
39503
+ off2 = "\x1B[" + off2 + "m";
39208
39504
  codeCache[on] = { set: name, to: true };
39209
- codeCache[off] = { set: name, to: false };
39210
- codeCache[name] = { on, off };
39505
+ codeCache[off2] = { set: name, to: false };
39506
+ codeCache[name] = { on, off: off2 };
39211
39507
  }
39212
39508
  addToCodeCache("bold", 1, 22);
39213
39509
  addToCodeCache("italics", 3, 23);
@@ -41089,7 +41385,7 @@ var require_core = __commonJS({
41089
41385
  return match && match.index === 0;
41090
41386
  }
41091
41387
  var BACKREF_RE = /\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;
41092
- function join6(regexps, separator = "|") {
41388
+ function join8(regexps, separator = "|") {
41093
41389
  let numCaptures = 0;
41094
41390
  return regexps.map((regex2) => {
41095
41391
  numCaptures += 1;
@@ -41393,7 +41689,7 @@ var require_core = __commonJS({
41393
41689
  this.exec = () => null;
41394
41690
  }
41395
41691
  const terminators = this.regexes.map((el) => el[1]);
41396
- this.matcherRe = langRe(join6(terminators), true);
41692
+ this.matcherRe = langRe(join8(terminators), true);
41397
41693
  this.lastIndex = 0;
41398
41694
  }
41399
41695
  /** @param {string} s */
@@ -84320,12 +84616,12 @@ var require_source = __commonJS({
84320
84616
  return openAll + string + closeAll;
84321
84617
  };
84322
84618
  var template;
84323
- var chalkTag = (chalk3, ...strings) => {
84324
- const [firstString] = strings;
84619
+ var chalkTag = (chalk3, ...strings2) => {
84620
+ const [firstString] = strings2;
84325
84621
  if (!isArray(firstString) || !isArray(firstString.raw)) {
84326
- return strings.join(" ");
84622
+ return strings2.join(" ");
84327
84623
  }
84328
- const arguments_ = strings.slice(1);
84624
+ const arguments_ = strings2.slice(1);
84329
84625
  const parts = [firstString.raw[0]];
84330
84626
  for (let i = 1; i < firstString.length; i++) {
84331
84627
  parts.push(
@@ -100195,6 +100491,7 @@ var init_slash_commands = __esm({
100195
100491
  { name: "compact", description: "Summarize the conversation to free up context" },
100196
100492
  { name: "model", description: "List gateway models; /model <n|id> switches for this session" },
100197
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" },
100198
100495
  { name: "tools", description: "List the tools available to the agent" },
100199
100496
  { name: "exit", description: "Quit the REPL (alias: /quit)" },
100200
100497
  { name: "quit", description: "Quit the REPL" }
@@ -100203,15 +100500,15 @@ var init_slash_commands = __esm({
100203
100500
  });
100204
100501
 
100205
100502
  // src/components/PromptEditor.tsx
100206
- function validateSubmission(text) {
100503
+ function validateSubmission(text, extra = []) {
100207
100504
  const slash = parseSlashCommand(text);
100208
- 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.`;
100209
100506
  return null;
100210
100507
  }
100211
100508
  function withCursor(row, col) {
100212
100509
  return `${row.slice(0, col)}\x1B[7m${row[col] ?? " "}\x1B[27m${row.slice(col + 1)}`;
100213
100510
  }
100214
- 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 = [] }) {
100215
100512
  const { exit } = use_app_default();
100216
100513
  const { columns } = useTerminalSize();
100217
100514
  const { isRawModeSupported } = use_stdin_default();
@@ -100314,7 +100611,7 @@ function PromptEditor({ onSubmit, disabled, history, listFiles: listFiles2, onTa
100314
100611
  else update(applyEdit(cur, { type: "delete" }));
100315
100612
  return;
100316
100613
  case "tab": {
100317
- const completed = completeSlash(cur.text, slashMatches(cur.text, SLASH_COMMANDS));
100614
+ const completed = completeSlash(cur.text, slashMatches(cur.text, [...SLASH_COMMANDS, ...extraCommands]));
100318
100615
  if (completed) update(createBuffer(completed));
100319
100616
  else onTab?.();
100320
100617
  return;
@@ -100347,7 +100644,7 @@ function PromptEditor({ onSubmit, disabled, history, listFiles: listFiles2, onTa
100347
100644
  return;
100348
100645
  }
100349
100646
  const text = inShell ? `!${typed}` : typed;
100350
- const problem = inShell ? null : validateSubmission(text);
100647
+ const problem = inShell ? null : validateSubmission(text, extraCommands);
100351
100648
  if (problem) {
100352
100649
  setHint(problem);
100353
100650
  return;
@@ -100367,7 +100664,7 @@ function PromptEditor({ onSubmit, disabled, history, listFiles: listFiles2, onTa
100367
100664
  }
100368
100665
  const rows = lines(buf);
100369
100666
  const { line, col } = lineCol(buf);
100370
- const matches = shell ? [] : slashMatches(buf.text, SLASH_COMMANDS);
100667
+ const matches = shell ? [] : slashMatches(buf.text, [...SLASH_COMMANDS, ...extraCommands]);
100371
100668
  const picks = mention && files ? rankPaths(files, mention.query, PICKER_ROWS) : [];
100372
100669
  const pickAt = Math.min(sel, Math.max(picks.length - 1, 0));
100373
100670
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(Box_default, { flexDirection: "column", children: [
@@ -100673,6 +100970,7 @@ function createLocalToolset(opts) {
100673
100970
  audit: new FileAuditLog(auditFile),
100674
100971
  mode: opts.yolo ? "yolo" : "enforce",
100675
100972
  permissions: opts.permissions,
100973
+ ...opts.rules ? { rules: opts.rules } : {},
100676
100974
  confirm: opts.confirm
100677
100975
  });
100678
100976
  const { handlers: githubHandlers } = createGitHubTools((command) => host.exec(command));
@@ -101040,7 +101338,7 @@ var init_node_mentions = __esm({
101040
101338
 
101041
101339
  // src/import-gate.ts
101042
101340
  import { createHash, randomUUID } from "node:crypto";
101043
- 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";
101044
101342
  import { dirname as dirname3, join as join4 } from "node:path";
101045
101343
  async function readOrNull(path12) {
101046
101344
  try {
@@ -101057,6 +101355,24 @@ async function writeTextAtomic(path12, text) {
101057
101355
  await chmod3(tmp, 384);
101058
101356
  await rename3(tmp, path12);
101059
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
+ }
101060
101376
  function nodeTrustFs() {
101061
101377
  return { readText: readOrNull, writeTextAtomic, realPath: (p) => realpath2(p) };
101062
101378
  }
@@ -101077,11 +101393,7 @@ async function importGateStatus(deps) {
101077
101393
  lines2.push(`imports: not read (${err instanceof Error ? err.message : String(err)}); every category off`);
101078
101394
  }
101079
101395
  try {
101080
- const files = {};
101081
- for (const name of REPO_SOURCE_FILES) {
101082
- const text = await readOrNull(join4(deps.cwd, name));
101083
- if (text !== null) files[name] = text;
101084
- }
101396
+ const files = await readRepoSources(deps.cwd);
101085
101397
  const found = Object.keys(files);
101086
101398
  if (found.length === 0) return lines2;
101087
101399
  const status = await createNodeTrustStore(deps.configDir).repoStatus(deps.cwd, files);
@@ -101104,10 +101416,167 @@ var init_import_gate = __esm({
101104
101416
  }
101105
101417
  });
101106
101418
 
101419
+ // src/permission-rules-loader.ts
101420
+ import { readFile as readFile5 } from "node:fs/promises";
101421
+ import { join as join5 } from "node:path";
101422
+ async function readOrNull2(path12) {
101423
+ try {
101424
+ return await readFile5(path12, "utf8");
101425
+ } catch (err) {
101426
+ if (err.code === "ENOENT") return null;
101427
+ throw err;
101428
+ }
101429
+ }
101430
+ async function loadPermissionRules(deps) {
101431
+ const none = (line) => ({ rules: void 0, defaultMode: void 0, statusLines: [line] });
101432
+ const configPath = join5(deps.configDir, "imports.json");
101433
+ try {
101434
+ const loaded = parseImportsConfig(await readOrNull2(configPath), configPath);
101435
+ if (loaded.error) return none("permission rules: off (imports.json unreadable)");
101436
+ if (!loaded.config.categories.permissions) return none("permission rules: off (imports.json has permissions off)");
101437
+ } catch (err) {
101438
+ return none(`permission rules: off (imports.json not read: ${err instanceof Error ? err.message : String(err)})`);
101439
+ }
101440
+ const lines2 = [];
101441
+ const files = [{ scope: "user", path: join5(deps.homeDir, ".claude", "settings.json") }];
101442
+ try {
101443
+ const sources2 = await readRepoSources(deps.cwd);
101444
+ if (sources2[".claude/settings.json"] !== void 0 || sources2[".claude/settings.local.json"] !== void 0) {
101445
+ const status = await createNodeTrustStore(deps.configDir).repoStatus(deps.cwd, sources2);
101446
+ 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") });
101448
+ } else lines2.push(`permission rules: repo settings NOT loaded (${status === "never" ? "marked never trust" : "untrusted repo"})`);
101449
+ }
101450
+ } catch (err) {
101451
+ lines2.push(`permission rules: repo settings NOT loaded (trust check failed: ${err instanceof Error ? err.message : String(err)})`);
101452
+ }
101453
+ const sources = [];
101454
+ let defaultMode;
101455
+ for (const f of files) {
101456
+ try {
101457
+ const text = await readOrNull2(f.path);
101458
+ if (text === null) continue;
101459
+ const parsed = JSON.parse(text);
101460
+ const perms = parsed?.permissions;
101461
+ if (!perms || typeof perms !== "object") continue;
101462
+ sources.push({ scope: f.scope, allow: strings(perms.allow), deny: strings(perms.deny), ask: strings(perms.ask) });
101463
+ if (typeof perms.defaultMode === "string") {
101464
+ const m2 = defaultModeFor(perms.defaultMode);
101465
+ if (m2.mode) defaultMode = m2.mode;
101466
+ else lines2.push(`permission rules: ${f.path}: ${m2.reason}`);
101467
+ }
101468
+ } catch (err) {
101469
+ lines2.push(`permission rules: ${f.path} NOT loaded (${err instanceof Error ? err.message : String(err)})`);
101470
+ }
101471
+ }
101472
+ const rules = buildRuleSet(sources);
101473
+ const parts = ["user", "repo", "local"].filter((s) => sources.some((x2) => x2.scope === s)).map((s) => `${s} ${rules.counts[s].allow} allow/${rules.counts[s].deny} deny/${rules.counts[s].ask} ask`);
101474
+ lines2.unshift(`permission rules: ${parts.length ? parts.join(", ") : "none found"}${defaultMode ? `; default mode ${defaultMode}` : ""}`);
101475
+ for (const s of rules.skipped) lines2.push(`permission rules: skipped [${s.scope}] "${s.text}" (${s.reason})`);
101476
+ return { rules, defaultMode, statusLines: lines2 };
101477
+ }
101478
+ var strings;
101479
+ var init_permission_rules_loader = __esm({
101480
+ "src/permission-rules-loader.ts"() {
101481
+ "use strict";
101482
+ init_src();
101483
+ init_import_gate();
101484
+ strings = (v2) => Array.isArray(v2) ? v2.filter((x2) => typeof x2 === "string") : void 0;
101485
+ }
101486
+ });
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
+
101107
101576
  // src/useReplController.ts
101108
- import { readFile as readFile5, writeFile as writeFile4, mkdir as mkdir4 } from "node:fs/promises";
101577
+ import { readFile as readFile6, writeFile as writeFile4, mkdir as mkdir4 } from "node:fs/promises";
101109
101578
  import { homedir as homedir2 } from "node:os";
101110
- import { join as join5 } from "node:path";
101579
+ import { join as join7 } from "node:path";
101111
101580
  import { execFile as execFile3 } from "node:child_process";
101112
101581
  function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mentionsOpt, sessions, resumeLatest: resumeLatest2, onViewTurns, onViewResume, isEscOwned }) {
101113
101582
  const { exit } = use_app_default();
@@ -101149,6 +101618,28 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
101149
101618
  const abortRef = (0, import_react30.useRef)(null);
101150
101619
  const pendingRef = (0, import_react30.useRef)(null);
101151
101620
  const toolsetRef = (0, import_react30.useRef)(null);
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]);
101152
101643
  (0, import_react30.useEffect)(() => {
101153
101644
  sessionTokensRef.current = sessionTokens;
101154
101645
  }, [sessionTokens]);
@@ -101201,6 +101692,7 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
101201
101692
  cwd: process.cwd(),
101202
101693
  permissions,
101203
101694
  onTodos: setTodos,
101695
+ ...rulesRef.current ? { rules: rulesRef.current } : {},
101204
101696
  ...profileRef.current && profileRef.current.tools.length > 0 ? { extra: { tools: profileRef.current.tools, handlers: profileRef.current.handlers } } : {},
101205
101697
  confirm: (req) => new Promise((resolve2) => {
101206
101698
  const item = {
@@ -101270,8 +101762,15 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
101270
101762
  }
101271
101763
  })();
101272
101764
  }, [resumeLatest2, sessions, projectRoot, notice, applySession]);
101273
- const submit = (0, import_react30.useCallback)(async (prompt) => {
101274
- 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);
101275
101774
  if (shellCommand !== null) {
101276
101775
  if (shellCommand === "") {
101277
101776
  notice("Type a command after !");
@@ -101305,7 +101804,7 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
101305
101804
  }
101306
101805
  return;
101307
101806
  }
101308
- const slash = parseSlashCommand(prompt);
101807
+ const slash = custom ? null : parseSlashCommand(prompt);
101309
101808
  if (slash && isKnownSlashCommand(slash.name)) {
101310
101809
  switch (slash.name) {
101311
101810
  case "exit":
@@ -101343,7 +101842,7 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
101343
101842
  maxContextTokens: DEFAULT_MAX_CONTEXT_TOKENS,
101344
101843
  tokens: sessionTokensRef.current,
101345
101844
  costUsd: estimateCostUsd(sessionTokensRef.current.input, sessionTokensRef.current.output),
101346
- 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]
101347
101846
  }));
101348
101847
  return;
101349
101848
  case "resume": {
@@ -101400,7 +101899,7 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
101400
101899
  }
101401
101900
  setIsRunning(true);
101402
101901
  try {
101403
- const apiKey2 = await resolveApiKey({ env: process.env, readFile: (p) => readFile5(p, "utf-8"), homedir: homedir2 });
101902
+ const apiKey2 = await resolveApiKey({ env: process.env, readFile: (p) => readFile6(p, "utf-8"), homedir: homedir2 });
101404
101903
  if (!apiKey2) {
101405
101904
  notice(MISSING_KEY_MESSAGE);
101406
101905
  return;
@@ -101422,7 +101921,7 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
101422
101921
  }
101423
101922
  case "model": {
101424
101923
  try {
101425
- const apiKey2 = await resolveApiKey({ env: process.env, readFile: (p) => readFile5(p, "utf-8"), homedir: homedir2 });
101924
+ const apiKey2 = await resolveApiKey({ env: process.env, readFile: (p) => readFile6(p, "utf-8"), homedir: homedir2 });
101426
101925
  if (!apiKey2) {
101427
101926
  notice(MISSING_KEY_MESSAGE);
101428
101927
  return;
@@ -101448,6 +101947,13 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
101448
101947
  if (onViewTurns) onViewTurns();
101449
101948
  else notice("The turn viewer is not available here.");
101450
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
+ }
101451
101957
  case "tools": {
101452
101958
  const tools = toolsetRef.current?.tools ?? createLocalToolset({ cwd: process.cwd(), permissions, onTodos: setTodos }).tools;
101453
101959
  notice(`Tools:
@@ -101464,7 +101970,7 @@ ${tools.map((t) => ` ${t.name}`).join("\n")}`);
101464
101970
  try {
101465
101971
  apiKey = await resolveApiKey({
101466
101972
  env: process.env,
101467
- readFile: (p) => readFile5(p, "utf-8"),
101973
+ readFile: (p) => readFile6(p, "utf-8"),
101468
101974
  homedir: homedir2
101469
101975
  });
101470
101976
  } catch (err) {
@@ -101500,7 +102006,7 @@ ${tools.map((t) => ` ${t.name}`).join("\n")}`);
101500
102006
  try {
101501
102007
  profilePromiseRef.current ??= prepareProfile({
101502
102008
  env: process.env,
101503
- readFile: (p) => readFile5(p, "utf-8"),
102009
+ readFile: (p) => readFile6(p, "utf-8"),
101504
102010
  writeFile: (p, d) => writeFile4(p, d, { encoding: "utf-8", mode: 384 }),
101505
102011
  mkdir: async (p) => {
101506
102012
  await mkdir4(p, { recursive: true });
@@ -101509,11 +102015,14 @@ ${tools.map((t) => ` ${t.name}`).join("\n")}`);
101509
102015
  fetch,
101510
102016
  cwd: process.cwd(),
101511
102017
  gitRoot: gitTopLevel,
101512
- importGate: () => importGateStatus({
101513
- configDir: process.env.SPACECODE_CONFIG_DIR?.trim() || join5(homedir2(), ".spacecode"),
101514
- cwd: process.cwd(),
101515
- interactive: Boolean(process.stdin.isTTY)
101516
- })
102018
+ importGate: async () => {
102019
+ const configDir = process.env.SPACECODE_CONFIG_DIR?.trim() || join7(homedir2(), ".spacecode");
102020
+ const gate = await importGateStatus({ configDir, cwd: process.cwd(), interactive: Boolean(process.stdin.isTTY) });
102021
+ const loaded = await loadPermissionRules({ configDir, homeDir: homedir2(), cwd: process.cwd() });
102022
+ rulesRef.current = loaded.rules;
102023
+ if (loaded.defaultMode && !yolo2) permissions.set(loaded.defaultMode);
102024
+ return [...gate, ...loaded.statusLines];
102025
+ }
101517
102026
  });
101518
102027
  profileRef.current = await profilePromiseRef.current;
101519
102028
  const toolset = createToolset();
@@ -101579,7 +102088,7 @@ ${tools.map((t) => ` ${t.name}`).join("\n")}`);
101579
102088
  abortRef.current = null;
101580
102089
  setIsRunning(false);
101581
102090
  }
101582
- }, [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]);
101583
102092
  const approxTokens = (0, import_react30.useMemo)(
101584
102093
  () => dropDiscardedEvents(events).reduce((n, e) => n + (e.type === "text" ? estimateTokens(e.content) : e.type === "text_delta" ? estimateTokens(e.text) : 0), 0),
101585
102094
  [events]
@@ -101618,7 +102127,7 @@ ${tools.map((t) => ` ${t.name}`).join("\n")}`);
101618
102127
  }),
101619
102128
  [sessions, applySession]
101620
102129
  );
101621
- 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 };
101622
102131
  }
101623
102132
  var import_react30, gitTopLevel, apiUrl;
101624
102133
  var init_useReplController = __esm({
@@ -101641,6 +102150,8 @@ var init_useReplController = __esm({
101641
102150
  init_api_key();
101642
102151
  init_profile_runtime();
101643
102152
  init_import_gate();
102153
+ init_permission_rules_loader();
102154
+ init_command_loader();
101644
102155
  init_slash_commands();
101645
102156
  init_useEventAccumulator();
101646
102157
  gitTopLevel = () => new Promise((resolve2) => {
@@ -101941,7 +102452,7 @@ function App2({ repoName: repoName2, onboardingMode: onboardingMode2, launchCoun
101941
102452
  const { isRawModeSupported } = use_stdin_default();
101942
102453
  const [resumeFound, setResumeFound] = (0, import_react34.useState)(null);
101943
102454
  const openResume = (0, import_react34.useCallback)((found) => setResumeFound(found), []);
101944
- 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 });
101945
102456
  const { columns, rows } = useTerminalSize();
101946
102457
  const viewing = viewingRaw && !pending;
101947
102458
  const picking = resumeFound !== null && !pending;
@@ -102036,7 +102547,7 @@ function App2({ repoName: repoName2, onboardingMode: onboardingMode2, launchCoun
102036
102547
  ] }),
102037
102548
  isRunning && !pending && stats.elapsedMs !== null && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Text, { ...paint("secondary"), children: formatThinking(stats.elapsedMs, stats.approxTokens, unicode) }),
102038
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" }),
102039
- /* @__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) }),
102040
102551
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Text, { ...footer.level === "critical" ? paint("delete") : footer.level === "warn" ? paint("needsYou") : paint("secondary"), children: footer.text }),
102041
102552
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Text, { ...paint("secondary"), children: formatHints({ running: isRunning, asking: Boolean(pending), columns }, activeGlyphs().sep) })
102042
102553
  ] })
@@ -102082,7 +102593,7 @@ function App2({ repoName: repoName2, onboardingMode: onboardingMode2, launchCoun
102082
102593
  pending && !waitingStep && promptNode,
102083
102594
  isRunning && !pending && stats.elapsedMs !== null && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Text, { ...paint("secondary"), children: formatThinking(stats.elapsedMs, stats.approxTokens, activeGlyphs().rail === "\u2502") }),
102084
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" }),
102085
- /* @__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) }),
102086
102597
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Text, { ...footer.level === "critical" ? paint("delete") : footer.level === "warn" ? paint("needsYou") : paint("secondary"), children: footer.text }),
102087
102598
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Text, { ...paint("secondary"), children: formatHints({ running: isRunning, asking: Boolean(pending), columns }, activeGlyphs().sep) })
102088
102599
  ] })
@@ -102225,7 +102736,7 @@ function clearOnResize(stdout) {
102225
102736
  var import_jsx_runtime14 = __toESM(require_jsx_runtime(), 1);
102226
102737
  if (process.argv[2] === "import-claude") {
102227
102738
  const { runImportClaude: runImportClaude2 } = await Promise.resolve().then(() => (init_import_claude(), import_claude_exports));
102228
- const { readFile: readFile6, readdir: readdir3 } = await import("node:fs/promises");
102739
+ const { readFile: readFile7, readdir: readdir4 } = await import("node:fs/promises");
102229
102740
  const { execFile: execFile4 } = await import("node:child_process");
102230
102741
  const gitRoot = () => new Promise((resolve2) => {
102231
102742
  execFile4("git", ["rev-parse", "--show-toplevel"], { cwd: process.cwd() }, (err, stdout) => resolve2(err ? null : stdout.trim() || null));
@@ -102233,8 +102744,8 @@ if (process.argv[2] === "import-claude") {
102233
102744
  const code = await runImportClaude2({
102234
102745
  argv: process.argv.slice(2),
102235
102746
  env: process.env,
102236
- fs: { readFile: (p) => readFile6(p, "utf-8"), readdir: (p) => readdir3(p) },
102237
- readFile: (p) => readFile6(p, "utf-8"),
102747
+ fs: { readFile: (p) => readFile7(p, "utf-8"), readdir: (p) => readdir4(p) },
102748
+ readFile: (p) => readFile7(p, "utf-8"),
102238
102749
  homedir: os5.homedir,
102239
102750
  cwd: process.cwd(),
102240
102751
  gitRoot,