mini-coder 0.7.3 → 0.8.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.
package/src/oauth.ts DELETED
@@ -1,157 +0,0 @@
1
- import { mkdir } from "node:fs/promises";
2
- import readline from "node:readline";
3
-
4
- import { getEnvApiKey, getProviders } from "@earendil-works/pi-ai";
5
- import {
6
- getOAuthApiKey,
7
- getOAuthProvider,
8
- getOAuthProviders,
9
- type OAuthLoginCallbacks,
10
- type OAuthPrompt,
11
- type OAuthProviderId,
12
- type OAuthSelectPrompt,
13
- } from "@earendil-works/pi-ai/oauth";
14
- import { AUTH_PATH as AUTH_FILE, DATA_DIR } from "./shared";
15
- import type { CliOptions, SavedOAuthCreds } from "./types";
16
-
17
- type ReadlineInterface = ReturnType<typeof readline.createInterface>;
18
-
19
- function ask(rl: ReadlineInterface, question: string): Promise<string> {
20
- return new Promise((resolve) => rl.question(question, resolve));
21
- }
22
-
23
- function formatPrompt(prompt: OAuthPrompt): string {
24
- const placeholder = prompt.placeholder ? ` (${prompt.placeholder})` : "";
25
- return `${prompt.message}${placeholder}: `;
26
- }
27
-
28
- async function selectOption(
29
- rl: ReadlineInterface,
30
- prompt: OAuthSelectPrompt,
31
- ): Promise<string | undefined> {
32
- console.log(prompt.message);
33
- for (let i = 0; i < prompt.options.length; i++) {
34
- console.log(`${i + 1}. ${prompt.options[i]?.label}`);
35
- }
36
-
37
- const choice = await ask(rl, `Enter number (1-${prompt.options.length}): `);
38
- const index = Number.parseInt(choice, 10) - 1;
39
- return prompt.options[index]?.id;
40
- }
41
-
42
- function createLoginCallbacks(rl: ReadlineInterface): OAuthLoginCallbacks {
43
- return {
44
- onAuth: ({ url, instructions }) => {
45
- console.log(`Open: ${url}`);
46
- if (instructions) console.log(instructions);
47
- },
48
- onDeviceCode: ({ userCode, verificationUri }) => {
49
- console.log(`Open: ${verificationUri}`);
50
- console.log(`Enter code: ${userCode}`);
51
- },
52
- onPrompt: (prompt) => ask(rl, formatPrompt(prompt)),
53
- onProgress: (message) => console.log(message),
54
- onSelect: (prompt) => selectOption(rl, prompt),
55
- };
56
- }
57
-
58
- export function isOAuthProvider(provider: string): boolean {
59
- return getOAuthProviders().some(
60
- (oauthProvider) => oauthProvider.id === provider,
61
- );
62
- }
63
-
64
- export async function getAvailableProviders(): Promise<string[]> {
65
- const auth = await readCreds();
66
- const loggedInOAuthProviders = getOAuthProviders()
67
- .map((provider) => provider.id)
68
- .filter((provider) => auth[provider]);
69
- const envKeyProviders = getProviders().filter(
70
- (provider) => !!getEnvApiKey(provider),
71
- );
72
- const providers: string[] = [];
73
- const providerIds = new Set<string>();
74
-
75
- for (const provider of [...loggedInOAuthProviders, ...envKeyProviders]) {
76
- if (providerIds.has(provider)) continue;
77
-
78
- providers.push(provider);
79
- providerIds.add(provider);
80
- }
81
-
82
- return providers;
83
- }
84
-
85
- export async function loginOAuth(provider: OAuthProviderId) {
86
- const oauthProvider = getOAuthProvider(provider);
87
- if (!oauthProvider) throw new Error(`Unknown OAuth provider: ${provider}`);
88
-
89
- const rl = readline.createInterface({
90
- input: process.stdin,
91
- output: process.stdout,
92
- });
93
-
94
- try {
95
- const creds = await oauthProvider.login(createLoginCallbacks(rl));
96
-
97
- await writeCreds({ [provider]: { type: "oauth", ...creds } });
98
- } finally {
99
- rl.close();
100
- }
101
-
102
- return await readCreds();
103
- }
104
-
105
- export async function getApiKey(options: CliOptions) {
106
- const provider = options.model.provider;
107
- const auth = await readCreds();
108
-
109
- if (isOAuthProvider(provider)) {
110
- const result = await getOAuthApiKey(provider, auth);
111
- if (result) {
112
- auth[provider] = { type: "oauth", ...result.newCredentials };
113
- await writeCreds(auth);
114
-
115
- return result.apiKey;
116
- }
117
- }
118
-
119
- const envApiKey = getEnvApiKey(provider);
120
- if (envApiKey) return envApiKey;
121
-
122
- const knownProviders = getProviders() as string[];
123
- if (!knownProviders.includes(provider)) {
124
- if (options.model.api === "openai-completions") {
125
- // pi-ai requires a truthy apiKey for OpenAI-compatible local providers like Ollama.
126
- return "dummy";
127
- }
128
-
129
- return undefined;
130
- }
131
-
132
- throw new Error("Not logged in");
133
- }
134
-
135
- export async function readCreds(): Promise<SavedOAuthCreds> {
136
- const file = Bun.file(AUTH_FILE);
137
- if (await file.exists()) {
138
- try {
139
- return JSON.parse(await file.text());
140
- } catch (err) {
141
- const message = err instanceof Error ? err.message : String(err);
142
- throw new Error(`Invalid auth JSON: ${message}`);
143
- }
144
- }
145
-
146
- return {};
147
- }
148
-
149
- async function writeCreds(creds: SavedOAuthCreds) {
150
- await mkdir(DATA_DIR, { recursive: true });
151
- await Bun.write(AUTH_FILE, JSON.stringify(await mergeCreds(creds)));
152
- }
153
-
154
- async function mergeCreds(newCreds: SavedOAuthCreds) {
155
- const oldCreds = await readCreds();
156
- return { ...oldCreds, ...newCreds };
157
- }
package/src/shared.ts DELETED
@@ -1,119 +0,0 @@
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 =
8
- Bun.env.MINI_CODER_DATA_DIR ?? join(homedir(), ".config", "mini-coder");
9
- export const SESSIONS_DIR = join(DATA_DIR, "sessions");
10
- export const AUTH_PATH = join(DATA_DIR, "auth.json");
11
- export const SETTINGS_PATH = join(DATA_DIR, "settings.json");
12
-
13
- export function secureRandomString(
14
- length: number,
15
- chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
16
- ): string {
17
- const result: string[] = [];
18
- const charsLength = chars.length;
19
- const maxValid = Math.floor(256 / charsLength) * charsLength;
20
- const randomBytes = new Uint8Array(length * 2);
21
-
22
- while (result.length < length) {
23
- crypto.getRandomValues(randomBytes);
24
-
25
- for (const byte of randomBytes) {
26
- if (byte < maxValid) {
27
- result.push(chars[byte % charsLength]);
28
- if (result.length === length) break;
29
- }
30
- }
31
- }
32
-
33
- return result.join("");
34
- }
35
-
36
- export function elapsedTime(seconds: number): string {
37
- if (seconds < 60) return `${seconds}s`;
38
-
39
- const minutes = Math.floor(seconds / 60);
40
- if (minutes < 60) return `${minutes}m`;
41
-
42
- const hours = Math.floor(minutes / 60);
43
- if (hours < 24) return `${hours}h`;
44
-
45
- const days = Math.floor(hours / 24);
46
- if (days < 7) return `${days}d`;
47
-
48
- const weeks = Math.floor(days / 7);
49
- if (weeks < 4) return `${weeks}w`;
50
-
51
- const months = Math.floor(days / 30);
52
- if (months < 12) return `${months}mo`;
53
-
54
- const years = Math.floor(days / 365);
55
- return `${years}y`;
56
- }
57
-
58
- export function relativeTime(timestamp: number): string {
59
- const seconds = Math.floor((Date.now() - timestamp) / 1000);
60
- return elapsedTime(seconds);
61
- }
62
-
63
- export function onceEvery<T extends unknown[]>(
64
- n: number,
65
- fn: (...args: T) => void,
66
- ) {
67
- let calls = 0;
68
-
69
- return (...args: T) => {
70
- calls++;
71
-
72
- if (calls % n === 0) {
73
- fn(...args);
74
- }
75
- };
76
- }
77
-
78
- export function takeTail<T>(arr: T[], x: number): T[] {
79
- return x <= 0 ? [] : arr.slice(-x);
80
- }
81
-
82
- export function estimateTokens(text: string): number {
83
- return Math.ceil(text.length / 4);
84
- }
85
-
86
- export function parseSkillFrontmatter(content: string) {
87
- const match = /^---\s*\n([\s\S]*?)\n---/.exec(content);
88
-
89
- if (!match) {
90
- return undefined;
91
- }
92
-
93
- const doc = parseDocument(match[1]);
94
- const data = doc.toJS() as unknown;
95
-
96
- if (!data || typeof data !== "object") {
97
- return undefined;
98
- }
99
-
100
- const record = data as Record<string, unknown>;
101
- const name = typeof record.name === "string" ? record.name.trim() : "";
102
- const description =
103
- typeof record.description === "string" ? record.description.trim() : "";
104
-
105
- if (!name || !description) {
106
- return undefined;
107
- }
108
-
109
- return { name, description };
110
- }
111
-
112
- export function formatTimestamp(timestampMs: number): string {
113
- return new Date(timestampMs).toLocaleTimeString("en-GB", {
114
- hour: "2-digit",
115
- minute: "2-digit",
116
- second: "2-digit",
117
- hour12: false,
118
- });
119
- }
package/src/themes.ts DELETED
@@ -1,234 +0,0 @@
1
- import type { SyntaxHighlightTheme } from "@cel-tui/components";
2
- import type { Color, Theme } from "@cel-tui/core";
3
-
4
- export const TUI_THEME_IDS = [
5
- "ansi16",
6
- "molokai-dark",
7
- "molokai-light",
8
- ] as const;
9
-
10
- export type TUIThemeId = (typeof TUI_THEME_IDS)[number];
11
-
12
- export interface TUIThemeDefinition {
13
- id: TUIThemeId;
14
- label: string;
15
- palette: Theme;
16
- syntax: SyntaxHighlightTheme;
17
- rootFgColor?: Color;
18
- rootBgColor?: Color;
19
- userMessageBgColor?: Color;
20
- }
21
-
22
- const ANSI_SLOT_HEX: Record<Color, string> = {
23
- color00: "#000000",
24
- color01: "#cd3131",
25
- color02: "#0dbc79",
26
- color03: "#e5e510",
27
- color04: "#2472c8",
28
- color05: "#bc3fbc",
29
- color06: "#11a8cd",
30
- color07: "#e5e5e5",
31
- color08: "#666666",
32
- color09: "#f14c4c",
33
- color10: "#23d18b",
34
- color11: "#f5f543",
35
- color12: "#3b8eea",
36
- color13: "#d670d6",
37
- color14: "#29b8db",
38
- color15: "#ffffff",
39
- };
40
-
41
- export const theme = {
42
- black: "color00",
43
- red: "color01",
44
- green: "color02",
45
- yellow: "color03",
46
- blue: "color04",
47
- magenta: "color05",
48
- cyan: "color06",
49
- white: "color07",
50
- bblack: "color08",
51
- bred: "color09",
52
- bgreen: "color10",
53
- byellow: "color11",
54
- bblue: "color12",
55
- bmagenta: "color13",
56
- bcyan: "color14",
57
- bwhite: "color15",
58
- } as const satisfies Record<string, Color>;
59
-
60
- const ansi16Palette: Theme = {
61
- color00: 0,
62
- color01: 1,
63
- color02: 2,
64
- color03: 3,
65
- color04: 4,
66
- color05: 5,
67
- color06: 6,
68
- color07: 7,
69
- color08: 8,
70
- color09: 9,
71
- color10: 10,
72
- color11: 11,
73
- color12: 12,
74
- color13: 13,
75
- color14: 14,
76
- color15: 15,
77
- };
78
-
79
- const molokaiDarkPalette: Theme = {
80
- color00: "#272822",
81
- color01: "#f92672",
82
- color02: "#a6e22e",
83
- color03: "#e6db74",
84
- color04: "#66d9ef",
85
- color05: "#f92672",
86
- color06: "#66d9ef",
87
- color07: "#f8f8f2",
88
- color08: "#6f705f",
89
- color09: "#ff6188",
90
- color10: "#a6e22e",
91
- color11: "#ffd866",
92
- color12: "#78dce8",
93
- color13: "#ae81ff",
94
- color14: "#66d9ef",
95
- color15: "#ffffff",
96
- };
97
-
98
- const molokaiLightPalette: Theme = {
99
- color00: "#272822",
100
- color01: "#ff5f87",
101
- color02: "#8bcf26",
102
- color03: "#c7a100",
103
- color04: "#61aeee",
104
- color05: "#d16dff",
105
- color06: "#00a8b5",
106
- color07: "#f2efe4",
107
- color08: "#5f6060",
108
- color09: "#b0003a",
109
- color10: "#3f7d00",
110
- color11: "#725f00",
111
- color12: "#005f9f",
112
- color13: "#7f2caf",
113
- color14: "#007885",
114
- color15: "#fffdf5",
115
- };
116
-
117
- const slotColor = (slot: Color) => ANSI_SLOT_HEX[slot];
118
-
119
- function syntaxScope(
120
- scope: string | readonly string[],
121
- foreground: Color,
122
- fontStyle?: string,
123
- ) {
124
- return {
125
- scope,
126
- settings: {
127
- foreground: slotColor(foreground),
128
- ...(fontStyle ? { fontStyle } : {}),
129
- },
130
- };
131
- }
132
-
133
- const molokaiDarkSyntax = {
134
- name: "mini-coder-molokai-dark",
135
- type: "dark",
136
- fg: slotColor("color07"),
137
- tokenColors: [
138
- syntaxScope(["comment", "markup.quote"], "color08", "italic"),
139
- syntaxScope(["keyword", "operator"], "color05"),
140
- syntaxScope(["string", "escape", "markup.list"], "color03"),
141
- syntaxScope(["number", "regexp"], "color13"),
142
- syntaxScope(["function", "command", "markup.code"], "color10"),
143
- syntaxScope(["builtin", "property", "type", "meta"], "color06"),
144
- syntaxScope("markup.heading", "color04", "bold"),
145
- syntaxScope(["diff.deleted", "diff.file.old"], "color09"),
146
- syntaxScope(["diff.inserted", "diff.file.new"], "color10"),
147
- syntaxScope("diff.hunk", "color13"),
148
- syntaxScope(["diff.header", "diff.no-newline"], "color08"),
149
- ],
150
- } as const satisfies SyntaxHighlightTheme;
151
-
152
- const molokaiLightSyntax = {
153
- name: "mini-coder-molokai-light",
154
- type: "light",
155
- fg: slotColor("color00"),
156
- tokenColors: [
157
- syntaxScope(["comment", "markup.quote"], "color08", "italic"),
158
- syntaxScope(["keyword", "operator"], "color13"),
159
- syntaxScope(["string", "escape", "markup.list"], "color11"),
160
- syntaxScope(["number", "regexp"], "color09"),
161
- syntaxScope(["function", "command", "markup.code"], "color10"),
162
- syntaxScope(["builtin", "property", "type", "meta"], "color14"),
163
- syntaxScope("markup.heading", "color12", "bold"),
164
- syntaxScope(["diff.deleted", "diff.file.old"], "color09"),
165
- syntaxScope(["diff.inserted", "diff.file.new"], "color10"),
166
- syntaxScope("diff.hunk", "color13"),
167
- syntaxScope(["diff.header", "diff.no-newline"], "color08"),
168
- ],
169
- } as const satisfies SyntaxHighlightTheme;
170
-
171
- export const DEFAULT_TUI_THEME_ID: TUIThemeId = "ansi16";
172
-
173
- export const TUI_THEMES = {
174
- ansi16: {
175
- id: "ansi16",
176
- label: "ansi16",
177
- palette: ansi16Palette,
178
- syntax: "default",
179
- userMessageBgColor: theme.bblack,
180
- },
181
- "molokai-dark": {
182
- id: "molokai-dark",
183
- label: "molokai dark",
184
- palette: molokaiDarkPalette,
185
- syntax: molokaiDarkSyntax,
186
- rootFgColor: theme.bwhite,
187
- rootBgColor: theme.black,
188
- userMessageBgColor: theme.bblack,
189
- },
190
- "molokai-light": {
191
- id: "molokai-light",
192
- label: "molokai light",
193
- palette: molokaiLightPalette,
194
- syntax: molokaiLightSyntax,
195
- rootFgColor: theme.black,
196
- rootBgColor: theme.bwhite,
197
- userMessageBgColor: theme.white,
198
- },
199
- } as const satisfies Record<TUIThemeId, TUIThemeDefinition>;
200
-
201
- export const activeTuiTheme: Theme = { ...ansi16Palette };
202
-
203
- export function applyTUITheme(id: TUIThemeId): void {
204
- Object.assign(activeTuiTheme, TUI_THEMES[id].palette);
205
- }
206
-
207
- export function getTUITheme(id: TUIThemeId): TUIThemeDefinition {
208
- return TUI_THEMES[id];
209
- }
210
-
211
- export function textColorForBackground(
212
- bgColor: Color,
213
- themeId: TUIThemeId,
214
- ): Color {
215
- if (bgColor === theme.bblack) return theme.bwhite;
216
-
217
- if (
218
- themeId === "molokai-light" &&
219
- (
220
- [
221
- theme.bred,
222
- theme.bgreen,
223
- theme.byellow,
224
- theme.bblue,
225
- theme.bmagenta,
226
- theme.bcyan,
227
- ] as readonly Color[]
228
- ).includes(bgColor)
229
- ) {
230
- return theme.bwhite;
231
- }
232
-
233
- return theme.black;
234
- }
package/src/tool-bash.ts DELETED
@@ -1,77 +0,0 @@
1
- import { type Tool, Type } from "@earendil-works/pi-ai";
2
- import type { ToolRunnerEvent } from "./types";
3
-
4
- const description = `Bash CLI tool
5
-
6
- Execute shell commands on the user's environment.
7
-
8
- - Chain commands **only** when failure should stop the flow. Avoid long chains, **2 to 3 maximum**.
9
- - Avoid overly complex one-liners; readability matters.
10
- - Quote filenames: use \`"$file"\` not \`$file\`.
11
- - Be careful with spaces in filenames.
12
-
13
- Commands run in: ${process.cwd()}
14
- `;
15
-
16
- export const bash: Tool = {
17
- name: "bash",
18
- description,
19
- parameters: Type.Object({
20
- command: Type.String({
21
- description:
22
- "Shell command to execute. Prefer simple, focused commands over complex one-liners.",
23
- }),
24
- }),
25
- };
26
-
27
- export async function* runBashTool(
28
- args: Record<string, any>,
29
- signal?: AbortSignal,
30
- ): AsyncGenerator<ToolRunnerEvent> {
31
- // Redirect stderr into stdout for the whole shell session.
32
- const proc = Bun.spawn(["bash", "-c", `exec 2>&1; ${args.command}`], {
33
- stdout: "pipe",
34
- stderr: "pipe",
35
- env: {
36
- ...Bun.env,
37
- NO_COLOR: "1",
38
- },
39
- signal,
40
- });
41
-
42
- const decoder = new TextDecoder();
43
- const reader = proc.stdout.getReader();
44
-
45
- let output = "";
46
- while (true) {
47
- const { done, value } = await reader.read();
48
-
49
- if (done) {
50
- const remaining = Bun.stripANSI(decoder.decode());
51
-
52
- if (remaining.length) {
53
- output += remaining;
54
- yield { type: "output", text: remaining };
55
- }
56
- break;
57
- }
58
-
59
- const text = Bun.stripANSI(decoder.decode(value, { stream: true }));
60
-
61
- if (text.length) {
62
- output += text;
63
- yield { type: "output", text: text };
64
- }
65
- }
66
-
67
- const exitCode = await proc.exited;
68
-
69
- const result = `${output.length ? output : "(no output)"}\n\nExit code: ${exitCode}`;
70
-
71
- yield {
72
- type: "result",
73
- text: result,
74
- };
75
-
76
- return result;
77
- }
package/src/tool-edit.ts DELETED
@@ -1,121 +0,0 @@
1
- import { isAbsolute, join } from "node:path";
2
- import { type Tool, Type } from "@earendil-works/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
-
12
- - The tool refuses to edit on multiple matches of \`oldText\`. Be specific with your matching text.
13
- - Prefer patch-based edits (small targeted replacements) for multi-line or semantic changes.
14
- - Do NOT reproduce entire files. Use shell file operations (\`cp\`, \`mv\`, etc) for wholesale file replacement instead.
15
-
16
- Failure modes
17
-
18
- - If \`oldText\` is not found, the edit fails. Verify the exact text first.
19
- - If \`oldText\` matches multiple locations, the edit fails. Narrow your match and retry.
20
- - If the file does not exist and \`oldText\` is non-empty, the edit fails.
21
- `;
22
-
23
- export const edit: Tool = {
24
- name: `edit`,
25
- description,
26
- parameters: Type.Object({
27
- path: Type.String({
28
- description:
29
- "File path. Absolute or relative to the current working directory.",
30
- }),
31
- oldText: Type.String({
32
- description:
33
- 'Exact text to find and replace. Empty string means "create new file".',
34
- }),
35
- newText: Type.String({
36
- description: "Replacement text (or full content for new files)",
37
- }),
38
- }),
39
- };
40
-
41
- function findAllIndexes(text: string, sub: string): number[] {
42
- if (sub.length === 0) return [];
43
-
44
- const indexes: number[] = [];
45
-
46
- let pos = text.indexOf(sub, 0);
47
- while (pos !== -1) {
48
- indexes.push(pos);
49
- pos += sub.length; // use pos += 1 if you want overlapping matches
50
- pos = text.indexOf(sub, pos);
51
- }
52
-
53
- return indexes;
54
- }
55
-
56
- export async function* runEditTool(
57
- args: Record<string, any>,
58
- signal?: AbortSignal,
59
- ): AsyncGenerator<ToolRunnerEvent> {
60
- const filePath = isAbsolute(args.path)
61
- ? args.path
62
- : join(process.cwd(), args.path);
63
- const file = Bun.file(filePath);
64
- const exists = await file.exists();
65
-
66
- if (args.oldText === "") {
67
- if (exists) {
68
- yield { type: "result", text: `File already exists: ${filePath}` };
69
- return;
70
- }
71
-
72
- await Bun.write(file, args.newText);
73
- yield {
74
- type: "result",
75
- text: `File written: ${filePath}\n\n${args.newText}`,
76
- };
77
- return;
78
- }
79
-
80
- if (!exists) {
81
- yield { type: "result", text: `File not found: ${filePath}` };
82
- return;
83
- }
84
-
85
- const content = await file.text();
86
- const matches = findAllIndexes(content, args.oldText);
87
-
88
- if (matches.length === 0) {
89
- yield { type: "result", text: `Old text not found in: ${filePath}` };
90
- return;
91
- }
92
-
93
- if (matches.length > 1) {
94
- yield {
95
- type: "result",
96
- text: `Multiple matches found in ${filePath}: ${matches.length} matches, be more specific and try again`,
97
- };
98
- return;
99
- }
100
-
101
- const idx = matches[0];
102
- const updated =
103
- content.slice(0, idx) +
104
- args.newText +
105
- content.slice(idx + args.oldText.length);
106
-
107
- if (signal?.aborted) {
108
- yield { type: "result", text: "Aborted before write." };
109
- return;
110
- }
111
-
112
- await file.write(updated);
113
- const patch = createPatch(filePath, content, updated);
114
-
115
- yield {
116
- type: "result",
117
- text: patch,
118
- };
119
-
120
- return;
121
- }