@jigyasudham/veto 1.1.0 → 1.2.1

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/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # veto
2
2
 
3
- > **50 agents. 34 tools. 3 AIs. Self-learning. Zero extra cost.**
3
+ > **50 agents. 41 tools. 3 AIs. Self-learning. Zero extra cost.**
4
4
 
5
- An MCP server that runs locally on your machine, plugs into Claude Code, Codex CLI, and Gemini CLI using your existing subscriptions — giving every AI a council of specialist agents, persistent cross-platform memory, a self-learning router, reactive file watching, sequential agent pipelines, and the ability to say no to bad decisions.
5
+ An MCP server that runs locally on your machine, plugs into Claude Code, Codex CLI, and Gemini CLI using your existing subscriptions — giving every AI a council of specialist agents, persistent cross-platform memory, a self-learning router, live usage tracking, CI/CD pipeline gates, live documentation fetching, and the ability to say no to bad decisions.
6
6
 
7
7
  ---
8
8
 
@@ -116,7 +116,7 @@ VS Code uses `"servers"` with `"type": "stdio"`:
116
116
 
117
117
  ---
118
118
 
119
- ## MCP Tools (34)
119
+ ## MCP Tools (41)
120
120
 
121
121
  | Category | Tools |
122
122
  |---|---|
@@ -130,6 +130,9 @@ VS Code uses `"servers"` with `"type": "stdio"`:
130
130
  | **Memory** | `veto_memory_store` · `veto_memory_search` · `veto_memory_delete` · `veto_project_map_update` · `veto_project_map_get` · `veto_pattern_store` · `veto_patterns_list` · `veto_memory_export` · `veto_memory_import` |
131
131
  | **Learning** | `veto_record_outcome` · `veto_learning_stats` · `veto_learning_apply` |
132
132
  | **Handoff** | `veto_handoff` · `veto_continue` · `veto_platform_setup` |
133
+ | **Intelligence** | `veto_docs_fetch` · `veto_context_status` · `veto_task_parse` |
134
+ | **Observability** | `veto_usage_status` · `veto_audit_log` · `veto_health` |
135
+ | **CI/CD** | `veto_ci_gate` |
133
136
  | **Plugins** | `veto_plugins` |
134
137
 
135
138
  ## MCP Resources
@@ -171,7 +174,7 @@ npx @jigyasudham/veto help # Same help output, no install needed
171
174
  npx @jigyasudham/veto status # Check status from any machine
172
175
  ```
173
176
 
174
- `veto help` shows all CLI commands, all 34 MCP tool names, MCP Resources, and MCP Prompts — the full reference in one place.
177
+ `veto help` shows all CLI commands, all 41 MCP tool names, MCP Resources, and MCP Prompts — the full reference in one place.
175
178
 
176
179
  ---
177
180
 
@@ -346,6 +349,9 @@ Machine B → veto_memory_import → veto_session_restore
346
349
  | 10 — Watch, Workflow, Explain, Plugins | ✅ Complete | v0.10.0 |
347
350
  | 11 — Smarter Council + Predictive Routing + Auto Project Map | ✅ Complete | v0.11.0 |
348
351
  | 12 — CLI Subcommands + Diff Review | ✅ Complete | v1.0.0 |
352
+ | 13 — Developer Intelligence + Auto Docs | ✅ Complete | v1.1.0 |
353
+ | 14 — Observability + Usage Stats + Audit Log | ✅ Complete | v1.2.0 |
354
+ | 15 — CI/CD Pipeline Gates | ✅ Complete | v1.2.0 |
349
355
 
350
356
  ---
351
357
 
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ process.removeAllListeners('warning');
5
5
  import { mkdirSync, existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
6
6
  import { join, dirname, extname, resolve } from 'node:path';
7
7
  import { homedir } from 'node:os';
8
- const VERSION = '1.0.0';
8
+ const VERSION = '1.2.0';
9
9
  const VETO_DIR = join(homedir(), '.veto');
10
10
  const HOME = homedir();
11
11
  const c = {
@@ -25,7 +25,7 @@ function printBanner() {
25
25
  console.log(c.bold(c.cyan(' ╚████╔╝ ███████╗ ██║ ╚██████╔╝')));
26
26
  console.log(c.bold(c.cyan(' ╚═══╝ ╚══════╝ ╚═╝ ╚═════╝')));
27
27
  console.log('');
28
- console.log(c.dim(` 50 agents. 33 tools. 3 AIs. Self-learning. Zero extra cost.`));
28
+ console.log(c.dim(` 50 agents. 41 tools. 3 AIs. Self-learning. Zero extra cost.`));
29
29
  console.log(c.dim(` v${VERSION}`));
30
30
  console.log('');
31
31
  }
@@ -333,7 +333,7 @@ async function patternsCommand() {
333
333
  }
334
334
  function helpCommand() {
335
335
  console.log('');
336
- console.log(c.bold(c.cyan(' veto')) + c.dim(` v${VERSION}`) + c.dim(' — 50 agents. 34 tools. 3 AIs. Self-learning. Zero extra cost.'));
336
+ console.log(c.bold(c.cyan(' veto')) + c.dim(` v${VERSION}`) + c.dim(' — 50 agents. 41 tools. 3 AIs. Self-learning. Zero extra cost.'));
337
337
  console.log('');
338
338
  console.log(c.bold(' CLI Commands'));
339
339
  console.log(c.dim(' ─────────────────────────────────────────────────────'));
@@ -344,22 +344,25 @@ function helpCommand() {
344
344
  console.log(` ${c.cyan('veto patterns')} ${c.dim('[prefix]')} List learned agent/routing patterns`);
345
345
  console.log(` ${c.cyan('veto help')} Show this help`);
346
346
  console.log('');
347
- console.log(c.bold(' MCP Tools (34)'));
347
+ console.log(c.bold(' MCP Tools (41)'));
348
348
  console.log(c.dim(' ─────────────────────────────────────────────────────'));
349
- console.log(` ${c.dim('Session')} veto_status · veto_session_save · veto_session_restore · veto_sessions_list`);
350
- console.log(` ${c.dim('Router')} veto_route_task · veto_rate_status`);
351
- console.log(` ${c.dim('Council')} veto_council_debate`);
352
- console.log(` ${c.dim('Agents')} veto_agent_plan · veto_execute_parallel · veto_explain`);
353
- console.log(` ${c.dim('Review')} veto_code_review · veto_security_scan · veto_secrets_scan · veto_diff_review`);
354
- console.log(` ${c.dim('Pipeline')} veto_workflow`);
355
- console.log(` ${c.dim('Watch')} veto_watch · veto_watch_poll · veto_watch_stop`);
356
- console.log(` ${c.dim('Memory')} veto_memory_store · veto_memory_search · veto_memory_delete`);
357
- console.log(` veto_project_map_update · veto_project_map_get`);
358
- console.log(` veto_pattern_store · veto_patterns_list`);
359
- console.log(` veto_memory_export · veto_memory_import`);
360
- console.log(` ${c.dim('Learning')} veto_record_outcome · veto_learning_stats · veto_learning_apply`);
361
- console.log(` ${c.dim('Handoff')} veto_handoff · veto_continue · veto_platform_setup`);
362
- console.log(` ${c.dim('Plugins')} veto_plugins`);
349
+ console.log(` ${c.dim('Session')} veto_status · veto_session_save · veto_session_restore · veto_sessions_list`);
350
+ console.log(` ${c.dim('Router')} veto_route_task · veto_rate_status`);
351
+ console.log(` ${c.dim('Council')} veto_council_debate`);
352
+ console.log(` ${c.dim('Agents')} veto_agent_plan · veto_execute_parallel · veto_explain`);
353
+ console.log(` ${c.dim('Review')} veto_code_review · veto_security_scan · veto_secrets_scan · veto_diff_review`);
354
+ console.log(` ${c.dim('Pipeline')} veto_workflow`);
355
+ console.log(` ${c.dim('Watch')} veto_watch · veto_watch_poll · veto_watch_stop`);
356
+ console.log(` ${c.dim('Memory')} veto_memory_store · veto_memory_search · veto_memory_delete`);
357
+ console.log(` veto_project_map_update · veto_project_map_get`);
358
+ console.log(` veto_pattern_store · veto_patterns_list`);
359
+ console.log(` veto_memory_export · veto_memory_import`);
360
+ console.log(` ${c.dim('Learning')} veto_record_outcome · veto_learning_stats · veto_learning_apply`);
361
+ console.log(` ${c.dim('Handoff')} veto_handoff · veto_continue · veto_platform_setup`);
362
+ console.log(` ${c.dim('Intelligence')} veto_docs_fetch · veto_context_status · veto_task_parse`);
363
+ console.log(` ${c.dim('Observability')} veto_usage_status · veto_audit_log · veto_health`);
364
+ console.log(` ${c.dim('CI/CD')} veto_ci_gate`);
365
+ console.log(` ${c.dim('Plugins')} veto_plugins`);
363
366
  console.log('');
364
367
  console.log(c.bold(' MCP Resources'));
365
368
  console.log(c.dim(' ─────────────────────────────────────────────────────'));
@@ -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 last_resumed_at columns if they don't exist
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 stmt = db.prepare(`
65
- INSERT INTO sessions (id, started_at, platform, project_dir, summary, context, task_state, token_count)
66
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
67
- `);
68
- stmt.run(id, now, input.platform ?? 'claude', input.project_dir ?? null, input.summary ?? null, input.context ? JSON.stringify(input.context) : null, input.task_state ? JSON.stringify(input.task_state) : null, input.token_count ?? 0);
69
- return { session_id: id, saved_at: now };
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
@@ -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
- const VERSION = '1.1.0';
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
- return {
619
- content: [
620
- {
621
- type: 'text',
622
- text: JSON.stringify({
623
- success: true,
624
- message: 'Session saved. Use this ID to restore on any AI platform.',
625
- ...result,
626
- }, null, 2),
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
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jigyasudham/veto",
3
- "version": "1.1.0",
4
- "description": "50 agents. 34 tools. 3 AIs. Self-learning. Zero extra cost.",
3
+ "version": "1.2.1",
4
+ "description": "50 agents. 41 tools. 3 AIs. Self-learning. Zero extra cost.",
5
5
  "keywords": [
6
6
  "mcp",
7
7
  "ai",