agent-tool-forge 0.3.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/LICENSE +21 -0
- package/README.md +209 -0
- package/lib/agent-registry.js +170 -0
- package/lib/api-client.js +792 -0
- package/lib/api-loader.js +260 -0
- package/lib/auth.d.ts +25 -0
- package/lib/auth.js +158 -0
- package/lib/checks/check-adapter.js +172 -0
- package/lib/checks/compose.js +42 -0
- package/lib/checks/content-match.js +14 -0
- package/lib/checks/cost-budget.js +11 -0
- package/lib/checks/index.js +18 -0
- package/lib/checks/json-valid.js +15 -0
- package/lib/checks/latency.js +11 -0
- package/lib/checks/length-bounds.js +17 -0
- package/lib/checks/negative-match.js +14 -0
- package/lib/checks/no-hallucinated-numbers.js +63 -0
- package/lib/checks/non-empty.js +34 -0
- package/lib/checks/regex-match.js +12 -0
- package/lib/checks/run-checks.js +84 -0
- package/lib/checks/schema-match.js +26 -0
- package/lib/checks/tool-call-count.js +16 -0
- package/lib/checks/tool-selection.js +34 -0
- package/lib/checks/types.js +45 -0
- package/lib/comparison/compare.js +86 -0
- package/lib/comparison/format.js +104 -0
- package/lib/comparison/index.js +6 -0
- package/lib/comparison/statistics.js +59 -0
- package/lib/comparison/types.js +41 -0
- package/lib/config-schema.js +200 -0
- package/lib/config.d.ts +66 -0
- package/lib/conversation-store.d.ts +77 -0
- package/lib/conversation-store.js +443 -0
- package/lib/db.d.ts +6 -0
- package/lib/db.js +1112 -0
- package/lib/dep-check.js +99 -0
- package/lib/drift-background.js +61 -0
- package/lib/drift-monitor.js +187 -0
- package/lib/eval-runner.js +566 -0
- package/lib/fixtures/fixture-store.js +161 -0
- package/lib/fixtures/index.js +11 -0
- package/lib/forge-engine.js +982 -0
- package/lib/forge-eval-generator.js +417 -0
- package/lib/forge-file-writer.js +386 -0
- package/lib/forge-service-client.js +190 -0
- package/lib/forge-service.d.ts +4 -0
- package/lib/forge-service.js +655 -0
- package/lib/forge-verifier-generator.js +271 -0
- package/lib/handlers/admin.js +151 -0
- package/lib/handlers/agents.js +229 -0
- package/lib/handlers/chat-resume.js +334 -0
- package/lib/handlers/chat-sync.js +320 -0
- package/lib/handlers/chat.js +320 -0
- package/lib/handlers/conversations.js +92 -0
- package/lib/handlers/preferences.js +88 -0
- package/lib/handlers/tools-list.js +58 -0
- package/lib/hitl-engine.d.ts +60 -0
- package/lib/hitl-engine.js +261 -0
- package/lib/http-utils.js +92 -0
- package/lib/index.d.ts +20 -0
- package/lib/index.js +141 -0
- package/lib/init.js +636 -0
- package/lib/manual-entry.js +59 -0
- package/lib/mcp-server.js +252 -0
- package/lib/output-groups.js +54 -0
- package/lib/postgres-store.d.ts +31 -0
- package/lib/postgres-store.js +465 -0
- package/lib/preference-store.d.ts +47 -0
- package/lib/preference-store.js +79 -0
- package/lib/prompt-store.d.ts +42 -0
- package/lib/prompt-store.js +60 -0
- package/lib/rate-limiter.d.ts +30 -0
- package/lib/rate-limiter.js +104 -0
- package/lib/react-engine.d.ts +110 -0
- package/lib/react-engine.js +337 -0
- package/lib/runner/cli.js +156 -0
- package/lib/runner/cost-estimator.js +71 -0
- package/lib/runner/gate.js +46 -0
- package/lib/runner/index.js +165 -0
- package/lib/sidecar.d.ts +83 -0
- package/lib/sidecar.js +161 -0
- package/lib/sse.d.ts +15 -0
- package/lib/sse.js +30 -0
- package/lib/tools-scanner.js +91 -0
- package/lib/tui.js +253 -0
- package/lib/verifier-report.js +78 -0
- package/lib/verifier-runner.js +338 -0
- package/lib/verifier-scanner.js +70 -0
- package/lib/verifier-worker-pool.js +196 -0
- package/lib/views/chat.js +340 -0
- package/lib/views/endpoints.js +203 -0
- package/lib/views/eval-run.js +206 -0
- package/lib/views/forge-agent.js +538 -0
- package/lib/views/forge.js +410 -0
- package/lib/views/main-menu.js +275 -0
- package/lib/views/mediation.js +381 -0
- package/lib/views/model-compare.js +430 -0
- package/lib/views/model-comparison.js +333 -0
- package/lib/views/onboarding.js +470 -0
- package/lib/views/performance.js +237 -0
- package/lib/views/run-evals.js +205 -0
- package/lib/views/settings.js +829 -0
- package/lib/views/tools-evals.js +514 -0
- package/lib/views/verifier-coverage.js +617 -0
- package/lib/workers/verifier-worker.js +52 -0
- package/package.json +123 -0
- package/widget/forge-chat.js +789 -0
package/lib/sse.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ServerResponse } from 'http';
|
|
2
|
+
|
|
3
|
+
export interface SSEHandle {
|
|
4
|
+
/** Write a named SSE event with a JSON-serialized data payload. */
|
|
5
|
+
send(event: string, data: unknown): void;
|
|
6
|
+
/** End the response stream. */
|
|
7
|
+
close(): void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Initialize an HTTP response for SSE streaming.
|
|
12
|
+
* Sets Content-Type, Cache-Control, and Connection headers, then returns a
|
|
13
|
+
* minimal send/close handle.
|
|
14
|
+
*/
|
|
15
|
+
export function initSSE(res: ServerResponse): SSEHandle;
|
package/lib/sse.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE (Server-Sent Events) streaming helper.
|
|
3
|
+
*
|
|
4
|
+
* Initializes an HTTP response for SSE and returns a simple send/close interface.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Initialize an HTTP response for SSE streaming.
|
|
9
|
+
* @param {import('http').ServerResponse} res
|
|
10
|
+
* @returns {{ send(event: string, data: object): void, close(): void }}
|
|
11
|
+
*/
|
|
12
|
+
export function initSSE(res) {
|
|
13
|
+
res.writeHead(200, {
|
|
14
|
+
'Content-Type': 'text/event-stream',
|
|
15
|
+
'Cache-Control': 'no-cache',
|
|
16
|
+
'Connection': 'keep-alive',
|
|
17
|
+
'X-Accel-Buffering': 'no'
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
send(event, data) {
|
|
22
|
+
// Sanitize event name — must not contain newlines or colons
|
|
23
|
+
const safeEvent = String(event).replace(/[\r\n:]/g, '_');
|
|
24
|
+
res.write(`event: ${safeEvent}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
25
|
+
},
|
|
26
|
+
close() {
|
|
27
|
+
res.end();
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools Scanner — Discovers existing tools from barrel and tool files.
|
|
3
|
+
* Used to diff: APIs without tools = available for MCP/tool creation.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
7
|
+
import { resolve, join } from 'path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Extract tool names from barrel file (exports).
|
|
11
|
+
* @param {string} barrelsPath
|
|
12
|
+
* @returns {string[]}
|
|
13
|
+
*/
|
|
14
|
+
function scanBarrel(barrelsPath) {
|
|
15
|
+
const abs = resolve(process.cwd(), barrelsPath);
|
|
16
|
+
if (!existsSync(abs)) return [];
|
|
17
|
+
const content = readFileSync(abs, 'utf-8');
|
|
18
|
+
const names = [];
|
|
19
|
+
const lines = content.split('\n').filter((l) => !l.trim().startsWith('//'));
|
|
20
|
+
const re = /export\s+\{\s*(\w+Tool)\s*\}\s+from\s+['"]\.\/([^'"]+)['"]/g;
|
|
21
|
+
let m;
|
|
22
|
+
const text = lines.join('\n');
|
|
23
|
+
while ((m = re.exec(text))) {
|
|
24
|
+
const exportName = m[1];
|
|
25
|
+
const fileBase = m[2].replace(/\.tool\.(ts|js)$/, '').replace(/-/g, '_');
|
|
26
|
+
const snake = exportName.replace(/Tool$/, '').replace(/([A-Z])/g, (c) => '_' + c.toLowerCase()).replace(/^_/, '');
|
|
27
|
+
names.push(snake || fileBase);
|
|
28
|
+
}
|
|
29
|
+
return names;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Scan tool files for `name: 'snake_case'` pattern.
|
|
34
|
+
* @param {string} toolsDir
|
|
35
|
+
* @returns {string[]}
|
|
36
|
+
*/
|
|
37
|
+
function scanToolFiles(toolsDir) {
|
|
38
|
+
const abs = resolve(process.cwd(), toolsDir);
|
|
39
|
+
if (!existsSync(abs)) return [];
|
|
40
|
+
const names = [];
|
|
41
|
+
const files = readdirSync(abs).filter((f) => f.endsWith('.tool.ts') || f.endsWith('.tool.js'));
|
|
42
|
+
for (const file of files) {
|
|
43
|
+
const content = readFileSync(join(abs, file), 'utf-8');
|
|
44
|
+
const m = /name:\s*['"]([^'"]+)['"]/.exec(content);
|
|
45
|
+
if (m) names.push(m[1]);
|
|
46
|
+
}
|
|
47
|
+
return names;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get existing tool names. Prefer file scan (has canonical name); fallback to barrel.
|
|
52
|
+
* @param {object} config - forge.config project section
|
|
53
|
+
* @returns {string[]}
|
|
54
|
+
*/
|
|
55
|
+
export function getExistingTools(config) {
|
|
56
|
+
const tools = [];
|
|
57
|
+
if (config?.toolsDir) {
|
|
58
|
+
const fromFiles = scanToolFiles(config.toolsDir);
|
|
59
|
+
tools.push(...fromFiles);
|
|
60
|
+
}
|
|
61
|
+
if (config?.barrelsFile && tools.length === 0) {
|
|
62
|
+
const fromBarrel = scanBarrel(config.barrelsFile);
|
|
63
|
+
tools.push(...fromBarrel);
|
|
64
|
+
}
|
|
65
|
+
return [...new Set(tools)];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get tools with metadata (name, description, tags) for verifier gap analysis.
|
|
70
|
+
* @param {object} config - forge.config project section
|
|
71
|
+
* @returns {{ name: string; description?: string; tags?: string[] }[]}
|
|
72
|
+
*/
|
|
73
|
+
export function getToolsWithMetadata(config) {
|
|
74
|
+
const abs = resolve(process.cwd(), config?.toolsDir || 'src/tools');
|
|
75
|
+
if (!existsSync(abs)) return [];
|
|
76
|
+
const files = readdirSync(abs).filter(
|
|
77
|
+
(f) => f.endsWith('.tool.ts') || f.endsWith('.tool.js')
|
|
78
|
+
);
|
|
79
|
+
const tools = [];
|
|
80
|
+
for (const file of files) {
|
|
81
|
+
const content = readFileSync(join(abs, file), 'utf-8');
|
|
82
|
+
const nameM = content.match(/name:\s*['"]([^'"]+)['"]/);
|
|
83
|
+
const descM = content.match(/description:\s*(?:['"`]([^'"`]{1,300})['"`]|[\s\S]*?\+[\s\S]*?['"`]([^'"`]{1,200})['"`])/);
|
|
84
|
+
const tagsM = content.match(/tags:\s*\[([^\]]+)\]/);
|
|
85
|
+
const name = nameM ? nameM[1] : file.replace(/\.tool\.(ts|js)$/, '').replace(/-/g, '_');
|
|
86
|
+
const description = descM ? (descM[1] || descM[2] || '').slice(0, 200) : undefined;
|
|
87
|
+
const tags = tagsM ? tagsM[1].split(',').map((t) => t.trim().replace(/['"]/g, '')) : undefined;
|
|
88
|
+
tools.push({ name, description, tags });
|
|
89
|
+
}
|
|
90
|
+
return tools;
|
|
91
|
+
}
|
package/lib/tui.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI — Full-screen blessed interface for Tool-Forge.
|
|
3
|
+
*
|
|
4
|
+
* Layout:
|
|
5
|
+
* row 0: header (blue bg, logo + service status)
|
|
6
|
+
* row 1: ─── divider ───
|
|
7
|
+
* rows 2…n-3: content area (views rendered here)
|
|
8
|
+
* row n-2: ─── divider ───
|
|
9
|
+
* row n-1: footer (key hints, updated per view)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync, unlinkSync } from 'fs';
|
|
13
|
+
import { resolve, dirname } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const PROJECT_ROOT = resolve(__dirname, '..');
|
|
18
|
+
const LOCK_FILE = resolve(PROJECT_ROOT, '.forge-service.lock');
|
|
19
|
+
|
|
20
|
+
function readLock() {
|
|
21
|
+
if (!existsSync(LOCK_FILE)) return null;
|
|
22
|
+
try { return JSON.parse(readFileSync(LOCK_FILE, 'utf-8')); } catch (_) { return null; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function runTui(config) {
|
|
26
|
+
let blessed;
|
|
27
|
+
try {
|
|
28
|
+
blessed = (await import('blessed')).default;
|
|
29
|
+
} catch {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'The "blessed" package is required for the TUI but is not installed. ' +
|
|
32
|
+
'Install it with: npm install blessed'
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const screen = blessed.screen({ smartCSR: true, title: 'Tool Forge', fullUnicode: true });
|
|
37
|
+
|
|
38
|
+
// ── Chrome ────────────────────────────────────────────────────────────────
|
|
39
|
+
const header = blessed.box({
|
|
40
|
+
top: 0, left: 0, width: '100%', height: 1,
|
|
41
|
+
tags: true, style: { bg: 'blue', fg: 'white', bold: true }
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const headerRule = blessed.box({
|
|
45
|
+
top: 1, left: 0, width: '100%', height: 1,
|
|
46
|
+
tags: true, content: '{cyan-fg}' + '─'.repeat(400) + '{/cyan-fg}'
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const footerRule = blessed.box({
|
|
50
|
+
bottom: 1, left: 0, width: '100%', height: 1,
|
|
51
|
+
tags: true, content: '{cyan-fg}' + '─'.repeat(400) + '{/cyan-fg}'
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const footer = blessed.box({
|
|
55
|
+
bottom: 0, left: 0, width: '100%', height: 1,
|
|
56
|
+
tags: true,
|
|
57
|
+
content: ' {cyan-fg}↑↓{/cyan-fg} navigate {cyan-fg}Enter{/cyan-fg} select {cyan-fg}b{/cyan-fg} back {cyan-fg}r{/cyan-fg} refresh {cyan-fg}q{/cyan-fg} quit',
|
|
58
|
+
style: { bg: 'black', fg: 'white' }
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Content sits between the two rules (rows 2 … n-3).
|
|
62
|
+
const content = blessed.box({
|
|
63
|
+
top: 2, left: 0, width: '100%', height: screen.rows - 4, tags: true
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
screen.append(header);
|
|
67
|
+
screen.append(headerRule);
|
|
68
|
+
screen.append(content);
|
|
69
|
+
screen.append(footerRule);
|
|
70
|
+
screen.append(footer);
|
|
71
|
+
|
|
72
|
+
// Chrome elements that should never be destroyed during view transitions.
|
|
73
|
+
const chromeElements = new Set([header, headerRule, content, footerRule, footer]);
|
|
74
|
+
|
|
75
|
+
// ── View management ───────────────────────────────────────────────────────
|
|
76
|
+
let currentViewName = 'main-menu';
|
|
77
|
+
let currentView = null;
|
|
78
|
+
const moduleCache = {}; // avoids re-awaiting the same ESM module import
|
|
79
|
+
let popupDepth = 0; // incremented by openPopup(), decremented by closePopup()
|
|
80
|
+
|
|
81
|
+
// View-specific screen key bindings: cleared each time we navigate away.
|
|
82
|
+
const viewKeys = []; // [{ keys, fn }, ...]
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Register a screen-level key that belongs to the current view.
|
|
86
|
+
* It will be automatically unregistered when the view is navigated away from.
|
|
87
|
+
*/
|
|
88
|
+
function screenKey(keys, fn) {
|
|
89
|
+
viewKeys.push({ keys, fn });
|
|
90
|
+
screen.key(keys, fn);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Call before showing any popup overlay. Prevents global 'b'/'escape' from navigating away. */
|
|
94
|
+
function openPopup() { popupDepth++; }
|
|
95
|
+
|
|
96
|
+
/** Call when a popup overlay is closed/destroyed. */
|
|
97
|
+
function closePopup() { popupDepth = Math.max(0, popupDepth - 1); }
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Start the forge service if it is not already running.
|
|
101
|
+
* Spawns forge-service.js detached. The header poll picks up the new lock within 3s.
|
|
102
|
+
*/
|
|
103
|
+
async function startService() {
|
|
104
|
+
const lock = readLock();
|
|
105
|
+
if (lock) {
|
|
106
|
+
// Stale lock — remove before spawning
|
|
107
|
+
try { unlinkSync(LOCK_FILE); } catch (_) { /* ignore */ }
|
|
108
|
+
}
|
|
109
|
+
const { spawn } = await import('child_process');
|
|
110
|
+
const child = spawn('node', [resolve(__dirname, 'forge-service.js')], {
|
|
111
|
+
detached: true,
|
|
112
|
+
stdio: 'ignore'
|
|
113
|
+
});
|
|
114
|
+
child.unref();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function loadModule(name) {
|
|
118
|
+
if (!moduleCache[name]) {
|
|
119
|
+
moduleCache[name] = await import(`./views/${name}.js`);
|
|
120
|
+
}
|
|
121
|
+
return moduleCache[name];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Auto-refresh timer ────────────────────────────────────────────────────
|
|
125
|
+
const VIEW_REFRESH_MS = 100;
|
|
126
|
+
let refreshTimer = null;
|
|
127
|
+
let refreshGeneration = 0; // bumped on navigation to cancel in-flight refreshes
|
|
128
|
+
|
|
129
|
+
function scheduleViewRefresh() {
|
|
130
|
+
clearTimeout(refreshTimer);
|
|
131
|
+
const gen = refreshGeneration;
|
|
132
|
+
refreshTimer = setTimeout(async () => {
|
|
133
|
+
if (gen !== refreshGeneration) return; // navigation happened — abort
|
|
134
|
+
if (popupDepth === 0 && currentView?.refresh) {
|
|
135
|
+
try { await currentView.refresh(); } catch (_) { /* swallow */ }
|
|
136
|
+
}
|
|
137
|
+
if (gen !== refreshGeneration) return; // navigation during refresh — don't reschedule
|
|
138
|
+
scheduleViewRefresh();
|
|
139
|
+
}, VIEW_REFRESH_MS);
|
|
140
|
+
refreshTimer.unref?.();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function showView(name) {
|
|
144
|
+
// 0. Stop the auto-refresh timer and invalidate any in-flight refresh.
|
|
145
|
+
clearTimeout(refreshTimer);
|
|
146
|
+
refreshGeneration++;
|
|
147
|
+
|
|
148
|
+
// 1. Unregister all view-local key bindings from the outgoing view.
|
|
149
|
+
for (const { keys, fn } of viewKeys) {
|
|
150
|
+
screen.unkey(keys, fn);
|
|
151
|
+
}
|
|
152
|
+
viewKeys.length = 0;
|
|
153
|
+
|
|
154
|
+
// 2. Reset popup depth — any orphaned popups from the outgoing view are gone.
|
|
155
|
+
popupDepth = 0;
|
|
156
|
+
|
|
157
|
+
// 3. Remove the outgoing view's DOM tree from the content node.
|
|
158
|
+
if (currentView) {
|
|
159
|
+
content.remove(currentView);
|
|
160
|
+
currentView = null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 3b. Destroy any orphaned overlays (popups parented directly to screen).
|
|
164
|
+
for (const child of [...screen.children]) {
|
|
165
|
+
if (!chromeElements.has(child)) {
|
|
166
|
+
try { child.destroy(); } catch (_) { /* ignore */ }
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 3c. Force full screen buffer reallocation so smartCSR doesn't leave
|
|
171
|
+
// stale characters from the outgoing view (e.g. noticeBar text).
|
|
172
|
+
screen.realloc();
|
|
173
|
+
|
|
174
|
+
currentViewName = name;
|
|
175
|
+
|
|
176
|
+
// 4. Create a fresh view instance.
|
|
177
|
+
const mod = await loadModule(name);
|
|
178
|
+
const viewBox = mod.createView({
|
|
179
|
+
screen, content, config, navigate, setFooter, screenKey,
|
|
180
|
+
openPopup, closePopup, startService
|
|
181
|
+
});
|
|
182
|
+
currentView = viewBox;
|
|
183
|
+
content.append(viewBox);
|
|
184
|
+
|
|
185
|
+
// 5. Trigger the view's initial data load if it has one.
|
|
186
|
+
if (typeof viewBox.refresh === 'function') {
|
|
187
|
+
await viewBox.refresh();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
screen.render();
|
|
191
|
+
|
|
192
|
+
// 6. Start auto-refresh cycle for this view.
|
|
193
|
+
scheduleViewRefresh();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function navigate(viewName) {
|
|
197
|
+
showView(viewName).catch((err) => setFooter(` {red-fg}⚠ ${err.message}{/red-fg}`));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function setFooter(text) {
|
|
201
|
+
footer.setContent(text);
|
|
202
|
+
screen.render();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── Header polling ────────────────────────────────────────────────────────
|
|
206
|
+
const pkg = JSON.parse(readFileSync(resolve(PROJECT_ROOT, 'package.json'), 'utf-8'));
|
|
207
|
+
const version = pkg.version || '0.2.0';
|
|
208
|
+
|
|
209
|
+
async function updateHeader() {
|
|
210
|
+
const modelName = config?.models?.generation || config?.model || 'claude-sonnet-4-6';
|
|
211
|
+
header.setContent(
|
|
212
|
+
` {bold}{white-fg}▸▸ TOOL FORGE{/white-fg}{/bold}` +
|
|
213
|
+
` {#888888-fg}build · test · verify{/#888888-fg}` +
|
|
214
|
+
`{|} {cyan-fg}${modelName}{/cyan-fg} {blue-fg}v${version}{/blue-fg} `
|
|
215
|
+
);
|
|
216
|
+
screen.render();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
updateHeader();
|
|
220
|
+
const headerTimer = setInterval(updateHeader, 30_000); // refresh every 30s for model changes
|
|
221
|
+
headerTimer.unref?.();
|
|
222
|
+
|
|
223
|
+
// ── Global key bindings (persist for the entire session) ──────────────────
|
|
224
|
+
screen.key(['q', 'C-c'], () => {
|
|
225
|
+
cleanup();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
screen.key(['b', 'escape'], () => {
|
|
229
|
+
if (popupDepth > 0) return; // a popup is open — let it handle the key
|
|
230
|
+
if (currentViewName !== 'main-menu') navigate('main-menu');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
screen.key('r', () => {
|
|
234
|
+
if (popupDepth > 0) return;
|
|
235
|
+
currentView?.refresh?.();
|
|
236
|
+
screen.render();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
function cleanup() {
|
|
240
|
+
clearTimeout(refreshTimer);
|
|
241
|
+
clearInterval(headerTimer);
|
|
242
|
+
try { screen.destroy(); } catch (_) {}
|
|
243
|
+
process.exit(0);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
screen.on('resize', () => {
|
|
247
|
+
content.height = screen.rows - 4;
|
|
248
|
+
screen.render();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
await showView(config._startOnOnboarding ? 'onboarding' : 'main-menu');
|
|
252
|
+
screen.render();
|
|
253
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifier Report — Gap detection: tools without verifier coverage.
|
|
3
|
+
* Consumed by the Verifier Coverage TUI view.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getToolsWithMetadata } from './tools-scanner.js';
|
|
7
|
+
import { getExistingVerifiers } from './verifier-scanner.js';
|
|
8
|
+
import { inferOutputGroups, getVerifiersForGroups } from './output-groups.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build and print verifier coverage report.
|
|
12
|
+
* @param {object} config - Full forge config
|
|
13
|
+
*/
|
|
14
|
+
export function runVerifierReport(config) {
|
|
15
|
+
const verification = config?.verification;
|
|
16
|
+
const project = config?.project;
|
|
17
|
+
|
|
18
|
+
if (!verification?.enabled) {
|
|
19
|
+
console.log('Verification is disabled in forge.config.json. Set verification.enabled: true');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const tools = getToolsWithMetadata(project);
|
|
24
|
+
const verifiers = getExistingVerifiers(verification);
|
|
25
|
+
|
|
26
|
+
console.log('\n--- Verifier Coverage Report ---\n');
|
|
27
|
+
console.log(`Tools: ${tools.length}`);
|
|
28
|
+
console.log(`Verifiers: ${verifiers.length}\n`);
|
|
29
|
+
|
|
30
|
+
if (tools.length === 0) {
|
|
31
|
+
console.log('No tools found. Add tools first.');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const rows = [];
|
|
36
|
+
const toolsWithoutCoverage = [];
|
|
37
|
+
const suggestedVerifiers = new Set();
|
|
38
|
+
|
|
39
|
+
for (const tool of tools) {
|
|
40
|
+
const groups = inferOutputGroups(tool);
|
|
41
|
+
const covering = getVerifiersForGroups(groups).filter((v) => verifiers.includes(v));
|
|
42
|
+
const suggested = getVerifiersForGroups(groups).filter((v) => !verifiers.includes(v));
|
|
43
|
+
suggested.forEach((v) => suggestedVerifiers.add(v));
|
|
44
|
+
|
|
45
|
+
const coverage = covering.length > 0 ? covering.join(', ') : '—';
|
|
46
|
+
rows.push({ tool: tool.name, groups: groups.join(', '), coverage });
|
|
47
|
+
|
|
48
|
+
if (covering.length === 0) {
|
|
49
|
+
toolsWithoutCoverage.push({ ...tool, groups, suggested });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log('| Tool | Output Groups | Verifier Coverage |');
|
|
54
|
+
console.log('|-------------------|-----------------|-------------------|');
|
|
55
|
+
for (const r of rows) {
|
|
56
|
+
const tool = r.tool.padEnd(17).slice(0, 17);
|
|
57
|
+
const groups = r.groups.padEnd(15).slice(0, 15);
|
|
58
|
+
const cov = r.coverage.padEnd(17).slice(0, 17);
|
|
59
|
+
console.log(`| ${tool} | ${groups} | ${cov} |`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (toolsWithoutCoverage.length > 0) {
|
|
63
|
+
console.log('\nTools without verifier coverage:');
|
|
64
|
+
for (const t of toolsWithoutCoverage) {
|
|
65
|
+
console.log(` • ${t.name} — output groups: ${t.groups.join(', ')}`);
|
|
66
|
+
}
|
|
67
|
+
if (suggestedVerifiers.size > 0) {
|
|
68
|
+
console.log('\nSuggested verifiers:');
|
|
69
|
+
for (const v of suggestedVerifiers) {
|
|
70
|
+
console.log(` • ${v}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
console.log('\nRun /forge-verifier in Claude to create verifiers.');
|
|
74
|
+
} else {
|
|
75
|
+
console.log('\nAll tools have verifier coverage.');
|
|
76
|
+
}
|
|
77
|
+
console.log('');
|
|
78
|
+
}
|