@dotdrelle/wiki-manager 0.14.20 → 0.14.23
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 +28 -0
- package/README.md +97 -11
- package/agents.docker-compose.yml +41 -0
- package/docker-compose.yml +11 -1
- package/mcp.endpoints.example.json +3 -3
- package/package.json +1 -2
- package/src/agent/graph.js +91 -11
- package/src/agent/graph.test.js +108 -6
- package/src/cli/runtimeStartup.test.js +14 -0
- package/src/cli/wiki-manager.js +190 -29
- package/src/cli/wiki-manager.test.js +91 -0
- package/src/commands/slash.js +58 -0
- package/src/commands/slash.test.js +18 -0
- package/src/core/activity.js +1 -1
- package/src/core/activity.test.js +5 -0
- package/src/core/buildInfo.json +2 -2
- package/src/core/compose.js +7 -1
- package/src/core/dockerCompose.test.js +27 -3
- package/src/core/env.js +16 -3
- package/src/core/env.test.js +8 -3
- package/src/core/mcp.js +7 -2
- package/src/core/mcp.test.js +55 -0
- package/src/core/wikiSetup.js +26 -2
- package/src/core/wikiWorkspace.test.js +25 -0
- package/src/orchestrator/dispatcher.js +1 -1
- package/src/orchestrator/dispatcher.test.js +48 -0
- package/src/runtime/client.js +23 -0
- package/src/shell/LeftPane.tsx +20 -1
- package/src/shell/repl.js +86 -9
- package/src/shell/repl.test.js +90 -2
- package/src/shell/useAgent.ts +8 -12
- package/tsconfig.json +2 -1
- package/wiki-workspace +110 -18
- package/agents.docker-compose.mailer.example.yml +0 -36
package/src/agent/graph.test.js
CHANGED
|
@@ -403,7 +403,8 @@ test('Donna refuses to delegate connector authentication to an export capability
|
|
|
403
403
|
globalThis.fetch = async (url, options = {}) => {
|
|
404
404
|
fetchedUrls.push(String(url));
|
|
405
405
|
const body = JSON.parse(String(options.body ?? '{}'));
|
|
406
|
-
assert.equal(body.params?.name, '
|
|
406
|
+
assert.equal(body.params?.name, 'connectors_google_oauth_start');
|
|
407
|
+
assert.deepEqual(body.params?.arguments, { workspace: 'docs' });
|
|
407
408
|
return {
|
|
408
409
|
ok: true,
|
|
409
410
|
status: 200,
|
|
@@ -415,16 +416,16 @@ test('Donna refuses to delegate connector authentication to an export capability
|
|
|
415
416
|
const session = sessionBase({
|
|
416
417
|
runtime: { url: 'http://runtime.test' },
|
|
417
418
|
mcp: {
|
|
418
|
-
|
|
419
|
+
connectors: {
|
|
419
420
|
status: 'connected',
|
|
420
421
|
url: 'http://google.test/mcp',
|
|
421
422
|
tools: [
|
|
422
423
|
{
|
|
423
|
-
name: '
|
|
424
|
+
name: 'connectors_google_oauth_start',
|
|
424
425
|
description: 'Manually initiate Google OAuth authentication flow.',
|
|
425
426
|
inputSchema: { type: 'object', additionalProperties: true },
|
|
426
427
|
},
|
|
427
|
-
{ name: '
|
|
428
|
+
{ name: 'connectors_google_status', inputSchema: { type: 'object', additionalProperties: true } },
|
|
428
429
|
],
|
|
429
430
|
},
|
|
430
431
|
},
|
|
@@ -439,7 +440,7 @@ test('Donna refuses to delegate connector authentication to an export capability
|
|
|
439
440
|
if (turn === 2) return {
|
|
440
441
|
content: null,
|
|
441
442
|
message: { role: 'assistant', content: null },
|
|
442
|
-
tool_calls: [{ id: 'google-auth', type: 'function', function: { name: '
|
|
443
|
+
tool_calls: [{ id: 'google-auth', type: 'function', function: { name: 'connectors__connectors_google_oauth_start', arguments: '{}' } }],
|
|
443
444
|
};
|
|
444
445
|
return {
|
|
445
446
|
content: 'J’ai lancé l’authentification. Ouvre le lien fourni.',
|
|
@@ -1146,7 +1147,8 @@ test('agent graph accepts plan steps with known capabilities and null capability
|
|
|
1146
1147
|
test('buildAgentSystemPrompt assigns capability resolution exclusively to the runtime', () => {
|
|
1147
1148
|
const withAgents = buildAgentSystemPrompt({ session: sessionBase({ agentRegistrySnapshot: orchestrableAgentSnapshot() }) });
|
|
1148
1149
|
assert.match(withAgents, /call runtime__delegate with the user objective only/);
|
|
1149
|
-
assert.match(withAgents, /
|
|
1150
|
+
assert.match(withAgents, /executor-only single-task agents/);
|
|
1151
|
+
assert.match(withAgents, /Never choose those identifiers yourself/);
|
|
1150
1152
|
assert.doesNotMatch(withAgents, /ONLY values allowed in requiredCapability/);
|
|
1151
1153
|
});
|
|
1152
1154
|
|
|
@@ -1310,6 +1312,106 @@ test('runtime action retries a text-only hallucination and requires a real tool
|
|
|
1310
1312
|
}
|
|
1311
1313
|
});
|
|
1312
1314
|
|
|
1315
|
+
test('interactive action delegates after a connector status check did not execute the requested collection', async () => {
|
|
1316
|
+
const originalFetch = globalThis.fetch;
|
|
1317
|
+
let delegatedObjective = null;
|
|
1318
|
+
globalThis.fetch = async (url, options = {}) => {
|
|
1319
|
+
if (String(url).includes('/delegate')) {
|
|
1320
|
+
const body = JSON.parse(String(options.body ?? '{}'));
|
|
1321
|
+
delegatedObjective = body.objective;
|
|
1322
|
+
return {
|
|
1323
|
+
ok: true,
|
|
1324
|
+
status: 202,
|
|
1325
|
+
json: async () => ({
|
|
1326
|
+
accepted: true,
|
|
1327
|
+
runId: 'collect-run',
|
|
1328
|
+
delegation: { tasks: 1, agent: 'connectors' },
|
|
1329
|
+
}),
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
return {
|
|
1333
|
+
ok: true,
|
|
1334
|
+
status: 200,
|
|
1335
|
+
headers: { get: () => null },
|
|
1336
|
+
text: async () => JSON.stringify({
|
|
1337
|
+
result: { content: [{ type: 'text', text: '{"ok":true,"status":"configured"}' }] },
|
|
1338
|
+
}),
|
|
1339
|
+
};
|
|
1340
|
+
};
|
|
1341
|
+
let modelCalls = 0;
|
|
1342
|
+
const session = sessionBase({
|
|
1343
|
+
runtime: { url: 'http://runtime.test' },
|
|
1344
|
+
mcp: {
|
|
1345
|
+
connectors: {
|
|
1346
|
+
status: 'connected',
|
|
1347
|
+
url: 'http://connectors.test/mcp/',
|
|
1348
|
+
tools: [{
|
|
1349
|
+
name: 'connectors_google_status',
|
|
1350
|
+
description: 'Read Google authorization status.',
|
|
1351
|
+
inputSchema: { type: 'object', properties: { workspace: { type: 'string' } } },
|
|
1352
|
+
}],
|
|
1353
|
+
},
|
|
1354
|
+
},
|
|
1355
|
+
llm: {
|
|
1356
|
+
async completeWithTools({ tools }) {
|
|
1357
|
+
if (tools.some((tool) => tool.function.name === 'classify_action_request')) {
|
|
1358
|
+
return {
|
|
1359
|
+
content: null,
|
|
1360
|
+
tool_calls: [{
|
|
1361
|
+
id: 'classification',
|
|
1362
|
+
type: 'function',
|
|
1363
|
+
function: { name: 'classify_action_request', arguments: '{"action":true}' },
|
|
1364
|
+
}],
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
modelCalls += 1;
|
|
1368
|
+
if (modelCalls === 1) {
|
|
1369
|
+
return {
|
|
1370
|
+
content: null,
|
|
1371
|
+
message: { role: 'assistant', content: null },
|
|
1372
|
+
tool_calls: [{
|
|
1373
|
+
id: 'status',
|
|
1374
|
+
type: 'function',
|
|
1375
|
+
function: { name: 'connectors__connectors_google_status', arguments: '{"workspace":"docs"}' },
|
|
1376
|
+
}],
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
if (modelCalls === 2) {
|
|
1380
|
+
return {
|
|
1381
|
+
content: 'Le statut est configuré mais je ne peux pas collecter.',
|
|
1382
|
+
message: { role: 'assistant', content: 'Le statut est configuré mais je ne peux pas collecter.' },
|
|
1383
|
+
tool_calls: null,
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
if (modelCalls === 3) {
|
|
1387
|
+
return {
|
|
1388
|
+
content: null,
|
|
1389
|
+
message: { role: 'assistant', content: null },
|
|
1390
|
+
tool_calls: [{
|
|
1391
|
+
id: 'delegate',
|
|
1392
|
+
type: 'function',
|
|
1393
|
+
function: { name: 'runtime__delegate', arguments: '{"objective":"charge les 10 derniers mails"}' },
|
|
1394
|
+
}],
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
return {
|
|
1398
|
+
content: 'Collecte lancée.',
|
|
1399
|
+
message: { role: 'assistant', content: 'Collecte lancée.' },
|
|
1400
|
+
tool_calls: null,
|
|
1401
|
+
};
|
|
1402
|
+
},
|
|
1403
|
+
},
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
try {
|
|
1407
|
+
const result = await createAgentGraph().invoke({ input: 'charge les 10 derniers mails', session });
|
|
1408
|
+
assert.equal(delegatedObjective, 'charge les 10 derniers mails');
|
|
1409
|
+
assert.equal(result.response, 'Collecte lancée.');
|
|
1410
|
+
} finally {
|
|
1411
|
+
globalThis.fetch = originalFetch;
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1313
1415
|
test('agent graph auto-declares the plan from an agent_plan task-graph fragment', async () => {
|
|
1314
1416
|
// The bridge that makes parallel ingestion real: when the LLM calls
|
|
1315
1417
|
// production__agent_plan, the shell integrates the fragment as the plan
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import test from 'node:test';
|
|
4
|
+
|
|
5
|
+
test('runtime startup is not blocked by optional Docker image maintenance', async () => {
|
|
6
|
+
const source = await readFile(new URL('./wiki-manager.js', import.meta.url), 'utf8');
|
|
7
|
+
const runtimeBranch = source.slice(
|
|
8
|
+
source.indexOf("if (argv[0] === 'runtime')"),
|
|
9
|
+
source.indexOf("if (argv.includes('--setup-wizard'))"),
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
assert.match(runtimeBranch, /await runRuntime\(argv\.slice\(1\), agent\)/);
|
|
13
|
+
assert.doesNotMatch(runtimeBranch, /refreshRunningContainers/);
|
|
14
|
+
});
|
package/src/cli/wiki-manager.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { dirname, join, resolve } from 'node:path';
|
|
@@ -25,7 +26,7 @@ import { listWorkspaces } from '../core/workspaces.js';
|
|
|
25
26
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
26
27
|
const packageJsonPath = resolve(__dirname, '../../package.json');
|
|
27
28
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
28
|
-
const SHELL_COMMANDS = ['help', 'version', 'exit', 'workspace', 'new', 'use', 'config', 'status', 'services', 'start', 'stop', 'logs', 'mcp', 'wiki', 'skills', 'clear', 'chat', 'agent', 'approve'];
|
|
29
|
+
const SHELL_COMMANDS = ['help', 'version', 'exit', 'workspace', 'new', 'use', 'config', 'status', 'services', 'start', 'stop', 'logs', 'mcp', 'connector', 'wiki', 'skills', 'clear', 'chat', 'agent', 'approve'];
|
|
29
30
|
|
|
30
31
|
function valueAfter(argv, flag) {
|
|
31
32
|
const index = argv.indexOf(flag);
|
|
@@ -54,6 +55,144 @@ function unavailableRuntime(err) {
|
|
|
54
55
|
return { url: null, error: reason };
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
export function buildExecutorOnlyFragment({ objective, workspace, selection }) {
|
|
59
|
+
const provider = selection.provider;
|
|
60
|
+
const capability = provider.capability ?? {};
|
|
61
|
+
const agentInstanceId = String(provider.agentInstanceId ?? provider.serverName);
|
|
62
|
+
const label = String(objective ?? '').trim() || `${selection.operation} ${selection.capability}`;
|
|
63
|
+
const mutationClass = typeof capability.mutationClass === 'string'
|
|
64
|
+
? capability.mutationClass
|
|
65
|
+
: null;
|
|
66
|
+
const requiresApproval = capability.defaultRequiresApproval === true || Boolean(mutationClass);
|
|
67
|
+
return {
|
|
68
|
+
contractVersion: String(provider.description?.contractVersion ?? '1'),
|
|
69
|
+
agentInstanceId,
|
|
70
|
+
capability: selection.capability,
|
|
71
|
+
summary: {
|
|
72
|
+
label,
|
|
73
|
+
initialSynthesis: [],
|
|
74
|
+
estimatedTasks: 1,
|
|
75
|
+
},
|
|
76
|
+
groups: [],
|
|
77
|
+
tasks: [{
|
|
78
|
+
id: `${selection.operation}-${randomUUID()}`,
|
|
79
|
+
label,
|
|
80
|
+
requiredCapability: selection.capability,
|
|
81
|
+
operation: selection.operation,
|
|
82
|
+
arguments: selection.arguments ?? {},
|
|
83
|
+
dependsOn: [],
|
|
84
|
+
parallelizable: false,
|
|
85
|
+
inputRefs: [],
|
|
86
|
+
expectedOutputRefs: [],
|
|
87
|
+
locks: [`${selection.capability}:${String(workspace)}`],
|
|
88
|
+
requiresApproval,
|
|
89
|
+
...(mutationClass ? { approvalClass: mutationClass } : {}),
|
|
90
|
+
...(requiresApproval ? { approvalSummary: label } : {}),
|
|
91
|
+
idempotencyKey: randomUUID(),
|
|
92
|
+
progressWeight: 1,
|
|
93
|
+
}],
|
|
94
|
+
expectedOutputs: [],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Fill a single-task executor's arguments from the natural-language objective,
|
|
100
|
+
* generically — against the capability's own declared `inputSchema`, with no
|
|
101
|
+
* per-agent or per-provider knowledge in the manager. This lets Donna honour
|
|
102
|
+
* stated constraints ("les 10 derniers mails", "de LinkedIn") while keeping
|
|
103
|
+
* `runtime__delegate` agnostic (it still only carries the objective).
|
|
104
|
+
*
|
|
105
|
+
* Degrades gracefully (cf. provider compatibility): forced tool_choice first,
|
|
106
|
+
* then a JSON-text completion, then no arguments — the executor uses its own
|
|
107
|
+
* defaults. It never throws and never invents identifiers.
|
|
108
|
+
*/
|
|
109
|
+
export async function resolveExecutorArguments({ llm, objective, capability, signal } = {}) {
|
|
110
|
+
const schema = capability?.inputSchema;
|
|
111
|
+
const objectiveText = String(objective ?? '').trim();
|
|
112
|
+
if (
|
|
113
|
+
!objectiveText
|
|
114
|
+
|| !llm
|
|
115
|
+
|| typeof llm.completeWithTools !== 'function'
|
|
116
|
+
|| !schema
|
|
117
|
+
|| typeof schema !== 'object'
|
|
118
|
+
|| !schema.properties
|
|
119
|
+
|| typeof schema.properties !== 'object'
|
|
120
|
+
|| Object.keys(schema.properties).length === 0
|
|
121
|
+
) {
|
|
122
|
+
return {};
|
|
123
|
+
}
|
|
124
|
+
const system = [
|
|
125
|
+
'Extract structured arguments for a task from the user objective.',
|
|
126
|
+
'Only fill a field when the objective explicitly states or clearly implies its value.',
|
|
127
|
+
'Omit every field that is not stated. Never invent identifiers, queries, filters or counts.',
|
|
128
|
+
'Return the arguments object only.',
|
|
129
|
+
].join('\n');
|
|
130
|
+
const tool = {
|
|
131
|
+
type: 'function',
|
|
132
|
+
function: {
|
|
133
|
+
name: 'set_task_arguments',
|
|
134
|
+
description: String(capability.description ?? 'Task arguments for the selected capability.'),
|
|
135
|
+
parameters: {
|
|
136
|
+
type: 'object',
|
|
137
|
+
additionalProperties: schema.additionalProperties ?? false,
|
|
138
|
+
properties: schema.properties,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
const messages = [{ role: 'user', content: objectiveText }];
|
|
143
|
+
try {
|
|
144
|
+
const result = await llm.completeWithTools({
|
|
145
|
+
system,
|
|
146
|
+
tools: [tool],
|
|
147
|
+
toolChoice: { type: 'function', function: { name: 'set_task_arguments' } },
|
|
148
|
+
messages,
|
|
149
|
+
signal,
|
|
150
|
+
});
|
|
151
|
+
const call = (result?.tool_calls ?? []).find((item) => item?.function?.name === 'set_task_arguments');
|
|
152
|
+
const fromCall = call ? safeParseArgumentObject(call.function?.arguments) : null;
|
|
153
|
+
if (fromCall) return pruneArgumentsToSchema(fromCall, schema);
|
|
154
|
+
const fromText = safeParseArgumentObject(result?.content);
|
|
155
|
+
if (fromText) return pruneArgumentsToSchema(fromText, schema);
|
|
156
|
+
} catch {
|
|
157
|
+
// Fall through to the tool-less path.
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const result = await llm.completeWithTools({
|
|
161
|
+
system: `${system}\nReturn a single JSON object only.`,
|
|
162
|
+
tools: [],
|
|
163
|
+
messages,
|
|
164
|
+
signal,
|
|
165
|
+
});
|
|
166
|
+
const fromText = safeParseArgumentObject(result?.content);
|
|
167
|
+
if (fromText) return pruneArgumentsToSchema(fromText, schema);
|
|
168
|
+
} catch {
|
|
169
|
+
// Give up: the executor will use its own defaults.
|
|
170
|
+
}
|
|
171
|
+
return {};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function safeParseArgumentObject(text) {
|
|
175
|
+
const cleaned = String(text ?? '').trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '');
|
|
176
|
+
if (!cleaned) return null;
|
|
177
|
+
try {
|
|
178
|
+
const value = JSON.parse(cleaned);
|
|
179
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function pruneArgumentsToSchema(value, schema) {
|
|
186
|
+
const allowed = schema.properties ?? {};
|
|
187
|
+
const acceptsExtra = schema.additionalProperties !== false;
|
|
188
|
+
const out = {};
|
|
189
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
190
|
+
if (entry === undefined || entry === null) continue;
|
|
191
|
+
if (Object.hasOwn(allowed, key) || acceptsExtra) out[key] = entry;
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
|
|
57
196
|
function createSession() {
|
|
58
197
|
return {
|
|
59
198
|
workspace: null,
|
|
@@ -831,34 +970,60 @@ async function runRuntime(argv, agent) {
|
|
|
831
970
|
}
|
|
832
971
|
const provider = selection.provider;
|
|
833
972
|
let planResult;
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
),
|
|
850
|
-
|
|
973
|
+
const orchestration = provider.description?.orchestration ?? {};
|
|
974
|
+
const canPlan = orchestration.canPlan !== false;
|
|
975
|
+
const singleTaskExecutor = orchestration.canPlan === false
|
|
976
|
+
&& orchestration.singleTaskOnly === true;
|
|
977
|
+
let fragment;
|
|
978
|
+
if (canPlan) {
|
|
979
|
+
try {
|
|
980
|
+
planResult = await callMcpTool(
|
|
981
|
+
session.mcp,
|
|
982
|
+
provider.serverName,
|
|
983
|
+
'agent_plan',
|
|
984
|
+
{
|
|
985
|
+
capability: selection.capability,
|
|
986
|
+
operation: selection.operation,
|
|
987
|
+
objective,
|
|
988
|
+
workspace: { revision: String(Date.now()) },
|
|
989
|
+
constraints: {
|
|
990
|
+
maxConcurrency: resolveCapabilityConcurrency(
|
|
991
|
+
provider,
|
|
992
|
+
undefined,
|
|
993
|
+
process.env.WIKI_MANAGER_CAPABILITY_CONCURRENCY,
|
|
994
|
+
),
|
|
995
|
+
requireApprovalForMutations: true,
|
|
996
|
+
},
|
|
851
997
|
},
|
|
998
|
+
);
|
|
999
|
+
} catch (err) {
|
|
1000
|
+
const endpoint = session.mcp?.[provider.serverName]?.url ?? 'unknown endpoint';
|
|
1001
|
+
throw new Error(
|
|
1002
|
+
`Delegation failed during agent_plan: provider=${provider.serverName} endpoint=${endpoint} ${errorDiagnostic(err)}`,
|
|
1003
|
+
{ cause: err },
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
fragment = parseJsonText(formatMcpToolResult(planResult));
|
|
1007
|
+
} else if (singleTaskExecutor) {
|
|
1008
|
+
const extractedArguments = await resolveExecutorArguments({
|
|
1009
|
+
llm: session.llm,
|
|
1010
|
+
objective,
|
|
1011
|
+
capability: provider.capability,
|
|
1012
|
+
signal: session._abortSignal,
|
|
1013
|
+
});
|
|
1014
|
+
fragment = buildExecutorOnlyFragment({
|
|
1015
|
+
objective,
|
|
1016
|
+
workspace: session.workspace ?? context.workspace ?? 'workspace',
|
|
1017
|
+
selection: {
|
|
1018
|
+
...selection,
|
|
1019
|
+
arguments: { ...(selection.arguments ?? {}), ...extractedArguments },
|
|
852
1020
|
},
|
|
853
|
-
);
|
|
854
|
-
}
|
|
855
|
-
const endpoint = session.mcp?.[provider.serverName]?.url ?? 'unknown endpoint';
|
|
1021
|
+
});
|
|
1022
|
+
} else {
|
|
856
1023
|
throw new Error(
|
|
857
|
-
`
|
|
858
|
-
{ cause: err },
|
|
1024
|
+
`Agent ${provider.agentInstanceId ?? provider.serverName} cannot plan and does not declare singleTaskOnly:true.`,
|
|
859
1025
|
);
|
|
860
1026
|
}
|
|
861
|
-
const fragment = parseJsonText(formatMcpToolResult(planResult));
|
|
862
1027
|
if (!Array.isArray(fragment?.tasks) || fragment.tasks.length === 0) {
|
|
863
1028
|
throw new Error(fragment?.summary?.initialSynthesis?.[0] ?? `No task was planned for ${selection.capability}/${selection.operation}.`);
|
|
864
1029
|
}
|
|
@@ -1204,10 +1369,6 @@ export async function runCli(argv) {
|
|
|
1204
1369
|
if (argv[0] === 'runtime') {
|
|
1205
1370
|
const scaffolded = ensureManagerScaffold({ log: (message) => console.log(`[wiki-manager] ${message}`) });
|
|
1206
1371
|
if (scaffolded.length > 0) loadManagerEnv();
|
|
1207
|
-
const imageRefresh = await refreshRunningContainers({
|
|
1208
|
-
onStep: (message) => console.log(`[wiki-manager] ${message}`),
|
|
1209
|
-
});
|
|
1210
|
-
logImageRefreshErrors(imageRefresh);
|
|
1211
1372
|
const agent = createAgentGraph();
|
|
1212
1373
|
await runRuntime(argv.slice(1), agent);
|
|
1213
1374
|
return;
|
|
@@ -1266,7 +1427,7 @@ export async function runCli(argv) {
|
|
|
1266
1427
|
}
|
|
1267
1428
|
const { runOpenTuiShell, runStartupWizard } = await import('../shell/tui.tsx');
|
|
1268
1429
|
// Fresh directory → copy mcp.endpoints.json/.env from the packaged
|
|
1269
|
-
// examples so external agents (cme
|
|
1430
|
+
// examples so external agents (cme and documents) connect out of
|
|
1270
1431
|
// the box. Done here (and in `runtime`), NOT at import time: --version
|
|
1271
1432
|
// in a random cwd must not litter files.
|
|
1272
1433
|
const scaffolded = ensureManagerScaffold({ log: (message) => console.log(`[wiki-manager] ${message}`) });
|
|
@@ -1,10 +1,101 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import test from 'node:test';
|
|
3
3
|
import {
|
|
4
|
+
buildExecutorOnlyFragment,
|
|
4
5
|
forwardRuntimeApproval,
|
|
6
|
+
resolveExecutorArguments,
|
|
5
7
|
resolvePreparedDelegationApproval,
|
|
6
8
|
} from './wiki-manager.js';
|
|
7
9
|
|
|
10
|
+
const COLLECT_CAPABILITY = {
|
|
11
|
+
description: 'Collect content from an external connector source.',
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
additionalProperties: true,
|
|
15
|
+
properties: {
|
|
16
|
+
maxMessages: { type: 'integer' },
|
|
17
|
+
query: { type: 'string' },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
test('executor-only capabilities receive one manager-authored executable task', () => {
|
|
23
|
+
const fragment = buildExecutorOnlyFragment({
|
|
24
|
+
objective: 'donne-moi mes derniers mails',
|
|
25
|
+
workspace: 'juno',
|
|
26
|
+
selection: {
|
|
27
|
+
capability: 'external-source.collect',
|
|
28
|
+
operation: 'collect',
|
|
29
|
+
arguments: { maxMessages: 10 },
|
|
30
|
+
provider: {
|
|
31
|
+
serverName: 'connectors',
|
|
32
|
+
agentInstanceId: 'connectors-1',
|
|
33
|
+
description: {
|
|
34
|
+
contractVersion: '1',
|
|
35
|
+
orchestration: { canPlan: false, singleTaskOnly: true },
|
|
36
|
+
},
|
|
37
|
+
capability: {
|
|
38
|
+
mutationClass: 'external-source',
|
|
39
|
+
defaultRequiresApproval: true,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
assert.equal(fragment.agentInstanceId, 'connectors-1');
|
|
46
|
+
assert.equal(fragment.tasks.length, 1);
|
|
47
|
+
assert.equal(fragment.tasks[0].requiredCapability, 'external-source.collect');
|
|
48
|
+
assert.equal(fragment.tasks[0].operation, 'collect');
|
|
49
|
+
assert.deepEqual(fragment.tasks[0].arguments, { maxMessages: 10 });
|
|
50
|
+
assert.deepEqual(fragment.tasks[0].locks, ['external-source.collect:juno']);
|
|
51
|
+
assert.equal(fragment.tasks[0].requiresApproval, true);
|
|
52
|
+
assert.equal(fragment.tasks[0].approvalClass, 'external-source');
|
|
53
|
+
assert.match(fragment.tasks[0].idempotencyKey, /^[0-9a-f-]{36}$/);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('generic argument extraction fills the executor task from the objective', async () => {
|
|
57
|
+
const llm = {
|
|
58
|
+
completeWithTools: async () => ({
|
|
59
|
+
tool_calls: [{
|
|
60
|
+
function: { name: 'set_task_arguments', arguments: JSON.stringify({ maxMessages: 10 }) },
|
|
61
|
+
}],
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
const args = await resolveExecutorArguments({
|
|
65
|
+
llm,
|
|
66
|
+
objective: 'récupère les 10 derniers mails',
|
|
67
|
+
capability: COLLECT_CAPABILITY,
|
|
68
|
+
});
|
|
69
|
+
assert.deepEqual(args, { maxMessages: 10 });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('argument extraction falls back to a JSON-text completion', async () => {
|
|
73
|
+
const llm = {
|
|
74
|
+
completeWithTools: async ({ tools }) =>
|
|
75
|
+
tools.length > 0 ? { content: '' } : { content: '{"query":"from:linkedin.com"}' },
|
|
76
|
+
};
|
|
77
|
+
const args = await resolveExecutorArguments({
|
|
78
|
+
llm,
|
|
79
|
+
objective: 'les mails de LinkedIn',
|
|
80
|
+
capability: COLLECT_CAPABILITY,
|
|
81
|
+
});
|
|
82
|
+
assert.deepEqual(args, { query: 'from:linkedin.com' });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('argument extraction stays agnostic and safe when it cannot extract', async () => {
|
|
86
|
+
// No inputSchema → no extraction attempted at all.
|
|
87
|
+
assert.deepEqual(
|
|
88
|
+
await resolveExecutorArguments({ llm: { completeWithTools: async () => ({}) }, objective: 'x', capability: {} }),
|
|
89
|
+
{},
|
|
90
|
+
);
|
|
91
|
+
// Provider/LLM failure degrades to the executor's own defaults, never throws.
|
|
92
|
+
const throwing = { completeWithTools: async () => { throw new Error('gateway rejected tool_choice'); } };
|
|
93
|
+
assert.deepEqual(
|
|
94
|
+
await resolveExecutorArguments({ llm: throwing, objective: 'les 10 derniers mails', capability: COLLECT_CAPABILITY }),
|
|
95
|
+
{},
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
8
99
|
test('runtime approval bridge preserves the complete run-scoped grant', async () => {
|
|
9
100
|
let forwarded = null;
|
|
10
101
|
const request = {
|
package/src/commands/slash.js
CHANGED
|
@@ -631,6 +631,7 @@ ${helpPair('/skills', 'List skills', '/skills show <n>', 'Show skill')}
|
|
|
631
631
|
${helpPair('/skills run <n>', 'Run skill guide', '/skills edit <n>', 'Edit skill')}
|
|
632
632
|
${helpPair('/mcp status', 'MCP status', '/mcp endpoints', 'MCP endpoints')}
|
|
633
633
|
${helpPair('/mcp tools [mcp]', 'MCP tools', '/mcp call ...', 'Call MCP tool')}
|
|
634
|
+
${helpPair('/connector list', 'Connector auth status', '/connector auth <n>', 'Authorize connector')}
|
|
634
635
|
${helpPair('/upload <path>', 'Upload document', '/uploads', 'Uploaded docs')}
|
|
635
636
|
${helpPair('/upload convert pending', 'Convert pending', '/uploads clean', 'Clean uploads')}
|
|
636
637
|
${helpPair('/wiki', 'Run wiki index', '/wiki run <args>', 'Raw wiki CLI')}
|
|
@@ -1025,6 +1026,63 @@ export async function handleSlashCommand(line, context) {
|
|
|
1025
1026
|
}
|
|
1026
1027
|
return { output: 'Usage: /mcp <status|endpoints|tools|call> [mcp]' };
|
|
1027
1028
|
}
|
|
1029
|
+
case 'connector': {
|
|
1030
|
+
const subcommand = args[1] ?? 'list';
|
|
1031
|
+
const connectorResult = (output) => rawCommandResult(`/connector ${args.slice(1).join(' ')}`.trim(), output);
|
|
1032
|
+
if (!context.session.workspace) {
|
|
1033
|
+
return connectorResult('No workspace is currently loaded.');
|
|
1034
|
+
}
|
|
1035
|
+
await refreshMcpRuntimeStatus(context.session);
|
|
1036
|
+
const connectorMcp = context.session.mcp?.connectors;
|
|
1037
|
+
if (!connectorMcp || connectorMcp.status !== 'connected') {
|
|
1038
|
+
return connectorResult('The connectors service is unavailable or disabled.');
|
|
1039
|
+
}
|
|
1040
|
+
if (subcommand === 'list') {
|
|
1041
|
+
if (args[2]) return connectorResult('The connector list request has invalid extra arguments.');
|
|
1042
|
+
try {
|
|
1043
|
+
const result = await callMcpTool(
|
|
1044
|
+
context.session.mcp,
|
|
1045
|
+
'connectors',
|
|
1046
|
+
'connectors_google_status',
|
|
1047
|
+
{ workspace: context.session.workspace },
|
|
1048
|
+
);
|
|
1049
|
+
const payload = parseJsonText(formatMcpToolResult(result));
|
|
1050
|
+
const status = payload?.status === 'configured' ? 'authorized' : 'not authorized';
|
|
1051
|
+
return connectorResult(`google (Gmail read-only): ${status}`);
|
|
1052
|
+
} catch (err) {
|
|
1053
|
+
return connectorResult(`google (Gmail read-only): unavailable (${err instanceof Error ? err.message : String(err)})`);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
if (subcommand === 'auth') {
|
|
1057
|
+
const connector = String(args[2] ?? '').toLowerCase();
|
|
1058
|
+
if (!['google', 'gmail'].includes(connector)) {
|
|
1059
|
+
return connectorResult('The requested connector is unsupported. The available connector is google (Gmail read-only).');
|
|
1060
|
+
}
|
|
1061
|
+
try {
|
|
1062
|
+
const result = await callMcpTool(
|
|
1063
|
+
context.session.mcp,
|
|
1064
|
+
'connectors',
|
|
1065
|
+
'connectors_google_oauth_start',
|
|
1066
|
+
{ workspace: context.session.workspace },
|
|
1067
|
+
);
|
|
1068
|
+
const payload = parseJsonText(formatMcpToolResult(result));
|
|
1069
|
+
const authorizationUrl = payload?.authorizationUrl;
|
|
1070
|
+
if (payload?.ok !== true || typeof authorizationUrl !== 'string') {
|
|
1071
|
+
return connectorResult(`Google authorization could not start (${payload?.error ?? 'missing authorization URL'}).`);
|
|
1072
|
+
}
|
|
1073
|
+
const opener = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
1074
|
+
try {
|
|
1075
|
+
execFileSync(opener, [authorizationUrl], { stdio: 'ignore' });
|
|
1076
|
+
return connectorResult('Google authorization opened successfully in the user browser.');
|
|
1077
|
+
} catch {
|
|
1078
|
+
return connectorResult(`Google authorization requires the user to open this URL: ${authorizationUrl}`);
|
|
1079
|
+
}
|
|
1080
|
+
} catch (err) {
|
|
1081
|
+
return connectorResult(`Google authorization could not start (${err instanceof Error ? err.message : String(err)}).`);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
return connectorResult('The requested connector action is unsupported. Available actions: list and auth google.');
|
|
1085
|
+
}
|
|
1028
1086
|
case 'cancel': {
|
|
1029
1087
|
// Alias of /run cancel — people type /cancel when they want out.
|
|
1030
1088
|
const runtime = context.runtime ?? {};
|
|
@@ -93,6 +93,24 @@ test('/new without a name shows usage', async () => {
|
|
|
93
93
|
assert.match(result.output ?? '', /Usage/i);
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
+
test('/connector completions expose list and Google authorization', () => {
|
|
97
|
+
const session = { commands: ['connector'] };
|
|
98
|
+
assert.deepEqual(completionContext('/connector ', session)?.matches, ['auth', 'list']);
|
|
99
|
+
assert.deepEqual(completionContext('/connector auth ', session)?.matches, ['google']);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('/connector hands deterministic facts to Donna for localized natural-language output', async () => {
|
|
103
|
+
const result = await handleSlashCommand('/connector list', {
|
|
104
|
+
packageJson: { version: 'test' },
|
|
105
|
+
session: {},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
assert.equal(result.rawOutput, true);
|
|
109
|
+
assert.match(result.output ?? '', /No workspace is currently loaded/);
|
|
110
|
+
assert.match(result.agentTrigger ?? '', /profil workspace/);
|
|
111
|
+
assert.match(result.agentTrigger ?? '', /Ne relance pas la commande/);
|
|
112
|
+
});
|
|
113
|
+
|
|
96
114
|
test('/skills run sends the private skill body to Donna without rendering it as command output', async () => {
|
|
97
115
|
const root = await mkdtemp(join(tmpdir(), 'wiki-manager-skill-run-'));
|
|
98
116
|
const skillDir = join(root, '.wiki', 'skills');
|
package/src/core/activity.js
CHANGED
|
@@ -13,7 +13,7 @@ function basename(value) {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
function terminalStatus(status) {
|
|
16
|
-
return ['done', 'failed', 'cancelled', 'canceled', 'complete', 'completed', 'success', 'error'].includes(String(status ?? '').toLowerCase());
|
|
16
|
+
return ['done', 'failed', 'cancelled', 'canceled', 'complete', 'completed', 'success', 'succeeded', 'error'].includes(String(status ?? '').toLowerCase());
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function activityKey(activity) {
|
|
@@ -29,6 +29,11 @@ test('normalizeActivity: plan null when no plan field', () => {
|
|
|
29
29
|
assert.equal(a.plan, null);
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
+
test('normalizeActivity: succeeded is terminal', () => {
|
|
33
|
+
const a = normalizeActivity({ id: 'job-connectors', status: 'succeeded' });
|
|
34
|
+
assert.equal(a.terminal, true);
|
|
35
|
+
});
|
|
36
|
+
|
|
32
37
|
test('isCancelledStatus recognizes both cancellation spellings', () => {
|
|
33
38
|
assert.equal(isCancelledStatus('cancelled'), true);
|
|
34
39
|
assert.equal(isCancelledStatus('canceled'), true);
|
package/src/core/buildInfo.json
CHANGED