@massu/core 1.10.7 → 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 (177) hide show
  1. package/CHANGELOG.md +1303 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +559 -400
  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/gitToplevel.ts +0 -22
  110. package/src/lib/hook-registry.ts +0 -43
  111. package/src/lib/installLock.ts +0 -66
  112. package/src/lib/memory-path.ts +0 -49
  113. package/src/lib/pidLiveness.ts +0 -67
  114. package/src/lib/settings-local.ts +0 -110
  115. package/src/lib/sql-table-names.ts +0 -71
  116. package/src/license.ts +0 -547
  117. package/src/lsp/auto-detect.ts +0 -98
  118. package/src/lsp/client.ts +0 -776
  119. package/src/lsp/enrich.ts +0 -127
  120. package/src/lsp/types.ts +0 -221
  121. package/src/memory-db.ts +0 -1614
  122. package/src/memory-file-ingest.ts +0 -128
  123. package/src/memory-tools.ts +0 -424
  124. package/src/middleware-tree.ts +0 -71
  125. package/src/observability-tools.ts +0 -343
  126. package/src/observation-extractor.ts +0 -418
  127. package/src/page-deps.ts +0 -285
  128. package/src/permissions.ts +0 -422
  129. package/src/prompt-analyzer.ts +0 -332
  130. package/src/python/coupling-detector.ts +0 -125
  131. package/src/python/domain-enforcer.ts +0 -84
  132. package/src/python/impact-analyzer.ts +0 -96
  133. package/src/python/import-parser.ts +0 -244
  134. package/src/python/import-resolver.ts +0 -136
  135. package/src/python/migration-indexer.ts +0 -116
  136. package/src/python/migration-parser.ts +0 -332
  137. package/src/python/model-indexer.ts +0 -71
  138. package/src/python/model-parser.ts +0 -279
  139. package/src/python/route-indexer.ts +0 -59
  140. package/src/python/route-parser.ts +0 -317
  141. package/src/python-tools.ts +0 -630
  142. package/src/regression-detector.ts +0 -319
  143. package/src/rules.ts +0 -57
  144. package/src/schema-mapper.ts +0 -232
  145. package/src/security/adapter-origin.ts +0 -130
  146. package/src/security/adapter-verifier.ts +0 -319
  147. package/src/security/atomic-write.ts +0 -164
  148. package/src/security/fetcher.ts +0 -200
  149. package/src/security/install-tracking.ts +0 -319
  150. package/src/security/license-pubkey.generated.ts +0 -30
  151. package/src/security/license-response-verifier.ts +0 -149
  152. package/src/security/local-fingerprint.ts +0 -225
  153. package/src/security/manifest-cache.ts +0 -333
  154. package/src/security/manifest-schema.ts +0 -129
  155. package/src/security/registry-pubkey.generated.ts +0 -35
  156. package/src/security/telemetry.ts +0 -320
  157. package/src/security-scorer.ts +0 -405
  158. package/src/security-utils.ts +0 -133
  159. package/src/sentinel-db.ts +0 -580
  160. package/src/sentinel-scanner.ts +0 -406
  161. package/src/sentinel-tools.ts +0 -513
  162. package/src/sentinel-types.ts +0 -140
  163. package/src/server-dispatch.ts +0 -225
  164. package/src/server.ts +0 -107
  165. package/src/session-archiver.ts +0 -111
  166. package/src/session-state-generator.ts +0 -174
  167. package/src/team-knowledge.ts +0 -407
  168. package/src/tool-db-needs.ts +0 -232
  169. package/src/tools.ts +0 -1205
  170. package/src/transcript-parser.ts +0 -458
  171. package/src/trpc-index.ts +0 -215
  172. package/src/validate-features-runner.ts +0 -107
  173. package/src/validation-engine.ts +0 -358
  174. package/src/watch/daemon.ts +0 -385
  175. package/src/watch/lockfile-detector.ts +0 -65
  176. package/src/watch/paths.ts +0 -279
  177. package/src/watch/state.ts +0 -178
package/src/license.ts DELETED
@@ -1,547 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- /**
5
- * License module — tier enforcement for Massu tools.
6
- *
7
- * Exports:
8
- * - ToolTier type and TOOL_TIER_MAP constant
9
- * - getCurrentTier() — cached license status for the session
10
- * - getToolTier(name) — required tier for a tool
11
- * - isToolAllowed(toolName, userTier) — gate check
12
- * - annotateToolDefinitions(defs) — add tier labels to descriptions
13
- * - getLicenseToolDefinitions / isLicenseTool / handleLicenseToolCall — 3-function pattern
14
- */
15
-
16
- import { createHash } from 'crypto';
17
- import type { ToolDefinition, ToolResult } from './tools.ts';
18
- import { getConfig } from './config.ts';
19
- import { getMemoryDb } from './memory-db.ts';
20
- import { verifyLicenseResponse, isLicenseSignatureRequired } from './security/license-response-verifier.ts';
21
-
22
- // P-H019 one-shot warning gate. We emit the stderr warning at most once
23
- // per process lifetime so the customer's terminal isn't spammed every
24
- // session-start until the operator provisions the Edge Function signing key.
25
- let _warnedLicenseSig = false;
26
- function warnLicenseSigOnce(reason: string): void {
27
- if (_warnedLicenseSig) return;
28
- _warnedLicenseSig = true;
29
- process.stderr.write(
30
- `[massu] WARNING: license-validate response is unsigned or signature invalid (${reason}). ` +
31
- `Acceptance permitted under transition mode. Operator: provision Supabase Edge Function ` +
32
- `LICENSE_RESPONSE_SIGNING_PRIVATE_KEY_B64 then set MASSU_REQUIRE_SIGNED_LICENSE=true to enforce strict mode.\n`,
33
- );
34
- }
35
-
36
- // ============================================================
37
- // Types
38
- // ============================================================
39
-
40
- export type ToolTier = 'free' | 'pro' | 'team' | 'enterprise';
41
-
42
- // ============================================================
43
- // Tier Ordering (for comparison)
44
- // ============================================================
45
-
46
- const TIER_LEVELS: Record<ToolTier, number> = {
47
- free: 0,
48
- pro: 1,
49
- team: 2,
50
- enterprise: 3,
51
- };
52
-
53
- /** Return numeric level for tier comparison. Higher = more permissive. */
54
- export function tierLevel(tier: ToolTier): number {
55
- return TIER_LEVELS[tier] ?? 0;
56
- }
57
-
58
- // ============================================================
59
- // P3-002: Tool Tier Map
60
- // ============================================================
61
-
62
- /**
63
- * Maps every tool base name (without prefix) to its required tier.
64
- * Tools not in this map default to 'free'.
65
- *
66
- * Free: core navigation + basic memory + regression
67
- * Pro: knowledge, quality, cost, prompt, validation, ADR, observability, docs
68
- * Team: sentinel, team knowledge
69
- * Enterprise: audit, security, dependency
70
- */
71
- export const TOOL_TIER_MAP: Record<string, ToolTier> = {
72
- // --- Free tier (12 tools: core navigation + basic memory + regression + license) ---
73
- sync: 'free',
74
- context: 'free',
75
- impact: 'free',
76
- domains: 'free',
77
- schema: 'free',
78
- trpc_map: 'free',
79
- coupling_check: 'free',
80
- memory_search: 'free',
81
- memory_ingest: 'free',
82
- regression_risk: 'free',
83
- feature_health: 'free',
84
- license_status: 'free',
85
-
86
- // --- Pro tier (35 tools: knowledge, quality, cost, prompt, validation, ADR, observability, docs, advanced memory) ---
87
- memory_timeline: 'pro',
88
- memory_detail: 'pro',
89
- memory_sessions: 'pro',
90
- memory_failures: 'pro',
91
- knowledge_search: 'pro',
92
- knowledge_rule: 'pro',
93
- knowledge_incident: 'pro',
94
- knowledge_schema_check: 'pro',
95
- knowledge_pattern: 'pro',
96
- knowledge_verification: 'pro',
97
- knowledge_graph: 'pro',
98
- knowledge_command: 'pro',
99
- knowledge_correct: 'pro',
100
- knowledge_plan: 'pro',
101
- knowledge_gaps: 'pro',
102
- knowledge_effectiveness: 'pro',
103
- quality_score: 'pro',
104
- quality_trend: 'pro',
105
- quality_report: 'pro',
106
- cost_session: 'pro',
107
- cost_trend: 'pro',
108
- cost_feature: 'pro',
109
- prompt_effectiveness: 'pro',
110
- prompt_suggestions: 'pro',
111
- validation_check: 'pro',
112
- validation_report: 'pro',
113
- adr_list: 'pro',
114
- adr_detail: 'pro',
115
- adr_create: 'pro',
116
- session_replay: 'pro',
117
- prompt_analysis: 'pro',
118
- tool_patterns: 'pro',
119
- session_stats: 'pro',
120
- docs_audit: 'pro',
121
- docs_coverage: 'pro',
122
-
123
- // Pro tier — Python code intelligence
124
- py_imports: 'pro',
125
- py_routes: 'pro',
126
- py_coupling: 'pro',
127
- py_models: 'pro',
128
- py_migrations: 'pro',
129
- py_domains: 'pro',
130
- py_impact: 'pro',
131
- py_context: 'pro',
132
-
133
- // --- Team tier (9 tools: sentinel feature registry + team knowledge) ---
134
- sentinel_search: 'team',
135
- sentinel_detail: 'team',
136
- sentinel_impact: 'team',
137
- sentinel_validate: 'team',
138
- sentinel_register: 'team',
139
- sentinel_parity: 'team',
140
- team_search: 'team',
141
- team_expertise: 'team',
142
- team_conflicts: 'team',
143
-
144
- // --- Enterprise tier (8 tools: audit trail + security scoring + dependency analysis) ---
145
- audit_log: 'enterprise',
146
- audit_report: 'enterprise',
147
- audit_chain: 'enterprise',
148
- security_score: 'enterprise',
149
- security_heatmap: 'enterprise',
150
- security_trend: 'enterprise',
151
- dep_score: 'enterprise',
152
- dep_alternatives: 'enterprise',
153
- };
154
-
155
- // ============================================================
156
- // P3-002: Plan-to-tier mapping (from organizations.plan values)
157
- // ============================================================
158
-
159
- export const PLAN_TO_TIER_MAP: Record<string, ToolTier> = {
160
- free: 'free',
161
- cloud_pro: 'pro',
162
- cloud_team: 'team',
163
- cloud_enterprise: 'enterprise',
164
- };
165
-
166
- // ============================================================
167
- // P3-003: getToolTier
168
- // ============================================================
169
-
170
- /**
171
- * Get the required tier for a tool by name.
172
- * Strips the configured prefix, looks up in TOOL_TIER_MAP, defaults to 'free'.
173
- */
174
- export function getToolTier(name: string): ToolTier {
175
- const pfx = getConfig().toolPrefix + '_';
176
- const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
177
- return TOOL_TIER_MAP[baseName] ?? 'free';
178
- }
179
-
180
- // ============================================================
181
- // P3-001: isToolAllowed
182
- // ============================================================
183
-
184
- /**
185
- * Check if a tool is accessible at the given user tier.
186
- * A user can access tools at their tier level or below.
187
- */
188
- export function isToolAllowed(toolName: string, userTier: ToolTier): boolean {
189
- const requiredTier = getToolTier(toolName);
190
- return tierLevel(userTier) >= tierLevel(requiredTier);
191
- }
192
-
193
- // ============================================================
194
- // P3-004: annotateToolDefinitions
195
- // ============================================================
196
-
197
- const TIER_LABELS: Record<ToolTier, string> = {
198
- free: '',
199
- pro: '[PRO] ',
200
- team: '[TEAM] ',
201
- enterprise: '[ENTERPRISE] ',
202
- };
203
-
204
- /**
205
- * Annotate tool definitions with tier labels in descriptions.
206
- * Stores the structured tier under `annotations.tier` (MCP-spec-sanctioned
207
- * extension point) — never as a top-level field, which violates the
208
- * canonical Tool schema (schema/2025-11-25/schema.ts line 1251) and is
209
- * silently rejected by Claude Code 2.1.143+ (massu-ai/massu#4).
210
- * Free tools get no label prefix.
211
- */
212
- export function annotateToolDefinitions(defs: ToolDefinition[]): ToolDefinition[] {
213
- return defs.map(def => {
214
- const tier = getToolTier(def.name);
215
- const label = TIER_LABELS[tier];
216
- return {
217
- ...def,
218
- annotations: { ...(def.annotations ?? {}), tier },
219
- description: label ? `${label}${def.description}` : def.description,
220
- };
221
- });
222
- }
223
-
224
- // ============================================================
225
- // plan-1.7.0-cohesive-cleanup P-A-003: Cloud feature availability gate
226
- // ============================================================
227
-
228
- /**
229
- * Whether cloud-gated tool surfaces (team knowledge, etc.) are exposed.
230
- *
231
- * Returns true ONLY when `massu.config.yaml` opts the workspace into the
232
- * cloud feature surface via `cloud.enabled: true`. Defaults to false for
233
- * fresh installs (the schema's `enabled` default is `false`).
234
- *
235
- * This is distinct from {@link isLicenseTool} (which matches tool NAMES);
236
- * `isCloudFeatureAvailable` is a runtime feature-availability check used
237
- * by `tools.ts` to gate team-tool registration and routing at the
238
- * tools-list and dispatch boundaries.
239
- */
240
- export function isCloudFeatureAvailable(): boolean {
241
- return getConfig().cloud?.enabled === true;
242
- }
243
-
244
- // ============================================================
245
- // P3-005/P3-006/P3-007/P3-013: License validation & caching
246
- // ============================================================
247
-
248
- interface LicenseInfo {
249
- tier: ToolTier;
250
- validUntil: string;
251
- features: string[];
252
- }
253
-
254
- /** In-memory cache for the current session. Refreshes every 15 minutes. */
255
- let cachedTier: LicenseInfo | null = null;
256
- let cachedTierTimestamp: number = 0;
257
- const IN_MEMORY_CACHE_TTL_MS = 15 * 60 * 1000; // 15 minutes
258
-
259
- /**
260
- * Validate a license key against the cloud endpoint.
261
- * Uses local cache in memory.db with 1-hour freshness window.
262
- * Performs async cloud validation via fetch() (Node 18+).
263
- * Falls back to 7-day grace period on network failure.
264
- */
265
- export async function validateLicense(apiKey: string): Promise<LicenseInfo> {
266
- const keyHash = createHash('sha256').update(apiKey).digest('hex');
267
-
268
- // 1. Check local cache
269
- const memDb = getMemoryDb();
270
- try {
271
- const cached = memDb.prepare(
272
- 'SELECT tier, valid_until, last_validated, features FROM license_cache WHERE api_key_hash = ?'
273
- ).get(keyHash) as { tier: string; valid_until: string; last_validated: string; features: string } | undefined;
274
-
275
- if (cached) {
276
- const lastValidated = new Date(cached.last_validated);
277
- const hourAgo = new Date(Date.now() - 60 * 60 * 1000);
278
-
279
- // Cache is fresh (< 1 hour old)
280
- if (lastValidated > hourAgo) {
281
- return {
282
- tier: cached.tier as ToolTier,
283
- validUntil: cached.valid_until,
284
- features: JSON.parse(cached.features || '[]'),
285
- };
286
- }
287
- }
288
-
289
- // 2. Try cloud validation via fetch (Node 18+ has native fetch)
290
- const config = getConfig();
291
- const endpoint = config.cloud?.endpoint;
292
-
293
- if (endpoint && /^https?:\/\/.+/.test(endpoint)) {
294
- try {
295
- const response = await fetch(`${endpoint}/validate-key`, {
296
- method: 'POST',
297
- headers: {
298
- 'Authorization': `Bearer ${apiKey}`,
299
- 'Content-Type': 'application/json',
300
- },
301
- signal: AbortSignal.timeout(10_000), // 10s timeout
302
- });
303
-
304
- if (response.ok) {
305
- const data = await response.json() as {
306
- valid: boolean;
307
- plan?: string;
308
- tier?: string;
309
- validUntil?: string;
310
- features?: string[];
311
- reason?: string;
312
- _signature?: string;
313
- _signature_alg?: string;
314
- _signature_payload_keys?: readonly string[];
315
- _signature_pubkey_fingerprint?: string;
316
- };
317
-
318
- // P-H019 (plan-stage-c-high-batch / 1.10.5): verify Ed25519
319
- // signature on the validate-key response. Closes the bug class
320
- // where MITM / malicious-cloud-endpoint / local SQLite edit
321
- // could grant arbitrary tier.
322
- //
323
- // Transition mode (default): unsigned/invalid-sig responses are
324
- // accepted with a one-shot stderr warning. Lets existing customers
325
- // keep working while operators provision the Edge Function signing
326
- // key.
327
- //
328
- // Strict mode (MASSU_REQUIRE_SIGNED_LICENSE=true, post-cutover):
329
- // unsigned/invalid-sig responses are rejected; caller falls
330
- // through to the grace-period cache or free tier.
331
- const sigResult = verifyLicenseResponse(data);
332
- if (sigResult.kind !== 'valid') {
333
- if (isLicenseSignatureRequired()) {
334
- // Strict mode — reject. Caller falls through to grace period.
335
- throw new Error(`License response signature invalid: ${sigResult.kind}`);
336
- }
337
- // Transition mode — one-shot stderr warning per session.
338
- warnLicenseSigOnce(sigResult.kind);
339
- }
340
-
341
- if (data.valid) {
342
- // Map plan name to tier using PLAN_TO_TIER_MAP
343
- const tier: ToolTier = data.plan
344
- ? (PLAN_TO_TIER_MAP[data.plan] ?? 'free')
345
- : (data.tier as ToolTier ?? 'free');
346
- const validUntil = data.validUntil ?? '';
347
- const features = data.features ?? [];
348
-
349
- // Update local cache
350
- updateLicenseCache(apiKey, tier, validUntil, features);
351
-
352
- return { tier, validUntil, features };
353
- }
354
- // Server said key is not valid — return free tier
355
- return { tier: 'free', validUntil: '', features: [] };
356
- }
357
- // Non-OK response — fall through to grace period
358
- } catch {
359
- // Network failure — fall through to grace period check
360
- }
361
- }
362
-
363
- // 3. Grace period: cache exists but stale (up to 7 days)
364
- if (cached) {
365
- const lastValidated = new Date(cached.last_validated);
366
- const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
367
-
368
- // P3-013: 7-day grace period
369
- if (lastValidated > sevenDaysAgo) {
370
- return {
371
- tier: cached.tier as ToolTier,
372
- validUntil: cached.valid_until,
373
- features: JSON.parse(cached.features || '[]'),
374
- };
375
- }
376
- }
377
-
378
- // 4. No valid cache — default to free
379
- return { tier: 'free', validUntil: '', features: [] };
380
- } finally {
381
- memDb.close();
382
- }
383
- }
384
-
385
- /**
386
- * Update the license cache in memory.db.
387
- * Called by the session-start hook after async cloud validation.
388
- */
389
- export function updateLicenseCache(
390
- apiKey: string,
391
- tier: ToolTier,
392
- validUntil: string,
393
- features: string[] = []
394
- ): void {
395
- const keyHash = createHash('sha256').update(apiKey).digest('hex');
396
- const memDb = getMemoryDb();
397
- try {
398
- memDb.prepare(`
399
- INSERT OR REPLACE INTO license_cache (api_key_hash, tier, valid_until, last_validated, features)
400
- VALUES (?, ?, ?, datetime('now'), ?)
401
- `).run(keyHash, tier, validUntil, JSON.stringify(features));
402
- } finally {
403
- memDb.close();
404
- }
405
- }
406
-
407
- // ============================================================
408
- // P3-007: getCurrentTier
409
- // ============================================================
410
-
411
- /**
412
- * Get the current user's tier. Cached in-memory for the server process lifetime.
413
- * If no API key configured, returns 'free'.
414
- */
415
- export async function getCurrentTier(): Promise<ToolTier> {
416
- // Check if in-memory cache is still fresh (15-minute TTL)
417
- if (cachedTier && (Date.now() - cachedTierTimestamp) < IN_MEMORY_CACHE_TTL_MS) {
418
- return cachedTier.tier;
419
- }
420
-
421
- const config = getConfig();
422
- const apiKey = config.cloud?.apiKey;
423
-
424
- if (!apiKey) {
425
- cachedTier = { tier: 'free', validUntil: '', features: [] };
426
- cachedTierTimestamp = Date.now();
427
- return 'free';
428
- }
429
-
430
- const info = await validateLicense(apiKey);
431
- cachedTier = info;
432
- cachedTierTimestamp = Date.now();
433
- return info.tier;
434
- }
435
-
436
- /**
437
- * Get full license info (tier, validUntil, features).
438
- * Triggers getCurrentTier() if not already cached.
439
- */
440
- export async function getLicenseInfo(): Promise<LicenseInfo> {
441
- if (!cachedTier || (Date.now() - cachedTierTimestamp) >= IN_MEMORY_CACHE_TTL_MS) {
442
- await getCurrentTier();
443
- }
444
- return cachedTier!;
445
- }
446
-
447
- /**
448
- * Days remaining until license expires. Returns -1 if no expiry set.
449
- */
450
- export async function daysUntilExpiry(): Promise<number> {
451
- const info = await getLicenseInfo();
452
- if (!info.validUntil) return -1;
453
- const expiry = new Date(info.validUntil);
454
- const now = new Date();
455
- const diffMs = expiry.getTime() - now.getTime();
456
- return Math.ceil(diffMs / (1000 * 60 * 60 * 24));
457
- }
458
-
459
- // ============================================================
460
- // P3-021: License Status Tool (3-function pattern)
461
- // ============================================================
462
-
463
- /**
464
- * Tool definitions for the license status tool.
465
- * Always available (free tier).
466
- */
467
- export function getLicenseToolDefinitions(): ToolDefinition[] {
468
- const pfx = getConfig().toolPrefix;
469
- return [
470
- {
471
- name: `${pfx}_license_status`,
472
- description: 'Show current license status, tier, features, and upgrade options.',
473
- inputSchema: {
474
- type: 'object',
475
- properties: {},
476
- required: [],
477
- },
478
- },
479
- ];
480
- }
481
-
482
- /**
483
- * Check if a tool name matches a license tool.
484
- */
485
- export function isLicenseTool(name: string): boolean {
486
- return name.endsWith('_license_status');
487
- }
488
-
489
- /**
490
- * Handle license tool calls.
491
- */
492
- export async function handleLicenseToolCall(
493
- name: string,
494
- _args: Record<string, unknown>,
495
- _memDb: import('better-sqlite3').Database
496
- ): Promise<ToolResult> {
497
- if (name.endsWith('_license_status')) {
498
- const info = await getLicenseInfo();
499
- const days = await daysUntilExpiry();
500
-
501
- const lines: string[] = [];
502
- lines.push('## License Status');
503
- lines.push('');
504
- lines.push(`**Tier**: ${info.tier.toUpperCase()}`);
505
-
506
- if (info.validUntil) {
507
- lines.push(`**Valid Until**: ${info.validUntil}`);
508
- if (days >= 0) {
509
- lines.push(`**Days Remaining**: ${days}`);
510
- }
511
- }
512
-
513
- if (info.features.length > 0) {
514
- lines.push('');
515
- lines.push('**Features**:');
516
- for (const f of info.features) {
517
- lines.push(`- ${f}`);
518
- }
519
- }
520
-
521
- lines.push('');
522
- lines.push('### Tier Capabilities');
523
- lines.push('- **Free**: Core navigation, memory, regression detection');
524
- lines.push('- **Pro**: Knowledge search, quality analytics, cost tracking, observability');
525
- lines.push('- **Team**: Sentinel feature registry, team knowledge sharing');
526
- lines.push('- **Enterprise**: Audit trail, security scoring, dependency analysis');
527
-
528
- if (info.tier === 'free') {
529
- lines.push('');
530
- lines.push('Upgrade at https://massu.ai/pricing');
531
- }
532
-
533
- return { content: [{ type: 'text', text: lines.join('\n') }] };
534
- }
535
-
536
- return { content: [{ type: 'text', text: `Unknown license tool: ${name}` }] };
537
- }
538
-
539
- // ============================================================
540
- // Reset (for testing)
541
- // ============================================================
542
-
543
- /** Reset cached tier (for testing only). */
544
- export function _resetCachedTier(): void {
545
- cachedTier = null;
546
- cachedTierTimestamp = 0;
547
- }
@@ -1,98 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- /**
5
- * Plan 3b — Phase 4: LSP server discovery.
6
- *
7
- * Per audit-iter-1 fix G4: this module's discovery is **explicit-only by
8
- * default**. The optional `lsof` port-scan path is GATED behind
9
- * `lsp.autoDetect.viaPortScan: true` and defaults to `false` (port-scanning
10
- * local processes is a security-sensitive default — opt-in only at v1).
11
- *
12
- * Empty-servers edge case (audit-iter-3 fix Z): when `lsp.enabled: true` AND
13
- * `lsp.servers` is empty AND `viaPortScan: false`, we log ONE informational
14
- * stderr line and return an empty list — the caller (`enrich.ts`) then
15
- * proceeds AST-only without throwing.
16
- *
17
- * VR-LSP-AUTODETECT-OFF-BY-DEFAULT: `viaPortScan` MUST be checked BEFORE any
18
- * `lsof` invocation. The grep `grep -nE 'viaPortScan' auto-detect.ts` MUST
19
- * show that boolean check ahead of any `lsof` call.
20
- */
21
-
22
- import type { LSPConfig } from '../config.ts';
23
- import type { LSPServerSpec } from './client.ts';
24
-
25
- /**
26
- * Find LSP servers to launch / connect to. Pure config-driven by default.
27
- *
28
- * @param config - The `lsp` block from `massu.config.yaml`. May be undefined
29
- * when LSP is not configured at all (returns empty list silently).
30
- * @returns A list of `LSPServerSpec` ready to feed `LSPClient.fromCommand()`.
31
- * Empty list is a valid, non-error outcome — callers MUST proceed AST-only.
32
- */
33
- export async function findRunningLSPs(
34
- config: LSPConfig | undefined
35
- ): Promise<LSPServerSpec[]> {
36
- // Disabled or absent: silently no-op (no log).
37
- if (!config || !config.enabled) {
38
- return [];
39
- }
40
-
41
- const explicit = (config.servers ?? []).map((s) => splitCommand(s));
42
-
43
- // VR-LSP-AUTODETECT-OFF-BY-DEFAULT: this boolean check happens BEFORE any
44
- // `lsof`/port-scan invocation. Default is false — explicit-only path.
45
- const viaPortScan = config.autoDetect?.viaPortScan === true;
46
-
47
- if (explicit.length === 0 && !viaPortScan) {
48
- // Empty-servers edge case: enabled but nothing configured AND auto-detect
49
- // is off. Log once, proceed AST-only.
50
- process.stderr.write(
51
- '[massu/lsp] INFO: LSP enabled but no servers configured and auto-detect off — skipping LSP enrichment.\n'
52
- );
53
- return [];
54
- }
55
-
56
- // Port-scan path is opt-in via `viaPortScan: true`. Implementation
57
- // intentionally minimal at v1 — emits an INFO stderr line and returns
58
- // explicit servers only. Plan 3d will flesh out actual `lsof` discovery
59
- // once the threat model is reviewed.
60
- if (viaPortScan) {
61
- process.stderr.write(
62
- '[massu/lsp] INFO: lsp.autoDetect.viaPortScan is enabled but port-scan auto-detect is reserved for Plan 3d — using explicit servers only.\n'
63
- );
64
- // (No `lsof` invocation yet. The viaPortScan gate exists so future
65
- // implementations slot in here without changing the default surface.)
66
- }
67
-
68
- return explicit;
69
- }
70
-
71
- /**
72
- * Parse a config-string `command` into an `LSPServerSpec`. Splits on
73
- * whitespace (no shell evaluation, no globbing). Strict path validation
74
- * happens later in `LSPClient.fromCommand()` — this just parses the shape.
75
- *
76
- * Note: callers passing untrusted commands MUST review the result before
77
- * passing it to `LSPClient.fromCommand`. The factory rejects relative paths
78
- * and `..`-containing argv elements.
79
- */
80
- function splitCommand(server: {
81
- language: string;
82
- command: string;
83
- allow_setuid?: boolean;
84
- max_rss_mb?: number;
85
- }): LSPServerSpec {
86
- const cmd = (server.command ?? '').trim();
87
- // Whitespace split — not a full shell parser. Quoted args with spaces are
88
- // not supported at v1; users with such commands should run a wrapper script.
89
- const argv = cmd.length === 0 ? [] : cmd.split(/\s+/);
90
- return {
91
- language: server.language,
92
- argv,
93
- // F-014 / F-015 (closed 2026-05-06): pass through the security knobs
94
- // from config so the spawn surface enforces them.
95
- allowSetuid: server.allow_setuid ?? false,
96
- maxRssMb: server.max_rss_mb ?? undefined,
97
- };
98
- }