mini-coder 0.5.12 → 0.5.14

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/src/index.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Entry point for mini-coder.
3
3
  *
4
- * Discovers available LLM providers, loads context (AGENTS.md, skills,
5
- * plugins), opens the session database, selects a model, and starts
6
- * the TUI.
4
+ * Discovers available LLM providers, loads configured MCP tools,
5
+ * loads prompt context (AGENTS.md, skills, and theme), opens the session
6
+ * database, selects a model, and starts the TUI.
7
7
  *
8
8
  * @module
9
9
  */
@@ -13,8 +13,8 @@ import { homedir } from "node:os";
13
13
  import { basename, dirname, join } from "node:path";
14
14
  import { isDeepStrictEqual } from "node:util";
15
15
  import type {
16
+ AssistantMessage,
16
17
  KnownProvider,
17
- Message,
18
18
  Model,
19
19
  OAuthCredentials,
20
20
  ThinkingLevel,
@@ -23,7 +23,16 @@ import type {
23
23
  } from "@mariozechner/pi-ai";
24
24
  import { getEnvApiKey, getModels, getProviders } from "@mariozechner/pi-ai";
25
25
  import { getOAuthApiKey, getOAuthProviders } from "@mariozechner/pi-ai/oauth";
26
- import type { ToolHandler } from "./agent.ts";
26
+ import {
27
+ runAgentLoop,
28
+ type ToolHandler,
29
+ type ToolUpdateCallback,
30
+ } from "./agent.ts";
31
+ import {
32
+ extractAssistantActivitySnippet,
33
+ extractAssistantErrorText,
34
+ extractAssistantText,
35
+ } from "./assistant-output.ts";
27
36
  import {
28
37
  type CliOptions,
29
38
  parseCliArgs,
@@ -31,17 +40,14 @@ import {
31
40
  shouldUseHeadlessMode,
32
41
  type TtyState,
33
42
  } from "./cli.ts";
43
+ import {
44
+ readShellDelegationContext,
45
+ type ShellDelegationContext,
46
+ } from "./delegation.ts";
34
47
  import { getErrorMessage } from "./errors.ts";
35
48
  import { type GitState, getGitState } from "./git.ts";
49
+ import { discoverMcpServers, type McpServerState } from "./mcp.ts";
36
50
  import { canonicalizePath } from "./paths.ts";
37
- import {
38
- type AgentContext,
39
- destroyPlugins,
40
- initPlugins,
41
- type LoadedPlugin,
42
- loadPluginConfig,
43
- type PluginEntry,
44
- } from "./plugins.ts";
45
51
  import {
46
52
  type AgentsMdFile,
47
53
  buildSystemPrompt,
@@ -52,7 +58,8 @@ import {
52
58
  appendMessage,
53
59
  createConversationSnapshot,
54
60
  createSession,
55
- filterModelMessages,
61
+ deleteSession,
62
+ loadCompactedModelMessages,
56
63
  type loadMessages,
57
64
  openDatabase,
58
65
  type Session,
@@ -61,15 +68,20 @@ import {
61
68
  } from "./session.ts";
62
69
  import {
63
70
  type CustomProvider,
64
- loadSettings,
71
+ loadStartupSettings,
72
+ mergeUserSettings,
65
73
  resolveStartupSettings,
66
74
  type UserSettings,
67
75
  } from "./settings.ts";
68
76
  import { discoverSkills, type Skill } from "./skills.ts";
69
- import { DEFAULT_THEME, mergeThemes, type Theme } from "./theme.ts";
77
+ import { DEFAULT_THEME, type Theme } from "./theme.ts";
70
78
  import {
79
+ createDelegateToolHandler,
80
+ createDelegationAwareShellToolHandler,
71
81
  createTodoReadToolHandler,
72
82
  createTodoWriteToolHandler,
83
+ type DelegateRunResult,
84
+ delegateTool,
73
85
  editTool,
74
86
  editToolHandler,
75
87
  grepTool,
@@ -79,7 +91,6 @@ import {
79
91
  readTool,
80
92
  readToolHandler,
81
93
  shellTool,
82
- shellToolHandler,
83
94
  todoReadTool,
84
95
  todoWriteTool,
85
96
  } from "./tools.ts";
@@ -95,9 +106,6 @@ const DATA_DIR = join(homedir(), ".config", "mini-coder");
95
106
  /** SQLite database path. */
96
107
  const DB_PATH = join(DATA_DIR, "mini-coder.db");
97
108
 
98
- /** Plugin config file path. */
99
- const PLUGIN_CONFIG_PATH = join(DATA_DIR, "plugins.json");
100
-
101
109
  /** OAuth credentials file path. */
102
110
  const AUTH_PATH = join(DATA_DIR, "auth.json");
103
111
 
@@ -362,19 +370,128 @@ function selectModel(
362
370
  // Tool wiring
363
371
  // ---------------------------------------------------------------------------
364
372
 
373
+ type ToolRuntimeState = Pick<
374
+ AppState,
375
+ | "agentsMd"
376
+ | "cwd"
377
+ | "db"
378
+ | "delegationDepth"
379
+ | "delegationBudgetRemaining"
380
+ | "effort"
381
+ | "git"
382
+ | "mcpServers"
383
+ | "messages"
384
+ | "providers"
385
+ | "skills"
386
+ > & {
387
+ model: Model<string>;
388
+ };
389
+
390
+ /**
391
+ * Run one isolated delegated subtask with the current model and toolset.
392
+ *
393
+ * @param task - Raw delegated subtask prompt.
394
+ * @param state - Runtime state to inherit into the delegated child run.
395
+ * @param context - Delegation context reserved for the child run.
396
+ * @param signal - Optional abort signal from the parent run.
397
+ * @param onUpdate - Optional progressive tool-update callback.
398
+ * @returns The delegated subagent result summary.
399
+ */
400
+ async function runDelegatedTask(
401
+ task: string,
402
+ state: ToolRuntimeState,
403
+ context: ShellDelegationContext,
404
+ signal?: AbortSignal,
405
+ onUpdate?: ToolUpdateCallback,
406
+ ): Promise<DelegateRunResult> {
407
+ const session = createSession(state.db, {
408
+ cwd: state.cwd,
409
+ model: `${state.model.provider}/${state.model.id}`,
410
+ effort: state.effort,
411
+ });
412
+ let finalAssistantMessage: AssistantMessage | null = null;
413
+
414
+ try {
415
+ const userMessage = {
416
+ role: "user",
417
+ content: task,
418
+ timestamp: Date.now(),
419
+ } satisfies UserMessage;
420
+ const turn = appendMessage(state.db, session.id, userMessage);
421
+ const messages = loadCompactedModelMessages(state.db, session.id);
422
+ const childState: ToolRuntimeState = {
423
+ ...state,
424
+ delegationDepth: context.depth,
425
+ delegationBudgetRemaining: context.remainingBudget,
426
+ messages,
427
+ };
428
+ const { tools, toolHandlers } = buildTools(childState);
429
+
430
+ const result = await runAgentLoop({
431
+ db: state.db,
432
+ sessionId: session.id,
433
+ turn,
434
+ model: state.model,
435
+ systemPrompt: buildPrompt(childState),
436
+ tools,
437
+ toolHandlers,
438
+ messages,
439
+ cwd: state.cwd,
440
+ apiKey: state.providers.get(state.model.provider),
441
+ effort: state.effort,
442
+ signal,
443
+ onEvent: (event) => {
444
+ switch (event.type) {
445
+ case "assistant_message": {
446
+ const snippet = extractAssistantActivitySnippet(event.message);
447
+ if (snippet) {
448
+ onUpdate?.({
449
+ content: [
450
+ {
451
+ type: "text",
452
+ text: `Subagent: ${snippet}`,
453
+ },
454
+ ],
455
+ isError: false,
456
+ });
457
+ }
458
+ break;
459
+ }
460
+ case "done":
461
+ case "error":
462
+ case "aborted":
463
+ finalAssistantMessage = event.message;
464
+ break;
465
+ default:
466
+ break;
467
+ }
468
+ },
469
+ });
470
+
471
+ return {
472
+ stopReason: result.stopReason,
473
+ finalText: extractAssistantText(finalAssistantMessage),
474
+ errorText: extractAssistantErrorText(finalAssistantMessage),
475
+ };
476
+ } finally {
477
+ deleteSession(state.db, session.id);
478
+ }
479
+ }
480
+
365
481
  /**
366
482
  * Build tool definitions and handler map for the current model.
367
483
  *
368
484
  * Returns the `Tool[]` to send to the model and the handler map
369
485
  * for the agent loop to dispatch tool calls.
370
486
  */
371
- function buildTools(
372
- model: Model<string>,
373
- plugins: LoadedPlugin[],
374
- messages: AppState["messages"],
375
- ): { tools: Tool[]; toolHandlers: Map<string, ToolHandler> } {
487
+ function buildTools(state: ToolRuntimeState): {
488
+ tools: Tool[];
489
+ toolHandlers: Map<string, ToolHandler>;
490
+ } {
491
+ const { mcpServers, messages, model } = state;
376
492
  const tools: Tool[] = [
377
493
  shellTool,
494
+ delegateTool,
378
495
  readTool,
379
496
  grepTool,
380
497
  editTool,
@@ -382,7 +499,33 @@ function buildTools(
382
499
  todoReadTool,
383
500
  ];
384
501
  const toolHandlers = new Map<string, ToolHandler>([
385
- [shellTool.name, shellToolHandler],
502
+ [
503
+ shellTool.name,
504
+ createDelegationAwareShellToolHandler({
505
+ getDelegationContext: () => ({
506
+ depth: state.delegationDepth,
507
+ remainingBudget: state.delegationBudgetRemaining,
508
+ }),
509
+ setDelegationContext: (context) => {
510
+ state.delegationBudgetRemaining = context.remainingBudget;
511
+ },
512
+ }),
513
+ ],
514
+ [
515
+ delegateTool.name,
516
+ createDelegateToolHandler({
517
+ getDelegationContext: () => ({
518
+ depth: state.delegationDepth,
519
+ remainingBudget: state.delegationBudgetRemaining,
520
+ }),
521
+ setDelegationContext: (context) => {
522
+ state.delegationBudgetRemaining = context.remainingBudget;
523
+ },
524
+ runSubagent: (task, context, signal, onUpdate) => {
525
+ return runDelegatedTask(task, state, context, signal, onUpdate);
526
+ },
527
+ }),
528
+ ],
386
529
  [readTool.name, readToolHandler],
387
530
  [grepTool.name, grepToolHandler],
388
531
  [editTool.name, editToolHandler],
@@ -390,26 +533,23 @@ function buildTools(
390
533
  [todoReadTool.name, createTodoReadToolHandler(messages)],
391
534
  ]);
392
535
 
536
+ for (const server of mcpServers) {
537
+ if (!server.enabled || !server.connected) {
538
+ continue;
539
+ }
540
+
541
+ tools.push(...server.tools);
542
+ for (const [name, handler] of server.toolHandlers) {
543
+ toolHandlers.set(name, handler);
544
+ }
545
+ }
546
+
393
547
  // Conditionally register readImage for vision-capable models
394
548
  if (model.input.includes("image")) {
395
549
  tools.push(readImageTool);
396
550
  toolHandlers.set(readImageTool.name, readImageToolHandler);
397
551
  }
398
552
 
399
- // Add plugin tools
400
- for (const plugin of plugins) {
401
- if (plugin.result.tools) {
402
- for (const tool of plugin.result.tools) {
403
- tools.push(tool);
404
- }
405
- }
406
- if (plugin.result.toolHandlers) {
407
- for (const [name, handler] of plugin.result.toolHandlers) {
408
- toolHandlers.set(name, handler);
409
- }
410
- }
411
- }
412
-
413
553
  return { tools, toolHandlers };
414
554
  }
415
555
 
@@ -429,21 +569,13 @@ function getSkillScanPaths(cwd: string, gitRoot: string | null): string[] {
429
569
  ];
430
570
  }
431
571
 
432
- /** Load AGENTS.md files, skills, plugins, git state, and the merged theme. */
433
- export async function loadPromptContext(
434
- messages: readonly Message[],
435
- opts?: {
436
- cwd?: string;
437
- pluginEntries?: PluginEntry[];
438
- pluginConfigPath?: string;
439
- },
440
- ): Promise<{
572
+ /** Load AGENTS.md files, skills, git state, and the active theme. */
573
+ export async function loadPromptContext(opts?: { cwd?: string }): Promise<{
441
574
  cwd: string;
442
575
  canonicalCwd: string;
443
576
  git: GitState | null;
444
577
  agentsMd: AgentsMdFile[];
445
578
  skills: Skill[];
446
- plugins: LoadedPlugin[];
447
579
  theme: Theme;
448
580
  }> {
449
581
  const cwd = opts?.cwd ?? process.cwd();
@@ -459,20 +591,6 @@ export async function loadPromptContext(
459
591
  );
460
592
  const agentsMd = discoverAgentsMd(cwd, scanRoot, join(home, ".agents"));
461
593
  const skills = discoverSkills(getSkillScanPaths(canonicalCwd, gitRoot));
462
- const pluginEntries =
463
- opts?.pluginEntries ??
464
- loadPluginConfig(opts?.pluginConfigPath ?? PLUGIN_CONFIG_PATH);
465
- const context: AgentContext = {
466
- cwd,
467
- messages,
468
- dataDir: DATA_DIR,
469
- };
470
- const plugins = await initPlugins(pluginEntries, context, (entry, err) => {
471
- console.error(`Plugin "${entry.name}" failed to init: ${err.message}`);
472
- });
473
- const themeOverrides = plugins
474
- .map((plugin) => plugin.result.theme)
475
- .filter((theme): theme is Partial<Theme> => theme != null);
476
594
 
477
595
  return {
478
596
  cwd,
@@ -480,8 +598,38 @@ export async function loadPromptContext(
480
598
  git,
481
599
  agentsMd,
482
600
  skills,
483
- plugins,
484
- theme: mergeThemes(DEFAULT_THEME, ...themeOverrides),
601
+ theme: DEFAULT_THEME,
602
+ };
603
+ }
604
+
605
+ /**
606
+ * Load global and repo-local settings for the current launch.
607
+ *
608
+ * The repo-local overlay is read only and is loaded only when a git root is
609
+ * known. Invalid startup content in either file is treated as empty settings.
610
+ *
611
+ * @param opts - Optional settings path and git-root override for tests.
612
+ * @returns Global settings, repo-local overlay settings, and the merged result.
613
+ */
614
+ export function loadUserSettingsForLaunch(opts?: {
615
+ settingsPath?: string;
616
+ gitRoot?: string | null;
617
+ }): {
618
+ settings: UserSettings;
619
+ repoSettings: UserSettings;
620
+ effectiveSettings: UserSettings;
621
+ } {
622
+ const settingsPath = opts?.settingsPath ?? SETTINGS_PATH;
623
+ const gitRoot = opts?.gitRoot ?? null;
624
+ const settings = loadStartupSettings(settingsPath);
625
+ const repoSettings = gitRoot
626
+ ? loadStartupSettings(join(gitRoot, ".mini-coder", "settings.json"))
627
+ : {};
628
+
629
+ return {
630
+ settings,
631
+ repoSettings,
632
+ effectiveSettings: mergeUserSettings(settings, repoSettings),
485
633
  };
486
634
  }
487
635
 
@@ -490,25 +638,17 @@ export async function reloadPromptContext(
490
638
  state: AppState,
491
639
  runtime?: {
492
640
  loadPromptContext?: typeof loadPromptContext;
493
- destroyPlugins?: typeof destroyPlugins;
494
641
  },
495
642
  ): Promise<void> {
496
643
  const loadContext = runtime?.loadPromptContext ?? loadPromptContext;
497
- const destroyLoadedPlugins = runtime?.destroyPlugins ?? destroyPlugins;
498
- const previousPlugins = state.plugins;
499
- const context = await loadContext(filterModelMessages(state.messages));
644
+ const context = await loadContext();
500
645
 
501
646
  state.cwd = context.cwd;
502
647
  state.canonicalCwd = context.canonicalCwd;
503
648
  state.git = context.git;
504
649
  state.agentsMd = context.agentsMd;
505
650
  state.skills = context.skills;
506
- state.plugins = context.plugins;
507
651
  state.theme = context.theme;
508
-
509
- await destroyLoadedPlugins(previousPlugins, (entry, err) => {
510
- console.error(`Plugin "${entry.name}" failed to destroy: ${err.message}`);
511
- });
512
652
  }
513
653
 
514
654
  // ---------------------------------------------------------------------------
@@ -535,20 +675,26 @@ export interface AppState {
535
675
  agentsMd: AgentsMdFile[];
536
676
  /** Discovered skills. */
537
677
  skills: Skill[];
538
- /** Loaded plugins. */
539
- plugins: LoadedPlugin[];
540
- /** Active theme (default + plugin overrides). */
678
+ /** Active theme. */
541
679
  theme: Theme;
542
680
  /** Version label shown in the empty conversation banner. */
543
681
  versionLabel: string;
544
682
  /** Current git state (null if not in a repo). */
545
683
  git: GitState | null;
684
+ /** Delegated-subagent depth inherited by this app process. */
685
+ delegationDepth: number;
686
+ /** Delegated-subagent budget reset at the start of each top-level agent run. */
687
+ delegationBudgetLimit: number;
688
+ /** Remaining delegated-subagent launches in the active run. */
689
+ delegationBudgetRemaining: number;
546
690
  /** Available provider credentials (provider → API key). */
547
691
  providers: Map<string, string>;
548
692
  /** OAuth credentials on disk. */
549
693
  oauthCredentials: Record<string, OAuthCredentials>;
550
694
  /** Loaded global user settings. */
551
695
  settings: UserSettings;
696
+ /** Loaded repo-local settings overlay for the current app run. */
697
+ repoSettings: UserSettings;
552
698
  /** Absolute path to the global settings file. */
553
699
  settingsPath: string;
554
700
  /** Working directory as entered by the user/shell (for display and tool execution). */
@@ -567,9 +713,11 @@ export interface AppState {
567
713
  showReasoning: boolean;
568
714
  /** Whether to show full (un-truncated) tool output. */
569
715
  verbose: boolean;
716
+ /** Configured MCP servers, including their current enabled/disabled state. */
717
+ mcpServers: McpServerState[];
570
718
  /** Models discovered from custom OpenAI-compatible providers. */
571
719
  customModels: Model<string>[];
572
- /** Warnings from startup (e.g. unreachable custom providers). */
720
+ /** Warnings from startup (e.g. unreachable custom providers or MCP servers). */
573
721
  startupWarnings: string[];
574
722
  }
575
723
 
@@ -587,13 +735,17 @@ export async function init(): Promise<AppState> {
587
735
  // Discover providers (env + OAuth)
588
736
  const { providers, oauthCredentials } = await discoverProviders();
589
737
 
590
- // Load user settings and resolve startup defaults
591
- const settings = loadSettings(SETTINGS_PATH);
738
+ const promptContext = await loadPromptContext({ cwd });
739
+ const { settings, repoSettings, effectiveSettings } =
740
+ loadUserSettingsForLaunch({
741
+ settingsPath: SETTINGS_PATH,
742
+ gitRoot: promptContext.git?.root ?? null,
743
+ });
592
744
 
593
- // Discover custom providers from settings
745
+ // Discover custom providers from effective settings
594
746
  const builtInProviderNames = new Set(providers.keys());
595
747
  const customResult = await discoverCustomProviders(
596
- settings.customProviders ?? [],
748
+ effectiveSettings.customProviders ?? [],
597
749
  builtInProviderNames,
598
750
  );
599
751
 
@@ -602,10 +754,13 @@ export async function init(): Promise<AppState> {
602
754
  providers.set(name, key);
603
755
  }
604
756
 
757
+ const mcpResult = await discoverMcpServers(effectiveSettings.mcp);
758
+ const delegation = readShellDelegationContext(process.env);
759
+
605
760
  const builtInModels = listAvailableModels(providers);
606
761
  const availableModels = [...builtInModels, ...customResult.models];
607
762
  const startup = resolveStartupSettings(
608
- settings,
763
+ effectiveSettings,
609
764
  availableModels.map((model) => `${model.provider}/${model.id}`),
610
765
  );
611
766
  const model = selectModel(availableModels, startup.modelId);
@@ -614,12 +769,6 @@ export async function init(): Promise<AppState> {
614
769
  const db = openDatabase(DB_PATH);
615
770
  const effort = startup.effort;
616
771
  const conversation = createConversationSnapshot();
617
- const promptContext = await loadPromptContext(
618
- filterModelMessages(conversation.messages),
619
- {
620
- cwd,
621
- },
622
- );
623
772
 
624
773
  return {
625
774
  db,
@@ -631,13 +780,16 @@ export async function init(): Promise<AppState> {
631
780
  contextTokens: conversation.contextTokens,
632
781
  agentsMd: promptContext.agentsMd,
633
782
  skills: promptContext.skills,
634
- plugins: promptContext.plugins,
635
783
  theme: promptContext.theme,
636
784
  versionLabel: resolveAppVersionLabel(),
637
785
  git: promptContext.git,
786
+ delegationDepth: delegation.depth,
787
+ delegationBudgetLimit: delegation.remainingBudget,
788
+ delegationBudgetRemaining: delegation.remainingBudget,
638
789
  providers,
639
790
  oauthCredentials,
640
791
  settings,
792
+ repoSettings,
641
793
  settingsPath: SETTINGS_PATH,
642
794
  cwd: promptContext.cwd,
643
795
  canonicalCwd: promptContext.canonicalCwd,
@@ -647,8 +799,9 @@ export async function init(): Promise<AppState> {
647
799
  queuedUserMessages: [],
648
800
  showReasoning: startup.showReasoning,
649
801
  verbose: startup.verbose,
802
+ mcpServers: mcpResult.servers,
650
803
  customModels: customResult.models,
651
- startupWarnings: customResult.warnings,
804
+ startupWarnings: [...customResult.warnings, ...mcpResult.warnings],
652
805
  };
653
806
  }
654
807
 
@@ -674,7 +827,9 @@ function resolvePromptOs(): "linux" | "mac" | "docker" {
674
827
  * Separated from `init` because turns still rebuild the assembled prompt
675
828
  * from the session-stable prompt context plus the current runtime state.
676
829
  */
677
- export function buildPrompt(state: AppState): string {
830
+ export function buildPrompt(
831
+ state: Pick<AppState, "cwd" | "model" | "git" | "agentsMd" | "skills">,
832
+ ): string {
678
833
  return buildSystemPrompt({
679
834
  cwd: state.cwd,
680
835
  modelLabel: state.model
@@ -686,9 +841,6 @@ export function buildPrompt(state: AppState): string {
686
841
  git: state.git,
687
842
  agentsMd: state.agentsMd,
688
843
  skills: state.skills,
689
- pluginSuffixes: state.plugins
690
- .map((p) => p.result.systemPromptSuffix)
691
- .filter((s): s is string => s != null),
692
844
  });
693
845
  }
694
846
 
@@ -697,8 +849,22 @@ export function buildToolList(state: AppState): {
697
849
  tools: Tool[];
698
850
  toolHandlers: Map<string, ToolHandler>;
699
851
  } {
700
- if (!state.model) return { tools: [], toolHandlers: new Map() };
701
- return buildTools(state.model, state.plugins, state.messages);
852
+ const { model } = state;
853
+ if (!model) return { tools: [], toolHandlers: new Map() };
854
+ return buildTools({
855
+ agentsMd: state.agentsMd,
856
+ cwd: state.cwd,
857
+ db: state.db,
858
+ delegationDepth: state.delegationDepth,
859
+ delegationBudgetRemaining: state.delegationBudgetRemaining,
860
+ effort: state.effort,
861
+ git: state.git,
862
+ mcpServers: state.mcpServers,
863
+ messages: state.messages,
864
+ model,
865
+ providers: state.providers,
866
+ skills: state.skills,
867
+ });
702
868
  }
703
869
 
704
870
  /**
@@ -747,9 +913,7 @@ export function getAvailableModels(state: AppState): Model<string>[] {
747
913
 
748
914
  /** Clean up resources on shutdown. */
749
915
  export async function shutdown(state: AppState): Promise<void> {
750
- await destroyPlugins(state.plugins, (entry, err) => {
751
- console.error(`Plugin "${entry.name}" failed to destroy: ${err.message}`);
752
- });
916
+ await Promise.allSettled(state.mcpServers.map((server) => server.close()));
753
917
  state.db.close();
754
918
  }
755
919
 
@@ -776,7 +940,8 @@ type HeadlessCliStopReason = "stop" | "length" | "error" | "aborted";
776
940
  *
777
941
  * Non-TTY detection only decides whether headless mode should run at all.
778
942
  * Once headless mode is selected, `--json` is the only switch that chooses
779
- * NDJSON streaming versus final-text output.
943
+ * NDJSON streaming versus the default text mode (stdout final answer plus
944
+ * stderr activity snippets).
780
945
  *
781
946
  * @param state - Initialized application state for the run.
782
947
  * @param cli - Parsed CLI options.
package/src/input.ts CHANGED
@@ -23,6 +23,7 @@ export const COMMANDS = [
23
23
  "undo",
24
24
  "reasoning",
25
25
  "verbose",
26
+ "mcp",
26
27
  "todo",
27
28
  "login",
28
29
  "logout",
@@ -31,8 +32,11 @@ export const COMMANDS = [
31
32
  "effort",
32
33
  ] as const;
33
34
 
35
+ /** Slash helper that opens the interactive skill picker when submitted alone. */
36
+ export const SKILL_COMMAND = "skill" as const;
37
+
34
38
  /** A recognized slash command name. */
35
- type Command = (typeof COMMANDS)[number];
39
+ type Command = (typeof COMMANDS)[number] | typeof SKILL_COMMAND;
36
40
 
37
41
  const COMMAND_SET: ReadonlySet<string> = new Set(COMMANDS);
38
42
 
@@ -73,6 +77,14 @@ function isCommand(value: string): value is Command {
73
77
  }
74
78
 
75
79
  function parseSlashInput(trimmed: string): ParsedInput | null {
80
+ if (trimmed === `/${SKILL_COMMAND}`) {
81
+ return {
82
+ type: "command",
83
+ command: SKILL_COMMAND,
84
+ args: "",
85
+ };
86
+ }
87
+
76
88
  const skillMatch = trimmed.match(/^\/skill:(\S+)(?:\s+(.*))?$/s);
77
89
  if (skillMatch?.[1]) {
78
90
  return {