@cjhyy/code-shell-server 0.8.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.
Files changed (70) hide show
  1. package/README.md +77 -0
  2. package/dist/attachment-service.d.ts +80 -0
  3. package/dist/attachment-service.js +587 -0
  4. package/dist/bin/code-shell-serve.d.ts +2 -0
  5. package/dist/bin/code-shell-serve.js +7 -0
  6. package/dist/client-message-id.d.ts +1 -0
  7. package/dist/client-message-id.js +8 -0
  8. package/dist/image-byte-probe.d.ts +17 -0
  9. package/dist/image-byte-probe.js +330 -0
  10. package/dist/index.d.ts +10 -0
  11. package/dist/index.js +10 -0
  12. package/dist/index.mobile-remote.d.ts +21 -0
  13. package/dist/index.mobile-remote.js +21 -0
  14. package/dist/index.serve.d.ts +3 -0
  15. package/dist/index.serve.js +3 -0
  16. package/dist/index.storage.d.ts +10 -0
  17. package/dist/index.storage.js +10 -0
  18. package/dist/index.worker.d.ts +2 -0
  19. package/dist/index.worker.js +2 -0
  20. package/dist/mobile-remote/access-passcode.d.ts +80 -0
  21. package/dist/mobile-remote/access-passcode.js +354 -0
  22. package/dist/mobile-remote/cloudflared-binary.d.ts +76 -0
  23. package/dist/mobile-remote/cloudflared-binary.js +242 -0
  24. package/dist/mobile-remote/codex-parse.d.ts +34 -0
  25. package/dist/mobile-remote/codex-parse.js +131 -0
  26. package/dist/mobile-remote/codex-room-agent.d.ts +94 -0
  27. package/dist/mobile-remote/codex-room-agent.js +213 -0
  28. package/dist/mobile-remote/mobile-attachments.d.ts +21 -0
  29. package/dist/mobile-remote/mobile-attachments.js +119 -0
  30. package/dist/mobile-remote/mobile-chat-turn.d.ts +32 -0
  31. package/dist/mobile-remote/mobile-chat-turn.js +67 -0
  32. package/dist/mobile-remote/mobile-history.d.ts +22 -0
  33. package/dist/mobile-remote/mobile-history.js +27 -0
  34. package/dist/mobile-remote/mobile-run-dispatch.d.ts +17 -0
  35. package/dist/mobile-remote/mobile-run-dispatch.js +56 -0
  36. package/dist/mobile-remote/mobile-static.d.ts +43 -0
  37. package/dist/mobile-remote/mobile-static.js +154 -0
  38. package/dist/mobile-remote/mobile-upload-service.d.ts +57 -0
  39. package/dist/mobile-remote/mobile-upload-service.js +370 -0
  40. package/dist/mobile-remote/pairing.d.ts +8 -0
  41. package/dist/mobile-remote/pairing.js +23 -0
  42. package/dist/mobile-remote/path-bins.d.ts +7 -0
  43. package/dist/mobile-remote/path-bins.js +16 -0
  44. package/dist/mobile-remote/pending-approvals.d.ts +14 -0
  45. package/dist/mobile-remote/pending-approvals.js +54 -0
  46. package/dist/mobile-remote/remote-host-manager.d.ts +117 -0
  47. package/dist/mobile-remote/remote-host-manager.js +345 -0
  48. package/dist/mobile-remote/resident-agent.d.ts +98 -0
  49. package/dist/mobile-remote/resident-agent.js +216 -0
  50. package/dist/mobile-remote/room-manager.d.ts +286 -0
  51. package/dist/mobile-remote/room-manager.js +765 -0
  52. package/dist/mobile-remote/trusted-device-store.d.ts +19 -0
  53. package/dist/mobile-remote/trusted-device-store.js +118 -0
  54. package/dist/mobile-remote/tunnel-manager.d.ts +105 -0
  55. package/dist/mobile-remote/tunnel-manager.js +375 -0
  56. package/dist/mobile-remote/types.d.ts +1 -0
  57. package/dist/mobile-remote/types.js +1 -0
  58. package/dist/mobile-remote/viewer-identity.d.ts +5 -0
  59. package/dist/mobile-remote/viewer-identity.js +5 -0
  60. package/dist/serve/cli.d.ts +15 -0
  61. package/dist/serve/cli.js +115 -0
  62. package/dist/serve/headless-server.d.ts +40 -0
  63. package/dist/serve/headless-server.js +276 -0
  64. package/dist/sessions-service.d.ts +73 -0
  65. package/dist/sessions-service.js +303 -0
  66. package/dist/static-files.d.ts +1 -0
  67. package/dist/static-files.js +25 -0
  68. package/dist/worker-bridge-core.d.ts +154 -0
  69. package/dist/worker-bridge-core.js +315 -0
  70. package/package.json +63 -0
@@ -0,0 +1,115 @@
1
+ // packages/server/src/serve/cli.ts
2
+ //
3
+ // `code-shell-serve` — boot the headless no-account web host from a terminal:
4
+ //
5
+ // code-shell-serve --cwd ~/work/repo [--port 8790] [--host 127.0.0.1]
6
+ // [--passcode <code>] [--data-dir <dir>]
7
+ //
8
+ // Access control is passcode + remember-cookie only (决策见 TODO 约束边界
9
+ // 「服务端部署不做账号体系」). Default bind is loopback; binding 0.0.0.0 is a
10
+ // deliberate operator choice.
11
+ import { createRequire } from "node:module";
12
+ import { homedir } from "node:os";
13
+ import { join, resolve } from "node:path";
14
+ import { existsSync } from "node:fs";
15
+ import { startHeadlessServer } from "./headless-server.js";
16
+ const SERVE_HELP = `Usage: code-shell-serve [options]
17
+
18
+ Run the CodeShell headless Web host for a workspace.
19
+
20
+ Options:
21
+ --cwd <path> Workspace root (default: current directory)
22
+ --host <host> Bind host (default: 127.0.0.1)
23
+ --port <port> Bind port, 0 selects a free port (default: 8790)
24
+ --passcode <code> Set or rotate the access passcode
25
+ --data-dir <path> Access-control data directory
26
+ --static-root <path> Override the built Web app directory
27
+ -h, --help Show this help and exit`;
28
+ const VALUE_FLAGS = new Set([
29
+ "--cwd",
30
+ "--host",
31
+ "--port",
32
+ "--passcode",
33
+ "--data-dir",
34
+ "--static-root",
35
+ ]);
36
+ export function parseServeArgs(argv, env = process.env) {
37
+ const args = {};
38
+ for (let i = 0; i < argv.length; i++) {
39
+ const key = argv[i];
40
+ if (!key || !VALUE_FLAGS.has(key)) {
41
+ throw new Error(`unknown argument: ${key ?? ""}\n\n${SERVE_HELP}`);
42
+ }
43
+ const value = argv[i + 1];
44
+ if (value === undefined || value.startsWith("--")) {
45
+ throw new Error(`missing value for ${key}\n\n${SERVE_HELP}`);
46
+ }
47
+ args[key.slice(2)] = value;
48
+ i++;
49
+ }
50
+ const home = env.CODE_SHELL_HOME || join(homedir(), ".code-shell");
51
+ const port = Number(args.port ?? "8790");
52
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
53
+ throw new Error(`invalid --port: ${args.port}`);
54
+ }
55
+ return {
56
+ cwd: resolve(args.cwd ?? process.cwd()),
57
+ host: args.host ?? "127.0.0.1",
58
+ port,
59
+ ...(args.passcode ? { passcode: args.passcode } : {}),
60
+ dataDir: args["data-dir"] ? resolve(args["data-dir"]) : join(home, "serve"),
61
+ ...(args["static-root"] ? { staticRootDir: resolve(args["static-root"]) } : {}),
62
+ };
63
+ }
64
+ /** Locate the stdio worker entry the same way desktop's AgentBridge does. */
65
+ export function resolveWorkerEntry() {
66
+ const require = createRequire(import.meta.url);
67
+ return require.resolve("@cjhyy/code-shell-capability-coding/bin/agent-server-stdio");
68
+ }
69
+ /** Locate the built browser app (packages/web `app` build), when present. */
70
+ export function resolveWebAppRoot() {
71
+ try {
72
+ const require = createRequire(import.meta.url);
73
+ // package.json is exposed via the web package's exports map exactly so a
74
+ // CJS-style resolve (createRequire) can locate the package root; dist-app
75
+ // is the app build output sibling.
76
+ const pkgJson = require.resolve("@cjhyy/code-shell-web/package.json");
77
+ const candidate = join(pkgJson, "..", "dist-app");
78
+ return existsSync(candidate) ? candidate : undefined;
79
+ }
80
+ catch {
81
+ return undefined;
82
+ }
83
+ }
84
+ export async function runServeCli(argv = process.argv.slice(2)) {
85
+ if (argv.includes("--help") || argv.includes("-h")) {
86
+ console.log(SERVE_HELP);
87
+ return;
88
+ }
89
+ const parsed = parseServeArgs(argv);
90
+ const staticRootDir = parsed.staticRootDir ?? resolveWebAppRoot();
91
+ const server = await startHeadlessServer({
92
+ host: parsed.host,
93
+ port: parsed.port,
94
+ cwd: parsed.cwd,
95
+ dataDir: parsed.dataDir,
96
+ workerEntryPath: resolveWorkerEntry(),
97
+ ...(staticRootDir ? { staticRootDir } : {}),
98
+ ...(parsed.passcode ? { passcode: parsed.passcode } : {}),
99
+ log: (event, data) => console.error(`[serve] ${event}${data ? ` ${JSON.stringify(data)}` : ""}`),
100
+ });
101
+ console.log(`CodeShell web host listening at ${server.url}`);
102
+ console.log(`Workspace: ${parsed.cwd}`);
103
+ if (!staticRootDir) {
104
+ console.log("No web app build found — WS endpoint only (/ws). Build packages/web first.");
105
+ }
106
+ if (server.generatedPasscode) {
107
+ console.log(`Access passcode (generated, save it now): ${server.generatedPasscode}`);
108
+ }
109
+ const shutdown = () => {
110
+ console.log("shutting down…");
111
+ void server.close().then(() => process.exit(0));
112
+ };
113
+ process.on("SIGINT", shutdown);
114
+ process.on("SIGTERM", shutdown);
115
+ }
@@ -0,0 +1,40 @@
1
+ import { AccessPasscode } from "../mobile-remote/access-passcode.js";
2
+ import { WorkerBridgeCore, type WorkerBridgeLog } from "../worker-bridge-core.js";
3
+ export interface HeadlessServeOptions {
4
+ /** Bind host. Default 127.0.0.1 — expose beyond loopback deliberately. */
5
+ host?: string;
6
+ /** Bind port. 0 picks an ephemeral port (tests). Default 8790. */
7
+ port?: number;
8
+ /** Workspace root the agent worker runs in. */
9
+ cwd: string;
10
+ /** Directory for serve state (access.json). */
11
+ dataDir: string;
12
+ /** Absolute path of the agent-server-stdio worker entry. */
13
+ workerEntryPath: string;
14
+ /** Session root override for tests/relocated workers. Defaults to core's canonical root. */
15
+ sessionRootDir?: string;
16
+ /** Runtime binary for the worker; defaults to process.execPath. */
17
+ execPath?: string;
18
+ /** Built web app root; when absent the server is WS/API-only. */
19
+ staticRootDir?: string;
20
+ /**
21
+ * Set (rotate) the access passcode at boot. When omitted and none is
22
+ * configured yet, a random one is generated and returned in
23
+ * `generatedPasscode` — the CLI prints it once.
24
+ */
25
+ passcode?: string;
26
+ log?: WorkerBridgeLog;
27
+ }
28
+ export interface HeadlessServer {
29
+ url: string;
30
+ host: string;
31
+ port: number;
32
+ /** Present only when this boot had to generate a fresh passcode. */
33
+ generatedPasscode?: string;
34
+ passcode: AccessPasscode;
35
+ bridge: WorkerBridgeCore;
36
+ /** Live authenticated tab count (for tests/status). */
37
+ tabCount(): number;
38
+ close(): Promise<void>;
39
+ }
40
+ export declare function startHeadlessServer(opts: HeadlessServeOptions): Promise<HeadlessServer>;
@@ -0,0 +1,276 @@
1
+ // packages/server/src/serve/headless-server.ts
2
+ //
3
+ // Headless serve entry — the no-account web host (TODO「服务端部署 + Web
4
+ // Client(无账号体系)」Phase 1').
5
+ //
6
+ // Composition, all pre-existing parts:
7
+ // - AccessPasscode HTTP gate (challenge page) + WS `allows()`;
8
+ // passcode + remember-cookie is the ONLY access
9
+ // control — no AuthN/AuthZ, no users, by decision.
10
+ // - WorkerBridgeCore spawns/drives ONE agent-server-stdio worker.
11
+ // - resolveSafe path-traversal-safe static file resolution.
12
+ //
13
+ // The browser speaks the CORE protocol (agent/run, agent/approve,
14
+ // agent/streamEvent, …) as a first-party front end: this module is a thin
15
+ // WS ↔ worker-stdio pipe, NOT a re-implementation of the desktop mobile
16
+ // orchestrator. Every authenticated tab sees the identical line stream
17
+ // (same semantics as AgentBridge's renderer fan-out).
18
+ //
19
+ // Restart recovery: sessions persist on disk in the worker's data dir; a
20
+ // server restart spawns a fresh worker on the first inbound frame and the
21
+ // browser re-lists sessions over the same protocol.
22
+ import { createServer } from "node:http";
23
+ import { readFileSync, existsSync } from "node:fs";
24
+ import { extname, join, resolve } from "node:path";
25
+ import { randomBytes } from "node:crypto";
26
+ import { WebSocketServer } from "ws";
27
+ import { SessionManager } from "@cjhyy/code-shell-core";
28
+ import { AccessPasscode } from "../mobile-remote/access-passcode.js";
29
+ import { resolveSafe } from "../mobile-remote/mobile-static.js";
30
+ import { contentTypeFor } from "../static-files.js";
31
+ import { WorkerBridgeCore, previewLine } from "../worker-bridge-core.js";
32
+ export async function startHeadlessServer(opts) {
33
+ const host = opts.host ?? "127.0.0.1";
34
+ const log = opts.log ?? (() => { });
35
+ const workspaceCwd = resolve(opts.cwd);
36
+ const sessionManager = new SessionManager(opts.sessionRootDir);
37
+ const passcode = new AccessPasscode({ filePath: join(opts.dataDir, "access.json") });
38
+ let generatedPasscode;
39
+ if (opts.passcode) {
40
+ passcode.set(opts.passcode);
41
+ }
42
+ else if (!passcode.isSet()) {
43
+ generatedPasscode = randomBytes(6).toString("base64url");
44
+ passcode.set(generatedPasscode);
45
+ }
46
+ const tabs = new Set();
47
+ const broadcast = (line) => {
48
+ for (const tab of tabs) {
49
+ if (tab.readyState === tab.OPEN)
50
+ tab.send(line);
51
+ }
52
+ };
53
+ const bridge = new WorkerBridgeCore({
54
+ entryPath: opts.workerEntryPath,
55
+ execPath: opts.execPath,
56
+ fallbackCwd: () => opts.cwd,
57
+ log,
58
+ onStderr: (text) => log("worker.stderr", { text: previewLine(text) }),
59
+ onExit: (info) => {
60
+ // Synthetic serve-level notification so the UI can show "agent worker
61
+ // stopped" without conflating it with in-protocol agent/status.
62
+ broadcast(JSON.stringify({
63
+ jsonrpc: "2.0",
64
+ method: "serve/workerExit",
65
+ params: { clean: info.clean, gaveUp: info.gaveUp },
66
+ }));
67
+ },
68
+ });
69
+ bridge.subscribeLines(broadcast);
70
+ const serveStatic = (req, res) => {
71
+ const root = opts.staticRootDir;
72
+ if (!root) {
73
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
74
+ res.end("no web app bundled; WS endpoint at /ws");
75
+ return;
76
+ }
77
+ const pathname = decodeSafely(new URL(req.url ?? "/", "http://localhost").pathname);
78
+ if (pathname === null) {
79
+ res.writeHead(400, { "content-type": "text/plain; charset=utf-8" });
80
+ res.end("bad path");
81
+ return;
82
+ }
83
+ let filePath = pathname === "/" ? null : resolveSafe(root, pathname.replace(/^\//, ""));
84
+ if (filePath === null || !existsSync(filePath)) {
85
+ // SPA fallback: any unknown (or traversal-rejected) HTML navigation gets
86
+ // index.html; non-navigation asset misses stay 404.
87
+ const accept = req.headers.accept ?? "";
88
+ const wantsHtml = typeof accept === "string" && accept.includes("text/html");
89
+ if (!wantsHtml && pathname !== "/") {
90
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
91
+ res.end("not found");
92
+ return;
93
+ }
94
+ filePath = join(root, "index.html");
95
+ if (!existsSync(filePath)) {
96
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
97
+ res.end("web app not built");
98
+ return;
99
+ }
100
+ }
101
+ try {
102
+ const body = readFileSync(filePath);
103
+ res.writeHead(200, {
104
+ "content-type": contentTypeFor(extname(filePath)),
105
+ "cache-control": "no-cache",
106
+ });
107
+ res.end(body);
108
+ }
109
+ catch {
110
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
111
+ res.end("not found");
112
+ }
113
+ };
114
+ const server = createServer((req, res) => {
115
+ // The passcode gate fronts EVERY route — same posture as the tunnel mode
116
+ // of the mobile remote host. gate() renders the challenge page for HTML
117
+ // navigations and sets the remember cookie on success.
118
+ if (!passcode.gate(req, res))
119
+ return;
120
+ serveStatic(req, res);
121
+ });
122
+ const wss = new WebSocketServer({ noServer: true });
123
+ server.on("upgrade", (req, socket, head) => {
124
+ const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
125
+ if (pathname !== "/ws" || !passcode.allows(req)) {
126
+ socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
127
+ socket.destroy();
128
+ return;
129
+ }
130
+ wss.handleUpgrade(req, socket, head, (ws) => {
131
+ tabs.add(ws);
132
+ log("tab.connected", { tabs: tabs.size });
133
+ ws.on("message", (data) => {
134
+ const line = String(data);
135
+ // Validate framing before touching the worker: a malformed frame from
136
+ // one tab must never kill the shared pipe.
137
+ let parsed;
138
+ try {
139
+ parsed = JSON.parse(line);
140
+ }
141
+ catch {
142
+ log("tab.frame_dropped", { reason: "not json", raw: previewLine(line) });
143
+ return;
144
+ }
145
+ if (!parsed || parsed.jsonrpc !== "2.0") {
146
+ log("tab.frame_dropped", { reason: "not jsonrpc", raw: previewLine(line) });
147
+ return;
148
+ }
149
+ const hostReply = replyToHostSessionQuery(parsed, sessionManager, workspaceCwd);
150
+ if (hostReply) {
151
+ ws.send(hostReply);
152
+ return;
153
+ }
154
+ // This host intentionally exposes one workspace. A browser must not be
155
+ // able to escape it by supplying another cwd, and a missing cwd must
156
+ // not silently become the worker's global no-repo conversation.
157
+ const workerLine = parsed.method === "agent/run"
158
+ ? JSON.stringify({
159
+ ...parsed,
160
+ params: { ...(parsed.params ?? {}), cwd: workspaceCwd },
161
+ })
162
+ : line;
163
+ // Spawn-on-first-frame (idempotent): the browser's first request wakes
164
+ // the worker, mirroring the renderer's spawn-on-agent/run semantics.
165
+ bridge.ensureWorker(opts.cwd);
166
+ bridge.injectWorkerMessage(workerLine);
167
+ });
168
+ ws.on("close", () => {
169
+ tabs.delete(ws);
170
+ log("tab.closed", { tabs: tabs.size });
171
+ });
172
+ ws.on("error", () => {
173
+ tabs.delete(ws);
174
+ });
175
+ });
176
+ });
177
+ await new Promise((resolve, reject) => {
178
+ server.once("error", reject);
179
+ server.listen(opts.port ?? 8790, host, () => {
180
+ server.off("error", reject);
181
+ resolve();
182
+ });
183
+ });
184
+ const address = server.address();
185
+ const port = typeof address === "object" && address ? address.port : (opts.port ?? 8790);
186
+ const url = `http://${host}:${port}`;
187
+ log("serve.listening", { url });
188
+ return {
189
+ url,
190
+ host,
191
+ port,
192
+ ...(generatedPasscode ? { generatedPasscode } : {}),
193
+ passcode,
194
+ bridge,
195
+ tabCount: () => tabs.size,
196
+ close: async () => {
197
+ for (const tab of tabs) {
198
+ try {
199
+ tab.terminate();
200
+ }
201
+ catch {
202
+ /* ignore */
203
+ }
204
+ }
205
+ tabs.clear();
206
+ bridge.kill();
207
+ await new Promise((resolve) => {
208
+ wss.close(() => {
209
+ server.close(() => resolve());
210
+ });
211
+ // server.close() alone waits out keep-alive HTTP sockets forever;
212
+ // drop them so shutdown is prompt.
213
+ server.closeAllConnections?.();
214
+ });
215
+ },
216
+ };
217
+ }
218
+ function replyToHostSessionQuery(message, sessionManager, workspaceCwd) {
219
+ if (message.method !== "agent/query" || message.id === undefined)
220
+ return undefined;
221
+ const queryType = message.params?.type;
222
+ if (queryType === "sessions") {
223
+ const sessions = sessionManager
224
+ .list()
225
+ .filter((session) => resolve(session.cwd) === workspaceCwd)
226
+ .map((session) => ({
227
+ sessionId: session.sessionId,
228
+ cwd: session.cwd,
229
+ startedAt: session.startedAt,
230
+ model: session.model,
231
+ status: session.status,
232
+ turnCount: session.turnCount,
233
+ ...(session.preview ? { preview: session.preview } : {}),
234
+ }));
235
+ return JSON.stringify({
236
+ jsonrpc: "2.0",
237
+ id: message.id,
238
+ result: { type: "sessions", data: sessions },
239
+ });
240
+ }
241
+ if (queryType !== "session_detail")
242
+ return undefined;
243
+ const sessionId = message.params?.sessionId;
244
+ if (typeof sessionId !== "string" || sessionId.length === 0) {
245
+ return hostQueryError(message.id, -32602, "sessionId required for session_detail");
246
+ }
247
+ try {
248
+ const bundle = sessionManager.resume(sessionId);
249
+ if (resolve(bundle.state.cwd) !== workspaceCwd) {
250
+ return hostQueryError(message.id, -32001, "Session not found in this workspace");
251
+ }
252
+ return JSON.stringify({
253
+ jsonrpc: "2.0",
254
+ id: message.id,
255
+ result: {
256
+ type: "session_detail",
257
+ data: { state: bundle.state, transcript: bundle.transcript.getEvents() },
258
+ },
259
+ });
260
+ }
261
+ catch (error) {
262
+ return hostQueryError(message.id, -32001, error instanceof Error ? error.message : "Session not found");
263
+ }
264
+ }
265
+ function hostQueryError(id, code, message) {
266
+ return JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } });
267
+ }
268
+ /** decodeURIComponent that returns null instead of throwing on bad input. */
269
+ function decodeSafely(value) {
270
+ try {
271
+ return decodeURIComponent(value);
272
+ }
273
+ catch {
274
+ return null;
275
+ }
276
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * List session files persisted by the agent worker.
3
+ * Uses core's canonical session root so CODE_SHELL_HOME matches the worker.
4
+ */
5
+ import { type TurnCompletionKind } from "@cjhyy/code-shell-core";
6
+ export interface DesktopSessionSummary {
7
+ id: string;
8
+ /** Path to the session file on disk. */
9
+ file: string;
10
+ size: number;
11
+ createdAt: number;
12
+ updatedAt: number;
13
+ }
14
+ export declare function listSessions(baseDir?: string): Promise<DesktopSessionSummary[]>;
15
+ /**
16
+ * Remove a session's on-disk data. Modern sessions are a directory
17
+ * `<sessionsDir>/<id>/` (transcript.jsonl + state.json — see core
18
+ * session-manager.ts:93,141). Pre-directory sessions were flat
19
+ * `<id>.jsonl`/`<id>.json` files; we remove those too for back-compat.
20
+ * `baseDir` is overridable for tests. Unsafe ids (slashes / "." / "..")
21
+ * are rejected without any filesystem access.
22
+ */
23
+ export declare function deleteSessionDir(id: string, baseDir?: string): Promise<void>;
24
+ /** @deprecated retained for the existing IPC handler; delegates to deleteSessionDir. */
25
+ export declare function deleteSession(id: string): Promise<void>;
26
+ /**
27
+ * Reap quick-chat data left by an unclean prior desktop exit. This must run
28
+ * before a new renderer/worker can claim a qchat id; normal live cleanup is
29
+ * coordinated by QuickChatOwnershipRegistry instead.
30
+ */
31
+ export declare function cleanupStaleQuickChatSessions(baseDir?: string): Promise<string[]>;
32
+ export interface DiskSessionMeta {
33
+ id: string;
34
+ engineSessionId: string;
35
+ cwd: string;
36
+ title: string;
37
+ updatedAt: number;
38
+ /** Session origin — only "desktop" / "automation" are listed (see filter). */
39
+ origin: "desktop" | "automation";
40
+ /** Normalized durable status for Pet/display consumers. */
41
+ status?: "active" | "paused" | "completed" | "failed" | "cancelled";
42
+ /** Exceptional completed-run boundary; absent means a normal final answer. */
43
+ completionKind?: TurnCompletionKind;
44
+ /** Durable archival timestamp; absent = not archived. */
45
+ archivedAt?: number;
46
+ /** Workspace profile/digital human bound to this Session. */
47
+ workspaceProfile?: string;
48
+ }
49
+ export interface ListDiskSessionsResult {
50
+ sessions: DiskSessionMeta[];
51
+ nextCursor: string | null;
52
+ }
53
+ /**
54
+ * List top-level (non-sub-agent) sessions from disk, newest first, paginated.
55
+ * Filter on state.json `parentSessionId`:
56
+ * - key absent → legacy → skip (存量 not auto-rebuilt)
57
+ * - null / "" → top-level → show
58
+ * - non-empty → sub-agent → filter out
59
+ */
60
+ export declare function listDiskSessions(opts: {
61
+ limit: number;
62
+ cursor?: string;
63
+ includeArchived?: boolean;
64
+ }, baseDir?: string): Promise<ListDiskSessionsResult>;
65
+ /**
66
+ * Set (number) or clear (undefined) a session's durable archival marker on
67
+ * disk, then atomically replace `state.json`. This is the main-process writer
68
+ * counterpart to `listDiskSessions`' `archivedAt` filter: main has no live
69
+ * Engine/SessionManager handle, so it mutates the same `state.json` the worker
70
+ * persists. The rest of the state is preserved verbatim; only `archivedAt` is
71
+ * added or removed. Unsafe ids and missing sessions throw.
72
+ */
73
+ export declare function archiveDiskSession(id: string, archivedAt: number | undefined, baseDir?: string): Promise<void>;