mini-codex 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 JJ Wang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,231 @@
1
+ # mini-codex
2
+
3
+ `mini-codex` is a Bun-based coding agent for a single local workspace.
4
+
5
+ It can:
6
+ - answer workspace questions
7
+ - inspect files and search code
8
+ - make focused file edits
9
+ - run bounded local commands
10
+ - ask short clarification questions when needed
11
+ - keep workspace-local memory
12
+ - use bounded child agents in separate git worktrees
13
+
14
+ For architecture details, see [doc/ARCHITECTURE.md](/Users/jjwang/repo/mini-codex/doc/ARCHITECTURE.md).
15
+
16
+ ## Who This README Is For
17
+
18
+ There are two main ways to use this project:
19
+
20
+ 1. Package user
21
+ Install `mini-codex` and use it against your own workspace or project folder.
22
+ 2. Developer
23
+ Clone this repo, build it, run tests, and change the code.
24
+
25
+ ## Package User
26
+
27
+ ### Requirements
28
+
29
+ - Bun
30
+ - `git`
31
+ - network access for the remote provider
32
+ - `rg` recommended for the best search experience
33
+
34
+ Install Bun first if you do not already have it:
35
+
36
+ ```bash
37
+ curl -fsSL https://bun.sh/install | bash
38
+ ```
39
+
40
+ ### Install
41
+
42
+ Install the package globally with:
43
+
44
+ ```bash
45
+ bun install -g mini-codex
46
+ ```
47
+
48
+ The installed command is:
49
+
50
+ ```bash
51
+ mini-codex
52
+ ```
53
+
54
+ ### Login
55
+
56
+ ```bash
57
+ mini-codex auth login
58
+ mini-codex auth status
59
+ ```
60
+
61
+ Tokens are stored at:
62
+
63
+ ```text
64
+ ~/.mini-codex/openai-oauth.json
65
+ ```
66
+
67
+ ### Happy Path
68
+
69
+ Run `mini-codex` against a local workspace:
70
+
71
+ ```bash
72
+ mini-codex run "where is auth login implemented" --repo .
73
+ ```
74
+
75
+ Another example:
76
+
77
+ ```bash
78
+ mini-codex run "explain the current repo" --repo .
79
+ ```
80
+
81
+ What to expect:
82
+ - interactive terminal runs show step-by-step progress
83
+ - the agent highlights LLM calls, tool calls, results, clarifications, and final answers
84
+ - each run writes a workspace-local log under `.codex/logs/`
85
+ - workspace memory is stored under `.codex/memory/`
86
+
87
+ Useful flags:
88
+ - `--json`
89
+ - `--show-llm-details`
90
+ - `--show-tool-results`
91
+ - `--max-steps <n>`
92
+
93
+ Example:
94
+
95
+ ```bash
96
+ mini-codex run "fix this" --repo . --show-llm-details
97
+ ```
98
+
99
+ ### Default Configuration
100
+
101
+ By default, `mini-codex` uses:
102
+
103
+ - `MINI_CODEX_PROVIDER=openai-oauth`
104
+ - `MINI_CODEX_MODEL=gpt-5.4`
105
+
106
+ You usually do not need to set them manually.
107
+
108
+ ## Developer
109
+
110
+ ### Clone And Install
111
+
112
+ ```bash
113
+ git clone https://github.com/wjjnova/mini-codex.git
114
+ cd mini-codex
115
+ bun install
116
+ ```
117
+
118
+ If Bun is not installed yet:
119
+
120
+ ```bash
121
+ curl -fsSL https://bun.sh/install | bash
122
+ ```
123
+
124
+ ### Build
125
+
126
+ ```bash
127
+ bun run build
128
+ ```
129
+
130
+ ### Run From Source
131
+
132
+ ```bash
133
+ bun run dev -- run "explain the current repo" --repo .
134
+ ```
135
+
136
+ Auth commands from source:
137
+
138
+ ```bash
139
+ bun run dev -- auth login
140
+ bun run dev -- auth status
141
+ bun run dev -- auth logout
142
+ ```
143
+
144
+ ### Link A Local Command
145
+
146
+ If you want a shell command while developing locally:
147
+
148
+ ```bash
149
+ bun run build
150
+ bun link
151
+ ```
152
+
153
+ Then:
154
+
155
+ ```bash
156
+ mini-codex run "where is auth login implemented" --repo .
157
+ ```
158
+
159
+ ### Publish Package
160
+
161
+ For package publishing steps, see [doc/PUBLISHING.md](/Users/jjwang/repo/mini-codex/doc/PUBLISHING.md).
162
+
163
+ ### Test
164
+
165
+ Run the full test suite:
166
+
167
+ ```bash
168
+ bun test
169
+ ```
170
+
171
+ Or:
172
+
173
+ ```bash
174
+ bun run test
175
+ ```
176
+
177
+ Run the CLI end-to-end tests:
178
+
179
+ ```bash
180
+ bun run test:e2e
181
+ ```
182
+
183
+ ### Develop The Code
184
+
185
+ Most important source areas:
186
+
187
+ - CLI: [src/cli.ts](/Users/jjwang/repo/mini-codex/src/cli.ts)
188
+ - Loop: [src/loop.ts](/Users/jjwang/repo/mini-codex/src/loop.ts)
189
+ - Runtime: [src/runtime.ts](/Users/jjwang/repo/mini-codex/src/runtime.ts)
190
+ - Tools: [src/tools.ts](/Users/jjwang/repo/mini-codex/src/tools.ts)
191
+ - Prompting: [src/prompt.ts](/Users/jjwang/repo/mini-codex/src/prompt.ts)
192
+ - Provider: [src/providers/openai.ts](/Users/jjwang/repo/mini-codex/src/providers/openai.ts)
193
+ - Terminal output: [src/terminal.ts](/Users/jjwang/repo/mini-codex/src/terminal.ts)
194
+
195
+ Repo-local supporting data:
196
+
197
+ - skills: [.codex/skills](/Users/jjwang/repo/mini-codex/.codex/skills)
198
+ - memory: `.codex/memory/`
199
+ - logs: `.codex/logs/`
200
+ - sub-agents: `.codex/subagents/`
201
+
202
+ ### Cross-Platform Notes
203
+
204
+ The package-install path is intended to work on macOS, Linux, and Windows with Bun.
205
+
206
+ Cross-platform behavior in this repo:
207
+ - the installable command prefers built `dist/cli.js`
208
+ - `run_command` uses the platform default shell
209
+ - `rg` is preferred for search
210
+ - `search_files` is the built-in fallback when `rg` is unavailable
211
+
212
+ ## Current Tool Set
213
+
214
+ - `list_files`
215
+ - `read_file`
216
+ - `search_files`
217
+ - `write_file`
218
+ - `edit_file`
219
+ - `run_command`
220
+ - `write_memory`
221
+ - `spawn_subagents`
222
+ - `wait_subagents`
223
+ - `finish`
224
+
225
+ ## Notes
226
+
227
+ - This project is intentionally small and local-first.
228
+ - It is not trying to be a full Codex clone.
229
+ - The current provider path is OpenAI OAuth oriented.
230
+ - License: MIT. See [LICENSE](/Users/jjwang/repo/mini-codex/LICENSE).
231
+ - For deeper design detail, use [doc/ARCHITECTURE.md](/Users/jjwang/repo/mini-codex/doc/ARCHITECTURE.md).
package/bin/mini-codex ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bun
2
+ import { existsSync } from "node:fs";
3
+
4
+ const distEntry = new URL("../dist/cli.js", import.meta.url);
5
+ const sourceEntry = new URL("../src/cli.ts", import.meta.url);
6
+
7
+ await import(existsSync(distEntry) ? distEntry.href : sourceEntry.href);
@@ -0,0 +1,13 @@
1
+ export interface AuthStatus {
2
+ provider: string;
3
+ loggedIn: boolean;
4
+ expiresAt?: number;
5
+ expired?: boolean;
6
+ accountId?: string;
7
+ }
8
+ export interface AuthManager {
9
+ login(): Promise<void>;
10
+ logout(): Promise<void>;
11
+ getStatus(): Promise<AuthStatus>;
12
+ getAccessToken(): Promise<string>;
13
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import type { AuthManager, AuthStatus } from "./base.js";
2
+ export declare class OpenAIOAuthManager implements AuthManager {
3
+ login(): Promise<void>;
4
+ logout(): Promise<void>;
5
+ getStatus(): Promise<AuthStatus>;
6
+ getAccessToken(): Promise<string>;
7
+ }
@@ -0,0 +1,218 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import http from "node:http";
5
+ import crypto from "node:crypto";
6
+ import { exec } from "node:child_process";
7
+ import { promisify } from "node:util";
8
+ const execAsync = promisify(exec);
9
+ const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
10
+ const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
11
+ const TOKEN_URL = "https://auth.openai.com/oauth/token";
12
+ const REDIRECT_URI = "http://localhost:1455/auth/callback";
13
+ const SCOPE = "openid profile email offline_access";
14
+ const JWT_CLAIM_PATH = "https://api.openai.com/auth";
15
+ const STORE_DIR = path.join(os.homedir(), ".mini-codex");
16
+ const STORE_FILE = path.join(STORE_DIR, "openai-oauth.json");
17
+ function base64url(input) {
18
+ return input.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
19
+ }
20
+ function generatePKCE() {
21
+ const verifier = base64url(crypto.randomBytes(32));
22
+ const challenge = base64url(crypto.createHash("sha256").update(verifier).digest());
23
+ return { verifier, challenge };
24
+ }
25
+ function randomState() {
26
+ return crypto.randomBytes(16).toString("hex");
27
+ }
28
+ function decodeJwt(token) {
29
+ try {
30
+ const parts = token.split(".");
31
+ if (parts.length !== 3)
32
+ return null;
33
+ const payload = parts[1];
34
+ const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
35
+ const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
36
+ return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ function extractAccountId(token) {
43
+ const payload = decodeJwt(token);
44
+ return payload?.[JWT_CLAIM_PATH]?.chatgpt_account_id;
45
+ }
46
+ async function ensureStoreDir() {
47
+ await fs.mkdir(STORE_DIR, { recursive: true });
48
+ }
49
+ async function readStoredToken() {
50
+ try {
51
+ const raw = await fs.readFile(STORE_FILE, "utf8");
52
+ return JSON.parse(raw);
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ async function writeStoredToken(token) {
59
+ await ensureStoreDir();
60
+ await fs.writeFile(STORE_FILE, JSON.stringify(token, null, 2), "utf8");
61
+ }
62
+ async function exchangeCode(code, verifier) {
63
+ const response = await fetch(TOKEN_URL, {
64
+ method: "POST",
65
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
66
+ body: new URLSearchParams({
67
+ grant_type: "authorization_code",
68
+ client_id: CLIENT_ID,
69
+ code,
70
+ code_verifier: verifier,
71
+ redirect_uri: REDIRECT_URI,
72
+ }),
73
+ });
74
+ if (!response.ok) {
75
+ throw new Error(`OAuth token exchange failed: HTTP ${response.status} ${await response.text()}`);
76
+ }
77
+ const json = await response.json();
78
+ return {
79
+ accessToken: json.access_token,
80
+ refreshToken: json.refresh_token,
81
+ expiresAt: Date.now() + Number(json.expires_in) * 1000,
82
+ accountId: extractAccountId(json.access_token),
83
+ };
84
+ }
85
+ async function refreshToken(refreshToken) {
86
+ const response = await fetch(TOKEN_URL, {
87
+ method: "POST",
88
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
89
+ body: new URLSearchParams({
90
+ grant_type: "refresh_token",
91
+ client_id: CLIENT_ID,
92
+ refresh_token: refreshToken,
93
+ }),
94
+ });
95
+ if (!response.ok) {
96
+ throw new Error(`OAuth token refresh failed: HTTP ${response.status} ${await response.text()}`);
97
+ }
98
+ const json = await response.json();
99
+ return {
100
+ accessToken: json.access_token,
101
+ refreshToken: json.refresh_token,
102
+ expiresAt: Date.now() + Number(json.expires_in) * 1000,
103
+ accountId: extractAccountId(json.access_token),
104
+ };
105
+ }
106
+ async function openBrowser(url) {
107
+ try {
108
+ await execAsync(`open '${url.replace(/'/g, "'\\''")}'`);
109
+ }
110
+ catch {
111
+ console.log(`Open this URL in your browser:\n${url}`);
112
+ }
113
+ }
114
+ async function waitForAuthorizationCode(state) {
115
+ return await new Promise((resolve, reject) => {
116
+ let settled = false;
117
+ const server = http.createServer((req, res) => {
118
+ try {
119
+ const url = new URL(req.url || "", "http://localhost");
120
+ if (url.pathname !== "/auth/callback") {
121
+ res.statusCode = 404;
122
+ res.end("Not found");
123
+ return;
124
+ }
125
+ if (url.searchParams.get("state") !== state) {
126
+ res.statusCode = 400;
127
+ res.end("State mismatch");
128
+ return;
129
+ }
130
+ const code = url.searchParams.get("code");
131
+ if (!code) {
132
+ res.statusCode = 400;
133
+ res.end("Missing code");
134
+ return;
135
+ }
136
+ res.statusCode = 200;
137
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
138
+ res.end("<p>Authentication successful. Return to mini-codex.</p>");
139
+ if (!settled) {
140
+ settled = true;
141
+ server.close();
142
+ resolve(code);
143
+ }
144
+ }
145
+ catch (error) {
146
+ if (!settled) {
147
+ settled = true;
148
+ server.close();
149
+ reject(error);
150
+ }
151
+ }
152
+ });
153
+ server.listen(1455, "127.0.0.1", () => undefined);
154
+ server.on("error", (error) => {
155
+ if (!settled) {
156
+ settled = true;
157
+ reject(error);
158
+ }
159
+ });
160
+ setTimeout(() => {
161
+ if (!settled) {
162
+ settled = true;
163
+ server.close();
164
+ reject(new Error("OAuth callback timed out after 5 minutes"));
165
+ }
166
+ }, 5 * 60 * 1000);
167
+ });
168
+ }
169
+ export class OpenAIOAuthManager {
170
+ async login() {
171
+ const { verifier, challenge } = generatePKCE();
172
+ const state = randomState();
173
+ const url = new URL(AUTHORIZE_URL);
174
+ url.searchParams.set("response_type", "code");
175
+ url.searchParams.set("client_id", CLIENT_ID);
176
+ url.searchParams.set("redirect_uri", REDIRECT_URI);
177
+ url.searchParams.set("scope", SCOPE);
178
+ url.searchParams.set("code_challenge", challenge);
179
+ url.searchParams.set("code_challenge_method", "S256");
180
+ url.searchParams.set("state", state);
181
+ url.searchParams.set("id_token_add_organizations", "true");
182
+ url.searchParams.set("codex_cli_simplified_flow", "true");
183
+ url.searchParams.set("originator", "pi");
184
+ console.log("Starting OpenAI OAuth login...");
185
+ await openBrowser(url.toString());
186
+ const code = await waitForAuthorizationCode(state);
187
+ const token = await exchangeCode(code, verifier);
188
+ await writeStoredToken(token);
189
+ console.log("Login successful.");
190
+ }
191
+ async logout() {
192
+ await fs.rm(STORE_FILE, { force: true });
193
+ }
194
+ async getStatus() {
195
+ const token = await readStoredToken();
196
+ if (!token) {
197
+ return { provider: "openai-oauth", loggedIn: false };
198
+ }
199
+ const expired = token.expiresAt <= Date.now();
200
+ return {
201
+ provider: "openai-oauth",
202
+ loggedIn: true,
203
+ expiresAt: token.expiresAt,
204
+ expired,
205
+ accountId: token.accountId,
206
+ };
207
+ }
208
+ async getAccessToken() {
209
+ const token = await readStoredToken();
210
+ if (!token)
211
+ throw new Error("Not logged in. Run: bun run dev -- auth login");
212
+ if (token.expiresAt > Date.now() + 60_000)
213
+ return token.accessToken;
214
+ const refreshed = await refreshToken(token.refreshToken);
215
+ await writeStoredToken(refreshed);
216
+ return refreshed.accessToken;
217
+ }
218
+ }
@@ -0,0 +1,10 @@
1
+ export declare function parseArgs(argv: string[]): {
2
+ command: string;
3
+ subcommandOrTask: string;
4
+ repoPath: string;
5
+ maxSteps: number;
6
+ json: boolean;
7
+ depth: number;
8
+ showLlmDetails: boolean;
9
+ showToolResults: boolean;
10
+ };
@@ -0,0 +1,20 @@
1
+ import path from "node:path";
2
+ export function parseArgs(argv) {
3
+ const [command, subcommandOrTask, ...rest] = argv;
4
+ const repoFlagIndex = rest.indexOf("--repo");
5
+ const maxStepsIndex = rest.indexOf("--max-steps");
6
+ const jsonFlag = rest.includes("--json");
7
+ const depthIndex = rest.indexOf("--depth");
8
+ const showLlmDetails = rest.includes("--show-llm-details");
9
+ const showToolResults = rest.includes("--show-tool-results");
10
+ return {
11
+ command,
12
+ subcommandOrTask,
13
+ repoPath: repoFlagIndex >= 0 && rest[repoFlagIndex + 1] ? path.resolve(rest[repoFlagIndex + 1]) : process.cwd(),
14
+ maxSteps: maxStepsIndex >= 0 && rest[maxStepsIndex + 1] ? Number(rest[maxStepsIndex + 1]) : 12,
15
+ json: jsonFlag,
16
+ depth: depthIndex >= 0 && rest[depthIndex + 1] ? Number(rest[depthIndex + 1]) : 0,
17
+ showLlmDetails,
18
+ showToolResults,
19
+ };
20
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env bun
2
+ import readline from "node:readline/promises";
3
+ import { renderSession, runLoop } from "./loop.js";
4
+ import { OpenAIOAuthManager } from "./auth/openai-oauth.js";
5
+ import { TerminalRenderer } from "./terminal.js";
6
+ import { parseArgs } from "./cli-args.js";
7
+ async function handleAuth(subcommand) {
8
+ const auth = new OpenAIOAuthManager();
9
+ switch (subcommand) {
10
+ case "login":
11
+ await auth.login();
12
+ return;
13
+ case "status": {
14
+ const status = await auth.getStatus();
15
+ console.log(JSON.stringify(status, null, 2));
16
+ return;
17
+ }
18
+ case "logout":
19
+ await auth.logout();
20
+ console.log("Logged out.");
21
+ return;
22
+ default:
23
+ console.error("Usage: bun run dev -- auth <login|status|logout>");
24
+ process.exit(1);
25
+ }
26
+ }
27
+ async function main() {
28
+ const args = parseArgs(process.argv.slice(2));
29
+ if (args.command === "auth") {
30
+ await handleAuth(args.subcommandOrTask);
31
+ return;
32
+ }
33
+ if (args.command !== "run" || !args.subcommandOrTask) {
34
+ console.error('Usage: bun run dev -- run "task" --repo <path> [--max-steps 12] [--json] [--depth 0] [--show-llm-details] [--show-tool-results]');
35
+ console.error(' or: bun run dev -- auth <login|status|logout>');
36
+ process.exit(1);
37
+ }
38
+ const interactive = !args.json && Boolean(process.stdin.isTTY && process.stdout.isTTY) && args.depth === 0;
39
+ const renderer = interactive ? new TerminalRenderer(true, args.showLlmDetails, args.showToolResults) : null;
40
+ const askUser = interactive
41
+ ? async (question) => {
42
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
43
+ try {
44
+ const answer = await rl.question("> ");
45
+ return answer.trim();
46
+ }
47
+ finally {
48
+ rl.close();
49
+ }
50
+ }
51
+ : undefined;
52
+ const session = await runLoop(args.subcommandOrTask, args.repoPath, {
53
+ maxSteps: args.maxSteps,
54
+ depth: args.depth,
55
+ interactive,
56
+ renderer,
57
+ askUser,
58
+ });
59
+ if (args.json) {
60
+ console.log(JSON.stringify(session, null, 2));
61
+ }
62
+ else {
63
+ const lastStep = session.steps.at(-1);
64
+ const answer = lastStep?.action.type === "tool" && typeof lastStep.action.toolCall.args.answer === "string"
65
+ ? lastStep.action.toolCall.args.answer
66
+ : null;
67
+ if (typeof answer === "string" && answer.trim()) {
68
+ renderer?.finalAnswer(answer.trim());
69
+ }
70
+ if (!interactive) {
71
+ console.log(renderSession(session, { showToolResults: args.showToolResults }));
72
+ }
73
+ }
74
+ }
75
+ main().catch((error) => {
76
+ console.error(error);
77
+ process.exit(1);
78
+ });
package/dist/loop.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { Session } from "./types.js";
2
+ import type { ModelProvider } from "./providers/base.js";
3
+ import { TerminalRenderer } from "./terminal.js";
4
+ export interface RunLoopOptions {
5
+ maxSteps?: number;
6
+ depth?: number;
7
+ interactive?: boolean;
8
+ renderer?: TerminalRenderer | null;
9
+ askUser?: (question: string) => Promise<string>;
10
+ provider?: ModelProvider;
11
+ }
12
+ export declare function runLoop(task: string, repoPath: string, options?: RunLoopOptions): Promise<Session>;
13
+ export declare function renderSession(session: Session, options?: {
14
+ showToolResults?: boolean;
15
+ }): string;