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 +21 -0
- package/README.md +231 -0
- package/bin/mini-codex +7 -0
- package/dist/auth/base.d.ts +13 -0
- package/dist/auth/base.js +1 -0
- package/dist/auth/openai-oauth.d.ts +7 -0
- package/dist/auth/openai-oauth.js +218 -0
- package/dist/cli-args.d.ts +10 -0
- package/dist/cli-args.js +20 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +78 -0
- package/dist/loop.d.ts +15 -0
- package/dist/loop.js +221 -0
- package/dist/memory.d.ts +7 -0
- package/dist/memory.js +84 -0
- package/dist/model.d.ts +5 -0
- package/dist/model.js +69 -0
- package/dist/prompt.d.ts +3 -0
- package/dist/prompt.js +91 -0
- package/dist/providers/base.d.ts +5 -0
- package/dist/providers/base.js +1 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.js +13 -0
- package/dist/providers/openai.d.ts +9 -0
- package/dist/providers/openai.js +278 -0
- package/dist/providers/test-fixture.d.ts +8 -0
- package/dist/providers/test-fixture.js +36 -0
- package/dist/runtime.d.ts +9 -0
- package/dist/runtime.js +99 -0
- package/dist/skills.d.ts +6 -0
- package/dist/skills.js +37 -0
- package/dist/subagents.d.ts +16 -0
- package/dist/subagents.js +196 -0
- package/dist/terminal.d.ts +29 -0
- package/dist/terminal.js +242 -0
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +136 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.js +1 -0
- package/package.json +57 -0
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,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,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
|
+
}
|
package/dist/cli-args.js
ADDED
|
@@ -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
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;
|