@dotdrelle/wiki-manager 0.6.17
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 +58 -0
- package/LICENSE +108 -0
- package/README.md +693 -0
- package/agents.docker-compose.yml +96 -0
- package/bin/wiki-manager.js +34 -0
- package/bunfig.toml +1 -0
- package/docker-compose.yml +135 -0
- package/mcp.endpoints.example.json +28 -0
- package/package.json +57 -0
- package/src/agent/graph.js +638 -0
- package/src/agent/llm.js +239 -0
- package/src/cli/wiki-manager.js +389 -0
- package/src/commands/slash.js +1044 -0
- package/src/core/activity.js +236 -0
- package/src/core/activity.test.js +127 -0
- package/src/core/agentEvents.js +238 -0
- package/src/core/agentEvents.test.js +134 -0
- package/src/core/compose.js +310 -0
- package/src/core/documentIntake.js +311 -0
- package/src/core/documentIntake.test.js +121 -0
- package/src/core/env.js +57 -0
- package/src/core/jobQueue.js +197 -0
- package/src/core/mcp.js +402 -0
- package/src/core/mcp.test.js +228 -0
- package/src/core/plan.js +181 -0
- package/src/core/plan.test.js +168 -0
- package/src/core/skills.js +142 -0
- package/src/core/wikirc.js +65 -0
- package/src/core/workspaces.js +81 -0
- package/src/shell/FileEditorDialog.tsx +94 -0
- package/src/shell/LeftPane.tsx +680 -0
- package/src/shell/RightPane.tsx +291 -0
- package/src/shell/SlashDialog.tsx +39 -0
- package/src/shell/renderer.ts +69 -0
- package/src/shell/repl.js +1490 -0
- package/src/shell/tui.tsx +205 -0
- package/src/shell/useAgent.ts +47 -0
- package/src/shell/useSession.ts +370 -0
- package/tsconfig.json +16 -0
- package/wiki-workspace +773 -0
package/wiki-workspace
ADDED
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_SOURCE="${BASH_SOURCE[0]}"
|
|
5
|
+
while [[ -L "$SCRIPT_SOURCE" ]]; do
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_SOURCE")" && pwd)"
|
|
7
|
+
SCRIPT_TARGET="$(readlink "$SCRIPT_SOURCE")"
|
|
8
|
+
if [[ "$SCRIPT_TARGET" = /* ]]; then
|
|
9
|
+
SCRIPT_SOURCE="$SCRIPT_TARGET"
|
|
10
|
+
else
|
|
11
|
+
SCRIPT_SOURCE="$SCRIPT_DIR/$SCRIPT_TARGET"
|
|
12
|
+
fi
|
|
13
|
+
done
|
|
14
|
+
ROOT_DIR="$(cd "$(dirname "$SCRIPT_SOURCE")" && pwd)"
|
|
15
|
+
DEFAULT_MANAGER_DIR="$PWD"
|
|
16
|
+
WORKSPACES_DIR="${WIKI_WORKSPACES_DIR:-$DEFAULT_MANAGER_DIR/workspaces}"
|
|
17
|
+
MANAGER_ENV_FILE="${WIKI_MANAGER_ENV_FILE:-$DEFAULT_MANAGER_DIR/.env}"
|
|
18
|
+
|
|
19
|
+
usage() {
|
|
20
|
+
cat <<'EOF'
|
|
21
|
+
Usage:
|
|
22
|
+
wiki-workspace config <workspace> [path]
|
|
23
|
+
wiki-workspace up <workspace>
|
|
24
|
+
wiki-workspace wiki <workspace> <command> [args...]
|
|
25
|
+
wiki-workspace list
|
|
26
|
+
|
|
27
|
+
Commands:
|
|
28
|
+
config <workspace> [path] Create workspace folder, auto-select ports, run wiki init
|
|
29
|
+
up <workspace> [--open] Start workspace stack: wiki + production
|
|
30
|
+
list List configured workspaces
|
|
31
|
+
|
|
32
|
+
agents up [--pull] Start all external agents (cme, documents, mailer)
|
|
33
|
+
agents down Stop all external agents
|
|
34
|
+
agents logs [args...] Follow external agent logs
|
|
35
|
+
agents pull Pull latest external agent images
|
|
36
|
+
agents status Check reachability of configured MCP endpoints
|
|
37
|
+
|
|
38
|
+
wiki <workspace> init Initialize the workspace with wiki init
|
|
39
|
+
wiki <workspace> up Start llm-wiki serve + mcp-http + production-mcp
|
|
40
|
+
wiki <workspace> down Stop all services for this workspace
|
|
41
|
+
wiki <workspace> logs [args...] Follow workspace service logs (tail 100 by default)
|
|
42
|
+
wiki <workspace> serve [--open] Start the web UI in foreground (--open launches browser)
|
|
43
|
+
wiki <workspace> doctor Run wiki doctor
|
|
44
|
+
wiki <workspace> ingest Run wiki ingest
|
|
45
|
+
wiki <workspace> build Run wiki build
|
|
46
|
+
wiki <workspace> export Run wiki export
|
|
47
|
+
wiki <workspace> run <args...> Run arbitrary wiki CLI args
|
|
48
|
+
|
|
49
|
+
Configuration:
|
|
50
|
+
workspaces/<workspace>/.env
|
|
51
|
+
Override directory: WIKI_WORKSPACES_DIR=/path/to/dir
|
|
52
|
+
EOF
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
die() {
|
|
56
|
+
printf 'Error: %s\n' "$*" >&2
|
|
57
|
+
exit 1
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
normalize_path() {
|
|
61
|
+
local value="$1"
|
|
62
|
+
if command -v wslpath >/dev/null 2>&1 && [[ "$value" =~ ^[A-Za-z]:[\\/] ]]; then
|
|
63
|
+
wslpath -u "$value"
|
|
64
|
+
return
|
|
65
|
+
fi
|
|
66
|
+
printf '%s\n' "$value"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
absolute_path() {
|
|
70
|
+
local value="$1"
|
|
71
|
+
local base="${2:-$ROOT_DIR}"
|
|
72
|
+
value="$(normalize_path "$value")"
|
|
73
|
+
if [[ "$value" = /* ]]; then
|
|
74
|
+
printf '%s\n' "$value"
|
|
75
|
+
else
|
|
76
|
+
printf '%s\n' "$base/$value"
|
|
77
|
+
fi
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
workspace_env_file() {
|
|
81
|
+
local workspace="$1"
|
|
82
|
+
printf '%s/%s/.env\n' "$WORKSPACES_DIR" "$workspace"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
need_workspace_env() {
|
|
86
|
+
local workspace="$1"
|
|
87
|
+
local file
|
|
88
|
+
file="$(workspace_env_file "$workspace")"
|
|
89
|
+
[[ -f "$file" ]] || die "missing workspace env: $file — run: wiki-workspace config $workspace"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
env_value() {
|
|
93
|
+
local file="$1"
|
|
94
|
+
local key="$2"
|
|
95
|
+
local default="${3-}"
|
|
96
|
+
local line value
|
|
97
|
+
[[ -f "$file" ]] || { printf '%s\n' "$default"; return 0; }
|
|
98
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
99
|
+
line="${line%$'\r'}"
|
|
100
|
+
[[ "$line" == "$key="* ]] || continue
|
|
101
|
+
value="${line#*=}"
|
|
102
|
+
if [[ "$value" == \"*\" && "$value" == *\" ]]; then
|
|
103
|
+
value="${value:1:${#value}-2}"
|
|
104
|
+
elif [[ "$value" == \'*\' && "$value" == *\' ]]; then
|
|
105
|
+
value="${value:1:${#value}-2}"
|
|
106
|
+
fi
|
|
107
|
+
printf '%s\n' "$value"
|
|
108
|
+
return 0
|
|
109
|
+
done < "$file"
|
|
110
|
+
printf '%s\n' "$default"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
generate_token() {
|
|
114
|
+
if command -v openssl >/dev/null 2>&1; then
|
|
115
|
+
openssl rand -hex 32
|
|
116
|
+
return
|
|
117
|
+
fi
|
|
118
|
+
if [[ -r /dev/urandom ]]; then
|
|
119
|
+
od -An -N32 -tx1 /dev/urandom | tr -d ' \n'
|
|
120
|
+
printf '\n'
|
|
121
|
+
return
|
|
122
|
+
fi
|
|
123
|
+
die "cannot generate MCP auth token: openssl and /dev/urandom are unavailable"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
set_env_value() {
|
|
127
|
+
local file="$1"
|
|
128
|
+
local key="$2"
|
|
129
|
+
local value="$3"
|
|
130
|
+
local tmp="${file}.tmp.$$"
|
|
131
|
+
local wrote=0 line
|
|
132
|
+
|
|
133
|
+
if [[ -f "$file" ]]; then
|
|
134
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
135
|
+
line="${line%$'\r'}"
|
|
136
|
+
if [[ "$line" == "$key="* ]]; then
|
|
137
|
+
printf '%s=%s\n' "$key" "$value"
|
|
138
|
+
wrote=1
|
|
139
|
+
else
|
|
140
|
+
printf '%s\n' "$line"
|
|
141
|
+
fi
|
|
142
|
+
done < "$file" > "$tmp"
|
|
143
|
+
else
|
|
144
|
+
: > "$tmp"
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
if [[ "$wrote" -eq 0 ]]; then
|
|
148
|
+
if [[ -s "$tmp" ]]; then
|
|
149
|
+
printf '\n' >> "$tmp"
|
|
150
|
+
fi
|
|
151
|
+
printf '%s=%s\n' "$key" "$value" >> "$tmp"
|
|
152
|
+
fi
|
|
153
|
+
mv "$tmp" "$file"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
agents_compose() {
|
|
157
|
+
local subcommand="$1"
|
|
158
|
+
shift
|
|
159
|
+
ensure_manager_endpoints_file
|
|
160
|
+
local agents_compose_file="$ROOT_DIR/agents.docker-compose.yml"
|
|
161
|
+
[[ -f "$agents_compose_file" ]] || die "agents compose file not found: $agents_compose_file"
|
|
162
|
+
|
|
163
|
+
local agents_data_dir workspaces_root
|
|
164
|
+
agents_data_dir="$(absolute_path "${AGENTS_DATA_DIR:-.agents-data}" "$DEFAULT_MANAGER_DIR")"
|
|
165
|
+
workspaces_root="$(absolute_path "$WORKSPACES_DIR" "$DEFAULT_MANAGER_DIR")"
|
|
166
|
+
|
|
167
|
+
local compose_env_args=()
|
|
168
|
+
[[ -f "$MANAGER_ENV_FILE" ]] && compose_env_args+=(--env-file "$MANAGER_ENV_FILE")
|
|
169
|
+
|
|
170
|
+
_agents_dc() {
|
|
171
|
+
WORKSPACES_ROOT="$workspaces_root" AGENTS_DATA_DIR="$agents_data_dir" \
|
|
172
|
+
docker compose "${compose_env_args[@]}" -f "$agents_compose_file" -p wiki-agents "$@"
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
case "$subcommand" in
|
|
176
|
+
up)
|
|
177
|
+
local do_pull=false
|
|
178
|
+
if [[ "${1:-}" == "--pull" ]]; then
|
|
179
|
+
do_pull=true
|
|
180
|
+
shift
|
|
181
|
+
fi
|
|
182
|
+
[[ $# -eq 0 ]] || die "agents up accepts only --pull"
|
|
183
|
+
mkdir -p "$agents_data_dir/cme" "$agents_data_dir/documents/input" "$agents_data_dir/documents/output" "$agents_data_dir/documents/uploads"
|
|
184
|
+
mkdir -p "$workspaces_root"
|
|
185
|
+
$do_pull && _agents_dc pull
|
|
186
|
+
_agents_dc up -d
|
|
187
|
+
printf 'Workspaces root: %s\n' "$workspaces_root"
|
|
188
|
+
printf 'Agents data: %s\n' "$agents_data_dir"
|
|
189
|
+
printf 'Agents: cme=:%s documents=:%s mailer=:%s\n' \
|
|
190
|
+
"${CME_MCP_PORT:-3336}" "${DOCUMENTS_MCP_PORT:-3337}" "${MAILER_MCP_PORT:-3335}"
|
|
191
|
+
;;
|
|
192
|
+
down)
|
|
193
|
+
[[ $# -eq 0 ]] || die "agents down does not take arguments"
|
|
194
|
+
_agents_dc down
|
|
195
|
+
;;
|
|
196
|
+
logs)
|
|
197
|
+
local log_args=()
|
|
198
|
+
read_lines_into_array log_args logs_args "$@"
|
|
199
|
+
_agents_dc logs "${log_args[@]}"
|
|
200
|
+
;;
|
|
201
|
+
pull)
|
|
202
|
+
[[ $# -eq 0 ]] || die "agents pull does not take arguments"
|
|
203
|
+
_agents_dc pull
|
|
204
|
+
;;
|
|
205
|
+
status)
|
|
206
|
+
[[ $# -eq 0 ]] || die "agents status does not take arguments"
|
|
207
|
+
local endpoints_file="$ROOT_DIR/mcp.endpoints.json"
|
|
208
|
+
if [[ ! -f "$endpoints_file" ]]; then
|
|
209
|
+
printf 'No mcp.endpoints.json found at %s\n' "$endpoints_file" >&2
|
|
210
|
+
exit 1
|
|
211
|
+
fi
|
|
212
|
+
command -v node >/dev/null 2>&1 || die "agents status requires node"
|
|
213
|
+
node - "$endpoints_file" "$MANAGER_ENV_FILE" <<'NODE'
|
|
214
|
+
import fs from 'node:fs';
|
|
215
|
+
|
|
216
|
+
const [, , endpointsFile, envFile] = process.argv;
|
|
217
|
+
|
|
218
|
+
function parseEnvValue(value) {
|
|
219
|
+
const trimmed = value.trim();
|
|
220
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) return trimmed.slice(1, -1);
|
|
221
|
+
if (trimmed.startsWith("'") && trimmed.endsWith("'")) return trimmed.slice(1, -1);
|
|
222
|
+
return trimmed;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function loadEnv(filePath) {
|
|
226
|
+
const values = { ...process.env };
|
|
227
|
+
try {
|
|
228
|
+
for (const raw of fs.readFileSync(filePath, 'utf8').split(/\r?\n/)) {
|
|
229
|
+
const line = raw.trim();
|
|
230
|
+
if (!line || line.startsWith('#')) continue;
|
|
231
|
+
const index = line.indexOf('=');
|
|
232
|
+
if (index === -1) continue;
|
|
233
|
+
const key = line.slice(0, index).trim();
|
|
234
|
+
if (!(key in values)) values[key] = parseEnvValue(line.slice(index + 1));
|
|
235
|
+
}
|
|
236
|
+
} catch (err) {
|
|
237
|
+
if (err.code !== 'ENOENT') throw err;
|
|
238
|
+
}
|
|
239
|
+
return values;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function interpolate(value, env) {
|
|
243
|
+
return String(value).replace(/\$\{([^}]+)\}/g, (_match, expr) => {
|
|
244
|
+
const sep = expr.indexOf(':-');
|
|
245
|
+
if (sep !== -1) return env[expr.slice(0, sep)] ?? expr.slice(sep + 2);
|
|
246
|
+
return env[expr] ?? '';
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function runtimeUrl(url) {
|
|
251
|
+
try {
|
|
252
|
+
const parsed = new URL(url);
|
|
253
|
+
if (parsed.hostname === 'host.docker.internal') parsed.hostname = 'localhost';
|
|
254
|
+
return parsed.toString();
|
|
255
|
+
} catch {
|
|
256
|
+
return url;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function normalizeHeaders(headers, env) {
|
|
261
|
+
if (!headers || typeof headers !== 'object' || Array.isArray(headers)) return {};
|
|
262
|
+
return Object.fromEntries(
|
|
263
|
+
Object.entries(headers)
|
|
264
|
+
.filter(([key, value]) => key && typeof value === 'string' && value)
|
|
265
|
+
.map(([key, value]) => [key.toLowerCase(), interpolate(value, env)]),
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function check(url, headers) {
|
|
270
|
+
const controller = new AbortController();
|
|
271
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
272
|
+
try {
|
|
273
|
+
const response = await fetch(runtimeUrl(url), {
|
|
274
|
+
method: 'POST',
|
|
275
|
+
signal: controller.signal,
|
|
276
|
+
headers: {
|
|
277
|
+
accept: 'application/json, text/event-stream',
|
|
278
|
+
'content-type': 'application/json',
|
|
279
|
+
...headers,
|
|
280
|
+
},
|
|
281
|
+
body: JSON.stringify({
|
|
282
|
+
jsonrpc: '2.0',
|
|
283
|
+
id: 1,
|
|
284
|
+
method: 'initialize',
|
|
285
|
+
params: {
|
|
286
|
+
protocolVersion: '2025-06-18',
|
|
287
|
+
capabilities: {},
|
|
288
|
+
clientInfo: { name: 'wiki-workspace', version: 'status' },
|
|
289
|
+
},
|
|
290
|
+
}),
|
|
291
|
+
});
|
|
292
|
+
await response.text();
|
|
293
|
+
return response.ok;
|
|
294
|
+
} catch {
|
|
295
|
+
return false;
|
|
296
|
+
} finally {
|
|
297
|
+
clearTimeout(timeout);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const env = loadEnv(envFile);
|
|
302
|
+
const config = JSON.parse(fs.readFileSync(endpointsFile, 'utf8'));
|
|
303
|
+
const servers = config.mcpServers ?? config.servers ?? {};
|
|
304
|
+
|
|
305
|
+
for (const [name, endpoint] of Object.entries(servers)) {
|
|
306
|
+
const url = interpolate(endpoint?.url ?? '', env);
|
|
307
|
+
if (!url) continue;
|
|
308
|
+
const ok = await check(url, normalizeHeaders(endpoint.headers, env));
|
|
309
|
+
console.log(`${name.padEnd(14)} ${url} ${ok ? 'ok' : 'unreachable'}`);
|
|
310
|
+
}
|
|
311
|
+
NODE
|
|
312
|
+
;;
|
|
313
|
+
*)
|
|
314
|
+
die "unknown agents command: $subcommand (expected: up, down, logs, pull, status)"
|
|
315
|
+
;;
|
|
316
|
+
esac
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
ensure_manager_endpoints_file() {
|
|
320
|
+
local endpoints_file="$ROOT_DIR/mcp.endpoints.json"
|
|
321
|
+
if [[ ! -f "$endpoints_file" && -f "$ROOT_DIR/mcp.endpoints.example.json" ]]; then
|
|
322
|
+
cp "$ROOT_DIR/mcp.endpoints.example.json" "$endpoints_file"
|
|
323
|
+
printf 'Created %s (from mcp.endpoints.example.json — add headers/tokens as needed)\n' "$endpoints_file"
|
|
324
|
+
fi
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
workspace_value() {
|
|
328
|
+
local workspace="$1"
|
|
329
|
+
local key="$2"
|
|
330
|
+
local default="${3-}"
|
|
331
|
+
env_value "$(workspace_env_file "$workspace")" "$key" "$default"
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
used_workspace_ports() {
|
|
335
|
+
local entry file key val
|
|
336
|
+
for entry in "$WORKSPACES_DIR"/*; do
|
|
337
|
+
[[ -d "$entry" ]] || continue
|
|
338
|
+
file="$entry/.env"
|
|
339
|
+
[[ -f "$file" ]] || continue
|
|
340
|
+
for key in WIKI_SERVE_PORT WIKI_MCP_PORT PRODUCTION_MCP_PORT; do
|
|
341
|
+
val="$(env_value "$file" "$key" "")"
|
|
342
|
+
[[ "$val" =~ ^[0-9]+$ ]] && printf '%s\n' "$val"
|
|
343
|
+
done
|
|
344
|
+
done
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
is_port_in_use() {
|
|
348
|
+
local port="$1"
|
|
349
|
+
if command -v ss >/dev/null 2>&1; then
|
|
350
|
+
ss -tln 2>/dev/null | grep -q ":${port} "
|
|
351
|
+
elif command -v lsof >/dev/null 2>&1; then
|
|
352
|
+
lsof -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1
|
|
353
|
+
else
|
|
354
|
+
(echo >/dev/tcp/127.0.0.1/"$port") 2>/dev/null
|
|
355
|
+
fi
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
find_free_port() {
|
|
359
|
+
local start="${1:-3100}"
|
|
360
|
+
local port="$start"
|
|
361
|
+
local used_ports=()
|
|
362
|
+
read_lines_into_array used_ports used_workspace_ports
|
|
363
|
+
while true; do
|
|
364
|
+
local taken=0 p
|
|
365
|
+
if (( ${#used_ports[@]} > 0 )); then
|
|
366
|
+
for p in "${used_ports[@]}"; do
|
|
367
|
+
[[ "$p" == "$port" ]] && { taken=1; break; }
|
|
368
|
+
done
|
|
369
|
+
fi
|
|
370
|
+
if [[ "$taken" -eq 0 ]] && ! is_port_in_use "$port"; then
|
|
371
|
+
printf '%s\n' "$port"
|
|
372
|
+
return 0
|
|
373
|
+
fi
|
|
374
|
+
port=$((port + 1))
|
|
375
|
+
done
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
find_free_workspace_port_base() {
|
|
379
|
+
local base="${1:-3100}"
|
|
380
|
+
local used_ports=()
|
|
381
|
+
read_lines_into_array used_ports used_workspace_ports
|
|
382
|
+
while true; do
|
|
383
|
+
local taken=0 offset port p
|
|
384
|
+
for offset in 0 1 2; do
|
|
385
|
+
port=$((base + offset))
|
|
386
|
+
if (( ${#used_ports[@]} > 0 )); then
|
|
387
|
+
for p in "${used_ports[@]}"; do
|
|
388
|
+
[[ "$p" == "$port" ]] && { taken=1; break; }
|
|
389
|
+
done
|
|
390
|
+
fi
|
|
391
|
+
[[ "$taken" -eq 1 ]] && break
|
|
392
|
+
if is_port_in_use "$port"; then
|
|
393
|
+
taken=1
|
|
394
|
+
break
|
|
395
|
+
fi
|
|
396
|
+
done
|
|
397
|
+
if [[ "$taken" -eq 0 ]]; then
|
|
398
|
+
printf '%s\n' "$base"
|
|
399
|
+
return 0
|
|
400
|
+
fi
|
|
401
|
+
base=$((base + 100))
|
|
402
|
+
done
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
list_workspaces() {
|
|
406
|
+
mkdir -p "$WORKSPACES_DIR"
|
|
407
|
+
local found=0 entry file name path serve mcp prod
|
|
408
|
+
|
|
409
|
+
for entry in "$WORKSPACES_DIR"/*; do
|
|
410
|
+
[[ -d "$entry" ]] || continue
|
|
411
|
+
file="$entry/.env"
|
|
412
|
+
[[ -f "$file" ]] || continue
|
|
413
|
+
found=1
|
|
414
|
+
name="$(env_value "$file" WORKSPACE_NAME "$(basename "$entry")")"
|
|
415
|
+
path="$(env_value "$file" WIKI_WORKSPACE_PATH "-")"
|
|
416
|
+
serve="$(env_value "$file" WIKI_SERVE_PORT "-")"
|
|
417
|
+
mcp="$(env_value "$file" WIKI_MCP_PORT "-")"
|
|
418
|
+
prod="$(env_value "$file" PRODUCTION_MCP_PORT "-")"
|
|
419
|
+
printf '%s\tpath=%s\tserve=%s\tmcp=%s\tprod=%s\n' \
|
|
420
|
+
"$name" "$path" "$serve" "$mcp" "$prod"
|
|
421
|
+
done
|
|
422
|
+
|
|
423
|
+
[[ "$found" -eq 1 ]] || printf 'No workspaces configured in %s\n' "$WORKSPACES_DIR"
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
compose_project() {
|
|
427
|
+
local workspace="$1"
|
|
428
|
+
printf 'wiki-%s' "$workspace" | tr -c '[:alnum:]_-' '-'
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
compose_for_workspace() {
|
|
432
|
+
local workspace="$1"
|
|
433
|
+
shift
|
|
434
|
+
need_workspace_env "$workspace"
|
|
435
|
+
local workspace_env ws_path serve_port mcp_port prod_port project
|
|
436
|
+
workspace_env="$(workspace_env_file "$workspace")"
|
|
437
|
+
ws_path="$(normalize_path "$(workspace_value "$workspace" WIKI_WORKSPACE_PATH)")"
|
|
438
|
+
serve_port="$(workspace_value "$workspace" WIKI_SERVE_PORT 3100)"
|
|
439
|
+
mcp_port="$(workspace_value "$workspace" WIKI_MCP_PORT 3101)"
|
|
440
|
+
prod_port="$(workspace_value "$workspace" PRODUCTION_MCP_PORT 3102)"
|
|
441
|
+
project="$(compose_project "$workspace")"
|
|
442
|
+
|
|
443
|
+
local compose_env_args=()
|
|
444
|
+
[[ -f "$MANAGER_ENV_FILE" ]] && compose_env_args+=(--env-file "$MANAGER_ENV_FILE")
|
|
445
|
+
compose_env_args+=(--env-file "$workspace_env")
|
|
446
|
+
|
|
447
|
+
WIKI_WORKSPACE_PATH="$ws_path" \
|
|
448
|
+
WIKI_SERVE_PORT="$serve_port" \
|
|
449
|
+
WIKI_MCP_PORT="$mcp_port" \
|
|
450
|
+
PRODUCTION_MCP_PORT="$prod_port" \
|
|
451
|
+
docker compose "${compose_env_args[@]}" -f "$ROOT_DIR/docker-compose.yml" -p "$project" "$@"
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
logs_args() {
|
|
455
|
+
if [[ $# -eq 0 ]]; then
|
|
456
|
+
printf '%s\n' --tail=100 -f
|
|
457
|
+
else
|
|
458
|
+
printf '%s\n' "$@"
|
|
459
|
+
fi
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
read_lines_into_array() {
|
|
464
|
+
local array_name="$1"
|
|
465
|
+
shift
|
|
466
|
+
[[ "$array_name" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || die "invalid array name: $array_name"
|
|
467
|
+
|
|
468
|
+
eval "$array_name=()"
|
|
469
|
+
local line
|
|
470
|
+
while IFS= read -r line; do
|
|
471
|
+
eval "$array_name+=(\"\$line\")"
|
|
472
|
+
done < <("$@")
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
run_wiki() {
|
|
476
|
+
local workspace="$1"
|
|
477
|
+
shift
|
|
478
|
+
local ws_path
|
|
479
|
+
ws_path="$(normalize_path "$(workspace_value "$workspace" WIKI_WORKSPACE_PATH)")"
|
|
480
|
+
ensure_workspace_dirs "$ws_path"
|
|
481
|
+
compose_for_workspace "$workspace" run --rm wiki "$@"
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
open_app_mode() {
|
|
485
|
+
local url="$1"
|
|
486
|
+
case "$(uname -s)" in
|
|
487
|
+
Darwin)
|
|
488
|
+
open -na "Google Chrome" --args "--app=${url}" 2>/dev/null \
|
|
489
|
+
|| open -na "Microsoft Edge" --args "--app=${url}" 2>/dev/null \
|
|
490
|
+
|| open -a Safari "${url}" 2>/dev/null \
|
|
491
|
+
|| open "${url}"
|
|
492
|
+
;;
|
|
493
|
+
Linux)
|
|
494
|
+
# WSL2: Windows browsers are not on PATH — probe known Windows paths via wslpath.
|
|
495
|
+
if command -v wslpath >/dev/null 2>&1; then
|
|
496
|
+
local wsl_candidate wsl_path
|
|
497
|
+
for wsl_candidate in \
|
|
498
|
+
"C:/Program Files/Google/Chrome/Application/chrome.exe" \
|
|
499
|
+
"C:/Program Files (x86)/Google/Chrome/Application/chrome.exe" \
|
|
500
|
+
"C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe" \
|
|
501
|
+
"C:/Program Files/Microsoft/Edge/Application/msedge.exe"; do
|
|
502
|
+
wsl_path="$(wslpath -u "$wsl_candidate" 2>/dev/null)" || continue
|
|
503
|
+
[[ -f "$wsl_path" ]] || continue
|
|
504
|
+
"$wsl_path" "--app=${url}" &
|
|
505
|
+
return
|
|
506
|
+
done
|
|
507
|
+
for wsl_candidate in \
|
|
508
|
+
"C:/Program Files/Mozilla Firefox/firefox.exe"; do
|
|
509
|
+
wsl_path="$(wslpath -u "$wsl_candidate" 2>/dev/null)" || continue
|
|
510
|
+
[[ -f "$wsl_path" ]] || continue
|
|
511
|
+
"$wsl_path" "${url}" &
|
|
512
|
+
return
|
|
513
|
+
done
|
|
514
|
+
fi
|
|
515
|
+
for candidate in google-chrome google-chrome-stable chromium chromium-browser microsoft-edge microsoft-edge-stable; do
|
|
516
|
+
if command -v "$candidate" >/dev/null 2>&1; then
|
|
517
|
+
"$candidate" "--app=${url}" &
|
|
518
|
+
return
|
|
519
|
+
fi
|
|
520
|
+
done
|
|
521
|
+
xdg-open "${url}" 2>/dev/null || true
|
|
522
|
+
;;
|
|
523
|
+
MINGW*|MSYS*|CYGWIN*)
|
|
524
|
+
for candidate in \
|
|
525
|
+
"C:/Program Files/Google/Chrome/Application/chrome.exe" \
|
|
526
|
+
"C:/Program Files (x86)/Google/Chrome/Application/chrome.exe" \
|
|
527
|
+
"C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe" \
|
|
528
|
+
"C:/Program Files/Microsoft/Edge/Application/msedge.exe"; do
|
|
529
|
+
if [[ -f "$candidate" ]]; then
|
|
530
|
+
"$candidate" "--app=${url}" &
|
|
531
|
+
return
|
|
532
|
+
fi
|
|
533
|
+
done
|
|
534
|
+
for candidate in \
|
|
535
|
+
"C:/Program Files/Mozilla Firefox/firefox.exe"; do
|
|
536
|
+
if [[ -f "$candidate" ]]; then
|
|
537
|
+
"$candidate" "${url}" &
|
|
538
|
+
return
|
|
539
|
+
fi
|
|
540
|
+
done
|
|
541
|
+
start "" "${url}" || true
|
|
542
|
+
;;
|
|
543
|
+
esac
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
wait_and_open() {
|
|
547
|
+
local url="$1"
|
|
548
|
+
local max_attempts=30
|
|
549
|
+
local i=0
|
|
550
|
+
while (( i < max_attempts )); do
|
|
551
|
+
sleep 1
|
|
552
|
+
if curl -sf "${url}" >/dev/null 2>&1; then
|
|
553
|
+
open_app_mode "${url}"
|
|
554
|
+
return
|
|
555
|
+
fi
|
|
556
|
+
(( i++ )) || true
|
|
557
|
+
done
|
|
558
|
+
printf 'wiki-workspace: server did not become ready at %s after %ds, opening anyway\n' "${url}" "$max_attempts" >&2
|
|
559
|
+
open_app_mode "${url}"
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
ensure_workspace_dirs() {
|
|
563
|
+
local ws_path="$1"
|
|
564
|
+
mkdir -p "$ws_path/.wiki/vector-index"
|
|
565
|
+
mkdir -p "$ws_path/raw/untracked"
|
|
566
|
+
# The llm-wiki container runs as the unprivileged `node` user. On WSL2,
|
|
567
|
+
# Rancher Desktop, and bind mounts created by the host, runtime folders can
|
|
568
|
+
# end up owned by a different UID and become read-only from the container.
|
|
569
|
+
chmod -R a+rwX "$ws_path/.wiki" "$ws_path/raw" 2>/dev/null || true
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
register_workspace_path() {
|
|
573
|
+
local workspace="$1"
|
|
574
|
+
local target_path="$2"
|
|
575
|
+
local registry_path registry_abs
|
|
576
|
+
registry_path="$WORKSPACES_DIR/$workspace"
|
|
577
|
+
registry_abs="$(absolute_path "$registry_path")"
|
|
578
|
+
|
|
579
|
+
[[ "$target_path" == "$registry_abs" ]] && return 0
|
|
580
|
+
|
|
581
|
+
mkdir -p "$WORKSPACES_DIR"
|
|
582
|
+
if [[ -e "$registry_path" || -L "$registry_path" ]]; then
|
|
583
|
+
die "workspace registry path already exists: $registry_path"
|
|
584
|
+
fi
|
|
585
|
+
ln -s "$target_path" "$registry_path"
|
|
586
|
+
printf 'Registered %s -> %s\n' "$registry_path" "$target_path"
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
config_workspace() {
|
|
590
|
+
local workspace="$1"
|
|
591
|
+
local target_path="${2:-$WORKSPACES_DIR/$workspace}"
|
|
592
|
+
target_path="$(absolute_path "$target_path")"
|
|
593
|
+
ensure_manager_endpoints_file
|
|
594
|
+
|
|
595
|
+
local env_file="$target_path/.env"
|
|
596
|
+
[[ -f "$env_file" ]] && die "workspace already configured: $env_file"
|
|
597
|
+
|
|
598
|
+
local port_base serve_port mcp_port prod_port wiki_token prod_token
|
|
599
|
+
port_base="$(find_free_workspace_port_base 3100)"
|
|
600
|
+
serve_port="$port_base"
|
|
601
|
+
mcp_port=$((port_base + 1))
|
|
602
|
+
prod_port=$((port_base + 2))
|
|
603
|
+
wiki_token="$(generate_token)"
|
|
604
|
+
prod_token="$(generate_token)"
|
|
605
|
+
|
|
606
|
+
mkdir -p "$target_path"
|
|
607
|
+
register_workspace_path "$workspace" "$target_path"
|
|
608
|
+
ensure_workspace_dirs "$target_path"
|
|
609
|
+
# Ensure the container (node, UID 1000) can write to the workspace.
|
|
610
|
+
# Needed on WSL2 when the host UID differs from the container UID.
|
|
611
|
+
chmod -R a+rwX "$target_path" 2>/dev/null || true
|
|
612
|
+
|
|
613
|
+
printf 'WORKSPACE_NAME=%s\nWIKI_WORKSPACE_PATH=%s\nWIKI_SERVE_PORT=%s\nWIKI_MCP_PORT=%s\nPRODUCTION_MCP_PORT=%s\nPRODUCTION_REQUIRE_CONFIRMATION=false\nWIKI_MCP_AUTH_TOKEN=%s\nPRODUCTION_MCP_AUTH_TOKEN=%s\n' \
|
|
614
|
+
"$workspace" "$target_path" "$serve_port" "$mcp_port" "$prod_port" "$wiki_token" "$prod_token" \
|
|
615
|
+
> "$env_file"
|
|
616
|
+
|
|
617
|
+
printf 'Created %s\n' "$env_file"
|
|
618
|
+
printf 'Ports serve=%s mcp=%s production=%s\n' \
|
|
619
|
+
"$serve_port" "$mcp_port" "$prod_port"
|
|
620
|
+
|
|
621
|
+
local compose_env_args=()
|
|
622
|
+
[[ -f "$MANAGER_ENV_FILE" ]] && compose_env_args+=(--env-file "$MANAGER_ENV_FILE")
|
|
623
|
+
compose_env_args+=(--env-file "$env_file")
|
|
624
|
+
|
|
625
|
+
WIKI_WORKSPACE_PATH="$target_path" \
|
|
626
|
+
WIKI_SERVE_PORT="$serve_port" \
|
|
627
|
+
WIKI_MCP_PORT="$mcp_port" \
|
|
628
|
+
docker compose "${compose_env_args[@]}" -f "$ROOT_DIR/docker-compose.yml" run --rm wiki init
|
|
629
|
+
|
|
630
|
+
printf 'Workspace ready: %s\n' "$target_path"
|
|
631
|
+
printf 'Start with: wiki-workspace up %s\n' "$workspace"
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
up_workspace() {
|
|
635
|
+
local workspace="$1"
|
|
636
|
+
local open_browser="${2:-0}"
|
|
637
|
+
need_workspace_env "$workspace"
|
|
638
|
+
local ws_path
|
|
639
|
+
ws_path="$(normalize_path "$(workspace_value "$workspace" WIKI_WORKSPACE_PATH)")"
|
|
640
|
+
ensure_workspace_dirs "$ws_path"
|
|
641
|
+
|
|
642
|
+
compose_for_workspace "$workspace" up -d serve mcp-http production-mcp
|
|
643
|
+
|
|
644
|
+
local serve_port prod_port
|
|
645
|
+
serve_port="$(workspace_value "$workspace" WIKI_SERVE_PORT 3100)"
|
|
646
|
+
prod_port="$(workspace_value "$workspace" PRODUCTION_MCP_PORT 3102)"
|
|
647
|
+
printf 'Workspace UI: http://localhost:%s\n' "$serve_port"
|
|
648
|
+
printf 'Chat: http://localhost:%s/chat\n' "$serve_port"
|
|
649
|
+
printf 'Production: http://localhost:%s/mcp/\n' "$prod_port"
|
|
650
|
+
printf 'External MCPs: see %s\n' "$ROOT_DIR/mcp.endpoints.json"
|
|
651
|
+
|
|
652
|
+
if [[ "$open_browser" == "1" ]]; then
|
|
653
|
+
wait_and_open "http://localhost:${serve_port}/chat" &
|
|
654
|
+
fi
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
main() {
|
|
658
|
+
if [[ $# -eq 1 && "$1" =~ ^(-h|--help|help)$ ]]; then
|
|
659
|
+
usage
|
|
660
|
+
exit 0
|
|
661
|
+
fi
|
|
662
|
+
|
|
663
|
+
if [[ $# -eq 1 && "$1" == "list" ]]; then
|
|
664
|
+
list_workspaces
|
|
665
|
+
exit 0
|
|
666
|
+
fi
|
|
667
|
+
|
|
668
|
+
# config <workspace> [path]
|
|
669
|
+
if [[ $# -ge 2 && "$1" == "config" ]]; then
|
|
670
|
+
local cfg_workspace="$2"
|
|
671
|
+
local cfg_path="${3:-}"
|
|
672
|
+
if [[ -n "$cfg_path" ]]; then
|
|
673
|
+
config_workspace "$cfg_workspace" "$cfg_path"
|
|
674
|
+
else
|
|
675
|
+
config_workspace "$cfg_workspace"
|
|
676
|
+
fi
|
|
677
|
+
exit 0
|
|
678
|
+
fi
|
|
679
|
+
|
|
680
|
+
# up <workspace> [--open]
|
|
681
|
+
if [[ $# -ge 2 && "$1" == "up" ]]; then
|
|
682
|
+
local up_open=0
|
|
683
|
+
[[ "${3:-}" == "--open" ]] && up_open=1
|
|
684
|
+
up_workspace "$2" "$up_open"
|
|
685
|
+
exit 0
|
|
686
|
+
fi
|
|
687
|
+
|
|
688
|
+
# agents <subcommand> [args...]
|
|
689
|
+
if [[ $# -ge 1 && "$1" == "agents" ]]; then
|
|
690
|
+
local agents_sub="${2:-up}"
|
|
691
|
+
if [[ $# -ge 2 ]]; then
|
|
692
|
+
shift 2
|
|
693
|
+
else
|
|
694
|
+
shift 1
|
|
695
|
+
fi
|
|
696
|
+
agents_compose "$agents_sub" "$@"
|
|
697
|
+
exit 0
|
|
698
|
+
fi
|
|
699
|
+
|
|
700
|
+
[[ $# -ge 2 ]] || { usage; exit 2; }
|
|
701
|
+
|
|
702
|
+
local scope="$1"
|
|
703
|
+
[[ "$scope" == "wiki" ]] || die "unknown scope: $scope (expected: wiki, agents, up, config, or list)"
|
|
704
|
+
shift
|
|
705
|
+
[[ $# -ge 2 ]] || die "wiki requires a workspace and a command"
|
|
706
|
+
|
|
707
|
+
local workspace="$1"
|
|
708
|
+
local command="$2"
|
|
709
|
+
shift 2
|
|
710
|
+
|
|
711
|
+
case "$command" in
|
|
712
|
+
init)
|
|
713
|
+
run_wiki "$workspace" init "$@"
|
|
714
|
+
;;
|
|
715
|
+
up)
|
|
716
|
+
compose_for_workspace "$workspace" up -d serve mcp-http production-mcp
|
|
717
|
+
local serve_port production_port
|
|
718
|
+
serve_port="$(workspace_value "$workspace" WIKI_SERVE_PORT 3100)"
|
|
719
|
+
production_port="$(workspace_value "$workspace" PRODUCTION_MCP_PORT 3102)"
|
|
720
|
+
printf 'Workspace UI: http://localhost:%s\n' "$serve_port"
|
|
721
|
+
printf 'Chat MCP: http://localhost:%s/chat\n' "$serve_port"
|
|
722
|
+
printf 'Production: http://localhost:%s/mcp/\n' "$production_port"
|
|
723
|
+
;;
|
|
724
|
+
down)
|
|
725
|
+
compose_for_workspace "$workspace" down
|
|
726
|
+
;;
|
|
727
|
+
logs)
|
|
728
|
+
local log_args=()
|
|
729
|
+
read_lines_into_array log_args logs_args "$@"
|
|
730
|
+
compose_for_workspace "$workspace" logs "${log_args[@]}" serve mcp-http production-mcp
|
|
731
|
+
;;
|
|
732
|
+
serve)
|
|
733
|
+
local open_browser=0
|
|
734
|
+
if [[ $# -eq 1 && "$1" == "--open" ]]; then
|
|
735
|
+
open_browser=1
|
|
736
|
+
elif [[ $# -ne 0 ]]; then
|
|
737
|
+
die "serve accepts only --open as optional flag; port is configured in the workspace .env"
|
|
738
|
+
fi
|
|
739
|
+
|
|
740
|
+
printf 'Starting mcp-http...\n'
|
|
741
|
+
compose_for_workspace "$workspace" up -d mcp-http
|
|
742
|
+
printf 'Starting production-mcp...\n'
|
|
743
|
+
compose_for_workspace "$workspace" up -d production-mcp
|
|
744
|
+
local serve_port production_port
|
|
745
|
+
serve_port="$(workspace_value "$workspace" WIKI_SERVE_PORT 3100)"
|
|
746
|
+
production_port="$(workspace_value "$workspace" PRODUCTION_MCP_PORT 3102)"
|
|
747
|
+
printf 'Workspace UI: http://localhost:%s\n' "$serve_port"
|
|
748
|
+
printf 'Chat MCP: http://localhost:%s/chat\n' "$serve_port"
|
|
749
|
+
printf 'Production: http://localhost:%s/mcp/\n' "$production_port"
|
|
750
|
+
printf 'Note: compose logs may show http://localhost:3000, which is the container port.\n'
|
|
751
|
+
if [[ $open_browser -eq 1 ]]; then
|
|
752
|
+
wait_and_open "http://localhost:${serve_port}" &
|
|
753
|
+
fi
|
|
754
|
+
|
|
755
|
+
compose_for_workspace "$workspace" up serve
|
|
756
|
+
;;
|
|
757
|
+
doctor|ingest|build|export)
|
|
758
|
+
run_wiki "$workspace" "$command" "$@"
|
|
759
|
+
;;
|
|
760
|
+
run)
|
|
761
|
+
[[ $# -gt 0 ]] || die "run requires wiki CLI args"
|
|
762
|
+
run_wiki "$workspace" "$@"
|
|
763
|
+
;;
|
|
764
|
+
-h|--help|help)
|
|
765
|
+
usage
|
|
766
|
+
;;
|
|
767
|
+
*)
|
|
768
|
+
die "unknown command: $command"
|
|
769
|
+
;;
|
|
770
|
+
esac
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
main "$@"
|