claude-brain 0.15.2 → 0.17.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 (250) hide show
  1. package/README.md +191 -191
  2. package/VERSION +1 -1
  3. package/assets/CLAUDE-unified.md +11 -11
  4. package/assets/CLAUDE.md +29 -11
  5. package/bunfig.toml +8 -8
  6. package/package.json +82 -82
  7. package/packs/backend/node.json +173 -173
  8. package/packs/core/javascript.json +176 -176
  9. package/packs/core/typescript.json +222 -222
  10. package/packs/frontend/react.json +254 -254
  11. package/packs/meta/testing.json +172 -172
  12. package/scripts/postinstall.mjs +341 -341
  13. package/src/automation/auto-context.ts +240 -240
  14. package/src/automation/decision-detector.ts +452 -452
  15. package/src/automation/index.ts +11 -11
  16. package/src/automation/phase12-manager.ts +456 -456
  17. package/src/automation/proactive-recall.ts +373 -373
  18. package/src/automation/project-detector.ts +310 -310
  19. package/src/automation/repo-scanner.ts +205 -205
  20. package/src/cli/auto-setup.ts +82 -82
  21. package/src/cli/bin.ts +209 -202
  22. package/src/cli/commands/chroma.ts +573 -573
  23. package/src/cli/commands/git-hook.ts +189 -189
  24. package/src/cli/commands/hooks.ts +213 -213
  25. package/src/cli/commands/init.ts +122 -122
  26. package/src/cli/commands/install-mcp.ts +92 -92
  27. package/src/cli/commands/pack.ts +197 -197
  28. package/src/cli/commands/refresh.ts +323 -0
  29. package/src/cli/commands/serve.ts +167 -173
  30. package/src/cli/commands/start.ts +42 -42
  31. package/src/cli/commands/uninstall-mcp.ts +41 -41
  32. package/src/cli/commands/update.ts +124 -121
  33. package/src/cli/diagnose.ts +4 -4
  34. package/src/cli/health-check.ts +4 -4
  35. package/src/cli/migrate-chroma.ts +106 -106
  36. package/src/cli/setup.ts +4 -4
  37. package/src/cli/ui/animations.ts +80 -80
  38. package/src/cli/ui/components.ts +82 -82
  39. package/src/cli/ui/index.ts +4 -4
  40. package/src/cli/ui/logo.ts +36 -36
  41. package/src/cli/ui/theme.ts +55 -55
  42. package/src/config/defaults.ts +50 -50
  43. package/src/config/home.ts +55 -55
  44. package/src/config/index.ts +7 -7
  45. package/src/config/loader.ts +166 -166
  46. package/src/config/migration.ts +76 -76
  47. package/src/config/schema.ts +360 -360
  48. package/src/config/validator.ts +184 -184
  49. package/src/config/watcher.ts +86 -86
  50. package/src/context/assembler.ts +398 -398
  51. package/src/context/cache-manager.ts +101 -101
  52. package/src/context/formatter.ts +84 -84
  53. package/src/context/hierarchy.ts +85 -85
  54. package/src/context/index.ts +83 -83
  55. package/src/context/progress-tracker.ts +174 -174
  56. package/src/context/standards-manager.ts +287 -287
  57. package/src/context/types.ts +252 -252
  58. package/src/context/validator.ts +58 -58
  59. package/src/diagnostics/index.ts +123 -123
  60. package/src/health/index.ts +229 -229
  61. package/src/hooks/brain-hook.ts +128 -112
  62. package/src/hooks/capture.ts +168 -205
  63. package/src/hooks/context-hook.ts +137 -0
  64. package/src/hooks/deduplicator.ts +72 -72
  65. package/src/hooks/git-capture.ts +109 -109
  66. package/src/hooks/git-hook-installer.ts +207 -207
  67. package/src/hooks/index.ts +20 -20
  68. package/src/hooks/installer.ts +244 -194
  69. package/src/hooks/passive-classifier.ts +404 -723
  70. package/src/hooks/queue.ts +129 -129
  71. package/src/hooks/session-tracker.ts +312 -275
  72. package/src/hooks/types.ts +52 -47
  73. package/src/index.ts +7 -7
  74. package/src/intelligence/cross-project/affinity.ts +162 -162
  75. package/src/intelligence/cross-project/generalizer.ts +283 -283
  76. package/src/intelligence/cross-project/index.ts +13 -13
  77. package/src/intelligence/cross-project/transfer.ts +201 -201
  78. package/src/intelligence/index.ts +24 -24
  79. package/src/intelligence/optimization/index.ts +10 -10
  80. package/src/intelligence/optimization/precompute.ts +202 -202
  81. package/src/intelligence/optimization/semantic-cache.ts +207 -207
  82. package/src/intelligence/prediction/context-anticipator.ts +198 -198
  83. package/src/intelligence/prediction/decision-predictor.ts +184 -184
  84. package/src/intelligence/prediction/index.ts +13 -13
  85. package/src/intelligence/prediction/recommender.ts +268 -268
  86. package/src/intelligence/reasoning/chain-retrieval.ts +247 -247
  87. package/src/intelligence/reasoning/counterfactual.ts +248 -248
  88. package/src/intelligence/reasoning/index.ts +13 -13
  89. package/src/intelligence/reasoning/synthesizer.ts +169 -169
  90. package/src/intelligence/temporal/evolution.ts +197 -197
  91. package/src/intelligence/temporal/index.ts +16 -16
  92. package/src/intelligence/temporal/query-processor.ts +190 -190
  93. package/src/intelligence/temporal/timeline.ts +259 -259
  94. package/src/intelligence/temporal/trends.ts +263 -263
  95. package/src/knowledge/entity-extractor.ts +416 -416
  96. package/src/knowledge/graph/builder.ts +185 -185
  97. package/src/knowledge/graph/linker.ts +201 -201
  98. package/src/knowledge/graph/memory-graph.ts +359 -359
  99. package/src/knowledge/graph/schema.ts +99 -99
  100. package/src/knowledge/graph/search.ts +168 -168
  101. package/src/knowledge/relationship-extractor.ts +108 -108
  102. package/src/memory/chroma/client.ts +174 -174
  103. package/src/memory/chroma/collection-manager.ts +94 -94
  104. package/src/memory/chroma/config.ts +57 -57
  105. package/src/memory/chroma/embeddings.ts +155 -155
  106. package/src/memory/chroma/index.ts +82 -82
  107. package/src/memory/chroma/migration.ts +270 -270
  108. package/src/memory/chroma/schemas.ts +69 -69
  109. package/src/memory/chroma/search.ts +315 -315
  110. package/src/memory/chroma/store.ts +741 -741
  111. package/src/memory/consolidation/archiver.ts +164 -164
  112. package/src/memory/consolidation/merger.ts +186 -186
  113. package/src/memory/consolidation/scorer.ts +138 -138
  114. package/src/memory/context-builder.ts +236 -236
  115. package/src/memory/database.ts +169 -169
  116. package/src/memory/embedding-utils.ts +156 -156
  117. package/src/memory/embeddings.ts +226 -226
  118. package/src/memory/episodic/detector.ts +108 -108
  119. package/src/memory/episodic/manager.ts +351 -351
  120. package/src/memory/episodic/summarizer.ts +179 -179
  121. package/src/memory/episodic/types.ts +52 -52
  122. package/src/memory/index.ts +582 -582
  123. package/src/memory/knowledge-extractor.ts +455 -455
  124. package/src/memory/learning.ts +378 -378
  125. package/src/memory/patterns.ts +396 -396
  126. package/src/memory/schema.ts +88 -88
  127. package/src/memory/search.ts +309 -309
  128. package/src/memory/store.ts +787 -787
  129. package/src/memory/types.ts +121 -121
  130. package/src/orchestrator/coordinator.ts +272 -272
  131. package/src/orchestrator/decision-logger.ts +228 -228
  132. package/src/orchestrator/event-emitter.ts +198 -198
  133. package/src/orchestrator/event-queue.ts +184 -184
  134. package/src/orchestrator/handlers/base-handler.ts +70 -70
  135. package/src/orchestrator/handlers/context-handler.ts +73 -73
  136. package/src/orchestrator/handlers/decision-handler.ts +204 -204
  137. package/src/orchestrator/handlers/index.ts +10 -10
  138. package/src/orchestrator/handlers/status-handler.ts +131 -131
  139. package/src/orchestrator/handlers/task-handler.ts +171 -171
  140. package/src/orchestrator/index.ts +275 -275
  141. package/src/orchestrator/task-parser.ts +284 -284
  142. package/src/orchestrator/types.ts +98 -98
  143. package/src/packs/index.ts +9 -9
  144. package/src/packs/loader.ts +134 -134
  145. package/src/packs/manager.ts +204 -204
  146. package/src/packs/ranker.ts +78 -78
  147. package/src/packs/types.ts +81 -81
  148. package/src/phase12/index.ts +5 -5
  149. package/src/retrieval/bm25/index.ts +300 -300
  150. package/src/retrieval/bm25/tokenizer.ts +184 -184
  151. package/src/retrieval/feedback/adaptive.ts +223 -223
  152. package/src/retrieval/feedback/index.ts +16 -16
  153. package/src/retrieval/feedback/metrics.ts +223 -223
  154. package/src/retrieval/feedback/store.ts +283 -283
  155. package/src/retrieval/fusion/index.ts +194 -194
  156. package/src/retrieval/fusion/rrf.ts +163 -163
  157. package/src/retrieval/index.ts +12 -12
  158. package/src/retrieval/pipeline.ts +375 -375
  159. package/src/retrieval/query/expander.ts +198 -198
  160. package/src/retrieval/query/index.ts +27 -27
  161. package/src/retrieval/query/intent-classifier.ts +236 -236
  162. package/src/retrieval/query/temporal-parser.ts +295 -295
  163. package/src/retrieval/reranker/index.ts +188 -188
  164. package/src/retrieval/reranker/model.ts +95 -95
  165. package/src/retrieval/service.ts +125 -125
  166. package/src/retrieval/types.ts +162 -162
  167. package/src/routing/entity-extractor.ts +428 -428
  168. package/src/routing/intent-classifier.ts +450 -436
  169. package/src/routing/response-filter.ts +261 -258
  170. package/src/routing/router.ts +1441 -1322
  171. package/src/routing/search-engine.ts +515 -475
  172. package/src/routing/types.ts +94 -94
  173. package/src/scripts/health-check.ts +118 -118
  174. package/src/scripts/setup.ts +122 -122
  175. package/src/server/handlers/call-tool.ts +156 -156
  176. package/src/server/handlers/index.ts +9 -9
  177. package/src/server/handlers/list-tools.ts +35 -35
  178. package/src/server/handlers/tools/analyze-decision-evolution.ts +151 -151
  179. package/src/server/handlers/tools/auto-remember.ts +200 -200
  180. package/src/server/handlers/tools/brain.ts +85 -85
  181. package/src/server/handlers/tools/create-project.ts +135 -135
  182. package/src/server/handlers/tools/detect-trends.ts +144 -144
  183. package/src/server/handlers/tools/find-cross-project-patterns.ts +168 -168
  184. package/src/server/handlers/tools/get-activity-log.ts +194 -194
  185. package/src/server/handlers/tools/get-code-standards.ts +124 -124
  186. package/src/server/handlers/tools/get-corrections.ts +154 -154
  187. package/src/server/handlers/tools/get-decision-timeline.ts +172 -172
  188. package/src/server/handlers/tools/get-episode.ts +103 -103
  189. package/src/server/handlers/tools/get-patterns.ts +158 -158
  190. package/src/server/handlers/tools/get-phase12-status.ts +63 -63
  191. package/src/server/handlers/tools/get-project-context.ts +75 -75
  192. package/src/server/handlers/tools/get-recommendations.ts +145 -145
  193. package/src/server/handlers/tools/index.ts +31 -31
  194. package/src/server/handlers/tools/init-project.ts +757 -757
  195. package/src/server/handlers/tools/list-episodes.ts +90 -90
  196. package/src/server/handlers/tools/list-projects.ts +125 -125
  197. package/src/server/handlers/tools/rate-memory.ts +101 -101
  198. package/src/server/handlers/tools/recall-similar.ts +87 -87
  199. package/src/server/handlers/tools/recognize-pattern.ts +126 -126
  200. package/src/server/handlers/tools/record-correction.ts +125 -125
  201. package/src/server/handlers/tools/remember-decision.ts +153 -153
  202. package/src/server/handlers/tools/schemas.ts +253 -253
  203. package/src/server/handlers/tools/search-knowledge-graph.ts +102 -102
  204. package/src/server/handlers/tools/smart-context.ts +146 -146
  205. package/src/server/handlers/tools/update-progress.ts +131 -131
  206. package/src/server/handlers/tools/what-if-analysis.ts +135 -135
  207. package/src/server/http-api.ts +761 -693
  208. package/src/server/index.ts +40 -40
  209. package/src/server/mcp-server.ts +283 -283
  210. package/src/server/providers/index.ts +7 -7
  211. package/src/server/providers/prompts.ts +327 -327
  212. package/src/server/providers/resources.ts +622 -622
  213. package/src/server/services.ts +468 -468
  214. package/src/server/types.ts +39 -39
  215. package/src/server/utils/error-handler.ts +155 -155
  216. package/src/server/utils/index.ts +13 -13
  217. package/src/server/utils/memory-indicator.ts +83 -83
  218. package/src/server/utils/request-context.ts +122 -122
  219. package/src/server/utils/response-formatter.ts +129 -129
  220. package/src/server/utils/validators.ts +210 -210
  221. package/src/setup/index.ts +48 -48
  222. package/src/setup/wizard.ts +461 -461
  223. package/src/tools/index.ts +24 -24
  224. package/src/tools/registry.ts +115 -115
  225. package/src/tools/schemas.test.ts +30 -30
  226. package/src/tools/schemas.ts +617 -617
  227. package/src/tools/types.ts +412 -412
  228. package/src/utils/circuit-breaker.ts +130 -130
  229. package/src/utils/cleanup.ts +34 -34
  230. package/src/utils/error-handler.ts +132 -132
  231. package/src/utils/error-messages.ts +60 -60
  232. package/src/utils/fallback.ts +45 -45
  233. package/src/utils/index.ts +54 -54
  234. package/src/utils/logger-utils.ts +80 -80
  235. package/src/utils/logger.ts +88 -88
  236. package/src/utils/phase12-helper.ts +56 -56
  237. package/src/utils/retry.ts +94 -94
  238. package/src/utils/timing.ts +47 -47
  239. package/src/utils/transaction.ts +63 -63
  240. package/src/vault/frontmatter.ts +264 -264
  241. package/src/vault/index.ts +318 -318
  242. package/src/vault/paths.ts +106 -106
  243. package/src/vault/query.ts +422 -422
  244. package/src/vault/reader.ts +264 -264
  245. package/src/vault/templates.ts +186 -186
  246. package/src/vault/types.ts +73 -73
  247. package/src/vault/watcher.ts +277 -277
  248. package/src/vault/writer.ts +413 -413
  249. package/tsconfig.json +30 -30
  250. package/src/cli/auto-update.ts +0 -157
@@ -1,723 +1,404 @@
1
- /**
2
- * Phase 17: Passive Classifier
3
- * Extracts knowledge from tool outputs using lightweight pattern matching.
4
- * No embeddings needed — runs fast enough for hook context (<200ms).
5
- *
6
- * v2: Fixes from HOOK-CLASSIFIER-IMPROVEMENTS.md
7
- * - Fix 1: Smart content summarization for Write tool
8
- * - Fix 2: Edit context capture (not just decision language)
9
- * - Fix 3: Expanded Bash command coverage (server, process, docker)
10
- * - Fix 5: Pre-storage deduplication via LRU hash set
11
- * - Fix 6: Stop event handler for session summaries
12
- */
13
-
14
- import type { HookInput, CapturedKnowledge } from './types'
15
-
16
- /** Phrases that indicate a decision was made */
17
- const DECISION_PHRASES = [
18
- 'i recommend', 'you should use', 'the best approach', 'i suggest',
19
- 'better to use', 'prefer using', 'go with', 'choose', 'instead of',
20
- 'the right choice', 'decided to', "let's use", 'we will use',
21
- 'the solution is', 'implement using', 'going with', 'switching to',
22
- 'adopting', 'we chose', 'the plan is to'
23
- ]
24
-
25
- /** Phrases indicating a correction or lesson */
26
- const CORRECTION_PHRASES = [
27
- 'the bug was', 'the issue was', 'the problem was', 'mistake was',
28
- 'should have', 'should not have', "shouldn't have",
29
- 'lesson learned', "don't use", 'avoid using', 'never use',
30
- 'the fix is', 'the fix was', 'fixed by', 'solved by'
31
- ]
32
-
33
- /** Package install patterns for bash commands */
34
- const INSTALL_PATTERNS = [
35
- /(?:npm|yarn|pnpm|bun)\s+(?:install|add|i)\s+(.+)/i,
36
- /pip\s+install\s+(.+)/i,
37
- /cargo\s+add\s+(.+)/i,
38
- /go\s+get\s+(.+)/i,
39
- /gem\s+install\s+(.+)/i,
40
- ]
41
-
42
- /** Git operation patterns */
43
- const GIT_PATTERNS = [
44
- /git\s+commit\s+.*-m\s+["'](.+?)["']/i,
45
- /git\s+merge\s+(\S+)/i,
46
- /git\s+checkout\s+-b\s+(\S+)/i,
47
- /git\s+branch\s+(\S+)/i,
48
- ]
49
-
50
- /** Test/build command patterns */
51
- const BUILD_PATTERNS = [
52
- /(?:npm|yarn|pnpm|bun)\s+(?:run\s+)?(?:test|build|lint|typecheck|check)/i,
53
- /(?:jest|vitest|pytest|cargo\s+test|go\s+test)/i,
54
- ]
55
-
56
- /** Fix 3: Server start patterns */
57
- const SERVER_PATTERNS = [
58
- /(?:bun|node|deno|python|python3)\s+(?:run\s+)?(\S+\.(?:ts|js|py|mjs))/i,
59
- /(?:npm|yarn|pnpm|bun)\s+(?:run\s+)?(?:start|dev|serve)/i,
60
- /(?:uvicorn|gunicorn|flask\s+run|rails\s+s)/i,
61
- ]
62
-
63
- /** Fix 3: Process management patterns */
64
- const PROCESS_PATTERNS = [
65
- /taskkill\s+.*(?:\/PID|\/IM)\s+(\S+)/i,
66
- /kill\s+(?:-\d+\s+)?(\d+)/i,
67
- /netstat\s+.*(?:-ano|-tulpn)/i,
68
- /lsof\s+.*-i/i,
69
- /fuser\s+.*\/tcp/i,
70
- ]
71
-
72
- /** Fix 3: Docker patterns */
73
- const DOCKER_PATTERNS = [
74
- /docker\s+(?:build|run|compose|push|pull|stop|rm|exec)\b/i,
75
- /docker-compose\s+(?:up|down|build|restart)\b/i,
76
- ]
77
-
78
- /** Bash commands to skip (low signal) */
79
- const SKIP_COMMANDS = new Set([
80
- 'cd', 'ls', 'cat', 'head', 'tail', 'pwd', 'echo', 'clear', 'which', 'whoami',
81
- 'date', 'env', 'printenv', 'export', 'source', 'alias', 'history',
82
- ])
83
-
84
- /** File extension to technology mapping */
85
- const EXT_TO_TECH: Record<string, string> = {
86
- '.ts': 'typescript', '.tsx': 'typescript', '.js': 'javascript', '.jsx': 'javascript',
87
- '.py': 'python', '.rs': 'rust', '.go': 'go', '.java': 'java',
88
- '.rb': 'ruby', '.php': 'php', '.swift': 'swift', '.kt': 'kotlin',
89
- '.vue': 'vue', '.svelte': 'svelte', '.astro': 'astro',
90
- '.css': 'css', '.scss': 'sass', '.less': 'less',
91
- '.sql': 'sql', '.graphql': 'graphql', '.gql': 'graphql',
92
- '.yml': 'yaml', '.yaml': 'yaml', '.toml': 'toml',
93
- '.dockerfile': 'docker', '.prisma': 'prisma',
94
- }
95
-
96
- /** Path segments that indicate file role */
97
- const PATH_ROLE_MAP: Record<string, string> = {
98
- 'test': 'testing', 'tests': 'testing', '__tests__': 'testing', 'spec': 'testing',
99
- 'src': 'source', 'lib': 'library', 'utils': 'utility', 'helpers': 'utility',
100
- 'components': 'component', 'pages': 'page', 'routes': 'routing',
101
- 'api': 'api', 'server': 'server', 'client': 'client',
102
- 'config': 'configuration', 'scripts': 'scripting',
103
- 'hooks': 'hooks', 'middleware': 'middleware', 'types': 'types',
104
- }
105
-
106
- /** Fix 1: Key API/framework patterns to detect in file content */
107
- const KEY_API_PATTERNS: [RegExp, string][] = [
108
- [/Bun\.serve\b/, 'Bun.serve'],
109
- [/express\s*\(\)/, 'Express'],
110
- [/new\s+Hono\b/, 'Hono'],
111
- [/createServer\b/, 'HTTP server'],
112
- [/WebSocket\b/i, 'WebSocket'],
113
- [/app\.(?:get|post|put|delete|patch|use)\s*\(/, 'REST routes'],
114
- [/useEffect|useState|useRef/, 'React hooks'],
115
- [/createContext|useContext/, 'React context'],
116
- [/prisma\.\w+/, 'Prisma ORM'],
117
- [/mongoose\.\w+/, 'Mongoose'],
118
- [/\.query\s*\(|\.execute\s*\(/, 'database queries'],
119
- [/jwt\.|jsonwebtoken/, 'JWT auth'],
120
- [/bcrypt|argon2|scrypt/, 'password hashing'],
121
- [/rate.?limit/i, 'rate limiting'],
122
- [/(?:sanitize|escapeHtml|xss)\b/, 'input sanitization'],
123
- [/cron|schedule/i, 'scheduled tasks'],
124
- [/createReadStream|\.pipe\s*\(/, 'streaming'],
125
- ]
126
-
127
- export class PassiveClassifier {
128
- /** Fix 5: LRU set of recent content hashes for deduplication */
129
- private recentHashes = new Set<string>()
130
- private readonly maxRecent = 50
131
-
132
- /**
133
- * Classify a hook event and extract knowledge if found.
134
- * Returns null if no knowledge worth capturing.
135
- */
136
- classify(input: HookInput): CapturedKnowledge | null {
137
- const toolName = input.tool_name?.toLowerCase()
138
-
139
- // Fix 6: Handle Stop events (no tool_name)
140
- if (!toolName) {
141
- if (input.hook_event_name === 'Stop') {
142
- return this.dedup(this.classifySessionEnd(input))
143
- }
144
- return null
145
- }
146
-
147
- let result: CapturedKnowledge | null = null
148
-
149
- switch (toolName) {
150
- case 'edit':
151
- case 'write':
152
- result = this.classifyFileEdit(input)
153
- break
154
- case 'bash':
155
- result = this.classifyBashCommand(input)
156
- break
157
- default:
158
- // Read, Glob, Grep — skip (read-only, low signal)
159
- return null
160
- }
161
-
162
- // Fix 5: Pre-storage deduplication
163
- return this.dedup(result)
164
- }
165
-
166
- /** Fix 5: Check and filter duplicates via LRU hash */
167
- private dedup(result: CapturedKnowledge | null): CapturedKnowledge | null {
168
- if (result && this.isDuplicate(result.content)) {
169
- return null
170
- }
171
- return result
172
- }
173
-
174
- private classifyFileEdit(input: HookInput): CapturedKnowledge | null {
175
- const toolInput = input.tool_input
176
- if (!toolInput) return null
177
-
178
- const filePath = (toolInput.file_path || toolInput.path || '') as string
179
- if (!filePath) return null
180
-
181
- const technologies = this.extractTechFromPath(filePath)
182
- const role = this.extractRoleFromPath(filePath)
183
-
184
- // Check if the edit content contains decision language
185
- const content = toolInput.new_string || toolInput.content || ''
186
- const responseText = this.extractResponseText(input.tool_response)
187
-
188
- // Check for new file creation (Write tool)
189
- if (input.tool_name?.toLowerCase() === 'write') {
190
- // Check file content for decision/correction language before defaulting to pattern
191
- if (typeof content === 'string' && content.length > 50) {
192
- const decisionInContent = this.detectDecisionLanguage(content)
193
- if (decisionInContent) {
194
- return {
195
- type: 'decision',
196
- confidence: 0.8,
197
- content: decisionInContent,
198
- project: this.extractProjectFromCwd(input.cwd),
199
- technologies,
200
- metadata: { filePath, role, action: 'create' },
201
- source: 'hook-passive',
202
- timestamp: new Date().toISOString(),
203
- }
204
- }
205
- const correctionInContent = this.detectCorrectionLanguage(content)
206
- if (correctionInContent) {
207
- return {
208
- type: 'correction',
209
- confidence: 0.75,
210
- content: correctionInContent,
211
- project: this.extractProjectFromCwd(input.cwd),
212
- technologies,
213
- metadata: { filePath, role, action: 'create' },
214
- source: 'hook-passive',
215
- timestamp: new Date().toISOString(),
216
- }
217
- }
218
- }
219
-
220
- // Fix 1: Smart content summarization instead of generic "Created file: path"
221
- if (typeof content === 'string' && content.length > 100) {
222
- const summary = this.summarizeFileContent(content, filePath)
223
- return {
224
- type: 'pattern',
225
- confidence: 0.75,
226
- content: summary,
227
- project: this.extractProjectFromCwd(input.cwd),
228
- technologies,
229
- metadata: { filePath, role, action: 'create' },
230
- source: 'hook-passive',
231
- timestamp: new Date().toISOString(),
232
- }
233
- }
234
-
235
- // Short files still get the basic pattern
236
- return {
237
- type: 'pattern',
238
- confidence: 0.7,
239
- content: `Created ${role ? role + ' ' : ''}file: ${this.shortenPath(filePath)}${technologies.length ? ` (${technologies.join(', ')})` : ''}`,
240
- project: this.extractProjectFromCwd(input.cwd),
241
- technologies,
242
- metadata: { filePath, role, action: 'create' },
243
- source: 'hook-passive',
244
- timestamp: new Date().toISOString(),
245
- }
246
- }
247
-
248
- // For edits, check for decision language in content
249
- if (typeof content === 'string' && content.length > 50) {
250
- const decisionInContent = this.detectDecisionLanguage(content)
251
- if (decisionInContent) {
252
- return {
253
- type: 'decision',
254
- confidence: 0.75,
255
- content: decisionInContent,
256
- project: this.extractProjectFromCwd(input.cwd),
257
- technologies,
258
- metadata: { filePath, role, action: 'edit' },
259
- source: 'hook-passive',
260
- timestamp: new Date().toISOString(),
261
- }
262
- }
263
- }
264
-
265
- // Check tool response for decision language
266
- if (responseText) {
267
- const decisionInResponse = this.detectDecisionLanguage(responseText)
268
- if (decisionInResponse) {
269
- return {
270
- type: 'decision',
271
- confidence: 0.7,
272
- content: decisionInResponse,
273
- project: this.extractProjectFromCwd(input.cwd),
274
- technologies,
275
- metadata: { filePath, role, action: 'edit' },
276
- source: 'hook-passive',
277
- timestamp: new Date().toISOString(),
278
- }
279
- }
280
- }
281
-
282
- // Fix 2: Capture meaningful edits even without decision language
283
- const oldStr = (toolInput.old_string || '') as string
284
- const newStr = (toolInput.new_string || '') as string
285
- if (oldStr && newStr && oldStr.trim() !== newStr.trim()) {
286
- const combinedLen = oldStr.length + newStr.length
287
- if (combinedLen > 60) {
288
- const oldPreview = oldStr.trim().slice(0, 60)
289
- const newPreview = newStr.trim().slice(0, 60)
290
- return {
291
- type: 'pattern',
292
- confidence: 0.7,
293
- content: `Edited ${this.shortenPath(filePath)}: "${oldPreview}${oldStr.trim().length > 60 ? '...' : ''}" \u2192 "${newPreview}${newStr.trim().length > 60 ? '...' : ''}"`,
294
- project: this.extractProjectFromCwd(input.cwd),
295
- technologies,
296
- metadata: { filePath, role, action: 'edit' },
297
- source: 'hook-passive',
298
- timestamp: new Date().toISOString(),
299
- }
300
- }
301
- }
302
-
303
- return null
304
- }
305
-
306
- private classifyBashCommand(input: HookInput): CapturedKnowledge | null {
307
- const rawCommand = (input.tool_input?.command || '') as string
308
- if (!rawCommand || rawCommand.length < 3) return null
309
-
310
- // Split compound commands (cd "..." && bun add react) into sub-commands
311
- const subCommands = rawCommand.split(/\s*(?:&&|\|\||;)\s*/).map(s => s.trim()).filter(Boolean)
312
-
313
- // Find the first meaningful sub-command (skip cd, export, etc.)
314
- const command = subCommands.find(sub => {
315
- const firstWord = sub.split(/\s+/)[0]?.toLowerCase()
316
- return !firstWord || !SKIP_COMMANDS.has(firstWord)
317
- }) || rawCommand
318
-
319
- // If all sub-commands are skip-worthy, bail
320
- const firstWord = command.trim().split(/\s+/)[0]?.toLowerCase()
321
- if (firstWord && SKIP_COMMANDS.has(firstWord)) return null
322
-
323
- // Package installs
324
- for (const pattern of INSTALL_PATTERNS) {
325
- const match = command.match(pattern)
326
- if (match) {
327
- const packages = match[1]?.trim()
328
- if (packages) {
329
- return {
330
- type: 'decision',
331
- confidence: 0.85,
332
- content: `Installed package(s): ${packages}`,
333
- project: this.extractProjectFromCwd(input.cwd),
334
- technologies: this.extractTechFromPackages(packages),
335
- metadata: { command: rawCommand, action: 'install' },
336
- source: 'hook-passive',
337
- timestamp: new Date().toISOString(),
338
- }
339
- }
340
- }
341
- }
342
-
343
- // Git operations
344
- for (const pattern of GIT_PATTERNS) {
345
- const match = command.match(pattern)
346
- if (match) {
347
- return {
348
- type: 'progress',
349
- confidence: 0.8,
350
- content: `Git: ${command.trim().slice(0, 200)}`,
351
- project: this.extractProjectFromCwd(input.cwd),
352
- technologies: ['git'],
353
- metadata: { command: rawCommand, action: 'git' },
354
- source: 'hook-passive',
355
- timestamp: new Date().toISOString(),
356
- }
357
- }
358
- }
359
-
360
- // Test/build runs check all sub-commands since test may follow cd
361
- const buildCommand = subCommands.find(sub => BUILD_PATTERNS.some(p => p.test(sub))) || command
362
- for (const pattern of BUILD_PATTERNS) {
363
- if (pattern.test(buildCommand)) {
364
- const responseText = this.extractResponseText(input.tool_response)
365
- const failed = responseText?.toLowerCase().includes('fail') ||
366
- responseText?.toLowerCase().includes('error')
367
-
368
- if (failed) {
369
- return {
370
- type: 'correction',
371
- confidence: 0.75,
372
- content: `Build/test failure: ${buildCommand.trim().slice(0, 100)}`,
373
- project: this.extractProjectFromCwd(input.cwd),
374
- technologies: [],
375
- metadata: { command: rawCommand, action: 'build', failed: true },
376
- source: 'hook-passive',
377
- timestamp: new Date().toISOString(),
378
- }
379
- }
380
-
381
- return {
382
- type: 'progress',
383
- confidence: 0.7,
384
- content: `Ran: ${buildCommand.trim().slice(0, 200)}`,
385
- project: this.extractProjectFromCwd(input.cwd),
386
- technologies: [],
387
- metadata: { command: rawCommand, action: 'build', failed: false },
388
- source: 'hook-passive',
389
- timestamp: new Date().toISOString(),
390
- }
391
- }
392
- }
393
-
394
- // Fix 3: Server start patterns
395
- for (const pattern of SERVER_PATTERNS) {
396
- if (pattern.test(command)) {
397
- return {
398
- type: 'progress',
399
- confidence: 0.7,
400
- content: `Started server: ${command.trim().slice(0, 200)}`,
401
- project: this.extractProjectFromCwd(input.cwd),
402
- technologies: [],
403
- metadata: { command: rawCommand, action: 'server' },
404
- source: 'hook-passive',
405
- timestamp: new Date().toISOString(),
406
- }
407
- }
408
- }
409
-
410
- // Fix 3: Process management patterns
411
- for (const pattern of PROCESS_PATTERNS) {
412
- if (pattern.test(command)) {
413
- return {
414
- type: 'pattern',
415
- confidence: 0.7,
416
- content: `Process management: ${command.trim().slice(0, 200)}`,
417
- project: this.extractProjectFromCwd(input.cwd),
418
- technologies: [],
419
- metadata: { command: rawCommand, action: 'process' },
420
- source: 'hook-passive',
421
- timestamp: new Date().toISOString(),
422
- }
423
- }
424
- }
425
-
426
- // Fix 3: Docker patterns
427
- for (const pattern of DOCKER_PATTERNS) {
428
- if (pattern.test(command)) {
429
- return {
430
- type: 'progress',
431
- confidence: 0.75,
432
- content: `Docker: ${command.trim().slice(0, 200)}`,
433
- project: this.extractProjectFromCwd(input.cwd),
434
- technologies: ['docker'],
435
- metadata: { command: rawCommand, action: 'docker' },
436
- source: 'hook-passive',
437
- timestamp: new Date().toISOString(),
438
- }
439
- }
440
- }
441
-
442
- // Check response text for decision/correction language
443
- const responseText = this.extractResponseText(input.tool_response)
444
- if (responseText) {
445
- const correction = this.detectCorrectionLanguage(responseText)
446
- if (correction) {
447
- return {
448
- type: 'correction',
449
- confidence: 0.7,
450
- content: correction,
451
- project: this.extractProjectFromCwd(input.cwd),
452
- technologies: [],
453
- metadata: { command: rawCommand, action: 'bash' },
454
- source: 'hook-passive',
455
- timestamp: new Date().toISOString(),
456
- }
457
- }
458
- }
459
-
460
- return null
461
- }
462
-
463
- // --- Fix 6: Stop event handler ---
464
-
465
- private classifySessionEnd(input: HookInput): CapturedKnowledge | null {
466
- const responseText = this.extractResponseText(input.tool_response)
467
- if (!responseText || responseText.length < 20) return null
468
-
469
- return {
470
- type: 'progress',
471
- confidence: 0.75,
472
- content: `Session summary: ${responseText.slice(0, 300)}`,
473
- project: this.extractProjectFromCwd(input.cwd),
474
- technologies: [],
475
- metadata: { action: 'session-end' },
476
- source: 'hook-passive',
477
- timestamp: new Date().toISOString(),
478
- }
479
- }
480
-
481
- // --- Fix 1: Smart content summarization ---
482
-
483
- /** Extract meaningful summary from file content using fast regex/string parsing */
484
- private summarizeFileContent(content: string, filePath: string): string {
485
- const basename = filePath.split(/[/\\]/).pop() || filePath
486
- const parts: string[] = []
487
-
488
- // Detect file purpose from structure
489
- const purpose = this.detectFilePurpose(content, filePath)
490
- if (purpose) parts.push(purpose)
491
-
492
- // Extract external imports/dependencies
493
- const imports = this.extractImportNames(content)
494
- if (imports.length > 0) parts.push(`uses ${imports.slice(0, 5).join(', ')}`)
495
-
496
- // Extract exported names
497
- const exports = this.extractExportNames(content)
498
- if (exports.length > 0) parts.push(`exports ${exports.slice(0, 5).join(', ')}`)
499
-
500
- // Extract key framework/API patterns
501
- const patterns = this.extractKeyPatterns(content)
502
- if (patterns.length > 0) parts.push(patterns.join(', '))
503
-
504
- if (parts.length === 0) {
505
- const role = this.extractRoleFromPath(filePath)
506
- const techs = this.extractTechFromPath(filePath)
507
- return `Created ${role ? role + ' ' : ''}file: ${this.shortenPath(filePath)}${techs.length ? ` (${techs.join(', ')})` : ''}`
508
- }
509
-
510
- return `Created ${basename}: ${parts.join(' \u2014 ')}`.slice(0, 300)
511
- }
512
-
513
- /** Detect file purpose from content patterns and path */
514
- private detectFilePurpose(content: string, filePath: string): string | null {
515
- const path = filePath.toLowerCase()
516
-
517
- if (path.includes('test') || path.includes('spec') || /\b(?:describe|it|test)\s*\(/.test(content)) {
518
- return 'test file'
519
- }
520
- if (/\b(?:createServer|Bun\.serve|express\s*\(\)|app\.listen|new\s+Hono|Fastify)\b/.test(content)) {
521
- return 'server'
522
- }
523
- if (/\breturn\s*\(?\s*</.test(content) && /(?:function|const)\s+[A-Z]\w*/.test(content)) {
524
- return 'React component'
525
- }
526
- if (/\b(?:eslint|prettier|jest|vite|webpack|tsconfig|babel)\b/i.test(path)) {
527
- return 'configuration'
528
- }
529
- if (/\b(?:middleware)\b/i.test(path) || /\b(?:req\s*,\s*res\s*,\s*next)\b/.test(content)) {
530
- return 'middleware'
531
- }
532
- if (/\b(?:interface|type)\s+[A-Z]\w+/.test(content) && !/\bfunction\b/.test(content)) {
533
- return 'type definitions'
534
- }
535
- if (/(?:#!\/usr\/bin|process\.argv|yargs|commander)/.test(content)) {
536
- return 'CLI script'
537
- }
538
-
539
- return null
540
- }
541
-
542
- /** Extract external package names from import/require statements */
543
- private extractImportNames(content: string): string[] {
544
- const names: string[] = []
545
-
546
- // ES imports: from 'package'
547
- const esImports = content.matchAll(/from\s+['"]([^'"./][^'"]*)['"]/g)
548
- for (const match of esImports) {
549
- const pkg = match[1].split('/')[0]
550
- if (pkg && !names.includes(pkg)) names.push(pkg)
551
- }
552
-
553
- // require('package')
554
- const requires = content.matchAll(/require\s*\(\s*['"]([^'"./][^'"]*)['"]\s*\)/g)
555
- for (const match of requires) {
556
- const pkg = match[1].split('/')[0]
557
- if (pkg && !names.includes(pkg)) names.push(pkg)
558
- }
559
-
560
- // Python imports
561
- const pyImports = content.matchAll(/^\s*(?:from|import)\s+(\w+)/gm)
562
- for (const match of pyImports) {
563
- if (match[1] && !names.includes(match[1]) && match[1] !== 'from' && match[1] !== 'import') {
564
- names.push(match[1])
565
- }
566
- }
567
-
568
- return names.slice(0, 8)
569
- }
570
-
571
- /** Extract exported function/class/const names */
572
- private extractExportNames(content: string): string[] {
573
- const names: string[] = []
574
- const exportMatches = content.matchAll(/export\s+(?:default\s+)?(?:function|class|const|let|var|interface|type|enum)\s+(\w+)/g)
575
- for (const match of exportMatches) {
576
- if (match[1] && !names.includes(match[1])) names.push(match[1])
577
- }
578
- return names.slice(0, 8)
579
- }
580
-
581
- /** Detect key API/framework patterns in content */
582
- private extractKeyPatterns(content: string): string[] {
583
- const found: string[] = []
584
- for (const [regex, label] of KEY_API_PATTERNS) {
585
- if (regex.test(content)) found.push(label)
586
- if (found.length >= 4) break
587
- }
588
- return found
589
- }
590
-
591
- // --- Fix 5: Pre-storage deduplication ---
592
-
593
- /** Check if content was recently seen (LRU hash set) */
594
- private isDuplicate(content: string): boolean {
595
- const hash = this.simpleHash(content)
596
- if (this.recentHashes.has(hash)) return true
597
- this.recentHashes.add(hash)
598
- if (this.recentHashes.size > this.maxRecent) {
599
- const first = this.recentHashes.values().next().value
600
- if (first !== undefined) this.recentHashes.delete(first)
601
- }
602
- return false
603
- }
604
-
605
- /** Fast 32-bit string hash */
606
- private simpleHash(str: string): string {
607
- let hash = 0
608
- for (let i = 0; i < str.length; i++) {
609
- const char = str.charCodeAt(i)
610
- hash = ((hash << 5) - hash) + char
611
- hash |= 0
612
- }
613
- return hash.toString(36)
614
- }
615
-
616
- // --- Existing helpers ---
617
-
618
- /** Extract technology names from file path based on extension */
619
- private extractTechFromPath(filePath: string): string[] {
620
- const techs: string[] = []
621
- const ext = filePath.match(/\.[a-z]+$/i)?.[0]?.toLowerCase()
622
- if (ext && EXT_TO_TECH[ext]) {
623
- techs.push(EXT_TO_TECH[ext])
624
- }
625
-
626
- // Check for Dockerfile without extension
627
- const basename = filePath.split('/').pop()?.toLowerCase() || ''
628
- if (basename === 'dockerfile' || basename.startsWith('dockerfile.')) {
629
- techs.push('docker')
630
- }
631
- if (basename === 'docker-compose.yml' || basename === 'docker-compose.yaml') {
632
- techs.push('docker')
633
- }
634
-
635
- return techs
636
- }
637
-
638
- /** Extract file role from path segments */
639
- private extractRoleFromPath(filePath: string): string | undefined {
640
- const segments = filePath.toLowerCase().split(/[/\\]/)
641
- for (const segment of segments) {
642
- if (PATH_ROLE_MAP[segment]) return PATH_ROLE_MAP[segment]
643
- }
644
- return undefined
645
- }
646
-
647
- /** Extract technology names from package install strings */
648
- private extractTechFromPackages(packages: string): string[] {
649
- return packages
650
- .split(/\s+/)
651
- .filter(p => !p.startsWith('-') && p.length > 1)
652
- .map(p => p.replace(/@[^/]+$/, '')) // strip version
653
- .slice(0, 10) // limit
654
- }
655
-
656
- /** Detect decision language in text, return the relevant sentence */
657
- private detectDecisionLanguage(text: string): string | null {
658
- const lower = text.toLowerCase()
659
- for (const phrase of DECISION_PHRASES) {
660
- const idx = lower.indexOf(phrase)
661
- if (idx !== -1) {
662
- // Extract surrounding context (up to 300 chars)
663
- const start = Math.max(0, text.lastIndexOf('\n', idx) + 1)
664
- const end = Math.min(text.length, text.indexOf('\n', idx + phrase.length))
665
- const sentence = text.slice(start, end === -1 ? Math.min(idx + 300, text.length) : end).trim()
666
- if (sentence.length > 10) return sentence
667
- }
668
- }
669
- return null
670
- }
671
-
672
- /** Detect correction/lesson language in text */
673
- private detectCorrectionLanguage(text: string): string | null {
674
- const lower = text.toLowerCase()
675
- for (const phrase of CORRECTION_PHRASES) {
676
- const idx = lower.indexOf(phrase)
677
- if (idx !== -1) {
678
- const start = Math.max(0, text.lastIndexOf('\n', idx) + 1)
679
- const end = Math.min(text.length, text.indexOf('\n', idx + phrase.length))
680
- const sentence = text.slice(start, end === -1 ? Math.min(idx + 300, text.length) : end).trim()
681
- if (sentence.length > 10) return sentence
682
- }
683
- }
684
- return null
685
- }
686
-
687
- /** Extract project name from cwd (last directory segment) */
688
- private extractProjectFromCwd(cwd: string): string | undefined {
689
- if (!cwd) return undefined
690
- // Split on both / and \ for cross-platform support
691
- const parts = cwd.split(/[/\\]/).filter(Boolean)
692
- const last = parts.pop()
693
- if (last && last.length > 1 && last.length < 50) {
694
- return last.replace(/\s+/g, '-').toLowerCase()
695
- }
696
- return undefined
697
- }
698
-
699
- /** Shorten a file path for display */
700
- private shortenPath(filePath: string): string {
701
- const parts = filePath.split(/[/\\]/)
702
- if (parts.length <= 3) return filePath
703
- return `.../${parts.slice(-3).join('/')}`
704
- }
705
-
706
- /** Extract text content from tool_response */
707
- private extractResponseText(response: HookInput['tool_response']): string | null {
708
- if (!response) return null
709
-
710
- if (typeof response.content === 'string') {
711
- return response.content.slice(0, 2000)
712
- }
713
-
714
- if (Array.isArray(response.content)) {
715
- const texts = response.content
716
- .filter(block => block.type === 'text' && block.text)
717
- .map(block => block.text!)
718
- return texts.join('\n').slice(0, 2000) || null
719
- }
720
-
721
- return null
722
- }
723
- }
1
+ /**
2
+ * Phase 17: Passive Classifier
3
+ * Extracts knowledge from tool outputs using lightweight pattern matching.
4
+ * No embeddings needed — runs fast enough for hook context (<200ms).
5
+ */
6
+
7
+ import type { HookInput, CapturedKnowledge } from './types'
8
+
9
+ /** Phrases that indicate a decision was made */
10
+ const DECISION_PHRASES = [
11
+ 'i recommend', 'you should use', 'the best approach', 'i suggest',
12
+ 'better to use', 'prefer using', 'go with', 'choose', 'instead of',
13
+ 'the right choice', 'decided to', "let's use", 'we will use',
14
+ 'the solution is', 'implement using', 'going with', 'switching to',
15
+ 'adopting', 'we chose', 'the plan is to'
16
+ ]
17
+
18
+ /** Phrases indicating a correction or lesson */
19
+ const CORRECTION_PHRASES = [
20
+ 'the bug was', 'the issue was', 'the problem was', 'mistake was',
21
+ 'should have', 'should not have', "shouldn't have",
22
+ 'lesson learned', "don't use", 'avoid using', 'never use',
23
+ 'the fix is', 'the fix was', 'fixed by', 'solved by'
24
+ ]
25
+
26
+ /** Package install patterns for bash commands */
27
+ const INSTALL_PATTERNS = [
28
+ /(?:npm|yarn|pnpm|bun)\s+(?:install|add|i)\s+(.+)/i,
29
+ /pip\s+install\s+(.+)/i,
30
+ /cargo\s+add\s+(.+)/i,
31
+ /go\s+get\s+(.+)/i,
32
+ /gem\s+install\s+(.+)/i,
33
+ ]
34
+
35
+ /** Git operation patterns */
36
+ const GIT_PATTERNS = [
37
+ /git\s+commit\s+.*-m\s+["'](.+?)["']/i,
38
+ /git\s+merge\s+(\S+)/i,
39
+ /git\s+checkout\s+-b\s+(\S+)/i,
40
+ /git\s+branch\s+(\S+)/i,
41
+ ]
42
+
43
+ /** Test/build command patterns */
44
+ const BUILD_PATTERNS = [
45
+ /(?:npm|yarn|pnpm|bun)\s+(?:run\s+)?(?:test|build|lint|typecheck|check)/i,
46
+ /(?:jest|vitest|pytest|cargo\s+test|go\s+test)/i,
47
+ ]
48
+
49
+ /** Bash commands to skip (low signal) */
50
+ const SKIP_COMMANDS = new Set([
51
+ 'cd', 'ls', 'cat', 'head', 'tail', 'pwd', 'echo', 'clear', 'which', 'whoami',
52
+ 'date', 'env', 'printenv', 'export', 'source', 'alias', 'history',
53
+ ])
54
+
55
+ /** File extension to technology mapping */
56
+ const EXT_TO_TECH: Record<string, string> = {
57
+ '.ts': 'typescript', '.tsx': 'typescript', '.js': 'javascript', '.jsx': 'javascript',
58
+ '.py': 'python', '.rs': 'rust', '.go': 'go', '.java': 'java',
59
+ '.rb': 'ruby', '.php': 'php', '.swift': 'swift', '.kt': 'kotlin',
60
+ '.vue': 'vue', '.svelte': 'svelte', '.astro': 'astro',
61
+ '.css': 'css', '.scss': 'sass', '.less': 'less',
62
+ '.sql': 'sql', '.graphql': 'graphql', '.gql': 'graphql',
63
+ '.yml': 'yaml', '.yaml': 'yaml', '.toml': 'toml',
64
+ '.dockerfile': 'docker', '.prisma': 'prisma',
65
+ }
66
+
67
+ /** Path segments that indicate file role */
68
+ const PATH_ROLE_MAP: Record<string, string> = {
69
+ 'test': 'testing', 'tests': 'testing', '__tests__': 'testing', 'spec': 'testing',
70
+ 'src': 'source', 'lib': 'library', 'utils': 'utility', 'helpers': 'utility',
71
+ 'components': 'component', 'pages': 'page', 'routes': 'routing',
72
+ 'api': 'api', 'server': 'server', 'client': 'client',
73
+ 'config': 'configuration', 'scripts': 'scripting',
74
+ 'hooks': 'hooks', 'middleware': 'middleware', 'types': 'types',
75
+ }
76
+
77
+ export class PassiveClassifier {
78
+ /**
79
+ * Classify a hook event and extract knowledge if found.
80
+ * Returns null if no knowledge worth capturing.
81
+ */
82
+ classify(input: HookInput): CapturedKnowledge | null {
83
+ const toolName = input.tool_name?.toLowerCase()
84
+ if (!toolName) return null
85
+
86
+ switch (toolName) {
87
+ case 'edit':
88
+ case 'write':
89
+ return this.classifyFileEdit(input)
90
+ case 'bash':
91
+ return this.classifyBashCommand(input)
92
+ default:
93
+ // Read, Glob, Grep — skip (read-only, low signal)
94
+ return null
95
+ }
96
+ }
97
+
98
+ private classifyFileEdit(input: HookInput): CapturedKnowledge | null {
99
+ const toolInput = input.tool_input
100
+ if (!toolInput) return null
101
+
102
+ const filePath = (toolInput.file_path || toolInput.path || '') as string
103
+ if (!filePath) return null
104
+
105
+ const technologies = this.extractTechFromPath(filePath)
106
+ const role = this.extractRoleFromPath(filePath)
107
+
108
+ // Check if the edit content contains decision language
109
+ const content = toolInput.new_string || toolInput.content || ''
110
+ const responseText = this.extractResponseText(input.tool_response)
111
+
112
+ // For Write tool: only capture if content contains decision or correction language
113
+ // Plain file creations ("Created file: X") are noise — zero recall value
114
+ if (input.tool_name?.toLowerCase() === 'write') {
115
+ const writeContent = typeof content === 'string' ? content : ''
116
+ const writeResponse = responseText || ''
117
+ const combined = writeContent + ' ' + writeResponse
118
+
119
+ // Check for decision language in file content or response
120
+ const decisionInWrite = this.detectDecisionLanguage(combined)
121
+ if (decisionInWrite) {
122
+ return {
123
+ type: 'decision',
124
+ confidence: 0.75,
125
+ content: decisionInWrite,
126
+ project: this.extractProjectFromCwd(input.cwd),
127
+ technologies,
128
+ metadata: { filePath, role, action: 'create' },
129
+ source: 'hook-passive',
130
+ timestamp: new Date().toISOString(),
131
+ }
132
+ }
133
+
134
+ // Check for correction language
135
+ const correctionInWrite = this.detectCorrectionLanguage(combined)
136
+ if (correctionInWrite) {
137
+ return {
138
+ type: 'correction',
139
+ confidence: 0.75,
140
+ content: correctionInWrite,
141
+ project: this.extractProjectFromCwd(input.cwd),
142
+ technologies,
143
+ metadata: { filePath, role, action: 'create' },
144
+ source: 'hook-passive',
145
+ timestamp: new Date().toISOString(),
146
+ }
147
+ }
148
+
149
+ // No decision or correction language found — skip this file creation
150
+ return null
151
+ }
152
+
153
+ // For edits, only capture if they look significant
154
+ if (typeof content === 'string' && content.length > 50) {
155
+ const decisionInContent = this.detectDecisionLanguage(content)
156
+ if (decisionInContent) {
157
+ return {
158
+ type: 'decision',
159
+ confidence: 0.75,
160
+ content: decisionInContent,
161
+ project: this.extractProjectFromCwd(input.cwd),
162
+ technologies,
163
+ metadata: { filePath, role, action: 'edit' },
164
+ source: 'hook-passive',
165
+ timestamp: new Date().toISOString(),
166
+ }
167
+ }
168
+ }
169
+
170
+ // Check tool response for decision language
171
+ if (responseText) {
172
+ const decisionInResponse = this.detectDecisionLanguage(responseText)
173
+ if (decisionInResponse) {
174
+ return {
175
+ type: 'decision',
176
+ confidence: 0.7,
177
+ content: decisionInResponse,
178
+ project: this.extractProjectFromCwd(input.cwd),
179
+ technologies,
180
+ metadata: { filePath, role, action: 'edit' },
181
+ source: 'hook-passive',
182
+ timestamp: new Date().toISOString(),
183
+ }
184
+ }
185
+ }
186
+
187
+ return null
188
+ }
189
+
190
+ private classifyBashCommand(input: HookInput): CapturedKnowledge | null {
191
+ const rawCommand = (input.tool_input?.command || '') as string
192
+ if (!rawCommand || rawCommand.length < 3) return null
193
+
194
+ // Split compound commands (cd "..." && bun add react) into sub-commands
195
+ const subCommands = rawCommand.split(/\s*(?:&&|\|\||;)\s*/).map(s => s.trim()).filter(Boolean)
196
+
197
+ // Find the first meaningful sub-command (skip cd, export, etc.)
198
+ const command = subCommands.find(sub => {
199
+ const firstWord = sub.split(/\s+/)[0]?.toLowerCase()
200
+ return !firstWord || !SKIP_COMMANDS.has(firstWord)
201
+ }) || rawCommand
202
+
203
+ // If all sub-commands are skip-worthy, bail
204
+ const firstWord = command.trim().split(/\s+/)[0]?.toLowerCase()
205
+ if (firstWord && SKIP_COMMANDS.has(firstWord)) return null
206
+
207
+ // Package installs
208
+ for (const pattern of INSTALL_PATTERNS) {
209
+ const match = command.match(pattern)
210
+ if (match) {
211
+ const packages = match[1]?.trim()
212
+ if (packages) {
213
+ return {
214
+ type: 'decision',
215
+ confidence: 0.85,
216
+ content: `Installed package(s): ${packages}`,
217
+ project: this.extractProjectFromCwd(input.cwd),
218
+ technologies: this.extractTechFromPackages(packages),
219
+ metadata: { command: rawCommand, action: 'install' },
220
+ source: 'hook-passive',
221
+ timestamp: new Date().toISOString(),
222
+ }
223
+ }
224
+ }
225
+ }
226
+
227
+ // Git operations
228
+ for (const pattern of GIT_PATTERNS) {
229
+ const match = command.match(pattern)
230
+ if (match) {
231
+ return {
232
+ type: 'progress',
233
+ confidence: 0.8,
234
+ content: `Git: ${command.trim().slice(0, 200)}`,
235
+ project: this.extractProjectFromCwd(input.cwd),
236
+ technologies: ['git'],
237
+ metadata: { command: rawCommand, action: 'git' },
238
+ source: 'hook-passive',
239
+ timestamp: new Date().toISOString(),
240
+ }
241
+ }
242
+ }
243
+
244
+ // Test/build runs — check all sub-commands since test may follow cd
245
+ const buildCommand = subCommands.find(sub => BUILD_PATTERNS.some(p => p.test(sub))) || command
246
+ for (const pattern of BUILD_PATTERNS) {
247
+ if (pattern.test(buildCommand)) {
248
+ const responseText = this.extractResponseText(input.tool_response)
249
+ const failed = responseText?.toLowerCase().includes('fail') ||
250
+ responseText?.toLowerCase().includes('error')
251
+
252
+ if (failed) {
253
+ return {
254
+ type: 'correction',
255
+ confidence: 0.75,
256
+ content: `Build/test failure: ${buildCommand.trim().slice(0, 100)}`,
257
+ project: this.extractProjectFromCwd(input.cwd),
258
+ technologies: [],
259
+ metadata: { command: rawCommand, action: 'build', failed: true },
260
+ source: 'hook-passive',
261
+ timestamp: new Date().toISOString(),
262
+ }
263
+ }
264
+
265
+ return {
266
+ type: 'progress',
267
+ confidence: 0.7,
268
+ content: `Ran: ${buildCommand.trim().slice(0, 200)}`,
269
+ project: this.extractProjectFromCwd(input.cwd),
270
+ technologies: [],
271
+ metadata: { command: rawCommand, action: 'build', failed: false },
272
+ source: 'hook-passive',
273
+ timestamp: new Date().toISOString(),
274
+ }
275
+ }
276
+ }
277
+
278
+ // Check response text for decision/correction language
279
+ const responseText = this.extractResponseText(input.tool_response)
280
+ if (responseText) {
281
+ const correction = this.detectCorrectionLanguage(responseText)
282
+ if (correction) {
283
+ return {
284
+ type: 'correction',
285
+ confidence: 0.7,
286
+ content: correction,
287
+ project: this.extractProjectFromCwd(input.cwd),
288
+ technologies: [],
289
+ metadata: { command: rawCommand, action: 'bash' },
290
+ source: 'hook-passive',
291
+ timestamp: new Date().toISOString(),
292
+ }
293
+ }
294
+ }
295
+
296
+ return null
297
+ }
298
+
299
+ /** Extract technology names from file path based on extension */
300
+ private extractTechFromPath(filePath: string): string[] {
301
+ const techs: string[] = []
302
+ const ext = filePath.match(/\.[a-z]+$/i)?.[0]?.toLowerCase()
303
+ if (ext && EXT_TO_TECH[ext]) {
304
+ techs.push(EXT_TO_TECH[ext])
305
+ }
306
+
307
+ // Check for Dockerfile without extension
308
+ const basename = filePath.split('/').pop()?.toLowerCase() || ''
309
+ if (basename === 'dockerfile' || basename.startsWith('dockerfile.')) {
310
+ techs.push('docker')
311
+ }
312
+ if (basename === 'docker-compose.yml' || basename === 'docker-compose.yaml') {
313
+ techs.push('docker')
314
+ }
315
+
316
+ return techs
317
+ }
318
+
319
+ /** Extract file role from path segments */
320
+ private extractRoleFromPath(filePath: string): string | undefined {
321
+ const segments = filePath.toLowerCase().split(/[/\\]/)
322
+ for (const segment of segments) {
323
+ if (PATH_ROLE_MAP[segment]) return PATH_ROLE_MAP[segment]
324
+ }
325
+ return undefined
326
+ }
327
+
328
+ /** Extract technology names from package install strings */
329
+ private extractTechFromPackages(packages: string): string[] {
330
+ return packages
331
+ .split(/\s+/)
332
+ .filter(p => !p.startsWith('-') && p.length > 1)
333
+ .map(p => p.replace(/@[^/]+$/, '')) // strip version
334
+ .slice(0, 10) // limit
335
+ }
336
+
337
+ /** Detect decision language in text, return the relevant sentence */
338
+ private detectDecisionLanguage(text: string): string | null {
339
+ const lower = text.toLowerCase()
340
+ for (const phrase of DECISION_PHRASES) {
341
+ const idx = lower.indexOf(phrase)
342
+ if (idx !== -1) {
343
+ // Extract surrounding context (up to 300 chars)
344
+ const start = Math.max(0, text.lastIndexOf('\n', idx) + 1)
345
+ const end = Math.min(text.length, text.indexOf('\n', idx + phrase.length))
346
+ const sentence = text.slice(start, end === -1 ? Math.min(idx + 300, text.length) : end).trim()
347
+ if (sentence.length > 10) return sentence
348
+ }
349
+ }
350
+ return null
351
+ }
352
+
353
+ /** Detect correction/lesson language in text */
354
+ private detectCorrectionLanguage(text: string): string | null {
355
+ const lower = text.toLowerCase()
356
+ for (const phrase of CORRECTION_PHRASES) {
357
+ const idx = lower.indexOf(phrase)
358
+ if (idx !== -1) {
359
+ const start = Math.max(0, text.lastIndexOf('\n', idx) + 1)
360
+ const end = Math.min(text.length, text.indexOf('\n', idx + phrase.length))
361
+ const sentence = text.slice(start, end === -1 ? Math.min(idx + 300, text.length) : end).trim()
362
+ if (sentence.length > 10) return sentence
363
+ }
364
+ }
365
+ return null
366
+ }
367
+
368
+ /** Extract project name from cwd (last directory segment) */
369
+ private extractProjectFromCwd(cwd: string): string | undefined {
370
+ if (!cwd) return undefined
371
+ // Split on both / and \ for cross-platform support
372
+ const parts = cwd.split(/[/\\]/).filter(Boolean)
373
+ const last = parts.pop()
374
+ if (last && last.length > 1 && last.length < 50) {
375
+ return last.replace(/\s+/g, '-').toLowerCase()
376
+ }
377
+ return undefined
378
+ }
379
+
380
+ /** Shorten a file path for display */
381
+ private shortenPath(filePath: string): string {
382
+ const parts = filePath.split(/[/\\]/)
383
+ if (parts.length <= 3) return filePath
384
+ return `.../${parts.slice(-3).join('/')}`
385
+ }
386
+
387
+ /** Extract text content from tool_response */
388
+ private extractResponseText(response: HookInput['tool_response']): string | null {
389
+ if (!response) return null
390
+
391
+ if (typeof response.content === 'string') {
392
+ return response.content.slice(0, 2000)
393
+ }
394
+
395
+ if (Array.isArray(response.content)) {
396
+ const texts = response.content
397
+ .filter(block => block.type === 'text' && block.text)
398
+ .map(block => block.text!)
399
+ return texts.join('\n').slice(0, 2000) || null
400
+ }
401
+
402
+ return null
403
+ }
404
+ }