pelican-cc 0.0.0-stage → 0.2.2
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/CHANGELOG.md +61 -0
- package/LICENSE +21 -0
- package/README.md +80 -2
- package/dist/server/index.js +4226 -0
- package/dist/server/index.js.map +7 -0
- package/dist/web/apple-touch-icon.png +0 -0
- package/dist/web/assets/index-DI8qSaKj.js +81 -0
- package/dist/web/assets/index-c7q8fFDv.css +2 -0
- package/dist/web/badge-96.png +0 -0
- package/dist/web/icon-192.png +0 -0
- package/dist/web/icon-512.png +0 -0
- package/dist/web/icon-maskable-512.png +0 -0
- package/dist/web/icon.svg +1 -0
- package/dist/web/index.html +22 -0
- package/dist/web/manifest.webmanifest +16 -0
- package/dist/web/sw.js +40 -0
- package/package.json +77 -4
|
@@ -0,0 +1,4226 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// package.json
|
|
13
|
+
var package_default;
|
|
14
|
+
var init_package = __esm({
|
|
15
|
+
"package.json"() {
|
|
16
|
+
package_default = {
|
|
17
|
+
name: "pelican-cc",
|
|
18
|
+
version: "0.2.2",
|
|
19
|
+
description: "Your Claude Code sessions, from any browser: a phone-first web app that runs next to Claude Code.",
|
|
20
|
+
keywords: [
|
|
21
|
+
"claude",
|
|
22
|
+
"claude-code",
|
|
23
|
+
"agent",
|
|
24
|
+
"mobile",
|
|
25
|
+
"web-ui",
|
|
26
|
+
"ai-coding"
|
|
27
|
+
],
|
|
28
|
+
homepage: "https://github.com/Ki-Seki/pelican#readme",
|
|
29
|
+
bugs: "https://github.com/Ki-Seki/pelican/issues",
|
|
30
|
+
repository: {
|
|
31
|
+
type: "git",
|
|
32
|
+
url: "git+https://github.com/Ki-Seki/pelican.git"
|
|
33
|
+
},
|
|
34
|
+
license: "MIT",
|
|
35
|
+
author: "Shichao Song",
|
|
36
|
+
type: "module",
|
|
37
|
+
bin: {
|
|
38
|
+
pelican: "dist/server/index.js"
|
|
39
|
+
},
|
|
40
|
+
files: [
|
|
41
|
+
"dist",
|
|
42
|
+
"README.md",
|
|
43
|
+
"LICENSE",
|
|
44
|
+
"CHANGELOG.md"
|
|
45
|
+
],
|
|
46
|
+
engines: {
|
|
47
|
+
node: ">=22.12"
|
|
48
|
+
},
|
|
49
|
+
scripts: {
|
|
50
|
+
dev: "node scripts/dev.mjs",
|
|
51
|
+
demo: "node dist/server/index.js --demo",
|
|
52
|
+
build: "vite build && node scripts/build-server.mjs",
|
|
53
|
+
start: "node dist/server/index.js",
|
|
54
|
+
typecheck: "tsc --noEmit -p tsconfig.json",
|
|
55
|
+
test: "vitest run",
|
|
56
|
+
screenshots: "node scripts/screenshots.mjs",
|
|
57
|
+
prepublishOnly: "npm run typecheck && npm test && npm run build"
|
|
58
|
+
},
|
|
59
|
+
dependencies: {
|
|
60
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.318",
|
|
61
|
+
"@hono/node-server": "^2.1.6",
|
|
62
|
+
hono: "^4.13.16",
|
|
63
|
+
"qrcode-generator": "^1.4.4",
|
|
64
|
+
"web-push": "^3.6.7",
|
|
65
|
+
ws: "^8.22.1"
|
|
66
|
+
},
|
|
67
|
+
devDependencies: {
|
|
68
|
+
"@tailwindcss/vite": "^4.3.4",
|
|
69
|
+
"@tanstack/react-query": "^5.105.3",
|
|
70
|
+
"@testing-library/react": "^16.3.0",
|
|
71
|
+
"@types/node": "^24.0.0",
|
|
72
|
+
"@types/react": "^19.1.0",
|
|
73
|
+
"@types/react-dom": "^19.1.0",
|
|
74
|
+
"@types/web-push": "^3.6.4",
|
|
75
|
+
"@types/ws": "^8.18.1",
|
|
76
|
+
"@vitejs/plugin-react": "^6.2.1",
|
|
77
|
+
diff: "^9.0.0",
|
|
78
|
+
esbuild: "^0.27.0",
|
|
79
|
+
jsdom: "^27.0.0",
|
|
80
|
+
"lucide-react": "^1.68.0",
|
|
81
|
+
"playwright-core": "^1.64.1",
|
|
82
|
+
react: "^19.3.1",
|
|
83
|
+
"react-dom": "^19.3.1",
|
|
84
|
+
"react-markdown": "^10.1.0",
|
|
85
|
+
"rehype-highlight": "^7.0.2",
|
|
86
|
+
"remark-breaks": "^4.0.0",
|
|
87
|
+
"remark-gfm": "^4.0.1",
|
|
88
|
+
tailwindcss: "^4.3.4",
|
|
89
|
+
tsx: "^4.23.15",
|
|
90
|
+
typescript: "^7.0.2",
|
|
91
|
+
vite: "^8.3.5",
|
|
92
|
+
vitest: "^5.1.0"
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// src/server/config.ts
|
|
99
|
+
var config_exports = {};
|
|
100
|
+
__export(config_exports, {
|
|
101
|
+
config: () => config,
|
|
102
|
+
configWarnings: () => configWarnings
|
|
103
|
+
});
|
|
104
|
+
import { existsSync } from "node:fs";
|
|
105
|
+
import os from "node:os";
|
|
106
|
+
import path from "node:path";
|
|
107
|
+
import { fileURLToPath } from "node:url";
|
|
108
|
+
function findWebDir() {
|
|
109
|
+
for (const candidate of [path.join(here, "../web"), path.join(here, "../../dist/web")]) {
|
|
110
|
+
if (existsSync(path.join(candidate, "index.html"))) return candidate;
|
|
111
|
+
}
|
|
112
|
+
return void 0;
|
|
113
|
+
}
|
|
114
|
+
function origins(name, value, wildcardHost = false) {
|
|
115
|
+
const kept = [];
|
|
116
|
+
for (const entry of (value ?? "").split(/[\s,]+/).filter(Boolean)) {
|
|
117
|
+
const wildcard = wildcardHost && /^https?:\/\/\*\.[^*]+$/i.test(entry);
|
|
118
|
+
let url;
|
|
119
|
+
try {
|
|
120
|
+
url = /^https?:\/\//i.test(entry) ? new URL(wildcard ? entry.replace("*.", "wildcard.") : entry) : void 0;
|
|
121
|
+
} catch {
|
|
122
|
+
url = void 0;
|
|
123
|
+
}
|
|
124
|
+
const origin = url && (wildcard ? url.origin.replace("://wildcard.", "://*.") : url.origin);
|
|
125
|
+
if (url && origin && url.pathname === "/" && !url.search && !url.hash && !url.username && !url.password && (wildcard ? /^https?:\/\/\*\.[a-z0-9.-]+(:\d+)?$/ : /^https?:\/\/([a-z0-9.-]+|\[[0-9a-f:.]+\])(:\d+)?$/).test(origin)) {
|
|
126
|
+
kept.push(origin);
|
|
127
|
+
} else {
|
|
128
|
+
configWarnings.push(`${name}: ignored "${entry}"; use an origin like https://pelican.example.com`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return kept;
|
|
132
|
+
}
|
|
133
|
+
var here, home, list, configWarnings, config;
|
|
134
|
+
var init_config = __esm({
|
|
135
|
+
"src/server/config.ts"() {
|
|
136
|
+
"use strict";
|
|
137
|
+
here = path.dirname(fileURLToPath(import.meta.url));
|
|
138
|
+
home = os.homedir();
|
|
139
|
+
list = (value) => (value ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
140
|
+
configWarnings = [];
|
|
141
|
+
config = {
|
|
142
|
+
host: process.env.PELICAN_HOST ?? "127.0.0.1",
|
|
143
|
+
port: Number(process.env.PELICAN_PORT ?? 7860),
|
|
144
|
+
/** Name shown in the app, for telling machines apart. */
|
|
145
|
+
machine: process.env.PELICAN_MACHINE ?? (os.hostname().split(".")[0] || "local"),
|
|
146
|
+
home,
|
|
147
|
+
claudeHome: process.env.CLAUDE_CONFIG_DIR ?? path.join(home, ".claude"),
|
|
148
|
+
/** The password hash lives here. */
|
|
149
|
+
configDir: process.env.PELICAN_CONFIG_DIR ?? path.join(home, ".config", "pelican"),
|
|
150
|
+
/** Uploads, push keys and subscriptions. */
|
|
151
|
+
stateDir: process.env.PELICAN_STATE_DIR ?? path.join(home, ".local", "state", "pelican"),
|
|
152
|
+
webDir: findWebDir(),
|
|
153
|
+
/**
|
|
154
|
+
* auto: sign in with the password when one is set (`pelican password`), else
|
|
155
|
+
* with the access token in the link Pelican prints. none: no sign-in, for
|
|
156
|
+
* when something in front already authenticates (a proxy, a gateway).
|
|
157
|
+
*/
|
|
158
|
+
auth: process.env.PELICAN_AUTH === "none" ? "none" : "auto",
|
|
159
|
+
/** Origins, besides the page's own, allowed to send requests (for example a proxy's public URL). */
|
|
160
|
+
allowedOrigins: origins("PELICAN_ALLOWED_ORIGINS", process.env.PELICAN_ALLOWED_ORIGINS),
|
|
161
|
+
/** Host names the server answers to, besides this machine's own names and addresses (and *.ts.net). */
|
|
162
|
+
allowedHosts: list(process.env.PELICAN_ALLOWED_HOSTS),
|
|
163
|
+
/**
|
|
164
|
+
* A proxy on this machine (Tailscale, Caddy, nginx) sets X-Forwarded-For truthfully: limit sign-in
|
|
165
|
+
* attempts by the client it names. Off by default, since anyone can write that header.
|
|
166
|
+
*/
|
|
167
|
+
trustedProxy: process.env.PELICAN_TRUSTED_PROXY === "1",
|
|
168
|
+
/** Pages, besides Pelican's own origin, that may show Pelican in a frame (space or comma separated). */
|
|
169
|
+
frameAncestors: origins("PELICAN_FRAME_ANCESTORS", process.env.PELICAN_FRAME_ANCESTORS, true),
|
|
170
|
+
/** Where a tapped notification opens a session; `{id}` is replaced. Default: the page that subscribed. */
|
|
171
|
+
sessionUrl: process.env.PELICAN_SESSION_URL || void 0,
|
|
172
|
+
/** A Claude process with nothing to do is closed after this long; the next message resumes the session. */
|
|
173
|
+
idleCloseMs: Number(process.env.PELICAN_IDLE_CLOSE_MS ?? 30 * 60 * 1e3),
|
|
174
|
+
/** A transcript written by someone else this recently counts as live. */
|
|
175
|
+
liveWindowMs: 45e3,
|
|
176
|
+
claudeExecutable: process.env.PELICAN_CLAUDE_PATH || void 0,
|
|
177
|
+
/** Demo mode: made-up sessions, nothing runs. */
|
|
178
|
+
demo: false
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// src/server/security.ts
|
|
184
|
+
import os2 from "node:os";
|
|
185
|
+
function isLoopback(host) {
|
|
186
|
+
const h = hostname(host);
|
|
187
|
+
return h === "localhost" || h.endsWith(".localhost") || h === "::1" || /^127(\.\d{1,3}){3}$/.test(h);
|
|
188
|
+
}
|
|
189
|
+
function listensLocally(host) {
|
|
190
|
+
return isLoopback(host);
|
|
191
|
+
}
|
|
192
|
+
function knownHosts() {
|
|
193
|
+
const names = /* @__PURE__ */ new Set();
|
|
194
|
+
const machine = os2.hostname().toLowerCase();
|
|
195
|
+
names.add(machine);
|
|
196
|
+
names.add(machine.split(".")[0]);
|
|
197
|
+
for (const address of Object.values(os2.networkInterfaces()).flat()) if (address) names.add(address.address.toLowerCase());
|
|
198
|
+
if (config.host !== "0.0.0.0" && config.host !== "::") names.add(hostname(config.host));
|
|
199
|
+
for (const host of config.allowedHosts) {
|
|
200
|
+
const name = hostname(host);
|
|
201
|
+
if (name) names.add(name);
|
|
202
|
+
}
|
|
203
|
+
for (const origin of config.allowedOrigins) {
|
|
204
|
+
try {
|
|
205
|
+
names.add(new URL(origin).hostname.toLowerCase().replace(/^\[(.*)\]$/, "$1"));
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return names;
|
|
210
|
+
}
|
|
211
|
+
function hostAllowed(host) {
|
|
212
|
+
if (!host) return false;
|
|
213
|
+
const name = hostname(host);
|
|
214
|
+
if (!name) return false;
|
|
215
|
+
if (isLoopback(host)) return true;
|
|
216
|
+
if (name.endsWith(".ts.net")) return true;
|
|
217
|
+
return knownHosts().has(name);
|
|
218
|
+
}
|
|
219
|
+
function originAllowed(origin, ...hosts) {
|
|
220
|
+
if (!origin) return true;
|
|
221
|
+
let url;
|
|
222
|
+
try {
|
|
223
|
+
url = new URL(origin);
|
|
224
|
+
} catch {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
if (config.allowedOrigins.includes(url.origin)) return true;
|
|
228
|
+
return hosts.flatMap((h) => h ? h.split(",").map((s) => s.trim()) : []).includes(url.host);
|
|
229
|
+
}
|
|
230
|
+
var hostname;
|
|
231
|
+
var init_security = __esm({
|
|
232
|
+
"src/server/security.ts"() {
|
|
233
|
+
"use strict";
|
|
234
|
+
init_config();
|
|
235
|
+
hostname = (host) => {
|
|
236
|
+
const h = host.trim().toLowerCase();
|
|
237
|
+
if (h.startsWith("[")) return h.slice(1, h.indexOf("]"));
|
|
238
|
+
const colon = h.lastIndexOf(":");
|
|
239
|
+
return colon > -1 && h.indexOf(":") === colon ? h.slice(0, colon) : h;
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// src/server/auth.ts
|
|
245
|
+
var auth_exports = {};
|
|
246
|
+
__export(auth_exports, {
|
|
247
|
+
accessToken: () => accessToken,
|
|
248
|
+
attemptAllowed: () => attemptAllowed,
|
|
249
|
+
authMode: () => authMode,
|
|
250
|
+
beginAttempt: () => beginAttempt,
|
|
251
|
+
checkPassword: () => checkPassword,
|
|
252
|
+
checkToken: () => checkToken,
|
|
253
|
+
clearPassword: () => clearPassword,
|
|
254
|
+
cookieName: () => cookieName,
|
|
255
|
+
issueCode: () => issueCode,
|
|
256
|
+
loadAuth: () => loadAuth,
|
|
257
|
+
newSessionCookie: () => newSessionCookie,
|
|
258
|
+
noteFailure: () => noteFailure,
|
|
259
|
+
passwordSet: () => passwordSet,
|
|
260
|
+
redeemCode: () => redeemCode,
|
|
261
|
+
resetAttempts: () => resetAttempts,
|
|
262
|
+
resetToken: () => resetToken,
|
|
263
|
+
setPassword: () => setPassword,
|
|
264
|
+
signedIn: () => signedIn
|
|
265
|
+
});
|
|
266
|
+
import { createHash, createHmac, randomBytes, scrypt, timingSafeEqual } from "node:crypto";
|
|
267
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
268
|
+
import path2 from "node:path";
|
|
269
|
+
function derive(text2, salt, params) {
|
|
270
|
+
return new Promise((resolve, reject) => {
|
|
271
|
+
scrypt(text2.normalize("NFKC"), salt, 32, { ...params, maxmem: 64 * 1024 * 1024 }, (error, key) => error ? reject(error) : resolve(key));
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
async function writePrivate(file2, value) {
|
|
275
|
+
await mkdir(path2.dirname(file2), { recursive: true, mode: 448 });
|
|
276
|
+
await writeFile(file2, `${JSON.stringify(value, null, 2)}
|
|
277
|
+
`, { mode: 384 });
|
|
278
|
+
}
|
|
279
|
+
async function loadAuth() {
|
|
280
|
+
if (loaded) return;
|
|
281
|
+
password = await readJson(passwordFile());
|
|
282
|
+
if (config.auth !== "none") {
|
|
283
|
+
token = await readJson(tokenFile());
|
|
284
|
+
if (!token?.token || !token.secret) await resetToken();
|
|
285
|
+
}
|
|
286
|
+
loaded = true;
|
|
287
|
+
}
|
|
288
|
+
function authMode() {
|
|
289
|
+
if (config.auth === "none") return "none";
|
|
290
|
+
return password ? "password" : "token";
|
|
291
|
+
}
|
|
292
|
+
function passwordSet() {
|
|
293
|
+
return Boolean(password);
|
|
294
|
+
}
|
|
295
|
+
function accessToken() {
|
|
296
|
+
return authMode() === "token" ? token?.token : void 0;
|
|
297
|
+
}
|
|
298
|
+
async function resetToken() {
|
|
299
|
+
token = { version: 1, token: randomBytes(24).toString("base64url"), secret: randomBytes(32).toString("base64") };
|
|
300
|
+
await writePrivate(tokenFile(), token);
|
|
301
|
+
return token.token;
|
|
302
|
+
}
|
|
303
|
+
async function setPassword(text2) {
|
|
304
|
+
const salt = randomBytes(16);
|
|
305
|
+
const hash = await derive(text2, salt, PARAMS);
|
|
306
|
+
password = { version: 1, salt: salt.toString("base64"), hash: hash.toString("base64"), secret: randomBytes(32).toString("base64"), ...PARAMS };
|
|
307
|
+
await writePrivate(passwordFile(), password);
|
|
308
|
+
loaded = true;
|
|
309
|
+
}
|
|
310
|
+
async function clearPassword() {
|
|
311
|
+
await rm(passwordFile(), { force: true });
|
|
312
|
+
password = void 0;
|
|
313
|
+
}
|
|
314
|
+
async function checkPassword(text2) {
|
|
315
|
+
if (!password) return false;
|
|
316
|
+
const expected = Buffer.from(password.hash, "base64");
|
|
317
|
+
const actual = await derive(text2, Buffer.from(password.salt, "base64"), password);
|
|
318
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
319
|
+
}
|
|
320
|
+
function checkToken(given) {
|
|
321
|
+
const current = accessToken();
|
|
322
|
+
return Boolean(current && given && same(given, current));
|
|
323
|
+
}
|
|
324
|
+
function cookieName() {
|
|
325
|
+
const value = secret();
|
|
326
|
+
return `pelican_${value ? createHash("sha256").update(value).digest("hex").slice(0, 8) : "none"}`;
|
|
327
|
+
}
|
|
328
|
+
function sign(expires) {
|
|
329
|
+
return createHmac("sha256", Buffer.from(secret() ?? "", "base64")).update(String(expires)).digest("base64url");
|
|
330
|
+
}
|
|
331
|
+
function newSessionCookie() {
|
|
332
|
+
const maxAge = SESSION_DAYS * 24 * 3600;
|
|
333
|
+
const expires = Date.now() + maxAge * 1e3;
|
|
334
|
+
return { name: cookieName(), value: `${expires}.${sign(expires)}`, maxAge };
|
|
335
|
+
}
|
|
336
|
+
function cookieValue(header, name) {
|
|
337
|
+
for (const part of (header ?? "").split(";")) {
|
|
338
|
+
const [key, ...rest] = part.trim().split("=");
|
|
339
|
+
if (key === name) return rest.join("=");
|
|
340
|
+
}
|
|
341
|
+
return void 0;
|
|
342
|
+
}
|
|
343
|
+
function signedIn(cookieHeader2, authorization) {
|
|
344
|
+
const mode = authMode();
|
|
345
|
+
if (mode === "none") return true;
|
|
346
|
+
if (mode === "token" && authorization?.startsWith("Bearer ") && checkToken(authorization.slice(7).trim())) return true;
|
|
347
|
+
if (!secret()) return false;
|
|
348
|
+
const value = cookieValue(cookieHeader2, cookieName());
|
|
349
|
+
if (!value) return false;
|
|
350
|
+
const [expires, mac] = value.split(".");
|
|
351
|
+
const when = Number(expires);
|
|
352
|
+
if (!mac || !Number.isFinite(when) || when < Date.now()) return false;
|
|
353
|
+
const expected = Buffer.from(sign(when));
|
|
354
|
+
const given = Buffer.from(mac);
|
|
355
|
+
return expected.length === given.length && timingSafeEqual(expected, given);
|
|
356
|
+
}
|
|
357
|
+
function attemptAllowed(client, now = Date.now()) {
|
|
358
|
+
allFailures = recent(allFailures, now);
|
|
359
|
+
if (allFailures.length >= IN_ALL) return false;
|
|
360
|
+
return recent(failures.get(client) ?? [], now).length < PER_CLIENT;
|
|
361
|
+
}
|
|
362
|
+
function noteFailure(client, now = Date.now()) {
|
|
363
|
+
allFailures.push(now);
|
|
364
|
+
failures.set(client, [...recent(failures.get(client) ?? [], now), now]);
|
|
365
|
+
if (failures.size > 1e3) {
|
|
366
|
+
for (const [key, times] of failures) if (recent(times, now).length === 0) failures.delete(key);
|
|
367
|
+
while (failures.size > 5e3) failures.delete(failures.keys().next().value);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function beginAttempt(client, now = Date.now()) {
|
|
371
|
+
if (!attemptAllowed(client, now)) return void 0;
|
|
372
|
+
noteFailure(client, now);
|
|
373
|
+
return () => {
|
|
374
|
+
const drop = (times) => {
|
|
375
|
+
const at = times?.lastIndexOf(now) ?? -1;
|
|
376
|
+
if (times && at >= 0) times.splice(at, 1);
|
|
377
|
+
};
|
|
378
|
+
drop(allFailures);
|
|
379
|
+
drop(failures.get(client));
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
function resetAttempts() {
|
|
383
|
+
failures.clear();
|
|
384
|
+
allFailures = [];
|
|
385
|
+
}
|
|
386
|
+
function issueCode(now = Date.now()) {
|
|
387
|
+
for (const [code2, expires] of codes) if (expires <= now) codes.delete(code2);
|
|
388
|
+
while (codes.size >= 20) codes.delete(codes.keys().next().value);
|
|
389
|
+
const code = randomBytes(18).toString("base64url");
|
|
390
|
+
codes.set(code, now + CODE_MS);
|
|
391
|
+
return code;
|
|
392
|
+
}
|
|
393
|
+
function redeemCode(given, now = Date.now()) {
|
|
394
|
+
if (!given || authMode() === "none") return false;
|
|
395
|
+
for (const [code, expires] of codes) {
|
|
396
|
+
if (same(code, given)) {
|
|
397
|
+
codes.delete(code);
|
|
398
|
+
return expires > now;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
var passwordFile, tokenFile, SESSION_DAYS, PARAMS, password, token, loaded, readJson, same, secret, WINDOW_MS, PER_CLIENT, IN_ALL, failures, allFailures, recent, CODE_MS, codes;
|
|
404
|
+
var init_auth = __esm({
|
|
405
|
+
"src/server/auth.ts"() {
|
|
406
|
+
"use strict";
|
|
407
|
+
init_config();
|
|
408
|
+
passwordFile = () => path2.join(config.configDir, "auth.json");
|
|
409
|
+
tokenFile = () => path2.join(config.configDir, "token.json");
|
|
410
|
+
SESSION_DAYS = 30;
|
|
411
|
+
PARAMS = { N: 16384, r: 8, p: 1 };
|
|
412
|
+
loaded = false;
|
|
413
|
+
readJson = async (file2) => {
|
|
414
|
+
try {
|
|
415
|
+
return JSON.parse(await readFile(file2, "utf8"));
|
|
416
|
+
} catch {
|
|
417
|
+
return void 0;
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
same = (a, b) => {
|
|
421
|
+
const x = createHash("sha256").update(a).digest();
|
|
422
|
+
const y = createHash("sha256").update(b).digest();
|
|
423
|
+
return timingSafeEqual(x, y);
|
|
424
|
+
};
|
|
425
|
+
secret = () => authMode() === "password" ? password?.secret : authMode() === "token" ? token?.secret : void 0;
|
|
426
|
+
WINDOW_MS = 10 * 6e4;
|
|
427
|
+
PER_CLIENT = 10;
|
|
428
|
+
IN_ALL = 30;
|
|
429
|
+
failures = /* @__PURE__ */ new Map();
|
|
430
|
+
allFailures = [];
|
|
431
|
+
recent = (times, now) => times.filter((t) => now - t < WINDOW_MS);
|
|
432
|
+
CODE_MS = 2 * 6e4;
|
|
433
|
+
codes = /* @__PURE__ */ new Map();
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// src/server/demo-image.ts
|
|
438
|
+
var SETTINGS_SCREENSHOT;
|
|
439
|
+
var init_demo_image = __esm({
|
|
440
|
+
"src/server/demo-image.ts"() {
|
|
441
|
+
"use strict";
|
|
442
|
+
SETTINGS_SCREENSHOT = "iVBORw0KGgoAAAANSUhEUgAAAtAAAAHCCAIAAABJw2nGAAAQAElEQVR4nOzdB3gUVRuG4ZPeIIkECDUQEnrvHQSpFkBFRBEpoojwIxaKomBDQSxYEESKSrPRERGQLr2HGloKJYQW0kjP/+0OjmsCMcjObkie+1r3mp2ZnZmsIefd75yZcY6PT1AAAABGclQAAAAGI3AAAADDETgAAIDhCBwAAMBwBA4AAGA4AgcAADAcgQMAABiOwAEAAAxH4AAAAIYjcAAAAMMROAAAgOEIHAAAwHAEDgAAYDgCBwAAMByBAwAAGI7AAQAADEfgAAAAhiNwAAAAwxE4AACA4QgcAADAcAQOAABgOAIHAAAwHIEDAAAYjsABAAAMR+AAAACGI3AAAADDETgAAIDhCBwAAMBwBA4AAGA4AgcAADAcgQMAABiOwAEAAAxH4AAAAIYjcAAAAMMROAAAgOEIHAAAwHAEDgAAYDhnlTdcu3bt3LnzZ8+euXr1qr+/f0BAQKlSpVxdXRUAALj72T9w7Nmz5/33x+/bty/7oqFD/9enz9M+Pj7KHmbNmjVu3AfadJ06dX755ScFAAD+E3sGjszMzNdff+Pnn3++1Qqff/7FjBkzv/pqcvPmzdQd+PDDiampadp0//59S5YsqS/aunXb2rXrtOmKFYN79HhMX5ScnKJPx8fHKwAA8F/ZM3DMmTM3h7ShSUhIeOGFwYsWLaxQIVD9JxkZGdOmfaO/fOCBzpaBY//+/VLJ0KZbtmxpGTgAAIC12G3Q6Pnz599++x3LOX5+fo891n3gwOcaN25sOV8yx9ChQ5XNOTr+/eE4OzspAADwX9mtwrF//wHLl998M61Nm3v1lxcuXOjTp9+JEye0l0ePHrt06VLRokWVDQ0Y8Mwzz/RXAADgjtktcBw6dEiflpKGZdoQ/v7+7733bs+eT+hzDhwIadu2jeU6oaGha9euO3z4yJEjh318fOvWrVu7dq0GDeqXKFFCWyElJUVWyMzMsHzXli1bo6KiZB03N/fw8LCjR4/qiy5ejF65cqUy9614eXmFhIRs375DWyTVl0cffUSbll6YvXtvDHGtWrVKo0aN1q/fsGzZsrCwMJlToUJQw4YNHnnkYRcXF5XNuXPn1q1bf+DAAfnxpWenWrVqTz3Vy9nZ+eeff9FWcHV17du3j+VboqMvzps3TyLXyZMnYmPjypYtW6pUqdatW91/f2cPDw8FAMDdwCE+PkHZw7hx78+a9a02LY3oihXLszSfaWlpEyZM1F926NBeGnJtOj09fcaMmR9+OPGmWx47dkzv3k8pc5mkefOWN11HtlasWLG5c+fddOmyZUslSUyd+vVHH32szQkODl65coU2/fnnX8hDm+7V60k5zh9/zHoCi/xEs2d/V6ZMGcuZO3fuGjDgWekhspwpyWbkyBFjxozV55w4EapPz507d+zYt292jKY3Tp/+jf6ZAACQl9mtwlGlShV9OjIyslevp5577tmmTZvqJ8HK9/7Ro1+76XtHjBi1ZMmSW2357bffcXR06NWrlzLe4sVLsgQIjfxEw4ePmDNntpPTjcEff/65pU+fvtnXlLdbpg1LUgu5VdrQ3ijxZeHCBUFBFRQAAHmb3QKH9CZYvpQekyFDTCNDa9Wq2axZs/r169etW8fX1zf7G3fs2JElbdSvX+/atVh9wIcyFTne7ty5s3SayKL09AzLi3xIrcLHx7tixYqSbGRpWFj45cuXtUVSM6hSpbJ5wlPljp42pI8mLi7OMnxIPePYsWPaj5mZmXmrekwORo36O2/J9qVHSXphdu3atWHDRn3vCxcuHD78VQUAQN5mt8AhfRbSH5G9U0OShzy0aQkEkhseffSRwoULa3Ok5R4z5i19ZWmG58+fK/0XMv3bb7/9738v6ovkpRQ5fvzxh4yMjEqV/q6mjB//fp06dbTp/v37Wfab1KtXb9asGeo2mXtPvi9TprR09LzzznvSCaIvCg8P1wLH+vUbLMesBAaWf++9d2vVqnX+fNTYsWO3bt2WfbNnzpzVk5D48svP9cMeOXLUggULtek//lhL4AAA5H32vJfKm2++0alTxxxW2L17z3vvjWvRotXGjTe+01+8eNGykiGJQUsbQqKJVEf0RT/99Iuyia++mixpQyak92To0CGWiyIjz2gTq1atzvKWxo0be3h4VKgQOH36N35+ftk3GxUV9c+3TN2zZ49kGpkeNOj5t98eqz3kE1AAAOR59rzwl7Oz85dffrF///4ffvhp7dq1ll/oLUnHQf/+A1at+l2a54iISMtFiYmJlgM2ixf3V+pGdUQqCteuXbPBZdG1XhiNRAfpl9E7VrR8ICIiIvR1WrRoLh06+ks3N7dnnumfvcOlbNl/DDhda6bMZ9A0btxIulcqVaqkAAC4S9j/Xiq1zaTjIzT0+G6paezZu3nz5uzhY9y4cTNmTI+M/EfgmDTpsxy2HB8fb3TgkG4OBwcHyzlFihTJPozUsipTsWLWoHDTi6j6+/u3bt1KH66h22Qm3UDBwcFvvTWmSZMmCgCAPC+v3J7e0dFRSgW9ej358ccTt23bsnz5Mu3UVt2uXbszMzMvXbqU+21K/UMZTB9ckjPL/OTrmzUDubu73/RdEyd+KP1E6hYkxDz11NPZz8gFACAPsluF4+2339ErAa1bt37ggfv1RVIzkPAxduwY6Z6YOvVrbaasHBUVpY2W0L3yyss57EKKDSpvCAwsf/p0mDZ99OixLEvPn4+66bvk+L/44rNDh55btWr10qXLslR3NKNHv3HffW1tfA1WAABulx0vbb5fPxtl7969loFDV7JkCcuXkj/KlStnOeeJJ57IXjDIgypWrKgHjh07dly/ft3yKmfa5U2zuHjxYlyc6Ra1Hh6evXr1eumlYefPn9+xY+fGjZuynBUcEnIwy3VaAQDIa+zWpVK7dm19WhrjRYsWZ1lBuiEszzQJDCzv7e0dEBBguc6cOXMsX06ZMvWJJ57UHs8997y6mZSUFHULOSy6Q5ZXOZOf6803x2Zk3Ljg+vz5P2QfqCEmTJjYoUNH7fH006aLnZcsWbJr1y7S5bRo0ULLNWNjrykAAPI2u1U4mjZtOnv233Fh+PARv/22skmTxqVKlYqJiYmIiJg7d57l6Mt7771XngsVKiTf5tetW6/NnDTpM6kWSJ+CMl2PfLnlBm81+kHeW6JEiXvuuSf78IuDBw8ePnzYx8endOnSyqqeeKKnfjV0Zbo+6eLVq1dLR1J4eLjl9TksVa9eVVbTpk+cOPHNN9OfeqqX1EXkM9m1a5flmpUrV1EAAORtdgscHTq079nz8R9++FGfo5/5mV1gYPlhw25c1Ou9997t0KGTnkW+/nqaPLK/ZdiwG3e0d3R0lIShX9ZCWm55yN6/+mqyMpcN9LfINrt06ab+upeKsp5ixYq9+OLQzz773HJfK1asyOEtrVvfO27cB/rLCRM+lIflObca+dGCg4MUAAB5m50v/NWyZct/Xc3Pz0/CgbS12kt/f/8PP5yQw/qy5jffTAsK+rsZlp6IW60sZQZlE88/P/BWN7vv2rVr9pkVKgR+8MH7WWZmP+F2ypSvnJ3tf24zAAA5s2fgcHNzmzVrxnfffatftDsLiQ6jRo1cv36t5ZWyRMeOHTZuXH///TcZZ9qiRfNVq1ZmGUT59NO9b3VJU19fn4kTP7zptT71+64p0zXKnP51fvY5UlzRp11cXF57bZQkITlsKUtoM+vXrzdjhqncoq+m5yrx2GPdZ8/+/lYfTrt27ZYuXVyzZg0FAECeZ7fb02dx/vz5c+fkce7s2bOFChUqXbp0mTLyKJPlnvVZXLp06dSpUydOnJSmXUKJVDVyOGklOTk5Ojo6Li7O1dXVx8dHujn0RZmZmTExMdHRF2XC3d1N9i75QBnp+vXr7u7u2kXDpk+fMX78jZpNcHDwypX/6GqRQzp16rR8PPIRXblytXjx4lLjCQgom+Xe9wAA5GV5JXDkb8uWLV+/foP+8tVXX9bHjkjyeOyxHvrFOaSP6T/cQA4AgDyO7n9bkEqG5cUzpI4zdOjQokX9pHTx7bffWl4KrH37dgoAgHyHCoctxMRca9363uxDPrPw8/NbtWqlDW44BwCAjeWVe6nkb76+PnPnzrYcEJpd48aNly9fStoAAORLVDhsJzo6esGChXPnztMvCqLMF9KoVq1aw4YN+vfvZ3n+CwAA+QmBww5SUlIuXryYlJRUqlSpnE/DAQAgfyBwAAAAwzGGAwAAGI7AAQAADEfgAAAAhiNwAAAAwxE4AACA4QgcAADAcAQOAABgOAIHAAAwHIEDAAAYjsABAAAMR+AAAACGI3AAAADDETgAAIDhCBwAAMBwBA4AAGA4AgcAADAcgQMAABiOwAEAAAxH4AAAAIZzVnlVenpGWlpaukmGyMzMVAAA4J8cHBwcHR2dnOTh5OzsLBMqT8qLgSMlJTUlJUXChkzLR+fi4uLs7CifpwIAAP+UmSnfz+UhTWeqMrebriYuKo9xiI9PUHlGampqUlKK1DQkY3h6eri7u8sHpwAAwL+RL+pJSUmJiddTU9Ok2uHu7irf2FWekYcCR2JikhQ2JGoULlzIw8NdAQCA23f9elJcXLzEDil0eHrmlfY0TwSOjIzMxMREiWYSNby9CykAAHBnYmPjJXZIR4Gnp6ejo4OyN/sHDkkbCQmJGRnp99zjS2EDAABrkVLH1asxjo5OXl72zxz2H4kptQ1JG35+RUgbAABYkTSs0rxKIytNrbI3OweOxMQk6UmR2oabm6sCAABWJc2rNLLS1EqDq+zKnoEjNdV0+itDRAEAMI40stLUSoMrza6yH3sGjqQk0zkpjBIFAMBQ0tRKgyvNrrIfuwUOSVrp6abTUhQAADCYNLjS7GoXB7MLOwYOU3mDzhQAAGxAGlxpdqXxVXZin8Bhvk9KuqenhwIAADYhza75HmUZyh7sEzjS0tLk2d2d8gYAADaiNbtaE2x79rl5m+QrZ2cn7pMCAIDNaC2vNMHKHuzWpeLsnOduZAcAQP4mjW/B6lLJyJDAwe3mAQCwKWl8pQlW9mCfLpXMzEwHBwIHAAA2JY2vNMHKHuwTOJTpZ1YAAKCAoMwAAAAMR+AAAACGI3AAAADDETgAAIDhCBwAAMBwBA4AAGA4AgcAADAcgQMAABiOwAEAAAxH4AAAAIYjcAAAAMMROAAAgOEIHAAAwHB2u1usFWVmZm7cuFEm6tat6+3trQAAMDt+/NT169dzWKFs2dL33OOrrCQsLCI2Ni4goIyvr4+8PHDgkDxXr17Fyckp5zdKQ7Zr1155btCgrqNj/qwF5IfAkZ6e3rNnL5lYsmRho0aNFAAAZvPn/xITcy2HFR555KEWLZooK1myZEV4eGSPHt2aNGmYkZHx7bfzZObYsSN9fP7ly7BEjfnzFyhTOqnq5eWp8qP8EDgAAMiBu7t74cKFbrrI27uwMpiDg4IicAAA8r3Ondu1bNlU2Zb0jEyc+I6ULpydCbrR7wAAEABJREFUaWpNCtynEBkZuWXLVjc3t3r16gYEBFguCgkJcXBwqFGjxrVr1/bvPxAeHh4UFFSzZo3Chf+Rf6VKdurUqaNHj5UuXVqWym9SVFSUPBctWlSWRkVdiI6+ULx48RIlSuhv0Wb6+5fw9y9uuZ3Dhw/v3r1HtiMHU6RIkSyHmvOONPKrfPp0mGwnOTm5Vq2awcHBDmRpALhN8if03LkoT08P+St95crVU6fC4uMTypUrW7Zs6SxxIS0tLTz8zMWLl2RpyZL+8jI2Nk4qKPLe7JuNjr4kf6VlNf0vc0TEGelziYuL9/cvFhBQplixojc9mNDQk9JwlC5dKjAwwMPDQ+ULBShwrF277vXXR4eHR+hzpOV+443XH3+8hzL/DnXo0Fkm5s6d3atXb8t1lixZVKFCoPbyypUrgwf/b/36DdrLQoUKjRgxfMyYseXKBWzbtkXmzJo16/PPvxwy5IXRo1/XNzJz5swvvpg8cuTwYcNe1OZ89tkXX345OT4+Xl+nQoUKU6dOrlmzZi53JCQYvfTSK7/9tlLfiCydPfu7ihUrKgBArp0+HT5t2ndlypQOCiq/YcOf+nwJBC+//IJ8R9VeSlaYOnVmcnKK9lJyRsOGdTdt2lqzZrV+/Xpl2aZ8aZw48XP11xiO9PT0b775TpKE5Tpt27Z64IEOll8UZe9r1qzXX0rcefXV/xUvXlTd/QpK4LhwIVqLEQ88cH/nzp0uX74s+WPDho3Dhr3crFlTCbH6mrKahIx+/fqkpKTMmDHr0qVLw4ePWLDgZ2UenfrYY48fPnxEmv8uXR4qUuSeH374SUKAuk1r1vwxfvwEmRg8+IUaNapLGWPBgkXy3KdP/z17duZyR6mpaV26PBwaGipHO2BAf0dHp99//13qJfff/9Dy5UsrV66kAAC348yZs/K45x7funVrSQ0jJOSwPK9Ysfrhhx9Upu+BVz/7bKpMeHl5Vq1aWdqIAwcOSdrI5cZlO5I2JEA0bdqwRAl/yS47duxeu3ZjUFBg1ap//8XW0oYkGF9fnz//3C5fhr/7bt7w4UPV3a+gBI59+/bJc/369aZPn6bNefrp3g0bNpE8cejQIcvAUadO7fnz5/r6ms6Satq0Sc+evaQLRmpikkAXL16ihYDVq38vX76crPDCC4M6d37AsmqSG5s3m+KzVDuk5qHN6dixY7t2Hc6fPy9JyM/PLzc7mjdvnqQNKWn8/vtvPj6m86+ef/65vn37S5D69tvvPvhgnAIAmC1ZsmLFilXZ51eqFJylMlGrVvXevR/XzmL9+efFW7fujIw8qy1auHC5PEsOGDXqJVdXF2Wui3zxxTSVOxJflOmvfdv77mutTO1LQwku69Zt2rlzj2XgkG+YEi/c3FxlunLlitOnf3/hwkWtDVJ3uYJy4a9KlSpNnfrVhAnj9TkpKanFixeTiaSkZMs1X3ttlJY2RMOGDbUJiQLyLDlAnocMGayFAHHPPfKb8aq6TQ899KAcTJ8+T+tz9C466brL5Y6+/362PEs/i5Y2hIuLy4ABz8jEL78skFKeAgCYyZ9E6QfJ/oiNjcuy5mOPddOvmVGliikHREVFK/OAucOHj8rE448/rKUNERhYrlq1Krk7BFPpWp6laCGVEm1Ohw5tX311iHSpWK720EOdtLRh3n6AdvD6W+5qBaXCERhYXh5SIfjssy9OnDghE0eOHLEcQqELCqqgT3t6ekqZQVZLTExUpgvIHFemyPmP3oqaNWuo2ySFFnns3Llr5syZYWHh8ggJCbFc4V93JL+4UgKRiZUrTd0o+vyEhAR5lgOOiYnJPgoVAAqmTp3ua9KkQfb58j3N8qW09JbXwNAu3iVdJ/KsX8yjVKkSlm+pUKGcFkT+VYsWTZYtWynbee+9j4oW9atSpWL16lUqVgzKcpmvEiX+Prcgy3fRu11BCRxSohgxYtSaNX/oc6R6ER19IXtvSJZzUixdvnxFnrOc2yKhRP2buLh/JJtt27aPHv2GlhiUeUBop04dJTrkfkeXLl3WJpYtW37TPWr/QgAAytxy5+Y61DmcD3L1aowyn+mapY3Qx5P+qzZtWvr4eK9bt/ns2XPyN3zzZnlsc3d3Hziwb7lyf3frS4ug8qmCEjhef/0NSRslS5YcPfq1xo0blSpVSn5v+vTpd1vDL+T3TIoHWa6SGxp6/F/fKDUVZa7IKVMPTlLv3n1kOy1aNH/uuWel1CGlCFlUqlTZ3O9IOvm0iWnTpjZtepNr5Pn5+SkAgJVo1wfLMLOsSZw5cy73G6lXr7Y8YmNjjx07sW9fyJEjodIifP31t++//6YqAArEGA5pzrX6weefT3r00UfKlCkjvy7yS7NlS25HF2u0Po49e/ZYztyxY4flSy3tXrx4SZ+Tlpa2b99+/aWED60r57vvZrVv307r+NASSe53JGXAcuVM9Y8DBw4UtXD16tXRo9+cOPFjrsYBAFakf80LD4/UZ0rjcvz4ydy8XULG5MnTp083jb2TWkvDhvWefbaPPJT5W2hCQqIqAPJV4DAPAsoqNTVV/VWkOnnyxm+G/JZ88smkm47hyEG7du2UKbV8GRYWrs3ZvPnPzz77wnKdqlWryvPGjZvk10ubM2HCh5Y7cnC48ZnrG5Gl77zz3u3uqHv37vK8YMGimJgYbY781o4Z89bSpcu0HxkAoJEvfikpqTd95HKIvXxN1To+fv55sT6iYvHiX3M5nNPLy+vkydOHDx+1DChxcTeGrHp4uKsCIF91qfTo0TP7TOlG2bNnZ4cO7RYuXDxq1OsHDoRI+Fi/fkNoaKg2IHT27Dk+Pj4tW7b4t82bzqT9/vvZ8sb27TtKh0hERIQ+DkNXu3YtZR4y0qFDp4YNG+7Zs/fUqVOWK1SsGCyHJCs8/vgTPXo8dvny5T/+WHvp0iXtYD788KMhQ17IzY4GDx40b9582U7btu0ffribs7PT8uUrZF+yneHDX1EAgL8sW7ZSHjdd1KxZo+7du6pceOKJR8ePnxQVFf3mm++XLVs6MvKs5BiVO05OTnXq1JRulClTZkp5o1gxv/DwM0eOHFOm+5zX0oruKr8rKKfFvvPO240bN1amy1fMnzbtm3Pnzk2cOOHHH+dLN4R0rKxbt15f86adEdpMFxfnZcuW9O/fT5lPD5EQ0Lp1q9mzv1OmnpQb+bRUqVJz55qKZuHhEb/8skASwOOP9xgz5g1lDsjy7OrqOmPGN5I5JGR89dWUH3/8ycvLc+nSxW+9NUaWysvjx0/kZkceHh6//fZr27ZtJHPIdqQcIvt64IH7V678VTauAAC5oP15t3zWOTo6qL/+dIvixYuNGPFi6dKlJGecPh0uEaFatSrt27dRpj/sLtm3lmUXjz3WtVKlIJnYuXPPihWrDx06IluoXr1qjx4PZ99ptoPMD421Q3x8grK5mJhYb+9Ct7p3n0G0e5ecOXM2ODioQoUK2u+Q+ZL1oeXKlcvNAGad9MhIXChSpIiEVuk9kVqFBIIffpinryC/jmFhYVeuXClfPlC72kcW169fP3jwkPSG1KxZs0QJf22mVDsuXIiWw5NQkssdyQpnz549cuSo/HRBQRWCg4MVAMBI6enpCQkJWquxYMHSP//cfu+9Lbp06Zyb9164cFH+zl+7Fuvj4126dEk/P1tfvyA2Nj4uLt7X9zaaPGspQPdSkYRRw8xyppubm377kn8lVYR58+Z16tRJqhHFit2IEQsXLlKmmlhdyzWdnZ1zbvulPtGwYdaTwv3MbmtHEpzLmCkAgJEmTZoSHX1JgkWTJg20tJGUlCy9JDJRvnxALjfi719MHqpA4p65t6Fq1SrSUfL119Okme/Qob2UMX79dYV0ghQqVKh790eU9dhsRwCAXPL3Lx4RcWbhwmUXL16SzpSrV6+uXbsxISHR27twtWqVFf5NAepSuXPSfzF27NvffDPdcqaEgJ9//rFOndrKemy2IwBALklX+KRJUy2veqBMf5m9hg//Xw5XjMxr7NilQuC4bbGxsdu379iyZauk2tq1a0sCMOgi4jbbEQAgly5fvnLkSOipU2HFixcLDAwoX76cfuuTuwKBAwAAGM6OgaOgnBYLAADsiMABAAAMR+AAAACGI3AAAADDETgAAIDhCBwAAMBwBA4AAGA4AgcAADAcgQMAABiOwAEAAAxH4AAAAIYjcAAAAMMROAAAgOEIHAAAwHAEDgAAYDgCBwAAMJyzusslJSUrAADyO3d3N3U3u+sDh0hOJnMAAPIzN7e7O22ofBA4JPHd7aEPAIB8jzEcAADAcAQOAABgOAIHAAAwHIEDAAAYjsABAAAMR+AAAACGI3AAAADDETgAAIDhCBwAAMBwBA4AAGA4AgcAADAcgQMAABiOwAEAAAxH4AAAAIYjcAAAAMMROAAAgOEIHAAAwHDOCgAAs/T09LQ0eUrPzMxU1uPg4ODk5OTsLE9O6j/5/fdVy5Yt37FjR2TkGWVVXl5e1apVbdu2TY8ej5UqVUrdPvms0syfWkZGxp1/bA4OytHR0fxxOcvnpvIRh/j4BGVzMTGx3t6FChcupAAAeYA0likpKdJoKiM5OTm6urpKg5r7t4SEhIwZ89a2bduV8UaOHD5s2Iu39ZbU1NSUlFRlDFdXF6GsKjY2Pi4u3tfXW9kcXSoAUNDJt/Pr15OMThvmHWWYd5Sey/WlsNGhQ2fbpA0xYcLEQYMG53795ORk49KGkI3LLlR+QeAAgAJNahtJSTZt1WR3stN/XU1qG3379le2tXjxkhEjRuVmTQkb0pGiDCa7kB2pfIHAAQAFmvSkKJvLzU7HjHlb2cPs2XNWrPgt53UkB6SlpSmbSDMxPNnYAIEDAAou0wBR43tSbrbfjJw7VqQzZdu2bcpOpk79OucV0tIM7Emx++4MQuAAgILLjl+dc971smXLlf3s3LkrNDT0VkulP8jGKc18BowdcqF1ETgAoODK/fhNG+96x44dyq727t13q0V2afvzQeDIJ9fh2L17T3x83K2Wenp6NmzYUOUZGzZskOdmzZq7uHAdFAD29B+ut5GUlLR69ZrTp0+fOXPGx8e3XLmAli1blCtXTt2mnHed++ttPPFEz6JF/ZTpT+umAwcOZF+hUqVKHTu2l4mrV2PmzJmbu62q8+fP32rRbX1o165dCwkJkYkWLVqoO2DdK6PYRT5p8F57bbT2f/SmKlSo8OefG5WVHD9+PCrqQnBwUMmSJdXtS0tL69mzl0wcOLC3WLFiCgDuHtu2bXv11RFRUVFZ5nfr1m3cuHfd3NyUzcl+PTw8ZOKRRx5p0+a+7Ct89NEE7Ttnampq7gNHDj0+t9X0S9eMdq7NsWNH/vN1z253p3lTvvqGLUG7RImbhICyZcso65ky5ev58394//33+vXrq+5APruEHIB8T770Dxw4KCEhITCw/JNPPlm8ePELFy78+eefGzZsXLx4sTI17R8q+6lcuZIkj+vXr1vOdJQvzxgAABAASURBVHR0rFu3rkIekK8Cx8CBz91hCLABZ2fniIgwSatWv34cABhq9uw5kjb8/PxWrPhV/wvWv3+/adO++fDDiZI5hg0bWqaMNb/g3Rb5Fte3b58pU6ZazuzWrYv81VXIAwrc/wYJv/v3Hzhy5EhGRmZwcAWps3l6eupLT506HR8fV7FiRSl8HTx4SFaTCF+zZs0SJfyVqRyXduTI4cuXL8t0RESkdBZK16C7u/tNd3T48JFDhw7JyuXLl69evVrZsmX1RRER4fIcGBgo0fvcuXOXLl266RaqVq2mD/KQSuAxk1CpWDZo0EA7HgCwJa3nWhJGlu9LvXs/JYFDmXoNjtkrcKSkpLi6uj75ZM8sgaNfv376UgW7KliBQ0LAs88OPHXqlD6nZMmSX375ebNmTbWXI0eO2rz5z8mTv/j4408tV5s1a0anTh2joy907Hi/Nmfq1K/lsXr1yho1amTZS1pa2rBhLy9YsNBy5ptvjh406HkJ4LK0RYvW6q8xHF99NWXGjFk3Pdrt27cEBAQo07/h0P79B1geT7t2902e/KW3d2EFALaiXepKviZlmS8dGfPmzbly5WrlypWVnWzfvqNlyxZBQUFeXl5ShtFmype6OnVq60sV7KoABY6oqAv33WcaqCxVje7dH0lPT1++/NctW7Y++uhjW7ZsDgwsr685ePD/5Pm5554tUqTIwoWLQkND+/V75tSp497e3iNGvCrvkuDStm2bBg3q+/vfpNIwZcrXkjaKFi3av39fKWNIpWTy5K/efXdcvXr1mjRpnGXlOnXqdO/+qOWcX35ZoE0UKmS6uV14eMS997aVifr163Xt2kXKIT//vGDNmj8eeqjr77+vuFV9BQCsrn79+jt37po7d57Ubrt27dqwYQNfX19tUaNGjZRdHTx4sFGjhlIDHjDgmc8++1ybKX9dpT9FitPbtm0ncNhdvgoc778//osvJmeZWblypfnzTcOSP/10kjKdjNpUXmq1taee6tW3b/+1a9eNGTN29uzv9LdIS79s2ZIqVUxRvXfvXtWr15KJkydPSjHjpZeGRUaekcAhNYZbjRdZvXqNPA8f/srTT/dWph7Erj4+Pu+//8GyZcuzBw7592AZOKTaoQWOWbOmS9xRpgHY78tzly4PSd1F64mU/bZv30likOzooYceVABgE88/P3DTps3SWbxy5e/ykDnBwcFNmza59957W7RofienYNw52bscm/xlfvzxHnrg6NPnaWW6itdOhTwgX134Kz4+/nw2586ZzqXOyMj4/vvZytzA6z150g2phQYpGFheU0VqG1raENLqS2lBJk6cOJnLw0hNNd0jYM6cefpp3AMG9N+0af3gwYNyfuOGDRveeONNmRg1amSnTp2U6azxq9rl9qRHRh/3VKJECemnlIlFixYrALAV+TL2ww/zxo17r2XLltqcEydOzJ4955lnBrRu3WbJkiXKrqSHWpmGx5WX73jKHEG0/pQZM2Yq5AH5qsLx6qsvd+/ePctM7bzw6Oho7aXU3CyX1qhRXZuIiooqVaqUNh0cHGS5TunSpXfv3pOQkKhy56mnntq3b39ISEi9etJ707BVq5atW7esW7duziOlpYIyYMBAZTqVvNvQoUO0mWFh4dqEdNNkWdn8fEoBgA15eHhICUEe169fP3LkyJ49e9etW799+3b5E/rKK8OLFi3WvHkzZSdbt25LTEz09PQcOPDZDz/8SA7S0dExNTV1xYrfqlSpomBv+SpwSDWiXLmAmy46e/bGKKcKFSpYztfGSSjz2Sv6zMKF72gwZq9eTxQrVvS7776XzpqdZh9//Ikc2IwZ06tXr3bTt1y7dq1Xr95SoZE8/tFHE/VLdJw9e1abmDnzJgNLk5OTFADYRHJyclxcnLOzi6+vqX4gyaOe2YABzxw/frxz5wdk5oIFC+wYOMTGjRulPCyVbAkcffqYOrW3b7fzJdKhKyiDRrXxEOLMmTOWZ6gePHhIm9DLG1bRoUN7eVy6dGnHjp2rVq3+8cefwsMjpOq4bduW7CunpaU9//wLskLRokVnzZqpXTJPI8FFm5A3enl5ZnkjV/IAYDPy3alv3/5eXl67d+/MUq+tWLHi00/3lm7rHK74bBtffDFZAof8kff3L66dQvj119MU8oaCcvM2/dTwLVu2Ws7fs2ePPEtLb9nM3wkJGQMHDpLSorbZ++/vPGnSJ4sWmcaBSqSQSkb2t7zzznvr15vurjJ79ndZLrBRrlx5bUK+QBS1IDlm9Og3Fy5cpADAJoKDg+U5ISFh79692Zdqwya0M/ntSLp4pFQsE/Pnz5P+lJSUlDVr/lDIGwpK4HBxce7S5SFl+i38Ue89uXLlyty582Vi0KCBud+U1t8h773pUh8f36VLl82bN3/nzl36TO1aYcqiB0cnB/DNN9OVaZTGZG18kyUJ6dotAH744UftDHhlvrrwyJGvyV7usOsHAHKvRIkS2mj6ESNGyVcgy0Vbt27Tun3r1Kmj7O2PP9Yq04UTTYM2tm7dqvIMST/J2djxbr22V4Cuw/Haa6Okkd6+fXunTg9I4UH+3//6669SdShZsqR2/mouaeNMP/rok2PHQseOfbN06dKWSyXZ9O/fT/7tdenS7YUXBpUtW0Z6bZYsWarM56o4OTnpuUHExsa9+upwbVrqHPKw3JSURlq1ajl+/Pv33df+119XdOv2yL333hsTc/XHH3+WCC9BpFu3rgoAbGXSpE87dbo/MjKyc+cHGjZsUKVKlaSk5GPHjh44YOpJqV69unYaqn1Jr0rXrl206SlT8lB/Ss2atbPPfPzxHuPGvacKhnwVOHK+HVr58uXWrFn13HPPh5ppM1u0aD516ld64UE7j1wKcZZv1F46Ot7Y+H33tZVODek6WbZs+dChQ7IEDmU6qXWEdJ0sWLBQO0dL06vXk6+//lqWzWZk/J1ts98KOSnJNCa0WrWqv/326/PPD9q9e488tEVDhrwwZMhgbhAAwJakV2XlyhUffDB+w4aNUsS1rOP269dXvmJ5e3srm9Pu266XCg4dOhQbGytHIvWDDRs2aDO1Cx/Y5Q7vDg6OOS4tQHfxdIiPT1A2FxMjvw2FChcupGxOCgynT58ODTVdNrRy5crFi/+XG8SnpqadO3fOzc3V39//Vr8uYWHh4eFhFy9eKlasqPwrzZ5LbvewT548JWVMLy+vatWqSVeLAoA7lvsT/i2dPXtW6hxnz57z8vKUrpZKlSpZ3pQq97KPhdeVLGm3m8BpRo4cPmzYizddlJqampKSqmzL1dXFKicKxMbGx8XF+/raIRoWuK/IUhWoaKbugPSb3Or8W50UVOShrEQOu3LlSvJQAGA98pXpP3zvL22m7kzOX+6lPzoy8oyyH+ltv9Uiu5Ql8kEtpKAMGgUAZGfH65HnvGu735ylbt1bDoDN0u1uG3bZqXUROACg4HJ2tlvgyHnX9r1RVMOGDaSf6FZLpe13crJp6ym7I3AAAO5iTiZ2aAhkpzlXODp27JD9bpc28/zz/3KtBGdnm1530ca7MwiBAwAKNP1+lnltp++885ayh969n7r//s45ryPlGZudJ+hsYs878VoLgQMACjSp1bu7uykbkt3lpoOgZs2a335r6xu9duvW9cMPx+dmTTc3VxvkANmF7EjlCwQOACjopHfDw8PdBn0rsgvzjnLbTkvHyqpVvzVp0kTZxMiRw6dMmZz79d0kC7ga2NkhG9dueJ4/FLjrcAAAbiU9PT0tTZ7SrXuNLAcHBwkZ8mX9P58U8/vvq5YtW75jxw6rnytrvrhR1bZt2/To8dh/u4unfFZp5k8tIyPjzj82BwdtUKqpy8aIU2HteB0OAgcAAAWFHQMHXSoAAMBwBA4AAGA4AgcAADAcgQMAABiOwAEAAAxH4AAAAIYjcAAAAMMROAAAgOEIHAAAwHAEDgAAYDgCBwAAMByBAwAAGI7AAQAADEfgAAAAhiNwAAAAwxE4AACA4QgcAADAcM7qLpeUlCwUAAD5l5ubm7u7m7qb3fWBQ5n/NygAAPIvBwd1t7vrA8fdnvgAACgIGMMBAAAMR+AAAACGI3AAAADDETgAAIDhCBwAAMBwBA4AAGA4AgcAADAcgQMAABiOwAEAAAxH4AAAAIYjcAAAAMMROAAAgOEIHAAAwHAEDgAAYDgCBwAAMByBAwAAGM5ZAQCAW4u4mB56Jv3clfS4xMyMTGUVjg6qsKdDqSJOlco4BRRzUgUAgQMAgJs4FJ62dHvyH3tTJHAoI0nguK+ua5fGbtXL5edG2SE+PkHZXExMrLd3ocKFCykAAPIYiRpTfk1ctSdF2VaHeq6DHvA0NHbExsbHxcX7+norm6PCAQDA375clvj5kkRlAOlDqRXoXL64k28h0wDKmPiMsOj0A6fTpKdGW0EijjyGdvUc8pCnyncIHAAAmKRnqBenxhpR2HigkdvDzdxa1XC96dKNB1MWbUn+dUey9lLiztHItM+e93bKX+d10KUCAIApbfT/9NrWI6nKqlpUd3npYa+a5f/9631IWNqnixI2H7pxAE2rusx8ycfqmcOOXSqcFgsAgKm2YfW0MfghTwkNuUkbQlaTlQf/1ZkiByOHpPIRAgcAoKD7cpn1h4iO7un1YtfbHoohb5E3atNySHJgKr8gcAAACrRD4WlWHyUqhYo+7TzUfyJv1OsccmByeCpfcHr99dHK5pKSkt3cXOWhrCEzM3Pjxo3h4eGxsbH+/v7ZV9i4cVNYWJgscnFxUf9JeHjE/v370tLSihQpos2Jirrw+++r1qz5w8+vSOHC3ps3b4qMPFOuXIAyRvYDAABYxTvz4k+et+aVNlpUd/mgb2F1B5pUcdl7MjXiYoZMX43PuL+hm7KS5OQU4e5utQ3mXn44SyU9Pb1nz17a9JYtmwMDy1sulTjy+ONPyMSmTeuDg4NVLiQnJ+/YsVPSSZMmjbU5ixcvHj/+wwED+r/77jvy8syZM23atIuPj5fpoKAKEji0Azh//oyykp07dyUlJdWvX8/T0zP7AQAArELqB1bvTHnpYS91x2Qjmw/FKHPHihxkPrgmWH7rUnnttdfVHZPqRY8ePR9++FGLeQ6m/xxufFx//rlF0kahQoVGjHi1Vq2aDg4Oytr69x8gx3D6dNhNDwAAYBVLtycrq3qgkVsuR4nmTDYim9KmrX6QdpHfGrANGzYuXrxEWduQIS9ERJweO/ZN7eXp06fluWfPHi+9NKxs2bLFixeTpRERYcowWQ4AAGAVf+y1cnnj4WZW663QN2X1g7SLfHXhr4YNG+7cuXP48JFt2tzr4+OT88pSPDh48GB4eETJkiVq166l97acPXv2yJEj2vSBAwe8vX3Kly8XFxd3+fJl2WaRIkUkbVy+fEWWpqSknjx5skSJEu7u7hEREY6OToGB5fXtJyQk7N695+jRo1KWkG6XVq1aOjv/49OWjhtZ4dixY9WrV6tZs6a1OG6tAAAQAElEQVSHx43hRdeuXQsPD5f+FJmWpfIsK+gHULRoUX0LMmf//gOhoaFSa6lcubL0vzg6/p0gQ0JCpPRSo0YN2aCsJtsMCgqqWbNG4cL/6FmMioqS7hvpJJIfpFKlSrIvBQAFQ8TFdOveJ6Wwp8Otru71H8imZINxiZnacd7t93jLV4HjzTdHP/nkU9LZ8f774ydM+OBWq6WlpX3yyaRPP51kObN//36vvz7Ky8tr0qTP58yZq83s2PH+zp07zZw5/bvvvteGULzyysstWrTWln7//Wx5TJ8+rUGDBtpMfQyHNPD9+j1z/vx5ffsVKlT44Ye5Ug5R5qgxbNjLWSoxrVu3mjr1K19f37Vr173wwhBt5uDB/9M2qx+APoZj+fJfX3rpFW0ciaZFi+YffzwxICBA+xk7dOgsE3Pnzu7Vq7e+juSVJUsWVagQqL385psZY8aMtTyMRx7pNmHCeEkwCgDyu9AzVr4rW61AK7eqssE/zZcCk0O92wNHvupSKVLknk8++UiZo4B8a7/Vah98MEFLG0OHDvnyy88HD35BpmfOnDV27NvKFDI6PPfcs9qaI0a8+vDD3SzfK3UImSmlFGVu4GW6UqWKWbYvOaNTp/vlWeoWkoGefrq3tN+nTp0aNGiwtsLEiR9L2pCZ0kUyefIXAwc+J9PSGTRunCkkValSRTarNfny3jfeuMmolNWr1zz77EBJG927P/r555NGjRpRrlzA5s1/PvHEUykp/6i8SdqQkDF8+Csvvvg/2ealS5eGDx+hLZJqkJY25ACmTJksByNrLly4ePr0GQoACoBzV6wcOMoXt3Im0Ddo9UO1vfx2L5WHHnqwXbv71qz549VXR6xZ83v282ClD+Wrr6bIxLx5c6TnRZspE92795g7d16vXk/K2ytWrDht2jcy/6WXhmV5u5ubm8yUEoW01i1atJBWXGZeuBBtuc5HH30iz9WqVV2yZKHWUdK3b5+2bdtJB4r0XJQpU2bdunUyc/r0r1u3NtVFHnnkYYkLr7/+xo4dO+Vl1apV5DFz5reSJyRwZO/jkOrFW2+Z6hwSmF57bZQ28/HHe7Rv30lizaxZ30qA0FeuU6f2/PlzpXAi002bNunZs9eWLVszMzOlt2Xjxk0ys0uXh956a4y2cv369bt06fbzzwuGDXtRAUB+p981zVq0u7IZsUGrH6rt5cOzHj74YJw8h4aGaqEhi5UrVypzz8K997bWZzZv3kyqEcr8pV/dmYyMjHnz5suEtNn6sAwJEC+8MEh6Z7TBH8OHvyq9J82bN9ffpdUzkpOTcrOLgwcPSbCQiR49HtNnlihRomfPHsp0YvBWy5UlkWhpQ5nHuGgTln09S5cu27t3nzbdoEH9TZvWf//9LAUAgFXlw7vFSgnhnXfels6C9957/8EHHwwIKGu5VDvRtHXrVlnOZa1Xr25ISMjJk6fUnYmOvlHtqFKlsuV86VvRpzt16piamrZq1eqtW7dGRESeOHFSCxC5FB4ersyZqUKFCpbza9Soof4aZ6oLCvp7HU9PT0k2UjhJTDRdU086j7RizP33PyibatNGCi6tJXtpV/4AgHyvsKeVL2oQE5+hrErfoNUP1fby53Ud+vXro1Ussl9HNTIyUp5r166dZb6Pj+nWeQkJd3rv3LNnz2kT5cuXv9U6s2fPqVevwTPPPDt9+kyJHbGxsW3btlG3sYuzylyNyJKZvL1NP4JWRNFlOSfFkgSUNWtWPfmk6apoknhmzJj19NN9a9eut3DhIgUABUCpIlYechEWbeWRFvoGrX6otpc/A4ezs/Mnn0yUibVr10mXgeWiYsWKyfPRo0ezvGXXrt3yrJ3icSfuuecebSIiIsJy/tWrV8+dO5ecnCx1lBEjRl26dKlPn6eXLl104sSxkJB9I0cOV7nm5+enzCfCZJm/f/9+ea5cuZLKterVq3388cRTp47//POPQ4a8IFUTqX8MHvw/OVQFAPldpTJWbsUPnLbyfU/0DVr9UG0v3165Ur6+SwsqE2+8McZyfmBgeXmWuoLlTOng0IY+6OeL/mdly5bRJrZv36HPvH79eqtWberXbySZQDt9pmHDhuPHvy/PXl6mK+CGhBxUuVa+fDllHodhcR1SE23LlSrlNnCMHv3mwIGDZDseHh4tWjQfPfr1zZs3aKNJDh8+ogAgvwso5mTdc03jEjM3HrTaRbpkU9pYUasfp13k50tlv/zySyVLlpRaguXM9u3by7PMXLPmD33mnDlzlHnkZqdOHWXC0fFGV8V/6GFxcXHRxnL+8MNPkjO0md999712GHXr1tX6QU6fPq2fv3rq1Okvvvgy+3aUuS6SfRe1atXSLv81d+48fea+ffulnKNM58E+qXJHulGk/LNgwd8dKPFmMlG8eDEFAAXAfXWtdp0uzaItVrsMub4pqx+kXeTDQaM6+eL+6acf6fd101SrVvXpp3t///3s3r37PP54j8DAwEOHDi1btlyZBnyM0r7fS0zRBld2797j4Ye76ZflyKXhw1/56aefd+7c2a5dxzZtWq9bt0EbEzpu3LsSI5o1a6bMiefBB7u2a3efdO789ttKbb/h4RHjx0948cWhcuR16tSW2sPQocNatWo5adInWX6ud999e9CgwZMnfyUH36RJ48jIM0uWLJVFnTt3ql+/Xi6PU6LJ+vUbxo17/9ixY/Xq1T179tzy5cu1j6hqVa43CqBA6NLYbdaq68p6ft2R3L+Dx53fTiUkLE02pU3LQaq7X76qcDg6Zq04tW7dunv3G/dg04dYvvfeu9p1Jn788Sdp4LW08eWXn/fr11dbwdnZuV+/PspcNti2bbv6665p+va1Cf064npFRFOmTBnpm5CuDW0kpjxLnnjrrTHa9itXrvTppx8r86XHP/10kqSN1q1bbdq0QdKPzPzssy+0ssoTT/RU5n4TOcjsB9CtW9dp06bKZiUxjB//oZQ6JB4NHPjc119PzfIJ3PTGctrMBx98QLuMxy+/LHj99Tckvkjiady48cyZM1xc8nMSBQBd9XLOHepZuX7w6aI7Pf/AciNyePngVrHCIT7eCp/L7YqJifX2LlS4sD2vnx0TE3PkyFGpNAQFBQUHB7m6Zv2Fk+6MmJhr/v7F//NpolFRUcePn/Dz8wsMLK9fk0Nz+fLlQ4cOS8Nfu3Ztb+8bJ5KEhYWnp6dVqFBBCwSJiYkXLkT7+vroA1GzkC6b0NDjYWFhEnEqVaqYwwkpOZBP4MSJk2fPnpW3BwQEZDmbFwDyvUPhaQ+/G6OsavBDni92/e+XGPhsSeLkZYna9KI3fa0YOGJj4+Pi4n19vZXNFdzAAQCA5stliZ8vSVRWNbqnV592Hrf/PvXdmuvjfrjRNA/t6jnkIWteG8mOgSM/DxoFACA3pFG3eseKhIbPbj/EyFv0tCGHZN20YV8EDgAA1GfPezet6qKsSrpF+n96LSQsVxfnkNVkZb0nRQ5GDknlI3SpAABgkp6hXpwau2qP1S6koXugkdvDzdxa1bh5EWXjwZRFW5L1c1KUubYhacPJgJoAYzgAAMgTjBjPoSns6VAr0Ll8cSftHrAx8Rlh0ekHTqdluROs1cdtWCJwAACQVxwKT5vya6IRpY6cSWFj0AOehp4ES+AAACBvkdixdHvyH3tTIi5a+ZZsWQQUc7qvrmuXxm42uN4GgQMAgDxKAkfomfRzV9Kl7yMjU1mFo4Oph6VUEadKZWx6nxQ7Bg4uKAkAQE7yx73T7I7TYgEAgOEIHAAAwHAEDgAAYDgCBwAAMByBAwAAGI7AAQAADEfgAAAAhiNwAAAAwxE4AACA4ex2pdFMK10dFgAA5H32CRwODg6ZmRkKAADYkDS+0gQre7BP4HB0dExLI3AAAGBT0vhKE6zswT6Bw8lJAkeqAgAANiSNrzTByh7ss1cnJ6e0tHR5KAAAYBNayytNsLIHO9VVnE2VlaSkJAUAAGxCa3a1Jtj27FXhcHR2dkpMvK4AAIBNSLMrjW/B6lIRrq6uqalp169T5AAAwHDS4EqzK42vshM7Bg4XSVlxcfEKAAAYTBpcaXal8VV2Ys8rjbq7m4ocsbFkDgAADCRNrTS40uwq+7Fn4HBxcZHajmQuOlYAADCINLLS1EqDK82ush8730vF09Pd2dnp6tWY5OQUBQAArEqaV2lkpamVBlfZlf1v3ubp6eno6HT58hXqHAAAWJE0rNK8SiMrTa2yN/sHDkdHBy8vTycnpytXYhjPAQCAVUiTKg2rNK/SyEpTq+zNIT4+QeUNiYlJKSkpLi7OhQsX8vCwc+UHAIC7lDZoQzsJ1u49Kbo8FDhEampqUlJKenq6xA5PTw93d9MIDwUAAP5NWlp6UlJSYuJ1iRpS2HB3t/Mo0SzyVuDQpKSkSqlDu9OKBA5nZxdnZ0cHB0cH+xeEAADIQzIzTXecT0uTR6rebrqa5KGoocmLgUOTni4fX1q6SYbIlA8VAAD8k4N8I3d0dHKSh3xFd7bXlcv/lX3u4JIb5s/OnpcoAQAA1pJHcxAAAMhPCBwAAMBwBA4AAGA4AgcAADAcgQMAABiOwAEAAAxH4AAAAIYjcAAAAMMROAAAgOEIHAAAwHAEDgAAYDgCBwAAMByBAwAAGI7AAQAADEfgAAAAhiNwAAAAwxE4AACA4QgcAADAcAQOAABgOAIHAAAwHIHD+lJT09LT0xUAAPiLs4JVJSenLF++2sencLt2rZTBEhIST5+OvHz5SlpaWuHChYKCyvv53XOrlS9fvhoZefbKlRhnZ2dfX+8qVYJdXV0VAAA2QeC4W0l02LBha0ZGhvYyJiY2MvJc9eqVJUlkX/nUqYi9e0P0lxcvXj5xIqxt2xaSPBQAAMYjcNyttm/fI2kjOLh8zZpVpQfn2LGT8jh06Fi5cmU8PNyzrLxv30EHB4dGjeqUKVMqMfH6tm27r169tnv3/vvua6kAADCePQOHfO2WIv/VqzGOjo733ONbtmyp8uXL6ksjIs4ePHgsKSlJpgsV8qpfv5bWX7BmzUY3NzdpaI8cOX7tWpy3d6GqVSv6+vrs2LE3JuaabMrfv1ijRnWlfdW2Exsbt2dPiKwp0/KFXmoARYsWuenxrFv3p3SItG3bXOtrkHdFR18KDAyoXDlIXl6/niQVBTmGhg3r5Gaz0vafPh2RkpLq5uZasWKFSpUqqNv0xx+bUlPTWrRoJD++Nic+PmHz5h2enh4yU3KDdI7Url1d5stPXaNGlZMnw6Vv5cKFi5YfozLXQjIzM+XIJW3IS3l7vXq1ZOOyBQUAgE3YbdDo4cOhUuS/dOmKi4uLtJfStO/efeDkyTBt6dGjJ3bu3Hf9+nVpreVlXFy8NPZaAxkbGy89Alu27JLv6NKOSlfCtm17Vq1af/nyVaUcpIU+c+a8bErbjrS+a9ZskkVSA3B0dJDdbdy4TboebnpIsrWEhMTz56O1lxERZ+RlWFik9vLs2Sh56eTklJvNVN+Z+AAACQRJREFUShCRHyE9PUOyUVJSckjIkf37D6vbJB+L7FFimT5HIoXMkc9Ekoc8lyhR/KbvyjLHyckxKKi8xDJ9joQ8eS5S5JYDPgAAsC67VTi0hrx166ZaYUCaZykJSJstTaNUBSSOyMxmzRqULOmfkZG5ceNWad2PHz9du3Y1ZU4G8n29adMG0rjKN3VpgzMyVKtWTYoV85NYIP0F0dGXtb3s2LFPVpbqglQgZGXZqWQRqUxINSX7IZUuXUJCTFRUtPRKSIsucUGZB2bKAUiqOH/+grwMCCidy81KmUGqMsqcVOSQJEtVqRKs5SdLskdZJEWUwoULZVkUHBwoZZszZ87VqlVVmyMFIXmuVCnI27vwgw+2t1xZPhwpb0hdp3jxolm24+PjXadOde1nOXbshIQhqXnImlrlBgAAG7BbhUNaa2kF9W4IbdiBNJnKXFqQ5lyaSUkbpkN0dKhTp4Y0sXoviTJnEWm8XVyctfZVsoKkDW1CVtM6YqQOkZKSIjWJmjWrat/7JQS4u7vLXqTFleKElEz0R0ZGRtmypjAh5Qp5PnfOFC9kZTmSixcvKfNZHrJlOeCcN6sdniytV6+mNi2HJMcp29G2nIUUcsLDz+zdezD7ojJlSsr2pStHK+1ImUd6fOSnvuceH8vVZL9bt+46cMBUQalWrZK7u5u6BekGOn06UjtIV1cXeSgAAGzCbhUO+fouPSObNm2XVlBKGvrZFsrcaSLPls2qr693+/Z/n2UqDb9+SqfWauqjHCxduxarzOWQlSvX6TMlK8izNOHS/EtPhz6/ceN60sDLZqUHRJpwqXPIzJo1q0ggkNqGl5enBBQtHuW8WVlTOx7LeFS0qJ9sRBJD9oMsV6700aMntcJJFrKFkiWLS4Hk9OkIKaVo/U2BgQGW60iHy4EDh6QYI9Gkbt0aWUZvZOHvX6xt2xbR0RfDw8/Kwaxdu7lbt84KAADj2S1wrF69UaKGTMhXdskTPj6F5cu3tkirT3h6etzqvdpACkvZBy4o0zDPZG3CMs1onRrOzk6enu6WZ3No8/39i0q3zoULl6SnQ5aWLl1SAoe89PAwHYw26DLnzf41I/OfB2w6PIsE8rfKlYPloW6hYsUKEjgiIs5K4IiMPK/MQU3f++bNOyS0aQdWr14NF5ebVyykLpKamiqfp3lwro88AgPLLV++WmKKlG1yuHQHAADWYp/AIV/3JW3IN/iOHe/VSgLS8umBQytXSFOqj3OMj0/Yv/+wdJrc1rke3t6mURHS0Mpe9JmyKalhSKMrqUULEJak0iCB49Qp0+ke0hUiQUEOLyEh8dy5KFlatmzJf91sWprpGqMJCf84AeTSpavmNxZWt0nSgJRwpHCi9eNIMtNHgezYsVc+IlnavHmjIkV8c9jItm27pZxTu3Y1PazIuyROyWblRyNwAABswD5jOKR5VuaOEi1tKNOYx1P6Uqn8K/PJnBkZN+oE+/cfkj4OrfKRe1oPiOwrNTVNmyMxQiorGzZsvdWlx2XXEoOio02DNrQRJDJHek/kYKSF1vpxcrNZmaMNMlWmgk2yHLl5/Iefun0BAWWUKTTsURblDdmRVD5k4t57m900bSQnJ1+7Fqd19Pj6mjqnLE+ikf4UbVwIaQMAYBv2qXD4+ZnabOm22Lv3oDTkZ86c1wZGSGFAgkWJEsWlyCEt+qpV66XkIHNkTWmwg4LK385OVOHChUqUKBYVdfH339dVqhQkuSE09KT0REiSuNVVvWUvEoNkdzIt71XmIZ9S8FB/5Y9/3az0X2irbd26u0yZklIIOXnS9PZy5cq4uPyXT7tixcATJ26cgaIP9dAikczZsmVXlvXr1KkuIWnXrgPyuQUGBtSrV7NKlaCTJ8MkM61du1k+WykmaR0xkjb0wAcAgKHsU+GQL+XSjkp7KW35oUPHpHulQYPa0tJLs7pv3yFZoU2bZtJzIQX/I0eOS/Mv3R9NmtT7D61j48b1pPWVEBAScuTgwaMpKally5aSmTm8RRKGMp0146GNFJF+HG34p+W4zn/drPSeSLeFFBWOHTspP1SpUiX0k1Zul0QWrY9JAo0+EPXCBVPgkKwjsSzL4/p1Ux3Icsiqm5tb06b1pXtIPkn5PCWsyBuLFy/avHlDBQCATThovRt2Ic2e9DtIg6rV/JXphiDXpEigDxeVzgj5Xi4vfXz+cU7s7dLOSdFOas1+2W/jNiufrfRrSGTJCyegpqdnSBlJsp15lK6PFT8HAAD+lT0DBwAAKCDsduEvAABQcBA4AACA4QgcAADAcAQOAABgOAIHAAAwHIEDAAAYjsABAAAMR+AAAACGI3AAAADDETgAAIDhCBwAAMBwBA4AAGA4AgcAADAcgQMAABiOwAEAAAxH4AAAAIYjcAAAAMMROAAAgOEIHAAAwHAEDgAAYDgCBwAAMByBAwAAGI7AAQAADEfgAAAAhiNwAAAAwxE4AACA4QgcAADAcAQOAABgOAIHAAAwHIEDAAAYjsABAAAMR+AAAACGI3AAAADDETgAAIDhCBwAAMBwBA4AAGA4AgcAADAcgQMAABiOwAEAAAxH4AAAAIYjcAAAAMMROAAAgOEIHAAAwHAEDgAAYDgCBwAAMByBAwAAGI7AAQAADEfgAAAAhiNwAAAAwxE4AACA4QgcAADAcAQOAABgOAIHAAAwHIEDAAAYjsABAAAMR+AAAACGI3AAAADDETgAAIDhCBwAAMBwBA4AAGA4AgcAADAcgQMAABiOwAEAAAxH4AAAAIYjcAAAAMMROAAAgOEIHAAAwHAEDgAAYDgCBwAAMByBAwAAGI7AAQAADEfgAAAAhiNwAAAAwxE4AACA4QgcAADAcAQOAABgOAIHAAAwHIEDAAAYjsABAAAMR+AAAACGI3AAAADDETgAAIDhCBwAAMBwBA4AAGA4AgcAADAcgQMAABiOwAEAAAxH4AAAAIYjcAAAAMMROAAAgOEIHAAAwHAEDgAAYDgCBwAAMByBAwAAGI7AAQAADEfgAAAAhiNwAAAAwxE4AACA4QgcAADAcAQOAABgOAIHAAAwHIEDAAAYjsABAAAMR+AAAACG+z8AAAD//1aW6CAAAAAGSURBVAMAjFgjFaA2jm8AAAAASUVORK5CYII=";
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// src/server/demo-visuals.ts
|
|
447
|
+
function signupsChart() {
|
|
448
|
+
const max = Math.max(...SIGNUPS);
|
|
449
|
+
const bars = SIGNUPS.map((value, i) => {
|
|
450
|
+
const h = Math.round(value / max * 128);
|
|
451
|
+
const x = 26 + i * 52;
|
|
452
|
+
const y = 196 - h;
|
|
453
|
+
const tone = value === max ? "c-purple" : "c-teal";
|
|
454
|
+
return `<g class="${tone}"><rect x="${x}" y="${y}" width="38" height="${h}" rx="6"/></g><text class="ts" x="${x + 19}" y="${y - 8}" text-anchor="middle">${(value / 1e3).toFixed(1)}k</text><text class="ts" x="${x + 19}" y="216" text-anchor="middle">${DAYS[i]}</text>`;
|
|
455
|
+
}).join("");
|
|
456
|
+
return `<svg width="100%" viewBox="0 0 400 228" role="img"><title>Signups by day, last week</title><text class="th" x="16" y="24">Signups by day \xB7 last week</text><text class="ts" x="16" y="42">12,480 in total, 18% more than the week before</text><line x1="16" y1="196.5" x2="384" y2="196.5" style="stroke:var(--line)"/>${bars}</svg>`;
|
|
457
|
+
}
|
|
458
|
+
var DAYS, SIGNUPS, SIGNUPS_CSV, REPORT_HTML;
|
|
459
|
+
var init_demo_visuals = __esm({
|
|
460
|
+
"src/server/demo-visuals.ts"() {
|
|
461
|
+
"use strict";
|
|
462
|
+
DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
|
463
|
+
SIGNUPS = [1420, 1588, 1710, 2214, 1966, 1702, 1880];
|
|
464
|
+
SIGNUPS_CSV = `day,signups
|
|
465
|
+
${DAYS.map((d, i) => `${d},${SIGNUPS[i]}`).join("\n")}
|
|
466
|
+
`;
|
|
467
|
+
REPORT_HTML = `<!doctype html>
|
|
468
|
+
<html lang="en">
|
|
469
|
+
<head>
|
|
470
|
+
<meta charset="utf-8">
|
|
471
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
472
|
+
<title>Signups \xB7 week 45</title>
|
|
473
|
+
<style>
|
|
474
|
+
:root { color-scheme: light dark; --ink: #17181a; --soft: #6b6e75; --line: #e6e6e3; --card: #fff; --bg: #f6f6f3; --a: #0e7490; --b: #7c6cf2; }
|
|
475
|
+
@media (prefers-color-scheme: dark) { :root { --ink: #ececea; --soft: #a5a8ae; --line: #2a2d31; --card: #16181b; --bg: #0e0f11; --a: #3ba7bd; --b: #9d90f5; } }
|
|
476
|
+
* { box-sizing: border-box; margin: 0; }
|
|
477
|
+
body { background: var(--bg); color: var(--ink); font: 15px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; padding: 28px 20px 40px; }
|
|
478
|
+
main { max-width: 640px; margin: 0 auto; }
|
|
479
|
+
.eyebrow { color: var(--a); font-weight: 600; font-size: 13px; letter-spacing: .02em; }
|
|
480
|
+
h1 { margin-top: 6px; font-size: 30px; line-height: 1.1; letter-spacing: -.03em; }
|
|
481
|
+
.kpis { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 22px; }
|
|
482
|
+
.kpi { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 14px; }
|
|
483
|
+
.kpi b { display: block; font-size: 24px; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
|
|
484
|
+
.kpi span { color: var(--soft); font-size: 12.5px; }
|
|
485
|
+
.up { color: #16a34a; }
|
|
486
|
+
.chart { margin-top: 14px; background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 16px 14px 10px; }
|
|
487
|
+
.bars { display: flex; align-items: flex-end; gap: 8px; height: 170px; }
|
|
488
|
+
.bar { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; gap: 6px; height: 100%; }
|
|
489
|
+
.bar i { display: block; width: 100%; border-radius: 8px 8px 4px 4px; background: color-mix(in srgb, var(--a) 75%, transparent); height: 0; transition: height .9s cubic-bezier(.2,.8,.2,1); }
|
|
490
|
+
.bar.top i { background: var(--b); }
|
|
491
|
+
.bar small { color: var(--soft); font-size: 11.5px; }
|
|
492
|
+
p.note { margin-top: 16px; color: var(--soft); }
|
|
493
|
+
</style>
|
|
494
|
+
</head>
|
|
495
|
+
<body>
|
|
496
|
+
<main>
|
|
497
|
+
<div class="eyebrow">Growth \xB7 week 45</div>
|
|
498
|
+
<h1>Signups grew 18%, and Thursday led the week</h1>
|
|
499
|
+
<div class="kpis">
|
|
500
|
+
<div class="kpi"><b data-count="12480">0</b><span>signups</span></div>
|
|
501
|
+
<div class="kpi"><b class="up">+18%</b><span>on week 44</span></div>
|
|
502
|
+
<div class="kpi"><b>Thu</b><span>best day, 2,214</span></div>
|
|
503
|
+
</div>
|
|
504
|
+
<div class="chart"><div class="bars" id="bars"></div></div>
|
|
505
|
+
<p class="note">Thursday's newsletter brought the peak; the weekend held above the weekday average.</p>
|
|
506
|
+
</main>
|
|
507
|
+
<script>
|
|
508
|
+
const days = ${JSON.stringify(DAYS)};
|
|
509
|
+
const values = ${JSON.stringify(SIGNUPS)};
|
|
510
|
+
const max = Math.max(...values);
|
|
511
|
+
const bars = document.getElementById('bars');
|
|
512
|
+
values.forEach((v, i) => {
|
|
513
|
+
const bar = document.createElement('div');
|
|
514
|
+
bar.className = 'bar' + (v === max ? ' top' : '');
|
|
515
|
+
bar.innerHTML = '<small>' + v.toLocaleString() + '</small><i></i><small>' + days[i] + '</small>';
|
|
516
|
+
bars.append(bar);
|
|
517
|
+
requestAnimationFrame(() => requestAnimationFrame(() => { bar.querySelector('i').style.height = (v / max * 120) + 'px'; }));
|
|
518
|
+
});
|
|
519
|
+
for (const el of document.querySelectorAll('[data-count]')) {
|
|
520
|
+
const end = Number(el.dataset.count); const start = performance.now();
|
|
521
|
+
const step = (t) => { const k = Math.min(1, (t - start) / 900); el.textContent = Math.round(end * (1 - Math.pow(1 - k, 3))).toLocaleString(); if (k < 1) requestAnimationFrame(step); };
|
|
522
|
+
requestAnimationFrame(step);
|
|
523
|
+
}
|
|
524
|
+
</script>
|
|
525
|
+
</body>
|
|
526
|
+
</html>
|
|
527
|
+
`;
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// src/server/demo.ts
|
|
532
|
+
var demo_exports = {};
|
|
533
|
+
__export(demo_exports, {
|
|
534
|
+
DEMO_LIVE_SESSION: () => DEMO_LIVE_SESSION,
|
|
535
|
+
DEMO_MODELS: () => DEMO_MODELS,
|
|
536
|
+
DEMO_WAITING_SESSION: () => DEMO_WAITING_SESSION,
|
|
537
|
+
demoRunState: () => demoRunState,
|
|
538
|
+
prepareDemo: () => prepareDemo
|
|
539
|
+
});
|
|
540
|
+
import { execFile } from "node:child_process";
|
|
541
|
+
import { randomUUID } from "node:crypto";
|
|
542
|
+
import { mkdir as mkdir2, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
|
|
543
|
+
import path3 from "node:path";
|
|
544
|
+
import { promisify } from "node:util";
|
|
545
|
+
function numbered(source) {
|
|
546
|
+
return source.split("\n").map((line, i) => `${String(i + 1).padStart(6)} ${line}`).join("\n");
|
|
547
|
+
}
|
|
548
|
+
async function demoRepository(dir) {
|
|
549
|
+
const env = { ...process.env, GIT_AUTHOR_NAME: "Demo", GIT_AUTHOR_EMAIL: "demo@example.com", GIT_COMMITTER_NAME: "Demo", GIT_COMMITTER_EMAIL: "demo@example.com" };
|
|
550
|
+
const git2 = (args) => run("git", ["-c", "commit.gpgsign=false", "-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false", "-c", "init.templateDir=", ...args], { cwd: dir, env, timeout: 1e4 });
|
|
551
|
+
try {
|
|
552
|
+
await git2(["init", "-q", "-b", "feat/dark-theme"]);
|
|
553
|
+
await rm2(path3.join(dir, "src/settings/ThemeToggle.tsx"), { force: true });
|
|
554
|
+
await git2(["add", "-A"]);
|
|
555
|
+
await git2(["commit", "-q", "--no-verify", "-m", "Settings page"]);
|
|
556
|
+
await writeFile2(path3.join(dir, "src/styles/theme.css"), DARK_CSS);
|
|
557
|
+
await writeFile2(path3.join(dir, "src/settings/ThemeToggle.tsx"), TOGGLE_TSX);
|
|
558
|
+
} catch {
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
async function prepareDemo() {
|
|
562
|
+
const root = path3.join(config.stateDir, "demo");
|
|
563
|
+
await rm2(root, { recursive: true, force: true });
|
|
564
|
+
const claudeHome = path3.join(root, ".claude");
|
|
565
|
+
for (const [project, files] of Object.entries(DEMO_FILES)) {
|
|
566
|
+
for (const file2 of files) {
|
|
567
|
+
const full = path3.join(root, "code", project, file2);
|
|
568
|
+
await mkdir2(path3.dirname(full), { recursive: true });
|
|
569
|
+
await writeFile2(full, FILE_TEXT[`${project}/${file2}`] ?? "");
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
await demoRepository(path3.join(root, "code", "acme-web"));
|
|
573
|
+
for (const session of SESSIONS) {
|
|
574
|
+
const cwd = session.scratch ? path3.join(root, "scratch", "2026-11-04-7c2e") : path3.join(root, "code", session.project);
|
|
575
|
+
await mkdir2(cwd, { recursive: true });
|
|
576
|
+
const dir = path3.join(claudeHome, "projects", cwd.replace(/[^a-zA-Z0-9]/g, "-"));
|
|
577
|
+
await mkdir2(dir, { recursive: true });
|
|
578
|
+
let parent = null;
|
|
579
|
+
const lines = [JSON.stringify({ type: "custom-title", customTitle: session.title, sessionId: session.id })];
|
|
580
|
+
for (const row of session.rows(cwd)) {
|
|
581
|
+
const uuid = randomUUID();
|
|
582
|
+
const message = row.role === "assistant" ? { id: `msg_demo_${uuid.slice(0, 8)}`, type: "message", role: "assistant", model: "claude-opus-5-5", content: row.content } : { role: "user", content: row.content };
|
|
583
|
+
lines.push(JSON.stringify({
|
|
584
|
+
parentUuid: parent,
|
|
585
|
+
isSidechain: false,
|
|
586
|
+
userType: "external",
|
|
587
|
+
cwd,
|
|
588
|
+
sessionId: session.id,
|
|
589
|
+
version: "2.1.318",
|
|
590
|
+
gitBranch: session.branch,
|
|
591
|
+
type: row.role,
|
|
592
|
+
message,
|
|
593
|
+
uuid,
|
|
594
|
+
timestamp: new Date(row.at).toISOString()
|
|
595
|
+
}));
|
|
596
|
+
parent = uuid;
|
|
597
|
+
}
|
|
598
|
+
await writeFile2(path3.join(dir, `${session.id}.jsonl`), `${lines.join("\n")}
|
|
599
|
+
`);
|
|
600
|
+
}
|
|
601
|
+
config.home = root;
|
|
602
|
+
config.claudeHome = claudeHome;
|
|
603
|
+
config.configDir = path3.join(root, "config");
|
|
604
|
+
config.stateDir = path3.join(root, "state");
|
|
605
|
+
process.env.CLAUDE_CONFIG_DIR = claudeHome;
|
|
606
|
+
process.env.PELICAN_SCRATCH_DIR = path3.join(root, "scratch");
|
|
607
|
+
}
|
|
608
|
+
function demoRunState(sessionId) {
|
|
609
|
+
const first = SESSIONS[0];
|
|
610
|
+
if (sessionId !== first.id) return void 0;
|
|
611
|
+
const cwd = path3.join(config.home, "code", first.project);
|
|
612
|
+
return {
|
|
613
|
+
sessionId,
|
|
614
|
+
cwd,
|
|
615
|
+
busy: true,
|
|
616
|
+
settings: { model: "default", effort: "high", permissionMode: "default" },
|
|
617
|
+
partial: [],
|
|
618
|
+
permissions: [{
|
|
619
|
+
id: "demo-permission",
|
|
620
|
+
sessionId,
|
|
621
|
+
toolName: "Edit",
|
|
622
|
+
input: {
|
|
623
|
+
file_path: `${cwd}/src/settings/Settings.tsx`,
|
|
624
|
+
old_string: " <FontSizePicker />\n </Section>",
|
|
625
|
+
new_string: " <FontSizePicker />\n <ThemeToggle value={theme} onChange={setTheme} />\n </Section>"
|
|
626
|
+
},
|
|
627
|
+
canRemember: true,
|
|
628
|
+
createdAt: Date.now() - 2e4
|
|
629
|
+
}],
|
|
630
|
+
context: { percent: 31, tokens: 62e3, max: 2e5 }
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
var run, MIN, HOUR, tool, result, text, THEME_CSS, SESSIONS, SETTINGS_TSX, TOGGLE_TSX, DARK_CSS, FILE_TEXT, DEMO_FILES, DEMO_MODELS, DEMO_LIVE_SESSION, DEMO_WAITING_SESSION;
|
|
634
|
+
var init_demo = __esm({
|
|
635
|
+
"src/server/demo.ts"() {
|
|
636
|
+
"use strict";
|
|
637
|
+
init_config();
|
|
638
|
+
init_demo_image();
|
|
639
|
+
init_demo_visuals();
|
|
640
|
+
run = promisify(execFile);
|
|
641
|
+
MIN = 6e4;
|
|
642
|
+
HOUR = 60 * MIN;
|
|
643
|
+
tool = (id, name, input) => ({ type: "tool_use", id, name, input });
|
|
644
|
+
result = (id, content, isError = false) => ({ type: "tool_result", tool_use_id: id, content, ...isError ? { is_error: true } : {} });
|
|
645
|
+
text = (value) => ({ type: "text", text: value });
|
|
646
|
+
THEME_CSS = `:root {
|
|
647
|
+
--bg: #ffffff;
|
|
648
|
+
--fg: #111111;
|
|
649
|
+
--muted: #6b6b6b;
|
|
650
|
+
--accent: #2563eb;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
body {
|
|
654
|
+
background: var(--bg);
|
|
655
|
+
color: var(--fg);
|
|
656
|
+
}`;
|
|
657
|
+
SESSIONS = [
|
|
658
|
+
{
|
|
659
|
+
id: "a3f1c2d4-1111-4a1b-8c3d-000000000001",
|
|
660
|
+
title: "Add a dark theme to the settings page",
|
|
661
|
+
project: "acme-web",
|
|
662
|
+
branch: "feat/dark-theme",
|
|
663
|
+
rows: (cwd) => {
|
|
664
|
+
const t = Date.now() - 4 * MIN;
|
|
665
|
+
return [
|
|
666
|
+
{
|
|
667
|
+
role: "user",
|
|
668
|
+
at: t,
|
|
669
|
+
content: [
|
|
670
|
+
{ type: "image", source: { type: "base64", media_type: "image/png", data: SETTINGS_SCREENSHOT } },
|
|
671
|
+
text("Add a dark theme to the settings page, with a toggle. Keep the colors in CSS variables.")
|
|
672
|
+
]
|
|
673
|
+
},
|
|
674
|
+
{ role: "assistant", at: t + 4e3, content: [text("I'll look at how the colors are set up first."), tool("t1", "Read", { file_path: `${cwd}/src/styles/theme.css` })] },
|
|
675
|
+
{ role: "user", at: t + 5e3, content: [result("t1", numbered(THEME_CSS))] },
|
|
676
|
+
{
|
|
677
|
+
role: "assistant",
|
|
678
|
+
at: t + 9e3,
|
|
679
|
+
content: [
|
|
680
|
+
text("The colors are already variables, so a dark theme is one more set of values plus a toggle that switches them."),
|
|
681
|
+
tool("t2", "TodoWrite", {
|
|
682
|
+
todos: [
|
|
683
|
+
{ content: "Add dark color values", status: "completed", activeForm: "Adding dark color values" },
|
|
684
|
+
{ content: "Add the toggle to Settings", status: "in_progress", activeForm: "Adding the toggle to Settings" },
|
|
685
|
+
{ content: "Remember the choice", status: "pending", activeForm: "Remembering the choice" },
|
|
686
|
+
{ content: "Run the tests", status: "pending", activeForm: "Running the tests" }
|
|
687
|
+
]
|
|
688
|
+
})
|
|
689
|
+
]
|
|
690
|
+
},
|
|
691
|
+
{ role: "user", at: t + 9500, content: [result("t2", "Todos have been modified successfully.")] },
|
|
692
|
+
{
|
|
693
|
+
role: "assistant",
|
|
694
|
+
at: t + 14e3,
|
|
695
|
+
content: [tool("t3", "Edit", {
|
|
696
|
+
file_path: `${cwd}/src/styles/theme.css`,
|
|
697
|
+
old_string: " --accent: #2563eb;\n}",
|
|
698
|
+
new_string: " --accent: #2563eb;\n}\n\n:root[data-theme='dark'] {\n --bg: #0b0b0c;\n --fg: #ededed;\n --muted: #a1a1a1;\n --accent: #60a5fa;\n}"
|
|
699
|
+
})]
|
|
700
|
+
},
|
|
701
|
+
{ role: "user", at: t + 15e3, content: [result("t3", `The file ${cwd}/src/styles/theme.css has been updated successfully.`)] },
|
|
702
|
+
{ role: "assistant", at: t + 19e3, content: [text("Now the toggle itself, next to the other display settings:")] }
|
|
703
|
+
];
|
|
704
|
+
}
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
id: "b7e2a9c1-2222-4b2c-9d4e-000000000002",
|
|
708
|
+
title: "Why is the nightly export slow?",
|
|
709
|
+
project: "data-pipeline",
|
|
710
|
+
branch: "main",
|
|
711
|
+
rows: (cwd) => {
|
|
712
|
+
const t = Date.now() - 26 * MIN;
|
|
713
|
+
return [
|
|
714
|
+
{ role: "user", at: t, content: "The nightly export went from 4 minutes to 40. Can you find out why?" },
|
|
715
|
+
{ role: "assistant", at: t + 5e3, content: [tool("g1", "Grep", { pattern: "def export_rows", path: `${cwd}/pipeline` })] },
|
|
716
|
+
{ role: "user", at: t + 6e3, content: [result("g1", "pipeline/export.py:41:def export_rows(batch):")] },
|
|
717
|
+
{ role: "assistant", at: t + 9e3, content: [tool("g2", "Read", { file_path: `${cwd}/pipeline/export.py`, offset: 35, limit: 30 })] },
|
|
718
|
+
{ role: "user", at: t + 1e4, content: [result("g2", numbered("def export_rows(batch):\n for row in batch:\n owner = db.fetch_owner(row.owner_id)\n write(row, owner)"))] },
|
|
719
|
+
{ role: "assistant", at: t + 15e3, content: [tool("g3", "Bash", { command: "python -m cProfile -s cumtime -m pipeline.export --sample 5000 | head -8", description: "Profile the export on a 5,000-row sample" })] },
|
|
720
|
+
{
|
|
721
|
+
role: "user",
|
|
722
|
+
at: t + 31e3,
|
|
723
|
+
content: [result("g3", " ncalls tottime cumtime filename:lineno(function)\n 5000 0.412 61.902 db.py:88(fetch_owner)\n 1 0.020 62.410 export.py:41(export_rows)\n 5000 0.301 0.488 writer.py:12(write)")]
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
role: "assistant",
|
|
727
|
+
at: t + 4e4,
|
|
728
|
+
content: [text([
|
|
729
|
+
"Found it: `export_rows` looks up each row's owner one query at a time. Since the owners table grew past a million rows that lookup is no longer cached, so 5,000 rows cost 5,000 round trips.",
|
|
730
|
+
"",
|
|
731
|
+
"| | Before | After batching |",
|
|
732
|
+
"|---|---|---|",
|
|
733
|
+
"| Queries per 5,000 rows | 5,000 | 1 |",
|
|
734
|
+
"| Time on the sample | 62.4 s | 0.9 s |",
|
|
735
|
+
"",
|
|
736
|
+
"The fix fetches the owners for the whole batch at once:",
|
|
737
|
+
"",
|
|
738
|
+
"```python",
|
|
739
|
+
"owners = db.fetch_owners({row.owner_id for row in batch})",
|
|
740
|
+
"for row in batch:",
|
|
741
|
+
" write(row, owners[row.owner_id])",
|
|
742
|
+
"```",
|
|
743
|
+
"",
|
|
744
|
+
"Want me to apply it and add a test that counts the queries?"
|
|
745
|
+
].join("\n"))]
|
|
746
|
+
}
|
|
747
|
+
];
|
|
748
|
+
}
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
id: "c9d3b8e2-3333-4c3d-ae5f-000000000003",
|
|
752
|
+
title: "Fix the flaky login test",
|
|
753
|
+
project: "acme-web",
|
|
754
|
+
branch: "fix/login-test",
|
|
755
|
+
rows: (cwd) => {
|
|
756
|
+
const t = Date.now() - 3 * HOUR;
|
|
757
|
+
return [
|
|
758
|
+
{ role: "user", at: t, content: "The login test fails about one run in five on CI. Plan a fix first." },
|
|
759
|
+
{
|
|
760
|
+
role: "assistant",
|
|
761
|
+
at: t + 3e4,
|
|
762
|
+
content: [tool("p1", "ExitPlanMode", {
|
|
763
|
+
plan: "## Fix the flaky login test\n\nThe test clicks **Sign in** before the form has loaded its CSRF token, so the request is sometimes rejected.\n\n1. Wait for the token field to be filled before submitting.\n2. Replace the fixed 500 ms sleep with that wait.\n3. Run the test 50 times in a row to confirm."
|
|
764
|
+
})]
|
|
765
|
+
},
|
|
766
|
+
{ role: "user", at: t + 9e4, content: [result("p1", "User has approved your plan. You can now start coding.")] },
|
|
767
|
+
{
|
|
768
|
+
role: "assistant",
|
|
769
|
+
at: t + 95e3,
|
|
770
|
+
content: [tool("p2", "Edit", {
|
|
771
|
+
file_path: `${cwd}/tests/login.spec.ts`,
|
|
772
|
+
old_string: " await page.waitForTimeout(500);\n await page.click('text=Sign in');",
|
|
773
|
+
new_string: " await expect(page.locator('input[name=csrf]')).not.toHaveValue('');\n await page.click('text=Sign in');"
|
|
774
|
+
})]
|
|
775
|
+
},
|
|
776
|
+
{ role: "user", at: t + 96e3, content: [result("p2", `The file ${cwd}/tests/login.spec.ts has been updated successfully.`)] },
|
|
777
|
+
{ role: "assistant", at: t + 99e3, content: [tool("p3", "Bash", { command: "npx playwright test tests/login.spec.ts --repeat-each=50", description: "Run the login test 50 times" })] },
|
|
778
|
+
{ role: "user", at: t + 16e4, content: [result("p3", "Running 50 tests using 4 workers\n\n 50 passed (58.2s)")] },
|
|
779
|
+
{ role: "assistant", at: t + 165e3, content: [text("All 50 runs pass now. The test waits for the CSRF token instead of sleeping, so it is also about 400 ms faster.")] }
|
|
780
|
+
];
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
id: "d1e4c7f3-4444-4d4e-bf60-000000000004",
|
|
785
|
+
title: "Release notes for v2.4",
|
|
786
|
+
project: "docs-site",
|
|
787
|
+
branch: "main",
|
|
788
|
+
rows: () => {
|
|
789
|
+
const t = Date.now() - 26 * HOUR;
|
|
790
|
+
return [
|
|
791
|
+
{ role: "user", at: t, content: "Write the release notes for v2.4 from the merged pull requests." },
|
|
792
|
+
{
|
|
793
|
+
role: "assistant",
|
|
794
|
+
at: t + 5e4,
|
|
795
|
+
content: [text("## v2.4\n\n**Highlights**\n\n- A dark theme, which follows your system setting or can be chosen in Settings.\n- Exports are about 60 times faster on large accounts.\n\n**Fixes**\n\n- The login page no longer fails when submitted right after it opens.\n- Long project names no longer push the menu off screen on phones.")]
|
|
796
|
+
}
|
|
797
|
+
];
|
|
798
|
+
}
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
id: "f3a6e9b5-6666-4f60-9182-000000000006",
|
|
802
|
+
title: "Chart last week's signups",
|
|
803
|
+
project: "data-pipeline",
|
|
804
|
+
branch: "main",
|
|
805
|
+
rows: (cwd) => {
|
|
806
|
+
const t = Date.now() - 50 * MIN;
|
|
807
|
+
return [
|
|
808
|
+
{ role: "user", at: t, content: "Chart last week's signups by day, and make me a one-page summary I can share." },
|
|
809
|
+
{ role: "assistant", at: t + 3e3, content: [text("I'll read the numbers first."), tool("v1", "Read", { file_path: `${cwd}/data/signups.csv` })] },
|
|
810
|
+
{ role: "user", at: t + 4e3, content: [result("v1", numbered(SIGNUPS_CSV.trimEnd()))] },
|
|
811
|
+
{ role: "assistant", at: t + 9e3, content: [tool("v2", "mcp__visualize__show_widget", { title: "signups_by_day", loading_messages: ["Stacking the bars"], widget_code: signupsChart() })] },
|
|
812
|
+
{ role: "user", at: t + 1e4, content: [result("v2", "Shown to the user.")] },
|
|
813
|
+
{ role: "assistant", at: t + 16e3, content: [tool("v3", "Write", { file_path: `${cwd}/reports/signups-week-45.html`, content: REPORT_HTML })] },
|
|
814
|
+
{ role: "user", at: t + 17e3, content: [result("v3", `File created successfully at: ${cwd}/reports/signups-week-45.html`)] },
|
|
815
|
+
{
|
|
816
|
+
role: "assistant",
|
|
817
|
+
at: t + 21e3,
|
|
818
|
+
content: [text("Signups grew **18%** to 12,480. Thursday was the best day (2,214), the day the newsletter went out, and the weekend stayed above the weekday average.\n\nThe one-pager is in `reports/signups-week-45.html`: the three numbers that matter and the chart. Open it from the step above.")]
|
|
819
|
+
}
|
|
820
|
+
];
|
|
821
|
+
}
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
id: "e2f5d8a4-5555-4e5f-8071-000000000005",
|
|
825
|
+
title: "Turn this CSV into JSON",
|
|
826
|
+
project: "scratch",
|
|
827
|
+
branch: "",
|
|
828
|
+
scratch: true,
|
|
829
|
+
rows: (cwd) => {
|
|
830
|
+
const t = Date.now() - 29 * HOUR;
|
|
831
|
+
return [
|
|
832
|
+
{ role: "user", at: t, content: "Turn contacts.csv into a JSON array, one object per row." },
|
|
833
|
+
{ role: "assistant", at: t + 6e3, content: [tool("s1", "Bash", { command: `python3 -c "import csv, json; print(json.dumps(list(csv.DictReader(open('contacts.csv'))), indent=2))" > contacts.json && wc -l contacts.json`, description: "Convert the CSV to JSON" })] },
|
|
834
|
+
{ role: "user", at: t + 8e3, content: [result("s1", ` 1203 ${cwd}/contacts.json`)] },
|
|
835
|
+
{ role: "assistant", at: t + 1e4, content: [text("Done: `contacts.json` has 200 objects, with the CSV header as keys.")] }
|
|
836
|
+
];
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
];
|
|
840
|
+
SETTINGS_TSX = `import { FontSizePicker } from './FontSizePicker';
|
|
841
|
+
import { Section } from './Section';
|
|
842
|
+
|
|
843
|
+
export function Settings() {
|
|
844
|
+
return (
|
|
845
|
+
<Section title="Display">
|
|
846
|
+
<FontSizePicker />
|
|
847
|
+
</Section>
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
`;
|
|
851
|
+
TOGGLE_TSX = `type Theme = 'light' | 'dark';
|
|
852
|
+
|
|
853
|
+
/** Switches the page between the light and dark color values in theme.css. */
|
|
854
|
+
export function ThemeToggle({ value, onChange }: { value: Theme; onChange: (theme: Theme) => void }) {
|
|
855
|
+
return (
|
|
856
|
+
<label className="row">
|
|
857
|
+
Dark theme
|
|
858
|
+
<input type="checkbox" checked={value === 'dark'} onChange={(e) => onChange(e.target.checked ? 'dark' : 'light')} />
|
|
859
|
+
</label>
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
`;
|
|
863
|
+
DARK_CSS = `${THEME_CSS.replace(" --accent: #2563eb;\n}", " --accent: #2563eb;\n}\n\n:root[data-theme='dark'] {\n --bg: #0b0b0c;\n --fg: #ededed;\n --muted: #a1a1a1;\n --accent: #60a5fa;\n}")}
|
|
864
|
+
`;
|
|
865
|
+
FILE_TEXT = {
|
|
866
|
+
"acme-web/src/styles/theme.css": `${THEME_CSS}
|
|
867
|
+
`,
|
|
868
|
+
"acme-web/src/settings/Settings.tsx": SETTINGS_TSX,
|
|
869
|
+
"acme-web/README.md": "# acme-web\n\nThe Acme web app.\n",
|
|
870
|
+
"data-pipeline/README.md": "# data-pipeline\n\nNightly exports and the numbers behind them.\n",
|
|
871
|
+
"data-pipeline/data/signups.csv": SIGNUPS_CSV,
|
|
872
|
+
"data-pipeline/reports/signups-week-45.html": REPORT_HTML,
|
|
873
|
+
"docs-site/README.md": "# docs-site\n\nThe public docs.\n"
|
|
874
|
+
};
|
|
875
|
+
DEMO_FILES = {
|
|
876
|
+
"acme-web": ["README.md", "package.json", "src/app.tsx", "src/settings/Settings.tsx", "src/settings/ThemeToggle.tsx", "src/styles/theme.css", "tests/login.spec.ts"],
|
|
877
|
+
"data-pipeline": ["README.md", "data/signups.csv", "pipeline/export.py", "pipeline/db.py", "pipeline/writer.py", "reports/signups-week-45.html", "tests/test_export.py"],
|
|
878
|
+
"docs-site": ["README.md", "docs/releases.md"]
|
|
879
|
+
};
|
|
880
|
+
DEMO_MODELS = [
|
|
881
|
+
{ value: "default", label: "Default (recommended)", description: "Opus \xB7 best for everyday, complex tasks", efforts: ["low", "medium", "high", "xhigh", "max"], supportsFast: true, resolvedModel: "claude-opus-5-5" },
|
|
882
|
+
{ value: "opus", label: "Opus", description: "For complex work", efforts: ["low", "medium", "high", "xhigh", "max"], supportsFast: true, resolvedModel: "claude-opus-5-5" },
|
|
883
|
+
{ value: "sonnet", label: "Sonnet", description: "Efficient for everyday tasks", efforts: ["low", "medium", "high", "xhigh", "max"], supportsFast: false, resolvedModel: "claude-sonnet-5-5" },
|
|
884
|
+
{ value: "haiku", label: "Haiku", description: "Fastest for quick answers", efforts: [], supportsFast: false, resolvedModel: "claude-haiku-5-5" }
|
|
885
|
+
];
|
|
886
|
+
DEMO_LIVE_SESSION = SESSIONS[1].id;
|
|
887
|
+
DEMO_WAITING_SESSION = SESSIONS[0].id;
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
// src/shared/tasks.ts
|
|
892
|
+
function todosOf(input) {
|
|
893
|
+
return Array.isArray(input.todos) ? input.todos.filter((t) => t && typeof t.content === "string") : [];
|
|
894
|
+
}
|
|
895
|
+
function currentTasks(messages) {
|
|
896
|
+
const results = /* @__PURE__ */ new Map();
|
|
897
|
+
for (const message of messages) {
|
|
898
|
+
for (const block of message.blocks) {
|
|
899
|
+
if (block.type !== "tool_result") continue;
|
|
900
|
+
const text2 = typeof block.content === "string" ? block.content : block.content.map((c) => c.type === "text" ? c.text : "").join("");
|
|
901
|
+
results.set(block.tool_use_id, text2);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
let tasks = [];
|
|
905
|
+
for (const message of messages) {
|
|
906
|
+
if (message.role !== "assistant" || message.parentToolUseId) continue;
|
|
907
|
+
for (const block of message.blocks) {
|
|
908
|
+
if (block.type !== "tool_use") continue;
|
|
909
|
+
const input = block.input;
|
|
910
|
+
if (block.name === "TodoWrite") {
|
|
911
|
+
tasks = todosOf(input).map((t) => ({ ...t }));
|
|
912
|
+
} else if (block.name === "TaskCreate") {
|
|
913
|
+
const id = results.get(block.id)?.match(/Task #(\w+)/)?.[1] ?? String(tasks.length + 1);
|
|
914
|
+
const activeForm = str(input.activeForm);
|
|
915
|
+
tasks.push({ id, content: str(input.subject) ?? str(input.description) ?? "Task", status: "pending", ...activeForm ? { activeForm } : {} });
|
|
916
|
+
} else if (block.name === "TaskUpdate") {
|
|
917
|
+
const task = tasks.find((t) => t.id !== void 0 && t.id === String(input.taskId));
|
|
918
|
+
if (!task) continue;
|
|
919
|
+
const status = str(input.status);
|
|
920
|
+
if (status === "deleted") {
|
|
921
|
+
tasks = tasks.filter((t) => t !== task);
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
if (status && STATUSES.has(status)) task.status = status;
|
|
925
|
+
if (str(input.subject)) task.content = str(input.subject);
|
|
926
|
+
if (str(input.activeForm)) task.activeForm = str(input.activeForm);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return tasks.map(({ id: _id, ...task }) => task);
|
|
931
|
+
}
|
|
932
|
+
var str, STATUSES;
|
|
933
|
+
var init_tasks = __esm({
|
|
934
|
+
"src/shared/tasks.ts"() {
|
|
935
|
+
"use strict";
|
|
936
|
+
str = (value) => typeof value === "string" && value ? value : void 0;
|
|
937
|
+
STATUSES = /* @__PURE__ */ new Set(["pending", "in_progress", "completed"]);
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
// src/server/archive.ts
|
|
942
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
943
|
+
import { mkdir as mkdir3, readFile as readFile2, rename, writeFile as writeFile3 } from "node:fs/promises";
|
|
944
|
+
import path4 from "node:path";
|
|
945
|
+
async function load() {
|
|
946
|
+
let text2;
|
|
947
|
+
try {
|
|
948
|
+
text2 = await readFile2(file(), "utf8");
|
|
949
|
+
} catch (error) {
|
|
950
|
+
if (error.code === "ENOENT") return /* @__PURE__ */ new Set();
|
|
951
|
+
throw error;
|
|
952
|
+
}
|
|
953
|
+
try {
|
|
954
|
+
const ids = JSON.parse(text2).ids;
|
|
955
|
+
return new Set(Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : []);
|
|
956
|
+
} catch {
|
|
957
|
+
await rename(file(), `${file()}.bad-${Date.now()}`).catch(() => void 0);
|
|
958
|
+
console.warn("[pelican] archived.json could not be read; it was set aside and the archive starts empty");
|
|
959
|
+
return /* @__PURE__ */ new Set();
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
function archived() {
|
|
963
|
+
loaded2 ??= load().catch((error) => {
|
|
964
|
+
loaded2 = void 0;
|
|
965
|
+
throw error;
|
|
966
|
+
});
|
|
967
|
+
return loaded2;
|
|
968
|
+
}
|
|
969
|
+
async function archivedIds() {
|
|
970
|
+
return archived().catch(() => /* @__PURE__ */ new Set());
|
|
971
|
+
}
|
|
972
|
+
async function setArchived(sessionId, on) {
|
|
973
|
+
const ids = await archived();
|
|
974
|
+
const before2 = ids.has(sessionId);
|
|
975
|
+
if (on) ids.add(sessionId);
|
|
976
|
+
else ids.delete(sessionId);
|
|
977
|
+
const write = writing.then(async () => {
|
|
978
|
+
await mkdir3(path4.dirname(file()), { recursive: true, mode: 448 });
|
|
979
|
+
const temp = `${file()}.${randomUUID2()}.tmp`;
|
|
980
|
+
await writeFile3(temp, `${JSON.stringify({ ids: [...ids] }, null, 2)}
|
|
981
|
+
`, { mode: 384 });
|
|
982
|
+
await rename(temp, file());
|
|
983
|
+
});
|
|
984
|
+
writing = write.catch(() => void 0);
|
|
985
|
+
try {
|
|
986
|
+
await write;
|
|
987
|
+
} catch (error) {
|
|
988
|
+
if (before2) ids.add(sessionId);
|
|
989
|
+
else ids.delete(sessionId);
|
|
990
|
+
throw error;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
var file, loaded2, writing;
|
|
994
|
+
var init_archive = __esm({
|
|
995
|
+
"src/server/archive.ts"() {
|
|
996
|
+
"use strict";
|
|
997
|
+
init_config();
|
|
998
|
+
file = () => path4.join(config.stateDir, "archived.json");
|
|
999
|
+
writing = Promise.resolve();
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
// src/server/folders.ts
|
|
1004
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1005
|
+
import { realpathSync } from "node:fs";
|
|
1006
|
+
import { mkdir as mkdir4, stat } from "node:fs/promises";
|
|
1007
|
+
import os3 from "node:os";
|
|
1008
|
+
import path5 from "node:path";
|
|
1009
|
+
function folderKind(dir) {
|
|
1010
|
+
if (config.demo && under(dir, [config.home.replace(/\/+$/, "")])) return under(dir, scratchRoots) ? "scratch" : "project";
|
|
1011
|
+
if (under(dir, tempRoots)) return "temp";
|
|
1012
|
+
if (under(dir, scratchRoots)) return "scratch";
|
|
1013
|
+
return "project";
|
|
1014
|
+
}
|
|
1015
|
+
async function isDirectory(dir) {
|
|
1016
|
+
return stat(dir).then((s) => s.isDirectory(), () => false);
|
|
1017
|
+
}
|
|
1018
|
+
async function createScratchFolder() {
|
|
1019
|
+
const now = /* @__PURE__ */ new Date();
|
|
1020
|
+
const day = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
1021
|
+
const dir = path5.join(scratchRoot, `${day}-${randomBytes2(2).toString("hex")}`);
|
|
1022
|
+
await mkdir4(dir, { recursive: true });
|
|
1023
|
+
return dir;
|
|
1024
|
+
}
|
|
1025
|
+
var realpath, scratchRoot, tempRoots, scratchRoots, under;
|
|
1026
|
+
var init_folders = __esm({
|
|
1027
|
+
"src/server/folders.ts"() {
|
|
1028
|
+
"use strict";
|
|
1029
|
+
init_config();
|
|
1030
|
+
realpath = (p) => {
|
|
1031
|
+
try {
|
|
1032
|
+
return realpathSync(p);
|
|
1033
|
+
} catch {
|
|
1034
|
+
return p;
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
scratchRoot = process.env.PELICAN_SCRATCH_DIR ?? path5.join(config.home, "scratch");
|
|
1038
|
+
tempRoots = [.../* @__PURE__ */ new Set(["/tmp", "/private/tmp", "/var/folders", "/private/var/folders", os3.tmpdir(), realpath(os3.tmpdir())])].map((p) => p.replace(/\/+$/, ""));
|
|
1039
|
+
scratchRoots = [path5.join(config.home, "Library/Application Support/Claude/scratch-workspaces"), scratchRoot].map((p) => p.replace(/\/+$/, ""));
|
|
1040
|
+
under = (dir, roots) => {
|
|
1041
|
+
const p = dir.replace(/\/+$/, "");
|
|
1042
|
+
return roots.some((root) => p === root || p.startsWith(`${root}/`));
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
// src/server/live.ts
|
|
1048
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
1049
|
+
import { readdir, readFile as readFile3 } from "node:fs/promises";
|
|
1050
|
+
import path6 from "node:path";
|
|
1051
|
+
import { promisify as promisify2 } from "node:util";
|
|
1052
|
+
async function processes() {
|
|
1053
|
+
try {
|
|
1054
|
+
const { stdout } = await run2("ps", ["-A", "-o", "pid=,ppid=,comm="], { timeout: 3e3, maxBuffer: 8 * 1024 * 1024 });
|
|
1055
|
+
const map = /* @__PURE__ */ new Map();
|
|
1056
|
+
for (const line of stdout.split("\n")) {
|
|
1057
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(line);
|
|
1058
|
+
if (match) map.set(Number(match[1]), { ppid: Number(match[2]), comm: match[3].trim() });
|
|
1059
|
+
}
|
|
1060
|
+
return map;
|
|
1061
|
+
} catch {
|
|
1062
|
+
return void 0;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
function descends(pid, procs) {
|
|
1066
|
+
for (let p = procs.get(pid)?.ppid, hops = 0; p && hops < 8; p = procs.get(p)?.ppid, hops += 1) {
|
|
1067
|
+
if (p === process.pid) return true;
|
|
1068
|
+
}
|
|
1069
|
+
return false;
|
|
1070
|
+
}
|
|
1071
|
+
function alive(pid) {
|
|
1072
|
+
try {
|
|
1073
|
+
process.kill(pid, 0);
|
|
1074
|
+
return true;
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
return error.code === "EPERM";
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
async function liveProcesses() {
|
|
1080
|
+
if (config.demo) return /* @__PURE__ */ new Map([[DEMO_LIVE_SESSION, { app: "Claude Desktop", busy: true }]]);
|
|
1081
|
+
if (cache && Date.now() - cache.at < 1500) return cache.byId;
|
|
1082
|
+
const dir = path6.join(config.claudeHome, "sessions");
|
|
1083
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1084
|
+
let names = [];
|
|
1085
|
+
try {
|
|
1086
|
+
names = (await readdir(dir)).filter((n) => /^\d+\.json$/.test(n));
|
|
1087
|
+
} catch {
|
|
1088
|
+
}
|
|
1089
|
+
const entries = (await Promise.all(names.map(async (name) => {
|
|
1090
|
+
try {
|
|
1091
|
+
const entry = JSON.parse(await readFile3(path6.join(dir, name), "utf8"));
|
|
1092
|
+
return entry.sessionId && typeof entry.pid === "number" && entry.pid !== process.pid && alive(entry.pid) ? entry : void 0;
|
|
1093
|
+
} catch {
|
|
1094
|
+
return void 0;
|
|
1095
|
+
}
|
|
1096
|
+
}))).filter((e) => Boolean(e));
|
|
1097
|
+
const procs = await processes();
|
|
1098
|
+
for (const entry of entries) {
|
|
1099
|
+
const proc = procs?.get(entry.pid);
|
|
1100
|
+
if (procs && (!proc || !/claude|node|bun|disclaimer/i.test(proc.comm))) continue;
|
|
1101
|
+
const ours = procs ? descends(entry.pid, procs) : entry.entrypoint === "sdk-ts";
|
|
1102
|
+
if (ours) continue;
|
|
1103
|
+
const busy = entry.status !== void 0 && entry.status !== "idle";
|
|
1104
|
+
const app2 = CLAUDE_CLIENTS[entry.entrypoint ?? ""];
|
|
1105
|
+
const known = byId.get(entry.sessionId);
|
|
1106
|
+
const worked = typeof entry.statusUpdatedAt === "number" ? entry.statusUpdatedAt : entry.updatedAt;
|
|
1107
|
+
const updatedAt = Math.max(known?.updatedAt ?? 0, typeof worked === "number" ? worked : 0) || void 0;
|
|
1108
|
+
const pick = !known || busy && !known.busy ? { ...app2 ? { app: app2 } : {}, busy } : known;
|
|
1109
|
+
byId.set(entry.sessionId, { ...pick, ...updatedAt ? { updatedAt } : {} });
|
|
1110
|
+
}
|
|
1111
|
+
cache = { at: Date.now(), byId };
|
|
1112
|
+
return byId;
|
|
1113
|
+
}
|
|
1114
|
+
var run2, CLAUDE_CLIENTS, cache;
|
|
1115
|
+
var init_live = __esm({
|
|
1116
|
+
"src/server/live.ts"() {
|
|
1117
|
+
"use strict";
|
|
1118
|
+
init_config();
|
|
1119
|
+
init_demo();
|
|
1120
|
+
run2 = promisify2(execFile2);
|
|
1121
|
+
CLAUDE_CLIENTS = {
|
|
1122
|
+
"claude-desktop": "Claude Desktop",
|
|
1123
|
+
cli: "the terminal",
|
|
1124
|
+
"claude-vscode": "VS Code"
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
// src/server/normalize.ts
|
|
1130
|
+
function clip(text2, max) {
|
|
1131
|
+
return text2.length > max ? { type: "text", text: `${text2.slice(0, max)}
|
|
1132
|
+
\u2026`, truncated: true } : { type: "text", text: text2 };
|
|
1133
|
+
}
|
|
1134
|
+
function image(raw) {
|
|
1135
|
+
const source = raw.source;
|
|
1136
|
+
if (!isRecord(source)) return void 0;
|
|
1137
|
+
if (source.type === "base64" && typeof source.data === "string") {
|
|
1138
|
+
const mediaType = typeof source.media_type === "string" ? source.media_type : "image/png";
|
|
1139
|
+
return source.data.length > MAX_IMAGE_BASE64 ? { type: "image", source: { type: "base64", media_type: mediaType, data: "" }, omitted: true } : { type: "image", source: { type: "base64", media_type: mediaType, data: source.data } };
|
|
1140
|
+
}
|
|
1141
|
+
if (source.type === "url" && typeof source.url === "string") return { type: "image", source: { type: "url", url: source.url } };
|
|
1142
|
+
return void 0;
|
|
1143
|
+
}
|
|
1144
|
+
function resultContent(content) {
|
|
1145
|
+
if (typeof content === "string") {
|
|
1146
|
+
const clipped = clip(content, MAX_RESULT_TEXT);
|
|
1147
|
+
return { content: clipped.text, truncated: Boolean(clipped.truncated) };
|
|
1148
|
+
}
|
|
1149
|
+
if (!Array.isArray(content)) return { content: "", truncated: false };
|
|
1150
|
+
let truncated = false;
|
|
1151
|
+
const parts = [];
|
|
1152
|
+
for (const part of content) {
|
|
1153
|
+
if (!isRecord(part)) continue;
|
|
1154
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
1155
|
+
const clipped = clip(part.text, MAX_RESULT_TEXT);
|
|
1156
|
+
truncated ||= Boolean(clipped.truncated);
|
|
1157
|
+
parts.push(clipped);
|
|
1158
|
+
} else if (part.type === "image") {
|
|
1159
|
+
const img = image(part);
|
|
1160
|
+
if (img) parts.push(img);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
return { content: parts, truncated };
|
|
1164
|
+
}
|
|
1165
|
+
function blocks(content) {
|
|
1166
|
+
if (typeof content === "string") return content ? [clip(content, MAX_TEXT)] : [];
|
|
1167
|
+
if (!Array.isArray(content)) return [];
|
|
1168
|
+
const out = [];
|
|
1169
|
+
for (const raw of content) {
|
|
1170
|
+
if (!isRecord(raw)) continue;
|
|
1171
|
+
switch (raw.type) {
|
|
1172
|
+
case "text":
|
|
1173
|
+
if (typeof raw.text === "string" && raw.text) out.push(clip(raw.text, MAX_TEXT));
|
|
1174
|
+
break;
|
|
1175
|
+
case "thinking":
|
|
1176
|
+
if (typeof raw.thinking === "string" && raw.thinking.trim()) out.push({ type: "thinking", thinking: raw.thinking });
|
|
1177
|
+
break;
|
|
1178
|
+
case "tool_use":
|
|
1179
|
+
case "server_tool_use":
|
|
1180
|
+
if (typeof raw.id === "string" && typeof raw.name === "string") {
|
|
1181
|
+
out.push({ type: "tool_use", id: raw.id, name: raw.name, input: isRecord(raw.input) ? raw.input : {} });
|
|
1182
|
+
}
|
|
1183
|
+
break;
|
|
1184
|
+
case "tool_result": {
|
|
1185
|
+
if (typeof raw.tool_use_id !== "string") break;
|
|
1186
|
+
const { content: resultBody, truncated } = resultContent(raw.content);
|
|
1187
|
+
out.push({
|
|
1188
|
+
type: "tool_result",
|
|
1189
|
+
tool_use_id: raw.tool_use_id,
|
|
1190
|
+
content: resultBody,
|
|
1191
|
+
...raw.is_error === true ? { is_error: true } : {},
|
|
1192
|
+
...truncated ? { truncated: true } : {}
|
|
1193
|
+
});
|
|
1194
|
+
break;
|
|
1195
|
+
}
|
|
1196
|
+
case "web_search_tool_result":
|
|
1197
|
+
if (typeof raw.tool_use_id === "string") {
|
|
1198
|
+
const results = Array.isArray(raw.content) ? raw.content.filter(isRecord) : [];
|
|
1199
|
+
const text2 = results.map((r) => `${String(r.title ?? "")} ${String(r.url ?? "")}`.trim()).join("\n");
|
|
1200
|
+
out.push({ type: "tool_result", tool_use_id: raw.tool_use_id, content: text2 });
|
|
1201
|
+
}
|
|
1202
|
+
break;
|
|
1203
|
+
case "image": {
|
|
1204
|
+
const img = image(raw);
|
|
1205
|
+
if (img) out.push(img);
|
|
1206
|
+
break;
|
|
1207
|
+
}
|
|
1208
|
+
case "document":
|
|
1209
|
+
out.push({ type: "text", text: "[document]" });
|
|
1210
|
+
break;
|
|
1211
|
+
default:
|
|
1212
|
+
break;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
return out;
|
|
1216
|
+
}
|
|
1217
|
+
function toChatMessage(raw) {
|
|
1218
|
+
const type = raw.type;
|
|
1219
|
+
if (type !== "user" && type !== "assistant") return void 0;
|
|
1220
|
+
if (typeof raw.uuid !== "string") return void 0;
|
|
1221
|
+
const message = isRecord(raw.message) ? raw.message : {};
|
|
1222
|
+
const out = {
|
|
1223
|
+
uuid: raw.uuid,
|
|
1224
|
+
role: type,
|
|
1225
|
+
blocks: blocks(message.content)
|
|
1226
|
+
};
|
|
1227
|
+
if (out.blocks.length === 0) return void 0;
|
|
1228
|
+
const timestamp = typeof raw.timestamp === "string" ? Date.parse(raw.timestamp) : NaN;
|
|
1229
|
+
if (!Number.isNaN(timestamp)) out.timestamp = timestamp;
|
|
1230
|
+
const parent = raw.parent_tool_use_id;
|
|
1231
|
+
if (typeof parent === "string") out.parentToolUseId = parent;
|
|
1232
|
+
if (type === "assistant") {
|
|
1233
|
+
if (typeof message.model === "string") out.model = message.model;
|
|
1234
|
+
if (typeof message.id === "string") out.messageId = message.id;
|
|
1235
|
+
}
|
|
1236
|
+
if (raw.is_meta === true || raw.isMeta === true || raw.isSynthetic === true) out.meta = true;
|
|
1237
|
+
if (raw.isCompactSummary === true) out.compactSummary = true;
|
|
1238
|
+
return out;
|
|
1239
|
+
}
|
|
1240
|
+
var MAX_TEXT, MAX_RESULT_TEXT, MAX_IMAGE_BASE64, isRecord;
|
|
1241
|
+
var init_normalize = __esm({
|
|
1242
|
+
"src/server/normalize.ts"() {
|
|
1243
|
+
"use strict";
|
|
1244
|
+
MAX_TEXT = 6e4;
|
|
1245
|
+
MAX_RESULT_TEXT = 24e3;
|
|
1246
|
+
MAX_IMAGE_BASE64 = 25e5;
|
|
1247
|
+
isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
// src/server/history.ts
|
|
1252
|
+
import { open, stat as stat2 } from "node:fs/promises";
|
|
1253
|
+
function empty(ino) {
|
|
1254
|
+
return {
|
|
1255
|
+
recorded: {},
|
|
1256
|
+
ino,
|
|
1257
|
+
offset: 0,
|
|
1258
|
+
carry: Buffer.alloc(0),
|
|
1259
|
+
nodes: /* @__PURE__ */ new Map(),
|
|
1260
|
+
order: [],
|
|
1261
|
+
leaf: void 0,
|
|
1262
|
+
chain: [],
|
|
1263
|
+
chainIndex: /* @__PURE__ */ new Map(),
|
|
1264
|
+
chainLeaf: void 0,
|
|
1265
|
+
messages: []
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
function note(recorded, row) {
|
|
1269
|
+
if (row.type === "assistant") {
|
|
1270
|
+
const model = str2(row.requestedModel) ?? str2(row.message?.model);
|
|
1271
|
+
if (model && !model.startsWith("<")) recorded.model = model;
|
|
1272
|
+
if (str2(row.effort)) recorded.effort = str2(row.effort);
|
|
1273
|
+
const speed = str2(row.message?.usage?.speed);
|
|
1274
|
+
if (speed) recorded.fast = speed === "fast";
|
|
1275
|
+
recorded.ultracode ??= false;
|
|
1276
|
+
} else if (row.type === "user" && str2(row.permissionMode) && MODES.has(str2(row.permissionMode))) {
|
|
1277
|
+
recorded.permissionMode = str2(row.permissionMode);
|
|
1278
|
+
} else if (row.type === "attachment") {
|
|
1279
|
+
const kind = str2(row.attachment?.type) ?? "";
|
|
1280
|
+
if (kind === "ultra_effort_enter") recorded.ultracode = true;
|
|
1281
|
+
else if (/^ultra_effort_(exit|leave|off)/.test(kind)) recorded.ultracode = false;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
function steeredMessage(row, uuid) {
|
|
1285
|
+
const attachment = row.attachment;
|
|
1286
|
+
if (row.type !== "attachment" || attachment?.type !== "queued_command") return void 0;
|
|
1287
|
+
if (attachment.commandMode !== void 0 && attachment.commandMode !== "prompt") return void 0;
|
|
1288
|
+
const message = toChatMessage({ type: "user", uuid, timestamp: row.timestamp, message: { role: "user", content: attachment.prompt } });
|
|
1289
|
+
if (!message) return void 0;
|
|
1290
|
+
const source = str2(attachment.source_uuid);
|
|
1291
|
+
return { ...message, steered: true, ...source ? { sourceUuid: source } : {} };
|
|
1292
|
+
}
|
|
1293
|
+
function addLine(t, line) {
|
|
1294
|
+
if (line.length < 2 || line.charCodeAt(0) !== 123 || !line.includes('"uuid"')) return;
|
|
1295
|
+
let row;
|
|
1296
|
+
try {
|
|
1297
|
+
row = JSON.parse(line);
|
|
1298
|
+
} catch {
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
const uuid = str2(row.uuid);
|
|
1302
|
+
if (!uuid || row.isSidechain === true) return;
|
|
1303
|
+
note(t.recorded, row);
|
|
1304
|
+
const turn = row.type === "user" || row.type === "assistant";
|
|
1305
|
+
const message = turn ? toChatMessage(row) : steeredMessage(row, uuid);
|
|
1306
|
+
const existing = t.nodes.get(uuid);
|
|
1307
|
+
if (existing) {
|
|
1308
|
+
existing.message = message;
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
const metadata = row.compactMetadata;
|
|
1312
|
+
const node = {
|
|
1313
|
+
uuid,
|
|
1314
|
+
parent: str2(row.parentUuid),
|
|
1315
|
+
logicalParent: str2(row.logicalParentUuid),
|
|
1316
|
+
preservedTail: str2(metadata?.preservedSegment?.tailUuid),
|
|
1317
|
+
index: t.order.length,
|
|
1318
|
+
message
|
|
1319
|
+
};
|
|
1320
|
+
t.nodes.set(uuid, node);
|
|
1321
|
+
t.order.push(node);
|
|
1322
|
+
if (turn) t.leaf = node;
|
|
1323
|
+
}
|
|
1324
|
+
async function readNew(file2, t, size) {
|
|
1325
|
+
const handle = await open(file2, "r");
|
|
1326
|
+
try {
|
|
1327
|
+
let position = t.offset;
|
|
1328
|
+
while (position < size) {
|
|
1329
|
+
const buffer = Buffer.allocUnsafe(Math.min(CHUNK, size - position));
|
|
1330
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
|
|
1331
|
+
if (bytesRead <= 0) break;
|
|
1332
|
+
position += bytesRead;
|
|
1333
|
+
const data = t.carry.length > 0 ? Buffer.concat([t.carry, buffer.subarray(0, bytesRead)]) : buffer.subarray(0, bytesRead);
|
|
1334
|
+
const lastBreak = data.lastIndexOf(10);
|
|
1335
|
+
if (lastBreak < 0) {
|
|
1336
|
+
t.carry = Buffer.from(data);
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
t.carry = Buffer.from(data.subarray(lastBreak + 1));
|
|
1340
|
+
for (const line of data.subarray(0, lastBreak).toString("utf8").split("\n")) addLine(t, line);
|
|
1341
|
+
}
|
|
1342
|
+
t.offset = position;
|
|
1343
|
+
if (t.carry.length > 0) addLine(t, t.carry.toString("utf8"));
|
|
1344
|
+
} finally {
|
|
1345
|
+
await handle.close();
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
function before(t, boundary) {
|
|
1349
|
+
return t.nodes.get(boundary.logicalParent) ?? (boundary.preservedTail ? t.nodes.get(boundary.preservedTail) : void 0) ?? (boundary.index > 0 ? t.order[boundary.index - 1] : void 0);
|
|
1350
|
+
}
|
|
1351
|
+
function walk(t, from, known) {
|
|
1352
|
+
const out = [];
|
|
1353
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1354
|
+
let node = from;
|
|
1355
|
+
while (node && !seen.has(node.uuid)) {
|
|
1356
|
+
const at = known?.get(node.uuid);
|
|
1357
|
+
if (at !== void 0) return { nodes: out.reverse(), joinedAt: at };
|
|
1358
|
+
seen.add(node.uuid);
|
|
1359
|
+
out.push(node);
|
|
1360
|
+
if (node.parent) node = t.nodes.get(node.parent);
|
|
1361
|
+
else if (node.logicalParent) node = before(t, node);
|
|
1362
|
+
else node = void 0;
|
|
1363
|
+
}
|
|
1364
|
+
return { nodes: out.reverse(), joinedAt: -1 };
|
|
1365
|
+
}
|
|
1366
|
+
function conversation(t) {
|
|
1367
|
+
const leaf = t.leaf;
|
|
1368
|
+
if (!leaf) return [];
|
|
1369
|
+
if (t.chainLeaf === leaf.uuid) return t.messages;
|
|
1370
|
+
const { nodes, joinedAt } = walk(t, leaf, t.chainIndex);
|
|
1371
|
+
t.chain = joinedAt >= 0 ? t.chain.slice(0, joinedAt + 1).concat(nodes) : nodes;
|
|
1372
|
+
t.chainIndex = new Map(t.chain.map((n, i) => [n.uuid, i]));
|
|
1373
|
+
t.chainLeaf = leaf.uuid;
|
|
1374
|
+
t.messages = t.chain.flatMap((n) => n.message ? [n.message] : []);
|
|
1375
|
+
return t.messages;
|
|
1376
|
+
}
|
|
1377
|
+
async function read(file2) {
|
|
1378
|
+
let info;
|
|
1379
|
+
try {
|
|
1380
|
+
info = await stat2(file2);
|
|
1381
|
+
} catch {
|
|
1382
|
+
transcripts.delete(file2);
|
|
1383
|
+
return [];
|
|
1384
|
+
}
|
|
1385
|
+
let t = transcripts.get(file2);
|
|
1386
|
+
if (!t || t.ino !== info.ino || info.size < t.offset) t = empty(info.ino);
|
|
1387
|
+
transcripts.delete(file2);
|
|
1388
|
+
transcripts.set(file2, t);
|
|
1389
|
+
while (transcripts.size > MAX_FILES) transcripts.delete(transcripts.keys().next().value);
|
|
1390
|
+
if (info.size > t.offset) await readNew(file2, t, info.size);
|
|
1391
|
+
return conversation(t);
|
|
1392
|
+
}
|
|
1393
|
+
async function readRecordedSettings(file2) {
|
|
1394
|
+
await readHistory(file2);
|
|
1395
|
+
return { ...transcripts.get(file2)?.recorded ?? {} };
|
|
1396
|
+
}
|
|
1397
|
+
function readHistory(file2) {
|
|
1398
|
+
const previous = locks.get(file2) ?? Promise.resolve();
|
|
1399
|
+
const next = previous.catch(() => void 0).then(() => read(file2));
|
|
1400
|
+
locks.set(file2, next);
|
|
1401
|
+
void next.finally(() => {
|
|
1402
|
+
if (locks.get(file2) === next) locks.delete(file2);
|
|
1403
|
+
}).catch(() => void 0);
|
|
1404
|
+
return next;
|
|
1405
|
+
}
|
|
1406
|
+
var CHUNK, MAX_FILES, transcripts, locks, str2, MODES;
|
|
1407
|
+
var init_history = __esm({
|
|
1408
|
+
"src/server/history.ts"() {
|
|
1409
|
+
"use strict";
|
|
1410
|
+
init_normalize();
|
|
1411
|
+
CHUNK = 8 * 1024 * 1024;
|
|
1412
|
+
MAX_FILES = 16;
|
|
1413
|
+
transcripts = /* @__PURE__ */ new Map();
|
|
1414
|
+
locks = /* @__PURE__ */ new Map();
|
|
1415
|
+
str2 = (value) => typeof value === "string" && value ? value : void 0;
|
|
1416
|
+
MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "auto", "bypassPermissions"]);
|
|
1417
|
+
}
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
// src/server/transcripts.ts
|
|
1421
|
+
import { createReadStream } from "node:fs";
|
|
1422
|
+
import { open as open2, readdir as readdir2, stat as stat3 } from "node:fs/promises";
|
|
1423
|
+
import readline from "node:readline";
|
|
1424
|
+
import path7 from "node:path";
|
|
1425
|
+
async function rebuildIndex() {
|
|
1426
|
+
const next = /* @__PURE__ */ new Map();
|
|
1427
|
+
let dirs = [];
|
|
1428
|
+
try {
|
|
1429
|
+
dirs = await readdir2(projectsDir());
|
|
1430
|
+
} catch {
|
|
1431
|
+
}
|
|
1432
|
+
await Promise.all(dirs.map(async (dir) => {
|
|
1433
|
+
try {
|
|
1434
|
+
for (const name of await readdir2(path7.join(projectsDir(), dir))) {
|
|
1435
|
+
if (name.endsWith(".jsonl")) next.set(name.slice(0, -".jsonl".length), path7.join(projectsDir(), dir, name));
|
|
1436
|
+
}
|
|
1437
|
+
} catch {
|
|
1438
|
+
}
|
|
1439
|
+
}));
|
|
1440
|
+
index = next;
|
|
1441
|
+
indexBuiltAt = Date.now();
|
|
1442
|
+
}
|
|
1443
|
+
async function transcriptPath(sessionId) {
|
|
1444
|
+
if (!index.has(sessionId) && Date.now() - indexBuiltAt > 1e3) await rebuildIndex();
|
|
1445
|
+
return index.get(sessionId);
|
|
1446
|
+
}
|
|
1447
|
+
async function refreshTranscriptIndex() {
|
|
1448
|
+
await rebuildIndex();
|
|
1449
|
+
}
|
|
1450
|
+
function replyText(row) {
|
|
1451
|
+
const content = row.message?.content;
|
|
1452
|
+
if (!Array.isArray(content)) return void 0;
|
|
1453
|
+
const text2 = content.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join(" ").replace(/[`*_#>|]+/g, "").replace(/\s+/g, " ").trim();
|
|
1454
|
+
return text2 ? text2.slice(0, 160) : void 0;
|
|
1455
|
+
}
|
|
1456
|
+
async function readLastTurn(file2) {
|
|
1457
|
+
let handle;
|
|
1458
|
+
let found;
|
|
1459
|
+
let foundAt = 0;
|
|
1460
|
+
try {
|
|
1461
|
+
handle = await open2(file2, "r");
|
|
1462
|
+
const { size } = await handle.stat();
|
|
1463
|
+
let end = size;
|
|
1464
|
+
let carry = Buffer.alloc(0);
|
|
1465
|
+
while (end > 0 && size - end < MAX_SCAN && !(found && foundAt - end > PREVIEW_SCAN)) {
|
|
1466
|
+
const start = Math.max(0, end - CHUNK2);
|
|
1467
|
+
const chunk = Buffer.alloc(end - start);
|
|
1468
|
+
await handle.read(chunk, 0, chunk.length, start);
|
|
1469
|
+
let data = Buffer.concat([chunk, carry]);
|
|
1470
|
+
end = start;
|
|
1471
|
+
if (start > 0) {
|
|
1472
|
+
const firstBreak = data.indexOf(10);
|
|
1473
|
+
if (firstBreak < 0) {
|
|
1474
|
+
carry = data;
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
carry = data.subarray(0, firstBreak);
|
|
1478
|
+
data = data.subarray(firstBreak + 1);
|
|
1479
|
+
}
|
|
1480
|
+
const lines = data.toString("utf8").split("\n");
|
|
1481
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
1482
|
+
const line = lines[i];
|
|
1483
|
+
if (!line.includes('"timestamp"') || !(line.includes('"type":"user"') || line.includes('"type":"assistant"'))) continue;
|
|
1484
|
+
if (found && !line.includes('"type":"text"') && !line.includes('"content":"')) continue;
|
|
1485
|
+
try {
|
|
1486
|
+
const row = JSON.parse(line);
|
|
1487
|
+
if (row.type !== "user" && row.type !== "assistant" || row.isSidechain || typeof row.timestamp !== "string") continue;
|
|
1488
|
+
if (!found) {
|
|
1489
|
+
const time = Date.parse(row.timestamp);
|
|
1490
|
+
if (Number.isNaN(time)) continue;
|
|
1491
|
+
found = {
|
|
1492
|
+
time,
|
|
1493
|
+
...typeof row.cwd === "string" ? { cwd: row.cwd } : {},
|
|
1494
|
+
...typeof row.gitBranch === "string" ? { gitBranch: row.gitBranch } : {},
|
|
1495
|
+
...row.forkedFrom ? { copied: true } : {}
|
|
1496
|
+
};
|
|
1497
|
+
foundAt = end;
|
|
1498
|
+
}
|
|
1499
|
+
if (row.type === "assistant") {
|
|
1500
|
+
const preview = replyText(row);
|
|
1501
|
+
if (preview) return { ...found, preview };
|
|
1502
|
+
} else if (found && row.type === "user" && typeof row.message?.content === "string") {
|
|
1503
|
+
return found;
|
|
1504
|
+
}
|
|
1505
|
+
} catch {
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
} catch {
|
|
1510
|
+
} finally {
|
|
1511
|
+
await handle?.close();
|
|
1512
|
+
}
|
|
1513
|
+
return found;
|
|
1514
|
+
}
|
|
1515
|
+
async function lastTurnTime(file2) {
|
|
1516
|
+
let info;
|
|
1517
|
+
try {
|
|
1518
|
+
info = await stat3(file2);
|
|
1519
|
+
} catch {
|
|
1520
|
+
return void 0;
|
|
1521
|
+
}
|
|
1522
|
+
const cached = lastTurnCache.get(file2);
|
|
1523
|
+
if (cached && cached.size === info.size && cached.mtimeMs === info.mtimeMs) {
|
|
1524
|
+
return { lastTurn: cached.value, mtimeMs: info.mtimeMs, size: info.size };
|
|
1525
|
+
}
|
|
1526
|
+
const value = await readLastTurn(file2);
|
|
1527
|
+
lastTurnCache.set(file2, { size: info.size, mtimeMs: info.mtimeMs, value });
|
|
1528
|
+
return { lastTurn: value, mtimeMs: info.mtimeMs, size: info.size };
|
|
1529
|
+
}
|
|
1530
|
+
async function firstTurnSession(file2) {
|
|
1531
|
+
const cached = headCache.get(file2);
|
|
1532
|
+
if (cached !== void 0) return cached ?? void 0;
|
|
1533
|
+
let found;
|
|
1534
|
+
let read2 = 0;
|
|
1535
|
+
try {
|
|
1536
|
+
const input = createReadStream(file2);
|
|
1537
|
+
const lines = readline.createInterface({ input, crlfDelay: Infinity });
|
|
1538
|
+
try {
|
|
1539
|
+
for await (const line of lines) {
|
|
1540
|
+
read2 += line.length + 1;
|
|
1541
|
+
if (read2 > HEAD_SCAN) break;
|
|
1542
|
+
if (!line.includes('"sessionId"') || !(line.includes('"type":"user"') || line.includes('"type":"assistant"'))) continue;
|
|
1543
|
+
try {
|
|
1544
|
+
const row = JSON.parse(line);
|
|
1545
|
+
if ((row.type === "user" || row.type === "assistant") && !row.isSidechain && typeof row.sessionId === "string") {
|
|
1546
|
+
found = row.sessionId;
|
|
1547
|
+
break;
|
|
1548
|
+
}
|
|
1549
|
+
} catch {
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
} finally {
|
|
1553
|
+
lines.close();
|
|
1554
|
+
input.destroy();
|
|
1555
|
+
}
|
|
1556
|
+
} catch {
|
|
1557
|
+
return void 0;
|
|
1558
|
+
}
|
|
1559
|
+
if (found) {
|
|
1560
|
+
if (headCache.size > 1e4) headCache.clear();
|
|
1561
|
+
headCache.set(file2, found);
|
|
1562
|
+
}
|
|
1563
|
+
return found;
|
|
1564
|
+
}
|
|
1565
|
+
async function rawEntry(file2, uuid) {
|
|
1566
|
+
const needle = `"uuid":"${uuid}"`;
|
|
1567
|
+
const input = createReadStream(file2);
|
|
1568
|
+
const lines = readline.createInterface({ input, crlfDelay: Infinity });
|
|
1569
|
+
try {
|
|
1570
|
+
for await (const line of lines) {
|
|
1571
|
+
if (!line.includes(needle)) continue;
|
|
1572
|
+
let row;
|
|
1573
|
+
try {
|
|
1574
|
+
row = JSON.parse(line);
|
|
1575
|
+
} catch {
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1578
|
+
if (row.uuid !== uuid) continue;
|
|
1579
|
+
const content = row.type === "attachment" && row.attachment?.type === "queued_command" ? row.attachment.prompt : row.message?.content;
|
|
1580
|
+
return { parentUuid: typeof row.parentUuid === "string" ? row.parentUuid : null, content };
|
|
1581
|
+
}
|
|
1582
|
+
} catch {
|
|
1583
|
+
return void 0;
|
|
1584
|
+
} finally {
|
|
1585
|
+
lines.close();
|
|
1586
|
+
input.destroy();
|
|
1587
|
+
}
|
|
1588
|
+
return void 0;
|
|
1589
|
+
}
|
|
1590
|
+
async function rawImages(file2, uuid) {
|
|
1591
|
+
const content = (await rawEntry(file2, uuid))?.content;
|
|
1592
|
+
if (!Array.isArray(content)) return [];
|
|
1593
|
+
const images = [];
|
|
1594
|
+
for (const block of content) {
|
|
1595
|
+
if (block?.type !== "image" || !block.source) continue;
|
|
1596
|
+
if (block.source.type === "base64" && typeof block.source.data === "string") {
|
|
1597
|
+
images.push({ type: "base64", mediaType: typeof block.source.media_type === "string" ? block.source.media_type : "image/png", data: block.source.data });
|
|
1598
|
+
} else if (block.source.type === "url" && typeof block.source.url === "string") {
|
|
1599
|
+
images.push({ type: "url" });
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
return images;
|
|
1603
|
+
}
|
|
1604
|
+
var projectsDir, index, indexBuiltAt, CHUNK2, MAX_SCAN, PREVIEW_SCAN, lastTurnCache, HEAD_SCAN, headCache;
|
|
1605
|
+
var init_transcripts = __esm({
|
|
1606
|
+
"src/server/transcripts.ts"() {
|
|
1607
|
+
"use strict";
|
|
1608
|
+
init_config();
|
|
1609
|
+
projectsDir = () => path7.join(config.claudeHome, "projects");
|
|
1610
|
+
index = /* @__PURE__ */ new Map();
|
|
1611
|
+
indexBuiltAt = 0;
|
|
1612
|
+
CHUNK2 = 256 * 1024;
|
|
1613
|
+
MAX_SCAN = 16 * 1024 * 1024;
|
|
1614
|
+
PREVIEW_SCAN = 4 * 1024 * 1024;
|
|
1615
|
+
lastTurnCache = /* @__PURE__ */ new Map();
|
|
1616
|
+
HEAD_SCAN = 64 * 1024 * 1024;
|
|
1617
|
+
headCache = /* @__PURE__ */ new Map();
|
|
1618
|
+
}
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
// src/server/sessions.ts
|
|
1622
|
+
import path8 from "node:path";
|
|
1623
|
+
import { getSessionInfo, getSubagentMessages, listSessions, renameSession } from "@anthropic-ai/claude-agent-sdk";
|
|
1624
|
+
function titleOf(info) {
|
|
1625
|
+
const title = info.customTitle || info.summary || info.firstPrompt || "";
|
|
1626
|
+
return title.replace(/\s+/g, " ").trim() || "Untitled session";
|
|
1627
|
+
}
|
|
1628
|
+
async function summarize(info, status, live, archived2) {
|
|
1629
|
+
const file2 = await transcriptPath(info.sessionId);
|
|
1630
|
+
const times = file2 ? await lastTurnTime(file2) : void 0;
|
|
1631
|
+
const lastTurn = times?.lastTurn;
|
|
1632
|
+
const lastActivity = lastTurn?.time ?? info.lastModified;
|
|
1633
|
+
const own = status(info.sessionId);
|
|
1634
|
+
let state = own ?? "idle";
|
|
1635
|
+
const other = live.get(info.sessionId);
|
|
1636
|
+
const elsewhere = own ? void 0 : other;
|
|
1637
|
+
const recent2 = lastTurn && !lastTurn.copied && Date.now() - lastTurn.time < config.liveWindowMs;
|
|
1638
|
+
if (!own && (elsewhere?.busy || recent2)) state = "live";
|
|
1639
|
+
else if (own === "idle" && other?.busy) state = "live";
|
|
1640
|
+
const cwd = info.cwd || lastTurn?.cwd || "";
|
|
1641
|
+
if (!info.cwd && cwd) infoCache.set(info.sessionId, { ...info, cwd });
|
|
1642
|
+
const gitBranch = lastTurn?.gitBranch ?? info.gitBranch;
|
|
1643
|
+
const title = titleOf(info);
|
|
1644
|
+
const copiedFrom = file2 && title === "Side chat" ? await firstTurnSession(file2) : void 0;
|
|
1645
|
+
const sideChatOf = copiedFrom && copiedFrom !== info.sessionId ? copiedFrom : void 0;
|
|
1646
|
+
return {
|
|
1647
|
+
id: info.sessionId,
|
|
1648
|
+
title,
|
|
1649
|
+
cwd,
|
|
1650
|
+
project: cwd ? path8.basename(cwd) : "",
|
|
1651
|
+
...gitBranch && gitBranch !== "HEAD" ? { gitBranch } : {},
|
|
1652
|
+
lastActivity,
|
|
1653
|
+
...info.createdAt ? { createdAt: info.createdAt } : {},
|
|
1654
|
+
status: state,
|
|
1655
|
+
...info.fileSize ? { sizeBytes: info.fileSize } : {},
|
|
1656
|
+
...cwd && folderKind(cwd) === "scratch" ? { scratch: true } : {},
|
|
1657
|
+
...cwd && folderKind(cwd) === "temp" ? { temp: true } : {},
|
|
1658
|
+
...state === "live" && other?.busy && other.app ? { runningIn: other.app } : {},
|
|
1659
|
+
...lastTurn?.preview ? { preview: lastTurn.preview } : {},
|
|
1660
|
+
...sideChatOf ? { sideChatOf } : {},
|
|
1661
|
+
...archived2.has(info.sessionId) ? { archived: true } : {},
|
|
1662
|
+
// Open, idle, in another app too: its Claude does not see what is sent here until it reopens.
|
|
1663
|
+
...other && !other.busy ? { openIn: other.app ?? "another app" } : {}
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
async function listSummaries(status) {
|
|
1667
|
+
const infos = await listSessions({ includeProgrammatic: true });
|
|
1668
|
+
for (const info of infos) infoCache.set(info.sessionId, info);
|
|
1669
|
+
const [live, archived2] = await Promise.all([liveProcesses(), archivedIds()]);
|
|
1670
|
+
const summaries = await Promise.all(infos.map((info) => summarize(info, status, live, archived2)));
|
|
1671
|
+
return summaries.sort((a, b) => b.lastActivity - a.lastActivity);
|
|
1672
|
+
}
|
|
1673
|
+
async function sessionInfo(sessionId) {
|
|
1674
|
+
if (!infoCache.has(sessionId)) {
|
|
1675
|
+
for (const info2 of await listSessions({ includeProgrammatic: true }).catch(() => [])) infoCache.set(info2.sessionId, info2);
|
|
1676
|
+
}
|
|
1677
|
+
const cached = infoCache.get(sessionId);
|
|
1678
|
+
const info = await getSessionInfo(sessionId, cached?.cwd ? { dir: cached.cwd } : {}).catch(() => void 0);
|
|
1679
|
+
if (!info) return cached;
|
|
1680
|
+
let cwd = info.cwd || cached?.cwd;
|
|
1681
|
+
if (!cwd) {
|
|
1682
|
+
const file2 = await transcriptPath(sessionId);
|
|
1683
|
+
cwd = file2 ? (await lastTurnTime(file2))?.lastTurn?.cwd : void 0;
|
|
1684
|
+
}
|
|
1685
|
+
const merged = { ...cached, ...info, ...cwd ? { cwd } : {} };
|
|
1686
|
+
infoCache.set(sessionId, merged);
|
|
1687
|
+
return merged;
|
|
1688
|
+
}
|
|
1689
|
+
async function sessionSummary(sessionId, status) {
|
|
1690
|
+
const info = await sessionInfo(sessionId);
|
|
1691
|
+
return info ? summarize(info, status, await liveProcesses(), await archivedIds()) : void 0;
|
|
1692
|
+
}
|
|
1693
|
+
async function renameSessionTitle(sessionId, title) {
|
|
1694
|
+
const dir = infoCache.get(sessionId)?.cwd;
|
|
1695
|
+
await renameSession(sessionId, title, dir ? { dir } : {});
|
|
1696
|
+
infoCache.delete(sessionId);
|
|
1697
|
+
}
|
|
1698
|
+
async function recordedSettings(sessionId) {
|
|
1699
|
+
const file2 = await transcriptPath(sessionId);
|
|
1700
|
+
return file2 ? readRecordedSettings(file2) : {};
|
|
1701
|
+
}
|
|
1702
|
+
async function loadMessages(sessionId) {
|
|
1703
|
+
const file2 = await transcriptPath(sessionId);
|
|
1704
|
+
return file2 ? readHistory(file2) : [];
|
|
1705
|
+
}
|
|
1706
|
+
async function sentFiles(sessionId) {
|
|
1707
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1708
|
+
for (const message of await loadMessages(sessionId)) {
|
|
1709
|
+
if (message.role !== "assistant") continue;
|
|
1710
|
+
for (const block of message.blocks) {
|
|
1711
|
+
if (block.type !== "tool_use") continue;
|
|
1712
|
+
if (block.name === "SendUserFile" && Array.isArray(block.input.files)) {
|
|
1713
|
+
for (const file2 of block.input.files) if (typeof file2 === "string") paths.add(file2);
|
|
1714
|
+
} else if (block.name === "Artifact" && typeof block.input.file_path === "string") {
|
|
1715
|
+
paths.add(block.input.file_path);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
return paths;
|
|
1720
|
+
}
|
|
1721
|
+
async function loadSubagent(sessionId, agentId) {
|
|
1722
|
+
if (!/^[\w-]{1,80}$/.test(agentId)) return [];
|
|
1723
|
+
const dir = infoCache.get(sessionId)?.cwd;
|
|
1724
|
+
const raw = await getSubagentMessages(sessionId, agentId, dir ? { dir } : {}).catch(() => []);
|
|
1725
|
+
return raw.map((message) => toChatMessage(message)).filter((message) => Boolean(message));
|
|
1726
|
+
}
|
|
1727
|
+
async function loadPage(sessionId, before2, limit) {
|
|
1728
|
+
const all = await loadMessages(sessionId);
|
|
1729
|
+
const end = Math.min(before2 ?? all.length, all.length);
|
|
1730
|
+
const offset = Math.max(0, end - limit);
|
|
1731
|
+
return { messages: all.slice(offset, end), total: all.length, offset };
|
|
1732
|
+
}
|
|
1733
|
+
var infoCache;
|
|
1734
|
+
var init_sessions = __esm({
|
|
1735
|
+
"src/server/sessions.ts"() {
|
|
1736
|
+
"use strict";
|
|
1737
|
+
init_archive();
|
|
1738
|
+
init_config();
|
|
1739
|
+
init_folders();
|
|
1740
|
+
init_live();
|
|
1741
|
+
init_history();
|
|
1742
|
+
init_normalize();
|
|
1743
|
+
init_transcripts();
|
|
1744
|
+
infoCache = /* @__PURE__ */ new Map();
|
|
1745
|
+
}
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1748
|
+
// src/server/actions.ts
|
|
1749
|
+
import { deleteSession, forkSession } from "@anthropic-ai/claude-agent-sdk";
|
|
1750
|
+
async function fork(sessionId, upTo, title) {
|
|
1751
|
+
if (config.demo) throw new Error("This is the demo: nothing changes.");
|
|
1752
|
+
const info = await sessionInfo(sessionId);
|
|
1753
|
+
const result2 = await forkSession(sessionId, {
|
|
1754
|
+
...info?.cwd ? { dir: info.cwd } : {},
|
|
1755
|
+
...upTo ? { upToMessageId: upTo } : {},
|
|
1756
|
+
...title ? { title } : {}
|
|
1757
|
+
});
|
|
1758
|
+
await refreshTranscriptIndex();
|
|
1759
|
+
return result2.sessionId;
|
|
1760
|
+
}
|
|
1761
|
+
async function messageBefore(sessionId, uuid) {
|
|
1762
|
+
const messages = await loadMessages(sessionId);
|
|
1763
|
+
const at = messages.findIndex((m) => m.uuid === uuid);
|
|
1764
|
+
if (at < 0) return void 0;
|
|
1765
|
+
return at === 0 ? null : messages[at - 1].uuid;
|
|
1766
|
+
}
|
|
1767
|
+
async function remove(sessionId) {
|
|
1768
|
+
if (config.demo) throw new Error("This is the demo: nothing changes.");
|
|
1769
|
+
const info = await sessionInfo(sessionId);
|
|
1770
|
+
await deleteSession(sessionId, info?.cwd ? { dir: info.cwd } : {});
|
|
1771
|
+
await refreshTranscriptIndex();
|
|
1772
|
+
}
|
|
1773
|
+
async function exportMarkdown(sessionId) {
|
|
1774
|
+
const info = await sessionInfo(sessionId);
|
|
1775
|
+
const title = info?.customTitle || info?.summary || "Claude Code session";
|
|
1776
|
+
const messages = await loadMessages(sessionId);
|
|
1777
|
+
const out = [`# ${title}`, ""];
|
|
1778
|
+
if (info?.cwd) out.push(`Folder: \`${info.cwd}\``, "");
|
|
1779
|
+
let speaker;
|
|
1780
|
+
for (const message of messages) {
|
|
1781
|
+
if (message.meta || message.parentToolUseId) continue;
|
|
1782
|
+
const parts = [];
|
|
1783
|
+
const tools = [];
|
|
1784
|
+
for (const block of message.blocks) {
|
|
1785
|
+
if (block.type === "text" && block.text.trim()) parts.push(block.text.trim());
|
|
1786
|
+
else if (block.type === "tool_use") tools.push(toolLine(block.name, block.input));
|
|
1787
|
+
}
|
|
1788
|
+
if (parts.length === 0 && tools.length === 0) continue;
|
|
1789
|
+
const who = message.role === "user" ? "You" : "Claude";
|
|
1790
|
+
if (who !== speaker) {
|
|
1791
|
+
out.push(`## ${who}`, "");
|
|
1792
|
+
speaker = who;
|
|
1793
|
+
}
|
|
1794
|
+
if (tools.length > 0) out.push(...tools, "");
|
|
1795
|
+
if (parts.length > 0) out.push(parts.join("\n\n"), "");
|
|
1796
|
+
}
|
|
1797
|
+
return { title, markdown: `${out.join("\n").trim()}
|
|
1798
|
+
` };
|
|
1799
|
+
}
|
|
1800
|
+
var toolLine;
|
|
1801
|
+
var init_actions = __esm({
|
|
1802
|
+
"src/server/actions.ts"() {
|
|
1803
|
+
"use strict";
|
|
1804
|
+
init_config();
|
|
1805
|
+
init_sessions();
|
|
1806
|
+
init_transcripts();
|
|
1807
|
+
toolLine = (name, input) => {
|
|
1808
|
+
const target = ["file_path", "command", "pattern", "url", "query", "description"].map((k) => input[k]).find((v) => typeof v === "string");
|
|
1809
|
+
return `- ${name}${target ? `: \`${String(target).split("\n")[0]}\`` : ""}`;
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
// src/server/feed.ts
|
|
1815
|
+
import { stat as stat4 } from "node:fs/promises";
|
|
1816
|
+
function feedFor(sessionId) {
|
|
1817
|
+
let feed = feeds.get(sessionId);
|
|
1818
|
+
if (!feed) {
|
|
1819
|
+
feed = new Feed(sessionId);
|
|
1820
|
+
feeds.set(sessionId, feed);
|
|
1821
|
+
}
|
|
1822
|
+
return feed;
|
|
1823
|
+
}
|
|
1824
|
+
function sessionsChanged() {
|
|
1825
|
+
if (changedTimer) return;
|
|
1826
|
+
changedTimer = setTimeout(() => {
|
|
1827
|
+
changedTimer = void 0;
|
|
1828
|
+
for (const listener of everyone) {
|
|
1829
|
+
try {
|
|
1830
|
+
listener({ type: "sessions_changed" });
|
|
1831
|
+
} catch {
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
}, 2e3);
|
|
1835
|
+
}
|
|
1836
|
+
function broadcastAll(event) {
|
|
1837
|
+
for (const listener of everyone) {
|
|
1838
|
+
try {
|
|
1839
|
+
listener(event);
|
|
1840
|
+
} catch {
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
function connect(listener) {
|
|
1845
|
+
everyone.add(listener);
|
|
1846
|
+
return () => everyone.delete(listener);
|
|
1847
|
+
}
|
|
1848
|
+
function subscribe(sessionId, listener) {
|
|
1849
|
+
const feed = feedFor(sessionId);
|
|
1850
|
+
feed.listeners.add(listener);
|
|
1851
|
+
feed.replayRecent(listener);
|
|
1852
|
+
feed.watch();
|
|
1853
|
+
return () => {
|
|
1854
|
+
feed.listeners.delete(listener);
|
|
1855
|
+
if (feed.listeners.size > 0) return;
|
|
1856
|
+
feed.unwatch();
|
|
1857
|
+
if (!feed.hasRecent && feed.publishing === 0 && feeds.get(sessionId) === feed) feeds.delete(sessionId);
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
var POLL_MS, Feed, feeds, everyone, changedTimer;
|
|
1861
|
+
var init_feed = __esm({
|
|
1862
|
+
"src/server/feed.ts"() {
|
|
1863
|
+
"use strict";
|
|
1864
|
+
init_sessions();
|
|
1865
|
+
init_transcripts();
|
|
1866
|
+
POLL_MS = 1e3;
|
|
1867
|
+
Feed = class {
|
|
1868
|
+
constructor(sessionId) {
|
|
1869
|
+
this.sessionId = sessionId;
|
|
1870
|
+
}
|
|
1871
|
+
sessionId;
|
|
1872
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1873
|
+
delivered = /* @__PURE__ */ new Set();
|
|
1874
|
+
/** Recently published messages, replayed to a client that subscribes late (a new session's first prompt). */
|
|
1875
|
+
recent = [];
|
|
1876
|
+
/** Ids last read from the file; one that disappears means the conversation was rewound. */
|
|
1877
|
+
fromFile = /* @__PURE__ */ new Set();
|
|
1878
|
+
primed;
|
|
1879
|
+
size = -1;
|
|
1880
|
+
mtimeMs = -1;
|
|
1881
|
+
timer;
|
|
1882
|
+
checking = false;
|
|
1883
|
+
async prime() {
|
|
1884
|
+
this.primed ??= (async () => {
|
|
1885
|
+
const file2 = await transcriptPath(this.sessionId);
|
|
1886
|
+
if (file2) {
|
|
1887
|
+
const info = await stat4(file2).catch(() => void 0);
|
|
1888
|
+
if (info) {
|
|
1889
|
+
this.size = info.size;
|
|
1890
|
+
this.mtimeMs = info.mtimeMs;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
for (const message of await loadMessages(this.sessionId)) {
|
|
1894
|
+
this.delivered.add(message.uuid);
|
|
1895
|
+
this.fromFile.add(message.uuid);
|
|
1896
|
+
}
|
|
1897
|
+
})();
|
|
1898
|
+
return this.primed;
|
|
1899
|
+
}
|
|
1900
|
+
broadcast(event) {
|
|
1901
|
+
for (const listener of this.listeners) {
|
|
1902
|
+
try {
|
|
1903
|
+
listener(event);
|
|
1904
|
+
} catch {
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
/** Publishes still on their way (priming reads the transcript first). */
|
|
1909
|
+
publishing = 0;
|
|
1910
|
+
async publish(message) {
|
|
1911
|
+
this.publishing += 1;
|
|
1912
|
+
try {
|
|
1913
|
+
await this.prime();
|
|
1914
|
+
if (this.delivered.has(message.uuid)) return;
|
|
1915
|
+
this.delivered.add(message.uuid);
|
|
1916
|
+
this.recent.push(message);
|
|
1917
|
+
if (this.recent.length > 40) this.recent.shift();
|
|
1918
|
+
this.broadcast({ type: "message", sessionId: this.sessionId, message });
|
|
1919
|
+
} finally {
|
|
1920
|
+
this.publishing -= 1;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
get hasRecent() {
|
|
1924
|
+
return this.recent.length > 0;
|
|
1925
|
+
}
|
|
1926
|
+
replayRecent(listener) {
|
|
1927
|
+
for (const message of this.recent) listener({ type: "message", sessionId: this.sessionId, message });
|
|
1928
|
+
}
|
|
1929
|
+
watch() {
|
|
1930
|
+
if (this.timer) return;
|
|
1931
|
+
void this.prime();
|
|
1932
|
+
this.timer = setInterval(() => {
|
|
1933
|
+
void this.check().then((changed) => {
|
|
1934
|
+
if (changed) sessionsChanged();
|
|
1935
|
+
});
|
|
1936
|
+
}, POLL_MS);
|
|
1937
|
+
}
|
|
1938
|
+
unwatch() {
|
|
1939
|
+
clearInterval(this.timer);
|
|
1940
|
+
this.timer = void 0;
|
|
1941
|
+
}
|
|
1942
|
+
get watching() {
|
|
1943
|
+
return Boolean(this.timer);
|
|
1944
|
+
}
|
|
1945
|
+
async check() {
|
|
1946
|
+
if (this.checking) return false;
|
|
1947
|
+
this.checking = true;
|
|
1948
|
+
try {
|
|
1949
|
+
await this.prime();
|
|
1950
|
+
const file2 = await transcriptPath(this.sessionId);
|
|
1951
|
+
if (!file2) return false;
|
|
1952
|
+
const info = await stat4(file2).catch(() => void 0);
|
|
1953
|
+
if (!info || info.size === this.size && info.mtimeMs === this.mtimeMs) return false;
|
|
1954
|
+
this.size = info.size;
|
|
1955
|
+
this.mtimeMs = info.mtimeMs;
|
|
1956
|
+
const messages = await loadMessages(this.sessionId);
|
|
1957
|
+
const present = new Set(messages.map((m) => m.uuid));
|
|
1958
|
+
for (const uuid of this.fromFile) {
|
|
1959
|
+
if (!present.has(uuid)) {
|
|
1960
|
+
this.fromFile = present;
|
|
1961
|
+
this.delivered = new Set(present);
|
|
1962
|
+
this.broadcast({ type: "reload", sessionId: this.sessionId });
|
|
1963
|
+
return true;
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
this.fromFile = present;
|
|
1967
|
+
for (const message of messages) {
|
|
1968
|
+
if (this.delivered.has(message.uuid)) continue;
|
|
1969
|
+
this.delivered.add(message.uuid);
|
|
1970
|
+
this.broadcast({ type: "message", sessionId: this.sessionId, message });
|
|
1971
|
+
}
|
|
1972
|
+
return true;
|
|
1973
|
+
} finally {
|
|
1974
|
+
this.checking = false;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
};
|
|
1978
|
+
feeds = /* @__PURE__ */ new Map();
|
|
1979
|
+
everyone = /* @__PURE__ */ new Set();
|
|
1980
|
+
}
|
|
1981
|
+
});
|
|
1982
|
+
|
|
1983
|
+
// src/server/files.ts
|
|
1984
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
1985
|
+
import { mkdir as mkdir5, readdir as readdir3, readFile as readFile4, rm as rm3, stat as stat5, writeFile as writeFile4 } from "node:fs/promises";
|
|
1986
|
+
import path9 from "node:path";
|
|
1987
|
+
async function saveUpload(file2) {
|
|
1988
|
+
if (file2.size > MAX_UPLOAD_BYTES) throw new Error(`File is larger than ${MAX_UPLOAD_BYTES / 1024 / 1024} MB`);
|
|
1989
|
+
const id = randomUUID3();
|
|
1990
|
+
const dir = path9.join(uploadsDir(), id);
|
|
1991
|
+
await mkdir5(dir, { recursive: true, mode: 448 });
|
|
1992
|
+
const name = safeName(file2.name);
|
|
1993
|
+
const filePath = path9.join(dir, name);
|
|
1994
|
+
await writeFile4(filePath, Buffer.from(await file2.arrayBuffer()), { mode: 384 });
|
|
1995
|
+
return { id, name, mime: file2.type || "application/octet-stream", size: file2.size, path: filePath };
|
|
1996
|
+
}
|
|
1997
|
+
async function findUpload(id) {
|
|
1998
|
+
if (!/^[0-9a-f-]{36}$/.test(id)) return void 0;
|
|
1999
|
+
const dir = path9.join(uploadsDir(), id);
|
|
2000
|
+
try {
|
|
2001
|
+
const [name] = await readdir3(dir);
|
|
2002
|
+
if (!name) return void 0;
|
|
2003
|
+
const filePath = path9.join(dir, name);
|
|
2004
|
+
const info = await stat5(filePath);
|
|
2005
|
+
return { id, name, mime: mimeOf(name), size: info.size, path: filePath };
|
|
2006
|
+
} catch {
|
|
2007
|
+
return void 0;
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
async function readUpload(id) {
|
|
2011
|
+
const attachment = await findUpload(id);
|
|
2012
|
+
if (!attachment) return void 0;
|
|
2013
|
+
return { attachment, data: await readFile4(attachment.path) };
|
|
2014
|
+
}
|
|
2015
|
+
async function listDirs(dir, showHidden) {
|
|
2016
|
+
const target = path9.resolve(dir || config.home);
|
|
2017
|
+
const entries = await readdir3(target, { withFileTypes: true });
|
|
2018
|
+
const dirs = entries.filter((e) => (e.isDirectory() || e.isSymbolicLink()) && (showHidden || !e.name.startsWith("."))).map((e) => ({ name: e.name, path: path9.join(target, e.name) })).sort((a, b) => a.name.localeCompare(b.name));
|
|
2019
|
+
const parent = path9.dirname(target);
|
|
2020
|
+
return { path: target, ...parent !== target ? { parent } : {}, dirs };
|
|
2021
|
+
}
|
|
2022
|
+
async function removeUpload(id) {
|
|
2023
|
+
if (!/^[0-9a-f-]{36}$/.test(id)) return;
|
|
2024
|
+
await rm3(path9.join(uploadsDir(), id), { recursive: true, force: true });
|
|
2025
|
+
}
|
|
2026
|
+
var uploadsDir, MAX_UPLOAD_BYTES, safeName, MIME, mimeOf;
|
|
2027
|
+
var init_files = __esm({
|
|
2028
|
+
"src/server/files.ts"() {
|
|
2029
|
+
"use strict";
|
|
2030
|
+
init_config();
|
|
2031
|
+
uploadsDir = () => path9.join(config.stateDir, "uploads");
|
|
2032
|
+
MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
|
|
2033
|
+
safeName = (name) => name.replace(/[/\\\0]/g, "_").replace(/^\.+/, "_").slice(0, 120) || "file";
|
|
2034
|
+
MIME = {
|
|
2035
|
+
png: "image/png",
|
|
2036
|
+
jpg: "image/jpeg",
|
|
2037
|
+
jpeg: "image/jpeg",
|
|
2038
|
+
gif: "image/gif",
|
|
2039
|
+
webp: "image/webp",
|
|
2040
|
+
pdf: "application/pdf",
|
|
2041
|
+
txt: "text/plain",
|
|
2042
|
+
md: "text/markdown",
|
|
2043
|
+
json: "application/json",
|
|
2044
|
+
csv: "text/csv"
|
|
2045
|
+
};
|
|
2046
|
+
mimeOf = (name) => MIME[name.split(".").pop()?.toLowerCase() ?? ""] ?? "application/octet-stream";
|
|
2047
|
+
}
|
|
2048
|
+
});
|
|
2049
|
+
|
|
2050
|
+
// src/server/trust.ts
|
|
2051
|
+
import { realpath as realpath2 } from "node:fs/promises";
|
|
2052
|
+
import os4 from "node:os";
|
|
2053
|
+
import { listSessions as listSessions2 } from "@anthropic-ai/claude-agent-sdk";
|
|
2054
|
+
async function claudeRanIn(folder) {
|
|
2055
|
+
const dir = await realpath2(folder).catch(() => folder);
|
|
2056
|
+
if (dir === os4.homedir()) return true;
|
|
2057
|
+
const cached = cache2.get(dir);
|
|
2058
|
+
if (cached && Date.now() - cached.at < (cached.value ? YES_MS : NO_MS)) return cached.value;
|
|
2059
|
+
let value = false;
|
|
2060
|
+
const sessions = await listSessions2({ dir, limit: 50, includeWorktrees: false }).catch(() => []);
|
|
2061
|
+
for (const session of sessions) {
|
|
2062
|
+
const file2 = await transcriptPath(session.sessionId);
|
|
2063
|
+
const recorded = file2 ? (await lastTurnTime(file2))?.lastTurn?.cwd : void 0;
|
|
2064
|
+
if (!recorded) continue;
|
|
2065
|
+
if (recorded === dir || await realpath2(recorded).catch(() => "") === dir) {
|
|
2066
|
+
value = true;
|
|
2067
|
+
break;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
if (cache2.size > 2e3) cache2.clear();
|
|
2071
|
+
cache2.set(dir, { at: Date.now(), value });
|
|
2072
|
+
return value;
|
|
2073
|
+
}
|
|
2074
|
+
var YES_MS, NO_MS, cache2;
|
|
2075
|
+
var init_trust = __esm({
|
|
2076
|
+
"src/server/trust.ts"() {
|
|
2077
|
+
"use strict";
|
|
2078
|
+
init_transcripts();
|
|
2079
|
+
YES_MS = 3e4;
|
|
2080
|
+
NO_MS = 3e3;
|
|
2081
|
+
cache2 = /* @__PURE__ */ new Map();
|
|
2082
|
+
}
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
// src/server/models.ts
|
|
2086
|
+
import { realpath as realpath3 } from "node:fs/promises";
|
|
2087
|
+
import os5 from "node:os";
|
|
2088
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
2089
|
+
async function slot(work) {
|
|
2090
|
+
if (running >= MAX_PROBES) await new Promise((resolve) => waiting.push(resolve));
|
|
2091
|
+
running += 1;
|
|
2092
|
+
try {
|
|
2093
|
+
return await work();
|
|
2094
|
+
} finally {
|
|
2095
|
+
running -= 1;
|
|
2096
|
+
waiting.shift()?.();
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
async function probe(key, cwd, ask) {
|
|
2100
|
+
const dir = await realpath3(cwd);
|
|
2101
|
+
const id = `${key}:${dir}`;
|
|
2102
|
+
const pending = inflight.get(id);
|
|
2103
|
+
if (pending) return pending;
|
|
2104
|
+
const work = slot(async () => {
|
|
2105
|
+
const q = query({
|
|
2106
|
+
prompt: idlePrompt,
|
|
2107
|
+
options: {
|
|
2108
|
+
cwd: dir,
|
|
2109
|
+
persistSession: false,
|
|
2110
|
+
settingSources: await claudeRanIn(dir) ? ["user", "project", "local"] : ["user"],
|
|
2111
|
+
...config.claudeExecutable ? { pathToClaudeCodeExecutable: config.claudeExecutable } : {}
|
|
2112
|
+
}
|
|
2113
|
+
});
|
|
2114
|
+
let timer;
|
|
2115
|
+
try {
|
|
2116
|
+
const timeout = new Promise((_, reject) => {
|
|
2117
|
+
timer = setTimeout(() => reject(new Error("Claude Code did not answer in time.")), PROBE_TIMEOUT_MS);
|
|
2118
|
+
});
|
|
2119
|
+
return await Promise.race([ask(q), timeout]);
|
|
2120
|
+
} finally {
|
|
2121
|
+
clearTimeout(timer);
|
|
2122
|
+
q.close();
|
|
2123
|
+
}
|
|
2124
|
+
}).finally(() => inflight.delete(id));
|
|
2125
|
+
inflight.set(id, work);
|
|
2126
|
+
return work;
|
|
2127
|
+
}
|
|
2128
|
+
function toOption(info) {
|
|
2129
|
+
return {
|
|
2130
|
+
value: info.value,
|
|
2131
|
+
label: info.displayName || info.value,
|
|
2132
|
+
description: info.description ?? "",
|
|
2133
|
+
efforts: info.supportedEffortLevels ?? [],
|
|
2134
|
+
supportsFast: Boolean(info.supportsFastMode),
|
|
2135
|
+
...info.supportsAutoMode !== void 0 ? { supportsAuto: info.supportsAutoMode } : {},
|
|
2136
|
+
...info.resolvedModel ? { resolvedModel: info.resolvedModel } : {}
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
async function listModels() {
|
|
2140
|
+
if (config.demo) return { models: DEMO_MODELS, signedIn: true, account: { email: "you@example.com", plan: "max" } };
|
|
2141
|
+
if (cache3 && Date.now() - cache3.at < TTL_MS) return cache3.value;
|
|
2142
|
+
const asking = probe("models", os5.homedir(), async (q) => {
|
|
2143
|
+
const models = (await q.supportedModels()).map(toOption);
|
|
2144
|
+
const account = await q.accountInfo().catch(() => void 0);
|
|
2145
|
+
const signedIn2 = account ? Boolean(account.email || account.tokenSource || account.apiKeySource || account.apiProvider) : null;
|
|
2146
|
+
const who = account?.email || account?.organization || account?.subscriptionType ? {
|
|
2147
|
+
...account.email ? { email: account.email } : {},
|
|
2148
|
+
...account.organization ? { organization: account.organization } : {},
|
|
2149
|
+
...account.subscriptionType ? { plan: account.subscriptionType } : {}
|
|
2150
|
+
} : void 0;
|
|
2151
|
+
return { models, signedIn: signedIn2, ...who ? { account: who } : {} };
|
|
2152
|
+
}).then((value) => {
|
|
2153
|
+
cache3 = { at: Date.now(), value };
|
|
2154
|
+
return value;
|
|
2155
|
+
});
|
|
2156
|
+
try {
|
|
2157
|
+
return await asking;
|
|
2158
|
+
} catch (error) {
|
|
2159
|
+
if (cache3) return cache3.value;
|
|
2160
|
+
throw error;
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
async function listCompletions(cwd) {
|
|
2164
|
+
if (config.demo) return { commands: DEMO_COMMANDS, agents: DEMO_AGENTS };
|
|
2165
|
+
const dir = await realpath3(cwd);
|
|
2166
|
+
const cached = completionCache.get(dir);
|
|
2167
|
+
if (cached && Date.now() - cached.at < TTL_MS) return cached.value;
|
|
2168
|
+
const value = await probe("completions", dir, async (q) => {
|
|
2169
|
+
const [commands, agents] = await Promise.all([q.supportedCommands(), q.supportedAgents().catch(() => [])]);
|
|
2170
|
+
return {
|
|
2171
|
+
commands: commands.map((c) => ({ name: c.name, description: c.description ?? "", argumentHint: c.argumentHint ?? "" })),
|
|
2172
|
+
agents: agents.map((a) => ({ name: a.name, description: a.description ?? "" }))
|
|
2173
|
+
};
|
|
2174
|
+
});
|
|
2175
|
+
completionCache.set(dir, { at: Date.now(), value });
|
|
2176
|
+
return value;
|
|
2177
|
+
}
|
|
2178
|
+
function toMcpServers(list2) {
|
|
2179
|
+
return list2.map((server) => ({
|
|
2180
|
+
name: server.name,
|
|
2181
|
+
status: server.status,
|
|
2182
|
+
...server.error ? { error: server.error } : {},
|
|
2183
|
+
tools: server.tools?.length ?? 0,
|
|
2184
|
+
...server.scope ? { scope: server.scope } : {}
|
|
2185
|
+
}));
|
|
2186
|
+
}
|
|
2187
|
+
async function probeMcpServers(cwd) {
|
|
2188
|
+
if (config.demo) return [{ name: "github", status: "connected", tools: 26, scope: "user" }, { name: "linear", status: "needs-auth", tools: 0, scope: "project" }];
|
|
2189
|
+
const dir = await realpath3(cwd);
|
|
2190
|
+
const cached = mcpCache.get(dir);
|
|
2191
|
+
if (cached && Date.now() - cached.at < 6e4) return cached.servers;
|
|
2192
|
+
const servers = toMcpServers(await probe("mcp", dir, (q) => q.mcpServerStatus()));
|
|
2193
|
+
mcpCache.set(dir, { at: Date.now(), servers });
|
|
2194
|
+
return servers;
|
|
2195
|
+
}
|
|
2196
|
+
async function planUsage() {
|
|
2197
|
+
if (config.demo) return DEMO_PLAN;
|
|
2198
|
+
if (planCache && Date.now() - planCache.at < 6e4) return planCache.value;
|
|
2199
|
+
const value = await probe("usage", os5.homedir(), async (q) => {
|
|
2200
|
+
const ask = q.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET;
|
|
2201
|
+
if (typeof ask !== "function") return null;
|
|
2202
|
+
const answer = await ask.call(q, { skipBehaviors: true });
|
|
2203
|
+
const limits3 = answer?.rate_limits;
|
|
2204
|
+
if (!answer?.rate_limits_available || !limits3) return null;
|
|
2205
|
+
const windows = [];
|
|
2206
|
+
const add = (type, window, label) => {
|
|
2207
|
+
if (!window || typeof window.utilization !== "number") return;
|
|
2208
|
+
const resetsAt = window.resets_at ? Date.parse(window.resets_at) : NaN;
|
|
2209
|
+
windows.push({ type, ...label ? { label } : {}, utilization: window.utilization / 100, ...Number.isFinite(resetsAt) ? { resetsAt } : {} });
|
|
2210
|
+
};
|
|
2211
|
+
add("five_hour", limits3.five_hour);
|
|
2212
|
+
add("seven_day", limits3.seven_day);
|
|
2213
|
+
add("seven_day_opus", limits3.seven_day_opus);
|
|
2214
|
+
add("seven_day_sonnet", limits3.seven_day_sonnet);
|
|
2215
|
+
for (const scoped of limits3.model_scoped ?? []) add(`model:${scoped.display_name}`, scoped, `Weekly ${scoped.display_name} limit`);
|
|
2216
|
+
const extra = limits3.extra_usage;
|
|
2217
|
+
return {
|
|
2218
|
+
windows,
|
|
2219
|
+
...extra ? {
|
|
2220
|
+
extra: {
|
|
2221
|
+
enabled: Boolean(extra.is_enabled),
|
|
2222
|
+
...typeof extra.used_credits === "number" ? { usedCents: extra.used_credits } : {},
|
|
2223
|
+
...typeof extra.monthly_limit === "number" ? { limitCents: extra.monthly_limit } : {},
|
|
2224
|
+
...typeof extra.utilization === "number" ? { utilization: extra.utilization / 100 } : {},
|
|
2225
|
+
...extra.currency ? { currency: extra.currency } : {}
|
|
2226
|
+
}
|
|
2227
|
+
} : {}
|
|
2228
|
+
};
|
|
2229
|
+
}).catch(() => null);
|
|
2230
|
+
planCache = { at: Date.now(), value };
|
|
2231
|
+
return value;
|
|
2232
|
+
}
|
|
2233
|
+
var TTL_MS, PROBE_TIMEOUT_MS, MAX_PROBES, idlePrompt, running, waiting, inflight, cache3, DEMO_COMMANDS, DEMO_AGENTS, completionCache, mcpCache, DEMO_PLAN, planCache;
|
|
2234
|
+
var init_models = __esm({
|
|
2235
|
+
"src/server/models.ts"() {
|
|
2236
|
+
"use strict";
|
|
2237
|
+
init_config();
|
|
2238
|
+
init_demo();
|
|
2239
|
+
init_trust();
|
|
2240
|
+
TTL_MS = 10 * 60 * 1e3;
|
|
2241
|
+
PROBE_TIMEOUT_MS = 3e4;
|
|
2242
|
+
MAX_PROBES = 2;
|
|
2243
|
+
idlePrompt = {
|
|
2244
|
+
[Symbol.asyncIterator]: () => ({ next: () => new Promise(() => {
|
|
2245
|
+
}) })
|
|
2246
|
+
};
|
|
2247
|
+
running = 0;
|
|
2248
|
+
waiting = [];
|
|
2249
|
+
inflight = /* @__PURE__ */ new Map();
|
|
2250
|
+
DEMO_COMMANDS = [
|
|
2251
|
+
{ name: "compact", description: "Summarize the conversation so far to free up context", argumentHint: "" },
|
|
2252
|
+
{ name: "review", description: "Review the changes on this branch", argumentHint: "" },
|
|
2253
|
+
{ name: "init", description: "Write a CLAUDE.md for this project", argumentHint: "" }
|
|
2254
|
+
];
|
|
2255
|
+
DEMO_AGENTS = [{ name: "code-reviewer", description: "Reviews a change for bugs and style" }];
|
|
2256
|
+
completionCache = /* @__PURE__ */ new Map();
|
|
2257
|
+
mcpCache = /* @__PURE__ */ new Map();
|
|
2258
|
+
DEMO_PLAN = {
|
|
2259
|
+
windows: [
|
|
2260
|
+
{ type: "five_hour", utilization: 0.42, resetsAt: Date.now() + 2.4 * 36e5 },
|
|
2261
|
+
{ type: "seven_day", utilization: 0.18, resetsAt: Date.now() + 3.5 * 864e5 }
|
|
2262
|
+
]
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
});
|
|
2266
|
+
|
|
2267
|
+
// src/server/git.ts
|
|
2268
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
2269
|
+
import { realpathSync as realpathSync2 } from "node:fs";
|
|
2270
|
+
import path10 from "node:path";
|
|
2271
|
+
import { promisify as promisify3 } from "node:util";
|
|
2272
|
+
function environment() {
|
|
2273
|
+
const env = { ...process.env, GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0", GIT_LITERAL_PATHSPECS: "1" };
|
|
2274
|
+
for (const name of Object.keys(env)) {
|
|
2275
|
+
if (/^GIT_(DIR|WORK_TREE|INDEX_FILE|COMMON_DIR|OBJECT_DIRECTORY|ALTERNATE_OBJECT_DIRECTORIES|NAMESPACE|CONFIG_PARAMETERS|CONFIG_COUNT|CONFIG_KEY_\d+|CONFIG_VALUE_\d+|CEILING_DIRECTORIES)$/.test(name)) delete env[name];
|
|
2276
|
+
}
|
|
2277
|
+
if (config.demo) {
|
|
2278
|
+
try {
|
|
2279
|
+
env.GIT_CEILING_DIRECTORIES = path10.dirname(realpathSync2(config.home));
|
|
2280
|
+
} catch {
|
|
2281
|
+
env.GIT_CEILING_DIRECTORIES = path10.dirname(config.home);
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
return env;
|
|
2285
|
+
}
|
|
2286
|
+
async function git(cwd, args, timeout = 15e3) {
|
|
2287
|
+
const command = args[0];
|
|
2288
|
+
const extra = command === "diff" ? ["--no-ext-diff", "--no-textconv", "--ignore-submodules=dirty"] : command === "status" ? ["--ignore-submodules=dirty"] : [];
|
|
2289
|
+
const { stdout } = await run3("git", [...SAFE, command, ...extra, ...args.slice(1)], { cwd, env: environment(), maxBuffer: 32 * 1024 * 1024, timeout });
|
|
2290
|
+
return stdout;
|
|
2291
|
+
}
|
|
2292
|
+
var run3, SAFE;
|
|
2293
|
+
var init_git = __esm({
|
|
2294
|
+
"src/server/git.ts"() {
|
|
2295
|
+
"use strict";
|
|
2296
|
+
init_config();
|
|
2297
|
+
run3 = promisify3(execFile3);
|
|
2298
|
+
SAFE = ["-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "-c", "safe.bareRepository=explicit", "-c", "core.quotepath=off"];
|
|
2299
|
+
}
|
|
2300
|
+
});
|
|
2301
|
+
|
|
2302
|
+
// src/server/project-files.ts
|
|
2303
|
+
import { readdir as readdir4 } from "node:fs/promises";
|
|
2304
|
+
import path11 from "node:path";
|
|
2305
|
+
async function projectFiles(cwd) {
|
|
2306
|
+
const cached = cache4.get(cwd);
|
|
2307
|
+
if (cached && Date.now() - cached.at < TTL_MS2) return cached.files;
|
|
2308
|
+
let files;
|
|
2309
|
+
try {
|
|
2310
|
+
if (!config.demo && !await claudeRanIn(cwd)) throw new Error("not trusted");
|
|
2311
|
+
const stdout = await git(cwd, ["ls-files", "--cached", "--others", "--exclude-standard"], 5e3);
|
|
2312
|
+
files = stdout.split("\n").filter(Boolean).slice(0, MAX_FILES2);
|
|
2313
|
+
} catch {
|
|
2314
|
+
files = [];
|
|
2315
|
+
const walk2 = async (dir, depth) => {
|
|
2316
|
+
if (depth > 8 || files.length >= MAX_FILES2) return;
|
|
2317
|
+
let entries;
|
|
2318
|
+
try {
|
|
2319
|
+
entries = await readdir4(path11.join(cwd, dir), { withFileTypes: true });
|
|
2320
|
+
} catch {
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
for (const entry of entries) {
|
|
2324
|
+
if (files.length >= MAX_FILES2) return;
|
|
2325
|
+
if (entry.name.startsWith(".") || SKIP.has(entry.name)) continue;
|
|
2326
|
+
const rel = dir ? `${dir}/${entry.name}` : entry.name;
|
|
2327
|
+
if (entry.isDirectory()) await walk2(rel, depth + 1);
|
|
2328
|
+
else files.push(rel);
|
|
2329
|
+
}
|
|
2330
|
+
};
|
|
2331
|
+
await walk2("", 0);
|
|
2332
|
+
}
|
|
2333
|
+
cache4.set(cwd, { at: Date.now(), files });
|
|
2334
|
+
return files;
|
|
2335
|
+
}
|
|
2336
|
+
function score(file2, query3) {
|
|
2337
|
+
const f = file2.toLowerCase();
|
|
2338
|
+
const q = query3.toLowerCase();
|
|
2339
|
+
const base = f.slice(f.lastIndexOf("/") + 1);
|
|
2340
|
+
if (base === q) return 1e3;
|
|
2341
|
+
if (base.startsWith(q)) return 800 - base.length;
|
|
2342
|
+
if (base.includes(q)) return 600 - base.length;
|
|
2343
|
+
if (f.includes(q)) return 400 - f.length / 10;
|
|
2344
|
+
let at = 0;
|
|
2345
|
+
for (const char of q) {
|
|
2346
|
+
at = f.indexOf(char, at);
|
|
2347
|
+
if (at === -1) return -1;
|
|
2348
|
+
at += 1;
|
|
2349
|
+
}
|
|
2350
|
+
return 100 - f.length / 10;
|
|
2351
|
+
}
|
|
2352
|
+
async function searchFiles(cwd, query3, limit = 30) {
|
|
2353
|
+
const files = await projectFiles(cwd);
|
|
2354
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
2355
|
+
for (const file2 of files) {
|
|
2356
|
+
let cut = file2.lastIndexOf("/");
|
|
2357
|
+
while (cut > 0) {
|
|
2358
|
+
dirs.add(`${file2.slice(0, cut)}/`);
|
|
2359
|
+
cut = file2.lastIndexOf("/", cut - 1);
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
const candidates = [...files, ...dirs];
|
|
2363
|
+
if (!query3) return files.slice().sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b)).slice(0, limit);
|
|
2364
|
+
return candidates.map((file2) => ({ file: file2, score: score(file2, query3) })).filter((m) => m.score >= 0).sort((a, b) => b.score - a.score || a.file.localeCompare(b.file)).slice(0, limit).map((m) => m.file);
|
|
2365
|
+
}
|
|
2366
|
+
var cache4, TTL_MS2, SKIP, MAX_FILES2;
|
|
2367
|
+
var init_project_files = __esm({
|
|
2368
|
+
"src/server/project-files.ts"() {
|
|
2369
|
+
"use strict";
|
|
2370
|
+
init_config();
|
|
2371
|
+
init_git();
|
|
2372
|
+
init_trust();
|
|
2373
|
+
cache4 = /* @__PURE__ */ new Map();
|
|
2374
|
+
TTL_MS2 = 3e4;
|
|
2375
|
+
SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", ".venv", "venv", "__pycache__", "target", ".cache"]);
|
|
2376
|
+
MAX_FILES2 = 2e4;
|
|
2377
|
+
}
|
|
2378
|
+
});
|
|
2379
|
+
|
|
2380
|
+
// src/server/push.ts
|
|
2381
|
+
import { mkdir as mkdir6, readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
|
|
2382
|
+
import path12 from "node:path";
|
|
2383
|
+
import webpush from "web-push";
|
|
2384
|
+
async function save(file2, value) {
|
|
2385
|
+
await mkdir6(path12.dirname(file2), { recursive: true, mode: 448 });
|
|
2386
|
+
await writeFile5(file2, JSON.stringify(value, null, 2), { mode: 384 });
|
|
2387
|
+
}
|
|
2388
|
+
async function initPush() {
|
|
2389
|
+
try {
|
|
2390
|
+
keys = JSON.parse(await readFile5(keysFile(), "utf8"));
|
|
2391
|
+
} catch {
|
|
2392
|
+
keys = webpush.generateVAPIDKeys();
|
|
2393
|
+
await save(keysFile(), keys);
|
|
2394
|
+
}
|
|
2395
|
+
webpush.setVapidDetails("https://github.com/Ki-Seki/pelican", keys.publicKey, keys.privateKey);
|
|
2396
|
+
try {
|
|
2397
|
+
subscriptions = JSON.parse(await readFile5(subsFile(), "utf8"));
|
|
2398
|
+
} catch {
|
|
2399
|
+
subscriptions = [];
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
function pushKey() {
|
|
2403
|
+
return keys?.publicKey;
|
|
2404
|
+
}
|
|
2405
|
+
async function addSubscription(subscription) {
|
|
2406
|
+
subscriptions = subscriptions.filter((s) => s.endpoint !== subscription.endpoint);
|
|
2407
|
+
subscriptions.push(subscription);
|
|
2408
|
+
await save(subsFile(), subscriptions);
|
|
2409
|
+
}
|
|
2410
|
+
async function removeSubscription(endpoint) {
|
|
2411
|
+
subscriptions = subscriptions.filter((s) => s.endpoint !== endpoint);
|
|
2412
|
+
await save(subsFile(), subscriptions);
|
|
2413
|
+
}
|
|
2414
|
+
async function notify(payload) {
|
|
2415
|
+
if (!keys || subscriptions.length === 0) return;
|
|
2416
|
+
const gone = [];
|
|
2417
|
+
await Promise.all(subscriptions.map(async (subscription) => {
|
|
2418
|
+
const id = encodeURIComponent(payload.sessionId);
|
|
2419
|
+
const url = config.sessionUrl ? config.sessionUrl.replaceAll("{id}", id) : subscription.openUrl ? `${subscription.openUrl}${id}` : void 0;
|
|
2420
|
+
const body = JSON.stringify({ ...payload, url });
|
|
2421
|
+
try {
|
|
2422
|
+
await webpush.sendNotification(subscription, body, { TTL: 3600, urgency: "high" });
|
|
2423
|
+
} catch (error) {
|
|
2424
|
+
const status = error.statusCode;
|
|
2425
|
+
if (status === 404 || status === 410) gone.push(subscription.endpoint);
|
|
2426
|
+
else console.warn("[pelican] push failed", status ?? error.message);
|
|
2427
|
+
}
|
|
2428
|
+
}));
|
|
2429
|
+
if (gone.length > 0) {
|
|
2430
|
+
subscriptions = subscriptions.filter((s) => !gone.includes(s.endpoint));
|
|
2431
|
+
await save(subsFile(), subscriptions);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
var keysFile, subsFile, keys, subscriptions;
|
|
2435
|
+
var init_push = __esm({
|
|
2436
|
+
"src/server/push.ts"() {
|
|
2437
|
+
"use strict";
|
|
2438
|
+
init_config();
|
|
2439
|
+
keysFile = () => path12.join(config.stateDir, "vapid.json");
|
|
2440
|
+
subsFile = () => path12.join(config.stateDir, "push-subscriptions.json");
|
|
2441
|
+
subscriptions = [];
|
|
2442
|
+
}
|
|
2443
|
+
});
|
|
2444
|
+
|
|
2445
|
+
// src/server/shell.ts
|
|
2446
|
+
import { spawn } from "node:child_process";
|
|
2447
|
+
function runCommand(cwd, command) {
|
|
2448
|
+
const shell = process.env.SHELL || "/bin/bash";
|
|
2449
|
+
const started = Date.now();
|
|
2450
|
+
return new Promise((resolve) => {
|
|
2451
|
+
let stdout = "";
|
|
2452
|
+
let stderr = "";
|
|
2453
|
+
let truncated = false;
|
|
2454
|
+
const child = spawn(shell, ["-lc", command], { cwd, env: { ...process.env, PAGER: "cat", GIT_PAGER: "cat", TERM: "dumb" }, stdio: ["ignore", "pipe", "pipe"] });
|
|
2455
|
+
const take = (current, chunk) => {
|
|
2456
|
+
if (current.length >= MAX_OUTPUT) {
|
|
2457
|
+
truncated = true;
|
|
2458
|
+
return current;
|
|
2459
|
+
}
|
|
2460
|
+
const next = current + chunk.toString("utf8");
|
|
2461
|
+
if (next.length > MAX_OUTPUT) truncated = true;
|
|
2462
|
+
return next.slice(0, MAX_OUTPUT);
|
|
2463
|
+
};
|
|
2464
|
+
child.stdout.on("data", (chunk) => {
|
|
2465
|
+
stdout = take(stdout, chunk);
|
|
2466
|
+
});
|
|
2467
|
+
child.stderr.on("data", (chunk) => {
|
|
2468
|
+
stderr = take(stderr, chunk);
|
|
2469
|
+
});
|
|
2470
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), TIMEOUT_MS);
|
|
2471
|
+
const finish = (code, error) => {
|
|
2472
|
+
clearTimeout(timer);
|
|
2473
|
+
resolve({
|
|
2474
|
+
command,
|
|
2475
|
+
stdout,
|
|
2476
|
+
stderr: error ? `${stderr}${error.message}` : stderr,
|
|
2477
|
+
code,
|
|
2478
|
+
durationMs: Date.now() - started,
|
|
2479
|
+
...truncated ? { truncated: true } : {}
|
|
2480
|
+
});
|
|
2481
|
+
};
|
|
2482
|
+
child.on("error", (error) => finish(null, error));
|
|
2483
|
+
child.on("close", (code, signal) => finish(signal ? null : code));
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
function bashModeText(result2) {
|
|
2487
|
+
const note2 = result2.code === null ? "\n(stopped after two minutes)" : result2.code !== 0 ? `
|
|
2488
|
+
(exit code ${result2.code})` : "";
|
|
2489
|
+
return `<bash-input>${escape(result2.command)}</bash-input><bash-stdout>${escape(result2.stdout)}</bash-stdout><bash-stderr>${escape(result2.stderr + note2)}</bash-stderr>`;
|
|
2490
|
+
}
|
|
2491
|
+
var MAX_OUTPUT, TIMEOUT_MS, escape;
|
|
2492
|
+
var init_shell = __esm({
|
|
2493
|
+
"src/server/shell.ts"() {
|
|
2494
|
+
"use strict";
|
|
2495
|
+
MAX_OUTPUT = 2e5;
|
|
2496
|
+
TIMEOUT_MS = 2 * 6e4;
|
|
2497
|
+
escape = (text2) => text2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2498
|
+
}
|
|
2499
|
+
});
|
|
2500
|
+
|
|
2501
|
+
// src/server/usage.ts
|
|
2502
|
+
function recordLimit(info) {
|
|
2503
|
+
const type = info.rateLimitType ?? "unknown";
|
|
2504
|
+
limits.set(type, {
|
|
2505
|
+
type,
|
|
2506
|
+
status: info.status,
|
|
2507
|
+
...typeof info.utilization === "number" ? { utilization: info.utilization } : {},
|
|
2508
|
+
...typeof info.resetsAt === "number" ? { resetsAt: info.resetsAt < 1e12 ? info.resetsAt * 1e3 : info.resetsAt } : {},
|
|
2509
|
+
at: Date.now()
|
|
2510
|
+
});
|
|
2511
|
+
}
|
|
2512
|
+
function usageLimits() {
|
|
2513
|
+
const now = Date.now();
|
|
2514
|
+
return [...limits.values()].filter((limit) => !limit.resetsAt || limit.resetsAt > now);
|
|
2515
|
+
}
|
|
2516
|
+
var limits;
|
|
2517
|
+
var init_usage = __esm({
|
|
2518
|
+
"src/server/usage.ts"() {
|
|
2519
|
+
"use strict";
|
|
2520
|
+
limits = /* @__PURE__ */ new Map();
|
|
2521
|
+
}
|
|
2522
|
+
});
|
|
2523
|
+
|
|
2524
|
+
// src/server/runner.ts
|
|
2525
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
2526
|
+
import { readFile as readFile6, stat as stat6 } from "node:fs/promises";
|
|
2527
|
+
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
2528
|
+
function readableError(text2) {
|
|
2529
|
+
if (text2.includes("Resume rejected by --resume-drops-turn")) {
|
|
2530
|
+
return "The conversation changed after that prompt (another app added to it), so the prompt was not replaced. Reload, then edit it again or send a new message.";
|
|
2531
|
+
}
|
|
2532
|
+
return text2;
|
|
2533
|
+
}
|
|
2534
|
+
function getRun(sessionId) {
|
|
2535
|
+
const run4 = runs.get(sessionId);
|
|
2536
|
+
return run4 && !run4.closed ? run4 : void 0;
|
|
2537
|
+
}
|
|
2538
|
+
function runStatus(sessionId) {
|
|
2539
|
+
if (config.demo) return sessionId === DEMO_WAITING_SESSION ? "waiting" : void 0;
|
|
2540
|
+
return getRun(sessionId)?.status();
|
|
2541
|
+
}
|
|
2542
|
+
function runState(sessionId) {
|
|
2543
|
+
if (config.demo) return demoRunState(sessionId);
|
|
2544
|
+
return getRun(sessionId)?.state();
|
|
2545
|
+
}
|
|
2546
|
+
async function isDirectory2(dir) {
|
|
2547
|
+
return stat6(dir).then((s) => s.isDirectory(), () => false);
|
|
2548
|
+
}
|
|
2549
|
+
async function staleRun(run4) {
|
|
2550
|
+
if (run4.busy || run4.hasBackgroundTasks) return false;
|
|
2551
|
+
const other = (await liveProcesses()).get(run4.sessionId);
|
|
2552
|
+
if (other?.updatedAt && other.updatedAt > run4.lastActive) return true;
|
|
2553
|
+
const file2 = await transcriptPath(run4.sessionId);
|
|
2554
|
+
const last = file2 ? (await lastTurnTime(file2))?.lastTurn : void 0;
|
|
2555
|
+
return Boolean(last && !last.copied && last.time > run4.lastActive + 2e3);
|
|
2556
|
+
}
|
|
2557
|
+
async function currentRun(sessionId) {
|
|
2558
|
+
const run4 = getRun(sessionId);
|
|
2559
|
+
if (run4 && await staleRun(run4)) {
|
|
2560
|
+
run4.close();
|
|
2561
|
+
if (runs.get(sessionId) === run4) runs.delete(sessionId);
|
|
2562
|
+
return getRun(sessionId);
|
|
2563
|
+
}
|
|
2564
|
+
return run4;
|
|
2565
|
+
}
|
|
2566
|
+
async function sendMessage(target, input) {
|
|
2567
|
+
if (config.demo) throw new Error("This is the demo: nothing runs. Start Pelican without --demo to talk to Claude.");
|
|
2568
|
+
if (target.sessionId) {
|
|
2569
|
+
const existing = await currentRun(target.sessionId);
|
|
2570
|
+
if (existing) {
|
|
2571
|
+
await existing.send(input);
|
|
2572
|
+
return existing.sessionId;
|
|
2573
|
+
}
|
|
2574
|
+
const info = await sessionInfo(target.sessionId);
|
|
2575
|
+
const cwd2 = info?.cwd ?? target.cwd;
|
|
2576
|
+
if (!cwd2 || !await isDirectory2(cwd2)) throw new Error(`The session's folder is missing: ${cwd2 ?? "(unknown)"}`);
|
|
2577
|
+
const started = getRun(target.sessionId);
|
|
2578
|
+
if (started) {
|
|
2579
|
+
await started.send(input);
|
|
2580
|
+
return started.sessionId;
|
|
2581
|
+
}
|
|
2582
|
+
const run5 = new SessionRun(target.sessionId, cwd2, input.settings, true);
|
|
2583
|
+
runs.set(run5.sessionId, run5);
|
|
2584
|
+
run5.start();
|
|
2585
|
+
await run5.send(input);
|
|
2586
|
+
return run5.sessionId;
|
|
2587
|
+
}
|
|
2588
|
+
const cwd = target.cwd;
|
|
2589
|
+
if (!cwd || !await isDirectory2(cwd)) throw new Error(`Not a folder: ${cwd ?? "(none)"}`);
|
|
2590
|
+
const run4 = new SessionRun(randomUUID4(), cwd, input.settings, false);
|
|
2591
|
+
runs.set(run4.sessionId, run4);
|
|
2592
|
+
run4.start();
|
|
2593
|
+
await run4.send(input);
|
|
2594
|
+
return run4.sessionId;
|
|
2595
|
+
}
|
|
2596
|
+
async function replaceLastTurn(sessionId, cwd, entry, prompt, input) {
|
|
2597
|
+
if (config.demo) throw new Error("This is the demo: nothing runs.");
|
|
2598
|
+
const existing = getRun(sessionId);
|
|
2599
|
+
if (existing?.busy) throw new Error("Stop Claude before editing a prompt.");
|
|
2600
|
+
if (existing) {
|
|
2601
|
+
existing.close();
|
|
2602
|
+
runs.delete(sessionId);
|
|
2603
|
+
}
|
|
2604
|
+
const run4 = new SessionRun(sessionId, cwd, input.settings, true, { entry, dropsTurn: prompt });
|
|
2605
|
+
runs.set(sessionId, run4);
|
|
2606
|
+
run4.start();
|
|
2607
|
+
await run4.send(input);
|
|
2608
|
+
return sessionId;
|
|
2609
|
+
}
|
|
2610
|
+
async function ensureRun(sessionId, settings = {}) {
|
|
2611
|
+
if (config.demo) throw new Error("This is the demo: nothing runs.");
|
|
2612
|
+
const existing = await currentRun(sessionId);
|
|
2613
|
+
if (existing) return existing;
|
|
2614
|
+
const info = await sessionInfo(sessionId);
|
|
2615
|
+
const cwd = info?.cwd;
|
|
2616
|
+
if (!cwd || !await isDirectory2(cwd)) throw new Error(`The session's folder is missing: ${cwd ?? "(unknown)"}`);
|
|
2617
|
+
const started = getRun(sessionId);
|
|
2618
|
+
if (started) return started;
|
|
2619
|
+
const run4 = new SessionRun(sessionId, cwd, settings, true);
|
|
2620
|
+
runs.set(sessionId, run4);
|
|
2621
|
+
run4.start();
|
|
2622
|
+
return run4;
|
|
2623
|
+
}
|
|
2624
|
+
function closeAll() {
|
|
2625
|
+
for (const run4 of runs.values()) run4.close();
|
|
2626
|
+
}
|
|
2627
|
+
var InputQueue, sdkModel, SessionRun, runs;
|
|
2628
|
+
var init_runner = __esm({
|
|
2629
|
+
"src/server/runner.ts"() {
|
|
2630
|
+
"use strict";
|
|
2631
|
+
init_config();
|
|
2632
|
+
init_demo();
|
|
2633
|
+
init_models();
|
|
2634
|
+
init_feed();
|
|
2635
|
+
init_live();
|
|
2636
|
+
init_normalize();
|
|
2637
|
+
init_push();
|
|
2638
|
+
init_sessions();
|
|
2639
|
+
init_shell();
|
|
2640
|
+
init_transcripts();
|
|
2641
|
+
init_usage();
|
|
2642
|
+
InputQueue = class {
|
|
2643
|
+
items = [];
|
|
2644
|
+
waiters = [];
|
|
2645
|
+
done = false;
|
|
2646
|
+
push(item) {
|
|
2647
|
+
const waiter = this.waiters.shift();
|
|
2648
|
+
if (waiter) waiter({ value: item, done: false });
|
|
2649
|
+
else this.items.push(item);
|
|
2650
|
+
}
|
|
2651
|
+
close() {
|
|
2652
|
+
this.done = true;
|
|
2653
|
+
for (const waiter of this.waiters.splice(0)) waiter({ value: void 0, done: true });
|
|
2654
|
+
}
|
|
2655
|
+
[Symbol.asyncIterator]() {
|
|
2656
|
+
return {
|
|
2657
|
+
next: () => {
|
|
2658
|
+
const item = this.items.shift();
|
|
2659
|
+
if (item) return Promise.resolve({ value: item, done: false });
|
|
2660
|
+
if (this.done) return Promise.resolve({ value: void 0, done: true });
|
|
2661
|
+
return new Promise((resolve) => this.waiters.push(resolve));
|
|
2662
|
+
}
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
};
|
|
2666
|
+
sdkModel = (model) => model && model !== "default" ? model : void 0;
|
|
2667
|
+
SessionRun = class {
|
|
2668
|
+
constructor(sessionId, cwd, settings, resume, resumeAt) {
|
|
2669
|
+
this.sessionId = sessionId;
|
|
2670
|
+
this.cwd = cwd;
|
|
2671
|
+
this.settings = settings;
|
|
2672
|
+
this.resume = resume;
|
|
2673
|
+
this.resumeAt = resumeAt;
|
|
2674
|
+
}
|
|
2675
|
+
sessionId;
|
|
2676
|
+
cwd;
|
|
2677
|
+
settings;
|
|
2678
|
+
resume;
|
|
2679
|
+
resumeAt;
|
|
2680
|
+
busy = false;
|
|
2681
|
+
closed = false;
|
|
2682
|
+
error;
|
|
2683
|
+
lastActive = Date.now();
|
|
2684
|
+
partial = [];
|
|
2685
|
+
completedBlocks = 0;
|
|
2686
|
+
permissions = /* @__PURE__ */ new Map();
|
|
2687
|
+
input = new InputQueue();
|
|
2688
|
+
q;
|
|
2689
|
+
emitTimer;
|
|
2690
|
+
title = "";
|
|
2691
|
+
/** Set by Stop: the turn then ends with an error result that is not a failure. */
|
|
2692
|
+
interrupted = false;
|
|
2693
|
+
lastTurn;
|
|
2694
|
+
context;
|
|
2695
|
+
activity;
|
|
2696
|
+
tasks = [];
|
|
2697
|
+
get hasBackgroundTasks() {
|
|
2698
|
+
return this.tasks.length > 0;
|
|
2699
|
+
}
|
|
2700
|
+
/** Messages waiting for the turn to end, published when it does. */
|
|
2701
|
+
queued = [];
|
|
2702
|
+
started;
|
|
2703
|
+
/** Settles once Claude has started (its first message), or fails when it ends before that. */
|
|
2704
|
+
ready = new Promise((resolve, reject) => {
|
|
2705
|
+
this.started = { resolve, reject };
|
|
2706
|
+
});
|
|
2707
|
+
// Most callers never wait for it: a failure here must not count as unhandled.
|
|
2708
|
+
readyHandled = this.ready.catch(() => void 0);
|
|
2709
|
+
suggestion;
|
|
2710
|
+
start() {
|
|
2711
|
+
const settings = this.settings;
|
|
2712
|
+
const options = {
|
|
2713
|
+
cwd: this.cwd,
|
|
2714
|
+
...this.resume ? { resume: this.sessionId } : { sessionId: this.sessionId },
|
|
2715
|
+
// Claude Code checks that everything past the entry belongs to the dropped turn, so a
|
|
2716
|
+
// message another app added meanwhile is never thrown away unseen.
|
|
2717
|
+
...this.resume && this.resumeAt ? { resumeSessionAt: this.resumeAt.entry, resumeDropsTurn: this.resumeAt.dropsTurn } : {},
|
|
2718
|
+
...sdkModel(settings.model) ? { model: sdkModel(settings.model) } : {},
|
|
2719
|
+
...settings.effort ? { effort: settings.effort } : {},
|
|
2720
|
+
permissionMode: settings.permissionMode ?? "default",
|
|
2721
|
+
allowDangerouslySkipPermissions: true,
|
|
2722
|
+
...settings.fast || settings.ultracode ? {
|
|
2723
|
+
settings: {
|
|
2724
|
+
...settings.fast ? { fastMode: true } : {},
|
|
2725
|
+
// Ultracode needs workflows; both are session settings, like the CLI's --settings.
|
|
2726
|
+
...settings.ultracode ? { ultracode: true, enableWorkflows: true } : {}
|
|
2727
|
+
}
|
|
2728
|
+
} : {},
|
|
2729
|
+
includePartialMessages: true,
|
|
2730
|
+
// Lets "Rewind code" put files back the way they were before a prompt.
|
|
2731
|
+
enableFileCheckpointing: true,
|
|
2732
|
+
promptSuggestions: true,
|
|
2733
|
+
settingSources: ["user", "project", "local"],
|
|
2734
|
+
systemPrompt: { type: "preset", preset: "claude_code" },
|
|
2735
|
+
tools: { type: "preset", preset: "claude_code" },
|
|
2736
|
+
canUseTool: this.canUseTool,
|
|
2737
|
+
...config.claudeExecutable ? { pathToClaudeCodeExecutable: config.claudeExecutable } : {},
|
|
2738
|
+
stderr: (data) => {
|
|
2739
|
+
if (process.env.PELICAN_DEBUG) process.stderr.write(`[claude ${this.sessionId.slice(0, 8)}] ${data}`);
|
|
2740
|
+
}
|
|
2741
|
+
};
|
|
2742
|
+
this.q = query2({ prompt: this.input, options });
|
|
2743
|
+
void this.pump();
|
|
2744
|
+
void sessionInfo(this.sessionId).then((info) => {
|
|
2745
|
+
if (info) this.title = info.customTitle || info.summary || "";
|
|
2746
|
+
});
|
|
2747
|
+
}
|
|
2748
|
+
state() {
|
|
2749
|
+
return {
|
|
2750
|
+
sessionId: this.sessionId,
|
|
2751
|
+
cwd: this.cwd,
|
|
2752
|
+
busy: this.busy,
|
|
2753
|
+
settings: this.settings,
|
|
2754
|
+
permissions: [...this.permissions.values()].map((p) => p.request),
|
|
2755
|
+
partial: this.partial.slice(this.completedBlocks).filter(Boolean),
|
|
2756
|
+
...this.error ? { error: this.error } : {},
|
|
2757
|
+
...this.lastTurn ? { lastTurn: this.lastTurn } : {},
|
|
2758
|
+
...this.context ? { context: this.context } : {},
|
|
2759
|
+
...this.activity ? { activity: this.activity } : {},
|
|
2760
|
+
...this.tasks.length > 0 ? { tasks: this.tasks } : {},
|
|
2761
|
+
...this.queued.length > 0 ? { queued: this.queued.map((m) => m.uuid) } : {},
|
|
2762
|
+
...this.suggestion && !this.busy ? { suggestion: this.suggestion } : {}
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
status() {
|
|
2766
|
+
if (this.permissions.size > 0) return "waiting";
|
|
2767
|
+
return this.busy ? "running" : "idle";
|
|
2768
|
+
}
|
|
2769
|
+
/** Sends the run state now, or within 80 ms when called for streamed text. */
|
|
2770
|
+
emit(soon = false) {
|
|
2771
|
+
if (soon) {
|
|
2772
|
+
if (this.emitTimer) return;
|
|
2773
|
+
this.emitTimer = setTimeout(() => {
|
|
2774
|
+
this.emitTimer = void 0;
|
|
2775
|
+
this.emitNow();
|
|
2776
|
+
}, 80);
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2779
|
+
clearTimeout(this.emitTimer);
|
|
2780
|
+
this.emitTimer = void 0;
|
|
2781
|
+
this.emitNow();
|
|
2782
|
+
}
|
|
2783
|
+
lastStatus;
|
|
2784
|
+
emitNow() {
|
|
2785
|
+
const state = this.state();
|
|
2786
|
+
feedFor(this.sessionId).broadcast({ type: "run", state });
|
|
2787
|
+
const status = this.status();
|
|
2788
|
+
if (status !== this.lastStatus) {
|
|
2789
|
+
this.lastStatus = status;
|
|
2790
|
+
broadcastAll({ type: "status", sessionId: this.sessionId, status });
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
setBusy(busy) {
|
|
2794
|
+
if (this.busy === busy) return;
|
|
2795
|
+
this.busy = busy;
|
|
2796
|
+
this.lastActive = Date.now();
|
|
2797
|
+
if (!busy) {
|
|
2798
|
+
this.partial = [];
|
|
2799
|
+
this.completedBlocks = 0;
|
|
2800
|
+
}
|
|
2801
|
+
this.emit();
|
|
2802
|
+
sessionsChanged();
|
|
2803
|
+
}
|
|
2804
|
+
async send(input) {
|
|
2805
|
+
await this.applySettings(input.settings);
|
|
2806
|
+
const uuid = input.uuid ?? randomUUID4();
|
|
2807
|
+
const content = [];
|
|
2808
|
+
const fileLines = [];
|
|
2809
|
+
for (const attachment of input.attachments) {
|
|
2810
|
+
if (attachment.mime.startsWith("image/") && attachment.size <= 5 * 1024 * 1024) {
|
|
2811
|
+
const data = await readFile6(attachment.path).catch(() => void 0);
|
|
2812
|
+
if (data) {
|
|
2813
|
+
content.push({ type: "image", source: { type: "base64", media_type: attachment.mime, data: data.toString("base64") } });
|
|
2814
|
+
continue;
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
fileLines.push(`- ${attachment.path} (${attachment.name})`);
|
|
2818
|
+
}
|
|
2819
|
+
let text2 = input.text;
|
|
2820
|
+
if (fileLines.length > 0) text2 = `${text2}${text2 ? "\n\n" : ""}Attached files:
|
|
2821
|
+
${fileLines.join("\n")}`;
|
|
2822
|
+
if (text2) content.push({ type: "text", text: text2 });
|
|
2823
|
+
const queue = this.busy && input.mode === "queue";
|
|
2824
|
+
const message = {
|
|
2825
|
+
type: "user",
|
|
2826
|
+
uuid,
|
|
2827
|
+
session_id: this.sessionId,
|
|
2828
|
+
parent_tool_use_id: null,
|
|
2829
|
+
origin: { kind: "human" },
|
|
2830
|
+
...this.busy ? { priority: queue ? "later" : "next" } : {},
|
|
2831
|
+
message: { role: "user", content }
|
|
2832
|
+
};
|
|
2833
|
+
this.error = void 0;
|
|
2834
|
+
this.suggestion = void 0;
|
|
2835
|
+
const chat = toChatMessage({ type: "user", uuid, message: { role: "user", content } });
|
|
2836
|
+
if (queue) {
|
|
2837
|
+
if (chat) this.queued.push({ ...chat, timestamp: Date.now() });
|
|
2838
|
+
this.emit();
|
|
2839
|
+
} else {
|
|
2840
|
+
this.setBusy(true);
|
|
2841
|
+
if (chat) await feedFor(this.sessionId).publish({ ...chat, timestamp: Date.now() });
|
|
2842
|
+
}
|
|
2843
|
+
this.input.push(message);
|
|
2844
|
+
}
|
|
2845
|
+
/**
|
|
2846
|
+
* Adds a `!` command and its output to the conversation without starting a
|
|
2847
|
+
* turn, the way Claude Code's bash mode does: Claude reads it with the next message.
|
|
2848
|
+
*/
|
|
2849
|
+
async appendShell(result2) {
|
|
2850
|
+
const uuid = randomUUID4();
|
|
2851
|
+
const text2 = bashModeText(result2);
|
|
2852
|
+
const message = {
|
|
2853
|
+
type: "user",
|
|
2854
|
+
uuid,
|
|
2855
|
+
session_id: this.sessionId,
|
|
2856
|
+
parent_tool_use_id: null,
|
|
2857
|
+
origin: { kind: "human" },
|
|
2858
|
+
shouldQuery: false,
|
|
2859
|
+
message: { role: "user", content: text2 }
|
|
2860
|
+
};
|
|
2861
|
+
const chat = toChatMessage({ type: "user", uuid, message: { role: "user", content: text2 } });
|
|
2862
|
+
if (chat) await feedFor(this.sessionId).publish({ ...chat, timestamp: Date.now() });
|
|
2863
|
+
this.input.push(message);
|
|
2864
|
+
}
|
|
2865
|
+
/** Puts files back the way they were before the prompt `uuid`; `dryRun` only says what would change. */
|
|
2866
|
+
async rewind(uuid, dryRun) {
|
|
2867
|
+
if (!this.q) throw new Error("Claude is not running in this session");
|
|
2868
|
+
const result2 = await this.q.rewindFiles(uuid, { dryRun });
|
|
2869
|
+
return {
|
|
2870
|
+
canRewind: result2.canRewind,
|
|
2871
|
+
...result2.error ? { error: result2.error } : {},
|
|
2872
|
+
...result2.filesChanged ? { files: result2.filesChanged } : {},
|
|
2873
|
+
...typeof result2.insertions === "number" ? { insertions: result2.insertions } : {},
|
|
2874
|
+
...typeof result2.deletions === "number" ? { deletions: result2.deletions } : {}
|
|
2875
|
+
};
|
|
2876
|
+
}
|
|
2877
|
+
async mcpServers() {
|
|
2878
|
+
if (!this.q) return [];
|
|
2879
|
+
return toMcpServers(await this.q.mcpServerStatus());
|
|
2880
|
+
}
|
|
2881
|
+
async setMcpServer(name, change) {
|
|
2882
|
+
if (!this.q) throw new Error("Claude is not running in this session");
|
|
2883
|
+
if (change.reconnect) await this.q.reconnectMcpServer(name);
|
|
2884
|
+
if (change.enabled !== void 0) await this.q.toggleMcpServer(name, change.enabled);
|
|
2885
|
+
}
|
|
2886
|
+
async stopTask(taskId) {
|
|
2887
|
+
await this.q?.stopTask(taskId);
|
|
2888
|
+
}
|
|
2889
|
+
async applySettings(next) {
|
|
2890
|
+
const q = this.q;
|
|
2891
|
+
if (!q) return;
|
|
2892
|
+
const prev = this.settings;
|
|
2893
|
+
try {
|
|
2894
|
+
if ((next.model ?? "default") !== (prev.model ?? "default")) await q.setModel(sdkModel(next.model));
|
|
2895
|
+
if ((next.effort ?? "") !== (prev.effort ?? "") || Boolean(next.ultracode) !== Boolean(prev.ultracode)) {
|
|
2896
|
+
await q.applyFlagSettings({
|
|
2897
|
+
effortLevel: next.effort ?? null,
|
|
2898
|
+
ultracode: Boolean(next.ultracode),
|
|
2899
|
+
...next.ultracode ? { enableWorkflows: true } : {}
|
|
2900
|
+
});
|
|
2901
|
+
}
|
|
2902
|
+
if (Boolean(next.fast) !== Boolean(prev.fast)) await q.applyFlagSettings({ fastMode: Boolean(next.fast) });
|
|
2903
|
+
if (next.permissionMode && next.permissionMode !== (prev.permissionMode ?? "default")) await q.setPermissionMode(next.permissionMode);
|
|
2904
|
+
} catch (error) {
|
|
2905
|
+
console.warn("[pelican] could not apply settings", error.message);
|
|
2906
|
+
}
|
|
2907
|
+
this.settings = { ...prev, ...next };
|
|
2908
|
+
}
|
|
2909
|
+
async setPermissionMode(mode) {
|
|
2910
|
+
await this.applySettings({ ...this.settings, permissionMode: mode });
|
|
2911
|
+
this.emit();
|
|
2912
|
+
}
|
|
2913
|
+
/** Settings changed in the composer, applied to Claude now: from its next step, also mid-turn. */
|
|
2914
|
+
async updateSettings(next) {
|
|
2915
|
+
await this.applySettings({ ...this.settings, ...next });
|
|
2916
|
+
this.emit();
|
|
2917
|
+
}
|
|
2918
|
+
canUseTool = (toolName, input, { signal, suggestions, blockedPath, decisionReason }) => {
|
|
2919
|
+
const id = randomUUID4();
|
|
2920
|
+
const request = {
|
|
2921
|
+
id,
|
|
2922
|
+
sessionId: this.sessionId,
|
|
2923
|
+
toolName,
|
|
2924
|
+
input,
|
|
2925
|
+
canRemember: Boolean(suggestions && suggestions.length > 0),
|
|
2926
|
+
...decisionReason ? { reason: decisionReason } : {},
|
|
2927
|
+
...blockedPath ? { blockedPath } : {},
|
|
2928
|
+
createdAt: Date.now()
|
|
2929
|
+
};
|
|
2930
|
+
return new Promise((resolve) => {
|
|
2931
|
+
this.permissions.set(id, { request, input, ...suggestions ? { suggestions } : {}, resolve });
|
|
2932
|
+
signal.addEventListener("abort", () => {
|
|
2933
|
+
if (this.permissions.delete(id)) {
|
|
2934
|
+
resolve({ behavior: "deny", message: "Cancelled." });
|
|
2935
|
+
this.emit();
|
|
2936
|
+
sessionsChanged();
|
|
2937
|
+
}
|
|
2938
|
+
});
|
|
2939
|
+
this.emit();
|
|
2940
|
+
sessionsChanged();
|
|
2941
|
+
const what = toolName === "AskUserQuestion" ? "has a question for you" : toolName === "ExitPlanMode" ? "has a plan for you to review" : `wants to use ${toolName}`;
|
|
2942
|
+
void notify({ title: this.title || "Claude Code", body: `Claude ${what}`, sessionId: this.sessionId, tag: `${this.sessionId}:ask` });
|
|
2943
|
+
});
|
|
2944
|
+
};
|
|
2945
|
+
decide(id, decision) {
|
|
2946
|
+
const pending = this.permissions.get(id);
|
|
2947
|
+
if (!pending) return false;
|
|
2948
|
+
this.permissions.delete(id);
|
|
2949
|
+
if (decision.behavior === "allow") {
|
|
2950
|
+
pending.resolve({
|
|
2951
|
+
behavior: "allow",
|
|
2952
|
+
updatedInput: decision.updatedInput ?? pending.input,
|
|
2953
|
+
...decision.remember && pending.suggestions ? { updatedPermissions: pending.suggestions } : {}
|
|
2954
|
+
});
|
|
2955
|
+
if (decision.permissionMode) void this.setPermissionMode(decision.permissionMode);
|
|
2956
|
+
} else {
|
|
2957
|
+
pending.resolve({ behavior: "deny", message: decision.message?.trim() || "The user declined this." });
|
|
2958
|
+
}
|
|
2959
|
+
this.emit();
|
|
2960
|
+
sessionsChanged();
|
|
2961
|
+
return true;
|
|
2962
|
+
}
|
|
2963
|
+
async interrupt() {
|
|
2964
|
+
this.interrupted = true;
|
|
2965
|
+
for (const [id] of this.permissions) this.decide(id, { behavior: "deny", message: "Interrupted by the user." });
|
|
2966
|
+
try {
|
|
2967
|
+
await this.q?.interrupt();
|
|
2968
|
+
} catch (error) {
|
|
2969
|
+
console.warn("[pelican] interrupt failed", error.message);
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
close() {
|
|
2973
|
+
if (this.closed) return;
|
|
2974
|
+
this.closed = true;
|
|
2975
|
+
for (const [id] of this.permissions) this.decide(id, { behavior: "deny", message: "Session closed." });
|
|
2976
|
+
this.input.close();
|
|
2977
|
+
try {
|
|
2978
|
+
this.q?.close();
|
|
2979
|
+
} catch {
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
/** How full the context window is now; shown in the session header. */
|
|
2983
|
+
async readContext() {
|
|
2984
|
+
try {
|
|
2985
|
+
const usage = await this.q?.getContextUsage();
|
|
2986
|
+
if (!usage) return;
|
|
2987
|
+
this.context = { percent: Math.round(usage.percentage), tokens: usage.totalTokens, max: usage.maxTokens };
|
|
2988
|
+
this.emit();
|
|
2989
|
+
} catch {
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
onStream(event) {
|
|
2993
|
+
const index2 = typeof event.index === "number" ? event.index : -1;
|
|
2994
|
+
switch (event.type) {
|
|
2995
|
+
case "message_start":
|
|
2996
|
+
this.partial = [];
|
|
2997
|
+
this.completedBlocks = 0;
|
|
2998
|
+
break;
|
|
2999
|
+
case "content_block_start": {
|
|
3000
|
+
const block = event.content_block ?? {};
|
|
3001
|
+
if (block.type === "text") this.partial[index2] = { type: "text", text: "" };
|
|
3002
|
+
else if (block.type === "thinking") this.partial[index2] = { type: "thinking", thinking: "" };
|
|
3003
|
+
else if (block.type === "tool_use" || block.type === "server_tool_use") {
|
|
3004
|
+
this.partial[index2] = { type: "tool_use", name: String(block.name ?? ""), inputJson: "" };
|
|
3005
|
+
}
|
|
3006
|
+
break;
|
|
3007
|
+
}
|
|
3008
|
+
case "content_block_delta": {
|
|
3009
|
+
const delta = event.delta ?? {};
|
|
3010
|
+
const block = this.partial[index2];
|
|
3011
|
+
if (!block) break;
|
|
3012
|
+
if (block.type === "text" && delta.type === "text_delta") block.text += String(delta.text ?? "");
|
|
3013
|
+
else if (block.type === "thinking" && delta.type === "thinking_delta") block.thinking += String(delta.thinking ?? "");
|
|
3014
|
+
else if (block.type === "tool_use" && delta.type === "input_json_delta") block.inputJson += String(delta.partial_json ?? "");
|
|
3015
|
+
break;
|
|
3016
|
+
}
|
|
3017
|
+
default:
|
|
3018
|
+
return;
|
|
3019
|
+
}
|
|
3020
|
+
this.setBusy(true);
|
|
3021
|
+
this.emit(true);
|
|
3022
|
+
}
|
|
3023
|
+
async pump() {
|
|
3024
|
+
const q = this.q;
|
|
3025
|
+
try {
|
|
3026
|
+
for await (const message of q) {
|
|
3027
|
+
this.lastActive = Date.now();
|
|
3028
|
+
this.started.resolve();
|
|
3029
|
+
const raw = message;
|
|
3030
|
+
switch (message.type) {
|
|
3031
|
+
case "stream_event":
|
|
3032
|
+
if (!message.parent_tool_use_id) this.onStream(message.event);
|
|
3033
|
+
break;
|
|
3034
|
+
case "assistant":
|
|
3035
|
+
case "user": {
|
|
3036
|
+
if (raw.parent_tool_use_id) break;
|
|
3037
|
+
if (message.type === "assistant") this.completedBlocks += 1;
|
|
3038
|
+
if (this.activity?.kind === "retrying") this.activity = void 0;
|
|
3039
|
+
const chat = toChatMessage(raw);
|
|
3040
|
+
if (chat) await feedFor(this.sessionId).publish({ ...chat, timestamp: chat.timestamp ?? Date.now() });
|
|
3041
|
+
this.emit(true);
|
|
3042
|
+
break;
|
|
3043
|
+
}
|
|
3044
|
+
case "system":
|
|
3045
|
+
if (message.subtype === "session_state_changed") {
|
|
3046
|
+
const state = raw.state;
|
|
3047
|
+
if (state === "running" || state === "requires_action") this.setBusy(true);
|
|
3048
|
+
else if (state === "idle") this.setBusy(false);
|
|
3049
|
+
} else if (message.subtype === "init") {
|
|
3050
|
+
void refreshTranscriptIndex().then(sessionsChanged);
|
|
3051
|
+
} else if (message.subtype === "status") {
|
|
3052
|
+
this.activity = message.status === "compacting" ? { kind: "compacting" } : void 0;
|
|
3053
|
+
this.emit();
|
|
3054
|
+
} else if (message.subtype === "api_retry") {
|
|
3055
|
+
this.activity = { kind: "retrying", attempt: message.attempt, max: message.max_retries, delayMs: message.retry_delay_ms };
|
|
3056
|
+
this.emit();
|
|
3057
|
+
} else if (message.subtype === "background_tasks_changed") {
|
|
3058
|
+
this.tasks = message.tasks.filter((t) => !t.ambient).map((t) => ({ id: t.task_id, description: t.description, type: t.subagent_type ?? t.task_type }));
|
|
3059
|
+
this.emit();
|
|
3060
|
+
}
|
|
3061
|
+
break;
|
|
3062
|
+
case "rate_limit_event":
|
|
3063
|
+
recordLimit(message.rate_limit_info);
|
|
3064
|
+
break;
|
|
3065
|
+
case "prompt_suggestion":
|
|
3066
|
+
this.suggestion = message.suggestion;
|
|
3067
|
+
this.emit();
|
|
3068
|
+
break;
|
|
3069
|
+
case "result": {
|
|
3070
|
+
const stopped = this.interrupted;
|
|
3071
|
+
this.interrupted = false;
|
|
3072
|
+
const failed = !stopped && (message.subtype !== "success" || message.is_error);
|
|
3073
|
+
this.error = failed ? readableError("errors" in message && message.errors.length > 0 ? message.errors.join("\n") : "result" in message ? String(message.result) : message.subtype) : void 0;
|
|
3074
|
+
this.lastTurn = {
|
|
3075
|
+
durationMs: message.duration_ms,
|
|
3076
|
+
...typeof message.total_cost_usd === "number" ? { costUsd: message.total_cost_usd } : {},
|
|
3077
|
+
...typeof message.usage?.output_tokens === "number" ? { outputTokens: message.usage.output_tokens } : {}
|
|
3078
|
+
};
|
|
3079
|
+
this.activity = void 0;
|
|
3080
|
+
for (const waiting2 of this.queued.splice(0)) await feedFor(this.sessionId).publish(waiting2);
|
|
3081
|
+
this.setBusy(false);
|
|
3082
|
+
this.emit();
|
|
3083
|
+
void this.readContext();
|
|
3084
|
+
if (!stopped) {
|
|
3085
|
+
void notify({
|
|
3086
|
+
title: this.title || "Claude Code",
|
|
3087
|
+
body: failed ? `Stopped: ${this.error ?? "error"}`.slice(0, 180) : "Finished",
|
|
3088
|
+
sessionId: this.sessionId,
|
|
3089
|
+
tag: `${this.sessionId}:done`
|
|
3090
|
+
});
|
|
3091
|
+
}
|
|
3092
|
+
break;
|
|
3093
|
+
}
|
|
3094
|
+
default:
|
|
3095
|
+
break;
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
} catch (error) {
|
|
3099
|
+
this.error = readableError(error.message);
|
|
3100
|
+
console.warn(`[pelican] session ${this.sessionId.slice(0, 8)} ended with an error:`, this.error);
|
|
3101
|
+
} finally {
|
|
3102
|
+
this.closed = true;
|
|
3103
|
+
this.started.reject(new Error(this.error ?? "Claude Code stopped before it started"));
|
|
3104
|
+
for (const [, pending] of this.permissions) pending.resolve({ behavior: "deny", message: "Session closed." });
|
|
3105
|
+
this.permissions.clear();
|
|
3106
|
+
this.setBusy(false);
|
|
3107
|
+
this.emit();
|
|
3108
|
+
if (runs.get(this.sessionId) === this) runs.delete(this.sessionId);
|
|
3109
|
+
sessionsChanged();
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
};
|
|
3113
|
+
runs = /* @__PURE__ */ new Map();
|
|
3114
|
+
setInterval(() => {
|
|
3115
|
+
for (const run4 of runs.values()) {
|
|
3116
|
+
if (!run4.busy && run4.status() === "idle" && Date.now() - run4.lastActive > config.idleCloseMs) run4.close();
|
|
3117
|
+
}
|
|
3118
|
+
}, 6e4).unref();
|
|
3119
|
+
}
|
|
3120
|
+
});
|
|
3121
|
+
|
|
3122
|
+
// src/server/workspace.ts
|
|
3123
|
+
import { open as open3, readdir as readdir5, realpath as realpath4, stat as stat7 } from "node:fs/promises";
|
|
3124
|
+
import path13 from "node:path";
|
|
3125
|
+
function inside(cwd, relative) {
|
|
3126
|
+
if ((relative ?? "").split(/[\\/]/).includes("..")) throw new Error(OUTSIDE);
|
|
3127
|
+
const root = path13.resolve(cwd);
|
|
3128
|
+
const full = path13.resolve(root, relative || ".");
|
|
3129
|
+
if (!within(root, full)) throw new Error(OUTSIDE);
|
|
3130
|
+
return full;
|
|
3131
|
+
}
|
|
3132
|
+
async function insideReal(cwd, relative) {
|
|
3133
|
+
const full = inside(cwd, relative);
|
|
3134
|
+
const [root, real] = await Promise.all([realpath4(cwd), realpath4(full)]);
|
|
3135
|
+
if (!within(root, real)) throw new Error(OUTSIDE);
|
|
3136
|
+
return real;
|
|
3137
|
+
}
|
|
3138
|
+
async function repositoryOf(cwd) {
|
|
3139
|
+
try {
|
|
3140
|
+
const top = (await git(cwd, ["rev-parse", "--show-toplevel"], 5e3)).trim();
|
|
3141
|
+
if (!top) return void 0;
|
|
3142
|
+
const [root, folder] = await Promise.all([realpath4(top), realpath4(cwd)]);
|
|
3143
|
+
return within(root, folder) ? root : void 0;
|
|
3144
|
+
} catch {
|
|
3145
|
+
return void 0;
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
async function gitStatus(cwd) {
|
|
3149
|
+
if (!await repositoryOf(cwd)) return { repo: false };
|
|
3150
|
+
let raw;
|
|
3151
|
+
let prefix = "";
|
|
3152
|
+
try {
|
|
3153
|
+
prefix = (await git(cwd, ["rev-parse", "--show-prefix"], 5e3)).trim();
|
|
3154
|
+
raw = await git(cwd, ["status", "--porcelain=v2", "--branch", "-z", "--untracked-files=all", "--", "."]);
|
|
3155
|
+
} catch {
|
|
3156
|
+
return { repo: false };
|
|
3157
|
+
}
|
|
3158
|
+
const local = (file2) => file2 && prefix && file2.startsWith(prefix) ? file2.slice(prefix.length) : file2;
|
|
3159
|
+
const out = { repo: true, files: [] };
|
|
3160
|
+
const entries = raw.split("\0");
|
|
3161
|
+
for (let i = 0; i < entries.length; i += 1) {
|
|
3162
|
+
const entry = entries[i];
|
|
3163
|
+
if (entry.startsWith("# branch.head ")) {
|
|
3164
|
+
const head = entry.slice(14);
|
|
3165
|
+
if (head !== "(detached)") out.branch = head;
|
|
3166
|
+
} else if (entry.startsWith("# branch.ab ")) {
|
|
3167
|
+
const [ahead, behind] = entry.slice(12).split(" ");
|
|
3168
|
+
out.ahead = Number(ahead?.slice(1)) || 0;
|
|
3169
|
+
out.behind = Number(behind?.slice(1)) || 0;
|
|
3170
|
+
} else if (entry.startsWith("1 ") || entry.startsWith("2 ") || entry.startsWith("u ")) {
|
|
3171
|
+
const parts = entry.split(" ");
|
|
3172
|
+
const xy = parts[1] ?? "..";
|
|
3173
|
+
const code = xy[0] !== "." ? xy[0] : xy[1];
|
|
3174
|
+
const file2 = entry.startsWith("1 ") ? parts.slice(8).join(" ") : entry.startsWith("2 ") ? parts.slice(9).join(" ") : parts.slice(10).join(" ");
|
|
3175
|
+
const status = entry.startsWith("u ") ? "conflicted" : KIND[code] ?? "modified";
|
|
3176
|
+
const item = { path: local(file2), status };
|
|
3177
|
+
if (entry.startsWith("2 ")) {
|
|
3178
|
+
const from = local(entries[i + 1]);
|
|
3179
|
+
if (from) item.from = from;
|
|
3180
|
+
i += 1;
|
|
3181
|
+
}
|
|
3182
|
+
out.files.push(item);
|
|
3183
|
+
} else if (entry.startsWith("? ")) {
|
|
3184
|
+
out.files.push({ path: local(entry.slice(2)), status: "untracked" });
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
try {
|
|
3188
|
+
const base = await git(cwd, ["rev-parse", "--verify", "-q", "HEAD"]).then(() => "HEAD", () => "4b825dc642cb6eb9a060e54bf8d69288fbee4904");
|
|
3189
|
+
const numstat = await git(cwd, ["diff", "--numstat", "--relative", "-z", base]);
|
|
3190
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3191
|
+
const fields = numstat.split("\0");
|
|
3192
|
+
for (let i = 0; i < fields.length; i += 1) {
|
|
3193
|
+
const field = fields[i];
|
|
3194
|
+
if (!field) continue;
|
|
3195
|
+
const [added, removed, name] = field.split(" ");
|
|
3196
|
+
const file2 = name === "" ? fields[i += 2] : name;
|
|
3197
|
+
if (!file2) continue;
|
|
3198
|
+
counts.set(file2, { ...added !== "-" ? { added: Number(added) } : {}, ...removed !== "-" ? { removed: Number(removed) } : {} });
|
|
3199
|
+
}
|
|
3200
|
+
for (const file2 of out.files) Object.assign(file2, counts.get(file2.path) ?? {});
|
|
3201
|
+
} catch {
|
|
3202
|
+
}
|
|
3203
|
+
await Promise.all(out.files.filter((f) => f.status === "untracked").slice(0, 200).map(async (file2) => {
|
|
3204
|
+
try {
|
|
3205
|
+
const content = await readProjectFile(cwd, file2.path);
|
|
3206
|
+
if (content.text !== void 0) file2.added = content.text.split("\n").length - (content.text.endsWith("\n") ? 1 : 0);
|
|
3207
|
+
} catch {
|
|
3208
|
+
}
|
|
3209
|
+
}));
|
|
3210
|
+
return out;
|
|
3211
|
+
}
|
|
3212
|
+
async function gitDiff(cwd, file2) {
|
|
3213
|
+
if (!await repositoryOf(cwd)) return "";
|
|
3214
|
+
const full = inside(cwd, file2);
|
|
3215
|
+
const [root, parent] = await Promise.all([realpath4(cwd), realpath4(path13.dirname(full)).catch(() => void 0)]);
|
|
3216
|
+
if (parent && !within(root, parent)) throw new Error(OUTSIDE);
|
|
3217
|
+
let diff = "";
|
|
3218
|
+
try {
|
|
3219
|
+
diff = await git(cwd, ["diff", "HEAD", "--", file2]);
|
|
3220
|
+
} catch {
|
|
3221
|
+
diff = "";
|
|
3222
|
+
}
|
|
3223
|
+
if (!diff) {
|
|
3224
|
+
diff = await git(cwd, ["diff", "--no-index", "--", "/dev/null", `./${file2}`]).catch((error) => error.stdout ?? "");
|
|
3225
|
+
}
|
|
3226
|
+
return diff.length > MAX_DIFF ? `${diff.slice(0, MAX_DIFF)}
|
|
3227
|
+
\u2026 (diff cut short)` : diff;
|
|
3228
|
+
}
|
|
3229
|
+
async function listTree(cwd, relative) {
|
|
3230
|
+
const dir = await insideReal(cwd, relative);
|
|
3231
|
+
const names = await readdir5(dir, { withFileTypes: true });
|
|
3232
|
+
const entries = [];
|
|
3233
|
+
for (const entry of names) {
|
|
3234
|
+
if (HIDDEN.has(entry.name)) continue;
|
|
3235
|
+
const rel = path13.join(relative, entry.name);
|
|
3236
|
+
if (entry.isDirectory()) entries.push({ name: entry.name, path: rel, dir: true });
|
|
3237
|
+
else if (entry.isFile() || entry.isSymbolicLink()) {
|
|
3238
|
+
const size = await stat7(path13.join(dir, entry.name)).then((s) => s.isDirectory() ? -1 : s.size, () => void 0);
|
|
3239
|
+
if (size === -1) entries.push({ name: entry.name, path: rel, dir: true });
|
|
3240
|
+
else entries.push({ name: entry.name, path: rel, dir: false, ...size !== void 0 ? { size } : {} });
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
return entries.sort((a, b) => Number(b.dir) - Number(a.dir) || a.name.localeCompare(b.name, void 0, { numeric: true }));
|
|
3244
|
+
}
|
|
3245
|
+
async function readProjectFile(cwd, relative) {
|
|
3246
|
+
const full = await insideReal(cwd, relative);
|
|
3247
|
+
const info = await stat7(full);
|
|
3248
|
+
const ext = path13.extname(full).slice(1).toLowerCase();
|
|
3249
|
+
const handle = await open3(full, "r");
|
|
3250
|
+
try {
|
|
3251
|
+
if (IMAGE[ext] && info.size <= 4 * 1024 * 1024) {
|
|
3252
|
+
const data2 = Buffer.alloc(info.size);
|
|
3253
|
+
await handle.read(data2, 0, info.size, 0);
|
|
3254
|
+
return { path: relative, size: info.size, image: `data:${IMAGE[ext]};base64,${data2.toString("base64")}` };
|
|
3255
|
+
}
|
|
3256
|
+
const length = Math.min(info.size, MAX_FILE);
|
|
3257
|
+
const data = Buffer.alloc(length);
|
|
3258
|
+
await handle.read(data, 0, length, 0);
|
|
3259
|
+
if (data.subarray(0, 8e3).includes(0)) return { path: relative, size: info.size, binary: true };
|
|
3260
|
+
return { path: relative, size: info.size, text: data.toString("utf8"), ...info.size > MAX_FILE ? { truncated: true } : {} };
|
|
3261
|
+
} finally {
|
|
3262
|
+
await handle.close();
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
var MAX_FILE, MAX_DIFF, OUTSIDE, within, KIND, HIDDEN, IMAGE;
|
|
3266
|
+
var init_workspace = __esm({
|
|
3267
|
+
"src/server/workspace.ts"() {
|
|
3268
|
+
"use strict";
|
|
3269
|
+
init_git();
|
|
3270
|
+
MAX_FILE = 512 * 1024;
|
|
3271
|
+
MAX_DIFF = 4e5;
|
|
3272
|
+
OUTSIDE = "That path is outside the session\u2019s folder";
|
|
3273
|
+
within = (root, full) => full === root || full.startsWith(`${root}${path13.sep}`);
|
|
3274
|
+
KIND = { M: "modified", T: "modified", A: "added", D: "deleted", R: "renamed", C: "added", U: "conflicted" };
|
|
3275
|
+
HIDDEN = /* @__PURE__ */ new Set([".git", "node_modules", ".DS_Store"]);
|
|
3276
|
+
IMAGE = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", svg: "image/svg+xml" };
|
|
3277
|
+
}
|
|
3278
|
+
});
|
|
3279
|
+
|
|
3280
|
+
// src/server/app.ts
|
|
3281
|
+
import { readFile as readFile7, realpath as realpath5, stat as stat8 } from "node:fs/promises";
|
|
3282
|
+
import { isIP } from "node:net";
|
|
3283
|
+
import path14 from "node:path";
|
|
3284
|
+
import { getConnInfo } from "@hono/node-server/conninfo";
|
|
3285
|
+
import { Hono } from "hono";
|
|
3286
|
+
import { bodyLimit } from "hono/body-limit";
|
|
3287
|
+
function clientAddress(c) {
|
|
3288
|
+
let remote = "unknown";
|
|
3289
|
+
try {
|
|
3290
|
+
remote = getConnInfo(c).remote.address ?? "unknown";
|
|
3291
|
+
} catch {
|
|
3292
|
+
}
|
|
3293
|
+
const address = remote.replace(/^::ffff:/, "");
|
|
3294
|
+
if (config.trustedProxy && isLoopback(address)) {
|
|
3295
|
+
const forwarded = c.req.header("x-forwarded-for")?.split(",").at(-1)?.trim();
|
|
3296
|
+
if (forwarded && isIP(forwarded)) return forwarded;
|
|
3297
|
+
}
|
|
3298
|
+
return address;
|
|
3299
|
+
}
|
|
3300
|
+
function originOf(header) {
|
|
3301
|
+
if (!header || header === "null") return void 0;
|
|
3302
|
+
try {
|
|
3303
|
+
return new URL(header).origin;
|
|
3304
|
+
} catch {
|
|
3305
|
+
return void 0;
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
function cleanSettings(raw) {
|
|
3309
|
+
const value = raw && typeof raw === "object" ? raw : {};
|
|
3310
|
+
const text2 = (v, max) => typeof v === "string" && v.length > 0 && v.length <= max ? v : void 0;
|
|
3311
|
+
const out = {};
|
|
3312
|
+
const model = text2(value.model, 200);
|
|
3313
|
+
const effort = text2(value.effort, 20);
|
|
3314
|
+
const mode = permissionMode(value.permissionMode);
|
|
3315
|
+
if (model) out.model = model;
|
|
3316
|
+
if (effort) out.effort = effort;
|
|
3317
|
+
if (typeof value.fast === "boolean") out.fast = value.fast;
|
|
3318
|
+
if (typeof value.ultracode === "boolean") out.ultracode = value.ultracode;
|
|
3319
|
+
if (mode) out.permissionMode = mode;
|
|
3320
|
+
return out;
|
|
3321
|
+
}
|
|
3322
|
+
function sendBody(raw) {
|
|
3323
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return "Not a message";
|
|
3324
|
+
const body = raw;
|
|
3325
|
+
if (body.text !== void 0 && typeof body.text !== "string") return "The text must be a string";
|
|
3326
|
+
if (body.attachments !== void 0 && (!Array.isArray(body.attachments) || body.attachments.length > 50 || !body.attachments.every((a) => typeof a === "string"))) return "Attachments must be upload ids";
|
|
3327
|
+
if (body.settings !== void 0 && (typeof body.settings !== "object" || body.settings === null)) return "Settings must be an object";
|
|
3328
|
+
if (body.cwd !== void 0 && typeof body.cwd !== "string") return "The folder must be a path";
|
|
3329
|
+
if (body.scratch !== void 0 && typeof body.scratch !== "boolean") return "scratch must be true or false";
|
|
3330
|
+
return body;
|
|
3331
|
+
}
|
|
3332
|
+
async function attachmentsFrom(ids) {
|
|
3333
|
+
const found = await Promise.all((ids ?? []).map((id) => findUpload(id)));
|
|
3334
|
+
return found.filter((a) => Boolean(a));
|
|
3335
|
+
}
|
|
3336
|
+
async function imagesOf(sessionId, uuid, keep) {
|
|
3337
|
+
const file2 = await transcriptPath(sessionId);
|
|
3338
|
+
if (!file2) return [];
|
|
3339
|
+
const images = await rawImages(file2, uuid);
|
|
3340
|
+
const wanted = Array.isArray(keep) ? new Set(keep.filter((i) => Number.isInteger(i))) : new Set(images.map((_, i) => i));
|
|
3341
|
+
const kept = [];
|
|
3342
|
+
for (const [i, image2] of images.entries()) {
|
|
3343
|
+
if (!wanted.has(i) || image2.type !== "base64" || !image2.data) continue;
|
|
3344
|
+
const ext = image2.mediaType.split("/")[1]?.replace("jpeg", "jpg") ?? "png";
|
|
3345
|
+
kept.push(await saveUpload(new File([Buffer.from(image2.data, "base64")], `image-${i + 1}.${ext}`, { type: image2.mediaType })));
|
|
3346
|
+
}
|
|
3347
|
+
return kept;
|
|
3348
|
+
}
|
|
3349
|
+
async function claudeStarted(sessionId) {
|
|
3350
|
+
const run4 = getRun(sessionId);
|
|
3351
|
+
if (!run4) return;
|
|
3352
|
+
let timer;
|
|
3353
|
+
try {
|
|
3354
|
+
await Promise.race([run4.ready, new Promise((resolve) => {
|
|
3355
|
+
timer = setTimeout(resolve, 45e3);
|
|
3356
|
+
})]);
|
|
3357
|
+
} finally {
|
|
3358
|
+
clearTimeout(timer);
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
async function makeEdit(id, before2, body, key) {
|
|
3362
|
+
const info = await sessionInfo(id);
|
|
3363
|
+
if (!info?.cwd || !await isDirectory(info.cwd)) return { error: "The session\u2019s folder is missing", status: 404 };
|
|
3364
|
+
if (getRun(id)?.busy) return { error: "Stop Claude before editing an earlier prompt.", status: 409 };
|
|
3365
|
+
const messages = await loadMessages(id);
|
|
3366
|
+
const at = messages.findIndex((m) => m.uuid === before2);
|
|
3367
|
+
if (at < 0) return { error: "That message is not in this session", status: 404 };
|
|
3368
|
+
const previous = at === 0 ? null : messages[at - 1].uuid;
|
|
3369
|
+
const last = !messages.slice(at + 1).some(isPrompt);
|
|
3370
|
+
const file2 = await transcriptPath(id);
|
|
3371
|
+
const entry = last && previous !== null && file2 && !getRun(id)?.hasBackgroundTasks ? (await rawEntry(file2, before2))?.parentUuid : void 0;
|
|
3372
|
+
const settings = cleanSettings(body.settings);
|
|
3373
|
+
if (!entry && settings.permissionMode === "bypassPermissions") settings.permissionMode = "default";
|
|
3374
|
+
const attachments = await imagesOf(id, before2, body.keepImages);
|
|
3375
|
+
const message = { text: body.text, attachments, settings, uuid: key };
|
|
3376
|
+
let branch;
|
|
3377
|
+
let started;
|
|
3378
|
+
try {
|
|
3379
|
+
if (entry) {
|
|
3380
|
+
started = await replaceLastTurn(id, info.cwd, entry, before2, message);
|
|
3381
|
+
await claudeStarted(started);
|
|
3382
|
+
return { sessionId: started, inPlace: true };
|
|
3383
|
+
}
|
|
3384
|
+
if (previous !== null) branch = await fork(id, previous);
|
|
3385
|
+
started = await sendMessage(branch ? { sessionId: branch } : { cwd: info.cwd }, message);
|
|
3386
|
+
await claudeStarted(started);
|
|
3387
|
+
return { sessionId: started, inPlace: false };
|
|
3388
|
+
} catch (error) {
|
|
3389
|
+
if (branch) await remove(branch).catch(() => void 0);
|
|
3390
|
+
else if (started && !entry) await remove(started).catch(() => void 0);
|
|
3391
|
+
await Promise.all(attachments.map((a) => removeUpload(a.id)));
|
|
3392
|
+
throw error;
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
async function inDemo(dir) {
|
|
3396
|
+
try {
|
|
3397
|
+
const [real, root] = await Promise.all([realpath5(dir), realpath5(config.home)]);
|
|
3398
|
+
return real === root || real.startsWith(`${root}${path14.sep}`);
|
|
3399
|
+
} catch {
|
|
3400
|
+
return false;
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
async function checkedFolder(cwd) {
|
|
3404
|
+
if (!cwd || !path14.isAbsolute(cwd) || !await isDirectory(cwd)) return void 0;
|
|
3405
|
+
if (config.demo && !await inDemo(cwd)) return void 0;
|
|
3406
|
+
return cwd;
|
|
3407
|
+
}
|
|
3408
|
+
async function insideRepository(cwd) {
|
|
3409
|
+
for (let dir = path14.resolve(cwd); ; dir = path14.dirname(dir)) {
|
|
3410
|
+
if (await stat8(path14.join(dir, ".git")).then(() => true, () => false)) return true;
|
|
3411
|
+
if (path14.dirname(dir) === dir) return false;
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
function validSubscription(value) {
|
|
3415
|
+
const { endpoint, keys: keys2 } = value ?? {};
|
|
3416
|
+
const bytes = (text2) => typeof text2 === "string" && /^[\w-]+=*$/.test(text2) ? Buffer.from(text2, "base64url").length : -1;
|
|
3417
|
+
return typeof endpoint === "string" && endpoint.length <= 2048 && endpoint.startsWith("https://") && bytes(keys2?.p256dh) === 65 && bytes(keys2?.auth) === 16;
|
|
3418
|
+
}
|
|
3419
|
+
var app, secure, cookieHeader, unmarked, tooLarge, limits2, PERMISSION_MODES, permissionMode, sendMode, uuidOf, edits, editing, isPrompt, folderOf, gitAllowed, pageUrl, TYPES, POLICY;
|
|
3420
|
+
var init_app = __esm({
|
|
3421
|
+
"src/server/app.ts"() {
|
|
3422
|
+
"use strict";
|
|
3423
|
+
init_package();
|
|
3424
|
+
init_tasks();
|
|
3425
|
+
init_actions();
|
|
3426
|
+
init_archive();
|
|
3427
|
+
init_auth();
|
|
3428
|
+
init_config();
|
|
3429
|
+
init_feed();
|
|
3430
|
+
init_files();
|
|
3431
|
+
init_folders();
|
|
3432
|
+
init_models();
|
|
3433
|
+
init_project_files();
|
|
3434
|
+
init_push();
|
|
3435
|
+
init_runner();
|
|
3436
|
+
init_shell();
|
|
3437
|
+
init_usage();
|
|
3438
|
+
init_workspace();
|
|
3439
|
+
init_security();
|
|
3440
|
+
init_sessions();
|
|
3441
|
+
init_transcripts();
|
|
3442
|
+
init_trust();
|
|
3443
|
+
app = new Hono();
|
|
3444
|
+
secure = (c) => c.req.header("x-forwarded-proto") === "https" || c.req.url.startsWith("https:");
|
|
3445
|
+
cookieHeader = (c, name, value, maxAge) => `${name}=${value}; Path=/; Max-Age=${maxAge}; HttpOnly; SameSite=Strict${secure(c) ? "; Secure" : ""}`;
|
|
3446
|
+
app.use("*", async (c, next) => {
|
|
3447
|
+
if (!hostAllowed(c.req.header("host"))) {
|
|
3448
|
+
return c.text("This host name is not allowed. Add it to PELICAN_ALLOWED_HOSTS.", 403);
|
|
3449
|
+
}
|
|
3450
|
+
await next();
|
|
3451
|
+
});
|
|
3452
|
+
unmarked = (c) => c.req.method === "GET" && (/^\/api\/uploads\/[^/]+$/.test(c.req.path) || /^\/api\/sessions\/[^/]+\/export$/.test(c.req.path));
|
|
3453
|
+
app.use("/api/*", async (c, next) => {
|
|
3454
|
+
const origin = c.req.header("origin");
|
|
3455
|
+
const parsed = originOf(origin);
|
|
3456
|
+
const allowedOrigin = Boolean(parsed && config.allowedOrigins.includes(parsed));
|
|
3457
|
+
const site = c.req.header("sec-fetch-site");
|
|
3458
|
+
if (site && site !== "same-origin" && site !== "none" && !allowedOrigin) {
|
|
3459
|
+
return c.json({ error: "Request from another site" }, 403);
|
|
3460
|
+
}
|
|
3461
|
+
if (!unmarked(c) && c.req.header("x-pelican") !== "1") {
|
|
3462
|
+
return c.json({ error: "Requests must come from Pelican (send the header X-Pelican: 1)" }, 403);
|
|
3463
|
+
}
|
|
3464
|
+
if (c.req.method !== "GET" && c.req.method !== "HEAD" && !originAllowed(origin, c.req.header("host"), c.req.header("x-forwarded-host"))) {
|
|
3465
|
+
return c.json({ error: "Request from another site" }, 403);
|
|
3466
|
+
}
|
|
3467
|
+
if (!c.req.path.startsWith("/api/auth") && !signedIn(c.req.header("cookie"), c.req.header("authorization"))) {
|
|
3468
|
+
return c.json({ error: "Sign in to continue", signIn: true }, 401);
|
|
3469
|
+
}
|
|
3470
|
+
if (config.demo && c.req.method !== "GET" && c.req.method !== "HEAD") {
|
|
3471
|
+
return c.json({ error: "This is the demo: nothing runs and nothing changes. Start Pelican without --demo to talk to Claude." }, 403);
|
|
3472
|
+
}
|
|
3473
|
+
await next();
|
|
3474
|
+
});
|
|
3475
|
+
tooLarge = (c) => c.json({ error: "Request too large" }, 413);
|
|
3476
|
+
limits2 = {
|
|
3477
|
+
auth: bodyLimit({ maxSize: 4 * 1024, onError: tooLarge }),
|
|
3478
|
+
upload: bodyLimit({ maxSize: MAX_UPLOAD_BYTES + 64 * 1024, onError: tooLarge }),
|
|
3479
|
+
json: bodyLimit({ maxSize: 1024 * 1024, onError: tooLarge })
|
|
3480
|
+
};
|
|
3481
|
+
app.use("/api/*", (c, next) => (c.req.path.startsWith("/api/auth") ? limits2.auth : c.req.path === "/api/uploads" ? limits2.upload : limits2.json)(c, next));
|
|
3482
|
+
app.onError((error, c) => {
|
|
3483
|
+
console.warn("[pelican]", c.req.method, c.req.path, error.message);
|
|
3484
|
+
return c.json({ error: error.message }, 500);
|
|
3485
|
+
});
|
|
3486
|
+
app.get("/api/auth", (c) => c.json({
|
|
3487
|
+
required: authMode() !== "none",
|
|
3488
|
+
mode: authMode(),
|
|
3489
|
+
signedIn: signedIn(c.req.header("cookie"), c.req.header("authorization"))
|
|
3490
|
+
}));
|
|
3491
|
+
app.post("/api/auth/login", async (c) => {
|
|
3492
|
+
const succeeded = beginAttempt(clientAddress(c));
|
|
3493
|
+
if (!succeeded) return c.json({ error: "Too many attempts. Try again in a few minutes." }, 429);
|
|
3494
|
+
const body = await c.req.json().catch(() => ({}));
|
|
3495
|
+
const mode = authMode();
|
|
3496
|
+
const ok = mode === "password" ? typeof body.password === "string" && await checkPassword(body.password) : mode === "token" ? typeof body.token === "string" && checkToken(body.token.trim()) : false;
|
|
3497
|
+
if (!ok) return c.json({ error: mode === "token" ? "That is not the access token" : "Wrong password", mode }, 401);
|
|
3498
|
+
succeeded();
|
|
3499
|
+
const cookie = newSessionCookie();
|
|
3500
|
+
c.header("Set-Cookie", cookieHeader(c, cookie.name, cookie.value, cookie.maxAge));
|
|
3501
|
+
return c.json({ ok: true });
|
|
3502
|
+
});
|
|
3503
|
+
app.post("/api/auth/logout", (c) => {
|
|
3504
|
+
c.header("Set-Cookie", cookieHeader(c, cookieName(), "", 0));
|
|
3505
|
+
return c.json({ ok: true });
|
|
3506
|
+
});
|
|
3507
|
+
app.post("/api/access/code", (c) => {
|
|
3508
|
+
if (authMode() === "none") return c.json({ code: null });
|
|
3509
|
+
return c.json({ code: issueCode() });
|
|
3510
|
+
});
|
|
3511
|
+
app.get("/api/info", (c) => {
|
|
3512
|
+
const info = { machine: config.machine, home: config.home, version: package_default.version, auth: authMode() !== "none", demo: config.demo };
|
|
3513
|
+
const key = pushKey();
|
|
3514
|
+
if (key) info.pushKey = key;
|
|
3515
|
+
return c.json(info);
|
|
3516
|
+
});
|
|
3517
|
+
app.get("/api/sessions", async (c) => c.json({ sessions: await listSummaries(runStatus) }));
|
|
3518
|
+
app.get("/api/sessions/:id", async (c) => {
|
|
3519
|
+
const id = c.req.param("id");
|
|
3520
|
+
const session = await sessionSummary(id, runStatus);
|
|
3521
|
+
const run4 = runState(id);
|
|
3522
|
+
if (!session && !run4) return c.json({ error: "No such session" }, 404);
|
|
3523
|
+
const tasks = currentTasks(await loadMessages(id));
|
|
3524
|
+
return c.json({ session: session ?? null, run: run4 ?? null, tasks, settings: await recordedSettings(id) });
|
|
3525
|
+
});
|
|
3526
|
+
app.get("/api/sessions/:id/messages", async (c) => {
|
|
3527
|
+
const before2 = c.req.query("before");
|
|
3528
|
+
const limit = Math.min(Number(c.req.query("limit") ?? 120) || 120, 500);
|
|
3529
|
+
return c.json(await loadPage(c.req.param("id"), before2 === void 0 ? void 0 : Number(before2), limit));
|
|
3530
|
+
});
|
|
3531
|
+
PERMISSION_MODES = ["default", "acceptEdits", "plan", "auto", "bypassPermissions"];
|
|
3532
|
+
permissionMode = (value) => PERMISSION_MODES.includes(value) ? value : void 0;
|
|
3533
|
+
sendMode = (value) => value === "steer" || value === "queue" ? value : void 0;
|
|
3534
|
+
uuidOf = (value) => typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value) ? value : void 0;
|
|
3535
|
+
app.post("/api/sessions", async (c) => {
|
|
3536
|
+
const body = sendBody(await c.req.json().catch(() => void 0));
|
|
3537
|
+
if (typeof body === "string") return c.json({ error: body }, 400);
|
|
3538
|
+
const cwd = body.scratch ? await createScratchFolder() : await checkedFolder(body.cwd);
|
|
3539
|
+
if (!cwd) return c.json({ error: "No such folder" }, 400);
|
|
3540
|
+
const uuid = uuidOf(body.uuid);
|
|
3541
|
+
const sessionId = await sendMessage({ cwd }, {
|
|
3542
|
+
text: typeof body.text === "string" ? body.text : "",
|
|
3543
|
+
attachments: await attachmentsFrom(body.attachments),
|
|
3544
|
+
settings: cleanSettings(body.settings),
|
|
3545
|
+
...uuid ? { uuid } : {}
|
|
3546
|
+
});
|
|
3547
|
+
return c.json({ sessionId });
|
|
3548
|
+
});
|
|
3549
|
+
app.post("/api/sessions/:id/messages", async (c) => {
|
|
3550
|
+
const body = sendBody(await c.req.json().catch(() => void 0));
|
|
3551
|
+
if (typeof body === "string") return c.json({ error: body }, 400);
|
|
3552
|
+
const uuid = uuidOf(body.uuid);
|
|
3553
|
+
const mode = sendMode(body.mode);
|
|
3554
|
+
const sessionId = await sendMessage({ sessionId: c.req.param("id") }, {
|
|
3555
|
+
text: typeof body.text === "string" ? body.text : "",
|
|
3556
|
+
attachments: await attachmentsFrom(body.attachments),
|
|
3557
|
+
settings: cleanSettings(body.settings),
|
|
3558
|
+
...uuid ? { uuid } : {},
|
|
3559
|
+
...mode ? { mode } : {}
|
|
3560
|
+
});
|
|
3561
|
+
return c.json({ sessionId });
|
|
3562
|
+
});
|
|
3563
|
+
edits = /* @__PURE__ */ new Map();
|
|
3564
|
+
editing = /* @__PURE__ */ new Map();
|
|
3565
|
+
isPrompt = (m) => m.role === "user" && !m.meta && m.blocks.some((b) => b.type === "text" || b.type === "image");
|
|
3566
|
+
app.post("/api/sessions/:id/edit", async (c) => {
|
|
3567
|
+
const id = c.req.param("id");
|
|
3568
|
+
const body = await c.req.json().catch(() => ({}));
|
|
3569
|
+
const key = uuidOf(body.uuid);
|
|
3570
|
+
const before2 = uuidOf(body.before);
|
|
3571
|
+
if (!key || !before2 || typeof body.text !== "string" || !body.text.trim()) return c.json({ error: "Nothing to send" }, 400);
|
|
3572
|
+
for (const [k, v] of edits) if (Date.now() - v.at > 10 * 6e4) edits.delete(k);
|
|
3573
|
+
const done = edits.get(key);
|
|
3574
|
+
if (done) return c.json({ sessionId: done.sessionId, inPlace: done.inPlace });
|
|
3575
|
+
let work = editing.get(key);
|
|
3576
|
+
if (!work) {
|
|
3577
|
+
const made = makeEdit(id, before2, { text: body.text, settings: body.settings, keepImages: body.keepImages }, key);
|
|
3578
|
+
work = made;
|
|
3579
|
+
editing.set(key, made);
|
|
3580
|
+
void made.then((result3) => {
|
|
3581
|
+
if (!("error" in result3)) edits.set(key, { at: Date.now(), ...result3 });
|
|
3582
|
+
}, () => void 0).finally(() => editing.delete(key));
|
|
3583
|
+
}
|
|
3584
|
+
const result2 = await work;
|
|
3585
|
+
if ("error" in result2) return c.json({ error: result2.error }, result2.status);
|
|
3586
|
+
return c.json(result2);
|
|
3587
|
+
});
|
|
3588
|
+
app.post("/api/sessions/:id/interrupt", async (c) => {
|
|
3589
|
+
await getRun(c.req.param("id"))?.interrupt();
|
|
3590
|
+
return c.json({ ok: true });
|
|
3591
|
+
});
|
|
3592
|
+
app.post("/api/sessions/:id/permissions/:requestId", async (c) => {
|
|
3593
|
+
const decision = await c.req.json().catch(() => void 0);
|
|
3594
|
+
const raw = decision ?? {};
|
|
3595
|
+
const valid = (raw.behavior === "allow" || raw.behavior === "deny") && (raw.message === void 0 || typeof raw.message === "string") && (raw.remember === void 0 || typeof raw.remember === "boolean") && (raw.updatedInput === void 0 || typeof raw.updatedInput === "object" && raw.updatedInput !== null && !Array.isArray(raw.updatedInput)) && (raw.permissionMode === void 0 || Boolean(permissionMode(raw.permissionMode)));
|
|
3596
|
+
if (!decision || !valid) return c.json({ error: "Not a decision" }, 400);
|
|
3597
|
+
const ok = getRun(c.req.param("id"))?.decide(c.req.param("requestId"), decision) ?? false;
|
|
3598
|
+
return ok ? c.json({ ok }) : c.json({ error: "That request is no longer pending" }, 409);
|
|
3599
|
+
});
|
|
3600
|
+
app.post("/api/sessions/:id/settings", async (c) => {
|
|
3601
|
+
const body = await c.req.json().catch(() => ({}));
|
|
3602
|
+
if (!body.settings || typeof body.settings !== "object") return c.json({ error: "No settings" }, 400);
|
|
3603
|
+
await getRun(c.req.param("id"))?.updateSettings(cleanSettings(body.settings));
|
|
3604
|
+
return c.json({ ok: true });
|
|
3605
|
+
});
|
|
3606
|
+
app.post("/api/sessions/:id/mode", async (c) => {
|
|
3607
|
+
const body = await c.req.json();
|
|
3608
|
+
const mode = permissionMode(body.permissionMode);
|
|
3609
|
+
if (!mode) return c.json({ error: "No such mode" }, 400);
|
|
3610
|
+
await getRun(c.req.param("id"))?.setPermissionMode(mode);
|
|
3611
|
+
return c.json({ ok: true });
|
|
3612
|
+
});
|
|
3613
|
+
app.patch("/api/sessions/:id", async (c) => {
|
|
3614
|
+
const { title } = await c.req.json();
|
|
3615
|
+
if (!title?.trim()) return c.json({ error: "Title is empty" }, 400);
|
|
3616
|
+
await renameSessionTitle(c.req.param("id"), title.trim());
|
|
3617
|
+
return c.json({ ok: true });
|
|
3618
|
+
});
|
|
3619
|
+
app.post("/api/sessions/:id/fork", async (c) => {
|
|
3620
|
+
const id = c.req.param("id");
|
|
3621
|
+
const body = await c.req.json().catch(() => ({}));
|
|
3622
|
+
let upTo = body.upTo;
|
|
3623
|
+
if (body.before) {
|
|
3624
|
+
const previous = await messageBefore(id, body.before);
|
|
3625
|
+
if (previous === void 0) return c.json({ error: "That message is not in this session" }, 404);
|
|
3626
|
+
if (previous === null) return c.json({ sessionId: null });
|
|
3627
|
+
upTo = previous;
|
|
3628
|
+
}
|
|
3629
|
+
return c.json({ sessionId: await fork(id, upTo, body.title) });
|
|
3630
|
+
});
|
|
3631
|
+
app.post("/api/sessions/:id/archive", async (c) => {
|
|
3632
|
+
const id = uuidOf(c.req.param("id"));
|
|
3633
|
+
const body = await c.req.json().catch(() => ({}));
|
|
3634
|
+
if (!id || typeof body.archived !== "boolean") return c.json({ error: "Say archived: true or false" }, 400);
|
|
3635
|
+
await setArchived(id, body.archived);
|
|
3636
|
+
sessionsChanged();
|
|
3637
|
+
return c.json({ ok: true });
|
|
3638
|
+
});
|
|
3639
|
+
app.delete("/api/sessions/:id", async (c) => {
|
|
3640
|
+
const id = c.req.param("id");
|
|
3641
|
+
if (getRun(id)?.busy) return c.json({ error: "Stop Claude in this session before deleting it." }, 409);
|
|
3642
|
+
getRun(id)?.close();
|
|
3643
|
+
await remove(id);
|
|
3644
|
+
return c.json({ ok: true });
|
|
3645
|
+
});
|
|
3646
|
+
app.get("/api/sessions/:id/export", async (c) => {
|
|
3647
|
+
const { title, markdown } = await exportMarkdown(c.req.param("id"));
|
|
3648
|
+
const name = `${title.replace(/[^\p{L}\p{N} _-]+/gu, "").trim().slice(0, 60) || "session"}.md`;
|
|
3649
|
+
return c.body(markdown, 200, {
|
|
3650
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
3651
|
+
"Content-Disposition": `attachment; filename*=UTF-8''${encodeURIComponent(name)}`,
|
|
3652
|
+
"X-Content-Type-Options": "nosniff"
|
|
3653
|
+
});
|
|
3654
|
+
});
|
|
3655
|
+
app.get("/api/models", async (c) => c.json(await listModels()));
|
|
3656
|
+
app.get("/api/commands", async (c) => {
|
|
3657
|
+
const cwd = await folderOf(c);
|
|
3658
|
+
if (!cwd) return c.json({ commands: [], agents: [] });
|
|
3659
|
+
return c.json(await listCompletions(cwd));
|
|
3660
|
+
});
|
|
3661
|
+
app.get("/api/usage/plan", async (c) => c.json({ plan: await planUsage() }));
|
|
3662
|
+
app.get("/api/usage", (c) => c.json({
|
|
3663
|
+
limits: config.demo ? [
|
|
3664
|
+
{ type: "five_hour", status: "allowed", utilization: 0.42, resetsAt: Date.now() + 2.4 * 36e5, at: Date.now() },
|
|
3665
|
+
{ type: "seven_day", status: "allowed", utilization: 0.18, resetsAt: Date.now() + 3.5 * 864e5, at: Date.now() }
|
|
3666
|
+
] : usageLimits()
|
|
3667
|
+
}));
|
|
3668
|
+
folderOf = (c) => checkedFolder(c.req.query("cwd"));
|
|
3669
|
+
gitAllowed = (cwd) => config.demo ? Promise.resolve(true) : claudeRanIn(cwd);
|
|
3670
|
+
app.get("/api/git", async (c) => {
|
|
3671
|
+
const cwd = await folderOf(c);
|
|
3672
|
+
if (!cwd) return c.json({ error: "No such folder" }, 404);
|
|
3673
|
+
if (!await gitAllowed(cwd)) return c.json(await insideRepository(cwd) ? { repo: false, untrusted: true } : { repo: false });
|
|
3674
|
+
return c.json(await gitStatus(cwd));
|
|
3675
|
+
});
|
|
3676
|
+
app.get("/api/git/diff", async (c) => {
|
|
3677
|
+
const cwd = await folderOf(c);
|
|
3678
|
+
const file2 = c.req.query("path");
|
|
3679
|
+
if (!cwd || !file2) return c.json({ error: "No such file" }, 404);
|
|
3680
|
+
if (!await gitAllowed(cwd)) return c.json({ diff: "" });
|
|
3681
|
+
return c.json({ diff: await gitDiff(cwd, file2) });
|
|
3682
|
+
});
|
|
3683
|
+
app.get("/api/tree", async (c) => {
|
|
3684
|
+
const cwd = await folderOf(c);
|
|
3685
|
+
if (!cwd) return c.json({ error: "No such folder" }, 404);
|
|
3686
|
+
return c.json({ entries: await listTree(cwd, c.req.query("path") ?? "") });
|
|
3687
|
+
});
|
|
3688
|
+
app.get("/api/file", async (c) => {
|
|
3689
|
+
const cwd = await folderOf(c);
|
|
3690
|
+
const file2 = c.req.query("path");
|
|
3691
|
+
if (!cwd || !file2) return c.json({ error: "No such file" }, 404);
|
|
3692
|
+
return c.json(await readProjectFile(cwd, file2));
|
|
3693
|
+
});
|
|
3694
|
+
app.post("/api/sessions/:id/shell", async (c) => {
|
|
3695
|
+
const id = c.req.param("id");
|
|
3696
|
+
const { command } = await c.req.json().catch(() => ({ command: void 0 }));
|
|
3697
|
+
if (!command?.trim()) return c.json({ error: "No command" }, 400);
|
|
3698
|
+
const info = await sessionInfo(id);
|
|
3699
|
+
if (!info?.cwd || !await isDirectory(info.cwd)) return c.json({ error: "The session\u2019s folder is missing" }, 404);
|
|
3700
|
+
const result2 = await runCommand(info.cwd, command.trim());
|
|
3701
|
+
await (await ensureRun(id)).appendShell(result2);
|
|
3702
|
+
return c.json(result2);
|
|
3703
|
+
});
|
|
3704
|
+
app.post("/api/sessions/:id/rewind", async (c) => {
|
|
3705
|
+
const { uuid, dryRun } = await c.req.json().catch(() => ({ uuid: void 0, dryRun: void 0 }));
|
|
3706
|
+
if (!uuid) return c.json({ error: "Which prompt?" }, 400);
|
|
3707
|
+
const run4 = await ensureRun(c.req.param("id"));
|
|
3708
|
+
if (run4.busy) return c.json({ error: "Stop Claude before rewinding files." }, 409);
|
|
3709
|
+
return c.json(await run4.rewind(uuid, dryRun !== false));
|
|
3710
|
+
});
|
|
3711
|
+
app.get("/api/sessions/:id/mcp", async (c) => {
|
|
3712
|
+
const id = c.req.param("id");
|
|
3713
|
+
const run4 = getRun(id);
|
|
3714
|
+
if (run4) return c.json({ servers: await run4.mcpServers(), live: true });
|
|
3715
|
+
const info = await sessionInfo(id);
|
|
3716
|
+
if (!info?.cwd || !await isDirectory(info.cwd)) return c.json({ servers: [], live: false });
|
|
3717
|
+
return c.json({ servers: await probeMcpServers(info.cwd), live: false });
|
|
3718
|
+
});
|
|
3719
|
+
app.post("/api/sessions/:id/mcp/:name", async (c) => {
|
|
3720
|
+
const run4 = getRun(c.req.param("id"));
|
|
3721
|
+
if (!run4) return c.json({ error: "Claude is not running in this session" }, 409);
|
|
3722
|
+
const change = await c.req.json().catch(() => ({}));
|
|
3723
|
+
await run4.setMcpServer(c.req.param("name"), change);
|
|
3724
|
+
return c.json({ servers: await run4.mcpServers() });
|
|
3725
|
+
});
|
|
3726
|
+
app.post("/api/sessions/:id/tasks/:task/stop", async (c) => {
|
|
3727
|
+
await getRun(c.req.param("id"))?.stopTask(c.req.param("task"));
|
|
3728
|
+
return c.json({ ok: true });
|
|
3729
|
+
});
|
|
3730
|
+
app.get("/api/sessions/:id/sent", async (c) => {
|
|
3731
|
+
const file2 = c.req.query("path") ?? "";
|
|
3732
|
+
if (!file2 || !(await sentFiles(c.req.param("id"))).has(file2)) return c.json({ error: "Not a file Claude sent in this session" }, 404);
|
|
3733
|
+
try {
|
|
3734
|
+
return c.json(await readProjectFile(path14.dirname(file2), path14.basename(file2)));
|
|
3735
|
+
} catch {
|
|
3736
|
+
return c.json({ error: "The file is gone" }, 404);
|
|
3737
|
+
}
|
|
3738
|
+
});
|
|
3739
|
+
app.get("/api/sessions/:id/agents/:agent", async (c) => {
|
|
3740
|
+
return c.json({ messages: await loadSubagent(c.req.param("id"), c.req.param("agent")) });
|
|
3741
|
+
});
|
|
3742
|
+
app.get("/api/files", async (c) => {
|
|
3743
|
+
const cwd = await folderOf(c);
|
|
3744
|
+
if (!cwd) return c.json({ files: [] });
|
|
3745
|
+
return c.json({ files: await searchFiles(cwd, c.req.query("q") ?? "") });
|
|
3746
|
+
});
|
|
3747
|
+
app.get("/api/projects", async (c) => {
|
|
3748
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3749
|
+
for (const s of await listSummaries(runStatus)) {
|
|
3750
|
+
if (!s.cwd || s.scratch || s.temp) continue;
|
|
3751
|
+
const entry = seen.get(s.cwd);
|
|
3752
|
+
if (entry) entry.sessions += 1;
|
|
3753
|
+
else seen.set(s.cwd, { path: s.cwd, name: s.project, lastActivity: s.lastActivity, sessions: 1 });
|
|
3754
|
+
}
|
|
3755
|
+
const existing = (await Promise.all([...seen.values()].map(async (p) => await isDirectory(p.path) ? p : void 0))).filter((p) => Boolean(p));
|
|
3756
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3757
|
+
for (const p of existing) counts.set(p.name, (counts.get(p.name) ?? 0) + 1);
|
|
3758
|
+
for (const p of existing) {
|
|
3759
|
+
if ((counts.get(p.name) ?? 0) > 1) p.name = `${path14.basename(path14.dirname(p.path))}/${p.name}`;
|
|
3760
|
+
}
|
|
3761
|
+
return c.json({ projects: existing });
|
|
3762
|
+
});
|
|
3763
|
+
app.get("/api/dirs", async (c) => {
|
|
3764
|
+
const asked = c.req.query("path");
|
|
3765
|
+
if (config.demo && asked && !await inDemo(asked)) return c.json({ error: "Not part of the demo" }, 404);
|
|
3766
|
+
try {
|
|
3767
|
+
return c.json(await listDirs(asked, c.req.query("hidden") === "1"));
|
|
3768
|
+
} catch (error) {
|
|
3769
|
+
return c.json({ error: error.message }, 400);
|
|
3770
|
+
}
|
|
3771
|
+
});
|
|
3772
|
+
app.post("/api/uploads", async (c) => {
|
|
3773
|
+
const body = await c.req.parseBody();
|
|
3774
|
+
const file2 = body.file;
|
|
3775
|
+
if (!(file2 instanceof File)) return c.json({ error: "No file" }, 400);
|
|
3776
|
+
return c.json({ attachment: await saveUpload(file2) });
|
|
3777
|
+
});
|
|
3778
|
+
app.get("/api/uploads/:id", async (c) => {
|
|
3779
|
+
const found = await readUpload(c.req.param("id"));
|
|
3780
|
+
if (!found) return c.notFound();
|
|
3781
|
+
const image2 = found.attachment.mime.startsWith("image/") && found.attachment.mime !== "image/svg+xml";
|
|
3782
|
+
return c.body(new Uint8Array(found.data), 200, {
|
|
3783
|
+
"Content-Type": found.attachment.mime,
|
|
3784
|
+
"Cache-Control": "private, max-age=86400",
|
|
3785
|
+
"X-Content-Type-Options": "nosniff",
|
|
3786
|
+
"Content-Security-Policy": "sandbox; default-src 'none'; img-src 'self' data:; frame-ancestors 'self'",
|
|
3787
|
+
"X-Frame-Options": "SAMEORIGIN",
|
|
3788
|
+
...image2 ? {} : { "Content-Disposition": `attachment; filename*=UTF-8''${encodeURIComponent(found.attachment.name)}` }
|
|
3789
|
+
});
|
|
3790
|
+
});
|
|
3791
|
+
app.get("/api/push/key", (c) => c.json({ key: pushKey() ?? null }));
|
|
3792
|
+
pageUrl = (value) => typeof value === "string" && value.length <= 2048 && /^https?:\/\//.test(value);
|
|
3793
|
+
app.post("/api/push/subscribe", async (c) => {
|
|
3794
|
+
const body = await c.req.json().catch(() => ({}));
|
|
3795
|
+
if (!validSubscription(body.subscription)) return c.json({ error: "Not a push subscription" }, 400);
|
|
3796
|
+
const { endpoint, keys: keys2 } = body.subscription;
|
|
3797
|
+
await addSubscription({ endpoint, keys: { p256dh: keys2.p256dh, auth: keys2.auth }, ...pageUrl(body.openUrl) ? { openUrl: body.openUrl } : {} });
|
|
3798
|
+
return c.json({ ok: true });
|
|
3799
|
+
});
|
|
3800
|
+
app.post("/api/push/unsubscribe", async (c) => {
|
|
3801
|
+
const { endpoint } = await c.req.json().catch(() => ({ endpoint: void 0 }));
|
|
3802
|
+
if (typeof endpoint !== "string") return c.json({ error: "No endpoint" }, 400);
|
|
3803
|
+
await removeSubscription(endpoint);
|
|
3804
|
+
return c.json({ ok: true });
|
|
3805
|
+
});
|
|
3806
|
+
app.all("/api/*", (c) => c.json({ error: "Not found" }, 404));
|
|
3807
|
+
TYPES = {
|
|
3808
|
+
".html": "text/html; charset=utf-8",
|
|
3809
|
+
".js": "text/javascript; charset=utf-8",
|
|
3810
|
+
".css": "text/css; charset=utf-8",
|
|
3811
|
+
".svg": "image/svg+xml",
|
|
3812
|
+
".png": "image/png",
|
|
3813
|
+
".ico": "image/x-icon",
|
|
3814
|
+
".webmanifest": "application/manifest+json",
|
|
3815
|
+
".json": "application/json",
|
|
3816
|
+
".woff2": "font/woff2",
|
|
3817
|
+
".txt": "text/plain; charset=utf-8"
|
|
3818
|
+
};
|
|
3819
|
+
POLICY = {
|
|
3820
|
+
"Content-Security-Policy": [
|
|
3821
|
+
`frame-ancestors 'self'${config.frameAncestors.map((o) => ` ${o}`).join("")}`,
|
|
3822
|
+
"frame-src 'self' blob: data:",
|
|
3823
|
+
"img-src 'self' data: blob:",
|
|
3824
|
+
"media-src 'self' data: blob:",
|
|
3825
|
+
"object-src 'none'",
|
|
3826
|
+
"base-uri 'self'"
|
|
3827
|
+
].join("; "),
|
|
3828
|
+
...config.frameAncestors.length === 0 ? { "X-Frame-Options": "SAMEORIGIN" } : {}
|
|
3829
|
+
};
|
|
3830
|
+
app.get("/*", async (c) => {
|
|
3831
|
+
if (!config.webDir) return c.text("The web app is not built. Run `npm run build`, or use `npm run dev`.", 503);
|
|
3832
|
+
const root = config.webDir;
|
|
3833
|
+
let pathname;
|
|
3834
|
+
try {
|
|
3835
|
+
pathname = decodeURIComponent(new URL(c.req.url).pathname);
|
|
3836
|
+
} catch {
|
|
3837
|
+
return c.text("Bad path", 400);
|
|
3838
|
+
}
|
|
3839
|
+
const token2 = c.req.query("token");
|
|
3840
|
+
const code = c.req.query("code");
|
|
3841
|
+
if (token2 !== void 0 || code !== void 0) {
|
|
3842
|
+
const url = new URL(c.req.url);
|
|
3843
|
+
url.searchParams.delete("token");
|
|
3844
|
+
url.searchParams.delete("code");
|
|
3845
|
+
const location = `./${url.pathname.split("/").pop() ?? ""}${url.search}`;
|
|
3846
|
+
const ok = token2 !== void 0 ? checkToken(token2) : redeemCode(code);
|
|
3847
|
+
if (!ok) return c.body(null, 302, { Location: location, "Cache-Control": "no-store" });
|
|
3848
|
+
const cookie = newSessionCookie();
|
|
3849
|
+
return c.body(null, 302, { Location: location, "Set-Cookie": cookieHeader(c, cookie.name, cookie.value, cookie.maxAge), "Cache-Control": "no-store" });
|
|
3850
|
+
}
|
|
3851
|
+
const requested = path14.normalize(pathname).replace(/^(\.\.[/\\])+/, "");
|
|
3852
|
+
let file2 = path14.join(root, requested);
|
|
3853
|
+
if (!file2.startsWith(root) || requested === "/" || !path14.extname(file2)) file2 = path14.join(root, "index.html");
|
|
3854
|
+
try {
|
|
3855
|
+
const data = await readFile7(file2);
|
|
3856
|
+
const ext = path14.extname(file2);
|
|
3857
|
+
const hashed = file2.includes(`${path14.sep}assets${path14.sep}`);
|
|
3858
|
+
return c.body(new Uint8Array(data), 200, {
|
|
3859
|
+
"Content-Type": TYPES[ext] ?? "application/octet-stream",
|
|
3860
|
+
"Cache-Control": hashed ? "public, max-age=31536000, immutable" : "no-cache",
|
|
3861
|
+
"X-Content-Type-Options": "nosniff",
|
|
3862
|
+
"Referrer-Policy": "same-origin",
|
|
3863
|
+
...POLICY
|
|
3864
|
+
});
|
|
3865
|
+
} catch {
|
|
3866
|
+
return c.notFound();
|
|
3867
|
+
}
|
|
3868
|
+
});
|
|
3869
|
+
}
|
|
3870
|
+
});
|
|
3871
|
+
|
|
3872
|
+
// src/server/server.ts
|
|
3873
|
+
var server_exports = {};
|
|
3874
|
+
__export(server_exports, {
|
|
3875
|
+
startServer: () => startServer
|
|
3876
|
+
});
|
|
3877
|
+
import { serve } from "@hono/node-server";
|
|
3878
|
+
import { WebSocketServer } from "ws";
|
|
3879
|
+
function clientEvent(text2) {
|
|
3880
|
+
let value;
|
|
3881
|
+
try {
|
|
3882
|
+
value = JSON.parse(text2);
|
|
3883
|
+
} catch {
|
|
3884
|
+
return void 0;
|
|
3885
|
+
}
|
|
3886
|
+
if (!value || typeof value !== "object") return void 0;
|
|
3887
|
+
const { type, sessionId } = value;
|
|
3888
|
+
if (type !== "subscribe" && type !== "unsubscribe" || typeof sessionId !== "string" || !SESSION_ID.test(sessionId)) return void 0;
|
|
3889
|
+
return { type, sessionId };
|
|
3890
|
+
}
|
|
3891
|
+
async function startServer() {
|
|
3892
|
+
await loadAuth();
|
|
3893
|
+
await initPush();
|
|
3894
|
+
const url = await new Promise((resolve, reject) => {
|
|
3895
|
+
const server = serve({ fetch: app.fetch, hostname: config.host, port: config.port }, (info) => {
|
|
3896
|
+
const host = info.family === "IPv6" ? `[${info.address}]` : info.address;
|
|
3897
|
+
resolve(`http://${host}:${info.port}/`);
|
|
3898
|
+
});
|
|
3899
|
+
server.on("error", reject);
|
|
3900
|
+
const wss = new WebSocketServer({ noServer: true, maxPayload: 64 * 1024 });
|
|
3901
|
+
server.on("upgrade", (req, socket, head) => {
|
|
3902
|
+
const pathname = new URL(req.url ?? "/", "http://x").pathname;
|
|
3903
|
+
const forwarded = req.headers["x-forwarded-host"];
|
|
3904
|
+
const ok = pathname.endsWith("/api/ws") && hostAllowed(req.headers.host) && originAllowed(req.headers.origin, req.headers.host, Array.isArray(forwarded) ? forwarded[0] : forwarded) && signedIn(req.headers.cookie, req.headers.authorization);
|
|
3905
|
+
if (!ok) {
|
|
3906
|
+
socket.destroy();
|
|
3907
|
+
return;
|
|
3908
|
+
}
|
|
3909
|
+
wss.handleUpgrade(req, socket, head, (ws) => wss.emit("connection", ws, req));
|
|
3910
|
+
});
|
|
3911
|
+
wss.on("connection", (ws) => {
|
|
3912
|
+
const send = (event) => {
|
|
3913
|
+
if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(event));
|
|
3914
|
+
};
|
|
3915
|
+
const disconnect = connect(send);
|
|
3916
|
+
const subscriptions2 = /* @__PURE__ */ new Map();
|
|
3917
|
+
const ping = setInterval(() => ws.ping(), 25e3);
|
|
3918
|
+
ws.on("error", () => ws.terminate());
|
|
3919
|
+
ws.on("message", (data) => {
|
|
3920
|
+
const event = clientEvent(String(data));
|
|
3921
|
+
if (!event) return;
|
|
3922
|
+
if (event.type === "subscribe" && !subscriptions2.has(event.sessionId)) {
|
|
3923
|
+
if (subscriptions2.size >= MAX_SUBSCRIPTIONS) return;
|
|
3924
|
+
subscriptions2.set(event.sessionId, subscribe(event.sessionId, send));
|
|
3925
|
+
const state = runState(event.sessionId);
|
|
3926
|
+
if (state) send({ type: "run", state });
|
|
3927
|
+
} else if (event.type === "unsubscribe") {
|
|
3928
|
+
subscriptions2.get(event.sessionId)?.();
|
|
3929
|
+
subscriptions2.delete(event.sessionId);
|
|
3930
|
+
}
|
|
3931
|
+
});
|
|
3932
|
+
ws.on("close", () => {
|
|
3933
|
+
clearInterval(ping);
|
|
3934
|
+
disconnect();
|
|
3935
|
+
for (const unsubscribe of subscriptions2.values()) unsubscribe();
|
|
3936
|
+
});
|
|
3937
|
+
});
|
|
3938
|
+
const shutdown = () => {
|
|
3939
|
+
closeAll();
|
|
3940
|
+
server.close();
|
|
3941
|
+
setTimeout(() => process.exit(0), 500).unref();
|
|
3942
|
+
};
|
|
3943
|
+
process.on("SIGINT", shutdown);
|
|
3944
|
+
process.on("SIGTERM", shutdown);
|
|
3945
|
+
});
|
|
3946
|
+
return { url };
|
|
3947
|
+
}
|
|
3948
|
+
var SESSION_ID, MAX_SUBSCRIPTIONS;
|
|
3949
|
+
var init_server = __esm({
|
|
3950
|
+
"src/server/server.ts"() {
|
|
3951
|
+
"use strict";
|
|
3952
|
+
init_app();
|
|
3953
|
+
init_auth();
|
|
3954
|
+
init_config();
|
|
3955
|
+
init_feed();
|
|
3956
|
+
init_push();
|
|
3957
|
+
init_runner();
|
|
3958
|
+
init_security();
|
|
3959
|
+
SESSION_ID = /^[\w-]{1,100}$/;
|
|
3960
|
+
MAX_SUBSCRIPTIONS = 64;
|
|
3961
|
+
}
|
|
3962
|
+
});
|
|
3963
|
+
|
|
3964
|
+
// src/shared/qr.ts
|
|
3965
|
+
import qrcode from "qrcode-generator";
|
|
3966
|
+
function qrModules(text2) {
|
|
3967
|
+
const qr = qrcode(0, "M");
|
|
3968
|
+
qr.addData(text2);
|
|
3969
|
+
qr.make();
|
|
3970
|
+
const size = qr.getModuleCount();
|
|
3971
|
+
const rows = [];
|
|
3972
|
+
for (let y = -QUIET; y < size + QUIET; y += 1) {
|
|
3973
|
+
const row = [];
|
|
3974
|
+
for (let x = -QUIET; x < size + QUIET; x += 1) row.push(x >= 0 && y >= 0 && x < size && y < size && qr.isDark(y, x));
|
|
3975
|
+
rows.push(row);
|
|
3976
|
+
}
|
|
3977
|
+
return rows;
|
|
3978
|
+
}
|
|
3979
|
+
var QUIET;
|
|
3980
|
+
var init_qr = __esm({
|
|
3981
|
+
"src/shared/qr.ts"() {
|
|
3982
|
+
"use strict";
|
|
3983
|
+
QUIET = 2;
|
|
3984
|
+
}
|
|
3985
|
+
});
|
|
3986
|
+
|
|
3987
|
+
// src/server/qr.ts
|
|
3988
|
+
var qr_exports = {};
|
|
3989
|
+
__export(qr_exports, {
|
|
3990
|
+
qrTerminal: () => qrTerminal
|
|
3991
|
+
});
|
|
3992
|
+
function qrTerminal(text2) {
|
|
3993
|
+
const rows = qrModules(text2);
|
|
3994
|
+
const lines = [];
|
|
3995
|
+
for (let y = 0; y < rows.length; y += 2) {
|
|
3996
|
+
let line = "";
|
|
3997
|
+
for (let x = 0; x < rows[y].length; x += 1) {
|
|
3998
|
+
const top = rows[y][x] ? 16 : 231;
|
|
3999
|
+
const bottom = rows[y + 1]?.[x] ? 16 : 231;
|
|
4000
|
+
line += `\x1B[38;5;${top};48;5;${bottom}m\u2580`;
|
|
4001
|
+
}
|
|
4002
|
+
lines.push(`${line}\x1B[0m`);
|
|
4003
|
+
}
|
|
4004
|
+
return lines.join("\n");
|
|
4005
|
+
}
|
|
4006
|
+
var init_qr2 = __esm({
|
|
4007
|
+
"src/server/qr.ts"() {
|
|
4008
|
+
"use strict";
|
|
4009
|
+
init_qr();
|
|
4010
|
+
}
|
|
4011
|
+
});
|
|
4012
|
+
|
|
4013
|
+
// src/server/cli.ts
|
|
4014
|
+
init_package();
|
|
4015
|
+
init_config();
|
|
4016
|
+
init_security();
|
|
4017
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
4018
|
+
import { mkdir as mkdir7, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
|
|
4019
|
+
import os6 from "node:os";
|
|
4020
|
+
import path15 from "node:path";
|
|
4021
|
+
import { pathToFileURL } from "node:url";
|
|
4022
|
+
import { parseArgs } from "node:util";
|
|
4023
|
+
var DOCS = "https://ki-seki.github.io/pelican/docs.html";
|
|
4024
|
+
var HELP = `Pelican ${package_default.version}: your Claude Code sessions, from any browser.
|
|
4025
|
+
|
|
4026
|
+
Usage
|
|
4027
|
+
pelican [options] Start the server
|
|
4028
|
+
pelican token Print the link that signs a browser in
|
|
4029
|
+
pelican token --reset Make a new access token; old links stop working
|
|
4030
|
+
pelican password Sign in with a password instead of the token
|
|
4031
|
+
pelican password --off Remove the password; the token signs in again
|
|
4032
|
+
|
|
4033
|
+
Options
|
|
4034
|
+
-p, --port <port> Port to listen on (default 7860, or PELICAN_PORT)
|
|
4035
|
+
-H, --host <address> Address to listen on (default 127.0.0.1, or PELICAN_HOST)
|
|
4036
|
+
--name <name> Name shown for this machine (default: its host name)
|
|
4037
|
+
--no-auth Ask no one to sign in. Only when something in front
|
|
4038
|
+
of Pelican already authenticates.
|
|
4039
|
+
--demo Show made-up sessions; nothing runs
|
|
4040
|
+
--open Open the browser
|
|
4041
|
+
-v, --version Print the version
|
|
4042
|
+
-h, --help Show this help
|
|
4043
|
+
|
|
4044
|
+
Pelican runs Claude Code, and so commands, as you. It listens on 127.0.0.1, and
|
|
4045
|
+
every browser signs in, with the access token in the link Pelican prints or
|
|
4046
|
+
with your password. To use it from your phone, see ${DOCS}#phone
|
|
4047
|
+
`;
|
|
4048
|
+
function askHidden(prompt) {
|
|
4049
|
+
return new Promise((resolve, reject) => {
|
|
4050
|
+
const input = process.stdin;
|
|
4051
|
+
if (!input.isTTY) {
|
|
4052
|
+
reject(new Error("Run `pelican password` in a terminal, or pipe the password in with --stdin."));
|
|
4053
|
+
return;
|
|
4054
|
+
}
|
|
4055
|
+
process.stdout.write(prompt);
|
|
4056
|
+
input.setRawMode(true);
|
|
4057
|
+
input.resume();
|
|
4058
|
+
input.setEncoding("utf8");
|
|
4059
|
+
let value = "";
|
|
4060
|
+
const onData = (chunk) => {
|
|
4061
|
+
for (const char of chunk) {
|
|
4062
|
+
if (char === "\r" || char === "\n") {
|
|
4063
|
+
input.setRawMode(false);
|
|
4064
|
+
input.pause();
|
|
4065
|
+
input.off("data", onData);
|
|
4066
|
+
process.stdout.write("\n");
|
|
4067
|
+
resolve(value);
|
|
4068
|
+
return;
|
|
4069
|
+
}
|
|
4070
|
+
if (char === "") {
|
|
4071
|
+
process.stdout.write("\n");
|
|
4072
|
+
process.exit(130);
|
|
4073
|
+
}
|
|
4074
|
+
if (char === "\x7F" || char === "\b") value = value.slice(0, -1);
|
|
4075
|
+
else value += char;
|
|
4076
|
+
}
|
|
4077
|
+
};
|
|
4078
|
+
input.on("data", onData);
|
|
4079
|
+
});
|
|
4080
|
+
}
|
|
4081
|
+
async function readStdin() {
|
|
4082
|
+
let data = "";
|
|
4083
|
+
for await (const chunk of process.stdin) data += chunk;
|
|
4084
|
+
return data.replace(/\r?\n$/, "");
|
|
4085
|
+
}
|
|
4086
|
+
async function passwordCommand(off, stdin) {
|
|
4087
|
+
const { clearPassword: clearPassword2, loadAuth: loadAuth2, setPassword: setPassword2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
|
|
4088
|
+
await loadAuth2();
|
|
4089
|
+
if (off) {
|
|
4090
|
+
await clearPassword2();
|
|
4091
|
+
console.log("Password removed: browsers sign in with the access token again (`pelican token`). Restart Pelican to apply.");
|
|
4092
|
+
return;
|
|
4093
|
+
}
|
|
4094
|
+
const password2 = stdin ? await readStdin() : await askHidden("New password: ");
|
|
4095
|
+
if (password2.length < 8) throw new Error("Use at least 8 characters.");
|
|
4096
|
+
if (!stdin && await askHidden("Repeat it: ") !== password2) throw new Error("The two did not match.");
|
|
4097
|
+
await setPassword2(password2);
|
|
4098
|
+
console.log(`Password saved in ${config.configDir}. Restart Pelican to apply; signed-in browsers are signed out.`);
|
|
4099
|
+
}
|
|
4100
|
+
var tokenLink = (base, token2) => `${base}?token=${token2}`;
|
|
4101
|
+
function localAddress() {
|
|
4102
|
+
const h = config.host;
|
|
4103
|
+
const host = ["0.0.0.0", "::", "127.0.0.1", "localhost", "::1"].includes(h) ? "localhost" : h.includes(":") ? `[${h}]` : h;
|
|
4104
|
+
return `http://${host}:${config.port}/`;
|
|
4105
|
+
}
|
|
4106
|
+
async function tokenCommand(reset) {
|
|
4107
|
+
const { accessToken: accessToken2, authMode: authMode2, loadAuth: loadAuth2, resetToken: resetToken2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
|
|
4108
|
+
await loadAuth2();
|
|
4109
|
+
if (authMode2() === "none") {
|
|
4110
|
+
console.log("Sign-in is off (--no-auth or PELICAN_AUTH=none), so there is no access token.");
|
|
4111
|
+
return;
|
|
4112
|
+
}
|
|
4113
|
+
if (reset) {
|
|
4114
|
+
await resetToken2();
|
|
4115
|
+
console.log("New access token made. Restart Pelican to apply; browsers signed in with the old one are signed out.");
|
|
4116
|
+
}
|
|
4117
|
+
if (authMode2() === "password") {
|
|
4118
|
+
console.log("A password is set, so browsers sign in with it. `pelican password --off` goes back to the token.");
|
|
4119
|
+
return;
|
|
4120
|
+
}
|
|
4121
|
+
const token2 = accessToken2();
|
|
4122
|
+
if (token2) console.log(tokenLink(localAddress(), token2));
|
|
4123
|
+
}
|
|
4124
|
+
async function openPelican(address, token2) {
|
|
4125
|
+
if (!token2 || process.platform !== "darwin") {
|
|
4126
|
+
openBrowser(address);
|
|
4127
|
+
return;
|
|
4128
|
+
}
|
|
4129
|
+
const file2 = path15.join(config.configDir, "open.html");
|
|
4130
|
+
try {
|
|
4131
|
+
await mkdir7(config.configDir, { recursive: true, mode: 448 });
|
|
4132
|
+
await rm4(file2, { force: true });
|
|
4133
|
+
const link = tokenLink(address, token2);
|
|
4134
|
+
await writeFile6(file2, `<!doctype html><meta http-equiv="refresh" content="0;url=${link}"><a href="${link}">Open Pelican</a>
|
|
4135
|
+
`, { mode: 384 });
|
|
4136
|
+
openBrowser(pathToFileURL(file2).href);
|
|
4137
|
+
setTimeout(() => void rm4(file2, { force: true }), 3e4).unref();
|
|
4138
|
+
} catch {
|
|
4139
|
+
openBrowser(address);
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
function openBrowser(url) {
|
|
4143
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
4144
|
+
try {
|
|
4145
|
+
spawn2(opener, [url], { stdio: "ignore", detached: true, shell: process.platform === "win32" }).on("error", () => {
|
|
4146
|
+
}).unref();
|
|
4147
|
+
} catch {
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
4150
|
+
async function main() {
|
|
4151
|
+
const { values, positionals } = parseArgs({
|
|
4152
|
+
allowPositionals: true,
|
|
4153
|
+
options: {
|
|
4154
|
+
port: { type: "string", short: "p" },
|
|
4155
|
+
host: { type: "string", short: "H" },
|
|
4156
|
+
name: { type: "string" },
|
|
4157
|
+
"no-auth": { type: "boolean" },
|
|
4158
|
+
demo: { type: "boolean" },
|
|
4159
|
+
open: { type: "boolean" },
|
|
4160
|
+
off: { type: "boolean" },
|
|
4161
|
+
reset: { type: "boolean" },
|
|
4162
|
+
stdin: { type: "boolean" },
|
|
4163
|
+
version: { type: "boolean", short: "v" },
|
|
4164
|
+
help: { type: "boolean", short: "h" }
|
|
4165
|
+
}
|
|
4166
|
+
});
|
|
4167
|
+
if (values.help) return void process.stdout.write(HELP);
|
|
4168
|
+
if (values.version) return void console.log(package_default.version);
|
|
4169
|
+
if (values.port) config.port = Number(values.port);
|
|
4170
|
+
if (values.host) config.host = values.host;
|
|
4171
|
+
if (values.name) config.machine = values.name;
|
|
4172
|
+
if (values["no-auth"]) config.auth = "none";
|
|
4173
|
+
if (!Number.isInteger(config.port) || config.port <= 0 || config.port > 65535) throw new Error(`Not a port: ${values.port}`);
|
|
4174
|
+
if (positionals[0] === "password") return passwordCommand(Boolean(values.off), Boolean(values.stdin));
|
|
4175
|
+
if (positionals[0] === "token") return tokenCommand(Boolean(values.reset));
|
|
4176
|
+
if (positionals.length > 0) throw new Error(`Unknown command: ${positionals.join(" ")}. See pelican --help.`);
|
|
4177
|
+
const noAuth = config.auth === "none";
|
|
4178
|
+
if (values.demo) {
|
|
4179
|
+
config.demo = true;
|
|
4180
|
+
const { prepareDemo: prepareDemo2 } = await Promise.resolve().then(() => (init_demo(), demo_exports));
|
|
4181
|
+
await prepareDemo2();
|
|
4182
|
+
if (!values.name) config.machine = "demo";
|
|
4183
|
+
}
|
|
4184
|
+
const local = listensLocally(config.host);
|
|
4185
|
+
if (config.demo) {
|
|
4186
|
+
if (!local && !noAuth) throw new Error("The demo signs no one in, so it listens on 127.0.0.1 only. Pass --no-auth to show it on your network.");
|
|
4187
|
+
config.auth = "none";
|
|
4188
|
+
}
|
|
4189
|
+
const { accessToken: accessToken2, authMode: authMode2, loadAuth: loadAuth2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
|
|
4190
|
+
await loadAuth2();
|
|
4191
|
+
const { configWarnings: configWarnings2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
4192
|
+
for (const warning of configWarnings2) console.warn(`pelican: ${warning}`);
|
|
4193
|
+
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
4194
|
+
const { url } = await startServer2();
|
|
4195
|
+
const shown = url.replace("://127.0.0.1:", "://localhost:");
|
|
4196
|
+
const token2 = accessToken2();
|
|
4197
|
+
const mode = authMode2();
|
|
4198
|
+
const interactive = Boolean(process.stdout.isTTY);
|
|
4199
|
+
const open4 = token2 && interactive ? tokenLink(shown, token2) : shown;
|
|
4200
|
+
const protection = config.demo ? "demo: made-up sessions, nothing runs" : mode === "password" ? "browsers sign in with your password" : mode === "token" ? interactive ? "the link signs a browser in (`pelican token` prints it again)" : "browsers sign in with the link `pelican token` prints" : local ? "no sign-in (--no-auth)" : "no sign-in (--no-auth): anyone who reaches this address can run commands as you";
|
|
4201
|
+
console.log(`
|
|
4202
|
+
Pelican ${package_default.version} \xB7 ${config.machine}
|
|
4203
|
+
${open4}
|
|
4204
|
+
${protection}
|
|
4205
|
+
`);
|
|
4206
|
+
if (!config.webDir) console.log(" The web app is not built: run `npm run build` (or use `npm run dev`).\n");
|
|
4207
|
+
if (!local) {
|
|
4208
|
+
const { qrTerminal: qrTerminal2 } = await Promise.resolve().then(() => (init_qr2(), qr_exports));
|
|
4209
|
+
const lan = Object.values(os6.networkInterfaces()).flat().find((a) => a && a.family === "IPv4" && !a.internal);
|
|
4210
|
+
const base = config.host === "0.0.0.0" || config.host === "::" ? lan ? `http://${lan.address}:${config.port}/` : void 0 : url;
|
|
4211
|
+
const phone = base && token2 && interactive ? tokenLink(base, token2) : base;
|
|
4212
|
+
if (phone) console.log(` On your phone: ${phone}
|
|
4213
|
+
${interactive ? `
|
|
4214
|
+
${qrTerminal2(phone).replace(/^/gm, " ")}
|
|
4215
|
+
` : ""}`);
|
|
4216
|
+
} else if (!config.demo) {
|
|
4217
|
+
console.log(` From your phone: ${DOCS}#phone
|
|
4218
|
+
`);
|
|
4219
|
+
}
|
|
4220
|
+
if (values.open) await openPelican(shown, token2);
|
|
4221
|
+
}
|
|
4222
|
+
main().catch((error) => {
|
|
4223
|
+
console.error(`pelican: ${error.message}`);
|
|
4224
|
+
process.exit(1);
|
|
4225
|
+
});
|
|
4226
|
+
//# sourceMappingURL=index.js.map
|