even-pf 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bun.lock CHANGED
@@ -5,48 +5,48 @@
5
5
  "": {
6
6
  "name": "tools",
7
7
  "dependencies": {
8
- "@openrouter/sdk": "^0.5.1",
8
+ "@openrouter/sdk": "^0.12.79",
9
9
  "chalk": "^5.6.2",
10
- "smol-toml": "^1.6.0",
10
+ "smol-toml": "^1.6.1",
11
11
  "zod-defaults": "^0.2.3",
12
12
  },
13
13
  "devDependencies": {
14
- "@types/bun": "latest",
14
+ "@types/bun": "1.3.14",
15
15
  },
16
16
  "optionalDependencies": {
17
- "even-pf-darwin-arm64": "0.3.4",
18
- "even-pf-darwin-x64": "0.3.4",
19
- "even-pf-linux-arm64": "0.3.4",
20
- "even-pf-linux-x64": "0.3.4",
21
- "even-pf-windows-x64": "0.3.4",
17
+ "even-pf-darwin-arm64": "0.4.2",
18
+ "even-pf-darwin-x64": "0.4.2",
19
+ "even-pf-linux-arm64": "0.4.2",
20
+ "even-pf-linux-x64": "0.4.2",
21
+ "even-pf-windows-x64": "0.4.2",
22
22
  },
23
23
  "peerDependencies": {
24
- "typescript": "^5.9.3",
24
+ "typescript": "^6.0.3",
25
25
  },
26
26
  },
27
27
  },
28
28
  "packages": {
29
- "@openrouter/sdk": ["@openrouter/sdk@0.5.1", "", { "dependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Kl0N1jIj7A3lnkM5dO3SGP8JP3jAozzs6JWcHVuZUBt5DsGKxFGNH1Y15bCfsJiLNA2ylAQpCN3aNcgEYkkL5Q=="],
29
+ "@openrouter/sdk": ["@openrouter/sdk@0.12.79", "", { "dependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-0ZpwtnuHh3/B1piW9kHCUIQy6PAsaK/vjFdZuHxmCdAenCyUNsLA2mFpmfHNWRNb+bOO3yBc4IALa264UyzmBA=="],
30
30
 
31
- "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
31
+ "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
32
32
 
33
33
  "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
34
34
 
35
- "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
35
+ "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
36
36
 
37
37
  "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
38
38
 
39
- "even-pf-darwin-arm64": ["even-pf-darwin-arm64@0.3.4", "", { "os": "darwin", "cpu": "arm64", "bin": { "even-pf": "bin/even-pf" } }, "sha512-x2vTM0ogvlFhUiHqb13kXJTKPRPU/VdoZa1G51c3IHsZz7wdDpkD/DxcEvxAmO28MbJtfjxig8nRFMvld5J6jg=="],
39
+ "even-pf-darwin-arm64": ["even-pf-darwin-arm64@0.4.2", "", { "os": "darwin", "cpu": "arm64", "bin": { "even-pf": "bin/even-pf" } }, "sha512-W1W1EhdqZmUW6wJi+bCuS537MWvMUO6ryRgOErG38tFGs1h91JXT/upiuoRv5L245tKrKLMM5HkVZ88qNVrTUQ=="],
40
40
 
41
- "even-pf-darwin-x64": ["even-pf-darwin-x64@0.3.4", "", { "os": "darwin", "cpu": "x64", "bin": { "even-pf": "bin/even-pf" } }, "sha512-L2hzBvSLFcWMB/MJQeZTQHI8mqpGMQ7T0tSPXjv4S1tFglF8ZtdxggDAhmItEyyqVfsAT6LY+HyOpJnUAga9tg=="],
41
+ "even-pf-darwin-x64": ["even-pf-darwin-x64@0.4.2", "", { "os": "darwin", "cpu": "x64", "bin": { "even-pf": "bin/even-pf" } }, "sha512-mysyZkNdQvlqOjM/gpEfoGZZJ5H2b7MT4Z7jwbJhZpy4XCwWO98x3MpMWlUqJW+wEAMea+PwPJVJ314hkeQEgw=="],
42
42
 
43
- "even-pf-linux-arm64": ["even-pf-linux-arm64@0.3.4", "", { "os": "linux", "cpu": "arm64", "bin": { "even-pf": "bin/even-pf" } }, "sha512-/5nLtKs+8xvTHEkrVPQQ5XQBTKROmF42z6+fo4AOkOj/TbDGwCher6RYYMHQ6pD7M0jjF5AdSlj5HLEGf/N9Qg=="],
43
+ "even-pf-linux-arm64": ["even-pf-linux-arm64@0.4.2", "", { "os": "linux", "cpu": "arm64", "bin": { "even-pf": "bin/even-pf" } }, "sha512-rIh+SDDN3oDAyNN94SDm3dDiJ7jarkelZoh3c3ibvLJ5+WHA4XFhiBIRQjPBv+VX49TFooPmjqPMH8X5pkUc8w=="],
44
44
 
45
- "even-pf-linux-x64": ["even-pf-linux-x64@0.3.4", "", { "os": "linux", "cpu": "x64", "bin": { "even-pf": "bin/even-pf" } }, "sha512-UN0wz2svjcjckugzFyc4tHxllrTM7IScSmnLDq5z9AB5cplHZrvAg8cYcvz20YEcHsr7aUkxrhA7iDv5KKYhkA=="],
45
+ "even-pf-linux-x64": ["even-pf-linux-x64@0.4.2", "", { "os": "linux", "cpu": "x64", "bin": { "even-pf": "bin/even-pf" } }, "sha512-WhE5k5se/vgLlfHTVtsU4mXa094pTmuPKps6l+Gctak+toPaGtmOlr+KpDT3CDOnMRvEwnJWMrx8QA1mpfzyuQ=="],
46
46
 
47
- "even-pf-windows-x64": ["even-pf-windows-x64@0.3.4", "", { "os": "win32", "cpu": "x64", "bin": { "even-pf": "bin/even-pf.exe" } }, "sha512-ni84uLUdo95TlACDUyz7Ia7+4wigSByvUuR+IrXbLzkN90mZTsJoZVbAoJMR8CnOlPPEClcPHqkTcYl1lbLOwA=="],
47
+ "even-pf-windows-x64": ["even-pf-windows-x64@0.4.2", "", { "os": "win32", "cpu": "x64", "bin": { "even-pf": "bin/even-pf.exe" } }, "sha512-CkYMt99VWmZixbXKxR2301lxiKKCr6XF3QxU2f9HO2z6vSBzWNbT70JsuBc8UtKJqeuxg4MW8LONYpAxfuaKgg=="],
48
48
 
49
- "smol-toml": ["smol-toml@1.6.0", "", {}, "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw=="],
49
+ "smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="],
50
50
 
51
51
  "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
52
52
 
package/epf.example.toml CHANGED
@@ -12,6 +12,8 @@ top_p = 1
12
12
  frequency_penalty = 0
13
13
  presence_penalty = 0
14
14
  reasoning_effort = "high"
15
+ max_retries = 1
16
+ retry_delay_ms = 1000
15
17
 
16
18
  [llm.models.output_comparison]
17
19
  sdk = "openrouter"
@@ -22,6 +24,8 @@ top_p = 1
22
24
  frequency_penalty = 0
23
25
  presence_penalty = 0
24
26
  reasoning_effort = "high"
27
+ max_retries = 1
28
+ retry_delay_ms = 1000
25
29
 
26
30
  [llm.prompt_replacement]
27
31
  role = "role_placeholder"
package/package.json CHANGED
@@ -1,41 +1,42 @@
1
1
  {
2
2
  "name": "even-pf",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "AI-assisted responsible grading tool for programming assignments",
5
- "module": "src/cli.ts",
5
+ "module": "src/hosts/cli-host.ts",
6
6
  "type": "module",
7
7
  "license": "UNLICENSED",
8
8
  "scripts": {
9
- "start": "bun run src/cli.ts",
10
- "build:dev": "bun build src/cli.ts --compile --outfile build/epf",
9
+ "start": "bun run src/hosts/cli-host.ts",
10
+ "build:dev": "bun build src/hosts/cli-host.ts --compile --outfile build/epf",
11
11
  "build:all": "bun scripts/build-all.ts",
12
12
  "publish:all": "bun scripts/publish-all.ts",
13
13
  "publish:dry": "bun scripts/publish-all.ts --dry-run",
14
14
  "bump": "bun scripts/bump-version.ts",
15
- "config-gen": "bun run --console-depth 6 src/generate-config.ts"
15
+ "config-gen": "bun run --console-depth 6 src/generate-config.ts",
16
+ "check": "bun x tsc --noEmit"
16
17
  },
17
18
  "bin": {
18
19
  "even-pf": "bin/even-pf.js",
19
- "e-pf": "src/cli.ts"
20
+ "e-pf": "src/hosts/cli-host.ts"
20
21
  },
21
22
  "devDependencies": {
22
- "@types/bun": "latest"
23
+ "@types/bun": "1.3.14"
23
24
  },
24
25
  "peerDependencies": {
25
- "typescript": "^5.9.3"
26
+ "typescript": "^6.0.3"
26
27
  },
27
28
  "dependencies": {
28
- "@openrouter/sdk": "^0.5.1",
29
+ "@openrouter/sdk": "^0.12.79",
29
30
  "chalk": "^5.6.2",
30
- "smol-toml": "^1.6.0",
31
+ "smol-toml": "^1.6.1",
31
32
  "zod-defaults": "^0.2.3"
32
33
  },
33
34
  "optionalDependencies": {
34
- "even-pf-linux-x64": "0.4.1",
35
- "even-pf-linux-arm64": "0.4.1",
36
- "even-pf-windows-x64": "0.4.1",
37
- "even-pf-darwin-x64": "0.4.1",
38
- "even-pf-darwin-arm64": "0.4.1"
35
+ "even-pf-linux-x64": "0.5.0",
36
+ "even-pf-linux-arm64": "0.5.0",
37
+ "even-pf-windows-x64": "0.5.0",
38
+ "even-pf-darwin-x64": "0.5.0",
39
+ "even-pf-darwin-arm64": "0.5.0"
39
40
  },
40
41
  "files": [
41
42
  "bin/even-pf.js",
@@ -0,0 +1,105 @@
1
+ import type { Engine } from "./engine/index.ts";
2
+ import { parseAndExecute } from "./command-handler.ts";
3
+
4
+ const CORS_HEADERS: Record<string, string> = {
5
+ "Access-Control-Allow-Origin": "*",
6
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
7
+ "Access-Control-Allow-Headers": "Content-Type",
8
+ };
9
+
10
+ function jsonResponse(data: unknown, status = 200): Response {
11
+ return new Response(JSON.stringify(data), {
12
+ status,
13
+ headers: {
14
+ "Content-Type": "application/json",
15
+ ...CORS_HEADERS,
16
+ },
17
+ });
18
+ }
19
+
20
+ function corsPreflightResponse(): Response {
21
+ return new Response(null, { status: 204, headers: CORS_HEADERS });
22
+ }
23
+
24
+ type ApiServerHandle = {
25
+ url: string;
26
+ stop: () => void;
27
+ };
28
+
29
+ /**
30
+ * HTTP server that serves both file-data routes (migrated from OutputViewer)
31
+ * and command routes (POST /api/commands). Both use the same Engine instance
32
+ * that the CLI REPL also talks to.
33
+ */
34
+ export function startApiServer(engine: Engine, port: number): ApiServerHandle {
35
+ const server = Bun.serve({
36
+ port,
37
+ routes: {
38
+ // --- File data routes (migrated from OutputViewer.serve) ---
39
+ "/": (req) => {
40
+ if (req.method === "OPTIONS") {
41
+ return corsPreflightResponse();
42
+ }
43
+ const files = engine.outputViewer.getFileList().map((f) => ({
44
+ name: f.name,
45
+ type: f.type,
46
+ modification_time: f.modification_time,
47
+ }));
48
+ return jsonResponse({ files });
49
+ },
50
+
51
+ // --- Command route ---
52
+ "/api/commands": async (req) => {
53
+ if (req.method === "OPTIONS") {
54
+ return corsPreflightResponse();
55
+ }
56
+ if (req.method !== "POST") {
57
+ return jsonResponse({ error: "Method Not Allowed. Use POST." }, 405);
58
+ }
59
+ try {
60
+ const body = await req.json() as { command?: string };
61
+ const command = body?.command?.trim() ?? "";
62
+ if (command.length === 0) {
63
+ return jsonResponse({ error: "Missing 'command' field in request body." }, 400);
64
+ }
65
+ const result = await parseAndExecute(engine, command);
66
+ return jsonResponse(result);
67
+ } catch (err) {
68
+ const message = err instanceof Error ? err.message : String(err);
69
+ return jsonResponse({ kind: "error", message: `Server error: ${message}` }, 500);
70
+ }
71
+ },
72
+
73
+ // --- Single file data route ---
74
+ "/:slug": (req) => {
75
+ if (req.method === "OPTIONS") {
76
+ return corsPreflightResponse();
77
+ }
78
+ const slug = req.params.slug;
79
+ const record = engine.outputViewer.getFile(slug);
80
+ if (!record) {
81
+ return jsonResponse({ error: "Not Found" }, 404);
82
+ }
83
+ return jsonResponse({
84
+ name: slug,
85
+ type: record.type,
86
+ content: record.content,
87
+ });
88
+ },
89
+ },
90
+ fetch(req) {
91
+ if (req.method === "OPTIONS") {
92
+ return corsPreflightResponse();
93
+ }
94
+ return jsonResponse({ error: "Not Found" }, 404);
95
+ },
96
+ });
97
+
98
+ const url = server.url.toString();
99
+ console.log(`API server listening at ${url}`);
100
+
101
+ return {
102
+ url,
103
+ stop: () => server.stop(),
104
+ };
105
+ }
@@ -0,0 +1,139 @@
1
+ import type { Engine } from "./engine/index.ts";
2
+
3
+ export type CommandResult = {
4
+ kind: "output" | "error" | "exit";
5
+ message: string;
6
+ };
7
+
8
+ /** All recognized command names — exported for tab-completion. */
9
+ export const COMMAND_NAMES: readonly string[] = [
10
+ "run",
11
+ "clear",
12
+ "status",
13
+ "list",
14
+ "help",
15
+ "exit",
16
+ "quit",
17
+ ] as const;
18
+
19
+ const HELP_TEXT = `Available commands:
20
+ run [slug...] Re-run workflows (all if no slug given)
21
+ clear [slug...] Clear output files (all if no slug given)
22
+ status Show in-flight workflows and output file count
23
+ list Show all configured workflow slugs
24
+ help Show this help message
25
+ exit / quit Shut down the program`;
26
+
27
+ /**
28
+ * Parse a raw command string and dispatch to the appropriate Engine method.
29
+ * Returns a structured result that any input channel (CLI REPL, HTTP API)
30
+ * can format however it likes.
31
+ */
32
+ export async function parseAndExecute(engine: Engine, rawInput: string): Promise<CommandResult> {
33
+ const trimmed = rawInput.trim();
34
+ if (trimmed.length === 0) {
35
+ return { kind: "output", message: "" };
36
+ }
37
+
38
+ const parts = trimmed.split(/\s+/);
39
+ const command = parts[0]!.toLowerCase();
40
+ const args = parts.slice(1);
41
+
42
+ switch (command) {
43
+ case "run":
44
+ return await handleRun(engine, args);
45
+ case "clear":
46
+ return handleClear(engine, args);
47
+ case "status":
48
+ return handleStatus(engine);
49
+ case "list":
50
+ return handleList(engine);
51
+ case "help":
52
+ return { kind: "output", message: HELP_TEXT };
53
+ case "exit":
54
+ case "quit":
55
+ return { kind: "exit", message: "Shutting down..." };
56
+ default:
57
+ return { kind: "error", message: `Unknown command: '${command}'. Type 'help' for available commands.` };
58
+ }
59
+ }
60
+
61
+ async function handleRun(engine: Engine, slugs: string[]): Promise<CommandResult> {
62
+ const filters = slugs.length > 0 ? { only: slugs } : undefined;
63
+ const results = await engine.runWorkflows(filters);
64
+
65
+ if (results.length === 0) {
66
+ return { kind: "output", message: "No workflows matched the given filter." };
67
+ }
68
+
69
+ const lines: string[] = [];
70
+ let hasFailures = false;
71
+ for (const r of results) {
72
+ switch (r.status) {
73
+ case "succeeded":
74
+ lines.push(` ✓ ${r.slug} (run ${r.runNumber}) — succeeded`);
75
+ break;
76
+ case "failed":
77
+ lines.push(` ✗ ${r.slug} (run ${r.runNumber}) — failed: ${r.error ?? "unknown error"}`);
78
+ hasFailures = true;
79
+ break;
80
+ case "rejected":
81
+ lines.push(` ⊘ ${r.slug} — rejected: ${r.error ?? "already running"}`);
82
+ hasFailures = true;
83
+ break;
84
+ }
85
+ }
86
+
87
+ const succeeded = results.filter((r) => r.status === "succeeded").length;
88
+ const failed = results.filter((r) => r.status === "failed").length;
89
+ const rejected = results.filter((r) => r.status === "rejected").length;
90
+
91
+ lines.unshift(`Workflow run complete. Succeeded: ${succeeded}, Failed: ${failed}, Rejected: ${rejected}`);
92
+ return { kind: hasFailures ? "error" : "output", message: lines.join("\n") };
93
+ }
94
+
95
+ function handleClear(engine: Engine, slugs: string[]): CommandResult {
96
+ const filter = slugs.length > 0 ? slugs : undefined;
97
+ engine.clearResults(filter);
98
+ const target = filter ? filter.join(", ") : "all";
99
+ return { kind: "output", message: `Cleared results: ${target}` };
100
+ }
101
+
102
+ function handleStatus(engine: Engine): CommandResult {
103
+ const status = engine.getStatus();
104
+ const lines: string[] = [];
105
+ lines.push(`In-flight workflows: ${status.inFlight.length > 0 ? status.inFlight.join(", ") : "(none)"}`);
106
+ lines.push(`Output files: ${status.completedFiles.length}`);
107
+ if (status.completedFiles.length > 0) {
108
+ for (const f of status.completedFiles) {
109
+ lines.push(` • ${f}`);
110
+ }
111
+ }
112
+ return { kind: "output", message: lines.join("\n") };
113
+ }
114
+
115
+ function handleList(engine: Engine): CommandResult {
116
+ const workflows = engine.listWorkflows();
117
+ const lines: string[] = [];
118
+
119
+ if (workflows.analysis.length > 0) {
120
+ lines.push("Analysis workflows:");
121
+ for (const slug of workflows.analysis) {
122
+ lines.push(` • ${slug}`);
123
+ }
124
+ }
125
+ if (workflows.testing.length > 0) {
126
+ if (lines.length > 0) {
127
+ lines.push("");
128
+ }
129
+ lines.push("Testing workflows:");
130
+ for (const slug of workflows.testing) {
131
+ lines.push(` • ${slug}`);
132
+ }
133
+ }
134
+
135
+ if (lines.length === 0) {
136
+ return { kind: "output", message: "No workflows configured." };
137
+ }
138
+ return { kind: "output", message: lines.join("\n") };
139
+ }
@@ -0,0 +1,165 @@
1
+ import { OpenRouter } from "@openrouter/sdk";
2
+
3
+ import { OutputViewer } from "../util/output-viewer.ts";
4
+ import { executeAnalysisWorkflow } from "../workflow/analysis-workflow.ts";
5
+ import { executeTestingWorkflow } from "../workflow/testing-workflow.ts";
6
+ import type { WorkflowDependencies } from "../workflow/index.ts";
7
+ import type { Config } from "../util/config.ts";
8
+
9
+ export type WorkflowRunResult = {
10
+ slug: string;
11
+ runNumber: number;
12
+ status: "succeeded" | "failed" | "rejected";
13
+ error?: string;
14
+ };
15
+
16
+ export type EngineStatus = {
17
+ inFlight: string[];
18
+ completedFiles: string[];
19
+ };
20
+
21
+ /**
22
+ * Stateful core that owns the OutputViewer, WorkflowDependencies, and
23
+ * in-flight tracking. Runtime-agnostic — no process.stdin / stdout,
24
+ * no HTTP server. Input channels (CLI REPL, HTTP API) call into this
25
+ * via the CommandHandler.
26
+ */
27
+ export class Engine {
28
+ readonly outputViewer: OutputViewer;
29
+ private readonly config: Config;
30
+ private readonly deps: WorkflowDependencies;
31
+ private readonly inFlightSlugs: Set<string> = new Set();
32
+
33
+ constructor(config: Config) {
34
+ this.config = config;
35
+ this.outputViewer = new OutputViewer();
36
+ this.deps = {
37
+ seed: Math.floor(Date.now() / 1000),
38
+ openRouter: new OpenRouter({
39
+ apiKey: config.vendors.openrouter.api_key,
40
+ }),
41
+ outputViewer: this.outputViewer,
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Run workflows matching the given filters.
47
+ * If `only` is provided, only run slugs in that list.
48
+ * If `skip` is provided, skip slugs in that list.
49
+ * Rejects any slug that is already in-flight.
50
+ */
51
+ async runWorkflows(filters?: { only?: string[]; skip?: string[] }): Promise<WorkflowRunResult[]> {
52
+ const onlySlugs = filters?.only;
53
+ const skipSlugs = filters?.skip;
54
+
55
+ let analysisWorkflows = this.applyFilters(this.config.analysis_workflows, onlySlugs, skipSlugs);
56
+ let testingWorkflows = this.applyFilters(this.config.testing_workflows, onlySlugs, skipSlugs);
57
+
58
+ console.log(`Starting execution of ${analysisWorkflows.length} analysis + ${testingWorkflows.length} testing workflows...`);
59
+ console.log([...analysisWorkflows, ...testingWorkflows].map((w) => w.slug));
60
+
61
+ const runs: { slug: string; runNumber: number; promise: Promise<void> }[] = [];
62
+ const results: WorkflowRunResult[] = [];
63
+
64
+ // Build run list, rejecting in-flight duplicates
65
+ for (const workflow of analysisWorkflows) {
66
+ if (this.inFlightSlugs.has(workflow.slug)) {
67
+ results.push({ slug: workflow.slug, runNumber: 0, status: "rejected", error: "already running" });
68
+ continue;
69
+ }
70
+ this.inFlightSlugs.add(workflow.slug);
71
+ for (let i = 0; i < workflow.runs; i++) {
72
+ runs.push({
73
+ slug: workflow.slug,
74
+ runNumber: i + 1,
75
+ promise: executeAnalysisWorkflow(workflow, i + 1, this.deps),
76
+ });
77
+ }
78
+ }
79
+
80
+ for (const workflow of testingWorkflows) {
81
+ if (this.inFlightSlugs.has(workflow.slug)) {
82
+ results.push({ slug: workflow.slug, runNumber: 0, status: "rejected", error: "already running" });
83
+ continue;
84
+ }
85
+ this.inFlightSlugs.add(workflow.slug);
86
+ for (let i = 0; i < workflow.runs; i++) {
87
+ runs.push({
88
+ slug: workflow.slug,
89
+ runNumber: i + 1,
90
+ promise: executeTestingWorkflow(workflow, i + 1, this.deps),
91
+ });
92
+ }
93
+ }
94
+
95
+ // Execute all non-rejected runs in parallel
96
+ const settled = await Promise.allSettled(runs.map((r) => r.promise));
97
+
98
+ // Collect results and clear in-flight tracking
99
+ const completedSlugs = new Set<string>();
100
+ for (let i = 0; i < settled.length; i++) {
101
+ const run = runs[i]!;
102
+ const outcome = settled[i]!;
103
+ completedSlugs.add(run.slug);
104
+ if (outcome.status === "fulfilled") {
105
+ results.push({ slug: run.slug, runNumber: run.runNumber, status: "succeeded" });
106
+ } else {
107
+ const errorMsg = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
108
+ console.warn(`Workflow '${run.slug}' run ${run.runNumber} failed:`, outcome.reason);
109
+ results.push({ slug: run.slug, runNumber: run.runNumber, status: "failed", error: errorMsg });
110
+ }
111
+ }
112
+
113
+ // Remove completed slugs from in-flight set
114
+ for (const slug of completedSlugs) {
115
+ this.inFlightSlugs.delete(slug);
116
+ }
117
+
118
+ return results;
119
+ }
120
+
121
+ /** Clear output files. If slugs provided, only clear matching filenames. */
122
+ clearResults(slugFilter?: string[]): void {
123
+ this.outputViewer.clearFiles(slugFilter);
124
+ }
125
+
126
+ /** Return current engine state snapshot. */
127
+ getStatus(): EngineStatus {
128
+ return {
129
+ inFlight: [...this.inFlightSlugs],
130
+ completedFiles: this.outputViewer.getFileList().map((f) => f.name),
131
+ };
132
+ }
133
+
134
+ /** List all configured workflow slugs grouped by type. */
135
+ listWorkflows(): { analysis: string[]; testing: string[] } {
136
+ return {
137
+ analysis: this.config.analysis_workflows.map((w) => w.slug),
138
+ testing: this.config.testing_workflows.map((w) => w.slug),
139
+ };
140
+ }
141
+
142
+ /** Apply --only / --skip filters to a workflow list. */
143
+ private applyFilters<T extends { slug: string }>(workflows: T[], onlySlugs?: string[], skipSlugs?: string[]): T[] {
144
+ let filtered = workflows;
145
+ if (onlySlugs && onlySlugs.length > 0) {
146
+ filtered = filtered.filter((w) => {
147
+ if (onlySlugs.includes(w.slug)) {
148
+ return true;
149
+ }
150
+ console.log(`Skipping workflow '${w.slug}' (not in --only_workflows list)`);
151
+ return false;
152
+ });
153
+ }
154
+ if (skipSlugs && skipSlugs.length > 0) {
155
+ filtered = filtered.filter((w) => {
156
+ if (skipSlugs.includes(w.slug)) {
157
+ console.log(`Skipping workflow '${w.slug}' (matched --skip_workflow)`);
158
+ return false;
159
+ }
160
+ return true;
161
+ });
162
+ }
163
+ return filtered;
164
+ }
165
+ }
@@ -0,0 +1,2 @@
1
+ export { Engine } from "./engine.ts";
2
+ export type { WorkflowRunResult, EngineStatus } from "./engine.ts";
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import "../version.ts";
4
+
5
+ import { createInterface } from "node:readline";
6
+
7
+ import chalk from "chalk";
8
+
9
+ import { ARGS } from "../util/args.ts";
10
+ import { CONFIG } from "../util/config.ts";
11
+ import { Engine } from "../engine/index.ts";
12
+ import { parseAndExecute, COMMAND_NAMES } from "../command-handler.ts";
13
+ import { startApiServer } from "../api-server.ts";
14
+ import { OutputViewingModeEnum } from "../util/config-schema.ts";
15
+
16
+
17
+ const engine = new Engine(CONFIG);
18
+
19
+ // --- Start API server (WebUI mode) or skip (Local mode) ---
20
+ let apiServerHandle: { url: string; stop: () => void } | null = null;
21
+ let frontendURL = "";
22
+
23
+ if (CONFIG.output_viewing.mode === OutputViewingModeEnum.WebUI) {
24
+ apiServerHandle = startApiServer(engine, CONFIG.output_viewing.api_port);
25
+ const params = new URLSearchParams();
26
+ params.set("api", apiServerHandle.url);
27
+ frontendURL = `${CONFIG.output_viewing.webui_base_url}/tools/results-viewer#${params.toString()}`;
28
+
29
+ console.log(chalk.cyan("Open the following URL to view all outputs:"));
30
+ console.log(frontendURL);
31
+ }
32
+
33
+ // --- Initial workflow run (same as old cli.ts) ---
34
+ const onlySlugs: string[] | undefined = ARGS.values.only_workflows;
35
+ const skipSlugs: string[] | undefined = ARGS.values.skip_workflow;
36
+
37
+ const initialResults = await engine.runWorkflows({
38
+ only: onlySlugs,
39
+ skip: skipSlugs,
40
+ });
41
+
42
+ // Print summary of initial run
43
+ const succeeded = initialResults.filter((r) => r.status === "succeeded").length;
44
+ const failed = initialResults.filter((r) => r.status === "failed").length;
45
+ console.log(`\nInitial run complete. Succeeded: ${succeeded}; Failed: ${failed}`);
46
+ if (failed > 0) {
47
+ for (const r of initialResults.filter((r) => r.status === "failed")) {
48
+ console.warn(` Workflow '${r.slug}' failed: ${r.error ?? "unknown"}`);
49
+ }
50
+ }
51
+
52
+ // In Local mode, print per-file links after initial run
53
+ if (CONFIG.output_viewing.mode === OutputViewingModeEnum.Local) {
54
+ const files = engine.outputViewer.getFileList();
55
+ if (files.length > 0) {
56
+ console.log("\nClick the following links to view the outputs in your browser:");
57
+ for (const file of files) {
58
+ const record = engine.outputViewer.getFile(file.name);
59
+ if (record) {
60
+ const params = new URLSearchParams();
61
+ params.set("name", file.name);
62
+ params.set("comp", "gzip");
63
+ params.set("data", Bun.gzipSync(record.content).toBase64());
64
+ const url = `${CONFIG.output_viewing.webui_base_url}/tools/results-viewer#${params.toString()}`;
65
+ console.log(`${chalk.cyan(file.name)}: ${url}\n`);
66
+ }
67
+ }
68
+ } else {
69
+ console.warn("No files to display.");
70
+ }
71
+ }
72
+
73
+ // --- Tab-completion ---
74
+ function completer(line: string): [string[], string] {
75
+ const trimmed = line.trimStart();
76
+ const parts = trimmed.split(/\s+/);
77
+
78
+ // Completing the command name (first token)
79
+ if (parts.length <= 1) {
80
+ const hits = COMMAND_NAMES.filter((c) => c.startsWith(trimmed.toLowerCase()));
81
+ return [hits, trimmed];
82
+ }
83
+
84
+ // Completing workflow slugs for "run" and "clear"
85
+ const command = parts[0]!.toLowerCase();
86
+ if (command === "run" || command === "clear") {
87
+ const partial = parts[parts.length - 1]!;
88
+ const workflows = engine.listWorkflows();
89
+ const allSlugs = [...workflows.analysis, ...workflows.testing];
90
+ const hits = allSlugs.filter((s) => s.startsWith(partial));
91
+ return [hits, partial];
92
+ }
93
+
94
+ return [[], line];
95
+ }
96
+
97
+ // --- REPL loop ---
98
+ const rl = createInterface({
99
+ input: process.stdin,
100
+ output: process.stdout,
101
+ prompt: chalk.green("epf> "),
102
+ completer,
103
+ });
104
+
105
+ console.log(chalk.gray("\nInteractive mode. Type 'help' for commands, 'exit' to quit.\n"));
106
+ rl.prompt();
107
+
108
+ rl.on("line", async (line: string) => {
109
+ const result = await parseAndExecute(engine, line);
110
+
111
+ if (result.kind === "exit") {
112
+ console.log(result.message);
113
+ rl.close();
114
+ return;
115
+ }
116
+
117
+ if (result.message.length > 0) {
118
+ if (result.kind === "error") {
119
+ console.error(chalk.red(result.message));
120
+ } else {
121
+ console.log(result.message);
122
+ }
123
+ }
124
+
125
+ rl.prompt();
126
+ });
127
+
128
+ rl.on("close", () => {
129
+ console.log(chalk.gray("Goodbye."));
130
+ if (apiServerHandle) {
131
+ apiServerHandle.stop();
132
+ }
133
+ process.exit(0);
134
+ });
135
+
136
+ // Graceful Ctrl+C handling
137
+ process.on("SIGINT", () => {
138
+ console.log(chalk.gray("\nReceived SIGINT."));
139
+ rl.close();
140
+ });
package/src/util/args.ts CHANGED
@@ -1,4 +1,4 @@
1
- import {parseArgs} from "util";
1
+ import { parseArgs } from "util";
2
2
 
3
3
  // console.log(Bun.argv);
4
4
  export const ARGS = parseArgs({
@@ -23,6 +23,11 @@ export const ARGS = parseArgs({
23
23
  short: "S",
24
24
  multiple: true,
25
25
  },
26
+ only_workflows: {
27
+ type: "string",
28
+ short: "O",
29
+ multiple: true,
30
+ },
26
31
  completion_inputs_destination: {
27
32
  type: "string",
28
33
  },
@@ -1,4 +1,4 @@
1
- import {z} from "zod";
1
+ import { z } from "zod";
2
2
 
3
3
  export enum OutputViewingModeEnum {
4
4
  Local = "local",
@@ -20,6 +20,8 @@ export const ModelConfigSchema = z.object({
20
20
  frequency_penalty: z.number().min(-2).max(2).default(0),
21
21
  presence_penalty: z.number().min(-2).max(2).default(0),
22
22
  reasoning_effort: z.enum(["low", "medium", "high"]).default("high"),
23
+ max_retries: z.number().min(0).default(1), // 0 for no retry
24
+ retry_delay_ms: z.number().min(0).default(1000),
23
25
  });
24
26
 
25
27
  export const LLMConfigSchema = z.object({
@@ -45,7 +47,7 @@ export const AnalysisWorkflowEntrySchema = BaseWorkflowEntrySchema.extend({
45
47
  prompt: z.string(),
46
48
  })
47
49
 
48
- export enum LLMJudgeInputModeEnum{
50
+ export enum LLMJudgeInputModeEnum {
49
51
  None = "NONE",
50
52
  Diff = "DIFF",
51
53
  Full = "FULL",
@@ -11,7 +11,7 @@ const homeDir: string = os.homedir();
11
11
  const defaultConfigFileName = "epf.toml";
12
12
  const configURLEnvVar = "EPF_CONFIG_URL";
13
13
 
14
- type Config = z.infer<typeof ConfigSchema>;
14
+ export type Config = z.infer<typeof ConfigSchema>;
15
15
 
16
16
  async function readConfig() {
17
17
  console.log(`Loading config`);
package/src/util/llm.ts CHANGED
@@ -1,46 +1,49 @@
1
- import type {SystemMessage, UserMessage} from "@openrouter/sdk/models";
1
+ import type {ChatSystemMessage, ChatUserMessage} from "@openrouter/sdk/models";
2
2
 
3
3
  import {CONFIG} from "./config.ts";
4
4
  import type {WorkflowDependencies} from "../workflow";
5
5
  import {recordCompletionInput} from "./eval-harness.ts";
6
6
 
7
7
 
8
+ async function delay(ms: number): Promise<void> {
9
+ return new Promise(resolve => setTimeout(resolve, ms));
10
+ }
11
+
8
12
  export async function generateCompletion(deps: WorkflowDependencies,
9
13
  log: (..._: any[])=>void,
10
14
  warn: (..._: any[])=>void,
11
15
  model: string,
12
16
  systemPrompt: string,
13
- content: UserMessage["content"]) {
17
+ content: ChatUserMessage["content"]) {
14
18
  let modelSettings = CONFIG.llm.models[model];
15
19
  if (!modelSettings) {
16
20
  throw new Error(`No model settings found for model "${model}"`);
17
21
  }
18
-
22
+
19
23
  let replacedCount = 0;
20
24
  for (const [replacementKey, replacementValue] of Object.entries(CONFIG.llm.prompt_replacement)) {
21
- if (systemPrompt.includes(replacementKey)) {replacedCount++}
25
+ if (systemPrompt.includes(replacementKey)) {replacedCount++;}
22
26
  systemPrompt = systemPrompt.replaceAll(`{{${replacementKey}}}`, replacementValue);
23
27
  if (typeof content === "string") {
24
- if (content.includes(replacementKey)) {replacedCount++}
28
+ if (content.includes(replacementKey)) {replacedCount++;}
25
29
  content = content.replaceAll(`{{${replacementKey}}}`, replacementValue);
26
30
  }
27
31
  else {
28
32
  for (let i = 0; i < content.length; i++) {
29
33
  const element = content[i];
30
34
  if (element && "type" in element && element.type === "text" && typeof element.text === "string") {
31
- if (element.text.includes(replacementKey)) {replacedCount++}
35
+ if (element.text.includes(replacementKey)) {replacedCount++;}
32
36
  content[i] = {
33
37
  ...element,
34
38
  text: element.text.replaceAll(`{{${replacementKey}}}`, replacementValue),
35
- }
39
+ };
36
40
  }
37
41
  }
38
-
39
42
  }
40
43
  }
41
44
  log(`Replaced ${replacedCount} instances of prompt variables in system prompt and content`);
42
-
43
- let messages: (SystemMessage | UserMessage)[] = [
45
+
46
+ let messages: (ChatSystemMessage | ChatUserMessage)[] = [
44
47
  {
45
48
  role: "system",
46
49
  content: systemPrompt,
@@ -51,30 +54,64 @@ export async function generateCompletion(deps: WorkflowDependencies,
51
54
  }
52
55
  ];
53
56
  setTimeout(async ()=> await recordCompletionInput(messages), 5);
54
-
55
- log("Sending chat completion request...");
56
- let startTime = Date.now();
57
- let completion = await deps.openRouter.chat.send({
58
- model: modelSettings.model_name,
59
- maxCompletionTokens: modelSettings.max_completion_tokens,
60
- messages: messages,
61
- stream: false,
62
- seed: deps.seed,
63
- frequencyPenalty: modelSettings.frequency_penalty,
64
- presencePenalty: modelSettings.presence_penalty,
65
- temperature: modelSettings.temperature,
66
- reasoning: {
67
- effort: modelSettings.reasoning_effort,
68
- },
69
- });
70
- log(`Completion response generated in ${(Date.now() - startTime) / 1000} seconds`);
71
- if (completion.choices.length < 1){
72
- warn("No choices returned from completion");
73
- console.log(completion);
57
+
58
+ const maxRetries = modelSettings.max_retries;
59
+ const retryDelayMs = modelSettings.retry_delay_ms;
60
+ const totalAttempts = maxRetries + 1;
61
+
62
+ let lastError: unknown = null;
63
+
64
+ for (let attempt = 0; attempt < totalAttempts; attempt++) {
65
+ const attemptLabel = `${attempt + 1}/${totalAttempts}`;
66
+
67
+ if (attempt > 0) {
68
+ const backoffMs = retryDelayMs * (2 ** (attempt - 1)) + Math.random() * 200;
69
+ warn(`Retrying after ${Math.round(backoffMs)}ms (attempt ${attemptLabel})...`);
70
+ await delay(backoffMs);
71
+ }
72
+
73
+ log(`Sending chat completion request (attempt ${attemptLabel})...`);
74
+ let startTime = Date.now();
75
+
76
+ try {
77
+ let completion = await deps.openRouter.chat.send({chatRequest: {
78
+ model: modelSettings.model_name,
79
+ maxCompletionTokens: modelSettings.max_completion_tokens,
80
+ messages: messages,
81
+ stream: false,
82
+ seed: deps.seed,
83
+ frequencyPenalty: modelSettings.frequency_penalty,
84
+ presencePenalty: modelSettings.presence_penalty,
85
+ temperature: modelSettings.temperature,
86
+ reasoning: {
87
+ effort: modelSettings.reasoning_effort,
88
+ },
89
+ }});
90
+ log(`Completion response received in ${(Date.now() - startTime) / 1000}s (attempt ${attemptLabel})`);
91
+
92
+ const text = completion.choices[0]?.message.content?.toString() ?? "";
93
+
94
+ if (completion.choices.length < 1 || text.length === 0) {
95
+ warn(`Empty completion on attempt ${attemptLabel}`);
96
+ console.log(completion);
97
+ // Retry if attempts remain; otherwise return empty
98
+ if (attempt < maxRetries) {
99
+ continue;
100
+ }
101
+ warn("Exhausted all retries — returning empty completion");
102
+ return {text: "", model: completion.model};
103
+ }
104
+
105
+ return {text, model: completion.model};
106
+
107
+ } catch (error) {
108
+ const message = error instanceof Error ? error.message : String(error);
109
+ warn(`Chat completion error on attempt ${attemptLabel}: ${message}`);
110
+ lastError = error;
111
+ // Loop continues to next attempt (or exits if this was the last)
112
+ }
74
113
  }
75
-
76
- return {
77
- text: completion.choices[0]?.message.content?.toString() ?? "",
78
- model: completion.model,
79
- };
114
+
115
+ warn("Exhausted all retries due to errors — re-throwing last error");
116
+ throw lastError;
80
117
  }
@@ -1,119 +1,56 @@
1
- import chalk from "chalk";
2
-
3
- import {CONFIG} from "./config.ts";
4
- import {OutputViewingModeEnum} from "./config-schema.ts";
5
-
6
1
  type FileRecord = {
7
2
  type: "markdown" | "text";
8
3
  content: string;
9
- }
10
-
11
- const CORS_HEADERS = {
12
- "Access-Control-Allow-Origin": "*",
13
- "Access-Control-Allow-Methods": "GET, OPTIONS",
14
- "Access-Control-Allow-Headers": "Content-Type",
4
+ modification_time: Date;
15
5
  };
16
6
 
17
- function jsonResponse(data: unknown, status = 200): Response {
18
- return new Response(JSON.stringify(data), {
19
- status,
20
- headers: {
21
- "Content-Type": "application/json",
22
- ...CORS_HEADERS,
23
- },
24
- });
25
- }
26
-
7
+ /**
8
+ * Pure data store for workflow output files.
9
+ * Persists across multiple workflow runs so that re-runs append/overwrite
10
+ * rather than starting from scratch.
11
+ *
12
+ * The HTTP serving layer lives in ApiServer (src/api-server.ts).
13
+ */
27
14
  export class OutputViewer {
28
- filesRecords: Record<string, FileRecord> = {};
29
- displayed: boolean = false;
30
-
31
- addFile(filename: string, _: FileRecord): void {
32
- this.filesRecords[filename] = _;
33
- }
34
-
35
- serve(): string {
36
- let files = Object.entries(this.filesRecords).sort((a, b) => a[0].localeCompare(b[0]));
37
-
38
- let server = Bun.serve({
39
- port: CONFIG.output_viewing.api_port,
40
- routes: {
41
- "/": (req) => {
42
- if (req.method === "OPTIONS") {
43
- return new Response(null, { status: 204, headers: CORS_HEADERS });
44
- }
45
- return jsonResponse({
46
- files: files.map(([filename, fileRecord]) => ({
47
- name: filename,
48
- type: fileRecord.type,
49
- })),
50
- });
51
- },
52
- "/:slug": (req) => {
53
- if (req.method === "OPTIONS") {
54
- return new Response(null, { status: 204, headers: CORS_HEADERS });
55
- }
56
- let slug = req.params.slug;
57
- let record = this.filesRecords[slug];
58
- if (!record) {
59
- return jsonResponse({ error: "Not Found" }, 404);
60
- }
61
- return jsonResponse({
62
- name: slug,
63
- type: record.type,
64
- content: record.content,
65
- });
66
- }
67
- },
68
- fetch(req) {
69
- if (req.method === "OPTIONS") {
70
- return new Response(null, { status: 204, headers: CORS_HEADERS });
71
- }
72
- return jsonResponse({ error: "Not Found" }, 404);
73
- },
74
- });
75
- console.log(server.url);
76
- return server.url.toString();
15
+ fileRecords: Record<string, FileRecord> = {};
16
+
17
+ async addFile(filename: string, fileRecord: Omit<FileRecord, "modification_time">): Promise<void> {
18
+ await Bun.write(filename, fileRecord.content);
19
+ this.fileRecords[filename] = {
20
+ ...fileRecord,
21
+ modification_time: new Date(),
22
+ };
77
23
  }
78
-
79
- display() {
80
- let frontendURL = "";
81
- switch (CONFIG.output_viewing.mode) {
82
- case OutputViewingModeEnum.Local:
83
- if (Object.keys(this.filesRecords).length === 0) {
84
- console.warn("No files to display (you can probably ignore this warning if your workflows haven't completed yet)");
85
- return;
86
- }
87
-
88
- console.log("Click the following links to view the outputs in your browser:");
89
-
90
- let files = Object.entries(this.filesRecords).sort((a, b) => a[0].localeCompare(b[0]));
91
- for (const [filename, fileRecord] of files) {
92
- let params = new URLSearchParams();
93
- params.set("name", filename);
94
- params.set("comp", "gzip");
95
- params.set("data", Bun.gzipSync(fileRecord.content).toBase64());
96
- frontendURL = `${CONFIG.output_viewing.webui_base_url}/tools/results-viewer#${params.toString()}`;
97
- console.log(`${chalk.cyan(filename)}: ${frontendURL}` + "\n");
98
- }
99
- break
100
- case OutputViewingModeEnum.WebUI:
101
- if (this.displayed){
102
- console.log("Output viewer API is already running");
103
- console.log(frontendURL + "\n");
104
- console.log("Press Ctrl+C to stop")
105
- return;
106
- }
107
- this.displayed = true;
108
- let apiURL = this.serve();
109
- let params = new URLSearchParams();
110
- params.set("api", apiURL);
111
- frontendURL = `${CONFIG.output_viewing.webui_base_url}/tools/results-viewer#${params.toString()}`;
112
-
113
- console.log(chalk.cyan("Open the following URL to view all outputs:"));
114
- console.log(frontendURL);
115
- console.log("Press Ctrl+C to stop the server")
24
+
25
+ /**
26
+ * Remove files matching the given slug substrings.
27
+ * If no filter is provided (or empty array), clear everything.
28
+ */
29
+ clearFiles(slugFilter?: string[]): void {
30
+ if (!slugFilter || slugFilter.length === 0) {
31
+ this.fileRecords = {};
32
+ return;
116
33
  }
117
-
34
+ for (const key of Object.keys(this.fileRecords)) {
35
+ if (slugFilter.some((s) => key.includes(s))) {
36
+ delete this.fileRecords[key];
37
+ }
38
+ }
39
+ }
40
+
41
+ /** Return the sorted list of files (metadata only, no content). */
42
+ getFileList(): { name: string; type: string; modification_time: Date }[] {
43
+ return Object.entries(this.fileRecords)
44
+ .sort((a, b) => a[0].localeCompare(b[0]))
45
+ .map(([filename, record]) => ({
46
+ name: filename,
47
+ type: record.type,
48
+ modification_time: record.modification_time,
49
+ }));
50
+ }
51
+
52
+ /** Return a single file's record, or null if not found. */
53
+ getFile(slug: string): FileRecord | null {
54
+ return this.fileRecords[slug] ?? null;
118
55
  }
119
56
  }
@@ -1,11 +1,11 @@
1
- import {Glob} from "bun";
1
+ import { Glob } from "bun";
2
2
 
3
3
  import chalk from "chalk";
4
4
 
5
- import {CONFIG} from "../util/config.ts";
6
- import {FilePayloadGenerator} from "../util/file-payload.ts";
7
- import type {WorkflowDependencies} from "./index.ts";
8
- import {generateCompletion} from "../util/llm.ts";
5
+ import { CONFIG } from "../util/config.ts";
6
+ import { FilePayloadGenerator } from "../util/file-payload.ts";
7
+ import type { WorkflowDependencies } from "./index.ts";
8
+ import { generateCompletion } from "../util/llm.ts";
9
9
 
10
10
 
11
11
  export async function executeAnalysisWorkflow(workflow: typeof CONFIG.analysis_workflows[number], runNum: number, deps: WorkflowDependencies) {
@@ -16,7 +16,7 @@ export async function executeAnalysisWorkflow(workflow: typeof CONFIG.analysis_w
16
16
  const warn = (...args: Parameters<typeof console.warn>) => {
17
17
  console.warn(chalk.red(`[${workflow.slug}]`), ...args);
18
18
  }
19
-
19
+
20
20
  let allFiles = (
21
21
  await Promise.all(
22
22
  workflow.input_files_searches.map(async (fileSearch) => {
@@ -35,7 +35,7 @@ export async function executeAnalysisWorkflow(workflow: typeof CONFIG.analysis_w
35
35
  })
36
36
  )
37
37
  ).flat();
38
-
38
+
39
39
  if (allFiles.length === 0) {
40
40
  warn(`No files found for workflow, skipping...`);
41
41
  return;
@@ -54,7 +54,7 @@ export async function executeAnalysisWorkflow(workflow: typeof CONFIG.analysis_w
54
54
  .replaceAll("[slug]", workflow.slug)
55
55
  .replaceAll("[model]", `(${completion.model.replaceAll("/", "--")})`)
56
56
  .replaceAll("[run]", runNum.toString());
57
- await Bun.write(outputFileName, completion.text);
58
- log(`Completion written to ${outputFileName}`);
59
- deps.outputViewer.addFile(outputFileName, {type: "markdown", content: completion.text});
57
+
58
+ await deps.outputViewer.addFile(outputFileName, { type: "markdown", content: completion.text });
59
+ log(`Completion saved as ${outputFileName}`);
60
60
  }
package/src/cli.ts DELETED
@@ -1,59 +0,0 @@
1
- #!/usr/bin/env bun
2
-
3
- import "./version.ts";
4
-
5
- import {OpenRouter} from "@openrouter/sdk";
6
-
7
- import {CONFIG} from "./util/config.ts";
8
- import {executeTestingWorkflow} from "./workflow/testing-workflow.ts";
9
- import {executeAnalysisWorkflow} from "./workflow/analysis-workflow.ts";
10
- import type {WorkflowDependencies} from "./workflow";
11
- import {OutputViewer} from "./util/output-viewer.ts";
12
-
13
-
14
- const workflowDependencies: WorkflowDependencies = {
15
- seed: Math.floor(Date.now() / 1000),
16
- openRouter: new OpenRouter({
17
- apiKey: CONFIG.vendors.openrouter.api_key,
18
- }),
19
- outputViewer: new OutputViewer(),
20
- }
21
-
22
- // Parallelize workflows with Promise.allSettled
23
- const analysisWorkflows = CONFIG.analysis_workflows;
24
- const testingWorkflows = CONFIG.testing_workflows;
25
- console.log(`Starting execution of ${analysisWorkflows.length} workflows...`);
26
- console.log(analysisWorkflows.map((w) => w.slug));
27
- let workflowRuns: Promise<void>[] = [];
28
- analysisWorkflows.forEach((workflow) => {
29
- for (let i = 0; i < workflow.runs; i++) {
30
- workflowRuns.push(executeAnalysisWorkflow(workflow, i+1, workflowDependencies));
31
- }
32
- });
33
- testingWorkflows.forEach((workflow) => {
34
- for (let i = 0; i < workflow.runs; i++) {
35
- workflowRuns.push(executeTestingWorkflow(workflow, i+1, workflowDependencies));
36
- }
37
- });
38
- workflowDependencies.outputViewer.display(); // For start the server early.
39
- const workflowsResults = await Promise.allSettled(workflowRuns);
40
- // Summarize with indices to include slugs in failure logs
41
- const failedIndices: number[] = [];
42
- const succeededIndices: number[] = [];
43
- workflowsResults.forEach((r, i) => {
44
- if (r.status === "rejected") failedIndices.push(i);
45
- else succeededIndices.push(i);
46
- });
47
-
48
- console.log(`Workflows completed. Succeeded: ${succeededIndices.length}; Failed: ${failedIndices.length}`);
49
- if (failedIndices.length > 0) {
50
- failedIndices.forEach((i) => {
51
- const r = workflowsResults[i] as PromiseRejectedResult;
52
- const slug = analysisWorkflows[i]?.slug ?? `#${i + 1}`;
53
- console.warn(`Workflow '${slug}' failed:`, r.reason);
54
- });
55
- }
56
-
57
- workflowDependencies.outputViewer.display();
58
-
59
- console.log("index.ts done");