mini-coder 0.5.11 → 0.5.13

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
  */
@@ -14,7 +14,6 @@ import { basename, dirname, join } from "node:path";
14
14
  import { isDeepStrictEqual } from "node:util";
15
15
  import type {
16
16
  KnownProvider,
17
- Message,
18
17
  Model,
19
18
  OAuthCredentials,
20
19
  ThinkingLevel,
@@ -33,15 +32,8 @@ import {
33
32
  } from "./cli.ts";
34
33
  import { getErrorMessage } from "./errors.ts";
35
34
  import { type GitState, getGitState } from "./git.ts";
35
+ import { discoverMcpServers, type McpServerState } from "./mcp.ts";
36
36
  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
37
  import {
46
38
  type AgentsMdFile,
47
39
  buildSystemPrompt,
@@ -50,10 +42,8 @@ import {
50
42
  } from "./prompt.ts";
51
43
  import {
52
44
  appendMessage,
53
- computeContextTokens,
54
- computeStats,
45
+ createConversationSnapshot,
55
46
  createSession,
56
- filterModelMessages,
57
47
  type loadMessages,
58
48
  openDatabase,
59
49
  type Session,
@@ -62,21 +52,26 @@ import {
62
52
  } from "./session.ts";
63
53
  import {
64
54
  type CustomProvider,
65
- loadSettings,
55
+ loadStartupSettings,
56
+ mergeUserSettings,
66
57
  resolveStartupSettings,
67
58
  type UserSettings,
68
59
  } from "./settings.ts";
69
60
  import { discoverSkills, type Skill } from "./skills.ts";
70
- import { DEFAULT_THEME, mergeThemes, type Theme } from "./theme.ts";
61
+ import { DEFAULT_THEME, type Theme } from "./theme.ts";
71
62
  import {
63
+ createTodoReadToolHandler,
64
+ createTodoWriteToolHandler,
72
65
  editTool,
73
- executeEdit,
74
- executeReadImage,
75
- executeShell,
76
- executeTodoRead,
77
- executeTodoWrite,
66
+ editToolHandler,
67
+ grepTool,
68
+ grepToolHandler,
78
69
  readImageTool,
70
+ readImageToolHandler,
71
+ readTool,
72
+ readToolHandler,
79
73
  shellTool,
74
+ shellToolHandler,
80
75
  todoReadTool,
81
76
  todoWriteTool,
82
77
  } from "./tools.ts";
@@ -92,9 +87,6 @@ const DATA_DIR = join(homedir(), ".config", "mini-coder");
92
87
  /** SQLite database path. */
93
88
  const DB_PATH = join(DATA_DIR, "mini-coder.db");
94
89
 
95
- /** Plugin config file path. */
96
- const PLUGIN_CONFIG_PATH = join(DATA_DIR, "plugins.json");
97
-
98
90
  /** OAuth credentials file path. */
99
91
  const AUTH_PATH = join(DATA_DIR, "auth.json");
100
92
 
@@ -359,26 +351,6 @@ function selectModel(
359
351
  // Tool wiring
360
352
  // ---------------------------------------------------------------------------
361
353
 
362
- /** Built-in tool handlers keyed by tool name. */
363
- const BUILTIN_HANDLERS: Record<string, ToolHandler> = {
364
- edit: (args, cwd) =>
365
- executeEdit(
366
- {
367
- path: args.path as string,
368
- oldText: args.oldText as string,
369
- newText: args.newText as string,
370
- },
371
- cwd,
372
- ),
373
- shell: (args, cwd, signal, onUpdate) =>
374
- executeShell({ command: args.command as string }, cwd, {
375
- ...(signal ? { signal } : {}),
376
- ...(onUpdate ? { onUpdate } : {}),
377
- }),
378
- readImage: (args, cwd) =>
379
- executeReadImage({ path: args.path as string }, cwd),
380
- };
381
-
382
354
  /**
383
355
  * Build tool definitions and handler map for the current model.
384
356
  *
@@ -387,49 +359,41 @@ const BUILTIN_HANDLERS: Record<string, ToolHandler> = {
387
359
  */
388
360
  function buildTools(
389
361
  model: Model<string>,
390
- plugins: LoadedPlugin[],
391
362
  messages: AppState["messages"],
363
+ mcpServers: readonly McpServerState[],
392
364
  ): { tools: Tool[]; toolHandlers: Map<string, ToolHandler> } {
393
- const tools: Tool[] = [editTool, shellTool, todoWriteTool, todoReadTool];
365
+ const tools: Tool[] = [
366
+ shellTool,
367
+ readTool,
368
+ grepTool,
369
+ editTool,
370
+ todoWriteTool,
371
+ todoReadTool,
372
+ ];
394
373
  const toolHandlers = new Map<string, ToolHandler>([
395
- [editTool.name, BUILTIN_HANDLERS.edit!],
396
- [shellTool.name, BUILTIN_HANDLERS.shell!],
397
- [
398
- todoWriteTool.name,
399
- (args) =>
400
- executeTodoWrite(
401
- {
402
- todos: Array.isArray(args.todos)
403
- ? (args.todos as Array<{
404
- content: string;
405
- status: "pending" | "in_progress" | "completed" | "cancelled";
406
- }>)
407
- : [],
408
- },
409
- messages,
410
- ),
411
- ],
412
- [todoReadTool.name, () => executeTodoRead(messages)],
374
+ [shellTool.name, shellToolHandler],
375
+ [readTool.name, readToolHandler],
376
+ [grepTool.name, grepToolHandler],
377
+ [editTool.name, editToolHandler],
378
+ [todoWriteTool.name, createTodoWriteToolHandler(messages)],
379
+ [todoReadTool.name, createTodoReadToolHandler(messages)],
413
380
  ]);
414
381
 
382
+ for (const server of mcpServers) {
383
+ if (!server.enabled || !server.connected) {
384
+ continue;
385
+ }
386
+
387
+ tools.push(...server.tools);
388
+ for (const [name, handler] of server.toolHandlers) {
389
+ toolHandlers.set(name, handler);
390
+ }
391
+ }
392
+
415
393
  // Conditionally register readImage for vision-capable models
416
394
  if (model.input.includes("image")) {
417
395
  tools.push(readImageTool);
418
- toolHandlers.set(readImageTool.name, BUILTIN_HANDLERS.readImage!);
419
- }
420
-
421
- // Add plugin tools
422
- for (const plugin of plugins) {
423
- if (plugin.result.tools) {
424
- for (const tool of plugin.result.tools) {
425
- tools.push(tool);
426
- }
427
- }
428
- if (plugin.result.toolHandlers) {
429
- for (const [name, handler] of plugin.result.toolHandlers) {
430
- toolHandlers.set(name, handler);
431
- }
432
- }
396
+ toolHandlers.set(readImageTool.name, readImageToolHandler);
433
397
  }
434
398
 
435
399
  return { tools, toolHandlers };
@@ -451,21 +415,13 @@ function getSkillScanPaths(cwd: string, gitRoot: string | null): string[] {
451
415
  ];
452
416
  }
453
417
 
454
- /** Load AGENTS.md files, skills, plugins, git state, and the merged theme. */
455
- export async function loadPromptContext(
456
- messages: readonly Message[],
457
- opts?: {
458
- cwd?: string;
459
- pluginEntries?: PluginEntry[];
460
- pluginConfigPath?: string;
461
- },
462
- ): Promise<{
418
+ /** Load AGENTS.md files, skills, git state, and the active theme. */
419
+ export async function loadPromptContext(opts?: { cwd?: string }): Promise<{
463
420
  cwd: string;
464
421
  canonicalCwd: string;
465
422
  git: GitState | null;
466
423
  agentsMd: AgentsMdFile[];
467
424
  skills: Skill[];
468
- plugins: LoadedPlugin[];
469
425
  theme: Theme;
470
426
  }> {
471
427
  const cwd = opts?.cwd ?? process.cwd();
@@ -481,20 +437,6 @@ export async function loadPromptContext(
481
437
  );
482
438
  const agentsMd = discoverAgentsMd(cwd, scanRoot, join(home, ".agents"));
483
439
  const skills = discoverSkills(getSkillScanPaths(canonicalCwd, gitRoot));
484
- const pluginEntries =
485
- opts?.pluginEntries ??
486
- loadPluginConfig(opts?.pluginConfigPath ?? PLUGIN_CONFIG_PATH);
487
- const context: AgentContext = {
488
- cwd,
489
- messages,
490
- dataDir: DATA_DIR,
491
- };
492
- const plugins = await initPlugins(pluginEntries, context, (entry, err) => {
493
- console.error(`Plugin "${entry.name}" failed to init: ${err.message}`);
494
- });
495
- const themeOverrides = plugins
496
- .map((plugin) => plugin.result.theme)
497
- .filter((theme): theme is Partial<Theme> => theme != null);
498
440
 
499
441
  return {
500
442
  cwd,
@@ -502,8 +444,38 @@ export async function loadPromptContext(
502
444
  git,
503
445
  agentsMd,
504
446
  skills,
505
- plugins,
506
- theme: mergeThemes(DEFAULT_THEME, ...themeOverrides),
447
+ theme: DEFAULT_THEME,
448
+ };
449
+ }
450
+
451
+ /**
452
+ * Load global and repo-local settings for the current launch.
453
+ *
454
+ * The repo-local overlay is read only and is loaded only when a git root is
455
+ * known. Invalid startup content in either file is treated as empty settings.
456
+ *
457
+ * @param opts - Optional settings path and git-root override for tests.
458
+ * @returns Global settings, repo-local overlay settings, and the merged result.
459
+ */
460
+ export function loadUserSettingsForLaunch(opts?: {
461
+ settingsPath?: string;
462
+ gitRoot?: string | null;
463
+ }): {
464
+ settings: UserSettings;
465
+ repoSettings: UserSettings;
466
+ effectiveSettings: UserSettings;
467
+ } {
468
+ const settingsPath = opts?.settingsPath ?? SETTINGS_PATH;
469
+ const gitRoot = opts?.gitRoot ?? null;
470
+ const settings = loadStartupSettings(settingsPath);
471
+ const repoSettings = gitRoot
472
+ ? loadStartupSettings(join(gitRoot, ".mini-coder", "settings.json"))
473
+ : {};
474
+
475
+ return {
476
+ settings,
477
+ repoSettings,
478
+ effectiveSettings: mergeUserSettings(settings, repoSettings),
507
479
  };
508
480
  }
509
481
 
@@ -512,25 +484,17 @@ export async function reloadPromptContext(
512
484
  state: AppState,
513
485
  runtime?: {
514
486
  loadPromptContext?: typeof loadPromptContext;
515
- destroyPlugins?: typeof destroyPlugins;
516
487
  },
517
488
  ): Promise<void> {
518
489
  const loadContext = runtime?.loadPromptContext ?? loadPromptContext;
519
- const destroyLoadedPlugins = runtime?.destroyPlugins ?? destroyPlugins;
520
- const previousPlugins = state.plugins;
521
- const context = await loadContext(filterModelMessages(state.messages));
490
+ const context = await loadContext();
522
491
 
523
492
  state.cwd = context.cwd;
524
493
  state.canonicalCwd = context.canonicalCwd;
525
494
  state.git = context.git;
526
495
  state.agentsMd = context.agentsMd;
527
496
  state.skills = context.skills;
528
- state.plugins = context.plugins;
529
497
  state.theme = context.theme;
530
-
531
- await destroyLoadedPlugins(previousPlugins, (entry, err) => {
532
- console.error(`Plugin "${entry.name}" failed to destroy: ${err.message}`);
533
- });
534
498
  }
535
499
 
536
500
  // ---------------------------------------------------------------------------
@@ -557,9 +521,7 @@ export interface AppState {
557
521
  agentsMd: AgentsMdFile[];
558
522
  /** Discovered skills. */
559
523
  skills: Skill[];
560
- /** Loaded plugins. */
561
- plugins: LoadedPlugin[];
562
- /** Active theme (default + plugin overrides). */
524
+ /** Active theme. */
563
525
  theme: Theme;
564
526
  /** Version label shown in the empty conversation banner. */
565
527
  versionLabel: string;
@@ -571,6 +533,8 @@ export interface AppState {
571
533
  oauthCredentials: Record<string, OAuthCredentials>;
572
534
  /** Loaded global user settings. */
573
535
  settings: UserSettings;
536
+ /** Loaded repo-local settings overlay for the current app run. */
537
+ repoSettings: UserSettings;
574
538
  /** Absolute path to the global settings file. */
575
539
  settingsPath: string;
576
540
  /** Working directory as entered by the user/shell (for display and tool execution). */
@@ -589,9 +553,11 @@ export interface AppState {
589
553
  showReasoning: boolean;
590
554
  /** Whether to show full (un-truncated) tool output. */
591
555
  verbose: boolean;
556
+ /** Configured MCP servers, including their current enabled/disabled state. */
557
+ mcpServers: McpServerState[];
592
558
  /** Models discovered from custom OpenAI-compatible providers. */
593
559
  customModels: Model<string>[];
594
- /** Warnings from startup (e.g. unreachable custom providers). */
560
+ /** Warnings from startup (e.g. unreachable custom providers or MCP servers). */
595
561
  startupWarnings: string[];
596
562
  }
597
563
 
@@ -609,13 +575,17 @@ export async function init(): Promise<AppState> {
609
575
  // Discover providers (env + OAuth)
610
576
  const { providers, oauthCredentials } = await discoverProviders();
611
577
 
612
- // Load user settings and resolve startup defaults
613
- const settings = loadSettings(SETTINGS_PATH);
578
+ const promptContext = await loadPromptContext({ cwd });
579
+ const { settings, repoSettings, effectiveSettings } =
580
+ loadUserSettingsForLaunch({
581
+ settingsPath: SETTINGS_PATH,
582
+ gitRoot: promptContext.git?.root ?? null,
583
+ });
614
584
 
615
- // Discover custom providers from settings
585
+ // Discover custom providers from effective settings
616
586
  const builtInProviderNames = new Set(providers.keys());
617
587
  const customResult = await discoverCustomProviders(
618
- settings.customProviders ?? [],
588
+ effectiveSettings.customProviders ?? [],
619
589
  builtInProviderNames,
620
590
  );
621
591
 
@@ -624,10 +594,12 @@ export async function init(): Promise<AppState> {
624
594
  providers.set(name, key);
625
595
  }
626
596
 
597
+ const mcpResult = await discoverMcpServers(effectiveSettings.mcp);
598
+
627
599
  const builtInModels = listAvailableModels(providers);
628
600
  const availableModels = [...builtInModels, ...customResult.models];
629
601
  const startup = resolveStartupSettings(
630
- settings,
602
+ effectiveSettings,
631
603
  availableModels.map((model) => `${model.provider}/${model.id}`),
632
604
  );
633
605
  const model = selectModel(availableModels, startup.modelId);
@@ -635,30 +607,25 @@ export async function init(): Promise<AppState> {
635
607
  // Open database. Sessions are created lazily on the first user message.
636
608
  const db = openDatabase(DB_PATH);
637
609
  const effort = startup.effort;
638
- const messages: ReturnType<typeof loadMessages> = [];
639
- const stats = computeStats(messages);
640
- const contextTokens = computeContextTokens(messages);
641
- const promptContext = await loadPromptContext(filterModelMessages(messages), {
642
- cwd,
643
- });
610
+ const conversation = createConversationSnapshot();
644
611
 
645
612
  return {
646
613
  db,
647
614
  session: null,
648
615
  model,
649
616
  effort,
650
- messages,
651
- stats,
652
- contextTokens,
617
+ messages: conversation.messages,
618
+ stats: conversation.stats,
619
+ contextTokens: conversation.contextTokens,
653
620
  agentsMd: promptContext.agentsMd,
654
621
  skills: promptContext.skills,
655
- plugins: promptContext.plugins,
656
622
  theme: promptContext.theme,
657
623
  versionLabel: resolveAppVersionLabel(),
658
624
  git: promptContext.git,
659
625
  providers,
660
626
  oauthCredentials,
661
627
  settings,
628
+ repoSettings,
662
629
  settingsPath: SETTINGS_PATH,
663
630
  cwd: promptContext.cwd,
664
631
  canonicalCwd: promptContext.canonicalCwd,
@@ -668,8 +635,9 @@ export async function init(): Promise<AppState> {
668
635
  queuedUserMessages: [],
669
636
  showReasoning: startup.showReasoning,
670
637
  verbose: startup.verbose,
638
+ mcpServers: mcpResult.servers,
671
639
  customModels: customResult.models,
672
- startupWarnings: customResult.warnings,
640
+ startupWarnings: [...customResult.warnings, ...mcpResult.warnings],
673
641
  };
674
642
  }
675
643
 
@@ -707,9 +675,6 @@ export function buildPrompt(state: AppState): string {
707
675
  git: state.git,
708
676
  agentsMd: state.agentsMd,
709
677
  skills: state.skills,
710
- pluginSuffixes: state.plugins
711
- .map((p) => p.result.systemPromptSuffix)
712
- .filter((s): s is string => s != null),
713
678
  });
714
679
  }
715
680
 
@@ -719,7 +684,7 @@ export function buildToolList(state: AppState): {
719
684
  toolHandlers: Map<string, ToolHandler>;
720
685
  } {
721
686
  if (!state.model) return { tools: [], toolHandlers: new Map() };
722
- return buildTools(state.model, state.plugins, state.messages);
687
+ return buildTools(state.model, state.messages, state.mcpServers);
723
688
  }
724
689
 
725
690
  /**
@@ -768,9 +733,7 @@ export function getAvailableModels(state: AppState): Model<string>[] {
768
733
 
769
734
  /** Clean up resources on shutdown. */
770
735
  export async function shutdown(state: AppState): Promise<void> {
771
- await destroyPlugins(state.plugins, (entry, err) => {
772
- console.error(`Plugin "${entry.name}" failed to destroy: ${err.message}`);
773
- });
736
+ await Promise.allSettled(state.mcpServers.map((server) => server.close()));
774
737
  state.db.close();
775
738
  }
776
739
 
@@ -797,7 +760,8 @@ type HeadlessCliStopReason = "stop" | "length" | "error" | "aborted";
797
760
  *
798
761
  * Non-TTY detection only decides whether headless mode should run at all.
799
762
  * Once headless mode is selected, `--json` is the only switch that chooses
800
- * NDJSON streaming versus final-text output.
763
+ * NDJSON streaming versus the default text mode (stdout final answer plus
764
+ * stderr activity snippets).
801
765
  *
802
766
  * @param state - Initialized application state for the run.
803
767
  * @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 {