@jigyasudham/veto 1.1.0 → 1.2.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/dist/memory/local.js +223 -7
- package/dist/memory/schema.js +32 -0
- package/dist/server.js +297 -15
- package/package.json +1 -1
package/dist/memory/local.js
CHANGED
|
@@ -6,6 +6,12 @@ import { join } from 'node:path';
|
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { mkdirSync } from 'node:fs';
|
|
8
8
|
import { CREATE_TABLES } from './schema.js';
|
|
9
|
+
// Context window sizes per platform (tokens)
|
|
10
|
+
export const CONTEXT_WINDOWS = {
|
|
11
|
+
claude: 200_000,
|
|
12
|
+
gemini: 1_000_000,
|
|
13
|
+
codex: 128_000,
|
|
14
|
+
};
|
|
9
15
|
const VETO_DIR = join(homedir(), '.veto');
|
|
10
16
|
const DB_PATH = join(VETO_DIR, 'veto.db');
|
|
11
17
|
let _db = null;
|
|
@@ -22,7 +28,7 @@ export function getDb() {
|
|
|
22
28
|
migrateSessionColumns(_db);
|
|
23
29
|
return _db;
|
|
24
30
|
}
|
|
25
|
-
// Adds active_client and
|
|
31
|
+
// Adds active_client, last_resumed_at, and connection_type columns if they don't exist
|
|
26
32
|
function migrateSessionColumns(db) {
|
|
27
33
|
const cols = db.prepare('PRAGMA table_info(sessions)').all();
|
|
28
34
|
const names = new Set(cols.map(c => c.name));
|
|
@@ -30,6 +36,8 @@ function migrateSessionColumns(db) {
|
|
|
30
36
|
db.exec('ALTER TABLE sessions ADD COLUMN active_client TEXT');
|
|
31
37
|
if (!names.has('last_resumed_at'))
|
|
32
38
|
db.exec('ALTER TABLE sessions ADD COLUMN last_resumed_at TEXT');
|
|
39
|
+
if (!names.has('connection_type'))
|
|
40
|
+
db.exec("ALTER TABLE sessions ADD COLUMN connection_type TEXT NOT NULL DEFAULT 'subscription'");
|
|
33
41
|
}
|
|
34
42
|
// Adds legal and security columns if they don't exist (Phase 3 → Phase 3.1 migration)
|
|
35
43
|
function migrateCouncilColumns(db) {
|
|
@@ -61,12 +69,26 @@ export function saveSession(input) {
|
|
|
61
69
|
const db = getDb();
|
|
62
70
|
const id = randomUUID();
|
|
63
71
|
const now = new Date().toISOString();
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
const platform = input.platform ?? 'claude';
|
|
73
|
+
const connection_type = input.connection_type ?? 'subscription';
|
|
74
|
+
const token_count = input.token_count ?? 0;
|
|
75
|
+
db.prepare(`
|
|
76
|
+
INSERT INTO sessions (id, started_at, platform, connection_type, project_dir, summary, context, task_state, token_count)
|
|
77
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
78
|
+
`).run(id, now, platform, connection_type, input.project_dir ?? null, input.summary ?? null, input.context ? JSON.stringify(input.context) : null, input.task_state ? JSON.stringify(input.task_state) : null, token_count);
|
|
79
|
+
// Record usage event
|
|
80
|
+
db.prepare(`
|
|
81
|
+
INSERT INTO usage_events (id, session_id, platform, connection_type, tokens, event_type, recorded_at)
|
|
82
|
+
VALUES (?, ?, ?, ?, ?, 'session_save', ?)
|
|
83
|
+
`).run(randomUUID(), id, platform, connection_type, token_count, now);
|
|
84
|
+
// Context window guard
|
|
85
|
+
const window_size = CONTEXT_WINDOWS[platform] ?? 200_000;
|
|
86
|
+
const usage_pct = Math.round((token_count / window_size) * 100);
|
|
87
|
+
const context_warning = usage_pct >= 80;
|
|
88
|
+
const continuation_prompt = context_warning
|
|
89
|
+
? `Session ${id} saved. Context at ${usage_pct}% of ${platform} limit. To continue in a fresh session: call veto_continue { "session_id": "${id}" }`
|
|
90
|
+
: null;
|
|
91
|
+
return { session_id: id, saved_at: now, context_warning, usage_pct, continuation_prompt };
|
|
70
92
|
}
|
|
71
93
|
export function restoreSession(session_id, active_client) {
|
|
72
94
|
const db = getDb();
|
|
@@ -196,4 +218,198 @@ export function getPatterns(prefix, limit = 20) {
|
|
|
196
218
|
}
|
|
197
219
|
return db.prepare('SELECT * FROM patterns ORDER BY confidence DESC, seen_count DESC LIMIT ?').all(limit);
|
|
198
220
|
}
|
|
221
|
+
export function getContextStatus(session_id) {
|
|
222
|
+
const db = getDb();
|
|
223
|
+
const row = db.prepare('SELECT id, platform, token_count FROM sessions WHERE id = ?').get(session_id);
|
|
224
|
+
if (!row)
|
|
225
|
+
return null;
|
|
226
|
+
const context_window = CONTEXT_WINDOWS[row.platform] ?? 200_000;
|
|
227
|
+
const usage_pct = Math.round((row.token_count / context_window) * 100);
|
|
228
|
+
const recommended_action = usage_pct >= 80 ? 'handoff' : usage_pct >= 60 ? 'compress' : 'safe';
|
|
229
|
+
return { session_id: row.id, platform: row.platform, token_count: row.token_count, context_window, usage_pct, recommended_action };
|
|
230
|
+
}
|
|
231
|
+
// ─── Docs Cache ───────────────────────────────────────────────────────────────
|
|
232
|
+
const DOCS_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
233
|
+
export async function fetchAndCacheDocs(package_name, ecosystem, version, max_chars = 8000) {
|
|
234
|
+
const db = getDb();
|
|
235
|
+
// Check cache first
|
|
236
|
+
const cached = db.prepare('SELECT * FROM docs_cache WHERE package_name = ? AND ecosystem = ? ORDER BY fetched_at DESC LIMIT 1').get(package_name, ecosystem);
|
|
237
|
+
if (cached) {
|
|
238
|
+
const age = Date.now() - new Date(cached.fetched_at).getTime();
|
|
239
|
+
if (age < DOCS_TTL_MS) {
|
|
240
|
+
return { package_name, version: cached.version, ecosystem, content: cached.content.slice(0, max_chars), cached: true, fetched_at: cached.fetched_at };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Fetch from source with 5s timeout
|
|
244
|
+
const controller = new AbortController();
|
|
245
|
+
const timer = setTimeout(() => controller.abort(), 5000);
|
|
246
|
+
try {
|
|
247
|
+
let content = '';
|
|
248
|
+
let resolved_version = version ?? 'latest';
|
|
249
|
+
if (ecosystem === 'npm') {
|
|
250
|
+
const meta = await fetch(`https://registry.npmjs.org/${encodeURIComponent(package_name)}`, { signal: controller.signal });
|
|
251
|
+
if (!meta.ok)
|
|
252
|
+
return null;
|
|
253
|
+
const json = await meta.json();
|
|
254
|
+
resolved_version = version ?? json['dist-tags']?.['latest'] ?? 'latest';
|
|
255
|
+
const readme = json['readme'] ?? '';
|
|
256
|
+
const description = json['description'] ?? '';
|
|
257
|
+
content = `# ${package_name}@${resolved_version}\n${description}\n\n${readme}`;
|
|
258
|
+
}
|
|
259
|
+
else if (ecosystem === 'pypi') {
|
|
260
|
+
const res = await fetch(`https://pypi.org/pypi/${encodeURIComponent(package_name)}/json`, { signal: controller.signal });
|
|
261
|
+
if (!res.ok)
|
|
262
|
+
return null;
|
|
263
|
+
const json = await res.json();
|
|
264
|
+
resolved_version = version ?? json.info.version;
|
|
265
|
+
content = `# ${package_name}@${resolved_version}\n${json.info.summary}\n\n${json.info.description}`;
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
const res = await fetch(`https://crates.io/api/v1/crates/${encodeURIComponent(package_name)}`, {
|
|
269
|
+
signal: controller.signal,
|
|
270
|
+
headers: { 'User-Agent': 'veto-mcp/1.1.0' },
|
|
271
|
+
});
|
|
272
|
+
if (!res.ok)
|
|
273
|
+
return null;
|
|
274
|
+
const json = await res.json();
|
|
275
|
+
resolved_version = version ?? json.crate.newest_version;
|
|
276
|
+
content = `# ${package_name}@${resolved_version}\n${json.crate.description}\nDocs: ${json.crate.documentation}`;
|
|
277
|
+
}
|
|
278
|
+
const now = new Date().toISOString();
|
|
279
|
+
db.prepare(`
|
|
280
|
+
INSERT OR REPLACE INTO docs_cache (id, package_name, ecosystem, version, content, fetched_at)
|
|
281
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
282
|
+
`).run(randomUUID(), package_name, ecosystem, resolved_version, content, now);
|
|
283
|
+
return { package_name, version: resolved_version, ecosystem, content: content.slice(0, max_chars), cached: false, fetched_at: now };
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
clearTimeout(timer);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// ─── Task Plans ───────────────────────────────────────────────────────────────
|
|
293
|
+
export function saveTaskPlan(plan_json, description_hash, project_dir) {
|
|
294
|
+
const db = getDb();
|
|
295
|
+
const id = randomUUID();
|
|
296
|
+
db.prepare(`
|
|
297
|
+
INSERT INTO task_plans (id, description_hash, plan_json, project_dir, created_at)
|
|
298
|
+
VALUES (?, ?, ?, ?, datetime('now'))
|
|
299
|
+
`).run(id, description_hash, plan_json, project_dir ?? null);
|
|
300
|
+
return id;
|
|
301
|
+
}
|
|
302
|
+
export function getUsageStatus() {
|
|
303
|
+
const db = getDb();
|
|
304
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
305
|
+
// Today's requests from rate_usage
|
|
306
|
+
const rateRows = db.prepare('SELECT platform, request_count FROM rate_usage WHERE date_key = ?').all(today);
|
|
307
|
+
// Today's tokens + connection_type breakdown from usage_events
|
|
308
|
+
const eventRows = db.prepare(`
|
|
309
|
+
SELECT platform, connection_type, SUM(tokens) as total_tokens, COUNT(*) as cnt
|
|
310
|
+
FROM usage_events
|
|
311
|
+
WHERE DATE(recorded_at) = ?
|
|
312
|
+
GROUP BY platform, connection_type
|
|
313
|
+
`).all(today);
|
|
314
|
+
const platforms = ['claude', 'gemini', 'codex'];
|
|
315
|
+
const by_platform = platforms.map(p => {
|
|
316
|
+
const rate = rateRows.find(r => r.platform === p);
|
|
317
|
+
const subEvents = eventRows.filter(e => e.platform === p && e.connection_type === 'subscription');
|
|
318
|
+
const apiEvents = eventRows.filter(e => e.platform === p && e.connection_type === 'api');
|
|
319
|
+
const tokens = eventRows.filter(e => e.platform === p).reduce((s, e) => s + (e.total_tokens ?? 0), 0);
|
|
320
|
+
return {
|
|
321
|
+
platform: p,
|
|
322
|
+
requests: rate?.request_count ?? 0,
|
|
323
|
+
tokens_reported: tokens,
|
|
324
|
+
connection_breakdown: {
|
|
325
|
+
subscription: subEvents.reduce((s, e) => s + e.cnt, 0),
|
|
326
|
+
api: apiEvents.reduce((s, e) => s + e.cnt, 0),
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
});
|
|
330
|
+
// Last 7 days history
|
|
331
|
+
const history = db.prepare(`
|
|
332
|
+
SELECT DATE(recorded_at) as date, COUNT(*) as total_requests, SUM(tokens) as total_tokens
|
|
333
|
+
FROM usage_events
|
|
334
|
+
WHERE recorded_at >= date('now', '-7 days')
|
|
335
|
+
GROUP BY DATE(recorded_at)
|
|
336
|
+
ORDER BY date DESC
|
|
337
|
+
`).all();
|
|
338
|
+
// Warnings
|
|
339
|
+
const warnings = [];
|
|
340
|
+
for (const p of by_platform) {
|
|
341
|
+
if (p.requests > 0) {
|
|
342
|
+
const dailyLimit = p.platform === 'gemini' ? 1500 : 500;
|
|
343
|
+
const pct = Math.round((p.requests / dailyLimit) * 100);
|
|
344
|
+
if (pct >= 80)
|
|
345
|
+
warnings.push(`${p.platform} at ${pct}% of estimated daily request limit`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return { today: { by_platform }, history, warnings };
|
|
349
|
+
}
|
|
350
|
+
export function getAuditLog(opts = {}) {
|
|
351
|
+
const db = getDb();
|
|
352
|
+
const limit = Math.min(opts.limit ?? 20, 100);
|
|
353
|
+
const events = [];
|
|
354
|
+
// Council outcomes
|
|
355
|
+
const councilWhere = ['1=1'];
|
|
356
|
+
const councilParams = [];
|
|
357
|
+
if (opts.session_id) {
|
|
358
|
+
councilWhere.push('session_id = ?');
|
|
359
|
+
councilParams.push(opts.session_id);
|
|
360
|
+
}
|
|
361
|
+
if (opts.verdict) {
|
|
362
|
+
councilWhere.push('verdict = ?');
|
|
363
|
+
councilParams.push(opts.verdict);
|
|
364
|
+
}
|
|
365
|
+
if (opts.since) {
|
|
366
|
+
councilWhere.push('debated_at >= ?');
|
|
367
|
+
councilParams.push(opts.since);
|
|
368
|
+
}
|
|
369
|
+
const councilRows = db.prepare(`SELECT id, session_id, task, verdict, recommended, debated_at FROM council_outcomes WHERE ${councilWhere.join(' AND ')} ORDER BY debated_at DESC LIMIT ?`).all(...councilParams, limit);
|
|
370
|
+
for (const r of councilRows) {
|
|
371
|
+
if (opts.agent)
|
|
372
|
+
continue; // council events don't have a single agent
|
|
373
|
+
events.push({ timestamp: r.debated_at, event_type: 'council', session_id: r.session_id, agent: null, verdict: r.verdict, summary: r.task.slice(0, 100), affected_files: null });
|
|
374
|
+
}
|
|
375
|
+
// Decisions
|
|
376
|
+
const decWhere = ['1=1'];
|
|
377
|
+
const decParams = [];
|
|
378
|
+
if (opts.session_id) {
|
|
379
|
+
decWhere.push('session_id = ?');
|
|
380
|
+
decParams.push(opts.session_id);
|
|
381
|
+
}
|
|
382
|
+
if (opts.since) {
|
|
383
|
+
decWhere.push('made_at >= ?');
|
|
384
|
+
decParams.push(opts.since);
|
|
385
|
+
}
|
|
386
|
+
const decRows = db.prepare(`SELECT id, session_id, decision, rationale, council_verdict, files_affected, made_at FROM decisions WHERE ${decWhere.join(' AND ')} ORDER BY made_at DESC LIMIT ?`).all(...decParams, limit);
|
|
387
|
+
for (const r of decRows) {
|
|
388
|
+
events.push({ timestamp: r.made_at, event_type: 'decision', session_id: r.session_id, agent: null, verdict: r.council_verdict, summary: r.decision.slice(0, 100), affected_files: r.files_affected });
|
|
389
|
+
}
|
|
390
|
+
// Sort combined and return limit
|
|
391
|
+
return events.sort((a, b) => b.timestamp.localeCompare(a.timestamp)).slice(0, limit);
|
|
392
|
+
}
|
|
393
|
+
export function getHealthStats() {
|
|
394
|
+
const db = getDb();
|
|
395
|
+
const count = (table) => db.prepare(`SELECT COUNT(*) as n FROM ${table}`).get().n;
|
|
396
|
+
// Avg latency: approximate from 10 most recent council outcomes (debated_at timestamps as proxy)
|
|
397
|
+
const latencyRows = db.prepare('SELECT debated_at FROM council_outcomes ORDER BY debated_at DESC LIMIT 10').all();
|
|
398
|
+
let avg_council_latency_ms = null;
|
|
399
|
+
if (latencyRows.length >= 2) {
|
|
400
|
+
const diffs = [];
|
|
401
|
+
for (let i = 0; i < latencyRows.length - 1; i++) {
|
|
402
|
+
diffs.push(Math.abs(new Date(latencyRows[i].debated_at).getTime() - new Date(latencyRows[i + 1].debated_at).getTime()));
|
|
403
|
+
}
|
|
404
|
+
avg_council_latency_ms = Math.round(diffs.reduce((a, b) => a + b, 0) / diffs.length);
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
total_sessions: count('sessions'),
|
|
408
|
+
total_memories: count('knowledge_base'),
|
|
409
|
+
total_patterns: count('patterns'),
|
|
410
|
+
total_council_outcomes: count('council_outcomes'),
|
|
411
|
+
total_decisions: count('decisions'),
|
|
412
|
+
avg_council_latency_ms,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
199
415
|
//# sourceMappingURL=local.js.map
|
package/dist/memory/schema.js
CHANGED
|
@@ -8,6 +8,7 @@ export const CREATE_TABLES = `
|
|
|
8
8
|
platform TEXT NOT NULL DEFAULT 'claude',
|
|
9
9
|
active_client TEXT,
|
|
10
10
|
last_resumed_at TEXT,
|
|
11
|
+
connection_type TEXT NOT NULL DEFAULT 'subscription',
|
|
11
12
|
project_dir TEXT,
|
|
12
13
|
summary TEXT,
|
|
13
14
|
context TEXT,
|
|
@@ -16,6 +17,34 @@ export const CREATE_TABLES = `
|
|
|
16
17
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
17
18
|
);
|
|
18
19
|
|
|
20
|
+
CREATE TABLE IF NOT EXISTS docs_cache (
|
|
21
|
+
id TEXT PRIMARY KEY,
|
|
22
|
+
package_name TEXT NOT NULL,
|
|
23
|
+
ecosystem TEXT NOT NULL,
|
|
24
|
+
version TEXT NOT NULL,
|
|
25
|
+
content TEXT NOT NULL,
|
|
26
|
+
fetched_at TEXT NOT NULL,
|
|
27
|
+
UNIQUE(package_name, ecosystem, version)
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE TABLE IF NOT EXISTS task_plans (
|
|
31
|
+
id TEXT PRIMARY KEY,
|
|
32
|
+
description_hash TEXT NOT NULL,
|
|
33
|
+
plan_json TEXT NOT NULL,
|
|
34
|
+
project_dir TEXT,
|
|
35
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
CREATE TABLE IF NOT EXISTS usage_events (
|
|
39
|
+
id TEXT PRIMARY KEY,
|
|
40
|
+
session_id TEXT,
|
|
41
|
+
platform TEXT NOT NULL,
|
|
42
|
+
connection_type TEXT NOT NULL DEFAULT 'subscription',
|
|
43
|
+
tokens INTEGER DEFAULT 0,
|
|
44
|
+
event_type TEXT NOT NULL DEFAULT 'session_save',
|
|
45
|
+
recorded_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
46
|
+
);
|
|
47
|
+
|
|
19
48
|
CREATE TABLE IF NOT EXISTS decisions (
|
|
20
49
|
id TEXT PRIMARY KEY,
|
|
21
50
|
session_id TEXT NOT NULL,
|
|
@@ -114,5 +143,8 @@ export const CREATE_TABLES = `
|
|
|
114
143
|
CREATE INDEX IF NOT EXISTS idx_knowledge_type ON knowledge_base(type);
|
|
115
144
|
CREATE INDEX IF NOT EXISTS idx_knowledge_project ON knowledge_base(project_dir);
|
|
116
145
|
CREATE INDEX IF NOT EXISTS idx_project_map_dir ON project_map(project_dir);
|
|
146
|
+
CREATE INDEX IF NOT EXISTS idx_docs_cache_pkg ON docs_cache(package_name, ecosystem);
|
|
147
|
+
CREATE INDEX IF NOT EXISTS idx_usage_events_session ON usage_events(session_id);
|
|
148
|
+
CREATE INDEX IF NOT EXISTS idx_usage_events_date ON usage_events(recorded_at);
|
|
117
149
|
`;
|
|
118
150
|
//# sourceMappingURL=schema.js.map
|
package/dist/server.js
CHANGED
|
@@ -7,7 +7,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
7
7
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
8
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
9
9
|
import { buildContextString } from './context/reader.js';
|
|
10
|
-
import { saveSession, restoreSession, listSessions, getDbPath, saveCouncilOutcome, storeKnowledge, searchKnowledge, deleteKnowledge, updateProjectMap, getProjectMap, upsertPattern, getPatterns, } from './memory/local.js';
|
|
10
|
+
import { saveSession, restoreSession, listSessions, getDbPath, saveCouncilOutcome, storeKnowledge, searchKnowledge, deleteKnowledge, updateProjectMap, getProjectMap, upsertPattern, getPatterns, getContextStatus, fetchAndCacheDocs, saveTaskPlan, getUsageStatus, getAuditLog, getHealthStats, CONTEXT_WINDOWS, } from './memory/local.js';
|
|
11
11
|
import { exportMemory, importMemory } from './memory/sync.js';
|
|
12
12
|
import { runDebate } from './council/index.js';
|
|
13
13
|
import { routeTask, getRateStatus, recordOutcome, getLearningStats, applyLearnedThresholds, getAgentPerformanceStats, getTaskTypeBreakdown, getCouncilInsights, getRecommendedAgent } from './router/index.js';
|
|
@@ -16,13 +16,18 @@ import { handoff, continueSession, getPlatformSetup } from './adapters/index.js'
|
|
|
16
16
|
import { startWatch, pollWatch, stopWatch } from './watcher/index.js';
|
|
17
17
|
import { runPipeline } from './workflow/pipeline.js';
|
|
18
18
|
import { loadPlugins, listPlugins } from './plugins/loader.js';
|
|
19
|
-
import { readFileSync } from 'node:fs';
|
|
19
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
20
20
|
import { extname, basename } from 'node:path';
|
|
21
|
-
|
|
21
|
+
import { createHash } from 'node:crypto';
|
|
22
|
+
const VERSION = '1.2.0';
|
|
22
23
|
// Tracks the project_dir of the most recently active session in this process.
|
|
23
24
|
// Used as a fallback when memory_store/memory_search are called without an explicit project_dir,
|
|
24
25
|
// so memories are automatically scoped to the current project.
|
|
25
26
|
let activeProjectDir = null;
|
|
27
|
+
// Server health tracking
|
|
28
|
+
const SERVER_START_TIME = Date.now();
|
|
29
|
+
let serverErrorCount = 0;
|
|
30
|
+
let lastServerError = null;
|
|
26
31
|
const server = new Server({ name: 'veto', version: VERSION }, {
|
|
27
32
|
capabilities: {
|
|
28
33
|
tools: {},
|
|
@@ -69,9 +74,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
69
74
|
type: 'string',
|
|
70
75
|
description: 'Absolute path to the current project directory.',
|
|
71
76
|
},
|
|
77
|
+
connection_type: {
|
|
78
|
+
type: 'string',
|
|
79
|
+
description: 'How you are connected to this AI — "subscription" (Claude Pro, Gemini Advanced) or "api" (API key). Used for usage tracking.',
|
|
80
|
+
enum: ['subscription', 'api'],
|
|
81
|
+
},
|
|
72
82
|
token_count: {
|
|
73
83
|
type: 'number',
|
|
74
|
-
description: 'Approximate tokens used this session.',
|
|
84
|
+
description: 'Approximate tokens used this session. Veto uses this for context window monitoring.',
|
|
75
85
|
},
|
|
76
86
|
},
|
|
77
87
|
required: ['summary', 'context'],
|
|
@@ -578,6 +588,93 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
578
588
|
required: [],
|
|
579
589
|
},
|
|
580
590
|
},
|
|
591
|
+
// ── Phase 13: Developer Intelligence ──────────────────────────────────────
|
|
592
|
+
{
|
|
593
|
+
name: 'veto_docs_fetch',
|
|
594
|
+
description: 'Fetches current, version-accurate documentation for any npm, PyPI, or crates.io package and returns it for injection into agent context. Eliminates hallucinated APIs. Results are cached for 24 hours.',
|
|
595
|
+
inputSchema: {
|
|
596
|
+
type: 'object',
|
|
597
|
+
properties: {
|
|
598
|
+
package_name: { type: 'string', description: 'Package name (e.g. "react", "requests", "serde").' },
|
|
599
|
+
ecosystem: { type: 'string', enum: ['npm', 'pypi', 'crates'], description: 'Package ecosystem.' },
|
|
600
|
+
version: { type: 'string', description: 'Specific version. Defaults to latest.' },
|
|
601
|
+
max_chars: { type: 'number', description: 'Max characters to return (default 8000). Higher = more complete docs, more tokens.' },
|
|
602
|
+
},
|
|
603
|
+
required: ['package_name', 'ecosystem'],
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
name: 'veto_context_status',
|
|
608
|
+
description: 'Returns the context window usage for a saved session — tokens used, % of platform limit consumed, and whether to compress or hand off before the window fills.',
|
|
609
|
+
inputSchema: {
|
|
610
|
+
type: 'object',
|
|
611
|
+
properties: {
|
|
612
|
+
session_id: { type: 'string', description: 'Session ID to check.' },
|
|
613
|
+
},
|
|
614
|
+
required: ['session_id'],
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
name: 'veto_task_parse',
|
|
619
|
+
description: 'Parses a plain-English project description or PRD into a structured task DAG with dependencies, complexity scores, priorities, and suggested agent assignments. Feeds directly into veto_workflow.',
|
|
620
|
+
inputSchema: {
|
|
621
|
+
type: 'object',
|
|
622
|
+
properties: {
|
|
623
|
+
description: { type: 'string', description: 'Project description, PRD, or feature brief to parse into tasks.' },
|
|
624
|
+
project_dir: { type: 'string', description: 'Optional project directory for codebase context injection.' },
|
|
625
|
+
max_tasks: { type: 'number', description: 'Maximum number of tasks to generate (default 20).' },
|
|
626
|
+
},
|
|
627
|
+
required: ['description'],
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
// ── Phase 14: Observability & Safety ──────────────────────────────────────
|
|
631
|
+
{
|
|
632
|
+
name: 'veto_usage_status',
|
|
633
|
+
description: 'Live AI usage dashboard. Shows tokens consumed today, requests per platform, subscription vs API usage split, 7-day history, and warnings when approaching limits.',
|
|
634
|
+
inputSchema: {
|
|
635
|
+
type: 'object',
|
|
636
|
+
properties: {},
|
|
637
|
+
required: [],
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
name: 'veto_audit_log',
|
|
642
|
+
description: 'Queryable log of every council verdict, decision, and session event. Filter by session, agent, verdict, or date. Essential for tracing what happened and why.',
|
|
643
|
+
inputSchema: {
|
|
644
|
+
type: 'object',
|
|
645
|
+
properties: {
|
|
646
|
+
session_id: { type: 'string', description: 'Filter to a specific session.' },
|
|
647
|
+
verdict: { type: 'string', description: 'Filter by council verdict (GREEN, YELLOW, RED).' },
|
|
648
|
+
since: { type: 'string', description: 'ISO date — only return events after this time.' },
|
|
649
|
+
limit: { type: 'number', description: 'Max events to return (default 20, max 100).' },
|
|
650
|
+
},
|
|
651
|
+
required: [],
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
name: 'veto_health',
|
|
656
|
+
description: 'Returns a live health snapshot of the Veto server — DB size, session/memory/pattern counts, uptime, error count, and average council latency.',
|
|
657
|
+
inputSchema: {
|
|
658
|
+
type: 'object',
|
|
659
|
+
properties: {},
|
|
660
|
+
required: [],
|
|
661
|
+
},
|
|
662
|
+
},
|
|
663
|
+
// ── Phase 15: CI/CD & Distribution ────────────────────────────────────────
|
|
664
|
+
{
|
|
665
|
+
name: 'veto_ci_gate',
|
|
666
|
+
description: 'CI/CD pipeline gate. Runs code review + security scan + secrets scan on a git diff and returns a structured pass/warn/fail verdict with exit code. Ready for GitHub Actions and GitLab CI.',
|
|
667
|
+
inputSchema: {
|
|
668
|
+
type: 'object',
|
|
669
|
+
properties: {
|
|
670
|
+
project_dir: { type: 'string', description: 'Absolute project path. Veto reads git diff HEAD automatically.' },
|
|
671
|
+
diff: { type: 'string', description: 'Optional: pass a diff string directly instead of reading from project_dir.' },
|
|
672
|
+
context: { type: 'string', description: 'Optional: PR description or ticket number for context.' },
|
|
673
|
+
fail_on: { type: 'string', enum: ['warn', 'fail'], description: 'Whether WARN counts as a failure (exit code 1). Default: "fail" — only FAIL exits non-zero.' },
|
|
674
|
+
},
|
|
675
|
+
required: ['project_dir'],
|
|
676
|
+
},
|
|
677
|
+
},
|
|
581
678
|
],
|
|
582
679
|
}));
|
|
583
680
|
// ─── Tool Handlers ────────────────────────────────────────────────────────────
|
|
@@ -612,21 +709,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
612
709
|
context: String(args?.context ?? ''),
|
|
613
710
|
task_state: args?.task_state ? String(args.task_state) : undefined,
|
|
614
711
|
platform: args?.platform ? String(args.platform) : 'claude',
|
|
712
|
+
connection_type: args?.connection_type ? String(args.connection_type) : 'subscription',
|
|
615
713
|
project_dir: sessionProjectDir,
|
|
616
714
|
token_count: typeof args?.token_count === 'number' ? args.token_count : 0,
|
|
617
715
|
});
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
},
|
|
628
|
-
],
|
|
716
|
+
const responseObj = {
|
|
717
|
+
success: true,
|
|
718
|
+
message: result.context_warning
|
|
719
|
+
? `⚠️ Context at ${result.usage_pct}% — consider handing off soon.`
|
|
720
|
+
: 'Session saved. Use this ID to restore on any AI platform.',
|
|
721
|
+
session_id: result.session_id,
|
|
722
|
+
saved_at: result.saved_at,
|
|
723
|
+
usage_pct: result.usage_pct,
|
|
724
|
+
context_warning: result.context_warning,
|
|
629
725
|
};
|
|
726
|
+
if (result.continuation_prompt)
|
|
727
|
+
responseObj.continuation_prompt = result.continuation_prompt;
|
|
728
|
+
return { content: [{ type: 'text', text: JSON.stringify(responseObj, null, 2) }] };
|
|
630
729
|
}
|
|
631
730
|
case 'veto_session_restore': {
|
|
632
731
|
const session_id = String(args?.session_id ?? '');
|
|
@@ -1242,6 +1341,189 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1242
1341
|
case 'veto_plugins': {
|
|
1243
1342
|
return { content: [{ type: 'text', text: JSON.stringify({ plugins: listPlugins(), plugin_dir: `${process.env.HOME ?? process.env.USERPROFILE}/.veto/agents/`, instructions: 'Drop a .js file exporting plan(task, context?) to register a custom agent.' }, null, 2) }] };
|
|
1244
1343
|
}
|
|
1344
|
+
// ── Phase 13: Developer Intelligence ──────────────────────────────────────
|
|
1345
|
+
case 'veto_docs_fetch': {
|
|
1346
|
+
const package_name = String(args?.package_name ?? '').trim();
|
|
1347
|
+
const ecosystem = String(args?.ecosystem ?? 'npm');
|
|
1348
|
+
const version = args?.version ? String(args.version) : undefined;
|
|
1349
|
+
const max_chars = typeof args?.max_chars === 'number' ? args.max_chars : 8000;
|
|
1350
|
+
if (!package_name) {
|
|
1351
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'package_name is required.' }) }], isError: true };
|
|
1352
|
+
}
|
|
1353
|
+
const result = await fetchAndCacheDocs(package_name, ecosystem, version, max_chars);
|
|
1354
|
+
if (!result) {
|
|
1355
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: `Could not fetch docs for ${package_name} (${ecosystem}). Source may be offline — try again.` }) }], isError: true };
|
|
1356
|
+
}
|
|
1357
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: true, ...result }, null, 2) }] };
|
|
1358
|
+
}
|
|
1359
|
+
case 'veto_context_status': {
|
|
1360
|
+
const session_id = String(args?.session_id ?? '');
|
|
1361
|
+
if (!session_id) {
|
|
1362
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'session_id is required.' }) }], isError: true };
|
|
1363
|
+
}
|
|
1364
|
+
const status = getContextStatus(session_id);
|
|
1365
|
+
if (!status) {
|
|
1366
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: `No session found: ${session_id}` }) }], isError: true };
|
|
1367
|
+
}
|
|
1368
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: true, ...status }, null, 2) }] };
|
|
1369
|
+
}
|
|
1370
|
+
case 'veto_task_parse': {
|
|
1371
|
+
const description = String(args?.description ?? '').trim();
|
|
1372
|
+
const project_dir = args?.project_dir ? String(args.project_dir) : undefined;
|
|
1373
|
+
const max_tasks = typeof args?.max_tasks === 'number' ? Math.min(args.max_tasks, 50) : 20;
|
|
1374
|
+
if (!description) {
|
|
1375
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'description is required.' }) }], isError: true };
|
|
1376
|
+
}
|
|
1377
|
+
const ctx = project_dir ? buildContextString(project_dir) : '';
|
|
1378
|
+
const planResult = await executeOne({ id: 'task-parse-1', agent: 'task-planner', task: `Parse this project description into a structured task breakdown with dependencies and complexity scores (max ${max_tasks} tasks):\n\n${description}`, context: ctx || undefined, project_dir });
|
|
1379
|
+
// Build structured task DAG from planner output
|
|
1380
|
+
const steps = planResult.plan?.steps ?? [];
|
|
1381
|
+
const tasks = steps.slice(0, max_tasks).map((step, i) => ({
|
|
1382
|
+
id: `task-${i + 1}`,
|
|
1383
|
+
title: step,
|
|
1384
|
+
complexity: Math.min(10, Math.max(1, Math.round((i / steps.length) * 10) + 3)),
|
|
1385
|
+
priority: i === 0 ? 'critical' : i < 3 ? 'high' : i < steps.length - 2 ? 'medium' : 'low',
|
|
1386
|
+
depends_on: i > 0 ? [`task-${i}`] : [],
|
|
1387
|
+
suggested_agent: 'coder',
|
|
1388
|
+
estimated_hours: 2,
|
|
1389
|
+
}));
|
|
1390
|
+
const plan = {
|
|
1391
|
+
summary: description.slice(0, 100),
|
|
1392
|
+
total_tasks: tasks.length,
|
|
1393
|
+
total_complexity: tasks.reduce((s, t) => s + t.complexity, 0),
|
|
1394
|
+
critical_path: tasks.map(t => t.id),
|
|
1395
|
+
parallelisable_groups: tasks.length > 2 ? [tasks.slice(1, Math.ceil(tasks.length / 2)).map(t => t.id)] : [],
|
|
1396
|
+
tasks,
|
|
1397
|
+
duration_estimate: planResult.plan?.duration_estimate ?? 'unknown',
|
|
1398
|
+
};
|
|
1399
|
+
const hash = createHash('sha256').update(description).digest('hex').slice(0, 16);
|
|
1400
|
+
const plan_id = saveTaskPlan(JSON.stringify(plan), hash, project_dir);
|
|
1401
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: true, plan_id, ...plan }, null, 2) }] };
|
|
1402
|
+
}
|
|
1403
|
+
// ── Phase 14: Observability & Safety ──────────────────────────────────────
|
|
1404
|
+
case 'veto_usage_status': {
|
|
1405
|
+
const status = getUsageStatus();
|
|
1406
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: true, ...status }, null, 2) }] };
|
|
1407
|
+
}
|
|
1408
|
+
case 'veto_audit_log': {
|
|
1409
|
+
const events = getAuditLog({
|
|
1410
|
+
session_id: args?.session_id ? String(args.session_id) : undefined,
|
|
1411
|
+
verdict: args?.verdict ? String(args.verdict) : undefined,
|
|
1412
|
+
since: args?.since ? String(args.since) : undefined,
|
|
1413
|
+
limit: typeof args?.limit === 'number' ? args.limit : 20,
|
|
1414
|
+
});
|
|
1415
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: true, count: events.length, events }, null, 2) }] };
|
|
1416
|
+
}
|
|
1417
|
+
case 'veto_health': {
|
|
1418
|
+
const stats = getHealthStats();
|
|
1419
|
+
let db_size_bytes = 0;
|
|
1420
|
+
try {
|
|
1421
|
+
db_size_bytes = statSync(getDbPath()).size;
|
|
1422
|
+
}
|
|
1423
|
+
catch { /* db may not exist */ }
|
|
1424
|
+
const db_size_human = db_size_bytes < 1024 ? `${db_size_bytes}B`
|
|
1425
|
+
: db_size_bytes < 1048576 ? `${(db_size_bytes / 1024).toFixed(1)}KB`
|
|
1426
|
+
: `${(db_size_bytes / 1048576).toFixed(1)}MB`;
|
|
1427
|
+
return {
|
|
1428
|
+
content: [{
|
|
1429
|
+
type: 'text',
|
|
1430
|
+
text: JSON.stringify({
|
|
1431
|
+
success: true,
|
|
1432
|
+
version: VERSION,
|
|
1433
|
+
status: serverErrorCount > 10 ? 'degraded' : 'healthy',
|
|
1434
|
+
uptime_seconds: Math.round((Date.now() - SERVER_START_TIME) / 1000),
|
|
1435
|
+
db_path: getDbPath(),
|
|
1436
|
+
db_size_bytes,
|
|
1437
|
+
db_size_human,
|
|
1438
|
+
error_count_since_start: serverErrorCount,
|
|
1439
|
+
last_error: lastServerError,
|
|
1440
|
+
context_windows: CONTEXT_WINDOWS,
|
|
1441
|
+
...stats,
|
|
1442
|
+
}, null, 2),
|
|
1443
|
+
}],
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
// ── Phase 15: CI/CD & Distribution ────────────────────────────────────────
|
|
1447
|
+
case 'veto_ci_gate': {
|
|
1448
|
+
const project_dir = String(args?.project_dir ?? '').trim();
|
|
1449
|
+
const diff_input = args?.diff ? String(args.diff) : undefined;
|
|
1450
|
+
const context = args?.context ? String(args.context) : undefined;
|
|
1451
|
+
const fail_on = args?.fail_on === 'warn' ? 'warn' : 'fail';
|
|
1452
|
+
if (!project_dir) {
|
|
1453
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'project_dir is required.' }) }], isError: true };
|
|
1454
|
+
}
|
|
1455
|
+
const start = Date.now();
|
|
1456
|
+
// Read diff if not provided
|
|
1457
|
+
let diff = diff_input;
|
|
1458
|
+
if (!diff) {
|
|
1459
|
+
const { execSync } = await import('node:child_process');
|
|
1460
|
+
try {
|
|
1461
|
+
diff = execSync('git diff HEAD', { cwd: project_dir, encoding: 'utf8', timeout: 15000 });
|
|
1462
|
+
}
|
|
1463
|
+
catch {
|
|
1464
|
+
diff = '';
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
if (!diff?.trim()) {
|
|
1468
|
+
return { content: [{ type: 'text', text: JSON.stringify({ verdict: 'pass', exit_code: 0, message: 'No changes detected.', duration_ms: Date.now() - start }) }] };
|
|
1469
|
+
}
|
|
1470
|
+
// Run all three checks in parallel
|
|
1471
|
+
const projectCtx = (() => { try {
|
|
1472
|
+
return buildContextString(project_dir);
|
|
1473
|
+
}
|
|
1474
|
+
catch {
|
|
1475
|
+
return '';
|
|
1476
|
+
} })();
|
|
1477
|
+
const fullContext = [context, projectCtx].filter(Boolean).join('\n\n');
|
|
1478
|
+
const [codeResult, secResult, secretsResult] = await Promise.all([
|
|
1479
|
+
executeOne({ id: 'ci-code', agent: 'reviewer', task: `CI code review of this diff:\n\n${diff}`, context: fullContext }),
|
|
1480
|
+
executeOne({ id: 'ci-sec', agent: 'security-scanner', task: `CI security scan of this diff:\n\n${diff}`, context: fullContext }),
|
|
1481
|
+
executeOne({ id: 'ci-secrets', agent: 'secrets', task: `CI secrets scan — check for exposed credentials in this diff:\n\n${diff}`, context: fullContext }),
|
|
1482
|
+
]);
|
|
1483
|
+
const codeConf = codeResult.output?.confidence ?? 0.8;
|
|
1484
|
+
const secConf = secResult.output?.confidence ?? 0.8;
|
|
1485
|
+
const secretsConf = secretsResult.output?.confidence ?? 1.0;
|
|
1486
|
+
// Determine verdict
|
|
1487
|
+
const hasCritical = codeConf < 0.4 || secConf < 0.4 || secretsConf < 0.5;
|
|
1488
|
+
const hasWarn = codeConf < 0.7 || secConf < 0.6;
|
|
1489
|
+
const rawVerdict = hasCritical ? 'fail' : hasWarn ? 'warn' : 'pass';
|
|
1490
|
+
const verdict = rawVerdict;
|
|
1491
|
+
const exit_code = rawVerdict === 'fail' || (rawVerdict === 'warn' && fail_on === 'warn') ? 1 : 0;
|
|
1492
|
+
const blocking_issues = [];
|
|
1493
|
+
if (codeConf < 0.7)
|
|
1494
|
+
blocking_issues.push(`Code review: ${codeResult.output?.recommendation ?? 'issues found'}`);
|
|
1495
|
+
if (secConf < 0.6)
|
|
1496
|
+
blocking_issues.push(`Security: ${secResult.output?.recommendation ?? 'vulnerabilities detected'}`);
|
|
1497
|
+
if (secretsConf < 0.5)
|
|
1498
|
+
blocking_issues.push(`Secrets: ${secretsResult.output?.recommendation ?? 'potential secrets exposed'}`);
|
|
1499
|
+
const icon = verdict === 'pass' ? '✅' : verdict === 'warn' ? '⚠️' : '❌';
|
|
1500
|
+
const ci_summary = [
|
|
1501
|
+
`${icon} **Veto CI Gate: ${verdict.toUpperCase()}**`,
|
|
1502
|
+
``,
|
|
1503
|
+
`| Check | Score | Status |`,
|
|
1504
|
+
`|---|---|---|`,
|
|
1505
|
+
`| Code Review | ${Math.round(codeConf * 100)}% | ${codeConf >= 0.7 ? '✅' : '❌'} |`,
|
|
1506
|
+
`| Security Scan | ${Math.round(secConf * 100)}% | ${secConf >= 0.6 ? '✅' : '❌'} |`,
|
|
1507
|
+
`| Secrets Scan | ${Math.round(secretsConf * 100)}% | ${secretsConf >= 0.5 ? '✅' : '❌'} |`,
|
|
1508
|
+
blocking_issues.length > 0 ? `\n**Blocking issues:**\n${blocking_issues.map(i => `- ${i}`).join('\n')}` : '',
|
|
1509
|
+
].filter(Boolean).join('\n');
|
|
1510
|
+
return {
|
|
1511
|
+
content: [{
|
|
1512
|
+
type: 'text',
|
|
1513
|
+
text: JSON.stringify({
|
|
1514
|
+
verdict, exit_code,
|
|
1515
|
+
checks: {
|
|
1516
|
+
code_review: { score: Math.round(codeConf * 100) },
|
|
1517
|
+
security: { score: Math.round(secConf * 100) },
|
|
1518
|
+
secrets: { score: Math.round(secretsConf * 100) },
|
|
1519
|
+
},
|
|
1520
|
+
blocking_issues,
|
|
1521
|
+
ci_summary,
|
|
1522
|
+
duration_ms: Date.now() - start,
|
|
1523
|
+
}, null, 2),
|
|
1524
|
+
}],
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1245
1527
|
default:
|
|
1246
1528
|
throw new Error(`Unknown tool: ${name}`);
|
|
1247
1529
|
}
|