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,294 +1,292 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from 'fs';
2
- import { join, dirname } from 'path';
3
- import { StoragePaths, ensureStorageDir } from '../config/storage-paths.js';
4
-
5
- interface CacheEntry<T> {
6
- data: T;
7
- timestamp: number;
8
- fileHashes: Map<string, number>; // file path -> mtime
9
- ttl?: number;
10
- }
11
-
12
- interface CacheOptions {
13
- ttl?: number; // Time-to-live in milliseconds (default: 5 minutes)
14
- cacheDir?: string; // Cache directory (default: .ccw-cache)
15
- }
16
-
17
- /**
18
- * CacheManager class for storing and retrieving dashboard data
19
- * Tracks file modification times to detect changes and invalidate cache
20
- */
21
- export class CacheManager<T> {
22
- private cacheFile: string;
23
- private ttl: number;
24
- private cacheDir: string;
25
-
26
- /**
27
- * Create a new CacheManager instance
28
- * @param cacheKey - Unique identifier for this cache (e.g., 'dashboard-data')
29
- * @param options - Cache configuration options
30
- */
31
- constructor(cacheKey: string, options: CacheOptions = {}) {
32
- if (!options.cacheDir) {
33
- throw new Error('CacheManager requires cacheDir option. Use StoragePaths.project(path).cache');
34
- }
35
- this.ttl = options.ttl || 5 * 60 * 1000; // Default: 5 minutes
36
- this.cacheDir = options.cacheDir;
37
- this.cacheFile = join(this.cacheDir, `${cacheKey}.json`);
38
- }
39
-
40
- /**
41
- * Get cached data if valid, otherwise return null
42
- * @param watchPaths - Array of file/directory paths to check for modifications
43
- * @returns Cached data or null if invalid/expired
44
- */
45
- get(watchPaths: string[] = []): T | null {
46
- if (!existsSync(this.cacheFile)) {
47
- return null;
48
- }
49
-
50
- try {
51
- const content = readFileSync(this.cacheFile, 'utf8');
52
- const entry: CacheEntry<T> = JSON.parse(content, (key, value) => {
53
- // Revive Map objects from JSON
54
- if (key === 'fileHashes' && value && typeof value === 'object') {
55
- return new Map(Object.entries(value));
56
- }
57
- return value;
58
- });
59
-
60
- // Check TTL expiration
61
- if (this.ttl > 0) {
62
- const age = Date.now() - entry.timestamp;
63
- if (age > this.ttl) {
64
- return null;
65
- }
66
- }
67
-
68
- // Check if any watched files have changed
69
- if (watchPaths.length > 0) {
70
- const currentHashes = this.computeFileHashes(watchPaths);
71
- if (!this.hashesMatch(entry.fileHashes, currentHashes)) {
72
- return null;
73
- }
74
- }
75
-
76
- return entry.data;
77
- } catch (err) {
78
- // If cache file is corrupted or unreadable, treat as invalid
79
- console.warn(`Cache read error for ${this.cacheFile}:`, (err as Error).message);
80
- return null;
81
- }
82
- }
83
-
84
- /**
85
- * Store data in cache with current timestamp and file hashes
86
- * @param data - Data to cache
87
- * @param watchPaths - Array of file/directory paths to track
88
- */
89
- set(data: T, watchPaths: string[] = []): void {
90
- try {
91
- // Ensure cache directory exists
92
- if (!existsSync(this.cacheDir)) {
93
- mkdirSync(this.cacheDir, { recursive: true });
94
- }
95
-
96
- const entry: CacheEntry<T> = {
97
- data,
98
- timestamp: Date.now(),
99
- fileHashes: this.computeFileHashes(watchPaths),
100
- ttl: this.ttl
101
- };
102
-
103
- // Convert Map to plain object for JSON serialization
104
- const serializable = {
105
- ...entry,
106
- fileHashes: Object.fromEntries(entry.fileHashes)
107
- };
108
-
109
- writeFileSync(this.cacheFile, JSON.stringify(serializable, null, 2), 'utf8');
110
- } catch (err) {
111
- console.warn(`Cache write error for ${this.cacheFile}:`, (err as Error).message);
112
- }
113
- }
114
-
115
- /**
116
- * Invalidate (delete) the cache
117
- */
118
- invalidate(): void {
119
- try {
120
- if (existsSync(this.cacheFile)) {
121
- const fs = require('fs');
122
- fs.unlinkSync(this.cacheFile);
123
- }
124
- } catch (err) {
125
- console.warn(`Cache invalidation error for ${this.cacheFile}:`, (err as Error).message);
126
- }
127
- }
128
-
129
- /**
130
- * Check if cache is valid without retrieving data
131
- * @param watchPaths - Array of file/directory paths to check
132
- * @returns True if cache exists and is valid
133
- */
134
- isValid(watchPaths: string[] = []): boolean {
135
- return this.get(watchPaths) !== null;
136
- }
137
-
138
- /**
139
- * Compute file modification times for all watched paths
140
- * @param watchPaths - Array of file/directory paths
141
- * @returns Map of path to mtime
142
- */
143
- private computeFileHashes(watchPaths: string[]): Map<string, number> {
144
- const hashes = new Map<string, number>();
145
-
146
- for (const path of watchPaths) {
147
- try {
148
- if (!existsSync(path)) {
149
- continue;
150
- }
151
-
152
- const stats = statSync(path);
153
-
154
- if (stats.isDirectory()) {
155
- // For directories, use directory mtime (detects file additions/deletions)
156
- hashes.set(path, stats.mtimeMs);
157
-
158
- // Also recursively scan for workflow session files
159
- this.scanDirectory(path, hashes);
160
- } else {
161
- // For files, use file mtime
162
- hashes.set(path, stats.mtimeMs);
163
- }
164
- } catch (err) {
165
- // Skip paths that can't be accessed
166
- console.warn(`Cannot access path ${path}:`, (err as Error).message);
167
- }
168
- }
169
-
170
- return hashes;
171
- }
172
-
173
- /**
174
- * Recursively scan directory for important files
175
- * @param dirPath - Directory to scan
176
- * @param hashes - Map to store file hashes
177
- * @param depth - Current recursion depth (max 3)
178
- */
179
- private scanDirectory(dirPath: string, hashes: Map<string, number>, depth: number = 0): void {
180
- if (depth > 3) return; // Limit recursion depth
181
-
182
- try {
183
- const fs = require('fs');
184
- const entries = fs.readdirSync(dirPath, { withFileTypes: true });
185
-
186
- for (const entry of entries) {
187
- const fullPath = join(dirPath, entry.name);
188
-
189
- if (entry.isDirectory()) {
190
- // Track important directories
191
- if (entry.name === '.task' || entry.name === '.review' || entry.name === '.summaries') {
192
- const stats = statSync(fullPath);
193
- hashes.set(fullPath, stats.mtimeMs);
194
- this.scanDirectory(fullPath, hashes, depth + 1);
195
- } else if (entry.name.startsWith('WFS-')) {
196
- // Scan WFS session directories
197
- const stats = statSync(fullPath);
198
- hashes.set(fullPath, stats.mtimeMs);
199
- this.scanDirectory(fullPath, hashes, depth + 1);
200
- }
201
- } else if (entry.isFile()) {
202
- // Track important files
203
- if (
204
- entry.name.endsWith('.json') ||
205
- entry.name === 'IMPL_PLAN.md' ||
206
- entry.name === 'TODO_LIST.md' ||
207
- entry.name === 'workflow-session.json'
208
- ) {
209
- const stats = statSync(fullPath);
210
- hashes.set(fullPath, stats.mtimeMs);
211
- }
212
- }
213
- }
214
- } catch (err) {
215
- // Skip directories that can't be read
216
- console.warn(`Cannot scan directory ${dirPath}:`, (err as Error).message);
217
- }
218
- }
219
-
220
- /**
221
- * Compare two file hash maps
222
- * @param oldHashes - Previous hashes
223
- * @param newHashes - Current hashes
224
- * @returns True if hashes match (no changes)
225
- */
226
- private hashesMatch(oldHashes: Map<string, number>, newHashes: Map<string, number>): boolean {
227
- // Check if any files were added or removed
228
- if (oldHashes.size !== newHashes.size) {
229
- return false;
230
- }
231
-
232
- // Check if any file mtimes changed
233
- const entries = Array.from(oldHashes.entries());
234
- for (let i = 0; i < entries.length; i++) {
235
- const path = entries[i][0];
236
- const oldMtime = entries[i][1];
237
- const newMtime = newHashes.get(path);
238
- if (newMtime === undefined || newMtime !== oldMtime) {
239
- return false;
240
- }
241
- }
242
-
243
- return true;
244
- }
245
-
246
- /**
247
- * Get cache statistics
248
- * @returns Cache info object
249
- */
250
- getStats(): { exists: boolean; age?: number; fileCount?: number; size?: number } {
251
- if (!existsSync(this.cacheFile)) {
252
- return { exists: false };
253
- }
254
-
255
- try {
256
- const stats = statSync(this.cacheFile);
257
- const content = readFileSync(this.cacheFile, 'utf8');
258
- const entry = JSON.parse(content);
259
-
260
- return {
261
- exists: true,
262
- age: Date.now() - entry.timestamp,
263
- fileCount: Object.keys(entry.fileHashes || {}).length,
264
- size: stats.size
265
- };
266
- } catch {
267
- return { exists: false };
268
- }
269
- }
270
- }
271
-
272
- /**
273
- * Extract project path from workflow directory
274
- * @param workflowDir - Path to .workflow directory (e.g., /project/.workflow)
275
- * @returns Project root path
276
- */
277
- function extractProjectPath(workflowDir: string): string {
278
- // workflowDir is typically {projectPath}/.workflow
279
- return workflowDir.replace(/[\/\\]\.workflow$/, '') || workflowDir;
280
- }
281
-
282
- /**
283
- * Create a cache manager for dashboard data
284
- * @param workflowDir - Path to .workflow directory
285
- * @param ttl - Optional TTL in milliseconds
286
- * @returns CacheManager instance
287
- */
288
- export function createDashboardCache(workflowDir: string, ttl?: number): CacheManager<any> {
289
- // Use centralized storage path
290
- const projectPath = extractProjectPath(workflowDir);
291
- const cacheDir = StoragePaths.project(projectPath).cache;
292
- ensureStorageDir(cacheDir);
293
- return new CacheManager('dashboard-data', { cacheDir, ttl });
294
- }
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync, unlinkSync, readdirSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { StoragePaths, ensureStorageDir } from '../config/storage-paths.js';
4
+
5
+ interface CacheEntry<T> {
6
+ data: T;
7
+ timestamp: number;
8
+ fileHashes: Map<string, number>; // file path -> mtime
9
+ ttl?: number;
10
+ }
11
+
12
+ interface CacheOptions {
13
+ ttl?: number; // Time-to-live in milliseconds (default: 5 minutes)
14
+ cacheDir?: string; // Cache directory (default: .ccw-cache)
15
+ }
16
+
17
+ /**
18
+ * CacheManager class for storing and retrieving dashboard data
19
+ * Tracks file modification times to detect changes and invalidate cache
20
+ */
21
+ export class CacheManager<T> {
22
+ private cacheFile: string;
23
+ private ttl: number;
24
+ private cacheDir: string;
25
+
26
+ /**
27
+ * Create a new CacheManager instance
28
+ * @param cacheKey - Unique identifier for this cache (e.g., 'dashboard-data')
29
+ * @param options - Cache configuration options
30
+ */
31
+ constructor(cacheKey: string, options: CacheOptions = {}) {
32
+ if (!options.cacheDir) {
33
+ throw new Error('CacheManager requires cacheDir option. Use StoragePaths.project(path).cache');
34
+ }
35
+ this.ttl = options.ttl || 5 * 60 * 1000; // Default: 5 minutes
36
+ this.cacheDir = options.cacheDir;
37
+ this.cacheFile = join(this.cacheDir, `${cacheKey}.json`);
38
+ }
39
+
40
+ /**
41
+ * Get cached data if valid, otherwise return null
42
+ * @param watchPaths - Array of file/directory paths to check for modifications
43
+ * @returns Cached data or null if invalid/expired
44
+ */
45
+ get(watchPaths: string[] = []): T | null {
46
+ if (!existsSync(this.cacheFile)) {
47
+ return null;
48
+ }
49
+
50
+ try {
51
+ const content = readFileSync(this.cacheFile, 'utf8');
52
+ const entry: CacheEntry<T> = JSON.parse(content, (key, value) => {
53
+ // Revive Map objects from JSON
54
+ if (key === 'fileHashes' && value && typeof value === 'object') {
55
+ return new Map(Object.entries(value));
56
+ }
57
+ return value;
58
+ });
59
+
60
+ // Check TTL expiration
61
+ if (this.ttl > 0) {
62
+ const age = Date.now() - entry.timestamp;
63
+ if (age > this.ttl) {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ // Check if any watched files have changed
69
+ if (watchPaths.length > 0) {
70
+ const currentHashes = this.computeFileHashes(watchPaths);
71
+ if (!this.hashesMatch(entry.fileHashes, currentHashes)) {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ return entry.data;
77
+ } catch (err) {
78
+ // If cache file is corrupted or unreadable, treat as invalid
79
+ console.warn(`Cache read error for ${this.cacheFile}:`, (err as Error).message);
80
+ return null;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Store data in cache with current timestamp and file hashes
86
+ * @param data - Data to cache
87
+ * @param watchPaths - Array of file/directory paths to track
88
+ */
89
+ set(data: T, watchPaths: string[] = []): void {
90
+ try {
91
+ // Ensure cache directory exists
92
+ if (!existsSync(this.cacheDir)) {
93
+ mkdirSync(this.cacheDir, { recursive: true });
94
+ }
95
+
96
+ const entry: CacheEntry<T> = {
97
+ data,
98
+ timestamp: Date.now(),
99
+ fileHashes: this.computeFileHashes(watchPaths),
100
+ ttl: this.ttl
101
+ };
102
+
103
+ // Convert Map to plain object for JSON serialization
104
+ const serializable = {
105
+ ...entry,
106
+ fileHashes: Object.fromEntries(entry.fileHashes)
107
+ };
108
+
109
+ writeFileSync(this.cacheFile, JSON.stringify(serializable, null, 2), 'utf8');
110
+ } catch (err) {
111
+ console.warn(`Cache write error for ${this.cacheFile}:`, (err as Error).message);
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Invalidate (delete) the cache
117
+ */
118
+ invalidate(): void {
119
+ try {
120
+ if (existsSync(this.cacheFile)) {
121
+ unlinkSync(this.cacheFile);
122
+ }
123
+ } catch (err) {
124
+ console.warn(`Cache invalidation error for ${this.cacheFile}:`, (err as Error).message);
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Check if cache is valid without retrieving data
130
+ * @param watchPaths - Array of file/directory paths to check
131
+ * @returns True if cache exists and is valid
132
+ */
133
+ isValid(watchPaths: string[] = []): boolean {
134
+ return this.get(watchPaths) !== null;
135
+ }
136
+
137
+ /**
138
+ * Compute file modification times for all watched paths
139
+ * @param watchPaths - Array of file/directory paths
140
+ * @returns Map of path to mtime
141
+ */
142
+ private computeFileHashes(watchPaths: string[]): Map<string, number> {
143
+ const hashes = new Map<string, number>();
144
+
145
+ for (const path of watchPaths) {
146
+ try {
147
+ if (!existsSync(path)) {
148
+ continue;
149
+ }
150
+
151
+ const stats = statSync(path);
152
+
153
+ if (stats.isDirectory()) {
154
+ // For directories, use directory mtime (detects file additions/deletions)
155
+ hashes.set(path, stats.mtimeMs);
156
+
157
+ // Also recursively scan for workflow session files
158
+ this.scanDirectory(path, hashes);
159
+ } else {
160
+ // For files, use file mtime
161
+ hashes.set(path, stats.mtimeMs);
162
+ }
163
+ } catch (err) {
164
+ // Skip paths that can't be accessed
165
+ console.warn(`Cannot access path ${path}:`, (err as Error).message);
166
+ }
167
+ }
168
+
169
+ return hashes;
170
+ }
171
+
172
+ /**
173
+ * Recursively scan directory for important files
174
+ * @param dirPath - Directory to scan
175
+ * @param hashes - Map to store file hashes
176
+ * @param depth - Current recursion depth (max 3)
177
+ */
178
+ private scanDirectory(dirPath: string, hashes: Map<string, number>, depth: number = 0): void {
179
+ if (depth > 3) return; // Limit recursion depth
180
+
181
+ try {
182
+ const entries = readdirSync(dirPath, { withFileTypes: true });
183
+
184
+ for (const entry of entries) {
185
+ const fullPath = join(dirPath, entry.name);
186
+
187
+ if (entry.isDirectory()) {
188
+ // Track important directories
189
+ if (entry.name === '.task' || entry.name === '.review' || entry.name === '.summaries') {
190
+ const stats = statSync(fullPath);
191
+ hashes.set(fullPath, stats.mtimeMs);
192
+ this.scanDirectory(fullPath, hashes, depth + 1);
193
+ } else if (entry.name.startsWith('WFS-')) {
194
+ // Scan WFS session directories
195
+ const stats = statSync(fullPath);
196
+ hashes.set(fullPath, stats.mtimeMs);
197
+ this.scanDirectory(fullPath, hashes, depth + 1);
198
+ }
199
+ } else if (entry.isFile()) {
200
+ // Track important files
201
+ if (
202
+ entry.name.endsWith('.json') ||
203
+ entry.name === 'IMPL_PLAN.md' ||
204
+ entry.name === 'TODO_LIST.md' ||
205
+ entry.name === 'workflow-session.json'
206
+ ) {
207
+ const stats = statSync(fullPath);
208
+ hashes.set(fullPath, stats.mtimeMs);
209
+ }
210
+ }
211
+ }
212
+ } catch (err) {
213
+ // Skip directories that can't be read
214
+ console.warn(`Cannot scan directory ${dirPath}:`, (err as Error).message);
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Compare two file hash maps
220
+ * @param oldHashes - Previous hashes
221
+ * @param newHashes - Current hashes
222
+ * @returns True if hashes match (no changes)
223
+ */
224
+ private hashesMatch(oldHashes: Map<string, number>, newHashes: Map<string, number>): boolean {
225
+ // Check if any files were added or removed
226
+ if (oldHashes.size !== newHashes.size) {
227
+ return false;
228
+ }
229
+
230
+ // Check if any file mtimes changed
231
+ const entries = Array.from(oldHashes.entries());
232
+ for (let i = 0; i < entries.length; i++) {
233
+ const path = entries[i][0];
234
+ const oldMtime = entries[i][1];
235
+ const newMtime = newHashes.get(path);
236
+ if (newMtime === undefined || newMtime !== oldMtime) {
237
+ return false;
238
+ }
239
+ }
240
+
241
+ return true;
242
+ }
243
+
244
+ /**
245
+ * Get cache statistics
246
+ * @returns Cache info object
247
+ */
248
+ getStats(): { exists: boolean; age?: number; fileCount?: number; size?: number } {
249
+ if (!existsSync(this.cacheFile)) {
250
+ return { exists: false };
251
+ }
252
+
253
+ try {
254
+ const stats = statSync(this.cacheFile);
255
+ const content = readFileSync(this.cacheFile, 'utf8');
256
+ const entry = JSON.parse(content);
257
+
258
+ return {
259
+ exists: true,
260
+ age: Date.now() - entry.timestamp,
261
+ fileCount: Object.keys(entry.fileHashes || {}).length,
262
+ size: stats.size
263
+ };
264
+ } catch {
265
+ return { exists: false };
266
+ }
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Extract project path from workflow directory
272
+ * @param workflowDir - Path to .workflow directory (e.g., /project/.workflow)
273
+ * @returns Project root path
274
+ */
275
+ function extractProjectPath(workflowDir: string): string {
276
+ // workflowDir is typically {projectPath}/.workflow
277
+ return workflowDir.replace(/[\/\\]\.workflow$/, '') || workflowDir;
278
+ }
279
+
280
+ /**
281
+ * Create a cache manager for dashboard data
282
+ * @param workflowDir - Path to .workflow directory
283
+ * @param ttl - Optional TTL in milliseconds
284
+ * @returns CacheManager instance
285
+ */
286
+ export function createDashboardCache(workflowDir: string, ttl?: number): CacheManager<any> {
287
+ // Use centralized storage path
288
+ const projectPath = extractProjectPath(workflowDir);
289
+ const cacheDir = StoragePaths.project(projectPath).cache;
290
+ ensureStorageDir(cacheDir);
291
+ return new CacheManager('dashboard-data', { cacheDir, ttl });
292
+ }
@@ -46,7 +46,8 @@ const MODULE_CSS_FILES = [
46
46
  '27-graph-explorer.css',
47
47
  '28-mcp-manager.css',
48
48
  '29-help.css',
49
- '30-core-memory.css'
49
+ '30-core-memory.css',
50
+ '31-api-settings.css'
50
51
  ];
51
52
 
52
53
  const MODULE_FILES = [
@@ -95,6 +96,7 @@ const MODULE_FILES = [
95
96
  'views/skills-manager.js',
96
97
  'views/rules-manager.js',
97
98
  'views/claude-manager.js',
99
+ 'views/api-settings.js',
98
100
  'views/help.js',
99
101
  'main.js'
100
102
  ];