claude-brain 0.15.2 → 0.16.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 (249) 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/deduplicator.ts +72 -72
  64. package/src/hooks/git-capture.ts +109 -109
  65. package/src/hooks/git-hook-installer.ts +207 -207
  66. package/src/hooks/index.ts +20 -20
  67. package/src/hooks/installer.ts +194 -194
  68. package/src/hooks/passive-classifier.ts +404 -723
  69. package/src/hooks/queue.ts +129 -129
  70. package/src/hooks/session-tracker.ts +312 -275
  71. package/src/hooks/types.ts +47 -47
  72. package/src/index.ts +7 -7
  73. package/src/intelligence/cross-project/affinity.ts +162 -162
  74. package/src/intelligence/cross-project/generalizer.ts +283 -283
  75. package/src/intelligence/cross-project/index.ts +13 -13
  76. package/src/intelligence/cross-project/transfer.ts +201 -201
  77. package/src/intelligence/index.ts +24 -24
  78. package/src/intelligence/optimization/index.ts +10 -10
  79. package/src/intelligence/optimization/precompute.ts +202 -202
  80. package/src/intelligence/optimization/semantic-cache.ts +207 -207
  81. package/src/intelligence/prediction/context-anticipator.ts +198 -198
  82. package/src/intelligence/prediction/decision-predictor.ts +184 -184
  83. package/src/intelligence/prediction/index.ts +13 -13
  84. package/src/intelligence/prediction/recommender.ts +268 -268
  85. package/src/intelligence/reasoning/chain-retrieval.ts +247 -247
  86. package/src/intelligence/reasoning/counterfactual.ts +248 -248
  87. package/src/intelligence/reasoning/index.ts +13 -13
  88. package/src/intelligence/reasoning/synthesizer.ts +169 -169
  89. package/src/intelligence/temporal/evolution.ts +197 -197
  90. package/src/intelligence/temporal/index.ts +16 -16
  91. package/src/intelligence/temporal/query-processor.ts +190 -190
  92. package/src/intelligence/temporal/timeline.ts +259 -259
  93. package/src/intelligence/temporal/trends.ts +263 -263
  94. package/src/knowledge/entity-extractor.ts +416 -416
  95. package/src/knowledge/graph/builder.ts +185 -185
  96. package/src/knowledge/graph/linker.ts +201 -201
  97. package/src/knowledge/graph/memory-graph.ts +359 -359
  98. package/src/knowledge/graph/schema.ts +99 -99
  99. package/src/knowledge/graph/search.ts +168 -168
  100. package/src/knowledge/relationship-extractor.ts +108 -108
  101. package/src/memory/chroma/client.ts +174 -174
  102. package/src/memory/chroma/collection-manager.ts +94 -94
  103. package/src/memory/chroma/config.ts +57 -57
  104. package/src/memory/chroma/embeddings.ts +155 -155
  105. package/src/memory/chroma/index.ts +82 -82
  106. package/src/memory/chroma/migration.ts +270 -270
  107. package/src/memory/chroma/schemas.ts +69 -69
  108. package/src/memory/chroma/search.ts +315 -315
  109. package/src/memory/chroma/store.ts +741 -741
  110. package/src/memory/consolidation/archiver.ts +164 -164
  111. package/src/memory/consolidation/merger.ts +186 -186
  112. package/src/memory/consolidation/scorer.ts +138 -138
  113. package/src/memory/context-builder.ts +236 -236
  114. package/src/memory/database.ts +169 -169
  115. package/src/memory/embedding-utils.ts +156 -156
  116. package/src/memory/embeddings.ts +226 -226
  117. package/src/memory/episodic/detector.ts +108 -108
  118. package/src/memory/episodic/manager.ts +351 -351
  119. package/src/memory/episodic/summarizer.ts +179 -179
  120. package/src/memory/episodic/types.ts +52 -52
  121. package/src/memory/index.ts +582 -582
  122. package/src/memory/knowledge-extractor.ts +455 -455
  123. package/src/memory/learning.ts +378 -378
  124. package/src/memory/patterns.ts +396 -396
  125. package/src/memory/schema.ts +88 -88
  126. package/src/memory/search.ts +309 -309
  127. package/src/memory/store.ts +787 -787
  128. package/src/memory/types.ts +121 -121
  129. package/src/orchestrator/coordinator.ts +272 -272
  130. package/src/orchestrator/decision-logger.ts +228 -228
  131. package/src/orchestrator/event-emitter.ts +198 -198
  132. package/src/orchestrator/event-queue.ts +184 -184
  133. package/src/orchestrator/handlers/base-handler.ts +70 -70
  134. package/src/orchestrator/handlers/context-handler.ts +73 -73
  135. package/src/orchestrator/handlers/decision-handler.ts +204 -204
  136. package/src/orchestrator/handlers/index.ts +10 -10
  137. package/src/orchestrator/handlers/status-handler.ts +131 -131
  138. package/src/orchestrator/handlers/task-handler.ts +171 -171
  139. package/src/orchestrator/index.ts +275 -275
  140. package/src/orchestrator/task-parser.ts +284 -284
  141. package/src/orchestrator/types.ts +98 -98
  142. package/src/packs/index.ts +9 -9
  143. package/src/packs/loader.ts +134 -134
  144. package/src/packs/manager.ts +204 -204
  145. package/src/packs/ranker.ts +78 -78
  146. package/src/packs/types.ts +81 -81
  147. package/src/phase12/index.ts +5 -5
  148. package/src/retrieval/bm25/index.ts +300 -300
  149. package/src/retrieval/bm25/tokenizer.ts +184 -184
  150. package/src/retrieval/feedback/adaptive.ts +223 -223
  151. package/src/retrieval/feedback/index.ts +16 -16
  152. package/src/retrieval/feedback/metrics.ts +223 -223
  153. package/src/retrieval/feedback/store.ts +283 -283
  154. package/src/retrieval/fusion/index.ts +194 -194
  155. package/src/retrieval/fusion/rrf.ts +163 -163
  156. package/src/retrieval/index.ts +12 -12
  157. package/src/retrieval/pipeline.ts +375 -375
  158. package/src/retrieval/query/expander.ts +198 -198
  159. package/src/retrieval/query/index.ts +27 -27
  160. package/src/retrieval/query/intent-classifier.ts +236 -236
  161. package/src/retrieval/query/temporal-parser.ts +295 -295
  162. package/src/retrieval/reranker/index.ts +188 -188
  163. package/src/retrieval/reranker/model.ts +95 -95
  164. package/src/retrieval/service.ts +125 -125
  165. package/src/retrieval/types.ts +162 -162
  166. package/src/routing/entity-extractor.ts +428 -428
  167. package/src/routing/intent-classifier.ts +450 -436
  168. package/src/routing/response-filter.ts +261 -258
  169. package/src/routing/router.ts +1441 -1322
  170. package/src/routing/search-engine.ts +515 -475
  171. package/src/routing/types.ts +94 -94
  172. package/src/scripts/health-check.ts +118 -118
  173. package/src/scripts/setup.ts +122 -122
  174. package/src/server/handlers/call-tool.ts +156 -156
  175. package/src/server/handlers/index.ts +9 -9
  176. package/src/server/handlers/list-tools.ts +35 -35
  177. package/src/server/handlers/tools/analyze-decision-evolution.ts +151 -151
  178. package/src/server/handlers/tools/auto-remember.ts +200 -200
  179. package/src/server/handlers/tools/brain.ts +85 -85
  180. package/src/server/handlers/tools/create-project.ts +135 -135
  181. package/src/server/handlers/tools/detect-trends.ts +144 -144
  182. package/src/server/handlers/tools/find-cross-project-patterns.ts +168 -168
  183. package/src/server/handlers/tools/get-activity-log.ts +194 -194
  184. package/src/server/handlers/tools/get-code-standards.ts +124 -124
  185. package/src/server/handlers/tools/get-corrections.ts +154 -154
  186. package/src/server/handlers/tools/get-decision-timeline.ts +172 -172
  187. package/src/server/handlers/tools/get-episode.ts +103 -103
  188. package/src/server/handlers/tools/get-patterns.ts +158 -158
  189. package/src/server/handlers/tools/get-phase12-status.ts +63 -63
  190. package/src/server/handlers/tools/get-project-context.ts +75 -75
  191. package/src/server/handlers/tools/get-recommendations.ts +145 -145
  192. package/src/server/handlers/tools/index.ts +31 -31
  193. package/src/server/handlers/tools/init-project.ts +757 -757
  194. package/src/server/handlers/tools/list-episodes.ts +90 -90
  195. package/src/server/handlers/tools/list-projects.ts +125 -125
  196. package/src/server/handlers/tools/rate-memory.ts +101 -101
  197. package/src/server/handlers/tools/recall-similar.ts +87 -87
  198. package/src/server/handlers/tools/recognize-pattern.ts +126 -126
  199. package/src/server/handlers/tools/record-correction.ts +125 -125
  200. package/src/server/handlers/tools/remember-decision.ts +153 -153
  201. package/src/server/handlers/tools/schemas.ts +253 -253
  202. package/src/server/handlers/tools/search-knowledge-graph.ts +102 -102
  203. package/src/server/handlers/tools/smart-context.ts +146 -146
  204. package/src/server/handlers/tools/update-progress.ts +131 -131
  205. package/src/server/handlers/tools/what-if-analysis.ts +135 -135
  206. package/src/server/http-api.ts +693 -693
  207. package/src/server/index.ts +40 -40
  208. package/src/server/mcp-server.ts +283 -283
  209. package/src/server/providers/index.ts +7 -7
  210. package/src/server/providers/prompts.ts +327 -327
  211. package/src/server/providers/resources.ts +622 -622
  212. package/src/server/services.ts +468 -468
  213. package/src/server/types.ts +39 -39
  214. package/src/server/utils/error-handler.ts +155 -155
  215. package/src/server/utils/index.ts +13 -13
  216. package/src/server/utils/memory-indicator.ts +83 -83
  217. package/src/server/utils/request-context.ts +122 -122
  218. package/src/server/utils/response-formatter.ts +129 -129
  219. package/src/server/utils/validators.ts +210 -210
  220. package/src/setup/index.ts +48 -48
  221. package/src/setup/wizard.ts +461 -461
  222. package/src/tools/index.ts +24 -24
  223. package/src/tools/registry.ts +115 -115
  224. package/src/tools/schemas.test.ts +30 -30
  225. package/src/tools/schemas.ts +617 -617
  226. package/src/tools/types.ts +412 -412
  227. package/src/utils/circuit-breaker.ts +130 -130
  228. package/src/utils/cleanup.ts +34 -34
  229. package/src/utils/error-handler.ts +132 -132
  230. package/src/utils/error-messages.ts +60 -60
  231. package/src/utils/fallback.ts +45 -45
  232. package/src/utils/index.ts +54 -54
  233. package/src/utils/logger-utils.ts +80 -80
  234. package/src/utils/logger.ts +88 -88
  235. package/src/utils/phase12-helper.ts +56 -56
  236. package/src/utils/retry.ts +94 -94
  237. package/src/utils/timing.ts +47 -47
  238. package/src/utils/transaction.ts +63 -63
  239. package/src/vault/frontmatter.ts +264 -264
  240. package/src/vault/index.ts +318 -318
  241. package/src/vault/paths.ts +106 -106
  242. package/src/vault/query.ts +422 -422
  243. package/src/vault/reader.ts +264 -264
  244. package/src/vault/templates.ts +186 -186
  245. package/src/vault/types.ts +73 -73
  246. package/src/vault/watcher.ts +277 -277
  247. package/src/vault/writer.ts +413 -413
  248. package/tsconfig.json +30 -30
  249. 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
+ }