spacecode 2.6.1 → 2.7.0

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
@@ -5705,9 +5705,9 @@ async function sha256(content) {
5705
5705
  const hashArray = new Uint8Array(hashBuffer);
5706
5706
  return Array.from(hashArray).map((b2) => b2.toString(16).padStart(2, "0")).join("");
5707
5707
  }
5708
- async function computeLockfileHash(readFile7) {
5708
+ async function computeLockfileHash(readFile9) {
5709
5709
  for (const name of LOCKFILES) {
5710
- const content = await readFile7(name);
5710
+ const content = await readFile9(name);
5711
5711
  if (content) {
5712
5712
  const hash = await sha256(content);
5713
5713
  return hash.slice(0, 12);
@@ -7772,7 +7772,7 @@ __export(api_key_exports, {
7772
7772
  });
7773
7773
  import { join } from "node:path";
7774
7774
  async function resolveApiKey(deps) {
7775
- const { env: env3, readFile: readFile7, homedir: homedir3 } = deps;
7775
+ const { env: env3, readFile: readFile9, homedir: homedir3 } = deps;
7776
7776
  for (const name of ["SPACECODE_API_KEY", "ANTHROPIC_API_KEY"]) {
7777
7777
  const value = env3[name]?.trim();
7778
7778
  if (value) return value;
@@ -7781,7 +7781,7 @@ async function resolveApiKey(deps) {
7781
7781
  const file = join(dir, "auth.json");
7782
7782
  let raw;
7783
7783
  try {
7784
- raw = await readFile7(file);
7784
+ raw = await readFile9(file);
7785
7785
  } catch (err) {
7786
7786
  if (err.code === "ENOENT") return null;
7787
7787
  throw new Error(`Cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
@@ -35292,7 +35292,7 @@ var require_websocket = __commonJS({
35292
35292
  var http = __require("http");
35293
35293
  var net = __require("net");
35294
35294
  var tls = __require("tls");
35295
- var { randomBytes: randomBytes2, createHash: createHash2 } = __require("crypto");
35295
+ var { randomBytes: randomBytes2, createHash: createHash5 } = __require("crypto");
35296
35296
  var { Duplex, Readable } = __require("stream");
35297
35297
  var { URL: URL2 } = __require("url");
35298
35298
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -35960,7 +35960,7 @@ var require_websocket = __commonJS({
35960
35960
  abortHandshake(websocket, socket, "Invalid Upgrade header");
35961
35961
  return;
35962
35962
  }
35963
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
35963
+ const digest = createHash5("sha1").update(key + GUID).digest("base64");
35964
35964
  if (res.headers["sec-websocket-accept"] !== digest) {
35965
35965
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
35966
35966
  return;
@@ -36329,7 +36329,7 @@ var require_websocket_server = __commonJS({
36329
36329
  var EventEmitter3 = __require("events");
36330
36330
  var http = __require("http");
36331
36331
  var { Duplex } = __require("stream");
36332
- var { createHash: createHash2 } = __require("crypto");
36332
+ var { createHash: createHash5 } = __require("crypto");
36333
36333
  var extension2 = require_extension();
36334
36334
  var PerMessageDeflate2 = require_permessage_deflate();
36335
36335
  var subprotocol2 = require_subprotocol();
@@ -36636,7 +36636,7 @@ var require_websocket_server = __commonJS({
36636
36636
  );
36637
36637
  }
36638
36638
  if (this._state > RUNNING) return abortHandshake(socket, 503);
36639
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
36639
+ const digest = createHash5("sha1").update(key + GUID).digest("base64");
36640
36640
  const headers = [
36641
36641
  "HTTP/1.1 101 Switching Protocols",
36642
36642
  "Upgrade: websocket",
@@ -40378,12 +40378,12 @@ var init_WelcomeBanner = __esm({
40378
40378
  });
40379
40379
 
40380
40380
  // src/render/hints.ts
40381
- function formatHints({ running, asking, columns }, sep2 = "\xB7") {
40381
+ function formatHints({ running, asking, columns }, sep3 = "\xB7") {
40382
40382
  columns = Math.min(columns, MAX_STATIC_LINE);
40383
40383
  const parts = asking ? ["1 2 3 answer", "esc refuse"] : running ? ["esc interrupt", "tab step", "shift+tab mode", "ctrl+o turns"] : ["enter send", "shift+tab mode", "/ commands", "@ file", "! shell", "ctrl+o turns"];
40384
40384
  const kept = [...parts];
40385
- while (kept.length > 1 && kept.join(` ${sep2} `).length > columns) kept.pop();
40386
- return kept.join(` ${sep2} `);
40385
+ while (kept.length > 1 && kept.join(` ${sep3} `).length > columns) kept.pop();
40386
+ return kept.join(` ${sep3} `);
40387
40387
  }
40388
40388
  var MAX_STATIC_LINE;
40389
40389
  var init_hints = __esm({
@@ -40398,13 +40398,13 @@ function formatDuration(ms) {
40398
40398
  const min = Math.floor(ms / 6e4);
40399
40399
  return min < 1 ? "<1 min" : min < 60 ? `${min} min` : `${Math.floor(min / 60)}h ${min % 60}m`;
40400
40400
  }
40401
- function formatHeader({ project, git, otherSessions, turns, elapsedMs, columns: terminalColumns }, sep2 = "\xB7") {
40401
+ function formatHeader({ project, git, otherSessions, turns, elapsedMs, columns: terminalColumns }, sep3 = "\xB7") {
40402
40402
  const columns = Math.min(terminalColumns, MAX_STATIC_LINE);
40403
40403
  const optional = [];
40404
40404
  const branch = [];
40405
40405
  if (git?.kind === "ok") {
40406
40406
  branch.push({ text: git.branch });
40407
- if (git.dirty > 0) branch.push({ text: `${sep2 === "\xB7" ? "\xB1" : "+-"}${git.dirty}`, role: "needsYou" });
40407
+ if (git.dirty > 0) branch.push({ text: `${sep3 === "\xB7" ? "\xB1" : "+-"}${git.dirty}`, role: "needsYou" });
40408
40408
  } else if (git?.kind === "unknown") {
40409
40409
  branch.push({ text: "git ?", role: "needsYou" });
40410
40410
  }
@@ -40413,7 +40413,7 @@ function formatHeader({ project, git, otherSessions, turns, elapsedMs, columns:
40413
40413
  optional.push({ text: formatDuration(elapsedMs), role: "secondary" });
40414
40414
  const base = [{ text: "spacecode", bold: true }, { text: project }];
40415
40415
  const all = [...base, ...branch, ...optional];
40416
- const width = (segs) => segs.reduce((n, s, i) => n + s.text.length + (i > 0 ? sep2.length + 2 : 0), 0);
40416
+ const width = (segs) => segs.reduce((n, s, i) => n + s.text.length + (i > 0 ? sep3.length + 2 : 0), 0);
40417
40417
  const kept = [...all];
40418
40418
  while (kept.length > base.length && width(kept) > columns) kept.pop();
40419
40419
  return kept;
@@ -40451,9 +40451,9 @@ function truncatePath(target, max, ellipsis = "\u2026") {
40451
40451
  }
40452
40452
  return max <= e ? ellipsis.slice(0, max) : `${target.slice(0, max - e)}${ellipsis}`;
40453
40453
  }
40454
- function fitStepLine({ verb, target, meta, columns, indent = 4, ellipsis = "\u2026", sep: sep2 = "\xB7" }) {
40454
+ function fitStepLine({ verb, target, meta, columns, indent = 4, ellipsis = "\u2026", sep: sep3 = "\xB7" }) {
40455
40455
  const room = Math.max(10, columns - indent);
40456
- const metaText = meta ? ` ${sep2} ${meta}` : "";
40456
+ const metaText = meta ? ` ${sep3} ${meta}` : "";
40457
40457
  const fixed = verb.length + 1;
40458
40458
  if (fixed + target.length + metaText.length <= room) return { verb, target, meta };
40459
40459
  const targetRoom = room - fixed - metaText.length;
@@ -40480,10 +40480,10 @@ var init_rail = __esm({
40480
40480
 
40481
40481
  // src/components/Header.tsx
40482
40482
  function Header(props) {
40483
- const sep2 = activeGlyphs().sep;
40484
- const segments2 = formatHeader(props, sep2);
40483
+ const sep3 = activeGlyphs().sep;
40484
+ const segments2 = formatHeader(props, sep3);
40485
40485
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Box_default, { children: segments2.map((seg, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react22.default.Fragment, { children: [
40486
- i > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { ...paint("secondary"), children: ` ${sep2} ` }),
40486
+ i > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { ...paint("secondary"), children: ` ${sep3} ` }),
40487
40487
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { bold: seg.bold, ...seg.role ? paint(seg.role) : {}, children: seg.text })
40488
40488
  ] }, i)) });
40489
40489
  }
@@ -44010,7 +44010,7 @@ var require_core = __commonJS({
44010
44010
  return match && match.index === 0;
44011
44011
  }
44012
44012
  var BACKREF_RE = /\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;
44013
- function join10(regexps, separator = "|") {
44013
+ function join12(regexps, separator = "|") {
44014
44014
  let numCaptures = 0;
44015
44015
  return regexps.map((regex2) => {
44016
44016
  numCaptures += 1;
@@ -44314,7 +44314,7 @@ var require_core = __commonJS({
44314
44314
  this.exec = () => null;
44315
44315
  }
44316
44316
  const terminators = this.regexes.map((el) => el[1]);
44317
- this.matcherRe = langRe(join10(terminators), true);
44317
+ this.matcherRe = langRe(join12(terminators), true);
44318
44318
  this.lastIndex = 0;
44319
44319
  }
44320
44320
  /** @param {string} s */
@@ -102186,6 +102186,7 @@ function classifyError(message) {
102186
102186
  }
102187
102187
  if (/^No API key found/.test(message)) return { kind: "missing-key" };
102188
102188
  if (/^Not approved\b/.test(message)) return { kind: "policy" };
102189
+ if (TIMEOUT.test(message)) return { kind: "timeout" };
102189
102190
  if (NETWORK.test(message)) return { kind: "network" };
102190
102191
  return { kind: "unknown" };
102191
102192
  }
@@ -102247,6 +102248,12 @@ function describeError(kind, cause, ctx = {}) {
102247
102248
  cause: `${shown}${retained ? " Your prompt is saved." : ""}`,
102248
102249
  action: retained ? "Check your connection, then press enter to retry." : "Check your connection, then send your prompt again."
102249
102250
  };
102251
+ case "timeout":
102252
+ return {
102253
+ title: "The model stopped responding",
102254
+ cause: `${shown}${retained ? " Your prompt is saved." : ""}`,
102255
+ action: retained ? "Press enter to retry." : "Send your prompt again."
102256
+ };
102250
102257
  case "session-save":
102251
102258
  return {
102252
102259
  title: "Session not saved",
@@ -102269,9 +102276,9 @@ function describeEventError(message, ctx = {}) {
102269
102276
  }
102270
102277
  function isRetryable(message) {
102271
102278
  const { kind } = classifyError(message);
102272
- return kind === "gateway" || kind === "rate-limit" || kind === "network" || kind === "missing-key" || kind === "auth";
102279
+ return kind === "gateway" || kind === "rate-limit" || kind === "network" || kind === "timeout" || kind === "missing-key" || kind === "auth";
102273
102280
  }
102274
- var BILLING_URL, MAX_CAUSE_LINES, MAX_CAUSE_CHARS, NETWORK;
102281
+ var BILLING_URL, MAX_CAUSE_LINES, MAX_CAUSE_CHARS, TIMEOUT, NETWORK;
102275
102282
  var init_describe_error = __esm({
102276
102283
  "src/render/describe-error.ts"() {
102277
102284
  "use strict";
@@ -102279,6 +102286,7 @@ var init_describe_error = __esm({
102279
102286
  BILLING_URL = "https://spacecode.g-a-l-a-c-t-i-c.com";
102280
102287
  MAX_CAUSE_LINES = 3;
102281
102288
  MAX_CAUSE_CHARS = 300;
102289
+ TIMEOUT = /Stream stalled|Stream exceeded|No event within/i;
102282
102290
  NETWORK = /fetch failed|ENOTFOUND|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EAI_AGAIN|network (error|request failed|is unreachable)|socket hang up/i;
102283
102291
  }
102284
102292
  });
@@ -102799,8 +102807,8 @@ function formatTurnSummary({ toolCount, elapsedMs, inputTokens, outputTokens, in
102799
102807
  return `${head} ${bar} ${formatTokens(inputTokens)} ${arrow} ${formatTokens(outputTokens)} ${bar} ${formatCost(estimateCostUsd(inputTokens, outputTokens))}`;
102800
102808
  }
102801
102809
  function formatThinking(elapsedMs, approxTokens, unicode = true) {
102802
- const [mark, dots, sep2] = unicode ? ["\u273B", "\u2026", "\xB7"] : ["*", "...", "-"];
102803
- return `${mark} Thinking${dots} (${Math.floor(elapsedMs / 1e3)}s ${sep2} ~${formatTokens(approxTokens)} tokens ${sep2} esc to interrupt)`;
102810
+ const [mark, dots, sep3] = unicode ? ["\u273B", "\u2026", "\xB7"] : ["*", "...", "-"];
102811
+ return `${mark} Thinking${dots} (${Math.floor(elapsedMs / 1e3)}s ${sep3} ~${formatTokens(approxTokens)} tokens ${sep3} esc to interrupt)`;
102804
102812
  }
102805
102813
  var ESTIMATED_USD_PER_MTOK, MODEL_PLACEHOLDER, CONTEXT_WARN_PCT, CONTEXT_CRITICAL_PCT, BAR_CELLS, BAR_MIN_COLUMNS;
102806
102814
  var init_footer = __esm({
@@ -103113,6 +103121,7 @@ var init_slash_commands = __esm({
103113
103121
  { name: "cost", description: "Show session token usage and approximate cost" },
103114
103122
  { name: "context", description: "Show how much of the context window is used" },
103115
103123
  { name: "status", description: "Show project, session, mode, context and cost" },
103124
+ { name: "record", description: "Session recording: /record off|on|status" },
103116
103125
  { name: "compact", description: "Summarize the conversation to free up context" },
103117
103126
  { name: "model", description: "List gateway models; /model <n|id> switches for this session" },
103118
103127
  { name: "turns", description: "Browse earlier turns and open their steps (shortcut: ctrl+o)" },
@@ -103962,6 +103971,439 @@ var init_node_mentions = __esm({
103962
103971
  }
103963
103972
  });
103964
103973
 
103974
+ // src/recording/message-diff.ts
103975
+ import { createHash as createHash2 } from "node:crypto";
103976
+ function stableStringify(v2) {
103977
+ if (Array.isArray(v2)) return `[${v2.map((x2) => stableStringify(x2 === void 0 ? null : x2)).join(",")}]`;
103978
+ if (v2 && typeof v2 === "object") {
103979
+ const entries = Object.entries(v2).filter(([, x2]) => x2 !== void 0).sort(([a], [b2]) => a < b2 ? -1 : a > b2 ? 1 : 0).map(([k2, x2]) => `${JSON.stringify(k2)}:${stableStringify(x2)}`);
103980
+ return `{${entries.join(",")}}`;
103981
+ }
103982
+ return JSON.stringify(v2) ?? "null";
103983
+ }
103984
+ function messageHash(m2) {
103985
+ return createHash2("sha256").update(stableStringify(m2)).digest("hex");
103986
+ }
103987
+ function diffMessages(prevHashes, messages) {
103988
+ const hashes = messages.map(messageHash);
103989
+ let p = 0;
103990
+ while (p < prevHashes.length && p < hashes.length && prevHashes[p] === hashes[p]) p++;
103991
+ if (p === prevHashes.length) {
103992
+ return { events: messages.length === p ? [] : [{ type: "append", messages: messages.slice(p) }], hashes };
103993
+ }
103994
+ return { events: [{ type: "rewrite", messages }], hashes };
103995
+ }
103996
+ var init_message_diff = __esm({
103997
+ "src/recording/message-diff.ts"() {
103998
+ "use strict";
103999
+ }
104000
+ });
104001
+
104002
+ // src/recording/recording-chunks.ts
104003
+ import { createHash as createHash3 } from "node:crypto";
104004
+ function splitOversize(event, maxBytes) {
104005
+ const s = JSON.stringify(event);
104006
+ const size = Math.max(1, Math.floor(maxBytes / 8));
104007
+ const pieces = [];
104008
+ for (let i = 0; i < s.length; i += size) pieces.push(s.slice(i, i + size));
104009
+ const id = createHash3("sha256").update(s).digest("hex");
104010
+ return pieces.map((data, index) => ({ type: "part", id, index, of: pieces.length, data }));
104011
+ }
104012
+ function packEvents(events, maxBytes = DEFAULT_MAX_BYTES) {
104013
+ const lines2 = [];
104014
+ for (const event of events) {
104015
+ const line = `${JSON.stringify(event)}
104016
+ `;
104017
+ if (Buffer.byteLength(line) <= maxBytes) {
104018
+ lines2.push(line);
104019
+ continue;
104020
+ }
104021
+ for (const part2 of splitOversize(event, maxBytes)) {
104022
+ const partLine = `${JSON.stringify(part2)}
104023
+ `;
104024
+ if (Buffer.byteLength(partLine) > maxBytes) throw new Error(`maxBytes ${maxBytes} is too small to hold a recording part`);
104025
+ lines2.push(partLine);
104026
+ }
104027
+ }
104028
+ const chunks = [];
104029
+ let current = "";
104030
+ let currentBytes = 0;
104031
+ for (const line of lines2) {
104032
+ const bytes = Buffer.byteLength(line);
104033
+ if (current !== "" && currentBytes + bytes > maxBytes) {
104034
+ chunks.push(current);
104035
+ current = "";
104036
+ currentBytes = 0;
104037
+ }
104038
+ current += line;
104039
+ currentBytes += bytes;
104040
+ }
104041
+ if (current !== "") chunks.push(current);
104042
+ return chunks;
104043
+ }
104044
+ var DEFAULT_MAX_BYTES;
104045
+ var init_recording_chunks = __esm({
104046
+ "src/recording/recording-chunks.ts"() {
104047
+ "use strict";
104048
+ DEFAULT_MAX_BYTES = 262144;
104049
+ }
104050
+ });
104051
+
104052
+ // src/recording/recorder.ts
104053
+ import { createHash as createHash4 } from "node:crypto";
104054
+ import { mkdir as mkdir4, readFile as readFile5, rename as rename4, rm as rm2, writeFile as writeFile4, readdir as readdir4, stat as stat3 } from "node:fs/promises";
104055
+ import { join as join7 } from "node:path";
104056
+ function projectHash(root) {
104057
+ return createHash4("sha256").update(root).digest("hex");
104058
+ }
104059
+ function createRecorder(deps) {
104060
+ const states = /* @__PURE__ */ new Map();
104061
+ let lastError = null;
104062
+ let off2 = null;
104063
+ let inflight = null;
104064
+ let retryAttempt = 0;
104065
+ let timerArmed = false;
104066
+ const stateFile = (sessionId) => join7(deps.dir, `${sessionId}.json`);
104067
+ async function save(sessionId, st) {
104068
+ await mkdir4(deps.dir, { recursive: true, mode: 448 });
104069
+ const tmp = `${stateFile(sessionId)}.${deps.randomId()}.tmp`;
104070
+ await writeFile4(tmp, JSON.stringify(st), { mode: 384 });
104071
+ await rename4(tmp, stateFile(sessionId));
104072
+ }
104073
+ async function load(sessionId) {
104074
+ const cached = states.get(sessionId);
104075
+ if (cached) return cached;
104076
+ let st;
104077
+ try {
104078
+ st = JSON.parse(await readFile5(stateFile(sessionId), "utf8"));
104079
+ } catch (e) {
104080
+ if (e.code !== "ENOENT") {
104081
+ const bad = join7(deps.dir, "bad");
104082
+ await mkdir4(bad, { recursive: true, mode: 448 });
104083
+ await rename4(stateFile(sessionId), join7(bad, `${sessionId}.${deps.now()}.json`)).catch((err) => deps.log(`recording: could not quarantine ${sessionId}: ${String(err)}`));
104084
+ lastError = `recording state for ${sessionId} was unreadable and was quarantined`;
104085
+ deps.log(`recording: ${lastError}`);
104086
+ }
104087
+ st = { recordingId: deps.randomId(), hashes: [], nextSeq: 1, queue: [], parentRecordingId: null, meta: null, stopped: null };
104088
+ }
104089
+ states.set(sessionId, st);
104090
+ return st;
104091
+ }
104092
+ async function queuedBytes() {
104093
+ let total = 0;
104094
+ for (const st of states.values()) for (const q2 of st.queue) total += q2.text.length;
104095
+ return total;
104096
+ }
104097
+ async function record(sessionId, messages, meta) {
104098
+ try {
104099
+ if (off2) return;
104100
+ const st = await load(sessionId);
104101
+ if (st.stopped) return;
104102
+ if (st.hashes.length === 0 && st.nextSeq === 1 && meta.parentRecordingId) st.parentRecordingId = meta.parentRecordingId;
104103
+ st.meta = meta;
104104
+ const safe = redactCredentials(messages);
104105
+ const { events, hashes } = diffMessages(st.hashes, safe);
104106
+ if (events.length > 0) {
104107
+ if (await queuedBytes() > MAX_QUEUE_BYTES) {
104108
+ st.stopped = "local recording queue is full (100 MB); recording stopped for this session";
104109
+ lastError = st.stopped;
104110
+ await save(sessionId, st);
104111
+ return;
104112
+ }
104113
+ const now = deps.now();
104114
+ for (const text of packEvents(events)) st.queue.push({ seq: st.nextSeq++, text, queuedAt: now });
104115
+ st.hashes = hashes;
104116
+ }
104117
+ await save(sessionId, st);
104118
+ void flush();
104119
+ } catch (e) {
104120
+ lastError = `could not queue recording: ${e instanceof Error ? e.message : String(e)}`;
104121
+ deps.log(`recording: ${lastError}`);
104122
+ }
104123
+ }
104124
+ async function sendOne(st, item) {
104125
+ const m2 = st.meta;
104126
+ const meta = m2 ? encodeURIComponent(JSON.stringify({
104127
+ sessionId: m2.sessionId,
104128
+ parentRecordingId: st.parentRecordingId,
104129
+ title: m2.title.slice(0, 200),
104130
+ projectHash: projectHash(m2.projectRoot),
104131
+ turns: m2.turns,
104132
+ inputTokens: m2.tokens.input,
104133
+ outputTokens: m2.tokens.output
104134
+ })) : "";
104135
+ let res;
104136
+ try {
104137
+ res = await deps.fetch(`${deps.baseUrl.replace(/\/$/, "")}/api/recordings/${st.recordingId}/chunks/${item.seq}`, {
104138
+ method: "PUT",
104139
+ headers: { authorization: `Bearer ${deps.apiKey}`, "content-type": "application/octet-stream", ...meta ? { "x-spacecode-recording-meta": meta } : {} },
104140
+ body: item.text
104141
+ });
104142
+ } catch (e) {
104143
+ lastError = `recording upload failed: ${e instanceof Error ? e.message : String(e)}`;
104144
+ return "retry";
104145
+ }
104146
+ if (res.ok) return "ok";
104147
+ const body = await res.text().catch(() => "");
104148
+ if (res.status === 409) {
104149
+ const lastSeq = (() => {
104150
+ try {
104151
+ return JSON.parse(body).last_seq ?? -1;
104152
+ } catch {
104153
+ return -1;
104154
+ }
104155
+ })();
104156
+ if (lastSeq >= item.seq) return "ok";
104157
+ lastError = `recording out of sync with the server (server has ${lastSeq}, next to send ${item.seq})`;
104158
+ st.stopped = lastError;
104159
+ return "stop";
104160
+ }
104161
+ if (res.status === 403) {
104162
+ st.stopped = "not recording: no tenant or recording is not enabled for this account";
104163
+ lastError = st.stopped;
104164
+ return "stop";
104165
+ }
104166
+ if (res.status === 401) {
104167
+ lastError = "not recording: sign in (spacecode login)";
104168
+ return "retry";
104169
+ }
104170
+ if (res.status === 413) {
104171
+ st.stopped = `not recording: server limit reached (${body.slice(0, 120)})`;
104172
+ lastError = st.stopped;
104173
+ return "stop";
104174
+ }
104175
+ lastError = `recording upload failed: HTTP ${res.status}`;
104176
+ return "retry";
104177
+ }
104178
+ async function flushOnce() {
104179
+ let allSent = true;
104180
+ for (const [sessionId, st] of states) {
104181
+ while (st.queue.length > 0 && !st.stopped) {
104182
+ const item = st.queue[0];
104183
+ const out = await sendOne(st, item);
104184
+ if (out === "ok") {
104185
+ st.queue.shift();
104186
+ lastError = null;
104187
+ await save(sessionId, st).catch((e) => deps.log(`recording: could not save state: ${String(e)}`));
104188
+ continue;
104189
+ }
104190
+ if (out === "retry") allSent = false;
104191
+ await save(sessionId, st).catch((e) => deps.log(`recording: could not save state: ${String(e)}`));
104192
+ break;
104193
+ }
104194
+ }
104195
+ return allSent;
104196
+ }
104197
+ function flush() {
104198
+ if (inflight) return inflight;
104199
+ inflight = (async () => {
104200
+ try {
104201
+ const done = await flushOnce();
104202
+ if (done) retryAttempt = 0;
104203
+ else scheduleRetry();
104204
+ } catch (e) {
104205
+ lastError = `recording flush failed: ${e instanceof Error ? e.message : String(e)}`;
104206
+ deps.log(`recording: ${lastError}`);
104207
+ scheduleRetry();
104208
+ } finally {
104209
+ inflight = null;
104210
+ }
104211
+ })();
104212
+ return inflight;
104213
+ }
104214
+ function scheduleRetry() {
104215
+ if (timerArmed) return;
104216
+ timerArmed = true;
104217
+ const ms = RETRY_MS[Math.min(retryAttempt++, RETRY_MS.length - 1)];
104218
+ const arm = deps.setTimer ?? ((fn, t) => setTimeout(fn, t));
104219
+ arm(() => {
104220
+ timerArmed = false;
104221
+ void flush();
104222
+ }, ms).unref?.();
104223
+ }
104224
+ function status() {
104225
+ if (off2) return { state: "off", detail: `not recording: ${off2}`, queued: 0, oldestAgeMs: null, lastError };
104226
+ let queued = 0;
104227
+ let oldest = null;
104228
+ let stopped = null;
104229
+ for (const st of states.values()) {
104230
+ queued += st.queue.length;
104231
+ for (const q2 of st.queue) oldest = oldest === null ? q2.queuedAt : Math.min(oldest, q2.queuedAt);
104232
+ stopped ??= st.stopped;
104233
+ }
104234
+ if (stopped) return { state: "stopped", detail: stopped, queued, oldestAgeMs: null, lastError };
104235
+ const age = oldest === null ? null : Math.max(0, deps.now() - oldest);
104236
+ if (queued > 0) return { state: lastError ? "queued" : "uploading", detail: `${queued} queued${lastError ? `, last error: ${lastError}` : ""}`, queued, oldestAgeMs: age, lastError };
104237
+ return { state: "idle", detail: "recording, all uploaded", queued: 0, oldestAgeMs: null, lastError };
104238
+ }
104239
+ function setOff(reason) {
104240
+ off2 = reason;
104241
+ }
104242
+ async function resume() {
104243
+ let names;
104244
+ try {
104245
+ names = await readdir4(deps.dir);
104246
+ } catch (e) {
104247
+ if (e.code === "ENOENT") return;
104248
+ throw e;
104249
+ }
104250
+ for (const n of names) {
104251
+ if (!n.endsWith(".json")) continue;
104252
+ const id = n.slice(0, -5);
104253
+ const st = await load(id);
104254
+ if (st.queue.length === 0) states.delete(id);
104255
+ }
104256
+ if ([...states.values()].some((s) => s.queue.length > 0)) void flush();
104257
+ }
104258
+ return { record, flush, status, setOff, resume };
104259
+ }
104260
+ var MAX_QUEUE_BYTES, RETRY_MS;
104261
+ var init_recorder = __esm({
104262
+ "src/recording/recorder.ts"() {
104263
+ "use strict";
104264
+ init_session_store();
104265
+ init_message_diff();
104266
+ init_recording_chunks();
104267
+ MAX_QUEUE_BYTES = 100 * 1024 * 1024;
104268
+ RETRY_MS = [2e3, 1e4, 3e4, 12e4, 3e5];
104269
+ }
104270
+ });
104271
+
104272
+ // src/recording/recording-runtime.ts
104273
+ import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile5, stat as stat4 } from "node:fs/promises";
104274
+ import { join as join8, sep as sep2 } from "node:path";
104275
+ function matchesExclude(pattern, root) {
104276
+ const re3 = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, `[^${sep2 === "\\" ? "\\\\" : "/"}]*`).replace(/\u0000/g, ".*");
104277
+ return new RegExp(`^${re3}(?:[/\\\\].*)?$`).test(root);
104278
+ }
104279
+ async function exists(path12) {
104280
+ try {
104281
+ await stat4(path12);
104282
+ return true;
104283
+ } catch (e) {
104284
+ if (e.code === "ENOENT") return false;
104285
+ throw e;
104286
+ }
104287
+ }
104288
+ function createRecordingRuntime(deps) {
104289
+ const dir = join8(deps.configDir, "recordings");
104290
+ const noticeFile = join8(dir, ".notice");
104291
+ let recorder = null;
104292
+ let standingOff;
104293
+ let sessionOff = false;
104294
+ async function decide() {
104295
+ if (deps.env.SPACECODE_RECORD?.trim().toLowerCase() === "off") return "SPACECODE_RECORD=off";
104296
+ let cfg = {};
104297
+ try {
104298
+ cfg = JSON.parse(await readFile6(join8(deps.configDir, "config.json"), "utf8"));
104299
+ } catch (e) {
104300
+ if (e.code !== "ENOENT") return `~/.spacecode/config.json is unreadable (${e instanceof Error ? e.message : String(e)}); fix it to record`;
104301
+ }
104302
+ if (cfg.recording?.enabled === false) return "turned off in ~/.spacecode/config.json (recording.enabled)";
104303
+ const excludes = Array.isArray(cfg.recording?.exclude) ? cfg.recording.exclude.filter((x2) => typeof x2 === "string") : [];
104304
+ const hit = excludes.find((p) => matchesExclude(p, deps.projectRoot));
104305
+ if (hit) return `this folder matches recording.exclude (${hit})`;
104306
+ if (await exists(join8(deps.projectRoot, ".spacecode", "norecord"))) return "this repo has .spacecode/norecord";
104307
+ return null;
104308
+ }
104309
+ async function noticeShown() {
104310
+ return exists(noticeFile);
104311
+ }
104312
+ async function ackNotice() {
104313
+ await mkdir5(dir, { recursive: true, mode: 448 });
104314
+ await writeFile5(noticeFile, new Date((deps.now ?? Date.now)()).toISOString(), { mode: 384 });
104315
+ }
104316
+ async function ensure() {
104317
+ if (recorder) return recorder;
104318
+ const apiKey = await deps.resolveApiKey();
104319
+ if (!apiKey) return null;
104320
+ recorder = createRecorder({
104321
+ dir,
104322
+ baseUrl: deps.baseUrl,
104323
+ apiKey,
104324
+ fetch: deps.fetch,
104325
+ now: deps.now ?? Date.now,
104326
+ randomId: () => `rec-${crypto.randomUUID()}`,
104327
+ log: deps.log ?? (() => {
104328
+ })
104329
+ });
104330
+ await recorder.resume();
104331
+ return recorder;
104332
+ }
104333
+ return {
104334
+ /**
104335
+ * Called after a successful session save. Returns the notice text the first time (the caller must show it; uploads start
104336
+ * on the next call, after it has been shown), otherwise null. Never throws.
104337
+ */
104338
+ async afterSave(sessionId, messages, meta) {
104339
+ try {
104340
+ if (sessionOff) return null;
104341
+ standingOff ??= await decide();
104342
+ if (standingOff) return null;
104343
+ if (!await noticeShown()) {
104344
+ await ackNotice();
104345
+ return RECORDING_NOTICE;
104346
+ }
104347
+ const r = await ensure();
104348
+ if (!r) return null;
104349
+ await r.record(sessionId, messages, meta);
104350
+ return null;
104351
+ } catch (e) {
104352
+ deps.log?.(`recording: ${e instanceof Error ? e.message : String(e)}`);
104353
+ return null;
104354
+ }
104355
+ },
104356
+ /** One status line for /status; always says why when nothing is being recorded. */
104357
+ async statusLine() {
104358
+ try {
104359
+ if (sessionOff) return "recording: off for this session (/record on to resume)";
104360
+ standingOff ??= await decide();
104361
+ if (standingOff) return `recording: not recording: ${standingOff}`;
104362
+ if (recorder) {
104363
+ const s = recorder.status();
104364
+ const age = s.oldestAgeMs !== null && s.queued > 0 ? `, oldest ${Math.round(s.oldestAgeMs / 1e3)}s` : "";
104365
+ return `recording: ${s.detail}${age}`;
104366
+ }
104367
+ return await noticeShown() ? "recording: on (starts with your next saved turn)" : "recording: on, starts after the first notice (shown after your next turn)";
104368
+ } catch (e) {
104369
+ return `recording: status unavailable (${e instanceof Error ? e.message : String(e)})`;
104370
+ }
104371
+ },
104372
+ /** `/record [on|off|status]`; returns the text to show. */
104373
+ async command(arg) {
104374
+ const a = arg.trim().toLowerCase();
104375
+ if (a === "off") {
104376
+ sessionOff = true;
104377
+ recorder?.setOff("turned off with /record off");
104378
+ return "Recording is off for this session. What was already uploaded stays until it expires (90 days).";
104379
+ }
104380
+ if (a === "on") {
104381
+ sessionOff = false;
104382
+ recorder?.setOff(null);
104383
+ standingOff = void 0;
104384
+ return `Recording resumes with your next saved turn.
104385
+ ${await this.statusLine()}`;
104386
+ }
104387
+ if (a === "" || a === "status") return `${await this.statusLine()}
104388
+ ${RECORDING_NOTICE}`;
104389
+ return "Usage: /record [on|off|status]";
104390
+ }
104391
+ };
104392
+ }
104393
+ var RECORDING_NOTICE;
104394
+ var init_recording_runtime = __esm({
104395
+ "src/recording/recording-runtime.ts"() {
104396
+ "use strict";
104397
+ init_recorder();
104398
+ RECORDING_NOTICE = [
104399
+ "Session recording is on for your account when your workspace has it enabled.",
104400
+ "Your prompts, the assistant's replies and thinking, and tool calls and results are uploaded, encrypted at rest, kept 90 days, and visible only to your account.",
104401
+ "Credential-shaped text is replaced before upload, but redaction is a best-effort net, not a guarantee.",
104402
+ "Turn it off: /record off (this session), `SPACECODE_RECORD=off` (always), or add the folder to `recording.exclude` in ~/.spacecode/config.json or create `.spacecode/norecord` in the repo."
104403
+ ].join("\n");
104404
+ }
104405
+ });
104406
+
103965
104407
  // src/skills-command.ts
103966
104408
  async function runSkillsCommand(args, deps) {
103967
104409
  const [sub, name, file] = args.trim().split(/\s+/);
@@ -104002,11 +104444,11 @@ var init_skills_command = __esm({
104002
104444
  });
104003
104445
 
104004
104446
  // src/permission-rules-loader.ts
104005
- import { readFile as readFile5 } from "node:fs/promises";
104006
- import { join as join7 } from "node:path";
104447
+ import { readFile as readFile7 } from "node:fs/promises";
104448
+ import { join as join9 } from "node:path";
104007
104449
  async function readOrNull2(path12) {
104008
104450
  try {
104009
- return await readFile5(path12, "utf8");
104451
+ return await readFile7(path12, "utf8");
104010
104452
  } catch (err) {
104011
104453
  if (err.code === "ENOENT") return null;
104012
104454
  throw err;
@@ -104014,7 +104456,7 @@ async function readOrNull2(path12) {
104014
104456
  }
104015
104457
  async function loadPermissionRules(deps) {
104016
104458
  const none = (line) => ({ rules: void 0, defaultMode: void 0, statusLines: [line] });
104017
- const configPath = join7(deps.configDir, "imports.json");
104459
+ const configPath = join9(deps.configDir, "imports.json");
104018
104460
  try {
104019
104461
  const loaded = parseImportsConfig(await readOrNull2(configPath), configPath);
104020
104462
  if (loaded.error) return none("permission rules: off (imports.json unreadable)");
@@ -104023,13 +104465,13 @@ async function loadPermissionRules(deps) {
104023
104465
  return none(`permission rules: off (imports.json not read: ${err instanceof Error ? err.message : String(err)})`);
104024
104466
  }
104025
104467
  const lines2 = [];
104026
- const files = [{ scope: "user", path: join7(deps.homeDir, ".claude", "settings.json") }];
104468
+ const files = [{ scope: "user", path: join9(deps.homeDir, ".claude", "settings.json") }];
104027
104469
  try {
104028
104470
  const sources2 = await readRepoSources(deps.cwd);
104029
104471
  if (sources2[".claude/settings.json"] !== void 0 || sources2[".claude/settings.local.json"] !== void 0) {
104030
104472
  const status = await createNodeTrustStore(deps.configDir).repoStatus(deps.cwd, sources2);
104031
104473
  if (status === "trusted" || status === "session") {
104032
- files.push({ scope: "repo", path: join7(deps.cwd, ".claude", "settings.json") }, { scope: "local", path: join7(deps.cwd, ".claude", "settings.local.json") });
104474
+ files.push({ scope: "repo", path: join9(deps.cwd, ".claude", "settings.json") }, { scope: "local", path: join9(deps.cwd, ".claude", "settings.local.json") });
104033
104475
  } else lines2.push(`permission rules: repo settings NOT loaded (${status === "never" ? "marked never trust" : "untrusted repo"})`);
104034
104476
  }
104035
104477
  } catch (err) {
@@ -104071,7 +104513,7 @@ var init_permission_rules_loader = __esm({
104071
104513
  });
104072
104514
 
104073
104515
  // src/command-loader.ts
104074
- import { join as join8 } from "node:path";
104516
+ import { join as join10 } from "node:path";
104075
104517
  async function userCommands(deps, lines2) {
104076
104518
  try {
104077
104519
  const apiKey = await resolveApiKey({ env: deps.env, readFile: deps.readFile, homedir: deps.homedir });
@@ -104126,7 +104568,7 @@ async function repoCommands(deps, lines2) {
104126
104568
  }
104127
104569
  }
104128
104570
  async function loadCommands(deps) {
104129
- const configPath = join8(deps.configDir, "imports.json");
104571
+ const configPath = join10(deps.configDir, "imports.json");
104130
104572
  try {
104131
104573
  const loaded = parseImportsConfig(await deps.readFile(configPath).catch((err) => {
104132
104574
  if (err.code === "ENOENT") return null;
@@ -104159,9 +104601,9 @@ var init_command_loader = __esm({
104159
104601
  });
104160
104602
 
104161
104603
  // src/useReplController.ts
104162
- import { readFile as readFile6, writeFile as writeFile4, mkdir as mkdir4 } from "node:fs/promises";
104604
+ import { readFile as readFile8, writeFile as writeFile6, mkdir as mkdir6 } from "node:fs/promises";
104163
104605
  import { homedir as homedir2 } from "node:os";
104164
- import { join as join9 } from "node:path";
104606
+ import { join as join11 } from "node:path";
104165
104607
  import { execFile as execFile3 } from "node:child_process";
104166
104608
  function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mentionsOpt, sessions, resumeLatest: resumeLatest2, onViewTurns, onViewResume, isEscOwned }) {
104167
104609
  const { exit } = use_app_default();
@@ -104189,6 +104631,16 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
104189
104631
  const profilePromiseRef = (0, import_react30.useRef)(null);
104190
104632
  const profileRef = (0, import_react30.useRef)(null);
104191
104633
  const mcpRef = (0, import_react30.useRef)(null);
104634
+ const recordingRef = (0, import_react30.useRef)(null);
104635
+ const recording = () => recordingRef.current ??= createRecordingRuntime({
104636
+ env: process.env,
104637
+ configDir: process.env.SPACECODE_CONFIG_DIR?.trim() || join11(homedir2(), ".spacecode"),
104638
+ projectRoot,
104639
+ fetch,
104640
+ baseUrl: backendUrl(process.env),
104641
+ resolveApiKey: () => resolveApiKey({ env: process.env, readFile: (p) => readFile8(p, "utf-8"), homedir: homedir2 }).catch(() => null),
104642
+ log: (m2) => console.error(`[agent-ui] ${m2}`)
104643
+ });
104192
104644
  const resumeListRef = (0, import_react30.useRef)([]);
104193
104645
  const [model, setModel] = (0, import_react30.useState)(null);
104194
104646
  const [todos, setTodosState] = (0, import_react30.useState)([]);
@@ -104209,10 +104661,10 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
104209
104661
  (0, import_react30.useEffect)(() => {
104210
104662
  let live2 = true;
104211
104663
  loadCommands({
104212
- configDir: process.env.SPACECODE_CONFIG_DIR?.trim() || join9(homedir2(), ".spacecode"),
104664
+ configDir: process.env.SPACECODE_CONFIG_DIR?.trim() || join11(homedir2(), ".spacecode"),
104213
104665
  cwd: process.cwd(),
104214
104666
  env: process.env,
104215
- readFile: (p) => readFile6(p, "utf-8"),
104667
+ readFile: (p) => readFile8(p, "utf-8"),
104216
104668
  homedir: homedir2,
104217
104669
  fetch,
104218
104670
  builtins: SLASH_COMMANDS.map((c) => c.name)
@@ -104312,6 +104764,9 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
104312
104764
  const id = sessionIdRef.current ??= sessions.newId();
104313
104765
  const createdAt = sessionCreatedAtRef.current ??= (/* @__PURE__ */ new Date()).toISOString();
104314
104766
  await sessions.save({ meta: { v: 1, id, projectRoot, createdAt, updatedAt: createdAt, tokens, title: titleFromMessages(messages), turns: countTurns(messages), todos: todosRef.current }, messages });
104767
+ void recording().afterSave(id, messages, { sessionId: id, projectRoot, title: titleFromMessages(messages), turns: countTurns(messages), tokens }).then((firstRunNotice) => {
104768
+ if (firstRunNotice) notice(firstRunNotice);
104769
+ });
104315
104770
  } catch (err) {
104316
104771
  noticeError(describeError("session-save", err instanceof Error ? err.message : String(err)));
104317
104772
  }
@@ -104426,7 +104881,11 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
104426
104881
  case "context":
104427
104882
  notice(formatContextReport({ contextTokens: estimateTotalTokens("", history), maxContextTokens: DEFAULT_MAX_CONTEXT_TOKENS }));
104428
104883
  return;
104429
- case "status":
104884
+ case "record":
104885
+ notice(await recording().command(slash.args));
104886
+ return;
104887
+ case "status": {
104888
+ const recordingLine = await recording().statusLine();
104430
104889
  notice(formatStatusReport({
104431
104890
  mode: permissions.mode,
104432
104891
  sessionId: sessionIdRef.current,
@@ -104435,9 +104894,10 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
104435
104894
  maxContextTokens: DEFAULT_MAX_CONTEXT_TOKENS,
104436
104895
  tokens: sessionTokensRef.current,
104437
104896
  costUsd: estimateCostUsd(sessionTokensRef.current.input, sessionTokensRef.current.output),
104438
- profileLines: [...profileRef.current?.statusLines ?? ["profile: not loaded yet (loads on your first message)"], ...mcpRef.current?.statusLines() ?? [], ...commandsLoaded.statusLines]
104897
+ profileLines: [...profileRef.current?.statusLines ?? ["profile: not loaded yet (loads on your first message)"], ...mcpRef.current?.statusLines() ?? [], ...commandsLoaded.statusLines, recordingLine]
104439
104898
  }));
104440
104899
  return;
104900
+ }
104441
104901
  case "resume": {
104442
104902
  if (!sessions) {
104443
104903
  notice("Session storage is not available, so there is nothing to resume.");
@@ -104492,7 +104952,7 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
104492
104952
  }
104493
104953
  setIsRunning(true);
104494
104954
  try {
104495
- const apiKey2 = await resolveApiKey({ env: process.env, readFile: (p) => readFile6(p, "utf-8"), homedir: homedir2 });
104955
+ const apiKey2 = await resolveApiKey({ env: process.env, readFile: (p) => readFile8(p, "utf-8"), homedir: homedir2 });
104496
104956
  if (!apiKey2) {
104497
104957
  notice(MISSING_KEY_MESSAGE);
104498
104958
  return;
@@ -104514,7 +104974,7 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
104514
104974
  }
104515
104975
  case "model": {
104516
104976
  try {
104517
- const apiKey2 = await resolveApiKey({ env: process.env, readFile: (p) => readFile6(p, "utf-8"), homedir: homedir2 });
104977
+ const apiKey2 = await resolveApiKey({ env: process.env, readFile: (p) => readFile8(p, "utf-8"), homedir: homedir2 });
104518
104978
  if (!apiKey2) {
104519
104979
  notice(MISSING_KEY_MESSAGE);
104520
104980
  return;
@@ -104549,7 +105009,7 @@ ${list2.map((c) => ` /${c.name} (${c.scope})${c.description ? ` ${c.descripti
104549
105009
  }
104550
105010
  case "skills": {
104551
105011
  const state = profileRef.current;
104552
- const apiKey2 = await resolveApiKey({ env: process.env, readFile: (p) => readFile6(p, "utf-8"), homedir: homedir2 }).catch(() => null);
105012
+ const apiKey2 = await resolveApiKey({ env: process.env, readFile: (p) => readFile8(p, "utf-8"), homedir: homedir2 }).catch(() => null);
104553
105013
  const client = apiKey2 ? createProfileClient({ baseUrl: backendUrl(process.env), apiKey: apiKey2, fetch }) : null;
104554
105014
  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)"] }));
104555
105015
  return;
@@ -104571,7 +105031,7 @@ ${list2.map((c) => ` /${c.name} (${c.scope})${c.description ? ` ${c.descripti
104571
105031
  try {
104572
105032
  apiKey = await resolveApiKey({
104573
105033
  env: process.env,
104574
- readFile: (p) => readFile6(p, "utf-8"),
105034
+ readFile: (p) => readFile8(p, "utf-8"),
104575
105035
  homedir: homedir2
104576
105036
  });
104577
105037
  } catch (err) {
@@ -104608,17 +105068,17 @@ ${list2.map((c) => ` /${c.name} (${c.scope})${c.description ? ` ${c.descripti
104608
105068
  try {
104609
105069
  profilePromiseRef.current ??= prepareProfile({
104610
105070
  env: process.env,
104611
- readFile: (p) => readFile6(p, "utf-8"),
104612
- writeFile: (p, d) => writeFile4(p, d, { encoding: "utf-8", mode: 384 }),
105071
+ readFile: (p) => readFile8(p, "utf-8"),
105072
+ writeFile: (p, d) => writeFile6(p, d, { encoding: "utf-8", mode: 384 }),
104613
105073
  mkdir: async (p) => {
104614
- await mkdir4(p, { recursive: true });
105074
+ await mkdir6(p, { recursive: true });
104615
105075
  },
104616
105076
  homedir: homedir2,
104617
105077
  fetch,
104618
105078
  cwd: process.cwd(),
104619
105079
  gitRoot: gitTopLevel,
104620
105080
  importGate: async () => {
104621
- const configDir = process.env.SPACECODE_CONFIG_DIR?.trim() || join9(homedir2(), ".spacecode");
105081
+ const configDir = process.env.SPACECODE_CONFIG_DIR?.trim() || join11(homedir2(), ".spacecode");
104622
105082
  const gate = await importGateStatus({ configDir, cwd: process.cwd(), interactive: Boolean(process.stdin.isTTY) });
104623
105083
  const loaded = await loadPermissionRules({ configDir, homeDir: homedir2(), cwd: process.cwd() });
104624
105084
  rulesRef.current = loaded.rules;
@@ -104628,7 +105088,7 @@ ${list2.map((c) => ` /${c.name} (${c.scope})${c.description ? ` ${c.descripti
104628
105088
  });
104629
105089
  profileRef.current = await profilePromiseRef.current;
104630
105090
  if (mcpRef.current === null && (profileRef.current?.mcpDefs?.length ?? 0) > 0) {
104631
- const configDir = process.env.SPACECODE_CONFIG_DIR?.trim() || join9(homedir2(), ".spacecode");
105091
+ const configDir = process.env.SPACECODE_CONFIG_DIR?.trim() || join11(homedir2(), ".spacecode");
104632
105092
  const trust = createNodeTrustStore(configDir);
104633
105093
  const manager = new McpManager({
104634
105094
  defs: profileRef.current.mcpDefs,
@@ -104638,7 +105098,7 @@ ${list2.map((c) => ` /${c.name} (${c.scope})${c.description ? ` ${c.descripti
104638
105098
  ...rulesRef.current ? { rules: rulesRef.current } : {},
104639
105099
  permissions,
104640
105100
  confirm: askUser,
104641
- audit: new FileAuditLog(join9(homedir2(), ".spacecode", "audit.jsonl")),
105101
+ audit: new FileAuditLog(join11(homedir2(), ".spacecode", "audit.jsonl")),
104642
105102
  clientVersion: process.env.SPACECODE_VERSION ?? "0"
104643
105103
  });
104644
105104
  mcpRef.current = manager;
@@ -104769,6 +105229,7 @@ var init_useReplController = __esm({
104769
105229
  init_node_mentions();
104770
105230
  init_api_key();
104771
105231
  init_profile_runtime();
105232
+ init_recording_runtime();
104772
105233
  init_skills_command();
104773
105234
  init_src();
104774
105235
  init_mcp_manager();
@@ -104905,14 +105366,14 @@ import path9 from "node:path";
104905
105366
  function orderSessions(sessions, projectRoot) {
104906
105367
  return [...sessions.filter((s) => s.projectRoot === projectRoot), ...sessions.filter((s) => s.projectRoot !== projectRoot)];
104907
105368
  }
104908
- function formatResumeRow(meta, now, columns, sep2 = "\xB7", ellipsis = "\u2026") {
105369
+ function formatResumeRow(meta, now, columns, sep3 = "\xB7", ellipsis = "\u2026") {
104909
105370
  const project = path9.basename(meta.projectRoot) || meta.projectRoot;
104910
- const tail = [meta.turns !== void 0 ? `${meta.turns} turn${meta.turns === 1 ? "" : "s"}` : null, formatCost(estimateCostUsd(meta.tokens.input, meta.tokens.output))].filter(Boolean).join(` ${sep2} `);
104911
- const head = `${formatAge(meta.updatedAt, now)} ${sep2} ${project} ${sep2} `;
104912
- const fixed = head.length + 2 + ` ${sep2} ${tail}`.length;
105371
+ const tail = [meta.turns !== void 0 ? `${meta.turns} turn${meta.turns === 1 ? "" : "s"}` : null, formatCost(estimateCostUsd(meta.tokens.input, meta.tokens.output))].filter(Boolean).join(` ${sep3} `);
105372
+ const head = `${formatAge(meta.updatedAt, now)} ${sep3} ${project} ${sep3} `;
105373
+ const fixed = head.length + 2 + ` ${sep3} ${tail}`.length;
104913
105374
  const room = Math.max(8, columns - fixed);
104914
105375
  const title = meta.title.length > room ? `${meta.title.slice(0, Math.max(1, room - ellipsis.length))}${ellipsis}` : meta.title;
104915
- return `${head}"${title}" ${sep2} ${tail}`;
105376
+ return `${head}"${title}" ${sep3} ${tail}`;
104916
105377
  }
104917
105378
  var init_resume_rows = __esm({
104918
105379
  "src/render/resume-rows.ts"() {
@@ -105359,7 +105820,7 @@ function clearOnResize(stdout) {
105359
105820
  var import_jsx_runtime14 = __toESM(require_jsx_runtime(), 1);
105360
105821
  if (process.argv[2] === "import-claude") {
105361
105822
  const { runImportClaude: runImportClaude2 } = await Promise.resolve().then(() => (init_import_claude(), import_claude_exports));
105362
- const { readFile: readFile7, readdir: readdir4 } = await import("node:fs/promises");
105823
+ const { readFile: readFile9, readdir: readdir5 } = await import("node:fs/promises");
105363
105824
  const { execFile: execFile4 } = await import("node:child_process");
105364
105825
  const gitRoot = () => new Promise((resolve2) => {
105365
105826
  execFile4("git", ["rev-parse", "--show-toplevel"], { cwd: process.cwd() }, (err, stdout) => resolve2(err ? null : stdout.trim() || null));
@@ -105367,8 +105828,8 @@ if (process.argv[2] === "import-claude") {
105367
105828
  const code = await runImportClaude2({
105368
105829
  argv: process.argv.slice(2),
105369
105830
  env: process.env,
105370
- fs: { readFile: (p) => readFile7(p, "utf-8"), readdir: (p) => readdir4(p) },
105371
- readFile: (p) => readFile7(p, "utf-8"),
105831
+ fs: { readFile: (p) => readFile9(p, "utf-8"), readdir: (p) => readdir5(p) },
105832
+ readFile: (p) => readFile9(p, "utf-8"),
105372
105833
  homedir: os5.homedir,
105373
105834
  cwd: process.cwd(),
105374
105835
  gitRoot,
@@ -105393,9 +105854,9 @@ if (process.argv[2] === "approve-mcp") {
105393
105854
  const { resolveApiKey: resolveApiKey2 } = await Promise.resolve().then(() => (init_api_key(), api_key_exports));
105394
105855
  const { backendUrl: backendUrl2 } = await Promise.resolve().then(() => (init_profile_runtime(), profile_runtime_exports));
105395
105856
  const { createProfileClient: createProfileClient2 } = await Promise.resolve().then(() => (init_src(), src_exports));
105396
- const { readFile: readFile7 } = await import("node:fs/promises");
105857
+ const { readFile: readFile9 } = await import("node:fs/promises");
105397
105858
  const configDir = process.env.SPACECODE_CONFIG_DIR?.trim() || path11.join(os5.homedir(), ".spacecode");
105398
- const apiKey = await resolveApiKey2({ env: process.env, readFile: (p) => readFile7(p, "utf-8"), homedir: os5.homedir }).catch(() => null);
105859
+ const apiKey = await resolveApiKey2({ env: process.env, readFile: (p) => readFile9(p, "utf-8"), homedir: os5.homedir }).catch(() => null);
105399
105860
  const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
105400
105861
  const { createInterface } = interactive ? await import("node:readline/promises") : { createInterface: void 0 };
105401
105862
  const rl = createInterface ? createInterface({ input: process.stdin, output: process.stdout }) : void 0;
@@ -105403,7 +105864,7 @@ if (process.argv[2] === "approve-mcp") {
105403
105864
  const code = await runApproveMcp2({
105404
105865
  configDir,
105405
105866
  cwd: process.cwd(),
105406
- readFile: (p) => readFile7(p, "utf-8"),
105867
+ readFile: (p) => readFile9(p, "utf-8"),
105407
105868
  client: apiKey ? createProfileClient2({ baseUrl: backendUrl2(process.env), apiKey, fetch }) : null,
105408
105869
  trust: createNodeTrustStore2(configDir),
105409
105870
  ...rl ? { ask: async (q2) => {