@evandu/super-pi 0.0.0-stage → 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 +79 -2
- package/agents/coder.md +10 -0
- package/agents/critic.md +20 -0
- package/agents/patcher.md +23 -0
- package/agents/planner.md +18 -0
- package/agents/reviewer.md +13 -0
- package/agents/scout.md +23 -0
- package/agents/super-pi-task/core.ts +485 -0
- package/agents/super-pi-task/index.ts +141 -0
- package/agents/verifier.md +21 -0
- package/bin/super-pi.js +17 -0
- package/dist/client/assets/index-BMQsEdio.js +24 -0
- package/dist/client/assets/index-CsbZzuMy.css +1 -0
- package/dist/client/index.html +14 -0
- package/dist/extension.d.ts +15 -0
- package/dist/extension.js +111 -0
- package/dist/server/agents.d.ts +177 -0
- package/dist/server/agents.js +484 -0
- package/dist/server/application/catalog-service.d.ts +44 -0
- package/dist/server/application/catalog-service.js +94 -0
- package/dist/server/application/conversation-service.d.ts +73 -0
- package/dist/server/application/conversation-service.js +202 -0
- package/dist/server/application/extension-ui-service.d.ts +28 -0
- package/dist/server/application/extension-ui-service.js +73 -0
- package/dist/server/application/run-service.d.ts +115 -0
- package/dist/server/application/run-service.js +341 -0
- package/dist/server/application/workspace-service.d.ts +45 -0
- package/dist/server/application/workspace-service.js +106 -0
- package/dist/server/command-handler.d.ts +22 -0
- package/dist/server/command-handler.js +137 -0
- package/dist/server/files.d.ts +202 -0
- package/dist/server/files.js +629 -0
- package/dist/server/http-server.d.ts +20 -0
- package/dist/server/http-server.js +313 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +103 -0
- package/dist/server/pi-command.d.ts +65 -0
- package/dist/server/pi-command.js +183 -0
- package/dist/server/runtime.d.ts +819 -0
- package/dist/server/runtime.js +1470 -0
- package/dist/server/server-context.d.ts +46 -0
- package/dist/server/server-context.js +1 -0
- package/dist/server/store.d.ts +424 -0
- package/dist/server/store.js +1121 -0
- package/dist/server/subagent-runs.d.ts +142 -0
- package/dist/server/subagent-runs.js +315 -0
- package/dist/server/subagents.d.ts +194 -0
- package/dist/server/subagents.js +542 -0
- package/dist/server/task-tool.d.ts +196 -0
- package/dist/server/task-tool.js +285 -0
- package/dist/server/tool-calls.d.ts +62 -0
- package/dist/server/tool-calls.js +141 -0
- package/dist/server/types.d.ts +920 -0
- package/dist/server/types.js +11 -0
- package/dist/server/upload.d.ts +37 -0
- package/dist/server/upload.js +169 -0
- package/dist/server/ws-server.d.ts +45 -0
- package/dist/server/ws-server.js +116 -0
- package/package.json +115 -4
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The nine methods Pi's `RpcExtensionUIRequest` union defines (rpc-types.ts:252-287), split by whether
|
|
3
|
+
* Pi waits for a reply. `blocking` is the only field the transport needs in order to decide, and it
|
|
4
|
+
* is derived here rather than at each call site so the two halves can never drift apart.
|
|
5
|
+
*/
|
|
6
|
+
export const EXTENSION_UI_METHODS = [
|
|
7
|
+
"select", "confirm", "input", "editor",
|
|
8
|
+
"notify", "setStatus", "setWidget", "setTitle", "set_editor_text",
|
|
9
|
+
];
|
|
10
|
+
/** Pi registers exactly these four in `pendingExtensionRequests`; the other five are fire-and-forget. */
|
|
11
|
+
export const BLOCKING_UI_METHODS = new Set(["select", "confirm", "input", "editor"]);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { MessageImage } from "./types.js";
|
|
2
|
+
/** 5MB maximum decoded image payload, matching the source-file size ceiling in files.ts. */
|
|
3
|
+
export declare const MAX_UPLOAD_BYTES: number;
|
|
4
|
+
/** 10MB maximum HTTP request body to safely handle base64 overhead before JSON parsing. */
|
|
5
|
+
export declare const MAX_UPLOAD_BODY_BYTES: number;
|
|
6
|
+
/**
|
|
7
|
+
* Strict image MIME type allowlist.
|
|
8
|
+
* SVGs are explicitly excluded because rendered SVGs can execute arbitrary JavaScript (XSS).
|
|
9
|
+
*/
|
|
10
|
+
export declare const ALLOWED_IMAGE_MIMES: Set<string>;
|
|
11
|
+
export declare function extForMime(mime: string): string;
|
|
12
|
+
export declare function mimeForExt(ext: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Sniffs magic bytes to verify that buffer contents match the declared image MIME type.
|
|
15
|
+
* This prevents masquerading non-image files or dangerous content under image extensions.
|
|
16
|
+
*/
|
|
17
|
+
export declare function validateImageMagic(buffer: Buffer, mimeType: string): boolean;
|
|
18
|
+
export type UploadRecord = {
|
|
19
|
+
id: string;
|
|
20
|
+
conversationId: string;
|
|
21
|
+
name: string;
|
|
22
|
+
mimeType: string;
|
|
23
|
+
bytes: number;
|
|
24
|
+
url: string;
|
|
25
|
+
};
|
|
26
|
+
export declare function saveUpload(uploadsDir: string, conversationId: string, clientName: string | undefined, mimeType: string, base64Data: string): Promise<UploadRecord>;
|
|
27
|
+
export declare function findUploadFile(uploadsDir: string, conversationId: string | undefined, filenameOrId: string): Promise<{
|
|
28
|
+
filePath: string;
|
|
29
|
+
mimeType: string;
|
|
30
|
+
size: number;
|
|
31
|
+
} | null>;
|
|
32
|
+
export declare function deleteConversationUploads(uploadsDir: string, conversationId: string): Promise<void>;
|
|
33
|
+
export declare function loadConversationImages(uploadsDir: string, conversationId: string, images: MessageImage[]): Promise<Array<{
|
|
34
|
+
type: "image";
|
|
35
|
+
data: string;
|
|
36
|
+
mimeType: string;
|
|
37
|
+
}>>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { extname, join } from "node:path";
|
|
4
|
+
/** 5MB maximum decoded image payload, matching the source-file size ceiling in files.ts. */
|
|
5
|
+
export const MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
|
|
6
|
+
/** 10MB maximum HTTP request body to safely handle base64 overhead before JSON parsing. */
|
|
7
|
+
export const MAX_UPLOAD_BODY_BYTES = 10 * 1024 * 1024;
|
|
8
|
+
/**
|
|
9
|
+
* Strict image MIME type allowlist.
|
|
10
|
+
* SVGs are explicitly excluded because rendered SVGs can execute arbitrary JavaScript (XSS).
|
|
11
|
+
*/
|
|
12
|
+
export const ALLOWED_IMAGE_MIMES = new Set([
|
|
13
|
+
"image/png",
|
|
14
|
+
"image/jpeg",
|
|
15
|
+
"image/gif",
|
|
16
|
+
"image/webp",
|
|
17
|
+
]);
|
|
18
|
+
export function extForMime(mime) {
|
|
19
|
+
switch (mime) {
|
|
20
|
+
case "image/png": return ".png";
|
|
21
|
+
case "image/jpeg": return ".jpg";
|
|
22
|
+
case "image/gif": return ".gif";
|
|
23
|
+
case "image/webp": return ".webp";
|
|
24
|
+
default: return "";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function mimeForExt(ext) {
|
|
28
|
+
switch (ext.toLowerCase()) {
|
|
29
|
+
case ".png": return "image/png";
|
|
30
|
+
case ".jpg":
|
|
31
|
+
case ".jpeg": return "image/jpeg";
|
|
32
|
+
case ".gif": return "image/gif";
|
|
33
|
+
case ".webp": return "image/webp";
|
|
34
|
+
default: return "application/octet-stream";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Sniffs magic bytes to verify that buffer contents match the declared image MIME type.
|
|
39
|
+
* This prevents masquerading non-image files or dangerous content under image extensions.
|
|
40
|
+
*/
|
|
41
|
+
export function validateImageMagic(buffer, mimeType) {
|
|
42
|
+
if (buffer.length < 8)
|
|
43
|
+
return false;
|
|
44
|
+
switch (mimeType) {
|
|
45
|
+
case "image/png":
|
|
46
|
+
return buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47 &&
|
|
47
|
+
buffer[4] === 0x0d && buffer[5] === 0x0a && buffer[6] === 0x1a && buffer[7] === 0x0a;
|
|
48
|
+
case "image/jpeg":
|
|
49
|
+
return buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff;
|
|
50
|
+
case "image/gif":
|
|
51
|
+
return buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x38 &&
|
|
52
|
+
(buffer[4] === 0x37 || buffer[4] === 0x39) && buffer[5] === 0x61;
|
|
53
|
+
case "image/webp":
|
|
54
|
+
return buffer.length >= 12 &&
|
|
55
|
+
buffer.toString("ascii", 0, 4) === "RIFF" &&
|
|
56
|
+
buffer.toString("ascii", 8, 12) === "WEBP";
|
|
57
|
+
default:
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function saveUpload(uploadsDir, conversationId, clientName, mimeType, base64Data) {
|
|
62
|
+
if (!ALLOWED_IMAGE_MIMES.has(mimeType)) {
|
|
63
|
+
throw new Error(`不支持的图片格式: ${mimeType}。仅支持 PNG、JPEG、GIF 和 WebP`);
|
|
64
|
+
}
|
|
65
|
+
const buffer = Buffer.from(base64Data, "base64");
|
|
66
|
+
if (buffer.length === 0) {
|
|
67
|
+
throw new Error("上传的图片数据为空");
|
|
68
|
+
}
|
|
69
|
+
if (buffer.length > MAX_UPLOAD_BYTES) {
|
|
70
|
+
throw new Error(`图片大小(${(buffer.length / 1024 / 1024).toFixed(1)}MB)超过 5MB 上限`);
|
|
71
|
+
}
|
|
72
|
+
if (!validateImageMagic(buffer, mimeType)) {
|
|
73
|
+
throw new Error("图片数据与声明的 MIME 类型不符或文件损坏");
|
|
74
|
+
}
|
|
75
|
+
const id = crypto.randomUUID();
|
|
76
|
+
const ext = extForMime(mimeType);
|
|
77
|
+
const filename = `${id}${ext}`;
|
|
78
|
+
const conversationDir = join(uploadsDir, conversationId);
|
|
79
|
+
await mkdir(conversationDir, { recursive: true });
|
|
80
|
+
const targetPath = join(conversationDir, filename);
|
|
81
|
+
await writeFile(targetPath, buffer);
|
|
82
|
+
const cleanName = (clientName?.trim() || filename).slice(0, 120);
|
|
83
|
+
const meta = {
|
|
84
|
+
id,
|
|
85
|
+
conversationId,
|
|
86
|
+
name: cleanName,
|
|
87
|
+
mimeType,
|
|
88
|
+
bytes: buffer.length,
|
|
89
|
+
createdAt: new Date().toISOString(),
|
|
90
|
+
};
|
|
91
|
+
await writeFile(join(conversationDir, `${id}.meta.json`), JSON.stringify(meta), "utf8");
|
|
92
|
+
return {
|
|
93
|
+
id,
|
|
94
|
+
conversationId,
|
|
95
|
+
name: cleanName,
|
|
96
|
+
mimeType,
|
|
97
|
+
bytes: buffer.length,
|
|
98
|
+
url: `/api/upload/${conversationId}/${filename}`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export async function findUploadFile(uploadsDir, conversationId, filenameOrId) {
|
|
102
|
+
if (!existsSync(uploadsDir))
|
|
103
|
+
return null;
|
|
104
|
+
// Syntactic safety check: reject path traversal
|
|
105
|
+
if (filenameOrId.includes("..") || filenameOrId.includes("/") || filenameOrId.includes("\\"))
|
|
106
|
+
return null;
|
|
107
|
+
if (conversationId) {
|
|
108
|
+
if (conversationId.includes("..") || conversationId.includes("/") || conversationId.includes("\\"))
|
|
109
|
+
return null;
|
|
110
|
+
const directPath = join(uploadsDir, conversationId, filenameOrId);
|
|
111
|
+
if (existsSync(directPath)) {
|
|
112
|
+
const s = await stat(directPath);
|
|
113
|
+
if (s.isFile()) {
|
|
114
|
+
const mime = mimeForExt(extname(directPath));
|
|
115
|
+
return { filePath: directPath, mimeType: mime, size: s.size };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const dir = join(uploadsDir, conversationId);
|
|
119
|
+
if (existsSync(dir)) {
|
|
120
|
+
const entries = await readdir(dir).catch(() => []);
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
if (!entry.endsWith(".meta.json") && (entry === filenameOrId || entry.startsWith(`${filenameOrId}.`))) {
|
|
123
|
+
const fp = join(dir, entry);
|
|
124
|
+
const s = await stat(fp);
|
|
125
|
+
if (s.isFile()) {
|
|
126
|
+
return { filePath: fp, mimeType: mimeForExt(extname(fp)), size: s.size };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Fallback: search across all conversation upload directories
|
|
133
|
+
const convDirs = await readdir(uploadsDir).catch(() => []);
|
|
134
|
+
for (const conv of convDirs) {
|
|
135
|
+
const dir = join(uploadsDir, conv);
|
|
136
|
+
const entries = await readdir(dir).catch(() => []);
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
if (!entry.endsWith(".meta.json") && (entry === filenameOrId || entry.startsWith(`${filenameOrId}.`))) {
|
|
139
|
+
const fp = join(dir, entry);
|
|
140
|
+
const s = await stat(fp);
|
|
141
|
+
if (s.isFile()) {
|
|
142
|
+
return { filePath: fp, mimeType: mimeForExt(extname(fp)), size: s.size };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
export async function deleteConversationUploads(uploadsDir, conversationId) {
|
|
150
|
+
if (conversationId.includes("..") || conversationId.includes("/") || conversationId.includes("\\"))
|
|
151
|
+
return;
|
|
152
|
+
const dir = join(uploadsDir, conversationId);
|
|
153
|
+
await rm(dir, { recursive: true, force: true }).catch(() => { });
|
|
154
|
+
}
|
|
155
|
+
export async function loadConversationImages(uploadsDir, conversationId, images) {
|
|
156
|
+
const result = [];
|
|
157
|
+
for (const img of images) {
|
|
158
|
+
const found = await findUploadFile(uploadsDir, conversationId, img.id);
|
|
159
|
+
if (found) {
|
|
160
|
+
const buf = await readFile(found.filePath);
|
|
161
|
+
result.push({
|
|
162
|
+
type: "image",
|
|
163
|
+
data: buf.toString("base64"),
|
|
164
|
+
mimeType: img.mimeType || found.mimeType,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Server as NodeHttpServer } from "node:http";
|
|
2
|
+
import { WebSocket } from "ws";
|
|
3
|
+
import type { AppState, ServerEvent } from "./types.js";
|
|
4
|
+
import type { EventBroadcaster } from "./server-context.js";
|
|
5
|
+
import type { CommandHandler } from "./command-handler.js";
|
|
6
|
+
import type { CatalogService } from "./application/catalog-service.js";
|
|
7
|
+
/**
|
|
8
|
+
* Concrete implementation of EventBroadcaster delivering server events
|
|
9
|
+
* across active WebSocket connections.
|
|
10
|
+
*/
|
|
11
|
+
export declare class WsBroadcaster implements EventBroadcaster {
|
|
12
|
+
private readonly getState;
|
|
13
|
+
private readonly sockets;
|
|
14
|
+
constructor(getState: () => AppState);
|
|
15
|
+
register(socket: WebSocket): void;
|
|
16
|
+
unregister(socket: WebSocket): void;
|
|
17
|
+
getSockets(): Set<WebSocket>;
|
|
18
|
+
broadcast(event: ServerEvent): void;
|
|
19
|
+
broadcastState(): void;
|
|
20
|
+
getStateSnapshot(): AppState;
|
|
21
|
+
}
|
|
22
|
+
export interface WsServerOptions {
|
|
23
|
+
httpServer: NodeHttpServer;
|
|
24
|
+
broadcaster: WsBroadcaster;
|
|
25
|
+
commandHandler: CommandHandler;
|
|
26
|
+
catalogService: CatalogService;
|
|
27
|
+
accessToken?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* WebSocket transport manager handling upgrade, connection lifecycles, and command dispatch.
|
|
31
|
+
*/
|
|
32
|
+
export declare class WsServer {
|
|
33
|
+
private readonly websocketServer;
|
|
34
|
+
private readonly broadcaster;
|
|
35
|
+
private readonly commandHandler;
|
|
36
|
+
private readonly catalogService;
|
|
37
|
+
private readonly accessToken?;
|
|
38
|
+
constructor(options: WsServerOptions);
|
|
39
|
+
private setupUpgrade;
|
|
40
|
+
private setupConnectionHandling;
|
|
41
|
+
/**
|
|
42
|
+
* Gracefully closes all active WebSocket connections and terminates the server.
|
|
43
|
+
*/
|
|
44
|
+
close(): void;
|
|
45
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { WebSocket, WebSocketServer } from "ws";
|
|
2
|
+
import { isAuthorized } from "./http-server.js";
|
|
3
|
+
/**
|
|
4
|
+
* Concrete implementation of EventBroadcaster delivering server events
|
|
5
|
+
* across active WebSocket connections.
|
|
6
|
+
*/
|
|
7
|
+
export class WsBroadcaster {
|
|
8
|
+
getState;
|
|
9
|
+
sockets = new Set();
|
|
10
|
+
constructor(getState) {
|
|
11
|
+
this.getState = getState;
|
|
12
|
+
}
|
|
13
|
+
register(socket) {
|
|
14
|
+
this.sockets.add(socket);
|
|
15
|
+
}
|
|
16
|
+
unregister(socket) {
|
|
17
|
+
this.sockets.delete(socket);
|
|
18
|
+
}
|
|
19
|
+
getSockets() {
|
|
20
|
+
return this.sockets;
|
|
21
|
+
}
|
|
22
|
+
broadcast(event) {
|
|
23
|
+
const payload = JSON.stringify(event);
|
|
24
|
+
for (const socket of this.sockets) {
|
|
25
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
26
|
+
socket.send(payload);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
broadcastState() {
|
|
31
|
+
this.broadcast({ type: "state", state: this.getState() });
|
|
32
|
+
}
|
|
33
|
+
getStateSnapshot() {
|
|
34
|
+
return this.getState();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* WebSocket transport manager handling upgrade, connection lifecycles, and command dispatch.
|
|
39
|
+
*/
|
|
40
|
+
export class WsServer {
|
|
41
|
+
websocketServer;
|
|
42
|
+
broadcaster;
|
|
43
|
+
commandHandler;
|
|
44
|
+
catalogService;
|
|
45
|
+
accessToken;
|
|
46
|
+
constructor(options) {
|
|
47
|
+
this.broadcaster = options.broadcaster;
|
|
48
|
+
this.commandHandler = options.commandHandler;
|
|
49
|
+
this.catalogService = options.catalogService;
|
|
50
|
+
this.accessToken = options.accessToken;
|
|
51
|
+
this.websocketServer = new WebSocketServer({ noServer: true });
|
|
52
|
+
this.setupUpgrade(options.httpServer);
|
|
53
|
+
this.setupConnectionHandling();
|
|
54
|
+
}
|
|
55
|
+
setupUpgrade(httpServer) {
|
|
56
|
+
httpServer.on("upgrade", (request, socket, head) => {
|
|
57
|
+
if (!isAuthorized(request, this.accessToken) || new URL(request.url ?? "/", "http://localhost").pathname !== "/ws") {
|
|
58
|
+
socket.destroy();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
this.websocketServer.handleUpgrade(request, socket, head, (ws) => {
|
|
62
|
+
this.websocketServer.emit("connection", ws, request);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
setupConnectionHandling() {
|
|
67
|
+
this.websocketServer.on("connection", (socket) => {
|
|
68
|
+
this.broadcaster.register(socket);
|
|
69
|
+
socket.send(JSON.stringify({ type: "state", state: this.broadcaster.getStateSnapshot() }));
|
|
70
|
+
void this.catalogService.agentCatalog().then((catalog) => {
|
|
71
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
72
|
+
socket.send(JSON.stringify({ type: "agent.catalog", catalog }));
|
|
73
|
+
}
|
|
74
|
+
}).catch((error) => console.warn(`Super Pi: agent catalog for a new connection failed: ${error instanceof Error ? error.message : "unknown error"}`));
|
|
75
|
+
void this.catalogService.subagentCatalog().then((catalog) => {
|
|
76
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
77
|
+
socket.send(JSON.stringify({ type: "subagent.catalog", catalog }));
|
|
78
|
+
}
|
|
79
|
+
}).catch((error) => console.warn(`Super Pi: sub-agent catalog for a new connection failed: ${error instanceof Error ? error.message : "unknown error"}`));
|
|
80
|
+
const heartbeat = setInterval(() => {
|
|
81
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
82
|
+
socket.ping();
|
|
83
|
+
}
|
|
84
|
+
}, 30_000);
|
|
85
|
+
socket.on("close", () => {
|
|
86
|
+
clearInterval(heartbeat);
|
|
87
|
+
this.broadcaster.unregister(socket);
|
|
88
|
+
});
|
|
89
|
+
socket.on("message", async (raw) => {
|
|
90
|
+
try {
|
|
91
|
+
const command = JSON.parse(raw.toString());
|
|
92
|
+
await this.commandHandler.handleCommand(command, (event) => {
|
|
93
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
94
|
+
socket.send(JSON.stringify(event));
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
socket.send(JSON.stringify({
|
|
100
|
+
type: "error",
|
|
101
|
+
message: error instanceof Error ? error.message : "command failed",
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Gracefully closes all active WebSocket connections and terminates the server.
|
|
109
|
+
*/
|
|
110
|
+
close() {
|
|
111
|
+
for (const socket of this.broadcaster.getSockets()) {
|
|
112
|
+
socket.close(1001, "server-shutdown");
|
|
113
|
+
}
|
|
114
|
+
this.websocketServer.close();
|
|
115
|
+
}
|
|
116
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,117 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evandu/super-pi",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Web control plane and companion for Pi",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"super-pi": "bin/super-pi.js"
|
|
9
|
+
},
|
|
10
|
+
"pi": {
|
|
11
|
+
"extensions": [
|
|
12
|
+
"./dist/extension.js"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"agents",
|
|
18
|
+
"bin",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=22.19.0"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"prepack": "npm run build",
|
|
26
|
+
"dev": "concurrently --kill-others-on-fail \"npm:dev:server\" \"npm:dev:client\"",
|
|
27
|
+
"dev:client": "vite --host 0.0.0.0",
|
|
28
|
+
"dev:server": "tsx watch src/server/index.ts",
|
|
29
|
+
"build": "tsc -b && vite build",
|
|
30
|
+
"start": "node dist/server/index.js",
|
|
31
|
+
"restart:server": "node scripts/restart-server.mjs",
|
|
32
|
+
"smoke": "npm run build && node scripts/smoke.mjs",
|
|
33
|
+
"check": "node scripts/run-check.mjs"
|
|
34
|
+
},
|
|
35
|
+
"check:logic": [
|
|
36
|
+
"scripts/dispatch-ancestry.test.mjs",
|
|
37
|
+
"scripts/dispatch-endpoint.test.mjs",
|
|
38
|
+
"scripts/dispatched-by.test.mjs",
|
|
39
|
+
"scripts/verify-source-text.test.mjs",
|
|
40
|
+
"scripts/runtime-extension-ui.test.mjs",
|
|
41
|
+
"scripts/agent-mechanism.test.mjs",
|
|
42
|
+
"scripts/agent-wiring.test.mjs",
|
|
43
|
+
"scripts/message-timestamps.test.mjs",
|
|
44
|
+
"scripts/pi-resolution.test.mjs",
|
|
45
|
+
"scripts/agent-real-pi.test.mjs",
|
|
46
|
+
"scripts/image-input-real-pi.test.mjs",
|
|
47
|
+
"scripts/image-upload.test.mjs",
|
|
48
|
+
"scripts/extension-ui-e2e.test.mjs",
|
|
49
|
+
"scripts/session-picker.test.mjs",
|
|
50
|
+
"scripts/workspace-cwd.test.mjs",
|
|
51
|
+
"scripts/subagents.test.mjs",
|
|
52
|
+
"scripts/subagent-noise.test.mjs",
|
|
53
|
+
"scripts/subagent-runs.test.mjs",
|
|
54
|
+
"scripts/conversation-reconcile.test.mjs",
|
|
55
|
+
"scripts/subagent-dispatch.test.mjs",
|
|
56
|
+
"scripts/agent-vis-records.test.mjs",
|
|
57
|
+
"scripts/queue-store.test.mjs",
|
|
58
|
+
"scripts/followup.test.mjs",
|
|
59
|
+
"scripts/md-render.test.mjs",
|
|
60
|
+
"scripts/revert-session-format.test.mjs",
|
|
61
|
+
"scripts/revert-session-file.test.mjs",
|
|
62
|
+
"scripts/revert-real-pi.test.mjs",
|
|
63
|
+
"scripts/bg-task.test.mjs",
|
|
64
|
+
"scripts/todo-tool.test.mjs",
|
|
65
|
+
"scripts/tool-calls.test.mjs",
|
|
66
|
+
"scripts/files-api.test.mjs",
|
|
67
|
+
"scripts/files-missing-path.test.mjs",
|
|
68
|
+
"scripts/task-tool.test.mjs",
|
|
69
|
+
"scripts/fork-server.test.mjs",
|
|
70
|
+
"scripts/styles-no-dangling.test.mjs",
|
|
71
|
+
"scripts/theme-no-hardcoded-colours.test.mjs"
|
|
72
|
+
],
|
|
73
|
+
"check:browser": [
|
|
74
|
+
"scripts/extension-ui-browser.test.mjs",
|
|
75
|
+
"scripts/inline-rename-blur.test.mjs",
|
|
76
|
+
"scripts/message-timestamps-browser.test.mjs",
|
|
77
|
+
"scripts/session-picker-browser.test.mjs",
|
|
78
|
+
"scripts/workspace-settings-browser.test.mjs",
|
|
79
|
+
"scripts/subagent-card-browser.test.mjs",
|
|
80
|
+
"scripts/conversation-reconcile-browser.test.mjs",
|
|
81
|
+
"scripts/visual-defects-browser.test.mjs",
|
|
82
|
+
"scripts/agent-vis-card-browser.test.mjs",
|
|
83
|
+
"scripts/queue-dock.test.mjs",
|
|
84
|
+
"scripts/md-render-browser.test.mjs",
|
|
85
|
+
"scripts/right-sidebar-browser.test.mjs",
|
|
86
|
+
"scripts/sidebar-tools-browser.test.mjs",
|
|
87
|
+
"scripts/tool-call-blocks-browser.test.mjs",
|
|
88
|
+
"scripts/files-tab-browser.test.mjs",
|
|
89
|
+
"scripts/msg-tabs-browser.test.mjs",
|
|
90
|
+
"scripts/tabs-on-top-browser.test.mjs",
|
|
91
|
+
"scripts/subagent-source.test.mjs",
|
|
92
|
+
"scripts/fork-resize-browser.test.mjs",
|
|
93
|
+
"scripts/msg-bubbles.test.mjs",
|
|
94
|
+
"scripts/narrow-layout.test.mjs",
|
|
95
|
+
"scripts/scrollbar-style.test.mjs",
|
|
96
|
+
"scripts/theme-contrast.test.mjs",
|
|
97
|
+
"scripts/theme-switch-state.test.mjs",
|
|
98
|
+
"scripts/settings-window-structure.test.mjs",
|
|
99
|
+
"scripts/file-tab-selected-contrast.test.mjs"
|
|
100
|
+
],
|
|
101
|
+
"dependencies": {
|
|
102
|
+
"ws": "latest"
|
|
103
|
+
},
|
|
104
|
+
"devDependencies": {
|
|
105
|
+
"@types/node": "latest",
|
|
106
|
+
"@types/react": "latest",
|
|
107
|
+
"@types/react-dom": "latest",
|
|
108
|
+
"@types/ws": "latest",
|
|
109
|
+
"@vitejs/plugin-react": "latest",
|
|
110
|
+
"concurrently": "latest",
|
|
111
|
+
"react": "latest",
|
|
112
|
+
"react-dom": "latest",
|
|
113
|
+
"tsx": "latest",
|
|
114
|
+
"typescript": "latest",
|
|
115
|
+
"vite": "latest"
|
|
116
|
+
}
|
|
117
|
+
}
|