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.
Files changed (51) hide show
  1. package/README.md +87 -48
  2. package/assets/icon-1-minimal.svg +31 -0
  3. package/assets/icon-2-dark-terminal.svg +48 -0
  4. package/assets/icon-3-gradient-modern.svg +45 -0
  5. package/assets/icon-4-filled-bold.svg +54 -0
  6. package/assets/icon-5-community-badge.svg +63 -0
  7. package/assets/preview-0-5-0.png +0 -0
  8. package/assets/preview.gif +0 -0
  9. package/bin/mc.ts +14 -0
  10. package/bun.lock +438 -0
  11. package/package.json +12 -29
  12. package/src/agent.ts +592 -0
  13. package/src/cli.ts +124 -0
  14. package/src/git.ts +164 -0
  15. package/src/headless.ts +140 -0
  16. package/src/index.ts +645 -0
  17. package/src/input.ts +155 -0
  18. package/src/paths.ts +37 -0
  19. package/src/plugins.ts +183 -0
  20. package/src/prompt.ts +294 -0
  21. package/src/session.ts +838 -0
  22. package/src/settings.ts +184 -0
  23. package/src/skills.ts +258 -0
  24. package/src/submit.ts +323 -0
  25. package/src/theme.ts +147 -0
  26. package/src/tools.ts +636 -0
  27. package/src/ui/agent.test.ts +49 -0
  28. package/src/ui/agent.ts +210 -0
  29. package/src/ui/commands.test.ts +610 -0
  30. package/src/ui/commands.ts +638 -0
  31. package/src/ui/conversation.test.ts +892 -0
  32. package/src/ui/conversation.ts +926 -0
  33. package/src/ui/help.test.ts +26 -0
  34. package/src/ui/help.ts +119 -0
  35. package/src/ui/input.test.ts +74 -0
  36. package/src/ui/input.ts +138 -0
  37. package/src/ui/overlay.test.ts +42 -0
  38. package/src/ui/overlay.ts +59 -0
  39. package/src/ui/status.test.ts +450 -0
  40. package/src/ui/status.ts +357 -0
  41. package/src/ui.ts +615 -0
  42. package/.claude/settings.local.json +0 -54
  43. package/.prettierignore +0 -7
  44. package/dist/mc-edit.js +0 -275
  45. package/dist/mc.js +0 -7355
  46. package/docs/KNOWN_ISSUES.md +0 -13
  47. package/docs/design-decisions.md +0 -31
  48. package/docs/mini-coder.1.md +0 -227
  49. package/docs/superpowers/plans/2026-03-30-anthropic-oauth-removal.md +0 -61
  50. package/docs/superpowers/specs/2026-03-30-anthropic-oauth-removal-design.md +0 -47
  51. 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
+ }
@@ -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
+ }