mini-coder 0.5.1 → 0.5.3
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/bun.lock +39 -40
- package/package.json +10 -11
- package/src/agent.ts +38 -15
- package/src/errors.ts +15 -0
- package/src/git.ts +95 -19
- package/src/index.ts +125 -6
- package/src/prompt.ts +7 -4
- package/src/settings.ts +66 -4
- package/src/submit.ts +4 -6
- package/src/tools.ts +671 -6
- package/src/ui/agent.ts +1 -4
- package/src/ui/commands.test.ts +3 -0
- package/src/ui/commands.ts +1 -4
- package/src/ui/conversation.test.ts +214 -15
- package/src/ui/conversation.ts +269 -37
- package/src/ui/input.test.ts +5 -1
- package/src/ui/input.ts +7 -1
- package/src/ui/status.test.ts +3 -0
- package/src/ui.ts +6 -0
- package/src/version.ts +48 -0
package/src/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
resolveHeadlessPrompt,
|
|
29
29
|
shouldUseHeadlessMode,
|
|
30
30
|
} from "./cli.ts";
|
|
31
|
+
import { getErrorMessage } from "./errors.ts";
|
|
31
32
|
import { type GitState, getGitState } from "./git.ts";
|
|
32
33
|
import { canonicalizePath } from "./paths.ts";
|
|
33
34
|
import {
|
|
@@ -56,6 +57,7 @@ import {
|
|
|
56
57
|
truncateSessions,
|
|
57
58
|
} from "./session.ts";
|
|
58
59
|
import {
|
|
60
|
+
type CustomProvider,
|
|
59
61
|
loadSettings,
|
|
60
62
|
resolveStartupSettings,
|
|
61
63
|
type UserSettings,
|
|
@@ -70,6 +72,7 @@ import {
|
|
|
70
72
|
readImageTool,
|
|
71
73
|
shellTool,
|
|
72
74
|
} from "./tools.ts";
|
|
75
|
+
import { resolveAppVersionLabel } from "./version.ts";
|
|
73
76
|
|
|
74
77
|
// ---------------------------------------------------------------------------
|
|
75
78
|
// Constants
|
|
@@ -98,10 +101,6 @@ export const MAX_SESSIONS_PER_CWD = 20;
|
|
|
98
101
|
/** Maximum raw prompt-history entries to retain globally. */
|
|
99
102
|
export const MAX_PROMPT_HISTORY = 1_000;
|
|
100
103
|
|
|
101
|
-
function getErrorMessage(error: unknown): string {
|
|
102
|
-
return error instanceof Error ? error.message : String(error);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
104
|
// ---------------------------------------------------------------------------
|
|
106
105
|
// OAuth credential persistence
|
|
107
106
|
// ---------------------------------------------------------------------------
|
|
@@ -211,6 +210,101 @@ async function discoverProviders(): Promise<DiscoveryResult> {
|
|
|
211
210
|
return { providers, oauthCredentials };
|
|
212
211
|
}
|
|
213
212
|
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Custom provider discovery
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
/** Timeout for custom provider model discovery requests. */
|
|
218
|
+
const CUSTOM_PROVIDER_TIMEOUT_MS = 3_000;
|
|
219
|
+
|
|
220
|
+
/** Default API key for custom providers that don't require authentication. */
|
|
221
|
+
const CUSTOM_PROVIDER_DEFAULT_KEY = "no-key";
|
|
222
|
+
|
|
223
|
+
/** Result of custom provider discovery. */
|
|
224
|
+
interface CustomDiscoveryResult {
|
|
225
|
+
/** Discovered models from all reachable custom providers. */
|
|
226
|
+
models: Model<"openai-completions">[];
|
|
227
|
+
/** Provider name → API key for discovered providers. */
|
|
228
|
+
providers: Map<string, string>;
|
|
229
|
+
/** Warning messages for unreachable or invalid providers. */
|
|
230
|
+
warnings: string[];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Discover models from user-configured OpenAI-compatible endpoints.
|
|
235
|
+
*
|
|
236
|
+
* Queries each provider's `/models` endpoint and constructs pi-ai Model
|
|
237
|
+
* objects from the response. Unreachable endpoints produce a warning
|
|
238
|
+
* instead of failing startup.
|
|
239
|
+
*
|
|
240
|
+
* @param customProviders - Configured custom provider entries.
|
|
241
|
+
* @param builtInProviderNames - Names of built-in providers (to detect collisions).
|
|
242
|
+
* @returns Discovered models, provider credentials, and warnings.
|
|
243
|
+
*/
|
|
244
|
+
export async function discoverCustomProviders(
|
|
245
|
+
customProviders: readonly CustomProvider[],
|
|
246
|
+
builtInProviderNames: ReadonlySet<string>,
|
|
247
|
+
): Promise<CustomDiscoveryResult> {
|
|
248
|
+
const models: Model<"openai-completions">[] = [];
|
|
249
|
+
const providers = new Map<string, string>();
|
|
250
|
+
const warnings: string[] = [];
|
|
251
|
+
|
|
252
|
+
for (const entry of customProviders) {
|
|
253
|
+
if (builtInProviderNames.has(entry.name)) {
|
|
254
|
+
warnings.push(
|
|
255
|
+
`Custom provider "${entry.name}" skipped: name conflicts with a built-in provider.`,
|
|
256
|
+
);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const apiKey = entry.apiKey ?? CUSTOM_PROVIDER_DEFAULT_KEY;
|
|
261
|
+
const modelsUrl = `${entry.baseUrl}/models`;
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const response = await fetch(modelsUrl, {
|
|
265
|
+
signal: AbortSignal.timeout(CUSTOM_PROVIDER_TIMEOUT_MS),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
if (!response.ok) {
|
|
269
|
+
warnings.push(
|
|
270
|
+
`Custom provider "${entry.name}": ${response.status} ${response.statusText} (${modelsUrl})`,
|
|
271
|
+
);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const body = (await response.json()) as {
|
|
276
|
+
data?: { id: string }[];
|
|
277
|
+
};
|
|
278
|
+
const modelList = body.data ?? [];
|
|
279
|
+
|
|
280
|
+
for (const item of modelList) {
|
|
281
|
+
if (typeof item.id !== "string" || !item.id) continue;
|
|
282
|
+
|
|
283
|
+
models.push({
|
|
284
|
+
id: item.id,
|
|
285
|
+
name: item.id,
|
|
286
|
+
api: "openai-completions",
|
|
287
|
+
provider: entry.name,
|
|
288
|
+
baseUrl: entry.baseUrl,
|
|
289
|
+
reasoning: false,
|
|
290
|
+
input: ["text"],
|
|
291
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
292
|
+
contextWindow: 131072,
|
|
293
|
+
maxTokens: 8192,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
providers.set(entry.name, apiKey);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
warnings.push(
|
|
300
|
+
`Custom provider "${entry.name}": ${getErrorMessage(error)} (${modelsUrl})`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return { models, providers, warnings };
|
|
306
|
+
}
|
|
307
|
+
|
|
214
308
|
// ---------------------------------------------------------------------------
|
|
215
309
|
// Model selection
|
|
216
310
|
// ---------------------------------------------------------------------------
|
|
@@ -440,8 +534,12 @@ export interface AppState {
|
|
|
440
534
|
plugins: LoadedPlugin[];
|
|
441
535
|
/** Active theme (default + plugin overrides). */
|
|
442
536
|
theme: Theme;
|
|
537
|
+
/** Version label shown in the empty conversation banner. */
|
|
538
|
+
versionLabel: string;
|
|
443
539
|
/** Current git state (null if not in a repo). */
|
|
444
540
|
git: GitState | null;
|
|
541
|
+
/** Optional git state loader override used by tests. */
|
|
542
|
+
loadGitState?: (cwd: string) => Promise<GitState | null>;
|
|
445
543
|
/** Available provider credentials (provider → API key). */
|
|
446
544
|
providers: Map<string, string>;
|
|
447
545
|
/** OAuth credentials on disk. */
|
|
@@ -464,6 +562,10 @@ export interface AppState {
|
|
|
464
562
|
showReasoning: boolean;
|
|
465
563
|
/** Whether to show full (un-truncated) tool output. */
|
|
466
564
|
verbose: boolean;
|
|
565
|
+
/** Models discovered from custom OpenAI-compatible providers. */
|
|
566
|
+
customModels: Model<string>[];
|
|
567
|
+
/** Warnings from startup (e.g. unreachable custom providers). */
|
|
568
|
+
startupWarnings: string[];
|
|
467
569
|
}
|
|
468
570
|
|
|
469
571
|
// ---------------------------------------------------------------------------
|
|
@@ -482,7 +584,21 @@ export async function init(): Promise<AppState> {
|
|
|
482
584
|
|
|
483
585
|
// Load user settings and resolve startup defaults
|
|
484
586
|
const settings = loadSettings(SETTINGS_PATH);
|
|
485
|
-
|
|
587
|
+
|
|
588
|
+
// Discover custom providers from settings
|
|
589
|
+
const builtInProviderNames = new Set(providers.keys());
|
|
590
|
+
const customResult = await discoverCustomProviders(
|
|
591
|
+
settings.customProviders ?? [],
|
|
592
|
+
builtInProviderNames,
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
// Merge custom provider credentials
|
|
596
|
+
for (const [name, key] of customResult.providers) {
|
|
597
|
+
providers.set(name, key);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const builtInModels = listAvailableModels(providers);
|
|
601
|
+
const availableModels = [...builtInModels, ...customResult.models];
|
|
486
602
|
const startup = resolveStartupSettings(
|
|
487
603
|
settings,
|
|
488
604
|
availableModels.map((model) => `${model.provider}/${model.id}`),
|
|
@@ -509,6 +625,7 @@ export async function init(): Promise<AppState> {
|
|
|
509
625
|
skills: promptContext.skills,
|
|
510
626
|
plugins: promptContext.plugins,
|
|
511
627
|
theme: promptContext.theme,
|
|
628
|
+
versionLabel: resolveAppVersionLabel(),
|
|
512
629
|
git: promptContext.git,
|
|
513
630
|
providers,
|
|
514
631
|
oauthCredentials,
|
|
@@ -521,6 +638,8 @@ export async function init(): Promise<AppState> {
|
|
|
521
638
|
activeTurnPromise: null,
|
|
522
639
|
showReasoning: startup.showReasoning,
|
|
523
640
|
verbose: startup.verbose,
|
|
641
|
+
customModels: customResult.models,
|
|
642
|
+
startupWarnings: customResult.warnings,
|
|
524
643
|
};
|
|
525
644
|
}
|
|
526
645
|
|
|
@@ -593,7 +712,7 @@ export function ensureSession(
|
|
|
593
712
|
* for, suitable for the `/model` selector.
|
|
594
713
|
*/
|
|
595
714
|
export function getAvailableModels(state: AppState): Model<string>[] {
|
|
596
|
-
return listAvailableModels(state.providers);
|
|
715
|
+
return [...listAvailableModels(state.providers), ...state.customModels];
|
|
597
716
|
}
|
|
598
717
|
|
|
599
718
|
/** Clean up resources on shutdown. */
|
package/src/prompt.ts
CHANGED
|
@@ -192,14 +192,14 @@ const BASE_INSTRUCTIONS = `You are mini-coder, a coding agent running in the use
|
|
|
192
192
|
|
|
193
193
|
# Role
|
|
194
194
|
|
|
195
|
-
You are an autonomous, senior-level coding assistant. When the user gives a direction, proactively gather context, plan with the user, implement, and verify. Bias toward action:
|
|
195
|
+
You are an autonomous, senior-level coding assistant. When the user gives a direction, proactively gather context, plan with the user, implement, and verify. Bias toward action: plan briefly when needed to clear important assumptions, then continue into implementation. First identify the task contract: required files, names, interfaces, output format, and checks for success. Treat those details as part of correctness, not as polish. Deliver working code, unless you are genuinely blocked.
|
|
196
196
|
|
|
197
197
|
# Tools
|
|
198
198
|
|
|
199
199
|
You have these core tools:
|
|
200
200
|
|
|
201
|
-
- \`shell\` — run commands in the user's shell. Use this to explore the codebase
|
|
202
|
-
- \`edit\` — make exact-text replacements in files. Provide the file path, the exact text to find, and the replacement text. The old text must match exactly one location in the file. To create a new file, use an empty old text and the full file content as new text.
|
|
201
|
+
- \`shell\` — run commands in the user's shell. Use this to explore the codebase, read tests/verifiers/examples, inspect required outputs, and run targeted checks, builds, or git commands. Prefer \`rg\` over \`grep\` for speed.
|
|
202
|
+
- \`edit\` — make exact-text replacements in files. Provide the file path, the exact text to find, and the replacement text. The old text must match exactly one location in the file. To create a new file, use an empty old text and the full file content as new text. Use this to write the exact final file content the task requires.
|
|
203
203
|
|
|
204
204
|
You may also have additional tools provided by plugins. Use them when they match the task.
|
|
205
205
|
|
|
@@ -208,7 +208,7 @@ Workflow: **inspect with shell → mutate with edit → verify with shell**.
|
|
|
208
208
|
# Code quality
|
|
209
209
|
|
|
210
210
|
- Conform to the codebase's existing conventions: patterns, naming, formatting, language idioms.
|
|
211
|
-
- Write correct, clear, minimal code. Don't over-engineer, don't add abstractions for hypothetical futures.
|
|
211
|
+
- Write correct, clear, minimal code. Prefer the simplest solution that satisfies the task's checks exactly. Don't over-engineer, don't add abstractions for hypothetical futures.
|
|
212
212
|
- Reuse before creating. Search for existing helpers before writing new ones.
|
|
213
213
|
- Tight error handling: no broad try/catch, no silent failures, no swallowed errors.
|
|
214
214
|
- Keep type safety. Avoid \`any\` casts. Use proper types and guards.
|
|
@@ -224,6 +224,7 @@ Workflow: **inspect with shell → mutate with edit → verify with shell**.
|
|
|
224
224
|
# Exploring the codebase
|
|
225
225
|
|
|
226
226
|
- Think first: before any tool call, decide all files and information you need.
|
|
227
|
+
- Early in the task, look for acceptance criteria in tests, verifier scripts, eval scripts, examples, and expected-output files. Do not rely on the task text alone when machine-checkable criteria are available.
|
|
227
228
|
- Batch reads: if you need multiple files, read them together in parallel rather than one at a time.
|
|
228
229
|
- Only make sequential calls when a later call genuinely depends on an earlier result.
|
|
229
230
|
|
|
@@ -238,7 +239,9 @@ Workflow: **inspect with shell → mutate with edit → verify with shell**.
|
|
|
238
239
|
# Persistence
|
|
239
240
|
|
|
240
241
|
- Carry work through to completion within the current turn. Don't stop at analysis or partial fixes.
|
|
242
|
+
- Once the contract is clear, create the required artifact early, then iterate and improve it. Do not spend most of the turn exploring.
|
|
241
243
|
- If you encounter an error, diagnose and fix it rather than reporting it and stopping.
|
|
244
|
+
- Before concluding, run the smallest targeted verification that checks the exact contract: required files exist, names and signatures match, outputs are in the required format, and no forbidden extra artifacts were left behind.
|
|
242
245
|
- Avoid excessive looping: if you're re-reading or re-editing the same files without progress, stop and ask the user.`;
|
|
243
246
|
|
|
244
247
|
// ---------------------------------------------------------------------------
|
package/src/settings.ts
CHANGED
|
@@ -10,6 +10,17 @@
|
|
|
10
10
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { dirname } from "node:path";
|
|
12
12
|
import type { ThinkingLevel } from "@mariozechner/pi-ai";
|
|
13
|
+
import { getErrorMessage } from "./errors.ts";
|
|
14
|
+
|
|
15
|
+
/** A user-configured OpenAI-compatible provider endpoint. */
|
|
16
|
+
export interface CustomProvider {
|
|
17
|
+
/** Provider identifier, e.g. "ollama". Shown as the provider prefix in model names. */
|
|
18
|
+
name: string;
|
|
19
|
+
/** OpenAI-compatible API base URL, e.g. "http://localhost:11434/v1". */
|
|
20
|
+
baseUrl: string;
|
|
21
|
+
/** Optional API key. Defaults to "no-key" at discovery time. */
|
|
22
|
+
apiKey?: string;
|
|
23
|
+
}
|
|
13
24
|
|
|
14
25
|
/** Default reasoning effort when no saved setting exists. */
|
|
15
26
|
const DEFAULT_EFFORT: ThinkingLevel = "medium";
|
|
@@ -30,6 +41,8 @@ export interface UserSettings {
|
|
|
30
41
|
showReasoning?: boolean;
|
|
31
42
|
/** Whether full tool output is shown in the UI. */
|
|
32
43
|
verbose?: boolean;
|
|
44
|
+
/** Custom OpenAI-compatible provider endpoints. */
|
|
45
|
+
customProviders?: CustomProvider[];
|
|
33
46
|
}
|
|
34
47
|
|
|
35
48
|
/** Resolved startup settings after applying defaults and availability checks. */
|
|
@@ -51,10 +64,6 @@ const THINKING_LEVELS = new Set<ThinkingLevel>([
|
|
|
51
64
|
"xhigh",
|
|
52
65
|
]);
|
|
53
66
|
|
|
54
|
-
function getErrorMessage(error: unknown): string {
|
|
55
|
-
return error instanceof Error ? error.message : String(error);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
67
|
/**
|
|
59
68
|
* Load and validate user settings from disk.
|
|
60
69
|
*
|
|
@@ -175,9 +184,62 @@ function sanitizeSettings(value: unknown): UserSettings {
|
|
|
175
184
|
settings.verbose = candidate.verbose;
|
|
176
185
|
}
|
|
177
186
|
|
|
187
|
+
const customProviders = sanitizeCustomProviders(candidate.customProviders);
|
|
188
|
+
if (customProviders) {
|
|
189
|
+
settings.customProviders = customProviders;
|
|
190
|
+
}
|
|
191
|
+
|
|
178
192
|
return settings;
|
|
179
193
|
}
|
|
180
194
|
|
|
195
|
+
/** Try to parse a single custom provider entry, returning null on failure. */
|
|
196
|
+
function parseCustomProvider(item: unknown): CustomProvider | null {
|
|
197
|
+
if (item == null || typeof item !== "object" || Array.isArray(item)) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const candidate = item as Record<string, unknown>;
|
|
202
|
+
const name = typeof candidate.name === "string" ? candidate.name.trim() : "";
|
|
203
|
+
const baseUrl =
|
|
204
|
+
typeof candidate.baseUrl === "string" ? candidate.baseUrl.trim() : "";
|
|
205
|
+
|
|
206
|
+
if (!name || !baseUrl) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const entry: CustomProvider = { name, baseUrl };
|
|
211
|
+
if (typeof candidate.apiKey === "string") {
|
|
212
|
+
entry.apiKey = candidate.apiKey;
|
|
213
|
+
}
|
|
214
|
+
return entry;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Validate and normalize custom provider entries.
|
|
219
|
+
*
|
|
220
|
+
* Drops entries with missing/empty name or baseUrl, and deduplicates by name
|
|
221
|
+
* (first entry wins).
|
|
222
|
+
*/
|
|
223
|
+
function sanitizeCustomProviders(value: unknown): CustomProvider[] | undefined {
|
|
224
|
+
if (!Array.isArray(value)) {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const result: CustomProvider[] = [];
|
|
229
|
+
const seen = new Set<string>();
|
|
230
|
+
|
|
231
|
+
for (const item of value) {
|
|
232
|
+
const entry = parseCustomProvider(item);
|
|
233
|
+
if (!entry || seen.has(entry.name)) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
seen.add(entry.name);
|
|
237
|
+
result.push(entry);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return result.length > 0 ? result : undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
181
243
|
/**
|
|
182
244
|
* Check whether a value is a valid thinking level.
|
|
183
245
|
*
|
package/src/submit.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { readFileSync } from "node:fs";
|
|
|
8
8
|
import type { UserMessage } from "@mariozechner/pi-ai";
|
|
9
9
|
import type { AgentEvent } from "./agent.ts";
|
|
10
10
|
import { runAgentLoop } from "./agent.ts";
|
|
11
|
+
import { getErrorMessage } from "./errors.ts";
|
|
11
12
|
import { getGitState } from "./git.ts";
|
|
12
13
|
import {
|
|
13
14
|
type AppState,
|
|
@@ -103,10 +104,6 @@ export function isEmptyUserContent(content: UserMessage["content"]): boolean {
|
|
|
103
104
|
);
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
function getErrorMessage(error: unknown): string {
|
|
107
|
-
return error instanceof Error ? error.message : String(error);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
107
|
function buildSkillMessageContent(
|
|
111
108
|
skillName: string,
|
|
112
109
|
userText: string,
|
|
@@ -276,12 +273,13 @@ export async function submitResolvedInput(
|
|
|
276
273
|
content,
|
|
277
274
|
timestamp: Date.now(),
|
|
278
275
|
} satisfies UserMessage;
|
|
276
|
+
const loadGitState = state.loadGitState ?? getGitState;
|
|
279
277
|
|
|
280
278
|
const turn = appendMessage(state.db, session.id, userMessage);
|
|
281
279
|
state.messages.push(userMessage);
|
|
282
280
|
hooks?.onUserMessage?.(state);
|
|
283
281
|
|
|
284
|
-
state.git = await
|
|
282
|
+
state.git = await loadGitState(state.cwd);
|
|
285
283
|
|
|
286
284
|
const systemPrompt = buildPrompt(state);
|
|
287
285
|
const { tools, toolHandlers } = buildToolList(state);
|
|
@@ -313,7 +311,7 @@ export async function submitResolvedInput(
|
|
|
313
311
|
},
|
|
314
312
|
});
|
|
315
313
|
stopReason = result.stopReason;
|
|
316
|
-
state.git = await
|
|
314
|
+
state.git = await loadGitState(state.cwd);
|
|
317
315
|
return result.stopReason;
|
|
318
316
|
} finally {
|
|
319
317
|
state.running = false;
|