@dotdrelle/wiki-manager 0.6.47 → 0.8.2

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.
Files changed (48) hide show
  1. package/.env.example +20 -0
  2. package/README.md +35 -1
  3. package/bin/wiki-manager.js +2 -2
  4. package/docker-compose.yml +2 -0
  5. package/mcp.endpoints.example.json +13 -0
  6. package/package.json +2 -2
  7. package/src/agent/graph.js +101 -15
  8. package/src/agent/graph.test.js +145 -0
  9. package/src/cli/wiki-manager.js +459 -131
  10. package/src/commands/slash.js +1 -1
  11. package/src/core/activity.js +14 -0
  12. package/src/core/agentEvents.js +148 -6
  13. package/src/core/agentEvents.test.js +145 -4
  14. package/src/core/agentLoop.js +167 -0
  15. package/src/core/agentLoop.test.js +85 -0
  16. package/src/core/cacert.js +4 -3
  17. package/src/core/dockerCompose.test.js +14 -0
  18. package/src/core/documentIntake.test.js +7 -7
  19. package/src/core/env.js +6 -0
  20. package/src/core/jobQueue.js +47 -16
  21. package/src/core/mcp.js +120 -10
  22. package/src/core/mcp.test.js +121 -1
  23. package/src/core/plan.js +33 -0
  24. package/src/core/queueStore.js +27 -0
  25. package/src/core/queueStore.test.js +32 -0
  26. package/src/core/wikiWorkspace.test.js +24 -0
  27. package/src/core/workspaces.js +8 -1
  28. package/src/runtime/approvals.js +113 -0
  29. package/src/runtime/auth.js +35 -0
  30. package/src/runtime/auth.test.js +29 -0
  31. package/src/runtime/client.js +154 -0
  32. package/src/runtime/lifecycle.js +84 -0
  33. package/src/runtime/queueStore.js +6 -0
  34. package/src/runtime/runner.js +413 -0
  35. package/src/runtime/runner.test.js +270 -0
  36. package/src/runtime/server.js +216 -0
  37. package/src/runtime/server.test.js +308 -0
  38. package/src/runtime/store.js +431 -0
  39. package/src/runtime/store.test.js +437 -0
  40. package/src/runtime/supervisor.js +104 -0
  41. package/src/runtime/supervisor.test.js +240 -0
  42. package/src/shell/RightPane.tsx +1 -1
  43. package/src/shell/repl.js +148 -18
  44. package/src/shell/repl.test.js +38 -0
  45. package/src/shell/tui.tsx +4 -1
  46. package/src/shell/useAgent.ts +20 -2
  47. package/src/shell/useSession.ts +153 -13
  48. package/wiki-workspace +221 -22
@@ -0,0 +1,431 @@
1
+ import { mkdirSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { DatabaseSync } from 'node:sqlite';
4
+ import { applyAgentProjectionToSession, dispatchAgentEvent, reduceAgentEvents } from '../core/agentEvents.js';
5
+ import { defaultRuntimeStateDir } from '../core/env.js';
6
+ import { projectQueue } from '../core/jobQueue.js';
7
+
8
+ export { defaultRuntimeStateDir };
9
+
10
+ const NON_PERSISTED_EVENT_TYPES = new Set(['runtime_log']);
11
+
12
+ const RUN_STATUS_BY_EVENT = {
13
+ run_done: 'done',
14
+ run_error: 'error',
15
+ run_cancelled: 'cancelled',
16
+ run_pending_approval: 'pending_approval',
17
+ run_approved: 'running',
18
+ };
19
+
20
+ const RECOVERABLE_RUN_STATUSES = ['running', 'waiting', 'pending_approval'];
21
+ export const RECOVERABLE_QUEUE_STATUSES = ['waiting', 'queued', 'starting', 'running', 'blocked', 'pending_approval'];
22
+
23
+ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName = 'runtime.db' } = {}) {
24
+ const resolvedStateDir = resolve(stateDir);
25
+ mkdirSync(resolvedStateDir, { recursive: true });
26
+ const dbPath = join(resolvedStateDir, fileName);
27
+ const db = new DatabaseSync(dbPath);
28
+ db.exec('PRAGMA journal_mode = WAL');
29
+ db.exec('PRAGMA foreign_keys = ON');
30
+ db.exec(`
31
+ CREATE TABLE IF NOT EXISTS events (
32
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
33
+ id TEXT NOT NULL UNIQUE,
34
+ ts TEXT NOT NULL,
35
+ type TEXT NOT NULL,
36
+ run_id TEXT,
37
+ turn_id TEXT,
38
+ workspace TEXT,
39
+ origin TEXT,
40
+ payload TEXT NOT NULL
41
+ );
42
+ CREATE INDEX IF NOT EXISTS idx_events_ts ON events(ts);
43
+ CREATE INDEX IF NOT EXISTS idx_events_run_id ON events(run_id);
44
+ CREATE TABLE IF NOT EXISTS runs (
45
+ id TEXT PRIMARY KEY,
46
+ workspace TEXT,
47
+ status TEXT NOT NULL,
48
+ input TEXT,
49
+ created_at TEXT NOT NULL,
50
+ updated_at TEXT NOT NULL
51
+ );
52
+ CREATE TABLE IF NOT EXISTS queue_items (
53
+ id TEXT PRIMARY KEY,
54
+ workspace TEXT,
55
+ server TEXT NOT NULL,
56
+ tool TEXT NOT NULL,
57
+ args TEXT NOT NULL,
58
+ lock_key TEXT,
59
+ status TEXT NOT NULL,
60
+ reason TEXT,
61
+ job_id TEXT,
62
+ activity_key TEXT,
63
+ error TEXT,
64
+ created_at TEXT,
65
+ started_at TEXT,
66
+ finished_at TEXT,
67
+ updated_at TEXT NOT NULL
68
+ );
69
+ `);
70
+ ensureColumn(db, 'events', 'sequence', 'INTEGER');
71
+ ensureColumn(db, 'events', 'workspace', 'TEXT');
72
+ ensureColumn(db, 'runs', 'workspace', 'TEXT');
73
+ backfillEventSequence(db);
74
+ db.exec('CREATE INDEX IF NOT EXISTS idx_events_workspace ON events(workspace)');
75
+ db.exec('CREATE INDEX IF NOT EXISTS idx_events_sequence ON events(sequence)');
76
+
77
+ let lastEventId = null;
78
+ let lastEventSequence = null;
79
+
80
+ const insertEvent = db.prepare(`
81
+ INSERT OR IGNORE INTO events (sequence, id, ts, type, run_id, turn_id, workspace, origin, payload)
82
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
83
+ `);
84
+ const nextEventSequenceStatement = db.prepare('SELECT COALESCE(MAX(sequence), 0) + 1 AS next_sequence FROM events');
85
+ const listEventsStatement = db.prepare(`
86
+ SELECT sequence, id, ts, type, run_id, turn_id, workspace, origin, payload
87
+ FROM events
88
+ ORDER BY sequence ASC
89
+ `);
90
+ const listEventsByWorkspaceStatement = db.prepare(`
91
+ SELECT sequence, id, ts, type, run_id, turn_id, workspace, origin, payload
92
+ FROM events
93
+ WHERE workspace = ?
94
+ ORDER BY sequence ASC
95
+ `);
96
+ const upsertRun = db.prepare(`
97
+ INSERT INTO runs (id, workspace, status, input, created_at, updated_at)
98
+ VALUES (?, ?, ?, ?, ?, ?)
99
+ ON CONFLICT(id) DO UPDATE SET
100
+ workspace = COALESCE(excluded.workspace, runs.workspace),
101
+ status = excluded.status,
102
+ input = COALESCE(excluded.input, runs.input),
103
+ updated_at = excluded.updated_at
104
+ `);
105
+ const listRunsStatement = db.prepare(`
106
+ SELECT id, workspace, status, input, created_at, updated_at
107
+ FROM runs
108
+ ORDER BY created_at DESC
109
+ `);
110
+ const listRunsByWorkspaceStatement = db.prepare(`
111
+ SELECT id, workspace, status, input, created_at, updated_at
112
+ FROM runs
113
+ WHERE workspace = ?
114
+ ORDER BY created_at DESC
115
+ `);
116
+ const listRecoverableRunsStatement = db.prepare(`
117
+ SELECT id, workspace, status, input, created_at, updated_at
118
+ FROM runs
119
+ WHERE status IN (${RECOVERABLE_RUN_STATUSES.map(() => '?').join(', ')})
120
+ ORDER BY created_at ASC
121
+ `);
122
+ const listRecoverableRunsByWorkspaceStatement = db.prepare(`
123
+ SELECT id, workspace, status, input, created_at, updated_at
124
+ FROM runs
125
+ WHERE workspace = ? AND status IN (${RECOVERABLE_RUN_STATUSES.map(() => '?').join(', ')})
126
+ ORDER BY created_at ASC
127
+ `);
128
+ const interruptRunsStatement = db.prepare(`
129
+ UPDATE runs
130
+ SET status = 'interrupted', updated_at = ?
131
+ WHERE workspace = ? AND status IN (${RECOVERABLE_RUN_STATUSES.map(() => '?').join(', ')})
132
+ `);
133
+ const upsertQueueItem = db.prepare(`
134
+ INSERT INTO queue_items (
135
+ id, workspace, server, tool, args, lock_key, status, reason, job_id, activity_key,
136
+ error, created_at, started_at, finished_at, updated_at
137
+ )
138
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
139
+ ON CONFLICT(id) DO UPDATE SET
140
+ workspace = excluded.workspace,
141
+ server = excluded.server,
142
+ tool = excluded.tool,
143
+ args = excluded.args,
144
+ lock_key = excluded.lock_key,
145
+ status = excluded.status,
146
+ reason = excluded.reason,
147
+ job_id = excluded.job_id,
148
+ activity_key = excluded.activity_key,
149
+ error = excluded.error,
150
+ created_at = excluded.created_at,
151
+ started_at = excluded.started_at,
152
+ finished_at = excluded.finished_at,
153
+ updated_at = excluded.updated_at
154
+ `);
155
+ const deleteMissingQueueItems = db.prepare(`
156
+ DELETE FROM queue_items
157
+ WHERE id NOT IN (SELECT value FROM json_each(?))
158
+ `);
159
+ const deleteMissingQueueItemsForWorkspace = db.prepare(`
160
+ DELETE FROM queue_items
161
+ WHERE workspace = ? AND id NOT IN (SELECT value FROM json_each(?))
162
+ `);
163
+ const clearQueueItemsForWorkspace = db.prepare('DELETE FROM queue_items WHERE workspace = ?');
164
+ const clearQueueItems = db.prepare('DELETE FROM queue_items');
165
+ const listQueueStatement = db.prepare(`
166
+ SELECT id, workspace, server, tool, args, lock_key, status, reason, job_id, activity_key,
167
+ error, created_at, started_at, finished_at, updated_at
168
+ FROM queue_items
169
+ ORDER BY COALESCE(created_at, updated_at) ASC, id ASC
170
+ `);
171
+ const listQueueByWorkspaceStatement = db.prepare(`
172
+ SELECT id, workspace, server, tool, args, lock_key, status, reason, job_id, activity_key,
173
+ error, created_at, started_at, finished_at, updated_at
174
+ FROM queue_items
175
+ WHERE workspace = ?
176
+ ORDER BY COALESCE(created_at, updated_at) ASC, id ASC
177
+ `);
178
+ const listRecoverableWorkspacesStatement = db.prepare(`
179
+ SELECT DISTINCT workspace FROM runs
180
+ WHERE workspace IS NOT NULL AND status IN (${RECOVERABLE_RUN_STATUSES.map(() => '?').join(', ')})
181
+ UNION
182
+ SELECT DISTINCT workspace FROM queue_items
183
+ WHERE workspace IS NOT NULL AND status IN (${RECOVERABLE_QUEUE_STATUSES.map(() => '?').join(', ')})
184
+ ORDER BY workspace
185
+ `);
186
+
187
+ function persistEvent(event) {
188
+ if (NON_PERSISTED_EVENT_TYPES.has(event.type)) return event;
189
+ const ws = event.workspace ?? event.payload?.workspace ?? null;
190
+ const sequence = nextEventSequenceStatement.get().next_sequence;
191
+ const result = insertEvent.run(
192
+ sequence,
193
+ event.id,
194
+ event.ts,
195
+ event.type,
196
+ event.runId ?? null,
197
+ event.turnId ?? null,
198
+ ws,
199
+ event.origin ?? null,
200
+ JSON.stringify(event.payload ?? {}),
201
+ );
202
+ if (Number(result.changes ?? 0) > 0) {
203
+ event.sequence = sequence;
204
+ lastEventId = event.id;
205
+ lastEventSequence = sequence;
206
+ }
207
+ if (event.type === 'run_started') {
208
+ persistRun({
209
+ id: event.runId ?? event.payload?.runId ?? event.id,
210
+ status: 'running',
211
+ input: event.payload?.input ?? null,
212
+ workspace: ws,
213
+ createdAt: event.ts,
214
+ updatedAt: event.ts,
215
+ });
216
+ } else if (RUN_STATUS_BY_EVENT[event.type]) {
217
+ const runId = event.runId ?? event.payload?.runId ?? null;
218
+ if (runId) {
219
+ persistRun({
220
+ id: runId,
221
+ status: RUN_STATUS_BY_EVENT[event.type],
222
+ workspace: ws,
223
+ updatedAt: event.ts,
224
+ });
225
+ }
226
+ }
227
+ return event;
228
+ }
229
+
230
+ function persistRun({ id, status, input = null, workspace = null, createdAt = null, updatedAt = null }) {
231
+ if (!id) return;
232
+ const now = new Date().toISOString();
233
+ upsertRun.run(id, workspace, status, input, createdAt ?? now, updatedAt ?? now);
234
+ }
235
+
236
+ function listEvents({ workspace = null } = {}) {
237
+ const rows = workspace
238
+ ? listEventsByWorkspaceStatement.all(workspace)
239
+ : listEventsStatement.all();
240
+ return rows.map(rowToEvent);
241
+ }
242
+
243
+ function listRuns({ workspace = null } = {}) {
244
+ const rows = workspace
245
+ ? listRunsByWorkspaceStatement.all(workspace)
246
+ : listRunsStatement.all();
247
+ return rows.map((row) => ({
248
+ id: row.id,
249
+ workspace: row.workspace ?? null,
250
+ status: row.status,
251
+ input: row.input,
252
+ createdAt: row.created_at,
253
+ updatedAt: row.updated_at,
254
+ }));
255
+ }
256
+
257
+ function listRecoverableRuns({ workspace = null } = {}) {
258
+ const rows = workspace
259
+ ? listRecoverableRunsByWorkspaceStatement.all(workspace, ...RECOVERABLE_RUN_STATUSES)
260
+ : listRecoverableRunsStatement.all(...RECOVERABLE_RUN_STATUSES);
261
+ return rows.map((row) => ({
262
+ id: row.id,
263
+ workspace: row.workspace ?? null,
264
+ status: row.status,
265
+ input: row.input,
266
+ createdAt: row.created_at,
267
+ updatedAt: row.updated_at,
268
+ }));
269
+ }
270
+
271
+ function listRecoverableWorkspaces() {
272
+ return listRecoverableWorkspacesStatement
273
+ .all(...RECOVERABLE_RUN_STATUSES, ...RECOVERABLE_QUEUE_STATUSES)
274
+ .map((row) => row.workspace);
275
+ }
276
+
277
+ function interruptRuns({ workspace, reason = 'Runtime restart recovery failed.' } = {}) {
278
+ if (!workspace) return 0;
279
+ const now = new Date().toISOString();
280
+ const result = interruptRunsStatement.run(now, workspace, ...RECOVERABLE_RUN_STATUSES);
281
+ return Number(result.changes ?? 0);
282
+ }
283
+
284
+ function saveQueue(queue = [], { workspace = null } = {}) {
285
+ const items = Array.isArray(queue) ? queue : [];
286
+ const now = new Date().toISOString();
287
+ db.exec('BEGIN');
288
+ try {
289
+ if (items.length === 0) {
290
+ if (workspace) clearQueueItemsForWorkspace.run(workspace);
291
+ else clearQueueItems.run();
292
+ } else {
293
+ if (workspace) deleteMissingQueueItemsForWorkspace.run(workspace, JSON.stringify(items.map((item) => item.id)));
294
+ else deleteMissingQueueItems.run(JSON.stringify(items.map((item) => item.id)));
295
+ for (const item of items) {
296
+ upsertQueueItem.run(
297
+ item.id,
298
+ item.workspace ?? workspace ?? null,
299
+ item.server ?? 'production',
300
+ item.tool ?? 'production_start_job',
301
+ JSON.stringify(item.args ?? {}),
302
+ item.lockKey ?? null,
303
+ item.status ?? 'waiting',
304
+ item.reason ?? null,
305
+ item.jobId ?? null,
306
+ item.activityKey ?? null,
307
+ item.error ?? null,
308
+ item.createdAt ?? now,
309
+ item.startedAt ?? null,
310
+ item.finishedAt ?? null,
311
+ now,
312
+ );
313
+ }
314
+ }
315
+ db.exec('COMMIT');
316
+ } catch (err) {
317
+ db.exec('ROLLBACK');
318
+ throw err;
319
+ }
320
+ }
321
+
322
+ function listQueue({ workspace = null } = {}) {
323
+ const rows = workspace
324
+ ? listQueueByWorkspaceStatement.all(workspace)
325
+ : listQueueStatement.all();
326
+ return rows.map((row) => ({
327
+ id: row.id,
328
+ workspace: row.workspace ?? null,
329
+ server: row.server,
330
+ tool: row.tool,
331
+ args: row.args ? JSON.parse(row.args) : {},
332
+ lockKey: row.lock_key ?? null,
333
+ status: row.status,
334
+ reason: row.reason ?? null,
335
+ jobId: row.job_id ?? undefined,
336
+ activityKey: row.activity_key ?? undefined,
337
+ error: row.error ?? undefined,
338
+ createdAt: row.created_at ?? undefined,
339
+ startedAt: row.started_at ?? undefined,
340
+ finishedAt: row.finished_at ?? undefined,
341
+ updatedAt: row.updated_at,
342
+ }));
343
+ }
344
+
345
+ function replayEvents(session, { workspace = null } = {}) {
346
+ const events = listEvents({ workspace });
347
+ for (const event of events) {
348
+ dispatchAgentEvent(session, event);
349
+ }
350
+ if (events.length > 0) {
351
+ lastEventId = events.at(-1).id;
352
+ lastEventSequence = events.at(-1).sequence ?? null;
353
+ }
354
+ return session.agentProjection ?? reduceAgentEvents([]);
355
+ }
356
+
357
+ function getProjection({ workspace = null } = {}) {
358
+ return reduceAgentEvents(listEvents({ workspace }));
359
+ }
360
+
361
+ function getState(session = null, { workspace = null } = {}) {
362
+ const events = session?.agentProjection ? null : listEvents({ workspace });
363
+ const projection = session?.agentProjection ?? reduceAgentEvents(events);
364
+ const rawQueue = session?.queueStore?.list() ?? listQueue({ workspace });
365
+ return {
366
+ ...projection,
367
+ runs: listRuns({ workspace }),
368
+ queue: projectQueue(projection.plan, rawQueue, { workspace }),
369
+ eventsCursor: session?.agentEvents?.at(-1)?.sequence ?? events?.at(-1)?.sequence ?? lastEventSequence,
370
+ };
371
+ }
372
+
373
+ function hydrateSession(session, { workspace = null } = {}) {
374
+ const projection = replayEvents(session, { workspace });
375
+ applyAgentProjectionToSession(session, projection);
376
+ session.jobQueue = listQueue({ workspace });
377
+ return projection;
378
+ }
379
+
380
+ function close() {
381
+ db.close();
382
+ }
383
+
384
+ return {
385
+ db,
386
+ dbPath,
387
+ stateDir: resolvedStateDir,
388
+ persistEvent,
389
+ persistRun,
390
+ listEvents,
391
+ listRuns,
392
+ listRecoverableRuns,
393
+ listRecoverableWorkspaces,
394
+ interruptRuns,
395
+ saveQueue,
396
+ listQueue,
397
+ replayEvents,
398
+ hydrateSession,
399
+ getProjection,
400
+ getState,
401
+ close,
402
+ };
403
+ }
404
+
405
+ function rowToEvent(row) {
406
+ return {
407
+ sequence: row.sequence ?? null,
408
+ id: row.id,
409
+ ts: row.ts,
410
+ type: row.type,
411
+ origin: row.origin ?? 'system',
412
+ runId: row.run_id ?? null,
413
+ turnId: row.turn_id ?? null,
414
+ workspace: row.workspace ?? null,
415
+ payload: row.payload ? JSON.parse(row.payload) : {},
416
+ };
417
+ }
418
+
419
+ function ensureColumn(db, table, column, definition) {
420
+ const existing = db.prepare(`PRAGMA table_info(${table})`).all();
421
+ if (existing.some((row) => row.name === column)) return;
422
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
423
+ }
424
+
425
+ function backfillEventSequence(db) {
426
+ db.exec(`
427
+ UPDATE events
428
+ SET sequence = rowid
429
+ WHERE sequence IS NULL
430
+ `);
431
+ }