spacecode 2.6.2 → 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 */
@@ -102807,8 +102807,8 @@ function formatTurnSummary({ toolCount, elapsedMs, inputTokens, outputTokens, in
102807
102807
  return `${head} ${bar} ${formatTokens(inputTokens)} ${arrow} ${formatTokens(outputTokens)} ${bar} ${formatCost(estimateCostUsd(inputTokens, outputTokens))}`;
102808
102808
  }
102809
102809
  function formatThinking(elapsedMs, approxTokens, unicode = true) {
102810
- const [mark, dots, sep2] = unicode ? ["\u273B", "\u2026", "\xB7"] : ["*", "...", "-"];
102811
- 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)`;
102812
102812
  }
102813
102813
  var ESTIMATED_USD_PER_MTOK, MODEL_PLACEHOLDER, CONTEXT_WARN_PCT, CONTEXT_CRITICAL_PCT, BAR_CELLS, BAR_MIN_COLUMNS;
102814
102814
  var init_footer = __esm({
@@ -103121,6 +103121,7 @@ var init_slash_commands = __esm({
103121
103121
  { name: "cost", description: "Show session token usage and approximate cost" },
103122
103122
  { name: "context", description: "Show how much of the context window is used" },
103123
103123
  { name: "status", description: "Show project, session, mode, context and cost" },
103124
+ { name: "record", description: "Session recording: /record off|on|status" },
103124
103125
  { name: "compact", description: "Summarize the conversation to free up context" },
103125
103126
  { name: "model", description: "List gateway models; /model <n|id> switches for this session" },
103126
103127
  { name: "turns", description: "Browse earlier turns and open their steps (shortcut: ctrl+o)" },
@@ -103970,6 +103971,439 @@ var init_node_mentions = __esm({
103970
103971
  }
103971
103972
  });
103972
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
+
103973
104407
  // src/skills-command.ts
103974
104408
  async function runSkillsCommand(args, deps) {
103975
104409
  const [sub, name, file] = args.trim().split(/\s+/);
@@ -104010,11 +104444,11 @@ var init_skills_command = __esm({
104010
104444
  });
104011
104445
 
104012
104446
  // src/permission-rules-loader.ts
104013
- import { readFile as readFile5 } from "node:fs/promises";
104014
- import { join as join7 } from "node:path";
104447
+ import { readFile as readFile7 } from "node:fs/promises";
104448
+ import { join as join9 } from "node:path";
104015
104449
  async function readOrNull2(path12) {
104016
104450
  try {
104017
- return await readFile5(path12, "utf8");
104451
+ return await readFile7(path12, "utf8");
104018
104452
  } catch (err) {
104019
104453
  if (err.code === "ENOENT") return null;
104020
104454
  throw err;
@@ -104022,7 +104456,7 @@ async function readOrNull2(path12) {
104022
104456
  }
104023
104457
  async function loadPermissionRules(deps) {
104024
104458
  const none = (line) => ({ rules: void 0, defaultMode: void 0, statusLines: [line] });
104025
- const configPath = join7(deps.configDir, "imports.json");
104459
+ const configPath = join9(deps.configDir, "imports.json");
104026
104460
  try {
104027
104461
  const loaded = parseImportsConfig(await readOrNull2(configPath), configPath);
104028
104462
  if (loaded.error) return none("permission rules: off (imports.json unreadable)");
@@ -104031,13 +104465,13 @@ async function loadPermissionRules(deps) {
104031
104465
  return none(`permission rules: off (imports.json not read: ${err instanceof Error ? err.message : String(err)})`);
104032
104466
  }
104033
104467
  const lines2 = [];
104034
- const files = [{ scope: "user", path: join7(deps.homeDir, ".claude", "settings.json") }];
104468
+ const files = [{ scope: "user", path: join9(deps.homeDir, ".claude", "settings.json") }];
104035
104469
  try {
104036
104470
  const sources2 = await readRepoSources(deps.cwd);
104037
104471
  if (sources2[".claude/settings.json"] !== void 0 || sources2[".claude/settings.local.json"] !== void 0) {
104038
104472
  const status = await createNodeTrustStore(deps.configDir).repoStatus(deps.cwd, sources2);
104039
104473
  if (status === "trusted" || status === "session") {
104040
- 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") });
104041
104475
  } else lines2.push(`permission rules: repo settings NOT loaded (${status === "never" ? "marked never trust" : "untrusted repo"})`);
104042
104476
  }
104043
104477
  } catch (err) {
@@ -104079,7 +104513,7 @@ var init_permission_rules_loader = __esm({
104079
104513
  });
104080
104514
 
104081
104515
  // src/command-loader.ts
104082
- import { join as join8 } from "node:path";
104516
+ import { join as join10 } from "node:path";
104083
104517
  async function userCommands(deps, lines2) {
104084
104518
  try {
104085
104519
  const apiKey = await resolveApiKey({ env: deps.env, readFile: deps.readFile, homedir: deps.homedir });
@@ -104134,7 +104568,7 @@ async function repoCommands(deps, lines2) {
104134
104568
  }
104135
104569
  }
104136
104570
  async function loadCommands(deps) {
104137
- const configPath = join8(deps.configDir, "imports.json");
104571
+ const configPath = join10(deps.configDir, "imports.json");
104138
104572
  try {
104139
104573
  const loaded = parseImportsConfig(await deps.readFile(configPath).catch((err) => {
104140
104574
  if (err.code === "ENOENT") return null;
@@ -104167,9 +104601,9 @@ var init_command_loader = __esm({
104167
104601
  });
104168
104602
 
104169
104603
  // src/useReplController.ts
104170
- 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";
104171
104605
  import { homedir as homedir2 } from "node:os";
104172
- import { join as join9 } from "node:path";
104606
+ import { join as join11 } from "node:path";
104173
104607
  import { execFile as execFile3 } from "node:child_process";
104174
104608
  function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mentionsOpt, sessions, resumeLatest: resumeLatest2, onViewTurns, onViewResume, isEscOwned }) {
104175
104609
  const { exit } = use_app_default();
@@ -104197,6 +104631,16 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
104197
104631
  const profilePromiseRef = (0, import_react30.useRef)(null);
104198
104632
  const profileRef = (0, import_react30.useRef)(null);
104199
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
+ });
104200
104644
  const resumeListRef = (0, import_react30.useRef)([]);
104201
104645
  const [model, setModel] = (0, import_react30.useState)(null);
104202
104646
  const [todos, setTodosState] = (0, import_react30.useState)([]);
@@ -104217,10 +104661,10 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
104217
104661
  (0, import_react30.useEffect)(() => {
104218
104662
  let live2 = true;
104219
104663
  loadCommands({
104220
- configDir: process.env.SPACECODE_CONFIG_DIR?.trim() || join9(homedir2(), ".spacecode"),
104664
+ configDir: process.env.SPACECODE_CONFIG_DIR?.trim() || join11(homedir2(), ".spacecode"),
104221
104665
  cwd: process.cwd(),
104222
104666
  env: process.env,
104223
- readFile: (p) => readFile6(p, "utf-8"),
104667
+ readFile: (p) => readFile8(p, "utf-8"),
104224
104668
  homedir: homedir2,
104225
104669
  fetch,
104226
104670
  builtins: SLASH_COMMANDS.map((c) => c.name)
@@ -104320,6 +104764,9 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
104320
104764
  const id = sessionIdRef.current ??= sessions.newId();
104321
104765
  const createdAt = sessionCreatedAtRef.current ??= (/* @__PURE__ */ new Date()).toISOString();
104322
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
+ });
104323
104770
  } catch (err) {
104324
104771
  noticeError(describeError("session-save", err instanceof Error ? err.message : String(err)));
104325
104772
  }
@@ -104434,7 +104881,11 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
104434
104881
  case "context":
104435
104882
  notice(formatContextReport({ contextTokens: estimateTotalTokens("", history), maxContextTokens: DEFAULT_MAX_CONTEXT_TOKENS }));
104436
104883
  return;
104437
- case "status":
104884
+ case "record":
104885
+ notice(await recording().command(slash.args));
104886
+ return;
104887
+ case "status": {
104888
+ const recordingLine = await recording().statusLine();
104438
104889
  notice(formatStatusReport({
104439
104890
  mode: permissions.mode,
104440
104891
  sessionId: sessionIdRef.current,
@@ -104443,9 +104894,10 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
104443
104894
  maxContextTokens: DEFAULT_MAX_CONTEXT_TOKENS,
104444
104895
  tokens: sessionTokensRef.current,
104445
104896
  costUsd: estimateCostUsd(sessionTokensRef.current.input, sessionTokensRef.current.output),
104446
- 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]
104447
104898
  }));
104448
104899
  return;
104900
+ }
104449
104901
  case "resume": {
104450
104902
  if (!sessions) {
104451
104903
  notice("Session storage is not available, so there is nothing to resume.");
@@ -104500,7 +104952,7 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
104500
104952
  }
104501
104953
  setIsRunning(true);
104502
104954
  try {
104503
- 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 });
104504
104956
  if (!apiKey2) {
104505
104957
  notice(MISSING_KEY_MESSAGE);
104506
104958
  return;
@@ -104522,7 +104974,7 @@ function useReplController({ repoName: repoName2, yolo: yolo2, mentions: mention
104522
104974
  }
104523
104975
  case "model": {
104524
104976
  try {
104525
- 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 });
104526
104978
  if (!apiKey2) {
104527
104979
  notice(MISSING_KEY_MESSAGE);
104528
104980
  return;
@@ -104557,7 +105009,7 @@ ${list2.map((c) => ` /${c.name} (${c.scope})${c.description ? ` ${c.descripti
104557
105009
  }
104558
105010
  case "skills": {
104559
105011
  const state = profileRef.current;
104560
- 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);
104561
105013
  const client = apiKey2 ? createProfileClient({ baseUrl: backendUrl(process.env), apiKey: apiKey2, fetch }) : null;
104562
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)"] }));
104563
105015
  return;
@@ -104579,7 +105031,7 @@ ${list2.map((c) => ` /${c.name} (${c.scope})${c.description ? ` ${c.descripti
104579
105031
  try {
104580
105032
  apiKey = await resolveApiKey({
104581
105033
  env: process.env,
104582
- readFile: (p) => readFile6(p, "utf-8"),
105034
+ readFile: (p) => readFile8(p, "utf-8"),
104583
105035
  homedir: homedir2
104584
105036
  });
104585
105037
  } catch (err) {
@@ -104616,17 +105068,17 @@ ${list2.map((c) => ` /${c.name} (${c.scope})${c.description ? ` ${c.descripti
104616
105068
  try {
104617
105069
  profilePromiseRef.current ??= prepareProfile({
104618
105070
  env: process.env,
104619
- readFile: (p) => readFile6(p, "utf-8"),
104620
- 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 }),
104621
105073
  mkdir: async (p) => {
104622
- await mkdir4(p, { recursive: true });
105074
+ await mkdir6(p, { recursive: true });
104623
105075
  },
104624
105076
  homedir: homedir2,
104625
105077
  fetch,
104626
105078
  cwd: process.cwd(),
104627
105079
  gitRoot: gitTopLevel,
104628
105080
  importGate: async () => {
104629
- const configDir = process.env.SPACECODE_CONFIG_DIR?.trim() || join9(homedir2(), ".spacecode");
105081
+ const configDir = process.env.SPACECODE_CONFIG_DIR?.trim() || join11(homedir2(), ".spacecode");
104630
105082
  const gate = await importGateStatus({ configDir, cwd: process.cwd(), interactive: Boolean(process.stdin.isTTY) });
104631
105083
  const loaded = await loadPermissionRules({ configDir, homeDir: homedir2(), cwd: process.cwd() });
104632
105084
  rulesRef.current = loaded.rules;
@@ -104636,7 +105088,7 @@ ${list2.map((c) => ` /${c.name} (${c.scope})${c.description ? ` ${c.descripti
104636
105088
  });
104637
105089
  profileRef.current = await profilePromiseRef.current;
104638
105090
  if (mcpRef.current === null && (profileRef.current?.mcpDefs?.length ?? 0) > 0) {
104639
- const configDir = process.env.SPACECODE_CONFIG_DIR?.trim() || join9(homedir2(), ".spacecode");
105091
+ const configDir = process.env.SPACECODE_CONFIG_DIR?.trim() || join11(homedir2(), ".spacecode");
104640
105092
  const trust = createNodeTrustStore(configDir);
104641
105093
  const manager = new McpManager({
104642
105094
  defs: profileRef.current.mcpDefs,
@@ -104646,7 +105098,7 @@ ${list2.map((c) => ` /${c.name} (${c.scope})${c.description ? ` ${c.descripti
104646
105098
  ...rulesRef.current ? { rules: rulesRef.current } : {},
104647
105099
  permissions,
104648
105100
  confirm: askUser,
104649
- audit: new FileAuditLog(join9(homedir2(), ".spacecode", "audit.jsonl")),
105101
+ audit: new FileAuditLog(join11(homedir2(), ".spacecode", "audit.jsonl")),
104650
105102
  clientVersion: process.env.SPACECODE_VERSION ?? "0"
104651
105103
  });
104652
105104
  mcpRef.current = manager;
@@ -104777,6 +105229,7 @@ var init_useReplController = __esm({
104777
105229
  init_node_mentions();
104778
105230
  init_api_key();
104779
105231
  init_profile_runtime();
105232
+ init_recording_runtime();
104780
105233
  init_skills_command();
104781
105234
  init_src();
104782
105235
  init_mcp_manager();
@@ -104913,14 +105366,14 @@ import path9 from "node:path";
104913
105366
  function orderSessions(sessions, projectRoot) {
104914
105367
  return [...sessions.filter((s) => s.projectRoot === projectRoot), ...sessions.filter((s) => s.projectRoot !== projectRoot)];
104915
105368
  }
104916
- function formatResumeRow(meta, now, columns, sep2 = "\xB7", ellipsis = "\u2026") {
105369
+ function formatResumeRow(meta, now, columns, sep3 = "\xB7", ellipsis = "\u2026") {
104917
105370
  const project = path9.basename(meta.projectRoot) || meta.projectRoot;
104918
- 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} `);
104919
- const head = `${formatAge(meta.updatedAt, now)} ${sep2} ${project} ${sep2} `;
104920
- 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;
104921
105374
  const room = Math.max(8, columns - fixed);
104922
105375
  const title = meta.title.length > room ? `${meta.title.slice(0, Math.max(1, room - ellipsis.length))}${ellipsis}` : meta.title;
104923
- return `${head}"${title}" ${sep2} ${tail}`;
105376
+ return `${head}"${title}" ${sep3} ${tail}`;
104924
105377
  }
104925
105378
  var init_resume_rows = __esm({
104926
105379
  "src/render/resume-rows.ts"() {
@@ -105367,7 +105820,7 @@ function clearOnResize(stdout) {
105367
105820
  var import_jsx_runtime14 = __toESM(require_jsx_runtime(), 1);
105368
105821
  if (process.argv[2] === "import-claude") {
105369
105822
  const { runImportClaude: runImportClaude2 } = await Promise.resolve().then(() => (init_import_claude(), import_claude_exports));
105370
- const { readFile: readFile7, readdir: readdir4 } = await import("node:fs/promises");
105823
+ const { readFile: readFile9, readdir: readdir5 } = await import("node:fs/promises");
105371
105824
  const { execFile: execFile4 } = await import("node:child_process");
105372
105825
  const gitRoot = () => new Promise((resolve2) => {
105373
105826
  execFile4("git", ["rev-parse", "--show-toplevel"], { cwd: process.cwd() }, (err, stdout) => resolve2(err ? null : stdout.trim() || null));
@@ -105375,8 +105828,8 @@ if (process.argv[2] === "import-claude") {
105375
105828
  const code = await runImportClaude2({
105376
105829
  argv: process.argv.slice(2),
105377
105830
  env: process.env,
105378
- fs: { readFile: (p) => readFile7(p, "utf-8"), readdir: (p) => readdir4(p) },
105379
- 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"),
105380
105833
  homedir: os5.homedir,
105381
105834
  cwd: process.cwd(),
105382
105835
  gitRoot,
@@ -105401,9 +105854,9 @@ if (process.argv[2] === "approve-mcp") {
105401
105854
  const { resolveApiKey: resolveApiKey2 } = await Promise.resolve().then(() => (init_api_key(), api_key_exports));
105402
105855
  const { backendUrl: backendUrl2 } = await Promise.resolve().then(() => (init_profile_runtime(), profile_runtime_exports));
105403
105856
  const { createProfileClient: createProfileClient2 } = await Promise.resolve().then(() => (init_src(), src_exports));
105404
- const { readFile: readFile7 } = await import("node:fs/promises");
105857
+ const { readFile: readFile9 } = await import("node:fs/promises");
105405
105858
  const configDir = process.env.SPACECODE_CONFIG_DIR?.trim() || path11.join(os5.homedir(), ".spacecode");
105406
- 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);
105407
105860
  const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
105408
105861
  const { createInterface } = interactive ? await import("node:readline/promises") : { createInterface: void 0 };
105409
105862
  const rl = createInterface ? createInterface({ input: process.stdin, output: process.stdout }) : void 0;
@@ -105411,7 +105864,7 @@ if (process.argv[2] === "approve-mcp") {
105411
105864
  const code = await runApproveMcp2({
105412
105865
  configDir,
105413
105866
  cwd: process.cwd(),
105414
- readFile: (p) => readFile7(p, "utf-8"),
105867
+ readFile: (p) => readFile9(p, "utf-8"),
105415
105868
  client: apiKey ? createProfileClient2({ baseUrl: backendUrl2(process.env), apiKey, fetch }) : null,
105416
105869
  trust: createNodeTrustStore2(configDir),
105417
105870
  ...rl ? { ask: async (q2) => {