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/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
- const availableModels = listAvailableModels(providers);
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: use planning first to clear any assumptions with the user, then implement the plan. Deliver working code, unless you are genuinely blocked.
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 (rg, find, ls, cat), run tests, build, git, and any other command. 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.
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 getGitState(state.cwd);
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 getGitState(state.cwd);
314
+ state.git = await loadGitState(state.cwd);
317
315
  return result.stopReason;
318
316
  } finally {
319
317
  state.running = false;