@jigyasudham/veto 1.0.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/README.md CHANGED
@@ -73,7 +73,7 @@ VS Code uses `"servers"` with `"type": "stdio"`:
73
73
 
74
74
  **50 Agents** — Domain experts for every task type. Each agent knows when it is the right tool and when to defer.
75
75
 
76
- **Memory** — Sessions, decisions, knowledge, and coding patterns persist across every conversation and every platform.
76
+ **Memory** — Sessions, decisions, knowledge, and coding patterns persist across every conversation and every platform. Memory is automatically scoped to the active session's project directory — two instances working on different projects stay isolated without any extra configuration.
77
77
 
78
78
  **Diff review** — `veto_diff_review` runs code review, security scan, and secrets scan in parallel across a git diff. Returns a pass/warn/fail verdict with per-file findings — ready for CI and pre-commit hooks.
79
79
 
@@ -303,10 +303,16 @@ Veto loads it on start. Use it in `veto_agent_plan { agent: "my-agent" }` or `ve
303
303
  **Rate limit mid-task:**
304
304
  ```
305
305
  Claude at 90% → veto_handoff { summary, context }
306
- Open Gemini → veto_continue
306
+ Open Gemini → veto_continue { resuming_as: "gemini" }
307
307
  Full context restored. Continue exactly where you stopped.
308
308
  ```
309
309
 
310
+ Every session tracks two fields:
311
+ - `created_by` — which AI originally saved the session
312
+ - `active_client` — which AI last resumed it (updated on every `veto_continue` or `veto_session_restore`)
313
+
314
+ **Multiple AIs on different projects simultaneously:** Each MCP server process is independent. Sessions are always separate. Memory is automatically scoped to each process's active project — no cross-contamination.
315
+
310
316
  **Switch machines:**
311
317
  ```
312
318
  Machine A → veto_memory_export → veto-export.json
@@ -37,10 +37,10 @@ export function handoff(options) {
37
37
  };
38
38
  }
39
39
  // ─── Continue ─────────────────────────────────────────────────────────────────
40
- export function continueSession(sessionId) {
40
+ export function continueSession(sessionId, active_client) {
41
41
  const now = new Date().toISOString();
42
42
  if (sessionId) {
43
- const result = restoreSession(sessionId);
43
+ const result = restoreSession(sessionId, active_client);
44
44
  if (!result.found || !result.session) {
45
45
  return { found: false, message: `No session found with ID: ${sessionId}`, restored_at: now };
46
46
  }
@@ -55,7 +55,7 @@ export function continueSession(sessionId) {
55
55
  restored_at: now,
56
56
  };
57
57
  }
58
- const result = restoreSession(sessions[0].id);
58
+ const result = restoreSession(sessions[0].id, active_client);
59
59
  if (!result.found || !result.session) {
60
60
  return { found: false, message: 'Could not restore the most recent session.', restored_at: now };
61
61
  }
@@ -98,6 +98,7 @@ function buildContinueResult(session, now) {
98
98
  found: true,
99
99
  session_id: session.id,
100
100
  platform: session.platform,
101
+ active_client: session.active_client ?? undefined,
101
102
  summary: session.summary ?? undefined,
102
103
  context,
103
104
  task_state,
@@ -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;
@@ -19,8 +25,20 @@ export function getDb() {
19
25
  _db.exec(CREATE_TABLES);
20
26
  migrateCouncilOutcomes(_db);
21
27
  migrateCouncilColumns(_db);
28
+ migrateSessionColumns(_db);
22
29
  return _db;
23
30
  }
31
+ // Adds active_client, last_resumed_at, and connection_type columns if they don't exist
32
+ function migrateSessionColumns(db) {
33
+ const cols = db.prepare('PRAGMA table_info(sessions)').all();
34
+ const names = new Set(cols.map(c => c.name));
35
+ if (!names.has('active_client'))
36
+ db.exec('ALTER TABLE sessions ADD COLUMN active_client TEXT');
37
+ if (!names.has('last_resumed_at'))
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'");
41
+ }
24
42
  // Adds legal and security columns if they don't exist (Phase 3 → Phase 3.1 migration)
25
43
  function migrateCouncilColumns(db) {
26
44
  const cols = db.prepare('PRAGMA table_info(council_outcomes)').all();
@@ -51,18 +69,39 @@ export function saveSession(input) {
51
69
  const db = getDb();
52
70
  const id = randomUUID();
53
71
  const now = new Date().toISOString();
54
- const stmt = db.prepare(`
55
- INSERT INTO sessions (id, started_at, platform, project_dir, summary, context, task_state, token_count)
56
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
57
- `);
58
- 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);
59
- 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 };
60
92
  }
61
- export function restoreSession(session_id) {
93
+ export function restoreSession(session_id, active_client) {
62
94
  const db = getDb();
63
95
  const row = db.prepare('SELECT * FROM sessions WHERE id = ?').get(session_id);
64
96
  if (!row)
65
97
  return { found: false };
98
+ if (active_client) {
99
+ const now = new Date().toISOString();
100
+ db.prepare('UPDATE sessions SET active_client = ?, last_resumed_at = ? WHERE id = ?')
101
+ .run(active_client, now, session_id);
102
+ row.active_client = active_client;
103
+ row.last_resumed_at = now;
104
+ }
66
105
  return { found: true, session: row };
67
106
  }
68
107
  export function listSessions(limit = 10) {
@@ -179,4 +218,198 @@ export function getPatterns(prefix, limit = 20) {
179
218
  }
180
219
  return db.prepare('SELECT * FROM patterns ORDER BY confidence DESC, seen_count DESC LIMIT ?').all(limit);
181
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
+ }
182
415
  //# sourceMappingURL=local.js.map
@@ -2,16 +2,47 @@
2
2
  // All tables created on first run — zero setup required
3
3
  export const CREATE_TABLES = `
4
4
  CREATE TABLE IF NOT EXISTS sessions (
5
- id TEXT PRIMARY KEY,
6
- started_at TEXT NOT NULL,
7
- ended_at TEXT,
8
- platform TEXT NOT NULL DEFAULT 'claude',
9
- project_dir TEXT,
10
- summary TEXT,
11
- context TEXT,
12
- task_state TEXT,
13
- token_count INTEGER DEFAULT 0,
14
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
5
+ id TEXT PRIMARY KEY,
6
+ started_at TEXT NOT NULL,
7
+ ended_at TEXT,
8
+ platform TEXT NOT NULL DEFAULT 'claude',
9
+ active_client TEXT,
10
+ last_resumed_at TEXT,
11
+ connection_type TEXT NOT NULL DEFAULT 'subscription',
12
+ project_dir TEXT,
13
+ summary TEXT,
14
+ context TEXT,
15
+ task_state TEXT,
16
+ token_count INTEGER DEFAULT 0,
17
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
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'))
15
46
  );
16
47
 
17
48
  CREATE TABLE IF NOT EXISTS decisions (
@@ -112,5 +143,8 @@ export const CREATE_TABLES = `
112
143
  CREATE INDEX IF NOT EXISTS idx_knowledge_type ON knowledge_base(type);
113
144
  CREATE INDEX IF NOT EXISTS idx_knowledge_project ON knowledge_base(project_dir);
114
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);
115
149
  `;
116
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,9 +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.0.0';
21
+ import { createHash } from 'node:crypto';
22
+ const VERSION = '1.2.0';
23
+ // Tracks the project_dir of the most recently active session in this process.
24
+ // Used as a fallback when memory_store/memory_search are called without an explicit project_dir,
25
+ // so memories are automatically scoped to the current project.
26
+ let activeProjectDir = null;
27
+ // Server health tracking
28
+ const SERVER_START_TIME = Date.now();
29
+ let serverErrorCount = 0;
30
+ let lastServerError = null;
22
31
  const server = new Server({ name: 'veto', version: VERSION }, {
23
32
  capabilities: {
24
33
  tools: {},
@@ -65,9 +74,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
65
74
  type: 'string',
66
75
  description: 'Absolute path to the current project directory.',
67
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
+ },
68
82
  token_count: {
69
83
  type: 'number',
70
- description: 'Approximate tokens used this session.',
84
+ description: 'Approximate tokens used this session. Veto uses this for context window monitoring.',
71
85
  },
72
86
  },
73
87
  required: ['summary', 'context'],
@@ -83,6 +97,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
83
97
  type: 'string',
84
98
  description: 'UUID of the session to restore.',
85
99
  },
100
+ resuming_as: {
101
+ type: 'string',
102
+ description: 'The AI client resuming this session (e.g. "claude", "gemini", "codex"). Recorded as active_client.',
103
+ enum: ['claude', 'gemini', 'codex'],
104
+ },
86
105
  },
87
106
  required: ['session_id'],
88
107
  },
@@ -470,6 +489,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
470
489
  type: 'object',
471
490
  properties: {
472
491
  session_id: { type: 'string', description: 'Optional. Session ID from veto_handoff. If omitted, the most recent saved session is restored.' },
492
+ resuming_as: { type: 'string', description: 'The AI client resuming this session (e.g. "gemini"). Recorded as active_client so you can track which tool is currently working on it.', enum: ['claude', 'gemini', 'codex'] },
473
493
  },
474
494
  required: [],
475
495
  },
@@ -568,6 +588,93 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
568
588
  required: [],
569
589
  },
570
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
+ },
571
678
  ],
572
679
  }));
573
680
  // ─── Tool Handlers ────────────────────────────────────────────────────────────
@@ -594,30 +701,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
594
701
  };
595
702
  }
596
703
  case 'veto_session_save': {
704
+ const sessionProjectDir = args?.project_dir ? String(args.project_dir) : undefined;
705
+ if (sessionProjectDir)
706
+ activeProjectDir = sessionProjectDir;
597
707
  const result = saveSession({
598
708
  summary: String(args?.summary ?? ''),
599
709
  context: String(args?.context ?? ''),
600
710
  task_state: args?.task_state ? String(args.task_state) : undefined,
601
711
  platform: args?.platform ? String(args.platform) : 'claude',
602
- project_dir: args?.project_dir ? String(args.project_dir) : undefined,
712
+ connection_type: args?.connection_type ? String(args.connection_type) : 'subscription',
713
+ project_dir: sessionProjectDir,
603
714
  token_count: typeof args?.token_count === 'number' ? args.token_count : 0,
604
715
  });
605
- return {
606
- content: [
607
- {
608
- type: 'text',
609
- text: JSON.stringify({
610
- success: true,
611
- message: 'Session saved. Use this ID to restore on any AI platform.',
612
- ...result,
613
- }, null, 2),
614
- },
615
- ],
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,
616
725
  };
726
+ if (result.continuation_prompt)
727
+ responseObj.continuation_prompt = result.continuation_prompt;
728
+ return { content: [{ type: 'text', text: JSON.stringify(responseObj, null, 2) }] };
617
729
  }
618
730
  case 'veto_session_restore': {
619
731
  const session_id = String(args?.session_id ?? '');
620
- const result = restoreSession(session_id);
732
+ const resuming_as = args?.resuming_as ? String(args.resuming_as) : undefined;
733
+ const result = restoreSession(session_id, resuming_as);
621
734
  if (!result.found) {
622
735
  return {
623
736
  content: [
@@ -630,6 +743,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
630
743
  };
631
744
  }
632
745
  const s = result.session;
746
+ if (s.project_dir)
747
+ activeProjectDir = s.project_dir;
633
748
  return {
634
749
  content: [
635
750
  {
@@ -637,7 +752,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
637
752
  text: JSON.stringify({
638
753
  success: true,
639
754
  session_id: s.id,
640
- platform: s.platform,
755
+ created_by: s.platform,
756
+ active_client: s.active_client ?? s.platform,
757
+ last_resumed_at: s.last_resumed_at,
641
758
  started_at: s.started_at,
642
759
  ended_at: s.ended_at,
643
760
  project_dir: s.project_dir,
@@ -932,7 +1049,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
932
1049
  content,
933
1050
  type: args?.type ? String(args.type) : 'solution',
934
1051
  tags: Array.isArray(args?.tags) ? args.tags.map(String) : undefined,
935
- project_dir: args?.project_dir ? String(args.project_dir) : undefined,
1052
+ project_dir: args?.project_dir ? String(args.project_dir) : (activeProjectDir ?? undefined),
936
1053
  session_id: args?.session_id ? String(args.session_id) : undefined,
937
1054
  relevance: typeof args?.relevance === 'number' ? args.relevance : 1.0,
938
1055
  });
@@ -942,7 +1059,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
942
1059
  const results = searchKnowledge({
943
1060
  query: args?.query ? String(args.query) : undefined,
944
1061
  type: args?.type ? String(args.type) : undefined,
945
- project_dir: args?.project_dir ? String(args.project_dir) : undefined,
1062
+ project_dir: args?.project_dir ? String(args.project_dir) : (activeProjectDir ?? undefined),
946
1063
  limit: typeof args?.limit === 'number' ? args.limit : 10,
947
1064
  });
948
1065
  return {
@@ -1061,16 +1178,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1061
1178
  return { content: [{ type: 'text', text: result.instructions + '\n\n' + JSON.stringify({ session_id: result.session_id, to_platform: result.to_platform, saved_at: result.saved_at, reason: result.reason }, null, 2) }] };
1062
1179
  }
1063
1180
  case 'veto_continue': {
1064
- const result = continueSession(args?.session_id ? String(args.session_id) : undefined);
1181
+ const resuming_as = args?.resuming_as ? String(args.resuming_as) : undefined;
1182
+ const result = continueSession(args?.session_id ? String(args.session_id) : undefined, resuming_as);
1065
1183
  if (!result.found) {
1066
1184
  return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: result.message }, null, 2) }], isError: true };
1067
1185
  }
1186
+ if (result.project_dir)
1187
+ activeProjectDir = result.project_dir;
1068
1188
  return {
1069
1189
  content: [{
1070
1190
  type: 'text',
1071
1191
  text: result.message + '\n\n' + JSON.stringify({
1072
1192
  session_id: result.session_id,
1073
- platform: result.platform,
1193
+ created_by: result.platform,
1194
+ active_client: result.active_client ?? result.platform,
1074
1195
  summary: result.summary,
1075
1196
  context: result.context,
1076
1197
  task_state: result.task_state,
@@ -1220,6 +1341,189 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1220
1341
  case 'veto_plugins': {
1221
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) }] };
1222
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
+ }
1223
1527
  default:
1224
1528
  throw new Error(`Unknown tool: ${name}`);
1225
1529
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jigyasudham/veto",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "50 agents. 34 tools. 3 AIs. Self-learning. Zero extra cost.",
5
5
  "keywords": [
6
6
  "mcp",