@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 CHANGED
@@ -551,8 +551,9 @@ The TUI uses a two-pane layout:
551
551
  Useful primitives:
552
552
 
553
553
  ```text
554
- /workspaces
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>
@@ -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.31",
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
+ }
@@ -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: /workspaces, /new <name> [path], /use <workspace>, /config, /status, /services, /skills, /skills show <name>, /skills run <name>, /upload <path>, /upload convert <id|pending>, /uploads.',
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 "/workspaces", "/new demo", or "/use juno".',
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 /workspaces, /new <name> [path], /use <workspace>, /config, /status, /services, /skills, /skills show <name>, and /skills run <name>.',
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 /new <name> [path] to create/configure a new workspace. 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.',
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
 
@@ -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', 'workspaces', 'new', 'use', 'config', 'status', 'services', 'start', 'stop', 'logs', 'mcp', 'wiki', 'skills', 'clear', 'chat', 'agent'];
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
  }
@@ -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/configures a new workspace through wiki-workspace config.',
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('/workspaces', 'Workspaces', '/new <n> [path]', 'New workspace')}
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', '/config status', 'Active 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
- const subcommand = args[1];
960
- if (subcommand !== 'init') {
961
- return { output: 'Usage: /new <name> [path]\nLegacy: /workspace init <name> [path]' };
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
+ });
@@ -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.31' },
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
+ });