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.
@@ -0,0 +1,278 @@
1
+ import { OpenAIOAuthManager } from "../auth/openai-oauth.js";
2
+ import { providerInstructions } from "../prompt.js";
3
+ const MODEL = process.env.MINI_CODEX_MODEL || "gpt-5.4";
4
+ const BASE_URL = "https://chatgpt.com/backend-api/codex/responses";
5
+ const JWT_CLAIM_PATH = "https://api.openai.com/auth";
6
+ const tools = [
7
+ "list_files",
8
+ "read_file",
9
+ "search_files",
10
+ "write_file",
11
+ "edit_file",
12
+ "run_command",
13
+ "write_memory",
14
+ "spawn_subagents",
15
+ "wait_subagents",
16
+ "finish",
17
+ ];
18
+ function toolCallSchema(tool, args, required = []) {
19
+ return {
20
+ type: "object",
21
+ additionalProperties: false,
22
+ properties: {
23
+ tool: { type: "string", const: tool },
24
+ args: {
25
+ type: "object",
26
+ additionalProperties: false,
27
+ properties: args,
28
+ required,
29
+ },
30
+ },
31
+ required: ["tool", "args"],
32
+ };
33
+ }
34
+ function decodeJwt(token) {
35
+ try {
36
+ const parts = token.split(".");
37
+ if (parts.length !== 3)
38
+ return null;
39
+ const payload = parts[1];
40
+ const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
41
+ const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
42
+ return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ function extractAccountId(token) {
49
+ const payload = decodeJwt(token);
50
+ const accountId = payload?.[JWT_CLAIM_PATH]?.chatgpt_account_id;
51
+ if (!accountId)
52
+ throw new Error("Failed to extract ChatGPT account id from access token");
53
+ return accountId;
54
+ }
55
+ async function parseSSE(response) {
56
+ const text = await response.text();
57
+ const chunks = text.split(/\n\n/);
58
+ const events = [];
59
+ for (const chunk of chunks) {
60
+ const dataLines = chunk
61
+ .split("\n")
62
+ .filter((line) => line.startsWith("data:"))
63
+ .map((line) => line.slice(5).trim());
64
+ if (!dataLines.length)
65
+ continue;
66
+ const data = dataLines.join("\n").trim();
67
+ if (!data || data === "[DONE]")
68
+ continue;
69
+ try {
70
+ events.push(JSON.parse(data));
71
+ }
72
+ catch {
73
+ // ignore malformed event
74
+ }
75
+ }
76
+ for (const event of events) {
77
+ if (event.type === "response.failed") {
78
+ throw new Error(event.response?.error?.message || "Codex/OpenAI response failed");
79
+ }
80
+ if (event.type === "error") {
81
+ throw new Error(event.message || event.code || "Codex/OpenAI error event");
82
+ }
83
+ }
84
+ const completed = [...events].reverse().find((event) => event.type === "response.completed" || event.type === "response.done");
85
+ const outputText = completed?.response?.output_text;
86
+ if (typeof outputText === "string" && outputText.trim())
87
+ return outputText;
88
+ const textDeltas = events
89
+ .filter((event) => event.type === "response.output_text.delta" && typeof event.delta === "string")
90
+ .map((event) => event.delta)
91
+ .join("");
92
+ return textDeltas;
93
+ }
94
+ export function extractJsonObjects(text) {
95
+ const values = [];
96
+ let cursor = 0;
97
+ while (cursor < text.length) {
98
+ const start = text.indexOf("{", cursor);
99
+ if (start < 0)
100
+ break;
101
+ let depth = 0;
102
+ let inString = false;
103
+ let escaped = false;
104
+ let end = -1;
105
+ for (let i = start; i < text.length; i++) {
106
+ const char = text[i];
107
+ if (inString) {
108
+ if (escaped) {
109
+ escaped = false;
110
+ }
111
+ else if (char === "\\") {
112
+ escaped = true;
113
+ }
114
+ else if (char === "\"") {
115
+ inString = false;
116
+ }
117
+ continue;
118
+ }
119
+ if (char === "\"") {
120
+ inString = true;
121
+ continue;
122
+ }
123
+ if (char === "{")
124
+ depth++;
125
+ if (char === "}")
126
+ depth--;
127
+ if (depth === 0) {
128
+ end = i + 1;
129
+ break;
130
+ }
131
+ }
132
+ if (end < 0)
133
+ break;
134
+ values.push(text.slice(start, end));
135
+ cursor = end;
136
+ }
137
+ return values;
138
+ }
139
+ export function parseDecision(raw) {
140
+ const trimmed = raw.trim();
141
+ const unwrapped = trimmed.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
142
+ const extracted = [...extractJsonObjects(trimmed), ...extractJsonObjects(unwrapped)];
143
+ const candidates = [trimmed, unwrapped, ...extracted].filter((candidate) => Boolean(candidate));
144
+ for (const candidate of candidates.reverse()) {
145
+ try {
146
+ return JSON.parse(candidate);
147
+ }
148
+ catch {
149
+ continue;
150
+ }
151
+ }
152
+ throw new Error(`OpenAI OAuth provider returned non-JSON decision text: ${trimmed.slice(0, 500)}`);
153
+ }
154
+ export class OpenAIOAuthProvider {
155
+ auth = new OpenAIOAuthManager();
156
+ getModelName() {
157
+ return MODEL;
158
+ }
159
+ async next(session, prompt) {
160
+ const accessToken = await this.auth.getAccessToken();
161
+ const accountId = extractAccountId(accessToken);
162
+ const decisionSchema = {
163
+ type: "object",
164
+ additionalProperties: false,
165
+ properties: {
166
+ rationale: { type: "string" },
167
+ action: {
168
+ anyOf: [
169
+ {
170
+ type: "object",
171
+ additionalProperties: false,
172
+ properties: {
173
+ type: { type: "string", const: "clarify" },
174
+ question: { type: "string" },
175
+ },
176
+ required: ["type", "question"],
177
+ },
178
+ {
179
+ type: "object",
180
+ additionalProperties: false,
181
+ properties: {
182
+ type: { type: "string", const: "tool" },
183
+ toolCall: {
184
+ anyOf: [
185
+ toolCallSchema("list_files", {}),
186
+ toolCallSchema("read_file", { path: { type: "string" } }, ["path"]),
187
+ toolCallSchema("search_files", { query: { type: "string" } }, ["query"]),
188
+ toolCallSchema("write_file", {
189
+ path: { type: "string" },
190
+ content: { type: "string" },
191
+ }, ["path", "content"]),
192
+ toolCallSchema("edit_file", {
193
+ path: { type: "string" },
194
+ oldText: { type: "string" },
195
+ newText: { type: "string" },
196
+ }, ["path", "oldText", "newText"]),
197
+ toolCallSchema("run_command", { command: { type: "string" } }, ["command"]),
198
+ toolCallSchema("write_memory", {
199
+ category: { type: "string" },
200
+ title: { type: "string" },
201
+ content: { type: "string" },
202
+ }, ["category", "title", "content"]),
203
+ toolCallSchema("spawn_subagents", {
204
+ agents: {
205
+ type: "array",
206
+ items: {
207
+ type: "object",
208
+ additionalProperties: false,
209
+ properties: {
210
+ name: { type: "string" },
211
+ task: { type: "string" },
212
+ },
213
+ required: ["name", "task"],
214
+ },
215
+ },
216
+ }, ["agents"]),
217
+ toolCallSchema("wait_subagents", {}),
218
+ toolCallSchema("finish", {
219
+ answer: { type: "string" },
220
+ }, ["answer"]),
221
+ ],
222
+ },
223
+ },
224
+ required: ["type", "toolCall"],
225
+ },
226
+ ],
227
+ },
228
+ },
229
+ required: ["rationale", "action"],
230
+ };
231
+ const body = {
232
+ model: MODEL,
233
+ store: false,
234
+ stream: true,
235
+ instructions: providerInstructions(session.depth),
236
+ input: [
237
+ {
238
+ role: "user",
239
+ content: prompt,
240
+ },
241
+ ],
242
+ text: {
243
+ verbosity: "low",
244
+ format: {
245
+ type: "json_schema",
246
+ name: "decision",
247
+ schema: decisionSchema,
248
+ },
249
+ },
250
+ tool_choice: "auto",
251
+ parallel_tool_calls: false,
252
+ };
253
+ const headers = new Headers();
254
+ headers.set("Authorization", `Bearer ${accessToken}`);
255
+ headers.set("chatgpt-account-id", accountId);
256
+ headers.set("OpenAI-Beta", "responses=experimental");
257
+ headers.set("originator", "pi");
258
+ headers.set("User-Agent", `mini-codex (${process.platform} ${process.arch})`);
259
+ headers.set("accept", "text/event-stream");
260
+ headers.set("content-type", "application/json");
261
+ const response = await fetch(BASE_URL, {
262
+ method: "POST",
263
+ headers,
264
+ body: JSON.stringify(body),
265
+ });
266
+ if (!response.ok) {
267
+ throw new Error(`OpenAI OAuth provider failed: HTTP ${response.status} ${await response.text()}`);
268
+ }
269
+ const raw = await parseSSE(response);
270
+ if (!raw.trim())
271
+ throw new Error("OpenAI OAuth provider returned no decision text");
272
+ return {
273
+ ...parseDecision(raw),
274
+ model: MODEL,
275
+ rawResponse: raw,
276
+ };
277
+ }
278
+ }
@@ -0,0 +1,8 @@
1
+ import type { ModelDecision, Session } from "../types.js";
2
+ import type { ModelProvider } from "./base.js";
3
+ export declare class TestFixtureProvider implements ModelProvider {
4
+ private fixturePromise;
5
+ private fixture;
6
+ getModelName(): string;
7
+ next(session: Session): Promise<ModelDecision>;
8
+ }
@@ -0,0 +1,36 @@
1
+ import fs from "node:fs/promises";
2
+ async function loadFixture() {
3
+ const fixturePath = process.env.MINI_CODEX_TEST_FIXTURE;
4
+ if (!fixturePath) {
5
+ throw new Error("MINI_CODEX_TEST_FIXTURE is required for the test-fixture provider");
6
+ }
7
+ const raw = await fs.readFile(fixturePath, "utf8");
8
+ const parsed = JSON.parse(raw);
9
+ if (!Array.isArray(parsed.decisions) || !parsed.decisions.length) {
10
+ throw new Error("Test fixture must include a non-empty decisions array");
11
+ }
12
+ return parsed;
13
+ }
14
+ export class TestFixtureProvider {
15
+ fixturePromise = null;
16
+ async fixture() {
17
+ if (!this.fixturePromise)
18
+ this.fixturePromise = loadFixture();
19
+ return this.fixturePromise;
20
+ }
21
+ getModelName() {
22
+ return process.env.MINI_CODEX_TEST_MODEL || "test-fixture-model";
23
+ }
24
+ async next(session) {
25
+ const fixture = await this.fixture();
26
+ const decision = fixture.decisions[session.steps.length];
27
+ if (!decision) {
28
+ throw new Error(`No scripted decision available for step ${session.steps.length + 1}`);
29
+ }
30
+ return {
31
+ ...decision,
32
+ model: decision.model || fixture.model || this.getModelName(),
33
+ rawResponse: decision.rawResponse || JSON.stringify(decision),
34
+ };
35
+ }
36
+ }
@@ -0,0 +1,9 @@
1
+ import type { Session, ToolCall, ToolResult } from "./types.js";
2
+ export declare class AgentRuntime {
3
+ private readonly session;
4
+ private readonly subagents;
5
+ constructor(session: Session);
6
+ refreshMemory(task: string): Promise<void>;
7
+ execute(call: ToolCall): Promise<ToolResult>;
8
+ cleanup(): Promise<void>;
9
+ }
@@ -0,0 +1,99 @@
1
+ import { loadProjectMemory, writeProjectMemory } from "./memory.js";
2
+ import { SubagentManager } from "./subagents.js";
3
+ import { runTool as runRepoTool } from "./tools.js";
4
+ function summarizeAgents(agents) {
5
+ return agents
6
+ .map((agent) => {
7
+ const status = `[${agent.status}] ${agent.name}`;
8
+ if (agent.status === "completed") {
9
+ return `${status}: ${agent.answer || agent.summary || "completed"}`;
10
+ }
11
+ return `${status}: ${agent.error || agent.task}`;
12
+ })
13
+ .join("\n");
14
+ }
15
+ export class AgentRuntime {
16
+ session;
17
+ subagents;
18
+ constructor(session) {
19
+ this.session = session;
20
+ this.subagents = new SubagentManager(session.repoPath, session.id, session.depth);
21
+ }
22
+ async refreshMemory(task) {
23
+ const memory = await loadProjectMemory(this.session.repoPath, task);
24
+ this.session.memorySummary = memory.summary;
25
+ this.session.memoryNotes = memory.relevantNotes;
26
+ }
27
+ async execute(call) {
28
+ switch (call.tool) {
29
+ case "write_memory": {
30
+ if (this.session.depth > 0) {
31
+ return { ok: false, summary: "Memory writes are only allowed in the main agent", error: "child agent" };
32
+ }
33
+ const category = String(call.args.category || "");
34
+ const title = String(call.args.title || "");
35
+ const content = String(call.args.content || "");
36
+ if (!category || !title || !content) {
37
+ return { ok: false, summary: "Missing memory args", error: "category, title, and content are required" };
38
+ }
39
+ const note = await writeProjectMemory(this.session.repoPath, { category, title, content });
40
+ await this.refreshMemory(this.session.task);
41
+ return {
42
+ ok: true,
43
+ summary: `Saved project memory ${note.title}`,
44
+ output: `${note.path}\n${note.preview}`,
45
+ };
46
+ }
47
+ case "spawn_subagents": {
48
+ const specs = Array.isArray(call.args.agents) ? call.args.agents : [];
49
+ const normalized = specs
50
+ .map((spec) => ({
51
+ name: String(spec.name || "").trim(),
52
+ task: String(spec.task || "").trim(),
53
+ }))
54
+ .filter((spec) => spec.name && spec.task);
55
+ if (!normalized.length) {
56
+ return { ok: false, summary: "Missing sub-agent specs", error: "agents must include name and task" };
57
+ }
58
+ try {
59
+ const spawned = await this.subagents.spawn(normalized);
60
+ this.session.subagents.push(...spawned);
61
+ return {
62
+ ok: true,
63
+ summary: `Spawned ${spawned.length} sub-agent(s)`,
64
+ output: spawned.map((agent) => `${agent.name} -> ${agent.task}`).join("\n"),
65
+ };
66
+ }
67
+ catch (error) {
68
+ return {
69
+ ok: false,
70
+ summary: "Failed to spawn sub-agents",
71
+ error: error instanceof Error ? error.message : String(error),
72
+ };
73
+ }
74
+ }
75
+ case "wait_subagents": {
76
+ const results = await this.subagents.wait();
77
+ if (!results.length)
78
+ return { ok: true, summary: "No active sub-agents" };
79
+ for (const result of results) {
80
+ const index = this.session.subagents.findIndex((agent) => agent.id === result.id);
81
+ if (index >= 0)
82
+ this.session.subagents[index] = result;
83
+ else
84
+ this.session.subagents.push(result);
85
+ }
86
+ return {
87
+ ok: true,
88
+ summary: `Collected ${results.length} sub-agent result(s)`,
89
+ output: summarizeAgents(results),
90
+ };
91
+ }
92
+ default:
93
+ return runRepoTool(this.session.repoPath, call);
94
+ }
95
+ }
96
+ async cleanup() {
97
+ await this.subagents.cleanupAll();
98
+ }
99
+ }
@@ -0,0 +1,6 @@
1
+ export interface SelectedSkill {
2
+ name: string;
3
+ path: string;
4
+ content: string;
5
+ }
6
+ export declare function selectSkill(task: string, repoPath: string): Promise<SelectedSkill | null>;
package/dist/skills.js ADDED
@@ -0,0 +1,37 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ const SKILL_RULES = [
4
+ {
5
+ name: "oauth-provider-development",
6
+ file: "oauth-provider-development/SKILL.md",
7
+ keywords: ["oauth", "auth", "token", "provider", "login", "gpt", "openai", "model"],
8
+ },
9
+ {
10
+ name: "testing-and-debugging",
11
+ file: "testing-and-debugging/SKILL.md",
12
+ keywords: ["test", "debug", "fix", "failing", "error", "reproduce", "validate"],
13
+ },
14
+ {
15
+ name: "harness-development",
16
+ file: "harness-development/SKILL.md",
17
+ keywords: ["loop", "cli", "tool", "runtime", "harness", "report", "edit", "repo"],
18
+ },
19
+ ];
20
+ export async function selectSkill(task, repoPath) {
21
+ const lower = task.toLowerCase();
22
+ const match = SKILL_RULES.find((rule) => rule.keywords.some((keyword) => lower.includes(keyword)));
23
+ if (!match)
24
+ return null;
25
+ const skillPath = path.join(repoPath, ".codex", "skills", match.file);
26
+ try {
27
+ const content = await fs.readFile(skillPath, "utf8");
28
+ return {
29
+ name: match.name,
30
+ path: skillPath,
31
+ content,
32
+ };
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
@@ -0,0 +1,16 @@
1
+ import type { SubagentRecord } from "./types.js";
2
+ export declare class SubagentManager {
3
+ private readonly repoPath;
4
+ private readonly sessionId;
5
+ private readonly depth;
6
+ private active;
7
+ private rootPathPromise;
8
+ constructor(repoPath: string, sessionId: string, depth: number);
9
+ private rootPath;
10
+ spawn(specs: Array<{
11
+ name: string;
12
+ task: string;
13
+ }>): Promise<SubagentRecord[]>;
14
+ wait(): Promise<SubagentRecord[]>;
15
+ cleanupAll(): Promise<void>;
16
+ }