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.
- package/README.md +26 -109
- package/bin/mc.ts +8 -11
- package/bun.lock +79 -269
- package/nono-mini-coder.json +42 -0
- package/package.json +17 -22
- package/src/agent.ts +243 -1403
- package/src/args.ts +289 -0
- package/src/headless.ts +41 -359
- package/src/index.ts +29 -1016
- package/src/oauth.ts +117 -0
- package/src/prompt.ts +219 -284
- package/src/session.ts +55 -1306
- package/src/shared.ts +117 -38
- package/src/tool-bash.ts +110 -0
- package/src/tool-edit.ts +133 -0
- package/src/tool-read.ts +80 -293
- package/src/tui-components.ts +150 -0
- package/src/tui-conversation.ts +271 -0
- package/src/tui-editor.ts +29 -0
- package/src/tui-overlay.ts +403 -0
- package/src/tui.ts +228 -0
- package/src/types.ts +164 -0
- package/tsconfig.json +17 -0
- package/BENCHMARK.md +0 -107
- package/LICENSE +0 -9
- package/PROGRESS.md +0 -5
- package/assets/icon-1-minimal.svg +0 -31
- package/assets/icon-2-dark-terminal.svg +0 -48
- package/assets/icon-3-gradient-modern.svg +0 -45
- package/assets/icon-4-filled-bold.svg +0 -54
- package/assets/icon-5-community-badge.svg +0 -63
- package/assets/mc-claude-smart.png +0 -0
- package/assets/mc-gpt-smart.png +0 -0
- package/assets/preview-0-5-0.png +0 -0
- package/assets/preview.gif +0 -0
- package/benchmark-baseline.sh +0 -15
- package/benchmark-loop.sh +0 -19
- package/skills-lock.json +0 -15
- package/src/assistant-output.ts +0 -73
- package/src/cli.ts +0 -134
- package/src/delegation.ts +0 -238
- package/src/errors.ts +0 -15
- package/src/git.ts +0 -247
- package/src/input.ts +0 -168
- package/src/mcp.ts +0 -609
- package/src/paths.ts +0 -37
- package/src/session-message.ts +0 -385
- package/src/settings.ts +0 -449
- package/src/skills.ts +0 -271
- package/src/submit.ts +0 -376
- package/src/text.ts +0 -71
- package/src/theme.ts +0 -330
- package/src/tool-common.ts +0 -93
- package/src/tool-delegate.ts +0 -125
- package/src/tool-grep.ts +0 -606
- package/src/tool-shell.ts +0 -1051
- package/src/tools.ts +0 -1179
- package/src/ui/agent.ts +0 -320
- package/src/ui/commands.test.ts +0 -957
- package/src/ui/commands.ts +0 -848
- package/src/ui/conversation.test.ts +0 -585
- package/src/ui/conversation.ts +0 -1836
- package/src/ui/help.ts +0 -158
- package/src/ui/input.test.ts +0 -64
- package/src/ui/input.ts +0 -138
- package/src/ui/overlay.ts +0 -59
- package/src/ui/runtime.ts +0 -69
- package/src/ui/status.ts +0 -220
- package/src/ui.ts +0 -1190
- package/src/version.ts +0 -48
package/src/shared.ts
CHANGED
|
@@ -1,39 +1,118 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parseDocument } from "yaml";
|
|
4
|
+
|
|
5
|
+
// Mixed bag of helpers that can be shared across the codebase
|
|
6
|
+
|
|
7
|
+
export const DATA_DIR = join(homedir(), ".config", "mini-coder");
|
|
8
|
+
export const SESSIONS_DIR = join(DATA_DIR, "sessions");
|
|
9
|
+
export const AUTH_PATH = join(DATA_DIR, "auth.json");
|
|
10
|
+
export const SETTINGS_PATH = join(DATA_DIR, "settings.json");
|
|
11
|
+
|
|
12
|
+
export function secureRandomString(
|
|
13
|
+
length: number,
|
|
14
|
+
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
|
|
15
|
+
): string {
|
|
16
|
+
const result: string[] = [];
|
|
17
|
+
const charsLength = chars.length;
|
|
18
|
+
const maxValid = Math.floor(256 / charsLength) * charsLength;
|
|
19
|
+
const randomBytes = new Uint8Array(length * 2);
|
|
20
|
+
|
|
21
|
+
while (result.length < length) {
|
|
22
|
+
crypto.getRandomValues(randomBytes);
|
|
23
|
+
|
|
24
|
+
for (const byte of randomBytes) {
|
|
25
|
+
if (byte < maxValid) {
|
|
26
|
+
result.push(chars[byte % charsLength]);
|
|
27
|
+
if (result.length === length) break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return result.join("");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function elapsedTime(seconds: number): string {
|
|
36
|
+
if (seconds < 60) return `${seconds}s`;
|
|
37
|
+
|
|
38
|
+
const minutes = Math.floor(seconds / 60);
|
|
39
|
+
if (minutes < 60) return `${minutes}m`;
|
|
40
|
+
|
|
41
|
+
const hours = Math.floor(minutes / 60);
|
|
42
|
+
if (hours < 24) return `${hours}h`;
|
|
43
|
+
|
|
44
|
+
const days = Math.floor(hours / 24);
|
|
45
|
+
if (days < 7) return `${days}d`;
|
|
46
|
+
|
|
47
|
+
const weeks = Math.floor(days / 7);
|
|
48
|
+
if (weeks < 4) return `${weeks}w`;
|
|
49
|
+
|
|
50
|
+
const months = Math.floor(days / 30);
|
|
51
|
+
if (months < 12) return `${months}mo`;
|
|
52
|
+
|
|
53
|
+
const years = Math.floor(days / 365);
|
|
54
|
+
return `${years}y`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function relativeTime(timestamp: number): string {
|
|
58
|
+
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
59
|
+
return elapsedTime(seconds);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function onceEvery<T extends unknown[]>(
|
|
63
|
+
n: number,
|
|
64
|
+
fn: (...args: T) => void,
|
|
65
|
+
) {
|
|
66
|
+
let calls = 0;
|
|
67
|
+
|
|
68
|
+
return (...args: T) => {
|
|
69
|
+
calls++;
|
|
70
|
+
|
|
71
|
+
if (calls % n === 0) {
|
|
72
|
+
fn(...args);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function takeTail<T>(arr: T[], x: number): T[] {
|
|
78
|
+
return x <= 0 ? [] : arr.slice(-x);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function estimateTokens(text: string): number {
|
|
82
|
+
return Math.ceil(text.length / 4);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function parseSkillFrontmatter(content: string) {
|
|
86
|
+
const match = /^---\s*\n([\s\S]*?)\n---/.exec(content);
|
|
87
|
+
|
|
88
|
+
if (!match) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const doc = parseDocument(match[1]);
|
|
93
|
+
const data = doc.toJS() as unknown;
|
|
94
|
+
|
|
95
|
+
if (!data || typeof data !== "object") {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const record = data as Record<string, unknown>;
|
|
100
|
+
const name = typeof record.name === "string" ? record.name.trim() : "";
|
|
101
|
+
const description =
|
|
102
|
+
typeof record.description === "string" ? record.description.trim() : "";
|
|
103
|
+
|
|
104
|
+
if (!name || !description) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { name, description };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function formatTimestamp(timestampMs: number): string {
|
|
112
|
+
return new Date(timestampMs).toLocaleTimeString("en-GB", {
|
|
113
|
+
hour: "2-digit",
|
|
114
|
+
minute: "2-digit",
|
|
115
|
+
second: "2-digit",
|
|
116
|
+
hour12: false,
|
|
117
|
+
});
|
|
39
118
|
}
|
package/src/tool-bash.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { type Tool, Type } from "@mariozechner/pi-ai";
|
|
2
|
+
import { secureRandomString } from "./shared";
|
|
3
|
+
import type { ToolRunnerEvent } from "./types";
|
|
4
|
+
|
|
5
|
+
const OUTPUT_THRESHOLD = 16000;
|
|
6
|
+
const description = `## Bash CLI tool
|
|
7
|
+
|
|
8
|
+
Execute shell commands on the user's environment.
|
|
9
|
+
|
|
10
|
+
Best practices:
|
|
11
|
+
|
|
12
|
+
- Use \`ls\` to list files.
|
|
13
|
+
- Use \`fd\` or \`find\` to locate files/directories by name, type, size, time, permissions, etc.
|
|
14
|
+
- Use \`rg\` or \`grep\` to search inside files for matching patterns.
|
|
15
|
+
- Use \`cat -n\` or \`nl\` to read small files, or when you need the whole file.
|
|
16
|
+
- Use \`sed -n\` with ranges to read sections of files. Prefer targeted reads. Avoid dumping very large (more than ~200 lines) files all at once.
|
|
17
|
+
- Use \`cp\`, \`mv\`, and \`mkdir\` for file and directory operations, and \`rm\` to remove files and directories.
|
|
18
|
+
- Use \`curl\` for web access. Use redirection to temp files for targeted reads.
|
|
19
|
+
- Use development tools like \`git\`, \`gh\`, \`jq\`, etc, when appropriate.
|
|
20
|
+
- Prefer \`cp -i\`, \`mv -i\`, \`rm -i\` when learning.
|
|
21
|
+
- NEVER run destructive commands (\`rm -rf\`, \`git reset --hard\`, overwriting files) without confirming the target first.
|
|
22
|
+
- Chain commands **only** when failure should stop the flow. Avoid long chains, **2 to 3 maximum**.
|
|
23
|
+
- Avoid overly complex one-liners; readability matters.
|
|
24
|
+
- Quote filenames: use \`"$file"\` not \`$file\`.
|
|
25
|
+
- Be careful with spaces in filenames.
|
|
26
|
+
|
|
27
|
+
Commands run in: ${process.cwd()}
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
export const bash: Tool = {
|
|
31
|
+
name: "bash",
|
|
32
|
+
description,
|
|
33
|
+
parameters: Type.Object({
|
|
34
|
+
command: Type.String({
|
|
35
|
+
description:
|
|
36
|
+
"Shell command to execute. Prefer simple, focused commands over complex one-liners.",
|
|
37
|
+
}),
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export async function* runBashTool(
|
|
42
|
+
args: Record<string, any>,
|
|
43
|
+
signal?: AbortSignal,
|
|
44
|
+
): AsyncGenerator<ToolRunnerEvent> {
|
|
45
|
+
// Redirect stderr into stdout for the whole shell session.
|
|
46
|
+
const proc = Bun.spawn(["bash", "-c", `exec 2>&1; ${args.command}`], {
|
|
47
|
+
stdout: "pipe",
|
|
48
|
+
stderr: "pipe",
|
|
49
|
+
env: {
|
|
50
|
+
...Bun.env,
|
|
51
|
+
NO_COLOR: "1",
|
|
52
|
+
},
|
|
53
|
+
signal,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const decoder = new TextDecoder();
|
|
57
|
+
const reader = proc.stdout.getReader();
|
|
58
|
+
|
|
59
|
+
let output = "";
|
|
60
|
+
while (true) {
|
|
61
|
+
const { done, value } = await reader.read();
|
|
62
|
+
|
|
63
|
+
if (done) {
|
|
64
|
+
const remaining = Bun.stripANSI(decoder.decode());
|
|
65
|
+
|
|
66
|
+
if (remaining.length) {
|
|
67
|
+
output += remaining;
|
|
68
|
+
yield { type: "output", text: remaining };
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const text = Bun.stripANSI(decoder.decode(value, { stream: true }));
|
|
74
|
+
|
|
75
|
+
if (text.length) {
|
|
76
|
+
output += text;
|
|
77
|
+
yield { type: "output", text: text };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const exitCode = await proc.exited;
|
|
82
|
+
|
|
83
|
+
let result = `# EXIT CODE: ${exitCode}`;
|
|
84
|
+
if (output.length) {
|
|
85
|
+
result += `
|
|
86
|
+
# OUTPUT:
|
|
87
|
+
|
|
88
|
+
${output}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// If `out` is too big, more than ~XXKB, write it to a temp file
|
|
92
|
+
// And add that to the truncation label for the agent to be able
|
|
93
|
+
// to continue the read with scans. This is to protect context,
|
|
94
|
+
// not a general read guard. The hint is for the agent, not the TUI
|
|
95
|
+
if (result.length > OUTPUT_THRESHOLD) {
|
|
96
|
+
const key = `${Date.now()}-${secureRandomString(4)}`;
|
|
97
|
+
const pathname = `/tmp/bash_result_${key}.txt`;
|
|
98
|
+
await Bun.write(pathname, result);
|
|
99
|
+
result = `${result.substring(0, OUTPUT_THRESHOLD)}
|
|
100
|
+
|
|
101
|
+
Truncated at ~${OUTPUT_THRESHOLD / 1000}KB. Full output at ${pathname}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
yield {
|
|
105
|
+
type: "result",
|
|
106
|
+
text: result,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return result;
|
|
110
|
+
}
|
package/src/tool-edit.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { isAbsolute, join } from "node:path";
|
|
2
|
+
import { type Tool, Type } from "@mariozechner/pi-ai";
|
|
3
|
+
import { createPatch } from "diff";
|
|
4
|
+
import type { ToolRunnerEvent } from "./types";
|
|
5
|
+
|
|
6
|
+
const description = `## Edit tool
|
|
7
|
+
|
|
8
|
+
A find-and-replace file editor. Use it to create new files or modify existing ones safely. Always prefer this tool over bash editing methods (sed, awk, etc).
|
|
9
|
+
|
|
10
|
+
### Rules
|
|
11
|
+
- The tool refuses to edit on multiple matches of \`oldText\`. Be specific with your matching text.
|
|
12
|
+
- Prefer patch-based edits (small targeted replacements) for multi-line or semantic changes.
|
|
13
|
+
- Do NOT reproduce entire files. Use shell file operations (\`cp\`, \`mv\`, etc) for wholesale file replacement instead.
|
|
14
|
+
|
|
15
|
+
### Failure modes
|
|
16
|
+
- If \`oldText\` is not found, the edit fails. Verify the exact text first.
|
|
17
|
+
- If \`oldText\` matches multiple locations, the edit fails. Narrow your match and retry.
|
|
18
|
+
- If the file does not exist and \`oldText\` is non-empty, the edit fails.
|
|
19
|
+
|
|
20
|
+
<example>
|
|
21
|
+
Edit a single line:
|
|
22
|
+
path: src/utils.ts
|
|
23
|
+
oldText: const MAX_RETRIES = 3;
|
|
24
|
+
newText: const MAX_RETRIES = 5;
|
|
25
|
+
</example>
|
|
26
|
+
|
|
27
|
+
<example>
|
|
28
|
+
Patch-based edit (preferred for multi-line changes):
|
|
29
|
+
path: src/utils.ts
|
|
30
|
+
oldText: function oldHelper() {\n return 1;\n}
|
|
31
|
+
newText: function newHelper() {\n return 2;\n}\n\nfunction oldHelper() {\n return 1;\n}
|
|
32
|
+
</example>
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
export const edit: Tool = {
|
|
36
|
+
name: `edit`,
|
|
37
|
+
description,
|
|
38
|
+
parameters: Type.Object({
|
|
39
|
+
path: Type.String({
|
|
40
|
+
description:
|
|
41
|
+
"File path. Absolute or relative to the current working directory.",
|
|
42
|
+
}),
|
|
43
|
+
oldText: Type.String({
|
|
44
|
+
description:
|
|
45
|
+
'Exact text to find and replace. Empty string means "create new file".',
|
|
46
|
+
}),
|
|
47
|
+
newText: Type.String({
|
|
48
|
+
description: "Replacement text (or full content for new files)",
|
|
49
|
+
}),
|
|
50
|
+
}),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function findAllIndexes(text: string, sub: string): number[] {
|
|
54
|
+
if (sub.length === 0) return [];
|
|
55
|
+
|
|
56
|
+
const indexes: number[] = [];
|
|
57
|
+
|
|
58
|
+
let pos = text.indexOf(sub, 0);
|
|
59
|
+
while (pos !== -1) {
|
|
60
|
+
indexes.push(pos);
|
|
61
|
+
pos += sub.length; // use pos += 1 if you want overlapping matches
|
|
62
|
+
pos = text.indexOf(sub, pos);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return indexes;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function* runEditTool(
|
|
69
|
+
args: Record<string, any>,
|
|
70
|
+
signal?: AbortSignal,
|
|
71
|
+
): AsyncGenerator<ToolRunnerEvent> {
|
|
72
|
+
const filePath = isAbsolute(args.path)
|
|
73
|
+
? args.path
|
|
74
|
+
: join(process.cwd(), args.path);
|
|
75
|
+
const file = Bun.file(filePath);
|
|
76
|
+
const exists = await file.exists();
|
|
77
|
+
|
|
78
|
+
if (args.oldText === "") {
|
|
79
|
+
if (exists) {
|
|
80
|
+
yield { type: "result", text: `File already exists: ${filePath}` };
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await Bun.write(file, args.newText);
|
|
85
|
+
yield {
|
|
86
|
+
type: "result",
|
|
87
|
+
text: `File written: ${filePath}\n\n${args.newText}`,
|
|
88
|
+
};
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!exists) {
|
|
93
|
+
yield { type: "result", text: `File not found: ${filePath}` };
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const content = await file.text();
|
|
98
|
+
const matches = findAllIndexes(content, args.oldText);
|
|
99
|
+
|
|
100
|
+
if (matches.length === 0) {
|
|
101
|
+
yield { type: "result", text: `Old text not found in: ${filePath}` };
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (matches.length > 1) {
|
|
106
|
+
yield {
|
|
107
|
+
type: "result",
|
|
108
|
+
text: `Multiple matches found in ${filePath}: ${matches.length} matches, be more specific and try again`,
|
|
109
|
+
};
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const idx = matches[0];
|
|
114
|
+
const updated =
|
|
115
|
+
content.slice(0, idx) +
|
|
116
|
+
args.newText +
|
|
117
|
+
content.slice(idx + args.oldText.length);
|
|
118
|
+
|
|
119
|
+
if (signal?.aborted) {
|
|
120
|
+
yield { type: "result", text: "Aborted before write." };
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await file.write(updated);
|
|
125
|
+
const patch = createPatch(filePath, content, updated);
|
|
126
|
+
|
|
127
|
+
yield {
|
|
128
|
+
type: "result",
|
|
129
|
+
text: patch,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return;
|
|
133
|
+
}
|