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/BENCHMARK.md +15 -316
- package/PROGRESS.md +3 -3
- package/README.md +54 -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 +526 -13
- package/src/assistant-output.ts +73 -0
- package/src/cli.ts +2 -1
- package/src/delegation.ts +238 -0
- package/src/headless.ts +90 -44
- package/src/index.ts +267 -102
- package/src/input.ts +13 -1
- package/src/mcp.ts +609 -0
- package/src/prompt.ts +11 -13
- package/src/session-message.ts +57 -65
- package/src/session.ts +389 -42
- package/src/settings.ts +199 -7
- package/src/skills.ts +12 -3
- package/src/submit.ts +24 -2
- package/src/theme.ts +186 -3
- package/src/tool-common.ts +2 -0
- package/src/tool-delegate.ts +125 -0
- package/src/tool-shell.ts +190 -8
- package/src/tools.ts +335 -6
- package/src/ui/agent.ts +10 -0
- package/src/ui/commands.test.ts +525 -10
- package/src/ui/commands.ts +224 -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
|
*/
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
[
|
|
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,
|
|
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
|
-
|
|
484
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
591
|
-
const settings =
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
701
|
-
|
|
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
|
|
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
|
|
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 {
|