@dotdrelle/wiki-manager 0.6.27 → 0.6.31
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/README.md +24 -10
- package/bin/wiki-manager +39 -1
- package/bin/wiki-manager.js +61 -2
- package/package.json +1 -1
- package/src/agent/graph.js +2 -2
- package/src/commands/slash.js +7 -1
- package/src/core/cacert.js +66 -0
- package/src/core/compose.js +9 -7
- package/src/core/env.js +13 -3
- package/src/core/mcp.js +14 -7
- package/src/core/mcp.test.js +103 -1
- package/wiki-workspace +116 -23
package/README.md
CHANGED
|
@@ -141,16 +141,6 @@ external agents (CME & co.) live in [docs/usage.md](https://raw.githubuserconten
|
|
|
141
141
|
story is in [The journey](#the-journey-from-first-launch-to-first-result); and
|
|
142
142
|
installing from source is in [Initial Setup](#initial-setup).
|
|
143
143
|
|
|
144
|
-
## The 4 ways to use it & agent configuration
|
|
145
|
-
|
|
146
|
-
The same system has four faces — the **web interface** (explore with the mouse),
|
|
147
|
-
**scripting** (let it run on its own), the **`donna` shell** (talk in plain
|
|
148
|
-
language), and the **shared external agents** (the common toolbox). Each external
|
|
149
|
-
agent (Confluence export with **CME**, document conversion, mail) also needs a
|
|
150
|
-
little setup the first time.
|
|
151
|
-
|
|
152
|
-
Both are covered in **[docs/usage.md](https://raw.githubusercontent.com/dotdrelle/llm-wiki-manager/main/docs/usage.md)**.
|
|
153
|
-
|
|
154
144
|
## The journey: from first launch to first result
|
|
155
145
|
|
|
156
146
|
Follow this little story in order. By the end you'll have seen it all: the
|
|
@@ -353,6 +343,30 @@ ingest -> build -> export -> polish
|
|
|
353
343
|
The legacy copy step is only for deployments that explicitly configure external
|
|
354
344
|
import mappings.
|
|
355
345
|
|
|
346
|
+
## Configuration overview
|
|
347
|
+
|
|
348
|
+
wikiLLM is configured by **four files** held together by two families of keys:
|
|
349
|
+
**MCP keys** (Bearer tokens that authenticate *who connects to whom*) and **LLM
|
|
350
|
+
keys** (`apiKey` + `baseUrl` that *reach a model*).
|
|
351
|
+
|
|
352
|
+

|
|
353
|
+
|
|
354
|
+
| File | Owner | Scope | Holds |
|
|
355
|
+
| --- | --- | --- | --- |
|
|
356
|
+
| `.env` | manager | global | shared secrets: agent MCP tokens, MailerSend, OCR LLM, port overrides |
|
|
357
|
+
| `mcp.endpoints.json` | manager | global | where each external agent lives + which `Bearer`/header to send |
|
|
358
|
+
| `workspaces/<name>/.env` | manager | per workspace | ports, workspace path, the wiki's own MCP tokens |
|
|
359
|
+
| `workspaces/<name>/.wikirc.yaml` (+ `.wikirc.yaml.<profile>`) | workspace | per workspace | LLM & vector keys (provider/model/apiKey/baseUrl/retrieval) |
|
|
360
|
+
|
|
361
|
+
Donna reaches the external agents and the internal wiki MCP through Bearer
|
|
362
|
+
tokens; the wiki then uses its `.wikirc.yaml` LLM keys to call models and
|
|
363
|
+
embeddings. Because every MCP server is an HTTP endpoint, remote MCP clients can
|
|
364
|
+
connect to the same surfaces with the same tokens. **MCP keys** are set in the
|
|
365
|
+
root `.env`; the wiki's **LLM keys** live in each workspace `.wikirc.yaml`.
|
|
366
|
+
|
|
367
|
+
See the full, field-by-field reference in
|
|
368
|
+
**[docs/configuration.md](https://raw.githubusercontent.com/dotdrelle/llm-wiki-manager/main/docs/configuration.md)**.
|
|
369
|
+
|
|
356
370
|
## Initial Setup
|
|
357
371
|
|
|
358
372
|
```bash
|
package/bin/wiki-manager
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
|
+
CACERT_PATH="${WIKI_MANAGER_CACERT_PATH:-}"
|
|
5
|
+
ARGS=()
|
|
6
|
+
while [[ $# -gt 0 ]]; do
|
|
7
|
+
case "$1" in
|
|
8
|
+
--cacert)
|
|
9
|
+
[[ $# -ge 2 ]] || { printf 'Error: --cacert requires a file path\n' >&2; exit 2; }
|
|
10
|
+
CACERT_PATH="$2"
|
|
11
|
+
shift 2
|
|
12
|
+
;;
|
|
13
|
+
*)
|
|
14
|
+
ARGS+=("$1")
|
|
15
|
+
shift
|
|
16
|
+
;;
|
|
17
|
+
esac
|
|
18
|
+
done
|
|
19
|
+
|
|
20
|
+
absolute_file_path() {
|
|
21
|
+
local value="$1"
|
|
22
|
+
local dir base
|
|
23
|
+
if [[ "$value" = /* ]]; then
|
|
24
|
+
printf '%s\n' "$value"
|
|
25
|
+
return
|
|
26
|
+
fi
|
|
27
|
+
dir="$(dirname "$value")"
|
|
28
|
+
base="$(basename "$value")"
|
|
29
|
+
printf '%s/%s\n' "$(cd "$dir" && pwd)" "$base"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if [[ -n "$CACERT_PATH" ]]; then
|
|
33
|
+
CACERT_PATH="$(absolute_file_path "$CACERT_PATH")"
|
|
34
|
+
[[ -f "$CACERT_PATH" ]] || { printf 'Error: --cacert file not found: %s\n' "$CACERT_PATH" >&2; exit 2; }
|
|
35
|
+
export WIKI_MANAGER_CACERT_PATH="$CACERT_PATH"
|
|
36
|
+
export NODE_EXTRA_CA_CERTS="$CACERT_PATH"
|
|
37
|
+
export SSL_CERT_FILE="$CACERT_PATH"
|
|
38
|
+
export REQUESTS_CA_BUNDLE="$CACERT_PATH"
|
|
39
|
+
export CURL_CA_BUNDLE="$CACERT_PATH"
|
|
40
|
+
fi
|
|
41
|
+
|
|
4
42
|
BUN_BIN="$(command -v bun 2>/dev/null || true)"
|
|
5
43
|
if [[ -z "$BUN_BIN" && -n "${BUN_INSTALL:-}" && -x "$BUN_INSTALL/bin/bun" ]]; then
|
|
6
44
|
BUN_BIN="$BUN_INSTALL/bin/bun"
|
|
@@ -27,4 +65,4 @@ while [[ -L "$SCRIPT_SOURCE" ]]; do
|
|
|
27
65
|
done
|
|
28
66
|
BIN_DIR="$(cd "$(dirname "$SCRIPT_SOURCE")" && pwd)"
|
|
29
67
|
|
|
30
|
-
exec "$BUN_BIN" "$BIN_DIR/wiki-manager.js" "
|
|
68
|
+
exec "$BUN_BIN" "$BIN_DIR/wiki-manager.js" "${ARGS[@]}"
|
package/bin/wiki-manager.js
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
function valueAfter(argv, flag) {
|
|
6
|
+
const index = argv.indexOf(flag);
|
|
7
|
+
if (index === -1) return undefined;
|
|
8
|
+
return argv[index + 1];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function stripOptionWithValue(argv, flag) {
|
|
12
|
+
const result = [];
|
|
13
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
14
|
+
if (argv[index] === flag) {
|
|
15
|
+
index += 1;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
result.push(argv[index]);
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function cacertEnvVars(cacert) {
|
|
24
|
+
return {
|
|
25
|
+
WIKI_MANAGER_CACERT_PATH: cacert,
|
|
26
|
+
NODE_EXTRA_CA_CERTS: cacert,
|
|
27
|
+
SSL_CERT_FILE: cacert,
|
|
28
|
+
REQUESTS_CA_BUNDLE: cacert,
|
|
29
|
+
CURL_CA_BUNDLE: cacert,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveCacert(argv) {
|
|
34
|
+
const cacert = valueAfter(argv, '--cacert');
|
|
35
|
+
if (!cacert) return { argv, cacert: null };
|
|
36
|
+
const absolute = resolve(cacert);
|
|
37
|
+
if (!existsSync(absolute)) {
|
|
38
|
+
throw new Error(`--cacert file not found: ${absolute}`);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
argv: stripOptionWithValue(argv, '--cacert'),
|
|
42
|
+
cacert: absolute,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function reexecWithCacertIfNeeded(argv, cacert) {
|
|
47
|
+
if (!cacert || process.env.WIKI_MANAGER_CACERT_BOOTSTRAPPED === '1') return;
|
|
48
|
+
const { spawnSync } = await import('node:child_process');
|
|
49
|
+
const env = { ...process.env, WIKI_MANAGER_CACERT_BOOTSTRAPPED: '1', ...cacertEnvVars(cacert) };
|
|
50
|
+
const result = spawnSync(process.execPath, [process.argv[1], ...argv], {
|
|
51
|
+
env,
|
|
52
|
+
stdio: 'inherit',
|
|
53
|
+
});
|
|
54
|
+
if (result.error) throw result.error;
|
|
55
|
+
process.exit(result.status ?? 1);
|
|
56
|
+
}
|
|
4
57
|
|
|
5
58
|
function formatStartupError(err) {
|
|
6
59
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -21,7 +74,13 @@ function formatStartupError(err) {
|
|
|
21
74
|
}
|
|
22
75
|
|
|
23
76
|
async function main() {
|
|
24
|
-
const
|
|
77
|
+
const parsed = resolveCacert(process.argv.slice(2));
|
|
78
|
+
const argv = parsed.argv;
|
|
79
|
+
await reexecWithCacertIfNeeded(argv, parsed.cacert);
|
|
80
|
+
// Fallback for already-bootstrapped direct invocations; the shell wrapper
|
|
81
|
+
// exports these before Bun starts.
|
|
82
|
+
if (parsed.cacert) Object.assign(process.env, cacertEnvVars(parsed.cacert));
|
|
83
|
+
await import('@opentui/solid/preload');
|
|
25
84
|
const interactive = process.stdout.isTTY && process.stdin.isTTY && !argv.includes('--headless') && !argv.includes('--once') && !argv.includes('--version') && !argv.includes('-v') && !argv.includes('--help') && !argv.includes('-h');
|
|
26
85
|
if (interactive) process.stdout.write('Starting wiki-manager…\r');
|
|
27
86
|
const { runCli } = await import('../src/cli/wiki-manager.js');
|
package/package.json
CHANGED
package/src/agent/graph.js
CHANGED
|
@@ -322,8 +322,8 @@ export function buildAgentSystemPrompt(state) {
|
|
|
322
322
|
skills,
|
|
323
323
|
'You can call MCP tools directly using the provided tool functions.',
|
|
324
324
|
'When the user asks for an action that can be performed with connected MCP tools or safe primitives, do not answer with future intent such as "I will call...", "I am going to run...", or "launching..." unless you also call the tool in the same turn. Either call the tool now, ask for the exact missing required arguments, or explain the concrete blocker.',
|
|
325
|
-
'For
|
|
326
|
-
'For workspace-scoped external MCP tools, the orchestrator enforces workspace injection.
|
|
325
|
+
'For connector configuration/setup/update requests, if a matching setup/configuration tool is connected and the required arguments are known, call it immediately. If the connector or tool is not connected, say which concrete capability is missing and recommend the exact service/status primitive to inspect it. Do not invent a pending connector action in plain text.',
|
|
326
|
+
'For workspace-scoped external MCP tools, the orchestrator enforces workspace injection. Use the active workspace for configuration, source, import, export, conversion, and generation tools unless a tool is explicitly job-scoped and only requires a job id.',
|
|
327
327
|
'You can call shell__run_command for safe manager slash commands such as /workspaces, /new <name> [path], /use <workspace>, /config, /status, /services, /skills, /skills show <name>, and /skills run <name>.',
|
|
328
328
|
'Skills are workflow instructions, not executable code. When a user asks to run a skill, inspect it, propose the concrete primitive/tool plan, and ask for confirmation before costly or mutating actions.',
|
|
329
329
|
[
|
package/src/commands/slash.js
CHANGED
|
@@ -562,6 +562,7 @@ Usage:
|
|
|
562
562
|
Options:
|
|
563
563
|
-v, --version Print version
|
|
564
564
|
-h, --help Print help
|
|
565
|
+
--cacert <path> Trust a local CA; Docker must be able to read this host path
|
|
565
566
|
--once <prompt> Run one agent turn and exit
|
|
566
567
|
--headless Run a workspace task non-interactively
|
|
567
568
|
--workspace <name> Workspace for --headless
|
|
@@ -658,7 +659,11 @@ export async function handleSlashCommand(line, context) {
|
|
|
658
659
|
context.session.workspacePath = workspace.workspacePath;
|
|
659
660
|
context.session.workspaceEnv = workspace.env;
|
|
660
661
|
context.session.workspaceEnvFile = workspace.envFile;
|
|
661
|
-
context.session.
|
|
662
|
+
context.session.wikirc = null;
|
|
663
|
+
context.session.wikircConfig = null;
|
|
664
|
+
context.session.language = null;
|
|
665
|
+
context.session.llm = null;
|
|
666
|
+
context.session.mcp = null;
|
|
662
667
|
context.session.systemPrompt = loadWorkspaceSystemPrompt(workspace.workspacePath);
|
|
663
668
|
try {
|
|
664
669
|
step(`Workspace: loading ${workspace.name} config…`);
|
|
@@ -702,6 +707,7 @@ export async function handleSlashCommand(line, context) {
|
|
|
702
707
|
}
|
|
703
708
|
try {
|
|
704
709
|
const summary = loadSessionWikirc(context.session, profileName);
|
|
710
|
+
await refreshMcpRuntimeStatus(context.session);
|
|
705
711
|
return {
|
|
706
712
|
output: [
|
|
707
713
|
'Active wikirc:',
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import YAML from 'yaml';
|
|
4
|
+
import { managerRuntimeDir } from './env.js';
|
|
5
|
+
|
|
6
|
+
export const CACERT_CONTAINER_PATH = '/wiki-manager-ca.pem';
|
|
7
|
+
export const CACERT_ENV_KEYS = [
|
|
8
|
+
'NODE_EXTRA_CA_CERTS',
|
|
9
|
+
'SSL_CERT_FILE',
|
|
10
|
+
'REQUESTS_CA_BUNDLE',
|
|
11
|
+
'CURL_CA_BUNDLE',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export function activeCacertPath() {
|
|
15
|
+
const value = process.env.WIKI_MANAGER_CACERT_PATH;
|
|
16
|
+
return value ? resolve(value) : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function cacertEnv(path = activeCacertPath(), targetPath = path) {
|
|
20
|
+
if (!path) return {};
|
|
21
|
+
return Object.fromEntries(CACERT_ENV_KEYS.map((key) => [key, targetPath]));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseComposeServices(composeFilePath) {
|
|
25
|
+
const parsed = YAML.parse(readFileSync(composeFilePath, 'utf8')) ?? {};
|
|
26
|
+
return Object.keys(parsed.services ?? {}).filter(Boolean);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function composeOverrideContent(cacertPath, services) {
|
|
30
|
+
const serviceEntries = Object.fromEntries(
|
|
31
|
+
services.map((service) => [
|
|
32
|
+
service,
|
|
33
|
+
{
|
|
34
|
+
volumes: [`${cacertPath}:${CACERT_CONTAINER_PATH}:ro`],
|
|
35
|
+
environment: cacertEnv(cacertPath, CACERT_CONTAINER_PATH),
|
|
36
|
+
},
|
|
37
|
+
]),
|
|
38
|
+
);
|
|
39
|
+
const doc = {
|
|
40
|
+
services: serviceEntries,
|
|
41
|
+
};
|
|
42
|
+
return [
|
|
43
|
+
'# Generated by wiki-manager. Safe to delete.',
|
|
44
|
+
'# Rewritten when --cacert is used with a Docker Compose command.',
|
|
45
|
+
YAML.stringify(doc).trimEnd(),
|
|
46
|
+
'',
|
|
47
|
+
].join('\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let _cacertOverrideCache;
|
|
51
|
+
|
|
52
|
+
export function ensureCacertComposeOverride(composeFilePath, fileName = 'cacert.compose.yml') {
|
|
53
|
+
if (_cacertOverrideCache !== undefined) return _cacertOverrideCache;
|
|
54
|
+
const cacertPath = activeCacertPath();
|
|
55
|
+
if (!cacertPath) return (_cacertOverrideCache = null);
|
|
56
|
+
if (!existsSync(cacertPath)) {
|
|
57
|
+
throw new Error(`--cacert file not found: ${cacertPath}`);
|
|
58
|
+
}
|
|
59
|
+
const services = parseComposeServices(composeFilePath);
|
|
60
|
+
if (services.length === 0) return null;
|
|
61
|
+
const runtimeDir = managerRuntimeDir();
|
|
62
|
+
mkdirSync(runtimeDir, { recursive: true });
|
|
63
|
+
const overridePath = join(runtimeDir, fileName);
|
|
64
|
+
writeFileSync(overridePath, composeOverrideContent(cacertPath, services), 'utf8');
|
|
65
|
+
return (_cacertOverrideCache = overridePath);
|
|
66
|
+
}
|
package/src/core/compose.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import YAML from 'yaml';
|
|
6
|
+
import { cacertEnv, ensureCacertComposeOverride } from './cacert.js';
|
|
6
7
|
import { managerEnvFile, readEnvFile } from './env.js';
|
|
7
8
|
import { managerRoot } from './workspaces.js';
|
|
8
9
|
|
|
@@ -93,14 +94,14 @@ function projectName(session) {
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
function composeBaseArgs(session) {
|
|
96
|
-
const
|
|
97
|
+
const compose = composeFile();
|
|
98
|
+
const cacertOverride = ensureCacertComposeOverride(compose);
|
|
99
|
+
const args = ['compose', '-f', compose];
|
|
100
|
+
if (cacertOverride) args.push('-f', cacertOverride);
|
|
101
|
+
args.push('-p', projectName(session));
|
|
97
102
|
const managerEnvPath = managerEnvFile();
|
|
98
|
-
if (existsSync(managerEnvPath))
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
if (session.workspaceEnvFile && existsSync(session.workspaceEnvFile)) {
|
|
102
|
-
args.push('--env-file', session.workspaceEnvFile);
|
|
103
|
-
}
|
|
103
|
+
if (existsSync(managerEnvPath)) args.push('--env-file', managerEnvPath);
|
|
104
|
+
if (session.workspaceEnvFile && existsSync(session.workspaceEnvFile)) args.push('--env-file', session.workspaceEnvFile);
|
|
104
105
|
return args;
|
|
105
106
|
}
|
|
106
107
|
|
|
@@ -109,6 +110,7 @@ function composeEnv(session) {
|
|
|
109
110
|
...process.env,
|
|
110
111
|
...readManagerEnv(),
|
|
111
112
|
...(session.workspaceEnv ?? {}),
|
|
113
|
+
...cacertEnv(),
|
|
112
114
|
WORKSPACE_NAME: session.workspace,
|
|
113
115
|
WIKI_WORKSPACE_PATH: session.workspacePath,
|
|
114
116
|
};
|
package/src/core/env.js
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { join, resolve } from 'node:path';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
3
|
|
|
4
4
|
export function userManagerDir() {
|
|
5
5
|
return process.cwd();
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
export function managerStateDir() {
|
|
9
|
+
return process.env.WIKI_MANAGER_ENV_FILE
|
|
10
|
+
? dirname(resolve(process.env.WIKI_MANAGER_ENV_FILE))
|
|
11
|
+
: userManagerDir();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function managerRuntimeDir() {
|
|
15
|
+
return join(managerStateDir(), '.wiki-manager');
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
export function managerEnvFile() {
|
|
9
19
|
return process.env.WIKI_MANAGER_ENV_FILE
|
|
10
20
|
? resolve(process.env.WIKI_MANAGER_ENV_FILE)
|
|
11
|
-
: join(
|
|
21
|
+
: join(managerStateDir(), '.env');
|
|
12
22
|
}
|
|
13
23
|
|
|
14
24
|
export function managerMcpEndpointsFile() {
|
|
15
|
-
return join(
|
|
25
|
+
return join(managerStateDir(), 'mcp.endpoints.json');
|
|
16
26
|
}
|
|
17
27
|
|
|
18
28
|
function parseEnvValue(value) {
|
package/src/core/mcp.js
CHANGED
|
@@ -2,10 +2,12 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { managerEnvFile, managerMcpEndpointsFile, readEnvFile } from './env.js';
|
|
3
3
|
|
|
4
4
|
function envValue(key) {
|
|
5
|
-
if (key in process.env) return process.env[key];
|
|
6
5
|
const filePath = managerEnvFile();
|
|
7
|
-
if (
|
|
8
|
-
|
|
6
|
+
if (existsSync(filePath)) {
|
|
7
|
+
const fileValue = readEnvFile(filePath)[key];
|
|
8
|
+
if (fileValue !== undefined) return fileValue;
|
|
9
|
+
}
|
|
10
|
+
return process.env[key];
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
function interpolateEnv(value) {
|
|
@@ -75,16 +77,20 @@ const MCP_SERVICE_MAP = {
|
|
|
75
77
|
|
|
76
78
|
export function buildMcpStatus(session) {
|
|
77
79
|
const workspaceEnv = session.workspaceEnv ?? {};
|
|
80
|
+
const wikiMcpToken = session.wikircConfig?.mcp?.accessKey;
|
|
81
|
+
const wikiMcpDetail = workspaceEnv.WIKI_MCP_PORT
|
|
82
|
+
? (wikiMcpToken ? `:${workspaceEnv.WIKI_MCP_PORT}` : `:${workspaceEnv.WIKI_MCP_PORT} (mcp.accessKey missing in active wikirc)`)
|
|
83
|
+
: '';
|
|
78
84
|
const external = readExternalMcpEndpoints();
|
|
79
85
|
|
|
80
86
|
return {
|
|
81
87
|
wiki: {
|
|
82
88
|
...endpointStatus(
|
|
83
|
-
workspaceEnv.WIKI_MCP_PORT &&
|
|
84
|
-
|
|
89
|
+
workspaceEnv.WIKI_MCP_PORT && wikiMcpToken,
|
|
90
|
+
wikiMcpDetail,
|
|
85
91
|
),
|
|
86
92
|
url: workspaceEnv.WIKI_MCP_PORT ? `http://127.0.0.1:${workspaceEnv.WIKI_MCP_PORT}/mcp` : null,
|
|
87
|
-
token:
|
|
93
|
+
token: wikiMcpToken || null,
|
|
88
94
|
},
|
|
89
95
|
production: {
|
|
90
96
|
...endpointStatus(
|
|
@@ -205,7 +211,7 @@ async function mcpRequest(endpoint, method, params, signal, options = {}) {
|
|
|
205
211
|
params: {
|
|
206
212
|
protocolVersion: '2025-06-18',
|
|
207
213
|
capabilities: {},
|
|
208
|
-
clientInfo: { name: 'wiki-manager', version: '0.6.
|
|
214
|
+
clientInfo: { name: 'wiki-manager', version: '0.6.31' },
|
|
209
215
|
},
|
|
210
216
|
}),
|
|
211
217
|
});
|
|
@@ -295,6 +301,7 @@ export async function discoverMcpTools(mcpStatus) {
|
|
|
295
301
|
const message = err instanceof Error ? err.message : String(err);
|
|
296
302
|
next[name] = {
|
|
297
303
|
...value,
|
|
304
|
+
status: value.status === 'connected' ? 'configured' : value.status,
|
|
298
305
|
tools: [],
|
|
299
306
|
toolError: message,
|
|
300
307
|
};
|
package/src/core/mcp.test.js
CHANGED
|
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
|
|
|
3
3
|
import { mkdtemp, writeFile } from 'node:fs/promises';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { buildMcpStatus, callMcpTool } from './mcp.js';
|
|
6
|
+
import { buildMcpStatus, callMcpTool, discoverMcpTools } from './mcp.js';
|
|
7
7
|
|
|
8
8
|
test('buildMcpStatus reads external MCP endpoints from mcp.endpoints.json', async () => {
|
|
9
9
|
const originalCwd = process.cwd();
|
|
@@ -87,6 +87,82 @@ test('buildMcpStatus interpolates external endpoints from manager .env', async (
|
|
|
87
87
|
}
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
test('buildMcpStatus reloads external endpoint keys changed in manager .env', async () => {
|
|
91
|
+
const originalCwd = process.cwd();
|
|
92
|
+
const originalToken = process.env.TEST_EXTERNAL_TOKEN;
|
|
93
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'wiki-manager-mcp-env-reload-'));
|
|
94
|
+
await writeFile(
|
|
95
|
+
path.join(root, '.env'),
|
|
96
|
+
'TEST_EXTERNAL_TOKEN=first-token\n',
|
|
97
|
+
'utf8',
|
|
98
|
+
);
|
|
99
|
+
await writeFile(
|
|
100
|
+
path.join(root, 'mcp.endpoints.json'),
|
|
101
|
+
JSON.stringify({
|
|
102
|
+
mcpServers: {
|
|
103
|
+
external: {
|
|
104
|
+
url: 'http://127.0.0.1:9999/mcp/',
|
|
105
|
+
headers: {
|
|
106
|
+
Authorization: 'Bearer ${TEST_EXTERNAL_TOKEN}',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
'utf8',
|
|
112
|
+
);
|
|
113
|
+
process.env.TEST_EXTERNAL_TOKEN = 'first-token';
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
process.chdir(root);
|
|
117
|
+
await writeFile(
|
|
118
|
+
path.join(root, '.env'),
|
|
119
|
+
'TEST_EXTERNAL_TOKEN=second-token\n',
|
|
120
|
+
'utf8',
|
|
121
|
+
);
|
|
122
|
+
const status = buildMcpStatus({ workspaceEnv: {} });
|
|
123
|
+
assert.deepEqual(status.external.headers, {
|
|
124
|
+
authorization: 'Bearer second-token',
|
|
125
|
+
});
|
|
126
|
+
} finally {
|
|
127
|
+
process.chdir(originalCwd);
|
|
128
|
+
if (originalToken === undefined) delete process.env.TEST_EXTERNAL_TOKEN;
|
|
129
|
+
else process.env.TEST_EXTERNAL_TOKEN = originalToken;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('buildMcpStatus does not use workspace env token for wiki MCP without active wikirc accessKey', () => {
|
|
134
|
+
const status = buildMcpStatus({
|
|
135
|
+
workspaceEnv: {
|
|
136
|
+
WIKI_MCP_PORT: '3101',
|
|
137
|
+
WIKI_MCP_AUTH_TOKEN: 'wiki-token-2',
|
|
138
|
+
PRODUCTION_MCP_PORT: '3102',
|
|
139
|
+
PRODUCTION_MCP_AUTH_TOKEN: 'production-token-2',
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
assert.equal(status.wiki.status, 'missing');
|
|
144
|
+
assert.equal(status.wiki.token, null);
|
|
145
|
+
assert.match(status.wiki.detail, /mcp\.accessKey missing/);
|
|
146
|
+
assert.equal(status.production.token, 'production-token-2');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('buildMcpStatus uses active wikirc mcp.accessKey for wiki MCP', () => {
|
|
150
|
+
const status = buildMcpStatus({
|
|
151
|
+
workspaceEnv: {
|
|
152
|
+
WIKI_MCP_PORT: '3101',
|
|
153
|
+
WIKI_MCP_AUTH_TOKEN: 'env-wiki-token',
|
|
154
|
+
},
|
|
155
|
+
wikircConfig: {
|
|
156
|
+
mcp: {
|
|
157
|
+
accessKey: 'wikirc-wiki-token',
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
assert.equal(status.wiki.status, 'configured');
|
|
163
|
+
assert.equal(status.wiki.token, 'wikirc-wiki-token');
|
|
164
|
+
});
|
|
165
|
+
|
|
90
166
|
test('callMcpTool injects active configPath for production_start_job', async () => {
|
|
91
167
|
const originalFetch = globalThis.fetch;
|
|
92
168
|
let requestBody = null;
|
|
@@ -193,6 +269,32 @@ test('callMcpTool sends configured endpoint headers', async () => {
|
|
|
193
269
|
}
|
|
194
270
|
});
|
|
195
271
|
|
|
272
|
+
test('discoverMcpTools downgrades connected endpoint when tool discovery fails', async () => {
|
|
273
|
+
const originalFetch = globalThis.fetch;
|
|
274
|
+
globalThis.fetch = async () => ({
|
|
275
|
+
ok: false,
|
|
276
|
+
status: 401,
|
|
277
|
+
headers: { get: () => null },
|
|
278
|
+
text: async () => '{"error":"invalid or missing bearer token"}',
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const status = await discoverMcpTools({
|
|
283
|
+
wiki: {
|
|
284
|
+
status: 'connected',
|
|
285
|
+
url: 'http://127.0.0.1:3201/mcp',
|
|
286
|
+
token: 'token',
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
assert.equal(status.wiki.status, 'configured');
|
|
291
|
+
assert.equal(status.wiki.tools.length, 0);
|
|
292
|
+
assert.match(status.wiki.toolError, /401/);
|
|
293
|
+
} finally {
|
|
294
|
+
globalThis.fetch = originalFetch;
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
196
298
|
test('callMcpTool parses SSE responses after keepalive comments', async () => {
|
|
197
299
|
const originalFetch = globalThis.fetch;
|
|
198
300
|
globalThis.fetch = async () => ({
|
package/wiki-workspace
CHANGED
|
@@ -16,10 +16,66 @@ DEFAULT_MANAGER_DIR="$PWD"
|
|
|
16
16
|
WORKSPACES_DIR="${WIKI_WORKSPACES_DIR:-$DEFAULT_MANAGER_DIR/workspaces}"
|
|
17
17
|
MANAGER_ENV_FILE="${WIKI_MANAGER_ENV_FILE:-$DEFAULT_MANAGER_DIR/.env}"
|
|
18
18
|
MANAGER_ENDPOINTS_FILE="${WIKI_MANAGER_ENDPOINTS_FILE:-$DEFAULT_MANAGER_DIR/mcp.endpoints.json}"
|
|
19
|
+
MANAGER_STATE_DIR="$(cd "$(dirname "$MANAGER_ENV_FILE")" && pwd)"
|
|
20
|
+
MANAGER_RUNTIME_DIR="$MANAGER_STATE_DIR/.wiki-manager"
|
|
21
|
+
CACERT_PATH="${WIKI_MANAGER_CACERT_PATH:-}"
|
|
22
|
+
|
|
23
|
+
normalize_path() {
|
|
24
|
+
local value="$1"
|
|
25
|
+
if command -v wslpath >/dev/null 2>&1 && [[ "$value" =~ ^[A-Za-z]:[\\/] ]]; then
|
|
26
|
+
wslpath -u "$value"
|
|
27
|
+
return
|
|
28
|
+
fi
|
|
29
|
+
printf '%s\n' "$value"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
absolute_path() {
|
|
33
|
+
local value="$1"
|
|
34
|
+
local base="${2:-$ROOT_DIR}"
|
|
35
|
+
value="$(normalize_path "$value")"
|
|
36
|
+
if [[ "$value" = /* ]]; then
|
|
37
|
+
printf '%s\n' "$value"
|
|
38
|
+
else
|
|
39
|
+
printf '%s\n' "$base/$value"
|
|
40
|
+
fi
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
parse_global_options() {
|
|
44
|
+
local parsed=()
|
|
45
|
+
while [[ $# -gt 0 ]]; do
|
|
46
|
+
case "$1" in
|
|
47
|
+
--cacert)
|
|
48
|
+
[[ $# -ge 2 ]] || { printf 'Error: --cacert requires a file path\n' >&2; exit 2; }
|
|
49
|
+
CACERT_PATH="$2"
|
|
50
|
+
shift 2
|
|
51
|
+
;;
|
|
52
|
+
*)
|
|
53
|
+
parsed+=("$1")
|
|
54
|
+
shift
|
|
55
|
+
;;
|
|
56
|
+
esac
|
|
57
|
+
done
|
|
58
|
+
if [[ -n "$CACERT_PATH" ]]; then
|
|
59
|
+
CACERT_PATH="$(absolute_path "$CACERT_PATH" "$PWD")"
|
|
60
|
+
[[ -f "$CACERT_PATH" ]] || { printf 'Error: --cacert file not found: %s\n' "$CACERT_PATH" >&2; exit 2; }
|
|
61
|
+
export WIKI_MANAGER_CACERT_PATH="$CACERT_PATH"
|
|
62
|
+
export NODE_EXTRA_CA_CERTS="$CACERT_PATH"
|
|
63
|
+
export SSL_CERT_FILE="$CACERT_PATH"
|
|
64
|
+
export REQUESTS_CA_BUNDLE="$CACERT_PATH"
|
|
65
|
+
export CURL_CA_BUNDLE="$CACERT_PATH"
|
|
66
|
+
fi
|
|
67
|
+
set -- "${parsed[@]}"
|
|
68
|
+
PARSED_ARGS=("$@")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
PARSED_ARGS=()
|
|
72
|
+
parse_global_options "$@"
|
|
73
|
+
set -- "${PARSED_ARGS[@]}"
|
|
19
74
|
|
|
20
75
|
usage() {
|
|
21
76
|
cat <<'EOF'
|
|
22
77
|
Usage:
|
|
78
|
+
wiki-workspace [--cacert <path>] <command> ...
|
|
23
79
|
wiki-workspace config <workspace> [path]
|
|
24
80
|
wiki-workspace up <workspace>
|
|
25
81
|
wiki-workspace wiki <workspace> <command> [args...]
|
|
@@ -50,6 +106,7 @@ Commands:
|
|
|
50
106
|
Configuration:
|
|
51
107
|
workspaces/<workspace>/.env
|
|
52
108
|
Override directory: WIKI_WORKSPACES_DIR=/path/to/dir
|
|
109
|
+
Local CA: --cacert /absolute/path/to/ca.pem (Docker must be able to read this host path)
|
|
53
110
|
EOF
|
|
54
111
|
}
|
|
55
112
|
|
|
@@ -58,26 +115,6 @@ die() {
|
|
|
58
115
|
exit 1
|
|
59
116
|
}
|
|
60
117
|
|
|
61
|
-
normalize_path() {
|
|
62
|
-
local value="$1"
|
|
63
|
-
if command -v wslpath >/dev/null 2>&1 && [[ "$value" =~ ^[A-Za-z]:[\\/] ]]; then
|
|
64
|
-
wslpath -u "$value"
|
|
65
|
-
return
|
|
66
|
-
fi
|
|
67
|
-
printf '%s\n' "$value"
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
absolute_path() {
|
|
71
|
-
local value="$1"
|
|
72
|
-
local base="${2:-$ROOT_DIR}"
|
|
73
|
-
value="$(normalize_path "$value")"
|
|
74
|
-
if [[ "$value" = /* ]]; then
|
|
75
|
-
printf '%s\n' "$value"
|
|
76
|
-
else
|
|
77
|
-
printf '%s\n' "$base/$value"
|
|
78
|
-
fi
|
|
79
|
-
}
|
|
80
|
-
|
|
81
118
|
workspace_env_file() {
|
|
82
119
|
local workspace="$1"
|
|
83
120
|
printf '%s/%s/.env\n' "$WORKSPACES_DIR" "$workspace"
|
|
@@ -169,9 +206,12 @@ agents_compose() {
|
|
|
169
206
|
[[ -f "$MANAGER_ENV_FILE" ]] && compose_env_args+=(--env-file "$MANAGER_ENV_FILE")
|
|
170
207
|
|
|
171
208
|
_agents_dc() {
|
|
209
|
+
local cacert_args=()
|
|
210
|
+
WORKSPACES_ROOT="$workspaces_root" AGENTS_DATA_DIR="$agents_data_dir" \
|
|
211
|
+
read_lines_into_array cacert_args cacert_compose_args "$agents_compose_file" "agents.cacert.compose.yml"
|
|
172
212
|
WORKSPACES_ROOT="$workspaces_root" AGENTS_DATA_DIR="$agents_data_dir" \
|
|
173
213
|
docker compose --project-directory "$DEFAULT_MANAGER_DIR" \
|
|
174
|
-
"${compose_env_args[@]}" -f "$agents_compose_file" -p wiki-agents "$@"
|
|
214
|
+
"${compose_env_args[@]}" -f "$agents_compose_file" "${cacert_args[@]}" -p wiki-agents "$@"
|
|
175
215
|
}
|
|
176
216
|
|
|
177
217
|
case "$subcommand" in
|
|
@@ -444,13 +484,19 @@ compose_for_workspace() {
|
|
|
444
484
|
local compose_env_args=()
|
|
445
485
|
[[ -f "$MANAGER_ENV_FILE" ]] && compose_env_args+=(--env-file "$MANAGER_ENV_FILE")
|
|
446
486
|
compose_env_args+=(--env-file "$workspace_env")
|
|
487
|
+
local cacert_args=()
|
|
488
|
+
WIKI_WORKSPACE_PATH="$ws_path" \
|
|
489
|
+
WIKI_SERVE_PORT="$serve_port" \
|
|
490
|
+
WIKI_MCP_PORT="$mcp_port" \
|
|
491
|
+
PRODUCTION_MCP_PORT="$prod_port" \
|
|
492
|
+
read_lines_into_array cacert_args cacert_compose_args "$ROOT_DIR/docker-compose.yml" "cacert.compose.yml"
|
|
447
493
|
|
|
448
494
|
WIKI_WORKSPACE_PATH="$ws_path" \
|
|
449
495
|
WIKI_SERVE_PORT="$serve_port" \
|
|
450
496
|
WIKI_MCP_PORT="$mcp_port" \
|
|
451
497
|
PRODUCTION_MCP_PORT="$prod_port" \
|
|
452
498
|
docker compose --project-directory "$DEFAULT_MANAGER_DIR" \
|
|
453
|
-
"${compose_env_args[@]}" -f "$ROOT_DIR/docker-compose.yml" -p "$project" "$@"
|
|
499
|
+
"${compose_env_args[@]}" -f "$ROOT_DIR/docker-compose.yml" "${cacert_args[@]}" -p "$project" "$@"
|
|
454
500
|
}
|
|
455
501
|
|
|
456
502
|
logs_args() {
|
|
@@ -474,6 +520,48 @@ read_lines_into_array() {
|
|
|
474
520
|
done < <("$@")
|
|
475
521
|
}
|
|
476
522
|
|
|
523
|
+
cacert_compose_args() {
|
|
524
|
+
local compose_file="$1"
|
|
525
|
+
local override_name="$2"
|
|
526
|
+
[[ -n "$CACERT_PATH" ]] || return 0
|
|
527
|
+
command -v node >/dev/null 2>&1 || die "--cacert requires node to generate Docker Compose overrides"
|
|
528
|
+
|
|
529
|
+
local services
|
|
530
|
+
services="$(
|
|
531
|
+
ROOT_DIR="$ROOT_DIR" _WIKI_COMPOSE_FILE_PATH="$compose_file" node <<'NODE'
|
|
532
|
+
const { createRequire } = require('node:module');
|
|
533
|
+
const fs = require('node:fs');
|
|
534
|
+
const requireFromPackage = createRequire(`${process.env.ROOT_DIR}/package.json`);
|
|
535
|
+
const YAML = requireFromPackage('yaml');
|
|
536
|
+
const parsed = YAML.parse(fs.readFileSync(process.env._WIKI_COMPOSE_FILE_PATH, 'utf8')) ?? {};
|
|
537
|
+
for (const service of Object.keys(parsed.services ?? {})) {
|
|
538
|
+
console.log(service);
|
|
539
|
+
}
|
|
540
|
+
NODE
|
|
541
|
+
)"
|
|
542
|
+
[[ -n "$services" ]] || return 0
|
|
543
|
+
|
|
544
|
+
mkdir -p "$MANAGER_RUNTIME_DIR"
|
|
545
|
+
local override_path="$MANAGER_RUNTIME_DIR/$override_name"
|
|
546
|
+
{
|
|
547
|
+
printf '# Generated by wiki-manager. Safe to delete.\n'
|
|
548
|
+
printf '# Rewritten when --cacert is used with a Docker Compose command.\n'
|
|
549
|
+
printf 'services:\n'
|
|
550
|
+
while IFS= read -r service || [[ -n "$service" ]]; do
|
|
551
|
+
[[ -n "$service" ]] || continue
|
|
552
|
+
printf ' %s:\n' "$service"
|
|
553
|
+
printf ' volumes:\n'
|
|
554
|
+
printf ' - %s:/wiki-manager-ca.pem:ro\n' "$CACERT_PATH"
|
|
555
|
+
printf ' environment:\n'
|
|
556
|
+
printf ' NODE_EXTRA_CA_CERTS: /wiki-manager-ca.pem\n'
|
|
557
|
+
printf ' SSL_CERT_FILE: /wiki-manager-ca.pem\n'
|
|
558
|
+
printf ' REQUESTS_CA_BUNDLE: /wiki-manager-ca.pem\n'
|
|
559
|
+
printf ' CURL_CA_BUNDLE: /wiki-manager-ca.pem\n'
|
|
560
|
+
done <<< "$services"
|
|
561
|
+
} > "$override_path"
|
|
562
|
+
printf '%s\n' -f "$override_path"
|
|
563
|
+
}
|
|
564
|
+
|
|
477
565
|
run_wiki() {
|
|
478
566
|
local workspace="$1"
|
|
479
567
|
shift
|
|
@@ -623,12 +711,17 @@ config_workspace() {
|
|
|
623
711
|
local compose_env_args=()
|
|
624
712
|
[[ -f "$MANAGER_ENV_FILE" ]] && compose_env_args+=(--env-file "$MANAGER_ENV_FILE")
|
|
625
713
|
compose_env_args+=(--env-file "$env_file")
|
|
714
|
+
local cacert_args=()
|
|
715
|
+
WIKI_WORKSPACE_PATH="$target_path" \
|
|
716
|
+
WIKI_SERVE_PORT="$serve_port" \
|
|
717
|
+
WIKI_MCP_PORT="$mcp_port" \
|
|
718
|
+
read_lines_into_array cacert_args cacert_compose_args "$ROOT_DIR/docker-compose.yml" "cacert.compose.yml"
|
|
626
719
|
|
|
627
720
|
WIKI_WORKSPACE_PATH="$target_path" \
|
|
628
721
|
WIKI_SERVE_PORT="$serve_port" \
|
|
629
722
|
WIKI_MCP_PORT="$mcp_port" \
|
|
630
723
|
docker compose --project-directory "$DEFAULT_MANAGER_DIR" \
|
|
631
|
-
"${compose_env_args[@]}" -f "$ROOT_DIR/docker-compose.yml" run --rm wiki init
|
|
724
|
+
"${compose_env_args[@]}" -f "$ROOT_DIR/docker-compose.yml" "${cacert_args[@]}" run --rm wiki init
|
|
632
725
|
|
|
633
726
|
printf 'Workspace ready: %s\n' "$target_path"
|
|
634
727
|
printf 'Start with: wiki-workspace up %s\n' "$workspace"
|