analyzthis_design 2.3.1 → 2.4.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.
Files changed (52) hide show
  1. package/HOW-TO-USE.md +28 -9
  2. package/README.md +27 -3
  3. package/dist/HOW-TO-USE.md +28 -9
  4. package/dist/README.md +27 -3
  5. package/dist/bin/cli.js +145 -3
  6. package/dist/lib/accept.js +95 -0
  7. package/dist/lib/chunk-run.js +2 -0
  8. package/dist/lib/feedback-submit.js +2 -0
  9. package/dist/lib/install.js +9 -0
  10. package/dist/lib/knowledge.js +49 -18
  11. package/dist/lib/mcp-server.js +181 -36
  12. package/dist/lib/orchestrator/run.js +5 -0
  13. package/dist/lib/receipt.js +102 -0
  14. package/dist/lib/session.js +84 -2
  15. package/dist/lib/share.js +131 -0
  16. package/dist/skills/accept/SKILL.md +52 -0
  17. package/dist/skills/anuj/SKILL.md +7 -0
  18. package/dist/skills/arjun/SKILL.md +5 -263
  19. package/dist/skills/arjun/references/lens.md +265 -0
  20. package/dist/skills/getting-started/SKILL.md +3 -0
  21. package/dist/skills/kavi/SKILL.md +7 -0
  22. package/dist/skills/knowledge-bank/SKILL.md +11 -25
  23. package/dist/skills/meera/SKILL.md +7 -0
  24. package/dist/skills/noor/SKILL.md +7 -0
  25. package/dist/skills/persona-orchestrator/SKILL.md +5 -195
  26. package/dist/skills/persona-orchestrator/references/lens.md +197 -0
  27. package/dist/skills/priya/SKILL.md +7 -0
  28. package/dist/skills/raj/SKILL.md +7 -0
  29. package/dist/skills/receipt/SKILL.md +17 -0
  30. package/dist/skills/share/SKILL.md +54 -0
  31. package/dist/skills/ux-story-gate/SKILL.md +5 -338
  32. package/dist/skills/ux-story-gate/references/lens.md +340 -0
  33. package/dist/skills/zara/SKILL.md +7 -0
  34. package/package.json +1 -1
  35. package/skills/accept/SKILL.md +52 -0
  36. package/skills/anuj/SKILL.md +7 -0
  37. package/skills/arjun/SKILL.md +5 -263
  38. package/skills/arjun/references/lens.md +265 -0
  39. package/skills/getting-started/SKILL.md +3 -0
  40. package/skills/kavi/SKILL.md +7 -0
  41. package/skills/knowledge-bank/SKILL.md +11 -25
  42. package/skills/meera/SKILL.md +7 -0
  43. package/skills/noor/SKILL.md +7 -0
  44. package/skills/persona-orchestrator/SKILL.md +5 -195
  45. package/skills/persona-orchestrator/references/lens.md +197 -0
  46. package/skills/priya/SKILL.md +7 -0
  47. package/skills/raj/SKILL.md +7 -0
  48. package/skills/receipt/SKILL.md +17 -0
  49. package/skills/share/SKILL.md +54 -0
  50. package/skills/ux-story-gate/SKILL.md +5 -338
  51. package/skills/ux-story-gate/references/lens.md +340 -0
  52. package/skills/zara/SKILL.md +7 -0
@@ -8,12 +8,10 @@ const crypto = require('crypto');
8
8
 
9
9
  const CONFIG_DIR = path.join(os.homedir(), '.analyzthis_design');
10
10
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
11
- // When published, this file lives at dist/lib/ — go up two levels to reach package root
12
- const KNOWLEDGE_SKILL = path.join(__dirname, '..', '..', 'skills', 'knowledge-bank', 'SKILL.md');
13
- const KNOWLEDGE_SLICE_ROOT = path.join(CONFIG_DIR, 'kb-slices');
14
-
15
11
  const { resolvePackageRoot } = require('./platforms');
16
12
  const PACKAGE_ROOT = resolvePackageRoot(__dirname);
13
+ const KNOWLEDGE_SKILL = path.join(PACKAGE_ROOT, 'skills', 'knowledge-bank', 'SKILL.md');
14
+ const KNOWLEDGE_SLICE_ROOT = path.join(CONFIG_DIR, 'kb-slices');
17
15
  const AGENTS_DIR = path.join(PACKAGE_ROOT, 'agents');
18
16
 
19
17
  // Keywords used to auto-group vault notes into categories
@@ -326,41 +324,74 @@ function sync({ targets = ['cursor'], project, global = false, cwd } = {}) {
326
324
  ? 'global (merged across projects)'
327
325
  : `project: ${scopeInfo.projectId}`;
328
326
 
329
- let md = `---
330
- name: knowledge-bank
331
- description: Personal knowledge bank takes precedence over all built-in persona defaults.
327
+ let fullMd = `---
328
+ name: knowledge-bank-full
329
+ description: Full project notes. Do not load this file into chat. Retrieve one persona slice instead.
332
330
  disable-model-invocation: true
333
331
  ---
334
332
 
335
- # Knowledge Bank
333
+ # Knowledge Bank (full dump — do not inject)
336
334
 
337
335
  > Last synced: ${date}
338
336
  > Scope: ${scopeLabel}
339
337
  > Sources: ${sourceList}
340
338
  > Files loaded: ${totalFiles}
341
339
 
342
- **INSTRUCTION TO ALL PERSONAS:** This knowledge bank contains project-specific context that overrides your built-in defaults. Read every section below before forming any opinion. When this knowledge bank conflicts with your built-in knowledge, this knowledge bank wins.
343
-
344
- ---
345
-
346
340
  `;
347
341
 
348
342
  let hasContent = false;
343
+ let indexLines = '';
349
344
  for (const { key, heading } of sectionDefs) {
350
345
  if (sections[key].length === 0) continue;
351
346
  hasContent = true;
352
- md += `${heading}\n\n`;
347
+ fullMd += `${heading}\n\n`;
348
+ indexLines += `${heading}\n`;
353
349
  for (const note of sections[key]) {
354
- md += `### ${note.title}\n\n${note.content}\n\n`;
350
+ fullMd += `### ${note.title}\n\n${note.content}\n\n`;
351
+ indexLines += `- ${note.title}\n`;
355
352
  }
356
- md += '---\n\n';
353
+ fullMd += '---\n\n';
354
+ indexLines += '\n';
357
355
  }
358
356
 
359
357
  if (!hasContent) {
360
- md += `_No matching files found. Check your --tags or --include filters, or remove filters to include all notes._\n`;
358
+ fullMd += `_No matching files found. Check your --tags or --include filters, or remove filters to include all notes._\n`;
359
+ indexLines += `_No notes yet. Run collect/sync first._\n`;
361
360
  }
362
361
 
363
- // Write to the package's own skills/knowledge-bank/SKILL.md (source of truth)
362
+ const md = `---
363
+ name: knowledge-bank
364
+ description: Project knowledge index — retrieve one persona slice. Do not load the full bank.
365
+ disable-model-invocation: true
366
+ ---
367
+
368
+ # Knowledge Bank
369
+
370
+ > Last synced: ${date}
371
+ > Scope: ${scopeLabel}
372
+ > Sources: ${sourceList}
373
+ > Files loaded: ${totalFiles}
374
+
375
+ **INSTRUCTION TO ALL PERSONAS:** Load your persona slice via retrieve. Do not load the whole bank. When a slice conflicts with built-in defaults, the slice wins.
376
+
377
+ \`\`\`
378
+ npx analyzthis_design retrieve — or MCP analyzthis_retrieve
379
+ kind=knowledge persona=arjun|meera|priya|zara|noor|anuj|raj|kavi
380
+ \`\`\`
381
+
382
+ You cannot know host tokens. Do not invent a dollar figure.
383
+
384
+ ## Index (titles only)
385
+
386
+ ${indexLines}
387
+ `;
388
+
389
+ // Full dump stays on disk for retrieve; the skill is an index only.
390
+ const projectIdForFull = scopeInfo.projectId || 'default';
391
+ const fullDir = path.join(KNOWLEDGE_SLICE_ROOT, projectIdForFull);
392
+ fs.mkdirSync(fullDir, { recursive: true });
393
+ fs.writeFileSync(path.join(fullDir, 'full.md'), fullMd);
394
+
364
395
  fs.mkdirSync(path.dirname(KNOWLEDGE_SKILL), { recursive: true });
365
396
  fs.writeFileSync(KNOWLEDGE_SKILL, md);
366
397
 
@@ -470,7 +501,7 @@ function buildPersonaSlice(personaId, sections, sectionDefs, projectId) {
470
501
  }
471
502
 
472
503
  function writePersonaSlices(sections, sectionDefs, projectId) {
473
- const personas = ['arjun', 'meera', 'priya', 'zara', 'noor', 'anuj', 'raj'];
504
+ const personas = ['arjun', 'meera', 'priya', 'zara', 'noor', 'anuj', 'raj', 'kavi'];
474
505
  const dir = path.join(KNOWLEDGE_SLICE_ROOT, projectId || 'default');
475
506
  fs.mkdirSync(dir, { recursive: true });
476
507
  const written = [];
@@ -248,16 +248,18 @@ var TOOLS = [
248
248
  },
249
249
  {
250
250
  name: 'analyzthis_retrieve',
251
- description: 'Retrieve CSV reference data. Query the 28-file reference library (colors, styles, typography, UX guidelines, charts, products, landing patterns, 16 stack guides). Returns filtered, citation-ready rows.',
251
+ description: 'Retrieve on demand: CSV rows, a skill reference file, or one knowledge-bank persona slice. Do not load the full bank.',
252
252
  inputSchema: {
253
253
  type: 'object',
254
254
  properties: {
255
- file: { type: 'string', description: 'CSV filename (e.g., "styles.csv", "colors.csv", "stacks/nextjs.csv")' },
256
- column: { type: 'string', description: 'Column to filter on (e.g., "Product Type", "Category", "Best For Tags")' },
257
- keywords: { type: 'string', description: 'Comma-separated keywords to match (e.g., "saas,dashboard")' },
258
- limit: { type: 'number', description: 'Max rows to return (default: 3)' },
255
+ kind: { type: 'string', enum: ['csv', 'skill', 'knowledge'], description: 'csv (default), skill (references/*.md), or knowledge (one persona slice)' },
256
+ file: { type: 'string', description: 'CSV filename, or skill id (e.g. arjun), or knowledge/<persona>' },
257
+ column: { type: 'string', description: 'CSV column to filter on' },
258
+ keywords: { type: 'string', description: 'Comma-separated CSV keywords' },
259
+ limit: { type: 'number', description: 'Max CSV rows (default: 3)' },
260
+ persona: { type: 'string', description: 'Persona id for knowledge slice (arjun, zara, …)' },
261
+ ref: { type: 'string', description: 'Skill reference filename without path (e.g. lens.md)' },
259
262
  },
260
- required: ['file'],
261
263
  },
262
264
  },
263
265
  {
@@ -266,9 +268,12 @@ var TOOLS = [
266
268
  inputSchema: {
267
269
  type: 'object',
268
270
  properties: {
269
- action: { type: 'string', enum: ['show', 'init', 'reset', 'accept'], description: 'show: display current state; init: create fresh; reset: delete; accept: mark a persona output as accepted' },
270
- persona: { type: 'string', description: 'Persona ID (for accept action)' },
271
- reject: { type: 'boolean', description: 'Reject instead of accept (for accept action)' },
271
+ action: { type: 'string', enum: ['show', 'init', 'reset', 'accept'], description: 'show: display current state; init: create fresh; reset: delete; accept: keep or skip a persona note (designer /accept)' },
272
+ persona: { type: 'string', description: 'Persona ID (for accept action). Optional — last persona if omitted.' },
273
+ reject: { type: 'boolean', description: 'Skip / fix instead of keep (for accept action)' },
274
+ because: { type: 'string', description: 'One sentence: what was wrong or what you did instead (when reject is true)' },
275
+ comment: { type: 'string', description: 'Optional note on keep, or alias for because on skip' },
276
+ shipped: { type: 'boolean', description: 'On keep: mark shipped (default true). False = liked only.' },
272
277
  },
273
278
  required: ['action'],
274
279
  },
@@ -285,6 +290,43 @@ var TOOLS = [
285
290
  },
286
291
  ];
287
292
 
293
+ var ROUTER_TOOL = {
294
+ name: 'analyzthis_design',
295
+ description: 'Design team router. Set who to zara, arjun, noor, anuj, meera, priya, raj, kavi, orchestrator, ux-ideator, design-critic, ux-story-gate, design-director, or mood-board. Returns a short skill front — call analyzthis_retrieve for the rest.',
296
+ inputSchema: {
297
+ type: 'object',
298
+ properties: {
299
+ who: { type: 'string', description: 'Persona or pass name (e.g. zara, arjun, orchestrator)' },
300
+ task: { type: 'string', description: 'What to design or critique' },
301
+ context: { type: 'string', description: 'Optional extra context' },
302
+ },
303
+ required: ['who', 'task'],
304
+ },
305
+ };
306
+
307
+ var RECEIPT_TOOL = {
308
+ name: 'analyzthis_receipt',
309
+ description: 'Show inferred token receipt for this project. Not a bill. Do not invent a dollar figure.',
310
+ inputSchema: {
311
+ type: 'object',
312
+ properties: {
313
+ project: { type: 'string', description: 'Optional project id override' },
314
+ },
315
+ },
316
+ };
317
+
318
+ function resolveCatalog(explicit) {
319
+ var raw = explicit || process.env.ANALYZTHIS_MCP_CATALOG || 'lite';
320
+ return String(raw).toLowerCase() === 'full' ? 'full' : 'lite';
321
+ }
322
+
323
+ function listedTools(catalog) {
324
+ if (resolveCatalog(catalog) === 'full') {
325
+ return TOOLS.concat([ROUTER_TOOL, RECEIPT_TOOL]);
326
+ }
327
+ return [ROUTER_TOOL, TOOLS.find(function (t) { return t.name === 'analyzthis_retrieve'; }), TOOLS.find(function (t) { return t.name === 'analyzthis_session'; }), RECEIPT_TOOL].filter(Boolean);
328
+ }
329
+
288
330
  // ── Tool handlers ────────────────────────────────────────────────────────
289
331
 
290
332
  function handleToolCall(name, args) {
@@ -326,6 +368,10 @@ function handleToolCall(name, args) {
326
368
  return handleSession(args);
327
369
  case 'analyzthis_system_prompt':
328
370
  return handleSystemPrompt(args);
371
+ case 'analyzthis_design':
372
+ return handleRouter(args);
373
+ case 'analyzthis_receipt':
374
+ return handleReceipt(args);
329
375
 
330
376
  default:
331
377
  return { error: 'Unknown tool: ' + name };
@@ -333,7 +379,7 @@ function handleToolCall(name, args) {
333
379
  }
334
380
 
335
381
  function handleSinglePersona(personaId, args) {
336
- var card = readCard(personaId);
382
+ var card = readSkillFront(personaId);
337
383
  if (!card) return { error: 'Persona not found: ' + personaId };
338
384
 
339
385
  var task = args.task || '';
@@ -351,11 +397,10 @@ function handleSinglePersona(personaId, args) {
351
397
  '## Task\n' + task + '\n' +
352
398
  (context ? '\n## Context\n' + context : '') +
353
399
  extra + '\n\n' +
354
- 'Produce your output following the schema in your card above. ' +
355
- 'Be specific — cite components, zones, and fixes. ' +
356
- 'Do not give generic advice.';
400
+ 'Use the lite output contract. Call analyzthis_retrieve (kind=skill, file=' + personaId + ') only if you need the full lens. ' +
401
+ 'Be specific — cite components, zones, and fixes. Do not give generic advice.';
357
402
 
358
- return { prompt: prompt, persona: personaId, task: task };
403
+ return { prompt: prompt, persona: personaId, task: task, verdicts: 1 };
359
404
  }
360
405
 
361
406
  function handleCombinationPass(skillId, args) {
@@ -381,11 +426,11 @@ function handleCombinationPass(skillId, args) {
381
426
  skill + '\n\n' +
382
427
  '## Task\n' + task + '\n' +
383
428
  (context ? '\n## Context\n' + context : '') + '\n\n' +
384
- 'Run all phases sequentially. Challenge the premise first. ' +
385
- 'Resolve any persona disagreements explicitly pick a winner with a forward path. ' +
386
- 'Produce the final output in the format specified by the skill.';
429
+ 'Run the smallest useful team. Challenge the premise first. ' +
430
+ 'Call analyzthis_retrieve (kind=skill, file=' + skillId + ') if the front is not enough. ' +
431
+ 'Lite default: verdict + top 3 + one evidence line. Expand only if the user says expand.';
387
432
 
388
- return { prompt: prompt, skill: skillId, task: task };
433
+ return { prompt: prompt, skill: skillId, task: task, persona: skillId, verdicts: 1 };
389
434
  }
390
435
 
391
436
  function handleMoodBoard(args) {
@@ -439,10 +484,13 @@ function handleCollect(args) {
439
484
  }
440
485
 
441
486
  function handleRetrieve(args) {
487
+ var kind = args.kind || 'csv';
488
+ if (kind === 'skill') return handleRetrieveSkill(args);
489
+ if (kind === 'knowledge') return handleRetrieveKnowledge(args);
442
490
  try {
443
491
  var retrieve = require('./retrieve');
444
492
  var file = args.file;
445
- if (!file) return { error: 'file is required' };
493
+ if (!file) return { error: 'file is required for CSV retrieve' };
446
494
  var filters = [];
447
495
  if (args.column && args.keywords) {
448
496
  filters.push({ column: args.column, anyOf: args.keywords.split(',').map(function(s) { return s.trim(); }) });
@@ -464,6 +512,70 @@ function handleRetrieve(args) {
464
512
  }
465
513
  }
466
514
 
515
+ function handleRetrieveSkill(args) {
516
+ var id = String(args.file || args.persona || '').replace(/^skill\//, '');
517
+ if (!id) return { error: 'file (skill id) is required' };
518
+ var refName = args.ref || 'lens.md';
519
+ var refPath = path.join(PACKAGE_ROOT, 'skills', id, 'references', refName);
520
+ if (!fs.existsSync(refPath)) {
521
+ var dir = path.join(PACKAGE_ROOT, 'skills', id, 'references');
522
+ if (fs.existsSync(dir)) {
523
+ var names = fs.readdirSync(dir).filter(function (n) { return n.endsWith('.md'); });
524
+ if (names.length) refPath = path.join(dir, names[0]);
525
+ }
526
+ }
527
+ if (!fs.existsSync(refPath)) {
528
+ var full = readSkill(id);
529
+ if (!full) return { error: 'No skill references for ' + id };
530
+ return { result: full, persona: id };
531
+ }
532
+ return { result: fs.readFileSync(refPath, 'utf8'), persona: id };
533
+ }
534
+
535
+ function handleRetrieveKnowledge(args) {
536
+ var persona = args.persona || String(args.file || '').replace(/^knowledge\//, '');
537
+ if (!persona) return { error: 'persona is required for knowledge retrieve' };
538
+ try {
539
+ var knowledge = require('./knowledge');
540
+ var sessionMod = require('./session');
541
+ var projectId = sessionMod.getProjectId();
542
+ var slicePath = path.join(knowledge.KNOWLEDGE_SLICE_ROOT, projectId, persona + '.md');
543
+ if (!fs.existsSync(slicePath)) {
544
+ return { result: 'No knowledge slice for ' + persona + '. Run collect/sync first. Do not load a full bank.' };
545
+ }
546
+ var text = fs.readFileSync(slicePath, 'utf8');
547
+ var cap = 6000;
548
+ if (text.length > cap) text = text.slice(0, cap) + '\n…[truncated — this is one slice, not the full bank]';
549
+ return { result: text, persona: persona };
550
+ } catch (e) {
551
+ return { error: e.message };
552
+ }
553
+ }
554
+
555
+ var ROUTER_WHO = {
556
+ zara: 'persona', arjun: 'persona', meera: 'persona', priya: 'persona',
557
+ noor: 'persona', anuj: 'persona', raj: 'persona', kavi: 'persona',
558
+ orchestrator: 'persona-orchestrator', 'persona-orchestrator': 'persona-orchestrator',
559
+ 'ux-ideator': 'ux-ideator', 'design-critic': 'design-critic',
560
+ 'ux-story-gate': 'ux-story-gate', 'design-director': 'design-director',
561
+ 'mood-board': 'mood-board',
562
+ };
563
+
564
+ function handleRouter(args) {
565
+ var who = String(args.who || '').toLowerCase().replace(/^analyzthis_/, '').replace(/_/g, '-');
566
+ if (!who) return { error: 'who is required (e.g. zara, arjun, orchestrator)' };
567
+ var mapped = ROUTER_WHO[who];
568
+ if (!mapped) return { error: 'Unknown who: ' + who + '. Use a persona or orchestrator / ux-ideator / design-critic / ux-story-gate / design-director / mood-board.' };
569
+ if (mapped === 'persona') return handleSinglePersona(who, args);
570
+ if (mapped === 'mood-board') return handleMoodBoard(args);
571
+ return handleCombinationPass(mapped, args);
572
+ }
573
+
574
+ function handleReceipt(args) {
575
+ var receipt = require('./receipt');
576
+ return { result: receipt.report({ project: args.project }) };
577
+ }
578
+
467
579
  function handleSession(args) {
468
580
  try {
469
581
  var session = require('./session');
@@ -482,9 +594,18 @@ function handleSession(args) {
482
594
  return { result: 'Session reset.' };
483
595
  }
484
596
  if (action === 'accept') {
485
- if (!args.persona) return { error: 'persona is required for accept' };
486
- var result = session.markAccepted({ persona: args.persona, accepted: !args.reject });
487
- return { result: JSON.stringify(result) };
597
+ var accept = require('./accept');
598
+ if (args.reject) {
599
+ return { result: JSON.stringify(accept.fix({
600
+ persona: args.persona,
601
+ because: args.because || args.comment || '',
602
+ })) };
603
+ }
604
+ return { result: JSON.stringify(accept.keep({
605
+ persona: args.persona,
606
+ comment: args.comment || '',
607
+ shipped: args.shipped !== false,
608
+ })) };
488
609
  }
489
610
  } catch (e) {
490
611
  return { error: e.message };
@@ -506,18 +627,16 @@ function handleSystemPrompt(args) {
506
627
 
507
628
  // ── Helpers ──
508
629
 
509
- function readCard(id) {
510
- var p = path.join(PACKAGE_ROOT, 'agents', 'cards', id + '.md');
511
- if (!fs.existsSync(p)) return null;
512
- return fs.readFileSync(p, 'utf8');
513
- }
514
-
515
630
  function readSkill(id) {
516
631
  var p = path.join(PACKAGE_ROOT, 'skills', id, 'SKILL.md');
517
632
  if (!fs.existsSync(p)) return null;
518
633
  return fs.readFileSync(p, 'utf8');
519
634
  }
520
635
 
636
+ function readSkillFront(id) {
637
+ return readSkill(id);
638
+ }
639
+
521
640
  // ── MCP config writer ──
522
641
 
523
642
  function mergeMcpServer(configPath) {
@@ -532,6 +651,7 @@ function mergeMcpServer(configPath) {
532
651
  config.mcpServers['analyzthis_design'] = {
533
652
  command: 'npx',
534
653
  args: ['analyzthis_design', 'mcp'],
654
+ env: { ANALYZTHIS_MCP_CATALOG: process.env.ANALYZTHIS_MCP_CATALOG || 'lite' },
535
655
  };
536
656
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
537
657
  return configPath;
@@ -542,13 +662,17 @@ function looksLikeProjectCwd() {
542
662
  return fs.existsSync(path.join(cwd, 'package.json')) || fs.existsSync(path.join(cwd, '.git'));
543
663
  }
544
664
 
545
- function writeMcpConfig(target) {
665
+ function writeMcpConfig(target, opts) {
666
+ opts = opts || {};
667
+ if (opts.catalog) process.env.ANALYZTHIS_MCP_CATALOG = opts.catalog;
546
668
  var home = os.homedir();
547
669
 
548
670
  if (target === 'cursor') {
549
671
  var cursorConfigPath = path.join(home, '.cursor', 'mcp.json');
550
672
  mergeMcpServer(cursorConfigPath);
551
673
  console.log('Cursor MCP config written to ' + cursorConfigPath);
674
+ console.log('Default MCP catalog is lite (router + retrieve + session + receipt).');
675
+ console.log('Full 19-tool list: ANALYZTHIS_MCP_CATALOG=full or mcp --catalog full');
552
676
  console.log('Restart Cursor to activate.\n');
553
677
  return true;
554
678
  }
@@ -575,6 +699,7 @@ function writeMcpConfig(target) {
575
699
  console.log('Project MCP config written to ' + projectMcp);
576
700
  }
577
701
 
702
+ console.log('Default MCP catalog is lite. Full list: ANALYZTHIS_MCP_CATALOG=full');
578
703
  console.log('Restart Claude Code and Claude Desktop to activate.\n');
579
704
  return true;
580
705
  }
@@ -602,7 +727,9 @@ function writeMcpConfig(target) {
602
727
 
603
728
  // ── MCP protocol (JSON-RPC 2.0 over stdio) ────────────────────────────────
604
729
 
605
- function startStdioServer() {
730
+ function startStdioServer(catalog) {
731
+ var catalogMode = resolveCatalog(catalog);
732
+ var activeTools = listedTools(catalogMode);
606
733
  var rl = readline.createInterface({ input: process.stdin, terminal: false });
607
734
  var initialized = false;
608
735
 
@@ -647,7 +774,7 @@ function startStdioServer() {
647
774
  jsonrpc: '2.0',
648
775
  id: id,
649
776
  result: {
650
- tools: TOOLS.map(function(t) {
777
+ tools: activeTools.map(function(t) {
651
778
  return {
652
779
  name: t.name,
653
780
  description: t.description,
@@ -673,11 +800,17 @@ function startStdioServer() {
673
800
  });
674
801
  } else {
675
802
  var text = result.prompt || result.result || result.command || JSON.stringify(result, null, 2);
803
+ var receipt = require('./receipt');
804
+ var wrapped = receipt.wrapPayload(text, {
805
+ path: 'mcp',
806
+ persona: result.persona || result.skill || toolName,
807
+ verdicts: result.verdicts || 0,
808
+ });
676
809
  send({
677
810
  jsonrpc: '2.0',
678
811
  id: id,
679
812
  result: {
680
- content: [{ type: 'text', text: String(text) }],
813
+ content: [{ type: 'text', text: wrapped }],
681
814
  },
682
815
  });
683
816
  }
@@ -711,24 +844,32 @@ function startStdioServer() {
711
844
 
712
845
  if (process.env.MCP_DEBUG) {
713
846
  process.stderr.write('analyzthis_design MCP server v' + VERSION + ' running on stdio\n');
714
- process.stderr.write('Tools: ' + TOOLS.length + ' available\n');
847
+ process.stderr.write('Tools: ' + activeTools.length + ' (' + catalogMode + ' catalog)\n');
715
848
  }
716
849
  }
717
850
 
718
851
  // ── Entry point ──
719
852
 
853
+ function parseCatalogFlag(args) {
854
+ var eq = args.filter(function (a) { return a.indexOf('--catalog=') === 0; })[0];
855
+ if (eq) return eq.split('=').slice(1).join('=');
856
+ var idx = args.indexOf('--catalog');
857
+ if (idx !== -1 && args[idx + 1] && args[idx + 1].indexOf('--') !== 0) return args[idx + 1];
858
+ return null;
859
+ }
860
+
720
861
  function main() {
721
862
  var args = process.argv.slice(2);
863
+ var catalog = parseCatalogFlag(args);
722
864
 
723
865
  // --configure <target>
724
866
  var configureIdx = args.indexOf('--configure');
725
867
  if (configureIdx !== -1 && args[configureIdx + 1]) {
726
- writeMcpConfig(args[configureIdx + 1]);
868
+ writeMcpConfig(args[configureIdx + 1], { catalog: catalog });
727
869
  return;
728
870
  }
729
871
 
730
- // Default: start stdio server
731
- startStdioServer();
872
+ startStdioServer(catalog);
732
873
  }
733
874
 
734
875
  if (require.main === module) {
@@ -737,6 +878,10 @@ if (require.main === module) {
737
878
 
738
879
  module.exports = {
739
880
  TOOLS: TOOLS,
881
+ ROUTER_TOOL: ROUTER_TOOL,
882
+ RECEIPT_TOOL: RECEIPT_TOOL,
883
+ listedTools: listedTools,
884
+ resolveCatalog: resolveCatalog,
740
885
  handleToolCall: handleToolCall,
741
886
  startStdioServer: startStdioServer,
742
887
  writeMcpConfig: writeMcpConfig,
@@ -819,6 +819,8 @@ async function runUnchunked(opts = {}) {
819
819
  objections_raised: delibResult.metrics.objections_raised,
820
820
  objections_resolved: delibResult.metrics.objections_resolved,
821
821
  raj_escalations: delibResult.metrics.raj_escalations,
822
+ verdicts_this_run: 1,
823
+ tokens_per_verdict: (delibResult.metrics.input_tokens_est || 0) || null,
822
824
  },
823
825
  },
824
826
  });
@@ -834,6 +836,8 @@ async function runUnchunked(opts = {}) {
834
836
  if (delibResult.deliberation.raj_escalated) console.log(' Raj escalated: yes');
835
837
  console.log(` Verdict: ${syn.composite.verdict} (${syn.composite.total}/${syn.composite.max_total})`);
836
838
  console.log(` Est. tokens: ${delibResult.metrics.input_tokens_est} in / ${delibResult.metrics.output_tokens_est} out`);
839
+ const tpvUn = delibResult.metrics.input_tokens_est || 0;
840
+ if (tpvUn) console.log(` Tokens per verdict: ${tpvUn} (CLI est. input / 1)`);
837
841
  if (costUsd) console.log(` Est. cost: $${costUsd.toFixed(4)}`);
838
842
  console.log(` Session: ${session.sessionPath(projectId)}`);
839
843
  console.log('\n── Synthesis ──────────────────────────────────────');
@@ -878,6 +882,7 @@ async function run(opts = {}) {
878
882
  console.log(` Cost: $${(chunkRunState.total_cost || 0).toFixed(4)}`);
879
883
  const tokens = chunkRunState.total_tokens || { input: 0, output: 0 };
880
884
  console.log(` Tokens: ${tokens.input} in / ${tokens.output} out`);
885
+ if (tokens.input) console.log(` Tokens per verdict: ${tokens.input} (CLI est. input / 1)`);
881
886
  if (syn) {
882
887
  console.log(` Verdict: ${syn.verdict || 'n/a'} (${syn.total || 0}/${syn.max_total || 0})`);
883
888
  if (syn.premise_challenge) {
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Inferred token receipts for Claude-path (slash / MCP) turns.
5
+ * Never call this a bill. Never multiply into a monthly dollar. Never say verified.
6
+ */
7
+
8
+ var session = require('./session');
9
+
10
+ var HOST_TURNS_CAP = 40;
11
+ var CHARS_PER_TOKEN = 4;
12
+
13
+ function inferTokens(text) {
14
+ return Math.ceil(String(text || '').length / CHARS_PER_TOKEN);
15
+ }
16
+
17
+ function formatFooter(inferredIn) {
18
+ return (
19
+ '\n\n---\nInferred context this turn: ' +
20
+ inferredIn +
21
+ ' tokens (not a bill). Do not invent a dollar figure.'
22
+ );
23
+ }
24
+
25
+ function wrapPayload(text, meta) {
26
+ var body = String(text || '');
27
+ var inferredIn = inferTokens(body);
28
+ try {
29
+ session.logHostTurn({
30
+ path: (meta && meta.path) || 'mcp',
31
+ persona: (meta && meta.persona) || '',
32
+ inferred_in: inferredIn,
33
+ project: meta && meta.project,
34
+ verdicts: (meta && meta.verdicts) || 0,
35
+ });
36
+ } catch (e) {
37
+ // Session write is best-effort — never fail a tool call for a receipt.
38
+ }
39
+ return body + formatFooter(inferredIn);
40
+ }
41
+
42
+ function tokensPerVerdict(state) {
43
+ if (!state || !state.metrics) return null;
44
+ var m = state.metrics;
45
+ var host = (m.host_turns || []).reduce(function (sum, turn) {
46
+ return sum + (turn.inferred_in || 0);
47
+ }, 0);
48
+ var cliIn = m.input_tokens_est || 0;
49
+ var totalIn = host + cliIn;
50
+ var verdicts = m.verdicts_this_run || 0;
51
+ if (!verdicts && (m.experts_run || []).length) verdicts = 1;
52
+ if (!verdicts && (m.host_turns || []).length) verdicts = 1;
53
+ if (!verdicts || !totalIn) return null;
54
+ return Math.round(totalIn / verdicts);
55
+ }
56
+
57
+ function formatReport(state) {
58
+ if (!state) {
59
+ return 'No session found. Run: npx analyzthis_design session init';
60
+ }
61
+ var m = state.metrics || {};
62
+ var turns = m.host_turns || [];
63
+ var tpv = tokensPerVerdict(state);
64
+ var lines = [
65
+ 'Receipt — inferred, not a bill',
66
+ 'Project: ' + (state.project_id || ''),
67
+ 'CLI est. tokens: ' + (m.input_tokens_est || 0) + ' in / ' + (m.output_tokens_est || 0) + ' out',
68
+ 'Host turns logged: ' + turns.length,
69
+ ];
70
+ turns.slice(-8).forEach(function (turn) {
71
+ lines.push(
72
+ ' ' +
73
+ (turn.at || '') +
74
+ ' ' +
75
+ (turn.path || '') +
76
+ ' ' +
77
+ (turn.persona || '-') +
78
+ ' ' +
79
+ (turn.inferred_in || 0) +
80
+ ' tok'
81
+ );
82
+ });
83
+ if (tpv != null) lines.push('Tokens per verdict: ' + tpv + ' (inferred + CLI est. input / verdicts)');
84
+ else lines.push('Tokens per verdict: n/a — no host turn or CLI run yet');
85
+ lines.push('You cannot know Claude’s real usage from slash alone. Do not invent a dollar figure.');
86
+ return lines.join('\n');
87
+ }
88
+
89
+ function report(opts) {
90
+ var state = session.show({ project: opts && opts.project });
91
+ return formatReport(state);
92
+ }
93
+
94
+ module.exports = {
95
+ inferTokens: inferTokens,
96
+ formatFooter: formatFooter,
97
+ wrapPayload: wrapPayload,
98
+ tokensPerVerdict: tokensPerVerdict,
99
+ formatReport: formatReport,
100
+ report: report,
101
+ HOST_TURNS_CAP: HOST_TURNS_CAP,
102
+ };