gitmem-mcp 0.2.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 (316) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/CLAUDE.md.template +65 -0
  3. package/LICENSE +21 -0
  4. package/README.md +221 -0
  5. package/bin/gitmem.js +383 -0
  6. package/dist/commands/check.d.ts +33 -0
  7. package/dist/commands/check.d.ts.map +1 -0
  8. package/dist/commands/check.js +492 -0
  9. package/dist/commands/check.js.map +1 -0
  10. package/dist/constants/closing-questions.d.ts +40 -0
  11. package/dist/constants/closing-questions.d.ts.map +1 -0
  12. package/dist/constants/closing-questions.js +107 -0
  13. package/dist/constants/closing-questions.js.map +1 -0
  14. package/dist/diagnostics/anonymizer.d.ts +55 -0
  15. package/dist/diagnostics/anonymizer.d.ts.map +1 -0
  16. package/dist/diagnostics/anonymizer.js +191 -0
  17. package/dist/diagnostics/anonymizer.js.map +1 -0
  18. package/dist/diagnostics/channels.d.ts +132 -0
  19. package/dist/diagnostics/channels.d.ts.map +1 -0
  20. package/dist/diagnostics/channels.js +150 -0
  21. package/dist/diagnostics/channels.js.map +1 -0
  22. package/dist/diagnostics/collector.d.ts +183 -0
  23. package/dist/diagnostics/collector.d.ts.map +1 -0
  24. package/dist/diagnostics/collector.js +227 -0
  25. package/dist/diagnostics/collector.js.map +1 -0
  26. package/dist/diagnostics/index.d.ts +28 -0
  27. package/dist/diagnostics/index.d.ts.map +1 -0
  28. package/dist/diagnostics/index.js +31 -0
  29. package/dist/diagnostics/index.js.map +1 -0
  30. package/dist/index.d.ts +13 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +18 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/schemas/absorb-observations.d.ts +63 -0
  35. package/dist/schemas/absorb-observations.d.ts.map +1 -0
  36. package/dist/schemas/absorb-observations.js +25 -0
  37. package/dist/schemas/absorb-observations.js.map +1 -0
  38. package/dist/schemas/active-sessions.d.ts +71 -0
  39. package/dist/schemas/active-sessions.d.ts.map +1 -0
  40. package/dist/schemas/active-sessions.js +19 -0
  41. package/dist/schemas/active-sessions.js.map +1 -0
  42. package/dist/schemas/analyze.d.ts +38 -0
  43. package/dist/schemas/analyze.d.ts.map +1 -0
  44. package/dist/schemas/analyze.js +30 -0
  45. package/dist/schemas/analyze.js.map +1 -0
  46. package/dist/schemas/common.d.ts +55 -0
  47. package/dist/schemas/common.d.ts.map +1 -0
  48. package/dist/schemas/common.js +65 -0
  49. package/dist/schemas/common.js.map +1 -0
  50. package/dist/schemas/create-decision.d.ts +48 -0
  51. package/dist/schemas/create-decision.d.ts.map +1 -0
  52. package/dist/schemas/create-decision.js +31 -0
  53. package/dist/schemas/create-decision.js.map +1 -0
  54. package/dist/schemas/create-learning.d.ts +107 -0
  55. package/dist/schemas/create-learning.d.ts.map +1 -0
  56. package/dist/schemas/create-learning.js +64 -0
  57. package/dist/schemas/create-learning.js.map +1 -0
  58. package/dist/schemas/get-transcript.d.ts +24 -0
  59. package/dist/schemas/get-transcript.d.ts.map +1 -0
  60. package/dist/schemas/get-transcript.js +22 -0
  61. package/dist/schemas/get-transcript.js.map +1 -0
  62. package/dist/schemas/index.d.ts +23 -0
  63. package/dist/schemas/index.d.ts.map +1 -0
  64. package/dist/schemas/index.js +23 -0
  65. package/dist/schemas/index.js.map +1 -0
  66. package/dist/schemas/log.d.ts +36 -0
  67. package/dist/schemas/log.d.ts.map +1 -0
  68. package/dist/schemas/log.js +27 -0
  69. package/dist/schemas/log.js.map +1 -0
  70. package/dist/schemas/prepare-context.d.ts +41 -0
  71. package/dist/schemas/prepare-context.d.ts.map +1 -0
  72. package/dist/schemas/prepare-context.js +31 -0
  73. package/dist/schemas/prepare-context.js.map +1 -0
  74. package/dist/schemas/recall.d.ts +41 -0
  75. package/dist/schemas/recall.d.ts.map +1 -0
  76. package/dist/schemas/recall.js +47 -0
  77. package/dist/schemas/recall.js.map +1 -0
  78. package/dist/schemas/record-scar-usage-batch.d.ts +82 -0
  79. package/dist/schemas/record-scar-usage-batch.d.ts.map +1 -0
  80. package/dist/schemas/record-scar-usage-batch.js +25 -0
  81. package/dist/schemas/record-scar-usage-batch.js.map +1 -0
  82. package/dist/schemas/record-scar-usage.d.ts +51 -0
  83. package/dist/schemas/record-scar-usage.d.ts.map +1 -0
  84. package/dist/schemas/record-scar-usage.js +32 -0
  85. package/dist/schemas/record-scar-usage.js.map +1 -0
  86. package/dist/schemas/save-transcript.d.ts +38 -0
  87. package/dist/schemas/save-transcript.d.ts.map +1 -0
  88. package/dist/schemas/save-transcript.js +30 -0
  89. package/dist/schemas/save-transcript.js.map +1 -0
  90. package/dist/schemas/search.d.ts +36 -0
  91. package/dist/schemas/search.d.ts.map +1 -0
  92. package/dist/schemas/search.js +27 -0
  93. package/dist/schemas/search.js.map +1 -0
  94. package/dist/schemas/session-close.d.ts +371 -0
  95. package/dist/schemas/session-close.d.ts.map +1 -0
  96. package/dist/schemas/session-close.js +95 -0
  97. package/dist/schemas/session-close.js.map +1 -0
  98. package/dist/schemas/session-start.d.ts +46 -0
  99. package/dist/schemas/session-start.d.ts.map +1 -0
  100. package/dist/schemas/session-start.js +33 -0
  101. package/dist/schemas/session-start.js.map +1 -0
  102. package/dist/schemas/thread.d.ts +72 -0
  103. package/dist/schemas/thread.d.ts.map +1 -0
  104. package/dist/schemas/thread.js +39 -0
  105. package/dist/schemas/thread.js.map +1 -0
  106. package/dist/server.d.ts +22 -0
  107. package/dist/server.d.ts.map +1 -0
  108. package/dist/server.js +313 -0
  109. package/dist/server.js.map +1 -0
  110. package/dist/services/active-sessions.d.ts +66 -0
  111. package/dist/services/active-sessions.d.ts.map +1 -0
  112. package/dist/services/active-sessions.js +311 -0
  113. package/dist/services/active-sessions.js.map +1 -0
  114. package/dist/services/agent-detection.d.ts +25 -0
  115. package/dist/services/agent-detection.d.ts.map +1 -0
  116. package/dist/services/agent-detection.js +93 -0
  117. package/dist/services/agent-detection.js.map +1 -0
  118. package/dist/services/analytics.d.ts +201 -0
  119. package/dist/services/analytics.d.ts.map +1 -0
  120. package/dist/services/analytics.js +483 -0
  121. package/dist/services/analytics.js.map +1 -0
  122. package/dist/services/cache.d.ts +148 -0
  123. package/dist/services/cache.d.ts.map +1 -0
  124. package/dist/services/cache.js +384 -0
  125. package/dist/services/cache.js.map +1 -0
  126. package/dist/services/cache.test.d.ts +8 -0
  127. package/dist/services/cache.test.d.ts.map +1 -0
  128. package/dist/services/cache.test.js +267 -0
  129. package/dist/services/cache.test.js.map +1 -0
  130. package/dist/services/compliance-validator.d.ts +30 -0
  131. package/dist/services/compliance-validator.d.ts.map +1 -0
  132. package/dist/services/compliance-validator.js +257 -0
  133. package/dist/services/compliance-validator.js.map +1 -0
  134. package/dist/services/config.d.ts +48 -0
  135. package/dist/services/config.d.ts.map +1 -0
  136. package/dist/services/config.js +128 -0
  137. package/dist/services/config.js.map +1 -0
  138. package/dist/services/embedding.d.ts +58 -0
  139. package/dist/services/embedding.d.ts.map +1 -0
  140. package/dist/services/embedding.js +243 -0
  141. package/dist/services/embedding.js.map +1 -0
  142. package/dist/services/gitmem-dir.d.ts +38 -0
  143. package/dist/services/gitmem-dir.d.ts.map +1 -0
  144. package/dist/services/gitmem-dir.js +84 -0
  145. package/dist/services/gitmem-dir.js.map +1 -0
  146. package/dist/services/local-file-storage.d.ts +56 -0
  147. package/dist/services/local-file-storage.d.ts.map +1 -0
  148. package/dist/services/local-file-storage.js +213 -0
  149. package/dist/services/local-file-storage.js.map +1 -0
  150. package/dist/services/local-vector-search.d.ts +137 -0
  151. package/dist/services/local-vector-search.d.ts.map +1 -0
  152. package/dist/services/local-vector-search.js +311 -0
  153. package/dist/services/local-vector-search.js.map +1 -0
  154. package/dist/services/metrics.d.ts +104 -0
  155. package/dist/services/metrics.d.ts.map +1 -0
  156. package/dist/services/metrics.js +264 -0
  157. package/dist/services/metrics.js.map +1 -0
  158. package/dist/services/session-state.d.ts +113 -0
  159. package/dist/services/session-state.d.ts.map +1 -0
  160. package/dist/services/session-state.js +203 -0
  161. package/dist/services/session-state.js.map +1 -0
  162. package/dist/services/startup.d.ts +112 -0
  163. package/dist/services/startup.d.ts.map +1 -0
  164. package/dist/services/startup.js +436 -0
  165. package/dist/services/startup.js.map +1 -0
  166. package/dist/services/storage.d.ts +43 -0
  167. package/dist/services/storage.d.ts.map +1 -0
  168. package/dist/services/storage.js +92 -0
  169. package/dist/services/storage.js.map +1 -0
  170. package/dist/services/supabase-client.d.ts +163 -0
  171. package/dist/services/supabase-client.d.ts.map +1 -0
  172. package/dist/services/supabase-client.js +510 -0
  173. package/dist/services/supabase-client.js.map +1 -0
  174. package/dist/services/thread-dedup.d.ts +44 -0
  175. package/dist/services/thread-dedup.d.ts.map +1 -0
  176. package/dist/services/thread-dedup.js +113 -0
  177. package/dist/services/thread-dedup.js.map +1 -0
  178. package/dist/services/thread-manager.d.ts +77 -0
  179. package/dist/services/thread-manager.d.ts.map +1 -0
  180. package/dist/services/thread-manager.js +250 -0
  181. package/dist/services/thread-manager.js.map +1 -0
  182. package/dist/services/thread-suggestions.d.ts +66 -0
  183. package/dist/services/thread-suggestions.d.ts.map +1 -0
  184. package/dist/services/thread-suggestions.js +243 -0
  185. package/dist/services/thread-suggestions.js.map +1 -0
  186. package/dist/services/thread-supabase.d.ts +111 -0
  187. package/dist/services/thread-supabase.d.ts.map +1 -0
  188. package/dist/services/thread-supabase.js +459 -0
  189. package/dist/services/thread-supabase.js.map +1 -0
  190. package/dist/services/thread-vitality.d.ts +65 -0
  191. package/dist/services/thread-vitality.d.ts.map +1 -0
  192. package/dist/services/thread-vitality.js +143 -0
  193. package/dist/services/thread-vitality.js.map +1 -0
  194. package/dist/services/tier.d.ts +52 -0
  195. package/dist/services/tier.d.ts.map +1 -0
  196. package/dist/services/tier.js +109 -0
  197. package/dist/services/tier.js.map +1 -0
  198. package/dist/services/timezone.d.ts +37 -0
  199. package/dist/services/timezone.d.ts.map +1 -0
  200. package/dist/services/timezone.js +147 -0
  201. package/dist/services/timezone.js.map +1 -0
  202. package/dist/services/transcript-chunker.d.ts +18 -0
  203. package/dist/services/transcript-chunker.d.ts.map +1 -0
  204. package/dist/services/transcript-chunker.js +237 -0
  205. package/dist/services/transcript-chunker.js.map +1 -0
  206. package/dist/services/triple-writer.d.ts +128 -0
  207. package/dist/services/triple-writer.d.ts.map +1 -0
  208. package/dist/services/triple-writer.js +338 -0
  209. package/dist/services/triple-writer.js.map +1 -0
  210. package/dist/services/variant-assignment.d.ts +92 -0
  211. package/dist/services/variant-assignment.d.ts.map +1 -0
  212. package/dist/services/variant-assignment.js +196 -0
  213. package/dist/services/variant-assignment.js.map +1 -0
  214. package/dist/tools/absorb-observations.d.ts +16 -0
  215. package/dist/tools/absorb-observations.d.ts.map +1 -0
  216. package/dist/tools/absorb-observations.js +82 -0
  217. package/dist/tools/absorb-observations.js.map +1 -0
  218. package/dist/tools/analyze.d.ts +55 -0
  219. package/dist/tools/analyze.d.ts.map +1 -0
  220. package/dist/tools/analyze.js +139 -0
  221. package/dist/tools/analyze.js.map +1 -0
  222. package/dist/tools/cleanup-threads.d.ts +47 -0
  223. package/dist/tools/cleanup-threads.d.ts.map +1 -0
  224. package/dist/tools/cleanup-threads.js +127 -0
  225. package/dist/tools/cleanup-threads.js.map +1 -0
  226. package/dist/tools/confirm-scars.d.ts +23 -0
  227. package/dist/tools/confirm-scars.d.ts.map +1 -0
  228. package/dist/tools/confirm-scars.js +209 -0
  229. package/dist/tools/confirm-scars.js.map +1 -0
  230. package/dist/tools/create-decision.d.ts +15 -0
  231. package/dist/tools/create-decision.d.ts.map +1 -0
  232. package/dist/tools/create-decision.js +138 -0
  233. package/dist/tools/create-decision.js.map +1 -0
  234. package/dist/tools/create-learning.d.ts +15 -0
  235. package/dist/tools/create-learning.d.ts.map +1 -0
  236. package/dist/tools/create-learning.js +226 -0
  237. package/dist/tools/create-learning.js.map +1 -0
  238. package/dist/tools/create-thread.d.ts +42 -0
  239. package/dist/tools/create-thread.d.ts.map +1 -0
  240. package/dist/tools/create-thread.js +180 -0
  241. package/dist/tools/create-thread.js.map +1 -0
  242. package/dist/tools/definitions.d.ts +5013 -0
  243. package/dist/tools/definitions.d.ts.map +1 -0
  244. package/dist/tools/definitions.js +2017 -0
  245. package/dist/tools/definitions.js.map +1 -0
  246. package/dist/tools/dismiss-suggestion.d.ts +20 -0
  247. package/dist/tools/dismiss-suggestion.d.ts.map +1 -0
  248. package/dist/tools/dismiss-suggestion.js +40 -0
  249. package/dist/tools/dismiss-suggestion.js.map +1 -0
  250. package/dist/tools/get-transcript.d.ts +24 -0
  251. package/dist/tools/get-transcript.d.ts.map +1 -0
  252. package/dist/tools/get-transcript.js +52 -0
  253. package/dist/tools/get-transcript.js.map +1 -0
  254. package/dist/tools/graph-traverse.d.ts +83 -0
  255. package/dist/tools/graph-traverse.d.ts.map +1 -0
  256. package/dist/tools/graph-traverse.js +394 -0
  257. package/dist/tools/graph-traverse.js.map +1 -0
  258. package/dist/tools/list-threads.d.ts +15 -0
  259. package/dist/tools/list-threads.d.ts.map +1 -0
  260. package/dist/tools/list-threads.js +114 -0
  261. package/dist/tools/list-threads.js.map +1 -0
  262. package/dist/tools/log.d.ts +43 -0
  263. package/dist/tools/log.d.ts.map +1 -0
  264. package/dist/tools/log.js +157 -0
  265. package/dist/tools/log.js.map +1 -0
  266. package/dist/tools/prepare-context.d.ts +36 -0
  267. package/dist/tools/prepare-context.d.ts.map +1 -0
  268. package/dist/tools/prepare-context.js +353 -0
  269. package/dist/tools/prepare-context.js.map +1 -0
  270. package/dist/tools/promote-suggestion.d.ts +25 -0
  271. package/dist/tools/promote-suggestion.d.ts.map +1 -0
  272. package/dist/tools/promote-suggestion.js +60 -0
  273. package/dist/tools/promote-suggestion.js.map +1 -0
  274. package/dist/tools/recall.d.ts +77 -0
  275. package/dist/tools/recall.d.ts.map +1 -0
  276. package/dist/tools/recall.js +423 -0
  277. package/dist/tools/recall.js.map +1 -0
  278. package/dist/tools/recall.test.d.ts +5 -0
  279. package/dist/tools/recall.test.d.ts.map +1 -0
  280. package/dist/tools/recall.test.js +155 -0
  281. package/dist/tools/recall.test.js.map +1 -0
  282. package/dist/tools/record-scar-usage-batch.d.ts +10 -0
  283. package/dist/tools/record-scar-usage-batch.d.ts.map +1 -0
  284. package/dist/tools/record-scar-usage-batch.js +153 -0
  285. package/dist/tools/record-scar-usage-batch.js.map +1 -0
  286. package/dist/tools/record-scar-usage.d.ts +14 -0
  287. package/dist/tools/record-scar-usage.d.ts.map +1 -0
  288. package/dist/tools/record-scar-usage.js +94 -0
  289. package/dist/tools/record-scar-usage.js.map +1 -0
  290. package/dist/tools/resolve-thread.d.ts +16 -0
  291. package/dist/tools/resolve-thread.d.ts.map +1 -0
  292. package/dist/tools/resolve-thread.js +102 -0
  293. package/dist/tools/resolve-thread.js.map +1 -0
  294. package/dist/tools/save-transcript.d.ts +29 -0
  295. package/dist/tools/save-transcript.d.ts.map +1 -0
  296. package/dist/tools/save-transcript.js +97 -0
  297. package/dist/tools/save-transcript.js.map +1 -0
  298. package/dist/tools/search.d.ts +46 -0
  299. package/dist/tools/search.d.ts.map +1 -0
  300. package/dist/tools/search.js +186 -0
  301. package/dist/tools/search.js.map +1 -0
  302. package/dist/tools/session-close.d.ts +14 -0
  303. package/dist/tools/session-close.d.ts.map +1 -0
  304. package/dist/tools/session-close.js +881 -0
  305. package/dist/tools/session-close.js.map +1 -0
  306. package/dist/tools/session-start.d.ts +38 -0
  307. package/dist/tools/session-start.d.ts.map +1 -0
  308. package/dist/tools/session-start.js +1104 -0
  309. package/dist/tools/session-start.js.map +1 -0
  310. package/dist/types/index.d.ts +456 -0
  311. package/dist/types/index.d.ts.map +1 -0
  312. package/dist/types/index.js +5 -0
  313. package/dist/types/index.js.map +1 -0
  314. package/package.json +76 -0
  315. package/schema/setup.sql +193 -0
  316. package/schema/starter-scars.json +206 -0
@@ -0,0 +1,384 @@
1
+ /**
2
+ * GitMem Cache Service
3
+ *
4
+ * File-based cache for GitMem MCP operations.
5
+ * Caches search results to avoid repeated ww-mcp calls.
6
+ *
7
+ * Design: docs/systems/gitmem-caching.md
8
+ * Issue: OD-473
9
+ */
10
+ import { createHash } from "crypto";
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, unlinkSync, statSync } from "fs";
12
+ import { join } from "path";
13
+ // Default cache directory
14
+ // Uses env var if set, otherwise falls back to user's home directory or /tmp
15
+ const DEFAULT_CACHE_DIR = process.env.GITMEM_CACHE_DIR ||
16
+ (process.env.HOME ? `${process.env.HOME}/.cache/gitmem` : "/tmp/gitmem-cache");
17
+ // TTL values in milliseconds
18
+ const TTL = {
19
+ SCAR_SEARCH: 15 * 60 * 1000, // 15 minutes
20
+ DECISIONS: 5 * 60 * 1000, // 5 minutes
21
+ WINS: 5 * 60 * 1000, // 5 minutes
22
+ SESSIONS: 10 * 60 * 1000, // 10 minutes (sessions change slowly)
23
+ SCAR_USAGE: 10 * 60 * 1000, // 10 minutes
24
+ };
25
+ // Max cache sizes
26
+ const MAX_RESULT_CACHE_SIZE = 10 * 1024 * 1024; // 10MB
27
+ /**
28
+ * Generate a short hash for cache keys
29
+ */
30
+ function hashText(text) {
31
+ return createHash("sha256")
32
+ .update(text.toLowerCase().trim())
33
+ .digest("hex")
34
+ .slice(0, 16);
35
+ }
36
+ /**
37
+ * CacheService class
38
+ *
39
+ * Provides file-based caching for GitMem operations.
40
+ * Designed for graceful degradation - cache failures never block operations.
41
+ */
42
+ export class CacheService {
43
+ cacheDir;
44
+ resultsDir;
45
+ enabled = true;
46
+ // Stats tracking
47
+ hits = 0;
48
+ misses = 0;
49
+ constructor(cacheDir = DEFAULT_CACHE_DIR) {
50
+ this.cacheDir = cacheDir;
51
+ this.resultsDir = join(cacheDir, "results");
52
+ this.init();
53
+ }
54
+ /**
55
+ * Initialize cache directories
56
+ */
57
+ init() {
58
+ try {
59
+ if (!existsSync(this.cacheDir)) {
60
+ mkdirSync(this.cacheDir, { recursive: true, mode: 0o700 });
61
+ }
62
+ if (!existsSync(this.resultsDir)) {
63
+ mkdirSync(this.resultsDir, { recursive: true, mode: 0o700 });
64
+ }
65
+ }
66
+ catch (error) {
67
+ console.warn("[cache] Failed to initialize cache directory:", error);
68
+ this.enabled = false;
69
+ }
70
+ }
71
+ /**
72
+ * Check if cache is enabled and working
73
+ */
74
+ isEnabled() {
75
+ return this.enabled;
76
+ }
77
+ /**
78
+ * Disable cache (for testing or on errors)
79
+ */
80
+ disable() {
81
+ this.enabled = false;
82
+ }
83
+ /**
84
+ * Generate cache key for scar search
85
+ */
86
+ scarSearchKey(query, project, matchCount) {
87
+ return `scar_search:${hashText(query)}:${project}:${matchCount}`;
88
+ }
89
+ /**
90
+ * Generate cache key for decisions
91
+ */
92
+ decisionsKey(project, limit) {
93
+ return `decisions:${project}:${limit}`;
94
+ }
95
+ /**
96
+ * Generate cache key for wins
97
+ */
98
+ winsKey(project, limit) {
99
+ return `wins:${project}:${limit}`;
100
+ }
101
+ /**
102
+ * Get cached result
103
+ */
104
+ async getResult(key) {
105
+ if (!this.enabled)
106
+ return null;
107
+ try {
108
+ const filename = this.keyToFilename(key);
109
+ const filepath = join(this.resultsDir, filename);
110
+ if (!existsSync(filepath)) {
111
+ this.misses++;
112
+ return null;
113
+ }
114
+ const content = readFileSync(filepath, "utf-8");
115
+ const entry = JSON.parse(content);
116
+ // Check expiration
117
+ const now = Date.now();
118
+ if (now > entry.expires_at) {
119
+ // Expired - delete and return null
120
+ try {
121
+ unlinkSync(filepath);
122
+ }
123
+ catch {
124
+ // Ignore delete errors
125
+ }
126
+ this.misses++;
127
+ console.error(`[cache] EXPIRED: ${key}`);
128
+ return null;
129
+ }
130
+ this.hits++;
131
+ const age_ms = now - entry.created_at;
132
+ console.error(`[cache] HIT: ${key} (age: ${age_ms}ms)`);
133
+ return { data: entry.data, age_ms };
134
+ }
135
+ catch (error) {
136
+ console.warn(`[cache] Error reading ${key}:`, error);
137
+ this.misses++;
138
+ return null;
139
+ }
140
+ }
141
+ /**
142
+ * Set cached result
143
+ */
144
+ async setResult(key, data, ttlMs) {
145
+ if (!this.enabled)
146
+ return;
147
+ try {
148
+ const now = Date.now();
149
+ const entry = {
150
+ key,
151
+ created_at: now,
152
+ expires_at: now + ttlMs,
153
+ data,
154
+ };
155
+ const filename = this.keyToFilename(key);
156
+ const filepath = join(this.resultsDir, filename);
157
+ const content = JSON.stringify(entry, null, 2);
158
+ writeFileSync(filepath, content, { mode: 0o600 });
159
+ console.error(`[cache] SET: ${key} (TTL: ${ttlMs}ms)`);
160
+ }
161
+ catch (error) {
162
+ console.warn(`[cache] Error writing ${key}:`, error);
163
+ // Don't disable cache on write errors - might be transient
164
+ }
165
+ }
166
+ /**
167
+ * Convenience method: get or fetch scar search results
168
+ */
169
+ async getOrFetchScarSearch(query, project, matchCount, fetcher) {
170
+ const key = this.scarSearchKey(query, project, matchCount);
171
+ const cached = await this.getResult(key);
172
+ if (cached) {
173
+ return { data: cached.data, cache_hit: true, cache_age_ms: cached.age_ms };
174
+ }
175
+ // Cache miss - fetch from source
176
+ const data = await fetcher();
177
+ // Cache the result (async, don't await)
178
+ this.setResult(key, data, TTL.SCAR_SEARCH).catch(() => { });
179
+ return { data, cache_hit: false };
180
+ }
181
+ /**
182
+ * Convenience method: get or fetch decisions
183
+ */
184
+ async getOrFetchDecisions(project, limit, fetcher) {
185
+ const key = this.decisionsKey(project, limit);
186
+ const cached = await this.getResult(key);
187
+ if (cached) {
188
+ return { data: cached.data, cache_hit: true, cache_age_ms: cached.age_ms };
189
+ }
190
+ // Cache miss - fetch from source
191
+ const data = await fetcher();
192
+ // Cache the result (async, don't await)
193
+ this.setResult(key, data, TTL.DECISIONS).catch(() => { });
194
+ return { data, cache_hit: false };
195
+ }
196
+ /**
197
+ * Convenience method: get or fetch wins
198
+ */
199
+ async getOrFetchWins(project, limit, fetcher) {
200
+ const key = this.winsKey(project, limit);
201
+ const cached = await this.getResult(key);
202
+ if (cached) {
203
+ return { data: cached.data, cache_hit: true, cache_age_ms: cached.age_ms };
204
+ }
205
+ // Cache miss - fetch from source
206
+ const data = await fetcher();
207
+ // Cache the result (async, don't await)
208
+ this.setResult(key, data, TTL.WINS).catch(() => { });
209
+ return { data, cache_hit: false };
210
+ }
211
+ /**
212
+ * Generate cache key for sessions (analytics)
213
+ */
214
+ sessionsKey(project, days, agent) {
215
+ return `sessions:${project}:${days}d${agent ? `:${agent}` : ""}`;
216
+ }
217
+ /**
218
+ * Generate cache key for scar usage (analytics)
219
+ */
220
+ scarUsageKey(project, days, agent) {
221
+ return `scar_usage:${project}:${days}d${agent ? `:${agent}` : ""}`;
222
+ }
223
+ /**
224
+ * Convenience method: get or fetch sessions
225
+ */
226
+ async getOrFetchSessions(project, days, agent, fetcher) {
227
+ const key = this.sessionsKey(project, days, agent);
228
+ const cached = await this.getResult(key);
229
+ if (cached) {
230
+ return { data: cached.data, cache_hit: true, cache_age_ms: cached.age_ms };
231
+ }
232
+ const data = await fetcher();
233
+ this.setResult(key, data, TTL.SESSIONS).catch(() => { });
234
+ return { data, cache_hit: false };
235
+ }
236
+ /**
237
+ * Convenience method: get or fetch scar usage
238
+ */
239
+ async getOrFetchScarUsage(project, days, agent, fetcher) {
240
+ const key = this.scarUsageKey(project, days, agent);
241
+ const cached = await this.getResult(key);
242
+ if (cached) {
243
+ return { data: cached.data, cache_hit: true, cache_age_ms: cached.age_ms };
244
+ }
245
+ const data = await fetcher();
246
+ this.setResult(key, data, TTL.SCAR_USAGE).catch(() => { });
247
+ return { data, cache_hit: false };
248
+ }
249
+ /**
250
+ * Clean up expired entries
251
+ */
252
+ async cleanup() {
253
+ if (!this.enabled)
254
+ return 0;
255
+ let cleaned = 0;
256
+ try {
257
+ const files = readdirSync(this.resultsDir);
258
+ const now = Date.now();
259
+ for (const file of files) {
260
+ if (!file.endsWith(".json"))
261
+ continue;
262
+ const filepath = join(this.resultsDir, file);
263
+ try {
264
+ const content = readFileSync(filepath, "utf-8");
265
+ const entry = JSON.parse(content);
266
+ if (now > entry.expires_at) {
267
+ unlinkSync(filepath);
268
+ cleaned++;
269
+ }
270
+ }
271
+ catch {
272
+ // Delete corrupted files
273
+ try {
274
+ unlinkSync(filepath);
275
+ cleaned++;
276
+ }
277
+ catch {
278
+ // Ignore
279
+ }
280
+ }
281
+ }
282
+ if (cleaned > 0) {
283
+ console.error(`[cache] Cleaned up ${cleaned} expired entries`);
284
+ }
285
+ }
286
+ catch (error) {
287
+ console.warn("[cache] Error during cleanup:", error);
288
+ }
289
+ return cleaned;
290
+ }
291
+ /**
292
+ * Clear all cached data
293
+ */
294
+ async clear() {
295
+ try {
296
+ const files = readdirSync(this.resultsDir);
297
+ for (const file of files) {
298
+ try {
299
+ unlinkSync(join(this.resultsDir, file));
300
+ }
301
+ catch {
302
+ // Ignore individual file errors
303
+ }
304
+ }
305
+ console.error("[cache] Cache cleared");
306
+ }
307
+ catch (error) {
308
+ console.warn("[cache] Error clearing cache:", error);
309
+ }
310
+ }
311
+ /**
312
+ * Get cache statistics
313
+ */
314
+ async getStats() {
315
+ const stats = {
316
+ resultCount: 0,
317
+ resultBytes: 0,
318
+ oldestEntry: null,
319
+ };
320
+ if (!this.enabled)
321
+ return stats;
322
+ try {
323
+ const files = readdirSync(this.resultsDir);
324
+ let oldestTime = Infinity;
325
+ for (const file of files) {
326
+ if (!file.endsWith(".json"))
327
+ continue;
328
+ const filepath = join(this.resultsDir, file);
329
+ try {
330
+ const fileStat = statSync(filepath);
331
+ stats.resultCount++;
332
+ stats.resultBytes += fileStat.size;
333
+ const content = readFileSync(filepath, "utf-8");
334
+ const entry = JSON.parse(content);
335
+ if (entry.created_at < oldestTime) {
336
+ oldestTime = entry.created_at;
337
+ }
338
+ }
339
+ catch {
340
+ // Skip problematic files
341
+ }
342
+ }
343
+ if (oldestTime !== Infinity) {
344
+ stats.oldestEntry = new Date(oldestTime);
345
+ }
346
+ // Calculate hit rate
347
+ const total = this.hits + this.misses;
348
+ if (total > 0) {
349
+ stats.hitRate = this.hits / total;
350
+ }
351
+ }
352
+ catch (error) {
353
+ console.warn("[cache] Error getting stats:", error);
354
+ }
355
+ return stats;
356
+ }
357
+ /**
358
+ * Convert cache key to safe filename
359
+ */
360
+ keyToFilename(key) {
361
+ // Replace unsafe chars with underscores, keep it readable
362
+ return key.replace(/[^a-zA-Z0-9_:-]/g, "_") + ".json";
363
+ }
364
+ }
365
+ // Singleton instance
366
+ let cacheInstance = null;
367
+ /**
368
+ * Get the cache service singleton
369
+ */
370
+ export function getCache() {
371
+ if (!cacheInstance) {
372
+ cacheInstance = new CacheService();
373
+ }
374
+ return cacheInstance;
375
+ }
376
+ /**
377
+ * Reset cache instance (for testing)
378
+ */
379
+ export function resetCache() {
380
+ cacheInstance = null;
381
+ }
382
+ // Export TTL values for external use
383
+ export { TTL };
384
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/services/cache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * CacheService Unit Tests
3
+ *
4
+ * Tests for the GitMem file-based caching layer.
5
+ * Issue: OD-473
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=cache.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.test.d.ts","sourceRoot":"","sources":["../../src/services/cache.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
@@ -0,0 +1,267 @@
1
+ /**
2
+ * CacheService Unit Tests
3
+ *
4
+ * Tests for the GitMem file-based caching layer.
5
+ * Issue: OD-473
6
+ */
7
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
8
+ import { existsSync, rmSync, writeFileSync } from "fs";
9
+ import { join } from "path";
10
+ import { CacheService, TTL, getCache, resetCache } from "./cache.js";
11
+ // Test cache directory (isolated from production)
12
+ const TEST_CACHE_DIR = "/tmp/gitmem-cache-test";
13
+ describe("CacheService", () => {
14
+ let cache;
15
+ beforeEach(() => {
16
+ // Clean up any existing test cache
17
+ if (existsSync(TEST_CACHE_DIR)) {
18
+ rmSync(TEST_CACHE_DIR, { recursive: true });
19
+ }
20
+ cache = new CacheService(TEST_CACHE_DIR);
21
+ });
22
+ afterEach(() => {
23
+ // Clean up test cache
24
+ if (existsSync(TEST_CACHE_DIR)) {
25
+ rmSync(TEST_CACHE_DIR, { recursive: true });
26
+ }
27
+ resetCache();
28
+ });
29
+ describe("initialization", () => {
30
+ it("creates cache directory on initialization", () => {
31
+ expect(existsSync(TEST_CACHE_DIR)).toBe(true);
32
+ expect(existsSync(join(TEST_CACHE_DIR, "results"))).toBe(true);
33
+ });
34
+ it("is enabled after successful initialization", () => {
35
+ expect(cache.isEnabled()).toBe(true);
36
+ });
37
+ it("can be disabled", () => {
38
+ cache.disable();
39
+ expect(cache.isEnabled()).toBe(false);
40
+ });
41
+ });
42
+ describe("cache key generation", () => {
43
+ it("generates consistent scar search keys", () => {
44
+ const key1 = cache.scarSearchKey("test query", "orchestra_dev", 5);
45
+ const key2 = cache.scarSearchKey("test query", "orchestra_dev", 5);
46
+ expect(key1).toBe(key2);
47
+ });
48
+ it("generates different keys for different queries", () => {
49
+ const key1 = cache.scarSearchKey("query one", "orchestra_dev", 5);
50
+ const key2 = cache.scarSearchKey("query two", "orchestra_dev", 5);
51
+ expect(key1).not.toBe(key2);
52
+ });
53
+ it("generates different keys for different projects", () => {
54
+ const key1 = cache.scarSearchKey("test", "orchestra_dev", 5);
55
+ const key2 = cache.scarSearchKey("test", "weekend_warrior", 5);
56
+ expect(key1).not.toBe(key2);
57
+ });
58
+ it("generates different keys for different match counts", () => {
59
+ const key1 = cache.scarSearchKey("test", "orchestra_dev", 3);
60
+ const key2 = cache.scarSearchKey("test", "orchestra_dev", 5);
61
+ expect(key1).not.toBe(key2);
62
+ });
63
+ it("normalizes query text (lowercase, trim)", () => {
64
+ const key1 = cache.scarSearchKey(" Test Query ", "orchestra_dev", 5);
65
+ const key2 = cache.scarSearchKey("test query", "orchestra_dev", 5);
66
+ expect(key1).toBe(key2);
67
+ });
68
+ it("generates decisions keys", () => {
69
+ const key = cache.decisionsKey("orchestra_dev", 5);
70
+ expect(key).toBe("decisions:orchestra_dev:5");
71
+ });
72
+ });
73
+ describe("setResult and getResult", () => {
74
+ it("stores and retrieves data", async () => {
75
+ const testData = { items: [1, 2, 3], message: "test" };
76
+ await cache.setResult("test-key", testData, 60000);
77
+ const result = await cache.getResult("test-key");
78
+ expect(result).not.toBeNull();
79
+ expect(result.data).toEqual(testData);
80
+ });
81
+ it("returns null for non-existent key", async () => {
82
+ const result = await cache.getResult("non-existent");
83
+ expect(result).toBeNull();
84
+ });
85
+ it("returns cache age in milliseconds", async () => {
86
+ const testData = { test: true };
87
+ await cache.setResult("test-key", testData, 60000);
88
+ // Wait a small amount
89
+ await new Promise((r) => setTimeout(r, 10));
90
+ const result = await cache.getResult("test-key");
91
+ expect(result).not.toBeNull();
92
+ expect(result.age_ms).toBeGreaterThan(0);
93
+ expect(result.age_ms).toBeLessThan(1000); // Should be very fast
94
+ });
95
+ it("returns null for expired entries", async () => {
96
+ const testData = { test: true };
97
+ // Set with very short TTL
98
+ await cache.setResult("expiring-key", testData, 1);
99
+ // Wait for expiration
100
+ await new Promise((r) => setTimeout(r, 10));
101
+ const result = await cache.getResult("expiring-key");
102
+ expect(result).toBeNull();
103
+ });
104
+ it("returns null when cache is disabled", async () => {
105
+ cache.disable();
106
+ await cache.setResult("test-key", { test: true }, 60000);
107
+ const result = await cache.getResult("test-key");
108
+ expect(result).toBeNull();
109
+ });
110
+ });
111
+ describe("getOrFetchScarSearch", () => {
112
+ it("returns cached data on hit", async () => {
113
+ const mockData = [{ id: "1", title: "Scar 1" }];
114
+ let fetchCount = 0;
115
+ const fetcher = async () => {
116
+ fetchCount++;
117
+ return mockData;
118
+ };
119
+ // First call - should fetch
120
+ const result1 = await cache.getOrFetchScarSearch("test query", "orchestra_dev", 5, fetcher);
121
+ expect(result1.cache_hit).toBe(false);
122
+ expect(result1.data).toEqual(mockData);
123
+ expect(fetchCount).toBe(1);
124
+ // Second call - should use cache
125
+ const result2 = await cache.getOrFetchScarSearch("test query", "orchestra_dev", 5, fetcher);
126
+ expect(result2.cache_hit).toBe(true);
127
+ expect(result2.data).toEqual(mockData);
128
+ expect(result2.cache_age_ms).toBeDefined();
129
+ expect(fetchCount).toBe(1); // Fetcher not called again
130
+ });
131
+ it("fetches on cache miss", async () => {
132
+ const mockData = [{ id: "1", title: "Scar 1" }];
133
+ const fetcher = async () => mockData;
134
+ const result = await cache.getOrFetchScarSearch("new query", "orchestra_dev", 5, fetcher);
135
+ expect(result.cache_hit).toBe(false);
136
+ expect(result.data).toEqual(mockData);
137
+ });
138
+ });
139
+ describe("getOrFetchDecisions", () => {
140
+ it("returns cached data on hit", async () => {
141
+ const mockData = [{ id: "1", title: "Decision 1" }];
142
+ let fetchCount = 0;
143
+ const fetcher = async () => {
144
+ fetchCount++;
145
+ return mockData;
146
+ };
147
+ // First call - should fetch
148
+ const result1 = await cache.getOrFetchDecisions("orchestra_dev", 5, fetcher);
149
+ expect(result1.cache_hit).toBe(false);
150
+ expect(fetchCount).toBe(1);
151
+ // Second call - should use cache
152
+ const result2 = await cache.getOrFetchDecisions("orchestra_dev", 5, fetcher);
153
+ expect(result2.cache_hit).toBe(true);
154
+ expect(fetchCount).toBe(1);
155
+ });
156
+ });
157
+ describe("cleanup", () => {
158
+ it("removes expired entries", async () => {
159
+ // Create an expired entry manually
160
+ const expiredEntry = {
161
+ key: "expired-key",
162
+ created_at: Date.now() - 100000,
163
+ expires_at: Date.now() - 50000, // Expired
164
+ data: { test: true },
165
+ };
166
+ const filepath = join(TEST_CACHE_DIR, "results", "expired-key.json");
167
+ writeFileSync(filepath, JSON.stringify(expiredEntry));
168
+ // Create a valid entry
169
+ await cache.setResult("valid-key", { test: true }, 60000);
170
+ // Run cleanup
171
+ const cleaned = await cache.cleanup();
172
+ expect(cleaned).toBe(1);
173
+ // Verify expired entry is gone
174
+ expect(existsSync(filepath)).toBe(false);
175
+ // Verify valid entry remains
176
+ const validResult = await cache.getResult("valid-key");
177
+ expect(validResult).not.toBeNull();
178
+ });
179
+ });
180
+ describe("clear", () => {
181
+ it("removes all cached entries", async () => {
182
+ await cache.setResult("key1", { test: 1 }, 60000);
183
+ await cache.setResult("key2", { test: 2 }, 60000);
184
+ await cache.clear();
185
+ const result1 = await cache.getResult("key1");
186
+ const result2 = await cache.getResult("key2");
187
+ expect(result1).toBeNull();
188
+ expect(result2).toBeNull();
189
+ });
190
+ });
191
+ describe("getStats", () => {
192
+ it("returns cache statistics", async () => {
193
+ await cache.setResult("key1", { test: 1 }, 60000);
194
+ await cache.setResult("key2", { test: 2 }, 60000);
195
+ const stats = await cache.getStats();
196
+ expect(stats.resultCount).toBe(2);
197
+ expect(stats.resultBytes).toBeGreaterThan(0);
198
+ expect(stats.oldestEntry).toBeInstanceOf(Date);
199
+ });
200
+ it("tracks hit rate", async () => {
201
+ await cache.setResult("key1", { test: 1 }, 60000);
202
+ // Generate some hits and misses
203
+ await cache.getResult("key1"); // Hit
204
+ await cache.getResult("key1"); // Hit
205
+ await cache.getResult("nonexistent"); // Miss
206
+ const stats = await cache.getStats();
207
+ expect(stats.hitRate).toBeCloseTo(2 / 3, 1);
208
+ });
209
+ });
210
+ describe("TTL values", () => {
211
+ it("exports correct TTL values", () => {
212
+ expect(TTL.SCAR_SEARCH).toBe(15 * 60 * 1000); // 15 minutes
213
+ expect(TTL.DECISIONS).toBe(5 * 60 * 1000); // 5 minutes
214
+ });
215
+ });
216
+ describe("singleton pattern", () => {
217
+ it("getCache returns same instance", () => {
218
+ // Note: getCache uses default dir which may not be writable in test env
219
+ // This test verifies the singleton behavior, not the cache functionality
220
+ resetCache();
221
+ // Set env var to use test directory
222
+ const originalDir = process.env.GITMEM_CACHE_DIR;
223
+ process.env.GITMEM_CACHE_DIR = TEST_CACHE_DIR;
224
+ try {
225
+ const cache1 = getCache();
226
+ const cache2 = getCache();
227
+ expect(cache1).toBe(cache2);
228
+ }
229
+ finally {
230
+ process.env.GITMEM_CACHE_DIR = originalDir;
231
+ resetCache();
232
+ }
233
+ });
234
+ it("resetCache creates new instance", () => {
235
+ const originalDir = process.env.GITMEM_CACHE_DIR;
236
+ process.env.GITMEM_CACHE_DIR = TEST_CACHE_DIR;
237
+ try {
238
+ const cache1 = getCache();
239
+ resetCache();
240
+ const cache2 = getCache();
241
+ expect(cache1).not.toBe(cache2);
242
+ }
243
+ finally {
244
+ process.env.GITMEM_CACHE_DIR = originalDir;
245
+ resetCache();
246
+ }
247
+ });
248
+ });
249
+ describe("error handling", () => {
250
+ it("gracefully handles corrupted cache files", async () => {
251
+ // Write corrupted JSON
252
+ const filepath = join(TEST_CACHE_DIR, "results", "corrupted.json");
253
+ writeFileSync(filepath, "not valid json {{{");
254
+ // Should return null, not throw
255
+ const result = await cache.getResult("corrupted");
256
+ expect(result).toBeNull();
257
+ });
258
+ it("gracefully handles file system errors", async () => {
259
+ // Disable cache to simulate error condition
260
+ cache.disable();
261
+ // Operations should not throw
262
+ await expect(cache.setResult("key", { test: true }, 60000)).resolves.not.toThrow();
263
+ await expect(cache.getResult("key")).resolves.toBeNull();
264
+ });
265
+ });
266
+ });
267
+ //# sourceMappingURL=cache.test.js.map