@dotdrelle/wiki-manager 0.6.31 → 0.6.34
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/README.md +3 -2
- package/bin/wiki-manager.js +1 -1
- package/package.json +8 -7
- package/src/agent/graph.js +5 -10
- package/src/cli/wiki-manager.js +17 -2
- package/src/commands/slash.js +104 -27
- package/src/commands/slash.test.js +68 -0
- package/src/core/compose.js +1 -1
- package/src/core/mcp.js +1 -1
- package/src/core/modelFetch.js +97 -0
- package/src/core/modelFetch.test.js +38 -0
- package/src/core/startupCheck.js +130 -0
- package/src/core/wikiSetup.js +156 -0
- package/src/core/wikirc.js +80 -1
- package/src/core/wikirc.test.js +111 -0
- package/src/core/workspaces.js +1 -1
- package/src/shell/LeftPane.tsx +54 -28
- package/src/shell/RightPane.tsx +25 -2
- package/src/shell/SetupWizard.tsx +806 -0
- package/src/shell/SlashDialog.tsx +4 -3
- package/src/shell/repl.js +20 -8
- package/src/shell/tui.tsx +85 -13
- package/src/shell/useSession.ts +15 -7
- package/wiki-workspace +3 -3
package/README.md
CHANGED
|
@@ -551,8 +551,9 @@ The TUI uses a two-pane layout:
|
|
|
551
551
|
Useful primitives:
|
|
552
552
|
|
|
553
553
|
```text
|
|
554
|
-
/
|
|
555
|
-
/new <name> [path]
|
|
554
|
+
/workspace list
|
|
555
|
+
/new <name> [path] # interactive TUI wizard
|
|
556
|
+
/workspace init <name> [path] # low-level non-interactive creation
|
|
556
557
|
/use <workspace>
|
|
557
558
|
/config list
|
|
558
559
|
/config use <name>
|
package/bin/wiki-manager.js
CHANGED
|
@@ -81,7 +81,7 @@ async function main() {
|
|
|
81
81
|
// exports these before Bun starts.
|
|
82
82
|
if (parsed.cacert) Object.assign(process.env, cacertEnvVars(parsed.cacert));
|
|
83
83
|
await import('@opentui/solid/preload');
|
|
84
|
-
const interactive = process.stdout.isTTY && process.stdin.isTTY && !argv.includes('--headless') && !argv.includes('--once') && !argv.includes('--version') && !argv.includes('-v') && !argv.includes('--help') && !argv.includes('-h');
|
|
84
|
+
const interactive = process.stdout.isTTY && process.stdin.isTTY && !argv.includes('--setup-wizard') && !argv.includes('--headless') && !argv.includes('--once') && !argv.includes('--version') && !argv.includes('-v') && !argv.includes('--help') && !argv.includes('-h');
|
|
85
85
|
if (interactive) process.stdout.write('Starting wiki-manager…\r');
|
|
86
86
|
const { runCli } = await import('../src/cli/wiki-manager.js');
|
|
87
87
|
await runCli(argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotdrelle/wiki-manager",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.34",
|
|
4
4
|
"description": "Agentic shell and orchestration cockpit for llm-wiki workspaces.",
|
|
5
5
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
6
6
|
"author": "dotrelle",
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
"wiki-manager": "bin/wiki-manager",
|
|
10
10
|
"wiki-workspace": "wiki-workspace"
|
|
11
11
|
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"start": "bun ./bin/wiki-manager.js",
|
|
14
|
+
"test": "node --test src/core/activity.test.js src/core/agentEvents.test.js src/core/plan.test.js src/core/mcp.test.js src/core/documentIntake.test.js src/core/wikirc.test.js src/core/modelFetch.test.js src/commands/slash.test.js",
|
|
15
|
+
"check": "bun ./bin/wiki-manager.js --version && bun ./bin/wiki-manager.js --help && bun ./bin/wiki-manager.js --once \"verifie le mode agent\""
|
|
16
|
+
},
|
|
12
17
|
"engines": {
|
|
13
18
|
"node": ">=22.0.0",
|
|
14
19
|
"bun": ">=1.2.0"
|
|
@@ -35,6 +40,7 @@
|
|
|
35
40
|
"orchestration",
|
|
36
41
|
"cli"
|
|
37
42
|
],
|
|
43
|
+
"packageManager": "pnpm@10.29.2",
|
|
38
44
|
"dependencies": {
|
|
39
45
|
"@langchain/langgraph": "^1.3.2",
|
|
40
46
|
"@opentui/core": "^0.3.2",
|
|
@@ -47,10 +53,5 @@
|
|
|
47
53
|
},
|
|
48
54
|
"devDependencies": {
|
|
49
55
|
"@types/node": "^26.0.0"
|
|
50
|
-
},
|
|
51
|
-
"scripts": {
|
|
52
|
-
"start": "bun ./bin/wiki-manager.js",
|
|
53
|
-
"test": "node --test src/core/activity.test.js src/core/agentEvents.test.js src/core/plan.test.js src/core/mcp.test.js src/core/documentIntake.test.js",
|
|
54
|
-
"check": "bun ./bin/wiki-manager.js --version && bun ./bin/wiki-manager.js --help && bun ./bin/wiki-manager.js --once \"verifie le mode agent\""
|
|
55
56
|
}
|
|
56
|
-
}
|
|
57
|
+
}
|
package/src/agent/graph.js
CHANGED
|
@@ -17,8 +17,6 @@ const MAX_SPINNER_ARG_LENGTH = 96;
|
|
|
17
17
|
const AGENT_SLASH_COMMANDS = new Set([
|
|
18
18
|
'help',
|
|
19
19
|
'version',
|
|
20
|
-
'workspaces',
|
|
21
|
-
'new',
|
|
22
20
|
'workspace',
|
|
23
21
|
'use',
|
|
24
22
|
'config',
|
|
@@ -36,8 +34,8 @@ const SHELL_RUN_COMMAND_TOOL = {
|
|
|
36
34
|
name: 'shell__run_command',
|
|
37
35
|
description: [
|
|
38
36
|
'Run a deterministic wiki-manager slash command inside the current shell session.',
|
|
39
|
-
'Allowed commands: /
|
|
40
|
-
'Do not use for arbitrary system shell commands, /mcp call, /wiki run, /start, /stop, /logs, or /exit.',
|
|
37
|
+
'Allowed commands: /workspace list, /workspace init <name> [path], /use <workspace>, /config, /status, /services, /skills, /skills show <name>, /skills run <name>, /upload <path>, /upload convert <id|pending>, /uploads.',
|
|
38
|
+
'Do not use for arbitrary system shell commands, /workspace delete, /mcp call, /wiki run, /start, /stop, /logs, or /exit.',
|
|
41
39
|
].join(' '),
|
|
42
40
|
parameters: {
|
|
43
41
|
type: 'object',
|
|
@@ -45,7 +43,7 @@ const SHELL_RUN_COMMAND_TOOL = {
|
|
|
45
43
|
properties: {
|
|
46
44
|
command: {
|
|
47
45
|
type: 'string',
|
|
48
|
-
description: 'Slash command to run, for example "/
|
|
46
|
+
description: 'Slash command to run, for example "/workspace list", "/workspace init demo", or "/use juno".',
|
|
49
47
|
},
|
|
50
48
|
},
|
|
51
49
|
required: ['command'],
|
|
@@ -217,9 +215,6 @@ function assertAgentSlashCommandAllowed(commandLine) {
|
|
|
217
215
|
if (!AGENT_SLASH_COMMANDS.has(command)) {
|
|
218
216
|
throw new Error(`Command is not available to the agent: /${command}`);
|
|
219
217
|
}
|
|
220
|
-
if (command === 'new' && parts.length < 2) {
|
|
221
|
-
throw new Error('Usage: /new <name> [path].');
|
|
222
|
-
}
|
|
223
218
|
if (command === 'workspace' && parts[1] !== 'init') {
|
|
224
219
|
throw new Error('Only /workspace init is available to the agent.');
|
|
225
220
|
}
|
|
@@ -324,7 +319,7 @@ export function buildAgentSystemPrompt(state) {
|
|
|
324
319
|
'When the user asks for an action that can be performed with connected MCP tools or safe primitives, do not answer with future intent such as "I will call...", "I am going to run...", or "launching..." unless you also call the tool in the same turn. Either call the tool now, ask for the exact missing required arguments, or explain the concrete blocker.',
|
|
325
320
|
'For connector configuration/setup/update requests, if a matching setup/configuration tool is connected and the required arguments are known, call it immediately. If the connector or tool is not connected, say which concrete capability is missing and recommend the exact service/status primitive to inspect it. Do not invent a pending connector action in plain text.',
|
|
326
321
|
'For workspace-scoped external MCP tools, the orchestrator enforces workspace injection. Use the active workspace for configuration, source, import, export, conversion, and generation tools unless a tool is explicitly job-scoped and only requires a job id.',
|
|
327
|
-
'You can call shell__run_command for safe manager slash commands such as /
|
|
322
|
+
'You can call shell__run_command for safe manager slash commands such as /workspace list, /workspace init <name> [path], /use <workspace>, /config, /status, /services, /skills, /skills show <name>, and /skills run <name>.',
|
|
328
323
|
'Skills are workflow instructions, not executable code. When a user asks to run a skill, inspect it, propose the concrete primitive/tool plan, and ask for confirmation before costly or mutating actions.',
|
|
329
324
|
[
|
|
330
325
|
state.session.headless ? 'HEADLESS MODE ACTIVE. Execute the requested skill or task autonomously using available safe primitives and MCP tools. Do not ask for interactive confirmation unless the request is genuinely ambiguous or outside the loaded workspace.' : null,
|
|
@@ -361,7 +356,7 @@ export function buildAgentSystemPrompt(state) {
|
|
|
361
356
|
'For ingest/build/export/polish/pipeline workflows, use production MCP tools. Do not route these through direct /wiki shortcuts. To chain multiple sequential steps (e.g. build then polish), always use a single production_start_job call with type="pipeline" and steps=["build","polish"] — never start them as separate jobs: the first job is asynchronous and the second would run before it completes. For existing deliverables where content stability matters, pass stabilize:true so the build step preserves unchanged sections; keep polish in the pipeline when publication output is requested. Do not ask the user to confirm between steps; start the pipeline call directly.',
|
|
362
357
|
'Long-running MCP jobs: do not call the same status tool more than once consecutively. When chaining jobs sequentially: (1) start the job, report job/activity id and status; (2) check status once — if done, proceed to the next step immediately; (3) if still running, report status, list the remaining steps, and return control; (4) when re-invoked, check status first, then proceed. Do not spin-poll (status → status → status with no new action between). The shell activity panel monitors non-terminal jobs automatically.',
|
|
363
358
|
'If production_start_job is returned as queued/waiting by the manager, report that it is waiting in the local queue and return control. Do not continue as if the production job has started.',
|
|
364
|
-
'For diagnostics, use /wiki run doctor when the user asks for doctor. Use /
|
|
359
|
+
'For diagnostics, use /wiki run doctor when the user asks for doctor. Use /workspace init <name> [path] for low-level non-interactive workspace creation. In the interactive TUI, /new <name> opens the setup wizard. Use /wiki for index, or /wiki run index through the explicit backup hatch. Use /wiki run init only for explicit current-workspace llm-wiki init.',
|
|
365
360
|
'If an action requires tools or skills not available yet, explain the limitation and name the expected primitive.',
|
|
366
361
|
].join('\n');
|
|
367
362
|
|
package/src/cli/wiki-manager.js
CHANGED
|
@@ -7,6 +7,7 @@ loadManagerEnv();
|
|
|
7
7
|
import { createAgentGraph, buildAgentSystemPrompt, buildLimitedAgentResponse } from '../agent/graph.js';
|
|
8
8
|
import { handleSlashCommand, printHelp, printVersion } from '../commands/slash.js';
|
|
9
9
|
import { runShell } from '../shell/repl.js';
|
|
10
|
+
import { runChecks } from '../core/startupCheck.js';
|
|
10
11
|
import { callMcpTool, formatMcpToolResult } from '../core/mcp.js';
|
|
11
12
|
import { extractActivity, parseJsonText, sessionActivities } from '../core/activity.js';
|
|
12
13
|
import { extractHeadlessPlan, syncActivitiesToPlan, formatPlanStatus, formatCompletedActivities } from '../core/plan.js';
|
|
@@ -15,7 +16,7 @@ import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
|
15
16
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
17
|
const packageJsonPath = resolve(__dirname, '../../package.json');
|
|
17
18
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
18
|
-
const SHELL_COMMANDS = ['help', 'version', 'exit', '
|
|
19
|
+
const SHELL_COMMANDS = ['help', 'version', 'exit', 'workspace', 'new', 'use', 'config', 'status', 'services', 'start', 'stop', 'logs', 'mcp', 'wiki', 'skills', 'clear', 'chat', 'agent'];
|
|
19
20
|
|
|
20
21
|
function valueAfter(argv, flag) {
|
|
21
22
|
const index = argv.indexOf(flag);
|
|
@@ -345,6 +346,18 @@ async function runHeadless(argv, agent) {
|
|
|
345
346
|
}
|
|
346
347
|
|
|
347
348
|
export async function runCli(argv) {
|
|
349
|
+
if (argv.includes('--setup-wizard')) {
|
|
350
|
+
if (!process.versions.bun) {
|
|
351
|
+
throw new Error('Setup wizard requires Bun. Run: bun ./bin/wiki-manager.js --setup-wizard');
|
|
352
|
+
}
|
|
353
|
+
const { runSetupWizard } = await import('../shell/tui.tsx');
|
|
354
|
+
await runSetupWizard({
|
|
355
|
+
workspaceName: valueAfter(argv, '--workspace-name'),
|
|
356
|
+
workspacePath: valueAfter(argv, '--workspace-path') ?? null,
|
|
357
|
+
});
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
348
361
|
if (argv.includes('--version') || argv.includes('-v')) {
|
|
349
362
|
printVersion(packageJson);
|
|
350
363
|
return;
|
|
@@ -380,7 +393,9 @@ export async function runCli(argv) {
|
|
|
380
393
|
if (!process.versions.bun) {
|
|
381
394
|
throw new Error('Interactive TUI requires Bun. Run: bun ./bin/wiki-manager.js');
|
|
382
395
|
}
|
|
383
|
-
const { runOpenTuiShell } = await import('../shell/tui.tsx');
|
|
396
|
+
const { runOpenTuiShell, runStartupWizard } = await import('../shell/tui.tsx');
|
|
397
|
+
const gaps = await runChecks();
|
|
398
|
+
if (gaps.length > 0) await runStartupWizard(gaps);
|
|
384
399
|
await runOpenTuiShell({ agent, packageJson });
|
|
385
400
|
return;
|
|
386
401
|
}
|
package/src/commands/slash.js
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
resolveWikircProfile,
|
|
31
31
|
summarizeWikircConfig,
|
|
32
32
|
} from '../core/wikirc.js';
|
|
33
|
+
import { deleteWorkspaceAndFiles, startAgents, stopAgents } from '../core/wikiSetup.js';
|
|
33
34
|
import {
|
|
34
35
|
cleanDocumentUploads,
|
|
35
36
|
convertPendingDocumentUploads,
|
|
@@ -433,8 +434,7 @@ async function createWorkspaceCommand(context, workspaceName, targetPath) {
|
|
|
433
434
|
output: [
|
|
434
435
|
'Usage: /new <name> [path]',
|
|
435
436
|
'',
|
|
436
|
-
'Creates
|
|
437
|
-
'Legacy form: /workspace init <name> [path].',
|
|
437
|
+
'Creates and registers a workspace via wiki-workspace config.',
|
|
438
438
|
'For llm-wiki init inside the current workspace, use /wiki run init.',
|
|
439
439
|
].join('\n'),
|
|
440
440
|
};
|
|
@@ -456,6 +456,7 @@ async function createWorkspaceCommand(context, workspaceName, targetPath) {
|
|
|
456
456
|
}
|
|
457
457
|
}
|
|
458
458
|
|
|
459
|
+
|
|
459
460
|
function formatMcpCallActivity(serverName, toolName, resultText) {
|
|
460
461
|
if (serverName === 'production') return null;
|
|
461
462
|
return formatActivitySummary(serverName, toolName, resultText);
|
|
@@ -551,6 +552,47 @@ function loadSessionWikirc(session, profileName = 'default') {
|
|
|
551
552
|
return summarizeWikircConfig(loaded.profile, loaded.config);
|
|
552
553
|
}
|
|
553
554
|
|
|
555
|
+
function clearWorkspaceSession(session) {
|
|
556
|
+
session.workspace = null;
|
|
557
|
+
session.workspacePath = null;
|
|
558
|
+
session.workspaceEnv = null;
|
|
559
|
+
session.workspaceEnvFile = null;
|
|
560
|
+
session.wikirc = null;
|
|
561
|
+
session.wikircConfig = null;
|
|
562
|
+
session.language = null;
|
|
563
|
+
session.llm = null;
|
|
564
|
+
session.mcp = null;
|
|
565
|
+
session.systemPrompt = null;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function formatWorkspaceList(workspaces, session = null) {
|
|
569
|
+
if (workspaces.length === 0) return 'No workspace configured.';
|
|
570
|
+
return [
|
|
571
|
+
'Workspaces',
|
|
572
|
+
'',
|
|
573
|
+
...workspaces.flatMap((workspace) => {
|
|
574
|
+
const active = workspace.name === session?.workspace ? 'active' : 'available';
|
|
575
|
+
return [
|
|
576
|
+
`${workspace.name}\t${active}`,
|
|
577
|
+
` path\t${workspace.workspacePath}`,
|
|
578
|
+
` use\t/use ${workspace.name}`,
|
|
579
|
+
` delete\t/workspace delete ${workspace.name}`,
|
|
580
|
+
'',
|
|
581
|
+
];
|
|
582
|
+
}),
|
|
583
|
+
].join('\n').trimEnd();
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function workspaceDeletePrompt(workspaces) {
|
|
587
|
+
if (workspaces.length === 0) return 'No workspace configured.';
|
|
588
|
+
return [
|
|
589
|
+
'Delete a workspace:',
|
|
590
|
+
...workspaces.map((workspace) => ` /workspace delete ${workspace.name}\t${workspace.workspacePath}`),
|
|
591
|
+
'',
|
|
592
|
+
'The next step asks for confirmation before deleting files.',
|
|
593
|
+
].join('\n');
|
|
594
|
+
}
|
|
595
|
+
|
|
554
596
|
export function helpText(packageJson) {
|
|
555
597
|
return `wiki-manager ${packageJson.version}
|
|
556
598
|
|
|
@@ -576,12 +618,12 @@ Options:
|
|
|
576
618
|
|
|
577
619
|
Interactive shell:
|
|
578
620
|
${helpPair('/help', 'Help', '/version', 'Version')}
|
|
579
|
-
${helpPair('/
|
|
621
|
+
${helpPair('/workspace list', 'Workspaces', '/new <n> [path]', 'New workspace')}
|
|
580
622
|
${helpPair('/use <workspace>', 'Use workspace', '/status', 'Session status')}
|
|
581
623
|
${helpPair('/config list', 'Config profiles', '/config use <n>', 'Use config')}
|
|
582
|
-
${helpPair('/config edit <n>', 'Edit config', '/
|
|
583
|
-
${helpPair('/services', 'Services', '/start [service]', 'Start service(s)')}
|
|
584
|
-
${helpPair('/stop [service]', 'Stop service(s)', '/logs <service>', 'Service logs')}
|
|
624
|
+
${helpPair('/config edit <n>', 'Edit config', '/workspace delete <n>', 'Delete workspace')}
|
|
625
|
+
${helpPair('/services', 'Services', '/start [service|agents]', 'Start service(s)')}
|
|
626
|
+
${helpPair('/stop [service|agents]', 'Stop service(s)', '/logs <service>', 'Service logs')}
|
|
585
627
|
${helpPair('/skills', 'List skills', '/skills show <n>', 'Show skill')}
|
|
586
628
|
${helpPair('/skills run <n>', 'Run skill guide', '/skills edit <n>', 'Edit skill')}
|
|
587
629
|
${helpPair('/mcp status', 'MCP status', '/mcp endpoints', 'MCP endpoints')}
|
|
@@ -618,6 +660,16 @@ export async function handleSlashCommand(line, context) {
|
|
|
618
660
|
const args = line.slice(1).trim().split(/\s+/).filter(Boolean);
|
|
619
661
|
const [command] = args;
|
|
620
662
|
const step = context.onStep ?? (() => {});
|
|
663
|
+
const runAgentCommand = async (fn, verb) => {
|
|
664
|
+
try {
|
|
665
|
+
step(`Agents: ${verb}ing external agents…`);
|
|
666
|
+
const output = await fn();
|
|
667
|
+
return { output: output || `Agents ${verb}ed.` };
|
|
668
|
+
} catch (err) {
|
|
669
|
+
step(formatActivityError('agents', verb, err));
|
|
670
|
+
return { output: err instanceof Error ? err.message : String(err) };
|
|
671
|
+
}
|
|
672
|
+
};
|
|
621
673
|
|
|
622
674
|
switch (command) {
|
|
623
675
|
case '':
|
|
@@ -631,17 +683,6 @@ export async function handleSlashCommand(line, context) {
|
|
|
631
683
|
case 'agent':
|
|
632
684
|
context.session.chatMode = false;
|
|
633
685
|
return { setMode: 'agent', output: 'Mode: agent' };
|
|
634
|
-
case 'workspaces': {
|
|
635
|
-
const workspaces = listWorkspaces();
|
|
636
|
-
if (workspaces.length === 0) {
|
|
637
|
-
return { output: 'No workspace configured.' };
|
|
638
|
-
}
|
|
639
|
-
return {
|
|
640
|
-
output: workspaces
|
|
641
|
-
.map((workspace) => `${workspace.name}\t${workspace.workspacePath}`)
|
|
642
|
-
.join('\n'),
|
|
643
|
-
};
|
|
644
|
-
}
|
|
645
686
|
case 'status': {
|
|
646
687
|
step('Shell: refreshing workspace, services and MCP status…');
|
|
647
688
|
return { output: await statusText(context.session) };
|
|
@@ -655,15 +696,11 @@ export async function handleSlashCommand(line, context) {
|
|
|
655
696
|
if (!workspace) {
|
|
656
697
|
return { output: `Workspace not found: ${workspaceName}` };
|
|
657
698
|
}
|
|
699
|
+
clearWorkspaceSession(context.session);
|
|
658
700
|
context.session.workspace = workspace.name;
|
|
659
701
|
context.session.workspacePath = workspace.workspacePath;
|
|
660
702
|
context.session.workspaceEnv = workspace.env;
|
|
661
703
|
context.session.workspaceEnvFile = workspace.envFile;
|
|
662
|
-
context.session.wikirc = null;
|
|
663
|
-
context.session.wikircConfig = null;
|
|
664
|
-
context.session.language = null;
|
|
665
|
-
context.session.llm = null;
|
|
666
|
-
context.session.mcp = null;
|
|
667
704
|
context.session.systemPrompt = loadWorkspaceSystemPrompt(workspace.workspacePath);
|
|
668
705
|
try {
|
|
669
706
|
step(`Workspace: loading ${workspace.name} config…`);
|
|
@@ -778,6 +815,7 @@ export async function handleSlashCommand(line, context) {
|
|
|
778
815
|
}
|
|
779
816
|
case 'start': {
|
|
780
817
|
const service = args[1];
|
|
818
|
+
if (service === 'agents') return runAgentCommand(startAgents, 'start');
|
|
781
819
|
try {
|
|
782
820
|
step(`Services: starting ${service ?? 'workspace services'}…`);
|
|
783
821
|
const output = await startService(context.session, service);
|
|
@@ -792,6 +830,7 @@ export async function handleSlashCommand(line, context) {
|
|
|
792
830
|
}
|
|
793
831
|
case 'stop': {
|
|
794
832
|
const service = args[1];
|
|
833
|
+
if (service === 'agents') return runAgentCommand(stopAgents, 'stop');
|
|
795
834
|
try {
|
|
796
835
|
step(`Services: stopping ${service ?? 'workspace services'}…`);
|
|
797
836
|
const output = await stopService(context.session, service);
|
|
@@ -955,12 +994,50 @@ export async function handleSlashCommand(line, context) {
|
|
|
955
994
|
case 'new': {
|
|
956
995
|
return createWorkspaceCommand(context, args[1], args[2] ?? null);
|
|
957
996
|
}
|
|
958
|
-
case 'workspace':
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
997
|
+
case 'workspace':
|
|
998
|
+
case 'workplace': {
|
|
999
|
+
const subcommand = args[1] ?? 'list';
|
|
1000
|
+
if (subcommand === 'list') {
|
|
1001
|
+
return { output: formatWorkspaceList(listWorkspaces(), context.session) };
|
|
1002
|
+
}
|
|
1003
|
+
if (subcommand === 'delete') {
|
|
1004
|
+
const workspaceName = args[2];
|
|
1005
|
+
const confirmed = args.includes('--confirm');
|
|
1006
|
+
const workspaces = listWorkspaces();
|
|
1007
|
+
if (!workspaceName) return { output: workspaceDeletePrompt(workspaces) };
|
|
1008
|
+
const workspace = workspaces.find((item) => item.name === workspaceName);
|
|
1009
|
+
if (!workspace) return { output: `Workspace not found: ${workspaceName}` };
|
|
1010
|
+
if (!confirmed) {
|
|
1011
|
+
return {
|
|
1012
|
+
output: [
|
|
1013
|
+
`Confirm workspace deletion: ${workspace.name}`,
|
|
1014
|
+
`Path: ${workspace.workspacePath}`,
|
|
1015
|
+
'This removes the registry entry and deletes the workspace files.',
|
|
1016
|
+
'',
|
|
1017
|
+
`Run: /workspace delete ${workspace.name} --confirm`,
|
|
1018
|
+
].join('\n'),
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
try {
|
|
1022
|
+
step(`Workspace: deleting ${workspace.name}…`);
|
|
1023
|
+
const result = await deleteWorkspaceAndFiles(workspace, workspace.workspacePath);
|
|
1024
|
+
const wasCurrent = context.session.workspace === workspace.name
|
|
1025
|
+
|| context.session.workspacePath === workspace.workspacePath;
|
|
1026
|
+
if (wasCurrent) clearWorkspaceSession(context.session);
|
|
1027
|
+
return {
|
|
1028
|
+
output: [
|
|
1029
|
+
`Deleted workspace: ${workspace.name}`,
|
|
1030
|
+
`Removed registry entry and files at: ${result.deletedPath}`,
|
|
1031
|
+
wasCurrent ? 'Current session cleared. Use /use <workspace> or /workspace init <name> [path].' : null,
|
|
1032
|
+
].filter(Boolean).join('\n'),
|
|
1033
|
+
};
|
|
1034
|
+
} catch (err) {
|
|
1035
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1036
|
+
return { output: message };
|
|
1037
|
+
}
|
|
962
1038
|
}
|
|
963
|
-
return createWorkspaceCommand(context, args[2], args[3] ?? null);
|
|
1039
|
+
if (subcommand === 'init') return createWorkspaceCommand(context, args[2], args[3] ?? null);
|
|
1040
|
+
return { output: 'Usage: /workspace <list|delete <name> --confirm|init <name> [path]>' };
|
|
964
1041
|
}
|
|
965
1042
|
case 'wiki': {
|
|
966
1043
|
const subcommand = args[1];
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { mkdtemp } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
import { handleSlashCommand } from './slash.js';
|
|
8
|
+
|
|
9
|
+
test('/workspace delete removes files and clears current session context after confirmation', async () => {
|
|
10
|
+
const root = await mkdtemp(join(tmpdir(), 'wiki-manager-delete-workspace-'));
|
|
11
|
+
const registryRoot = join(root, 'registry');
|
|
12
|
+
const registryPath = join(registryRoot, 'demo');
|
|
13
|
+
const workspacePath = join(root, 'workspace');
|
|
14
|
+
mkdirSync(registryPath, { recursive: true });
|
|
15
|
+
mkdirSync(workspacePath, { recursive: true });
|
|
16
|
+
writeFileSync(join(registryPath, '.env'), [
|
|
17
|
+
'WORKSPACE_NAME=demo',
|
|
18
|
+
`WIKI_WORKSPACE_PATH=${workspacePath}`,
|
|
19
|
+
'',
|
|
20
|
+
].join('\n'), 'utf8');
|
|
21
|
+
|
|
22
|
+
const previousDir = process.env.WIKI_WORKSPACES_DIR;
|
|
23
|
+
process.env.WIKI_WORKSPACES_DIR = registryRoot;
|
|
24
|
+
const session = {
|
|
25
|
+
workspace: 'demo',
|
|
26
|
+
workspacePath,
|
|
27
|
+
workspaceEnv: { WORKSPACE_NAME: 'demo' },
|
|
28
|
+
workspaceEnvFile: join(registryPath, '.env'),
|
|
29
|
+
wikirc: { profile: 'default' },
|
|
30
|
+
wikircConfig: {},
|
|
31
|
+
language: 'en-US',
|
|
32
|
+
llm: {},
|
|
33
|
+
mcp: {},
|
|
34
|
+
systemPrompt: 'prompt',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const prompt = await handleSlashCommand('/workspace delete demo', {
|
|
39
|
+
packageJson: { version: 'test' },
|
|
40
|
+
session,
|
|
41
|
+
});
|
|
42
|
+
assert.match(prompt.output, /Confirm workspace deletion: demo/);
|
|
43
|
+
assert.equal(existsSync(workspacePath), true);
|
|
44
|
+
assert.equal(session.workspace, 'demo');
|
|
45
|
+
|
|
46
|
+
const result = await handleSlashCommand('/workspace delete demo --confirm', {
|
|
47
|
+
packageJson: { version: 'test' },
|
|
48
|
+
session,
|
|
49
|
+
});
|
|
50
|
+
assert.match(result.output, /Deleted workspace: demo/);
|
|
51
|
+
assert.equal(session.workspace, null);
|
|
52
|
+
assert.equal(session.workspacePath, null);
|
|
53
|
+
assert.equal(session.llm, null);
|
|
54
|
+
assert.equal(session.mcp, null);
|
|
55
|
+
} finally {
|
|
56
|
+
if (previousDir === undefined) delete process.env.WIKI_WORKSPACES_DIR;
|
|
57
|
+
else process.env.WIKI_WORKSPACES_DIR = previousDir;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('/new without a name shows usage', async () => {
|
|
62
|
+
const result = await handleSlashCommand('/new', {
|
|
63
|
+
packageJson: { version: 'test' },
|
|
64
|
+
session: {},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
assert.match(result.output ?? '', /Usage/i);
|
|
68
|
+
});
|
package/src/core/compose.js
CHANGED
|
@@ -238,7 +238,7 @@ export async function composePs(session) {
|
|
|
238
238
|
return runCompose(session, ['ps'], { timeout: 30_000 });
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
function parseComposePsJson(output) {
|
|
241
|
+
export function parseComposePsJson(output) {
|
|
242
242
|
const text = output.trim();
|
|
243
243
|
if (!text) return [];
|
|
244
244
|
try {
|
package/src/core/mcp.js
CHANGED
|
@@ -211,7 +211,7 @@ async function mcpRequest(endpoint, method, params, signal, options = {}) {
|
|
|
211
211
|
params: {
|
|
212
212
|
protocolVersion: '2025-06-18',
|
|
213
213
|
capabilities: {},
|
|
214
|
-
clientInfo: { name: 'wiki-manager', version: '0.6.
|
|
214
|
+
clientInfo: { name: 'wiki-manager', version: '0.6.34' },
|
|
215
215
|
},
|
|
216
216
|
}),
|
|
217
217
|
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const FALLBACK_MODELS = {
|
|
2
|
+
openai: ['gpt-5.4', 'gpt-5.4-mini', 'gpt-4.1', 'gpt-4.1-mini'],
|
|
3
|
+
anthropic: ['claude-sonnet-4-5', 'claude-opus-4-1', 'claude-3-7-sonnet-latest'],
|
|
4
|
+
ollama: ['llama3.2', 'qwen2.5', 'mistral', 'nomic-embed-text'],
|
|
5
|
+
'openai-compatible': ['gpt-4.1-mini', 'llama3.2'],
|
|
6
|
+
other: ['gpt-4.1-mini', 'llama3.2'],
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const FALLBACK_EMBEDDINGS = {
|
|
10
|
+
openai: ['text-embedding-3-small', 'text-embedding-3-large'],
|
|
11
|
+
anthropic: ['text-embedding-3-small'],
|
|
12
|
+
ollama: ['nomic-embed-text', 'mxbai-embed-large'],
|
|
13
|
+
'openai-compatible': ['BAAI/bge-m3', 'text-embedding-3-small', 'nomic-embed-text'],
|
|
14
|
+
other: ['text-embedding-3-small', 'nomic-embed-text'],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function normalizeProvider(provider) {
|
|
18
|
+
const value = String(provider ?? '').toLowerCase();
|
|
19
|
+
if (value.includes('compatible') || value.includes('other')) return 'openai-compatible';
|
|
20
|
+
if (value.includes('anthropic')) return 'anthropic';
|
|
21
|
+
if (value.includes('ollama')) return 'ollama';
|
|
22
|
+
if (value.includes('openai')) return 'openai';
|
|
23
|
+
return 'openai-compatible';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function fallbackFor(provider, kind) {
|
|
27
|
+
const normalized = normalizeProvider(provider);
|
|
28
|
+
const source = kind === 'embedding' ? FALLBACK_EMBEDDINGS : FALLBACK_MODELS;
|
|
29
|
+
return source[normalized] ?? source.other;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function endpointFor(provider, baseUrl) {
|
|
33
|
+
const normalized = normalizeProvider(provider);
|
|
34
|
+
if (normalized === 'anthropic') return 'https://api.anthropic.com/v1/models';
|
|
35
|
+
const root = String(baseUrl || (normalized === 'ollama' ? 'http://localhost:11434' : 'https://api.openai.com')).replace(/\/+$/g, '');
|
|
36
|
+
return normalized === 'ollama' ? `${root}/api/tags` : `${root}/v1/models`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function headersFor(provider, apiKey) {
|
|
40
|
+
const normalized = normalizeProvider(provider);
|
|
41
|
+
if (normalized === 'ollama') return {};
|
|
42
|
+
if (normalized === 'anthropic') {
|
|
43
|
+
return {
|
|
44
|
+
'x-api-key': apiKey,
|
|
45
|
+
'anthropic-version': '2023-06-01',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return { Authorization: `Bearer ${apiKey}` };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseModelNames(provider, payload) {
|
|
52
|
+
const normalized = normalizeProvider(provider);
|
|
53
|
+
const items = normalized === 'ollama' ? payload?.models : payload?.data;
|
|
54
|
+
if (!Array.isArray(items)) return [];
|
|
55
|
+
return items
|
|
56
|
+
.map((item) => item?.id ?? item?.name ?? item?.model)
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.map(String)
|
|
59
|
+
.sort((a, b) => a.localeCompare(b));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function fetchModels(provider, baseUrl, apiKey, options = {}) {
|
|
63
|
+
const normalized = normalizeProvider(provider);
|
|
64
|
+
if (normalized === 'anthropic') {
|
|
65
|
+
return { ok: false, models: fallbackFor(normalized, options.kind), source: 'fallback', error: 'Anthropic model listing is not supported' };
|
|
66
|
+
}
|
|
67
|
+
const timeoutMs = options.timeoutMs ?? 10000;
|
|
68
|
+
const controller = new AbortController();
|
|
69
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
70
|
+
try {
|
|
71
|
+
if (normalized !== 'ollama' && !apiKey) {
|
|
72
|
+
throw new Error('API key is required to fetch remote models');
|
|
73
|
+
}
|
|
74
|
+
const response = await fetch(endpointFor(normalized, baseUrl), {
|
|
75
|
+
headers: headersFor(normalized, apiKey),
|
|
76
|
+
signal: controller.signal,
|
|
77
|
+
});
|
|
78
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
79
|
+
const payload = await response.json();
|
|
80
|
+
const models = parseModelNames(normalized, payload);
|
|
81
|
+
if (models.length === 0) throw new Error('No models returned');
|
|
82
|
+
return { ok: true, models, source: 'remote' };
|
|
83
|
+
} catch (err) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
models: fallbackFor(normalized, options.kind),
|
|
87
|
+
source: 'fallback',
|
|
88
|
+
error: err instanceof Error ? err.message : String(err),
|
|
89
|
+
};
|
|
90
|
+
} finally {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function fallbackModels(provider, kind) {
|
|
96
|
+
return fallbackFor(provider, kind);
|
|
97
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { fallbackModels, fetchModels } from './modelFetch.js';
|
|
4
|
+
|
|
5
|
+
test('fetchModels returns remote OpenAI-compatible model ids', async () => {
|
|
6
|
+
const originalFetch = globalThis.fetch;
|
|
7
|
+
globalThis.fetch = async () => ({
|
|
8
|
+
ok: true,
|
|
9
|
+
json: async () => ({ data: [{ id: 'b-model' }, { id: 'a-model' }] }),
|
|
10
|
+
});
|
|
11
|
+
try {
|
|
12
|
+
const result = await fetchModels('openai', 'http://models.local', 'key', { timeoutMs: 100 });
|
|
13
|
+
assert.deepEqual(result, { ok: true, models: ['a-model', 'b-model'], source: 'remote' });
|
|
14
|
+
} finally {
|
|
15
|
+
globalThis.fetch = originalFetch;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('fetchModels falls back on invalid remote response', async () => {
|
|
20
|
+
const originalFetch = globalThis.fetch;
|
|
21
|
+
globalThis.fetch = async () => ({
|
|
22
|
+
ok: true,
|
|
23
|
+
json: async () => ({ data: [] }),
|
|
24
|
+
});
|
|
25
|
+
try {
|
|
26
|
+
const result = await fetchModels('openai', 'http://models.local', 'key', { timeoutMs: 100 });
|
|
27
|
+
assert.equal(result.ok, false);
|
|
28
|
+
assert.equal(result.source, 'fallback');
|
|
29
|
+
assert.ok(result.models.includes('gpt-5.4-mini'));
|
|
30
|
+
assert.match(result.error, /No models returned/);
|
|
31
|
+
} finally {
|
|
32
|
+
globalThis.fetch = originalFetch;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('fallbackModels leaves custom-model to the wizard append action', () => {
|
|
37
|
+
assert.deepEqual(fallbackModels('openai-compatible'), ['gpt-4.1-mini', 'llama3.2']);
|
|
38
|
+
});
|