@dotdrelle/wiki-manager 0.12.8 → 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
@@ -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`).
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"
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.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",
@@ -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.9",
3
+ "commit": "286bec2"
4
4
  }
package/src/core/env.js CHANGED
@@ -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();
@@ -55,7 +55,7 @@ export function ensureManagerScaffold({ log = () => {} } = {}) {
55
55
  created.push('.env');
56
56
  }
57
57
  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.`);
58
+ log(`configuration initialized successfully in ${managerStateDir()} — created ${created.join(' and ')} from packaged defaults. Optional credentials can be added later for external services.`);
59
59
  }
60
60
  return created;
61
61
  }
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.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
+ });