@dotdrelle/wiki-manager 0.12.8 → 0.12.10

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 CHANGED
@@ -23,33 +23,28 @@ WORKSPACES_ROOT=/path/to/workspaces
23
23
  #
24
24
  # "cme": { "headers": { "Authorization": "Bearer <CME_MCP_AUTH_TOKEN>" } }
25
25
  # "documents": { "headers": { "Authorization": "Bearer <DOCUMENTS_MCP_AUTH_TOKEN>" } }
26
- # "mailer": { "headers": { "Authorization": "Bearer <MAILER_MCP_AUTH_TOKEN>" } }
27
26
  #
28
27
  # Leave empty to disable authentication on that agent (not recommended in production).
29
28
 
30
29
  CME_MCP_AUTH_TOKEN=
31
30
  DOCUMENTS_MCP_AUTH_TOKEN=
32
- MAILER_MCP_AUTH_TOKEN=
33
31
 
34
32
  # ── Mailer (MailerSend) ────────────────────────────────────────────────────────
35
33
 
36
- MAILERSEND_API_KEY=
37
- MAILERSEND_FROM_EMAIL=no-reply@example.com
38
- MAILERSEND_FROM_NAME=Donna
39
34
 
40
35
  # ── Documents LLM OCR / Mermaid (optional) ─────────────────────────────────────
41
36
 
42
37
  # All image OCR is delegated to an OpenAI-compatible vision LLM. The default
43
- # base URL is OpenAI; set DOCUMENT_LLM_API_KEY or OPENAI_API_KEY.
44
- # DOCUMENT_LLM_BASE_URL=https://api.openai.com/v1
45
- # DOCUMENT_LLM_MODEL=gpt-5.4-mini
38
+ # Default target is the Albert API (etalab); set DOCUMENT_LLM_API_KEY.
39
+ # DOCUMENT_LLM_BASE_URL=https://albert.api.etalab.gouv.fr/v1
40
+ # DOCUMENT_LLM_MODEL=LightOnOCR-2-1B
46
41
  # DOCUMENT_LLM_API_KEY=
47
42
  # DOCUMENT_LLM_TIMEOUT_SECONDS=120
48
43
 
49
44
  # ── Optional external MCP providers ────────────────────────────────────────────
50
-
51
- # Used by mcp.endpoints.json when the Exa MCP endpoint is enabled.
52
- EXA_MCP_API_KEY=
45
+ # Doctrine: every variable an external MCP endpoint needs (API key, token…)
46
+ # lives HERE and is referenced from mcp.endpoints.json as ${VAR_NAME}.
47
+ # Add your own entries when you declare additional endpoints.
53
48
 
54
49
  # ── MCP retry policy (optional) ────────────────────────────────────────────────
55
50
 
@@ -75,4 +70,3 @@ EXA_MCP_API_KEY=
75
70
 
76
71
  # CME_MCP_PORT=3336
77
72
  # DOCUMENTS_MCP_PORT=3337
78
- # MAILER_MCP_PORT=3335
package/README.md CHANGED
@@ -48,6 +48,7 @@ Use a local installation when the manager should be pinned in a project's
48
48
 
49
49
  ```bash
50
50
  npm install @dotdrelle/wiki-manager
51
+ npm approve-scripts bun # only when npm reports that Bun's postinstall is pending
51
52
  npx wiki-manager
52
53
  npx wiki-workspace --help
53
54
  ```
@@ -55,7 +56,9 @@ npx wiki-workspace --help
55
56
  `npm install` does not add `wiki-manager` or `wiki-workspace` to the shell's
56
57
  global `PATH`. Run local executables with `npx` (or `npm exec wiki-manager` and
57
58
  `npm exec wiki-workspace`). Bun is installed automatically as a package runtime;
58
- you do not need to add `~/.bun/bin` to `PATH`.
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.
59
62
 
60
63
  ### Global installation
61
64
 
@@ -69,7 +72,12 @@ wiki-workspace --help
69
72
  ```
70
73
 
71
74
  The global installation also installs the required Bun runtime. A separately
72
- installed Bun remains supported, but is not required.
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
+ ```
73
81
 
74
82
  In both modes, launch the commands from the directory that should hold the
75
83
  manager state (`workspaces/`, `.env`, and `mcp.endpoints.json`).
@@ -117,7 +125,7 @@ cp .env.example .env
117
125
  ```
118
126
 
119
127
  **3 — Start the shared agents.**
120
- Start the common toolbox once (Confluence export `cme`, `documents`, `mailer`).
128
+ Start the common toolbox once (Confluence export `cme`, `documents`).
121
129
  They run in the background and serve every workspace.
122
130
 
123
131
  ```bash
@@ -215,7 +223,7 @@ Start the common toolbox once and for all (Confluence, documents, e-mail,
215
223
  production). These agents run in the background and serve **all your projects**:
216
224
 
217
225
  ```bash
218
- wiki-workspace agents up # start cme, documents, mailer…
226
+ wiki-workspace agents up # start cme, documents…
219
227
  wiki-workspace agents status # check they respond
220
228
  ```
221
229
 
@@ -360,7 +368,7 @@ of you in the browser (create → configure → start the agents → open).
360
368
  | [`agent-cme`](https://github.com/dotdrelle/agent-cme) | Global Confluence to Markdown MCP exporter; workspace injected automatically by Donna |
361
369
  | [`agent-wiki-production`](https://github.com/dotdrelle/agent-wiki-production) | Workspace-scoped production jobs: ingest, build, export, polish, pipeline |
362
370
  | [`agent-wiki-documents`](https://github.com/dotdrelle/agent-wiki-documents) | Document conversion MCP: PDF/Office/HTML/images → Markdown (OCR-capable) |
363
- | [`agent-mailer-api`](https://github.com/dotdrelle/agent-mailer-api) | Optional external mailer MCP endpoint |
371
+ | [`agent-mailer-api`](https://github.com/dotdrelle/agent-mailer-api) | Optional external mailer MCP endpoint (user-side override, not in the default stack) |
364
372
 
365
373
  ## Workspace Model
366
374
 
@@ -406,7 +414,7 @@ keys** (`apiKey` + `baseUrl` that *reach a model*).
406
414
 
407
415
  | File | Owner | Scope | Holds |
408
416
  | --- | --- | --- | --- |
409
- | `.env` | manager | global | shared secrets: agent MCP tokens, MailerSend, OCR LLM, port overrides |
417
+ | `.env` | manager | global | shared secrets: agent MCP tokens, OCR LLM, port overrides, variables for any user-declared external MCP |
410
418
  | `mcp.endpoints.json` | manager | global | where each external agent lives + which `Bearer`/header to send |
411
419
  | `workspaces/<name>/.env` | manager | per workspace | ports, workspace path, the wiki's own MCP tokens |
412
420
  | `workspaces/<name>/.wikirc.yaml` (+ `.wikirc.yaml.<profile>`) | workspace | per workspace | LLM & vector keys (provider/model/apiKey/baseUrl/retrieval) |
@@ -454,7 +462,7 @@ cp .env.example .env
454
462
 
455
463
  The `.env` file is loaded automatically by both `wiki-manager` (Node/Bun process)
456
464
  and `wiki-workspace` (Docker Compose). It sets `WORKSPACES_ROOT`, per-agent auth
457
- tokens, mailer credentials, and optional port overrides.
465
+ tokens and optional port overrides; credentials of optional connectors you enable via the user override.
458
466
 
459
467
  ### External MCP endpoints
460
468
 
@@ -530,16 +538,40 @@ than tracking its own state.
530
538
 
531
539
  ### Starting external agents
532
540
 
533
- Start CME, documents, and mailer once for all workspaces:
541
+ Start CME and documents once for all workspaces:
534
542
 
535
543
  ```bash
536
544
  wiki-workspace agents up
537
545
  ```
538
546
 
539
- This uses the packaged `agents.docker-compose.yml`. `WORKSPACES_ROOT` is resolved
547
+ This uses the packaged `agents.docker-compose.yml` (it lives inside the npm
548
+ package — never edit it, updates overwrite it). On first run, `agents up`
549
+ generates the missing agent auth tokens into your manager `.env` and seeds
550
+ `mcp.endpoints.json` from the packaged example. `WORKSPACES_ROOT` is resolved
540
551
  automatically from the manager workspaces directory. Agent state is stored under
541
552
  `./.agents-data/` unless `AGENTS_DATA_DIR` is set.
542
553
 
554
+ #### Optional agents and user overrides
555
+
556
+ Anything beyond the default stack (for example the MailerSend agent) is an
557
+ external connector operated at the user's charge — built and published, but
558
+ not part of the delivery. To enable one, create a file named
559
+ `agents.docker-compose.override.yml` **next to your `.env`**:
560
+
561
+ ```bash
562
+ cp "$(npm root -g)/@dotdrelle/wiki-manager/agents.docker-compose.mailer.example.yml" \
563
+ agents.docker-compose.override.yml
564
+ ```
565
+
566
+ `agents up` includes it automatically when present (standard Docker Compose
567
+ merge: new services are added, same-name keys override the defaults — you can
568
+ also use it to pin a port or a variable of a default agent). The file is
569
+ yours: wiki-manager never generates or overwrites it. Complete the setup by
570
+ adding the connector's variables to your `.env` and its endpoint block to
571
+ your `mcp.endpoints.json` — every variable an external MCP endpoint needs
572
+ lives in the `.env` and is referenced as `${VAR_NAME}` from
573
+ `mcp.endpoints.json`. Detailed steps are in the example file header.
574
+
543
575
  Workspace-native MCP servers (`llm-wiki`, `production`) stay configured through
544
576
  each workspace `.env`. External agents are workspace-agnostic: the active
545
577
  `/use <workspace>` is injected automatically on every CME and documents tool
@@ -0,0 +1,36 @@
1
+ # agents.docker-compose.mailer.example.yml — optional MailerSend agent
2
+ #
3
+ # The mailer agent is NOT part of the default delivery: it is an external
4
+ # MCP connector operated at the user's charge. To enable it:
5
+ #
6
+ # 1. Copy this file NEXT TO YOUR .env (the directory where you run
7
+ # wiki-manager) under the name: agents.docker-compose.override.yml
8
+ # That file is yours — wiki-manager never generates or overwrites it,
9
+ # and `wiki-workspace agents up` includes it automatically when present.
10
+ # 2. Add to your .env:
11
+ # MAILER_MCP_AUTH_TOKEN=$(openssl rand -hex 32)
12
+ # MAILERSEND_API_KEY=...
13
+ # MAILERSEND_FROM_EMAIL=...
14
+ # 3. Declare the endpoint in your mcp.endpoints.json:
15
+ # "mailer": {
16
+ # "url": "http://host.docker.internal:${MAILER_MCP_PORT:-3335}/mcp/",
17
+ # "headers": { "Authorization": "Bearer ${MAILER_MCP_AUTH_TOKEN}" }
18
+ # }
19
+ #
20
+ services:
21
+ mailer:
22
+ image: dotdrelle/agent-mailer-api:latest
23
+ user: "${UID:-1000}:${GID:-1000}"
24
+ ports:
25
+ - "${MAILER_MCP_PORT:-3335}:8080"
26
+ environment:
27
+ - MAILERSEND_API_KEY=${MAILERSEND_API_KEY:-}
28
+ - MAILERSEND_FROM_EMAIL=${MAILERSEND_FROM_EMAIL:-no-reply@example.com}
29
+ - MAILERSEND_FROM_NAME=${MAILERSEND_FROM_NAME:-Mailer Agent}
30
+ - MAILERSEND_USER_AGENT=${MAILERSEND_USER_AGENT:-curl/8.7.1}
31
+ - MAILERSEND_VERIFY_SSL=${MAILERSEND_VERIFY_SSL:-true}
32
+ - MAILERSEND_CA_CERT=${MAILERSEND_CA_CERT:-}
33
+ - MAILER_REQUIRE_CONFIRMATION=${MAILER_REQUIRE_CONFIRMATION:-true}
34
+ - MAILER_DRY_RUN=${MAILER_DRY_RUN:-false}
35
+ - MCP_AUTH_TOKEN=${MAILER_MCP_AUTH_TOKEN:-}
36
+ restart: unless-stopped
@@ -1,6 +1,6 @@
1
1
  # agents.docker-compose.yml — external agent stack
2
2
  #
3
- # Starts cme, documents, and mailer as workspace-agnostic global services.
3
+ # Starts cme and documents as workspace-agnostic global services.
4
4
  # All workspace paths are resolved at tool-call time via the `workspace` param.
5
5
  #
6
6
  # Required:
@@ -10,18 +10,12 @@
10
10
  # AGENTS_DATA_DIR — persistent state for stateful agents (default: ./.agents-data)
11
11
  # CME_MCP_PORT — 3336
12
12
  # DOCUMENTS_MCP_PORT — 3337
13
- # MAILER_MCP_PORT — 3335
14
13
  # CME_MCP_AUTH_TOKEN — bearer token for cme agent (empty = no auth)
15
14
  # DOCUMENTS_MCP_AUTH_TOKEN — bearer token for documents agent (empty = no auth)
16
- # DOCUMENT_LLM_BASE_URL — https://api.openai.com/v1
17
- # DOCUMENT_LLM_MODEL — gpt-5.4-mini
15
+ # DOCUMENT_LLM_BASE_URL — https://albert.api.etalab.gouv.fr/v1
16
+ # DOCUMENT_LLM_MODEL — LightOnOCR-2-1B
18
17
  # DOCUMENT_LLM_API_KEY — OpenAI/OpenAI-compatible API key for document OCR
19
18
  # DOCUMENT_LLM_TIMEOUT_SECONDS — 120
20
- # MAILER_MCP_AUTH_TOKEN — bearer token for mailer agent (empty = no auth)
21
- # MAILERSEND_API_KEY
22
- # MAILERSEND_FROM_EMAIL
23
- # MAILERSEND_FROM_NAME
24
- # MAILERSEND_CA_CERT — optional CA bundle path inside the container
25
19
  #
26
20
  # Set these variables in a .env file at the directory where you run wiki-workspace,
27
21
  # or export them in your shell before running wiki-workspace agents up.
@@ -83,22 +77,3 @@ services:
83
77
  #- ${AGENTS_DATA_DIR:-./.agents-data}/certs:/certs:ro
84
78
  restart: unless-stopped
85
79
 
86
- mailer:
87
- image: dotdrelle/agent-mailer-api:latest
88
- user: "${UID:-1000}:${GID:-1000}"
89
- ports:
90
- - "${MAILER_MCP_PORT:-3335}:8080"
91
- environment:
92
- - MAILERSEND_API_KEY=${MAILERSEND_API_KEY:-}
93
- - MAILERSEND_FROM_EMAIL=${MAILERSEND_FROM_EMAIL:-no-reply@example.com}
94
- - MAILERSEND_FROM_NAME=${MAILERSEND_FROM_NAME:-Mailer Agent}
95
- - MAILERSEND_USER_AGENT=${MAILERSEND_USER_AGENT:-curl/8.7.1}
96
- - MAILERSEND_VERIFY_SSL=${MAILERSEND_VERIFY_SSL:-true}
97
- - MAILERSEND_CA_CERT=${MAILERSEND_CA_CERT:-}
98
- - MAILER_REQUIRE_CONFIRMATION=${MAILER_REQUIRE_CONFIRMATION:-true}
99
- - MAILER_DRY_RUN=${MAILER_DRY_RUN:-false}
100
- - MCP_AUTH_TOKEN=${MAILER_MCP_AUTH_TOKEN:-}
101
- # Optional: mount a CA bundle and set MAILERSEND_CA_CERT to its container path.
102
- # volumes:
103
- # - ${AGENTS_DATA_DIR:-./.agents-data}/certs:/certs:ro
104
- restart: unless-stopped
package/bin/wiki-manager CHANGED
@@ -71,6 +71,13 @@ if [[ -z "$BUN_BIN" ]]; then
71
71
  'Reinstall @dotdrelle/wiki-manager or install Bun from https://bun.sh/docs/installation.' >&2
72
72
  exit 127
73
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
74
81
 
75
82
  if [[ ${#ARGS[@]} -eq 0 ]]; then
76
83
  exec "$BUN_BIN" "$BIN_DIR/wiki-manager.js"
@@ -51,10 +51,8 @@ services:
51
51
  - PRODUCTION_MCP_PORT
52
52
  - CME_MCP_PORT
53
53
  - DOCUMENTS_MCP_PORT
54
- - MAILER_MCP_PORT
55
54
  - CME_MCP_AUTH_TOKEN
56
55
  - DOCUMENTS_MCP_AUTH_TOKEN
57
- - MAILER_MCP_AUTH_TOKEN
58
56
  - WORKSPACE_NAME
59
57
  - DOCUMENT_INPUT_DIR=/documents/input
60
58
  - DOCUMENT_UPLOADS_DIR=/documents/uploads
@@ -24,12 +24,6 @@
24
24
  "headers": {
25
25
  "Authorization": "Bearer ${DOCUMENTS_MCP_AUTH_TOKEN}"
26
26
  }
27
- },
28
- "mailer": {
29
- "url": "http://host.docker.internal:${MAILER_MCP_PORT:-3335}/mcp/",
30
- "headers": {
31
- "Authorization": "Bearer ${MAILER_MCP_AUTH_TOKEN}"
32
- }
33
27
  }
34
28
  }
35
29
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotdrelle/wiki-manager",
3
- "version": "0.12.8",
3
+ "version": "0.12.10",
4
4
  "description": "Agentic shell and orchestration cockpit for llm-wiki workspaces.",
5
5
  "license": "PolyForm-Noncommercial-1.0.0",
6
6
  "author": "dotrelle",
@@ -27,6 +27,7 @@
27
27
  "wiki-workspace",
28
28
  "docker-compose.yml",
29
29
  "agents.docker-compose.yml",
30
+ "agents.docker-compose.mailer.example.yml",
30
31
  "mcp.endpoints.example.json",
31
32
  ".env.example",
32
33
  "tsconfig.json",
@@ -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 }),
@@ -801,12 +804,18 @@ export async function runCli(argv) {
801
804
  let runtime = null;
802
805
  try {
803
806
  const { ensureRuntime } = await import('../runtime/lifecycle.js');
804
- 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 });
805
810
  } catch (err) {
806
811
  runtime = unavailableRuntime(err);
807
812
  console.error(`Runtime unavailable: ${runtime.error}`);
808
813
  }
809
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
+ }
810
819
  return;
811
820
  }
812
821
 
@@ -821,4 +830,8 @@ export async function runCli(argv) {
821
830
  }
822
831
  }
823
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
+ }
824
837
  }
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "0.12.8",
3
- "commit": "eefd59f"
2
+ "version": "0.12.10",
3
+ "commit": "e256e16"
4
4
  }
@@ -22,5 +22,7 @@ test('agent compose services run as the host uid and gid', async () => {
22
22
  const agentsCompose = YAML.parse(agentsRaw);
23
23
  assert.equal(agentsCompose.services.cme.user, '${UID:-1000}:${GID:-1000}');
24
24
  assert.equal(agentsCompose.services.documents.user, '${UID:-1000}:${GID:-1000}');
25
- assert.equal(agentsCompose.services.mailer.user, '${UID:-1000}:${GID:-1000}');
25
+ // MailerSend was an external-connector experiment — removed from the
26
+ // default stack (the agent repo still exists for ad-hoc use).
27
+ assert.equal(agentsCompose.services.mailer, undefined);
26
28
  });
package/src/core/env.js CHANGED
@@ -1,4 +1,4 @@
1
- import { copyFileSync, existsSync, readFileSync } from 'node:fs';
1
+ import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, isAbsolute, join, resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
 
@@ -38,8 +38,8 @@ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..')
38
38
  // mcp.endpoints.json nor .env, so the external agents (cme, mailer,
39
39
  // documents) silently never connect — /status shows no agents and Donna has
40
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; the
42
- // user only has to fill in tokens/keys.
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
43
  export function ensureManagerScaffold({ log = () => {} } = {}) {
44
44
  const created = [];
45
45
  const endpointsFile = managerMcpEndpointsFile();
@@ -51,11 +51,18 @@ export function ensureManagerScaffold({ log = () => {} } = {}) {
51
51
  const envFile = managerEnvFile();
52
52
  const envExample = join(packageRoot, '.env.example');
53
53
  if (!existsSync(envFile) && existsSync(envExample)) {
54
- copyFileSync(envExample, envFile);
54
+ // Substitute the documentation placeholders with real paths: a copied
55
+ // WORKSPACES_ROOT=/path/to/workspaces silently broke agents compose
56
+ // mounts until manually edited.
57
+ const workspacesRoot = join(managerStateDir(), 'workspaces');
58
+ const content = readFileSync(envExample, 'utf8')
59
+ .replace(/^WORKSPACES_ROOT=.*$/m, `WORKSPACES_ROOT=${workspacesRoot}`)
60
+ .replace(/^# WIKI_WORKSPACES_DIR=.*$/m, `WIKI_WORKSPACES_DIR=${workspacesRoot}`);
61
+ writeFileSync(envFile, content);
55
62
  created.push('.env');
56
63
  }
57
64
  if (created.length > 0) {
58
- log(`scaffold: created ${created.join(' and ')} in ${managerStateDir()} from packaged example(s) — fill in tokens/keys (WIKI_WORKSPACES_DIR, *_MCP_AUTH_TOKEN…) before starting agents.`);
65
+ log(`configuration initialized successfully in ${managerStateDir()} — created ${created.join(' and ')} from packaged defaults. Optional credentials can be added later for external services.`);
59
66
  }
60
67
  return created;
61
68
  }
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.8';
4
+ const WIKI_MANAGER_VERSION = '0.12.10';
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/src/shell/tui.tsx CHANGED
@@ -393,10 +393,13 @@ export async function runOpenTuiShell({
393
393
  packageJson: Record<string, unknown>;
394
394
  runtime?: any;
395
395
  }) {
396
- await render(() => <App agent={agent} packageJson={packageJson} runtime={runtime} />, {
397
- exitOnCtrlC: false,
398
- useMouse: true,
399
- targetFps: 30,
396
+ await new Promise<void>((resolve, reject) => {
397
+ render(() => <App agent={agent} packageJson={packageJson} runtime={runtime} />, {
398
+ exitOnCtrlC: false,
399
+ useMouse: true,
400
+ targetFps: 30,
401
+ onDestroy: resolve,
402
+ }).catch(reject);
400
403
  });
401
404
  return {};
402
405
  }
package/wiki-workspace CHANGED
@@ -86,7 +86,7 @@ Commands:
86
86
  up <workspace> [--open] Start workspace stack: wiki + production
87
87
  list List configured workspaces
88
88
 
89
- agents up [--pull] Start all external agents (cme, documents, mailer)
89
+ agents up [--pull] Start all external agents (cme, documents)
90
90
  agents down Stop all external agents
91
91
  agents logs [args...] Follow external agent logs
92
92
  agents pull Pull latest external agent images
@@ -215,9 +215,15 @@ agents_compose() {
215
215
  local cacert_args=()
216
216
  WORKSPACES_ROOT="$workspaces_root" AGENTS_DATA_DIR="$agents_data_dir" \
217
217
  read_lines_into_array cacert_args cacert_compose_args "$agents_compose_file" "agents.cacert.compose.yml"
218
+ # User-owned override: optional external agents (e.g. mailer) live in a
219
+ # file the user maintains NEXT TO the .env — never generated, never
220
+ # overwritten by package updates.
221
+ local override_args=()
222
+ local user_override="$MANAGER_STATE_DIR/agents.docker-compose.override.yml"
223
+ [[ -f "$user_override" ]] && override_args+=(-f "$user_override")
218
224
  WORKSPACES_ROOT="$workspaces_root" AGENTS_DATA_DIR="$agents_data_dir" \
219
225
  docker compose --project-directory "$DEFAULT_MANAGER_DIR" \
220
- ${compose_env_args[@]+"${compose_env_args[@]}"} -f "$agents_compose_file" ${cacert_args[@]+"${cacert_args[@]}"} -p wiki-agents "$@"
226
+ ${compose_env_args[@]+"${compose_env_args[@]}"} -f "$agents_compose_file" ${override_args[@]+"${override_args[@]}"} ${cacert_args[@]+"${cacert_args[@]}"} -p wiki-agents "$@"
221
227
  }
222
228
 
223
229
  case "$subcommand" in
@@ -245,8 +251,9 @@ agents_compose() {
245
251
  printf 'Workspaces root: %s\n' "$workspaces_root"
246
252
  printf 'Agents data: %s\n' "$agents_data_dir"
247
253
  [[ -z "$CACERT_PATH" ]] || printf 'CA cert: %s -> /wiki-manager-ca.pem\n' "$CACERT_PATH"
248
- printf 'Agents: cme=:%s documents=:%s mailer=:%s\n' \
249
- "${CME_MCP_PORT:-3336}" "${DOCUMENTS_MCP_PORT:-3337}" "${MAILER_MCP_PORT:-3335}"
254
+ printf 'Agents: cme=:%s documents=:%s\n' \
255
+ "${CME_MCP_PORT:-3336}" "${DOCUMENTS_MCP_PORT:-3337}"
256
+ [[ ! -f "$MANAGER_STATE_DIR/agents.docker-compose.override.yml" ]] || printf 'User override: agents.docker-compose.override.yml included\n'
250
257
  ;;
251
258
  down)
252
259
  [[ $# -eq 0 ]] || die "agents down does not take arguments"
@@ -389,12 +396,12 @@ ensure_runtime_node() {
389
396
  }
390
397
 
391
398
  ensure_agent_tokens() {
392
- # The external agents (cme, documents, mailer) authenticate with tokens
399
+ # The external agents (cme, documents) authenticate with tokens
393
400
  # from the manager .env — generate any missing one, exactly like the
394
401
  # workspace and runtime tokens are generated. Leaving them empty made a
395
402
  # fresh install start agents nobody could talk to.
396
403
  local key token
397
- for key in CME_MCP_AUTH_TOKEN DOCUMENTS_MCP_AUTH_TOKEN MAILER_MCP_AUTH_TOKEN; do
404
+ for key in CME_MCP_AUTH_TOKEN DOCUMENTS_MCP_AUTH_TOKEN; do
398
405
  token="$(env_value "$MANAGER_ENV_FILE" "$key" "")"
399
406
  if [[ -z "$token" ]]; then
400
407
  token="$(generate_token)"