mini-coder 0.4.1 → 0.5.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/README.md +87 -48
- package/assets/icon-1-minimal.svg +31 -0
- package/assets/icon-2-dark-terminal.svg +48 -0
- package/assets/icon-3-gradient-modern.svg +45 -0
- package/assets/icon-4-filled-bold.svg +54 -0
- package/assets/icon-5-community-badge.svg +63 -0
- package/assets/preview-0-5-0.png +0 -0
- package/assets/preview.gif +0 -0
- package/bin/mc.ts +14 -0
- package/bun.lock +438 -0
- package/package.json +12 -29
- package/src/agent.ts +592 -0
- package/src/cli.ts +124 -0
- package/src/git.ts +164 -0
- package/src/headless.ts +140 -0
- package/src/index.ts +645 -0
- package/src/input.ts +155 -0
- package/src/paths.ts +37 -0
- package/src/plugins.ts +183 -0
- package/src/prompt.ts +294 -0
- package/src/session.ts +838 -0
- package/src/settings.ts +184 -0
- package/src/skills.ts +258 -0
- package/src/submit.ts +323 -0
- package/src/theme.ts +147 -0
- package/src/tools.ts +636 -0
- package/src/ui/agent.test.ts +49 -0
- package/src/ui/agent.ts +210 -0
- package/src/ui/commands.test.ts +610 -0
- package/src/ui/commands.ts +638 -0
- package/src/ui/conversation.test.ts +892 -0
- package/src/ui/conversation.ts +926 -0
- package/src/ui/help.test.ts +26 -0
- package/src/ui/help.ts +119 -0
- package/src/ui/input.test.ts +74 -0
- package/src/ui/input.ts +138 -0
- package/src/ui/overlay.test.ts +42 -0
- package/src/ui/overlay.ts +59 -0
- package/src/ui/status.test.ts +450 -0
- package/src/ui/status.ts +357 -0
- package/src/ui.ts +615 -0
- package/.claude/settings.local.json +0 -54
- package/.prettierignore +0 -7
- package/dist/mc-edit.js +0 -275
- package/dist/mc.js +0 -7355
- package/docs/KNOWN_ISSUES.md +0 -13
- package/docs/design-decisions.md +0 -31
- package/docs/mini-coder.1.md +0 -227
- package/docs/superpowers/plans/2026-03-30-anthropic-oauth-removal.md +0 -61
- package/docs/superpowers/specs/2026-03-30-anthropic-oauth-removal-design.md +0 -47
- package/lefthook.yml +0 -4
package/src/settings.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User settings persistence and startup resolution.
|
|
3
|
+
*
|
|
4
|
+
* Stores global defaults such as model, effort, reasoning visibility,
|
|
5
|
+
* and verbose tool output in a JSON file under the app data directory.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { dirname } from "node:path";
|
|
12
|
+
import type { ThinkingLevel } from "@mariozechner/pi-ai";
|
|
13
|
+
|
|
14
|
+
/** Default reasoning effort when no saved setting exists. */
|
|
15
|
+
const DEFAULT_EFFORT: ThinkingLevel = "medium";
|
|
16
|
+
|
|
17
|
+
/** Default reasoning visibility when no saved setting exists. */
|
|
18
|
+
export const DEFAULT_SHOW_REASONING = true;
|
|
19
|
+
|
|
20
|
+
/** Default verbose tool rendering flag when no saved setting exists. */
|
|
21
|
+
export const DEFAULT_VERBOSE = false;
|
|
22
|
+
|
|
23
|
+
/** Persisted global user settings. */
|
|
24
|
+
export interface UserSettings {
|
|
25
|
+
/** Preferred provider/model identifier. */
|
|
26
|
+
defaultModel?: string;
|
|
27
|
+
/** Preferred reasoning effort. */
|
|
28
|
+
defaultEffort?: ThinkingLevel;
|
|
29
|
+
/** Whether reasoning blocks are shown in the UI. */
|
|
30
|
+
showReasoning?: boolean;
|
|
31
|
+
/** Whether full tool output is shown in the UI. */
|
|
32
|
+
verbose?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Resolved startup settings after applying defaults and availability checks. */
|
|
36
|
+
interface StartupSettings {
|
|
37
|
+
/** Model to use for this launch, or `null` if none are available. */
|
|
38
|
+
modelId: string | null;
|
|
39
|
+
/** Effective reasoning effort. */
|
|
40
|
+
effort: ThinkingLevel;
|
|
41
|
+
/** Effective reasoning visibility. */
|
|
42
|
+
showReasoning: boolean;
|
|
43
|
+
/** Effective verbose flag. */
|
|
44
|
+
verbose: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const THINKING_LEVELS = new Set<ThinkingLevel>([
|
|
48
|
+
"low",
|
|
49
|
+
"medium",
|
|
50
|
+
"high",
|
|
51
|
+
"xhigh",
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Load and validate user settings from disk.
|
|
56
|
+
*
|
|
57
|
+
* Invalid or missing files are treated as empty settings.
|
|
58
|
+
*
|
|
59
|
+
* @param path - Absolute path to `settings.json`.
|
|
60
|
+
* @returns The validated settings object.
|
|
61
|
+
*/
|
|
62
|
+
export function loadSettings(path: string): UserSettings {
|
|
63
|
+
if (!existsSync(path)) {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const raw = JSON.parse(readFileSync(path, "utf-8")) as unknown;
|
|
69
|
+
return sanitizeSettings(raw);
|
|
70
|
+
} catch {
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Save user settings to disk.
|
|
77
|
+
*
|
|
78
|
+
* Parent directories are created automatically. Only validated fields are
|
|
79
|
+
* written to disk.
|
|
80
|
+
*
|
|
81
|
+
* @param path - Absolute path to `settings.json`.
|
|
82
|
+
* @param settings - Settings to persist.
|
|
83
|
+
* @returns The validated settings that were written.
|
|
84
|
+
*/
|
|
85
|
+
export function saveSettings(
|
|
86
|
+
path: string,
|
|
87
|
+
settings: UserSettings,
|
|
88
|
+
): UserSettings {
|
|
89
|
+
const sanitized = sanitizeSettings(settings);
|
|
90
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
91
|
+
writeFileSync(path, JSON.stringify(sanitized, null, 2), "utf-8");
|
|
92
|
+
return sanitized;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Merge a partial settings update with the current file contents and persist it.
|
|
97
|
+
*
|
|
98
|
+
* Invalid fields in the update are ignored.
|
|
99
|
+
*
|
|
100
|
+
* @param path - Absolute path to `settings.json`.
|
|
101
|
+
* @param update - Partial settings update to merge.
|
|
102
|
+
* @returns The merged settings after persistence.
|
|
103
|
+
*/
|
|
104
|
+
export function updateSettings(
|
|
105
|
+
path: string,
|
|
106
|
+
update: Partial<UserSettings>,
|
|
107
|
+
): UserSettings {
|
|
108
|
+
const current = loadSettings(path);
|
|
109
|
+
const merged = { ...current, ...sanitizeSettings(update) };
|
|
110
|
+
return saveSettings(path, merged);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Resolve the effective startup settings for the current launch.
|
|
115
|
+
*
|
|
116
|
+
* The saved preferred model is only used when it is currently available.
|
|
117
|
+
* Otherwise the first available model is used for this launch, while the saved
|
|
118
|
+
* preference remains unchanged on disk.
|
|
119
|
+
*
|
|
120
|
+
* @param settings - Saved user settings.
|
|
121
|
+
* @param availableModelIds - Provider/model identifiers available this launch.
|
|
122
|
+
* @returns Effective startup settings with fallbacks applied.
|
|
123
|
+
*/
|
|
124
|
+
export function resolveStartupSettings(
|
|
125
|
+
settings: UserSettings,
|
|
126
|
+
availableModelIds: readonly string[],
|
|
127
|
+
): StartupSettings {
|
|
128
|
+
const preferredModel = settings.defaultModel;
|
|
129
|
+
const modelId =
|
|
130
|
+
preferredModel && availableModelIds.includes(preferredModel)
|
|
131
|
+
? preferredModel
|
|
132
|
+
: (availableModelIds[0] ?? null);
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
modelId,
|
|
136
|
+
effort: settings.defaultEffort ?? DEFAULT_EFFORT,
|
|
137
|
+
showReasoning: settings.showReasoning ?? DEFAULT_SHOW_REASONING,
|
|
138
|
+
verbose: settings.verbose ?? DEFAULT_VERBOSE,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Validate and normalize a parsed settings object.
|
|
144
|
+
*
|
|
145
|
+
* Unknown or invalid fields are dropped.
|
|
146
|
+
*
|
|
147
|
+
* @param value - Parsed JSON value.
|
|
148
|
+
* @returns Sanitized settings.
|
|
149
|
+
*/
|
|
150
|
+
function sanitizeSettings(value: unknown): UserSettings {
|
|
151
|
+
if (value == null || typeof value !== "object" || Array.isArray(value)) {
|
|
152
|
+
return {};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const candidate = value as Record<string, unknown>;
|
|
156
|
+
const settings: UserSettings = {};
|
|
157
|
+
|
|
158
|
+
if (typeof candidate.defaultModel === "string") {
|
|
159
|
+
settings.defaultModel = candidate.defaultModel;
|
|
160
|
+
}
|
|
161
|
+
if (isThinkingLevel(candidate.defaultEffort)) {
|
|
162
|
+
settings.defaultEffort = candidate.defaultEffort;
|
|
163
|
+
}
|
|
164
|
+
if (typeof candidate.showReasoning === "boolean") {
|
|
165
|
+
settings.showReasoning = candidate.showReasoning;
|
|
166
|
+
}
|
|
167
|
+
if (typeof candidate.verbose === "boolean") {
|
|
168
|
+
settings.verbose = candidate.verbose;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return settings;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Check whether a value is a valid thinking level.
|
|
176
|
+
*
|
|
177
|
+
* @param value - Value to validate.
|
|
178
|
+
* @returns `true` when the value is a supported thinking level.
|
|
179
|
+
*/
|
|
180
|
+
function isThinkingLevel(value: unknown): value is ThinkingLevel {
|
|
181
|
+
return (
|
|
182
|
+
typeof value === "string" && THINKING_LEVELS.has(value as ThinkingLevel)
|
|
183
|
+
);
|
|
184
|
+
}
|
package/src/skills.ts
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Skills (agentskills.io) discovery, parsing, and catalog generation.
|
|
3
|
+
*
|
|
4
|
+
* Scans configured directories for `SKILL.md` files, extracts YAML
|
|
5
|
+
* frontmatter (name, description), resolves name collisions (earlier
|
|
6
|
+
* scan paths win), and generates an XML catalog string for inclusion
|
|
7
|
+
* in the system prompt.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Types
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A discovered agent skill.
|
|
21
|
+
*
|
|
22
|
+
* Represents a single SKILL.md file that has been parsed and is ready
|
|
23
|
+
* for inclusion in the system prompt catalog.
|
|
24
|
+
*/
|
|
25
|
+
export interface Skill {
|
|
26
|
+
/** Skill name (from frontmatter or directory name fallback). */
|
|
27
|
+
name: string;
|
|
28
|
+
/** Skill description (from frontmatter, empty if absent). */
|
|
29
|
+
description: string;
|
|
30
|
+
/** Absolute path to the SKILL.md file. */
|
|
31
|
+
path: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Frontmatter parsing
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
function getFrontmatterLines(content: string): string[] | null {
|
|
39
|
+
if (!content.startsWith("---")) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const endIdx = content.indexOf("\n---", 3);
|
|
44
|
+
if (endIdx === -1) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return content.slice(4, endIdx).split("\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseFrontmatterEntry(
|
|
52
|
+
line: string,
|
|
53
|
+
): { key: string; value: string } | null {
|
|
54
|
+
const colonIdx = line.indexOf(":");
|
|
55
|
+
if (colonIdx === -1) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
key: line.slice(0, colonIdx).trim(),
|
|
61
|
+
value: line.slice(colonIdx + 1).trim(),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isIndentedFrontmatterLine(line: string): boolean {
|
|
66
|
+
return line.startsWith(" ") || line.startsWith("\t");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readFoldedFrontmatterValue(
|
|
70
|
+
lines: string[],
|
|
71
|
+
startIndex: number,
|
|
72
|
+
): string {
|
|
73
|
+
const folded: string[] = [];
|
|
74
|
+
for (let i = startIndex; i < lines.length; i++) {
|
|
75
|
+
const line = lines[i];
|
|
76
|
+
if (!line || !isIndentedFrontmatterLine(line)) {
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
folded.push(line.trim());
|
|
80
|
+
}
|
|
81
|
+
return folded.join(" ");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readDescriptionValue(
|
|
85
|
+
lines: string[],
|
|
86
|
+
lineIndex: number,
|
|
87
|
+
value: string,
|
|
88
|
+
): string {
|
|
89
|
+
if (value === ">") {
|
|
90
|
+
return readFoldedFrontmatterValue(lines, lineIndex + 1);
|
|
91
|
+
}
|
|
92
|
+
return stripQuotes(value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parse YAML frontmatter from a SKILL.md file's content.
|
|
97
|
+
*
|
|
98
|
+
* Handles simple key-value pairs, quoted strings, and YAML folded
|
|
99
|
+
* scalars (`>`). Does not use a full YAML parser — just enough to
|
|
100
|
+
* extract `name` and `description`.
|
|
101
|
+
*
|
|
102
|
+
* @param content - Raw file content.
|
|
103
|
+
* @returns Extracted name and description (both may be undefined).
|
|
104
|
+
*/
|
|
105
|
+
function parseFrontmatter(content: string): {
|
|
106
|
+
name: string | undefined;
|
|
107
|
+
description: string | undefined;
|
|
108
|
+
} {
|
|
109
|
+
const lines = getFrontmatterLines(content);
|
|
110
|
+
if (!lines) {
|
|
111
|
+
return { name: undefined, description: undefined };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let name: string | undefined;
|
|
115
|
+
let description: string | undefined;
|
|
116
|
+
|
|
117
|
+
for (const [index, line] of lines.entries()) {
|
|
118
|
+
const entry = parseFrontmatterEntry(line);
|
|
119
|
+
if (!entry) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (entry.key === "name") {
|
|
123
|
+
name = stripQuotes(entry.value);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (entry.key === "description") {
|
|
127
|
+
description = readDescriptionValue(lines, index, entry.value);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { name, description };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Strip surrounding single or double quotes from a string.
|
|
136
|
+
*
|
|
137
|
+
* @param s - The string to strip.
|
|
138
|
+
* @returns The string without surrounding quotes.
|
|
139
|
+
*/
|
|
140
|
+
function stripQuotes(s: string): string {
|
|
141
|
+
if (s.length >= 2) {
|
|
142
|
+
if (
|
|
143
|
+
(s[0] === '"' && s[s.length - 1] === '"') ||
|
|
144
|
+
(s[0] === "'" && s[s.length - 1] === "'")
|
|
145
|
+
) {
|
|
146
|
+
return s.slice(1, -1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return s;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Discovery
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
function listSkillEntries(basePath: string): string[] {
|
|
157
|
+
if (!existsSync(basePath)) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
return readdirSync(basePath);
|
|
162
|
+
} catch {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isDirectory(path: string): boolean {
|
|
168
|
+
try {
|
|
169
|
+
return statSync(path).isDirectory();
|
|
170
|
+
} catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function readSkill(basePath: string, entry: string): Skill | null {
|
|
176
|
+
const dir = join(basePath, entry);
|
|
177
|
+
if (!isDirectory(dir)) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const skillPath = join(dir, "SKILL.md");
|
|
182
|
+
if (!existsSync(skillPath)) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const content = readFileSync(skillPath, "utf-8");
|
|
187
|
+
const frontmatter = parseFrontmatter(content);
|
|
188
|
+
return {
|
|
189
|
+
name: frontmatter.name ?? entry,
|
|
190
|
+
description: frontmatter.description ?? "",
|
|
191
|
+
path: skillPath,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Discover agent skills from the given scan paths.
|
|
197
|
+
*
|
|
198
|
+
* Scans each path for subdirectories containing a `SKILL.md` file.
|
|
199
|
+
* Earlier paths in the array have higher priority — on name collision,
|
|
200
|
+
* the first-seen skill wins (project-level over user-level).
|
|
201
|
+
*
|
|
202
|
+
* @param scanPaths - Ordered directories to scan (project paths first).
|
|
203
|
+
* @returns Array of discovered {@link Skill} records, deduplicated by name.
|
|
204
|
+
*/
|
|
205
|
+
export function discoverSkills(scanPaths: string[]): Skill[] {
|
|
206
|
+
const seen = new Map<string, Skill>();
|
|
207
|
+
|
|
208
|
+
for (const basePath of scanPaths) {
|
|
209
|
+
for (const entry of listSkillEntries(basePath)) {
|
|
210
|
+
const skill = readSkill(basePath, entry);
|
|
211
|
+
if (!skill || seen.has(skill.name)) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
seen.set(skill.name, skill);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return [...seen.values()];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// Catalog generation
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Build the XML skill catalog for the system prompt.
|
|
227
|
+
*
|
|
228
|
+
* Returns the full catalog section including the preamble text and
|
|
229
|
+
* `<available_skills>` XML block. Returns an empty string if no
|
|
230
|
+
* skills are provided.
|
|
231
|
+
*
|
|
232
|
+
* @param skills - Discovered skills to include.
|
|
233
|
+
* @returns The catalog string to append to the system prompt.
|
|
234
|
+
*/
|
|
235
|
+
export function buildSkillCatalog(skills: Skill[]): string {
|
|
236
|
+
if (skills.length === 0) return "";
|
|
237
|
+
|
|
238
|
+
const entries = skills
|
|
239
|
+
.map(
|
|
240
|
+
(s) =>
|
|
241
|
+
` <skill>\n` +
|
|
242
|
+
` <name>${s.name}</name>\n` +
|
|
243
|
+
` <description>${s.description}</description>\n` +
|
|
244
|
+
` <location>${s.path}</location>\n` +
|
|
245
|
+
` </skill>`,
|
|
246
|
+
)
|
|
247
|
+
.join("\n");
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
`The following skills provide specialized instructions for specific tasks.\n` +
|
|
251
|
+
`Use the shell tool to read a skill's file when the task matches its description.\n` +
|
|
252
|
+
`When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md) and use that absolute path in tool commands.\n` +
|
|
253
|
+
`\n` +
|
|
254
|
+
`<available_skills>\n` +
|
|
255
|
+
entries +
|
|
256
|
+
`\n</available_skills>`
|
|
257
|
+
);
|
|
258
|
+
}
|