@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.
Files changed (178) hide show
  1. package/CHANGELOG.md +1303 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +517 -397
  4. package/dist/detect/adapters/.bundle-shasums.json +4 -7
  5. package/dist/detect/adapters/rails.js +6 -4
  6. package/dist/detect/adapters/spring.js +6 -4
  7. package/dist/detect/adapters/tree-sitter-loader.js +6 -4
  8. package/dist/hooks/auto-learning-pipeline.js +24 -1
  9. package/dist/hooks/classify-failure.js +32 -3
  10. package/dist/hooks/cost-tracker.js +24 -0
  11. package/dist/hooks/fix-detector.js +97 -1
  12. package/dist/hooks/incident-pipeline.js +30 -1
  13. package/dist/hooks/intent-suggester.js +6 -3
  14. package/dist/hooks/post-edit-context.js +24 -1
  15. package/dist/hooks/post-tool-use.js +66 -18
  16. package/dist/hooks/pre-compact.js +27 -2
  17. package/dist/hooks/pre-delete-check.js +48 -22
  18. package/dist/hooks/pre-tool-use-gate.js +962 -0
  19. package/dist/hooks/quality-event.js +24 -0
  20. package/dist/hooks/rule-enforcement-pipeline.js +24 -1
  21. package/dist/hooks/security-gate.js +29 -16
  22. package/dist/hooks/session-end.js +24 -0
  23. package/dist/hooks/session-start.js +8710 -8460
  24. package/dist/hooks/user-prompt.js +24 -0
  25. package/package.json +15 -18
  26. package/commands/README.md +0 -134
  27. package/src/adapter.ts +0 -121
  28. package/src/adr-generator.ts +0 -292
  29. package/src/analytics.ts +0 -373
  30. package/src/audit-trail.ts +0 -450
  31. package/src/backfill-sessions.ts +0 -182
  32. package/src/changelog-generator.ts +0 -178
  33. package/src/cli.ts +0 -246
  34. package/src/cloud-sync.ts +0 -234
  35. package/src/commands/adapters.ts +0 -824
  36. package/src/commands/changelog.ts +0 -165
  37. package/src/commands/config-check-drift.ts +0 -133
  38. package/src/commands/config-refresh.ts +0 -396
  39. package/src/commands/config-upgrade.ts +0 -127
  40. package/src/commands/doctor.ts +0 -485
  41. package/src/commands/hook-runner.ts +0 -145
  42. package/src/commands/init.ts +0 -1823
  43. package/src/commands/install-commands.ts +0 -692
  44. package/src/commands/install-hooks.ts +0 -27
  45. package/src/commands/permissions.ts +0 -150
  46. package/src/commands/refresh-log.ts +0 -37
  47. package/src/commands/show-template.ts +0 -65
  48. package/src/commands/template-engine.ts +0 -301
  49. package/src/commands/watch.ts +0 -430
  50. package/src/config.ts +0 -765
  51. package/src/cost-tracker.ts +0 -355
  52. package/src/db.ts +0 -370
  53. package/src/dependency-scorer.ts +0 -337
  54. package/src/detect/adapters/aspnet.ts +0 -4
  55. package/src/detect/adapters/discover.ts +0 -469
  56. package/src/detect/adapters/file-sampler.ts +0 -225
  57. package/src/detect/adapters/go-chi.ts +0 -4
  58. package/src/detect/adapters/index.ts +0 -51
  59. package/src/detect/adapters/nextjs-trpc.ts +0 -166
  60. package/src/detect/adapters/parse-guard.ts +0 -133
  61. package/src/detect/adapters/phoenix.ts +0 -4
  62. package/src/detect/adapters/python-django.ts +0 -208
  63. package/src/detect/adapters/python-fastapi.ts +0 -223
  64. package/src/detect/adapters/python-flask.ts +0 -235
  65. package/src/detect/adapters/query-helpers.ts +0 -170
  66. package/src/detect/adapters/rails.ts +0 -4
  67. package/src/detect/adapters/runner.ts +0 -284
  68. package/src/detect/adapters/spring.ts +0 -4
  69. package/src/detect/adapters/swift-swiftui.ts +0 -171
  70. package/src/detect/adapters/tree-sitter-loader.ts +0 -517
  71. package/src/detect/adapters/types.ts +0 -191
  72. package/src/detect/codebase-introspector.ts +0 -210
  73. package/src/detect/domain-inferrer.ts +0 -197
  74. package/src/detect/drift.ts +0 -199
  75. package/src/detect/framework-detector.ts +0 -307
  76. package/src/detect/index.ts +0 -209
  77. package/src/detect/manifest-registry.ts +0 -261
  78. package/src/detect/migrate.ts +0 -362
  79. package/src/detect/monorepo-detector.ts +0 -348
  80. package/src/detect/package-detector.ts +0 -828
  81. package/src/detect/passthrough.ts +0 -108
  82. package/src/detect/regex-fallback.ts +0 -449
  83. package/src/detect/source-dir-detector.ts +0 -297
  84. package/src/detect/vr-command-map.ts +0 -167
  85. package/src/docs-map.json +0 -100
  86. package/src/docs-tools.ts +0 -519
  87. package/src/domains.ts +0 -182
  88. package/src/hooks/auto-learning-pipeline.ts +0 -228
  89. package/src/hooks/classify-failure.ts +0 -259
  90. package/src/hooks/cost-tracker.ts +0 -66
  91. package/src/hooks/fix-detector.ts +0 -186
  92. package/src/hooks/incident-pipeline.ts +0 -190
  93. package/src/hooks/intent-suggester.ts +0 -131
  94. package/src/hooks/post-edit-context.ts +0 -91
  95. package/src/hooks/post-tool-use.ts +0 -413
  96. package/src/hooks/pre-compact.ts +0 -168
  97. package/src/hooks/pre-delete-check.ts +0 -243
  98. package/src/hooks/quality-event.ts +0 -127
  99. package/src/hooks/rule-enforcement-pipeline.ts +0 -159
  100. package/src/hooks/security-gate.ts +0 -153
  101. package/src/hooks/session-end.ts +0 -467
  102. package/src/hooks/session-start.ts +0 -436
  103. package/src/hooks/user-prompt.ts +0 -157
  104. package/src/import-resolver.ts +0 -226
  105. package/src/knowledge-db.ts +0 -176
  106. package/src/knowledge-indexer.ts +0 -708
  107. package/src/knowledge-tools.ts +0 -1414
  108. package/src/lib/fileLock.ts +0 -203
  109. package/src/lib/framework-supports.ts +0 -85
  110. package/src/lib/gitToplevel.ts +0 -22
  111. package/src/lib/hook-registry.ts +0 -43
  112. package/src/lib/installLock.ts +0 -66
  113. package/src/lib/memory-path.ts +0 -49
  114. package/src/lib/pidLiveness.ts +0 -67
  115. package/src/lib/settings-local.ts +0 -110
  116. package/src/lib/sql-table-names.ts +0 -71
  117. package/src/license.ts +0 -547
  118. package/src/lsp/auto-detect.ts +0 -98
  119. package/src/lsp/client.ts +0 -776
  120. package/src/lsp/enrich.ts +0 -127
  121. package/src/lsp/types.ts +0 -221
  122. package/src/memory-db.ts +0 -1614
  123. package/src/memory-file-ingest.ts +0 -128
  124. package/src/memory-tools.ts +0 -424
  125. package/src/middleware-tree.ts +0 -71
  126. package/src/observability-tools.ts +0 -343
  127. package/src/observation-extractor.ts +0 -418
  128. package/src/page-deps.ts +0 -285
  129. package/src/permissions.ts +0 -422
  130. package/src/prompt-analyzer.ts +0 -332
  131. package/src/python/coupling-detector.ts +0 -125
  132. package/src/python/domain-enforcer.ts +0 -84
  133. package/src/python/impact-analyzer.ts +0 -96
  134. package/src/python/import-parser.ts +0 -244
  135. package/src/python/import-resolver.ts +0 -136
  136. package/src/python/migration-indexer.ts +0 -116
  137. package/src/python/migration-parser.ts +0 -332
  138. package/src/python/model-indexer.ts +0 -71
  139. package/src/python/model-parser.ts +0 -279
  140. package/src/python/route-indexer.ts +0 -59
  141. package/src/python/route-parser.ts +0 -317
  142. package/src/python-tools.ts +0 -630
  143. package/src/regression-detector.ts +0 -319
  144. package/src/rules.ts +0 -57
  145. package/src/schema-mapper.ts +0 -232
  146. package/src/security/adapter-origin.ts +0 -130
  147. package/src/security/adapter-verifier.ts +0 -319
  148. package/src/security/atomic-write.ts +0 -164
  149. package/src/security/fetcher.ts +0 -200
  150. package/src/security/install-tracking.ts +0 -319
  151. package/src/security/license-pubkey.generated.ts +0 -30
  152. package/src/security/license-response-verifier.ts +0 -149
  153. package/src/security/local-fingerprint.ts +0 -225
  154. package/src/security/manifest-cache.ts +0 -333
  155. package/src/security/manifest-schema.ts +0 -129
  156. package/src/security/registry-pubkey.generated.ts +0 -35
  157. package/src/security/telemetry.ts +0 -320
  158. package/src/security-scorer.ts +0 -405
  159. package/src/security-utils.ts +0 -133
  160. package/src/sentinel-db.ts +0 -580
  161. package/src/sentinel-scanner.ts +0 -406
  162. package/src/sentinel-tools.ts +0 -513
  163. package/src/sentinel-types.ts +0 -140
  164. package/src/server-dispatch.ts +0 -225
  165. package/src/server.ts +0 -107
  166. package/src/session-archiver.ts +0 -111
  167. package/src/session-state-generator.ts +0 -174
  168. package/src/team-knowledge.ts +0 -407
  169. package/src/tool-db-needs.ts +0 -232
  170. package/src/tools.ts +0 -1206
  171. package/src/transcript-parser.ts +0 -458
  172. package/src/trpc-index.ts +0 -215
  173. package/src/validate-features-runner.ts +0 -107
  174. package/src/validation-engine.ts +0 -358
  175. package/src/watch/daemon.ts +0 -385
  176. package/src/watch/lockfile-detector.ts +0 -65
  177. package/src/watch/paths.ts +0 -279
  178. 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
- }