@jigyasudham/veto 1.2.16 → 1.2.19

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/server.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- // Veto MCP Server — 45 tools, 19 phases complete, auto-learning router
2
+ // Veto MCP Server — 46 tools, 21 phases complete, auto-learning router
3
3
  // Suppress node:sqlite experimental warning — it would corrupt the MCP stdio protocol
4
4
  process.removeAllListeners('warning');
5
5
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
6
6
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
7
  import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
8
8
  import { buildContextString } from './context/reader.js';
9
- import { saveSession, restoreSession, listSessions, closeSession, getDbPath, saveCouncilOutcome, storeKnowledge, searchKnowledge, deleteKnowledge, updateProjectMap, getProjectMap, upsertPattern, getPatterns, getContextStatus, fetchAndCacheDocs, saveTaskPlan, getUsageStatus, getAuditLog, getHealthStats, CONTEXT_WINDOWS, logUsage, getUsageLogs, } from './memory/local.js';
9
+ import { saveSession, restoreSession, listSessions, closeSession, getDbPath, saveCouncilOutcome, storeKnowledge, searchKnowledge, deleteKnowledge, updateProjectMap, getProjectMap, upsertPattern, getPatterns, getContextStatus, fetchAndCacheDocs, saveTaskPlan, getUsageStatus, getAuditLog, getHealthStats, CONTEXT_WINDOWS, logUsage, getUsageLogs, getDb, storeScanDiagnostics, clearScanDiagnostics, updateSession, } from './memory/local.js';
10
10
  import { exportMemory, importMemory } from './memory/sync.js';
11
11
  import { runDebate } from './council/index.js';
12
12
  import { routeTask, getRateStatus, trackTokens, recordOutcome, getLearningStats, getLearnedThresholds, applyLearnedThresholds, getAgentPerformanceStats, getTaskTypeBreakdown, getCouncilInsights, getRecommendedAgent } from './router/index.js';
@@ -52,7 +52,7 @@ function maybeAutoSave(token_count, platform) {
52
52
  if (elapsed < autoSave.cooldown_ms)
53
53
  return { triggered: false };
54
54
  }
55
- const result = saveSession({ ...autoSave.cached, token_count, platform });
55
+ const result = saveSession({ ...autoSave.cached, token_count, platform, save_type: 'auto' });
56
56
  autoSave.last_save_at = result.saved_at;
57
57
  autoSave.last_session_id = result.session_id;
58
58
  return { triggered: true, session_id: result.session_id, usage_pct };
@@ -90,6 +90,7 @@ const TOOL_ANNOTATIONS = {
90
90
  veto_discover: { readOnlyHint: true },
91
91
  veto_summarize: { readOnlyHint: true },
92
92
  veto_explain: { readOnlyHint: true },
93
+ veto_benchmark: { readOnlyHint: false, destructiveHint: false },
93
94
  // read-only + open world (external network)
94
95
  veto_docs_fetch: { readOnlyHint: true, openWorldHint: true },
95
96
  veto_pr_review: { readOnlyHint: true, openWorldHint: true },
@@ -150,7 +151,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
150
151
  },
151
152
  {
152
153
  name: 'veto_session_save',
153
- description: 'Saves the current session context to SQLite for later restoration across AI platforms.',
154
+ description: 'Saves the current session context to SQLite. Pass session_id to update an existing session instead of creating a new one — use this when refreshing context mid-conversation rather than starting a new snapshot.',
154
155
  inputSchema: {
155
156
  type: 'object',
156
157
  properties: {
@@ -184,6 +185,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
184
185
  type: 'number',
185
186
  description: 'Approximate tokens used this session. Veto uses this for context window monitoring.',
186
187
  },
188
+ session_id: {
189
+ type: 'string',
190
+ description: 'Optional. UUID of an existing session to update in-place. When provided, Veto updates that row instead of inserting a new one — prevents session inflation when refreshing mid-conversation.',
191
+ },
187
192
  },
188
193
  required: ['summary', 'context'],
189
194
  },
@@ -322,12 +327,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
322
327
  },
323
328
  {
324
329
  name: 'veto_code_review',
325
- description: 'Runs the Code Reviewer agent on provided code. Returns scored findings (complexity, error handling, magic numbers, nesting, dead code) with severity and fixes.',
330
+ description: 'Runs the Code Reviewer agent on provided code. Returns scored findings (complexity, error handling, magic numbers, nesting, dead code) with severity and fixes. Pass file_path to surface findings as VS Code inline diagnostics (squiggles).',
326
331
  inputSchema: {
327
332
  type: 'object',
328
333
  properties: {
329
334
  code: { type: 'string', description: 'The code to review.' },
330
335
  context: { type: 'string', description: 'Optional: file name, module description, or review focus.' },
336
+ file_path: { type: 'string', description: 'Optional: absolute path to the file being reviewed. When provided, findings are stored as VS Code inline diagnostics.' },
331
337
  },
332
338
  required: ['code'],
333
339
  },
@@ -347,23 +353,25 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
347
353
  },
348
354
  {
349
355
  name: 'veto_security_scan',
350
- description: 'Runs the Security Scanner (OWASP Top 10) on provided code. Returns vulnerabilities with severity, CWE/OWASP category, and remediation steps.',
356
+ description: 'Runs the Security Scanner (OWASP Top 10) on provided code. Returns vulnerabilities with severity, CWE/OWASP category, and remediation steps. Pass file_path to surface findings as VS Code inline diagnostics.',
351
357
  inputSchema: {
352
358
  type: 'object',
353
359
  properties: {
354
360
  code: { type: 'string', description: 'The code to scan.' },
355
361
  context: { type: 'string', description: 'Optional: language, framework, or specific concerns.' },
362
+ file_path: { type: 'string', description: 'Optional: absolute path to the file being scanned. When provided, findings are stored as VS Code inline diagnostics.' },
356
363
  },
357
364
  required: ['code'],
358
365
  },
359
366
  },
360
367
  {
361
368
  name: 'veto_secrets_scan',
362
- description: 'Scans text or code for exposed credentials — API keys, tokens, passwords, connection strings, private keys. Returns findings with masked values and line numbers.',
369
+ description: 'Scans text or code for exposed credentials — API keys, tokens, passwords, connection strings, private keys. Returns findings with masked values and line numbers. Pass file_path to surface findings as VS Code inline diagnostics.',
363
370
  inputSchema: {
364
371
  type: 'object',
365
372
  properties: {
366
373
  text: { type: 'string', description: 'The text or code to scan for secrets.' },
374
+ file_path: { type: 'string', description: 'Optional: absolute path to the file being scanned. When provided, findings are stored as VS Code inline diagnostics.' },
367
375
  },
368
376
  required: ['text'],
369
377
  },
@@ -825,6 +833,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
825
833
  required: [],
826
834
  },
827
835
  },
836
+ {
837
+ name: 'veto_benchmark',
838
+ description: 'Compares two competing approaches by running a full council debate on each in parallel, then returns a structured winner analysis with verdict, confidence delta, warning counts, and council reasoning. Use when you have two valid options and want an unbiased council judgment before committing.',
839
+ inputSchema: {
840
+ type: 'object',
841
+ properties: {
842
+ task: { type: 'string', description: 'The decision context — what problem are both approaches solving?' },
843
+ approach_a: { type: 'string', description: 'First approach to evaluate. Be specific about tech choices, trade-offs, and constraints.' },
844
+ approach_b: { type: 'string', description: 'Second approach to evaluate. Same level of detail as approach_a.' },
845
+ context: { type: 'string', description: 'Optional: shared context for both debates (architecture notes, constraints, team size, etc.).' },
846
+ project_dir: { type: 'string', description: 'Optional: auto-inject package.json and git diff context.' },
847
+ },
848
+ required: ['task', 'approach_a', 'approach_b'],
849
+ },
850
+ },
828
851
  ];
829
852
  return { tools: tools.map(t => ({ ...t, annotations: TOOL_ANNOTATIONS[t.name] ?? {} })) };
830
853
  });
@@ -847,11 +870,31 @@ async function runTripleScan(diff, context) {
847
870
  autoRecord('scan', 'secrets', (secretsResult.analysis?.findings?.length ?? 0) === 0 ? 100 : secretsResult.analysis?.score ?? Math.round(secretsResult.output.confidence * 100));
848
871
  return { reviewResult, secResult, secretsResult, verdict };
849
872
  }
873
+ function parseLineFromLocation(location) {
874
+ if (!location)
875
+ return 1;
876
+ const m = location.match(/(?:^|line\s+)(\d+)/i);
877
+ return m ? parseInt(m[1], 10) : 1;
878
+ }
879
+ function mapSeverityToVscode(severity) {
880
+ if (severity === 'critical' || severity === 'high')
881
+ return 'error';
882
+ if (severity === 'medium')
883
+ return 'warning';
884
+ return 'information';
885
+ }
850
886
  // Auto-learning helper — records a learning_data row from any agent result.
851
887
  // Keeps call sites to one line rather than repeating the tier/quality logic.
888
+ // After every 20 outcomes the thresholds are applied automatically.
852
889
  function autoRecord(taskType, agent, quality, complexity = 50) {
853
890
  const tier = quality >= 80 ? 1 : quality >= 40 ? 2 : 3;
854
891
  recordOutcome(taskType.slice(0, 50), complexity, tier, agent, quality);
892
+ try {
893
+ const count = getDb().prepare('SELECT COUNT(*) as c FROM learning_data').get().c;
894
+ if (count >= 20 && count % 20 === 0)
895
+ applyLearnedThresholds();
896
+ }
897
+ catch { /* never interrupt the caller */ }
855
898
  }
856
899
  // Auto-store helper — writes a knowledge_base entry when a scan produces critical/blocking issues.
857
900
  // This is what populates the Memory panel in the VS Code extension automatically.
@@ -938,7 +981,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
938
981
  const saveSummary = String(args?.summary ?? '');
939
982
  const saveContext = String(args?.context ?? '');
940
983
  const saveTaskState = args?.task_state ? String(args.task_state) : undefined;
941
- const result = saveSession({
984
+ const existingId = args?.session_id ? String(args.session_id) : undefined;
985
+ const sessionInput = {
942
986
  summary: saveSummary,
943
987
  context: saveContext,
944
988
  task_state: saveTaskState,
@@ -946,20 +990,37 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
946
990
  connection_type: args?.connection_type ? String(args.connection_type) : 'subscription',
947
991
  project_dir: sessionProjectDir,
948
992
  token_count: typeof args?.token_count === 'number' ? args.token_count : 0,
949
- });
993
+ };
994
+ let result;
995
+ let wasUpdate = false;
996
+ if (existingId) {
997
+ const updated = updateSession(existingId, sessionInput);
998
+ if (updated) {
999
+ result = { session_id: updated.session_id, saved_at: updated.saved_at, usage_pct: 0, context_warning: false, continuation_prompt: null };
1000
+ wasUpdate = true;
1001
+ }
1002
+ else {
1003
+ result = saveSession(sessionInput);
1004
+ }
1005
+ }
1006
+ else {
1007
+ result = saveSession(sessionInput);
1008
+ }
950
1009
  // Cache for auto-save: future veto_status calls with high token_count will re-save this context
951
1010
  autoSave.cached = { summary: saveSummary, context: saveContext, task_state: saveTaskState, platform: savePlatform, project_dir: sessionProjectDir };
952
1011
  autoSave.last_save_at = result.saved_at;
953
1012
  autoSave.last_session_id = result.session_id;
954
1013
  const responseObj = {
955
1014
  success: true,
956
- message: result.context_warning
957
- ? `⚠️ Context at ${result.usage_pct}% — consider handing off soon.`
958
- : 'Session saved. Use this ID to restore on any AI platform.',
1015
+ message: wasUpdate
1016
+ ? `Session updated in-place. ID unchanged: ${result.session_id}`
1017
+ : result.context_warning
1018
+ ? `⚠️ Context at ${result.usage_pct}% — consider handing off soon.`
1019
+ : 'Session saved. Use this ID to restore on any AI platform.',
959
1020
  session_id: result.session_id,
960
1021
  saved_at: result.saved_at,
961
- usage_pct: result.usage_pct,
962
- context_warning: result.context_warning,
1022
+ updated: wasUpdate,
1023
+ ...(wasUpdate ? {} : { usage_pct: result.usage_pct, context_warning: result.context_warning }),
963
1024
  };
964
1025
  if (result.continuation_prompt)
965
1026
  responseObj.continuation_prompt = result.continuation_prompt;
@@ -1170,11 +1231,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1170
1231
  if (!code) {
1171
1232
  return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'code is required.' }) }], isError: true };
1172
1233
  }
1234
+ const reviewFilePath = args?.file_path ? String(args.file_path) : undefined;
1173
1235
  const result = await executeOne({ id: 'review-1', agent: 'reviewer', task: 'review this code', code, context: args?.context ? String(args.context) : undefined });
1174
1236
  if (result.error) {
1175
1237
  return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: result.error }) }], isError: true };
1176
1238
  }
1177
1239
  autoRecord('code review', 'reviewer', result.analysis?.score ?? Math.round(result.output.confidence * 100));
1240
+ if (reviewFilePath && result.analysis?.findings) {
1241
+ storeScanDiagnostics(reviewFilePath, result.analysis.findings.map((f) => ({
1242
+ line: parseLineFromLocation(f.location),
1243
+ message: f.description ?? '',
1244
+ severity: mapSeverityToVscode(f.severity),
1245
+ })), 'code-review');
1246
+ }
1247
+ else if (reviewFilePath) {
1248
+ clearScanDiagnostics(reviewFilePath);
1249
+ }
1178
1250
  return { content: [{ type: 'text', text: JSON.stringify(result.analysis, null, 2) }] };
1179
1251
  }
1180
1252
  case 'veto_diff_review': {
@@ -1265,11 +1337,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1265
1337
  if (!code) {
1266
1338
  return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'code is required.' }) }], isError: true };
1267
1339
  }
1340
+ const secFilePath = args?.file_path ? String(args.file_path) : undefined;
1268
1341
  const result = await executeOne({ id: 'scan-1', agent: 'security-scanner', task: 'scan this code for security issues', code, context: args?.context ? String(args.context) : undefined });
1269
1342
  if (result.error) {
1270
1343
  return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: result.error }) }], isError: true };
1271
1344
  }
1272
1345
  autoRecord('security scan', 'security-scanner', result.analysis?.score ?? Math.round(result.output.confidence * 100));
1346
+ if (secFilePath && result.analysis?.findings) {
1347
+ storeScanDiagnostics(secFilePath, result.analysis.findings.map((f) => ({
1348
+ line: parseLineFromLocation(f.location),
1349
+ message: f.description ?? '',
1350
+ severity: mapSeverityToVscode(f.severity),
1351
+ })), 'security');
1352
+ }
1353
+ else if (secFilePath) {
1354
+ clearScanDiagnostics(secFilePath);
1355
+ }
1273
1356
  return { content: [{ type: 'text', text: JSON.stringify(result.analysis, null, 2) }] };
1274
1357
  }
1275
1358
  case 'veto_secrets_scan': {
@@ -1277,11 +1360,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1277
1360
  if (!text) {
1278
1361
  return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'text is required.' }) }], isError: true };
1279
1362
  }
1363
+ const secretsFilePath = args?.file_path ? String(args.file_path) : undefined;
1280
1364
  const result = await executeOne({ id: 'secrets-1', agent: 'secrets', task: 'scan for exposed credentials', code: text });
1281
1365
  if (result.error) {
1282
1366
  return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: result.error }) }], isError: true };
1283
1367
  }
1284
1368
  autoRecord('secrets scan', 'secrets', (result.analysis?.findings?.length ?? 0) === 0 ? 100 : result.analysis?.score ?? Math.round(result.output.confidence * 100));
1369
+ if (secretsFilePath && result.analysis?.findings) {
1370
+ storeScanDiagnostics(secretsFilePath, result.analysis.findings.map((f) => ({
1371
+ line: parseLineFromLocation(f.location),
1372
+ message: f.description ?? '',
1373
+ severity: mapSeverityToVscode(f.severity),
1374
+ })), 'secrets');
1375
+ }
1376
+ else if (secretsFilePath) {
1377
+ clearScanDiagnostics(secretsFilePath);
1378
+ }
1285
1379
  return { content: [{ type: 'text', text: JSON.stringify(result.analysis, null, 2) }] };
1286
1380
  }
1287
1381
  case 'veto_execute_parallel': {
@@ -2092,6 +2186,86 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2092
2186
  agent_used: 'project-mapper', duration_ms: Date.now() - sumStart,
2093
2187
  }, null, 2) }] };
2094
2188
  }
2189
+ case 'veto_benchmark': {
2190
+ const task = String(args?.task ?? '');
2191
+ const approachA = String(args?.approach_a ?? '');
2192
+ const approachB = String(args?.approach_b ?? '');
2193
+ const ctx = args?.context ? String(args.context) : undefined;
2194
+ const projectDir = args?.project_dir ? String(args.project_dir) : undefined;
2195
+ if (!task || !approachA || !approachB) {
2196
+ throw new Error('veto_benchmark requires task, approach_a, and approach_b');
2197
+ }
2198
+ const bmStart = Date.now();
2199
+ // Run both debates synchronously (council agents are all sync)
2200
+ const debateA = runDebate({ task: `${task}\n\nApproach A: ${approachA}`, context: ctx, project_dir: projectDir });
2201
+ const debateB = runDebate({ task: `${task}\n\nApproach B: ${approachB}`, context: ctx, project_dir: projectDir });
2202
+ // Score: GREEN=3, YELLOW=2, RED=1, DEADLOCK=0
2203
+ const verdictScore = { GREEN: 3, YELLOW: 2, RED: 1, DEADLOCK: 0 };
2204
+ const scoreA = verdictScore[debateA.final_verdict] ?? 0;
2205
+ const scoreB = verdictScore[debateB.final_verdict] ?? 0;
2206
+ const warnCountA = debateA.warnings.length;
2207
+ const warnCountB = debateB.warnings.length;
2208
+ const blockCountA = debateA.block_reasons.length;
2209
+ const blockCountB = debateB.block_reasons.length;
2210
+ let winner;
2211
+ let confidence;
2212
+ let reasoning;
2213
+ if (scoreA !== scoreB) {
2214
+ winner = scoreA > scoreB ? 'A' : 'B';
2215
+ const diff = Math.abs(scoreA - scoreB);
2216
+ confidence = diff >= 2 ? 'high' : 'medium';
2217
+ reasoning = `Approach ${winner} received a ${winner === 'A' ? debateA.final_verdict : debateB.final_verdict} verdict vs ${winner === 'A' ? debateB.final_verdict : debateA.final_verdict} for Approach ${winner === 'A' ? 'B' : 'A'}.`;
2218
+ }
2219
+ else {
2220
+ // Same verdict — break tie on warnings, then block reasons
2221
+ if (warnCountA !== warnCountB) {
2222
+ winner = warnCountA < warnCountB ? 'A' : 'B';
2223
+ confidence = 'low';
2224
+ reasoning = `Both approaches received ${debateA.final_verdict}. Approach ${winner} had fewer warnings (${winner === 'A' ? warnCountA : warnCountB} vs ${winner === 'A' ? warnCountB : warnCountA}).`;
2225
+ }
2226
+ else if (blockCountA !== blockCountB) {
2227
+ winner = blockCountA < blockCountB ? 'A' : 'B';
2228
+ confidence = 'low';
2229
+ reasoning = `Both approaches received ${debateA.final_verdict} with equal warnings. Approach ${winner} had fewer blocking concerns.`;
2230
+ }
2231
+ else {
2232
+ winner = 'TIE';
2233
+ confidence = 'low';
2234
+ reasoning = `Both approaches received ${debateA.final_verdict} with equal warnings and blocks. Council cannot differentiate — consider a more specific framing.`;
2235
+ }
2236
+ }
2237
+ // Auto-record both debates
2238
+ const qMap = { GREEN: 90, YELLOW: 60, RED: 20, DEADLOCK: 50 };
2239
+ autoRecord('benchmark', 'council', qMap[debateA.final_verdict] ?? 50);
2240
+ autoRecord('benchmark', 'council', qMap[debateB.final_verdict] ?? 50);
2241
+ return { content: [{ type: 'text', text: JSON.stringify({
2242
+ winner,
2243
+ confidence,
2244
+ reasoning,
2245
+ recommendation: winner !== 'TIE'
2246
+ ? `Use Approach ${winner}. ${winner === 'A' ? debateA.recommended : debateB.recommended}`
2247
+ : `No clear winner. ${debateA.recommended}`,
2248
+ approach_a: {
2249
+ label: 'A',
2250
+ description: approachA.slice(0, 120),
2251
+ verdict: debateA.final_verdict,
2252
+ warnings: warnCountA,
2253
+ block_reasons: blockCountA,
2254
+ recommended: debateA.recommended,
2255
+ votes: Object.fromEntries(Object.entries(debateA.votes).map(([k, v]) => [k, v.verdict])),
2256
+ },
2257
+ approach_b: {
2258
+ label: 'B',
2259
+ description: approachB.slice(0, 120),
2260
+ verdict: debateB.final_verdict,
2261
+ warnings: warnCountB,
2262
+ block_reasons: blockCountB,
2263
+ recommended: debateB.recommended,
2264
+ votes: Object.fromEntries(Object.entries(debateB.votes).map(([k, v]) => [k, v.verdict])),
2265
+ },
2266
+ duration_ms: Date.now() - bmStart,
2267
+ }, null, 2) }] };
2268
+ }
2095
2269
  default:
2096
2270
  throw new Error(`Unknown tool: ${name}`);
2097
2271
  }