@massu/core 0.1.2 → 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.
- 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 +12522 -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 -0
- 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
|
|
|
751
826
|
// src/hooks/cost-tracker.ts
|
|
@@ -7,6 +7,7 @@ import Database from "better-sqlite3";
|
|
|
7
7
|
// src/config.ts
|
|
8
8
|
import { resolve, dirname } from "path";
|
|
9
9
|
import { existsSync, readFileSync } from "fs";
|
|
10
|
+
import { homedir } from "os";
|
|
10
11
|
import { parse as parseYaml } from "yaml";
|
|
11
12
|
import { z } from "zod";
|
|
12
13
|
var DomainConfigSchema = z.object({
|
|
@@ -138,6 +139,49 @@ var CloudConfigSchema = z.object({
|
|
|
138
139
|
audit: z.boolean().default(true)
|
|
139
140
|
}).default({ memory: true, analytics: true, audit: true })
|
|
140
141
|
}).optional();
|
|
142
|
+
var ConventionsConfigSchema = z.object({
|
|
143
|
+
claudeDirName: z.string().default(".claude").refine(
|
|
144
|
+
(s) => !s.includes("..") && !s.startsWith("/"),
|
|
145
|
+
{ message: 'claudeDirName must not contain ".." or start with "/"' }
|
|
146
|
+
),
|
|
147
|
+
sessionStatePath: z.string().default(".claude/session-state/CURRENT.md").refine(
|
|
148
|
+
(s) => !s.includes("..") && !s.startsWith("/"),
|
|
149
|
+
{ message: 'sessionStatePath must not contain ".." or start with "/"' }
|
|
150
|
+
),
|
|
151
|
+
sessionArchivePath: z.string().default(".claude/session-state/archive").refine(
|
|
152
|
+
(s) => !s.includes("..") && !s.startsWith("/"),
|
|
153
|
+
{ message: 'sessionArchivePath must not contain ".." or start with "/"' }
|
|
154
|
+
),
|
|
155
|
+
knowledgeCategories: z.array(z.string()).default([
|
|
156
|
+
"patterns",
|
|
157
|
+
"commands",
|
|
158
|
+
"incidents",
|
|
159
|
+
"reference",
|
|
160
|
+
"protocols",
|
|
161
|
+
"checklists",
|
|
162
|
+
"playbooks",
|
|
163
|
+
"critical",
|
|
164
|
+
"scripts",
|
|
165
|
+
"status",
|
|
166
|
+
"templates",
|
|
167
|
+
"loop-state",
|
|
168
|
+
"session-state",
|
|
169
|
+
"agents"
|
|
170
|
+
]),
|
|
171
|
+
knowledgeSourceFiles: z.array(z.string()).default(["CLAUDE.md", "MEMORY.md", "corrections.md"]),
|
|
172
|
+
excludePatterns: z.array(z.string()).default(["/ARCHIVE/", "/SESSION-HISTORY/"])
|
|
173
|
+
}).optional();
|
|
174
|
+
var PythonDomainConfigSchema = z.object({
|
|
175
|
+
name: z.string(),
|
|
176
|
+
packages: z.array(z.string()),
|
|
177
|
+
allowed_imports_from: z.array(z.string()).default([])
|
|
178
|
+
});
|
|
179
|
+
var PythonConfigSchema = z.object({
|
|
180
|
+
root: z.string(),
|
|
181
|
+
alembic_dir: z.string().optional(),
|
|
182
|
+
domains: z.array(PythonDomainConfigSchema).default([]),
|
|
183
|
+
exclude_dirs: z.array(z.string()).default(["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"])
|
|
184
|
+
}).optional();
|
|
141
185
|
var PathsConfigSchema = z.object({
|
|
142
186
|
source: z.string().default("src"),
|
|
143
187
|
aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
|
|
@@ -172,7 +216,9 @@ var RawConfigSchema = z.object({
|
|
|
172
216
|
security: SecurityConfigSchema,
|
|
173
217
|
team: TeamConfigSchema,
|
|
174
218
|
regression: RegressionConfigSchema,
|
|
175
|
-
cloud: CloudConfigSchema
|
|
219
|
+
cloud: CloudConfigSchema,
|
|
220
|
+
conventions: ConventionsConfigSchema,
|
|
221
|
+
python: PythonConfigSchema
|
|
176
222
|
}).passthrough();
|
|
177
223
|
var _config = null;
|
|
178
224
|
var _projectRoot = null;
|
|
@@ -236,13 +282,24 @@ function getConfig() {
|
|
|
236
282
|
security: parsed.security,
|
|
237
283
|
team: parsed.team,
|
|
238
284
|
regression: parsed.regression,
|
|
239
|
-
cloud: parsed.cloud
|
|
285
|
+
cloud: parsed.cloud,
|
|
286
|
+
conventions: parsed.conventions,
|
|
287
|
+
python: parsed.python
|
|
240
288
|
};
|
|
289
|
+
if (!_config.cloud?.apiKey && process.env.MASSU_API_KEY) {
|
|
290
|
+
_config.cloud = {
|
|
291
|
+
enabled: true,
|
|
292
|
+
sync: { memory: true, analytics: true, audit: true },
|
|
293
|
+
..._config.cloud,
|
|
294
|
+
apiKey: process.env.MASSU_API_KEY
|
|
295
|
+
};
|
|
296
|
+
}
|
|
241
297
|
return _config;
|
|
242
298
|
}
|
|
243
299
|
function getResolvedPaths() {
|
|
244
300
|
const config = getConfig();
|
|
245
301
|
const root = getProjectRoot();
|
|
302
|
+
const claudeDirName = config.conventions?.claudeDirName ?? ".claude";
|
|
246
303
|
return {
|
|
247
304
|
codegraphDbPath: resolve(root, ".codegraph/codegraph.db"),
|
|
248
305
|
dataDbPath: resolve(root, ".massu/data.db"),
|
|
@@ -258,11 +315,20 @@ function getResolvedPaths() {
|
|
|
258
315
|
),
|
|
259
316
|
extensions: [".ts", ".tsx", ".js", ".jsx"],
|
|
260
317
|
indexFiles: ["index.ts", "index.tsx", "index.js", "index.jsx"],
|
|
261
|
-
patternsDir: resolve(root, "
|
|
262
|
-
claudeMdPath: resolve(root, "
|
|
318
|
+
patternsDir: resolve(root, claudeDirName, "patterns"),
|
|
319
|
+
claudeMdPath: resolve(root, claudeDirName, "CLAUDE.md"),
|
|
263
320
|
docsMapPath: resolve(root, ".massu/docs-map.json"),
|
|
264
321
|
helpSitePath: resolve(root, "../" + config.project.name + "-help"),
|
|
265
|
-
memoryDbPath: resolve(root, ".massu/memory.db")
|
|
322
|
+
memoryDbPath: resolve(root, ".massu/memory.db"),
|
|
323
|
+
knowledgeDbPath: resolve(root, ".massu/knowledge.db"),
|
|
324
|
+
plansDir: resolve(root, "docs/plans"),
|
|
325
|
+
docsDir: resolve(root, "docs"),
|
|
326
|
+
claudeDir: resolve(root, claudeDirName),
|
|
327
|
+
memoryDir: resolve(homedir(), claudeDirName, "projects", root.replace(/\//g, "-"), "memory"),
|
|
328
|
+
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
329
|
+
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
330
|
+
mcpJsonPath: resolve(root, ".mcp.json"),
|
|
331
|
+
settingsLocalPath: resolve(root, claudeDirName, "settings.local.json")
|
|
266
332
|
};
|
|
267
333
|
}
|
|
268
334
|
|
|
@@ -302,7 +368,7 @@ async function main() {
|
|
|
302
368
|
}
|
|
303
369
|
const root = getProjectRoot();
|
|
304
370
|
const rel = filePath.startsWith(root + "/") ? filePath.slice(root.length + 1) : filePath;
|
|
305
|
-
if (!rel.startsWith("src/")) {
|
|
371
|
+
if (!rel.startsWith("src/") && !rel.endsWith(".py")) {
|
|
306
372
|
process.exit(0);
|
|
307
373
|
return;
|
|
308
374
|
}
|
|
@@ -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 assignImportance(type, vrResult) {
|
|
751
826
|
switch (type) {
|
|
@@ -861,6 +936,7 @@ function detectDecisionPatterns(text) {
|
|
|
861
936
|
}
|
|
862
937
|
|
|
863
938
|
// src/observation-extractor.ts
|
|
939
|
+
import { homedir as homedir2 } from "os";
|
|
864
940
|
var PRIVATE_PATTERNS = [
|
|
865
941
|
/\/Users\/\w+/,
|
|
866
942
|
// Absolute macOS paths
|
|
@@ -939,7 +1015,9 @@ function classifyToolCall(tc) {
|
|
|
939
1015
|
}
|
|
940
1016
|
case "Read": {
|
|
941
1017
|
const filePath = tc.input.file_path ?? "unknown";
|
|
942
|
-
|
|
1018
|
+
const knowledgeSourceFiles = getConfig().conventions?.knowledgeSourceFiles ?? ["CLAUDE.md", "MEMORY.md", "corrections.md"];
|
|
1019
|
+
const plansDir = getResolvedPaths().plansDir;
|
|
1020
|
+
if (filePath.includes(plansDir) || knowledgeSourceFiles.some((f) => filePath.includes(f))) {
|
|
943
1021
|
const title = `Read: ${shortenPath(filePath)}`;
|
|
944
1022
|
return {
|
|
945
1023
|
type: "discovery",
|
|
@@ -1049,7 +1127,11 @@ function shortenPath(filePath) {
|
|
|
1049
1127
|
if (filePath.startsWith(root + "/")) {
|
|
1050
1128
|
return filePath.slice(root.length + 1);
|
|
1051
1129
|
}
|
|
1052
|
-
|
|
1130
|
+
const home = homedir2();
|
|
1131
|
+
if (filePath.startsWith(home + "/")) {
|
|
1132
|
+
return "~/" + filePath.slice(home.length + 1);
|
|
1133
|
+
}
|
|
1134
|
+
return filePath;
|
|
1053
1135
|
}
|
|
1054
1136
|
function classifyRealTimeToolCall(toolName, toolInput, toolResponse, seenReads2) {
|
|
1055
1137
|
const tc = {
|
|
@@ -1160,59 +1242,13 @@ function trackModification(db, featureKey) {
|
|
|
1160
1242
|
|
|
1161
1243
|
// src/import-resolver.ts
|
|
1162
1244
|
import { readFileSync as readFileSync2, existsSync as existsSync3, statSync } from "fs";
|
|
1163
|
-
import { resolve as
|
|
1164
|
-
function resolveImportPath(specifier, fromFile) {
|
|
1165
|
-
if (!specifier.startsWith(".") && !specifier.startsWith("@/")) {
|
|
1166
|
-
return null;
|
|
1167
|
-
}
|
|
1168
|
-
let basePath;
|
|
1169
|
-
if (specifier.startsWith("@/")) {
|
|
1170
|
-
const paths = getResolvedPaths();
|
|
1171
|
-
basePath = resolve3(paths.pathAlias["@"] ?? paths.srcDir, specifier.slice(2));
|
|
1172
|
-
} else {
|
|
1173
|
-
basePath = resolve3(dirname3(fromFile), specifier);
|
|
1174
|
-
}
|
|
1175
|
-
if (existsSync3(basePath) && !isDirectory(basePath)) {
|
|
1176
|
-
return toRelative(basePath);
|
|
1177
|
-
}
|
|
1178
|
-
const resolvedPaths = getResolvedPaths();
|
|
1179
|
-
for (const ext of resolvedPaths.extensions) {
|
|
1180
|
-
const withExt = basePath + ext;
|
|
1181
|
-
if (existsSync3(withExt)) {
|
|
1182
|
-
return toRelative(withExt);
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
for (const indexFile of resolvedPaths.indexFiles) {
|
|
1186
|
-
const indexPath = join(basePath, indexFile);
|
|
1187
|
-
if (existsSync3(indexPath)) {
|
|
1188
|
-
return toRelative(indexPath);
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
return null;
|
|
1192
|
-
}
|
|
1193
|
-
function isDirectory(path) {
|
|
1194
|
-
try {
|
|
1195
|
-
return statSync(path).isDirectory();
|
|
1196
|
-
} catch {
|
|
1197
|
-
return false;
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
function toRelative(absPath) {
|
|
1201
|
-
const root = getProjectRoot();
|
|
1202
|
-
if (absPath.startsWith(root)) {
|
|
1203
|
-
return absPath.slice(root.length + 1);
|
|
1204
|
-
}
|
|
1205
|
-
return absPath;
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
// src/validation-engine.ts
|
|
1209
|
-
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
1245
|
+
import { resolve as resolve4, dirname as dirname3, join } from "path";
|
|
1210
1246
|
|
|
1211
1247
|
// src/security-utils.ts
|
|
1212
|
-
import { resolve as
|
|
1248
|
+
import { resolve as resolve3, normalize } from "path";
|
|
1213
1249
|
function ensureWithinRoot(filePath, projectRoot) {
|
|
1214
|
-
const resolvedRoot =
|
|
1215
|
-
const resolvedPath =
|
|
1250
|
+
const resolvedRoot = resolve3(projectRoot);
|
|
1251
|
+
const resolvedPath = resolve3(resolvedRoot, filePath);
|
|
1216
1252
|
const normalizedPath = normalize(resolvedPath);
|
|
1217
1253
|
const normalizedRoot = normalize(resolvedRoot);
|
|
1218
1254
|
if (!normalizedPath.startsWith(normalizedRoot + "/") && normalizedPath !== normalizedRoot) {
|
|
@@ -1254,7 +1290,53 @@ function enforceSeverityFloors(configWeights, defaults) {
|
|
|
1254
1290
|
return result;
|
|
1255
1291
|
}
|
|
1256
1292
|
|
|
1293
|
+
// src/import-resolver.ts
|
|
1294
|
+
function resolveImportPath(specifier, fromFile) {
|
|
1295
|
+
if (!specifier.startsWith(".") && !specifier.startsWith("@/")) {
|
|
1296
|
+
return null;
|
|
1297
|
+
}
|
|
1298
|
+
let basePath;
|
|
1299
|
+
if (specifier.startsWith("@/")) {
|
|
1300
|
+
const paths = getResolvedPaths();
|
|
1301
|
+
basePath = resolve4(paths.pathAlias["@"] ?? paths.srcDir, specifier.slice(2));
|
|
1302
|
+
} else {
|
|
1303
|
+
basePath = resolve4(dirname3(fromFile), specifier);
|
|
1304
|
+
}
|
|
1305
|
+
if (existsSync3(basePath) && !isDirectory(basePath)) {
|
|
1306
|
+
return toRelative(basePath);
|
|
1307
|
+
}
|
|
1308
|
+
const resolvedPaths = getResolvedPaths();
|
|
1309
|
+
for (const ext of resolvedPaths.extensions) {
|
|
1310
|
+
const withExt = basePath + ext;
|
|
1311
|
+
if (existsSync3(withExt)) {
|
|
1312
|
+
return toRelative(withExt);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
for (const indexFile of resolvedPaths.indexFiles) {
|
|
1316
|
+
const indexPath = join(basePath, indexFile);
|
|
1317
|
+
if (existsSync3(indexPath)) {
|
|
1318
|
+
return toRelative(indexPath);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
return null;
|
|
1322
|
+
}
|
|
1323
|
+
function isDirectory(path) {
|
|
1324
|
+
try {
|
|
1325
|
+
return statSync(path).isDirectory();
|
|
1326
|
+
} catch {
|
|
1327
|
+
return false;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
function toRelative(absPath) {
|
|
1331
|
+
const root = getProjectRoot();
|
|
1332
|
+
if (absPath.startsWith(root)) {
|
|
1333
|
+
return absPath.slice(root.length + 1);
|
|
1334
|
+
}
|
|
1335
|
+
return absPath;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1257
1338
|
// src/validation-engine.ts
|
|
1339
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
1258
1340
|
function getValidationChecks() {
|
|
1259
1341
|
return getConfig().governance?.validation?.checks ?? {
|
|
1260
1342
|
rule_compliance: true,
|
|
@@ -1524,6 +1606,9 @@ function storeSecurityScore(db, sessionId, filePath, riskScore, findings) {
|
|
|
1524
1606
|
}
|
|
1525
1607
|
|
|
1526
1608
|
// src/hooks/post-tool-use.ts
|
|
1609
|
+
import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
|
|
1610
|
+
import { join as join2 } from "path";
|
|
1611
|
+
import { parse as parseYaml2 } from "yaml";
|
|
1527
1612
|
var seenReads = /* @__PURE__ */ new Set();
|
|
1528
1613
|
var currentSessionId = null;
|
|
1529
1614
|
async function main() {
|
|
@@ -1601,6 +1686,41 @@ async function main() {
|
|
|
1601
1686
|
}
|
|
1602
1687
|
} catch (_securityErr) {
|
|
1603
1688
|
}
|
|
1689
|
+
try {
|
|
1690
|
+
if (tool_name === "Edit" || tool_name === "Write") {
|
|
1691
|
+
const filePath = tool_input.file_path ?? "";
|
|
1692
|
+
if (filePath && filePath.endsWith("MEMORY.md") && filePath.includes("/memory/")) {
|
|
1693
|
+
const issues = checkMemoryFileIntegrity(filePath);
|
|
1694
|
+
if (issues.length > 0) {
|
|
1695
|
+
addObservation(
|
|
1696
|
+
db,
|
|
1697
|
+
session_id,
|
|
1698
|
+
"incident_near_miss",
|
|
1699
|
+
"MEMORY.md integrity issue detected",
|
|
1700
|
+
issues.join("; "),
|
|
1701
|
+
{ importance: 4 }
|
|
1702
|
+
);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
} catch (_memoryErr) {
|
|
1707
|
+
}
|
|
1708
|
+
try {
|
|
1709
|
+
if (tool_name === "Edit" || tool_name === "Write") {
|
|
1710
|
+
const filePath = tool_input.file_path ?? "";
|
|
1711
|
+
if (filePath && isKnowledgeSourceFile(filePath)) {
|
|
1712
|
+
addObservation(
|
|
1713
|
+
db,
|
|
1714
|
+
session_id,
|
|
1715
|
+
"discovery",
|
|
1716
|
+
"Knowledge source file modified - index may be stale",
|
|
1717
|
+
`Edited ${filePath.split("/").pop() ?? filePath}. Run knowledge re-index to update.`,
|
|
1718
|
+
{ importance: 3 }
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
} catch (_knowledgeErr) {
|
|
1723
|
+
}
|
|
1604
1724
|
} finally {
|
|
1605
1725
|
db.close();
|
|
1606
1726
|
}
|
|
@@ -1640,4 +1760,61 @@ function readStdin() {
|
|
|
1640
1760
|
setTimeout(() => resolve5(data), 3e3);
|
|
1641
1761
|
});
|
|
1642
1762
|
}
|
|
1763
|
+
function readConventions(cwd) {
|
|
1764
|
+
const defaults = {
|
|
1765
|
+
knowledgeSourceFiles: ["CLAUDE.md", "MEMORY.md", "corrections.md"],
|
|
1766
|
+
claudeDirName: ".claude"
|
|
1767
|
+
};
|
|
1768
|
+
try {
|
|
1769
|
+
const projectRoot = cwd ?? process.cwd();
|
|
1770
|
+
const configPath = join2(projectRoot, "massu.config.yaml");
|
|
1771
|
+
if (!existsSync6(configPath)) return defaults;
|
|
1772
|
+
const content = readFileSync5(configPath, "utf-8");
|
|
1773
|
+
const parsed = parseYaml2(content);
|
|
1774
|
+
if (!parsed || typeof parsed !== "object") return defaults;
|
|
1775
|
+
const conventions = parsed.conventions;
|
|
1776
|
+
if (!conventions || typeof conventions !== "object") return defaults;
|
|
1777
|
+
return {
|
|
1778
|
+
knowledgeSourceFiles: Array.isArray(conventions.knowledgeSourceFiles) ? conventions.knowledgeSourceFiles : defaults.knowledgeSourceFiles,
|
|
1779
|
+
claudeDirName: typeof conventions.claudeDirName === "string" ? conventions.claudeDirName : defaults.claudeDirName
|
|
1780
|
+
};
|
|
1781
|
+
} catch {
|
|
1782
|
+
return defaults;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
function isKnowledgeSourceFile(filePath) {
|
|
1786
|
+
const basename2 = filePath.split("/").pop() ?? "";
|
|
1787
|
+
const conventions = readConventions();
|
|
1788
|
+
const knowledgeSourcePatterns = [
|
|
1789
|
+
...conventions.knowledgeSourceFiles,
|
|
1790
|
+
"file-index.md",
|
|
1791
|
+
"knowledge-db.ts",
|
|
1792
|
+
"knowledge-indexer.ts",
|
|
1793
|
+
"knowledge-tools.ts"
|
|
1794
|
+
];
|
|
1795
|
+
return knowledgeSourcePatterns.some((p) => basename2 === p) || filePath.includes("/memory/") || filePath.includes(conventions.claudeDirName + "/");
|
|
1796
|
+
}
|
|
1797
|
+
function checkMemoryFileIntegrity(filePath) {
|
|
1798
|
+
const issues = [];
|
|
1799
|
+
try {
|
|
1800
|
+
if (!existsSync6(filePath)) {
|
|
1801
|
+
issues.push("MEMORY.md file does not exist after write");
|
|
1802
|
+
return issues;
|
|
1803
|
+
}
|
|
1804
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
1805
|
+
const lines = content.split("\n");
|
|
1806
|
+
const MAX_LINES = 200;
|
|
1807
|
+
if (lines.length > MAX_LINES) {
|
|
1808
|
+
issues.push(`MEMORY.md exceeds ${MAX_LINES} lines (currently ${lines.length}). Consider archiving old entries.`);
|
|
1809
|
+
}
|
|
1810
|
+
const requiredSections = ["# Massu Memory", "## Key Learnings", "## Common Gotchas"];
|
|
1811
|
+
for (const section of requiredSections) {
|
|
1812
|
+
if (!content.includes(section)) {
|
|
1813
|
+
issues.push(`Missing required section: "${section}"`);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
} catch (_e) {
|
|
1817
|
+
}
|
|
1818
|
+
return issues;
|
|
1819
|
+
}
|
|
1643
1820
|
main();
|