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