fly-to-moon 0.1.12
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 +237 -0
- package/dist/cli.mjs +3374 -0
- package/package.json +51 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,3374 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { appendFileSync, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, rmdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir, tmpdir } from "node:os";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import process$1 from "node:process";
|
|
6
|
+
import { createInterface } from "node:readline";
|
|
7
|
+
import { Command, InvalidArgumentError } from "commander";
|
|
8
|
+
import yaml from "js-yaml";
|
|
9
|
+
import { execFileSync, execSync, spawn } from "node:child_process";
|
|
10
|
+
import { createServer } from "node:net";
|
|
11
|
+
import { EventEmitter } from "node:events";
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
//#region src/core/config.ts
|
|
14
|
+
const AGENT_NAMES = [
|
|
15
|
+
"claude",
|
|
16
|
+
"codex",
|
|
17
|
+
"rovodev",
|
|
18
|
+
"opencode"
|
|
19
|
+
];
|
|
20
|
+
const DEFAULT_CONFIG = {
|
|
21
|
+
agent: "claude",
|
|
22
|
+
agentPathOverride: {},
|
|
23
|
+
maxConsecutiveFailures: 3,
|
|
24
|
+
preventSleep: true
|
|
25
|
+
};
|
|
26
|
+
var InvalidConfigError = class extends Error {};
|
|
27
|
+
function normalizePreventSleep(value) {
|
|
28
|
+
if (typeof value === "boolean") return value;
|
|
29
|
+
if (typeof value !== "string") return void 0;
|
|
30
|
+
if (value === "true") return true;
|
|
31
|
+
if (value === "false") return false;
|
|
32
|
+
if (value === "on") return true;
|
|
33
|
+
if (value === "off") return false;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve a user-supplied path against the config directory (~/.fttm).
|
|
37
|
+
* Expands leading `~` or `~/` to the home directory, then resolves relative
|
|
38
|
+
* paths against `baseDir` so that entries like `./bin/codex` work predictably
|
|
39
|
+
* regardless of the repo's cwd. Bare executable names and absolute paths pass
|
|
40
|
+
* through unchanged.
|
|
41
|
+
*/
|
|
42
|
+
function resolveConfigPath(raw, baseDir) {
|
|
43
|
+
if (raw !== "~" && !raw.startsWith("~/") && !raw.startsWith("~\\") && !raw.includes("/") && !raw.includes("\\")) return raw;
|
|
44
|
+
const home = homedir();
|
|
45
|
+
let expanded = raw;
|
|
46
|
+
if (expanded === "~") expanded = home;
|
|
47
|
+
else if (expanded.startsWith("~/") || expanded.startsWith("~\\")) expanded = join(home, expanded.slice(2));
|
|
48
|
+
return resolve(baseDir, expanded);
|
|
49
|
+
}
|
|
50
|
+
function normalizeAgentPathOverride(value, configDir) {
|
|
51
|
+
if (value === void 0 || value === null) return void 0;
|
|
52
|
+
if (typeof value !== "object" || Array.isArray(value)) throw new InvalidConfigError(`Invalid config value for agentPathOverride: expected an object mapping agent names to paths`);
|
|
53
|
+
const validNames = new Set(AGENT_NAMES);
|
|
54
|
+
const result = {};
|
|
55
|
+
for (const [key, val] of Object.entries(value)) {
|
|
56
|
+
if (!validNames.has(key)) throw new InvalidConfigError(`Invalid agent name in agentPathOverride: "${key}". Use "claude", "codex", "rovodev", or "opencode".`);
|
|
57
|
+
if (typeof val !== "string") throw new InvalidConfigError(`Invalid path for agentPathOverride.${key}: expected a string`);
|
|
58
|
+
if (val.trim() === "") throw new InvalidConfigError(`Invalid path for agentPathOverride.${key}: expected a non-empty string`);
|
|
59
|
+
result[key] = resolveConfigPath(val, configDir);
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
function normalizeConfig(config, configDir) {
|
|
64
|
+
const normalized = { ...config };
|
|
65
|
+
const hasPreventSleep = Object.prototype.hasOwnProperty.call(config, "preventSleep");
|
|
66
|
+
const preventSleep = normalizePreventSleep(config.preventSleep);
|
|
67
|
+
if (preventSleep === void 0) {
|
|
68
|
+
if (hasPreventSleep && config.preventSleep !== void 0) throw new InvalidConfigError(`Invalid config value for preventSleep: ${String(config.preventSleep)}`);
|
|
69
|
+
delete normalized.preventSleep;
|
|
70
|
+
} else normalized.preventSleep = preventSleep;
|
|
71
|
+
if (Object.prototype.hasOwnProperty.call(config, "agentPathOverride")) {
|
|
72
|
+
const resolveDir = configDir ?? join(homedir(), ".fttm");
|
|
73
|
+
const agentPathOverride = normalizeAgentPathOverride(config.agentPathOverride, resolveDir);
|
|
74
|
+
if (agentPathOverride === void 0) delete normalized.agentPathOverride;
|
|
75
|
+
else normalized.agentPathOverride = agentPathOverride;
|
|
76
|
+
} else delete normalized.agentPathOverride;
|
|
77
|
+
return normalized;
|
|
78
|
+
}
|
|
79
|
+
function isMissingConfigError(error) {
|
|
80
|
+
if (!(error instanceof Error)) return false;
|
|
81
|
+
return "code" in error ? error.code === "ENOENT" : error.message.includes("ENOENT");
|
|
82
|
+
}
|
|
83
|
+
function serializeAgentPathOverride(agentPathOverride) {
|
|
84
|
+
const serializedOverrides = Object.fromEntries(AGENT_NAMES.flatMap((name) => {
|
|
85
|
+
const value = agentPathOverride[name];
|
|
86
|
+
return value === void 0 ? [] : [[name, value]];
|
|
87
|
+
}));
|
|
88
|
+
if (Object.keys(serializedOverrides).length === 0) return "";
|
|
89
|
+
return yaml.dump({ agentPathOverride: serializedOverrides }, {
|
|
90
|
+
lineWidth: -1,
|
|
91
|
+
noRefs: true,
|
|
92
|
+
sortKeys: false
|
|
93
|
+
}).trimEnd();
|
|
94
|
+
}
|
|
95
|
+
function serializeConfig(config) {
|
|
96
|
+
const agentPathOverrideSection = serializeAgentPathOverride(config.agentPathOverride);
|
|
97
|
+
const lines = [
|
|
98
|
+
"# Agent to use by default",
|
|
99
|
+
`agent: ${config.agent}`,
|
|
100
|
+
"",
|
|
101
|
+
"# Custom paths to agent binaries (optional)",
|
|
102
|
+
"# Paths may be absolute, bare executable names on PATH,",
|
|
103
|
+
"# ~-prefixed, or relative to this config directory.",
|
|
104
|
+
"# Note: rovodev overrides must point to an acli-compatible binary.",
|
|
105
|
+
"# agentPathOverride:",
|
|
106
|
+
"# claude: /path/to/custom-claude",
|
|
107
|
+
"# codex: /path/to/custom-codex"
|
|
108
|
+
];
|
|
109
|
+
if (agentPathOverrideSection) lines.push(...agentPathOverrideSection.split("\n"));
|
|
110
|
+
lines.push("", "# Abort after this many consecutive failures", `maxConsecutiveFailures: ${config.maxConsecutiveFailures}`, "", "# Prevent the machine from sleeping during a run", `preventSleep: ${config.preventSleep}`, "");
|
|
111
|
+
return lines.join("\n");
|
|
112
|
+
}
|
|
113
|
+
function loadConfig(overrides) {
|
|
114
|
+
const configDir = join(homedir(), ".fttm");
|
|
115
|
+
const configPath = join(configDir, "config.yml");
|
|
116
|
+
let fileConfig = {};
|
|
117
|
+
let shouldBootstrapConfig = false;
|
|
118
|
+
try {
|
|
119
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
120
|
+
fileConfig = normalizeConfig(yaml.load(raw) ?? {}, configDir);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error instanceof InvalidConfigError) throw error;
|
|
123
|
+
if (isMissingConfigError(error)) shouldBootstrapConfig = true;
|
|
124
|
+
}
|
|
125
|
+
const resolvedConfig = {
|
|
126
|
+
...DEFAULT_CONFIG,
|
|
127
|
+
...fileConfig,
|
|
128
|
+
...normalizeConfig(overrides ?? {})
|
|
129
|
+
};
|
|
130
|
+
if (shouldBootstrapConfig) try {
|
|
131
|
+
mkdirSync(configDir, { recursive: true });
|
|
132
|
+
writeFileSync(configPath, serializeConfig(resolvedConfig), "utf-8");
|
|
133
|
+
} catch {}
|
|
134
|
+
return resolvedConfig;
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/core/debug-log.ts
|
|
138
|
+
function appendDebugLog(event, details = {}) {
|
|
139
|
+
const logPath = process.env.GNHF_DEBUG_LOG_PATH;
|
|
140
|
+
if (!logPath) return;
|
|
141
|
+
try {
|
|
142
|
+
appendFileSync(logPath, `${JSON.stringify({
|
|
143
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
144
|
+
pid: process.pid,
|
|
145
|
+
event,
|
|
146
|
+
...details
|
|
147
|
+
})}\n`, "utf-8");
|
|
148
|
+
} catch {}
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/core/git.ts
|
|
152
|
+
const NOT_GIT_REPOSITORY_MESSAGE = "This command must be run inside a Git repository. Change into a repo or run \"git init\" first.";
|
|
153
|
+
function translateGitError(error) {
|
|
154
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
155
|
+
}
|
|
156
|
+
function git(args, cwd) {
|
|
157
|
+
try {
|
|
158
|
+
return execSync(`git ${args}`, {
|
|
159
|
+
cwd,
|
|
160
|
+
encoding: "utf-8",
|
|
161
|
+
stdio: "pipe"
|
|
162
|
+
}).trim();
|
|
163
|
+
} catch (error) {
|
|
164
|
+
throw translateGitError(error);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function isGitRepository(cwd) {
|
|
168
|
+
try {
|
|
169
|
+
execSync("git rev-parse --git-dir", {
|
|
170
|
+
cwd,
|
|
171
|
+
encoding: "utf-8",
|
|
172
|
+
stdio: "pipe",
|
|
173
|
+
env: {
|
|
174
|
+
...process.env,
|
|
175
|
+
LC_ALL: "C"
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
return true;
|
|
179
|
+
} catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function ensureGitRepository(cwd) {
|
|
184
|
+
if (!isGitRepository(cwd)) throw new Error(NOT_GIT_REPOSITORY_MESSAGE);
|
|
185
|
+
}
|
|
186
|
+
function getCurrentBranch(cwd) {
|
|
187
|
+
ensureGitRepository(cwd);
|
|
188
|
+
try {
|
|
189
|
+
return git("symbolic-ref --short HEAD", cwd);
|
|
190
|
+
} catch {
|
|
191
|
+
return git("rev-parse --abbrev-ref HEAD", cwd);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function ensureCleanWorkingTree(cwd) {
|
|
195
|
+
if (git("status --porcelain", cwd)) throw new Error("Working tree is not clean. Commit or stash changes first.");
|
|
196
|
+
}
|
|
197
|
+
function createBranch(branchName, cwd) {
|
|
198
|
+
git(`checkout -b ${branchName}`, cwd);
|
|
199
|
+
}
|
|
200
|
+
function getHeadCommit(cwd) {
|
|
201
|
+
return git("rev-parse HEAD", cwd);
|
|
202
|
+
}
|
|
203
|
+
function findLegacyRunBaseCommit(runId, cwd) {
|
|
204
|
+
try {
|
|
205
|
+
const marker = git("log --first-parent --reverse --format=%H%x09%s HEAD", cwd).split("\n").map((line) => {
|
|
206
|
+
const [sha, ...subjectParts] = line.split(" ");
|
|
207
|
+
return {
|
|
208
|
+
sha,
|
|
209
|
+
subject: subjectParts.join(" ")
|
|
210
|
+
};
|
|
211
|
+
}).find(({ subject }) => subject === `fttm: initialize run ${runId}` || subject === `fttm: overwrite run ${runId}`);
|
|
212
|
+
if (!marker?.sha) return null;
|
|
213
|
+
return git(`rev-parse ${marker.sha}^`, cwd);
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function getBranchCommitCount(baseCommit, cwd) {
|
|
219
|
+
if (!baseCommit) return 0;
|
|
220
|
+
return Number.parseInt(git(`rev-list --count --first-parent ${baseCommit}..HEAD`, cwd), 10);
|
|
221
|
+
}
|
|
222
|
+
function commitAll(message, cwd) {
|
|
223
|
+
git("add -A", cwd);
|
|
224
|
+
try {
|
|
225
|
+
git(`commit -m "${message.replace(/"/g, "\\\"")}"`, cwd);
|
|
226
|
+
} catch {}
|
|
227
|
+
}
|
|
228
|
+
function resetHard(cwd) {
|
|
229
|
+
git("reset --hard HEAD", cwd);
|
|
230
|
+
git("clean -fd", cwd);
|
|
231
|
+
}
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/core/agents/types.ts
|
|
234
|
+
const AGENT_OUTPUT_SCHEMA = {
|
|
235
|
+
type: "object",
|
|
236
|
+
additionalProperties: false,
|
|
237
|
+
properties: {
|
|
238
|
+
success: { type: "boolean" },
|
|
239
|
+
summary: { type: "string" },
|
|
240
|
+
key_changes_made: {
|
|
241
|
+
type: "array",
|
|
242
|
+
items: { type: "string" }
|
|
243
|
+
},
|
|
244
|
+
key_learnings: {
|
|
245
|
+
type: "array",
|
|
246
|
+
items: { type: "string" }
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
required: [
|
|
250
|
+
"success",
|
|
251
|
+
"summary",
|
|
252
|
+
"key_changes_made",
|
|
253
|
+
"key_learnings"
|
|
254
|
+
]
|
|
255
|
+
};
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/core/run.ts
|
|
258
|
+
function writeSchemaFile(schemaPath) {
|
|
259
|
+
writeFileSync(schemaPath, JSON.stringify(AGENT_OUTPUT_SCHEMA, null, 2), "utf-8");
|
|
260
|
+
}
|
|
261
|
+
function ensureRunMetadataIgnored(cwd) {
|
|
262
|
+
const excludePath = execFileSync("git", [
|
|
263
|
+
"rev-parse",
|
|
264
|
+
"--git-path",
|
|
265
|
+
"info/exclude"
|
|
266
|
+
], {
|
|
267
|
+
cwd,
|
|
268
|
+
encoding: "utf-8"
|
|
269
|
+
}).trim();
|
|
270
|
+
const resolved = isAbsolute(excludePath) ? excludePath : join(cwd, excludePath);
|
|
271
|
+
const entry = ".fttm/runs/";
|
|
272
|
+
mkdirSync(dirname(resolved), { recursive: true });
|
|
273
|
+
if (existsSync(resolved)) {
|
|
274
|
+
const content = readFileSync(resolved, "utf-8");
|
|
275
|
+
if (content.split("\n").some((line) => line.trim() === entry)) return;
|
|
276
|
+
appendFileSync(resolved, `${content.length > 0 && !content.endsWith("\n") ? "\n" : ""}${entry}\n`, "utf-8");
|
|
277
|
+
} else writeFileSync(resolved, `${entry}\n`, "utf-8");
|
|
278
|
+
}
|
|
279
|
+
function setupRun(runId, prompt, baseCommit, cwd) {
|
|
280
|
+
ensureRunMetadataIgnored(cwd);
|
|
281
|
+
const runDir = join(cwd, ".fttm", "runs", runId);
|
|
282
|
+
mkdirSync(runDir, { recursive: true });
|
|
283
|
+
const promptPath = join(runDir, "prompt.md");
|
|
284
|
+
writeFileSync(promptPath, prompt, "utf-8");
|
|
285
|
+
const notesPath = join(runDir, "notes.md");
|
|
286
|
+
writeFileSync(notesPath, `# fttm run: ${runId}\n\nObjective: ${prompt}\n\n## Iteration Log\n`, "utf-8");
|
|
287
|
+
const schemaPath = join(runDir, "output-schema.json");
|
|
288
|
+
writeSchemaFile(schemaPath);
|
|
289
|
+
const baseCommitPath = join(runDir, "base-commit");
|
|
290
|
+
const hasStoredBaseCommit = existsSync(baseCommitPath);
|
|
291
|
+
const resolvedBaseCommit = hasStoredBaseCommit ? readFileSync(baseCommitPath, "utf-8").trim() : baseCommit;
|
|
292
|
+
if (!hasStoredBaseCommit) writeFileSync(baseCommitPath, `${baseCommit}\n`, "utf-8");
|
|
293
|
+
return {
|
|
294
|
+
runId,
|
|
295
|
+
runDir,
|
|
296
|
+
promptPath,
|
|
297
|
+
notesPath,
|
|
298
|
+
schemaPath,
|
|
299
|
+
baseCommit: resolvedBaseCommit,
|
|
300
|
+
baseCommitPath
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function resumeRun(runId, cwd) {
|
|
304
|
+
const runDir = join(cwd, ".fttm", "runs", runId);
|
|
305
|
+
if (!existsSync(runDir)) throw new Error(`Run directory not found: ${runDir}`);
|
|
306
|
+
const promptPath = join(runDir, "prompt.md");
|
|
307
|
+
const notesPath = join(runDir, "notes.md");
|
|
308
|
+
const schemaPath = join(runDir, "output-schema.json");
|
|
309
|
+
writeSchemaFile(schemaPath);
|
|
310
|
+
const baseCommitPath = join(runDir, "base-commit");
|
|
311
|
+
return {
|
|
312
|
+
runId,
|
|
313
|
+
runDir,
|
|
314
|
+
promptPath,
|
|
315
|
+
notesPath,
|
|
316
|
+
schemaPath,
|
|
317
|
+
baseCommit: existsSync(baseCommitPath) ? readFileSync(baseCommitPath, "utf-8").trim() : backfillLegacyBaseCommit(runId, baseCommitPath, cwd),
|
|
318
|
+
baseCommitPath
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function backfillLegacyBaseCommit(runId, baseCommitPath, cwd) {
|
|
322
|
+
const baseCommit = findLegacyRunBaseCommit(runId, cwd) ?? getHeadCommit(cwd);
|
|
323
|
+
writeFileSync(baseCommitPath, `${baseCommit}\n`, "utf-8");
|
|
324
|
+
return baseCommit;
|
|
325
|
+
}
|
|
326
|
+
function getLastIterationNumber(runInfo) {
|
|
327
|
+
const files = readdirSync(runInfo.runDir);
|
|
328
|
+
let max = 0;
|
|
329
|
+
for (const f of files) {
|
|
330
|
+
const m = f.match(/^iteration-(\d+)\.jsonl$/);
|
|
331
|
+
if (m) {
|
|
332
|
+
const n = parseInt(m[1], 10);
|
|
333
|
+
if (n > max) max = n;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return max;
|
|
337
|
+
}
|
|
338
|
+
function formatListSection(title, items) {
|
|
339
|
+
if (items.length === 0) return "";
|
|
340
|
+
return `**${title}:**\n${items.map((item) => `- ${item}`).join("\n")}\n`;
|
|
341
|
+
}
|
|
342
|
+
function appendNotes(notesPath, iteration, summary, changes, learnings) {
|
|
343
|
+
appendFileSync(notesPath, [
|
|
344
|
+
`\n### Iteration ${iteration}\n`,
|
|
345
|
+
`**Summary:** ${summary}\n`,
|
|
346
|
+
formatListSection("Changes", changes),
|
|
347
|
+
formatListSection("Learnings", learnings)
|
|
348
|
+
].join("\n"), "utf-8");
|
|
349
|
+
}
|
|
350
|
+
//#endregion
|
|
351
|
+
//#region src/core/stdin.ts
|
|
352
|
+
async function readStdinText(input) {
|
|
353
|
+
const chunks = [];
|
|
354
|
+
for await (const chunk of input) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
355
|
+
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
356
|
+
}
|
|
357
|
+
//#endregion
|
|
358
|
+
//#region src/core/agents/managed-process.ts
|
|
359
|
+
const POST_SIGKILL_GRACE_MS = 100;
|
|
360
|
+
function signalChildProcess(child, options) {
|
|
361
|
+
const killProcess = options.killProcess ?? process.kill.bind(process);
|
|
362
|
+
if (options.detached && child.pid) try {
|
|
363
|
+
killProcess(-child.pid, options.signal);
|
|
364
|
+
return;
|
|
365
|
+
} catch {}
|
|
366
|
+
child.kill(options.signal);
|
|
367
|
+
}
|
|
368
|
+
async function shutdownChildProcess(child, options) {
|
|
369
|
+
if (child.exitCode != null || child.signalCode != null) return;
|
|
370
|
+
const timeoutMs = options.timeoutMs ?? 3e3;
|
|
371
|
+
await new Promise((resolve) => {
|
|
372
|
+
let forceKillTimer = null;
|
|
373
|
+
let hardDeadlineTimer = null;
|
|
374
|
+
let settled = false;
|
|
375
|
+
const settle = () => {
|
|
376
|
+
if (settled) return;
|
|
377
|
+
settled = true;
|
|
378
|
+
if (forceKillTimer) {
|
|
379
|
+
clearTimeout(forceKillTimer);
|
|
380
|
+
forceKillTimer = null;
|
|
381
|
+
}
|
|
382
|
+
if (hardDeadlineTimer) {
|
|
383
|
+
clearTimeout(hardDeadlineTimer);
|
|
384
|
+
hardDeadlineTimer = null;
|
|
385
|
+
}
|
|
386
|
+
child.off("close", handleClose);
|
|
387
|
+
resolve();
|
|
388
|
+
};
|
|
389
|
+
const handleClose = () => {
|
|
390
|
+
settle();
|
|
391
|
+
};
|
|
392
|
+
child.on("close", handleClose);
|
|
393
|
+
try {
|
|
394
|
+
signalChildProcess(child, {
|
|
395
|
+
...options,
|
|
396
|
+
signal: "SIGTERM"
|
|
397
|
+
});
|
|
398
|
+
} catch {}
|
|
399
|
+
forceKillTimer = setTimeout(() => {
|
|
400
|
+
try {
|
|
401
|
+
signalChildProcess(child, {
|
|
402
|
+
...options,
|
|
403
|
+
signal: "SIGKILL"
|
|
404
|
+
});
|
|
405
|
+
} catch {}
|
|
406
|
+
hardDeadlineTimer = setTimeout(() => {
|
|
407
|
+
settle();
|
|
408
|
+
}, POST_SIGKILL_GRACE_MS);
|
|
409
|
+
hardDeadlineTimer.unref?.();
|
|
410
|
+
}, timeoutMs);
|
|
411
|
+
forceKillTimer.unref?.();
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
//#endregion
|
|
415
|
+
//#region src/core/sleep.ts
|
|
416
|
+
const SYSTEMD_INHIBIT_READY_TIMEOUT_MS = 5e3;
|
|
417
|
+
const SYSTEMD_INHIBIT_READY_POLL_MS = 25;
|
|
418
|
+
const GNHF_SLEEP_REEXEC_READY_PATH = "GNHF_SLEEP_REEXEC_READY_PATH";
|
|
419
|
+
const GNHF_SLEEP_REEXEC_READY_DIR_PREFIX = "fttm-sleep-";
|
|
420
|
+
const GNHF_SLEEP_REEXEC_READY_FILENAME = "reexec-ready";
|
|
421
|
+
const HELPER_STARTUP_GRACE_MS = 100;
|
|
422
|
+
function getSignalExitCode$1(signal) {
|
|
423
|
+
if (signal === "SIGINT") return 130;
|
|
424
|
+
if (signal === "SIGTERM") return 143;
|
|
425
|
+
return 1;
|
|
426
|
+
}
|
|
427
|
+
async function waitForSpawn(child) {
|
|
428
|
+
return await new Promise((resolve) => {
|
|
429
|
+
child.once("spawn", () => resolve(true));
|
|
430
|
+
child.once("error", () => resolve(false));
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
async function waitForHelperStability(child, timeoutMs) {
|
|
434
|
+
return await new Promise((resolve) => {
|
|
435
|
+
let settled = false;
|
|
436
|
+
let timer = null;
|
|
437
|
+
const settle = (value) => {
|
|
438
|
+
if (settled) return;
|
|
439
|
+
settled = true;
|
|
440
|
+
if (timer) {
|
|
441
|
+
clearTimeout(timer);
|
|
442
|
+
timer = null;
|
|
443
|
+
}
|
|
444
|
+
resolve(value);
|
|
445
|
+
};
|
|
446
|
+
child.once("exit", () => {
|
|
447
|
+
settle(false);
|
|
448
|
+
});
|
|
449
|
+
child.once("error", () => {
|
|
450
|
+
settle(false);
|
|
451
|
+
});
|
|
452
|
+
if (child.exitCode != null || child.signalCode != null) {
|
|
453
|
+
settle(false);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
timer = setTimeout(() => {
|
|
457
|
+
settle(true);
|
|
458
|
+
}, timeoutMs);
|
|
459
|
+
timer.unref?.();
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
function isTrustedLinuxReexecReadyPath(readyPath) {
|
|
463
|
+
const resolvedReadyPath = resolve(readyPath);
|
|
464
|
+
const readyDir = dirname(resolvedReadyPath);
|
|
465
|
+
return basename(resolvedReadyPath) === GNHF_SLEEP_REEXEC_READY_FILENAME && dirname(readyDir) === resolve(tmpdir()) && basename(readyDir).startsWith(GNHF_SLEEP_REEXEC_READY_DIR_PREFIX);
|
|
466
|
+
}
|
|
467
|
+
function signalLinuxReexecReady(env) {
|
|
468
|
+
const readyPath = env[GNHF_SLEEP_REEXEC_READY_PATH];
|
|
469
|
+
if (!readyPath) return;
|
|
470
|
+
if (!isTrustedLinuxReexecReadyPath(readyPath)) {
|
|
471
|
+
appendDebugLog("sleep:ready-signal-failed", {
|
|
472
|
+
command: "systemd-inhibit",
|
|
473
|
+
error: "untrusted ready path"
|
|
474
|
+
});
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
writeFileSync(readyPath, "ready\n", {
|
|
479
|
+
encoding: "utf-8",
|
|
480
|
+
flag: "wx"
|
|
481
|
+
});
|
|
482
|
+
} catch (error) {
|
|
483
|
+
appendDebugLog("sleep:ready-signal-failed", {
|
|
484
|
+
command: "systemd-inhibit",
|
|
485
|
+
error: error instanceof Error ? error.message : String(error)
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async function waitForLinuxReexecReady(readyPath, exitStatePromise, timeoutMs) {
|
|
490
|
+
if (existsSync(readyPath)) return { type: "ready" };
|
|
491
|
+
return await new Promise((resolve) => {
|
|
492
|
+
let settled = false;
|
|
493
|
+
const settle = (result) => {
|
|
494
|
+
if (settled) return;
|
|
495
|
+
settled = true;
|
|
496
|
+
clearInterval(poller);
|
|
497
|
+
clearTimeout(timeout);
|
|
498
|
+
resolve(result);
|
|
499
|
+
};
|
|
500
|
+
const poller = setInterval(() => {
|
|
501
|
+
if (existsSync(readyPath)) settle({ type: "ready" });
|
|
502
|
+
}, SYSTEMD_INHIBIT_READY_POLL_MS);
|
|
503
|
+
poller.unref?.();
|
|
504
|
+
const timeout = setTimeout(() => {
|
|
505
|
+
settle({ type: "timeout" });
|
|
506
|
+
}, timeoutMs);
|
|
507
|
+
timeout.unref?.();
|
|
508
|
+
exitStatePromise.then(({ exitCode, signal }) => {
|
|
509
|
+
settle({
|
|
510
|
+
type: "exit",
|
|
511
|
+
exitCode,
|
|
512
|
+
signal
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
function forwardTerminationSignalsToChild(child, detached, killProcess, processOn, processOff) {
|
|
518
|
+
const listeners = [];
|
|
519
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
520
|
+
const listener = () => {
|
|
521
|
+
try {
|
|
522
|
+
signalChildProcess(child, {
|
|
523
|
+
detached,
|
|
524
|
+
killProcess,
|
|
525
|
+
signal
|
|
526
|
+
});
|
|
527
|
+
} catch {}
|
|
528
|
+
};
|
|
529
|
+
processOn(signal, listener);
|
|
530
|
+
listeners.push([signal, listener]);
|
|
531
|
+
}
|
|
532
|
+
return () => {
|
|
533
|
+
for (const [signal, listener] of listeners) processOff(signal, listener);
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function buildPowerShellCommand(parentPid) {
|
|
537
|
+
return [
|
|
538
|
+
"Add-Type @'",
|
|
539
|
+
"using System;",
|
|
540
|
+
"using System.Runtime.InteropServices;",
|
|
541
|
+
"public static class SleepBlock {",
|
|
542
|
+
" [DllImport(\"kernel32.dll\")]",
|
|
543
|
+
" public static extern uint SetThreadExecutionState(uint flags);",
|
|
544
|
+
"}",
|
|
545
|
+
"'@;",
|
|
546
|
+
"$ES_CONTINUOUS = 0x80000000;",
|
|
547
|
+
"$ES_SYSTEM_REQUIRED = 0x00000001;",
|
|
548
|
+
"[SleepBlock]::SetThreadExecutionState($ES_CONTINUOUS -bor $ES_SYSTEM_REQUIRED) | Out-Null;",
|
|
549
|
+
`try { Wait-Process -Id ${parentPid} } catch { } finally { [SleepBlock]::SetThreadExecutionState($ES_CONTINUOUS) | Out-Null }`
|
|
550
|
+
].join("\n");
|
|
551
|
+
}
|
|
552
|
+
async function startHelperProcess(command, args, spawnFn, env) {
|
|
553
|
+
const child = spawnFn(command, args, {
|
|
554
|
+
env,
|
|
555
|
+
stdio: "ignore"
|
|
556
|
+
});
|
|
557
|
+
if (!await waitForSpawn(child)) {
|
|
558
|
+
appendDebugLog("sleep:unavailable", { command });
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
if (!await waitForHelperStability(child, HELPER_STARTUP_GRACE_MS)) {
|
|
562
|
+
appendDebugLog("sleep:unavailable", {
|
|
563
|
+
command,
|
|
564
|
+
reason: "early-exit"
|
|
565
|
+
});
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
return child;
|
|
569
|
+
}
|
|
570
|
+
async function startSleepPrevention(argv, deps = {}) {
|
|
571
|
+
const env = deps.env ?? process.env;
|
|
572
|
+
const killProcess = deps.killProcess ?? process.kill.bind(process);
|
|
573
|
+
const pid = deps.pid ?? process.pid;
|
|
574
|
+
const platform = deps.platform ?? process.platform;
|
|
575
|
+
const processExecArgv = deps.processExecArgv ?? process.execArgv;
|
|
576
|
+
const processArgv1 = deps.processArgv1 ?? process.argv[1];
|
|
577
|
+
const processExecPath = deps.processExecPath ?? process.execPath;
|
|
578
|
+
const processOn = deps.processOn ?? process.on.bind(process);
|
|
579
|
+
const processOff = deps.processOff ?? process.off.bind(process);
|
|
580
|
+
const reexecEnv = deps.reexecEnv ?? {};
|
|
581
|
+
const spawnFn = deps.spawn ?? spawn;
|
|
582
|
+
const detach = deps.detach ?? false;
|
|
583
|
+
if (platform === "linux") {
|
|
584
|
+
if (env.GNHF_SLEEP_INHIBITED === "1") {
|
|
585
|
+
signalLinuxReexecReady(env);
|
|
586
|
+
return {
|
|
587
|
+
type: "skipped",
|
|
588
|
+
reason: "already-inhibited"
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
const readyDir = mkdtempSync(join(tmpdir(), GNHF_SLEEP_REEXEC_READY_DIR_PREFIX));
|
|
592
|
+
const readyPath = join(readyDir, GNHF_SLEEP_REEXEC_READY_FILENAME);
|
|
593
|
+
const child = spawnFn("systemd-inhibit", [
|
|
594
|
+
"--what=idle:sleep",
|
|
595
|
+
"--mode=block",
|
|
596
|
+
"--who=fttm",
|
|
597
|
+
"--why=Prevent sleep while fttm is running",
|
|
598
|
+
processExecPath,
|
|
599
|
+
...processExecArgv,
|
|
600
|
+
processArgv1,
|
|
601
|
+
...argv
|
|
602
|
+
], {
|
|
603
|
+
detached: detach,
|
|
604
|
+
env: {
|
|
605
|
+
...env,
|
|
606
|
+
...reexecEnv,
|
|
607
|
+
GNHF_SLEEP_INHIBITED: "1",
|
|
608
|
+
[GNHF_SLEEP_REEXEC_READY_PATH]: readyPath
|
|
609
|
+
},
|
|
610
|
+
stdio: "inherit"
|
|
611
|
+
});
|
|
612
|
+
const exitStatePromise = new Promise((resolve) => {
|
|
613
|
+
child.once("exit", (code, signal) => {
|
|
614
|
+
resolve({
|
|
615
|
+
exitCode: signal ? getSignalExitCode$1(signal) : code ?? 1,
|
|
616
|
+
signal
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
const stopForwardingSignals = forwardTerminationSignalsToChild(child, true, killProcess, processOn, processOff);
|
|
621
|
+
if (!await waitForSpawn(child)) {
|
|
622
|
+
stopForwardingSignals();
|
|
623
|
+
rmSync(readyDir, {
|
|
624
|
+
recursive: true,
|
|
625
|
+
force: true
|
|
626
|
+
});
|
|
627
|
+
appendDebugLog("sleep:unavailable", { command: "systemd-inhibit" });
|
|
628
|
+
return {
|
|
629
|
+
type: "skipped",
|
|
630
|
+
reason: "unavailable"
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
try {
|
|
634
|
+
const readyState = await waitForLinuxReexecReady(readyPath, exitStatePromise, SYSTEMD_INHIBIT_READY_TIMEOUT_MS);
|
|
635
|
+
try {
|
|
636
|
+
if (readyState.type === "ready") {
|
|
637
|
+
appendDebugLog("sleep:reexec", { command: "systemd-inhibit" });
|
|
638
|
+
const { exitCode } = await exitStatePromise;
|
|
639
|
+
return {
|
|
640
|
+
type: "reexeced",
|
|
641
|
+
exitCode
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
if (readyState.type === "exit") {
|
|
645
|
+
if (readyState.signal === "SIGINT" || readyState.signal === "SIGTERM") {
|
|
646
|
+
appendDebugLog("sleep:reexec", {
|
|
647
|
+
command: "systemd-inhibit",
|
|
648
|
+
signal: readyState.signal
|
|
649
|
+
});
|
|
650
|
+
return {
|
|
651
|
+
type: "reexeced",
|
|
652
|
+
exitCode: readyState.exitCode
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
if (readyState.exitCode !== 0) {
|
|
656
|
+
if (existsSync(readyPath)) {
|
|
657
|
+
appendDebugLog("sleep:reexec", {
|
|
658
|
+
command: "systemd-inhibit",
|
|
659
|
+
exitCode: readyState.exitCode,
|
|
660
|
+
readySignal: "late"
|
|
661
|
+
});
|
|
662
|
+
return {
|
|
663
|
+
type: "reexeced",
|
|
664
|
+
exitCode: readyState.exitCode
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
appendDebugLog("sleep:unavailable", {
|
|
668
|
+
command: "systemd-inhibit",
|
|
669
|
+
exitCode: readyState.exitCode
|
|
670
|
+
});
|
|
671
|
+
return {
|
|
672
|
+
type: "skipped",
|
|
673
|
+
reason: "unavailable"
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
appendDebugLog("sleep:reexec", {
|
|
677
|
+
command: "systemd-inhibit",
|
|
678
|
+
readySignal: false
|
|
679
|
+
});
|
|
680
|
+
return {
|
|
681
|
+
type: "reexeced",
|
|
682
|
+
exitCode: readyState.exitCode
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
appendDebugLog("sleep:unavailable", {
|
|
686
|
+
command: "systemd-inhibit",
|
|
687
|
+
reason: "timeout",
|
|
688
|
+
timeoutMs: SYSTEMD_INHIBIT_READY_TIMEOUT_MS
|
|
689
|
+
});
|
|
690
|
+
await shutdownChildProcess(child, {
|
|
691
|
+
detached: true,
|
|
692
|
+
killProcess,
|
|
693
|
+
timeoutMs: 1e3
|
|
694
|
+
});
|
|
695
|
+
return {
|
|
696
|
+
type: "skipped",
|
|
697
|
+
reason: "unavailable"
|
|
698
|
+
};
|
|
699
|
+
} finally {
|
|
700
|
+
stopForwardingSignals();
|
|
701
|
+
}
|
|
702
|
+
} finally {
|
|
703
|
+
rmSync(readyDir, {
|
|
704
|
+
recursive: true,
|
|
705
|
+
force: true
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (platform === "darwin") {
|
|
710
|
+
if (detach) {
|
|
711
|
+
if (!await startHelperProcess("caffeinate", ["-i"], spawnFn, env)) return {
|
|
712
|
+
type: "skipped",
|
|
713
|
+
reason: "unavailable"
|
|
714
|
+
};
|
|
715
|
+
appendDebugLog("sleep:reexec", { command: "caffeinate" });
|
|
716
|
+
return {
|
|
717
|
+
type: "reexeced",
|
|
718
|
+
exitCode: 0
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
const child = await startHelperProcess("caffeinate", [
|
|
722
|
+
"-i",
|
|
723
|
+
"-w",
|
|
724
|
+
String(pid)
|
|
725
|
+
], spawnFn, env);
|
|
726
|
+
if (!child) return {
|
|
727
|
+
type: "skipped",
|
|
728
|
+
reason: "unavailable"
|
|
729
|
+
};
|
|
730
|
+
appendDebugLog("sleep:active", { command: "caffeinate" });
|
|
731
|
+
return {
|
|
732
|
+
type: "active",
|
|
733
|
+
cleanup: async () => {
|
|
734
|
+
appendDebugLog("sleep:cleanup", { command: "caffeinate" });
|
|
735
|
+
await shutdownChildProcess(child, {
|
|
736
|
+
detached: false,
|
|
737
|
+
timeoutMs: 1e3
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
if (platform === "win32") {
|
|
743
|
+
const child = await startHelperProcess("powershell.exe", [
|
|
744
|
+
"-NoLogo",
|
|
745
|
+
"-NoProfile",
|
|
746
|
+
"-NonInteractive",
|
|
747
|
+
"-ExecutionPolicy",
|
|
748
|
+
"Bypass",
|
|
749
|
+
"-Command",
|
|
750
|
+
buildPowerShellCommand(pid)
|
|
751
|
+
], spawnFn, env);
|
|
752
|
+
if (!child) return {
|
|
753
|
+
type: "skipped",
|
|
754
|
+
reason: "unavailable"
|
|
755
|
+
};
|
|
756
|
+
appendDebugLog("sleep:active", { command: "powershell.exe" });
|
|
757
|
+
return {
|
|
758
|
+
type: "active",
|
|
759
|
+
cleanup: async () => {
|
|
760
|
+
appendDebugLog("sleep:cleanup", { command: "powershell.exe" });
|
|
761
|
+
await shutdownChildProcess(child, {
|
|
762
|
+
detached: false,
|
|
763
|
+
timeoutMs: 1e3
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
return {
|
|
769
|
+
type: "skipped",
|
|
770
|
+
reason: "unsupported"
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
//#endregion
|
|
774
|
+
//#region src/core/agents/stream-utils.ts
|
|
775
|
+
/**
|
|
776
|
+
* Wire stderr collection, spawn-error handling, and the common close-handler
|
|
777
|
+
* prefix (logStream.end + non-zero exit code rejection) for a child process.
|
|
778
|
+
* Calls `onSuccess` only when the process exits with code 0.
|
|
779
|
+
*/
|
|
780
|
+
function setupChildProcessHandlers(child, agentName, logStream, reject, onSuccess) {
|
|
781
|
+
let stderr = "";
|
|
782
|
+
child.stderr.on("data", (data) => {
|
|
783
|
+
stderr += data.toString();
|
|
784
|
+
});
|
|
785
|
+
child.on("error", (err) => {
|
|
786
|
+
reject(/* @__PURE__ */ new Error(`Failed to spawn ${agentName}: ${err.message}`));
|
|
787
|
+
});
|
|
788
|
+
child.on("close", (code) => {
|
|
789
|
+
logStream?.end();
|
|
790
|
+
if (code !== 0) {
|
|
791
|
+
reject(/* @__PURE__ */ new Error(`${agentName} exited with code ${code}: ${stderr}`));
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
onSuccess();
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Parse a JSONL stream, calling the callback for each parsed event.
|
|
799
|
+
* Handles buffering of incomplete lines and skips unparseable lines.
|
|
800
|
+
*/
|
|
801
|
+
function parseJSONLStream(stream, logStream, callback) {
|
|
802
|
+
let buffer = "";
|
|
803
|
+
stream.on("data", (data) => {
|
|
804
|
+
logStream?.write(data);
|
|
805
|
+
buffer += data.toString();
|
|
806
|
+
const lines = buffer.split("\n");
|
|
807
|
+
buffer = lines.pop() ?? "";
|
|
808
|
+
for (const line of lines) {
|
|
809
|
+
if (!line.trim()) continue;
|
|
810
|
+
try {
|
|
811
|
+
callback(JSON.parse(line));
|
|
812
|
+
} catch {}
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Wire an AbortSignal to kill a child process.
|
|
818
|
+
* Returns true if the signal was already aborted (caller should return early).
|
|
819
|
+
*/
|
|
820
|
+
function setupAbortHandler(signal, child, reject, abortChild = () => {
|
|
821
|
+
child.kill("SIGTERM");
|
|
822
|
+
}) {
|
|
823
|
+
if (!signal) return false;
|
|
824
|
+
const onAbort = () => {
|
|
825
|
+
abortChild();
|
|
826
|
+
reject(/* @__PURE__ */ new Error("Agent was aborted"));
|
|
827
|
+
};
|
|
828
|
+
if (signal.aborted) {
|
|
829
|
+
onAbort();
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
833
|
+
child.on("close", () => signal.removeEventListener("abort", onAbort));
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
//#endregion
|
|
837
|
+
//#region src/core/agents/claude.ts
|
|
838
|
+
function shouldUseWindowsShell$2(bin, platform) {
|
|
839
|
+
if (platform !== "win32") return false;
|
|
840
|
+
if (/\.(cmd|bat)$/i.test(bin)) return true;
|
|
841
|
+
if (/[\\/]/.test(bin)) return false;
|
|
842
|
+
try {
|
|
843
|
+
const firstMatch = execFileSync("where", [bin], {
|
|
844
|
+
encoding: "utf8",
|
|
845
|
+
stdio: [
|
|
846
|
+
"ignore",
|
|
847
|
+
"pipe",
|
|
848
|
+
"ignore"
|
|
849
|
+
]
|
|
850
|
+
}).split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
851
|
+
return firstMatch ? /\.(cmd|bat)$/i.test(firstMatch) : false;
|
|
852
|
+
} catch {
|
|
853
|
+
return false;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
function terminateClaudeProcess(child, platform) {
|
|
857
|
+
if (platform === "win32" && child.pid) {
|
|
858
|
+
try {
|
|
859
|
+
execFileSync("taskkill", [
|
|
860
|
+
"/T",
|
|
861
|
+
"/F",
|
|
862
|
+
"/PID",
|
|
863
|
+
String(child.pid)
|
|
864
|
+
], { stdio: "ignore" });
|
|
865
|
+
} catch {}
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
child.kill("SIGTERM");
|
|
869
|
+
}
|
|
870
|
+
var ClaudeAgent = class {
|
|
871
|
+
name = "claude";
|
|
872
|
+
bin;
|
|
873
|
+
platform;
|
|
874
|
+
constructor(binOrDeps = {}) {
|
|
875
|
+
const deps = typeof binOrDeps === "string" ? { bin: binOrDeps } : binOrDeps;
|
|
876
|
+
this.bin = deps.bin ?? "claude";
|
|
877
|
+
this.platform = deps.platform ?? process.platform;
|
|
878
|
+
}
|
|
879
|
+
run(prompt, cwd, options) {
|
|
880
|
+
const { onUsage, onMessage, signal, logPath } = options ?? {};
|
|
881
|
+
return new Promise((resolve, reject) => {
|
|
882
|
+
const logStream = logPath ? createWriteStream(logPath) : null;
|
|
883
|
+
const child = spawn(this.bin, [
|
|
884
|
+
"-p",
|
|
885
|
+
prompt,
|
|
886
|
+
"--verbose",
|
|
887
|
+
"--output-format",
|
|
888
|
+
"stream-json",
|
|
889
|
+
"--json-schema",
|
|
890
|
+
JSON.stringify(AGENT_OUTPUT_SCHEMA),
|
|
891
|
+
"--dangerously-skip-permissions"
|
|
892
|
+
], {
|
|
893
|
+
cwd,
|
|
894
|
+
shell: shouldUseWindowsShell$2(this.bin, this.platform),
|
|
895
|
+
stdio: [
|
|
896
|
+
"ignore",
|
|
897
|
+
"pipe",
|
|
898
|
+
"pipe"
|
|
899
|
+
],
|
|
900
|
+
env: process.env
|
|
901
|
+
});
|
|
902
|
+
if (setupAbortHandler(signal, child, reject, () => terminateClaudeProcess(child, this.platform))) return;
|
|
903
|
+
let resultEvent = null;
|
|
904
|
+
const cumulative = {
|
|
905
|
+
inputTokens: 0,
|
|
906
|
+
outputTokens: 0,
|
|
907
|
+
cacheReadTokens: 0,
|
|
908
|
+
cacheCreationTokens: 0
|
|
909
|
+
};
|
|
910
|
+
parseJSONLStream(child.stdout, logStream, (event) => {
|
|
911
|
+
if (event.type === "assistant") {
|
|
912
|
+
const msg = event.message;
|
|
913
|
+
cumulative.inputTokens += (msg.usage.input_tokens ?? 0) + (msg.usage.cache_read_input_tokens ?? 0);
|
|
914
|
+
cumulative.outputTokens += msg.usage.output_tokens ?? 0;
|
|
915
|
+
cumulative.cacheReadTokens += msg.usage.cache_read_input_tokens ?? 0;
|
|
916
|
+
cumulative.cacheCreationTokens += msg.usage.cache_creation_input_tokens ?? 0;
|
|
917
|
+
onUsage?.({ ...cumulative });
|
|
918
|
+
if (onMessage) {
|
|
919
|
+
const content = msg.content;
|
|
920
|
+
if (Array.isArray(content)) {
|
|
921
|
+
for (const block of content) if (block?.type === "text" && typeof block.text === "string" && block.text.trim()) onMessage(block.text.trim());
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
if (event.type === "result") resultEvent = event;
|
|
926
|
+
});
|
|
927
|
+
setupChildProcessHandlers(child, "claude", logStream, reject, () => {
|
|
928
|
+
if (!resultEvent) {
|
|
929
|
+
reject(/* @__PURE__ */ new Error("claude returned no result event"));
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
if (resultEvent.is_error || resultEvent.subtype !== "success") {
|
|
933
|
+
reject(/* @__PURE__ */ new Error(`claude reported error: ${JSON.stringify(resultEvent)}`));
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
if (!resultEvent.structured_output) {
|
|
937
|
+
reject(/* @__PURE__ */ new Error("claude returned no structured_output"));
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
const output = resultEvent.structured_output;
|
|
941
|
+
const usage = {
|
|
942
|
+
inputTokens: (resultEvent.usage.input_tokens ?? 0) + (resultEvent.usage.cache_read_input_tokens ?? 0),
|
|
943
|
+
outputTokens: resultEvent.usage.output_tokens ?? 0,
|
|
944
|
+
cacheReadTokens: resultEvent.usage.cache_read_input_tokens ?? 0,
|
|
945
|
+
cacheCreationTokens: resultEvent.usage.cache_creation_input_tokens ?? 0
|
|
946
|
+
};
|
|
947
|
+
onUsage?.(usage);
|
|
948
|
+
resolve({
|
|
949
|
+
output,
|
|
950
|
+
usage
|
|
951
|
+
});
|
|
952
|
+
});
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
//#endregion
|
|
957
|
+
//#region src/core/agents/codex.ts
|
|
958
|
+
function shouldUseWindowsShell$1(bin, platform) {
|
|
959
|
+
if (platform !== "win32") return false;
|
|
960
|
+
if (/\.(cmd|bat)$/i.test(bin)) return true;
|
|
961
|
+
if (/[\\/]/.test(bin)) return false;
|
|
962
|
+
try {
|
|
963
|
+
const firstMatch = execFileSync("where", [bin], {
|
|
964
|
+
encoding: "utf8",
|
|
965
|
+
stdio: [
|
|
966
|
+
"ignore",
|
|
967
|
+
"pipe",
|
|
968
|
+
"ignore"
|
|
969
|
+
]
|
|
970
|
+
}).split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
971
|
+
return firstMatch ? /\.(cmd|bat)$/i.test(firstMatch) : false;
|
|
972
|
+
} catch {
|
|
973
|
+
return false;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
function terminateCodexProcess(child, platform) {
|
|
977
|
+
if (platform === "win32" && child.pid) {
|
|
978
|
+
try {
|
|
979
|
+
execFileSync("taskkill", [
|
|
980
|
+
"/T",
|
|
981
|
+
"/F",
|
|
982
|
+
"/PID",
|
|
983
|
+
String(child.pid)
|
|
984
|
+
], { stdio: "ignore" });
|
|
985
|
+
} catch {}
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
child.kill("SIGTERM");
|
|
989
|
+
}
|
|
990
|
+
var CodexAgent = class {
|
|
991
|
+
name = "codex";
|
|
992
|
+
bin;
|
|
993
|
+
platform;
|
|
994
|
+
schemaPath;
|
|
995
|
+
constructor(schemaPath, binOrDeps = {}) {
|
|
996
|
+
const deps = typeof binOrDeps === "string" ? { bin: binOrDeps } : binOrDeps;
|
|
997
|
+
this.bin = deps.bin ?? "codex";
|
|
998
|
+
this.platform = deps.platform ?? process.platform;
|
|
999
|
+
this.schemaPath = schemaPath;
|
|
1000
|
+
}
|
|
1001
|
+
run(prompt, cwd, options) {
|
|
1002
|
+
const { onUsage, onMessage, signal, logPath } = options ?? {};
|
|
1003
|
+
return new Promise((resolve, reject) => {
|
|
1004
|
+
const logStream = logPath ? createWriteStream(logPath) : null;
|
|
1005
|
+
const child = spawn(this.bin, [
|
|
1006
|
+
"exec",
|
|
1007
|
+
prompt,
|
|
1008
|
+
"--json",
|
|
1009
|
+
"--output-schema",
|
|
1010
|
+
this.schemaPath,
|
|
1011
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
1012
|
+
"--color",
|
|
1013
|
+
"never"
|
|
1014
|
+
], {
|
|
1015
|
+
cwd,
|
|
1016
|
+
shell: shouldUseWindowsShell$1(this.bin, this.platform),
|
|
1017
|
+
stdio: [
|
|
1018
|
+
"ignore",
|
|
1019
|
+
"pipe",
|
|
1020
|
+
"pipe"
|
|
1021
|
+
],
|
|
1022
|
+
env: process.env
|
|
1023
|
+
});
|
|
1024
|
+
if (setupAbortHandler(signal, child, reject, () => terminateCodexProcess(child, this.platform))) return;
|
|
1025
|
+
let lastAgentMessage = null;
|
|
1026
|
+
const cumulative = {
|
|
1027
|
+
inputTokens: 0,
|
|
1028
|
+
outputTokens: 0,
|
|
1029
|
+
cacheReadTokens: 0,
|
|
1030
|
+
cacheCreationTokens: 0
|
|
1031
|
+
};
|
|
1032
|
+
parseJSONLStream(child.stdout, logStream, (event) => {
|
|
1033
|
+
if (event.type === "item.completed" && "item" in event && event.item.type === "agent_message") {
|
|
1034
|
+
lastAgentMessage = event.item.text;
|
|
1035
|
+
onMessage?.(lastAgentMessage);
|
|
1036
|
+
}
|
|
1037
|
+
if (event.type === "turn.completed" && "usage" in event) {
|
|
1038
|
+
const u = event.usage;
|
|
1039
|
+
cumulative.inputTokens += u.input_tokens ?? 0;
|
|
1040
|
+
cumulative.outputTokens += u.output_tokens ?? 0;
|
|
1041
|
+
cumulative.cacheReadTokens += u.cached_input_tokens ?? 0;
|
|
1042
|
+
onUsage?.({ ...cumulative });
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
setupChildProcessHandlers(child, "codex", logStream, reject, () => {
|
|
1046
|
+
if (!lastAgentMessage) {
|
|
1047
|
+
reject(/* @__PURE__ */ new Error("codex returned no agent message"));
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
try {
|
|
1051
|
+
resolve({
|
|
1052
|
+
output: JSON.parse(lastAgentMessage),
|
|
1053
|
+
usage: cumulative
|
|
1054
|
+
});
|
|
1055
|
+
} catch (err) {
|
|
1056
|
+
reject(/* @__PURE__ */ new Error(`Failed to parse codex output: ${err instanceof Error ? err.message : err}`));
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
//#endregion
|
|
1063
|
+
//#region src/core/agents/opencode.ts
|
|
1064
|
+
const BLANKET_PERMISSION_RULESET = [{
|
|
1065
|
+
permission: "*",
|
|
1066
|
+
pattern: "*",
|
|
1067
|
+
action: "allow"
|
|
1068
|
+
}];
|
|
1069
|
+
const STRUCTURED_OUTPUT_FORMAT = {
|
|
1070
|
+
type: "json_schema",
|
|
1071
|
+
schema: AGENT_OUTPUT_SCHEMA,
|
|
1072
|
+
retryCount: 1
|
|
1073
|
+
};
|
|
1074
|
+
function buildOpencodeChildEnv() {
|
|
1075
|
+
const env = { ...process.env };
|
|
1076
|
+
delete env.OPENCODE_SERVER_USERNAME;
|
|
1077
|
+
delete env.OPENCODE_SERVER_PASSWORD;
|
|
1078
|
+
return env;
|
|
1079
|
+
}
|
|
1080
|
+
function buildPrompt(prompt) {
|
|
1081
|
+
return [
|
|
1082
|
+
prompt,
|
|
1083
|
+
"",
|
|
1084
|
+
"When you finish, reply with only valid JSON.",
|
|
1085
|
+
"Do not wrap the JSON in markdown fences.",
|
|
1086
|
+
"Do not include any prose before or after the JSON.",
|
|
1087
|
+
`The JSON must match this schema exactly: ${JSON.stringify(AGENT_OUTPUT_SCHEMA)}`
|
|
1088
|
+
].join("\n");
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* On Windows with `shell: true`, `child.pid` is the `cmd.exe` wrapper, not
|
|
1092
|
+
* the actual server process. `taskkill /T` terminates the entire process
|
|
1093
|
+
* tree rooted at that PID so the real server doesn't survive shutdown.
|
|
1094
|
+
*/
|
|
1095
|
+
async function killWindowsProcessTree(pid) {
|
|
1096
|
+
try {
|
|
1097
|
+
execFileSync("taskkill", [
|
|
1098
|
+
"/T",
|
|
1099
|
+
"/F",
|
|
1100
|
+
"/PID",
|
|
1101
|
+
String(pid)
|
|
1102
|
+
], { stdio: "ignore" });
|
|
1103
|
+
} catch {}
|
|
1104
|
+
}
|
|
1105
|
+
function createAbortError$1() {
|
|
1106
|
+
return /* @__PURE__ */ new Error("Agent was aborted");
|
|
1107
|
+
}
|
|
1108
|
+
function isAgentAbortError(error) {
|
|
1109
|
+
return error instanceof Error && error.message === "Agent was aborted";
|
|
1110
|
+
}
|
|
1111
|
+
function isAbortError$1(error) {
|
|
1112
|
+
return error instanceof Error && error.name === "AbortError";
|
|
1113
|
+
}
|
|
1114
|
+
function getAvailablePort$1() {
|
|
1115
|
+
return new Promise((resolve, reject) => {
|
|
1116
|
+
const server = createServer();
|
|
1117
|
+
server.unref();
|
|
1118
|
+
server.on("error", reject);
|
|
1119
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1120
|
+
const address = server.address();
|
|
1121
|
+
if (!address || typeof address === "string") {
|
|
1122
|
+
server.close();
|
|
1123
|
+
reject(/* @__PURE__ */ new Error("Failed to allocate a port for opencode"));
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
server.close((error) => {
|
|
1127
|
+
if (error) {
|
|
1128
|
+
reject(error);
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
resolve(address.port);
|
|
1132
|
+
});
|
|
1133
|
+
});
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
async function delay$1(ms, signal) {
|
|
1137
|
+
if (!signal) {
|
|
1138
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
await new Promise((resolve, reject) => {
|
|
1142
|
+
const timer = setTimeout(() => {
|
|
1143
|
+
signal.removeEventListener("abort", onAbort);
|
|
1144
|
+
resolve();
|
|
1145
|
+
}, ms);
|
|
1146
|
+
const onAbort = () => {
|
|
1147
|
+
clearTimeout(timer);
|
|
1148
|
+
signal.removeEventListener("abort", onAbort);
|
|
1149
|
+
reject(createAbortError$1());
|
|
1150
|
+
};
|
|
1151
|
+
if (signal.aborted) {
|
|
1152
|
+
onAbort();
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
function toUsage(tokens) {
|
|
1159
|
+
return {
|
|
1160
|
+
inputTokens: tokens?.input ?? 0,
|
|
1161
|
+
outputTokens: tokens?.output ?? 0,
|
|
1162
|
+
cacheReadTokens: tokens?.cache?.read ?? 0,
|
|
1163
|
+
cacheCreationTokens: tokens?.cache?.write ?? 0
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
function withTimeoutSignal$1(signal, timeoutMs) {
|
|
1167
|
+
if (timeoutMs === void 0) return signal;
|
|
1168
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
1169
|
+
return signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
1170
|
+
}
|
|
1171
|
+
var OpenCodeAgent = class {
|
|
1172
|
+
name = "opencode";
|
|
1173
|
+
bin;
|
|
1174
|
+
model;
|
|
1175
|
+
fetchFn;
|
|
1176
|
+
getPortFn;
|
|
1177
|
+
killProcessFn;
|
|
1178
|
+
platform;
|
|
1179
|
+
spawnFn;
|
|
1180
|
+
server = null;
|
|
1181
|
+
closingPromise = null;
|
|
1182
|
+
constructor(deps = {}) {
|
|
1183
|
+
this.bin = deps.bin ?? "opencode";
|
|
1184
|
+
this.model = deps.model ?? "";
|
|
1185
|
+
this.fetchFn = deps.fetch ?? fetch;
|
|
1186
|
+
this.getPortFn = deps.getPort ?? getAvailablePort$1;
|
|
1187
|
+
this.killProcessFn = deps.killProcess ?? process.kill.bind(process);
|
|
1188
|
+
this.platform = deps.platform ?? process.platform;
|
|
1189
|
+
this.spawnFn = deps.spawn ?? spawn;
|
|
1190
|
+
}
|
|
1191
|
+
async run(prompt, cwd, options) {
|
|
1192
|
+
const { onUsage, onMessage, signal, logPath } = options ?? {};
|
|
1193
|
+
const logStream = logPath ? createWriteStream(logPath) : null;
|
|
1194
|
+
const runController = new AbortController();
|
|
1195
|
+
let sessionId = null;
|
|
1196
|
+
const onAbort = () => {
|
|
1197
|
+
runController.abort();
|
|
1198
|
+
};
|
|
1199
|
+
if (signal?.aborted) {
|
|
1200
|
+
logStream?.end();
|
|
1201
|
+
throw createAbortError$1();
|
|
1202
|
+
}
|
|
1203
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1204
|
+
try {
|
|
1205
|
+
const server = await this.ensureServer(cwd, runController.signal);
|
|
1206
|
+
sessionId = await this.createSession(server, cwd, runController.signal);
|
|
1207
|
+
return await this.streamMessage(server, sessionId, buildPrompt(prompt), runController.signal, logStream, onUsage, onMessage);
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
if (runController.signal.aborted || isAbortError$1(error)) throw createAbortError$1();
|
|
1210
|
+
throw error;
|
|
1211
|
+
} finally {
|
|
1212
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1213
|
+
logStream?.end();
|
|
1214
|
+
if (this.server && sessionId) {
|
|
1215
|
+
if (runController.signal.aborted) await this.abortSession(this.server, sessionId);
|
|
1216
|
+
await this.deleteSession(this.server, sessionId);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
async close() {
|
|
1221
|
+
await this.shutdownServer();
|
|
1222
|
+
}
|
|
1223
|
+
async ensureServer(cwd, signal) {
|
|
1224
|
+
if (this.server && !this.server.closed) if (this.server.cwd !== cwd) await this.shutdownServer();
|
|
1225
|
+
else {
|
|
1226
|
+
await this.server.readyPromise;
|
|
1227
|
+
return this.server;
|
|
1228
|
+
}
|
|
1229
|
+
if (this.server && !this.server.closed) {
|
|
1230
|
+
await this.server.readyPromise;
|
|
1231
|
+
return this.server;
|
|
1232
|
+
}
|
|
1233
|
+
const port = await this.getPortFn();
|
|
1234
|
+
const isWindows = this.platform === "win32";
|
|
1235
|
+
const detached = !isWindows;
|
|
1236
|
+
const args = [
|
|
1237
|
+
"serve",
|
|
1238
|
+
"--hostname",
|
|
1239
|
+
"127.0.0.1",
|
|
1240
|
+
"--port",
|
|
1241
|
+
String(port),
|
|
1242
|
+
"--print-logs"
|
|
1243
|
+
];
|
|
1244
|
+
const child = this.spawnFn(this.bin, args, {
|
|
1245
|
+
cwd,
|
|
1246
|
+
detached,
|
|
1247
|
+
shell: isWindows,
|
|
1248
|
+
stdio: [
|
|
1249
|
+
"ignore",
|
|
1250
|
+
"pipe",
|
|
1251
|
+
"pipe"
|
|
1252
|
+
],
|
|
1253
|
+
env: buildOpencodeChildEnv()
|
|
1254
|
+
});
|
|
1255
|
+
const server = {
|
|
1256
|
+
baseUrl: `http://127.0.0.1:${port}`,
|
|
1257
|
+
child,
|
|
1258
|
+
closed: false,
|
|
1259
|
+
cwd,
|
|
1260
|
+
detached,
|
|
1261
|
+
port,
|
|
1262
|
+
readyPromise: Promise.resolve(),
|
|
1263
|
+
stderr: "",
|
|
1264
|
+
stdout: ""
|
|
1265
|
+
};
|
|
1266
|
+
const maxOutput = 64 * 1024;
|
|
1267
|
+
child.stdout.on("data", (data) => {
|
|
1268
|
+
server.stdout += data.toString();
|
|
1269
|
+
if (server.stdout.length > maxOutput) server.stdout = server.stdout.slice(-maxOutput);
|
|
1270
|
+
});
|
|
1271
|
+
child.stderr.on("data", (data) => {
|
|
1272
|
+
server.stderr += data.toString();
|
|
1273
|
+
if (server.stderr.length > maxOutput) server.stderr = server.stderr.slice(-maxOutput);
|
|
1274
|
+
});
|
|
1275
|
+
child.on("close", () => {
|
|
1276
|
+
server.closed = true;
|
|
1277
|
+
if (this.server === server) this.server = null;
|
|
1278
|
+
});
|
|
1279
|
+
this.server = server;
|
|
1280
|
+
appendDebugLog("opencode:spawn", {
|
|
1281
|
+
cwd,
|
|
1282
|
+
port,
|
|
1283
|
+
detached
|
|
1284
|
+
});
|
|
1285
|
+
server.readyPromise = this.waitForHealthy(server, signal).catch(async (error) => {
|
|
1286
|
+
await this.shutdownServer();
|
|
1287
|
+
throw error;
|
|
1288
|
+
});
|
|
1289
|
+
await server.readyPromise;
|
|
1290
|
+
return server;
|
|
1291
|
+
}
|
|
1292
|
+
async waitForHealthy(server, signal) {
|
|
1293
|
+
const deadline = Date.now() + 3e4;
|
|
1294
|
+
let spawnErrorMessage = null;
|
|
1295
|
+
server.child.once("error", (error) => {
|
|
1296
|
+
spawnErrorMessage = error.message;
|
|
1297
|
+
});
|
|
1298
|
+
while (Date.now() < deadline) {
|
|
1299
|
+
if (signal?.aborted) throw createAbortError$1();
|
|
1300
|
+
if (spawnErrorMessage) throw new Error(`Failed to spawn opencode: ${spawnErrorMessage}`);
|
|
1301
|
+
if (server.closed) {
|
|
1302
|
+
const output = server.stderr.trim() || server.stdout.trim();
|
|
1303
|
+
throw new Error(output ? `opencode exited before becoming ready: ${output}` : "opencode exited before becoming ready");
|
|
1304
|
+
}
|
|
1305
|
+
try {
|
|
1306
|
+
if ((await this.fetchFn(`${server.baseUrl}/global/health`, {
|
|
1307
|
+
method: "GET",
|
|
1308
|
+
signal
|
|
1309
|
+
})).ok) return;
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
if (isAbortError$1(error)) throw createAbortError$1();
|
|
1312
|
+
}
|
|
1313
|
+
await delay$1(250, signal);
|
|
1314
|
+
}
|
|
1315
|
+
throw new Error(`Timed out waiting for opencode serve to become ready on port ${server.port}`);
|
|
1316
|
+
}
|
|
1317
|
+
async createSession(server, cwd, signal) {
|
|
1318
|
+
return (await this.requestJSON(server, "/session", {
|
|
1319
|
+
method: "POST",
|
|
1320
|
+
body: {
|
|
1321
|
+
directory: cwd,
|
|
1322
|
+
permission: BLANKET_PERMISSION_RULESET
|
|
1323
|
+
},
|
|
1324
|
+
signal
|
|
1325
|
+
})).id;
|
|
1326
|
+
}
|
|
1327
|
+
async streamMessage(server, sessionId, prompt, signal, logStream, onUsage, onMessage) {
|
|
1328
|
+
const streamAbortController = new AbortController();
|
|
1329
|
+
const streamSignal = AbortSignal.any([signal, streamAbortController.signal]);
|
|
1330
|
+
const eventResponse = await this.request(server, "/global/event", {
|
|
1331
|
+
method: "GET",
|
|
1332
|
+
headers: { accept: "text/event-stream" },
|
|
1333
|
+
signal: streamSignal
|
|
1334
|
+
});
|
|
1335
|
+
if (!eventResponse.body) throw new Error("opencode returned no event stream body");
|
|
1336
|
+
let messageRequestError = null;
|
|
1337
|
+
const messageRequest = (async () => {
|
|
1338
|
+
try {
|
|
1339
|
+
return {
|
|
1340
|
+
ok: true,
|
|
1341
|
+
body: await this.requestText(server, `/session/${sessionId}/message`, {
|
|
1342
|
+
method: "POST",
|
|
1343
|
+
body: {
|
|
1344
|
+
role: "user",
|
|
1345
|
+
parts: [{
|
|
1346
|
+
type: "text",
|
|
1347
|
+
text: prompt
|
|
1348
|
+
}],
|
|
1349
|
+
...this.model ? { model: (() => {
|
|
1350
|
+
const parts = this.model.split("/");
|
|
1351
|
+
if (parts.length === 2) return {
|
|
1352
|
+
providerID: parts[0],
|
|
1353
|
+
modelID: parts[1]
|
|
1354
|
+
};
|
|
1355
|
+
return {
|
|
1356
|
+
providerID: "opencode",
|
|
1357
|
+
modelID: this.model
|
|
1358
|
+
};
|
|
1359
|
+
})() } : {},
|
|
1360
|
+
format: STRUCTURED_OUTPUT_FORMAT
|
|
1361
|
+
},
|
|
1362
|
+
signal
|
|
1363
|
+
})
|
|
1364
|
+
};
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
messageRequestError = error;
|
|
1367
|
+
streamAbortController.abort();
|
|
1368
|
+
return {
|
|
1369
|
+
ok: false,
|
|
1370
|
+
error
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
})();
|
|
1374
|
+
const usage = {
|
|
1375
|
+
inputTokens: 0,
|
|
1376
|
+
outputTokens: 0,
|
|
1377
|
+
cacheReadTokens: 0,
|
|
1378
|
+
cacheCreationTokens: 0
|
|
1379
|
+
};
|
|
1380
|
+
const usageByMessageId = /* @__PURE__ */ new Map();
|
|
1381
|
+
const textParts = /* @__PURE__ */ new Map();
|
|
1382
|
+
let lastText = null;
|
|
1383
|
+
let lastFinalAnswerText = null;
|
|
1384
|
+
let lastUsageSignature = "0:0:0:0";
|
|
1385
|
+
const updateUsage = (messageId, tokens) => {
|
|
1386
|
+
if (!messageId || !tokens) return;
|
|
1387
|
+
usageByMessageId.set(messageId, toUsage(tokens));
|
|
1388
|
+
let nextInputTokens = 0;
|
|
1389
|
+
let nextOutputTokens = 0;
|
|
1390
|
+
let nextCacheReadTokens = 0;
|
|
1391
|
+
let nextCacheCreationTokens = 0;
|
|
1392
|
+
for (const messageUsage of usageByMessageId.values()) {
|
|
1393
|
+
nextInputTokens += messageUsage.inputTokens;
|
|
1394
|
+
nextOutputTokens += messageUsage.outputTokens;
|
|
1395
|
+
nextCacheReadTokens += messageUsage.cacheReadTokens;
|
|
1396
|
+
nextCacheCreationTokens += messageUsage.cacheCreationTokens;
|
|
1397
|
+
}
|
|
1398
|
+
const signature = [
|
|
1399
|
+
nextInputTokens,
|
|
1400
|
+
nextOutputTokens,
|
|
1401
|
+
nextCacheReadTokens,
|
|
1402
|
+
nextCacheCreationTokens
|
|
1403
|
+
].join(":");
|
|
1404
|
+
usage.inputTokens = nextInputTokens;
|
|
1405
|
+
usage.outputTokens = nextOutputTokens;
|
|
1406
|
+
usage.cacheReadTokens = nextCacheReadTokens;
|
|
1407
|
+
usage.cacheCreationTokens = nextCacheCreationTokens;
|
|
1408
|
+
if (signature !== lastUsageSignature) {
|
|
1409
|
+
lastUsageSignature = signature;
|
|
1410
|
+
onUsage?.({ ...usage });
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
const emitText = (partId, nextText, phase) => {
|
|
1414
|
+
const trimmed = nextText.trim();
|
|
1415
|
+
textParts.set(partId, {
|
|
1416
|
+
text: nextText,
|
|
1417
|
+
phase
|
|
1418
|
+
});
|
|
1419
|
+
if (!trimmed) return;
|
|
1420
|
+
lastText = nextText;
|
|
1421
|
+
if (phase === "final_answer") lastFinalAnswerText = nextText;
|
|
1422
|
+
onMessage?.(trimmed);
|
|
1423
|
+
};
|
|
1424
|
+
const handleEvent = (event) => {
|
|
1425
|
+
const payload = event.payload;
|
|
1426
|
+
const properties = payload?.properties;
|
|
1427
|
+
if (!properties || properties.sessionID !== sessionId) return false;
|
|
1428
|
+
if (payload?.type === "message.part.delta" && properties.field === "text" && typeof properties.partID === "string" && typeof properties.delta === "string") {
|
|
1429
|
+
const current = textParts.get(properties.partID);
|
|
1430
|
+
emitText(properties.partID, `${current?.text ?? ""}${properties.delta}`, current?.phase);
|
|
1431
|
+
return false;
|
|
1432
|
+
}
|
|
1433
|
+
if (payload?.type === "message.part.updated") {
|
|
1434
|
+
const part = properties.part;
|
|
1435
|
+
if (!part) return false;
|
|
1436
|
+
if (part.type === "text" && typeof part.id === "string") {
|
|
1437
|
+
emitText(part.id, part.text ?? "", part.metadata?.openai?.phase);
|
|
1438
|
+
return false;
|
|
1439
|
+
}
|
|
1440
|
+
if (part.type === "step-finish") {
|
|
1441
|
+
updateUsage(part.messageID, part.tokens);
|
|
1442
|
+
return false;
|
|
1443
|
+
}
|
|
1444
|
+
return false;
|
|
1445
|
+
}
|
|
1446
|
+
if (payload?.type === "message.updated") {
|
|
1447
|
+
if (properties.info?.role === "assistant") updateUsage(properties.info.id, properties.info.tokens);
|
|
1448
|
+
return false;
|
|
1449
|
+
}
|
|
1450
|
+
return payload?.type === "session.idle";
|
|
1451
|
+
};
|
|
1452
|
+
const decoder = new TextDecoder();
|
|
1453
|
+
const reader = eventResponse.body.getReader();
|
|
1454
|
+
let buffer = "";
|
|
1455
|
+
let sawSessionIdle = false;
|
|
1456
|
+
const processRawEvent = (rawEvent) => {
|
|
1457
|
+
if (!rawEvent.trim()) return;
|
|
1458
|
+
const dataLines = rawEvent.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart());
|
|
1459
|
+
if (dataLines.length === 0) return;
|
|
1460
|
+
try {
|
|
1461
|
+
if (handleEvent(JSON.parse(dataLines.join("\n")))) sawSessionIdle = true;
|
|
1462
|
+
} catch {}
|
|
1463
|
+
};
|
|
1464
|
+
const processBufferedEvents = (flushRemainder = false) => {
|
|
1465
|
+
while (true) {
|
|
1466
|
+
const lfBoundary = buffer.indexOf("\n\n");
|
|
1467
|
+
const crlfBoundary = buffer.indexOf("\r\n\r\n");
|
|
1468
|
+
let boundary;
|
|
1469
|
+
let separatorLen;
|
|
1470
|
+
if (lfBoundary === -1 && crlfBoundary === -1) break;
|
|
1471
|
+
if (crlfBoundary !== -1 && (lfBoundary === -1 || crlfBoundary < lfBoundary)) {
|
|
1472
|
+
boundary = crlfBoundary;
|
|
1473
|
+
separatorLen = 4;
|
|
1474
|
+
} else {
|
|
1475
|
+
boundary = lfBoundary;
|
|
1476
|
+
separatorLen = 2;
|
|
1477
|
+
}
|
|
1478
|
+
processRawEvent(buffer.slice(0, boundary));
|
|
1479
|
+
buffer = buffer.slice(boundary + separatorLen);
|
|
1480
|
+
if (sawSessionIdle) return;
|
|
1481
|
+
}
|
|
1482
|
+
if (flushRemainder && buffer.trim()) {
|
|
1483
|
+
processRawEvent(buffer);
|
|
1484
|
+
buffer = "";
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
try {
|
|
1488
|
+
while (!sawSessionIdle) {
|
|
1489
|
+
let readResult;
|
|
1490
|
+
try {
|
|
1491
|
+
readResult = await reader.read();
|
|
1492
|
+
} catch (error) {
|
|
1493
|
+
if (messageRequestError) {
|
|
1494
|
+
if (isAbortError$1(messageRequestError) || isAgentAbortError(messageRequestError)) throw createAbortError$1();
|
|
1495
|
+
throw messageRequestError;
|
|
1496
|
+
}
|
|
1497
|
+
if (isAbortError$1(error)) throw createAbortError$1();
|
|
1498
|
+
throw error;
|
|
1499
|
+
}
|
|
1500
|
+
if (readResult.done) {
|
|
1501
|
+
const tail = decoder.decode();
|
|
1502
|
+
if (tail) {
|
|
1503
|
+
logStream?.write(tail);
|
|
1504
|
+
buffer += tail;
|
|
1505
|
+
}
|
|
1506
|
+
processBufferedEvents(true);
|
|
1507
|
+
break;
|
|
1508
|
+
}
|
|
1509
|
+
const chunk = decoder.decode(readResult.value, { stream: true });
|
|
1510
|
+
logStream?.write(chunk);
|
|
1511
|
+
buffer += chunk;
|
|
1512
|
+
processBufferedEvents();
|
|
1513
|
+
}
|
|
1514
|
+
} finally {
|
|
1515
|
+
streamAbortController.abort();
|
|
1516
|
+
await reader.cancel().catch(() => void 0);
|
|
1517
|
+
}
|
|
1518
|
+
const messageResult = await messageRequest;
|
|
1519
|
+
if (!messageResult.ok) {
|
|
1520
|
+
if (isAbortError$1(messageResult.error) || isAgentAbortError(messageResult.error)) throw createAbortError$1();
|
|
1521
|
+
throw messageResult.error;
|
|
1522
|
+
}
|
|
1523
|
+
const body = messageResult.body;
|
|
1524
|
+
let response;
|
|
1525
|
+
try {
|
|
1526
|
+
response = JSON.parse(body);
|
|
1527
|
+
} catch (error) {
|
|
1528
|
+
throw new Error(`Failed to parse opencode response: ${error instanceof Error ? error.message : String(error)}`);
|
|
1529
|
+
}
|
|
1530
|
+
if (response.info?.role === "assistant") updateUsage(response.info.id, response.info.tokens);
|
|
1531
|
+
for (const part of response.parts ?? []) {
|
|
1532
|
+
if (part.type !== "text" || typeof part.text !== "string") continue;
|
|
1533
|
+
if (!part.text.trim()) continue;
|
|
1534
|
+
lastText = part.text;
|
|
1535
|
+
if (part.metadata?.openai?.phase === "final_answer") lastFinalAnswerText = part.text;
|
|
1536
|
+
}
|
|
1537
|
+
if (response.info?.structured) return {
|
|
1538
|
+
output: response.info.structured,
|
|
1539
|
+
usage
|
|
1540
|
+
};
|
|
1541
|
+
const outputText = lastFinalAnswerText ?? lastText;
|
|
1542
|
+
if (!outputText) throw new Error("opencode returned no text output");
|
|
1543
|
+
try {
|
|
1544
|
+
return {
|
|
1545
|
+
output: JSON.parse(outputText),
|
|
1546
|
+
usage
|
|
1547
|
+
};
|
|
1548
|
+
} catch (error) {
|
|
1549
|
+
throw new Error(`Failed to parse opencode output: ${error instanceof Error ? error.message : String(error)}`);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
async deleteSession(server, sessionId) {
|
|
1553
|
+
try {
|
|
1554
|
+
await this.request(server, `/session/${sessionId}`, {
|
|
1555
|
+
method: "DELETE",
|
|
1556
|
+
timeoutMs: 1e3
|
|
1557
|
+
});
|
|
1558
|
+
} catch {}
|
|
1559
|
+
}
|
|
1560
|
+
async abortSession(server, sessionId) {
|
|
1561
|
+
try {
|
|
1562
|
+
await this.request(server, `/session/${sessionId}/abort`, {
|
|
1563
|
+
method: "POST",
|
|
1564
|
+
timeoutMs: 1e3
|
|
1565
|
+
});
|
|
1566
|
+
} catch {}
|
|
1567
|
+
}
|
|
1568
|
+
async shutdownServer() {
|
|
1569
|
+
if (!this.server || this.server.closed) {
|
|
1570
|
+
this.server = null;
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
if (this.closingPromise) {
|
|
1574
|
+
await this.closingPromise;
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
const server = this.server;
|
|
1578
|
+
appendDebugLog("opencode:shutdown", {
|
|
1579
|
+
cwd: server.cwd,
|
|
1580
|
+
port: server.port
|
|
1581
|
+
});
|
|
1582
|
+
this.closingPromise = (this.platform === "win32" && server.child.pid ? killWindowsProcessTree(server.child.pid) : shutdownChildProcess(server.child, {
|
|
1583
|
+
detached: server.detached,
|
|
1584
|
+
killProcess: this.killProcessFn,
|
|
1585
|
+
timeoutMs: 3e3
|
|
1586
|
+
})).finally(() => {
|
|
1587
|
+
if (this.server === server) this.server = null;
|
|
1588
|
+
this.closingPromise = null;
|
|
1589
|
+
});
|
|
1590
|
+
await this.closingPromise;
|
|
1591
|
+
}
|
|
1592
|
+
async requestJSON(server, path, options) {
|
|
1593
|
+
const body = await this.requestText(server, path, options);
|
|
1594
|
+
return JSON.parse(body);
|
|
1595
|
+
}
|
|
1596
|
+
async requestText(server, path, options) {
|
|
1597
|
+
return await (await this.request(server, path, options)).text();
|
|
1598
|
+
}
|
|
1599
|
+
async request(server, path, options) {
|
|
1600
|
+
const headers = new Headers(options.headers);
|
|
1601
|
+
if (options.body !== void 0) headers.set("content-type", "application/json");
|
|
1602
|
+
const signal = withTimeoutSignal$1(options.signal, options.timeoutMs);
|
|
1603
|
+
const response = await this.fetchFn(`${server.baseUrl}${path}`, {
|
|
1604
|
+
method: options.method,
|
|
1605
|
+
headers,
|
|
1606
|
+
body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
|
|
1607
|
+
signal
|
|
1608
|
+
});
|
|
1609
|
+
if (!response.ok) {
|
|
1610
|
+
const body = await response.text();
|
|
1611
|
+
throw new Error(`opencode ${options.method} ${path} failed with ${response.status}: ${body}`);
|
|
1612
|
+
}
|
|
1613
|
+
return response;
|
|
1614
|
+
}
|
|
1615
|
+
};
|
|
1616
|
+
//#endregion
|
|
1617
|
+
//#region src/core/agents/rovodev.ts
|
|
1618
|
+
function buildSystemPrompt(schema) {
|
|
1619
|
+
return [
|
|
1620
|
+
"You are the coding agent used by fttm.",
|
|
1621
|
+
"Work autonomously in the current workspace and use tools when needed.",
|
|
1622
|
+
"When you finish, reply with only valid JSON.",
|
|
1623
|
+
"Do not wrap the JSON in markdown fences.",
|
|
1624
|
+
"Do not include any prose before or after the JSON.",
|
|
1625
|
+
`The JSON must match this schema exactly: ${schema}`
|
|
1626
|
+
].join(" ");
|
|
1627
|
+
}
|
|
1628
|
+
function createAbortError() {
|
|
1629
|
+
return /* @__PURE__ */ new Error("Agent was aborted");
|
|
1630
|
+
}
|
|
1631
|
+
function isAbortError(error) {
|
|
1632
|
+
return error instanceof Error && error.name === "AbortError";
|
|
1633
|
+
}
|
|
1634
|
+
function shouldUseWindowsShell(bin, platform) {
|
|
1635
|
+
if (platform !== "win32") return false;
|
|
1636
|
+
if (/\.(cmd|bat)$/i.test(bin)) return true;
|
|
1637
|
+
if (/[\\/]/.test(bin)) return false;
|
|
1638
|
+
try {
|
|
1639
|
+
const firstMatch = execFileSync("where", [bin], {
|
|
1640
|
+
encoding: "utf8",
|
|
1641
|
+
stdio: [
|
|
1642
|
+
"ignore",
|
|
1643
|
+
"pipe",
|
|
1644
|
+
"ignore"
|
|
1645
|
+
]
|
|
1646
|
+
}).split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
1647
|
+
return firstMatch ? /\.(cmd|bat)$/i.test(firstMatch) : false;
|
|
1648
|
+
} catch {
|
|
1649
|
+
return false;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
function terminateRovoDevProcess(child, platform) {
|
|
1653
|
+
if (platform === "win32" && child.pid) {
|
|
1654
|
+
try {
|
|
1655
|
+
execFileSync("taskkill", [
|
|
1656
|
+
"/T",
|
|
1657
|
+
"/F",
|
|
1658
|
+
"/PID",
|
|
1659
|
+
String(child.pid)
|
|
1660
|
+
], { stdio: "ignore" });
|
|
1661
|
+
} catch {}
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
child.kill("SIGTERM");
|
|
1665
|
+
}
|
|
1666
|
+
function getAvailablePort() {
|
|
1667
|
+
return new Promise((resolve, reject) => {
|
|
1668
|
+
const server = createServer();
|
|
1669
|
+
server.unref();
|
|
1670
|
+
server.on("error", reject);
|
|
1671
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1672
|
+
const address = server.address();
|
|
1673
|
+
if (!address || typeof address === "string") {
|
|
1674
|
+
server.close();
|
|
1675
|
+
reject(/* @__PURE__ */ new Error("Failed to allocate a port for rovodev"));
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
server.close((error) => {
|
|
1679
|
+
if (error) {
|
|
1680
|
+
reject(error);
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
resolve(address.port);
|
|
1684
|
+
});
|
|
1685
|
+
});
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
async function delay(ms, signal) {
|
|
1689
|
+
if (!signal) {
|
|
1690
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
await new Promise((resolve, reject) => {
|
|
1694
|
+
const timer = setTimeout(() => {
|
|
1695
|
+
signal.removeEventListener("abort", onAbort);
|
|
1696
|
+
resolve();
|
|
1697
|
+
}, ms);
|
|
1698
|
+
const onAbort = () => {
|
|
1699
|
+
clearTimeout(timer);
|
|
1700
|
+
signal.removeEventListener("abort", onAbort);
|
|
1701
|
+
reject(createAbortError());
|
|
1702
|
+
};
|
|
1703
|
+
if (signal.aborted) {
|
|
1704
|
+
onAbort();
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
var RovoDevAgent = class {
|
|
1711
|
+
name = "rovodev";
|
|
1712
|
+
bin;
|
|
1713
|
+
schemaPath;
|
|
1714
|
+
fetchFn;
|
|
1715
|
+
getPortFn;
|
|
1716
|
+
killProcessFn;
|
|
1717
|
+
platform;
|
|
1718
|
+
spawnFn;
|
|
1719
|
+
server = null;
|
|
1720
|
+
closingPromise = null;
|
|
1721
|
+
constructor(schemaPath, deps = {}) {
|
|
1722
|
+
this.bin = deps.bin ?? "acli";
|
|
1723
|
+
this.schemaPath = schemaPath;
|
|
1724
|
+
this.fetchFn = deps.fetch ?? fetch;
|
|
1725
|
+
this.getPortFn = deps.getPort ?? getAvailablePort;
|
|
1726
|
+
this.killProcessFn = deps.killProcess ?? process.kill.bind(process);
|
|
1727
|
+
this.platform = deps.platform ?? process.platform;
|
|
1728
|
+
this.spawnFn = deps.spawn ?? spawn;
|
|
1729
|
+
}
|
|
1730
|
+
async run(prompt, cwd, options) {
|
|
1731
|
+
const { onUsage, onMessage, signal, logPath } = options ?? {};
|
|
1732
|
+
const logStream = logPath ? createWriteStream(logPath) : null;
|
|
1733
|
+
const runController = new AbortController();
|
|
1734
|
+
let sessionId = null;
|
|
1735
|
+
const onAbort = () => {
|
|
1736
|
+
runController.abort();
|
|
1737
|
+
};
|
|
1738
|
+
if (signal?.aborted) {
|
|
1739
|
+
logStream?.end();
|
|
1740
|
+
throw createAbortError();
|
|
1741
|
+
}
|
|
1742
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1743
|
+
try {
|
|
1744
|
+
const server = await this.ensureServer(cwd, runController.signal);
|
|
1745
|
+
sessionId = await this.createSession(server, runController.signal);
|
|
1746
|
+
await this.setInlineSystemPrompt(server, sessionId, runController.signal);
|
|
1747
|
+
await this.setChatMessage(server, sessionId, prompt, runController.signal);
|
|
1748
|
+
return await this.streamChat(server, sessionId, runController.signal, logStream, onUsage, onMessage);
|
|
1749
|
+
} catch (error) {
|
|
1750
|
+
if (runController.signal.aborted || isAbortError(error)) throw createAbortError();
|
|
1751
|
+
throw error;
|
|
1752
|
+
} finally {
|
|
1753
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1754
|
+
logStream?.end();
|
|
1755
|
+
if (this.server && sessionId) {
|
|
1756
|
+
if (runController.signal.aborted) await this.cancelSession(this.server, sessionId);
|
|
1757
|
+
await this.deleteSession(this.server, sessionId);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
async close() {
|
|
1762
|
+
await this.shutdownServer();
|
|
1763
|
+
}
|
|
1764
|
+
async ensureServer(cwd, signal) {
|
|
1765
|
+
if (this.server && !this.server.closed && this.server.cwd === cwd) {
|
|
1766
|
+
await this.server.readyPromise;
|
|
1767
|
+
return this.server;
|
|
1768
|
+
}
|
|
1769
|
+
if (this.server && !this.server.closed) await this.shutdownServer();
|
|
1770
|
+
const port = await this.getPortFn();
|
|
1771
|
+
const detached = this.platform !== "win32";
|
|
1772
|
+
const child = this.spawnFn(this.bin, [
|
|
1773
|
+
"rovodev",
|
|
1774
|
+
"serve",
|
|
1775
|
+
"--disable-session-token",
|
|
1776
|
+
String(port)
|
|
1777
|
+
], {
|
|
1778
|
+
cwd,
|
|
1779
|
+
detached,
|
|
1780
|
+
shell: shouldUseWindowsShell(this.bin, this.platform),
|
|
1781
|
+
stdio: [
|
|
1782
|
+
"ignore",
|
|
1783
|
+
"pipe",
|
|
1784
|
+
"pipe"
|
|
1785
|
+
],
|
|
1786
|
+
env: process.env
|
|
1787
|
+
});
|
|
1788
|
+
const server = {
|
|
1789
|
+
baseUrl: `http://127.0.0.1:${port}`,
|
|
1790
|
+
child,
|
|
1791
|
+
cwd,
|
|
1792
|
+
detached,
|
|
1793
|
+
port,
|
|
1794
|
+
readyPromise: Promise.resolve(),
|
|
1795
|
+
closed: false,
|
|
1796
|
+
stdout: "",
|
|
1797
|
+
stderr: ""
|
|
1798
|
+
};
|
|
1799
|
+
const MAX_OUTPUT = 64 * 1024;
|
|
1800
|
+
child.stdout.on("data", (data) => {
|
|
1801
|
+
server.stdout += data.toString();
|
|
1802
|
+
if (server.stdout.length > MAX_OUTPUT) server.stdout = server.stdout.slice(-MAX_OUTPUT);
|
|
1803
|
+
});
|
|
1804
|
+
child.stderr.on("data", (data) => {
|
|
1805
|
+
server.stderr += data.toString();
|
|
1806
|
+
if (server.stderr.length > MAX_OUTPUT) server.stderr = server.stderr.slice(-MAX_OUTPUT);
|
|
1807
|
+
});
|
|
1808
|
+
child.on("close", () => {
|
|
1809
|
+
server.closed = true;
|
|
1810
|
+
if (this.server === server) this.server = null;
|
|
1811
|
+
});
|
|
1812
|
+
this.server = server;
|
|
1813
|
+
appendDebugLog("rovodev:spawn", {
|
|
1814
|
+
cwd,
|
|
1815
|
+
port,
|
|
1816
|
+
detached
|
|
1817
|
+
});
|
|
1818
|
+
server.readyPromise = this.waitForHealthy(server, signal).catch(async (error) => {
|
|
1819
|
+
await this.shutdownServer();
|
|
1820
|
+
throw error;
|
|
1821
|
+
});
|
|
1822
|
+
await server.readyPromise;
|
|
1823
|
+
return server;
|
|
1824
|
+
}
|
|
1825
|
+
async waitForHealthy(server, signal) {
|
|
1826
|
+
const deadline = Date.now() + 3e4;
|
|
1827
|
+
let spawnErrorMessage = null;
|
|
1828
|
+
server.child.once("error", (error) => {
|
|
1829
|
+
spawnErrorMessage = error.message;
|
|
1830
|
+
});
|
|
1831
|
+
while (Date.now() < deadline) {
|
|
1832
|
+
if (signal?.aborted) throw createAbortError();
|
|
1833
|
+
if (spawnErrorMessage) throw new Error(`Failed to spawn rovodev: ${spawnErrorMessage}`);
|
|
1834
|
+
if (server.closed) {
|
|
1835
|
+
const output = server.stderr.trim() || server.stdout.trim();
|
|
1836
|
+
throw new Error(output ? `rovodev exited before becoming ready: ${output}` : "rovodev exited before becoming ready");
|
|
1837
|
+
}
|
|
1838
|
+
try {
|
|
1839
|
+
if ((await this.fetchFn(`${server.baseUrl}/healthcheck`, {
|
|
1840
|
+
method: "GET",
|
|
1841
|
+
signal
|
|
1842
|
+
})).ok) return;
|
|
1843
|
+
} catch (error) {
|
|
1844
|
+
if (isAbortError(error)) throw createAbortError();
|
|
1845
|
+
}
|
|
1846
|
+
await delay(250, signal);
|
|
1847
|
+
}
|
|
1848
|
+
throw new Error(`Timed out waiting for rovodev serve to become ready on port ${server.port}`);
|
|
1849
|
+
}
|
|
1850
|
+
async createSession(server, signal) {
|
|
1851
|
+
return (await this.requestJSON(server, "/v3/sessions/create", {
|
|
1852
|
+
method: "POST",
|
|
1853
|
+
body: { custom_title: "fttm" },
|
|
1854
|
+
signal
|
|
1855
|
+
})).session_id;
|
|
1856
|
+
}
|
|
1857
|
+
async setInlineSystemPrompt(server, sessionId, signal) {
|
|
1858
|
+
const schema = readFileSync(this.schemaPath, "utf-8").trim();
|
|
1859
|
+
await this.requestJSON(server, "/v3/inline-system-prompt", {
|
|
1860
|
+
method: "PUT",
|
|
1861
|
+
sessionId,
|
|
1862
|
+
body: { prompt: buildSystemPrompt(schema) },
|
|
1863
|
+
signal
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
async setChatMessage(server, sessionId, prompt, signal) {
|
|
1867
|
+
await this.requestJSON(server, "/v3/set_chat_message", {
|
|
1868
|
+
method: "POST",
|
|
1869
|
+
sessionId,
|
|
1870
|
+
body: { message: prompt },
|
|
1871
|
+
signal
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
async cancelSession(server, sessionId) {
|
|
1875
|
+
try {
|
|
1876
|
+
await this.request(server, "/v3/cancel", {
|
|
1877
|
+
method: "POST",
|
|
1878
|
+
sessionId,
|
|
1879
|
+
timeoutMs: 1e3
|
|
1880
|
+
});
|
|
1881
|
+
} catch {}
|
|
1882
|
+
}
|
|
1883
|
+
async deleteSession(server, sessionId) {
|
|
1884
|
+
try {
|
|
1885
|
+
await this.request(server, `/v3/sessions/${sessionId}`, {
|
|
1886
|
+
method: "DELETE",
|
|
1887
|
+
sessionId,
|
|
1888
|
+
timeoutMs: 1e3
|
|
1889
|
+
});
|
|
1890
|
+
} catch {}
|
|
1891
|
+
}
|
|
1892
|
+
async streamChat(server, sessionId, signal, logStream, onUsage, onMessage) {
|
|
1893
|
+
const response = await this.request(server, "/v3/stream_chat", {
|
|
1894
|
+
method: "GET",
|
|
1895
|
+
sessionId,
|
|
1896
|
+
headers: { accept: "text/event-stream" },
|
|
1897
|
+
signal
|
|
1898
|
+
});
|
|
1899
|
+
if (!response.body) throw new Error("rovodev returned no response body");
|
|
1900
|
+
const usage = {
|
|
1901
|
+
inputTokens: 0,
|
|
1902
|
+
outputTokens: 0,
|
|
1903
|
+
cacheReadTokens: 0,
|
|
1904
|
+
cacheCreationTokens: 0
|
|
1905
|
+
};
|
|
1906
|
+
let latestTextSegment = "";
|
|
1907
|
+
let currentTextParts = [];
|
|
1908
|
+
let currentTextIndexes = /* @__PURE__ */ new Map();
|
|
1909
|
+
const decoder = new TextDecoder();
|
|
1910
|
+
const reader = response.body.getReader();
|
|
1911
|
+
let buffer = "";
|
|
1912
|
+
const emitMessage = () => {
|
|
1913
|
+
const message = currentTextParts.join("").trim();
|
|
1914
|
+
if (message) {
|
|
1915
|
+
latestTextSegment = message;
|
|
1916
|
+
onMessage?.(message);
|
|
1917
|
+
}
|
|
1918
|
+
};
|
|
1919
|
+
const resetCurrentMessage = () => {
|
|
1920
|
+
currentTextParts = [];
|
|
1921
|
+
currentTextIndexes = /* @__PURE__ */ new Map();
|
|
1922
|
+
};
|
|
1923
|
+
const handleUsage = (event) => {
|
|
1924
|
+
usage.inputTokens += event.input_tokens ?? 0;
|
|
1925
|
+
usage.outputTokens += event.output_tokens ?? 0;
|
|
1926
|
+
usage.cacheReadTokens += event.cache_read_tokens ?? 0;
|
|
1927
|
+
usage.cacheCreationTokens += event.cache_write_tokens ?? 0;
|
|
1928
|
+
onUsage?.({ ...usage });
|
|
1929
|
+
};
|
|
1930
|
+
const handleEvent = (rawEvent) => {
|
|
1931
|
+
const lines = rawEvent.split(/\r?\n/);
|
|
1932
|
+
let eventName = "";
|
|
1933
|
+
const dataLines = [];
|
|
1934
|
+
for (const line of lines) {
|
|
1935
|
+
if (line.startsWith("event:")) {
|
|
1936
|
+
eventName = line.slice(6).trim();
|
|
1937
|
+
continue;
|
|
1938
|
+
}
|
|
1939
|
+
if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart());
|
|
1940
|
+
}
|
|
1941
|
+
const rawData = dataLines.join("\n");
|
|
1942
|
+
if (rawData.length === 0) return;
|
|
1943
|
+
let payload;
|
|
1944
|
+
try {
|
|
1945
|
+
payload = JSON.parse(rawData);
|
|
1946
|
+
} catch {
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
const kind = eventName || (typeof payload.event_kind === "string" ? payload.event_kind : "");
|
|
1950
|
+
if (kind === "request-usage") {
|
|
1951
|
+
handleUsage(payload);
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
if (kind === "tool-return" || kind === "on_call_tools_start") {
|
|
1955
|
+
resetCurrentMessage();
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
if (kind === "text") {
|
|
1959
|
+
const content = payload.content;
|
|
1960
|
+
if (typeof content === "string") {
|
|
1961
|
+
currentTextParts = [content];
|
|
1962
|
+
currentTextIndexes = /* @__PURE__ */ new Map();
|
|
1963
|
+
emitMessage();
|
|
1964
|
+
}
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
if (kind === "part_start") {
|
|
1968
|
+
const partStart = payload;
|
|
1969
|
+
if (typeof partStart.index === "number" && partStart.part?.part_kind === "text" && typeof partStart.part.content === "string") {
|
|
1970
|
+
const nextIndex = currentTextParts.push(partStart.part.content) - 1;
|
|
1971
|
+
currentTextIndexes.set(partStart.index, nextIndex);
|
|
1972
|
+
emitMessage();
|
|
1973
|
+
}
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
if (kind === "part_delta") {
|
|
1977
|
+
const partDelta = payload;
|
|
1978
|
+
if (typeof partDelta.index === "number" && partDelta.delta?.part_delta_kind === "text" && typeof partDelta.delta.content_delta === "string") {
|
|
1979
|
+
const textIndex = currentTextIndexes.get(partDelta.index);
|
|
1980
|
+
if (textIndex === void 0) {
|
|
1981
|
+
const nextIndex = currentTextParts.push(partDelta.delta.content_delta) - 1;
|
|
1982
|
+
currentTextIndexes.set(partDelta.index, nextIndex);
|
|
1983
|
+
} else currentTextParts[textIndex] += partDelta.delta.content_delta;
|
|
1984
|
+
emitMessage();
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
};
|
|
1988
|
+
while (true) {
|
|
1989
|
+
let readResult;
|
|
1990
|
+
try {
|
|
1991
|
+
readResult = await reader.read();
|
|
1992
|
+
} catch (error) {
|
|
1993
|
+
if (isAbortError(error)) throw createAbortError();
|
|
1994
|
+
throw error;
|
|
1995
|
+
}
|
|
1996
|
+
if (readResult.done) break;
|
|
1997
|
+
const chunk = decoder.decode(readResult.value, { stream: true });
|
|
1998
|
+
logStream?.write(chunk);
|
|
1999
|
+
buffer += chunk;
|
|
2000
|
+
while (true) {
|
|
2001
|
+
const lfBoundary = buffer.indexOf("\n\n");
|
|
2002
|
+
const crlfBoundary = buffer.indexOf("\r\n\r\n");
|
|
2003
|
+
let boundary;
|
|
2004
|
+
let separatorLen;
|
|
2005
|
+
if (lfBoundary === -1 && crlfBoundary === -1) break;
|
|
2006
|
+
if (crlfBoundary !== -1 && (lfBoundary === -1 || crlfBoundary < lfBoundary)) {
|
|
2007
|
+
boundary = crlfBoundary;
|
|
2008
|
+
separatorLen = 4;
|
|
2009
|
+
} else {
|
|
2010
|
+
boundary = lfBoundary;
|
|
2011
|
+
separatorLen = 2;
|
|
2012
|
+
}
|
|
2013
|
+
const rawEvent = buffer.slice(0, boundary);
|
|
2014
|
+
buffer = buffer.slice(boundary + separatorLen);
|
|
2015
|
+
if (rawEvent.trim()) handleEvent(rawEvent);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
buffer += decoder.decode();
|
|
2019
|
+
if (buffer.trim()) handleEvent(buffer);
|
|
2020
|
+
const finalText = latestTextSegment.trim();
|
|
2021
|
+
if (!finalText) throw new Error("rovodev returned no text output");
|
|
2022
|
+
try {
|
|
2023
|
+
return {
|
|
2024
|
+
output: JSON.parse(finalText),
|
|
2025
|
+
usage
|
|
2026
|
+
};
|
|
2027
|
+
} catch (error) {
|
|
2028
|
+
throw new Error(`Failed to parse rovodev output: ${error instanceof Error ? error.message : String(error)}`);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
async shutdownServer() {
|
|
2032
|
+
if (!this.server || this.server.closed) {
|
|
2033
|
+
this.server = null;
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
if (this.closingPromise) {
|
|
2037
|
+
await this.closingPromise;
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
const server = this.server;
|
|
2041
|
+
appendDebugLog("rovodev:shutdown", {
|
|
2042
|
+
cwd: server.cwd,
|
|
2043
|
+
port: server.port
|
|
2044
|
+
});
|
|
2045
|
+
this.closingPromise = this.platform === "win32" ? new Promise((resolve) => {
|
|
2046
|
+
const handleClose = () => {
|
|
2047
|
+
server.child.off("close", handleClose);
|
|
2048
|
+
resolve();
|
|
2049
|
+
};
|
|
2050
|
+
server.child.on("close", handleClose);
|
|
2051
|
+
try {
|
|
2052
|
+
terminateRovoDevProcess(server.child, this.platform);
|
|
2053
|
+
} catch {
|
|
2054
|
+
server.child.off("close", handleClose);
|
|
2055
|
+
resolve();
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
setTimeout(() => {
|
|
2059
|
+
server.child.off("close", handleClose);
|
|
2060
|
+
resolve();
|
|
2061
|
+
}, 100).unref?.();
|
|
2062
|
+
}) : shutdownChildProcess(server.child, {
|
|
2063
|
+
detached: server.detached,
|
|
2064
|
+
killProcess: this.killProcessFn,
|
|
2065
|
+
timeoutMs: 3e3
|
|
2066
|
+
});
|
|
2067
|
+
this.closingPromise = this.closingPromise.finally(() => {
|
|
2068
|
+
if (this.server === server) this.server = null;
|
|
2069
|
+
this.closingPromise = null;
|
|
2070
|
+
});
|
|
2071
|
+
await this.closingPromise;
|
|
2072
|
+
}
|
|
2073
|
+
async requestJSON(server, path, options) {
|
|
2074
|
+
return await (await this.request(server, path, options)).json();
|
|
2075
|
+
}
|
|
2076
|
+
async request(server, path, options) {
|
|
2077
|
+
const headers = new Headers(options.headers);
|
|
2078
|
+
if (options.sessionId) headers.set("x-session-id", options.sessionId);
|
|
2079
|
+
if (options.body !== void 0 && !headers.has("content-type")) headers.set("content-type", "application/json");
|
|
2080
|
+
const signal = withTimeoutSignal(options.signal, options.timeoutMs);
|
|
2081
|
+
const response = await this.fetchFn(`${server.baseUrl}${path}`, {
|
|
2082
|
+
method: options.method,
|
|
2083
|
+
headers,
|
|
2084
|
+
body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
|
|
2085
|
+
signal
|
|
2086
|
+
});
|
|
2087
|
+
if (!response.ok) {
|
|
2088
|
+
const body = await response.text();
|
|
2089
|
+
throw new Error(`rovodev ${options.method} ${path} failed with ${response.status}: ${body}`);
|
|
2090
|
+
}
|
|
2091
|
+
return response;
|
|
2092
|
+
}
|
|
2093
|
+
};
|
|
2094
|
+
function withTimeoutSignal(signal, timeoutMs) {
|
|
2095
|
+
if (timeoutMs === void 0) return signal;
|
|
2096
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
2097
|
+
return signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
2098
|
+
}
|
|
2099
|
+
//#endregion
|
|
2100
|
+
//#region src/core/agents/factory.ts
|
|
2101
|
+
function createAgent(name, runInfo, pathOverride, model) {
|
|
2102
|
+
switch (name) {
|
|
2103
|
+
case "claude": return new ClaudeAgent(pathOverride);
|
|
2104
|
+
case "codex": return new CodexAgent(runInfo.schemaPath, pathOverride);
|
|
2105
|
+
case "opencode": return new OpenCodeAgent({
|
|
2106
|
+
bin: pathOverride,
|
|
2107
|
+
model
|
|
2108
|
+
});
|
|
2109
|
+
case "rovodev": return new RovoDevAgent(runInfo.schemaPath, { bin: pathOverride });
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
//#endregion
|
|
2113
|
+
//#region src/templates/iteration-prompt.ts
|
|
2114
|
+
function buildIterationPrompt(params) {
|
|
2115
|
+
return `You are working autonomously on an objective given below.
|
|
2116
|
+
This is iteration ${params.n} of an ongoing loop to fully accomplish the objective.
|
|
2117
|
+
|
|
2118
|
+
## Instructions
|
|
2119
|
+
|
|
2120
|
+
1. Read .fttm/runs/${params.runId}/notes.md first to understand what has been done in previous iterations.
|
|
2121
|
+
2. Focus on the next smallest logical unit of work that's individually testable and would make incremental progress towards the objective - that's the scope of this iteration.
|
|
2122
|
+
3. If you made code changes, run build/tests/linters/formatters if available to validate your work.
|
|
2123
|
+
4. Do NOT make any git commits. Commits will be handled automatically by the fttm orchestrator.
|
|
2124
|
+
5. When you are done, respond with a JSON object according to the provided schema.
|
|
2125
|
+
|
|
2126
|
+
## Output
|
|
2127
|
+
|
|
2128
|
+
- success: whether you were able to complete your iteration. set to false only if something made it impossible for you to do your work
|
|
2129
|
+
- summary: a concise one-sentence summary of the accomplishment in this iteration
|
|
2130
|
+
- key_changes_made: an array of descriptions for key changes you made. don't group this by file - group by logical units of work. don't describe activities - describe material outcomes
|
|
2131
|
+
- key_learnings: an array of new learnings that were surprising and weren't captured by previous notes
|
|
2132
|
+
|
|
2133
|
+
## Objective
|
|
2134
|
+
|
|
2135
|
+
${params.prompt}`;
|
|
2136
|
+
}
|
|
2137
|
+
//#endregion
|
|
2138
|
+
//#region src/core/orchestrator.ts
|
|
2139
|
+
const STOP_CLOSE_AGENT_GRACE_MS = 250;
|
|
2140
|
+
var Orchestrator = class extends EventEmitter {
|
|
2141
|
+
config;
|
|
2142
|
+
agent;
|
|
2143
|
+
runInfo;
|
|
2144
|
+
cwd;
|
|
2145
|
+
prompt;
|
|
2146
|
+
limits;
|
|
2147
|
+
stopRequested = false;
|
|
2148
|
+
stopPromise = null;
|
|
2149
|
+
activeIterationPromise = null;
|
|
2150
|
+
activeAbortController = null;
|
|
2151
|
+
pendingAbortReason = null;
|
|
2152
|
+
state = {
|
|
2153
|
+
status: "running",
|
|
2154
|
+
currentIteration: 0,
|
|
2155
|
+
maxIterations: void 0,
|
|
2156
|
+
totalInputTokens: 0,
|
|
2157
|
+
totalOutputTokens: 0,
|
|
2158
|
+
commitCount: 0,
|
|
2159
|
+
iterations: [],
|
|
2160
|
+
successCount: 0,
|
|
2161
|
+
failCount: 0,
|
|
2162
|
+
consecutiveFailures: 0,
|
|
2163
|
+
startTime: /* @__PURE__ */ new Date(),
|
|
2164
|
+
waitingUntil: null,
|
|
2165
|
+
lastMessage: null
|
|
2166
|
+
};
|
|
2167
|
+
constructor(config, agent, runInfo, prompt, cwd, startIteration = 0, limits = {}) {
|
|
2168
|
+
super();
|
|
2169
|
+
this.config = config;
|
|
2170
|
+
this.agent = agent;
|
|
2171
|
+
this.runInfo = runInfo;
|
|
2172
|
+
this.prompt = prompt;
|
|
2173
|
+
this.cwd = cwd;
|
|
2174
|
+
this.limits = limits;
|
|
2175
|
+
this.state.currentIteration = startIteration;
|
|
2176
|
+
this.state.maxIterations = limits.maxIterations;
|
|
2177
|
+
this.state.commitCount = getBranchCommitCount(this.runInfo.baseCommit, this.cwd);
|
|
2178
|
+
}
|
|
2179
|
+
getState() {
|
|
2180
|
+
return { ...this.state };
|
|
2181
|
+
}
|
|
2182
|
+
stop() {
|
|
2183
|
+
this.stopRequested = true;
|
|
2184
|
+
this.activeAbortController?.abort();
|
|
2185
|
+
if (this.stopPromise) return;
|
|
2186
|
+
this.stopPromise = (async () => {
|
|
2187
|
+
if (this.activeIterationPromise) {
|
|
2188
|
+
const iterationPromise = this.activeIterationPromise.catch(() => void 0);
|
|
2189
|
+
await new Promise((resolve) => {
|
|
2190
|
+
let settled = false;
|
|
2191
|
+
const settle = () => {
|
|
2192
|
+
if (settled) return;
|
|
2193
|
+
settled = true;
|
|
2194
|
+
clearTimeout(timer);
|
|
2195
|
+
resolve();
|
|
2196
|
+
};
|
|
2197
|
+
const timer = setTimeout(settle, STOP_CLOSE_AGENT_GRACE_MS);
|
|
2198
|
+
timer.unref?.();
|
|
2199
|
+
iterationPromise.finally(settle);
|
|
2200
|
+
});
|
|
2201
|
+
await this.closeAgent();
|
|
2202
|
+
await iterationPromise;
|
|
2203
|
+
} else await this.closeAgent();
|
|
2204
|
+
resetHard(this.cwd);
|
|
2205
|
+
this.state.status = "stopped";
|
|
2206
|
+
this.emit("state", this.getState());
|
|
2207
|
+
this.emit("stopped");
|
|
2208
|
+
})();
|
|
2209
|
+
}
|
|
2210
|
+
async start() {
|
|
2211
|
+
this.state.startTime = /* @__PURE__ */ new Date();
|
|
2212
|
+
this.state.status = "running";
|
|
2213
|
+
this.emit("state", this.getState());
|
|
2214
|
+
try {
|
|
2215
|
+
while (!this.stopRequested) {
|
|
2216
|
+
const preIterationAbortReason = this.getPreIterationAbortReason();
|
|
2217
|
+
if (preIterationAbortReason) {
|
|
2218
|
+
this.abort(preIterationAbortReason);
|
|
2219
|
+
break;
|
|
2220
|
+
}
|
|
2221
|
+
this.state.currentIteration++;
|
|
2222
|
+
this.state.status = "running";
|
|
2223
|
+
this.emit("iteration:start", this.state.currentIteration);
|
|
2224
|
+
this.emit("state", this.getState());
|
|
2225
|
+
const iterationPrompt = buildIterationPrompt({
|
|
2226
|
+
n: this.state.currentIteration,
|
|
2227
|
+
runId: this.runInfo.runId,
|
|
2228
|
+
prompt: this.prompt
|
|
2229
|
+
});
|
|
2230
|
+
this.activeIterationPromise = this.runIteration(iterationPrompt);
|
|
2231
|
+
const result = await this.activeIterationPromise;
|
|
2232
|
+
this.activeIterationPromise = null;
|
|
2233
|
+
if (result.type === "stopped") break;
|
|
2234
|
+
if (result.type === "aborted") {
|
|
2235
|
+
this.abort(result.reason);
|
|
2236
|
+
break;
|
|
2237
|
+
}
|
|
2238
|
+
const { record } = result;
|
|
2239
|
+
this.state.iterations.push(record);
|
|
2240
|
+
this.emit("iteration:end", record);
|
|
2241
|
+
this.emit("state", this.getState());
|
|
2242
|
+
const postIterationAbortReason = this.getPostIterationAbortReason();
|
|
2243
|
+
if (postIterationAbortReason) {
|
|
2244
|
+
this.abort(postIterationAbortReason);
|
|
2245
|
+
break;
|
|
2246
|
+
}
|
|
2247
|
+
if (this.state.consecutiveFailures >= this.config.maxConsecutiveFailures) {
|
|
2248
|
+
this.abort(`${this.config.maxConsecutiveFailures} consecutive failures`);
|
|
2249
|
+
break;
|
|
2250
|
+
}
|
|
2251
|
+
if (this.state.consecutiveFailures > 0 && !this.stopRequested) {
|
|
2252
|
+
const backoffMs = 6e4 * Math.pow(2, this.state.consecutiveFailures - 1);
|
|
2253
|
+
this.state.status = "waiting";
|
|
2254
|
+
this.state.waitingUntil = new Date(Date.now() + backoffMs);
|
|
2255
|
+
this.emit("state", this.getState());
|
|
2256
|
+
await this.interruptibleSleep(backoffMs);
|
|
2257
|
+
this.state.waitingUntil = null;
|
|
2258
|
+
if (!this.stopRequested) {
|
|
2259
|
+
this.state.status = "running";
|
|
2260
|
+
this.emit("state", this.getState());
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
} finally {
|
|
2265
|
+
this.activeIterationPromise = null;
|
|
2266
|
+
if (this.stopPromise) await this.stopPromise;
|
|
2267
|
+
else await this.closeAgent();
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
async runIteration(prompt) {
|
|
2271
|
+
const baseInputTokens = this.state.totalInputTokens;
|
|
2272
|
+
const baseOutputTokens = this.state.totalOutputTokens;
|
|
2273
|
+
this.activeAbortController = new AbortController();
|
|
2274
|
+
this.pendingAbortReason = null;
|
|
2275
|
+
const onUsage = (usage) => {
|
|
2276
|
+
this.state.totalInputTokens = baseInputTokens + usage.inputTokens;
|
|
2277
|
+
this.state.totalOutputTokens = baseOutputTokens + usage.outputTokens;
|
|
2278
|
+
this.emit("state", this.getState());
|
|
2279
|
+
const reason = this.getTokenAbortReason();
|
|
2280
|
+
if (reason && this.activeAbortController && !this.activeAbortController.signal.aborted) {
|
|
2281
|
+
this.pendingAbortReason = reason;
|
|
2282
|
+
this.activeAbortController.abort();
|
|
2283
|
+
}
|
|
2284
|
+
};
|
|
2285
|
+
const onMessage = (text) => {
|
|
2286
|
+
this.state.lastMessage = text;
|
|
2287
|
+
this.emit("state", this.getState());
|
|
2288
|
+
};
|
|
2289
|
+
const logPath = join(this.runInfo.runDir, `iteration-${this.state.currentIteration}.jsonl`);
|
|
2290
|
+
try {
|
|
2291
|
+
const result = await this.agent.run(prompt, this.cwd, {
|
|
2292
|
+
onUsage,
|
|
2293
|
+
onMessage,
|
|
2294
|
+
signal: this.activeAbortController.signal,
|
|
2295
|
+
logPath
|
|
2296
|
+
});
|
|
2297
|
+
if (this.stopRequested) return { type: "stopped" };
|
|
2298
|
+
if (result.output.success) return {
|
|
2299
|
+
type: "completed",
|
|
2300
|
+
record: this.recordSuccess(result.output)
|
|
2301
|
+
};
|
|
2302
|
+
return {
|
|
2303
|
+
type: "completed",
|
|
2304
|
+
record: this.recordFailure(`[FAIL] ${result.output.summary}`, result.output.summary, result.output.key_learnings)
|
|
2305
|
+
};
|
|
2306
|
+
} catch (err) {
|
|
2307
|
+
if (this.pendingAbortReason && err instanceof Error && err.message === "Agent was aborted") {
|
|
2308
|
+
resetHard(this.cwd);
|
|
2309
|
+
return {
|
|
2310
|
+
type: "aborted",
|
|
2311
|
+
reason: this.pendingAbortReason
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
2314
|
+
if (this.stopRequested) return { type: "stopped" };
|
|
2315
|
+
const summary = err instanceof Error ? err.message : String(err);
|
|
2316
|
+
return {
|
|
2317
|
+
type: "completed",
|
|
2318
|
+
record: this.recordFailure(`[ERROR] ${summary}`, summary, [])
|
|
2319
|
+
};
|
|
2320
|
+
} finally {
|
|
2321
|
+
this.activeAbortController = null;
|
|
2322
|
+
this.pendingAbortReason = null;
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
recordSuccess(output) {
|
|
2326
|
+
appendNotes(this.runInfo.notesPath, this.state.currentIteration, output.summary, output.key_changes_made, output.key_learnings);
|
|
2327
|
+
commitAll(`fttm #${this.state.currentIteration}: ${output.summary}`, this.cwd);
|
|
2328
|
+
this.state.commitCount = getBranchCommitCount(this.runInfo.baseCommit, this.cwd);
|
|
2329
|
+
this.state.successCount++;
|
|
2330
|
+
this.state.consecutiveFailures = 0;
|
|
2331
|
+
return {
|
|
2332
|
+
number: this.state.currentIteration,
|
|
2333
|
+
success: true,
|
|
2334
|
+
summary: output.summary,
|
|
2335
|
+
keyChanges: output.key_changes_made,
|
|
2336
|
+
keyLearnings: output.key_learnings,
|
|
2337
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
recordFailure(notesSummary, recordSummary, learnings) {
|
|
2341
|
+
appendNotes(this.runInfo.notesPath, this.state.currentIteration, notesSummary, [], learnings);
|
|
2342
|
+
resetHard(this.cwd);
|
|
2343
|
+
this.state.failCount++;
|
|
2344
|
+
this.state.consecutiveFailures++;
|
|
2345
|
+
return {
|
|
2346
|
+
number: this.state.currentIteration,
|
|
2347
|
+
success: false,
|
|
2348
|
+
summary: recordSummary,
|
|
2349
|
+
keyChanges: [],
|
|
2350
|
+
keyLearnings: learnings,
|
|
2351
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2354
|
+
interruptibleSleep(ms) {
|
|
2355
|
+
return new Promise((resolve) => {
|
|
2356
|
+
this.activeAbortController = new AbortController();
|
|
2357
|
+
const timer = setTimeout(() => {
|
|
2358
|
+
this.activeAbortController = null;
|
|
2359
|
+
resolve();
|
|
2360
|
+
}, ms);
|
|
2361
|
+
this.activeAbortController.signal.addEventListener("abort", () => {
|
|
2362
|
+
clearTimeout(timer);
|
|
2363
|
+
this.activeAbortController = null;
|
|
2364
|
+
resolve();
|
|
2365
|
+
});
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
getPreIterationAbortReason() {
|
|
2369
|
+
if (this.limits.maxIterations !== void 0 && this.state.currentIteration >= this.limits.maxIterations) return `max iterations reached (${this.limits.maxIterations})`;
|
|
2370
|
+
return this.getTokenAbortReason();
|
|
2371
|
+
}
|
|
2372
|
+
getPostIterationAbortReason() {
|
|
2373
|
+
if (this.limits.maxIterations !== void 0 && this.state.currentIteration >= this.limits.maxIterations) return `max iterations reached (${this.limits.maxIterations})`;
|
|
2374
|
+
return this.getTokenAbortReason();
|
|
2375
|
+
}
|
|
2376
|
+
getTokenAbortReason() {
|
|
2377
|
+
if (this.limits.maxTokens === void 0) return null;
|
|
2378
|
+
const totalTokens = this.state.totalInputTokens + this.state.totalOutputTokens;
|
|
2379
|
+
if (totalTokens < this.limits.maxTokens) return null;
|
|
2380
|
+
return `max tokens reached (${totalTokens}/${this.limits.maxTokens})`;
|
|
2381
|
+
}
|
|
2382
|
+
abort(reason) {
|
|
2383
|
+
this.state.status = "aborted";
|
|
2384
|
+
this.state.lastMessage = reason;
|
|
2385
|
+
this.state.waitingUntil = null;
|
|
2386
|
+
this.emit("abort", reason);
|
|
2387
|
+
this.emit("state", this.getState());
|
|
2388
|
+
}
|
|
2389
|
+
async closeAgent() {
|
|
2390
|
+
try {
|
|
2391
|
+
await this.agent.close?.();
|
|
2392
|
+
} catch {}
|
|
2393
|
+
}
|
|
2394
|
+
};
|
|
2395
|
+
//#endregion
|
|
2396
|
+
//#region src/mock-orchestrator.ts
|
|
2397
|
+
function mockIter(n, success, summary, agoMs) {
|
|
2398
|
+
return {
|
|
2399
|
+
number: n,
|
|
2400
|
+
success,
|
|
2401
|
+
summary,
|
|
2402
|
+
keyChanges: [],
|
|
2403
|
+
keyLearnings: [],
|
|
2404
|
+
timestamp: new Date(Date.now() - agoMs)
|
|
2405
|
+
};
|
|
2406
|
+
}
|
|
2407
|
+
const MOCK_ITERATIONS = [
|
|
2408
|
+
mockIter(1, true, "Profiled cold start — identified 3 major bottlenecks", 252e5),
|
|
2409
|
+
mockIter(2, true, "Lazy-loaded config module, shaved 340ms off init", 24e6),
|
|
2410
|
+
mockIter(3, true, "Deferred plugin discovery to post-render", 228e5),
|
|
2411
|
+
mockIter(4, false, "Attempted parallel module init — race condition in DI container", 21e6),
|
|
2412
|
+
mockIter(5, true, "Fixed DI ordering, parallelized safe modules only", 192e5),
|
|
2413
|
+
mockIter(6, true, "Replaced synchronous JSON parse with streaming decoder", 174e5),
|
|
2414
|
+
mockIter(7, true, "Cached resolved dependency graph across restarts", 15e6),
|
|
2415
|
+
mockIter(8, false, "Tree-shaking broke runtime dynamic import paths", 126e5),
|
|
2416
|
+
mockIter(9, true, "Restored dynamic imports, added explicit entry chunks", 108e5),
|
|
2417
|
+
mockIter(10, true, "Inlined critical-path CSS, deferred non-essential styles", 84e5),
|
|
2418
|
+
mockIter(11, true, "Switched from full Intl polyfill to locale-on-demand", 6e6),
|
|
2419
|
+
mockIter(12, true, "Pre-compiled handlebars templates at build time", 36e5),
|
|
2420
|
+
mockIter(13, true, "Moved telemetry init behind requestIdleCallback", 18e5)
|
|
2421
|
+
];
|
|
2422
|
+
const AGENT_MESSAGES = [
|
|
2423
|
+
"Reading src/bootstrap.ts to trace the module init order",
|
|
2424
|
+
"Let me profile the require() chain with --cpu-prof",
|
|
2425
|
+
"Running integration tests after the lazy-load refactor",
|
|
2426
|
+
"Let me make sure HMR still works in dev mode",
|
|
2427
|
+
"Nice — startup dropped from 1.24s to 0.41s so far",
|
|
2428
|
+
"Found it! There's a sync readFileSync in the config loader hot path — that's blocking the entire init sequence",
|
|
2429
|
+
"I'm analyzing the import graph for circular dependencies. So far I've found 3 cycles that force eager evaluation",
|
|
2430
|
+
"Now I'll move database pool creation behind a first-request gate so we don't pay the connection cost at boot time",
|
|
2431
|
+
"Let me check if the logger init can be deferred safely. It looks like only the error handler depends on it early",
|
|
2432
|
+
"Checking the bundle size delta after tree-shaking. Went from 847KB down to 612KB — solid improvement",
|
|
2433
|
+
"I'm replacing those sync fs calls with a pre-cached config lookup that gets populated during the build step",
|
|
2434
|
+
"Running a cold start benchmark to establish a baseline. Currently at 1.24s — I think we can get under 500ms",
|
|
2435
|
+
"I'm looking at the startup flame graph and there are three clear bottlenecks: config parsing (310ms), plugin discovery (280ms), and template compilation (190ms)",
|
|
2436
|
+
"Let me verify the middleware registration order is still correct after the lazy-load refactor. The auth middleware needs to run before any route handlers get registered",
|
|
2437
|
+
"I need to test what happens when the first request arrives before deferred services finish initializing. Adding a readiness gate that blocks until all critical paths are up",
|
|
2438
|
+
"Now I'll investigate whether route registration can be made async without breaking the Express contract. The docs say app.listen() waits, but I want to confirm with a test",
|
|
2439
|
+
"Auditing all feature flags to see if any depend on early init. Looks like none of them do — they all read from a lazy-loaded remote config that we fetch on first access",
|
|
2440
|
+
"I'm adding startup timing spans to the OpenTelemetry traces so we can track cold start regressions in prod. Each phase will get its own span: config, plugins, routes, middleware",
|
|
2441
|
+
"Confirming all 47 tests still pass after these changes. The config loader tests needed updating since they were assuming synchronous initialization — fixing those now",
|
|
2442
|
+
"Let me validate the health check endpoint still responds within 50ms even during the deferred init window. I'll add a lightweight synthetic probe that skips the full stack"
|
|
2443
|
+
];
|
|
2444
|
+
function randInt(min, max) {
|
|
2445
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
2446
|
+
}
|
|
2447
|
+
const INITIAL_ELAPSED_MS = 29237e3;
|
|
2448
|
+
var MockOrchestrator = class extends EventEmitter {
|
|
2449
|
+
state = {
|
|
2450
|
+
status: "running",
|
|
2451
|
+
currentIteration: 14,
|
|
2452
|
+
totalInputTokens: 873e5,
|
|
2453
|
+
totalOutputTokens: 86e4,
|
|
2454
|
+
commitCount: 11,
|
|
2455
|
+
iterations: [...MOCK_ITERATIONS],
|
|
2456
|
+
successCount: 11,
|
|
2457
|
+
failCount: 2,
|
|
2458
|
+
consecutiveFailures: 0,
|
|
2459
|
+
startTime: new Date(Date.now() - INITIAL_ELAPSED_MS),
|
|
2460
|
+
waitingUntil: null,
|
|
2461
|
+
lastMessage: AGENT_MESSAGES[0]
|
|
2462
|
+
};
|
|
2463
|
+
tokenTimer = null;
|
|
2464
|
+
messageTimer = null;
|
|
2465
|
+
messageIndex = 0;
|
|
2466
|
+
getState() {
|
|
2467
|
+
return {
|
|
2468
|
+
...this.state,
|
|
2469
|
+
iterations: [...this.state.iterations]
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
stop() {
|
|
2473
|
+
if (this.tokenTimer) clearTimeout(this.tokenTimer);
|
|
2474
|
+
if (this.messageTimer) clearTimeout(this.messageTimer);
|
|
2475
|
+
this.tokenTimer = null;
|
|
2476
|
+
this.messageTimer = null;
|
|
2477
|
+
this.state.status = "stopped";
|
|
2478
|
+
this.emit("state", this.getState());
|
|
2479
|
+
this.emit("stopped");
|
|
2480
|
+
}
|
|
2481
|
+
start() {
|
|
2482
|
+
this.emit("state", this.getState());
|
|
2483
|
+
this.scheduleTokenBump();
|
|
2484
|
+
this.scheduleNextMessage();
|
|
2485
|
+
}
|
|
2486
|
+
scheduleTokenBump() {
|
|
2487
|
+
this.tokenTimer = setTimeout(() => {
|
|
2488
|
+
this.state.totalInputTokens += randInt(4e4, 18e4);
|
|
2489
|
+
this.state.totalOutputTokens += randInt(200, 2e3);
|
|
2490
|
+
this.emit("state", this.getState());
|
|
2491
|
+
if (this.state.status === "running") this.scheduleTokenBump();
|
|
2492
|
+
}, randInt(1500, 7e3));
|
|
2493
|
+
}
|
|
2494
|
+
scheduleNextMessage() {
|
|
2495
|
+
const delay = randInt(3e3, 7e3);
|
|
2496
|
+
this.messageTimer = setTimeout(() => {
|
|
2497
|
+
this.messageIndex = (this.messageIndex + 1) % AGENT_MESSAGES.length;
|
|
2498
|
+
this.state.lastMessage = AGENT_MESSAGES[this.messageIndex];
|
|
2499
|
+
this.emit("state", this.getState());
|
|
2500
|
+
this.scheduleNextMessage();
|
|
2501
|
+
}, delay);
|
|
2502
|
+
}
|
|
2503
|
+
};
|
|
2504
|
+
//#endregion
|
|
2505
|
+
//#region src/utils/stars.ts
|
|
2506
|
+
const STAR_CHARS = [
|
|
2507
|
+
"·",
|
|
2508
|
+
"·",
|
|
2509
|
+
"·",
|
|
2510
|
+
"·",
|
|
2511
|
+
"·",
|
|
2512
|
+
"·",
|
|
2513
|
+
"✧",
|
|
2514
|
+
"⋆",
|
|
2515
|
+
"⋆",
|
|
2516
|
+
"⋆",
|
|
2517
|
+
"°",
|
|
2518
|
+
"°"
|
|
2519
|
+
];
|
|
2520
|
+
function generateStarField(width, height, density, seed) {
|
|
2521
|
+
const stars = [];
|
|
2522
|
+
let s = seed;
|
|
2523
|
+
const rand = () => {
|
|
2524
|
+
s = (s * 16807 + 0) % 2147483647;
|
|
2525
|
+
return s / 2147483647;
|
|
2526
|
+
};
|
|
2527
|
+
for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) if (rand() < density) {
|
|
2528
|
+
const charIdx = Math.floor(rand() * STAR_CHARS.length);
|
|
2529
|
+
const r = rand();
|
|
2530
|
+
const rest = r < .15 ? "hidden" : r < .4 ? "dim" : "bright";
|
|
2531
|
+
stars.push({
|
|
2532
|
+
x,
|
|
2533
|
+
y,
|
|
2534
|
+
char: STAR_CHARS[charIdx],
|
|
2535
|
+
phase: rand() * Math.PI * 2,
|
|
2536
|
+
period: 1e4 + rand() * 15e3,
|
|
2537
|
+
rest
|
|
2538
|
+
});
|
|
2539
|
+
}
|
|
2540
|
+
return stars;
|
|
2541
|
+
}
|
|
2542
|
+
function getStarState(star, now) {
|
|
2543
|
+
const t = (now % star.period / star.period + star.phase / (Math.PI * 2)) % 1;
|
|
2544
|
+
if (t > .05) return star.rest;
|
|
2545
|
+
if (star.rest === "bright" || star.rest === "hidden") {
|
|
2546
|
+
const opposite = star.rest === "bright" ? "hidden" : "bright";
|
|
2547
|
+
if (t > .0325) return "dim";
|
|
2548
|
+
if (t > .0175) return opposite;
|
|
2549
|
+
return "dim";
|
|
2550
|
+
}
|
|
2551
|
+
if (t > .025) return "bright";
|
|
2552
|
+
return "dim";
|
|
2553
|
+
}
|
|
2554
|
+
//#endregion
|
|
2555
|
+
//#region src/utils/moon.ts
|
|
2556
|
+
const MOON_PHASES = [
|
|
2557
|
+
"🌑",
|
|
2558
|
+
"🌒",
|
|
2559
|
+
"🌓",
|
|
2560
|
+
"🌔",
|
|
2561
|
+
"🌕",
|
|
2562
|
+
"🌖",
|
|
2563
|
+
"🌗",
|
|
2564
|
+
"🌘"
|
|
2565
|
+
];
|
|
2566
|
+
function getMoonPhase(state, now = 0, periodMs = 4e3) {
|
|
2567
|
+
if (state === "success") return "🌕";
|
|
2568
|
+
if (state === "fail") return "🌑";
|
|
2569
|
+
return MOON_PHASES[Math.floor(now % periodMs / periodMs * 8) % 8];
|
|
2570
|
+
}
|
|
2571
|
+
//#endregion
|
|
2572
|
+
//#region src/utils/time.ts
|
|
2573
|
+
function formatElapsed(ms) {
|
|
2574
|
+
const s = Math.floor(ms / 1e3);
|
|
2575
|
+
return `${String(Math.floor(s / 3600)).padStart(2, "0")}:${String(Math.floor(s % 3600 / 60)).padStart(2, "0")}:${String(s % 60).padStart(2, "0")}`;
|
|
2576
|
+
}
|
|
2577
|
+
//#endregion
|
|
2578
|
+
//#region src/utils/tokens.ts
|
|
2579
|
+
function formatTokens(count) {
|
|
2580
|
+
if (count >= 0xe8d4a51000) return `${(count / 0xe8d4a51000).toFixed(1)}T`;
|
|
2581
|
+
if (count >= 1e9) return `${(count / 1e9).toFixed(1)}B`;
|
|
2582
|
+
if (count >= 1e6) return `${(count / 1e6).toFixed(1)}M`;
|
|
2583
|
+
if (count >= 1e3) return `${Math.round(count / 1e3)}K`;
|
|
2584
|
+
return String(count);
|
|
2585
|
+
}
|
|
2586
|
+
//#endregion
|
|
2587
|
+
//#region src/utils/terminal-width.ts
|
|
2588
|
+
const graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
2589
|
+
const MARK_REGEX = /\p{Mark}/u;
|
|
2590
|
+
const REGIONAL_INDICATOR_REGEX = /\p{Regional_Indicator}/u;
|
|
2591
|
+
const EXTENDED_PICTOGRAPHIC_REGEX = /\p{Extended_Pictographic}/u;
|
|
2592
|
+
function isFullWidthCodePoint(codePoint) {
|
|
2593
|
+
return codePoint >= 4352 && (codePoint <= 4447 || codePoint === 9001 || codePoint === 9002 || codePoint >= 11904 && codePoint <= 12871 && codePoint !== 12351 || codePoint >= 12880 && codePoint <= 19903 || codePoint >= 19968 && codePoint <= 42182 || codePoint >= 43360 && codePoint <= 43388 || codePoint >= 44032 && codePoint <= 55203 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 65040 && codePoint <= 65049 || codePoint >= 65072 && codePoint <= 65131 || codePoint >= 65281 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510 || codePoint >= 110592 && codePoint <= 110593 || codePoint >= 127488 && codePoint <= 127569 || codePoint >= 131072 && codePoint <= 262141);
|
|
2594
|
+
}
|
|
2595
|
+
function codePointWidth(codePoint) {
|
|
2596
|
+
if (codePoint === 0 || codePoint === 8204 || codePoint === 8205 || codePoint === 65038 || codePoint === 65039) return 0;
|
|
2597
|
+
if (MARK_REGEX.test(String.fromCodePoint(codePoint))) return 0;
|
|
2598
|
+
return isFullWidthCodePoint(codePoint) ? 2 : 1;
|
|
2599
|
+
}
|
|
2600
|
+
function isWideEmojiGrapheme(grapheme) {
|
|
2601
|
+
return grapheme.includes("") || grapheme.includes("️") || grapheme.includes("⃣") || REGIONAL_INDICATOR_REGEX.test(grapheme) || Array.from(grapheme).some((char) => EXTENDED_PICTOGRAPHIC_REGEX.test(char));
|
|
2602
|
+
}
|
|
2603
|
+
function splitGraphemes(text) {
|
|
2604
|
+
return Array.from(graphemeSegmenter.segment(text), ({ segment }) => segment);
|
|
2605
|
+
}
|
|
2606
|
+
function graphemeWidth(grapheme) {
|
|
2607
|
+
if (!grapheme) return 0;
|
|
2608
|
+
if (isWideEmojiGrapheme(grapheme)) return 2;
|
|
2609
|
+
let width = 0;
|
|
2610
|
+
for (const char of grapheme) width += codePointWidth(char.codePointAt(0) ?? 0);
|
|
2611
|
+
return width;
|
|
2612
|
+
}
|
|
2613
|
+
function stringWidth(text) {
|
|
2614
|
+
let width = 0;
|
|
2615
|
+
for (const grapheme of splitGraphemes(text)) width += graphemeWidth(grapheme);
|
|
2616
|
+
return width;
|
|
2617
|
+
}
|
|
2618
|
+
//#endregion
|
|
2619
|
+
//#region src/utils/wordwrap.ts
|
|
2620
|
+
function sliceToWidth(text, width) {
|
|
2621
|
+
let result = "";
|
|
2622
|
+
let currentWidth = 0;
|
|
2623
|
+
for (const grapheme of splitGraphemes(text)) {
|
|
2624
|
+
const nextWidth = currentWidth + graphemeWidth(grapheme);
|
|
2625
|
+
if (nextWidth > width) break;
|
|
2626
|
+
result += grapheme;
|
|
2627
|
+
currentWidth = nextWidth;
|
|
2628
|
+
}
|
|
2629
|
+
return result;
|
|
2630
|
+
}
|
|
2631
|
+
function splitByWidth(text, width) {
|
|
2632
|
+
const lines = [];
|
|
2633
|
+
let current = "";
|
|
2634
|
+
let currentWidth = 0;
|
|
2635
|
+
for (const grapheme of splitGraphemes(text)) {
|
|
2636
|
+
const glyphWidth = graphemeWidth(grapheme);
|
|
2637
|
+
if (current && currentWidth + glyphWidth > width) {
|
|
2638
|
+
lines.push(current);
|
|
2639
|
+
current = grapheme;
|
|
2640
|
+
currentWidth = glyphWidth;
|
|
2641
|
+
continue;
|
|
2642
|
+
}
|
|
2643
|
+
current += grapheme;
|
|
2644
|
+
currentWidth += glyphWidth;
|
|
2645
|
+
}
|
|
2646
|
+
if (current) lines.push(current);
|
|
2647
|
+
return lines;
|
|
2648
|
+
}
|
|
2649
|
+
function wordWrap(text, width, maxLines) {
|
|
2650
|
+
if (!text) return [];
|
|
2651
|
+
const lines = [];
|
|
2652
|
+
for (const paragraph of text.split("\n")) {
|
|
2653
|
+
const words = paragraph.split(/\s+/).filter(Boolean);
|
|
2654
|
+
if (words.length === 0) {
|
|
2655
|
+
lines.push("");
|
|
2656
|
+
continue;
|
|
2657
|
+
}
|
|
2658
|
+
let current = "";
|
|
2659
|
+
let currentWidth = 0;
|
|
2660
|
+
for (const word of words) {
|
|
2661
|
+
const wordWidth = stringWidth(word);
|
|
2662
|
+
if (wordWidth > width) {
|
|
2663
|
+
if (current) {
|
|
2664
|
+
lines.push(current);
|
|
2665
|
+
current = "";
|
|
2666
|
+
currentWidth = 0;
|
|
2667
|
+
}
|
|
2668
|
+
for (const slice of splitByWidth(word, width)) lines.push(slice);
|
|
2669
|
+
continue;
|
|
2670
|
+
}
|
|
2671
|
+
const nextWidth = current ? currentWidth + 1 + wordWidth : wordWidth;
|
|
2672
|
+
if (current && nextWidth > width) {
|
|
2673
|
+
lines.push(current);
|
|
2674
|
+
current = word;
|
|
2675
|
+
currentWidth = wordWidth;
|
|
2676
|
+
} else {
|
|
2677
|
+
current = current ? current + " " + word : word;
|
|
2678
|
+
currentWidth = nextWidth;
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
if (current) lines.push(current);
|
|
2682
|
+
}
|
|
2683
|
+
if (maxLines && lines.length > maxLines) {
|
|
2684
|
+
const capped = lines.slice(0, maxLines);
|
|
2685
|
+
const last = capped[maxLines - 1];
|
|
2686
|
+
capped[maxLines - 1] = stringWidth(last) >= width ? sliceToWidth(last, width - 1) + "…" : last + "…";
|
|
2687
|
+
return capped;
|
|
2688
|
+
}
|
|
2689
|
+
return lines;
|
|
2690
|
+
}
|
|
2691
|
+
//#endregion
|
|
2692
|
+
//#region src/renderer-diff.ts
|
|
2693
|
+
const SPACE = {
|
|
2694
|
+
char: " ",
|
|
2695
|
+
style: "normal",
|
|
2696
|
+
width: 1
|
|
2697
|
+
};
|
|
2698
|
+
function makeCell(char, style) {
|
|
2699
|
+
return {
|
|
2700
|
+
char,
|
|
2701
|
+
style,
|
|
2702
|
+
width: graphemeWidth(char)
|
|
2703
|
+
};
|
|
2704
|
+
}
|
|
2705
|
+
function textToCells(text, style) {
|
|
2706
|
+
const cells = [];
|
|
2707
|
+
for (const grapheme of splitGraphemes(text)) {
|
|
2708
|
+
const cell = makeCell(grapheme, style);
|
|
2709
|
+
cells.push(cell);
|
|
2710
|
+
if (cell.width === 2) cells.push({
|
|
2711
|
+
char: "",
|
|
2712
|
+
style: "normal",
|
|
2713
|
+
width: 0
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2716
|
+
return cells;
|
|
2717
|
+
}
|
|
2718
|
+
function emptyCells(n) {
|
|
2719
|
+
const cells = [];
|
|
2720
|
+
for (let i = 0; i < n; i++) cells.push({ ...SPACE });
|
|
2721
|
+
return cells;
|
|
2722
|
+
}
|
|
2723
|
+
function rowToString(cells) {
|
|
2724
|
+
let result = "";
|
|
2725
|
+
let currentStyle = "normal";
|
|
2726
|
+
for (const cell of cells) {
|
|
2727
|
+
if (cell.width === 0) continue;
|
|
2728
|
+
if (cell.style !== currentStyle) {
|
|
2729
|
+
if (currentStyle !== "normal") result += "\x1B[0m";
|
|
2730
|
+
if (cell.style === "bold") result += "\x1B[1m";
|
|
2731
|
+
else if (cell.style === "dim") result += "\x1B[2m";
|
|
2732
|
+
currentStyle = cell.style;
|
|
2733
|
+
}
|
|
2734
|
+
result += cell.char;
|
|
2735
|
+
}
|
|
2736
|
+
if (currentStyle !== "normal") result += "\x1B[0m";
|
|
2737
|
+
return result;
|
|
2738
|
+
}
|
|
2739
|
+
function diffFrames(prev, next) {
|
|
2740
|
+
const changes = [];
|
|
2741
|
+
const rows = Math.min(prev.length, next.length);
|
|
2742
|
+
for (let r = 0; r < rows; r++) {
|
|
2743
|
+
const prevRow = prev[r];
|
|
2744
|
+
const nextRow = next[r];
|
|
2745
|
+
const cols = Math.min(prevRow.length, nextRow.length);
|
|
2746
|
+
for (let c = 0; c < cols; c++) {
|
|
2747
|
+
const n = nextRow[c];
|
|
2748
|
+
if (n.width === 0) continue;
|
|
2749
|
+
const p = prevRow[c];
|
|
2750
|
+
if (p.char !== n.char || p.style !== n.style || p.width !== n.width) changes.push({
|
|
2751
|
+
row: r,
|
|
2752
|
+
col: c,
|
|
2753
|
+
cell: n
|
|
2754
|
+
});
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
return changes;
|
|
2758
|
+
}
|
|
2759
|
+
function emitDiff(changes) {
|
|
2760
|
+
if (changes.length === 0) return "";
|
|
2761
|
+
let result = "";
|
|
2762
|
+
let currentStyle = null;
|
|
2763
|
+
let cursorRow = -1;
|
|
2764
|
+
let cursorCol = -1;
|
|
2765
|
+
for (const { row, col, cell } of changes) {
|
|
2766
|
+
if (row !== cursorRow || col !== cursorCol) result += `\x1b[${row + 1};${col + 1}H`;
|
|
2767
|
+
if (cell.style !== currentStyle) {
|
|
2768
|
+
result += "\x1B[0m";
|
|
2769
|
+
if (cell.style === "bold") result += "\x1B[1m";
|
|
2770
|
+
else if (cell.style === "dim") result += "\x1B[2m";
|
|
2771
|
+
currentStyle = cell.style;
|
|
2772
|
+
}
|
|
2773
|
+
result += cell.char;
|
|
2774
|
+
cursorRow = row;
|
|
2775
|
+
cursorCol = col + cell.width;
|
|
2776
|
+
}
|
|
2777
|
+
if (currentStyle !== "normal") result += "\x1B[0m";
|
|
2778
|
+
return result;
|
|
2779
|
+
}
|
|
2780
|
+
//#endregion
|
|
2781
|
+
//#region src/renderer.ts
|
|
2782
|
+
const CONTENT_WIDTH = 63;
|
|
2783
|
+
const MAX_PROMPT_LINES = 3;
|
|
2784
|
+
const BASE_CONTENT_ROWS = 24;
|
|
2785
|
+
const STAR_DENSITY = .035;
|
|
2786
|
+
const TICK_MS = 200;
|
|
2787
|
+
const MOONS_PER_ROW = 30;
|
|
2788
|
+
const MOON_PHASE_PERIOD = 1600;
|
|
2789
|
+
const MAX_MSG_LINES = 3;
|
|
2790
|
+
const MAX_MSG_LINE_LEN = CONTENT_WIDTH;
|
|
2791
|
+
const RESUME_HINT = "[ctrl+c to stop, fttm again to resume]";
|
|
2792
|
+
function spacedLabel(text) {
|
|
2793
|
+
return text.split("").join(" ");
|
|
2794
|
+
}
|
|
2795
|
+
function renderTitleCells(agentName) {
|
|
2796
|
+
return [
|
|
2797
|
+
[...textToCells(spacedLabel("fttm"), "dim"), ...agentName ? [
|
|
2798
|
+
...textToCells(" ", "normal"),
|
|
2799
|
+
...textToCells("·", "dim"),
|
|
2800
|
+
...textToCells(" ", "normal"),
|
|
2801
|
+
...textToCells(spacedLabel(agentName), "dim")
|
|
2802
|
+
] : []],
|
|
2803
|
+
[],
|
|
2804
|
+
textToCells("┏━╸╻ ╻ ╻ ╺┳╸┏━┓ ╺┳╸╻ ╻┏━╸ ┏┳┓┏━┓┏━┓┏┓╻", "bold"),
|
|
2805
|
+
textToCells("┣╸ ┃ ┗┳┛ ┃ ┃ ┃ ┃ ┣━┫┣╸ ┃┃┃┃ ┃┃ ┃┃┗┫", "bold"),
|
|
2806
|
+
textToCells("╹ ┗━┛ ╹ ╹ ┗━┛ ╹ ╹ ╹┗━╸ ╹ ╹┗━┛┗━┛╹ ╹", "bold")
|
|
2807
|
+
];
|
|
2808
|
+
}
|
|
2809
|
+
function renderStatsCells(elapsed, inputTokens, outputTokens, commitCount, currentIteration, maxIterations) {
|
|
2810
|
+
const commitLabel = commitCount === 1 ? "commit" : "commits";
|
|
2811
|
+
const iterationStr = maxIterations !== void 0 ? `${currentIteration}/${maxIterations}` : `${currentIteration}/\u221e`;
|
|
2812
|
+
return [
|
|
2813
|
+
...textToCells(elapsed, "bold"),
|
|
2814
|
+
...textToCells(" ", "normal"),
|
|
2815
|
+
...textToCells("·", "dim"),
|
|
2816
|
+
...textToCells(" ", "normal"),
|
|
2817
|
+
...textToCells(`iter ${iterationStr}`, "normal"),
|
|
2818
|
+
...textToCells(" ", "normal"),
|
|
2819
|
+
...textToCells("·", "dim"),
|
|
2820
|
+
...textToCells(" ", "normal"),
|
|
2821
|
+
...textToCells(`${formatTokens(inputTokens)} in`, "normal"),
|
|
2822
|
+
...textToCells(" ", "normal"),
|
|
2823
|
+
...textToCells("·", "dim"),
|
|
2824
|
+
...textToCells(" ", "normal"),
|
|
2825
|
+
...textToCells(`${formatTokens(outputTokens)} out`, "normal"),
|
|
2826
|
+
...textToCells(" ", "normal"),
|
|
2827
|
+
...textToCells("·", "dim"),
|
|
2828
|
+
...textToCells(" ", "normal"),
|
|
2829
|
+
...textToCells(`${commitCount} ${commitLabel}`, "normal")
|
|
2830
|
+
];
|
|
2831
|
+
}
|
|
2832
|
+
function renderAgentMessageCells(message, status) {
|
|
2833
|
+
const lines = [];
|
|
2834
|
+
if (status === "waiting") lines.push("waiting (backoff)...");
|
|
2835
|
+
else if (status === "aborted" && !message) lines.push("max consecutive failures reached");
|
|
2836
|
+
else if (!message) lines.push("working...");
|
|
2837
|
+
else {
|
|
2838
|
+
const wrapped = wordWrap(message, MAX_MSG_LINE_LEN, MAX_MSG_LINES);
|
|
2839
|
+
for (const wl of wrapped) lines.push(wl);
|
|
2840
|
+
}
|
|
2841
|
+
while (lines.length < MAX_MSG_LINES) lines.push("");
|
|
2842
|
+
return lines.map((l) => l ? textToCells(l, "dim") : []);
|
|
2843
|
+
}
|
|
2844
|
+
function renderMoonStripCells(iterations, isRunning, now) {
|
|
2845
|
+
const moons = iterations.map((iter) => getMoonPhase(iter.success ? "success" : "fail"));
|
|
2846
|
+
if (isRunning) moons.push(getMoonPhase("active", now, MOON_PHASE_PERIOD));
|
|
2847
|
+
if (moons.length === 0) return [[]];
|
|
2848
|
+
const rows = [];
|
|
2849
|
+
for (let i = 0; i < moons.length; i += MOONS_PER_ROW) {
|
|
2850
|
+
const slice = moons.slice(i, i + MOONS_PER_ROW);
|
|
2851
|
+
const cells = [];
|
|
2852
|
+
for (const moon of slice) cells.push(...textToCells(moon, "normal"));
|
|
2853
|
+
rows.push(cells);
|
|
2854
|
+
}
|
|
2855
|
+
return rows;
|
|
2856
|
+
}
|
|
2857
|
+
function starStyle(state) {
|
|
2858
|
+
if (state === "bright") return "bold";
|
|
2859
|
+
if (state === "dim") return "dim";
|
|
2860
|
+
return "normal";
|
|
2861
|
+
}
|
|
2862
|
+
function placeStarsInCells(cells, stars, row, xMin, xMax, xOffset, now) {
|
|
2863
|
+
for (const star of stars) {
|
|
2864
|
+
if (star.y !== row || star.x < xMin || star.x >= xMax) continue;
|
|
2865
|
+
const state = getStarState(star, now);
|
|
2866
|
+
const localX = star.x - xOffset;
|
|
2867
|
+
cells[localX] = state === "hidden" ? {
|
|
2868
|
+
char: " ",
|
|
2869
|
+
style: "normal",
|
|
2870
|
+
width: 1
|
|
2871
|
+
} : {
|
|
2872
|
+
char: star.char,
|
|
2873
|
+
style: starStyle(state),
|
|
2874
|
+
width: 1
|
|
2875
|
+
};
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
function renderStarLineCells(stars, width, y, now) {
|
|
2879
|
+
const cells = emptyCells(width);
|
|
2880
|
+
placeStarsInCells(cells, stars, y, 0, width, 0, now);
|
|
2881
|
+
return cells;
|
|
2882
|
+
}
|
|
2883
|
+
function renderSideStarsCells(stars, rowIndex, xOffset, sideWidth, now) {
|
|
2884
|
+
if (sideWidth <= 0) return [];
|
|
2885
|
+
const cells = emptyCells(sideWidth);
|
|
2886
|
+
placeStarsInCells(cells, stars, rowIndex, xOffset, xOffset + sideWidth, xOffset, now);
|
|
2887
|
+
return cells;
|
|
2888
|
+
}
|
|
2889
|
+
function clampCellsToWidth(content, width) {
|
|
2890
|
+
if (content.length <= width) return content;
|
|
2891
|
+
const clamped = [];
|
|
2892
|
+
let remaining = width;
|
|
2893
|
+
for (let i = 0; i < content.length && remaining > 0; i++) {
|
|
2894
|
+
const cell = content[i];
|
|
2895
|
+
if (cell.width === 0) continue;
|
|
2896
|
+
if (cell.width > remaining) break;
|
|
2897
|
+
clamped.push(cell);
|
|
2898
|
+
remaining -= cell.width;
|
|
2899
|
+
if (cell.width === 2 && content[i + 1]?.width === 0) {
|
|
2900
|
+
clamped.push(content[i + 1]);
|
|
2901
|
+
i += 1;
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
return clamped;
|
|
2905
|
+
}
|
|
2906
|
+
function centerLineCells(content, width) {
|
|
2907
|
+
const clamped = clampCellsToWidth(content, width);
|
|
2908
|
+
const w = clamped.length;
|
|
2909
|
+
const pad = Math.max(0, Math.floor((width - w) / 2));
|
|
2910
|
+
const rightPad = Math.max(0, width - w - pad);
|
|
2911
|
+
return [
|
|
2912
|
+
...emptyCells(pad),
|
|
2913
|
+
...clamped,
|
|
2914
|
+
...emptyCells(rightPad)
|
|
2915
|
+
];
|
|
2916
|
+
}
|
|
2917
|
+
function renderResumeHintCells(width, modelName) {
|
|
2918
|
+
const hint = RESUME_HINT;
|
|
2919
|
+
if (modelName) return centerLineCells(textToCells(`${modelName} ${hint}`, "dim"), width);
|
|
2920
|
+
return centerLineCells(textToCells(hint, "dim"), width);
|
|
2921
|
+
}
|
|
2922
|
+
/**
|
|
2923
|
+
* Builds the centered content viewport for the renderer.
|
|
2924
|
+
*
|
|
2925
|
+
* When `availableHeight` is constrained, the layout drops optional sections in
|
|
2926
|
+
* priority order (ASCII art, eyebrow, agent message, then prompt) so the stats
|
|
2927
|
+
* row remains visible and any remaining space is used for the newest moon rows.
|
|
2928
|
+
*/
|
|
2929
|
+
function buildContentCells(prompt, agentName, modelName, state, elapsed, now, availableHeight) {
|
|
2930
|
+
const isRunning = state.status === "running" || state.status === "waiting";
|
|
2931
|
+
const moonRows = renderMoonStripCells(state.iterations, isRunning, now);
|
|
2932
|
+
const maxRows = availableHeight ?? Infinity;
|
|
2933
|
+
if (maxRows <= 0) return [];
|
|
2934
|
+
const titleCells = renderTitleCells(agentName);
|
|
2935
|
+
const titleSpacer = titleCells[1] ?? [];
|
|
2936
|
+
const promptLines = wordWrap(prompt, CONTENT_WIDTH, MAX_PROMPT_LINES);
|
|
2937
|
+
const promptRows = [];
|
|
2938
|
+
for (let i = 0; i < MAX_PROMPT_LINES; i++) {
|
|
2939
|
+
const pl = promptLines[i] ?? "";
|
|
2940
|
+
promptRows.push(pl ? textToCells(pl, "dim") : []);
|
|
2941
|
+
}
|
|
2942
|
+
const sections = {
|
|
2943
|
+
top: [[]],
|
|
2944
|
+
eyebrow: [
|
|
2945
|
+
titleCells[0],
|
|
2946
|
+
[],
|
|
2947
|
+
[]
|
|
2948
|
+
],
|
|
2949
|
+
art: titleCells.slice(2),
|
|
2950
|
+
prompt: [
|
|
2951
|
+
titleSpacer,
|
|
2952
|
+
...promptRows,
|
|
2953
|
+
[],
|
|
2954
|
+
[]
|
|
2955
|
+
],
|
|
2956
|
+
stats: [renderStatsCells(elapsed, state.totalInputTokens, state.totalOutputTokens, state.commitCount, state.currentIteration, state.maxIterations)],
|
|
2957
|
+
agent: [
|
|
2958
|
+
[],
|
|
2959
|
+
[],
|
|
2960
|
+
...renderAgentMessageCells(state.lastMessage, state.status)
|
|
2961
|
+
],
|
|
2962
|
+
moon: [
|
|
2963
|
+
[],
|
|
2964
|
+
[],
|
|
2965
|
+
...moonRows
|
|
2966
|
+
]
|
|
2967
|
+
};
|
|
2968
|
+
const flattenSections = () => [
|
|
2969
|
+
...sections.top,
|
|
2970
|
+
...sections.eyebrow,
|
|
2971
|
+
...sections.art,
|
|
2972
|
+
...sections.prompt,
|
|
2973
|
+
...sections.stats,
|
|
2974
|
+
...sections.agent,
|
|
2975
|
+
...sections.moon
|
|
2976
|
+
];
|
|
2977
|
+
const optionalSections = [
|
|
2978
|
+
"art",
|
|
2979
|
+
"eyebrow",
|
|
2980
|
+
"agent",
|
|
2981
|
+
"prompt"
|
|
2982
|
+
];
|
|
2983
|
+
let rows = flattenSections();
|
|
2984
|
+
for (const section of optionalSections) {
|
|
2985
|
+
if (rows.length <= maxRows) break;
|
|
2986
|
+
sections[section] = [];
|
|
2987
|
+
rows = flattenSections();
|
|
2988
|
+
}
|
|
2989
|
+
if (rows.length > maxRows) rows = rows.filter((row) => row.length > 0);
|
|
2990
|
+
if (rows.length > maxRows) {
|
|
2991
|
+
const nonMoonRows = [
|
|
2992
|
+
...sections.top,
|
|
2993
|
+
...sections.eyebrow,
|
|
2994
|
+
...sections.art,
|
|
2995
|
+
...sections.prompt,
|
|
2996
|
+
...sections.stats,
|
|
2997
|
+
...sections.agent
|
|
2998
|
+
].filter((row) => row.length > 0);
|
|
2999
|
+
const allowedMoonRows = Math.max(0, maxRows - nonMoonRows.length);
|
|
3000
|
+
const visibleMoonRows = allowedMoonRows === 0 ? [] : moonRows.filter((row) => row.length > 0).slice(-allowedMoonRows);
|
|
3001
|
+
rows = [...nonMoonRows, ...visibleMoonRows];
|
|
3002
|
+
}
|
|
3003
|
+
return rows;
|
|
3004
|
+
}
|
|
3005
|
+
function buildFrameCells(prompt, agentName, modelName, state, topStars, bottomStars, sideStars, now, terminalWidth, terminalHeight) {
|
|
3006
|
+
const elapsed = formatElapsed(now - state.startTime.getTime());
|
|
3007
|
+
const availableHeight = Math.max(0, terminalHeight - 2);
|
|
3008
|
+
const contentRows = buildContentCells(prompt, agentName, modelName, state, elapsed, now, availableHeight);
|
|
3009
|
+
while (contentRows.length < Math.min(BASE_CONTENT_ROWS, availableHeight)) contentRows.push([]);
|
|
3010
|
+
const contentCount = contentRows.length;
|
|
3011
|
+
const remaining = Math.max(0, availableHeight - contentCount);
|
|
3012
|
+
const topHeight = Math.max(0, Math.ceil(remaining / 2));
|
|
3013
|
+
const bottomHeight = remaining - topHeight;
|
|
3014
|
+
const sideWidth = Math.max(0, Math.floor((terminalWidth - CONTENT_WIDTH) / 2));
|
|
3015
|
+
const frame = [];
|
|
3016
|
+
for (let y = 0; y < topHeight; y++) frame.push(renderStarLineCells(topStars, terminalWidth, y, now));
|
|
3017
|
+
for (let i = 0; i < contentRows.length; i++) {
|
|
3018
|
+
const left = renderSideStarsCells(sideStars, i, 0, sideWidth, now);
|
|
3019
|
+
const center = centerLineCells(contentRows[i], CONTENT_WIDTH);
|
|
3020
|
+
const right = renderSideStarsCells(sideStars, i, terminalWidth - sideWidth, sideWidth, now);
|
|
3021
|
+
frame.push([
|
|
3022
|
+
...left,
|
|
3023
|
+
...center,
|
|
3024
|
+
...right
|
|
3025
|
+
]);
|
|
3026
|
+
}
|
|
3027
|
+
for (let y = 0; y < bottomHeight; y++) frame.push(renderStarLineCells(bottomStars, terminalWidth, y, now));
|
|
3028
|
+
frame.push(renderResumeHintCells(terminalWidth, modelName));
|
|
3029
|
+
frame.push(emptyCells(terminalWidth));
|
|
3030
|
+
return frame;
|
|
3031
|
+
}
|
|
3032
|
+
var Renderer = class {
|
|
3033
|
+
seedTop;
|
|
3034
|
+
seedBottom;
|
|
3035
|
+
seedSide;
|
|
3036
|
+
modelName;
|
|
3037
|
+
constructor(orchestrator, prompt, agentName, modelName) {
|
|
3038
|
+
this.orchestrator = orchestrator;
|
|
3039
|
+
this.prompt = prompt;
|
|
3040
|
+
this.agentName = agentName;
|
|
3041
|
+
this.modelName = modelName;
|
|
3042
|
+
this.state = orchestrator.getState();
|
|
3043
|
+
this.seedTop = Math.floor(Math.random() * 2147483646) + 1;
|
|
3044
|
+
this.seedBottom = Math.floor(Math.random() * 2147483646) + 1;
|
|
3045
|
+
this.seedSide = Math.floor(Math.random() * 2147483646) + 1;
|
|
3046
|
+
this.exitPromise = new Promise((resolve) => {
|
|
3047
|
+
this.exitResolve = resolve;
|
|
3048
|
+
});
|
|
3049
|
+
}
|
|
3050
|
+
start() {
|
|
3051
|
+
this.orchestrator.on("state", (newState) => {
|
|
3052
|
+
this.state = {
|
|
3053
|
+
...newState,
|
|
3054
|
+
iterations: [...newState.iterations]
|
|
3055
|
+
};
|
|
3056
|
+
});
|
|
3057
|
+
this.orchestrator.on("stopped", () => {
|
|
3058
|
+
this.stop("stopped");
|
|
3059
|
+
});
|
|
3060
|
+
if (process$1.stdin.isTTY) {
|
|
3061
|
+
process$1.stdin.setRawMode(true);
|
|
3062
|
+
process$1.stdin.resume();
|
|
3063
|
+
process$1.stdin.on("data", (data) => {
|
|
3064
|
+
if (data[0] === 3) {
|
|
3065
|
+
this.stop("interrupted");
|
|
3066
|
+
this.orchestrator.stop();
|
|
3067
|
+
}
|
|
3068
|
+
});
|
|
3069
|
+
}
|
|
3070
|
+
this.interval = setInterval(() => this.render(), TICK_MS);
|
|
3071
|
+
this.render();
|
|
3072
|
+
}
|
|
3073
|
+
stop(reason = "stopped") {
|
|
3074
|
+
if (this.interval) {
|
|
3075
|
+
clearInterval(this.interval);
|
|
3076
|
+
this.interval = null;
|
|
3077
|
+
}
|
|
3078
|
+
if (process$1.stdin.isTTY) {
|
|
3079
|
+
process$1.stdin.setRawMode(false);
|
|
3080
|
+
process$1.stdin.pause();
|
|
3081
|
+
process$1.stdin.removeAllListeners("data");
|
|
3082
|
+
}
|
|
3083
|
+
this.exitResolve(reason);
|
|
3084
|
+
}
|
|
3085
|
+
waitUntilExit() {
|
|
3086
|
+
return this.exitPromise;
|
|
3087
|
+
}
|
|
3088
|
+
ensureStarFields(w, h) {
|
|
3089
|
+
if (w !== this.cachedWidth || h !== this.cachedHeight) {
|
|
3090
|
+
this.cachedWidth = w;
|
|
3091
|
+
this.cachedHeight = h;
|
|
3092
|
+
const contentStart = Math.max(0, Math.floor((w - CONTENT_WIDTH) / 2) - 8);
|
|
3093
|
+
const contentEnd = contentStart + CONTENT_WIDTH + 16;
|
|
3094
|
+
const availableHeight = Math.max(0, h - 2);
|
|
3095
|
+
const remaining = Math.max(0, availableHeight - BASE_CONTENT_ROWS);
|
|
3096
|
+
const topHeight = Math.max(0, Math.ceil(remaining / 2));
|
|
3097
|
+
const proximityRows = 8;
|
|
3098
|
+
const shrinkBig = (s, nearContentRow) => {
|
|
3099
|
+
if (!nearContentRow || s.x < contentStart || s.x >= contentEnd) return s;
|
|
3100
|
+
const star = s.char !== "·" ? {
|
|
3101
|
+
...s,
|
|
3102
|
+
char: "·"
|
|
3103
|
+
} : s;
|
|
3104
|
+
return star.rest === "bright" ? {
|
|
3105
|
+
...star,
|
|
3106
|
+
rest: "dim"
|
|
3107
|
+
} : star;
|
|
3108
|
+
};
|
|
3109
|
+
this.topStars = generateStarField(w, h, STAR_DENSITY, this.seedTop).map((s) => shrinkBig(s, s.y >= topHeight - proximityRows));
|
|
3110
|
+
this.bottomStars = generateStarField(w, h, STAR_DENSITY, this.seedBottom).map((s) => shrinkBig(s, s.y < proximityRows));
|
|
3111
|
+
this.sideStars = generateStarField(w, Math.max(BASE_CONTENT_ROWS, availableHeight), STAR_DENSITY, this.seedSide);
|
|
3112
|
+
return true;
|
|
3113
|
+
}
|
|
3114
|
+
return false;
|
|
3115
|
+
}
|
|
3116
|
+
render() {
|
|
3117
|
+
const now = Date.now();
|
|
3118
|
+
const w = process$1.stdout.columns || 80;
|
|
3119
|
+
const h = process$1.stdout.rows || 24;
|
|
3120
|
+
const resized = this.ensureStarFields(w, h);
|
|
3121
|
+
const nextCells = buildFrameCells(this.prompt, this.agentName, this.modelName, this.state, this.topStars, this.bottomStars, this.sideStars, now, w, h);
|
|
3122
|
+
if (this.isFirstFrame || resized) {
|
|
3123
|
+
process$1.stdout.write("\x1B[H" + nextCells.map(rowToString).join("\n"));
|
|
3124
|
+
this.isFirstFrame = false;
|
|
3125
|
+
} else {
|
|
3126
|
+
const changes = diffFrames(this.prevCells, nextCells);
|
|
3127
|
+
if (changes.length > 0) process$1.stdout.write(emitDiff(changes));
|
|
3128
|
+
}
|
|
3129
|
+
this.prevCells = nextCells;
|
|
3130
|
+
}
|
|
3131
|
+
};
|
|
3132
|
+
//#endregion
|
|
3133
|
+
//#region src/utils/slugify.ts
|
|
3134
|
+
function slugifyPrompt(prompt) {
|
|
3135
|
+
return `fttm/${prompt.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 20).replace(/-+$/, "")}-${createHash("sha256").update(prompt).digest("hex").slice(0, 6)}`;
|
|
3136
|
+
}
|
|
3137
|
+
//#endregion
|
|
3138
|
+
//#region src/cli.ts
|
|
3139
|
+
const packageVersion = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8")).version;
|
|
3140
|
+
const FORCE_EXIT_TIMEOUT_MS = 5e3;
|
|
3141
|
+
const GNHF_REEXEC_STDIN_PROMPT = "GNHF_REEXEC_STDIN_PROMPT";
|
|
3142
|
+
const GNHF_REEXEC_STDIN_PROMPT_FILE = "GNHF_REEXEC_STDIN_PROMPT_FILE";
|
|
3143
|
+
const GNHF_REEXEC_STDIN_PROMPT_DIR_PREFIX = "fttm-stdin-";
|
|
3144
|
+
const GNHF_REEXEC_STDIN_PROMPT_FILENAME = "prompt.txt";
|
|
3145
|
+
function parseNonNegativeInteger(value) {
|
|
3146
|
+
if (!/^\d+$/.test(value)) throw new InvalidArgumentError("must be a non-negative integer");
|
|
3147
|
+
const parsed = Number.parseInt(value, 10);
|
|
3148
|
+
if (!Number.isSafeInteger(parsed)) throw new InvalidArgumentError("must be a safe integer");
|
|
3149
|
+
return parsed;
|
|
3150
|
+
}
|
|
3151
|
+
function parseOnOffBoolean(value) {
|
|
3152
|
+
if (value === "on" || value === "true") return true;
|
|
3153
|
+
if (value === "off" || value === "false") return false;
|
|
3154
|
+
throw new InvalidArgumentError("must be one of: \"on\", \"off\", \"true\", \"false\"");
|
|
3155
|
+
}
|
|
3156
|
+
function humanizeErrorMessage(message) {
|
|
3157
|
+
if (message.includes("not a git repository")) return "This command must be run inside a Git repository. Change into a repo or run \"git init\" first.";
|
|
3158
|
+
return message;
|
|
3159
|
+
}
|
|
3160
|
+
function initializeNewBranch(prompt, cwd) {
|
|
3161
|
+
ensureCleanWorkingTree(cwd);
|
|
3162
|
+
const baseCommit = getHeadCommit(cwd);
|
|
3163
|
+
const branchName = slugifyPrompt(prompt);
|
|
3164
|
+
createBranch(branchName, cwd);
|
|
3165
|
+
const runId = branchName.split("/")[1];
|
|
3166
|
+
return setupRun(runId, prompt, baseCommit, cwd);
|
|
3167
|
+
}
|
|
3168
|
+
function ask(question) {
|
|
3169
|
+
const rl = createInterface({
|
|
3170
|
+
input: process$1.stdin,
|
|
3171
|
+
output: process$1.stderr
|
|
3172
|
+
});
|
|
3173
|
+
return new Promise((resolve) => {
|
|
3174
|
+
rl.question(question, (answer) => {
|
|
3175
|
+
rl.close();
|
|
3176
|
+
resolve(answer.trim().toLowerCase());
|
|
3177
|
+
});
|
|
3178
|
+
});
|
|
3179
|
+
}
|
|
3180
|
+
function getSignalExitCode(signal) {
|
|
3181
|
+
return signal === "SIGINT" ? 130 : 143;
|
|
3182
|
+
}
|
|
3183
|
+
function persistStdinPromptForReexec(prompt) {
|
|
3184
|
+
const promptDir = mkdtempSync(join(tmpdir(), GNHF_REEXEC_STDIN_PROMPT_DIR_PREFIX));
|
|
3185
|
+
const promptPath = join(promptDir, GNHF_REEXEC_STDIN_PROMPT_FILENAME);
|
|
3186
|
+
writeFileSync(promptPath, prompt, {
|
|
3187
|
+
encoding: "utf-8",
|
|
3188
|
+
mode: 384
|
|
3189
|
+
});
|
|
3190
|
+
return {
|
|
3191
|
+
path: promptPath,
|
|
3192
|
+
cleanup: () => {
|
|
3193
|
+
rmSync(promptDir, {
|
|
3194
|
+
recursive: true,
|
|
3195
|
+
force: true
|
|
3196
|
+
});
|
|
3197
|
+
}
|
|
3198
|
+
};
|
|
3199
|
+
}
|
|
3200
|
+
function isTrustedReexecPromptPath(promptPath) {
|
|
3201
|
+
const resolvedPromptPath = resolve(promptPath);
|
|
3202
|
+
const promptDir = dirname(resolvedPromptPath);
|
|
3203
|
+
return basename(resolvedPromptPath) === GNHF_REEXEC_STDIN_PROMPT_FILENAME && dirname(promptDir) === resolve(tmpdir()) && basename(promptDir).startsWith(GNHF_REEXEC_STDIN_PROMPT_DIR_PREFIX);
|
|
3204
|
+
}
|
|
3205
|
+
function cleanupTrustedReexecPromptPath(promptPath) {
|
|
3206
|
+
if (!isTrustedReexecPromptPath(promptPath)) return;
|
|
3207
|
+
const resolvedPromptPath = resolve(promptPath);
|
|
3208
|
+
rmSync(resolvedPromptPath, { force: true });
|
|
3209
|
+
try {
|
|
3210
|
+
rmdirSync(dirname(resolvedPromptPath));
|
|
3211
|
+
} catch {}
|
|
3212
|
+
}
|
|
3213
|
+
function readReexecStdinPrompt(env) {
|
|
3214
|
+
const promptPath = env[GNHF_REEXEC_STDIN_PROMPT_FILE];
|
|
3215
|
+
if (promptPath !== void 0) {
|
|
3216
|
+
delete env[GNHF_REEXEC_STDIN_PROMPT_FILE];
|
|
3217
|
+
try {
|
|
3218
|
+
return readFileSync(promptPath, "utf-8");
|
|
3219
|
+
} finally {
|
|
3220
|
+
cleanupTrustedReexecPromptPath(promptPath);
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
const prompt = env[GNHF_REEXEC_STDIN_PROMPT];
|
|
3224
|
+
if (prompt !== void 0) {
|
|
3225
|
+
delete env[GNHF_REEXEC_STDIN_PROMPT];
|
|
3226
|
+
return prompt;
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
const program = new Command();
|
|
3230
|
+
program.name("fttm").description("Fly to the moon, fly to the mars - AI agents for humans").version(packageVersion).argument("[prompt]", "The objective for the coding agent").option("--agent <agent>", "Agent to use (claude, codex, rovodev, or opencode)").option("--model <model>", "Model to use (e.g., opencode/claude-sonnet-4-6, minimax-m2.7)").option("--max-iterations <n>", "Abort after N total iterations", parseNonNegativeInteger).option("--detach", "Run in background (default: false, use with --max-iterations for true daemon mode)", false).option("--max-tokens <n>", "Abort after N total input+output tokens", parseNonNegativeInteger).option("--prevent-sleep <mode>", "Prevent system sleep during the run (\"on\" or \"off\")", parseOnOffBoolean).option("--mock", "", false).action(async (promptArg, options) => {
|
|
3231
|
+
if (options.mock) {
|
|
3232
|
+
const mock = new MockOrchestrator();
|
|
3233
|
+
enterAltScreen();
|
|
3234
|
+
const renderer = new Renderer(mock, "let's minimize app startup latency without sacrificing any functionality", "claude");
|
|
3235
|
+
renderer.start();
|
|
3236
|
+
mock.start();
|
|
3237
|
+
await renderer.waitUntilExit();
|
|
3238
|
+
exitAltScreen();
|
|
3239
|
+
return;
|
|
3240
|
+
}
|
|
3241
|
+
let initialSleepPrevention = null;
|
|
3242
|
+
if (process$1.env.GNHF_SLEEP_INHIBITED === "1") initialSleepPrevention = await startSleepPrevention(process$1.argv.slice(2));
|
|
3243
|
+
let prompt = promptArg;
|
|
3244
|
+
let promptFromStdin = false;
|
|
3245
|
+
const agentName = options.agent;
|
|
3246
|
+
if (agentName !== void 0 && agentName !== "claude" && agentName !== "codex" && agentName !== "rovodev" && agentName !== "opencode") {
|
|
3247
|
+
console.error(`Unknown agent: ${options.agent}. Use "claude", "codex", "rovodev", or "opencode".`);
|
|
3248
|
+
process$1.exit(1);
|
|
3249
|
+
}
|
|
3250
|
+
const config = {
|
|
3251
|
+
...loadConfig(agentName ? { agent: agentName } : {}),
|
|
3252
|
+
...options.preventSleep === void 0 ? {} : { preventSleep: options.preventSleep }
|
|
3253
|
+
};
|
|
3254
|
+
if (config.agent !== "claude" && config.agent !== "codex" && config.agent !== "rovodev" && config.agent !== "opencode") {
|
|
3255
|
+
console.error(`Unknown agent: ${config.agent}. Use "claude", "codex", "rovodev", or "opencode".`);
|
|
3256
|
+
process$1.exit(1);
|
|
3257
|
+
}
|
|
3258
|
+
if (!prompt && process$1.env.GNHF_SLEEP_INHIBITED === "1") prompt = readReexecStdinPrompt(process$1.env);
|
|
3259
|
+
if (!prompt && !process$1.stdin.isTTY) {
|
|
3260
|
+
prompt = await readStdinText(process$1.stdin);
|
|
3261
|
+
promptFromStdin = true;
|
|
3262
|
+
}
|
|
3263
|
+
const cwd = process$1.cwd();
|
|
3264
|
+
const currentBranch = getCurrentBranch(cwd);
|
|
3265
|
+
const onGnhfBranch = currentBranch.startsWith("fttm/");
|
|
3266
|
+
let runInfo;
|
|
3267
|
+
let startIteration = 0;
|
|
3268
|
+
if (onGnhfBranch) {
|
|
3269
|
+
const existingRunId = currentBranch.slice(5);
|
|
3270
|
+
const existing = resumeRun(existingRunId, cwd);
|
|
3271
|
+
const existingPrompt = readFileSync(existing.promptPath, "utf-8");
|
|
3272
|
+
if (!prompt || prompt === existingPrompt) {
|
|
3273
|
+
prompt = existingPrompt;
|
|
3274
|
+
runInfo = existing;
|
|
3275
|
+
startIteration = getLastIterationNumber(existing);
|
|
3276
|
+
} else {
|
|
3277
|
+
const answer = await ask(`You are on fttm branch "${currentBranch}".\n (o) Overwrite current run with new prompt\n (n) Start a new branch on top of this one\n (q) Quit\nChoose [o/n/q]: `);
|
|
3278
|
+
if (answer === "o") {
|
|
3279
|
+
ensureCleanWorkingTree(cwd);
|
|
3280
|
+
runInfo = setupRun(existingRunId, prompt, existing.baseCommit, cwd);
|
|
3281
|
+
} else if (answer === "n") runInfo = initializeNewBranch(prompt, cwd);
|
|
3282
|
+
else process$1.exit(0);
|
|
3283
|
+
}
|
|
3284
|
+
} else {
|
|
3285
|
+
if (!prompt) {
|
|
3286
|
+
program.help();
|
|
3287
|
+
return;
|
|
3288
|
+
}
|
|
3289
|
+
runInfo = initializeNewBranch(prompt, cwd);
|
|
3290
|
+
}
|
|
3291
|
+
let sleepPreventionCleanup = null;
|
|
3292
|
+
if (config.preventSleep) {
|
|
3293
|
+
const persistedPrompt = promptFromStdin && prompt !== void 0 ? persistStdinPromptForReexec(prompt) : null;
|
|
3294
|
+
let reexeced = false;
|
|
3295
|
+
try {
|
|
3296
|
+
const sleepPrevention = initialSleepPrevention ?? await startSleepPrevention(process$1.argv.slice(2), {
|
|
3297
|
+
reexecEnv: persistedPrompt ? { [GNHF_REEXEC_STDIN_PROMPT_FILE]: persistedPrompt.path } : void 0,
|
|
3298
|
+
detach: options.detach
|
|
3299
|
+
});
|
|
3300
|
+
if (sleepPrevention.type === "reexeced") {
|
|
3301
|
+
reexeced = true;
|
|
3302
|
+
process$1.exit(sleepPrevention.exitCode);
|
|
3303
|
+
}
|
|
3304
|
+
if (sleepPrevention.type === "active") sleepPreventionCleanup = sleepPrevention.cleanup;
|
|
3305
|
+
} finally {
|
|
3306
|
+
if (!reexeced) persistedPrompt?.cleanup();
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
appendDebugLog("run:start", { args: process$1.argv.slice(2) });
|
|
3310
|
+
const orchestrator = new Orchestrator(config, createAgent(config.agent, runInfo, config.agentPathOverride[config.agent], options.model), runInfo, prompt, cwd, startIteration, {
|
|
3311
|
+
maxIterations: options.maxIterations,
|
|
3312
|
+
maxTokens: options.maxTokens
|
|
3313
|
+
});
|
|
3314
|
+
let shutdownSignal = null;
|
|
3315
|
+
enterAltScreen();
|
|
3316
|
+
const renderer = new Renderer(orchestrator, prompt, config.agent, options.model);
|
|
3317
|
+
renderer.start();
|
|
3318
|
+
const requestShutdown = (signal) => {
|
|
3319
|
+
if (shutdownSignal) return;
|
|
3320
|
+
shutdownSignal = signal;
|
|
3321
|
+
appendDebugLog(`signal:${signal}`);
|
|
3322
|
+
renderer.stop();
|
|
3323
|
+
orchestrator.stop();
|
|
3324
|
+
};
|
|
3325
|
+
const handleSigInt = () => requestShutdown("SIGINT");
|
|
3326
|
+
const handleSigTerm = () => requestShutdown("SIGTERM");
|
|
3327
|
+
process$1.on("SIGINT", handleSigInt);
|
|
3328
|
+
process$1.on("SIGTERM", handleSigTerm);
|
|
3329
|
+
const orchestratorPromise = orchestrator.start().finally(() => {
|
|
3330
|
+
renderer.stop();
|
|
3331
|
+
}).catch((err) => {
|
|
3332
|
+
exitAltScreen();
|
|
3333
|
+
die(err instanceof Error ? err.message : String(err));
|
|
3334
|
+
});
|
|
3335
|
+
try {
|
|
3336
|
+
if (await renderer.waitUntilExit() === "interrupted" && !shutdownSignal) {
|
|
3337
|
+
shutdownSignal = "SIGINT";
|
|
3338
|
+
appendDebugLog("signal:SIGINT");
|
|
3339
|
+
}
|
|
3340
|
+
exitAltScreen();
|
|
3341
|
+
if (await Promise.race([orchestratorPromise.then(() => "done"), new Promise((resolve) => {
|
|
3342
|
+
setTimeout(() => resolve("timeout"), FORCE_EXIT_TIMEOUT_MS).unref();
|
|
3343
|
+
})]) === "timeout") {
|
|
3344
|
+
appendDebugLog("run:shutdown-timeout", { timeoutMs: FORCE_EXIT_TIMEOUT_MS });
|
|
3345
|
+
console.error(`\n fttm: shutdown timed out after ${FORCE_EXIT_TIMEOUT_MS / 1e3}s, forcing exit\n`);
|
|
3346
|
+
process$1.exit(getSignalExitCode(shutdownSignal ?? "SIGINT"));
|
|
3347
|
+
}
|
|
3348
|
+
} finally {
|
|
3349
|
+
process$1.off("SIGINT", handleSigInt);
|
|
3350
|
+
process$1.off("SIGTERM", handleSigTerm);
|
|
3351
|
+
await sleepPreventionCleanup?.();
|
|
3352
|
+
}
|
|
3353
|
+
appendDebugLog("run:complete", { signal: shutdownSignal });
|
|
3354
|
+
if (shutdownSignal) process$1.exit(getSignalExitCode(shutdownSignal));
|
|
3355
|
+
});
|
|
3356
|
+
function enterAltScreen() {
|
|
3357
|
+
process$1.stdout.write("\x1B[?1049h");
|
|
3358
|
+
process$1.stdout.write("\x1B[?25l");
|
|
3359
|
+
}
|
|
3360
|
+
function exitAltScreen() {
|
|
3361
|
+
process$1.stdout.write("\x1B[?25h");
|
|
3362
|
+
process$1.stdout.write("\x1B[?1049l");
|
|
3363
|
+
}
|
|
3364
|
+
function die(message) {
|
|
3365
|
+
console.error(`\n fttm: ${humanizeErrorMessage(message)}\n`);
|
|
3366
|
+
process$1.exit(1);
|
|
3367
|
+
}
|
|
3368
|
+
try {
|
|
3369
|
+
await program.parseAsync();
|
|
3370
|
+
} catch (err) {
|
|
3371
|
+
die(err instanceof Error ? err.message : String(err));
|
|
3372
|
+
}
|
|
3373
|
+
//#endregion
|
|
3374
|
+
export {};
|