@massu/core 1.10.8 → 1.12.1

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 +1334 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +581 -448
  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 +67 -19
  16. package/dist/hooks/pre-compact.js +28 -3
  17. package/dist/hooks/pre-delete-check.js +53 -27
  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 +31 -6
  23. package/dist/hooks/session-start.js +8709 -8459
  24. package/dist/hooks/user-prompt.js +25 -1
  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,458 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- import { createReadStream } from 'fs';
5
- import { createInterface } from 'readline';
6
-
7
- // ============================================================
8
- // P2-001: JSONL Transcript Parser
9
- // ============================================================
10
-
11
- /**
12
- * Represents a parsed transcript entry.
13
- */
14
- export interface TranscriptEntry {
15
- type: 'user' | 'assistant' | 'system' | 'progress' | 'summary' | 'file-history-snapshot' | 'unknown';
16
- sessionId?: string;
17
- gitBranch?: string;
18
- timestamp?: string;
19
- uuid?: string;
20
- isMeta?: boolean;
21
- message?: TranscriptMessage;
22
- data?: Record<string, unknown>;
23
- }
24
-
25
- export interface TranscriptMessage {
26
- role: 'user' | 'assistant' | 'system';
27
- content: TranscriptContentBlock[];
28
- }
29
-
30
- export type TranscriptContentBlock =
31
- | { type: 'text'; text: string }
32
- | { type: 'tool_use'; id: string; name: string; input: Record<string, unknown> }
33
- | { type: 'tool_result'; tool_use_id: string; content: string | TranscriptContentBlock[]; is_error?: boolean }
34
- | { type: string; [key: string]: unknown };
35
-
36
- /**
37
- * Parsed tool call with input and result linked.
38
- */
39
- export interface ParsedToolCall {
40
- toolName: string;
41
- toolUseId: string;
42
- input: Record<string, unknown>;
43
- result?: string;
44
- isError?: boolean;
45
- timestamp?: string;
46
- }
47
-
48
- /**
49
- * File operation extracted from tool calls.
50
- */
51
- export interface FileOperation {
52
- type: 'read' | 'write' | 'edit' | 'glob' | 'grep' | 'delete';
53
- filePath: string;
54
- toolName: string;
55
- }
56
-
57
- /**
58
- * Extracted decision from assistant text.
59
- */
60
- export interface ExtractedDecision {
61
- text: string;
62
- context: string;
63
- }
64
-
65
- /**
66
- * Extracted failed attempt from assistant text.
67
- */
68
- export interface ExtractedFailedAttempt {
69
- text: string;
70
- context: string;
71
- }
72
-
73
- /**
74
- * Parse a JSONL transcript file line-by-line (streaming).
75
- * Handles 400MB+ files without loading entire file into memory.
76
- */
77
- export async function parseTranscript(filePath: string): Promise<TranscriptEntry[]> {
78
- const entries: TranscriptEntry[] = [];
79
-
80
- const rl = createInterface({
81
- input: createReadStream(filePath, { encoding: 'utf-8' }),
82
- crlfDelay: Infinity,
83
- });
84
-
85
- for await (const line of rl) {
86
- const trimmed = line.trim();
87
- if (!trimmed) continue;
88
-
89
- try {
90
- const raw = JSON.parse(trimmed) as Record<string, unknown>;
91
- const entry = parseEntry(raw);
92
- if (entry) {
93
- entries.push(entry);
94
- }
95
- } catch (_e) {
96
- // Skip unparseable lines - defensive parsing
97
- continue;
98
- }
99
- }
100
-
101
- return entries;
102
- }
103
-
104
- /**
105
- * Parse a single JSONL entry into a TranscriptEntry.
106
- */
107
- function parseEntry(raw: Record<string, unknown>): TranscriptEntry | null {
108
- const entryType = raw.type as string | undefined;
109
- if (!entryType) return null;
110
-
111
- const base: TranscriptEntry = {
112
- type: (['user', 'assistant', 'system', 'progress', 'summary', 'file-history-snapshot'].includes(entryType)
113
- ? entryType
114
- : 'unknown') as TranscriptEntry['type'],
115
- sessionId: raw.sessionId as string | undefined,
116
- gitBranch: raw.gitBranch as string | undefined,
117
- timestamp: raw.timestamp as string | undefined,
118
- uuid: raw.uuid as string | undefined,
119
- };
120
-
121
- if (raw.isMeta) {
122
- base.isMeta = true;
123
- }
124
-
125
- if (entryType === 'user' || entryType === 'assistant') {
126
- const msgRaw = raw.message as Record<string, unknown> | undefined;
127
- if (msgRaw) {
128
- base.message = {
129
- role: (msgRaw.role as string ?? entryType) as 'user' | 'assistant',
130
- content: normalizeContent(msgRaw.content),
131
- };
132
- }
133
- }
134
-
135
- if (entryType === 'progress') {
136
- base.data = raw.data as Record<string, unknown> | undefined;
137
- }
138
-
139
- return base;
140
- }
141
-
142
- /**
143
- * Normalize content to array of content blocks.
144
- */
145
- function normalizeContent(content: unknown): TranscriptContentBlock[] {
146
- if (!content) return [];
147
- if (typeof content === 'string') {
148
- return [{ type: 'text', text: content }];
149
- }
150
- if (Array.isArray(content)) {
151
- return content.filter((block): block is TranscriptContentBlock =>
152
- typeof block === 'object' && block !== null && 'type' in block
153
- );
154
- }
155
- return [];
156
- }
157
-
158
- // ============================================================
159
- // Extraction utilities
160
- // ============================================================
161
-
162
- /**
163
- * Extract all user messages from transcript entries.
164
- */
165
- export function extractUserMessages(entries: TranscriptEntry[]): Array<{ text: string; timestamp?: string }> {
166
- const messages: Array<{ text: string; timestamp?: string }> = [];
167
- for (const entry of entries) {
168
- if (entry.type !== 'user' || !entry.message) continue;
169
- // Skip meta/system messages
170
- if (entry.isMeta) continue;
171
-
172
- const text = getTextFromContent(entry.message.content);
173
- if (text.trim()) {
174
- messages.push({ text: text.trim(), timestamp: entry.timestamp });
175
- }
176
- }
177
- return messages;
178
- }
179
-
180
- /**
181
- * Extract all assistant text messages.
182
- */
183
- export function extractAssistantMessages(entries: TranscriptEntry[]): Array<{ text: string; timestamp?: string }> {
184
- const messages: Array<{ text: string; timestamp?: string }> = [];
185
- for (const entry of entries) {
186
- if (entry.type !== 'assistant' || !entry.message) continue;
187
- const text = getTextFromContent(entry.message.content);
188
- if (text.trim()) {
189
- messages.push({ text: text.trim(), timestamp: entry.timestamp });
190
- }
191
- }
192
- return messages;
193
- }
194
-
195
- /**
196
- * Extract all tool calls from transcript entries.
197
- */
198
- export function extractToolCalls(entries: TranscriptEntry[]): ParsedToolCall[] {
199
- const toolCalls: ParsedToolCall[] = [];
200
- const toolUseMap = new Map<string, ParsedToolCall>();
201
-
202
- for (const entry of entries) {
203
- if (!entry.message?.content) continue;
204
-
205
- for (const block of entry.message.content) {
206
- if (block.type === 'tool_use') {
207
- const tc: ParsedToolCall = {
208
- toolName: (block as { name: string }).name,
209
- toolUseId: (block as { id: string }).id,
210
- input: (block as { input: Record<string, unknown> }).input ?? {},
211
- timestamp: entry.timestamp,
212
- };
213
- toolCalls.push(tc);
214
- toolUseMap.set(tc.toolUseId, tc);
215
- } else if (block.type === 'tool_result') {
216
- const toolUseId = (block as { tool_use_id: string }).tool_use_id;
217
- const existing = toolUseMap.get(toolUseId);
218
- if (existing) {
219
- existing.result = getToolResultText(block);
220
- existing.isError = (block as { is_error?: boolean }).is_error ?? false;
221
- }
222
- }
223
- }
224
- }
225
-
226
- return toolCalls;
227
- }
228
-
229
- /**
230
- * Extract file operations from tool calls.
231
- */
232
- export function extractFileOperations(toolCalls: ParsedToolCall[]): FileOperation[] {
233
- const ops: FileOperation[] = [];
234
-
235
- for (const tc of toolCalls) {
236
- switch (tc.toolName) {
237
- case 'Read': {
238
- const filePath = tc.input.file_path as string;
239
- if (filePath) ops.push({ type: 'read', filePath, toolName: 'Read' });
240
- break;
241
- }
242
- case 'Write': {
243
- const filePath = tc.input.file_path as string;
244
- if (filePath) ops.push({ type: 'write', filePath, toolName: 'Write' });
245
- break;
246
- }
247
- case 'Edit': {
248
- const filePath = tc.input.file_path as string;
249
- if (filePath) ops.push({ type: 'edit', filePath, toolName: 'Edit' });
250
- break;
251
- }
252
- case 'Glob': {
253
- ops.push({ type: 'glob', filePath: tc.input.pattern as string ?? '', toolName: 'Glob' });
254
- break;
255
- }
256
- case 'Grep': {
257
- ops.push({ type: 'grep', filePath: tc.input.path as string ?? '', toolName: 'Grep' });
258
- break;
259
- }
260
- }
261
- }
262
-
263
- return ops;
264
- }
265
-
266
- /**
267
- * Extract verification commands from tool calls.
268
- */
269
- export function extractVerificationCommands(toolCalls: ParsedToolCall[]): Array<{
270
- vrType: string;
271
- command: string;
272
- result: string;
273
- passed: boolean;
274
- }> {
275
- const verifications: Array<{
276
- vrType: string;
277
- command: string;
278
- result: string;
279
- passed: boolean;
280
- }> = [];
281
-
282
- for (const tc of toolCalls) {
283
- if (tc.toolName !== 'Bash') continue;
284
- const cmd = tc.input.command as string ?? '';
285
- const result = tc.result ?? '';
286
-
287
- // Pattern scanner
288
- if (cmd.includes('pattern-scanner')) {
289
- verifications.push({
290
- vrType: 'VR-PATTERN',
291
- command: cmd,
292
- result: result.slice(0, 500),
293
- passed: !result.includes('FAIL') && !result.includes('BLOCKED'),
294
- });
295
- }
296
- // Build
297
- if (cmd.includes('npm run build')) {
298
- verifications.push({
299
- vrType: 'VR-BUILD',
300
- command: cmd,
301
- result: result.slice(0, 500),
302
- passed: !tc.isError && !result.includes('error'),
303
- });
304
- }
305
- // Type check
306
- if (cmd.includes('tsc --noEmit')) {
307
- verifications.push({
308
- vrType: 'VR-TYPE',
309
- command: cmd,
310
- result: result.slice(0, 500),
311
- passed: !tc.isError && !result.includes('error'),
312
- });
313
- }
314
- // Tests
315
- if (cmd.includes('npm test') || cmd.includes('vitest run') || cmd.includes('vitest ')) {
316
- verifications.push({
317
- vrType: 'VR-TEST',
318
- command: cmd,
319
- result: result.slice(0, 500),
320
- passed: !tc.isError && !result.includes('FAIL'),
321
- });
322
- }
323
- }
324
-
325
- return verifications;
326
- }
327
-
328
- /**
329
- * Extract decisions from assistant text (heuristic).
330
- */
331
- export function extractDecisions(entries: TranscriptEntry[]): ExtractedDecision[] {
332
- const decisions: ExtractedDecision[] = [];
333
- const decisionPatterns = /\b(decided|chose|chosen|decision|instead of|opted for|going with|approach:|strategy:)\b/i;
334
-
335
- for (const entry of entries) {
336
- if (entry.type !== 'assistant' || !entry.message) continue;
337
- const text = getTextFromContent(entry.message.content);
338
- if (!text) continue;
339
-
340
- // Split into sentences/paragraphs
341
- const paragraphs = text.split(/\n\n|\.\s+/);
342
- for (const para of paragraphs) {
343
- if (decisionPatterns.test(para) && para.length > 20 && para.length < 500) {
344
- decisions.push({
345
- text: para.trim().slice(0, 300),
346
- context: text.slice(0, 200),
347
- });
348
- }
349
- }
350
- }
351
-
352
- return decisions;
353
- }
354
-
355
- /**
356
- * Extract failed attempts from assistant text (heuristic).
357
- */
358
- export function extractFailedAttempts(entries: TranscriptEntry[]): ExtractedFailedAttempt[] {
359
- const failures: ExtractedFailedAttempt[] = [];
360
- const failurePatterns = /\b(error|failed|doesn't work|didn't work|reverted|rolled back|bug|broken|issue:|problem:)\b/i;
361
-
362
- for (const entry of entries) {
363
- if (entry.type !== 'assistant' || !entry.message) continue;
364
- const text = getTextFromContent(entry.message.content);
365
- if (!text) continue;
366
-
367
- const paragraphs = text.split(/\n\n|\.\s+/);
368
- for (const para of paragraphs) {
369
- if (failurePatterns.test(para) && para.length > 20 && para.length < 500) {
370
- failures.push({
371
- text: para.trim().slice(0, 300),
372
- context: text.slice(0, 200),
373
- });
374
- }
375
- }
376
- }
377
-
378
- return failures;
379
- }
380
-
381
- /**
382
- * Parse a JSONL transcript file starting from a specific line (for incremental parsing).
383
- * Skips the first `startLine` lines and returns entries from the rest.
384
- */
385
- export async function parseTranscriptFrom(filePath: string, startLine: number): Promise<{ entries: TranscriptEntry[]; totalLines: number }> {
386
- const entries: TranscriptEntry[] = [];
387
- let lineNumber = 0;
388
-
389
- const rl = createInterface({
390
- input: createReadStream(filePath, { encoding: 'utf-8' }),
391
- crlfDelay: Infinity,
392
- });
393
-
394
- for await (const line of rl) {
395
- lineNumber++;
396
- if (lineNumber <= startLine) continue;
397
-
398
- const trimmed = line.trim();
399
- if (!trimmed) continue;
400
-
401
- try {
402
- const raw = JSON.parse(trimmed) as Record<string, unknown>;
403
- const entry = parseEntry(raw);
404
- if (entry) {
405
- entries.push(entry);
406
- }
407
- } catch (_e) {
408
- // Skip unparseable lines
409
- continue;
410
- }
411
- }
412
-
413
- return { entries, totalLines: lineNumber };
414
- }
415
-
416
- /**
417
- * Estimate token count for a text string.
418
- * Approximation: chars / 4.
419
- */
420
- export function estimateTokens(text: string): number {
421
- return Math.ceil(text.length / 4);
422
- }
423
-
424
- /**
425
- * Get the last assistant message from entries (useful for session summaries).
426
- */
427
- export function getLastAssistantMessage(entries: TranscriptEntry[]): string | null {
428
- for (let i = entries.length - 1; i >= 0; i--) {
429
- if (entries[i].type === 'assistant' && entries[i].message) {
430
- const text = getTextFromContent(entries[i].message!.content);
431
- if (text.trim()) return text.trim();
432
- }
433
- }
434
- return null;
435
- }
436
-
437
- // ============================================================
438
- // Helpers
439
- // ============================================================
440
-
441
- function getTextFromContent(content: TranscriptContentBlock[]): string {
442
- return content
443
- .filter((block): block is { type: 'text'; text: string } => block.type === 'text')
444
- .map(block => block.text)
445
- .join('\n');
446
- }
447
-
448
- function getToolResultText(block: TranscriptContentBlock): string {
449
- const content = (block as { content: string | TranscriptContentBlock[] }).content;
450
- if (typeof content === 'string') return content;
451
- if (Array.isArray(content)) {
452
- return content
453
- .filter((b): b is { type: 'text'; text: string } => typeof b === 'object' && b !== null && b.type === 'text')
454
- .map(b => b.text)
455
- .join('\n');
456
- }
457
- return '';
458
- }
package/src/trpc-index.ts DELETED
@@ -1,215 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- import { readFileSync, existsSync, readdirSync } from 'fs';
5
- import { resolve, join } from 'path';
6
- import type Database from 'better-sqlite3';
7
- import { getConfig, getResolvedPaths, getProjectRoot } from './config.ts';
8
- import { t } from './lib/sql-table-names.ts';
9
-
10
- interface RouterMapping {
11
- key: string; // e.g., "orders" (used as api.orders.*)
12
- variable: string; // e.g., "ordersRouter"
13
- file: string; // e.g., "src/server/api/routers/orders.ts"
14
- }
15
-
16
- interface ProcedureInfo {
17
- name: string;
18
- type: 'query' | 'mutation';
19
- isProtected: boolean;
20
- }
21
-
22
- /**
23
- * Parse src/server/api/root.ts to extract router key-to-file mapping.
24
- * The key is what UI code uses: api.[key].[procedure]
25
- */
26
- export function parseRootRouter(): RouterMapping[] {
27
- const paths = getResolvedPaths();
28
- const rootPath = paths.rootRouterPath;
29
- if (!existsSync(rootPath)) {
30
- throw new Error(`Root router not found at ${rootPath}`);
31
- }
32
-
33
- const source = readFileSync(rootPath, 'utf-8');
34
- const mappings: RouterMapping[] = [];
35
-
36
- // Step 1: Parse imports to map variable names to file paths
37
- // import { ordersRouter } from './routers/orders'
38
- const importMap = new Map<string, string>();
39
- const importRegex = /import\s+\{[^}]*?(\w+Router)[^}]*\}\s+from\s+['"]\.\/routers\/([^'"]+)['"]/g;
40
- let match;
41
- while ((match = importRegex.exec(source)) !== null) {
42
- const variable = match[1];
43
- let filePath = match[2];
44
- // Resolve to actual file path
45
- const fullPath = resolve(paths.routersDir, filePath);
46
- // Try with extensions
47
- for (const ext of ['.ts', '.tsx', '']) {
48
- const candidate = fullPath + ext;
49
- const routersRelPath = getConfig().paths.routers ?? 'src/server/api/routers';
50
- if (existsSync(candidate)) {
51
- filePath = routersRelPath + '/' + filePath + ext;
52
- break;
53
- }
54
- // Check if it's a directory with index
55
- const indexCandidate = join(fullPath, 'index.ts');
56
- if (existsSync(indexCandidate)) {
57
- filePath = routersRelPath + '/' + filePath + '/index.ts';
58
- break;
59
- }
60
- }
61
- importMap.set(variable, filePath);
62
- }
63
-
64
- // Step 2: Parse router registration - "orders: ordersRouter"
65
- const regRegex = /(\w+)\s*:\s*(\w+Router)/g;
66
- while ((match = regRegex.exec(source)) !== null) {
67
- const key = match[1];
68
- const variable = match[2];
69
- const file = importMap.get(variable);
70
- if (file) {
71
- mappings.push({ key, variable, file });
72
- }
73
- }
74
-
75
- return mappings;
76
- }
77
-
78
- /**
79
- * Extract procedure definitions from a router file.
80
- */
81
- export function extractProcedures(routerFilePath: string): ProcedureInfo[] {
82
- const absPath = resolve(getProjectRoot(), routerFilePath);
83
- if (!existsSync(absPath)) return [];
84
-
85
- const source = readFileSync(absPath, 'utf-8');
86
- const procedures: ProcedureInfo[] = [];
87
- const seen = new Set<string>();
88
-
89
- // Pattern: procedureName: protectedProcedure or publicProcedure
90
- const procRegex = /(\w+)\s*:\s*(protected|public)Procedure/g;
91
- let match;
92
- while ((match = procRegex.exec(source)) !== null) {
93
- const name = match[1];
94
- const isProtected = match[2] === 'protected';
95
- if (seen.has(name)) continue;
96
- seen.add(name);
97
-
98
- // Determine if query or mutation by looking ahead
99
- const afterMatch = source.slice(match.index);
100
- const typeMatch = afterMatch.match(/\.(query|mutation)\s*\(/);
101
- const type = typeMatch ? (typeMatch[1] as 'query' | 'mutation') : 'query';
102
-
103
- procedures.push({ name, type, isProtected });
104
- }
105
-
106
- return procedures;
107
- }
108
-
109
- /**
110
- * Find UI call sites for a given router key and procedure name.
111
- */
112
- export function findUICallSites(routerKey: string, procedureName: string): { file: string; line: number; pattern: string }[] {
113
- const callSites: { file: string; line: number; pattern: string }[] = [];
114
- const config = getConfig();
115
- const root = getProjectRoot();
116
- const src = config.paths.source;
117
- const searchDirs = [
118
- resolve(root, config.paths.pages ?? (src + '/app')),
119
- resolve(root, config.paths.components ?? (src + '/components')),
120
- resolve(root, config.paths.hooks ?? (src + '/hooks')),
121
- ];
122
-
123
- const searchPattern = `api.${routerKey}.${procedureName}`;
124
-
125
- for (const dir of searchDirs) {
126
- if (!existsSync(dir)) continue;
127
- searchDirectory(dir, searchPattern, callSites);
128
- }
129
-
130
- return callSites;
131
- }
132
-
133
- function searchDirectory(dir: string, pattern: string, results: { file: string; line: number; pattern: string }[]): void {
134
- const entries = readdirSync(dir, { withFileTypes: true });
135
- for (const entry of entries) {
136
- const fullPath = join(dir, entry.name);
137
- if (entry.isDirectory()) {
138
- if (entry.name === 'node_modules' || entry.name === '.next') continue;
139
- searchDirectory(fullPath, pattern, results);
140
- } else if (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx')) {
141
- try {
142
- const source = readFileSync(fullPath, 'utf-8');
143
- const lines = source.split('\n');
144
- for (let i = 0; i < lines.length; i++) {
145
- if (lines[i].includes(pattern)) {
146
- const relPath = fullPath.slice(getProjectRoot().length + 1);
147
- // Extract the full call pattern (e.g., api.orders.create.useMutation())
148
- const lineContent = lines[i].trim();
149
- const callMatch = lineContent.match(new RegExp(`(api\\.${escapeRegex(pattern.slice(4))}\\.[\\w.()]+)`));
150
- results.push({
151
- file: relPath,
152
- line: i + 1,
153
- pattern: callMatch ? callMatch[1] : pattern,
154
- });
155
- }
156
- }
157
- } catch {
158
- // Skip unreadable files
159
- }
160
- }
161
- }
162
- }
163
-
164
- function escapeRegex(str: string): string {
165
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
166
- }
167
-
168
- /**
169
- * Build the full tRPC procedure index.
170
- * Stores results in massu_trpc_procedures and massu_trpc_call_sites tables.
171
- */
172
- export function buildTrpcIndex(dataDb: Database.Database): { totalProcedures: number; withCallers: number; withoutCallers: number } {
173
- // Clear existing data
174
- dataDb.exec(`DELETE FROM ${t('trpc_call_sites')}`);
175
- dataDb.exec(`DELETE FROM ${t('trpc_procedures')}`);
176
-
177
- const routerMappings = parseRootRouter();
178
-
179
- const insertProc = dataDb.prepare(
180
- `INSERT INTO ${t('trpc_procedures')} (router_file, router_name, procedure_name, procedure_type, has_ui_caller) VALUES (?, ?, ?, ?, ?)`
181
- );
182
- const insertCallSite = dataDb.prepare(
183
- `INSERT INTO ${t('trpc_call_sites')} (procedure_id, file, line, call_pattern) VALUES (?, ?, ?, ?)`
184
- );
185
-
186
- let totalProcedures = 0;
187
- let withCallers = 0;
188
- let withoutCallers = 0;
189
-
190
- const insertAll = dataDb.transaction(() => {
191
- for (const router of routerMappings) {
192
- const procedures = extractProcedures(router.file);
193
-
194
- for (const proc of procedures) {
195
- const callSites = findUICallSites(router.key, proc.name);
196
- const hasUICaller = callSites.length > 0 ? 1 : 0;
197
-
198
- const result = insertProc.run(router.file, router.key, proc.name, proc.type, hasUICaller);
199
- const procId = result.lastInsertRowid;
200
-
201
- for (const site of callSites) {
202
- insertCallSite.run(procId, site.file, site.line, site.pattern);
203
- }
204
-
205
- totalProcedures++;
206
- if (hasUICaller) withCallers++;
207
- else withoutCallers++;
208
- }
209
- }
210
- });
211
-
212
- insertAll();
213
-
214
- return { totalProcedures, withCallers, withoutCallers };
215
- }