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,196 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { execFile, spawn } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+ import { fileURLToPath } from "node:url";
6
+ const execFileAsync = promisify(execFile);
7
+ const MAX_PARALLEL_SUBAGENTS = 2;
8
+ const MAX_SUBAGENT_DEPTH = 1;
9
+ const CHILD_MAX_STEPS = 8;
10
+ const WAIT_TIMEOUT_MS = 30_000;
11
+ const CLI_ENTRY = fileURLToPath(new URL("./cli.ts", import.meta.url));
12
+ function slugify(value) {
13
+ return value
14
+ .toLowerCase()
15
+ .replace(/[^a-z0-9]+/g, "-")
16
+ .replace(/^-+|-+$/g, "")
17
+ .slice(0, 40) || "subagent";
18
+ }
19
+ function touchedFiles(session) {
20
+ const files = new Set();
21
+ for (const step of session.steps) {
22
+ if (step.action.type !== "tool")
23
+ continue;
24
+ const p = step.action.toolCall.args.path;
25
+ if (typeof p === "string" && (step.action.toolCall.tool === "write_file" || step.action.toolCall.tool === "edit_file")) {
26
+ files.add(p);
27
+ }
28
+ }
29
+ return [...files];
30
+ }
31
+ function finalAnswer(session) {
32
+ const lastStep = session.steps.at(-1);
33
+ if (!lastStep)
34
+ return undefined;
35
+ if (lastStep.action.type === "clarify") {
36
+ return lastStep.userResponse ? undefined : lastStep.action.question;
37
+ }
38
+ const answer = lastStep.action.toolCall.args.answer;
39
+ return typeof answer === "string" && answer.trim() ? answer.trim() : undefined;
40
+ }
41
+ async function maybeCopyMemory(sourceRepoPath, worktreePath) {
42
+ const source = path.join(sourceRepoPath, ".codex", "memory");
43
+ const target = path.join(worktreePath, ".codex", "memory");
44
+ try {
45
+ await fs.access(source);
46
+ await fs.mkdir(path.dirname(target), { recursive: true });
47
+ await fs.cp(source, target, { recursive: true, force: true });
48
+ }
49
+ catch {
50
+ // no memory to copy
51
+ }
52
+ }
53
+ async function gitRoot(repoPath) {
54
+ const { stdout } = await execFileAsync("git", ["-C", repoPath, "rev-parse", "--show-toplevel"]);
55
+ return stdout.trim();
56
+ }
57
+ export class SubagentManager {
58
+ repoPath;
59
+ sessionId;
60
+ depth;
61
+ active = new Map();
62
+ rootPathPromise = null;
63
+ constructor(repoPath, sessionId, depth) {
64
+ this.repoPath = repoPath;
65
+ this.sessionId = sessionId;
66
+ this.depth = depth;
67
+ }
68
+ async rootPath() {
69
+ if (!this.rootPathPromise)
70
+ this.rootPathPromise = gitRoot(this.repoPath);
71
+ return this.rootPathPromise;
72
+ }
73
+ async spawn(specs) {
74
+ if (this.depth >= MAX_SUBAGENT_DEPTH) {
75
+ throw new Error(`Sub-agent depth limit reached (${MAX_SUBAGENT_DEPTH})`);
76
+ }
77
+ const availableSlots = Math.max(0, MAX_PARALLEL_SUBAGENTS - this.active.size);
78
+ if (specs.length > availableSlots) {
79
+ throw new Error(`Cannot spawn ${specs.length} sub-agents with ${availableSlots} slot${availableSlots === 1 ? "" : "s"} available (limit ${MAX_PARALLEL_SUBAGENTS}, active ${this.active.size})`);
80
+ }
81
+ const root = await this.rootPath();
82
+ const spawned = [];
83
+ for (const spec of specs) {
84
+ const id = `${slugify(spec.name)}-${Date.now().toString(36)}`;
85
+ const worktreePath = path.join(root, ".codex", "subagents", this.sessionId, id);
86
+ await fs.mkdir(path.dirname(worktreePath), { recursive: true });
87
+ await execFileAsync("git", ["-C", root, "worktree", "add", "--detach", worktreePath, "HEAD"]);
88
+ await maybeCopyMemory(this.repoPath, worktreePath);
89
+ const record = {
90
+ id,
91
+ name: spec.name.trim() || id,
92
+ task: spec.task.trim(),
93
+ status: "running",
94
+ worktreePath,
95
+ };
96
+ const promise = new Promise((resolve) => {
97
+ const child = spawn("bun", [
98
+ "run",
99
+ CLI_ENTRY,
100
+ "run",
101
+ spec.task,
102
+ "--repo",
103
+ worktreePath,
104
+ "--max-steps",
105
+ String(CHILD_MAX_STEPS),
106
+ "--json",
107
+ "--depth",
108
+ String(this.depth + 1),
109
+ ], {
110
+ cwd: root,
111
+ env: process.env,
112
+ stdio: ["ignore", "pipe", "pipe"],
113
+ });
114
+ let stdout = "";
115
+ let stderr = "";
116
+ child.stdout.on("data", (chunk) => {
117
+ stdout += String(chunk);
118
+ });
119
+ child.stderr.on("data", (chunk) => {
120
+ stderr += String(chunk);
121
+ });
122
+ const timer = setTimeout(() => {
123
+ child.kill("SIGTERM");
124
+ }, WAIT_TIMEOUT_MS);
125
+ child.on("close", async (code) => {
126
+ clearTimeout(timer);
127
+ let result = { ...record };
128
+ try {
129
+ if (code === 0) {
130
+ const session = JSON.parse(stdout);
131
+ const changedFiles = touchedFiles(session);
132
+ result = {
133
+ ...result,
134
+ status: "completed",
135
+ summary: session.steps.at(-1)?.result?.summary || "Completed",
136
+ answer: finalAnswer(session),
137
+ changedFiles,
138
+ };
139
+ }
140
+ else {
141
+ result = {
142
+ ...result,
143
+ status: "failed",
144
+ error: stderr.trim() || stdout.trim() || `Sub-agent exited with code ${code}`,
145
+ };
146
+ }
147
+ }
148
+ catch (error) {
149
+ result = {
150
+ ...result,
151
+ status: "failed",
152
+ error: error instanceof Error ? error.message : String(error),
153
+ };
154
+ }
155
+ try {
156
+ if (!result.changedFiles?.length) {
157
+ await execFileAsync("git", ["-C", root, "worktree", "remove", "--force", worktreePath]);
158
+ }
159
+ }
160
+ catch {
161
+ // keep worktree if cleanup fails
162
+ }
163
+ resolve(result);
164
+ });
165
+ });
166
+ this.active.set(id, { record, promise });
167
+ spawned.push(record);
168
+ }
169
+ return spawned;
170
+ }
171
+ async wait() {
172
+ const entries = [...this.active.values()];
173
+ if (!entries.length)
174
+ return [];
175
+ const settled = await Promise.all(entries.map((entry) => entry.promise));
176
+ for (const agent of settled) {
177
+ this.active.delete(agent.id);
178
+ }
179
+ return settled;
180
+ }
181
+ async cleanupAll() {
182
+ const root = await this.rootPath().catch(() => null);
183
+ if (!root)
184
+ return;
185
+ const entries = [...this.active.values()];
186
+ this.active.clear();
187
+ for (const entry of entries) {
188
+ try {
189
+ await execFileAsync("git", ["-C", root, "worktree", "remove", "--force", entry.record.worktreePath]);
190
+ }
191
+ catch {
192
+ // ignore cleanup failure
193
+ }
194
+ }
195
+ }
196
+ }
@@ -0,0 +1,29 @@
1
+ import type { ModelAction, ToolCall, ToolResult } from "./types.js";
2
+ export declare class TerminalRenderer {
3
+ private readonly enabled;
4
+ private readonly showLlmDetails;
5
+ private readonly showToolResults;
6
+ constructor(enabled: boolean, showLlmDetails?: boolean, showToolResults?: boolean);
7
+ private line;
8
+ step(index: number, name: string): void;
9
+ llmStart(model: string, prompt?: string): void;
10
+ llmAction(action: ModelAction, stepName?: string): void;
11
+ toolStart(call: ToolCall): void;
12
+ toolResult(result: ToolResult): void;
13
+ clarifyPrompt(question: string): void;
14
+ clarifyAnswer(answer: string): void;
15
+ finalAnswer(answer: string): void;
16
+ warning(message: string): void;
17
+ }
18
+ export declare class SessionLogger {
19
+ private readonly filePath;
20
+ private llmExchanges;
21
+ private toolCalls;
22
+ private clarifyCount;
23
+ constructor(filePath: string);
24
+ start(sessionId: string, task: string, repoPath: string): Promise<void>;
25
+ llm(stepIndex: number, stepName: string, prompt: string, rawResponse: string, model: string): Promise<void>;
26
+ tool(call: ToolCall): Promise<void>;
27
+ clarify(question: string, answer?: string): Promise<void>;
28
+ final(answer: string, totalSteps: number): Promise<void>;
29
+ }
@@ -0,0 +1,242 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ const RESET = "\x1b[0m";
4
+ const BOLD = "\x1b[1m";
5
+ const DIM = "\x1b[2m";
6
+ const BLUE = "\x1b[34m";
7
+ const MAGENTA = "\x1b[35m";
8
+ const CYAN = "\x1b[36m";
9
+ const GREEN = "\x1b[32m";
10
+ const YELLOW = "\x1b[33m";
11
+ const RED = "\x1b[31m";
12
+ async function appendLogLine(filePath, text) {
13
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
14
+ await fs.appendFile(filePath, `${text}\n`, "utf8");
15
+ }
16
+ function stats(label, text) {
17
+ const chars = text.length;
18
+ const lines = text ? text.split("\n").length : 0;
19
+ const bytes = Buffer.byteLength(text, "utf8");
20
+ return [`${label}_chars: ${chars}`, `${label}_lines: ${lines}`, `${label}_bytes: ${bytes}`];
21
+ }
22
+ function formatJsonIfPossible(text) {
23
+ try {
24
+ return JSON.stringify(JSON.parse(text), null, 2);
25
+ }
26
+ catch {
27
+ const objects = extractJsonObjects(text);
28
+ if (!objects.length)
29
+ return text;
30
+ return objects
31
+ .map((value, index) => [`JSON_OBJECT_${index + 1}`, JSON.stringify(value, null, 2)].join("\n"))
32
+ .join(`\n${"-".repeat(40)}\n`);
33
+ }
34
+ }
35
+ function extractJsonObjects(text) {
36
+ const values = [];
37
+ let cursor = 0;
38
+ while (cursor < text.length) {
39
+ const start = text.indexOf("{", cursor);
40
+ if (start < 0)
41
+ break;
42
+ let depth = 0;
43
+ let inString = false;
44
+ let escaped = false;
45
+ let end = -1;
46
+ for (let i = start; i < text.length; i++) {
47
+ const char = text[i];
48
+ if (inString) {
49
+ if (escaped) {
50
+ escaped = false;
51
+ }
52
+ else if (char === "\\") {
53
+ escaped = true;
54
+ }
55
+ else if (char === "\"") {
56
+ inString = false;
57
+ }
58
+ continue;
59
+ }
60
+ if (char === "\"") {
61
+ inString = true;
62
+ continue;
63
+ }
64
+ if (char === "{")
65
+ depth++;
66
+ if (char === "}")
67
+ depth--;
68
+ if (depth === 0) {
69
+ end = i + 1;
70
+ break;
71
+ }
72
+ }
73
+ if (end < 0)
74
+ break;
75
+ const candidate = text.slice(start, end);
76
+ try {
77
+ values.push(JSON.parse(candidate));
78
+ }
79
+ catch {
80
+ // ignore invalid object candidate and continue scanning
81
+ }
82
+ cursor = end;
83
+ }
84
+ return values;
85
+ }
86
+ function color(enabled, code, text) {
87
+ return enabled ? `${code}${text}${RESET}` : text;
88
+ }
89
+ export class TerminalRenderer {
90
+ enabled;
91
+ showLlmDetails;
92
+ showToolResults;
93
+ constructor(enabled, showLlmDetails = false, showToolResults = false) {
94
+ this.enabled = enabled;
95
+ this.showLlmDetails = showLlmDetails;
96
+ this.showToolResults = showToolResults;
97
+ }
98
+ line(text = "") {
99
+ process.stdout.write(`${text}\n`);
100
+ }
101
+ step(index, name) {
102
+ this.line(color(this.enabled, `${BOLD}${CYAN}`, `\n🧭 Step ${index} · ${name}`));
103
+ }
104
+ llmStart(model, prompt) {
105
+ this.line(color(this.enabled, `${BOLD}${BLUE}`, `🤖 [LLM:${model}] Calling remote model...`));
106
+ if (!prompt?.trim())
107
+ return;
108
+ const lines = prompt.split("\n").length;
109
+ const chars = prompt.length;
110
+ if (this.showLlmDetails) {
111
+ this.line(color(this.enabled, `${BOLD}${BLUE}`, `[LLM] Prompt (${lines} lines, ${chars} chars)`));
112
+ this.line(color(this.enabled, DIM, prompt));
113
+ return;
114
+ }
115
+ this.line(color(this.enabled, DIM, `🤖 Prompt collapsed by default (${lines} lines, ${chars} chars). Full prompt/response logging is still enabled. Use --show-llm-details to expand.`));
116
+ }
117
+ llmAction(action, stepName) {
118
+ if (stepName) {
119
+ this.line(color(this.enabled, `${BOLD}${CYAN}`, `🧭 Resolved Step · ${stepName}`));
120
+ }
121
+ if (action.type === "clarify") {
122
+ this.line(color(this.enabled, `${BOLD}${MAGENTA}`, `🤖 [LLM] Clarification requested: ${action.question}`));
123
+ return;
124
+ }
125
+ this.line(color(this.enabled, `${BOLD}${BLUE}`, `🤖 [LLM] Next action: ${action.toolCall.tool}`));
126
+ this.line(color(this.enabled, DIM, ` args: ${JSON.stringify(action.toolCall.args)}`));
127
+ }
128
+ toolStart(call) {
129
+ this.line(color(this.enabled, `${BOLD}${YELLOW}`, `🛠️ [TOOL] ${call.tool}`));
130
+ this.line(color(this.enabled, DIM, ` args: ${JSON.stringify(call.args)}`));
131
+ }
132
+ toolResult(result) {
133
+ const tone = result.ok ? `${BOLD}${GREEN}` : `${BOLD}${RED}`;
134
+ this.line(color(this.enabled, tone, `📦 [RESULT] ${result.summary}`));
135
+ if (this.showToolResults && result.output)
136
+ this.line(color(this.enabled, DIM, result.output));
137
+ if (this.showToolResults && result.error)
138
+ this.line(color(this.enabled, `${BOLD}${RED}`, result.error));
139
+ }
140
+ clarifyPrompt(question) {
141
+ this.line(color(this.enabled, `${BOLD}${MAGENTA}`, `💬 [CLARIFY] ${question}`));
142
+ }
143
+ clarifyAnswer(answer) {
144
+ this.line(color(this.enabled, `${BOLD}${MAGENTA}`, `🧑 [YOU] ${answer}`));
145
+ }
146
+ finalAnswer(answer) {
147
+ this.line(color(this.enabled, `${BOLD}${GREEN}`, `\n✅ Answer\n${answer}`));
148
+ }
149
+ warning(message) {
150
+ this.line(color(this.enabled, `${BOLD}${YELLOW}`, `⚠️ [WARN] ${message}`));
151
+ }
152
+ }
153
+ export class SessionLogger {
154
+ filePath;
155
+ llmExchanges = 0;
156
+ toolCalls = 0;
157
+ clarifyCount = 0;
158
+ constructor(filePath) {
159
+ this.filePath = filePath;
160
+ }
161
+ async start(sessionId, task, repoPath) {
162
+ await appendLogLine(this.filePath, [
163
+ "=".repeat(88),
164
+ "SESSION START",
165
+ "-".repeat(88),
166
+ `session_id: ${sessionId}`,
167
+ `task: ${task}`,
168
+ `repo: ${repoPath}`,
169
+ "=".repeat(88),
170
+ "",
171
+ ].join("\n"));
172
+ }
173
+ async llm(stepIndex, stepName, prompt, rawResponse, model) {
174
+ this.llmExchanges += 1;
175
+ const formattedResponse = formatJsonIfPossible(rawResponse);
176
+ await appendLogLine(this.filePath, [
177
+ "=".repeat(88),
178
+ `🧭 STEP ${stepIndex} · ${stepName}`,
179
+ "🤖 LLM EXCHANGE",
180
+ "-".repeat(88),
181
+ `step: ${stepIndex}`,
182
+ `step_name: ${stepName}`,
183
+ `model: ${model}`,
184
+ ...stats("prompt", prompt),
185
+ "-".repeat(88),
186
+ "PROMPT",
187
+ "-".repeat(88),
188
+ prompt,
189
+ "-".repeat(88),
190
+ ...stats("response", formattedResponse),
191
+ "-".repeat(88),
192
+ "RESPONSE",
193
+ "-".repeat(88),
194
+ formattedResponse,
195
+ "=".repeat(88),
196
+ "",
197
+ ].join("\n"));
198
+ }
199
+ async tool(call) {
200
+ this.toolCalls += 1;
201
+ await appendLogLine(this.filePath, [
202
+ "=".repeat(88),
203
+ "🛠️ TOOL CALL",
204
+ "-".repeat(88),
205
+ `tool: ${call.tool}`,
206
+ `args: ${JSON.stringify(call.args)}`,
207
+ "=".repeat(88),
208
+ "",
209
+ ].join("\n"));
210
+ }
211
+ async clarify(question, answer) {
212
+ this.clarifyCount += 1;
213
+ await appendLogLine(this.filePath, [
214
+ "=".repeat(88),
215
+ "💬 CLARIFICATION",
216
+ "-".repeat(88),
217
+ `question: ${question}`,
218
+ `answer: ${answer || ""}`,
219
+ "=".repeat(88),
220
+ "",
221
+ ].join("\n"));
222
+ }
223
+ async final(answer, totalSteps) {
224
+ await appendLogLine(this.filePath, [
225
+ "=".repeat(88),
226
+ "📘 SESSION SUMMARY",
227
+ "-".repeat(88),
228
+ `total_steps: ${totalSteps}`,
229
+ `llm_exchanges: ${this.llmExchanges}`,
230
+ `tool_calls: ${this.toolCalls}`,
231
+ `clarifications: ${this.clarifyCount}`,
232
+ "=".repeat(88),
233
+ "",
234
+ "=".repeat(88),
235
+ "✅ FINAL ANSWER",
236
+ "-".repeat(88),
237
+ answer,
238
+ "=".repeat(88),
239
+ "",
240
+ ].join("\n"));
241
+ }
242
+ }
@@ -0,0 +1,2 @@
1
+ import type { ToolCall, ToolResult } from "./types.js";
2
+ export declare function runTool(repoPath: string, call: ToolCall): Promise<ToolResult>;
package/dist/tools.js ADDED
@@ -0,0 +1,136 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { exec } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+ const execAsync = promisify(exec);
6
+ const MAX_OUTPUT = 12000;
7
+ const MAX_MATCHES = 50;
8
+ const SKIPPED_DIRECTORIES = new Set([
9
+ ".git",
10
+ "node_modules",
11
+ "dist",
12
+ path.join(".codex", "logs"),
13
+ path.join(".codex", "subagents"),
14
+ ]);
15
+ const DANGEROUS_COMMAND = /\b(rm\s+-rf|sudo|shutdown|reboot|mkfs|dd\s+if=|curl\b|wget\b|ssh\b|scp\b|sftp\b|nc\b|ncat\b|telnet\b|git\s+push\b|git\s+reset\s+--hard\b)\b/;
16
+ function defaultShell() {
17
+ if (process.platform === "win32") {
18
+ return process.env.ComSpec || "cmd.exe";
19
+ }
20
+ return process.env.SHELL || "/bin/sh";
21
+ }
22
+ function insideRepo(repoPath, targetPath) {
23
+ const repo = path.resolve(repoPath);
24
+ const full = path.resolve(repo, targetPath);
25
+ if (full !== repo && !full.startsWith(repo + path.sep)) {
26
+ throw new Error(`Path escapes repo: ${targetPath}`);
27
+ }
28
+ return full;
29
+ }
30
+ function trim(text) {
31
+ return text.length > MAX_OUTPUT ? text.slice(0, MAX_OUTPUT) + "\n...[truncated]" : text;
32
+ }
33
+ async function walk(dir, base = "") {
34
+ const entries = await fs.readdir(dir, { withFileTypes: true });
35
+ const out = [];
36
+ for (const entry of entries) {
37
+ const rel = path.join(base, entry.name);
38
+ if (SKIPPED_DIRECTORIES.has(rel) || (base === "" && SKIPPED_DIRECTORIES.has(entry.name)))
39
+ continue;
40
+ const full = path.join(dir, entry.name);
41
+ if (entry.isDirectory())
42
+ out.push(...(await walk(full, rel)));
43
+ else
44
+ out.push(rel);
45
+ }
46
+ return out;
47
+ }
48
+ export async function runTool(repoPath, call) {
49
+ try {
50
+ switch (call.tool) {
51
+ case "list_files": {
52
+ const files = await walk(repoPath);
53
+ return { ok: true, summary: `Found ${files.length} files`, output: files.slice(0, 300).join("\n") };
54
+ }
55
+ case "read_file": {
56
+ const file = String(call.args.path || "");
57
+ const content = await fs.readFile(insideRepo(repoPath, file), "utf8");
58
+ return { ok: true, summary: `Read ${file}`, output: trim(content) };
59
+ }
60
+ case "search_files": {
61
+ const query = String(call.args.query || "").toLowerCase();
62
+ if (!query)
63
+ return { ok: false, summary: "Missing query", error: "query is required" };
64
+ const files = await walk(repoPath);
65
+ const matches = [];
66
+ for (const rel of files) {
67
+ const content = await fs.readFile(insideRepo(repoPath, rel), "utf8").catch(() => "");
68
+ const lines = content.split(/\r?\n/);
69
+ for (let i = 0; i < lines.length; i++) {
70
+ if (lines[i].toLowerCase().includes(query)) {
71
+ matches.push(`${rel}:${i + 1}: ${lines[i]}`);
72
+ }
73
+ if (matches.length >= MAX_MATCHES)
74
+ break;
75
+ }
76
+ if (matches.length >= MAX_MATCHES)
77
+ break;
78
+ }
79
+ return { ok: true, summary: `Found ${matches.length} matches`, output: matches.join("\n") || "No matches" };
80
+ }
81
+ case "write_file": {
82
+ const file = String(call.args.path || "");
83
+ const content = String(call.args.content || "");
84
+ const full = insideRepo(repoPath, file);
85
+ await fs.mkdir(path.dirname(full), { recursive: true });
86
+ await fs.writeFile(full, content, "utf8");
87
+ return { ok: true, summary: `Wrote ${file}` };
88
+ }
89
+ case "edit_file": {
90
+ const file = String(call.args.path || "");
91
+ const oldText = String(call.args.oldText || "");
92
+ const newText = String(call.args.newText || "");
93
+ if (!file || !oldText) {
94
+ return { ok: false, summary: "Missing edit args", error: "path and oldText are required" };
95
+ }
96
+ const full = insideRepo(repoPath, file);
97
+ const current = await fs.readFile(full, "utf8");
98
+ if (!current.includes(oldText)) {
99
+ return { ok: false, summary: `Target text not found in ${file}`, error: "oldText not found" };
100
+ }
101
+ const updated = current.replace(oldText, newText);
102
+ await fs.writeFile(full, updated, "utf8");
103
+ return { ok: true, summary: `Edited ${file}` };
104
+ }
105
+ case "run_command": {
106
+ const command = String(call.args.command || "");
107
+ if (!command)
108
+ return { ok: false, summary: "Missing command", error: "command is required" };
109
+ if (DANGEROUS_COMMAND.test(command)) {
110
+ return { ok: false, summary: "Blocked dangerous command", error: command };
111
+ }
112
+ const { stdout, stderr } = await execAsync(command, {
113
+ cwd: repoPath,
114
+ shell: defaultShell(),
115
+ timeout: 20000,
116
+ maxBuffer: 1024 * 1024,
117
+ env: process.env,
118
+ });
119
+ return { ok: true, summary: `Ran ${command}`, output: trim(`${stdout}${stderr}`.trim()) };
120
+ }
121
+ case "write_memory":
122
+ case "spawn_subagents":
123
+ case "wait_subagents":
124
+ return { ok: false, summary: `Tool ${call.tool} must be handled by the agent runtime`, error: call.tool };
125
+ case "finish":
126
+ return { ok: true, summary: "Finished" };
127
+ }
128
+ }
129
+ catch (error) {
130
+ return {
131
+ ok: false,
132
+ summary: `Tool ${call.tool} failed`,
133
+ error: error instanceof Error ? error.message : String(error),
134
+ };
135
+ }
136
+ }
@@ -0,0 +1,78 @@
1
+ export type ToolName = "list_files" | "read_file" | "search_files" | "write_file" | "edit_file" | "run_command" | "write_memory" | "spawn_subagents" | "wait_subagents" | "finish";
2
+ export interface ToolCall {
3
+ tool: ToolName;
4
+ args: Record<string, unknown>;
5
+ }
6
+ export interface ClarifyAction {
7
+ type: "clarify";
8
+ question: string;
9
+ }
10
+ export interface ToolAction {
11
+ type: "tool";
12
+ toolCall: ToolCall;
13
+ }
14
+ export type ModelAction = ClarifyAction | ToolAction;
15
+ export interface ToolResult {
16
+ ok: boolean;
17
+ summary: string;
18
+ output?: string;
19
+ error?: string;
20
+ }
21
+ export interface ConversationTurn {
22
+ role: "user" | "assistant";
23
+ kind: "task" | "clarification_question" | "clarification_answer";
24
+ content: string;
25
+ }
26
+ export interface Step {
27
+ index: number;
28
+ name?: string;
29
+ rationale: string;
30
+ model?: string;
31
+ action: ModelAction;
32
+ result?: ToolResult;
33
+ userResponse?: string;
34
+ }
35
+ export interface MemoryNote {
36
+ slug: string;
37
+ title: string;
38
+ category: string;
39
+ path: string;
40
+ preview: string;
41
+ }
42
+ export interface ProjectMemory {
43
+ summary: string | null;
44
+ relevantNotes: MemoryNote[];
45
+ }
46
+ export type SubagentStatus = "running" | "completed" | "failed";
47
+ export interface SubagentRecord {
48
+ id: string;
49
+ name: string;
50
+ task: string;
51
+ status: SubagentStatus;
52
+ worktreePath: string;
53
+ summary?: string;
54
+ answer?: string;
55
+ changedFiles?: string[];
56
+ error?: string;
57
+ }
58
+ export interface Session {
59
+ id: string;
60
+ task: string;
61
+ repoPath: string;
62
+ maxSteps: number;
63
+ depth: number;
64
+ interactive: boolean;
65
+ turns: ConversationTurn[];
66
+ steps: Step[];
67
+ skillName?: string;
68
+ skillContent?: string;
69
+ memorySummary?: string | null;
70
+ memoryNotes?: MemoryNote[];
71
+ subagents: SubagentRecord[];
72
+ }
73
+ export interface ModelDecision {
74
+ rationale: string;
75
+ action: ModelAction;
76
+ model?: string;
77
+ rawResponse?: string;
78
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};