@massu/core 0.1.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/commands/_shared-preamble.md +76 -0
  2. package/commands/massu-audit-deps.md +211 -0
  3. package/commands/massu-changelog.md +174 -0
  4. package/commands/massu-cleanup.md +315 -0
  5. package/commands/massu-commit.md +481 -0
  6. package/commands/massu-create-plan.md +752 -0
  7. package/commands/massu-dead-code.md +131 -0
  8. package/commands/massu-debug.md +484 -0
  9. package/commands/massu-deploy.md +91 -0
  10. package/commands/massu-deps.md +374 -0
  11. package/commands/massu-doc-gen.md +279 -0
  12. package/commands/massu-docs.md +364 -0
  13. package/commands/massu-estimate.md +313 -0
  14. package/commands/massu-golden-path.md +973 -0
  15. package/commands/massu-guide.md +167 -0
  16. package/commands/massu-hotfix.md +480 -0
  17. package/commands/massu-loop-playwright.md +837 -0
  18. package/commands/massu-loop.md +775 -0
  19. package/commands/massu-new-feature.md +511 -0
  20. package/commands/massu-parity.md +214 -0
  21. package/commands/massu-plan.md +456 -0
  22. package/commands/massu-push-light.md +207 -0
  23. package/commands/massu-push.md +434 -0
  24. package/commands/massu-refactor.md +410 -0
  25. package/commands/massu-release.md +363 -0
  26. package/commands/massu-review.md +238 -0
  27. package/commands/massu-simplify.md +281 -0
  28. package/commands/massu-status.md +278 -0
  29. package/commands/massu-tdd.md +201 -0
  30. package/commands/massu-test.md +516 -0
  31. package/commands/massu-verify-playwright.md +281 -0
  32. package/commands/massu-verify.md +667 -0
  33. package/dist/cli.js +7772 -3140
  34. package/dist/hooks/cost-tracker.js +103 -40
  35. package/dist/hooks/post-edit-context.js +74 -8
  36. package/dist/hooks/post-tool-use.js +268 -106
  37. package/dist/hooks/pre-compact.js +167 -43
  38. package/dist/hooks/pre-delete-check.js +159 -42
  39. package/dist/hooks/quality-event.js +103 -40
  40. package/dist/hooks/security-gate.js +29 -0
  41. package/dist/hooks/session-end.js +143 -84
  42. package/dist/hooks/session-start.js +186 -49
  43. package/dist/hooks/user-prompt.js +189 -43
  44. package/package.json +10 -15
  45. package/src/adr-generator.ts +9 -2
  46. package/src/analytics.ts +9 -3
  47. package/src/audit-trail.ts +10 -3
  48. package/src/backfill-sessions.ts +5 -4
  49. package/src/cli.ts +6 -0
  50. package/src/cloud-sync.ts +14 -18
  51. package/src/commands/doctor.ts +193 -6
  52. package/src/commands/init.ts +230 -5
  53. package/src/commands/install-commands.ts +137 -0
  54. package/src/config.ts +68 -2
  55. package/src/cost-tracker.ts +11 -6
  56. package/src/db.ts +115 -2
  57. package/src/dependency-scorer.ts +9 -2
  58. package/src/docs-tools.ts +21 -16
  59. package/src/hooks/post-edit-context.ts +4 -4
  60. package/src/hooks/post-tool-use.ts +130 -0
  61. package/src/hooks/pre-compact.ts +23 -1
  62. package/src/hooks/pre-delete-check.ts +92 -4
  63. package/src/hooks/security-gate.ts +32 -0
  64. package/src/hooks/session-end.ts +3 -3
  65. package/src/hooks/session-start.ts +99 -6
  66. package/src/hooks/user-prompt.ts +46 -1
  67. package/src/import-resolver.ts +2 -1
  68. package/src/knowledge-db.ts +169 -0
  69. package/src/knowledge-indexer.ts +704 -0
  70. package/src/knowledge-tools.ts +1413 -0
  71. package/src/license.ts +482 -0
  72. package/src/memory-db.ts +1364 -23
  73. package/src/memory-tools.ts +14 -15
  74. package/src/observability-tools.ts +13 -2
  75. package/src/observation-extractor.ts +11 -4
  76. package/src/page-deps.ts +3 -2
  77. package/src/prompt-analyzer.ts +9 -2
  78. package/src/python/coupling-detector.ts +124 -0
  79. package/src/python/domain-enforcer.ts +83 -0
  80. package/src/python/impact-analyzer.ts +95 -0
  81. package/src/python/import-parser.ts +244 -0
  82. package/src/python/import-resolver.ts +135 -0
  83. package/src/python/migration-indexer.ts +115 -0
  84. package/src/python/migration-parser.ts +332 -0
  85. package/src/python/model-indexer.ts +70 -0
  86. package/src/python/model-parser.ts +279 -0
  87. package/src/python/route-indexer.ts +58 -0
  88. package/src/python/route-parser.ts +317 -0
  89. package/src/python-tools.ts +629 -0
  90. package/src/regression-detector.ts +9 -3
  91. package/src/security-scorer.ts +9 -2
  92. package/src/sentinel-db.ts +45 -89
  93. package/src/sentinel-tools.ts +8 -11
  94. package/src/server.ts +29 -7
  95. package/src/session-archiver.ts +4 -5
  96. package/src/team-knowledge.ts +9 -2
  97. package/src/tools.ts +1032 -44
  98. package/src/validate-features-runner.ts +0 -1
  99. package/src/validation-engine.ts +9 -2
  100. package/README.md +0 -40
  101. package/dist/server.js +0 -7008
  102. package/src/__tests__/adr-generator.test.ts +0 -260
  103. package/src/__tests__/analytics.test.ts +0 -282
  104. package/src/__tests__/audit-trail.test.ts +0 -382
  105. package/src/__tests__/backfill-sessions.test.ts +0 -690
  106. package/src/__tests__/cli.test.ts +0 -290
  107. package/src/__tests__/cloud-sync.test.ts +0 -261
  108. package/src/__tests__/config-sections.test.ts +0 -359
  109. package/src/__tests__/config.test.ts +0 -732
  110. package/src/__tests__/cost-tracker.test.ts +0 -348
  111. package/src/__tests__/db.test.ts +0 -177
  112. package/src/__tests__/dependency-scorer.test.ts +0 -325
  113. package/src/__tests__/docs-integration.test.ts +0 -178
  114. package/src/__tests__/docs-tools.test.ts +0 -199
  115. package/src/__tests__/domains.test.ts +0 -236
  116. package/src/__tests__/hooks.test.ts +0 -221
  117. package/src/__tests__/import-resolver.test.ts +0 -95
  118. package/src/__tests__/integration/path-traversal.test.ts +0 -134
  119. package/src/__tests__/integration/pricing-consistency.test.ts +0 -88
  120. package/src/__tests__/integration/tool-registration.test.ts +0 -146
  121. package/src/__tests__/memory-db.test.ts +0 -404
  122. package/src/__tests__/memory-enhancements.test.ts +0 -316
  123. package/src/__tests__/memory-tools.test.ts +0 -199
  124. package/src/__tests__/middleware-tree.test.ts +0 -177
  125. package/src/__tests__/observability-tools.test.ts +0 -595
  126. package/src/__tests__/observability.test.ts +0 -437
  127. package/src/__tests__/observation-extractor.test.ts +0 -167
  128. package/src/__tests__/page-deps.test.ts +0 -60
  129. package/src/__tests__/prompt-analyzer.test.ts +0 -298
  130. package/src/__tests__/regression-detector.test.ts +0 -295
  131. package/src/__tests__/rules.test.ts +0 -87
  132. package/src/__tests__/schema-mapper.test.ts +0 -29
  133. package/src/__tests__/security-scorer.test.ts +0 -238
  134. package/src/__tests__/security-utils.test.ts +0 -175
  135. package/src/__tests__/sentinel-db.test.ts +0 -491
  136. package/src/__tests__/sentinel-scanner.test.ts +0 -750
  137. package/src/__tests__/sentinel-tools.test.ts +0 -324
  138. package/src/__tests__/sentinel-types.test.ts +0 -750
  139. package/src/__tests__/server.test.ts +0 -452
  140. package/src/__tests__/session-archiver.test.ts +0 -524
  141. package/src/__tests__/session-state-generator.test.ts +0 -900
  142. package/src/__tests__/team-knowledge.test.ts +0 -327
  143. package/src/__tests__/tools.test.ts +0 -340
  144. package/src/__tests__/transcript-parser.test.ts +0 -195
  145. package/src/__tests__/trpc-index.test.ts +0 -25
  146. package/src/__tests__/validate-features-runner.test.ts +0 -517
  147. package/src/__tests__/validation-engine.test.ts +0 -300
  148. package/src/core-tools.ts +0 -685
  149. package/src/memory-queries.ts +0 -804
  150. package/src/memory-schema.ts +0 -546
  151. package/src/tool-helpers.ts +0 -41
@@ -3,12 +3,13 @@ import{createRequire as __cr}from"module";const require=__cr(import.meta.url);
3
3
 
4
4
  // src/memory-db.ts
5
5
  import Database from "better-sqlite3";
6
- import { dirname as dirname2 } from "path";
6
+ import { dirname as dirname2, basename } from "path";
7
7
  import { existsSync as existsSync2, mkdirSync } from "fs";
8
8
 
9
9
  // src/config.ts
10
10
  import { resolve, dirname } from "path";
11
11
  import { existsSync, readFileSync } from "fs";
12
+ import { homedir } from "os";
12
13
  import { parse as parseYaml } from "yaml";
13
14
  import { z } from "zod";
14
15
  var DomainConfigSchema = z.object({
@@ -140,6 +141,49 @@ var CloudConfigSchema = z.object({
140
141
  audit: z.boolean().default(true)
141
142
  }).default({ memory: true, analytics: true, audit: true })
142
143
  }).optional();
144
+ var ConventionsConfigSchema = z.object({
145
+ claudeDirName: z.string().default(".claude").refine(
146
+ (s) => !s.includes("..") && !s.startsWith("/"),
147
+ { message: 'claudeDirName must not contain ".." or start with "/"' }
148
+ ),
149
+ sessionStatePath: z.string().default(".claude/session-state/CURRENT.md").refine(
150
+ (s) => !s.includes("..") && !s.startsWith("/"),
151
+ { message: 'sessionStatePath must not contain ".." or start with "/"' }
152
+ ),
153
+ sessionArchivePath: z.string().default(".claude/session-state/archive").refine(
154
+ (s) => !s.includes("..") && !s.startsWith("/"),
155
+ { message: 'sessionArchivePath must not contain ".." or start with "/"' }
156
+ ),
157
+ knowledgeCategories: z.array(z.string()).default([
158
+ "patterns",
159
+ "commands",
160
+ "incidents",
161
+ "reference",
162
+ "protocols",
163
+ "checklists",
164
+ "playbooks",
165
+ "critical",
166
+ "scripts",
167
+ "status",
168
+ "templates",
169
+ "loop-state",
170
+ "session-state",
171
+ "agents"
172
+ ]),
173
+ knowledgeSourceFiles: z.array(z.string()).default(["CLAUDE.md", "MEMORY.md", "corrections.md"]),
174
+ excludePatterns: z.array(z.string()).default(["/ARCHIVE/", "/SESSION-HISTORY/"])
175
+ }).optional();
176
+ var PythonDomainConfigSchema = z.object({
177
+ name: z.string(),
178
+ packages: z.array(z.string()),
179
+ allowed_imports_from: z.array(z.string()).default([])
180
+ });
181
+ var PythonConfigSchema = z.object({
182
+ root: z.string(),
183
+ alembic_dir: z.string().optional(),
184
+ domains: z.array(PythonDomainConfigSchema).default([]),
185
+ exclude_dirs: z.array(z.string()).default(["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"])
186
+ }).optional();
143
187
  var PathsConfigSchema = z.object({
144
188
  source: z.string().default("src"),
145
189
  aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
@@ -174,7 +218,9 @@ var RawConfigSchema = z.object({
174
218
  security: SecurityConfigSchema,
175
219
  team: TeamConfigSchema,
176
220
  regression: RegressionConfigSchema,
177
- cloud: CloudConfigSchema
221
+ cloud: CloudConfigSchema,
222
+ conventions: ConventionsConfigSchema,
223
+ python: PythonConfigSchema
178
224
  }).passthrough();
179
225
  var _config = null;
180
226
  var _projectRoot = null;
@@ -238,13 +284,24 @@ function getConfig() {
238
284
  security: parsed.security,
239
285
  team: parsed.team,
240
286
  regression: parsed.regression,
241
- cloud: parsed.cloud
287
+ cloud: parsed.cloud,
288
+ conventions: parsed.conventions,
289
+ python: parsed.python
242
290
  };
291
+ if (!_config.cloud?.apiKey && process.env.MASSU_API_KEY) {
292
+ _config.cloud = {
293
+ enabled: true,
294
+ sync: { memory: true, analytics: true, audit: true },
295
+ ..._config.cloud,
296
+ apiKey: process.env.MASSU_API_KEY
297
+ };
298
+ }
243
299
  return _config;
244
300
  }
245
301
  function getResolvedPaths() {
246
302
  const config = getConfig();
247
303
  const root = getProjectRoot();
304
+ const claudeDirName = config.conventions?.claudeDirName ?? ".claude";
248
305
  return {
249
306
  codegraphDbPath: resolve(root, ".codegraph/codegraph.db"),
250
307
  dataDbPath: resolve(root, ".massu/data.db"),
@@ -260,22 +317,43 @@ function getResolvedPaths() {
260
317
  ),
261
318
  extensions: [".ts", ".tsx", ".js", ".jsx"],
262
319
  indexFiles: ["index.ts", "index.tsx", "index.js", "index.jsx"],
263
- patternsDir: resolve(root, ".claude/patterns"),
264
- claudeMdPath: resolve(root, ".claude/CLAUDE.md"),
320
+ patternsDir: resolve(root, claudeDirName, "patterns"),
321
+ claudeMdPath: resolve(root, claudeDirName, "CLAUDE.md"),
265
322
  docsMapPath: resolve(root, ".massu/docs-map.json"),
266
323
  helpSitePath: resolve(root, "../" + config.project.name + "-help"),
267
- memoryDbPath: resolve(root, ".massu/memory.db")
324
+ memoryDbPath: resolve(root, ".massu/memory.db"),
325
+ knowledgeDbPath: resolve(root, ".massu/knowledge.db"),
326
+ plansDir: resolve(root, "docs/plans"),
327
+ docsDir: resolve(root, "docs"),
328
+ claudeDir: resolve(root, claudeDirName),
329
+ memoryDir: resolve(homedir(), claudeDirName, "projects", root.replace(/\//g, "-"), "memory"),
330
+ sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
331
+ sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
332
+ mcpJsonPath: resolve(root, ".mcp.json"),
333
+ settingsLocalPath: resolve(root, claudeDirName, "settings.local.json")
268
334
  };
269
335
  }
270
336
 
271
- // src/memory-schema.ts
337
+ // src/memory-db.ts
338
+ function getMemoryDb() {
339
+ const dbPath = getResolvedPaths().memoryDbPath;
340
+ const dir = dirname2(dbPath);
341
+ if (!existsSync2(dir)) {
342
+ mkdirSync(dir, { recursive: true });
343
+ }
344
+ const db = new Database(dbPath);
345
+ db.pragma("journal_mode = WAL");
346
+ db.pragma("foreign_keys = ON");
347
+ initMemorySchema(db);
348
+ return db;
349
+ }
272
350
  function initMemorySchema(db) {
273
351
  db.exec(`
274
352
  -- Sessions table (linked to Claude Code session IDs)
275
353
  CREATE TABLE IF NOT EXISTS sessions (
276
354
  id INTEGER PRIMARY KEY AUTOINCREMENT,
277
355
  session_id TEXT UNIQUE NOT NULL,
278
- project TEXT NOT NULL DEFAULT 'unknown',
356
+ project TEXT NOT NULL DEFAULT 'my-project',
279
357
  git_branch TEXT,
280
358
  started_at TEXT NOT NULL,
281
359
  started_at_epoch INTEGER NOT NULL,
@@ -330,9 +408,7 @@ function initMemorySchema(db) {
330
408
  content_rowid='id'
331
409
  );
332
410
  `);
333
- } catch (e) {
334
- process.stderr.write(`FTS5 setup warning: ${e instanceof Error ? e.message : String(e)}
335
- `);
411
+ } catch (_e) {
336
412
  }
337
413
  db.exec(`
338
414
  CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
@@ -392,9 +468,7 @@ function initMemorySchema(db) {
392
468
  content_rowid='id'
393
469
  );
394
470
  `);
395
- } catch (e) {
396
- process.stderr.write(`FTS5 setup warning: ${e instanceof Error ? e.message : String(e)}
397
- `);
471
+ } catch (_e) {
398
472
  }
399
473
  db.exec(`
400
474
  CREATE TRIGGER IF NOT EXISTS prompts_ai AFTER INSERT ON user_prompts BEGIN
@@ -464,9 +538,7 @@ function initMemorySchema(db) {
464
538
  content_rowid=id
465
539
  );
466
540
  `);
467
- } catch (e) {
468
- process.stderr.write(`FTS5 setup warning: ${e instanceof Error ? e.message : String(e)}
469
- `);
541
+ } catch (_e) {
470
542
  }
471
543
  db.exec(`
472
544
  CREATE TRIGGER IF NOT EXISTS ct_fts_insert AFTER INSERT ON conversation_turns BEGIN
@@ -490,7 +562,7 @@ function initMemorySchema(db) {
490
562
  CREATE TABLE IF NOT EXISTS session_quality_scores (
491
563
  id INTEGER PRIMARY KEY AUTOINCREMENT,
492
564
  session_id TEXT NOT NULL UNIQUE,
493
- project TEXT NOT NULL DEFAULT 'unknown',
565
+ project TEXT NOT NULL DEFAULT 'my-project',
494
566
  score INTEGER NOT NULL DEFAULT 100,
495
567
  security_score INTEGER NOT NULL DEFAULT 100,
496
568
  architecture_score INTEGER NOT NULL DEFAULT 100,
@@ -513,7 +585,7 @@ function initMemorySchema(db) {
513
585
  CREATE TABLE IF NOT EXISTS session_costs (
514
586
  id INTEGER PRIMARY KEY AUTOINCREMENT,
515
587
  session_id TEXT NOT NULL UNIQUE,
516
- project TEXT NOT NULL DEFAULT 'unknown',
588
+ project TEXT NOT NULL DEFAULT 'my-project',
517
589
  input_tokens INTEGER NOT NULL DEFAULT 0,
518
590
  output_tokens INTEGER NOT NULL DEFAULT 0,
519
591
  cache_read_tokens INTEGER NOT NULL DEFAULT 0,
@@ -740,10 +812,40 @@ function initMemorySchema(db) {
740
812
  );
741
813
  CREATE INDEX IF NOT EXISTS idx_pending_sync_created ON pending_sync(created_at ASC);
742
814
  `);
815
+ db.exec(`
816
+ CREATE TABLE IF NOT EXISTS license_cache (
817
+ api_key_hash TEXT PRIMARY KEY,
818
+ tier TEXT NOT NULL,
819
+ valid_until TEXT NOT NULL,
820
+ last_validated TEXT NOT NULL,
821
+ features TEXT DEFAULT '[]'
822
+ );
823
+ `);
824
+ }
825
+ function assignImportance(type, vrResult) {
826
+ switch (type) {
827
+ case "decision":
828
+ case "failed_attempt":
829
+ return 5;
830
+ case "cr_violation":
831
+ case "incident_near_miss":
832
+ return 4;
833
+ case "vr_check":
834
+ return vrResult === "PASS" ? 2 : 4;
835
+ case "pattern_compliance":
836
+ return vrResult === "PASS" ? 2 : 4;
837
+ case "feature":
838
+ case "bugfix":
839
+ return 3;
840
+ case "refactor":
841
+ return 2;
842
+ case "file_change":
843
+ case "discovery":
844
+ return 1;
845
+ default:
846
+ return 3;
847
+ }
743
848
  }
744
-
745
- // src/memory-queries.ts
746
- import { basename } from "path";
747
849
  function autoDetectTaskId(planFile) {
748
850
  if (!planFile) return null;
749
851
  const base = basename(planFile);
@@ -757,6 +859,29 @@ function createSession(db, sessionId, opts) {
757
859
  VALUES (?, ?, ?, ?, ?, ?)
758
860
  `).run(sessionId, opts?.branch ?? null, opts?.planFile ?? null, taskId, now.toISOString(), Math.floor(now.getTime() / 1e3));
759
861
  }
862
+ function addObservation(db, sessionId, type, title, detail, opts) {
863
+ const now = /* @__PURE__ */ new Date();
864
+ const importance = opts?.importance ?? assignImportance(type, opts?.evidence?.includes("PASS") ? "PASS" : void 0);
865
+ const result = db.prepare(`
866
+ 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)
867
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
868
+ `).run(
869
+ sessionId,
870
+ type,
871
+ title,
872
+ detail,
873
+ JSON.stringify(opts?.filesInvolved ?? []),
874
+ opts?.planItem ?? null,
875
+ opts?.crRule ?? null,
876
+ opts?.vrType ?? null,
877
+ opts?.evidence ?? null,
878
+ importance,
879
+ opts?.originalTokens ?? 0,
880
+ now.toISOString(),
881
+ Math.floor(now.getTime() / 1e3)
882
+ );
883
+ return Number(result.lastInsertRowid);
884
+ }
760
885
  function addSummary(db, sessionId, summary) {
761
886
  const now = /* @__PURE__ */ new Date();
762
887
  db.prepare(`
@@ -779,24 +904,6 @@ function addSummary(db, sessionId, summary) {
779
904
  );
780
905
  }
781
906
 
782
- // src/memory-db.ts
783
- var initializedPaths = /* @__PURE__ */ new Set();
784
- function getMemoryDb() {
785
- const dbPath = getResolvedPaths().memoryDbPath;
786
- const dir = dirname2(dbPath);
787
- if (!existsSync2(dir)) {
788
- mkdirSync(dir, { recursive: true });
789
- }
790
- const db = new Database(dbPath);
791
- db.pragma("journal_mode = WAL");
792
- db.pragma("foreign_keys = ON");
793
- if (!initializedPaths.has(dbPath)) {
794
- initMemorySchema(db);
795
- initializedPaths.add(dbPath);
796
- }
797
- return db;
798
- }
799
-
800
907
  // src/audit-trail.ts
801
908
  function logAuditEntry(db, entry) {
802
909
  db.prepare(`
@@ -833,6 +940,23 @@ async function main() {
833
940
  ).all(session_id);
834
941
  const summary = buildSnapshotSummary(observations, prompts);
835
942
  addSummary(db, session_id, summary);
943
+ try {
944
+ const knowledgeObs = observations.filter(
945
+ (o) => o.title?.includes("knowledge") || o.title?.includes("Knowledge") || o.detail?.includes("knowledge")
946
+ );
947
+ if (knowledgeObs.length > 0) {
948
+ const knowledgeContext = knowledgeObs.map((o) => `[${o.type}] ${o.title}`).join("; ");
949
+ addObservation(
950
+ db,
951
+ session_id,
952
+ "discovery",
953
+ "Knowledge context preserved before compaction",
954
+ knowledgeContext,
955
+ { importance: 4 }
956
+ );
957
+ }
958
+ } catch (_knowledgeErr) {
959
+ }
836
960
  try {
837
961
  logAuditEntry(db, {
838
962
  sessionId: session_id,
@@ -894,14 +1018,14 @@ function safeParseJson(json, fallback) {
894
1018
  }
895
1019
  }
896
1020
  function readStdin() {
897
- return new Promise((resolve2) => {
1021
+ return new Promise((resolve3) => {
898
1022
  let data = "";
899
1023
  process.stdin.setEncoding("utf-8");
900
1024
  process.stdin.on("data", (chunk) => {
901
1025
  data += chunk;
902
1026
  });
903
- process.stdin.on("end", () => resolve2(data));
904
- setTimeout(() => resolve2(data), 3e3);
1027
+ process.stdin.on("end", () => resolve3(data));
1028
+ setTimeout(() => resolve3(data), 3e3);
905
1029
  });
906
1030
  }
907
1031
  main();
@@ -2,12 +2,13 @@
2
2
  import{createRequire as __cr}from"module";const require=__cr(import.meta.url);
3
3
 
4
4
  // src/hooks/pre-delete-check.ts
5
- import Database from "better-sqlite3";
5
+ import Database2 from "better-sqlite3";
6
6
  import { existsSync as existsSync2 } from "fs";
7
7
 
8
8
  // src/config.ts
9
9
  import { resolve, dirname } from "path";
10
10
  import { existsSync, readFileSync } from "fs";
11
+ import { homedir } from "os";
11
12
  import { parse as parseYaml } from "yaml";
12
13
  import { z } from "zod";
13
14
  var DomainConfigSchema = z.object({
@@ -139,6 +140,49 @@ var CloudConfigSchema = z.object({
139
140
  audit: z.boolean().default(true)
140
141
  }).default({ memory: true, analytics: true, audit: true })
141
142
  }).optional();
143
+ var ConventionsConfigSchema = z.object({
144
+ claudeDirName: z.string().default(".claude").refine(
145
+ (s) => !s.includes("..") && !s.startsWith("/"),
146
+ { message: 'claudeDirName must not contain ".." or start with "/"' }
147
+ ),
148
+ sessionStatePath: z.string().default(".claude/session-state/CURRENT.md").refine(
149
+ (s) => !s.includes("..") && !s.startsWith("/"),
150
+ { message: 'sessionStatePath must not contain ".." or start with "/"' }
151
+ ),
152
+ sessionArchivePath: z.string().default(".claude/session-state/archive").refine(
153
+ (s) => !s.includes("..") && !s.startsWith("/"),
154
+ { message: 'sessionArchivePath must not contain ".." or start with "/"' }
155
+ ),
156
+ knowledgeCategories: z.array(z.string()).default([
157
+ "patterns",
158
+ "commands",
159
+ "incidents",
160
+ "reference",
161
+ "protocols",
162
+ "checklists",
163
+ "playbooks",
164
+ "critical",
165
+ "scripts",
166
+ "status",
167
+ "templates",
168
+ "loop-state",
169
+ "session-state",
170
+ "agents"
171
+ ]),
172
+ knowledgeSourceFiles: z.array(z.string()).default(["CLAUDE.md", "MEMORY.md", "corrections.md"]),
173
+ excludePatterns: z.array(z.string()).default(["/ARCHIVE/", "/SESSION-HISTORY/"])
174
+ }).optional();
175
+ var PythonDomainConfigSchema = z.object({
176
+ name: z.string(),
177
+ packages: z.array(z.string()),
178
+ allowed_imports_from: z.array(z.string()).default([])
179
+ });
180
+ var PythonConfigSchema = z.object({
181
+ root: z.string(),
182
+ alembic_dir: z.string().optional(),
183
+ domains: z.array(PythonDomainConfigSchema).default([]),
184
+ exclude_dirs: z.array(z.string()).default(["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"])
185
+ }).optional();
142
186
  var PathsConfigSchema = z.object({
143
187
  source: z.string().default("src"),
144
188
  aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
@@ -173,7 +217,9 @@ var RawConfigSchema = z.object({
173
217
  security: SecurityConfigSchema,
174
218
  team: TeamConfigSchema,
175
219
  regression: RegressionConfigSchema,
176
- cloud: CloudConfigSchema
220
+ cloud: CloudConfigSchema,
221
+ conventions: ConventionsConfigSchema,
222
+ python: PythonConfigSchema
177
223
  }).passthrough();
178
224
  var _config = null;
179
225
  var _projectRoot = null;
@@ -237,13 +283,24 @@ function getConfig() {
237
283
  security: parsed.security,
238
284
  team: parsed.team,
239
285
  regression: parsed.regression,
240
- cloud: parsed.cloud
286
+ cloud: parsed.cloud,
287
+ conventions: parsed.conventions,
288
+ python: parsed.python
241
289
  };
290
+ if (!_config.cloud?.apiKey && process.env.MASSU_API_KEY) {
291
+ _config.cloud = {
292
+ enabled: true,
293
+ sync: { memory: true, analytics: true, audit: true },
294
+ ..._config.cloud,
295
+ apiKey: process.env.MASSU_API_KEY
296
+ };
297
+ }
242
298
  return _config;
243
299
  }
244
300
  function getResolvedPaths() {
245
301
  const config = getConfig();
246
302
  const root = getProjectRoot();
303
+ const claudeDirName = config.conventions?.claudeDirName ?? ".claude";
247
304
  return {
248
305
  codegraphDbPath: resolve(root, ".codegraph/codegraph.db"),
249
306
  dataDbPath: resolve(root, ".massu/data.db"),
@@ -259,14 +316,26 @@ function getResolvedPaths() {
259
316
  ),
260
317
  extensions: [".ts", ".tsx", ".js", ".jsx"],
261
318
  indexFiles: ["index.ts", "index.tsx", "index.js", "index.jsx"],
262
- patternsDir: resolve(root, ".claude/patterns"),
263
- claudeMdPath: resolve(root, ".claude/CLAUDE.md"),
319
+ patternsDir: resolve(root, claudeDirName, "patterns"),
320
+ claudeMdPath: resolve(root, claudeDirName, "CLAUDE.md"),
264
321
  docsMapPath: resolve(root, ".massu/docs-map.json"),
265
322
  helpSitePath: resolve(root, "../" + config.project.name + "-help"),
266
- memoryDbPath: resolve(root, ".massu/memory.db")
323
+ memoryDbPath: resolve(root, ".massu/memory.db"),
324
+ knowledgeDbPath: resolve(root, ".massu/knowledge.db"),
325
+ plansDir: resolve(root, "docs/plans"),
326
+ docsDir: resolve(root, "docs"),
327
+ claudeDir: resolve(root, claudeDirName),
328
+ memoryDir: resolve(homedir(), claudeDirName, "projects", root.replace(/\//g, "-"), "memory"),
329
+ sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
330
+ sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
331
+ mcpJsonPath: resolve(root, ".mcp.json"),
332
+ settingsLocalPath: resolve(root, claudeDirName, "settings.local.json")
267
333
  };
268
334
  }
269
335
 
336
+ // src/memory-db.ts
337
+ import Database from "better-sqlite3";
338
+
270
339
  // src/sentinel-db.ts
271
340
  var PROJECT_ROOT = getProjectRoot();
272
341
  function parsePortalScope(raw) {
@@ -294,45 +363,32 @@ function toFeature(row) {
294
363
  removed_reason: row.removed_reason || null
295
364
  };
296
365
  }
366
+ function getFeatureById(db, id) {
367
+ const row = db.prepare("SELECT * FROM massu_sentinel WHERE id = ?").get(id);
368
+ return row ? toFeature(row) : null;
369
+ }
297
370
  function getFeatureImpact(db, filePaths) {
298
- if (filePaths.length === 0) {
299
- return { files_analyzed: [], orphaned: [], degraded: [], unaffected: [], blocked: false, block_reason: null };
300
- }
301
371
  const fileSet = new Set(filePaths);
302
- const placeholders = filePaths.map(() => "?").join(",");
303
- const featureLinks = db.prepare(
304
- `SELECT DISTINCT feature_id FROM massu_sentinel_components WHERE component_file IN (${placeholders})`
305
- ).all(...filePaths);
306
- const affectedFeatureIds = featureLinks.map((l) => l.feature_id);
307
- if (affectedFeatureIds.length === 0) {
308
- return { files_analyzed: filePaths, orphaned: [], degraded: [], unaffected: [], blocked: false, block_reason: null };
309
- }
310
- const featurePlaceholders = affectedFeatureIds.map(() => "?").join(",");
311
- const featureRows = db.prepare(
312
- `SELECT * FROM massu_sentinel WHERE id IN (${featurePlaceholders}) AND status = 'active'`
313
- ).all(...affectedFeatureIds);
314
- const featuresById = new Map(featureRows.map((r) => [r.id, toFeature(r)]));
315
- const allComponents = db.prepare(
316
- `SELECT feature_id, component_file, is_primary FROM massu_sentinel_components WHERE feature_id IN (${featurePlaceholders})`
317
- ).all(...affectedFeatureIds);
318
- const componentsByFeature = /* @__PURE__ */ new Map();
319
- for (const comp of allComponents) {
320
- let list = componentsByFeature.get(comp.feature_id);
321
- if (!list) {
322
- list = [];
323
- componentsByFeature.set(comp.feature_id, list);
372
+ const affectedFeatureIds = /* @__PURE__ */ new Set();
373
+ for (const filePath of filePaths) {
374
+ const links = db.prepare(
375
+ "SELECT feature_id FROM massu_sentinel_components WHERE component_file = ?"
376
+ ).all(filePath);
377
+ for (const link of links) {
378
+ affectedFeatureIds.add(link.feature_id);
324
379
  }
325
- list.push(comp);
326
380
  }
327
381
  const orphaned = [];
328
382
  const degraded = [];
329
383
  const unaffected = [];
330
384
  for (const featureId of affectedFeatureIds) {
331
- const feature = featuresById.get(featureId);
332
- if (!feature) continue;
333
- const comps = componentsByFeature.get(featureId) ?? [];
334
- const affected = comps.filter((c) => fileSet.has(c.component_file));
335
- const remaining = comps.filter((c) => !fileSet.has(c.component_file));
385
+ const feature = getFeatureById(db, featureId);
386
+ if (!feature || feature.status !== "active") continue;
387
+ const allComponents = db.prepare(
388
+ "SELECT component_file, is_primary FROM massu_sentinel_components WHERE feature_id = ?"
389
+ ).all(featureId);
390
+ const affected = allComponents.filter((c) => fileSet.has(c.component_file));
391
+ const remaining = allComponents.filter((c) => !fileSet.has(c.component_file));
336
392
  const primaryAffected = affected.some((c) => c.is_primary);
337
393
  const item = {
338
394
  feature,
@@ -364,17 +420,46 @@ function getFeatureImpact(db, filePaths) {
364
420
 
365
421
  // src/hooks/pre-delete-check.ts
366
422
  var PROJECT_ROOT2 = getProjectRoot();
423
+ var KNOWLEDGE_PROTECTED_FILES = [
424
+ "knowledge-db.ts",
425
+ "knowledge-indexer.ts",
426
+ "knowledge-tools.ts"
427
+ ];
367
428
  function getDataDb() {
368
429
  const dbPath = getResolvedPaths().dataDbPath;
369
430
  if (!existsSync2(dbPath)) return null;
370
431
  try {
371
- const db = new Database(dbPath, { readonly: true });
432
+ const db = new Database2(dbPath, { readonly: true });
372
433
  db.pragma("journal_mode = WAL");
373
434
  return db;
374
435
  } catch {
375
436
  return null;
376
437
  }
377
438
  }
439
+ function checkKnowledgeFileProtection(input) {
440
+ const candidateFiles = [];
441
+ if (input.tool_name === "Bash" && input.tool_input.command) {
442
+ const cmd = input.tool_input.command;
443
+ const rmMatch = cmd.match(/(?:rm|git\s+rm)\s+(?:-[rf]*\s+)*(.+)/);
444
+ if (rmMatch) {
445
+ const parts = rmMatch[1].split(/\s+/).filter((p) => !p.startsWith("-"));
446
+ candidateFiles.push(...parts);
447
+ }
448
+ }
449
+ if (input.tool_name === "Write" && input.tool_input.file_path) {
450
+ const content = input.tool_input.content || "";
451
+ if (content.trim().length === 0) {
452
+ candidateFiles.push(input.tool_input.file_path);
453
+ }
454
+ }
455
+ for (const f of candidateFiles) {
456
+ const basename = f.split("/").pop() ?? f;
457
+ if (KNOWLEDGE_PROTECTED_FILES.includes(basename)) {
458
+ return `KNOWLEDGE SYSTEM PROTECTION: "${basename}" is a core knowledge system file. Deleting it will break knowledge indexing and memory retrieval. Create a replacement before removing.`;
459
+ }
460
+ }
461
+ return null;
462
+ }
378
463
  function extractDeletedFiles(input) {
379
464
  const files = [];
380
465
  if (input.tool_name === "Bash" && input.tool_input.command) {
@@ -384,7 +469,7 @@ function extractDeletedFiles(input) {
384
469
  const paths = rmMatch[1].split(/\s+/).filter((p) => !p.startsWith("-"));
385
470
  for (const p of paths) {
386
471
  const relPath = p.startsWith("src/") ? p : p.replace(PROJECT_ROOT2 + "/", "");
387
- if (relPath.startsWith("src/")) {
472
+ if (relPath.startsWith("src/") || relPath.endsWith(".py")) {
388
473
  files.push(relPath);
389
474
  }
390
475
  }
@@ -394,7 +479,7 @@ function extractDeletedFiles(input) {
394
479
  const content = input.tool_input.content || "";
395
480
  if (content.trim().length === 0) {
396
481
  const relPath = input.tool_input.file_path.replace(PROJECT_ROOT2 + "/", "");
397
- if (relPath.startsWith("src/")) {
482
+ if (relPath.startsWith("src/") || relPath.endsWith(".py")) {
398
483
  files.push(relPath);
399
484
  }
400
485
  }
@@ -405,6 +490,12 @@ async function main() {
405
490
  try {
406
491
  const input = await readStdin();
407
492
  const hookInput = JSON.parse(input);
493
+ const knowledgeWarning = checkKnowledgeFileProtection(hookInput);
494
+ if (knowledgeWarning) {
495
+ process.stdout.write(JSON.stringify({ message: knowledgeWarning }));
496
+ process.exit(0);
497
+ return;
498
+ }
408
499
  const deletedFiles = extractDeletedFiles(hookInput);
409
500
  if (deletedFiles.length === 0) {
410
501
  process.exit(0);
@@ -415,10 +506,11 @@ async function main() {
415
506
  process.exit(0);
416
507
  return;
417
508
  }
509
+ const SENTINEL_TABLE = "massu_sentinel";
418
510
  try {
419
511
  const tableExists = db.prepare(
420
- "SELECT name FROM sqlite_master WHERE type='table' AND name='massu_sentinel'"
421
- ).get();
512
+ `SELECT name FROM sqlite_master WHERE type='table' AND name=?`
513
+ ).get(SENTINEL_TABLE);
422
514
  if (!tableExists) {
423
515
  process.exit(0);
424
516
  return;
@@ -445,6 +537,31 @@ async function main() {
445
537
  msg.push("Create a migration plan before deleting these files.");
446
538
  process.stdout.write(JSON.stringify({ message: msg.join("\n") }));
447
539
  }
540
+ const pyFiles = deletedFiles.filter((f) => f.endsWith(".py"));
541
+ if (pyFiles.length > 0) {
542
+ try {
543
+ for (const pyFile of pyFiles) {
544
+ const importers = db.prepare(
545
+ "SELECT source_file FROM massu_py_imports WHERE target_file = ?"
546
+ ).all(pyFile);
547
+ const routes = db.prepare(
548
+ "SELECT method, path FROM massu_py_routes WHERE file = ?"
549
+ ).all(pyFile);
550
+ const models = db.prepare(
551
+ "SELECT class_name FROM massu_py_models WHERE file = ?"
552
+ ).all(pyFile);
553
+ if (importers.length > 0 || routes.length > 0 || models.length > 0) {
554
+ const parts = [];
555
+ if (importers.length > 0) parts.push(`imported by ${importers.length} files`);
556
+ if (routes.length > 0) parts.push(`defines ${routes.length} routes`);
557
+ if (models.length > 0) parts.push(`defines ${models.length} models`);
558
+ const msg = `PYTHON IMPACT: "${pyFile}" ${parts.join(", ")}. Check dependents before deleting.`;
559
+ process.stdout.write(JSON.stringify({ message: msg }));
560
+ }
561
+ }
562
+ } catch {
563
+ }
564
+ }
448
565
  } finally {
449
566
  db.close();
450
567
  }