@fede0089/skill-eval 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,58 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import { ConfigError } from './errors.js';
4
- const CONFIG_FILE = '.skill-eval.json';
5
- /**
6
- * Loads configuration from a `.skill-eval.json` file in the given directory.
7
- * Returns an empty object if the file does not exist (config is optional).
8
- *
9
- * CLI flags always take precedence over config file values — this function
10
- * only provides defaults for flags not explicitly passed on the command line.
11
- *
12
- * @throws ConfigError on malformed JSON or type mismatches.
13
- */
14
- export function loadConfig(cwd) {
15
- const configPath = path.join(cwd, CONFIG_FILE);
16
- if (!fs.existsSync(configPath)) {
17
- return {};
18
- }
19
- let raw;
20
- try {
21
- raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
22
- }
23
- catch (err) {
24
- throw new ConfigError(`Failed to parse ${CONFIG_FILE}: ${err instanceof Error ? err.message : String(err)}`);
25
- }
26
- if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
27
- throw new ConfigError(`${CONFIG_FILE} must be a JSON object.`);
28
- }
29
- const config = raw;
30
- const result = {};
31
- if ('agent' in config) {
32
- if (typeof config.agent !== 'string')
33
- throw new ConfigError(`${CONFIG_FILE}: 'agent' must be a string.`);
34
- result.agent = config.agent;
35
- }
36
- if ('concurrency' in config) {
37
- if (typeof config.concurrency !== 'number')
38
- throw new ConfigError(`${CONFIG_FILE}: 'concurrency' must be a number.`);
39
- result.concurrency = config.concurrency;
40
- }
41
- if ('trials' in config) {
42
- if (typeof config.trials !== 'number')
43
- throw new ConfigError(`${CONFIG_FILE}: 'trials' must be a number.`);
44
- result.trials = config.trials;
45
- }
46
- if ('report' in config) {
47
- if (config.report !== 'html' && config.report !== 'json') {
48
- throw new ConfigError(`${CONFIG_FILE}: 'report' must be 'html' or 'json'.`);
49
- }
50
- result.report = config.report;
51
- }
52
- if ('skill' in config) {
53
- if (typeof config.skill !== 'string')
54
- throw new ConfigError(`${CONFIG_FILE}: 'skill' must be a string.`);
55
- result.skill = config.skill;
56
- }
57
- return result;
58
- }
@@ -1,354 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { Logger } from '../../utils/logger.js';
4
- export class HtmlReporter {
5
- generate(report, runDir) {
6
- const html = generateHtml(report);
7
- const htmlPath = path.join(runDir, 'report.html');
8
- fs.writeFileSync(htmlPath, html, 'utf-8');
9
- Logger.write(`\n Report: file://${htmlPath}\n`);
10
- }
11
- }
12
- // ---------------------------------------------------------------------------
13
- // HTML generation (module-private)
14
- // ---------------------------------------------------------------------------
15
- function escapeHtml(s) {
16
- return s
17
- .replace(/&/g, '&')
18
- .replace(/</g, '&lt;')
19
- .replace(/>/g, '&gt;')
20
- .replace(/"/g, '&quot;')
21
- .replace(/'/g, '&#39;');
22
- }
23
- function formatPercent(val) {
24
- return `${Math.round(val * 100)}%`;
25
- }
26
- function passColorClass(val) {
27
- if (val >= 0.8)
28
- return 'green';
29
- if (val >= 0.5)
30
- return 'amber';
31
- return 'red';
32
- }
33
- function isFunctional(report) {
34
- return report.metrics.withoutSkillScore !== undefined;
35
- }
36
- // ---------------------------------------------------------------------------
37
- // Cards
38
- // ---------------------------------------------------------------------------
39
- function renderCard(label, value, colorClass) {
40
- return `<div class="card"><div class="card-value ${colorClass}">${escapeHtml(value)}</div><div class="card-label">${escapeHtml(label)}</div></div>`;
41
- }
42
- function renderMetricsCards(report) {
43
- const { metrics } = report;
44
- const numTrials = metrics.numTrials ?? 1;
45
- const cards = [];
46
- if (isFunctional(report)) {
47
- const bk = metrics.withoutSkillPassAtK ?? 0;
48
- const tk = metrics.passAtK ?? 0;
49
- const upliftRaw = parseInt(metrics.skillUplift ?? '0', 10);
50
- const upliftClass = upliftRaw > 0 ? 'green' : upliftRaw < 0 ? 'red' : 'amber';
51
- cards.push(renderCard('Without Skill p@1', formatPercent(bk), passColorClass(bk)));
52
- if (numTrials > 1) {
53
- const bn = metrics.withoutSkillPassAtN ?? 0;
54
- cards.push(renderCard(`Without Skill p@${numTrials}`, formatPercent(bn), passColorClass(bn)));
55
- }
56
- cards.push(renderCard('With Skill p@1', formatPercent(tk), passColorClass(tk)));
57
- if (numTrials > 1) {
58
- const tn = metrics.passAtN ?? 0;
59
- cards.push(renderCard(`With Skill p@${numTrials}`, formatPercent(tn), passColorClass(tn)));
60
- }
61
- cards.push(renderCard('Skill Uplift', escapeHtml(metrics.skillUplift ?? '0%'), upliftClass));
62
- }
63
- else {
64
- const k = metrics.passAtK ?? 0;
65
- cards.push(renderCard('pass@1', formatPercent(k), passColorClass(k)));
66
- if (numTrials > 1) {
67
- const n = metrics.passAtN ?? 0;
68
- cards.push(renderCard(`pass@${numTrials}`, formatPercent(n), passColorClass(n)));
69
- }
70
- cards.push(renderCard('Tasks passed', `${metrics.passedCount}/${metrics.totalCount}`, passColorClass(metrics.passedCount / Math.max(metrics.totalCount, 1))));
71
- }
72
- return `<div class="cards">${cards.join('')}</div>`;
73
- }
74
- // ---------------------------------------------------------------------------
75
- // Chart
76
- // ---------------------------------------------------------------------------
77
- function renderChart(report) {
78
- const { results, metrics } = report;
79
- const numTrials = metrics.numTrials ?? 1;
80
- const labels = results.map(r => `Task #${r.taskId}`);
81
- let datasets;
82
- if (isFunctional(report)) {
83
- const withoutSkillData = results.map(r => {
84
- const bt = r.withoutSkillTrials ?? [];
85
- return bt.length === 0 ? 0 : Math.round((bt.filter(t => t.trialPassed).length / bt.length) * 100);
86
- });
87
- const withSkillData = results.map(r => Math.round((r.trials.filter(t => t.trialPassed).length / Math.max(r.trials.length, 1)) * 100));
88
- datasets = [
89
- { label: 'Without Skill p@1', data: withoutSkillData, backgroundColor: '#94a3b8', borderRadius: 4 },
90
- { label: 'With Skill p@1', data: withSkillData, backgroundColor: '#3b82f6', borderRadius: 4 },
91
- ];
92
- }
93
- else {
94
- const passData = results.map(r => Math.round((r.trials.filter(t => t.trialPassed).length / Math.max(r.trials.length, 1)) * 100));
95
- datasets = [{
96
- label: numTrials > 1 ? 'pass@1' : 'Score',
97
- data: passData,
98
- backgroundColor: passData.map(v => v >= 80 ? '#22c55e' : v >= 50 ? '#f59e0b' : '#ef4444'),
99
- borderRadius: 4,
100
- }];
101
- }
102
- const chartData = JSON.stringify({ labels, datasets });
103
- return `
104
- <div class="chart-wrap">
105
- <script type="application/json" id="chart-data">${chartData}</script>
106
- <canvas id="eval-chart"></canvas>
107
- </div>`;
108
- }
109
- // ---------------------------------------------------------------------------
110
- // Trial details
111
- // ---------------------------------------------------------------------------
112
- function renderAssertions(assertions) {
113
- if (assertions.length === 0)
114
- return '<p class="muted">No assertions recorded.</p>';
115
- return assertions.map(a => {
116
- const icon = a.passed ? '✓' : '✗';
117
- const cls = a.passed ? 'assert-pass' : 'assert-fail';
118
- const grader = a.graderType ? `<span class="badge">${escapeHtml(a.graderType)}</span>` : '';
119
- return `<div class="assertion ${cls}">
120
- <span class="assert-icon">${icon}</span>
121
- <div class="assert-body">
122
- <div class="assert-text">${escapeHtml(a.assertion)}${grader}</div>
123
- ${a.reason ? `<div class="assert-reason">${escapeHtml(a.reason)}</div>` : ''}
124
- </div>
125
- </div>`;
126
- }).join('');
127
- }
128
- function renderTrial(trial, prefix) {
129
- const cls = trial.trialPassed ? 'trial-pass' : 'trial-fail';
130
- const badge = trial.trialPassed
131
- ? '<span class="pill green">PASS</span>'
132
- : '<span class="pill red">FAIL</span>';
133
- return `<div class="trial ${cls}">
134
- <div class="trial-header">${escapeHtml(prefix)} Trial ${trial.id} ${badge}</div>
135
- <div class="trial-assertions">${renderAssertions(trial.assertionResults)}</div>
136
- </div>`;
137
- }
138
- function renderTaskDetails(result, isFunctionalEval) {
139
- const sections = [];
140
- if (isFunctionalEval && result.withoutSkillTrials && result.withoutSkillTrials.length > 0) {
141
- sections.push('<div class="trial-group-label">Without Skill</div>');
142
- sections.push(...result.withoutSkillTrials.map(t => renderTrial(t, 'Without Skill')));
143
- sections.push('<div class="trial-group-label">With Skill</div>');
144
- }
145
- sections.push(...result.trials.map(t => renderTrial(t, 'With Skill')));
146
- return `<div class="task-details" id="details-${result.taskId}">${sections.join('')}</div>`;
147
- }
148
- // ---------------------------------------------------------------------------
149
- // Task table
150
- // ---------------------------------------------------------------------------
151
- function renderTaskTable(report) {
152
- const { results, metrics } = report;
153
- const numTrials = metrics.numTrials ?? 1;
154
- const functional = isFunctional(report);
155
- const headerCells = functional
156
- ? ['#', 'Prompt', 'W/o p@1', 'W/ p@1', 'Details']
157
- : numTrials > 1
158
- ? ['#', 'Prompt', 'pass@1', `pass@${numTrials}`, 'Details']
159
- : ['#', 'Prompt', 'Status', 'Details'];
160
- const headerRow = `<tr>${headerCells.map(h => `<th>${escapeHtml(h)}</th>`).join('')}</tr>`;
161
- const rows = results.map(result => {
162
- const prompt = escapeHtml(result.prompt);
163
- const trials = result.trials;
164
- const bt = result.withoutSkillTrials ?? [];
165
- let statCells;
166
- if (functional) {
167
- const bp1 = bt.length ? Math.round((bt.filter(t => t.trialPassed).length / bt.length) * 100) : 0;
168
- const tp1 = trials.length ? Math.round((trials.filter(t => t.trialPassed).length / trials.length) * 100) : 0;
169
- statCells = `<td class="${passColorClass(bp1 / 100)}">${bp1}%</td><td class="${passColorClass(tp1 / 100)}">${tp1}%</td>`;
170
- }
171
- else if (numTrials > 1) {
172
- const p1 = Math.round((trials.filter(t => t.trialPassed).length / Math.max(trials.length, 1)) * 100);
173
- const passed = trials.filter(t => t.trialPassed).length;
174
- const pn = trials.length > 0 ? Math.round((passed / trials.length) * 100) : 0;
175
- statCells = `<td class="${passColorClass(p1 / 100)}">${p1}%</td><td class="${passColorClass(pn / 100)}">${pn}%</td>`;
176
- }
177
- else {
178
- const passed = trials[0]?.trialPassed ?? false;
179
- statCells = `<td class="${passed ? 'green' : 'red'}">${passed ? 'PASS' : 'FAIL'}</td>`;
180
- }
181
- const detailsBtn = `<button class="details-btn" data-target="details-${result.taskId}">▶</button>`;
182
- const detailsRow = `<tr class="details-row"><td colspan="${headerCells.length}">${renderTaskDetails(result, functional)}</td></tr>`;
183
- return `<tr><td>${result.taskId}</td><td class="prompt-cell">${prompt}</td>${statCells}<td>${detailsBtn}</td></tr>${detailsRow}`;
184
- }).join('');
185
- return `<div class="table-wrap"><table><thead>${headerRow}</thead><tbody>${rows}</tbody></table></div>`;
186
- }
187
- // ---------------------------------------------------------------------------
188
- // Full document
189
- // ---------------------------------------------------------------------------
190
- export function generateHtml(report) {
191
- const { skill_name, agent, timestamp, metrics } = report;
192
- const functional = isFunctional(report);
193
- const evalType = functional ? 'Functional' : 'Trigger';
194
- const overallScore = metrics.passAtK ?? 0;
195
- const statusClass = passColorClass(overallScore);
196
- const formattedDate = new Date(timestamp).toLocaleString();
197
- return `<!DOCTYPE html>
198
- <html lang="en">
199
- <head>
200
- <meta charset="UTF-8">
201
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
202
- <title>Skill Eval — ${escapeHtml(skill_name)}</title>
203
- <script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
204
- <style>
205
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
206
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f8fafc; color: #1e293b; font-size: 14px; }
207
- a { color: #3b82f6; }
208
-
209
- /* Layout */
210
- .container { max-width: 960px; margin: 0 auto; padding: 24px 16px 48px; }
211
-
212
- /* Header */
213
- .header { background: #1e293b; color: #f1f5f9; padding: 24px 28px; border-radius: 10px; margin-bottom: 24px; }
214
- .header h1 { font-size: 22px; font-weight: 700; margin-bottom: 8px; }
215
- .header-meta { display: flex; gap: 24px; flex-wrap: wrap; font-size: 13px; color: #94a3b8; }
216
- .header-meta span b { color: #e2e8f0; }
217
- .status-bar { height: 4px; border-radius: 2px; margin-top: 16px; }
218
- .status-bar.green { background: #22c55e; }
219
- .status-bar.amber { background: #f59e0b; }
220
- .status-bar.red { background: #ef4444; }
221
-
222
- /* Cards */
223
- .cards { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 24px; }
224
- .card { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; padding: 16px 20px; min-width: 120px; flex: 1; }
225
- .card-value { font-size: 28px; font-weight: 700; line-height: 1; margin-bottom: 4px; }
226
- .card-label { font-size: 12px; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; }
227
-
228
- /* Color utilities */
229
- .green { color: #16a34a; }
230
- .amber { color: #d97706; }
231
- .red { color: #dc2626; }
232
-
233
- /* Section */
234
- .section { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; margin-bottom: 20px; overflow: hidden; }
235
- .section-title { font-weight: 600; font-size: 13px; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b; padding: 12px 16px; border-bottom: 1px solid #f1f5f9; }
236
-
237
- /* Chart */
238
- .chart-wrap { padding: 16px; }
239
- #eval-chart { max-height: 300px; }
240
-
241
- /* Table */
242
- .table-wrap { overflow-x: auto; }
243
- table { width: 100%; border-collapse: collapse; }
244
- th { background: #f8fafc; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b; padding: 10px 12px; text-align: left; border-bottom: 1px solid #e2e8f0; }
245
- td { padding: 10px 12px; border-bottom: 1px solid #f1f5f9; vertical-align: top; }
246
- tr:last-child td { border-bottom: none; }
247
- .prompt-cell { max-width: 360px; word-break: break-word; color: #334155; }
248
-
249
- /* Details */
250
- .details-btn { background: none; border: 1px solid #e2e8f0; border-radius: 4px; cursor: pointer; padding: 2px 8px; font-size: 11px; color: #64748b; transition: background 0.15s; }
251
- .details-btn:hover { background: #f1f5f9; }
252
- .details-btn.open { color: #3b82f6; border-color: #3b82f6; }
253
- .details-row > td { padding: 0; background: #f8fafc; }
254
- .task-details { display: none; padding: 12px 16px; }
255
- .task-details.visible { display: block; }
256
- .trial-group-label { font-size: 11px; font-weight: 600; text-transform: uppercase; color: #94a3b8; letter-spacing: 0.05em; margin: 8px 0 4px; }
257
-
258
- /* Trials */
259
- .trial { border: 1px solid #e2e8f0; border-radius: 6px; margin-bottom: 8px; overflow: hidden; }
260
- .trial-header { display: flex; align-items: center; gap: 8px; padding: 8px 12px; font-weight: 500; font-size: 13px; background: #f8fafc; }
261
- .trial-pass .trial-header { border-left: 3px solid #22c55e; }
262
- .trial-fail .trial-header { border-left: 3px solid #ef4444; }
263
- .trial-assertions { padding: 8px 12px; display: flex; flex-direction: column; gap: 6px; }
264
-
265
- /* Pills */
266
- .pill { display: inline-block; font-size: 10px; font-weight: 700; padding: 2px 7px; border-radius: 99px; letter-spacing: 0.05em; }
267
- .pill.green { background: #dcfce7; color: #15803d; }
268
- .pill.red { background: #fee2e2; color: #b91c1c; }
269
-
270
- /* Badge */
271
- .badge { display: inline-block; font-size: 10px; font-weight: 500; padding: 1px 6px; border-radius: 4px; background: #e2e8f0; color: #475569; margin-left: 6px; vertical-align: middle; }
272
-
273
- /* Assertions */
274
- .assertion { display: flex; gap: 8px; }
275
- .assert-icon { flex-shrink: 0; font-size: 14px; margin-top: 1px; }
276
- .assert-pass .assert-icon { color: #16a34a; }
277
- .assert-fail .assert-icon { color: #dc2626; }
278
- .assert-body { flex: 1; min-width: 0; }
279
- .assert-text { font-size: 13px; color: #1e293b; word-break: break-word; }
280
- .assert-reason { font-size: 12px; color: #64748b; margin-top: 2px; word-break: break-word; }
281
- .muted { color: #94a3b8; font-size: 13px; }
282
- </style>
283
- </head>
284
- <body>
285
- <div class="container">
286
-
287
- <!-- Header -->
288
- <div class="header">
289
- <h1>${escapeHtml(skill_name)}</h1>
290
- <div class="header-meta">
291
- <span><b>Agent</b> ${escapeHtml(agent)}</span>
292
- <span><b>Type</b> ${evalType}</span>
293
- <span><b>Date</b> ${escapeHtml(formattedDate)}</span>
294
- <span><b>Score</b> ${escapeHtml(metrics.withSkillScore)}</span>
295
- </div>
296
- <div class="status-bar ${statusClass}"></div>
297
- </div>
298
-
299
- <!-- Metric Cards -->
300
- ${renderMetricsCards(report)}
301
-
302
- <!-- Chart -->
303
- <div class="section">
304
- <div class="section-title">Pass rate by task</div>
305
- ${renderChart(report)}
306
- </div>
307
-
308
- <!-- Task Table -->
309
- <div class="section">
310
- <div class="section-title">Task results</div>
311
- ${renderTaskTable(report)}
312
- </div>
313
-
314
- </div>
315
- <script>
316
- (function () {
317
- // Chart
318
- const rawData = document.getElementById('chart-data');
319
- if (rawData) {
320
- const data = JSON.parse(rawData.textContent || '{}');
321
- const ctx = document.getElementById('eval-chart');
322
- if (ctx) {
323
- new Chart(ctx, {
324
- type: 'bar',
325
- data: data,
326
- options: {
327
- indexAxis: 'y',
328
- responsive: true,
329
- plugins: { legend: { display: ${functional ? 'true' : 'false'} } },
330
- scales: {
331
- x: { min: 0, max: 100, ticks: { callback: v => v + '%' }, grid: { color: '#f1f5f9' } },
332
- y: { grid: { display: false } }
333
- }
334
- }
335
- });
336
- }
337
- }
338
-
339
- // Accordion
340
- document.querySelectorAll('.details-btn').forEach(function (btn) {
341
- btn.addEventListener('click', function () {
342
- const target = document.getElementById(btn.getAttribute('data-target'));
343
- if (!target) return;
344
- const isOpen = target.classList.contains('visible');
345
- target.classList.toggle('visible', !isOpen);
346
- btn.classList.toggle('open', !isOpen);
347
- btn.textContent = isOpen ? '▶' : '▼';
348
- });
349
- });
350
- }());
351
- </script>
352
- </body>
353
- </html>`;
354
- }
@@ -1,9 +0,0 @@
1
- import { HtmlReporter } from './html-reporter.js';
2
- import { JsonReporter } from './json-reporter.js';
3
- export { HtmlReporter } from './html-reporter.js';
4
- export { JsonReporter } from './json-reporter.js';
5
- export function createReporter(format) {
6
- if (format === 'html')
7
- return new HtmlReporter();
8
- return new JsonReporter();
9
- }
@@ -1,7 +0,0 @@
1
- import path from 'path';
2
- import { Logger } from '../../utils/logger.js';
3
- export class JsonReporter {
4
- generate(_report, runDir) {
5
- Logger.write(`\n Report: file://${path.join(runDir, 'summary.json')}\n`);
6
- }
7
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,75 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HeadlessRunner = void 0;
4
- const child_process_1 = require("child_process");
5
- class HeadlessRunner {
6
- agent;
7
- constructor(agent) {
8
- this.agent = agent;
9
- }
10
- /**
11
- * Runs the prompt through a headless isolated gemini instance in auto_edit mode.
12
- * Auto Edit mode automatically allows tools meant for modifying files, falling back interactively
13
- * only for severe system-level actions (which we assume tests shouldn't invoke, or will fail if they do in non-interactive).
14
- * @param prompt The evaluation prompt text
15
- * @returns Parsed JSON output from Gemini
16
- */
17
- runPrompt(prompt) {
18
- if (this.agent !== 'gemini-cli') {
19
- console.error(`\n[Runner] Agent '${this.agent}' is not supported yet.`);
20
- return null;
21
- }
22
- // Escaping the prompt just in case. spawnSync handles formatting but better safe.
23
- try {
24
- const child = (0, child_process_1.spawnSync)('gemini', [
25
- '-p', prompt,
26
- '-o', 'json',
27
- '--approval-mode', 'auto_edit'
28
- ], {
29
- encoding: 'utf-8',
30
- // Not passing default stdio because we need to parse stdout
31
- // silently without printing all the tool interactions to the user terminal.
32
- stdio: ['ignore', 'pipe', 'pipe']
33
- });
34
- if (child.error) {
35
- console.error(`\n[Runner] Failed to start gemini CLI. Error: ${child.error.message}`);
36
- return null;
37
- }
38
- // If exit code is not 0, there was likely a crash or unhanded exception in gemini cli.
39
- if (child.status !== 0) {
40
- console.error(`\n[Runner] Gemini CLI exited with status ${child.status}`);
41
- if (child.stderr) {
42
- console.error(`[Runner Stderr] ${child.stderr.trim()}`);
43
- }
44
- }
45
- const rawOutput = child.stdout;
46
- if (!rawOutput || rawOutput.trim() === '') {
47
- console.error(`\n[Runner] Received empty output from Gemini CLI. Cannot evaluate.`);
48
- return null;
49
- }
50
- // We need to safely extract the JSON part because Gemini CLI might emit
51
- // non-JSON warnings like "MCP issues detected. Run /mcp list for status." to stdout.
52
- // We'll search for the first '{' to begin our JSON payload.
53
- let jsonPart = rawOutput.trim();
54
- const firstBraceIndex = jsonPart.indexOf('{');
55
- if (firstBraceIndex > 0) {
56
- jsonPart = jsonPart.substring(firstBraceIndex);
57
- }
58
- try {
59
- const parsed = JSON.parse(jsonPart);
60
- return parsed;
61
- }
62
- catch (parseError) {
63
- console.error(`\n[Runner] Failed to parse JSON output: ${parseError}`);
64
- // Dump first 300 chars of raw to debug
65
- console.error(`[Runner Output Preview] ${rawOutput.substring(0, 300)}`);
66
- return null;
67
- }
68
- }
69
- catch (e) {
70
- console.error(`\n[Runner] Unexpected error running process: ${e}`);
71
- return null;
72
- }
73
- }
74
- }
75
- exports.HeadlessRunner = HeadlessRunner;
@@ -1,18 +0,0 @@
1
- import { GeminiCliRunner } from './gemini-cli.runner.js';
2
- export class RunnerFactory {
3
- /**
4
- * Factory method to create an agent runner based on the agent name.
5
- * Add new agent implementations here.
6
- * @param agent The name of the agent to run
7
- * @returns An implementation of AgentRunner
8
- * @throws Error if the agent is not supported
9
- */
10
- static create(agent) {
11
- switch (agent) {
12
- case 'gemini-cli':
13
- return new GeminiCliRunner();
14
- default:
15
- throw new Error(`Agent '${agent}' is not supported yet.`);
16
- }
17
- }
18
- }
@@ -1,138 +0,0 @@
1
- import * as fs from 'fs';
2
- import child_process from 'child_process';
3
- import { Logger } from '../../utils/logger.js';
4
- export class GeminiCliRunner {
5
- /**
6
- * Runs the prompt through an isolated gemini instance.
7
- * Default mode is headless using --approval-mode auto_edit.
8
- *
9
- * @param prompt The evaluation prompt text
10
- * @param cwd Optional execution directory
11
- * @param onLog Callback to receive real-time logs (from stderr)
12
- * @param logPath Optional file path to save raw execution logs
13
- * @returns Raw output from Gemini
14
- */
15
- async runPrompt(prompt, cwd, onLog, logPath, extraArgs = []) {
16
- return new Promise((resolve) => {
17
- let stdout = '';
18
- let stderr = '';
19
- let resolved = false;
20
- // Use -p and --approval-mode auto_edit for headless mode.
21
- const args = ['-p', prompt, '--approval-mode', 'auto_edit', ...extraArgs];
22
- const spawnOptions = {
23
- cwd: cwd,
24
- env: { ...process.env, FORCE_COLOR: '1' }
25
- };
26
- const child = child_process.spawn('gemini', args, spawnOptions);
27
- // Setup log stream if path is provided
28
- let logStream = null;
29
- let logStreamDone = true; // Default to true if no logPath
30
- if (logPath) {
31
- try {
32
- logStream = fs.createWriteStream(logPath, { flags: 'a' });
33
- logStreamDone = false;
34
- logStream.on('finish', () => {
35
- logStreamDone = true;
36
- checkAllDone();
37
- });
38
- logStream.write(`--- Gemini CLI Execution Start: ${new Date().toISOString()} ---\n`);
39
- logStream.write(`Command: gemini ${args.join(' ')}\n\n`);
40
- }
41
- catch (err) {
42
- Logger.warn(`Failed to create log file at ${logPath} — verbose output will not be saved. Continuing. Reason: ${err}`);
43
- logStreamDone = true;
44
- }
45
- }
46
- // Safety timeout: 5 minutes (300,000 ms)
47
- const timeout = setTimeout(() => {
48
- if (!resolved) {
49
- resolved = true;
50
- child.kill('SIGKILL');
51
- if (logStream) {
52
- logStream.write('\n\n--- Gemini CLI process timed out ---\n');
53
- logStream.end();
54
- }
55
- Logger.error('\nGemini CLI process timed out after 5 minutes.');
56
- resolve({ error: 'Process timeout exceeded (5 minutes)', raw_output: stderr });
57
- }
58
- }, 300000);
59
- // Track completion of streams
60
- let stdoutDone = false;
61
- let stderrDone = false;
62
- let processDone = false;
63
- function checkAllDone() {
64
- if (stdoutDone && stderrDone && processDone && logStreamDone && !resolved) {
65
- resolved = true;
66
- clearTimeout(timeout);
67
- if (!stdout || stdout.trim() === '') {
68
- return resolve({ error: 'Empty output from Gemini CLI', raw_output: stderr });
69
- }
70
- return resolve({
71
- response: stdout.trim(),
72
- raw_output: `${stdout}\n--- STDERR ---\n${stderr}`
73
- });
74
- }
75
- }
76
- if (child.stdout) {
77
- child.stdout.on('data', (data) => {
78
- const chunk = data.toString();
79
- stdout += chunk;
80
- if (logStream)
81
- logStream.write(chunk);
82
- });
83
- child.stdout.on('end', () => {
84
- stdoutDone = true;
85
- checkAllDone();
86
- });
87
- }
88
- else {
89
- stdoutDone = true;
90
- }
91
- if (child.stderr) {
92
- child.stderr.on('data', (data) => {
93
- const chunk = data.toString();
94
- stderr += chunk;
95
- if (onLog) {
96
- const lines = chunk.split('\n').filter((l) => l.trim() !== '');
97
- if (lines.length > 0) {
98
- onLog(lines[lines.length - 1]);
99
- }
100
- }
101
- });
102
- child.stderr.on('end', () => {
103
- stderrDone = true;
104
- checkAllDone();
105
- });
106
- }
107
- else {
108
- stderrDone = true;
109
- }
110
- child.on('error', (err) => {
111
- if (!resolved) {
112
- resolved = true;
113
- clearTimeout(timeout);
114
- if (logStream) {
115
- logStream.write(`\n\n--- Error starting Gemini CLI: ${err.message} ---\n`);
116
- logStream.end();
117
- }
118
- Logger.error(`Failed to start gemini CLI. Error: ${err.message}`);
119
- resolve(null);
120
- }
121
- });
122
- child.on('close', (code) => {
123
- if (logStream) {
124
- logStream.write(`\n\n--- Gemini CLI exited with status ${code} ---\n`);
125
- logStream.end();
126
- }
127
- if (code !== 0 && !resolved) {
128
- Logger.error(`Gemini CLI exited with status ${code}`);
129
- if (stderr) {
130
- Logger.debug(`Gemini CLI Stderr: ${stderr.trim()}`);
131
- }
132
- }
133
- processDone = true;
134
- checkAllDone();
135
- });
136
- });
137
- }
138
- }
@@ -1,3 +0,0 @@
1
- export * from './runner.interface.js';
2
- export * from './gemini-cli.runner.js';
3
- export * from './factory.js';
@@ -1 +0,0 @@
1
- export {};