@pi-harness/pi-harness 0.1.1
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 +161 -0
- package/apps/web/dist/assets/index-C4tLweAB.js +20 -0
- package/apps/web/dist/assets/index-DPgRbQ4_.css +1 -0
- package/apps/web/dist/index.html +14 -0
- package/apps/web/index.html +13 -0
- package/apps/web/package.json +24 -0
- package/apps/web/profile/cordis.yml +44 -0
- package/apps/web/server-dist/bin.d.ts +3 -0
- package/apps/web/server-dist/bin.d.ts.map +1 -0
- package/apps/web/server-dist/bin.js +67 -0
- package/apps/web/server-dist/bin.js.map +1 -0
- package/apps/web/src/command-palette.ts +84 -0
- package/apps/web/src/file-diff.ts +42 -0
- package/apps/web/src/main.ts +12 -0
- package/apps/web/src/runtime.css +4 -0
- package/apps/web/src/server/bin.ts +64 -0
- package/apps/web/src/style.css +7 -0
- package/apps/web/tsconfig.build.json +1 -0
- package/apps/web/tsconfig.json +1 -0
- package/package.json +48 -0
- package/packages/api-gateway/dist/index.d.ts +8 -0
- package/packages/api-gateway/dist/index.d.ts.map +1 -0
- package/packages/api-gateway/dist/index.js +582 -0
- package/packages/api-gateway/dist/index.js.map +1 -0
- package/packages/api-gateway/package.json +29 -0
- package/packages/api-gateway/src/index.ts +577 -0
- package/packages/api-gateway/test/index.test.ts +327 -0
- package/packages/api-gateway/tsconfig.build.json +1 -0
- package/packages/api-gateway/tsconfig.json +1 -0
- package/packages/bundle-web-app/cordis.patch.yml +12 -0
- package/packages/bundle-web-app/dist/index.d.ts +12 -0
- package/packages/bundle-web-app/dist/index.d.ts.map +1 -0
- package/packages/bundle-web-app/dist/index.js +63 -0
- package/packages/bundle-web-app/dist/index.js.map +1 -0
- package/packages/bundle-web-app/package.json +30 -0
- package/packages/bundle-web-app/src/index.ts +69 -0
- package/packages/bundle-web-app/test/index.test.ts +29 -0
- package/packages/bundle-web-app/tsconfig.build.json +1 -0
- package/packages/bundle-web-app/tsconfig.json +1 -0
- package/packages/cli/dist/args.d.ts +16 -0
- package/packages/cli/dist/args.d.ts.map +1 -0
- package/packages/cli/dist/args.js +53 -0
- package/packages/cli/dist/args.js.map +1 -0
- package/packages/cli/dist/bin.d.ts +3 -0
- package/packages/cli/dist/bin.d.ts.map +1 -0
- package/packages/cli/dist/bin.js +40 -0
- package/packages/cli/dist/bin.js.map +1 -0
- package/packages/cli/dist/main.d.ts +14 -0
- package/packages/cli/dist/main.d.ts.map +1 -0
- package/packages/cli/dist/main.js +148 -0
- package/packages/cli/dist/main.js.map +1 -0
- package/packages/cli/dist/node-stdio.d.ts +10 -0
- package/packages/cli/dist/node-stdio.d.ts.map +1 -0
- package/packages/cli/dist/node-stdio.js +34 -0
- package/packages/cli/dist/node-stdio.js.map +1 -0
- package/packages/cli/dist/relaunch.d.ts +5 -0
- package/packages/cli/dist/relaunch.d.ts.map +1 -0
- package/packages/cli/dist/relaunch.js +52 -0
- package/packages/cli/dist/relaunch.js.map +1 -0
- package/packages/cli/package.json +25 -0
- package/packages/cli/src/args.ts +50 -0
- package/packages/cli/src/bin.ts +41 -0
- package/packages/cli/src/main.ts +155 -0
- package/packages/cli/src/node-stdio.ts +40 -0
- package/packages/cli/src/relaunch.ts +48 -0
- package/packages/cli/test/args.test.ts +28 -0
- package/packages/cli/test/cli.test.ts +153 -0
- package/packages/cli/test/relaunch.test.ts +29 -0
- package/packages/cli/tsconfig.build.json +11 -0
- package/packages/cli/tsconfig.json +10 -0
- package/packages/client-web/dist/control-room.d.ts +102 -0
- package/packages/client-web/dist/control-room.d.ts.map +1 -0
- package/packages/client-web/dist/control-room.js +12 -0
- package/packages/client-web/dist/control-room.js.map +1 -0
- package/packages/client-web/dist/design-contract.d.ts +85 -0
- package/packages/client-web/dist/design-contract.d.ts.map +1 -0
- package/packages/client-web/dist/design-contract.js +99 -0
- package/packages/client-web/dist/design-contract.js.map +1 -0
- package/packages/client-web/dist/index.d.ts +17 -0
- package/packages/client-web/dist/index.d.ts.map +1 -0
- package/packages/client-web/dist/index.js +19 -0
- package/packages/client-web/dist/index.js.map +1 -0
- package/packages/client-web/dist/react-room.d.ts +6 -0
- package/packages/client-web/dist/react-room.d.ts.map +1 -0
- package/packages/client-web/dist/react-room.js +103 -0
- package/packages/client-web/dist/react-room.js.map +1 -0
- package/packages/client-web/package.json +29 -0
- package/packages/client-web/src/control-room.ts +10 -0
- package/packages/client-web/src/design-contract.ts +112 -0
- package/packages/client-web/src/index.ts +21 -0
- package/packages/client-web/src/react-room.tsx +78 -0
- package/packages/client-web/test/design-contract.test.ts +24 -0
- package/packages/client-web/tsconfig.build.json +1 -0
- package/packages/client-web/tsconfig.json +1 -0
- package/packages/core/dist/boot.d.ts +13 -0
- package/packages/core/dist/boot.d.ts.map +1 -0
- package/packages/core/dist/boot.js +96 -0
- package/packages/core/dist/boot.js.map +1 -0
- package/packages/core/dist/context.d.ts +3 -0
- package/packages/core/dist/context.d.ts.map +1 -0
- package/packages/core/dist/context.js +2 -0
- package/packages/core/dist/context.js.map +1 -0
- package/packages/core/dist/index.d.ts +7 -0
- package/packages/core/dist/index.d.ts.map +1 -0
- package/packages/core/dist/index.js +6 -0
- package/packages/core/dist/index.js.map +1 -0
- package/packages/core/dist/plugins/model.d.ts +8 -0
- package/packages/core/dist/plugins/model.d.ts.map +1 -0
- package/packages/core/dist/plugins/model.js +12 -0
- package/packages/core/dist/plugins/model.js.map +1 -0
- package/packages/core/dist/plugins/models.d.ts +16 -0
- package/packages/core/dist/plugins/models.d.ts.map +1 -0
- package/packages/core/dist/plugins/models.js +24 -0
- package/packages/core/dist/plugins/models.js.map +1 -0
- package/packages/core/dist/plugins/resources.d.ts +18 -0
- package/packages/core/dist/plugins/resources.d.ts.map +1 -0
- package/packages/core/dist/plugins/resources.js +48 -0
- package/packages/core/dist/plugins/resources.js.map +1 -0
- package/packages/core/dist/plugins/runtime.d.ts +14 -0
- package/packages/core/dist/plugins/runtime.d.ts.map +1 -0
- package/packages/core/dist/plugins/runtime.js +68 -0
- package/packages/core/dist/plugins/runtime.js.map +1 -0
- package/packages/core/dist/plugins/session.d.ts +15 -0
- package/packages/core/dist/plugins/session.d.ts.map +1 -0
- package/packages/core/dist/plugins/session.js +20 -0
- package/packages/core/dist/plugins/session.js.map +1 -0
- package/packages/core/dist/plugins/stdio.d.ts +8 -0
- package/packages/core/dist/plugins/stdio.d.ts.map +1 -0
- package/packages/core/dist/plugins/stdio.js +20 -0
- package/packages/core/dist/plugins/stdio.js.map +1 -0
- package/packages/core/dist/plugins/tools.d.ts +13 -0
- package/packages/core/dist/plugins/tools.d.ts.map +1 -0
- package/packages/core/dist/plugins/tools.js +17 -0
- package/packages/core/dist/plugins/tools.js.map +1 -0
- package/packages/core/dist/profile.d.ts +8 -0
- package/packages/core/dist/profile.d.ts.map +1 -0
- package/packages/core/dist/profile.js +28 -0
- package/packages/core/dist/profile.js.map +1 -0
- package/packages/core/dist/runtime.d.ts +16 -0
- package/packages/core/dist/runtime.d.ts.map +1 -0
- package/packages/core/dist/runtime.js +33 -0
- package/packages/core/dist/runtime.js.map +1 -0
- package/packages/core/dist/services.d.ts +63 -0
- package/packages/core/dist/services.d.ts.map +1 -0
- package/packages/core/dist/services.js +39 -0
- package/packages/core/dist/services.js.map +1 -0
- package/packages/core/dist/stdio.d.ts +25 -0
- package/packages/core/dist/stdio.d.ts.map +1 -0
- package/packages/core/dist/stdio.js +74 -0
- package/packages/core/dist/stdio.js.map +1 -0
- package/packages/core/package.json +44 -0
- package/packages/core/profiles/default/cordis.yml +35 -0
- package/packages/core/profiles/development/cordis.yml +47 -0
- package/packages/core/src/boot.ts +103 -0
- package/packages/core/src/context.ts +3 -0
- package/packages/core/src/index.ts +6 -0
- package/packages/core/src/plugins/model.ts +12 -0
- package/packages/core/src/plugins/models.ts +32 -0
- package/packages/core/src/plugins/resources.ts +57 -0
- package/packages/core/src/plugins/runtime.ts +73 -0
- package/packages/core/src/plugins/session.ts +27 -0
- package/packages/core/src/plugins/stdio.ts +20 -0
- package/packages/core/src/plugins/tools.ts +23 -0
- package/packages/core/src/profile.ts +31 -0
- package/packages/core/src/runtime.ts +39 -0
- package/packages/core/src/services.ts +105 -0
- package/packages/core/src/stdio.ts +88 -0
- package/packages/core/test/boot.test.ts +86 -0
- package/packages/core/test/profile.test.ts +39 -0
- package/packages/core/test/profiles.integration.test.ts +93 -0
- package/packages/core/test/runtime-fixture.ts +31 -0
- package/packages/core/test/runtime.test.ts +54 -0
- package/packages/core/test/services.test.ts +131 -0
- package/packages/core/test/stdio.test.ts +87 -0
- package/packages/core/tsconfig.build.json +10 -0
- package/packages/core/tsconfig.json +10 -0
- package/packages/host-webserver/dist/index.d.ts +29 -0
- package/packages/host-webserver/dist/index.d.ts.map +1 -0
- package/packages/host-webserver/dist/index.js +73 -0
- package/packages/host-webserver/dist/index.js.map +1 -0
- package/packages/host-webserver/package.json +27 -0
- package/packages/host-webserver/src/index.ts +97 -0
- package/packages/host-webserver/test/index.test.ts +43 -0
- package/packages/host-webserver/tsconfig.build.json +1 -0
- package/packages/host-webserver/tsconfig.json +1 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pi-harness/api-gateway",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Cordis HTTP API routes for the Pi Harness browser client",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22.19.0"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.build.json",
|
|
22
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@deepseek-ai/cordis": "4.0.1",
|
|
26
|
+
"@pi-harness/core": "0.1.1",
|
|
27
|
+
"@pi-harness/host-webserver": "0.1.1"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
4
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
5
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
6
|
+
import { SessionManager, type AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type Loader from "@deepseek-ai/cordis-plugin-loader";
|
|
8
|
+
import type { PiRuntimeService, PiModelsService, PiHarnessLaunch } from "@pi-harness/core";
|
|
9
|
+
import type { WebServer } from "@pi-harness/host-webserver";
|
|
10
|
+
|
|
11
|
+
interface ApiServices {
|
|
12
|
+
readonly runtime: PiRuntimeService;
|
|
13
|
+
readonly models: PiModelsService;
|
|
14
|
+
readonly launch: PiHarnessLaunch;
|
|
15
|
+
readonly webServer: WebServer;
|
|
16
|
+
readonly loader: Loader | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sendJson(response: ServerResponse, status: number, payload: unknown): void {
|
|
20
|
+
const body = JSON.stringify(payload);
|
|
21
|
+
response.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", "content-length": Buffer.byteLength(body) });
|
|
22
|
+
response.end(body);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function bodyText(request: IncomingMessage): Promise<string> {
|
|
26
|
+
const chunks: string[] = [];
|
|
27
|
+
let length = 0;
|
|
28
|
+
for await (const chunk of request) {
|
|
29
|
+
const input: string | Uint8Array = chunk as string | Uint8Array;
|
|
30
|
+
const value = typeof input === "string" ? input : Buffer.from(input).toString("utf8");
|
|
31
|
+
length += Buffer.byteLength(value);
|
|
32
|
+
if (length > 64 * 1024) throw new Error("Request body is too large");
|
|
33
|
+
chunks.push(value);
|
|
34
|
+
}
|
|
35
|
+
return chunks.join("");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function errorText(error: unknown): string {
|
|
39
|
+
return error instanceof Error ? error.message : String(error);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function jsonSafe(value: unknown, seen = new WeakSet<object>()): unknown {
|
|
43
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
44
|
+
if (typeof value === "bigint") return value.toString();
|
|
45
|
+
if (typeof value === "undefined") return null;
|
|
46
|
+
if (value instanceof Error) return { name: value.name, message: value.message, stack: value.stack };
|
|
47
|
+
if (value instanceof Date) return value.toISOString();
|
|
48
|
+
if (typeof value !== "object") return Object.prototype.toString.call(value);
|
|
49
|
+
if (seen.has(value)) return "[Circular]";
|
|
50
|
+
seen.add(value);
|
|
51
|
+
if (Array.isArray(value)) return value.map((item) => jsonSafe(item, seen));
|
|
52
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, jsonSafe(item, seen)]));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createStatus(services: ApiServices, events: readonly AgentSessionEvent[]) {
|
|
56
|
+
const activeModel = services.runtime.session.model ?? services.models.model;
|
|
57
|
+
return {
|
|
58
|
+
status: services.runtime.session.isStreaming ? "running" : "ready",
|
|
59
|
+
model: activeModel.provider + "/" + activeModel.id,
|
|
60
|
+
messages: services.runtime.session.messages.length,
|
|
61
|
+
events: events.length,
|
|
62
|
+
sessionId: services.runtime.session.sessionId,
|
|
63
|
+
sessionFile: services.runtime.session.sessionFile,
|
|
64
|
+
cwd: services.launch.cwd,
|
|
65
|
+
agentDir: services.launch.agentDir,
|
|
66
|
+
plugins: services.loader ? [...services.loader.entries()].filter((entry) => !entry.disabled).map((entry) => entry.options.name) : [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function modelSummary(model: { provider: string; id: string; name?: string; reasoning?: boolean; contextWindow?: number }, active: boolean) {
|
|
71
|
+
return { provider: model.provider, id: model.id, name: model.name ?? model.id, reasoning: model.reasoning ?? false, contextWindow: model.contextWindow ?? null, active };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function pluginSummary(entry: { options: { id: string; name: string; disabled?: boolean | null }; fiber?: { state: unknown } }) {
|
|
75
|
+
const states = ["pending", "loading", "active", "failed", "disposed", "unloading"];
|
|
76
|
+
const rawState = entry.fiber?.state;
|
|
77
|
+
const state = typeof rawState === "number" ? states[rawState] ?? String(rawState) : typeof rawState === "string" ? rawState : rawState === undefined || rawState === null ? "unloaded" : "unknown";
|
|
78
|
+
return { id: entry.options.id, name: entry.options.name, enabled: !entry.options.disabled, state };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function writeSse(response: ServerResponse, payload: unknown): void {
|
|
82
|
+
response.write(`data: ${JSON.stringify(jsonSafe(payload))}\n\n`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function gitStatus(cwd: string): Promise<string> {
|
|
86
|
+
return new Promise((resolve) => {
|
|
87
|
+
execFile("git", ["status", "--short", "--untracked-files=all"], { cwd, maxBuffer: 512 * 1024 }, (error, stdout) => resolve(error ? "" : stdout));
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function gitDiff(cwd: string, path: string): Promise<string> {
|
|
92
|
+
return new Promise((resolveOutput) => {
|
|
93
|
+
execFile("git", ["diff", "--no-ext-diff", "--", path], { cwd, maxBuffer: 1024 * 1024 }, (error, stdout) => resolveOutput(error && stdout.length === 0 ? "" : stdout));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function gitCommand(cwd: string, args: readonly string[]): Promise<{ stdout: string; stderr: string; code: number }> {
|
|
98
|
+
return new Promise((resolveResult) => {
|
|
99
|
+
execFile("git", [...args], { cwd, maxBuffer: 2 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
100
|
+
const code = error && typeof error.code === "number" ? error.code : error ? 1 : 0;
|
|
101
|
+
resolveResult({ stdout, stderr, code });
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function workspacePaths(root: string, paths: unknown): string[] | Error {
|
|
107
|
+
if (!Array.isArray(paths) || paths.length === 0 || paths.some((path) => typeof path !== "string" || path.trim() === "")) return new Error("paths must be a non-empty array of strings");
|
|
108
|
+
const normalized: string[] = [];
|
|
109
|
+
for (const path of paths) {
|
|
110
|
+
const requested = path as string;
|
|
111
|
+
const absolute = resolve(root, requested);
|
|
112
|
+
const relativePath = relative(root, absolute);
|
|
113
|
+
if (isAbsolute(relativePath) || relativePath === ".." || relativePath.startsWith("../")) return new Error("path must stay inside the workspace");
|
|
114
|
+
normalized.push(relativePath);
|
|
115
|
+
}
|
|
116
|
+
return [...new Set(normalized)];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export default {
|
|
120
|
+
name: "pi-api-gateway",
|
|
121
|
+
inject: ["webServer", "piRuntime", "piModels", "piHarnessLaunch"],
|
|
122
|
+
apply(context: Context) {
|
|
123
|
+
const services: ApiServices = {
|
|
124
|
+
runtime: context.piRuntime,
|
|
125
|
+
models: context.piModels,
|
|
126
|
+
launch: context.piHarnessLaunch,
|
|
127
|
+
webServer: context.webServer,
|
|
128
|
+
loader: context.reflect.get("loader") as Loader | undefined,
|
|
129
|
+
};
|
|
130
|
+
let busy = false;
|
|
131
|
+
const events: AgentSessionEvent[] = [];
|
|
132
|
+
const eventClients = new Set<ServerResponse>();
|
|
133
|
+
const handleEvent = (event: AgentSessionEvent) => {
|
|
134
|
+
events.push(event);
|
|
135
|
+
for (const response of eventClients) {
|
|
136
|
+
if (response.writableEnded || response.destroyed) {
|
|
137
|
+
eventClients.delete(response);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
writeSse(response, { type: "event", event });
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const unsubscribeEvents = services.runtime.sessionRuntime
|
|
144
|
+
? context.on("pi/session-event", handleEvent)
|
|
145
|
+
: services.runtime.session.subscribe(handleEvent);
|
|
146
|
+
const disposeStatus = services.webServer.register({
|
|
147
|
+
path: "/api/status",
|
|
148
|
+
handler(_request, response) {
|
|
149
|
+
sendJson(response, 200, createStatus(services, events));
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
const disposeModels = services.webServer.register({
|
|
153
|
+
path: "/api/models",
|
|
154
|
+
handler(_request, response) {
|
|
155
|
+
const active = services.runtime.session.model ?? services.models.model;
|
|
156
|
+
sendJson(response, 200, jsonSafe({ items: services.models.runtime.getModels().map((model) => modelSummary(model, model.provider === active.provider && model.id === active.id)) }));
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
const disposeProviders = services.webServer.register({
|
|
160
|
+
path: "/api/providers",
|
|
161
|
+
handler(_request, response) {
|
|
162
|
+
const active = services.runtime.session.model ?? services.models.model;
|
|
163
|
+
const runtime = services.models.runtime as typeof services.models.runtime & { getProviders?: () => readonly { id: string; name?: string }[]; getProviderAuthStatus?: (provider: string) => unknown };
|
|
164
|
+
const providers = typeof runtime.getProviders === "function" ? runtime.getProviders() : [{ id: active.provider, name: active.provider }];
|
|
165
|
+
const visible = providers.filter((provider) => {
|
|
166
|
+
if (provider.id === active.provider) return true;
|
|
167
|
+
if (typeof runtime.getProviderAuthStatus !== "function") return false;
|
|
168
|
+
const auth = runtime.getProviderAuthStatus(provider.id);
|
|
169
|
+
return typeof auth === "object" && auth !== null && (auth as { configured?: unknown }).configured === true;
|
|
170
|
+
});
|
|
171
|
+
sendJson(response, 200, jsonSafe({ items: visible.map((provider) => ({ provider: provider.id, name: provider.name ?? provider.id, active: provider.id === active.provider, auth: typeof runtime.getProviderAuthStatus === "function" ? runtime.getProviderAuthStatus(provider.id) : undefined, activeModel: provider.id === active.provider ? modelSummary(active, true) : undefined, models: services.models.runtime.getModels(provider.id).map((model) => modelSummary(model, model.provider === active.provider && model.id === active.id)) })) }));
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
const disposeProviderTest = services.webServer.register({
|
|
175
|
+
path: "/api/providers/test",
|
|
176
|
+
async handler(request, response) {
|
|
177
|
+
if (request.method !== "POST") {
|
|
178
|
+
sendJson(response, 405, { error: "Method not allowed" });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const payload = JSON.parse(await bodyText(request)) as { provider?: unknown };
|
|
183
|
+
if (typeof payload.provider !== "string" || payload.provider.trim() === "") {
|
|
184
|
+
sendJson(response, 400, { error: "provider is required" });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const runtime = services.models.runtime as typeof services.models.runtime & { checkAuth?: (provider: string) => Promise<unknown> };
|
|
188
|
+
if (typeof runtime.checkAuth !== "function") {
|
|
189
|
+
sendJson(response, 501, { error: "The active model runtime does not support provider checks" });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const auth = await runtime.checkAuth(payload.provider);
|
|
193
|
+
sendJson(response, 200, jsonSafe({ provider: payload.provider, reachable: auth !== undefined, auth }));
|
|
194
|
+
} catch (error) {
|
|
195
|
+
sendJson(response, 502, { error: errorText(error) });
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
const disposeProviderRefresh = services.webServer.register({
|
|
200
|
+
path: "/api/providers/refresh",
|
|
201
|
+
async handler(request, response) {
|
|
202
|
+
if (request.method !== "POST") {
|
|
203
|
+
sendJson(response, 405, { error: "Method not allowed" });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
const payload = JSON.parse(await bodyText(request)) as { provider?: unknown };
|
|
208
|
+
if (typeof payload.provider !== "string" || payload.provider.trim() === "") {
|
|
209
|
+
sendJson(response, 400, { error: "provider is required" });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const runtime = services.models.runtime as typeof services.models.runtime & { getAvailable?: (provider: string) => Promise<readonly unknown[]> };
|
|
213
|
+
if (typeof runtime.getAvailable !== "function") {
|
|
214
|
+
sendJson(response, 501, { error: "The active model runtime does not support provider refresh" });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const models = await runtime.getAvailable(payload.provider);
|
|
218
|
+
sendJson(response, 200, jsonSafe({ provider: payload.provider, models }));
|
|
219
|
+
} catch (error) {
|
|
220
|
+
sendJson(response, 502, { error: errorText(error) });
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
const disposePlugins = services.webServer.register({
|
|
225
|
+
path: "/api/plugins",
|
|
226
|
+
handler(_request, response) {
|
|
227
|
+
const items = services.loader ? [...services.loader.entries()].map(pluginSummary) : [];
|
|
228
|
+
sendJson(response, 200, jsonSafe({ items }));
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
const disposeCommands = services.webServer.register({
|
|
232
|
+
path: "/api/commands",
|
|
233
|
+
handler(_request, response) {
|
|
234
|
+
const commands = services.runtime.session.extensionRunner.getRegisteredCommands();
|
|
235
|
+
const items = commands.map((command) => ({ name: command.name, invocationName: command.invocationName, description: command.description, source: command.sourceInfo.path }));
|
|
236
|
+
sendJson(response, 200, jsonSafe({ items }));
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
const disposeModel = services.webServer.register({
|
|
240
|
+
path: "/api/model",
|
|
241
|
+
async handler(request, response) {
|
|
242
|
+
if (request.method !== "POST") {
|
|
243
|
+
sendJson(response, 405, { error: "Method not allowed" });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (services.runtime.session.isStreaming) {
|
|
247
|
+
sendJson(response, 409, { error: "Cannot change model while a prompt is running" });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
const payload = JSON.parse(await bodyText(request)) as { provider?: unknown; model?: unknown };
|
|
252
|
+
if (typeof payload.provider !== "string" || typeof payload.model !== "string" || payload.provider.trim() === "" || payload.model.trim() === "") {
|
|
253
|
+
sendJson(response, 400, { error: "provider and model are required" });
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const model = services.models.runtime.getModel(payload.provider, payload.model);
|
|
257
|
+
if (model === undefined) {
|
|
258
|
+
sendJson(response, 404, { error: `Model not found: ${payload.provider}/${payload.model}` });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (typeof services.runtime.session.setModel !== "function") {
|
|
262
|
+
sendJson(response, 501, { error: "The active Pi session does not support model switching" });
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
await services.runtime.session.setModel(model);
|
|
266
|
+
sendJson(response, 200, jsonSafe({ model: modelSummary(model, true) }));
|
|
267
|
+
} catch (error) {
|
|
268
|
+
sendJson(response, 400, { error: errorText(error) });
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
const disposeFiles = services.webServer.register({
|
|
273
|
+
path: "/api/files",
|
|
274
|
+
async handler(_request, response) {
|
|
275
|
+
const output = await gitStatus(services.launch.cwd);
|
|
276
|
+
const items = output.split("\n").map((line) => line.trimEnd()).filter((line) => line.length > 0).map((line) => {
|
|
277
|
+
const status = line.slice(0, 2).trim() || "??";
|
|
278
|
+
return { path: line.slice(3), status, label: status === "??" ? "untracked" : status.includes("D") ? "deleted" : status.includes("A") ? "added" : "modified" };
|
|
279
|
+
});
|
|
280
|
+
sendJson(response, 200, { items });
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
const disposeFileDiff = services.webServer.register({
|
|
284
|
+
path: "/api/files/diff",
|
|
285
|
+
async handler(request, response) {
|
|
286
|
+
if (request.method !== "GET") {
|
|
287
|
+
sendJson(response, 405, { error: "Method not allowed" });
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const url = new URL(request.url ?? "/api/files/diff", "http://localhost");
|
|
291
|
+
const requested = url.searchParams.get("path");
|
|
292
|
+
if (requested === null || requested.trim() === "") {
|
|
293
|
+
sendJson(response, 400, { error: "path is required" });
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const root = resolve(services.launch.cwd);
|
|
297
|
+
const absolute = resolve(root, requested);
|
|
298
|
+
const relativePath = relative(root, absolute);
|
|
299
|
+
if (isAbsolute(relativePath) || relativePath === ".." || relativePath.startsWith(".." + "/")) {
|
|
300
|
+
sendJson(response, 400, { error: "path must stay inside the workspace" });
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
sendJson(response, 200, { path: relativePath, diff: await gitDiff(root, relativePath) });
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
const disposeFileCommit = services.webServer.register({
|
|
307
|
+
path: "/api/files/commit",
|
|
308
|
+
async handler(request, response) {
|
|
309
|
+
if (request.method !== "POST") {
|
|
310
|
+
sendJson(response, 405, { error: "Method not allowed" });
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
const payload = JSON.parse(await bodyText(request)) as { paths?: unknown; message?: unknown };
|
|
315
|
+
const paths = workspacePaths(resolve(services.launch.cwd), payload.paths);
|
|
316
|
+
if (paths instanceof Error) {
|
|
317
|
+
sendJson(response, 400, { error: paths.message });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (typeof payload.message !== "string" || payload.message.trim() === "") {
|
|
321
|
+
sendJson(response, 400, { error: "message is required" });
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const root = resolve(services.launch.cwd);
|
|
325
|
+
const add = await gitCommand(root, ["add", "-A", "--", ...paths]);
|
|
326
|
+
if (add.code !== 0) {
|
|
327
|
+
sendJson(response, 409, { error: add.stderr.trim() || "Unable to stage workspace files" });
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const commit = await gitCommand(root, ["commit", "-m", payload.message.trim(), "--", ...paths]);
|
|
331
|
+
if (commit.code !== 0) {
|
|
332
|
+
sendJson(response, 409, { error: commit.stdout.trim() || commit.stderr.trim() || "Nothing to commit" });
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const head = await gitCommand(root, ["rev-parse", "--short", "HEAD"]);
|
|
336
|
+
sendJson(response, 200, { committed: true, message: payload.message.trim(), commit: head.stdout.trim() });
|
|
337
|
+
} catch (error) {
|
|
338
|
+
sendJson(response, 400, { error: errorText(error) });
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
const disposeFileRevert = services.webServer.register({
|
|
343
|
+
path: "/api/files/revert",
|
|
344
|
+
async handler(request, response) {
|
|
345
|
+
if (request.method !== "POST") {
|
|
346
|
+
sendJson(response, 405, { error: "Method not allowed" });
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
const payload = JSON.parse(await bodyText(request)) as { paths?: unknown; confirm?: unknown };
|
|
351
|
+
if (payload.confirm !== true) {
|
|
352
|
+
sendJson(response, 400, { error: "confirm must be true to discard workspace changes" });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const root = resolve(services.launch.cwd);
|
|
356
|
+
const paths = workspacePaths(root, payload.paths);
|
|
357
|
+
if (paths instanceof Error) {
|
|
358
|
+
sendJson(response, 400, { error: paths.message });
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const untracked = await gitCommand(root, ["ls-files", "--others", "--exclude-standard", "--", ...paths]);
|
|
362
|
+
const restore = await gitCommand(root, ["restore", "--worktree", "--staged", "--", ...paths]);
|
|
363
|
+
if (restore.code !== 0 && !restore.stderr.includes("pathspec")) {
|
|
364
|
+
sendJson(response, 409, { error: restore.stderr.trim() || "Unable to restore workspace files" });
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
for (const path of untracked.stdout.split("\n").map((item) => item.trim()).filter(Boolean)) {
|
|
368
|
+
await rm(resolve(root, path), { recursive: true, force: true });
|
|
369
|
+
}
|
|
370
|
+
sendJson(response, 200, { reverted: true, paths });
|
|
371
|
+
} catch (error) {
|
|
372
|
+
sendJson(response, 400, { error: errorText(error) });
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
const disposeEvents = services.webServer.register({
|
|
377
|
+
path: "/api/events",
|
|
378
|
+
handler(_request, response) {
|
|
379
|
+
response.writeHead(200, { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-cache, no-store", connection: "keep-alive", "x-accel-buffering": "no" });
|
|
380
|
+
writeSse(response, { type: "snapshot", sessionId: services.runtime.session.sessionId, events });
|
|
381
|
+
eventClients.add(response);
|
|
382
|
+
const heartbeat = setInterval(() => {
|
|
383
|
+
if (response.writableEnded || response.destroyed) {
|
|
384
|
+
clearInterval(heartbeat);
|
|
385
|
+
eventClients.delete(response);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
response.write(": heartbeat\n\n");
|
|
389
|
+
}, 15_000);
|
|
390
|
+
response.on("close", () => {
|
|
391
|
+
clearInterval(heartbeat);
|
|
392
|
+
eventClients.delete(response);
|
|
393
|
+
});
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
const disposePrompt = services.webServer.register({
|
|
397
|
+
path: "/api/prompt",
|
|
398
|
+
async handler(request, response) {
|
|
399
|
+
if (busy) {
|
|
400
|
+
sendJson(response, 409, { error: "Another prompt is already running" });
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
busy = true;
|
|
404
|
+
let unsubscribe: (() => void) | undefined;
|
|
405
|
+
try {
|
|
406
|
+
const payload = JSON.parse(await bodyText(request)) as { prompt?: unknown };
|
|
407
|
+
if (typeof payload.prompt !== "string" || payload.prompt.trim().length === 0) {
|
|
408
|
+
sendJson(response, 400, { error: "Prompt must be a non-empty string" });
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const chunks: string[] = [];
|
|
412
|
+
unsubscribe = services.runtime.session.subscribe((event) => {
|
|
413
|
+
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") chunks.push(event.assistantMessageEvent.delta);
|
|
414
|
+
});
|
|
415
|
+
await services.runtime.prompt(payload.prompt);
|
|
416
|
+
const last = services.runtime.session.messages.at(-1);
|
|
417
|
+
if (last?.role === "assistant" && (last.stopReason === "error" || last.stopReason === "aborted")) {
|
|
418
|
+
sendJson(response, 502, { error: last.errorMessage ?? "Request " + last.stopReason });
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
sendJson(response, 200, { reply: chunks.join(""), messages: services.runtime.session.messages.length });
|
|
422
|
+
} catch (error) {
|
|
423
|
+
sendJson(response, 400, { error: errorText(error) });
|
|
424
|
+
} finally {
|
|
425
|
+
unsubscribe?.();
|
|
426
|
+
busy = false;
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
const disposeAbort = services.webServer.register({
|
|
431
|
+
path: "/api/abort",
|
|
432
|
+
async handler(request, response) {
|
|
433
|
+
if (request.method !== "POST") {
|
|
434
|
+
sendJson(response, 405, { error: "Method not allowed" });
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
const wasStreaming = services.runtime.session.isStreaming;
|
|
439
|
+
await services.runtime.abort();
|
|
440
|
+
sendJson(response, 200, { aborted: wasStreaming });
|
|
441
|
+
} catch (error) {
|
|
442
|
+
sendJson(response, 500, { error: errorText(error) });
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
const disposeSession = services.webServer.register({
|
|
447
|
+
path: "/api/session",
|
|
448
|
+
handler(_request, response) {
|
|
449
|
+
const session = services.runtime.session;
|
|
450
|
+
const sessionManager = session.sessionManager;
|
|
451
|
+
sendJson(response, 200, jsonSafe({
|
|
452
|
+
sessionId: session.sessionId,
|
|
453
|
+
sessionFile: session.sessionFile,
|
|
454
|
+
messages: session.messages,
|
|
455
|
+
entries: typeof sessionManager?.getEntries === "function" ? sessionManager.getEntries() : [],
|
|
456
|
+
events,
|
|
457
|
+
}));
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
const disposeNewSession = services.webServer.register({
|
|
461
|
+
path: "/api/session/new",
|
|
462
|
+
async handler(request, response) {
|
|
463
|
+
if (request.method !== "POST") {
|
|
464
|
+
sendJson(response, 405, { error: "Method not allowed" });
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (services.runtime.session.isStreaming) {
|
|
468
|
+
sendJson(response, 409, { error: "Cannot create a session while a prompt is running" });
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
try {
|
|
472
|
+
if (services.runtime.sessionRuntime) {
|
|
473
|
+
const result = await services.runtime.sessionRuntime.newSession();
|
|
474
|
+
if (result.cancelled) {
|
|
475
|
+
sendJson(response, 409, { error: "Session creation was cancelled by an extension" });
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
const session = services.runtime.session;
|
|
481
|
+
session.sessionManager.newSession();
|
|
482
|
+
session.agent.state.messages = [];
|
|
483
|
+
}
|
|
484
|
+
events.length = 0;
|
|
485
|
+
const session = services.runtime.session;
|
|
486
|
+
for (const client of eventClients) writeSse(client, { type: "session", sessionId: session.sessionId, events: [] });
|
|
487
|
+
sendJson(response, 200, jsonSafe({ sessionId: session.sessionId, sessionFile: session.sessionFile, messages: services.runtime.sessionRuntime ? session.messages : [], events: [] }));
|
|
488
|
+
} catch (error) {
|
|
489
|
+
sendJson(response, 500, { error: errorText(error) });
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
const disposeOpenSession = services.webServer.register({
|
|
494
|
+
path: "/api/session/open",
|
|
495
|
+
async handler(request, response) {
|
|
496
|
+
if (request.method !== "POST") {
|
|
497
|
+
sendJson(response, 405, { error: "Method not allowed" });
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (services.runtime.session.isStreaming) {
|
|
501
|
+
sendJson(response, 409, { error: "Cannot switch sessions while a prompt is running" });
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
try {
|
|
505
|
+
const payload = JSON.parse(await bodyText(request)) as { path?: unknown; sessionId?: unknown };
|
|
506
|
+
const manager = services.runtime.session.sessionManager;
|
|
507
|
+
if (!manager.isPersisted()) {
|
|
508
|
+
sendJson(response, 409, { error: "Session switching requires JSONL session storage" });
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const items = await SessionManager.list(services.launch.cwd, manager.getSessionDir());
|
|
512
|
+
const target = items.find((item) => (typeof payload.path === "string" && item.path === payload.path) || (typeof payload.sessionId === "string" && item.id === payload.sessionId));
|
|
513
|
+
if (target === undefined) {
|
|
514
|
+
sendJson(response, 404, { error: "Session not found" });
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (services.runtime.sessionRuntime) {
|
|
518
|
+
const result = await services.runtime.sessionRuntime.switchSession(target.path, { cwdOverride: target.cwd });
|
|
519
|
+
if (result.cancelled) {
|
|
520
|
+
sendJson(response, 409, { error: "Session switch was cancelled by an extension" });
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
manager.setSessionFile(target.path);
|
|
526
|
+
if (typeof services.runtime.session.reload === "function") await services.runtime.session.reload();
|
|
527
|
+
else services.runtime.session.agent.state.messages = manager.buildSessionContext().messages;
|
|
528
|
+
}
|
|
529
|
+
events.length = 0;
|
|
530
|
+
for (const client of eventClients) writeSse(client, { type: "session", sessionId: services.runtime.session.sessionId, events: [] });
|
|
531
|
+
sendJson(response, 200, jsonSafe({ sessionId: services.runtime.session.sessionId, sessionFile: services.runtime.session.sessionFile, messages: services.runtime.session.messages, events: [] }));
|
|
532
|
+
} catch (error) {
|
|
533
|
+
sendJson(response, 400, { error: errorText(error) });
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
const disposeSessions = services.webServer.register({
|
|
538
|
+
path: "/api/sessions",
|
|
539
|
+
async handler(_request, response) {
|
|
540
|
+
try {
|
|
541
|
+
const session = services.runtime.session;
|
|
542
|
+
const manager = session.sessionManager;
|
|
543
|
+
const items = typeof manager.isPersisted === "function" && manager.isPersisted()
|
|
544
|
+
? await SessionManager.list(services.launch.cwd, manager.getSessionDir())
|
|
545
|
+
: [];
|
|
546
|
+
sendJson(response, 200, jsonSafe({ items: items.map((item) => ({ sessionId: item.id, path: item.path, name: item.name, cwd: item.cwd, created: item.created, modified: item.modified, messageCount: item.messageCount, firstMessage: item.firstMessage })) }));
|
|
547
|
+
} catch (error) {
|
|
548
|
+
sendJson(response, 500, { error: errorText(error) });
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
context.effect(() => () => {
|
|
553
|
+
disposeStatus();
|
|
554
|
+
disposeEvents();
|
|
555
|
+
disposeModels();
|
|
556
|
+
disposeProviders();
|
|
557
|
+
disposeProviderTest();
|
|
558
|
+
disposeProviderRefresh();
|
|
559
|
+
disposePlugins();
|
|
560
|
+
disposeCommands();
|
|
561
|
+
disposeModel();
|
|
562
|
+
disposeFiles();
|
|
563
|
+
disposeFileDiff();
|
|
564
|
+
disposeFileCommit();
|
|
565
|
+
disposeFileRevert();
|
|
566
|
+
disposePrompt();
|
|
567
|
+
disposeAbort();
|
|
568
|
+
disposeSession();
|
|
569
|
+
disposeNewSession();
|
|
570
|
+
disposeOpenSession();
|
|
571
|
+
disposeSessions();
|
|
572
|
+
unsubscribeEvents();
|
|
573
|
+
for (const response of eventClients) response.end();
|
|
574
|
+
eventClients.clear();
|
|
575
|
+
});
|
|
576
|
+
},
|
|
577
|
+
};
|