@dotdrelle/wiki-manager 0.15.34 → 0.15.38
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 +8 -5
- package/README.md +42 -0
- package/agents.docker-compose.yml +11 -16
- package/docker-compose.yml +9 -8
- package/package.json +2 -2
- package/src/agent/graph.js +20 -0
- package/src/cli/wiki-manager.js +20 -0
- package/src/commands/slash.js +85 -13
- package/src/commands/slash.test.js +85 -3
- package/src/core/agentsCompose.js +86 -1
- package/src/core/buildInfo.json +2 -2
- package/src/core/compose.js +24 -2
- package/src/core/dockerCompose.test.js +18 -0
- package/src/core/env.js +2 -1
- package/src/core/env.test.js +14 -0
- package/src/core/googleGrants.js +38 -0
- package/src/core/googleGrants.test.js +59 -0
- package/src/core/mcp.js +1 -1
- package/src/core/mcp.test.js +1 -0
- package/src/core/mcpEndpoints.js +96 -0
- package/src/core/mcpEndpoints.test.js +126 -0
- package/src/core/modelFetch.js +241 -27
- package/src/core/modelFetch.test.js +213 -0
- package/src/core/profileServiceStatus.test.js +146 -0
- package/src/core/wikiSetup.js +11 -2
- package/src/core/wikiWorkspace.test.js +16 -3
- package/src/orchestrator/agentRegistry.js +13 -1
- package/src/orchestrator/agentRegistry.test.js +20 -0
- package/src/runtime/lifecycle.js +105 -3
- package/src/runtime/lifecycle.test.js +68 -0
- package/src/runtime/server.js +28 -0
- package/src/shell/LeftPane.tsx +55 -41
- package/src/shell/SetupWizard.tsx +416 -116
- package/src/shell/repl.js +61 -12
- package/src/shell/repl.test.js +48 -19
- package/src/shell/setupWizardDiscovery.test.js +48 -0
- package/src/shell/setupWizardPlaceholders.test.js +15 -2
- package/src/shell/setupWizardSuggestions.test.js +36 -2
- package/src/shell/wrapText.js +57 -0
- package/src/shell/wrapText.test.js +48 -0
- package/wiki-workspace +122 -32
package/.env.example
CHANGED
|
@@ -57,12 +57,15 @@ CONNECTORS_MCP_AUTH_TOKEN=
|
|
|
57
57
|
CONNECTORS_ENABLED=false
|
|
58
58
|
#
|
|
59
59
|
# The wikiLLM Google OAuth application is baked into the agent-connectors image
|
|
60
|
-
# at build time, from agent-external/agent-connectors/.env.build.local.
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
60
|
+
# at build time, from agent-external/agent-connectors/.env.build.local. Neither
|
|
61
|
+
# value is a security boundary (public Desktop-type client); baking them is what
|
|
62
|
+
# makes the connector work out of the box.
|
|
63
|
+
#
|
|
64
|
+
# Set either key below to use YOUR OWN Google application instead — it overrides
|
|
65
|
+
# the baked default at runtime, no rebuild needed. LEAVE THEM COMMENTED to keep
|
|
66
|
+
# the baked one: an empty value here is forwarded as an empty value and disables
|
|
67
|
+
# the client entirely.
|
|
64
68
|
# GOOGLE_OAUTH_CLIENT_ID=
|
|
65
|
-
# Optional confidential-client compatibility override:
|
|
66
69
|
# GOOGLE_OAUTH_CLIENT_SECRET=
|
|
67
70
|
#
|
|
68
71
|
# The callback URL is generated automatically
|
package/README.md
CHANGED
|
@@ -556,6 +556,48 @@ including under `"*"`. Multi-step work belongs to `/agent`.
|
|
|
556
556
|
An `allowActions` key written by an older manager is folded into `allow` on
|
|
557
557
|
read and removed on the next `agents up`.
|
|
558
558
|
|
|
559
|
+
### Adding a connector from the served chat UI
|
|
560
|
+
|
|
561
|
+
`mcp.endpoints.json` stays hand-editable, but the Connectors panel of
|
|
562
|
+
`llm-wiki serve` can now write it. Connecting a card there upserts the endpoint
|
|
563
|
+
through the runtime (`POST /mcp/endpoints`), and the runtime immediately
|
|
564
|
+
re-reads the file and rediscovers tools and agents — no restart, and the new
|
|
565
|
+
tools are usable in the same breath by `/chat`, `/agent` and any subsequent
|
|
566
|
+
plan.
|
|
567
|
+
|
|
568
|
+
Because a server absent from `chatAccess` gets **zero** tools in `/chat`, the
|
|
569
|
+
upsert writes `"allow": "*"` for it. That is the deliberate difference between
|
|
570
|
+
a connector declared by hand — where you choose the tool list — and one added
|
|
571
|
+
from the UI, where the person adding it is the person who will use it. Narrow
|
|
572
|
+
it afterwards by editing the file.
|
|
573
|
+
|
|
574
|
+
Three origins are distinguished, and the UI labels each card:
|
|
575
|
+
|
|
576
|
+
| origin | shown as | who owns it |
|
|
577
|
+
| --- | --- | --- |
|
|
578
|
+
| `wiki`, `production`, `llm-wiki`, `wiki-production` | `internal` | the workspace stack. Fields read-only, no delete — the runtime rejects any change to these names |
|
|
579
|
+
| declared in `mcp.endpoints.json` by hand or by `agents up` | `global config` | the operator. CME, Documents, Mailer, Connectors, Exa… |
|
|
580
|
+
| added from the UI | `added here` | carries `"managedBy": "serve-ui"` in the file |
|
|
581
|
+
|
|
582
|
+
Removing a `global config` connector is a workspace-wide act — it leaves every
|
|
583
|
+
chat, agent and future plan — so the UI says so before confirming. The
|
|
584
|
+
container and its data are untouched; only the wiring is removed. The name is
|
|
585
|
+
also pushed into `disabledMcpServers`, which the scaffold honours, so a
|
|
586
|
+
connector you removed on purpose is not silently restored by the next
|
|
587
|
+
`agents up` merging the packaged example back in.
|
|
588
|
+
|
|
589
|
+
Renaming is atomic: the UI sends `previousName`, and the endpoint, its
|
|
590
|
+
`Authorization` header and its `chatAccess` entry move together under the new
|
|
591
|
+
key. A rename onto an existing name, or from a name that is not there, is
|
|
592
|
+
rejected rather than half-applied.
|
|
593
|
+
|
|
594
|
+
`POST /mcp/endpoints` returns **409 while a plan is running** — connector
|
|
595
|
+
wiring must not change under a run that already resolved its agents. The chat
|
|
596
|
+
UI treats that as what it is: the MCP handshake succeeded, so the card stays
|
|
597
|
+
connected and usable in this browser, badged `local only` with
|
|
598
|
+
"runtime synchronization pending", and the write is retried on the next
|
|
599
|
+
reconnect. A busy runtime never presents itself as a broken connector.
|
|
600
|
+
|
|
559
601
|
MCP `tools/call` requests retry transient HTTP/MCP failures before the run fails.
|
|
560
602
|
They also share a per-endpoint outbound control budget (45 RPM by default,
|
|
561
603
|
configurable with `WIKI_MANAGER_MCP_REQUESTS_PER_MINUTE`). This budget is
|
|
@@ -94,22 +94,13 @@ services:
|
|
|
94
94
|
|
|
95
95
|
connectors:
|
|
96
96
|
profiles: [connectors]
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
build
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
# Without these the ARGs stay unset and the image ships empty
|
|
105
|
-
# WIKILLM_GOOGLE_OAUTH_* values, so the container ends up with no Google
|
|
106
|
-
# client at all. `wiki-workspace agents up` exports them from the
|
|
107
|
-
# connectors repo's .env.build.local before invoking Compose.
|
|
108
|
-
# Deliberately value-less: Compose then forwards each host variable only
|
|
109
|
-
# when it is defined. Never write `${VAR:-}` here — an empty
|
|
110
|
-
# --build-arg pins the ARG to the empty string.
|
|
111
|
-
WIKILLM_GOOGLE_OAUTH_CLIENT_ID:
|
|
112
|
-
WIKILLM_GOOGLE_OAUTH_CLIENT_SECRET:
|
|
97
|
+
# Image only, jamais de `build:` ici. Ce fichier est livré dans le paquet
|
|
98
|
+
# npm, où le dépôt agent-connectors n'existe pas : un contexte de build
|
|
99
|
+
# pointant vers `../agent-external/…` y est irrésolvable. La construction
|
|
100
|
+
# appartient à `build-and-push.sh`, qui cuit l'application OAuth wikiLLM
|
|
101
|
+
# dans l'image publiée. Pour utiliser sa propre application Google, on
|
|
102
|
+
# surcharge GOOGLE_OAUTH_CLIENT_ID / _SECRET dans le .env du manager — sans
|
|
103
|
+
# reconstruire quoi que ce soit.
|
|
113
104
|
image: dotdrelle/agent-connectors:latest
|
|
114
105
|
user: "${UID:-1000}:${GID:-1000}"
|
|
115
106
|
ports:
|
|
@@ -119,6 +110,10 @@ services:
|
|
|
119
110
|
- MCP_AUTH_TOKEN=${CONNECTORS_MCP_AUTH_TOKEN:-}
|
|
120
111
|
- WORKSPACES_ROOT=/workspaces
|
|
121
112
|
- AGENT_DATA_DIR=/data
|
|
113
|
+
# Vides par défaut, et c'est sans conséquence : l'application OAuth
|
|
114
|
+
# embarquée dans l'image est lue depuis un fichier, pas depuis
|
|
115
|
+
# l'environnement. Renseigner l'une de ces deux clés dans le .env du
|
|
116
|
+
# manager la fait gagner sur la valeur embarquée.
|
|
122
117
|
- GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID:-}
|
|
123
118
|
- GOOGLE_OAUTH_CLIENT_SECRET=${GOOGLE_OAUTH_CLIENT_SECRET:-}
|
|
124
119
|
- GOOGLE_OAUTH_CALLBACK_URL=${GOOGLE_OAUTH_CALLBACK_URL:-}
|
package/docker-compose.yml
CHANGED
|
@@ -144,13 +144,14 @@ x-wiki-manager:
|
|
|
144
144
|
description: "Full workspace service set."
|
|
145
145
|
ui:
|
|
146
146
|
targets: [serve]
|
|
147
|
-
description: "
|
|
148
|
-
wiki:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
147
|
+
description: "Chat, wiki browser and Donna's web UI."
|
|
148
|
+
# Pas d'alias `wiki` ici : ce nom est deja celui d'un service Compose (le
|
|
149
|
+
# CLI one-shot de `/wiki run`). L'alias le masquait, et `/start wiki`
|
|
150
|
+
# demarrait mcp-http sans le dire.
|
|
151
|
+
#
|
|
152
|
+
# Pas d'alias `mcp` non plus : mcp-http fait partie du socle demarre par
|
|
153
|
+
# `all` et `services`, personne ne le demarre seul. Il reste adressable
|
|
154
|
+
# sous son nom Compose.
|
|
154
155
|
production:
|
|
155
156
|
targets: [production-mcp]
|
|
156
|
-
description: "
|
|
157
|
+
description: "Ingest, build, export and polish jobs."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotdrelle/wiki-manager",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.38",
|
|
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/agentsCompose.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/wikiSetup.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/shell/setupWizardSuggestions.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/agentsCompose.test.js src/core/profileServiceStatus.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/wikiSetup.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/googleGrants.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/shell/setupWizardSuggestions.test.js src/shell/setupWizardDiscovery.test.js src/shell/wrapText.test.js src/runtime/lifecycle.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
|
@@ -1019,12 +1019,32 @@ export function buildAgentSystemPrompt(state) {
|
|
|
1019
1019
|
? 'The runtime is connected and runtime__delegate is available for any requested capability action that has no matching direct tool. When a matching direct tool is offered, call it directly; otherwise let the runtime resolve the objective from discovered capability contracts.'
|
|
1020
1020
|
: 'No runtime is connected, so you cannot execute actions. State that plainly and name the runtime connection as the missing capability — do not invent a workaround.',
|
|
1021
1021
|
'If the connector or service needed for a requested read or action is absent from the Connected MCP tools above (its service is not running — e.g. CME, documents, or production), say plainly that this service is not connected and name it as the missing capability. Never redirect a simple read (e.g. "give me the CME config") to an "agent action", never invent its result, and never propose a workaround. Only requests you can actually serve with a listed tool are answered with data.',
|
|
1022
|
+
// Cas observé : « récupère ce mail » et « envoie un mail » refusés comme
|
|
1023
|
+
// impossibles, alors que l'agent connectors déclare `external-source.collect`
|
|
1024
|
+
// (écrit dans raw/untracked/) et `communication.send-email`. Les outils de
|
|
1025
|
+
// lecture directs ne rendent que des métadonnées : les prendre pour la
|
|
1026
|
+
// limite de l'agent transforme un travail délégable en refus.
|
|
1027
|
+
'The direct read tools of a connector are a preview, not the measure of what its agent can do. Bringing external content INTO the workspace (retrieve, import, fetch, save, "récupère") is a collect capability, and acting on the outside world (send, publish, notify) is an action capability: both are delegated through runtime__delegate, not answered from a read tool. Never conclude that something is impossible because the listed read tool returns only metadata — check the capabilities the agents declare, and delegate the objective as stated.',
|
|
1028
|
+
// Un droit manquant n'est pas une fonctionnalité absente : l'un se
|
|
1029
|
+
// réautorise en une commande, l'autre n'existe pas. Les confondre envoie
|
|
1030
|
+
// l'utilisateur croire que le produit ne sait pas faire.
|
|
1031
|
+
'When an action fails or is refused for lack of an authorization grant or scope (rather than a missing capability), say exactly that and name the primitive that grants it. Do not describe the feature as unavailable.',
|
|
1022
1032
|
'For an action with no matching direct tool, call runtime__delegate with the user objective only. The runtime chooses the capability, operation, agent and plan, including a validated single task for executor-only agents. Never choose those identifiers yourself. Never call <provider>__agent_plan, <provider>__agent_execute, legacy production__production_start_job, wiki__plan_set, or wiki__plan_done from interactive chat.',
|
|
1023
1033
|
'Do not ask the user which sources, files, connectors, or templates to use for an ingest, build, or export: the specialized agent discovers them from the workspace. When the objective is clear (e.g. "lance une ingestion"), delegate it as stated, without a clarifying question.',
|
|
1024
1034
|
'Promise only what the resolved capability actually exposes in its declared contract (the input schema the specialized agent publishes for that capability). When the user requests an execution parameter — a batch or chunk size, a count "N at a time", concurrency, ordering, priority, or any tuning knob — apply it only if that parameter exists in the target capability\'s published input schema. Otherwise do not confirm or promise it: delegate the objective, and if the user explicitly asked for that parameter, say plainly in one line that you started the work but do not control that aspect (the runtime and the specialized agent decide it). Never state or imply a parameter was applied when the agent contract cannot enforce it.',
|
|
1025
1035
|
'If runtime__delegate returns a blocker or no specialized provider is available, report only that concrete blocker concisely. Never replace the missing execution path with a suggested slash command, skill, MCP tool name, manual file move, administrator escalation, or alternative workflow unless the user explicitly asks for alternatives.',
|
|
1026
1036
|
'For workspace inventory and page listings, use the connected wiki MCP read tools. Never invent or call a /wiki shell command through shell__run_command. Use /workspace init <name> [path] for low-level non-interactive workspace creation; in the interactive TUI, /new <name> opens the setup wizard.',
|
|
1027
1037
|
'If an action requires tools or skills not available yet, explain the limitation and name the expected primitive.',
|
|
1038
|
+
// Les outils help_* étaient exposés — ils font partie de l'allow-list du
|
|
1039
|
+
// mode chat — mais rien dans ce prompt ne disait qu'ils sont LA source des
|
|
1040
|
+
// questions produit. Donna répondait donc de mémoire. Cas observé :
|
|
1041
|
+
// « comment activer les connecteurs ? » → un `cme.yaml`, un « manifeste des
|
|
1042
|
+
// services actifs » et un redémarrage de service, tous inventés, là où la
|
|
1043
|
+
// réponse tient dans un drapeau du `.env` du manager.
|
|
1044
|
+
'The bundled product documentation is your source for how llm-wiki works: what a feature is, how to enable or configure it, where a setting lives, what a message means, how to get started, how to troubleshoot. For any such question, call the documentation search/read tools FIRST and answer from what they return. Do not answer these from memory, even when you feel certain.',
|
|
1045
|
+
// Une réponse fausse et assurée sur la configuration coûte plus cher qu'un
|
|
1046
|
+
// « je ne sais pas » : elle envoie éditer des fichiers qui n'existent pas.
|
|
1047
|
+
'Configuration facts are never answered from memory. File names, environment variables, config keys, directory layouts and enabling procedures must come from a tool result or from the documentation you actually read in this conversation. If the documentation does not cover it, say plainly that you could not find where it is configured — never reconstruct a plausible-looking file name, key or procedure. A confident wrong answer sends the user editing files that do not exist.',
|
|
1028
1048
|
workspaceProfile
|
|
1029
1049
|
? `Workspace profile (.wiki/profile.md) — durable user preferences, apply these to every reply (tone, tutoiement/vouvoiement, formatting, etc.):\n${workspaceProfile}`
|
|
1030
1050
|
: null,
|
package/src/cli/wiki-manager.js
CHANGED
|
@@ -14,6 +14,7 @@ import { refreshRunningContainers } from '../core/wikiSetup.js';
|
|
|
14
14
|
import { applySessionWikircProfile } from '../core/sessionConfig.js';
|
|
15
15
|
import { listWikircProfiles } from '../core/wikirc.js';
|
|
16
16
|
import { callMcpTool, formatMcpToolResult, readChatAccessConfig } from '../core/mcp.js';
|
|
17
|
+
import { deleteManagedMcpEndpoint, listManagedMcpEndpoints, upsertManagedMcpEndpoint } from '../core/mcpEndpoints.js';
|
|
17
18
|
import { extractActivity, parseJsonText, sessionActivities, terminalFailures } from '../core/activity.js';
|
|
18
19
|
import { syncActivitiesToPlan, formatPlanStatus } from '../core/plan.js';
|
|
19
20
|
import { createAgentEvent, dispatchAgentEvent, reduceAgentEvents } from '../core/agentEvents.js';
|
|
@@ -740,6 +741,14 @@ async function runRuntime(argv, agent) {
|
|
|
740
741
|
let serverHandle = null;
|
|
741
742
|
const contexts = new Map();
|
|
742
743
|
|
|
744
|
+
async function refreshAllMcpContexts() {
|
|
745
|
+
const resolved = await Promise.all([...new Set(contexts.values())].map((value) => Promise.resolve(value)));
|
|
746
|
+
await Promise.all(resolved.map(async (context) => {
|
|
747
|
+
await refreshMcpRuntimeStatus(context.session);
|
|
748
|
+
await discoverAgentsOnce(context.session);
|
|
749
|
+
}));
|
|
750
|
+
}
|
|
751
|
+
|
|
743
752
|
async function getWorkspaceContext(workspaceName = null) {
|
|
744
753
|
const requestedWorkspace = workspaceName ? String(workspaceName).trim() : null;
|
|
745
754
|
const key = requestedWorkspace ?? '__default__';
|
|
@@ -1416,6 +1425,17 @@ async function runRuntime(argv, agent) {
|
|
|
1416
1425
|
config,
|
|
1417
1426
|
};
|
|
1418
1427
|
},
|
|
1428
|
+
listMcpEndpoints: async () => ({ endpoints: listManagedMcpEndpoints() }),
|
|
1429
|
+
upsertMcpEndpoint: async (_context, body) => {
|
|
1430
|
+
const endpoint = upsertManagedMcpEndpoint(body);
|
|
1431
|
+
await refreshAllMcpContexts();
|
|
1432
|
+
return { ok: true, endpoint };
|
|
1433
|
+
},
|
|
1434
|
+
deleteMcpEndpoint: async (_context, body) => {
|
|
1435
|
+
const endpoint = deleteManagedMcpEndpoint(body.name);
|
|
1436
|
+
await refreshAllMcpContexts();
|
|
1437
|
+
return { ok: true, endpoint };
|
|
1438
|
+
},
|
|
1419
1439
|
token: auth.token,
|
|
1420
1440
|
});
|
|
1421
1441
|
const recovery = await recoverRuntime();
|
package/src/commands/slash.js
CHANGED
|
@@ -3,6 +3,8 @@ import { openExternalUrl } from '../shell/openExternal.js';
|
|
|
3
3
|
import { classifyCommandFailure, failureHint, rawFailureText } from '../core/commandFailure.js';
|
|
4
4
|
import { join, relative } from 'node:path';
|
|
5
5
|
import { composeServices, listServices, runWikiCli, serviceLogs, serviceNames, serviceStates, startService, stopService } from '../core/compose.js';
|
|
6
|
+
import { agentServiceNames, profileServiceStatus } from '../core/agentsCompose.js';
|
|
7
|
+
import { GOOGLE_GRANTS, GOOGLE_GRANT_LABELS, defaultGoogleGrants } from '../core/googleGrants.js';
|
|
6
8
|
import {
|
|
7
9
|
applyMcpRuntimeStatus,
|
|
8
10
|
buildMcpStatus,
|
|
@@ -336,7 +338,7 @@ function workspaceStatsColumns(stats, session) {
|
|
|
336
338
|
};
|
|
337
339
|
}
|
|
338
340
|
|
|
339
|
-
function workspaceLoadedText(workspace, summary, session) {
|
|
341
|
+
function workspaceLoadedText(workspace, summary, session, mcpError = null) {
|
|
340
342
|
const profiles = listWikircProfiles(workspace.workspacePath);
|
|
341
343
|
const profileLines = profiles.length > 0
|
|
342
344
|
? profiles.map((profile) => {
|
|
@@ -372,6 +374,7 @@ function workspaceLoadedText(workspace, summary, session) {
|
|
|
372
374
|
'',
|
|
373
375
|
`llm: ${session.llm ? 'configured' : 'missing config'}`,
|
|
374
376
|
`mcp: ${Object.values(session.mcp ?? {}).filter((value) => value.status === 'connected').length} connected`,
|
|
377
|
+
...(mcpError ? ['', `MCP discovery failed: ${mcpError}`] : []),
|
|
375
378
|
].join('\n');
|
|
376
379
|
}
|
|
377
380
|
|
|
@@ -939,9 +942,17 @@ export async function handleSlashCommand(line, context) {
|
|
|
939
942
|
context.session.workspaceEnv = workspace.env;
|
|
940
943
|
context.session.workspaceEnvFile = workspace.envFile;
|
|
941
944
|
context.session.systemPrompt = loadWorkspaceSystemPrompt(workspace.workspacePath);
|
|
945
|
+
let summary;
|
|
942
946
|
try {
|
|
943
947
|
step(`Workspace: loading ${workspace.name} config…`);
|
|
944
|
-
|
|
948
|
+
({ summary } = applySessionWikircProfile(context.session, 'default'));
|
|
949
|
+
} catch (err) {
|
|
950
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
951
|
+
return {
|
|
952
|
+
output: workspaceLoadedWithoutConfigText(workspace, message),
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
try {
|
|
945
956
|
step(`Workspace: discovering ${workspace.name} MCP tools…`);
|
|
946
957
|
await refreshMcpRuntimeStatus(context.session);
|
|
947
958
|
return {
|
|
@@ -950,7 +961,7 @@ export async function handleSlashCommand(line, context) {
|
|
|
950
961
|
} catch (err) {
|
|
951
962
|
const message = err instanceof Error ? err.message : String(err);
|
|
952
963
|
return {
|
|
953
|
-
output:
|
|
964
|
+
output: workspaceLoadedText(workspace, summary, context.session, message),
|
|
954
965
|
};
|
|
955
966
|
}
|
|
956
967
|
}
|
|
@@ -1058,6 +1069,12 @@ export async function handleSlashCommand(line, context) {
|
|
|
1058
1069
|
// falls back to the hardcoded COMPOSE_SERVICES constant instead.
|
|
1059
1070
|
const service = args[1];
|
|
1060
1071
|
if (service === 'agents' || service === 'agent') return runAgentCommand(startAgents, 'start');
|
|
1072
|
+
// Un agent nommé appartient à la pile agents (projet Compose distinct),
|
|
1073
|
+
// pas à celle du workspace : le router ici évite un « no such service »
|
|
1074
|
+
// sur un nom que la complétion propose pourtant.
|
|
1075
|
+
if (agentServiceNames().includes(service)) {
|
|
1076
|
+
return runAgentCommand((options) => startAgents({ ...options, services: [service] }), 'start');
|
|
1077
|
+
}
|
|
1061
1078
|
// "all" used to mean "the workspace services", which left the external
|
|
1062
1079
|
// agents down and looked like nothing had happened. It now means what an
|
|
1063
1080
|
// operator reads into it: the whole stack. `/start services` keeps the
|
|
@@ -1094,9 +1111,22 @@ export async function handleSlashCommand(line, context) {
|
|
|
1094
1111
|
case 'stop': {
|
|
1095
1112
|
const service = args[1];
|
|
1096
1113
|
if (service === 'agents') return runAgentCommand(stopAgents, 'stop');
|
|
1114
|
+
if (agentServiceNames().includes(service)) {
|
|
1115
|
+
return runAgentCommand((options) => stopAgents({ ...options, services: [service] }), 'stop');
|
|
1116
|
+
}
|
|
1117
|
+
// Symétrique de `/start all` : « all » désigne toute la pile, agents
|
|
1118
|
+
// compris. Il ne stoppait que les services du workspace et laissait les
|
|
1119
|
+
// agents debout — donc `/start all` puis `/stop all` ne revenait pas à
|
|
1120
|
+
// l'état de départ.
|
|
1121
|
+
const stopsAgents = service === 'all';
|
|
1122
|
+
const stopTarget = service === 'services' ? undefined : service;
|
|
1097
1123
|
try {
|
|
1098
1124
|
step(`Services: stopping ${service ?? 'workspace services'}…`);
|
|
1099
|
-
await stopService(context.session,
|
|
1125
|
+
await stopService(context.session, stopTarget);
|
|
1126
|
+
if (stopsAgents) {
|
|
1127
|
+
const agentsResult = await runAgentCommand(stopAgents, 'stop');
|
|
1128
|
+
if (agentsResult?.failed) return agentsResult;
|
|
1129
|
+
}
|
|
1100
1130
|
step('Services: refreshing MCP runtime…');
|
|
1101
1131
|
await refreshMcpRuntimeStatus(context.session);
|
|
1102
1132
|
return localizedOperationResult({
|
|
@@ -1186,7 +1216,9 @@ export async function handleSlashCommand(line, context) {
|
|
|
1186
1216
|
await refreshMcpRuntimeStatus(context.session);
|
|
1187
1217
|
const connectorMcp = context.session.mcp?.connectors;
|
|
1188
1218
|
if (!connectorMcp || connectorMcp.status !== 'connected') {
|
|
1189
|
-
|
|
1219
|
+
// Le manager sait exactement pourquoi : ne pas renvoyer un constat
|
|
1220
|
+
// vague que Donna comblerait en inventant.
|
|
1221
|
+
return connectorResult(profileServiceStatus('connectors').message);
|
|
1190
1222
|
}
|
|
1191
1223
|
if (subcommand === 'list') {
|
|
1192
1224
|
if (args[2]) return connectorResult('The connector list request has invalid extra arguments.');
|
|
@@ -1198,38 +1230,78 @@ export async function handleSlashCommand(line, context) {
|
|
|
1198
1230
|
{ workspace: context.session.workspace },
|
|
1199
1231
|
);
|
|
1200
1232
|
const payload = parseJsonText(formatMcpToolResult(result));
|
|
1201
|
-
|
|
1202
|
-
|
|
1233
|
+
if (payload?.status !== 'configured') {
|
|
1234
|
+
return connectorResult('google (Gmail): not authorized. Run `/connector auth google` to authorize reading and sending.');
|
|
1235
|
+
}
|
|
1236
|
+
// Le libellé annonçait « read-only » quels que soient les droits
|
|
1237
|
+
// réellement accordés — donc il mentait dès qu'on autorisait l'envoi,
|
|
1238
|
+
// et n'aidait pas à comprendre pourquoi l'envoi échouait sinon.
|
|
1239
|
+
const grants = Array.isArray(payload?.grants) ? payload.grants : [];
|
|
1240
|
+
const missing = GOOGLE_GRANTS.filter((grant) => !grants.includes(grant));
|
|
1241
|
+
const held = grants.map((grant) => `${grant} — ${GOOGLE_GRANT_LABELS[grant] ?? 'unknown grant'}`);
|
|
1242
|
+
const lines = [
|
|
1243
|
+
`google (Gmail): authorized for ${grants.join(', ') || 'nothing'}`,
|
|
1244
|
+
...held.map((line) => ` ✓ ${line}`),
|
|
1245
|
+
...missing.map((grant) => ` ✗ ${grant} — ${GOOGLE_GRANT_LABELS[grant]}`),
|
|
1246
|
+
];
|
|
1247
|
+
if (missing.length > 0) {
|
|
1248
|
+
lines.push(`Run \`/connector auth google ${missing.join(' ')}\` to add the missing grant(s); existing ones are kept.`);
|
|
1249
|
+
}
|
|
1250
|
+
return connectorResult(lines.join('\n'));
|
|
1203
1251
|
} catch (err) {
|
|
1204
|
-
return connectorResult(`google (Gmail
|
|
1252
|
+
return connectorResult(`google (Gmail): unavailable (${err instanceof Error ? err.message : String(err)})`);
|
|
1205
1253
|
}
|
|
1206
1254
|
}
|
|
1207
1255
|
if (subcommand === 'auth') {
|
|
1208
1256
|
const connector = String(args[2] ?? '').toLowerCase();
|
|
1209
1257
|
if (!['google', 'gmail'].includes(connector)) {
|
|
1210
|
-
return connectorResult('The requested connector is unsupported. The available connector is google (Gmail
|
|
1258
|
+
return connectorResult('The requested connector is unsupported. The available connector is google (Gmail).');
|
|
1259
|
+
}
|
|
1260
|
+
// Les droits demandés à Google. L'appel ne les passait pas, et le
|
|
1261
|
+
// serveur retombait sur son défaut `["read"]` : l'agent sait envoyer un
|
|
1262
|
+
// courriel, l'autorisation obtenue ne le permettait pas, et le refus
|
|
1263
|
+
// ressemblait à une fonctionnalité absente. On demande donc lecture ET
|
|
1264
|
+
// envoi par défaut, et les droits restants s'ajoutent à la demande.
|
|
1265
|
+
const requested = args.slice(3).map((value) => String(value).toLowerCase());
|
|
1266
|
+
const unknown = requested.filter((grant) => !GOOGLE_GRANTS.includes(grant));
|
|
1267
|
+
if (unknown.length > 0) {
|
|
1268
|
+
const available = GOOGLE_GRANTS.map((grant) => `${grant} (${GOOGLE_GRANT_LABELS[grant]})`).join('; ');
|
|
1269
|
+
return connectorResult(`Unsupported grant(s): ${unknown.join(', ')}. Available grants: ${available}.`);
|
|
1211
1270
|
}
|
|
1271
|
+
// Par défaut, tout ce que l'agent sait faire — y compris `modify`, sans
|
|
1272
|
+
// quoi les actions que Donna propose d'elle-même (« marquer comme lu »,
|
|
1273
|
+
// « archiver ») échouent après coup. C'est la même incohérence que
|
|
1274
|
+
// l'envoi : promettre une action que l'autorisation ne couvre pas.
|
|
1275
|
+
const grants = requested.length > 0 ? [...new Set(requested)] : defaultGoogleGrants();
|
|
1212
1276
|
try {
|
|
1213
1277
|
const result = await callMcpTool(
|
|
1214
1278
|
context.session.mcp,
|
|
1215
1279
|
'connectors',
|
|
1216
1280
|
'connectors_google_oauth_start',
|
|
1217
|
-
{ workspace: context.session.workspace },
|
|
1281
|
+
{ workspace: context.session.workspace, grants },
|
|
1218
1282
|
);
|
|
1219
1283
|
const payload = parseJsonText(formatMcpToolResult(result));
|
|
1220
1284
|
const authorizationUrl = payload?.authorizationUrl;
|
|
1285
|
+
if (payload?.error === 'send_capability_disabled') {
|
|
1286
|
+
return connectorResult(
|
|
1287
|
+
'Sending is disabled in this deployment (CONNECTORS_SEND_ENABLED=false in the manager .env), so the send grant cannot be authorized. Run `/connector auth google read` for read-only access, or enable sending and restart the connectors agent.',
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1221
1290
|
if (payload?.ok !== true || typeof authorizationUrl !== 'string') {
|
|
1222
1291
|
return connectorResult(`Google authorization could not start (${payload?.error ?? 'missing authorization URL'}).`);
|
|
1223
1292
|
}
|
|
1293
|
+
// L'autorisation est incrémentale côté Google : redemander avec un
|
|
1294
|
+
// droit de plus ne révoque pas les précédents.
|
|
1295
|
+
const scopeNote = `Requested grants: ${grants.join(', ')}.`;
|
|
1224
1296
|
if (openExternalUrl(authorizationUrl)) {
|
|
1225
|
-
return connectorResult(
|
|
1297
|
+
return connectorResult(`Google authorization opened successfully in the user browser. ${scopeNote}`);
|
|
1226
1298
|
}
|
|
1227
|
-
return connectorResult(`Google authorization requires the user to open this URL: ${authorizationUrl}`);
|
|
1299
|
+
return connectorResult(`Google authorization requires the user to open this URL: ${authorizationUrl} — ${scopeNote}`);
|
|
1228
1300
|
} catch (err) {
|
|
1229
1301
|
return connectorResult(`Google authorization could not start (${err instanceof Error ? err.message : String(err)}).`);
|
|
1230
1302
|
}
|
|
1231
1303
|
}
|
|
1232
|
-
return connectorResult(
|
|
1304
|
+
return connectorResult(`The requested connector action is unsupported. Available actions: list, and auth google [${GOOGLE_GRANTS.join('|')}].`);
|
|
1233
1305
|
}
|
|
1234
1306
|
case 'cancel': {
|
|
1235
1307
|
// Alias of /run cancel — people type /cancel when they want out.
|
|
@@ -126,9 +126,43 @@ test('/start completes to the three documented targets', async () => {
|
|
|
126
126
|
for (const expected of ['all', 'agents', 'services']) {
|
|
127
127
|
assert.ok(matches.includes(expected), `${expected} missing from ${JSON.stringify(matches)}`);
|
|
128
128
|
}
|
|
129
|
-
assert.match(completionDescription('all', ['/start']), /services AND
|
|
130
|
-
assert.match(completionDescription('agents', ['/start']), /agents only/);
|
|
131
|
-
assert.match(completionDescription('services', ['/start']), /workspace services only/);
|
|
129
|
+
assert.match(completionDescription('all', ['/start']), /services AND external agents/);
|
|
130
|
+
assert.match(completionDescription('agents', ['/start']), /agents only/i);
|
|
131
|
+
assert.match(completionDescription('services', ['/start']), /workspace services only/i);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('/start offers the operator vocabulary, not the Compose service names twice', async () => {
|
|
135
|
+
const { completionDescription } = await import('../shell/repl.js');
|
|
136
|
+
const { matches } = completionContext('/start ', {});
|
|
137
|
+
|
|
138
|
+
// `all` apparaissait deux fois — mot-clé du shell ET alias Compose — et
|
|
139
|
+
// chaque alias était doublé du service qu'il désigne : dix entrées pour cinq
|
|
140
|
+
// actions.
|
|
141
|
+
assert.equal(new Set(matches).size, matches.length, `duplicates in ${JSON.stringify(matches)}`);
|
|
142
|
+
for (const raw of ['serve', 'mcp-http', 'production-mcp']) {
|
|
143
|
+
assert.ok(!matches.includes(raw), `${raw} is already covered by an alias`);
|
|
144
|
+
}
|
|
145
|
+
for (const alias of ['ui', 'production']) {
|
|
146
|
+
assert.ok(matches.includes(alias), `${alias} missing from ${JSON.stringify(matches)}`);
|
|
147
|
+
assert.notEqual(
|
|
148
|
+
completionDescription(alias, ['/start']),
|
|
149
|
+
'Start this Docker Compose service.',
|
|
150
|
+
`${alias} must explain what it starts`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
// `wiki` est le service one-shot de `/wiki run` : en faire un alias de
|
|
154
|
+
// mcp-http le masquait silencieusement.
|
|
155
|
+
assert.ok(!matches.includes('wiki'), 'wiki must not shadow the one-shot CLI service');
|
|
156
|
+
// mcp-http fait partie du socle demarre par `all` et `services` : son alias
|
|
157
|
+
// n'ajoutait qu'une ligne a lire pour un service que personne ne lance seul.
|
|
158
|
+
assert.ok(!matches.includes('mcp'), 'mcp is covered by services/all');
|
|
159
|
+
// Les agents du socle ne s'adressent pas un par un, et `mailer` n'existe
|
|
160
|
+
// meme pas tant qu'il n'est pas decommente dans l'override de l'operateur.
|
|
161
|
+
for (const agent of ['cme', 'documents', 'mailer']) {
|
|
162
|
+
assert.ok(!matches.includes(agent), `${agent} is started by "agents", not on its own`);
|
|
163
|
+
}
|
|
164
|
+
// Seuls les agents derriere un drapeau de profil se pilotent separement.
|
|
165
|
+
assert.ok(matches.includes('connectors'), 'connectors is opt-in, so it must be addressable');
|
|
132
166
|
});
|
|
133
167
|
|
|
134
168
|
test('/workspace delete removes files and clears current session context after confirmation', async () => {
|
|
@@ -311,6 +345,54 @@ test('/use loads only workspaces and /config use switches wikirc profiles', asyn
|
|
|
311
345
|
}
|
|
312
346
|
});
|
|
313
347
|
|
|
348
|
+
test('/use keeps a loaded wikirc when MCP discovery fails', async () => {
|
|
349
|
+
const root = await mkdtemp(join(tmpdir(), 'wiki-manager-use-mcp-error-'));
|
|
350
|
+
const registryRoot = join(root, 'registry');
|
|
351
|
+
const workspacePath = join(root, 'workspace');
|
|
352
|
+
const registryPath = join(registryRoot, 'demo');
|
|
353
|
+
mkdirSync(registryPath, { recursive: true });
|
|
354
|
+
mkdirSync(workspacePath, { recursive: true });
|
|
355
|
+
mkdirSync(join(root, 'mcp.endpoints.json'));
|
|
356
|
+
writeFileSync(join(root, '.env'), '', 'utf8');
|
|
357
|
+
writeFileSync(join(registryPath, '.env'), [
|
|
358
|
+
'WORKSPACE_NAME=demo',
|
|
359
|
+
`WIKI_WORKSPACE_PATH=${workspacePath}`,
|
|
360
|
+
'',
|
|
361
|
+
].join('\n'), 'utf8');
|
|
362
|
+
writeFileSync(join(workspacePath, '.wikirc.yaml'), [
|
|
363
|
+
'language: fr',
|
|
364
|
+
'llm:',
|
|
365
|
+
' provider: openai-compatible',
|
|
366
|
+
' engine: openai',
|
|
367
|
+
' model: test-model',
|
|
368
|
+
' apiKey: test-key',
|
|
369
|
+
'',
|
|
370
|
+
].join('\n'), 'utf8');
|
|
371
|
+
|
|
372
|
+
const previousDir = process.env.WIKI_WORKSPACES_DIR;
|
|
373
|
+
const previousEnvFile = process.env.WIKI_MANAGER_ENV_FILE;
|
|
374
|
+
process.env.WIKI_WORKSPACES_DIR = registryRoot;
|
|
375
|
+
process.env.WIKI_MANAGER_ENV_FILE = join(root, '.env');
|
|
376
|
+
try {
|
|
377
|
+
const session = {};
|
|
378
|
+
const result = await handleSlashCommand('/use demo', {
|
|
379
|
+
packageJson: { version: 'test' },
|
|
380
|
+
session,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
assert.equal(session.wikirc?.profile, 'default');
|
|
384
|
+
assert.equal(session.wikircConfig?.llm?.model, 'test-model');
|
|
385
|
+
assert.match(result.output ?? '', /profile: default/);
|
|
386
|
+
assert.match(result.output ?? '', /MCP discovery failed:.*EISDIR/s);
|
|
387
|
+
assert.doesNotMatch(result.output ?? '', /Wikirc not loaded/);
|
|
388
|
+
} finally {
|
|
389
|
+
if (previousDir === undefined) delete process.env.WIKI_WORKSPACES_DIR;
|
|
390
|
+
else process.env.WIKI_WORKSPACES_DIR = previousDir;
|
|
391
|
+
if (previousEnvFile === undefined) delete process.env.WIKI_MANAGER_ENV_FILE;
|
|
392
|
+
else process.env.WIKI_MANAGER_ENV_FILE = previousEnvFile;
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
314
396
|
test('/queue cancel refuses runtime-managed items instead of fake-cancelling locally', async () => {
|
|
315
397
|
// syncRuntimeState replaces session.jobQueue with the runtime queue and tags
|
|
316
398
|
// origin:'runtime' — a local cancel would be reverted by the next SSE sync,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import YAML from 'yaml';
|
|
3
4
|
|
|
4
5
|
import { managerComposeOverrideFile, managerEnvFile, readEnvFile } from './env.js';
|
|
5
6
|
import { managerRoot } from './workspaces.js';
|
|
@@ -16,10 +17,94 @@ export const PROFILE_FLAGS = Object.freeze({
|
|
|
16
17
|
connectors: 'CONNECTORS_ENABLED',
|
|
17
18
|
});
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Services réellement déclarés dans la pile agents, override compris.
|
|
22
|
+
*
|
|
23
|
+
* Lue du fichier plutôt que codée en dur : une liste figée avait fini par
|
|
24
|
+
* annoncer `mailer`, qui ne vit que dans l'exemple d'override et n'existe donc
|
|
25
|
+
* pas tant que l'opérateur ne l'a pas décommenté. Proposer un service absent,
|
|
26
|
+
* c'est promettre un « no such service ».
|
|
27
|
+
*
|
|
28
|
+
* Sert au ROUTAGE : la pile agents est un projet Compose distinct de celui du
|
|
29
|
+
* workspace, donc `/start <nom>` doit partir vers `wiki-workspace agents`.
|
|
30
|
+
*/
|
|
31
|
+
export function agentServiceNames({ env = resolvedManagerEnv() } = {}) {
|
|
32
|
+
const { composeFiles } = resolveAgentsComposeContext({ env });
|
|
33
|
+
const names = new Set();
|
|
34
|
+
for (const file of composeFiles) {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = YAML.parse(readFileSync(file, 'utf8')) ?? {};
|
|
37
|
+
for (const name of Object.keys(parsed.services ?? {})) names.add(name);
|
|
38
|
+
} catch { /* fichier illisible : il ne déclare rien d'adressable */ }
|
|
39
|
+
}
|
|
40
|
+
return [...names].sort();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Agents qu'un opérateur pilote un par un, proposés dans les complétions.
|
|
45
|
+
*
|
|
46
|
+
* Uniquement ceux placés derrière un drapeau de profil : ce sont les seuls
|
|
47
|
+
* qu'on active et désactive délibérément. `cme` et `documents` font partie du
|
|
48
|
+
* socle — on les démarre avec `agents`, les distinguer n'apporte rien et
|
|
49
|
+
* allonge une liste que l'opérateur doit lire à chaque fois.
|
|
50
|
+
*/
|
|
51
|
+
export function togglableAgentNames() {
|
|
52
|
+
return Object.keys(PROFILE_FLAGS).sort();
|
|
53
|
+
}
|
|
54
|
+
|
|
19
55
|
export function isEnabled(value) {
|
|
20
56
|
return /^(?:1|true|yes|on)$/i.test(String(value ?? '').trim());
|
|
21
57
|
}
|
|
22
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Pourquoi un service à profil est indisponible, et comment l'activer.
|
|
61
|
+
*
|
|
62
|
+
* Le message d'origine — « le service des connecteurs est indisponible ou
|
|
63
|
+
* désactivé » — ne disait ni lequel des deux, ni où regarder. Donna, à qui on
|
|
64
|
+
* demandait ensuite « comment l'activer », n'avait aucun fait à citer et
|
|
65
|
+
* inventait des fichiers (`cme.yaml`, un « manifeste des services actifs »)
|
|
66
|
+
* qui n'existent nulle part. Une réponse fausse et confiante coûte plus cher
|
|
67
|
+
* qu'un « je ne sais pas » : c'est le message qu'il faut rendre suffisant,
|
|
68
|
+
* pas le modèle qu'il faut espérer plus prudent.
|
|
69
|
+
*
|
|
70
|
+
* @returns {{ enabled: boolean, flag: string, envFile: string, message: string }}
|
|
71
|
+
*/
|
|
72
|
+
export function profileServiceStatus(profile, { env = resolvedManagerEnv() } = {}) {
|
|
73
|
+
const flag = PROFILE_FLAGS[profile];
|
|
74
|
+
const envFile = managerEnvFile();
|
|
75
|
+
if (!flag) {
|
|
76
|
+
return {
|
|
77
|
+
enabled: false,
|
|
78
|
+
flag: null,
|
|
79
|
+
envFile,
|
|
80
|
+
message: `There is no "${profile}" service in this deployment.`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const enabled = isEnabled(env[flag]);
|
|
84
|
+
if (enabled) {
|
|
85
|
+
return {
|
|
86
|
+
enabled: true,
|
|
87
|
+
flag,
|
|
88
|
+
envFile,
|
|
89
|
+
message: `The ${profile} service is enabled (${flag}) but not reachable — its container is probably not running. Start it with \`/start agents\`.`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const current = String(env[flag] ?? '').trim();
|
|
93
|
+
return {
|
|
94
|
+
enabled: false,
|
|
95
|
+
flag,
|
|
96
|
+
envFile,
|
|
97
|
+
message: [
|
|
98
|
+
`The ${profile} service is disabled: ${flag} is ${current === '' ? 'not set' : `"${current}"`} in ${envFile}.`,
|
|
99
|
+
'To enable it:',
|
|
100
|
+
` 1. set ${flag}=true in ${envFile}`,
|
|
101
|
+
' 2. run `/start agents` (or `wiki-workspace agents up`) to start its container',
|
|
102
|
+
` 3. run \`/connector list\` to check it answers`,
|
|
103
|
+
`The ${profile} service runs behind a Compose profile, so while ${flag} is off its container does not exist at all — it will not appear in \`docker compose ps\`.`,
|
|
104
|
+
].join('\n'),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
23
108
|
/**
|
|
24
109
|
* The manager `.env` wins over the ambient process environment.
|
|
25
110
|
*
|