@dotdrelle/wiki-manager 0.14.10 → 0.14.13
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 +3 -1
- package/README.md +42 -2
- package/agents.docker-compose.yml +4 -5
- package/mcp.endpoints.example.json +2 -2
- package/package.json +2 -2
- package/src/agent/graph.js +8 -5
- package/src/agent/graph.test.js +4 -1
- package/src/cli/wiki-manager.js +23 -4
- package/src/core/activity.js +4 -0
- package/src/core/activity.test.js +31 -0
- package/src/core/agentEvents.js +11 -2
- package/src/core/buildInfo.json +2 -2
- package/src/core/dockerCompose.test.js +3 -0
- package/src/core/env.js +43 -7
- package/src/core/env.test.js +72 -0
- package/src/core/mcp.js +1 -1
- package/src/core/startupCheck.js +340 -7
- package/src/core/startupCheck.test.js +190 -3
- package/src/core/wikiSetup.js +8 -1
- package/src/core/wikiWorkspace.test.js +9 -0
- package/src/runtime/lifecycle.js +1 -1
- package/src/runtime/runner.js +19 -2
- package/src/runtime/runner.test.js +32 -1
- package/src/shell/SetupWizard.tsx +25 -1
- package/src/shell/StartupScreen.tsx +42 -9
- package/src/shell/repl.js +34 -6
- package/src/shell/repl.test.js +77 -0
- package/src/shell/tui.tsx +60 -3
- package/wiki-workspace +28 -20
package/.env.example
CHANGED
|
@@ -27,6 +27,8 @@ WORKSPACES_ROOT=/path/to/workspaces
|
|
|
27
27
|
# HTTP_PROXY=http://proxy.example:11011
|
|
28
28
|
# Always bypass the proxy for the local runtime, local MCPs, and Docker host.
|
|
29
29
|
# NO_PROXY=localhost,127.0.0.1,host.docker.internal
|
|
30
|
+
# Optional organization-approved HTTPS endpoint for the ShellUI startup probe.
|
|
31
|
+
# WIKI_MANAGER_CONNECTIVITY_URL=https://registry.npmjs.org/-/ping
|
|
30
32
|
|
|
31
33
|
# ── Agent auth tokens ──────────────────────────────────────────────────────────
|
|
32
34
|
#
|
|
@@ -51,7 +53,7 @@ DOCUMENTS_MCP_AUTH_TOKEN=
|
|
|
51
53
|
# All image OCR is delegated to an OpenAI-compatible vision LLM. The default
|
|
52
54
|
# Default target is the Albert API (etalab); set DOCUMENT_LLM_API_KEY.
|
|
53
55
|
# DOCUMENT_LLM_BASE_URL=https://albert.api.etalab.gouv.fr/v1
|
|
54
|
-
# DOCUMENT_LLM_MODEL=LightOnOCR-2-1B
|
|
56
|
+
# DOCUMENT_LLM_MODEL=lightonai/LightOnOCR-2-1B
|
|
55
57
|
# DOCUMENT_LLM_API_KEY=
|
|
56
58
|
# DOCUMENT_LLM_TIMEOUT_SECONDS=120
|
|
57
59
|
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ endpoints, and provides the `donna` shell: an agent-first terminal UI that can
|
|
|
8
8
|
inspect workspaces, run safe manager commands, call MCP tools, guide production
|
|
9
9
|
jobs, and run one-shot headless tasks.
|
|
10
10
|
|
|
11
|
-
Current coordinated release: **0.14.
|
|
11
|
+
Current coordinated release: **0.14.11**. Managed `llm-wiki` services expose
|
|
12
12
|
the Wiki Graph v2 browser and APIs; rebuild the `llm-wiki` image when deploying
|
|
13
13
|
this release through Docker.
|
|
14
14
|
|
|
@@ -86,6 +86,27 @@ npm install --global --allow-scripts=bun @dotdrelle/wiki-manager
|
|
|
86
86
|
In both modes, launch the commands from the directory that should hold the
|
|
87
87
|
manager state (`workspaces/`, `.env`, and `mcp.endpoints.json`).
|
|
88
88
|
|
|
89
|
+
At interactive ShellUI startup, the manager checks Docker availability first,
|
|
90
|
+
then outbound HTTPS connectivity, followed by agent and workspace
|
|
91
|
+
configuration, workspace containers, MCP handshakes (`tools/list`), and the
|
|
92
|
+
Donna runtime. The connectivity probe uses the configured HTTP(S) proxy and
|
|
93
|
+
custom CA settings. MCP diagnostics distinguish configuration, authentication,
|
|
94
|
+
reachability, and protocol failures. Remote MCPs are skipped when Internet is
|
|
95
|
+
offline, while local endpoints are still checked; MCPs are also checked when
|
|
96
|
+
Docker is unavailable because they may run elsewhere.
|
|
97
|
+
|
|
98
|
+
The startup screen reports **Ready**, **Degraded**, or **Setup required** and
|
|
99
|
+
keeps the shell usable in degraded mode. It offers **Retry pending checks**,
|
|
100
|
+
**Start services**, and **Open diagnostics** (service logs plus detailed MCP
|
|
101
|
+
status). Set
|
|
102
|
+
`WIKI_MANAGER_CONNECTIVITY_URL` only when the default npm-registry ping endpoint
|
|
103
|
+
must be replaced by an organization-approved HTTPS endpoint.
|
|
104
|
+
|
|
105
|
+
After a workspace is loaded, ShellUI displays `/status` automatically as the
|
|
106
|
+
first operational view. When **Open workspace** also starts services, the status
|
|
107
|
+
snapshot is produced afterwards so it reflects their current container and MCP
|
|
108
|
+
state.
|
|
109
|
+
|
|
89
110
|
## Functional overview
|
|
90
111
|
|
|
91
112
|
The diagram below shows the whole picture at a glance: how **inputs** (external
|
|
@@ -664,7 +685,18 @@ Original files are stored under
|
|
|
664
685
|
the documents agent to `<workspace>/raw/untracked/`. If the documents agent is
|
|
665
686
|
down, the upload remains stored and can be converted later.
|
|
666
687
|
Image files, scanned PDFs, and images detected inside PDF or Office documents
|
|
667
|
-
are sent through LLM OCR automatically.
|
|
688
|
+
are sent through LLM OCR automatically. The bundled agent stack defaults to
|
|
689
|
+
the Albert-compatible endpoint and
|
|
690
|
+
[`lightonai/LightOnOCR-2-1B`](https://huggingface.co/lightonai/LightOnOCR-2-1B);
|
|
691
|
+
configure it with `DOCUMENT_LLM_BASE_URL`, `DOCUMENT_LLM_MODEL`, and the
|
|
692
|
+
dedicated `DOCUMENT_LLM_API_KEY` (an ambient `OPENAI_API_KEY` is not used).
|
|
693
|
+
|
|
694
|
+
In the served web Chat, a successful conversion also adds the new
|
|
695
|
+
`raw/untracked/*.md` path as a document-context badge. Up to five wiki or
|
|
696
|
+
pending Markdown documents can be selected. Only their paths are sent to
|
|
697
|
+
Donna; she reads the relevant documents through the configured read-only MCP
|
|
698
|
+
tools when the question refers to them. This does not ingest the document:
|
|
699
|
+
`wiki ingest` remains the explicit transition into the durable wiki.
|
|
668
700
|
|
|
669
701
|
## The `donna` Shell
|
|
670
702
|
|
|
@@ -701,6 +733,14 @@ The TUI uses a two-pane layout:
|
|
|
701
733
|
`Ctrl+Q` toggles the tabs; clicking `Plan` or `Queue (N)` selects that tab
|
|
702
734
|
directly. MCP connection details remain available through `/mcp status`.
|
|
703
735
|
|
|
736
|
+
In the served browser Activity panel, `Clear` is a per-tab display cleanup and
|
|
737
|
+
`Clear all` applies it to Plan, Local activity, Runtime activity, and Logs. It
|
|
738
|
+
does not delete a plan. **Reset plan** is the confirmed destructive operation:
|
|
739
|
+
it stops active work and purges the workspace runtime plan, activities, logs,
|
|
740
|
+
queue, and persisted runtime state. Donna can perform the same operation when
|
|
741
|
+
the user explicitly asks to delete, reset, abandon, or replace the current
|
|
742
|
+
plan. A request that only says to stop or cancel remains non-purging.
|
|
743
|
+
|
|
704
744
|
Useful primitives:
|
|
705
745
|
|
|
706
746
|
```text
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
# CME_MCP_AUTH_TOKEN — bearer token for cme agent (empty = no auth)
|
|
14
14
|
# DOCUMENTS_MCP_AUTH_TOKEN — bearer token for documents agent (empty = no auth)
|
|
15
15
|
# DOCUMENT_LLM_BASE_URL — https://albert.api.etalab.gouv.fr/v1
|
|
16
|
-
# DOCUMENT_LLM_MODEL — LightOnOCR-2-1B
|
|
16
|
+
# DOCUMENT_LLM_MODEL — lightonai/LightOnOCR-2-1B
|
|
17
17
|
# DOCUMENT_LLM_API_KEY — OpenAI/OpenAI-compatible API key for document OCR
|
|
18
18
|
# DOCUMENT_LLM_TIMEOUT_SECONDS — 120
|
|
19
19
|
#
|
|
@@ -63,9 +63,9 @@ services:
|
|
|
63
63
|
- DOCUMENT_OUTPUT_DIR=/documents/output
|
|
64
64
|
- WORKSPACES_ROOT=/workspaces
|
|
65
65
|
- DOCUMENT_MAX_UPLOAD_BYTES=${DOCUMENT_MAX_UPLOAD_BYTES:-52428800}
|
|
66
|
-
- DOCUMENT_LLM_BASE_URL=${DOCUMENT_LLM_BASE_URL:-https://api.
|
|
67
|
-
- DOCUMENT_LLM_MODEL=${DOCUMENT_LLM_MODEL:-
|
|
68
|
-
- DOCUMENT_LLM_API_KEY=${DOCUMENT_LLM_API_KEY
|
|
66
|
+
- DOCUMENT_LLM_BASE_URL=${DOCUMENT_LLM_BASE_URL:-https://albert.api.etalab.gouv.fr/v1}
|
|
67
|
+
- DOCUMENT_LLM_MODEL=${DOCUMENT_LLM_MODEL:-lightonai/LightOnOCR-2-1B}
|
|
68
|
+
- DOCUMENT_LLM_API_KEY=${DOCUMENT_LLM_API_KEY:-}
|
|
69
69
|
- DOCUMENT_LLM_TIMEOUT_SECONDS=${DOCUMENT_LLM_TIMEOUT_SECONDS:-120}
|
|
70
70
|
# HTTPS — uncomment and place cert files in AGENTS_DATA_DIR/certs/
|
|
71
71
|
#- MCP_SSL_CERTFILE=/certs/server.crt
|
|
@@ -76,4 +76,3 @@ services:
|
|
|
76
76
|
- ${WORKSPACES_ROOT:?Set WORKSPACES_ROOT to the directory containing all workspace folders}:/workspaces
|
|
77
77
|
#- ${AGENTS_DATA_DIR:-./.agents-data}/certs:/certs:ro
|
|
78
78
|
restart: unless-stopped
|
|
79
|
-
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
}
|
|
28
28
|
},
|
|
29
29
|
"chatAccess": {
|
|
30
|
-
"maxToolIterations":
|
|
30
|
+
"maxToolIterations": 8,
|
|
31
31
|
"servers": {
|
|
32
|
-
"wiki": { "allow": ["help_list", "help_read"] },
|
|
32
|
+
"wiki": { "allow": ["help_list", "help_read", "wiki_workspace_status", "wiki_list_pages", "wiki_read_page", "wiki_read_pages", "wiki_search_context", "wiki_collect_context", "wiki_read_ingested_source"] },
|
|
33
33
|
"production": { "allow": ["production_job_status", "production_jobs_list"] },
|
|
34
34
|
"cme": { "allow": ["cme_status", "cme_sources_list", "cme_export_status"] }
|
|
35
35
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotdrelle/wiki-manager",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.13",
|
|
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/agent/graph.test.js src/contracts/schemas.test.js src/core/activity.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/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/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/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",
|
|
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",
|
package/src/agent/graph.js
CHANGED
|
@@ -120,7 +120,10 @@ const RUNTIME_KILL_TOOL = {
|
|
|
120
120
|
function: {
|
|
121
121
|
name: 'runtime__kill',
|
|
122
122
|
description: 'Hard-stop the workspace runtime: abort the active run, cancel its agent jobs, mark persisted runs interrupted and purge the control queue. Use when the user asks to remove/kill/clean the current run, its jobs or the queue.',
|
|
123
|
-
parameters: { type: 'object', additionalProperties: false, properties: {
|
|
123
|
+
parameters: { type: 'object', additionalProperties: false, properties: {
|
|
124
|
+
runId: { type: 'string', description: 'Optional specific run id; omit to kill everything active in the workspace.' },
|
|
125
|
+
purge: { type: 'boolean', description: 'Set true only when the user explicitly asks to delete/reset/replace the current plan and runtime history.' },
|
|
126
|
+
} },
|
|
124
127
|
},
|
|
125
128
|
};
|
|
126
129
|
|
|
@@ -694,8 +697,8 @@ async function handleRuntimeControlTool(session, tool, args = {}) {
|
|
|
694
697
|
const workspace = session.workspace ?? null;
|
|
695
698
|
try {
|
|
696
699
|
if (tool === 'kill') {
|
|
697
|
-
const result = await postRuntimeKill({ url, workspace, runId: args.runId ?? null });
|
|
698
|
-
return `Runtime killed: ${result.runs ?? 0} run(s) interrupted, ${result.tasks ?? 0} task(s) cancelled, ${result.queued ?? 0} queued control request(s) purged.`;
|
|
700
|
+
const result = await postRuntimeKill({ url, workspace, runId: args.runId ?? null, purge: args.purge === true });
|
|
701
|
+
return `Runtime killed: ${result.runs ?? 0} run(s) interrupted, ${result.tasks ?? 0} task(s) cancelled, ${result.queued ?? 0} queued control request(s) purged${result.purged ? `; runtime state reset (${result.purged.events ?? 0} event(s))` : ''}.`;
|
|
699
702
|
}
|
|
700
703
|
if (tool === 'cancel') {
|
|
701
704
|
const result = await postRuntimeCancel({ url, workspace });
|
|
@@ -942,7 +945,7 @@ export function buildAgentSystemPrompt(state) {
|
|
|
942
945
|
workspaceProfile
|
|
943
946
|
? `Workspace profile (.wiki/profile.md) — durable user preferences, apply these to every reply (tone, tutoiement/vouvoiement, formatting, etc.):\n${workspaceProfile}`
|
|
944
947
|
: null,
|
|
945
|
-
'Runtime control: you have runtime__status, runtime__cancel, runtime__kill, runtime__approve and runtime__enqueue. When the user asks to stop, remove, clean or kill the current run, its jobs or the queue ("supprime le job et la queue", "arr\u00eate tout"), call runtime__kill (or runtime__cancel for a soft stop of just the run) and confirm what was stopped. For questions about what is running or queued, call runtime__status and answer from its data. When the user consents to a pending approval in any phrasing ("vas-y", "ok pour l\'export"), call runtime__approve. When the user asks for a NEW action while a run is active, do not execute it: propose runtime__enqueue (run it after) or, if they insist it replaces the current work, runtime__kill then the new action.',
|
|
948
|
+
'Runtime control: you have runtime__status, runtime__cancel, runtime__kill, runtime__approve and runtime__enqueue. When the user asks to stop, remove, clean or kill the current run, its jobs or the queue ("supprime le job et la queue", "arr\u00eate tout"), call runtime__kill (or runtime__cancel for a soft stop of just the run) and confirm what was stopped. When the user explicitly asks to delete, reset, abandon or replace the current plan, call runtime__kill with purge=true; never set purge=true for a simple stop. For questions about what is running or queued, call runtime__status and answer from its data. When the user consents to a pending approval in any phrasing ("vas-y", "ok pour l\'export"), call runtime__approve. When the user asks for a NEW action while a run is active, do not execute it: propose runtime__enqueue (run it after) or, if they insist it replaces the current work, runtime__kill then the new action.',
|
|
946
949
|
'Report every runtime control outcome exactly as the tool returned it \u2014 never embellish. If runtime__kill reports 0 run(s)/0 task(s)/0 purged, say there was nothing active to stop or purge; do NOT claim a run, plan, pending approval or queue item was removed. If runtime__status returns an error or could not be read, say the runtime state could not be retrieved and do not describe a state you never obtained. Never assert that something was cleaned, cancelled, approved or purged unless that specific tool result confirms it.',
|
|
947
950
|
'Durable profile updates are actions in this stabilized version: delegate them instead of writing directly.',
|
|
948
951
|
].filter(Boolean).join('\n');
|
|
@@ -1005,7 +1008,7 @@ function toolsForClassification(classification, writeTools, session = null) {
|
|
|
1005
1008
|
return [SHELL_READ_COMMAND_TOOL, ...controlTools, ...capabilityRunTools, ...writeTools];
|
|
1006
1009
|
}
|
|
1007
1010
|
|
|
1008
|
-
const DONNA_READ_VERBS = new Set(['status', 'list', 'search', 'read', 'get', 'fetch']);
|
|
1011
|
+
const DONNA_READ_VERBS = new Set(['status', 'list', 'search', 'read', 'get', 'fetch', 'collect']);
|
|
1009
1012
|
|
|
1010
1013
|
export function isDonnaReadTool(item) {
|
|
1011
1014
|
const name = String(item?.function?.name ?? '');
|
package/src/agent/graph.test.js
CHANGED
|
@@ -1349,11 +1349,14 @@ test('Donna interprets a cleanup request and calls runtime__kill herself', async
|
|
|
1349
1349
|
runtime: { url: 'http://runtime.test' },
|
|
1350
1350
|
agentProjection: { status: 'running', activities: [], conversation: [] },
|
|
1351
1351
|
llm: {
|
|
1352
|
-
async completeWithTools({ tools }) {
|
|
1352
|
+
async completeWithTools({ tools, system }) {
|
|
1353
1353
|
calls += 1;
|
|
1354
1354
|
if (calls === 1) {
|
|
1355
1355
|
const names = tools.map((tool) => tool.function.name);
|
|
1356
1356
|
assert.ok(names.includes('runtime__kill'), 'control tools must be bound during an active run');
|
|
1357
|
+
const killTool = tools.find((tool) => tool.function.name === 'runtime__kill');
|
|
1358
|
+
assert.equal(killTool.function.parameters.properties.purge.type, 'boolean');
|
|
1359
|
+
assert.match(system, /delete, reset, abandon or replace the current plan/);
|
|
1357
1360
|
return {
|
|
1358
1361
|
content: null,
|
|
1359
1362
|
message: { role: 'assistant', content: null },
|
package/src/cli/wiki-manager.js
CHANGED
|
@@ -7,7 +7,7 @@ loadManagerEnv();
|
|
|
7
7
|
import { createAgentGraph } from '../agent/graph.js';
|
|
8
8
|
import { handleSlashCommand, printHelp, printVersion, refreshMcpRuntimeStatus } from '../commands/slash.js';
|
|
9
9
|
import { runShell, runHeadlessChatTurn } from '../shell/repl.js';
|
|
10
|
-
import {
|
|
10
|
+
import { runPreflightChecks, withRuntimePreflight } from '../core/startupCheck.js';
|
|
11
11
|
import { applySessionWikircProfile } from '../core/sessionConfig.js';
|
|
12
12
|
import { listWikircProfiles } from '../core/wikirc.js';
|
|
13
13
|
import { callMcpTool, formatMcpToolResult, readChatAccessConfig } from '../core/mcp.js';
|
|
@@ -1085,6 +1085,9 @@ async function runRuntime(argv, agent) {
|
|
|
1085
1085
|
response = await runHeadlessChatTurn(ephemeral, input, {
|
|
1086
1086
|
history,
|
|
1087
1087
|
onStep: ephemeral._onStep,
|
|
1088
|
+
// UI context from `wiki serve`: up to five selected wiki or raw
|
|
1089
|
+
// documents. Only paths are prompted; Donna reads through tools.
|
|
1090
|
+
openWikiPages: body.context?.openWikiPages ?? body.context?.openWikiPage,
|
|
1088
1091
|
});
|
|
1089
1092
|
} else {
|
|
1090
1093
|
response = await runAgentTurn(agent, ephemeral, input, { messages, signal });
|
|
@@ -1222,8 +1225,23 @@ export async function runCli(argv) {
|
|
|
1222
1225
|
// in a random cwd must not litter files.
|
|
1223
1226
|
const scaffolded = ensureManagerScaffold({ log: (message) => console.log(`[wiki-manager] ${message}`) });
|
|
1224
1227
|
if (scaffolded.length > 0) loadManagerEnv();
|
|
1225
|
-
const
|
|
1226
|
-
|
|
1228
|
+
const reportCheck = ({ kind, ok, detail, context, skipped, pending }) => {
|
|
1229
|
+
const labels = { docker: 'Docker', internet: 'Internet', agents: 'Agent containers', workspace: 'Workspaces', containers: 'Workspace containers', mcp: 'MCP' };
|
|
1230
|
+
const label = labels[kind] ?? kind;
|
|
1231
|
+
const instruction = !ok && context?.command ? ` — command: ${context.command}` : '';
|
|
1232
|
+
const suffix = detail ? ` — ${detail}` : ` — ${context?.error ?? context?.dockerError ?? 'waiting'}`;
|
|
1233
|
+
const color = ok ? '\x1b[32m' : '\x1b[33m';
|
|
1234
|
+
const state = ok ? 'ready' : pending || skipped ? 'waiting' : 'needs attention';
|
|
1235
|
+
const icon = ok ? '✓' : pending || skipped ? '◐' : '✗';
|
|
1236
|
+
console.log(`${color}${icon} configuration: ${label} ${state}${suffix}${instruction}\x1b[0m`);
|
|
1237
|
+
};
|
|
1238
|
+
let preflight = await runPreflightChecks({ onCheck: reportCheck });
|
|
1239
|
+
if (preflight.gaps.length > 0) {
|
|
1240
|
+
await runStartupWizard(preflight.gaps);
|
|
1241
|
+
// The wizard may have created a workspace, started agents or repaired
|
|
1242
|
+
// configuration. Re-read everything before drawing the home screen.
|
|
1243
|
+
preflight = await runPreflightChecks();
|
|
1244
|
+
}
|
|
1227
1245
|
let runtime = null;
|
|
1228
1246
|
try {
|
|
1229
1247
|
const { ensureRuntime } = await import('../runtime/lifecycle.js');
|
|
@@ -1234,11 +1252,12 @@ export async function runCli(argv) {
|
|
|
1234
1252
|
runtime = unavailableRuntime(err);
|
|
1235
1253
|
console.error(`Runtime unavailable: ${runtime.error}`);
|
|
1236
1254
|
}
|
|
1255
|
+
preflight = withRuntimePreflight(preflight, runtime);
|
|
1237
1256
|
// The owned-runtime shutdown happens inside the TUI's own exit paths
|
|
1238
1257
|
// (see tui.tsx onShellExit): render() resolves at MOUNT, so anything
|
|
1239
1258
|
// after this await would run while the shell is still on screen —
|
|
1240
1259
|
// 0.12.9 shipped exactly that bug and killed the runtime under the user.
|
|
1241
|
-
await runOpenTuiShell({ agent, packageJson, runtime });
|
|
1260
|
+
await runOpenTuiShell({ agent, packageJson, runtime, preflight });
|
|
1242
1261
|
return;
|
|
1243
1262
|
}
|
|
1244
1263
|
|
package/src/core/activity.js
CHANGED
|
@@ -31,6 +31,9 @@ function normalizePlanSteps(steps) {
|
|
|
31
31
|
executor: s?.executor ?? null,
|
|
32
32
|
executorQuery: s?.executorQuery ?? null,
|
|
33
33
|
outputRefs: Array.isArray(s?.outputRefs) ? s.outputRefs.map(String) : [],
|
|
34
|
+
...(s?.status != null ? { status: String(s.status) } : {}),
|
|
35
|
+
...(s?.startedAt != null ? { startedAt: s.startedAt } : {}),
|
|
36
|
+
...(s?.finishedAt != null ? { finishedAt: s.finishedAt } : {}),
|
|
34
37
|
}));
|
|
35
38
|
}
|
|
36
39
|
|
|
@@ -149,6 +152,7 @@ function productionActivityFromPayload(payload, context = {}) {
|
|
|
149
152
|
step,
|
|
150
153
|
...(payload?.taskId ? { stepId: String(payload.taskId) } : {}),
|
|
151
154
|
},
|
|
155
|
+
plan: Array.isArray(progress?.steps) ? { steps: progress.steps } : null,
|
|
152
156
|
poll: jobId ? {
|
|
153
157
|
server: 'production',
|
|
154
158
|
tool: context.tool === 'agent_status' ? 'agent_status' : 'production_job_status',
|
|
@@ -109,6 +109,37 @@ test('extractActivity: documents conversion activity carries plan and percent',
|
|
|
109
109
|
assert.deepEqual(activity.plan.steps.map((step) => step.id), ['resolve', 'convert', 'write']);
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
+
test('extractActivity: agent_status carries structured processing progress', () => {
|
|
113
|
+
const activity = extractActivity({
|
|
114
|
+
jobId: 'job-build',
|
|
115
|
+
taskId: 'build-report',
|
|
116
|
+
operation: 'build',
|
|
117
|
+
status: 'running',
|
|
118
|
+
progress: {
|
|
119
|
+
percent: 47,
|
|
120
|
+
phase: 'build',
|
|
121
|
+
detail: 'Batch 2/4 · LLM throttled',
|
|
122
|
+
stepIndex: 1,
|
|
123
|
+
stepTotal: 2,
|
|
124
|
+
steps: [
|
|
125
|
+
{ id: 'build', name: 'build', status: 'running' },
|
|
126
|
+
{ id: 'export', name: 'export', status: 'pending' },
|
|
127
|
+
],
|
|
128
|
+
batch: { index: 2, total: 4, status: 'running' },
|
|
129
|
+
throttling: { active: true, waitMs: 1200, retryAt: '2026-07-21T10:00:01Z' },
|
|
130
|
+
processing: { instructionCount: 8 },
|
|
131
|
+
},
|
|
132
|
+
}, { server: 'production', tool: 'agent_status' });
|
|
133
|
+
|
|
134
|
+
assert.deepEqual(activity.plan.steps.map((step) => [step.id, step.status]), [
|
|
135
|
+
['build', 'running'],
|
|
136
|
+
['export', 'pending'],
|
|
137
|
+
]);
|
|
138
|
+
assert.equal(activity.progress.batch.index, 2);
|
|
139
|
+
assert.equal(activity.progress.throttling.active, true);
|
|
140
|
+
assert.equal(activity.progress.processing.instructionCount, 8);
|
|
141
|
+
});
|
|
142
|
+
|
|
112
143
|
test('rememberActivity: returns normalized activity on success', () => {
|
|
113
144
|
const session = {};
|
|
114
145
|
const result = rememberActivity(session, { id: '1', status: 'running', source: 'x', kind: 'job' });
|
package/src/core/agentEvents.js
CHANGED
|
@@ -122,13 +122,22 @@ export function resetSessionProjection(session) {
|
|
|
122
122
|
|
|
123
123
|
function withSessionRunIdentity(event, session) {
|
|
124
124
|
const identity = session?._currentRunIdentity;
|
|
125
|
-
|
|
125
|
+
// The workspace must be attached even OUTSIDE a run identity: the SSE
|
|
126
|
+
// publisher drops events whose workspace does not match the (always
|
|
127
|
+
// workspace-scoped) serve UI client. Events dispatched between chained
|
|
128
|
+
// executions — e.g. the plan_set of the next action, run evaluation,
|
|
129
|
+
// assistant messages — used to leave with workspace=null and never reached
|
|
130
|
+
// the UI, which then displayed a stale plan from the previous action.
|
|
131
|
+
const workspace = event.workspace ?? identity?.workspace ?? session?.workspace ?? null;
|
|
132
|
+
if (!identity) {
|
|
133
|
+
return workspace === (event.workspace ?? null) ? event : { ...event, workspace };
|
|
134
|
+
}
|
|
126
135
|
return {
|
|
127
136
|
...event,
|
|
128
137
|
runId: event.runId ?? identity.runId ?? null,
|
|
129
138
|
turnId: event.turnId ?? identity.turnId ?? null,
|
|
130
139
|
taskId: event.taskId ?? identity.taskId ?? null,
|
|
131
|
-
workspace
|
|
140
|
+
workspace,
|
|
132
141
|
};
|
|
133
142
|
}
|
|
134
143
|
|
package/src/core/buildInfo.json
CHANGED
|
@@ -22,6 +22,9 @@ 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.documents.environment.includes('DOCUMENT_LLM_BASE_URL=${DOCUMENT_LLM_BASE_URL:-https://albert.api.etalab.gouv.fr/v1}'), true);
|
|
26
|
+
assert.equal(agentsCompose.services.documents.environment.includes('DOCUMENT_LLM_MODEL=${DOCUMENT_LLM_MODEL:-lightonai/LightOnOCR-2-1B}'), true);
|
|
27
|
+
assert.equal(agentsCompose.services.documents.environment.includes('DOCUMENT_LLM_API_KEY=${DOCUMENT_LLM_API_KEY:-}'), true);
|
|
25
28
|
// MailerSend was an external-connector experiment — removed from the
|
|
26
29
|
// default stack (the agent repo still exists for ad-hoc use).
|
|
27
30
|
assert.equal(agentsCompose.services.mailer, undefined);
|
package/src/core/env.js
CHANGED
|
@@ -51,9 +51,33 @@ export function ensureManagerScaffold({ log = () => {} } = {}) {
|
|
|
51
51
|
const created = [];
|
|
52
52
|
const endpointsFile = managerMcpEndpointsFile();
|
|
53
53
|
const endpointsExample = join(packageRoot, 'mcp.endpoints.example.json');
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
if (existsSync(endpointsExample)) {
|
|
55
|
+
if (!existsSync(endpointsFile)) {
|
|
56
|
+
copyFileSync(endpointsExample, endpointsFile);
|
|
57
|
+
created.push('mcp.endpoints.json');
|
|
58
|
+
} else {
|
|
59
|
+
// Additive migration: the scaffold only copies the example on first run,
|
|
60
|
+
// so installs that predate a new top-level key (e.g. chatAccess) never
|
|
61
|
+
// receive it and the feature stays silently disabled. Merge ONLY the
|
|
62
|
+
// top-level keys missing from the operator's file; existing keys —
|
|
63
|
+
// including a hand-edited chatAccess — are never touched. To disable a
|
|
64
|
+
// feature key permanently, set it to null instead of deleting it: null is
|
|
65
|
+
// "present", so the merge preserves it and readers treat it as absent.
|
|
66
|
+
try {
|
|
67
|
+
const current = JSON.parse(readFileSync(endpointsFile, 'utf8'));
|
|
68
|
+
const example = JSON.parse(readFileSync(endpointsExample, 'utf8'));
|
|
69
|
+
if (current && typeof current === 'object' && !Array.isArray(current)) {
|
|
70
|
+
const missing = Object.keys(example).filter((key) => !(key in current));
|
|
71
|
+
if (missing.length > 0) {
|
|
72
|
+
for (const key of missing) current[key] = example[key];
|
|
73
|
+
writeFileSync(endpointsFile, `${JSON.stringify(current, null, 2)}\n`);
|
|
74
|
+
created.push(`mcp.endpoints.json keys: ${missing.join(', ')}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
// Unreadable or invalid JSON: leave the operator's file strictly alone.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
57
81
|
}
|
|
58
82
|
const envFile = managerEnvFile();
|
|
59
83
|
const envExample = join(packageRoot, '.env.example');
|
|
@@ -74,14 +98,26 @@ export function ensureManagerScaffold({ log = () => {} } = {}) {
|
|
|
74
98
|
return created;
|
|
75
99
|
}
|
|
76
100
|
|
|
101
|
+
// Mirrors workspacesDir() in core/workspaces.js (which imports this module,
|
|
102
|
+
// so it cannot be imported here without a cycle).
|
|
103
|
+
function workspacesRootDir() {
|
|
104
|
+
return process.env.WIKI_WORKSPACES_DIR
|
|
105
|
+
? resolve(process.env.WIKI_WORKSPACES_DIR)
|
|
106
|
+
: join(userManagerDir(), 'workspaces');
|
|
107
|
+
}
|
|
108
|
+
|
|
77
109
|
// Single source of truth for where `.agents-data` lives, shared by the
|
|
78
|
-
// manager's own document intake
|
|
79
|
-
//
|
|
110
|
+
// manager's own document intake, the per-workspace serve stack, and the
|
|
111
|
+
// GLOBAL agents stack (documents/cme containers). All three must resolve the
|
|
112
|
+
// same host directory: serve writes an upload into its mount and the
|
|
113
|
+
// documents container must find it under the identical path — a divergence
|
|
114
|
+
// here is exactly the clean-install "Input file does not exist" bug.
|
|
115
|
+
// Canonical location: `<workspaces root>/.agents-data`, session or not.
|
|
80
116
|
export function resolveAgentsDataDir(session = null) {
|
|
81
117
|
const configured = process.env.AGENTS_DATA_DIR;
|
|
82
|
-
if (configured) return isAbsolute(configured) ? configured : resolve(
|
|
118
|
+
if (configured) return isAbsolute(configured) ? configured : resolve(managerStateDir(), configured);
|
|
83
119
|
if (session?.workspacePath) return resolve(dirname(session.workspacePath), '.agents-data');
|
|
84
|
-
return resolve(
|
|
120
|
+
return resolve(workspacesRootDir(), '.agents-data');
|
|
85
121
|
}
|
|
86
122
|
|
|
87
123
|
function parseEnvValue(value) {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { ensureManagerScaffold } from './env.js';
|
|
8
|
+
|
|
9
|
+
// ensureManagerScaffold resolves the manager state dir from
|
|
10
|
+
// WIKI_MANAGER_ENV_FILE — point it at a temp dir for each test.
|
|
11
|
+
function withTempManagerDir(fn) {
|
|
12
|
+
const dir = mkdtempSync(join(tmpdir(), 'wiki-manager-env-test-'));
|
|
13
|
+
const previous = process.env.WIKI_MANAGER_ENV_FILE;
|
|
14
|
+
process.env.WIKI_MANAGER_ENV_FILE = join(dir, '.env');
|
|
15
|
+
try {
|
|
16
|
+
return fn(dir);
|
|
17
|
+
} finally {
|
|
18
|
+
if (previous === undefined) delete process.env.WIKI_MANAGER_ENV_FILE;
|
|
19
|
+
else process.env.WIKI_MANAGER_ENV_FILE = previous;
|
|
20
|
+
rmSync(dir, { recursive: true, force: true });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test('scaffold copies the packaged examples into a fresh directory', () => {
|
|
25
|
+
withTempManagerDir((dir) => {
|
|
26
|
+
const created = ensureManagerScaffold();
|
|
27
|
+
assert.ok(created.includes('mcp.endpoints.json'));
|
|
28
|
+
assert.ok(created.includes('.env'));
|
|
29
|
+
const endpoints = JSON.parse(readFileSync(join(dir, 'mcp.endpoints.json'), 'utf8'));
|
|
30
|
+
assert.ok(endpoints.mcpServers);
|
|
31
|
+
assert.ok(endpoints.chatAccess);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('scaffold merges missing top-level keys into an existing endpoints file', () => {
|
|
36
|
+
withTempManagerDir((dir) => {
|
|
37
|
+
const endpointsFile = join(dir, 'mcp.endpoints.json');
|
|
38
|
+
// Pre-chatAccess install: only mcpServers, with operator edits.
|
|
39
|
+
writeFileSync(endpointsFile, JSON.stringify({
|
|
40
|
+
mcpServers: { custom: { url: 'http://localhost:9999/mcp/' } },
|
|
41
|
+
}, null, 2));
|
|
42
|
+
const created = ensureManagerScaffold();
|
|
43
|
+
assert.ok(created.some((item) => item.includes('chatAccess')));
|
|
44
|
+
const merged = JSON.parse(readFileSync(endpointsFile, 'utf8'));
|
|
45
|
+
// Operator's servers untouched; new key added from the example.
|
|
46
|
+
assert.deepEqual(merged.mcpServers, { custom: { url: 'http://localhost:9999/mcp/' } });
|
|
47
|
+
assert.ok(merged.chatAccess?.servers?.wiki);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('scaffold never overwrites an existing chatAccess, including explicit null', () => {
|
|
52
|
+
withTempManagerDir((dir) => {
|
|
53
|
+
const endpointsFile = join(dir, 'mcp.endpoints.json');
|
|
54
|
+
writeFileSync(endpointsFile, JSON.stringify({
|
|
55
|
+
mcpServers: {},
|
|
56
|
+
chatAccess: null,
|
|
57
|
+
}, null, 2));
|
|
58
|
+
ensureManagerScaffold();
|
|
59
|
+
const after = JSON.parse(readFileSync(endpointsFile, 'utf8'));
|
|
60
|
+
// null means "deliberately disabled" — the merge must preserve it.
|
|
61
|
+
assert.equal(after.chatAccess, null);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('scaffold leaves an invalid endpoints file strictly alone', () => {
|
|
66
|
+
withTempManagerDir((dir) => {
|
|
67
|
+
const endpointsFile = join(dir, 'mcp.endpoints.json');
|
|
68
|
+
writeFileSync(endpointsFile, '{ not json');
|
|
69
|
+
ensureManagerScaffold();
|
|
70
|
+
assert.equal(readFileSync(endpointsFile, 'utf8'), '{ not json');
|
|
71
|
+
});
|
|
72
|
+
});
|
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.14.
|
|
4
|
+
const WIKI_MANAGER_VERSION = '0.14.13';
|
|
5
5
|
|
|
6
6
|
function envValue(key) {
|
|
7
7
|
const filePath = managerEnvFile();
|