@massu/core 0.4.2 → 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 +1530 -1177
- package/dist/hooks/post-tool-use.js +75 -6
- package/dist/hooks/user-prompt.js +16 -0
- package/package.json +1 -1
- package/src/commands/init.ts +27 -0
- package/src/hooks/post-tool-use.ts +17 -0
- package/src/hooks/user-prompt.ts +21 -0
- package/src/memory-file-ingest.ts +127 -0
- package/src/memory-tools.ts +34 -1
package/dist/cli.js
CHANGED
|
@@ -344,822 +344,323 @@ var init_config = __esm({
|
|
|
344
344
|
}
|
|
345
345
|
});
|
|
346
346
|
|
|
347
|
-
// src/
|
|
348
|
-
var
|
|
349
|
-
__export(
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
|
355
|
-
import {
|
|
356
|
-
import {
|
|
357
|
-
function
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
|
|
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
|
|
621
|
+
session_id TEXT NOT NULL UNIQUE,
|
|
912
622
|
project TEXT NOT NULL DEFAULT 'my-project',
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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
|
-
|
|
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
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
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
|
|
1406
|
-
const
|
|
1407
|
-
|
|
1408
|
-
const
|
|
1409
|
-
|
|
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
|
-
|
|
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
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
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
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
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 (
|
|
1447
|
-
|
|
1448
|
-
params.push(opts.dateFrom);
|
|
1601
|
+
if (!result.root) {
|
|
1602
|
+
result.root = ".";
|
|
1449
1603
|
}
|
|
1450
|
-
|
|
1451
|
-
params.push(limit);
|
|
1452
|
-
return db.prepare(sql).all(...params);
|
|
1604
|
+
return result;
|
|
1453
1605
|
}
|
|
1454
|
-
function
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
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
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
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
|
|
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
|
|
1478
|
-
const
|
|
1479
|
-
const
|
|
1480
|
-
|
|
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
|
|
1483
|
-
const
|
|
1484
|
-
const
|
|
1485
|
-
|
|
1486
|
-
|
|
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
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
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
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
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
|
-
|
|
1500
|
-
|
|
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
|
|
1503
|
-
const
|
|
1504
|
-
let
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
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
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
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
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
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
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
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
|
-
|
|
1528
|
-
params.push(limit);
|
|
1529
|
-
return db.prepare(sql).all(...params);
|
|
1844
|
+
return { created, memoryMdCreated };
|
|
1530
1845
|
}
|
|
1531
|
-
function
|
|
1532
|
-
const
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
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
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
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
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
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
|
-
|
|
1549
|
-
|
|
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
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
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
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
const
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
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
|
|
1623
|
-
|
|
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
|
|
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
|
|
2252
|
+
import { parse as parseYaml3 } from "yaml";
|
|
1928
2253
|
function checkConfig(projectRoot) {
|
|
1929
2254
|
const configPath = resolve5(projectRoot, "massu.config.yaml");
|
|
1930
|
-
if (!
|
|
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 =
|
|
1935
|
-
const parsed =
|
|
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 (!
|
|
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(
|
|
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 (!
|
|
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(
|
|
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 (!
|
|
2317
|
+
if (!existsSync6(nodeModulesHooksDir)) {
|
|
1993
2318
|
const devHooksDir = resolve5(__dirname3, "../../dist/hooks");
|
|
1994
|
-
if (
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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(
|
|
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 (!
|
|
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 =
|
|
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 && !
|
|
2474
|
+
if (depth <= 2 && !existsSync6(resolve5(subdir, "__init__.py"))) {
|
|
2150
2475
|
try {
|
|
2151
|
-
const subEntries =
|
|
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 (!
|
|
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 =
|
|
2253
|
-
const parsed =
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
2944
|
+
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
2620
2945
|
for (const entry of entries) {
|
|
2621
|
-
const fullPath =
|
|
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
|
|
2723
|
-
import { resolve as resolve7, dirname as dirname7, join as
|
|
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 (
|
|
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 (
|
|
3114
|
+
if (existsSync8(withExt)) {
|
|
2790
3115
|
return toRelative(withExt);
|
|
2791
3116
|
}
|
|
2792
3117
|
}
|
|
2793
3118
|
for (const indexFile of resolvedPaths.indexFiles) {
|
|
2794
|
-
const indexPath =
|
|
2795
|
-
if (
|
|
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 (!
|
|
3157
|
+
if (!existsSync8(absPath)) continue;
|
|
2833
3158
|
let source;
|
|
2834
3159
|
try {
|
|
2835
|
-
source =
|
|
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
|
|
2872
|
-
import { resolve as resolve8, join as
|
|
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 (!
|
|
3201
|
+
if (!existsSync9(rootPath)) {
|
|
2877
3202
|
throw new Error(`Root router not found at ${rootPath}`);
|
|
2878
3203
|
}
|
|
2879
|
-
const source =
|
|
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 (
|
|
3216
|
+
if (existsSync9(candidate)) {
|
|
2892
3217
|
filePath = routersRelPath + "/" + filePath + ext;
|
|
2893
3218
|
break;
|
|
2894
3219
|
}
|
|
2895
|
-
const indexCandidate =
|
|
2896
|
-
if (
|
|
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 (!
|
|
2917
|
-
const source =
|
|
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 (!
|
|
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 =
|
|
3277
|
+
const entries = readdirSync6(dir, { withFileTypes: true });
|
|
2953
3278
|
for (const entry of entries) {
|
|
2954
|
-
const fullPath =
|
|
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 =
|
|
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
|
|
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 (!
|
|
3388
|
+
if (!existsSync10(absPath)) continue;
|
|
3064
3389
|
try {
|
|
3065
|
-
const source =
|
|
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 (!
|
|
3409
|
+
if (!existsSync10(absPath)) continue;
|
|
3085
3410
|
try {
|
|
3086
|
-
const source =
|
|
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
|
|
3356
|
-
import { join as
|
|
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 (!
|
|
3684
|
+
if (!existsSync11(schemaPath)) {
|
|
3360
3685
|
throw new Error(`Prisma schema not found at ${schemaPath}`);
|
|
3361
3686
|
}
|
|
3362
|
-
const source =
|
|
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 (!
|
|
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 =
|
|
3750
|
+
const entries = readdirSync7(dir, { withFileTypes: true });
|
|
3426
3751
|
for (const entry of entries) {
|
|
3427
|
-
const fullPath =
|
|
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 =
|
|
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 (!
|
|
3483
|
-
const entries =
|
|
3807
|
+
if (!existsSync11(dir)) return result;
|
|
3808
|
+
const entries = readdirSync7(dir, { withFileTypes: true });
|
|
3484
3809
|
for (const entry of entries) {
|
|
3485
|
-
const fullPath =
|
|
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 =
|
|
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
|
|
3636
|
-
import { resolve as resolve11, join as
|
|
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(
|
|
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 =
|
|
3977
|
+
const candidate = join6(resolve11(projectRoot, pythonRoot), ...parts);
|
|
3653
3978
|
return tryResolvePythonPath(candidate, projectRoot);
|
|
3654
3979
|
}
|
|
3655
3980
|
function tryResolvePythonPath(basePath, projectRoot) {
|
|
3656
|
-
if (
|
|
3981
|
+
if (existsSync12(basePath + ".py")) {
|
|
3657
3982
|
return relative(projectRoot, basePath + ".py");
|
|
3658
3983
|
}
|
|
3659
|
-
if (
|
|
3660
|
-
return relative(projectRoot,
|
|
3984
|
+
if (existsSync12(join6(basePath, "__init__.py"))) {
|
|
3985
|
+
return relative(projectRoot, join6(basePath, "__init__.py"));
|
|
3661
3986
|
}
|
|
3662
|
-
if (basePath.endsWith(".py") &&
|
|
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 =
|
|
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(
|
|
3999
|
+
files.push(...walkPythonFiles(join6(dir, entry.name), excludeDirs));
|
|
3675
4000
|
} else if (entry.name.endsWith(".py")) {
|
|
3676
|
-
files.push(
|
|
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 =
|
|
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
|
|
3969
|
-
import { join as
|
|
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 =
|
|
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(
|
|
4302
|
+
files.push(...walkPyFiles(join7(dir, entry.name), excludeDirs));
|
|
3978
4303
|
} else if (entry.name.endsWith(".py")) {
|
|
3979
|
-
files.push(
|
|
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 =
|
|
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 =
|
|
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
|
|
4213
|
-
import { join as
|
|
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 =
|
|
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(
|
|
4546
|
+
files.push(...walkPyFiles2(join8(dir, entry.name), excludeDirs));
|
|
4222
4547
|
} else if (entry.name.endsWith(".py")) {
|
|
4223
|
-
files.push(
|
|
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 =
|
|
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 =
|
|
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
|
|
4509
|
-
import { join as
|
|
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 =
|
|
4837
|
+
const absDir = join9(projectRoot, alembicDir);
|
|
4513
4838
|
dataDb2.exec("DELETE FROM massu_py_migrations");
|
|
4514
|
-
const versionsDir =
|
|
4839
|
+
const versionsDir = join9(absDir, "versions");
|
|
4515
4840
|
let files = [];
|
|
4516
4841
|
try {
|
|
4517
|
-
files =
|
|
4842
|
+
files = readdirSync11(versionsDir).filter((f) => f.endsWith(".py")).map((f) => join9(versionsDir, f));
|
|
4518
4843
|
} catch {
|
|
4519
4844
|
try {
|
|
4520
|
-
files =
|
|
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 =
|
|
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
|
|
4569
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
4962
|
+
files.push(...walkFrontendFiles(join10(dir, entry.name)));
|
|
4638
4963
|
} else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
|
|
4639
|
-
files.push(
|
|
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
|
|
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 (!
|
|
5391
|
+
if (!existsSync13(mapPath)) {
|
|
5039
5392
|
throw new Error(`docs-map.json not found at ${mapPath}`);
|
|
5040
5393
|
}
|
|
5041
|
-
return JSON.parse(
|
|
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 (!
|
|
5465
|
+
if (!existsSync13(absPath)) {
|
|
5113
5466
|
const altPath = ensureWithinRoot(resolve12(getResolvedPaths().srcDir, "../server/api/routers", basename3(routerPath)), root);
|
|
5114
|
-
if (!
|
|
5115
|
-
return extractProcedureNamesFromContent(
|
|
5467
|
+
if (!existsSync13(altPath)) return [];
|
|
5468
|
+
return extractProcedureNamesFromContent(readFileSync15(altPath, "utf-8"));
|
|
5116
5469
|
}
|
|
5117
|
-
return extractProcedureNamesFromContent(
|
|
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 (!
|
|
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 =
|
|
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 (
|
|
5212
|
-
const guideContent =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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("/") && !
|
|
6197
|
+
if (page.page_route.startsWith("/") && !existsSync14(absPath)) {
|
|
5845
6198
|
const altPath = resolve13(PROJECT_ROOT, `src/app${page.page_route}/page.tsx`);
|
|
5846
|
-
if (!
|
|
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
|
|
6371
|
-
import { resolve as resolve14, join as
|
|
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 (
|
|
6854
|
+
if (existsSync15(basePath)) {
|
|
6502
6855
|
try {
|
|
6503
|
-
const entries =
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
6920
|
+
const entries = readdirSync13(dir);
|
|
6568
6921
|
for (const entry of entries) {
|
|
6569
|
-
const fullPath =
|
|
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
|
|
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 (!
|
|
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 =
|
|
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
|
|
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 (!
|
|
8419
|
+
if (!existsSync17(absPath)) {
|
|
8067
8420
|
return { riskScore: 0, findings: [] };
|
|
8068
8421
|
}
|
|
8069
8422
|
let source;
|
|
8070
8423
|
try {
|
|
8071
|
-
source =
|
|
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
|
|
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 (!
|
|
8747
|
+
if (!existsSync18(pkgPath)) return /* @__PURE__ */ new Map();
|
|
8395
8748
|
try {
|
|
8396
|
-
const pkg = JSON.parse(
|
|
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
|
|
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 =
|
|
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 (
|
|
9659
|
+
if (existsSync19(paths.plansDir)) {
|
|
9307
9660
|
const planFiles = discoverMarkdownFiles(paths.plansDir);
|
|
9308
9661
|
files.push(...planFiles);
|
|
9309
9662
|
}
|
|
9310
|
-
if (
|
|
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 (!
|
|
9354
|
-
const content =
|
|
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 (
|
|
9827
|
+
if (existsSync19(paths.plansDir)) {
|
|
9475
9828
|
files.push(...discoverMarkdownFiles(paths.plansDir));
|
|
9476
9829
|
}
|
|
9477
|
-
if (
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 (!
|
|
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
|
|
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 (
|
|
11771
|
-
const fileContent =
|
|
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 (!
|
|
12549
|
+
if (!existsSync22(absPath)) {
|
|
12197
12550
|
return text17(`File not found: ${file}`);
|
|
12198
12551
|
}
|
|
12199
|
-
const source =
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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");
|