@epoch-agent/server 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -0
- package/dist/index.d.ts +146 -1
- package/dist/index.js +2342 -2157
- package/dist/web/assets/{index-jZ8vNU-i.css → index-BKuZsaI1.css} +1 -1
- package/dist/web/assets/index-DFppot0z.js +378 -0
- package/dist/web/index.html +2 -2
- package/package.json +3 -3
- package/dist/web/assets/index-BDgdZXwy.js +0 -378
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { createServer } from 'http';
|
|
2
2
|
import { createLogger, t, ApprovalRelay, QuestionRelay, serializeStream, withRoleScope, withModelScope, withToolScope, nextRunAt, observeBackgroundTasks, skillIndexDescription, SANDBOX_EXCLUDES, SANDBOX_COVERS, canProbeGit, guardSpawn, isSystemOpenable, backgroundTaskOutput, listBackgroundTasks, providerLabel, MAX_REASON_CHARS, MAX_OBJECTIVE_CHARS, MAX_GOAL_MAX_ROUNDS, DEFAULT_GOAL_MAX_ROUNDS, SCHEDULE_DEFAULT_ALLOWLIST, SCHEDULE_FORM_BUDGET_USD, SCHEDULE_DEFAULTS, coalesceRecording, readRecording } from '@epoch-agent/runtime';
|
|
3
|
-
import { timingSafeEqual, randomBytes } from 'crypto';
|
|
3
|
+
import { timingSafeEqual, randomBytes, randomUUID } from 'crypto';
|
|
4
4
|
import { PLAN_OUTCOMES, WIRE_AUTH_COOKIE, WIRE_AUTH_COOKIE_ATTRS, WIRE_AUTH_TOKEN_PARAM, extractAllMentions, MAX_ATTACH_FILES, utf8ByteLength, MAX_SESSION_REFS, attachSessionSurface, isWireRewindScope, WIRE_REWIND_SCOPES, mcpConnectorState, MAX_ATTACH_TOTAL_BYTES, MAX_ATTACH_BYTES, truncateToBudget, WIRE_LANG_PARAM, isLang, conformsToWire, WIRE_LAST_SESSION_FIELDS, isQuestionAnswerMap, isProviderType, apiKeyEnvVar, WIRE_FILE_STAT_MAX, WIRE_SETTING_WRITE_LAYERS, isWirePlanAction, WIRE_PLAN_ACTIONS, isPermissionLevel, PERMISSION_LEVELS, WIRE_TRUST_REQUEST_FIELDS, WIRE_FORGET_WORKSPACE_FIELDS, collectPendingApprovals, unfixedRules, isWireSettingWriteLayer, parseModelRef, WIRE_GOAL_ACTIONS, OPERATION_TYPES, isOperationType, WIRE_PLUGIN_UPLOAD_FORM_FIELDS } from '@epoch-agent/protocol';
|
|
5
5
|
import { resolve, join, relative, isAbsolute, sep, dirname, basename, extname } from 'path';
|
|
6
6
|
import { existsSync, realpathSync, statSync, mkdirSync, createReadStream, readdirSync, createWriteStream, accessSync, constants } from 'fs';
|
|
7
7
|
import { stat, readFile, rm } from 'fs/promises';
|
|
8
8
|
import { pipeline } from 'stream/promises';
|
|
9
|
-
import { homedir } from 'os';
|
|
10
9
|
import { execFile, spawn } from 'child_process';
|
|
10
|
+
import { homedir } from 'os';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
12
|
|
|
13
13
|
// src/index.ts
|
|
@@ -137,6 +137,70 @@ function firstScreenUrl(host, port, token, ui = {}) {
|
|
|
137
137
|
if (ui.lang) params.push(`${UI_LANG_PARAM}=${encodeURIComponent(ui.lang)}`);
|
|
138
138
|
return `http://${authority}:${port}/?${params.join("&")}`;
|
|
139
139
|
}
|
|
140
|
+
var HOST_COMPOSE_TTL_MS = 10 * 6e4;
|
|
141
|
+
var HostComposeStore = class {
|
|
142
|
+
items = /* @__PURE__ */ new Map();
|
|
143
|
+
now;
|
|
144
|
+
ttlMs;
|
|
145
|
+
newId;
|
|
146
|
+
constructor(opts = {}) {
|
|
147
|
+
this.now = opts.now ?? Date.now;
|
|
148
|
+
this.ttlMs = opts.ttlMs ?? HOST_COMPOSE_TTL_MS;
|
|
149
|
+
this.newId = opts.newId ?? (() => `hc_${randomUUID()}`);
|
|
150
|
+
}
|
|
151
|
+
record(input) {
|
|
152
|
+
const now = this.now();
|
|
153
|
+
this.sweep(now);
|
|
154
|
+
for (const [id, item2] of this.items) {
|
|
155
|
+
if (item2.sessionId === input.sessionId && item2.takenAt === void 0) this.items.delete(id);
|
|
156
|
+
}
|
|
157
|
+
const item = {
|
|
158
|
+
id: this.newId(),
|
|
159
|
+
sessionId: input.sessionId,
|
|
160
|
+
text: input.text,
|
|
161
|
+
autoSend: input.autoSend,
|
|
162
|
+
createdAt: now
|
|
163
|
+
};
|
|
164
|
+
this.items.set(item.id, item);
|
|
165
|
+
return item;
|
|
166
|
+
}
|
|
167
|
+
pending() {
|
|
168
|
+
this.sweep(this.now());
|
|
169
|
+
let newest = null;
|
|
170
|
+
for (const item of this.items.values()) {
|
|
171
|
+
if (item.takenAt !== void 0) continue;
|
|
172
|
+
if (newest === null || item.createdAt >= newest.createdAt) newest = item;
|
|
173
|
+
}
|
|
174
|
+
return newest;
|
|
175
|
+
}
|
|
176
|
+
take(id) {
|
|
177
|
+
const now = this.now();
|
|
178
|
+
this.sweep(now, id);
|
|
179
|
+
const item = this.items.get(id);
|
|
180
|
+
if (item === void 0) return "unknown";
|
|
181
|
+
if (now - item.createdAt >= this.ttlMs) {
|
|
182
|
+
this.items.delete(id);
|
|
183
|
+
return "expired";
|
|
184
|
+
}
|
|
185
|
+
if (item.takenAt !== void 0) return "taken";
|
|
186
|
+
item.takenAt = now;
|
|
187
|
+
return "ok";
|
|
188
|
+
}
|
|
189
|
+
sweep(now, keep) {
|
|
190
|
+
for (const [id, item] of this.items) {
|
|
191
|
+
if (id === keep) continue;
|
|
192
|
+
if (now - item.createdAt >= this.ttlMs) this.items.delete(id);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
function toWireHostCompose(item) {
|
|
197
|
+
return {
|
|
198
|
+
id: item.id,
|
|
199
|
+
sessionId: item.sessionId,
|
|
200
|
+
text: item.text,
|
|
201
|
+
autoSend: item.autoSend
|
|
202
|
+
};
|
|
203
|
+
}
|
|
140
204
|
// src/ring.ts
|
|
141
205
|
var DEFAULT_RING_CAPACITY = 512;
|
|
142
206
|
var EnvelopeRing = class {
|
|
@@ -597,6 +661,14 @@ var SessionHub = class {
|
|
|
597
661
|
if (this.closed) return;
|
|
598
662
|
this.publish(sessionId, { type: "sessions-changed" });
|
|
599
663
|
}
|
|
664
|
+
notifyPluginsChanged() {
|
|
665
|
+
if (this.closed) return;
|
|
666
|
+
this.publish(void 0, { type: "plugins-changed" });
|
|
667
|
+
}
|
|
668
|
+
notifyHostCompose(sessionId, payload) {
|
|
669
|
+
if (this.closed) return;
|
|
670
|
+
this.publish(sessionId, { type: "host-compose", ...payload });
|
|
671
|
+
}
|
|
600
672
|
shutdown() {
|
|
601
673
|
if (this.closed) return;
|
|
602
674
|
this.closed = true;
|
|
@@ -607,10 +679,15 @@ var SessionHub = class {
|
|
|
607
679
|
this.counted.clear();
|
|
608
680
|
}
|
|
609
681
|
publish(sessionId, event) {
|
|
610
|
-
const frame = {
|
|
682
|
+
const frame = {
|
|
683
|
+
seq: ++this.seq,
|
|
684
|
+
...sessionId === void 0 ? {} : { sessionId },
|
|
685
|
+
ts: this.clock(),
|
|
686
|
+
event
|
|
687
|
+
};
|
|
611
688
|
this.ring.push(frame);
|
|
612
689
|
for (const sink of this.sinks) sink(frame);
|
|
613
|
-
for (const sink of this.observers) sink(frame);
|
|
690
|
+
if (sessionId !== void 0) for (const sink of this.observers) sink(frame);
|
|
614
691
|
return frame;
|
|
615
692
|
}
|
|
616
693
|
connectionFrame(event) {
|
|
@@ -1654,12 +1731,19 @@ function lastSessionField(ctx) {
|
|
|
1654
1731
|
if (!ctx.hub.has(recorded) && !ctx.runtime.sessions?.get(recorded)) return {};
|
|
1655
1732
|
return { lastSessionId: recorded };
|
|
1656
1733
|
}
|
|
1734
|
+
function hostComposeField(ctx) {
|
|
1735
|
+
const item = ctx.hostCompose.pending();
|
|
1736
|
+
if (!item) return {};
|
|
1737
|
+
if (!ctx.hub.has(item.sessionId) && !ctx.runtime.sessions?.get(item.sessionId)) return {};
|
|
1738
|
+
return { hostCompose: toWireHostCompose(item) };
|
|
1739
|
+
}
|
|
1657
1740
|
function config(ctx, res, lang) {
|
|
1658
1741
|
const { runtime } = ctx;
|
|
1659
1742
|
const bound = runtime.workspaces.of(runtime.sessionId);
|
|
1660
1743
|
sendJson(res, 200, {
|
|
1661
1744
|
sessionId: runtime.sessionId,
|
|
1662
1745
|
...lastSessionField(ctx),
|
|
1746
|
+
...hostComposeField(ctx),
|
|
1663
1747
|
model: runtime.config.model,
|
|
1664
1748
|
permission: runtime.config.permission,
|
|
1665
1749
|
workspace: bound ? toWireSessionWorkspace(bound) : null,
|
|
@@ -2369,2467 +2453,2555 @@ function listBlockedGoals(ctx, res, _lang) {
|
|
|
2369
2453
|
);
|
|
2370
2454
|
sendJson(res, 200, { items });
|
|
2371
2455
|
}
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
2389
|
-
return sendError(res, 400, "bad-request", t("web.last_session_bad_request", {}, lang));
|
|
2390
|
-
}
|
|
2391
|
-
if (!ctx.hub.has(sessionId) && !ctx.runtime.sessions?.get(sessionId)) {
|
|
2392
|
-
return sendUnknownSession(res, sessionId, lang);
|
|
2393
|
-
}
|
|
2394
|
-
const failure = control.set(sessionId);
|
|
2395
|
-
if (failure) return sendError(res, 500, "write-failed", failure(lang));
|
|
2396
|
-
sendJson(res, 200, { ok: true });
|
|
2397
|
-
}
|
|
2398
|
-
function readConfig(ctx, res, lang) {
|
|
2399
|
-
if (ctx.lanExposed) return sendLanExposed(res, lang);
|
|
2400
|
-
const outcome = ctx.runtime.mcp.readConfig();
|
|
2401
|
-
if (!outcome.ok) {
|
|
2402
|
-
return sendError(
|
|
2403
|
-
res,
|
|
2404
|
-
409,
|
|
2405
|
-
"mcp-config-unreadable",
|
|
2406
|
-
t("web.mcp_config_unreadable", { detail: outcome.detail }, lang)
|
|
2456
|
+
var GIT_TIMEOUT_MS = 1e4;
|
|
2457
|
+
var GIT_MAX_BUFFER = 16 * 1024 * 1024;
|
|
2458
|
+
function runGit(cwd, args) {
|
|
2459
|
+
if (!canProbeGit()) return Promise.resolve({ ok: false, error: "git unavailable" });
|
|
2460
|
+
return new Promise((resolve4) => {
|
|
2461
|
+
guardSpawn(
|
|
2462
|
+
() => execFile(
|
|
2463
|
+
"git",
|
|
2464
|
+
[...args],
|
|
2465
|
+
{ cwd, timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_MAX_BUFFER, encoding: "buffer" },
|
|
2466
|
+
(err, stdout) => {
|
|
2467
|
+
if (err) return resolve4({ ok: false, error: err.message });
|
|
2468
|
+
resolve4({ ok: true, stdout });
|
|
2469
|
+
}
|
|
2470
|
+
),
|
|
2471
|
+
(err) => resolve4({ ok: false, error: `${err.message} (cwd=${cwd})` })
|
|
2407
2472
|
);
|
|
2408
|
-
}
|
|
2409
|
-
const { snapshot } = outcome;
|
|
2410
|
-
sendJson(res, 200, {
|
|
2411
|
-
configPath: snapshot.configPath,
|
|
2412
|
-
text: snapshot.text,
|
|
2413
|
-
exists: snapshot.exists,
|
|
2414
|
-
revision: snapshot.revision,
|
|
2415
|
-
issues: toWireIssues(snapshot.issues)
|
|
2416
2473
|
});
|
|
2417
2474
|
}
|
|
2418
|
-
async function
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2475
|
+
async function repoRoot(cwd) {
|
|
2476
|
+
const result = await runGit(cwd, ["rev-parse", "--show-toplevel"]);
|
|
2477
|
+
if (!result.ok) return null;
|
|
2478
|
+
const root = result.stdout.toString("utf-8").trim();
|
|
2479
|
+
return root.length > 0 ? root : null;
|
|
2480
|
+
}
|
|
2481
|
+
async function hasHead(root) {
|
|
2482
|
+
return (await runGit(root, ["rev-parse", "--verify", "--quiet", "HEAD"])).ok;
|
|
2483
|
+
}
|
|
2484
|
+
var NULL_SHA = /^0+$/;
|
|
2485
|
+
async function rawChanges(root) {
|
|
2486
|
+
const result = await runGit(root, ["diff", "--raw", "-z", "HEAD", "--"]);
|
|
2487
|
+
if (!result.ok) return [];
|
|
2488
|
+
return parseRawZ(result.stdout.toString("utf-8"));
|
|
2489
|
+
}
|
|
2490
|
+
function parseRawZ(text) {
|
|
2491
|
+
const tokens = text.split("\0");
|
|
2492
|
+
const changes = [];
|
|
2493
|
+
let i = 0;
|
|
2494
|
+
while (i < tokens.length) {
|
|
2495
|
+
const meta = tokens[i];
|
|
2496
|
+
if (meta === void 0 || !meta.startsWith(":")) {
|
|
2497
|
+
i += 1;
|
|
2498
|
+
continue;
|
|
2499
|
+
}
|
|
2500
|
+
const parts = meta.slice(1).split(" ");
|
|
2501
|
+
const oldSha = parts[2] ?? "";
|
|
2502
|
+
const status = (parts[4] ?? "").trim();
|
|
2503
|
+
const renamed = status.startsWith("R") || status.startsWith("C");
|
|
2504
|
+
const first = tokens[i + 1];
|
|
2505
|
+
const second = renamed ? tokens[i + 2] : void 0;
|
|
2506
|
+
i += renamed ? 3 : 2;
|
|
2507
|
+
if (first === void 0 || renamed && second === void 0) break;
|
|
2508
|
+
const path = renamed ? second : first;
|
|
2509
|
+
changes.push({
|
|
2510
|
+
status: status.charAt(0),
|
|
2511
|
+
path,
|
|
2512
|
+
...renamed ? { oldPath: first } : {},
|
|
2513
|
+
oldSha: NULL_SHA.test(oldSha) ? null : oldSha
|
|
2514
|
+
});
|
|
2432
2515
|
}
|
|
2433
|
-
|
|
2434
|
-
if (!outcome.ok) return sendError(res, ...saveFailure(outcome.reason, outcome.detail, lang));
|
|
2435
|
-
sendJson(res, 200, {
|
|
2436
|
-
configPath: outcome.configPath,
|
|
2437
|
-
revision: outcome.revision,
|
|
2438
|
-
issues: toWireIssues(outcome.issues)
|
|
2439
|
-
});
|
|
2516
|
+
return changes;
|
|
2440
2517
|
}
|
|
2441
|
-
async function
|
|
2442
|
-
|
|
2443
|
-
const
|
|
2444
|
-
if (!
|
|
2445
|
-
const
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
);
|
|
2518
|
+
async function treeBlobSizes(root) {
|
|
2519
|
+
const sizes = /* @__PURE__ */ new Map();
|
|
2520
|
+
const result = await runGit(root, ["ls-tree", "-r", "-l", "-z", "HEAD"]);
|
|
2521
|
+
if (!result.ok) return sizes;
|
|
2522
|
+
for (const record of splitZ(result.stdout.toString("utf-8"))) {
|
|
2523
|
+
const tab = record.indexOf(" ");
|
|
2524
|
+
if (tab < 0) continue;
|
|
2525
|
+
const columns = record.slice(0, tab).split(/\s+/).filter(Boolean);
|
|
2526
|
+
const sha = columns[2];
|
|
2527
|
+
const size = Number(columns[3]);
|
|
2528
|
+
if (columns[1] === "blob" && sha && Number.isFinite(size)) sizes.set(sha, size);
|
|
2453
2529
|
}
|
|
2454
|
-
|
|
2455
|
-
if (!outcome.ok) return sendError(res, ...applyFailure(outcome.reason, outcome.detail, lang));
|
|
2456
|
-
sendJson(res, 200, {
|
|
2457
|
-
configPath: outcome.configPath,
|
|
2458
|
-
revision: outcome.revision,
|
|
2459
|
-
issues: toWireIssues(outcome.issues),
|
|
2460
|
-
changes: outcome.changes.map((change) => ({
|
|
2461
|
-
name: change.name,
|
|
2462
|
-
action: change.action,
|
|
2463
|
-
...change.status === void 0 ? {} : { server: toWireMcp(change.status) },
|
|
2464
|
-
...change.reason === void 0 ? {} : { reason: change.reason }
|
|
2465
|
-
}))
|
|
2466
|
-
});
|
|
2530
|
+
return sizes;
|
|
2467
2531
|
}
|
|
2468
|
-
function
|
|
2469
|
-
|
|
2532
|
+
async function untrackedFiles(root) {
|
|
2533
|
+
const result = await runGit(root, ["ls-files", "-z", "--others", "--exclude-standard"]);
|
|
2534
|
+
if (!result.ok) return [];
|
|
2535
|
+
return splitZ(result.stdout.toString("utf-8"));
|
|
2470
2536
|
}
|
|
2471
|
-
function
|
|
2472
|
-
|
|
2473
|
-
|
|
2537
|
+
async function trackedFiles(root) {
|
|
2538
|
+
const result = await runGit(root, ["ls-files", "-z"]);
|
|
2539
|
+
if (!result.ok) return [];
|
|
2540
|
+
return splitZ(result.stdout.toString("utf-8"));
|
|
2541
|
+
}
|
|
2542
|
+
async function catFileBlob(root, sha) {
|
|
2543
|
+
const result = await runGit(root, ["cat-file", "blob", sha]);
|
|
2544
|
+
return result.ok ? result.stdout : null;
|
|
2545
|
+
}
|
|
2546
|
+
function splitZ(text) {
|
|
2547
|
+
return text.split("\0").filter((s) => s.length > 0);
|
|
2548
|
+
}
|
|
2549
|
+
// src/workspace/diff.ts
|
|
2550
|
+
var MAX_DIFF_BYTES = 512 * 1024;
|
|
2551
|
+
var MAX_DIFF_FILES = 200;
|
|
2552
|
+
var BINARY_SNIFF_BYTES = 8e3;
|
|
2553
|
+
var ABSENT = { kind: "absent" };
|
|
2554
|
+
async function collectWorkspaceDiff(opts) {
|
|
2555
|
+
const root = await repoRoot(opts.workDir);
|
|
2556
|
+
return root === null ? await checkpointDiff(opts) : await gitDiff(root);
|
|
2557
|
+
}
|
|
2558
|
+
async function gitDiff(root) {
|
|
2559
|
+
const wanted = await collectGitPaths(root);
|
|
2560
|
+
const notes = [];
|
|
2561
|
+
const truncated = wanted.length > MAX_DIFF_FILES;
|
|
2562
|
+
const picked = truncated ? wanted.slice(0, MAX_DIFF_FILES) : wanted;
|
|
2563
|
+
if (truncated) notes.push("truncated");
|
|
2564
|
+
if (wanted.length === 0) notes.push("no-changes");
|
|
2565
|
+
const sizes = picked.some((p) => p.oldSha !== null) ? await treeBlobSizes(root) : /* @__PURE__ */ new Map();
|
|
2566
|
+
const files = [];
|
|
2567
|
+
for (const item of picked) {
|
|
2568
|
+
const old = await gitSide(root, item.oldSha, sizes);
|
|
2569
|
+
const fresh = await worktreeSide(join(root, ...item.path.split("/")));
|
|
2570
|
+
files.push(describe3(item.path, old, fresh));
|
|
2474
2571
|
}
|
|
2475
|
-
|
|
2476
|
-
return [409, "mcp-config-unwritable", t("web.mcp_config_unwritable", { detail }, lang)];
|
|
2572
|
+
return { source: "git", root, files, truncated, notes };
|
|
2477
2573
|
}
|
|
2478
|
-
function
|
|
2479
|
-
|
|
2480
|
-
|
|
2574
|
+
async function collectGitPaths(root) {
|
|
2575
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
2576
|
+
if (await hasHead(root)) {
|
|
2577
|
+
for (const change of await rawChanges(root)) {
|
|
2578
|
+
if (change.oldPath !== void 0) {
|
|
2579
|
+
byPath.set(change.oldPath, { path: change.oldPath, oldSha: change.oldSha });
|
|
2580
|
+
}
|
|
2581
|
+
byPath.set(change.path, {
|
|
2582
|
+
path: change.path,
|
|
2583
|
+
oldSha: change.oldPath === void 0 ? change.oldSha : null
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
} else {
|
|
2587
|
+
for (const path of await trackedFiles(root)) byPath.set(path, { path, oldSha: null });
|
|
2481
2588
|
}
|
|
2482
|
-
|
|
2483
|
-
|
|
2589
|
+
for (const path of await untrackedFiles(root)) {
|
|
2590
|
+
if (!byPath.has(path)) byPath.set(path, { path, oldSha: null });
|
|
2484
2591
|
}
|
|
2485
|
-
|
|
2486
|
-
return [409, "mcp-config-no-servers", t("web.mcp_config_no_servers", { detail }, lang)];
|
|
2592
|
+
return [...byPath.values()].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
2487
2593
|
}
|
|
2488
|
-
function
|
|
2489
|
-
|
|
2594
|
+
async function gitSide(root, sha, sizes) {
|
|
2595
|
+
if (sha === null) return ABSENT;
|
|
2596
|
+
const known = sizes.get(sha);
|
|
2597
|
+
if (known !== void 0 && known > MAX_DIFF_BYTES) return { kind: "oversize", bytes: known };
|
|
2598
|
+
const data = await catFileBlob(root, sha);
|
|
2599
|
+
if (data === null) return { kind: "oversize", bytes: known ?? -1 };
|
|
2600
|
+
return { kind: "content", data };
|
|
2490
2601
|
}
|
|
2491
|
-
async function
|
|
2492
|
-
const
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
}
|
|
2497
|
-
const status = {
|
|
2498
|
-
...outcome.status,
|
|
2499
|
-
...outcome.detail === void 0 ? {} : { lastError: outcome.detail }
|
|
2500
|
-
};
|
|
2501
|
-
return sendJson(res, 200, {
|
|
2502
|
-
ok: false,
|
|
2503
|
-
server: toWireMcp(status)
|
|
2504
|
-
});
|
|
2602
|
+
async function checkpointDiff(opts) {
|
|
2603
|
+
const notes = ["not-a-git-repo"];
|
|
2604
|
+
const paths = await checkpointPaths(opts.checkpoints);
|
|
2605
|
+
if (paths.length === 0) {
|
|
2606
|
+
notes.push(opts.checkpoints === null ? "no-workspace-history" : "no-changes");
|
|
2607
|
+
return { source: "none", root: opts.workDir, files: [], truncated: false, notes };
|
|
2505
2608
|
}
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
t("web.mcp_add_lan_exposed", void 0, lang)
|
|
2609
|
+
const truncated = paths.length > MAX_DIFF_FILES;
|
|
2610
|
+
const picked = truncated ? paths.slice(0, MAX_DIFF_FILES) : paths;
|
|
2611
|
+
if (truncated) notes.push("truncated");
|
|
2612
|
+
const files = [];
|
|
2613
|
+
for (const absolute of picked) {
|
|
2614
|
+
const fresh = await worktreeSide(absolute);
|
|
2615
|
+
files.push(
|
|
2616
|
+
describe3(displayPath(opts.workDir, absolute), ABSENT, fresh, {
|
|
2617
|
+
status: fresh.kind === "absent" ? "deleted" : "modified",
|
|
2618
|
+
omitted: "no-snapshot"
|
|
2619
|
+
})
|
|
2518
2620
|
);
|
|
2519
2621
|
}
|
|
2520
|
-
|
|
2521
|
-
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
2522
|
-
const payload = asRecord(body.value);
|
|
2523
|
-
const name = typeof payload["name"] === "string" ? payload["name"].trim() : "";
|
|
2524
|
-
const transport = payload["transport"];
|
|
2525
|
-
if (transport !== "stdio" && transport !== "sse" && transport !== "http") {
|
|
2526
|
-
return sendError(res, 400, "bad-mcp-config", t("web.mcp_add_bad_transport", void 0, lang));
|
|
2527
|
-
}
|
|
2528
|
-
const outcome = await ctx.runtime.mcp.add({
|
|
2529
|
-
name,
|
|
2530
|
-
transport,
|
|
2531
|
-
...typeof payload["command"] === "string" ? { command: payload["command"].trim() } : {},
|
|
2532
|
-
...Array.isArray(payload["args"]) ? { args: stringList(payload["args"]) } : {},
|
|
2533
|
-
...typeof payload["url"] === "string" ? { url: payload["url"].trim() } : {},
|
|
2534
|
-
...payload["env"] === void 0 ? {} : { env: stringMap(payload["env"]) }
|
|
2535
|
-
});
|
|
2536
|
-
if (!outcome.ok) {
|
|
2537
|
-
return sendError(res, ...addFailure(outcome.reason, outcome.detail, name, lang));
|
|
2538
|
-
}
|
|
2539
|
-
sendJson(res, 200, {
|
|
2540
|
-
server: toWireMcp(outcome.status),
|
|
2541
|
-
configPath: outcome.configPath
|
|
2542
|
-
});
|
|
2622
|
+
return { source: "checkpoints", root: opts.workDir, files, truncated, notes };
|
|
2543
2623
|
}
|
|
2544
|
-
function
|
|
2545
|
-
if (
|
|
2546
|
-
|
|
2624
|
+
async function checkpointPaths(checkpoints) {
|
|
2625
|
+
if (!checkpoints) return [];
|
|
2626
|
+
const paths = /* @__PURE__ */ new Set();
|
|
2627
|
+
try {
|
|
2628
|
+
for (const summary of await checkpoints.list()) {
|
|
2629
|
+
const preview = await checkpoints.preview(summary.turnIndex);
|
|
2630
|
+
if (!preview) continue;
|
|
2631
|
+
for (const plan of [...preview.files, ...preview.conflicts]) {
|
|
2632
|
+
if (plan.action !== "skip") paths.add(plan.path);
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
} catch {
|
|
2547
2636
|
}
|
|
2548
|
-
|
|
2549
|
-
if (reason === "duplicate")
|
|
2550
|
-
return [409, "mcp-name-taken", t("web.mcp_add_taken", { name }, lang)];
|
|
2551
|
-
return [409, "mcp-config-unwritable", t("web.mcp_add_unwritable", { detail }, lang)];
|
|
2637
|
+
return [...paths].sort();
|
|
2552
2638
|
}
|
|
2553
|
-
function
|
|
2554
|
-
|
|
2639
|
+
function displayPath(workDir, absolute) {
|
|
2640
|
+
const rel = relative(workDir, absolute);
|
|
2641
|
+
if (rel.length === 0 || rel.startsWith("..") || isAbsolute(rel)) return absolute;
|
|
2642
|
+
return rel.split(sep).join("/");
|
|
2555
2643
|
}
|
|
2556
|
-
function
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
if (
|
|
2644
|
+
async function worktreeSide(absolute) {
|
|
2645
|
+
try {
|
|
2646
|
+
const info = await stat(absolute);
|
|
2647
|
+
if (!info.isFile()) return ABSENT;
|
|
2648
|
+
if (info.size > MAX_DIFF_BYTES) return { kind: "oversize", bytes: info.size };
|
|
2649
|
+
return { kind: "content", data: await readFile(absolute) };
|
|
2650
|
+
} catch {
|
|
2651
|
+
return ABSENT;
|
|
2560
2652
|
}
|
|
2561
|
-
return out;
|
|
2562
2653
|
}
|
|
2563
|
-
function
|
|
2564
|
-
|
|
2654
|
+
function describe3(path, old, fresh, override = {}) {
|
|
2655
|
+
const status = override.status ?? (old.kind === "absent" ? "added" : fresh.kind === "absent" ? "deleted" : "modified");
|
|
2656
|
+
const oldBytes = bytesOf(old);
|
|
2657
|
+
const newBytes = bytesOf(fresh);
|
|
2658
|
+
const hard = omitReason(old, fresh);
|
|
2659
|
+
if (hard !== void 0) {
|
|
2660
|
+
return { path, status, oldContent: null, newContent: null, oldBytes, newBytes, omitted: hard };
|
|
2661
|
+
}
|
|
2662
|
+
return {
|
|
2663
|
+
path,
|
|
2664
|
+
status,
|
|
2665
|
+
oldContent: textOf(old),
|
|
2666
|
+
newContent: textOf(fresh),
|
|
2667
|
+
oldBytes,
|
|
2668
|
+
newBytes,
|
|
2669
|
+
...override.omitted === void 0 ? {} : { omitted: override.omitted }
|
|
2670
|
+
};
|
|
2565
2671
|
}
|
|
2566
|
-
function
|
|
2567
|
-
|
|
2672
|
+
function bytesOf(side) {
|
|
2673
|
+
if (side.kind === "absent") return null;
|
|
2674
|
+
if (side.kind === "oversize") return side.bytes < 0 ? null : side.bytes;
|
|
2675
|
+
return side.data.byteLength;
|
|
2568
2676
|
}
|
|
2569
|
-
function
|
|
2570
|
-
|
|
2571
|
-
return history.some((m) => m.parts?.some((p) => p.type === "image"));
|
|
2677
|
+
function textOf(side) {
|
|
2678
|
+
return side.kind === "content" ? side.data.toString("utf-8") : null;
|
|
2572
2679
|
}
|
|
2573
|
-
function
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
sendJson(res, 200, {
|
|
2578
|
-
selection: selection ? toWireSelection(selection) : null,
|
|
2579
|
-
configured: (live ? live.model.configured() : runtime.config.model) || null
|
|
2580
|
-
});
|
|
2680
|
+
function omitReason(old, fresh) {
|
|
2681
|
+
if (isBinary(old) || isBinary(fresh)) return "binary";
|
|
2682
|
+
if (old.kind === "oversize" || fresh.kind === "oversize") return "too-large";
|
|
2683
|
+
return void 0;
|
|
2581
2684
|
}
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2685
|
+
function isBinary(side) {
|
|
2686
|
+
return side.kind === "content" && side.data.subarray(0, BINARY_SNIFF_BYTES).includes(0);
|
|
2687
|
+
}
|
|
2688
|
+
// src/workspace/handlers.ts
|
|
2689
|
+
async function createSession(ctx, req, res, lang) {
|
|
2586
2690
|
const body = await readJsonBody(req);
|
|
2587
2691
|
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
2588
|
-
const
|
|
2589
|
-
if (!
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
}
|
|
2595
|
-
const selectionCtx = {
|
|
2596
|
-
hasImages: sessionHasImages(live.session),
|
|
2597
|
-
...input.usedTokens === void 0 ? {} : { usedTokens: input.usedTokens }
|
|
2598
|
-
};
|
|
2599
|
-
const result = ref === null ? control.reset(selectionCtx) : control.set(ref, selectionCtx);
|
|
2600
|
-
const payload = result.ok ? {
|
|
2601
|
-
ok: true,
|
|
2602
|
-
selection: toWireSelection(result.selection),
|
|
2603
|
-
contextLength: result.contextLength,
|
|
2604
|
-
caveats: result.caveats
|
|
2605
|
-
} : { ok: false, rejection: result.rejection };
|
|
2606
|
-
sendJson(res, 200, payload);
|
|
2692
|
+
const outcome = await ensureSession(ctx, asRecord(body.value), lang);
|
|
2693
|
+
if (!outcome.ok) return sendError(res, outcome.status, outcome.code, outcome.message);
|
|
2694
|
+
sendJson(res, 200, {
|
|
2695
|
+
sessionId: outcome.sessionId,
|
|
2696
|
+
created: outcome.created,
|
|
2697
|
+
workspace: outcome.workspace
|
|
2698
|
+
});
|
|
2607
2699
|
}
|
|
2608
|
-
function
|
|
2609
|
-
const
|
|
2610
|
-
if (
|
|
2611
|
-
const
|
|
2700
|
+
async function ensureSession(ctx, payload, lang) {
|
|
2701
|
+
const field = readWorkspaceField(payload, lang);
|
|
2702
|
+
if (!field.ok) return { ok: false, status: 400, code: "bad-workspace", message: field.message };
|
|
2703
|
+
const defer = payload["deferWorkspace"] === true;
|
|
2704
|
+
if (defer && field.dir !== null) {
|
|
2705
|
+
return {
|
|
2706
|
+
ok: false,
|
|
2707
|
+
status: 400,
|
|
2708
|
+
code: "bad-request",
|
|
2709
|
+
message: t("web.workspace_defer_with_root", void 0, lang)
|
|
2710
|
+
};
|
|
2711
|
+
}
|
|
2712
|
+
if (defer && payload["create"] !== true) {
|
|
2713
|
+
return {
|
|
2714
|
+
ok: false,
|
|
2715
|
+
status: 400,
|
|
2716
|
+
code: "bad-request",
|
|
2717
|
+
message: t("web.workspace_defer_needs_create", void 0, lang)
|
|
2718
|
+
};
|
|
2719
|
+
}
|
|
2720
|
+
if (field.dir === null && payload["create"] !== true) {
|
|
2721
|
+
const sessionId = ctx.runtime.sessionId;
|
|
2722
|
+
const bound = ctx.runtime.workspaces.of(sessionId);
|
|
2723
|
+
return {
|
|
2724
|
+
ok: true,
|
|
2725
|
+
sessionId,
|
|
2726
|
+
created: false,
|
|
2727
|
+
workspace: bound ? toWireSessionWorkspace(bound) : null
|
|
2728
|
+
};
|
|
2729
|
+
}
|
|
2730
|
+
const role = typeof payload["role"] === "string" ? payload["role"].trim() : "";
|
|
2731
|
+
const made = ctx.runtime.sessionFactory.create({
|
|
2732
|
+
workspace: field.dir,
|
|
2733
|
+
...lang === void 0 ? {} : { lang },
|
|
2734
|
+
reuse: payload["create"] !== true,
|
|
2735
|
+
...role === "" ? {} : { role }
|
|
2736
|
+
});
|
|
2737
|
+
if (!made.ok) {
|
|
2738
|
+
if (made.reason === "unavailable") return { ok: false, ...unavailableToHttp(lang) };
|
|
2739
|
+
if (made.reason === "unknown-role") {
|
|
2740
|
+
return { ok: false, ...unknownRoleToHttp(made.detail, lang) };
|
|
2741
|
+
}
|
|
2742
|
+
return { ok: false, ...bindFailureToHttp(made, lang) };
|
|
2743
|
+
}
|
|
2744
|
+
ctx.hub.register(made.live.session);
|
|
2745
|
+
if (defer) ctx.runtime.workspaces.defer(made.live.sessionId);
|
|
2746
|
+
if (made.created) ctx.hub.notifySessionsChanged(made.live.sessionId);
|
|
2612
2747
|
return {
|
|
2613
2748
|
ok: true,
|
|
2614
|
-
|
|
2615
|
-
|
|
2749
|
+
sessionId: made.live.sessionId,
|
|
2750
|
+
created: made.created,
|
|
2751
|
+
workspace: made.live.workspace ? toWireSessionWorkspace(made.live.workspace) : null
|
|
2616
2752
|
};
|
|
2617
2753
|
}
|
|
2618
|
-
function
|
|
2619
|
-
if (!ctx.runtime.
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2754
|
+
function sessionWorkspace(ctx, res, sessionId, lang) {
|
|
2755
|
+
if (!ctx.hub.has(sessionId) && !ctx.runtime.sessions?.get(sessionId)) {
|
|
2756
|
+
return sendUnknownSession(res, sessionId);
|
|
2757
|
+
}
|
|
2758
|
+
const state = ctx.runtime.workspaces.stateOf(sessionId);
|
|
2759
|
+
if (state !== "none") {
|
|
2760
|
+
return sendJson(res, 200, {
|
|
2761
|
+
binding: toWireBinding(state, ctx.runtime.workspaces.of(sessionId))
|
|
2762
|
+
});
|
|
2763
|
+
}
|
|
2764
|
+
if (ctx.hub.has(sessionId) || ctx.hub.isCooled(sessionId)) {
|
|
2765
|
+
return sendJson(res, 200, {
|
|
2766
|
+
binding: { state: "none" }
|
|
2767
|
+
});
|
|
2768
|
+
}
|
|
2769
|
+
sendError(res, 409, "not-live-session", t("web.workspace_not_live", void 0, lang));
|
|
2623
2770
|
}
|
|
2624
|
-
async function
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
const plan = live.plan;
|
|
2628
|
-
if (!plan) {
|
|
2629
|
-
return sendError(res, 503, "no-plan-mode", t("web.plan_mode_unavailable", void 0, lang));
|
|
2771
|
+
async function bindWorkspace(ctx, req, res, sessionId, lang) {
|
|
2772
|
+
if (!ctx.hub.has(sessionId) && !ctx.runtime.sessions?.get(sessionId)) {
|
|
2773
|
+
return sendUnknownSession(res, sessionId);
|
|
2630
2774
|
}
|
|
2631
2775
|
const body = await readJsonBody(req);
|
|
2632
2776
|
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
2633
|
-
const
|
|
2634
|
-
if (
|
|
2777
|
+
const parsed = readBindWorkspaceBody(asRecord(body.value), lang);
|
|
2778
|
+
if (!parsed.ok) return sendError(res, 400, "bad-workspace", parsed.message);
|
|
2779
|
+
const state = ctx.runtime.workspaces.stateOf(sessionId);
|
|
2780
|
+
if (state !== "unbound") {
|
|
2635
2781
|
return sendError(
|
|
2636
2782
|
res,
|
|
2637
|
-
|
|
2638
|
-
"
|
|
2639
|
-
t("web.
|
|
2783
|
+
409,
|
|
2784
|
+
"workspace-locked",
|
|
2785
|
+
t("web.workspace_already_decided", void 0, lang)
|
|
2640
2786
|
);
|
|
2641
2787
|
}
|
|
2642
|
-
const
|
|
2788
|
+
const turn = ctx.hub.stateOf(sessionId);
|
|
2789
|
+
if (turn !== "idle") {
|
|
2790
|
+
return sendError(res, 409, "busy", t("web.workspace_turn_running", void 0, lang), {
|
|
2791
|
+
"X-Epoch-Turn-State": turn
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
if (parsed.kind === "none") {
|
|
2795
|
+
ctx.runtime.workspaces.decideNone(sessionId);
|
|
2796
|
+
return sendJson(res, 200, {
|
|
2797
|
+
binding: { state: "none" }
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
2800
|
+
const outcome = ctx.runtime.workspaces.bind(sessionId, parsed.root, lang);
|
|
2801
|
+
if (!outcome.ok) {
|
|
2802
|
+
const mapped = bindFailureToHttp(outcome, lang);
|
|
2803
|
+
return sendError(res, mapped.status, mapped.code, mapped.message);
|
|
2804
|
+
}
|
|
2643
2805
|
sendJson(res, 200, {
|
|
2644
|
-
|
|
2645
|
-
changed
|
|
2806
|
+
binding: { state: "bound", workspace: toWireSessionWorkspace(outcome.bound) }
|
|
2646
2807
|
});
|
|
2647
2808
|
}
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
}
|
|
2652
|
-
|
|
2653
|
-
|
|
2809
|
+
async function sessionDiff(ctx, res, sessionId, _lang) {
|
|
2810
|
+
if (!ctx.hub.has(sessionId) && !ctx.runtime.sessions?.get(sessionId)) {
|
|
2811
|
+
return sendUnknownSession(res, sessionId);
|
|
2812
|
+
}
|
|
2813
|
+
const bound = ctx.runtime.workspaces.of(sessionId);
|
|
2814
|
+
if (!bound) {
|
|
2815
|
+
return sendJson(res, 200, {
|
|
2816
|
+
source: "none",
|
|
2817
|
+
root: "",
|
|
2818
|
+
files: [],
|
|
2819
|
+
truncated: false,
|
|
2820
|
+
notes: ["no-workspace"]
|
|
2821
|
+
});
|
|
2822
|
+
}
|
|
2823
|
+
sendJson(
|
|
2824
|
+
res,
|
|
2825
|
+
200,
|
|
2826
|
+
await collectWorkspaceDiff({
|
|
2827
|
+
workDir: bound.workspace.root,
|
|
2828
|
+
checkpoints: checkpointsOf(ctx, sessionId)
|
|
2829
|
+
})
|
|
2830
|
+
);
|
|
2654
2831
|
}
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2832
|
+
// src/host-compose.ts
|
|
2833
|
+
async function postHostCompose(ctx, req, res, lang) {
|
|
2834
|
+
const body = await readJsonBody(req, void 0, lang);
|
|
2835
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
2836
|
+
const payload = asRecord(body.value);
|
|
2837
|
+
const sessionId = payload["sessionId"];
|
|
2838
|
+
if (sessionId !== void 0 && typeof sessionId !== "string") {
|
|
2839
|
+
return sendError(res, 400, "bad-request", t("web.host_compose_bad_request", void 0, lang));
|
|
2840
|
+
}
|
|
2841
|
+
if (sessionId !== void 0 && ["workspace", "create", "role", "deferWorkspace"].some((key) => payload[key] !== void 0)) {
|
|
2842
|
+
return sendError(res, 400, "bad-request", t("web.host_compose_exclusive", void 0, lang));
|
|
2843
|
+
}
|
|
2844
|
+
const rawText = payload["text"];
|
|
2845
|
+
if (rawText !== void 0 && typeof rawText !== "string") {
|
|
2846
|
+
return sendError(res, 400, "bad-request", t("web.host_compose_bad_request", void 0, lang));
|
|
2847
|
+
}
|
|
2848
|
+
const text = rawText?.trim() ?? "";
|
|
2849
|
+
if (rawText !== void 0 && text === "") {
|
|
2850
|
+
return sendError(res, 400, "bad-request", t("web.host_compose_blank_text", void 0, lang));
|
|
2851
|
+
}
|
|
2852
|
+
const autoSend = payload["autoSend"] === true;
|
|
2853
|
+
if (autoSend && text === "") {
|
|
2854
|
+
return sendError(
|
|
2855
|
+
res,
|
|
2856
|
+
400,
|
|
2857
|
+
"bad-request",
|
|
2858
|
+
t("web.host_compose_autosend_needs_text", void 0, lang)
|
|
2859
|
+
);
|
|
2860
|
+
}
|
|
2861
|
+
let target;
|
|
2862
|
+
let created = false;
|
|
2863
|
+
if (typeof sessionId === "string") {
|
|
2864
|
+
if (!ctx.hub.has(sessionId) && !ctx.runtime.sessions?.get(sessionId)) {
|
|
2865
|
+
return sendUnknownSession(res, sessionId, lang);
|
|
2866
|
+
}
|
|
2867
|
+
target = sessionId;
|
|
2868
|
+
} else {
|
|
2869
|
+
const outcome = await ensureSession(ctx, payload, lang);
|
|
2870
|
+
if (!outcome.ok) return sendError(res, outcome.status, outcome.code, outcome.message);
|
|
2871
|
+
target = outcome.sessionId;
|
|
2872
|
+
created = outcome.created;
|
|
2873
|
+
}
|
|
2874
|
+
const item = ctx.hostCompose.record({ sessionId: target, text, autoSend });
|
|
2875
|
+
ctx.hub.notifyHostCompose(target, { id: item.id, text, autoSend });
|
|
2876
|
+
sendJson(res, 200, {
|
|
2877
|
+
sessionId: target,
|
|
2878
|
+
created,
|
|
2879
|
+
composeId: item.id,
|
|
2880
|
+
expiresAt: item.createdAt + HOST_COMPOSE_TTL_MS
|
|
2881
|
+
});
|
|
2663
2882
|
}
|
|
2664
|
-
function
|
|
2665
|
-
|
|
2666
|
-
sendJson(res, 200,
|
|
2883
|
+
function getHostCompose(ctx, res) {
|
|
2884
|
+
const item = ctx.hostCompose.pending();
|
|
2885
|
+
sendJson(res, 200, {
|
|
2886
|
+
compose: item === null ? null : toWireHostCompose(item)
|
|
2887
|
+
});
|
|
2667
2888
|
}
|
|
2668
|
-
|
|
2669
|
-
const
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2889
|
+
function takeHostCompose(ctx, res, id) {
|
|
2890
|
+
const result = ctx.hostCompose.take(id);
|
|
2891
|
+
sendJson(
|
|
2892
|
+
res,
|
|
2893
|
+
200,
|
|
2894
|
+
result === "ok" ? { ok: true } : { ok: false, reason: result }
|
|
2895
|
+
);
|
|
2896
|
+
}
|
|
2897
|
+
async function putLastSession(ctx, req, res, lang) {
|
|
2898
|
+
const control = ctx.runtime.lastOpenSession;
|
|
2899
|
+
if (!control) {
|
|
2673
2900
|
return sendError(
|
|
2674
2901
|
res,
|
|
2675
2902
|
503,
|
|
2676
|
-
"no-
|
|
2677
|
-
t("web.
|
|
2903
|
+
"no-last-session-store",
|
|
2904
|
+
t("web.last_session_unavailable", {}, lang)
|
|
2905
|
+
);
|
|
2906
|
+
}
|
|
2907
|
+
const body = await readJsonBody(req, void 0, lang);
|
|
2908
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
2909
|
+
if (!conformsToWire(body.value, WIRE_LAST_SESSION_FIELDS)) {
|
|
2910
|
+
return sendError(res, 400, "bad-request", t("web.last_session_bad_request", {}, lang));
|
|
2911
|
+
}
|
|
2912
|
+
const { sessionId } = body.value;
|
|
2913
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
2914
|
+
return sendError(res, 400, "bad-request", t("web.last_session_bad_request", {}, lang));
|
|
2915
|
+
}
|
|
2916
|
+
if (!ctx.hub.has(sessionId) && !ctx.runtime.sessions?.get(sessionId)) {
|
|
2917
|
+
return sendUnknownSession(res, sessionId, lang);
|
|
2918
|
+
}
|
|
2919
|
+
const failure = control.set(sessionId);
|
|
2920
|
+
if (failure) return sendError(res, 500, "write-failed", failure(lang));
|
|
2921
|
+
sendJson(res, 200, { ok: true });
|
|
2922
|
+
}
|
|
2923
|
+
function readConfig(ctx, res, lang) {
|
|
2924
|
+
if (ctx.lanExposed) return sendLanExposed(res, lang);
|
|
2925
|
+
const outcome = ctx.runtime.mcp.readConfig();
|
|
2926
|
+
if (!outcome.ok) {
|
|
2927
|
+
return sendError(
|
|
2928
|
+
res,
|
|
2929
|
+
409,
|
|
2930
|
+
"mcp-config-unreadable",
|
|
2931
|
+
t("web.mcp_config_unreadable", { detail: outcome.detail }, lang)
|
|
2678
2932
|
);
|
|
2679
2933
|
}
|
|
2934
|
+
const { snapshot } = outcome;
|
|
2935
|
+
sendJson(res, 200, {
|
|
2936
|
+
configPath: snapshot.configPath,
|
|
2937
|
+
text: snapshot.text,
|
|
2938
|
+
exists: snapshot.exists,
|
|
2939
|
+
revision: snapshot.revision,
|
|
2940
|
+
issues: toWireIssues(snapshot.issues)
|
|
2941
|
+
});
|
|
2942
|
+
}
|
|
2943
|
+
async function saveConfig(ctx, req, res, lang) {
|
|
2944
|
+
if (ctx.lanExposed) return sendLanExposed(res, lang);
|
|
2680
2945
|
const body = await readJsonBody(req);
|
|
2681
2946
|
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
2682
|
-
const
|
|
2683
|
-
|
|
2947
|
+
const payload = asRecord(body.value);
|
|
2948
|
+
const text = payload["text"];
|
|
2949
|
+
const revision = payload["revision"];
|
|
2950
|
+
if (typeof text !== "string" || typeof revision !== "string") {
|
|
2684
2951
|
return sendError(
|
|
2685
2952
|
res,
|
|
2686
2953
|
400,
|
|
2687
|
-
"bad-
|
|
2688
|
-
t("web.
|
|
2954
|
+
"bad-mcp-config-body",
|
|
2955
|
+
t("web.mcp_config_bad_body", void 0, lang)
|
|
2689
2956
|
);
|
|
2690
2957
|
}
|
|
2691
|
-
const
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
...result.notRemembered ? { notRemembered: result.notRemembered } : {}
|
|
2699
|
-
} : { ok: false, refused: REFUSAL[result.reason], changed: false, state };
|
|
2700
|
-
sendJson(res, 200, payload);
|
|
2958
|
+
const outcome = ctx.runtime.mcp.writeConfig(text, revision, lang);
|
|
2959
|
+
if (!outcome.ok) return sendError(res, ...saveFailure(outcome.reason, outcome.detail, lang));
|
|
2960
|
+
sendJson(res, 200, {
|
|
2961
|
+
configPath: outcome.configPath,
|
|
2962
|
+
revision: outcome.revision,
|
|
2963
|
+
issues: toWireIssues(outcome.issues)
|
|
2964
|
+
});
|
|
2701
2965
|
}
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2966
|
+
async function applyConfig(ctx, req, res, lang) {
|
|
2967
|
+
if (ctx.lanExposed) return sendLanExposed(res, lang);
|
|
2968
|
+
const body = await readJsonBody(req);
|
|
2969
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
2970
|
+
const revision = asRecord(body.value)["revision"];
|
|
2971
|
+
if (typeof revision !== "string") {
|
|
2972
|
+
return sendError(
|
|
2973
|
+
res,
|
|
2974
|
+
400,
|
|
2975
|
+
"bad-mcp-config-body",
|
|
2976
|
+
t("web.mcp_config_bad_body", void 0, lang)
|
|
2977
|
+
);
|
|
2706
2978
|
}
|
|
2707
|
-
const
|
|
2708
|
-
|
|
2979
|
+
const outcome = await ctx.runtime.mcp.applyConfig(revision, lang);
|
|
2980
|
+
if (!outcome.ok) return sendError(res, ...applyFailure(outcome.reason, outcome.detail, lang));
|
|
2709
2981
|
sendJson(res, 200, {
|
|
2710
|
-
|
|
2711
|
-
|
|
2982
|
+
configPath: outcome.configPath,
|
|
2983
|
+
revision: outcome.revision,
|
|
2984
|
+
issues: toWireIssues(outcome.issues),
|
|
2985
|
+
changes: outcome.changes.map((change) => ({
|
|
2986
|
+
name: change.name,
|
|
2987
|
+
action: change.action,
|
|
2988
|
+
...change.status === void 0 ? {} : { server: toWireMcp(change.status) },
|
|
2989
|
+
...change.reason === void 0 ? {} : { reason: change.reason }
|
|
2990
|
+
}))
|
|
2712
2991
|
});
|
|
2713
2992
|
}
|
|
2714
|
-
function
|
|
2715
|
-
|
|
2993
|
+
function sendLanExposed(res, lang) {
|
|
2994
|
+
sendError(res, 403, "mcp-config-lan-exposed", t("web.mcp_config_lan_exposed", void 0, lang));
|
|
2716
2995
|
}
|
|
2717
|
-
function
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
denyRules: counts.denyRules,
|
|
2724
|
-
mcpServers: counts.mcpServers
|
|
2725
|
-
};
|
|
2996
|
+
function saveFailure(reason, detail, lang) {
|
|
2997
|
+
if (reason === "stale") {
|
|
2998
|
+
return [409, "mcp-config-stale", t("web.mcp_config_stale", void 0, lang)];
|
|
2999
|
+
}
|
|
3000
|
+
if (reason === "invalid-json") return [400, "mcp-config-invalid-json", detail];
|
|
3001
|
+
return [409, "mcp-config-unwritable", t("web.mcp_config_unwritable", { detail }, lang)];
|
|
2726
3002
|
}
|
|
2727
|
-
function
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
const shown = hit.entry.displayName;
|
|
2731
|
-
if (shown !== void 0 && shown !== "") out.set(`${hit.marketplace}/${hit.entry.name}`, shown);
|
|
3003
|
+
function applyFailure(reason, detail, lang) {
|
|
3004
|
+
if (reason === "unreadable") {
|
|
3005
|
+
return [409, "mcp-config-unreadable", t("web.mcp_config_unreadable", { detail }, lang)];
|
|
2732
3006
|
}
|
|
2733
|
-
|
|
3007
|
+
if (reason === "stale") {
|
|
3008
|
+
return [409, "mcp-config-stale", t("web.mcp_config_stale", void 0, lang)];
|
|
3009
|
+
}
|
|
3010
|
+
if (reason === "invalid-json") return [400, "mcp-config-invalid-json", detail];
|
|
3011
|
+
return [409, "mcp-config-no-servers", t("web.mcp_config_no_servers", { detail }, lang)];
|
|
2734
3012
|
}
|
|
2735
|
-
function
|
|
2736
|
-
return {
|
|
2737
|
-
name: entry.name,
|
|
2738
|
-
displayName: displayNames.get(`${entry.marketplace}/${entry.name}`) ?? null,
|
|
2739
|
-
version: entry.version,
|
|
2740
|
-
source: entry.source,
|
|
2741
|
-
sourceType: entry.sourceType,
|
|
2742
|
-
path: entry.path,
|
|
2743
|
-
linked: entry.linked,
|
|
2744
|
-
enabled: entry.enabled,
|
|
2745
|
-
installedAt: entry.installedAt,
|
|
2746
|
-
marketplace: entry.marketplace ?? null,
|
|
2747
|
-
active: entry.active,
|
|
2748
|
-
counts: entry.counts ? toWireCounts(entry.counts) : null
|
|
2749
|
-
};
|
|
3013
|
+
function toWireIssues(issues) {
|
|
3014
|
+
return issues.map((issue) => ({ path: issue.path, message: issue.message }));
|
|
2750
3015
|
}
|
|
2751
|
-
function
|
|
2752
|
-
const
|
|
2753
|
-
return {
|
|
2754
|
-
marketplace: hit.marketplace,
|
|
2755
|
-
ref: hit.ref,
|
|
2756
|
-
name: hit.entry.name,
|
|
2757
|
-
description: hit.entry.description ?? "",
|
|
2758
|
-
category: hit.entry.category ?? null,
|
|
2759
|
-
keywords: [...hit.entry.keywords ?? []],
|
|
2760
|
-
sourceType: hit.sourceType ?? null,
|
|
2761
|
-
installable: hit.installable,
|
|
2762
|
-
installed: mine !== void 0,
|
|
2763
|
-
author: hit.entry.author?.name ?? null,
|
|
2764
|
-
downloads: hit.entry.downloads ?? null,
|
|
2765
|
-
likes: hit.entry.likes ?? null,
|
|
2766
|
-
displayName: hit.entry.displayName ?? null,
|
|
2767
|
-
icon: hit.entry.icon ?? null,
|
|
2768
|
-
version: hit.entry.version ?? null,
|
|
2769
|
-
changelog: hit.entry.changelog ?? null,
|
|
2770
|
-
size: hit.entry.size ?? null,
|
|
2771
|
-
homepage: hit.entry.homepage ?? null,
|
|
2772
|
-
visibility: hit.entry.visibility ?? null,
|
|
2773
|
-
status: hit.entry.status ?? null,
|
|
2774
|
-
updateAvailable: mine !== void 0 && !mine.linked && hit.entry.version !== void 0 && hit.entry.version !== mine.version
|
|
2775
|
-
};
|
|
2776
|
-
}
|
|
2777
|
-
function toWireMarketplace(market) {
|
|
2778
|
-
return {
|
|
2779
|
-
name: market.name,
|
|
2780
|
-
source: market.source,
|
|
2781
|
-
entries: market.entries,
|
|
2782
|
-
addedAt: market.addedAt,
|
|
2783
|
-
local: market.local,
|
|
2784
|
-
categories: (market.categories ?? []).map(toWireCategory)
|
|
2785
|
-
};
|
|
2786
|
-
}
|
|
2787
|
-
function toWireCategory(node) {
|
|
2788
|
-
return {
|
|
2789
|
-
id: node.id,
|
|
2790
|
-
label: node.label ?? node.id,
|
|
2791
|
-
children: (node.children ?? []).map(toWireCategory)
|
|
2792
|
-
};
|
|
2793
|
-
}
|
|
2794
|
-
function toWirePreview2(preview) {
|
|
2795
|
-
const { manifest, source, inventory } = preview;
|
|
2796
|
-
return {
|
|
2797
|
-
name: manifest.name,
|
|
2798
|
-
version: manifest.version,
|
|
2799
|
-
description: manifest.description ?? "",
|
|
2800
|
-
source: source.raw,
|
|
2801
|
-
sourceType: source.type,
|
|
2802
|
-
checksum: source.sha256 ?? null,
|
|
2803
|
-
author: manifest.author?.name ?? null,
|
|
2804
|
-
homepage: manifest.homepage ?? null,
|
|
2805
|
-
commands: [...inventory.commands],
|
|
2806
|
-
roles: [...inventory.roles],
|
|
2807
|
-
skills: [...inventory.skills],
|
|
2808
|
-
hooks: inventory.hooks.map((h) => ({ type: h.type, count: h.count })),
|
|
2809
|
-
denyRules: inventory.denyRules,
|
|
2810
|
-
mcpServers: [...inventory.mcpServers],
|
|
2811
|
-
ignoredBuckets: [...inventory.ignoredBuckets],
|
|
2812
|
-
hasJsTools: inventory.jsTools,
|
|
2813
|
-
conflict: preview.conflict ? { name: preview.conflict.name, version: preview.conflict.version } : null
|
|
2814
|
-
};
|
|
2815
|
-
}
|
|
2816
|
-
function controlOf(ctx, res, lang) {
|
|
2817
|
-
const control = ctx.runtime.plugins;
|
|
2818
|
-
if (!control) {
|
|
2819
|
-
sendError(res, 503, "no-plugin-control", t("web.plugin_not_available", void 0, lang));
|
|
2820
|
-
return null;
|
|
2821
|
-
}
|
|
2822
|
-
return control;
|
|
2823
|
-
}
|
|
2824
|
-
function refusedByLan(ctx, res, lang) {
|
|
2825
|
-
if (!ctx.lanExposed) return false;
|
|
2826
|
-
sendError(
|
|
2827
|
-
res,
|
|
2828
|
-
403,
|
|
2829
|
-
"plugin-write-lan-exposed",
|
|
2830
|
-
t("web.plugin_write_lan_exposed", void 0, lang)
|
|
2831
|
-
);
|
|
2832
|
-
return true;
|
|
2833
|
-
}
|
|
2834
|
-
function fieldOf(payload, key) {
|
|
2835
|
-
const value = payload[key];
|
|
2836
|
-
return typeof value === "string" ? value.trim() : "";
|
|
2837
|
-
}
|
|
2838
|
-
function uploadFormOf(payload) {
|
|
2839
|
-
const form = {};
|
|
2840
|
-
for (const name of WIRE_PLUGIN_UPLOAD_FORM_FIELDS) {
|
|
2841
|
-
const raw = fieldOf(payload, name);
|
|
2842
|
-
if (raw === "") continue;
|
|
2843
|
-
if (name === "visibility") {
|
|
2844
|
-
if (raw === "private" || raw === "public") form[name] = raw;
|
|
2845
|
-
continue;
|
|
2846
|
-
}
|
|
2847
|
-
form[name] = raw;
|
|
2848
|
-
}
|
|
2849
|
-
return form;
|
|
2850
|
-
}
|
|
2851
|
-
function listPlugins(ctx, res, lang) {
|
|
2852
|
-
const control = controlOf(ctx, res, lang);
|
|
2853
|
-
if (!control) return;
|
|
2854
|
-
sendJson(res, 200, pluginsPayload(control));
|
|
2855
|
-
}
|
|
2856
|
-
function pluginsPayload(control) {
|
|
2857
|
-
const installed = control.list();
|
|
2858
|
-
const installedMap = new Map(
|
|
2859
|
-
installed.map((p) => [p.name, { version: p.version, linked: p.linked }])
|
|
2860
|
-
);
|
|
2861
|
-
const shown = displayNamesOf(control);
|
|
2862
|
-
return {
|
|
2863
|
-
installed: installed.map((p) => toWirePluginEntry(p, shown)),
|
|
2864
|
-
hits: control.search().map((hit) => toWirePluginHit(hit, installedMap)),
|
|
2865
|
-
marketplaces: control.marketplaces().map(toWireMarketplace),
|
|
2866
|
-
pendingRestart: control.pendingRestart,
|
|
2867
|
-
uploadEnabled: control.uploadEnabled,
|
|
2868
|
-
uploadOptions: control.uploadOptions,
|
|
2869
|
-
refreshEnabled: control.refreshEnabled
|
|
2870
|
-
};
|
|
2871
|
-
}
|
|
2872
|
-
function sendPreview(res, outcome) {
|
|
3016
|
+
async function reconnectMcp(ctx, res, name, lang) {
|
|
3017
|
+
const outcome = await ctx.runtime.mcp.reconnect(name);
|
|
2873
3018
|
if (!outcome.ok) {
|
|
3019
|
+
if (outcome.reason === "unknown") {
|
|
3020
|
+
return sendError(res, 404, "unknown-mcp-server", t("web.mcp_unknown_server", { name }, lang));
|
|
3021
|
+
}
|
|
3022
|
+
const status = {
|
|
3023
|
+
...outcome.status,
|
|
3024
|
+
...outcome.detail === void 0 ? {} : { lastError: outcome.detail }
|
|
3025
|
+
};
|
|
2874
3026
|
return sendJson(res, 200, {
|
|
2875
3027
|
ok: false,
|
|
2876
|
-
|
|
2877
|
-
token: null,
|
|
2878
|
-
reason: outcome.reason,
|
|
2879
|
-
detail: outcome.detail
|
|
3028
|
+
server: toWireMcp(status)
|
|
2880
3029
|
});
|
|
2881
3030
|
}
|
|
2882
3031
|
sendJson(res, 200, {
|
|
2883
3032
|
ok: true,
|
|
2884
|
-
|
|
2885
|
-
token: outcome.token,
|
|
2886
|
-
reason: null,
|
|
2887
|
-
detail: null
|
|
3033
|
+
server: toWireMcp(outcome.status)
|
|
2888
3034
|
});
|
|
2889
3035
|
}
|
|
2890
|
-
async function
|
|
2891
|
-
if (
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
if (refusedByLan(ctx, res, lang)) return;
|
|
2900
|
-
const control = controlOf(ctx, res, lang);
|
|
2901
|
-
if (!control) return;
|
|
2902
|
-
const body = await readJsonBody(req);
|
|
2903
|
-
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
2904
|
-
sendPreview(res, await control.previewLocal(fieldOf(asRecord(body.value), "path"), lang));
|
|
2905
|
-
}
|
|
2906
|
-
async function previewPluginUpdate(ctx, req, res, lang) {
|
|
2907
|
-
if (refusedByLan(ctx, res, lang)) return;
|
|
2908
|
-
const control = controlOf(ctx, res, lang);
|
|
2909
|
-
if (!control) return;
|
|
3036
|
+
async function addMcp(ctx, req, res, lang) {
|
|
3037
|
+
if (ctx.lanExposed) {
|
|
3038
|
+
return sendError(
|
|
3039
|
+
res,
|
|
3040
|
+
403,
|
|
3041
|
+
"mcp-add-lan-exposed",
|
|
3042
|
+
t("web.mcp_add_lan_exposed", void 0, lang)
|
|
3043
|
+
);
|
|
3044
|
+
}
|
|
2910
3045
|
const body = await readJsonBody(req);
|
|
2911
3046
|
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
3047
|
+
const payload = asRecord(body.value);
|
|
3048
|
+
const name = typeof payload["name"] === "string" ? payload["name"].trim() : "";
|
|
3049
|
+
const transport = payload["transport"];
|
|
3050
|
+
if (transport !== "stdio" && transport !== "sse" && transport !== "http") {
|
|
3051
|
+
return sendError(res, 400, "bad-mcp-config", t("web.mcp_add_bad_transport", void 0, lang));
|
|
3052
|
+
}
|
|
3053
|
+
const outcome = await ctx.runtime.mcp.add({
|
|
3054
|
+
name,
|
|
3055
|
+
transport,
|
|
3056
|
+
...typeof payload["command"] === "string" ? { command: payload["command"].trim() } : {},
|
|
3057
|
+
...Array.isArray(payload["args"]) ? { args: stringList(payload["args"]) } : {},
|
|
3058
|
+
...typeof payload["url"] === "string" ? { url: payload["url"].trim() } : {},
|
|
3059
|
+
...payload["env"] === void 0 ? {} : { env: stringMap(payload["env"]) }
|
|
3060
|
+
});
|
|
2915
3061
|
if (!outcome.ok) {
|
|
2916
|
-
return
|
|
2917
|
-
ok: false,
|
|
2918
|
-
entry: null,
|
|
2919
|
-
pendingRestart: control.pendingRestart,
|
|
2920
|
-
detail: outcome.detail,
|
|
2921
|
-
reason: outcome.reason
|
|
2922
|
-
});
|
|
3062
|
+
return sendError(res, ...addFailure(outcome.reason, outcome.detail, name, lang));
|
|
2923
3063
|
}
|
|
2924
|
-
const fresh = "record" in outcome ? control.list().find((p) => p.name === outcome.record.name) : void 0;
|
|
2925
3064
|
sendJson(res, 200, {
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
pendingRestart: control.pendingRestart,
|
|
2929
|
-
detail: "detail" in outcome ? outcome.detail : null,
|
|
2930
|
-
reason: null
|
|
3065
|
+
server: toWireMcp(outcome.status),
|
|
3066
|
+
configPath: outcome.configPath
|
|
2931
3067
|
});
|
|
2932
3068
|
}
|
|
2933
|
-
|
|
2934
|
-
if (
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
if (
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
res,
|
|
2942
|
-
control,
|
|
2943
|
-
await control.install(fieldOf(payload, "ref"), fieldOf(payload, "token"), lang)
|
|
2944
|
-
);
|
|
3069
|
+
function addFailure(reason, detail, name, lang) {
|
|
3070
|
+
if (reason === "invalid-name") {
|
|
3071
|
+
return [400, "bad-mcp-name", t("web.mcp_add_bad_name", { name }, lang)];
|
|
3072
|
+
}
|
|
3073
|
+
if (reason === "invalid-config") return [400, "bad-mcp-config", detail];
|
|
3074
|
+
if (reason === "duplicate")
|
|
3075
|
+
return [409, "mcp-name-taken", t("web.mcp_add_taken", { name }, lang)];
|
|
3076
|
+
return [409, "mcp-config-unwritable", t("web.mcp_add_unwritable", { detail }, lang)];
|
|
2945
3077
|
}
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
const control = controlOf(ctx, res, lang);
|
|
2949
|
-
if (!control) return;
|
|
2950
|
-
const body = await readJsonBody(req);
|
|
2951
|
-
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
2952
|
-
const payload = asRecord(body.value);
|
|
2953
|
-
sendAction(
|
|
2954
|
-
res,
|
|
2955
|
-
control,
|
|
2956
|
-
await control.installLocal(fieldOf(payload, "path"), fieldOf(payload, "token"), lang)
|
|
2957
|
-
);
|
|
3078
|
+
function stringList(value) {
|
|
3079
|
+
return value.filter((v) => typeof v === "string");
|
|
2958
3080
|
}
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
const
|
|
2962
|
-
|
|
2963
|
-
const asJson = (req.headers["content-type"] ?? "").toLowerCase().includes("application/json");
|
|
2964
|
-
if (asJson) {
|
|
2965
|
-
const body = await readJsonBody(req, void 0, lang);
|
|
2966
|
-
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
2967
|
-
return sendStage(
|
|
2968
|
-
res,
|
|
2969
|
-
await control.stage({ kind: "path", path: fieldOf(asRecord(body.value), "path") }, lang)
|
|
2970
|
-
);
|
|
3081
|
+
function stringMap(value) {
|
|
3082
|
+
const out = {};
|
|
3083
|
+
for (const [k, v] of Object.entries(asRecord(value))) {
|
|
3084
|
+
if (typeof v === "string") out[k] = v;
|
|
2971
3085
|
}
|
|
2972
|
-
|
|
2973
|
-
if (!raw.ok) return sendError(res, raw.status, "bad-body", raw.message);
|
|
2974
|
-
return sendStage(
|
|
2975
|
-
res,
|
|
2976
|
-
await control.stage({ kind: "bytes", name: nameParam2(req), data: raw.value }, lang)
|
|
2977
|
-
);
|
|
3086
|
+
return out;
|
|
2978
3087
|
}
|
|
2979
|
-
function
|
|
2980
|
-
|
|
2981
|
-
return new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("name") ?? "";
|
|
2982
|
-
} catch {
|
|
2983
|
-
return "";
|
|
2984
|
-
}
|
|
3088
|
+
function toRef(raw) {
|
|
3089
|
+
return parseModelRef(raw);
|
|
2985
3090
|
}
|
|
2986
|
-
function
|
|
2987
|
-
|
|
2988
|
-
ok: true,
|
|
2989
|
-
path: outcome.path,
|
|
2990
|
-
name: outcome.name,
|
|
2991
|
-
version: outcome.version,
|
|
2992
|
-
detail: null,
|
|
2993
|
-
reason: null
|
|
2994
|
-
} : {
|
|
2995
|
-
ok: false,
|
|
2996
|
-
path: null,
|
|
2997
|
-
name: null,
|
|
2998
|
-
version: null,
|
|
2999
|
-
detail: outcome.detail,
|
|
3000
|
-
reason: outcome.reason
|
|
3001
|
-
};
|
|
3002
|
-
sendJson(res, 200, payload);
|
|
3003
|
-
}
|
|
3004
|
-
async function updateInstalledPlugin(ctx, req, res, lang) {
|
|
3005
|
-
if (refusedByLan(ctx, res, lang)) return;
|
|
3006
|
-
const control = controlOf(ctx, res, lang);
|
|
3007
|
-
if (!control) return;
|
|
3008
|
-
const body = await readJsonBody(req);
|
|
3009
|
-
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3010
|
-
const payload = asRecord(body.value);
|
|
3011
|
-
sendAction(
|
|
3012
|
-
res,
|
|
3013
|
-
control,
|
|
3014
|
-
await control.update(fieldOf(payload, "name"), fieldOf(payload, "token"), lang)
|
|
3015
|
-
);
|
|
3091
|
+
function toWireSelection(selection) {
|
|
3092
|
+
return { provider: selection.provider, model: selection.model, origin: selection.origin };
|
|
3016
3093
|
}
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
if (!control) return;
|
|
3021
|
-
const body = await readJsonBody(req);
|
|
3022
|
-
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3023
|
-
sendAction(res, control, await control.uninstall(fieldOf(asRecord(body.value), "name"), lang));
|
|
3094
|
+
function sessionHasImages(session) {
|
|
3095
|
+
const history = session?.getHistory() ?? [];
|
|
3096
|
+
return history.some((m) => m.parts?.some((p) => p.type === "image"));
|
|
3024
3097
|
}
|
|
3025
|
-
function
|
|
3098
|
+
function getModel(ctx, res, sessionId, _lang) {
|
|
3099
|
+
const { runtime } = ctx;
|
|
3100
|
+
const live = runtime.sessionFactory.get(sessionId);
|
|
3101
|
+
const selection = live?.model.get();
|
|
3026
3102
|
sendJson(res, 200, {
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
reason: outcome.ok ? null : outcome.reason,
|
|
3030
|
-
detail: outcome.ok ? null : outcome.detail
|
|
3031
|
-
});
|
|
3032
|
-
}
|
|
3033
|
-
async function addPluginMarketplace(ctx, req, res, lang) {
|
|
3034
|
-
if (refusedByLan(ctx, res, lang)) return;
|
|
3035
|
-
const control = controlOf(ctx, res, lang);
|
|
3036
|
-
if (!control) return;
|
|
3037
|
-
const body = await readJsonBody(req);
|
|
3038
|
-
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3039
|
-
sendMarket(res, await control.addMarketplace(fieldOf(asRecord(body.value), "source"), lang));
|
|
3040
|
-
}
|
|
3041
|
-
async function removePluginMarketplace(ctx, req, res, lang) {
|
|
3042
|
-
if (refusedByLan(ctx, res, lang)) return;
|
|
3043
|
-
const control = controlOf(ctx, res, lang);
|
|
3044
|
-
if (!control) return;
|
|
3045
|
-
const body = await readJsonBody(req);
|
|
3046
|
-
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3047
|
-
sendMarket(res, await control.removeMarketplace(fieldOf(asRecord(body.value), "name"), lang));
|
|
3048
|
-
}
|
|
3049
|
-
function previewPluginUpload(ctx, req, res, lang) {
|
|
3050
|
-
return withUploadBody(ctx, req, res, lang, (control, payload) => {
|
|
3051
|
-
sendUploadPreview(res, control.previewUpload(fieldOf(payload, "path"), lang));
|
|
3052
|
-
});
|
|
3053
|
-
}
|
|
3054
|
-
function uploadPlugin(ctx, req, res, lang) {
|
|
3055
|
-
return withUploadBody(ctx, req, res, lang, async (control, payload) => {
|
|
3056
|
-
sendUpload(
|
|
3057
|
-
res,
|
|
3058
|
-
await control.upload(
|
|
3059
|
-
fieldOf(payload, "path"),
|
|
3060
|
-
fieldOf(payload, "token"),
|
|
3061
|
-
uploadFormOf(payload),
|
|
3062
|
-
lang
|
|
3063
|
-
)
|
|
3064
|
-
);
|
|
3103
|
+
selection: selection ? toWireSelection(selection) : null,
|
|
3104
|
+
configured: (live ? live.model.configured() : runtime.config.model) || null
|
|
3065
3105
|
});
|
|
3066
3106
|
}
|
|
3067
|
-
async function
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3107
|
+
async function setModel(ctx, req, res, sessionId, lang) {
|
|
3108
|
+
const live = ctx.runtime.sessionFactory.get(sessionId);
|
|
3109
|
+
if (!live) return sendUnknownSession(res, sessionId);
|
|
3110
|
+
const control = live.model;
|
|
3071
3111
|
const body = await readJsonBody(req);
|
|
3072
3112
|
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
plan: null,
|
|
3080
|
-
token: null,
|
|
3081
|
-
prefill: null,
|
|
3082
|
-
fromManifest: null,
|
|
3083
|
-
reason: outcome.reason,
|
|
3084
|
-
detail: outcome.detail
|
|
3085
|
-
});
|
|
3086
|
-
}
|
|
3087
|
-
const { manifest, files, totalBytes, suspicious, skippedLinks } = outcome.plan;
|
|
3088
|
-
sendJson(res, 200, {
|
|
3089
|
-
ok: true,
|
|
3090
|
-
plan: {
|
|
3091
|
-
name: manifest.name,
|
|
3092
|
-
version: manifest.version,
|
|
3093
|
-
description: manifest.description ?? "",
|
|
3094
|
-
author: manifest.author?.name ?? null,
|
|
3095
|
-
files: files.map((f) => ({ path: f.path, bytes: f.bytes })),
|
|
3096
|
-
totalBytes,
|
|
3097
|
-
suspicious: [...suspicious],
|
|
3098
|
-
skippedLinks: [...skippedLinks]
|
|
3099
|
-
},
|
|
3100
|
-
token: outcome.token,
|
|
3101
|
-
prefill: outcome.prefill,
|
|
3102
|
-
fromManifest: [...outcome.fromManifest],
|
|
3103
|
-
reason: null,
|
|
3104
|
-
detail: null
|
|
3105
|
-
});
|
|
3106
|
-
}
|
|
3107
|
-
function sendUpload(res, outcome) {
|
|
3108
|
-
sendJson(res, 200, {
|
|
3109
|
-
ok: outcome.ok,
|
|
3110
|
-
detail: outcome.detail,
|
|
3111
|
-
uploaded: outcome.ok ? { name: outcome.name, version: outcome.version, bytes: outcome.bytes } : null,
|
|
3112
|
-
reason: outcome.ok ? null : outcome.reason
|
|
3113
|
-
});
|
|
3114
|
-
}
|
|
3115
|
-
async function refreshPlugins(ctx, res, lang) {
|
|
3116
|
-
if (refusedByLan(ctx, res, lang)) return;
|
|
3117
|
-
const control = controlOf(ctx, res, lang);
|
|
3118
|
-
if (!control) return;
|
|
3119
|
-
const outcome = await control.refresh(lang);
|
|
3120
|
-
if (!outcome.ok) {
|
|
3121
|
-
return sendJson(res, 200, {
|
|
3122
|
-
ok: false,
|
|
3123
|
-
plugins: null,
|
|
3124
|
-
reason: outcome.reason,
|
|
3125
|
-
detail: outcome.detail
|
|
3126
|
-
});
|
|
3113
|
+
const input = readModelSetBody(asRecord(body.value));
|
|
3114
|
+
if (!input.ok)
|
|
3115
|
+
return sendError(res, 400, "bad-model", t("web.model_bad_request", void 0, lang));
|
|
3116
|
+
const ref = input.model === null ? null : toRef(input.model);
|
|
3117
|
+
if (ref === void 0) {
|
|
3118
|
+
return sendError(res, 400, "bad-model", t("web.model_bad_request", void 0, lang));
|
|
3127
3119
|
}
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
reason: null,
|
|
3132
|
-
detail: outcome.detail === "" ? null : outcome.detail
|
|
3133
|
-
});
|
|
3134
|
-
}
|
|
3135
|
-
function toWireProvider(option, lang) {
|
|
3136
|
-
return {
|
|
3137
|
-
type: option.type,
|
|
3138
|
-
label: providerLabel(option, lang),
|
|
3139
|
-
envVar: option.envVar,
|
|
3140
|
-
hasKey: option.hasKey,
|
|
3141
|
-
keyHint: option.keyHint,
|
|
3142
|
-
current: option.current
|
|
3120
|
+
const selectionCtx = {
|
|
3121
|
+
hasImages: sessionHasImages(live.session),
|
|
3122
|
+
...input.usedTokens === void 0 ? {} : { usedTokens: input.usedTokens }
|
|
3143
3123
|
};
|
|
3124
|
+
const result = ref === null ? control.reset(selectionCtx) : control.set(ref, selectionCtx);
|
|
3125
|
+
const payload = result.ok ? {
|
|
3126
|
+
ok: true,
|
|
3127
|
+
selection: toWireSelection(result.selection),
|
|
3128
|
+
contextLength: result.contextLength,
|
|
3129
|
+
caveats: result.caveats
|
|
3130
|
+
} : { ok: false, rejection: result.rejection };
|
|
3131
|
+
sendJson(res, 200, payload);
|
|
3144
3132
|
}
|
|
3145
|
-
function
|
|
3133
|
+
function readModelSetBody(body) {
|
|
3134
|
+
const { model, usedTokens } = body;
|
|
3135
|
+
if (model !== null && (typeof model !== "string" || model.trim() === "")) return { ok: false };
|
|
3136
|
+
const used = typeof usedTokens === "number" && Number.isFinite(usedTokens) && usedTokens >= 0 ? Math.floor(usedTokens) : void 0;
|
|
3146
3137
|
return {
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
status: view.status
|
|
3138
|
+
ok: true,
|
|
3139
|
+
model: model === null ? null : model.trim(),
|
|
3140
|
+
...used === void 0 ? {} : { usedTokens: used }
|
|
3151
3141
|
};
|
|
3152
3142
|
}
|
|
3153
|
-
function
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
}
|
|
3143
|
+
function planModeState(ctx, sessionId) {
|
|
3144
|
+
if (!ctx.runtime.plan) return null;
|
|
3145
|
+
const plan = ctx.runtime.sessionFactory.get(sessionId)?.plan;
|
|
3146
|
+
if (!plan) return { active: false, from: null };
|
|
3147
|
+
return { active: plan.active(), from: plan.from() };
|
|
3157
3148
|
}
|
|
3158
|
-
async function
|
|
3159
|
-
|
|
3160
|
-
|
|
3149
|
+
async function setPlanMode(ctx, req, res, sessionId, lang) {
|
|
3150
|
+
const live = ctx.runtime.sessionFactory.get(sessionId);
|
|
3151
|
+
if (!live) return sendUnknownSession(res, sessionId);
|
|
3152
|
+
const plan = live.plan;
|
|
3153
|
+
if (!plan) {
|
|
3154
|
+
return sendError(res, 503, "no-plan-mode", t("web.plan_mode_unavailable", void 0, lang));
|
|
3161
3155
|
}
|
|
3162
3156
|
const body = await readJsonBody(req);
|
|
3163
3157
|
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3164
|
-
const
|
|
3165
|
-
|
|
3166
|
-
sendJson(res, 200, {
|
|
3167
|
-
suggestions: toWireSuggestions(suggestions)
|
|
3168
|
-
});
|
|
3169
|
-
}
|
|
3170
|
-
async function setProviderKey(ctx, req, res, type, lang) {
|
|
3171
|
-
if (!isProviderType(type)) {
|
|
3172
|
-
return sendError(res, 404, "unknown-provider", t("web.provider_unknown", { name: type }, lang));
|
|
3173
|
-
}
|
|
3174
|
-
const envVar = apiKeyEnvVar(type);
|
|
3175
|
-
if (envVar === null) {
|
|
3158
|
+
const action = asRecord(body.value)["action"];
|
|
3159
|
+
if (typeof action !== "string" || !isWirePlanAction(action)) {
|
|
3176
3160
|
return sendError(
|
|
3177
3161
|
res,
|
|
3178
3162
|
400,
|
|
3179
|
-
"
|
|
3180
|
-
t("web.
|
|
3163
|
+
"bad-action",
|
|
3164
|
+
t("web.plan_mode_bad_action", { actions: WIRE_PLAN_ACTIONS.join("/") }, lang)
|
|
3181
3165
|
);
|
|
3182
3166
|
}
|
|
3183
|
-
const
|
|
3184
|
-
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3185
|
-
const raw = asRecord(body.value)["apiKey"];
|
|
3186
|
-
const apiKey = typeof raw === "string" ? raw.trim() : "";
|
|
3187
|
-
if (apiKey === "") {
|
|
3188
|
-
return sendError(res, 400, "empty-key", t("web.provider_key_empty", void 0, lang));
|
|
3189
|
-
}
|
|
3190
|
-
const written = await ctx.runtime.modelCatalog.setKey(type, apiKey);
|
|
3167
|
+
const changed = action === "enter" ? plan.enter().ok : plan.leave() !== null;
|
|
3191
3168
|
sendJson(res, 200, {
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
keyHint: written.keyHint,
|
|
3195
|
-
backend: written.backend,
|
|
3196
|
-
encrypted: written.encrypted,
|
|
3197
|
-
envPath: written.envPath
|
|
3169
|
+
mode: { active: plan.active(), from: plan.from() },
|
|
3170
|
+
changed
|
|
3198
3171
|
});
|
|
3199
3172
|
}
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
t("web.model_default_has_provider", void 0, lang)
|
|
3207
|
-
);
|
|
3208
|
-
}
|
|
3209
|
-
const body = await readJsonBody(req, void 0, lang);
|
|
3210
|
-
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3211
|
-
const raw = asRecord(body.value)["model"];
|
|
3212
|
-
const model = typeof raw === "string" ? raw.trim() : "";
|
|
3213
|
-
if (model === "") {
|
|
3214
|
-
return sendError(res, 400, "bad-request", t("web.model_default_empty", void 0, lang));
|
|
3215
|
-
}
|
|
3216
|
-
const report = ctx.runtime.settings.write({ key: "model", layer: "user", value: model });
|
|
3217
|
-
if (!report.ok) {
|
|
3218
|
-
const failure = settingWriteFailure(report);
|
|
3219
|
-
return sendError(
|
|
3220
|
-
res,
|
|
3221
|
-
failure.status,
|
|
3222
|
-
failure.code,
|
|
3223
|
-
t(failure.key, { path: failure.path }, lang)
|
|
3224
|
-
);
|
|
3225
|
-
}
|
|
3226
|
-
sendJson(res, 200, toWireSettingsWrite(report));
|
|
3173
|
+
// src/permission.ts
|
|
3174
|
+
var REFUSAL = {
|
|
3175
|
+
"managed-bypass-disabled": "managed"
|
|
3176
|
+
};
|
|
3177
|
+
function blockedLevels(managed) {
|
|
3178
|
+
return managed.bypassDisabled ? [{ level: "bypass", reason: "managed" }] : [];
|
|
3227
3179
|
}
|
|
3228
|
-
|
|
3229
|
-
|
|
3180
|
+
function permissionState(ctx, sessionId) {
|
|
3181
|
+
const perms = ctx.runtime.sessionFactory.get(sessionId)?.permissions ?? null;
|
|
3182
|
+
return {
|
|
3183
|
+
level: perms?.level() ?? ctx.runtime.config.permission,
|
|
3184
|
+
editable: perms !== null,
|
|
3185
|
+
blocked: perms ? blockedLevels(perms.managed()) : [],
|
|
3186
|
+
planMode: planModeState(ctx, sessionId)
|
|
3187
|
+
};
|
|
3188
|
+
}
|
|
3189
|
+
function getPermission(ctx, res, sessionId, _lang) {
|
|
3190
|
+
if (!ctx.hub.has(sessionId)) return sendUnknownSession(res, sessionId);
|
|
3191
|
+
sendJson(res, 200, permissionState(ctx, sessionId));
|
|
3192
|
+
}
|
|
3193
|
+
async function setPermission(ctx, req, res, sessionId, lang) {
|
|
3194
|
+
const live = ctx.runtime.sessionFactory.get(sessionId);
|
|
3195
|
+
if (!live) return sendUnknownSession(res, sessionId);
|
|
3196
|
+
const perms = live.permissions;
|
|
3197
|
+
if (!perms) {
|
|
3230
3198
|
return sendError(
|
|
3231
3199
|
res,
|
|
3232
|
-
|
|
3233
|
-
"
|
|
3234
|
-
t("web.
|
|
3200
|
+
503,
|
|
3201
|
+
"no-permission-layer",
|
|
3202
|
+
t("web.permission_unavailable", void 0, lang)
|
|
3235
3203
|
);
|
|
3236
3204
|
}
|
|
3237
3205
|
const body = await readJsonBody(req);
|
|
3238
3206
|
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3239
|
-
const
|
|
3240
|
-
|
|
3241
|
-
{
|
|
3242
|
-
name: typeof payload["name"] === "string" ? payload["name"].trim() : "",
|
|
3243
|
-
description: typeof payload["description"] === "string" ? payload["description"].trim() : "",
|
|
3244
|
-
...typeof payload["prompt"] === "string" ? { prompt: payload["prompt"] } : {},
|
|
3245
|
-
...Array.isArray(payload["tools"]) ? { tools: stringList2(payload["tools"]) } : {},
|
|
3246
|
-
...Array.isArray(payload["skills"]) ? { skills: stringList2(payload["skills"]) } : {},
|
|
3247
|
-
...typeof payload["maxTurns"] === "number" ? { maxTurns: payload["maxTurns"] } : {}
|
|
3248
|
-
},
|
|
3249
|
-
lang
|
|
3250
|
-
);
|
|
3251
|
-
if (!outcome.ok) return sendError(res, ...addFailure2(outcome.reason, outcome.detail, lang));
|
|
3252
|
-
sendJson(res, 200, {
|
|
3253
|
-
role: toWireRole(outcome.role, ctx.runtime.agentRoleNotices),
|
|
3254
|
-
path: outcome.path
|
|
3255
|
-
});
|
|
3256
|
-
}
|
|
3257
|
-
function addFailure2(reason, detail, lang) {
|
|
3258
|
-
if (reason === "invalid-name") return [400, "bad-role-name", detail];
|
|
3259
|
-
if (reason === "invalid-field")
|
|
3260
|
-
return [400, "bad-role-field", t("web.role_add_bad_field", { detail }, lang)];
|
|
3261
|
-
if (reason === "unknown-tool") return [400, "bad-role-tools", detail];
|
|
3262
|
-
if (reason === "duplicate") return [409, "role-name-taken", detail];
|
|
3263
|
-
return [409, "role-unwritable", detail];
|
|
3264
|
-
}
|
|
3265
|
-
function stringList2(value) {
|
|
3266
|
-
return value.filter((v) => typeof v === "string");
|
|
3267
|
-
}
|
|
3268
|
-
async function removeRole(ctx, req, res, lang) {
|
|
3269
|
-
if (ctx.lanExposed) {
|
|
3207
|
+
const level = asRecord(body.value)["level"];
|
|
3208
|
+
if (typeof level !== "string" || !isPermissionLevel(level)) {
|
|
3270
3209
|
return sendError(
|
|
3271
3210
|
res,
|
|
3272
|
-
|
|
3273
|
-
"
|
|
3274
|
-
t("web.
|
|
3211
|
+
400,
|
|
3212
|
+
"bad-level",
|
|
3213
|
+
t("web.permission_bad_level", { levels: PERMISSION_LEVELS.join("/") }, lang)
|
|
3275
3214
|
);
|
|
3276
3215
|
}
|
|
3277
|
-
const
|
|
3278
|
-
|
|
3279
|
-
const
|
|
3280
|
-
const
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3216
|
+
const before = perms.level();
|
|
3217
|
+
const result = perms.setLevel(level, { remember: true });
|
|
3218
|
+
const state = permissionState(ctx, sessionId);
|
|
3219
|
+
const payload = result.ok ? {
|
|
3220
|
+
ok: true,
|
|
3221
|
+
changed: state.level !== before,
|
|
3222
|
+
state,
|
|
3223
|
+
...result.notRemembered ? { notRemembered: result.notRemembered } : {}
|
|
3224
|
+
} : { ok: false, refused: REFUSAL[result.reason], changed: false, state };
|
|
3225
|
+
sendJson(res, 200, payload);
|
|
3226
|
+
}
|
|
3227
|
+
// src/plan.ts
|
|
3228
|
+
function getPlan(ctx, res, sessionId, _lang) {
|
|
3229
|
+
if (!ctx.hub.has(sessionId) && !ctx.runtime.sessions?.get(sessionId)) {
|
|
3230
|
+
return sendUnknownSession(res, sessionId);
|
|
3291
3231
|
}
|
|
3232
|
+
const live = ctx.runtime.sessionFactory.get(sessionId);
|
|
3233
|
+
const markdown = live ? live.plan?.approvedPlan() ?? null : ctx.runtime.sessionStore?.loadPlan(sessionId) ?? null;
|
|
3292
3234
|
sendJson(res, 200, {
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
path: outcome.path,
|
|
3296
|
-
takenOverBy: outcome.takenOverBy,
|
|
3297
|
-
detail: null,
|
|
3298
|
-
reason: null
|
|
3235
|
+
plan: normalize(markdown),
|
|
3236
|
+
mode: planModeState(ctx, sessionId)
|
|
3299
3237
|
});
|
|
3300
3238
|
}
|
|
3301
|
-
function
|
|
3302
|
-
|
|
3303
|
-
const out = [];
|
|
3304
|
-
for (const item of value) {
|
|
3305
|
-
if (typeof item !== "string" || item.trim().length === 0) return null;
|
|
3306
|
-
out.push(item);
|
|
3307
|
-
}
|
|
3308
|
-
return out;
|
|
3239
|
+
function normalize(markdown) {
|
|
3240
|
+
return markdown?.trim() ? markdown : null;
|
|
3309
3241
|
}
|
|
3310
|
-
function
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3242
|
+
function toWireCounts(counts) {
|
|
3243
|
+
return {
|
|
3244
|
+
commands: counts.commands,
|
|
3245
|
+
roles: counts.roles,
|
|
3246
|
+
skills: counts.skills,
|
|
3247
|
+
hooks: counts.hooks,
|
|
3248
|
+
denyRules: counts.denyRules,
|
|
3249
|
+
mcpServers: counts.mcpServers
|
|
3250
|
+
};
|
|
3251
|
+
}
|
|
3252
|
+
function displayNamesOf(control) {
|
|
3253
|
+
const out = /* @__PURE__ */ new Map();
|
|
3254
|
+
for (const hit of control.search()) {
|
|
3255
|
+
const shown = hit.entry.displayName;
|
|
3256
|
+
if (shown !== void 0 && shown !== "") out.set(`${hit.marketplace}/${hit.entry.name}`, shown);
|
|
3316
3257
|
}
|
|
3317
3258
|
return out;
|
|
3318
3259
|
}
|
|
3319
|
-
function
|
|
3320
|
-
return
|
|
3260
|
+
function toWirePluginEntry(entry, displayNames) {
|
|
3261
|
+
return {
|
|
3262
|
+
name: entry.name,
|
|
3263
|
+
displayName: displayNames.get(`${entry.marketplace}/${entry.name}`) ?? null,
|
|
3264
|
+
version: entry.version,
|
|
3265
|
+
source: entry.source,
|
|
3266
|
+
sourceType: entry.sourceType,
|
|
3267
|
+
path: entry.path,
|
|
3268
|
+
linked: entry.linked,
|
|
3269
|
+
enabled: entry.enabled,
|
|
3270
|
+
installedAt: entry.installedAt,
|
|
3271
|
+
marketplace: entry.marketplace ?? null,
|
|
3272
|
+
active: entry.active,
|
|
3273
|
+
counts: entry.counts ? toWireCounts(entry.counts) : null,
|
|
3274
|
+
updateAllowed: entry.updateAllowed,
|
|
3275
|
+
uninstallAllowed: entry.uninstallAllowed
|
|
3276
|
+
};
|
|
3321
3277
|
}
|
|
3322
|
-
function
|
|
3323
|
-
const
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3278
|
+
function toWirePluginHit(hit, installed) {
|
|
3279
|
+
const mine = installed.get(hit.entry.name);
|
|
3280
|
+
return {
|
|
3281
|
+
marketplace: hit.marketplace,
|
|
3282
|
+
ref: hit.ref,
|
|
3283
|
+
name: hit.entry.name,
|
|
3284
|
+
description: hit.entry.description ?? "",
|
|
3285
|
+
category: hit.entry.category ?? null,
|
|
3286
|
+
keywords: [...hit.entry.keywords ?? []],
|
|
3287
|
+
sourceType: hit.sourceType ?? null,
|
|
3288
|
+
installable: hit.installable,
|
|
3289
|
+
installed: mine !== void 0,
|
|
3290
|
+
author: hit.entry.author?.name ?? null,
|
|
3291
|
+
downloads: hit.entry.downloads ?? null,
|
|
3292
|
+
likes: hit.entry.likes ?? null,
|
|
3293
|
+
displayName: hit.entry.displayName ?? null,
|
|
3294
|
+
icon: hit.entry.icon ?? null,
|
|
3295
|
+
version: hit.entry.version ?? null,
|
|
3296
|
+
changelog: hit.entry.changelog ?? null,
|
|
3297
|
+
size: hit.entry.size ?? null,
|
|
3298
|
+
homepage: hit.entry.homepage ?? null,
|
|
3299
|
+
visibility: hit.entry.visibility ?? null,
|
|
3300
|
+
status: hit.entry.status ?? null,
|
|
3301
|
+
updateAvailable: mine !== void 0 && !mine.linked && hit.entry.version !== void 0 && hit.entry.version !== mine.version,
|
|
3302
|
+
installAllowed: hit.installAllowed,
|
|
3303
|
+
updateAllowed: mine?.updateAllowed ?? false,
|
|
3304
|
+
menu: hit.menu.map((item) => ({
|
|
3305
|
+
id: item.id,
|
|
3306
|
+
label: item.label,
|
|
3307
|
+
danger: item.danger === true
|
|
3308
|
+
}))
|
|
3309
|
+
};
|
|
3347
3310
|
}
|
|
3348
|
-
function
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3311
|
+
function toWireMarketplace(market) {
|
|
3312
|
+
return {
|
|
3313
|
+
name: market.name,
|
|
3314
|
+
source: market.source,
|
|
3315
|
+
entries: market.entries,
|
|
3316
|
+
addedAt: market.addedAt,
|
|
3317
|
+
local: market.local,
|
|
3318
|
+
categories: (market.categories ?? []).map(toWireCategory)
|
|
3319
|
+
};
|
|
3356
3320
|
}
|
|
3357
|
-
function
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
}
|
|
3364
|
-
return out;
|
|
3321
|
+
function toWireCategory(node) {
|
|
3322
|
+
return {
|
|
3323
|
+
id: node.id,
|
|
3324
|
+
label: node.label ?? node.id,
|
|
3325
|
+
children: (node.children ?? []).map(toWireCategory)
|
|
3326
|
+
};
|
|
3365
3327
|
}
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
const out = {};
|
|
3369
|
-
const bad = (field) => ({
|
|
3370
|
-
ok: false,
|
|
3371
|
-
message: t("web.schedule_bad_field", { field }, lang)
|
|
3372
|
-
});
|
|
3373
|
-
for (const key of ["name", "prompt"]) {
|
|
3374
|
-
const value = raw[key];
|
|
3375
|
-
if (value === void 0) continue;
|
|
3376
|
-
if (typeof value !== "string") return bad(key);
|
|
3377
|
-
out[key] = value;
|
|
3378
|
-
}
|
|
3379
|
-
for (const key of CLEARABLE) {
|
|
3380
|
-
const value = raw[key];
|
|
3381
|
-
if (value === void 0) continue;
|
|
3382
|
-
if (value === null) {
|
|
3383
|
-
if (!allowClear) return bad(key);
|
|
3384
|
-
out[key] = null;
|
|
3385
|
-
continue;
|
|
3386
|
-
}
|
|
3387
|
-
if (typeof value !== "string") return bad(key);
|
|
3388
|
-
out[key] = value;
|
|
3389
|
-
}
|
|
3390
|
-
for (const key of ["maxTurns", "maxBudgetUsd", "timeoutMs"]) {
|
|
3391
|
-
const value = raw[key];
|
|
3392
|
-
if (value === void 0) continue;
|
|
3393
|
-
const num = finite(value);
|
|
3394
|
-
if (num === null)
|
|
3395
|
-
return { ok: false, message: t("web.schedule_bad_field", { field: key }, lang) };
|
|
3396
|
-
out[key] = num;
|
|
3397
|
-
}
|
|
3398
|
-
for (const key of ["allowTools", "allowRules"]) {
|
|
3399
|
-
const value = raw[key];
|
|
3400
|
-
if (value === void 0) continue;
|
|
3401
|
-
const list = stringList3(value);
|
|
3402
|
-
if (list === null)
|
|
3403
|
-
return { ok: false, message: t("web.schedule_bad_field", { field: key }, lang) };
|
|
3404
|
-
out[key] = list;
|
|
3405
|
-
}
|
|
3406
|
-
if (raw["allowOperations"] !== void 0) {
|
|
3407
|
-
const ops = operationList(raw["allowOperations"]);
|
|
3408
|
-
if (ops === null) {
|
|
3409
|
-
return {
|
|
3410
|
-
ok: false,
|
|
3411
|
-
message: t("web.schedule_bad_operation", { types: OPERATION_TYPES.join("/") }, lang)
|
|
3412
|
-
};
|
|
3413
|
-
}
|
|
3414
|
-
out.allowOperations = ops;
|
|
3415
|
-
}
|
|
3416
|
-
for (const key of ["enabled", "showInSessionList"]) {
|
|
3417
|
-
const value = raw[key];
|
|
3418
|
-
if (value === void 0) continue;
|
|
3419
|
-
if (typeof value !== "boolean") {
|
|
3420
|
-
return { ok: false, message: t("web.schedule_bad_field", { field: key }, lang) };
|
|
3421
|
-
}
|
|
3422
|
-
out[key] = value;
|
|
3423
|
-
}
|
|
3424
|
-
if (raw["permission"] !== void 0) {
|
|
3425
|
-
const level = raw["permission"];
|
|
3426
|
-
if (typeof level !== "string" || !isPermissionLevel(level)) {
|
|
3427
|
-
return { ok: false, message: t("web.schedule_bad_permission", void 0, lang) };
|
|
3428
|
-
}
|
|
3429
|
-
out.permission = level;
|
|
3430
|
-
}
|
|
3431
|
-
if (raw["trigger"] !== void 0) {
|
|
3432
|
-
const trigger = parseTrigger(raw["trigger"]);
|
|
3433
|
-
if (trigger === null)
|
|
3434
|
-
return { ok: false, message: t("web.schedule_bad_trigger", void 0, lang) };
|
|
3435
|
-
out.trigger = trigger;
|
|
3436
|
-
}
|
|
3437
|
-
return { ok: true, value: out };
|
|
3438
|
-
}
|
|
3439
|
-
function bypassAcknowledged(body) {
|
|
3440
|
-
return asRecord(body)["bypassAcknowledged"] === true;
|
|
3441
|
-
}
|
|
3442
|
-
function parseCreate2(body, lang) {
|
|
3443
|
-
const raw = asRecord(body);
|
|
3444
|
-
const name = raw["name"];
|
|
3445
|
-
const prompt = raw["prompt"];
|
|
3446
|
-
const permission = raw["permission"];
|
|
3447
|
-
const budget = finite(raw["maxBudgetUsd"]);
|
|
3448
|
-
if (typeof name !== "string" || typeof prompt !== "string") {
|
|
3449
|
-
return { ok: false, message: t("web.schedule_create_incomplete", void 0, lang) };
|
|
3450
|
-
}
|
|
3451
|
-
if (typeof permission !== "string" || !isPermissionLevel(permission)) {
|
|
3452
|
-
return { ok: false, message: t("web.schedule_bad_permission", void 0, lang) };
|
|
3453
|
-
}
|
|
3454
|
-
const trigger = parseTrigger(raw["trigger"]);
|
|
3455
|
-
if (trigger === null)
|
|
3456
|
-
return { ok: false, message: t("web.schedule_bad_trigger", void 0, lang) };
|
|
3457
|
-
if (budget === null)
|
|
3458
|
-
return { ok: false, message: t("web.schedule_budget_required", void 0, lang) };
|
|
3459
|
-
const rest = optionals(raw, false, lang);
|
|
3460
|
-
if (!rest.ok) return rest;
|
|
3328
|
+
function toWirePreview2(preview) {
|
|
3329
|
+
const { manifest, source, inventory } = preview;
|
|
3461
3330
|
return {
|
|
3462
|
-
|
|
3463
|
-
|
|
3331
|
+
name: manifest.name,
|
|
3332
|
+
version: manifest.version,
|
|
3333
|
+
description: manifest.description ?? "",
|
|
3334
|
+
source: source.raw,
|
|
3335
|
+
sourceType: source.type,
|
|
3336
|
+
checksum: source.sha256 ?? null,
|
|
3337
|
+
author: manifest.author?.name ?? null,
|
|
3338
|
+
homepage: manifest.homepage ?? null,
|
|
3339
|
+
commands: [...inventory.commands],
|
|
3340
|
+
roles: [...inventory.roles],
|
|
3341
|
+
skills: [...inventory.skills],
|
|
3342
|
+
hooks: inventory.hooks.map((h) => ({ type: h.type, count: h.count })),
|
|
3343
|
+
denyRules: inventory.denyRules,
|
|
3344
|
+
mcpServers: [...inventory.mcpServers],
|
|
3345
|
+
ignoredBuckets: [...inventory.ignoredBuckets],
|
|
3346
|
+
hasJsTools: inventory.jsTools,
|
|
3347
|
+
conflict: preview.conflict ? { name: preview.conflict.name, version: preview.conflict.version } : null
|
|
3464
3348
|
};
|
|
3465
3349
|
}
|
|
3466
|
-
function
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
const runId = asRecord(body)["runId"];
|
|
3471
|
-
if (typeof runId !== "string" || runId.trim().length === 0) {
|
|
3472
|
-
return { ok: false, message: t("web.schedule_fix_needs_run", void 0, lang) };
|
|
3473
|
-
}
|
|
3474
|
-
return { ok: true, value: runId };
|
|
3475
|
-
}
|
|
3476
|
-
// src/schedule/handlers.ts
|
|
3477
|
-
var RECENT_RUNS_LIMIT = 100;
|
|
3478
|
-
var RUNS_LIMIT = 20;
|
|
3479
|
-
var MAX_RECORDING_FRAMES = 4e3;
|
|
3480
|
-
function schedulesOf(ctx, res, lang) {
|
|
3481
|
-
const control = ctx.runtime.schedules;
|
|
3482
|
-
if (!control.storeAvailable) {
|
|
3483
|
-
sendError(res, 503, "no-schedule-store", t("web.schedule_store_unavailable", void 0, lang));
|
|
3350
|
+
function controlOf(ctx, res, lang) {
|
|
3351
|
+
const control = ctx.runtime.plugins;
|
|
3352
|
+
if (!control) {
|
|
3353
|
+
sendError(res, 503, "no-plugin-control", t("web.plugin_not_available", void 0, lang));
|
|
3484
3354
|
return null;
|
|
3485
3355
|
}
|
|
3486
3356
|
return control;
|
|
3487
3357
|
}
|
|
3488
|
-
function
|
|
3489
|
-
return
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3358
|
+
function refusedByLan(ctx, res, lang) {
|
|
3359
|
+
if (!ctx.lanExposed) return false;
|
|
3360
|
+
sendError(
|
|
3361
|
+
res,
|
|
3362
|
+
403,
|
|
3363
|
+
"plugin-write-lan-exposed",
|
|
3364
|
+
t("web.plugin_write_lan_exposed", void 0, lang)
|
|
3365
|
+
);
|
|
3366
|
+
return true;
|
|
3494
3367
|
}
|
|
3495
|
-
function
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
timeoutMs: SCHEDULE_DEFAULTS.timeoutMs,
|
|
3499
|
-
maxBudgetUsd: SCHEDULE_FORM_BUDGET_USD,
|
|
3500
|
-
allowTools: SCHEDULE_DEFAULT_ALLOWLIST.allowTools,
|
|
3501
|
-
allowOperations: SCHEDULE_DEFAULT_ALLOWLIST.allowOperations,
|
|
3502
|
-
allowRules: SCHEDULE_DEFAULT_ALLOWLIST.allowRules
|
|
3503
|
-
};
|
|
3368
|
+
function fieldOf(payload, key) {
|
|
3369
|
+
const value = payload[key];
|
|
3370
|
+
return typeof value === "string" ? value.trim() : "";
|
|
3504
3371
|
}
|
|
3505
|
-
|
|
3506
|
-
const
|
|
3372
|
+
function uploadFormOf(payload) {
|
|
3373
|
+
const form = {};
|
|
3374
|
+
for (const name of WIRE_PLUGIN_UPLOAD_FORM_FIELDS) {
|
|
3375
|
+
const raw = fieldOf(payload, name);
|
|
3376
|
+
if (raw === "") continue;
|
|
3377
|
+
if (name === "visibility") {
|
|
3378
|
+
if (raw === "private" || raw === "public") form[name] = raw;
|
|
3379
|
+
continue;
|
|
3380
|
+
}
|
|
3381
|
+
form[name] = raw;
|
|
3382
|
+
}
|
|
3383
|
+
return form;
|
|
3384
|
+
}
|
|
3385
|
+
function listPlugins(ctx, res, lang) {
|
|
3386
|
+
const control = controlOf(ctx, res, lang);
|
|
3507
3387
|
if (!control) return;
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3388
|
+
sendJson(res, 200, pluginsPayload(control));
|
|
3389
|
+
}
|
|
3390
|
+
function pluginsPayload(control) {
|
|
3391
|
+
const installed = control.list();
|
|
3392
|
+
const installedMap = new Map(
|
|
3393
|
+
installed.map((p) => [
|
|
3394
|
+
p.name,
|
|
3395
|
+
{ version: p.version, linked: p.linked, updateAllowed: p.updateAllowed }
|
|
3396
|
+
])
|
|
3397
|
+
);
|
|
3398
|
+
const shown = displayNamesOf(control);
|
|
3399
|
+
return {
|
|
3400
|
+
installed: installed.map((p) => toWirePluginEntry(p, shown)),
|
|
3401
|
+
hits: control.search().map((hit) => toWirePluginHit(hit, installedMap)),
|
|
3402
|
+
marketplaces: control.marketplaces().map(toWireMarketplace),
|
|
3403
|
+
pendingRestart: control.pendingRestart,
|
|
3404
|
+
uploadEnabled: control.uploadEnabled,
|
|
3405
|
+
uploadOptions: control.uploadOptions,
|
|
3406
|
+
refreshEnabled: control.refreshEnabled,
|
|
3407
|
+
localInstallEnabled: control.localInstallEnabled
|
|
3514
3408
|
};
|
|
3515
|
-
sendJson(res, 200, payload);
|
|
3516
3409
|
}
|
|
3517
|
-
function
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3410
|
+
function sendPreview(res, outcome) {
|
|
3411
|
+
if (!outcome.ok) {
|
|
3412
|
+
return sendJson(res, 200, {
|
|
3413
|
+
ok: false,
|
|
3414
|
+
preview: null,
|
|
3415
|
+
token: null,
|
|
3416
|
+
reason: outcome.reason,
|
|
3417
|
+
detail: outcome.detail
|
|
3418
|
+
});
|
|
3419
|
+
}
|
|
3420
|
+
sendJson(res, 200, {
|
|
3421
|
+
ok: true,
|
|
3422
|
+
preview: toWirePreview2(outcome.preview),
|
|
3423
|
+
token: outcome.token,
|
|
3424
|
+
reason: null,
|
|
3425
|
+
detail: null
|
|
3426
|
+
});
|
|
3522
3427
|
}
|
|
3523
|
-
function
|
|
3524
|
-
|
|
3428
|
+
async function previewPluginInstall(ctx, req, res, lang) {
|
|
3429
|
+
if (refusedByLan(ctx, res, lang)) return;
|
|
3430
|
+
const control = controlOf(ctx, res, lang);
|
|
3525
3431
|
if (!control) return;
|
|
3526
|
-
const
|
|
3527
|
-
|
|
3432
|
+
const body = await readJsonBody(req);
|
|
3433
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3434
|
+
sendPreview(res, await control.preview(fieldOf(asRecord(body.value), "ref"), lang));
|
|
3528
3435
|
}
|
|
3529
|
-
function
|
|
3530
|
-
|
|
3436
|
+
async function previewLocalPluginInstall(ctx, req, res, lang) {
|
|
3437
|
+
if (refusedByLan(ctx, res, lang)) return;
|
|
3438
|
+
const control = controlOf(ctx, res, lang);
|
|
3531
3439
|
if (!control) return;
|
|
3532
|
-
const
|
|
3533
|
-
if (!
|
|
3534
|
-
|
|
3440
|
+
const body = await readJsonBody(req);
|
|
3441
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3442
|
+
sendPreview(res, await control.previewLocal(fieldOf(asRecord(body.value), "path"), lang));
|
|
3535
3443
|
}
|
|
3536
|
-
function
|
|
3537
|
-
|
|
3444
|
+
async function previewPluginUpdate(ctx, req, res, lang) {
|
|
3445
|
+
if (refusedByLan(ctx, res, lang)) return;
|
|
3446
|
+
const control = controlOf(ctx, res, lang);
|
|
3538
3447
|
if (!control) return;
|
|
3539
|
-
if (!control.get(id)) return sendNotFound(res, id, lang);
|
|
3540
|
-
sendJson(res, 200, { runs: control.runs(id, RUNS_LIMIT) });
|
|
3541
|
-
}
|
|
3542
|
-
async function createSchedule(ctx, req, res, lang) {
|
|
3543
3448
|
const body = await readJsonBody(req);
|
|
3544
3449
|
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3545
|
-
|
|
3546
|
-
if (!parsed.ok) return sendError(res, 400, "bad-schedule", parsed.message);
|
|
3547
|
-
const control = schedulesOf(ctx, res, lang);
|
|
3548
|
-
if (!control) return;
|
|
3549
|
-
const saved = await control.create(parsed.value, {
|
|
3550
|
-
bypassAcknowledged: bypassAcknowledged(body.value)
|
|
3551
|
-
});
|
|
3552
|
-
sendJson(res, 200, toSaveResponse(control, saved));
|
|
3553
|
-
}
|
|
3554
|
-
async function patchSchedule(ctx, req, res, id, lang) {
|
|
3555
|
-
const control = schedulesOf(ctx, res, lang);
|
|
3556
|
-
if (!control) return;
|
|
3557
|
-
if (!control.get(id)) return sendNotFound(res, id, lang);
|
|
3558
|
-
const body = await readJsonBody(req);
|
|
3559
|
-
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3560
|
-
const parsed = parseUpdate2(body.value, lang);
|
|
3561
|
-
if (!parsed.ok) return sendError(res, 400, "bad-schedule", parsed.message);
|
|
3562
|
-
const { enabled, ...rest } = parsed.value;
|
|
3563
|
-
const saved = Object.keys(rest).length > 0 ? await control.update(id, rest, { bypassAcknowledged: bypassAcknowledged(body.value) }) : { issues: [] };
|
|
3564
|
-
if (saved.issues.length > 0) return sendJson(res, 200, toSaveResponse(control, saved));
|
|
3565
|
-
const registration = enabled === void 0 ? saved.registration : await control.setEnabled(id, enabled);
|
|
3566
|
-
const next = control.get(id) ?? saved.schedule;
|
|
3567
|
-
sendJson(
|
|
3568
|
-
res,
|
|
3569
|
-
200,
|
|
3570
|
-
toSaveResponse(control, {
|
|
3571
|
-
...next ? { schedule: next } : {},
|
|
3572
|
-
issues: [],
|
|
3573
|
-
...registration ? { registration } : {}
|
|
3574
|
-
})
|
|
3575
|
-
);
|
|
3576
|
-
}
|
|
3577
|
-
async function deleteSchedule(ctx, res, id, lang) {
|
|
3578
|
-
const control = schedulesOf(ctx, res, lang);
|
|
3579
|
-
if (!control) return;
|
|
3580
|
-
const removed = await control.remove(id);
|
|
3581
|
-
sendJson(res, 200, { removed });
|
|
3582
|
-
}
|
|
3583
|
-
async function runSchedule(ctx, res, id, lang) {
|
|
3584
|
-
const control = schedulesOf(ctx, res, lang);
|
|
3585
|
-
if (!control) return;
|
|
3586
|
-
if (!control.get(id)) return sendNotFound(res, id, lang);
|
|
3587
|
-
const result = await control.fire(id, { manual: true });
|
|
3588
|
-
const payload = {
|
|
3589
|
-
status: result.status,
|
|
3590
|
-
exitCode: result.exitCode,
|
|
3591
|
-
...result.run ? { run: result.run } : {},
|
|
3592
|
-
...result.detail === void 0 ? {} : { detail: result.detail }
|
|
3593
|
-
};
|
|
3594
|
-
sendJson(res, 200, payload);
|
|
3595
|
-
}
|
|
3596
|
-
async function fixSchedule(ctx, req, res, id, lang) {
|
|
3597
|
-
const control = schedulesOf(ctx, res, lang);
|
|
3598
|
-
if (!control) return;
|
|
3599
|
-
const def = control.get(id);
|
|
3600
|
-
if (!def) return sendNotFound(res, id, lang);
|
|
3601
|
-
const body = await readJsonBody(req);
|
|
3602
|
-
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3603
|
-
const parsed = parseFix(body.value, lang);
|
|
3604
|
-
if (!parsed.ok) return sendError(res, 400, "bad-schedule", parsed.message);
|
|
3605
|
-
const run = control.runs(id, RUNS_LIMIT).find((one) => one.id === parsed.value);
|
|
3606
|
-
if (!run) {
|
|
3607
|
-
return sendError(
|
|
3608
|
-
res,
|
|
3609
|
-
404,
|
|
3610
|
-
"not-found",
|
|
3611
|
-
t("web.schedule_run_unknown", { runId: parsed.value }, lang)
|
|
3612
|
-
);
|
|
3613
|
-
}
|
|
3614
|
-
const added = unfixedRules(def.allowRules, run.pendingApprovals);
|
|
3615
|
-
if (added.length === 0) {
|
|
3616
|
-
return sendJson(res, 200, {
|
|
3617
|
-
added: [],
|
|
3618
|
-
row: toWireRow(control, def)
|
|
3619
|
-
});
|
|
3620
|
-
}
|
|
3621
|
-
const saved = await control.update(id, { allowRules: [...def.allowRules, ...added] });
|
|
3622
|
-
const next = saved.schedule ?? def;
|
|
3623
|
-
sendJson(res, 200, {
|
|
3624
|
-
added,
|
|
3625
|
-
row: toWireRow(control, next)
|
|
3626
|
-
});
|
|
3627
|
-
}
|
|
3628
|
-
function getRecording(ctx, res, id, runId, lang) {
|
|
3629
|
-
const control = schedulesOf(ctx, res, lang);
|
|
3630
|
-
if (!control) return;
|
|
3631
|
-
if (!control.get(id)) return sendNotFound(res, id, lang);
|
|
3632
|
-
const run = control.runs(id, RUNS_LIMIT).find((one) => one.id === runId);
|
|
3633
|
-
if (!run) {
|
|
3634
|
-
return sendError(res, 404, "not-found", t("web.schedule_run_unknown", { runId }, lang));
|
|
3635
|
-
}
|
|
3636
|
-
sendJson(res, 200, toRecording(run));
|
|
3637
|
-
}
|
|
3638
|
-
function toRecording(run) {
|
|
3639
|
-
if (run.logPath === void 0) {
|
|
3640
|
-
return { runId: run.id, frames: [], missing: true, truncated: false };
|
|
3641
|
-
}
|
|
3642
|
-
const frames = coalesceRecording(readRecording(run.logPath));
|
|
3643
|
-
if (frames.length === 0) {
|
|
3644
|
-
return { runId: run.id, frames: [], missing: true, truncated: false };
|
|
3645
|
-
}
|
|
3646
|
-
const truncated = frames.length > MAX_RECORDING_FRAMES;
|
|
3647
|
-
return {
|
|
3648
|
-
runId: run.id,
|
|
3649
|
-
frames: truncated ? frames.slice(-MAX_RECORDING_FRAMES) : frames,
|
|
3650
|
-
missing: false,
|
|
3651
|
-
truncated
|
|
3652
|
-
};
|
|
3653
|
-
}
|
|
3654
|
-
function toSaveResponse(control, saved) {
|
|
3655
|
-
return {
|
|
3656
|
-
ok: saved.issues.length === 0,
|
|
3657
|
-
issues: saved.issues,
|
|
3658
|
-
...saved.schedule ? { row: toWireRow(control, saved.schedule) } : {},
|
|
3659
|
-
...saved.registration ? { registration: saved.registration } : {}
|
|
3660
|
-
};
|
|
3661
|
-
}
|
|
3662
|
-
function sendNotFound(res, id, lang) {
|
|
3663
|
-
sendError(res, 404, "unknown-schedule", t("web.schedule_unknown", { id }, lang));
|
|
3664
|
-
}
|
|
3665
|
-
// src/skill-import.ts
|
|
3666
|
-
function pathOf(payload) {
|
|
3667
|
-
return typeof payload["path"] === "string" ? payload["path"].trim() : "";
|
|
3668
|
-
}
|
|
3669
|
-
function toWireIssues2(issues) {
|
|
3670
|
-
return (issues ?? []).map((i) => ({ path: i.path, message: i.message }));
|
|
3671
|
-
}
|
|
3672
|
-
function toWireEntry(entry) {
|
|
3673
|
-
return {
|
|
3674
|
-
name: entry.name,
|
|
3675
|
-
category: entry.category,
|
|
3676
|
-
description: entry.description,
|
|
3677
|
-
files: entry.files,
|
|
3678
|
-
bytes: entry.bytes,
|
|
3679
|
-
conflict: entry.conflict ?? null
|
|
3680
|
-
};
|
|
3450
|
+
sendPreview(res, await control.previewUpdate(fieldOf(asRecord(body.value), "name"), lang));
|
|
3681
3451
|
}
|
|
3682
|
-
|
|
3683
|
-
const body = await readJsonBody(req);
|
|
3684
|
-
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3685
|
-
const source = pathOf(asRecord(body.value));
|
|
3686
|
-
const outcome = await ctx.runtime.skillWrite.preview(source, lang);
|
|
3452
|
+
function sendAction(res, control, outcome) {
|
|
3687
3453
|
if (!outcome.ok) {
|
|
3688
3454
|
return sendJson(res, 200, {
|
|
3689
3455
|
ok: false,
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
skills: [],
|
|
3693
|
-
issues: toWireIssues2(outcome.issues),
|
|
3694
|
-
token: "",
|
|
3456
|
+
entry: null,
|
|
3457
|
+
pendingRestart: control.pendingRestart,
|
|
3695
3458
|
detail: outcome.detail,
|
|
3696
3459
|
reason: outcome.reason
|
|
3697
3460
|
});
|
|
3698
3461
|
}
|
|
3699
|
-
const
|
|
3462
|
+
const fresh = "record" in outcome ? control.list().find((p) => p.name === outcome.record.name) : void 0;
|
|
3700
3463
|
sendJson(res, 200, {
|
|
3701
3464
|
ok: true,
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
issues: toWireIssues2(preview.issues),
|
|
3706
|
-
token: preview.token,
|
|
3707
|
-
detail: null,
|
|
3465
|
+
entry: fresh ? toWirePluginEntry(fresh, displayNamesOf(control)) : null,
|
|
3466
|
+
pendingRestart: control.pendingRestart,
|
|
3467
|
+
detail: "detail" in outcome ? outcome.detail : null,
|
|
3708
3468
|
reason: null
|
|
3709
3469
|
});
|
|
3710
3470
|
}
|
|
3711
|
-
async function
|
|
3471
|
+
async function installPluginFromMarket(ctx, req, res, lang) {
|
|
3472
|
+
if (refusedByLan(ctx, res, lang)) return;
|
|
3473
|
+
const control = controlOf(ctx, res, lang);
|
|
3474
|
+
if (!control) return;
|
|
3712
3475
|
const body = await readJsonBody(req);
|
|
3713
3476
|
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3714
3477
|
const payload = asRecord(body.value);
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
ok: false,
|
|
3721
|
-
imported: [],
|
|
3722
|
-
skipped: toWireIssues2(outcome.issues),
|
|
3723
|
-
detail: outcome.detail,
|
|
3724
|
-
reason: outcome.reason
|
|
3725
|
-
});
|
|
3726
|
-
}
|
|
3727
|
-
sendJson(res, 200, {
|
|
3728
|
-
ok: outcome.imported.length > 0,
|
|
3729
|
-
imported: [...outcome.imported],
|
|
3730
|
-
skipped: toWireIssues2(outcome.skipped),
|
|
3731
|
-
detail: null,
|
|
3732
|
-
reason: null
|
|
3733
|
-
});
|
|
3478
|
+
sendAction(
|
|
3479
|
+
res,
|
|
3480
|
+
control,
|
|
3481
|
+
await control.install(fieldOf(payload, "ref"), fieldOf(payload, "token"), lang)
|
|
3482
|
+
);
|
|
3734
3483
|
}
|
|
3735
|
-
|
|
3736
|
-
|
|
3484
|
+
async function installLocalPlugin(ctx, req, res, lang) {
|
|
3485
|
+
if (refusedByLan(ctx, res, lang)) return;
|
|
3486
|
+
const control = controlOf(ctx, res, lang);
|
|
3487
|
+
if (!control) return;
|
|
3737
3488
|
const body = await readJsonBody(req);
|
|
3738
3489
|
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3739
3490
|
const payload = asRecord(body.value);
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
name: "",
|
|
3746
|
-
path: null,
|
|
3747
|
-
detail: outcome.detail,
|
|
3748
|
-
reason: outcome.reason
|
|
3749
|
-
});
|
|
3750
|
-
}
|
|
3751
|
-
sendJson(res, 200, {
|
|
3752
|
-
ok: true,
|
|
3753
|
-
name: outcome.name,
|
|
3754
|
-
path: outcome.path,
|
|
3755
|
-
detail: null,
|
|
3756
|
-
reason: null
|
|
3757
|
-
});
|
|
3758
|
-
}
|
|
3759
|
-
function refusedByLan2(ctx, res, lang) {
|
|
3760
|
-
if (!ctx.lanExposed) return false;
|
|
3761
|
-
sendError(res, 403, "skill-stage-lan-exposed", t("web.skill_stage_lan_exposed", void 0, lang));
|
|
3762
|
-
return true;
|
|
3491
|
+
sendAction(
|
|
3492
|
+
res,
|
|
3493
|
+
control,
|
|
3494
|
+
await control.installLocal(fieldOf(payload, "path"), fieldOf(payload, "token"), lang)
|
|
3495
|
+
);
|
|
3763
3496
|
}
|
|
3764
|
-
async function
|
|
3765
|
-
if (
|
|
3497
|
+
async function stagePlugin(ctx, req, res, lang) {
|
|
3498
|
+
if (refusedByLan(ctx, res, lang)) return;
|
|
3499
|
+
const control = controlOf(ctx, res, lang);
|
|
3500
|
+
if (!control) return;
|
|
3501
|
+
const asJson = (req.headers["content-type"] ?? "").toLowerCase().includes("application/json");
|
|
3502
|
+
if (asJson) {
|
|
3503
|
+
const body = await readJsonBody(req, void 0, lang);
|
|
3504
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3505
|
+
return sendStage(
|
|
3506
|
+
res,
|
|
3507
|
+
await control.stage({ kind: "path", path: fieldOf(asRecord(body.value), "path") }, lang)
|
|
3508
|
+
);
|
|
3509
|
+
}
|
|
3766
3510
|
const raw = await readRawBody(req, PLUGIN_UPLOAD_BODY_LIMIT, lang);
|
|
3767
3511
|
if (!raw.ok) return sendError(res, raw.status, "bad-body", raw.message);
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
lang
|
|
3512
|
+
return sendStage(
|
|
3513
|
+
res,
|
|
3514
|
+
await control.stage({ kind: "bytes", name: nameParam2(req), data: raw.value }, lang)
|
|
3771
3515
|
);
|
|
3772
|
-
sendJson(res, 200, {
|
|
3773
|
-
ok: outcome.ok,
|
|
3774
|
-
path: outcome.ok ? outcome.path : null,
|
|
3775
|
-
detail: outcome.ok ? null : outcome.detail,
|
|
3776
|
-
reason: outcome.ok ? null : outcome.reason
|
|
3777
|
-
});
|
|
3778
3516
|
}
|
|
3779
|
-
function
|
|
3517
|
+
function nameParam2(req) {
|
|
3780
3518
|
try {
|
|
3781
3519
|
return new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("name") ?? "";
|
|
3782
3520
|
} catch {
|
|
3783
3521
|
return "";
|
|
3784
3522
|
}
|
|
3785
3523
|
}
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3524
|
+
function sendStage(res, outcome) {
|
|
3525
|
+
const payload = outcome.ok ? {
|
|
3526
|
+
ok: true,
|
|
3527
|
+
path: outcome.path,
|
|
3528
|
+
name: outcome.name,
|
|
3529
|
+
version: outcome.version,
|
|
3530
|
+
detail: null,
|
|
3531
|
+
reason: null
|
|
3532
|
+
} : {
|
|
3533
|
+
ok: false,
|
|
3534
|
+
path: null,
|
|
3535
|
+
name: null,
|
|
3536
|
+
version: null,
|
|
3537
|
+
detail: outcome.detail,
|
|
3538
|
+
reason: outcome.reason
|
|
3539
|
+
};
|
|
3540
|
+
sendJson(res, 200, payload);
|
|
3794
3541
|
}
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3542
|
+
async function updateInstalledPlugin(ctx, req, res, lang) {
|
|
3543
|
+
if (refusedByLan(ctx, res, lang)) return;
|
|
3544
|
+
const control = controlOf(ctx, res, lang);
|
|
3545
|
+
if (!control) return;
|
|
3546
|
+
const body = await readJsonBody(req);
|
|
3547
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3548
|
+
const payload = asRecord(body.value);
|
|
3549
|
+
sendAction(
|
|
3550
|
+
res,
|
|
3551
|
+
control,
|
|
3552
|
+
await control.update(fieldOf(payload, "name"), fieldOf(payload, "token"), lang)
|
|
3553
|
+
);
|
|
3554
|
+
}
|
|
3555
|
+
async function uninstallPluginByName(ctx, req, res, lang) {
|
|
3556
|
+
if (refusedByLan(ctx, res, lang)) return;
|
|
3557
|
+
const control = controlOf(ctx, res, lang);
|
|
3558
|
+
if (!control) return;
|
|
3559
|
+
const body = await readJsonBody(req);
|
|
3560
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3561
|
+
sendAction(res, control, await control.uninstall(fieldOf(asRecord(body.value), "name"), lang));
|
|
3562
|
+
}
|
|
3563
|
+
function sendMarket(res, outcome) {
|
|
3564
|
+
sendJson(res, 200, {
|
|
3565
|
+
ok: outcome.ok,
|
|
3566
|
+
marketplace: outcome.ok && outcome.marketplace ? toWireMarketplace(outcome.marketplace) : null,
|
|
3567
|
+
reason: outcome.ok ? null : outcome.reason,
|
|
3568
|
+
detail: outcome.ok ? null : outcome.detail
|
|
3569
|
+
});
|
|
3570
|
+
}
|
|
3571
|
+
async function addPluginMarketplace(ctx, req, res, lang) {
|
|
3572
|
+
if (refusedByLan(ctx, res, lang)) return;
|
|
3573
|
+
const control = controlOf(ctx, res, lang);
|
|
3574
|
+
if (!control) return;
|
|
3575
|
+
const body = await readJsonBody(req);
|
|
3576
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3577
|
+
sendMarket(res, await control.addMarketplace(fieldOf(asRecord(body.value), "source"), lang));
|
|
3578
|
+
}
|
|
3579
|
+
async function removePluginMarketplace(ctx, req, res, lang) {
|
|
3580
|
+
if (refusedByLan(ctx, res, lang)) return;
|
|
3581
|
+
const control = controlOf(ctx, res, lang);
|
|
3582
|
+
if (!control) return;
|
|
3583
|
+
const body = await readJsonBody(req);
|
|
3584
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3585
|
+
sendMarket(res, await control.removeMarketplace(fieldOf(asRecord(body.value), "name"), lang));
|
|
3586
|
+
}
|
|
3587
|
+
function previewPluginUpload(ctx, req, res, lang) {
|
|
3588
|
+
return withUploadBody(ctx, req, res, lang, (control, payload) => {
|
|
3589
|
+
sendUploadPreview(res, control.previewUpload(fieldOf(payload, "path"), lang));
|
|
3590
|
+
});
|
|
3591
|
+
}
|
|
3592
|
+
function uploadPlugin(ctx, req, res, lang) {
|
|
3593
|
+
return withUploadBody(ctx, req, res, lang, async (control, payload) => {
|
|
3594
|
+
sendUpload(
|
|
3595
|
+
res,
|
|
3596
|
+
await control.upload(
|
|
3597
|
+
fieldOf(payload, "path"),
|
|
3598
|
+
fieldOf(payload, "token"),
|
|
3599
|
+
uploadFormOf(payload),
|
|
3600
|
+
lang
|
|
3601
|
+
)
|
|
3602
|
+
);
|
|
3603
|
+
});
|
|
3604
|
+
}
|
|
3605
|
+
async function withUploadBody(ctx, req, res, lang, run) {
|
|
3606
|
+
if (refusedByLan(ctx, res, lang)) return;
|
|
3607
|
+
const control = controlOf(ctx, res, lang);
|
|
3608
|
+
if (!control) return;
|
|
3609
|
+
const body = await readJsonBody(req);
|
|
3610
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3611
|
+
await run(control, asRecord(body.value));
|
|
3612
|
+
}
|
|
3613
|
+
function sendUploadPreview(res, outcome) {
|
|
3614
|
+
if (!outcome.ok) {
|
|
3615
|
+
return sendJson(res, 200, {
|
|
3616
|
+
ok: false,
|
|
3617
|
+
plan: null,
|
|
3618
|
+
token: null,
|
|
3619
|
+
prefill: null,
|
|
3620
|
+
fromManifest: null,
|
|
3621
|
+
reason: outcome.reason,
|
|
3622
|
+
detail: outcome.detail
|
|
3805
3623
|
});
|
|
3806
|
-
res.flushHeaders();
|
|
3807
|
-
this.heartbeat = setInterval(() => this.ping(), heartbeatMs);
|
|
3808
|
-
this.heartbeat.unref();
|
|
3809
3624
|
}
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3625
|
+
const { manifest, files, totalBytes, suspicious, skippedLinks } = outcome.plan;
|
|
3626
|
+
sendJson(res, 200, {
|
|
3627
|
+
ok: true,
|
|
3628
|
+
plan: {
|
|
3629
|
+
name: manifest.name,
|
|
3630
|
+
version: manifest.version,
|
|
3631
|
+
description: manifest.description ?? "",
|
|
3632
|
+
author: manifest.author?.name ?? null,
|
|
3633
|
+
files: files.map((f) => ({ path: f.path, bytes: f.bytes })),
|
|
3634
|
+
totalBytes,
|
|
3635
|
+
suspicious: [...suspicious],
|
|
3636
|
+
skippedLinks: [...skippedLinks]
|
|
3637
|
+
},
|
|
3638
|
+
token: outcome.token,
|
|
3639
|
+
prefill: outcome.prefill,
|
|
3640
|
+
fromManifest: [...outcome.fromManifest],
|
|
3641
|
+
reason: null,
|
|
3642
|
+
detail: null
|
|
3643
|
+
});
|
|
3644
|
+
}
|
|
3645
|
+
function sendUpload(res, outcome) {
|
|
3646
|
+
sendJson(res, 200, {
|
|
3647
|
+
ok: outcome.ok,
|
|
3648
|
+
detail: outcome.detail,
|
|
3649
|
+
uploaded: outcome.ok ? { name: outcome.name, version: outcome.version, bytes: outcome.bytes } : null,
|
|
3650
|
+
reason: outcome.ok ? null : outcome.reason
|
|
3651
|
+
});
|
|
3652
|
+
}
|
|
3653
|
+
async function refreshPlugins(ctx, res, lang) {
|
|
3654
|
+
if (refusedByLan(ctx, res, lang)) return;
|
|
3655
|
+
const control = controlOf(ctx, res, lang);
|
|
3656
|
+
if (!control) return;
|
|
3657
|
+
const outcome = await control.refresh(lang);
|
|
3658
|
+
if (!outcome.ok) {
|
|
3659
|
+
return sendJson(res, 200, {
|
|
3660
|
+
ok: false,
|
|
3661
|
+
plugins: null,
|
|
3662
|
+
reason: outcome.reason,
|
|
3663
|
+
detail: outcome.detail
|
|
3664
|
+
});
|
|
3820
3665
|
}
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3666
|
+
sendJson(res, 200, {
|
|
3667
|
+
ok: true,
|
|
3668
|
+
plugins: pluginsPayload(control),
|
|
3669
|
+
reason: null,
|
|
3670
|
+
detail: outcome.detail === "" ? null : outcome.detail
|
|
3671
|
+
});
|
|
3672
|
+
}
|
|
3673
|
+
function toWireProvider(option, lang) {
|
|
3674
|
+
return {
|
|
3675
|
+
type: option.type,
|
|
3676
|
+
label: providerLabel(option, lang),
|
|
3677
|
+
envVar: option.envVar,
|
|
3678
|
+
hasKey: option.hasKey,
|
|
3679
|
+
keyHint: option.keyHint,
|
|
3680
|
+
current: option.current
|
|
3681
|
+
};
|
|
3682
|
+
}
|
|
3683
|
+
function toWireSuggestions(view) {
|
|
3684
|
+
return {
|
|
3685
|
+
provider: view.provider,
|
|
3686
|
+
models: view.models,
|
|
3687
|
+
source: view.source,
|
|
3688
|
+
status: view.status
|
|
3689
|
+
};
|
|
3690
|
+
}
|
|
3691
|
+
function listProviders(ctx, res, lang) {
|
|
3692
|
+
sendJson(res, 200, {
|
|
3693
|
+
providers: ctx.runtime.modelCatalog.providers().map((o) => toWireProvider(o, lang))
|
|
3694
|
+
});
|
|
3695
|
+
}
|
|
3696
|
+
async function discoverProviderModels(ctx, req, res, type, lang) {
|
|
3697
|
+
if (!isProviderType(type)) {
|
|
3698
|
+
return sendError(res, 404, "unknown-provider", t("web.provider_unknown", { name: type }, lang));
|
|
3699
|
+
}
|
|
3700
|
+
const body = await readJsonBody(req);
|
|
3701
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3702
|
+
const refresh = asRecord(body.value).refresh === true;
|
|
3703
|
+
const suggestions = await ctx.runtime.modelCatalog.discover(type, { refresh });
|
|
3704
|
+
sendJson(res, 200, {
|
|
3705
|
+
suggestions: toWireSuggestions(suggestions)
|
|
3706
|
+
});
|
|
3707
|
+
}
|
|
3708
|
+
async function setProviderKey(ctx, req, res, type, lang) {
|
|
3709
|
+
if (!isProviderType(type)) {
|
|
3710
|
+
return sendError(res, 404, "unknown-provider", t("web.provider_unknown", { name: type }, lang));
|
|
3711
|
+
}
|
|
3712
|
+
const envVar = apiKeyEnvVar(type);
|
|
3713
|
+
if (envVar === null) {
|
|
3714
|
+
return sendError(
|
|
3715
|
+
res,
|
|
3716
|
+
400,
|
|
3717
|
+
"key-not-applicable",
|
|
3718
|
+
t("web.provider_key_not_applicable", { name: type }, lang)
|
|
3719
|
+
);
|
|
3720
|
+
}
|
|
3721
|
+
const body = await readJsonBody(req);
|
|
3722
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3723
|
+
const raw = asRecord(body.value)["apiKey"];
|
|
3724
|
+
const apiKey = typeof raw === "string" ? raw.trim() : "";
|
|
3725
|
+
if (apiKey === "") {
|
|
3726
|
+
return sendError(res, 400, "empty-key", t("web.provider_key_empty", void 0, lang));
|
|
3727
|
+
}
|
|
3728
|
+
const written = await ctx.runtime.modelCatalog.setKey(type, apiKey);
|
|
3729
|
+
sendJson(res, 200, {
|
|
3730
|
+
provider: type,
|
|
3731
|
+
hasKey: true,
|
|
3732
|
+
keyHint: written.keyHint,
|
|
3733
|
+
backend: written.backend,
|
|
3734
|
+
encrypted: written.encrypted,
|
|
3735
|
+
envPath: written.envPath
|
|
3736
|
+
});
|
|
3737
|
+
}
|
|
3738
|
+
async function setDefaultModel(ctx, req, res, lang) {
|
|
3739
|
+
if (ctx.runtime.providerInfo !== null) {
|
|
3740
|
+
return sendError(
|
|
3741
|
+
res,
|
|
3742
|
+
409,
|
|
3743
|
+
"provider-available",
|
|
3744
|
+
t("web.model_default_has_provider", void 0, lang)
|
|
3745
|
+
);
|
|
3746
|
+
}
|
|
3747
|
+
const body = await readJsonBody(req, void 0, lang);
|
|
3748
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3749
|
+
const raw = asRecord(body.value)["model"];
|
|
3750
|
+
const model = typeof raw === "string" ? raw.trim() : "";
|
|
3751
|
+
if (model === "") {
|
|
3752
|
+
return sendError(res, 400, "bad-request", t("web.model_default_empty", void 0, lang));
|
|
3753
|
+
}
|
|
3754
|
+
const report = ctx.runtime.settings.write({ key: "model", layer: "user", value: model });
|
|
3755
|
+
if (!report.ok) {
|
|
3756
|
+
const failure = settingWriteFailure(report);
|
|
3757
|
+
return sendError(
|
|
3758
|
+
res,
|
|
3759
|
+
failure.status,
|
|
3760
|
+
failure.code,
|
|
3761
|
+
t(failure.key, { path: failure.path }, lang)
|
|
3762
|
+
);
|
|
3763
|
+
}
|
|
3764
|
+
sendJson(res, 200, toWireSettingsWrite(report));
|
|
3765
|
+
}
|
|
3766
|
+
async function addRole(ctx, req, res, lang) {
|
|
3767
|
+
if (ctx.lanExposed) {
|
|
3768
|
+
return sendError(
|
|
3769
|
+
res,
|
|
3770
|
+
403,
|
|
3771
|
+
"role-add-lan-exposed",
|
|
3772
|
+
t("web.role_add_lan_exposed", void 0, lang)
|
|
3773
|
+
);
|
|
3774
|
+
}
|
|
3775
|
+
const body = await readJsonBody(req);
|
|
3776
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3777
|
+
const payload = asRecord(body.value);
|
|
3778
|
+
const outcome = ctx.runtime.roleWrite.add(
|
|
3779
|
+
{
|
|
3780
|
+
name: typeof payload["name"] === "string" ? payload["name"].trim() : "",
|
|
3781
|
+
description: typeof payload["description"] === "string" ? payload["description"].trim() : "",
|
|
3782
|
+
...typeof payload["prompt"] === "string" ? { prompt: payload["prompt"] } : {},
|
|
3783
|
+
...Array.isArray(payload["tools"]) ? { tools: stringList2(payload["tools"]) } : {},
|
|
3784
|
+
...Array.isArray(payload["skills"]) ? { skills: stringList2(payload["skills"]) } : {},
|
|
3785
|
+
...typeof payload["maxTurns"] === "number" ? { maxTurns: payload["maxTurns"] } : {}
|
|
3786
|
+
},
|
|
3787
|
+
lang
|
|
3788
|
+
);
|
|
3789
|
+
if (!outcome.ok) return sendError(res, ...addFailure2(outcome.reason, outcome.detail, lang));
|
|
3790
|
+
sendJson(res, 200, {
|
|
3791
|
+
role: toWireRole(outcome.role, ctx.runtime.agentRoleNotices),
|
|
3792
|
+
path: outcome.path
|
|
3793
|
+
});
|
|
3794
|
+
}
|
|
3795
|
+
function addFailure2(reason, detail, lang) {
|
|
3796
|
+
if (reason === "invalid-name") return [400, "bad-role-name", detail];
|
|
3797
|
+
if (reason === "invalid-field")
|
|
3798
|
+
return [400, "bad-role-field", t("web.role_add_bad_field", { detail }, lang)];
|
|
3799
|
+
if (reason === "unknown-tool") return [400, "bad-role-tools", detail];
|
|
3800
|
+
if (reason === "duplicate") return [409, "role-name-taken", detail];
|
|
3801
|
+
return [409, "role-unwritable", detail];
|
|
3802
|
+
}
|
|
3803
|
+
function stringList2(value) {
|
|
3804
|
+
return value.filter((v) => typeof v === "string");
|
|
3805
|
+
}
|
|
3806
|
+
async function removeRole(ctx, req, res, lang) {
|
|
3807
|
+
if (ctx.lanExposed) {
|
|
3808
|
+
return sendError(
|
|
3809
|
+
res,
|
|
3810
|
+
403,
|
|
3811
|
+
"role-remove-lan-exposed",
|
|
3812
|
+
t("web.role_remove_lan_exposed", void 0, lang)
|
|
3813
|
+
);
|
|
3814
|
+
}
|
|
3815
|
+
const body = await readJsonBody(req);
|
|
3816
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
3817
|
+
const payload = asRecord(body.value);
|
|
3818
|
+
const name = typeof payload["name"] === "string" ? payload["name"].trim() : "";
|
|
3819
|
+
const outcome = ctx.runtime.roleWrite.remove(name, lang);
|
|
3820
|
+
if (!outcome.ok) {
|
|
3821
|
+
return sendJson(res, 200, {
|
|
3822
|
+
ok: false,
|
|
3823
|
+
name: "",
|
|
3824
|
+
path: null,
|
|
3825
|
+
takenOverBy: null,
|
|
3826
|
+
detail: outcome.detail,
|
|
3827
|
+
reason: outcome.reason
|
|
3828
|
+
});
|
|
3829
|
+
}
|
|
3830
|
+
sendJson(res, 200, {
|
|
3831
|
+
ok: true,
|
|
3832
|
+
name: outcome.name,
|
|
3833
|
+
path: outcome.path,
|
|
3834
|
+
takenOverBy: outcome.takenOverBy,
|
|
3835
|
+
detail: null,
|
|
3836
|
+
reason: null
|
|
3837
|
+
});
|
|
3838
|
+
}
|
|
3839
|
+
function stringList3(value) {
|
|
3840
|
+
if (!Array.isArray(value)) return null;
|
|
3841
|
+
const out = [];
|
|
3842
|
+
for (const item of value) {
|
|
3843
|
+
if (typeof item !== "string" || item.trim().length === 0) return null;
|
|
3844
|
+
out.push(item);
|
|
3845
|
+
}
|
|
3846
|
+
return out;
|
|
3847
|
+
}
|
|
3848
|
+
function operationList(value) {
|
|
3849
|
+
if (!Array.isArray(value)) return null;
|
|
3850
|
+
const out = [];
|
|
3851
|
+
for (const item of value) {
|
|
3852
|
+
if (typeof item !== "string" || !isOperationType(item)) return null;
|
|
3853
|
+
out.push(item);
|
|
3854
|
+
}
|
|
3855
|
+
return out;
|
|
3856
|
+
}
|
|
3857
|
+
function finite(value) {
|
|
3858
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
3859
|
+
}
|
|
3860
|
+
function parseTrigger(value) {
|
|
3861
|
+
const raw = asRecord(value);
|
|
3862
|
+
const kind = raw["kind"];
|
|
3863
|
+
const at = raw["at"];
|
|
3864
|
+
if (kind === "interval") {
|
|
3865
|
+
const every = finite(raw["everyMinutes"]);
|
|
3866
|
+
return every === null ? null : { kind: "interval", everyMinutes: every };
|
|
3867
|
+
}
|
|
3868
|
+
if (typeof at !== "string") return null;
|
|
3869
|
+
if (kind === "once") {
|
|
3870
|
+
const date = raw["date"];
|
|
3871
|
+
return typeof date === "string" ? { kind: "once", date, at } : null;
|
|
3872
|
+
}
|
|
3873
|
+
if (kind !== "cron") return null;
|
|
3874
|
+
const cycle = raw["cycle"];
|
|
3875
|
+
if (cycle === "daily") return { kind: "cron", cycle: "daily", at };
|
|
3876
|
+
if (cycle === "weekly") {
|
|
3877
|
+
const days = weekdayList(raw["weekdays"]);
|
|
3878
|
+
return days === null ? null : { kind: "cron", cycle: "weekly", at, weekdays: days };
|
|
3879
|
+
}
|
|
3880
|
+
if (cycle === "monthly") {
|
|
3881
|
+
const days = numberList(raw["days"]);
|
|
3882
|
+
return days === null ? null : { kind: "cron", cycle: "monthly", at, days };
|
|
3883
|
+
}
|
|
3884
|
+
return null;
|
|
3885
|
+
}
|
|
3886
|
+
function weekdayList(value) {
|
|
3887
|
+
if (!Array.isArray(value)) return null;
|
|
3888
|
+
const out = [];
|
|
3889
|
+
for (const item of value) {
|
|
3890
|
+
if (typeof item !== "number" || !Number.isInteger(item) || item < 0 || item > 6) return null;
|
|
3891
|
+
out.push(item);
|
|
3892
|
+
}
|
|
3893
|
+
return out;
|
|
3894
|
+
}
|
|
3895
|
+
function numberList(value) {
|
|
3896
|
+
if (!Array.isArray(value)) return null;
|
|
3897
|
+
const out = [];
|
|
3898
|
+
for (const item of value) {
|
|
3899
|
+
if (typeof item !== "number" || !Number.isInteger(item)) return null;
|
|
3900
|
+
out.push(item);
|
|
3901
|
+
}
|
|
3902
|
+
return out;
|
|
3903
|
+
}
|
|
3904
|
+
var CLEARABLE = ["model", "role", "workDir", "startDate", "endDate"];
|
|
3905
|
+
function optionals(raw, allowClear, lang) {
|
|
3906
|
+
const out = {};
|
|
3907
|
+
const bad = (field) => ({
|
|
3908
|
+
ok: false,
|
|
3909
|
+
message: t("web.schedule_bad_field", { field }, lang)
|
|
3910
|
+
});
|
|
3911
|
+
for (const key of ["name", "prompt"]) {
|
|
3912
|
+
const value = raw[key];
|
|
3913
|
+
if (value === void 0) continue;
|
|
3914
|
+
if (typeof value !== "string") return bad(key);
|
|
3915
|
+
out[key] = value;
|
|
3916
|
+
}
|
|
3917
|
+
for (const key of CLEARABLE) {
|
|
3918
|
+
const value = raw[key];
|
|
3919
|
+
if (value === void 0) continue;
|
|
3920
|
+
if (value === null) {
|
|
3921
|
+
if (!allowClear) return bad(key);
|
|
3922
|
+
out[key] = null;
|
|
3923
|
+
continue;
|
|
3924
|
+
}
|
|
3925
|
+
if (typeof value !== "string") return bad(key);
|
|
3926
|
+
out[key] = value;
|
|
3927
|
+
}
|
|
3928
|
+
for (const key of ["maxTurns", "maxBudgetUsd", "timeoutMs"]) {
|
|
3929
|
+
const value = raw[key];
|
|
3930
|
+
if (value === void 0) continue;
|
|
3931
|
+
const num = finite(value);
|
|
3932
|
+
if (num === null)
|
|
3933
|
+
return { ok: false, message: t("web.schedule_bad_field", { field: key }, lang) };
|
|
3934
|
+
out[key] = num;
|
|
3935
|
+
}
|
|
3936
|
+
for (const key of ["allowTools", "allowRules"]) {
|
|
3937
|
+
const value = raw[key];
|
|
3938
|
+
if (value === void 0) continue;
|
|
3939
|
+
const list = stringList3(value);
|
|
3940
|
+
if (list === null)
|
|
3941
|
+
return { ok: false, message: t("web.schedule_bad_field", { field: key }, lang) };
|
|
3942
|
+
out[key] = list;
|
|
3943
|
+
}
|
|
3944
|
+
if (raw["allowOperations"] !== void 0) {
|
|
3945
|
+
const ops = operationList(raw["allowOperations"]);
|
|
3946
|
+
if (ops === null) {
|
|
3947
|
+
return {
|
|
3948
|
+
ok: false,
|
|
3949
|
+
message: t("web.schedule_bad_operation", { types: OPERATION_TYPES.join("/") }, lang)
|
|
3950
|
+
};
|
|
3951
|
+
}
|
|
3952
|
+
out.allowOperations = ops;
|
|
3953
|
+
}
|
|
3954
|
+
for (const key of ["enabled", "showInSessionList"]) {
|
|
3955
|
+
const value = raw[key];
|
|
3956
|
+
if (value === void 0) continue;
|
|
3957
|
+
if (typeof value !== "boolean") {
|
|
3958
|
+
return { ok: false, message: t("web.schedule_bad_field", { field: key }, lang) };
|
|
3959
|
+
}
|
|
3960
|
+
out[key] = value;
|
|
3961
|
+
}
|
|
3962
|
+
if (raw["permission"] !== void 0) {
|
|
3963
|
+
const level = raw["permission"];
|
|
3964
|
+
if (typeof level !== "string" || !isPermissionLevel(level)) {
|
|
3965
|
+
return { ok: false, message: t("web.schedule_bad_permission", void 0, lang) };
|
|
3966
|
+
}
|
|
3967
|
+
out.permission = level;
|
|
3968
|
+
}
|
|
3969
|
+
if (raw["trigger"] !== void 0) {
|
|
3970
|
+
const trigger = parseTrigger(raw["trigger"]);
|
|
3971
|
+
if (trigger === null)
|
|
3972
|
+
return { ok: false, message: t("web.schedule_bad_trigger", void 0, lang) };
|
|
3973
|
+
out.trigger = trigger;
|
|
3974
|
+
}
|
|
3975
|
+
return { ok: true, value: out };
|
|
3976
|
+
}
|
|
3977
|
+
function bypassAcknowledged(body) {
|
|
3978
|
+
return asRecord(body)["bypassAcknowledged"] === true;
|
|
3979
|
+
}
|
|
3980
|
+
function parseCreate2(body, lang) {
|
|
3981
|
+
const raw = asRecord(body);
|
|
3982
|
+
const name = raw["name"];
|
|
3983
|
+
const prompt = raw["prompt"];
|
|
3984
|
+
const permission = raw["permission"];
|
|
3985
|
+
const budget = finite(raw["maxBudgetUsd"]);
|
|
3986
|
+
if (typeof name !== "string" || typeof prompt !== "string") {
|
|
3987
|
+
return { ok: false, message: t("web.schedule_create_incomplete", void 0, lang) };
|
|
3824
3988
|
}
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
this.closed = true;
|
|
3828
|
-
clearInterval(this.heartbeat);
|
|
3829
|
-
this.res.end();
|
|
3989
|
+
if (typeof permission !== "string" || !isPermissionLevel(permission)) {
|
|
3990
|
+
return { ok: false, message: t("web.schedule_bad_permission", void 0, lang) };
|
|
3830
3991
|
}
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
const since = Math.max(0, info.outputBytes - TASK_TAIL_BYTES);
|
|
3839
|
-
const chunk = registry.output(sessionId, info.id, since);
|
|
3840
|
-
if (!chunk) return { info, output: "", omittedBytes: info.outputBytes };
|
|
3992
|
+
const trigger = parseTrigger(raw["trigger"]);
|
|
3993
|
+
if (trigger === null)
|
|
3994
|
+
return { ok: false, message: t("web.schedule_bad_trigger", void 0, lang) };
|
|
3995
|
+
if (budget === null)
|
|
3996
|
+
return { ok: false, message: t("web.schedule_budget_required", void 0, lang) };
|
|
3997
|
+
const rest = optionals(raw, false, lang);
|
|
3998
|
+
if (!rest.ok) return rest;
|
|
3841
3999
|
return {
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
omittedBytes: Math.max(0, chunk.nextCursor - chunk.output.length)
|
|
4000
|
+
ok: true,
|
|
4001
|
+
value: { ...rest.value, name, prompt, permission, trigger, maxBudgetUsd: budget }
|
|
3845
4002
|
};
|
|
3846
4003
|
}
|
|
3847
|
-
function
|
|
3848
|
-
return
|
|
4004
|
+
function parseUpdate2(body, lang) {
|
|
4005
|
+
return optionals(asRecord(body), true, lang);
|
|
3849
4006
|
}
|
|
3850
|
-
function
|
|
3851
|
-
|
|
3852
|
-
|
|
4007
|
+
function parseFix(body, lang) {
|
|
4008
|
+
const runId = asRecord(body)["runId"];
|
|
4009
|
+
if (typeof runId !== "string" || runId.trim().length === 0) {
|
|
4010
|
+
return { ok: false, message: t("web.schedule_fix_needs_run", void 0, lang) };
|
|
3853
4011
|
}
|
|
3854
|
-
|
|
4012
|
+
return { ok: true, value: runId };
|
|
3855
4013
|
}
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
4014
|
+
// src/schedule/handlers.ts
|
|
4015
|
+
var RECENT_RUNS_LIMIT = 100;
|
|
4016
|
+
var RUNS_LIMIT = 20;
|
|
4017
|
+
var MAX_RECORDING_FRAMES = 4e3;
|
|
4018
|
+
function schedulesOf(ctx, res, lang) {
|
|
4019
|
+
const control = ctx.runtime.schedules;
|
|
4020
|
+
if (!control.storeAvailable) {
|
|
4021
|
+
sendError(res, 503, "no-schedule-store", t("web.schedule_store_unavailable", void 0, lang));
|
|
4022
|
+
return null;
|
|
4023
|
+
}
|
|
4024
|
+
return control;
|
|
3859
4025
|
}
|
|
3860
|
-
function
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
const path = resolve(raw);
|
|
3866
|
-
if (seen.has(path)) return;
|
|
3867
|
-
seen.add(path);
|
|
3868
|
-
entries.push({
|
|
3869
|
-
name: displayName2(path),
|
|
3870
|
-
path,
|
|
3871
|
-
known: known.has(path),
|
|
3872
|
-
readable: canList(path),
|
|
3873
|
-
group
|
|
3874
|
-
});
|
|
4026
|
+
function toWireRow(control, def) {
|
|
4027
|
+
return {
|
|
4028
|
+
schedule: def,
|
|
4029
|
+
nextRunAt: def.enabled ? nextRunAt(def.trigger, Date.now(), def.startDate, def.endDate) : null,
|
|
4030
|
+
allowlistApplies: control.needsAllowlist(def.permission)
|
|
3875
4031
|
};
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
for (const one of workspaces.known()) push(one.root, "known");
|
|
4032
|
+
}
|
|
4033
|
+
function defaultsOf(ctx) {
|
|
3879
4034
|
return {
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
4035
|
+
maxTurns: ctx.runtime.config.maxTurns,
|
|
4036
|
+
timeoutMs: SCHEDULE_DEFAULTS.timeoutMs,
|
|
4037
|
+
maxBudgetUsd: SCHEDULE_FORM_BUDGET_USD,
|
|
4038
|
+
allowTools: SCHEDULE_DEFAULT_ALLOWLIST.allowTools,
|
|
4039
|
+
allowOperations: SCHEDULE_DEFAULT_ALLOWLIST.allowOperations,
|
|
4040
|
+
allowRules: SCHEDULE_DEFAULT_ALLOWLIST.allowRules
|
|
3885
4041
|
};
|
|
3886
4042
|
}
|
|
3887
|
-
function
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
const
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
4043
|
+
async function listSchedules(ctx, res, lang) {
|
|
4044
|
+
const control = schedulesOf(ctx, res, lang);
|
|
4045
|
+
if (!control) return;
|
|
4046
|
+
const capability = await control.capability();
|
|
4047
|
+
const payload = {
|
|
4048
|
+
schedules: control.list().map((def) => toWireRow(control, def)),
|
|
4049
|
+
capability: { backend: capability.backend, canRegister: capability.canRegister },
|
|
4050
|
+
defaults: defaultsOf(ctx),
|
|
4051
|
+
recentRuns: control.recentRuns(RECENT_RUNS_LIMIT)
|
|
4052
|
+
};
|
|
4053
|
+
sendJson(res, 200, payload);
|
|
4054
|
+
}
|
|
4055
|
+
function listPending(ctx, res, lang) {
|
|
4056
|
+
const control = schedulesOf(ctx, res, lang);
|
|
4057
|
+
if (!control) return;
|
|
4058
|
+
const items = collectPendingApprovals(control.list(), control.recentRuns(RECENT_RUNS_LIMIT));
|
|
4059
|
+
sendJson(res, 200, { items });
|
|
4060
|
+
}
|
|
4061
|
+
function listRunning(ctx, res, lang) {
|
|
4062
|
+
const control = schedulesOf(ctx, res, lang);
|
|
4063
|
+
if (!control) return;
|
|
4064
|
+
const items = control.running().map(({ scheduleId, startedAt }) => ({ scheduleId, startedAt }));
|
|
4065
|
+
sendJson(res, 200, { items });
|
|
4066
|
+
}
|
|
4067
|
+
function getSchedule(ctx, res, id, lang) {
|
|
4068
|
+
const control = schedulesOf(ctx, res, lang);
|
|
4069
|
+
if (!control) return;
|
|
4070
|
+
const def = control.get(id);
|
|
4071
|
+
if (!def) return sendNotFound(res, id, lang);
|
|
4072
|
+
sendJson(res, 200, { row: toWireRow(control, def) });
|
|
4073
|
+
}
|
|
4074
|
+
function listRuns(ctx, res, id, lang) {
|
|
4075
|
+
const control = schedulesOf(ctx, res, lang);
|
|
4076
|
+
if (!control) return;
|
|
4077
|
+
if (!control.get(id)) return sendNotFound(res, id, lang);
|
|
4078
|
+
sendJson(res, 200, { runs: control.runs(id, RUNS_LIMIT) });
|
|
4079
|
+
}
|
|
4080
|
+
async function createSchedule(ctx, req, res, lang) {
|
|
4081
|
+
const body = await readJsonBody(req);
|
|
4082
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
4083
|
+
const parsed = parseCreate2(body.value, lang);
|
|
4084
|
+
if (!parsed.ok) return sendError(res, 400, "bad-schedule", parsed.message);
|
|
4085
|
+
const control = schedulesOf(ctx, res, lang);
|
|
4086
|
+
if (!control) return;
|
|
4087
|
+
const saved = await control.create(parsed.value, {
|
|
4088
|
+
bypassAcknowledged: bypassAcknowledged(body.value)
|
|
4089
|
+
});
|
|
4090
|
+
sendJson(res, 200, toSaveResponse(control, saved));
|
|
4091
|
+
}
|
|
4092
|
+
async function patchSchedule(ctx, req, res, id, lang) {
|
|
4093
|
+
const control = schedulesOf(ctx, res, lang);
|
|
4094
|
+
if (!control) return;
|
|
4095
|
+
if (!control.get(id)) return sendNotFound(res, id, lang);
|
|
4096
|
+
const body = await readJsonBody(req);
|
|
4097
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
4098
|
+
const parsed = parseUpdate2(body.value, lang);
|
|
4099
|
+
if (!parsed.ok) return sendError(res, 400, "bad-schedule", parsed.message);
|
|
4100
|
+
const { enabled, ...rest } = parsed.value;
|
|
4101
|
+
const saved = Object.keys(rest).length > 0 ? await control.update(id, rest, { bypassAcknowledged: bypassAcknowledged(body.value) }) : { issues: [] };
|
|
4102
|
+
if (saved.issues.length > 0) return sendJson(res, 200, toSaveResponse(control, saved));
|
|
4103
|
+
const registration = enabled === void 0 ? saved.registration : await control.setEnabled(id, enabled);
|
|
4104
|
+
const next = control.get(id) ?? saved.schedule;
|
|
4105
|
+
sendJson(
|
|
4106
|
+
res,
|
|
4107
|
+
200,
|
|
4108
|
+
toSaveResponse(control, {
|
|
4109
|
+
...next ? { schedule: next } : {},
|
|
4110
|
+
issues: [],
|
|
4111
|
+
...registration ? { registration } : {}
|
|
4112
|
+
})
|
|
4113
|
+
);
|
|
4114
|
+
}
|
|
4115
|
+
async function deleteSchedule(ctx, res, id, lang) {
|
|
4116
|
+
const control = schedulesOf(ctx, res, lang);
|
|
4117
|
+
if (!control) return;
|
|
4118
|
+
const removed = await control.remove(id);
|
|
4119
|
+
sendJson(res, 200, { removed });
|
|
4120
|
+
}
|
|
4121
|
+
async function runSchedule(ctx, res, id, lang) {
|
|
4122
|
+
const control = schedulesOf(ctx, res, lang);
|
|
4123
|
+
if (!control) return;
|
|
4124
|
+
if (!control.get(id)) return sendNotFound(res, id, lang);
|
|
4125
|
+
const result = await control.fire(id, { manual: true });
|
|
4126
|
+
const payload = {
|
|
4127
|
+
status: result.status,
|
|
4128
|
+
exitCode: result.exitCode,
|
|
4129
|
+
...result.run ? { run: result.run } : {},
|
|
4130
|
+
...result.detail === void 0 ? {} : { detail: result.detail }
|
|
4131
|
+
};
|
|
4132
|
+
sendJson(res, 200, payload);
|
|
4133
|
+
}
|
|
4134
|
+
async function fixSchedule(ctx, req, res, id, lang) {
|
|
4135
|
+
const control = schedulesOf(ctx, res, lang);
|
|
4136
|
+
if (!control) return;
|
|
4137
|
+
const def = control.get(id);
|
|
4138
|
+
if (!def) return sendNotFound(res, id, lang);
|
|
4139
|
+
const body = await readJsonBody(req);
|
|
4140
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
4141
|
+
const parsed = parseFix(body.value, lang);
|
|
4142
|
+
if (!parsed.ok) return sendError(res, 400, "bad-schedule", parsed.message);
|
|
4143
|
+
const run = control.runs(id, RUNS_LIMIT).find((one) => one.id === parsed.value);
|
|
4144
|
+
if (!run) {
|
|
4145
|
+
return sendError(
|
|
4146
|
+
res,
|
|
4147
|
+
404,
|
|
4148
|
+
"not-found",
|
|
4149
|
+
t("web.schedule_run_unknown", { runId: parsed.value }, lang)
|
|
4150
|
+
);
|
|
3897
4151
|
}
|
|
3898
|
-
const
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
if (!dirent.isDirectory()) continue;
|
|
3904
|
-
if (!opts.hidden && dirent.name.startsWith(".")) {
|
|
3905
|
-
hidden += 1;
|
|
3906
|
-
continue;
|
|
3907
|
-
}
|
|
3908
|
-
if (kept.length >= DIR_PAGE_LIMIT) {
|
|
3909
|
-
omitted += 1;
|
|
3910
|
-
continue;
|
|
3911
|
-
}
|
|
3912
|
-
const child = resolve(path, dirent.name);
|
|
3913
|
-
kept.push({
|
|
3914
|
-
name: dirent.name,
|
|
3915
|
-
path: child,
|
|
3916
|
-
known: known.has(child),
|
|
3917
|
-
readable: canList(child),
|
|
3918
|
-
group: "child"
|
|
4152
|
+
const added = unfixedRules(def.allowRules, run.pendingApprovals);
|
|
4153
|
+
if (added.length === 0) {
|
|
4154
|
+
return sendJson(res, 200, {
|
|
4155
|
+
added: [],
|
|
4156
|
+
row: toWireRow(control, def)
|
|
3919
4157
|
});
|
|
3920
4158
|
}
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
omitted,
|
|
3928
|
-
hidden
|
|
3929
|
-
}
|
|
3930
|
-
};
|
|
4159
|
+
const saved = await control.update(id, { allowRules: [...def.allowRules, ...added] });
|
|
4160
|
+
const next = saved.schedule ?? def;
|
|
4161
|
+
sendJson(res, 200, {
|
|
4162
|
+
added,
|
|
4163
|
+
row: toWireRow(control, next)
|
|
4164
|
+
});
|
|
3931
4165
|
}
|
|
3932
|
-
function
|
|
3933
|
-
const
|
|
3934
|
-
if (
|
|
3935
|
-
if (
|
|
3936
|
-
|
|
4166
|
+
function getRecording(ctx, res, id, runId, lang) {
|
|
4167
|
+
const control = schedulesOf(ctx, res, lang);
|
|
4168
|
+
if (!control) return;
|
|
4169
|
+
if (!control.get(id)) return sendNotFound(res, id, lang);
|
|
4170
|
+
const run = control.runs(id, RUNS_LIMIT).find((one) => one.id === runId);
|
|
4171
|
+
if (!run) {
|
|
4172
|
+
return sendError(res, 404, "not-found", t("web.schedule_run_unknown", { runId }, lang));
|
|
4173
|
+
}
|
|
4174
|
+
sendJson(res, 200, toRecording(run));
|
|
3937
4175
|
}
|
|
3938
|
-
function
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
4176
|
+
function toRecording(run) {
|
|
4177
|
+
if (run.logPath === void 0) {
|
|
4178
|
+
return { runId: run.id, frames: [], missing: true, truncated: false };
|
|
4179
|
+
}
|
|
4180
|
+
const frames = coalesceRecording(readRecording(run.logPath));
|
|
4181
|
+
if (frames.length === 0) {
|
|
4182
|
+
return { runId: run.id, frames: [], missing: true, truncated: false };
|
|
3944
4183
|
}
|
|
4184
|
+
const truncated = frames.length > MAX_RECORDING_FRAMES;
|
|
4185
|
+
return {
|
|
4186
|
+
runId: run.id,
|
|
4187
|
+
frames: truncated ? frames.slice(-MAX_RECORDING_FRAMES) : frames,
|
|
4188
|
+
missing: false,
|
|
4189
|
+
truncated
|
|
4190
|
+
};
|
|
3945
4191
|
}
|
|
3946
|
-
function
|
|
3947
|
-
|
|
3948
|
-
|
|
4192
|
+
function toSaveResponse(control, saved) {
|
|
4193
|
+
return {
|
|
4194
|
+
ok: saved.issues.length === 0,
|
|
4195
|
+
issues: saved.issues,
|
|
4196
|
+
...saved.schedule ? { row: toWireRow(control, saved.schedule) } : {},
|
|
4197
|
+
...saved.registration ? { registration: saved.registration } : {}
|
|
4198
|
+
};
|
|
3949
4199
|
}
|
|
3950
|
-
function
|
|
3951
|
-
|
|
4200
|
+
function sendNotFound(res, id, lang) {
|
|
4201
|
+
sendError(res, 404, "unknown-schedule", t("web.schedule_unknown", { id }, lang));
|
|
3952
4202
|
}
|
|
3953
|
-
|
|
3954
|
-
|
|
4203
|
+
// src/skill-import.ts
|
|
4204
|
+
function pathOf(payload) {
|
|
4205
|
+
return typeof payload["path"] === "string" ? payload["path"].trim() : "";
|
|
3955
4206
|
}
|
|
3956
|
-
function
|
|
3957
|
-
return
|
|
4207
|
+
function toWireIssues2(issues) {
|
|
4208
|
+
return (issues ?? []).map((i) => ({ path: i.path, message: i.message }));
|
|
3958
4209
|
}
|
|
3959
|
-
function
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
4210
|
+
function toWireEntry(entry) {
|
|
4211
|
+
return {
|
|
4212
|
+
name: entry.name,
|
|
4213
|
+
category: entry.category,
|
|
4214
|
+
description: entry.description,
|
|
4215
|
+
files: entry.files,
|
|
4216
|
+
bytes: entry.bytes,
|
|
4217
|
+
conflict: entry.conflict ?? null
|
|
4218
|
+
};
|
|
3963
4219
|
}
|
|
3964
|
-
function
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
return [
|
|
3981
|
-
{ command: "pwsh", args: ["-NoProfile", "-STA", "-Command", script] },
|
|
3982
|
-
{ command: "powershell.exe", args: ["-NoProfile", "-STA", "-Command", script] }
|
|
3983
|
-
];
|
|
4220
|
+
async function previewSkillImport(ctx, req, res, lang) {
|
|
4221
|
+
const body = await readJsonBody(req);
|
|
4222
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
4223
|
+
const source = pathOf(asRecord(body.value));
|
|
4224
|
+
const outcome = await ctx.runtime.skillWrite.preview(source, lang);
|
|
4225
|
+
if (!outcome.ok) {
|
|
4226
|
+
return sendJson(res, 200, {
|
|
4227
|
+
ok: false,
|
|
4228
|
+
source,
|
|
4229
|
+
form: null,
|
|
4230
|
+
skills: [],
|
|
4231
|
+
issues: toWireIssues2(outcome.issues),
|
|
4232
|
+
token: "",
|
|
4233
|
+
detail: outcome.detail,
|
|
4234
|
+
reason: outcome.reason
|
|
4235
|
+
});
|
|
3984
4236
|
}
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
"$epochOwner.Size = New-Object System.Drawing.Size 1, 1",
|
|
3997
|
-
"$epochOwner.Location = New-Object System.Drawing.Point -32000, -32000",
|
|
3998
|
-
"$epochOwner.Show()",
|
|
3999
|
-
"$epochOwner.Activate()",
|
|
4000
|
-
"try {",
|
|
4001
|
-
"Add-Type -Namespace EpochPick -Name Win -MemberDefinition @'",
|
|
4002
|
-
'[DllImport("user32.dll")] public static extern IntPtr GetWindow(IntPtr h, uint cmd);',
|
|
4003
|
-
'[DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr h, IntPtr after, int x, int y, int cx, int cy, uint flags);',
|
|
4004
|
-
'[DllImport("user32.dll")] public static extern void SwitchToThisWindow(IntPtr h, bool useAltTab);',
|
|
4005
|
-
'[DllImport("user32.dll")] public static extern int GetWindowLong(IntPtr h, int i);',
|
|
4006
|
-
'[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();',
|
|
4007
|
-
'[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, IntPtr pid);',
|
|
4008
|
-
'[DllImport("user32.dll")] public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);',
|
|
4009
|
-
'[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);',
|
|
4010
|
-
'[DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr h);',
|
|
4011
|
-
'[DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();',
|
|
4012
|
-
"static bool Raise(IntPtr h) {",
|
|
4013
|
-
" IntPtr fg = GetForegroundWindow();",
|
|
4014
|
-
" uint fgThread = GetWindowThreadProcessId(fg, IntPtr.Zero);",
|
|
4015
|
-
" uint me = GetCurrentThreadId();",
|
|
4016
|
-
" bool attached = false;",
|
|
4017
|
-
" if (fgThread != 0 && fgThread != me) { attached = AttachThreadInput(me, fgThread, true); }",
|
|
4018
|
-
" bool okPos = SetWindowPos(h, new IntPtr(-1), 0, 0, 0, 0, 3);",
|
|
4019
|
-
" BringWindowToTop(h);",
|
|
4020
|
-
" bool okFg = SetForegroundWindow(h);",
|
|
4021
|
-
" SwitchToThisWindow(h, true);",
|
|
4022
|
-
" if (attached) { AttachThreadInput(me, fgThread, false); }",
|
|
4023
|
-
" return okPos && okFg;",
|
|
4024
|
-
"}",
|
|
4025
|
-
"public static void RaiseSoon(IntPtr owner) {",
|
|
4026
|
-
" System.Threading.Thread t = new System.Threading.Thread(delegate() {",
|
|
4027
|
-
' string note = "dialog window never appeared";',
|
|
4028
|
-
" for (int i = 0; i < 400; i++) {",
|
|
4029
|
-
" System.Threading.Thread.Sleep(30);",
|
|
4030
|
-
" IntPtr dlg = GetWindow(owner, 6);",
|
|
4031
|
-
" if (dlg == IntPtr.Zero) { continue; }",
|
|
4032
|
-
" bool ok = false;",
|
|
4033
|
-
" for (int k = 0; k < 4; k++) {",
|
|
4034
|
-
" Raise(owner);",
|
|
4035
|
-
" ok = Raise(dlg) || ok;",
|
|
4036
|
-
" System.Threading.Thread.Sleep(120);",
|
|
4037
|
-
" }",
|
|
4038
|
-
' note = "raised hwnd=" + dlg.ToInt64() + " ok=" + ok + " dlgEx=0x" + GetWindowLong(dlg, -20).ToString("X") + " ownerEx=0x" + GetWindowLong(owner, -20).ToString("X") + " isFg=" + (GetForegroundWindow() == dlg);',
|
|
4039
|
-
" break;",
|
|
4040
|
-
" }",
|
|
4041
|
-
' string dbg = System.Environment.GetEnvironmentVariable("EPOCH_PICK_DEBUG");',
|
|
4042
|
-
" if (!string.IsNullOrEmpty(dbg)) {",
|
|
4043
|
-
' try { System.IO.File.AppendAllText(System.IO.Path.Combine(System.IO.Path.GetTempPath(), "epoch-pick-debug.log"), System.DateTime.Now.ToString("HH:mm:ss.fff") + " " + note + System.Environment.NewLine); } catch { }',
|
|
4044
|
-
" }",
|
|
4045
|
-
" });",
|
|
4046
|
-
" t.IsBackground = true;",
|
|
4047
|
-
" t.Start();",
|
|
4048
|
-
"}",
|
|
4049
|
-
"'@",
|
|
4050
|
-
"[EpochPick.Win]::RaiseSoon($epochOwner.Handle)",
|
|
4051
|
-
"} catch { }",
|
|
4052
|
-
"$d = New-Object System.Windows.Forms.FolderBrowserDialog",
|
|
4053
|
-
`$d.Description = ${asPowerShellString(prompt)}`,
|
|
4054
|
-
"$d.ShowNewFolderButton = $true",
|
|
4055
|
-
"$epochResult = $d.ShowDialog($epochOwner)",
|
|
4056
|
-
"$epochOwner.Close()",
|
|
4057
|
-
"if ($epochResult -ne [System.Windows.Forms.DialogResult]::OK) { exit 1 }",
|
|
4058
|
-
"[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($d.SelectedPath))"
|
|
4059
|
-
].join("\n");
|
|
4237
|
+
const preview = outcome.preview;
|
|
4238
|
+
sendJson(res, 200, {
|
|
4239
|
+
ok: true,
|
|
4240
|
+
source: preview.source,
|
|
4241
|
+
form: preview.form,
|
|
4242
|
+
skills: preview.skills.map(toWireEntry),
|
|
4243
|
+
issues: toWireIssues2(preview.issues),
|
|
4244
|
+
token: preview.token,
|
|
4245
|
+
detail: null,
|
|
4246
|
+
reason: null
|
|
4247
|
+
});
|
|
4060
4248
|
}
|
|
4061
|
-
function
|
|
4062
|
-
|
|
4249
|
+
async function importSkills(ctx, req, res, lang) {
|
|
4250
|
+
const body = await readJsonBody(req);
|
|
4251
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
4252
|
+
const payload = asRecord(body.value);
|
|
4253
|
+
const source = pathOf(payload);
|
|
4254
|
+
const token = typeof payload["token"] === "string" ? payload["token"] : "";
|
|
4255
|
+
const outcome = await ctx.runtime.skillWrite.import(source, token, lang);
|
|
4256
|
+
if (!outcome.ok) {
|
|
4257
|
+
return sendJson(res, 200, {
|
|
4258
|
+
ok: false,
|
|
4259
|
+
imported: [],
|
|
4260
|
+
skipped: toWireIssues2(outcome.issues),
|
|
4261
|
+
detail: outcome.detail,
|
|
4262
|
+
reason: outcome.reason
|
|
4263
|
+
});
|
|
4264
|
+
}
|
|
4265
|
+
sendJson(res, 200, {
|
|
4266
|
+
ok: outcome.imported.length > 0,
|
|
4267
|
+
imported: [...outcome.imported],
|
|
4268
|
+
skipped: toWireIssues2(outcome.skipped),
|
|
4269
|
+
detail: null,
|
|
4270
|
+
reason: null
|
|
4271
|
+
});
|
|
4063
4272
|
}
|
|
4064
|
-
|
|
4065
|
-
|
|
4273
|
+
// src/skill-remove.ts
|
|
4274
|
+
async function removeSkill(ctx, req, res, lang) {
|
|
4275
|
+
const body = await readJsonBody(req);
|
|
4276
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
4277
|
+
const payload = asRecord(body.value);
|
|
4278
|
+
const name = typeof payload["name"] === "string" ? payload["name"].trim() : "";
|
|
4279
|
+
const outcome = await ctx.runtime.skillWrite.remove(name, lang);
|
|
4280
|
+
if (!outcome.ok) {
|
|
4281
|
+
return sendJson(res, 200, {
|
|
4282
|
+
ok: false,
|
|
4283
|
+
name: "",
|
|
4284
|
+
path: null,
|
|
4285
|
+
detail: outcome.detail,
|
|
4286
|
+
reason: outcome.reason
|
|
4287
|
+
});
|
|
4288
|
+
}
|
|
4289
|
+
sendJson(res, 200, {
|
|
4290
|
+
ok: true,
|
|
4291
|
+
name: outcome.name,
|
|
4292
|
+
path: outcome.path,
|
|
4293
|
+
detail: null,
|
|
4294
|
+
reason: null
|
|
4295
|
+
});
|
|
4066
4296
|
}
|
|
4067
|
-
function
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
return
|
|
4297
|
+
function refusedByLan2(ctx, res, lang) {
|
|
4298
|
+
if (!ctx.lanExposed) return false;
|
|
4299
|
+
sendError(res, 403, "skill-stage-lan-exposed", t("web.skill_stage_lan_exposed", void 0, lang));
|
|
4300
|
+
return true;
|
|
4071
4301
|
}
|
|
4072
|
-
function
|
|
4073
|
-
if (
|
|
4074
|
-
|
|
4075
|
-
return
|
|
4302
|
+
async function stageSkillDrop(ctx, req, res, lang) {
|
|
4303
|
+
if (refusedByLan2(ctx, res, lang)) return;
|
|
4304
|
+
const raw = await readRawBody(req, PLUGIN_UPLOAD_BODY_LIMIT, lang);
|
|
4305
|
+
if (!raw.ok) return sendError(res, raw.status, "bad-body", raw.message);
|
|
4306
|
+
const outcome = await ctx.runtime.skillStage.stage(
|
|
4307
|
+
{ name: nameParam3(req), data: raw.value },
|
|
4308
|
+
lang
|
|
4309
|
+
);
|
|
4310
|
+
sendJson(res, 200, {
|
|
4311
|
+
ok: outcome.ok,
|
|
4312
|
+
path: outcome.ok ? outcome.path : null,
|
|
4313
|
+
detail: outcome.ok ? null : outcome.detail,
|
|
4314
|
+
reason: outcome.ok ? null : outcome.reason
|
|
4315
|
+
});
|
|
4076
4316
|
}
|
|
4077
|
-
|
|
4078
|
-
async function pickNativeDirectory(opts) {
|
|
4079
|
-
const platform = opts.platform ?? process.platform;
|
|
4080
|
-
const candidates = nativePickerCommands(platform, opts.prompt);
|
|
4081
|
-
if (!candidates) {
|
|
4082
|
-
return { ok: false, reason: "unsupported", message: `unsupported platform: ${platform}` };
|
|
4083
|
-
}
|
|
4084
|
-
if (inFlight) {
|
|
4085
|
-
return { ok: false, reason: "busy", message: "a directory picker is already open" };
|
|
4086
|
-
}
|
|
4087
|
-
const run = opts.runner ?? execFileRunner;
|
|
4088
|
-
inFlight = true;
|
|
4317
|
+
function nameParam3(req) {
|
|
4089
4318
|
try {
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
if (attempt.missing) {
|
|
4094
|
-
last = attempt.message || `${candidate.command} not found`;
|
|
4095
|
-
if (index < candidates.length - 1) continue;
|
|
4096
|
-
return { ok: false, reason: "failed", message: last };
|
|
4097
|
-
}
|
|
4098
|
-
if (!attempt.code) {
|
|
4099
|
-
const path = decodeNativePick(platform, attempt.stdout);
|
|
4100
|
-
return path ? { ok: true, path } : { ok: false, reason: "failed", message: "picker returned an empty path" };
|
|
4101
|
-
}
|
|
4102
|
-
if (isPickCanceled(platform, attempt.code, attempt.stderr)) {
|
|
4103
|
-
return { ok: true, canceled: true };
|
|
4104
|
-
}
|
|
4105
|
-
return { ok: false, reason: "failed", message: attempt.stderr.trim() || attempt.message };
|
|
4106
|
-
}
|
|
4107
|
-
return { ok: false, reason: "failed", message: last };
|
|
4108
|
-
} finally {
|
|
4109
|
-
inFlight = false;
|
|
4319
|
+
return new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("name") ?? "";
|
|
4320
|
+
} catch {
|
|
4321
|
+
return "";
|
|
4110
4322
|
}
|
|
4111
4323
|
}
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
),
|
|
4134
|
-
(
|
|
4135
|
-
code: err.code ?? 1,
|
|
4136
|
-
stdout: "",
|
|
4137
|
-
stderr: "",
|
|
4138
|
-
missing: err.code === "ENOENT" || err.code === "ENOTDIR",
|
|
4139
|
-
message: `${err.message} (command=${command})`
|
|
4140
|
-
})
|
|
4141
|
-
);
|
|
4142
|
-
});
|
|
4143
|
-
// src/workspace/dirs.ts
|
|
4144
|
-
function browseWorkspaceDirs(ctx, res, rawUrl, lang) {
|
|
4145
|
-
const query = new URL(rawUrl ?? "/", "http://127.0.0.1").searchParams;
|
|
4146
|
-
const path = dirsQuery(query) ?? atQuery(query);
|
|
4147
|
-
if (path === void 0) {
|
|
4148
|
-
return sendJson(res, 200, workspaceAnchors(ctx.runtime.workspaces));
|
|
4324
|
+
// src/sse.ts
|
|
4325
|
+
var DEFAULT_HEARTBEAT_MS = 15e3;
|
|
4326
|
+
function parseLastEventId(req) {
|
|
4327
|
+
const raw = req.headers["last-event-id"];
|
|
4328
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
4329
|
+
if (typeof value !== "string" || value.trim().length === 0) return void 0;
|
|
4330
|
+
const parsed = Number(value.trim());
|
|
4331
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : void 0;
|
|
4332
|
+
}
|
|
4333
|
+
var SseConnection = class {
|
|
4334
|
+
constructor(res, heartbeatMs = DEFAULT_HEARTBEAT_MS) {
|
|
4335
|
+
this.res = res;
|
|
4336
|
+
res.writeHead(200, {
|
|
4337
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
4338
|
+
"Cache-Control": "no-cache, no-transform",
|
|
4339
|
+
Connection: "keep-alive",
|
|
4340
|
+
"X-Accel-Buffering": "no",
|
|
4341
|
+
"Referrer-Policy": "no-referrer",
|
|
4342
|
+
"X-Content-Type-Options": "nosniff"
|
|
4343
|
+
});
|
|
4344
|
+
res.flushHeaders();
|
|
4345
|
+
this.heartbeat = setInterval(() => this.ping(), heartbeatMs);
|
|
4346
|
+
this.heartbeat.unref();
|
|
4149
4347
|
}
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
);
|
|
4348
|
+
res;
|
|
4349
|
+
heartbeat;
|
|
4350
|
+
closed = false;
|
|
4351
|
+
send(frame) {
|
|
4352
|
+
if (this.closed) return;
|
|
4353
|
+
const body = frame.seq > 0 ? `id: ${frame.seq}
|
|
4354
|
+
` : "";
|
|
4355
|
+
this.res.write(`${body}data: ${JSON.stringify(frame)}
|
|
4356
|
+
|
|
4357
|
+
`);
|
|
4161
4358
|
}
|
|
4162
|
-
|
|
4163
|
-
|
|
4359
|
+
ping() {
|
|
4360
|
+
if (this.closed) return;
|
|
4361
|
+
this.res.write(": ping\n\n");
|
|
4164
4362
|
}
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4363
|
+
close() {
|
|
4364
|
+
if (this.closed) return;
|
|
4365
|
+
this.closed = true;
|
|
4366
|
+
clearInterval(this.heartbeat);
|
|
4367
|
+
this.res.end();
|
|
4368
|
+
}
|
|
4369
|
+
};
|
|
4370
|
+
var TASK_TAIL_BYTES = 8 * 1024;
|
|
4371
|
+
var LIVE_TASKS = {
|
|
4372
|
+
list: (sessionId) => listBackgroundTasks(sessionId),
|
|
4373
|
+
output: (sessionId, id, since) => backgroundTaskOutput(sessionId, id, since)
|
|
4374
|
+
};
|
|
4375
|
+
function toWireTask(sessionId, info, registry) {
|
|
4376
|
+
const since = Math.max(0, info.outputBytes - TASK_TAIL_BYTES);
|
|
4377
|
+
const chunk = registry.output(sessionId, info.id, since);
|
|
4378
|
+
if (!chunk) return { info, output: "", omittedBytes: info.outputBytes };
|
|
4379
|
+
return {
|
|
4380
|
+
info,
|
|
4381
|
+
output: chunk.output,
|
|
4382
|
+
omittedBytes: Math.max(0, chunk.nextCursor - chunk.output.length)
|
|
4383
|
+
};
|
|
4171
4384
|
}
|
|
4172
|
-
function
|
|
4173
|
-
|
|
4174
|
-
return value ? value : void 0;
|
|
4385
|
+
function collectTasks(sessionId, registry) {
|
|
4386
|
+
return { tasks: registry.list(sessionId).map((info) => toWireTask(sessionId, info, registry)) };
|
|
4175
4387
|
}
|
|
4176
|
-
function
|
|
4177
|
-
|
|
4388
|
+
function getTasks(ctx, res, sessionId, _lang, registry = LIVE_TASKS) {
|
|
4389
|
+
if (!ctx.hub.has(sessionId) && !ctx.runtime.sessions?.get(sessionId)) {
|
|
4390
|
+
return sendUnknownSession(res, sessionId);
|
|
4391
|
+
}
|
|
4392
|
+
sendJson(res, 200, collectTasks(sessionId, registry));
|
|
4178
4393
|
}
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
return
|
|
4394
|
+
var DIR_PAGE_LIMIT = 1e3;
|
|
4395
|
+
function workspaceHome() {
|
|
4396
|
+
return homedir();
|
|
4182
4397
|
}
|
|
4183
|
-
|
|
4184
|
-
const
|
|
4185
|
-
|
|
4186
|
-
const
|
|
4187
|
-
const
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4398
|
+
function workspaceAnchors(workspaces) {
|
|
4399
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4400
|
+
const entries = [];
|
|
4401
|
+
const known = new Set(workspaces.known().map((one) => resolve(one.root)));
|
|
4402
|
+
const push = (raw, group) => {
|
|
4403
|
+
const path = resolve(raw);
|
|
4404
|
+
if (seen.has(path)) return;
|
|
4405
|
+
seen.add(path);
|
|
4406
|
+
entries.push({
|
|
4407
|
+
name: displayName2(path),
|
|
4408
|
+
path,
|
|
4409
|
+
known: known.has(path),
|
|
4410
|
+
readable: canList(path),
|
|
4411
|
+
group
|
|
4412
|
+
});
|
|
4413
|
+
};
|
|
4414
|
+
push(homedir(), "home");
|
|
4415
|
+
push(process.cwd(), "cwd");
|
|
4416
|
+
for (const one of workspaces.known()) push(one.root, "known");
|
|
4417
|
+
return {
|
|
4418
|
+
path: null,
|
|
4419
|
+
parent: null,
|
|
4420
|
+
entries,
|
|
4421
|
+
omitted: 0,
|
|
4422
|
+
hidden: 0
|
|
4423
|
+
};
|
|
4424
|
+
}
|
|
4425
|
+
function listWorkspaceDirs(workspaces, raw, opts = {}) {
|
|
4426
|
+
if (raw.length === 0 || !isAbsolute(raw)) {
|
|
4427
|
+
return { ok: false, reason: "not-absolute", path: raw };
|
|
4199
4428
|
}
|
|
4200
|
-
const path = resolve(
|
|
4429
|
+
const path = resolve(raw);
|
|
4430
|
+
let dirents;
|
|
4201
4431
|
try {
|
|
4202
|
-
|
|
4432
|
+
dirents = readdirSync(path, { withFileTypes: true });
|
|
4203
4433
|
} catch (cause) {
|
|
4204
|
-
return
|
|
4434
|
+
return { ok: false, reason: readdirFailure(cause), path };
|
|
4435
|
+
}
|
|
4436
|
+
const known = new Set(workspaces.known().map((one) => resolve(one.root)));
|
|
4437
|
+
const kept = [];
|
|
4438
|
+
let omitted = 0;
|
|
4439
|
+
let hidden = 0;
|
|
4440
|
+
for (const dirent of dirents) {
|
|
4441
|
+
if (!dirent.isDirectory()) continue;
|
|
4442
|
+
if (!opts.hidden && dirent.name.startsWith(".")) {
|
|
4443
|
+
hidden += 1;
|
|
4444
|
+
continue;
|
|
4445
|
+
}
|
|
4446
|
+
if (kept.length >= DIR_PAGE_LIMIT) {
|
|
4447
|
+
omitted += 1;
|
|
4448
|
+
continue;
|
|
4449
|
+
}
|
|
4450
|
+
const child = resolve(path, dirent.name);
|
|
4451
|
+
kept.push({
|
|
4452
|
+
name: dirent.name,
|
|
4453
|
+
path: child,
|
|
4454
|
+
known: known.has(child),
|
|
4455
|
+
readable: canList(child),
|
|
4456
|
+
group: "child"
|
|
4457
|
+
});
|
|
4205
4458
|
}
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4459
|
+
return {
|
|
4460
|
+
ok: true,
|
|
4461
|
+
view: {
|
|
4209
4462
|
path,
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4463
|
+
parent: parentOf(path),
|
|
4464
|
+
entries: kept,
|
|
4465
|
+
omitted,
|
|
4466
|
+
hidden
|
|
4213
4467
|
}
|
|
4214
|
-
}
|
|
4215
|
-
}
|
|
4216
|
-
async function pickWorkspaceDir(ctx, req, res, lang) {
|
|
4217
|
-
if (!ctx.nativeDirPicker) {
|
|
4218
|
-
return sendError(res, 403, "native-picker-unavailable", t("web.pick_unavailable", {}, lang));
|
|
4219
|
-
}
|
|
4220
|
-
const abort = new AbortController();
|
|
4221
|
-
req.on("close", () => abort.abort());
|
|
4222
|
-
const outcome = await pickNativeDirectory({
|
|
4223
|
-
prompt: t("web.pick_prompt", {}, lang),
|
|
4224
|
-
signal: abort.signal
|
|
4225
|
-
});
|
|
4226
|
-
if (res.writableEnded || abort.signal.aborted) return;
|
|
4227
|
-
if (outcome.ok) {
|
|
4228
|
-
return sendJson(
|
|
4229
|
-
res,
|
|
4230
|
-
200,
|
|
4231
|
-
outcome.path === void 0 ? { canceled: true } : { path: outcome.path }
|
|
4232
|
-
);
|
|
4233
|
-
}
|
|
4234
|
-
if (outcome.reason === "busy") {
|
|
4235
|
-
return sendError(res, 409, "native-picker-busy", t("web.pick_busy", {}, lang));
|
|
4236
|
-
}
|
|
4237
|
-
sendError(
|
|
4238
|
-
res,
|
|
4239
|
-
500,
|
|
4240
|
-
"native-picker-failed",
|
|
4241
|
-
t("web.pick_failed", { detail: outcome.message }, lang)
|
|
4242
|
-
);
|
|
4243
|
-
}
|
|
4244
|
-
function isSingleSegment(name) {
|
|
4245
|
-
if (name.length === 0) return false;
|
|
4246
|
-
if (name === "." || name === "..") return false;
|
|
4247
|
-
return !name.includes("/") && !name.includes(sep);
|
|
4468
|
+
};
|
|
4248
4469
|
}
|
|
4249
|
-
function
|
|
4470
|
+
function readdirFailure(cause) {
|
|
4250
4471
|
const code = cause?.code;
|
|
4251
|
-
if (code === "
|
|
4252
|
-
if (code === "
|
|
4253
|
-
|
|
4254
|
-
}
|
|
4255
|
-
if (code === "ENOENT") return [404, "not-found", t("web.mkdir_parent_missing", { path }, lang)];
|
|
4256
|
-
return [500, "internal", t("web.mkdir_failed", { path }, lang)];
|
|
4257
|
-
}
|
|
4258
|
-
var MAX_TEXT_BYTES = 2 * 1024 * 1024;
|
|
4259
|
-
var VIEWABLE = /* @__PURE__ */ new Map([
|
|
4260
|
-
[".png", { kind: "image", mime: "image/png" }],
|
|
4261
|
-
[".jpg", { kind: "image", mime: "image/jpeg" }],
|
|
4262
|
-
[".jpeg", { kind: "image", mime: "image/jpeg" }],
|
|
4263
|
-
[".gif", { kind: "image", mime: "image/gif" }],
|
|
4264
|
-
[".webp", { kind: "image", mime: "image/webp" }],
|
|
4265
|
-
[".bmp", { kind: "image", mime: "image/bmp" }],
|
|
4266
|
-
[".ico", { kind: "image", mime: "image/x-icon" }],
|
|
4267
|
-
[".avif", { kind: "image", mime: "image/avif" }],
|
|
4268
|
-
[".pdf", { kind: "pdf", mime: "application/pdf" }],
|
|
4269
|
-
[".mp3", { kind: "audio", mime: "audio/mpeg" }],
|
|
4270
|
-
[".wav", { kind: "audio", mime: "audio/wav" }],
|
|
4271
|
-
[".ogg", { kind: "audio", mime: "audio/ogg" }],
|
|
4272
|
-
[".m4a", { kind: "audio", mime: "audio/mp4" }],
|
|
4273
|
-
[".flac", { kind: "audio", mime: "audio/flac" }],
|
|
4274
|
-
[".mp4", { kind: "video", mime: "video/mp4" }],
|
|
4275
|
-
[".webm", { kind: "video", mime: "video/webm" }],
|
|
4276
|
-
[".mov", { kind: "video", mime: "video/quicktime" }]
|
|
4277
|
-
]);
|
|
4278
|
-
function kindOf(stat3) {
|
|
4279
|
-
if (stat3.kind === "text") return "text";
|
|
4280
|
-
return VIEWABLE.get(extname(stat3.rel).toLowerCase())?.kind ?? "opaque";
|
|
4472
|
+
if (code === "EACCES" || code === "EPERM") return "denied";
|
|
4473
|
+
if (code === "ENOTDIR") return "not-a-directory";
|
|
4474
|
+
return "missing";
|
|
4281
4475
|
}
|
|
4282
|
-
function
|
|
4283
|
-
|
|
4476
|
+
function canList(path) {
|
|
4477
|
+
try {
|
|
4478
|
+
accessSync(path, constants.R_OK | constants.X_OK);
|
|
4479
|
+
return true;
|
|
4480
|
+
} catch {
|
|
4481
|
+
return false;
|
|
4482
|
+
}
|
|
4284
4483
|
}
|
|
4285
|
-
function
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
if (refusal === "too-large") return 413;
|
|
4289
|
-
return refusal === "not-a-file" ? 400 : 503;
|
|
4484
|
+
function parentOf(path) {
|
|
4485
|
+
const up = dirname(path);
|
|
4486
|
+
return up === path ? null : up;
|
|
4290
4487
|
}
|
|
4291
|
-
function
|
|
4292
|
-
|
|
4293
|
-
if (refusal === "missing") return t("web.file_view_missing", void 0, lang);
|
|
4294
|
-
if (refusal === "not-a-file") return t("web.file_view_not_a_file", void 0, lang);
|
|
4295
|
-
if (refusal === "too-large") {
|
|
4296
|
-
return t("web.file_view_too_large", { max: String(MAX_TEXT_BYTES) }, lang);
|
|
4297
|
-
}
|
|
4298
|
-
return t("web.file_view_unreadable", void 0, lang);
|
|
4488
|
+
function displayName2(path) {
|
|
4489
|
+
return basename(path) || path;
|
|
4299
4490
|
}
|
|
4300
|
-
function
|
|
4301
|
-
|
|
4302
|
-
sendUnknownSession(res, sessionId);
|
|
4303
|
-
return null;
|
|
4304
|
-
}
|
|
4305
|
-
const root = workspaceRootOf(ctx, sessionId);
|
|
4306
|
-
if (!root) {
|
|
4307
|
-
sendError(res, 409, "no-workspace", t("web.file_view_no_workspace", void 0, lang));
|
|
4308
|
-
return null;
|
|
4309
|
-
}
|
|
4310
|
-
return root;
|
|
4491
|
+
function present(value) {
|
|
4492
|
+
return value !== void 0 && value !== "";
|
|
4311
4493
|
}
|
|
4312
|
-
function
|
|
4313
|
-
|
|
4314
|
-
const path = params.get("path")?.trim();
|
|
4315
|
-
if (!path) {
|
|
4316
|
-
sendError(res, 400, "missing-path", t("web.file_view_missing_path", void 0, lang));
|
|
4317
|
-
return null;
|
|
4318
|
-
}
|
|
4319
|
-
return path;
|
|
4494
|
+
function resolveNativeDirPicker(facts) {
|
|
4495
|
+
return isSameMachine(facts);
|
|
4320
4496
|
}
|
|
4321
|
-
function
|
|
4322
|
-
|
|
4323
|
-
if (
|
|
4324
|
-
|
|
4325
|
-
const asked = params.getAll("path").filter((p) => p.trim() !== "");
|
|
4326
|
-
const taken = asked.slice(0, WIRE_FILE_STAT_MAX);
|
|
4327
|
-
const files = taken.map((path) => {
|
|
4328
|
-
const stat3 = ctx.runtime.workspaceFiles.statFile(path, root);
|
|
4329
|
-
if (!stat3.ok) return { path, ok: false, refusal: toWireRefusal(stat3.reason) };
|
|
4330
|
-
return {
|
|
4331
|
-
path,
|
|
4332
|
-
ok: true,
|
|
4333
|
-
kind: kindOf(stat3),
|
|
4334
|
-
bytes: stat3.bytes,
|
|
4335
|
-
mtimeMs: stat3.mtimeMs,
|
|
4336
|
-
openable: ctx.sameMachine && isSystemOpenable(stat3.rel)
|
|
4337
|
-
};
|
|
4338
|
-
});
|
|
4339
|
-
sendJson(res, 200, {
|
|
4340
|
-
files,
|
|
4341
|
-
overLimit: asked.length - taken.length,
|
|
4342
|
-
root
|
|
4343
|
-
});
|
|
4497
|
+
function isSameMachine(facts) {
|
|
4498
|
+
if (facts.lanExposed) return false;
|
|
4499
|
+
if (present(facts.env.SSH_CONNECTION) || present(facts.env.SSH_TTY)) return false;
|
|
4500
|
+
return facts.platform === "darwin" || facts.platform === "win32";
|
|
4344
4501
|
}
|
|
4345
|
-
function
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
if (stat3.bytes > MAX_TEXT_BYTES) {
|
|
4359
|
-
return sendError(res, 413, "too-large", describeRefusal("too-large", lang));
|
|
4502
|
+
function nativePickerCommands(platform, prompt) {
|
|
4503
|
+
if (platform === "darwin") {
|
|
4504
|
+
return [
|
|
4505
|
+
{
|
|
4506
|
+
command: "osascript",
|
|
4507
|
+
args: [
|
|
4508
|
+
"-e",
|
|
4509
|
+
`set epochChosenFolder to choose folder with prompt ${asAppleScriptString(prompt)}`,
|
|
4510
|
+
"-e",
|
|
4511
|
+
"POSIX path of epochChosenFolder"
|
|
4512
|
+
]
|
|
4513
|
+
}
|
|
4514
|
+
];
|
|
4360
4515
|
}
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4516
|
+
if (platform === "win32") {
|
|
4517
|
+
const script = win32PickScript(prompt);
|
|
4518
|
+
return [
|
|
4519
|
+
{ command: "pwsh", args: ["-NoProfile", "-STA", "-Command", script] },
|
|
4520
|
+
{ command: "powershell.exe", args: ["-NoProfile", "-STA", "-Command", script] }
|
|
4521
|
+
];
|
|
4365
4522
|
}
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4523
|
+
return null;
|
|
4524
|
+
}
|
|
4525
|
+
function win32PickScript(prompt) {
|
|
4526
|
+
return [
|
|
4527
|
+
"Add-Type -AssemblyName System.Windows.Forms",
|
|
4528
|
+
"Add-Type -AssemblyName System.Drawing",
|
|
4529
|
+
"$epochOwner = New-Object System.Windows.Forms.Form",
|
|
4530
|
+
"$epochOwner.FormBorderStyle = 'None'",
|
|
4531
|
+
"$epochOwner.ShowInTaskbar = $false",
|
|
4532
|
+
"$epochOwner.TopMost = $true",
|
|
4533
|
+
"$epochOwner.StartPosition = 'Manual'",
|
|
4534
|
+
"$epochOwner.Size = New-Object System.Drawing.Size 1, 1",
|
|
4535
|
+
"$epochOwner.Location = New-Object System.Drawing.Point -32000, -32000",
|
|
4536
|
+
"$epochOwner.Show()",
|
|
4537
|
+
"$epochOwner.Activate()",
|
|
4538
|
+
"try {",
|
|
4539
|
+
"Add-Type -Namespace EpochPick -Name Win -MemberDefinition @'",
|
|
4540
|
+
'[DllImport("user32.dll")] public static extern IntPtr GetWindow(IntPtr h, uint cmd);',
|
|
4541
|
+
'[DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr h, IntPtr after, int x, int y, int cx, int cy, uint flags);',
|
|
4542
|
+
'[DllImport("user32.dll")] public static extern void SwitchToThisWindow(IntPtr h, bool useAltTab);',
|
|
4543
|
+
'[DllImport("user32.dll")] public static extern int GetWindowLong(IntPtr h, int i);',
|
|
4544
|
+
'[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();',
|
|
4545
|
+
'[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, IntPtr pid);',
|
|
4546
|
+
'[DllImport("user32.dll")] public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);',
|
|
4547
|
+
'[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);',
|
|
4548
|
+
'[DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr h);',
|
|
4549
|
+
'[DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();',
|
|
4550
|
+
"static bool Raise(IntPtr h) {",
|
|
4551
|
+
" IntPtr fg = GetForegroundWindow();",
|
|
4552
|
+
" uint fgThread = GetWindowThreadProcessId(fg, IntPtr.Zero);",
|
|
4553
|
+
" uint me = GetCurrentThreadId();",
|
|
4554
|
+
" bool attached = false;",
|
|
4555
|
+
" if (fgThread != 0 && fgThread != me) { attached = AttachThreadInput(me, fgThread, true); }",
|
|
4556
|
+
" bool okPos = SetWindowPos(h, new IntPtr(-1), 0, 0, 0, 0, 3);",
|
|
4557
|
+
" BringWindowToTop(h);",
|
|
4558
|
+
" bool okFg = SetForegroundWindow(h);",
|
|
4559
|
+
" SwitchToThisWindow(h, true);",
|
|
4560
|
+
" if (attached) { AttachThreadInput(me, fgThread, false); }",
|
|
4561
|
+
" return okPos && okFg;",
|
|
4562
|
+
"}",
|
|
4563
|
+
"public static void RaiseSoon(IntPtr owner) {",
|
|
4564
|
+
" System.Threading.Thread t = new System.Threading.Thread(delegate() {",
|
|
4565
|
+
' string note = "dialog window never appeared";',
|
|
4566
|
+
" for (int i = 0; i < 400; i++) {",
|
|
4567
|
+
" System.Threading.Thread.Sleep(30);",
|
|
4568
|
+
" IntPtr dlg = GetWindow(owner, 6);",
|
|
4569
|
+
" if (dlg == IntPtr.Zero) { continue; }",
|
|
4570
|
+
" bool ok = false;",
|
|
4571
|
+
" for (int k = 0; k < 4; k++) {",
|
|
4572
|
+
" Raise(owner);",
|
|
4573
|
+
" ok = Raise(dlg) || ok;",
|
|
4574
|
+
" System.Threading.Thread.Sleep(120);",
|
|
4575
|
+
" }",
|
|
4576
|
+
' note = "raised hwnd=" + dlg.ToInt64() + " ok=" + ok + " dlgEx=0x" + GetWindowLong(dlg, -20).ToString("X") + " ownerEx=0x" + GetWindowLong(owner, -20).ToString("X") + " isFg=" + (GetForegroundWindow() == dlg);',
|
|
4577
|
+
" break;",
|
|
4578
|
+
" }",
|
|
4579
|
+
' string dbg = System.Environment.GetEnvironmentVariable("EPOCH_PICK_DEBUG");',
|
|
4580
|
+
" if (!string.IsNullOrEmpty(dbg)) {",
|
|
4581
|
+
' try { System.IO.File.AppendAllText(System.IO.Path.Combine(System.IO.Path.GetTempPath(), "epoch-pick-debug.log"), System.DateTime.Now.ToString("HH:mm:ss.fff") + " " + note + System.Environment.NewLine); } catch { }',
|
|
4582
|
+
" }",
|
|
4583
|
+
" });",
|
|
4584
|
+
" t.IsBackground = true;",
|
|
4585
|
+
" t.Start();",
|
|
4586
|
+
"}",
|
|
4587
|
+
"'@",
|
|
4588
|
+
"[EpochPick.Win]::RaiseSoon($epochOwner.Handle)",
|
|
4589
|
+
"} catch { }",
|
|
4590
|
+
"$d = New-Object System.Windows.Forms.FolderBrowserDialog",
|
|
4591
|
+
`$d.Description = ${asPowerShellString(prompt)}`,
|
|
4592
|
+
"$d.ShowNewFolderButton = $true",
|
|
4593
|
+
"$epochResult = $d.ShowDialog($epochOwner)",
|
|
4594
|
+
"$epochOwner.Close()",
|
|
4595
|
+
"if ($epochResult -ne [System.Windows.Forms.DialogResult]::OK) { exit 1 }",
|
|
4596
|
+
"[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($d.SelectedPath))"
|
|
4597
|
+
].join("\n");
|
|
4371
4598
|
}
|
|
4372
|
-
function
|
|
4373
|
-
|
|
4374
|
-
if (root === null) return;
|
|
4375
|
-
const path = singlePath(res, rawUrl, lang);
|
|
4376
|
-
if (path === null) return;
|
|
4377
|
-
const stat3 = ctx.runtime.workspaceFiles.statFile(path, root);
|
|
4378
|
-
if (!stat3.ok) {
|
|
4379
|
-
const refusal = toWireRefusal(stat3.reason);
|
|
4380
|
-
return sendError(res, statusFor(refusal), refusal, describeRefusal(refusal, lang));
|
|
4381
|
-
}
|
|
4382
|
-
const known = VIEWABLE.get(extname(stat3.rel).toLowerCase());
|
|
4383
|
-
const inline = known !== void 0 && stat3.kind === "binary";
|
|
4384
|
-
const headers = {
|
|
4385
|
-
"Content-Type": inline ? known.mime : "application/octet-stream",
|
|
4386
|
-
"X-Content-Type-Options": "nosniff",
|
|
4387
|
-
"Content-Disposition": inline ? "inline" : `attachment; filename*=UTF-8''${encodeURIComponent(basenameOf(stat3.rel))}`
|
|
4388
|
-
};
|
|
4389
|
-
if (inline && known.kind === "pdf") headers["Content-Security-Policy"] = "sandbox";
|
|
4390
|
-
if (headOnly) {
|
|
4391
|
-
return sendEmpty(res, 200, {
|
|
4392
|
-
...headers,
|
|
4393
|
-
"Content-Length": String(stat3.bytes)
|
|
4394
|
-
});
|
|
4395
|
-
}
|
|
4396
|
-
sendFile(res, stat3.absPath, headers);
|
|
4599
|
+
function asAppleScriptString(text) {
|
|
4600
|
+
return `"${text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
4397
4601
|
}
|
|
4398
|
-
function
|
|
4399
|
-
|
|
4400
|
-
return cut < 0 ? rel : rel.slice(cut + 1);
|
|
4602
|
+
function asPowerShellString(text) {
|
|
4603
|
+
return `'${text.replace(/'/g, "''")}'`;
|
|
4401
4604
|
}
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
if (
|
|
4405
|
-
|
|
4406
|
-
}
|
|
4407
|
-
if (refusal === "not-same-machine") return t("web.file_open_not_same_machine", void 0, lang);
|
|
4408
|
-
if (refusal === "launch-failed") return t("web.file_open_launch_failed", void 0, lang);
|
|
4409
|
-
return describeRefusal(refusal, lang);
|
|
4605
|
+
function decodeNativePick(platform, stdout) {
|
|
4606
|
+
const raw = stdout.trim();
|
|
4607
|
+
if (platform !== "win32") return raw;
|
|
4608
|
+
return Buffer.from(raw, "base64").toString("utf-8").trim();
|
|
4410
4609
|
}
|
|
4411
|
-
function
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
message: describeOpenRefusal(refusal, lang)
|
|
4416
|
-
});
|
|
4610
|
+
function isPickCanceled(platform, code, stderr) {
|
|
4611
|
+
if (code !== 1) return false;
|
|
4612
|
+
if (platform === "win32") return true;
|
|
4613
|
+
return /-128|user canceled/i.test(stderr);
|
|
4417
4614
|
}
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4615
|
+
var inFlight = false;
|
|
4616
|
+
async function pickNativeDirectory(opts) {
|
|
4617
|
+
const platform = opts.platform ?? process.platform;
|
|
4618
|
+
const candidates = nativePickerCommands(platform, opts.prompt);
|
|
4619
|
+
if (!candidates) {
|
|
4620
|
+
return { ok: false, reason: "unsupported", message: `unsupported platform: ${platform}` };
|
|
4421
4621
|
}
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
const body = await readJsonBody(req);
|
|
4425
|
-
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
4426
|
-
const payload = asRecord(body.value);
|
|
4427
|
-
const path = typeof payload["path"] === "string" ? payload["path"].trim() : "";
|
|
4428
|
-
if (path === "") {
|
|
4429
|
-
return sendError(res, 400, "missing-path", t("web.file_view_missing_path", void 0, lang));
|
|
4622
|
+
if (inFlight) {
|
|
4623
|
+
return { ok: false, reason: "busy", message: "a directory picker is already open" };
|
|
4430
4624
|
}
|
|
4431
|
-
const
|
|
4432
|
-
|
|
4625
|
+
const run = opts.runner ?? execFileRunner;
|
|
4626
|
+
inFlight = true;
|
|
4433
4627
|
try {
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4628
|
+
let last = "";
|
|
4629
|
+
for (const [index, candidate] of candidates.entries()) {
|
|
4630
|
+
const attempt = await run(candidate.command, candidate.args, opts.signal);
|
|
4631
|
+
if (attempt.missing) {
|
|
4632
|
+
last = attempt.message || `${candidate.command} not found`;
|
|
4633
|
+
if (index < candidates.length - 1) continue;
|
|
4634
|
+
return { ok: false, reason: "failed", message: last };
|
|
4635
|
+
}
|
|
4636
|
+
if (!attempt.code) {
|
|
4637
|
+
const path = decodeNativePick(platform, attempt.stdout);
|
|
4638
|
+
return path ? { ok: true, path } : { ok: false, reason: "failed", message: "picker returned an empty path" };
|
|
4639
|
+
}
|
|
4640
|
+
if (isPickCanceled(platform, attempt.code, attempt.stderr)) {
|
|
4641
|
+
return { ok: true, canceled: true };
|
|
4642
|
+
}
|
|
4643
|
+
return { ok: false, reason: "failed", message: attempt.stderr.trim() || attempt.message };
|
|
4644
|
+
}
|
|
4645
|
+
return { ok: false, reason: "failed", message: last };
|
|
4646
|
+
} finally {
|
|
4647
|
+
inFlight = false;
|
|
4444
4648
|
}
|
|
4445
|
-
sendJson(res, 200, { ok: true });
|
|
4446
4649
|
}
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4650
|
+
var execFileRunner = (command, args, signal) => new Promise((done) => {
|
|
4651
|
+
guardSpawn(
|
|
4652
|
+
() => execFile(
|
|
4653
|
+
command,
|
|
4654
|
+
[...args],
|
|
4655
|
+
{
|
|
4656
|
+
...signal ? { signal } : {},
|
|
4657
|
+
encoding: "utf-8",
|
|
4658
|
+
windowsHide: true,
|
|
4659
|
+
maxBuffer: 1024 * 1024
|
|
4660
|
+
},
|
|
4661
|
+
(error, stdout, stderr) => {
|
|
4662
|
+
const err = error;
|
|
4663
|
+
done({
|
|
4664
|
+
code: err?.code ?? 0,
|
|
4665
|
+
stdout,
|
|
4666
|
+
stderr,
|
|
4667
|
+
missing: err?.code === "ENOENT",
|
|
4668
|
+
message: err?.message ?? ""
|
|
4669
|
+
});
|
|
4670
|
+
}
|
|
4671
|
+
),
|
|
4672
|
+
(err) => done({
|
|
4673
|
+
code: err.code ?? 1,
|
|
4674
|
+
stdout: "",
|
|
4675
|
+
stderr: "",
|
|
4676
|
+
missing: err.code === "ENOENT" || err.code === "ENOTDIR",
|
|
4677
|
+
message: `${err.message} (command=${command})`
|
|
4678
|
+
})
|
|
4679
|
+
);
|
|
4680
|
+
});
|
|
4681
|
+
// src/workspace/dirs.ts
|
|
4682
|
+
function browseWorkspaceDirs(ctx, res, rawUrl, lang) {
|
|
4683
|
+
const query = new URL(rawUrl ?? "/", "http://127.0.0.1").searchParams;
|
|
4684
|
+
const path = dirsQuery(query) ?? atQuery(query);
|
|
4685
|
+
if (path === void 0) {
|
|
4686
|
+
return sendJson(res, 200, workspaceAnchors(ctx.runtime.workspaces));
|
|
4461
4687
|
}
|
|
4462
|
-
workspaces
|
|
4463
|
-
|
|
4464
|
-
known: toWireTrustList(workspaces.known(), workspaces, current ?? null)
|
|
4465
|
-
});
|
|
4466
|
-
}
|
|
4467
|
-
var GIT_TIMEOUT_MS = 1e4;
|
|
4468
|
-
var GIT_MAX_BUFFER = 16 * 1024 * 1024;
|
|
4469
|
-
function runGit(cwd, args) {
|
|
4470
|
-
if (!canProbeGit()) return Promise.resolve({ ok: false, error: "git unavailable" });
|
|
4471
|
-
return new Promise((resolve4) => {
|
|
4472
|
-
guardSpawn(
|
|
4473
|
-
() => execFile(
|
|
4474
|
-
"git",
|
|
4475
|
-
[...args],
|
|
4476
|
-
{ cwd, timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_MAX_BUFFER, encoding: "buffer" },
|
|
4477
|
-
(err, stdout) => {
|
|
4478
|
-
if (err) return resolve4({ ok: false, error: err.message });
|
|
4479
|
-
resolve4({ ok: true, stdout });
|
|
4480
|
-
}
|
|
4481
|
-
),
|
|
4482
|
-
(err) => resolve4({ ok: false, error: `${err.message} (cwd=${cwd})` })
|
|
4483
|
-
);
|
|
4688
|
+
const outcome = listWorkspaceDirs(ctx.runtime.workspaces, path, {
|
|
4689
|
+
hidden: hiddenQuery(query)
|
|
4484
4690
|
});
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
}
|
|
4492
|
-
|
|
4493
|
-
return (await runGit(root, ["rev-parse", "--verify", "--quiet", "HEAD"])).ok;
|
|
4494
|
-
}
|
|
4495
|
-
var NULL_SHA = /^0+$/;
|
|
4496
|
-
async function rawChanges(root) {
|
|
4497
|
-
const result = await runGit(root, ["diff", "--raw", "-z", "HEAD", "--"]);
|
|
4498
|
-
if (!result.ok) return [];
|
|
4499
|
-
return parseRawZ(result.stdout.toString("utf-8"));
|
|
4500
|
-
}
|
|
4501
|
-
function parseRawZ(text) {
|
|
4502
|
-
const tokens = text.split("\0");
|
|
4503
|
-
const changes = [];
|
|
4504
|
-
let i = 0;
|
|
4505
|
-
while (i < tokens.length) {
|
|
4506
|
-
const meta = tokens[i];
|
|
4507
|
-
if (meta === void 0 || !meta.startsWith(":")) {
|
|
4508
|
-
i += 1;
|
|
4509
|
-
continue;
|
|
4510
|
-
}
|
|
4511
|
-
const parts = meta.slice(1).split(" ");
|
|
4512
|
-
const oldSha = parts[2] ?? "";
|
|
4513
|
-
const status = (parts[4] ?? "").trim();
|
|
4514
|
-
const renamed = status.startsWith("R") || status.startsWith("C");
|
|
4515
|
-
const first = tokens[i + 1];
|
|
4516
|
-
const second = renamed ? tokens[i + 2] : void 0;
|
|
4517
|
-
i += renamed ? 3 : 2;
|
|
4518
|
-
if (first === void 0 || renamed && second === void 0) break;
|
|
4519
|
-
const path = renamed ? second : first;
|
|
4520
|
-
changes.push({
|
|
4521
|
-
status: status.charAt(0),
|
|
4522
|
-
path,
|
|
4523
|
-
...renamed ? { oldPath: first } : {},
|
|
4524
|
-
oldSha: NULL_SHA.test(oldSha) ? null : oldSha
|
|
4525
|
-
});
|
|
4691
|
+
if (outcome.ok) return sendJson(res, 200, outcome.view);
|
|
4692
|
+
if (outcome.reason === "denied") {
|
|
4693
|
+
return sendError(
|
|
4694
|
+
res,
|
|
4695
|
+
403,
|
|
4696
|
+
"workspace-denied",
|
|
4697
|
+
t("web.dirs_denied", { path: outcome.path }, lang)
|
|
4698
|
+
);
|
|
4526
4699
|
}
|
|
4527
|
-
|
|
4528
|
-
}
|
|
4529
|
-
async function treeBlobSizes(root) {
|
|
4530
|
-
const sizes = /* @__PURE__ */ new Map();
|
|
4531
|
-
const result = await runGit(root, ["ls-tree", "-r", "-l", "-z", "HEAD"]);
|
|
4532
|
-
if (!result.ok) return sizes;
|
|
4533
|
-
for (const record of splitZ(result.stdout.toString("utf-8"))) {
|
|
4534
|
-
const tab = record.indexOf(" ");
|
|
4535
|
-
if (tab < 0) continue;
|
|
4536
|
-
const columns = record.slice(0, tab).split(/\s+/).filter(Boolean);
|
|
4537
|
-
const sha = columns[2];
|
|
4538
|
-
const size = Number(columns[3]);
|
|
4539
|
-
if (columns[1] === "blob" && sha && Number.isFinite(size)) sizes.set(sha, size);
|
|
4700
|
+
if (outcome.reason === "missing") {
|
|
4701
|
+
return sendError(res, 404, "not-found", t("web.dirs_missing", { path: outcome.path }, lang));
|
|
4540
4702
|
}
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
}
|
|
4548
|
-
async function trackedFiles(root) {
|
|
4549
|
-
const result = await runGit(root, ["ls-files", "-z"]);
|
|
4550
|
-
if (!result.ok) return [];
|
|
4551
|
-
return splitZ(result.stdout.toString("utf-8"));
|
|
4703
|
+
sendError(
|
|
4704
|
+
res,
|
|
4705
|
+
400,
|
|
4706
|
+
"bad-workspace",
|
|
4707
|
+
outcome.reason === "not-absolute" ? t("web.dirs_not_absolute", { path: outcome.path }, lang) : t("web.dirs_not_a_directory", { path: outcome.path }, lang)
|
|
4708
|
+
);
|
|
4552
4709
|
}
|
|
4553
|
-
|
|
4554
|
-
const
|
|
4555
|
-
return
|
|
4710
|
+
function dirsQuery(query) {
|
|
4711
|
+
const value = query.get("path")?.trim();
|
|
4712
|
+
return value ? value : void 0;
|
|
4556
4713
|
}
|
|
4557
|
-
function
|
|
4558
|
-
return
|
|
4714
|
+
function atQuery(query) {
|
|
4715
|
+
return query.get("at")?.trim().toLowerCase() === "home" ? workspaceHome() : void 0;
|
|
4559
4716
|
}
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
var BINARY_SNIFF_BYTES = 8e3;
|
|
4564
|
-
var ABSENT = { kind: "absent" };
|
|
4565
|
-
async function collectWorkspaceDiff(opts) {
|
|
4566
|
-
const root = await repoRoot(opts.workDir);
|
|
4567
|
-
return root === null ? await checkpointDiff(opts) : await gitDiff(root);
|
|
4717
|
+
function hiddenQuery(query) {
|
|
4718
|
+
const value = query.get("hidden")?.trim().toLowerCase();
|
|
4719
|
+
return value === "1" || value === "true";
|
|
4568
4720
|
}
|
|
4569
|
-
async function
|
|
4570
|
-
const
|
|
4571
|
-
|
|
4572
|
-
const
|
|
4573
|
-
const
|
|
4574
|
-
|
|
4575
|
-
if (
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4721
|
+
async function createWorkspaceDir(req, res, lang) {
|
|
4722
|
+
const body = await readJsonBody(req);
|
|
4723
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
4724
|
+
const payload = asRecord(body.value);
|
|
4725
|
+
const parent = typeof payload["parent"] === "string" ? payload["parent"].trim() : "";
|
|
4726
|
+
const name = typeof payload["name"] === "string" ? payload["name"].trim() : "";
|
|
4727
|
+
if (parent.length === 0 || !isAbsolute(parent)) {
|
|
4728
|
+
return sendError(
|
|
4729
|
+
res,
|
|
4730
|
+
400,
|
|
4731
|
+
"bad-workspace",
|
|
4732
|
+
t("web.mkdir_parent_not_absolute", { parent }, lang)
|
|
4733
|
+
);
|
|
4582
4734
|
}
|
|
4583
|
-
|
|
4584
|
-
}
|
|
4585
|
-
async function collectGitPaths(root) {
|
|
4586
|
-
const byPath = /* @__PURE__ */ new Map();
|
|
4587
|
-
if (await hasHead(root)) {
|
|
4588
|
-
for (const change of await rawChanges(root)) {
|
|
4589
|
-
if (change.oldPath !== void 0) {
|
|
4590
|
-
byPath.set(change.oldPath, { path: change.oldPath, oldSha: change.oldSha });
|
|
4591
|
-
}
|
|
4592
|
-
byPath.set(change.path, {
|
|
4593
|
-
path: change.path,
|
|
4594
|
-
oldSha: change.oldPath === void 0 ? change.oldSha : null
|
|
4595
|
-
});
|
|
4596
|
-
}
|
|
4597
|
-
} else {
|
|
4598
|
-
for (const path of await trackedFiles(root)) byPath.set(path, { path, oldSha: null });
|
|
4735
|
+
if (!isSingleSegment(name)) {
|
|
4736
|
+
return sendError(res, 400, "bad-workspace", t("web.mkdir_bad_name", { name }, lang));
|
|
4599
4737
|
}
|
|
4600
|
-
|
|
4601
|
-
|
|
4738
|
+
const path = resolve(parent, name);
|
|
4739
|
+
try {
|
|
4740
|
+
mkdirSync(path);
|
|
4741
|
+
} catch (cause) {
|
|
4742
|
+
return sendError(res, ...mkdirFailure(cause, path, lang));
|
|
4602
4743
|
}
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4744
|
+
sendJson(res, 200, {
|
|
4745
|
+
entry: {
|
|
4746
|
+
name,
|
|
4747
|
+
path,
|
|
4748
|
+
readable: true,
|
|
4749
|
+
known: false,
|
|
4750
|
+
group: "child"
|
|
4751
|
+
}
|
|
4752
|
+
});
|
|
4612
4753
|
}
|
|
4613
|
-
async function
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
if (paths.length === 0) {
|
|
4617
|
-
notes.push(opts.checkpoints === null ? "no-workspace-history" : "no-changes");
|
|
4618
|
-
return { source: "none", root: opts.workDir, files: [], truncated: false, notes };
|
|
4754
|
+
async function pickWorkspaceDir(ctx, req, res, lang) {
|
|
4755
|
+
if (!ctx.nativeDirPicker) {
|
|
4756
|
+
return sendError(res, 403, "native-picker-unavailable", t("web.pick_unavailable", {}, lang));
|
|
4619
4757
|
}
|
|
4620
|
-
const
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4758
|
+
const abort = new AbortController();
|
|
4759
|
+
req.on("close", () => abort.abort());
|
|
4760
|
+
const outcome = await pickNativeDirectory({
|
|
4761
|
+
prompt: t("web.pick_prompt", {}, lang),
|
|
4762
|
+
signal: abort.signal
|
|
4763
|
+
});
|
|
4764
|
+
if (res.writableEnded || abort.signal.aborted) return;
|
|
4765
|
+
if (outcome.ok) {
|
|
4766
|
+
return sendJson(
|
|
4767
|
+
res,
|
|
4768
|
+
200,
|
|
4769
|
+
outcome.path === void 0 ? { canceled: true } : { path: outcome.path }
|
|
4631
4770
|
);
|
|
4632
4771
|
}
|
|
4633
|
-
|
|
4634
|
-
}
|
|
4635
|
-
async function checkpointPaths(checkpoints) {
|
|
4636
|
-
if (!checkpoints) return [];
|
|
4637
|
-
const paths = /* @__PURE__ */ new Set();
|
|
4638
|
-
try {
|
|
4639
|
-
for (const summary of await checkpoints.list()) {
|
|
4640
|
-
const preview = await checkpoints.preview(summary.turnIndex);
|
|
4641
|
-
if (!preview) continue;
|
|
4642
|
-
for (const plan of [...preview.files, ...preview.conflicts]) {
|
|
4643
|
-
if (plan.action !== "skip") paths.add(plan.path);
|
|
4644
|
-
}
|
|
4645
|
-
}
|
|
4646
|
-
} catch {
|
|
4772
|
+
if (outcome.reason === "busy") {
|
|
4773
|
+
return sendError(res, 409, "native-picker-busy", t("web.pick_busy", {}, lang));
|
|
4647
4774
|
}
|
|
4648
|
-
|
|
4775
|
+
sendError(
|
|
4776
|
+
res,
|
|
4777
|
+
500,
|
|
4778
|
+
"native-picker-failed",
|
|
4779
|
+
t("web.pick_failed", { detail: outcome.message }, lang)
|
|
4780
|
+
);
|
|
4649
4781
|
}
|
|
4650
|
-
function
|
|
4651
|
-
|
|
4652
|
-
if (
|
|
4653
|
-
return
|
|
4782
|
+
function isSingleSegment(name) {
|
|
4783
|
+
if (name.length === 0) return false;
|
|
4784
|
+
if (name === "." || name === "..") return false;
|
|
4785
|
+
return !name.includes("/") && !name.includes(sep);
|
|
4654
4786
|
}
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
return { kind: "content", data: await readFile(absolute) };
|
|
4661
|
-
} catch {
|
|
4662
|
-
return ABSENT;
|
|
4787
|
+
function mkdirFailure(cause, path, lang) {
|
|
4788
|
+
const code = cause?.code;
|
|
4789
|
+
if (code === "EEXIST") return [409, "workspace-exists", t("web.mkdir_exists", { path }, lang)];
|
|
4790
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
4791
|
+
return [403, "workspace-denied", t("web.mkdir_denied", { path }, lang)];
|
|
4663
4792
|
}
|
|
4793
|
+
if (code === "ENOENT") return [404, "not-found", t("web.mkdir_parent_missing", { path }, lang)];
|
|
4794
|
+
return [500, "internal", t("web.mkdir_failed", { path }, lang)];
|
|
4664
4795
|
}
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
}
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
}
|
|
4796
|
+
var MAX_TEXT_BYTES = 2 * 1024 * 1024;
|
|
4797
|
+
var VIEWABLE = /* @__PURE__ */ new Map([
|
|
4798
|
+
[".png", { kind: "image", mime: "image/png" }],
|
|
4799
|
+
[".jpg", { kind: "image", mime: "image/jpeg" }],
|
|
4800
|
+
[".jpeg", { kind: "image", mime: "image/jpeg" }],
|
|
4801
|
+
[".gif", { kind: "image", mime: "image/gif" }],
|
|
4802
|
+
[".webp", { kind: "image", mime: "image/webp" }],
|
|
4803
|
+
[".bmp", { kind: "image", mime: "image/bmp" }],
|
|
4804
|
+
[".ico", { kind: "image", mime: "image/x-icon" }],
|
|
4805
|
+
[".avif", { kind: "image", mime: "image/avif" }],
|
|
4806
|
+
[".pdf", { kind: "pdf", mime: "application/pdf" }],
|
|
4807
|
+
[".mp3", { kind: "audio", mime: "audio/mpeg" }],
|
|
4808
|
+
[".wav", { kind: "audio", mime: "audio/wav" }],
|
|
4809
|
+
[".ogg", { kind: "audio", mime: "audio/ogg" }],
|
|
4810
|
+
[".m4a", { kind: "audio", mime: "audio/mp4" }],
|
|
4811
|
+
[".flac", { kind: "audio", mime: "audio/flac" }],
|
|
4812
|
+
[".mp4", { kind: "video", mime: "video/mp4" }],
|
|
4813
|
+
[".webm", { kind: "video", mime: "video/webm" }],
|
|
4814
|
+
[".mov", { kind: "video", mime: "video/quicktime" }]
|
|
4815
|
+
]);
|
|
4816
|
+
function kindOf(stat3) {
|
|
4817
|
+
if (stat3.kind === "text") return "text";
|
|
4818
|
+
return VIEWABLE.get(extname(stat3.rel).toLowerCase())?.kind ?? "opaque";
|
|
4682
4819
|
}
|
|
4683
|
-
function
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4820
|
+
function toWireRefusal(reason) {
|
|
4821
|
+
return reason;
|
|
4822
|
+
}
|
|
4823
|
+
function statusFor(refusal) {
|
|
4824
|
+
if (refusal === "denied") return 403;
|
|
4825
|
+
if (refusal === "missing") return 404;
|
|
4826
|
+
if (refusal === "too-large") return 413;
|
|
4827
|
+
return refusal === "not-a-file" ? 400 : 503;
|
|
4828
|
+
}
|
|
4829
|
+
function describeRefusal(refusal, lang) {
|
|
4830
|
+
if (refusal === "denied") return t("web.file_view_denied", void 0, lang);
|
|
4831
|
+
if (refusal === "missing") return t("web.file_view_missing", void 0, lang);
|
|
4832
|
+
if (refusal === "not-a-file") return t("web.file_view_not_a_file", void 0, lang);
|
|
4833
|
+
if (refusal === "too-large") {
|
|
4834
|
+
return t("web.file_view_too_large", { max: String(MAX_TEXT_BYTES) }, lang);
|
|
4835
|
+
}
|
|
4836
|
+
return t("web.file_view_unreadable", void 0, lang);
|
|
4687
4837
|
}
|
|
4688
|
-
function
|
|
4689
|
-
|
|
4838
|
+
function resolveViewRoot(ctx, res, sessionId, lang) {
|
|
4839
|
+
if (!ctx.hub.has(sessionId) && !ctx.runtime.sessions?.get(sessionId)) {
|
|
4840
|
+
sendUnknownSession(res, sessionId);
|
|
4841
|
+
return null;
|
|
4842
|
+
}
|
|
4843
|
+
const root = workspaceRootOf(ctx, sessionId);
|
|
4844
|
+
if (!root) {
|
|
4845
|
+
sendError(res, 409, "no-workspace", t("web.file_view_no_workspace", void 0, lang));
|
|
4846
|
+
return null;
|
|
4847
|
+
}
|
|
4848
|
+
return root;
|
|
4690
4849
|
}
|
|
4691
|
-
function
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4850
|
+
function singlePath(res, rawUrl, lang) {
|
|
4851
|
+
const params = new URLSearchParams((rawUrl ?? "").split("?")[1] ?? "");
|
|
4852
|
+
const path = params.get("path")?.trim();
|
|
4853
|
+
if (!path) {
|
|
4854
|
+
sendError(res, 400, "missing-path", t("web.file_view_missing_path", void 0, lang));
|
|
4855
|
+
return null;
|
|
4856
|
+
}
|
|
4857
|
+
return path;
|
|
4695
4858
|
}
|
|
4696
|
-
function
|
|
4697
|
-
|
|
4859
|
+
function statFiles(ctx, res, sessionId, rawUrl, lang) {
|
|
4860
|
+
const root = resolveViewRoot(ctx, res, sessionId, lang);
|
|
4861
|
+
if (root === null) return;
|
|
4862
|
+
const params = new URLSearchParams((rawUrl ?? "").split("?")[1] ?? "");
|
|
4863
|
+
const asked = params.getAll("path").filter((p) => p.trim() !== "");
|
|
4864
|
+
const taken = asked.slice(0, WIRE_FILE_STAT_MAX);
|
|
4865
|
+
const files = taken.map((path) => {
|
|
4866
|
+
const stat3 = ctx.runtime.workspaceFiles.statFile(path, root);
|
|
4867
|
+
if (!stat3.ok) return { path, ok: false, refusal: toWireRefusal(stat3.reason) };
|
|
4868
|
+
return {
|
|
4869
|
+
path,
|
|
4870
|
+
ok: true,
|
|
4871
|
+
kind: kindOf(stat3),
|
|
4872
|
+
bytes: stat3.bytes,
|
|
4873
|
+
mtimeMs: stat3.mtimeMs,
|
|
4874
|
+
openable: ctx.sameMachine && isSystemOpenable(stat3.rel)
|
|
4875
|
+
};
|
|
4876
|
+
});
|
|
4877
|
+
sendJson(res, 200, {
|
|
4878
|
+
files,
|
|
4879
|
+
overLimit: asked.length - taken.length,
|
|
4880
|
+
root
|
|
4881
|
+
});
|
|
4698
4882
|
}
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
const
|
|
4705
|
-
if (!
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
return sendError(res, 400, "bad-request", t("web.workspace_defer_with_root", void 0, lang));
|
|
4883
|
+
function readFileText(ctx, res, sessionId, rawUrl, lang) {
|
|
4884
|
+
const root = resolveViewRoot(ctx, res, sessionId, lang);
|
|
4885
|
+
if (root === null) return;
|
|
4886
|
+
const path = singlePath(res, rawUrl, lang);
|
|
4887
|
+
if (path === null) return;
|
|
4888
|
+
const stat3 = ctx.runtime.workspaceFiles.statFile(path, root);
|
|
4889
|
+
if (!stat3.ok) {
|
|
4890
|
+
const refusal = toWireRefusal(stat3.reason);
|
|
4891
|
+
return sendError(res, statusFor(refusal), refusal, describeRefusal(refusal, lang));
|
|
4709
4892
|
}
|
|
4710
|
-
if (
|
|
4711
|
-
return sendError(
|
|
4712
|
-
res,
|
|
4713
|
-
400,
|
|
4714
|
-
"bad-request",
|
|
4715
|
-
t("web.workspace_defer_needs_create", void 0, lang)
|
|
4716
|
-
);
|
|
4893
|
+
if (stat3.kind !== "text") {
|
|
4894
|
+
return sendError(res, 415, "not-text", t("web.file_view_not_text", void 0, lang));
|
|
4717
4895
|
}
|
|
4718
|
-
if (
|
|
4719
|
-
|
|
4720
|
-
const bound = ctx.runtime.workspaces.of(sessionId);
|
|
4721
|
-
return sendJson(res, 200, {
|
|
4722
|
-
sessionId,
|
|
4723
|
-
created: false,
|
|
4724
|
-
workspace: bound ? toWireSessionWorkspace(bound) : null
|
|
4725
|
-
});
|
|
4896
|
+
if (stat3.bytes > MAX_TEXT_BYTES) {
|
|
4897
|
+
return sendError(res, 413, "too-large", describeRefusal("too-large", lang));
|
|
4726
4898
|
}
|
|
4727
|
-
const
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
reuse: payload["create"] !== true,
|
|
4732
|
-
...role === "" ? {} : { role }
|
|
4733
|
-
});
|
|
4734
|
-
if (!outcome.ok) {
|
|
4735
|
-
if (outcome.reason === "unavailable") {
|
|
4736
|
-
const mapped2 = unavailableToHttp(lang);
|
|
4737
|
-
return sendError(res, mapped2.status, mapped2.code, mapped2.message);
|
|
4738
|
-
}
|
|
4739
|
-
if (outcome.reason === "unknown-role") {
|
|
4740
|
-
const mapped2 = unknownRoleToHttp(outcome.detail, lang);
|
|
4741
|
-
return sendError(res, mapped2.status, mapped2.code, mapped2.message);
|
|
4742
|
-
}
|
|
4743
|
-
const mapped = bindFailureToHttp(outcome, lang);
|
|
4744
|
-
return sendError(res, mapped.status, mapped.code, mapped.message);
|
|
4899
|
+
const out = ctx.runtime.workspaceFiles.readFile(stat3.rel, root);
|
|
4900
|
+
if (!out.ok) {
|
|
4901
|
+
const refusal = out.reason === "binary" ? "unreadable" : out.reason;
|
|
4902
|
+
return sendError(res, statusFor(refusal), refusal, describeRefusal(refusal, lang));
|
|
4745
4903
|
}
|
|
4746
|
-
ctx.hub.register(outcome.live.session);
|
|
4747
|
-
if (defer) ctx.runtime.workspaces.defer(outcome.live.sessionId);
|
|
4748
|
-
if (outcome.created) ctx.hub.notifySessionsChanged(outcome.live.sessionId);
|
|
4749
4904
|
sendJson(res, 200, {
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4905
|
+
path,
|
|
4906
|
+
text: out.text,
|
|
4907
|
+
bytes: out.bytes
|
|
4753
4908
|
});
|
|
4754
4909
|
}
|
|
4755
|
-
function
|
|
4756
|
-
|
|
4757
|
-
|
|
4910
|
+
function readFileBytes(ctx, res, sessionId, rawUrl, headOnly, lang) {
|
|
4911
|
+
const root = resolveViewRoot(ctx, res, sessionId, lang);
|
|
4912
|
+
if (root === null) return;
|
|
4913
|
+
const path = singlePath(res, rawUrl, lang);
|
|
4914
|
+
if (path === null) return;
|
|
4915
|
+
const stat3 = ctx.runtime.workspaceFiles.statFile(path, root);
|
|
4916
|
+
if (!stat3.ok) {
|
|
4917
|
+
const refusal = toWireRefusal(stat3.reason);
|
|
4918
|
+
return sendError(res, statusFor(refusal), refusal, describeRefusal(refusal, lang));
|
|
4758
4919
|
}
|
|
4759
|
-
const
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4920
|
+
const known = VIEWABLE.get(extname(stat3.rel).toLowerCase());
|
|
4921
|
+
const inline = known !== void 0 && stat3.kind === "binary";
|
|
4922
|
+
const headers = {
|
|
4923
|
+
"Content-Type": inline ? known.mime : "application/octet-stream",
|
|
4924
|
+
"X-Content-Type-Options": "nosniff",
|
|
4925
|
+
"Content-Disposition": inline ? "inline" : `attachment; filename*=UTF-8''${encodeURIComponent(basenameOf(stat3.rel))}`
|
|
4926
|
+
};
|
|
4927
|
+
if (inline && known.kind === "pdf") headers["Content-Security-Policy"] = "sandbox";
|
|
4928
|
+
if (headOnly) {
|
|
4929
|
+
return sendEmpty(res, 200, {
|
|
4930
|
+
...headers,
|
|
4931
|
+
"Content-Length": String(stat3.bytes)
|
|
4763
4932
|
});
|
|
4764
4933
|
}
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4934
|
+
sendFile(res, stat3.absPath, headers);
|
|
4935
|
+
}
|
|
4936
|
+
function basenameOf(rel) {
|
|
4937
|
+
const cut = rel.lastIndexOf("/");
|
|
4938
|
+
return cut < 0 ? rel : rel.slice(cut + 1);
|
|
4939
|
+
}
|
|
4940
|
+
// src/workspace/file-open.ts
|
|
4941
|
+
function describeOpenRefusal(refusal, lang) {
|
|
4942
|
+
if (refusal === "extension-not-allowed") {
|
|
4943
|
+
return t("web.file_open_extension_not_allowed", void 0, lang);
|
|
4769
4944
|
}
|
|
4770
|
-
|
|
4945
|
+
if (refusal === "not-same-machine") return t("web.file_open_not_same_machine", void 0, lang);
|
|
4946
|
+
if (refusal === "launch-failed") return t("web.file_open_launch_failed", void 0, lang);
|
|
4947
|
+
return describeRefusal(refusal, lang);
|
|
4771
4948
|
}
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4949
|
+
function refuse(res, refusal, lang) {
|
|
4950
|
+
sendJson(res, 200, {
|
|
4951
|
+
ok: false,
|
|
4952
|
+
refusal,
|
|
4953
|
+
message: describeOpenRefusal(refusal, lang)
|
|
4954
|
+
});
|
|
4955
|
+
}
|
|
4956
|
+
async function openFileWithSystemApp(ctx, req, res, sessionId, lang) {
|
|
4957
|
+
if (!ctx.sameMachine) {
|
|
4958
|
+
return sendError(res, 403, "not-same-machine", describeOpenRefusal("not-same-machine", lang));
|
|
4775
4959
|
}
|
|
4960
|
+
const root = resolveViewRoot(ctx, res, sessionId, lang);
|
|
4961
|
+
if (root === null) return;
|
|
4776
4962
|
const body = await readJsonBody(req);
|
|
4777
4963
|
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
4778
|
-
const
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
return sendError(
|
|
4783
|
-
res,
|
|
4784
|
-
409,
|
|
4785
|
-
"workspace-locked",
|
|
4786
|
-
t("web.workspace_already_decided", void 0, lang)
|
|
4787
|
-
);
|
|
4964
|
+
const payload = asRecord(body.value);
|
|
4965
|
+
const path = typeof payload["path"] === "string" ? payload["path"].trim() : "";
|
|
4966
|
+
if (path === "") {
|
|
4967
|
+
return sendError(res, 400, "missing-path", t("web.file_view_missing_path", void 0, lang));
|
|
4788
4968
|
}
|
|
4789
|
-
const
|
|
4790
|
-
if (
|
|
4791
|
-
|
|
4792
|
-
|
|
4969
|
+
const plan = ctx.runtime.workspaceFiles.planSystemOpen(path, root);
|
|
4970
|
+
if (!plan.ok) return refuse(res, plan.reason, lang);
|
|
4971
|
+
try {
|
|
4972
|
+
const child = spawn(plan.command, [...plan.args], { stdio: "ignore", detached: true });
|
|
4973
|
+
await new Promise((done, fail) => {
|
|
4974
|
+
child.on("error", fail);
|
|
4975
|
+
child.on("spawn", () => {
|
|
4976
|
+
child.unref();
|
|
4977
|
+
done();
|
|
4978
|
+
});
|
|
4793
4979
|
});
|
|
4980
|
+
} catch {
|
|
4981
|
+
return refuse(res, "launch-failed", lang);
|
|
4794
4982
|
}
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4983
|
+
sendJson(res, 200, { ok: true });
|
|
4984
|
+
}
|
|
4985
|
+
async function forgetWorkspace(ctx, sessionId, req, res, lang) {
|
|
4986
|
+
const body = await readJsonBody(req, void 0, lang);
|
|
4987
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
4988
|
+
if (!conformsToWire(body.value, WIRE_FORGET_WORKSPACE_FIELDS)) {
|
|
4989
|
+
return sendError(res, 400, "bad-request", t("web.forget_bad_request", {}, lang));
|
|
4800
4990
|
}
|
|
4801
|
-
const
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
return sendError(res,
|
|
4991
|
+
const { root } = body.value;
|
|
4992
|
+
const workspaces = ctx.runtime.workspaces;
|
|
4993
|
+
if (!workspaces.known().some((w) => w.root === root)) {
|
|
4994
|
+
return sendError(res, 404, "unknown-workspace", t("web.forget_unknown", {}, lang));
|
|
4995
|
+
}
|
|
4996
|
+
const current = workspaces.of(sessionId)?.workspace.root;
|
|
4997
|
+
if (current !== void 0 && current === root) {
|
|
4998
|
+
return sendError(res, 409, "workspace-in-use", t("web.forget_in_use", {}, lang));
|
|
4805
4999
|
}
|
|
5000
|
+
workspaces.forget(root);
|
|
4806
5001
|
sendJson(res, 200, {
|
|
4807
|
-
|
|
5002
|
+
known: toWireTrustList(workspaces.known(), workspaces, current ?? null)
|
|
4808
5003
|
});
|
|
4809
5004
|
}
|
|
4810
|
-
async function sessionDiff(ctx, res, sessionId, _lang) {
|
|
4811
|
-
if (!ctx.hub.has(sessionId) && !ctx.runtime.sessions?.get(sessionId)) {
|
|
4812
|
-
return sendUnknownSession(res, sessionId);
|
|
4813
|
-
}
|
|
4814
|
-
const bound = ctx.runtime.workspaces.of(sessionId);
|
|
4815
|
-
if (!bound) {
|
|
4816
|
-
return sendJson(res, 200, {
|
|
4817
|
-
source: "none",
|
|
4818
|
-
root: "",
|
|
4819
|
-
files: [],
|
|
4820
|
-
truncated: false,
|
|
4821
|
-
notes: ["no-workspace"]
|
|
4822
|
-
});
|
|
4823
|
-
}
|
|
4824
|
-
sendJson(
|
|
4825
|
-
res,
|
|
4826
|
-
200,
|
|
4827
|
-
await collectWorkspaceDiff({
|
|
4828
|
-
workDir: bound.workspace.root,
|
|
4829
|
-
checkpoints: checkpointsOf(ctx, sessionId)
|
|
4830
|
-
})
|
|
4831
|
-
);
|
|
4832
|
-
}
|
|
4833
5005
|
function isKnownRoot(ctx, sessionId, root) {
|
|
4834
5006
|
if (ctx.runtime.workspaces.of(sessionId)?.workspace.root === root) return true;
|
|
4835
5007
|
return ctx.runtime.workspaces.known().some((w) => w.root === root);
|
|
@@ -4929,6 +5101,15 @@ async function handleApi(opts, req, res, method, segments, lang) {
|
|
|
4929
5101
|
if (resource === "last-session" && first === void 0 && method === "PUT") {
|
|
4930
5102
|
return putLastSession(ctx, req, res, lang);
|
|
4931
5103
|
}
|
|
5104
|
+
if (resource === "host" && first === "compose" && second === void 0 && method === "POST") {
|
|
5105
|
+
return postHostCompose(ctx, req, res, lang);
|
|
5106
|
+
}
|
|
5107
|
+
if (resource === "host" && first === "compose" && second === void 0 && isRead(method)) {
|
|
5108
|
+
return getHostCompose(ctx, res);
|
|
5109
|
+
}
|
|
5110
|
+
if (resource === "host" && first === "compose" && second !== void 0 && detail === "take" && method === "POST") {
|
|
5111
|
+
return takeHostCompose(ctx, res, second);
|
|
5112
|
+
}
|
|
4932
5113
|
if (resource === "sessions") {
|
|
4933
5114
|
return handleSessions(opts, req, res, method, first, second, detail, lang);
|
|
4934
5115
|
}
|
|
@@ -5270,6 +5451,7 @@ async function createWebServer(opts) {
|
|
|
5270
5451
|
const unsubscribeTasks = observeBackgroundTasks((sessionId) => {
|
|
5271
5452
|
hub.notifyTasksChanged(sessionId);
|
|
5272
5453
|
});
|
|
5454
|
+
const unsubscribePlugins = opts.runtime.plugins?.onChange(() => hub.notifyPluginsChanged()) ?? (() => void 0);
|
|
5273
5455
|
let handle = null;
|
|
5274
5456
|
const server = createServer((req, res) => {
|
|
5275
5457
|
if (handle) return handle(req, res);
|
|
@@ -5280,6 +5462,7 @@ async function createWebServer(opts) {
|
|
|
5280
5462
|
const listened = await listen(server, binding.host, binding.port);
|
|
5281
5463
|
if (!listened.ok) {
|
|
5282
5464
|
unsubscribeTasks();
|
|
5465
|
+
unsubscribePlugins();
|
|
5283
5466
|
return listened;
|
|
5284
5467
|
}
|
|
5285
5468
|
handle = buildHandler(opts, hub, binding, listened.port, webRoot);
|
|
@@ -5293,7 +5476,7 @@ async function createWebServer(opts) {
|
|
|
5293
5476
|
...webRoot === void 0 ? {} : { webRoot },
|
|
5294
5477
|
observe: (sink, opts2) => hub.observe(sink, opts2 ?? {}),
|
|
5295
5478
|
snapshot: () => hub.listSessions(),
|
|
5296
|
-
close: makeCloser(server, hub, opts.runtime, unsubscribeTasks)
|
|
5479
|
+
close: makeCloser(server, hub, opts.runtime, unsubscribeTasks, unsubscribePlugins)
|
|
5297
5480
|
}
|
|
5298
5481
|
};
|
|
5299
5482
|
}
|
|
@@ -5306,6 +5489,7 @@ function buildHandler(opts, hub, binding, port, webRoot) {
|
|
|
5306
5489
|
return createRequestHandler({
|
|
5307
5490
|
ctx: {
|
|
5308
5491
|
hub,
|
|
5492
|
+
hostCompose: new HostComposeStore(),
|
|
5309
5493
|
runtime: opts.runtime,
|
|
5310
5494
|
version: opts.version,
|
|
5311
5495
|
artifactsRoot: opts.artifactsRoot ?? opts.runtime.artifactsRoot,
|
|
@@ -5345,11 +5529,12 @@ function describeListenError(err, host, port) {
|
|
|
5345
5529
|
}
|
|
5346
5530
|
return t("web.listen_failed", { host, port, reason: err.message }, void 0);
|
|
5347
5531
|
}
|
|
5348
|
-
function makeCloser(server, hub, runtime, unsubscribeTasks) {
|
|
5532
|
+
function makeCloser(server, hub, runtime, unsubscribeTasks, unsubscribePlugins) {
|
|
5349
5533
|
let closing = null;
|
|
5350
5534
|
return () => {
|
|
5351
5535
|
closing ??= (async () => {
|
|
5352
5536
|
unsubscribeTasks();
|
|
5537
|
+
unsubscribePlugins();
|
|
5353
5538
|
hub.shutdown();
|
|
5354
5539
|
server.closeAllConnections();
|
|
5355
5540
|
await new Promise((resolve4) => server.close(() => resolve4()));
|