mini-coder 0.5.14 → 0.6.0

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 (70) hide show
  1. package/README.md +25 -109
  2. package/bin/mc.ts +8 -11
  3. package/bun.lock +79 -269
  4. package/package.json +17 -22
  5. package/src/agent.ts +237 -1403
  6. package/src/args.ts +289 -0
  7. package/src/headless.ts +43 -358
  8. package/src/index.ts +29 -1016
  9. package/src/oauth.ts +117 -0
  10. package/src/prompt.ts +227 -284
  11. package/src/session.ts +55 -1306
  12. package/src/shared.ts +117 -38
  13. package/src/tool-bash.ts +110 -0
  14. package/src/tool-edit.ts +133 -0
  15. package/src/tool-task.ts +114 -0
  16. package/src/tui-components.ts +150 -0
  17. package/src/tui-conversation.ts +262 -0
  18. package/src/tui-editor.ts +29 -0
  19. package/src/tui-overlay.ts +403 -0
  20. package/src/tui.ts +236 -0
  21. package/src/types.ts +160 -0
  22. package/tsconfig.json +17 -0
  23. package/BENCHMARK.md +0 -107
  24. package/LICENSE +0 -9
  25. package/PROGRESS.md +0 -5
  26. package/assets/icon-1-minimal.svg +0 -31
  27. package/assets/icon-2-dark-terminal.svg +0 -48
  28. package/assets/icon-3-gradient-modern.svg +0 -45
  29. package/assets/icon-4-filled-bold.svg +0 -54
  30. package/assets/icon-5-community-badge.svg +0 -63
  31. package/assets/mc-claude-smart.png +0 -0
  32. package/assets/mc-gpt-smart.png +0 -0
  33. package/assets/preview-0-5-0.png +0 -0
  34. package/assets/preview.gif +0 -0
  35. package/benchmark-baseline.sh +0 -15
  36. package/benchmark-loop.sh +0 -19
  37. package/skills-lock.json +0 -15
  38. package/src/assistant-output.ts +0 -73
  39. package/src/cli.ts +0 -134
  40. package/src/delegation.ts +0 -238
  41. package/src/errors.ts +0 -15
  42. package/src/git.ts +0 -247
  43. package/src/input.ts +0 -168
  44. package/src/mcp.ts +0 -609
  45. package/src/paths.ts +0 -37
  46. package/src/session-message.ts +0 -385
  47. package/src/settings.ts +0 -449
  48. package/src/skills.ts +0 -271
  49. package/src/submit.ts +0 -376
  50. package/src/text.ts +0 -71
  51. package/src/theme.ts +0 -330
  52. package/src/tool-common.ts +0 -93
  53. package/src/tool-delegate.ts +0 -125
  54. package/src/tool-grep.ts +0 -606
  55. package/src/tool-read.ts +0 -313
  56. package/src/tool-shell.ts +0 -1051
  57. package/src/tools.ts +0 -1179
  58. package/src/ui/agent.ts +0 -320
  59. package/src/ui/commands.test.ts +0 -957
  60. package/src/ui/commands.ts +0 -848
  61. package/src/ui/conversation.test.ts +0 -585
  62. package/src/ui/conversation.ts +0 -1836
  63. package/src/ui/help.ts +0 -158
  64. package/src/ui/input.test.ts +0 -64
  65. package/src/ui/input.ts +0 -138
  66. package/src/ui/overlay.ts +0 -59
  67. package/src/ui/runtime.ts +0 -69
  68. package/src/ui/status.ts +0 -220
  69. package/src/ui.ts +0 -1190
  70. package/src/version.ts +0 -48
package/src/mcp.ts DELETED
@@ -1,609 +0,0 @@
1
- /**
2
- * Model Context Protocol client discovery and tool integration.
3
- *
4
- * mini-coder tracks configured Streamable HTTP MCP servers, connects enabled
5
- * ones when needed, imports their tools, and exposes those tools through the
6
- * normal pi-ai tool interface.
7
- *
8
- * @module
9
- */
10
-
11
- import type { Static, Tool, ToolCall, TSchema } from "@mariozechner/pi-ai";
12
- import { Type, validateToolArguments } from "@mariozechner/pi-ai";
13
- import { Client } from "@modelcontextprotocol/sdk/client";
14
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
15
- import type { ToolHandler, ToolUpdateCallback } from "./agent.ts";
16
- import { getErrorMessage } from "./errors.ts";
17
- import type { McpSettings } from "./settings.ts";
18
- import { type ToolExecResult, textResult } from "./tool-common.ts";
19
-
20
- const MCP_DISCOVERY_TIMEOUT_MS = 3_000;
21
-
22
- interface McpListedTool {
23
- /** JSON Schema describing the tool input object. */
24
- inputSchema: {
25
- properties?: Record<string, object>;
26
- required?: string[];
27
- type: "object";
28
- [key: string]: unknown;
29
- };
30
- /** Human-readable tool description when provided by the server. */
31
- description?: string;
32
- /** Server-defined MCP tool name. */
33
- name: string;
34
- }
35
-
36
- type McpCallToolResult =
37
- | {
38
- /** Tool content blocks returned by the MCP server. */
39
- content: McpToolResultContent[];
40
- /** Optional structured data returned alongside content. */
41
- structuredContent?: unknown;
42
- /** Whether the server marked this result as an error. */
43
- isError?: boolean;
44
- }
45
- | {
46
- /** Compatibility payload from older MCP tool result formats. */
47
- toolResult: unknown;
48
- };
49
-
50
- type McpToolResultContent =
51
- | {
52
- type: "text";
53
- text: string;
54
- }
55
- | {
56
- type: "image";
57
- data: string;
58
- mimeType: string;
59
- }
60
- | {
61
- type: "audio";
62
- data: string;
63
- mimeType: string;
64
- }
65
- | {
66
- type: "resource";
67
- resource:
68
- | {
69
- uri: string;
70
- text: string;
71
- mimeType?: string;
72
- }
73
- | {
74
- uri: string;
75
- blob: string;
76
- mimeType?: string;
77
- };
78
- }
79
- | {
80
- type: "resource_link";
81
- uri: string;
82
- name: string;
83
- description?: string;
84
- mimeType?: string;
85
- size?: number;
86
- title?: string;
87
- };
88
-
89
- type McpProgress = {
90
- /** Current completed work units reported by the server. */
91
- progress: number;
92
- /** Total work units when the server reports them. */
93
- total?: number;
94
- /** Optional human-readable progress message. */
95
- message?: string;
96
- };
97
-
98
- interface McpTransportLike {
99
- close(): Promise<void>;
100
- }
101
-
102
- interface McpClientLike {
103
- connect(
104
- transport: McpTransportLike,
105
- options?: { timeout?: number },
106
- ): Promise<void>;
107
- listTools(
108
- params?: { cursor?: string },
109
- options?: { timeout?: number },
110
- ): Promise<{
111
- tools: McpListedTool[];
112
- nextCursor?: string;
113
- }>;
114
- callTool(
115
- params: {
116
- name: string;
117
- arguments?: Record<string, unknown>;
118
- },
119
- resultSchema?: undefined,
120
- options?: {
121
- signal?: AbortSignal;
122
- onprogress?: (progress: McpProgress) => void;
123
- },
124
- ): Promise<McpCallToolResult>;
125
- }
126
-
127
- interface McpRuntime {
128
- createClient(): McpClientLike;
129
- createTransport(url: URL): McpTransportLike;
130
- }
131
-
132
- const defaultMcpRuntime: McpRuntime = {
133
- createClient() {
134
- return new Client(
135
- {
136
- name: "mini-coder",
137
- version: "0.0.0",
138
- },
139
- { capabilities: {} },
140
- );
141
- },
142
- createTransport(url) {
143
- return new StreamableHTTPClientTransport(url);
144
- },
145
- };
146
-
147
- async function noopClose(): Promise<void> {}
148
-
149
- /** A configured MCP server and its current runtime connection state. */
150
- export interface McpServerState {
151
- /** User-configured server identifier from `settings.json`. */
152
- name: string;
153
- /** Absolute Streamable HTTP endpoint URL. */
154
- url: string;
155
- /** Whether this server should be available to future turns. */
156
- enabled: boolean;
157
- /** Whether mini-coder currently has an active connection to this server. */
158
- connected: boolean;
159
- /** pi-ai tool definitions derived from the server's tool list. */
160
- tools: Tool[];
161
- /** Tool name → handler map for calling the remote MCP tools. */
162
- toolHandlers: Map<string, ToolHandler>;
163
- /** Close the underlying MCP transport when connected. */
164
- close(): Promise<void>;
165
- }
166
-
167
- interface McpConnectionState {
168
- /** Non-fatal warnings encountered while importing tools. */
169
- warnings: string[];
170
- /** pi-ai tool definitions derived from the server's tool list. */
171
- tools: Tool[];
172
- /** Tool name → handler map for calling the remote MCP tools. */
173
- toolHandlers: Map<string, ToolHandler>;
174
- /** Close the underlying MCP transport. */
175
- close(): Promise<void>;
176
- }
177
-
178
- interface McpDiscoveryResult {
179
- /** MCP servers kept in runtime state after startup discovery. */
180
- servers: McpServerState[];
181
- /** Non-fatal startup warnings for skipped or unreachable servers. */
182
- warnings: string[];
183
- }
184
-
185
- function createDisconnectedMcpServer(
186
- entry: NonNullable<McpSettings["servers"]>[number],
187
- ): McpServerState {
188
- return {
189
- name: entry.name,
190
- url: entry.url,
191
- enabled: entry.enabled,
192
- connected: false,
193
- tools: [],
194
- toolHandlers: new Map(),
195
- close: noopClose,
196
- };
197
- }
198
-
199
- function resetMcpServerConnection(server: McpServerState): void {
200
- server.connected = false;
201
- server.tools = [];
202
- server.toolHandlers = new Map();
203
- server.close = noopClose;
204
- }
205
-
206
- function parseMcpServerUrl(rawUrl: string): URL | null {
207
- try {
208
- return new URL(rawUrl);
209
- } catch {
210
- return null;
211
- }
212
- }
213
-
214
- /**
215
- * Connect a configured MCP server and populate its imported tools.
216
- *
217
- * On failure the server is left disconnected and the returned warnings describe
218
- * why the connection was skipped.
219
- *
220
- * @param server - Runtime MCP server state to connect.
221
- * @param runtime - Internal runtime injection for tests.
222
- * @returns Non-fatal warnings produced while connecting or importing tools.
223
- */
224
- export async function connectMcpServer(
225
- server: McpServerState,
226
- runtime: McpRuntime = defaultMcpRuntime,
227
- ): Promise<string[]> {
228
- if (server.connected) {
229
- return [];
230
- }
231
-
232
- const connection = await createMcpServerConnection(
233
- server.name,
234
- server.url,
235
- runtime,
236
- );
237
-
238
- if (!("tools" in connection)) {
239
- resetMcpServerConnection(server);
240
- return [connection.warning];
241
- }
242
-
243
- server.connected = true;
244
- server.tools = connection.tools;
245
- server.toolHandlers = connection.toolHandlers;
246
- server.close = connection.close;
247
- return connection.warnings;
248
- }
249
-
250
- /**
251
- * Disconnect an MCP server and clear its imported tool state.
252
- *
253
- * @param server - Runtime MCP server state to disconnect.
254
- */
255
- export async function disconnectMcpServer(
256
- server: McpServerState,
257
- ): Promise<void> {
258
- if (!server.connected) {
259
- return;
260
- }
261
-
262
- const close = server.close;
263
- resetMcpServerConnection(server);
264
- await close();
265
- }
266
-
267
- /**
268
- * Connect enabled MCP servers at startup while keeping disabled ones in state.
269
- *
270
- * Invalid or unreachable servers do not fail startup. Enabled servers that
271
- * cannot connect are skipped with warnings. Disabled servers stay disconnected
272
- * until the user turns them back on.
273
- *
274
- * @param settings - Optional MCP settings from `settings.json`.
275
- * @param runtime - Internal runtime injection for tests.
276
- * @returns MCP server state plus any non-fatal startup warnings.
277
- */
278
- export async function discoverMcpServers(
279
- settings: McpSettings | undefined,
280
- runtime: McpRuntime = defaultMcpRuntime,
281
- ): Promise<McpDiscoveryResult> {
282
- const servers: McpServerState[] = [];
283
- const warnings: string[] = [];
284
-
285
- for (const entry of settings?.servers ?? []) {
286
- if (!parseMcpServerUrl(entry.url)) {
287
- warnings.push(`MCP server "${entry.name}": invalid URL (${entry.url})`);
288
- continue;
289
- }
290
-
291
- const server = createDisconnectedMcpServer(entry);
292
- if (!entry.enabled) {
293
- servers.push(server);
294
- continue;
295
- }
296
-
297
- const connectWarnings = await connectMcpServer(server, runtime);
298
- warnings.push(...connectWarnings);
299
- if (!server.connected) {
300
- continue;
301
- }
302
-
303
- servers.push(server);
304
- }
305
-
306
- return { servers, warnings };
307
- }
308
-
309
- async function createMcpServerConnection(
310
- serverName: string,
311
- serverUrl: string,
312
- runtime: McpRuntime,
313
- ): Promise<McpConnectionState | { warning: string }> {
314
- const url = parseMcpServerUrl(serverUrl);
315
- if (!url) {
316
- return {
317
- warning: `MCP server "${serverName}": invalid URL (${serverUrl})`,
318
- };
319
- }
320
-
321
- const transport = runtime.createTransport(url);
322
- const client = runtime.createClient();
323
-
324
- try {
325
- await client.connect(transport, { timeout: MCP_DISCOVERY_TIMEOUT_MS });
326
- const listedTools = await listAllTools(client);
327
-
328
- if (listedTools.length === 0) {
329
- await closeQuietly(transport);
330
- return {
331
- warning: `MCP server "${serverName}" exposes no tools and was skipped.`,
332
- };
333
- }
334
-
335
- const warnings: string[] = [];
336
- const tools: Tool[] = [];
337
- const toolHandlers = new Map<string, ToolHandler>();
338
-
339
- for (const listedTool of listedTools) {
340
- const { tool, handler } = createMcpTool(serverName, listedTool, client);
341
- if (toolHandlers.has(tool.name)) {
342
- warnings.push(
343
- `MCP server "${serverName}": duplicate tool name "${listedTool.name}" was skipped.`,
344
- );
345
- continue;
346
- }
347
- tools.push(tool);
348
- toolHandlers.set(tool.name, handler);
349
- }
350
-
351
- if (tools.length === 0) {
352
- await closeQuietly(transport);
353
- return {
354
- warning: `MCP server "${serverName}" exposes no usable tools and was skipped.`,
355
- };
356
- }
357
-
358
- return {
359
- warnings,
360
- tools,
361
- toolHandlers,
362
- close: () => transport.close(),
363
- };
364
- } catch (error) {
365
- await closeQuietly(transport);
366
- return {
367
- warning: `MCP server "${serverName}": ${getErrorMessage(error)} (${serverUrl})`,
368
- };
369
- }
370
- }
371
-
372
- async function listAllTools(client: McpClientLike): Promise<McpListedTool[]> {
373
- const tools: McpListedTool[] = [];
374
- let cursor: string | undefined;
375
-
376
- do {
377
- const page = await client.listTools(cursor ? { cursor } : undefined, {
378
- timeout: MCP_DISCOVERY_TIMEOUT_MS,
379
- });
380
- tools.push(...page.tools);
381
- cursor = page.nextCursor;
382
- } while (cursor);
383
-
384
- return tools;
385
- }
386
-
387
- function createMcpTool(
388
- serverName: string,
389
- listedTool: McpListedTool,
390
- client: McpClientLike,
391
- ): {
392
- tool: Tool;
393
- handler: ToolHandler;
394
- } {
395
- const toolName = `${serverName}__${listedTool.name}`;
396
- const tool: Tool = {
397
- name: toolName,
398
- description: listedTool.description
399
- ? `[MCP ${serverName}] ${listedTool.description}`
400
- : `[MCP ${serverName}] ${listedTool.name}`,
401
- parameters: Type.Unsafe<Record<string, unknown>>(listedTool.inputSchema),
402
- };
403
-
404
- const handler: ToolHandler = async (args, _cwd, signal, onUpdate) => {
405
- const validatedArgs = validateMcpToolArgs(tool, args) as Record<
406
- string,
407
- unknown
408
- >;
409
-
410
- try {
411
- const result = await client.callTool(
412
- {
413
- name: listedTool.name,
414
- arguments: validatedArgs,
415
- },
416
- undefined,
417
- {
418
- ...(signal ? { signal } : {}),
419
- ...(onUpdate
420
- ? {
421
- onprogress: (progress: McpProgress) => {
422
- emitMcpProgressUpdate(
423
- serverName,
424
- listedTool.name,
425
- progress,
426
- onUpdate,
427
- );
428
- },
429
- }
430
- : {}),
431
- },
432
- );
433
-
434
- return convertMcpToolResult(result);
435
- } catch (error) {
436
- return textResult(
437
- `MCP server "${serverName}" tool "${listedTool.name}" failed: ${getErrorMessage(error)}`,
438
- true,
439
- );
440
- }
441
- };
442
-
443
- return { tool, handler };
444
- }
445
-
446
- function emitMcpProgressUpdate(
447
- serverName: string,
448
- toolName: string,
449
- progress: McpProgress,
450
- onUpdate: ToolUpdateCallback,
451
- ): void {
452
- const status =
453
- progress.total != null
454
- ? `${progress.progress}/${progress.total}`
455
- : `${progress.progress}`;
456
- const suffix = progress.message ? ` — ${progress.message}` : "";
457
- onUpdate(
458
- textResult(
459
- `MCP ${serverName}/${toolName} progress: ${status}${suffix}`,
460
- false,
461
- ),
462
- );
463
- }
464
-
465
- function convertMcpToolResult(result: McpCallToolResult): ToolExecResult {
466
- if ("toolResult" in result) {
467
- return textResult(formatUnknownValue(result.toolResult), false);
468
- }
469
-
470
- const content: ToolExecResult["content"] = [];
471
-
472
- for (const item of result.content) {
473
- switch (item.type) {
474
- case "text":
475
- content.push({ type: "text", text: item.text });
476
- break;
477
- case "image":
478
- content.push({
479
- type: "image",
480
- data: item.data,
481
- mimeType: item.mimeType,
482
- });
483
- break;
484
- case "audio":
485
- content.push({
486
- type: "text",
487
- text: `Audio output (${item.mimeType}) is not renderable in mini-coder tool results.`,
488
- });
489
- break;
490
- case "resource":
491
- content.push({
492
- type: "text",
493
- text: formatResourceContent(item.resource),
494
- });
495
- break;
496
- case "resource_link":
497
- content.push({
498
- type: "text",
499
- text: formatResourceLinkContent(item),
500
- });
501
- break;
502
- }
503
- }
504
-
505
- if (content.length === 0) {
506
- if (result.structuredContent !== undefined) {
507
- content.push({
508
- type: "text",
509
- text: formatUnknownValue(result.structuredContent),
510
- });
511
- } else {
512
- content.push({
513
- type: "text",
514
- text: "MCP tool returned no content.",
515
- });
516
- }
517
- }
518
-
519
- return {
520
- content,
521
- details:
522
- result.structuredContent === undefined
523
- ? undefined
524
- : { structuredContent: result.structuredContent },
525
- isError: result.isError ?? false,
526
- };
527
- }
528
-
529
- function formatResourceContent(
530
- resource:
531
- | {
532
- uri: string;
533
- text: string;
534
- mimeType?: string;
535
- }
536
- | {
537
- uri: string;
538
- blob: string;
539
- mimeType?: string;
540
- },
541
- ): string {
542
- if ("text" in resource) {
543
- return resource.mimeType
544
- ? `Resource ${resource.uri} (${resource.mimeType})\n\n${resource.text}`
545
- : `Resource ${resource.uri}\n\n${resource.text}`;
546
- }
547
-
548
- return resource.mimeType
549
- ? `Resource blob ${resource.uri} (${resource.mimeType})`
550
- : `Resource blob ${resource.uri}`;
551
- }
552
-
553
- function formatResourceLinkContent(item: {
554
- uri: string;
555
- name: string;
556
- description?: string;
557
- mimeType?: string;
558
- size?: number;
559
- title?: string;
560
- }): string {
561
- const lines = [
562
- item.title ? `Resource link: ${item.title}` : `Resource link: ${item.name}`,
563
- `URI: ${item.uri}`,
564
- ];
565
-
566
- if (item.description) {
567
- lines.push(`Description: ${item.description}`);
568
- }
569
- if (item.mimeType) {
570
- lines.push(`MIME type: ${item.mimeType}`);
571
- }
572
- if (item.size != null) {
573
- lines.push(`Size: ${item.size}`);
574
- }
575
-
576
- return lines.join("\n");
577
- }
578
-
579
- function formatUnknownValue(value: unknown): string {
580
- if (typeof value === "string") {
581
- return value;
582
- }
583
-
584
- try {
585
- return JSON.stringify(value, null, 2);
586
- } catch {
587
- return String(value);
588
- }
589
- }
590
-
591
- function validateMcpToolArgs<TParameters extends TSchema>(
592
- tool: Tool<TParameters>,
593
- args: Record<string, unknown>,
594
- ): Static<TParameters> {
595
- return validateToolArguments(tool, {
596
- type: "toolCall",
597
- id: tool.name,
598
- name: tool.name,
599
- arguments: args,
600
- } satisfies ToolCall) as Static<TParameters>;
601
- }
602
-
603
- async function closeQuietly(transport: McpTransportLike): Promise<void> {
604
- try {
605
- await transport.close();
606
- } catch {
607
- // Best effort only.
608
- }
609
- }
package/src/paths.ts DELETED
@@ -1,37 +0,0 @@
1
- /**
2
- * Filesystem path normalization helpers.
3
- *
4
- * Provides a small shared policy for path identity across the app:
5
- * when paths are used for comparison or persistence, we canonicalize them
6
- * to an absolute, symlink-resolved spelling.
7
- *
8
- * @module
9
- */
10
-
11
- import { realpathSync } from "node:fs";
12
- import { resolve } from "node:path";
13
-
14
- /**
15
- * Return the canonical absolute path for an existing filesystem entry.
16
- *
17
- * Canonicalization resolves `.`/`..` segments and follows symlinks,
18
- * producing a stable spelling suitable for path equality checks and
19
- * persistence keys.
20
- *
21
- * @param path - An existing filesystem path.
22
- * @returns The canonical absolute path.
23
- */
24
- export function canonicalizePath(path: string): string {
25
- return realpathSync(resolve(path));
26
- }
27
-
28
- /**
29
- * Check whether two paths refer to the same existing filesystem entry.
30
- *
31
- * @param a - The first path to compare.
32
- * @param b - The second path to compare.
33
- * @returns `true` when both paths resolve to the same canonical location.
34
- */
35
- export function isSamePath(a: string, b: string): boolean {
36
- return canonicalizePath(a) === canonicalizePath(b);
37
- }