claude-code-workflow 6.2.7 → 6.3.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 (208) hide show
  1. package/.claude/CLAUDE.md +16 -1
  2. package/.claude/workflows/cli-templates/protocols/analysis-protocol.md +11 -4
  3. package/.claude/workflows/cli-templates/protocols/write-protocol.md +10 -75
  4. package/.claude/workflows/cli-tools-usage.md +14 -24
  5. package/.codex/AGENTS.md +51 -1
  6. package/.codex/prompts/compact.md +378 -0
  7. package/.gemini/GEMINI.md +57 -20
  8. package/ccw/dist/cli.d.ts.map +1 -1
  9. package/ccw/dist/cli.js +21 -8
  10. package/ccw/dist/cli.js.map +1 -1
  11. package/ccw/dist/commands/cli.d.ts +2 -0
  12. package/ccw/dist/commands/cli.d.ts.map +1 -1
  13. package/ccw/dist/commands/cli.js +129 -8
  14. package/ccw/dist/commands/cli.js.map +1 -1
  15. package/ccw/dist/commands/hook.d.ts.map +1 -1
  16. package/ccw/dist/commands/hook.js +3 -2
  17. package/ccw/dist/commands/hook.js.map +1 -1
  18. package/ccw/dist/config/litellm-api-config-manager.d.ts +180 -0
  19. package/ccw/dist/config/litellm-api-config-manager.d.ts.map +1 -0
  20. package/ccw/dist/config/litellm-api-config-manager.js +770 -0
  21. package/ccw/dist/config/litellm-api-config-manager.js.map +1 -0
  22. package/ccw/dist/config/provider-models.d.ts +73 -0
  23. package/ccw/dist/config/provider-models.d.ts.map +1 -0
  24. package/ccw/dist/config/provider-models.js +172 -0
  25. package/ccw/dist/config/provider-models.js.map +1 -0
  26. package/ccw/dist/core/cache-manager.d.ts.map +1 -1
  27. package/ccw/dist/core/cache-manager.js +3 -5
  28. package/ccw/dist/core/cache-manager.js.map +1 -1
  29. package/ccw/dist/core/dashboard-generator.d.ts.map +1 -1
  30. package/ccw/dist/core/dashboard-generator.js +3 -1
  31. package/ccw/dist/core/dashboard-generator.js.map +1 -1
  32. package/ccw/dist/core/routes/cli-routes.d.ts.map +1 -1
  33. package/ccw/dist/core/routes/cli-routes.js +169 -0
  34. package/ccw/dist/core/routes/cli-routes.js.map +1 -1
  35. package/ccw/dist/core/routes/codexlens-routes.d.ts.map +1 -1
  36. package/ccw/dist/core/routes/codexlens-routes.js +234 -18
  37. package/ccw/dist/core/routes/codexlens-routes.js.map +1 -1
  38. package/ccw/dist/core/routes/hooks-routes.d.ts.map +1 -1
  39. package/ccw/dist/core/routes/hooks-routes.js +30 -32
  40. package/ccw/dist/core/routes/hooks-routes.js.map +1 -1
  41. package/ccw/dist/core/routes/litellm-api-routes.d.ts +21 -0
  42. package/ccw/dist/core/routes/litellm-api-routes.d.ts.map +1 -0
  43. package/ccw/dist/core/routes/litellm-api-routes.js +780 -0
  44. package/ccw/dist/core/routes/litellm-api-routes.js.map +1 -0
  45. package/ccw/dist/core/routes/litellm-routes.d.ts +20 -0
  46. package/ccw/dist/core/routes/litellm-routes.d.ts.map +1 -0
  47. package/ccw/dist/core/routes/litellm-routes.js +85 -0
  48. package/ccw/dist/core/routes/litellm-routes.js.map +1 -0
  49. package/ccw/dist/core/routes/mcp-routes.js +2 -2
  50. package/ccw/dist/core/routes/mcp-routes.js.map +1 -1
  51. package/ccw/dist/core/routes/status-routes.d.ts.map +1 -1
  52. package/ccw/dist/core/routes/status-routes.js +39 -0
  53. package/ccw/dist/core/routes/status-routes.js.map +1 -1
  54. package/ccw/dist/core/routes/system-routes.js +1 -1
  55. package/ccw/dist/core/routes/system-routes.js.map +1 -1
  56. package/ccw/dist/core/server.d.ts.map +1 -1
  57. package/ccw/dist/core/server.js +15 -1
  58. package/ccw/dist/core/server.js.map +1 -1
  59. package/ccw/dist/mcp-server/index.js +1 -1
  60. package/ccw/dist/mcp-server/index.js.map +1 -1
  61. package/ccw/dist/tools/claude-cli-tools.d.ts +82 -0
  62. package/ccw/dist/tools/claude-cli-tools.d.ts.map +1 -0
  63. package/ccw/dist/tools/claude-cli-tools.js +216 -0
  64. package/ccw/dist/tools/claude-cli-tools.js.map +1 -0
  65. package/ccw/dist/tools/cli-executor.d.ts.map +1 -1
  66. package/ccw/dist/tools/cli-executor.js +76 -14
  67. package/ccw/dist/tools/cli-executor.js.map +1 -1
  68. package/ccw/dist/tools/codex-lens.d.ts +9 -2
  69. package/ccw/dist/tools/codex-lens.d.ts.map +1 -1
  70. package/ccw/dist/tools/codex-lens.js +114 -9
  71. package/ccw/dist/tools/codex-lens.js.map +1 -1
  72. package/ccw/dist/tools/context-cache-store.d.ts +136 -0
  73. package/ccw/dist/tools/context-cache-store.d.ts.map +1 -0
  74. package/ccw/dist/tools/context-cache-store.js +256 -0
  75. package/ccw/dist/tools/context-cache-store.js.map +1 -0
  76. package/ccw/dist/tools/context-cache.d.ts +56 -0
  77. package/ccw/dist/tools/context-cache.d.ts.map +1 -0
  78. package/ccw/dist/tools/context-cache.js +294 -0
  79. package/ccw/dist/tools/context-cache.js.map +1 -0
  80. package/ccw/dist/tools/core-memory.d.ts.map +1 -1
  81. package/ccw/dist/tools/core-memory.js +33 -19
  82. package/ccw/dist/tools/core-memory.js.map +1 -1
  83. package/ccw/dist/tools/index.d.ts.map +1 -1
  84. package/ccw/dist/tools/index.js +2 -0
  85. package/ccw/dist/tools/index.js.map +1 -1
  86. package/ccw/dist/tools/litellm-client.d.ts +85 -0
  87. package/ccw/dist/tools/litellm-client.d.ts.map +1 -0
  88. package/ccw/dist/tools/litellm-client.js +188 -0
  89. package/ccw/dist/tools/litellm-client.js.map +1 -0
  90. package/ccw/dist/tools/litellm-executor.d.ts +34 -0
  91. package/ccw/dist/tools/litellm-executor.d.ts.map +1 -0
  92. package/ccw/dist/tools/litellm-executor.js +192 -0
  93. package/ccw/dist/tools/litellm-executor.js.map +1 -0
  94. package/ccw/dist/tools/pattern-parser.d.ts +55 -0
  95. package/ccw/dist/tools/pattern-parser.d.ts.map +1 -0
  96. package/ccw/dist/tools/pattern-parser.js +237 -0
  97. package/ccw/dist/tools/pattern-parser.js.map +1 -0
  98. package/ccw/dist/tools/smart-search.d.ts +1 -0
  99. package/ccw/dist/tools/smart-search.d.ts.map +1 -1
  100. package/ccw/dist/tools/smart-search.js +117 -41
  101. package/ccw/dist/tools/smart-search.js.map +1 -1
  102. package/ccw/dist/types/litellm-api-config.d.ts +294 -0
  103. package/ccw/dist/types/litellm-api-config.d.ts.map +1 -0
  104. package/ccw/dist/types/litellm-api-config.js +8 -0
  105. package/ccw/dist/types/litellm-api-config.js.map +1 -0
  106. package/ccw/src/cli.ts +258 -244
  107. package/ccw/src/commands/cli.ts +153 -9
  108. package/ccw/src/commands/hook.ts +3 -2
  109. package/ccw/src/config/.litellm-api-config-manager.ts.2025-12-23T11-57-43-727Z.bak +441 -0
  110. package/ccw/src/config/litellm-api-config-manager.ts +1012 -0
  111. package/ccw/src/config/provider-models.ts +222 -0
  112. package/ccw/src/core/cache-manager.ts +292 -294
  113. package/ccw/src/core/dashboard-generator.ts +3 -1
  114. package/ccw/src/core/routes/cli-routes.ts +192 -0
  115. package/ccw/src/core/routes/codexlens-routes.ts +241 -19
  116. package/ccw/src/core/routes/hooks-routes.ts +399 -405
  117. package/ccw/src/core/routes/litellm-api-routes.ts +930 -0
  118. package/ccw/src/core/routes/litellm-routes.ts +107 -0
  119. package/ccw/src/core/routes/mcp-routes.ts +1271 -1271
  120. package/ccw/src/core/routes/status-routes.ts +51 -0
  121. package/ccw/src/core/routes/system-routes.ts +1 -1
  122. package/ccw/src/core/server.ts +15 -1
  123. package/ccw/src/mcp-server/index.ts +1 -1
  124. package/ccw/src/templates/dashboard-css/12-cli-legacy.css +44 -0
  125. package/ccw/src/templates/dashboard-css/31-api-settings.css +2265 -0
  126. package/ccw/src/templates/dashboard-js/components/cli-history.js +15 -8
  127. package/ccw/src/templates/dashboard-js/components/cli-status.js +323 -9
  128. package/ccw/src/templates/dashboard-js/components/navigation.js +329 -313
  129. package/ccw/src/templates/dashboard-js/i18n.js +583 -1
  130. package/ccw/src/templates/dashboard-js/views/api-settings.js +3362 -0
  131. package/ccw/src/templates/dashboard-js/views/cli-manager.js +199 -24
  132. package/ccw/src/templates/dashboard-js/views/codexlens-manager.js +1265 -27
  133. package/ccw/src/templates/dashboard.html +840 -831
  134. package/ccw/src/tools/claude-cli-tools.ts +300 -0
  135. package/ccw/src/tools/cli-executor.ts +83 -14
  136. package/ccw/src/tools/codex-lens.ts +146 -9
  137. package/ccw/src/tools/context-cache-store.ts +368 -0
  138. package/ccw/src/tools/context-cache.ts +393 -0
  139. package/ccw/src/tools/core-memory.ts +33 -19
  140. package/ccw/src/tools/index.ts +2 -0
  141. package/ccw/src/tools/litellm-client.ts +246 -0
  142. package/ccw/src/tools/litellm-executor.ts +241 -0
  143. package/ccw/src/tools/pattern-parser.ts +329 -0
  144. package/ccw/src/tools/smart-search.ts +142 -41
  145. package/ccw/src/types/litellm-api-config.ts +402 -0
  146. package/ccw-litellm/README.md +180 -0
  147. package/ccw-litellm/pyproject.toml +35 -0
  148. package/ccw-litellm/src/ccw_litellm/__init__.py +47 -0
  149. package/ccw-litellm/src/ccw_litellm/__pycache__/__init__.cpython-313.pyc +0 -0
  150. package/ccw-litellm/src/ccw_litellm/__pycache__/cli.cpython-313.pyc +0 -0
  151. package/ccw-litellm/src/ccw_litellm/cli.py +108 -0
  152. package/ccw-litellm/src/ccw_litellm/clients/__init__.py +12 -0
  153. package/ccw-litellm/src/ccw_litellm/clients/__pycache__/__init__.cpython-313.pyc +0 -0
  154. package/ccw-litellm/src/ccw_litellm/clients/__pycache__/litellm_embedder.cpython-313.pyc +0 -0
  155. package/ccw-litellm/src/ccw_litellm/clients/__pycache__/litellm_llm.cpython-313.pyc +0 -0
  156. package/ccw-litellm/src/ccw_litellm/clients/litellm_embedder.py +251 -0
  157. package/ccw-litellm/src/ccw_litellm/clients/litellm_llm.py +165 -0
  158. package/ccw-litellm/src/ccw_litellm/config/__init__.py +22 -0
  159. package/ccw-litellm/src/ccw_litellm/config/__pycache__/__init__.cpython-313.pyc +0 -0
  160. package/ccw-litellm/src/ccw_litellm/config/__pycache__/loader.cpython-313.pyc +0 -0
  161. package/ccw-litellm/src/ccw_litellm/config/__pycache__/models.cpython-313.pyc +0 -0
  162. package/ccw-litellm/src/ccw_litellm/config/loader.py +316 -0
  163. package/ccw-litellm/src/ccw_litellm/config/models.py +130 -0
  164. package/ccw-litellm/src/ccw_litellm/interfaces/__init__.py +14 -0
  165. package/ccw-litellm/src/ccw_litellm/interfaces/__pycache__/__init__.cpython-313.pyc +0 -0
  166. package/ccw-litellm/src/ccw_litellm/interfaces/__pycache__/embedder.cpython-313.pyc +0 -0
  167. package/ccw-litellm/src/ccw_litellm/interfaces/__pycache__/llm.cpython-313.pyc +0 -0
  168. package/ccw-litellm/src/ccw_litellm/interfaces/embedder.py +52 -0
  169. package/ccw-litellm/src/ccw_litellm/interfaces/llm.py +45 -0
  170. package/codex-lens/src/codexlens/__pycache__/config.cpython-313.pyc +0 -0
  171. package/codex-lens/src/codexlens/cli/__pycache__/commands.cpython-313.pyc +0 -0
  172. package/codex-lens/src/codexlens/cli/__pycache__/embedding_manager.cpython-313.pyc +0 -0
  173. package/codex-lens/src/codexlens/cli/__pycache__/model_manager.cpython-313.pyc +0 -0
  174. package/codex-lens/src/codexlens/cli/__pycache__/output.cpython-313.pyc +0 -0
  175. package/codex-lens/src/codexlens/cli/commands.py +378 -23
  176. package/codex-lens/src/codexlens/cli/embedding_manager.py +660 -56
  177. package/codex-lens/src/codexlens/cli/model_manager.py +31 -18
  178. package/codex-lens/src/codexlens/cli/output.py +12 -1
  179. package/codex-lens/src/codexlens/config.py +93 -0
  180. package/codex-lens/src/codexlens/search/__pycache__/chain_search.cpython-313.pyc +0 -0
  181. package/codex-lens/src/codexlens/search/__pycache__/hybrid_search.cpython-313.pyc +0 -0
  182. package/codex-lens/src/codexlens/search/__pycache__/ranking.cpython-313.pyc +0 -0
  183. package/codex-lens/src/codexlens/search/chain_search.py +6 -2
  184. package/codex-lens/src/codexlens/search/hybrid_search.py +44 -21
  185. package/codex-lens/src/codexlens/search/ranking.py +1 -1
  186. package/codex-lens/src/codexlens/semantic/__init__.py +42 -0
  187. package/codex-lens/src/codexlens/semantic/__pycache__/__init__.cpython-313.pyc +0 -0
  188. package/codex-lens/src/codexlens/semantic/__pycache__/base.cpython-313.pyc +0 -0
  189. package/codex-lens/src/codexlens/semantic/__pycache__/chunker.cpython-313.pyc +0 -0
  190. package/codex-lens/src/codexlens/semantic/__pycache__/embedder.cpython-313.pyc +0 -0
  191. package/codex-lens/src/codexlens/semantic/__pycache__/factory.cpython-313.pyc +0 -0
  192. package/codex-lens/src/codexlens/semantic/__pycache__/gpu_support.cpython-313.pyc +0 -0
  193. package/codex-lens/src/codexlens/semantic/__pycache__/litellm_embedder.cpython-313.pyc +0 -0
  194. package/codex-lens/src/codexlens/semantic/__pycache__/vector_store.cpython-313.pyc +0 -0
  195. package/codex-lens/src/codexlens/semantic/base.py +61 -0
  196. package/codex-lens/src/codexlens/semantic/chunker.py +43 -20
  197. package/codex-lens/src/codexlens/semantic/embedder.py +60 -13
  198. package/codex-lens/src/codexlens/semantic/factory.py +98 -0
  199. package/codex-lens/src/codexlens/semantic/gpu_support.py +225 -3
  200. package/codex-lens/src/codexlens/semantic/litellm_embedder.py +144 -0
  201. package/codex-lens/src/codexlens/semantic/rotational_embedder.py +434 -0
  202. package/codex-lens/src/codexlens/semantic/vector_store.py +33 -8
  203. package/codex-lens/src/codexlens/storage/__pycache__/path_mapper.cpython-313.pyc +0 -0
  204. package/codex-lens/src/codexlens/storage/migrations/__pycache__/migration_004_dual_fts.cpython-313.pyc +0 -0
  205. package/codex-lens/src/codexlens/storage/path_mapper.py +27 -1
  206. package/package.json +15 -5
  207. package/.codex/prompts.zip +0 -0
  208. package/ccw/package.json +0 -65
@@ -1,405 +1,399 @@
1
- // @ts-nocheck
2
- /**
3
- * Hooks Routes Module
4
- * Handles all hooks-related API endpoints
5
- */
6
- import type { IncomingMessage, ServerResponse } from 'http';
7
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
8
- import { join, dirname } from 'path';
9
- import { homedir } from 'os';
10
-
11
- export interface RouteContext {
12
- pathname: string;
13
- url: URL;
14
- req: IncomingMessage;
15
- res: ServerResponse;
16
- initialPath: string;
17
- handlePostRequest: (req: IncomingMessage, res: ServerResponse, handler: (body: unknown) => Promise<any>) => void;
18
- broadcastToClients: (data: unknown) => void;
19
- extractSessionIdFromPath: (filePath: string) => string | null;
20
- }
21
-
22
- // ========================================
23
- // Helper Functions
24
- // ========================================
25
-
26
- const GLOBAL_SETTINGS_PATH = join(homedir(), '.claude', 'settings.json');
27
-
28
- /**
29
- * Get project settings path
30
- * @param {string} projectPath
31
- * @returns {string}
32
- */
33
- function getProjectSettingsPath(projectPath) {
34
- const normalizedPath = projectPath.replace(/\//g, '\\').replace(/^\\([a-zA-Z])\\/, '$1:\\');
35
- return join(normalizedPath, '.claude', 'settings.json');
36
- }
37
-
38
- /**
39
- * Read settings file safely
40
- * @param {string} filePath
41
- * @returns {Object}
42
- */
43
- function readSettingsFile(filePath) {
44
- try {
45
- if (!existsSync(filePath)) {
46
- return {};
47
- }
48
- const content = readFileSync(filePath, 'utf8');
49
- return JSON.parse(content);
50
- } catch (error: unknown) {
51
- console.error(`Error reading settings file ${filePath}:`, error);
52
- return {};
53
- }
54
- }
55
-
56
- /**
57
- * Get hooks configuration from global and project settings
58
- * @param {string} projectPath
59
- * @returns {Object}
60
- */
61
- function getHooksConfig(projectPath) {
62
- const globalSettings = readSettingsFile(GLOBAL_SETTINGS_PATH);
63
- const projectSettingsPath = projectPath ? getProjectSettingsPath(projectPath) : null;
64
- const projectSettings = projectSettingsPath ? readSettingsFile(projectSettingsPath) : {};
65
-
66
- return {
67
- global: {
68
- path: GLOBAL_SETTINGS_PATH,
69
- hooks: globalSettings.hooks || {}
70
- },
71
- project: {
72
- path: projectSettingsPath,
73
- hooks: projectSettings.hooks || {}
74
- }
75
- };
76
- }
77
-
78
- /**
79
- * Save a hook to settings file
80
- * @param {string} projectPath
81
- * @param {string} scope - 'global' or 'project'
82
- * @param {string} event - Hook event type
83
- * @param {Object} hookData - Hook configuration
84
- * @returns {Object}
85
- */
86
- function saveHookToSettings(projectPath, scope, event, hookData) {
87
- try {
88
- const filePath = scope === 'global' ? GLOBAL_SETTINGS_PATH : getProjectSettingsPath(projectPath);
89
- const settings = readSettingsFile(filePath);
90
-
91
- // Ensure hooks object exists
92
- if (!settings.hooks) {
93
- settings.hooks = {};
94
- }
95
-
96
- // Ensure the event array exists
97
- if (!settings.hooks[event]) {
98
- settings.hooks[event] = [];
99
- }
100
-
101
- // Ensure it's an array
102
- if (!Array.isArray(settings.hooks[event])) {
103
- settings.hooks[event] = [settings.hooks[event]];
104
- }
105
-
106
- // Check if we're replacing an existing hook
107
- if (hookData.replaceIndex !== undefined) {
108
- const index = hookData.replaceIndex;
109
- delete hookData.replaceIndex;
110
- if (index >= 0 && index < settings.hooks[event].length) {
111
- settings.hooks[event][index] = hookData;
112
- }
113
- } else {
114
- // Add new hook
115
- settings.hooks[event].push(hookData);
116
- }
117
-
118
- // Ensure directory exists and write file
119
- const dirPath = dirname(filePath);
120
- if (!existsSync(dirPath)) {
121
- mkdirSync(dirPath, { recursive: true });
122
- }
123
- writeFileSync(filePath, JSON.stringify(settings, null, 2), 'utf8');
124
-
125
- return {
126
- success: true,
127
- event,
128
- hookData
129
- };
130
- } catch (error: unknown) {
131
- console.error('Error saving hook:', error);
132
- return { error: (error as Error).message };
133
- }
134
- }
135
-
136
- /**
137
- * Delete a hook from settings file
138
- * @param {string} projectPath
139
- * @param {string} scope - 'global' or 'project'
140
- * @param {string} event - Hook event type
141
- * @param {number} hookIndex - Index of hook to delete
142
- * @returns {Object}
143
- */
144
- function deleteHookFromSettings(projectPath, scope, event, hookIndex) {
145
- try {
146
- const filePath = scope === 'global' ? GLOBAL_SETTINGS_PATH : getProjectSettingsPath(projectPath);
147
- const settings = readSettingsFile(filePath);
148
-
149
- if (!settings.hooks || !settings.hooks[event]) {
150
- return { error: 'Hook not found' };
151
- }
152
-
153
- // Ensure it's an array
154
- if (!Array.isArray(settings.hooks[event])) {
155
- settings.hooks[event] = [settings.hooks[event]];
156
- }
157
-
158
- if (hookIndex < 0 || hookIndex >= settings.hooks[event].length) {
159
- return { error: 'Invalid hook index' };
160
- }
161
-
162
- // Remove the hook
163
- settings.hooks[event].splice(hookIndex, 1);
164
-
165
- // Remove empty event arrays
166
- if (settings.hooks[event].length === 0) {
167
- delete settings.hooks[event];
168
- }
169
-
170
- writeFileSync(filePath, JSON.stringify(settings, null, 2), 'utf8');
171
-
172
- return {
173
- success: true,
174
- event,
175
- hookIndex
176
- };
177
- } catch (error: unknown) {
178
- console.error('Error deleting hook:', error);
179
- return { error: (error as Error).message };
180
- }
181
- }
182
-
183
- // ========================================
184
- // Session State Tracking (for progressive disclosure)
185
- // ========================================
186
-
187
- // Track sessions that have received startup context
188
- // Key: sessionId, Value: timestamp of first context load
189
- const sessionContextState = new Map<string, {
190
- firstLoad: string;
191
- loadCount: number;
192
- lastPrompt?: string;
193
- }>();
194
-
195
- // Cleanup old sessions (older than 24 hours)
196
- function cleanupOldSessions() {
197
- const cutoff = Date.now() - 24 * 60 * 60 * 1000;
198
- for (const [sessionId, state] of sessionContextState.entries()) {
199
- if (new Date(state.firstLoad).getTime() < cutoff) {
200
- sessionContextState.delete(sessionId);
201
- }
202
- }
203
- }
204
-
205
- // Run cleanup every hour
206
- setInterval(cleanupOldSessions, 60 * 60 * 1000);
207
-
208
- // ========================================
209
- // Route Handler
210
- // ========================================
211
-
212
- /**
213
- * Handle hooks routes
214
- * @returns true if route was handled, false otherwise
215
- */
216
- export async function handleHooksRoutes(ctx: RouteContext): Promise<boolean> {
217
- const { pathname, url, req, res, initialPath, handlePostRequest, broadcastToClients, extractSessionIdFromPath } = ctx;
218
-
219
- // API: Hook endpoint for Claude Code notifications
220
- if (pathname === '/api/hook' && req.method === 'POST') {
221
- handlePostRequest(req, res, async (body) => {
222
- const { type, filePath, sessionId, ...extraData } = body;
223
-
224
- // Determine session ID from file path if not provided
225
- let resolvedSessionId = sessionId;
226
- if (!resolvedSessionId && filePath) {
227
- resolvedSessionId = extractSessionIdFromPath(filePath);
228
- }
229
-
230
- // Handle context hooks (session-start, context)
231
- if (type === 'session-start' || type === 'context') {
232
- try {
233
- const projectPath = url.searchParams.get('path') || initialPath;
234
- const { SessionClusteringService } = await import('../session-clustering-service.js');
235
- const clusteringService = new SessionClusteringService(projectPath);
236
-
237
- const format = url.searchParams.get('format') || 'markdown';
238
-
239
- // Pass type and prompt to getProgressiveIndex
240
- // session-start: returns recent sessions by time
241
- // context: returns intent-matched sessions based on prompt
242
- const index = await clusteringService.getProgressiveIndex({
243
- type: type as 'session-start' | 'context',
244
- sessionId: resolvedSessionId,
245
- prompt: extraData.prompt // Pass user prompt for intent matching
246
- });
247
-
248
- // Return context directly
249
- return {
250
- success: true,
251
- type: 'context',
252
- format,
253
- content: index,
254
- sessionId: resolvedSessionId
255
- };
256
- } catch (error) {
257
- console.error('[Hooks] Failed to generate context:', error);
258
- // Return empty content on failure (fail silently)
259
- return {
260
- success: true,
261
- type: 'context',
262
- format: 'markdown',
263
- content: '',
264
- sessionId: resolvedSessionId,
265
- error: (error as Error).message
266
- };
267
- }
268
- }
269
-
270
- // Broadcast to all connected WebSocket clients
271
- const notification = {
272
- type: type || 'session_updated',
273
- payload: {
274
- sessionId: resolvedSessionId,
275
- filePath: filePath,
276
- timestamp: new Date().toISOString(),
277
- ...extraData // Pass through toolName, status, result, params, error, etc.
278
- }
279
- };
280
-
281
- broadcastToClients(notification);
282
-
283
- return { success: true, notification };
284
- });
285
- return true;
286
- }
287
-
288
- // API: Unified Session Context endpoint (Progressive Disclosure)
289
- // Automatically detects first prompt vs subsequent prompts
290
- // - First prompt: returns cluster-based session overview
291
- // - Subsequent prompts: returns intent-matched sessions based on prompt
292
- if (pathname === '/api/hook/session-context' && req.method === 'POST') {
293
- handlePostRequest(req, res, async (body) => {
294
- const { sessionId, prompt } = body as { sessionId?: string; prompt?: string };
295
-
296
- if (!sessionId) {
297
- return {
298
- success: true,
299
- content: '',
300
- error: 'sessionId is required'
301
- };
302
- }
303
-
304
- try {
305
- const projectPath = url.searchParams.get('path') || initialPath;
306
- const { SessionClusteringService } = await import('../session-clustering-service.js');
307
- const clusteringService = new SessionClusteringService(projectPath);
308
-
309
- // Check if this is the first prompt for this session
310
- const existingState = sessionContextState.get(sessionId);
311
- const isFirstPrompt = !existingState;
312
-
313
- // Update session state
314
- if (isFirstPrompt) {
315
- sessionContextState.set(sessionId, {
316
- firstLoad: new Date().toISOString(),
317
- loadCount: 1,
318
- lastPrompt: prompt
319
- });
320
- } else {
321
- existingState.loadCount++;
322
- existingState.lastPrompt = prompt;
323
- }
324
-
325
- // Determine which type of context to return
326
- let contextType: 'session-start' | 'context';
327
- let content: string;
328
-
329
- if (isFirstPrompt) {
330
- // First prompt: return session overview with clusters
331
- contextType = 'session-start';
332
- content = await clusteringService.getProgressiveIndex({
333
- type: 'session-start',
334
- sessionId
335
- });
336
- } else if (prompt && prompt.trim().length > 0) {
337
- // Subsequent prompts with content: return intent-matched sessions
338
- contextType = 'context';
339
- content = await clusteringService.getProgressiveIndex({
340
- type: 'context',
341
- sessionId,
342
- prompt
343
- });
344
- } else {
345
- // Subsequent prompts without content: return minimal context
346
- contextType = 'context';
347
- content = ''; // No context needed for empty prompts
348
- }
349
-
350
- return {
351
- success: true,
352
- type: contextType,
353
- isFirstPrompt,
354
- loadCount: sessionContextState.get(sessionId)?.loadCount || 1,
355
- content,
356
- sessionId
357
- };
358
- } catch (error) {
359
- console.error('[Hooks] Failed to generate session context:', error);
360
- return {
361
- success: true,
362
- content: '',
363
- sessionId,
364
- error: (error as Error).message
365
- };
366
- }
367
- });
368
- return true;
369
- }
370
-
371
- // API: Get hooks configuration
372
- if (pathname === '/api/hooks' && req.method === 'GET') {
373
- const projectPathParam = url.searchParams.get('path');
374
- const hooksData = getHooksConfig(projectPathParam);
375
- res.writeHead(200, { 'Content-Type': 'application/json' });
376
- res.end(JSON.stringify(hooksData));
377
- return true;
378
- }
379
-
380
- // API: Save hook
381
- if (pathname === '/api/hooks' && req.method === 'POST') {
382
- handlePostRequest(req, res, async (body) => {
383
- const { projectPath, scope, event, hookData } = body;
384
- if (!scope || !event || !hookData) {
385
- return { error: 'scope, event, and hookData are required', status: 400 };
386
- }
387
- return saveHookToSettings(projectPath, scope, event, hookData);
388
- });
389
- return true;
390
- }
391
-
392
- // API: Delete hook
393
- if (pathname === '/api/hooks' && req.method === 'DELETE') {
394
- handlePostRequest(req, res, async (body) => {
395
- const { projectPath, scope, event, hookIndex } = body;
396
- if (!scope || !event || hookIndex === undefined) {
397
- return { error: 'scope, event, and hookIndex are required', status: 400 };
398
- }
399
- return deleteHookFromSettings(projectPath, scope, event, hookIndex);
400
- });
401
- return true;
402
- }
403
-
404
- return false;
405
- }
1
+ // @ts-nocheck
2
+ /**
3
+ * Hooks Routes Module
4
+ * Handles all hooks-related API endpoints
5
+ */
6
+ import type { IncomingMessage, ServerResponse } from 'http';
7
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
8
+ import { join, dirname } from 'path';
9
+ import { homedir } from 'os';
10
+
11
+ export interface RouteContext {
12
+ pathname: string;
13
+ url: URL;
14
+ req: IncomingMessage;
15
+ res: ServerResponse;
16
+ initialPath: string;
17
+ handlePostRequest: (req: IncomingMessage, res: ServerResponse, handler: (body: unknown) => Promise<any>) => void;
18
+ broadcastToClients: (data: unknown) => void;
19
+ extractSessionIdFromPath: (filePath: string) => string | null;
20
+ }
21
+
22
+ // ========================================
23
+ // Helper Functions
24
+ // ========================================
25
+
26
+ const GLOBAL_SETTINGS_PATH = join(homedir(), '.claude', 'settings.json');
27
+
28
+ /**
29
+ * Get project settings path
30
+ * @param {string} projectPath
31
+ * @returns {string}
32
+ */
33
+ function getProjectSettingsPath(projectPath) {
34
+ // path.join automatically handles cross-platform path separators
35
+ return join(projectPath, '.claude', 'settings.json');
36
+ }
37
+
38
+ /**
39
+ * Read settings file safely
40
+ * @param {string} filePath
41
+ * @returns {Object}
42
+ */
43
+ function readSettingsFile(filePath) {
44
+ try {
45
+ if (!existsSync(filePath)) {
46
+ return {};
47
+ }
48
+ const content = readFileSync(filePath, 'utf8');
49
+ return JSON.parse(content);
50
+ } catch (error: unknown) {
51
+ console.error(`Error reading settings file ${filePath}:`, error);
52
+ return {};
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Get hooks configuration from global and project settings
58
+ * @param {string} projectPath
59
+ * @returns {Object}
60
+ */
61
+ function getHooksConfig(projectPath) {
62
+ const globalSettings = readSettingsFile(GLOBAL_SETTINGS_PATH);
63
+ const projectSettingsPath = projectPath ? getProjectSettingsPath(projectPath) : null;
64
+ const projectSettings = projectSettingsPath ? readSettingsFile(projectSettingsPath) : {};
65
+
66
+ return {
67
+ global: {
68
+ path: GLOBAL_SETTINGS_PATH,
69
+ hooks: globalSettings.hooks || {}
70
+ },
71
+ project: {
72
+ path: projectSettingsPath,
73
+ hooks: projectSettings.hooks || {}
74
+ }
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Save a hook to settings file
80
+ * @param {string} projectPath
81
+ * @param {string} scope - 'global' or 'project'
82
+ * @param {string} event - Hook event type
83
+ * @param {Object} hookData - Hook configuration
84
+ * @returns {Object}
85
+ */
86
+ function saveHookToSettings(projectPath, scope, event, hookData) {
87
+ try {
88
+ const filePath = scope === 'global' ? GLOBAL_SETTINGS_PATH : getProjectSettingsPath(projectPath);
89
+ const settings = readSettingsFile(filePath);
90
+
91
+ // Ensure hooks object exists
92
+ if (!settings.hooks) {
93
+ settings.hooks = {};
94
+ }
95
+
96
+ // Ensure the event array exists
97
+ if (!settings.hooks[event]) {
98
+ settings.hooks[event] = [];
99
+ }
100
+
101
+ // Ensure it's an array
102
+ if (!Array.isArray(settings.hooks[event])) {
103
+ settings.hooks[event] = [settings.hooks[event]];
104
+ }
105
+
106
+ // Check if we're replacing an existing hook
107
+ if (hookData.replaceIndex !== undefined) {
108
+ const index = hookData.replaceIndex;
109
+ delete hookData.replaceIndex;
110
+ if (index >= 0 && index < settings.hooks[event].length) {
111
+ settings.hooks[event][index] = hookData;
112
+ }
113
+ } else {
114
+ // Add new hook
115
+ settings.hooks[event].push(hookData);
116
+ }
117
+
118
+ // Ensure directory exists and write file
119
+ const dirPath = dirname(filePath);
120
+ if (!existsSync(dirPath)) {
121
+ mkdirSync(dirPath, { recursive: true });
122
+ }
123
+ writeFileSync(filePath, JSON.stringify(settings, null, 2), 'utf8');
124
+
125
+ return {
126
+ success: true,
127
+ event,
128
+ hookData
129
+ };
130
+ } catch (error: unknown) {
131
+ console.error('Error saving hook:', error);
132
+ return { error: (error as Error).message };
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Delete a hook from settings file
138
+ * @param {string} projectPath
139
+ * @param {string} scope - 'global' or 'project'
140
+ * @param {string} event - Hook event type
141
+ * @param {number} hookIndex - Index of hook to delete
142
+ * @returns {Object}
143
+ */
144
+ function deleteHookFromSettings(projectPath, scope, event, hookIndex) {
145
+ try {
146
+ const filePath = scope === 'global' ? GLOBAL_SETTINGS_PATH : getProjectSettingsPath(projectPath);
147
+ const settings = readSettingsFile(filePath);
148
+
149
+ if (!settings.hooks || !settings.hooks[event]) {
150
+ return { error: 'Hook not found' };
151
+ }
152
+
153
+ // Ensure it's an array
154
+ if (!Array.isArray(settings.hooks[event])) {
155
+ settings.hooks[event] = [settings.hooks[event]];
156
+ }
157
+
158
+ if (hookIndex < 0 || hookIndex >= settings.hooks[event].length) {
159
+ return { error: 'Invalid hook index' };
160
+ }
161
+
162
+ // Remove the hook
163
+ settings.hooks[event].splice(hookIndex, 1);
164
+
165
+ // Remove empty event arrays
166
+ if (settings.hooks[event].length === 0) {
167
+ delete settings.hooks[event];
168
+ }
169
+
170
+ writeFileSync(filePath, JSON.stringify(settings, null, 2), 'utf8');
171
+
172
+ return {
173
+ success: true,
174
+ event,
175
+ hookIndex
176
+ };
177
+ } catch (error: unknown) {
178
+ console.error('Error deleting hook:', error);
179
+ return { error: (error as Error).message };
180
+ }
181
+ }
182
+
183
+ // ========================================
184
+ // Session State Tracking
185
+ // ========================================
186
+ // NOTE: Session state is managed by the CLI command (src/commands/hook.ts)
187
+ // using file-based persistence (~/.claude/.ccw-sessions/).
188
+ // This ensures consistent state tracking across all invocation methods.
189
+ // The /api/hook endpoint delegates to SessionClusteringService without
190
+ // managing its own state, as the authoritative state lives in the CLI layer.
191
+
192
+ // ========================================
193
+ // Route Handler
194
+ // ========================================
195
+
196
+ /**
197
+ * Handle hooks routes
198
+ * @returns true if route was handled, false otherwise
199
+ */
200
+ export async function handleHooksRoutes(ctx: RouteContext): Promise<boolean> {
201
+ const { pathname, url, req, res, initialPath, handlePostRequest, broadcastToClients, extractSessionIdFromPath } = ctx;
202
+
203
+ // API: Hook endpoint for Claude Code notifications
204
+ if (pathname === '/api/hook' && req.method === 'POST') {
205
+ handlePostRequest(req, res, async (body) => {
206
+ const { type, filePath, sessionId, ...extraData } = body;
207
+
208
+ // Determine session ID from file path if not provided
209
+ let resolvedSessionId = sessionId;
210
+ if (!resolvedSessionId && filePath) {
211
+ resolvedSessionId = extractSessionIdFromPath(filePath);
212
+ }
213
+
214
+ // Handle context hooks (session-start, context)
215
+ if (type === 'session-start' || type === 'context') {
216
+ try {
217
+ const projectPath = url.searchParams.get('path') || initialPath;
218
+ const { SessionClusteringService } = await import('../session-clustering-service.js');
219
+ const clusteringService = new SessionClusteringService(projectPath);
220
+
221
+ const format = url.searchParams.get('format') || 'markdown';
222
+
223
+ // Pass type and prompt to getProgressiveIndex
224
+ // session-start: returns recent sessions by time
225
+ // context: returns intent-matched sessions based on prompt
226
+ const index = await clusteringService.getProgressiveIndex({
227
+ type: type as 'session-start' | 'context',
228
+ sessionId: resolvedSessionId,
229
+ prompt: extraData.prompt // Pass user prompt for intent matching
230
+ });
231
+
232
+ // Return context directly
233
+ return {
234
+ success: true,
235
+ type: 'context',
236
+ format,
237
+ content: index,
238
+ sessionId: resolvedSessionId
239
+ };
240
+ } catch (error) {
241
+ console.error('[Hooks] Failed to generate context:', error);
242
+ // Return empty content on failure (fail silently)
243
+ return {
244
+ success: true,
245
+ type: 'context',
246
+ format: 'markdown',
247
+ content: '',
248
+ sessionId: resolvedSessionId,
249
+ error: (error as Error).message
250
+ };
251
+ }
252
+ }
253
+
254
+ // Broadcast to all connected WebSocket clients
255
+ const notification = {
256
+ type: type || 'session_updated',
257
+ payload: {
258
+ sessionId: resolvedSessionId,
259
+ filePath: filePath,
260
+ timestamp: new Date().toISOString(),
261
+ ...extraData // Pass through toolName, status, result, params, error, etc.
262
+ }
263
+ };
264
+
265
+ broadcastToClients(notification);
266
+
267
+ return { success: true, notification };
268
+ });
269
+ return true;
270
+ }
271
+
272
+ // API: Unified Session Context endpoint (Progressive Disclosure)
273
+ // DEPRECATED: Use CLI command `ccw hook session-context --stdin` instead.
274
+ // This endpoint now uses file-based state (shared with CLI) for consistency.
275
+ // - First prompt: returns cluster-based session overview
276
+ // - Subsequent prompts: returns intent-matched sessions based on prompt
277
+ if (pathname === '/api/hook/session-context' && req.method === 'POST') {
278
+ handlePostRequest(req, res, async (body) => {
279
+ const { sessionId, prompt } = body as { sessionId?: string; prompt?: string };
280
+
281
+ if (!sessionId) {
282
+ return {
283
+ success: true,
284
+ content: '',
285
+ error: 'sessionId is required'
286
+ };
287
+ }
288
+
289
+ try {
290
+ const projectPath = url.searchParams.get('path') || initialPath;
291
+ const { SessionClusteringService } = await import('../session-clustering-service.js');
292
+ const clusteringService = new SessionClusteringService(projectPath);
293
+
294
+ // Use file-based session state (shared with CLI hook.ts)
295
+ const sessionStateDir = join(homedir(), '.claude', '.ccw-sessions');
296
+ const sessionStateFile = join(sessionStateDir, `session-${sessionId}.json`);
297
+
298
+ let existingState: { firstLoad: string; loadCount: number; lastPrompt?: string } | null = null;
299
+ if (existsSync(sessionStateFile)) {
300
+ try {
301
+ existingState = JSON.parse(readFileSync(sessionStateFile, 'utf-8'));
302
+ } catch {
303
+ existingState = null;
304
+ }
305
+ }
306
+
307
+ const isFirstPrompt = !existingState;
308
+
309
+ // Update session state (file-based)
310
+ const newState = isFirstPrompt
311
+ ? { firstLoad: new Date().toISOString(), loadCount: 1, lastPrompt: prompt }
312
+ : { ...existingState!, loadCount: existingState!.loadCount + 1, lastPrompt: prompt };
313
+
314
+ if (!existsSync(sessionStateDir)) {
315
+ mkdirSync(sessionStateDir, { recursive: true });
316
+ }
317
+ writeFileSync(sessionStateFile, JSON.stringify(newState, null, 2));
318
+
319
+ // Determine which type of context to return
320
+ let contextType: 'session-start' | 'context';
321
+ let content: string;
322
+
323
+ if (isFirstPrompt) {
324
+ // First prompt: return session overview with clusters
325
+ contextType = 'session-start';
326
+ content = await clusteringService.getProgressiveIndex({
327
+ type: 'session-start',
328
+ sessionId
329
+ });
330
+ } else if (prompt && prompt.trim().length > 0) {
331
+ // Subsequent prompts with content: return intent-matched sessions
332
+ contextType = 'context';
333
+ content = await clusteringService.getProgressiveIndex({
334
+ type: 'context',
335
+ sessionId,
336
+ prompt
337
+ });
338
+ } else {
339
+ // Subsequent prompts without content: return minimal context
340
+ contextType = 'context';
341
+ content = ''; // No context needed for empty prompts
342
+ }
343
+
344
+ return {
345
+ success: true,
346
+ type: contextType,
347
+ isFirstPrompt,
348
+ loadCount: newState.loadCount,
349
+ content,
350
+ sessionId
351
+ };
352
+ } catch (error) {
353
+ console.error('[Hooks] Failed to generate session context:', error);
354
+ return {
355
+ success: true,
356
+ content: '',
357
+ sessionId,
358
+ error: (error as Error).message
359
+ };
360
+ }
361
+ });
362
+ return true;
363
+ }
364
+
365
+ // API: Get hooks configuration
366
+ if (pathname === '/api/hooks' && req.method === 'GET') {
367
+ const projectPathParam = url.searchParams.get('path');
368
+ const hooksData = getHooksConfig(projectPathParam);
369
+ res.writeHead(200, { 'Content-Type': 'application/json' });
370
+ res.end(JSON.stringify(hooksData));
371
+ return true;
372
+ }
373
+
374
+ // API: Save hook
375
+ if (pathname === '/api/hooks' && req.method === 'POST') {
376
+ handlePostRequest(req, res, async (body) => {
377
+ const { projectPath, scope, event, hookData } = body;
378
+ if (!scope || !event || !hookData) {
379
+ return { error: 'scope, event, and hookData are required', status: 400 };
380
+ }
381
+ return saveHookToSettings(projectPath, scope, event, hookData);
382
+ });
383
+ return true;
384
+ }
385
+
386
+ // API: Delete hook
387
+ if (pathname === '/api/hooks' && req.method === 'DELETE') {
388
+ handlePostRequest(req, res, async (body) => {
389
+ const { projectPath, scope, event, hookIndex } = body;
390
+ if (!scope || !event || hookIndex === undefined) {
391
+ return { error: 'scope, event, and hookIndex are required', status: 400 };
392
+ }
393
+ return deleteHookFromSettings(projectPath, scope, event, hookIndex);
394
+ });
395
+ return true;
396
+ }
397
+
398
+ return false;
399
+ }