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/git.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git state gathering.
|
|
3
|
+
*
|
|
4
|
+
* Runs fast git commands to collect branch name, working tree counts,
|
|
5
|
+
* and ahead/behind status. Used by the system prompt footer and the
|
|
6
|
+
* status bar to give the model and user situational awareness.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Types
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Snapshot of the current git repository state.
|
|
17
|
+
*
|
|
18
|
+
* All counts are non-negative integers. When there is no upstream
|
|
19
|
+
* tracking branch, `ahead` and `behind` are both `0`.
|
|
20
|
+
*/
|
|
21
|
+
export interface GitState {
|
|
22
|
+
/** Absolute path to the repository root. */
|
|
23
|
+
root: string;
|
|
24
|
+
/** Current branch name (empty string for detached HEAD). */
|
|
25
|
+
branch: string;
|
|
26
|
+
/** Number of staged (index) changes. */
|
|
27
|
+
staged: number;
|
|
28
|
+
/** Number of unstaged working-tree modifications. */
|
|
29
|
+
modified: number;
|
|
30
|
+
/** Number of untracked files. */
|
|
31
|
+
untracked: number;
|
|
32
|
+
/** Commits ahead of the upstream tracking branch. */
|
|
33
|
+
ahead: number;
|
|
34
|
+
/** Commits behind the upstream tracking branch. */
|
|
35
|
+
behind: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Helpers
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Run a git command and return its trimmed stdout.
|
|
44
|
+
* Returns `null` if the command fails (non-zero exit).
|
|
45
|
+
*/
|
|
46
|
+
async function run(
|
|
47
|
+
args: string[],
|
|
48
|
+
cwd: string,
|
|
49
|
+
trim = true,
|
|
50
|
+
): Promise<string | null> {
|
|
51
|
+
const proc = Bun.spawn(["git", ...args], {
|
|
52
|
+
cwd,
|
|
53
|
+
stdout: "pipe",
|
|
54
|
+
stderr: "pipe",
|
|
55
|
+
});
|
|
56
|
+
const out = await new Response(proc.stdout as ReadableStream).text();
|
|
57
|
+
const code = await proc.exited;
|
|
58
|
+
if (code !== 0) return null;
|
|
59
|
+
return trim ? out.trim() : out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isUntrackedStatus(
|
|
63
|
+
indexStatus: string,
|
|
64
|
+
workingTreeStatus: string,
|
|
65
|
+
): boolean {
|
|
66
|
+
return indexStatus === "?" && workingTreeStatus === "?";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function hasTrackedChange(status: string): boolean {
|
|
70
|
+
return status !== " " && status !== "?";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parse `git status --porcelain` output into staged, modified, and untracked counts.
|
|
75
|
+
*
|
|
76
|
+
* Porcelain v1 format: two-character status code per line.
|
|
77
|
+
* - Column 1 = index (staged) status
|
|
78
|
+
* - Column 2 = working tree status
|
|
79
|
+
* - `?` in both columns = untracked
|
|
80
|
+
*
|
|
81
|
+
* @param output - Raw `git status --porcelain` output.
|
|
82
|
+
* @returns Counts of staged, modified, and untracked files.
|
|
83
|
+
*/
|
|
84
|
+
function parseStatus(output: string): {
|
|
85
|
+
staged: number;
|
|
86
|
+
modified: number;
|
|
87
|
+
untracked: number;
|
|
88
|
+
} {
|
|
89
|
+
let staged = 0;
|
|
90
|
+
let modified = 0;
|
|
91
|
+
let untracked = 0;
|
|
92
|
+
|
|
93
|
+
for (const line of output.split("\n")) {
|
|
94
|
+
if (line.length < 2) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const indexStatus = line[0];
|
|
99
|
+
const workingTreeStatus = line[1];
|
|
100
|
+
if (!indexStatus || !workingTreeStatus) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (isUntrackedStatus(indexStatus, workingTreeStatus)) {
|
|
104
|
+
untracked++;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (hasTrackedChange(indexStatus)) {
|
|
108
|
+
staged++;
|
|
109
|
+
}
|
|
110
|
+
if (hasTrackedChange(workingTreeStatus)) {
|
|
111
|
+
modified++;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { staged, modified, untracked };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Public API
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Gather the current git state for a directory.
|
|
124
|
+
*
|
|
125
|
+
* Runs several fast git commands in parallel to collect branch, working
|
|
126
|
+
* tree status, and ahead/behind counts. Returns `null` if the directory
|
|
127
|
+
* is not inside a git repository.
|
|
128
|
+
*
|
|
129
|
+
* @param cwd - The directory to query (can be a subdirectory of the repo).
|
|
130
|
+
* @returns A {@link GitState} snapshot, or `null` if not in a git repo.
|
|
131
|
+
*/
|
|
132
|
+
export async function getGitState(cwd: string): Promise<GitState | null> {
|
|
133
|
+
// Check if we're in a repo and get the root
|
|
134
|
+
const root = await run(["rev-parse", "--show-toplevel"], cwd);
|
|
135
|
+
if (root === null) return null;
|
|
136
|
+
|
|
137
|
+
// Run remaining commands in parallel
|
|
138
|
+
const [branch, status, revList] = await Promise.all([
|
|
139
|
+
run(["branch", "--show-current"], cwd),
|
|
140
|
+
run(["status", "--porcelain"], cwd, false),
|
|
141
|
+
run(["rev-list", "--left-right", "--count", "HEAD...@{upstream}"], cwd),
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
const { staged, modified, untracked } = parseStatus(status ?? "");
|
|
145
|
+
|
|
146
|
+
// Parse ahead/behind from rev-list output (format: "ahead\tbehind")
|
|
147
|
+
let ahead = 0;
|
|
148
|
+
let behind = 0;
|
|
149
|
+
if (revList) {
|
|
150
|
+
const parts = revList.split(/\s+/);
|
|
151
|
+
ahead = parseInt(parts[0] ?? "0", 10) || 0;
|
|
152
|
+
behind = parseInt(parts[1] ?? "0", 10) || 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
root,
|
|
157
|
+
branch: branch ?? "",
|
|
158
|
+
staged,
|
|
159
|
+
modified,
|
|
160
|
+
untracked,
|
|
161
|
+
ahead,
|
|
162
|
+
behind,
|
|
163
|
+
};
|
|
164
|
+
}
|
package/src/headless.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless one-shot execution.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AppState } from "./index.ts";
|
|
8
|
+
import {
|
|
9
|
+
resolveRawInput,
|
|
10
|
+
type SubmitTurnHooks,
|
|
11
|
+
submitResolvedInput,
|
|
12
|
+
} from "./submit.ts";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Types
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/** Options for a headless one-shot run. */
|
|
19
|
+
export interface HeadlessRunOptions {
|
|
20
|
+
/** Optional line writer for NDJSON event output. */
|
|
21
|
+
writeLine?: (line: string) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Helpers
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
function defaultWriteLine(line: string): void {
|
|
29
|
+
process.stdout.write(`${line}\n`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isBrokenPipeError(error: unknown): boolean {
|
|
33
|
+
return (
|
|
34
|
+
typeof error === "object" &&
|
|
35
|
+
error !== null &&
|
|
36
|
+
(("code" in error && error.code === "EPIPE") ||
|
|
37
|
+
("message" in error &&
|
|
38
|
+
typeof error.message === "string" &&
|
|
39
|
+
error.message.includes("broken pipe")))
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createSigintHandler(state: AppState): () => void {
|
|
44
|
+
return () => {
|
|
45
|
+
state.abortController?.abort();
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildCommandError(command: string): Error {
|
|
50
|
+
return new Error(
|
|
51
|
+
`Headless mode does not support slash commands: /${command}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Run a single headless prompt to completion and stream NDJSON events.
|
|
57
|
+
*
|
|
58
|
+
* The raw input is parsed with the same rules as interactive input. Slash
|
|
59
|
+
* commands are rejected in headless mode. Assistant/tool events are written as
|
|
60
|
+
* one JSON object per line.
|
|
61
|
+
*
|
|
62
|
+
* @param state - Mutable application state for the run.
|
|
63
|
+
* @param rawInput - Exact raw prompt text supplied by the user.
|
|
64
|
+
* @param options - Optional event-output overrides.
|
|
65
|
+
* @returns The terminal stop reason for the agent loop.
|
|
66
|
+
*/
|
|
67
|
+
export async function runHeadlessPrompt(
|
|
68
|
+
state: AppState,
|
|
69
|
+
rawInput: string,
|
|
70
|
+
options?: HeadlessRunOptions,
|
|
71
|
+
): Promise<"stop" | "length" | "error" | "aborted"> {
|
|
72
|
+
const resolved = resolveRawInput(rawInput, state);
|
|
73
|
+
switch (resolved.type) {
|
|
74
|
+
case "empty":
|
|
75
|
+
throw new Error("Headless input is empty.");
|
|
76
|
+
case "error":
|
|
77
|
+
throw new Error(resolved.message);
|
|
78
|
+
case "command":
|
|
79
|
+
throw buildCommandError(resolved.command);
|
|
80
|
+
case "message":
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let brokenPipe = false;
|
|
85
|
+
let outputError: unknown = null;
|
|
86
|
+
const stopForBrokenPipe = (): void => {
|
|
87
|
+
if (brokenPipe) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
brokenPipe = true;
|
|
91
|
+
state.abortController?.abort();
|
|
92
|
+
};
|
|
93
|
+
const writeLineImpl = options?.writeLine ?? defaultWriteLine;
|
|
94
|
+
const writeLine = (line: string): void => {
|
|
95
|
+
if (brokenPipe) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
writeLineImpl(line);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (!isBrokenPipeError(error)) {
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
stopForBrokenPipe();
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const hooks: SubmitTurnHooks = {
|
|
109
|
+
onEvent: (event) => {
|
|
110
|
+
writeLine(JSON.stringify(event));
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
const sigintHandler = createSigintHandler(state);
|
|
114
|
+
const stdoutErrorHandler = (error: unknown): void => {
|
|
115
|
+
if (isBrokenPipeError(error)) {
|
|
116
|
+
stopForBrokenPipe();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
outputError = error;
|
|
120
|
+
state.abortController?.abort();
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
process.stdout.on("error", stdoutErrorHandler);
|
|
124
|
+
process.on("SIGINT", sigintHandler);
|
|
125
|
+
try {
|
|
126
|
+
const stopReason = await submitResolvedInput(
|
|
127
|
+
rawInput,
|
|
128
|
+
resolved.content,
|
|
129
|
+
state,
|
|
130
|
+
hooks,
|
|
131
|
+
);
|
|
132
|
+
if (outputError) {
|
|
133
|
+
throw outputError;
|
|
134
|
+
}
|
|
135
|
+
return brokenPipe ? "stop" : stopReason;
|
|
136
|
+
} finally {
|
|
137
|
+
process.stdout.off("error", stdoutErrorHandler);
|
|
138
|
+
process.off("SIGINT", sigintHandler);
|
|
139
|
+
}
|
|
140
|
+
}
|