@dotdrelle/wiki-manager 0.12.7 → 0.12.9

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 CHANGED
@@ -36,6 +36,52 @@ everything — either with the mouse in a browser, or by talking to an assistant
36
36
  A **workspace** = a project. Each project is isolated: its documents, settings,
37
37
  and results never get mixed up with the others.
38
38
 
39
+ ## Installation and command modes
40
+
41
+ The package can be installed either locally in a project or globally. The
42
+ command used afterwards depends on the installation mode.
43
+
44
+ ### Local installation
45
+
46
+ Use a local installation when the manager should be pinned in a project's
47
+ `package.json`:
48
+
49
+ ```bash
50
+ npm install @dotdrelle/wiki-manager
51
+ npm approve-scripts bun # only when npm reports that Bun's postinstall is pending
52
+ npx wiki-manager
53
+ npx wiki-workspace --help
54
+ ```
55
+
56
+ `npm install` does not add `wiki-manager` or `wiki-workspace` to the shell's
57
+ global `PATH`. Run local executables with `npx` (or `npm exec wiki-manager` and
58
+ `npm exec wiki-workspace`). Bun is installed automatically as a package runtime;
59
+ you do not need to add `~/.bun/bin` to `PATH`. Recent npm versions may require
60
+ the explicit `npm approve-scripts bun` security approval shown above before the
61
+ first launch.
62
+
63
+ ### Global installation
64
+
65
+ Use a global installation when the commands should be available directly from
66
+ any directory:
67
+
68
+ ```bash
69
+ npm install --global @dotdrelle/wiki-manager
70
+ wiki-manager
71
+ wiki-workspace --help
72
+ ```
73
+
74
+ The global installation also installs the required Bun runtime. A separately
75
+ installed Bun remains supported, but is not required. If npm blocks Bun's
76
+ installation script, reinstall with:
77
+
78
+ ```bash
79
+ npm install --global --allow-scripts=bun @dotdrelle/wiki-manager
80
+ ```
81
+
82
+ In both modes, launch the commands from the directory that should hold the
83
+ manager state (`workspaces/`, `.env`, and `mcp.endpoints.json`).
84
+
39
85
  ## Functional overview
40
86
 
41
87
  The diagram below shows the whole picture at a glance: how **inputs** (external
@@ -54,14 +100,17 @@ chat** — all on the **shipped example**, with **no external source to
54
100
  configure**. External agents (Confluence, mail…) come later, only when you plug
55
101
  in real sources.
56
102
 
57
- > **Prerequisites:** Docker running and Node ≥ 22.
103
+ > **Prerequisites:** Docker running and Node ≥ 22. Bun is installed with the npm
104
+ > package.
58
105
 
59
- **1 — Install once, pick a home folder.**
106
+ **1 — Install globally, pick a home folder.**
60
107
  The manager keeps its state (workspaces, `.env`, endpoints) in the directory
61
- where you launch it, so give it a home:
108
+ where you launch it, so give it a home. This quick start uses the global mode;
109
+ see [Installation and command modes](#installation-and-command-modes) for the
110
+ local `npm install` + `npx` mode.
62
111
 
63
112
  ```bash
64
- npm i -g @dotdrelle/wiki-manager
113
+ npm install --global @dotdrelle/wiki-manager
65
114
  mkdir -p ~/llm-wiki && cd ~/llm-wiki # all manager state lives here
66
115
  ```
67
116
 
@@ -151,7 +200,7 @@ wiki-workspace wiki demo build # or, in the shell: /skills run pipeli
151
200
  That's the whole loop. Next: the four ways to use it and how to configure the
152
201
  external agents (CME & co.) live in [docs/usage.md](https://raw.githubusercontent.com/dotdrelle/llm-wiki-manager/main/docs/usage.md); the detailed
153
202
  story is in [The journey](#the-journey-from-first-launch-to-first-result); and
154
- installing from source is in [Initial Setup](#initial-setup).
203
+ installing from source is in [Installing from source](#installing-from-source).
155
204
 
156
205
  ## The journey: from first launch to first result
157
206
 
@@ -379,15 +428,15 @@ root `.env`; the wiki's **LLM keys** live in each workspace `.wikirc.yaml`.
379
428
  See the full, field-by-field reference in
380
429
  **[docs/configuration.md](https://raw.githubusercontent.com/dotdrelle/llm-wiki-manager/main/docs/configuration.md)**.
381
430
 
382
- ## Initial Setup
431
+ ## Installing from source
383
432
 
384
433
  ```bash
385
434
  corepack enable
386
435
  pnpm install
387
436
  ```
388
437
 
389
- When installed through `npm`/`npx`/`bun`, `wiki-manager` keeps its state outside
390
- the package, in the directory where the command is launched:
438
+ Whether installed locally, globally, or from source, `wiki-manager` keeps its
439
+ state outside the package, in the directory where the command is launched:
391
440
 
392
441
  ```text
393
442
  ./workspaces/ # workspace registry
package/bin/wiki-manager CHANGED
@@ -39,7 +39,26 @@ if [[ -n "$CACERT_PATH" ]]; then
39
39
  export CURL_CA_BUNDLE="$CACERT_PATH"
40
40
  fi
41
41
 
42
+ SCRIPT_SOURCE="${BASH_SOURCE[0]}"
43
+ while [[ -L "$SCRIPT_SOURCE" ]]; do
44
+ SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_SOURCE")" && pwd)"
45
+ SCRIPT_TARGET="$(readlink "$SCRIPT_SOURCE")"
46
+ if [[ "$SCRIPT_TARGET" = /* ]]; then
47
+ SCRIPT_SOURCE="$SCRIPT_TARGET"
48
+ else
49
+ SCRIPT_SOURCE="$SCRIPT_DIR/$SCRIPT_TARGET"
50
+ fi
51
+ done
52
+ BIN_DIR="$(cd "$(dirname "$SCRIPT_SOURCE")" && pwd)"
53
+ PACKAGE_DIR="$(cd "$BIN_DIR/.." && pwd)"
54
+
42
55
  BUN_BIN="$(command -v bun 2>/dev/null || true)"
56
+ if [[ -z "$BUN_BIN" && -x "$PACKAGE_DIR/node_modules/.bin/bun" ]]; then
57
+ BUN_BIN="$PACKAGE_DIR/node_modules/.bin/bun"
58
+ fi
59
+ if [[ -z "$BUN_BIN" && -x "$PACKAGE_DIR/../../.bin/bun" ]]; then
60
+ BUN_BIN="$PACKAGE_DIR/../../.bin/bun"
61
+ fi
43
62
  if [[ -z "$BUN_BIN" && -n "${BUN_INSTALL:-}" && -x "$BUN_INSTALL/bin/bun" ]]; then
44
63
  BUN_BIN="$BUN_INSTALL/bin/bun"
45
64
  fi
@@ -49,20 +68,19 @@ fi
49
68
  if [[ -z "$BUN_BIN" ]]; then
50
69
  printf '%s\n' \
51
70
  'wiki-manager requires Bun >= 1.2.' \
52
- 'Install Bun from https://bun.sh/docs/installation or add it to PATH.' >&2
71
+ 'Reinstall @dotdrelle/wiki-manager or install Bun from https://bun.sh/docs/installation.' >&2
53
72
  exit 127
54
73
  fi
74
+ if ! "$BUN_BIN" --version >/dev/null 2>&1; then
75
+ printf '%s\n' \
76
+ 'The bundled Bun runtime is installed but not initialized.' \
77
+ 'Local install: npm approve-scripts bun' \
78
+ 'Global install: npm install -g --allow-scripts=bun @dotdrelle/wiki-manager' >&2
79
+ exit 126
80
+ fi
55
81
 
56
- SCRIPT_SOURCE="${BASH_SOURCE[0]}"
57
- while [[ -L "$SCRIPT_SOURCE" ]]; do
58
- SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_SOURCE")" && pwd)"
59
- SCRIPT_TARGET="$(readlink "$SCRIPT_SOURCE")"
60
- if [[ "$SCRIPT_TARGET" = /* ]]; then
61
- SCRIPT_SOURCE="$SCRIPT_TARGET"
62
- else
63
- SCRIPT_SOURCE="$SCRIPT_DIR/$SCRIPT_TARGET"
64
- fi
65
- done
66
- BIN_DIR="$(cd "$(dirname "$SCRIPT_SOURCE")" && pwd)"
82
+ if [[ ${#ARGS[@]} -eq 0 ]]; then
83
+ exec "$BUN_BIN" "$BIN_DIR/wiki-manager.js"
84
+ fi
67
85
 
68
86
  exec "$BUN_BIN" "$BIN_DIR/wiki-manager.js" "${ARGS[@]}"
@@ -55,7 +55,6 @@ services:
55
55
  - CME_MCP_AUTH_TOKEN
56
56
  - DOCUMENTS_MCP_AUTH_TOKEN
57
57
  - MAILER_MCP_AUTH_TOKEN
58
- - ATLASSIAN_MCP_AUTH_TOKEN
59
58
  - WORKSPACE_NAME
60
59
  - DOCUMENT_INPUT_DIR=/documents/input
61
60
  - DOCUMENT_UPLOADS_DIR=/documents/uploads
@@ -30,12 +30,6 @@
30
30
  "headers": {
31
31
  "Authorization": "Bearer ${MAILER_MCP_AUTH_TOKEN}"
32
32
  }
33
- },
34
- "atlassian": {
35
- "url": "http://host.docker.internal:9000/mcp",
36
- "headers": {
37
- "Authorization": "Bearer ${ATLASSIAN_MCP_AUTH_TOKEN}"
38
- }
39
33
  }
40
34
  }
41
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotdrelle/wiki-manager",
3
- "version": "0.12.7",
3
+ "version": "0.12.9",
4
4
  "description": "Agentic shell and orchestration cockpit for llm-wiki workspaces.",
5
5
  "license": "PolyForm-Noncommercial-1.0.0",
6
6
  "author": "dotrelle",
@@ -49,6 +49,7 @@
49
49
  "@langchain/langgraph": "^1.3.2",
50
50
  "@opentui/core": "^0.3.2",
51
51
  "@opentui/solid": "^0.3.2",
52
+ "bun": "^1.3.14",
52
53
  "marked": "^15.0.12",
53
54
  "marked-terminal": "^7.3.0",
54
55
  "openai": "^6.42.0",
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs';
2
2
  import { mkdir, writeFile } from 'node:fs/promises';
3
3
  import { dirname, join, resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
- import { loadManagerEnv } from '../core/env.js';
5
+ import { ensureManagerScaffold, loadManagerEnv } from '../core/env.js';
6
6
  loadManagerEnv();
7
7
  import { createAgentGraph } from '../agent/graph.js';
8
8
  import { handleSlashCommand, printHelp, printVersion, refreshMcpRuntimeStatus } from '../commands/slash.js';
@@ -677,6 +677,9 @@ async function runRuntime(argv, agent) {
677
677
  port,
678
678
  store,
679
679
  getContext: getWorkspaceContext,
680
+ listActiveRuns: () => [...contexts.values()]
681
+ .filter((context) => context?.running)
682
+ .map((context) => ({ workspace: context.workspace ?? null, runId: context.currentRunId ?? null })),
680
683
  run: executeRun,
681
684
  cancel: (context) => emitRuntimeLog(context.session, 'runtime: cancel requested'),
682
685
  resume: ({ workspace }) => recoverRuntime({ workspace, manual: true }),
@@ -735,6 +738,8 @@ async function runRuntime(argv, agent) {
735
738
 
736
739
  export async function runCli(argv) {
737
740
  if (argv[0] === 'runtime') {
741
+ const scaffolded = ensureManagerScaffold({ log: (message) => console.log(`[wiki-manager] ${message}`) });
742
+ if (scaffolded.length > 0) loadManagerEnv();
738
743
  const agent = createAgentGraph();
739
744
  await runRuntime(argv.slice(1), agent);
740
745
  return;
@@ -788,17 +793,29 @@ export async function runCli(argv) {
788
793
  throw new Error('Interactive TUI requires Bun. Run: bun ./bin/wiki-manager.js');
789
794
  }
790
795
  const { runOpenTuiShell, runStartupWizard } = await import('../shell/tui.tsx');
796
+ // Fresh directory → copy mcp.endpoints.json/.env from the packaged
797
+ // examples so external agents (cme, mailer, documents) connect out of
798
+ // the box. Done here (and in `runtime`), NOT at import time: --version
799
+ // in a random cwd must not litter files.
800
+ const scaffolded = ensureManagerScaffold({ log: (message) => console.log(`[wiki-manager] ${message}`) });
801
+ if (scaffolded.length > 0) loadManagerEnv();
791
802
  const gaps = await runChecks();
792
803
  if (gaps.length > 0) await runStartupWizard(gaps);
793
804
  let runtime = null;
794
805
  try {
795
806
  const { ensureRuntime } = await import('../runtime/lifecycle.js');
796
- runtime = await ensureRuntime();
807
+ // If the manager files were just scaffolded, a pre-existing runtime
808
+ // predates them: restart it so its sessions load the new endpoints.
809
+ runtime = await ensureRuntime({ forceRestart: scaffolded.length > 0 });
797
810
  } catch (err) {
798
811
  runtime = unavailableRuntime(err);
799
812
  console.error(`Runtime unavailable: ${runtime.error}`);
800
813
  }
801
814
  await runOpenTuiShell({ agent, packageJson, runtime });
815
+ if (runtime?.url) {
816
+ const { shutdownOwnedRuntime } = await import('../runtime/lifecycle.js');
817
+ await shutdownOwnedRuntime(runtime, { log: (message) => console.log(`[wiki-manager] ${message}`) });
818
+ }
802
819
  return;
803
820
  }
804
821
 
@@ -813,4 +830,8 @@ export async function runCli(argv) {
813
830
  }
814
831
  }
815
832
  await runShell({ agent, packageJson, runtime });
833
+ if (runtime?.url) {
834
+ const { shutdownOwnedRuntime } = await import('../runtime/lifecycle.js');
835
+ await shutdownOwnedRuntime(runtime, { log: (message) => console.log(`[wiki-manager] ${message}`) });
836
+ }
816
837
  }
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "0.12.7",
3
- "commit": "0bab10f"
2
+ "version": "0.12.9",
3
+ "commit": "286bec2"
4
4
  }
package/src/core/env.js CHANGED
@@ -1,5 +1,6 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { copyFileSync, existsSync, readFileSync } from 'node:fs';
2
2
  import { dirname, isAbsolute, join, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
3
4
 
4
5
  export function userManagerDir() {
5
6
  return process.cwd();
@@ -31,6 +32,34 @@ export function managerMcpEndpointsFile() {
31
32
  return join(managerStateDir(), 'mcp.endpoints.json');
32
33
  }
33
34
 
35
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
36
+
37
+ // First-run scaffolding: a fresh install directory has neither
38
+ // mcp.endpoints.json nor .env, so the external agents (cme, mailer,
39
+ // documents) silently never connect — /status shows no agents and Donna has
40
+ // no CME tools to configure anything with. Copy the packaged examples so a
41
+ // fresh directory works out of the box with the default agent ports. Optional
42
+ // credentials can be added later for the external services the user enables.
43
+ export function ensureManagerScaffold({ log = () => {} } = {}) {
44
+ const created = [];
45
+ const endpointsFile = managerMcpEndpointsFile();
46
+ const endpointsExample = join(packageRoot, 'mcp.endpoints.example.json');
47
+ if (!existsSync(endpointsFile) && existsSync(endpointsExample)) {
48
+ copyFileSync(endpointsExample, endpointsFile);
49
+ created.push('mcp.endpoints.json');
50
+ }
51
+ const envFile = managerEnvFile();
52
+ const envExample = join(packageRoot, '.env.example');
53
+ if (!existsSync(envFile) && existsSync(envExample)) {
54
+ copyFileSync(envExample, envFile);
55
+ created.push('.env');
56
+ }
57
+ if (created.length > 0) {
58
+ log(`configuration initialized successfully in ${managerStateDir()} — created ${created.join(' and ')} from packaged defaults. Optional credentials can be added later for external services.`);
59
+ }
60
+ return created;
61
+ }
62
+
34
63
  // Single source of truth for where `.agents-data` lives, shared by the
35
64
  // manager's own document intake and by the host path it mounts into agent
36
65
  // containers — keeping them in sync avoids the two silently drifting apart.
package/src/core/mcp.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { managerEnvFile, managerMcpEndpointsFile, readEnvFile } from './env.js';
3
3
 
4
- const WIKI_MANAGER_VERSION = '0.12.7';
4
+ const WIKI_MANAGER_VERSION = '0.12.9';
5
5
 
6
6
  function envValue(key) {
7
7
  const filePath = managerEnvFile();
@@ -3,6 +3,7 @@ import { readFile, rename, rm, writeFile } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { promisify } from 'node:util';
5
5
  import { patchWikircProfile } from './wikirc.js';
6
+ import { managerEnvFile, managerMcpEndpointsFile } from './env.js';
6
7
  import { createWorkspace, findWorkspace, isValidWorkspaceName, listWorkspaces, managerRoot, workspacesDir } from './workspaces.js';
7
8
 
8
9
  const execFileAsync = promisify(execFile);
@@ -39,6 +40,13 @@ export async function startAgents(options = {}) {
39
40
  env: {
40
41
  ...process.env,
41
42
  WIKI_WORKSPACES_DIR: workspacesDir(),
43
+ // cwd is the npm package root (the script and compose files live
44
+ // there), so the manager files MUST be pinned explicitly: without
45
+ // these, wiki-workspace resolved .env and mcp.endpoints.json against
46
+ // $PWD and generated agent tokens into the PACKAGE directory — the
47
+ // user's .env stayed empty on a fresh install.
48
+ WIKI_MANAGER_ENV_FILE: managerEnvFile(),
49
+ WIKI_MANAGER_ENDPOINTS_FILE: managerMcpEndpointsFile(),
42
50
  },
43
51
  timeout: options.timeout ?? 180_000,
44
52
  maxBuffer: options.maxBuffer ?? 1024 * 1024 * 8,
@@ -39,6 +39,7 @@ export async function ensureRuntime({
39
39
  stateDir = process.env.WIKI_MANAGER_STATE_DIR ?? defaultRuntimeStateDir(),
40
40
  url = process.env.WIKI_MANAGER_RUNTIME_URL ?? `http://127.0.0.1:${port}`,
41
41
  timeoutMs = 5000,
42
+ forceRestart = false,
42
43
  } = {}) {
43
44
  const auth = resolveRuntimeAuthToken({ host, stateDir });
44
45
  if (auth.token) process.env.WIKI_MANAGER_RUNTIME_TOKEN = auth.token;
@@ -46,7 +47,11 @@ export async function ensureRuntime({
46
47
  if (existing) {
47
48
  const expectedCacertPath = activeCacertPath();
48
49
  const actualCacertPath = existing.cacertPath ? resolve(existing.cacertPath) : null;
49
- if (actualCacertPath === expectedCacertPath) {
50
+ // forceRestart: the caller knows the manager configuration just changed
51
+ // (e.g. mcp.endpoints.json scaffolded on first run) — a runtime started
52
+ // BEFORE that only knows the old endpoints and would keep answering
53
+ // without the agents until manually restarted.
54
+ if (!forceRestart && actualCacertPath === expectedCacertPath) {
50
55
  return { url, started: false, health: existing, token: auth.token, tokenPath: auth.tokenPath };
51
56
  }
52
57
  await postRuntimeShutdown({ url, token: auth.token });
@@ -99,3 +104,28 @@ export async function runtimeHealthOrNull(url = runtimeUrlFromEnv(), token = run
99
104
  return null;
100
105
  }
101
106
  }
107
+
108
+ // Contract with the user: the shell OWNS the runtime it started — leaving it
109
+ // alive after exit produced zombie runtimes running yesterday's code and
110
+ // yesterday's endpoints. Nuance preserved: if a run is active anywhere, the
111
+ // runtime is left alive so the run survives the shell (that promise stays).
112
+ export async function shutdownOwnedRuntime(runtime, { log = () => {} } = {}) {
113
+ if (!runtime?.url || !runtime?.started) return { action: 'kept', reason: 'not_owned' };
114
+ try {
115
+ const health = await runtimeHealthOrNull(runtime.url, runtime.token);
116
+ if (!health) return { action: 'kept', reason: 'unreachable' };
117
+ const activeRuns = Array.isArray(health.activeRuns) ? health.activeRuns : [];
118
+ if (activeRuns.length > 0) {
119
+ const labels = activeRuns
120
+ .map((run) => [run.workspace, run.runId ? String(run.runId).slice(0, 8) : null].filter(Boolean).join('/'))
121
+ .join(', ');
122
+ log(`runtime laissé actif : run en cours (${labels}) — il survivra à ce shell ; relance wiki-manager pour le retrouver.`);
123
+ return { action: 'kept', reason: 'run_active', activeRuns };
124
+ }
125
+ await postRuntimeShutdown({ url: runtime.url, token: runtime.token });
126
+ log('runtime arrêté (démarré par ce shell, aucun run en cours).');
127
+ return { action: 'shutdown' };
128
+ } catch (err) {
129
+ return { action: 'error', reason: err instanceof Error ? err.message : String(err) };
130
+ }
131
+ }
@@ -20,6 +20,7 @@ export function startRuntimeServer({
20
20
  approve,
21
21
  configProfiles,
22
22
  useConfigProfile,
23
+ listActiveRuns = null,
23
24
  exitOnShutdown = process.env.WIKI_MANAGER_RUNTIME_CHILD === '1',
24
25
  } = {}) {
25
26
  const clients = new Set();
@@ -52,10 +53,14 @@ export function startRuntimeServer({
52
53
  if (request.method === 'GET' && url.pathname === '/health') {
53
54
  const workspace = workspaceFromUrl(url);
54
55
  const context = workspace ? await resolveContext({ workspace }) : null;
56
+ // activeRuns spans ALL workspace contexts: the shell uses it at exit
57
+ // to decide whether shutting down its own runtime would kill work.
58
+ const activeRuns = typeof listActiveRuns === 'function' ? listActiveRuns() : [];
55
59
  sendJson(response, 200, {
56
60
  ok: true,
57
61
  status: context?.running ? 'running' : 'idle',
58
62
  workspace: context?.workspace ?? workspace ?? null,
63
+ activeRuns,
59
64
  dbPath: store.dbPath,
60
65
  cacertPath: activeCacertPath(),
61
66
  nodeExtraCaCerts: process.env.NODE_EXTRA_CA_CERTS ?? null,
@@ -1396,3 +1396,31 @@ test('runtime server answers control messages posted to /run during an active ru
1396
1396
  await handle.close();
1397
1397
  }
1398
1398
  });
1399
+
1400
+ test('runtime health reports active runs across workspaces', async (t) => {
1401
+ let handle;
1402
+ try {
1403
+ handle = await startRuntimeServer({
1404
+ host: '127.0.0.1',
1405
+ port: 0,
1406
+ store: { dbPath: ':memory:', getState: () => ({ status: 'idle' }), listEvents: () => [] },
1407
+ session: {},
1408
+ // The shell reads this at exit: shutting down its own runtime must not
1409
+ // kill a run that is supposed to survive the shell.
1410
+ listActiveRuns: () => [{ workspace: 'juno', runId: 'run-1234abcd' }],
1411
+ });
1412
+ } catch (err) {
1413
+ if (err?.code === 'EPERM') {
1414
+ t.skip('network listen is not permitted in this sandbox');
1415
+ return;
1416
+ }
1417
+ throw err;
1418
+ }
1419
+
1420
+ try {
1421
+ const health = await (await fetch(`http://127.0.0.1:${handle.port}/health`)).json();
1422
+ assert.deepEqual(health.activeRuns, [{ workspace: 'juno', runId: 'run-1234abcd' }]);
1423
+ } finally {
1424
+ await handle.close();
1425
+ }
1426
+ });
package/wiki-workspace CHANGED
@@ -228,6 +228,12 @@ agents_compose() {
228
228
  shift
229
229
  fi
230
230
  [[ $# -eq 0 ]] || die "agents up accepts only --pull"
231
+ ensure_agent_tokens
232
+ ensure_endpoints_file
233
+ # The .env may have just been created above — recompute the env-file
234
+ # flag so freshly generated tokens reach the containers on first run.
235
+ compose_env_args=()
236
+ [[ -f "$MANAGER_ENV_FILE" ]] && compose_env_args+=(--env-file "$MANAGER_ENV_FILE")
231
237
  mkdir -p "$agents_data_dir/cme" "$agents_data_dir/documents/input" "$agents_data_dir/documents/output" "$agents_data_dir/documents/uploads"
232
238
  mkdir -p "$workspaces_root"
233
239
  $do_pull && _agents_dc pull
@@ -382,6 +388,33 @@ ensure_runtime_node() {
382
388
  [[ "$major" =~ ^[0-9]+$ && "$major" -ge 22 ]] || die "runtime requires Node.js 22+ for node:sqlite; $node_bin is Node $version"
383
389
  }
384
390
 
391
+ ensure_agent_tokens() {
392
+ # The external agents (cme, documents, mailer) authenticate with tokens
393
+ # from the manager .env — generate any missing one, exactly like the
394
+ # workspace and runtime tokens are generated. Leaving them empty made a
395
+ # fresh install start agents nobody could talk to.
396
+ local key token
397
+ for key in CME_MCP_AUTH_TOKEN DOCUMENTS_MCP_AUTH_TOKEN MAILER_MCP_AUTH_TOKEN; do
398
+ token="$(env_value "$MANAGER_ENV_FILE" "$key" "")"
399
+ if [[ -z "$token" ]]; then
400
+ token="$(generate_token)"
401
+ mkdir -p "$(dirname "$MANAGER_ENV_FILE")"
402
+ set_env_value "$MANAGER_ENV_FILE" "$key" "$token"
403
+ printf 'Generated %s in %s\n' "$key" "$MANAGER_ENV_FILE"
404
+ fi
405
+ done
406
+ }
407
+
408
+ ensure_endpoints_file() {
409
+ # Without mcp.endpoints.json the shell never connects to the agents it
410
+ # just started (and `agents status` refuses to run). Seed it from the
411
+ # packaged example; the \${VAR} placeholders resolve against the .env.
412
+ if [[ ! -f "$MANAGER_ENDPOINTS_FILE" && -f "$ROOT_DIR/mcp.endpoints.example.json" ]]; then
413
+ cp "$ROOT_DIR/mcp.endpoints.example.json" "$MANAGER_ENDPOINTS_FILE"
414
+ printf 'Created %s from packaged example\n' "$MANAGER_ENDPOINTS_FILE"
415
+ fi
416
+ }
417
+
385
418
  ensure_manager_runtime_token() {
386
419
  local token
387
420
  token="$(env_value "$MANAGER_ENV_FILE" WIKI_MANAGER_RUNTIME_TOKEN "")"