@dotdrelle/wiki-manager 0.15.26 → 0.15.27

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.
Files changed (38) hide show
  1. package/.env.example +18 -5
  2. package/README.md +35 -13
  3. package/agents.docker-compose.override.example.yml +82 -0
  4. package/agents.docker-compose.yml +8 -2
  5. package/docker-compose.override.example.yml +81 -0
  6. package/docker-compose.yml +23 -17
  7. package/package.json +4 -2
  8. package/src/cli/wiki-manager.js +3 -1
  9. package/src/commands/slash.js +53 -39
  10. package/src/core/agentEvents.js +20 -0
  11. package/src/core/agentEvents.test.js +38 -1
  12. package/src/core/buildInfo.json +2 -2
  13. package/src/core/commandFailure.js +81 -0
  14. package/src/core/commandFailure.test.js +67 -0
  15. package/src/core/compose.js +20 -3
  16. package/src/core/composeOverrides.test.js +74 -0
  17. package/src/core/dockerCompose.test.js +69 -2
  18. package/src/core/env.js +59 -0
  19. package/src/core/env.test.js +33 -1
  20. package/src/core/mcp.js +1 -1
  21. package/src/core/setEnvValue.test.js +96 -0
  22. package/src/runtime/client.js +18 -0
  23. package/src/runtime/lifecycle.js +5 -1
  24. package/src/runtime/server.js +43 -1
  25. package/src/runtime/server.test.js +117 -0
  26. package/src/runtime/store.js +47 -0
  27. package/src/runtime/store.test.js +89 -1
  28. package/src/shell/LeftPane.tsx +46 -16
  29. package/src/shell/RightPane.tsx +2 -4
  30. package/src/shell/SetupWizard.tsx +19 -7
  31. package/src/shell/externalLinks.js +24 -2
  32. package/src/shell/openExternal.js +53 -0
  33. package/src/shell/repl.js +33 -10
  34. package/src/shell/repl.test.js +91 -9
  35. package/src/shell/setupWizardPlaceholders.test.js +66 -0
  36. package/src/shell/tui.tsx +54 -25
  37. package/src/shell/useSession.ts +42 -6
  38. package/wiki-workspace +84 -6
package/.env.example CHANGED
@@ -78,10 +78,15 @@ CONNECTORS_ENABLED=false
78
78
 
79
79
  # ── Documents LLM OCR / Mermaid (optional) ─────────────────────────────────────
80
80
 
81
- # All image OCR is delegated to an OpenAI-compatible vision LLM. The default
82
- # Default target is the Albert API (etalab); set DOCUMENT_LLM_API_KEY.
83
- # DOCUMENT_LLM_BASE_URL=https://albert.api.etalab.gouv.fr/v1
84
- # DOCUMENT_LLM_MODEL=lightonai/LightOnOCR-2-1B
81
+ # All image OCR is delegated to an OpenAI-compatible vision LLM, and it is OFF
82
+ # until you configure it here. The compose file passes empty defaults on
83
+ # purpose, so no deployment ever calls a provider it was not pointed at.
84
+ #
85
+ # Uncomment the three keys together — the agent disables LLM OCR when any of
86
+ # the endpoint, the model or the key is missing.
87
+ #
88
+ # DOCUMENT_LLM_BASE_URL=
89
+ # DOCUMENT_LLM_MODEL=
85
90
  # DOCUMENT_LLM_API_KEY=
86
91
  # DOCUMENT_LLM_TIMEOUT_SECONDS=120
87
92
 
@@ -99,7 +104,15 @@ CONNECTORS_ENABLED=false
99
104
  # WIKI_MANAGER_RUNTIME_PORT=7788
100
105
  # Set to 0 to skip pulling and renewing already-running containers at startup.
101
106
  # WIKI_MANAGER_AUTO_UPDATE=1
102
- # WIKI_MANAGER_RUNTIME_HOST=0.0.0.0
107
+ #
108
+ # Active by default, and required: `serve` runs in Docker and reaches the host
109
+ # runtime through host.docker.internal. A runtime bound to 127.0.0.1 only
110
+ # accepts connections from the host itself, so the container gets ECONNREFUSED
111
+ # and the workspace UI shows no runtime at all. Binding all interfaces always
112
+ # generates WIKI_MANAGER_RUNTIME_TOKEN (0600 in .wiki/runtime/runtime.token),
113
+ # so the exposed port is never unauthenticated.
114
+ # Set to 127.0.0.1 only when nothing runs in a container.
115
+ WIKI_MANAGER_RUNTIME_HOST=0.0.0.0
103
116
 
104
117
 
105
118
  # ── Parallelism & throughput ───────────────────────────────────────────────────
package/README.md CHANGED
@@ -729,17 +729,33 @@ capabilityRouting:
729
729
  allowedAgents: [connectors]
730
730
  ```
731
731
 
732
- #### Optional agents and user overrides
732
+ #### Compose overrides — optional agents, proxies, local fixes
733
733
 
734
- Anything beyond the packaged stack is an external connector operated and
735
- configured independently by the user. To run one alongside the packaged
736
- agents, create a file named
737
- `agents.docker-compose.override.yml` **next to your `.env`**:
734
+ Two override files sit **next to your `.env`**, one per stack:
738
735
 
739
- `agents up` includes it automatically when present (standard Docker Compose
740
- merge: new services are added, same-name keys override the defaults — you can
741
- also use it to pin a port or a variable of a default agent). The file is
742
- yours: wiki-manager never generates or overwrites it. Complete the setup by
736
+ | File | Applies to |
737
+ | --- | --- |
738
+ | `docker-compose.override.yml` | workspace stack (`serve`, `mcp-http`, `production-mcp`, `wiki`) |
739
+ | `agents.docker-compose.override.yml` | agents stack (`cme`, `documents`, `connectors`) |
740
+
741
+ Both are created for you on first use, from packaged templates full of
742
+ ready-to-uncomment examples, and are **never rewritten afterwards** — your edits
743
+ survive package updates. Do not confuse them with `.wiki/runtime/*.compose.yml`,
744
+ which the manager regenerates on every Compose command; editing those is always
745
+ lost.
746
+
747
+ Compose merge is standard: new services are added, same-name keys override the
748
+ defaults, `environment` merges per variable name. Only extend services the
749
+ packaged file declares — an invented service name becomes a phantom service
750
+ Compose keeps trying to start.
751
+
752
+ The most common use behind a VPN is proxy passthrough: containers do not inherit
753
+ the host environment, and only `connectors` ships proxy variables by default. See
754
+ [`docs/configuration.md`](docs/configuration.md) § "Compose overrides" for a
755
+ copy-paste block and the `host.docker.internal` / `NO_PROXY` pitfalls.
756
+
757
+ The second use is running an external connector alongside the packaged agents.
758
+ Complete the setup by
743
759
  adding the connector's variables to your `.env` and its endpoint block to
744
760
  your `mcp.endpoints.json` — every variable an external MCP endpoint needs
745
761
  lives in the `.env` and is referenced as `${VAR_NAME}` from
@@ -872,11 +888,17 @@ The TUI uses a two-pane layout:
872
888
  - **Left** — scrollable conversation thread with a chat input at the bottom.
873
889
  Typing `/` opens a slash-command completion overlay just above the input.
874
890
  Mouse wheel scrolls the conversation, and selecting text copies it through the
875
- TUI clipboard bridge. Message headers also expose a `[ copy ]` target for
876
- copying one message. PageUp/PageDown remain available for keyboard scrolling.
891
+ TUI clipboard bridge. Message headers expose a `[ copy ]` target for copying
892
+ one message, and user questions also carry `[ redo ]`: it discards every
893
+ answer, plan step and activity recorded after that question — in the runtime
894
+ as well as on screen — then re-asks it. Redo is refused while a run is still
895
+ active; cancel it first. PageUp/PageDown remain available for keyboard
896
+ scrolling.
877
897
  - **Right** — Plan/Queue tabs, active MCP jobs, plus a live log/trace panel.
878
- `Ctrl+Q` toggles the tabs; clicking `Plan` or `Queue (N)` selects that tab
879
- directly. MCP connection details remain available through `/mcp status`.
898
+ Click `Plan` or `Queue (N)` to select a tab. `Queue (N)` counts run requests
899
+ sent while the runtime is already busy, so it reads `(0)` whenever you submit
900
+ one request at a time. MCP connection details remain available through
901
+ `/mcp status`.
880
902
 
881
903
  In the served browser Activity panel, `Clear` is a per-tab display cleanup and
882
904
  `Clear all` applies it to Plan, Local activity, Runtime activity, and Logs. It
@@ -0,0 +1,82 @@
1
+ # agents.docker-compose.override.yml — user-owned overrides for the agents stack
2
+ #
3
+ # Copied here once by wiki-manager when absent, then NEVER touched again:
4
+ # your edits survive every package update. Do not confuse it with
5
+ # .wiki/runtime/agents.cacert.compose.yml, which is generated state and is
6
+ # rewritten on every compose command — editing that one is always lost.
7
+ #
8
+ # Merge order: agents.docker-compose.yml → this file → generated CA override.
9
+ # `environment` entries merge per variable name; `volumes`, `extra_hosts` and
10
+ # other sequences are appended. Only services declared in the packaged file
11
+ # (cme, documents, connectors) should be extended — inventing a service name
12
+ # here creates a phantom service that compose will try to start.
13
+ #
14
+ # To activate anything below: replace the `services: {}` line at the bottom
15
+ # with your own `services:` block, then `wiki-workspace agents up`.
16
+ # Check the result with `wiki-workspace agents config` before restarting.
17
+ #
18
+ # ── Corporate proxy / VPN ─────────────────────────────────────────────────────
19
+ #
20
+ # Docker never inherits the host environment: exporting HTTP_PROXY in your shell
21
+ # does NOT reach a container. Only `connectors` declares proxy variables in the
22
+ # packaged file, so cme and documents try to reach Confluence and the LLM APIs
23
+ # directly. Declare the variables here, and set their values in the manager .env:
24
+ #
25
+ # HTTP_PROXY=http://host.docker.internal:3128
26
+ # HTTPS_PROXY=http://host.docker.internal:3128
27
+ # NO_PROXY=localhost,127.0.0.1,host.docker.internal
28
+ #
29
+ # Use `host.docker.internal` (never `localhost`, which is the container itself)
30
+ # when the proxy listens on the host — this requires the extra_hosts mapping
31
+ # below on Linux. Set both the upper- and lower-case spellings: Node reads the
32
+ # upper-case ones, Python/requests accepts either but some libraries only look
33
+ # at the lower-case ones.
34
+ #
35
+ # services:
36
+ # cme:
37
+ # environment:
38
+ # - HTTP_PROXY=${HTTP_PROXY:-}
39
+ # - HTTPS_PROXY=${HTTPS_PROXY:-}
40
+ # - http_proxy=${HTTP_PROXY:-}
41
+ # - https_proxy=${HTTPS_PROXY:-}
42
+ # - NO_PROXY=${NO_PROXY:-localhost,127.0.0.1,host.docker.internal}
43
+ # - no_proxy=${NO_PROXY:-localhost,127.0.0.1,host.docker.internal}
44
+ # extra_hosts:
45
+ # - host.docker.internal:host-gateway
46
+ # documents:
47
+ # environment:
48
+ # - HTTP_PROXY=${HTTP_PROXY:-}
49
+ # - HTTPS_PROXY=${HTTPS_PROXY:-}
50
+ # - http_proxy=${HTTP_PROXY:-}
51
+ # - https_proxy=${HTTPS_PROXY:-}
52
+ # - NO_PROXY=${NO_PROXY:-localhost,127.0.0.1,host.docker.internal}
53
+ # - no_proxy=${NO_PROXY:-localhost,127.0.0.1,host.docker.internal}
54
+ # extra_hosts:
55
+ # - host.docker.internal:host-gateway
56
+ #
57
+ # A TLS-intercepting proxy also needs its CA inside the containers. Do not mount
58
+ # it here: pass it once with `wiki-workspace --cacert /absolute/path/to/ca.pem`
59
+ # and the manager mounts it into every service with the right env variables
60
+ # (NODE_EXTRA_CA_CERTS, SSL_CERT_FILE, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE).
61
+ #
62
+ # ── Optional external agents ──────────────────────────────────────────────────
63
+ #
64
+ # The MailerSend agent is built and version-checked but not part of the default
65
+ # delivery. Enable it by declaring it here (credentials stay in the container
66
+ # environment; set MAILERSEND_API_KEY in the manager .env):
67
+ #
68
+ # services:
69
+ # mailer:
70
+ # image: dotdrelle/agent-mailer-api:latest
71
+ # user: "${UID:-1000}:${GID:-1000}"
72
+ # ports:
73
+ # - "${MAILER_MCP_PORT:-3339}:8080"
74
+ # environment:
75
+ # - MCP_AUTH_TOKEN=${MAILER_MCP_AUTH_TOKEN:-}
76
+ # - MAILERSEND_API_KEY=${MAILERSEND_API_KEY:-}
77
+ # restart: unless-stopped
78
+ #
79
+ # Remember to declare the matching endpoint in mcp.endpoints.json, otherwise
80
+ # Donna never discovers the agent.
81
+
82
+ services: {}
@@ -72,8 +72,14 @@ services:
72
72
  - DOCUMENT_OUTPUT_DIR=/documents/output
73
73
  - WORKSPACES_ROOT=/workspaces
74
74
  - DOCUMENT_MAX_UPLOAD_BYTES=${DOCUMENT_MAX_UPLOAD_BYTES:-52428800}
75
- - DOCUMENT_LLM_BASE_URL=${DOCUMENT_LLM_BASE_URL:-https://albert.api.etalab.gouv.fr/v1}
76
- - DOCUMENT_LLM_MODEL=${DOCUMENT_LLM_MODEL:-lightonai/LightOnOCR-2-1B}
75
+ # Deliberately empty defaults: the .env is the only source. A hardcoded
76
+ # default here sent every deployment to a provider the operator never
77
+ # chose and could not see, since the keys ship commented. An empty value
78
+ # also overrides the image's own ENV fallback, so nothing is called until
79
+ # the operator opts in. The agent already disables LLM OCR when the key or
80
+ # the model is unset, so this degrades cleanly rather than failing.
81
+ - DOCUMENT_LLM_BASE_URL=${DOCUMENT_LLM_BASE_URL:-}
82
+ - DOCUMENT_LLM_MODEL=${DOCUMENT_LLM_MODEL:-}
77
83
  - DOCUMENT_LLM_API_KEY=${DOCUMENT_LLM_API_KEY:-}
78
84
  - DOCUMENT_LLM_TIMEOUT_SECONDS=${DOCUMENT_LLM_TIMEOUT_SECONDS:-120}
79
85
  # HTTPS — uncomment and place cert files in AGENTS_DATA_DIR/certs/
@@ -0,0 +1,81 @@
1
+ # docker-compose.override.yml — user-owned overrides for the workspace stack
2
+ #
3
+ # Copied here once by wiki-manager when absent, then NEVER touched again:
4
+ # your edits survive every package update. Do not confuse it with
5
+ # .wiki/runtime/cacert.compose.yml, which is generated state and is rewritten
6
+ # on every compose command — editing that one is always lost.
7
+ #
8
+ # This file is GLOBAL: it lives next to the manager .env and applies to every
9
+ # workspace stack (`wiki-workspace up <workspace>`, `/start` in the shell). A
10
+ # proxy or a private registry is a property of the machine, not of a workspace,
11
+ # so there is deliberately no per-workspace variant. For values that really do
12
+ # differ per workspace, use workspaces/<name>/.env and reference them here with
13
+ # ${VARIABLE}.
14
+ #
15
+ # Merge order: docker-compose.yml → this file → generated CA override.
16
+ # `environment` entries merge per variable name; `volumes`, `extra_hosts` and
17
+ # other sequences are appended. Only services declared in the packaged file
18
+ # (wiki, serve, mcp-http, production-mcp) should be extended — inventing a
19
+ # service name here creates a phantom service that compose will try to start.
20
+ #
21
+ # To activate anything below: replace the `services: {}` line at the bottom
22
+ # with your own `services:` block, then restart the workspace.
23
+ # Check the result with `wiki-workspace compose <workspace> config`.
24
+ #
25
+ # ── Corporate proxy / VPN ─────────────────────────────────────────────────────
26
+ #
27
+ # Docker never inherits the host environment: exporting HTTP_PROXY in your shell
28
+ # does NOT reach a container. None of the workspace services declares proxy
29
+ # variables, so serve and production-mcp reach LLM endpoints directly. Declare
30
+ # them here and set the values in the manager .env:
31
+ #
32
+ # HTTP_PROXY=http://host.docker.internal:3128
33
+ # HTTPS_PROXY=http://host.docker.internal:3128
34
+ # NO_PROXY=localhost,127.0.0.1,host.docker.internal
35
+ #
36
+ # NO_PROXY must keep host.docker.internal: serve, the runtime and the MCP
37
+ # servers talk to each other through it, and routing that traffic through the
38
+ # proxy breaks the workspace even when the outside world works.
39
+ #
40
+ # services:
41
+ # serve:
42
+ # environment:
43
+ # - HTTP_PROXY=${HTTP_PROXY:-}
44
+ # - HTTPS_PROXY=${HTTPS_PROXY:-}
45
+ # - http_proxy=${HTTP_PROXY:-}
46
+ # - https_proxy=${HTTPS_PROXY:-}
47
+ # - NO_PROXY=${NO_PROXY:-localhost,127.0.0.1,host.docker.internal}
48
+ # - no_proxy=${NO_PROXY:-localhost,127.0.0.1,host.docker.internal}
49
+ # production-mcp:
50
+ # environment:
51
+ # - HTTP_PROXY=${HTTP_PROXY:-}
52
+ # - HTTPS_PROXY=${HTTPS_PROXY:-}
53
+ # - http_proxy=${HTTP_PROXY:-}
54
+ # - https_proxy=${HTTPS_PROXY:-}
55
+ # - NO_PROXY=${NO_PROXY:-localhost,127.0.0.1,host.docker.internal}
56
+ # - no_proxy=${NO_PROXY:-localhost,127.0.0.1,host.docker.internal}
57
+ # # production-mcp has no host mapping in the packaged file; add it when
58
+ # # the proxy (or an LLM endpoint) listens on the host.
59
+ # extra_hosts:
60
+ # - host.docker.internal:host-gateway
61
+ #
62
+ # A TLS-intercepting proxy also needs its CA inside the containers. Do not mount
63
+ # it here: pass it once with `wiki-workspace --cacert /absolute/path/to/ca.pem`
64
+ # and the manager mounts it into every service with the right env variables
65
+ # (NODE_EXTRA_CA_CERTS, SSL_CERT_FILE, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE).
66
+ #
67
+ # ── Other common uses ─────────────────────────────────────────────────────────
68
+ #
69
+ # services:
70
+ # serve:
71
+ # # Extra read-only mount, e.g. a shared templates directory.
72
+ # volumes:
73
+ # - /srv/shared/templates:/templates:ro
74
+ # production-mcp:
75
+ # # Pin resources on a loaded host.
76
+ # deploy:
77
+ # resources:
78
+ # limits:
79
+ # cpus: '2.0'
80
+
81
+ services: {}
@@ -25,7 +25,7 @@ services:
25
25
  - ${WIKI_WORKSPACE_PATH:-/tmp/llm-wiki-workspace-not-set}:/workspace
26
26
  environment:
27
27
  - WIKI_WORKSPACE_PATH=/workspace
28
- - WIKI_CONFIG_PATH
28
+ - WIKI_CONFIG_PATH=${WIKI_CONFIG_PATH:-}
29
29
  extra_hosts:
30
30
  - host.docker.internal:host-gateway
31
31
 
@@ -47,22 +47,28 @@ services:
47
47
  #- ${WIKI_SERVE_CERTS_DIR:-./certs}:/certs:ro
48
48
  environment:
49
49
  - WIKI_WORKSPACE_PATH=/workspace
50
- - WIKI_CONFIG_PATH
50
+ # Every variable below is written as ${VAR:-default}, never as a bare
51
+ # `- VAR` passthrough. The bare form makes Compose emit
52
+ # `The "VAR" variable is not set. Defaulting to a blank string.` on stderr
53
+ # for any key the operator has not written in a .env — the ports and
54
+ # tokens shipped as commented placeholders in .env.example. The warning is
55
+ # noise that surfaces in the ShellUI as if it were a failure.
56
+ - WIKI_CONFIG_PATH=${WIKI_CONFIG_PATH:-}
51
57
  - WIKI_MCP_HTTP_PORT=${WIKI_MCP_PORT:-3101}
52
- - WIKI_MCP_AUTH_TOKEN
53
- - PRODUCTION_MCP_AUTH_TOKEN
54
- - PRODUCTION_MCP_PORT
55
- - CME_MCP_PORT
56
- - DOCUMENTS_MCP_PORT
57
- - CME_MCP_AUTH_TOKEN
58
- - DOCUMENTS_MCP_AUTH_TOKEN
58
+ - WIKI_MCP_AUTH_TOKEN=${WIKI_MCP_AUTH_TOKEN:-}
59
+ - PRODUCTION_MCP_AUTH_TOKEN=${PRODUCTION_MCP_AUTH_TOKEN:-}
60
+ - PRODUCTION_MCP_PORT=${PRODUCTION_MCP_PORT:-3102}
61
+ - CME_MCP_PORT=${CME_MCP_PORT:-3336}
62
+ - DOCUMENTS_MCP_PORT=${DOCUMENTS_MCP_PORT:-3337}
63
+ - CME_MCP_AUTH_TOKEN=${CME_MCP_AUTH_TOKEN:-}
64
+ - DOCUMENTS_MCP_AUTH_TOKEN=${DOCUMENTS_MCP_AUTH_TOKEN:-}
59
65
  # Connectors MCP: the serve panel resolves ${CONNECTORS_MCP_AUTH_TOKEN} in
60
66
  # /mcp.endpoints.json from process.env only, so it must be forwarded here
61
67
  # (symmetric with cme/documents) or the panel sends an empty Bearer → 401.
62
- - CONNECTORS_MCP_PORT
63
- - CONNECTORS_MCP_AUTH_TOKEN
68
+ - CONNECTORS_MCP_PORT=${CONNECTORS_MCP_PORT:-3338}
69
+ - CONNECTORS_MCP_AUTH_TOKEN=${CONNECTORS_MCP_AUTH_TOKEN:-}
64
70
  - CONNECTORS_OAUTH_START_TOKEN=${OAUTH_START_TOKEN:-}
65
- - WORKSPACE_NAME
71
+ - WORKSPACE_NAME=${WORKSPACE_NAME:-}
66
72
  - DOCUMENT_INPUT_DIR=/documents/input
67
73
  - DOCUMENT_UPLOADS_DIR=/documents/uploads
68
74
  - DOCUMENT_MAX_UPLOAD_BYTES=${DOCUMENT_MAX_UPLOAD_BYTES:-52428800}
@@ -92,12 +98,12 @@ services:
92
98
  #- ${WIKI_MCP_CERTS_DIR:-./certs}:/certs:ro
93
99
  environment:
94
100
  - WIKI_WORKSPACE_PATH=/workspace
95
- - WIKI_CONFIG_PATH
96
- - WIKI_MCP_AUTH_TOKEN
101
+ - WIKI_CONFIG_PATH=${WIKI_CONFIG_PATH:-}
102
+ - WIKI_MCP_AUTH_TOKEN=${WIKI_MCP_AUTH_TOKEN:-}
97
103
  # HTTPS — set paths inside the container (e.g. /certs/server.crt) and uncomment the volume above
98
- - WIKI_MCP_TLS_CERT_PATH
99
- - WIKI_MCP_TLS_KEY_PATH
100
- - WIKI_MCP_TLS_CA_PATH
104
+ - WIKI_MCP_TLS_CERT_PATH=${WIKI_MCP_TLS_CERT_PATH:-}
105
+ - WIKI_MCP_TLS_KEY_PATH=${WIKI_MCP_TLS_KEY_PATH:-}
106
+ - WIKI_MCP_TLS_CA_PATH=${WIKI_MCP_TLS_CA_PATH:-}
101
107
  ports:
102
108
  - '${WIKI_MCP_PORT:-3101}:3333'
103
109
  extra_hosts:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotdrelle/wiki-manager",
3
- "version": "0.15.26",
3
+ "version": "0.15.27",
4
4
  "description": "Agentic shell and orchestration cockpit for llm-wiki workspaces.",
5
5
  "license": "PolyForm-Noncommercial-1.0.0",
6
6
  "author": "dotrelle",
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "scripts": {
13
13
  "start": "bun ./bin/wiki-manager.js",
14
- "test": "node --test src/cli/runtimeStartup.test.js src/cli/wiki-manager.test.js src/agent/graph.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/env.test.js src/core/buildInfo.test.js src/core/agentEvents.test.js src/core/runtimeLog.test.js src/activity/activityAggregator.test.js src/graph/runGraphProjector.test.js src/core/workflow.test.js src/core/planPatch.test.js src/core/agentLoop.test.js src/core/plan.test.js src/core/mcp.test.js src/core/toolLoop.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/wikiWorkspace.test.js src/core/wikirc.test.js src/core/cacert.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/core/queueStore.test.js src/orchestrator/agentRegistry.test.js src/orchestrator/capabilityRegistry.test.js src/orchestrator/capabilityResolver.test.js src/orchestrator/planValidator.test.js src/orchestrator/planIntegrator.test.js src/orchestrator/scheduler.test.js src/orchestrator/attemptManager.test.js src/orchestrator/resultAggregator.test.js src/orchestrator/approvalPolicy.test.js src/orchestrator/dispatcher.test.js src/orchestrator/objectiveResolver.test.js src/commands/slash.test.js src/shell/repl.test.js src/runtime/store.test.js src/runtime/controlMessages.test.js src/runtime/recoveryManager.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/runner.test.js src/runtime/runner.e2e.test.js src/runtime/donna-contract.test.js src/runtime/auth.test.js",
14
+ "test": "node --test src/cli/runtimeStartup.test.js src/cli/wiki-manager.test.js src/agent/graph.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/env.test.js src/core/buildInfo.test.js src/core/agentEvents.test.js src/core/runtimeLog.test.js src/activity/activityAggregator.test.js src/graph/runGraphProjector.test.js src/core/workflow.test.js src/core/planPatch.test.js src/core/agentLoop.test.js src/core/plan.test.js src/core/mcp.test.js src/core/toolLoop.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/wikiWorkspace.test.js src/core/wikirc.test.js src/core/cacert.test.js src/core/composeOverrides.test.js src/core/setEnvValue.test.js src/core/commandFailure.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/core/queueStore.test.js src/orchestrator/agentRegistry.test.js src/orchestrator/capabilityRegistry.test.js src/orchestrator/capabilityResolver.test.js src/orchestrator/planValidator.test.js src/orchestrator/planIntegrator.test.js src/orchestrator/scheduler.test.js src/orchestrator/attemptManager.test.js src/orchestrator/resultAggregator.test.js src/orchestrator/approvalPolicy.test.js src/orchestrator/dispatcher.test.js src/orchestrator/objectiveResolver.test.js src/commands/slash.test.js src/shell/repl.test.js src/shell/setupWizardPlaceholders.test.js src/runtime/store.test.js src/runtime/controlMessages.test.js src/runtime/recoveryManager.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/runner.test.js src/runtime/runner.e2e.test.js src/runtime/donna-contract.test.js src/runtime/auth.test.js",
15
15
  "check-versions": "node scripts/check-versions.js",
16
16
  "prepack": "node scripts/check-versions.js",
17
17
  "prepublishOnly": "node scripts/check-versions.js",
@@ -27,6 +27,8 @@
27
27
  "wiki-workspace",
28
28
  "docker-compose.yml",
29
29
  "agents.docker-compose.yml",
30
+ "docker-compose.override.example.yml",
31
+ "agents.docker-compose.override.example.yml",
30
32
  "mcp.endpoints.example.json",
31
33
  ".env.example",
32
34
  "tsconfig.json",
@@ -660,7 +660,9 @@ async function runRuntime(argv, agent) {
660
660
  const { createApprovalManager } = await import('../runtime/approvals.js');
661
661
  const { conversationSeed, runRuntimeAgenticWorkflow } = await import('../runtime/runner.js');
662
662
 
663
- const host = valueAfter(argv, '--host') ?? process.env.WIKI_MANAGER_RUNTIME_HOST ?? '127.0.0.1';
663
+ // Same default as wiki-workspace runtime up and ensureRuntime: a loopback
664
+ // bind is invisible to the serve container.
665
+ const host = valueAfter(argv, '--host') ?? process.env.WIKI_MANAGER_RUNTIME_HOST ?? '0.0.0.0';
664
666
  const port = Number(valueAfter(argv, '--port') ?? process.env.WIKI_MANAGER_RUNTIME_PORT ?? 7788);
665
667
  const stateDir = valueAfter(argv, '--state-dir') ?? defaultRuntimeStateDir();
666
668
  const auth = resolveRuntimeAuthToken({ host, stateDir });
@@ -1,7 +1,8 @@
1
- import { execFileSync } from 'node:child_process';
2
1
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { openExternalUrl } from '../shell/openExternal.js';
3
+ import { classifyCommandFailure, failureHint, rawFailureText } from '../core/commandFailure.js';
3
4
  import { join, relative } from 'node:path';
4
- import { composeServices, listServices, runWikiCli, serviceLogs, serviceStates, startService, stopService } from '../core/compose.js';
5
+ import { composeServices, listServices, runWikiCli, serviceLogs, serviceNames, serviceStates, startService, stopService } from '../core/compose.js';
5
6
  import {
6
7
  applyMcpRuntimeStatus,
7
8
  buildMcpStatus,
@@ -187,17 +188,7 @@ function countIndexLinks(workspacePath) {
187
188
  function folderStats(files) {
188
189
  const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
189
190
  const latest = files.reduce((best, file) => (file.mtimeMs > (best?.mtimeMs ?? 0) ? file : best), null);
190
- const largest = files.reduce((best, file) => (file.size > (best?.size ?? 0) ? file : best), null);
191
- return { count: files.length, totalBytes, latest, largest };
192
- }
193
-
194
- function formatRecentFiles(files, limit = 3) {
195
- const recent = [...files].sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
196
- if (recent.length === 0) return ' recent: -';
197
- return [
198
- ' recent:',
199
- ...recent.map((file) => ` - ${file.relativePath} (${formatBytes(file.size)})`),
200
- ].join('\n');
191
+ return { count: files.length, totalBytes, latest };
201
192
  }
202
193
 
203
194
  function collectWorkspaceStats(session) {
@@ -224,7 +215,6 @@ function collectWorkspaceStats(session) {
224
215
  deliverables: folderStats(deliverables),
225
216
  logs: folderStats(logs),
226
217
  index,
227
- untrackedFiles: untracked,
228
218
  };
229
219
  }
230
220
 
@@ -265,11 +255,12 @@ function workspaceStatsColumns(stats) {
265
255
  statLine('answers', stats.answers),
266
256
  `index: ${stats.index.exists ? 'ok' : 'missing'} (${stats.index.links} links)`,
267
257
  ]);
258
+ // Counts only. `largest:` and the `recent:` list printed full relative paths
259
+ // in a half-width column, so every line wrapped or was clipped and the block
260
+ // read as noise. The file listing belongs to a command with room for it.
268
261
  const rawColumn = sectionBlock('Raw sources', [
269
262
  statLine('untracked', stats.untracked),
270
263
  statLine('ingested', stats.ingested),
271
- stats.untracked.largest ? `largest: ${stats.untracked.largest.relativePath} (${formatBytes(stats.untracked.largest.size)})` : 'largest: -',
272
- ...formatRecentFiles(stats.untrackedFiles).split('\n'),
273
264
  ]);
274
265
  const deliveryColumn = sectionBlock(`Deliverables: ${deliverablesLatest}`, [
275
266
  statLine('templates', stats.templates),
@@ -723,6 +714,32 @@ export function localizedOperationResult({ operation, target, status = 'succeede
723
714
  };
724
715
  }
725
716
 
717
+ // Failure counterpart of localizedOperationResult. The raw docker output never
718
+ // reaches the conversation: it goes to the runtime log lane through `step()`,
719
+ // while Donna gets a stable reason code she can phrase — and act on, since
720
+ // every reason maps to something the operator can actually do.
721
+ export function localizedOperationFailure({ operation, target, error }) {
722
+ const reason = classifyCommandFailure(error);
723
+ const hint = reason === 'unknown' ? failureHint(error) : '';
724
+ const facts = JSON.stringify({
725
+ operation,
726
+ target,
727
+ status: 'failed',
728
+ reason,
729
+ ...(hint ? { detail: hint } : {}),
730
+ });
731
+ return {
732
+ output: facts,
733
+ rawOutput: true,
734
+ agentTrigger: [
735
+ "Formule l'échec structuré suivant dans la langue et le ton demandés par le profil du workspace.",
736
+ "Réponds en une ou deux phrases humaines: ce qui a échoué, et l'action concrète que la personne peut faire.",
737
+ 'Ne cite aucune commande, aucun chemin de fichier, aucun drapeau shell ni sortie docker.',
738
+ `Résultat: ${facts}`,
739
+ ].join('\n'),
740
+ };
741
+ }
742
+
726
743
  function formatRuntimeRunStatus(state) {
727
744
  const status = state?.status ?? 'unknown';
728
745
  const runId = state?.runId ? ` run=${state.runId}` : '';
@@ -770,7 +787,8 @@ export async function handleSlashCommand(line, context) {
770
787
  });
771
788
  } catch (err) {
772
789
  step(formatActivityError('agents', verb, err));
773
- return { output: err instanceof Error ? err.message : String(err) };
790
+ step(`Agents: docker output — ${rawFailureText(err)}`);
791
+ return localizedOperationFailure({ operation: verb, target: 'agents', error: err });
774
792
  }
775
793
  };
776
794
 
@@ -915,9 +933,9 @@ export async function handleSlashCommand(line, context) {
915
933
  const output = await listServices(context.session);
916
934
  return rawCommandResult('/services', output);
917
935
  } catch (err) {
918
- const message = err instanceof Error ? err.message : String(err);
919
936
  step(formatActivityError('services', 'list', err));
920
- return { output: message };
937
+ step(`Services: docker output — ${rawFailureText(err)}`);
938
+ return localizedOperationFailure({ operation: 'list', target: 'workspace-services', error: err });
921
939
  }
922
940
  }
923
941
  case 'start': {
@@ -939,9 +957,9 @@ export async function handleSlashCommand(line, context) {
939
957
  images: missingImages,
940
958
  });
941
959
  } catch (err) {
942
- const message = err instanceof Error ? err.message : String(err);
943
- step(formatActivityError('services', 'stop', err));
944
- return { output: message };
960
+ step(formatActivityError('services', 'start', err));
961
+ step(`Services: docker output — ${rawFailureText(err)}`);
962
+ return localizedOperationFailure({ operation: 'start', target: service || 'workspace-services', error: err });
945
963
  }
946
964
  }
947
965
  case 'stop': {
@@ -957,21 +975,25 @@ export async function handleSlashCommand(line, context) {
957
975
  target: service || 'workspace-services',
958
976
  });
959
977
  } catch (err) {
960
- const message = err instanceof Error ? err.message : String(err);
961
- step(formatActivityError('services', 'logs', err));
962
- return { output: message };
978
+ step(formatActivityError('services', 'stop', err));
979
+ step(`Services: docker output — ${rawFailureText(err)}`);
980
+ return localizedOperationFailure({ operation: 'stop', target: service || 'workspace-services', error: err });
963
981
  }
964
982
  }
965
983
  case 'logs': {
966
984
  const service = args[1];
985
+ // A usage mistake is not a runtime failure: keep it a plain instruction
986
+ // rather than sending it to Donna to be rephrased.
987
+ if (!service) return { output: `Usage: /logs <service> [tail] — services: ${serviceNames().join(', ')}` };
967
988
  const tail = args[2] ? Number(args[2]) : 120;
968
989
  try {
969
990
  step(`Services: reading logs for ${service ?? 'service'}…`);
970
991
  const output = await serviceLogs(context.session, service, { tail });
971
992
  return rawCommandResult(`/logs ${[service, args[2]].filter(Boolean).join(' ')}`.trim(), output);
972
993
  } catch (err) {
973
- const message = err instanceof Error ? err.message : String(err);
974
- return { output: message };
994
+ step(formatActivityError('services', 'logs', err));
995
+ step(`Services: docker output — ${rawFailureText(err)}`);
996
+ return localizedOperationFailure({ operation: 'logs', target: service || 'workspace-services', error: err });
975
997
  }
976
998
  }
977
999
  case 'mcp': {
@@ -1070,13 +1092,10 @@ export async function handleSlashCommand(line, context) {
1070
1092
  if (payload?.ok !== true || typeof authorizationUrl !== 'string') {
1071
1093
  return connectorResult(`Google authorization could not start (${payload?.error ?? 'missing authorization URL'}).`);
1072
1094
  }
1073
- const opener = process.platform === 'darwin' ? 'open' : 'xdg-open';
1074
- try {
1075
- execFileSync(opener, [authorizationUrl], { stdio: 'ignore' });
1095
+ if (openExternalUrl(authorizationUrl)) {
1076
1096
  return connectorResult('Google authorization opened successfully in the user browser.');
1077
- } catch {
1078
- return connectorResult(`Google authorization requires the user to open this URL: ${authorizationUrl}`);
1079
1097
  }
1098
+ return connectorResult(`Google authorization requires the user to open this URL: ${authorizationUrl}`);
1080
1099
  } catch (err) {
1081
1100
  return connectorResult(`Google authorization could not start (${err instanceof Error ? err.message : String(err)}).`);
1082
1101
  }
@@ -1362,13 +1381,8 @@ export async function handleSlashCommand(line, context) {
1362
1381
  const port = context.session.workspaceEnv?.WIKI_SERVE_PORT ?? '3100';
1363
1382
  const url = `http://localhost:${port}`;
1364
1383
  const note = context.session.workspaceEnv ? '' : ' (no workspace loaded — using default port)';
1365
- const opener = process.platform === 'darwin' ? 'open' : 'xdg-open';
1366
- try {
1367
- execFileSync(opener, [url], { stdio: 'ignore' });
1368
- return { output: `Opening web UI: ${url}${note}` };
1369
- } catch {
1370
- return { output: `Web UI: ${url}${note}` };
1371
- }
1384
+ if (openExternalUrl(url)) return { output: `Opening web UI: ${url}${note}` };
1385
+ return { output: `Web UI: ${url}${note}` };
1372
1386
  }
1373
1387
  case 'clear': {
1374
1388
  const key = context.session.workspace || '__global__';
@@ -151,6 +151,26 @@ export function reduceAgentEvents(events = []) {
151
151
  return publicProjection(state);
152
152
  }
153
153
 
154
+ // Maps every projected conversation entry back to the sequence of the event
155
+ // that produced it. The conversation is derived from the event log, never
156
+ // stored, so "delete everything below this question" can only be expressed as
157
+ // "delete every event after the one that appended it". Replays the same
158
+ // applyEvent used by reduceAgentEvents, so the mapping cannot drift from the
159
+ // projection it describes.
160
+ export function conversationEventSequences(events = []) {
161
+ const state = createProjectionState();
162
+ const sequences = [];
163
+ for (const event of events) {
164
+ applyEvent(state, event);
165
+ const sequence = Number.isFinite(Number(event?.sequence)) ? Number(event.sequence) : null;
166
+ // Streaming deltas mutate the last entry in place instead of appending, so
167
+ // an entry keeps the sequence of the event that first created it.
168
+ while (sequences.length < state.conversation.length) sequences.push(sequence);
169
+ if (sequences.length > state.conversation.length) sequences.length = state.conversation.length;
170
+ }
171
+ return sequences;
172
+ }
173
+
154
174
  function createProjectionState() {
155
175
  return {
156
176
  conversation: [],