@massu/core 0.1.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/_shared-preamble.md +76 -0
- package/commands/massu-audit-deps.md +211 -0
- package/commands/massu-changelog.md +174 -0
- package/commands/massu-cleanup.md +315 -0
- package/commands/massu-commit.md +481 -0
- package/commands/massu-create-plan.md +752 -0
- package/commands/massu-dead-code.md +131 -0
- package/commands/massu-debug.md +484 -0
- package/commands/massu-deploy.md +91 -0
- package/commands/massu-deps.md +374 -0
- package/commands/massu-doc-gen.md +279 -0
- package/commands/massu-docs.md +364 -0
- package/commands/massu-estimate.md +313 -0
- package/commands/massu-golden-path.md +973 -0
- package/commands/massu-guide.md +167 -0
- package/commands/massu-hotfix.md +480 -0
- package/commands/massu-loop-playwright.md +837 -0
- package/commands/massu-loop.md +775 -0
- package/commands/massu-new-feature.md +511 -0
- package/commands/massu-parity.md +214 -0
- package/commands/massu-plan.md +456 -0
- package/commands/massu-push-light.md +207 -0
- package/commands/massu-push.md +434 -0
- package/commands/massu-refactor.md +410 -0
- package/commands/massu-release.md +363 -0
- package/commands/massu-review.md +238 -0
- package/commands/massu-simplify.md +281 -0
- package/commands/massu-status.md +278 -0
- package/commands/massu-tdd.md +201 -0
- package/commands/massu-test.md +516 -0
- package/commands/massu-verify-playwright.md +281 -0
- package/commands/massu-verify.md +667 -0
- package/dist/cli.js +12521 -0
- package/dist/hooks/cost-tracker.js +80 -5
- package/dist/hooks/post-edit-context.js +72 -6
- package/dist/hooks/post-tool-use.js +234 -57
- package/dist/hooks/pre-compact.js +144 -5
- package/dist/hooks/pre-delete-check.js +141 -11
- package/dist/hooks/quality-event.js +80 -5
- package/dist/hooks/security-gate.js +29 -0
- package/dist/hooks/session-end.js +83 -8
- package/dist/hooks/session-start.js +153 -7
- package/dist/hooks/user-prompt.js +166 -5
- package/package.json +6 -5
- package/src/backfill-sessions.ts +5 -4
- package/src/cli.ts +6 -1
- package/src/commands/doctor.ts +193 -6
- package/src/commands/init.ts +235 -6
- package/src/commands/install-commands.ts +137 -0
- package/src/config.ts +68 -2
- package/src/db.ts +115 -2
- package/src/docs-tools.ts +8 -6
- package/src/hooks/post-edit-context.ts +1 -1
- package/src/hooks/post-tool-use.ts +130 -0
- package/src/hooks/pre-compact.ts +23 -1
- package/src/hooks/pre-delete-check.ts +92 -4
- package/src/hooks/security-gate.ts +32 -0
- package/src/hooks/session-start.ts +97 -4
- package/src/hooks/user-prompt.ts +46 -1
- package/src/import-resolver.ts +2 -1
- package/src/knowledge-db.ts +169 -0
- package/src/knowledge-indexer.ts +704 -0
- package/src/knowledge-tools.ts +1413 -0
- package/src/license.ts +482 -0
- package/src/memory-db.ts +14 -1
- package/src/observation-extractor.ts +11 -4
- package/src/page-deps.ts +3 -2
- package/src/python/coupling-detector.ts +124 -0
- package/src/python/domain-enforcer.ts +83 -0
- package/src/python/impact-analyzer.ts +95 -0
- package/src/python/import-parser.ts +244 -0
- package/src/python/import-resolver.ts +135 -0
- package/src/python/migration-indexer.ts +115 -0
- package/src/python/migration-parser.ts +332 -0
- package/src/python/model-indexer.ts +70 -0
- package/src/python/model-parser.ts +279 -0
- package/src/python/route-indexer.ts +58 -0
- package/src/python/route-parser.ts +317 -0
- package/src/python-tools.ts +629 -0
- package/src/sentinel-db.ts +2 -1
- package/src/server.ts +29 -6
- package/src/session-archiver.ts +4 -5
- package/src/tools.ts +283 -31
- package/README.md +0 -40
|
@@ -9,6 +9,7 @@ import { existsSync as existsSync2, mkdirSync } from "fs";
|
|
|
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,11 +317,20 @@ 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, "
|
|
264
|
-
claudeMdPath: resolve(root, "
|
|
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
|
|
|
@@ -746,6 +812,39 @@ function initMemorySchema(db) {
|
|
|
746
812
|
);
|
|
747
813
|
CREATE INDEX IF NOT EXISTS idx_pending_sync_created ON pending_sync(created_at ASC);
|
|
748
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
|
+
}
|
|
749
848
|
}
|
|
750
849
|
function autoDetectTaskId(planFile) {
|
|
751
850
|
if (!planFile) return null;
|
|
@@ -760,6 +859,29 @@ function createSession(db, sessionId, opts) {
|
|
|
760
859
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
761
860
|
`).run(sessionId, opts?.branch ?? null, opts?.planFile ?? null, taskId, now.toISOString(), Math.floor(now.getTime() / 1e3));
|
|
762
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
|
+
}
|
|
763
885
|
function addSummary(db, sessionId, summary) {
|
|
764
886
|
const now = /* @__PURE__ */ new Date();
|
|
765
887
|
db.prepare(`
|
|
@@ -818,6 +940,23 @@ async function main() {
|
|
|
818
940
|
).all(session_id);
|
|
819
941
|
const summary = buildSnapshotSummary(observations, prompts);
|
|
820
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
|
+
}
|
|
821
960
|
try {
|
|
822
961
|
logAuditEntry(db, {
|
|
823
962
|
sessionId: session_id,
|
|
@@ -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
|
|
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, "
|
|
263
|
-
claudeMdPath: resolve(root, "
|
|
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) {
|
|
@@ -351,17 +420,46 @@ function getFeatureImpact(db, filePaths) {
|
|
|
351
420
|
|
|
352
421
|
// src/hooks/pre-delete-check.ts
|
|
353
422
|
var PROJECT_ROOT2 = getProjectRoot();
|
|
423
|
+
var KNOWLEDGE_PROTECTED_FILES = [
|
|
424
|
+
"knowledge-db.ts",
|
|
425
|
+
"knowledge-indexer.ts",
|
|
426
|
+
"knowledge-tools.ts"
|
|
427
|
+
];
|
|
354
428
|
function getDataDb() {
|
|
355
429
|
const dbPath = getResolvedPaths().dataDbPath;
|
|
356
430
|
if (!existsSync2(dbPath)) return null;
|
|
357
431
|
try {
|
|
358
|
-
const db = new
|
|
432
|
+
const db = new Database2(dbPath, { readonly: true });
|
|
359
433
|
db.pragma("journal_mode = WAL");
|
|
360
434
|
return db;
|
|
361
435
|
} catch {
|
|
362
436
|
return null;
|
|
363
437
|
}
|
|
364
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
|
+
}
|
|
365
463
|
function extractDeletedFiles(input) {
|
|
366
464
|
const files = [];
|
|
367
465
|
if (input.tool_name === "Bash" && input.tool_input.command) {
|
|
@@ -371,7 +469,7 @@ function extractDeletedFiles(input) {
|
|
|
371
469
|
const paths = rmMatch[1].split(/\s+/).filter((p) => !p.startsWith("-"));
|
|
372
470
|
for (const p of paths) {
|
|
373
471
|
const relPath = p.startsWith("src/") ? p : p.replace(PROJECT_ROOT2 + "/", "");
|
|
374
|
-
if (relPath.startsWith("src/")) {
|
|
472
|
+
if (relPath.startsWith("src/") || relPath.endsWith(".py")) {
|
|
375
473
|
files.push(relPath);
|
|
376
474
|
}
|
|
377
475
|
}
|
|
@@ -381,7 +479,7 @@ function extractDeletedFiles(input) {
|
|
|
381
479
|
const content = input.tool_input.content || "";
|
|
382
480
|
if (content.trim().length === 0) {
|
|
383
481
|
const relPath = input.tool_input.file_path.replace(PROJECT_ROOT2 + "/", "");
|
|
384
|
-
if (relPath.startsWith("src/")) {
|
|
482
|
+
if (relPath.startsWith("src/") || relPath.endsWith(".py")) {
|
|
385
483
|
files.push(relPath);
|
|
386
484
|
}
|
|
387
485
|
}
|
|
@@ -392,6 +490,12 @@ async function main() {
|
|
|
392
490
|
try {
|
|
393
491
|
const input = await readStdin();
|
|
394
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
|
+
}
|
|
395
499
|
const deletedFiles = extractDeletedFiles(hookInput);
|
|
396
500
|
if (deletedFiles.length === 0) {
|
|
397
501
|
process.exit(0);
|
|
@@ -402,10 +506,11 @@ async function main() {
|
|
|
402
506
|
process.exit(0);
|
|
403
507
|
return;
|
|
404
508
|
}
|
|
509
|
+
const SENTINEL_TABLE = "massu_sentinel";
|
|
405
510
|
try {
|
|
406
511
|
const tableExists = db.prepare(
|
|
407
|
-
|
|
408
|
-
).get();
|
|
512
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`
|
|
513
|
+
).get(SENTINEL_TABLE);
|
|
409
514
|
if (!tableExists) {
|
|
410
515
|
process.exit(0);
|
|
411
516
|
return;
|
|
@@ -432,6 +537,31 @@ async function main() {
|
|
|
432
537
|
msg.push("Create a migration plan before deleting these files.");
|
|
433
538
|
process.stdout.write(JSON.stringify({ message: msg.join("\n") }));
|
|
434
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
|
+
}
|
|
435
565
|
} finally {
|
|
436
566
|
db.close();
|
|
437
567
|
}
|
|
@@ -9,6 +9,7 @@ import { existsSync as existsSync2, mkdirSync } from "fs";
|
|
|
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,11 +317,20 @@ 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, "
|
|
264
|
-
claudeMdPath: resolve(root, "
|
|
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
|
|
|
@@ -746,6 +812,15 @@ function initMemorySchema(db) {
|
|
|
746
812
|
);
|
|
747
813
|
CREATE INDEX IF NOT EXISTS idx_pending_sync_created ON pending_sync(created_at ASC);
|
|
748
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
|
+
`);
|
|
749
824
|
}
|
|
750
825
|
|
|
751
826
|
// src/hooks/quality-event.ts
|
|
@@ -52,6 +52,24 @@ function checkFilePath(filePath) {
|
|
|
52
52
|
}
|
|
53
53
|
return null;
|
|
54
54
|
}
|
|
55
|
+
var DANGEROUS_PYTHON_PATTERNS = [
|
|
56
|
+
{ pattern: /\beval\s*\(/, label: "Python eval() \u2014 arbitrary code execution" },
|
|
57
|
+
{ pattern: /\bexec\s*\(/, label: "Python exec() \u2014 arbitrary code execution" },
|
|
58
|
+
{ pattern: /\b__import__\s*\(/, label: "Python __import__() \u2014 dynamic import (potential code injection)" },
|
|
59
|
+
{ pattern: /subprocess\.call\([^)]*shell\s*=\s*True/, label: "subprocess.call(shell=True) \u2014 shell injection risk" },
|
|
60
|
+
{ pattern: /subprocess\.Popen\([^)]*shell\s*=\s*True/, label: "subprocess.Popen(shell=True) \u2014 shell injection risk" },
|
|
61
|
+
{ pattern: /os\.system\s*\(/, label: "os.system() \u2014 shell injection risk" },
|
|
62
|
+
{ pattern: /\bf['"].*\{.*\}.*['"].*(?:execute|cursor|query)/, label: "f-string in SQL \u2014 SQL injection risk" },
|
|
63
|
+
{ pattern: /['"].*%s.*['"].*%.*(?:execute|cursor|query)/, label: "String formatting in SQL \u2014 SQL injection risk" }
|
|
64
|
+
];
|
|
65
|
+
function checkPythonContent(content) {
|
|
66
|
+
for (const { pattern, label } of DANGEROUS_PYTHON_PATTERNS) {
|
|
67
|
+
if (pattern.test(content)) {
|
|
68
|
+
return label;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
55
73
|
async function main() {
|
|
56
74
|
try {
|
|
57
75
|
const input = await readStdin();
|
|
@@ -77,6 +95,17 @@ Ensure this is intentional and no secrets will be exposed.`
|
|
|
77
95
|
}));
|
|
78
96
|
}
|
|
79
97
|
}
|
|
98
|
+
const pyContent = tool_input.content || tool_input.new_string;
|
|
99
|
+
if ((tool_name === "Write" || tool_name === "Edit") && tool_input.file_path?.endsWith(".py") && pyContent) {
|
|
100
|
+
const pyViolation = checkPythonContent(pyContent);
|
|
101
|
+
if (pyViolation) {
|
|
102
|
+
process.stdout.write(JSON.stringify({
|
|
103
|
+
message: `SECURITY GATE: Dangerous Python pattern detected: ${pyViolation}
|
|
104
|
+
File: ${tool_input.file_path}
|
|
105
|
+
Review carefully before proceeding.`
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
80
109
|
} catch {
|
|
81
110
|
}
|
|
82
111
|
process.exit(0);
|