@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
@@ -5,7 +5,9 @@ import { formatMcpToolResult, callMcpTool } from '../core/mcp.js';
5
5
  import { extractActivity, parseJsonText, sessionActivities } from '../core/activity.js';
6
6
  import { formatPlanStatus, formatCompletedActivities } from '../core/plan.js';
7
7
  import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
8
- import { queueCounts, startNextQueuedJob, syncQueueWithActivity } from '../core/jobQueue.js';
8
+ import { projectQueue, queueCounts, startNextQueuedJob, syncQueueWithActivity } from '../core/jobQueue.js';
9
+ import { queueStoreFor } from '../core/queueStore.js';
10
+ import { fetchRuntimeState, streamRuntimeEvents } from '../runtime/client.js';
9
11
  import type { ActiveFileEditor } from './FileEditorDialog';
10
12
  import {
11
13
  completionContext,
@@ -15,6 +17,16 @@ import {
15
17
  } from './repl.js';
16
18
  import { useAgent } from './useAgent';
17
19
 
20
+ function runtimeStatusText(status: 'disabled' | 'connected' | 'disconnected', runStatus: string): string {
21
+ if (status === 'connected') return `runtime ${runStatus}`;
22
+ if (status === 'disconnected') return 'runtime offline';
23
+ return 'runtime off';
24
+ }
25
+
26
+ function nonEmptyRuntimeArray<T>(value: T[] | undefined | null): T[] | null {
27
+ return Array.isArray(value) && value.length > 0 ? value : null;
28
+ }
29
+
18
30
  function buildContinuationPrompt(session: any, completed: any[]): string {
19
31
  const originalTask = [...conversationMessages(session)]
20
32
  .reverse()
@@ -31,10 +43,12 @@ function buildContinuationPrompt(session: any, completed: any[]): string {
31
43
  ].filter(Boolean).join('\n');
32
44
  }
33
45
 
34
- export function useSession(props: { agent: unknown; packageJson: Record<string, unknown> }) {
46
+ export function useSession(props: { agent: unknown; packageJson: Record<string, unknown>; runtime?: any }) {
35
47
  const session = createSession();
36
48
  const [version, setVersion] = createSignal(0);
37
49
  const [logs, setLogs] = createSignal<string[]>([]);
50
+ const [runtimeState, setRuntimeState] = createSignal<any | null>(null);
51
+ const [runtimeStatus, setRuntimeStatus] = createSignal<'disabled' | 'connected' | 'disconnected'>(props.runtime ? 'disconnected' : 'disabled');
38
52
  const [input, setInput] = createSignal('');
39
53
  const [chatMode, setChatMode] = createSignal(true);
40
54
  (session as any).chatMode = true;
@@ -60,10 +74,45 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
60
74
  const addLog = (line: string) => {
61
75
  setLogs((items) => [...items, `${new Date().toLocaleTimeString()} ${line}`].slice(-200));
62
76
  };
63
- const agent = useAgent({ agent: props.agent, packageJson: props.packageJson, session, chatMode, refresh, addLog });
77
+ if (props.runtime?.url) addLog(`runtime: connected ${props.runtime.url}${props.runtime.started ? ' (started)' : ''}`);
78
+ const agent = useAgent({
79
+ agent: props.agent,
80
+ packageJson: props.packageJson,
81
+ session,
82
+ chatMode,
83
+ runtimeUrl: props.runtime?.url ?? null,
84
+ refresh,
85
+ addLog,
86
+ onRuntimeAccepted: () => {
87
+ setRuntimeState((state) => ({ ...(state ?? {}), status: 'running' }));
88
+ setRuntimeStatus('connected');
89
+ refresh();
90
+ },
91
+ });
92
+ const runtimeRunStatus = createMemo(() => String(runtimeState()?.status ?? 'idle'));
93
+ const agentBusy = createMemo(() =>
94
+ props.runtime?.url
95
+ ? runtimeRunStatus() === 'running' || agent.busy()
96
+ : agent.busy(),
97
+ );
98
+ const localFallbackActive = createMemo(() => !props.runtime?.url || runtimeStatus() === 'disconnected');
64
99
 
65
100
  const messages = createMemo(() => {
66
101
  version();
102
+ const runtimeConversation = runtimeState()?.conversation;
103
+ const localCommands = conversationMessages(session)
104
+ .filter((message: any) => message.role === 'command')
105
+ .map((message: any) => ({
106
+ role: 'command',
107
+ content: String(message.content ?? ''),
108
+ }));
109
+ if (Array.isArray(runtimeConversation) && runtimeConversation.length > 0) {
110
+ const runtimeMessages = runtimeConversation.map((message: any) => ({
111
+ role: message.role === 'assistant' ? 'donna' : message.role,
112
+ content: String(message.content ?? ''),
113
+ }));
114
+ return [...runtimeMessages, ...localCommands].slice(-200);
115
+ }
67
116
  return [...conversationMessages(session)];
68
117
  });
69
118
  const prompt = createMemo(() => {
@@ -84,6 +133,7 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
84
133
  session.wikirc?.profile ? session.wikirc.profile : 'no wikirc',
85
134
  session.language ? session.language : 'no language',
86
135
  session.llm ? 'llm ready' : 'llm limited',
136
+ runtimeStatusText(runtimeStatus(), runtimeRunStatus()),
87
137
  ].join(' ');
88
138
  });
89
139
  const matchContext = createMemo(() => {
@@ -119,37 +169,122 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
119
169
  let lastVisiblePlan: Array<{ step: number; description: string; status: string }> | null = null;
120
170
  const activities = createMemo(() => {
121
171
  version();
172
+ const runtimeActivities = nonEmptyRuntimeArray(runtimeState()?.activities);
173
+ if (runtimeActivities) {
174
+ return runtimeActivities.map((activity: any) => ({ ...activity, _runtime: true }));
175
+ }
122
176
  const current = sessionActivities(session);
123
177
  if (current.length > 0) {
124
178
  lastVisibleActivities = current.map((activity) => ({ ...activity }));
125
179
  return current;
126
180
  }
127
- return agent.busy() && lastVisibleActivities.length > 0
181
+ return agentBusy() && lastVisibleActivities.length > 0
128
182
  ? lastVisibleActivities.map((activity) => ({ ...activity }))
129
183
  : current;
130
184
  });
131
185
  const queueItems = createMemo(() => {
132
186
  version();
133
- return ((session as any).jobQueue ?? []).map((item: any) => ({ ...item }));
187
+ const runtimeQueue = nonEmptyRuntimeArray(runtimeState()?.queue);
188
+ if (runtimeQueue) return runtimeQueue.map((item: any) => ({ ...item, _runtime: true }));
189
+ return projectQueue((session as any).headlessPlan, (session as any).jobQueue ?? [], { workspace: (session as any).workspace ?? null })
190
+ .map((item: any) => ({ ...item }));
134
191
  });
135
192
  const queueInfo = createMemo(() => {
136
193
  version();
194
+ const runtimeQueue = runtimeState()?.queue;
195
+ if (Array.isArray(runtimeQueue)) {
196
+ return {
197
+ active: runtimeQueue.filter((item: any) => ['waiting', 'starting', 'running', 'queued', 'pending', 'pending_approval'].includes(String(item.status ?? '').toLowerCase())).length,
198
+ current: runtimeQueue.filter((item: any) => ['starting', 'running'].includes(String(item.status ?? '').toLowerCase())).length,
199
+ frozen: 0,
200
+ };
201
+ }
137
202
  return queueCounts(session);
138
203
  });
139
204
  const plan = createMemo(() => {
140
205
  version();
206
+ const runtimePlan = nonEmptyRuntimeArray(runtimeState()?.plan);
207
+ if (runtimePlan) {
208
+ return runtimePlan.map((step: any, index: number) => ({
209
+ step: Number(step.step ?? index + 1),
210
+ description: String(step.description ?? step.label ?? step.name ?? `Step ${index + 1}`),
211
+ status: String(step.status ?? 'pending'),
212
+ }));
213
+ }
141
214
  const p = (session as any).headlessPlan as Array<{ step: number; description: string; status: string }> | null;
142
215
  const current = p ? p.map((s) => ({ ...s })) : null;
143
216
  if (current && current.length > 0) {
144
217
  lastVisiblePlan = current.map((step) => ({ ...step }));
145
218
  return current;
146
219
  }
147
- return agent.busy() && lastVisiblePlan && lastVisiblePlan.length > 0
220
+ return agentBusy() && lastVisiblePlan && lastVisiblePlan.length > 0
148
221
  ? lastVisiblePlan.map((step) => ({ ...step }))
149
222
  : current;
150
223
  });
224
+ const visibleLogs = createMemo(() => {
225
+ version();
226
+ const runtimeLogs = runtimeState()?.logs;
227
+ if (!Array.isArray(runtimeLogs) || runtimeLogs.length === 0) return logs();
228
+ const tagged = runtimeLogs.slice(-80).map((line: any) => `runtime ${String(line)}`);
229
+ return [...logs(), ...tagged].slice(-200);
230
+ });
231
+
232
+ function syncRuntimeState() {
233
+ void fetchRuntimeState({ url: props.runtime.url, workspace: (session as any).workspace ?? null })
234
+ .then((state) => {
235
+ setRuntimeState(state);
236
+ setRuntimeStatus('connected');
237
+ refresh();
238
+ })
239
+ .catch(() => {
240
+ setRuntimeStatus('disconnected');
241
+ refresh();
242
+ });
243
+ }
244
+ let runtimeStreamAbort: AbortController | null = null;
245
+ let runtimeSyncDebounce: ReturnType<typeof setTimeout> | null = null;
246
+ let runtimeReconnectTimer: ReturnType<typeof setTimeout> | null = null;
247
+ let runtimeStreamStopped = false;
248
+
249
+ function debouncedSyncRuntimeState() {
250
+ if (runtimeSyncDebounce) clearTimeout(runtimeSyncDebounce);
251
+ runtimeSyncDebounce = setTimeout(syncRuntimeState, 200);
252
+ }
253
+
254
+ async function subscribeRuntimeEvents() {
255
+ if (!props.runtime?.url || runtimeStreamStopped) return;
256
+ runtimeStreamAbort = new AbortController();
257
+ try {
258
+ for await (const _event of streamRuntimeEvents({
259
+ url: props.runtime.url,
260
+ signal: runtimeStreamAbort.signal,
261
+ workspace: (session as any).workspace ?? null,
262
+ })) {
263
+ setRuntimeStatus('connected');
264
+ debouncedSyncRuntimeState();
265
+ }
266
+ } catch {
267
+ // stream dropped or errored — fall through to reconnect below
268
+ }
269
+ if (runtimeStreamStopped) return;
270
+ setRuntimeStatus('disconnected');
271
+ refresh();
272
+ runtimeReconnectTimer = setTimeout(() => { void subscribeRuntimeEvents(); }, 1500);
273
+ }
274
+
275
+ if (props.runtime?.url) {
276
+ syncRuntimeState();
277
+ void subscribeRuntimeEvents();
278
+ }
279
+ onCleanup(() => {
280
+ runtimeStreamStopped = true;
281
+ runtimeStreamAbort?.abort();
282
+ if (runtimeSyncDebounce) clearTimeout(runtimeSyncDebounce);
283
+ if (runtimeReconnectTimer) clearTimeout(runtimeReconnectTimer);
284
+ });
151
285
 
152
286
  const activityPollTimer = setInterval(() => {
287
+ if (!localFallbackActive()) return;
153
288
  for (const activity of sessionActivities(session)) {
154
289
  if (activity.terminal || !activity.poll) continue;
155
290
  const key = activity.key ?? `${activity.poll.server}:${activity.id ?? activity.label}`;
@@ -195,7 +330,7 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
195
330
  const plan = (session as any).headlessPlan;
196
331
  const stillRunning = sessionActivities(session).filter((a) => !a.terminal && a.poll);
197
332
  const pendingSteps = (plan ?? []).filter((s: any) => s.status === 'pending');
198
- if (stillRunning.length === 0 && pendingSteps.length > 0 && !agent.busy()) {
333
+ if (stillRunning.length === 0 && pendingSteps.length > 0 && !agentBusy()) {
199
334
  const completedAll = sessionActivities(session).filter((a) => a.terminal);
200
335
  const prompt = buildContinuationPrompt(session, completedAll);
201
336
  void agent.submit(prompt);
@@ -211,18 +346,23 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
211
346
  });
212
347
  }
213
348
  }, 1000);
214
- onCleanup(() => clearInterval(activityPollTimer));
349
+ onCleanup(() => {
350
+ if (activityPollTimer) clearInterval(activityPollTimer);
351
+ });
215
352
 
216
353
  const queueFallbackTimer = setInterval(() => {
217
- if ((session as any).jobQueue?.some((item: any) => item.status === 'waiting')) {
354
+ if (!localFallbackActive()) return;
355
+ if (queueStoreFor(session).list().some((item: any) => item.status === 'waiting')) {
218
356
  void startNextQueuedJob(session, { addLog, refresh });
219
357
  }
220
358
  }, 10000);
221
- onCleanup(() => clearInterval(queueFallbackTimer));
359
+ onCleanup(() => {
360
+ if (queueFallbackTimer) clearInterval(queueFallbackTimer);
361
+ });
222
362
 
223
363
  async function submitInput(submittedValue?: string) {
224
364
  const line = typeof submittedValue === 'string' && submittedValue.trim() ? submittedValue : input();
225
- if (agent.busy()) return { exit: false, busy: true };
365
+ if (agentBusy()) return { exit: false, busy: true };
226
366
  setInput('');
227
367
  setConversationScroll(0);
228
368
  if (line.trim()) {
@@ -344,7 +484,7 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
344
484
  return {
345
485
  session,
346
486
  messages,
347
- logs,
487
+ logs: visibleLogs,
348
488
  input,
349
489
  setInput: updateInput,
350
490
  title,
@@ -364,7 +504,7 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
364
504
  plan,
365
505
  conversationScroll,
366
506
  scrollConversation,
367
- busy: agent.busy,
507
+ busy: agentBusy,
368
508
  abort,
369
509
  submitInput,
370
510
  completeSelected,
package/wiki-workspace CHANGED
@@ -92,6 +92,11 @@ Commands:
92
92
  agents pull Pull latest external agent images
93
93
  agents status Check reachability of configured MCP endpoints
94
94
 
95
+ runtime up Start the single host agent-runtime on Node.js 22
96
+ runtime down Stop the host agent-runtime
97
+ runtime status Check host agent-runtime health
98
+ runtime logs [args...] Show host agent-runtime logs
99
+
95
100
  wiki <workspace> init Initialize the workspace with wiki init
96
101
  wiki <workspace> up Start llm-wiki serve + mcp-http + production-mcp
97
102
  wiki <workspace> down Stop all services for this workspace
@@ -106,6 +111,7 @@ Commands:
106
111
  Configuration:
107
112
  workspaces/<workspace>/.env
108
113
  Override directory: WIKI_WORKSPACES_DIR=/path/to/dir
114
+ Runtime node: WIKI_MANAGER_NODE_BIN=/path/to/node
109
115
  Local CA: --cacert /absolute/path/to/ca.pem (Docker must be able to read this host path)
110
116
  EOF
111
117
  }
@@ -362,6 +368,179 @@ NODE
362
368
  esac
363
369
  }
364
370
 
371
+ runtime_node_bin() {
372
+ printf '%s\n' "${WIKI_MANAGER_NODE_BIN:-node}"
373
+ }
374
+
375
+ ensure_runtime_node() {
376
+ local node_bin
377
+ node_bin="$(runtime_node_bin)"
378
+ command -v "$node_bin" >/dev/null 2>&1 || die "runtime requires Node.js 22+: command not found: $node_bin (set WIKI_MANAGER_NODE_BIN)"
379
+ local version major
380
+ version="$("$node_bin" -p 'process.versions.node' 2>/dev/null)" || die "could not execute Node runtime: $node_bin"
381
+ major="${version%%.*}"
382
+ [[ "$major" =~ ^[0-9]+$ && "$major" -ge 22 ]] || die "runtime requires Node.js 22+ for node:sqlite; $node_bin is Node $version"
383
+ }
384
+
385
+ ensure_manager_runtime_token() {
386
+ local token
387
+ token="$(env_value "$MANAGER_ENV_FILE" WIKI_MANAGER_RUNTIME_TOKEN "")"
388
+ if [[ -z "$token" ]]; then
389
+ token="$(generate_token)"
390
+ mkdir -p "$(dirname "$MANAGER_ENV_FILE")"
391
+ set_env_value "$MANAGER_ENV_FILE" WIKI_MANAGER_RUNTIME_TOKEN "$token"
392
+ fi
393
+ printf '%s\n' "$token"
394
+ }
395
+
396
+ runtime_pid_file() {
397
+ printf '%s/runtime.pid\n' "$MANAGER_RUNTIME_DIR"
398
+ }
399
+
400
+ runtime_log_file() {
401
+ printf '%s/runtime.log\n' "$MANAGER_RUNTIME_DIR"
402
+ }
403
+
404
+ runtime_pid_running() {
405
+ local pid_file pid
406
+ pid_file="$(runtime_pid_file)"
407
+ [[ -f "$pid_file" ]] || return 1
408
+ pid="$(cat "$pid_file" 2>/dev/null || true)"
409
+ [[ "$pid" =~ ^[0-9]+$ ]] || return 1
410
+ kill -0 "$pid" >/dev/null 2>&1
411
+ }
412
+
413
+ runtime_pid_command() {
414
+ local pid="$1"
415
+ ps -p "$pid" -o command= 2>/dev/null || ps -p "$pid" -o args= 2>/dev/null || true
416
+ }
417
+
418
+ runtime_pid_matches() {
419
+ local pid command
420
+ pid="$(cat "$(runtime_pid_file)" 2>/dev/null || true)"
421
+ [[ "$pid" =~ ^[0-9]+$ ]] || return 1
422
+ command="$(runtime_pid_command "$pid")"
423
+ [[ "$command" == *"/bin/wiki-manager.js runtime"* || "$command" == *" bin/wiki-manager.js runtime"* ]]
424
+ }
425
+
426
+ runtime_health_url() {
427
+ printf 'http://127.0.0.1:%s/health\n' "${WIKI_MANAGER_RUNTIME_PORT:-7788}"
428
+ }
429
+
430
+ runtime_health_check() {
431
+ local token url
432
+ token="$(env_value "$MANAGER_ENV_FILE" WIKI_MANAGER_RUNTIME_TOKEN "")"
433
+ url="$(runtime_health_url)"
434
+ if command -v curl >/dev/null 2>&1; then
435
+ if [[ -n "$token" ]]; then
436
+ curl -fsS -H "Authorization: Bearer $token" "$url" >/dev/null 2>&1
437
+ else
438
+ curl -fsS "$url" >/dev/null 2>&1
439
+ fi
440
+ return
441
+ fi
442
+ local node_bin
443
+ node_bin="$(runtime_node_bin)"
444
+ RUNTIME_URL="$url" RUNTIME_TOKEN="$token" "$node_bin" --input-type=module -e "const h=process.env.RUNTIME_TOKEN?{Authorization:'Bearer '+process.env.RUNTIME_TOKEN}:{}; const r=await fetch(process.env.RUNTIME_URL,{headers:h}); process.exit(r.ok?0:1);"
445
+ }
446
+
447
+ runtime_command() {
448
+ local subcommand="$1"
449
+ shift
450
+ local host="${WIKI_MANAGER_RUNTIME_HOST:-0.0.0.0}"
451
+ local port="${WIKI_MANAGER_RUNTIME_PORT:-7788}"
452
+ local pid_file log_file node_bin token
453
+ pid_file="$(runtime_pid_file)"
454
+ log_file="$(runtime_log_file)"
455
+ node_bin="$(runtime_node_bin)"
456
+
457
+ case "$subcommand" in
458
+ up)
459
+ [[ $# -eq 0 ]] || die "runtime up does not take arguments"
460
+ mkdir -p "$MANAGER_RUNTIME_DIR"
461
+ ensure_runtime_node
462
+ token="$(ensure_manager_runtime_token)"
463
+ export WIKI_MANAGER_RUNTIME_TOKEN="$token"
464
+ if runtime_health_check; then
465
+ printf 'agent-runtime already healthy at %s\n' "$(runtime_health_url)"
466
+ return
467
+ fi
468
+ if runtime_pid_running; then
469
+ printf 'agent-runtime process is already running (pid %s), but health is not ready yet.\n' "$(cat "$pid_file")"
470
+ return
471
+ fi
472
+ WIKI_WORKSPACES_DIR="$WORKSPACES_DIR" \
473
+ WIKI_MANAGER_ENV_FILE="$MANAGER_ENV_FILE" \
474
+ WIKI_MANAGER_STATE_DIR="$MANAGER_RUNTIME_DIR" \
475
+ WIKI_MANAGER_RUNTIME_TOKEN="$token" \
476
+ nohup "$node_bin" "$ROOT_DIR/bin/wiki-manager.js" runtime --host "$host" --port "$port" --state-dir "$MANAGER_RUNTIME_DIR" > "$log_file" 2>&1 &
477
+ printf '%s\n' "$!" > "$pid_file"
478
+ sleep 0.5
479
+ printf 'agent-runtime started on http://127.0.0.1:%s (pid %s)\n' "$port" "$(cat "$pid_file")"
480
+ printf 'state: %s\nlog: %s\n' "$MANAGER_RUNTIME_DIR" "$log_file"
481
+ ;;
482
+ down|stop)
483
+ [[ $# -eq 0 ]] || die "runtime down does not take arguments"
484
+ if ! runtime_pid_running; then
485
+ printf 'agent-runtime is not running from %s\n' "$pid_file"
486
+ rm -f "$pid_file"
487
+ return
488
+ fi
489
+ if ! runtime_pid_matches; then
490
+ printf 'refusing to stop pid %s: command does not look like wiki-manager runtime\n' "$(cat "$pid_file")" >&2
491
+ printf 'command: %s\n' "$(runtime_pid_command "$(cat "$pid_file")")" >&2
492
+ rm -f "$pid_file"
493
+ exit 1
494
+ fi
495
+ kill "$(cat "$pid_file")"
496
+ rm -f "$pid_file"
497
+ printf 'agent-runtime stopped\n'
498
+ ;;
499
+ status)
500
+ [[ $# -eq 0 ]] || die "runtime status does not take arguments"
501
+ if runtime_health_check; then
502
+ printf 'agent-runtime healthy at %s\n' "$(runtime_health_url)"
503
+ elif runtime_pid_running; then
504
+ printf 'agent-runtime process running (pid %s), health check failed\n' "$(cat "$pid_file")"
505
+ exit 1
506
+ else
507
+ printf 'agent-runtime is not running\n'
508
+ exit 1
509
+ fi
510
+ ;;
511
+ logs)
512
+ if [[ ! -f "$log_file" ]]; then
513
+ printf 'No runtime log found at %s\n' "$log_file" >&2
514
+ exit 1
515
+ fi
516
+ if [[ $# -eq 0 ]]; then
517
+ tail -100 -f "$log_file"
518
+ else
519
+ tail "$@" "$log_file"
520
+ fi
521
+ ;;
522
+ *)
523
+ die "unknown runtime command: $subcommand (expected: up, down, status, logs)"
524
+ ;;
525
+ esac
526
+ }
527
+
528
+ ensure_runtime_up() {
529
+ if [[ "${WIKI_MANAGER_RUNTIME_AUTOSTART:-1}" == "0" ]]; then
530
+ return
531
+ fi
532
+ if runtime_health_check; then
533
+ return
534
+ fi
535
+ printf 'Starting host agent-runtime...\n'
536
+ runtime_command up
537
+ }
538
+
539
+ start_workspace_services() {
540
+ ensure_runtime_up
541
+ compose_for_workspace "$1" up -d serve mcp-http production-mcp
542
+ }
543
+
365
544
  ensure_manager_endpoints_file() {
366
545
  if [[ ! -f "$MANAGER_ENDPOINTS_FILE" && -f "$ROOT_DIR/mcp.endpoints.example.json" ]]; then
367
546
  mkdir -p "$(dirname "$MANAGER_ENDPOINTS_FILE")"
@@ -478,13 +657,14 @@ compose_for_workspace() {
478
657
  local workspace="$1"
479
658
  shift
480
659
  need_workspace_env "$workspace"
481
- local workspace_env ws_path serve_port mcp_port prod_port project
660
+ local workspace_env ws_path serve_port mcp_port prod_port project agents_data_dir
482
661
  workspace_env="$(workspace_env_file "$workspace")"
483
662
  ws_path="$(normalize_path "$(workspace_value "$workspace" WIKI_WORKSPACE_PATH)")"
484
663
  serve_port="$(workspace_value "$workspace" WIKI_SERVE_PORT 3100)"
485
664
  mcp_port="$(workspace_value "$workspace" WIKI_MCP_PORT 3101)"
486
665
  prod_port="$(workspace_value "$workspace" PRODUCTION_MCP_PORT 3102)"
487
666
  project="$(compose_project "$workspace")"
667
+ agents_data_dir="$(absolute_path "${AGENTS_DATA_DIR:-.agents-data}" "$DEFAULT_MANAGER_DIR")"
488
668
 
489
669
  local compose_env_args=()
490
670
  [[ -f "$MANAGER_ENV_FILE" ]] && compose_env_args+=(--env-file "$MANAGER_ENV_FILE")
@@ -500,6 +680,7 @@ compose_for_workspace() {
500
680
  WIKI_SERVE_PORT="$serve_port" \
501
681
  WIKI_MCP_PORT="$mcp_port" \
502
682
  PRODUCTION_MCP_PORT="$prod_port" \
683
+ AGENTS_DATA_DIR="$agents_data_dir" \
503
684
  docker compose --project-directory "$DEFAULT_MANAGER_DIR" \
504
685
  ${compose_env_args[@]+"${compose_env_args[@]}"} -f "$ROOT_DIR/docker-compose.yml" ${cacert_args[@]+"${cacert_args[@]}"} -p "$project" "$@"
505
686
  }
@@ -547,22 +728,23 @@ cacert_compose_args() {
547
728
 
548
729
  mkdir -p "$MANAGER_RUNTIME_DIR"
549
730
  local override_path="$MANAGER_RUNTIME_DIR/$override_name"
550
- {
551
- printf '# Generated by wiki-manager. Safe to delete.\n'
552
- printf '# Rewritten when --cacert is used with a Docker Compose command.\n'
553
- printf 'services:\n'
554
- while IFS= read -r service || [[ -n "$service" ]]; do
555
- [[ -n "$service" ]] || continue
556
- printf ' %s:\n' "$service"
557
- printf ' volumes:\n'
558
- printf ' - %s:/wiki-manager-ca.pem:ro\n' "$CACERT_PATH"
559
- printf ' environment:\n'
560
- printf ' NODE_EXTRA_CA_CERTS: /wiki-manager-ca.pem\n'
561
- printf ' SSL_CERT_FILE: /wiki-manager-ca.pem\n'
562
- printf ' REQUESTS_CA_BUNDLE: /wiki-manager-ca.pem\n'
563
- printf ' CURL_CA_BUNDLE: /wiki-manager-ca.pem\n'
564
- done <<< "$services"
565
- } > "$override_path"
731
+ if [[ ! -f "$override_path" ]]; then
732
+ {
733
+ printf '# Generated by wiki-manager. Edit freely; delete to regenerate.\n'
734
+ printf 'services:\n'
735
+ while IFS= read -r service || [[ -n "$service" ]]; do
736
+ [[ -n "$service" ]] || continue
737
+ printf ' %s:\n' "$service"
738
+ printf ' volumes:\n'
739
+ printf ' - %s:/wiki-manager-ca.pem:ro\n' "$CACERT_PATH"
740
+ printf ' environment:\n'
741
+ printf ' NODE_EXTRA_CA_CERTS: /wiki-manager-ca.pem\n'
742
+ printf ' SSL_CERT_FILE: /wiki-manager-ca.pem\n'
743
+ printf ' REQUESTS_CA_BUNDLE: /wiki-manager-ca.pem\n'
744
+ printf ' CURL_CA_BUNDLE: /wiki-manager-ca.pem\n'
745
+ done <<< "$services"
746
+ } > "$override_path"
747
+ fi
566
748
  printf '%s\n' -f "$override_path"
567
749
  }
568
750
 
@@ -689,7 +871,8 @@ config_workspace() {
689
871
  local env_file="$target_path/.env"
690
872
  [[ -f "$env_file" ]] && die "workspace already configured: $env_file"
691
873
 
692
- local port_base serve_port mcp_port prod_port wiki_token prod_token
874
+ local port_base serve_port mcp_port prod_port wiki_token prod_token agents_data_dir
875
+ agents_data_dir="$(absolute_path "${AGENTS_DATA_DIR:-.agents-data}" "$DEFAULT_MANAGER_DIR")"
693
876
  port_base="$(find_free_workspace_port_base 3100)"
694
877
  serve_port="$port_base"
695
878
  mcp_port=$((port_base + 1))
@@ -724,6 +907,7 @@ config_workspace() {
724
907
  WIKI_WORKSPACE_PATH="$target_path" \
725
908
  WIKI_SERVE_PORT="$serve_port" \
726
909
  WIKI_MCP_PORT="$mcp_port" \
910
+ AGENTS_DATA_DIR="$agents_data_dir" \
727
911
  docker compose --project-directory "$DEFAULT_MANAGER_DIR" \
728
912
  ${compose_env_args[@]+"${compose_env_args[@]}"} -f "$ROOT_DIR/docker-compose.yml" ${cacert_args[@]+"${cacert_args[@]}"} run --rm wiki init
729
913
 
@@ -738,8 +922,7 @@ up_workspace() {
738
922
  local ws_path
739
923
  ws_path="$(normalize_path "$(workspace_value "$workspace" WIKI_WORKSPACE_PATH)")"
740
924
  ensure_workspace_dirs "$ws_path"
741
-
742
- compose_for_workspace "$workspace" up -d serve mcp-http production-mcp
925
+ start_workspace_services "$workspace"
743
926
 
744
927
  local serve_port prod_port
745
928
  serve_port="$(workspace_value "$workspace" WIKI_SERVE_PORT 3100)"
@@ -747,6 +930,7 @@ up_workspace() {
747
930
  printf 'Workspace UI: http://localhost:%s\n' "$serve_port"
748
931
  printf 'Chat: http://localhost:%s/chat\n' "$serve_port"
749
932
  printf 'Production: http://localhost:%s/mcp/\n' "$prod_port"
933
+ printf 'Runtime: %s\n' "$(runtime_health_check && printf 'healthy at %s' "$(runtime_health_url)" || printf 'start with: wiki-workspace runtime up')"
750
934
  printf 'External MCPs: see %s\n' "$MANAGER_ENDPOINTS_FILE"
751
935
 
752
936
  if [[ "$open_browser" == "1" ]]; then
@@ -797,10 +981,22 @@ main() {
797
981
  exit 0
798
982
  fi
799
983
 
984
+ # runtime <subcommand> [args...]
985
+ if [[ $# -ge 1 && "$1" == "runtime" ]]; then
986
+ local runtime_sub="${2:-status}"
987
+ if [[ $# -ge 2 ]]; then
988
+ shift 2
989
+ else
990
+ shift 1
991
+ fi
992
+ runtime_command "$runtime_sub" "$@"
993
+ exit 0
994
+ fi
995
+
800
996
  [[ $# -ge 2 ]] || { usage; exit 2; }
801
997
 
802
998
  local scope="$1"
803
- [[ "$scope" == "wiki" ]] || die "unknown scope: $scope (expected: wiki, agents, up, config, or list)"
999
+ [[ "$scope" == "wiki" ]] || die "unknown scope: $scope (expected: wiki, agents, runtime, up, config, or list)"
804
1000
  shift
805
1001
  [[ $# -ge 2 ]] || die "wiki requires a workspace and a command"
806
1002
 
@@ -813,13 +1009,14 @@ main() {
813
1009
  run_wiki "$workspace" init "$@"
814
1010
  ;;
815
1011
  up)
816
- compose_for_workspace "$workspace" up -d serve mcp-http production-mcp
1012
+ start_workspace_services "$workspace"
817
1013
  local serve_port production_port
818
1014
  serve_port="$(workspace_value "$workspace" WIKI_SERVE_PORT 3100)"
819
1015
  production_port="$(workspace_value "$workspace" PRODUCTION_MCP_PORT 3102)"
820
1016
  printf 'Workspace UI: http://localhost:%s\n' "$serve_port"
821
1017
  printf 'Chat MCP: http://localhost:%s/chat\n' "$serve_port"
822
1018
  printf 'Production: http://localhost:%s/mcp/\n' "$production_port"
1019
+ printf 'Runtime: %s\n' "$(runtime_health_check && printf 'healthy at %s' "$(runtime_health_url)" || printf 'start with: wiki-workspace runtime up')"
823
1020
  ;;
824
1021
  down)
825
1022
  compose_for_workspace "$workspace" down
@@ -837,6 +1034,7 @@ main() {
837
1034
  die "serve accepts only --open as optional flag; port is configured in the workspace .env"
838
1035
  fi
839
1036
 
1037
+ ensure_runtime_up
840
1038
  printf 'Starting mcp-http...\n'
841
1039
  compose_for_workspace "$workspace" up -d mcp-http
842
1040
  printf 'Starting production-mcp...\n'
@@ -847,6 +1045,7 @@ main() {
847
1045
  printf 'Workspace UI: http://localhost:%s\n' "$serve_port"
848
1046
  printf 'Chat MCP: http://localhost:%s/chat\n' "$serve_port"
849
1047
  printf 'Production: http://localhost:%s/mcp/\n' "$production_port"
1048
+ printf 'Runtime: %s\n' "$(runtime_health_check && printf 'healthy at %s' "$(runtime_health_url)" || printf 'start with: wiki-workspace runtime up')"
850
1049
  printf 'Note: compose logs may show http://localhost:3000, which is the container port.\n'
851
1050
  if [[ $open_browser -eq 1 ]]; then
852
1051
  wait_and_open "http://localhost:${serve_port}" &