@massu/core 1.10.8 → 1.12.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/CHANGELOG.md +1303 -0
- package/dist/adapter.js +6 -4
- package/dist/cli.js +517 -397
- package/dist/detect/adapters/.bundle-shasums.json +4 -7
- package/dist/detect/adapters/rails.js +6 -4
- package/dist/detect/adapters/spring.js +6 -4
- package/dist/detect/adapters/tree-sitter-loader.js +6 -4
- package/dist/hooks/auto-learning-pipeline.js +24 -1
- package/dist/hooks/classify-failure.js +32 -3
- package/dist/hooks/cost-tracker.js +24 -0
- package/dist/hooks/fix-detector.js +97 -1
- package/dist/hooks/incident-pipeline.js +30 -1
- package/dist/hooks/intent-suggester.js +6 -3
- package/dist/hooks/post-edit-context.js +24 -1
- package/dist/hooks/post-tool-use.js +66 -18
- package/dist/hooks/pre-compact.js +27 -2
- package/dist/hooks/pre-delete-check.js +48 -22
- package/dist/hooks/pre-tool-use-gate.js +962 -0
- package/dist/hooks/quality-event.js +24 -0
- package/dist/hooks/rule-enforcement-pipeline.js +24 -1
- package/dist/hooks/security-gate.js +29 -16
- package/dist/hooks/session-end.js +24 -0
- package/dist/hooks/session-start.js +8710 -8460
- package/dist/hooks/user-prompt.js +24 -0
- package/package.json +15 -18
- package/commands/README.md +0 -134
- package/src/adapter.ts +0 -121
- package/src/adr-generator.ts +0 -292
- package/src/analytics.ts +0 -373
- package/src/audit-trail.ts +0 -450
- package/src/backfill-sessions.ts +0 -182
- package/src/changelog-generator.ts +0 -178
- package/src/cli.ts +0 -246
- package/src/cloud-sync.ts +0 -234
- package/src/commands/adapters.ts +0 -824
- package/src/commands/changelog.ts +0 -165
- package/src/commands/config-check-drift.ts +0 -133
- package/src/commands/config-refresh.ts +0 -396
- package/src/commands/config-upgrade.ts +0 -127
- package/src/commands/doctor.ts +0 -485
- package/src/commands/hook-runner.ts +0 -145
- package/src/commands/init.ts +0 -1823
- package/src/commands/install-commands.ts +0 -692
- package/src/commands/install-hooks.ts +0 -27
- package/src/commands/permissions.ts +0 -150
- package/src/commands/refresh-log.ts +0 -37
- package/src/commands/show-template.ts +0 -65
- package/src/commands/template-engine.ts +0 -301
- package/src/commands/watch.ts +0 -430
- package/src/config.ts +0 -765
- package/src/cost-tracker.ts +0 -355
- package/src/db.ts +0 -370
- package/src/dependency-scorer.ts +0 -337
- package/src/detect/adapters/aspnet.ts +0 -4
- package/src/detect/adapters/discover.ts +0 -469
- package/src/detect/adapters/file-sampler.ts +0 -225
- package/src/detect/adapters/go-chi.ts +0 -4
- package/src/detect/adapters/index.ts +0 -51
- package/src/detect/adapters/nextjs-trpc.ts +0 -166
- package/src/detect/adapters/parse-guard.ts +0 -133
- package/src/detect/adapters/phoenix.ts +0 -4
- package/src/detect/adapters/python-django.ts +0 -208
- package/src/detect/adapters/python-fastapi.ts +0 -223
- package/src/detect/adapters/python-flask.ts +0 -235
- package/src/detect/adapters/query-helpers.ts +0 -170
- package/src/detect/adapters/rails.ts +0 -4
- package/src/detect/adapters/runner.ts +0 -284
- package/src/detect/adapters/spring.ts +0 -4
- package/src/detect/adapters/swift-swiftui.ts +0 -171
- package/src/detect/adapters/tree-sitter-loader.ts +0 -517
- package/src/detect/adapters/types.ts +0 -191
- package/src/detect/codebase-introspector.ts +0 -210
- package/src/detect/domain-inferrer.ts +0 -197
- package/src/detect/drift.ts +0 -199
- package/src/detect/framework-detector.ts +0 -307
- package/src/detect/index.ts +0 -209
- package/src/detect/manifest-registry.ts +0 -261
- package/src/detect/migrate.ts +0 -362
- package/src/detect/monorepo-detector.ts +0 -348
- package/src/detect/package-detector.ts +0 -828
- package/src/detect/passthrough.ts +0 -108
- package/src/detect/regex-fallback.ts +0 -449
- package/src/detect/source-dir-detector.ts +0 -297
- package/src/detect/vr-command-map.ts +0 -167
- package/src/docs-map.json +0 -100
- package/src/docs-tools.ts +0 -519
- package/src/domains.ts +0 -182
- package/src/hooks/auto-learning-pipeline.ts +0 -228
- package/src/hooks/classify-failure.ts +0 -259
- package/src/hooks/cost-tracker.ts +0 -66
- package/src/hooks/fix-detector.ts +0 -186
- package/src/hooks/incident-pipeline.ts +0 -190
- package/src/hooks/intent-suggester.ts +0 -131
- package/src/hooks/post-edit-context.ts +0 -91
- package/src/hooks/post-tool-use.ts +0 -413
- package/src/hooks/pre-compact.ts +0 -168
- package/src/hooks/pre-delete-check.ts +0 -243
- package/src/hooks/quality-event.ts +0 -127
- package/src/hooks/rule-enforcement-pipeline.ts +0 -159
- package/src/hooks/security-gate.ts +0 -153
- package/src/hooks/session-end.ts +0 -467
- package/src/hooks/session-start.ts +0 -436
- package/src/hooks/user-prompt.ts +0 -157
- package/src/import-resolver.ts +0 -226
- package/src/knowledge-db.ts +0 -176
- package/src/knowledge-indexer.ts +0 -708
- package/src/knowledge-tools.ts +0 -1414
- package/src/lib/fileLock.ts +0 -203
- package/src/lib/framework-supports.ts +0 -85
- package/src/lib/gitToplevel.ts +0 -22
- package/src/lib/hook-registry.ts +0 -43
- package/src/lib/installLock.ts +0 -66
- package/src/lib/memory-path.ts +0 -49
- package/src/lib/pidLiveness.ts +0 -67
- package/src/lib/settings-local.ts +0 -110
- package/src/lib/sql-table-names.ts +0 -71
- package/src/license.ts +0 -547
- package/src/lsp/auto-detect.ts +0 -98
- package/src/lsp/client.ts +0 -776
- package/src/lsp/enrich.ts +0 -127
- package/src/lsp/types.ts +0 -221
- package/src/memory-db.ts +0 -1614
- package/src/memory-file-ingest.ts +0 -128
- package/src/memory-tools.ts +0 -424
- package/src/middleware-tree.ts +0 -71
- package/src/observability-tools.ts +0 -343
- package/src/observation-extractor.ts +0 -418
- package/src/page-deps.ts +0 -285
- package/src/permissions.ts +0 -422
- package/src/prompt-analyzer.ts +0 -332
- package/src/python/coupling-detector.ts +0 -125
- package/src/python/domain-enforcer.ts +0 -84
- package/src/python/impact-analyzer.ts +0 -96
- package/src/python/import-parser.ts +0 -244
- package/src/python/import-resolver.ts +0 -136
- package/src/python/migration-indexer.ts +0 -116
- package/src/python/migration-parser.ts +0 -332
- package/src/python/model-indexer.ts +0 -71
- package/src/python/model-parser.ts +0 -279
- package/src/python/route-indexer.ts +0 -59
- package/src/python/route-parser.ts +0 -317
- package/src/python-tools.ts +0 -630
- package/src/regression-detector.ts +0 -319
- package/src/rules.ts +0 -57
- package/src/schema-mapper.ts +0 -232
- package/src/security/adapter-origin.ts +0 -130
- package/src/security/adapter-verifier.ts +0 -319
- package/src/security/atomic-write.ts +0 -164
- package/src/security/fetcher.ts +0 -200
- package/src/security/install-tracking.ts +0 -319
- package/src/security/license-pubkey.generated.ts +0 -30
- package/src/security/license-response-verifier.ts +0 -149
- package/src/security/local-fingerprint.ts +0 -225
- package/src/security/manifest-cache.ts +0 -333
- package/src/security/manifest-schema.ts +0 -129
- package/src/security/registry-pubkey.generated.ts +0 -35
- package/src/security/telemetry.ts +0 -320
- package/src/security-scorer.ts +0 -405
- package/src/security-utils.ts +0 -133
- package/src/sentinel-db.ts +0 -580
- package/src/sentinel-scanner.ts +0 -406
- package/src/sentinel-tools.ts +0 -513
- package/src/sentinel-types.ts +0 -140
- package/src/server-dispatch.ts +0 -225
- package/src/server.ts +0 -107
- package/src/session-archiver.ts +0 -111
- package/src/session-state-generator.ts +0 -174
- package/src/team-knowledge.ts +0 -407
- package/src/tool-db-needs.ts +0 -232
- package/src/tools.ts +0 -1206
- package/src/transcript-parser.ts +0 -458
- package/src/trpc-index.ts +0 -215
- package/src/validate-features-runner.ts +0 -107
- package/src/validation-engine.ts +0 -358
- package/src/watch/daemon.ts +0 -385
- package/src/watch/lockfile-detector.ts +0 -65
- package/src/watch/paths.ts +0 -279
- package/src/watch/state.ts +0 -178
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Release-boundary CHANGELOG entry generator. Reads `git log <last-tag>..HEAD`
|
|
6
|
-
* commit subjects, groups by `(plan-<token>)` paren-notation prefix, looks up
|
|
7
|
-
* the matching `docs/plans/*.md` files for their `## Changelog Summary` section,
|
|
8
|
-
* and emits a Keep-a-Changelog 1.1.0-compliant entry.
|
|
9
|
-
*
|
|
10
|
-
* Plan-token regex mirrors `scripts/lib/plan-token-regex.sh` (SSOT). See
|
|
11
|
-
* plan-1.9.0-plan-token-aware-changelog-batcher Phase B.
|
|
12
|
-
*
|
|
13
|
-
* Pure functions only — caller threads in commit subjects + plan directory.
|
|
14
|
-
* No git invocations inside this module (those live in commands/changelog.ts).
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
18
|
-
import { resolve } from 'path';
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Plan-token regex (TS shim of scripts/lib/plan-token-regex.sh PLAN_TOKEN_REGEX).
|
|
22
|
-
* Matches subject prefix `<type>(plan-<token>):` where <type> ∈ {feat, fix, chore, docs}.
|
|
23
|
-
* Captures: group 1 = type, group 2 = full plan-<token>.
|
|
24
|
-
*/
|
|
25
|
-
const PLAN_TOKEN_RE = /^(feat|fix|chore|docs)\((plan-[a-z0-9._-]+)\)/;
|
|
26
|
-
|
|
27
|
-
export class MissingPlanFileError extends Error {
|
|
28
|
-
constructor(token: string) {
|
|
29
|
-
super(`No plan file found in plans directory matching Plan Token: ${token}`);
|
|
30
|
-
this.name = 'MissingPlanFileError';
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export class MissingChangelogSummaryError extends Error {
|
|
35
|
-
constructor(token: string, planFile: string) {
|
|
36
|
-
super(`Plan file ${planFile} for token ${token} has no '## Changelog Summary' section`);
|
|
37
|
-
this.name = 'MissingChangelogSummaryError';
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface PlanSummary {
|
|
42
|
-
title: string;
|
|
43
|
-
summary: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface ParseResult {
|
|
47
|
-
tokens: Set<string>;
|
|
48
|
-
maintenance: string[];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Parse commit subject lines into:
|
|
53
|
-
* - `tokens`: Set of unique `plan-<token>` strings (with `plan-` prefix)
|
|
54
|
-
* - `maintenance`: subjects that DID NOT match the paren-notation pattern
|
|
55
|
-
*/
|
|
56
|
-
export function parseCommitsForPlanTokens(subjects: readonly string[]): ParseResult {
|
|
57
|
-
const tokens = new Set<string>();
|
|
58
|
-
const maintenance: string[] = [];
|
|
59
|
-
for (const subject of subjects) {
|
|
60
|
-
const m = subject.match(PLAN_TOKEN_RE);
|
|
61
|
-
if (m && m[2]) {
|
|
62
|
-
tokens.add(m[2]);
|
|
63
|
-
} else {
|
|
64
|
-
maintenance.push(subject);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return { tokens, maintenance };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* For each plan-token, find the corresponding `docs/plans/*.md` file and
|
|
72
|
-
* extract its title (first `# Plan: ...` heading) and `## Changelog Summary`
|
|
73
|
-
* section body. Throws on missing file or missing section.
|
|
74
|
-
*/
|
|
75
|
-
export function loadPlanSummaries(
|
|
76
|
-
tokens: ReadonlySet<string>,
|
|
77
|
-
planDir: string,
|
|
78
|
-
): Map<string, PlanSummary> {
|
|
79
|
-
const result = new Map<string, PlanSummary>();
|
|
80
|
-
if (tokens.size === 0) return result;
|
|
81
|
-
|
|
82
|
-
if (!existsSync(planDir)) {
|
|
83
|
-
throw new Error(`Plan directory does not exist: ${planDir}`);
|
|
84
|
-
}
|
|
85
|
-
const files = readdirSync(planDir).filter((f) => f.endsWith('.md'));
|
|
86
|
-
|
|
87
|
-
for (const token of tokens) {
|
|
88
|
-
let matchedFile: string | null = null;
|
|
89
|
-
let content = '';
|
|
90
|
-
for (const file of files) {
|
|
91
|
-
const path = resolve(planDir, file);
|
|
92
|
-
const text = readFileSync(path, 'utf-8');
|
|
93
|
-
// Match `**Plan Token**: `plan-<token>`` allowing optional surrounding text.
|
|
94
|
-
const tokenRe = new RegExp(
|
|
95
|
-
`^\\*\\*Plan Token\\*\\*:\\s*\`?${token.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\`?(\\s|$)`,
|
|
96
|
-
'm',
|
|
97
|
-
);
|
|
98
|
-
if (tokenRe.test(text)) {
|
|
99
|
-
matchedFile = file;
|
|
100
|
-
content = text;
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
if (!matchedFile) {
|
|
105
|
-
throw new MissingPlanFileError(token);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Extract title: first line starting with `# ` (after the metadata block).
|
|
109
|
-
const titleMatch = content.match(/^# (.+)$/m);
|
|
110
|
-
const title = titleMatch ? titleMatch[1].trim() : token;
|
|
111
|
-
|
|
112
|
-
// Extract `## Changelog Summary` section body.
|
|
113
|
-
const sectionRe = /^## Changelog Summary\s*\n([\s\S]*?)(?=\n## |\n---|\n# |$)/m;
|
|
114
|
-
const sectionMatch = content.match(sectionRe);
|
|
115
|
-
if (!sectionMatch || !sectionMatch[1].trim()) {
|
|
116
|
-
throw new MissingChangelogSummaryError(token, matchedFile);
|
|
117
|
-
}
|
|
118
|
-
const summary = sectionMatch[1].trim();
|
|
119
|
-
result.set(token, { title, summary });
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return result;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export interface GenerateOptions {
|
|
126
|
-
version: string;
|
|
127
|
-
date: string;
|
|
128
|
-
planSummaries: ReadonlyMap<string, PlanSummary>;
|
|
129
|
-
maintenance: readonly string[];
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Emit a CHANGELOG.md entry in Keep-a-Changelog 1.1.0 format. Each plan-token
|
|
134
|
-
* becomes one paragraph (or `### Added` subsection if the plan summary itself
|
|
135
|
-
* uses Keep-a-Changelog subsections). Maintenance commits go under a
|
|
136
|
-
* `### Maintenance` subsection at the end.
|
|
137
|
-
*/
|
|
138
|
-
export function generateChangelogEntry(opts: GenerateOptions): string {
|
|
139
|
-
const parts: string[] = [];
|
|
140
|
-
parts.push(`## [${opts.version}] - ${opts.date}\n`);
|
|
141
|
-
parts.push('');
|
|
142
|
-
|
|
143
|
-
// Plan summaries, in iteration order
|
|
144
|
-
for (const [, planSum] of opts.planSummaries) {
|
|
145
|
-
parts.push(planSum.summary);
|
|
146
|
-
parts.push('');
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Maintenance fallback
|
|
150
|
-
if (opts.maintenance.length > 0) {
|
|
151
|
-
parts.push('### Maintenance');
|
|
152
|
-
parts.push('');
|
|
153
|
-
for (const subject of opts.maintenance) {
|
|
154
|
-
parts.push(`- ${subject}`);
|
|
155
|
-
}
|
|
156
|
-
parts.push('');
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return parts.join('\n') + '\n';
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Given a CHANGELOG.md entry body and a set of plan-tokens that should be
|
|
164
|
-
* referenced, return the subset of tokens NOT mentioned in the body.
|
|
165
|
-
* Used by `permissions check-drift` verification + pre-tag gate.
|
|
166
|
-
*/
|
|
167
|
-
export function findCoverageGaps(
|
|
168
|
-
entryText: string,
|
|
169
|
-
tokens: ReadonlySet<string>,
|
|
170
|
-
): string[] {
|
|
171
|
-
const gaps: string[] = [];
|
|
172
|
-
for (const token of tokens) {
|
|
173
|
-
if (!entryText.includes(token)) {
|
|
174
|
-
gaps.push(token);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
return gaps;
|
|
178
|
-
}
|
package/src/cli.ts
DELETED
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Massu CLI Entry Point
|
|
6
|
-
*
|
|
7
|
-
* Routes subcommands to handlers, falls through to MCP server mode
|
|
8
|
-
* when no subcommand is provided (backward compatible).
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
* npx massu init - Full project setup
|
|
12
|
-
* npx massu doctor - Health check
|
|
13
|
-
* npx massu install-hooks - Install hooks only
|
|
14
|
-
* npx massu validate-config - Validate configuration
|
|
15
|
-
* npx @massu/core - MCP server mode (no args)
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { readFileSync } from 'fs';
|
|
19
|
-
import { resolve, dirname } from 'path';
|
|
20
|
-
import { fileURLToPath } from 'url';
|
|
21
|
-
|
|
22
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
-
const __dirname = dirname(__filename);
|
|
24
|
-
|
|
25
|
-
const args = process.argv.slice(2);
|
|
26
|
-
const subcommand = args[0];
|
|
27
|
-
|
|
28
|
-
async function main(): Promise<void> {
|
|
29
|
-
switch (subcommand) {
|
|
30
|
-
case 'init': {
|
|
31
|
-
const { runInit } = await import('./commands/init.ts');
|
|
32
|
-
await runInit();
|
|
33
|
-
break;
|
|
34
|
-
}
|
|
35
|
-
case 'doctor': {
|
|
36
|
-
const { runDoctor } = await import('./commands/doctor.ts');
|
|
37
|
-
await runDoctor();
|
|
38
|
-
break;
|
|
39
|
-
}
|
|
40
|
-
case 'install-hooks': {
|
|
41
|
-
const { runInstallHooks } = await import('./commands/install-hooks.ts');
|
|
42
|
-
await runInstallHooks();
|
|
43
|
-
break;
|
|
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
|
-
}
|
|
55
|
-
case 'install-commands': {
|
|
56
|
-
const { runInstallCommands } = await import('./commands/install-commands.ts');
|
|
57
|
-
await runInstallCommands();
|
|
58
|
-
break;
|
|
59
|
-
}
|
|
60
|
-
case 'permissions': {
|
|
61
|
-
const { handlePermissionsSubcommand } = await import('./commands/permissions.ts');
|
|
62
|
-
const result = await handlePermissionsSubcommand(args.slice(1));
|
|
63
|
-
process.exit(result.exitCode);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
case 'changelog': {
|
|
67
|
-
const { handleChangelogSubcommand } = await import('./commands/changelog.ts');
|
|
68
|
-
const result = await handleChangelogSubcommand(args.slice(1));
|
|
69
|
-
process.exit(result.exitCode);
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
case 'show-template': {
|
|
73
|
-
const { runShowTemplate } = await import('./commands/show-template.ts');
|
|
74
|
-
await runShowTemplate(args.slice(1));
|
|
75
|
-
break;
|
|
76
|
-
}
|
|
77
|
-
case 'watch': {
|
|
78
|
-
const { runWatch } = await import('./commands/watch.ts');
|
|
79
|
-
const result = await runWatch(args.slice(1));
|
|
80
|
-
process.exit(result.exitCode);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
case 'refresh-log': {
|
|
84
|
-
const { runRefreshLog } = await import('./commands/refresh-log.ts');
|
|
85
|
-
const result = await runRefreshLog(args.slice(1));
|
|
86
|
-
process.exit(result.exitCode);
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
case 'validate-config': {
|
|
90
|
-
const { runValidateConfig } = await import('./commands/doctor.ts');
|
|
91
|
-
await runValidateConfig();
|
|
92
|
-
break;
|
|
93
|
-
}
|
|
94
|
-
case 'config': {
|
|
95
|
-
await handleConfigSubcommand(args.slice(1));
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
98
|
-
case 'adapters': {
|
|
99
|
-
const { handleAdaptersSubcommand } = await import('./commands/adapters.ts');
|
|
100
|
-
const result = await handleAdaptersSubcommand(args.slice(1));
|
|
101
|
-
process.exit(result.exitCode);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
case '--help':
|
|
105
|
-
case '-h': {
|
|
106
|
-
printHelp();
|
|
107
|
-
break;
|
|
108
|
-
}
|
|
109
|
-
case '--version':
|
|
110
|
-
case '-v': {
|
|
111
|
-
printVersion();
|
|
112
|
-
break;
|
|
113
|
-
}
|
|
114
|
-
default: {
|
|
115
|
-
// No subcommand or unknown: fall through to MCP server mode
|
|
116
|
-
// This maintains backward compatibility with `npx @massu/core`
|
|
117
|
-
await import('./server.ts');
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async function handleConfigSubcommand(configArgs: string[]): Promise<void> {
|
|
123
|
-
const sub = configArgs[0];
|
|
124
|
-
const flags = new Set(configArgs.slice(1));
|
|
125
|
-
switch (sub) {
|
|
126
|
-
case 'refresh': {
|
|
127
|
-
const { runConfigRefresh } = await import('./commands/config-refresh.ts');
|
|
128
|
-
const result = await runConfigRefresh({
|
|
129
|
-
dryRun: flags.has('--dry-run'),
|
|
130
|
-
skipCommands: flags.has('--skip-commands'),
|
|
131
|
-
autoYes: flags.has('--yes') || flags.has('-y'),
|
|
132
|
-
});
|
|
133
|
-
process.exit(result.exitCode);
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
case 'validate': {
|
|
137
|
-
const { runValidateConfig } = await import('./commands/doctor.ts');
|
|
138
|
-
await runValidateConfig();
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
case 'upgrade': {
|
|
142
|
-
const { runConfigUpgrade } = await import('./commands/config-upgrade.ts');
|
|
143
|
-
const result = await runConfigUpgrade({
|
|
144
|
-
rollback: flags.has('--rollback'),
|
|
145
|
-
ci: flags.has('--ci') || flags.has('--yes'),
|
|
146
|
-
});
|
|
147
|
-
process.exit(result.exitCode);
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
case 'doctor': {
|
|
151
|
-
const { runDoctor } = await import('./commands/doctor.ts');
|
|
152
|
-
await runDoctor();
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
case 'check-drift': {
|
|
156
|
-
const { runConfigCheckDrift } = await import('./commands/config-check-drift.ts');
|
|
157
|
-
const result = await runConfigCheckDrift({ verbose: flags.has('--verbose') });
|
|
158
|
-
process.exit(result.exitCode);
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
case '--help':
|
|
162
|
-
case '-h':
|
|
163
|
-
case undefined: {
|
|
164
|
-
printConfigHelp();
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
default: {
|
|
168
|
-
process.stderr.write(`massu: unknown config subcommand: ${sub}\n`);
|
|
169
|
-
printConfigHelp();
|
|
170
|
-
process.exit(1);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function printHelp(): void {
|
|
177
|
-
console.log(`
|
|
178
|
-
Massu AI - Engineering Governance Platform
|
|
179
|
-
|
|
180
|
-
Usage:
|
|
181
|
-
massu <command>
|
|
182
|
-
|
|
183
|
-
Commands:
|
|
184
|
-
init Set up Massu AI in your project (one command, full setup)
|
|
185
|
-
doctor Check installation health
|
|
186
|
-
install-hooks Install/update Claude Code hooks
|
|
187
|
-
install-commands Install/update slash commands (use --skip-permissions to opt out of MCP allowlist seeding)
|
|
188
|
-
show-template Print the resolved variant of a bundled template (e.g. for diffs)
|
|
189
|
-
watch Run the file-watcher daemon (auto-refresh on stack changes)
|
|
190
|
-
refresh-log [N] Show the last N watcher auto-refresh events
|
|
191
|
-
validate-config Validate massu.config.yaml (alias: config validate)
|
|
192
|
-
config <sub> Config lifecycle: refresh | validate | upgrade | doctor | check-drift
|
|
193
|
-
permissions <sub> MCP permission lifecycle: install | verify | check-drift
|
|
194
|
-
changelog <sub> CHANGELOG generation / verification: generate | verify
|
|
195
|
-
adapters <sub> Third-party adapter registry: list | refresh | search | add-local | remove-local | install | resign
|
|
196
|
-
|
|
197
|
-
Options:
|
|
198
|
-
--help, -h Show this help message
|
|
199
|
-
--version, -v Show version
|
|
200
|
-
|
|
201
|
-
Getting started:
|
|
202
|
-
npx massu init # Full setup in one command
|
|
203
|
-
npx massu init --help # Show all init options (--ci, --force, --template)
|
|
204
|
-
npx massu config --help # Show config subcommands
|
|
205
|
-
|
|
206
|
-
Documentation: https://massu.ai/docs
|
|
207
|
-
`);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function printConfigHelp(): void {
|
|
211
|
-
console.log(`
|
|
212
|
-
massu config <subcommand>
|
|
213
|
-
|
|
214
|
-
Subcommands:
|
|
215
|
-
refresh Re-run detection and apply changes to massu.config.yaml.
|
|
216
|
-
--dry-run Print diff and exit without writing.
|
|
217
|
-
--skip-commands Don't re-template .claude/commands/.
|
|
218
|
-
--yes, -y Auto-apply without prompting (CI / scripts).
|
|
219
|
-
validate Validate massu.config.yaml (alias of \`massu validate-config\`).
|
|
220
|
-
upgrade Migrate a v1 config to schema_version=2.
|
|
221
|
-
--rollback Restore from .bak file.
|
|
222
|
-
--ci, --yes Non-interactive mode (no prompts).
|
|
223
|
-
doctor Run the full health check (alias of \`massu doctor\`).
|
|
224
|
-
check-drift CI-safe drift gate; exits 1 on drift.
|
|
225
|
-
--verbose Print detailed changes to stdout.
|
|
226
|
-
|
|
227
|
-
Examples:
|
|
228
|
-
npx massu config refresh --dry-run
|
|
229
|
-
npx massu config upgrade --ci
|
|
230
|
-
npx massu config check-drift --verbose
|
|
231
|
-
`);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function printVersion(): void {
|
|
235
|
-
try {
|
|
236
|
-
const pkg = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf-8'));
|
|
237
|
-
console.log(`massu v${pkg.version}`);
|
|
238
|
-
} catch {
|
|
239
|
-
console.log('massu v0.1.0');
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
main().catch((err) => {
|
|
244
|
-
console.error(`massu: ${err instanceof Error ? err.message : String(err)}`);
|
|
245
|
-
process.exit(1);
|
|
246
|
-
});
|
package/src/cloud-sync.ts
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
import type Database from 'better-sqlite3';
|
|
5
|
-
import { getConfig } from './config.ts';
|
|
6
|
-
import {
|
|
7
|
-
enqueueSyncPayload,
|
|
8
|
-
dequeuePendingSync,
|
|
9
|
-
removePendingSync,
|
|
10
|
-
incrementRetryCount,
|
|
11
|
-
} from './memory-db.ts';
|
|
12
|
-
import { classifyVisibility } from './observation-extractor.ts';
|
|
13
|
-
|
|
14
|
-
// ============================================================
|
|
15
|
-
// Cloud Sync Module
|
|
16
|
-
// Internal module — NOT an MCP tool. Called by session-end hook.
|
|
17
|
-
// ============================================================
|
|
18
|
-
|
|
19
|
-
export interface SyncPayload {
|
|
20
|
-
sessions?: Array<{
|
|
21
|
-
local_session_id: string;
|
|
22
|
-
project_name?: string;
|
|
23
|
-
summary?: string;
|
|
24
|
-
started_at?: string;
|
|
25
|
-
ended_at?: string;
|
|
26
|
-
turns?: number;
|
|
27
|
-
tokens_used?: number;
|
|
28
|
-
estimated_cost?: number;
|
|
29
|
-
tools_used?: string[];
|
|
30
|
-
}>;
|
|
31
|
-
observations?: Array<{
|
|
32
|
-
local_observation_id: string;
|
|
33
|
-
session_id?: string;
|
|
34
|
-
type: string;
|
|
35
|
-
content: string;
|
|
36
|
-
importance: number;
|
|
37
|
-
file_path?: string;
|
|
38
|
-
}>;
|
|
39
|
-
analytics?: Array<{
|
|
40
|
-
event_type: string;
|
|
41
|
-
event_data: Record<string, unknown>;
|
|
42
|
-
}>;
|
|
43
|
-
audit?: Array<{
|
|
44
|
-
action: string;
|
|
45
|
-
resource?: string;
|
|
46
|
-
details: Record<string, unknown>;
|
|
47
|
-
}>;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface SyncResult {
|
|
51
|
-
success: boolean;
|
|
52
|
-
synced: {
|
|
53
|
-
sessions: number;
|
|
54
|
-
observations: number;
|
|
55
|
-
analytics: number;
|
|
56
|
-
audit: number;
|
|
57
|
-
};
|
|
58
|
-
error?: string;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const MAX_RETRIES = 3;
|
|
62
|
-
const RETRY_DELAYS = [1000, 2000, 4000]; // exponential backoff
|
|
63
|
-
|
|
64
|
-
// P-H003 (plan-stage-c-high-batch): bound each HTTP request so offline
|
|
65
|
-
// customers don't burn the entire Stop-hook 15s budget on a single
|
|
66
|
-
// unreachable endpoint. Default 2_000ms is well under hook timeout while
|
|
67
|
-
// still tolerating typical latency. Override via config.cloud.requestTimeoutMs.
|
|
68
|
-
const DEFAULT_CLOUD_REQUEST_TIMEOUT_MS = 2_000;
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Sync data to the cloud endpoint.
|
|
72
|
-
* Respects config flags for selective sync.
|
|
73
|
-
* On failure after retries, enqueues payload for later retry.
|
|
74
|
-
*/
|
|
75
|
-
export async function syncToCloud(
|
|
76
|
-
db: Database.Database,
|
|
77
|
-
payload: SyncPayload
|
|
78
|
-
): Promise<SyncResult> {
|
|
79
|
-
const config = getConfig();
|
|
80
|
-
const cloud = config.cloud;
|
|
81
|
-
|
|
82
|
-
// Check if cloud sync is enabled
|
|
83
|
-
if (!cloud?.enabled) {
|
|
84
|
-
return { success: true, synced: { sessions: 0, observations: 0, analytics: 0, audit: 0 } };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Check API key
|
|
88
|
-
if (!cloud.apiKey) {
|
|
89
|
-
return { success: false, synced: { sessions: 0, observations: 0, analytics: 0, audit: 0 }, error: 'No API key configured' };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Check endpoint
|
|
93
|
-
const endpoint = cloud.endpoint;
|
|
94
|
-
if (!endpoint) {
|
|
95
|
-
return { success: false, synced: { sessions: 0, observations: 0, analytics: 0, audit: 0 }, error: 'No sync endpoint configured' };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Apply selective sync filters
|
|
99
|
-
const filteredPayload: SyncPayload = {};
|
|
100
|
-
if (cloud.sync?.memory !== false) {
|
|
101
|
-
filteredPayload.sessions = payload.sessions;
|
|
102
|
-
// P-H020 (plan-stage-c-high-batch): consume classifyVisibility() to drop
|
|
103
|
-
// observations whose title/detail matches PRIVATE_PATTERNS (Stripe keys,
|
|
104
|
-
// env var names, file paths, Bearer tokens, etc.). Pre-fix: cloud-sync
|
|
105
|
-
// transmitted EVERY observation to Massu's Supabase, leaking customer
|
|
106
|
-
// secrets and absolute file paths.
|
|
107
|
-
if (payload.observations) {
|
|
108
|
-
let droppedPrivate = 0;
|
|
109
|
-
filteredPayload.observations = payload.observations.filter((obs) => {
|
|
110
|
-
if (classifyVisibility(obs.content ?? '', obs.content ?? '') === 'private') {
|
|
111
|
-
droppedPrivate += 1;
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
// Belt-and-suspenders: also drop if file_path matches private patterns.
|
|
115
|
-
if (obs.file_path && classifyVisibility(obs.file_path, obs.file_path) === 'private') {
|
|
116
|
-
droppedPrivate += 1;
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
|
-
return true;
|
|
120
|
-
});
|
|
121
|
-
if (droppedPrivate > 0) {
|
|
122
|
-
// Surface to the customer's stderr so they can audit what got filtered.
|
|
123
|
-
process.stderr.write(
|
|
124
|
-
`[massu] cloud-sync: dropped ${droppedPrivate} private observation(s) (PRIVATE_PATTERNS match)\n`,
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
if (cloud.sync?.analytics !== false) {
|
|
130
|
-
filteredPayload.analytics = payload.analytics;
|
|
131
|
-
}
|
|
132
|
-
if (cloud.sync?.audit !== false) {
|
|
133
|
-
filteredPayload.audit = payload.audit;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Attempt sync with retry
|
|
137
|
-
let lastError = '';
|
|
138
|
-
const requestTimeoutMs = (cloud as { requestTimeoutMs?: number }).requestTimeoutMs
|
|
139
|
-
?? DEFAULT_CLOUD_REQUEST_TIMEOUT_MS;
|
|
140
|
-
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
141
|
-
try {
|
|
142
|
-
const response = await fetch(endpoint, {
|
|
143
|
-
method: 'POST',
|
|
144
|
-
headers: {
|
|
145
|
-
'Content-Type': 'application/json',
|
|
146
|
-
'Authorization': `Bearer ${cloud.apiKey}`,
|
|
147
|
-
},
|
|
148
|
-
body: JSON.stringify(filteredPayload),
|
|
149
|
-
// P-H003: bounded request — AbortSignal.timeout fires AbortError when
|
|
150
|
-
// the request stalls (DNS failure, TCP unreachable, slow server). Cleans
|
|
151
|
-
// up before hook timeout kills the whole process.
|
|
152
|
-
signal: AbortSignal.timeout(requestTimeoutMs),
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
if (!response.ok) {
|
|
156
|
-
lastError = `HTTP ${response.status}: ${response.statusText}`;
|
|
157
|
-
if (response.status >= 400 && response.status < 500) {
|
|
158
|
-
// Client errors are not retryable
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
// Server errors — retry
|
|
162
|
-
if (attempt < MAX_RETRIES - 1) {
|
|
163
|
-
await sleep(RETRY_DELAYS[attempt]);
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const result = await response.json() as { synced?: { sessions?: number; observations?: number; analytics?: number } };
|
|
170
|
-
return {
|
|
171
|
-
success: true,
|
|
172
|
-
synced: {
|
|
173
|
-
sessions: result.synced?.sessions ?? 0,
|
|
174
|
-
observations: result.synced?.observations ?? 0,
|
|
175
|
-
analytics: result.synced?.analytics ?? 0,
|
|
176
|
-
audit: 0,
|
|
177
|
-
},
|
|
178
|
-
};
|
|
179
|
-
} catch (err) {
|
|
180
|
-
lastError = err instanceof Error ? err.message : String(err);
|
|
181
|
-
// P-H003: AbortError from AbortSignal.timeout means the request stalled
|
|
182
|
-
// (customer offline / DNS failure / unreachable). Don't burn the remaining
|
|
183
|
-
// hook budget retrying; queue for later and bail.
|
|
184
|
-
if (err instanceof Error && (err.name === 'AbortError' || err.name === 'TimeoutError')) {
|
|
185
|
-
break;
|
|
186
|
-
}
|
|
187
|
-
if (attempt < MAX_RETRIES - 1) {
|
|
188
|
-
await sleep(RETRY_DELAYS[attempt]);
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// All retries exhausted — enqueue for later
|
|
195
|
-
try {
|
|
196
|
-
enqueueSyncPayload(db, JSON.stringify(payload));
|
|
197
|
-
} catch (_e) {
|
|
198
|
-
// Best effort — don't crash if queue fails
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return {
|
|
202
|
-
success: false,
|
|
203
|
-
synced: { sessions: 0, observations: 0, analytics: 0, audit: 0 },
|
|
204
|
-
error: lastError,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Drain the pending sync queue. Processes items oldest-first.
|
|
210
|
-
* Successfully synced items are removed; failed items get their retry count incremented.
|
|
211
|
-
*/
|
|
212
|
-
export async function drainSyncQueue(db: Database.Database): Promise<void> {
|
|
213
|
-
const config = getConfig();
|
|
214
|
-
if (!config.cloud?.enabled || !config.cloud?.apiKey) return;
|
|
215
|
-
|
|
216
|
-
const pending = dequeuePendingSync(db, 10);
|
|
217
|
-
for (const item of pending) {
|
|
218
|
-
try {
|
|
219
|
-
const payload = JSON.parse(item.payload) as SyncPayload;
|
|
220
|
-
const result = await syncToCloud(db, payload);
|
|
221
|
-
if (result.success) {
|
|
222
|
-
removePendingSync(db, item.id);
|
|
223
|
-
} else {
|
|
224
|
-
incrementRetryCount(db, item.id, result.error ?? 'Unknown error');
|
|
225
|
-
}
|
|
226
|
-
} catch (err) {
|
|
227
|
-
incrementRetryCount(db, item.id, err instanceof Error ? err.message : String(err));
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function sleep(ms: number): Promise<void> {
|
|
233
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
234
|
-
}
|