@dotdrelle/wiki-manager 0.6.31 → 0.6.47

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.47",
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/core/startupCheck.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 <name>", or "/use <workspace>".',
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
 
@@ -371,23 +366,6 @@ export function buildAgentSystemPrompt(state) {
371
366
  export function buildLimitedAgentResponse(state, reason = 'no workspace loaded with .wikirc.yaml') {
372
367
  const workspace = state.session.workspace ?? 'no workspace selected';
373
368
  const wikirc = state.session.wikirc?.profile ?? 'no profile loaded';
374
- const language = state.session.language ?? 'en-US';
375
- if (language.toLowerCase().startsWith('fr')) {
376
- return [
377
- `Donna est active. Workspace courant: ${workspace}.`,
378
- `Profil wikirc courant: ${wikirc}.`,
379
- '',
380
- "Je suis le mode agent du shell: utilise `/agent` pour router les entrees libres vers ce graphe LangGraph, ou `/chat` pour revenir au chat direct.",
381
- `Connexion LLM: mode limite (${reason}).`,
382
- `Primitives disponibles maintenant: ${commandList(state.session)}.`,
383
- '',
384
- 'Outils MCP connectes:',
385
- formatMcpToolsForAgent(state.session.mcp),
386
- '',
387
- 'Mode limite: workspace, Docker Compose, appels MCP, echappatoire /wiki, decouverte skills et mode headless sont branches.',
388
- "Utilise `/help` pour voir les commandes deterministes disponibles.",
389
- ].join('\n');
390
- }
391
369
  return [
392
370
  `Donna is active. Current workspace: ${workspace}.`,
393
371
  `Current wikirc profile: ${wikirc}.`,
@@ -511,7 +489,7 @@ export function createAgentGraph(options = {}) {
511
489
  } catch (err) {
512
490
  if (err.name === 'AbortError') throw err;
513
491
  const message = err instanceof Error ? err.message : String(err);
514
- return { response: buildLimitedAgentResponse(state, `LLM indisponible: ${message}`), pendingToolCalls: null, readyToStream: false };
492
+ return { response: buildLimitedAgentResponse(state, `LLM unavailable: ${message}`), pendingToolCalls: null, readyToStream: false };
515
493
  }
516
494
  }
517
495
 
@@ -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,
@@ -281,6 +282,13 @@ function workspaceStatsText(stats) {
281
282
  }
282
283
 
283
284
  function workspaceLoadedText(workspace, summary, session) {
285
+ const profiles = listWikircProfiles(workspace.workspacePath);
286
+ const profileLines = profiles.length > 0
287
+ ? profiles.map((profile) => {
288
+ const marker = profile.name === summary.profile ? '*' : ' ';
289
+ return `${marker} ${profile.name}\t${profile.fileName}`;
290
+ })
291
+ : ['No .wikirc.yaml profile found.'];
284
292
  return [
285
293
  `Workspace: ${workspace.name}`,
286
294
  '',
@@ -299,6 +307,12 @@ function workspaceLoadedText(workspace, summary, session) {
299
307
  `vector: ${summary.vectorEnabled ? 'enabled' : 'disabled'}`,
300
308
  `embedding: ${summary.embeddingModel ?? '-'}`,
301
309
  '',
310
+ 'Available configs',
311
+ '',
312
+ ...profileLines,
313
+ '',
314
+ `Switch config: /config use <profile>`,
315
+ '',
302
316
  'Session',
303
317
  '',
304
318
  `llm: ${session.llm ? 'configured' : 'missing config'}`,
@@ -433,8 +447,7 @@ async function createWorkspaceCommand(context, workspaceName, targetPath) {
433
447
  output: [
434
448
  'Usage: /new <name> [path]',
435
449
  '',
436
- 'Creates/configures a new workspace through wiki-workspace config.',
437
- 'Legacy form: /workspace init <name> [path].',
450
+ 'Creates and registers a workspace via wiki-workspace config.',
438
451
  'For llm-wiki init inside the current workspace, use /wiki run init.',
439
452
  ].join('\n'),
440
453
  };
@@ -456,6 +469,7 @@ async function createWorkspaceCommand(context, workspaceName, targetPath) {
456
469
  }
457
470
  }
458
471
 
472
+
459
473
  function formatMcpCallActivity(serverName, toolName, resultText) {
460
474
  if (serverName === 'production') return null;
461
475
  return formatActivitySummary(serverName, toolName, resultText);
@@ -551,6 +565,47 @@ function loadSessionWikirc(session, profileName = 'default') {
551
565
  return summarizeWikircConfig(loaded.profile, loaded.config);
552
566
  }
553
567
 
568
+ function clearWorkspaceSession(session) {
569
+ session.workspace = null;
570
+ session.workspacePath = null;
571
+ session.workspaceEnv = null;
572
+ session.workspaceEnvFile = null;
573
+ session.wikirc = null;
574
+ session.wikircConfig = null;
575
+ session.language = null;
576
+ session.llm = null;
577
+ session.mcp = null;
578
+ session.systemPrompt = null;
579
+ }
580
+
581
+ function formatWorkspaceList(workspaces, session = null) {
582
+ if (workspaces.length === 0) return 'No workspace configured.';
583
+ return [
584
+ 'Workspaces',
585
+ '',
586
+ ...workspaces.flatMap((workspace) => {
587
+ const active = workspace.name === session?.workspace ? 'active' : 'available';
588
+ return [
589
+ `${workspace.name}\t${active}`,
590
+ ` path\t${workspace.workspacePath}`,
591
+ ` use\t/use ${workspace.name}`,
592
+ ` delete\t/workspace delete ${workspace.name}`,
593
+ '',
594
+ ];
595
+ }),
596
+ ].join('\n').trimEnd();
597
+ }
598
+
599
+ function workspaceDeletePrompt(workspaces) {
600
+ if (workspaces.length === 0) return 'No workspace configured.';
601
+ return [
602
+ 'Delete a workspace:',
603
+ ...workspaces.map((workspace) => ` /workspace delete ${workspace.name}\t${workspace.workspacePath}`),
604
+ '',
605
+ 'The next step asks for confirmation before deleting files.',
606
+ ].join('\n');
607
+ }
608
+
554
609
  export function helpText(packageJson) {
555
610
  return `wiki-manager ${packageJson.version}
556
611
 
@@ -576,12 +631,12 @@ Options:
576
631
 
577
632
  Interactive shell:
578
633
  ${helpPair('/help', 'Help', '/version', 'Version')}
579
- ${helpPair('/workspaces', 'Workspaces', '/new <n> [path]', 'New workspace')}
634
+ ${helpPair('/workspace list', 'Workspaces', '/new <n> [path]', 'New workspace')}
580
635
  ${helpPair('/use <workspace>', 'Use workspace', '/status', 'Session status')}
581
636
  ${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')}
637
+ ${helpPair('/config edit <n>', 'Edit config', '/workspace delete <n>', 'Delete workspace')}
638
+ ${helpPair('/services', 'Services', '/start [service|agents]', 'Start service(s)')}
639
+ ${helpPair('/stop [service|agents]', 'Stop service(s)', '/logs <service>', 'Service logs')}
585
640
  ${helpPair('/skills', 'List skills', '/skills show <n>', 'Show skill')}
586
641
  ${helpPair('/skills run <n>', 'Run skill guide', '/skills edit <n>', 'Edit skill')}
587
642
  ${helpPair('/mcp status', 'MCP status', '/mcp endpoints', 'MCP endpoints')}
@@ -605,7 +660,7 @@ Modes:
605
660
  Status:
606
661
  Agent-first shell is installed with workspace services, MCP calls, wiki CLI, skill discovery, and headless runs.
607
662
  Shell UI is English. Agent exchange language is read from the active .wikirc.yaml.
608
- LLM config is intentionally workspace-scoped and will be read from .wikirc.yaml after /use <workspace>.
663
+ LLM config is intentionally workspace-scoped and is read from .wikirc.yaml after /use <workspace>.
609
664
  Headless mode supports one-shot workspace prompts and skill runs with log output.
610
665
  `;
611
666
  }
@@ -618,6 +673,16 @@ export async function handleSlashCommand(line, context) {
618
673
  const args = line.slice(1).trim().split(/\s+/).filter(Boolean);
619
674
  const [command] = args;
620
675
  const step = context.onStep ?? (() => {});
676
+ const runAgentCommand = async (fn, verb) => {
677
+ try {
678
+ step(`Agents: ${verb}ing external agents…`);
679
+ const output = await fn();
680
+ return { output: output || `Agents ${verb}ed.` };
681
+ } catch (err) {
682
+ step(formatActivityError('agents', verb, err));
683
+ return { output: err instanceof Error ? err.message : String(err) };
684
+ }
685
+ };
621
686
 
622
687
  switch (command) {
623
688
  case '':
@@ -631,17 +696,6 @@ export async function handleSlashCommand(line, context) {
631
696
  case 'agent':
632
697
  context.session.chatMode = false;
633
698
  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
699
  case 'status': {
646
700
  step('Shell: refreshing workspace, services and MCP status…');
647
701
  return { output: await statusText(context.session) };
@@ -649,21 +703,20 @@ export async function handleSlashCommand(line, context) {
649
703
  case 'use': {
650
704
  const workspaceName = args[1];
651
705
  if (!workspaceName) {
706
+ return { output: formatWorkspaceList(listWorkspaces(), context.session) };
707
+ }
708
+ if (args[2]) {
652
709
  return { output: 'Usage: /use <workspace>' };
653
710
  }
654
711
  const workspace = findWorkspace(workspaceName);
655
712
  if (!workspace) {
656
713
  return { output: `Workspace not found: ${workspaceName}` };
657
714
  }
715
+ clearWorkspaceSession(context.session);
658
716
  context.session.workspace = workspace.name;
659
717
  context.session.workspacePath = workspace.workspacePath;
660
718
  context.session.workspaceEnv = workspace.env;
661
719
  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
720
  context.session.systemPrompt = loadWorkspaceSystemPrompt(workspace.workspacePath);
668
721
  try {
669
722
  step(`Workspace: loading ${workspace.name} config…`);
@@ -778,6 +831,7 @@ export async function handleSlashCommand(line, context) {
778
831
  }
779
832
  case 'start': {
780
833
  const service = args[1];
834
+ if (service === 'agents') return runAgentCommand(startAgents, 'start');
781
835
  try {
782
836
  step(`Services: starting ${service ?? 'workspace services'}…`);
783
837
  const output = await startService(context.session, service);
@@ -792,6 +846,7 @@ export async function handleSlashCommand(line, context) {
792
846
  }
793
847
  case 'stop': {
794
848
  const service = args[1];
849
+ if (service === 'agents') return runAgentCommand(stopAgents, 'stop');
795
850
  try {
796
851
  step(`Services: stopping ${service ?? 'workspace services'}…`);
797
852
  const output = await stopService(context.session, service);
@@ -955,12 +1010,50 @@ export async function handleSlashCommand(line, context) {
955
1010
  case 'new': {
956
1011
  return createWorkspaceCommand(context, args[1], args[2] ?? null);
957
1012
  }
958
- case 'workspace': {
959
- const subcommand = args[1];
960
- if (subcommand !== 'init') {
961
- return { output: 'Usage: /new <name> [path]\nLegacy: /workspace init <name> [path]' };
1013
+ case 'workspace':
1014
+ case 'workplace': {
1015
+ const subcommand = args[1] ?? 'list';
1016
+ if (subcommand === 'list') {
1017
+ return { output: formatWorkspaceList(listWorkspaces(), context.session) };
1018
+ }
1019
+ if (subcommand === 'delete') {
1020
+ const workspaceName = args[2];
1021
+ const confirmed = args.includes('--confirm');
1022
+ const workspaces = listWorkspaces();
1023
+ if (!workspaceName) return { output: workspaceDeletePrompt(workspaces) };
1024
+ const workspace = workspaces.find((item) => item.name === workspaceName);
1025
+ if (!workspace) return { output: `Workspace not found: ${workspaceName}` };
1026
+ if (!confirmed) {
1027
+ return {
1028
+ output: [
1029
+ `Confirm workspace deletion: ${workspace.name}`,
1030
+ `Path: ${workspace.workspacePath}`,
1031
+ 'This removes the registry entry and deletes the workspace files.',
1032
+ '',
1033
+ `Run: /workspace delete ${workspace.name} --confirm`,
1034
+ ].join('\n'),
1035
+ };
1036
+ }
1037
+ try {
1038
+ step(`Workspace: deleting ${workspace.name}…`);
1039
+ const result = await deleteWorkspaceAndFiles(workspace, workspace.workspacePath);
1040
+ const wasCurrent = context.session.workspace === workspace.name
1041
+ || context.session.workspacePath === workspace.workspacePath;
1042
+ if (wasCurrent) clearWorkspaceSession(context.session);
1043
+ return {
1044
+ output: [
1045
+ `Deleted workspace: ${workspace.name}`,
1046
+ `Removed registry entry and files at: ${result.deletedPath}`,
1047
+ wasCurrent ? 'Current session cleared. Use /use <workspace> or /workspace init <name> [path].' : null,
1048
+ ].filter(Boolean).join('\n'),
1049
+ };
1050
+ } catch (err) {
1051
+ const message = err instanceof Error ? err.message : String(err);
1052
+ return { output: message };
1053
+ }
962
1054
  }
963
- return createWorkspaceCommand(context, args[2], args[3] ?? null);
1055
+ if (subcommand === 'init') return createWorkspaceCommand(context, args[2], args[3] ?? null);
1056
+ return { output: 'Usage: /workspace <list|delete <name> --confirm|init <name> [path]>' };
964
1057
  }
965
1058
  case 'wiki': {
966
1059
  const subcommand = args[1];
@@ -0,0 +1,146 @@
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
+ import { completionContext } from '../shell/repl.js';
9
+
10
+ test('/workspace delete removes files and clears current session context after confirmation', async () => {
11
+ const root = await mkdtemp(join(tmpdir(), 'wiki-manager-delete-workspace-'));
12
+ const registryRoot = join(root, 'registry');
13
+ const registryPath = join(registryRoot, 'demo');
14
+ const workspacePath = join(root, 'workspace');
15
+ mkdirSync(registryPath, { recursive: true });
16
+ mkdirSync(workspacePath, { recursive: true });
17
+ writeFileSync(join(registryPath, '.env'), [
18
+ 'WORKSPACE_NAME=demo',
19
+ `WIKI_WORKSPACE_PATH=${workspacePath}`,
20
+ '',
21
+ ].join('\n'), 'utf8');
22
+
23
+ const previousDir = process.env.WIKI_WORKSPACES_DIR;
24
+ process.env.WIKI_WORKSPACES_DIR = registryRoot;
25
+ const session = {
26
+ workspace: 'demo',
27
+ workspacePath,
28
+ workspaceEnv: { WORKSPACE_NAME: 'demo' },
29
+ workspaceEnvFile: join(registryPath, '.env'),
30
+ wikirc: { profile: 'default' },
31
+ wikircConfig: {},
32
+ language: 'en-US',
33
+ llm: {},
34
+ mcp: {},
35
+ systemPrompt: 'prompt',
36
+ };
37
+
38
+ try {
39
+ const prompt = await handleSlashCommand('/workspace delete demo', {
40
+ packageJson: { version: 'test' },
41
+ session,
42
+ });
43
+ assert.match(prompt.output, /Confirm workspace deletion: demo/);
44
+ assert.equal(existsSync(workspacePath), true);
45
+ assert.equal(session.workspace, 'demo');
46
+
47
+ const result = await handleSlashCommand('/workspace delete demo --confirm', {
48
+ packageJson: { version: 'test' },
49
+ session,
50
+ });
51
+ assert.match(result.output, /Deleted workspace: demo/);
52
+ assert.equal(session.workspace, null);
53
+ assert.equal(session.workspacePath, null);
54
+ assert.equal(session.llm, null);
55
+ assert.equal(session.mcp, null);
56
+ } finally {
57
+ if (previousDir === undefined) delete process.env.WIKI_WORKSPACES_DIR;
58
+ else process.env.WIKI_WORKSPACES_DIR = previousDir;
59
+ }
60
+ });
61
+
62
+ test('/new without a name shows usage', async () => {
63
+ const result = await handleSlashCommand('/new', {
64
+ packageJson: { version: 'test' },
65
+ session: {},
66
+ });
67
+
68
+ assert.match(result.output ?? '', /Usage/i);
69
+ });
70
+
71
+ test('/use loads only workspaces and /config use switches wikirc profiles', async () => {
72
+ const root = await mkdtemp(join(tmpdir(), 'wiki-manager-use-profile-'));
73
+ const registryRoot = join(root, 'registry');
74
+ const registryPath = join(registryRoot, 'demo');
75
+ const workspacePath = join(root, 'workspace');
76
+ mkdirSync(registryPath, { recursive: true });
77
+ mkdirSync(workspacePath, { recursive: true });
78
+ writeFileSync(join(registryPath, '.env'), [
79
+ 'WORKSPACE_NAME=demo',
80
+ `WIKI_WORKSPACE_PATH=${workspacePath}`,
81
+ '',
82
+ ].join('\n'), 'utf8');
83
+ writeFileSync(join(workspacePath, '.wikirc.yaml'), [
84
+ 'language: fr',
85
+ 'llm:',
86
+ ' provider: default-provider',
87
+ ' model: default-model',
88
+ '',
89
+ ].join('\n'), 'utf8');
90
+ writeFileSync(join(workspacePath, '.wikirc.yaml.vpn'), [
91
+ 'language: fr',
92
+ 'llm:',
93
+ ' provider: vpn-provider',
94
+ ' model: vpn-model',
95
+ '',
96
+ ].join('\n'), 'utf8');
97
+
98
+ const previousDir = process.env.WIKI_WORKSPACES_DIR;
99
+ process.env.WIKI_WORKSPACES_DIR = registryRoot;
100
+
101
+ try {
102
+ const session = {};
103
+ const listResult = await handleSlashCommand('/use', {
104
+ packageJson: { version: 'test' },
105
+ session,
106
+ });
107
+ assert.match(listResult.output ?? '', /Workspaces/);
108
+ assert.match(listResult.output ?? '', /demo\tavailable/);
109
+ assert.doesNotMatch(listResult.output ?? '', /vpn\t\.wikirc\.yaml\.vpn/);
110
+
111
+ const useResult = await handleSlashCommand('/use demo', {
112
+ packageJson: { version: 'test' },
113
+ session,
114
+ });
115
+
116
+ assert.equal(session.workspace, 'demo');
117
+ assert.equal(session.wikirc.profile, 'default');
118
+ assert.match(useResult.output ?? '', /profile: default/);
119
+ assert.match(useResult.output ?? '', /\* default\t\.wikirc\.yaml/);
120
+ assert.match(useResult.output ?? '', /vpn\t\.wikirc\.yaml\.vpn/);
121
+ assert.match(useResult.output ?? '', /Switch config: \/config use <profile>/);
122
+
123
+ const invalidUse = await handleSlashCommand('/use demo vpn', {
124
+ packageJson: { version: 'test' },
125
+ session,
126
+ });
127
+ assert.match(invalidUse.output ?? '', /Usage: \/use <workspace>/);
128
+ assert.equal(session.wikirc.profile, 'default');
129
+
130
+ const result = await handleSlashCommand('/config use vpn', {
131
+ packageJson: { version: 'test' },
132
+ session,
133
+ });
134
+ assert.equal(session.workspace, 'demo');
135
+ assert.equal(session.wikirc.profile, 'vpn');
136
+ assert.match(result.output ?? '', /profile=vpn/);
137
+
138
+ const completion = completionContext('/use ', { commands: ['use'] });
139
+ assert.deepEqual(completion?.matches, ['demo']);
140
+ const configCompletion = completionContext('/config use ', session);
141
+ assert.deepEqual(configCompletion?.matches, ['default', 'vpn']);
142
+ } finally {
143
+ if (previousDir === undefined) delete process.env.WIKI_WORKSPACES_DIR;
144
+ else process.env.WIKI_WORKSPACES_DIR = previousDir;
145
+ }
146
+ });
@@ -113,6 +113,7 @@ function composeEnv(session) {
113
113
  ...cacertEnv(),
114
114
  WORKSPACE_NAME: session.workspace,
115
115
  WIKI_WORKSPACE_PATH: session.workspacePath,
116
+ ...(session.wikirc?.fileName && { WIKI_CONFIG_PATH: session.wikirc.fileName }),
116
117
  };
117
118
  }
118
119
 
@@ -238,7 +239,7 @@ export async function composePs(session) {
238
239
  return runCompose(session, ['ps'], { timeout: 30_000 });
239
240
  }
240
241
 
241
- function parseComposePsJson(output) {
242
+ export function parseComposePsJson(output) {
242
243
  const text = output.trim();
243
244
  if (!text) return [];
244
245
  try {
@@ -303,8 +304,7 @@ export async function runWikiCli(session, args, options = {}) {
303
304
  if (!Array.isArray(args) || args.length === 0) {
304
305
  throw new Error('Usage: /wiki run <args...>');
305
306
  }
306
- const configEnv = session.wikirc?.fileName ? ['-e', `WIKI_CONFIG_PATH=${session.wikirc.fileName}`] : [];
307
- return runCompose(session, ['run', '--rm', ...configEnv, 'wiki', ...args], {
307
+ return runCompose(session, ['run', '--rm', 'wiki', ...args], {
308
308
  timeout: options.timeout ?? 180_000,
309
309
  maxBuffer: options.maxBuffer ?? 1024 * 1024 * 8,
310
310
  onOutput: options.onOutput,
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.47' },
215
215
  },
216
216
  }),
217
217
  });