@massu/core 1.9.2 → 1.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +473 -261
- package/dist/hooks/classify-failure.js +8 -1
- package/dist/hooks/cost-tracker.js +8 -1
- package/dist/hooks/fix-detector.js +3 -3
- package/dist/hooks/incident-pipeline.js +8 -1
- package/dist/hooks/post-edit-context.js +8 -1
- package/dist/hooks/post-tool-use.js +8 -1
- package/dist/hooks/pre-compact.js +8 -1
- package/dist/hooks/pre-delete-check.js +8 -1
- package/dist/hooks/quality-event.js +8 -1
- package/dist/hooks/session-end.js +8 -1
- package/dist/hooks/session-start.js +8 -1
- package/dist/hooks/user-prompt.js +8 -1
- package/package.json +2 -2
- package/src/backfill-sessions.ts +3 -2
- package/src/cli.ts +10 -0
- package/src/commands/hook-runner.ts +145 -0
- package/src/commands/init.ts +223 -29
- package/src/config.ts +2 -1
- package/src/hooks/fix-detector.ts +3 -3
- package/src/lib/memory-path.ts +49 -0
- package/src/license.ts +5 -2
- package/src/security/registry-pubkey.generated.ts +1 -1
- package/src/tools.ts +1 -1
|
@@ -12,6 +12,13 @@ import { existsSync, readFileSync } from "fs";
|
|
|
12
12
|
import { homedir } from "os";
|
|
13
13
|
import { parse as parseYaml } from "yaml";
|
|
14
14
|
import { z } from "zod";
|
|
15
|
+
|
|
16
|
+
// src/lib/memory-path.ts
|
|
17
|
+
function encodeMemoryDirName(projectRoot) {
|
|
18
|
+
return projectRoot.replace(/\//g, "-");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/config.ts
|
|
15
22
|
var DomainConfigSchema = z.object({
|
|
16
23
|
name: z.string().default("Unknown"),
|
|
17
24
|
routers: z.array(z.string()).default([]),
|
|
@@ -516,7 +523,7 @@ function getResolvedPaths() {
|
|
|
516
523
|
plansDir: resolve(root, "docs/plans"),
|
|
517
524
|
docsDir: resolve(root, "docs"),
|
|
518
525
|
claudeDir: resolve(root, claudeDirName),
|
|
519
|
-
memoryDir: resolve(homedir(), claudeDirName, "projects", root
|
|
526
|
+
memoryDir: resolve(homedir(), claudeDirName, "projects", encodeMemoryDirName(root), "memory"),
|
|
520
527
|
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
521
528
|
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
522
529
|
mcpJsonPath: resolve(root, ".mcp.json"),
|
|
@@ -12,6 +12,13 @@ import { existsSync, readFileSync } from "fs";
|
|
|
12
12
|
import { homedir } from "os";
|
|
13
13
|
import { parse as parseYaml } from "yaml";
|
|
14
14
|
import { z } from "zod";
|
|
15
|
+
|
|
16
|
+
// src/lib/memory-path.ts
|
|
17
|
+
function encodeMemoryDirName(projectRoot) {
|
|
18
|
+
return projectRoot.replace(/\//g, "-");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/config.ts
|
|
15
22
|
var DomainConfigSchema = z.object({
|
|
16
23
|
name: z.string().default("Unknown"),
|
|
17
24
|
routers: z.array(z.string()).default([]),
|
|
@@ -516,7 +523,7 @@ function getResolvedPaths() {
|
|
|
516
523
|
plansDir: resolve(root, "docs/plans"),
|
|
517
524
|
docsDir: resolve(root, "docs"),
|
|
518
525
|
claudeDir: resolve(root, claudeDirName),
|
|
519
|
-
memoryDir: resolve(homedir(), claudeDirName, "projects", root
|
|
526
|
+
memoryDir: resolve(homedir(), claudeDirName, "projects", encodeMemoryDirName(root), "memory"),
|
|
520
527
|
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
521
528
|
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
522
529
|
mcpJsonPath: resolve(root, ".mcp.json"),
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import{createRequire as __cr}from"module";const require=__cr(import.meta.url);
|
|
3
3
|
|
|
4
4
|
// src/hooks/fix-detector.ts
|
|
5
|
-
import {
|
|
5
|
+
import { execFileSync } from "child_process";
|
|
6
6
|
import { existsSync as existsSync2, appendFileSync, mkdirSync, readFileSync as readFileSync2 } from "fs";
|
|
7
7
|
import { tmpdir } from "os";
|
|
8
8
|
import { join } from "path";
|
|
@@ -565,9 +565,9 @@ async function main() {
|
|
|
565
565
|
const root = getProjectRoot();
|
|
566
566
|
let diff = "";
|
|
567
567
|
try {
|
|
568
|
-
diff =
|
|
568
|
+
diff = execFileSync("git", ["diff", "--", filePath], { cwd: root, timeout: 3e3, encoding: "utf-8" });
|
|
569
569
|
if (!diff) {
|
|
570
|
-
diff =
|
|
570
|
+
diff = execFileSync("git", ["diff", "HEAD", "--", filePath], { cwd: root, timeout: 3e3, encoding: "utf-8" });
|
|
571
571
|
}
|
|
572
572
|
} catch {
|
|
573
573
|
process.exit(0);
|
|
@@ -11,6 +11,13 @@ import { existsSync, readFileSync } from "fs";
|
|
|
11
11
|
import { homedir } from "os";
|
|
12
12
|
import { parse as parseYaml } from "yaml";
|
|
13
13
|
import { z } from "zod";
|
|
14
|
+
|
|
15
|
+
// src/lib/memory-path.ts
|
|
16
|
+
function encodeMemoryDirName(projectRoot) {
|
|
17
|
+
return projectRoot.replace(/\//g, "-");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/config.ts
|
|
14
21
|
var DomainConfigSchema = z.object({
|
|
15
22
|
name: z.string().default("Unknown"),
|
|
16
23
|
routers: z.array(z.string()).default([]),
|
|
@@ -515,7 +522,7 @@ function getResolvedPaths() {
|
|
|
515
522
|
plansDir: resolve(root, "docs/plans"),
|
|
516
523
|
docsDir: resolve(root, "docs"),
|
|
517
524
|
claudeDir: resolve(root, claudeDirName),
|
|
518
|
-
memoryDir: resolve(homedir(), claudeDirName, "projects", root
|
|
525
|
+
memoryDir: resolve(homedir(), claudeDirName, "projects", encodeMemoryDirName(root), "memory"),
|
|
519
526
|
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
520
527
|
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
521
528
|
mcpJsonPath: resolve(root, ".mcp.json"),
|
|
@@ -10,6 +10,13 @@ import { existsSync, readFileSync } from "fs";
|
|
|
10
10
|
import { homedir } from "os";
|
|
11
11
|
import { parse as parseYaml } from "yaml";
|
|
12
12
|
import { z } from "zod";
|
|
13
|
+
|
|
14
|
+
// src/lib/memory-path.ts
|
|
15
|
+
function encodeMemoryDirName(projectRoot) {
|
|
16
|
+
return projectRoot.replace(/\//g, "-");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/config.ts
|
|
13
20
|
var DomainConfigSchema = z.object({
|
|
14
21
|
name: z.string().default("Unknown"),
|
|
15
22
|
routers: z.array(z.string()).default([]),
|
|
@@ -514,7 +521,7 @@ function getResolvedPaths() {
|
|
|
514
521
|
plansDir: resolve(root, "docs/plans"),
|
|
515
522
|
docsDir: resolve(root, "docs"),
|
|
516
523
|
claudeDir: resolve(root, claudeDirName),
|
|
517
|
-
memoryDir: resolve(homedir(), claudeDirName, "projects", root
|
|
524
|
+
memoryDir: resolve(homedir(), claudeDirName, "projects", encodeMemoryDirName(root), "memory"),
|
|
518
525
|
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
519
526
|
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
520
527
|
mcpJsonPath: resolve(root, ".mcp.json"),
|
|
@@ -12,6 +12,13 @@ import { existsSync, readFileSync } from "fs";
|
|
|
12
12
|
import { homedir } from "os";
|
|
13
13
|
import { parse as parseYaml } from "yaml";
|
|
14
14
|
import { z } from "zod";
|
|
15
|
+
|
|
16
|
+
// src/lib/memory-path.ts
|
|
17
|
+
function encodeMemoryDirName(projectRoot) {
|
|
18
|
+
return projectRoot.replace(/\//g, "-");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/config.ts
|
|
15
22
|
var DomainConfigSchema = z.object({
|
|
16
23
|
name: z.string().default("Unknown"),
|
|
17
24
|
routers: z.array(z.string()).default([]),
|
|
@@ -516,7 +523,7 @@ function getResolvedPaths() {
|
|
|
516
523
|
plansDir: resolve(root, "docs/plans"),
|
|
517
524
|
docsDir: resolve(root, "docs"),
|
|
518
525
|
claudeDir: resolve(root, claudeDirName),
|
|
519
|
-
memoryDir: resolve(homedir(), claudeDirName, "projects", root
|
|
526
|
+
memoryDir: resolve(homedir(), claudeDirName, "projects", encodeMemoryDirName(root), "memory"),
|
|
520
527
|
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
521
528
|
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
522
529
|
mcpJsonPath: resolve(root, ".mcp.json"),
|
|
@@ -12,6 +12,13 @@ import { existsSync, readFileSync } from "fs";
|
|
|
12
12
|
import { homedir } from "os";
|
|
13
13
|
import { parse as parseYaml } from "yaml";
|
|
14
14
|
import { z } from "zod";
|
|
15
|
+
|
|
16
|
+
// src/lib/memory-path.ts
|
|
17
|
+
function encodeMemoryDirName(projectRoot) {
|
|
18
|
+
return projectRoot.replace(/\//g, "-");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/config.ts
|
|
15
22
|
var DomainConfigSchema = z.object({
|
|
16
23
|
name: z.string().default("Unknown"),
|
|
17
24
|
routers: z.array(z.string()).default([]),
|
|
@@ -516,7 +523,7 @@ function getResolvedPaths() {
|
|
|
516
523
|
plansDir: resolve(root, "docs/plans"),
|
|
517
524
|
docsDir: resolve(root, "docs"),
|
|
518
525
|
claudeDir: resolve(root, claudeDirName),
|
|
519
|
-
memoryDir: resolve(homedir(), claudeDirName, "projects", root
|
|
526
|
+
memoryDir: resolve(homedir(), claudeDirName, "projects", encodeMemoryDirName(root), "memory"),
|
|
520
527
|
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
521
528
|
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
522
529
|
mcpJsonPath: resolve(root, ".mcp.json"),
|
|
@@ -11,6 +11,13 @@ import { existsSync, readFileSync } from "fs";
|
|
|
11
11
|
import { homedir } from "os";
|
|
12
12
|
import { parse as parseYaml } from "yaml";
|
|
13
13
|
import { z } from "zod";
|
|
14
|
+
|
|
15
|
+
// src/lib/memory-path.ts
|
|
16
|
+
function encodeMemoryDirName(projectRoot) {
|
|
17
|
+
return projectRoot.replace(/\//g, "-");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/config.ts
|
|
14
21
|
var DomainConfigSchema = z.object({
|
|
15
22
|
name: z.string().default("Unknown"),
|
|
16
23
|
routers: z.array(z.string()).default([]),
|
|
@@ -515,7 +522,7 @@ function getResolvedPaths() {
|
|
|
515
522
|
plansDir: resolve(root, "docs/plans"),
|
|
516
523
|
docsDir: resolve(root, "docs"),
|
|
517
524
|
claudeDir: resolve(root, claudeDirName),
|
|
518
|
-
memoryDir: resolve(homedir(), claudeDirName, "projects", root
|
|
525
|
+
memoryDir: resolve(homedir(), claudeDirName, "projects", encodeMemoryDirName(root), "memory"),
|
|
519
526
|
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
520
527
|
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
521
528
|
mcpJsonPath: resolve(root, ".mcp.json"),
|
|
@@ -12,6 +12,13 @@ import { existsSync, readFileSync } from "fs";
|
|
|
12
12
|
import { homedir } from "os";
|
|
13
13
|
import { parse as parseYaml } from "yaml";
|
|
14
14
|
import { z } from "zod";
|
|
15
|
+
|
|
16
|
+
// src/lib/memory-path.ts
|
|
17
|
+
function encodeMemoryDirName(projectRoot) {
|
|
18
|
+
return projectRoot.replace(/\//g, "-");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/config.ts
|
|
15
22
|
var DomainConfigSchema = z.object({
|
|
16
23
|
name: z.string().default("Unknown"),
|
|
17
24
|
routers: z.array(z.string()).default([]),
|
|
@@ -516,7 +523,7 @@ function getResolvedPaths() {
|
|
|
516
523
|
plansDir: resolve(root, "docs/plans"),
|
|
517
524
|
docsDir: resolve(root, "docs"),
|
|
518
525
|
claudeDir: resolve(root, claudeDirName),
|
|
519
|
-
memoryDir: resolve(homedir(), claudeDirName, "projects", root
|
|
526
|
+
memoryDir: resolve(homedir(), claudeDirName, "projects", encodeMemoryDirName(root), "memory"),
|
|
520
527
|
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
521
528
|
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
522
529
|
mcpJsonPath: resolve(root, ".mcp.json"),
|
|
@@ -12,6 +12,13 @@ import { existsSync, readFileSync } from "fs";
|
|
|
12
12
|
import { homedir } from "os";
|
|
13
13
|
import { parse as parseYaml } from "yaml";
|
|
14
14
|
import { z } from "zod";
|
|
15
|
+
|
|
16
|
+
// src/lib/memory-path.ts
|
|
17
|
+
function encodeMemoryDirName(projectRoot) {
|
|
18
|
+
return projectRoot.replace(/\//g, "-");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/config.ts
|
|
15
22
|
var DomainConfigSchema = z.object({
|
|
16
23
|
name: z.string().default("Unknown"),
|
|
17
24
|
routers: z.array(z.string()).default([]),
|
|
@@ -516,7 +523,7 @@ function getResolvedPaths() {
|
|
|
516
523
|
plansDir: resolve(root, "docs/plans"),
|
|
517
524
|
docsDir: resolve(root, "docs"),
|
|
518
525
|
claudeDir: resolve(root, claudeDirName),
|
|
519
|
-
memoryDir: resolve(homedir(), claudeDirName, "projects", root
|
|
526
|
+
memoryDir: resolve(homedir(), claudeDirName, "projects", encodeMemoryDirName(root), "memory"),
|
|
520
527
|
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
521
528
|
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
522
529
|
mcpJsonPath: resolve(root, ".mcp.json"),
|
|
@@ -5804,6 +5804,13 @@ import { existsSync, readFileSync } from "fs";
|
|
|
5804
5804
|
import { homedir } from "os";
|
|
5805
5805
|
import { parse as parseYaml } from "yaml";
|
|
5806
5806
|
import { z } from "zod";
|
|
5807
|
+
|
|
5808
|
+
// src/lib/memory-path.ts
|
|
5809
|
+
function encodeMemoryDirName(projectRoot) {
|
|
5810
|
+
return projectRoot.replace(/\//g, "-");
|
|
5811
|
+
}
|
|
5812
|
+
|
|
5813
|
+
// src/config.ts
|
|
5807
5814
|
var DomainConfigSchema = z.object({
|
|
5808
5815
|
name: z.string().default("Unknown"),
|
|
5809
5816
|
routers: z.array(z.string()).default([]),
|
|
@@ -6308,7 +6315,7 @@ function getResolvedPaths() {
|
|
|
6308
6315
|
plansDir: resolve(root, "docs/plans"),
|
|
6309
6316
|
docsDir: resolve(root, "docs"),
|
|
6310
6317
|
claudeDir: resolve(root, claudeDirName),
|
|
6311
|
-
memoryDir: resolve(homedir(), claudeDirName, "projects", root
|
|
6318
|
+
memoryDir: resolve(homedir(), claudeDirName, "projects", encodeMemoryDirName(root), "memory"),
|
|
6312
6319
|
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
6313
6320
|
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
6314
6321
|
mcpJsonPath: resolve(root, ".mcp.json"),
|
|
@@ -12,6 +12,13 @@ import { existsSync, readFileSync } from "fs";
|
|
|
12
12
|
import { homedir } from "os";
|
|
13
13
|
import { parse as parseYaml } from "yaml";
|
|
14
14
|
import { z } from "zod";
|
|
15
|
+
|
|
16
|
+
// src/lib/memory-path.ts
|
|
17
|
+
function encodeMemoryDirName(projectRoot) {
|
|
18
|
+
return projectRoot.replace(/\//g, "-");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/config.ts
|
|
15
22
|
var DomainConfigSchema = z.object({
|
|
16
23
|
name: z.string().default("Unknown"),
|
|
17
24
|
routers: z.array(z.string()).default([]),
|
|
@@ -516,7 +523,7 @@ function getResolvedPaths() {
|
|
|
516
523
|
plansDir: resolve(root, "docs/plans"),
|
|
517
524
|
docsDir: resolve(root, "docs"),
|
|
518
525
|
claudeDir: resolve(root, claudeDirName),
|
|
519
|
-
memoryDir: resolve(homedir(), claudeDirName, "projects", root
|
|
526
|
+
memoryDir: resolve(homedir(), claudeDirName, "projects", encodeMemoryDirName(root), "memory"),
|
|
520
527
|
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
521
528
|
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
522
529
|
mcpJsonPath: resolve(root, ".mcp.json"),
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@massu/core",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.5",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free /
|
|
5
|
+
"description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 73 total), 59 workflow commands, 11 agents, 20+ patterns",
|
|
6
6
|
"main": "src/server.ts",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/server.ts",
|
package/src/backfill-sessions.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { getMemoryDb, createSession, addObservation, addSummary, addUserPrompt,
|
|
|
14
14
|
import { parseTranscript, extractUserMessages, getLastAssistantMessage } from './transcript-parser.ts';
|
|
15
15
|
import { extractObservationsFromEntries } from './observation-extractor.ts';
|
|
16
16
|
import { getProjectRoot, getConfig } from './config.ts';
|
|
17
|
+
import { encodeMemoryDirName } from './lib/memory-path.ts';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Auto-detect the Claude Code project transcript directory.
|
|
@@ -23,8 +24,8 @@ function findTranscriptDir(): string {
|
|
|
23
24
|
const home = process.env.HOME ?? '~';
|
|
24
25
|
const projectRoot = getProjectRoot();
|
|
25
26
|
const claudeDirName = getConfig().conventions?.claudeDirName ?? '.claude';
|
|
26
|
-
// Claude Code escapes the path by replacing / with
|
|
27
|
-
const escapedPath = projectRoot
|
|
27
|
+
// Claude Code escapes the path by replacing / with -. Shared helper is SoT.
|
|
28
|
+
const escapedPath = encodeMemoryDirName(projectRoot);
|
|
28
29
|
const candidate = resolve(home, `${claudeDirName}/projects`, escapedPath);
|
|
29
30
|
if (existsSync(candidate)) return candidate;
|
|
30
31
|
// Fallback: scan projects dir for directories matching the project name
|
package/src/cli.ts
CHANGED
|
@@ -42,6 +42,16 @@ async function main(): Promise<void> {
|
|
|
42
42
|
await runInstallHooks();
|
|
43
43
|
break;
|
|
44
44
|
}
|
|
45
|
+
case 'hook-runner': {
|
|
46
|
+
// Dynamic hook dispatcher — invoked by Claude Code's hook command lines
|
|
47
|
+
// (settings.json -> `npx -y @massu/core@<version> hook-runner <name>`).
|
|
48
|
+
// Closes P-003 by resolving the hook file at fire-time instead of baking
|
|
49
|
+
// an absolute npx-cache path at install-time.
|
|
50
|
+
const { runHookRunner } = await import('./commands/hook-runner.ts');
|
|
51
|
+
const result = await runHookRunner(args.slice(1));
|
|
52
|
+
process.exit(result.exitCode);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
45
55
|
case 'install-commands': {
|
|
46
56
|
const { runInstallCommands } = await import('./commands/install-commands.ts');
|
|
47
57
|
await runInstallCommands();
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `massu hook-runner <hook-name>` — dynamic hook dispatcher.
|
|
6
|
+
*
|
|
7
|
+
* Closes the P-003 install-path drift class: previously, `installHooks`
|
|
8
|
+
* baked an ABSOLUTE path to the installer's `dist/hooks/*.js` location
|
|
9
|
+
* (whatever npx happened to cache, e.g. `/opt/homebrew/lib/node_modules/...`).
|
|
10
|
+
* Any cache clear, global-install relocation, or npx upgrade silently 404'd
|
|
11
|
+
* every hook — customers thought auto-learning was working but nothing fired.
|
|
12
|
+
*
|
|
13
|
+
* The fix: settings.json now invokes `npx -y @massu/core@<pinned-version> hook-runner <name>`.
|
|
14
|
+
* This subcommand resolves the hook script via Node's module resolver at
|
|
15
|
+
* fire-time, dispatching to the same compiled hook file that ships with
|
|
16
|
+
* the installer. Customer never sees an absolute path.
|
|
17
|
+
*
|
|
18
|
+
* Performance: each hook fire spawns npx + node. Measured ~120-300ms cold
|
|
19
|
+
* (npx cache hit). Acceptable for hooks not on UI critical path; SessionStart
|
|
20
|
+
* and PreCompact are infrequent, PostToolUse is per-tool-call.
|
|
21
|
+
*
|
|
22
|
+
* Hook name → compiled-file mapping is exhaustive (closed enum) so we fail
|
|
23
|
+
* loudly on typos rather than silent-no-op. Unknown hook names print a
|
|
24
|
+
* diagnostic to stderr and exit 2 (distinct from hook's own non-zero exits).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { existsSync } from 'fs';
|
|
28
|
+
import { resolve, dirname } from 'path';
|
|
29
|
+
import { fileURLToPath } from 'url';
|
|
30
|
+
import { spawn } from 'child_process';
|
|
31
|
+
|
|
32
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
33
|
+
const __dirname = dirname(__filename);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Closed enum of recognized hook names → compiled JS filename under `dist/hooks/`.
|
|
37
|
+
* Keep in sync with `buildHooksConfig` in `commands/init.ts` and the source
|
|
38
|
+
* files in `packages/core/src/hooks/`.
|
|
39
|
+
*/
|
|
40
|
+
export const HOOK_NAME_TO_FILE: Record<string, string> = {
|
|
41
|
+
'session-start': 'session-start.js',
|
|
42
|
+
'session-end': 'session-end.js',
|
|
43
|
+
'security-gate': 'security-gate.js',
|
|
44
|
+
'pre-delete-check': 'pre-delete-check.js',
|
|
45
|
+
'post-tool-use': 'post-tool-use.js',
|
|
46
|
+
'post-edit-context': 'post-edit-context.js',
|
|
47
|
+
'quality-event': 'quality-event.js',
|
|
48
|
+
'cost-tracker': 'cost-tracker.js',
|
|
49
|
+
'fix-detector': 'fix-detector.js',
|
|
50
|
+
'classify-failure': 'classify-failure.js',
|
|
51
|
+
'incident-pipeline': 'incident-pipeline.js',
|
|
52
|
+
'rule-enforcement-pipeline': 'rule-enforcement-pipeline.js',
|
|
53
|
+
'auto-learning-pipeline': 'auto-learning-pipeline.js',
|
|
54
|
+
'pre-compact': 'pre-compact.js',
|
|
55
|
+
'user-prompt': 'user-prompt.js',
|
|
56
|
+
'intent-suggester': 'intent-suggester.js',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the compiled hook file path for a given hook name.
|
|
61
|
+
*
|
|
62
|
+
* Search order (in order of likelihood at runtime):
|
|
63
|
+
* 1. `./hooks/<file>` — bundled compiled layout: dist/cli.js + dist/hooks/*.js
|
|
64
|
+
* (the canonical layout under npx cache + global install).
|
|
65
|
+
* 2. `../hooks/<file>` — TS-source dev layout: src/commands/hook-runner.ts +
|
|
66
|
+
* src/hooks/<file>.ts (used by direct-tsx invocation in tests).
|
|
67
|
+
* 3. `../../dist/hooks/<file>` — TS-source dev layout fallback after build.
|
|
68
|
+
*
|
|
69
|
+
* Hard error on miss — silently swallowing a missing hook is exactly the bug
|
|
70
|
+
* class P-003 closes.
|
|
71
|
+
*/
|
|
72
|
+
export function resolveHookFile(hookName: string): string {
|
|
73
|
+
const file = HOOK_NAME_TO_FILE[hookName];
|
|
74
|
+
if (!file) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Unknown hook: "${hookName}". Recognized: ${Object.keys(HOOK_NAME_TO_FILE).join(', ')}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const candidates = [
|
|
80
|
+
// Bundled compiled layout: dist/cli.js → ./hooks/<file>.js
|
|
81
|
+
resolve(__dirname, 'hooks', file),
|
|
82
|
+
// TS-source dev / sibling layout: src/commands/ → ../hooks/<file>
|
|
83
|
+
resolve(__dirname, '../hooks', file),
|
|
84
|
+
// TS-source dev fallback: src/commands/ → ../../dist/hooks/<file>
|
|
85
|
+
resolve(__dirname, '../../dist/hooks', file),
|
|
86
|
+
];
|
|
87
|
+
for (const candidate of candidates) {
|
|
88
|
+
if (existsSync(candidate)) {
|
|
89
|
+
return candidate;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Hook file not found for "${hookName}". Searched: ${candidates.join(', ')}. ` +
|
|
94
|
+
'This indicates a broken @massu/core install. Re-run `npx -y @massu/core init`.',
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Subcommand entrypoint. Spawns `node <resolved-hook-file>` as a child
|
|
100
|
+
* with stdin/stdout/stderr piped through, so the hook receives the same
|
|
101
|
+
* JSON-on-stdin contract Claude Code expects, and stdout/stderr surface
|
|
102
|
+
* to Claude Code unchanged.
|
|
103
|
+
*
|
|
104
|
+
* Returns the child's exit code (or 2 on resolution error before spawn).
|
|
105
|
+
*/
|
|
106
|
+
export async function runHookRunner(args: string[]): Promise<{ exitCode: number }> {
|
|
107
|
+
const hookName = args[0];
|
|
108
|
+
if (!hookName) {
|
|
109
|
+
process.stderr.write(
|
|
110
|
+
'massu hook-runner: missing hook name.\n' +
|
|
111
|
+
'Usage: massu hook-runner <hook-name>\n' +
|
|
112
|
+
`Recognized: ${Object.keys(HOOK_NAME_TO_FILE).join(', ')}\n`,
|
|
113
|
+
);
|
|
114
|
+
return { exitCode: 2 };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let hookFile: string;
|
|
118
|
+
try {
|
|
119
|
+
hookFile = resolveHookFile(hookName);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
process.stderr.write(`massu hook-runner: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
122
|
+
return { exitCode: 2 };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return new Promise((resolvePromise) => {
|
|
126
|
+
const child = spawn(process.execPath, [hookFile], {
|
|
127
|
+
stdio: ['inherit', 'inherit', 'inherit'],
|
|
128
|
+
env: process.env,
|
|
129
|
+
});
|
|
130
|
+
child.on('exit', (code, signal) => {
|
|
131
|
+
if (signal) {
|
|
132
|
+
// Mirror typical shell convention: 128 + signal number; signals are not
|
|
133
|
+
// easily mapped to numbers here without an explicit table, so we just
|
|
134
|
+
// report 128 as a sentinel "killed by signal".
|
|
135
|
+
resolvePromise({ exitCode: 128 });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
resolvePromise({ exitCode: code ?? 0 });
|
|
139
|
+
});
|
|
140
|
+
child.on('error', (err) => {
|
|
141
|
+
process.stderr.write(`massu hook-runner: failed to spawn hook "${hookName}": ${err.message}\n`);
|
|
142
|
+
resolvePromise({ exitCode: 2 });
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|