chapterhouse 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/korg.agent.md +65 -0
- package/dist/api/agent-edit-access.js +11 -0
- package/dist/api/agents.api.test.js +48 -0
- package/dist/api/korg.js +34 -0
- package/dist/api/korg.test.js +42 -0
- package/dist/api/server.js +420 -13
- package/dist/api/server.test.js +533 -3
- package/dist/config.js +28 -0
- package/dist/config.test.js +20 -0
- package/dist/copilot/agent-event-bus.js +1 -0
- package/dist/copilot/agents.js +117 -50
- package/dist/copilot/agents.mcp-servers.test.js +87 -0
- package/dist/copilot/agents.parse.test.js +69 -0
- package/dist/copilot/agents.test.js +137 -2
- package/dist/copilot/orchestrator.js +62 -13
- package/dist/copilot/orchestrator.test.js +130 -8
- package/dist/copilot/session-manager.js +34 -0
- package/dist/copilot/system-message.js +11 -10
- package/dist/copilot/system-message.test.js +6 -1
- package/dist/copilot/tools.js +184 -376
- package/dist/copilot/tools.memory.test.js +32 -0
- package/dist/copilot/tools.wiki.test.js +53 -59
- package/dist/daemon.js +9 -0
- package/dist/memory/decisions.js +6 -5
- package/dist/memory/entities.js +20 -9
- package/dist/memory/hooks.js +151 -0
- package/dist/memory/hooks.test.js +325 -0
- package/dist/memory/hot-tier.js +37 -0
- package/dist/memory/hot-tier.test.js +30 -0
- package/dist/memory/housekeeping-scheduler.js +35 -0
- package/dist/memory/housekeeping-scheduler.test.js +50 -0
- package/dist/memory/inbox.js +10 -0
- package/dist/memory/index.js +3 -1
- package/dist/memory/migration.js +244 -0
- package/dist/memory/migration.test.js +100 -0
- package/dist/memory/reflect.js +273 -0
- package/dist/memory/reflect.test.js +254 -0
- package/dist/store/db.js +119 -4
- package/dist/store/db.test.js +19 -1
- package/dist/test/setup-env.js +3 -1
- package/dist/test/setup-env.test.js +8 -1
- package/dist/wiki/consolidation.js +641 -0
- package/dist/wiki/consolidation.test.js +140 -0
- package/dist/wiki/frontmatter.js +48 -0
- package/dist/wiki/frontmatter.test.js +42 -0
- package/dist/wiki/index-manager.js +246 -330
- package/dist/wiki/index-manager.test.js +138 -145
- package/dist/wiki/ingest.js +347 -0
- package/dist/wiki/ingest.test.js +111 -0
- package/dist/wiki/links.js +151 -0
- package/dist/wiki/links.test.js +176 -0
- package/dist/wiki/migrate-topics.test.js +16 -6
- package/dist/wiki/scheduler.js +118 -0
- package/dist/wiki/scheduler.test.js +64 -0
- package/dist/wiki/timeline.js +51 -0
- package/dist/wiki/timeline.test.js +65 -0
- package/dist/wiki/topic-structure.js +1 -1
- package/package.json +3 -1
- package/skills/pkb-ideas/SKILL.md +78 -0
- package/skills/pkb-ideas/_meta.json +4 -0
- package/skills/pkb-org/SKILL.md +82 -0
- package/skills/pkb-org/_meta.json +4 -0
- package/skills/pkb-people/SKILL.md +74 -0
- package/skills/pkb-people/_meta.json +4 -0
- package/skills/pkb-research/SKILL.md +83 -0
- package/skills/pkb-research/_meta.json +4 -0
- package/skills/pkb-source/SKILL.md +38 -0
- package/skills/pkb-source/_meta.json +4 -0
- package/skills/wiki-conventions/SKILL.md +5 -5
- package/web/dist/assets/index-5kz9aRU9.css +10 -0
- package/web/dist/assets/{index-B5oDsQ5y.js → index-BbX9RKf3.js} +101 -99
- package/web/dist/assets/index-BbX9RKf3.js.map +1 -0
- package/web/dist/index.html +2 -2
- package/dist/wiki/context.js +0 -138
- package/dist/wiki/fix.js +0 -335
- package/dist/wiki/fix.test.js +0 -350
- package/dist/wiki/lint.js +0 -451
- package/dist/wiki/lint.test.js +0 -329
- package/web/dist/assets/index-B5oDsQ5y.js.map +0 -1
- package/web/dist/assets/index-DknKAtDS.css +0 -10
package/dist/api/server.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import cors from "cors";
|
|
2
2
|
import express from "express";
|
|
3
3
|
import helmet from "helmet";
|
|
4
|
-
import { existsSync } from "fs";
|
|
5
|
-
import { join, dirname } from "path";
|
|
4
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
5
|
+
import { join, dirname, resolve, sep } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
import { z } from "zod";
|
|
8
|
-
import { sendToOrchestrator, interruptCurrentTurn, enqueueForSse,
|
|
8
|
+
import { sendToOrchestrator, interruptCurrentTurn, enqueueForSse, cancelCurrentMessage, interruptSessionTurn, getLastRouteResult, getCurrentSessionKey, getPersistentAgentSessionState, reloadPersistentAgent } from "../copilot/orchestrator.js";
|
|
9
9
|
import { agentEventBus } from "../copilot/agent-event-bus.js";
|
|
10
|
-
import { ensureDefaultAgents, getAgentRegistry, loadAgents } from "../copilot/agents.js";
|
|
10
|
+
import { ensureDefaultAgents, getAgent, getAgentRegistry, isBuiltinAgent, loadAgents, notifyAgentSaved, parseAgentMd, parseAgentMdOrThrow, serializeAgentMd, setAgentSaveRuntimeHooks, SLUG_REGEX, } from "../copilot/agents.js";
|
|
11
11
|
import { config, persistModel } from "../config.js";
|
|
12
12
|
import { ModeContext } from "../mode-context.js";
|
|
13
13
|
import { getRouterConfig, updateRouterConfig } from "../copilot/router.js";
|
|
14
14
|
import { searchIndex, parseIndex } from "../wiki/index-manager.js";
|
|
15
15
|
import { createAuthMiddleware, getBootstrapAuthResponse } from "./auth.js";
|
|
16
|
+
import { assertAgentEditAccess } from "./agent-edit-access.js";
|
|
16
17
|
import { createConcurrentConnectionLimiter, createFixedWindowRateLimiter } from "./rate-limit.js";
|
|
17
18
|
import { createTeamRouter } from "./team.js";
|
|
18
19
|
import { writePage, deletePage, pageExists, listPages, ensureWikiStructure, assertPagePath, } from "../wiki/fs.js";
|
|
@@ -23,7 +24,7 @@ import { readWikiPage, teamWikiSync } from "../wiki/team-sync.js";
|
|
|
23
24
|
import { withWikiWrite } from "../wiki/lock.js";
|
|
24
25
|
import { listSkills, removeSkill } from "../copilot/skills.js";
|
|
25
26
|
import { restartDaemon } from "../daemon.js";
|
|
26
|
-
import { API_TOKEN_PATH } from "../paths.js";
|
|
27
|
+
import { AGENTS_DIR, API_TOKEN_PATH } from "../paths.js";
|
|
27
28
|
import { getCurrentRunId, getDb, getSessionMessages, getTaskEvents } from "../store/db.js";
|
|
28
29
|
import { getTaskLogEvents, subscribeTaskLog } from "../copilot/task-event-log.js";
|
|
29
30
|
import { subscribeSession, getSessionEventsFromDb, getSessionMaxSeqFromDb, oldestSessionSeq, } from "../copilot/turn-event-log.js";
|
|
@@ -32,8 +33,14 @@ import { formatSseData, formatSseEvent } from "./sse.js";
|
|
|
32
33
|
import { assertAuthenticationConfigured, createHealthPayload, createPublicConfigPayload, getDisplayHost, resolveApiToken, shouldServeSpaPath, } from "./server-runtime.js";
|
|
33
34
|
import { BadRequestError, ForbiddenError, InternalServerError, NotFoundError, apiNotFoundHandler, asBadRequest, createApiErrorHandler, parseRequest, } from "./errors.js";
|
|
34
35
|
import { childLogger } from "../util/logger.js";
|
|
35
|
-
import { getActiveScope } from "../memory/active-scope.js";
|
|
36
|
-
import { createScope, getScope } from "../memory/scopes.js";
|
|
36
|
+
import { getActiveScope, setActiveScope } from "../memory/active-scope.js";
|
|
37
|
+
import { createScope, getScope, listScopes } from "../memory/scopes.js";
|
|
38
|
+
import { handleGitCommitHook, handlePrMergeHook } from "../memory/hooks.js";
|
|
39
|
+
import { recordObservation } from "../memory/observations.js";
|
|
40
|
+
import { recordDecision } from "../memory/decisions.js";
|
|
41
|
+
import { upsertEntity } from "../memory/entities.js";
|
|
42
|
+
import { getInboxItem, listPendingInboxItems, resolveInboxItem } from "../memory/inbox.js";
|
|
43
|
+
import { listKorgResearchSessions, routeKorgMessage } from "./korg.js";
|
|
37
44
|
const log = childLogger("server");
|
|
38
45
|
const modeContext = new ModeContext(config);
|
|
39
46
|
void searchIndex; // re-exported by index-manager; reference here documents the dep
|
|
@@ -78,6 +85,22 @@ const autoRequestSchema = z.object({
|
|
|
78
85
|
const wikiWriteSchema = z.object({
|
|
79
86
|
content: z.string({ error: "Missing 'content' string in request body" }),
|
|
80
87
|
}).strict();
|
|
88
|
+
const agentPatchSchema = z.object({
|
|
89
|
+
name: requiredString("name must be a non-empty string").optional(),
|
|
90
|
+
description: requiredString("description must be a non-empty string").optional(),
|
|
91
|
+
model: requiredString("model must be a non-empty string").optional(),
|
|
92
|
+
systemPrompt: z.string().optional(),
|
|
93
|
+
}).passthrough().superRefine((value, ctx) => {
|
|
94
|
+
for (const key of ["slug", "scope", "tools", "skills"]) {
|
|
95
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
96
|
+
ctx.addIssue({
|
|
97
|
+
code: "custom",
|
|
98
|
+
path: [key],
|
|
99
|
+
message: `'${key}' is read-only`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
81
104
|
const projectCreateSchema = z.object({
|
|
82
105
|
slug: requiredString("Missing 'slug' in request body")
|
|
83
106
|
.regex(/^[a-z0-9][a-z0-9-]*$/, "Project slug must be a lowercase slug"),
|
|
@@ -90,6 +113,41 @@ const scopeCreateSchema = z.object({
|
|
|
90
113
|
title: requiredString("Missing 'title' in request body"),
|
|
91
114
|
description: z.string().optional(),
|
|
92
115
|
}).strict();
|
|
116
|
+
const setActiveScopeSchema = z.object({
|
|
117
|
+
scope: z.string().nullable(),
|
|
118
|
+
});
|
|
119
|
+
const memoryEntriesQuerySchema = z.object({
|
|
120
|
+
store: z.enum(["observations", "decisions", "entities", "action_items"]).optional(),
|
|
121
|
+
tier: z.enum(["hot", "warm", "cold"]).optional(),
|
|
122
|
+
});
|
|
123
|
+
const memoryRememberSchema = z.object({
|
|
124
|
+
content: requiredString("Missing 'content' in request body"),
|
|
125
|
+
kind: z.enum(["observation", "decision"]).optional(),
|
|
126
|
+
entity_name: z.string().optional(),
|
|
127
|
+
entity_kind: z.string().optional(),
|
|
128
|
+
title: z.string().optional(),
|
|
129
|
+
decided_at: z.string().optional(),
|
|
130
|
+
tier: z.enum(["hot", "warm", "cold"]).optional(),
|
|
131
|
+
});
|
|
132
|
+
const inboxRouteSchema = z.object({
|
|
133
|
+
action: z.enum(["accept", "reject", "route"]),
|
|
134
|
+
reason: z.string().optional(),
|
|
135
|
+
target_scope: z.string().optional(),
|
|
136
|
+
});
|
|
137
|
+
const gitCommitHookSchema = z.object({
|
|
138
|
+
message: requiredString("Missing 'message' in request body"),
|
|
139
|
+
stat: z.string().optional(),
|
|
140
|
+
});
|
|
141
|
+
const prMergeHookSchema = z.object({
|
|
142
|
+
number: z.number({ error: "Missing or invalid 'number' in request body" }).int().positive(),
|
|
143
|
+
title: requiredString("Missing 'title' in request body"),
|
|
144
|
+
body: z.string().optional(),
|
|
145
|
+
files_changed: z.array(z.string()).optional(),
|
|
146
|
+
});
|
|
147
|
+
const korgRequestSchema = z.object({
|
|
148
|
+
message: requiredString("Missing 'message' in request body"),
|
|
149
|
+
session_id: z.string().trim().min(1).optional(),
|
|
150
|
+
}).strict();
|
|
93
151
|
const projectHardRulesSchema = z.object({
|
|
94
152
|
hardRules: z.object({
|
|
95
153
|
auto_pr: z.boolean({ error: "hardRules.auto_pr must be a boolean" }),
|
|
@@ -284,6 +342,18 @@ Create your first page via the wiki UI or by editing files under \`pages/\`.
|
|
|
284
342
|
const sseClients = new Map();
|
|
285
343
|
const pendingSseMessages = [];
|
|
286
344
|
let connectionCounter = 0;
|
|
345
|
+
function broadcastSsePayload(payload) {
|
|
346
|
+
for (const [, res] of sseClients) {
|
|
347
|
+
res.write(formatSseData(payload));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
setAgentSaveRuntimeHooks({
|
|
351
|
+
getPersistentSessionState: getPersistentAgentSessionState,
|
|
352
|
+
reloadPersistentSession: reloadPersistentAgent,
|
|
353
|
+
emitAgentReloadEvent: (event) => {
|
|
354
|
+
broadcastSsePayload(event);
|
|
355
|
+
},
|
|
356
|
+
});
|
|
287
357
|
// ---------------------------------------------------------------------------
|
|
288
358
|
// Bootstrap — hands the API token to the same-origin SPA on first load.
|
|
289
359
|
// Loopback-only by IP / Origin check.
|
|
@@ -316,18 +386,145 @@ const handleHealth = (_req, res) => {
|
|
|
316
386
|
};
|
|
317
387
|
app.get("/status", handleHealth);
|
|
318
388
|
app.get("/health", handleHealth);
|
|
389
|
+
function getLoadedAgents() {
|
|
390
|
+
let agents = getAgentRegistry();
|
|
391
|
+
if (agents.length === 0) {
|
|
392
|
+
ensureDefaultAgents();
|
|
393
|
+
agents = loadAgents();
|
|
394
|
+
}
|
|
395
|
+
return agents;
|
|
396
|
+
}
|
|
397
|
+
function getAgentLastEdited(slug) {
|
|
398
|
+
try {
|
|
399
|
+
return statSync(join(AGENTS_DIR, `${slug}.agent.md`)).mtime.toISOString();
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
319
405
|
// ---------------------------------------------------------------------------
|
|
320
406
|
// Workers / agents
|
|
321
407
|
// ---------------------------------------------------------------------------
|
|
322
408
|
app.get("/api/agents", (_req, res) => {
|
|
323
|
-
|
|
409
|
+
const agents = getLoadedAgents()
|
|
410
|
+
.map((agent) => ({
|
|
411
|
+
name: agent.name,
|
|
412
|
+
slug: agent.slug,
|
|
413
|
+
description: agent.description,
|
|
414
|
+
model: agent.model,
|
|
415
|
+
scope: agent.scope ?? null,
|
|
416
|
+
type: isBuiltinAgent(agent.slug) ? "builtin" : "custom",
|
|
417
|
+
lastEdited: getAgentLastEdited(agent.slug),
|
|
418
|
+
}))
|
|
419
|
+
.sort((left, right) => {
|
|
420
|
+
if (left.type !== right.type) {
|
|
421
|
+
return left.type === "builtin" ? -1 : 1;
|
|
422
|
+
}
|
|
423
|
+
return left.name.localeCompare(right.name);
|
|
424
|
+
});
|
|
425
|
+
res.json(agents);
|
|
324
426
|
});
|
|
325
|
-
app.get("/api/
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
427
|
+
app.get("/api/agents/:slug", (req, res, next) => {
|
|
428
|
+
const slugParam = req.params.slug;
|
|
429
|
+
const slug = Array.isArray(slugParam) ? slugParam[0] : slugParam;
|
|
430
|
+
if (slug === "stream") {
|
|
431
|
+
next();
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (!SLUG_REGEX.test(slug)) {
|
|
435
|
+
res.status(400).json({ error: "Invalid slug" });
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
getLoadedAgents();
|
|
439
|
+
const filePath = join(AGENTS_DIR, `${slug}.agent.md`);
|
|
440
|
+
const resolvedAgentsDir = resolve(AGENTS_DIR);
|
|
441
|
+
const resolvedFilePath = resolve(filePath);
|
|
442
|
+
if (!(resolvedFilePath === resolvedAgentsDir || resolvedFilePath.startsWith(`${resolvedAgentsDir}${sep}`))) {
|
|
443
|
+
res.status(403).json({ error: "Access denied" });
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
let content;
|
|
447
|
+
try {
|
|
448
|
+
content = readFileSync(filePath, "utf-8");
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
res.status(404).json({ error: `Agent '${slug}' not found` });
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const agent = parseAgentMd(content, slug);
|
|
455
|
+
if (!agent) {
|
|
456
|
+
res.status(500).json({ error: `Agent '${slug}' could not be parsed` });
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const builtin = isBuiltinAgent(slug);
|
|
460
|
+
res.json({
|
|
461
|
+
name: agent.name,
|
|
462
|
+
slug: agent.slug,
|
|
463
|
+
description: agent.description,
|
|
464
|
+
model: agent.model,
|
|
465
|
+
scope: agent.scope ?? null,
|
|
466
|
+
persistent: agent.persistent ?? false,
|
|
467
|
+
skills: agent.skills ?? [],
|
|
468
|
+
type: builtin ? "builtin" : "custom",
|
|
469
|
+
editable: !builtin,
|
|
470
|
+
systemPrompt: agent.systemMessage,
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
app.patch("/api/agents/:slug", async (req, res) => {
|
|
474
|
+
const slugParam = req.params.slug;
|
|
475
|
+
const slug = Array.isArray(slugParam) ? slugParam[0] : slugParam;
|
|
476
|
+
if (!SLUG_REGEX.test(slug)) {
|
|
477
|
+
throw new BadRequestError("Invalid slug");
|
|
478
|
+
}
|
|
479
|
+
if (modeContext.isTeam() && req.user?.role !== "team-lead") {
|
|
480
|
+
throw new ForbiddenError("Forbidden");
|
|
481
|
+
}
|
|
482
|
+
const patch = parseRequest(agentPatchSchema, req.body ?? {});
|
|
483
|
+
const filePath = join(AGENTS_DIR, `${slug}.agent.md`);
|
|
484
|
+
const resolvedAgentsDir = resolve(AGENTS_DIR);
|
|
485
|
+
const resolvedFilePath = resolve(filePath);
|
|
486
|
+
if (!(resolvedFilePath === resolvedAgentsDir || resolvedFilePath.startsWith(`${resolvedAgentsDir}${sep}`))) {
|
|
487
|
+
throw new ForbiddenError("Access denied");
|
|
488
|
+
}
|
|
489
|
+
assertAgentEditAccess({ entraAuthEnabled: config.entraAuthEnabled }, req.user);
|
|
490
|
+
if (isBuiltinAgent(slug)) {
|
|
491
|
+
throw new ForbiddenError("Built-in agents are read-only");
|
|
492
|
+
}
|
|
493
|
+
if (!existsSync(filePath)) {
|
|
494
|
+
throw new NotFoundError("Agent not found");
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
const current = parseAgentMdOrThrow(readFileSync(filePath, "utf-8"), slug);
|
|
498
|
+
const updated = {
|
|
499
|
+
...current,
|
|
500
|
+
name: patch.name ?? current.name,
|
|
501
|
+
description: patch.description ?? current.description,
|
|
502
|
+
model: patch.model ?? current.model,
|
|
503
|
+
systemMessage: patch.systemPrompt ?? current.systemMessage,
|
|
504
|
+
};
|
|
505
|
+
const nextContent = serializeAgentMd(updated);
|
|
506
|
+
writeFileSync(filePath, nextContent, "utf-8");
|
|
507
|
+
await notifyAgentSaved(slug, updated);
|
|
508
|
+
res.json({
|
|
509
|
+
name: updated.name,
|
|
510
|
+
slug: updated.slug,
|
|
511
|
+
description: updated.description,
|
|
512
|
+
model: updated.model,
|
|
513
|
+
scope: updated.scope ?? null,
|
|
514
|
+
persistent: updated.persistent ?? false,
|
|
515
|
+
skills: updated.skills ?? [],
|
|
516
|
+
type: "custom",
|
|
517
|
+
editable: true,
|
|
518
|
+
systemPrompt: updated.systemMessage,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
523
|
+
throw new BadRequestError(`Invalid content: ${message}`);
|
|
330
524
|
}
|
|
525
|
+
});
|
|
526
|
+
app.get("/api/channels", (_req, res) => {
|
|
527
|
+
const agents = getLoadedAgents();
|
|
331
528
|
const persistentAgentChannels = agents
|
|
332
529
|
.filter((agent) => agent.persistent)
|
|
333
530
|
.map((agent) => ({
|
|
@@ -650,6 +847,21 @@ app.post("/api/cancel", async (_req, res) => {
|
|
|
650
847
|
}
|
|
651
848
|
res.json({ status: "ok", cancelled });
|
|
652
849
|
});
|
|
850
|
+
app.post("/api/agents/:slug/reload-confirm", async (req, res) => {
|
|
851
|
+
const slugParam = Array.isArray(req.params.slug) ? req.params.slug[0] : req.params.slug;
|
|
852
|
+
const slug = slugParam?.trim() || "";
|
|
853
|
+
const agent = getAgent(slug);
|
|
854
|
+
if (!agent?.persistent) {
|
|
855
|
+
throw new NotFoundError("Agent not found");
|
|
856
|
+
}
|
|
857
|
+
const sessionKey = `agent:${agent.slug}`;
|
|
858
|
+
await interruptSessionTurn(sessionKey);
|
|
859
|
+
const reloadResult = await reloadPersistentAgent(agent.slug);
|
|
860
|
+
if (reloadResult === "reloaded") {
|
|
861
|
+
broadcastSsePayload({ type: "agent_reloaded", slug: agent.slug, reason: "confirmed_restart" });
|
|
862
|
+
}
|
|
863
|
+
res.json({ status: "ok" });
|
|
864
|
+
});
|
|
653
865
|
// Cancel the active turn for one session key without touching other channels.
|
|
654
866
|
app.post("/api/session/:sessionKey/interrupt", async (req, res) => {
|
|
655
867
|
const sessionKey = Array.isArray(req.params.sessionKey)
|
|
@@ -901,6 +1113,193 @@ app.get("/api/memory/active-scope", (_req, res) => {
|
|
|
901
1113
|
title: activeScope.title,
|
|
902
1114
|
});
|
|
903
1115
|
});
|
|
1116
|
+
app.post("/api/memory/active-scope", (req, res) => {
|
|
1117
|
+
const body = parseRequest(setActiveScopeSchema, req.body ?? {});
|
|
1118
|
+
try {
|
|
1119
|
+
const scope = setActiveScope(body.scope);
|
|
1120
|
+
res.json({ ok: true, scope: scope?.slug ?? null });
|
|
1121
|
+
}
|
|
1122
|
+
catch (err) {
|
|
1123
|
+
res.status(404).json({ error: err instanceof Error ? err.message : String(err) });
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
app.get("/api/memory/scopes", (_req, res) => {
|
|
1127
|
+
const db = getDb();
|
|
1128
|
+
const activeScope = getActiveScope();
|
|
1129
|
+
const scopes = listScopes();
|
|
1130
|
+
const result = scopes.map((scope) => {
|
|
1131
|
+
const counts = {
|
|
1132
|
+
observations: db.prepare(`SELECT COUNT(*) AS count FROM mem_observations WHERE scope_id = ?`).get(scope.id).count,
|
|
1133
|
+
decisions: db.prepare(`SELECT COUNT(*) AS count FROM mem_decisions WHERE scope_id = ?`).get(scope.id).count,
|
|
1134
|
+
entities: db.prepare(`SELECT COUNT(*) AS count FROM mem_entities WHERE scope_id = ?`).get(scope.id).count,
|
|
1135
|
+
action_items: db.prepare(`SELECT COUNT(*) AS count FROM mem_action_items WHERE scope_id = ?`).get(scope.id).count,
|
|
1136
|
+
};
|
|
1137
|
+
return {
|
|
1138
|
+
slug: scope.slug,
|
|
1139
|
+
title: scope.title,
|
|
1140
|
+
description: scope.description,
|
|
1141
|
+
active: activeScope?.slug === scope.slug,
|
|
1142
|
+
counts,
|
|
1143
|
+
};
|
|
1144
|
+
});
|
|
1145
|
+
res.json({ scopes: result });
|
|
1146
|
+
});
|
|
1147
|
+
app.get("/api/memory/inbox", (_req, res) => {
|
|
1148
|
+
const items = listPendingInboxItems();
|
|
1149
|
+
const result = items.map((item) => ({
|
|
1150
|
+
id: item.id,
|
|
1151
|
+
scope_slug: item.scopeId
|
|
1152
|
+
? getDb().prepare(`SELECT slug FROM mem_scopes WHERE id = ?`).get(item.scopeId)?.slug ?? null
|
|
1153
|
+
: null,
|
|
1154
|
+
kind: item.kind,
|
|
1155
|
+
payload: item.payload,
|
|
1156
|
+
source_agent: item.sourceAgent,
|
|
1157
|
+
created_at: item.createdAt,
|
|
1158
|
+
}));
|
|
1159
|
+
res.json({ items: result, total: result.length });
|
|
1160
|
+
});
|
|
1161
|
+
app.post("/api/memory/inbox/:id/route", (req, res) => {
|
|
1162
|
+
const id = Number(req.params.id);
|
|
1163
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
1164
|
+
res.status(400).json({ error: "Invalid inbox item id" });
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
const body = parseRequest(inboxRouteSchema, req.body ?? {});
|
|
1168
|
+
const item = getInboxItem(id);
|
|
1169
|
+
if (!item) {
|
|
1170
|
+
res.status(404).json({ error: `Inbox item '${id}' not found` });
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
if (item.status !== "pending") {
|
|
1174
|
+
res.status(409).json({ error: `Inbox item '${id}' is already resolved` });
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
const status = body.action === "accept" ? "accepted" : "rejected";
|
|
1178
|
+
const reason = body.reason ?? (body.action === "accept" ? "Accepted via web UI" : "Rejected via web UI");
|
|
1179
|
+
resolveInboxItem(id, status, reason);
|
|
1180
|
+
log.info({ id, action: body.action }, "inbox item routed via web UI");
|
|
1181
|
+
res.json({ ok: true });
|
|
1182
|
+
});
|
|
1183
|
+
app.get("/api/memory/:scope", (req, res) => {
|
|
1184
|
+
const scopeSlug = String(req.params.scope);
|
|
1185
|
+
const scope = getScope(scopeSlug);
|
|
1186
|
+
if (!scope) {
|
|
1187
|
+
res.status(404).json({ error: `Memory scope '${scopeSlug}' not found` });
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
const query = parseRequest(memoryEntriesQuerySchema, req.query);
|
|
1191
|
+
const store = query.store ?? "observations";
|
|
1192
|
+
const tier = query.tier;
|
|
1193
|
+
const db = getDb();
|
|
1194
|
+
let entries;
|
|
1195
|
+
let total;
|
|
1196
|
+
if (store === "observations") {
|
|
1197
|
+
if (tier) {
|
|
1198
|
+
entries = db.prepare(`SELECT id, content, source, tier, entity_id, created_at FROM mem_observations WHERE scope_id = ? AND tier = ? AND archived_at IS NULL ORDER BY id DESC LIMIT 100`).all(scope.id, tier);
|
|
1199
|
+
total = db.prepare(`SELECT COUNT(*) AS n FROM mem_observations WHERE scope_id = ? AND tier = ? AND archived_at IS NULL`).get(scope.id, tier).n;
|
|
1200
|
+
}
|
|
1201
|
+
else {
|
|
1202
|
+
entries = db.prepare(`SELECT id, content, source, tier, entity_id, created_at FROM mem_observations WHERE scope_id = ? AND archived_at IS NULL ORDER BY id DESC LIMIT 100`).all(scope.id);
|
|
1203
|
+
total = db.prepare(`SELECT COUNT(*) AS n FROM mem_observations WHERE scope_id = ? AND archived_at IS NULL`).get(scope.id).n;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
else if (store === "decisions") {
|
|
1207
|
+
if (tier) {
|
|
1208
|
+
entries = db.prepare(`SELECT id, title, rationale, tier, entity_id, decided_at, created_at FROM mem_decisions WHERE scope_id = ? AND tier = ? AND archived_at IS NULL ORDER BY decided_at DESC, id DESC LIMIT 100`).all(scope.id, tier);
|
|
1209
|
+
total = db.prepare(`SELECT COUNT(*) AS n FROM mem_decisions WHERE scope_id = ? AND tier = ? AND archived_at IS NULL`).get(scope.id, tier).n;
|
|
1210
|
+
}
|
|
1211
|
+
else {
|
|
1212
|
+
entries = db.prepare(`SELECT id, title, rationale, tier, entity_id, decided_at, created_at FROM mem_decisions WHERE scope_id = ? AND archived_at IS NULL ORDER BY decided_at DESC, id DESC LIMIT 100`).all(scope.id);
|
|
1213
|
+
total = db.prepare(`SELECT COUNT(*) AS n FROM mem_decisions WHERE scope_id = ? AND archived_at IS NULL`).get(scope.id).n;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
else if (store === "entities") {
|
|
1217
|
+
if (tier) {
|
|
1218
|
+
entries = db.prepare(`SELECT id, kind, name, summary, tier, created_at, updated_at FROM mem_entities WHERE scope_id = ? AND tier = ? ORDER BY updated_at DESC, id DESC LIMIT 100`).all(scope.id, tier);
|
|
1219
|
+
total = db.prepare(`SELECT COUNT(*) AS n FROM mem_entities WHERE scope_id = ? AND tier = ?`).get(scope.id, tier).n;
|
|
1220
|
+
}
|
|
1221
|
+
else {
|
|
1222
|
+
entries = db.prepare(`SELECT id, kind, name, summary, tier, created_at, updated_at FROM mem_entities WHERE scope_id = ? ORDER BY updated_at DESC, id DESC LIMIT 100`).all(scope.id);
|
|
1223
|
+
total = db.prepare(`SELECT COUNT(*) AS n FROM mem_entities WHERE scope_id = ?`).get(scope.id).n;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
else {
|
|
1227
|
+
if (tier) {
|
|
1228
|
+
entries = db.prepare(`SELECT id, title, detail, status, tier, due_at, entity_id, created_at FROM mem_action_items WHERE scope_id = ? AND tier = ? ORDER BY created_at DESC, id DESC LIMIT 100`).all(scope.id, tier);
|
|
1229
|
+
total = db.prepare(`SELECT COUNT(*) AS n FROM mem_action_items WHERE scope_id = ? AND tier = ?`).get(scope.id, tier).n;
|
|
1230
|
+
}
|
|
1231
|
+
else {
|
|
1232
|
+
entries = db.prepare(`SELECT id, title, detail, status, tier, due_at, entity_id, created_at FROM mem_action_items WHERE scope_id = ? ORDER BY created_at DESC, id DESC LIMIT 100`).all(scope.id);
|
|
1233
|
+
total = db.prepare(`SELECT COUNT(*) AS n FROM mem_action_items WHERE scope_id = ?`).get(scope.id).n;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
res.json({ entries, total });
|
|
1237
|
+
});
|
|
1238
|
+
app.post("/api/memory/:scope/remember", (req, res) => {
|
|
1239
|
+
const scopeSlug = String(req.params.scope);
|
|
1240
|
+
const scope = getScope(scopeSlug);
|
|
1241
|
+
if (!scope) {
|
|
1242
|
+
res.status(404).json({ error: `Memory scope '${scopeSlug}' not found` });
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
const body = parseRequest(memoryRememberSchema, req.body ?? {});
|
|
1246
|
+
if (body.entity_name && !body.entity_kind) {
|
|
1247
|
+
res.status(400).json({ error: "entity_kind is required when entity_name is provided" });
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
const kind = body.kind ?? "observation";
|
|
1251
|
+
const entity = body.entity_name
|
|
1252
|
+
? upsertEntity({
|
|
1253
|
+
scope_id: scope.id,
|
|
1254
|
+
kind: body.entity_kind,
|
|
1255
|
+
name: body.entity_name,
|
|
1256
|
+
tier: body.tier ?? "warm",
|
|
1257
|
+
})
|
|
1258
|
+
: undefined;
|
|
1259
|
+
if (kind === "decision") {
|
|
1260
|
+
if (!body.title) {
|
|
1261
|
+
res.status(400).json({ error: "title is required when kind='decision'" });
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
const decision = recordDecision({
|
|
1265
|
+
scope_id: scope.id,
|
|
1266
|
+
entity_id: entity?.id,
|
|
1267
|
+
title: body.title,
|
|
1268
|
+
rationale: body.content,
|
|
1269
|
+
decided_at: body.decided_at ?? new Date().toISOString().slice(0, 10),
|
|
1270
|
+
tier: body.tier ?? "warm",
|
|
1271
|
+
});
|
|
1272
|
+
log.info({ id: decision.id, scope: scopeSlug, kind }, "memory written via web UI");
|
|
1273
|
+
res.json({ ok: true, id: String(decision.id) });
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
const observation = recordObservation({
|
|
1277
|
+
scope_id: scope.id,
|
|
1278
|
+
entity_id: entity?.id,
|
|
1279
|
+
content: body.content,
|
|
1280
|
+
source: "agent:web-ui",
|
|
1281
|
+
tier: body.tier ?? "warm",
|
|
1282
|
+
});
|
|
1283
|
+
log.info({ id: observation.id, scope: scopeSlug, kind }, "memory written via web UI");
|
|
1284
|
+
res.json({ ok: true, id: String(observation.id) });
|
|
1285
|
+
});
|
|
1286
|
+
app.post("/api/memory/hooks/git-commit", authMiddleware, (req, res, next) => {
|
|
1287
|
+
const body = parseRequest(gitCommitHookSchema, req.body ?? {});
|
|
1288
|
+
handleGitCommitHook({ message: body.message, stat: body.stat })
|
|
1289
|
+
.then((result) => res.json({ ok: true, observation_id: result.observation_id }))
|
|
1290
|
+
.catch(next);
|
|
1291
|
+
});
|
|
1292
|
+
app.post("/api/memory/hooks/pr-merge", authMiddleware, (req, res, next) => {
|
|
1293
|
+
const body = parseRequest(prMergeHookSchema, req.body ?? {});
|
|
1294
|
+
handlePrMergeHook({
|
|
1295
|
+
number: body.number,
|
|
1296
|
+
title: body.title,
|
|
1297
|
+
body: body.body,
|
|
1298
|
+
files_changed: body.files_changed,
|
|
1299
|
+
})
|
|
1300
|
+
.then((result) => res.json({ ok: true, observation_id: result.observation_id }))
|
|
1301
|
+
.catch(next);
|
|
1302
|
+
});
|
|
904
1303
|
app.post("/api/scopes", (req, res) => {
|
|
905
1304
|
const body = parseRequest(scopeCreateSchema, req.body ?? {});
|
|
906
1305
|
if (getScope(body.slug)) {
|
|
@@ -1095,6 +1494,14 @@ app.delete("/api/wiki/page", async (req, res) => {
|
|
|
1095
1494
|
const removed = await withWikiWrite(() => deletePage(path));
|
|
1096
1495
|
res.json({ ok: removed, path });
|
|
1097
1496
|
});
|
|
1497
|
+
app.post("/api/wiki/korg", authMiddleware, async (req, res) => {
|
|
1498
|
+
const body = parseRequest(korgRequestSchema, req.body ?? {});
|
|
1499
|
+
const result = await routeKorgMessage(body);
|
|
1500
|
+
res.json(result);
|
|
1501
|
+
});
|
|
1502
|
+
app.get("/api/wiki/korg/sessions", authMiddleware, (_req, res) => {
|
|
1503
|
+
res.json({ sessions: listKorgResearchSessions(getDb()) });
|
|
1504
|
+
});
|
|
1098
1505
|
// ---------------------------------------------------------------------------
|
|
1099
1506
|
// Skills
|
|
1100
1507
|
// ---------------------------------------------------------------------------
|