mini-coder 0.5.13 → 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 (67) hide show
  1. package/README.md +25 -108
  2. package/bin/mc.ts +8 -11
  3. package/bun.lock +79 -269
  4. package/package.json +17 -22
  5. package/src/agent.ts +242 -915
  6. package/src/args.ts +289 -0
  7. package/src/headless.ts +43 -385
  8. package/src/index.ts +29 -836
  9. package/src/oauth.ts +117 -0
  10. package/src/prompt.ts +227 -276
  11. package/src/session.ts +57 -961
  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 -4
  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/cli.ts +0 -134
  39. package/src/errors.ts +0 -15
  40. package/src/git.ts +0 -247
  41. package/src/input.ts +0 -168
  42. package/src/mcp.ts +0 -609
  43. package/src/paths.ts +0 -37
  44. package/src/session-message.ts +0 -393
  45. package/src/settings.ts +0 -449
  46. package/src/skills.ts +0 -271
  47. package/src/submit.ts +0 -371
  48. package/src/text.ts +0 -71
  49. package/src/theme.ts +0 -330
  50. package/src/tool-common.ts +0 -93
  51. package/src/tool-grep.ts +0 -606
  52. package/src/tool-read.ts +0 -313
  53. package/src/tool-shell.ts +0 -1001
  54. package/src/tools.ts +0 -854
  55. package/src/ui/agent.ts +0 -317
  56. package/src/ui/commands.test.ts +0 -913
  57. package/src/ui/commands.ts +0 -834
  58. package/src/ui/conversation.test.ts +0 -585
  59. package/src/ui/conversation.ts +0 -1836
  60. package/src/ui/help.ts +0 -158
  61. package/src/ui/input.test.ts +0 -64
  62. package/src/ui/input.ts +0 -138
  63. package/src/ui/overlay.ts +0 -59
  64. package/src/ui/runtime.ts +0 -69
  65. package/src/ui/status.ts +0 -220
  66. package/src/ui.ts +0 -1190
  67. package/src/version.ts +0 -48
package/src/submit.ts DELETED
@@ -1,371 +0,0 @@
1
- /**
2
- * Shared raw-input resolution and turn submission logic.
3
- *
4
- * @module
5
- */
6
-
7
- import { readFileSync } from "node:fs";
8
- import type { UserMessage } from "@mariozechner/pi-ai";
9
- import type { AgentEvent } from "./agent.ts";
10
- import { runAgentLoop } from "./agent.ts";
11
- import { getErrorMessage } from "./errors.ts";
12
- import {
13
- type AppState,
14
- buildPrompt,
15
- buildToolList,
16
- ensureSession,
17
- MAX_PROMPT_HISTORY,
18
- } from "./index.ts";
19
- import { parseInput } from "./input.ts";
20
- import {
21
- appendConversationMessage,
22
- appendMessage,
23
- appendPromptHistory,
24
- filterModelMessages,
25
- truncatePromptHistory,
26
- } from "./session.ts";
27
- import { executeReadImage } from "./tools.ts";
28
-
29
- // ---------------------------------------------------------------------------
30
- // Types
31
- // ---------------------------------------------------------------------------
32
-
33
- /** Result of resolving raw user input into a command, message, or error. */
34
- export type ResolvedInput =
35
- | {
36
- /** Empty/whitespace-only input that should be ignored. */
37
- type: "empty";
38
- }
39
- | {
40
- /** Parsed slash command. */
41
- type: "command";
42
- /** Command name without the leading slash. */
43
- command: string;
44
- /** Raw command arguments after trimming. */
45
- args: string;
46
- }
47
- | {
48
- /** Validation or resolution error for the raw input. */
49
- type: "error";
50
- /** User-facing error message. */
51
- message: string;
52
- }
53
- | {
54
- /** Model-visible message content ready for submission. */
55
- type: "message";
56
- /** Fully resolved user content. */
57
- content: UserMessage["content"];
58
- };
59
-
60
- /** Hooks used by UI and headless mode around a submitted turn. */
61
- export interface SubmitTurnHooks {
62
- /** Called after the user message is persisted and added to state. */
63
- onUserMessage?: (state: AppState) => void;
64
- /** Called after the run switches into the active streaming state. */
65
- onTurnStart?: (state: AppState) => void;
66
- /** Called for each agent event emitted during the turn. */
67
- onEvent?: (event: AgentEvent, state: AppState) => void;
68
- /** Called after the turn finishes or aborts its active state. */
69
- onTurnEnd?: (
70
- state: AppState,
71
- stopReason: "stop" | "length" | "error" | "aborted" | null,
72
- ) => void;
73
- }
74
-
75
- // ---------------------------------------------------------------------------
76
- // Input resolution helpers
77
- // ---------------------------------------------------------------------------
78
-
79
- /**
80
- * Strip YAML frontmatter from a skill file.
81
- *
82
- * @param content - Raw `SKILL.md` file content.
83
- * @returns The content without a leading frontmatter block.
84
- */
85
- export function stripSkillFrontmatter(content: string): string {
86
- const frontmatter = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
87
- return frontmatter ? content.slice(frontmatter[0].length) : content;
88
- }
89
-
90
- /**
91
- * Check whether user content contains any meaningful model-visible payload.
92
- *
93
- * @param content - User message content to inspect.
94
- * @returns `true` when the content is empty or whitespace-only.
95
- */
96
- export function isEmptyUserContent(content: UserMessage["content"]): boolean {
97
- if (typeof content === "string") {
98
- return content.trim().length === 0;
99
- }
100
-
101
- return content.every(
102
- (block) => block.type === "text" && block.text.trim().length === 0,
103
- );
104
- }
105
-
106
- function buildSkillMessageContent(
107
- skillName: string,
108
- userText: string,
109
- state: Pick<AppState, "skills">,
110
- ): string | null {
111
- const skill = state.skills.find((entry) => entry.name === skillName);
112
- if (!skill) {
113
- throw new Error(`Unknown skill: ${skillName}`);
114
- }
115
-
116
- let skillBody: string;
117
- try {
118
- skillBody = stripSkillFrontmatter(readFileSync(skill.path, "utf-8")).trim();
119
- } catch (error) {
120
- throw new Error(
121
- `Failed to read skill ${skillName}: ${getErrorMessage(error)}`,
122
- );
123
- }
124
- const parts = [skillBody, userText].filter((part) => part.length > 0);
125
- return parts.length > 0 ? parts.join("\n\n") : null;
126
- }
127
-
128
- function buildImageMessageContent(
129
- imagePath: string,
130
- rawInput: string,
131
- state: Pick<AppState, "cwd">,
132
- ): UserMessage["content"] | null {
133
- const displayPath = rawInput.trim();
134
-
135
- try {
136
- const result = executeReadImage({ path: imagePath }, state.cwd);
137
- if (result.isError) {
138
- return displayPath || null;
139
- }
140
-
141
- return [
142
- { type: "text", text: displayPath },
143
- ...result.content.filter((block) => block.type === "image"),
144
- ];
145
- } catch {
146
- return displayPath || null;
147
- }
148
- }
149
-
150
- /**
151
- * Resolve raw submitted input into a command, a model-visible message, or an error.
152
- *
153
- * This reuses the same parsing rules for interactive and headless input.
154
- *
155
- * @param raw - Raw user input.
156
- * @param state - Current app state needed for skill/image resolution.
157
- * @returns The resolved input result.
158
- */
159
- export function resolveRawInput(
160
- raw: string,
161
- state: Pick<AppState, "model" | "cwd" | "skills">,
162
- ): ResolvedInput {
163
- const parsed = parseInput(raw, {
164
- supportsImages: state.model?.input.includes("image") ?? false,
165
- cwd: state.cwd,
166
- });
167
-
168
- switch (parsed.type) {
169
- case "command":
170
- return {
171
- type: "command",
172
- command: parsed.command,
173
- args: parsed.args,
174
- };
175
- case "skill": {
176
- try {
177
- const content = buildSkillMessageContent(
178
- parsed.skillName,
179
- parsed.userText,
180
- state,
181
- );
182
- return content && !isEmptyUserContent(content)
183
- ? { type: "message", content }
184
- : { type: "empty" };
185
- } catch (error) {
186
- return {
187
- type: "error",
188
- message: getErrorMessage(error),
189
- };
190
- }
191
- }
192
- case "image": {
193
- const content = buildImageMessageContent(parsed.path, raw, state);
194
- return content && !isEmptyUserContent(content)
195
- ? { type: "message", content }
196
- : { type: "empty" };
197
- }
198
- case "text":
199
- return parsed.text && !isEmptyUserContent(parsed.text)
200
- ? { type: "message", content: parsed.text }
201
- : { type: "empty" };
202
- }
203
- }
204
-
205
- function recordRawPromptHistory(
206
- rawInput: string,
207
- state: Pick<AppState, "db" | "cwd">,
208
- sessionId: string,
209
- ): void {
210
- appendPromptHistory(state.db, {
211
- text: rawInput,
212
- cwd: state.cwd,
213
- sessionId,
214
- });
215
- truncatePromptHistory(state.db, MAX_PROMPT_HISTORY);
216
- }
217
-
218
- /**
219
- * Drop any resolved steering messages still queued on the active app state.
220
- *
221
- * Raw prompt-history rows are intentionally left untouched because queue resets
222
- * must not rewrite global input-history behavior.
223
- *
224
- * @param state - Mutable application state containing the queued steering list.
225
- */
226
- export function clearQueuedUserMessages(
227
- state: Pick<AppState, "queuedUserMessages">,
228
- ): void {
229
- state.queuedUserMessages.length = 0;
230
- }
231
-
232
- /**
233
- * Queue resolved user content for the next model-request boundary of an active run.
234
- *
235
- * The raw prompt is recorded immediately in prompt history, but the model-visible
236
- * `UserMessage` is only appended to session history when the agent loop consumes it.
237
- *
238
- * @param rawInput - Exact raw submitted prompt text.
239
- * @param content - Resolved model-visible user content.
240
- * @param state - Mutable application state.
241
- */
242
- export function queueResolvedInput(
243
- rawInput: string,
244
- content: UserMessage["content"],
245
- state: AppState,
246
- ): void {
247
- if (!state.running) {
248
- throw new Error("Cannot queue input while no turn is running.");
249
- }
250
- if (isEmptyUserContent(content)) {
251
- throw new Error("Cannot queue empty input.");
252
- }
253
-
254
- const session = ensureSession(state);
255
- recordRawPromptHistory(rawInput, state, session.id);
256
- state.queuedUserMessages.push({
257
- role: "user",
258
- content,
259
- timestamp: Date.now(),
260
- });
261
- }
262
-
263
- // ---------------------------------------------------------------------------
264
- // Turn submission
265
- // ---------------------------------------------------------------------------
266
-
267
- function handleAgentEvent(event: AgentEvent, state: AppState): void {
268
- switch (event.type) {
269
- case "user_message":
270
- case "assistant_message":
271
- case "tool_result":
272
- appendConversationMessage(state, event.message);
273
- break;
274
- case "text_delta":
275
- case "thinking_delta":
276
- case "toolcall_start":
277
- case "toolcall_delta":
278
- case "toolcall_end":
279
- case "tool_start":
280
- case "tool_delta":
281
- case "tool_end":
282
- case "done":
283
- case "error":
284
- case "aborted":
285
- break;
286
- }
287
- }
288
-
289
- /**
290
- * Submit already-resolved user content as one conversational turn.
291
- *
292
- * Persists the raw prompt, appends the user message, runs the full agent loop,
293
- * updates in-memory state as assistant/tool messages arrive, and returns the
294
- * final stop reason for the turn.
295
- *
296
- * @param rawInput - Exact raw submitted prompt text.
297
- * @param content - Resolved model-visible user content.
298
- * @param state - Mutable application state.
299
- * @param hooks - Optional lifecycle hooks for UI/headless integrations.
300
- * @returns The terminal stop reason for the turn.
301
- */
302
- export async function submitResolvedInput(
303
- rawInput: string,
304
- content: UserMessage["content"],
305
- state: AppState,
306
- hooks?: SubmitTurnHooks,
307
- ): Promise<"stop" | "length" | "error" | "aborted"> {
308
- if (!state.model) {
309
- throw new Error("No model is available for this run.");
310
- }
311
- if (state.running) {
312
- throw new Error("A turn is already running.");
313
- }
314
- if (isEmptyUserContent(content)) {
315
- throw new Error("Cannot submit empty input.");
316
- }
317
-
318
- clearQueuedUserMessages(state);
319
-
320
- const session = ensureSession(state);
321
- recordRawPromptHistory(rawInput, state, session.id);
322
-
323
- const userMessage = {
324
- role: "user",
325
- content,
326
- timestamp: Date.now(),
327
- } satisfies UserMessage;
328
-
329
- const turn = appendMessage(state.db, session.id, userMessage);
330
- appendConversationMessage(state, userMessage);
331
- hooks?.onUserMessage?.(state);
332
-
333
- const systemPrompt = buildPrompt(state);
334
- const { tools, toolHandlers } = buildToolList(state);
335
- const modelMessages = filterModelMessages(state.messages);
336
-
337
- state.running = true;
338
- state.abortController = new AbortController();
339
- hooks?.onTurnStart?.(state);
340
-
341
- let stopReason: "stop" | "length" | "error" | "aborted" | null = null;
342
-
343
- try {
344
- const result = await runAgentLoop({
345
- db: state.db,
346
- sessionId: session.id,
347
- turn,
348
- model: state.model,
349
- systemPrompt,
350
- tools,
351
- toolHandlers,
352
- messages: modelMessages,
353
- cwd: state.cwd,
354
- apiKey: state.providers.get(state.model.provider),
355
- effort: state.effort,
356
- signal: state.abortController.signal,
357
- takeQueuedUserMessage: () => state.queuedUserMessages.shift() ?? null,
358
- onEvent: (event) => {
359
- handleAgentEvent(event, state);
360
- hooks?.onEvent?.(event, state);
361
- },
362
- });
363
- stopReason = result.stopReason;
364
- return result.stopReason;
365
- } finally {
366
- clearQueuedUserMessages(state);
367
- state.running = false;
368
- state.abortController = null;
369
- hooks?.onTurnEnd?.(state, stopReason);
370
- }
371
- }
package/src/text.ts DELETED
@@ -1,71 +0,0 @@
1
- /**
2
- * Shared text-shaping helpers.
3
- *
4
- * @module
5
- */
6
-
7
- /**
8
- * Collapse whitespace runs to single spaces and trim the ends.
9
- *
10
- * @param text - Raw text to normalize.
11
- * @returns The collapsed single-line text.
12
- */
13
- export function collapseWhitespace(text: string): string {
14
- return text.replace(/\s+/g, " ").trim();
15
- }
16
-
17
- /**
18
- * Collapse whitespace into a single line, returning `null` when nothing remains.
19
- *
20
- * @param text - Raw text to normalize.
21
- * @returns Collapsed text, or `null` when the result is empty.
22
- */
23
- export function collapseWhitespaceToNull(text: string): string | null {
24
- const collapsed = collapseWhitespace(text);
25
- return collapsed.length > 0 ? collapsed : null;
26
- }
27
-
28
- /**
29
- * Join only `text` blocks from multipart content into one space-separated string.
30
- *
31
- * @param content - Multipart content blocks.
32
- * @returns Concatenated text-block content.
33
- */
34
- export function joinTextBlocks<T extends { type: string }>(
35
- content: readonly T[],
36
- ): string {
37
- return content
38
- .flatMap((block) => {
39
- return block.type === "text" &&
40
- "text" in block &&
41
- typeof block.text === "string"
42
- ? [block.text]
43
- : [];
44
- })
45
- .join(" ");
46
- }
47
-
48
- /**
49
- * Truncate text with an ellipsis from the start or end.
50
- *
51
- * @param text - Text to truncate.
52
- * @param maxChars - Maximum visible characters including the ellipsis.
53
- * @param side - Which side to truncate from.
54
- * @returns Truncated text when needed.
55
- */
56
- export function truncateText(
57
- text: string,
58
- maxChars: number,
59
- side: "start" | "end" = "end",
60
- ): string {
61
- if (text.length <= maxChars) {
62
- return text;
63
- }
64
- if (maxChars <= 1) {
65
- return "…";
66
- }
67
- if (side === "start") {
68
- return `…${text.slice(text.length - (maxChars - 1))}`;
69
- }
70
- return `${text.slice(0, maxChars - 1)}…`;
71
- }