@massu/core 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -344,822 +344,323 @@ 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);
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
+ );
424
+
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
+ `);
460
+ try {
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) {
438
469
  }
439
- });
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;
440
475
 
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;
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
+ `);
468
520
  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 {
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) {
490
529
  }
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
530
+ db.exec(`
531
+ CREATE TRIGGER IF NOT EXISTS prompts_ai AFTER INSERT ON user_prompts BEGIN
532
+ INSERT INTO user_prompts_fts(rowid, prompt_text) VALUES (new.id, new.prompt_text);
533
+ END;
633
534
 
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;
535
+ CREATE TRIGGER IF NOT EXISTS prompts_ad AFTER DELETE ON user_prompts BEGIN
536
+ INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
537
+ VALUES ('delete', old.id, old.prompt_text);
538
+ END;
539
+ `);
540
+ db.exec(`
541
+ CREATE TABLE IF NOT EXISTS memory_meta (
542
+ key TEXT PRIMARY KEY,
543
+ value TEXT NOT NULL
544
+ );
545
+ `);
546
+ db.exec(`
547
+ CREATE TABLE IF NOT EXISTS conversation_turns (
548
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
549
+ session_id TEXT NOT NULL,
550
+ turn_number INTEGER NOT NULL,
551
+ user_prompt TEXT NOT NULL,
552
+ assistant_response TEXT,
553
+ tool_calls_json TEXT,
554
+ tool_call_count INTEGER DEFAULT 0,
555
+ model_used TEXT,
556
+ duration_ms INTEGER,
557
+ prompt_tokens INTEGER,
558
+ response_tokens INTEGER,
559
+ created_at TEXT DEFAULT (datetime('now')),
560
+ created_at_epoch INTEGER DEFAULT (unixepoch()),
561
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
562
+ );
563
+
564
+ CREATE INDEX IF NOT EXISTS idx_ct_session ON conversation_turns(session_id);
565
+ CREATE INDEX IF NOT EXISTS idx_ct_created ON conversation_turns(created_at DESC);
566
+ CREATE INDEX IF NOT EXISTS idx_ct_turn ON conversation_turns(session_id, turn_number);
567
+ `);
568
+ db.exec(`
569
+ CREATE TABLE IF NOT EXISTS tool_call_details (
570
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
571
+ session_id TEXT NOT NULL,
572
+ turn_number INTEGER NOT NULL,
573
+ tool_name TEXT NOT NULL,
574
+ tool_input_summary TEXT,
575
+ tool_input_size INTEGER,
576
+ tool_output_size INTEGER,
577
+ tool_success INTEGER DEFAULT 1,
578
+ duration_ms INTEGER,
579
+ files_involved TEXT,
580
+ created_at TEXT DEFAULT (datetime('now')),
581
+ created_at_epoch INTEGER DEFAULT (unixepoch()),
582
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
583
+ );
584
+
585
+ CREATE INDEX IF NOT EXISTS idx_tcd_session ON tool_call_details(session_id);
586
+ CREATE INDEX IF NOT EXISTS idx_tcd_tool ON tool_call_details(tool_name);
587
+ CREATE INDEX IF NOT EXISTS idx_tcd_created ON tool_call_details(created_at DESC);
588
+ `);
589
+ try {
590
+ db.exec(`
591
+ CREATE VIRTUAL TABLE IF NOT EXISTS conversation_turns_fts USING fts5(
592
+ user_prompt,
593
+ assistant_response,
594
+ content=conversation_turns,
595
+ content_rowid=id
596
+ );
597
+ `);
598
+ } catch (_e) {
670
599
  }
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 -->
600
+ db.exec(`
601
+ CREATE TRIGGER IF NOT EXISTS ct_fts_insert AFTER INSERT ON conversation_turns BEGIN
602
+ INSERT INTO conversation_turns_fts(rowid, user_prompt, assistant_response)
603
+ VALUES (new.id, new.user_prompt, new.assistant_response);
604
+ END;
787
605
 
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
- });
606
+ CREATE TRIGGER IF NOT EXISTS ct_fts_delete AFTER DELETE ON conversation_turns BEGIN
607
+ INSERT INTO conversation_turns_fts(conversation_turns_fts, rowid, user_prompt, assistant_response)
608
+ VALUES ('delete', old.id, old.user_prompt, old.assistant_response);
609
+ END;
883
610
 
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) {
611
+ CREATE TRIGGER IF NOT EXISTS ct_fts_update AFTER UPDATE ON conversation_turns BEGIN
612
+ INSERT INTO conversation_turns_fts(conversation_turns_fts, rowid, user_prompt, assistant_response)
613
+ VALUES ('delete', old.id, old.user_prompt, old.assistant_response);
614
+ INSERT INTO conversation_turns_fts(rowid, user_prompt, assistant_response)
615
+ VALUES (new.id, new.user_prompt, new.assistant_response);
616
+ END;
617
+ `);
907
618
  db.exec(`
908
- -- Sessions table (linked to Claude Code session IDs)
909
- CREATE TABLE IF NOT EXISTS sessions (
619
+ CREATE TABLE IF NOT EXISTS session_quality_scores (
910
620
  id INTEGER PRIMARY KEY AUTOINCREMENT,
911
- session_id TEXT UNIQUE NOT NULL,
621
+ session_id TEXT NOT NULL UNIQUE,
912
622
  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
623
+ score INTEGER NOT NULL DEFAULT 100,
624
+ security_score INTEGER NOT NULL DEFAULT 100,
625
+ architecture_score INTEGER NOT NULL DEFAULT 100,
626
+ coupling_score INTEGER NOT NULL DEFAULT 100,
627
+ test_score INTEGER NOT NULL DEFAULT 100,
628
+ rule_compliance_score INTEGER NOT NULL DEFAULT 100,
629
+ observations_total INTEGER NOT NULL DEFAULT 0,
630
+ bugs_found INTEGER NOT NULL DEFAULT 0,
631
+ bugs_fixed INTEGER NOT NULL DEFAULT 0,
632
+ vr_checks_passed INTEGER NOT NULL DEFAULT 0,
633
+ vr_checks_failed INTEGER NOT NULL DEFAULT 0,
634
+ incidents_triggered INTEGER NOT NULL DEFAULT 0,
635
+ created_at TEXT DEFAULT (datetime('now')),
636
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
922
637
  );
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 (
638
+ CREATE INDEX IF NOT EXISTS idx_sqs_session ON session_quality_scores(session_id);
639
+ CREATE INDEX IF NOT EXISTS idx_sqs_project ON session_quality_scores(project);
640
+ `);
641
+ db.exec(`
642
+ CREATE TABLE IF NOT EXISTS session_costs (
643
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
644
+ session_id TEXT NOT NULL UNIQUE,
645
+ project TEXT NOT NULL DEFAULT 'my-project',
646
+ input_tokens INTEGER NOT NULL DEFAULT 0,
647
+ output_tokens INTEGER NOT NULL DEFAULT 0,
648
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
649
+ cache_write_tokens INTEGER NOT NULL DEFAULT 0,
650
+ total_tokens INTEGER NOT NULL DEFAULT 0,
651
+ estimated_cost_usd REAL NOT NULL DEFAULT 0.0,
652
+ model TEXT,
653
+ duration_minutes REAL NOT NULL DEFAULT 0.0,
654
+ tool_calls INTEGER NOT NULL DEFAULT 0,
655
+ created_at TEXT DEFAULT (datetime('now')),
656
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
657
+ );
658
+ CREATE INDEX IF NOT EXISTS idx_sc_session ON session_costs(session_id);
659
+ `);
660
+ db.exec(`
661
+ CREATE TABLE IF NOT EXISTS feature_costs (
930
662
  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) {
1028
- }
1029
- db.exec(`
1030
- CREATE TRIGGER IF NOT EXISTS prompts_ai AFTER INSERT ON user_prompts BEGIN
1031
- INSERT INTO user_prompts_fts(rowid, prompt_text) VALUES (new.id, new.prompt_text);
1032
- END;
1033
-
1034
- CREATE TRIGGER IF NOT EXISTS prompts_ad AFTER DELETE ON user_prompts BEGIN
1035
- INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
1036
- VALUES ('delete', old.id, old.prompt_text);
1037
- END;
1038
- `);
1039
- db.exec(`
1040
- CREATE TABLE IF NOT EXISTS memory_meta (
1041
- key TEXT PRIMARY KEY,
1042
- value TEXT NOT NULL
1043
- );
1044
- `);
1045
- db.exec(`
1046
- CREATE TABLE IF NOT EXISTS conversation_turns (
1047
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1048
- session_id TEXT NOT NULL,
1049
- turn_number INTEGER NOT NULL,
1050
- user_prompt TEXT NOT NULL,
1051
- assistant_response TEXT,
1052
- tool_calls_json TEXT,
1053
- tool_call_count INTEGER DEFAULT 0,
1054
- model_used TEXT,
1055
- duration_ms INTEGER,
1056
- prompt_tokens INTEGER,
1057
- response_tokens INTEGER,
1058
- created_at TEXT DEFAULT (datetime('now')),
1059
- created_at_epoch INTEGER DEFAULT (unixepoch()),
1060
- FOREIGN KEY (session_id) REFERENCES sessions(session_id)
1061
- );
1062
-
1063
- CREATE INDEX IF NOT EXISTS idx_ct_session ON conversation_turns(session_id);
1064
- CREATE INDEX IF NOT EXISTS idx_ct_created ON conversation_turns(created_at DESC);
1065
- CREATE INDEX IF NOT EXISTS idx_ct_turn ON conversation_turns(session_id, turn_number);
1066
- `);
1067
- db.exec(`
1068
- CREATE TABLE IF NOT EXISTS tool_call_details (
1069
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1070
- session_id TEXT NOT NULL,
1071
- turn_number INTEGER NOT NULL,
1072
- tool_name TEXT NOT NULL,
1073
- tool_input_summary TEXT,
1074
- tool_input_size INTEGER,
1075
- tool_output_size INTEGER,
1076
- tool_success INTEGER DEFAULT 1,
1077
- duration_ms INTEGER,
1078
- files_involved TEXT,
1079
- created_at TEXT DEFAULT (datetime('now')),
1080
- created_at_epoch INTEGER DEFAULT (unixepoch()),
1081
- FOREIGN KEY (session_id) REFERENCES sessions(session_id)
1082
- );
1083
-
1084
- CREATE INDEX IF NOT EXISTS idx_tcd_session ON tool_call_details(session_id);
1085
- CREATE INDEX IF NOT EXISTS idx_tcd_tool ON tool_call_details(tool_name);
1086
- CREATE INDEX IF NOT EXISTS idx_tcd_created ON tool_call_details(created_at DESC);
1087
- `);
1088
- try {
1089
- db.exec(`
1090
- CREATE VIRTUAL TABLE IF NOT EXISTS conversation_turns_fts USING fts5(
1091
- user_prompt,
1092
- assistant_response,
1093
- content=conversation_turns,
1094
- content_rowid=id
1095
- );
1096
- `);
1097
- } catch (_e) {
1098
- }
1099
- db.exec(`
1100
- CREATE TRIGGER IF NOT EXISTS ct_fts_insert AFTER INSERT ON conversation_turns BEGIN
1101
- INSERT INTO conversation_turns_fts(rowid, user_prompt, assistant_response)
1102
- VALUES (new.id, new.user_prompt, new.assistant_response);
1103
- END;
1104
-
1105
- CREATE TRIGGER IF NOT EXISTS ct_fts_delete AFTER DELETE ON conversation_turns BEGIN
1106
- INSERT INTO conversation_turns_fts(conversation_turns_fts, rowid, user_prompt, assistant_response)
1107
- VALUES ('delete', old.id, old.user_prompt, old.assistant_response);
1108
- END;
1109
-
1110
- CREATE TRIGGER IF NOT EXISTS ct_fts_update AFTER UPDATE ON conversation_turns BEGIN
1111
- INSERT INTO conversation_turns_fts(conversation_turns_fts, rowid, user_prompt, assistant_response)
1112
- VALUES ('delete', old.id, old.user_prompt, old.assistant_response);
1113
- INSERT INTO conversation_turns_fts(rowid, user_prompt, assistant_response)
1114
- VALUES (new.id, new.user_prompt, new.assistant_response);
1115
- END;
1116
- `);
1117
- db.exec(`
1118
- CREATE TABLE IF NOT EXISTS session_quality_scores (
1119
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1120
- session_id TEXT NOT NULL UNIQUE,
1121
- project TEXT NOT NULL DEFAULT 'my-project',
1122
- score INTEGER NOT NULL DEFAULT 100,
1123
- security_score INTEGER NOT NULL DEFAULT 100,
1124
- architecture_score INTEGER NOT NULL DEFAULT 100,
1125
- coupling_score INTEGER NOT NULL DEFAULT 100,
1126
- test_score INTEGER NOT NULL DEFAULT 100,
1127
- rule_compliance_score INTEGER NOT NULL DEFAULT 100,
1128
- observations_total INTEGER NOT NULL DEFAULT 0,
1129
- bugs_found INTEGER NOT NULL DEFAULT 0,
1130
- bugs_fixed INTEGER NOT NULL DEFAULT 0,
1131
- vr_checks_passed INTEGER NOT NULL DEFAULT 0,
1132
- vr_checks_failed INTEGER NOT NULL DEFAULT 0,
1133
- incidents_triggered INTEGER NOT NULL DEFAULT 0,
1134
- created_at TEXT DEFAULT (datetime('now')),
1135
- FOREIGN KEY (session_id) REFERENCES sessions(session_id)
1136
- );
1137
- CREATE INDEX IF NOT EXISTS idx_sqs_session ON session_quality_scores(session_id);
1138
- CREATE INDEX IF NOT EXISTS idx_sqs_project ON session_quality_scores(project);
1139
- `);
1140
- db.exec(`
1141
- CREATE TABLE IF NOT EXISTS session_costs (
1142
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1143
- session_id TEXT NOT NULL UNIQUE,
1144
- project TEXT NOT NULL DEFAULT 'my-project',
1145
- input_tokens INTEGER NOT NULL DEFAULT 0,
1146
- output_tokens INTEGER NOT NULL DEFAULT 0,
1147
- cache_read_tokens INTEGER NOT NULL DEFAULT 0,
1148
- cache_write_tokens INTEGER NOT NULL DEFAULT 0,
1149
- total_tokens INTEGER NOT NULL DEFAULT 0,
1150
- estimated_cost_usd REAL NOT NULL DEFAULT 0.0,
1151
- model TEXT,
1152
- duration_minutes REAL NOT NULL DEFAULT 0.0,
1153
- tool_calls INTEGER NOT NULL DEFAULT 0,
1154
- created_at TEXT DEFAULT (datetime('now')),
1155
- FOREIGN KEY (session_id) REFERENCES sessions(session_id)
1156
- );
1157
- CREATE INDEX IF NOT EXISTS idx_sc_session ON session_costs(session_id);
1158
- `);
1159
- db.exec(`
1160
- CREATE TABLE IF NOT EXISTS feature_costs (
1161
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1162
- feature_key TEXT NOT NULL,
663
+ feature_key TEXT NOT NULL,
1163
664
  session_id TEXT NOT NULL,
1164
665
  tokens_used INTEGER NOT NULL DEFAULT 0,
1165
666
  estimated_cost_usd REAL NOT NULL DEFAULT 0.0,
@@ -1378,251 +879,1075 @@ function initMemorySchema(db) {
1378
879
  );
1379
880
  `);
1380
881
  }
1381
- function assignImportance(type, vrResult) {
1382
- switch (type) {
1383
- case "decision":
1384
- case "failed_attempt":
1385
- return 5;
1386
- case "cr_violation":
1387
- case "incident_near_miss":
1388
- return 4;
1389
- case "vr_check":
1390
- return vrResult === "PASS" ? 2 : 4;
1391
- case "pattern_compliance":
1392
- return vrResult === "PASS" ? 2 : 4;
1393
- case "feature":
1394
- case "bugfix":
1395
- return 3;
1396
- case "refactor":
1397
- return 2;
1398
- case "file_change":
1399
- case "discovery":
1400
- return 1;
1401
- default:
1402
- return 3;
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
+ }
905
+ function assignImportance(type, vrResult) {
906
+ switch (type) {
907
+ case "decision":
908
+ case "failed_attempt":
909
+ return 5;
910
+ case "cr_violation":
911
+ case "incident_near_miss":
912
+ return 4;
913
+ case "vr_check":
914
+ return vrResult === "PASS" ? 2 : 4;
915
+ case "pattern_compliance":
916
+ return vrResult === "PASS" ? 2 : 4;
917
+ case "feature":
918
+ case "bugfix":
919
+ return 3;
920
+ case "refactor":
921
+ return 2;
922
+ case "file_change":
923
+ case "discovery":
924
+ return 1;
925
+ default:
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;
1268
+ }
1269
+ return db.prepare(sql).all(...params);
1270
+ }
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";
1403
1361
  }
1404
1362
  }
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)
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
+ installCommands: () => installCommands,
1401
+ resolveCommandsDir: () => resolveCommandsDir,
1402
+ runInstallCommands: () => runInstallCommands
1403
+ });
1404
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync, mkdirSync as mkdirSync2, readdirSync as readdirSync2 } from "fs";
1405
+ import { resolve as resolve3, dirname as dirname3 } from "path";
1406
+ import { fileURLToPath } from "url";
1407
+ function resolveCommandsDir() {
1408
+ const cwd = process.cwd();
1409
+ const nodeModulesPath = resolve3(cwd, "node_modules/@massu/core/commands");
1410
+ if (existsSync4(nodeModulesPath)) {
1411
+ return nodeModulesPath;
1412
+ }
1413
+ const distRelPath = resolve3(__dirname, "../commands");
1414
+ if (existsSync4(distRelPath)) {
1415
+ return distRelPath;
1416
+ }
1417
+ const srcRelPath = resolve3(__dirname, "../../commands");
1418
+ if (existsSync4(srcRelPath)) {
1419
+ return srcRelPath;
1420
+ }
1421
+ return null;
1422
+ }
1423
+ function installCommands(projectRoot) {
1424
+ const claudeDirName = getConfig().conventions?.claudeDirName ?? ".claude";
1425
+ const targetDir = resolve3(projectRoot, claudeDirName, "commands");
1426
+ if (!existsSync4(targetDir)) {
1427
+ mkdirSync2(targetDir, { recursive: true });
1428
+ }
1429
+ const sourceDir = resolveCommandsDir();
1430
+ if (!sourceDir) {
1431
+ console.error(" ERROR: Could not find massu commands directory.");
1432
+ console.error(" Try reinstalling: npm install @massu/core");
1433
+ return { installed: 0, updated: 0, skipped: 0, commandsDir: targetDir };
1434
+ }
1435
+ const sourceFiles = readdirSync2(sourceDir).filter((f) => f.endsWith(".md"));
1436
+ let installed = 0;
1437
+ let updated = 0;
1438
+ let skipped = 0;
1439
+ for (const file of sourceFiles) {
1440
+ const sourcePath = resolve3(sourceDir, file);
1441
+ const targetPath = resolve3(targetDir, file);
1442
+ const sourceContent = readFileSync3(sourcePath, "utf-8");
1443
+ if (existsSync4(targetPath)) {
1444
+ const existingContent = readFileSync3(targetPath, "utf-8");
1445
+ if (existingContent === sourceContent) {
1446
+ skipped++;
1447
+ continue;
1448
+ }
1449
+ writeFileSync(targetPath, sourceContent, "utf-8");
1450
+ updated++;
1451
+ } else {
1452
+ writeFileSync(targetPath, sourceContent, "utf-8");
1453
+ installed++;
1454
+ }
1455
+ }
1456
+ return { installed, updated, skipped, commandsDir: targetDir };
1457
+ }
1458
+ async function runInstallCommands() {
1459
+ const projectRoot = process.cwd();
1460
+ console.log("");
1461
+ console.log("Massu AI - Install Slash Commands");
1462
+ console.log("==================================");
1463
+ console.log("");
1464
+ const result = installCommands(projectRoot);
1465
+ if (result.installed > 0) {
1466
+ console.log(` Installed ${result.installed} new commands`);
1467
+ }
1468
+ if (result.updated > 0) {
1469
+ console.log(` Updated ${result.updated} existing commands`);
1470
+ }
1471
+ if (result.skipped > 0) {
1472
+ console.log(` ${result.skipped} commands already up to date`);
1473
+ }
1474
+ const total = result.installed + result.updated + result.skipped;
1475
+ console.log("");
1476
+ console.log(` ${total} slash commands available in ${result.commandsDir}`);
1477
+ console.log("");
1478
+ console.log(" Restart your Claude Code session to use them.");
1479
+ console.log("");
1480
+ }
1481
+ var __filename, __dirname;
1482
+ var init_install_commands = __esm({
1483
+ "src/commands/install-commands.ts"() {
1484
+ "use strict";
1485
+ init_config();
1486
+ __filename = fileURLToPath(import.meta.url);
1487
+ __dirname = dirname3(__filename);
1488
+ }
1489
+ });
1490
+
1491
+ // src/commands/init.ts
1492
+ var init_exports = {};
1493
+ __export(init_exports, {
1494
+ buildHooksConfig: () => buildHooksConfig,
1495
+ detectFramework: () => detectFramework,
1496
+ detectPython: () => detectPython,
1497
+ generateConfig: () => generateConfig,
1498
+ initMemoryDir: () => initMemoryDir,
1499
+ installHooks: () => installHooks,
1500
+ registerMcpServer: () => registerMcpServer,
1501
+ resolveHooksDir: () => resolveHooksDir,
1502
+ runInit: () => runInit
1503
+ });
1504
+ import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync3 } from "fs";
1505
+ import { resolve as resolve4, basename as basename2, dirname as dirname4 } from "path";
1506
+ import { fileURLToPath as fileURLToPath2 } from "url";
1507
+ import { homedir as homedir2 } from "os";
1508
+ import { stringify as yamlStringify } from "yaml";
1509
+ function detectFramework(projectRoot) {
1510
+ const result = {
1511
+ type: "javascript",
1512
+ router: "none",
1513
+ orm: "none",
1514
+ ui: "none"
1515
+ };
1516
+ const pkgPath = resolve4(projectRoot, "package.json");
1517
+ if (!existsSync5(pkgPath)) return result;
1518
+ try {
1519
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
1520
+ const allDeps = {
1521
+ ...pkg.dependencies,
1522
+ ...pkg.devDependencies
1523
+ };
1524
+ if (allDeps["typescript"]) result.type = "typescript";
1525
+ if (allDeps["next"]) result.ui = "nextjs";
1526
+ else if (allDeps["@sveltejs/kit"]) result.ui = "sveltekit";
1527
+ else if (allDeps["nuxt"]) result.ui = "nuxt";
1528
+ else if (allDeps["@angular/core"]) result.ui = "angular";
1529
+ else if (allDeps["vue"]) result.ui = "vue";
1530
+ else if (allDeps["react"]) result.ui = "react";
1531
+ if (allDeps["@trpc/server"]) result.router = "trpc";
1532
+ else if (allDeps["graphql"] || allDeps["@apollo/server"]) result.router = "graphql";
1533
+ else if (allDeps["express"] || allDeps["fastify"] || allDeps["hono"]) result.router = "rest";
1534
+ if (allDeps["@prisma/client"] || allDeps["prisma"]) result.orm = "prisma";
1535
+ else if (allDeps["drizzle-orm"]) result.orm = "drizzle";
1536
+ else if (allDeps["typeorm"]) result.orm = "typeorm";
1537
+ else if (allDeps["sequelize"]) result.orm = "sequelize";
1538
+ else if (allDeps["mongoose"]) result.orm = "mongoose";
1539
+ } catch {
1540
+ }
1541
+ return result;
1542
+ }
1543
+ function detectPython(projectRoot) {
1544
+ const result = {
1545
+ detected: false,
1546
+ root: "",
1547
+ hasFastapi: false,
1548
+ hasSqlalchemy: false,
1549
+ hasAlembic: false,
1550
+ alembicDir: null
1551
+ };
1552
+ const markers = ["pyproject.toml", "setup.py", "requirements.txt", "Pipfile"];
1553
+ const hasMarker = markers.some((m) => existsSync5(resolve4(projectRoot, m)));
1554
+ if (!hasMarker) return result;
1555
+ result.detected = true;
1556
+ const depFiles = [
1557
+ { file: "pyproject.toml", parser: parsePyprojectDeps },
1558
+ { file: "requirements.txt", parser: parseRequirementsDeps },
1559
+ { file: "setup.py", parser: parseSetupPyDeps },
1560
+ { file: "Pipfile", parser: parsePipfileDeps }
1561
+ ];
1562
+ for (const { file, parser } of depFiles) {
1563
+ const filePath = resolve4(projectRoot, file);
1564
+ if (existsSync5(filePath)) {
1565
+ try {
1566
+ const content = readFileSync4(filePath, "utf-8");
1567
+ const deps = parser(content);
1568
+ if (deps.includes("fastapi")) result.hasFastapi = true;
1569
+ if (deps.includes("sqlalchemy")) result.hasSqlalchemy = true;
1570
+ } catch {
1571
+ }
1572
+ }
1573
+ }
1574
+ if (existsSync5(resolve4(projectRoot, "alembic.ini"))) {
1575
+ result.hasAlembic = true;
1576
+ if (existsSync5(resolve4(projectRoot, "alembic"))) {
1577
+ result.alembicDir = "alembic";
1578
+ }
1579
+ } else if (existsSync5(resolve4(projectRoot, "alembic"))) {
1580
+ result.hasAlembic = true;
1581
+ result.alembicDir = "alembic";
1441
1582
  }
1442
- if (opts?.crRule) {
1443
- sql += " AND o.cr_rule = ?";
1444
- params.push(opts.crRule);
1583
+ const candidateRoots = ["app", "src", "backend", "api"];
1584
+ for (const candidate of candidateRoots) {
1585
+ const candidatePath = resolve4(projectRoot, candidate);
1586
+ if (existsSync5(candidatePath) && existsSync5(resolve4(candidatePath, "__init__.py"))) {
1587
+ result.root = candidate;
1588
+ break;
1589
+ }
1590
+ if (existsSync5(candidatePath)) {
1591
+ try {
1592
+ const files = readdirSync3(candidatePath);
1593
+ if (files.some((f) => f.endsWith(".py"))) {
1594
+ result.root = candidate;
1595
+ break;
1596
+ }
1597
+ } catch {
1598
+ }
1599
+ }
1445
1600
  }
1446
- if (opts?.dateFrom) {
1447
- sql += " AND o.created_at >= ?";
1448
- params.push(opts.dateFrom);
1601
+ if (!result.root) {
1602
+ result.root = ".";
1449
1603
  }
1450
- sql += " ORDER BY rank LIMIT ?";
1451
- params.push(limit);
1452
- return db.prepare(sql).all(...params);
1604
+ return result;
1453
1605
  }
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);
1606
+ function parsePyprojectDeps(content) {
1607
+ const deps = [];
1608
+ const lower = content.toLowerCase();
1609
+ if (lower.includes("fastapi")) deps.push("fastapi");
1610
+ if (lower.includes("sqlalchemy")) deps.push("sqlalchemy");
1611
+ return deps;
1460
1612
  }
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);
1613
+ function parseRequirementsDeps(content) {
1614
+ const deps = [];
1615
+ const lower = content.toLowerCase();
1616
+ for (const line of lower.split("\n")) {
1617
+ const trimmed = line.trim();
1618
+ if (trimmed.startsWith("fastapi")) deps.push("fastapi");
1619
+ if (trimmed.startsWith("sqlalchemy")) deps.push("sqlalchemy");
1470
1620
  }
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);
1621
+ return deps;
1476
1622
  }
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;
1623
+ function parseSetupPyDeps(content) {
1624
+ const deps = [];
1625
+ const lower = content.toLowerCase();
1626
+ if (lower.includes("fastapi")) deps.push("fastapi");
1627
+ if (lower.includes("sqlalchemy")) deps.push("sqlalchemy");
1628
+ return deps;
1481
1629
  }
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 };
1630
+ function parsePipfileDeps(content) {
1631
+ const deps = [];
1632
+ const lower = content.toLowerCase();
1633
+ if (lower.includes("fastapi")) deps.push("fastapi");
1634
+ if (lower.includes("sqlalchemy")) deps.push("sqlalchemy");
1635
+ return deps;
1487
1636
  }
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);
1637
+ function generateConfig(projectRoot, framework) {
1638
+ const configPath = resolve4(projectRoot, "massu.config.yaml");
1639
+ if (existsSync5(configPath)) {
1640
+ return false;
1494
1641
  }
1495
- if (opts?.turnTo !== void 0) {
1496
- sql += " AND turn_number <= ?";
1497
- params.push(opts.turnTo);
1642
+ const projectName = basename2(projectRoot);
1643
+ const config = {
1644
+ project: {
1645
+ name: projectName,
1646
+ root: "auto"
1647
+ },
1648
+ framework: {
1649
+ type: framework.type,
1650
+ router: framework.router,
1651
+ orm: framework.orm,
1652
+ ui: framework.ui
1653
+ },
1654
+ paths: {
1655
+ source: "src",
1656
+ aliases: { "@": "src" }
1657
+ },
1658
+ toolPrefix: "massu",
1659
+ domains: [],
1660
+ rules: [
1661
+ {
1662
+ pattern: "src/**/*.ts",
1663
+ rules: ["Use ESM imports, not CommonJS"]
1664
+ }
1665
+ ]
1666
+ };
1667
+ const python = detectPython(projectRoot);
1668
+ if (python.detected) {
1669
+ const pythonConfig = {
1670
+ root: python.root,
1671
+ exclude_dirs: ["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"]
1672
+ };
1673
+ if (python.hasFastapi) pythonConfig.framework = "fastapi";
1674
+ if (python.hasSqlalchemy) pythonConfig.orm = "sqlalchemy";
1675
+ if (python.hasAlembic && python.alembicDir) {
1676
+ pythonConfig.alembic_dir = python.alembicDir;
1677
+ }
1678
+ config.python = pythonConfig;
1498
1679
  }
1499
- sql += " ORDER BY turn_number ASC";
1500
- return db.prepare(sql).all(...params);
1680
+ const yamlContent = `# Massu AI Configuration
1681
+ # Generated by: npx massu init
1682
+ # Documentation: https://massu.ai/docs/getting-started/configuration
1683
+
1684
+ ${yamlStringify(config)}`;
1685
+ writeFileSync2(configPath, yamlContent, "utf-8");
1686
+ return true;
1501
1687
  }
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);
1688
+ function registerMcpServer(projectRoot) {
1689
+ const mcpPath = resolve4(projectRoot, ".mcp.json");
1690
+ let existing = {};
1691
+ if (existsSync5(mcpPath)) {
1692
+ try {
1693
+ existing = JSON.parse(readFileSync4(mcpPath, "utf-8"));
1694
+ } catch {
1695
+ existing = {};
1696
+ }
1514
1697
  }
1515
- if (opts?.dateFrom) {
1516
- sql += " AND ct.created_at >= ?";
1517
- params.push(opts.dateFrom);
1698
+ const servers = existing.mcpServers ?? {};
1699
+ if (servers.massu) {
1700
+ return false;
1701
+ }
1702
+ servers.massu = {
1703
+ type: "stdio",
1704
+ command: "npx",
1705
+ args: ["-y", "@massu/core"]
1706
+ };
1707
+ existing.mcpServers = servers;
1708
+ writeFileSync2(mcpPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
1709
+ return true;
1710
+ }
1711
+ function resolveHooksDir() {
1712
+ const cwd = process.cwd();
1713
+ const nodeModulesPath = resolve4(cwd, "node_modules/@massu/core/dist/hooks");
1714
+ if (existsSync5(nodeModulesPath)) {
1715
+ return "node_modules/@massu/core/dist/hooks";
1716
+ }
1717
+ const localPath = resolve4(__dirname2, "../dist/hooks");
1718
+ if (existsSync5(localPath)) {
1719
+ return localPath;
1720
+ }
1721
+ return "node_modules/@massu/core/dist/hooks";
1722
+ }
1723
+ function hookCmd(hooksDir, hookFile) {
1724
+ return `node ${hooksDir}/${hookFile}`;
1725
+ }
1726
+ function buildHooksConfig(hooksDir) {
1727
+ return {
1728
+ SessionStart: [
1729
+ {
1730
+ hooks: [
1731
+ { type: "command", command: hookCmd(hooksDir, "session-start.js"), timeout: 10 }
1732
+ ]
1733
+ }
1734
+ ],
1735
+ PreToolUse: [
1736
+ {
1737
+ matcher: "Bash",
1738
+ hooks: [
1739
+ { type: "command", command: hookCmd(hooksDir, "security-gate.js"), timeout: 5 }
1740
+ ]
1741
+ },
1742
+ {
1743
+ matcher: "Bash|Write",
1744
+ hooks: [
1745
+ { type: "command", command: hookCmd(hooksDir, "pre-delete-check.js"), timeout: 5 }
1746
+ ]
1747
+ }
1748
+ ],
1749
+ PostToolUse: [
1750
+ {
1751
+ hooks: [
1752
+ { type: "command", command: hookCmd(hooksDir, "post-tool-use.js"), timeout: 10 },
1753
+ { type: "command", command: hookCmd(hooksDir, "quality-event.js"), timeout: 5 },
1754
+ { type: "command", command: hookCmd(hooksDir, "cost-tracker.js"), timeout: 5 }
1755
+ ]
1756
+ },
1757
+ {
1758
+ matcher: "Edit|Write",
1759
+ hooks: [
1760
+ { type: "command", command: hookCmd(hooksDir, "post-edit-context.js"), timeout: 5 }
1761
+ ]
1762
+ }
1763
+ ],
1764
+ Stop: [
1765
+ {
1766
+ hooks: [
1767
+ { type: "command", command: hookCmd(hooksDir, "session-end.js"), timeout: 15 }
1768
+ ]
1769
+ }
1770
+ ],
1771
+ PreCompact: [
1772
+ {
1773
+ hooks: [
1774
+ { type: "command", command: hookCmd(hooksDir, "pre-compact.js"), timeout: 10 }
1775
+ ]
1776
+ }
1777
+ ],
1778
+ UserPromptSubmit: [
1779
+ {
1780
+ hooks: [
1781
+ { type: "command", command: hookCmd(hooksDir, "user-prompt.js"), timeout: 5 },
1782
+ { type: "command", command: hookCmd(hooksDir, "intent-suggester.js"), timeout: 5 }
1783
+ ]
1784
+ }
1785
+ ]
1786
+ };
1787
+ }
1788
+ function installHooks(projectRoot) {
1789
+ const claudeDirName = getConfig().conventions?.claudeDirName ?? ".claude";
1790
+ const claudeDir = resolve4(projectRoot, claudeDirName);
1791
+ const settingsPath = resolve4(claudeDir, "settings.local.json");
1792
+ if (!existsSync5(claudeDir)) {
1793
+ mkdirSync3(claudeDir, { recursive: true });
1794
+ }
1795
+ let settings = {};
1796
+ if (existsSync5(settingsPath)) {
1797
+ try {
1798
+ settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
1799
+ } catch {
1800
+ settings = {};
1801
+ }
1802
+ }
1803
+ const hooksDir = resolveHooksDir();
1804
+ const hooksConfig = buildHooksConfig(hooksDir);
1805
+ let hookCount = 0;
1806
+ for (const groups of Object.values(hooksConfig)) {
1807
+ for (const group of groups) {
1808
+ hookCount += group.hooks.length;
1809
+ }
1518
1810
  }
1519
- if (opts?.dateTo) {
1520
- sql += " AND ct.created_at <= ?";
1521
- params.push(opts.dateTo);
1811
+ settings.hooks = hooksConfig;
1812
+ writeFileSync2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
1813
+ return { installed: true, count: hookCount };
1814
+ }
1815
+ function initMemoryDir(projectRoot) {
1816
+ const encodedRoot = "-" + projectRoot.replace(/\//g, "-");
1817
+ const memoryDir = resolve4(homedir2(), `.claude/projects/${encodedRoot}/memory`);
1818
+ let created = false;
1819
+ if (!existsSync5(memoryDir)) {
1820
+ mkdirSync3(memoryDir, { recursive: true });
1821
+ created = true;
1522
1822
  }
1523
- if (opts?.minToolCalls !== void 0) {
1524
- sql += " AND ct.tool_call_count >= ?";
1525
- params.push(opts.minToolCalls);
1823
+ const memoryMdPath = resolve4(memoryDir, "MEMORY.md");
1824
+ let memoryMdCreated = false;
1825
+ if (!existsSync5(memoryMdPath)) {
1826
+ const projectName = basename2(projectRoot);
1827
+ const memoryContent = `# ${projectName} - Massu Memory
1828
+
1829
+ ## Key Learnings
1830
+ <!-- Important patterns and conventions discovered during development -->
1831
+
1832
+ ## Common Gotchas
1833
+ <!-- Non-obvious issues and how to avoid them -->
1834
+
1835
+ ## Corrections
1836
+ <!-- Wrong behaviors that were corrected and how to prevent them -->
1837
+
1838
+ ## File Index
1839
+ <!-- Significant files and directories -->
1840
+ `;
1841
+ writeFileSync2(memoryMdPath, memoryContent, "utf-8");
1842
+ memoryMdCreated = true;
1526
1843
  }
1527
- sql += " ORDER BY rank LIMIT ?";
1528
- params.push(limit);
1529
- return db.prepare(sql).all(...params);
1844
+ return { created, memoryMdCreated };
1530
1845
  }
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);
1846
+ async function runInit() {
1847
+ const projectRoot = process.cwd();
1848
+ console.log("");
1849
+ console.log("Massu AI - Project Setup");
1850
+ console.log("========================");
1851
+ console.log("");
1852
+ const framework = detectFramework(projectRoot);
1853
+ const frameworkParts = [];
1854
+ if (framework.type !== "javascript") frameworkParts.push(capitalize(framework.type));
1855
+ if (framework.ui !== "none") frameworkParts.push(formatName(framework.ui));
1856
+ if (framework.orm !== "none") frameworkParts.push(capitalize(framework.orm));
1857
+ if (framework.router !== "none") frameworkParts.push(framework.router.toUpperCase());
1858
+ const detected = frameworkParts.length > 0 ? frameworkParts.join(", ") : "JavaScript";
1859
+ console.log(` Detected: ${detected}`);
1860
+ const python = detectPython(projectRoot);
1861
+ if (python.detected) {
1862
+ const pyParts = ["Python"];
1863
+ if (python.hasFastapi) pyParts.push("FastAPI");
1864
+ if (python.hasSqlalchemy) pyParts.push("SQLAlchemy");
1865
+ if (python.hasAlembic) pyParts.push("Alembic");
1866
+ console.log(` Detected: ${pyParts.join(", ")} (root: ${python.root})`);
1539
1867
  }
1540
- if (opts?.toolName) {
1541
- conditions.push("tool_name = ?");
1542
- params.push(opts.toolName);
1868
+ const configCreated = generateConfig(projectRoot, framework);
1869
+ if (configCreated) {
1870
+ console.log(" Created massu.config.yaml");
1871
+ } else {
1872
+ console.log(" massu.config.yaml already exists (preserved)");
1543
1873
  }
1544
- if (opts?.dateFrom) {
1545
- conditions.push("created_at >= ?");
1546
- params.push(opts.dateFrom);
1874
+ const mcpRegistered = registerMcpServer(projectRoot);
1875
+ if (mcpRegistered) {
1876
+ console.log(" Registered MCP server in .mcp.json");
1877
+ } else {
1878
+ console.log(" MCP server already registered in .mcp.json");
1547
1879
  }
1548
- if (conditions.length > 0) {
1549
- whereClause = "WHERE " + conditions.join(" AND ");
1880
+ const { count: hooksCount } = installHooks(projectRoot);
1881
+ console.log(` Installed ${hooksCount} hooks in .claude/settings.local.json`);
1882
+ const cmdResult = installCommands(projectRoot);
1883
+ const cmdTotal = cmdResult.installed + cmdResult.updated + cmdResult.skipped;
1884
+ if (cmdResult.installed > 0 || cmdResult.updated > 0) {
1885
+ console.log(` Installed ${cmdTotal} slash commands (${cmdResult.installed} new, ${cmdResult.updated} updated)`);
1886
+ } else {
1887
+ console.log(` ${cmdTotal} slash commands already up to date`);
1550
1888
  }
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;
1889
+ const { created: memDirCreated, memoryMdCreated } = initMemoryDir(projectRoot);
1890
+ if (memDirCreated) {
1891
+ console.log(" Created memory directory (~/.claude/projects/.../memory/)");
1892
+ } else {
1893
+ console.log(" Memory directory already exists");
1576
1894
  }
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
- }];
1895
+ if (memoryMdCreated) {
1896
+ console.log(" Created initial MEMORY.md");
1592
1897
  }
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);
1898
+ try {
1899
+ const claudeDirName = ".claude";
1900
+ const encodedRoot = projectRoot.replace(/\//g, "-");
1901
+ const computedMemoryDir = resolve4(homedir2(), claudeDirName, "projects", encodedRoot, "memory");
1902
+ const memFiles = existsSync5(computedMemoryDir) ? readdirSync3(computedMemoryDir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md") : [];
1903
+ if (memFiles.length > 0) {
1904
+ const { getMemoryDb: getMemoryDb2 } = await Promise.resolve().then(() => (init_memory_db(), memory_db_exports));
1905
+ const db = getMemoryDb2();
1906
+ try {
1907
+ const stats = backfillMemoryFiles(db, computedMemoryDir, `init-${Date.now()}`);
1908
+ if (stats.inserted > 0 || stats.updated > 0) {
1909
+ console.log(` Backfilled ${stats.inserted + stats.updated} memory files into database (${stats.inserted} new, ${stats.updated} updated)`);
1910
+ }
1911
+ } finally {
1912
+ db.close();
1913
+ }
1914
+ }
1915
+ } catch (_backfillErr) {
1916
+ }
1917
+ console.log(" Databases will auto-create on first session");
1918
+ console.log("");
1919
+ console.log("Massu AI is ready. Start a Claude Code session to begin.");
1920
+ console.log("");
1921
+ console.log("Next steps:");
1922
+ console.log(" claude # Start a session (hooks activate automatically)");
1923
+ console.log(" npx massu doctor # Verify installation health");
1924
+ console.log("");
1925
+ console.log("Documentation: https://massu.ai/docs");
1926
+ console.log("");
1606
1927
  }
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
1928
+ function capitalize(str) {
1929
+ return str.charAt(0).toUpperCase() + str.slice(1);
1930
+ }
1931
+ function formatName(name) {
1932
+ const names = {
1933
+ nextjs: "Next.js",
1934
+ sveltekit: "SvelteKit",
1935
+ nuxt: "Nuxt",
1936
+ angular: "Angular",
1937
+ vue: "Vue",
1938
+ react: "React"
1620
1939
  };
1940
+ return names[name] ?? capitalize(name);
1621
1941
  }
1622
- var init_memory_db = __esm({
1623
- "src/memory-db.ts"() {
1942
+ var __filename2, __dirname2;
1943
+ var init_init = __esm({
1944
+ "src/commands/init.ts"() {
1624
1945
  "use strict";
1946
+ init_memory_file_ingest();
1625
1947
  init_config();
1948
+ init_install_commands();
1949
+ __filename2 = fileURLToPath2(import.meta.url);
1950
+ __dirname2 = dirname4(__filename2);
1626
1951
  }
1627
1952
  });
1628
1953
 
@@ -1921,18 +2246,18 @@ __export(doctor_exports, {
1921
2246
  runDoctor: () => runDoctor,
1922
2247
  runValidateConfig: () => runValidateConfig
1923
2248
  });
1924
- import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
2249
+ import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync4 } from "fs";
1925
2250
  import { resolve as resolve5, dirname as dirname5 } from "path";
1926
2251
  import { fileURLToPath as fileURLToPath3 } from "url";
1927
- import { parse as parseYaml2 } from "yaml";
2252
+ import { parse as parseYaml3 } from "yaml";
1928
2253
  function checkConfig(projectRoot) {
1929
2254
  const configPath = resolve5(projectRoot, "massu.config.yaml");
1930
- if (!existsSync5(configPath)) {
2255
+ if (!existsSync6(configPath)) {
1931
2256
  return { name: "Configuration", status: "fail", detail: "massu.config.yaml not found. Run: npx massu init" };
1932
2257
  }
1933
2258
  try {
1934
- const content = readFileSync4(configPath, "utf-8");
1935
- const parsed = parseYaml2(content);
2259
+ const content = readFileSync5(configPath, "utf-8");
2260
+ const parsed = parseYaml3(content);
1936
2261
  if (!parsed || typeof parsed !== "object") {
1937
2262
  return { name: "Configuration", status: "fail", detail: "massu.config.yaml is empty or invalid YAML" };
1938
2263
  }
@@ -1943,11 +2268,11 @@ function checkConfig(projectRoot) {
1943
2268
  }
1944
2269
  function checkMcpServer(projectRoot) {
1945
2270
  const mcpPath = getResolvedPaths().mcpJsonPath;
1946
- if (!existsSync5(mcpPath)) {
2271
+ if (!existsSync6(mcpPath)) {
1947
2272
  return { name: "MCP Server", status: "fail", detail: ".mcp.json not found. Run: npx massu init" };
1948
2273
  }
1949
2274
  try {
1950
- const content = JSON.parse(readFileSync4(mcpPath, "utf-8"));
2275
+ const content = JSON.parse(readFileSync5(mcpPath, "utf-8"));
1951
2276
  const servers = content.mcpServers ?? {};
1952
2277
  if (!servers.massu) {
1953
2278
  return { name: "MCP Server", status: "fail", detail: "massu not registered in .mcp.json. Run: npx massu init" };
@@ -1959,11 +2284,11 @@ function checkMcpServer(projectRoot) {
1959
2284
  }
1960
2285
  function checkHooksConfig(projectRoot) {
1961
2286
  const settingsPath = getResolvedPaths().settingsLocalPath;
1962
- if (!existsSync5(settingsPath)) {
2287
+ if (!existsSync6(settingsPath)) {
1963
2288
  return { name: "Hooks Config", status: "fail", detail: ".claude/settings.local.json not found. Run: npx massu init" };
1964
2289
  }
1965
2290
  try {
1966
- const content = JSON.parse(readFileSync4(settingsPath, "utf-8"));
2291
+ const content = JSON.parse(readFileSync5(settingsPath, "utf-8"));
1967
2292
  if (!content.hooks) {
1968
2293
  return { name: "Hooks Config", status: "fail", detail: "No hooks configured. Run: npx massu install-hooks" };
1969
2294
  }
@@ -1989,9 +2314,9 @@ function checkHooksConfig(projectRoot) {
1989
2314
  function checkHookFiles(projectRoot) {
1990
2315
  const nodeModulesHooksDir = resolve5(projectRoot, "node_modules/@massu/core/dist/hooks");
1991
2316
  let hooksDir = nodeModulesHooksDir;
1992
- if (!existsSync5(nodeModulesHooksDir)) {
2317
+ if (!existsSync6(nodeModulesHooksDir)) {
1993
2318
  const devHooksDir = resolve5(__dirname3, "../../dist/hooks");
1994
- if (existsSync5(devHooksDir)) {
2319
+ if (existsSync6(devHooksDir)) {
1995
2320
  hooksDir = devHooksDir;
1996
2321
  } else {
1997
2322
  return { name: "Hook Files", status: "fail", detail: "Compiled hooks not found. Run: npm install @massu/core" };
@@ -1999,7 +2324,7 @@ function checkHookFiles(projectRoot) {
1999
2324
  }
2000
2325
  const missing = [];
2001
2326
  for (const hookFile of EXPECTED_HOOKS) {
2002
- if (!existsSync5(resolve5(hooksDir, hookFile))) {
2327
+ if (!existsSync6(resolve5(hooksDir, hookFile))) {
2003
2328
  missing.push(hookFile);
2004
2329
  }
2005
2330
  }
@@ -2026,7 +2351,7 @@ function checkNodeVersion() {
2026
2351
  }
2027
2352
  async function checkGitRepo(projectRoot) {
2028
2353
  const gitDir = resolve5(projectRoot, ".git");
2029
- if (!existsSync5(gitDir)) {
2354
+ if (!existsSync6(gitDir)) {
2030
2355
  return { name: "Git Repository", status: "warn", detail: "Not a git repository (optional but recommended)" };
2031
2356
  }
2032
2357
  try {
@@ -2044,7 +2369,7 @@ async function checkGitRepo(projectRoot) {
2044
2369
  }
2045
2370
  function checkKnowledgeDb(projectRoot) {
2046
2371
  const knowledgeDbPath = getResolvedPaths().memoryDbPath;
2047
- if (!existsSync5(knowledgeDbPath)) {
2372
+ if (!existsSync6(knowledgeDbPath)) {
2048
2373
  return {
2049
2374
  name: "Knowledge DB",
2050
2375
  status: "warn",
@@ -2055,7 +2380,7 @@ function checkKnowledgeDb(projectRoot) {
2055
2380
  }
2056
2381
  function checkMemoryDir(_projectRoot2) {
2057
2382
  const memoryDir = getResolvedPaths().memoryDir;
2058
- if (!existsSync5(memoryDir)) {
2383
+ if (!existsSync6(memoryDir)) {
2059
2384
  return {
2060
2385
  name: "Memory Directory",
2061
2386
  status: "warn",
@@ -2066,7 +2391,7 @@ function checkMemoryDir(_projectRoot2) {
2066
2391
  }
2067
2392
  function checkShellHooksWired(_projectRoot2) {
2068
2393
  const settingsPath = getResolvedPaths().settingsLocalPath;
2069
- if (!existsSync5(settingsPath)) {
2394
+ if (!existsSync6(settingsPath)) {
2070
2395
  return {
2071
2396
  name: "Shell Hooks",
2072
2397
  status: "fail",
@@ -2074,7 +2399,7 @@ function checkShellHooksWired(_projectRoot2) {
2074
2399
  };
2075
2400
  }
2076
2401
  try {
2077
- const content = JSON.parse(readFileSync4(settingsPath, "utf-8"));
2402
+ const content = JSON.parse(readFileSync5(settingsPath, "utf-8"));
2078
2403
  const hooks = content.hooks ?? {};
2079
2404
  const hasSessionStart = Array.isArray(hooks.SessionStart) && hooks.SessionStart.length > 0;
2080
2405
  const hasPreToolUse = Array.isArray(hooks.PreToolUse) && hooks.PreToolUse.length > 0;
@@ -2126,7 +2451,7 @@ function checkPythonHealth(projectRoot) {
2126
2451
  const config = getConfig();
2127
2452
  if (!config.python?.root) return null;
2128
2453
  const pythonRoot = resolve5(projectRoot, config.python.root);
2129
- if (!existsSync5(pythonRoot)) {
2454
+ if (!existsSync6(pythonRoot)) {
2130
2455
  return {
2131
2456
  name: "Python",
2132
2457
  status: "fail",
@@ -2140,15 +2465,15 @@ function checkPythonHealth(projectRoot) {
2140
2465
  function scanDir(dir, depth) {
2141
2466
  if (depth > 5) return;
2142
2467
  try {
2143
- const entries = readdirSync3(dir, { withFileTypes: true });
2468
+ const entries = readdirSync4(dir, { withFileTypes: true });
2144
2469
  for (const entry of entries) {
2145
2470
  if (entry.isDirectory()) {
2146
2471
  const excludeDirs = config.python?.exclude_dirs || ["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"];
2147
2472
  if (!excludeDirs.includes(entry.name)) {
2148
2473
  const subdir = resolve5(dir, entry.name);
2149
- if (depth <= 2 && !existsSync5(resolve5(subdir, "__init__.py"))) {
2474
+ if (depth <= 2 && !existsSync6(resolve5(subdir, "__init__.py"))) {
2150
2475
  try {
2151
- const subEntries = readdirSync3(subdir);
2476
+ const subEntries = readdirSync4(subdir);
2152
2477
  if (subEntries.some((f) => f.endsWith(".py") && f !== "__init__.py")) {
2153
2478
  initPyMissing.push(entry.name);
2154
2479
  }
@@ -2242,15 +2567,15 @@ async function runDoctor() {
2242
2567
  async function runValidateConfig() {
2243
2568
  const projectRoot = process.cwd();
2244
2569
  const configPath = resolve5(projectRoot, "massu.config.yaml");
2245
- if (!existsSync5(configPath)) {
2570
+ if (!existsSync6(configPath)) {
2246
2571
  console.error("Error: massu.config.yaml not found in current directory");
2247
2572
  console.error("Run: npx massu init");
2248
2573
  process.exit(1);
2249
2574
  return;
2250
2575
  }
2251
2576
  try {
2252
- const content = readFileSync4(configPath, "utf-8");
2253
- const parsed = parseYaml2(content);
2577
+ const content = readFileSync5(configPath, "utf-8");
2578
+ const parsed = parseYaml3(content);
2254
2579
  if (!parsed || typeof parsed !== "object") {
2255
2580
  console.error("Error: massu.config.yaml is empty or not a valid YAML object");
2256
2581
  process.exit(1);
@@ -2331,11 +2656,11 @@ var init_install_hooks = __esm({
2331
2656
 
2332
2657
  // src/db.ts
2333
2658
  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";
2659
+ import { dirname as dirname6, join as join2 } from "path";
2660
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readdirSync as readdirSync5, statSync } from "fs";
2336
2661
  function getCodeGraphDb() {
2337
2662
  const dbPath = getResolvedPaths().codegraphDbPath;
2338
- if (!existsSync6(dbPath)) {
2663
+ if (!existsSync7(dbPath)) {
2339
2664
  throw new Error(`CodeGraph database not found at ${dbPath}. Run 'npx @colbymchenry/codegraph sync' first.`);
2340
2665
  }
2341
2666
  const db = new Database2(dbPath, { readonly: true });
@@ -2345,7 +2670,7 @@ function getCodeGraphDb() {
2345
2670
  function getDataDb() {
2346
2671
  const dbPath = getResolvedPaths().dataDbPath;
2347
2672
  const dir = dirname6(dbPath);
2348
- if (!existsSync6(dir)) {
2673
+ if (!existsSync7(dir)) {
2349
2674
  mkdirSync4(dir, { recursive: true });
2350
2675
  }
2351
2676
  const db = new Database2(dbPath);
@@ -2616,9 +2941,9 @@ function isPythonDataStale(dataDb2, pythonRoot) {
2616
2941
  const lastBuildTime = new Date(lastBuild.value).getTime();
2617
2942
  function checkDir(dir) {
2618
2943
  try {
2619
- const entries = readdirSync4(dir, { withFileTypes: true });
2944
+ const entries = readdirSync5(dir, { withFileTypes: true });
2620
2945
  for (const entry of entries) {
2621
- const fullPath = join(dir, entry.name);
2946
+ const fullPath = join2(dir, entry.name);
2622
2947
  if (entry.isDirectory()) {
2623
2948
  if (["__pycache__", ".venv", "venv", "node_modules", ".mypy_cache", ".pytest_cache"].includes(entry.name)) continue;
2624
2949
  if (checkDir(fullPath)) return true;
@@ -2719,8 +3044,8 @@ var init_rules = __esm({
2719
3044
  });
2720
3045
 
2721
3046
  // 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";
3047
+ import { readFileSync as readFileSync6, existsSync as existsSync8, statSync as statSync2 } from "fs";
3048
+ import { resolve as resolve7, dirname as dirname7, join as join3 } from "path";
2724
3049
  function parseImports(source) {
2725
3050
  const imports = [];
2726
3051
  const lines = source.split("\n");
@@ -2780,19 +3105,19 @@ function resolveImportPath(specifier, fromFile) {
2780
3105
  } else {
2781
3106
  basePath = resolve7(dirname7(fromFile), specifier);
2782
3107
  }
2783
- if (existsSync7(basePath) && !isDirectory(basePath)) {
3108
+ if (existsSync8(basePath) && !isDirectory(basePath)) {
2784
3109
  return toRelative(basePath);
2785
3110
  }
2786
3111
  const resolvedPaths = getResolvedPaths();
2787
3112
  for (const ext of resolvedPaths.extensions) {
2788
3113
  const withExt = basePath + ext;
2789
- if (existsSync7(withExt)) {
3114
+ if (existsSync8(withExt)) {
2790
3115
  return toRelative(withExt);
2791
3116
  }
2792
3117
  }
2793
3118
  for (const indexFile of resolvedPaths.indexFiles) {
2794
- const indexPath = join2(basePath, indexFile);
2795
- if (existsSync7(indexPath)) {
3119
+ const indexPath = join3(basePath, indexFile);
3120
+ if (existsSync8(indexPath)) {
2796
3121
  return toRelative(indexPath);
2797
3122
  }
2798
3123
  }
@@ -2829,10 +3154,10 @@ function buildImportIndex(dataDb2, codegraphDb2) {
2829
3154
  let batch = [];
2830
3155
  for (const file of files) {
2831
3156
  const absPath = ensureWithinRoot(resolve7(projectRoot, file.path), projectRoot);
2832
- if (!existsSync7(absPath)) continue;
3157
+ if (!existsSync8(absPath)) continue;
2833
3158
  let source;
2834
3159
  try {
2835
- source = readFileSync5(absPath, "utf-8");
3160
+ source = readFileSync6(absPath, "utf-8");
2836
3161
  } catch {
2837
3162
  continue;
2838
3163
  }
@@ -2868,15 +3193,15 @@ var init_import_resolver = __esm({
2868
3193
  });
2869
3194
 
2870
3195
  // 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";
3196
+ import { readFileSync as readFileSync7, existsSync as existsSync9, readdirSync as readdirSync6 } from "fs";
3197
+ import { resolve as resolve8, join as join4 } from "path";
2873
3198
  function parseRootRouter() {
2874
3199
  const paths = getResolvedPaths();
2875
3200
  const rootPath = paths.rootRouterPath;
2876
- if (!existsSync8(rootPath)) {
3201
+ if (!existsSync9(rootPath)) {
2877
3202
  throw new Error(`Root router not found at ${rootPath}`);
2878
3203
  }
2879
- const source = readFileSync6(rootPath, "utf-8");
3204
+ const source = readFileSync7(rootPath, "utf-8");
2880
3205
  const mappings = [];
2881
3206
  const importMap = /* @__PURE__ */ new Map();
2882
3207
  const importRegex = /import\s+\{[^}]*?(\w+Router)[^}]*\}\s+from\s+['"]\.\/routers\/([^'"]+)['"]/g;
@@ -2888,12 +3213,12 @@ function parseRootRouter() {
2888
3213
  for (const ext of [".ts", ".tsx", ""]) {
2889
3214
  const candidate = fullPath + ext;
2890
3215
  const routersRelPath = getConfig().paths.routers ?? "src/server/api/routers";
2891
- if (existsSync8(candidate)) {
3216
+ if (existsSync9(candidate)) {
2892
3217
  filePath = routersRelPath + "/" + filePath + ext;
2893
3218
  break;
2894
3219
  }
2895
- const indexCandidate = join3(fullPath, "index.ts");
2896
- if (existsSync8(indexCandidate)) {
3220
+ const indexCandidate = join4(fullPath, "index.ts");
3221
+ if (existsSync9(indexCandidate)) {
2897
3222
  filePath = routersRelPath + "/" + filePath + "/index.ts";
2898
3223
  break;
2899
3224
  }
@@ -2913,8 +3238,8 @@ function parseRootRouter() {
2913
3238
  }
2914
3239
  function extractProcedures(routerFilePath) {
2915
3240
  const absPath = resolve8(getProjectRoot(), routerFilePath);
2916
- if (!existsSync8(absPath)) return [];
2917
- const source = readFileSync6(absPath, "utf-8");
3241
+ if (!existsSync9(absPath)) return [];
3242
+ const source = readFileSync7(absPath, "utf-8");
2918
3243
  const procedures = [];
2919
3244
  const seen = /* @__PURE__ */ new Set();
2920
3245
  const procRegex = /(\w+)\s*:\s*(protected|public)Procedure/g;
@@ -2943,21 +3268,21 @@ function findUICallSites(routerKey, procedureName) {
2943
3268
  ];
2944
3269
  const searchPattern = `api.${routerKey}.${procedureName}`;
2945
3270
  for (const dir of searchDirs) {
2946
- if (!existsSync8(dir)) continue;
3271
+ if (!existsSync9(dir)) continue;
2947
3272
  searchDirectory(dir, searchPattern, callSites);
2948
3273
  }
2949
3274
  return callSites;
2950
3275
  }
2951
3276
  function searchDirectory(dir, pattern, results) {
2952
- const entries = readdirSync5(dir, { withFileTypes: true });
3277
+ const entries = readdirSync6(dir, { withFileTypes: true });
2953
3278
  for (const entry of entries) {
2954
- const fullPath = join3(dir, entry.name);
3279
+ const fullPath = join4(dir, entry.name);
2955
3280
  if (entry.isDirectory()) {
2956
3281
  if (entry.name === "node_modules" || entry.name === ".next") continue;
2957
3282
  searchDirectory(fullPath, pattern, results);
2958
3283
  } else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
2959
3284
  try {
2960
- const source = readFileSync6(fullPath, "utf-8");
3285
+ const source = readFileSync7(fullPath, "utf-8");
2961
3286
  const lines = source.split("\n");
2962
3287
  for (let i = 0; i < lines.length; i++) {
2963
3288
  if (lines[i].includes(pattern)) {
@@ -3020,7 +3345,7 @@ var init_trpc_index = __esm({
3020
3345
  });
3021
3346
 
3022
3347
  // src/page-deps.ts
3023
- import { readFileSync as readFileSync7, existsSync as existsSync9 } from "fs";
3348
+ import { readFileSync as readFileSync8, existsSync as existsSync10 } from "fs";
3024
3349
  import { resolve as resolve9 } from "path";
3025
3350
  function deriveRoute(pageFile) {
3026
3351
  let route = pageFile.replace(/^src\/app/, "").replace(/\/page\.tsx?$/, "").replace(/\/page\.jsx?$/, "");
@@ -3060,9 +3385,9 @@ function findRouterCalls(files) {
3060
3385
  const projectRoot = getProjectRoot();
3061
3386
  for (const file of files) {
3062
3387
  const absPath = ensureWithinRoot(resolve9(projectRoot, file), projectRoot);
3063
- if (!existsSync9(absPath)) continue;
3388
+ if (!existsSync10(absPath)) continue;
3064
3389
  try {
3065
- const source = readFileSync7(absPath, "utf-8");
3390
+ const source = readFileSync8(absPath, "utf-8");
3066
3391
  const apiCallRegex = /api\.(\w+)\.\w+/g;
3067
3392
  let match;
3068
3393
  while ((match = apiCallRegex.exec(source)) !== null) {
@@ -3081,9 +3406,9 @@ function findTablesFromRouters(routerNames, dataDb2) {
3081
3406
  ).all(routerName);
3082
3407
  for (const proc of procs) {
3083
3408
  const absPath = ensureWithinRoot(resolve9(getProjectRoot(), proc.router_file), getProjectRoot());
3084
- if (!existsSync9(absPath)) continue;
3409
+ if (!existsSync10(absPath)) continue;
3085
3410
  try {
3086
- const source = readFileSync7(absPath, "utf-8");
3411
+ const source = readFileSync8(absPath, "utf-8");
3087
3412
  const dbPattern = getConfig().dbAccessPattern ?? "ctx.db.{table}";
3088
3413
  const regexStr = dbPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace("\\{table\\}", "(\\w+)");
3089
3414
  const tableRegex = new RegExp(regexStr + "\\.", "g");
@@ -3352,14 +3677,14 @@ var init_domains = __esm({
3352
3677
  });
3353
3678
 
3354
3679
  // src/schema-mapper.ts
3355
- import { readFileSync as readFileSync8, existsSync as existsSync10, readdirSync as readdirSync6 } from "fs";
3356
- import { join as join4 } from "path";
3680
+ import { readFileSync as readFileSync9, existsSync as existsSync11, readdirSync as readdirSync7 } from "fs";
3681
+ import { join as join5 } from "path";
3357
3682
  function parsePrismaSchema() {
3358
3683
  const schemaPath = getResolvedPaths().prismaSchemaPath;
3359
- if (!existsSync10(schemaPath)) {
3684
+ if (!existsSync11(schemaPath)) {
3360
3685
  throw new Error(`Prisma schema not found at ${schemaPath}`);
3361
3686
  }
3362
- const source = readFileSync8(schemaPath, "utf-8");
3687
+ const source = readFileSync9(schemaPath, "utf-8");
3363
3688
  const models = [];
3364
3689
  const sourceLines = source.split("\n");
3365
3690
  let i = 0;
@@ -3417,14 +3742,14 @@ function toSnakeCase(str) {
3417
3742
  function findColumnUsageInRouters(tableName) {
3418
3743
  const usage = /* @__PURE__ */ new Map();
3419
3744
  const routersDir = getResolvedPaths().routersDir;
3420
- if (!existsSync10(routersDir)) return usage;
3745
+ if (!existsSync11(routersDir)) return usage;
3421
3746
  scanDirectory(routersDir, tableName, usage);
3422
3747
  return usage;
3423
3748
  }
3424
3749
  function scanDirectory(dir, tableName, usage) {
3425
- const entries = readdirSync6(dir, { withFileTypes: true });
3750
+ const entries = readdirSync7(dir, { withFileTypes: true });
3426
3751
  for (const entry of entries) {
3427
- const fullPath = join4(dir, entry.name);
3752
+ const fullPath = join5(dir, entry.name);
3428
3753
  if (entry.isDirectory()) {
3429
3754
  scanDirectory(fullPath, tableName, usage);
3430
3755
  } else if (entry.name.endsWith(".ts")) {
@@ -3434,7 +3759,7 @@ function scanDirectory(dir, tableName, usage) {
3434
3759
  }
3435
3760
  function scanFile(absPath, tableName, usage) {
3436
3761
  try {
3437
- const source = readFileSync8(absPath, "utf-8");
3762
+ const source = readFileSync9(absPath, "utf-8");
3438
3763
  if (!source.includes(tableName)) return;
3439
3764
  const relPath = absPath.slice(getProjectRoot().length + 1);
3440
3765
  const lines = source.split("\n");
@@ -3479,15 +3804,15 @@ function detectMismatches(models) {
3479
3804
  }
3480
3805
  function findFilesUsingColumn(dir, column, tableName) {
3481
3806
  const result = [];
3482
- if (!existsSync10(dir)) return result;
3483
- const entries = readdirSync6(dir, { withFileTypes: true });
3807
+ if (!existsSync11(dir)) return result;
3808
+ const entries = readdirSync7(dir, { withFileTypes: true });
3484
3809
  for (const entry of entries) {
3485
- const fullPath = join4(dir, entry.name);
3810
+ const fullPath = join5(dir, entry.name);
3486
3811
  if (entry.isDirectory()) {
3487
3812
  result.push(...findFilesUsingColumn(fullPath, column, tableName));
3488
3813
  } else if (entry.name.endsWith(".ts")) {
3489
3814
  try {
3490
- const source = readFileSync8(fullPath, "utf-8");
3815
+ const source = readFileSync9(fullPath, "utf-8");
3491
3816
  if (source.includes(tableName) && source.includes(column)) {
3492
3817
  result.push(fullPath.slice(getProjectRoot().length + 1));
3493
3818
  }
@@ -3632,8 +3957,8 @@ var init_import_parser = __esm({
3632
3957
  });
3633
3958
 
3634
3959
  // 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";
3960
+ import { readFileSync as readFileSync10, existsSync as existsSync12, readdirSync as readdirSync8 } from "fs";
3961
+ import { resolve as resolve11, join as join6, relative, dirname as dirname8 } from "path";
3637
3962
  function resolvePythonModulePath(module, fromFile, pythonRoot, level) {
3638
3963
  const projectRoot = getProjectRoot();
3639
3964
  if (level > 0) {
@@ -3644,22 +3969,22 @@ function resolvePythonModulePath(module, fromFile, pythonRoot, level) {
3644
3969
  const modulePart = module.replace(/^\.+/, "");
3645
3970
  if (modulePart) {
3646
3971
  const parts2 = modulePart.split(".");
3647
- return tryResolvePythonPath(join5(baseDir, ...parts2), projectRoot);
3972
+ return tryResolvePythonPath(join6(baseDir, ...parts2), projectRoot);
3648
3973
  }
3649
3974
  return tryResolvePythonPath(baseDir, projectRoot);
3650
3975
  }
3651
3976
  const parts = module.split(".");
3652
- const candidate = join5(resolve11(projectRoot, pythonRoot), ...parts);
3977
+ const candidate = join6(resolve11(projectRoot, pythonRoot), ...parts);
3653
3978
  return tryResolvePythonPath(candidate, projectRoot);
3654
3979
  }
3655
3980
  function tryResolvePythonPath(basePath, projectRoot) {
3656
- if (existsSync11(basePath + ".py")) {
3981
+ if (existsSync12(basePath + ".py")) {
3657
3982
  return relative(projectRoot, basePath + ".py");
3658
3983
  }
3659
- if (existsSync11(join5(basePath, "__init__.py"))) {
3660
- return relative(projectRoot, join5(basePath, "__init__.py"));
3984
+ if (existsSync12(join6(basePath, "__init__.py"))) {
3985
+ return relative(projectRoot, join6(basePath, "__init__.py"));
3661
3986
  }
3662
- if (basePath.endsWith(".py") && existsSync11(basePath)) {
3987
+ if (basePath.endsWith(".py") && existsSync12(basePath)) {
3663
3988
  return relative(projectRoot, basePath);
3664
3989
  }
3665
3990
  return null;
@@ -3667,13 +3992,13 @@ function tryResolvePythonPath(basePath, projectRoot) {
3667
3992
  function walkPythonFiles(dir, excludeDirs) {
3668
3993
  const files = [];
3669
3994
  try {
3670
- const entries = readdirSync7(dir, { withFileTypes: true });
3995
+ const entries = readdirSync8(dir, { withFileTypes: true });
3671
3996
  for (const entry of entries) {
3672
3997
  if (entry.isDirectory()) {
3673
3998
  if (excludeDirs.includes(entry.name)) continue;
3674
- files.push(...walkPythonFiles(join5(dir, entry.name), excludeDirs));
3999
+ files.push(...walkPythonFiles(join6(dir, entry.name), excludeDirs));
3675
4000
  } else if (entry.name.endsWith(".py")) {
3676
- files.push(join5(dir, entry.name));
4001
+ files.push(join6(dir, entry.name));
3677
4002
  }
3678
4003
  }
3679
4004
  } catch {
@@ -3699,7 +4024,7 @@ function buildPythonImportIndex(dataDb2, pythonRoot, excludeDirs = ["__pycache__
3699
4024
  const relFile = relative(projectRoot, absFile);
3700
4025
  let source;
3701
4026
  try {
3702
- source = readFileSync9(absFile, "utf-8");
4027
+ source = readFileSync10(absFile, "utf-8");
3703
4028
  } catch {
3704
4029
  continue;
3705
4030
  }
@@ -3965,18 +4290,18 @@ var init_route_parser = __esm({
3965
4290
  });
3966
4291
 
3967
4292
  // 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";
4293
+ import { readFileSync as readFileSync11, readdirSync as readdirSync9 } from "fs";
4294
+ import { join as join7, relative as relative2 } from "path";
3970
4295
  function walkPyFiles(dir, excludeDirs) {
3971
4296
  const files = [];
3972
4297
  try {
3973
- const entries = readdirSync8(dir, { withFileTypes: true });
4298
+ const entries = readdirSync9(dir, { withFileTypes: true });
3974
4299
  for (const entry of entries) {
3975
4300
  if (entry.isDirectory()) {
3976
4301
  if (excludeDirs.includes(entry.name)) continue;
3977
- files.push(...walkPyFiles(join6(dir, entry.name), excludeDirs));
4302
+ files.push(...walkPyFiles(join7(dir, entry.name), excludeDirs));
3978
4303
  } else if (entry.name.endsWith(".py")) {
3979
- files.push(join6(dir, entry.name));
4304
+ files.push(join7(dir, entry.name));
3980
4305
  }
3981
4306
  }
3982
4307
  } catch {
@@ -3985,7 +4310,7 @@ function walkPyFiles(dir, excludeDirs) {
3985
4310
  }
3986
4311
  function buildPythonRouteIndex(dataDb2, pythonRoot, excludeDirs = ["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"]) {
3987
4312
  const projectRoot = getProjectRoot();
3988
- const absRoot = join6(projectRoot, pythonRoot);
4313
+ const absRoot = join7(projectRoot, pythonRoot);
3989
4314
  dataDb2.exec("DELETE FROM massu_py_routes");
3990
4315
  dataDb2.exec("DELETE FROM massu_py_route_callers");
3991
4316
  const insertStmt = dataDb2.prepare(
@@ -3998,7 +4323,7 @@ function buildPythonRouteIndex(dataDb2, pythonRoot, excludeDirs = ["__pycache__"
3998
4323
  const relFile = relative2(projectRoot, absFile);
3999
4324
  let source;
4000
4325
  try {
4001
- source = readFileSync10(absFile, "utf-8");
4326
+ source = readFileSync11(absFile, "utf-8");
4002
4327
  } catch {
4003
4328
  continue;
4004
4329
  }
@@ -4209,18 +4534,18 @@ var init_model_parser = __esm({
4209
4534
  });
4210
4535
 
4211
4536
  // 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";
4537
+ import { readFileSync as readFileSync12, readdirSync as readdirSync10 } from "fs";
4538
+ import { join as join8, relative as relative3 } from "path";
4214
4539
  function walkPyFiles2(dir, excludeDirs) {
4215
4540
  const files = [];
4216
4541
  try {
4217
- const entries = readdirSync9(dir, { withFileTypes: true });
4542
+ const entries = readdirSync10(dir, { withFileTypes: true });
4218
4543
  for (const entry of entries) {
4219
4544
  if (entry.isDirectory()) {
4220
4545
  if (excludeDirs.includes(entry.name)) continue;
4221
- files.push(...walkPyFiles2(join7(dir, entry.name), excludeDirs));
4546
+ files.push(...walkPyFiles2(join8(dir, entry.name), excludeDirs));
4222
4547
  } else if (entry.name.endsWith(".py")) {
4223
- files.push(join7(dir, entry.name));
4548
+ files.push(join8(dir, entry.name));
4224
4549
  }
4225
4550
  }
4226
4551
  } catch {
@@ -4229,7 +4554,7 @@ function walkPyFiles2(dir, excludeDirs) {
4229
4554
  }
4230
4555
  function buildPythonModelIndex(dataDb2, pythonRoot, excludeDirs = ["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"]) {
4231
4556
  const projectRoot = getProjectRoot();
4232
- const absRoot = join7(projectRoot, pythonRoot);
4557
+ const absRoot = join8(projectRoot, pythonRoot);
4233
4558
  dataDb2.exec("DELETE FROM massu_py_models");
4234
4559
  dataDb2.exec("DELETE FROM massu_py_fk_edges");
4235
4560
  const insertModel = dataDb2.prepare(
@@ -4245,7 +4570,7 @@ function buildPythonModelIndex(dataDb2, pythonRoot, excludeDirs = ["__pycache__"
4245
4570
  const relFile = relative3(projectRoot, absFile);
4246
4571
  let source;
4247
4572
  try {
4248
- source = readFileSync11(absFile, "utf-8");
4573
+ source = readFileSync12(absFile, "utf-8");
4249
4574
  } catch {
4250
4575
  continue;
4251
4576
  }
@@ -4505,19 +4830,19 @@ var init_migration_parser = __esm({
4505
4830
  });
4506
4831
 
4507
4832
  // 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";
4833
+ import { readFileSync as readFileSync13, readdirSync as readdirSync11 } from "fs";
4834
+ import { join as join9, relative as relative4 } from "path";
4510
4835
  function buildPythonMigrationIndex(dataDb2, alembicDir) {
4511
4836
  const projectRoot = getProjectRoot();
4512
- const absDir = join8(projectRoot, alembicDir);
4837
+ const absDir = join9(projectRoot, alembicDir);
4513
4838
  dataDb2.exec("DELETE FROM massu_py_migrations");
4514
- const versionsDir = join8(absDir, "versions");
4839
+ const versionsDir = join9(absDir, "versions");
4515
4840
  let files = [];
4516
4841
  try {
4517
- files = readdirSync10(versionsDir).filter((f) => f.endsWith(".py")).map((f) => join8(versionsDir, f));
4842
+ files = readdirSync11(versionsDir).filter((f) => f.endsWith(".py")).map((f) => join9(versionsDir, f));
4518
4843
  } catch {
4519
4844
  try {
4520
- files = readdirSync10(absDir).filter((f) => f.endsWith(".py") && f !== "env.py").map((f) => join8(absDir, f));
4845
+ files = readdirSync11(absDir).filter((f) => f.endsWith(".py") && f !== "env.py").map((f) => join9(absDir, f));
4521
4846
  } catch {
4522
4847
  }
4523
4848
  }
@@ -4531,7 +4856,7 @@ function buildPythonMigrationIndex(dataDb2, alembicDir) {
4531
4856
  for (const absFile of files) {
4532
4857
  let source;
4533
4858
  try {
4534
- source = readFileSync12(absFile, "utf-8");
4859
+ source = readFileSync13(absFile, "utf-8");
4535
4860
  } catch {
4536
4861
  continue;
4537
4862
  }
@@ -4565,12 +4890,12 @@ var init_migration_indexer = __esm({
4565
4890
  });
4566
4891
 
4567
4892
  // 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";
4893
+ import { readFileSync as readFileSync14, readdirSync as readdirSync12 } from "fs";
4894
+ import { join as join10, relative as relative5 } from "path";
4570
4895
  function buildPythonCouplingIndex(dataDb2) {
4571
4896
  const projectRoot = getProjectRoot();
4572
4897
  const config = getConfig();
4573
- const srcDir = join9(projectRoot, config.paths.source);
4898
+ const srcDir = join10(projectRoot, config.paths.source);
4574
4899
  const routes = dataDb2.prepare("SELECT id, method, path FROM massu_py_routes").all();
4575
4900
  if (routes.length === 0) return 0;
4576
4901
  dataDb2.exec("DELETE FROM massu_py_route_callers");
@@ -4602,7 +4927,7 @@ function buildPythonCouplingIndex(dataDb2) {
4602
4927
  const relFile = relative5(projectRoot, absFile);
4603
4928
  let source;
4604
4929
  try {
4605
- source = readFileSync13(absFile, "utf-8");
4930
+ source = readFileSync14(absFile, "utf-8");
4606
4931
  } catch {
4607
4932
  continue;
4608
4933
  }
@@ -4630,13 +4955,13 @@ function walkFrontendFiles(dir) {
4630
4955
  const files = [];
4631
4956
  const exclude = ["node_modules", ".next", "dist", ".git", "__pycache__", ".venv", "venv"];
4632
4957
  try {
4633
- const entries = readdirSync11(dir, { withFileTypes: true });
4958
+ const entries = readdirSync12(dir, { withFileTypes: true });
4634
4959
  for (const entry of entries) {
4635
4960
  if (entry.isDirectory()) {
4636
4961
  if (exclude.includes(entry.name)) continue;
4637
- files.push(...walkFrontendFiles(join9(dir, entry.name)));
4962
+ files.push(...walkFrontendFiles(join10(dir, entry.name)));
4638
4963
  } else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
4639
- files.push(join9(dir, entry.name));
4964
+ files.push(join10(dir, entry.name));
4640
4965
  }
4641
4966
  }
4642
4967
  } catch {
@@ -4742,6 +5067,16 @@ function getMemoryToolDefinitions() {
4742
5067
  required: []
4743
5068
  }
4744
5069
  },
5070
+ // P4-007: memory_backfill
5071
+ {
5072
+ name: p("memory_backfill"),
5073
+ 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.",
5074
+ inputSchema: {
5075
+ type: "object",
5076
+ properties: {},
5077
+ required: []
5078
+ }
5079
+ },
4745
5080
  // P4-006: memory_ingest
4746
5081
  {
4747
5082
  name: p("memory_ingest"),
@@ -4786,6 +5121,8 @@ function handleMemoryToolCall(name, args2, memoryDb) {
4786
5121
  return handleFailures(args2, memoryDb);
4787
5122
  case "memory_ingest":
4788
5123
  return handleIngest(args2, memoryDb);
5124
+ case "memory_backfill":
5125
+ return handleBackfill(memoryDb);
4789
5126
  default:
4790
5127
  return text(`Unknown memory tool: ${name}`);
4791
5128
  }
@@ -4960,6 +5297,21 @@ Title: ${title}
4960
5297
  Importance: ${importance}
4961
5298
  Session: ${activeSession.session_id.slice(0, 8)}...`);
4962
5299
  }
5300
+ function handleBackfill(db) {
5301
+ const memoryDir = getResolvedPaths().memoryDir;
5302
+ const stats = backfillMemoryFiles(db, memoryDir);
5303
+ const lines = [
5304
+ "## Memory Backfill Results",
5305
+ "",
5306
+ `- **Total files scanned**: ${stats.total}`,
5307
+ `- **Inserted (new)**: ${stats.inserted}`,
5308
+ `- **Updated (existing)**: ${stats.updated}`,
5309
+ `- **Skipped (not found)**: ${stats.skipped}`,
5310
+ "",
5311
+ stats.total === 0 ? "No memory files found in memory directory." : `Successfully processed ${stats.inserted + stats.updated} of ${stats.total} memory files.`
5312
+ ];
5313
+ return text(lines.join("\n"));
5314
+ }
4963
5315
  function text(content) {
4964
5316
  return { content: [{ type: "text", text: content }] };
4965
5317
  }
@@ -4975,11 +5327,12 @@ var init_memory_tools = __esm({
4975
5327
  "use strict";
4976
5328
  init_memory_db();
4977
5329
  init_config();
5330
+ init_memory_file_ingest();
4978
5331
  }
4979
5332
  });
4980
5333
 
4981
5334
  // src/docs-tools.ts
4982
- import { readFileSync as readFileSync14, existsSync as existsSync12 } from "fs";
5335
+ import { readFileSync as readFileSync15, existsSync as existsSync13 } from "fs";
4983
5336
  import { resolve as resolve12, basename as basename3 } from "path";
4984
5337
  function p2(baseName) {
4985
5338
  return `${getConfig().toolPrefix}_${baseName}`;
@@ -5035,10 +5388,10 @@ function handleDocsToolCall(name, args2) {
5035
5388
  }
5036
5389
  function loadDocsMap() {
5037
5390
  const mapPath = getResolvedPaths().docsMapPath;
5038
- if (!existsSync12(mapPath)) {
5391
+ if (!existsSync13(mapPath)) {
5039
5392
  throw new Error(`docs-map.json not found at ${mapPath}`);
5040
5393
  }
5041
- return JSON.parse(readFileSync14(mapPath, "utf-8"));
5394
+ return JSON.parse(readFileSync15(mapPath, "utf-8"));
5042
5395
  }
5043
5396
  function matchesPattern(filePath, pattern) {
5044
5397
  const regexStr = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
@@ -5109,12 +5462,12 @@ function extractFrontmatter(content) {
5109
5462
  function extractProcedureNames(routerPath) {
5110
5463
  const root = getProjectRoot();
5111
5464
  const absPath = ensureWithinRoot(resolve12(getResolvedPaths().srcDir, "..", routerPath), root);
5112
- if (!existsSync12(absPath)) {
5465
+ if (!existsSync13(absPath)) {
5113
5466
  const altPath = ensureWithinRoot(resolve12(getResolvedPaths().srcDir, "../server/api/routers", basename3(routerPath)), root);
5114
- if (!existsSync12(altPath)) return [];
5115
- return extractProcedureNamesFromContent(readFileSync14(altPath, "utf-8"));
5467
+ if (!existsSync13(altPath)) return [];
5468
+ return extractProcedureNamesFromContent(readFileSync15(altPath, "utf-8"));
5116
5469
  }
5117
- return extractProcedureNamesFromContent(readFileSync14(absPath, "utf-8"));
5470
+ return extractProcedureNamesFromContent(readFileSync15(absPath, "utf-8"));
5118
5471
  }
5119
5472
  function extractProcedureNamesFromContent(content) {
5120
5473
  const procRegex = /\.(?:query|mutation)\s*\(/g;
@@ -5155,7 +5508,7 @@ function handleDocsAudit(args2) {
5155
5508
  const mapping = docsMap.mappings.find((m) => m.id === mappingId);
5156
5509
  if (!mapping) continue;
5157
5510
  const helpPagePath = ensureWithinRoot(resolve12(getResolvedPaths().helpSitePath, mapping.helpPage), getProjectRoot());
5158
- if (!existsSync12(helpPagePath)) {
5511
+ if (!existsSync13(helpPagePath)) {
5159
5512
  results.push({
5160
5513
  helpPage: mapping.helpPage,
5161
5514
  mappingId,
@@ -5167,7 +5520,7 @@ function handleDocsAudit(args2) {
5167
5520
  });
5168
5521
  continue;
5169
5522
  }
5170
- const content = readFileSync14(helpPagePath, "utf-8");
5523
+ const content = readFileSync15(helpPagePath, "utf-8");
5171
5524
  const sections = extractSections(content);
5172
5525
  const frontmatter = extractFrontmatter(content);
5173
5526
  const staleReasons = [];
@@ -5208,8 +5561,8 @@ function handleDocsAudit(args2) {
5208
5561
  for (const [guideName, parentId] of Object.entries(docsMap.userGuideInheritance.examples)) {
5209
5562
  if (parentId === mappingId) {
5210
5563
  const guidePath = ensureWithinRoot(resolve12(getResolvedPaths().helpSitePath, `pages/user-guides/${guideName}/index.mdx`), getProjectRoot());
5211
- if (existsSync12(guidePath)) {
5212
- const guideContent = readFileSync14(guidePath, "utf-8");
5564
+ if (existsSync13(guidePath)) {
5565
+ const guideContent = readFileSync15(guidePath, "utf-8");
5213
5566
  const guideFrontmatter = extractFrontmatter(guideContent);
5214
5567
  if (!guideFrontmatter?.lastVerified || status === "STALE") {
5215
5568
  results.push({
@@ -5243,13 +5596,13 @@ function handleDocsCoverage(args2) {
5243
5596
  const mappings = filterDomain ? docsMap.mappings.filter((m) => m.id === filterDomain) : docsMap.mappings;
5244
5597
  for (const mapping of mappings) {
5245
5598
  const helpPagePath = ensureWithinRoot(resolve12(getResolvedPaths().helpSitePath, mapping.helpPage), getProjectRoot());
5246
- const exists = existsSync12(helpPagePath);
5599
+ const exists = existsSync13(helpPagePath);
5247
5600
  let hasContent = false;
5248
5601
  let lineCount = 0;
5249
5602
  let lastVerified = null;
5250
5603
  let status = null;
5251
5604
  if (exists) {
5252
- const content = readFileSync14(helpPagePath, "utf-8");
5605
+ const content = readFileSync15(helpPagePath, "utf-8");
5253
5606
  lineCount = content.split("\n").length;
5254
5607
  hasContent = lineCount > 10;
5255
5608
  const frontmatter = extractFrontmatter(content);
@@ -5590,7 +5943,7 @@ var init_observability_tools = __esm({
5590
5943
  });
5591
5944
 
5592
5945
  // src/sentinel-db.ts
5593
- import { existsSync as existsSync13 } from "fs";
5946
+ import { existsSync as existsSync14 } from "fs";
5594
5947
  import { resolve as resolve13 } from "path";
5595
5948
  function parsePortalScope(raw) {
5596
5949
  if (!raw) return [];
@@ -5828,22 +6181,22 @@ function validateFeatures(db, domainFilter) {
5828
6181
  const missingPages = [];
5829
6182
  for (const comp of components) {
5830
6183
  const absPath = resolve13(PROJECT_ROOT, comp.component_file);
5831
- if (!existsSync13(absPath)) {
6184
+ if (!existsSync14(absPath)) {
5832
6185
  missingComponents.push(comp.component_file);
5833
6186
  }
5834
6187
  }
5835
6188
  for (const proc of procedures) {
5836
6189
  const routerPath = resolve13(PROJECT_ROOT, `src/server/api/routers/${proc.router_name}.ts`);
5837
- if (!existsSync13(routerPath)) {
6190
+ if (!existsSync14(routerPath)) {
5838
6191
  missingProcedures.push({ router: proc.router_name, procedure: proc.procedure_name });
5839
6192
  }
5840
6193
  }
5841
6194
  for (const page of pages) {
5842
6195
  const routeToPath = page.page_route.replace(/^\/(portal-[^/]+\/)?/, "src/app/").replace(/\/$/, "") + "/page.tsx";
5843
6196
  const absPath = resolve13(PROJECT_ROOT, routeToPath);
5844
- if (page.page_route.startsWith("/") && !existsSync13(absPath)) {
6197
+ if (page.page_route.startsWith("/") && !existsSync14(absPath)) {
5845
6198
  const altPath = resolve13(PROJECT_ROOT, `src/app${page.page_route}/page.tsx`);
5846
- if (!existsSync13(altPath)) {
6199
+ if (!existsSync14(altPath)) {
5847
6200
  missingPages.push(page.page_route);
5848
6201
  }
5849
6202
  }
@@ -6367,8 +6720,8 @@ var init_sentinel_tools = __esm({
6367
6720
  });
6368
6721
 
6369
6722
  // 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";
6723
+ import { readFileSync as readFileSync16, existsSync as existsSync15, readdirSync as readdirSync13, statSync as statSync3 } from "fs";
6724
+ import { resolve as resolve14, join as join11, basename as basename4, dirname as dirname9, relative as relative6 } from "path";
6372
6725
  function inferDomain(filePath) {
6373
6726
  const domains = getConfig().domains;
6374
6727
  const path = filePath.toLowerCase();
@@ -6498,9 +6851,9 @@ function scanComponentExports(dataDb2) {
6498
6851
  const componentsBase = config.paths.components ?? config.paths.source + "/components";
6499
6852
  const componentDirs = [];
6500
6853
  const basePath = resolve14(projectRoot, componentsBase);
6501
- if (existsSync14(basePath)) {
6854
+ if (existsSync15(basePath)) {
6502
6855
  try {
6503
- const entries = readdirSync12(basePath, { withFileTypes: true });
6856
+ const entries = readdirSync13(basePath, { withFileTypes: true });
6504
6857
  for (const entry of entries) {
6505
6858
  if (entry.isDirectory()) {
6506
6859
  componentDirs.push(componentsBase + "/" + entry.name);
@@ -6511,11 +6864,11 @@ function scanComponentExports(dataDb2) {
6511
6864
  }
6512
6865
  for (const dir of componentDirs) {
6513
6866
  const absDir = resolve14(projectRoot, dir);
6514
- if (!existsSync14(absDir)) continue;
6867
+ if (!existsSync15(absDir)) continue;
6515
6868
  const files = walkDir(absDir).filter((f) => f.endsWith(".tsx") || f.endsWith(".ts"));
6516
6869
  for (const file of files) {
6517
6870
  const relPath = relative6(projectRoot, file);
6518
- const source = readFileSync15(file, "utf-8");
6871
+ const source = readFileSync16(file, "utf-8");
6519
6872
  const annotations = parseFeatureAnnotations(source);
6520
6873
  if (annotations.length > 0) {
6521
6874
  for (const ann of annotations) {
@@ -6564,9 +6917,9 @@ function scanComponentExports(dataDb2) {
6564
6917
  function walkDir(dir) {
6565
6918
  const results = [];
6566
6919
  try {
6567
- const entries = readdirSync12(dir);
6920
+ const entries = readdirSync13(dir);
6568
6921
  for (const entry of entries) {
6569
- const fullPath = join10(dir, entry);
6922
+ const fullPath = join11(dir, entry);
6570
6923
  try {
6571
6924
  const stat = statSync3(fullPath);
6572
6925
  if (stat.isDirectory()) {
@@ -7554,7 +7907,7 @@ var init_audit_trail = __esm({
7554
7907
  });
7555
7908
 
7556
7909
  // src/validation-engine.ts
7557
- import { existsSync as existsSync15, readFileSync as readFileSync16 } from "fs";
7910
+ import { existsSync as existsSync16, readFileSync as readFileSync17 } from "fs";
7558
7911
  function p9(baseName) {
7559
7912
  return `${getConfig().toolPrefix}_${baseName}`;
7560
7913
  }
@@ -7585,7 +7938,7 @@ function validateFile(filePath, projectRoot) {
7585
7938
  });
7586
7939
  return checks;
7587
7940
  }
7588
- if (!existsSync15(absPath)) {
7941
+ if (!existsSync16(absPath)) {
7589
7942
  checks.push({
7590
7943
  name: "file_exists",
7591
7944
  severity: "error",
@@ -7594,7 +7947,7 @@ function validateFile(filePath, projectRoot) {
7594
7947
  });
7595
7948
  return checks;
7596
7949
  }
7597
- const source = readFileSync16(absPath, "utf-8");
7950
+ const source = readFileSync17(absPath, "utf-8");
7598
7951
  const lines = source.split("\n");
7599
7952
  if (activeChecks.rule_compliance !== false) {
7600
7953
  for (const ruleSet of config.rules) {
@@ -8039,7 +8392,7 @@ var init_adr_generator = __esm({
8039
8392
  });
8040
8393
 
8041
8394
  // src/security-scorer.ts
8042
- import { existsSync as existsSync16, readFileSync as readFileSync17 } from "fs";
8395
+ import { existsSync as existsSync17, readFileSync as readFileSync18 } from "fs";
8043
8396
  function p11(baseName) {
8044
8397
  return `${getConfig().toolPrefix}_${baseName}`;
8045
8398
  }
@@ -8063,12 +8416,12 @@ function scoreFileSecurity(filePath, projectRoot) {
8063
8416
  }]
8064
8417
  };
8065
8418
  }
8066
- if (!existsSync16(absPath)) {
8419
+ if (!existsSync17(absPath)) {
8067
8420
  return { riskScore: 0, findings: [] };
8068
8421
  }
8069
8422
  let source;
8070
8423
  try {
8071
- source = readFileSync17(absPath, "utf-8");
8424
+ source = readFileSync18(absPath, "utf-8");
8072
8425
  } catch {
8073
8426
  return { riskScore: 0, findings: [] };
8074
8427
  }
@@ -8357,7 +8710,7 @@ var init_security_scorer = __esm({
8357
8710
  });
8358
8711
 
8359
8712
  // src/dependency-scorer.ts
8360
- import { existsSync as existsSync17, readFileSync as readFileSync18 } from "fs";
8713
+ import { existsSync as existsSync18, readFileSync as readFileSync19 } from "fs";
8361
8714
  import { resolve as resolve15 } from "path";
8362
8715
  function p12(baseName) {
8363
8716
  return `${getConfig().toolPrefix}_${baseName}`;
@@ -8391,9 +8744,9 @@ function calculateDepRisk(factors) {
8391
8744
  }
8392
8745
  function getInstalledPackages(projectRoot) {
8393
8746
  const pkgPath = resolve15(projectRoot, "package.json");
8394
- if (!existsSync17(pkgPath)) return /* @__PURE__ */ new Map();
8747
+ if (!existsSync18(pkgPath)) return /* @__PURE__ */ new Map();
8395
8748
  try {
8396
- const pkg = JSON.parse(readFileSync18(pkgPath, "utf-8"));
8749
+ const pkg = JSON.parse(readFileSync19(pkgPath, "utf-8"));
8397
8750
  const packages = /* @__PURE__ */ new Map();
8398
8751
  for (const [name, version] of Object.entries(pkg.dependencies ?? {})) {
8399
8752
  packages.set(name, version);
@@ -8996,7 +9349,7 @@ var init_regression_detector = __esm({
8996
9349
 
8997
9350
  // src/knowledge-indexer.ts
8998
9351
  import { createHash as createHash2 } from "crypto";
8999
- import { readFileSync as readFileSync19, readdirSync as readdirSync13, statSync as statSync4, existsSync as existsSync18 } from "fs";
9352
+ import { readFileSync as readFileSync20, readdirSync as readdirSync14, statSync as statSync4, existsSync as existsSync19 } from "fs";
9000
9353
  import { resolve as resolve16, relative as relative7, basename as basename5, extname } from "path";
9001
9354
  function getKnowledgePaths() {
9002
9355
  const resolved = getResolvedPaths();
@@ -9023,7 +9376,7 @@ function discoverMarkdownFiles(baseDir) {
9023
9376
  const files = [];
9024
9377
  function walk(dir) {
9025
9378
  try {
9026
- const entries = readdirSync13(dir, { withFileTypes: true });
9379
+ const entries = readdirSync14(dir, { withFileTypes: true });
9027
9380
  for (const entry of entries) {
9028
9381
  const fullPath = resolve16(dir, entry.name);
9029
9382
  if (entry.isDirectory()) {
@@ -9303,11 +9656,11 @@ function indexAllKnowledge(db) {
9303
9656
  files.push(...memFiles);
9304
9657
  } catch {
9305
9658
  }
9306
- if (existsSync18(paths.plansDir)) {
9659
+ if (existsSync19(paths.plansDir)) {
9307
9660
  const planFiles = discoverMarkdownFiles(paths.plansDir);
9308
9661
  files.push(...planFiles);
9309
9662
  }
9310
- if (existsSync18(paths.docsDir)) {
9663
+ if (existsSync19(paths.docsDir)) {
9311
9664
  const excludePatterns = getConfig().conventions?.excludePatterns ?? ["/ARCHIVE/", "/SESSION-HISTORY/"];
9312
9665
  const docsFiles = discoverMarkdownFiles(paths.docsDir).filter((f) => !f.includes("/plans/") && !excludePatterns.some((p18) => f.includes(p18)));
9313
9666
  files.push(...docsFiles);
@@ -9350,8 +9703,8 @@ function indexAllKnowledge(db) {
9350
9703
  } catch {
9351
9704
  }
9352
9705
  for (const filePath of files) {
9353
- if (!existsSync18(filePath)) continue;
9354
- const content = readFileSync19(filePath, "utf-8");
9706
+ if (!existsSync19(filePath)) continue;
9707
+ const content = readFileSync20(filePath, "utf-8");
9355
9708
  const hash = hashContent(content);
9356
9709
  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);
9357
9710
  const category = categorizeFile(filePath);
@@ -9471,10 +9824,10 @@ function isKnowledgeStale(db) {
9471
9824
  files.push(...discoverMarkdownFiles(paths.memoryDir));
9472
9825
  } catch {
9473
9826
  }
9474
- if (existsSync18(paths.plansDir)) {
9827
+ if (existsSync19(paths.plansDir)) {
9475
9828
  files.push(...discoverMarkdownFiles(paths.plansDir));
9476
9829
  }
9477
- if (existsSync18(paths.docsDir)) {
9830
+ if (existsSync19(paths.docsDir)) {
9478
9831
  const excludePatterns = getConfig().conventions?.excludePatterns ?? ["/ARCHIVE/", "/SESSION-HISTORY/"];
9479
9832
  const docsFiles = discoverMarkdownFiles(paths.docsDir).filter((f) => !f.includes("/plans/") && !excludePatterns.some((p18) => f.includes(p18)));
9480
9833
  files.push(...docsFiles);
@@ -9503,7 +9856,7 @@ var init_knowledge_indexer = __esm({
9503
9856
  });
9504
9857
 
9505
9858
  // src/knowledge-tools.ts
9506
- import { readFileSync as readFileSync20, writeFileSync as writeFileSync3, appendFileSync, readdirSync as readdirSync14 } from "fs";
9859
+ import { readFileSync as readFileSync21, writeFileSync as writeFileSync3, appendFileSync, readdirSync as readdirSync15 } from "fs";
9507
9860
  import { resolve as resolve17, basename as basename6 } from "path";
9508
9861
  function p15(baseName) {
9509
9862
  return `${getConfig().toolPrefix}_${baseName}`;
@@ -10254,7 +10607,7 @@ ${crRule ? `- **CR**: ${crRule}
10254
10607
  `;
10255
10608
  let existing = "";
10256
10609
  try {
10257
- existing = readFileSync20(correctionsPath, "utf-8");
10610
+ existing = readFileSync21(correctionsPath, "utf-8");
10258
10611
  } catch {
10259
10612
  }
10260
10613
  const archiveIdx = existing.indexOf("## Archived");
@@ -10436,7 +10789,7 @@ function handleGaps(db, args2) {
10436
10789
  } else if (checkType === "routers") {
10437
10790
  try {
10438
10791
  const routersDir = getResolvedPaths().routersDir;
10439
- const routerFiles = readdirSync14(routersDir).filter((f) => f.endsWith(".ts") && !f.startsWith("_"));
10792
+ const routerFiles = readdirSync15(routersDir).filter((f) => f.endsWith(".ts") && !f.startsWith("_"));
10440
10793
  lines.push(`| Router | Knowledge Hits | Status |`);
10441
10794
  lines.push(`|--------|----------------|--------|`);
10442
10795
  for (const file of routerFiles) {
@@ -10593,11 +10946,11 @@ var init_knowledge_tools = __esm({
10593
10946
  // src/knowledge-db.ts
10594
10947
  import Database3 from "better-sqlite3";
10595
10948
  import { dirname as dirname10 } from "path";
10596
- import { existsSync as existsSync20, mkdirSync as mkdirSync5 } from "fs";
10949
+ import { existsSync as existsSync21, mkdirSync as mkdirSync5 } from "fs";
10597
10950
  function getKnowledgeDb() {
10598
10951
  const dbPath = getResolvedPaths().knowledgeDbPath;
10599
10952
  const dir = dirname10(dbPath);
10600
- if (!existsSync20(dir)) {
10953
+ if (!existsSync21(dir)) {
10601
10954
  mkdirSync5(dir, { recursive: true });
10602
10955
  }
10603
10956
  const db = new Database3(dbPath);
@@ -11331,7 +11684,7 @@ var init_python_tools = __esm({
11331
11684
  });
11332
11685
 
11333
11686
  // src/tools.ts
11334
- import { readFileSync as readFileSync21, existsSync as existsSync21 } from "fs";
11687
+ import { readFileSync as readFileSync22, existsSync as existsSync22 } from "fs";
11335
11688
  import { resolve as resolve18, basename as basename7 } from "path";
11336
11689
  function prefix() {
11337
11690
  return getConfig().toolPrefix;
@@ -11767,8 +12120,8 @@ function handleContext(file, dataDb2, codegraphDb2) {
11767
12120
  const resolvedPaths = getResolvedPaths();
11768
12121
  const root = getProjectRoot();
11769
12122
  const absFilePath = ensureWithinRoot(resolve18(resolvedPaths.srcDir, "..", file), root);
11770
- if (existsSync21(absFilePath)) {
11771
- const fileContent = readFileSync21(absFilePath, "utf-8").slice(0, 3e3);
12123
+ if (existsSync22(absFilePath)) {
12124
+ const fileContent = readFileSync22(absFilePath, "utf-8").slice(0, 3e3);
11772
12125
  const keywords = [];
11773
12126
  if (fileContent.includes("ctx.db")) keywords.push("database", "schema");
11774
12127
  if (fileContent.includes("BigInt") || fileContent.includes("Decimal")) keywords.push("BigInt", "serialization");
@@ -12193,10 +12546,10 @@ function handleSchema(args2) {
12193
12546
  lines.push("");
12194
12547
  const projectRoot = getProjectRoot();
12195
12548
  const absPath = ensureWithinRoot(resolve18(projectRoot, file), projectRoot);
12196
- if (!existsSync21(absPath)) {
12549
+ if (!existsSync22(absPath)) {
12197
12550
  return text17(`File not found: ${file}`);
12198
12551
  }
12199
- const source = readFileSync21(absPath, "utf-8");
12552
+ const source = readFileSync22(absPath, "utf-8");
12200
12553
  const config = getConfig();
12201
12554
  const dbPattern = config.dbAccessPattern ?? "ctx.db.{table}";
12202
12555
  const regexStr = dbPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace("\\{table\\}", "(\\w+)");
@@ -12274,7 +12627,7 @@ var init_tools = __esm({
12274
12627
 
12275
12628
  // src/server.ts
12276
12629
  var server_exports = {};
12277
- import { readFileSync as readFileSync22 } from "fs";
12630
+ import { readFileSync as readFileSync23 } from "fs";
12278
12631
  import { resolve as resolve19, dirname as dirname11 } from "path";
12279
12632
  import { fileURLToPath as fileURLToPath4 } from "url";
12280
12633
  function getDb() {
@@ -12370,7 +12723,7 @@ var init_server = __esm({
12370
12723
  __dirname4 = dirname11(fileURLToPath4(import.meta.url));
12371
12724
  PKG_VERSION = (() => {
12372
12725
  try {
12373
- const pkg = JSON.parse(readFileSync22(resolve19(__dirname4, "..", "package.json"), "utf-8"));
12726
+ const pkg = JSON.parse(readFileSync23(resolve19(__dirname4, "..", "package.json"), "utf-8"));
12374
12727
  return pkg.version ?? "0.0.0";
12375
12728
  } catch {
12376
12729
  return "0.0.0";
@@ -12434,7 +12787,7 @@ var init_server = __esm({
12434
12787
  });
12435
12788
 
12436
12789
  // src/cli.ts
12437
- import { readFileSync as readFileSync23 } from "fs";
12790
+ import { readFileSync as readFileSync24 } from "fs";
12438
12791
  import { resolve as resolve20, dirname as dirname12 } from "path";
12439
12792
  import { fileURLToPath as fileURLToPath5 } from "url";
12440
12793
  var __filename4 = fileURLToPath5(import.meta.url);
@@ -12509,7 +12862,7 @@ Documentation: https://massu.ai/docs
12509
12862
  }
12510
12863
  function printVersion() {
12511
12864
  try {
12512
- const pkg = JSON.parse(readFileSync23(resolve20(__dirname5, "../package.json"), "utf-8"));
12865
+ const pkg = JSON.parse(readFileSync24(resolve20(__dirname5, "../package.json"), "utf-8"));
12513
12866
  console.log(`massu v${pkg.version}`);
12514
12867
  } catch {
12515
12868
  console.log("massu v0.1.0");