spacecode 2.9.0 → 2.10.1

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
@@ -6019,9 +6019,9 @@ async function sha256(content) {
6019
6019
  const hashArray = new Uint8Array(hashBuffer);
6020
6020
  return Array.from(hashArray).map((b2) => b2.toString(16).padStart(2, "0")).join("");
6021
6021
  }
6022
- async function computeLockfileHash(readFile10) {
6022
+ async function computeLockfileHash(readFile11) {
6023
6023
  for (const name of LOCKFILES) {
6024
- const content = await readFile10(name);
6024
+ const content = await readFile11(name);
6025
6025
  if (content) {
6026
6026
  const hash = await sha256(content);
6027
6027
  return hash.slice(0, 12);
@@ -7252,6 +7252,92 @@ var init_trust = __esm({
7252
7252
  }
7253
7253
  });
7254
7254
 
7255
+ // ../agent/src/import/source-changes.ts
7256
+ function fingerprintSources(files, hash) {
7257
+ const fingerprint = {};
7258
+ for (const [path12, content] of Object.entries(files)) fingerprint[path12] = hash(content);
7259
+ return fingerprint;
7260
+ }
7261
+ function diffSources(before, after) {
7262
+ const has2 = (o, k2) => Object.hasOwn(o, k2);
7263
+ const added = Object.keys(after).filter((k2) => !has2(before, k2)).sort();
7264
+ const removed = Object.keys(before).filter((k2) => !has2(after, k2)).sort();
7265
+ const modified = Object.keys(after).filter((k2) => has2(before, k2) && before[k2] !== after[k2]).sort();
7266
+ return { added, modified, removed, changed: added.length + modified.length + removed.length > 0 };
7267
+ }
7268
+ function formatSourceChanges(c) {
7269
+ return [...c.added.map((p) => `added: ${p}`), ...c.modified.map((p) => `modified: ${p}`), ...c.removed.map((p) => `removed: ${p}`)];
7270
+ }
7271
+ var init_source_changes = __esm({
7272
+ "../agent/src/import/source-changes.ts"() {
7273
+ "use strict";
7274
+ }
7275
+ });
7276
+
7277
+ // ../agent/src/import/mcp-tool-cache.ts
7278
+ function serverCacheKey(def, hash) {
7279
+ return hash(JSON.stringify([def.name, def.command, def.args, [...def.envNames].sort()]));
7280
+ }
7281
+ function cleanTool(raw) {
7282
+ if (!isPlainObject(raw) || typeof raw.name !== "string" || raw.name === "" || typeof raw.description !== "string" || !isPlainObject(raw.inputSchema)) return null;
7283
+ return { name: raw.name, description: raw.description.slice(0, MAX_DESCRIPTION), inputSchema: raw.inputSchema };
7284
+ }
7285
+ function parseToolCache(text) {
7286
+ if (text === null || text === "") return { cache: {} };
7287
+ let raw;
7288
+ try {
7289
+ raw = JSON.parse(text);
7290
+ } catch (err) {
7291
+ if (!(err instanceof SyntaxError)) throw err;
7292
+ return { cache: {}, error: "tool cache ignored: not valid JSON" };
7293
+ }
7294
+ if (!isPlainObject(raw) || raw.version !== 1 || !isPlainObject(raw.servers)) return { cache: {}, error: "tool cache ignored: unsupported format" };
7295
+ const cache4 = {};
7296
+ for (const key of Object.keys(raw.servers)) {
7297
+ if (Object.keys(cache4).length >= MAX_SERVERS) break;
7298
+ const entry = raw.servers[key];
7299
+ if (!isPlainObject(entry) || !Array.isArray(entry.tools)) continue;
7300
+ const tools = [];
7301
+ for (const t of entry.tools) {
7302
+ const clean = cleanTool(t);
7303
+ if (clean) tools.push(clean);
7304
+ if (tools.length >= MAX_TOOLS_PER_SERVER) break;
7305
+ }
7306
+ Object.defineProperty(cache4, key, { value: { tools }, enumerable: true, writable: true, configurable: true });
7307
+ }
7308
+ return { cache: cache4 };
7309
+ }
7310
+ function serializeToolCache(cache4) {
7311
+ return JSON.stringify({ version: 1, servers: cache4 });
7312
+ }
7313
+ function getCachedTools(cache4, key) {
7314
+ return own(cache4, key) ? cache4[key].tools : void 0;
7315
+ }
7316
+ function sameTools(a, b2) {
7317
+ return JSON.stringify(a) === JSON.stringify(b2);
7318
+ }
7319
+ function putCachedTools(cache4, key, tools) {
7320
+ const existing = getCachedTools(cache4, key);
7321
+ if (existing && sameTools(existing, tools)) return cache4;
7322
+ const next = {};
7323
+ for (const k2 of Object.keys(cache4)) if (k2 !== key) Object.defineProperty(next, k2, { value: cache4[k2], enumerable: true, writable: true, configurable: true });
7324
+ Object.defineProperty(next, key, { value: { tools }, enumerable: true, writable: true, configurable: true });
7325
+ const keys = Object.keys(next);
7326
+ for (const k2 of keys.slice(0, Math.max(0, keys.length - MAX_SERVERS))) delete next[k2];
7327
+ return next;
7328
+ }
7329
+ var MAX_TOOLS_PER_SERVER, MAX_SERVERS, MAX_DESCRIPTION, own, isPlainObject;
7330
+ var init_mcp_tool_cache = __esm({
7331
+ "../agent/src/import/mcp-tool-cache.ts"() {
7332
+ "use strict";
7333
+ MAX_TOOLS_PER_SERVER = 200;
7334
+ MAX_SERVERS = 500;
7335
+ MAX_DESCRIPTION = 1024;
7336
+ own = (o, k2) => Object.hasOwn(o, k2);
7337
+ isPlainObject = (v2) => typeof v2 === "object" && v2 !== null && !Array.isArray(v2);
7338
+ }
7339
+ });
7340
+
7255
7341
  // ../agent/src/import/imports-config.ts
7256
7342
  function allOffConfig() {
7257
7343
  return {
@@ -7267,7 +7353,7 @@ function allOffConfig() {
7267
7353
  models: {}
7268
7354
  };
7269
7355
  }
7270
- function isPlainObject(value) {
7356
+ function isPlainObject2(value) {
7271
7357
  return typeof value === "object" && value !== null && !Array.isArray(value);
7272
7358
  }
7273
7359
  function parseImportsConfig(text, path12) {
@@ -7279,11 +7365,11 @@ function parseImportsConfig(text, path12) {
7279
7365
  } catch {
7280
7366
  return fail("invalid JSON");
7281
7367
  }
7282
- if (!isPlainObject(parsed)) return fail("top-level JSON must be an object");
7368
+ if (!isPlainObject2(parsed)) return fail("top-level JSON must be an object");
7283
7369
  if (parsed.version !== 1) return fail(`unsupported version ${String(parsed.version)}`);
7284
7370
  const config = allOffConfig();
7285
7371
  if (parsed.categories !== void 0) {
7286
- if (!isPlainObject(parsed.categories)) return fail("categories must be an object");
7372
+ if (!isPlainObject2(parsed.categories)) return fail("categories must be an object");
7287
7373
  for (const [key, value] of Object.entries(parsed.categories)) {
7288
7374
  if (!IMPORT_CATEGORIES.includes(key)) return fail(`unknown category key "${key}"`);
7289
7375
  if (typeof value !== "boolean") return fail(`category "${key}" must be a boolean`);
@@ -7291,7 +7377,7 @@ function parseImportsConfig(text, path12) {
7291
7377
  }
7292
7378
  }
7293
7379
  if (parsed.models !== void 0) {
7294
- if (!isPlainObject(parsed.models)) return fail("models must be an object");
7380
+ if (!isPlainObject2(parsed.models)) return fail("models must be an object");
7295
7381
  for (const key of MODEL_KEYS) {
7296
7382
  const value = parsed.models[key];
7297
7383
  if (value === void 0) continue;
@@ -7986,6 +8072,7 @@ __export(src_exports, {
7986
8072
  createSkillTool: () => createSkillTool,
7987
8073
  createTodoTool: () => createTodoTool,
7988
8074
  defaultModeFor: () => defaultModeFor,
8075
+ diffSources: () => diffSources,
7989
8076
  estimateTokens: () => estimateTokens,
7990
8077
  estimateTotalTokens: () => estimateTotalTokens,
7991
8078
  expandCommand: () => expandCommand,
@@ -7993,9 +8080,12 @@ __export(src_exports, {
7993
8080
  expandServerEnv: () => expandServerEnv,
7994
8081
  extractRuleLines: () => extractRuleLines,
7995
8082
  findUnsupported: () => findUnsupported,
8083
+ fingerprintSources: () => fingerprintSources,
7996
8084
  flattenMessageText: () => flattenMessageText,
7997
8085
  formatSkillBody: () => formatSkillBody,
8086
+ formatSourceChanges: () => formatSourceChanges,
7998
8087
  formatToolResult: () => formatToolResult,
8088
+ getCachedTools: () => getCachedTools,
7999
8089
  guardHost: () => guardHost,
8000
8090
  isReadOnlyCommand: () => isReadOnlyCommand,
8001
8091
  joinSections: () => joinSections,
@@ -8007,13 +8097,18 @@ __export(src_exports, {
8007
8097
  parseImportsConfig: () => parseImportsConfig,
8008
8098
  parseSkillFile: () => parseSkillFile,
8009
8099
  parseTodos: () => parseTodos,
8100
+ parseToolCache: () => parseToolCache,
8010
8101
  prepareAdaptation: () => prepareAdaptation,
8011
8102
  projectKey: () => projectKey,
8103
+ putCachedTools: () => putCachedTools,
8012
8104
  readClaudeAssets: () => readClaudeAssets,
8013
8105
  renderProjectCard: () => renderProjectCard,
8014
8106
  runAgentLoop: () => runAgentLoop,
8015
8107
  safeSkillPath: () => safeSkillPath,
8108
+ sameTools: () => sameTools,
8016
8109
  serializeImportsConfig: () => serializeImportsConfig,
8110
+ serializeToolCache: () => serializeToolCache,
8111
+ serverCacheKey: () => serverCacheKey,
8017
8112
  siblingAssetName: () => siblingAssetName,
8018
8113
  splitCommand: () => splitCommand,
8019
8114
  splitSections: () => splitSections,
@@ -8060,6 +8155,8 @@ var init_src = __esm({
8060
8155
  init_adapt_plan();
8061
8156
  init_instructions_tool();
8062
8157
  init_trust();
8158
+ init_source_changes();
8159
+ init_mcp_tool_cache();
8063
8160
  init_imports_config();
8064
8161
  init_permission_rules();
8065
8162
  init_command_args();
@@ -8089,7 +8186,7 @@ __export(api_key_exports, {
8089
8186
  });
8090
8187
  import { join } from "node:path";
8091
8188
  async function resolveApiKey(deps) {
8092
- const { env: env3, readFile: readFile10, homedir: homedir3 } = deps;
8189
+ const { env: env3, readFile: readFile11, homedir: homedir3 } = deps;
8093
8190
  for (const name of ["SPACECODE_API_KEY", "ANTHROPIC_API_KEY"]) {
8094
8191
  const value = env3[name]?.trim();
8095
8192
  if (value) return value;
@@ -8098,7 +8195,7 @@ async function resolveApiKey(deps) {
8098
8195
  const file = join(dir, "auth.json");
8099
8196
  let raw;
8100
8197
  try {
8101
- raw = await readFile10(file);
8198
+ raw = await readFile11(file);
8102
8199
  } catch (err) {
8103
8200
  if (err.code === "ENOENT") return null;
8104
8201
  throw new Error(`Cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
@@ -8878,6 +8975,14 @@ var init_mcp_manager = __esm({
8878
8975
  servers = /* @__PURE__ */ new Map();
8879
8976
  turnSignal;
8880
8977
  startPromise = null;
8978
+ primePromise = null;
8979
+ cache = {};
8980
+ /** Cached tools of approved, startable servers, usable until the live list replaces them. */
8981
+ cachedTools = /* @__PURE__ */ new Map();
8982
+ /** Settles when a server has connected or failed, so a call on a cached tool can wait for it. */
8983
+ connected = /* @__PURE__ */ new Map();
8984
+ cacheNotes = [];
8985
+ cacheWrite = Promise.resolve();
8881
8986
  /** Server states in definition order, so output does not depend on which server connected first. */
8882
8987
  ordered() {
8883
8988
  return this.deps.defs.map((d) => this.servers.get(d.name)).filter((s) => s !== void 0);
@@ -8886,11 +8991,86 @@ var init_mcp_manager = __esm({
8886
8991
  beginTurn(signal) {
8887
8992
  this.turnSignal = signal;
8888
8993
  }
8994
+ /**
8995
+ * Loads cached tool lists for servers that could start right now (stdio, approved on this machine, environment set),
8996
+ * without spawning anything. Await it before building the first toolset. Never throws; a bad cache is noted in /status.
8997
+ */
8998
+ prime() {
8999
+ this.primePromise ??= this.primeOnce();
9000
+ return this.primePromise;
9001
+ }
9002
+ async primeOnce() {
9003
+ const tc = this.deps.toolCache;
9004
+ if (!tc) return;
9005
+ try {
9006
+ const parsed = parseToolCache(await tc.read());
9007
+ this.cache = parsed.cache;
9008
+ if (parsed.error) this.cacheNotes.push(parsed.error);
9009
+ } catch (err) {
9010
+ this.cacheNotes.push(`tool cache not read (${err instanceof Error ? err.message : String(err)})`);
9011
+ return;
9012
+ }
9013
+ for (const def of this.deps.defs) {
9014
+ try {
9015
+ if (def.transport !== "stdio") continue;
9016
+ const envNames = approvalEnv(def.env);
9017
+ if (!await this.deps.isStdioApproved({ command: def.command, args: def.args, envNames })) continue;
9018
+ if (!expandServerEnv({ command: def.command, args: def.args, env: def.env }, this.deps.env).ok) continue;
9019
+ const hit = getCachedTools(this.cache, serverCacheKey({ name: def.name, command: def.command, args: def.args, envNames }, tc.hash));
9020
+ if (hit) this.cachedTools.set(def.name, this.usable(def.name, hit).tools);
9021
+ } catch (err) {
9022
+ this.cacheNotes.push(`${def.name}: cached tools skipped (${err instanceof Error ? err.message : String(err)})`);
9023
+ }
9024
+ }
9025
+ }
8889
9026
  /** Starts every eligible server in the background. Resolves when all have connected or failed; callers need not await it. */
8890
9027
  start() {
8891
- this.startPromise ??= Promise.all(this.deps.defs.map((def) => this.startOne(def))).then(() => void 0);
9028
+ this.startPromise ??= Promise.all(this.deps.defs.map((def) => {
9029
+ const p = this.startOne(def);
9030
+ this.connected.set(def.name, p);
9031
+ return p;
9032
+ })).then(() => void 0);
8892
9033
  return this.startPromise;
8893
9034
  }
9035
+ /** Tools whose full names are valid and unique, plus the ones that are not. */
9036
+ usable(server, listed) {
9037
+ const tools = [];
9038
+ const skipped = [];
9039
+ const seen = /* @__PURE__ */ new Set();
9040
+ for (const tool of listed) {
9041
+ const full = mcpToolName(server, tool.name);
9042
+ if (!TOOL_NAME_RE.test(full) || seen.has(full)) skipped.push(tool.name);
9043
+ else {
9044
+ seen.add(full);
9045
+ tools.push(tool);
9046
+ }
9047
+ }
9048
+ return { tools, skipped };
9049
+ }
9050
+ /** Live tools once connected; cached ones while a server is still connecting (or not started yet). */
9051
+ toolsOf(def) {
9052
+ const s = this.servers.get(def.name);
9053
+ if (s?.status === "ready") return s.tools;
9054
+ if (s === void 0 || s.status === "connecting") return this.cachedTools.get(def.name) ?? [];
9055
+ return [];
9056
+ }
9057
+ /** Stores the live list for the next start. Writes are serialized; a failure becomes a /status line. */
9058
+ saveToolCache(def, tools) {
9059
+ const tc = this.deps.toolCache;
9060
+ if (!tc) return Promise.resolve();
9061
+ this.cacheWrite = this.cacheWrite.then(async () => {
9062
+ try {
9063
+ const key = serverCacheKey({ name: def.name, command: def.command, args: def.args, envNames: approvalEnv(def.env) }, tc.hash);
9064
+ const next = putCachedTools(this.cache, key, tools.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })));
9065
+ if (next === this.cache) return;
9066
+ await tc.write(serializeToolCache(next));
9067
+ this.cache = next;
9068
+ } catch (err) {
9069
+ this.cacheNotes.push(`${def.name}: tool list not cached (${err instanceof Error ? err.message : String(err)})`);
9070
+ }
9071
+ });
9072
+ return this.cacheWrite;
9073
+ }
8894
9074
  async startOne(def) {
8895
9075
  const name = def.name;
8896
9076
  const skip = (reason) => void this.servers.set(name, { name, def, status: "skipped", reason });
@@ -8941,19 +9121,11 @@ var init_mcp_manager = __esm({
8941
9121
  });
8942
9122
  await client.initialize();
8943
9123
  const listed = await client.listTools();
8944
- const tools = [];
8945
- const skippedTools = [];
8946
- const seen = /* @__PURE__ */ new Set();
8947
- for (const tool of listed.tools) {
8948
- const full = mcpToolName(name, tool.name);
8949
- if (!TOOL_NAME_RE.test(full) || seen.has(full)) skippedTools.push(tool.name);
8950
- else {
8951
- seen.add(full);
8952
- tools.push(tool);
8953
- }
8954
- }
9124
+ const { tools, skipped: skippedTools } = this.usable(name, listed.tools);
8955
9125
  if (listed.skipped > 0) skippedTools.push(`${listed.skipped} unnamed`);
8956
9126
  this.servers.set(name, { name, def, status: "ready", client, transport, session: session2, tools, skippedTools });
9127
+ this.cachedTools.delete(name);
9128
+ await this.saveToolCache(def, tools);
8957
9129
  } catch (err) {
8958
9130
  await transport?.close().catch(() => void 0);
8959
9131
  fail(err instanceof Error ? err.message : String(err));
@@ -8962,22 +9134,29 @@ var init_mcp_manager = __esm({
8962
9134
  /** Tools of every connected server, as the model sees them. */
8963
9135
  tools() {
8964
9136
  const out = [];
8965
- for (const s of this.ordered()) {
8966
- if (s.status !== "ready") continue;
8967
- for (const t of s.tools) {
8968
- out.push({ name: mcpToolName(s.name, t.name), description: `[MCP server ${s.name}] ${t.description}`.slice(0, 1024), input_schema: t.inputSchema });
9137
+ for (const def of this.deps.defs) {
9138
+ for (const t of this.toolsOf(def)) {
9139
+ out.push({ name: mcpToolName(def.name, t.name), description: `[MCP server ${def.name}] ${t.description}`.slice(0, 1024), input_schema: t.inputSchema });
8969
9140
  }
8970
9141
  }
8971
9142
  return out;
8972
9143
  }
8973
9144
  handlers() {
8974
9145
  const out = {};
8975
- for (const s of this.ordered()) {
8976
- if (s.status !== "ready") continue;
8977
- for (const t of s.tools) out[mcpToolName(s.name, t.name)] = (input) => this.call(s.name, t.name, input);
9146
+ for (const def of this.deps.defs) {
9147
+ for (const t of this.toolsOf(def)) out[mcpToolName(def.name, t.name)] = (input) => this.callWhenReady(def.name, t.name, input);
8978
9148
  }
8979
9149
  return out;
8980
9150
  }
9151
+ /** A call on a cached tool waits for its server to connect (bounded by the connect timeout) and then goes through the normal checks. */
9152
+ async callWhenReady(server, tool, input) {
9153
+ await this.connected.get(server);
9154
+ const state = this.servers.get(server);
9155
+ if (state?.status === "ready" && !state.tools.some((t) => t.name === tool)) {
9156
+ return { output: `MCP server "${server}" no longer offers ${tool}; its tool list changed since it was cached.`, is_error: true };
9157
+ }
9158
+ return this.call(server, tool, input);
9159
+ }
8981
9160
  async call(server, tool, input) {
8982
9161
  const full = mcpToolName(server, tool);
8983
9162
  const state = this.servers.get(server);
@@ -9019,11 +9198,18 @@ var init_mcp_manager = __esm({
9019
9198
  /** One line per server for /status; reasons never include env values or stderr. */
9020
9199
  statusLines() {
9021
9200
  const lines2 = [];
9022
- for (const s of this.ordered()) {
9201
+ for (const def of this.deps.defs) {
9202
+ const s = this.servers.get(def.name);
9203
+ if (s === void 0) {
9204
+ const cached3 = this.cachedTools.get(def.name);
9205
+ if (cached3) lines2.push(`mcp: ${def.name} connecting... (${cached3.length} tools from cache)`);
9206
+ continue;
9207
+ }
9023
9208
  if (s.status === "ready") lines2.push(`mcp: ${s.name} ready, ${s.tools.length} tools${s.skippedTools.length ? ` (not usable: ${s.skippedTools.join(", ")})` : ""}`);
9024
- else if (s.status === "connecting") lines2.push(`mcp: ${s.name} connecting...`);
9209
+ else if (s.status === "connecting") lines2.push(`mcp: ${s.name} connecting...${this.cachedTools.has(s.name) ? ` (${this.cachedTools.get(s.name).length} tools from cache)` : ""}`);
9025
9210
  else lines2.push(`mcp: ${s.name} NOT available (${s.reason})`);
9026
9211
  }
9212
+ for (const n of this.cacheNotes) lines2.push(`mcp: ${n}`);
9027
9213
  return lines2;
9028
9214
  }
9029
9215
  /** For /tools: names grouped by server. */
@@ -44327,7 +44513,7 @@ var require_core = __commonJS({
44327
44513
  return match && match.index === 0;
44328
44514
  }
44329
44515
  var BACKREF_RE = /\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;
44330
- function join14(regexps, separator = "|") {
44516
+ function join15(regexps, separator = "|") {
44331
44517
  let numCaptures = 0;
44332
44518
  return regexps.map((regex2) => {
44333
44519
  numCaptures += 1;
@@ -44631,7 +44817,7 @@ var require_core = __commonJS({
44631
44817
  this.exec = () => null;
44632
44818
  }
44633
44819
  const terminators = this.regexes.map((el) => el[1]);
44634
- this.matcherRe = langRe(join14(terminators), true);
44820
+ this.matcherRe = langRe(join15(terminators), true);
44635
44821
  this.lastIndex = 0;
44636
44822
  }
44637
44823
  /** @param {string} s */
@@ -103444,6 +103630,7 @@ var init_slash_commands = __esm({
103444
103630
  { name: "turns", description: "Browse earlier turns and open their steps (shortcut: ctrl+o)" },
103445
103631
  { name: "commands", description: "List the slash commands imported from Claude Code" },
103446
103632
  { name: "skills", description: "List imported skills; /skills show <name> [file], /skills delete <name>" },
103633
+ { name: "import", description: "Re-check imported Claude Code sources: /import reload shows changes; /import reload trust applies them" },
103447
103634
  { name: "tools", description: "List the tools available to the agent" },
103448
103635
  { name: "exit", description: "Quit the REPL (alias: /quit)" },
103449
103636
  { name: "quit", description: "Quit the REPL" }
@@ -104974,12 +105161,37 @@ var init_skills_command = __esm({
104974
105161
  }
104975
105162
  });
104976
105163
 
104977
- // src/permission-rules-loader.ts
105164
+ // src/mcp/tool-cache-file.ts
104978
105165
  import { readFile as readFile8 } from "node:fs/promises";
104979
105166
  import { join as join11 } from "node:path";
105167
+ function nodeToolCache(configDir) {
105168
+ const path12 = join11(configDir, "mcp-tools-cache.json");
105169
+ return {
105170
+ read: async () => {
105171
+ try {
105172
+ return await readFile8(path12, "utf8");
105173
+ } catch (err) {
105174
+ if (err.code === "ENOENT") return null;
105175
+ throw err;
105176
+ }
105177
+ },
105178
+ write: (text) => writeTextAtomic(path12, text),
105179
+ hash: sha2562
105180
+ };
105181
+ }
105182
+ var init_tool_cache_file = __esm({
105183
+ "src/mcp/tool-cache-file.ts"() {
105184
+ "use strict";
105185
+ init_import_gate();
105186
+ }
105187
+ });
105188
+
105189
+ // src/permission-rules-loader.ts
105190
+ import { readFile as readFile9 } from "node:fs/promises";
105191
+ import { join as join12 } from "node:path";
104980
105192
  async function readOrNull2(path12) {
104981
105193
  try {
104982
- return await readFile8(path12, "utf8");
105194
+ return await readFile9(path12, "utf8");
104983
105195
  } catch (err) {
104984
105196
  if (err.code === "ENOENT") return null;
104985
105197
  throw err;
@@ -104987,7 +105199,7 @@ async function readOrNull2(path12) {
104987
105199
  }
104988
105200
  async function loadPermissionRules(deps) {
104989
105201
  const none = (line) => ({ rules: void 0, defaultMode: void 0, statusLines: [line] });
104990
- const configPath = join11(deps.configDir, "imports.json");
105202
+ const configPath = join12(deps.configDir, "imports.json");
104991
105203
  try {
104992
105204
  const loaded = parseImportsConfig(await readOrNull2(configPath), configPath);
104993
105205
  if (loaded.error) return none("permission rules: off (imports.json unreadable)");
@@ -104996,13 +105208,13 @@ async function loadPermissionRules(deps) {
104996
105208
  return none(`permission rules: off (imports.json not read: ${err instanceof Error ? err.message : String(err)})`);
104997
105209
  }
104998
105210
  const lines2 = [];
104999
- const files = [{ scope: "user", path: join11(deps.homeDir, ".claude", "settings.json") }];
105211
+ const files = [{ scope: "user", path: join12(deps.homeDir, ".claude", "settings.json") }];
105000
105212
  try {
105001
105213
  const sources2 = await readRepoSources(deps.cwd);
105002
105214
  if (sources2[".claude/settings.json"] !== void 0 || sources2[".claude/settings.local.json"] !== void 0) {
105003
105215
  const status = await createNodeTrustStore(deps.configDir).repoStatus(deps.cwd, sources2);
105004
105216
  if (status === "trusted" || status === "session") {
105005
- files.push({ scope: "repo", path: join11(deps.cwd, ".claude", "settings.json") }, { scope: "local", path: join11(deps.cwd, ".claude", "settings.local.json") });
105217
+ files.push({ scope: "repo", path: join12(deps.cwd, ".claude", "settings.json") }, { scope: "local", path: join12(deps.cwd, ".claude", "settings.local.json") });
105006
105218
  } else lines2.push(`permission rules: repo settings NOT loaded (${status === "never" ? "marked never trust" : "untrusted repo"})`);
105007
105219
  }
105008
105220
  } catch (err) {
@@ -105043,8 +105255,43 @@ var init_permission_rules_loader = __esm({
105043
105255
  }
105044
105256
  });
105045
105257
 
105258
+ // src/import-reload.ts
105259
+ async function checkRepoSources(deps, loaded) {
105260
+ const files = await readRepoSources(deps.cwd);
105261
+ const current = fingerprintSources(files, sha2562);
105262
+ const status = Object.keys(files).length === 0 ? "untrusted" : await createNodeTrustStore(deps.configDir).repoStatus(deps.cwd, files);
105263
+ return { current, changes: diffSources(loaded, current), trusted: status === "trusted" || status === "session" };
105264
+ }
105265
+ async function describeReload(deps, loaded) {
105266
+ const check = await checkRepoSources(deps, loaded);
105267
+ if (!check.changes.changed) return ["No change in the repo import sources since they were loaded."];
105268
+ const files = await readRepoSources(deps.cwd);
105269
+ return [
105270
+ "Repo import sources changed since they were loaded:",
105271
+ ...formatSourceChanges(check.changes).map((l) => ` ${l}`),
105272
+ "",
105273
+ "Trusting the new state would load:",
105274
+ ...describeRepoSources(files).length ? describeRepoSources(files) : [" (nothing)"],
105275
+ "",
105276
+ "Run /import reload trust to trust it and apply the rules and commands. MCP servers and skills apply on the next start."
105277
+ ];
105278
+ }
105279
+ async function trustCurrentSources(deps) {
105280
+ const files = await readRepoSources(deps.cwd);
105281
+ await createNodeTrustStore(deps.configDir).setRepo(deps.cwd, files, "trusted");
105282
+ return fingerprintSources(files, sha2562);
105283
+ }
105284
+ var init_import_reload = __esm({
105285
+ "src/import-reload.ts"() {
105286
+ "use strict";
105287
+ init_src();
105288
+ init_import_gate();
105289
+ init_repo_trust_prompt();
105290
+ }
105291
+ });
105292
+
105046
105293
  // src/command-loader.ts
105047
- import { join as join12 } from "node:path";
105294
+ import { join as join13 } from "node:path";
105048
105295
  async function userCommands(deps, lines2) {
105049
105296
  try {
105050
105297
  const apiKey = await resolveApiKey({ env: deps.env, readFile: deps.readFile, homedir: deps.homedir });
@@ -105099,7 +105346,7 @@ async function repoCommands(deps, lines2) {
105099
105346
  }
105100
105347
  }
105101
105348
  async function loadCommands(deps) {
105102
- const configPath = join12(deps.configDir, "imports.json");
105349
+ const configPath = join13(deps.configDir, "imports.json");
105103
105350
  try {
105104
105351
  const loaded = parseImportsConfig(await deps.readFile(configPath).catch((err) => {
105105
105352
  if (err.code === "ENOENT") return null;
@@ -105132,9 +105379,9 @@ var init_command_loader = __esm({
105132
105379
  });
105133
105380
 
105134
105381
  // src/useReplController.ts
105135
- import { readFile as readFile9, writeFile as writeFile6, mkdir as mkdir6 } from "node:fs/promises";
105382
+ import { readFile as readFile10, writeFile as writeFile6, mkdir as mkdir6 } from "node:fs/promises";
105136
105383
  import { homedir as homedir2 } from "node:os";
105137
- import { join as join13 } from "node:path";
105384
+ import { join as join14 } from "node:path";
105138
105385
  import { execFile as execFile4 } from "node:child_process";
105139
105386
  function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mentionsOpt, sessions, resumeLatest: resumeLatest2, onViewTurns, onViewResume, isEscOwned }) {
105140
105387
  const { exit } = use_app_default();
@@ -105157,7 +105404,7 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
105157
105404
  }, [turnStartedAt]);
105158
105405
  const projectRoot = (0, import_react30.useMemo)(() => process.cwd(), []);
105159
105406
  const promptTemplates = (0, import_react30.useCallback)(
105160
- () => loadPromptTemplates({ env: process.env, readFile: (p) => readFile9(p, "utf-8"), writeFile: (p, d) => writeFile6(p, d, "utf-8"), mkdir: (p) => mkdir6(p, { recursive: true }).then(() => void 0), homedir: homedir2, fetch }),
105407
+ () => loadPromptTemplates({ env: process.env, readFile: (p) => readFile10(p, "utf-8"), writeFile: (p, d) => writeFile6(p, d, "utf-8"), mkdir: (p) => mkdir6(p, { recursive: true }).then(() => void 0), homedir: homedir2, fetch }),
105161
105408
  []
105162
105409
  );
105163
105410
  (0, import_react30.useEffect)(() => {
@@ -105173,11 +105420,11 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
105173
105420
  const recordingRef = (0, import_react30.useRef)(null);
105174
105421
  const recording = () => recordingRef.current ??= createRecordingRuntime({
105175
105422
  env: process.env,
105176
- configDir: process.env.SPACECODE_CONFIG_DIR?.trim() || join13(homedir2(), ".spacecode"),
105423
+ configDir: process.env.SPACECODE_CONFIG_DIR?.trim() || join14(homedir2(), ".spacecode"),
105177
105424
  projectRoot,
105178
105425
  fetch,
105179
105426
  baseUrl: backendUrl(process.env),
105180
- resolveApiKey: () => resolveApiKey({ env: process.env, readFile: (p) => readFile9(p, "utf-8"), homedir: homedir2 }).catch(() => null),
105427
+ resolveApiKey: () => resolveApiKey({ env: process.env, readFile: (p) => readFile10(p, "utf-8"), homedir: homedir2 }).catch(() => null),
105181
105428
  log: (m2) => console.error(`[agent-ui] ${m2}`)
105182
105429
  });
105183
105430
  const resumeListRef = (0, import_react30.useRef)([]);
@@ -105196,14 +105443,15 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
105196
105443
  const pendingRef = (0, import_react30.useRef)(null);
105197
105444
  const toolsetRef = (0, import_react30.useRef)(null);
105198
105445
  const rulesRef = (0, import_react30.useRef)(void 0);
105446
+ const loadedSourcesRef = (0, import_react30.useRef)(null);
105199
105447
  const [commandsLoaded, setCommandsLoaded] = (0, import_react30.useState)({ commands: [], statusLines: [] });
105200
105448
  (0, import_react30.useEffect)(() => {
105201
105449
  let live2 = true;
105202
105450
  loadCommands({
105203
- configDir: process.env.SPACECODE_CONFIG_DIR?.trim() || join13(homedir2(), ".spacecode"),
105451
+ configDir: process.env.SPACECODE_CONFIG_DIR?.trim() || join14(homedir2(), ".spacecode"),
105204
105452
  cwd: process.cwd(),
105205
105453
  env: process.env,
105206
- readFile: (p) => readFile9(p, "utf-8"),
105454
+ readFile: (p) => readFile10(p, "utf-8"),
105207
105455
  homedir: homedir2,
105208
105456
  fetch,
105209
105457
  builtins: SLASH_COMMANDS.map((c) => c.name)
@@ -105420,6 +105668,29 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
105420
105668
  case "context":
105421
105669
  notice(formatContextReport({ contextTokens: estimateTotalTokens("", history), maxContextTokens: DEFAULT_MAX_CONTEXT_TOKENS }));
105422
105670
  return;
105671
+ case "import": {
105672
+ const configDir = process.env.SPACECODE_CONFIG_DIR?.trim() || join14(homedir2(), ".spacecode");
105673
+ const deps = { configDir, cwd: process.cwd() };
105674
+ const [sub, ...rest] = slash.args.split(/\s+/).filter(Boolean);
105675
+ if (sub !== "reload" || rest.length > 1 || rest[0] !== void 0 && rest[0] !== "trust") {
105676
+ notice("Usage: /import reload (show changes to the repo import sources) or /import reload trust (trust the new state and apply it).");
105677
+ return;
105678
+ }
105679
+ try {
105680
+ const baseline = loadedSourcesRef.current ?? fingerprintSources(await readRepoSources(process.cwd()), sha2562);
105681
+ if (rest[0] === "trust") {
105682
+ loadedSourcesRef.current = await trustCurrentSources(deps);
105683
+ const loaded = await loadPermissionRules({ configDir, homeDir: homedir2(), cwd: process.cwd() });
105684
+ rulesRef.current = loaded.rules;
105685
+ const cmds = await loadCommands({ configDir, cwd: process.cwd(), env: process.env, readFile: (p) => readFile10(p, "utf-8"), homedir: homedir2, fetch, builtins: SLASH_COMMANDS.map((c) => c.name) });
105686
+ setCommandsLoaded(cmds);
105687
+ notice(["Trusted the repo import sources as they are now.", ...loaded.statusLines, ...cmds.statusLines, "MCP servers and skills apply on the next start."].join("\n"));
105688
+ } else notice((await describeReload(deps, baseline)).join("\n"));
105689
+ } catch (err) {
105690
+ notice(`/import reload failed (${err instanceof Error ? err.message : String(err)}); nothing was changed.`);
105691
+ }
105692
+ return;
105693
+ }
105423
105694
  case "record":
105424
105695
  notice(await recording().command(slash.args));
105425
105696
  return;
@@ -105491,7 +105762,7 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
105491
105762
  }
105492
105763
  setIsRunning(true);
105493
105764
  try {
105494
- const apiKey2 = await resolveApiKey({ env: process.env, readFile: (p) => readFile9(p, "utf-8"), homedir: homedir2 });
105765
+ const apiKey2 = await resolveApiKey({ env: process.env, readFile: (p) => readFile10(p, "utf-8"), homedir: homedir2 });
105495
105766
  if (!apiKey2) {
105496
105767
  notice(MISSING_KEY_MESSAGE);
105497
105768
  return;
@@ -105513,7 +105784,7 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
105513
105784
  }
105514
105785
  case "model": {
105515
105786
  try {
105516
- const apiKey2 = await resolveApiKey({ env: process.env, readFile: (p) => readFile9(p, "utf-8"), homedir: homedir2 });
105787
+ const apiKey2 = await resolveApiKey({ env: process.env, readFile: (p) => readFile10(p, "utf-8"), homedir: homedir2 });
105517
105788
  if (!apiKey2) {
105518
105789
  notice(MISSING_KEY_MESSAGE);
105519
105790
  return;
@@ -105548,7 +105819,7 @@ ${list2.map((c) => ` /${c.name} (${c.scope})${c.description ? ` ${c.descripti
105548
105819
  }
105549
105820
  case "skills": {
105550
105821
  const state = profileRef.current;
105551
- const apiKey2 = await resolveApiKey({ env: process.env, readFile: (p) => readFile9(p, "utf-8"), homedir: homedir2 }).catch(() => null);
105822
+ const apiKey2 = await resolveApiKey({ env: process.env, readFile: (p) => readFile10(p, "utf-8"), homedir: homedir2 }).catch(() => null);
105552
105823
  const client = apiKey2 ? createProfileClient({ baseUrl: backendUrl(process.env), apiKey: apiKey2, fetch }) : null;
105553
105824
  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)"] }));
105554
105825
  return;
@@ -105570,7 +105841,7 @@ ${list2.map((c) => ` /${c.name} (${c.scope})${c.description ? ` ${c.descripti
105570
105841
  try {
105571
105842
  apiKey = await resolveApiKey({
105572
105843
  env: process.env,
105573
- readFile: (p) => readFile9(p, "utf-8"),
105844
+ readFile: (p) => readFile10(p, "utf-8"),
105574
105845
  homedir: homedir2
105575
105846
  });
105576
105847
  } catch (err) {
@@ -105605,9 +105876,25 @@ ${list2.map((c) => ` /${c.name} (${c.scope})${c.description ? ` ${c.descripti
105605
105876
  let failed = false;
105606
105877
  let keepForRetry = false;
105607
105878
  try {
105879
+ if (loadedSourcesRef.current) {
105880
+ const configDir = process.env.SPACECODE_CONFIG_DIR?.trim() || join14(homedir2(), ".spacecode");
105881
+ try {
105882
+ const check = await checkRepoSources({ configDir, cwd: process.cwd() }, loadedSourcesRef.current);
105883
+ if (check.changes.changed) {
105884
+ const loaded = await loadPermissionRules({ configDir, homeDir: homedir2(), cwd: process.cwd() });
105885
+ rulesRef.current = loaded.rules;
105886
+ loadedSourcesRef.current = check.current;
105887
+ const lead = check.trusted ? "Repo import sources changed since they were loaded; the new state is already trusted, so its permission rules now apply:" : "Repo import sources changed since they were loaded; repo permission rules are dropped until you trust the new state:";
105888
+ notice([lead, ...formatSourceChanges(check.changes).map((l) => ` ${l}`), ...check.trusted ? [] : ["Run /import reload to review, /import reload trust to apply."]].join("\n"));
105889
+ }
105890
+ } catch (err) {
105891
+ rulesRef.current = void 0;
105892
+ notice(`Repo import source check failed (${err instanceof Error ? err.message : String(err)}); repo permission rules are dropped.`);
105893
+ }
105894
+ }
105608
105895
  profilePromiseRef.current ??= prepareProfile({
105609
105896
  env: process.env,
105610
- readFile: (p) => readFile9(p, "utf-8"),
105897
+ readFile: (p) => readFile10(p, "utf-8"),
105611
105898
  writeFile: (p, d) => writeFile6(p, d, { encoding: "utf-8", mode: 384 }),
105612
105899
  mkdir: async (p) => {
105613
105900
  await mkdir6(p, { recursive: true });
@@ -105617,17 +105904,19 @@ ${list2.map((c) => ` /${c.name} (${c.scope})${c.description ? ` ${c.descripti
105617
105904
  cwd: process.cwd(),
105618
105905
  gitRoot: gitTopLevel,
105619
105906
  importGate: async () => {
105620
- const configDir = process.env.SPACECODE_CONFIG_DIR?.trim() || join13(homedir2(), ".spacecode");
105907
+ const configDir = process.env.SPACECODE_CONFIG_DIR?.trim() || join14(homedir2(), ".spacecode");
105908
+ const fingerprint = fingerprintSources(await readRepoSources(process.cwd()), sha2562);
105621
105909
  const gate = await importGateStatus({ configDir, cwd: process.cwd(), interactive: Boolean(process.stdin.isTTY) });
105622
105910
  const loaded = await loadPermissionRules({ configDir, homeDir: homedir2(), cwd: process.cwd() });
105623
105911
  rulesRef.current = loaded.rules;
105912
+ loadedSourcesRef.current = fingerprint;
105624
105913
  if (loaded.defaultMode && !yolo2) permissions.set(loaded.defaultMode);
105625
105914
  return [...gate, ...loaded.statusLines];
105626
105915
  }
105627
105916
  });
105628
105917
  profileRef.current = await profilePromiseRef.current;
105629
105918
  if (mcpRef.current === null && (profileRef.current?.mcpDefs?.length ?? 0) > 0) {
105630
- const configDir = process.env.SPACECODE_CONFIG_DIR?.trim() || join13(homedir2(), ".spacecode");
105919
+ const configDir = process.env.SPACECODE_CONFIG_DIR?.trim() || join14(homedir2(), ".spacecode");
105631
105920
  const trust = createNodeTrustStore(configDir);
105632
105921
  const manager = new McpManager({
105633
105922
  defs: profileRef.current.mcpDefs,
@@ -105637,10 +105926,12 @@ ${list2.map((c) => ` /${c.name} (${c.scope})${c.description ? ` ${c.descripti
105637
105926
  ...rulesRef.current ? { rules: rulesRef.current } : {},
105638
105927
  permissions,
105639
105928
  confirm: askUser,
105640
- audit: new FileAuditLog(join13(homedir2(), ".spacecode", "audit.jsonl")),
105641
- clientVersion: process.env.SPACECODE_VERSION ?? "0"
105929
+ audit: new FileAuditLog(join14(homedir2(), ".spacecode", "audit.jsonl")),
105930
+ clientVersion: process.env.SPACECODE_VERSION ?? "0",
105931
+ toolCache: nodeToolCache(configDir)
105642
105932
  });
105643
105933
  mcpRef.current = manager;
105934
+ await manager.prime();
105644
105935
  void manager.start();
105645
105936
  }
105646
105937
  const toolset = createToolset();
@@ -105778,8 +106069,11 @@ var init_useReplController = __esm({
105778
106069
  init_skills_command();
105779
106070
  init_src();
105780
106071
  init_mcp_manager();
106072
+ init_tool_cache_file();
105781
106073
  init_import_gate();
105782
106074
  init_permission_rules_loader();
106075
+ init_import_reload();
106076
+ init_src();
105783
106077
  init_command_loader();
105784
106078
  init_slash_commands();
105785
106079
  init_useEventAccumulator();
@@ -106365,7 +106659,7 @@ function clearOnResize(stdout) {
106365
106659
  var import_jsx_runtime14 = __toESM(require_jsx_runtime(), 1);
106366
106660
  if (process.argv[2] === "import-claude") {
106367
106661
  const { runImportClaude: runImportClaude2 } = await Promise.resolve().then(() => (init_import_claude(), import_claude_exports));
106368
- const { readFile: readFile10, readdir: readdir6 } = await import("node:fs/promises");
106662
+ const { readFile: readFile11, readdir: readdir6 } = await import("node:fs/promises");
106369
106663
  const { execFile: execFile5 } = await import("node:child_process");
106370
106664
  const gitRoot = () => new Promise((resolve2) => {
106371
106665
  execFile5("git", ["rev-parse", "--show-toplevel"], { cwd: process.cwd() }, (err, stdout) => resolve2(err ? null : stdout.trim() || null));
@@ -106373,8 +106667,8 @@ if (process.argv[2] === "import-claude") {
106373
106667
  const code = await runImportClaude2({
106374
106668
  argv: process.argv.slice(2),
106375
106669
  env: process.env,
106376
- fs: { readFile: (p) => readFile10(p, "utf-8"), readdir: (p) => readdir6(p) },
106377
- readFile: (p) => readFile10(p, "utf-8"),
106670
+ fs: { readFile: (p) => readFile11(p, "utf-8"), readdir: (p) => readdir6(p) },
106671
+ readFile: (p) => readFile11(p, "utf-8"),
106378
106672
  homedir: os5.homedir,
106379
106673
  cwd: process.cwd(),
106380
106674
  gitRoot,
@@ -106399,9 +106693,9 @@ if (process.argv[2] === "approve-mcp") {
106399
106693
  const { resolveApiKey: resolveApiKey2 } = await Promise.resolve().then(() => (init_api_key(), api_key_exports));
106400
106694
  const { backendUrl: backendUrl2 } = await Promise.resolve().then(() => (init_profile_runtime(), profile_runtime_exports));
106401
106695
  const { createProfileClient: createProfileClient2 } = await Promise.resolve().then(() => (init_src(), src_exports));
106402
- const { readFile: readFile10 } = await import("node:fs/promises");
106696
+ const { readFile: readFile11 } = await import("node:fs/promises");
106403
106697
  const configDir = process.env.SPACECODE_CONFIG_DIR?.trim() || path11.join(os5.homedir(), ".spacecode");
106404
- const apiKey = await resolveApiKey2({ env: process.env, readFile: (p) => readFile10(p, "utf-8"), homedir: os5.homedir }).catch(() => null);
106698
+ const apiKey = await resolveApiKey2({ env: process.env, readFile: (p) => readFile11(p, "utf-8"), homedir: os5.homedir }).catch(() => null);
106405
106699
  const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
106406
106700
  const { createInterface } = interactive ? await import("node:readline/promises") : { createInterface: void 0 };
106407
106701
  const rl = createInterface ? createInterface({ input: process.stdin, output: process.stdout }) : void 0;
@@ -106409,7 +106703,7 @@ if (process.argv[2] === "approve-mcp") {
106409
106703
  const code = await runApproveMcp2({
106410
106704
  configDir,
106411
106705
  cwd: process.cwd(),
106412
- readFile: (p) => readFile10(p, "utf-8"),
106706
+ readFile: (p) => readFile11(p, "utf-8"),
106413
106707
  client: apiKey ? createProfileClient2({ baseUrl: backendUrl2(process.env), apiKey, fetch }) : null,
106414
106708
  trust: createNodeTrustStore2(configDir),
106415
106709
  ...rl ? { ask: async (q2) => {