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/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/repl.mjs +511 -58
- package/package.json +1 -1
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(
|
|
5708
|
+
async function computeLockfileHash(readFile9) {
|
|
5709
5709
|
for (const name of LOCKFILES) {
|
|
5710
|
-
const content = await
|
|
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:
|
|
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
|
|
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:
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 },
|
|
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(` ${
|
|
40386
|
-
return kept.join(` ${
|
|
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 },
|
|
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: `${
|
|
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 ?
|
|
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:
|
|
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 ? ` ${
|
|
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
|
|
40484
|
-
const segments2 = formatHeader(props,
|
|
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: ` ${
|
|
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
|
|
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(
|
|
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,
|
|
102811
|
-
return `${mark} Thinking${dots} (${Math.floor(elapsedMs / 1e3)}s ${
|
|
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
|
|
104014
|
-
import { join as
|
|
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
|
|
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 =
|
|
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:
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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() ||
|
|
104664
|
+
configDir: process.env.SPACECODE_CONFIG_DIR?.trim() || join11(homedir2(), ".spacecode"),
|
|
104221
104665
|
cwd: process.cwd(),
|
|
104222
104666
|
env: process.env,
|
|
104223
|
-
readFile: (p) =>
|
|
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 "
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
104620
|
-
writeFile: (p, d) =>
|
|
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
|
|
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() ||
|
|
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() ||
|
|
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(
|
|
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,
|
|
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(` ${
|
|
104919
|
-
const head = `${formatAge(meta.updatedAt, now)} ${
|
|
104920
|
-
const fixed = head.length + 2 + ` ${
|
|
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}" ${
|
|
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:
|
|
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) =>
|
|
105379
|
-
readFile: (p) =>
|
|
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:
|
|
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) =>
|
|
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) =>
|
|
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) => {
|