@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.
- package/.env.example +20 -0
- package/README.md +35 -1
- package/bin/wiki-manager.js +2 -2
- package/docker-compose.yml +2 -0
- package/mcp.endpoints.example.json +13 -0
- package/package.json +2 -2
- package/src/agent/graph.js +101 -15
- package/src/agent/graph.test.js +145 -0
- package/src/cli/wiki-manager.js +459 -131
- package/src/commands/slash.js +1 -1
- package/src/core/activity.js +14 -0
- package/src/core/agentEvents.js +148 -6
- package/src/core/agentEvents.test.js +145 -4
- package/src/core/agentLoop.js +167 -0
- package/src/core/agentLoop.test.js +85 -0
- package/src/core/cacert.js +4 -3
- package/src/core/dockerCompose.test.js +14 -0
- package/src/core/documentIntake.test.js +7 -7
- package/src/core/env.js +6 -0
- package/src/core/jobQueue.js +47 -16
- package/src/core/mcp.js +120 -10
- package/src/core/mcp.test.js +121 -1
- package/src/core/plan.js +33 -0
- package/src/core/queueStore.js +27 -0
- package/src/core/queueStore.test.js +32 -0
- package/src/core/wikiWorkspace.test.js +24 -0
- package/src/core/workspaces.js +8 -1
- package/src/runtime/approvals.js +113 -0
- package/src/runtime/auth.js +35 -0
- package/src/runtime/auth.test.js +29 -0
- package/src/runtime/client.js +154 -0
- package/src/runtime/lifecycle.js +84 -0
- package/src/runtime/queueStore.js +6 -0
- package/src/runtime/runner.js +413 -0
- package/src/runtime/runner.test.js +270 -0
- package/src/runtime/server.js +216 -0
- package/src/runtime/server.test.js +308 -0
- package/src/runtime/store.js +431 -0
- package/src/runtime/store.test.js +437 -0
- package/src/runtime/supervisor.js +104 -0
- package/src/runtime/supervisor.test.js +240 -0
- package/src/shell/RightPane.tsx +1 -1
- package/src/shell/repl.js +148 -18
- package/src/shell/repl.test.js +38 -0
- package/src/shell/tui.tsx +4 -1
- package/src/shell/useAgent.ts +20 -2
- package/src/shell/useSession.ts +153 -13
- package/wiki-workspace +221 -22
package/src/shell/useSession.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 && !
|
|
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(() =>
|
|
349
|
+
onCleanup(() => {
|
|
350
|
+
if (activityPollTimer) clearInterval(activityPollTimer);
|
|
351
|
+
});
|
|
215
352
|
|
|
216
353
|
const queueFallbackTimer = setInterval(() => {
|
|
217
|
-
if ((
|
|
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(() =>
|
|
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 (
|
|
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:
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
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}" &
|