@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,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
|
function enqueueSyncPayload(db, payload) {
|
|
751
826
|
db.prepare("INSERT INTO pending_sync (payload) VALUES (?)").run(payload);
|
|
@@ -996,10 +1071,10 @@ function safeParseJson(json, fallback) {
|
|
|
996
1071
|
}
|
|
997
1072
|
|
|
998
1073
|
// src/session-archiver.ts
|
|
999
|
-
var PROJECT_ROOT = getProjectRoot();
|
|
1000
1074
|
function archiveAndRegenerate(db, sessionId) {
|
|
1001
|
-
const
|
|
1002
|
-
const
|
|
1075
|
+
const resolved = getResolvedPaths();
|
|
1076
|
+
const currentMdPath = resolved.sessionStatePath;
|
|
1077
|
+
const archiveDir = resolved.sessionArchivePath;
|
|
1003
1078
|
let archived = false;
|
|
1004
1079
|
let archivePath;
|
|
1005
1080
|
if (existsSync3(currentMdPath)) {
|
|
@@ -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
|
|
|
@@ -752,6 +818,15 @@ function initMemorySchema(db) {
|
|
|
752
818
|
);
|
|
753
819
|
CREATE INDEX IF NOT EXISTS idx_pending_sync_created ON pending_sync(created_at ASC);
|
|
754
820
|
`);
|
|
821
|
+
db.exec(`
|
|
822
|
+
CREATE TABLE IF NOT EXISTS license_cache (
|
|
823
|
+
api_key_hash TEXT PRIMARY KEY,
|
|
824
|
+
tier TEXT NOT NULL,
|
|
825
|
+
valid_until TEXT NOT NULL,
|
|
826
|
+
last_validated TEXT NOT NULL,
|
|
827
|
+
features TEXT DEFAULT '[]'
|
|
828
|
+
);
|
|
829
|
+
`);
|
|
755
830
|
}
|
|
756
831
|
function autoDetectTaskId(planFile) {
|
|
757
832
|
if (!planFile) return null;
|
|
@@ -831,6 +906,8 @@ function linkSessionToTask(db, sessionId, taskId) {
|
|
|
831
906
|
}
|
|
832
907
|
|
|
833
908
|
// src/hooks/session-start.ts
|
|
909
|
+
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
|
|
910
|
+
import { join } from "path";
|
|
834
911
|
async function main() {
|
|
835
912
|
try {
|
|
836
913
|
const input = await readStdin();
|
|
@@ -857,7 +934,7 @@ Session memory, code intelligence, and governance are now active.
|
|
|
857
934
|
`
|
|
858
935
|
);
|
|
859
936
|
}
|
|
860
|
-
const context = buildContext(db, session_id, source ?? "startup", tokenBudget, session?.task_id ?? null);
|
|
937
|
+
const context = await buildContext(db, session_id, source ?? "startup", tokenBudget, session?.task_id ?? null);
|
|
861
938
|
if (context.trim()) {
|
|
862
939
|
process.stdout.write(context);
|
|
863
940
|
}
|
|
@@ -882,7 +959,7 @@ function getTokenBudget(source) {
|
|
|
882
959
|
return 2e3;
|
|
883
960
|
}
|
|
884
961
|
}
|
|
885
|
-
function buildContext(db, sessionId, source, tokenBudget, taskId) {
|
|
962
|
+
async function buildContext(db, sessionId, source, tokenBudget, taskId) {
|
|
886
963
|
const sections = [];
|
|
887
964
|
const failures = getFailedAttempts(db, void 0, 10);
|
|
888
965
|
if (failures.length > 0) {
|
|
@@ -939,6 +1016,47 @@ function buildContext(db, sessionId, source, tokenBudget, taskId) {
|
|
|
939
1016
|
sections.push({ text: progressText, importance: 8 });
|
|
940
1017
|
}
|
|
941
1018
|
}
|
|
1019
|
+
const preventionRules = loadCorrectionsPreventionRules();
|
|
1020
|
+
if (preventionRules.length > 0) {
|
|
1021
|
+
let rulesText = "### Active Prevention Rules (from corrections.md)\n";
|
|
1022
|
+
for (const rule of preventionRules) {
|
|
1023
|
+
rulesText += `- ${rule}
|
|
1024
|
+
`;
|
|
1025
|
+
}
|
|
1026
|
+
sections.push({ text: rulesText, importance: 9 });
|
|
1027
|
+
}
|
|
1028
|
+
try {
|
|
1029
|
+
const knowledgeDbPath = getResolvedPaths().knowledgeDbPath;
|
|
1030
|
+
if (existsSync3(knowledgeDbPath)) {
|
|
1031
|
+
const Database2 = (await import("better-sqlite3")).default;
|
|
1032
|
+
const kdb = new Database2(knowledgeDbPath, { readonly: true });
|
|
1033
|
+
try {
|
|
1034
|
+
const stats = kdb.prepare(
|
|
1035
|
+
"SELECT COUNT(*) as doc_count, MAX(indexed_at) as last_indexed FROM knowledge_documents"
|
|
1036
|
+
).get();
|
|
1037
|
+
if (stats.doc_count > 0 && stats.last_indexed) {
|
|
1038
|
+
const ageMs = Date.now() - new Date(stats.last_indexed).getTime();
|
|
1039
|
+
const ageHours = Math.round(ageMs / 36e5);
|
|
1040
|
+
if (ageHours > 24) {
|
|
1041
|
+
sections.push({
|
|
1042
|
+
text: `### Knowledge Index Status
|
|
1043
|
+
Index has ${stats.doc_count} documents, last indexed ${ageHours}h ago. Consider re-indexing.
|
|
1044
|
+
`,
|
|
1045
|
+
importance: 3
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
} else if (stats.doc_count === 0) {
|
|
1049
|
+
sections.push({
|
|
1050
|
+
text: "### Knowledge Index Status\nKnowledge index is empty. Run knowledge indexing to populate it.\n",
|
|
1051
|
+
importance: 2
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
} finally {
|
|
1055
|
+
kdb.close();
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
} catch (_knowledgeErr) {
|
|
1059
|
+
}
|
|
942
1060
|
const recentObs = getRecentObservations(db, 20);
|
|
943
1061
|
if (recentObs.length > 0) {
|
|
944
1062
|
let obsText = "### Recent Observations\n";
|
|
@@ -1002,4 +1120,32 @@ function safeParseJson(json) {
|
|
|
1002
1120
|
return null;
|
|
1003
1121
|
}
|
|
1004
1122
|
}
|
|
1123
|
+
function loadCorrectionsPreventionRules() {
|
|
1124
|
+
try {
|
|
1125
|
+
const homeDir = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
1126
|
+
const cwd = process.cwd();
|
|
1127
|
+
const config = getConfig();
|
|
1128
|
+
const claudeDirName = config.conventions?.claudeDirName ?? ".claude";
|
|
1129
|
+
const projectDirName = cwd.replace(/\//g, "-").replace(/^-/, "");
|
|
1130
|
+
const correctionsPath = join(homeDir, claudeDirName, "projects", projectDirName, "memory", "corrections.md");
|
|
1131
|
+
if (!existsSync3(correctionsPath)) return [];
|
|
1132
|
+
const content = readFileSync2(correctionsPath, "utf-8");
|
|
1133
|
+
const lines = content.split("\n");
|
|
1134
|
+
const rules = [];
|
|
1135
|
+
for (const line of lines) {
|
|
1136
|
+
const trimmed = line.trim();
|
|
1137
|
+
if (!trimmed.startsWith("|") || !trimmed.endsWith("|")) continue;
|
|
1138
|
+
const cells = trimmed.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
|
|
1139
|
+
if (cells.length < 4) continue;
|
|
1140
|
+
if (cells[0] === "Date" || cells[0].startsWith("-")) continue;
|
|
1141
|
+
const preventionRule = cells[3];
|
|
1142
|
+
if (preventionRule && !preventionRule.startsWith("-") && !preventionRule.startsWith("<!--")) {
|
|
1143
|
+
rules.push(preventionRule);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
return rules;
|
|
1147
|
+
} catch (_e) {
|
|
1148
|
+
return [];
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1005
1151
|
main();
|
|
@@ -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 addUserPrompt(db, sessionId, text, promptNumber) {
|
|
764
886
|
const now = /* @__PURE__ */ new Date();
|
|
765
887
|
db.prepare(`
|
|
@@ -772,6 +894,7 @@ function linkSessionToTask(db, sessionId, taskId) {
|
|
|
772
894
|
}
|
|
773
895
|
|
|
774
896
|
// src/hooks/user-prompt.ts
|
|
897
|
+
import { existsSync as existsSync3 } from "fs";
|
|
775
898
|
async function main() {
|
|
776
899
|
try {
|
|
777
900
|
const input = await readStdin();
|
|
@@ -799,6 +922,35 @@ async function main() {
|
|
|
799
922
|
).get(session_id);
|
|
800
923
|
const promptNumber = countResult.count + 1;
|
|
801
924
|
addUserPrompt(db, session_id, prompt.trim(), promptNumber);
|
|
925
|
+
try {
|
|
926
|
+
const fileRefs = extractFileReferences(prompt);
|
|
927
|
+
if (fileRefs.length > 0) {
|
|
928
|
+
const knowledgeDbPath = getResolvedPaths().knowledgeDbPath;
|
|
929
|
+
if (knowledgeDbPath && existsSync3(knowledgeDbPath)) {
|
|
930
|
+
const Database2 = (await import("better-sqlite3")).default;
|
|
931
|
+
const kdb = new Database2(knowledgeDbPath, { readonly: true });
|
|
932
|
+
try {
|
|
933
|
+
const placeholders = fileRefs.map(() => "?").join(",");
|
|
934
|
+
const matches = kdb.prepare(
|
|
935
|
+
`SELECT DISTINCT file_path FROM knowledge_documents WHERE file_path IN (${placeholders})`
|
|
936
|
+
).all(...fileRefs);
|
|
937
|
+
if (matches.length > 0) {
|
|
938
|
+
addObservation(
|
|
939
|
+
db,
|
|
940
|
+
session_id,
|
|
941
|
+
"discovery",
|
|
942
|
+
`Knowledge entries exist for referenced files`,
|
|
943
|
+
`Files with knowledge context: ${matches.map((m) => m.file_path).join(", ")}`,
|
|
944
|
+
{ importance: 2 }
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
} finally {
|
|
948
|
+
kdb.close();
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
} catch (_knowledgeErr) {
|
|
953
|
+
}
|
|
802
954
|
} finally {
|
|
803
955
|
db.close();
|
|
804
956
|
}
|
|
@@ -806,6 +958,15 @@ async function main() {
|
|
|
806
958
|
}
|
|
807
959
|
process.exit(0);
|
|
808
960
|
}
|
|
961
|
+
function extractFileReferences(prompt) {
|
|
962
|
+
const filePattern = /(?:^|\s)((?:src|packages|lib)\/[\w./-]+\.(?:ts|tsx|js|jsx|md))/g;
|
|
963
|
+
const matches = [];
|
|
964
|
+
let match;
|
|
965
|
+
while ((match = filePattern.exec(prompt)) !== null) {
|
|
966
|
+
matches.push(match[1]);
|
|
967
|
+
}
|
|
968
|
+
return [...new Set(matches)];
|
|
969
|
+
}
|
|
809
970
|
async function getGitBranch() {
|
|
810
971
|
try {
|
|
811
972
|
const { spawnSync } = await import("child_process");
|
package/package.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@massu/core",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "AI Engineering Governance MCP Server - Session memory, feature registry, code intelligence,
|
|
5
|
+
"description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 72 total), 31 workflow commands",
|
|
6
6
|
"main": "src/server.ts",
|
|
7
7
|
"bin": {
|
|
8
|
-
"massu": "./
|
|
8
|
+
"massu": "./dist/cli.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "npx tsx src/server.ts",
|
|
12
12
|
"test": "vitest run",
|
|
13
|
-
"build": "tsc --noEmit && npm run build:hooks",
|
|
13
|
+
"build": "tsc --noEmit && npm run build:cli && npm run build:hooks",
|
|
14
|
+
"build:cli": "esbuild --bundle --platform=node --format=esm --outfile=dist/cli.js src/cli.ts --external:better-sqlite3 --external:yaml --external:zod --banner:js='#!/usr/bin/env node\nimport{createRequire as __cr}from\"module\";const require=__cr(import.meta.url);'",
|
|
14
15
|
"build:hooks": "esbuild --bundle --platform=node --format=esm --outdir=dist/hooks src/hooks/*.ts --external:better-sqlite3 --external:yaml --external:zod --banner:js='import{createRequire as __cr}from\"module\";const require=__cr(import.meta.url);'",
|
|
15
16
|
"prepublishOnly": "bash ../../scripts/prepublish-check.sh && npm run build"
|
|
16
17
|
},
|
|
17
18
|
"dependencies": {
|
|
18
|
-
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
19
19
|
"better-sqlite3": "^12.6.2",
|
|
20
20
|
"yaml": "^2.4.0",
|
|
21
21
|
"zod": "^3.23.0"
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"src/**/*",
|
|
32
32
|
"!src/__tests__/**",
|
|
33
33
|
"dist/**/*",
|
|
34
|
+
"commands/**/*",
|
|
34
35
|
"LICENSE"
|
|
35
36
|
],
|
|
36
37
|
"keywords": [
|