mini-coder 0.5.12 → 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/BENCHMARK.md +15 -316
- package/PROGRESS.md +1 -2
- package/README.md +53 -21
- package/benchmark-baseline.sh +15 -0
- package/bun.lock +265 -90
- package/package.json +8 -7
- package/skills-lock.json +15 -0
- package/src/agent.ts +20 -0
- package/src/cli.ts +2 -1
- package/src/headless.ts +97 -24
- package/src/index.ts +78 -93
- package/src/input.ts +13 -1
- package/src/mcp.ts +609 -0
- package/src/prompt.ts +2 -12
- package/src/settings.ts +199 -7
- package/src/skills.ts +12 -3
- package/src/submit.ts +17 -0
- package/src/theme.ts +186 -3
- package/src/tool-common.ts +2 -0
- package/src/tool-shell.ts +138 -6
- package/src/tools.ts +4 -0
- package/src/ui/agent.ts +7 -0
- package/src/ui/commands.test.ts +475 -4
- package/src/ui/commands.ts +210 -8
- package/src/ui/conversation.test.ts +252 -27
- package/src/ui/conversation.ts +468 -390
- package/src/ui/help.ts +27 -11
- package/src/ui.ts +230 -75
- package/src/plugins.ts +0 -183
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
|
|
5
|
-
*
|
|
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,
|
|
@@ -52,7 +44,6 @@ import {
|
|
|
52
44
|
appendMessage,
|
|
53
45
|
createConversationSnapshot,
|
|
54
46
|
createSession,
|
|
55
|
-
filterModelMessages,
|
|
56
47
|
type loadMessages,
|
|
57
48
|
openDatabase,
|
|
58
49
|
type Session,
|
|
@@ -61,12 +52,13 @@ import {
|
|
|
61
52
|
} from "./session.ts";
|
|
62
53
|
import {
|
|
63
54
|
type CustomProvider,
|
|
64
|
-
|
|
55
|
+
loadStartupSettings,
|
|
56
|
+
mergeUserSettings,
|
|
65
57
|
resolveStartupSettings,
|
|
66
58
|
type UserSettings,
|
|
67
59
|
} from "./settings.ts";
|
|
68
60
|
import { discoverSkills, type Skill } from "./skills.ts";
|
|
69
|
-
import { DEFAULT_THEME,
|
|
61
|
+
import { DEFAULT_THEME, type Theme } from "./theme.ts";
|
|
70
62
|
import {
|
|
71
63
|
createTodoReadToolHandler,
|
|
72
64
|
createTodoWriteToolHandler,
|
|
@@ -95,9 +87,6 @@ const DATA_DIR = join(homedir(), ".config", "mini-coder");
|
|
|
95
87
|
/** SQLite database path. */
|
|
96
88
|
const DB_PATH = join(DATA_DIR, "mini-coder.db");
|
|
97
89
|
|
|
98
|
-
/** Plugin config file path. */
|
|
99
|
-
const PLUGIN_CONFIG_PATH = join(DATA_DIR, "plugins.json");
|
|
100
|
-
|
|
101
90
|
/** OAuth credentials file path. */
|
|
102
91
|
const AUTH_PATH = join(DATA_DIR, "auth.json");
|
|
103
92
|
|
|
@@ -370,8 +359,8 @@ function selectModel(
|
|
|
370
359
|
*/
|
|
371
360
|
function buildTools(
|
|
372
361
|
model: Model<string>,
|
|
373
|
-
plugins: LoadedPlugin[],
|
|
374
362
|
messages: AppState["messages"],
|
|
363
|
+
mcpServers: readonly McpServerState[],
|
|
375
364
|
): { tools: Tool[]; toolHandlers: Map<string, ToolHandler> } {
|
|
376
365
|
const tools: Tool[] = [
|
|
377
366
|
shellTool,
|
|
@@ -390,26 +379,23 @@ function buildTools(
|
|
|
390
379
|
[todoReadTool.name, createTodoReadToolHandler(messages)],
|
|
391
380
|
]);
|
|
392
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
|
+
|
|
393
393
|
// Conditionally register readImage for vision-capable models
|
|
394
394
|
if (model.input.includes("image")) {
|
|
395
395
|
tools.push(readImageTool);
|
|
396
396
|
toolHandlers.set(readImageTool.name, readImageToolHandler);
|
|
397
397
|
}
|
|
398
398
|
|
|
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
399
|
return { tools, toolHandlers };
|
|
414
400
|
}
|
|
415
401
|
|
|
@@ -429,21 +415,13 @@ function getSkillScanPaths(cwd: string, gitRoot: string | null): string[] {
|
|
|
429
415
|
];
|
|
430
416
|
}
|
|
431
417
|
|
|
432
|
-
/** Load AGENTS.md files, skills,
|
|
433
|
-
export async function loadPromptContext(
|
|
434
|
-
messages: readonly Message[],
|
|
435
|
-
opts?: {
|
|
436
|
-
cwd?: string;
|
|
437
|
-
pluginEntries?: PluginEntry[];
|
|
438
|
-
pluginConfigPath?: string;
|
|
439
|
-
},
|
|
440
|
-
): Promise<{
|
|
418
|
+
/** Load AGENTS.md files, skills, git state, and the active theme. */
|
|
419
|
+
export async function loadPromptContext(opts?: { cwd?: string }): Promise<{
|
|
441
420
|
cwd: string;
|
|
442
421
|
canonicalCwd: string;
|
|
443
422
|
git: GitState | null;
|
|
444
423
|
agentsMd: AgentsMdFile[];
|
|
445
424
|
skills: Skill[];
|
|
446
|
-
plugins: LoadedPlugin[];
|
|
447
425
|
theme: Theme;
|
|
448
426
|
}> {
|
|
449
427
|
const cwd = opts?.cwd ?? process.cwd();
|
|
@@ -459,20 +437,6 @@ export async function loadPromptContext(
|
|
|
459
437
|
);
|
|
460
438
|
const agentsMd = discoverAgentsMd(cwd, scanRoot, join(home, ".agents"));
|
|
461
439
|
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
440
|
|
|
477
441
|
return {
|
|
478
442
|
cwd,
|
|
@@ -480,8 +444,38 @@ export async function loadPromptContext(
|
|
|
480
444
|
git,
|
|
481
445
|
agentsMd,
|
|
482
446
|
skills,
|
|
483
|
-
|
|
484
|
-
|
|
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),
|
|
485
479
|
};
|
|
486
480
|
}
|
|
487
481
|
|
|
@@ -490,25 +484,17 @@ export async function reloadPromptContext(
|
|
|
490
484
|
state: AppState,
|
|
491
485
|
runtime?: {
|
|
492
486
|
loadPromptContext?: typeof loadPromptContext;
|
|
493
|
-
destroyPlugins?: typeof destroyPlugins;
|
|
494
487
|
},
|
|
495
488
|
): Promise<void> {
|
|
496
489
|
const loadContext = runtime?.loadPromptContext ?? loadPromptContext;
|
|
497
|
-
const
|
|
498
|
-
const previousPlugins = state.plugins;
|
|
499
|
-
const context = await loadContext(filterModelMessages(state.messages));
|
|
490
|
+
const context = await loadContext();
|
|
500
491
|
|
|
501
492
|
state.cwd = context.cwd;
|
|
502
493
|
state.canonicalCwd = context.canonicalCwd;
|
|
503
494
|
state.git = context.git;
|
|
504
495
|
state.agentsMd = context.agentsMd;
|
|
505
496
|
state.skills = context.skills;
|
|
506
|
-
state.plugins = context.plugins;
|
|
507
497
|
state.theme = context.theme;
|
|
508
|
-
|
|
509
|
-
await destroyLoadedPlugins(previousPlugins, (entry, err) => {
|
|
510
|
-
console.error(`Plugin "${entry.name}" failed to destroy: ${err.message}`);
|
|
511
|
-
});
|
|
512
498
|
}
|
|
513
499
|
|
|
514
500
|
// ---------------------------------------------------------------------------
|
|
@@ -535,9 +521,7 @@ export interface AppState {
|
|
|
535
521
|
agentsMd: AgentsMdFile[];
|
|
536
522
|
/** Discovered skills. */
|
|
537
523
|
skills: Skill[];
|
|
538
|
-
/**
|
|
539
|
-
plugins: LoadedPlugin[];
|
|
540
|
-
/** Active theme (default + plugin overrides). */
|
|
524
|
+
/** Active theme. */
|
|
541
525
|
theme: Theme;
|
|
542
526
|
/** Version label shown in the empty conversation banner. */
|
|
543
527
|
versionLabel: string;
|
|
@@ -549,6 +533,8 @@ export interface AppState {
|
|
|
549
533
|
oauthCredentials: Record<string, OAuthCredentials>;
|
|
550
534
|
/** Loaded global user settings. */
|
|
551
535
|
settings: UserSettings;
|
|
536
|
+
/** Loaded repo-local settings overlay for the current app run. */
|
|
537
|
+
repoSettings: UserSettings;
|
|
552
538
|
/** Absolute path to the global settings file. */
|
|
553
539
|
settingsPath: string;
|
|
554
540
|
/** Working directory as entered by the user/shell (for display and tool execution). */
|
|
@@ -567,9 +553,11 @@ export interface AppState {
|
|
|
567
553
|
showReasoning: boolean;
|
|
568
554
|
/** Whether to show full (un-truncated) tool output. */
|
|
569
555
|
verbose: boolean;
|
|
556
|
+
/** Configured MCP servers, including their current enabled/disabled state. */
|
|
557
|
+
mcpServers: McpServerState[];
|
|
570
558
|
/** Models discovered from custom OpenAI-compatible providers. */
|
|
571
559
|
customModels: Model<string>[];
|
|
572
|
-
/** Warnings from startup (e.g. unreachable custom providers). */
|
|
560
|
+
/** Warnings from startup (e.g. unreachable custom providers or MCP servers). */
|
|
573
561
|
startupWarnings: string[];
|
|
574
562
|
}
|
|
575
563
|
|
|
@@ -587,13 +575,17 @@ export async function init(): Promise<AppState> {
|
|
|
587
575
|
// Discover providers (env + OAuth)
|
|
588
576
|
const { providers, oauthCredentials } = await discoverProviders();
|
|
589
577
|
|
|
590
|
-
|
|
591
|
-
const settings =
|
|
578
|
+
const promptContext = await loadPromptContext({ cwd });
|
|
579
|
+
const { settings, repoSettings, effectiveSettings } =
|
|
580
|
+
loadUserSettingsForLaunch({
|
|
581
|
+
settingsPath: SETTINGS_PATH,
|
|
582
|
+
gitRoot: promptContext.git?.root ?? null,
|
|
583
|
+
});
|
|
592
584
|
|
|
593
|
-
// Discover custom providers from settings
|
|
585
|
+
// Discover custom providers from effective settings
|
|
594
586
|
const builtInProviderNames = new Set(providers.keys());
|
|
595
587
|
const customResult = await discoverCustomProviders(
|
|
596
|
-
|
|
588
|
+
effectiveSettings.customProviders ?? [],
|
|
597
589
|
builtInProviderNames,
|
|
598
590
|
);
|
|
599
591
|
|
|
@@ -602,10 +594,12 @@ export async function init(): Promise<AppState> {
|
|
|
602
594
|
providers.set(name, key);
|
|
603
595
|
}
|
|
604
596
|
|
|
597
|
+
const mcpResult = await discoverMcpServers(effectiveSettings.mcp);
|
|
598
|
+
|
|
605
599
|
const builtInModels = listAvailableModels(providers);
|
|
606
600
|
const availableModels = [...builtInModels, ...customResult.models];
|
|
607
601
|
const startup = resolveStartupSettings(
|
|
608
|
-
|
|
602
|
+
effectiveSettings,
|
|
609
603
|
availableModels.map((model) => `${model.provider}/${model.id}`),
|
|
610
604
|
);
|
|
611
605
|
const model = selectModel(availableModels, startup.modelId);
|
|
@@ -614,12 +608,6 @@ export async function init(): Promise<AppState> {
|
|
|
614
608
|
const db = openDatabase(DB_PATH);
|
|
615
609
|
const effort = startup.effort;
|
|
616
610
|
const conversation = createConversationSnapshot();
|
|
617
|
-
const promptContext = await loadPromptContext(
|
|
618
|
-
filterModelMessages(conversation.messages),
|
|
619
|
-
{
|
|
620
|
-
cwd,
|
|
621
|
-
},
|
|
622
|
-
);
|
|
623
611
|
|
|
624
612
|
return {
|
|
625
613
|
db,
|
|
@@ -631,13 +619,13 @@ export async function init(): Promise<AppState> {
|
|
|
631
619
|
contextTokens: conversation.contextTokens,
|
|
632
620
|
agentsMd: promptContext.agentsMd,
|
|
633
621
|
skills: promptContext.skills,
|
|
634
|
-
plugins: promptContext.plugins,
|
|
635
622
|
theme: promptContext.theme,
|
|
636
623
|
versionLabel: resolveAppVersionLabel(),
|
|
637
624
|
git: promptContext.git,
|
|
638
625
|
providers,
|
|
639
626
|
oauthCredentials,
|
|
640
627
|
settings,
|
|
628
|
+
repoSettings,
|
|
641
629
|
settingsPath: SETTINGS_PATH,
|
|
642
630
|
cwd: promptContext.cwd,
|
|
643
631
|
canonicalCwd: promptContext.canonicalCwd,
|
|
@@ -647,8 +635,9 @@ export async function init(): Promise<AppState> {
|
|
|
647
635
|
queuedUserMessages: [],
|
|
648
636
|
showReasoning: startup.showReasoning,
|
|
649
637
|
verbose: startup.verbose,
|
|
638
|
+
mcpServers: mcpResult.servers,
|
|
650
639
|
customModels: customResult.models,
|
|
651
|
-
startupWarnings: customResult.warnings,
|
|
640
|
+
startupWarnings: [...customResult.warnings, ...mcpResult.warnings],
|
|
652
641
|
};
|
|
653
642
|
}
|
|
654
643
|
|
|
@@ -686,9 +675,6 @@ export function buildPrompt(state: AppState): string {
|
|
|
686
675
|
git: state.git,
|
|
687
676
|
agentsMd: state.agentsMd,
|
|
688
677
|
skills: state.skills,
|
|
689
|
-
pluginSuffixes: state.plugins
|
|
690
|
-
.map((p) => p.result.systemPromptSuffix)
|
|
691
|
-
.filter((s): s is string => s != null),
|
|
692
678
|
});
|
|
693
679
|
}
|
|
694
680
|
|
|
@@ -698,7 +684,7 @@ export function buildToolList(state: AppState): {
|
|
|
698
684
|
toolHandlers: Map<string, ToolHandler>;
|
|
699
685
|
} {
|
|
700
686
|
if (!state.model) return { tools: [], toolHandlers: new Map() };
|
|
701
|
-
return buildTools(state.model, state.
|
|
687
|
+
return buildTools(state.model, state.messages, state.mcpServers);
|
|
702
688
|
}
|
|
703
689
|
|
|
704
690
|
/**
|
|
@@ -747,9 +733,7 @@ export function getAvailableModels(state: AppState): Model<string>[] {
|
|
|
747
733
|
|
|
748
734
|
/** Clean up resources on shutdown. */
|
|
749
735
|
export async function shutdown(state: AppState): Promise<void> {
|
|
750
|
-
await
|
|
751
|
-
console.error(`Plugin "${entry.name}" failed to destroy: ${err.message}`);
|
|
752
|
-
});
|
|
736
|
+
await Promise.allSettled(state.mcpServers.map((server) => server.close()));
|
|
753
737
|
state.db.close();
|
|
754
738
|
}
|
|
755
739
|
|
|
@@ -776,7 +760,8 @@ type HeadlessCliStopReason = "stop" | "length" | "error" | "aborted";
|
|
|
776
760
|
*
|
|
777
761
|
* Non-TTY detection only decides whether headless mode should run at all.
|
|
778
762
|
* Once headless mode is selected, `--json` is the only switch that chooses
|
|
779
|
-
* NDJSON streaming versus
|
|
763
|
+
* NDJSON streaming versus the default text mode (stdout final answer plus
|
|
764
|
+
* stderr activity snippets).
|
|
780
765
|
*
|
|
781
766
|
* @param state - Initialized application state for the run.
|
|
782
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 {
|