@tea-agent/loop-agent 0.40.0-next.12 → 0.40.0-next.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +1 -1
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +9 -5
- package/dist/cli/program.js +38 -3
- package/dist/commands/task-advance.js +23 -1
- package/dist/commands/task-source-prepare.js +5 -0
- package/dist/commands/task-status.js +1 -0
- package/dist/shared/operator/capabilities.js +9 -1
- package/dist/worker/console/chat/assistant-content.js +7 -0
- package/dist/worker/console/chat/chat-event-store.js +82 -11
- package/dist/worker/console/chat/goal-round-driver.js +72 -0
- package/dist/worker/console/chat/goals.js +300 -0
- package/dist/worker/console/chat/pi-runtime.js +140 -4
- package/dist/worker/console/chat/repo-browser.js +94 -12
- package/dist/worker/console/chat/routes.js +482 -29
- package/dist/worker/console/chat/session-catalog.js +48 -0
- package/dist/worker/console/chat/session-store.js +27 -18
- package/dist/worker/console/chat/shortcuts.js +7 -0
- package/dist/worker/console/chat/turn-process.js +32 -3
- package/dist/worker/console/operator-actions.js +16 -2
- package/dist/worker/console/operator-selection.js +6 -2
- package/dist/worker/console/pi-plugins.catalog.json +73658 -0
- package/dist/worker/console/pi-plugins.js +100 -0
- package/dist/worker/console/routes.js +221 -6
- package/dist/worker/console/server.js +172 -42
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C_hkwt2J.js → abnfDiagram-N423BO3Z-Wj-fsQlk.js} +1 -1
- package/dist/worker/console/static/assets/{arc-s2Q6r96B.js → arc-CkXT-vzQ.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CtgsrwU1.js → architectureDiagram-T3A2C74G-xlfnojWU.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DZ-A7EiJ.js → blockDiagram-VBNYF7ZC-BilahvuB.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-D2qv0Nyo.js → c4Diagram-5PPSVZJV-DqtAVhjz.js} +1 -1
- package/dist/worker/console/static/assets/channel-SnQ_u-Td.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D-UYpSaW.js → chunk-2GRJ4B5K-pAqQXZ5y.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DAS3jrrt.js → chunk-2Q5K7J3B-BEWxeq2r.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CSbuRcGB.js → chunk-5RXB4S5H-CvlbGDTp.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BDH1ZO40.js → chunk-5VM5RSS4-D0uGd9-f.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Dc2cqQIb.js → chunk-6Q2QTUOP-DSdWIteg.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CkZuemIW.js → chunk-GF5L2VYU-Dweg7f0v.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-URwjQnte.js → chunk-JWPE2WC7-BpZKI_k7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-04RHylJK.js → chunk-KBJHAD2P-IPLknC4m.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CvQX56Hh.js → chunk-RYQCIY6F-E93k9lVI.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-dUF7qura.js → chunk-XXDRQBXY-BrESwkPq.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-SGM51NVY.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-SGM51NVY.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-COwbfxJE.js → cose-bilkent-JH36ORCC-DmFHvJkB.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CaBEJekQ.js → cynefin-VYW2F7L2-CDigXiSL.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-COWUBdet.js → cynefinDiagram-MW4NZA55-B0zhrbaA.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BLxVqTn4.js → dagre-VZM6K2ZE-DrKybVxm.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-WL0yKfEQ.js → diagram-7IWD3JNH-CeUrnbFk.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DaVkKCle.js → diagram-B4RE2ZJO-BJyunUMi.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DdTgnRGf.js → diagram-LBJQPF4R-a5TBP73u.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-PG2Axo6A.js → diagram-Q27KOJAE-ChRgBURJ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-6vuQzlzI.js → diagram-UB23O5K3-CqCnPRSk.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BpXeBz-g.js → ebnfDiagram-BXEA7PRR-BI-UD9_S.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-PxtUnaa1.js → erDiagram-JOGREHBK-CDLvvkYZ.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-B--Qgvs1.js → flowDiagram-UKHOOZJN-Heoo4afj.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BIfQCgtx.js → ganttDiagram-PKOTCBZU-kFpOitHd.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BRaX_E4j.js → gitGraphDiagram-DS77QQ5N-FvyZ9WAY.js} +1 -1
- package/dist/worker/console/static/assets/index-C80SO8y0.js +437 -0
- package/dist/worker/console/static/assets/index-CmJCHmJP.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Cd93r4Ab.js → infoDiagram-6WML65LV-HBzlwtgI.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-C4c80iHt.js → ishikawaDiagram-WSZJBQD7-C9Iim5sm.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Ch5ajf3s.js → journeyDiagram-NVQOT4AX-CJwJTg0S.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DbpyW1Ut.js → kanban-definition-27J2QSJJ-CZNJ7S4q.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BHVjBnTW.js → linear-RZ0AnGi8.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DO70kJ34.js → mermaid.core-EYSdm-nd.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-_P44Zpvs.js → mindmap-definition-FAOFIHXS-D0OLECFN.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Bgi9FmEd.js → pegDiagram-VL7TDLO6-CPTvGbh7.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-C9InDfJn.js → pieDiagram-7S7Q4E2Y-Ddc1aDea.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DtKrUouv.js → quadrantDiagram-CIZ2JOQS-CWCTCdAt.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-zgAD_6Yo.js → railroadDiagram-AXF67PYL-CfsaBL8E.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CQNO6kj4.js → requirementDiagram-LRYGKXZP-BH2rqGf6.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-knKu111P.js → sankeyDiagram-W5VNT64P-BZgJ1Til.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-D6tshiQ6.js → sequenceDiagram-SI44F4Z6-DmPr-o-y.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-BMIWJcir.js → sizeCapture-X5ZJPWSS-CF6HzDh_.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BP9wRDqc.js → stateDiagram-OKZ733FA-DhS8N_B9.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-N56zxwsk.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DEkM5Dhe.js → swimlanes-SLNWSIFB-CYNzoiaD.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-o47kGgVt.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ByBKsL35.js → timeline-definition-Z64GVDOM-QBICF5qH.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-waFbNCUy.js → vennDiagram-T6HMQDX7-fquBvWcf.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-1ljiY0eZ.js → wardleyDiagram-T6FBY63Y-BYaJLlSd.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-B4ZLHr_L.js → xychartDiagram-ELKLHX3M--ePe_JCJ.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +95 -25
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +35 -1
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +3 -1
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +24 -19
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +0 -11
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +8 -1
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +34 -4
- package/dist/worker/console/static-src/operator-chat/workspace-reorder.js +18 -0
- package/dist/worker/console/static-src/shell/useWorkspaces.js +150 -34
- package/dist/worker/console/workspace-context.js +28 -3
- package/dist/worker/console/workspace-registry.js +333 -60
- package/dist/worker/observe/static/inspect-bootstrap.d.ts +8 -0
- package/dist/worker/observe/static/inspect-bootstrap.js +57 -0
- package/dist/worker/observe/static/inspect-workspace.js +54 -10
- package/dist/worker/observe/static/operator-chrome.css +23 -4
- package/dist/worker/observe/static/operator-chrome.js +6 -2
- package/dist/worker/observe/static/styles.css +78 -25
- package/dist/worker/observe/static/views/dag-inspector.js +2 -1
- package/dist/worker/observe/static/views/dags.js +7 -5
- package/dist/workflows/dag/init-hybrid.js +3 -3
- package/docs/templates/backend-test-dag.json +3 -3
- package/package.json +1 -1
- package/skills/loop-agent/references/command-reference.md +14 -0
- package/dist/worker/console/static/assets/channel-BluaMT3-.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-H-ZpSafr.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-H-ZpSafr.js +0 -1
- package/dist/worker/console/static/assets/index-Crrgl8Mx.css +0 -1
- package/dist/worker/console/static/assets/index-vPsp-idE.js +0 -437
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-ClEhytQG.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPg5IMuu.js +0 -8
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import catalogData from "./pi-plugins.catalog.json" with { type: "json" };
|
|
6
|
+
export function getPiAgentDir() {
|
|
7
|
+
const envDir = process.env.PI_CODING_AGENT_DIR;
|
|
8
|
+
return envDir ? envDir : path.join(os.homedir(), ".pi", "agent");
|
|
9
|
+
}
|
|
10
|
+
function npmRoot() {
|
|
11
|
+
return path.join(getPiAgentDir(), "npm");
|
|
12
|
+
}
|
|
13
|
+
function nodeModulesRoot() {
|
|
14
|
+
return path.join(npmRoot(), "node_modules");
|
|
15
|
+
}
|
|
16
|
+
function settingsPath() {
|
|
17
|
+
return path.join(getPiAgentDir(), "settings.json");
|
|
18
|
+
}
|
|
19
|
+
function readSettings() {
|
|
20
|
+
try {
|
|
21
|
+
if (!existsSync(settingsPath()))
|
|
22
|
+
return {};
|
|
23
|
+
return JSON.parse(readFileSync(settingsPath(), "utf8"));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function packageNameFromSpec(spec) {
|
|
30
|
+
return spec.startsWith("npm:") ? spec.slice(4) : spec;
|
|
31
|
+
}
|
|
32
|
+
function listInstalledPackageNames() {
|
|
33
|
+
const settings = readSettings();
|
|
34
|
+
return (settings.packages ?? []).map(packageNameFromSpec);
|
|
35
|
+
}
|
|
36
|
+
function isPackageInstalledOnDisk(pkg) {
|
|
37
|
+
return existsSync(path.join(nodeModulesRoot(), ...pkg.split("/")));
|
|
38
|
+
}
|
|
39
|
+
/** 目录 + 每个插件的已装状态(settings.packages 声明 + node_modules 存在)。 */
|
|
40
|
+
export function listPiPlugins() {
|
|
41
|
+
const installed = new Set(listInstalledPackageNames());
|
|
42
|
+
const plugins = catalogData.plugins.map((p) => ({
|
|
43
|
+
...p,
|
|
44
|
+
installed: installed.has(p.name) && isPackageInstalledOnDisk(p.name),
|
|
45
|
+
}));
|
|
46
|
+
return { summary: catalogData.summary, plugins };
|
|
47
|
+
}
|
|
48
|
+
function runPi(args) {
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
mkdirSync(getPiAgentDir(), { recursive: true });
|
|
51
|
+
// Windows 上 pi 是 pi.cmd(无 .exe),.cmd/.bat 只能 shell:true 解析。
|
|
52
|
+
const child = spawn("pi", args, {
|
|
53
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
54
|
+
cwd: getPiAgentDir(),
|
|
55
|
+
shell: process.platform === "win32",
|
|
56
|
+
});
|
|
57
|
+
let stdout = "";
|
|
58
|
+
let stderr = "";
|
|
59
|
+
child.stdout.on("data", (d) => (stdout += d.toString("utf8")));
|
|
60
|
+
child.stderr.on("data", (d) => (stderr += d.toString("utf8")));
|
|
61
|
+
child.on("close", (code) => resolve({ code: code ?? -1, stdout, stderr }));
|
|
62
|
+
child.on("error", (err) => resolve({ code: -1, stdout, stderr: String(err?.message ?? err) }));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/** 校验安装目标是否在目录中,且该包确实声明了 pi.extensions(可安装为 Pi 扩展)。 */
|
|
66
|
+
export function findCatalogPlugin(name) {
|
|
67
|
+
return catalogData.plugins.find((p) => p.name === name);
|
|
68
|
+
}
|
|
69
|
+
/** 全局(用户级 ~/.pi/agent)安装:调用 `pi install npm:<name>`。 */
|
|
70
|
+
export async function installPiPlugin(name) {
|
|
71
|
+
const entry = findCatalogPlugin(name);
|
|
72
|
+
if (!entry) {
|
|
73
|
+
return { ok: false, error: `包 ${name} 不在插件目录中` };
|
|
74
|
+
}
|
|
75
|
+
if (!entry.valid) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
error: `包 ${name} 未声明 pi.extensions,无法作为 Pi 扩展使用`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const result = await runPi(["install", `npm:${name}`]);
|
|
82
|
+
if (result.code !== 0) {
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
error: `pi install 失败 (exit ${result.code}): ${(result.stderr || result.stdout).slice(0, 400)}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return { ok: true };
|
|
89
|
+
}
|
|
90
|
+
/** 全局卸载:调用 `pi remove npm:<name>`。 */
|
|
91
|
+
export async function uninstallPiPlugin(name) {
|
|
92
|
+
const result = await runPi(["remove", `npm:${name}`]);
|
|
93
|
+
if (result.code !== 0) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
error: `pi remove 失败 (exit ${result.code}): ${(result.stderr || result.stdout).slice(0, 400)}`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return { ok: true };
|
|
100
|
+
}
|
|
@@ -8,6 +8,7 @@ import { buildOperatorSurfaceHealthV1 } from "./operator-surface-health.js";
|
|
|
8
8
|
import { ROUTES } from "../observe/routes.js";
|
|
9
9
|
import { evaluateMutationGate, injectConfirmationTokenScript, isMutationMethod, } from "./security.js";
|
|
10
10
|
import { listDagRunsForRecovery } from "./resolve-dag-run-for-task.js";
|
|
11
|
+
import { installPiPlugin, listPiPlugins, uninstallPiPlugin, } from "./pi-plugins.js";
|
|
11
12
|
import { pickNativeWorkspaceDirectory } from "./native-directory-picker.js";
|
|
12
13
|
export function sendJson(res, status, body, extraHeaders) {
|
|
13
14
|
const payload = JSON.stringify(body);
|
|
@@ -462,6 +463,94 @@ async function handleNightAuxTickerPost(req, res, ctx) {
|
|
|
462
463
|
const status = ticker.setEnabled(body.enabled);
|
|
463
464
|
sendJson(res, 200, { ok: true, ticker: status });
|
|
464
465
|
}
|
|
466
|
+
async function handlePiPluginInstall(req, res) {
|
|
467
|
+
let body;
|
|
468
|
+
try {
|
|
469
|
+
body = (await readJsonBody(req));
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
sendJson(res, 400, {
|
|
473
|
+
ok: false,
|
|
474
|
+
error: {
|
|
475
|
+
code: "INVALID_INPUT",
|
|
476
|
+
message: error instanceof Error ? error.message : String(error),
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
482
|
+
if (!name) {
|
|
483
|
+
sendJson(res, 400, {
|
|
484
|
+
ok: false,
|
|
485
|
+
error: { code: "INVALID_INPUT", message: "missing package name" },
|
|
486
|
+
});
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
try {
|
|
490
|
+
const result = await installPiPlugin(name);
|
|
491
|
+
if (!result.ok) {
|
|
492
|
+
sendJson(res, 400, {
|
|
493
|
+
ok: false,
|
|
494
|
+
error: { code: "INSTALL_FAILED", message: result.error },
|
|
495
|
+
});
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
sendJson(res, 200, { ok: true });
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
sendJson(res, 500, {
|
|
502
|
+
ok: false,
|
|
503
|
+
error: {
|
|
504
|
+
code: "INTERNAL_ERROR",
|
|
505
|
+
message: error instanceof Error ? error.message : String(error),
|
|
506
|
+
},
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
async function handlePiPluginUninstall(req, res) {
|
|
511
|
+
let body;
|
|
512
|
+
try {
|
|
513
|
+
body = (await readJsonBody(req));
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
sendJson(res, 400, {
|
|
517
|
+
ok: false,
|
|
518
|
+
error: {
|
|
519
|
+
code: "INVALID_INPUT",
|
|
520
|
+
message: error instanceof Error ? error.message : String(error),
|
|
521
|
+
},
|
|
522
|
+
});
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
526
|
+
if (!name) {
|
|
527
|
+
sendJson(res, 400, {
|
|
528
|
+
ok: false,
|
|
529
|
+
error: { code: "INVALID_INPUT", message: "missing package name" },
|
|
530
|
+
});
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
try {
|
|
534
|
+
const result = await uninstallPiPlugin(name);
|
|
535
|
+
if (!result.ok) {
|
|
536
|
+
sendJson(res, 400, {
|
|
537
|
+
ok: false,
|
|
538
|
+
error: { code: "UNINSTALL_FAILED", message: result.error },
|
|
539
|
+
});
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
sendJson(res, 200, { ok: true });
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
sendJson(res, 500, {
|
|
546
|
+
ok: false,
|
|
547
|
+
error: {
|
|
548
|
+
code: "INTERNAL_ERROR",
|
|
549
|
+
message: error instanceof Error ? error.message : String(error),
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
465
554
|
export async function handleConsoleOperatorRequest(req, res, ctx) {
|
|
466
555
|
const method = (req.method ?? "GET").toUpperCase();
|
|
467
556
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
@@ -495,6 +584,10 @@ export async function handleConsoleOperatorRequest(req, res, ctx) {
|
|
|
495
584
|
await handleNightAuxTickerGet(req, res, ctx);
|
|
496
585
|
return true;
|
|
497
586
|
}
|
|
587
|
+
if (method === "GET" && pathname === "/api/operator/v1/pi-plugins") {
|
|
588
|
+
sendJson(res, 200, { ok: true, data: listPiPlugins() });
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
498
591
|
const opMatch = pathname.match(/^\/api\/operator\/v1\/operations\/([^/]+)(?:\/(events))?$/);
|
|
499
592
|
if (opMatch) {
|
|
500
593
|
const operationId = decodeURIComponent(opMatch[1]);
|
|
@@ -526,6 +619,14 @@ export async function handleConsoleOperatorRequest(req, res, ctx) {
|
|
|
526
619
|
await handleNightAuxTickerPost(req, res, ctx);
|
|
527
620
|
return true;
|
|
528
621
|
}
|
|
622
|
+
if (method === "POST" && pathname === "/api/operator/v1/pi-plugins/install") {
|
|
623
|
+
await handlePiPluginInstall(req, res);
|
|
624
|
+
return true;
|
|
625
|
+
}
|
|
626
|
+
if (method === "POST" && pathname === "/api/operator/v1/pi-plugins/uninstall") {
|
|
627
|
+
await handlePiPluginUninstall(req, res);
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
529
630
|
// Allow other operator POSTs only under known routes; unknown → 404
|
|
530
631
|
sendJson(res, 404, {
|
|
531
632
|
ok: false,
|
|
@@ -587,9 +688,11 @@ function pendingReadinessSummary(inspectReady) {
|
|
|
587
688
|
* POST /api/operator/v1/workspaces/pick — host native folder chooser
|
|
588
689
|
* POST /api/operator/v1/workspaces — body { "name": "<label>",
|
|
589
690
|
* "directoryPath": "<dir>" }
|
|
691
|
+
* PATCH /api/operator/v1/workspaces/:workspaceId — body { "beforeWorkspaceId":
|
|
692
|
+
* "<id>" | null }
|
|
590
693
|
* DELETE /api/operator/v1/workspaces/:workspaceId — soft remove (restore on re-add)
|
|
591
694
|
*
|
|
592
|
-
* POST/DELETE are mutations and pass the Console mutation gate. GET carries no
|
|
695
|
+
* POST/PATCH/DELETE are mutations and pass the Console mutation gate. GET carries no
|
|
593
696
|
* workspace selection parameter.
|
|
594
697
|
*
|
|
595
698
|
* AC-006 (2026-08-23): GET is NON-BLOCKING — registry + process-level
|
|
@@ -598,8 +701,53 @@ function pendingReadinessSummary(inspectReady) {
|
|
|
598
701
|
* requests. A hanging probe can never delay the 200 list.
|
|
599
702
|
*/
|
|
600
703
|
export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathname, method, readinessDeps) {
|
|
704
|
+
if (method === "GET" &&
|
|
705
|
+
pathname === "/api/operator/v1/workspaces/events") {
|
|
706
|
+
res.writeHead(200, {
|
|
707
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
708
|
+
"Cache-Control": "no-cache, no-transform",
|
|
709
|
+
Connection: "keep-alive",
|
|
710
|
+
"X-Accel-Buffering": "no",
|
|
711
|
+
});
|
|
712
|
+
const sendRevision = (event, revision) => {
|
|
713
|
+
if (res.destroyed || res.writableEnded)
|
|
714
|
+
return;
|
|
715
|
+
res.write(`id: ${revision}\nevent: ${event}\ndata: ${JSON.stringify({ schemaVersion: 1, revision })}\n\n`);
|
|
716
|
+
};
|
|
717
|
+
let baselineSent = false;
|
|
718
|
+
let pendingRevision = 0;
|
|
719
|
+
const unsubscribe = registry.subscribe((revision) => {
|
|
720
|
+
if (!baselineSent)
|
|
721
|
+
pendingRevision = Math.max(pendingRevision, revision);
|
|
722
|
+
else
|
|
723
|
+
sendRevision("workspace_registry_changed", revision);
|
|
724
|
+
});
|
|
725
|
+
const initial = await registry.snapshotVisible();
|
|
726
|
+
sendRevision("workspace_registry_ready", initial.revision);
|
|
727
|
+
baselineSent = true;
|
|
728
|
+
if (pendingRevision > initial.revision) {
|
|
729
|
+
sendRevision("workspace_registry_changed", pendingRevision);
|
|
730
|
+
}
|
|
731
|
+
const heartbeat = setInterval(() => {
|
|
732
|
+
if (!res.destroyed && !res.writableEnded)
|
|
733
|
+
res.write(": keepalive\n\n");
|
|
734
|
+
}, 15_000);
|
|
735
|
+
heartbeat.unref();
|
|
736
|
+
let closed = false;
|
|
737
|
+
const close = () => {
|
|
738
|
+
if (closed)
|
|
739
|
+
return;
|
|
740
|
+
closed = true;
|
|
741
|
+
clearInterval(heartbeat);
|
|
742
|
+
unsubscribe();
|
|
743
|
+
};
|
|
744
|
+
req.once("aborted", close);
|
|
745
|
+
res.once("close", close);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
601
748
|
if (method === "GET" && pathname === "/api/operator/v1/workspaces") {
|
|
602
|
-
const
|
|
749
|
+
const snapshot = await registry.snapshotVisible();
|
|
750
|
+
const records = snapshot.workspaces;
|
|
603
751
|
const inspectReady = readinessDeps.inspectReady;
|
|
604
752
|
const readinessCache = readinessDeps.readinessCache ?? defaultWorkspaceReadinessCache;
|
|
605
753
|
const workspaces = records.map((record) => {
|
|
@@ -611,6 +759,7 @@ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathn
|
|
|
611
759
|
repoFingerprint: record.repoFingerprint,
|
|
612
760
|
createdAt: record.createdAt,
|
|
613
761
|
lastAccessedAt: record.lastAccessedAt,
|
|
762
|
+
sessionIds: record.sessionIds,
|
|
614
763
|
readiness: cached
|
|
615
764
|
? {
|
|
616
765
|
state: cached.state,
|
|
@@ -622,7 +771,8 @@ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathn
|
|
|
622
771
|
// Respond FIRST, then refresh readiness in the background (AC-006).
|
|
623
772
|
sendJson(res, 200, {
|
|
624
773
|
ok: true,
|
|
625
|
-
schemaVersion:
|
|
774
|
+
schemaVersion: 2,
|
|
775
|
+
revision: snapshot.revision,
|
|
626
776
|
workspaces,
|
|
627
777
|
});
|
|
628
778
|
void Promise.all(records.map(async (record) => {
|
|
@@ -789,8 +939,73 @@ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathn
|
|
|
789
939
|
});
|
|
790
940
|
return;
|
|
791
941
|
}
|
|
792
|
-
const
|
|
793
|
-
if (method === "
|
|
942
|
+
const workspaceMatch = pathname.match(/^\/api\/operator\/v1\/workspaces\/([^/]+)$/);
|
|
943
|
+
if (method === "PATCH" && workspaceMatch) {
|
|
944
|
+
const failure = evaluateMutationGate(req, {
|
|
945
|
+
cookieName: deps.bootToken.cookieName,
|
|
946
|
+
bootToken: deps.bootToken.value,
|
|
947
|
+
confirmationToken: deps.bootToken.confirmationToken,
|
|
948
|
+
consoleOrigin: deps.getConsoleOrigin(),
|
|
949
|
+
allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
|
|
950
|
+
});
|
|
951
|
+
if (failure) {
|
|
952
|
+
sendJson(res, failure.status, {
|
|
953
|
+
ok: false,
|
|
954
|
+
error: { code: failure.code, message: failure.message },
|
|
955
|
+
});
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
let body;
|
|
959
|
+
try {
|
|
960
|
+
body = (await readJsonBody(req));
|
|
961
|
+
}
|
|
962
|
+
catch (error) {
|
|
963
|
+
sendJson(res, 400, {
|
|
964
|
+
ok: false,
|
|
965
|
+
error: {
|
|
966
|
+
code: "INVALID_INPUT",
|
|
967
|
+
message: error instanceof Error ? error.message : String(error),
|
|
968
|
+
},
|
|
969
|
+
});
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
if (body.beforeWorkspaceId !== null &&
|
|
973
|
+
typeof body.beforeWorkspaceId !== "string") {
|
|
974
|
+
sendJson(res, 400, {
|
|
975
|
+
ok: false,
|
|
976
|
+
error: {
|
|
977
|
+
code: "INVALID_INPUT",
|
|
978
|
+
message: "beforeWorkspaceId must be a workspace id or null",
|
|
979
|
+
},
|
|
980
|
+
});
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const workspaceId = decodeURIComponent(workspaceMatch[1]);
|
|
984
|
+
const beforeWorkspaceId = typeof body.beforeWorkspaceId === "string"
|
|
985
|
+
? body.beforeWorkspaceId
|
|
986
|
+
: null;
|
|
987
|
+
const reordered = await registry.insertBefore(workspaceId, beforeWorkspaceId);
|
|
988
|
+
if (!reordered.ok) {
|
|
989
|
+
sendJson(res, 404, {
|
|
990
|
+
ok: false,
|
|
991
|
+
error: {
|
|
992
|
+
code: reordered.code,
|
|
993
|
+
message: reordered.code === "ANCHOR_NOT_FOUND"
|
|
994
|
+
? `unknown workspace anchor: ${beforeWorkspaceId}`
|
|
995
|
+
: `unknown workspace: ${workspaceId}`,
|
|
996
|
+
},
|
|
997
|
+
});
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
sendJson(res, 200, {
|
|
1001
|
+
ok: true,
|
|
1002
|
+
workspaceId,
|
|
1003
|
+
beforeWorkspaceId,
|
|
1004
|
+
changed: reordered.changed,
|
|
1005
|
+
});
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
if (method === "DELETE" && workspaceMatch) {
|
|
794
1009
|
const failure = evaluateMutationGate(req, {
|
|
795
1010
|
cookieName: deps.bootToken.cookieName,
|
|
796
1011
|
bootToken: deps.bootToken.value,
|
|
@@ -805,7 +1020,7 @@ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathn
|
|
|
805
1020
|
});
|
|
806
1021
|
return;
|
|
807
1022
|
}
|
|
808
|
-
const workspaceId = decodeURIComponent(
|
|
1023
|
+
const workspaceId = decodeURIComponent(workspaceMatch[1]);
|
|
809
1024
|
const removed = await registry.remove(workspaceId);
|
|
810
1025
|
if (!removed) {
|
|
811
1026
|
sendJson(res, 404, {
|