@dotdrelle/wiki-manager 0.15.93 → 0.15.94
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/docker-compose.yml +1 -1
- package/package.json +2 -2
- package/src/agent/graph.js +42 -8
- package/src/agent/graph.test.js +24 -1
- package/src/cli/wiki-manager.js +103 -1
- package/src/commands/slash.js +38 -4
- package/src/commands/slash.test.js +11 -1
- package/src/core/agentEvents.js +23 -0
- package/src/core/agentEvents.test.js +37 -0
- package/src/core/buildInfo.json +2 -2
- package/src/core/dockerCompose.test.js +14 -0
- package/src/core/mcp.js +1 -1
- package/src/core/skillCompiler.test.js +21 -1
- package/src/orchestrator/objectiveResolver.test.js +27 -0
- package/src/runtime/loginPage.js +132 -0
- package/src/runtime/loginRoutes.test.js +131 -0
- package/src/runtime/loginSession.js +226 -0
- package/src/runtime/loginSession.test.js +143 -0
- package/src/runtime/qrCode.js +15 -0
- package/src/runtime/runner.js +12 -0
- package/src/runtime/runner.test.js +58 -0
- package/src/runtime/server.js +206 -1
- package/src/runtime/totp.js +87 -0
- package/src/runtime/totp.test.js +80 -0
- package/src/runtime/totpLogin.js +123 -0
- package/src/runtime/vendor/qrcode.cjs +2297 -0
package/docker-compose.yml
CHANGED
|
@@ -130,7 +130,7 @@ services:
|
|
|
130
130
|
# error. Every compose-deployed ingest then ran without the Lot 4 barrier
|
|
131
131
|
# and left the published map stale — the very defect that work fixed.
|
|
132
132
|
# `copy` stays out on purpose: it is the legacy step, opt-in only.
|
|
133
|
-
- PRODUCTION_ALLOWED_STEPS=${PRODUCTION_ALLOWED_STEPS:-doctor,doctor_apply,ingest,ingest_plan,ingest_apply,build,export,polish,restore,pipeline}
|
|
133
|
+
- PRODUCTION_ALLOWED_STEPS=${PRODUCTION_ALLOWED_STEPS:-doctor,doctor_apply,copy,ingest,ingest_plan,ingest_apply,ingest_rebuild,build,export,polish,restore,pipeline,lint}
|
|
134
134
|
- PRODUCTION_REQUIRE_CONFIRMATION=${PRODUCTION_REQUIRE_CONFIRMATION:-false}
|
|
135
135
|
# Parallelism levers — effective concurrency ≈ recommendedConcurrency.
|
|
136
136
|
# Intermediate defaults (4/8). Low profile 2/4, high profile 8/16.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotdrelle/wiki-manager",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.94",
|
|
4
4
|
"description": "Agentic shell and orchestration cockpit for llm-wiki workspaces.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
21
|
"start": "bun ./bin/wiki-manager.js",
|
|
22
|
-
"test": "node --test src/core/skillInvocation.test.js src/core/skillCompiler.test.js src/runtime/skillRun.test.js src/runtime/controlDrain.test.js src/runtime/controlCancellation.test.js src/cli/runtimeStartup.test.js src/cli/wiki-manager.test.js src/agent/graph.test.js src/agent/skillRecursion.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/workspaceProfile.test.js src/core/buildInfo.test.js src/core/agentEvents.test.js src/core/skillChainView.test.js src/core/runtimeLog.test.js src/core/runtimeEventAdapter.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/otherWorkspacesRunning.test.js src/core/wikiSetup.test.js src/core/wikiWorkspace.test.js src/core/wikiWorkspaceStart.test.js src/core/wikirc.test.js src/core/workspaceInherit.test.js src/core/cacert.test.js src/core/composeOverrides.test.js src/core/setEnvValue.test.js src/core/commandFailure.test.js src/core/currentArtifact.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/taskStatuses.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/orchestrator/providers/fakeRuntimeProvider.test.js src/orchestrator/providers/runtimeProviders.test.js src/orchestrator/providers/dispatcherExternalRuntime.test.js src/orchestrator/providers/deepAgentsProvider.test.js src/commands/slash.test.js src/shell/repl.test.js src/shell/setupWizardModality.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/workspaceIsolation.test.js src/runtime/controlMessages.test.js src/runtime/recoveryManager.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/delegation.test.js src/runtime/runner.test.js src/runtime/runner.e2e.test.js src/runtime/skillChain.e2e.test.js src/runtime/donna-contract.test.js src/runtime/approvals.test.js src/runtime/auth.test.js",
|
|
22
|
+
"test": "node --test src/core/skillInvocation.test.js src/core/skillCompiler.test.js src/runtime/skillRun.test.js src/runtime/controlDrain.test.js src/runtime/controlCancellation.test.js src/cli/runtimeStartup.test.js src/cli/wiki-manager.test.js src/agent/graph.test.js src/agent/skillRecursion.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/workspaceProfile.test.js src/core/buildInfo.test.js src/core/agentEvents.test.js src/core/skillChainView.test.js src/core/runtimeLog.test.js src/core/runtimeEventAdapter.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/otherWorkspacesRunning.test.js src/core/wikiSetup.test.js src/core/wikiWorkspace.test.js src/core/wikiWorkspaceStart.test.js src/core/wikirc.test.js src/core/workspaceInherit.test.js src/core/cacert.test.js src/core/composeOverrides.test.js src/core/setEnvValue.test.js src/core/commandFailure.test.js src/core/currentArtifact.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/taskStatuses.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/orchestrator/providers/fakeRuntimeProvider.test.js src/orchestrator/providers/runtimeProviders.test.js src/orchestrator/providers/dispatcherExternalRuntime.test.js src/orchestrator/providers/deepAgentsProvider.test.js src/commands/slash.test.js src/shell/repl.test.js src/shell/setupWizardModality.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/workspaceIsolation.test.js src/runtime/controlMessages.test.js src/runtime/recoveryManager.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/delegation.test.js src/runtime/runner.test.js src/runtime/runner.e2e.test.js src/runtime/skillChain.e2e.test.js src/runtime/donna-contract.test.js src/runtime/approvals.test.js src/runtime/auth.test.js src/runtime/totp.test.js src/runtime/loginSession.test.js src/runtime/loginRoutes.test.js",
|
|
23
23
|
"check-versions": "node scripts/check-versions.js",
|
|
24
24
|
"prepack": "node scripts/check-versions.js",
|
|
25
25
|
"prepublishOnly": "node scripts/check-versions.js",
|
package/src/agent/graph.js
CHANGED
|
@@ -555,6 +555,27 @@ function isUnresolvedTargetFailure(rawFailure) {
|
|
|
555
555
|
return /file does not exist|does not exist|no files match/i.test(rawFailure);
|
|
556
556
|
}
|
|
557
557
|
|
|
558
|
+
// An agent that plans zero tasks is not a failure: it found nothing to act on
|
|
559
|
+
// (no pending source, no template, no deliverable, no archived source to
|
|
560
|
+
// re-file) and says so in its own synthesis sentence. Relaying that outcome is
|
|
561
|
+
// the whole point — the old generic path called it "could not be started" and
|
|
562
|
+
// sent the reader hunting a connectivity problem that did not exist.
|
|
563
|
+
export function nothingToDoForDonna(rawReason) {
|
|
564
|
+
const reason = String(rawReason ?? '')
|
|
565
|
+
.replace(/\s*Available capabilities:\s*[\s\S]*$/i, '')
|
|
566
|
+
.replace(/\b(?:provider|endpoint)=[^\s,]+/g, '')
|
|
567
|
+
.replace(/\s{2,}/g, ' ')
|
|
568
|
+
.trim()
|
|
569
|
+
.slice(0, 400);
|
|
570
|
+
return JSON.stringify({
|
|
571
|
+
delegated: false,
|
|
572
|
+
nothingToDo: true,
|
|
573
|
+
reason,
|
|
574
|
+
instruction:
|
|
575
|
+
'The agent ran its planning and found nothing to act on. Answer the user naturally in their language, stating that plainly (use the reason above), and do not call it a failure, do not suggest restarting a service, and do not retry the same action. Do not expose exception names, capability identifiers, tool names, UUIDs, or internal routing details.',
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
558
579
|
function unresolvedTargetForDonna(rawFailure) {
|
|
559
580
|
const cleaned = String(rawFailure ?? '')
|
|
560
581
|
.replace(/\b(?:provider|endpoint)=[^\s,]+/g, '')
|
|
@@ -941,14 +962,19 @@ export async function handleRuntimeControlTool(session, tool, args = {}) {
|
|
|
941
962
|
});
|
|
942
963
|
}
|
|
943
964
|
const result = await postRuntimeDelegate(objective, { url, workspace });
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
965
|
+
if (result?.runId) {
|
|
966
|
+
return JSON.stringify({
|
|
967
|
+
delegated: true,
|
|
968
|
+
runId: result.runId,
|
|
969
|
+
summary: result.delegation ?? null,
|
|
970
|
+
message: `Action started (${String(result.runId).slice(0, 8)}) after real-plan validation: ${result.delegation?.tasks ?? 0} task(s), ${result.delegation?.agent ?? 'resolved agent'}. Execution in progress.`,
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
const refused = String(result?.error ?? JSON.stringify(result));
|
|
974
|
+
// Same EMPTY_PLAN outcome as the in-run path above: relay it, don't call
|
|
975
|
+
// it a failure.
|
|
976
|
+
if (/^EMPTY_PLAN:\s*/i.test(refused)) return nothingToDoForDonna(refused.replace(/^EMPTY_PLAN:\s*/i, ''));
|
|
977
|
+
return `Delegation refused: ${refused}`;
|
|
952
978
|
}
|
|
953
979
|
if (tool === 'run_skill') {
|
|
954
980
|
const skillName = String(args.skillName ?? '').trim();
|
|
@@ -2009,6 +2035,14 @@ export function createAgentGraph(options = {}) {
|
|
|
2009
2035
|
missingRequiredFields: needsInput[1].split(',').map((item) => item.trim()).filter(Boolean),
|
|
2010
2036
|
instruction: 'Ask the user for the missing required information. Do not expose internal validation details.',
|
|
2011
2037
|
});
|
|
2038
|
+
} else if (/^EMPTY_PLAN:\s*/i.test(delegationFailure)) {
|
|
2039
|
+
// Valid planning, nothing to do (no pending source/template/
|
|
2040
|
+
// deliverable). Not a failure and not retryable — Donna relays
|
|
2041
|
+
// the agent's own sentence instead of a connectivity-sounding
|
|
2042
|
+
// generic error. Checked before the target heuristic: a plan
|
|
2043
|
+
// reason is authoritative and could mention a missing file
|
|
2044
|
+
// without meaning "unresolved target".
|
|
2045
|
+
resultText = nothingToDoForDonna(delegationFailure.replace(/^EMPTY_PLAN:\s*/i, ''));
|
|
2012
2046
|
} else if (isUnresolvedTargetFailure(delegationFailure)) {
|
|
2013
2047
|
// A named target that resolves to nothing is not an unsupported
|
|
2014
2048
|
// action: Donna can look it up and retry (or ask), so the turn
|
package/src/agent/graph.test.js
CHANGED
|
@@ -3,7 +3,7 @@ import test from 'node:test';
|
|
|
3
3
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
-
import { bareToolCallJson, buildAgentSystemPrompt, connectorConfigurationTarget, createAgentGraph, invalidSuggestedSlashCommands, invalidUserFacingToolNames, isOrchestrationBypassTool, knownCapabilityIds, normalizeToolArgumentsFromSchema } from './graph.js';
|
|
6
|
+
import { bareToolCallJson, buildAgentSystemPrompt, connectorConfigurationTarget, createAgentGraph, invalidSuggestedSlashCommands, invalidUserFacingToolNames, isOrchestrationBypassTool, knownCapabilityIds, normalizeToolArgumentsFromSchema, nothingToDoForDonna } from './graph.js';
|
|
7
7
|
|
|
8
8
|
test('user-facing response guard hides MCP identifiers generically', () => {
|
|
9
9
|
const session = sessionBase();
|
|
@@ -13,6 +13,29 @@ test('user-facing response guard hides MCP identifiers generically', () => {
|
|
|
13
13
|
);
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
+
test('an empty agent plan is relayed as "nothing to do", not as a failure', () => {
|
|
17
|
+
// The agent planned zero tasks and said why: no pending source, no template,
|
|
18
|
+
// no deliverable. That is an outcome. The old generic path called it "could
|
|
19
|
+
// not be started" and sent the reader after a connectivity problem that did
|
|
20
|
+
// not exist.
|
|
21
|
+
const payload = JSON.parse(nothingToDoForDonna(
|
|
22
|
+
'No Markdown files were found in raw/untracked; no ingest operation was planned.',
|
|
23
|
+
));
|
|
24
|
+
assert.equal(payload.delegated, false);
|
|
25
|
+
assert.equal(payload.nothingToDo, true);
|
|
26
|
+
assert.match(payload.reason, /No Markdown files were found in raw\/untracked/);
|
|
27
|
+
assert.match(payload.instruction, /do not call it a failure/i);
|
|
28
|
+
assert.doesNotMatch(payload.instruction, /EMPTY_PLAN/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('the nothing-to-do reason strips internal routing details', () => {
|
|
32
|
+
const payload = JSON.parse(nothingToDoForDonna(
|
|
33
|
+
'No templates were found. Available capabilities: document.build, knowledge.update provider=production endpoint=http://host:3202',
|
|
34
|
+
));
|
|
35
|
+
assert.doesNotMatch(payload.reason, /Available capabilities/);
|
|
36
|
+
assert.doesNotMatch(payload.reason, /provider=|endpoint=/);
|
|
37
|
+
});
|
|
38
|
+
|
|
16
39
|
test('CME setup stays direct while CME export execution stays orchestrated', () => {
|
|
17
40
|
assert.equal(isOrchestrationBypassTool('cme__cme_export_run'), true);
|
|
18
41
|
assert.equal(isOrchestrationBypassTool('cme__cme_setup'), false);
|
package/src/cli/wiki-manager.js
CHANGED
|
@@ -11,6 +11,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
11
11
|
import { spawnSync } from 'node:child_process';
|
|
12
12
|
import { readFileSync } from 'node:fs';
|
|
13
13
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
14
|
+
import { createInterface } from 'node:readline/promises';
|
|
14
15
|
import { dirname, join, resolve } from 'node:path';
|
|
15
16
|
import { fileURLToPath } from 'node:url';
|
|
16
17
|
import { ensureManagerScaffold, loadManagerEnv } from '../core/env.js';
|
|
@@ -49,6 +50,17 @@ function valueAfter(argv, flag) {
|
|
|
49
50
|
return argv[index + 1];
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
async function confirmOnTty(question, defaultValue) {
|
|
54
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return defaultValue;
|
|
55
|
+
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
56
|
+
try {
|
|
57
|
+
const answer = (await readline.question(`${question} [y/N] `)).trim().toLowerCase();
|
|
58
|
+
return answer === 'y' || answer === 'yes';
|
|
59
|
+
} finally {
|
|
60
|
+
readline.close();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
52
64
|
function errorDiagnostic(err) {
|
|
53
65
|
const parts = [];
|
|
54
66
|
let current = err;
|
|
@@ -949,6 +961,11 @@ async function runRuntime(argv, agent) {
|
|
|
949
961
|
const host = valueAfter(argv, '--host') ?? process.env.WIKI_MANAGER_RUNTIME_HOST ?? '0.0.0.0';
|
|
950
962
|
const port = Number(valueAfter(argv, '--port') ?? process.env.WIKI_MANAGER_RUNTIME_PORT ?? 7788);
|
|
951
963
|
const stateDir = valueAfter(argv, '--state-dir') ?? defaultRuntimeStateDir();
|
|
964
|
+
// The TOTP login modules resolve their state directory from the ENV, not
|
|
965
|
+
// from the CLI option: without this, `runtime --state-dir X` would store
|
|
966
|
+
// totp.json/session.json in the DEFAULT manager state dir while the rest of
|
|
967
|
+
// the runtime uses X — sessions issued on one side invisible to the other.
|
|
968
|
+
process.env.WIKI_MANAGER_STATE_DIR = stateDir;
|
|
952
969
|
const auth = resolveRuntimeAuthToken({ host, stateDir });
|
|
953
970
|
if (auth.token) process.env.WIKI_MANAGER_RUNTIME_TOKEN = auth.token;
|
|
954
971
|
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
@@ -1351,7 +1368,15 @@ async function runRuntime(argv, agent) {
|
|
|
1351
1368
|
);
|
|
1352
1369
|
}
|
|
1353
1370
|
if (!Array.isArray(fragment?.tasks) || fragment.tasks.length === 0) {
|
|
1354
|
-
|
|
1371
|
+
// An agent may legitimately plan nothing: no pending source to ingest, no
|
|
1372
|
+
// template, no deliverable, no archived source to re-file. That is an
|
|
1373
|
+
// OUTCOME, not a failure — the agent carries its own human-readable
|
|
1374
|
+
// sentence in summary.initialSynthesis. The stable EMPTY_PLAN sentinel
|
|
1375
|
+
// tells graph.js to relay that outcome instead of the generic "could not
|
|
1376
|
+
// be started" failure (which read as a connectivity problem).
|
|
1377
|
+
const plannedReason = String(fragment?.summary?.initialSynthesis?.[0] ?? '').replace(/\s+/g, ' ').trim();
|
|
1378
|
+
const reason = plannedReason || `${selection.capability} planned no task for ${selection.operation}.`;
|
|
1379
|
+
throw new Error(`EMPTY_PLAN: ${reason}`);
|
|
1355
1380
|
}
|
|
1356
1381
|
const validation = validateFragment(fragment, {
|
|
1357
1382
|
registry: capabilityRegistryForSession(session),
|
|
@@ -1898,6 +1923,65 @@ export async function runCli(argv) {
|
|
|
1898
1923
|
return;
|
|
1899
1924
|
}
|
|
1900
1925
|
|
|
1926
|
+
if (argv[0] === 'login' || argv[0] === 'logout') {
|
|
1927
|
+
const { ensureRuntime } = await import('../runtime/lifecycle.js');
|
|
1928
|
+
const { fetchLoginStatus, requestTotpSession, revokeRuntimeSession } = await import('../runtime/totpLogin.js');
|
|
1929
|
+
let runtime = null;
|
|
1930
|
+
try {
|
|
1931
|
+
runtime = await ensureRuntime();
|
|
1932
|
+
} catch (err) {
|
|
1933
|
+
runtime = { url: null, error: err instanceof Error ? err.message : String(err) };
|
|
1934
|
+
}
|
|
1935
|
+
if (!runtime.url) throw new Error(`Runtime unavailable: ${runtime.error}`);
|
|
1936
|
+
if (argv[0] === 'logout') {
|
|
1937
|
+
const { currentSessionToken } = await import('../runtime/loginSession.js');
|
|
1938
|
+
const revoked = await revokeRuntimeSession(runtime, currentSessionToken());
|
|
1939
|
+
console.log(revoked ? 'Session revoked.' : 'No active session to revoke (or the runtime is not answering).');
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
if (argv.includes('--reset')) {
|
|
1943
|
+
// Re-enrollment after a lost authenticator: wipes the secret and the
|
|
1944
|
+
// active session (local files, not an HTTP route) then runs the normal
|
|
1945
|
+
// login flow, which lands on a fresh enrollment QR code.
|
|
1946
|
+
const confirmed = argv.includes('--yes') || (await confirmOnTty(
|
|
1947
|
+
'Reset the TOTP enrollment? This revokes the active session.\nContinue?',
|
|
1948
|
+
false,
|
|
1949
|
+
));
|
|
1950
|
+
if (!confirmed) {
|
|
1951
|
+
console.log('Aborted.');
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
const { resetTotpEnrollment } = await import('../runtime/loginSession.js');
|
|
1955
|
+
resetTotpEnrollment();
|
|
1956
|
+
console.log('TOTP enrollment and the active session were reset — enrolling a new authenticator now.');
|
|
1957
|
+
}
|
|
1958
|
+
const status = await fetchLoginStatus(runtime);
|
|
1959
|
+
if (status && !status.enabled) {
|
|
1960
|
+
console.log('TOTP login is disabled (WIKI_MANAGER_TOTP=off).');
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
if (!argv.includes('--reset') && status?.sessionActive) {
|
|
1964
|
+
// A session already exists, but the *browser* only gets the shared
|
|
1965
|
+
// wiki_session cookie the moment it verifies a code on the runtime's
|
|
1966
|
+
// origin. Re-open the page when asked so a browser that has none can
|
|
1967
|
+
// seed it (and then reuse it on serve, same host) without waiting for
|
|
1968
|
+
// the session to expire first.
|
|
1969
|
+
if (!argv.includes('--no-open')) {
|
|
1970
|
+
const { loginPageUrl, openBrowser } = await import('../runtime/totpLogin.js');
|
|
1971
|
+
try {
|
|
1972
|
+
await openBrowser(loginPageUrl(runtime));
|
|
1973
|
+
} catch {
|
|
1974
|
+
console.log(`Open ${loginPageUrl(runtime)} to seed this browser's session.`);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
console.log(`Session already active until ${new Date(status.sessionExpiresAt).toLocaleString()}.`);
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
const result = await requestTotpSession(runtime, { open: !argv.includes('--no-open') });
|
|
1981
|
+
if (!result.ok) throw new Error(result.error);
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1901
1985
|
if (argv.includes('--setup-wizard')) {
|
|
1902
1986
|
if (!process.versions.bun) {
|
|
1903
1987
|
throw new Error('Setup wizard requires Bun. Run: bun ./bin/wiki-manager.js --setup-wizard');
|
|
@@ -2008,6 +2092,15 @@ export async function runCli(argv) {
|
|
|
2008
2092
|
runtime = unavailableRuntime(err);
|
|
2009
2093
|
console.error(`Runtime unavailable: ${runtime.error}`);
|
|
2010
2094
|
}
|
|
2095
|
+
if (runtime.url) {
|
|
2096
|
+
const { requestTotpSession } = await import('../runtime/totpLogin.js');
|
|
2097
|
+
const gate = await requestTotpSession(runtime);
|
|
2098
|
+
if (!gate.ok) {
|
|
2099
|
+
console.error(gate.error);
|
|
2100
|
+
process.exitCode = 1;
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2011
2104
|
preflight = withRuntimePreflight(preflight, runtime);
|
|
2012
2105
|
// render() resolves at mount; the TUI owns renderer teardown. The shared
|
|
2013
2106
|
// runtime deliberately survives shell exit because `serve` may use it.
|
|
@@ -2030,6 +2123,15 @@ export async function runCli(argv) {
|
|
|
2030
2123
|
runtime = unavailableRuntime(err);
|
|
2031
2124
|
console.error(`Runtime unavailable: ${runtime.error}`);
|
|
2032
2125
|
}
|
|
2126
|
+
if (runtime.url) {
|
|
2127
|
+
const { requestTotpSession } = await import('../runtime/totpLogin.js');
|
|
2128
|
+
const gate = await requestTotpSession(runtime);
|
|
2129
|
+
if (!gate.ok) {
|
|
2130
|
+
console.error(gate.error);
|
|
2131
|
+
process.exitCode = 1;
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2033
2135
|
}
|
|
2034
2136
|
await runShell({ agent, packageJson, runtime });
|
|
2035
2137
|
}
|
package/src/commands/slash.js
CHANGED
|
@@ -109,6 +109,26 @@ export function compactBaseUrl(value) {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/*
|
|
113
|
+
The web UI URL `/openui` opens. The runtime sets the `wiki_session` cookie on
|
|
114
|
+
its own origin when the ShellUI logs in, and cookies ignore the port, so we
|
|
115
|
+
reuse the runtime's loopback hostname: serve on that same host then validates
|
|
116
|
+
the shared session instead of asking for a second TOTP code. A non-loopback
|
|
117
|
+
runtime (remote deployment) shares no cookie with the local browser, so we
|
|
118
|
+
keep the plain `localhost` URL there — behaving exactly as before.
|
|
119
|
+
*/
|
|
120
|
+
export function webUiUrl(port, runtimeUrl) {
|
|
121
|
+
let runtimeHost = null;
|
|
122
|
+
try {
|
|
123
|
+
if (runtimeUrl) runtimeHost = new URL(runtimeUrl).hostname || null;
|
|
124
|
+
} catch {
|
|
125
|
+
runtimeHost = null;
|
|
126
|
+
}
|
|
127
|
+
const loopback = runtimeHost === 'localhost' || runtimeHost === '::1' || /^127\./.test(runtimeHost ?? '');
|
|
128
|
+
const host = loopback ? runtimeHost : 'localhost';
|
|
129
|
+
return `http://${host}:${port}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
112
132
|
function commandLabel(value) {
|
|
113
133
|
return `${styles.bold}${styles.cyan}${value}${styles.reset}`;
|
|
114
134
|
}
|
|
@@ -726,8 +746,8 @@ async function statusText(session) {
|
|
|
726
746
|
const runtimesColumn = runtimeProvidersSection(session);
|
|
727
747
|
const stats = workspaceStatsColumns(workspaceStats, session);
|
|
728
748
|
|
|
729
|
-
const wikiColumnAll = [workspaceColumn, stats.wiki, runtimeColumn].filter(Boolean).join('\n\n');
|
|
730
|
-
const configColumnAll = [configColumn, stats.tuning,
|
|
749
|
+
const wikiColumnAll = [workspaceColumn, stats.wiki, runtimeColumn, mcpColumn].filter(Boolean).join('\n\n');
|
|
750
|
+
const configColumnAll = [configColumn, stats.tuning, runtimesColumn].filter(Boolean).join('\n\n');
|
|
731
751
|
|
|
732
752
|
// Leading/trailing blank row so the boxed pair doesn't butt directly against
|
|
733
753
|
// the pane border when the view is scrolled to show the tail. It is padding,
|
|
@@ -1734,9 +1754,23 @@ export async function handleSlashCommand(line, context) {
|
|
|
1734
1754
|
}
|
|
1735
1755
|
case 'openui': {
|
|
1736
1756
|
const port = context.session.workspaceEnv?.WIKI_SERVE_PORT ?? '3100';
|
|
1737
|
-
const url =
|
|
1757
|
+
const url = webUiUrl(port, context.runtime?.url ?? context.session?.runtime?.url ?? null);
|
|
1758
|
+
// Hand the ShellUI session to the browser so serve opens without a second
|
|
1759
|
+
// TOTP code: the token rides in the URL fragment (never sent to the
|
|
1760
|
+
// server, so it stays out of logs), and serve's login page exchanges it
|
|
1761
|
+
// for its cookie on load. Open `/login` directly rather than the root so
|
|
1762
|
+
// the fragment never has to survive a redirect. The printed line never
|
|
1763
|
+
// carries the token.
|
|
1764
|
+
let openUrl = url;
|
|
1765
|
+
try {
|
|
1766
|
+
const { currentSessionToken } = await import('../runtime/loginSession.js');
|
|
1767
|
+
const token = currentSessionToken();
|
|
1768
|
+
if (token) openUrl = `${url}/login#t=${encodeURIComponent(token)}`;
|
|
1769
|
+
} catch {
|
|
1770
|
+
// No readable session (gate off, or a custom state dir): plain URL.
|
|
1771
|
+
}
|
|
1738
1772
|
const note = context.session.workspaceEnv ? '' : ' (no workspace loaded — using default port)';
|
|
1739
|
-
if (openExternalUrl(
|
|
1773
|
+
if (openExternalUrl(openUrl)) return { output: `Opening web UI: ${url}${note}` };
|
|
1740
1774
|
return { output: `Web UI: ${url}${note}` };
|
|
1741
1775
|
}
|
|
1742
1776
|
case 'clear': {
|
|
@@ -4,7 +4,7 @@ import { mkdtemp } from 'node:fs/promises';
|
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import test from 'node:test';
|
|
7
|
-
import { agentConcurrencySections, compactBaseUrl, compactMcpStatus, handleSlashCommand, localizedOperationResult, refreshMcpRuntimeStatus } from './slash.js';
|
|
7
|
+
import { agentConcurrencySections, compactBaseUrl, compactMcpStatus, handleSlashCommand, localizedOperationResult, refreshMcpRuntimeStatus, webUiUrl } from './slash.js';
|
|
8
8
|
import { completionContext } from '../shell/repl.js';
|
|
9
9
|
|
|
10
10
|
test('deterministic operation results ask Donna to localize compact facts without leaking commands', () => {
|
|
@@ -52,6 +52,16 @@ test('/status base URL displays only its domain while retaining the full link',
|
|
|
52
52
|
assert.equal(compactBaseUrl(undefined), '-');
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
+
test('/openui reuses the loopback runtime host so serve shares the TOTP session', () => {
|
|
56
|
+
// The runtime sets wiki_session on its own origin and cookies ignore the
|
|
57
|
+
// port: opening serve on the same host avoids a second login. Non-loopback
|
|
58
|
+
// runtimes share no cookie, so the plain localhost URL stays.
|
|
59
|
+
assert.equal(webUiUrl('3200', 'http://127.0.0.1:7788'), 'http://127.0.0.1:3200');
|
|
60
|
+
assert.equal(webUiUrl('3200', 'http://localhost:7788'), 'http://localhost:3200');
|
|
61
|
+
assert.equal(webUiUrl('3200', null), 'http://localhost:3200');
|
|
62
|
+
assert.equal(webUiUrl('3200', 'https://manager.example.com'), 'http://localhost:3200');
|
|
63
|
+
});
|
|
64
|
+
|
|
55
65
|
test('/status replaces Internal and Hints with effective agent concurrency', () => {
|
|
56
66
|
const sections = agentConcurrencySections({
|
|
57
67
|
agentRegistrySnapshot: [
|
package/src/core/agentEvents.js
CHANGED
|
@@ -215,6 +215,8 @@ export function conversationEventSequences(events = []) {
|
|
|
215
215
|
function createProjectionState() {
|
|
216
216
|
return {
|
|
217
217
|
conversation: [],
|
|
218
|
+
conversationSeedStart: 0,
|
|
219
|
+
conversationSummary: null,
|
|
218
220
|
chain: [],
|
|
219
221
|
plan: null,
|
|
220
222
|
activities: {},
|
|
@@ -234,6 +236,8 @@ function createProjectionState() {
|
|
|
234
236
|
function publicProjection(state) {
|
|
235
237
|
const projection = {
|
|
236
238
|
conversation: state.conversation.map((message) => ({ ...message })),
|
|
239
|
+
conversationSeedStart: state.conversationSeedStart ?? 0,
|
|
240
|
+
conversationSummary: state.conversationSummary ?? null,
|
|
237
241
|
chain: state.chain.map((step) => ({ ...step })),
|
|
238
242
|
plan: state.plan ? state.plan.map((step) => ({ ...step })) : null,
|
|
239
243
|
activities: sortedActivities(state.activities).map((activity) => ({ ...activity })),
|
|
@@ -313,6 +317,25 @@ function applyEvent(state, event) {
|
|
|
313
317
|
case 'user_message':
|
|
314
318
|
state.conversation.push({ role: 'user', content: String(event.payload?.content ?? '') });
|
|
315
319
|
return;
|
|
320
|
+
case 'conversation_reset':
|
|
321
|
+
// A deliberate user action (the served chat's memory gauge): everything
|
|
322
|
+
// said before this point stops feeding conversationSeed's LLM context,
|
|
323
|
+
// WITHOUT touching the displayed conversation — the thread stays visible
|
|
324
|
+
// everywhere (ShellUI and serve), only what Donna is told going forward
|
|
325
|
+
// is reset. Nothing is removed from the event log. Marking the boundary
|
|
326
|
+
// (length at reset time) instead of clearing state.conversation is what
|
|
327
|
+
// keeps the two concerns apart: the gauge/seed read conversationSeedStart,
|
|
328
|
+
// the display reads conversation.
|
|
329
|
+
state.conversationSeedStart = state.conversation.length;
|
|
330
|
+
// The summary is best-effort (an LLM call the compact route makes before
|
|
331
|
+
// dispatching this event): when it succeeds it REPLACES the previous one
|
|
332
|
+
// — it is a rolling summary of "everything before this point", not an
|
|
333
|
+
// accumulating log — and when it fails or is skipped (no LLM configured)
|
|
334
|
+
// the previous summary survives rather than being wiped by an empty one.
|
|
335
|
+
if (typeof event.payload?.summary === 'string' && event.payload.summary.trim()) {
|
|
336
|
+
state.conversationSummary = event.payload.summary.trim();
|
|
337
|
+
}
|
|
338
|
+
return;
|
|
316
339
|
case 'assistant_message':
|
|
317
340
|
finalizeAssistantMessage(state, String(event.payload?.content ?? ''));
|
|
318
341
|
return;
|
|
@@ -39,6 +39,43 @@ test('a streamed reply keeps the sequence of the delta that created it', () => {
|
|
|
39
39
|
assert.deepEqual(conversationEventSequences(events), [1, 2]);
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
+
test('conversation_reset marks a seed boundary and keeps the displayed thread', () => {
|
|
43
|
+
const events = sequenced([
|
|
44
|
+
createAgentEvent('user_message', { origin: 'user', payload: { content: 'avant' } }),
|
|
45
|
+
createAgentEvent('assistant_message', { origin: 'runtime', payload: { content: 'réponse avant' } }),
|
|
46
|
+
createAgentEvent('conversation_reset', { origin: 'user', payload: {} }),
|
|
47
|
+
createAgentEvent('user_message', { origin: 'user', payload: { content: 'après' } }),
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
const projection = reduceAgentEvents(events);
|
|
51
|
+
// The thread stays whole — compacting must not erase what the reader sees…
|
|
52
|
+
assert.deepEqual(projection.conversation.map((message) => message.content), ['avant', 'réponse avant', 'après']);
|
|
53
|
+
// …only the grounding boundary moves.
|
|
54
|
+
assert.equal(projection.conversationSeedStart, 2);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('conversation_reset stores the summary it carries and keeps it through a later compact with none', () => {
|
|
58
|
+
const events = sequenced([
|
|
59
|
+
createAgentEvent('user_message', { origin: 'user', payload: { content: 'avant' } }),
|
|
60
|
+
createAgentEvent('conversation_reset', { origin: 'user', payload: { summary: 'Résumé 1' } }),
|
|
61
|
+
createAgentEvent('user_message', { origin: 'user', payload: { content: 'entre' } }),
|
|
62
|
+
// A summary is best-effort (an LLM call): a compact with no summary (LLM
|
|
63
|
+
// unavailable, or nothing worth summarizing) must not erase the last one.
|
|
64
|
+
createAgentEvent('conversation_reset', { origin: 'user', payload: {} }),
|
|
65
|
+
]);
|
|
66
|
+
const projection = reduceAgentEvents(events);
|
|
67
|
+
assert.equal(projection.conversationSummary, 'Résumé 1');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('a later compact with a new summary replaces the previous one', () => {
|
|
71
|
+
const events = sequenced([
|
|
72
|
+
createAgentEvent('conversation_reset', { origin: 'user', payload: { summary: 'Résumé 1' } }),
|
|
73
|
+
createAgentEvent('conversation_reset', { origin: 'user', payload: { summary: 'Résumé 2' } }),
|
|
74
|
+
]);
|
|
75
|
+
const projection = reduceAgentEvents(events);
|
|
76
|
+
assert.equal(projection.conversationSummary, 'Résumé 2');
|
|
77
|
+
});
|
|
78
|
+
|
|
42
79
|
test('an independent queued skill invocation never inherits the active run identity', () => {
|
|
43
80
|
const session = {
|
|
44
81
|
workspace: 'docs',
|
package/src/core/buildInfo.json
CHANGED
|
@@ -27,6 +27,20 @@ test('workspace production agent enables restore by default', async () => {
|
|
|
27
27
|
assert.match(String(allowed), /(?:^|,)restore(?:,|})/);
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
test('the manager allowlist carries the new knowledge steps, not only the agent default', async () => {
|
|
31
|
+
// This env OVERRIDES agent-production's own default: a value missing
|
|
32
|
+
// ingest_rebuild/lint silently filters knowledge.rebuild/knowledge.check out
|
|
33
|
+
// of agent_describe (the capability is published only if one of its steps
|
|
34
|
+
// survives). That is exactly what made the wiki-row rebuild button run a
|
|
35
|
+
// plain ingest from raw/untracked instead of re-filing raw/ingested.
|
|
36
|
+
const raw = await readFile(new URL('../../docker-compose.yml', import.meta.url), 'utf8');
|
|
37
|
+
const compose = YAML.parse(raw);
|
|
38
|
+
const allowed = String(compose.services['production-mcp'].environment
|
|
39
|
+
.find((entry) => String(entry).startsWith('PRODUCTION_ALLOWED_STEPS=')));
|
|
40
|
+
assert.match(allowed, /(?:^|,)ingest_rebuild(?:,|})/);
|
|
41
|
+
assert.match(allowed, /(?:^|,)lint(?:,|})/);
|
|
42
|
+
});
|
|
43
|
+
|
|
30
44
|
test('the shipped default carries no step the engine retired in 0.15.66', async () => {
|
|
31
45
|
/*
|
|
32
46
|
0.15.66 a retiré du moteur llm-wiki les commandes `wiki concepts`,
|
package/src/core/mcp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { managerEnvFile, managerMcpEndpointsFile, readEnvFile } from './env.js';
|
|
3
3
|
|
|
4
|
-
const WIKI_MANAGER_VERSION = '0.15.
|
|
4
|
+
const WIKI_MANAGER_VERSION = '0.15.94';
|
|
5
5
|
|
|
6
6
|
function envValue(key) {
|
|
7
7
|
const filePath = managerEnvFile();
|
|
@@ -31,7 +31,7 @@ test('validation rejects technical routing details', () => {
|
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
test('every shipped scaffold skill compiles to a single intention, deterministically', async () => {
|
|
34
|
-
const expected = { pipeline: 1, 'wiki-sync': 1, 'wiki-ingest': 1, 'wiki-build': 1, deliver: 1, diagnose: 1, status: 1, 'new-template': 1 };
|
|
34
|
+
const expected = { pipeline: 1, 'wiki-sync': 1, 'wiki-ingest': 1, 'wiki-build': 1, deliver: 1, diagnose: 1, status: 1, 'new-template': 1, 'wiki-rebuild': 1 };
|
|
35
35
|
// Passing no llmFallback used to make this test assert the one path
|
|
36
36
|
// production never takes: an ambiguous body silently returns the safe
|
|
37
37
|
// mono-intention fallback, so the count was 1 and the test was green while
|
|
@@ -47,6 +47,26 @@ test('every shipped scaffold skill compiles to a single intention, deterministic
|
|
|
47
47
|
}
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
test('the shipped wiki-rebuild skill resolves through the deterministic alias path only', async () => {
|
|
51
|
+
// The objective resolver's fast path fires only when EXACTLY ONE capability
|
|
52
|
+
// alias phrase matches the objective. Bare words other agents alias ('build',
|
|
53
|
+
// 'rebuild', 'ingest', 'check', 'export'…) would make the LLM resolver decide
|
|
54
|
+
// instead. The shipped body is worded to stay on the deterministic path: it
|
|
55
|
+
// must carry the agent-production knowledge.rebuild alias phrase and none of
|
|
56
|
+
// the colliding words — verified against the alias lists actually shipped in
|
|
57
|
+
// agent-production (knowledge.rebuild / knowledge.check) and the other
|
|
58
|
+
// agents (cme: 'export sources'…, gateway: 'check'…).
|
|
59
|
+
const raw = readFileSync(resolve('../llm-wiki/scaffold/workspace/.wiki/skills', 'wiki-rebuild.md'), 'utf8');
|
|
60
|
+
const { body } = parseFrontmatter(raw);
|
|
61
|
+
const objectives = await compileSkillObjectives({ body }, {});
|
|
62
|
+
assert.equal(objectives.length, 1);
|
|
63
|
+
const text = objectives[0].text;
|
|
64
|
+
assert.match(text, /file the archived sources/i);
|
|
65
|
+
for (const word of ['ingest', 'build', 'rebuild', 'export', 'publish', 'okf', 'frontmatter', 'diagnose', 'restore', 'pipeline', 'check', 'audit', 'review', 'analyze', 'compare']) {
|
|
66
|
+
assert.doesNotMatch(text, new RegExp(`\\b${word}\\b`, 'i'), `word "${word}" must not appear in the objective`);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
50
70
|
test('every orchestrated scaffold skill declares the capability it targets', () => {
|
|
51
71
|
// Without a declaration the capability is inferred from the body's prose by
|
|
52
72
|
// alias matching, which any runtime added to agent-runtimes.json can break by
|
|
@@ -108,6 +108,33 @@ test('resolveObjective resolves an ingest objective deterministically despite no
|
|
|
108
108
|
assert.equal(result.operation, 'ingest');
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
test('"rebuild" resolves to knowledge.rebuild, not document.build', async () => {
|
|
112
|
+
// document.build used to alias "rebuild" too, so the wiki row's rebuild
|
|
113
|
+
// button ran a template build instead of re-filing the archive. The alias is
|
|
114
|
+
// unique to knowledge.rebuild now, and the bare /wiki-rebuild invocation must
|
|
115
|
+
// resolve deterministically (no LLM) to ingest_rebuild.
|
|
116
|
+
const knowledgeRebuild = makeCapability('knowledge.rebuild', {
|
|
117
|
+
operations: ['ingest_rebuild'],
|
|
118
|
+
aliases: ['file the archived sources', 'archived sources into the wiki', 'concept pages from the archive', 'rebuild', 'rebuild concepts'],
|
|
119
|
+
description: 'Re-file the archived sources into their concept folders.',
|
|
120
|
+
});
|
|
121
|
+
const documentBuild = makeCapability('document.build', {
|
|
122
|
+
operations: ['build'],
|
|
123
|
+
aliases: ['build', 'generate deliverable'],
|
|
124
|
+
description: 'Build llm-wiki deliverables from templates.',
|
|
125
|
+
});
|
|
126
|
+
const session = sessionWith([
|
|
127
|
+
provider('production-1', knowledgeRebuild),
|
|
128
|
+
provider('production-1', documentBuild),
|
|
129
|
+
]);
|
|
130
|
+
session.llm.completeWithTools = async () => {
|
|
131
|
+
throw new Error('the rebuild alias must resolve without the LLM');
|
|
132
|
+
};
|
|
133
|
+
const result = await resolveObjective('/wiki-rebuild', session);
|
|
134
|
+
assert.equal(result.capability, 'knowledge.rebuild');
|
|
135
|
+
assert.equal(result.operation, 'ingest_rebuild');
|
|
136
|
+
});
|
|
137
|
+
|
|
111
138
|
test('resolveObjective resolves diagnose via alias despite the notification "send"', async () => {
|
|
112
139
|
const session = sessionWith([
|
|
113
140
|
provider('production-1', diagnose),
|