@dotdrelle/wiki-manager 0.15.29 → 0.15.32
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 +5 -2
- package/agents.docker-compose.yml +10 -0
- package/bin/wiki-manager.js +1 -1
- package/docker-compose.override.example.yml +1 -0
- package/package.json +1 -1
- package/src/agent/graph.test.js +17 -4
- package/src/cli/runtimeStartup.test.js +14 -0
- package/src/cli/wiki-manager.js +79 -13
- package/src/cli/wiki-manager.test.js +157 -0
- package/src/commands/slash.js +9 -0
- package/src/core/buildInfo.json +2 -2
- package/src/core/mcp.js +113 -38
- package/src/core/mcp.test.js +181 -20
- package/src/core/wikiWorkspace.test.js +16 -0
- package/src/orchestrator/agentRegistry.js +35 -7
- package/src/orchestrator/agentRegistry.test.js +73 -0
- package/src/orchestrator/dispatcher.js +6 -1
- package/src/orchestrator/dispatcher.test.js +24 -1
- package/src/runtime/auth.test.js +1 -65
- package/src/runtime/donna-contract.test.js +2 -1
- package/src/runtime/lifecycle.js +0 -33
- package/src/runtime/runner.test.js +10 -1
- package/src/runtime/supervisor.test.js +11 -1
- package/src/shell/tui.tsx +10 -18
- package/wiki-workspace +69 -0
package/.env.example
CHANGED
|
@@ -56,8 +56,11 @@ CONNECTORS_MCP_AUTH_TOKEN=
|
|
|
56
56
|
# Enable the opt-in agent-connectors service:
|
|
57
57
|
CONNECTORS_ENABLED=false
|
|
58
58
|
#
|
|
59
|
-
# The
|
|
60
|
-
#
|
|
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. When you
|
|
61
|
+
# build the image locally, that file must exist — otherwise the container starts
|
|
62
|
+
# with empty credentials.
|
|
63
|
+
# Optional runtime override (no rebuild needed):
|
|
61
64
|
# GOOGLE_OAUTH_CLIENT_ID=
|
|
62
65
|
# Optional confidential-client compatibility override:
|
|
63
66
|
# GOOGLE_OAUTH_CLIENT_SECRET=
|
|
@@ -100,6 +100,16 @@ services:
|
|
|
100
100
|
build:
|
|
101
101
|
context: ../agent-external/agent-connectors
|
|
102
102
|
dockerfile: Dockerfile
|
|
103
|
+
args:
|
|
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:
|
|
103
113
|
image: dotdrelle/agent-connectors:latest
|
|
104
114
|
user: "${UID:-1000}:${GID:-1000}"
|
|
105
115
|
ports:
|
package/bin/wiki-manager.js
CHANGED
|
@@ -80,7 +80,7 @@ async function main() {
|
|
|
80
80
|
// Fallback for already-bootstrapped direct invocations; the shell wrapper
|
|
81
81
|
// exports these before Bun starts.
|
|
82
82
|
if (parsed.cacert) Object.assign(process.env, cacertEnvVars(parsed.cacert));
|
|
83
|
-
const interactive = process.stdout.isTTY && process.stdin.isTTY && argv[0] !== 'runtime' && !argv.includes('--setup-wizard') && !argv.includes('--headless') && !argv.includes('--once') && !argv.includes('--version') && !argv.includes('-v') && !argv.includes('--help') && !argv.includes('-h');
|
|
83
|
+
const interactive = process.stdout.isTTY && process.stdin.isTTY && argv[0] !== 'runtime' && !argv.includes('--refresh') && !argv.includes('--setup-wizard') && !argv.includes('--headless') && !argv.includes('--once') && !argv.includes('--version') && !argv.includes('-v') && !argv.includes('--help') && !argv.includes('-h');
|
|
84
84
|
if (interactive || argv.includes('--setup-wizard')) await import('@opentui/solid/preload');
|
|
85
85
|
if (interactive) process.stdout.write('Starting wiki-manager…\r');
|
|
86
86
|
const { runCli } = await import('../src/cli/wiki-manager.js');
|
package/package.json
CHANGED
package/src/agent/graph.test.js
CHANGED
|
@@ -203,8 +203,10 @@ function toolCallingLlm() {
|
|
|
203
203
|
test('agent graph waits for run-level approval before first MCP action', async () => {
|
|
204
204
|
const originalFetch = globalThis.fetch;
|
|
205
205
|
let fetchCalls = 0;
|
|
206
|
-
globalThis.fetch = async () => {
|
|
207
|
-
|
|
206
|
+
globalThis.fetch = async (_url, init) => {
|
|
207
|
+
// Count tool traffic only: the MCP session handshake is transport
|
|
208
|
+
// plumbing, not an action the user needs to approve or observe.
|
|
209
|
+
if (JSON.parse(init.body).method === 'tools/call') fetchCalls += 1;
|
|
208
210
|
return {
|
|
209
211
|
ok: true,
|
|
210
212
|
status: 200,
|
|
@@ -488,6 +490,15 @@ test('Donna refuses to delegate connector authentication to an export capability
|
|
|
488
490
|
globalThis.fetch = async (url, options = {}) => {
|
|
489
491
|
fetchedUrls.push(String(url));
|
|
490
492
|
const body = JSON.parse(String(options.body ?? '{}'));
|
|
493
|
+
// MCP session handshake: answer it, then assert on the real tool call.
|
|
494
|
+
if (body.method === 'initialize') {
|
|
495
|
+
return {
|
|
496
|
+
ok: true,
|
|
497
|
+
status: 200,
|
|
498
|
+
headers: { get: () => null },
|
|
499
|
+
text: async () => '{"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2025-06-18"}}',
|
|
500
|
+
};
|
|
501
|
+
}
|
|
491
502
|
assert.equal(body.params?.name, 'connectors_google_oauth_start');
|
|
492
503
|
assert.deepEqual(body.params?.arguments, { workspace: 'docs' });
|
|
493
504
|
return {
|
|
@@ -1302,8 +1313,10 @@ test('agent graph executes action inputs inside a runtime run instead of asking
|
|
|
1302
1313
|
// returned a canned clarification — "lance l'ingestion" did nothing.
|
|
1303
1314
|
const originalFetch = globalThis.fetch;
|
|
1304
1315
|
let fetchCalls = 0;
|
|
1305
|
-
globalThis.fetch = async () => {
|
|
1306
|
-
|
|
1316
|
+
globalThis.fetch = async (_url, init) => {
|
|
1317
|
+
// Count tool traffic only: the MCP session handshake is transport
|
|
1318
|
+
// plumbing, not an action the user needs to approve or observe.
|
|
1319
|
+
if (JSON.parse(init.body).method === 'tools/call') fetchCalls += 1;
|
|
1307
1320
|
return {
|
|
1308
1321
|
ok: true,
|
|
1309
1322
|
status: 200,
|
|
@@ -12,3 +12,17 @@ test('runtime startup is not blocked by optional Docker image maintenance', asyn
|
|
|
12
12
|
assert.match(runtimeBranch, /await runRuntime\(argv\.slice\(1\), agent\)/);
|
|
13
13
|
assert.doesNotMatch(runtimeBranch, /refreshRunningContainers/);
|
|
14
14
|
});
|
|
15
|
+
|
|
16
|
+
test('shell exit leaves the shared runtime alive and refresh is explicit', async () => {
|
|
17
|
+
const cli = await readFile(new URL('./wiki-manager.js', import.meta.url), 'utf8');
|
|
18
|
+
const tui = await readFile(new URL('../shell/tui.tsx', import.meta.url), 'utf8');
|
|
19
|
+
const bin = await readFile(new URL('../../bin/wiki-manager.js', import.meta.url), 'utf8');
|
|
20
|
+
|
|
21
|
+
assert.doesNotMatch(cli, /await shutdownOwnedRuntime\(runtime/);
|
|
22
|
+
assert.doesNotMatch(tui, /shutdownOwnedRuntime/);
|
|
23
|
+
assert.match(tui, /shell closed; shared runtime left running/);
|
|
24
|
+
assert.match(tui, /process\.exit\(0\)/);
|
|
25
|
+
assert.match(cli, /argv\.includes\('--refresh'\)/);
|
|
26
|
+
assert.match(cli, /spawnSync\(workspaceCliPath, \['refresh'\]/);
|
|
27
|
+
assert.match(bin, /!argv\.includes\('--refresh'\)/);
|
|
28
|
+
});
|
package/src/cli/wiki-manager.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
2
3
|
import { readFileSync } from 'node:fs';
|
|
3
4
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
5
|
import { dirname, join, resolve } from 'node:path';
|
|
@@ -25,6 +26,7 @@ import { listWorkspaces } from '../core/workspaces.js';
|
|
|
25
26
|
|
|
26
27
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
27
28
|
const packageJsonPath = resolve(__dirname, '../../package.json');
|
|
29
|
+
const workspaceCliPath = resolve(__dirname, '../../wiki-workspace');
|
|
28
30
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
29
31
|
const SHELL_COMMANDS = ['help', 'version', 'exit', 'workspace', 'new', 'use', 'config', 'status', 'services', 'start', 'stop', 'logs', 'mcp', 'connector', 'wiki', 'skills', 'clear', 'chat', 'agent', 'approve'];
|
|
30
32
|
|
|
@@ -106,7 +108,13 @@ export function buildExecutorOnlyFragment({ objective, workspace, selection }) {
|
|
|
106
108
|
* then a JSON-text completion, then no arguments — the executor uses its own
|
|
107
109
|
* defaults. It never throws and never invents identifiers.
|
|
108
110
|
*/
|
|
109
|
-
export async function resolveExecutorArguments({
|
|
111
|
+
export async function resolveExecutorArguments({
|
|
112
|
+
llm,
|
|
113
|
+
objective,
|
|
114
|
+
capability,
|
|
115
|
+
workspace,
|
|
116
|
+
signal,
|
|
117
|
+
} = {}) {
|
|
110
118
|
const schema = capability?.inputSchema;
|
|
111
119
|
const objectiveText = String(objective ?? '').trim();
|
|
112
120
|
if (
|
|
@@ -121,10 +129,22 @@ export async function resolveExecutorArguments({ llm, objective, capability, sig
|
|
|
121
129
|
) {
|
|
122
130
|
return {};
|
|
123
131
|
}
|
|
132
|
+
const workspaceName = String(workspace ?? '').trim();
|
|
124
133
|
const system = [
|
|
125
134
|
'Extract structured arguments for a task from the user objective.',
|
|
126
135
|
'Only fill a field when the objective explicitly states or clearly implies its value.',
|
|
127
136
|
'Omit every field that is not stated. Never invent identifiers, queries, filters or counts.',
|
|
137
|
+
// The objective almost always names the workspace ("export the pages of
|
|
138
|
+
// workspace acpi"), and the orchestrator already binds it out of band. With
|
|
139
|
+
// one free-text field in the schema and no field for the workspace, a model
|
|
140
|
+
// reliably misbinds the two — that is how a workspace name ended up as a
|
|
141
|
+
// source name and failed the task.
|
|
142
|
+
...(workspaceName
|
|
143
|
+
? [
|
|
144
|
+
`The task already runs against workspace "${workspaceName}"; the orchestrator supplies it separately.`,
|
|
145
|
+
`Never use "${workspaceName}" as the value of any field. If the objective only names the workspace, return {}.`,
|
|
146
|
+
]
|
|
147
|
+
: []),
|
|
128
148
|
'Return the arguments object only.',
|
|
129
149
|
].join('\n');
|
|
130
150
|
const tool = {
|
|
@@ -150,9 +170,9 @@ export async function resolveExecutorArguments({ llm, objective, capability, sig
|
|
|
150
170
|
});
|
|
151
171
|
const call = (result?.tool_calls ?? []).find((item) => item?.function?.name === 'set_task_arguments');
|
|
152
172
|
const fromCall = call ? safeParseArgumentObject(call.function?.arguments) : null;
|
|
153
|
-
if (fromCall) return pruneArgumentsToSchema(fromCall, schema);
|
|
173
|
+
if (fromCall) return pruneArgumentsToSchema(fromCall, schema, workspaceName);
|
|
154
174
|
const fromText = safeParseArgumentObject(result?.content);
|
|
155
|
-
if (fromText) return pruneArgumentsToSchema(fromText, schema);
|
|
175
|
+
if (fromText) return pruneArgumentsToSchema(fromText, schema, workspaceName);
|
|
156
176
|
} catch {
|
|
157
177
|
// Fall through to the tool-less path.
|
|
158
178
|
}
|
|
@@ -164,13 +184,36 @@ export async function resolveExecutorArguments({ llm, objective, capability, sig
|
|
|
164
184
|
signal,
|
|
165
185
|
});
|
|
166
186
|
const fromText = safeParseArgumentObject(result?.content);
|
|
167
|
-
if (fromText) return pruneArgumentsToSchema(fromText, schema);
|
|
187
|
+
if (fromText) return pruneArgumentsToSchema(fromText, schema, workspaceName);
|
|
168
188
|
} catch {
|
|
169
189
|
// Give up: the executor will use its own defaults.
|
|
170
190
|
}
|
|
171
191
|
return {};
|
|
172
192
|
}
|
|
173
193
|
|
|
194
|
+
// The system prompt above is advice a model may ignore; this is not. A value
|
|
195
|
+
// that merely echoes the workspace name carries no information the orchestrator
|
|
196
|
+
// does not already hold, so dropping it can only widen the task to the
|
|
197
|
+
// executor's own default — never narrow it to something wrong. (A source
|
|
198
|
+
// genuinely named after its workspace degrades to "process everything", which
|
|
199
|
+
// still includes it.)
|
|
200
|
+
function echoesWorkspace(key, value, workspaceName) {
|
|
201
|
+
if (!workspaceName || typeof value !== 'string') return false;
|
|
202
|
+
if (/workspace/i.test(key)) return false;
|
|
203
|
+
return value.trim().toLowerCase() === String(workspaceName).trim().toLowerCase();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// A field with an `enum` declares a closed vocabulary, so an extracted value
|
|
207
|
+
// outside it is checkably wrong — not a judgement call about meaning. This is
|
|
208
|
+
// the only class of hallucinated identifier the orchestrator can reject
|
|
209
|
+
// without knowing anything about the business domain, which is exactly why
|
|
210
|
+
// agents must publish the vocabulary instead of a bare string.
|
|
211
|
+
function violatesEnum(schemaEntry, value) {
|
|
212
|
+
const values = schemaEntry?.enum;
|
|
213
|
+
if (!Array.isArray(values) || values.length === 0) return false;
|
|
214
|
+
return !values.some((allowed) => allowed === value);
|
|
215
|
+
}
|
|
216
|
+
|
|
174
217
|
export function missingRequiredArguments(schema, args) {
|
|
175
218
|
const required = Array.isArray(schema?.required) ? schema.required.map(String) : [];
|
|
176
219
|
const values = args && typeof args === 'object' && !Array.isArray(args) ? args : {};
|
|
@@ -194,12 +237,14 @@ function safeParseArgumentObject(text) {
|
|
|
194
237
|
}
|
|
195
238
|
}
|
|
196
239
|
|
|
197
|
-
function pruneArgumentsToSchema(value, schema) {
|
|
240
|
+
function pruneArgumentsToSchema(value, schema, workspaceName = '') {
|
|
198
241
|
const allowed = schema.properties ?? {};
|
|
199
242
|
const acceptsExtra = schema.additionalProperties !== false;
|
|
200
243
|
const out = {};
|
|
201
244
|
for (const [key, entry] of Object.entries(value)) {
|
|
202
245
|
if (entry === undefined || entry === null) continue;
|
|
246
|
+
if (echoesWorkspace(key, entry, workspaceName)) continue;
|
|
247
|
+
if (violatesEnum(allowed[key], entry)) continue;
|
|
203
248
|
if (Object.hasOwn(allowed, key) || acceptsExtra) out[key] = entry;
|
|
204
249
|
}
|
|
205
250
|
return out;
|
|
@@ -260,6 +305,12 @@ export function ensureInteractiveAssistantMessage(session, response, { turnId, w
|
|
|
260
305
|
return true;
|
|
261
306
|
}
|
|
262
307
|
|
|
308
|
+
export function mcpStatusNeedsRefresh(mcpStatus) {
|
|
309
|
+
return Object.values(mcpStatus ?? {}).some(
|
|
310
|
+
(endpoint) => endpoint?.url && endpoint.status !== 'connected',
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
263
314
|
export async function forwardRuntimeApproval(getWorkspaceContext, request = {}) {
|
|
264
315
|
const context = await getWorkspaceContext(request.workspace ?? null);
|
|
265
316
|
return context.approvalManager?.approve(request) ?? { approved: false };
|
|
@@ -1023,6 +1074,7 @@ async function runRuntime(argv, agent) {
|
|
|
1023
1074
|
llm: session.llm,
|
|
1024
1075
|
objective,
|
|
1025
1076
|
capability: provider.capability,
|
|
1077
|
+
workspace: session.workspace ?? context.workspace ?? '',
|
|
1026
1078
|
signal: session._abortSignal,
|
|
1027
1079
|
});
|
|
1028
1080
|
const missingArguments = missingRequiredArguments(
|
|
@@ -1255,6 +1307,14 @@ async function runRuntime(argv, agent) {
|
|
|
1255
1307
|
async function executeInteractiveTurn(context, body, { signal, turnId } = {}) {
|
|
1256
1308
|
const input = String(body.input ?? body.prompt ?? '').trim();
|
|
1257
1309
|
if (!input) throw new Error('Missing input.');
|
|
1310
|
+
// The runtime may start while optional agents are still stopped. `/start
|
|
1311
|
+
// agents` happens in the shell process, so its refreshed MCP snapshot does
|
|
1312
|
+
// not mutate this long-lived runtime context. Re-probe only while at least
|
|
1313
|
+
// one configured endpoint is disconnected; once connected, mcpRequest's
|
|
1314
|
+
// stale-session recovery handles later server restarts cheaply.
|
|
1315
|
+
if (mcpStatusNeedsRefresh(context.session.mcp)) {
|
|
1316
|
+
await refreshMcpRuntimeStatus(context.session);
|
|
1317
|
+
}
|
|
1258
1318
|
const ephemeral = createInteractiveSession(context, { runtimeUrl: selfRuntimeUrl, turnId, signal });
|
|
1259
1319
|
// Seed from a freshly reduced COPY of persisted events. Interactive turn
|
|
1260
1320
|
// events deliberately do not mutate the canonical run projection, so the
|
|
@@ -1387,6 +1447,18 @@ function logImageRefreshErrors(imageRefresh) {
|
|
|
1387
1447
|
}
|
|
1388
1448
|
|
|
1389
1449
|
export async function runCli(argv) {
|
|
1450
|
+
if (argv.includes('--refresh')) {
|
|
1451
|
+
if (argv.length !== 1) throw new Error('--refresh does not accept other options.');
|
|
1452
|
+
const result = spawnSync(workspaceCliPath, ['refresh'], {
|
|
1453
|
+
cwd: process.cwd(),
|
|
1454
|
+
env: process.env,
|
|
1455
|
+
stdio: 'inherit',
|
|
1456
|
+
});
|
|
1457
|
+
if (result.error) throw result.error;
|
|
1458
|
+
if (result.status !== 0) throw new Error(`Refresh failed with exit code ${result.status ?? 1}.`);
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1390
1462
|
if (argv[0] === 'runtime') {
|
|
1391
1463
|
const scaffolded = ensureManagerScaffold({ log: (message) => console.log(`[wiki-manager] ${message}`) });
|
|
1392
1464
|
if (scaffolded.length > 0) loadManagerEnv();
|
|
@@ -1506,10 +1578,8 @@ export async function runCli(argv) {
|
|
|
1506
1578
|
console.error(`Runtime unavailable: ${runtime.error}`);
|
|
1507
1579
|
}
|
|
1508
1580
|
preflight = withRuntimePreflight(preflight, runtime);
|
|
1509
|
-
//
|
|
1510
|
-
//
|
|
1511
|
-
// after this await would run while the shell is still on screen —
|
|
1512
|
-
// 0.12.9 shipped exactly that bug and killed the runtime under the user.
|
|
1581
|
+
// render() resolves at mount; the TUI owns renderer teardown. The shared
|
|
1582
|
+
// runtime deliberately survives shell exit because `serve` may use it.
|
|
1513
1583
|
await runOpenTuiShell({
|
|
1514
1584
|
agent,
|
|
1515
1585
|
packageJson,
|
|
@@ -1531,10 +1601,6 @@ export async function runCli(argv) {
|
|
|
1531
1601
|
}
|
|
1532
1602
|
}
|
|
1533
1603
|
await runShell({ agent, packageJson, runtime });
|
|
1534
|
-
if (runtime?.url) {
|
|
1535
|
-
const { shutdownOwnedRuntime } = await import('../runtime/lifecycle.js');
|
|
1536
|
-
await shutdownOwnedRuntime(runtime, { log: (message) => console.log(`[wiki-manager] ${message}`) });
|
|
1537
|
-
}
|
|
1538
1604
|
}
|
|
1539
1605
|
|
|
1540
1606
|
// Missing/stopped agents are status information, never a reason to interrupt
|
|
@@ -3,6 +3,7 @@ import test from 'node:test';
|
|
|
3
3
|
import {
|
|
4
4
|
buildExecutorOnlyFragment,
|
|
5
5
|
forwardRuntimeApproval,
|
|
6
|
+
mcpStatusNeedsRefresh,
|
|
6
7
|
missingRequiredArguments,
|
|
7
8
|
resolveExecutorArguments,
|
|
8
9
|
resolvePreparedDelegationApproval,
|
|
@@ -33,6 +34,20 @@ test('startup never opens the setup wizard just because agents are stopped', ()
|
|
|
33
34
|
assert.deepEqual(startupWizardGaps([{ kind: 'agents' }]), []);
|
|
34
35
|
});
|
|
35
36
|
|
|
37
|
+
test('interactive runtime refreshes configured MCP endpoints that started late', () => {
|
|
38
|
+
assert.equal(mcpStatusNeedsRefresh({
|
|
39
|
+
wiki: { url: 'http://127.0.0.1:3201/mcp', status: 'connected' },
|
|
40
|
+
cme: { url: 'http://127.0.0.1:3336/mcp/', status: 'configured' },
|
|
41
|
+
}), true);
|
|
42
|
+
assert.equal(mcpStatusNeedsRefresh({
|
|
43
|
+
wiki: { url: 'http://127.0.0.1:3201/mcp', status: 'connected' },
|
|
44
|
+
cme: { url: 'http://127.0.0.1:3336/mcp/', status: 'connected' },
|
|
45
|
+
}), false);
|
|
46
|
+
assert.equal(mcpStatusNeedsRefresh({
|
|
47
|
+
disabled: { url: null, status: 'missing' },
|
|
48
|
+
}), false);
|
|
49
|
+
});
|
|
50
|
+
|
|
36
51
|
test('executor-only capabilities receive one manager-authored executable task', () => {
|
|
37
52
|
const fragment = buildExecutorOnlyFragment({
|
|
38
53
|
objective: 'donne-moi mes derniers mails',
|
|
@@ -191,3 +206,145 @@ test('prepared delegation only approves when autoApprove is explicitly true', ()
|
|
|
191
206
|
result: { approved: true },
|
|
192
207
|
});
|
|
193
208
|
});
|
|
209
|
+
|
|
210
|
+
const EXPORT_CAPABILITY = {
|
|
211
|
+
description: 'Export configured sources to workspace markdown files.',
|
|
212
|
+
inputSchema: {
|
|
213
|
+
type: 'object',
|
|
214
|
+
additionalProperties: true,
|
|
215
|
+
properties: { source_name: { type: 'string' } },
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
test('argument extraction drops a value that only echoes the active workspace', async () => {
|
|
220
|
+
// Regression: "exporter les pages Confluence du workspace acpi" against a
|
|
221
|
+
// schema whose single free-text field is source_name. The model binds the
|
|
222
|
+
// workspace name to it, and the executor fails with "source 'acpi' not
|
|
223
|
+
// found". The workspace is already bound out of band, so the echo is noise.
|
|
224
|
+
const llm = {
|
|
225
|
+
completeWithTools: async () => ({
|
|
226
|
+
tool_calls: [{
|
|
227
|
+
function: { name: 'set_task_arguments', arguments: JSON.stringify({ source_name: 'acpi' }) },
|
|
228
|
+
}],
|
|
229
|
+
}),
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
assert.deepEqual(
|
|
233
|
+
await resolveExecutorArguments({
|
|
234
|
+
llm,
|
|
235
|
+
objective: 'exporter les pages Confluence du workspace acpi',
|
|
236
|
+
capability: EXPORT_CAPABILITY,
|
|
237
|
+
workspace: 'acpi',
|
|
238
|
+
}),
|
|
239
|
+
{},
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('argument extraction keeps a real value that is not the workspace name', async () => {
|
|
244
|
+
const llm = {
|
|
245
|
+
completeWithTools: async () => ({
|
|
246
|
+
tool_calls: [{
|
|
247
|
+
function: {
|
|
248
|
+
name: 'set_task_arguments',
|
|
249
|
+
arguments: JSON.stringify({ source_name: 'EAS_Avant_projet_ACPI' }),
|
|
250
|
+
},
|
|
251
|
+
}],
|
|
252
|
+
}),
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
assert.deepEqual(
|
|
256
|
+
await resolveExecutorArguments({
|
|
257
|
+
llm,
|
|
258
|
+
objective: 'exporter la source EAS_Avant_projet_ACPI',
|
|
259
|
+
capability: EXPORT_CAPABILITY,
|
|
260
|
+
workspace: 'acpi',
|
|
261
|
+
}),
|
|
262
|
+
{ source_name: 'EAS_Avant_projet_ACPI' },
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('argument extraction tells the model the workspace is already bound', async () => {
|
|
267
|
+
let seenSystem = '';
|
|
268
|
+
const llm = {
|
|
269
|
+
completeWithTools: async ({ system }) => {
|
|
270
|
+
seenSystem = system;
|
|
271
|
+
return { tool_calls: [] };
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
await resolveExecutorArguments({
|
|
276
|
+
llm,
|
|
277
|
+
objective: 'exporter les pages du workspace acpi',
|
|
278
|
+
capability: EXPORT_CAPABILITY,
|
|
279
|
+
workspace: 'acpi',
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
assert.match(seenSystem, /already runs against workspace "acpi"/);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('argument extraction rejects a value outside the closed vocabulary', async () => {
|
|
286
|
+
// Regression: "exporter les pages Confluence du workspace juno" — the
|
|
287
|
+
// workspace guard removes "juno", so the model reaches for the next noun and
|
|
288
|
+
// emits "Confluence". Only the agent knows the valid names; once it publishes
|
|
289
|
+
// them as an enum, the orchestrator can check without guessing at meaning.
|
|
290
|
+
const capability = {
|
|
291
|
+
description: 'Export configured sources.',
|
|
292
|
+
inputSchema: {
|
|
293
|
+
type: 'object',
|
|
294
|
+
additionalProperties: true,
|
|
295
|
+
properties: {
|
|
296
|
+
source_name: { type: 'string', enum: ['EAS_Avant_projet_ACPI'] },
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
const llm = {
|
|
301
|
+
completeWithTools: async () => ({
|
|
302
|
+
tool_calls: [{
|
|
303
|
+
function: { name: 'set_task_arguments', arguments: JSON.stringify({ source_name: 'Confluence' }) },
|
|
304
|
+
}],
|
|
305
|
+
}),
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
assert.deepEqual(
|
|
309
|
+
await resolveExecutorArguments({
|
|
310
|
+
llm,
|
|
311
|
+
objective: 'exporter les pages Confluence du workspace juno',
|
|
312
|
+
capability,
|
|
313
|
+
workspace: 'juno',
|
|
314
|
+
}),
|
|
315
|
+
{},
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('argument extraction keeps a value the vocabulary allows', async () => {
|
|
320
|
+
const capability = {
|
|
321
|
+
description: 'Export configured sources.',
|
|
322
|
+
inputSchema: {
|
|
323
|
+
type: 'object',
|
|
324
|
+
additionalProperties: true,
|
|
325
|
+
properties: {
|
|
326
|
+
source_name: { type: 'string', enum: ['EAS_Avant_projet_ACPI', 'autre'] },
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
const llm = {
|
|
331
|
+
completeWithTools: async () => ({
|
|
332
|
+
tool_calls: [{
|
|
333
|
+
function: {
|
|
334
|
+
name: 'set_task_arguments',
|
|
335
|
+
arguments: JSON.stringify({ source_name: 'EAS_Avant_projet_ACPI' }),
|
|
336
|
+
},
|
|
337
|
+
}],
|
|
338
|
+
}),
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
assert.deepEqual(
|
|
342
|
+
await resolveExecutorArguments({
|
|
343
|
+
llm,
|
|
344
|
+
objective: 'exporter la source EAS_Avant_projet_ACPI',
|
|
345
|
+
capability,
|
|
346
|
+
workspace: 'acpi',
|
|
347
|
+
}),
|
|
348
|
+
{ source_name: 'EAS_Avant_projet_ACPI' },
|
|
349
|
+
);
|
|
350
|
+
});
|
package/src/commands/slash.js
CHANGED
|
@@ -432,6 +432,14 @@ export function compactMcpStatus(mcpStatus) {
|
|
|
432
432
|
return entries
|
|
433
433
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
434
434
|
.map(([name, endpoint]) => {
|
|
435
|
+
// `configured` covers two very different situations: an endpoint we
|
|
436
|
+
// never probed (service stopped) and one whose probe failed. Reporting
|
|
437
|
+
// both as "configured" is what makes a live agent look unconfigured —
|
|
438
|
+
// surface the failure as its own state, with the cause one command away
|
|
439
|
+
// (`/mcp status` prints the full toolsError).
|
|
440
|
+
if (endpoint.status !== 'connected' && endpoint.toolError) {
|
|
441
|
+
return `✕ ${name} :${mcpPort(endpoint)} unreachable (/mcp status)`;
|
|
442
|
+
}
|
|
435
443
|
const marker = endpoint.status === 'connected'
|
|
436
444
|
? '●'
|
|
437
445
|
: endpoint.status === 'configured'
|
|
@@ -684,6 +692,7 @@ Usage:
|
|
|
684
692
|
Options:
|
|
685
693
|
-v, --version Print version
|
|
686
694
|
-h, --help Print help
|
|
695
|
+
--refresh Stop runtime and project containers; remove project images
|
|
687
696
|
--cacert <path> Trust a local CA; Docker must be able to read this host path
|
|
688
697
|
--once <prompt> Run one agent turn and exit
|
|
689
698
|
--headless Run a workspace task non-interactively
|
package/src/core/buildInfo.json
CHANGED