@runcore-sh/runcore 0.5.6 → 0.5.8
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/dictionary.json +2 -2
- package/dist/.extensions/ext-byok.json +1 -0
- package/dist/.extensions/ext-hosted.json +1 -0
- package/dist/.extensions/ext-spawn.json +1 -0
- package/dist/agents/autonomous.d.ts.map +1 -1
- package/dist/agents/runtime/bus.d.ts +1 -0
- package/dist/agents/runtime/bus.d.ts.map +1 -1
- package/dist/agents/spawn.d.ts.map +1 -1
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +3 -2
- package/dist/auth/middleware.js.map +1 -1
- package/dist/calendar/routes.d.ts.map +1 -1
- package/dist/calendar/routes.js +8 -7
- package/dist/calendar/routes.js.map +1 -1
- package/dist/files/registry.d.ts +4 -5
- package/dist/files/registry.d.ts.map +1 -1
- package/dist/files/registry.js +4 -5
- package/dist/files/registry.js.map +1 -1
- package/dist/files/store.d.ts +2 -0
- package/dist/files/store.d.ts.map +1 -1
- package/dist/files/store.js +5 -1
- package/dist/files/store.js.map +1 -1
- package/dist/instance.d.ts +2 -0
- package/dist/instance.d.ts.map +1 -1
- package/dist/instance.js +2 -0
- package/dist/instance.js.map +1 -1
- package/dist/lib/paths.d.ts.map +1 -1
- package/dist/lib/paths.js +15 -1
- package/dist/lib/paths.js.map +1 -1
- package/dist/library/brain-shadow.d.ts.map +1 -1
- package/dist/library/brain-shadow.js +5 -4
- package/dist/library/brain-shadow.js.map +1 -1
- package/dist/library/routes.d.ts.map +1 -1
- package/dist/library/routes.js +10 -9
- package/dist/library/routes.js.map +1 -1
- package/dist/llm/cache.d.ts.map +1 -1
- package/dist/llm/cache.js +7 -5
- package/dist/llm/cache.js.map +1 -1
- package/dist/llm/complete.js +2 -0
- package/dist/llm/complete.js.map +1 -1
- package/dist/llm/providers/ollama.d.ts.map +1 -1
- package/dist/llm/providers/ollama.js +32 -7
- package/dist/llm/providers/ollama.js.map +1 -1
- package/dist/llm/providers/openrouter.d.ts.map +1 -1
- package/dist/llm/providers/openrouter.js +105 -12
- package/dist/llm/providers/openrouter.js.map +1 -1
- package/dist/llm/providers/types.d.ts +18 -0
- package/dist/llm/providers/types.d.ts.map +1 -1
- package/dist/llm/retry.d.ts.map +1 -1
- package/dist/llm/retry.js +6 -0
- package/dist/llm/retry.js.map +1 -1
- package/dist/llm/tools/handlers.d.ts +27 -0
- package/dist/llm/tools/handlers.d.ts.map +1 -0
- package/dist/llm/tools/handlers.js +842 -0
- package/dist/llm/tools/handlers.js.map +1 -0
- package/dist/llm/tools/index.d.ts +12 -0
- package/dist/llm/tools/index.d.ts.map +1 -0
- package/dist/llm/tools/index.js +10 -0
- package/dist/llm/tools/index.js.map +1 -0
- package/dist/llm/tools/loop.d.ts +47 -0
- package/dist/llm/tools/loop.d.ts.map +1 -0
- package/dist/llm/tools/loop.js +126 -0
- package/dist/llm/tools/loop.js.map +1 -0
- package/dist/llm/tools/registry.d.ts +27 -0
- package/dist/llm/tools/registry.d.ts.map +1 -0
- package/dist/llm/tools/registry.js +60 -0
- package/dist/llm/tools/registry.js.map +1 -0
- package/dist/llm/tools/schemas.d.ts +92 -0
- package/dist/llm/tools/schemas.d.ts.map +1 -0
- package/dist/llm/tools/schemas.js +154 -0
- package/dist/llm/tools/schemas.js.map +1 -0
- package/dist/llm/tools/types.d.ts +44 -0
- package/dist/llm/tools/types.d.ts.map +1 -0
- package/dist/llm/tools/types.js +9 -0
- package/dist/llm/tools/types.js.map +1 -0
- package/dist/mcp-server.d.ts +1 -1
- package/dist/mcp-server.js +249 -5
- package/dist/mcp-server.js.map +1 -1
- package/dist/memory/visual.d.ts.map +1 -1
- package/dist/memory/visual.js +3 -6
- package/dist/memory/visual.js.map +1 -1
- package/dist/openloop/foldback.js +1 -1
- package/dist/openloop/foldback.js.map +1 -1
- package/dist/openloop/resolution-scanner.d.ts.map +1 -1
- package/dist/openloop/resolution-scanner.js +76 -63
- package/dist/openloop/resolution-scanner.js.map +1 -1
- package/dist/plugins/github/index.d.ts +49 -0
- package/dist/plugins/github/index.d.ts.map +1 -0
- package/dist/plugins/github/index.js +153 -0
- package/dist/plugins/github/index.js.map +1 -0
- package/dist/plugins/index.d.ts +1 -2
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +79 -2
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/slack/index.d.ts +43 -0
- package/dist/plugins/slack/index.d.ts.map +1 -0
- package/dist/plugins/slack/index.js +158 -0
- package/dist/plugins/slack/index.js.map +1 -0
- package/dist/plugins/twilio/index.d.ts +41 -0
- package/dist/plugins/twilio/index.d.ts.map +1 -0
- package/dist/plugins/twilio/index.js +102 -0
- package/dist/plugins/twilio/index.js.map +1 -0
- package/dist/pulse/tier.d.ts +1 -1
- package/dist/pulse/tier.js +2 -2
- package/dist/pulse/tier.js.map +1 -1
- package/dist/search/gemini.d.ts +27 -0
- package/dist/search/gemini.d.ts.map +1 -0
- package/dist/search/gemini.js +103 -0
- package/dist/search/gemini.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +850 -536
- package/dist/server.js.map +1 -1
- package/dist/services/routine-patterns.d.ts.map +1 -1
- package/dist/services/routine-patterns.js +6 -0
- package/dist/services/routine-patterns.js.map +1 -1
- package/dist/services/traceInsights.d.ts +5 -0
- package/dist/services/traceInsights.d.ts.map +1 -1
- package/dist/services/traceInsights.js +18 -1
- package/dist/services/traceInsights.js.map +1 -1
- package/dist/settings.d.ts +1 -1
- package/dist/settings.d.ts.map +1 -1
- package/dist/types.d.ts +26 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/vault/store.d.ts +1 -1
- package/dist/vault/store.d.ts.map +1 -1
- package/dist/webhooks/mount.d.ts.map +1 -1
- package/dist/whiteboard/store.d.ts +40 -0
- package/dist/whiteboard/store.d.ts.map +1 -0
- package/dist/whiteboard/store.js +280 -0
- package/dist/whiteboard/store.js.map +1 -0
- package/dist/whiteboard/types.d.ts +55 -0
- package/dist/whiteboard/types.d.ts.map +1 -0
- package/dist/whiteboard/types.js +9 -0
- package/dist/whiteboard/types.js.map +1 -0
- package/dist/whiteboard/weight.d.ts +23 -0
- package/dist/whiteboard/weight.d.ts.map +1 -0
- package/dist/whiteboard/weight.js +126 -0
- package/dist/whiteboard/weight.js.map +1 -0
- package/package.json +3 -2
- package/public/index.html +225 -51
- package/public/search-flyout.js +324 -0
- package/public/whiteboard.html +915 -0
- package/public/avatar/Hey-Dash_en_windows_v4_0_0.zip +0 -0
- package/public/avatar/README.md +0 -43
- package/public/avatar/cache/06fa55aececcc478.mp4 +0 -0
- package/public/avatar/cache/07a65738ba170827.mp4 +0 -0
- package/public/avatar/cache/08b6f4880f59a385.mp4 +0 -0
- package/public/avatar/cache/0ef9e0e78d715af4.mp4 +0 -0
- package/public/avatar/cache/0fa85e9e8f444a8b.mp4 +0 -0
- package/public/avatar/cache/1184385ec5522b57.mp4 +0 -0
- package/public/avatar/cache/1185fd491f413406.mp4 +0 -0
- package/public/avatar/cache/1b374d5390258fea.mp4 +0 -0
- package/public/avatar/cache/1e2367029b92f8aa.mp4 +0 -0
- package/public/avatar/cache/1f15f6a1ebd7e439.mp4 +0 -0
- package/public/avatar/cache/272c004a41087de5.mp4 +0 -0
- package/public/avatar/cache/2a0f3ff34d92521a.mp4 +0 -0
- package/public/avatar/cache/2c7e47ff0bdeb8d1.mp4 +0 -0
- package/public/avatar/cache/307a6f70859aeab8.mp4 +0 -0
- package/public/avatar/cache/332384e088ca214b.mp4 +0 -0
- package/public/avatar/cache/39fc4e81574d14ed.mp4 +0 -0
- package/public/avatar/cache/4a5c6051c1ef6a71.mp4 +0 -0
- package/public/avatar/cache/51f4aa76398c8c29.mp4 +0 -0
- package/public/avatar/cache/5d9a960bbf71732c.mp4 +0 -0
- package/public/avatar/cache/5e0954401e15af89.mp4 +0 -0
- package/public/avatar/cache/5f308566f7abb8f2.mp4 +0 -0
- package/public/avatar/cache/62f9cfba848d724e.mp4 +0 -0
- package/public/avatar/cache/6d64e657e6bf2aab.mp4 +0 -0
- package/public/avatar/cache/763ad0349e0b6f26.mp4 +0 -0
- package/public/avatar/cache/81a516cfd461b2b9.mp4 +0 -0
- package/public/avatar/cache/884ae6717fcacdd5.mp4 +0 -0
- package/public/avatar/cache/8ea0b7220d139615.mp4 +0 -0
- package/public/avatar/cache/9366de15fd6910ca.mp4 +0 -0
- package/public/avatar/cache/9b9c4f7b8508eecc.mp4 +0 -0
- package/public/avatar/cache/9be1030ec2aa2b01.mp4 +0 -0
- package/public/avatar/cache/ade41a846b283895.mp4 +0 -0
- package/public/avatar/cache/b35f7a3d558f22cb.mp4 +0 -0
- package/public/avatar/cache/b6066e5c65383eec.mp4 +0 -0
- package/public/avatar/cache/be89f49970672374.mp4 +0 -0
- package/public/avatar/cache/c11fdc99479492b6.mp4 +0 -0
- package/public/avatar/cache/c900811e3382ac6d.mp4 +0 -0
- package/public/avatar/cache/d42a73667acf5716.mp4 +0 -0
- package/public/avatar/cache/e539f247a8908603.mp4 +0 -0
- package/public/avatar/cache/e78fceae2373b7c1.mp4 +0 -0
- package/public/avatar/cache/ec95af57d33b3f07.mp4 +0 -0
- package/public/avatar/cache/edadb75d37891fc7.mp4 +0 -0
- package/public/avatar/cache/eeb8d775f40dbe2c.mp4 +0 -0
- package/public/avatar/cache/f0ae159640621dd9.mp4 +0 -0
- package/public/avatar/cache/fc2e5419adf29d96.mp4 +0 -0
- package/public/avatar/dash_headhshot_v1.png +0 -0
- package/public/avatar/idle.mp4 +0 -0
- package/public/avatar/photo.png +0 -0
package/dist/server.js
CHANGED
|
@@ -10,6 +10,9 @@ import { join, dirname } from "node:path";
|
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
12
12
|
import { acquireLock, releaseLock } from "./runtime-lock.js";
|
|
13
|
+
import { ToolRegistry } from "./llm/tools/registry.js";
|
|
14
|
+
import { createToolHandlers } from "./llm/tools/handlers.js";
|
|
15
|
+
import { streamWithTools } from "./llm/tools/loop.js";
|
|
13
16
|
// Package root — works whether run from CWD or npx
|
|
14
17
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
18
|
const __dirname = dirname(__filename);
|
|
@@ -146,7 +149,7 @@ import { brainShadowRoutes, initBrainShadow } from "./library/brain-shadow.js";
|
|
|
146
149
|
import { createCalendarStore } from "./calendar/store.js";
|
|
147
150
|
import { calendarRoutes } from "./calendar/routes.js";
|
|
148
151
|
import { getGoogleCalendarAdapter } from "./calendar/google-adapter.js";
|
|
149
|
-
import { saveVisualMemory,
|
|
152
|
+
import { saveVisualMemory, isVisualMemory, searchVisualMemories } from "./memory/visual.js";
|
|
150
153
|
// --- LLM provider selection (via settings) ---
|
|
151
154
|
function pickStreamFn() {
|
|
152
155
|
const providerName = resolveProvider();
|
|
@@ -284,6 +287,42 @@ async function getOrCreateChatSession(sessionId, name) {
|
|
|
284
287
|
}
|
|
285
288
|
}
|
|
286
289
|
catch { }
|
|
290
|
+
// Fetch whiteboard status for system prompt injection
|
|
291
|
+
let whiteboardContext = "";
|
|
292
|
+
try {
|
|
293
|
+
const { WhiteboardStore } = await import("./whiteboard/store.js");
|
|
294
|
+
const wbStore = new WhiteboardStore(BRAIN_DIR);
|
|
295
|
+
const summary = await wbStore.getSummary();
|
|
296
|
+
if (summary.total > 0) {
|
|
297
|
+
const parts = [`Whiteboard: ${summary.total} items (${summary.open} open, ${summary.done} done, ${summary.openQuestions} open questions)`];
|
|
298
|
+
// Open questions from human
|
|
299
|
+
const openQs = await wbStore.getOpenQuestions();
|
|
300
|
+
const humanQs = openQs.filter((q) => q.plantedBy === "human");
|
|
301
|
+
if (humanQs.length > 0) {
|
|
302
|
+
parts.push("Questions from human (answer these):");
|
|
303
|
+
for (const q of humanQs)
|
|
304
|
+
parts.push(` → "${q.question || q.title}" (id: ${q.id})`);
|
|
305
|
+
}
|
|
306
|
+
// Answered questions to act on
|
|
307
|
+
const allNodes = await wbStore.list();
|
|
308
|
+
const answered = allNodes.filter((n) => n.type === "question" && n.answer && n.status === "done");
|
|
309
|
+
if (answered.length > 0) {
|
|
310
|
+
parts.push("Answered questions (act on these — the answer IS the instruction):");
|
|
311
|
+
for (const a of answered)
|
|
312
|
+
parts.push(` Q: "${a.question || a.title}" → A: ${a.answer}`);
|
|
313
|
+
}
|
|
314
|
+
// Top weighted
|
|
315
|
+
if (summary.topWeighted.length > 0) {
|
|
316
|
+
parts.push("Top attention items:");
|
|
317
|
+
for (const n of summary.topWeighted) {
|
|
318
|
+
const icon = n.type === "question" ? "?" : n.type === "decision" ? "!" : "-";
|
|
319
|
+
parts.push(` ${icon} [${n.weight}] ${n.title}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
whiteboardContext = parts.join("\n");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch { }
|
|
287
326
|
const encryptionKey = sessionKeys.get(sessionId) ?? null;
|
|
288
327
|
const ltm = new FileSystemLongTermMemory(MEMORY_DIR, encryptionKey ?? undefined);
|
|
289
328
|
await ltm.init();
|
|
@@ -398,6 +437,7 @@ async function getOrCreateChatSession(sessionId, name) {
|
|
|
398
437
|
...(TIER_CAPS[activeTier].spawning ? [
|
|
399
438
|
`## Agent spawning (CRITICAL — follow exactly)`,
|
|
400
439
|
`When a task requires code editing, file operations, or shell commands, you MUST spawn a Claude Code agent.`,
|
|
440
|
+
`EXCEPTION: Your built-in tools (whiteboard_plant, whiteboard_status, memory_learn, memory_retrieve, files_search, etc.) are NOT agent tasks. Call them directly via function calling — NEVER spawn an agent to call a tool you already have.`,
|
|
401
441
|
`Do NOT describe what you would do — actually spawn the agent by including the block below.`,
|
|
402
442
|
`The block content MUST be valid JSON with "label" and "prompt" keys. No markdown, no backticks, no explanation inside the block.`,
|
|
403
443
|
``,
|
|
@@ -424,9 +464,31 @@ async function getOrCreateChatSession(sessionId, name) {
|
|
|
424
464
|
`Agent failures are normal (auth issues, timeouts, environment mismatches). Never stop spawning agents because of past failures.`,
|
|
425
465
|
`IMPORTANT: Do NOT announce agent spawns in your visible response text. No "Agent spawned to...", no "I'll spawn an agent...", no "Let me run an agent...". The UI shows agent status automatically. Just include the [AGENT_REQUEST] block silently at the end. Your visible text should answer the user's question or continue the conversation naturally.`,
|
|
426
466
|
] : []), // end spawning gate
|
|
467
|
+
``,
|
|
468
|
+
`## Tools`,
|
|
469
|
+
`You have function-calling tools available. Use them directly — they execute server-side and return results inline.`,
|
|
470
|
+
`Key tools: whiteboard_plant, whiteboard_status, memory_retrieve, memory_learn, files_search, read_brain_file.`,
|
|
471
|
+
`Call tools whenever you need to read or write to the brain, whiteboard, or memory. Do NOT describe what you would do — call the tool.`,
|
|
472
|
+
``,
|
|
473
|
+
`## Whiteboard (shared collaboration surface)`,
|
|
474
|
+
`You and ${name} share a whiteboard — a tree of goals, tasks, questions, decisions, and notes at /whiteboard.`,
|
|
475
|
+
`Use the whiteboard_plant tool to plant nodes. Use whiteboard_status to check the board.`,
|
|
476
|
+
``,
|
|
477
|
+
...(whiteboardContext ? [
|
|
478
|
+
`### Current whiteboard state`,
|
|
479
|
+
whiteboardContext,
|
|
480
|
+
``,
|
|
481
|
+
] : []),
|
|
482
|
+
`### When to plant questions (IMPORTANT)`,
|
|
483
|
+
`Before spawning an agent for an ambiguous task, plant a question on the whiteboard using whiteboard_plant.`,
|
|
484
|
+
`If a task has multiple valid approaches and you're unsure which ${name} wants, plant a question and wait for their answer.`,
|
|
485
|
+
``,
|
|
486
|
+
`### Answered questions`,
|
|
487
|
+
`If you see answered questions above, ACT ON THEM IMMEDIATELY. Do not ask "which one should I focus on?" or "what would you like me to do?" — the answer IS the instruction. Read it, do the work, report what you did. ${name} already told you what to do by answering — don't make them say it twice.`,
|
|
488
|
+
``,
|
|
427
489
|
// Inject instance-readable vault values (CORE_*/DASH_* prefixed only — never secrets)
|
|
428
490
|
...(() => {
|
|
429
|
-
const readable = (_vaultStore ? _vaultStore.
|
|
491
|
+
const readable = (_vaultStore ? _vaultStore.getInstanceReadableVault() : []);
|
|
430
492
|
if (readable.length === 0)
|
|
431
493
|
return [];
|
|
432
494
|
const lines = readable.map((r) => `- ${r.name}: ${r.value}`);
|
|
@@ -507,7 +569,7 @@ import { TIER_CAPS } from "./tier/types.js";
|
|
|
507
569
|
let activeTier = "local";
|
|
508
570
|
const app = new Hono();
|
|
509
571
|
// Global error handler — structured JSON errors
|
|
510
|
-
import { errorHandler, ApiError } from "./middleware/error-handler.js";
|
|
572
|
+
import { errorHandler, ApiError, badRequest, unauthorized, forbidden, notFound } from "./middleware/error-handler.js";
|
|
511
573
|
app.onError((err, c) => {
|
|
512
574
|
// Use structured handler for ApiErrors; preserve original behavior for others
|
|
513
575
|
if (err instanceof ApiError)
|
|
@@ -671,13 +733,13 @@ app.post("/api/pair", async (c) => {
|
|
|
671
733
|
// Backward compat: accept "safeWord" from old clients
|
|
672
734
|
const pw = password || safeWord;
|
|
673
735
|
if (!code || !name || !pw) {
|
|
674
|
-
return
|
|
736
|
+
return badRequest("Name and password required");
|
|
675
737
|
}
|
|
676
738
|
// Auto-token bypass: startup token already proved the user is local.
|
|
677
739
|
const skipCodeCheck = code === "__auto_token__";
|
|
678
740
|
const result = await pair({ code, name, password: pw, recoveryQuestion, recoveryAnswer, skipCodeCheck });
|
|
679
741
|
if ("error" in result) {
|
|
680
|
-
return
|
|
742
|
+
return badRequest(result.error);
|
|
681
743
|
}
|
|
682
744
|
sessionKeys.set(result.session.id, result.sessionKey);
|
|
683
745
|
setEncryptionKey(result.sessionKey);
|
|
@@ -706,11 +768,11 @@ app.post("/api/auth", async (c) => {
|
|
|
706
768
|
// Backward compat: accept "safeWord" from old clients
|
|
707
769
|
const pw = password || safeWord;
|
|
708
770
|
if (!pw) {
|
|
709
|
-
return
|
|
771
|
+
return badRequest("Password required");
|
|
710
772
|
}
|
|
711
773
|
const result = await authenticate(pw);
|
|
712
774
|
if ("error" in result) {
|
|
713
|
-
return
|
|
775
|
+
return unauthorized(result.error);
|
|
714
776
|
}
|
|
715
777
|
sessionKeys.set(result.session.id, result.sessionKey);
|
|
716
778
|
setEncryptionKey(result.sessionKey);
|
|
@@ -755,7 +817,7 @@ app.get("/api/auth/token", async (c) => {
|
|
|
755
817
|
app.get("/api/recover", async (c) => {
|
|
756
818
|
const question = await getRecoveryQuestion();
|
|
757
819
|
if (!question) {
|
|
758
|
-
return
|
|
820
|
+
return badRequest("Not paired yet");
|
|
759
821
|
}
|
|
760
822
|
return c.json({ question });
|
|
761
823
|
});
|
|
@@ -766,11 +828,11 @@ app.post("/api/recover", async (c) => {
|
|
|
766
828
|
// Backward compat: accept "newSafeWord" from old clients
|
|
767
829
|
const newPw = newPassword || newSafeWord;
|
|
768
830
|
if (!answer || !newPw) {
|
|
769
|
-
return
|
|
831
|
+
return badRequest("Answer and new password required");
|
|
770
832
|
}
|
|
771
833
|
const result = await recover(answer, newPw);
|
|
772
834
|
if ("error" in result) {
|
|
773
|
-
return
|
|
835
|
+
return unauthorized(result.error);
|
|
774
836
|
}
|
|
775
837
|
sessionKeys.set(result.session.id, result.sessionKey);
|
|
776
838
|
setEncryptionKey(result.sessionKey);
|
|
@@ -808,7 +870,7 @@ async function savePairedDevices() {
|
|
|
808
870
|
app.post("/api/mobile/voucher", async (c) => {
|
|
809
871
|
const sessionId = c.req.query("sessionId") || c.req.header("x-session-id");
|
|
810
872
|
if (!sessionId || !validateSession(sessionId)) {
|
|
811
|
-
return
|
|
873
|
+
return unauthorized("Unauthorized");
|
|
812
874
|
}
|
|
813
875
|
const { randomBytes: rng } = await import("node:crypto");
|
|
814
876
|
const { createHash } = await import("node:crypto");
|
|
@@ -867,7 +929,7 @@ app.get("/api/mobile/info/:token", async (c) => {
|
|
|
867
929
|
const token = c.req.param("token");
|
|
868
930
|
const voucher = deviceVouchers.get(token);
|
|
869
931
|
if (!voucher || voucher.consumed || voucher.expiresAt < Date.now()) {
|
|
870
|
-
return
|
|
932
|
+
return notFound("Invalid or expired voucher");
|
|
871
933
|
}
|
|
872
934
|
return c.json({
|
|
873
935
|
instanceName: voucher.instanceName,
|
|
@@ -879,16 +941,16 @@ app.post("/api/mobile/redeem", async (c) => {
|
|
|
879
941
|
const body = await c.req.json();
|
|
880
942
|
const { token, password } = body;
|
|
881
943
|
if (!token || !password) {
|
|
882
|
-
return
|
|
944
|
+
return badRequest("Voucher token and password required");
|
|
883
945
|
}
|
|
884
946
|
const voucher = deviceVouchers.get(token);
|
|
885
947
|
if (!voucher || voucher.consumed || voucher.expiresAt < Date.now()) {
|
|
886
|
-
return
|
|
948
|
+
return notFound("Invalid or expired voucher");
|
|
887
949
|
}
|
|
888
950
|
// Validate safe word via existing auth
|
|
889
951
|
const authResult = await authenticate(password);
|
|
890
952
|
if ("error" in authResult) {
|
|
891
|
-
return
|
|
953
|
+
return unauthorized("Invalid safe word");
|
|
892
954
|
}
|
|
893
955
|
// Consume voucher
|
|
894
956
|
voucher.consumed = true;
|
|
@@ -919,7 +981,7 @@ app.post("/api/mobile/redeem", async (c) => {
|
|
|
919
981
|
app.get("/api/mobile/devices", async (c) => {
|
|
920
982
|
const sessionId = c.req.query("sessionId") || c.req.header("x-session-id");
|
|
921
983
|
if (!sessionId || !validateSession(sessionId)) {
|
|
922
|
-
return
|
|
984
|
+
return unauthorized("Unauthorized");
|
|
923
985
|
}
|
|
924
986
|
return c.json({
|
|
925
987
|
devices: [...pairedDevices.values()].map((d) => ({
|
|
@@ -933,7 +995,7 @@ app.get("/api/mobile/devices", async (c) => {
|
|
|
933
995
|
app.delete("/api/mobile/devices/:label", async (c) => {
|
|
934
996
|
const sessionId = c.req.query("sessionId") || c.req.header("x-session-id");
|
|
935
997
|
if (!sessionId || !validateSession(sessionId)) {
|
|
936
|
-
return
|
|
998
|
+
return unauthorized("Unauthorized");
|
|
937
999
|
}
|
|
938
1000
|
const label = c.req.param("label");
|
|
939
1001
|
for (const [token, d] of pairedDevices) {
|
|
@@ -943,7 +1005,7 @@ app.delete("/api/mobile/devices/:label", async (c) => {
|
|
|
943
1005
|
return c.json({ ok: true });
|
|
944
1006
|
}
|
|
945
1007
|
}
|
|
946
|
-
return
|
|
1008
|
+
return notFound("Device not found");
|
|
947
1009
|
});
|
|
948
1010
|
// --- Relay polling (receive messages from paired phones) ---
|
|
949
1011
|
let relayPollInterval = null;
|
|
@@ -1156,28 +1218,28 @@ async function handleRelayPair(token, password, label, instanceHash) {
|
|
|
1156
1218
|
app.get("/api/vault", async (c) => {
|
|
1157
1219
|
const sessionId = c.req.query("sessionId");
|
|
1158
1220
|
if (!sessionId)
|
|
1159
|
-
return
|
|
1221
|
+
return badRequest("sessionId required");
|
|
1160
1222
|
const session = validateSession(sessionId);
|
|
1161
1223
|
if (!session)
|
|
1162
|
-
return
|
|
1224
|
+
return unauthorized("Invalid or expired session");
|
|
1163
1225
|
return c.json({ keys: (_vaultStore ? _vaultStore.listVaultKeys() : []) });
|
|
1164
1226
|
});
|
|
1165
1227
|
// Add or update a vault key
|
|
1166
1228
|
app.put("/api/vault/:name", async (c) => {
|
|
1167
1229
|
const sessionId = c.req.query("sessionId");
|
|
1168
1230
|
if (!sessionId)
|
|
1169
|
-
return
|
|
1231
|
+
return badRequest("sessionId required");
|
|
1170
1232
|
const session = validateSession(sessionId);
|
|
1171
1233
|
if (!session)
|
|
1172
|
-
return
|
|
1234
|
+
return unauthorized("Invalid or expired session");
|
|
1173
1235
|
const key = sessionKeys.get(sessionId);
|
|
1174
1236
|
if (!key)
|
|
1175
|
-
return
|
|
1237
|
+
return unauthorized("Session key not found");
|
|
1176
1238
|
const name = c.req.param("name");
|
|
1177
1239
|
const body = await c.req.json();
|
|
1178
1240
|
const { value, label } = body;
|
|
1179
1241
|
if (!value)
|
|
1180
|
-
return
|
|
1242
|
+
return badRequest("value required");
|
|
1181
1243
|
await _vaultStore.setVaultKey(name, value, key, label);
|
|
1182
1244
|
return c.json({ ok: true });
|
|
1183
1245
|
});
|
|
@@ -1185,13 +1247,13 @@ app.put("/api/vault/:name", async (c) => {
|
|
|
1185
1247
|
app.delete("/api/vault/:name", async (c) => {
|
|
1186
1248
|
const sessionId = c.req.query("sessionId");
|
|
1187
1249
|
if (!sessionId)
|
|
1188
|
-
return
|
|
1250
|
+
return badRequest("sessionId required");
|
|
1189
1251
|
const session = validateSession(sessionId);
|
|
1190
1252
|
if (!session)
|
|
1191
|
-
return
|
|
1253
|
+
return unauthorized("Invalid or expired session");
|
|
1192
1254
|
const key = sessionKeys.get(sessionId);
|
|
1193
1255
|
if (!key)
|
|
1194
|
-
return
|
|
1256
|
+
return unauthorized("Session key not found");
|
|
1195
1257
|
const name = c.req.param("name");
|
|
1196
1258
|
await _vaultStore.deleteVaultKey(name, key);
|
|
1197
1259
|
return c.json({ ok: true });
|
|
@@ -1200,13 +1262,13 @@ app.delete("/api/vault/:name", async (c) => {
|
|
|
1200
1262
|
app.post("/api/vault/export", async (c) => {
|
|
1201
1263
|
const sessionId = c.req.query("sessionId");
|
|
1202
1264
|
if (!sessionId)
|
|
1203
|
-
return
|
|
1265
|
+
return badRequest("sessionId required");
|
|
1204
1266
|
const session = validateSession(sessionId);
|
|
1205
1267
|
if (!session)
|
|
1206
|
-
return
|
|
1268
|
+
return unauthorized("Invalid or expired session");
|
|
1207
1269
|
const body = await c.req.json();
|
|
1208
1270
|
if (!body.passphrase || body.passphrase.length < 8) {
|
|
1209
|
-
return
|
|
1271
|
+
return badRequest("Passphrase required (min 8 characters)");
|
|
1210
1272
|
}
|
|
1211
1273
|
try {
|
|
1212
1274
|
const result = await _vaultTransfer.exportVault(body.passphrase);
|
|
@@ -1220,16 +1282,16 @@ app.post("/api/vault/export", async (c) => {
|
|
|
1220
1282
|
app.post("/api/vault/import", async (c) => {
|
|
1221
1283
|
const sessionId = c.req.query("sessionId");
|
|
1222
1284
|
if (!sessionId)
|
|
1223
|
-
return
|
|
1285
|
+
return badRequest("sessionId required");
|
|
1224
1286
|
const session = validateSession(sessionId);
|
|
1225
1287
|
if (!session)
|
|
1226
|
-
return
|
|
1288
|
+
return unauthorized("Invalid or expired session");
|
|
1227
1289
|
const key = sessionKeys.get(sessionId);
|
|
1228
1290
|
if (!key)
|
|
1229
|
-
return
|
|
1291
|
+
return unauthorized("Session key not found");
|
|
1230
1292
|
const body = await c.req.json();
|
|
1231
1293
|
if (!body.filePath || !body.passphrase) {
|
|
1232
|
-
return
|
|
1294
|
+
return badRequest("filePath and passphrase required");
|
|
1233
1295
|
}
|
|
1234
1296
|
const strategy = body.strategy ?? "skip";
|
|
1235
1297
|
try {
|
|
@@ -1237,27 +1299,27 @@ app.post("/api/vault/import", async (c) => {
|
|
|
1237
1299
|
return c.json({ ok: true, stats: result.stats });
|
|
1238
1300
|
}
|
|
1239
1301
|
catch (e) {
|
|
1240
|
-
return
|
|
1302
|
+
return badRequest(e.message);
|
|
1241
1303
|
}
|
|
1242
1304
|
});
|
|
1243
1305
|
// Verify a vault export file without importing
|
|
1244
1306
|
app.post("/api/vault/verify-export", async (c) => {
|
|
1245
1307
|
const sessionId = c.req.query("sessionId");
|
|
1246
1308
|
if (!sessionId)
|
|
1247
|
-
return
|
|
1309
|
+
return badRequest("sessionId required");
|
|
1248
1310
|
const session = validateSession(sessionId);
|
|
1249
1311
|
if (!session)
|
|
1250
|
-
return
|
|
1312
|
+
return unauthorized("Invalid or expired session");
|
|
1251
1313
|
const body = await c.req.json();
|
|
1252
1314
|
if (!body.filePath || !body.passphrase) {
|
|
1253
|
-
return
|
|
1315
|
+
return badRequest("filePath and passphrase required");
|
|
1254
1316
|
}
|
|
1255
1317
|
try {
|
|
1256
1318
|
const result = await _vaultTransfer.verifyExport(body.filePath, body.passphrase);
|
|
1257
1319
|
return c.json({ ok: true, message: result.message, stats: result.stats });
|
|
1258
1320
|
}
|
|
1259
1321
|
catch (e) {
|
|
1260
|
-
return
|
|
1322
|
+
return badRequest(e.message);
|
|
1261
1323
|
}
|
|
1262
1324
|
});
|
|
1263
1325
|
// --- Google OAuth2 routes ---
|
|
@@ -1325,16 +1387,16 @@ app.get("/api/google/status", async (c) => {
|
|
|
1325
1387
|
app.post("/api/google/send-email", async (c) => {
|
|
1326
1388
|
const sessionId = c.req.query("sessionId");
|
|
1327
1389
|
if (!sessionId)
|
|
1328
|
-
return
|
|
1390
|
+
return badRequest("sessionId required");
|
|
1329
1391
|
const session = validateSession(sessionId);
|
|
1330
1392
|
if (!session)
|
|
1331
|
-
return
|
|
1393
|
+
return unauthorized("Invalid or expired session");
|
|
1332
1394
|
if (!(_googleAuth?.isGoogleAuthenticated() ?? false)) {
|
|
1333
|
-
return
|
|
1395
|
+
return badRequest("Google not authenticated");
|
|
1334
1396
|
}
|
|
1335
1397
|
const body = await c.req.json();
|
|
1336
1398
|
if (!body.to || !body.subject || !body.body) {
|
|
1337
|
-
return
|
|
1399
|
+
return badRequest("to, subject, and body are required");
|
|
1338
1400
|
}
|
|
1339
1401
|
const result = await _googleGmailSend.sendEmail(body);
|
|
1340
1402
|
if (!result.ok)
|
|
@@ -1347,12 +1409,12 @@ app.post("/api/google/send-email", async (c) => {
|
|
|
1347
1409
|
app.get("/api/google/gmail/inbox-summary", async (c) => {
|
|
1348
1410
|
const sessionId = c.req.query("sessionId");
|
|
1349
1411
|
if (!sessionId)
|
|
1350
|
-
return
|
|
1412
|
+
return badRequest("sessionId required");
|
|
1351
1413
|
const session = validateSession(sessionId);
|
|
1352
1414
|
if (!session)
|
|
1353
|
-
return
|
|
1415
|
+
return unauthorized("Invalid or expired session");
|
|
1354
1416
|
if (!(_googleGmail?.isGmailAvailable() ?? false))
|
|
1355
|
-
return
|
|
1417
|
+
return badRequest("Google not authenticated");
|
|
1356
1418
|
const hours = parseInt(c.req.query("hours") ?? "24", 10);
|
|
1357
1419
|
const result = await _googleGmail.getInboxSummary(hours);
|
|
1358
1420
|
if (!result.ok)
|
|
@@ -1363,12 +1425,12 @@ app.get("/api/google/gmail/inbox-summary", async (c) => {
|
|
|
1363
1425
|
app.get("/api/google/gmail/categorize", async (c) => {
|
|
1364
1426
|
const sessionId = c.req.query("sessionId");
|
|
1365
1427
|
if (!sessionId)
|
|
1366
|
-
return
|
|
1428
|
+
return badRequest("sessionId required");
|
|
1367
1429
|
const session = validateSession(sessionId);
|
|
1368
1430
|
if (!session)
|
|
1369
|
-
return
|
|
1431
|
+
return unauthorized("Invalid or expired session");
|
|
1370
1432
|
if (!(_googleGmail?.isGmailAvailable() ?? false))
|
|
1371
|
-
return
|
|
1433
|
+
return badRequest("Google not authenticated");
|
|
1372
1434
|
const hours = parseInt(c.req.query("hours") ?? "24", 10);
|
|
1373
1435
|
const result = await _googleGmail.categorizeMessages(hours);
|
|
1374
1436
|
if (!result.ok)
|
|
@@ -1379,12 +1441,12 @@ app.get("/api/google/gmail/categorize", async (c) => {
|
|
|
1379
1441
|
app.get("/api/google/gmail/prioritize", async (c) => {
|
|
1380
1442
|
const sessionId = c.req.query("sessionId");
|
|
1381
1443
|
if (!sessionId)
|
|
1382
|
-
return
|
|
1444
|
+
return badRequest("sessionId required");
|
|
1383
1445
|
const session = validateSession(sessionId);
|
|
1384
1446
|
if (!session)
|
|
1385
|
-
return
|
|
1447
|
+
return unauthorized("Invalid or expired session");
|
|
1386
1448
|
if (!(_googleGmail?.isGmailAvailable() ?? false))
|
|
1387
|
-
return
|
|
1449
|
+
return badRequest("Google not authenticated");
|
|
1388
1450
|
const hours = parseInt(c.req.query("hours") ?? "24", 10);
|
|
1389
1451
|
const result = await _googleGmail.prioritizeInbox(hours);
|
|
1390
1452
|
if (!result.ok)
|
|
@@ -1395,12 +1457,12 @@ app.get("/api/google/gmail/prioritize", async (c) => {
|
|
|
1395
1457
|
app.post("/api/google/gmail/mark-read", async (c) => {
|
|
1396
1458
|
const sessionId = c.req.query("sessionId");
|
|
1397
1459
|
if (!sessionId)
|
|
1398
|
-
return
|
|
1460
|
+
return badRequest("sessionId required");
|
|
1399
1461
|
const session = validateSession(sessionId);
|
|
1400
1462
|
if (!session)
|
|
1401
|
-
return
|
|
1463
|
+
return unauthorized("Invalid or expired session");
|
|
1402
1464
|
if (!(_googleGmail?.isGmailAvailable() ?? false))
|
|
1403
|
-
return
|
|
1465
|
+
return badRequest("Google not authenticated");
|
|
1404
1466
|
const body = await c.req.json();
|
|
1405
1467
|
if (body.messageIds && body.messageIds.length > 0) {
|
|
1406
1468
|
const result = await _googleGmail.batchMarkAsRead(body.messageIds);
|
|
@@ -1409,7 +1471,7 @@ app.post("/api/google/gmail/mark-read", async (c) => {
|
|
|
1409
1471
|
return c.json(result);
|
|
1410
1472
|
}
|
|
1411
1473
|
if (!body.messageId)
|
|
1412
|
-
return
|
|
1474
|
+
return badRequest("messageId or messageIds required");
|
|
1413
1475
|
const result = await _googleGmail.markAsRead(body.messageId);
|
|
1414
1476
|
if (!result.ok)
|
|
1415
1477
|
return c.json({ error: result.message }, 500);
|
|
@@ -1419,12 +1481,12 @@ app.post("/api/google/gmail/mark-read", async (c) => {
|
|
|
1419
1481
|
app.post("/api/google/gmail/mark-unread", async (c) => {
|
|
1420
1482
|
const sessionId = c.req.query("sessionId");
|
|
1421
1483
|
if (!sessionId)
|
|
1422
|
-
return
|
|
1484
|
+
return badRequest("sessionId required");
|
|
1423
1485
|
const session = validateSession(sessionId);
|
|
1424
1486
|
if (!session)
|
|
1425
|
-
return
|
|
1487
|
+
return unauthorized("Invalid or expired session");
|
|
1426
1488
|
if (!(_googleGmail?.isGmailAvailable() ?? false))
|
|
1427
|
-
return
|
|
1489
|
+
return badRequest("Google not authenticated");
|
|
1428
1490
|
const body = await c.req.json();
|
|
1429
1491
|
if (body.messageIds && body.messageIds.length > 0) {
|
|
1430
1492
|
const result = await _googleGmail.batchMarkAsUnread(body.messageIds);
|
|
@@ -1433,7 +1495,7 @@ app.post("/api/google/gmail/mark-unread", async (c) => {
|
|
|
1433
1495
|
return c.json(result);
|
|
1434
1496
|
}
|
|
1435
1497
|
if (!body.messageId)
|
|
1436
|
-
return
|
|
1498
|
+
return badRequest("messageId or messageIds required");
|
|
1437
1499
|
const result = await _googleGmail.markAsUnread(body.messageId);
|
|
1438
1500
|
if (!result.ok)
|
|
1439
1501
|
return c.json({ error: result.message }, 500);
|
|
@@ -1443,7 +1505,7 @@ app.post("/api/google/gmail/mark-unread", async (c) => {
|
|
|
1443
1505
|
// Get today's schedule
|
|
1444
1506
|
app.get("/api/google/calendar/today", async (c) => {
|
|
1445
1507
|
if (!(_googleCalendar?.isCalendarAvailable() ?? false))
|
|
1446
|
-
return
|
|
1508
|
+
return badRequest("Google not authenticated");
|
|
1447
1509
|
const result = await _googleCalendar.getTodaySchedule();
|
|
1448
1510
|
if (!result.ok)
|
|
1449
1511
|
return c.json({ error: result.message }, 500);
|
|
@@ -1452,7 +1514,7 @@ app.get("/api/google/calendar/today", async (c) => {
|
|
|
1452
1514
|
// Get upcoming events
|
|
1453
1515
|
app.get("/api/google/calendar/upcoming", async (c) => {
|
|
1454
1516
|
if (!(_googleCalendar?.isCalendarAvailable() ?? false))
|
|
1455
|
-
return
|
|
1517
|
+
return badRequest("Google not authenticated");
|
|
1456
1518
|
const hours = parseInt(c.req.query("hours") ?? "4", 10);
|
|
1457
1519
|
const result = await _googleCalendar.getUpcomingEvents(hours);
|
|
1458
1520
|
if (!result.ok)
|
|
@@ -1462,10 +1524,10 @@ app.get("/api/google/calendar/upcoming", async (c) => {
|
|
|
1462
1524
|
// Get free/busy
|
|
1463
1525
|
app.post("/api/google/calendar/freebusy", async (c) => {
|
|
1464
1526
|
if (!(_googleCalendar?.isCalendarAvailable() ?? false))
|
|
1465
|
-
return
|
|
1527
|
+
return badRequest("Google not authenticated");
|
|
1466
1528
|
const body = await c.req.json();
|
|
1467
1529
|
if (!body.start || !body.end)
|
|
1468
|
-
return
|
|
1530
|
+
return badRequest("start and end are required");
|
|
1469
1531
|
const result = await _googleCalendar.getFreeBusy(body.start, body.end);
|
|
1470
1532
|
if (!result.ok)
|
|
1471
1533
|
return c.json({ error: result.message }, 500);
|
|
@@ -1474,10 +1536,10 @@ app.post("/api/google/calendar/freebusy", async (c) => {
|
|
|
1474
1536
|
// Create calendar event
|
|
1475
1537
|
app.post("/api/google/calendar/events", async (c) => {
|
|
1476
1538
|
if (!(_googleCalendar?.isCalendarAvailable() ?? false))
|
|
1477
|
-
return
|
|
1539
|
+
return badRequest("Google not authenticated");
|
|
1478
1540
|
const body = await c.req.json();
|
|
1479
1541
|
if (!body.title || !body.start || !body.end) {
|
|
1480
|
-
return
|
|
1542
|
+
return badRequest("title, start, and end are required");
|
|
1481
1543
|
}
|
|
1482
1544
|
// Temporal validation: catch day-of-week mismatches before creating events (ts_temporal_mismatch_01)
|
|
1483
1545
|
const temporalCheck = _googleTemporal.validateCalendarEntry(body.start, body.expectedDayOfWeek);
|
|
@@ -1503,7 +1565,7 @@ app.post("/api/google/calendar/events", async (c) => {
|
|
|
1503
1565
|
// List events with flexible filtering
|
|
1504
1566
|
app.get("/api/google/calendar/events", async (c) => {
|
|
1505
1567
|
if (!(_googleCalendar?.isCalendarAvailable() ?? false))
|
|
1506
|
-
return
|
|
1568
|
+
return badRequest("Google not authenticated");
|
|
1507
1569
|
const result = await _googleCalendar.listEvents({
|
|
1508
1570
|
timeMin: c.req.query("timeMin"),
|
|
1509
1571
|
timeMax: c.req.query("timeMax"),
|
|
@@ -1518,7 +1580,7 @@ app.get("/api/google/calendar/events", async (c) => {
|
|
|
1518
1580
|
// Update calendar event
|
|
1519
1581
|
app.patch("/api/google/calendar/events/:eventId", async (c) => {
|
|
1520
1582
|
if (!(_googleCalendar?.isCalendarAvailable() ?? false))
|
|
1521
|
-
return
|
|
1583
|
+
return badRequest("Google not authenticated");
|
|
1522
1584
|
const eventId = c.req.param("eventId");
|
|
1523
1585
|
const body = await c.req.json();
|
|
1524
1586
|
// Temporal validation on updated start date (ts_temporal_mismatch_01)
|
|
@@ -1541,7 +1603,7 @@ app.patch("/api/google/calendar/events/:eventId", async (c) => {
|
|
|
1541
1603
|
// Delete calendar event
|
|
1542
1604
|
app.delete("/api/google/calendar/events/:eventId", async (c) => {
|
|
1543
1605
|
if (!(_googleCalendar?.isCalendarAvailable() ?? false))
|
|
1544
|
-
return
|
|
1606
|
+
return badRequest("Google not authenticated");
|
|
1545
1607
|
const eventId = c.req.param("eventId");
|
|
1546
1608
|
const sendUpdates = c.req.query("sendUpdates");
|
|
1547
1609
|
const result = await _googleCalendar.deleteEvent(eventId, { sendUpdates });
|
|
@@ -1553,10 +1615,10 @@ app.delete("/api/google/calendar/events/:eventId", async (c) => {
|
|
|
1553
1615
|
// Search calendar events by text
|
|
1554
1616
|
app.get("/api/google/calendar/search", async (c) => {
|
|
1555
1617
|
if (!(_googleCalendar?.isCalendarAvailable() ?? false))
|
|
1556
|
-
return
|
|
1618
|
+
return badRequest("Google not authenticated");
|
|
1557
1619
|
const query = c.req.query("q");
|
|
1558
1620
|
if (!query)
|
|
1559
|
-
return
|
|
1621
|
+
return badRequest("q (search query) is required");
|
|
1560
1622
|
const result = await _googleCalendar.searchEvents(query, {
|
|
1561
1623
|
timeMin: c.req.query("timeMin"),
|
|
1562
1624
|
timeMax: c.req.query("timeMax"),
|
|
@@ -1571,12 +1633,12 @@ app.get("/api/google/calendar/search", async (c) => {
|
|
|
1571
1633
|
app.get("/api/google/tasks/lists", async (c) => {
|
|
1572
1634
|
const sessionId = c.req.query("sessionId");
|
|
1573
1635
|
if (!sessionId)
|
|
1574
|
-
return
|
|
1636
|
+
return badRequest("sessionId required");
|
|
1575
1637
|
const session = validateSession(sessionId);
|
|
1576
1638
|
if (!session)
|
|
1577
|
-
return
|
|
1639
|
+
return unauthorized("Invalid or expired session");
|
|
1578
1640
|
if (!(_googleTasks?.isTasksAvailable() ?? false))
|
|
1579
|
-
return
|
|
1641
|
+
return badRequest("Google not authenticated");
|
|
1580
1642
|
const result = await _googleTasks.listTaskLists();
|
|
1581
1643
|
if (!result.ok)
|
|
1582
1644
|
return c.json({ error: result.message }, 500);
|
|
@@ -1586,15 +1648,15 @@ app.get("/api/google/tasks/lists", async (c) => {
|
|
|
1586
1648
|
app.post("/api/google/tasks/lists", async (c) => {
|
|
1587
1649
|
const sessionId = c.req.query("sessionId");
|
|
1588
1650
|
if (!sessionId)
|
|
1589
|
-
return
|
|
1651
|
+
return badRequest("sessionId required");
|
|
1590
1652
|
const session = validateSession(sessionId);
|
|
1591
1653
|
if (!session)
|
|
1592
|
-
return
|
|
1654
|
+
return unauthorized("Invalid or expired session");
|
|
1593
1655
|
if (!(_googleTasks?.isTasksAvailable() ?? false))
|
|
1594
|
-
return
|
|
1656
|
+
return badRequest("Google not authenticated");
|
|
1595
1657
|
const body = await c.req.json();
|
|
1596
1658
|
if (!body.title)
|
|
1597
|
-
return
|
|
1659
|
+
return badRequest("title is required");
|
|
1598
1660
|
const result = await _googleTasks.createTaskList(body.title);
|
|
1599
1661
|
if (!result.ok)
|
|
1600
1662
|
return c.json({ error: result.message }, 500);
|
|
@@ -1605,16 +1667,16 @@ app.post("/api/google/tasks/lists", async (c) => {
|
|
|
1605
1667
|
app.patch("/api/google/tasks/lists/:listId", async (c) => {
|
|
1606
1668
|
const sessionId = c.req.query("sessionId");
|
|
1607
1669
|
if (!sessionId)
|
|
1608
|
-
return
|
|
1670
|
+
return badRequest("sessionId required");
|
|
1609
1671
|
const session = validateSession(sessionId);
|
|
1610
1672
|
if (!session)
|
|
1611
|
-
return
|
|
1673
|
+
return unauthorized("Invalid or expired session");
|
|
1612
1674
|
if (!(_googleTasks?.isTasksAvailable() ?? false))
|
|
1613
|
-
return
|
|
1675
|
+
return badRequest("Google not authenticated");
|
|
1614
1676
|
const listId = c.req.param("listId");
|
|
1615
1677
|
const body = await c.req.json();
|
|
1616
1678
|
if (!body.title)
|
|
1617
|
-
return
|
|
1679
|
+
return badRequest("title is required");
|
|
1618
1680
|
const result = await _googleTasks.updateTaskList(listId, body.title);
|
|
1619
1681
|
if (!result.ok)
|
|
1620
1682
|
return c.json({ error: result.message }, 500);
|
|
@@ -1624,12 +1686,12 @@ app.patch("/api/google/tasks/lists/:listId", async (c) => {
|
|
|
1624
1686
|
app.delete("/api/google/tasks/lists/:listId", async (c) => {
|
|
1625
1687
|
const sessionId = c.req.query("sessionId");
|
|
1626
1688
|
if (!sessionId)
|
|
1627
|
-
return
|
|
1689
|
+
return badRequest("sessionId required");
|
|
1628
1690
|
const session = validateSession(sessionId);
|
|
1629
1691
|
if (!session)
|
|
1630
|
-
return
|
|
1692
|
+
return unauthorized("Invalid or expired session");
|
|
1631
1693
|
if (!(_googleTasks?.isTasksAvailable() ?? false))
|
|
1632
|
-
return
|
|
1694
|
+
return badRequest("Google not authenticated");
|
|
1633
1695
|
const listId = c.req.param("listId");
|
|
1634
1696
|
const result = await _googleTasks.deleteTaskList(listId);
|
|
1635
1697
|
if (!result.ok)
|
|
@@ -1641,19 +1703,19 @@ app.delete("/api/google/tasks/lists/:listId", async (c) => {
|
|
|
1641
1703
|
app.post("/api/google/tasks/recurring", async (c) => {
|
|
1642
1704
|
const sessionId = c.req.query("sessionId");
|
|
1643
1705
|
if (!sessionId)
|
|
1644
|
-
return
|
|
1706
|
+
return badRequest("sessionId required");
|
|
1645
1707
|
const session = validateSession(sessionId);
|
|
1646
1708
|
if (!session)
|
|
1647
|
-
return
|
|
1709
|
+
return unauthorized("Invalid or expired session");
|
|
1648
1710
|
if (!(_googleTasks?.isTasksAvailable() ?? false))
|
|
1649
|
-
return
|
|
1711
|
+
return badRequest("Google not authenticated");
|
|
1650
1712
|
const body = await c.req.json();
|
|
1651
1713
|
if (!body.title)
|
|
1652
|
-
return
|
|
1714
|
+
return badRequest("title is required");
|
|
1653
1715
|
if (body.dayOfWeek === undefined)
|
|
1654
|
-
return
|
|
1716
|
+
return badRequest("dayOfWeek is required (0=Sun, 6=Sat)");
|
|
1655
1717
|
if (body.hour === undefined)
|
|
1656
|
-
return
|
|
1718
|
+
return badRequest("hour is required (0-23)");
|
|
1657
1719
|
// Temporal validation: cross-check dayOfWeek number against expectedDayName (ts_temporal_mismatch_01)
|
|
1658
1720
|
if (body.expectedDayName) {
|
|
1659
1721
|
const dayNames = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
|
|
@@ -1679,12 +1741,12 @@ app.post("/api/google/tasks/recurring", async (c) => {
|
|
|
1679
1741
|
app.get("/api/google/tasks/:listId", async (c) => {
|
|
1680
1742
|
const sessionId = c.req.query("sessionId");
|
|
1681
1743
|
if (!sessionId)
|
|
1682
|
-
return
|
|
1744
|
+
return badRequest("sessionId required");
|
|
1683
1745
|
const session = validateSession(sessionId);
|
|
1684
1746
|
if (!session)
|
|
1685
|
-
return
|
|
1747
|
+
return unauthorized("Invalid or expired session");
|
|
1686
1748
|
if (!(_googleTasks?.isTasksAvailable() ?? false))
|
|
1687
|
-
return
|
|
1749
|
+
return badRequest("Google not authenticated");
|
|
1688
1750
|
const listId = c.req.param("listId");
|
|
1689
1751
|
const showCompleted = c.req.query("showCompleted") === "true";
|
|
1690
1752
|
const dueMin = c.req.query("dueMin");
|
|
@@ -1702,12 +1764,12 @@ app.get("/api/google/tasks/:listId", async (c) => {
|
|
|
1702
1764
|
app.get("/api/google/tasks/:listId/:taskId", async (c) => {
|
|
1703
1765
|
const sessionId = c.req.query("sessionId");
|
|
1704
1766
|
if (!sessionId)
|
|
1705
|
-
return
|
|
1767
|
+
return badRequest("sessionId required");
|
|
1706
1768
|
const session = validateSession(sessionId);
|
|
1707
1769
|
if (!session)
|
|
1708
|
-
return
|
|
1770
|
+
return unauthorized("Invalid or expired session");
|
|
1709
1771
|
if (!(_googleTasks?.isTasksAvailable() ?? false))
|
|
1710
|
-
return
|
|
1772
|
+
return badRequest("Google not authenticated");
|
|
1711
1773
|
const listId = c.req.param("listId");
|
|
1712
1774
|
const taskId = c.req.param("taskId");
|
|
1713
1775
|
const result = await _googleTasks.getTask(listId, taskId);
|
|
@@ -1719,16 +1781,16 @@ app.get("/api/google/tasks/:listId/:taskId", async (c) => {
|
|
|
1719
1781
|
app.post("/api/google/tasks/:listId", async (c) => {
|
|
1720
1782
|
const sessionId = c.req.query("sessionId");
|
|
1721
1783
|
if (!sessionId)
|
|
1722
|
-
return
|
|
1784
|
+
return badRequest("sessionId required");
|
|
1723
1785
|
const session = validateSession(sessionId);
|
|
1724
1786
|
if (!session)
|
|
1725
|
-
return
|
|
1787
|
+
return unauthorized("Invalid or expired session");
|
|
1726
1788
|
if (!(_googleTasks?.isTasksAvailable() ?? false))
|
|
1727
|
-
return
|
|
1789
|
+
return badRequest("Google not authenticated");
|
|
1728
1790
|
const listId = c.req.param("listId");
|
|
1729
1791
|
const body = await c.req.json();
|
|
1730
1792
|
if (!body.title)
|
|
1731
|
-
return
|
|
1793
|
+
return badRequest("title is required");
|
|
1732
1794
|
const result = await _googleTasks.createTask(listId, body);
|
|
1733
1795
|
if (!result.ok)
|
|
1734
1796
|
return c.json({ error: result.message }, 500);
|
|
@@ -1739,12 +1801,12 @@ app.post("/api/google/tasks/:listId", async (c) => {
|
|
|
1739
1801
|
app.patch("/api/google/tasks/:listId/:taskId", async (c) => {
|
|
1740
1802
|
const sessionId = c.req.query("sessionId");
|
|
1741
1803
|
if (!sessionId)
|
|
1742
|
-
return
|
|
1804
|
+
return badRequest("sessionId required");
|
|
1743
1805
|
const session = validateSession(sessionId);
|
|
1744
1806
|
if (!session)
|
|
1745
|
-
return
|
|
1807
|
+
return unauthorized("Invalid or expired session");
|
|
1746
1808
|
if (!(_googleTasks?.isTasksAvailable() ?? false))
|
|
1747
|
-
return
|
|
1809
|
+
return badRequest("Google not authenticated");
|
|
1748
1810
|
const listId = c.req.param("listId");
|
|
1749
1811
|
const taskId = c.req.param("taskId");
|
|
1750
1812
|
const body = await c.req.json();
|
|
@@ -1758,12 +1820,12 @@ app.patch("/api/google/tasks/:listId/:taskId", async (c) => {
|
|
|
1758
1820
|
app.post("/api/google/tasks/:listId/:taskId/complete", async (c) => {
|
|
1759
1821
|
const sessionId = c.req.query("sessionId");
|
|
1760
1822
|
if (!sessionId)
|
|
1761
|
-
return
|
|
1823
|
+
return badRequest("sessionId required");
|
|
1762
1824
|
const session = validateSession(sessionId);
|
|
1763
1825
|
if (!session)
|
|
1764
|
-
return
|
|
1826
|
+
return unauthorized("Invalid or expired session");
|
|
1765
1827
|
if (!(_googleTasks?.isTasksAvailable() ?? false))
|
|
1766
|
-
return
|
|
1828
|
+
return badRequest("Google not authenticated");
|
|
1767
1829
|
const listId = c.req.param("listId");
|
|
1768
1830
|
const taskId = c.req.param("taskId");
|
|
1769
1831
|
const result = await _googleTasks.completeTask(listId, taskId);
|
|
@@ -1776,12 +1838,12 @@ app.post("/api/google/tasks/:listId/:taskId/complete", async (c) => {
|
|
|
1776
1838
|
app.post("/api/google/tasks/:listId/:taskId/uncomplete", async (c) => {
|
|
1777
1839
|
const sessionId = c.req.query("sessionId");
|
|
1778
1840
|
if (!sessionId)
|
|
1779
|
-
return
|
|
1841
|
+
return badRequest("sessionId required");
|
|
1780
1842
|
const session = validateSession(sessionId);
|
|
1781
1843
|
if (!session)
|
|
1782
|
-
return
|
|
1844
|
+
return unauthorized("Invalid or expired session");
|
|
1783
1845
|
if (!(_googleTasks?.isTasksAvailable() ?? false))
|
|
1784
|
-
return
|
|
1846
|
+
return badRequest("Google not authenticated");
|
|
1785
1847
|
const listId = c.req.param("listId");
|
|
1786
1848
|
const taskId = c.req.param("taskId");
|
|
1787
1849
|
const result = await _googleTasks.uncompleteTask(listId, taskId);
|
|
@@ -1793,12 +1855,12 @@ app.post("/api/google/tasks/:listId/:taskId/uncomplete", async (c) => {
|
|
|
1793
1855
|
app.delete("/api/google/tasks/:listId/:taskId", async (c) => {
|
|
1794
1856
|
const sessionId = c.req.query("sessionId");
|
|
1795
1857
|
if (!sessionId)
|
|
1796
|
-
return
|
|
1858
|
+
return badRequest("sessionId required");
|
|
1797
1859
|
const session = validateSession(sessionId);
|
|
1798
1860
|
if (!session)
|
|
1799
|
-
return
|
|
1861
|
+
return unauthorized("Invalid or expired session");
|
|
1800
1862
|
if (!(_googleTasks?.isTasksAvailable() ?? false))
|
|
1801
|
-
return
|
|
1863
|
+
return badRequest("Google not authenticated");
|
|
1802
1864
|
const listId = c.req.param("listId");
|
|
1803
1865
|
const taskId = c.req.param("taskId");
|
|
1804
1866
|
const result = await _googleTasks.deleteTask(listId, taskId);
|
|
@@ -1812,10 +1874,10 @@ app.delete("/api/google/tasks/:listId/:taskId", async (c) => {
|
|
|
1812
1874
|
app.get("/api/prompt", async (c) => {
|
|
1813
1875
|
const sessionId = c.req.query("sessionId");
|
|
1814
1876
|
if (!sessionId)
|
|
1815
|
-
return
|
|
1877
|
+
return badRequest("sessionId required");
|
|
1816
1878
|
const session = validateSession(sessionId);
|
|
1817
1879
|
if (!session)
|
|
1818
|
-
return
|
|
1880
|
+
return unauthorized("Invalid or expired session");
|
|
1819
1881
|
let prompt = "";
|
|
1820
1882
|
try {
|
|
1821
1883
|
prompt = await readBrainFile(PERSONALITY_PATH);
|
|
@@ -1828,10 +1890,10 @@ app.put("/api/prompt", async (c) => {
|
|
|
1828
1890
|
const body = await c.req.json();
|
|
1829
1891
|
const { sessionId, prompt } = body;
|
|
1830
1892
|
if (!sessionId)
|
|
1831
|
-
return
|
|
1893
|
+
return badRequest("sessionId required");
|
|
1832
1894
|
const session = validateSession(sessionId);
|
|
1833
1895
|
if (!session)
|
|
1834
|
-
return
|
|
1896
|
+
return unauthorized("Invalid or expired session");
|
|
1835
1897
|
await mkdir(join(BRAIN_DIR, "identity"), { recursive: true });
|
|
1836
1898
|
await writeBrainFile(PERSONALITY_PATH, prompt ?? "");
|
|
1837
1899
|
return c.json({ ok: true });
|
|
@@ -1839,32 +1901,47 @@ app.put("/api/prompt", async (c) => {
|
|
|
1839
1901
|
// --- Model discovery ---
|
|
1840
1902
|
app.get("/api/models", async (c) => {
|
|
1841
1903
|
const ollamaUrl = process.env.OLLAMA_URL ?? "http://localhost:11434";
|
|
1904
|
+
const results = [];
|
|
1905
|
+
// Fetch local Ollama models
|
|
1842
1906
|
try {
|
|
1843
1907
|
const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(3000) });
|
|
1844
|
-
if (
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
modified: m.modified_at,
|
|
1851
|
-
}));
|
|
1852
|
-
return c.json({ models });
|
|
1908
|
+
if (res.ok) {
|
|
1909
|
+
const data = await res.json();
|
|
1910
|
+
for (const m of data.models ?? []) {
|
|
1911
|
+
results.push({ name: m.name, size: m.size, modified: m.modified_at, source: "ollama" });
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1853
1914
|
}
|
|
1854
|
-
catch {
|
|
1855
|
-
|
|
1915
|
+
catch { /* Ollama not reachable */ }
|
|
1916
|
+
// Fetch OpenRouter models if API key is set
|
|
1917
|
+
const orKey = process.env.OPENROUTER_API_KEY;
|
|
1918
|
+
if (orKey) {
|
|
1919
|
+
try {
|
|
1920
|
+
const res = await fetch("https://openrouter.ai/api/v1/models", {
|
|
1921
|
+
headers: { Authorization: `Bearer ${orKey}` },
|
|
1922
|
+
signal: AbortSignal.timeout(5000),
|
|
1923
|
+
});
|
|
1924
|
+
if (res.ok) {
|
|
1925
|
+
const data = await res.json();
|
|
1926
|
+
for (const m of data.data ?? []) {
|
|
1927
|
+
results.push({ name: m.id, source: "openrouter" });
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
catch { /* OpenRouter not reachable */ }
|
|
1856
1932
|
}
|
|
1933
|
+
return c.json({ models: results });
|
|
1857
1934
|
});
|
|
1858
1935
|
// --- Sensitivity trainer ---
|
|
1859
1936
|
app.post("/api/sensitive/flag", async (c) => {
|
|
1860
1937
|
const body = await c.req.json().catch(() => null);
|
|
1861
1938
|
if (!body?.value || typeof body.value !== "string") {
|
|
1862
|
-
return
|
|
1939
|
+
return badRequest("value required");
|
|
1863
1940
|
}
|
|
1864
1941
|
const category = (body.category || "FLAGGED").toUpperCase();
|
|
1865
1942
|
const value = body.value.trim();
|
|
1866
1943
|
if (value.length < 2) {
|
|
1867
|
-
return
|
|
1944
|
+
return badRequest("value too short");
|
|
1868
1945
|
}
|
|
1869
1946
|
if (!activeSensitiveRegistry) {
|
|
1870
1947
|
return c.json({ error: "registry not initialized" }, 503);
|
|
@@ -1984,7 +2061,7 @@ app.get("/api/voice-status", async (c) => {
|
|
|
1984
2061
|
app.get("/api/tts", async (c) => {
|
|
1985
2062
|
const text = c.req.query("text");
|
|
1986
2063
|
if (!text)
|
|
1987
|
-
return
|
|
2064
|
+
return badRequest("text query param required");
|
|
1988
2065
|
if (!(_ttsClient?.isTtsAvailable() ?? false))
|
|
1989
2066
|
return c.json({ error: "TTS not available" }, 503);
|
|
1990
2067
|
const wav = await _ttsClient.synthesize(text);
|
|
@@ -2003,7 +2080,7 @@ app.post("/api/stt", async (c) => {
|
|
|
2003
2080
|
return c.json({ error: "STT not available" }, 503);
|
|
2004
2081
|
const body = await c.req.arrayBuffer();
|
|
2005
2082
|
if (!body || body.byteLength === 0)
|
|
2006
|
-
return
|
|
2083
|
+
return badRequest("Audio body required");
|
|
2007
2084
|
const text = await _sttClient.transcribe(Buffer.from(body));
|
|
2008
2085
|
if (!text)
|
|
2009
2086
|
return c.json({ error: "Transcription failed" }, 502);
|
|
@@ -2036,7 +2113,7 @@ app.get("/api/avatar/video/:hash", async (c) => {
|
|
|
2036
2113
|
const hash = c.req.param("hash");
|
|
2037
2114
|
// Sanitize: only allow alphanumeric + .mp4
|
|
2038
2115
|
if (!/^[a-f0-9]+\.mp4$/.test(hash)) {
|
|
2039
|
-
return
|
|
2116
|
+
return badRequest("Invalid hash");
|
|
2040
2117
|
}
|
|
2041
2118
|
const filePath = join(UI_DIR, "avatar", "cache", hash);
|
|
2042
2119
|
try {
|
|
@@ -2050,22 +2127,22 @@ app.get("/api/avatar/video/:hash", async (c) => {
|
|
|
2050
2127
|
});
|
|
2051
2128
|
}
|
|
2052
2129
|
catch {
|
|
2053
|
-
return
|
|
2130
|
+
return notFound();
|
|
2054
2131
|
}
|
|
2055
2132
|
});
|
|
2056
2133
|
// Upload a new reference photo, re-prepare, clear cache
|
|
2057
2134
|
app.post("/api/avatar/photo", async (c) => {
|
|
2058
2135
|
const sessionId = c.req.query("sessionId");
|
|
2059
2136
|
if (!sessionId)
|
|
2060
|
-
return
|
|
2137
|
+
return badRequest("sessionId required");
|
|
2061
2138
|
const session = validateSession(sessionId);
|
|
2062
2139
|
if (!session)
|
|
2063
|
-
return
|
|
2140
|
+
return unauthorized("Invalid or expired session");
|
|
2064
2141
|
if (!(_avatarSidecar?.isAvatarAvailable() ?? false))
|
|
2065
2142
|
return c.json({ error: "Avatar not available" }, 503);
|
|
2066
2143
|
const body = await c.req.arrayBuffer();
|
|
2067
2144
|
if (!body || body.byteLength === 0)
|
|
2068
|
-
return
|
|
2145
|
+
return badRequest("Photo body required");
|
|
2069
2146
|
const avatarConfig = _settingsVoice ? _settingsVoice.getAvatarConfig() : { photoPath: "public/avatar/photo.png", port: 0, enabled: false };
|
|
2070
2147
|
const photoPath = join(process.cwd(), avatarConfig.photoPath);
|
|
2071
2148
|
await mkdir(join(UI_DIR, "avatar"), { recursive: true });
|
|
@@ -2084,7 +2161,7 @@ app.post("/api/extract", async (c) => {
|
|
|
2084
2161
|
const formData = await c.req.formData();
|
|
2085
2162
|
const file = formData.get("file");
|
|
2086
2163
|
if (!file)
|
|
2087
|
-
return
|
|
2164
|
+
return badRequest("No file provided");
|
|
2088
2165
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
2089
2166
|
const name = file.name.toLowerCase();
|
|
2090
2167
|
let text = "";
|
|
@@ -2116,16 +2193,16 @@ app.post("/api/extract", async (c) => {
|
|
|
2116
2193
|
app.get("/api/history", async (c) => {
|
|
2117
2194
|
const sessionId = c.req.query("sessionId");
|
|
2118
2195
|
if (!sessionId)
|
|
2119
|
-
return
|
|
2196
|
+
return badRequest("sessionId required");
|
|
2120
2197
|
const session = validateSession(sessionId);
|
|
2121
2198
|
if (!session)
|
|
2122
|
-
return
|
|
2199
|
+
return unauthorized("Invalid or expired session");
|
|
2123
2200
|
const cs = await getOrCreateChatSession(sessionId, session.name);
|
|
2124
2201
|
// Always return main history (not active thread's)
|
|
2125
2202
|
const historySource = cs.activeThreadId ? cs.mainHistory : cs.history;
|
|
2126
2203
|
const messages = historySource
|
|
2127
2204
|
.filter((m) => m.role === "user" || m.role === "assistant")
|
|
2128
|
-
.map((m) => ({ role: m.role, content: m.content }));
|
|
2205
|
+
.map((m) => ({ role: m.role, content: m.content, ...(m.toolsUsed ? { toolsUsed: m.toolsUsed } : {}), ...(m.agentsUsed ? { agentsUsed: m.agentsUsed } : {}) }));
|
|
2129
2206
|
return c.json({ messages });
|
|
2130
2207
|
});
|
|
2131
2208
|
// Persist intro message so it appears in all tabs/devices
|
|
@@ -2133,10 +2210,10 @@ app.post("/api/history/intro", async (c) => {
|
|
|
2133
2210
|
const body = await c.req.json();
|
|
2134
2211
|
const { sessionId, message } = body;
|
|
2135
2212
|
if (!sessionId || !message)
|
|
2136
|
-
return
|
|
2213
|
+
return badRequest("sessionId and message required");
|
|
2137
2214
|
const session = validateSession(sessionId);
|
|
2138
2215
|
if (!session)
|
|
2139
|
-
return
|
|
2216
|
+
return unauthorized("Invalid session");
|
|
2140
2217
|
const cs = await getOrCreateChatSession(sessionId, session.name);
|
|
2141
2218
|
// Only add if history is empty (first run)
|
|
2142
2219
|
if (cs.history.length === 0) {
|
|
@@ -2148,10 +2225,10 @@ app.post("/api/history/intro", async (c) => {
|
|
|
2148
2225
|
app.get("/api/threads", async (c) => {
|
|
2149
2226
|
const sessionId = c.req.query("sessionId");
|
|
2150
2227
|
if (!sessionId)
|
|
2151
|
-
return
|
|
2228
|
+
return badRequest("sessionId required");
|
|
2152
2229
|
const session = validateSession(sessionId);
|
|
2153
2230
|
if (!session)
|
|
2154
|
-
return
|
|
2231
|
+
return unauthorized("Invalid session");
|
|
2155
2232
|
const threads = getThreadsForSession(sessionId);
|
|
2156
2233
|
const list = [...threads.values()]
|
|
2157
2234
|
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
|
|
@@ -2162,10 +2239,10 @@ app.post("/api/threads", async (c) => {
|
|
|
2162
2239
|
const body = await c.req.json();
|
|
2163
2240
|
const sessionId = body.sessionId;
|
|
2164
2241
|
if (!sessionId)
|
|
2165
|
-
return
|
|
2242
|
+
return badRequest("sessionId required");
|
|
2166
2243
|
const session = validateSession(sessionId);
|
|
2167
2244
|
if (!session)
|
|
2168
|
-
return
|
|
2245
|
+
return unauthorized("Invalid session");
|
|
2169
2246
|
const threads = getThreadsForSession(sessionId);
|
|
2170
2247
|
const now = new Date().toISOString();
|
|
2171
2248
|
const thread = {
|
|
@@ -2177,8 +2254,7 @@ app.post("/api/threads", async (c) => {
|
|
|
2177
2254
|
updatedAt: now,
|
|
2178
2255
|
};
|
|
2179
2256
|
threads.set(thread.id, thread);
|
|
2180
|
-
//
|
|
2181
|
-
// If currently on a thread, save it back first.
|
|
2257
|
+
// Save current thread's history before creating new one
|
|
2182
2258
|
const cs = chatSessions.get(sessionId);
|
|
2183
2259
|
if (cs && cs.activeThreadId) {
|
|
2184
2260
|
const prevThread = threads.get(cs.activeThreadId);
|
|
@@ -2186,44 +2262,41 @@ app.post("/api/threads", async (c) => {
|
|
|
2186
2262
|
prevThread.history = cs.history;
|
|
2187
2263
|
prevThread.historySummary = cs.historySummary;
|
|
2188
2264
|
}
|
|
2189
|
-
cs.history = cs.mainHistory;
|
|
2190
|
-
cs.historySummary = cs.mainHistorySummary;
|
|
2191
|
-
cs.activeThreadId = null;
|
|
2192
2265
|
}
|
|
2193
2266
|
return c.json({ thread: { id: thread.id, title: thread.title, createdAt: thread.createdAt, updatedAt: thread.updatedAt } });
|
|
2194
2267
|
});
|
|
2195
2268
|
app.get("/api/threads/:id/history", async (c) => {
|
|
2196
2269
|
const sessionId = c.req.query("sessionId");
|
|
2197
2270
|
if (!sessionId)
|
|
2198
|
-
return
|
|
2271
|
+
return badRequest("sessionId required");
|
|
2199
2272
|
const session = validateSession(sessionId);
|
|
2200
2273
|
if (!session)
|
|
2201
|
-
return
|
|
2274
|
+
return unauthorized("Invalid session");
|
|
2202
2275
|
const threadId = c.req.param("id");
|
|
2203
2276
|
const threads = getThreadsForSession(sessionId);
|
|
2204
2277
|
const thread = threads.get(threadId);
|
|
2205
2278
|
if (!thread)
|
|
2206
|
-
return
|
|
2279
|
+
return notFound("Thread not found");
|
|
2207
2280
|
// If this thread is currently active on cs, its live history is in cs.history
|
|
2208
2281
|
const cs = chatSessions.get(sessionId);
|
|
2209
2282
|
const historySource = (cs && cs.activeThreadId === threadId) ? cs.history : thread.history;
|
|
2210
2283
|
const messages = historySource
|
|
2211
2284
|
.filter((m) => m.role === "user" || m.role === "assistant")
|
|
2212
|
-
.map((m) => ({ role: m.role, content: m.content }));
|
|
2285
|
+
.map((m) => ({ role: m.role, content: m.content, ...(m.toolsUsed ? { toolsUsed: m.toolsUsed } : {}), ...(m.agentsUsed ? { agentsUsed: m.agentsUsed } : {}) }));
|
|
2213
2286
|
return c.json({ messages });
|
|
2214
2287
|
});
|
|
2215
2288
|
app.patch("/api/threads/:id", async (c) => {
|
|
2216
2289
|
const body = await c.req.json();
|
|
2217
2290
|
const sessionId = body.sessionId;
|
|
2218
2291
|
if (!sessionId)
|
|
2219
|
-
return
|
|
2292
|
+
return badRequest("sessionId required");
|
|
2220
2293
|
const session = validateSession(sessionId);
|
|
2221
2294
|
if (!session)
|
|
2222
|
-
return
|
|
2295
|
+
return unauthorized("Invalid session");
|
|
2223
2296
|
const threads = getThreadsForSession(sessionId);
|
|
2224
2297
|
const thread = threads.get(c.req.param("id"));
|
|
2225
2298
|
if (!thread)
|
|
2226
|
-
return
|
|
2299
|
+
return notFound("Thread not found");
|
|
2227
2300
|
if (body.title)
|
|
2228
2301
|
thread.title = body.title;
|
|
2229
2302
|
thread.updatedAt = new Date().toISOString();
|
|
@@ -2232,16 +2305,18 @@ app.patch("/api/threads/:id", async (c) => {
|
|
|
2232
2305
|
app.delete("/api/threads/:id", async (c) => {
|
|
2233
2306
|
const sessionId = c.req.query("sessionId");
|
|
2234
2307
|
if (!sessionId)
|
|
2235
|
-
return
|
|
2308
|
+
return badRequest("sessionId required");
|
|
2236
2309
|
const session = validateSession(sessionId);
|
|
2237
2310
|
if (!session)
|
|
2238
|
-
return
|
|
2311
|
+
return unauthorized("Invalid session");
|
|
2239
2312
|
const threadId = c.req.param("id");
|
|
2240
2313
|
const threads = getThreadsForSession(sessionId);
|
|
2241
|
-
// If deleting the active thread,
|
|
2314
|
+
// If deleting the active thread, clear it (frontend creates a new one)
|
|
2242
2315
|
const cs = chatSessions.get(sessionId);
|
|
2243
2316
|
if (cs && cs.activeThreadId === threadId) {
|
|
2244
|
-
|
|
2317
|
+
cs.history = [];
|
|
2318
|
+
cs.historySummary = "";
|
|
2319
|
+
cs.activeThreadId = null;
|
|
2245
2320
|
}
|
|
2246
2321
|
threads.delete(threadId);
|
|
2247
2322
|
return c.json({ ok: true });
|
|
@@ -2251,10 +2326,10 @@ app.delete("/api/threads/:id", async (c) => {
|
|
|
2251
2326
|
app.get("/api/activity/stream", async (c) => {
|
|
2252
2327
|
const sessionId = c.req.query("sessionId");
|
|
2253
2328
|
if (!sessionId)
|
|
2254
|
-
return
|
|
2329
|
+
return badRequest("sessionId required");
|
|
2255
2330
|
const session = validateSession(sessionId);
|
|
2256
2331
|
if (!session)
|
|
2257
|
-
return
|
|
2332
|
+
return unauthorized("Invalid or expired session");
|
|
2258
2333
|
const { onActivity } = await import("./activity/log.js");
|
|
2259
2334
|
return streamSSE(c, async (stream) => {
|
|
2260
2335
|
// Send heartbeat immediately
|
|
@@ -2279,10 +2354,10 @@ app.get("/api/activity/stream", async (c) => {
|
|
|
2279
2354
|
app.get("/api/activity", async (c) => {
|
|
2280
2355
|
const sessionId = c.req.query("sessionId");
|
|
2281
2356
|
if (!sessionId)
|
|
2282
|
-
return
|
|
2357
|
+
return badRequest("sessionId required");
|
|
2283
2358
|
const session = validateSession(sessionId);
|
|
2284
2359
|
if (!session)
|
|
2285
|
-
return
|
|
2360
|
+
return unauthorized("Invalid or expired session");
|
|
2286
2361
|
const since = parseInt(c.req.query("since") ?? "0", 10) || 0;
|
|
2287
2362
|
return c.json({ activities: await getActivities(since) });
|
|
2288
2363
|
});
|
|
@@ -2291,14 +2366,14 @@ app.post("/api/branch", async (c) => {
|
|
|
2291
2366
|
const body = await c.req.json();
|
|
2292
2367
|
const { sessionId, entryIds, question } = body;
|
|
2293
2368
|
if (!sessionId || !entryIds?.length || !question) {
|
|
2294
|
-
return
|
|
2369
|
+
return badRequest("sessionId, entryIds, and question required");
|
|
2295
2370
|
}
|
|
2296
2371
|
const session = validateSession(sessionId);
|
|
2297
2372
|
if (!session)
|
|
2298
|
-
return
|
|
2373
|
+
return unauthorized("Invalid or expired session");
|
|
2299
2374
|
const selected = await getActivitiesByIds(entryIds);
|
|
2300
2375
|
if (selected.length === 0) {
|
|
2301
|
-
return
|
|
2376
|
+
return notFound("No matching activity entries");
|
|
2302
2377
|
}
|
|
2303
2378
|
// Generate a trace for this branch, backreffing the first selected entry
|
|
2304
2379
|
const branchTraceId = generateTraceId();
|
|
@@ -2436,7 +2511,7 @@ app.post("/api/agents/tasks", async (c) => {
|
|
|
2436
2511
|
const body = await c.req.json();
|
|
2437
2512
|
const { label, prompt, cwd, origin, sessionId: sid, timeoutMs } = body;
|
|
2438
2513
|
if (!prompt)
|
|
2439
|
-
return
|
|
2514
|
+
return badRequest("prompt required");
|
|
2440
2515
|
try {
|
|
2441
2516
|
const task = await submitTask({
|
|
2442
2517
|
label: label || prompt.slice(0, 60),
|
|
@@ -2459,7 +2534,7 @@ app.get("/api/agents/tasks", async (c) => {
|
|
|
2459
2534
|
app.get("/api/agents/tasks/:id", async (c) => {
|
|
2460
2535
|
const task = await getTask(c.req.param("id"));
|
|
2461
2536
|
if (!task)
|
|
2462
|
-
return
|
|
2537
|
+
return notFound();
|
|
2463
2538
|
return c.json(task);
|
|
2464
2539
|
});
|
|
2465
2540
|
app.get("/api/agents/tasks/:id/output", async (c) => {
|
|
@@ -2469,7 +2544,7 @@ app.get("/api/agents/tasks/:id/output", async (c) => {
|
|
|
2469
2544
|
app.post("/api/agents/tasks/:id/cancel", async (c) => {
|
|
2470
2545
|
const ok = await cancelTask(c.req.param("id"));
|
|
2471
2546
|
if (!ok)
|
|
2472
|
-
return
|
|
2547
|
+
return notFound();
|
|
2473
2548
|
return c.json({ ok: true });
|
|
2474
2549
|
});
|
|
2475
2550
|
// --- File lock routes ---
|
|
@@ -2481,7 +2556,7 @@ app.post("/api/agents/locks/acquire", async (c) => {
|
|
|
2481
2556
|
const body = await c.req.json();
|
|
2482
2557
|
const { agentId, agentLabel, filePaths, timeoutMs } = body;
|
|
2483
2558
|
if (!agentId || !filePaths || !Array.isArray(filePaths)) {
|
|
2484
|
-
return
|
|
2559
|
+
return badRequest("agentId and filePaths[] required");
|
|
2485
2560
|
}
|
|
2486
2561
|
const result = await _agentLocks.acquireLocks(agentId, agentLabel || agentId, filePaths, timeoutMs);
|
|
2487
2562
|
return c.json(result, result.acquired ? 200 : 409);
|
|
@@ -2490,7 +2565,7 @@ app.post("/api/agents/locks/release", async (c) => {
|
|
|
2490
2565
|
const body = await c.req.json();
|
|
2491
2566
|
const { agentId, filePath } = body;
|
|
2492
2567
|
if (!agentId)
|
|
2493
|
-
return
|
|
2568
|
+
return badRequest("agentId required");
|
|
2494
2569
|
if (filePath) {
|
|
2495
2570
|
const ok = await _agentLocks.releaseFileLock(agentId, filePath);
|
|
2496
2571
|
return c.json({ released: ok ? 1 : 0 });
|
|
@@ -2502,7 +2577,7 @@ app.post("/api/agents/locks/force-release", async (c) => {
|
|
|
2502
2577
|
const body = await c.req.json();
|
|
2503
2578
|
const { filePath } = body;
|
|
2504
2579
|
if (!filePath)
|
|
2505
|
-
return
|
|
2580
|
+
return badRequest("filePath required");
|
|
2506
2581
|
const ok = await _agentLocks.forceReleaseLock(filePath);
|
|
2507
2582
|
return c.json({ released: ok });
|
|
2508
2583
|
});
|
|
@@ -2510,7 +2585,7 @@ app.post("/api/agents/locks/check", async (c) => {
|
|
|
2510
2585
|
const body = await c.req.json();
|
|
2511
2586
|
const { filePaths } = body;
|
|
2512
2587
|
if (!filePaths || !Array.isArray(filePaths)) {
|
|
2513
|
-
return
|
|
2588
|
+
return badRequest("filePaths[] required");
|
|
2514
2589
|
}
|
|
2515
2590
|
const conflicts = await _agentLocks.checkLocks(filePaths);
|
|
2516
2591
|
return c.json({ locked: conflicts.length > 0, conflicts });
|
|
@@ -2549,7 +2624,7 @@ app.get("/api/runtime/instances/:id", async (c) => {
|
|
|
2549
2624
|
return c.json({ error: "Runtime not initialized" }, 503);
|
|
2550
2625
|
const inst = rt.getInstance(c.req.param("id"));
|
|
2551
2626
|
if (!inst)
|
|
2552
|
-
return
|
|
2627
|
+
return notFound();
|
|
2553
2628
|
return c.json(inst);
|
|
2554
2629
|
});
|
|
2555
2630
|
app.post("/api/runtime/spawn", async (c) => {
|
|
@@ -2559,7 +2634,7 @@ app.post("/api/runtime/spawn", async (c) => {
|
|
|
2559
2634
|
const body = await c.req.json();
|
|
2560
2635
|
const { taskId, label, prompt, cwd, origin, parentId, tags, config, resources } = body;
|
|
2561
2636
|
if (!prompt)
|
|
2562
|
-
return
|
|
2637
|
+
return badRequest("prompt required");
|
|
2563
2638
|
try {
|
|
2564
2639
|
// Create the underlying AgentTask first
|
|
2565
2640
|
const { createTask } = await import("./agents/store.js");
|
|
@@ -2640,7 +2715,7 @@ app.post("/api/runtime/instances/:id/message", async (c) => {
|
|
|
2640
2715
|
const body = await c.req.json();
|
|
2641
2716
|
const { to, type, payload } = body;
|
|
2642
2717
|
if (!to || !type)
|
|
2643
|
-
return
|
|
2718
|
+
return badRequest("to and type required");
|
|
2644
2719
|
const msg = rt.sendMessage(c.req.param("id"), to, type, payload);
|
|
2645
2720
|
return c.json(msg);
|
|
2646
2721
|
});
|
|
@@ -2733,7 +2808,7 @@ app.get("/api/workflows/:id", async (c) => {
|
|
|
2733
2808
|
return c.json({ error: "Workflow engine not initialized" }, 503);
|
|
2734
2809
|
const wf = engine.getOrchestrator().getWorkflow(c.req.param("id"));
|
|
2735
2810
|
if (!wf)
|
|
2736
|
-
return
|
|
2811
|
+
return notFound("Workflow not found");
|
|
2737
2812
|
// Serialize Map<string, WorkflowTask> to plain object
|
|
2738
2813
|
const tasks = {};
|
|
2739
2814
|
for (const [key, task] of wf.tasks) {
|
|
@@ -2750,7 +2825,7 @@ app.get("/api/workflows/:id/results", async (c) => {
|
|
|
2750
2825
|
return c.json(result);
|
|
2751
2826
|
}
|
|
2752
2827
|
catch {
|
|
2753
|
-
return
|
|
2828
|
+
return notFound("Workflow not found");
|
|
2754
2829
|
}
|
|
2755
2830
|
});
|
|
2756
2831
|
// --- Tracing routes ---
|
|
@@ -2762,16 +2837,16 @@ app.get("/api/traces", async (c) => {
|
|
|
2762
2837
|
app.get("/api/traces/:traceId", async (c) => {
|
|
2763
2838
|
const detail = tracer.getTraceDetail(c.req.param("traceId"));
|
|
2764
2839
|
if (!detail)
|
|
2765
|
-
return
|
|
2840
|
+
return notFound("Trace not found");
|
|
2766
2841
|
return c.json(detail);
|
|
2767
2842
|
});
|
|
2768
2843
|
app.get("/api/traces/agent/:agentId", async (c) => {
|
|
2769
2844
|
const traceId = tracer.getAgentTraceId(c.req.param("agentId"));
|
|
2770
2845
|
if (!traceId)
|
|
2771
|
-
return
|
|
2846
|
+
return notFound("No trace for agent");
|
|
2772
2847
|
const detail = tracer.getTraceDetail(traceId);
|
|
2773
2848
|
if (!detail)
|
|
2774
|
-
return
|
|
2849
|
+
return notFound("Trace not found");
|
|
2775
2850
|
return c.json(detail);
|
|
2776
2851
|
});
|
|
2777
2852
|
// --- GitHub routes ---
|
|
@@ -2784,7 +2859,7 @@ app.post("/api/github/webhooks", async (c) => {
|
|
|
2784
2859
|
const secret = process.env.GITHUB_WEBHOOK_SECRET;
|
|
2785
2860
|
if (secret && signature) {
|
|
2786
2861
|
if (!_githubWebhooks.verifyWebhookSignature(rawBody, signature, secret)) {
|
|
2787
|
-
return
|
|
2862
|
+
return unauthorized("Invalid signature");
|
|
2788
2863
|
}
|
|
2789
2864
|
}
|
|
2790
2865
|
let payload;
|
|
@@ -2792,7 +2867,7 @@ app.post("/api/github/webhooks", async (c) => {
|
|
|
2792
2867
|
payload = JSON.parse(rawBody);
|
|
2793
2868
|
}
|
|
2794
2869
|
catch {
|
|
2795
|
-
return
|
|
2870
|
+
return badRequest("Invalid JSON");
|
|
2796
2871
|
}
|
|
2797
2872
|
const result = await _integrationsGithub.processWebhook(eventType, payload);
|
|
2798
2873
|
return c.json(result);
|
|
@@ -2806,14 +2881,14 @@ app.get("/api/github/status", async (c) => {
|
|
|
2806
2881
|
app.post("/api/github/pr/review", async (c) => {
|
|
2807
2882
|
const sessionId = c.req.query("sessionId");
|
|
2808
2883
|
if (!sessionId)
|
|
2809
|
-
return
|
|
2884
|
+
return badRequest("sessionId required");
|
|
2810
2885
|
const session = validateSession(sessionId);
|
|
2811
2886
|
if (!session)
|
|
2812
|
-
return
|
|
2887
|
+
return unauthorized("Invalid or expired session");
|
|
2813
2888
|
const body = await c.req.json();
|
|
2814
2889
|
const { prNumber, repo, postComment } = body;
|
|
2815
2890
|
if (!prNumber)
|
|
2816
|
-
return
|
|
2891
|
+
return badRequest("prNumber required");
|
|
2817
2892
|
const result = postComment
|
|
2818
2893
|
? await _integrationsGithub.reviewAndCommentPR(prNumber, repo)
|
|
2819
2894
|
: await _integrationsGithub.reviewPullRequest(prNumber, repo);
|
|
@@ -2825,14 +2900,14 @@ app.post("/api/github/pr/review", async (c) => {
|
|
|
2825
2900
|
app.post("/api/github/issues/triage", async (c) => {
|
|
2826
2901
|
const sessionId = c.req.query("sessionId");
|
|
2827
2902
|
if (!sessionId)
|
|
2828
|
-
return
|
|
2903
|
+
return badRequest("sessionId required");
|
|
2829
2904
|
const session = validateSession(sessionId);
|
|
2830
2905
|
if (!session)
|
|
2831
|
-
return
|
|
2906
|
+
return unauthorized("Invalid or expired session");
|
|
2832
2907
|
const body = await c.req.json();
|
|
2833
2908
|
const { issueNumber, repo, apply } = body;
|
|
2834
2909
|
if (!issueNumber)
|
|
2835
|
-
return
|
|
2910
|
+
return badRequest("issueNumber required");
|
|
2836
2911
|
const result = apply
|
|
2837
2912
|
? await _integrationsGithub.triageAndLabelIssue(issueNumber, repo)
|
|
2838
2913
|
: await _integrationsGithub.triageGitHubIssue(issueNumber, repo);
|
|
@@ -2844,10 +2919,10 @@ app.post("/api/github/issues/triage", async (c) => {
|
|
|
2844
2919
|
app.post("/api/github/issues/triage/batch", async (c) => {
|
|
2845
2920
|
const sessionId = c.req.query("sessionId");
|
|
2846
2921
|
if (!sessionId)
|
|
2847
|
-
return
|
|
2922
|
+
return badRequest("sessionId required");
|
|
2848
2923
|
const session = validateSession(sessionId);
|
|
2849
2924
|
if (!session)
|
|
2850
|
-
return
|
|
2925
|
+
return unauthorized("Invalid or expired session");
|
|
2851
2926
|
const body = await c.req.json();
|
|
2852
2927
|
const { repo, apply } = body;
|
|
2853
2928
|
const results = await _integrationsGithub.batchTriageIssues(repo, { apply });
|
|
@@ -2857,10 +2932,10 @@ app.post("/api/github/issues/triage/batch", async (c) => {
|
|
|
2857
2932
|
app.post("/api/github/commits/analyze", async (c) => {
|
|
2858
2933
|
const sessionId = c.req.query("sessionId");
|
|
2859
2934
|
if (!sessionId)
|
|
2860
|
-
return
|
|
2935
|
+
return badRequest("sessionId required");
|
|
2861
2936
|
const session = validateSession(sessionId);
|
|
2862
2937
|
if (!session)
|
|
2863
|
-
return
|
|
2938
|
+
return unauthorized("Invalid or expired session");
|
|
2864
2939
|
const body = await c.req.json();
|
|
2865
2940
|
const { sha, repo, count, since } = body;
|
|
2866
2941
|
if (sha) {
|
|
@@ -2958,16 +3033,16 @@ app.get("/api/slack/status", async (c) => {
|
|
|
2958
3033
|
app.post("/api/slack/send", async (c) => {
|
|
2959
3034
|
const sessionId = c.req.query("sessionId");
|
|
2960
3035
|
if (!sessionId)
|
|
2961
|
-
return
|
|
3036
|
+
return badRequest("sessionId required");
|
|
2962
3037
|
const session = validateSession(sessionId);
|
|
2963
3038
|
if (!session)
|
|
2964
|
-
return
|
|
3039
|
+
return unauthorized("Invalid or expired session");
|
|
2965
3040
|
const client = (_slackClient?.getClient() ?? null);
|
|
2966
3041
|
if (!client)
|
|
2967
3042
|
return c.json({ error: "Slack not available" }, 503);
|
|
2968
3043
|
const body = await c.req.json();
|
|
2969
3044
|
if (!body.channel || !body.text)
|
|
2970
|
-
return
|
|
3045
|
+
return badRequest("channel and text required");
|
|
2971
3046
|
const result = await client.sendMessage(body.channel, body.text, {
|
|
2972
3047
|
thread_ts: body.thread_ts,
|
|
2973
3048
|
blocks: body.blocks,
|
|
@@ -2981,16 +3056,16 @@ app.post("/api/slack/send", async (c) => {
|
|
|
2981
3056
|
app.post("/api/slack/dm", async (c) => {
|
|
2982
3057
|
const sessionId = c.req.query("sessionId");
|
|
2983
3058
|
if (!sessionId)
|
|
2984
|
-
return
|
|
3059
|
+
return badRequest("sessionId required");
|
|
2985
3060
|
const session = validateSession(sessionId);
|
|
2986
3061
|
if (!session)
|
|
2987
|
-
return
|
|
3062
|
+
return unauthorized("Invalid or expired session");
|
|
2988
3063
|
const client = (_slackClient?.getClient() ?? null);
|
|
2989
3064
|
if (!client)
|
|
2990
3065
|
return c.json({ error: "Slack not available" }, 503);
|
|
2991
3066
|
const body = await c.req.json();
|
|
2992
3067
|
if (!body.user || !body.text)
|
|
2993
|
-
return
|
|
3068
|
+
return badRequest("user and text required");
|
|
2994
3069
|
const result = await client.sendDm(body.user, body.text, { blocks: body.blocks });
|
|
2995
3070
|
if (!result.ok)
|
|
2996
3071
|
return c.json({ error: result.error }, 500);
|
|
@@ -3001,10 +3076,10 @@ app.post("/api/slack/dm", async (c) => {
|
|
|
3001
3076
|
app.get("/api/slack/channels", async (c) => {
|
|
3002
3077
|
const sessionId = c.req.query("sessionId");
|
|
3003
3078
|
if (!sessionId)
|
|
3004
|
-
return
|
|
3079
|
+
return badRequest("sessionId required");
|
|
3005
3080
|
const session = validateSession(sessionId);
|
|
3006
3081
|
if (!session)
|
|
3007
|
-
return
|
|
3082
|
+
return unauthorized("Invalid or expired session");
|
|
3008
3083
|
const types = c.req.query("types") || undefined;
|
|
3009
3084
|
const result = await _slackChannels.listChannels({ types });
|
|
3010
3085
|
if (!result.ok)
|
|
@@ -3015,10 +3090,10 @@ app.get("/api/slack/channels", async (c) => {
|
|
|
3015
3090
|
app.get("/api/slack/channels/:id", async (c) => {
|
|
3016
3091
|
const sessionId = c.req.query("sessionId");
|
|
3017
3092
|
if (!sessionId)
|
|
3018
|
-
return
|
|
3093
|
+
return badRequest("sessionId required");
|
|
3019
3094
|
const session = validateSession(sessionId);
|
|
3020
3095
|
if (!session)
|
|
3021
|
-
return
|
|
3096
|
+
return unauthorized("Invalid or expired session");
|
|
3022
3097
|
const result = await _slackChannels.getChannelInfo(c.req.param("id"));
|
|
3023
3098
|
if (!result.ok)
|
|
3024
3099
|
return c.json({ error: result.message }, 502);
|
|
@@ -3028,10 +3103,10 @@ app.get("/api/slack/channels/:id", async (c) => {
|
|
|
3028
3103
|
app.post("/api/slack/channels/:id/join", async (c) => {
|
|
3029
3104
|
const sessionId = c.req.query("sessionId");
|
|
3030
3105
|
if (!sessionId)
|
|
3031
|
-
return
|
|
3106
|
+
return badRequest("sessionId required");
|
|
3032
3107
|
const session = validateSession(sessionId);
|
|
3033
3108
|
if (!session)
|
|
3034
|
-
return
|
|
3109
|
+
return unauthorized("Invalid or expired session");
|
|
3035
3110
|
const result = await _slackChannels.joinChannel(c.req.param("id"));
|
|
3036
3111
|
if (!result.ok)
|
|
3037
3112
|
return c.json({ error: result.message }, 502);
|
|
@@ -3041,10 +3116,10 @@ app.post("/api/slack/channels/:id/join", async (c) => {
|
|
|
3041
3116
|
app.get("/api/slack/channels/:id/history", async (c) => {
|
|
3042
3117
|
const sessionId = c.req.query("sessionId");
|
|
3043
3118
|
if (!sessionId)
|
|
3044
|
-
return
|
|
3119
|
+
return badRequest("sessionId required");
|
|
3045
3120
|
const session = validateSession(sessionId);
|
|
3046
3121
|
if (!session)
|
|
3047
|
-
return
|
|
3122
|
+
return unauthorized("Invalid or expired session");
|
|
3048
3123
|
const limit = c.req.query("limit") ? parseInt(c.req.query("limit"), 10) : undefined;
|
|
3049
3124
|
const result = await _slackChannels.getChannelHistory(c.req.param("id"), { limit });
|
|
3050
3125
|
if (!result.ok)
|
|
@@ -3055,10 +3130,10 @@ app.get("/api/slack/channels/:id/history", async (c) => {
|
|
|
3055
3130
|
app.get("/api/slack/users/:id", async (c) => {
|
|
3056
3131
|
const sessionId = c.req.query("sessionId");
|
|
3057
3132
|
if (!sessionId)
|
|
3058
|
-
return
|
|
3133
|
+
return badRequest("sessionId required");
|
|
3059
3134
|
const session = validateSession(sessionId);
|
|
3060
3135
|
if (!session)
|
|
3061
|
-
return
|
|
3136
|
+
return unauthorized("Invalid or expired session");
|
|
3062
3137
|
const client = (_slackClient?.getClient() ?? null);
|
|
3063
3138
|
if (!client)
|
|
3064
3139
|
return c.json({ error: "Slack not available" }, 503);
|
|
@@ -3071,7 +3146,7 @@ app.get("/api/slack/users/:id", async (c) => {
|
|
|
3071
3146
|
// Routed through the generic webhook system with challenge response handling.
|
|
3072
3147
|
app.post("/api/slack/events", async (c) => {
|
|
3073
3148
|
if (!_webhooksMount)
|
|
3074
|
-
return
|
|
3149
|
+
return forbidden("Webhooks require BYOK tier");
|
|
3075
3150
|
return _webhooksMount.createWebhookRoute({
|
|
3076
3151
|
provider: "slack-events",
|
|
3077
3152
|
transformResponse: (result, ctx) => {
|
|
@@ -3084,30 +3159,30 @@ app.post("/api/slack/events", async (c) => {
|
|
|
3084
3159
|
// Slack slash commands: routed through the generic webhook system.
|
|
3085
3160
|
app.post("/api/slack/commands", async (c) => {
|
|
3086
3161
|
if (!_webhooksMount)
|
|
3087
|
-
return
|
|
3162
|
+
return forbidden("Webhooks require BYOK tier");
|
|
3088
3163
|
return _webhooksMount.createWebhookRoute({ provider: "slack-commands" })(c);
|
|
3089
3164
|
});
|
|
3090
3165
|
// Slack interactions: routed through the generic webhook system.
|
|
3091
3166
|
app.post("/api/slack/interactions", async (c) => {
|
|
3092
3167
|
if (!_webhooksMount)
|
|
3093
|
-
return
|
|
3168
|
+
return forbidden("Webhooks require BYOK tier");
|
|
3094
3169
|
return _webhooksMount.createWebhookRoute({ provider: "slack-interactions" })(c);
|
|
3095
3170
|
});
|
|
3096
3171
|
// --- Resend inbound email ---
|
|
3097
3172
|
// Resend webhook: receive inbound emails via Svix-signed webhooks (direct path).
|
|
3098
3173
|
app.post("/api/resend/webhooks", async (c) => {
|
|
3099
3174
|
if (!_webhooksMount)
|
|
3100
|
-
return
|
|
3175
|
+
return forbidden("Webhooks require BYOK tier");
|
|
3101
3176
|
return _webhooksMount.createWebhookRoute({ provider: "resend" })(c);
|
|
3102
3177
|
});
|
|
3103
3178
|
// Resend inbox: manually trigger inbox check (pulls from Worker KV).
|
|
3104
3179
|
app.post("/api/resend/check-inbox", async (c) => {
|
|
3105
3180
|
const sessionId = c.req.query("sessionId");
|
|
3106
3181
|
if (!sessionId)
|
|
3107
|
-
return
|
|
3182
|
+
return badRequest("sessionId required");
|
|
3108
3183
|
const session = validateSession(sessionId);
|
|
3109
3184
|
if (!session)
|
|
3110
|
-
return
|
|
3185
|
+
return unauthorized("Invalid or expired session");
|
|
3111
3186
|
const { forceCheckResendInbox } = await import("./resend/inbox.js");
|
|
3112
3187
|
const count = await forceCheckResendInbox();
|
|
3113
3188
|
return c.json({ ok: true, processed: count });
|
|
@@ -3168,7 +3243,7 @@ app.post("/api/relay/whatsapp", async (c) => {
|
|
|
3168
3243
|
const relaySecret = process.env.RELAY_SECRET ?? "";
|
|
3169
3244
|
const verification = _webhooksRelay.verifyRelaySignature(rawBody, headers, relaySecret);
|
|
3170
3245
|
if (!verification.valid) {
|
|
3171
|
-
return
|
|
3246
|
+
return unauthorized(verification.error ?? "Invalid relay signature");
|
|
3172
3247
|
}
|
|
3173
3248
|
const params = _webhooksTwilio.parseFormBody(rawBody);
|
|
3174
3249
|
const payload = params;
|
|
@@ -3195,14 +3270,14 @@ app.post("/api/relay/resend", async (c) => {
|
|
|
3195
3270
|
const relaySecret = process.env.RELAY_SECRET ?? "";
|
|
3196
3271
|
const verification = _webhooksRelay.verifyRelaySignature(rawBody, headers, relaySecret);
|
|
3197
3272
|
if (!verification.valid) {
|
|
3198
|
-
return
|
|
3273
|
+
return unauthorized(verification.error ?? "Invalid relay signature");
|
|
3199
3274
|
}
|
|
3200
3275
|
let payload;
|
|
3201
3276
|
try {
|
|
3202
3277
|
payload = JSON.parse(rawBody);
|
|
3203
3278
|
}
|
|
3204
3279
|
catch {
|
|
3205
|
-
return
|
|
3280
|
+
return badRequest("Invalid JSON");
|
|
3206
3281
|
}
|
|
3207
3282
|
if (payload.type !== "email.received" || !payload.body?.trim()) {
|
|
3208
3283
|
return c.json({ ok: true, message: "No actionable content" });
|
|
@@ -3240,16 +3315,16 @@ app.post("/api/relay/resend", async (c) => {
|
|
|
3240
3315
|
app.post("/api/whatsapp/send", async (c) => {
|
|
3241
3316
|
const sessionId = c.req.query("sessionId");
|
|
3242
3317
|
if (!sessionId)
|
|
3243
|
-
return
|
|
3318
|
+
return badRequest("sessionId required");
|
|
3244
3319
|
const session = validateSession(sessionId);
|
|
3245
3320
|
if (!session)
|
|
3246
|
-
return
|
|
3321
|
+
return unauthorized("Invalid or expired session");
|
|
3247
3322
|
const client = (_channelsWhatsapp?.getClient() ?? null);
|
|
3248
3323
|
if (!client)
|
|
3249
3324
|
return c.json({ error: "WhatsApp not configured. Add TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER to vault." }, 503);
|
|
3250
3325
|
const body = await c.req.json();
|
|
3251
3326
|
if (!body.to || !body.message)
|
|
3252
|
-
return
|
|
3327
|
+
return badRequest("to and message required");
|
|
3253
3328
|
const result = await client.sendMessage(body.to, body.message);
|
|
3254
3329
|
if (!result.ok)
|
|
3255
3330
|
return c.json({ error: result.message }, 502);
|
|
@@ -3259,10 +3334,10 @@ app.post("/api/whatsapp/send", async (c) => {
|
|
|
3259
3334
|
app.get("/api/whatsapp/contacts", async (c) => {
|
|
3260
3335
|
const sessionId = c.req.query("sessionId");
|
|
3261
3336
|
if (!sessionId)
|
|
3262
|
-
return
|
|
3337
|
+
return badRequest("sessionId required");
|
|
3263
3338
|
const session = validateSession(sessionId);
|
|
3264
3339
|
if (!session)
|
|
3265
|
-
return
|
|
3340
|
+
return unauthorized("Invalid or expired session");
|
|
3266
3341
|
const client = (_channelsWhatsapp?.getClient() ?? null);
|
|
3267
3342
|
if (!client)
|
|
3268
3343
|
return c.json({ error: "WhatsApp not configured" }, 503);
|
|
@@ -3273,10 +3348,10 @@ app.get("/api/whatsapp/contacts", async (c) => {
|
|
|
3273
3348
|
app.get("/api/whatsapp/history", async (c) => {
|
|
3274
3349
|
const sessionId = c.req.query("sessionId");
|
|
3275
3350
|
if (!sessionId)
|
|
3276
|
-
return
|
|
3351
|
+
return badRequest("sessionId required");
|
|
3277
3352
|
const session = validateSession(sessionId);
|
|
3278
3353
|
if (!session)
|
|
3279
|
-
return
|
|
3354
|
+
return unauthorized("Invalid or expired session");
|
|
3280
3355
|
const client = (_channelsWhatsapp?.getClient() ?? null);
|
|
3281
3356
|
if (!client)
|
|
3282
3357
|
return c.json({ error: "WhatsApp not configured" }, 503);
|
|
@@ -3323,10 +3398,10 @@ app.get("/api/board/status", async (c) => {
|
|
|
3323
3398
|
app.get("/api/board/teams", async (c) => {
|
|
3324
3399
|
const sessionId = c.req.query("sessionId");
|
|
3325
3400
|
if (!sessionId)
|
|
3326
|
-
return
|
|
3401
|
+
return badRequest("sessionId required");
|
|
3327
3402
|
const session = validateSession(sessionId);
|
|
3328
3403
|
if (!session)
|
|
3329
|
-
return
|
|
3404
|
+
return unauthorized("Invalid or expired session");
|
|
3330
3405
|
const board = getBoardProvider();
|
|
3331
3406
|
if (!board || !board.isAvailable())
|
|
3332
3407
|
return c.json({ teams: [] });
|
|
@@ -3337,10 +3412,10 @@ app.get("/api/board/teams", async (c) => {
|
|
|
3337
3412
|
app.get("/api/board/issues", async (c) => {
|
|
3338
3413
|
const sessionId = c.req.query("sessionId");
|
|
3339
3414
|
if (!sessionId)
|
|
3340
|
-
return
|
|
3415
|
+
return badRequest("sessionId required");
|
|
3341
3416
|
const session = validateSession(sessionId);
|
|
3342
3417
|
if (!session)
|
|
3343
|
-
return
|
|
3418
|
+
return unauthorized("Invalid or expired session");
|
|
3344
3419
|
const board = getBoardProvider();
|
|
3345
3420
|
if (!board || !board.isAvailable())
|
|
3346
3421
|
return c.json({ issues: [] });
|
|
@@ -3353,17 +3428,17 @@ app.get("/api/board/issues", async (c) => {
|
|
|
3353
3428
|
app.post("/api/board/issues", async (c) => {
|
|
3354
3429
|
const sessionId = c.req.query("sessionId");
|
|
3355
3430
|
if (!sessionId)
|
|
3356
|
-
return
|
|
3431
|
+
return badRequest("sessionId required");
|
|
3357
3432
|
const session = validateSession(sessionId);
|
|
3358
3433
|
if (!session)
|
|
3359
|
-
return
|
|
3434
|
+
return unauthorized("Invalid or expired session");
|
|
3360
3435
|
const board = getBoardProvider();
|
|
3361
3436
|
if (!board || !board.isAvailable())
|
|
3362
3437
|
return c.json({ error: "No board provider configured" }, 503);
|
|
3363
3438
|
const body = await c.req.json();
|
|
3364
3439
|
const { title, description, teamId, priority } = body;
|
|
3365
3440
|
if (!title)
|
|
3366
|
-
return
|
|
3441
|
+
return badRequest("title required");
|
|
3367
3442
|
const issue = await board.createIssue(title, { description, teamId, priority });
|
|
3368
3443
|
if (!issue)
|
|
3369
3444
|
return c.json({ error: "Failed to create issue" }, 502);
|
|
@@ -3373,10 +3448,10 @@ app.post("/api/board/issues", async (c) => {
|
|
|
3373
3448
|
app.patch("/api/board/issues/:id", async (c) => {
|
|
3374
3449
|
const sessionId = c.req.query("sessionId");
|
|
3375
3450
|
if (!sessionId)
|
|
3376
|
-
return
|
|
3451
|
+
return badRequest("sessionId required");
|
|
3377
3452
|
const session = validateSession(sessionId);
|
|
3378
3453
|
if (!session)
|
|
3379
|
-
return
|
|
3454
|
+
return unauthorized("Invalid or expired session");
|
|
3380
3455
|
const board = getBoardProvider();
|
|
3381
3456
|
if (!board || !board.isAvailable())
|
|
3382
3457
|
return c.json({ error: "No board provider configured" }, 503);
|
|
@@ -3392,10 +3467,10 @@ app.patch("/api/board/issues/:id", async (c) => {
|
|
|
3392
3467
|
app.post("/api/board/issues/:id/comments", async (c) => {
|
|
3393
3468
|
const sessionId = c.req.query("sessionId");
|
|
3394
3469
|
if (!sessionId)
|
|
3395
|
-
return
|
|
3470
|
+
return badRequest("sessionId required");
|
|
3396
3471
|
const session = validateSession(sessionId);
|
|
3397
3472
|
if (!session)
|
|
3398
|
-
return
|
|
3473
|
+
return unauthorized("Invalid or expired session");
|
|
3399
3474
|
const board = getBoardProvider();
|
|
3400
3475
|
if (!board || !board.isAvailable())
|
|
3401
3476
|
return c.json({ error: "No board provider configured" }, 503);
|
|
@@ -3403,7 +3478,7 @@ app.post("/api/board/issues/:id/comments", async (c) => {
|
|
|
3403
3478
|
const body = await c.req.json();
|
|
3404
3479
|
const { body: commentBody } = body;
|
|
3405
3480
|
if (!commentBody)
|
|
3406
|
-
return
|
|
3481
|
+
return badRequest("body required");
|
|
3407
3482
|
const ok = await board.addComment(id, commentBody);
|
|
3408
3483
|
return ok ? c.json({ ok: true }) : c.json({ error: "Failed to add comment" }, 502);
|
|
3409
3484
|
});
|
|
@@ -3411,10 +3486,10 @@ app.post("/api/board/issues/:id/comments", async (c) => {
|
|
|
3411
3486
|
app.get("/api/board/issues/:id/exchanges", async (c) => {
|
|
3412
3487
|
const sessionId = c.req.query("sessionId");
|
|
3413
3488
|
if (!sessionId)
|
|
3414
|
-
return
|
|
3489
|
+
return badRequest("sessionId required");
|
|
3415
3490
|
const session = validateSession(sessionId);
|
|
3416
3491
|
if (!session)
|
|
3417
|
-
return
|
|
3492
|
+
return unauthorized("Invalid or expired session");
|
|
3418
3493
|
const board = getBoardProvider();
|
|
3419
3494
|
const store = board?.getStore?.();
|
|
3420
3495
|
if (!store)
|
|
@@ -3426,10 +3501,10 @@ app.get("/api/board/issues/:id/exchanges", async (c) => {
|
|
|
3426
3501
|
app.post("/api/board/issues/:id/exchanges", async (c) => {
|
|
3427
3502
|
const sessionId = c.req.query("sessionId");
|
|
3428
3503
|
if (!sessionId)
|
|
3429
|
-
return
|
|
3504
|
+
return badRequest("sessionId required");
|
|
3430
3505
|
const session = validateSession(sessionId);
|
|
3431
3506
|
if (!session)
|
|
3432
|
-
return
|
|
3507
|
+
return unauthorized("Invalid or expired session");
|
|
3433
3508
|
const board = getBoardProvider();
|
|
3434
3509
|
const store = board?.getStore?.();
|
|
3435
3510
|
if (!store)
|
|
@@ -3438,7 +3513,7 @@ app.post("/api/board/issues/:id/exchanges", async (c) => {
|
|
|
3438
3513
|
const body = await c.req.json();
|
|
3439
3514
|
const { author, body: exBody, source } = body;
|
|
3440
3515
|
if (!author || !exBody)
|
|
3441
|
-
return
|
|
3516
|
+
return badRequest("author and body required");
|
|
3442
3517
|
const exchange = await store.addExchange(id, {
|
|
3443
3518
|
author,
|
|
3444
3519
|
body: exBody,
|
|
@@ -3448,7 +3523,7 @@ app.post("/api/board/issues/:id/exchanges", async (c) => {
|
|
|
3448
3523
|
logActivity({ source: "board", summary: `Exchange on issue ${id} from ${author}`, actionLabel: "PROMPTED", reason: "user added board exchange" });
|
|
3449
3524
|
return c.json({ exchange });
|
|
3450
3525
|
}
|
|
3451
|
-
return
|
|
3526
|
+
return notFound("Task not found or archived");
|
|
3452
3527
|
});
|
|
3453
3528
|
// --- Weekly backlog review endpoints (DASH-59) ---
|
|
3454
3529
|
// Get the last backlog review report.
|
|
@@ -3466,6 +3541,158 @@ app.post("/api/board/review/trigger", async (c) => {
|
|
|
3466
3541
|
return c.json({ ok: true, report });
|
|
3467
3542
|
});
|
|
3468
3543
|
// ---------------------------------------------------------------------------
|
|
3544
|
+
// Gemini Search
|
|
3545
|
+
// ---------------------------------------------------------------------------
|
|
3546
|
+
app.post("/api/search/gemini", async (c) => {
|
|
3547
|
+
const { query } = await c.req.json();
|
|
3548
|
+
if (!query?.trim())
|
|
3549
|
+
return c.json({ ok: false, message: "Query is required" }, 400);
|
|
3550
|
+
const { geminiSearch, isGeminiAvailable } = await import("./search/gemini.js");
|
|
3551
|
+
if (!isGeminiAvailable()) {
|
|
3552
|
+
return c.json({ ok: false, message: "GEMINI_API_KEY not configured" }, 503);
|
|
3553
|
+
}
|
|
3554
|
+
const result = await geminiSearch(query.trim());
|
|
3555
|
+
return c.json(result);
|
|
3556
|
+
});
|
|
3557
|
+
app.get("/api/search/gemini/status", async (c) => {
|
|
3558
|
+
const { isGeminiAvailable } = await import("./search/gemini.js");
|
|
3559
|
+
return c.json({ available: isGeminiAvailable() });
|
|
3560
|
+
});
|
|
3561
|
+
// ---------------------------------------------------------------------------
|
|
3562
|
+
// Whiteboard routes
|
|
3563
|
+
// ---------------------------------------------------------------------------
|
|
3564
|
+
import { WhiteboardStore } from "./whiteboard/store.js";
|
|
3565
|
+
let _whiteboardStore = null;
|
|
3566
|
+
function getWhiteboardStore() {
|
|
3567
|
+
if (!_whiteboardStore)
|
|
3568
|
+
_whiteboardStore = new WhiteboardStore(BRAIN_DIR);
|
|
3569
|
+
return _whiteboardStore;
|
|
3570
|
+
}
|
|
3571
|
+
// Whiteboard is always accessible — it's the human-agent collaboration surface
|
|
3572
|
+
// List / tree / questions / weighted view
|
|
3573
|
+
app.get("/api/whiteboard", async (c) => {
|
|
3574
|
+
const store = getWhiteboardStore();
|
|
3575
|
+
const view = c.req.query("view") ?? "tree";
|
|
3576
|
+
const root = c.req.query("root");
|
|
3577
|
+
const status = c.req.query("status");
|
|
3578
|
+
const type = c.req.query("type");
|
|
3579
|
+
const tagsParam = c.req.query("tags");
|
|
3580
|
+
const search = c.req.query("search");
|
|
3581
|
+
if (view === "tree") {
|
|
3582
|
+
const tree = await store.getTree(root ?? undefined);
|
|
3583
|
+
return c.json({ nodes: tree });
|
|
3584
|
+
}
|
|
3585
|
+
if (view === "questions") {
|
|
3586
|
+
const questions = await store.getOpenQuestions();
|
|
3587
|
+
// Attach breadcrumb path to each question
|
|
3588
|
+
const withPaths = await Promise.all(questions.map(async (q) => {
|
|
3589
|
+
const ancestors = await store.getAncestors(q.id);
|
|
3590
|
+
return { ...q, path: ancestors.map((a) => ({ id: a.id, title: a.title })) };
|
|
3591
|
+
}));
|
|
3592
|
+
return c.json({ questions: withPaths });
|
|
3593
|
+
}
|
|
3594
|
+
if (view === "weighted") {
|
|
3595
|
+
const weighted = await store.getWeighted();
|
|
3596
|
+
return c.json({ nodes: weighted });
|
|
3597
|
+
}
|
|
3598
|
+
// Flat view with filters
|
|
3599
|
+
const tags = tagsParam ? tagsParam.split(",").map((t) => t.trim()) : undefined;
|
|
3600
|
+
const nodes = await store.list({ type, status, tags, search });
|
|
3601
|
+
return c.json({ nodes });
|
|
3602
|
+
});
|
|
3603
|
+
// Summary
|
|
3604
|
+
app.get("/api/whiteboard/summary", async (c) => {
|
|
3605
|
+
const store = getWhiteboardStore();
|
|
3606
|
+
const summary = await store.getSummary();
|
|
3607
|
+
return c.json(summary);
|
|
3608
|
+
});
|
|
3609
|
+
// Recently answered questions (for agent goals loop)
|
|
3610
|
+
app.get("/api/whiteboard/answered", async (c) => {
|
|
3611
|
+
const store = getWhiteboardStore();
|
|
3612
|
+
const since = c.req.query("since");
|
|
3613
|
+
if (!since)
|
|
3614
|
+
return c.json({ error: "Missing ?since= parameter" }, 400);
|
|
3615
|
+
const answered = await store.getAnsweredSince(since);
|
|
3616
|
+
return c.json({ answered });
|
|
3617
|
+
});
|
|
3618
|
+
// Get single node
|
|
3619
|
+
app.get("/api/whiteboard/:id", async (c) => {
|
|
3620
|
+
const store = getWhiteboardStore();
|
|
3621
|
+
const node = await store.get(c.req.param("id"));
|
|
3622
|
+
if (!node)
|
|
3623
|
+
return c.json({ error: "Node not found" }, 404);
|
|
3624
|
+
return c.json(node);
|
|
3625
|
+
});
|
|
3626
|
+
// Get ancestors (breadcrumb path)
|
|
3627
|
+
app.get("/api/whiteboard/:id/path", async (c) => {
|
|
3628
|
+
const store = getWhiteboardStore();
|
|
3629
|
+
const node = await store.get(c.req.param("id"));
|
|
3630
|
+
if (!node)
|
|
3631
|
+
return c.json({ error: "Node not found" }, 404);
|
|
3632
|
+
const ancestors = await store.getAncestors(c.req.param("id"));
|
|
3633
|
+
return c.json({ path: [...ancestors, node].map((n) => ({ id: n.id, title: n.title, type: n.type })) });
|
|
3634
|
+
});
|
|
3635
|
+
// Create node
|
|
3636
|
+
app.post("/api/whiteboard", async (c) => {
|
|
3637
|
+
const body = await c.req.json();
|
|
3638
|
+
if (!body.title?.trim())
|
|
3639
|
+
return c.json({ error: "Title is required" }, 400);
|
|
3640
|
+
if (!body.type)
|
|
3641
|
+
return c.json({ error: "Type is required (goal, task, question, decision, note)" }, 400);
|
|
3642
|
+
if (!body.plantedBy)
|
|
3643
|
+
return c.json({ error: "plantedBy is required (agent, human)" }, 400);
|
|
3644
|
+
const store = getWhiteboardStore();
|
|
3645
|
+
// Validate parentId exists if provided
|
|
3646
|
+
if (body.parentId) {
|
|
3647
|
+
const parent = await store.get(body.parentId);
|
|
3648
|
+
if (!parent)
|
|
3649
|
+
return c.json({ error: `Parent node not found: ${body.parentId}` }, 400);
|
|
3650
|
+
}
|
|
3651
|
+
const node = await store.create({
|
|
3652
|
+
title: body.title.trim(),
|
|
3653
|
+
type: body.type,
|
|
3654
|
+
parentId: body.parentId ?? null,
|
|
3655
|
+
tags: body.tags ?? [],
|
|
3656
|
+
plantedBy: body.plantedBy,
|
|
3657
|
+
body: body.body,
|
|
3658
|
+
question: body.question,
|
|
3659
|
+
boardTaskId: body.boardTaskId,
|
|
3660
|
+
});
|
|
3661
|
+
return c.json(node, 201);
|
|
3662
|
+
});
|
|
3663
|
+
// Update node
|
|
3664
|
+
app.patch("/api/whiteboard/:id", async (c) => {
|
|
3665
|
+
const body = await c.req.json();
|
|
3666
|
+
const store = getWhiteboardStore();
|
|
3667
|
+
const updated = await store.update(c.req.param("id"), body);
|
|
3668
|
+
if (!updated)
|
|
3669
|
+
return c.json({ error: "Node not found" }, 404);
|
|
3670
|
+
return c.json(updated);
|
|
3671
|
+
});
|
|
3672
|
+
// Answer a question
|
|
3673
|
+
app.post("/api/whiteboard/:id/answer", async (c) => {
|
|
3674
|
+
const { answer } = await c.req.json();
|
|
3675
|
+
if (!answer?.trim())
|
|
3676
|
+
return c.json({ error: "Answer is required" }, 400);
|
|
3677
|
+
const store = getWhiteboardStore();
|
|
3678
|
+
const node = await store.get(c.req.param("id"));
|
|
3679
|
+
if (!node)
|
|
3680
|
+
return c.json({ error: "Node not found" }, 404);
|
|
3681
|
+
if (node.type !== "question")
|
|
3682
|
+
return c.json({ error: "Only question nodes can be answered" }, 400);
|
|
3683
|
+
const updated = await store.answerQuestion(c.req.param("id"), answer.trim());
|
|
3684
|
+
return c.json(updated);
|
|
3685
|
+
});
|
|
3686
|
+
// Archive node
|
|
3687
|
+
app.delete("/api/whiteboard/:id", async (c) => {
|
|
3688
|
+
const cascade = c.req.query("cascade") === "true";
|
|
3689
|
+
const store = getWhiteboardStore();
|
|
3690
|
+
const result = await store.archive(c.req.param("id"), cascade);
|
|
3691
|
+
if (!result.ok)
|
|
3692
|
+
return c.json({ error: result.message }, 404);
|
|
3693
|
+
return c.json(result);
|
|
3694
|
+
});
|
|
3695
|
+
// ---------------------------------------------------------------------------
|
|
3469
3696
|
// Skills routes
|
|
3470
3697
|
// ---------------------------------------------------------------------------
|
|
3471
3698
|
// List all registered skills (metadata only)
|
|
@@ -3487,7 +3714,7 @@ app.get("/api/skills/:name", async (c) => {
|
|
|
3487
3714
|
const name = c.req.param("name");
|
|
3488
3715
|
const skill = await _skillRegistry.get(name);
|
|
3489
3716
|
if (!skill)
|
|
3490
|
-
return
|
|
3717
|
+
return notFound("Skill not found");
|
|
3491
3718
|
const content = await _skillRegistry.getContent(name);
|
|
3492
3719
|
return c.json({
|
|
3493
3720
|
...skill,
|
|
@@ -3498,10 +3725,10 @@ app.get("/api/skills/:name", async (c) => {
|
|
|
3498
3725
|
app.post("/api/skills/resolve", async (c) => {
|
|
3499
3726
|
const { trigger } = await c.req.json();
|
|
3500
3727
|
if (!trigger)
|
|
3501
|
-
return
|
|
3728
|
+
return badRequest("trigger is required");
|
|
3502
3729
|
const skill = await _skillRegistry.findByTrigger(trigger);
|
|
3503
3730
|
if (!skill)
|
|
3504
|
-
return
|
|
3731
|
+
return notFound("No matching skill");
|
|
3505
3732
|
return c.json({
|
|
3506
3733
|
id: skill.id,
|
|
3507
3734
|
name: skill.name,
|
|
@@ -3514,9 +3741,19 @@ app.post("/api/skills/resolve", async (c) => {
|
|
|
3514
3741
|
import { getPluginStatusSummary } from "./plugins/status.js";
|
|
3515
3742
|
import { initPlugins, shutdownPlugins } from "./plugins/index.js";
|
|
3516
3743
|
// --- File management routes ---
|
|
3517
|
-
import {
|
|
3518
|
-
import { validateUpload } from "./files/validate.js";
|
|
3519
|
-
|
|
3744
|
+
import { FileStore, computeChecksum } from "./files/store.js";
|
|
3745
|
+
import { validateUpload, slugify } from "./files/validate.js";
|
|
3746
|
+
// Shared file store instance — initialized lazily on first use
|
|
3747
|
+
let _fileStore = null;
|
|
3748
|
+
function getFileStore() {
|
|
3749
|
+
if (!_fileStore)
|
|
3750
|
+
_fileStore = new FileStore(BRAIN_DIR);
|
|
3751
|
+
return _fileStore;
|
|
3752
|
+
}
|
|
3753
|
+
/** Map FileEntry to API response shape (adds `filename` alias for backward compat). */
|
|
3754
|
+
function toFileResponse(entry) {
|
|
3755
|
+
return { ...entry, filename: entry.name };
|
|
3756
|
+
}
|
|
3520
3757
|
app.get("/api/plugins", (c) => {
|
|
3521
3758
|
return c.json(getPluginStatusSummary());
|
|
3522
3759
|
});
|
|
@@ -3524,10 +3761,11 @@ app.get("/api/plugins", (c) => {
|
|
|
3524
3761
|
// Upload file — persist to brain/files/data/, register in JSONL
|
|
3525
3762
|
app.post("/api/files/upload", async (c) => {
|
|
3526
3763
|
try {
|
|
3764
|
+
const store = getFileStore();
|
|
3527
3765
|
const formData = await c.req.formData();
|
|
3528
3766
|
const file = formData.get("file");
|
|
3529
3767
|
if (!file)
|
|
3530
|
-
return
|
|
3768
|
+
return badRequest("No file provided");
|
|
3531
3769
|
const source = formData.get("source") || "user-upload";
|
|
3532
3770
|
const tagsRaw = formData.get("tags");
|
|
3533
3771
|
const tags = tagsRaw ? tagsRaw.split(",").map(t => t.trim()).filter(Boolean) : [];
|
|
@@ -3540,17 +3778,17 @@ app.post("/api/files/upload", async (c) => {
|
|
|
3540
3778
|
// Validate: extension allowlist, magic bytes, size, content scan
|
|
3541
3779
|
const validation = await validateUpload(buffer, file.name, file.type, maxUploadBytes);
|
|
3542
3780
|
if (!validation.valid) {
|
|
3543
|
-
return
|
|
3781
|
+
return badRequest(validation.rejected ?? "Upload rejected");
|
|
3544
3782
|
}
|
|
3545
3783
|
// Generate storage path: brain/files/data/YYYY-MM-DD/slug_id.ext
|
|
3546
3784
|
const dateDir = new Date().toISOString().slice(0, 10);
|
|
3547
3785
|
const slug = slugify(file.name.replace(/\.[^.]+$/, ""));
|
|
3548
3786
|
const checksum = computeChecksum(buffer);
|
|
3549
3787
|
// Check for duplicate by checksum
|
|
3550
|
-
const existing = await
|
|
3788
|
+
const existing = await store.list({});
|
|
3551
3789
|
const dup = existing.find(r => r.checksum === checksum && r.status === "active");
|
|
3552
3790
|
if (dup) {
|
|
3553
|
-
return c.json({ file: dup, duplicate: true });
|
|
3791
|
+
return c.json({ file: toFileResponse(dup), duplicate: true });
|
|
3554
3792
|
}
|
|
3555
3793
|
const storageDir = join(BRAIN_DIR, "files", "data", dateDir);
|
|
3556
3794
|
await mkdir(storageDir, { recursive: true });
|
|
@@ -3571,20 +3809,29 @@ app.post("/api/files/upload", async (c) => {
|
|
|
3571
3809
|
}
|
|
3572
3810
|
catch { /* PDF extraction optional */ }
|
|
3573
3811
|
}
|
|
3574
|
-
// Register in file
|
|
3575
|
-
const record = await
|
|
3576
|
-
|
|
3812
|
+
// Register in file store
|
|
3813
|
+
const record = await store.create({
|
|
3814
|
+
name: validation.sanitizedName,
|
|
3815
|
+
slug,
|
|
3577
3816
|
storagePath,
|
|
3578
3817
|
mimeType: validation.detectedMime || file.type,
|
|
3579
3818
|
sizeBytes: buffer.length,
|
|
3580
3819
|
checksum,
|
|
3581
3820
|
tags,
|
|
3582
|
-
source,
|
|
3821
|
+
origin: source,
|
|
3822
|
+
ownerId: null,
|
|
3823
|
+
taskId: null,
|
|
3824
|
+
parentId: null,
|
|
3825
|
+
version: 1,
|
|
3826
|
+
encrypted: false,
|
|
3827
|
+
visibility: "private",
|
|
3583
3828
|
status: "active",
|
|
3829
|
+
category: "upload",
|
|
3830
|
+
textPreview,
|
|
3584
3831
|
});
|
|
3585
3832
|
// Trigger volume replication (on-write event)
|
|
3586
3833
|
volumeManager.handleEvent({ type: "write", fileId: record.id, volume: "primary" }).catch((err) => log.warn("Volume on-write event failed", { error: String(err) }));
|
|
3587
|
-
return c.json({ file: record, duplicate: false });
|
|
3834
|
+
return c.json({ file: toFileResponse(record), duplicate: false });
|
|
3588
3835
|
}
|
|
3589
3836
|
catch (err) {
|
|
3590
3837
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -3594,68 +3841,86 @@ app.post("/api/files/upload", async (c) => {
|
|
|
3594
3841
|
});
|
|
3595
3842
|
// Download / serve a stored file
|
|
3596
3843
|
app.get("/api/files/:id/download", async (c) => {
|
|
3597
|
-
const record = await
|
|
3844
|
+
const record = await getFileStore().get(c.req.param("id"));
|
|
3598
3845
|
if (!record)
|
|
3599
|
-
return
|
|
3846
|
+
return notFound("File not found");
|
|
3600
3847
|
const fullPath = join(BRAIN_DIR, record.storagePath);
|
|
3601
3848
|
try {
|
|
3602
3849
|
const data = await readFile(fullPath);
|
|
3603
3850
|
return c.newResponse(data, 200, {
|
|
3604
3851
|
"Content-Type": record.mimeType,
|
|
3605
|
-
"Content-Disposition": `inline; filename="${record.
|
|
3852
|
+
"Content-Disposition": `inline; filename="${record.name}"`,
|
|
3606
3853
|
"Content-Length": String(data.length),
|
|
3607
3854
|
});
|
|
3608
3855
|
}
|
|
3609
3856
|
catch {
|
|
3610
|
-
return
|
|
3857
|
+
return notFound("File data not found on disk");
|
|
3611
3858
|
}
|
|
3612
3859
|
});
|
|
3613
3860
|
// List virtual folders (must be before :id route)
|
|
3614
3861
|
app.get("/api/files/folders", async (c) => {
|
|
3615
|
-
const
|
|
3616
|
-
|
|
3862
|
+
const store = getFileStore();
|
|
3863
|
+
const all = await store.list({ status: "active" });
|
|
3864
|
+
const folders = new Set();
|
|
3865
|
+
for (const f of all) {
|
|
3866
|
+
for (const t of f.tags ?? []) {
|
|
3867
|
+
if (t.startsWith("folder:"))
|
|
3868
|
+
folders.add(t.slice(7));
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
return c.json({ folders: [...folders].sort() });
|
|
3617
3872
|
});
|
|
3618
3873
|
app.get("/api/files", async (c) => {
|
|
3874
|
+
const store = getFileStore();
|
|
3619
3875
|
const status = c.req.query("status");
|
|
3620
3876
|
const source = c.req.query("source");
|
|
3621
3877
|
const q = c.req.query("q");
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3878
|
+
const filter = {};
|
|
3879
|
+
if (status)
|
|
3880
|
+
filter.status = status;
|
|
3881
|
+
if (source)
|
|
3882
|
+
filter.origin = source;
|
|
3883
|
+
if (q)
|
|
3884
|
+
filter.search = q;
|
|
3885
|
+
const results = await store.list(filter);
|
|
3886
|
+
const mapped = results.map(toFileResponse);
|
|
3887
|
+
return c.json({ files: mapped, total: mapped.length });
|
|
3628
3888
|
});
|
|
3629
3889
|
app.get("/api/files/:id", async (c) => {
|
|
3630
|
-
const record = await
|
|
3890
|
+
const record = await getFileStore().get(c.req.param("id"));
|
|
3631
3891
|
if (!record)
|
|
3632
|
-
return
|
|
3633
|
-
return c.json(record);
|
|
3892
|
+
return notFound("File not found");
|
|
3893
|
+
return c.json(toFileResponse(record));
|
|
3634
3894
|
});
|
|
3635
3895
|
app.post("/api/files/:id/archive", async (c) => {
|
|
3636
|
-
const result = await
|
|
3637
|
-
if (!result)
|
|
3638
|
-
return
|
|
3896
|
+
const result = await getFileStore().archive(c.req.param("id"), "user");
|
|
3897
|
+
if (!result.ok)
|
|
3898
|
+
return notFound(result.message);
|
|
3639
3899
|
return c.json(result);
|
|
3640
3900
|
});
|
|
3641
3901
|
// Update file tags / move to virtual folder
|
|
3642
3902
|
app.put("/api/files/:id", async (c) => {
|
|
3903
|
+
const store = getFileStore();
|
|
3643
3904
|
const body = await c.req.json();
|
|
3644
3905
|
const { tags, source, folder } = body;
|
|
3645
3906
|
const id = c.req.param("id");
|
|
3646
|
-
const record = await
|
|
3907
|
+
const record = await store.get(id);
|
|
3647
3908
|
if (!record)
|
|
3648
|
-
return
|
|
3909
|
+
return notFound("File not found");
|
|
3649
3910
|
// Handle virtual folder: stored as tag "folder:Name"
|
|
3650
3911
|
let updatedTags = tags ?? [...(record.tags ?? [])];
|
|
3651
3912
|
if (folder !== undefined) {
|
|
3652
|
-
// Remove existing folder tags, add new one
|
|
3653
3913
|
updatedTags = updatedTags.filter(t => !t.startsWith("folder:"));
|
|
3654
3914
|
if (folder)
|
|
3655
3915
|
updatedTags.push("folder:" + folder);
|
|
3656
3916
|
}
|
|
3657
|
-
const
|
|
3658
|
-
|
|
3917
|
+
const patch = { tags: updatedTags };
|
|
3918
|
+
if (source)
|
|
3919
|
+
patch.origin = source;
|
|
3920
|
+
const result = await store.update(id, patch);
|
|
3921
|
+
if (!result)
|
|
3922
|
+
return notFound("File not found");
|
|
3923
|
+
return c.json(toFileResponse(result));
|
|
3659
3924
|
});
|
|
3660
3925
|
// --- Volume management routes ---
|
|
3661
3926
|
app.get("/api/volumes", async (c) => {
|
|
@@ -3676,7 +3941,7 @@ app.post("/api/volumes/probe", async (c) => {
|
|
|
3676
3941
|
app.post("/api/volumes/event", async (c) => {
|
|
3677
3942
|
const event = await c.req.json();
|
|
3678
3943
|
if (!event?.type)
|
|
3679
|
-
return
|
|
3944
|
+
return badRequest("Missing event type");
|
|
3680
3945
|
await volumeManager.handleEvent(event);
|
|
3681
3946
|
return c.json({ ok: true });
|
|
3682
3947
|
});
|
|
@@ -3736,7 +4001,7 @@ app.get("/api/alerts/history", (c) => {
|
|
|
3736
4001
|
app.get("/api/alerts/:id", (c) => {
|
|
3737
4002
|
const alert = alertManager.getAlert(c.req.param("id"));
|
|
3738
4003
|
if (!alert)
|
|
3739
|
-
return
|
|
4004
|
+
return notFound("alert not found");
|
|
3740
4005
|
return c.json(alert);
|
|
3741
4006
|
});
|
|
3742
4007
|
// Acknowledge a firing alert.
|
|
@@ -3745,14 +4010,14 @@ app.post("/api/alerts/:id/acknowledge", async (c) => {
|
|
|
3745
4010
|
const by = body.by;
|
|
3746
4011
|
const ok = alertManager.acknowledge(c.req.param("id"), by);
|
|
3747
4012
|
if (!ok)
|
|
3748
|
-
return
|
|
4013
|
+
return notFound("alert not found or not in firing state");
|
|
3749
4014
|
return c.json({ acknowledged: true });
|
|
3750
4015
|
});
|
|
3751
4016
|
// Manually resolve an alert.
|
|
3752
4017
|
app.post("/api/alerts/:id/resolve", (c) => {
|
|
3753
4018
|
const ok = alertManager.resolve(c.req.param("id"));
|
|
3754
4019
|
if (!ok)
|
|
3755
|
-
return
|
|
4020
|
+
return notFound("alert not found");
|
|
3756
4021
|
return c.json({ resolved: true });
|
|
3757
4022
|
});
|
|
3758
4023
|
// Trigger manual evaluation of alert thresholds.
|
|
@@ -3838,7 +4103,7 @@ app.post("/api/scheduling/blocks", async (c) => {
|
|
|
3838
4103
|
return c.json({ error: "Scheduling not initialized" }, 503);
|
|
3839
4104
|
const body = await c.req.json();
|
|
3840
4105
|
if (!body.type || !body.title) {
|
|
3841
|
-
return
|
|
4106
|
+
return badRequest("type and title required");
|
|
3842
4107
|
}
|
|
3843
4108
|
const block = await store.create(body);
|
|
3844
4109
|
return c.json(block, 201);
|
|
@@ -3852,7 +4117,7 @@ app.patch("/api/scheduling/blocks/:id", async (c) => {
|
|
|
3852
4117
|
const body = await c.req.json();
|
|
3853
4118
|
const updated = await store.update(id, body);
|
|
3854
4119
|
if (!updated)
|
|
3855
|
-
return
|
|
4120
|
+
return notFound("Block not found");
|
|
3856
4121
|
return c.json(updated);
|
|
3857
4122
|
});
|
|
3858
4123
|
// Get today's schedule.
|
|
@@ -3880,7 +4145,7 @@ app.post("/api/contacts/entities", async (c) => {
|
|
|
3880
4145
|
return c.json({ error: "Contacts not initialized" }, 503);
|
|
3881
4146
|
const body = await c.req.json();
|
|
3882
4147
|
if (!body.type || !body.name) {
|
|
3883
|
-
return
|
|
4148
|
+
return badRequest("type and name required");
|
|
3884
4149
|
}
|
|
3885
4150
|
const entity = await store.createEntity(body);
|
|
3886
4151
|
return c.json(entity, 201);
|
|
@@ -3894,7 +4159,7 @@ app.patch("/api/contacts/entities/:id", async (c) => {
|
|
|
3894
4159
|
const body = await c.req.json();
|
|
3895
4160
|
const updated = await store.updateEntity(id, body);
|
|
3896
4161
|
if (!updated)
|
|
3897
|
-
return
|
|
4162
|
+
return notFound("Entity not found");
|
|
3898
4163
|
return c.json(updated);
|
|
3899
4164
|
});
|
|
3900
4165
|
// Get an entity's relationships (all edges where entity is from or to).
|
|
@@ -3913,7 +4178,7 @@ app.post("/api/contacts/edges", async (c) => {
|
|
|
3913
4178
|
return c.json({ error: "Contacts not initialized" }, 503);
|
|
3914
4179
|
const body = await c.req.json();
|
|
3915
4180
|
if (!body.from || !body.to || !body.type) {
|
|
3916
|
-
return
|
|
4181
|
+
return badRequest("from, to, and type required");
|
|
3917
4182
|
}
|
|
3918
4183
|
const edge = await store.createEdge(body);
|
|
3919
4184
|
return c.json(edge, 201);
|
|
@@ -3947,7 +4212,7 @@ app.get("/api/credentials/:id", async (c) => {
|
|
|
3947
4212
|
return c.json({ error: "Credentials not initialized" }, 503);
|
|
3948
4213
|
const cred = await store.get(c.req.param("id"));
|
|
3949
4214
|
if (!cred)
|
|
3950
|
-
return
|
|
4215
|
+
return notFound("Credential not found");
|
|
3951
4216
|
return c.json(cred);
|
|
3952
4217
|
});
|
|
3953
4218
|
// Create credential.
|
|
@@ -3957,7 +4222,7 @@ app.post("/api/credentials", async (c) => {
|
|
|
3957
4222
|
return c.json({ error: "Credentials not initialized" }, 503);
|
|
3958
4223
|
const body = await c.req.json();
|
|
3959
4224
|
if (!body.name || !body.service || !body.type || !body.value) {
|
|
3960
|
-
return
|
|
4225
|
+
return badRequest("name, service, type, and value required");
|
|
3961
4226
|
}
|
|
3962
4227
|
const cred = await store.create(body);
|
|
3963
4228
|
// Hydrate immediately if envVar set
|
|
@@ -3975,7 +4240,7 @@ app.patch("/api/credentials/:id", async (c) => {
|
|
|
3975
4240
|
const body = await c.req.json();
|
|
3976
4241
|
const updated = await store.update(id, body);
|
|
3977
4242
|
if (!updated)
|
|
3978
|
-
return
|
|
4243
|
+
return notFound("Credential not found");
|
|
3979
4244
|
// Re-hydrate if envVar changed
|
|
3980
4245
|
if (updated.envVar && updated.value && updated.status === "active") {
|
|
3981
4246
|
process.env[updated.envVar] = updated.value;
|
|
@@ -3990,7 +4255,7 @@ app.delete("/api/credentials/:id", async (c) => {
|
|
|
3990
4255
|
const id = c.req.param("id");
|
|
3991
4256
|
const archived = await store.archive(id);
|
|
3992
4257
|
if (!archived)
|
|
3993
|
-
return
|
|
4258
|
+
return notFound("Credential not found");
|
|
3994
4259
|
// Remove from process.env
|
|
3995
4260
|
if (archived.envVar) {
|
|
3996
4261
|
delete process.env[archived.envVar];
|
|
@@ -4116,17 +4381,17 @@ app.put("/api/open-loops/:id/resolve", async (c) => {
|
|
|
4116
4381
|
const body = await c.req.json().catch(() => ({}));
|
|
4117
4382
|
const updated = await transitionLoop(id, "resonant", body.resolvedBy);
|
|
4118
4383
|
if (!updated)
|
|
4119
|
-
return
|
|
4384
|
+
return notFound("Loop not found");
|
|
4120
4385
|
return c.json({ ok: true, loop: updated });
|
|
4121
4386
|
});
|
|
4122
4387
|
// Trigger fold-back for a session.
|
|
4123
4388
|
app.post("/api/open-loops/foldback", async (c) => {
|
|
4124
4389
|
const body = await c.req.json();
|
|
4125
4390
|
if (!body.sessionId)
|
|
4126
|
-
return
|
|
4391
|
+
return badRequest("sessionId required");
|
|
4127
4392
|
const cs = chatSessions.get(body.sessionId);
|
|
4128
4393
|
if (!cs)
|
|
4129
|
-
return
|
|
4394
|
+
return notFound("Session not found");
|
|
4130
4395
|
const result = await foldBack({
|
|
4131
4396
|
history: cs.history,
|
|
4132
4397
|
historySummary: cs.historySummary || undefined,
|
|
@@ -4162,7 +4427,7 @@ app.get("/api/metrics", async (c) => {
|
|
|
4162
4427
|
app.get("/api/metrics/summary", async (c) => {
|
|
4163
4428
|
const name = c.req.query("name");
|
|
4164
4429
|
if (!name)
|
|
4165
|
-
return
|
|
4430
|
+
return badRequest("name parameter required");
|
|
4166
4431
|
const windowMs = parseInt(c.req.query("window") ?? "60000", 10);
|
|
4167
4432
|
const summary = await metricsStore.summarize(name, { windowMs });
|
|
4168
4433
|
return c.json({ summary });
|
|
@@ -4176,7 +4441,7 @@ app.get("/api/metrics/names", async (c) => {
|
|
|
4176
4441
|
app.get("/api/metrics/series", async (c) => {
|
|
4177
4442
|
const name = c.req.query("name");
|
|
4178
4443
|
if (!name)
|
|
4179
|
-
return
|
|
4444
|
+
return badRequest("name parameter required");
|
|
4180
4445
|
const now = Date.now();
|
|
4181
4446
|
const since = c.req.query("since") ?? new Date(now - 60 * 60 * 1000).toISOString();
|
|
4182
4447
|
const until = c.req.query("until") ?? new Date(now).toISOString();
|
|
@@ -4257,7 +4522,7 @@ app.get("/api/metrics/firewall/compare", async (c) => {
|
|
|
4257
4522
|
const afterSince = c.req.query("after_since");
|
|
4258
4523
|
const afterUntil = c.req.query("after_until");
|
|
4259
4524
|
if (!beforeSince || !beforeUntil || !afterSince) {
|
|
4260
|
-
return
|
|
4525
|
+
return badRequest("Required: before_since, before_until, after_since");
|
|
4261
4526
|
}
|
|
4262
4527
|
const report = await generateComparisonReport(metricsStore, { since: beforeSince, until: beforeUntil }, { since: afterSince, until: afterUntil ?? new Date().toISOString() });
|
|
4263
4528
|
return c.text(report);
|
|
@@ -4322,6 +4587,10 @@ app.get("/board", requireSurface("pages"), async (c) => {
|
|
|
4322
4587
|
const html = await serveHtmlTemplate(join(UI_DIR, "board.html"));
|
|
4323
4588
|
return c.html(html);
|
|
4324
4589
|
});
|
|
4590
|
+
app.get("/whiteboard", async (c) => {
|
|
4591
|
+
const html = await serveHtmlTemplate(join(UI_DIR, "whiteboard.html"));
|
|
4592
|
+
return c.html(html);
|
|
4593
|
+
});
|
|
4325
4594
|
app.get("/library", requireSurface("pages"), async (c) => {
|
|
4326
4595
|
const html = await serveHtmlTemplate(join(UI_DIR, "library.html"));
|
|
4327
4596
|
return c.html(html);
|
|
@@ -4356,7 +4625,7 @@ app.get("/api/roadmap", async (c) => {
|
|
|
4356
4625
|
app.get("/api/roadmap/recent", async (c) => {
|
|
4357
4626
|
const hours = parseInt(c.req.query("hours") || "24", 10);
|
|
4358
4627
|
if (isNaN(hours) || hours < 1 || hours > 168) {
|
|
4359
|
-
return
|
|
4628
|
+
return badRequest("hours must be between 1 and 168");
|
|
4360
4629
|
}
|
|
4361
4630
|
try {
|
|
4362
4631
|
const { gitAvailable } = await import("./utils/git.js");
|
|
@@ -4425,12 +4694,12 @@ app.get("/life", async (c) => {
|
|
|
4425
4694
|
app.get("/api/browse", async (c) => {
|
|
4426
4695
|
const url = c.req.query("url");
|
|
4427
4696
|
if (!url)
|
|
4428
|
-
return
|
|
4697
|
+
return badRequest("url parameter required");
|
|
4429
4698
|
try {
|
|
4430
4699
|
new URL(url);
|
|
4431
4700
|
}
|
|
4432
4701
|
catch {
|
|
4433
|
-
return
|
|
4702
|
+
return badRequest("Invalid URL");
|
|
4434
4703
|
}
|
|
4435
4704
|
const result = await _browse.browseUrl(url);
|
|
4436
4705
|
if (!result) {
|
|
@@ -4442,7 +4711,7 @@ app.get("/api/browse", async (c) => {
|
|
|
4442
4711
|
app.post("/api/share", async (c) => {
|
|
4443
4712
|
const { email, note } = await c.req.json();
|
|
4444
4713
|
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
4445
|
-
return
|
|
4714
|
+
return badRequest("Valid email address required");
|
|
4446
4715
|
}
|
|
4447
4716
|
const resendKey = process.env.RESEND_API_KEY;
|
|
4448
4717
|
if (!resendKey) {
|
|
@@ -4604,7 +4873,7 @@ app.post("/api/ops/sidecars/:name/restart", async (c) => {
|
|
|
4604
4873
|
ok = await _avatarSidecar?.startAvatarSidecar() ?? false;
|
|
4605
4874
|
break;
|
|
4606
4875
|
default:
|
|
4607
|
-
return
|
|
4876
|
+
return badRequest(`Unknown sidecar: ${name}`);
|
|
4608
4877
|
}
|
|
4609
4878
|
logActivity({ source: "system", summary: `Restarted sidecar: ${name} (${ok ? "up" : "failed"})` });
|
|
4610
4879
|
return c.json({ name, available: ok });
|
|
@@ -4693,7 +4962,7 @@ app.patch("/api/ops/queue/:id", async (c) => {
|
|
|
4693
4962
|
const body = await c.req.json();
|
|
4694
4963
|
const updated = await store.update(id, body);
|
|
4695
4964
|
if (!updated)
|
|
4696
|
-
return
|
|
4965
|
+
return notFound("Task not found");
|
|
4697
4966
|
return c.json(updated);
|
|
4698
4967
|
});
|
|
4699
4968
|
// --- Project endpoints ---
|
|
@@ -4713,7 +4982,7 @@ app.post("/api/ops/projects", async (c) => {
|
|
|
4713
4982
|
const body = await c.req.json();
|
|
4714
4983
|
const { name, prefix, description } = body;
|
|
4715
4984
|
if (!name || !prefix)
|
|
4716
|
-
return
|
|
4985
|
+
return badRequest("name and prefix required");
|
|
4717
4986
|
try {
|
|
4718
4987
|
const project = await projectStore.create({ name, prefix, description });
|
|
4719
4988
|
return c.json(project, 201);
|
|
@@ -4731,7 +5000,7 @@ app.patch("/api/ops/projects/:id", async (c) => {
|
|
|
4731
5000
|
const body = await c.req.json();
|
|
4732
5001
|
const updated = await projectStore.update(id, body);
|
|
4733
5002
|
if (!updated)
|
|
4734
|
-
return
|
|
5003
|
+
return notFound("Project not found");
|
|
4735
5004
|
return c.json(updated);
|
|
4736
5005
|
});
|
|
4737
5006
|
app.delete("/api/ops/projects/:id", async (c) => {
|
|
@@ -4751,7 +5020,7 @@ app.delete("/api/ops/projects/:id", async (c) => {
|
|
|
4751
5020
|
}
|
|
4752
5021
|
const ok = await projectStore.delete(id);
|
|
4753
5022
|
if (!ok)
|
|
4754
|
-
return
|
|
5023
|
+
return notFound("Project not found");
|
|
4755
5024
|
return c.json({ ok: true });
|
|
4756
5025
|
});
|
|
4757
5026
|
// --- Posture API (UI surface assembly) ---
|
|
@@ -4817,7 +5086,7 @@ app.post("/api/nerve/subscribe", async (c) => {
|
|
|
4817
5086
|
const body = await c.req.json();
|
|
4818
5087
|
const { subscription, label } = body;
|
|
4819
5088
|
if (!subscription?.endpoint || !subscription?.keys) {
|
|
4820
|
-
return
|
|
5089
|
+
return badRequest("Invalid subscription");
|
|
4821
5090
|
}
|
|
4822
5091
|
const id = await addSubscription(subscription, label);
|
|
4823
5092
|
return c.json({ id });
|
|
@@ -4876,7 +5145,7 @@ app.post("/api/nerve/accept-update", async (c) => {
|
|
|
4876
5145
|
app.get("/api/chat/poll", async (c) => {
|
|
4877
5146
|
const sessionId = c.req.query("sessionId") || c.req.header("x-session-id");
|
|
4878
5147
|
if (!sessionId)
|
|
4879
|
-
return
|
|
5148
|
+
return badRequest("sessionId required");
|
|
4880
5149
|
const since = parseInt(c.req.query("since") || "0", 10);
|
|
4881
5150
|
const cs = chatSessions.get(sessionId) || (chatSessions.size > 0 ? chatSessions.values().next().value : null);
|
|
4882
5151
|
if (!cs)
|
|
@@ -4889,6 +5158,8 @@ app.get("/api/chat/poll", async (c) => {
|
|
|
4889
5158
|
role: m.role,
|
|
4890
5159
|
content: typeof m.content === "string" ? m.content : JSON.stringify(m.content),
|
|
4891
5160
|
source: m.source || "pc",
|
|
5161
|
+
...(m.toolsUsed ? { toolsUsed: m.toolsUsed } : {}),
|
|
5162
|
+
...(m.agentsUsed ? { agentsUsed: m.agentsUsed } : {}),
|
|
4892
5163
|
}));
|
|
4893
5164
|
return c.json({ messages: newMsgs, total });
|
|
4894
5165
|
});
|
|
@@ -4897,11 +5168,11 @@ app.post("/api/chat", async (c) => {
|
|
|
4897
5168
|
const body = await c.req.json();
|
|
4898
5169
|
const { sessionId, message, images } = body;
|
|
4899
5170
|
if (!sessionId || !message) {
|
|
4900
|
-
return
|
|
5171
|
+
return badRequest("sessionId and message required");
|
|
4901
5172
|
}
|
|
4902
5173
|
const session = validateSession(sessionId);
|
|
4903
5174
|
if (!session) {
|
|
4904
|
-
return
|
|
5175
|
+
return unauthorized("Invalid or expired session");
|
|
4905
5176
|
}
|
|
4906
5177
|
const cs = await getOrCreateChatSession(sessionId, session.name);
|
|
4907
5178
|
// Route to thread history if threadId is provided
|
|
@@ -5231,14 +5502,16 @@ app.post("/api/chat", async (c) => {
|
|
|
5231
5502
|
visualEntries = await searchVisualMemories(chatMessage, maxImages);
|
|
5232
5503
|
}
|
|
5233
5504
|
if (visualEntries.length > 0) {
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5505
|
+
// Inject text descriptions only — the model already analyzed images on upload.
|
|
5506
|
+
// Sending raw base64 every turn wastes tokens and slows responses.
|
|
5507
|
+
const descriptions = visualEntries
|
|
5508
|
+
.map((e) => {
|
|
5509
|
+
const desc = e.meta?.description ?? e.content;
|
|
5510
|
+
return `[Visual memory from ${e.createdAt}]: ${desc}`;
|
|
5511
|
+
})
|
|
5512
|
+
.slice(0, maxImages);
|
|
5513
|
+
if (descriptions.length > 0) {
|
|
5514
|
+
ctx.messages.splice(1, 0, { role: "user", content: descriptions.join("\n") });
|
|
5242
5515
|
}
|
|
5243
5516
|
}
|
|
5244
5517
|
}
|
|
@@ -5693,6 +5966,10 @@ app.post("/api/chat", async (c) => {
|
|
|
5693
5966
|
resolve();
|
|
5694
5967
|
};
|
|
5695
5968
|
reqSignal?.addEventListener("abort", onAbort, { once: true });
|
|
5969
|
+
// Track tool usage for history persistence
|
|
5970
|
+
const toolsUsedInTurn = [];
|
|
5971
|
+
// Track spawned agents for history persistence
|
|
5972
|
+
const agentsSpawnedInTurn = [];
|
|
5696
5973
|
// Token buffer for split-placeholder rehydration
|
|
5697
5974
|
let tokenBuf2 = "";
|
|
5698
5975
|
const streamStartMs2 = performance.now();
|
|
@@ -5706,10 +5983,28 @@ app.post("/api/chat", async (c) => {
|
|
|
5706
5983
|
tokenBuf2 = "";
|
|
5707
5984
|
stream.writeSSE({ data: JSON.stringify({ token: rehydrated }) }).catch(() => { });
|
|
5708
5985
|
};
|
|
5709
|
-
|
|
5986
|
+
// Initialize tool registry for this session (kept for when tool layer is re-enabled)
|
|
5987
|
+
const toolHandlerCtx = {
|
|
5988
|
+
brainDir: BRAIN_DIR,
|
|
5989
|
+
encryptionKey: sessionKeys.get(sessionId) ?? undefined,
|
|
5990
|
+
getBrain: () => cs.brain,
|
|
5991
|
+
};
|
|
5992
|
+
const chatToolRegistry = new ToolRegistry();
|
|
5993
|
+
chatToolRegistry.registerAll(createToolHandlers(toolHandlerCtx));
|
|
5994
|
+
streamWithTools({
|
|
5995
|
+
streamFn: stream_fn,
|
|
5710
5996
|
messages: redactedMessages,
|
|
5711
5997
|
model: activeChatModel,
|
|
5712
5998
|
signal: reqSignal,
|
|
5999
|
+
registry: chatToolRegistry,
|
|
6000
|
+
tier: activeTier,
|
|
6001
|
+
onToolCall: (call) => {
|
|
6002
|
+
stream.writeSSE({ data: JSON.stringify({ toolCall: { id: call.id, name: call.name, arguments: call.arguments } }) }).catch(() => { });
|
|
6003
|
+
},
|
|
6004
|
+
onToolResult: (name, result, isError) => {
|
|
6005
|
+
toolsUsedInTurn.push({ name, isError });
|
|
6006
|
+
stream.writeSSE({ data: JSON.stringify({ toolResult: { name, result: result.slice(0, 500), isError } }) }).catch(() => { });
|
|
6007
|
+
},
|
|
5713
6008
|
onToken: (token) => {
|
|
5714
6009
|
tokenBuf2 += token;
|
|
5715
6010
|
// Hold if buffer ends with partial placeholder
|
|
@@ -5719,232 +6014,251 @@ app.post("/api/chat", async (c) => {
|
|
|
5719
6014
|
flushBuf2();
|
|
5720
6015
|
},
|
|
5721
6016
|
onDone: async () => {
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
const
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
6017
|
+
try {
|
|
6018
|
+
flushBuf2(); // flush remainder
|
|
6019
|
+
reqSignal?.removeEventListener("abort", onAbort);
|
|
6020
|
+
logLlmCall({
|
|
6021
|
+
ts: new Date().toISOString(), mode: "stream",
|
|
6022
|
+
provider: activeProvider, model: streamModel2,
|
|
6023
|
+
durationMs: Math.round(performance.now() - streamStartMs2),
|
|
6024
|
+
outputTokens: Math.ceil(fullResponse.length / 4), ok: true,
|
|
6025
|
+
});
|
|
6026
|
+
// Process action blocks BEFORE sending done — ensures SSE events reach client before stream closes.
|
|
6027
|
+
// ALWAYS strip [AGENT_REQUEST] blocks from response, even if they can't be parsed.
|
|
6028
|
+
// Capture raw block content first for logging/parsing.
|
|
6029
|
+
const rawAgentBlocks = [...fullResponse.matchAll(/\[AGENT_REQUEST\]\s*([\s\S]*?)\s*\[\/AGENT_REQUEST\]/g)];
|
|
6030
|
+
if (rawAgentBlocks.length > 0) {
|
|
6031
|
+
fullResponse = fullResponse.replace(/\s*\[AGENT_REQUEST\][\s\S]*?\[\/AGENT_REQUEST\]\s*/g, "").trim();
|
|
6032
|
+
agentLog.info(` Found ${rawAgentBlocks.length} AGENT_REQUEST block(s)`);
|
|
6033
|
+
let spawnCount = 0;
|
|
6034
|
+
for (const block of rawAgentBlocks) {
|
|
6035
|
+
const rawContent = block[1].trim();
|
|
6036
|
+
agentLog.info(` Block content: ${rawContent.slice(0, 300)}`);
|
|
6037
|
+
// Try to extract JSON from the block — may be wrapped in backticks, code fences, or prose
|
|
6038
|
+
let jsonStr = rawContent;
|
|
6039
|
+
// Strip code fence wrappers: ```json ... ``` or ``` ... ```
|
|
6040
|
+
jsonStr = jsonStr.replace(/^`{3,}(?:json)?\s*/i, "").replace(/\s*`{3,}$/i, "");
|
|
6041
|
+
// Strip inline backticks
|
|
6042
|
+
jsonStr = jsonStr.replace(/^`+|`+$/g, "");
|
|
6043
|
+
// Try to find a JSON object in the content
|
|
6044
|
+
const jsonMatch = jsonStr.match(/\{[\s\S]*\}/);
|
|
6045
|
+
if (!jsonMatch) {
|
|
6046
|
+
agentLog.error(` No JSON object found in block. Raw content: ${rawContent.slice(0, 300)}`);
|
|
6047
|
+
logActivity({ source: "agent", summary: `AGENT_REQUEST has no JSON`, detail: rawContent.slice(0, 300) });
|
|
6048
|
+
continue;
|
|
6049
|
+
}
|
|
6050
|
+
try {
|
|
6051
|
+
const req = JSON.parse(jsonMatch[0]);
|
|
6052
|
+
if (req.prompt) {
|
|
6053
|
+
let finalPrompt = req.prompt;
|
|
6054
|
+
const label = req.label || finalPrompt.slice(0, 60);
|
|
6055
|
+
// Guard: detect vague prompts and prepend grounding instructions
|
|
6056
|
+
const hasFilePath = /(?:src\/|brain\/|public\/|\.ts|\.js|\.md|\.json|\.yaml|\.yml)/.test(finalPrompt);
|
|
6057
|
+
const isVague = /\b(?:comprehensive|robust|production-ready|enterprise|scalable|world-class)\b/i.test(finalPrompt)
|
|
6058
|
+
&& !hasFilePath;
|
|
6059
|
+
const isWishList = (finalPrompt.match(/^\d+\.\s/gm) || []).length >= 5 && !hasFilePath;
|
|
6060
|
+
if (isVague || isWishList) {
|
|
6061
|
+
agentLog.warn(` Vague prompt detected, adding grounding preamble: ${label}`);
|
|
6062
|
+
finalPrompt = [
|
|
6063
|
+
`IMPORTANT: The original request below is vague. Do NOT try to build everything listed.`,
|
|
6064
|
+
`Instead: 1) Read the existing codebase (start with src/ and package.json) to understand what exists.`,
|
|
6065
|
+
`2) Pick ONE small, concrete piece you can actually implement that connects to existing code.`,
|
|
6066
|
+
`3) Build that one thing well, with tests if a test framework exists.`,
|
|
6067
|
+
`4) If nothing concrete can be built without more requirements, just create a brief spec document at brain/knowledge/notes/ describing what decisions are needed and exit.`,
|
|
6068
|
+
``,
|
|
6069
|
+
`Original request:`,
|
|
6070
|
+
finalPrompt,
|
|
6071
|
+
].join("\n");
|
|
6072
|
+
}
|
|
6073
|
+
spawnCount++;
|
|
6074
|
+
agentLog.info(` Spawning: ${label}${(isVague || isWishList) ? " (grounded)" : ""}`);
|
|
6075
|
+
// Await task submission so we can send the real task ID to the client
|
|
6076
|
+
try {
|
|
6077
|
+
const task = await submitTask({
|
|
6078
|
+
label,
|
|
6079
|
+
prompt: finalPrompt,
|
|
6080
|
+
origin: "ai",
|
|
6081
|
+
sessionId,
|
|
6082
|
+
boardTaskId: req.taskId,
|
|
6083
|
+
});
|
|
6084
|
+
stream.writeSSE({ data: JSON.stringify({ agentSpawned: { label, taskId: task.id } }) }).catch(() => { });
|
|
6085
|
+
agentsSpawnedInTurn.push({ label, taskId: task.id });
|
|
6086
|
+
logActivity({ source: "agent", summary: `AI-triggered agent: ${task.label}`, detail: `Task ${task.id}, PID ${task.pid}`, actionLabel: "PROMPTED", reason: "user chat triggered agent" });
|
|
6087
|
+
}
|
|
6088
|
+
catch (err) {
|
|
6089
|
+
agentLog.error(`Spawn failed for "${label}": ${err.message}`);
|
|
6090
|
+
logActivity({ source: "agent", summary: `AI agent spawn failed: ${err.message}`, actionLabel: "PROMPTED", reason: "user chat triggered agent spawn failed" });
|
|
6091
|
+
stream.writeSSE({ data: JSON.stringify({ agentError: { label, error: err.message } }) }).catch(() => { });
|
|
6092
|
+
}
|
|
5790
6093
|
}
|
|
5791
|
-
|
|
5792
|
-
agentLog.
|
|
5793
|
-
logActivity({ source: "agent", summary: `AI agent spawn failed: ${err.message}`, actionLabel: "PROMPTED", reason: "user chat triggered agent spawn failed" });
|
|
5794
|
-
stream.writeSSE({ data: JSON.stringify({ agentError: { label, error: err.message } }) }).catch(() => { });
|
|
6094
|
+
else {
|
|
6095
|
+
agentLog.warn(`Parsed JSON but missing "prompt" field: ${jsonMatch[0].slice(0, 200)}`);
|
|
5795
6096
|
}
|
|
5796
6097
|
}
|
|
5797
|
-
|
|
5798
|
-
|
|
6098
|
+
catch (err) {
|
|
6099
|
+
const snippet = jsonMatch[0].slice(0, 300).replace(/\n/g, " ");
|
|
6100
|
+
agentLog.error(` JSON parse failed: ${snippet}`);
|
|
6101
|
+
logActivity({ source: "agent", summary: `AGENT_REQUEST parse error`, detail: snippet });
|
|
5799
6102
|
}
|
|
5800
6103
|
}
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
agentLog.error(` JSON parse failed: ${snippet}`);
|
|
5804
|
-
logActivity({ source: "agent", summary: `AGENT_REQUEST parse error`, detail: snippet });
|
|
6104
|
+
if (spawnCount === 0) {
|
|
6105
|
+
agentLog.warn(` ${rawAgentBlocks.length} block(s) found but 0 spawned — check server logs for block content`);
|
|
5805
6106
|
}
|
|
5806
6107
|
}
|
|
5807
|
-
if (
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
6108
|
+
// Check if AI requested board VIEW (read-only card rendering)
|
|
6109
|
+
// Parsed here (not by registry) because BOARD_VIEW uses a different tag than BOARD_ACTION,
|
|
6110
|
+
// but execution is routed through the board capability's "view" action.
|
|
6111
|
+
const boardViewRe = /\[BOARD_VIEW\]\s*(\{[\s\S]*?\})\s*(?:\[\/BOARD_VIEW\])?/g;
|
|
6112
|
+
const boardViewBlocks = [...fullResponse.matchAll(boardViewRe)];
|
|
6113
|
+
const boardViewPromises = [];
|
|
6114
|
+
const boardViewResults = [];
|
|
6115
|
+
if (boardViewBlocks.length > 0) {
|
|
6116
|
+
// Strip blocks from visible response
|
|
6117
|
+
fullResponse = fullResponse.replace(/\s*\[BOARD_VIEW\][\s\S]*?\[\/BOARD_VIEW\]\s*/g, "").trim();
|
|
6118
|
+
fullResponse = fullResponse.replace(/\s*\[BOARD_VIEW\]\s*\{[\s\S]*?\}\s*/g, "").trim();
|
|
6119
|
+
for (const block of boardViewBlocks) {
|
|
6120
|
+
try {
|
|
6121
|
+
const req = JSON.parse(block[1]);
|
|
6122
|
+
boardViewPromises.push((async () => {
|
|
6123
|
+
try {
|
|
6124
|
+
const result = await boardCapability.execute({ action: "view", ...req }, { origin: "chat" });
|
|
6125
|
+
const issues = result.data;
|
|
6126
|
+
const viewLabel = req.stateType || req.filter || "board";
|
|
6127
|
+
if (result.ok && issues && issues.length > 0) {
|
|
6128
|
+
boardViewResults.push({ query: viewLabel, issues });
|
|
6129
|
+
stream.writeSSE({
|
|
6130
|
+
data: JSON.stringify({ boardItems: { issues: issuesToCardPayload(issues) } }),
|
|
6131
|
+
}).catch(() => { });
|
|
6132
|
+
}
|
|
6133
|
+
else if (result.ok) {
|
|
6134
|
+
boardViewResults.push({ query: viewLabel, issues: [] });
|
|
6135
|
+
// Empty result — send a system message so the agent/user know
|
|
6136
|
+
stream.writeSSE({
|
|
6137
|
+
data: JSON.stringify({ boardItems: { issues: [], empty: true } }),
|
|
6138
|
+
}).catch(() => { });
|
|
6139
|
+
}
|
|
5835
6140
|
}
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
// Empty result — send a system message so the agent/user know
|
|
5839
|
-
stream.writeSSE({
|
|
5840
|
-
data: JSON.stringify({ boardItems: { issues: [], empty: true } }),
|
|
5841
|
-
}).catch(() => { });
|
|
6141
|
+
catch {
|
|
6142
|
+
log.warn("BOARD_VIEW fetch error");
|
|
5842
6143
|
}
|
|
5843
|
-
}
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
}
|
|
5848
|
-
}
|
|
5849
|
-
catch {
|
|
5850
|
-
log.warn("BOARD_VIEW parse error");
|
|
6144
|
+
})());
|
|
6145
|
+
}
|
|
6146
|
+
catch {
|
|
6147
|
+
log.warn("BOARD_VIEW parse error");
|
|
6148
|
+
}
|
|
5851
6149
|
}
|
|
5852
6150
|
}
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
6151
|
+
// Process capability action blocks (board, calendar, email, docs) via registry
|
|
6152
|
+
{
|
|
6153
|
+
const capReg = getCapabilityRegistry();
|
|
6154
|
+
if (capReg) {
|
|
6155
|
+
const blocks = capReg.parseActionBlocks(fullResponse);
|
|
6156
|
+
if (blocks.length > 0) {
|
|
6157
|
+
fullResponse = capReg.stripActionBlocks(fullResponse);
|
|
6158
|
+
// Fire-and-forget: execute action blocks
|
|
6159
|
+
(async () => {
|
|
6160
|
+
for (const block of blocks) {
|
|
6161
|
+
if (!block.payload)
|
|
6162
|
+
continue;
|
|
6163
|
+
const def = capReg.get(block.capabilityId);
|
|
6164
|
+
if (!def || def.pattern !== "action")
|
|
6165
|
+
continue;
|
|
6166
|
+
try {
|
|
6167
|
+
await def.execute(block.payload, { origin: "chat" });
|
|
6168
|
+
}
|
|
6169
|
+
catch { }
|
|
5871
6170
|
}
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
})();
|
|
6171
|
+
})();
|
|
6172
|
+
}
|
|
5875
6173
|
}
|
|
5876
6174
|
}
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
userMessage: message,
|
|
5888
|
-
provider: resolveProvider(),
|
|
5889
|
-
model: resolveUtilityModel(),
|
|
5890
|
-
lastExtractionTurn: cs.lastExtractionTurn,
|
|
5891
|
-
currentTurn: cs.turnCount,
|
|
5892
|
-
}).then((result) => {
|
|
5893
|
-
if (result.extracted > 0) {
|
|
5894
|
-
logActivity({ source: "learn", summary: `Extracted ${result.extracted} fact(s)`, actionLabel: "PROMPTED", reason: "user conversation triggered learning" });
|
|
5895
|
-
cs.lastExtractionTurn = cs.turnCount;
|
|
6175
|
+
// Save assistant response to history (with AGENT_REQUEST + BOARD_ACTION + action blocks stripped)
|
|
6176
|
+
const historyEntry = { role: "assistant", content: fullResponse };
|
|
6177
|
+
if (toolsUsedInTurn.length > 0)
|
|
6178
|
+
historyEntry.toolsUsed = toolsUsedInTurn;
|
|
6179
|
+
if (agentsSpawnedInTurn.length > 0) {
|
|
6180
|
+
historyEntry.agentsUsed = agentsSpawnedInTurn.map((a) => ({
|
|
6181
|
+
label: a.label,
|
|
6182
|
+
status: "completed", // placeholder — frontend polls for real status
|
|
6183
|
+
taskId: a.taskId,
|
|
6184
|
+
}));
|
|
5896
6185
|
}
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
6186
|
+
cs.history.push(historyEntry);
|
|
6187
|
+
persistSession();
|
|
6188
|
+
// Fire-and-forget: extract learnable facts from conversation
|
|
6189
|
+
cs.turnCount++;
|
|
6190
|
+
const recentMessages = cs.history.slice(-4);
|
|
6191
|
+
extractAndLearn({
|
|
6192
|
+
brain: cs.brain,
|
|
6193
|
+
recentMessages,
|
|
6194
|
+
userMessage: message,
|
|
6195
|
+
provider: resolveProvider(),
|
|
6196
|
+
model: resolveUtilityModel(),
|
|
6197
|
+
lastExtractionTurn: cs.lastExtractionTurn,
|
|
6198
|
+
currentTurn: cs.turnCount,
|
|
6199
|
+
}).then((result) => {
|
|
6200
|
+
if (result.extracted > 0) {
|
|
6201
|
+
logActivity({ source: "learn", summary: `Extracted ${result.extracted} fact(s)`, actionLabel: "PROMPTED", reason: "user conversation triggered learning" });
|
|
6202
|
+
cs.lastExtractionTurn = cs.turnCount;
|
|
5911
6203
|
}
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
return;
|
|
5915
|
-
const filename = await _avatarClient.cacheVideo(mp4, wavBuffer);
|
|
5916
|
-
pushPendingVideo(filename);
|
|
5917
|
-
logActivity({ source: "avatar", summary: "Generated avatar video", actionLabel: "PROMPTED", reason: "user conversation triggered avatar" });
|
|
5918
|
-
}).catch((err) => {
|
|
5919
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
5920
|
-
logActivity({ source: "avatar", summary: `Avatar generation failed: ${msg}` });
|
|
5921
|
-
});
|
|
5922
|
-
}
|
|
5923
|
-
// Wait for board view fetches so boardItems SSE events reach the client
|
|
5924
|
-
// BEFORE resolve() closes the stream. Previously resolve() fired immediately,
|
|
5925
|
-
// racing with the async boardViewPromises — cards never reached the client.
|
|
5926
|
-
if (boardViewPromises.length > 0) {
|
|
5927
|
-
Promise.all(boardViewPromises).finally(() => {
|
|
5928
|
-
// Store board view results in history so next turn has context
|
|
5929
|
-
if (boardViewResults.length > 0) {
|
|
5930
|
-
const lines = boardViewResults.map((r) => {
|
|
5931
|
-
if (r.issues.length === 0) {
|
|
5932
|
-
return `Displayed 0 ${r.query} items to user.`;
|
|
5933
|
-
}
|
|
5934
|
-
const itemLines = r.issues.map((i) => `- ${i.identifier}: ${i.title} [${i.state || "unknown"}]`);
|
|
5935
|
-
return `Displayed ${r.issues.length} ${r.query} item(s) to user:\n${itemLines.join("\n")}`;
|
|
5936
|
-
});
|
|
5937
|
-
cs.history.push({
|
|
5938
|
-
role: "system",
|
|
5939
|
-
content: `[BOARD_VIEW_RESULT]\n${lines.join("\n")}`,
|
|
5940
|
-
});
|
|
5941
|
-
persistSession();
|
|
6204
|
+
if (result.error) {
|
|
6205
|
+
logActivity({ source: "learn", summary: `Extraction error: ${result.error}` });
|
|
5942
6206
|
}
|
|
6207
|
+
}).catch(() => { });
|
|
6208
|
+
// Fire-and-forget: generate avatar video (TTS → MuseTalk → MP4)
|
|
6209
|
+
if ((_avatarSidecar?.isAvatarAvailable() ?? false) && (_ttsClient?.isTtsAvailable() ?? false) && fullResponse) {
|
|
6210
|
+
const trimmedText = fullResponse.slice(0, 2000);
|
|
6211
|
+
_ttsClient.synthesize(trimmedText).then(async (wavBuffer) => {
|
|
6212
|
+
if (!wavBuffer)
|
|
6213
|
+
return;
|
|
6214
|
+
const cached = await _avatarClient.getCachedVideo(wavBuffer);
|
|
6215
|
+
if (cached) {
|
|
6216
|
+
pushPendingVideo(cached);
|
|
6217
|
+
return;
|
|
6218
|
+
}
|
|
6219
|
+
const mp4 = await _avatarClient.generateVideo(wavBuffer);
|
|
6220
|
+
if (!mp4)
|
|
6221
|
+
return;
|
|
6222
|
+
const filename = await _avatarClient.cacheVideo(mp4, wavBuffer);
|
|
6223
|
+
pushPendingVideo(filename);
|
|
6224
|
+
logActivity({ source: "avatar", summary: "Generated avatar video", actionLabel: "PROMPTED", reason: "user conversation triggered avatar" });
|
|
6225
|
+
}).catch((err) => {
|
|
6226
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6227
|
+
logActivity({ source: "avatar", summary: `Avatar generation failed: ${msg}` });
|
|
6228
|
+
});
|
|
6229
|
+
}
|
|
6230
|
+
// Wait for board view fetches so boardItems SSE events reach the client
|
|
6231
|
+
// BEFORE resolve() closes the stream. Previously resolve() fired immediately,
|
|
6232
|
+
// racing with the async boardViewPromises — cards never reached the client.
|
|
6233
|
+
if (boardViewPromises.length > 0) {
|
|
6234
|
+
Promise.all(boardViewPromises).finally(() => {
|
|
6235
|
+
// Store board view results in history so next turn has context
|
|
6236
|
+
if (boardViewResults.length > 0) {
|
|
6237
|
+
const lines = boardViewResults.map((r) => {
|
|
6238
|
+
if (r.issues.length === 0) {
|
|
6239
|
+
return `Displayed 0 ${r.query} items to user.`;
|
|
6240
|
+
}
|
|
6241
|
+
const itemLines = r.issues.map((i) => `- ${i.identifier}: ${i.title} [${i.state || "unknown"}]`);
|
|
6242
|
+
return `Displayed ${r.issues.length} ${r.query} item(s) to user:\n${itemLines.join("\n")}`;
|
|
6243
|
+
});
|
|
6244
|
+
cs.history.push({
|
|
6245
|
+
role: "system",
|
|
6246
|
+
content: `[BOARD_VIEW_RESULT]\n${lines.join("\n")}`,
|
|
6247
|
+
});
|
|
6248
|
+
persistSession();
|
|
6249
|
+
}
|
|
6250
|
+
stream.writeSSE({ data: JSON.stringify({ done: true }) }).catch(() => { });
|
|
6251
|
+
resolve();
|
|
6252
|
+
});
|
|
6253
|
+
}
|
|
6254
|
+
else {
|
|
5943
6255
|
stream.writeSSE({ data: JSON.stringify({ done: true }) }).catch(() => { });
|
|
5944
6256
|
resolve();
|
|
5945
|
-
}
|
|
6257
|
+
}
|
|
5946
6258
|
}
|
|
5947
|
-
|
|
6259
|
+
catch (doneErr) {
|
|
6260
|
+
log.error("onDone handler crashed", { error: doneErr?.message ?? String(doneErr), stack: doneErr?.stack?.slice(0, 300) });
|
|
6261
|
+
stream.writeSSE({ data: JSON.stringify({ error: "Internal error processing response", errorDetail: doneErr?.message }) }).catch(() => { });
|
|
5948
6262
|
stream.writeSSE({ data: JSON.stringify({ done: true }) }).catch(() => { });
|
|
5949
6263
|
resolve();
|
|
5950
6264
|
}
|
|
@@ -5983,7 +6297,7 @@ app.post("/api/chat", async (c) => {
|
|
|
5983
6297
|
}
|
|
5984
6298
|
resolve(); // Still resolve so stream closes
|
|
5985
6299
|
},
|
|
5986
|
-
});
|
|
6300
|
+
}).catch(reject);
|
|
5987
6301
|
});
|
|
5988
6302
|
});
|
|
5989
6303
|
});
|
|
@@ -5991,7 +6305,7 @@ app.post("/api/chat", async (c) => {
|
|
|
5991
6305
|
app.post("/api/freeze", async (c) => {
|
|
5992
6306
|
const body = await c.req.json().catch(() => null);
|
|
5993
6307
|
if (!body?.signal)
|
|
5994
|
-
return
|
|
6308
|
+
return badRequest("Missing freeze signal");
|
|
5995
6309
|
const { freeze, isFrozen } = await import("./tier/freeze.js");
|
|
5996
6310
|
if (isFrozen())
|
|
5997
6311
|
return c.json({ error: "Already frozen" }, 409);
|
|
@@ -6011,7 +6325,7 @@ app.get("/api/freeze/status", async (c) => {
|
|
|
6011
6325
|
app.post("/api/import/folder", async (c) => {
|
|
6012
6326
|
const body = await c.req.json();
|
|
6013
6327
|
if (!body.paths || !Array.isArray(body.paths) || body.paths.length === 0) {
|
|
6014
|
-
return
|
|
6328
|
+
return badRequest("paths[] required");
|
|
6015
6329
|
}
|
|
6016
6330
|
const { resolve } = await import("node:path");
|
|
6017
6331
|
const { importToBrain } = await import("./files/import.js");
|
|
@@ -6064,7 +6378,7 @@ app.post("/api/import/files", async (c) => {
|
|
|
6064
6378
|
const formData = await c.req.formData();
|
|
6065
6379
|
const files = formData.getAll("files");
|
|
6066
6380
|
if (files.length === 0)
|
|
6067
|
-
return
|
|
6381
|
+
return badRequest("No files provided");
|
|
6068
6382
|
const { mkdir: mkdirFs, writeFile: writeFs } = await import("node:fs/promises");
|
|
6069
6383
|
const { join: joinPath } = await import("node:path");
|
|
6070
6384
|
const ingestDir = joinPath(process.cwd(), "ingest");
|