mini-coder 0.5.14 → 0.6.1

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 +26 -109
  2. package/bin/mc.ts +8 -11
  3. package/bun.lock +79 -269
  4. package/nono-mini-coder.json +42 -0
  5. package/package.json +17 -22
  6. package/src/agent.ts +243 -1403
  7. package/src/args.ts +289 -0
  8. package/src/headless.ts +41 -359
  9. package/src/index.ts +29 -1016
  10. package/src/oauth.ts +117 -0
  11. package/src/prompt.ts +219 -284
  12. package/src/session.ts +55 -1306
  13. package/src/shared.ts +117 -38
  14. package/src/tool-bash.ts +110 -0
  15. package/src/tool-edit.ts +133 -0
  16. package/src/tool-read.ts +80 -293
  17. package/src/tui-components.ts +150 -0
  18. package/src/tui-conversation.ts +271 -0
  19. package/src/tui-editor.ts +29 -0
  20. package/src/tui-overlay.ts +403 -0
  21. package/src/tui.ts +228 -0
  22. package/src/types.ts +164 -0
  23. package/tsconfig.json +17 -0
  24. package/BENCHMARK.md +0 -107
  25. package/LICENSE +0 -9
  26. package/PROGRESS.md +0 -5
  27. package/assets/icon-1-minimal.svg +0 -31
  28. package/assets/icon-2-dark-terminal.svg +0 -48
  29. package/assets/icon-3-gradient-modern.svg +0 -45
  30. package/assets/icon-4-filled-bold.svg +0 -54
  31. package/assets/icon-5-community-badge.svg +0 -63
  32. package/assets/mc-claude-smart.png +0 -0
  33. package/assets/mc-gpt-smart.png +0 -0
  34. package/assets/preview-0-5-0.png +0 -0
  35. package/assets/preview.gif +0 -0
  36. package/benchmark-baseline.sh +0 -15
  37. package/benchmark-loop.sh +0 -19
  38. package/skills-lock.json +0 -15
  39. package/src/assistant-output.ts +0 -73
  40. package/src/cli.ts +0 -134
  41. package/src/delegation.ts +0 -238
  42. package/src/errors.ts +0 -15
  43. package/src/git.ts +0 -247
  44. package/src/input.ts +0 -168
  45. package/src/mcp.ts +0 -609
  46. package/src/paths.ts +0 -37
  47. package/src/session-message.ts +0 -385
  48. package/src/settings.ts +0 -449
  49. package/src/skills.ts +0 -271
  50. package/src/submit.ts +0 -376
  51. package/src/text.ts +0 -71
  52. package/src/theme.ts +0 -330
  53. package/src/tool-common.ts +0 -93
  54. package/src/tool-delegate.ts +0 -125
  55. package/src/tool-grep.ts +0 -606
  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/submit.ts DELETED
@@ -1,376 +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
- loadCompactedModelMessages,
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 "context_compacted":
275
- state.contextTokens = event.contextTokens;
276
- state.stats = event.stats;
277
- break;
278
- case "text_delta":
279
- case "thinking_delta":
280
- case "toolcall_start":
281
- case "toolcall_delta":
282
- case "toolcall_end":
283
- case "tool_start":
284
- case "tool_delta":
285
- case "tool_end":
286
- case "done":
287
- case "error":
288
- case "aborted":
289
- break;
290
- }
291
- }
292
-
293
- /**
294
- * Submit already-resolved user content as one conversational turn.
295
- *
296
- * Persists the raw prompt, appends the user message, runs the full agent loop,
297
- * updates in-memory state as assistant/tool messages arrive, and returns the
298
- * final stop reason for the turn.
299
- *
300
- * @param rawInput - Exact raw submitted prompt text.
301
- * @param content - Resolved model-visible user content.
302
- * @param state - Mutable application state.
303
- * @param hooks - Optional lifecycle hooks for UI/headless integrations.
304
- * @returns The terminal stop reason for the turn.
305
- */
306
- export async function submitResolvedInput(
307
- rawInput: string,
308
- content: UserMessage["content"],
309
- state: AppState,
310
- hooks?: SubmitTurnHooks,
311
- ): Promise<"stop" | "length" | "error" | "aborted"> {
312
- if (!state.model) {
313
- throw new Error("No model is available for this run.");
314
- }
315
- if (state.running) {
316
- throw new Error("A turn is already running.");
317
- }
318
- if (isEmptyUserContent(content)) {
319
- throw new Error("Cannot submit empty input.");
320
- }
321
-
322
- clearQueuedUserMessages(state);
323
- state.delegationBudgetRemaining = state.delegationBudgetLimit;
324
-
325
- const session = ensureSession(state);
326
- recordRawPromptHistory(rawInput, state, session.id);
327
-
328
- const userMessage = {
329
- role: "user",
330
- content,
331
- timestamp: Date.now(),
332
- } satisfies UserMessage;
333
-
334
- const turn = appendMessage(state.db, session.id, userMessage);
335
- appendConversationMessage(state, userMessage);
336
- hooks?.onUserMessage?.(state);
337
-
338
- const systemPrompt = buildPrompt(state);
339
- const { tools, toolHandlers } = buildToolList(state);
340
- const modelMessages = loadCompactedModelMessages(state.db, session.id);
341
-
342
- state.running = true;
343
- state.abortController = new AbortController();
344
- hooks?.onTurnStart?.(state);
345
-
346
- let stopReason: "stop" | "length" | "error" | "aborted" | null = null;
347
-
348
- try {
349
- const result = await runAgentLoop({
350
- db: state.db,
351
- sessionId: session.id,
352
- turn,
353
- model: state.model,
354
- systemPrompt,
355
- tools,
356
- toolHandlers,
357
- messages: modelMessages,
358
- cwd: state.cwd,
359
- apiKey: state.providers.get(state.model.provider),
360
- effort: state.effort,
361
- signal: state.abortController.signal,
362
- takeQueuedUserMessage: () => state.queuedUserMessages.shift() ?? null,
363
- onEvent: (event) => {
364
- handleAgentEvent(event, state);
365
- hooks?.onEvent?.(event, state);
366
- },
367
- });
368
- stopReason = result.stopReason;
369
- return result.stopReason;
370
- } finally {
371
- clearQueuedUserMessages(state);
372
- state.running = false;
373
- state.abortController = null;
374
- hooks?.onTurnEnd?.(state, stopReason);
375
- }
376
- }
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
- }