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
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HITL Engine — pause/resume for confirmable tool calls.
|
|
3
|
+
*
|
|
4
|
+
* Sensitivity levels:
|
|
5
|
+
* autonomous — never pause
|
|
6
|
+
* cautious — pause if tool spec has requiresConfirmation flag
|
|
7
|
+
* standard — pause for POST/PUT/PATCH/DELETE methods
|
|
8
|
+
* paranoid — always pause
|
|
9
|
+
*
|
|
10
|
+
* Storage backends:
|
|
11
|
+
* memory — in-process Map (default, single-instance only)
|
|
12
|
+
* sqlite — hitl_pending table (single-instance, survives restart)
|
|
13
|
+
* redis — forge:hitl:{token} keys with TTL (multi-instance, recommended for production)
|
|
14
|
+
*
|
|
15
|
+
* Paused state has a TTL — expired states cannot be resumed.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { randomUUID } from 'crypto';
|
|
19
|
+
|
|
20
|
+
const DEFAULT_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
21
|
+
const REDIS_KEY_PREFIX = 'forge:hitl:';
|
|
22
|
+
|
|
23
|
+
export class HitlEngine {
|
|
24
|
+
/**
|
|
25
|
+
* @param {object} opts
|
|
26
|
+
* @param {import('better-sqlite3').Database} [opts.db] — SQLite backend
|
|
27
|
+
* @param {object} [opts.redis] — Redis client instance (ioredis or node-redis compatible)
|
|
28
|
+
* @param {import('pg').Pool} [opts.pgPool] — Postgres pool instance
|
|
29
|
+
* @param {number} [opts.ttlMs] — pause state TTL (default 5 min)
|
|
30
|
+
*/
|
|
31
|
+
constructor(opts = {}) {
|
|
32
|
+
this._db = opts.db ?? null;
|
|
33
|
+
this._redis = opts.redis ?? null;
|
|
34
|
+
this._pgPool = opts.pgPool ?? null;
|
|
35
|
+
this._ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
36
|
+
|
|
37
|
+
// In-memory store as fallback when no DB, no Redis, and no Postgres
|
|
38
|
+
this._memStore = new Map();
|
|
39
|
+
|
|
40
|
+
// Periodic cleanup of expired in-memory entries (every 60s)
|
|
41
|
+
if (!this._db && !this._redis && !this._pgPool) {
|
|
42
|
+
this._cleanupTimer = setInterval(() => {
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
for (const [key, entry] of this._memStore) {
|
|
45
|
+
if (now > entry.expiresAt) this._memStore.delete(key);
|
|
46
|
+
}
|
|
47
|
+
}, 60_000);
|
|
48
|
+
this._cleanupTimer.unref();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Ensure hitl_pending table exists if using SQLite (and not Redis/Postgres)
|
|
52
|
+
if (this._db && !this._redis && !this._pgPool) {
|
|
53
|
+
this._db.exec(`
|
|
54
|
+
CREATE TABLE IF NOT EXISTS hitl_pending (
|
|
55
|
+
resume_token TEXT PRIMARY KEY,
|
|
56
|
+
state_json TEXT NOT NULL,
|
|
57
|
+
expires_at TEXT NOT NULL,
|
|
58
|
+
created_at TEXT NOT NULL
|
|
59
|
+
)
|
|
60
|
+
`);
|
|
61
|
+
|
|
62
|
+
// SQLite path — cleanup expired rows every 5 minutes.
|
|
63
|
+
// DESIGN NOTE: 5-minute interval chosen deliberately. A shorter interval (e.g. 60s)
|
|
64
|
+
// risks write contention under high HITL volume; a longer one (e.g. 30min) lets
|
|
65
|
+
// stale rows accumulate more. 5min is a conservative middle ground.
|
|
66
|
+
// If you see write contention on hitl_pending at scale, reduce to 60s.
|
|
67
|
+
const db = this._db;
|
|
68
|
+
this._sqliteCleanupTimer = setInterval(() => {
|
|
69
|
+
try {
|
|
70
|
+
db.prepare('DELETE FROM hitl_pending WHERE expires_at < ?')
|
|
71
|
+
.run(new Date().toISOString());
|
|
72
|
+
} catch { /* cleanup failure is non-fatal */ }
|
|
73
|
+
}, 5 * 60_000);
|
|
74
|
+
this._sqliteCleanupTimer.unref();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Postgres table creation is deferred to first use (_ensurePgTable)
|
|
78
|
+
this._pgTableReady = false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** @private */
|
|
82
|
+
async _ensurePgTable() {
|
|
83
|
+
if (this._pgTableReady) return;
|
|
84
|
+
await this._pgPool.query(`
|
|
85
|
+
CREATE TABLE IF NOT EXISTS hitl_pending (
|
|
86
|
+
resume_token TEXT PRIMARY KEY,
|
|
87
|
+
state_json TEXT NOT NULL,
|
|
88
|
+
expires_at TEXT NOT NULL,
|
|
89
|
+
created_at TEXT NOT NULL
|
|
90
|
+
)
|
|
91
|
+
`);
|
|
92
|
+
this._pgTableReady = true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Determine whether a tool call should pause for confirmation.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} hitlLevel — user's HITL sensitivity level
|
|
99
|
+
* @param {object} toolSpec — { name, method?, requiresConfirmation? }
|
|
100
|
+
* @returns {boolean}
|
|
101
|
+
*/
|
|
102
|
+
shouldPause(hitlLevel, toolSpec = {}) {
|
|
103
|
+
switch (hitlLevel) {
|
|
104
|
+
case 'autonomous':
|
|
105
|
+
return false;
|
|
106
|
+
case 'cautious':
|
|
107
|
+
return !!toolSpec.requiresConfirmation;
|
|
108
|
+
case 'standard': {
|
|
109
|
+
const method = (toolSpec.method || 'GET').toUpperCase();
|
|
110
|
+
return ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method);
|
|
111
|
+
}
|
|
112
|
+
case 'paranoid':
|
|
113
|
+
return true;
|
|
114
|
+
default:
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Store paused state and return a resume token.
|
|
121
|
+
*
|
|
122
|
+
* @param {object} state — arbitrary state to store (conversation, pending tool calls, etc.)
|
|
123
|
+
* @returns {Promise<string>} resumeToken
|
|
124
|
+
*/
|
|
125
|
+
async pause(state) {
|
|
126
|
+
const resumeToken = randomUUID();
|
|
127
|
+
const stateJson = JSON.stringify(state);
|
|
128
|
+
|
|
129
|
+
if (this._redis) {
|
|
130
|
+
const ttlSeconds = Math.ceil(this._ttlMs / 1000);
|
|
131
|
+
await this._redis.set(
|
|
132
|
+
REDIS_KEY_PREFIX + resumeToken,
|
|
133
|
+
stateJson,
|
|
134
|
+
'EX',
|
|
135
|
+
ttlSeconds
|
|
136
|
+
);
|
|
137
|
+
return resumeToken;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (this._pgPool) {
|
|
141
|
+
await this._ensurePgTable();
|
|
142
|
+
const expiresAt = new Date(Date.now() + this._ttlMs).toISOString();
|
|
143
|
+
await this._pgPool.query(
|
|
144
|
+
`INSERT INTO hitl_pending (resume_token, state_json, expires_at, created_at)
|
|
145
|
+
VALUES ($1, $2, $3, $4)`,
|
|
146
|
+
[resumeToken, stateJson, expiresAt, new Date().toISOString()]
|
|
147
|
+
);
|
|
148
|
+
return resumeToken;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (this._db) {
|
|
152
|
+
const expiresAt = new Date(Date.now() + this._ttlMs).toISOString();
|
|
153
|
+
this._db.prepare(`
|
|
154
|
+
INSERT INTO hitl_pending (resume_token, state_json, expires_at, created_at)
|
|
155
|
+
VALUES (?, ?, ?, ?)
|
|
156
|
+
`).run(resumeToken, stateJson, expiresAt, new Date().toISOString());
|
|
157
|
+
return resumeToken;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// In-memory fallback
|
|
161
|
+
this._memStore.set(resumeToken, { state, expiresAt: Date.now() + this._ttlMs });
|
|
162
|
+
return resumeToken;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Resume a paused state. Returns the state if valid, null if expired or not found.
|
|
167
|
+
* Deletes the state on successful resume (one-time use).
|
|
168
|
+
*
|
|
169
|
+
* @param {string} resumeToken
|
|
170
|
+
* @returns {Promise<object|null>}
|
|
171
|
+
*/
|
|
172
|
+
async resume(resumeToken) {
|
|
173
|
+
if (this._redis) {
|
|
174
|
+
const key = REDIS_KEY_PREFIX + resumeToken;
|
|
175
|
+
// Atomic get-and-delete: use a pipeline or multi/exec
|
|
176
|
+
// For simplicity, GET then DEL — race window is acceptable for HITL
|
|
177
|
+
const stateJson = await this._redis.get(key);
|
|
178
|
+
if (!stateJson) return null;
|
|
179
|
+
await this._redis.del(key);
|
|
180
|
+
try {
|
|
181
|
+
return JSON.parse(stateJson);
|
|
182
|
+
} catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (this._pgPool) {
|
|
188
|
+
await this._ensurePgTable();
|
|
189
|
+
const { rows } = await this._pgPool.query(
|
|
190
|
+
'SELECT * FROM hitl_pending WHERE resume_token = $1',
|
|
191
|
+
[resumeToken]
|
|
192
|
+
);
|
|
193
|
+
if (rows.length === 0) return null;
|
|
194
|
+
|
|
195
|
+
const row = rows[0];
|
|
196
|
+
// Delete regardless (one-time use)
|
|
197
|
+
await this._pgPool.query(
|
|
198
|
+
'DELETE FROM hitl_pending WHERE resume_token = $1',
|
|
199
|
+
[resumeToken]
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// Check expiry
|
|
203
|
+
if (new Date(row.expires_at) < new Date()) return null;
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
return JSON.parse(row.state_json);
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (this._db) {
|
|
213
|
+
const row = this._db.prepare(
|
|
214
|
+
'SELECT * FROM hitl_pending WHERE resume_token = ?'
|
|
215
|
+
).get(resumeToken);
|
|
216
|
+
|
|
217
|
+
if (!row) return null;
|
|
218
|
+
|
|
219
|
+
// Delete regardless (one-time use)
|
|
220
|
+
this._db.prepare('DELETE FROM hitl_pending WHERE resume_token = ?').run(resumeToken);
|
|
221
|
+
|
|
222
|
+
// Check expiry
|
|
223
|
+
if (new Date(row.expires_at) < new Date()) return null;
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
return JSON.parse(row.state_json);
|
|
227
|
+
} catch {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// In-memory fallback
|
|
233
|
+
const entry = this._memStore.get(resumeToken);
|
|
234
|
+
if (!entry) return null;
|
|
235
|
+
this._memStore.delete(resumeToken);
|
|
236
|
+
if (Date.now() > entry.expiresAt) return null;
|
|
237
|
+
return entry.state;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Factory — selects storage backend from config.
|
|
243
|
+
* Priority: Redis → Postgres → SQLite → in-memory.
|
|
244
|
+
*
|
|
245
|
+
* @param {object} config — forge config
|
|
246
|
+
* @param {import('better-sqlite3').Database} [db]
|
|
247
|
+
* @param {object} [redis] — pre-created Redis client instance
|
|
248
|
+
* @param {import('pg').Pool} [pgPool] — pre-created Postgres pool instance
|
|
249
|
+
* @returns {HitlEngine}
|
|
250
|
+
*/
|
|
251
|
+
export function makeHitlEngine(config, db, redis, pgPool) {
|
|
252
|
+
const ttlMs = config?.hitl?.ttlMs ?? config?.ttlMs ?? undefined;
|
|
253
|
+
const ttlOpt = ttlMs !== undefined ? { ttlMs } : {};
|
|
254
|
+
if (redis) {
|
|
255
|
+
return new HitlEngine({ redis, ...ttlOpt });
|
|
256
|
+
}
|
|
257
|
+
if (pgPool) {
|
|
258
|
+
return new HitlEngine({ pgPool, ...ttlOpt });
|
|
259
|
+
}
|
|
260
|
+
return new HitlEngine({ db, ...ttlOpt });
|
|
261
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared HTTP helpers for sidecar request handlers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getAllToolRegistry } from './db.js';
|
|
6
|
+
|
|
7
|
+
const MAX_BODY_SIZE = 1_048_576; // 1 MB
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Read and JSON-parse a request body. Returns {} on parse failure.
|
|
11
|
+
* Rejects bodies larger than MAX_BODY_SIZE.
|
|
12
|
+
* @param {import('http').IncomingMessage} req
|
|
13
|
+
* @returns {Promise<object>}
|
|
14
|
+
*/
|
|
15
|
+
export function readBody(req) {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
let data = '';
|
|
18
|
+
let size = 0;
|
|
19
|
+
req.on('data', (chunk) => {
|
|
20
|
+
size += chunk.length;
|
|
21
|
+
if (size > MAX_BODY_SIZE) {
|
|
22
|
+
req.destroy();
|
|
23
|
+
reject(new Error('Request body too large'));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
data += chunk;
|
|
27
|
+
});
|
|
28
|
+
req.on('end', () => {
|
|
29
|
+
try { resolve(data ? JSON.parse(data) : {}); } catch { resolve({}); }
|
|
30
|
+
});
|
|
31
|
+
req.on('error', () => resolve({}));
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Send a JSON response with the given status code.
|
|
37
|
+
* @param {import('http').ServerResponse} res
|
|
38
|
+
* @param {number} statusCode
|
|
39
|
+
* @param {object} body
|
|
40
|
+
*/
|
|
41
|
+
export function sendJson(res, statusCode, body) {
|
|
42
|
+
const payload = JSON.stringify(body);
|
|
43
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
|
|
44
|
+
res.end(payload);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extract a JWT from an HTTP request.
|
|
49
|
+
* Checks Authorization: Bearer <token> first, then falls back to ?token= query param.
|
|
50
|
+
* @param {import('http').IncomingMessage} req
|
|
51
|
+
* @returns {string|null}
|
|
52
|
+
*/
|
|
53
|
+
export function extractJwt(req) {
|
|
54
|
+
const auth = req.headers.authorization ?? '';
|
|
55
|
+
if (auth.startsWith('Bearer ')) return auth.slice(7) || null;
|
|
56
|
+
try {
|
|
57
|
+
return new URL(req.url, 'http://localhost').searchParams.get('token') || null;
|
|
58
|
+
} catch { return null; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Load promoted tools from the tool registry and convert to LLM-format tool defs.
|
|
63
|
+
* @param {import('better-sqlite3').Database} db
|
|
64
|
+
* @param {string|string[]} [allowlist='*'] — '*' for all, or array of tool_names to include
|
|
65
|
+
* @returns {{ toolRows: object[], tools: object[] }}
|
|
66
|
+
*/
|
|
67
|
+
export function loadPromotedTools(db, allowlist = '*') {
|
|
68
|
+
let toolRows = getAllToolRegistry(db).filter(r => r.lifecycle_state === 'promoted');
|
|
69
|
+
if (Array.isArray(allowlist)) {
|
|
70
|
+
const allowSet = new Set(allowlist);
|
|
71
|
+
toolRows = toolRows.filter(r => allowSet.has(r.tool_name));
|
|
72
|
+
}
|
|
73
|
+
const tools = [];
|
|
74
|
+
for (const row of toolRows) {
|
|
75
|
+
try {
|
|
76
|
+
const spec = JSON.parse(row.spec_json);
|
|
77
|
+
const schema = spec.schema || {};
|
|
78
|
+
const properties = {};
|
|
79
|
+
const required = [];
|
|
80
|
+
for (const [k, v] of Object.entries(schema)) {
|
|
81
|
+
properties[k] = { type: v.type || 'string', description: v.description || k };
|
|
82
|
+
if (!v.optional) required.push(k);
|
|
83
|
+
}
|
|
84
|
+
tools.push({
|
|
85
|
+
name: spec.name || row.tool_name,
|
|
86
|
+
description: spec.description || '',
|
|
87
|
+
inputSchema: { type: 'object', properties, required }
|
|
88
|
+
});
|
|
89
|
+
} catch { /* skip malformed specs */ }
|
|
90
|
+
}
|
|
91
|
+
return { toolRows, tools };
|
|
92
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Barrel re-export of all public types for `import type { ... } from 'agent-tool-forge'`
|
|
2
|
+
export * from './sidecar.js';
|
|
3
|
+
export type { AuthResult, AuthConfig, Authenticator, AdminAuthResult } from './auth.js';
|
|
4
|
+
export type { ConversationMessage, SessionSummary, ConversationStore } from './conversation-store.js';
|
|
5
|
+
export type {
|
|
6
|
+
ReactEvent, ReactLoopParams, ReactEventType,
|
|
7
|
+
TextEvent, TextDeltaEvent, ToolCallEvent, ToolResultEvent,
|
|
8
|
+
ToolWarningEvent, HitlEvent, ErrorEvent, DoneEvent
|
|
9
|
+
} from './react-engine.js';
|
|
10
|
+
export type {
|
|
11
|
+
SidecarConfig, AgentConfig, RateLimitConfig, VerificationConfig,
|
|
12
|
+
ConversationConfig, DatabaseConfig, AuthConfig as SidecarAuthConfig
|
|
13
|
+
} from './config-schema.js';
|
|
14
|
+
export type { HitlEngine, HitlEngineOptions, HitlLevel, HitlToolSpec } from './hitl-engine.js';
|
|
15
|
+
export type { PromptStore, PromptVersion } from './prompt-store.js';
|
|
16
|
+
export type { PreferenceStore, UserPreferences, EffectiveSettings } from './preference-store.js';
|
|
17
|
+
export type { RateLimiter, RateLimitResult } from './rate-limiter.js';
|
|
18
|
+
export type { PostgresStore } from './postgres-store.js';
|
|
19
|
+
export type { Db } from './db.js';
|
|
20
|
+
export type { SSEHandle } from './sse.js';
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forge CLI — Entry point.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node lib/index.js # Full-screen TUI
|
|
7
|
+
* node lib/index.js --manual # Skip to manual endpoint entry (fallback)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync, existsSync, writeFileSync } from 'fs';
|
|
11
|
+
import { resolve, dirname } from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
import { runTui } from './tui.js';
|
|
14
|
+
import { addEndpointManually } from './manual-entry.js';
|
|
15
|
+
import * as readline from 'readline';
|
|
16
|
+
|
|
17
|
+
const CONFIG_FILE = 'forge.config.json';
|
|
18
|
+
const PENDING_SPEC_FILE = 'forge-pending-tool.json';
|
|
19
|
+
|
|
20
|
+
function findProjectRoot() {
|
|
21
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function loadConfig() {
|
|
25
|
+
const projectRoot = findProjectRoot();
|
|
26
|
+
const configPath = resolve(projectRoot, CONFIG_FILE);
|
|
27
|
+
if (!existsSync(configPath)) {
|
|
28
|
+
console.error(`No ${CONFIG_FILE} found in ${projectRoot}.\nRun "forge init" to set up your project, or create one from config/forge.config.template.json`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(raw);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error(`${CONFIG_FILE} contains invalid JSON: ${err.message}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns true if the user needs to go through onboarding:
|
|
42
|
+
* - No API key in .env AND no API key in environment variables AND no key in config
|
|
43
|
+
*/
|
|
44
|
+
function needsOnboarding(config) {
|
|
45
|
+
const projectRoot = findProjectRoot();
|
|
46
|
+
const envPath = resolve(projectRoot, '.env');
|
|
47
|
+
|
|
48
|
+
// Check process env first
|
|
49
|
+
if (process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY ||
|
|
50
|
+
process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY) return false;
|
|
51
|
+
|
|
52
|
+
// Check .env file
|
|
53
|
+
if (existsSync(envPath)) {
|
|
54
|
+
try {
|
|
55
|
+
const envText = readFileSync(envPath, 'utf-8');
|
|
56
|
+
if (/ANTHROPIC_API_KEY\s*=\s*\S/.test(envText)) return false;
|
|
57
|
+
if (/OPENAI_API_KEY\s*=\s*\S/.test(envText)) return false;
|
|
58
|
+
if (/GOOGLE_API_KEY\s*=\s*\S/.test(envText)) return false;
|
|
59
|
+
if (/GEMINI_API_KEY\s*=\s*\S/.test(envText)) return false;
|
|
60
|
+
} catch (_) { /* ignore */ }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check config object for configured credentials
|
|
64
|
+
if (config?.auth?.signingKey && typeof config.auth.signingKey === 'string' &&
|
|
65
|
+
config.auth.signingKey.trim() && !config.auth.signingKey.startsWith('${')) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
if (config?.adminKey && typeof config.adminKey === 'string' &&
|
|
69
|
+
config.adminKey.trim() && !config.adminKey.startsWith('${')) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return true; // No key found anywhere
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function main() {
|
|
77
|
+
process.chdir(findProjectRoot());
|
|
78
|
+
|
|
79
|
+
const args = process.argv.slice(2);
|
|
80
|
+
|
|
81
|
+
if (args[0] === 'run') {
|
|
82
|
+
const { runCli } = await import('./runner/cli.js');
|
|
83
|
+
await runCli(args.slice(1));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (args[0] === 'init') {
|
|
88
|
+
const { runInit } = await import('./init.js');
|
|
89
|
+
await runInit();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const manualOnly = args.includes('--manual') || args.includes('-m');
|
|
94
|
+
|
|
95
|
+
if (manualOnly) {
|
|
96
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
97
|
+
let endpoint;
|
|
98
|
+
try {
|
|
99
|
+
endpoint = await addEndpointManually(rl);
|
|
100
|
+
} finally {
|
|
101
|
+
rl.close();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const projectRoot = findProjectRoot();
|
|
105
|
+
let config;
|
|
106
|
+
if (existsSync(resolve(projectRoot, CONFIG_FILE))) {
|
|
107
|
+
try {
|
|
108
|
+
config = JSON.parse(readFileSync(resolve(projectRoot, CONFIG_FILE), 'utf-8'));
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.error('Error reading forge.config.json:', err.message);
|
|
111
|
+
config = {};
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
config = { project: {} };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const pendingSpec = {
|
|
118
|
+
_source: 'forge-api-tui',
|
|
119
|
+
_createdAt: new Date().toISOString(),
|
|
120
|
+
endpoint,
|
|
121
|
+
project: config.project || {}
|
|
122
|
+
};
|
|
123
|
+
writeFileSync(resolve(projectRoot, PENDING_SPEC_FILE), JSON.stringify(pendingSpec, null, 2), 'utf-8');
|
|
124
|
+
console.log(`\nWrote ${PENDING_SPEC_FILE}. Run /forge-tool in Claude.\n`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const config = loadConfig();
|
|
129
|
+
|
|
130
|
+
// Check if onboarding is needed, and if so, pass the flag to the TUI
|
|
131
|
+
if (needsOnboarding(config)) {
|
|
132
|
+
config._startOnOnboarding = true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await runTui(config);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
main().catch((err) => {
|
|
139
|
+
console.error(err);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
});
|