@massu/core 0.4.2 → 0.6.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.
Files changed (125) hide show
  1. package/README.md +40 -0
  2. package/agents/massu-architecture-reviewer.md +104 -0
  3. package/agents/massu-blast-radius-analyzer.md +84 -0
  4. package/agents/massu-competitive-scorer.md +126 -0
  5. package/agents/massu-help-sync.md +73 -0
  6. package/agents/massu-migration-writer.md +94 -0
  7. package/agents/massu-output-scorer.md +87 -0
  8. package/agents/massu-pattern-reviewer.md +84 -0
  9. package/agents/massu-plan-auditor.md +170 -0
  10. package/agents/massu-schema-sync-verifier.md +70 -0
  11. package/agents/massu-security-reviewer.md +98 -0
  12. package/agents/massu-ux-reviewer.md +106 -0
  13. package/commands/_shared-preamble.md +53 -23
  14. package/commands/_shared-references/auto-learning-protocol.md +71 -0
  15. package/commands/_shared-references/blast-radius-protocol.md +76 -0
  16. package/commands/_shared-references/security-pre-screen.md +64 -0
  17. package/commands/_shared-references/test-first-protocol.md +87 -0
  18. package/commands/_shared-references/verification-table.md +52 -0
  19. package/commands/massu-article-review.md +343 -0
  20. package/commands/massu-autoresearch/references/eval-runner.md +84 -0
  21. package/commands/massu-autoresearch/references/safety-rails.md +125 -0
  22. package/commands/massu-autoresearch/references/scoring-protocol.md +151 -0
  23. package/commands/massu-autoresearch.md +258 -0
  24. package/commands/massu-batch.md +44 -12
  25. package/commands/massu-bearings.md +42 -8
  26. package/commands/massu-checkpoint.md +588 -0
  27. package/commands/massu-ci-fix.md +2 -2
  28. package/commands/massu-command-health.md +132 -0
  29. package/commands/massu-command-improve.md +232 -0
  30. package/commands/massu-commit.md +205 -44
  31. package/commands/massu-create-plan.md +239 -57
  32. package/commands/massu-data/references/common-queries.md +79 -0
  33. package/commands/massu-data/references/table-guide.md +50 -0
  34. package/commands/massu-data.md +66 -0
  35. package/commands/massu-dead-code.md +29 -34
  36. package/commands/massu-debug/references/auto-learning.md +61 -0
  37. package/commands/massu-debug/references/codegraph-tracing.md +80 -0
  38. package/commands/massu-debug/references/common-shortcuts.md +98 -0
  39. package/commands/massu-debug/references/investigation-phases.md +294 -0
  40. package/commands/massu-debug/references/report-format.md +107 -0
  41. package/commands/massu-debug.md +105 -386
  42. package/commands/massu-docs.md +1 -1
  43. package/commands/massu-full-audit.md +61 -0
  44. package/commands/massu-gap-enhancement-analyzer.md +276 -16
  45. package/commands/massu-golden-path/references/approval-points.md +216 -0
  46. package/commands/massu-golden-path/references/competitive-mode.md +273 -0
  47. package/commands/massu-golden-path/references/error-handling.md +121 -0
  48. package/commands/massu-golden-path/references/phase-0-requirements.md +53 -0
  49. package/commands/massu-golden-path/references/phase-1-plan-creation.md +168 -0
  50. package/commands/massu-golden-path/references/phase-2-implementation.md +397 -0
  51. package/commands/massu-golden-path/references/phase-2.5-gap-analyzer.md +156 -0
  52. package/commands/massu-golden-path/references/phase-3-simplify.md +40 -0
  53. package/commands/massu-golden-path/references/phase-4-commit.md +94 -0
  54. package/commands/massu-golden-path/references/phase-5-push.md +116 -0
  55. package/commands/massu-golden-path/references/phase-5.5-production-verify.md +170 -0
  56. package/commands/massu-golden-path/references/phase-6-completion.md +113 -0
  57. package/commands/massu-golden-path/references/qa-evaluator-spec.md +137 -0
  58. package/commands/massu-golden-path/references/sprint-contract-protocol.md +117 -0
  59. package/commands/massu-golden-path/references/vr-visual-calibration.md +73 -0
  60. package/commands/massu-golden-path.md +114 -848
  61. package/commands/massu-guide.md +72 -69
  62. package/commands/massu-hooks.md +27 -12
  63. package/commands/massu-hotfix.md +221 -144
  64. package/commands/massu-incident.md +49 -20
  65. package/commands/massu-infra-audit.md +187 -0
  66. package/commands/massu-learning-audit.md +211 -0
  67. package/commands/massu-loop/references/auto-learning.md +49 -0
  68. package/commands/massu-loop/references/checkpoint-audit.md +40 -0
  69. package/commands/massu-loop/references/guardrails.md +17 -0
  70. package/commands/massu-loop/references/iteration-structure.md +115 -0
  71. package/commands/massu-loop/references/loop-controller.md +188 -0
  72. package/commands/massu-loop/references/plan-extraction.md +78 -0
  73. package/commands/massu-loop/references/vr-plan-spec.md +140 -0
  74. package/commands/massu-loop-playwright.md +9 -9
  75. package/commands/massu-loop.md +115 -670
  76. package/commands/massu-new-pattern.md +423 -0
  77. package/commands/massu-perf.md +422 -0
  78. package/commands/massu-plan-audit.md +1 -1
  79. package/commands/massu-plan.md +389 -122
  80. package/commands/massu-production-verify.md +433 -0
  81. package/commands/massu-push.md +62 -378
  82. package/commands/massu-recap.md +29 -3
  83. package/commands/massu-rollback.md +613 -0
  84. package/commands/massu-scaffold-hook.md +2 -4
  85. package/commands/massu-scaffold-page.md +2 -3
  86. package/commands/massu-scaffold-router.md +1 -2
  87. package/commands/massu-security.md +619 -0
  88. package/commands/massu-simplify.md +115 -85
  89. package/commands/massu-squirrels.md +2 -2
  90. package/commands/massu-tdd.md +38 -22
  91. package/commands/massu-test.md +3 -3
  92. package/commands/massu-type-mismatch-audit.md +469 -0
  93. package/commands/massu-ui-audit.md +587 -0
  94. package/commands/massu-verify-playwright.md +287 -32
  95. package/commands/massu-verify.md +150 -46
  96. package/dist/cli.js +1451 -1047
  97. package/dist/hooks/post-tool-use.js +75 -6
  98. package/dist/hooks/user-prompt.js +16 -0
  99. package/package.json +6 -2
  100. package/patterns/build-patterns.md +302 -0
  101. package/patterns/component-patterns.md +246 -0
  102. package/patterns/display-patterns.md +185 -0
  103. package/patterns/form-patterns.md +890 -0
  104. package/patterns/integration-testing-checklist.md +445 -0
  105. package/patterns/security-patterns.md +219 -0
  106. package/patterns/testing-patterns.md +569 -0
  107. package/patterns/tool-routing.md +81 -0
  108. package/patterns/ui-patterns.md +371 -0
  109. package/protocols/plan-implementation.md +267 -0
  110. package/protocols/recovery.md +225 -0
  111. package/protocols/verification.md +404 -0
  112. package/reference/command-taxonomy.md +178 -0
  113. package/reference/cr-rules-reference.md +76 -0
  114. package/reference/hook-execution-order.md +148 -0
  115. package/reference/lessons-learned.md +175 -0
  116. package/reference/patterns-quickref.md +208 -0
  117. package/reference/standards.md +135 -0
  118. package/reference/subagents-reference.md +17 -0
  119. package/reference/vr-verification-reference.md +867 -0
  120. package/src/commands/init.ts +27 -0
  121. package/src/commands/install-commands.ts +149 -53
  122. package/src/hooks/post-tool-use.ts +17 -0
  123. package/src/hooks/user-prompt.ts +21 -0
  124. package/src/memory-file-ingest.ts +127 -0
  125. package/src/memory-tools.ts +34 -1
package/dist/cli.js CHANGED
@@ -344,687 +344,188 @@ var init_config = __esm({
344
344
  }
345
345
  });
346
346
 
347
- // src/commands/install-commands.ts
348
- var install_commands_exports = {};
349
- __export(install_commands_exports, {
350
- installCommands: () => installCommands,
351
- resolveCommandsDir: () => resolveCommandsDir,
352
- runInstallCommands: () => runInstallCommands
347
+ // src/memory-db.ts
348
+ var memory_db_exports = {};
349
+ __export(memory_db_exports, {
350
+ addConversationTurn: () => addConversationTurn,
351
+ addObservation: () => addObservation,
352
+ addSummary: () => addSummary,
353
+ addToolCallDetail: () => addToolCallDetail,
354
+ addUserPrompt: () => addUserPrompt,
355
+ assignImportance: () => assignImportance,
356
+ autoDetectTaskId: () => autoDetectTaskId,
357
+ createSession: () => createSession,
358
+ deduplicateFailedAttempt: () => deduplicateFailedAttempt,
359
+ dequeuePendingSync: () => dequeuePendingSync,
360
+ endSession: () => endSession,
361
+ enqueueSyncPayload: () => enqueueSyncPayload,
362
+ getConversationTurns: () => getConversationTurns,
363
+ getCrossTaskProgress: () => getCrossTaskProgress,
364
+ getDecisionsAbout: () => getDecisionsAbout,
365
+ getFailedAttempts: () => getFailedAttempts,
366
+ getLastProcessedLine: () => getLastProcessedLine,
367
+ getMemoryDb: () => getMemoryDb,
368
+ getObservabilityDbSize: () => getObservabilityDbSize,
369
+ getRecentObservations: () => getRecentObservations,
370
+ getSessionStats: () => getSessionStats,
371
+ getSessionSummaries: () => getSessionSummaries,
372
+ getSessionTimeline: () => getSessionTimeline,
373
+ getSessionsByTask: () => getSessionsByTask,
374
+ getToolPatterns: () => getToolPatterns,
375
+ incrementRetryCount: () => incrementRetryCount,
376
+ initMemorySchema: () => initMemorySchema,
377
+ linkSessionToTask: () => linkSessionToTask,
378
+ pruneOldConversationTurns: () => pruneOldConversationTurns,
379
+ pruneOldObservations: () => pruneOldObservations,
380
+ removePendingSync: () => removePendingSync,
381
+ sanitizeFts5Query: () => sanitizeFts5Query,
382
+ searchConversationTurns: () => searchConversationTurns,
383
+ searchObservations: () => searchObservations,
384
+ setLastProcessedLine: () => setLastProcessedLine
353
385
  });
354
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync, readdirSync } from "fs";
355
- import { resolve as resolve2, dirname as dirname2 } from "path";
356
- import { fileURLToPath } from "url";
357
- function resolveCommandsDir() {
358
- const cwd = process.cwd();
359
- const nodeModulesPath = resolve2(cwd, "node_modules/@massu/core/commands");
360
- if (existsSync2(nodeModulesPath)) {
361
- return nodeModulesPath;
362
- }
363
- const distRelPath = resolve2(__dirname, "../commands");
364
- if (existsSync2(distRelPath)) {
365
- return distRelPath;
366
- }
367
- const srcRelPath = resolve2(__dirname, "../../commands");
368
- if (existsSync2(srcRelPath)) {
369
- return srcRelPath;
370
- }
371
- return null;
372
- }
373
- function installCommands(projectRoot) {
374
- const claudeDirName = getConfig().conventions?.claudeDirName ?? ".claude";
375
- const targetDir = resolve2(projectRoot, claudeDirName, "commands");
376
- if (!existsSync2(targetDir)) {
377
- mkdirSync(targetDir, { recursive: true });
378
- }
379
- const sourceDir = resolveCommandsDir();
380
- if (!sourceDir) {
381
- console.error(" ERROR: Could not find massu commands directory.");
382
- console.error(" Try reinstalling: npm install @massu/core");
383
- return { installed: 0, updated: 0, skipped: 0, commandsDir: targetDir };
384
- }
385
- const sourceFiles = readdirSync(sourceDir).filter((f) => f.endsWith(".md"));
386
- let installed = 0;
387
- let updated = 0;
388
- let skipped = 0;
389
- for (const file of sourceFiles) {
390
- const sourcePath = resolve2(sourceDir, file);
391
- const targetPath = resolve2(targetDir, file);
392
- const sourceContent = readFileSync2(sourcePath, "utf-8");
393
- if (existsSync2(targetPath)) {
394
- const existingContent = readFileSync2(targetPath, "utf-8");
395
- if (existingContent === sourceContent) {
396
- skipped++;
397
- continue;
398
- }
399
- writeFileSync(targetPath, sourceContent, "utf-8");
400
- updated++;
401
- } else {
402
- writeFileSync(targetPath, sourceContent, "utf-8");
403
- installed++;
404
- }
405
- }
406
- return { installed, updated, skipped, commandsDir: targetDir };
386
+ import Database from "better-sqlite3";
387
+ import { dirname as dirname2, basename } from "path";
388
+ import { existsSync as existsSync2, mkdirSync } from "fs";
389
+ function sanitizeFts5Query(raw) {
390
+ const trimmed = raw.trim();
391
+ if (!trimmed) return '""';
392
+ const tokens = trimmed.replace(/"/g, "").split(/\s+/).filter(Boolean);
393
+ return tokens.map((t) => `"${t}"`).join(" ");
407
394
  }
408
- async function runInstallCommands() {
409
- const projectRoot = process.cwd();
410
- console.log("");
411
- console.log("Massu AI - Install Slash Commands");
412
- console.log("==================================");
413
- console.log("");
414
- const result = installCommands(projectRoot);
415
- if (result.installed > 0) {
416
- console.log(` Installed ${result.installed} new commands`);
417
- }
418
- if (result.updated > 0) {
419
- console.log(` Updated ${result.updated} existing commands`);
420
- }
421
- if (result.skipped > 0) {
422
- console.log(` ${result.skipped} commands already up to date`);
395
+ function getMemoryDb() {
396
+ const dbPath = getResolvedPaths().memoryDbPath;
397
+ const dir = dirname2(dbPath);
398
+ if (!existsSync2(dir)) {
399
+ mkdirSync(dir, { recursive: true });
423
400
  }
424
- const total = result.installed + result.updated + result.skipped;
425
- console.log("");
426
- console.log(` ${total} slash commands available in ${result.commandsDir}`);
427
- console.log("");
428
- console.log(" Restart your Claude Code session to use them.");
429
- console.log("");
401
+ const db = new Database(dbPath);
402
+ db.pragma("journal_mode = WAL");
403
+ db.pragma("foreign_keys = ON");
404
+ initMemorySchema(db);
405
+ return db;
430
406
  }
431
- var __filename, __dirname;
432
- var init_install_commands = __esm({
433
- "src/commands/install-commands.ts"() {
434
- "use strict";
435
- init_config();
436
- __filename = fileURLToPath(import.meta.url);
437
- __dirname = dirname2(__filename);
438
- }
439
- });
407
+ function initMemorySchema(db) {
408
+ db.exec(`
409
+ -- Sessions table (linked to Claude Code session IDs)
410
+ CREATE TABLE IF NOT EXISTS sessions (
411
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
412
+ session_id TEXT UNIQUE NOT NULL,
413
+ project TEXT NOT NULL DEFAULT 'my-project',
414
+ git_branch TEXT,
415
+ started_at TEXT NOT NULL,
416
+ started_at_epoch INTEGER NOT NULL,
417
+ ended_at TEXT,
418
+ ended_at_epoch INTEGER,
419
+ status TEXT CHECK(status IN ('active', 'completed', 'abandoned')) NOT NULL DEFAULT 'active',
420
+ plan_file TEXT,
421
+ plan_phase TEXT,
422
+ task_id TEXT
423
+ );
440
424
 
441
- // src/commands/init.ts
442
- var init_exports = {};
443
- __export(init_exports, {
444
- buildHooksConfig: () => buildHooksConfig,
445
- detectFramework: () => detectFramework,
446
- detectPython: () => detectPython,
447
- generateConfig: () => generateConfig,
448
- initMemoryDir: () => initMemoryDir,
449
- installHooks: () => installHooks,
450
- registerMcpServer: () => registerMcpServer,
451
- resolveHooksDir: () => resolveHooksDir,
452
- runInit: () => runInit
453
- });
454
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2 } from "fs";
455
- import { resolve as resolve3, basename, dirname as dirname3 } from "path";
456
- import { fileURLToPath as fileURLToPath2 } from "url";
457
- import { homedir as homedir2 } from "os";
458
- import { stringify as yamlStringify } from "yaml";
459
- function detectFramework(projectRoot) {
460
- const result = {
461
- type: "javascript",
462
- router: "none",
463
- orm: "none",
464
- ui: "none"
465
- };
466
- const pkgPath = resolve3(projectRoot, "package.json");
467
- if (!existsSync3(pkgPath)) return result;
425
+ CREATE INDEX IF NOT EXISTS idx_sessions_session_id ON sessions(session_id);
426
+ CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at_epoch DESC);
427
+ CREATE INDEX IF NOT EXISTS idx_sessions_task_id ON sessions(task_id);
428
+
429
+ -- Observations table (structured knowledge from tool usage)
430
+ CREATE TABLE IF NOT EXISTS observations (
431
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
432
+ session_id TEXT NOT NULL,
433
+ type TEXT NOT NULL CHECK(type IN (
434
+ 'decision', 'bugfix', 'feature', 'refactor', 'discovery',
435
+ 'cr_violation', 'vr_check', 'pattern_compliance', 'failed_attempt',
436
+ 'file_change', 'incident_near_miss'
437
+ )),
438
+ title TEXT NOT NULL,
439
+ detail TEXT,
440
+ files_involved TEXT DEFAULT '[]',
441
+ plan_item TEXT,
442
+ cr_rule TEXT,
443
+ vr_type TEXT,
444
+ evidence TEXT,
445
+ importance INTEGER NOT NULL DEFAULT 3 CHECK(importance BETWEEN 1 AND 5),
446
+ recurrence_count INTEGER NOT NULL DEFAULT 1,
447
+ original_tokens INTEGER DEFAULT 0,
448
+ created_at TEXT NOT NULL,
449
+ created_at_epoch INTEGER NOT NULL,
450
+ FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
451
+ );
452
+
453
+ CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
454
+ CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
455
+ CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch DESC);
456
+ CREATE INDEX IF NOT EXISTS idx_observations_plan_item ON observations(plan_item);
457
+ CREATE INDEX IF NOT EXISTS idx_observations_cr_rule ON observations(cr_rule);
458
+ CREATE INDEX IF NOT EXISTS idx_observations_importance ON observations(importance DESC);
459
+ `);
468
460
  try {
469
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
470
- const allDeps = {
471
- ...pkg.dependencies,
472
- ...pkg.devDependencies
473
- };
474
- if (allDeps["typescript"]) result.type = "typescript";
475
- if (allDeps["next"]) result.ui = "nextjs";
476
- else if (allDeps["@sveltejs/kit"]) result.ui = "sveltekit";
477
- else if (allDeps["nuxt"]) result.ui = "nuxt";
478
- else if (allDeps["@angular/core"]) result.ui = "angular";
479
- else if (allDeps["vue"]) result.ui = "vue";
480
- else if (allDeps["react"]) result.ui = "react";
481
- if (allDeps["@trpc/server"]) result.router = "trpc";
482
- else if (allDeps["graphql"] || allDeps["@apollo/server"]) result.router = "graphql";
483
- else if (allDeps["express"] || allDeps["fastify"] || allDeps["hono"]) result.router = "rest";
484
- if (allDeps["@prisma/client"] || allDeps["prisma"]) result.orm = "prisma";
485
- else if (allDeps["drizzle-orm"]) result.orm = "drizzle";
486
- else if (allDeps["typeorm"]) result.orm = "typeorm";
487
- else if (allDeps["sequelize"]) result.orm = "sequelize";
488
- else if (allDeps["mongoose"]) result.orm = "mongoose";
489
- } catch {
461
+ db.exec(`
462
+ CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
463
+ title, detail, evidence,
464
+ content='observations',
465
+ content_rowid='id'
466
+ );
467
+ `);
468
+ } catch (_e) {
490
469
  }
491
- return result;
492
- }
493
- function detectPython(projectRoot) {
494
- const result = {
495
- detected: false,
496
- root: "",
497
- hasFastapi: false,
498
- hasSqlalchemy: false,
499
- hasAlembic: false,
500
- alembicDir: null
501
- };
502
- const markers = ["pyproject.toml", "setup.py", "requirements.txt", "Pipfile"];
503
- const hasMarker = markers.some((m) => existsSync3(resolve3(projectRoot, m)));
504
- if (!hasMarker) return result;
505
- result.detected = true;
506
- const depFiles = [
507
- { file: "pyproject.toml", parser: parsePyprojectDeps },
508
- { file: "requirements.txt", parser: parseRequirementsDeps },
509
- { file: "setup.py", parser: parseSetupPyDeps },
510
- { file: "Pipfile", parser: parsePipfileDeps }
511
- ];
512
- for (const { file, parser } of depFiles) {
513
- const filePath = resolve3(projectRoot, file);
514
- if (existsSync3(filePath)) {
515
- try {
516
- const content = readFileSync3(filePath, "utf-8");
517
- const deps = parser(content);
518
- if (deps.includes("fastapi")) result.hasFastapi = true;
519
- if (deps.includes("sqlalchemy")) result.hasSqlalchemy = true;
520
- } catch {
521
- }
522
- }
523
- }
524
- if (existsSync3(resolve3(projectRoot, "alembic.ini"))) {
525
- result.hasAlembic = true;
526
- if (existsSync3(resolve3(projectRoot, "alembic"))) {
527
- result.alembicDir = "alembic";
528
- }
529
- } else if (existsSync3(resolve3(projectRoot, "alembic"))) {
530
- result.hasAlembic = true;
531
- result.alembicDir = "alembic";
532
- }
533
- const candidateRoots = ["app", "src", "backend", "api"];
534
- for (const candidate of candidateRoots) {
535
- const candidatePath = resolve3(projectRoot, candidate);
536
- if (existsSync3(candidatePath) && existsSync3(resolve3(candidatePath, "__init__.py"))) {
537
- result.root = candidate;
538
- break;
539
- }
540
- if (existsSync3(candidatePath)) {
541
- try {
542
- const files = readdirSync2(candidatePath);
543
- if (files.some((f) => f.endsWith(".py"))) {
544
- result.root = candidate;
545
- break;
546
- }
547
- } catch {
548
- }
549
- }
550
- }
551
- if (!result.root) {
552
- result.root = ".";
553
- }
554
- return result;
555
- }
556
- function parsePyprojectDeps(content) {
557
- const deps = [];
558
- const lower = content.toLowerCase();
559
- if (lower.includes("fastapi")) deps.push("fastapi");
560
- if (lower.includes("sqlalchemy")) deps.push("sqlalchemy");
561
- return deps;
562
- }
563
- function parseRequirementsDeps(content) {
564
- const deps = [];
565
- const lower = content.toLowerCase();
566
- for (const line of lower.split("\n")) {
567
- const trimmed = line.trim();
568
- if (trimmed.startsWith("fastapi")) deps.push("fastapi");
569
- if (trimmed.startsWith("sqlalchemy")) deps.push("sqlalchemy");
570
- }
571
- return deps;
572
- }
573
- function parseSetupPyDeps(content) {
574
- const deps = [];
575
- const lower = content.toLowerCase();
576
- if (lower.includes("fastapi")) deps.push("fastapi");
577
- if (lower.includes("sqlalchemy")) deps.push("sqlalchemy");
578
- return deps;
579
- }
580
- function parsePipfileDeps(content) {
581
- const deps = [];
582
- const lower = content.toLowerCase();
583
- if (lower.includes("fastapi")) deps.push("fastapi");
584
- if (lower.includes("sqlalchemy")) deps.push("sqlalchemy");
585
- return deps;
586
- }
587
- function generateConfig(projectRoot, framework) {
588
- const configPath = resolve3(projectRoot, "massu.config.yaml");
589
- if (existsSync3(configPath)) {
590
- return false;
591
- }
592
- const projectName = basename(projectRoot);
593
- const config = {
594
- project: {
595
- name: projectName,
596
- root: "auto"
597
- },
598
- framework: {
599
- type: framework.type,
600
- router: framework.router,
601
- orm: framework.orm,
602
- ui: framework.ui
603
- },
604
- paths: {
605
- source: "src",
606
- aliases: { "@": "src" }
607
- },
608
- toolPrefix: "massu",
609
- domains: [],
610
- rules: [
611
- {
612
- pattern: "src/**/*.ts",
613
- rules: ["Use ESM imports, not CommonJS"]
614
- }
615
- ]
616
- };
617
- const python = detectPython(projectRoot);
618
- if (python.detected) {
619
- const pythonConfig = {
620
- root: python.root,
621
- exclude_dirs: ["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"]
622
- };
623
- if (python.hasFastapi) pythonConfig.framework = "fastapi";
624
- if (python.hasSqlalchemy) pythonConfig.orm = "sqlalchemy";
625
- if (python.hasAlembic && python.alembicDir) {
626
- pythonConfig.alembic_dir = python.alembicDir;
627
- }
628
- config.python = pythonConfig;
629
- }
630
- const yamlContent = `# Massu AI Configuration
631
- # Generated by: npx massu init
632
- # Documentation: https://massu.ai/docs/getting-started/configuration
633
-
634
- ${yamlStringify(config)}`;
635
- writeFileSync2(configPath, yamlContent, "utf-8");
636
- return true;
637
- }
638
- function registerMcpServer(projectRoot) {
639
- const mcpPath = resolve3(projectRoot, ".mcp.json");
640
- let existing = {};
641
- if (existsSync3(mcpPath)) {
642
- try {
643
- existing = JSON.parse(readFileSync3(mcpPath, "utf-8"));
644
- } catch {
645
- existing = {};
646
- }
647
- }
648
- const servers = existing.mcpServers ?? {};
649
- if (servers.massu) {
650
- return false;
651
- }
652
- servers.massu = {
653
- type: "stdio",
654
- command: "npx",
655
- args: ["-y", "@massu/core"]
656
- };
657
- existing.mcpServers = servers;
658
- writeFileSync2(mcpPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
659
- return true;
660
- }
661
- function resolveHooksDir() {
662
- const cwd = process.cwd();
663
- const nodeModulesPath = resolve3(cwd, "node_modules/@massu/core/dist/hooks");
664
- if (existsSync3(nodeModulesPath)) {
665
- return "node_modules/@massu/core/dist/hooks";
666
- }
667
- const localPath = resolve3(__dirname2, "../dist/hooks");
668
- if (existsSync3(localPath)) {
669
- return localPath;
670
- }
671
- return "node_modules/@massu/core/dist/hooks";
672
- }
673
- function hookCmd(hooksDir, hookFile) {
674
- return `node ${hooksDir}/${hookFile}`;
675
- }
676
- function buildHooksConfig(hooksDir) {
677
- return {
678
- SessionStart: [
679
- {
680
- hooks: [
681
- { type: "command", command: hookCmd(hooksDir, "session-start.js"), timeout: 10 }
682
- ]
683
- }
684
- ],
685
- PreToolUse: [
686
- {
687
- matcher: "Bash",
688
- hooks: [
689
- { type: "command", command: hookCmd(hooksDir, "security-gate.js"), timeout: 5 }
690
- ]
691
- },
692
- {
693
- matcher: "Bash|Write",
694
- hooks: [
695
- { type: "command", command: hookCmd(hooksDir, "pre-delete-check.js"), timeout: 5 }
696
- ]
697
- }
698
- ],
699
- PostToolUse: [
700
- {
701
- hooks: [
702
- { type: "command", command: hookCmd(hooksDir, "post-tool-use.js"), timeout: 10 },
703
- { type: "command", command: hookCmd(hooksDir, "quality-event.js"), timeout: 5 },
704
- { type: "command", command: hookCmd(hooksDir, "cost-tracker.js"), timeout: 5 }
705
- ]
706
- },
707
- {
708
- matcher: "Edit|Write",
709
- hooks: [
710
- { type: "command", command: hookCmd(hooksDir, "post-edit-context.js"), timeout: 5 }
711
- ]
712
- }
713
- ],
714
- Stop: [
715
- {
716
- hooks: [
717
- { type: "command", command: hookCmd(hooksDir, "session-end.js"), timeout: 15 }
718
- ]
719
- }
720
- ],
721
- PreCompact: [
722
- {
723
- hooks: [
724
- { type: "command", command: hookCmd(hooksDir, "pre-compact.js"), timeout: 10 }
725
- ]
726
- }
727
- ],
728
- UserPromptSubmit: [
729
- {
730
- hooks: [
731
- { type: "command", command: hookCmd(hooksDir, "user-prompt.js"), timeout: 5 },
732
- { type: "command", command: hookCmd(hooksDir, "intent-suggester.js"), timeout: 5 }
733
- ]
734
- }
735
- ]
736
- };
737
- }
738
- function installHooks(projectRoot) {
739
- const claudeDirName = getConfig().conventions?.claudeDirName ?? ".claude";
740
- const claudeDir = resolve3(projectRoot, claudeDirName);
741
- const settingsPath = resolve3(claudeDir, "settings.local.json");
742
- if (!existsSync3(claudeDir)) {
743
- mkdirSync2(claudeDir, { recursive: true });
744
- }
745
- let settings = {};
746
- if (existsSync3(settingsPath)) {
747
- try {
748
- settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
749
- } catch {
750
- settings = {};
751
- }
752
- }
753
- const hooksDir = resolveHooksDir();
754
- const hooksConfig = buildHooksConfig(hooksDir);
755
- let hookCount = 0;
756
- for (const groups of Object.values(hooksConfig)) {
757
- for (const group of groups) {
758
- hookCount += group.hooks.length;
759
- }
760
- }
761
- settings.hooks = hooksConfig;
762
- writeFileSync2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
763
- return { installed: true, count: hookCount };
764
- }
765
- function initMemoryDir(projectRoot) {
766
- const encodedRoot = "-" + projectRoot.replace(/\//g, "-");
767
- const memoryDir = resolve3(homedir2(), `.claude/projects/${encodedRoot}/memory`);
768
- let created = false;
769
- if (!existsSync3(memoryDir)) {
770
- mkdirSync2(memoryDir, { recursive: true });
771
- created = true;
772
- }
773
- const memoryMdPath = resolve3(memoryDir, "MEMORY.md");
774
- let memoryMdCreated = false;
775
- if (!existsSync3(memoryMdPath)) {
776
- const projectName = basename(projectRoot);
777
- const memoryContent = `# ${projectName} - Massu Memory
778
-
779
- ## Key Learnings
780
- <!-- Important patterns and conventions discovered during development -->
781
-
782
- ## Common Gotchas
783
- <!-- Non-obvious issues and how to avoid them -->
784
-
785
- ## Corrections
786
- <!-- Wrong behaviors that were corrected and how to prevent them -->
787
-
788
- ## File Index
789
- <!-- Significant files and directories -->
790
- `;
791
- writeFileSync2(memoryMdPath, memoryContent, "utf-8");
792
- memoryMdCreated = true;
793
- }
794
- return { created, memoryMdCreated };
795
- }
796
- async function runInit() {
797
- const projectRoot = process.cwd();
798
- console.log("");
799
- console.log("Massu AI - Project Setup");
800
- console.log("========================");
801
- console.log("");
802
- const framework = detectFramework(projectRoot);
803
- const frameworkParts = [];
804
- if (framework.type !== "javascript") frameworkParts.push(capitalize(framework.type));
805
- if (framework.ui !== "none") frameworkParts.push(formatName(framework.ui));
806
- if (framework.orm !== "none") frameworkParts.push(capitalize(framework.orm));
807
- if (framework.router !== "none") frameworkParts.push(framework.router.toUpperCase());
808
- const detected = frameworkParts.length > 0 ? frameworkParts.join(", ") : "JavaScript";
809
- console.log(` Detected: ${detected}`);
810
- const python = detectPython(projectRoot);
811
- if (python.detected) {
812
- const pyParts = ["Python"];
813
- if (python.hasFastapi) pyParts.push("FastAPI");
814
- if (python.hasSqlalchemy) pyParts.push("SQLAlchemy");
815
- if (python.hasAlembic) pyParts.push("Alembic");
816
- console.log(` Detected: ${pyParts.join(", ")} (root: ${python.root})`);
817
- }
818
- const configCreated = generateConfig(projectRoot, framework);
819
- if (configCreated) {
820
- console.log(" Created massu.config.yaml");
821
- } else {
822
- console.log(" massu.config.yaml already exists (preserved)");
823
- }
824
- const mcpRegistered = registerMcpServer(projectRoot);
825
- if (mcpRegistered) {
826
- console.log(" Registered MCP server in .mcp.json");
827
- } else {
828
- console.log(" MCP server already registered in .mcp.json");
829
- }
830
- const { count: hooksCount } = installHooks(projectRoot);
831
- console.log(` Installed ${hooksCount} hooks in .claude/settings.local.json`);
832
- const cmdResult = installCommands(projectRoot);
833
- const cmdTotal = cmdResult.installed + cmdResult.updated + cmdResult.skipped;
834
- if (cmdResult.installed > 0 || cmdResult.updated > 0) {
835
- console.log(` Installed ${cmdTotal} slash commands (${cmdResult.installed} new, ${cmdResult.updated} updated)`);
836
- } else {
837
- console.log(` ${cmdTotal} slash commands already up to date`);
838
- }
839
- const { created: memDirCreated, memoryMdCreated } = initMemoryDir(projectRoot);
840
- if (memDirCreated) {
841
- console.log(" Created memory directory (~/.claude/projects/.../memory/)");
842
- } else {
843
- console.log(" Memory directory already exists");
844
- }
845
- if (memoryMdCreated) {
846
- console.log(" Created initial MEMORY.md");
847
- }
848
- console.log(" Databases will auto-create on first session");
849
- console.log("");
850
- console.log("Massu AI is ready. Start a Claude Code session to begin.");
851
- console.log("");
852
- console.log("Next steps:");
853
- console.log(" claude # Start a session (hooks activate automatically)");
854
- console.log(" npx massu doctor # Verify installation health");
855
- console.log("");
856
- console.log("Documentation: https://massu.ai/docs");
857
- console.log("");
858
- }
859
- function capitalize(str) {
860
- return str.charAt(0).toUpperCase() + str.slice(1);
861
- }
862
- function formatName(name) {
863
- const names = {
864
- nextjs: "Next.js",
865
- sveltekit: "SvelteKit",
866
- nuxt: "Nuxt",
867
- angular: "Angular",
868
- vue: "Vue",
869
- react: "React"
870
- };
871
- return names[name] ?? capitalize(name);
872
- }
873
- var __filename2, __dirname2;
874
- var init_init = __esm({
875
- "src/commands/init.ts"() {
876
- "use strict";
877
- init_config();
878
- init_install_commands();
879
- __filename2 = fileURLToPath2(import.meta.url);
880
- __dirname2 = dirname3(__filename2);
881
- }
882
- });
883
-
884
- // src/memory-db.ts
885
- import Database from "better-sqlite3";
886
- import { dirname as dirname4, basename as basename2 } from "path";
887
- import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
888
- function sanitizeFts5Query(raw) {
889
- const trimmed = raw.trim();
890
- if (!trimmed) return '""';
891
- const tokens = trimmed.replace(/"/g, "").split(/\s+/).filter(Boolean);
892
- return tokens.map((t) => `"${t}"`).join(" ");
893
- }
894
- function getMemoryDb() {
895
- const dbPath = getResolvedPaths().memoryDbPath;
896
- const dir = dirname4(dbPath);
897
- if (!existsSync4(dir)) {
898
- mkdirSync3(dir, { recursive: true });
899
- }
900
- const db = new Database(dbPath);
901
- db.pragma("journal_mode = WAL");
902
- db.pragma("foreign_keys = ON");
903
- initMemorySchema(db);
904
- return db;
905
- }
906
- function initMemorySchema(db) {
907
- db.exec(`
908
- -- Sessions table (linked to Claude Code session IDs)
909
- CREATE TABLE IF NOT EXISTS sessions (
910
- id INTEGER PRIMARY KEY AUTOINCREMENT,
911
- session_id TEXT UNIQUE NOT NULL,
912
- project TEXT NOT NULL DEFAULT 'my-project',
913
- git_branch TEXT,
914
- started_at TEXT NOT NULL,
915
- started_at_epoch INTEGER NOT NULL,
916
- ended_at TEXT,
917
- ended_at_epoch INTEGER,
918
- status TEXT CHECK(status IN ('active', 'completed', 'abandoned')) NOT NULL DEFAULT 'active',
919
- plan_file TEXT,
920
- plan_phase TEXT,
921
- task_id TEXT
922
- );
923
-
924
- CREATE INDEX IF NOT EXISTS idx_sessions_session_id ON sessions(session_id);
925
- CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at_epoch DESC);
926
- CREATE INDEX IF NOT EXISTS idx_sessions_task_id ON sessions(task_id);
927
-
928
- -- Observations table (structured knowledge from tool usage)
929
- CREATE TABLE IF NOT EXISTS observations (
930
- id INTEGER PRIMARY KEY AUTOINCREMENT,
931
- session_id TEXT NOT NULL,
932
- type TEXT NOT NULL CHECK(type IN (
933
- 'decision', 'bugfix', 'feature', 'refactor', 'discovery',
934
- 'cr_violation', 'vr_check', 'pattern_compliance', 'failed_attempt',
935
- 'file_change', 'incident_near_miss'
936
- )),
937
- title TEXT NOT NULL,
938
- detail TEXT,
939
- files_involved TEXT DEFAULT '[]',
940
- plan_item TEXT,
941
- cr_rule TEXT,
942
- vr_type TEXT,
943
- evidence TEXT,
944
- importance INTEGER NOT NULL DEFAULT 3 CHECK(importance BETWEEN 1 AND 5),
945
- recurrence_count INTEGER NOT NULL DEFAULT 1,
946
- original_tokens INTEGER DEFAULT 0,
947
- created_at TEXT NOT NULL,
948
- created_at_epoch INTEGER NOT NULL,
949
- FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
950
- );
951
-
952
- CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
953
- CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
954
- CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch DESC);
955
- CREATE INDEX IF NOT EXISTS idx_observations_plan_item ON observations(plan_item);
956
- CREATE INDEX IF NOT EXISTS idx_observations_cr_rule ON observations(cr_rule);
957
- CREATE INDEX IF NOT EXISTS idx_observations_importance ON observations(importance DESC);
958
- `);
959
- try {
960
- db.exec(`
961
- CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
962
- title, detail, evidence,
963
- content='observations',
964
- content_rowid='id'
965
- );
966
- `);
967
- } catch (_e) {
968
- }
969
- db.exec(`
970
- CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
971
- INSERT INTO observations_fts(rowid, title, detail, evidence)
972
- VALUES (new.id, new.title, new.detail, new.evidence);
973
- END;
974
-
975
- CREATE TRIGGER IF NOT EXISTS observations_ad AFTER DELETE ON observations BEGIN
976
- INSERT INTO observations_fts(observations_fts, rowid, title, detail, evidence)
977
- VALUES ('delete', old.id, old.title, old.detail, old.evidence);
978
- END;
979
-
980
- CREATE TRIGGER IF NOT EXISTS observations_au AFTER UPDATE ON observations BEGIN
981
- INSERT INTO observations_fts(observations_fts, rowid, title, detail, evidence)
982
- VALUES ('delete', old.id, old.title, old.detail, old.evidence);
983
- INSERT INTO observations_fts(rowid, title, detail, evidence)
984
- VALUES (new.id, new.title, new.detail, new.evidence);
985
- END;
986
- `);
987
- db.exec(`
988
- CREATE TABLE IF NOT EXISTS session_summaries (
989
- id INTEGER PRIMARY KEY AUTOINCREMENT,
990
- session_id TEXT NOT NULL,
991
- request TEXT,
992
- investigated TEXT,
993
- decisions TEXT,
994
- completed TEXT,
995
- failed_attempts TEXT,
996
- next_steps TEXT,
997
- files_created TEXT DEFAULT '[]',
998
- files_modified TEXT DEFAULT '[]',
999
- verification_results TEXT DEFAULT '{}',
1000
- plan_progress TEXT DEFAULT '{}',
1001
- created_at TEXT NOT NULL,
1002
- created_at_epoch INTEGER NOT NULL,
1003
- FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
1004
- );
1005
-
1006
- CREATE INDEX IF NOT EXISTS idx_summaries_session ON session_summaries(session_id);
1007
- `);
1008
- db.exec(`
1009
- CREATE TABLE IF NOT EXISTS user_prompts (
1010
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1011
- session_id TEXT NOT NULL,
1012
- prompt_text TEXT NOT NULL,
1013
- prompt_number INTEGER NOT NULL DEFAULT 1,
1014
- created_at TEXT NOT NULL,
1015
- created_at_epoch INTEGER NOT NULL,
1016
- FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
1017
- );
1018
- `);
1019
- try {
1020
- db.exec(`
1021
- CREATE VIRTUAL TABLE IF NOT EXISTS user_prompts_fts USING fts5(
1022
- prompt_text,
1023
- content='user_prompts',
1024
- content_rowid='id'
1025
- );
1026
- `);
1027
- } catch (_e) {
470
+ db.exec(`
471
+ CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
472
+ INSERT INTO observations_fts(rowid, title, detail, evidence)
473
+ VALUES (new.id, new.title, new.detail, new.evidence);
474
+ END;
475
+
476
+ CREATE TRIGGER IF NOT EXISTS observations_ad AFTER DELETE ON observations BEGIN
477
+ INSERT INTO observations_fts(observations_fts, rowid, title, detail, evidence)
478
+ VALUES ('delete', old.id, old.title, old.detail, old.evidence);
479
+ END;
480
+
481
+ CREATE TRIGGER IF NOT EXISTS observations_au AFTER UPDATE ON observations BEGIN
482
+ INSERT INTO observations_fts(observations_fts, rowid, title, detail, evidence)
483
+ VALUES ('delete', old.id, old.title, old.detail, old.evidence);
484
+ INSERT INTO observations_fts(rowid, title, detail, evidence)
485
+ VALUES (new.id, new.title, new.detail, new.evidence);
486
+ END;
487
+ `);
488
+ db.exec(`
489
+ CREATE TABLE IF NOT EXISTS session_summaries (
490
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
491
+ session_id TEXT NOT NULL,
492
+ request TEXT,
493
+ investigated TEXT,
494
+ decisions TEXT,
495
+ completed TEXT,
496
+ failed_attempts TEXT,
497
+ next_steps TEXT,
498
+ files_created TEXT DEFAULT '[]',
499
+ files_modified TEXT DEFAULT '[]',
500
+ verification_results TEXT DEFAULT '{}',
501
+ plan_progress TEXT DEFAULT '{}',
502
+ created_at TEXT NOT NULL,
503
+ created_at_epoch INTEGER NOT NULL,
504
+ FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
505
+ );
506
+
507
+ CREATE INDEX IF NOT EXISTS idx_summaries_session ON session_summaries(session_id);
508
+ `);
509
+ db.exec(`
510
+ CREATE TABLE IF NOT EXISTS user_prompts (
511
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
512
+ session_id TEXT NOT NULL,
513
+ prompt_text TEXT NOT NULL,
514
+ prompt_number INTEGER NOT NULL DEFAULT 1,
515
+ created_at TEXT NOT NULL,
516
+ created_at_epoch INTEGER NOT NULL,
517
+ FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
518
+ );
519
+ `);
520
+ try {
521
+ db.exec(`
522
+ CREATE VIRTUAL TABLE IF NOT EXISTS user_prompts_fts USING fts5(
523
+ prompt_text,
524
+ content='user_prompts',
525
+ content_rowid='id'
526
+ );
527
+ `);
528
+ } catch (_e) {
1028
529
  }
1029
530
  db.exec(`
1030
531
  CREATE TRIGGER IF NOT EXISTS prompts_ai AFTER INSERT ON user_prompts BEGIN
@@ -1378,6 +879,29 @@ function initMemorySchema(db) {
1378
879
  );
1379
880
  `);
1380
881
  }
882
+ function enqueueSyncPayload(db, payload) {
883
+ db.prepare("INSERT INTO pending_sync (payload) VALUES (?)").run(payload);
884
+ }
885
+ function dequeuePendingSync(db, limit = 10) {
886
+ const stale = db.prepare(
887
+ "SELECT id FROM pending_sync WHERE retry_count >= 10"
888
+ ).all();
889
+ if (stale.length > 0) {
890
+ const ids = stale.map((s) => s.id);
891
+ db.prepare(`DELETE FROM pending_sync WHERE id IN (${ids.map(() => "?").join(",")})`).run(...ids);
892
+ }
893
+ return db.prepare(
894
+ "SELECT id, payload, retry_count FROM pending_sync ORDER BY created_at ASC LIMIT ?"
895
+ ).all(limit);
896
+ }
897
+ function removePendingSync(db, id) {
898
+ db.prepare("DELETE FROM pending_sync WHERE id = ?").run(id);
899
+ }
900
+ function incrementRetryCount(db, id, error) {
901
+ db.prepare(
902
+ "UPDATE pending_sync SET retry_count = retry_count + 1, last_error = ? WHERE id = ?"
903
+ ).run(error, id);
904
+ }
1381
905
  function assignImportance(type, vrResult) {
1382
906
  switch (type) {
1383
907
  case "decision":
@@ -1399,230 +923,1082 @@ function assignImportance(type, vrResult) {
1399
923
  case "discovery":
1400
924
  return 1;
1401
925
  default:
1402
- return 3;
926
+ return 3;
927
+ }
928
+ }
929
+ function autoDetectTaskId(planFile) {
930
+ if (!planFile) return null;
931
+ const base = basename(planFile);
932
+ return base.replace(/\.md$/, "");
933
+ }
934
+ function createSession(db, sessionId, opts) {
935
+ const now = /* @__PURE__ */ new Date();
936
+ const taskId = autoDetectTaskId(opts?.planFile);
937
+ db.prepare(`
938
+ INSERT OR IGNORE INTO sessions (session_id, git_branch, plan_file, task_id, started_at, started_at_epoch)
939
+ VALUES (?, ?, ?, ?, ?, ?)
940
+ `).run(sessionId, opts?.branch ?? null, opts?.planFile ?? null, taskId, now.toISOString(), Math.floor(now.getTime() / 1e3));
941
+ }
942
+ function endSession(db, sessionId, status = "completed") {
943
+ const now = /* @__PURE__ */ new Date();
944
+ db.prepare(`
945
+ UPDATE sessions SET status = ?, ended_at = ?, ended_at_epoch = ? WHERE session_id = ?
946
+ `).run(status, now.toISOString(), Math.floor(now.getTime() / 1e3), sessionId);
947
+ }
948
+ function addObservation(db, sessionId, type, title, detail, opts) {
949
+ const now = /* @__PURE__ */ new Date();
950
+ const importance = opts?.importance ?? assignImportance(type, opts?.evidence?.includes("PASS") ? "PASS" : void 0);
951
+ const result = db.prepare(`
952
+ INSERT INTO observations (session_id, type, title, detail, files_involved, plan_item, cr_rule, vr_type, evidence, importance, original_tokens, created_at, created_at_epoch)
953
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
954
+ `).run(
955
+ sessionId,
956
+ type,
957
+ title,
958
+ detail,
959
+ JSON.stringify(opts?.filesInvolved ?? []),
960
+ opts?.planItem ?? null,
961
+ opts?.crRule ?? null,
962
+ opts?.vrType ?? null,
963
+ opts?.evidence ?? null,
964
+ importance,
965
+ opts?.originalTokens ?? 0,
966
+ now.toISOString(),
967
+ Math.floor(now.getTime() / 1e3)
968
+ );
969
+ return Number(result.lastInsertRowid);
970
+ }
971
+ function addSummary(db, sessionId, summary) {
972
+ const now = /* @__PURE__ */ new Date();
973
+ db.prepare(`
974
+ INSERT INTO session_summaries (session_id, request, investigated, decisions, completed, failed_attempts, next_steps, files_created, files_modified, verification_results, plan_progress, created_at, created_at_epoch)
975
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
976
+ `).run(
977
+ sessionId,
978
+ summary.request ?? null,
979
+ summary.investigated ?? null,
980
+ summary.decisions ?? null,
981
+ summary.completed ?? null,
982
+ summary.failedAttempts ?? null,
983
+ summary.nextSteps ?? null,
984
+ JSON.stringify(summary.filesCreated ?? []),
985
+ JSON.stringify(summary.filesModified ?? []),
986
+ JSON.stringify(summary.verificationResults ?? {}),
987
+ JSON.stringify(summary.planProgress ?? {}),
988
+ now.toISOString(),
989
+ Math.floor(now.getTime() / 1e3)
990
+ );
991
+ }
992
+ function addUserPrompt(db, sessionId, text18, promptNumber) {
993
+ const now = /* @__PURE__ */ new Date();
994
+ db.prepare(`
995
+ INSERT INTO user_prompts (session_id, prompt_text, prompt_number, created_at, created_at_epoch)
996
+ VALUES (?, ?, ?, ?, ?)
997
+ `).run(sessionId, text18, promptNumber, now.toISOString(), Math.floor(now.getTime() / 1e3));
998
+ }
999
+ function searchObservations(db, query, opts) {
1000
+ const limit = opts?.limit ?? 20;
1001
+ let sql = `
1002
+ SELECT o.id, o.type, o.title, o.created_at, o.session_id, o.importance,
1003
+ rank
1004
+ FROM observations_fts
1005
+ JOIN observations o ON observations_fts.rowid = o.id
1006
+ WHERE observations_fts MATCH ?
1007
+ `;
1008
+ const params = [sanitizeFts5Query(query)];
1009
+ if (opts?.type) {
1010
+ sql += " AND o.type = ?";
1011
+ params.push(opts.type);
1012
+ }
1013
+ if (opts?.crRule) {
1014
+ sql += " AND o.cr_rule = ?";
1015
+ params.push(opts.crRule);
1016
+ }
1017
+ if (opts?.dateFrom) {
1018
+ sql += " AND o.created_at >= ?";
1019
+ params.push(opts.dateFrom);
1020
+ }
1021
+ sql += " ORDER BY rank LIMIT ?";
1022
+ params.push(limit);
1023
+ return db.prepare(sql).all(...params);
1024
+ }
1025
+ function getRecentObservations(db, limit = 20, sessionId) {
1026
+ if (sessionId) {
1027
+ return db.prepare(`
1028
+ SELECT id, type, title, detail, importance, created_at, session_id
1029
+ FROM observations WHERE session_id = ?
1030
+ ORDER BY created_at_epoch DESC LIMIT ?
1031
+ `).all(sessionId, limit);
1032
+ }
1033
+ return db.prepare(`
1034
+ SELECT id, type, title, detail, importance, created_at, session_id
1035
+ FROM observations
1036
+ ORDER BY created_at_epoch DESC LIMIT ?
1037
+ `).all(limit);
1038
+ }
1039
+ function getSessionSummaries(db, limit = 10) {
1040
+ return db.prepare(`
1041
+ SELECT session_id, request, completed, failed_attempts, plan_progress, created_at
1042
+ FROM session_summaries
1043
+ ORDER BY created_at_epoch DESC LIMIT ?
1044
+ `).all(limit);
1045
+ }
1046
+ function getSessionTimeline(db, sessionId) {
1047
+ const session = db.prepare("SELECT * FROM sessions WHERE session_id = ?").get(sessionId);
1048
+ const observations = db.prepare("SELECT * FROM observations WHERE session_id = ? ORDER BY created_at_epoch ASC").all(sessionId);
1049
+ const summary = db.prepare("SELECT * FROM session_summaries WHERE session_id = ? ORDER BY created_at_epoch DESC LIMIT 1").get(sessionId);
1050
+ const prompts = db.prepare("SELECT * FROM user_prompts WHERE session_id = ? ORDER BY prompt_number ASC").all(sessionId);
1051
+ return {
1052
+ session: session ?? null,
1053
+ observations,
1054
+ summary: summary ?? null,
1055
+ prompts
1056
+ };
1057
+ }
1058
+ function getFailedAttempts(db, query, limit = 20) {
1059
+ if (query) {
1060
+ return db.prepare(`
1061
+ SELECT o.id, o.title, o.detail, o.session_id, o.recurrence_count, o.created_at
1062
+ FROM observations_fts
1063
+ JOIN observations o ON observations_fts.rowid = o.id
1064
+ WHERE observations_fts MATCH ? AND o.type = 'failed_attempt'
1065
+ ORDER BY o.recurrence_count DESC, rank LIMIT ?
1066
+ `).all(sanitizeFts5Query(query), limit);
1067
+ }
1068
+ return db.prepare(`
1069
+ SELECT id, title, detail, session_id, recurrence_count, created_at
1070
+ FROM observations WHERE type = 'failed_attempt'
1071
+ ORDER BY recurrence_count DESC, created_at_epoch DESC LIMIT ?
1072
+ `).all(limit);
1073
+ }
1074
+ function getDecisionsAbout(db, query, limit = 20) {
1075
+ return db.prepare(`
1076
+ SELECT o.id, o.title, o.detail, o.session_id, o.created_at
1077
+ FROM observations_fts
1078
+ JOIN observations o ON observations_fts.rowid = o.id
1079
+ WHERE observations_fts MATCH ? AND o.type = 'decision'
1080
+ ORDER BY rank LIMIT ?
1081
+ `).all(sanitizeFts5Query(query), limit);
1082
+ }
1083
+ function pruneOldObservations(db, retentionDays = 90) {
1084
+ const cutoffEpoch = Math.floor(Date.now() / 1e3) - retentionDays * 86400;
1085
+ const result = db.prepare("DELETE FROM observations WHERE created_at_epoch < ?").run(cutoffEpoch);
1086
+ return result.changes;
1087
+ }
1088
+ function deduplicateFailedAttempt(db, sessionId, title, detail, opts) {
1089
+ const existing = db.prepare(`
1090
+ SELECT id, recurrence_count FROM observations
1091
+ WHERE type = 'failed_attempt' AND title = ?
1092
+ ORDER BY created_at_epoch DESC LIMIT 1
1093
+ `).get(title);
1094
+ if (existing) {
1095
+ db.prepare("UPDATE observations SET recurrence_count = recurrence_count + 1, detail = COALESCE(?, detail) WHERE id = ?").run(detail, existing.id);
1096
+ return existing.id;
1097
+ }
1098
+ return addObservation(db, sessionId, "failed_attempt", title, detail, {
1099
+ ...opts,
1100
+ importance: 5
1101
+ });
1102
+ }
1103
+ function getSessionsByTask(db, taskId) {
1104
+ return db.prepare(`
1105
+ SELECT session_id, status, started_at, ended_at, plan_phase
1106
+ FROM sessions WHERE task_id = ?
1107
+ ORDER BY started_at_epoch DESC
1108
+ `).all(taskId);
1109
+ }
1110
+ function getCrossTaskProgress(db, taskId) {
1111
+ const sessions = db.prepare(`
1112
+ SELECT session_id FROM sessions WHERE task_id = ?
1113
+ `).all(taskId);
1114
+ const merged = {};
1115
+ for (const session of sessions) {
1116
+ const summaries = db.prepare(`
1117
+ SELECT plan_progress FROM session_summaries WHERE session_id = ?
1118
+ `).all(session.session_id);
1119
+ for (const summary of summaries) {
1120
+ try {
1121
+ const progress = JSON.parse(summary.plan_progress);
1122
+ for (const [key, value] of Object.entries(progress)) {
1123
+ if (!merged[key] || value === "complete" || value === "in_progress" && merged[key] === "pending") {
1124
+ merged[key] = value;
1125
+ }
1126
+ }
1127
+ } catch (_e) {
1128
+ }
1129
+ }
1130
+ }
1131
+ return merged;
1132
+ }
1133
+ function linkSessionToTask(db, sessionId, taskId) {
1134
+ db.prepare("UPDATE sessions SET task_id = ? WHERE session_id = ?").run(taskId, sessionId);
1135
+ }
1136
+ function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantResponse, toolCallsJson, toolCallCount, promptTokens, responseTokens) {
1137
+ const result = db.prepare(`
1138
+ INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens)
1139
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1140
+ `).run(
1141
+ sessionId,
1142
+ turnNumber,
1143
+ userPrompt,
1144
+ assistantResponse ? assistantResponse.slice(0, 1e4) : null,
1145
+ toolCallsJson,
1146
+ toolCallCount,
1147
+ promptTokens,
1148
+ responseTokens
1149
+ );
1150
+ return Number(result.lastInsertRowid);
1151
+ }
1152
+ function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, inputSize, outputSize, success, filesInvolved) {
1153
+ db.prepare(`
1154
+ INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved)
1155
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1156
+ `).run(
1157
+ sessionId,
1158
+ turnNumber,
1159
+ toolName,
1160
+ inputSummary ? inputSummary.slice(0, 500) : null,
1161
+ inputSize,
1162
+ outputSize,
1163
+ success ? 1 : 0,
1164
+ filesInvolved ? JSON.stringify(filesInvolved) : null
1165
+ );
1166
+ }
1167
+ function getLastProcessedLine(db, sessionId) {
1168
+ const row = db.prepare("SELECT value FROM memory_meta WHERE key = ?").get(`last_processed_line:${sessionId}`);
1169
+ return row ? parseInt(row.value, 10) : 0;
1170
+ }
1171
+ function setLastProcessedLine(db, sessionId, lineNumber) {
1172
+ db.prepare("INSERT OR REPLACE INTO memory_meta (key, value) VALUES (?, ?)").run(`last_processed_line:${sessionId}`, String(lineNumber));
1173
+ }
1174
+ function pruneOldConversationTurns(db, retentionDays = 90) {
1175
+ const cutoffEpoch = Math.floor(Date.now() / 1e3) - retentionDays * 86400;
1176
+ const turnsResult = db.prepare("DELETE FROM conversation_turns WHERE created_at_epoch < ?").run(cutoffEpoch);
1177
+ const detailsResult = db.prepare("DELETE FROM tool_call_details WHERE created_at_epoch < ?").run(cutoffEpoch);
1178
+ return { turnsDeleted: turnsResult.changes, detailsDeleted: detailsResult.changes };
1179
+ }
1180
+ function getConversationTurns(db, sessionId, opts) {
1181
+ let sql = "SELECT id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens, created_at FROM conversation_turns WHERE session_id = ?";
1182
+ const params = [sessionId];
1183
+ if (opts?.turnFrom !== void 0) {
1184
+ sql += " AND turn_number >= ?";
1185
+ params.push(opts.turnFrom);
1186
+ }
1187
+ if (opts?.turnTo !== void 0) {
1188
+ sql += " AND turn_number <= ?";
1189
+ params.push(opts.turnTo);
1190
+ }
1191
+ sql += " ORDER BY turn_number ASC";
1192
+ return db.prepare(sql).all(...params);
1193
+ }
1194
+ function searchConversationTurns(db, query, opts) {
1195
+ const limit = opts?.limit ?? 20;
1196
+ let sql = `
1197
+ SELECT ct.id, ct.session_id, ct.turn_number, ct.user_prompt, ct.tool_call_count, ct.response_tokens, ct.created_at, rank
1198
+ FROM conversation_turns_fts
1199
+ JOIN conversation_turns ct ON conversation_turns_fts.rowid = ct.id
1200
+ WHERE conversation_turns_fts MATCH ?
1201
+ `;
1202
+ const params = [sanitizeFts5Query(query)];
1203
+ if (opts?.sessionId) {
1204
+ sql += " AND ct.session_id = ?";
1205
+ params.push(opts.sessionId);
1206
+ }
1207
+ if (opts?.dateFrom) {
1208
+ sql += " AND ct.created_at >= ?";
1209
+ params.push(opts.dateFrom);
1210
+ }
1211
+ if (opts?.dateTo) {
1212
+ sql += " AND ct.created_at <= ?";
1213
+ params.push(opts.dateTo);
1214
+ }
1215
+ if (opts?.minToolCalls !== void 0) {
1216
+ sql += " AND ct.tool_call_count >= ?";
1217
+ params.push(opts.minToolCalls);
1218
+ }
1219
+ sql += " ORDER BY rank LIMIT ?";
1220
+ params.push(limit);
1221
+ return db.prepare(sql).all(...params);
1222
+ }
1223
+ function getToolPatterns(db, opts) {
1224
+ const groupBy = opts?.groupBy ?? "tool";
1225
+ const params = [];
1226
+ let whereClause = "";
1227
+ const conditions = [];
1228
+ if (opts?.sessionId) {
1229
+ conditions.push("session_id = ?");
1230
+ params.push(opts.sessionId);
1231
+ }
1232
+ if (opts?.toolName) {
1233
+ conditions.push("tool_name = ?");
1234
+ params.push(opts.toolName);
1235
+ }
1236
+ if (opts?.dateFrom) {
1237
+ conditions.push("created_at >= ?");
1238
+ params.push(opts.dateFrom);
1239
+ }
1240
+ if (conditions.length > 0) {
1241
+ whereClause = "WHERE " + conditions.join(" AND ");
1242
+ }
1243
+ let sql;
1244
+ switch (groupBy) {
1245
+ case "session":
1246
+ sql = `SELECT session_id, COUNT(*) as call_count, COUNT(DISTINCT tool_name) as unique_tools,
1247
+ SUM(CASE WHEN tool_success = 1 THEN 1 ELSE 0 END) as successes,
1248
+ SUM(CASE WHEN tool_success = 0 THEN 1 ELSE 0 END) as failures,
1249
+ AVG(tool_output_size) as avg_output_size
1250
+ FROM tool_call_details ${whereClause}
1251
+ GROUP BY session_id ORDER BY call_count DESC`;
1252
+ break;
1253
+ case "day":
1254
+ sql = `SELECT date(created_at) as day, COUNT(*) as call_count, COUNT(DISTINCT tool_name) as unique_tools,
1255
+ SUM(CASE WHEN tool_success = 1 THEN 1 ELSE 0 END) as successes
1256
+ FROM tool_call_details ${whereClause}
1257
+ GROUP BY date(created_at) ORDER BY day DESC`;
1258
+ break;
1259
+ default:
1260
+ sql = `SELECT tool_name, COUNT(*) as call_count,
1261
+ SUM(CASE WHEN tool_success = 1 THEN 1 ELSE 0 END) as successes,
1262
+ SUM(CASE WHEN tool_success = 0 THEN 1 ELSE 0 END) as failures,
1263
+ AVG(tool_output_size) as avg_output_size,
1264
+ AVG(tool_input_size) as avg_input_size
1265
+ FROM tool_call_details ${whereClause}
1266
+ GROUP BY tool_name ORDER BY call_count DESC`;
1267
+ break;
1403
1268
  }
1269
+ return db.prepare(sql).all(...params);
1404
1270
  }
1405
- function addObservation(db, sessionId, type, title, detail, opts) {
1406
- const now = /* @__PURE__ */ new Date();
1407
- const importance = opts?.importance ?? assignImportance(type, opts?.evidence?.includes("PASS") ? "PASS" : void 0);
1408
- const result = db.prepare(`
1409
- INSERT INTO observations (session_id, type, title, detail, files_involved, plan_item, cr_rule, vr_type, evidence, importance, original_tokens, created_at, created_at_epoch)
1410
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1411
- `).run(
1412
- sessionId,
1413
- type,
1414
- title,
1415
- detail,
1416
- JSON.stringify(opts?.filesInvolved ?? []),
1417
- opts?.planItem ?? null,
1418
- opts?.crRule ?? null,
1419
- opts?.vrType ?? null,
1420
- opts?.evidence ?? null,
1421
- importance,
1422
- opts?.originalTokens ?? 0,
1423
- now.toISOString(),
1424
- Math.floor(now.getTime() / 1e3)
1271
+ function getSessionStats(db, opts) {
1272
+ if (opts?.sessionId) {
1273
+ const turns = db.prepare("SELECT COUNT(*) as turn_count, SUM(tool_call_count) as total_tool_calls, SUM(prompt_tokens) as total_prompt_tokens, SUM(response_tokens) as total_response_tokens FROM conversation_turns WHERE session_id = ?").get(opts.sessionId);
1274
+ const toolBreakdown = db.prepare("SELECT tool_name, COUNT(*) as count FROM tool_call_details WHERE session_id = ? GROUP BY tool_name ORDER BY count DESC").all(opts.sessionId);
1275
+ const session = db.prepare("SELECT * FROM sessions WHERE session_id = ?").get(opts.sessionId);
1276
+ return [{
1277
+ session_id: opts.sessionId,
1278
+ status: session?.status ?? "unknown",
1279
+ started_at: session?.started_at ?? null,
1280
+ ended_at: session?.ended_at ?? null,
1281
+ ...turns,
1282
+ tool_breakdown: toolBreakdown
1283
+ }];
1284
+ }
1285
+ const limit = opts?.limit ?? 10;
1286
+ return db.prepare(`
1287
+ SELECT s.session_id, s.status, s.started_at, s.ended_at,
1288
+ COUNT(ct.id) as turn_count,
1289
+ COALESCE(SUM(ct.tool_call_count), 0) as total_tool_calls,
1290
+ COALESCE(SUM(ct.prompt_tokens), 0) as total_prompt_tokens,
1291
+ COALESCE(SUM(ct.response_tokens), 0) as total_response_tokens
1292
+ FROM sessions s
1293
+ LEFT JOIN conversation_turns ct ON s.session_id = ct.session_id
1294
+ GROUP BY s.session_id
1295
+ ORDER BY s.started_at_epoch DESC
1296
+ LIMIT ?
1297
+ `).all(limit);
1298
+ }
1299
+ function getObservabilityDbSize(db) {
1300
+ const turnsCount = db.prepare("SELECT COUNT(*) as c FROM conversation_turns").get().c;
1301
+ const detailsCount = db.prepare("SELECT COUNT(*) as c FROM tool_call_details").get().c;
1302
+ const obsCount = db.prepare("SELECT COUNT(*) as c FROM observations").get().c;
1303
+ const pageCount = db.pragma("page_count")[0]?.page_count ?? 0;
1304
+ const pageSize = db.pragma("page_size")[0]?.page_size ?? 4096;
1305
+ return {
1306
+ conversation_turns_count: turnsCount,
1307
+ tool_call_details_count: detailsCount,
1308
+ observations_count: obsCount,
1309
+ db_page_count: pageCount,
1310
+ db_page_size: pageSize,
1311
+ estimated_size_mb: Math.round(pageCount * pageSize / (1024 * 1024) * 100) / 100
1312
+ };
1313
+ }
1314
+ var init_memory_db = __esm({
1315
+ "src/memory-db.ts"() {
1316
+ "use strict";
1317
+ init_config();
1318
+ }
1319
+ });
1320
+
1321
+ // src/memory-file-ingest.ts
1322
+ import { readFileSync as readFileSync2, existsSync as existsSync3, readdirSync } from "fs";
1323
+ import { join } from "path";
1324
+ import { parse as parseYaml2 } from "yaml";
1325
+ function ingestMemoryFile(db, sessionId, filePath) {
1326
+ if (!existsSync3(filePath)) return "skipped";
1327
+ const content = readFileSync2(filePath, "utf-8");
1328
+ const basename8 = (filePath.split("/").pop() ?? "").replace(".md", "");
1329
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
1330
+ let name = basename8;
1331
+ let description = "";
1332
+ let type = "discovery";
1333
+ let confidence;
1334
+ if (frontmatterMatch) {
1335
+ try {
1336
+ const fm = parseYaml2(frontmatterMatch[1]);
1337
+ name = fm.name ?? basename8;
1338
+ description = fm.description ?? "";
1339
+ type = fm.type ?? "discovery";
1340
+ confidence = fm.confidence != null ? Number(fm.confidence) : void 0;
1341
+ } catch {
1342
+ }
1343
+ }
1344
+ const obsType = mapMemoryTypeToObservationType(type);
1345
+ const importance = confidence != null ? Math.max(1, Math.min(5, Math.round(confidence * 4 + 1))) : 4;
1346
+ const bodyMatch = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)/);
1347
+ const body = bodyMatch ? bodyMatch[1].trim().slice(0, 500) : "";
1348
+ const title = `[memory-file] ${name}`;
1349
+ const detail = description ? `${description}
1350
+
1351
+ ${body}` : body;
1352
+ const existing = db.prepare(
1353
+ "SELECT id FROM observations WHERE title = ? LIMIT 1"
1354
+ ).get(title);
1355
+ if (existing) {
1356
+ db.prepare("UPDATE observations SET detail = ?, importance = ? WHERE id = ?").run(detail, importance, existing.id);
1357
+ return "updated";
1358
+ } else {
1359
+ addObservation(db, sessionId, obsType, title, detail, { importance });
1360
+ return "inserted";
1361
+ }
1362
+ }
1363
+ function backfillMemoryFiles(db, memoryDir, sessionId) {
1364
+ const stats = { inserted: 0, updated: 0, skipped: 0, total: 0 };
1365
+ if (!existsSync3(memoryDir)) return stats;
1366
+ const files = readdirSync(memoryDir).filter(
1367
+ (f) => f.endsWith(".md") && f !== "MEMORY.md"
1425
1368
  );
1426
- return Number(result.lastInsertRowid);
1369
+ stats.total = files.length;
1370
+ const sid = sessionId ?? `backfill-${Date.now()}`;
1371
+ for (const file of files) {
1372
+ const result = ingestMemoryFile(db, sid, join(memoryDir, file));
1373
+ stats[result]++;
1374
+ }
1375
+ return stats;
1427
1376
  }
1428
- function searchObservations(db, query, opts) {
1429
- const limit = opts?.limit ?? 20;
1430
- let sql = `
1431
- SELECT o.id, o.type, o.title, o.created_at, o.session_id, o.importance,
1432
- rank
1433
- FROM observations_fts
1434
- JOIN observations o ON observations_fts.rowid = o.id
1435
- WHERE observations_fts MATCH ?
1436
- `;
1437
- const params = [sanitizeFts5Query(query)];
1438
- if (opts?.type) {
1439
- sql += " AND o.type = ?";
1440
- params.push(opts.type);
1377
+ function mapMemoryTypeToObservationType(memoryType) {
1378
+ switch (memoryType) {
1379
+ case "user":
1380
+ case "feedback":
1381
+ return "decision";
1382
+ case "project":
1383
+ return "feature";
1384
+ case "reference":
1385
+ return "discovery";
1386
+ default:
1387
+ return "discovery";
1388
+ }
1389
+ }
1390
+ var init_memory_file_ingest = __esm({
1391
+ "src/memory-file-ingest.ts"() {
1392
+ "use strict";
1393
+ init_memory_db();
1394
+ }
1395
+ });
1396
+
1397
+ // src/commands/install-commands.ts
1398
+ var install_commands_exports = {};
1399
+ __export(install_commands_exports, {
1400
+ installAll: () => installAll,
1401
+ installCommands: () => installCommands,
1402
+ resolveAssetDir: () => resolveAssetDir,
1403
+ resolveCommandsDir: () => resolveCommandsDir,
1404
+ runInstallCommands: () => runInstallCommands
1405
+ });
1406
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync, mkdirSync as mkdirSync2, readdirSync as readdirSync2, statSync } from "fs";
1407
+ import { resolve as resolve3, dirname as dirname3 } from "path";
1408
+ import { fileURLToPath } from "url";
1409
+ function resolveAssetDir(assetName) {
1410
+ const cwd = process.cwd();
1411
+ const nodeModulesPath = resolve3(cwd, "node_modules/@massu/core", assetName);
1412
+ if (existsSync4(nodeModulesPath)) {
1413
+ return nodeModulesPath;
1414
+ }
1415
+ const distRelPath = resolve3(__dirname, "..", assetName);
1416
+ if (existsSync4(distRelPath)) {
1417
+ return distRelPath;
1418
+ }
1419
+ const srcRelPath = resolve3(__dirname, "../..", assetName);
1420
+ if (existsSync4(srcRelPath)) {
1421
+ return srcRelPath;
1422
+ }
1423
+ return null;
1424
+ }
1425
+ function resolveCommandsDir() {
1426
+ return resolveAssetDir("commands");
1427
+ }
1428
+ function syncDirectory(sourceDir, targetDir) {
1429
+ const stats = { installed: 0, updated: 0, skipped: 0 };
1430
+ if (!existsSync4(targetDir)) {
1431
+ mkdirSync2(targetDir, { recursive: true });
1432
+ }
1433
+ const entries = readdirSync2(sourceDir);
1434
+ for (const entry of entries) {
1435
+ const sourcePath = resolve3(sourceDir, entry);
1436
+ const targetPath = resolve3(targetDir, entry);
1437
+ const entryStat = statSync(sourcePath);
1438
+ if (entryStat.isDirectory()) {
1439
+ const subStats = syncDirectory(sourcePath, targetPath);
1440
+ stats.installed += subStats.installed;
1441
+ stats.updated += subStats.updated;
1442
+ stats.skipped += subStats.skipped;
1443
+ } else if (entry.endsWith(".md")) {
1444
+ const sourceContent = readFileSync3(sourcePath, "utf-8");
1445
+ if (existsSync4(targetPath)) {
1446
+ const existingContent = readFileSync3(targetPath, "utf-8");
1447
+ if (existingContent === sourceContent) {
1448
+ stats.skipped++;
1449
+ continue;
1450
+ }
1451
+ writeFileSync(targetPath, sourceContent, "utf-8");
1452
+ stats.updated++;
1453
+ } else {
1454
+ writeFileSync(targetPath, sourceContent, "utf-8");
1455
+ stats.installed++;
1456
+ }
1457
+ }
1458
+ }
1459
+ return stats;
1460
+ }
1461
+ function installCommands(projectRoot) {
1462
+ const claudeDirName = getConfig().conventions?.claudeDirName ?? ".claude";
1463
+ const targetDir = resolve3(projectRoot, claudeDirName, "commands");
1464
+ if (!existsSync4(targetDir)) {
1465
+ mkdirSync2(targetDir, { recursive: true });
1466
+ }
1467
+ const sourceDir = resolveAssetDir("commands");
1468
+ if (!sourceDir) {
1469
+ console.error(" ERROR: Could not find massu commands directory.");
1470
+ console.error(" Try reinstalling: npm install @massu/core");
1471
+ return { installed: 0, updated: 0, skipped: 0, commandsDir: targetDir };
1472
+ }
1473
+ const stats = syncDirectory(sourceDir, targetDir);
1474
+ return { ...stats, commandsDir: targetDir };
1475
+ }
1476
+ function installAll(projectRoot) {
1477
+ const claudeDirName = getConfig().conventions?.claudeDirName ?? ".claude";
1478
+ const claudeDir = resolve3(projectRoot, claudeDirName);
1479
+ const assets = {};
1480
+ let totalInstalled = 0;
1481
+ let totalUpdated = 0;
1482
+ let totalSkipped = 0;
1483
+ for (const assetType of ASSET_TYPES) {
1484
+ const sourceDir = resolveAssetDir(assetType.name);
1485
+ if (!sourceDir) {
1486
+ continue;
1487
+ }
1488
+ const targetDir = resolve3(claudeDir, assetType.targetSubdir);
1489
+ const stats = syncDirectory(sourceDir, targetDir);
1490
+ assets[assetType.name] = stats;
1491
+ totalInstalled += stats.installed;
1492
+ totalUpdated += stats.updated;
1493
+ totalSkipped += stats.skipped;
1494
+ }
1495
+ return { assets, totalInstalled, totalUpdated, totalSkipped, claudeDir };
1496
+ }
1497
+ async function runInstallCommands() {
1498
+ const projectRoot = process.cwd();
1499
+ console.log("");
1500
+ console.log("Massu AI - Install Project Assets");
1501
+ console.log("==================================");
1502
+ console.log("");
1503
+ const result = installAll(projectRoot);
1504
+ for (const assetType of ASSET_TYPES) {
1505
+ const stats = result.assets[assetType.name];
1506
+ if (!stats) {
1507
+ continue;
1508
+ }
1509
+ const total = stats.installed + stats.updated + stats.skipped;
1510
+ if (total === 0) continue;
1511
+ const parts = [];
1512
+ if (stats.installed > 0) parts.push(`${stats.installed} new`);
1513
+ if (stats.updated > 0) parts.push(`${stats.updated} updated`);
1514
+ if (stats.skipped > 0) parts.push(`${stats.skipped} current`);
1515
+ const description = assetType.description;
1516
+ console.log(` ${description}: ${parts.join(", ")} (${total} total)`);
1517
+ }
1518
+ const grandTotal = result.totalInstalled + result.totalUpdated + result.totalSkipped;
1519
+ console.log("");
1520
+ console.log(` ${grandTotal} total files synced to ${result.claudeDir}`);
1521
+ console.log("");
1522
+ console.log(" Restart your Claude Code session to use them.");
1523
+ console.log("");
1524
+ }
1525
+ var __filename, __dirname, ASSET_TYPES;
1526
+ var init_install_commands = __esm({
1527
+ "src/commands/install-commands.ts"() {
1528
+ "use strict";
1529
+ init_config();
1530
+ __filename = fileURLToPath(import.meta.url);
1531
+ __dirname = dirname3(__filename);
1532
+ ASSET_TYPES = [
1533
+ { name: "commands", targetSubdir: "commands", description: "slash commands" },
1534
+ { name: "agents", targetSubdir: "agents", description: "agent definitions" },
1535
+ { name: "patterns", targetSubdir: "patterns", description: "pattern files" },
1536
+ { name: "protocols", targetSubdir: "protocols", description: "protocol files" },
1537
+ { name: "reference", targetSubdir: "reference", description: "reference files" }
1538
+ ];
1539
+ }
1540
+ });
1541
+
1542
+ // src/commands/init.ts
1543
+ var init_exports = {};
1544
+ __export(init_exports, {
1545
+ buildHooksConfig: () => buildHooksConfig,
1546
+ detectFramework: () => detectFramework,
1547
+ detectPython: () => detectPython,
1548
+ generateConfig: () => generateConfig,
1549
+ initMemoryDir: () => initMemoryDir,
1550
+ installHooks: () => installHooks,
1551
+ registerMcpServer: () => registerMcpServer,
1552
+ resolveHooksDir: () => resolveHooksDir,
1553
+ runInit: () => runInit
1554
+ });
1555
+ import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync3 } from "fs";
1556
+ import { resolve as resolve4, basename as basename2, dirname as dirname4 } from "path";
1557
+ import { fileURLToPath as fileURLToPath2 } from "url";
1558
+ import { homedir as homedir2 } from "os";
1559
+ import { stringify as yamlStringify } from "yaml";
1560
+ function detectFramework(projectRoot) {
1561
+ const result = {
1562
+ type: "javascript",
1563
+ router: "none",
1564
+ orm: "none",
1565
+ ui: "none"
1566
+ };
1567
+ const pkgPath = resolve4(projectRoot, "package.json");
1568
+ if (!existsSync5(pkgPath)) return result;
1569
+ try {
1570
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
1571
+ const allDeps = {
1572
+ ...pkg.dependencies,
1573
+ ...pkg.devDependencies
1574
+ };
1575
+ if (allDeps["typescript"]) result.type = "typescript";
1576
+ if (allDeps["next"]) result.ui = "nextjs";
1577
+ else if (allDeps["@sveltejs/kit"]) result.ui = "sveltekit";
1578
+ else if (allDeps["nuxt"]) result.ui = "nuxt";
1579
+ else if (allDeps["@angular/core"]) result.ui = "angular";
1580
+ else if (allDeps["vue"]) result.ui = "vue";
1581
+ else if (allDeps["react"]) result.ui = "react";
1582
+ if (allDeps["@trpc/server"]) result.router = "trpc";
1583
+ else if (allDeps["graphql"] || allDeps["@apollo/server"]) result.router = "graphql";
1584
+ else if (allDeps["express"] || allDeps["fastify"] || allDeps["hono"]) result.router = "rest";
1585
+ if (allDeps["@prisma/client"] || allDeps["prisma"]) result.orm = "prisma";
1586
+ else if (allDeps["drizzle-orm"]) result.orm = "drizzle";
1587
+ else if (allDeps["typeorm"]) result.orm = "typeorm";
1588
+ else if (allDeps["sequelize"]) result.orm = "sequelize";
1589
+ else if (allDeps["mongoose"]) result.orm = "mongoose";
1590
+ } catch {
1591
+ }
1592
+ return result;
1593
+ }
1594
+ function detectPython(projectRoot) {
1595
+ const result = {
1596
+ detected: false,
1597
+ root: "",
1598
+ hasFastapi: false,
1599
+ hasSqlalchemy: false,
1600
+ hasAlembic: false,
1601
+ alembicDir: null
1602
+ };
1603
+ const markers = ["pyproject.toml", "setup.py", "requirements.txt", "Pipfile"];
1604
+ const hasMarker = markers.some((m) => existsSync5(resolve4(projectRoot, m)));
1605
+ if (!hasMarker) return result;
1606
+ result.detected = true;
1607
+ const depFiles = [
1608
+ { file: "pyproject.toml", parser: parsePyprojectDeps },
1609
+ { file: "requirements.txt", parser: parseRequirementsDeps },
1610
+ { file: "setup.py", parser: parseSetupPyDeps },
1611
+ { file: "Pipfile", parser: parsePipfileDeps }
1612
+ ];
1613
+ for (const { file, parser } of depFiles) {
1614
+ const filePath = resolve4(projectRoot, file);
1615
+ if (existsSync5(filePath)) {
1616
+ try {
1617
+ const content = readFileSync4(filePath, "utf-8");
1618
+ const deps = parser(content);
1619
+ if (deps.includes("fastapi")) result.hasFastapi = true;
1620
+ if (deps.includes("sqlalchemy")) result.hasSqlalchemy = true;
1621
+ } catch {
1622
+ }
1623
+ }
1624
+ }
1625
+ if (existsSync5(resolve4(projectRoot, "alembic.ini"))) {
1626
+ result.hasAlembic = true;
1627
+ if (existsSync5(resolve4(projectRoot, "alembic"))) {
1628
+ result.alembicDir = "alembic";
1629
+ }
1630
+ } else if (existsSync5(resolve4(projectRoot, "alembic"))) {
1631
+ result.hasAlembic = true;
1632
+ result.alembicDir = "alembic";
1441
1633
  }
1442
- if (opts?.crRule) {
1443
- sql += " AND o.cr_rule = ?";
1444
- params.push(opts.crRule);
1634
+ const candidateRoots = ["app", "src", "backend", "api"];
1635
+ for (const candidate of candidateRoots) {
1636
+ const candidatePath = resolve4(projectRoot, candidate);
1637
+ if (existsSync5(candidatePath) && existsSync5(resolve4(candidatePath, "__init__.py"))) {
1638
+ result.root = candidate;
1639
+ break;
1640
+ }
1641
+ if (existsSync5(candidatePath)) {
1642
+ try {
1643
+ const files = readdirSync3(candidatePath);
1644
+ if (files.some((f) => f.endsWith(".py"))) {
1645
+ result.root = candidate;
1646
+ break;
1647
+ }
1648
+ } catch {
1649
+ }
1650
+ }
1445
1651
  }
1446
- if (opts?.dateFrom) {
1447
- sql += " AND o.created_at >= ?";
1448
- params.push(opts.dateFrom);
1652
+ if (!result.root) {
1653
+ result.root = ".";
1449
1654
  }
1450
- sql += " ORDER BY rank LIMIT ?";
1451
- params.push(limit);
1452
- return db.prepare(sql).all(...params);
1655
+ return result;
1453
1656
  }
1454
- function getSessionSummaries(db, limit = 10) {
1455
- return db.prepare(`
1456
- SELECT session_id, request, completed, failed_attempts, plan_progress, created_at
1457
- FROM session_summaries
1458
- ORDER BY created_at_epoch DESC LIMIT ?
1459
- `).all(limit);
1657
+ function parsePyprojectDeps(content) {
1658
+ const deps = [];
1659
+ const lower = content.toLowerCase();
1660
+ if (lower.includes("fastapi")) deps.push("fastapi");
1661
+ if (lower.includes("sqlalchemy")) deps.push("sqlalchemy");
1662
+ return deps;
1460
1663
  }
1461
- function getFailedAttempts(db, query, limit = 20) {
1462
- if (query) {
1463
- return db.prepare(`
1464
- SELECT o.id, o.title, o.detail, o.session_id, o.recurrence_count, o.created_at
1465
- FROM observations_fts
1466
- JOIN observations o ON observations_fts.rowid = o.id
1467
- WHERE observations_fts MATCH ? AND o.type = 'failed_attempt'
1468
- ORDER BY o.recurrence_count DESC, rank LIMIT ?
1469
- `).all(sanitizeFts5Query(query), limit);
1664
+ function parseRequirementsDeps(content) {
1665
+ const deps = [];
1666
+ const lower = content.toLowerCase();
1667
+ for (const line of lower.split("\n")) {
1668
+ const trimmed = line.trim();
1669
+ if (trimmed.startsWith("fastapi")) deps.push("fastapi");
1670
+ if (trimmed.startsWith("sqlalchemy")) deps.push("sqlalchemy");
1470
1671
  }
1471
- return db.prepare(`
1472
- SELECT id, title, detail, session_id, recurrence_count, created_at
1473
- FROM observations WHERE type = 'failed_attempt'
1474
- ORDER BY recurrence_count DESC, created_at_epoch DESC LIMIT ?
1475
- `).all(limit);
1672
+ return deps;
1476
1673
  }
1477
- function pruneOldObservations(db, retentionDays = 90) {
1478
- const cutoffEpoch = Math.floor(Date.now() / 1e3) - retentionDays * 86400;
1479
- const result = db.prepare("DELETE FROM observations WHERE created_at_epoch < ?").run(cutoffEpoch);
1480
- return result.changes;
1674
+ function parseSetupPyDeps(content) {
1675
+ const deps = [];
1676
+ const lower = content.toLowerCase();
1677
+ if (lower.includes("fastapi")) deps.push("fastapi");
1678
+ if (lower.includes("sqlalchemy")) deps.push("sqlalchemy");
1679
+ return deps;
1481
1680
  }
1482
- function pruneOldConversationTurns(db, retentionDays = 90) {
1483
- const cutoffEpoch = Math.floor(Date.now() / 1e3) - retentionDays * 86400;
1484
- const turnsResult = db.prepare("DELETE FROM conversation_turns WHERE created_at_epoch < ?").run(cutoffEpoch);
1485
- const detailsResult = db.prepare("DELETE FROM tool_call_details WHERE created_at_epoch < ?").run(cutoffEpoch);
1486
- return { turnsDeleted: turnsResult.changes, detailsDeleted: detailsResult.changes };
1681
+ function parsePipfileDeps(content) {
1682
+ const deps = [];
1683
+ const lower = content.toLowerCase();
1684
+ if (lower.includes("fastapi")) deps.push("fastapi");
1685
+ if (lower.includes("sqlalchemy")) deps.push("sqlalchemy");
1686
+ return deps;
1487
1687
  }
1488
- function getConversationTurns(db, sessionId, opts) {
1489
- let sql = "SELECT id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens, created_at FROM conversation_turns WHERE session_id = ?";
1490
- const params = [sessionId];
1491
- if (opts?.turnFrom !== void 0) {
1492
- sql += " AND turn_number >= ?";
1493
- params.push(opts.turnFrom);
1688
+ function generateConfig(projectRoot, framework) {
1689
+ const configPath = resolve4(projectRoot, "massu.config.yaml");
1690
+ if (existsSync5(configPath)) {
1691
+ return false;
1494
1692
  }
1495
- if (opts?.turnTo !== void 0) {
1496
- sql += " AND turn_number <= ?";
1497
- params.push(opts.turnTo);
1693
+ const projectName = basename2(projectRoot);
1694
+ const config = {
1695
+ project: {
1696
+ name: projectName,
1697
+ root: "auto"
1698
+ },
1699
+ framework: {
1700
+ type: framework.type,
1701
+ router: framework.router,
1702
+ orm: framework.orm,
1703
+ ui: framework.ui
1704
+ },
1705
+ paths: {
1706
+ source: "src",
1707
+ aliases: { "@": "src" }
1708
+ },
1709
+ toolPrefix: "massu",
1710
+ domains: [],
1711
+ rules: [
1712
+ {
1713
+ pattern: "src/**/*.ts",
1714
+ rules: ["Use ESM imports, not CommonJS"]
1715
+ }
1716
+ ]
1717
+ };
1718
+ const python = detectPython(projectRoot);
1719
+ if (python.detected) {
1720
+ const pythonConfig = {
1721
+ root: python.root,
1722
+ exclude_dirs: ["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"]
1723
+ };
1724
+ if (python.hasFastapi) pythonConfig.framework = "fastapi";
1725
+ if (python.hasSqlalchemy) pythonConfig.orm = "sqlalchemy";
1726
+ if (python.hasAlembic && python.alembicDir) {
1727
+ pythonConfig.alembic_dir = python.alembicDir;
1728
+ }
1729
+ config.python = pythonConfig;
1498
1730
  }
1499
- sql += " ORDER BY turn_number ASC";
1500
- return db.prepare(sql).all(...params);
1731
+ const yamlContent = `# Massu AI Configuration
1732
+ # Generated by: npx massu init
1733
+ # Documentation: https://massu.ai/docs/getting-started/configuration
1734
+
1735
+ ${yamlStringify(config)}`;
1736
+ writeFileSync2(configPath, yamlContent, "utf-8");
1737
+ return true;
1501
1738
  }
1502
- function searchConversationTurns(db, query, opts) {
1503
- const limit = opts?.limit ?? 20;
1504
- let sql = `
1505
- SELECT ct.id, ct.session_id, ct.turn_number, ct.user_prompt, ct.tool_call_count, ct.response_tokens, ct.created_at, rank
1506
- FROM conversation_turns_fts
1507
- JOIN conversation_turns ct ON conversation_turns_fts.rowid = ct.id
1508
- WHERE conversation_turns_fts MATCH ?
1509
- `;
1510
- const params = [sanitizeFts5Query(query)];
1511
- if (opts?.sessionId) {
1512
- sql += " AND ct.session_id = ?";
1513
- params.push(opts.sessionId);
1739
+ function registerMcpServer(projectRoot) {
1740
+ const mcpPath = resolve4(projectRoot, ".mcp.json");
1741
+ let existing = {};
1742
+ if (existsSync5(mcpPath)) {
1743
+ try {
1744
+ existing = JSON.parse(readFileSync4(mcpPath, "utf-8"));
1745
+ } catch {
1746
+ existing = {};
1747
+ }
1514
1748
  }
1515
- if (opts?.dateFrom) {
1516
- sql += " AND ct.created_at >= ?";
1517
- params.push(opts.dateFrom);
1749
+ const servers = existing.mcpServers ?? {};
1750
+ if (servers.massu) {
1751
+ return false;
1752
+ }
1753
+ servers.massu = {
1754
+ type: "stdio",
1755
+ command: "npx",
1756
+ args: ["-y", "@massu/core"]
1757
+ };
1758
+ existing.mcpServers = servers;
1759
+ writeFileSync2(mcpPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
1760
+ return true;
1761
+ }
1762
+ function resolveHooksDir() {
1763
+ const cwd = process.cwd();
1764
+ const nodeModulesPath = resolve4(cwd, "node_modules/@massu/core/dist/hooks");
1765
+ if (existsSync5(nodeModulesPath)) {
1766
+ return "node_modules/@massu/core/dist/hooks";
1767
+ }
1768
+ const localPath = resolve4(__dirname2, "../dist/hooks");
1769
+ if (existsSync5(localPath)) {
1770
+ return localPath;
1771
+ }
1772
+ return "node_modules/@massu/core/dist/hooks";
1773
+ }
1774
+ function hookCmd(hooksDir, hookFile) {
1775
+ return `node ${hooksDir}/${hookFile}`;
1776
+ }
1777
+ function buildHooksConfig(hooksDir) {
1778
+ return {
1779
+ SessionStart: [
1780
+ {
1781
+ hooks: [
1782
+ { type: "command", command: hookCmd(hooksDir, "session-start.js"), timeout: 10 }
1783
+ ]
1784
+ }
1785
+ ],
1786
+ PreToolUse: [
1787
+ {
1788
+ matcher: "Bash",
1789
+ hooks: [
1790
+ { type: "command", command: hookCmd(hooksDir, "security-gate.js"), timeout: 5 }
1791
+ ]
1792
+ },
1793
+ {
1794
+ matcher: "Bash|Write",
1795
+ hooks: [
1796
+ { type: "command", command: hookCmd(hooksDir, "pre-delete-check.js"), timeout: 5 }
1797
+ ]
1798
+ }
1799
+ ],
1800
+ PostToolUse: [
1801
+ {
1802
+ hooks: [
1803
+ { type: "command", command: hookCmd(hooksDir, "post-tool-use.js"), timeout: 10 },
1804
+ { type: "command", command: hookCmd(hooksDir, "quality-event.js"), timeout: 5 },
1805
+ { type: "command", command: hookCmd(hooksDir, "cost-tracker.js"), timeout: 5 }
1806
+ ]
1807
+ },
1808
+ {
1809
+ matcher: "Edit|Write",
1810
+ hooks: [
1811
+ { type: "command", command: hookCmd(hooksDir, "post-edit-context.js"), timeout: 5 }
1812
+ ]
1813
+ }
1814
+ ],
1815
+ Stop: [
1816
+ {
1817
+ hooks: [
1818
+ { type: "command", command: hookCmd(hooksDir, "session-end.js"), timeout: 15 }
1819
+ ]
1820
+ }
1821
+ ],
1822
+ PreCompact: [
1823
+ {
1824
+ hooks: [
1825
+ { type: "command", command: hookCmd(hooksDir, "pre-compact.js"), timeout: 10 }
1826
+ ]
1827
+ }
1828
+ ],
1829
+ UserPromptSubmit: [
1830
+ {
1831
+ hooks: [
1832
+ { type: "command", command: hookCmd(hooksDir, "user-prompt.js"), timeout: 5 },
1833
+ { type: "command", command: hookCmd(hooksDir, "intent-suggester.js"), timeout: 5 }
1834
+ ]
1835
+ }
1836
+ ]
1837
+ };
1838
+ }
1839
+ function installHooks(projectRoot) {
1840
+ const claudeDirName = getConfig().conventions?.claudeDirName ?? ".claude";
1841
+ const claudeDir = resolve4(projectRoot, claudeDirName);
1842
+ const settingsPath = resolve4(claudeDir, "settings.local.json");
1843
+ if (!existsSync5(claudeDir)) {
1844
+ mkdirSync3(claudeDir, { recursive: true });
1845
+ }
1846
+ let settings = {};
1847
+ if (existsSync5(settingsPath)) {
1848
+ try {
1849
+ settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
1850
+ } catch {
1851
+ settings = {};
1852
+ }
1853
+ }
1854
+ const hooksDir = resolveHooksDir();
1855
+ const hooksConfig = buildHooksConfig(hooksDir);
1856
+ let hookCount = 0;
1857
+ for (const groups of Object.values(hooksConfig)) {
1858
+ for (const group of groups) {
1859
+ hookCount += group.hooks.length;
1860
+ }
1518
1861
  }
1519
- if (opts?.dateTo) {
1520
- sql += " AND ct.created_at <= ?";
1521
- params.push(opts.dateTo);
1862
+ settings.hooks = hooksConfig;
1863
+ writeFileSync2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
1864
+ return { installed: true, count: hookCount };
1865
+ }
1866
+ function initMemoryDir(projectRoot) {
1867
+ const encodedRoot = "-" + projectRoot.replace(/\//g, "-");
1868
+ const memoryDir = resolve4(homedir2(), `.claude/projects/${encodedRoot}/memory`);
1869
+ let created = false;
1870
+ if (!existsSync5(memoryDir)) {
1871
+ mkdirSync3(memoryDir, { recursive: true });
1872
+ created = true;
1522
1873
  }
1523
- if (opts?.minToolCalls !== void 0) {
1524
- sql += " AND ct.tool_call_count >= ?";
1525
- params.push(opts.minToolCalls);
1874
+ const memoryMdPath = resolve4(memoryDir, "MEMORY.md");
1875
+ let memoryMdCreated = false;
1876
+ if (!existsSync5(memoryMdPath)) {
1877
+ const projectName = basename2(projectRoot);
1878
+ const memoryContent = `# ${projectName} - Massu Memory
1879
+
1880
+ ## Key Learnings
1881
+ <!-- Important patterns and conventions discovered during development -->
1882
+
1883
+ ## Common Gotchas
1884
+ <!-- Non-obvious issues and how to avoid them -->
1885
+
1886
+ ## Corrections
1887
+ <!-- Wrong behaviors that were corrected and how to prevent them -->
1888
+
1889
+ ## File Index
1890
+ <!-- Significant files and directories -->
1891
+ `;
1892
+ writeFileSync2(memoryMdPath, memoryContent, "utf-8");
1893
+ memoryMdCreated = true;
1526
1894
  }
1527
- sql += " ORDER BY rank LIMIT ?";
1528
- params.push(limit);
1529
- return db.prepare(sql).all(...params);
1895
+ return { created, memoryMdCreated };
1530
1896
  }
1531
- function getToolPatterns(db, opts) {
1532
- const groupBy = opts?.groupBy ?? "tool";
1533
- const params = [];
1534
- let whereClause = "";
1535
- const conditions = [];
1536
- if (opts?.sessionId) {
1537
- conditions.push("session_id = ?");
1538
- params.push(opts.sessionId);
1897
+ async function runInit() {
1898
+ const projectRoot = process.cwd();
1899
+ console.log("");
1900
+ console.log("Massu AI - Project Setup");
1901
+ console.log("========================");
1902
+ console.log("");
1903
+ const framework = detectFramework(projectRoot);
1904
+ const frameworkParts = [];
1905
+ if (framework.type !== "javascript") frameworkParts.push(capitalize(framework.type));
1906
+ if (framework.ui !== "none") frameworkParts.push(formatName(framework.ui));
1907
+ if (framework.orm !== "none") frameworkParts.push(capitalize(framework.orm));
1908
+ if (framework.router !== "none") frameworkParts.push(framework.router.toUpperCase());
1909
+ const detected = frameworkParts.length > 0 ? frameworkParts.join(", ") : "JavaScript";
1910
+ console.log(` Detected: ${detected}`);
1911
+ const python = detectPython(projectRoot);
1912
+ if (python.detected) {
1913
+ const pyParts = ["Python"];
1914
+ if (python.hasFastapi) pyParts.push("FastAPI");
1915
+ if (python.hasSqlalchemy) pyParts.push("SQLAlchemy");
1916
+ if (python.hasAlembic) pyParts.push("Alembic");
1917
+ console.log(` Detected: ${pyParts.join(", ")} (root: ${python.root})`);
1539
1918
  }
1540
- if (opts?.toolName) {
1541
- conditions.push("tool_name = ?");
1542
- params.push(opts.toolName);
1919
+ const configCreated = generateConfig(projectRoot, framework);
1920
+ if (configCreated) {
1921
+ console.log(" Created massu.config.yaml");
1922
+ } else {
1923
+ console.log(" massu.config.yaml already exists (preserved)");
1543
1924
  }
1544
- if (opts?.dateFrom) {
1545
- conditions.push("created_at >= ?");
1546
- params.push(opts.dateFrom);
1925
+ const mcpRegistered = registerMcpServer(projectRoot);
1926
+ if (mcpRegistered) {
1927
+ console.log(" Registered MCP server in .mcp.json");
1928
+ } else {
1929
+ console.log(" MCP server already registered in .mcp.json");
1547
1930
  }
1548
- if (conditions.length > 0) {
1549
- whereClause = "WHERE " + conditions.join(" AND ");
1931
+ const { count: hooksCount } = installHooks(projectRoot);
1932
+ console.log(` Installed ${hooksCount} hooks in .claude/settings.local.json`);
1933
+ const cmdResult = installCommands(projectRoot);
1934
+ const cmdTotal = cmdResult.installed + cmdResult.updated + cmdResult.skipped;
1935
+ if (cmdResult.installed > 0 || cmdResult.updated > 0) {
1936
+ console.log(` Installed ${cmdTotal} slash commands (${cmdResult.installed} new, ${cmdResult.updated} updated)`);
1937
+ } else {
1938
+ console.log(` ${cmdTotal} slash commands already up to date`);
1550
1939
  }
1551
- let sql;
1552
- switch (groupBy) {
1553
- case "session":
1554
- sql = `SELECT session_id, COUNT(*) as call_count, COUNT(DISTINCT tool_name) as unique_tools,
1555
- SUM(CASE WHEN tool_success = 1 THEN 1 ELSE 0 END) as successes,
1556
- SUM(CASE WHEN tool_success = 0 THEN 1 ELSE 0 END) as failures,
1557
- AVG(tool_output_size) as avg_output_size
1558
- FROM tool_call_details ${whereClause}
1559
- GROUP BY session_id ORDER BY call_count DESC`;
1560
- break;
1561
- case "day":
1562
- sql = `SELECT date(created_at) as day, COUNT(*) as call_count, COUNT(DISTINCT tool_name) as unique_tools,
1563
- SUM(CASE WHEN tool_success = 1 THEN 1 ELSE 0 END) as successes
1564
- FROM tool_call_details ${whereClause}
1565
- GROUP BY date(created_at) ORDER BY day DESC`;
1566
- break;
1567
- default:
1568
- sql = `SELECT tool_name, COUNT(*) as call_count,
1569
- SUM(CASE WHEN tool_success = 1 THEN 1 ELSE 0 END) as successes,
1570
- SUM(CASE WHEN tool_success = 0 THEN 1 ELSE 0 END) as failures,
1571
- AVG(tool_output_size) as avg_output_size,
1572
- AVG(tool_input_size) as avg_input_size
1573
- FROM tool_call_details ${whereClause}
1574
- GROUP BY tool_name ORDER BY call_count DESC`;
1575
- break;
1940
+ const { created: memDirCreated, memoryMdCreated } = initMemoryDir(projectRoot);
1941
+ if (memDirCreated) {
1942
+ console.log(" Created memory directory (~/.claude/projects/.../memory/)");
1943
+ } else {
1944
+ console.log(" Memory directory already exists");
1576
1945
  }
1577
- return db.prepare(sql).all(...params);
1578
- }
1579
- function getSessionStats(db, opts) {
1580
- if (opts?.sessionId) {
1581
- const turns = db.prepare("SELECT COUNT(*) as turn_count, SUM(tool_call_count) as total_tool_calls, SUM(prompt_tokens) as total_prompt_tokens, SUM(response_tokens) as total_response_tokens FROM conversation_turns WHERE session_id = ?").get(opts.sessionId);
1582
- const toolBreakdown = db.prepare("SELECT tool_name, COUNT(*) as count FROM tool_call_details WHERE session_id = ? GROUP BY tool_name ORDER BY count DESC").all(opts.sessionId);
1583
- const session = db.prepare("SELECT * FROM sessions WHERE session_id = ?").get(opts.sessionId);
1584
- return [{
1585
- session_id: opts.sessionId,
1586
- status: session?.status ?? "unknown",
1587
- started_at: session?.started_at ?? null,
1588
- ended_at: session?.ended_at ?? null,
1589
- ...turns,
1590
- tool_breakdown: toolBreakdown
1591
- }];
1946
+ if (memoryMdCreated) {
1947
+ console.log(" Created initial MEMORY.md");
1592
1948
  }
1593
- const limit = opts?.limit ?? 10;
1594
- return db.prepare(`
1595
- SELECT s.session_id, s.status, s.started_at, s.ended_at,
1596
- COUNT(ct.id) as turn_count,
1597
- COALESCE(SUM(ct.tool_call_count), 0) as total_tool_calls,
1598
- COALESCE(SUM(ct.prompt_tokens), 0) as total_prompt_tokens,
1599
- COALESCE(SUM(ct.response_tokens), 0) as total_response_tokens
1600
- FROM sessions s
1601
- LEFT JOIN conversation_turns ct ON s.session_id = ct.session_id
1602
- GROUP BY s.session_id
1603
- ORDER BY s.started_at_epoch DESC
1604
- LIMIT ?
1605
- `).all(limit);
1949
+ try {
1950
+ const claudeDirName = ".claude";
1951
+ const encodedRoot = projectRoot.replace(/\//g, "-");
1952
+ const computedMemoryDir = resolve4(homedir2(), claudeDirName, "projects", encodedRoot, "memory");
1953
+ const memFiles = existsSync5(computedMemoryDir) ? readdirSync3(computedMemoryDir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md") : [];
1954
+ if (memFiles.length > 0) {
1955
+ const { getMemoryDb: getMemoryDb2 } = await Promise.resolve().then(() => (init_memory_db(), memory_db_exports));
1956
+ const db = getMemoryDb2();
1957
+ try {
1958
+ const stats = backfillMemoryFiles(db, computedMemoryDir, `init-${Date.now()}`);
1959
+ if (stats.inserted > 0 || stats.updated > 0) {
1960
+ console.log(` Backfilled ${stats.inserted + stats.updated} memory files into database (${stats.inserted} new, ${stats.updated} updated)`);
1961
+ }
1962
+ } finally {
1963
+ db.close();
1964
+ }
1965
+ }
1966
+ } catch (_backfillErr) {
1967
+ }
1968
+ console.log(" Databases will auto-create on first session");
1969
+ console.log("");
1970
+ console.log("Massu AI is ready. Start a Claude Code session to begin.");
1971
+ console.log("");
1972
+ console.log("Next steps:");
1973
+ console.log(" claude # Start a session (hooks activate automatically)");
1974
+ console.log(" npx massu doctor # Verify installation health");
1975
+ console.log("");
1976
+ console.log("Documentation: https://massu.ai/docs");
1977
+ console.log("");
1606
1978
  }
1607
- function getObservabilityDbSize(db) {
1608
- const turnsCount = db.prepare("SELECT COUNT(*) as c FROM conversation_turns").get().c;
1609
- const detailsCount = db.prepare("SELECT COUNT(*) as c FROM tool_call_details").get().c;
1610
- const obsCount = db.prepare("SELECT COUNT(*) as c FROM observations").get().c;
1611
- const pageCount = db.pragma("page_count")[0]?.page_count ?? 0;
1612
- const pageSize = db.pragma("page_size")[0]?.page_size ?? 4096;
1613
- return {
1614
- conversation_turns_count: turnsCount,
1615
- tool_call_details_count: detailsCount,
1616
- observations_count: obsCount,
1617
- db_page_count: pageCount,
1618
- db_page_size: pageSize,
1619
- estimated_size_mb: Math.round(pageCount * pageSize / (1024 * 1024) * 100) / 100
1979
+ function capitalize(str) {
1980
+ return str.charAt(0).toUpperCase() + str.slice(1);
1981
+ }
1982
+ function formatName(name) {
1983
+ const names = {
1984
+ nextjs: "Next.js",
1985
+ sveltekit: "SvelteKit",
1986
+ nuxt: "Nuxt",
1987
+ angular: "Angular",
1988
+ vue: "Vue",
1989
+ react: "React"
1620
1990
  };
1991
+ return names[name] ?? capitalize(name);
1621
1992
  }
1622
- var init_memory_db = __esm({
1623
- "src/memory-db.ts"() {
1993
+ var __filename2, __dirname2;
1994
+ var init_init = __esm({
1995
+ "src/commands/init.ts"() {
1624
1996
  "use strict";
1997
+ init_memory_file_ingest();
1625
1998
  init_config();
1999
+ init_install_commands();
2000
+ __filename2 = fileURLToPath2(import.meta.url);
2001
+ __dirname2 = dirname4(__filename2);
1626
2002
  }
1627
2003
  });
1628
2004
 
@@ -1921,18 +2297,18 @@ __export(doctor_exports, {
1921
2297
  runDoctor: () => runDoctor,
1922
2298
  runValidateConfig: () => runValidateConfig
1923
2299
  });
1924
- import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
2300
+ import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync4 } from "fs";
1925
2301
  import { resolve as resolve5, dirname as dirname5 } from "path";
1926
2302
  import { fileURLToPath as fileURLToPath3 } from "url";
1927
- import { parse as parseYaml2 } from "yaml";
2303
+ import { parse as parseYaml3 } from "yaml";
1928
2304
  function checkConfig(projectRoot) {
1929
2305
  const configPath = resolve5(projectRoot, "massu.config.yaml");
1930
- if (!existsSync5(configPath)) {
2306
+ if (!existsSync6(configPath)) {
1931
2307
  return { name: "Configuration", status: "fail", detail: "massu.config.yaml not found. Run: npx massu init" };
1932
2308
  }
1933
2309
  try {
1934
- const content = readFileSync4(configPath, "utf-8");
1935
- const parsed = parseYaml2(content);
2310
+ const content = readFileSync5(configPath, "utf-8");
2311
+ const parsed = parseYaml3(content);
1936
2312
  if (!parsed || typeof parsed !== "object") {
1937
2313
  return { name: "Configuration", status: "fail", detail: "massu.config.yaml is empty or invalid YAML" };
1938
2314
  }
@@ -1943,11 +2319,11 @@ function checkConfig(projectRoot) {
1943
2319
  }
1944
2320
  function checkMcpServer(projectRoot) {
1945
2321
  const mcpPath = getResolvedPaths().mcpJsonPath;
1946
- if (!existsSync5(mcpPath)) {
2322
+ if (!existsSync6(mcpPath)) {
1947
2323
  return { name: "MCP Server", status: "fail", detail: ".mcp.json not found. Run: npx massu init" };
1948
2324
  }
1949
2325
  try {
1950
- const content = JSON.parse(readFileSync4(mcpPath, "utf-8"));
2326
+ const content = JSON.parse(readFileSync5(mcpPath, "utf-8"));
1951
2327
  const servers = content.mcpServers ?? {};
1952
2328
  if (!servers.massu) {
1953
2329
  return { name: "MCP Server", status: "fail", detail: "massu not registered in .mcp.json. Run: npx massu init" };
@@ -1959,11 +2335,11 @@ function checkMcpServer(projectRoot) {
1959
2335
  }
1960
2336
  function checkHooksConfig(projectRoot) {
1961
2337
  const settingsPath = getResolvedPaths().settingsLocalPath;
1962
- if (!existsSync5(settingsPath)) {
2338
+ if (!existsSync6(settingsPath)) {
1963
2339
  return { name: "Hooks Config", status: "fail", detail: ".claude/settings.local.json not found. Run: npx massu init" };
1964
2340
  }
1965
2341
  try {
1966
- const content = JSON.parse(readFileSync4(settingsPath, "utf-8"));
2342
+ const content = JSON.parse(readFileSync5(settingsPath, "utf-8"));
1967
2343
  if (!content.hooks) {
1968
2344
  return { name: "Hooks Config", status: "fail", detail: "No hooks configured. Run: npx massu install-hooks" };
1969
2345
  }
@@ -1989,9 +2365,9 @@ function checkHooksConfig(projectRoot) {
1989
2365
  function checkHookFiles(projectRoot) {
1990
2366
  const nodeModulesHooksDir = resolve5(projectRoot, "node_modules/@massu/core/dist/hooks");
1991
2367
  let hooksDir = nodeModulesHooksDir;
1992
- if (!existsSync5(nodeModulesHooksDir)) {
2368
+ if (!existsSync6(nodeModulesHooksDir)) {
1993
2369
  const devHooksDir = resolve5(__dirname3, "../../dist/hooks");
1994
- if (existsSync5(devHooksDir)) {
2370
+ if (existsSync6(devHooksDir)) {
1995
2371
  hooksDir = devHooksDir;
1996
2372
  } else {
1997
2373
  return { name: "Hook Files", status: "fail", detail: "Compiled hooks not found. Run: npm install @massu/core" };
@@ -1999,7 +2375,7 @@ function checkHookFiles(projectRoot) {
1999
2375
  }
2000
2376
  const missing = [];
2001
2377
  for (const hookFile of EXPECTED_HOOKS) {
2002
- if (!existsSync5(resolve5(hooksDir, hookFile))) {
2378
+ if (!existsSync6(resolve5(hooksDir, hookFile))) {
2003
2379
  missing.push(hookFile);
2004
2380
  }
2005
2381
  }
@@ -2026,7 +2402,7 @@ function checkNodeVersion() {
2026
2402
  }
2027
2403
  async function checkGitRepo(projectRoot) {
2028
2404
  const gitDir = resolve5(projectRoot, ".git");
2029
- if (!existsSync5(gitDir)) {
2405
+ if (!existsSync6(gitDir)) {
2030
2406
  return { name: "Git Repository", status: "warn", detail: "Not a git repository (optional but recommended)" };
2031
2407
  }
2032
2408
  try {
@@ -2044,7 +2420,7 @@ async function checkGitRepo(projectRoot) {
2044
2420
  }
2045
2421
  function checkKnowledgeDb(projectRoot) {
2046
2422
  const knowledgeDbPath = getResolvedPaths().memoryDbPath;
2047
- if (!existsSync5(knowledgeDbPath)) {
2423
+ if (!existsSync6(knowledgeDbPath)) {
2048
2424
  return {
2049
2425
  name: "Knowledge DB",
2050
2426
  status: "warn",
@@ -2055,7 +2431,7 @@ function checkKnowledgeDb(projectRoot) {
2055
2431
  }
2056
2432
  function checkMemoryDir(_projectRoot2) {
2057
2433
  const memoryDir = getResolvedPaths().memoryDir;
2058
- if (!existsSync5(memoryDir)) {
2434
+ if (!existsSync6(memoryDir)) {
2059
2435
  return {
2060
2436
  name: "Memory Directory",
2061
2437
  status: "warn",
@@ -2066,7 +2442,7 @@ function checkMemoryDir(_projectRoot2) {
2066
2442
  }
2067
2443
  function checkShellHooksWired(_projectRoot2) {
2068
2444
  const settingsPath = getResolvedPaths().settingsLocalPath;
2069
- if (!existsSync5(settingsPath)) {
2445
+ if (!existsSync6(settingsPath)) {
2070
2446
  return {
2071
2447
  name: "Shell Hooks",
2072
2448
  status: "fail",
@@ -2074,7 +2450,7 @@ function checkShellHooksWired(_projectRoot2) {
2074
2450
  };
2075
2451
  }
2076
2452
  try {
2077
- const content = JSON.parse(readFileSync4(settingsPath, "utf-8"));
2453
+ const content = JSON.parse(readFileSync5(settingsPath, "utf-8"));
2078
2454
  const hooks = content.hooks ?? {};
2079
2455
  const hasSessionStart = Array.isArray(hooks.SessionStart) && hooks.SessionStart.length > 0;
2080
2456
  const hasPreToolUse = Array.isArray(hooks.PreToolUse) && hooks.PreToolUse.length > 0;
@@ -2126,7 +2502,7 @@ function checkPythonHealth(projectRoot) {
2126
2502
  const config = getConfig();
2127
2503
  if (!config.python?.root) return null;
2128
2504
  const pythonRoot = resolve5(projectRoot, config.python.root);
2129
- if (!existsSync5(pythonRoot)) {
2505
+ if (!existsSync6(pythonRoot)) {
2130
2506
  return {
2131
2507
  name: "Python",
2132
2508
  status: "fail",
@@ -2140,15 +2516,15 @@ function checkPythonHealth(projectRoot) {
2140
2516
  function scanDir(dir, depth) {
2141
2517
  if (depth > 5) return;
2142
2518
  try {
2143
- const entries = readdirSync3(dir, { withFileTypes: true });
2519
+ const entries = readdirSync4(dir, { withFileTypes: true });
2144
2520
  for (const entry of entries) {
2145
2521
  if (entry.isDirectory()) {
2146
2522
  const excludeDirs = config.python?.exclude_dirs || ["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"];
2147
2523
  if (!excludeDirs.includes(entry.name)) {
2148
2524
  const subdir = resolve5(dir, entry.name);
2149
- if (depth <= 2 && !existsSync5(resolve5(subdir, "__init__.py"))) {
2525
+ if (depth <= 2 && !existsSync6(resolve5(subdir, "__init__.py"))) {
2150
2526
  try {
2151
- const subEntries = readdirSync3(subdir);
2527
+ const subEntries = readdirSync4(subdir);
2152
2528
  if (subEntries.some((f) => f.endsWith(".py") && f !== "__init__.py")) {
2153
2529
  initPyMissing.push(entry.name);
2154
2530
  }
@@ -2242,15 +2618,15 @@ async function runDoctor() {
2242
2618
  async function runValidateConfig() {
2243
2619
  const projectRoot = process.cwd();
2244
2620
  const configPath = resolve5(projectRoot, "massu.config.yaml");
2245
- if (!existsSync5(configPath)) {
2621
+ if (!existsSync6(configPath)) {
2246
2622
  console.error("Error: massu.config.yaml not found in current directory");
2247
2623
  console.error("Run: npx massu init");
2248
2624
  process.exit(1);
2249
2625
  return;
2250
2626
  }
2251
2627
  try {
2252
- const content = readFileSync4(configPath, "utf-8");
2253
- const parsed = parseYaml2(content);
2628
+ const content = readFileSync5(configPath, "utf-8");
2629
+ const parsed = parseYaml3(content);
2254
2630
  if (!parsed || typeof parsed !== "object") {
2255
2631
  console.error("Error: massu.config.yaml is empty or not a valid YAML object");
2256
2632
  process.exit(1);
@@ -2331,11 +2707,11 @@ var init_install_hooks = __esm({
2331
2707
 
2332
2708
  // src/db.ts
2333
2709
  import Database2 from "better-sqlite3";
2334
- import { dirname as dirname6, join } from "path";
2335
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync as readdirSync4, statSync } from "fs";
2710
+ import { dirname as dirname6, join as join3 } from "path";
2711
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
2336
2712
  function getCodeGraphDb() {
2337
2713
  const dbPath = getResolvedPaths().codegraphDbPath;
2338
- if (!existsSync6(dbPath)) {
2714
+ if (!existsSync7(dbPath)) {
2339
2715
  throw new Error(`CodeGraph database not found at ${dbPath}. Run 'npx @colbymchenry/codegraph sync' first.`);
2340
2716
  }
2341
2717
  const db = new Database2(dbPath, { readonly: true });
@@ -2345,7 +2721,7 @@ function getCodeGraphDb() {
2345
2721
  function getDataDb() {
2346
2722
  const dbPath = getResolvedPaths().dataDbPath;
2347
2723
  const dir = dirname6(dbPath);
2348
- if (!existsSync6(dir)) {
2724
+ if (!existsSync7(dir)) {
2349
2725
  mkdirSync4(dir, { recursive: true });
2350
2726
  }
2351
2727
  const db = new Database2(dbPath);
@@ -2616,14 +2992,14 @@ function isPythonDataStale(dataDb2, pythonRoot) {
2616
2992
  const lastBuildTime = new Date(lastBuild.value).getTime();
2617
2993
  function checkDir(dir) {
2618
2994
  try {
2619
- const entries = readdirSync4(dir, { withFileTypes: true });
2995
+ const entries = readdirSync5(dir, { withFileTypes: true });
2620
2996
  for (const entry of entries) {
2621
- const fullPath = join(dir, entry.name);
2997
+ const fullPath = join3(dir, entry.name);
2622
2998
  if (entry.isDirectory()) {
2623
2999
  if (["__pycache__", ".venv", "venv", "node_modules", ".mypy_cache", ".pytest_cache"].includes(entry.name)) continue;
2624
3000
  if (checkDir(fullPath)) return true;
2625
3001
  } else if (entry.name.endsWith(".py")) {
2626
- if (statSync(fullPath).mtimeMs > lastBuildTime) return true;
3002
+ if (statSync2(fullPath).mtimeMs > lastBuildTime) return true;
2627
3003
  }
2628
3004
  }
2629
3005
  } catch {
@@ -2719,8 +3095,8 @@ var init_rules = __esm({
2719
3095
  });
2720
3096
 
2721
3097
  // src/import-resolver.ts
2722
- import { readFileSync as readFileSync5, existsSync as existsSync7, statSync as statSync2 } from "fs";
2723
- import { resolve as resolve7, dirname as dirname7, join as join2 } from "path";
3098
+ import { readFileSync as readFileSync6, existsSync as existsSync8, statSync as statSync3 } from "fs";
3099
+ import { resolve as resolve7, dirname as dirname7, join as join4 } from "path";
2724
3100
  function parseImports(source) {
2725
3101
  const imports = [];
2726
3102
  const lines = source.split("\n");
@@ -2780,19 +3156,19 @@ function resolveImportPath(specifier, fromFile) {
2780
3156
  } else {
2781
3157
  basePath = resolve7(dirname7(fromFile), specifier);
2782
3158
  }
2783
- if (existsSync7(basePath) && !isDirectory(basePath)) {
3159
+ if (existsSync8(basePath) && !isDirectory(basePath)) {
2784
3160
  return toRelative(basePath);
2785
3161
  }
2786
3162
  const resolvedPaths = getResolvedPaths();
2787
3163
  for (const ext of resolvedPaths.extensions) {
2788
3164
  const withExt = basePath + ext;
2789
- if (existsSync7(withExt)) {
3165
+ if (existsSync8(withExt)) {
2790
3166
  return toRelative(withExt);
2791
3167
  }
2792
3168
  }
2793
3169
  for (const indexFile of resolvedPaths.indexFiles) {
2794
- const indexPath = join2(basePath, indexFile);
2795
- if (existsSync7(indexPath)) {
3170
+ const indexPath = join4(basePath, indexFile);
3171
+ if (existsSync8(indexPath)) {
2796
3172
  return toRelative(indexPath);
2797
3173
  }
2798
3174
  }
@@ -2800,7 +3176,7 @@ function resolveImportPath(specifier, fromFile) {
2800
3176
  }
2801
3177
  function isDirectory(path) {
2802
3178
  try {
2803
- return statSync2(path).isDirectory();
3179
+ return statSync3(path).isDirectory();
2804
3180
  } catch {
2805
3181
  return false;
2806
3182
  }
@@ -2829,10 +3205,10 @@ function buildImportIndex(dataDb2, codegraphDb2) {
2829
3205
  let batch = [];
2830
3206
  for (const file of files) {
2831
3207
  const absPath = ensureWithinRoot(resolve7(projectRoot, file.path), projectRoot);
2832
- if (!existsSync7(absPath)) continue;
3208
+ if (!existsSync8(absPath)) continue;
2833
3209
  let source;
2834
3210
  try {
2835
- source = readFileSync5(absPath, "utf-8");
3211
+ source = readFileSync6(absPath, "utf-8");
2836
3212
  } catch {
2837
3213
  continue;
2838
3214
  }
@@ -2868,15 +3244,15 @@ var init_import_resolver = __esm({
2868
3244
  });
2869
3245
 
2870
3246
  // src/trpc-index.ts
2871
- import { readFileSync as readFileSync6, existsSync as existsSync8, readdirSync as readdirSync5 } from "fs";
2872
- import { resolve as resolve8, join as join3 } from "path";
3247
+ import { readFileSync as readFileSync7, existsSync as existsSync9, readdirSync as readdirSync6 } from "fs";
3248
+ import { resolve as resolve8, join as join5 } from "path";
2873
3249
  function parseRootRouter() {
2874
3250
  const paths = getResolvedPaths();
2875
3251
  const rootPath = paths.rootRouterPath;
2876
- if (!existsSync8(rootPath)) {
3252
+ if (!existsSync9(rootPath)) {
2877
3253
  throw new Error(`Root router not found at ${rootPath}`);
2878
3254
  }
2879
- const source = readFileSync6(rootPath, "utf-8");
3255
+ const source = readFileSync7(rootPath, "utf-8");
2880
3256
  const mappings = [];
2881
3257
  const importMap = /* @__PURE__ */ new Map();
2882
3258
  const importRegex = /import\s+\{[^}]*?(\w+Router)[^}]*\}\s+from\s+['"]\.\/routers\/([^'"]+)['"]/g;
@@ -2888,12 +3264,12 @@ function parseRootRouter() {
2888
3264
  for (const ext of [".ts", ".tsx", ""]) {
2889
3265
  const candidate = fullPath + ext;
2890
3266
  const routersRelPath = getConfig().paths.routers ?? "src/server/api/routers";
2891
- if (existsSync8(candidate)) {
3267
+ if (existsSync9(candidate)) {
2892
3268
  filePath = routersRelPath + "/" + filePath + ext;
2893
3269
  break;
2894
3270
  }
2895
- const indexCandidate = join3(fullPath, "index.ts");
2896
- if (existsSync8(indexCandidate)) {
3271
+ const indexCandidate = join5(fullPath, "index.ts");
3272
+ if (existsSync9(indexCandidate)) {
2897
3273
  filePath = routersRelPath + "/" + filePath + "/index.ts";
2898
3274
  break;
2899
3275
  }
@@ -2913,8 +3289,8 @@ function parseRootRouter() {
2913
3289
  }
2914
3290
  function extractProcedures(routerFilePath) {
2915
3291
  const absPath = resolve8(getProjectRoot(), routerFilePath);
2916
- if (!existsSync8(absPath)) return [];
2917
- const source = readFileSync6(absPath, "utf-8");
3292
+ if (!existsSync9(absPath)) return [];
3293
+ const source = readFileSync7(absPath, "utf-8");
2918
3294
  const procedures = [];
2919
3295
  const seen = /* @__PURE__ */ new Set();
2920
3296
  const procRegex = /(\w+)\s*:\s*(protected|public)Procedure/g;
@@ -2943,21 +3319,21 @@ function findUICallSites(routerKey, procedureName) {
2943
3319
  ];
2944
3320
  const searchPattern = `api.${routerKey}.${procedureName}`;
2945
3321
  for (const dir of searchDirs) {
2946
- if (!existsSync8(dir)) continue;
3322
+ if (!existsSync9(dir)) continue;
2947
3323
  searchDirectory(dir, searchPattern, callSites);
2948
3324
  }
2949
3325
  return callSites;
2950
3326
  }
2951
3327
  function searchDirectory(dir, pattern, results) {
2952
- const entries = readdirSync5(dir, { withFileTypes: true });
3328
+ const entries = readdirSync6(dir, { withFileTypes: true });
2953
3329
  for (const entry of entries) {
2954
- const fullPath = join3(dir, entry.name);
3330
+ const fullPath = join5(dir, entry.name);
2955
3331
  if (entry.isDirectory()) {
2956
3332
  if (entry.name === "node_modules" || entry.name === ".next") continue;
2957
3333
  searchDirectory(fullPath, pattern, results);
2958
3334
  } else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
2959
3335
  try {
2960
- const source = readFileSync6(fullPath, "utf-8");
3336
+ const source = readFileSync7(fullPath, "utf-8");
2961
3337
  const lines = source.split("\n");
2962
3338
  for (let i = 0; i < lines.length; i++) {
2963
3339
  if (lines[i].includes(pattern)) {
@@ -3020,7 +3396,7 @@ var init_trpc_index = __esm({
3020
3396
  });
3021
3397
 
3022
3398
  // src/page-deps.ts
3023
- import { readFileSync as readFileSync7, existsSync as existsSync9 } from "fs";
3399
+ import { readFileSync as readFileSync8, existsSync as existsSync10 } from "fs";
3024
3400
  import { resolve as resolve9 } from "path";
3025
3401
  function deriveRoute(pageFile) {
3026
3402
  let route = pageFile.replace(/^src\/app/, "").replace(/\/page\.tsx?$/, "").replace(/\/page\.jsx?$/, "");
@@ -3060,9 +3436,9 @@ function findRouterCalls(files) {
3060
3436
  const projectRoot = getProjectRoot();
3061
3437
  for (const file of files) {
3062
3438
  const absPath = ensureWithinRoot(resolve9(projectRoot, file), projectRoot);
3063
- if (!existsSync9(absPath)) continue;
3439
+ if (!existsSync10(absPath)) continue;
3064
3440
  try {
3065
- const source = readFileSync7(absPath, "utf-8");
3441
+ const source = readFileSync8(absPath, "utf-8");
3066
3442
  const apiCallRegex = /api\.(\w+)\.\w+/g;
3067
3443
  let match;
3068
3444
  while ((match = apiCallRegex.exec(source)) !== null) {
@@ -3081,9 +3457,9 @@ function findTablesFromRouters(routerNames, dataDb2) {
3081
3457
  ).all(routerName);
3082
3458
  for (const proc of procs) {
3083
3459
  const absPath = ensureWithinRoot(resolve9(getProjectRoot(), proc.router_file), getProjectRoot());
3084
- if (!existsSync9(absPath)) continue;
3460
+ if (!existsSync10(absPath)) continue;
3085
3461
  try {
3086
- const source = readFileSync7(absPath, "utf-8");
3462
+ const source = readFileSync8(absPath, "utf-8");
3087
3463
  const dbPattern = getConfig().dbAccessPattern ?? "ctx.db.{table}";
3088
3464
  const regexStr = dbPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace("\\{table\\}", "(\\w+)");
3089
3465
  const tableRegex = new RegExp(regexStr + "\\.", "g");
@@ -3352,14 +3728,14 @@ var init_domains = __esm({
3352
3728
  });
3353
3729
 
3354
3730
  // src/schema-mapper.ts
3355
- import { readFileSync as readFileSync8, existsSync as existsSync10, readdirSync as readdirSync6 } from "fs";
3356
- import { join as join4 } from "path";
3731
+ import { readFileSync as readFileSync9, existsSync as existsSync11, readdirSync as readdirSync7 } from "fs";
3732
+ import { join as join6 } from "path";
3357
3733
  function parsePrismaSchema() {
3358
3734
  const schemaPath = getResolvedPaths().prismaSchemaPath;
3359
- if (!existsSync10(schemaPath)) {
3735
+ if (!existsSync11(schemaPath)) {
3360
3736
  throw new Error(`Prisma schema not found at ${schemaPath}`);
3361
3737
  }
3362
- const source = readFileSync8(schemaPath, "utf-8");
3738
+ const source = readFileSync9(schemaPath, "utf-8");
3363
3739
  const models = [];
3364
3740
  const sourceLines = source.split("\n");
3365
3741
  let i = 0;
@@ -3417,14 +3793,14 @@ function toSnakeCase(str) {
3417
3793
  function findColumnUsageInRouters(tableName) {
3418
3794
  const usage = /* @__PURE__ */ new Map();
3419
3795
  const routersDir = getResolvedPaths().routersDir;
3420
- if (!existsSync10(routersDir)) return usage;
3796
+ if (!existsSync11(routersDir)) return usage;
3421
3797
  scanDirectory(routersDir, tableName, usage);
3422
3798
  return usage;
3423
3799
  }
3424
3800
  function scanDirectory(dir, tableName, usage) {
3425
- const entries = readdirSync6(dir, { withFileTypes: true });
3801
+ const entries = readdirSync7(dir, { withFileTypes: true });
3426
3802
  for (const entry of entries) {
3427
- const fullPath = join4(dir, entry.name);
3803
+ const fullPath = join6(dir, entry.name);
3428
3804
  if (entry.isDirectory()) {
3429
3805
  scanDirectory(fullPath, tableName, usage);
3430
3806
  } else if (entry.name.endsWith(".ts")) {
@@ -3434,7 +3810,7 @@ function scanDirectory(dir, tableName, usage) {
3434
3810
  }
3435
3811
  function scanFile(absPath, tableName, usage) {
3436
3812
  try {
3437
- const source = readFileSync8(absPath, "utf-8");
3813
+ const source = readFileSync9(absPath, "utf-8");
3438
3814
  if (!source.includes(tableName)) return;
3439
3815
  const relPath = absPath.slice(getProjectRoot().length + 1);
3440
3816
  const lines = source.split("\n");
@@ -3479,15 +3855,15 @@ function detectMismatches(models) {
3479
3855
  }
3480
3856
  function findFilesUsingColumn(dir, column, tableName) {
3481
3857
  const result = [];
3482
- if (!existsSync10(dir)) return result;
3483
- const entries = readdirSync6(dir, { withFileTypes: true });
3858
+ if (!existsSync11(dir)) return result;
3859
+ const entries = readdirSync7(dir, { withFileTypes: true });
3484
3860
  for (const entry of entries) {
3485
- const fullPath = join4(dir, entry.name);
3861
+ const fullPath = join6(dir, entry.name);
3486
3862
  if (entry.isDirectory()) {
3487
3863
  result.push(...findFilesUsingColumn(fullPath, column, tableName));
3488
3864
  } else if (entry.name.endsWith(".ts")) {
3489
3865
  try {
3490
- const source = readFileSync8(fullPath, "utf-8");
3866
+ const source = readFileSync9(fullPath, "utf-8");
3491
3867
  if (source.includes(tableName) && source.includes(column)) {
3492
3868
  result.push(fullPath.slice(getProjectRoot().length + 1));
3493
3869
  }
@@ -3632,8 +4008,8 @@ var init_import_parser = __esm({
3632
4008
  });
3633
4009
 
3634
4010
  // src/python/import-resolver.ts
3635
- import { readFileSync as readFileSync9, existsSync as existsSync11, readdirSync as readdirSync7 } from "fs";
3636
- import { resolve as resolve11, join as join5, relative, dirname as dirname8 } from "path";
4011
+ import { readFileSync as readFileSync10, existsSync as existsSync12, readdirSync as readdirSync8 } from "fs";
4012
+ import { resolve as resolve11, join as join7, relative as relative2, dirname as dirname8 } from "path";
3637
4013
  function resolvePythonModulePath(module, fromFile, pythonRoot, level) {
3638
4014
  const projectRoot = getProjectRoot();
3639
4015
  if (level > 0) {
@@ -3644,36 +4020,36 @@ function resolvePythonModulePath(module, fromFile, pythonRoot, level) {
3644
4020
  const modulePart = module.replace(/^\.+/, "");
3645
4021
  if (modulePart) {
3646
4022
  const parts2 = modulePart.split(".");
3647
- return tryResolvePythonPath(join5(baseDir, ...parts2), projectRoot);
4023
+ return tryResolvePythonPath(join7(baseDir, ...parts2), projectRoot);
3648
4024
  }
3649
4025
  return tryResolvePythonPath(baseDir, projectRoot);
3650
4026
  }
3651
4027
  const parts = module.split(".");
3652
- const candidate = join5(resolve11(projectRoot, pythonRoot), ...parts);
4028
+ const candidate = join7(resolve11(projectRoot, pythonRoot), ...parts);
3653
4029
  return tryResolvePythonPath(candidate, projectRoot);
3654
4030
  }
3655
4031
  function tryResolvePythonPath(basePath, projectRoot) {
3656
- if (existsSync11(basePath + ".py")) {
3657
- return relative(projectRoot, basePath + ".py");
4032
+ if (existsSync12(basePath + ".py")) {
4033
+ return relative2(projectRoot, basePath + ".py");
3658
4034
  }
3659
- if (existsSync11(join5(basePath, "__init__.py"))) {
3660
- return relative(projectRoot, join5(basePath, "__init__.py"));
4035
+ if (existsSync12(join7(basePath, "__init__.py"))) {
4036
+ return relative2(projectRoot, join7(basePath, "__init__.py"));
3661
4037
  }
3662
- if (basePath.endsWith(".py") && existsSync11(basePath)) {
3663
- return relative(projectRoot, basePath);
4038
+ if (basePath.endsWith(".py") && existsSync12(basePath)) {
4039
+ return relative2(projectRoot, basePath);
3664
4040
  }
3665
4041
  return null;
3666
4042
  }
3667
4043
  function walkPythonFiles(dir, excludeDirs) {
3668
4044
  const files = [];
3669
4045
  try {
3670
- const entries = readdirSync7(dir, { withFileTypes: true });
4046
+ const entries = readdirSync8(dir, { withFileTypes: true });
3671
4047
  for (const entry of entries) {
3672
4048
  if (entry.isDirectory()) {
3673
4049
  if (excludeDirs.includes(entry.name)) continue;
3674
- files.push(...walkPythonFiles(join5(dir, entry.name), excludeDirs));
4050
+ files.push(...walkPythonFiles(join7(dir, entry.name), excludeDirs));
3675
4051
  } else if (entry.name.endsWith(".py")) {
3676
- files.push(join5(dir, entry.name));
4052
+ files.push(join7(dir, entry.name));
3677
4053
  }
3678
4054
  }
3679
4055
  } catch {
@@ -3696,10 +4072,10 @@ function buildPythonImportIndex(dataDb2, pythonRoot, excludeDirs = ["__pycache__
3696
4072
  });
3697
4073
  const batch = [];
3698
4074
  for (const absFile of files) {
3699
- const relFile = relative(projectRoot, absFile);
4075
+ const relFile = relative2(projectRoot, absFile);
3700
4076
  let source;
3701
4077
  try {
3702
- source = readFileSync9(absFile, "utf-8");
4078
+ source = readFileSync10(absFile, "utf-8");
3703
4079
  } catch {
3704
4080
  continue;
3705
4081
  }
@@ -3965,18 +4341,18 @@ var init_route_parser = __esm({
3965
4341
  });
3966
4342
 
3967
4343
  // src/python/route-indexer.ts
3968
- import { readFileSync as readFileSync10, readdirSync as readdirSync8 } from "fs";
3969
- import { join as join6, relative as relative2 } from "path";
4344
+ import { readFileSync as readFileSync11, readdirSync as readdirSync9 } from "fs";
4345
+ import { join as join8, relative as relative3 } from "path";
3970
4346
  function walkPyFiles(dir, excludeDirs) {
3971
4347
  const files = [];
3972
4348
  try {
3973
- const entries = readdirSync8(dir, { withFileTypes: true });
4349
+ const entries = readdirSync9(dir, { withFileTypes: true });
3974
4350
  for (const entry of entries) {
3975
4351
  if (entry.isDirectory()) {
3976
4352
  if (excludeDirs.includes(entry.name)) continue;
3977
- files.push(...walkPyFiles(join6(dir, entry.name), excludeDirs));
4353
+ files.push(...walkPyFiles(join8(dir, entry.name), excludeDirs));
3978
4354
  } else if (entry.name.endsWith(".py")) {
3979
- files.push(join6(dir, entry.name));
4355
+ files.push(join8(dir, entry.name));
3980
4356
  }
3981
4357
  }
3982
4358
  } catch {
@@ -3985,7 +4361,7 @@ function walkPyFiles(dir, excludeDirs) {
3985
4361
  }
3986
4362
  function buildPythonRouteIndex(dataDb2, pythonRoot, excludeDirs = ["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"]) {
3987
4363
  const projectRoot = getProjectRoot();
3988
- const absRoot = join6(projectRoot, pythonRoot);
4364
+ const absRoot = join8(projectRoot, pythonRoot);
3989
4365
  dataDb2.exec("DELETE FROM massu_py_routes");
3990
4366
  dataDb2.exec("DELETE FROM massu_py_route_callers");
3991
4367
  const insertStmt = dataDb2.prepare(
@@ -3995,10 +4371,10 @@ function buildPythonRouteIndex(dataDb2, pythonRoot, excludeDirs = ["__pycache__"
3995
4371
  let count = 0;
3996
4372
  dataDb2.transaction(() => {
3997
4373
  for (const absFile of files) {
3998
- const relFile = relative2(projectRoot, absFile);
4374
+ const relFile = relative3(projectRoot, absFile);
3999
4375
  let source;
4000
4376
  try {
4001
- source = readFileSync10(absFile, "utf-8");
4377
+ source = readFileSync11(absFile, "utf-8");
4002
4378
  } catch {
4003
4379
  continue;
4004
4380
  }
@@ -4209,18 +4585,18 @@ var init_model_parser = __esm({
4209
4585
  });
4210
4586
 
4211
4587
  // src/python/model-indexer.ts
4212
- import { readFileSync as readFileSync11, readdirSync as readdirSync9 } from "fs";
4213
- import { join as join7, relative as relative3 } from "path";
4588
+ import { readFileSync as readFileSync12, readdirSync as readdirSync10 } from "fs";
4589
+ import { join as join9, relative as relative4 } from "path";
4214
4590
  function walkPyFiles2(dir, excludeDirs) {
4215
4591
  const files = [];
4216
4592
  try {
4217
- const entries = readdirSync9(dir, { withFileTypes: true });
4593
+ const entries = readdirSync10(dir, { withFileTypes: true });
4218
4594
  for (const entry of entries) {
4219
4595
  if (entry.isDirectory()) {
4220
4596
  if (excludeDirs.includes(entry.name)) continue;
4221
- files.push(...walkPyFiles2(join7(dir, entry.name), excludeDirs));
4597
+ files.push(...walkPyFiles2(join9(dir, entry.name), excludeDirs));
4222
4598
  } else if (entry.name.endsWith(".py")) {
4223
- files.push(join7(dir, entry.name));
4599
+ files.push(join9(dir, entry.name));
4224
4600
  }
4225
4601
  }
4226
4602
  } catch {
@@ -4229,7 +4605,7 @@ function walkPyFiles2(dir, excludeDirs) {
4229
4605
  }
4230
4606
  function buildPythonModelIndex(dataDb2, pythonRoot, excludeDirs = ["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"]) {
4231
4607
  const projectRoot = getProjectRoot();
4232
- const absRoot = join7(projectRoot, pythonRoot);
4608
+ const absRoot = join9(projectRoot, pythonRoot);
4233
4609
  dataDb2.exec("DELETE FROM massu_py_models");
4234
4610
  dataDb2.exec("DELETE FROM massu_py_fk_edges");
4235
4611
  const insertModel = dataDb2.prepare(
@@ -4242,10 +4618,10 @@ function buildPythonModelIndex(dataDb2, pythonRoot, excludeDirs = ["__pycache__"
4242
4618
  let count = 0;
4243
4619
  dataDb2.transaction(() => {
4244
4620
  for (const absFile of files) {
4245
- const relFile = relative3(projectRoot, absFile);
4621
+ const relFile = relative4(projectRoot, absFile);
4246
4622
  let source;
4247
4623
  try {
4248
- source = readFileSync11(absFile, "utf-8");
4624
+ source = readFileSync12(absFile, "utf-8");
4249
4625
  } catch {
4250
4626
  continue;
4251
4627
  }
@@ -4505,19 +4881,19 @@ var init_migration_parser = __esm({
4505
4881
  });
4506
4882
 
4507
4883
  // src/python/migration-indexer.ts
4508
- import { readFileSync as readFileSync12, readdirSync as readdirSync10 } from "fs";
4509
- import { join as join8, relative as relative4 } from "path";
4884
+ import { readFileSync as readFileSync13, readdirSync as readdirSync11 } from "fs";
4885
+ import { join as join10, relative as relative5 } from "path";
4510
4886
  function buildPythonMigrationIndex(dataDb2, alembicDir) {
4511
4887
  const projectRoot = getProjectRoot();
4512
- const absDir = join8(projectRoot, alembicDir);
4888
+ const absDir = join10(projectRoot, alembicDir);
4513
4889
  dataDb2.exec("DELETE FROM massu_py_migrations");
4514
- const versionsDir = join8(absDir, "versions");
4890
+ const versionsDir = join10(absDir, "versions");
4515
4891
  let files = [];
4516
4892
  try {
4517
- files = readdirSync10(versionsDir).filter((f) => f.endsWith(".py")).map((f) => join8(versionsDir, f));
4893
+ files = readdirSync11(versionsDir).filter((f) => f.endsWith(".py")).map((f) => join10(versionsDir, f));
4518
4894
  } catch {
4519
4895
  try {
4520
- files = readdirSync10(absDir).filter((f) => f.endsWith(".py") && f !== "env.py").map((f) => join8(absDir, f));
4896
+ files = readdirSync11(absDir).filter((f) => f.endsWith(".py") && f !== "env.py").map((f) => join10(absDir, f));
4521
4897
  } catch {
4522
4898
  }
4523
4899
  }
@@ -4531,7 +4907,7 @@ function buildPythonMigrationIndex(dataDb2, alembicDir) {
4531
4907
  for (const absFile of files) {
4532
4908
  let source;
4533
4909
  try {
4534
- source = readFileSync12(absFile, "utf-8");
4910
+ source = readFileSync13(absFile, "utf-8");
4535
4911
  } catch {
4536
4912
  continue;
4537
4913
  }
@@ -4542,7 +4918,7 @@ function buildPythonMigrationIndex(dataDb2, alembicDir) {
4542
4918
  rows.push({
4543
4919
  revision: parsed.revision,
4544
4920
  downRevision: parsed.downRevision,
4545
- file: relative4(projectRoot, absFile),
4921
+ file: relative5(projectRoot, absFile),
4546
4922
  description: parsed.description,
4547
4923
  operations: JSON.stringify(parsed.operations)
4548
4924
  });
@@ -4565,12 +4941,12 @@ var init_migration_indexer = __esm({
4565
4941
  });
4566
4942
 
4567
4943
  // src/python/coupling-detector.ts
4568
- import { readFileSync as readFileSync13, readdirSync as readdirSync11 } from "fs";
4569
- import { join as join9, relative as relative5 } from "path";
4944
+ import { readFileSync as readFileSync14, readdirSync as readdirSync12 } from "fs";
4945
+ import { join as join11, relative as relative6 } from "path";
4570
4946
  function buildPythonCouplingIndex(dataDb2) {
4571
4947
  const projectRoot = getProjectRoot();
4572
4948
  const config = getConfig();
4573
- const srcDir = join9(projectRoot, config.paths.source);
4949
+ const srcDir = join11(projectRoot, config.paths.source);
4574
4950
  const routes = dataDb2.prepare("SELECT id, method, path FROM massu_py_routes").all();
4575
4951
  if (routes.length === 0) return 0;
4576
4952
  dataDb2.exec("DELETE FROM massu_py_route_callers");
@@ -4599,10 +4975,10 @@ function buildPythonCouplingIndex(dataDb2) {
4599
4975
  ];
4600
4976
  dataDb2.transaction(() => {
4601
4977
  for (const absFile of frontendFiles) {
4602
- const relFile = relative5(projectRoot, absFile);
4978
+ const relFile = relative6(projectRoot, absFile);
4603
4979
  let source;
4604
4980
  try {
4605
- source = readFileSync13(absFile, "utf-8");
4981
+ source = readFileSync14(absFile, "utf-8");
4606
4982
  } catch {
4607
4983
  continue;
4608
4984
  }
@@ -4630,13 +5006,13 @@ function walkFrontendFiles(dir) {
4630
5006
  const files = [];
4631
5007
  const exclude = ["node_modules", ".next", "dist", ".git", "__pycache__", ".venv", "venv"];
4632
5008
  try {
4633
- const entries = readdirSync11(dir, { withFileTypes: true });
5009
+ const entries = readdirSync12(dir, { withFileTypes: true });
4634
5010
  for (const entry of entries) {
4635
5011
  if (entry.isDirectory()) {
4636
5012
  if (exclude.includes(entry.name)) continue;
4637
- files.push(...walkFrontendFiles(join9(dir, entry.name)));
5013
+ files.push(...walkFrontendFiles(join11(dir, entry.name)));
4638
5014
  } else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
4639
- files.push(join9(dir, entry.name));
5015
+ files.push(join11(dir, entry.name));
4640
5016
  }
4641
5017
  }
4642
5018
  } catch {
@@ -4742,6 +5118,16 @@ function getMemoryToolDefinitions() {
4742
5118
  required: []
4743
5119
  }
4744
5120
  },
5121
+ // P4-007: memory_backfill
5122
+ {
5123
+ name: p("memory_backfill"),
5124
+ description: "Scan all memory/*.md files and ingest into database. Run after massu init or to recover from DB loss. Parses YAML frontmatter and deduplicates by title.",
5125
+ inputSchema: {
5126
+ type: "object",
5127
+ properties: {},
5128
+ required: []
5129
+ }
5130
+ },
4745
5131
  // P4-006: memory_ingest
4746
5132
  {
4747
5133
  name: p("memory_ingest"),
@@ -4786,6 +5172,8 @@ function handleMemoryToolCall(name, args2, memoryDb) {
4786
5172
  return handleFailures(args2, memoryDb);
4787
5173
  case "memory_ingest":
4788
5174
  return handleIngest(args2, memoryDb);
5175
+ case "memory_backfill":
5176
+ return handleBackfill(memoryDb);
4789
5177
  default:
4790
5178
  return text(`Unknown memory tool: ${name}`);
4791
5179
  }
@@ -4960,6 +5348,21 @@ Title: ${title}
4960
5348
  Importance: ${importance}
4961
5349
  Session: ${activeSession.session_id.slice(0, 8)}...`);
4962
5350
  }
5351
+ function handleBackfill(db) {
5352
+ const memoryDir = getResolvedPaths().memoryDir;
5353
+ const stats = backfillMemoryFiles(db, memoryDir);
5354
+ const lines = [
5355
+ "## Memory Backfill Results",
5356
+ "",
5357
+ `- **Total files scanned**: ${stats.total}`,
5358
+ `- **Inserted (new)**: ${stats.inserted}`,
5359
+ `- **Updated (existing)**: ${stats.updated}`,
5360
+ `- **Skipped (not found)**: ${stats.skipped}`,
5361
+ "",
5362
+ stats.total === 0 ? "No memory files found in memory directory." : `Successfully processed ${stats.inserted + stats.updated} of ${stats.total} memory files.`
5363
+ ];
5364
+ return text(lines.join("\n"));
5365
+ }
4963
5366
  function text(content) {
4964
5367
  return { content: [{ type: "text", text: content }] };
4965
5368
  }
@@ -4975,11 +5378,12 @@ var init_memory_tools = __esm({
4975
5378
  "use strict";
4976
5379
  init_memory_db();
4977
5380
  init_config();
5381
+ init_memory_file_ingest();
4978
5382
  }
4979
5383
  });
4980
5384
 
4981
5385
  // src/docs-tools.ts
4982
- import { readFileSync as readFileSync14, existsSync as existsSync12 } from "fs";
5386
+ import { readFileSync as readFileSync15, existsSync as existsSync13 } from "fs";
4983
5387
  import { resolve as resolve12, basename as basename3 } from "path";
4984
5388
  function p2(baseName) {
4985
5389
  return `${getConfig().toolPrefix}_${baseName}`;
@@ -5035,10 +5439,10 @@ function handleDocsToolCall(name, args2) {
5035
5439
  }
5036
5440
  function loadDocsMap() {
5037
5441
  const mapPath = getResolvedPaths().docsMapPath;
5038
- if (!existsSync12(mapPath)) {
5442
+ if (!existsSync13(mapPath)) {
5039
5443
  throw new Error(`docs-map.json not found at ${mapPath}`);
5040
5444
  }
5041
- return JSON.parse(readFileSync14(mapPath, "utf-8"));
5445
+ return JSON.parse(readFileSync15(mapPath, "utf-8"));
5042
5446
  }
5043
5447
  function matchesPattern(filePath, pattern) {
5044
5448
  const regexStr = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
@@ -5109,12 +5513,12 @@ function extractFrontmatter(content) {
5109
5513
  function extractProcedureNames(routerPath) {
5110
5514
  const root = getProjectRoot();
5111
5515
  const absPath = ensureWithinRoot(resolve12(getResolvedPaths().srcDir, "..", routerPath), root);
5112
- if (!existsSync12(absPath)) {
5516
+ if (!existsSync13(absPath)) {
5113
5517
  const altPath = ensureWithinRoot(resolve12(getResolvedPaths().srcDir, "../server/api/routers", basename3(routerPath)), root);
5114
- if (!existsSync12(altPath)) return [];
5115
- return extractProcedureNamesFromContent(readFileSync14(altPath, "utf-8"));
5518
+ if (!existsSync13(altPath)) return [];
5519
+ return extractProcedureNamesFromContent(readFileSync15(altPath, "utf-8"));
5116
5520
  }
5117
- return extractProcedureNamesFromContent(readFileSync14(absPath, "utf-8"));
5521
+ return extractProcedureNamesFromContent(readFileSync15(absPath, "utf-8"));
5118
5522
  }
5119
5523
  function extractProcedureNamesFromContent(content) {
5120
5524
  const procRegex = /\.(?:query|mutation)\s*\(/g;
@@ -5155,7 +5559,7 @@ function handleDocsAudit(args2) {
5155
5559
  const mapping = docsMap.mappings.find((m) => m.id === mappingId);
5156
5560
  if (!mapping) continue;
5157
5561
  const helpPagePath = ensureWithinRoot(resolve12(getResolvedPaths().helpSitePath, mapping.helpPage), getProjectRoot());
5158
- if (!existsSync12(helpPagePath)) {
5562
+ if (!existsSync13(helpPagePath)) {
5159
5563
  results.push({
5160
5564
  helpPage: mapping.helpPage,
5161
5565
  mappingId,
@@ -5167,7 +5571,7 @@ function handleDocsAudit(args2) {
5167
5571
  });
5168
5572
  continue;
5169
5573
  }
5170
- const content = readFileSync14(helpPagePath, "utf-8");
5574
+ const content = readFileSync15(helpPagePath, "utf-8");
5171
5575
  const sections = extractSections(content);
5172
5576
  const frontmatter = extractFrontmatter(content);
5173
5577
  const staleReasons = [];
@@ -5208,8 +5612,8 @@ function handleDocsAudit(args2) {
5208
5612
  for (const [guideName, parentId] of Object.entries(docsMap.userGuideInheritance.examples)) {
5209
5613
  if (parentId === mappingId) {
5210
5614
  const guidePath = ensureWithinRoot(resolve12(getResolvedPaths().helpSitePath, `pages/user-guides/${guideName}/index.mdx`), getProjectRoot());
5211
- if (existsSync12(guidePath)) {
5212
- const guideContent = readFileSync14(guidePath, "utf-8");
5615
+ if (existsSync13(guidePath)) {
5616
+ const guideContent = readFileSync15(guidePath, "utf-8");
5213
5617
  const guideFrontmatter = extractFrontmatter(guideContent);
5214
5618
  if (!guideFrontmatter?.lastVerified || status === "STALE") {
5215
5619
  results.push({
@@ -5243,13 +5647,13 @@ function handleDocsCoverage(args2) {
5243
5647
  const mappings = filterDomain ? docsMap.mappings.filter((m) => m.id === filterDomain) : docsMap.mappings;
5244
5648
  for (const mapping of mappings) {
5245
5649
  const helpPagePath = ensureWithinRoot(resolve12(getResolvedPaths().helpSitePath, mapping.helpPage), getProjectRoot());
5246
- const exists = existsSync12(helpPagePath);
5650
+ const exists = existsSync13(helpPagePath);
5247
5651
  let hasContent = false;
5248
5652
  let lineCount = 0;
5249
5653
  let lastVerified = null;
5250
5654
  let status = null;
5251
5655
  if (exists) {
5252
- const content = readFileSync14(helpPagePath, "utf-8");
5656
+ const content = readFileSync15(helpPagePath, "utf-8");
5253
5657
  lineCount = content.split("\n").length;
5254
5658
  hasContent = lineCount > 10;
5255
5659
  const frontmatter = extractFrontmatter(content);
@@ -5590,7 +5994,7 @@ var init_observability_tools = __esm({
5590
5994
  });
5591
5995
 
5592
5996
  // src/sentinel-db.ts
5593
- import { existsSync as existsSync13 } from "fs";
5997
+ import { existsSync as existsSync14 } from "fs";
5594
5998
  import { resolve as resolve13 } from "path";
5595
5999
  function parsePortalScope(raw) {
5596
6000
  if (!raw) return [];
@@ -5828,22 +6232,22 @@ function validateFeatures(db, domainFilter) {
5828
6232
  const missingPages = [];
5829
6233
  for (const comp of components) {
5830
6234
  const absPath = resolve13(PROJECT_ROOT, comp.component_file);
5831
- if (!existsSync13(absPath)) {
6235
+ if (!existsSync14(absPath)) {
5832
6236
  missingComponents.push(comp.component_file);
5833
6237
  }
5834
6238
  }
5835
6239
  for (const proc of procedures) {
5836
6240
  const routerPath = resolve13(PROJECT_ROOT, `src/server/api/routers/${proc.router_name}.ts`);
5837
- if (!existsSync13(routerPath)) {
6241
+ if (!existsSync14(routerPath)) {
5838
6242
  missingProcedures.push({ router: proc.router_name, procedure: proc.procedure_name });
5839
6243
  }
5840
6244
  }
5841
6245
  for (const page of pages) {
5842
6246
  const routeToPath = page.page_route.replace(/^\/(portal-[^/]+\/)?/, "src/app/").replace(/\/$/, "") + "/page.tsx";
5843
6247
  const absPath = resolve13(PROJECT_ROOT, routeToPath);
5844
- if (page.page_route.startsWith("/") && !existsSync13(absPath)) {
6248
+ if (page.page_route.startsWith("/") && !existsSync14(absPath)) {
5845
6249
  const altPath = resolve13(PROJECT_ROOT, `src/app${page.page_route}/page.tsx`);
5846
- if (!existsSync13(altPath)) {
6250
+ if (!existsSync14(altPath)) {
5847
6251
  missingPages.push(page.page_route);
5848
6252
  }
5849
6253
  }
@@ -6367,8 +6771,8 @@ var init_sentinel_tools = __esm({
6367
6771
  });
6368
6772
 
6369
6773
  // src/sentinel-scanner.ts
6370
- import { readFileSync as readFileSync15, existsSync as existsSync14, readdirSync as readdirSync12, statSync as statSync3 } from "fs";
6371
- import { resolve as resolve14, join as join10, basename as basename4, dirname as dirname9, relative as relative6 } from "path";
6774
+ import { readFileSync as readFileSync16, existsSync as existsSync15, readdirSync as readdirSync13, statSync as statSync4 } from "fs";
6775
+ import { resolve as resolve14, join as join12, basename as basename4, dirname as dirname9, relative as relative7 } from "path";
6372
6776
  function inferDomain(filePath) {
6373
6777
  const domains = getConfig().domains;
6374
6778
  const path = filePath.toLowerCase();
@@ -6498,9 +6902,9 @@ function scanComponentExports(dataDb2) {
6498
6902
  const componentsBase = config.paths.components ?? config.paths.source + "/components";
6499
6903
  const componentDirs = [];
6500
6904
  const basePath = resolve14(projectRoot, componentsBase);
6501
- if (existsSync14(basePath)) {
6905
+ if (existsSync15(basePath)) {
6502
6906
  try {
6503
- const entries = readdirSync12(basePath, { withFileTypes: true });
6907
+ const entries = readdirSync13(basePath, { withFileTypes: true });
6504
6908
  for (const entry of entries) {
6505
6909
  if (entry.isDirectory()) {
6506
6910
  componentDirs.push(componentsBase + "/" + entry.name);
@@ -6511,11 +6915,11 @@ function scanComponentExports(dataDb2) {
6511
6915
  }
6512
6916
  for (const dir of componentDirs) {
6513
6917
  const absDir = resolve14(projectRoot, dir);
6514
- if (!existsSync14(absDir)) continue;
6918
+ if (!existsSync15(absDir)) continue;
6515
6919
  const files = walkDir(absDir).filter((f) => f.endsWith(".tsx") || f.endsWith(".ts"));
6516
6920
  for (const file of files) {
6517
- const relPath = relative6(projectRoot, file);
6518
- const source = readFileSync15(file, "utf-8");
6921
+ const relPath = relative7(projectRoot, file);
6922
+ const source = readFileSync16(file, "utf-8");
6519
6923
  const annotations = parseFeatureAnnotations(source);
6520
6924
  if (annotations.length > 0) {
6521
6925
  for (const ann of annotations) {
@@ -6564,11 +6968,11 @@ function scanComponentExports(dataDb2) {
6564
6968
  function walkDir(dir) {
6565
6969
  const results = [];
6566
6970
  try {
6567
- const entries = readdirSync12(dir);
6971
+ const entries = readdirSync13(dir);
6568
6972
  for (const entry of entries) {
6569
- const fullPath = join10(dir, entry);
6973
+ const fullPath = join12(dir, entry);
6570
6974
  try {
6571
- const stat = statSync3(fullPath);
6975
+ const stat = statSync4(fullPath);
6572
6976
  if (stat.isDirectory()) {
6573
6977
  results.push(...walkDir(fullPath));
6574
6978
  } else {
@@ -7554,7 +7958,7 @@ var init_audit_trail = __esm({
7554
7958
  });
7555
7959
 
7556
7960
  // src/validation-engine.ts
7557
- import { existsSync as existsSync15, readFileSync as readFileSync16 } from "fs";
7961
+ import { existsSync as existsSync16, readFileSync as readFileSync17 } from "fs";
7558
7962
  function p9(baseName) {
7559
7963
  return `${getConfig().toolPrefix}_${baseName}`;
7560
7964
  }
@@ -7585,7 +7989,7 @@ function validateFile(filePath, projectRoot) {
7585
7989
  });
7586
7990
  return checks;
7587
7991
  }
7588
- if (!existsSync15(absPath)) {
7992
+ if (!existsSync16(absPath)) {
7589
7993
  checks.push({
7590
7994
  name: "file_exists",
7591
7995
  severity: "error",
@@ -7594,7 +7998,7 @@ function validateFile(filePath, projectRoot) {
7594
7998
  });
7595
7999
  return checks;
7596
8000
  }
7597
- const source = readFileSync16(absPath, "utf-8");
8001
+ const source = readFileSync17(absPath, "utf-8");
7598
8002
  const lines = source.split("\n");
7599
8003
  if (activeChecks.rule_compliance !== false) {
7600
8004
  for (const ruleSet of config.rules) {
@@ -8039,7 +8443,7 @@ var init_adr_generator = __esm({
8039
8443
  });
8040
8444
 
8041
8445
  // src/security-scorer.ts
8042
- import { existsSync as existsSync16, readFileSync as readFileSync17 } from "fs";
8446
+ import { existsSync as existsSync17, readFileSync as readFileSync18 } from "fs";
8043
8447
  function p11(baseName) {
8044
8448
  return `${getConfig().toolPrefix}_${baseName}`;
8045
8449
  }
@@ -8063,12 +8467,12 @@ function scoreFileSecurity(filePath, projectRoot) {
8063
8467
  }]
8064
8468
  };
8065
8469
  }
8066
- if (!existsSync16(absPath)) {
8470
+ if (!existsSync17(absPath)) {
8067
8471
  return { riskScore: 0, findings: [] };
8068
8472
  }
8069
8473
  let source;
8070
8474
  try {
8071
- source = readFileSync17(absPath, "utf-8");
8475
+ source = readFileSync18(absPath, "utf-8");
8072
8476
  } catch {
8073
8477
  return { riskScore: 0, findings: [] };
8074
8478
  }
@@ -8357,7 +8761,7 @@ var init_security_scorer = __esm({
8357
8761
  });
8358
8762
 
8359
8763
  // src/dependency-scorer.ts
8360
- import { existsSync as existsSync17, readFileSync as readFileSync18 } from "fs";
8764
+ import { existsSync as existsSync18, readFileSync as readFileSync19 } from "fs";
8361
8765
  import { resolve as resolve15 } from "path";
8362
8766
  function p12(baseName) {
8363
8767
  return `${getConfig().toolPrefix}_${baseName}`;
@@ -8391,9 +8795,9 @@ function calculateDepRisk(factors) {
8391
8795
  }
8392
8796
  function getInstalledPackages(projectRoot) {
8393
8797
  const pkgPath = resolve15(projectRoot, "package.json");
8394
- if (!existsSync17(pkgPath)) return /* @__PURE__ */ new Map();
8798
+ if (!existsSync18(pkgPath)) return /* @__PURE__ */ new Map();
8395
8799
  try {
8396
- const pkg = JSON.parse(readFileSync18(pkgPath, "utf-8"));
8800
+ const pkg = JSON.parse(readFileSync19(pkgPath, "utf-8"));
8397
8801
  const packages = /* @__PURE__ */ new Map();
8398
8802
  for (const [name, version] of Object.entries(pkg.dependencies ?? {})) {
8399
8803
  packages.set(name, version);
@@ -8996,8 +9400,8 @@ var init_regression_detector = __esm({
8996
9400
 
8997
9401
  // src/knowledge-indexer.ts
8998
9402
  import { createHash as createHash2 } from "crypto";
8999
- import { readFileSync as readFileSync19, readdirSync as readdirSync13, statSync as statSync4, existsSync as existsSync18 } from "fs";
9000
- import { resolve as resolve16, relative as relative7, basename as basename5, extname } from "path";
9403
+ import { readFileSync as readFileSync20, readdirSync as readdirSync14, statSync as statSync5, existsSync as existsSync19 } from "fs";
9404
+ import { resolve as resolve16, relative as relative8, basename as basename5, extname } from "path";
9001
9405
  function getKnowledgePaths() {
9002
9406
  const resolved = getResolvedPaths();
9003
9407
  const config = getConfig();
@@ -9023,7 +9427,7 @@ function discoverMarkdownFiles(baseDir) {
9023
9427
  const files = [];
9024
9428
  function walk(dir) {
9025
9429
  try {
9026
- const entries = readdirSync13(dir, { withFileTypes: true });
9430
+ const entries = readdirSync14(dir, { withFileTypes: true });
9027
9431
  for (const entry of entries) {
9028
9432
  const fullPath = resolve16(dir, entry.name);
9029
9433
  if (entry.isDirectory()) {
@@ -9045,7 +9449,7 @@ function categorizeFile(filePath) {
9045
9449
  const paths = getKnowledgePaths();
9046
9450
  if (filePath.startsWith(paths.plansDir)) return "plan";
9047
9451
  if (filePath.startsWith(paths.docsDir)) {
9048
- const relFromDocs = relative7(paths.docsDir, filePath).replace(/\\/g, "/").toLowerCase();
9452
+ const relFromDocs = relative8(paths.docsDir, filePath).replace(/\\/g, "/").toLowerCase();
9049
9453
  if (relFromDocs.startsWith("plans/")) return "plan";
9050
9454
  if (relFromDocs.includes("architecture")) return "architecture";
9051
9455
  if (relFromDocs.includes("security")) return "security";
@@ -9061,7 +9465,7 @@ function categorizeFile(filePath) {
9061
9465
  }
9062
9466
  const claudeDirName = getConfig().conventions?.claudeDirName ?? ".claude";
9063
9467
  if (filePath.includes(`${claudeDirName}/projects/`) && filePath.includes("/memory/")) return "memory";
9064
- const rel = relative7(paths.claudeDir, filePath).replace(/\\/g, "/");
9468
+ const rel = relative8(paths.claudeDir, filePath).replace(/\\/g, "/");
9065
9469
  const firstDir = rel.split("/")[0];
9066
9470
  const knownCategories = getConfig().conventions?.knowledgeCategories ?? [
9067
9471
  "patterns",
@@ -9303,11 +9707,11 @@ function indexAllKnowledge(db) {
9303
9707
  files.push(...memFiles);
9304
9708
  } catch {
9305
9709
  }
9306
- if (existsSync18(paths.plansDir)) {
9710
+ if (existsSync19(paths.plansDir)) {
9307
9711
  const planFiles = discoverMarkdownFiles(paths.plansDir);
9308
9712
  files.push(...planFiles);
9309
9713
  }
9310
- if (existsSync18(paths.docsDir)) {
9714
+ if (existsSync19(paths.docsDir)) {
9311
9715
  const excludePatterns = getConfig().conventions?.excludePatterns ?? ["/ARCHIVE/", "/SESSION-HISTORY/"];
9312
9716
  const docsFiles = discoverMarkdownFiles(paths.docsDir).filter((f) => !f.includes("/plans/") && !excludePatterns.some((p18) => f.includes(p18)));
9313
9717
  files.push(...docsFiles);
@@ -9350,10 +9754,10 @@ function indexAllKnowledge(db) {
9350
9754
  } catch {
9351
9755
  }
9352
9756
  for (const filePath of files) {
9353
- if (!existsSync18(filePath)) continue;
9354
- const content = readFileSync19(filePath, "utf-8");
9757
+ if (!existsSync19(filePath)) continue;
9758
+ const content = readFileSync20(filePath, "utf-8");
9355
9759
  const hash = hashContent(content);
9356
- const relPath = filePath.startsWith(paths.claudeDir) ? relative7(paths.claudeDir, filePath) : filePath.startsWith(paths.plansDir) ? "plans/" + relative7(paths.plansDir, filePath) : filePath.startsWith(paths.docsDir) ? "docs/" + relative7(paths.docsDir, filePath) : filePath.startsWith(paths.memoryDir) ? `memory/${relative7(paths.memoryDir, filePath)}` : basename5(filePath);
9760
+ const relPath = filePath.startsWith(paths.claudeDir) ? relative8(paths.claudeDir, filePath) : filePath.startsWith(paths.plansDir) ? "plans/" + relative8(paths.plansDir, filePath) : filePath.startsWith(paths.docsDir) ? "docs/" + relative8(paths.docsDir, filePath) : filePath.startsWith(paths.memoryDir) ? `memory/${relative8(paths.memoryDir, filePath)}` : basename5(filePath);
9357
9761
  const category = categorizeFile(filePath);
9358
9762
  const title = extractTitle(content, filePath);
9359
9763
  const description = extractDescription2(content);
@@ -9471,17 +9875,17 @@ function isKnowledgeStale(db) {
9471
9875
  files.push(...discoverMarkdownFiles(paths.memoryDir));
9472
9876
  } catch {
9473
9877
  }
9474
- if (existsSync18(paths.plansDir)) {
9878
+ if (existsSync19(paths.plansDir)) {
9475
9879
  files.push(...discoverMarkdownFiles(paths.plansDir));
9476
9880
  }
9477
- if (existsSync18(paths.docsDir)) {
9881
+ if (existsSync19(paths.docsDir)) {
9478
9882
  const excludePatterns = getConfig().conventions?.excludePatterns ?? ["/ARCHIVE/", "/SESSION-HISTORY/"];
9479
9883
  const docsFiles = discoverMarkdownFiles(paths.docsDir).filter((f) => !f.includes("/plans/") && !excludePatterns.some((p18) => f.includes(p18)));
9480
9884
  files.push(...docsFiles);
9481
9885
  }
9482
9886
  for (const filePath of files) {
9483
9887
  try {
9484
- const stat = statSync4(filePath);
9888
+ const stat = statSync5(filePath);
9485
9889
  if (stat.mtimeMs > lastIndexTime) return true;
9486
9890
  } catch {
9487
9891
  continue;
@@ -9503,7 +9907,7 @@ var init_knowledge_indexer = __esm({
9503
9907
  });
9504
9908
 
9505
9909
  // src/knowledge-tools.ts
9506
- import { readFileSync as readFileSync20, writeFileSync as writeFileSync3, appendFileSync, readdirSync as readdirSync14 } from "fs";
9910
+ import { readFileSync as readFileSync21, writeFileSync as writeFileSync3, appendFileSync, readdirSync as readdirSync15 } from "fs";
9507
9911
  import { resolve as resolve17, basename as basename6 } from "path";
9508
9912
  function p15(baseName) {
9509
9913
  return `${getConfig().toolPrefix}_${baseName}`;
@@ -10254,7 +10658,7 @@ ${crRule ? `- **CR**: ${crRule}
10254
10658
  `;
10255
10659
  let existing = "";
10256
10660
  try {
10257
- existing = readFileSync20(correctionsPath, "utf-8");
10661
+ existing = readFileSync21(correctionsPath, "utf-8");
10258
10662
  } catch {
10259
10663
  }
10260
10664
  const archiveIdx = existing.indexOf("## Archived");
@@ -10436,7 +10840,7 @@ function handleGaps(db, args2) {
10436
10840
  } else if (checkType === "routers") {
10437
10841
  try {
10438
10842
  const routersDir = getResolvedPaths().routersDir;
10439
- const routerFiles = readdirSync14(routersDir).filter((f) => f.endsWith(".ts") && !f.startsWith("_"));
10843
+ const routerFiles = readdirSync15(routersDir).filter((f) => f.endsWith(".ts") && !f.startsWith("_"));
10440
10844
  lines.push(`| Router | Knowledge Hits | Status |`);
10441
10845
  lines.push(`|--------|----------------|--------|`);
10442
10846
  for (const file of routerFiles) {
@@ -10593,11 +10997,11 @@ var init_knowledge_tools = __esm({
10593
10997
  // src/knowledge-db.ts
10594
10998
  import Database3 from "better-sqlite3";
10595
10999
  import { dirname as dirname10 } from "path";
10596
- import { existsSync as existsSync20, mkdirSync as mkdirSync5 } from "fs";
11000
+ import { existsSync as existsSync21, mkdirSync as mkdirSync5 } from "fs";
10597
11001
  function getKnowledgeDb() {
10598
11002
  const dbPath = getResolvedPaths().knowledgeDbPath;
10599
11003
  const dir = dirname10(dbPath);
10600
- if (!existsSync20(dir)) {
11004
+ if (!existsSync21(dir)) {
10601
11005
  mkdirSync5(dir, { recursive: true });
10602
11006
  }
10603
11007
  const db = new Database3(dbPath);
@@ -11331,7 +11735,7 @@ var init_python_tools = __esm({
11331
11735
  });
11332
11736
 
11333
11737
  // src/tools.ts
11334
- import { readFileSync as readFileSync21, existsSync as existsSync21 } from "fs";
11738
+ import { readFileSync as readFileSync22, existsSync as existsSync22 } from "fs";
11335
11739
  import { resolve as resolve18, basename as basename7 } from "path";
11336
11740
  function prefix() {
11337
11741
  return getConfig().toolPrefix;
@@ -11767,8 +12171,8 @@ function handleContext(file, dataDb2, codegraphDb2) {
11767
12171
  const resolvedPaths = getResolvedPaths();
11768
12172
  const root = getProjectRoot();
11769
12173
  const absFilePath = ensureWithinRoot(resolve18(resolvedPaths.srcDir, "..", file), root);
11770
- if (existsSync21(absFilePath)) {
11771
- const fileContent = readFileSync21(absFilePath, "utf-8").slice(0, 3e3);
12174
+ if (existsSync22(absFilePath)) {
12175
+ const fileContent = readFileSync22(absFilePath, "utf-8").slice(0, 3e3);
11772
12176
  const keywords = [];
11773
12177
  if (fileContent.includes("ctx.db")) keywords.push("database", "schema");
11774
12178
  if (fileContent.includes("BigInt") || fileContent.includes("Decimal")) keywords.push("BigInt", "serialization");
@@ -12193,10 +12597,10 @@ function handleSchema(args2) {
12193
12597
  lines.push("");
12194
12598
  const projectRoot = getProjectRoot();
12195
12599
  const absPath = ensureWithinRoot(resolve18(projectRoot, file), projectRoot);
12196
- if (!existsSync21(absPath)) {
12600
+ if (!existsSync22(absPath)) {
12197
12601
  return text17(`File not found: ${file}`);
12198
12602
  }
12199
- const source = readFileSync21(absPath, "utf-8");
12603
+ const source = readFileSync22(absPath, "utf-8");
12200
12604
  const config = getConfig();
12201
12605
  const dbPattern = config.dbAccessPattern ?? "ctx.db.{table}";
12202
12606
  const regexStr = dbPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace("\\{table\\}", "(\\w+)");
@@ -12274,7 +12678,7 @@ var init_tools = __esm({
12274
12678
 
12275
12679
  // src/server.ts
12276
12680
  var server_exports = {};
12277
- import { readFileSync as readFileSync22 } from "fs";
12681
+ import { readFileSync as readFileSync23 } from "fs";
12278
12682
  import { resolve as resolve19, dirname as dirname11 } from "path";
12279
12683
  import { fileURLToPath as fileURLToPath4 } from "url";
12280
12684
  function getDb() {
@@ -12370,7 +12774,7 @@ var init_server = __esm({
12370
12774
  __dirname4 = dirname11(fileURLToPath4(import.meta.url));
12371
12775
  PKG_VERSION = (() => {
12372
12776
  try {
12373
- const pkg = JSON.parse(readFileSync22(resolve19(__dirname4, "..", "package.json"), "utf-8"));
12777
+ const pkg = JSON.parse(readFileSync23(resolve19(__dirname4, "..", "package.json"), "utf-8"));
12374
12778
  return pkg.version ?? "0.0.0";
12375
12779
  } catch {
12376
12780
  return "0.0.0";
@@ -12434,7 +12838,7 @@ var init_server = __esm({
12434
12838
  });
12435
12839
 
12436
12840
  // src/cli.ts
12437
- import { readFileSync as readFileSync23 } from "fs";
12841
+ import { readFileSync as readFileSync24 } from "fs";
12438
12842
  import { resolve as resolve20, dirname as dirname12 } from "path";
12439
12843
  import { fileURLToPath as fileURLToPath5 } from "url";
12440
12844
  var __filename4 = fileURLToPath5(import.meta.url);
@@ -12509,7 +12913,7 @@ Documentation: https://massu.ai/docs
12509
12913
  }
12510
12914
  function printVersion() {
12511
12915
  try {
12512
- const pkg = JSON.parse(readFileSync23(resolve20(__dirname5, "../package.json"), "utf-8"));
12916
+ const pkg = JSON.parse(readFileSync24(resolve20(__dirname5, "../package.json"), "utf-8"));
12513
12917
  console.log(`massu v${pkg.version}`);
12514
12918
  } catch {
12515
12919
  console.log("massu v0.1.0");