@raingor/pi-web-switch 0.4.1 → 0.4.3
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.ja.md +32 -8
- package/README.md +58 -10
- package/README.zh-CN.md +31 -7
- package/dist/index.html +18 -0
- package/package.json +5 -25
- package/pi-package/index.ts +228 -4
- package/public/trayIconTemplate.png +0 -0
- package/server/pi-reader.ts +251 -3
- package/src/App.tsx +0 -2
- package/src/components/dashboard/DashboardPage.tsx +43 -40
- package/src/components/layout/Sidebar.tsx +1 -3
- package/src/components/providers/ProvidersModelsPage.tsx +56 -4
- package/src/components/sessions/MemoryPage.tsx +2 -1
- package/src/components/settings/SettingsPage.tsx +72 -0
- package/src/lib/translations/en.ts +15 -58
- package/src/lib/translations/ja.ts +15 -58
- package/src/lib/translations/zh-CN.ts +15 -58
- package/src/lib/translations/zh-TW.ts +15 -58
- package/src/types/index.ts +1 -0
- package/vite.config.ts +89 -15
- package/server/agent-session-manager.ts +0 -827
- package/server/chat-api-plugin.ts +0 -488
- package/src/components/chat/ChatInput.tsx +0 -863
- package/src/components/chat/ChatPage.tsx +0 -617
- package/src/components/chat/ChatWindow.tsx +0 -338
- package/src/components/chat/MessageView.tsx +0 -595
- package/src/hooks/useAgentSession.ts +0 -1104
|
@@ -1,488 +0,0 @@
|
|
|
1
|
-
// Chat API Plugin for Vite — exposes agent session management endpoints.
|
|
2
|
-
// This is the server-side bridge between the React chat UI and the pi SDK.
|
|
3
|
-
|
|
4
|
-
import type { Plugin } from "vite";
|
|
5
|
-
import type { Connect } from "vite";
|
|
6
|
-
import { existsSync } from "fs";
|
|
7
|
-
import { resolve } from "path";
|
|
8
|
-
import { homedir } from "os";
|
|
9
|
-
import {
|
|
10
|
-
startRpcSession,
|
|
11
|
-
getRpcSession,
|
|
12
|
-
getRunningRpcSessionIds,
|
|
13
|
-
listAllSessions,
|
|
14
|
-
getSessionData,
|
|
15
|
-
resolveSessionPath,
|
|
16
|
-
cacheSessionPath,
|
|
17
|
-
invalidateSessionListCache,
|
|
18
|
-
loadModels,
|
|
19
|
-
type AgentEvent,
|
|
20
|
-
} from "./agent-session-manager";
|
|
21
|
-
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
22
|
-
import { unlinkSync, readdirSync, readFileSync, writeFileSync, statSync } from "fs";
|
|
23
|
-
import { dirname, join } from "path";
|
|
24
|
-
|
|
25
|
-
// ─── Helpers ─────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
function parseBody(req: Connect.IncomingMessage): Promise<any> {
|
|
28
|
-
return new Promise((resolve, reject) => {
|
|
29
|
-
let body = "";
|
|
30
|
-
req.on("data", (chunk: string) => (body += chunk));
|
|
31
|
-
req.on("end", () => {
|
|
32
|
-
try {
|
|
33
|
-
resolve(body ? JSON.parse(body) : {});
|
|
34
|
-
} catch (e) {
|
|
35
|
-
reject(e);
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
req.on("error", reject);
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function sendJSON(res: any, data: any, status = 200) {
|
|
43
|
-
res.statusCode = status;
|
|
44
|
-
res.setHeader("Content-Type", "application/json");
|
|
45
|
-
// Use a replacer that breaks circular references so a corrupted session
|
|
46
|
-
// file (or an SDK tree with bidirectional parent/child links) never
|
|
47
|
-
// crashes JSON.stringify with "Maximum call stack size exceeded".
|
|
48
|
-
const seen = new WeakSet();
|
|
49
|
-
const replacer = (_key: string, value: any) => {
|
|
50
|
-
if (typeof value === "object" && value !== null) {
|
|
51
|
-
if (seen.has(value)) return "[Circular]";
|
|
52
|
-
seen.add(value);
|
|
53
|
-
}
|
|
54
|
-
return value;
|
|
55
|
-
};
|
|
56
|
-
res.end(JSON.stringify(data, replacer));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const OMITTED_EVENT_TYPES = new Set(["turn_start", "turn_end", "tool_execution_update"]);
|
|
60
|
-
|
|
61
|
-
function toClientEvent(event: AgentEvent): AgentEvent | null {
|
|
62
|
-
if (OMITTED_EVENT_TYPES.has(event.type)) return null;
|
|
63
|
-
if (event.type === "message_update") {
|
|
64
|
-
const clientEvent = { ...event };
|
|
65
|
-
delete (clientEvent as any).assistantMessageEvent;
|
|
66
|
-
return clientEvent;
|
|
67
|
-
}
|
|
68
|
-
if (event.type === "agent_end") return { type: "agent_end" };
|
|
69
|
-
return event;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ─── Chat API Plugin ────────────────────────────────────
|
|
73
|
-
|
|
74
|
-
export function chatApiPlugin(): Plugin {
|
|
75
|
-
return {
|
|
76
|
-
name: "chat-api",
|
|
77
|
-
configureServer(server) {
|
|
78
|
-
server.middlewares.use(async (req, res, next) => {
|
|
79
|
-
const method = req.method!;
|
|
80
|
-
const url = req.url!;
|
|
81
|
-
if (!url.startsWith("/api/chat/")) return next();
|
|
82
|
-
|
|
83
|
-
const pathOnly = url.split("?")[0];
|
|
84
|
-
const searchParams = new URL(url, "http://localhost").searchParams;
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
// ─── POST /api/chat/agent/new ───────────────────
|
|
88
|
-
// Create a new agent session
|
|
89
|
-
if (method === "POST" && pathOnly === "/api/chat/agent/new") {
|
|
90
|
-
const body = await parseBody(req);
|
|
91
|
-
const { cwd, ...command } = body;
|
|
92
|
-
if (!cwd || typeof cwd !== "string") {
|
|
93
|
-
return sendJSON(res, { error: "cwd is required" }, 400);
|
|
94
|
-
}
|
|
95
|
-
if (!existsSync(cwd)) {
|
|
96
|
-
return sendJSON(res, { error: `Directory does not exist: ${cwd}` }, 400);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const { provider, modelId, toolNames, thinkingLevel, ...promptCommand } = command;
|
|
100
|
-
const tempKey = `__new__${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
101
|
-
const { session, realSessionId } = await startRpcSession(tempKey, "", cwd, {
|
|
102
|
-
...(toolNames ? { toolNames } : {}),
|
|
103
|
-
...(provider && modelId ? { initialModel: { provider, modelId } } : {}),
|
|
104
|
-
...(thinkingLevel ? { thinkingLevel } : {}),
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
allowFileRoot(cwd);
|
|
108
|
-
invalidateSessionListCache();
|
|
109
|
-
|
|
110
|
-
const state = await session.send({ type: "get_state" }) as any;
|
|
111
|
-
|
|
112
|
-
if (promptCommand.type === "ensure_session") {
|
|
113
|
-
return sendJSON(res, {
|
|
114
|
-
success: true,
|
|
115
|
-
sessionId: realSessionId,
|
|
116
|
-
data: null,
|
|
117
|
-
model: state.model ? { provider: state.model.provider, modelId: state.model.id } : null,
|
|
118
|
-
thinkingLevel: state.thinkingLevel,
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const result = await session.send(promptCommand);
|
|
123
|
-
return sendJSON(res, {
|
|
124
|
-
success: true,
|
|
125
|
-
sessionId: realSessionId,
|
|
126
|
-
data: result,
|
|
127
|
-
model: state.model ? { provider: state.model.provider, modelId: state.model.id } : null,
|
|
128
|
-
thinkingLevel: state.thinkingLevel,
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// ─── POST /api/chat/agent/:id ───────────────────
|
|
133
|
-
// Send a command to an existing session
|
|
134
|
-
const agentCommandMatch = pathOnly.match(/^\/api\/chat\/agent\/([^/]+)$/);
|
|
135
|
-
if (agentCommandMatch && method === "POST") {
|
|
136
|
-
const id = decodeURIComponent(agentCommandMatch[1]);
|
|
137
|
-
const body = await parseBody(req);
|
|
138
|
-
|
|
139
|
-
const existing = getRpcSession(id);
|
|
140
|
-
if (existing?.isAlive()) {
|
|
141
|
-
const result = await existing.send(body);
|
|
142
|
-
return sendJSON(res, { success: true, data: result });
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const filePath = await resolveSessionPath(id);
|
|
146
|
-
if (!filePath) {
|
|
147
|
-
return sendJSON(res, { error: "Session not found" }, 404);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const { session } = await startRpcSession(id, filePath, undefined);
|
|
151
|
-
const result = await session.send(body);
|
|
152
|
-
return sendJSON(res, { success: true, data: result });
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// ─── GET /api/chat/agent/:id ────────────────────
|
|
156
|
-
// Get current agent state
|
|
157
|
-
if (agentCommandMatch && method === "GET") {
|
|
158
|
-
const id = decodeURIComponent(agentCommandMatch[1]);
|
|
159
|
-
const session = getRpcSession(id);
|
|
160
|
-
if (!session || !session.isAlive()) {
|
|
161
|
-
return sendJSON(res, { running: false });
|
|
162
|
-
}
|
|
163
|
-
const state = await session.send({ type: "get_state" });
|
|
164
|
-
return sendJSON(res, { running: true, state });
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// ─── GET /api/chat/agent/:id/events ─────────────
|
|
168
|
-
// SSE stream of agent events
|
|
169
|
-
const eventsMatch = pathOnly.match(/^\/api\/chat\/agent\/([^/]+)\/events$/);
|
|
170
|
-
if (eventsMatch && method === "GET") {
|
|
171
|
-
const id = decodeURIComponent(eventsMatch[1]);
|
|
172
|
-
|
|
173
|
-
let session = getRpcSession(id);
|
|
174
|
-
if (!session || !session.isAlive()) {
|
|
175
|
-
const filePath = await resolveSessionPath(id);
|
|
176
|
-
if (!filePath) {
|
|
177
|
-
res.statusCode = 404;
|
|
178
|
-
return res.end("Session not found");
|
|
179
|
-
}
|
|
180
|
-
try {
|
|
181
|
-
({ session } = await startRpcSession(id, filePath, undefined));
|
|
182
|
-
} catch (error) {
|
|
183
|
-
res.statusCode = 500;
|
|
184
|
-
return res.end(`Failed to start agent: ${error}`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
res.setHeader("Content-Type", "text/event-stream");
|
|
189
|
-
res.setHeader("Cache-Control", "no-cache");
|
|
190
|
-
res.setHeader("Connection", "keep-alive");
|
|
191
|
-
res.writeHead(200);
|
|
192
|
-
|
|
193
|
-
const encoder = new TextEncoder();
|
|
194
|
-
const encode = (data: unknown) => {
|
|
195
|
-
const text = `data: ${JSON.stringify(data)}\n\n`;
|
|
196
|
-
res.write(text);
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
// Send initial connected event
|
|
200
|
-
encode({ type: "connected", sessionId: id });
|
|
201
|
-
|
|
202
|
-
const unsubscribe = session.onEvent((event) => {
|
|
203
|
-
const clientEvent = toClientEvent(event);
|
|
204
|
-
if (clientEvent) encode(clientEvent);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
// Heartbeat every 30s
|
|
208
|
-
const heartbeat = setInterval(() => {
|
|
209
|
-
try {
|
|
210
|
-
res.write(":\n\n");
|
|
211
|
-
} catch {
|
|
212
|
-
// controller already closed
|
|
213
|
-
}
|
|
214
|
-
}, 30_000);
|
|
215
|
-
|
|
216
|
-
// Cleanup on disconnect
|
|
217
|
-
const cleanup = () => {
|
|
218
|
-
clearInterval(heartbeat);
|
|
219
|
-
unsubscribe();
|
|
220
|
-
try { res.end(); } catch { /* already closed */ }
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
req.on("close", cleanup);
|
|
224
|
-
req.on("error", cleanup);
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// ─── GET /api/chat/sessions ─────────────────────
|
|
229
|
-
// List all sessions
|
|
230
|
-
if (method === "GET" && pathOnly === "/api/chat/sessions") {
|
|
231
|
-
const sessions = await listAllSessions();
|
|
232
|
-
return sendJSON(res, { sessions, runningSessionIds: getRunningRpcSessionIds() });
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// ─── GET /api/chat/sessions/:id ─────────────────
|
|
236
|
-
// Get session data (messages, tree, context)
|
|
237
|
-
const sessionMatch = pathOnly.match(/^\/api\/chat\/sessions\/([^/]+)$/);
|
|
238
|
-
if (sessionMatch && method === "GET") {
|
|
239
|
-
const id = decodeURIComponent(sessionMatch[1]);
|
|
240
|
-
const data = await getSessionData(id);
|
|
241
|
-
if (!data) {
|
|
242
|
-
return sendJSON(res, { error: "Session not found" }, 404);
|
|
243
|
-
}
|
|
244
|
-
return sendJSON(res, data);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// ─── PATCH /api/chat/sessions/:id ───────────────
|
|
248
|
-
// Rename session
|
|
249
|
-
if (sessionMatch && method === "PATCH") {
|
|
250
|
-
const id = decodeURIComponent(sessionMatch[1]);
|
|
251
|
-
const { name } = await parseBody(req);
|
|
252
|
-
if (typeof name !== "string") {
|
|
253
|
-
return sendJSON(res, { error: "name is required" }, 400);
|
|
254
|
-
}
|
|
255
|
-
const filePath = await resolveSessionPath(id);
|
|
256
|
-
if (!filePath) {
|
|
257
|
-
return sendJSON(res, { error: "Session not found" }, 404);
|
|
258
|
-
}
|
|
259
|
-
const sm = SessionManager.open(filePath);
|
|
260
|
-
sm.appendSessionInfo(name.trim());
|
|
261
|
-
invalidateSessionListCache();
|
|
262
|
-
return sendJSON(res, { ok: true });
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// ─── DELETE /api/chat/sessions/:id ──────────────
|
|
266
|
-
// Delete session
|
|
267
|
-
if (sessionMatch && method === "DELETE") {
|
|
268
|
-
const id = decodeURIComponent(sessionMatch[1]);
|
|
269
|
-
const filePath = await resolveSessionPath(id);
|
|
270
|
-
if (!filePath) {
|
|
271
|
-
return sendJSON(res, { error: "Session not found" }, 404);
|
|
272
|
-
}
|
|
273
|
-
// Re-attach children to parent
|
|
274
|
-
const dir = dirname(filePath);
|
|
275
|
-
try {
|
|
276
|
-
const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl") && join(dir, f) !== filePath);
|
|
277
|
-
for (const file of files) {
|
|
278
|
-
const childPath = join(dir, file);
|
|
279
|
-
try {
|
|
280
|
-
const content = readFileSync(childPath, "utf8");
|
|
281
|
-
const lines = content.split("\n");
|
|
282
|
-
const header = JSON.parse(lines[0]);
|
|
283
|
-
if (header.type === "session" && header.parentSession === filePath) {
|
|
284
|
-
header.parentSession = undefined;
|
|
285
|
-
lines[0] = JSON.stringify(header);
|
|
286
|
-
writeFileSync(childPath, lines.join("\n"));
|
|
287
|
-
}
|
|
288
|
-
} catch { /* skip malformed */ }
|
|
289
|
-
}
|
|
290
|
-
} catch { /* skip if dir unreadable */ }
|
|
291
|
-
|
|
292
|
-
await getRpcSession(id)?.shutdown();
|
|
293
|
-
unlinkSync(filePath);
|
|
294
|
-
invalidateSessionListCache();
|
|
295
|
-
return sendJSON(res, { ok: true });
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// ─── GET /api/chat/sessions/:id/context ─────────
|
|
299
|
-
// Get session context (branch navigation)
|
|
300
|
-
const contextMatch = pathOnly.match(/^\/api\/chat\/sessions\/([^/]+)\/context$/);
|
|
301
|
-
if (contextMatch && method === "GET") {
|
|
302
|
-
const id = decodeURIComponent(contextMatch[1]);
|
|
303
|
-
const leafId = searchParams.get("leafId");
|
|
304
|
-
const filePath = await resolveSessionPath(id);
|
|
305
|
-
if (!filePath) {
|
|
306
|
-
return sendJSON(res, { error: "Session not found" }, 404);
|
|
307
|
-
}
|
|
308
|
-
const sm = SessionManager.open(filePath);
|
|
309
|
-
const entries = sm.getEntries();
|
|
310
|
-
const { buildContextEntries } = await import("@earendil-works/pi-coding-agent");
|
|
311
|
-
const byId = new Map<string, any>();
|
|
312
|
-
for (const e of entries) byId.set(e.id, e);
|
|
313
|
-
const contextEntries = buildContextEntries(entries, leafId, byId);
|
|
314
|
-
const messages = contextEntries.filter((e: any) => e.type === "message").map((e: any) => e.message);
|
|
315
|
-
const entryIds = contextEntries.filter((e: any) => e.type === "message").map((e: any) => e.id);
|
|
316
|
-
return sendJSON(res, { context: { messages, entryIds } });
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// ─── GET /api/chat/sessions/:id/state ───────────
|
|
320
|
-
// Get live agent state for a session
|
|
321
|
-
const stateMatch = pathOnly.match(/^\/api\/chat\/sessions\/([^/]+)\/state$/);
|
|
322
|
-
if (stateMatch && method === "GET") {
|
|
323
|
-
const id = decodeURIComponent(stateMatch[1]);
|
|
324
|
-
const session = getRpcSession(id);
|
|
325
|
-
if (!session || !session.isAlive()) {
|
|
326
|
-
return sendJSON(res, { running: false });
|
|
327
|
-
}
|
|
328
|
-
const state = await session.send({ type: "get_state" });
|
|
329
|
-
return sendJSON(res, { running: true, state });
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// ─── POST /api/chat/sessions/:id/auto-name ──────
|
|
333
|
-
// Auto-generate session name (simplified — just uses first message)
|
|
334
|
-
const autoNameMatch = pathOnly.match(/^\/api\/chat\/sessions\/([^/]+)\/auto-name$/);
|
|
335
|
-
if (autoNameMatch && method === "POST") {
|
|
336
|
-
const id = decodeURIComponent(autoNameMatch[1]);
|
|
337
|
-
const filePath = await resolveSessionPath(id);
|
|
338
|
-
if (!filePath) {
|
|
339
|
-
return sendJSON(res, { error: "Session not found" }, 404);
|
|
340
|
-
}
|
|
341
|
-
const sm = SessionManager.open(filePath);
|
|
342
|
-
const entries = sm.getEntries();
|
|
343
|
-
const firstUserMsg = entries.find((e: any) => e.type === "message" && e.message?.role === "user");
|
|
344
|
-
let title = "New Session";
|
|
345
|
-
if (firstUserMsg) {
|
|
346
|
-
const content = firstUserMsg.message.content;
|
|
347
|
-
title = typeof content === "string"
|
|
348
|
-
? content.slice(0, 60)
|
|
349
|
-
: Array.isArray(content)
|
|
350
|
-
? (content.find((b: any) => b.type === "text")?.text ?? "New Session").slice(0, 60)
|
|
351
|
-
: "New Session";
|
|
352
|
-
}
|
|
353
|
-
sm.appendSessionInfo(title);
|
|
354
|
-
invalidateSessionListCache();
|
|
355
|
-
return sendJSON(res, { title });
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// ─── GET /api/chat/models ───────────────────────
|
|
359
|
-
// Get available models for a cwd
|
|
360
|
-
if (method === "GET" && pathOnly === "/api/chat/models") {
|
|
361
|
-
const cwd = searchParams.get("cwd") || process.cwd();
|
|
362
|
-
const resolved = resolve(cwd);
|
|
363
|
-
if (!existsSync(resolved)) {
|
|
364
|
-
return sendJSON(res, { error: `Directory does not exist: ${resolved}` }, 400);
|
|
365
|
-
}
|
|
366
|
-
try {
|
|
367
|
-
const data = await loadModels(resolved);
|
|
368
|
-
return sendJSON(res, data);
|
|
369
|
-
} catch (error) {
|
|
370
|
-
console.error("[chat-api] loadModels error:", error);
|
|
371
|
-
return sendJSON(res, {
|
|
372
|
-
models: {},
|
|
373
|
-
modelList: [],
|
|
374
|
-
defaultModel: null,
|
|
375
|
-
thinkingLevels: {},
|
|
376
|
-
modelError: String(error),
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// ─── GET /api/chat/default-cwd ──────────────────
|
|
382
|
-
// Get the default working directory
|
|
383
|
-
if (method === "GET" && pathOnly === "/api/chat/default-cwd") {
|
|
384
|
-
const home = homedir();
|
|
385
|
-
return sendJSON(res, { cwd: home });
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// ─── GET /api/chat/home ─────────────────────────
|
|
389
|
-
// Get user home directory
|
|
390
|
-
if (method === "GET" && pathOnly === "/api/chat/home") {
|
|
391
|
-
return sendJSON(res, { home: homedir() });
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// ─── POST /api/chat/cwd/validate ────────────────
|
|
395
|
-
// Validate a working directory
|
|
396
|
-
if (method === "POST" && pathOnly === "/api/chat/cwd/validate") {
|
|
397
|
-
const { cwd } = await parseBody(req);
|
|
398
|
-
if (!cwd || typeof cwd !== "string") {
|
|
399
|
-
return sendJSON(res, { error: "cwd is required" }, 400);
|
|
400
|
-
}
|
|
401
|
-
const resolved = resolve(cwd);
|
|
402
|
-
if (!existsSync(resolved)) {
|
|
403
|
-
return sendJSON(res, { error: `Directory does not exist: ${resolved}` }, 400);
|
|
404
|
-
}
|
|
405
|
-
try {
|
|
406
|
-
const stat = statSync(resolved);
|
|
407
|
-
if (!stat.isDirectory()) {
|
|
408
|
-
return sendJSON(res, { error: `Not a directory: ${resolved}` }, 400);
|
|
409
|
-
}
|
|
410
|
-
return sendJSON(res, { cwd: resolved });
|
|
411
|
-
} catch {
|
|
412
|
-
return sendJSON(res, { error: `Cannot access: ${resolved}` }, 400);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// ─── GET /api/chat/cwd/browse ───────────────────
|
|
417
|
-
// Browse directory listing
|
|
418
|
-
if (method === "GET" && pathOnly === "/api/chat/cwd/browse") {
|
|
419
|
-
const dirPath = searchParams.get("path") || homedir();
|
|
420
|
-
const resolved = resolve(dirPath);
|
|
421
|
-
if (!existsSync(resolved)) {
|
|
422
|
-
return sendJSON(res, { error: "Directory not found" }, 404);
|
|
423
|
-
}
|
|
424
|
-
try {
|
|
425
|
-
const entries = readdirSync(resolved, { withFileTypes: true });
|
|
426
|
-
const items = entries
|
|
427
|
-
.filter((e) => !e.name.startsWith("."))
|
|
428
|
-
.map((e) => ({
|
|
429
|
-
name: e.name,
|
|
430
|
-
isDirectory: e.isDirectory(),
|
|
431
|
-
path: join(resolved, e.name),
|
|
432
|
-
}))
|
|
433
|
-
.sort((a, b) => {
|
|
434
|
-
if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1;
|
|
435
|
-
return a.name.localeCompare(b.name);
|
|
436
|
-
});
|
|
437
|
-
return sendJSON(res, { path: resolved, items });
|
|
438
|
-
} catch {
|
|
439
|
-
return sendJSON(res, { error: "Cannot read directory" }, 400);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// ─── GET /api/chat/files/* ──────────────────────
|
|
444
|
-
// Read file content
|
|
445
|
-
const filesMatch = pathOnly.match(/^\/api\/chat\/files\/(.+)$/);
|
|
446
|
-
if (filesMatch && method === "GET") {
|
|
447
|
-
const filePath = decodeURIComponent(filesMatch[1]);
|
|
448
|
-
if (!existsSync(filePath)) {
|
|
449
|
-
return sendJSON(res, { error: "File not found" }, 404);
|
|
450
|
-
}
|
|
451
|
-
try {
|
|
452
|
-
const content = readFileSync(filePath, "utf8");
|
|
453
|
-
const stat = statSync(filePath);
|
|
454
|
-
return sendJSON(res, {
|
|
455
|
-
path: filePath,
|
|
456
|
-
content,
|
|
457
|
-
size: stat.size,
|
|
458
|
-
modified: stat.mtime.toISOString(),
|
|
459
|
-
});
|
|
460
|
-
} catch {
|
|
461
|
-
return sendJSON(res, { error: "Cannot read file" }, 400);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// ─── GET /api/chat/running ──────────────────────
|
|
466
|
-
// Get list of running session IDs
|
|
467
|
-
if (method === "GET" && pathOnly === "/api/chat/running") {
|
|
468
|
-
return sendJSON(res, { ids: getRunningRpcSessionIds() });
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// Not found
|
|
472
|
-
return sendJSON(res, { error: "Not found" }, 404);
|
|
473
|
-
} catch (error) {
|
|
474
|
-
console.error("[chat-api] Error:", error);
|
|
475
|
-
return sendJSON(res, { error: String(error) }, 500);
|
|
476
|
-
}
|
|
477
|
-
});
|
|
478
|
-
},
|
|
479
|
-
};
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// ─── File Access Control ────────────────────────────────
|
|
483
|
-
|
|
484
|
-
const allowedRoots = new Set<string>();
|
|
485
|
-
|
|
486
|
-
function allowFileRoot(root: string) {
|
|
487
|
-
allowedRoots.add(resolve(root));
|
|
488
|
-
}
|