@tea-agent/loop-agent 0.40.0-next.8 → 0.40.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/AGENTS.md +1 -1
- package/CHANGELOG.md +21 -20
- package/README.md +2 -2
- package/dist/application/dag/generate-task-dag.js +6 -2
- 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/client-recovery.js +8 -36
- package/dist/commands/init-model-catalog.js +22 -0
- 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/executors/dag-pi-executor.js +3 -0
- package/dist/executors/pi-executor.js +4 -2
- package/dist/executors/pi-sdk-executor.js +79 -6
- package/dist/executors/shell-executor.js +94 -20
- package/dist/governance/checks.js +1 -0
- package/dist/shared/operator/capabilities.js +9 -1
- package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
- package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
- package/dist/shared/pi-context-pressure/env.js +58 -0
- package/dist/shared/pi-context-pressure/extension.js +100 -0
- package/dist/shared/pi-context-pressure/index.js +7 -0
- package/dist/shared/pi-context-pressure/overflow.js +252 -0
- package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
- package/dist/shared/pi-context-pressure/telemetry.js +51 -0
- 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 +285 -8
- package/dist/worker/console/chat/provider-error.js +2 -1
- package/dist/worker/console/chat/repo-browser.js +94 -12
- package/dist/worker/console/chat/routes.js +495 -30
- package/dist/worker/console/chat/session-catalog.js +48 -0
- package/dist/worker/console/chat/session-stats.js +96 -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/sift-bridge.js +1 -0
- package/dist/worker/console/chat/turn-process.js +42 -5
- package/dist/worker/console/dag-execution-receipt.js +20 -2
- package/dist/worker/console/operator-actions.js +20 -5
- 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 +179 -0
- package/dist/worker/console/routes.js +246 -6
- package/dist/worker/console/server.js +173 -42
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BYBmDfPW.js → abnfDiagram-N423BO3Z-Dy71QJp3.js} +1 -1
- package/dist/worker/console/static/assets/{arc-BKMmxpGF.js → arc-M25fNMTE.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DJpwxr1d.js → architectureDiagram-T3A2C74G-CUWlVl1a.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BaPj_ZlY.js → blockDiagram-VBNYF7ZC-Dd0Whpx2.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B8iGmHlm.js → c4Diagram-5PPSVZJV-BdfeCkI5.js} +1 -1
- package/dist/worker/console/static/assets/channel-Ba0AKiEZ.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DaxXQayc.js → chunk-2GRJ4B5K-D_1sUkyD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DqQKGA7q.js → chunk-2Q5K7J3B-Cxnxf36E.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CgMKpJBB.js → chunk-5RXB4S5H-D9RG3G_x.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BpToUlaW.js → chunk-5VM5RSS4-Zz9jgjLX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BsEWOHxv.js → chunk-6Q2QTUOP-BCggDRCH.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CKGSCe71.js → chunk-GF5L2VYU-CFg-5iAS.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-Bm1U5299.js → chunk-JWPE2WC7-DRQTE8x_.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BI0ov870.js → chunk-KBJHAD2P-C_Dw7uZ8.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BmnQo0iJ.js → chunk-RYQCIY6F-BjIiApsk.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BJBqa6Xc.js → chunk-XXDRQBXY-B5XQD4KG.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BU3IjVEe.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BU3IjVEe.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BuN9-7AR.js → cose-bilkent-JH36ORCC-huHqH6WQ.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-Bw22jc2k.js → cynefin-VYW2F7L2-BSi6yZqP.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BHnSH8Dr.js → cynefinDiagram-MW4NZA55-Bs_Jp31y.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BoXXIegj.js → dagre-VZM6K2ZE-CYxavikT.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CU8qyPhT.js → diagram-7IWD3JNH-B6TErl71.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-lrjhF0S8.js → diagram-B4RE2ZJO-CvH4J2ii.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DL0QDEpo.js → diagram-LBJQPF4R-v1NBAqXS.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DxVMJMBH.js → diagram-Q27KOJAE-CUNHVAn2.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-CFg9FzvG.js → diagram-UB23O5K3-Dzz1gUu-.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CQgP9fTG.js → ebnfDiagram-BXEA7PRR-CCF8_LjK.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-A5vQXx9U.js → erDiagram-JOGREHBK-DObdPVLa.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-ytXnLyXD.js → flowDiagram-UKHOOZJN-DlN8Pxrj.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CYuksfnS.js → ganttDiagram-PKOTCBZU-BJKIPTGX.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-f_jW3zgU.js → gitGraphDiagram-DS77QQ5N-XcSSnObi.js} +1 -1
- package/dist/worker/console/static/assets/index-CIYzS16i.css +1 -0
- package/dist/worker/console/static/assets/index-CS3QOTfl.js +451 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-C3HLE-xV.js → infoDiagram-6WML65LV-BKrRKMww.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DwpgejS6.js → ishikawaDiagram-WSZJBQD7-CN5dk-yb.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-ll0YWX1y.js → journeyDiagram-NVQOT4AX-B4P4KZ-Y.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Hq3rchvU.js → kanban-definition-27J2QSJJ-BRlteP32.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BaGjJx8z.js → linear-CG8D9fuL.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-CKGQHWX_.js → mermaid.core-BuB6P2rX.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DM1FA6Dr.js → mindmap-definition-FAOFIHXS-DYXGl_DR.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Xa9fHkE2.js → pegDiagram-VL7TDLO6-BM48yEVG.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-pCd4PDqJ.js → pieDiagram-7S7Q4E2Y-mT8NL-Ow.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-KqjpEBLk.js → quadrantDiagram-CIZ2JOQS-C4pZ6DH_.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DgvR7LZD.js → railroadDiagram-AXF67PYL-BKAcr70m.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-Bys1UCWA.js → requirementDiagram-LRYGKXZP-CRxcKpoO.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Ms9npnZt.js → sankeyDiagram-W5VNT64P-Ds4LxQDI.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DEIWrtQZ.js → sequenceDiagram-SI44F4Z6-CPLwPf_p.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DELJ0AhZ.js → sizeCapture-X5ZJPWSS-CL65uij0.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BpHt9wKh.js → stateDiagram-OKZ733FA-CO0Mgd1W.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BUewAVYS.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-GKRsBluT.js → swimlanes-SLNWSIFB-Cxv3TSNw.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bhe67Wjl.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cwsq1DxL.js → timeline-definition-Z64GVDOM-Dw85f9L2.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CfaqtKx0.js → vennDiagram-T6HMQDX7-B4QLCqCy.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BlEUtK_g.js → wardleyDiagram-T6FBY63Y-B_yKCWwn.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CHSRXPvO.js → xychartDiagram-ELKLHX3M-nniBEGv2.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/spatial-overlay.js +1 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +26 -19
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +0 -11
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +14 -1
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +34 -4
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
- 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/static-src/shell/workspace-route.js +3 -1
- 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 +7 -2
- package/dist/worker/observe/static/styles.css +87 -25
- package/dist/worker/observe/static/views/dag-inspector.js +42 -1
- package/dist/worker/observe/static/views/dags.js +7 -5
- package/dist/worker/observe/static/views/session-timeline.js +135 -0
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +162 -8
- package/dist/workflows/dag/backend-test-pytest-collection.js +70 -2
- package/dist/workflows/dag/backend-test-result-contract.js +5 -1
- package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
- package/dist/workflows/dag/backend-test-writer-completeness.js +18 -12
- package/dist/workflows/dag/init-hybrid.js +32 -9
- package/dist/workflows/dag/types.js +13 -0
- package/docs/architecture/runtime-boundaries.md +2 -1
- package/docs/templates/agent-dag.schema.json +18 -0
- package/docs/templates/backend-test-dag.json +27 -9
- package/docs/templates/init-managed-agents.md +1 -1
- package/package.json +3 -2
- package/skills/loop-agent/references/command-reference.md +15 -1
- package/dist/worker/console/static/assets/channel-CrzAa3FA.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-B7QwCCb4.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-B7QwCCb4.js +0 -1
- package/dist/worker/console/static/assets/index-BB6KxwT_.css +0 -1
- package/dist/worker/console/static/assets/index-D_wxbadT.js +0 -407
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BEeSswlv.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B2m_wca8.js +0 -8
- package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
- package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
|
@@ -0,0 +1,179 @@
|
|
|
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
|
+
const base = spec.startsWith("npm:") ? spec.slice(4) : spec;
|
|
31
|
+
// 剥掉尾部 @<version>(如 pi-lens@3.8.72 → pi-lens),但保留 scoped 包名开头的 @
|
|
32
|
+
// (如 @scope/pkg → @scope/pkg,@scope/pkg@1.0.0 → @scope/pkg)。
|
|
33
|
+
const at = base.lastIndexOf("@");
|
|
34
|
+
if (at > 0)
|
|
35
|
+
return base.slice(0, at);
|
|
36
|
+
return base;
|
|
37
|
+
}
|
|
38
|
+
function listInstalledPackageNames() {
|
|
39
|
+
const settings = readSettings();
|
|
40
|
+
return (settings.packages ?? []).map(packageNameFromSpec);
|
|
41
|
+
}
|
|
42
|
+
function isPackageInstalledOnDisk(pkg) {
|
|
43
|
+
return existsSync(path.join(nodeModulesRoot(), ...pkg.split("/")));
|
|
44
|
+
}
|
|
45
|
+
/** 读取已安装包的实际版本(node_modules 里 package.json 的 version)。 */
|
|
46
|
+
function readInstalledVersion(pkg) {
|
|
47
|
+
try {
|
|
48
|
+
const manifest = JSON.parse(readFileSync(path.join(nodeModulesRoot(), ...pkg.split("/"), "package.json"), "utf8"));
|
|
49
|
+
return typeof manifest.version === "string" ? manifest.version : undefined;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** 目录 + 每个插件的已装状态(settings.packages 声明 + node_modules 存在)+ 已装版本。 */
|
|
56
|
+
export function listPiPlugins() {
|
|
57
|
+
const installed = new Set(listInstalledPackageNames());
|
|
58
|
+
const plugins = catalogData.plugins.map((p) => {
|
|
59
|
+
const isInstalled = installed.has(p.name) && isPackageInstalledOnDisk(p.name);
|
|
60
|
+
return {
|
|
61
|
+
...p,
|
|
62
|
+
installed: isInstalled,
|
|
63
|
+
installedVersion: isInstalled ? readInstalledVersion(p.name) : undefined,
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
return { summary: catalogData.summary, plugins };
|
|
67
|
+
}
|
|
68
|
+
let cachedRegistry;
|
|
69
|
+
/** 读取 npm 配置的 registry(`npm config get registry`),失败回退官方源。 */
|
|
70
|
+
function getNpmRegistry() {
|
|
71
|
+
if (cachedRegistry)
|
|
72
|
+
return Promise.resolve(cachedRegistry);
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
const fallback = () => {
|
|
75
|
+
cachedRegistry = "https://registry.npmjs.org/";
|
|
76
|
+
resolve(cachedRegistry);
|
|
77
|
+
};
|
|
78
|
+
try {
|
|
79
|
+
const child = spawn("npm", ["config", "get", "registry"], {
|
|
80
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
81
|
+
cwd: getPiAgentDir(),
|
|
82
|
+
shell: process.platform === "win32",
|
|
83
|
+
});
|
|
84
|
+
let out = "";
|
|
85
|
+
child.stdout.on("data", (d) => (out += d.toString("utf8")));
|
|
86
|
+
child.on("close", () => {
|
|
87
|
+
const reg = out.trim();
|
|
88
|
+
cachedRegistry = reg || "https://registry.npmjs.org/";
|
|
89
|
+
resolve(cachedRegistry);
|
|
90
|
+
});
|
|
91
|
+
child.on("error", fallback);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
fallback();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/** 从 npm 配置的仓库拉取某包的全部已发布版本 + latest。 */
|
|
99
|
+
export async function listPiPluginVersions(name) {
|
|
100
|
+
const registry = (await getNpmRegistry()).replace(/\/+$/, "");
|
|
101
|
+
const url = `${registry}/${encodeURIComponent(name).replace(/%2F/g, "/")}`;
|
|
102
|
+
try {
|
|
103
|
+
const res = await fetch(url, {
|
|
104
|
+
headers: { accept: "application/json", "user-agent": "loop-agent/1.0" },
|
|
105
|
+
});
|
|
106
|
+
if (!res.ok)
|
|
107
|
+
return { ok: false, error: `npm registry HTTP ${res.status}` };
|
|
108
|
+
const body = (await res.json());
|
|
109
|
+
const versions = body.versions && typeof body.versions === "object"
|
|
110
|
+
? Object.keys(body.versions).sort((a, b) => b.localeCompare(a, undefined, { numeric: true }))
|
|
111
|
+
: [];
|
|
112
|
+
const latest = body["dist-tags"]?.latest;
|
|
113
|
+
return {
|
|
114
|
+
ok: true,
|
|
115
|
+
versions,
|
|
116
|
+
latest: typeof latest === "string" ? latest : undefined,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
error: error instanceof Error ? error.message : String(error),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function runPi(args) {
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
mkdirSync(getPiAgentDir(), { recursive: true });
|
|
129
|
+
// Windows 上 pi 是 pi.cmd(无 .exe),.cmd/.bat 只能 shell:true 解析。
|
|
130
|
+
const child = spawn("pi", args, {
|
|
131
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
132
|
+
cwd: getPiAgentDir(),
|
|
133
|
+
shell: process.platform === "win32",
|
|
134
|
+
});
|
|
135
|
+
let stdout = "";
|
|
136
|
+
let stderr = "";
|
|
137
|
+
child.stdout.on("data", (d) => (stdout += d.toString("utf8")));
|
|
138
|
+
child.stderr.on("data", (d) => (stderr += d.toString("utf8")));
|
|
139
|
+
child.on("close", (code) => resolve({ code: code ?? -1, stdout, stderr }));
|
|
140
|
+
child.on("error", (err) => resolve({ code: -1, stdout, stderr: String(err?.message ?? err) }));
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/** 校验安装目标是否在目录中,且该包确实声明了 pi.extensions(可安装为 Pi 扩展)。 */
|
|
144
|
+
export function findCatalogPlugin(name) {
|
|
145
|
+
return catalogData.plugins.find((p) => p.name === name);
|
|
146
|
+
}
|
|
147
|
+
/** 全局(用户级 ~/.pi/agent)安装:调用 `pi install npm:<name>`(可带版本,缺省为最新)。 */
|
|
148
|
+
export async function installPiPlugin(name, version) {
|
|
149
|
+
const entry = findCatalogPlugin(name);
|
|
150
|
+
if (!entry) {
|
|
151
|
+
return { ok: false, error: `包 ${name} 不在插件目录中` };
|
|
152
|
+
}
|
|
153
|
+
if (!entry.valid) {
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
error: `包 ${name} 未声明 pi.extensions,无法作为 Pi 扩展使用`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const spec = version && version.trim() ? `npm:${name}@${version.trim()}` : `npm:${name}`;
|
|
160
|
+
const result = await runPi(["install", spec]);
|
|
161
|
+
if (result.code !== 0) {
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
error: `pi install 失败 (exit ${result.code}): ${(result.stderr || result.stdout).slice(0, 400)}`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return { ok: true };
|
|
168
|
+
}
|
|
169
|
+
/** 全局卸载:调用 `pi remove npm:<name>`。 */
|
|
170
|
+
export async function uninstallPiPlugin(name) {
|
|
171
|
+
const result = await runPi(["remove", `npm:${name}`]);
|
|
172
|
+
if (result.code !== 0) {
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
error: `pi remove 失败 (exit ${result.code}): ${(result.stderr || result.stdout).slice(0, 400)}`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return { ok: true };
|
|
179
|
+
}
|
|
@@ -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, listPiPluginVersions, 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,95 @@ 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
|
+
const version = typeof body.version === "string" ? body.version.trim() : undefined;
|
|
490
|
+
try {
|
|
491
|
+
const result = await installPiPlugin(name, version);
|
|
492
|
+
if (!result.ok) {
|
|
493
|
+
sendJson(res, 400, {
|
|
494
|
+
ok: false,
|
|
495
|
+
error: { code: "INSTALL_FAILED", message: result.error },
|
|
496
|
+
});
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
sendJson(res, 200, { ok: true });
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
sendJson(res, 500, {
|
|
503
|
+
ok: false,
|
|
504
|
+
error: {
|
|
505
|
+
code: "INTERNAL_ERROR",
|
|
506
|
+
message: error instanceof Error ? error.message : String(error),
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async function handlePiPluginUninstall(req, res) {
|
|
512
|
+
let body;
|
|
513
|
+
try {
|
|
514
|
+
body = (await readJsonBody(req));
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
sendJson(res, 400, {
|
|
518
|
+
ok: false,
|
|
519
|
+
error: {
|
|
520
|
+
code: "INVALID_INPUT",
|
|
521
|
+
message: error instanceof Error ? error.message : String(error),
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
527
|
+
if (!name) {
|
|
528
|
+
sendJson(res, 400, {
|
|
529
|
+
ok: false,
|
|
530
|
+
error: { code: "INVALID_INPUT", message: "missing package name" },
|
|
531
|
+
});
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
try {
|
|
535
|
+
const result = await uninstallPiPlugin(name);
|
|
536
|
+
if (!result.ok) {
|
|
537
|
+
sendJson(res, 400, {
|
|
538
|
+
ok: false,
|
|
539
|
+
error: { code: "UNINSTALL_FAILED", message: result.error },
|
|
540
|
+
});
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
sendJson(res, 200, { ok: true });
|
|
544
|
+
}
|
|
545
|
+
catch (error) {
|
|
546
|
+
sendJson(res, 500, {
|
|
547
|
+
ok: false,
|
|
548
|
+
error: {
|
|
549
|
+
code: "INTERNAL_ERROR",
|
|
550
|
+
message: error instanceof Error ? error.message : String(error),
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
465
555
|
export async function handleConsoleOperatorRequest(req, res, ctx) {
|
|
466
556
|
const method = (req.method ?? "GET").toUpperCase();
|
|
467
557
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
@@ -495,6 +585,34 @@ export async function handleConsoleOperatorRequest(req, res, ctx) {
|
|
|
495
585
|
await handleNightAuxTickerGet(req, res, ctx);
|
|
496
586
|
return true;
|
|
497
587
|
}
|
|
588
|
+
if (method === "GET" && pathname === "/api/operator/v1/pi-plugins") {
|
|
589
|
+
sendJson(res, 200, { ok: true, data: listPiPlugins() });
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
if (method === "GET" &&
|
|
593
|
+
pathname === "/api/operator/v1/pi-plugins/versions") {
|
|
594
|
+
const name = url.searchParams.get("name")?.trim() ?? "";
|
|
595
|
+
if (!name) {
|
|
596
|
+
sendJson(res, 400, {
|
|
597
|
+
ok: false,
|
|
598
|
+
error: { code: "INVALID_INPUT", message: "missing name" },
|
|
599
|
+
});
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
const result = await listPiPluginVersions(name);
|
|
603
|
+
if (!result.ok) {
|
|
604
|
+
sendJson(res, 400, {
|
|
605
|
+
ok: false,
|
|
606
|
+
error: { code: "FETCH_FAILED", message: result.error },
|
|
607
|
+
});
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
sendJson(res, 200, {
|
|
611
|
+
ok: true,
|
|
612
|
+
data: { versions: result.versions, latest: result.latest },
|
|
613
|
+
});
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
498
616
|
const opMatch = pathname.match(/^\/api\/operator\/v1\/operations\/([^/]+)(?:\/(events))?$/);
|
|
499
617
|
if (opMatch) {
|
|
500
618
|
const operationId = decodeURIComponent(opMatch[1]);
|
|
@@ -526,6 +644,14 @@ export async function handleConsoleOperatorRequest(req, res, ctx) {
|
|
|
526
644
|
await handleNightAuxTickerPost(req, res, ctx);
|
|
527
645
|
return true;
|
|
528
646
|
}
|
|
647
|
+
if (method === "POST" && pathname === "/api/operator/v1/pi-plugins/install") {
|
|
648
|
+
await handlePiPluginInstall(req, res);
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
if (method === "POST" && pathname === "/api/operator/v1/pi-plugins/uninstall") {
|
|
652
|
+
await handlePiPluginUninstall(req, res);
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
529
655
|
// Allow other operator POSTs only under known routes; unknown → 404
|
|
530
656
|
sendJson(res, 404, {
|
|
531
657
|
ok: false,
|
|
@@ -587,9 +713,11 @@ function pendingReadinessSummary(inspectReady) {
|
|
|
587
713
|
* POST /api/operator/v1/workspaces/pick — host native folder chooser
|
|
588
714
|
* POST /api/operator/v1/workspaces — body { "name": "<label>",
|
|
589
715
|
* "directoryPath": "<dir>" }
|
|
716
|
+
* PATCH /api/operator/v1/workspaces/:workspaceId — body { "beforeWorkspaceId":
|
|
717
|
+
* "<id>" | null }
|
|
590
718
|
* DELETE /api/operator/v1/workspaces/:workspaceId — soft remove (restore on re-add)
|
|
591
719
|
*
|
|
592
|
-
* POST/DELETE are mutations and pass the Console mutation gate. GET carries no
|
|
720
|
+
* POST/PATCH/DELETE are mutations and pass the Console mutation gate. GET carries no
|
|
593
721
|
* workspace selection parameter.
|
|
594
722
|
*
|
|
595
723
|
* AC-006 (2026-08-23): GET is NON-BLOCKING — registry + process-level
|
|
@@ -598,8 +726,53 @@ function pendingReadinessSummary(inspectReady) {
|
|
|
598
726
|
* requests. A hanging probe can never delay the 200 list.
|
|
599
727
|
*/
|
|
600
728
|
export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathname, method, readinessDeps) {
|
|
729
|
+
if (method === "GET" &&
|
|
730
|
+
pathname === "/api/operator/v1/workspaces/events") {
|
|
731
|
+
res.writeHead(200, {
|
|
732
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
733
|
+
"Cache-Control": "no-cache, no-transform",
|
|
734
|
+
Connection: "keep-alive",
|
|
735
|
+
"X-Accel-Buffering": "no",
|
|
736
|
+
});
|
|
737
|
+
const sendRevision = (event, revision) => {
|
|
738
|
+
if (res.destroyed || res.writableEnded)
|
|
739
|
+
return;
|
|
740
|
+
res.write(`id: ${revision}\nevent: ${event}\ndata: ${JSON.stringify({ schemaVersion: 1, revision })}\n\n`);
|
|
741
|
+
};
|
|
742
|
+
let baselineSent = false;
|
|
743
|
+
let pendingRevision = 0;
|
|
744
|
+
const unsubscribe = registry.subscribe((revision) => {
|
|
745
|
+
if (!baselineSent)
|
|
746
|
+
pendingRevision = Math.max(pendingRevision, revision);
|
|
747
|
+
else
|
|
748
|
+
sendRevision("workspace_registry_changed", revision);
|
|
749
|
+
});
|
|
750
|
+
const initial = await registry.snapshotVisible();
|
|
751
|
+
sendRevision("workspace_registry_ready", initial.revision);
|
|
752
|
+
baselineSent = true;
|
|
753
|
+
if (pendingRevision > initial.revision) {
|
|
754
|
+
sendRevision("workspace_registry_changed", pendingRevision);
|
|
755
|
+
}
|
|
756
|
+
const heartbeat = setInterval(() => {
|
|
757
|
+
if (!res.destroyed && !res.writableEnded)
|
|
758
|
+
res.write(": keepalive\n\n");
|
|
759
|
+
}, 15_000);
|
|
760
|
+
heartbeat.unref();
|
|
761
|
+
let closed = false;
|
|
762
|
+
const close = () => {
|
|
763
|
+
if (closed)
|
|
764
|
+
return;
|
|
765
|
+
closed = true;
|
|
766
|
+
clearInterval(heartbeat);
|
|
767
|
+
unsubscribe();
|
|
768
|
+
};
|
|
769
|
+
req.once("aborted", close);
|
|
770
|
+
res.once("close", close);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
601
773
|
if (method === "GET" && pathname === "/api/operator/v1/workspaces") {
|
|
602
|
-
const
|
|
774
|
+
const snapshot = await registry.snapshotVisible();
|
|
775
|
+
const records = snapshot.workspaces;
|
|
603
776
|
const inspectReady = readinessDeps.inspectReady;
|
|
604
777
|
const readinessCache = readinessDeps.readinessCache ?? defaultWorkspaceReadinessCache;
|
|
605
778
|
const workspaces = records.map((record) => {
|
|
@@ -611,6 +784,7 @@ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathn
|
|
|
611
784
|
repoFingerprint: record.repoFingerprint,
|
|
612
785
|
createdAt: record.createdAt,
|
|
613
786
|
lastAccessedAt: record.lastAccessedAt,
|
|
787
|
+
sessionIds: record.sessionIds,
|
|
614
788
|
readiness: cached
|
|
615
789
|
? {
|
|
616
790
|
state: cached.state,
|
|
@@ -622,7 +796,8 @@ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathn
|
|
|
622
796
|
// Respond FIRST, then refresh readiness in the background (AC-006).
|
|
623
797
|
sendJson(res, 200, {
|
|
624
798
|
ok: true,
|
|
625
|
-
schemaVersion:
|
|
799
|
+
schemaVersion: 2,
|
|
800
|
+
revision: snapshot.revision,
|
|
626
801
|
workspaces,
|
|
627
802
|
});
|
|
628
803
|
void Promise.all(records.map(async (record) => {
|
|
@@ -789,8 +964,73 @@ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathn
|
|
|
789
964
|
});
|
|
790
965
|
return;
|
|
791
966
|
}
|
|
792
|
-
const
|
|
793
|
-
if (method === "
|
|
967
|
+
const workspaceMatch = pathname.match(/^\/api\/operator\/v1\/workspaces\/([^/]+)$/);
|
|
968
|
+
if (method === "PATCH" && workspaceMatch) {
|
|
969
|
+
const failure = evaluateMutationGate(req, {
|
|
970
|
+
cookieName: deps.bootToken.cookieName,
|
|
971
|
+
bootToken: deps.bootToken.value,
|
|
972
|
+
confirmationToken: deps.bootToken.confirmationToken,
|
|
973
|
+
consoleOrigin: deps.getConsoleOrigin(),
|
|
974
|
+
allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
|
|
975
|
+
});
|
|
976
|
+
if (failure) {
|
|
977
|
+
sendJson(res, failure.status, {
|
|
978
|
+
ok: false,
|
|
979
|
+
error: { code: failure.code, message: failure.message },
|
|
980
|
+
});
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
let body;
|
|
984
|
+
try {
|
|
985
|
+
body = (await readJsonBody(req));
|
|
986
|
+
}
|
|
987
|
+
catch (error) {
|
|
988
|
+
sendJson(res, 400, {
|
|
989
|
+
ok: false,
|
|
990
|
+
error: {
|
|
991
|
+
code: "INVALID_INPUT",
|
|
992
|
+
message: error instanceof Error ? error.message : String(error),
|
|
993
|
+
},
|
|
994
|
+
});
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
if (body.beforeWorkspaceId !== null &&
|
|
998
|
+
typeof body.beforeWorkspaceId !== "string") {
|
|
999
|
+
sendJson(res, 400, {
|
|
1000
|
+
ok: false,
|
|
1001
|
+
error: {
|
|
1002
|
+
code: "INVALID_INPUT",
|
|
1003
|
+
message: "beforeWorkspaceId must be a workspace id or null",
|
|
1004
|
+
},
|
|
1005
|
+
});
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
const workspaceId = decodeURIComponent(workspaceMatch[1]);
|
|
1009
|
+
const beforeWorkspaceId = typeof body.beforeWorkspaceId === "string"
|
|
1010
|
+
? body.beforeWorkspaceId
|
|
1011
|
+
: null;
|
|
1012
|
+
const reordered = await registry.insertBefore(workspaceId, beforeWorkspaceId);
|
|
1013
|
+
if (!reordered.ok) {
|
|
1014
|
+
sendJson(res, 404, {
|
|
1015
|
+
ok: false,
|
|
1016
|
+
error: {
|
|
1017
|
+
code: reordered.code,
|
|
1018
|
+
message: reordered.code === "ANCHOR_NOT_FOUND"
|
|
1019
|
+
? `unknown workspace anchor: ${beforeWorkspaceId}`
|
|
1020
|
+
: `unknown workspace: ${workspaceId}`,
|
|
1021
|
+
},
|
|
1022
|
+
});
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
sendJson(res, 200, {
|
|
1026
|
+
ok: true,
|
|
1027
|
+
workspaceId,
|
|
1028
|
+
beforeWorkspaceId,
|
|
1029
|
+
changed: reordered.changed,
|
|
1030
|
+
});
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
if (method === "DELETE" && workspaceMatch) {
|
|
794
1034
|
const failure = evaluateMutationGate(req, {
|
|
795
1035
|
cookieName: deps.bootToken.cookieName,
|
|
796
1036
|
bootToken: deps.bootToken.value,
|
|
@@ -805,7 +1045,7 @@ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathn
|
|
|
805
1045
|
});
|
|
806
1046
|
return;
|
|
807
1047
|
}
|
|
808
|
-
const workspaceId = decodeURIComponent(
|
|
1048
|
+
const workspaceId = decodeURIComponent(workspaceMatch[1]);
|
|
809
1049
|
const removed = await registry.remove(workspaceId);
|
|
810
1050
|
if (!removed) {
|
|
811
1051
|
sendJson(res, 404, {
|