@dotdrelle/wiki-manager 0.7.3 → 0.9.3

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.
Files changed (39) hide show
  1. package/.env.example +20 -0
  2. package/README.md +50 -1
  3. package/docker-compose.yml +1 -23
  4. package/mcp.endpoints.example.json +13 -0
  5. package/package.json +2 -2
  6. package/src/agent/graph.js +101 -15
  7. package/src/agent/graph.test.js +145 -0
  8. package/src/cli/wiki-manager.js +306 -53
  9. package/src/commands/slash.js +4 -24
  10. package/src/core/agentEvents.js +169 -4
  11. package/src/core/agentEvents.test.js +176 -4
  12. package/src/core/agentLoop.js +3 -0
  13. package/src/core/compose.js +1 -2
  14. package/src/core/dockerCompose.test.js +5 -5
  15. package/src/core/jobQueue.js +29 -12
  16. package/src/core/mcp.js +120 -10
  17. package/src/core/mcp.test.js +121 -1
  18. package/src/core/plan.js +33 -0
  19. package/src/core/queueStore.test.js +1 -0
  20. package/src/core/sessionConfig.js +24 -0
  21. package/src/core/wikiWorkspace.test.js +24 -0
  22. package/src/runtime/approvals.js +113 -0
  23. package/src/runtime/auth.test.js +8 -0
  24. package/src/runtime/client.js +52 -6
  25. package/src/runtime/lifecycle.js +27 -3
  26. package/src/runtime/queueStore.js +3 -3
  27. package/src/runtime/runner.js +340 -0
  28. package/src/runtime/runner.test.js +270 -0
  29. package/src/runtime/server.js +252 -33
  30. package/src/runtime/server.test.js +577 -0
  31. package/src/runtime/store.js +181 -39
  32. package/src/runtime/store.test.js +363 -4
  33. package/src/runtime/supervisor.js +6 -0
  34. package/src/runtime/supervisor.test.js +141 -0
  35. package/src/shell/RightPane.tsx +1 -1
  36. package/src/shell/repl.js +22 -6
  37. package/src/shell/useAgent.ts +1 -1
  38. package/src/shell/useSession.ts +10 -5
  39. package/wiki-workspace +198 -4
@@ -1,4 +1,3 @@
1
- import { randomUUID } from 'node:crypto';
2
1
  import { readFileSync } from 'node:fs';
3
2
  import { mkdir, writeFile } from 'node:fs/promises';
4
3
  import { dirname, join, resolve } from 'node:path';
@@ -6,9 +5,11 @@ import { fileURLToPath } from 'node:url';
6
5
  import { loadManagerEnv } from '../core/env.js';
7
6
  loadManagerEnv();
8
7
  import { createAgentGraph } from '../agent/graph.js';
9
- import { handleSlashCommand, printHelp, printVersion } from '../commands/slash.js';
8
+ import { handleSlashCommand, printHelp, printVersion, refreshMcpRuntimeStatus } from '../commands/slash.js';
10
9
  import { runShell } from '../shell/repl.js';
11
10
  import { runChecks } from '../core/startupCheck.js';
11
+ import { applySessionWikircProfile } from '../core/sessionConfig.js';
12
+ import { listWikircProfiles } from '../core/wikirc.js';
12
13
  import { callMcpTool, formatMcpToolResult } from '../core/mcp.js';
13
14
  import { extractActivity, parseJsonText, sessionActivities, terminalFailures } from '../core/activity.js';
14
15
  import { syncActivitiesToPlan, formatPlanStatus } from '../core/plan.js';
@@ -20,7 +21,7 @@ import { runAgentTurn, runAgenticLoop } from '../core/agentLoop.js';
20
21
  const __dirname = dirname(fileURLToPath(import.meta.url));
21
22
  const packageJsonPath = resolve(__dirname, '../../package.json');
22
23
  const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
23
- const SHELL_COMMANDS = ['help', 'version', 'exit', 'workspace', 'new', 'use', 'config', 'status', 'services', 'start', 'stop', 'logs', 'mcp', 'wiki', 'skills', 'clear', 'chat', 'agent'];
24
+ const SHELL_COMMANDS = ['help', 'version', 'exit', 'workspace', 'new', 'use', 'config', 'status', 'services', 'start', 'stop', 'logs', 'mcp', 'wiki', 'skills', 'clear', 'chat', 'agent', 'approve'];
24
25
 
25
26
  function valueAfter(argv, flag) {
26
27
  const index = argv.indexOf(flag);
@@ -286,12 +287,13 @@ async function runRuntime(argv, agent) {
286
287
  ].join('\n'));
287
288
  return;
288
289
  }
289
- const { defaultRuntimeStateDir, openRuntimeStore } = await import('../runtime/store.js');
290
+ const { defaultRuntimeStateDir, openRuntimeStore, RECOVERABLE_QUEUE_STATUSES } = await import('../runtime/store.js');
290
291
  const { startRuntimeServer } = await import('../runtime/server.js');
291
292
  const { emitRuntimeLog, startActivitySupervisor } = await import('../runtime/supervisor.js');
292
293
  const { resolveRuntimeAuthToken } = await import('../runtime/auth.js');
293
294
  const { createSqliteQueueStore } = await import('../runtime/queueStore.js');
294
- const { runRuntimeAgenticLoop } = await import('../runtime/runner.js');
295
+ const { createApprovalManager } = await import('../runtime/approvals.js');
296
+ const { runRuntimeAgenticWorkflow } = await import('../runtime/runner.js');
295
297
 
296
298
  const host = valueAfter(argv, '--host') ?? process.env.WIKI_MANAGER_RUNTIME_HOST ?? '0.0.0.0';
297
299
  const port = Number(valueAfter(argv, '--port') ?? process.env.WIKI_MANAGER_RUNTIME_PORT ?? 7788);
@@ -302,45 +304,270 @@ async function runRuntime(argv, agent) {
302
304
  throw new Error(`Invalid runtime port: ${port}`);
303
305
  }
304
306
 
305
- const session = createSession();
306
- session.headless = true;
307
- session.chatMode = false;
308
- session.packageJson = packageJson;
309
-
310
307
  const store = openRuntimeStore({ stateDir });
311
- store.hydrateSession(session);
312
- session.queueStore = createSqliteQueueStore(store, session);
313
-
314
308
  let serverHandle = null;
315
- session._onAgentEvent = (event) => {
316
- store.persistEvent(event);
317
- serverHandle?.publish(event);
318
- };
319
- session._onRuntimeError = (err) => {
320
- const message = err instanceof Error ? err.message : String(err);
321
- dispatchAgentEvent(session, createAgentEvent('run_error', {
322
- origin: 'runtime',
323
- payload: { message },
324
- }));
325
- };
309
+ const contexts = new Map();
310
+
311
+ async function getWorkspaceContext(workspaceName = null) {
312
+ const requestedWorkspace = workspaceName ? String(workspaceName).trim() : null;
313
+ const key = requestedWorkspace ?? '__default__';
314
+ if (contexts.has(key)) return contexts.get(key);
315
+
316
+ const pending = (async () => {
317
+ const session = createSession();
318
+ session.headless = true;
319
+ session.chatMode = false;
320
+ session.packageJson = packageJson;
321
+
322
+ if (requestedWorkspace) {
323
+ const result = await handleSlashCommand(`/use ${requestedWorkspace}`, { packageJson, session });
324
+ if (!session.workspacePath) throw new Error(result.output || `Workspace not loaded: ${requestedWorkspace}`);
325
+ }
326
+ const workspace = session.workspace ?? requestedWorkspace ?? null;
327
+ store.hydrateSession(session, { workspace });
328
+ session.queueStore = createSqliteQueueStore(store, session, { workspace });
329
+
330
+ const context = {
331
+ workspace,
332
+ session,
333
+ supervisor: null,
334
+ running: false,
335
+ currentAbortController: null,
336
+ approvalManager: null,
337
+ };
338
+ session._onAgentEvent = (event) => {
339
+ store.persistEvent(event);
340
+ serverHandle?.publish(event);
341
+ };
342
+ session._onRuntimeError = (err) => {
343
+ const message = err instanceof Error ? err.message : String(err);
344
+ dispatchAgentEvent(session, createAgentEvent('run_error', {
345
+ origin: 'runtime',
346
+ payload: { message, workspace },
347
+ }));
348
+ };
349
+ context.approvalManager = createApprovalManager(session, {
350
+ defaultTimeoutMs: Number.isFinite(Number(process.env.WIKI_MANAGER_APPROVAL_TIMEOUT_MS))
351
+ ? Math.max(1, Number(process.env.WIKI_MANAGER_APPROVAL_TIMEOUT_MS))
352
+ : undefined,
353
+ });
354
+ session._requestApproval = (request) => context.approvalManager.requestApproval(request);
355
+ context.supervisor = startActivitySupervisor(session);
356
+ contexts.set(key, context);
357
+ if (workspace && workspace !== key) contexts.set(workspace, context);
358
+ return context;
359
+ })();
360
+ contexts.set(key, pending);
361
+ pending.catch(() => {
362
+ if (contexts.get(key) === pending) contexts.delete(key);
363
+ });
364
+ return pending;
365
+ }
366
+
367
+ function recoveryMcpGaps(context) {
368
+ const gaps = [];
369
+ const session = context.session;
370
+ for (const activity of sessionActivities(session)) {
371
+ if (activity.terminal || !activity.poll?.server) continue;
372
+ const endpoint = session.mcp?.[activity.poll.server];
373
+ if (endpoint?.status !== 'connected') gaps.push(activity.poll.server);
374
+ }
375
+ for (const item of session.queueStore?.list?.() ?? []) {
376
+ const status = String(item.status ?? '').toLowerCase();
377
+ if (!RECOVERABLE_QUEUE_STATUSES.includes(status)) continue;
378
+ const endpoint = session.mcp?.[item.server ?? 'production'];
379
+ if (endpoint?.status !== 'connected') gaps.push(item.server ?? 'production');
380
+ }
381
+ return [...new Set(gaps)];
382
+ }
383
+
384
+ function activeNonTerminalActivities(session) {
385
+ return sessionActivities(session).filter((activity) => !activity.terminal);
386
+ }
387
+
388
+ function activePollingActivities(session) {
389
+ return activeNonTerminalActivities(session).filter((activity) => activity.poll);
390
+ }
391
+
392
+ function buildRecoveryPrompt(run, session) {
393
+ const conversation = session.agentProjection?.conversation ?? [];
394
+ const recentConversation = conversation
395
+ .slice(-8)
396
+ .map((message) => `${message.role}: ${message.content}`)
397
+ .join('\n');
398
+ return [
399
+ 'Resume an interrupted runtime run.',
400
+ '',
401
+ 'Original task:',
402
+ run.input ?? '(unknown)',
403
+ '',
404
+ session.headlessPlan ? `Current plan:\n${formatPlanStatus(session.headlessPlan)}` : null,
405
+ recentConversation ? `Recent conversation:\n${recentConversation}` : null,
406
+ '',
407
+ 'Continue from the current plan state. Start only the next pending step.',
408
+ 'If the work is already complete, provide a concise final summary.',
409
+ ].filter(Boolean).join('\n');
410
+ }
411
+
412
+ function startRecoveredAgenticRun(context, run) {
413
+ if (context.running) return false;
414
+ const session = context.session;
415
+ const supervisor = context.supervisor;
416
+ const runId = run.id;
417
+ const input = buildRecoveryPrompt(run, session);
418
+ context.running = true;
419
+ context.currentAbortController = new AbortController();
420
+ session._currentRunIdentity = {
421
+ runId,
422
+ turnId: `${runId}:resume-0`,
423
+ workspace: context.workspace,
424
+ };
425
+ session._abortSignal = context.currentAbortController.signal;
426
+ supervisor?.setRunSignal(context.currentAbortController.signal);
427
+ session._onStep = (message) => emitRuntimeLog(session, message);
428
+ emitRuntimeLog(session, `runtime: resuming interrupted run ${runId}`);
429
+
430
+ runRuntimeAgenticWorkflow(agent, session, run.input ?? input, {
431
+ initialInput: input,
432
+ signal: context.currentAbortController.signal,
433
+ timeoutMs: 3600 * 1000,
434
+ maxTurns: 20,
435
+ runId,
436
+ pollBusy: supervisor?.pollBusy,
437
+ })
438
+ .catch((err) => {
439
+ if (err?.name === 'AbortError') {
440
+ dispatchAgentEvent(session, createAgentEvent('run_cancelled', {
441
+ origin: 'runtime',
442
+ runId,
443
+ payload: { runId, message: 'Recovered runtime run cancelled.' },
444
+ }));
445
+ return;
446
+ }
447
+ dispatchAgentEvent(session, createAgentEvent('run_error', {
448
+ origin: 'runtime',
449
+ runId,
450
+ payload: {
451
+ runId,
452
+ message: err instanceof Error ? err.message : String(err),
453
+ },
454
+ }));
455
+ })
456
+ .finally(() => {
457
+ context.running = false;
458
+ context.currentAbortController = null;
459
+ supervisor?.setRunSignal(null);
460
+ delete session._abortSignal;
461
+ delete session._onStep;
462
+ delete session._currentRunIdentity;
463
+ });
326
464
 
327
- let supervisor = null;
465
+ return true;
466
+ }
328
467
 
329
- async function executeRun(body, { signal } = {}) {
468
+ async function recoverWorkspace(workspace, { manual = false } = {}) {
469
+ try {
470
+ const context = await getWorkspaceContext(workspace);
471
+ await refreshMcpRuntimeStatus(context.session);
472
+ const gaps = recoveryMcpGaps(context);
473
+ if (gaps.length > 0) {
474
+ const interrupted = store.interruptRuns({ workspace: context.workspace });
475
+ return {
476
+ workspace: context.workspace ?? workspace ?? null,
477
+ resumed: false,
478
+ interrupted,
479
+ reason: `MCP unavailable: ${gaps.join(', ')}`,
480
+ };
481
+ }
482
+ const recoverableRuns = store.listRecoverableRuns({ workspace: context.workspace });
483
+ const runningRun = recoverableRuns.find((run) => run.status === 'running');
484
+ const activeActivities = activeNonTerminalActivities(context.session);
485
+ const pollingActivities = activePollingActivities(context.session);
486
+ if (runningRun && activeActivities.length === 0) {
487
+ if (!runningRun.input) {
488
+ const interrupted = store.interruptRuns({ workspace: context.workspace });
489
+ return {
490
+ workspace: context.workspace ?? workspace ?? null,
491
+ resumed: false,
492
+ interrupted,
493
+ reason: 'Missing original run input.',
494
+ };
495
+ }
496
+ const started = startRecoveredAgenticRun(context, runningRun);
497
+ return {
498
+ workspace: context.workspace ?? workspace ?? null,
499
+ resumed: started,
500
+ interrupted: 0,
501
+ mode: 'agentic_loop',
502
+ };
503
+ }
504
+ if (runningRun && runningRun.input && pollingActivities.length > 0) {
505
+ context.session._onActivitiesTerminal = () => {
506
+ startRecoveredAgenticRun(context, runningRun);
507
+ };
508
+ emitRuntimeLog(context.session, `runtime: recovery watching ${pollingActivities.length} active activity(s)`);
509
+ return {
510
+ workspace: context.workspace ?? workspace ?? null,
511
+ resumed: true,
512
+ interrupted: 0,
513
+ mode: 'activity_poll_then_resume',
514
+ };
515
+ }
516
+ emitRuntimeLog(context.session, manual ? 'runtime: manual resume completed' : 'runtime: recovery completed');
517
+ const controlStarted = pollingActivities.length === 0
518
+ ? serverHandle?.drainControl?.(context) === true
519
+ : false;
520
+ return {
521
+ workspace: context.workspace ?? workspace ?? null,
522
+ resumed: true,
523
+ interrupted: 0,
524
+ mode: controlStarted ? 'control_queue' : pollingActivities.length > 0 ? 'activity_poll' : 'context',
525
+ };
526
+ } catch (err) {
527
+ const interrupted = store.interruptRuns({ workspace });
528
+ return {
529
+ workspace: workspace ?? null,
530
+ resumed: false,
531
+ interrupted,
532
+ reason: err instanceof Error ? err.message : String(err),
533
+ };
534
+ }
535
+ }
536
+
537
+ async function recoverRuntime({ workspace = null, manual = false } = {}) {
538
+ const workspaces = workspace ? [workspace] : store.listRecoverableWorkspaces();
539
+ const results = await Promise.all(workspaces.map((item) => recoverWorkspace(item, { manual })));
540
+ return {
541
+ resumed: results.filter((result) => result.resumed).length,
542
+ interrupted: results.reduce((sum, result) => sum + Number(result.interrupted ?? 0), 0),
543
+ workspaces: results,
544
+ };
545
+ }
546
+
547
+ async function executeRun(context, body, { signal } = {}) {
548
+ const session = context.session;
549
+ const supervisor = context.supervisor;
330
550
  const input = String(body.input ?? body.prompt ?? '').trim();
331
551
  const workspace = body.workspace ? String(body.workspace).trim() : null;
332
552
  const timeoutMs = (Number.isFinite(Number(body.timeout)) ? Math.max(1, Number(body.timeout)) : 3600) * 1000;
333
553
  const maxTurns = Number.isFinite(Number(body.maxTurns)) ? Math.max(1, Number(body.maxTurns)) : 20;
334
- const runId = randomUUID();
554
+ const maxReplans = Number.isFinite(Number(body.replans)) ? Math.max(0, Math.floor(Number(body.replans))) : undefined;
555
+ const runId = String(body.runId);
335
556
  try {
336
557
  if (workspace && session.workspace !== workspace) {
337
558
  const result = await handleSlashCommand(`/use ${workspace}`, { packageJson, session });
338
559
  if (!session.workspacePath) throw new Error(result.output || `Workspace not loaded: ${workspace}`);
339
560
  }
561
+ context.workspace = session.workspace ?? workspace ?? context.workspace ?? null;
562
+ session._currentRunIdentity = {
563
+ runId,
564
+ turnId: `${runId}:turn-0`,
565
+ workspace: context.workspace,
566
+ };
340
567
  dispatchAgentEvent(session, createAgentEvent('run_started', {
341
568
  origin: 'runtime',
342
569
  runId,
343
- payload: { input, workspace },
570
+ payload: { input, workspace: session._currentRunIdentity.workspace },
344
571
  }));
345
572
  dispatchAgentEvent(session, createAgentEvent('user_message', {
346
573
  origin: 'user',
@@ -348,35 +575,22 @@ async function runRuntime(argv, agent) {
348
575
  payload: { content: input },
349
576
  }));
350
577
  session._abortSignal = signal ?? null;
578
+ session._runApprovalRequired = body.requireApproval === true;
579
+ session._runApprovalResolved = false;
580
+ session._approvalTimeoutMs = Number.isFinite(Number(body.approvalTimeoutMs))
581
+ ? Math.max(1, Number(body.approvalTimeoutMs))
582
+ : undefined;
351
583
  supervisor?.setRunSignal(signal);
352
584
  session._onStep = (message) => emitRuntimeLog(session, message);
353
- const result = await runRuntimeAgenticLoop(agent, session, input, {
585
+ await runRuntimeAgenticWorkflow(agent, session, input, {
354
586
  signal,
355
587
  timeoutMs,
356
588
  maxTurns,
357
589
  runId,
358
590
  pollBusy: supervisor?.pollBusy,
591
+ evaluate: body.evaluate !== false,
592
+ ...(maxReplans === undefined ? {} : { maxReplans }),
359
593
  });
360
- if (!result.ok) {
361
- dispatchAgentEvent(session, createAgentEvent('run_error', {
362
- origin: 'runtime',
363
- runId,
364
- payload: {
365
- runId,
366
- message: result.timedOut
367
- ? 'Runtime agentic loop timed out.'
368
- : result.maxTurns
369
- ? `Runtime agentic loop reached max turns (${maxTurns}).`
370
- : 'Runtime agentic loop failed.',
371
- },
372
- }));
373
- return;
374
- }
375
- dispatchAgentEvent(session, createAgentEvent('run_done', {
376
- origin: 'runtime',
377
- runId,
378
- payload: { runId },
379
- }));
380
594
  } catch (err) {
381
595
  if (err?.name === 'AbortError') {
382
596
  dispatchAgentEvent(session, createAgentEvent('run_cancelled', {
@@ -401,6 +615,10 @@ async function runRuntime(argv, agent) {
401
615
  supervisor?.setRunSignal(null);
402
616
  delete session._abortSignal;
403
617
  delete session._onStep;
618
+ delete session._currentRunIdentity;
619
+ delete session._runApprovalRequired;
620
+ delete session._runApprovalResolved;
621
+ delete session._approvalTimeoutMs;
404
622
  }
405
623
  }
406
624
 
@@ -408,19 +626,54 @@ async function runRuntime(argv, agent) {
408
626
  host,
409
627
  port,
410
628
  store,
411
- session,
629
+ getContext: getWorkspaceContext,
412
630
  run: executeRun,
413
- cancel: () => emitRuntimeLog(session, 'runtime: cancel requested'),
631
+ cancel: (context) => emitRuntimeLog(context.session, 'runtime: cancel requested'),
632
+ resume: ({ workspace }) => recoverRuntime({ workspace, manual: true }),
633
+ approve: async ({ workspace, runId, itemId, approvalId }) => {
634
+ const context = await getWorkspaceContext(workspace);
635
+ return context.approvalManager?.approve({ runId, itemId, approvalId }) ?? { approved: false };
636
+ },
637
+ configProfiles: async (context) => {
638
+ const profiles = listWikircProfiles(context.session.workspacePath);
639
+ return {
640
+ profiles: profiles.map((profile) => profile.name),
641
+ active: context.session.wikirc?.profile ?? null,
642
+ items: profiles.map((profile) => ({
643
+ name: profile.name,
644
+ fileName: profile.fileName,
645
+ default: Boolean(profile.default),
646
+ })),
647
+ };
648
+ },
649
+ useConfigProfile: async (context, profile) => {
650
+ const { summary, config } = applySessionWikircProfile(context.session, profile);
651
+ await refreshMcpRuntimeStatus(context.session);
652
+ dispatchAgentEvent(context.session, createAgentEvent('runtime_log', {
653
+ origin: 'runtime',
654
+ payload: { message: `runtime: config profile switched to ${context.session.wikirc?.profile ?? profile}` },
655
+ }));
656
+ return {
657
+ ok: true,
658
+ active: context.session.wikirc?.profile ?? profile,
659
+ fileName: context.session.wikirc?.fileName ?? null,
660
+ summary,
661
+ config,
662
+ };
663
+ },
414
664
  token: auth.token,
415
665
  });
416
- supervisor = startActivitySupervisor(session);
666
+ const recovery = await recoverRuntime();
417
667
 
418
668
  console.log(`wiki-manager runtime listening on http://${host}:${port}`);
419
669
  console.log(`runtime state: ${store.dbPath}`);
670
+ if (recovery.resumed > 0 || recovery.interrupted > 0) {
671
+ console.log(`runtime recovery: resumed=${recovery.resumed} interrupted=${recovery.interrupted}`);
672
+ }
420
673
  if (auth.tokenPath) console.log(`runtime token: ${auth.tokenPath}`);
421
674
 
422
675
  const shutdown = async () => {
423
- supervisor?.stop();
676
+ await Promise.all([...new Set(contexts.values())].map(async (v) => { (await v).supervisor?.stop(); }));
424
677
  await serverHandle.close();
425
678
  store.close();
426
679
  process.exit(0);
@@ -1,7 +1,6 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
3
3
  import { join, relative } from 'node:path';
4
- import { createLlmClientFromWikiConfig } from '../agent/llm.js';
5
4
  import { composeServices, listServices, runWikiCli, serviceLogs, serviceStates, startService, stopService } from '../core/compose.js';
6
5
  import {
7
6
  applyMcpRuntimeStatus,
@@ -26,10 +25,10 @@ import {
26
25
  } from '../core/jobQueue.js';
27
26
  import {
28
27
  listWikircProfiles,
29
- loadWikircProfile,
30
28
  resolveWikircProfile,
31
29
  summarizeWikircConfig,
32
30
  } from '../core/wikirc.js';
31
+ import { applySessionWikircProfile } from '../core/sessionConfig.js';
33
32
  import { deleteWorkspaceAndFiles, startAgents, stopAgents } from '../core/wikiSetup.js';
34
33
  import {
35
34
  cleanDocumentUploads,
@@ -490,7 +489,7 @@ function publishDocumentActivity(session, activity) {
490
489
  return publishPayloadActivity(session, { _activity: activity }, { server: 'documents', tool: 'documents_convert_to_markdown' });
491
490
  }
492
491
 
493
- async function refreshMcpRuntimeStatus(session) {
492
+ export async function refreshMcpRuntimeStatus(session) {
494
493
  session.mcp = buildMcpStatus(session);
495
494
  if (!session.workspacePath) return null;
496
495
  try {
@@ -546,25 +545,6 @@ function loadWorkspaceSystemPrompt(workspacePath) {
546
545
  return existsSync(promptPath) ? readFileSync(promptPath, 'utf8').trim() || null : null;
547
546
  }
548
547
 
549
- function loadSessionWikirc(session, profileName = 'default') {
550
- if (!session.workspacePath) {
551
- throw new Error('No workspace loaded. Use /use <workspace>.');
552
- }
553
- const loaded = loadWikircProfile(session.workspacePath, profileName);
554
- session.wikirc = {
555
- profile: loaded.profile.name,
556
- fileName: loaded.profile.fileName,
557
- path: loaded.profile.path,
558
- };
559
- session.wikircConfig = loaded.config;
560
- session.language = loaded.config?.language ?? null;
561
- session.llm = createLlmClientFromWikiConfig(loaded.config);
562
- if (session.mcp?.production) {
563
- session.mcp.production.activeConfigPath = loaded.profile.fileName;
564
- }
565
- return summarizeWikircConfig(loaded.profile, loaded.config);
566
- }
567
-
568
548
  function clearWorkspaceSession(session) {
569
549
  session.workspace = null;
570
550
  session.workspacePath = null;
@@ -720,7 +700,7 @@ export async function handleSlashCommand(line, context) {
720
700
  context.session.systemPrompt = loadWorkspaceSystemPrompt(workspace.workspacePath);
721
701
  try {
722
702
  step(`Workspace: loading ${workspace.name} config…`);
723
- const summary = loadSessionWikirc(context.session, 'default');
703
+ const { summary } = applySessionWikircProfile(context.session, 'default');
724
704
  step(`Workspace: discovering ${workspace.name} MCP tools…`);
725
705
  await refreshMcpRuntimeStatus(context.session);
726
706
  return {
@@ -759,7 +739,7 @@ export async function handleSlashCommand(line, context) {
759
739
  return { output: 'Usage: /config use <default|name>' };
760
740
  }
761
741
  try {
762
- const summary = loadSessionWikirc(context.session, profileName);
742
+ const { summary } = applySessionWikircProfile(context.session, profileName);
763
743
  await refreshMcpRuntimeStatus(context.session);
764
744
  return {
765
745
  output: [