@prestyj/cli 4.12.1 → 4.14.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/dist/app-sidecar.js +258 -45
- package/dist/app-sidecar.js.map +1 -1
- package/dist/cli/auth.d.ts.map +1 -1
- package/dist/cli/auth.js +5 -2
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli/shared.d.ts.map +1 -1
- package/dist/cli/shared.js +2 -0
- package/dist/cli/shared.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/core/auth-providers.d.ts.map +1 -1
- package/dist/core/auth-providers.js +7 -0
- package/dist/core/auth-providers.js.map +1 -1
- package/dist/core/encode-cwd.d.ts +25 -0
- package/dist/core/encode-cwd.d.ts.map +1 -0
- package/dist/core/encode-cwd.js +40 -0
- package/dist/core/encode-cwd.js.map +1 -0
- package/dist/core/encode-cwd.test.d.ts +2 -0
- package/dist/core/encode-cwd.test.d.ts.map +1 -0
- package/dist/core/encode-cwd.test.js +61 -0
- package/dist/core/encode-cwd.test.js.map +1 -0
- package/dist/core/mcp/client.d.ts +11 -0
- package/dist/core/mcp/client.d.ts.map +1 -1
- package/dist/core/mcp/client.js +87 -35
- package/dist/core/mcp/client.js.map +1 -1
- package/dist/core/mcp/defaults.d.ts +1 -1
- package/dist/core/mcp/defaults.d.ts.map +1 -1
- package/dist/core/mcp/defaults.js +9 -40
- package/dist/core/mcp/defaults.js.map +1 -1
- package/dist/core/mcp/loopback.d.ts +27 -0
- package/dist/core/mcp/loopback.d.ts.map +1 -0
- package/dist/core/mcp/loopback.js +66 -0
- package/dist/core/mcp/loopback.js.map +1 -0
- package/dist/core/mcp/loopback.test.d.ts +2 -0
- package/dist/core/mcp/loopback.test.d.ts.map +1 -0
- package/dist/core/mcp/loopback.test.js +87 -0
- package/dist/core/mcp/loopback.test.js.map +1 -0
- package/dist/core/mcp/parse-add-command.d.ts.map +1 -1
- package/dist/core/mcp/parse-add-command.js +1 -0
- package/dist/core/mcp/parse-add-command.js.map +1 -1
- package/dist/core/mcp/parse-add-command.test.js +8 -2
- package/dist/core/mcp/parse-add-command.test.js.map +1 -1
- package/dist/core/mcp/resolve-stdio.d.ts +27 -0
- package/dist/core/mcp/resolve-stdio.d.ts.map +1 -0
- package/dist/core/mcp/resolve-stdio.js +149 -0
- package/dist/core/mcp/resolve-stdio.js.map +1 -0
- package/dist/core/mcp/resolve-stdio.test.d.ts +2 -0
- package/dist/core/mcp/resolve-stdio.test.d.ts.map +1 -0
- package/dist/core/mcp/resolve-stdio.test.js +63 -0
- package/dist/core/mcp/resolve-stdio.test.js.map +1 -0
- package/dist/core/mcp/store.d.ts +4 -4
- package/dist/core/mcp/store.d.ts.map +1 -1
- package/dist/core/mcp/store.js +7 -1
- package/dist/core/mcp/store.js.map +1 -1
- package/dist/core/mcp/store.test.js +11 -2
- package/dist/core/mcp/store.test.js.map +1 -1
- package/dist/core/mcp/types.d.ts +5 -1
- package/dist/core/mcp/types.d.ts.map +1 -1
- package/dist/core/project-discovery.d.ts.map +1 -1
- package/dist/core/project-discovery.js +2 -9
- package/dist/core/project-discovery.js.map +1 -1
- package/dist/core/radio.d.ts +7 -2
- package/dist/core/radio.d.ts.map +1 -1
- package/dist/core/radio.js.map +1 -1
- package/dist/core/resolve-start.test.js +1 -0
- package/dist/core/resolve-start.test.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +1 -3
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +1 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +1 -3
- package/dist/session.js.map +1 -1
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +9 -4
- package/dist/system-prompt.js.map +1 -1
- package/dist/system-prompt.test.js +6 -3
- package/dist/system-prompt.test.js.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +10 -2
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/code-skeleton.d.ts +29 -0
- package/dist/tools/code-skeleton.d.ts.map +1 -0
- package/dist/tools/code-skeleton.js +115 -0
- package/dist/tools/code-skeleton.js.map +1 -0
- package/dist/tools/code-skeleton.test.d.ts +2 -0
- package/dist/tools/code-skeleton.test.d.ts.map +1 -0
- package/dist/tools/code-skeleton.test.js +115 -0
- package/dist/tools/code-skeleton.test.js.map +1 -0
- package/dist/tools/compress-integration.test.d.ts +2 -0
- package/dist/tools/compress-integration.test.d.ts.map +1 -0
- package/dist/tools/compress-integration.test.js +59 -0
- package/dist/tools/compress-integration.test.js.map +1 -0
- package/dist/tools/compress.d.ts +57 -0
- package/dist/tools/compress.d.ts.map +1 -0
- package/dist/tools/compress.js +181 -0
- package/dist/tools/compress.js.map +1 -0
- package/dist/tools/compress.test.d.ts +2 -0
- package/dist/tools/compress.test.d.ts.map +1 -0
- package/dist/tools/compress.test.js +60 -0
- package/dist/tools/compress.test.js.map +1 -0
- package/dist/tools/edit-diff.d.ts.map +1 -1
- package/dist/tools/edit-diff.js +25 -8
- package/dist/tools/edit-diff.js.map +1 -1
- package/dist/tools/ls.d.ts.map +1 -1
- package/dist/tools/ls.js +7 -4
- package/dist/tools/ls.js.map +1 -1
- package/dist/tools/task-output.d.ts.map +1 -1
- package/dist/tools/task-output.js +13 -3
- package/dist/tools/task-output.js.map +1 -1
- package/dist/tools/tasks.d.ts +6 -0
- package/dist/tools/tasks.d.ts.map +1 -1
- package/dist/tools/tasks.js +41 -4
- package/dist/tools/tasks.js.map +1 -1
- package/dist/tools/tasks.test.d.ts +2 -0
- package/dist/tools/tasks.test.d.ts.map +1 -0
- package/dist/tools/tasks.test.js +62 -0
- package/dist/tools/tasks.test.js.map +1 -0
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/components/ChatScreen.d.ts +5 -5
- package/dist/ui/components/ChatScreen.d.ts.map +1 -1
- package/dist/ui/components/InputArea.d.ts +5 -5
- package/dist/ui/components/InputArea.d.ts.map +1 -1
- package/dist/ui/components/ModelSelector.d.ts.map +1 -1
- package/dist/ui/components/ModelSelector.js +1 -0
- package/dist/ui/components/ModelSelector.js.map +1 -1
- package/dist/ui/components/TaskPickerMenu.d.ts +2 -2
- package/dist/ui/components/TaskPickerMenu.d.ts.map +1 -1
- package/dist/ui/components/TaskPickerMenu.js.map +1 -1
- package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
- package/dist/ui/hooks/useAgentLoop.js +13 -4
- package/dist/ui/hooks/useAgentLoop.js.map +1 -1
- package/dist/ui/login.d.ts.map +1 -1
- package/dist/ui/login.js +1 -0
- package/dist/ui/login.js.map +1 -1
- package/package.json +6 -5
- package/dist/core/tasks-store.d.ts +0 -24
- package/dist/core/tasks-store.d.ts.map +0 -1
- package/dist/core/tasks-store.js +0 -81
- package/dist/core/tasks-store.js.map +0 -1
package/dist/app-sidecar.js
CHANGED
|
@@ -15,6 +15,7 @@ import http from "node:http";
|
|
|
15
15
|
import fs from "node:fs/promises";
|
|
16
16
|
import os from "node:os";
|
|
17
17
|
import path from "node:path";
|
|
18
|
+
import { randomUUID } from "node:crypto";
|
|
18
19
|
import { parseArgs } from "node:util";
|
|
19
20
|
import { runJsonMode } from "./modes/json-mode.js";
|
|
20
21
|
import { AgentSession } from "./core/agent-session.js";
|
|
@@ -34,7 +35,7 @@ import { getNextThinkingLevel, getSupportedThinkingLevels, isThinkingLevelSuppor
|
|
|
34
35
|
import { PROMPT_COMMANDS } from "./core/prompt-commands.js";
|
|
35
36
|
import { loadCustomCommands } from "./core/custom-commands.js";
|
|
36
37
|
import { discoverProjects, listRecentSessions } from "./core/project-discovery.js";
|
|
37
|
-
import { loadTasksSync, saveTasksSync, getNextPendingTask, markTaskInProgress, } from "./core/
|
|
38
|
+
import { loadTasksSync, saveTasksSync, getNextPendingTask, markTaskInProgress, } from "./core/task-store.js";
|
|
38
39
|
import { initLogger, log } from "./core/logger.js";
|
|
39
40
|
import { RADIO_STATIONS, getCurrentStation, playRadio, stopRadio } from "./core/radio.js";
|
|
40
41
|
import { enrichProcessPath } from "./core/shell-path.js";
|
|
@@ -52,6 +53,7 @@ const ALL_PROVIDERS = [
|
|
|
52
53
|
"minimax",
|
|
53
54
|
"deepseek",
|
|
54
55
|
"openrouter",
|
|
56
|
+
"sakana",
|
|
55
57
|
];
|
|
56
58
|
function appSettingsFile() {
|
|
57
59
|
return path.join(os.homedir(), ".ezcoder", "ezcoder-app.json");
|
|
@@ -362,11 +364,28 @@ async function runJsonModeIfRequested() {
|
|
|
362
364
|
});
|
|
363
365
|
return true;
|
|
364
366
|
}
|
|
367
|
+
// ── Daemon-level HTTP helpers (shared by the session-management routes) ─────
|
|
368
|
+
// The per-session route table has its own local copies; these serve the
|
|
369
|
+
// daemon's own POST /session / DELETE /session routes.
|
|
370
|
+
function daemonReadBody(req) {
|
|
371
|
+
return new Promise((resolve, reject) => {
|
|
372
|
+
const chunks = [];
|
|
373
|
+
req.on("data", (c) => chunks.push(c));
|
|
374
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
375
|
+
req.on("error", reject);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
function daemonJson(res, status, body) {
|
|
379
|
+
res.writeHead(status, {
|
|
380
|
+
"content-type": "application/json",
|
|
381
|
+
"access-control-allow-origin": "*",
|
|
382
|
+
});
|
|
383
|
+
res.end(JSON.stringify(body));
|
|
384
|
+
}
|
|
365
385
|
async function main() {
|
|
366
386
|
// Sub-agent JSON-mode dispatch must win before any sidecar/server setup.
|
|
367
387
|
if (await runJsonModeIfRequested())
|
|
368
388
|
return;
|
|
369
|
-
const cwd = process.env.GG_APP_CWD ?? process.cwd();
|
|
370
389
|
// Default to an ephemeral port (0) so concurrent/orphaned instances never
|
|
371
390
|
// collide on a fixed port. The actual port is reported via the
|
|
372
391
|
// GG_APP_LISTENING handshake and consumed by the shell.
|
|
@@ -404,6 +423,119 @@ async function main() {
|
|
|
404
423
|
await enrichProcessPath();
|
|
405
424
|
const auth = new AuthStorage(paths.authFile);
|
|
406
425
|
await auth.load();
|
|
426
|
+
// Every window's session lives here as an in-process object, keyed by the id
|
|
427
|
+
// the daemon hands back from POST /session. The Rust shell routes each proxy
|
|
428
|
+
// request to its window's session via the `x-gg-session` header (and the
|
|
429
|
+
// `?session=` query for the SSE /events stream).
|
|
430
|
+
const sessions = new Map();
|
|
431
|
+
/** Resolve the target session id: the `x-gg-session` header, else a
|
|
432
|
+
* `?session=` query param (used by the SSE /events connection). */
|
|
433
|
+
function sessionIdFromReq(req, url) {
|
|
434
|
+
const header = req.headers["x-gg-session"];
|
|
435
|
+
if (typeof header === "string" && header.length > 0)
|
|
436
|
+
return header;
|
|
437
|
+
try {
|
|
438
|
+
return new URL(url, `http://${host}`).searchParams.get("session");
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const server = http.createServer((req, res) => {
|
|
445
|
+
const url = req.url ?? "/";
|
|
446
|
+
const method = req.method ?? "GET";
|
|
447
|
+
// CORS preflight — the webview origin differs from 127.0.0.1.
|
|
448
|
+
if (method === "OPTIONS") {
|
|
449
|
+
res.writeHead(204, {
|
|
450
|
+
"access-control-allow-origin": "*",
|
|
451
|
+
"access-control-allow-methods": "GET, POST, DELETE, OPTIONS",
|
|
452
|
+
"access-control-allow-headers": "content-type, x-gg-session",
|
|
453
|
+
});
|
|
454
|
+
res.end();
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
// ── Daemon-level routes (session lifecycle) ──────────────────────────
|
|
458
|
+
// Create a session for a window: { cwd, sessionPath? } → { sessionId }.
|
|
459
|
+
if (method === "POST" && url === "/session") {
|
|
460
|
+
void daemonReadBody(req).then(async (raw) => {
|
|
461
|
+
let body = {};
|
|
462
|
+
try {
|
|
463
|
+
body = raw ? JSON.parse(raw) : {};
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
/* empty/invalid body → defaults below */
|
|
467
|
+
}
|
|
468
|
+
const sessionCwd = typeof body.cwd === "string" && body.cwd
|
|
469
|
+
? body.cwd
|
|
470
|
+
: (process.env.GG_APP_CWD ?? process.cwd());
|
|
471
|
+
const sessionPath = typeof body.sessionPath === "string" && body.sessionPath ? body.sessionPath : undefined;
|
|
472
|
+
const id = randomUUID();
|
|
473
|
+
try {
|
|
474
|
+
const ctx = await createSession({ auth, paths }, { id, cwd: sessionCwd, sessionPath });
|
|
475
|
+
sessions.set(id, ctx);
|
|
476
|
+
log("INFO", "app-sidecar", "session created", { id, cwd: sessionCwd });
|
|
477
|
+
daemonJson(res, 200, { sessionId: id });
|
|
478
|
+
}
|
|
479
|
+
catch (err) {
|
|
480
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
481
|
+
log("ERROR", "app-sidecar", "session create failed", { message });
|
|
482
|
+
daemonJson(res, 500, { error: message });
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
// Dispose a session: DELETE /session/:id.
|
|
488
|
+
if (method === "DELETE" && url.startsWith("/session/")) {
|
|
489
|
+
const id = decodeURIComponent(url.slice("/session/".length));
|
|
490
|
+
const ctx = sessions.get(id);
|
|
491
|
+
if (ctx) {
|
|
492
|
+
sessions.delete(id);
|
|
493
|
+
void ctx.dispose().catch(() => { });
|
|
494
|
+
log("INFO", "app-sidecar", "session disposed", { id });
|
|
495
|
+
}
|
|
496
|
+
daemonJson(res, 200, { ok: true });
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
// ── Per-session delegation ───────────────────────────────────────────
|
|
500
|
+
const id = sessionIdFromReq(req, url);
|
|
501
|
+
const ctx = id ? sessions.get(id) : undefined;
|
|
502
|
+
if (!ctx) {
|
|
503
|
+
daemonJson(res, 404, { error: "unknown session" });
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
ctx.handle(req, res, url, method);
|
|
507
|
+
});
|
|
508
|
+
server.listen(port, host, () => {
|
|
509
|
+
const addr = server.address();
|
|
510
|
+
// The Rust shell reads this line to learn the daemon port.
|
|
511
|
+
process.stdout.write(`GG_APP_LISTENING ${addr.port}\n`);
|
|
512
|
+
log("INFO", "app-sidecar", "daemon listening", { port: String(addr.port), host });
|
|
513
|
+
});
|
|
514
|
+
const shutdown = async () => {
|
|
515
|
+
// Radio playback is app-wide (one stream across all windows), so it stops
|
|
516
|
+
// at the daemon level, not per session.
|
|
517
|
+
stopRadio();
|
|
518
|
+
await Promise.all([...sessions.values()].map((c) => c.dispose().catch(() => { })));
|
|
519
|
+
server.close();
|
|
520
|
+
process.exit(0);
|
|
521
|
+
};
|
|
522
|
+
process.on("SIGINT", () => void shutdown());
|
|
523
|
+
process.on("SIGTERM", () => void shutdown());
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Build one in-process agent session: its AgentSession, SSE client set, event
|
|
527
|
+
* bridge, task runner, auth/login bridge, and the full HTTP route table exposed
|
|
528
|
+
* as a `handle()` method. Many of these live inside one daemon process, fully
|
|
529
|
+
* isolated (separate AgentSession, cwd, history, model) — only the HTTP server,
|
|
530
|
+
* logger, PATH, shared auth file, and radio live at the daemon level.
|
|
531
|
+
*/
|
|
532
|
+
async function createSession(deps, opts) {
|
|
533
|
+
const { auth } = deps;
|
|
534
|
+
const paths = deps.paths;
|
|
535
|
+
const cwd = opts.cwd;
|
|
536
|
+
// Base host for parsing request-URL query params (value is irrelevant to
|
|
537
|
+
// parsing); the daemon owns the real listen host.
|
|
538
|
+
const host = "127.0.0.1";
|
|
407
539
|
const saved = loadSavedSettings(paths.settingsFile);
|
|
408
540
|
// Per-project model/thinking prefs win over the shared global settings.json:
|
|
409
541
|
// each window (one project cwd) restores its own selection instead of every
|
|
@@ -435,9 +567,9 @@ async function main() {
|
|
|
435
567
|
for (const c of clients)
|
|
436
568
|
c.res.write(frame);
|
|
437
569
|
}
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
const resumeSessionPath =
|
|
570
|
+
// The session file path to resume (passed by the daemon's POST /session);
|
|
571
|
+
// empty/unset starts a fresh session.
|
|
572
|
+
const resumeSessionPath = opts.sessionPath;
|
|
441
573
|
let abort = new AbortController();
|
|
442
574
|
const session = new AgentSession({
|
|
443
575
|
provider,
|
|
@@ -495,26 +627,33 @@ async function main() {
|
|
|
495
627
|
tasks: session.listBackgroundProcesses(),
|
|
496
628
|
};
|
|
497
629
|
}
|
|
630
|
+
// tool_call_end carries no tool name (only the id), so remember each call's
|
|
631
|
+
// name from tool_call_start to log a useful line on completion. Mirrors the
|
|
632
|
+
// CLI's logging so the app sidecar's ~/.ezcoder/ezcoder-app-sidecar.log records tool
|
|
633
|
+
// failures (e.g. repeated invalid-argument errors) instead of leaving the
|
|
634
|
+
// fatal-abort path with no forensic trail.
|
|
635
|
+
const toolCallNames = new Map();
|
|
498
636
|
// Forward every relevant bus event to the webview.
|
|
499
637
|
session.eventBus.on("text_delta", (d) => broadcast("text_delta", d));
|
|
500
638
|
session.eventBus.on("thinking_delta", (d) => broadcast("thinking_delta", d));
|
|
501
|
-
// tool_call_end omits the tool name, so remember it from tool_call_start (keyed
|
|
502
|
-
// by toolCallId) to detect when the `tasks` tool just mutated the task store.
|
|
503
|
-
const toolNamesById = new Map();
|
|
504
639
|
session.eventBus.on("tool_call_start", (d) => {
|
|
505
|
-
|
|
506
|
-
toolNamesById.set(d.toolCallId, d.name);
|
|
640
|
+
toolCallNames.set(d.toolCallId, d.name);
|
|
507
641
|
broadcast("tool_call_start", d);
|
|
508
642
|
});
|
|
509
643
|
session.eventBus.on("tool_call_update", (d) => broadcast("tool_call_update", d));
|
|
510
644
|
session.eventBus.on("tool_call_end", (d) => {
|
|
645
|
+
const name = toolCallNames.get(d.toolCallId) ?? "unknown";
|
|
646
|
+
toolCallNames.delete(d.toolCallId);
|
|
647
|
+
log(d.isError ? "ERROR" : "INFO", "tool", `Tool call ended: ${name}`, {
|
|
648
|
+
id: d.toolCallId,
|
|
649
|
+
durationMs: String(d.durationMs),
|
|
650
|
+
isError: String(d.isError),
|
|
651
|
+
...(d.isError ? { result: d.result.slice(0, 500) } : {}),
|
|
652
|
+
});
|
|
511
653
|
broadcast("tool_call_end", d);
|
|
512
654
|
// The agent can add/done/remove project tasks via the `tasks` tool during a
|
|
513
655
|
// normal chat run. Push the refreshed list so the webview's Tasks modal and
|
|
514
656
|
// footer badge update live instead of going stale until the modal reopens.
|
|
515
|
-
const name = d?.toolCallId ? toolNamesById.get(d.toolCallId) : undefined;
|
|
516
|
-
if (d?.toolCallId)
|
|
517
|
-
toolNamesById.delete(d.toolCallId);
|
|
518
657
|
if (name === "tasks") {
|
|
519
658
|
broadcast("tasks_list", { tasks: loadTasksSync(cwd) });
|
|
520
659
|
}
|
|
@@ -526,7 +665,11 @@ async function main() {
|
|
|
526
665
|
session.eventBus.on("server_tool_call", (d) => broadcast("server_tool_call", d));
|
|
527
666
|
session.eventBus.on("turn_end", (d) => broadcast("turn_end", d));
|
|
528
667
|
session.eventBus.on("agent_done", (d) => broadcast("agent_done", d));
|
|
529
|
-
session.eventBus.on("error", (d) =>
|
|
668
|
+
session.eventBus.on("error", (d) => {
|
|
669
|
+
const message = d.error instanceof Error ? d.error.message : String(d.error);
|
|
670
|
+
log("ERROR", "app-sidecar", "agent error", { message });
|
|
671
|
+
broadcast("error", { message });
|
|
672
|
+
});
|
|
530
673
|
session.eventBus.on("model_change", (d) => broadcast("model_change", d));
|
|
531
674
|
session.eventBus.on("hook", (d) => broadcast("hook", d));
|
|
532
675
|
session.eventBus.on("compaction_start", (d) => broadcast("compaction_start", d));
|
|
@@ -694,19 +837,8 @@ async function main() {
|
|
|
694
837
|
});
|
|
695
838
|
res.end(payload);
|
|
696
839
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
const method = req.method ?? "GET";
|
|
700
|
-
// CORS preflight — the webview origin differs from 127.0.0.1.
|
|
701
|
-
if (method === "OPTIONS") {
|
|
702
|
-
res.writeHead(204, {
|
|
703
|
-
"access-control-allow-origin": "*",
|
|
704
|
-
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
705
|
-
"access-control-allow-headers": "content-type",
|
|
706
|
-
});
|
|
707
|
-
res.end();
|
|
708
|
-
return;
|
|
709
|
-
}
|
|
840
|
+
// OPTIONS/CORS preflight is handled at the daemon level before delegation.
|
|
841
|
+
function handle(req, res, url, method) {
|
|
710
842
|
if (method === "GET" && url === "/state") {
|
|
711
843
|
const st = session.getState();
|
|
712
844
|
json(res, 200, {
|
|
@@ -720,7 +852,7 @@ async function main() {
|
|
|
720
852
|
});
|
|
721
853
|
return;
|
|
722
854
|
}
|
|
723
|
-
if (method === "GET" && url === "/events") {
|
|
855
|
+
if (method === "GET" && (url === "/events" || url.startsWith("/events?"))) {
|
|
724
856
|
res.writeHead(200, {
|
|
725
857
|
"content-type": "text/event-stream",
|
|
726
858
|
"cache-control": "no-cache",
|
|
@@ -1064,9 +1196,14 @@ async function main() {
|
|
|
1064
1196
|
json(res, 200, { tasks: loadTasksSync(cwd) });
|
|
1065
1197
|
return;
|
|
1066
1198
|
}
|
|
1067
|
-
// ── Radio
|
|
1068
|
-
//
|
|
1069
|
-
//
|
|
1199
|
+
// ── Radio (app-wide) ──────────────────────────────────────
|
|
1200
|
+
// Radio is now APP-WIDE: all windows share one daemon process, and the
|
|
1201
|
+
// player lives in `core/radio.ts` module-level singletons (one stream for
|
|
1202
|
+
// the whole app). Any window's /radio reads/controls that single stream —
|
|
1203
|
+
// starting a station in one window replaces whatever was playing, and every
|
|
1204
|
+
// window's footer reflects the same `current`. This intentionally prevents
|
|
1205
|
+
// duplicate audio across windows (the original per-window goal), now for
|
|
1206
|
+
// free. (To restore per-window radio, key playback by sessionId.)
|
|
1070
1207
|
if (method === "GET" && url === "/radio") {
|
|
1071
1208
|
json(res, 200, { stations: RADIO_STATIONS, current: getCurrentStation() });
|
|
1072
1209
|
return;
|
|
@@ -1117,6 +1254,83 @@ async function main() {
|
|
|
1117
1254
|
});
|
|
1118
1255
|
return;
|
|
1119
1256
|
}
|
|
1257
|
+
if (method === "POST" && url === "/tasks/add") {
|
|
1258
|
+
void readBody(req).then((raw) => {
|
|
1259
|
+
let title;
|
|
1260
|
+
let prompt;
|
|
1261
|
+
try {
|
|
1262
|
+
const body = JSON.parse(raw);
|
|
1263
|
+
title = (body.title ?? "").trim();
|
|
1264
|
+
prompt = (body.prompt ?? "").trim();
|
|
1265
|
+
}
|
|
1266
|
+
catch {
|
|
1267
|
+
json(res, 400, { error: "invalid JSON body" });
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
if (!title) {
|
|
1271
|
+
json(res, 400, { error: "missing task title" });
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
const tasks = loadTasksSync(cwd);
|
|
1275
|
+
const newTask = {
|
|
1276
|
+
id: randomUUID(),
|
|
1277
|
+
title,
|
|
1278
|
+
prompt: prompt || title,
|
|
1279
|
+
status: "pending",
|
|
1280
|
+
createdAt: new Date().toISOString(),
|
|
1281
|
+
};
|
|
1282
|
+
const next = [...tasks, newTask];
|
|
1283
|
+
saveTasksSync(cwd, next);
|
|
1284
|
+
broadcast("tasks_list", { tasks: next });
|
|
1285
|
+
json(res, 200, { tasks: next });
|
|
1286
|
+
});
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
if (method === "POST" && url === "/tasks/update") {
|
|
1290
|
+
void readBody(req).then((raw) => {
|
|
1291
|
+
let id;
|
|
1292
|
+
let status;
|
|
1293
|
+
let title;
|
|
1294
|
+
let prompt;
|
|
1295
|
+
try {
|
|
1296
|
+
const body = JSON.parse(raw);
|
|
1297
|
+
id = (body.id ?? "").trim();
|
|
1298
|
+
status = body.status;
|
|
1299
|
+
title = body.title;
|
|
1300
|
+
prompt = body.prompt;
|
|
1301
|
+
}
|
|
1302
|
+
catch {
|
|
1303
|
+
json(res, 400, { error: "invalid JSON body" });
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
if (!id) {
|
|
1307
|
+
json(res, 400, { error: "missing task id" });
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
if (status && status !== "pending" && status !== "in-progress" && status !== "done") {
|
|
1311
|
+
json(res, 400, { error: `invalid status: ${status}` });
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
const tasks = loadTasksSync(cwd);
|
|
1315
|
+
const task = tasks.find((t) => t.id === id || t.id.startsWith(id));
|
|
1316
|
+
if (!task) {
|
|
1317
|
+
json(res, 404, { error: `no task found matching id "${id}"` });
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
const next = tasks.map((t) => t.id === task.id
|
|
1321
|
+
? {
|
|
1322
|
+
...t,
|
|
1323
|
+
...(status ? { status: status } : {}),
|
|
1324
|
+
...(typeof title === "string" && title.trim() ? { title: title.trim() } : {}),
|
|
1325
|
+
...(typeof prompt === "string" && prompt.trim() ? { prompt: prompt.trim() } : {}),
|
|
1326
|
+
}
|
|
1327
|
+
: t);
|
|
1328
|
+
saveTasksSync(cwd, next);
|
|
1329
|
+
broadcast("tasks_list", { tasks: next });
|
|
1330
|
+
json(res, 200, { tasks: next });
|
|
1331
|
+
});
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1120
1334
|
if (method === "POST" && url === "/tasks/delete") {
|
|
1121
1335
|
void readBody(req).then((raw) => {
|
|
1122
1336
|
let id;
|
|
@@ -1133,6 +1347,7 @@ async function main() {
|
|
|
1133
1347
|
}
|
|
1134
1348
|
const remaining = loadTasksSync(cwd).filter((t) => t.id !== id && !t.id.startsWith(id));
|
|
1135
1349
|
saveTasksSync(cwd, remaining);
|
|
1350
|
+
broadcast("tasks_list", { tasks: remaining });
|
|
1136
1351
|
json(res, 200, { tasks: remaining });
|
|
1137
1352
|
});
|
|
1138
1353
|
return;
|
|
@@ -1705,30 +1920,28 @@ async function main() {
|
|
|
1705
1920
|
return;
|
|
1706
1921
|
}
|
|
1707
1922
|
json(res, 404, { error: "not found" });
|
|
1708
|
-
}
|
|
1709
|
-
|
|
1710
|
-
const addr = server.address();
|
|
1711
|
-
// The Rust shell reads this line to learn the port.
|
|
1712
|
-
process.stdout.write(`GG_APP_LISTENING ${addr.port}\n`);
|
|
1713
|
-
log("INFO", "app-sidecar", "listening", { port: String(addr.port), host });
|
|
1714
|
-
});
|
|
1715
|
-
const shutdown = async () => {
|
|
1923
|
+
}
|
|
1924
|
+
async function dispose() {
|
|
1716
1925
|
tasksPollStopped = true;
|
|
1717
1926
|
if (tasksPoll)
|
|
1718
1927
|
clearTimeout(tasksPoll);
|
|
1719
|
-
// Kill any playing radio so the stream dies with its window.
|
|
1720
|
-
stopRadio();
|
|
1721
1928
|
// Stop the Telegram serve loop + dispose its per-chat sessions.
|
|
1722
1929
|
if (serveController)
|
|
1723
1930
|
await serveController.stop().catch(() => { });
|
|
1724
1931
|
for (const c of clients)
|
|
1725
1932
|
c.res.end();
|
|
1726
|
-
server.close();
|
|
1727
1933
|
await session.dispose().catch(() => { });
|
|
1728
|
-
|
|
1934
|
+
}
|
|
1935
|
+
return {
|
|
1936
|
+
id: opts.id,
|
|
1937
|
+
cwd,
|
|
1938
|
+
sessionPath: opts.sessionPath,
|
|
1939
|
+
session,
|
|
1940
|
+
clients,
|
|
1941
|
+
broadcast,
|
|
1942
|
+
handle,
|
|
1943
|
+
dispose,
|
|
1729
1944
|
};
|
|
1730
|
-
process.on("SIGINT", () => void shutdown());
|
|
1731
|
-
process.on("SIGTERM", () => void shutdown());
|
|
1732
1945
|
}
|
|
1733
1946
|
main().catch((err) => {
|
|
1734
1947
|
const message = err instanceof Error ? err.message : String(err);
|