@stackmemoryai/stackmemory 1.12.0 → 1.14.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 (162) hide show
  1. package/LICENSE +131 -64
  2. package/README.md +3 -1
  3. package/bin/claude-sm +16 -1
  4. package/bin/claude-smd +16 -1
  5. package/bin/codex-smd +16 -1
  6. package/bin/gemini-sm +16 -1
  7. package/bin/hermes-sm +21 -0
  8. package/bin/hermes-smd +21 -0
  9. package/bin/opencode-sm +16 -1
  10. package/dist/src/cli/codex-sm.js +51 -11
  11. package/dist/src/cli/commands/brain.js +206 -0
  12. package/dist/src/cli/commands/company-os.js +184 -0
  13. package/dist/src/cli/commands/context.js +5 -0
  14. package/dist/src/cli/commands/operator.js +127 -0
  15. package/dist/src/cli/commands/orchestrate.js +2 -0
  16. package/dist/src/cli/commands/orchestrator.js +3 -2
  17. package/dist/src/cli/commands/patterns.js +254 -0
  18. package/dist/src/cli/commands/portal.js +161 -0
  19. package/dist/src/cli/commands/scaffold.js +92 -0
  20. package/dist/src/cli/commands/setup.js +1 -4
  21. package/dist/src/cli/commands/sync.js +253 -0
  22. package/dist/src/cli/commands/tasks.js +130 -1
  23. package/dist/src/cli/commands/vision.js +221 -0
  24. package/dist/src/cli/hermes-sm.js +224 -0
  25. package/dist/src/cli/index.js +15 -10
  26. package/dist/src/cli/utils/real-cli-bin.js +72 -0
  27. package/dist/src/core/brain/brain-store.js +187 -0
  28. package/dist/src/core/brain/brain-sync.js +193 -0
  29. package/dist/src/core/brain/index.js +78 -0
  30. package/dist/src/core/brain/types.js +10 -0
  31. package/dist/src/core/cache/token-estimator.js +24 -1
  32. package/dist/src/core/config/feature-flags.js +2 -6
  33. package/dist/src/core/context/frame-database.js +44 -0
  34. package/dist/src/core/context/recursive-context-manager.js +1 -1
  35. package/dist/src/core/context/rehydration.js +2 -1
  36. package/dist/src/core/database/sqlite-adapter.js +14 -1
  37. package/dist/src/core/models/model-router.js +33 -1
  38. package/dist/src/core/models/provider-pricing.js +58 -4
  39. package/dist/src/core/patterns/index.js +22 -0
  40. package/dist/src/core/patterns/pattern-applier.js +39 -0
  41. package/dist/src/core/patterns/pattern-observer.js +157 -0
  42. package/dist/src/core/patterns/pattern-store.js +259 -0
  43. package/dist/src/core/patterns/types.js +19 -0
  44. package/dist/src/core/retrieval/llm-context-retrieval.js +5 -4
  45. package/dist/src/core/retrieval/unified-context-assembler.js +11 -66
  46. package/dist/src/core/skill-packs/types.js +14 -1
  47. package/dist/src/core/storage/cloud-sync-manager.js +116 -0
  48. package/dist/src/core/storage/cloud-sync.js +574 -0
  49. package/dist/src/core/storage/two-tier-storage.js +5 -1
  50. package/dist/src/core/tasks/master-tasks-template.js +43 -0
  51. package/dist/src/core/tasks/md-task-parser.js +138 -0
  52. package/dist/src/core/vision/index.js +27 -0
  53. package/dist/src/core/vision/signals.js +79 -0
  54. package/dist/src/core/vision/types.js +22 -0
  55. package/dist/src/core/vision/vision-file.js +146 -0
  56. package/dist/src/core/vision/vision-loop.js +220 -0
  57. package/dist/src/core/wiki/wiki-compiler.js +103 -1
  58. package/dist/src/daemon/daemon-config.js +45 -0
  59. package/dist/src/daemon/services/desire-path-service.js +566 -0
  60. package/dist/src/daemon/services/research-stream-service.js +320 -0
  61. package/dist/src/daemon/services/telemetry-service.js +192 -0
  62. package/dist/src/daemon/unified-daemon.js +28 -1
  63. package/dist/src/features/browser/cli-browser-agent.js +417 -0
  64. package/dist/src/features/browser/stagehand-workflows.js +578 -0
  65. package/dist/src/features/operator/adapter-factory.js +62 -0
  66. package/dist/src/features/operator/browser-adapter.js +109 -0
  67. package/dist/src/features/operator/desktop-adapter.js +125 -0
  68. package/dist/src/features/operator/index.js +39 -0
  69. package/dist/src/features/operator/llm-decision.js +137 -0
  70. package/dist/src/features/operator/operator-logger.js +92 -0
  71. package/dist/src/features/operator/overnight-runner.js +327 -0
  72. package/dist/src/features/operator/screen-adapter.js +91 -0
  73. package/dist/src/features/operator/session-manager.js +127 -0
  74. package/dist/src/features/operator/state-machine.js +227 -0
  75. package/dist/src/features/operator/task-queue.js +81 -0
  76. package/dist/src/features/portal/index.js +26 -0
  77. package/dist/src/features/portal/server.js +240 -0
  78. package/dist/src/features/portal/types.js +14 -0
  79. package/dist/src/features/portal/ui.js +195 -0
  80. package/dist/src/features/tasks/task-aware-context.js +2 -1
  81. package/dist/src/features/tui/simple-monitor.js +0 -23
  82. package/dist/src/features/tui/swarm-monitor.js +8 -66
  83. package/dist/src/{integrations/diffmem/index.js → features/web/client/hooks/use-socket.js} +6 -5
  84. package/dist/src/features/web/client/lib/utils.js +12 -0
  85. package/dist/src/features/web/client/stores/session-store.js +12 -0
  86. package/dist/src/features/web/server/gcp-billing.js +76 -0
  87. package/dist/src/features/web/server/index.js +10 -0
  88. package/dist/src/features/web/server/spend-calculator.js +228 -0
  89. package/dist/src/hooks/schemas.js +4 -1
  90. package/dist/src/integrations/anthropic/client.js +3 -2
  91. package/dist/src/integrations/claude-code/agent-bridge.js +0 -3
  92. package/dist/src/integrations/claude-code/subagent-client.js +218 -11
  93. package/dist/src/integrations/claude-code/task-coordinator.js +2 -1
  94. package/dist/src/integrations/linear/webhook-retry.js +196 -0
  95. package/dist/src/integrations/linear/webhook-server.js +18 -22
  96. package/dist/src/integrations/mcp/handlers/cloud-sync-handlers.js +101 -0
  97. package/dist/src/integrations/mcp/handlers/index.js +27 -52
  98. package/dist/src/integrations/mcp/server.js +122 -335
  99. package/dist/src/integrations/mcp/tool-alias-registry.js +0 -73
  100. package/dist/src/integrations/mcp/tool-definitions.js +111 -510
  101. package/dist/src/mcp/stackmemory-mcp-server.js +404 -379
  102. package/dist/src/orchestrators/multimodal/determinism.js +2 -1
  103. package/dist/src/orchestrators/multimodal/harness.js +2 -1
  104. package/dist/src/skills/recursive-agent-orchestrator.js +2 -4
  105. package/dist/src/utils/process-cleanup.js +1 -7
  106. package/docs/README.md +42 -0
  107. package/docs/guides/README_INSTALL.md +208 -0
  108. package/package.json +18 -9
  109. package/scripts/claude-code-wrapper.sh +11 -0
  110. package/scripts/claude-sm-setup.sh +12 -1
  111. package/scripts/codex-wrapper.sh +11 -0
  112. package/scripts/git-hooks/branch-context-manager.sh +11 -0
  113. package/scripts/git-hooks/post-checkout-stackmemory.sh +11 -0
  114. package/scripts/git-hooks/post-commit-stackmemory.sh +11 -0
  115. package/scripts/git-hooks/pre-commit-stackmemory.sh +11 -0
  116. package/scripts/hooks/cleanup-shell.sh +12 -1
  117. package/scripts/hooks/task-complete.sh +12 -1
  118. package/scripts/install-code-execution-hooks.sh +12 -1
  119. package/scripts/install-sweep-hook.sh +12 -0
  120. package/scripts/install.sh +11 -0
  121. package/scripts/opencode-wrapper.sh +11 -0
  122. package/scripts/portal/cloud-init.yaml +69 -0
  123. package/scripts/portal/setup.sh +69 -0
  124. package/scripts/portal/stackmemory-portal.service +34 -0
  125. package/scripts/setup-claude-integration.sh +12 -1
  126. package/scripts/smoke-init-db.sh +23 -0
  127. package/scripts/stackmemory-daemon.sh +11 -0
  128. package/scripts/verify-dist.cjs +11 -4
  129. package/dist/src/cli/commands/ralph.js +0 -1053
  130. package/dist/src/hooks/diffmem-hooks.js +0 -376
  131. package/dist/src/integrations/diffmem/client.js +0 -208
  132. package/dist/src/integrations/diffmem/config.js +0 -14
  133. package/dist/src/integrations/greptile/client.js +0 -101
  134. package/dist/src/integrations/greptile/config.js +0 -14
  135. package/dist/src/integrations/greptile/index.js +0 -11
  136. package/dist/src/integrations/mcp/handlers/cross-search-handlers.js +0 -188
  137. package/dist/src/integrations/mcp/handlers/diffmem-handlers.js +0 -455
  138. package/dist/src/integrations/mcp/handlers/greptile-handlers.js +0 -456
  139. package/dist/src/integrations/mcp/handlers/provider-handlers.js +0 -227
  140. package/dist/src/integrations/ralph/bridge/ralph-stackmemory-bridge.js +0 -863
  141. package/dist/src/integrations/ralph/context/context-budget-manager.js +0 -308
  142. package/dist/src/integrations/ralph/context/stackmemory-context-loader.js +0 -354
  143. package/dist/src/integrations/ralph/index.js +0 -17
  144. package/dist/src/integrations/ralph/learning/pattern-learner.js +0 -416
  145. package/dist/src/integrations/ralph/lifecycle/iteration-lifecycle.js +0 -448
  146. package/dist/src/integrations/ralph/loopmax.js +0 -488
  147. package/dist/src/integrations/ralph/monitoring/swarm-dashboard.js +0 -293
  148. package/dist/src/integrations/ralph/monitoring/swarm-registry.js +0 -107
  149. package/dist/src/integrations/ralph/orchestration/multi-loop-orchestrator.js +0 -508
  150. package/dist/src/integrations/ralph/patterns/compounding-engineering-pattern.js +0 -407
  151. package/dist/src/integrations/ralph/patterns/extended-coherence-sessions.js +0 -495
  152. package/dist/src/integrations/ralph/patterns/oracle-worker-pattern.js +0 -387
  153. package/dist/src/integrations/ralph/performance/performance-optimizer.js +0 -357
  154. package/dist/src/integrations/ralph/recovery/crash-recovery.js +0 -461
  155. package/dist/src/integrations/ralph/state/state-reconciler.js +0 -420
  156. package/dist/src/integrations/ralph/swarm/git-workflow-manager.js +0 -444
  157. package/dist/src/integrations/ralph/swarm/swarm-coordinator.js +0 -1005
  158. package/dist/src/integrations/ralph/visualization/ralph-debugger.js +0 -635
  159. package/scripts/ralph-loop-implementation.js +0 -404
  160. /package/dist/src/{integrations/diffmem/types.js → core/storage/cloud-sync-types.js} +0 -0
  161. /package/dist/src/{integrations/greptile → features/operator}/types.js +0 -0
  162. /package/dist/src/{integrations/ralph/types.js → features/web/client/next-env.d.js} +0 -0
@@ -0,0 +1,320 @@
1
+ import { fileURLToPath as __fileURLToPath } from 'url';
2
+ import { dirname as __pathDirname } from 'path';
3
+ const __filename = __fileURLToPath(import.meta.url);
4
+ const __dirname = __pathDirname(__filename);
5
+ import {
6
+ existsSync,
7
+ readFileSync,
8
+ writeFileSync,
9
+ appendFileSync,
10
+ mkdirSync
11
+ } from "fs";
12
+ import { join } from "path";
13
+ import { homedir } from "os";
14
+ const SM_DIR = join(homedir(), ".stackmemory");
15
+ const DP_DIR = join(SM_DIR, "desire-paths");
16
+ const STREAM_FILE = join(DP_DIR, "research-stream.jsonl");
17
+ const DIGEST_FILE = join(DP_DIR, "research-digest.json");
18
+ const HN_TOP_URL = "https://hacker-news.firebaseio.com/v0/topstories.json";
19
+ const HN_ITEM_URL = "https://hacker-news.firebaseio.com/v0/item";
20
+ const GH_SEARCH_URL = "https://api.github.com/search/repositories";
21
+ const RATE_LIMIT_MS = 1100;
22
+ function sleep(ms) {
23
+ return new Promise((resolve) => setTimeout(resolve, ms));
24
+ }
25
+ function isoWeek(date) {
26
+ const d = new Date(date.getTime());
27
+ d.setHours(0, 0, 0, 0);
28
+ d.setDate(d.getDate() + 3 - (d.getDay() + 6) % 7);
29
+ const week1 = new Date(d.getFullYear(), 0, 4);
30
+ const weekNum = 1 + Math.round(
31
+ ((d.getTime() - week1.getTime()) / 864e5 - 3 + (week1.getDay() + 6) % 7) / 7
32
+ );
33
+ return `${d.getFullYear()}-W${String(weekNum).padStart(2, "0")}`;
34
+ }
35
+ function scoreRelevance(title, keywords) {
36
+ const lower = title.toLowerCase();
37
+ const matched = [];
38
+ for (const kw of keywords) {
39
+ if (lower.includes(kw.toLowerCase())) {
40
+ matched.push(kw);
41
+ }
42
+ }
43
+ if (matched.length === 0) return { score: 0, matched: [] };
44
+ const score = Math.min(1, 0.3 + matched.length * 0.2);
45
+ return { score, matched };
46
+ }
47
+ async function safeFetch(url, timeoutMs = 1e4) {
48
+ try {
49
+ const controller = new AbortController();
50
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
51
+ const res = await fetch(url, {
52
+ signal: controller.signal,
53
+ headers: { "User-Agent": "StackMemory-ResearchStream/1.0" }
54
+ });
55
+ clearTimeout(timer);
56
+ if (!res.ok) return null;
57
+ return await res.json();
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+ class ResearchStreamService {
63
+ config;
64
+ state;
65
+ intervalId;
66
+ isRunning = false;
67
+ onLog;
68
+ constructor(config, onLog) {
69
+ this.config = config;
70
+ this.onLog = onLog;
71
+ this.state = {
72
+ lastScanTime: 0,
73
+ signalsCollected: 0,
74
+ digestsGenerated: 0,
75
+ errors: []
76
+ };
77
+ }
78
+ isOptedOut() {
79
+ if (process.env.STACKMEMORY_RESEARCH_STREAM === "0" || process.env.STACKMEMORY_RESEARCH_STREAM === "false") {
80
+ return true;
81
+ }
82
+ return !this.config.enabled;
83
+ }
84
+ // ─── Source: Hacker News ──────────────────────────────────
85
+ async fetchHackerNews() {
86
+ const signals = [];
87
+ const topIds = await safeFetch(HN_TOP_URL);
88
+ if (!topIds || !Array.isArray(topIds)) {
89
+ this.onLog("WARN", "HN top stories fetch failed");
90
+ return signals;
91
+ }
92
+ const ids = topIds.slice(0, 10);
93
+ for (const id of ids) {
94
+ await sleep(200);
95
+ const item = await safeFetch(`${HN_ITEM_URL}/${id}.json`);
96
+ if (!item || !item.title) continue;
97
+ const { score: relevance, matched } = scoreRelevance(
98
+ item.title,
99
+ this.config.keywords
100
+ );
101
+ if (relevance === 0) continue;
102
+ signals.push({
103
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
104
+ source: "hackernews",
105
+ type: "trending",
106
+ title: item.title,
107
+ url: item.url || `https://news.ycombinator.com/item?id=${id}`,
108
+ score: item.score || 0,
109
+ keywords_matched: matched,
110
+ relevance
111
+ });
112
+ }
113
+ return signals;
114
+ }
115
+ // ─── Source: GitHub Trending ───────────────────────────────
116
+ async fetchGitHubTrending() {
117
+ const signals = [];
118
+ const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3);
119
+ const dateStr = sevenDaysAgo.toISOString().split("T")[0];
120
+ const url = `${GH_SEARCH_URL}?q=created:>${dateStr}&sort=stars&order=desc&per_page=10`;
121
+ await sleep(RATE_LIMIT_MS);
122
+ const data = await safeFetch(url);
123
+ if (!data || !data.items) {
124
+ this.onLog("WARN", "GitHub trending fetch failed");
125
+ return signals;
126
+ }
127
+ for (const repo of data.items) {
128
+ const text = `${repo.full_name || ""} ${repo.description || ""}`;
129
+ const { score: relevance, matched } = scoreRelevance(
130
+ text,
131
+ this.config.keywords
132
+ );
133
+ if (relevance === 0) continue;
134
+ signals.push({
135
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
136
+ source: "github",
137
+ type: "new_repo",
138
+ title: `${repo.full_name}: ${(repo.description || "").slice(0, 120)}`,
139
+ url: repo.html_url || "",
140
+ score: repo.stargazers_count || 0,
141
+ keywords_matched: matched,
142
+ relevance
143
+ });
144
+ }
145
+ return signals;
146
+ }
147
+ // ─── Source: Product Hunt (placeholder) ────────────────────
148
+ async fetchProductHunt() {
149
+ this.onLog("DEBUG", "Product Hunt source unavailable (no API key)");
150
+ return [];
151
+ }
152
+ // ─── Core Scan ────────────────────────────────────────────
153
+ async runScan() {
154
+ try {
155
+ mkdirSync(DP_DIR, { recursive: true });
156
+ const [hnSignals, ghSignals, phSignals] = await Promise.all([
157
+ this.fetchHackerNews(),
158
+ this.fetchGitHubTrending(),
159
+ this.fetchProductHunt()
160
+ ]);
161
+ const allSignals = [...hnSignals, ...ghSignals, ...phSignals];
162
+ allSignals.sort((a, b) => b.relevance - a.relevance || b.score - a.score);
163
+ const capped = allSignals.slice(0, this.config.maxSignalsPerScan);
164
+ const existingUrls = this.loadExistingUrls();
165
+ const newSignals = capped.filter((s) => !existingUrls.has(s.url));
166
+ if (newSignals.length > 0) {
167
+ const lines = newSignals.map((s) => JSON.stringify(s)).join("\n") + "\n";
168
+ appendFileSync(STREAM_FILE, lines, "utf-8");
169
+ this.state.signalsCollected += newSignals.length;
170
+ }
171
+ this.state.lastScanTime = Date.now();
172
+ this.onLog("INFO", "Research scan complete", {
173
+ hn: hnSignals.length,
174
+ gh: ghSignals.length,
175
+ ph: phSignals.length,
176
+ new: newSignals.length,
177
+ total: this.state.signalsCollected
178
+ });
179
+ this.updateDigest();
180
+ } catch (err) {
181
+ this.addError(String(err));
182
+ this.onLog("ERROR", "Research scan failed", { error: String(err) });
183
+ }
184
+ }
185
+ /** Load existing URLs from the stream file for dedup. */
186
+ loadExistingUrls() {
187
+ const urls = /* @__PURE__ */ new Set();
188
+ try {
189
+ if (!existsSync(STREAM_FILE)) return urls;
190
+ const lines = readFileSync(STREAM_FILE, "utf-8").trim().split("\n");
191
+ for (const line of lines) {
192
+ try {
193
+ const entry = JSON.parse(line);
194
+ if (entry.url) urls.add(entry.url);
195
+ } catch {
196
+ }
197
+ }
198
+ } catch {
199
+ }
200
+ return urls;
201
+ }
202
+ // ─── Weekly Digest ────────────────────────────────────────
203
+ updateDigest() {
204
+ try {
205
+ if (!existsSync(STREAM_FILE)) return;
206
+ const lines = readFileSync(STREAM_FILE, "utf-8").trim().split("\n");
207
+ const now = /* @__PURE__ */ new Date();
208
+ const currentWeek = isoWeek(now);
209
+ const weekStart = Date.now() - 7 * 24 * 60 * 60 * 1e3;
210
+ const weekSignals = [];
211
+ for (const line of lines) {
212
+ try {
213
+ const entry = JSON.parse(line);
214
+ if (new Date(entry.ts).getTime() >= weekStart) {
215
+ weekSignals.push(entry);
216
+ }
217
+ } catch {
218
+ }
219
+ }
220
+ if (weekSignals.length === 0) return;
221
+ weekSignals.sort(
222
+ (a, b) => b.relevance - a.relevance || b.score - a.score
223
+ );
224
+ const topSignals = weekSignals.slice(0, 20);
225
+ const keywordCounts = /* @__PURE__ */ new Map();
226
+ for (const signal of topSignals) {
227
+ for (const kw of signal.keywords_matched) {
228
+ keywordCounts.set(kw, (keywordCounts.get(kw) || 0) + 1);
229
+ }
230
+ }
231
+ const themes = [...keywordCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([kw, count]) => `${kw} (${count} signals)`);
232
+ const digest = {
233
+ week: currentWeek,
234
+ signals: topSignals,
235
+ themes,
236
+ generated_at: now.toISOString()
237
+ };
238
+ writeFileSync(DIGEST_FILE, JSON.stringify(digest, null, 2), "utf-8");
239
+ this.state.digestsGenerated++;
240
+ this.onLog("INFO", "Research digest updated", {
241
+ week: currentWeek,
242
+ signals: topSignals.length,
243
+ themes: themes.length
244
+ });
245
+ } catch (err) {
246
+ this.addError(String(err));
247
+ }
248
+ }
249
+ // ─── Lifecycle ────────────────────────────────────────────
250
+ start() {
251
+ if (this.isRunning || this.isOptedOut()) {
252
+ if (this.isOptedOut()) {
253
+ this.onLog("INFO", "Research stream disabled");
254
+ }
255
+ return;
256
+ }
257
+ this.isRunning = true;
258
+ mkdirSync(DP_DIR, { recursive: true });
259
+ const intervalMs = (this.config.interval || 360) * 60 * 1e3;
260
+ this.onLog("INFO", "Research stream service started", {
261
+ interval_min: this.config.interval,
262
+ keywords: this.config.keywords.length
263
+ });
264
+ setTimeout(() => {
265
+ if (!this.isRunning) return;
266
+ this.runScan();
267
+ }, 6e4);
268
+ this.intervalId = setInterval(() => {
269
+ if (!this.isRunning) return;
270
+ this.runScan();
271
+ }, intervalMs);
272
+ if (this.intervalId.unref) this.intervalId.unref();
273
+ }
274
+ stop() {
275
+ if (this.intervalId) {
276
+ clearInterval(this.intervalId);
277
+ this.intervalId = void 0;
278
+ }
279
+ this.isRunning = false;
280
+ }
281
+ getState() {
282
+ return { ...this.state };
283
+ }
284
+ /** Manually trigger a scan (for CLI/MCP). */
285
+ async triggerScan() {
286
+ const before = this.state.signalsCollected;
287
+ await this.runScan();
288
+ try {
289
+ if (!existsSync(STREAM_FILE)) return [];
290
+ const lines = readFileSync(STREAM_FILE, "utf-8").trim().split("\n");
291
+ return lines.slice(-(this.state.signalsCollected - before)).map((l) => {
292
+ try {
293
+ return JSON.parse(l);
294
+ } catch {
295
+ return null;
296
+ }
297
+ }).filter(Boolean);
298
+ } catch {
299
+ return [];
300
+ }
301
+ }
302
+ /** Get the latest digest. */
303
+ getDigest() {
304
+ try {
305
+ if (!existsSync(DIGEST_FILE)) return null;
306
+ return JSON.parse(readFileSync(DIGEST_FILE, "utf-8"));
307
+ } catch {
308
+ return null;
309
+ }
310
+ }
311
+ addError(err) {
312
+ this.state.errors.push(err);
313
+ if (this.state.errors.length > 10) {
314
+ this.state.errors = this.state.errors.slice(-10);
315
+ }
316
+ }
317
+ }
318
+ export {
319
+ ResearchStreamService
320
+ };
@@ -0,0 +1,192 @@
1
+ import { fileURLToPath as __fileURLToPath } from 'url';
2
+ import { dirname as __pathDirname } from 'path';
3
+ const __filename = __fileURLToPath(import.meta.url);
4
+ const __dirname = __pathDirname(__filename);
5
+ import {
6
+ existsSync,
7
+ readFileSync,
8
+ writeFileSync,
9
+ readdirSync,
10
+ statSync,
11
+ mkdirSync
12
+ } from "fs";
13
+ import { join, dirname } from "path";
14
+ import { homedir, platform } from "os";
15
+ import { randomBytes } from "crypto";
16
+ const SM_DIR = join(homedir(), ".stackmemory");
17
+ const INSTANCE_ID_FILE = join(SM_DIR, "instance-id");
18
+ const TELEMETRY_FILE = join(SM_DIR, "telemetry.json");
19
+ const SESSIONS_DIR = join(SM_DIR, "sessions");
20
+ const STALE_MS = 10 * 60 * 1e3;
21
+ class DaemonTelemetryService {
22
+ config;
23
+ state;
24
+ intervalId;
25
+ isRunning = false;
26
+ onLog;
27
+ getDaemonState;
28
+ constructor(config, onLog, getDaemonState) {
29
+ this.config = config;
30
+ this.onLog = onLog;
31
+ this.getDaemonState = getDaemonState;
32
+ this.state = { lastSnapshotTime: 0, snapshotCount: 0, errors: [] };
33
+ }
34
+ isOptedOut() {
35
+ if (process.env.STACKMEMORY_TELEMETRY === "0" || process.env.STACKMEMORY_TELEMETRY === "false") {
36
+ return true;
37
+ }
38
+ return !this.config.enabled;
39
+ }
40
+ getInstanceId() {
41
+ try {
42
+ if (existsSync(INSTANCE_ID_FILE)) {
43
+ return readFileSync(INSTANCE_ID_FILE, "utf-8").trim();
44
+ }
45
+ } catch {
46
+ }
47
+ const id = randomBytes(16).toString("hex");
48
+ try {
49
+ writeFileSync(INSTANCE_ID_FILE, id, "utf-8");
50
+ } catch {
51
+ }
52
+ return id;
53
+ }
54
+ countSessions() {
55
+ try {
56
+ if (!existsSync(SESSIONS_DIR))
57
+ return { total_heartbeats: 0, active_now: 0 };
58
+ const files = readdirSync(SESSIONS_DIR).filter(
59
+ (f) => f.endsWith(".heartbeat")
60
+ );
61
+ const now = Date.now();
62
+ let active = 0;
63
+ for (const file of files) {
64
+ try {
65
+ const stat = statSync(join(SESSIONS_DIR, file));
66
+ if (now - stat.mtimeMs < STALE_MS) active++;
67
+ } catch {
68
+ }
69
+ }
70
+ return { total_heartbeats: files.length, active_now: active };
71
+ } catch {
72
+ return { total_heartbeats: 0, active_now: 0 };
73
+ }
74
+ }
75
+ countSkillAudit() {
76
+ try {
77
+ const auditPath = join(SM_DIR, "skill-audit.jsonl");
78
+ if (!existsSync(auditPath)) return 0;
79
+ return readFileSync(auditPath, "utf-8").trim().split("\n").length;
80
+ } catch {
81
+ return 0;
82
+ }
83
+ }
84
+ countHandoffs() {
85
+ try {
86
+ const handoffsDir = join(SM_DIR, "handoffs");
87
+ if (!existsSync(handoffsDir)) return 0;
88
+ return readdirSync(handoffsDir).filter((f) => f.endsWith(".md")).length;
89
+ } catch {
90
+ return 0;
91
+ }
92
+ }
93
+ collect() {
94
+ if (this.isOptedOut()) return { opted_out: true };
95
+ const daemonState = this.getDaemonState?.();
96
+ const sessions = this.countSessions();
97
+ return {
98
+ instance_id: this.getInstanceId(),
99
+ collected_at: (/* @__PURE__ */ new Date()).toISOString(),
100
+ platform: platform(),
101
+ node_version: process.version,
102
+ daemon: daemonState ? {
103
+ uptime_s: Math.round((daemonState.uptime || 0) / 1e3),
104
+ context_saves: daemonState.services?.context?.saveCount || 0,
105
+ memory_triggers: daemonState.services?.memory?.triggerCount || 0,
106
+ ram_percent: Math.round(
107
+ (daemonState.services?.memory?.currentRamPercent || 0) * 100
108
+ ),
109
+ errors: (daemonState.errors || []).length
110
+ } : null,
111
+ sessions,
112
+ skills: { audit_entries: this.countSkillAudit() },
113
+ handoffs: { total: this.countHandoffs() }
114
+ };
115
+ }
116
+ save() {
117
+ const snapshot = this.collect();
118
+ if ("opted_out" in snapshot) return null;
119
+ let history = [];
120
+ try {
121
+ if (existsSync(TELEMETRY_FILE)) {
122
+ const data = JSON.parse(readFileSync(TELEMETRY_FILE, "utf-8"));
123
+ history = Array.isArray(data.snapshots) ? data.snapshots : [];
124
+ }
125
+ } catch {
126
+ history = [];
127
+ }
128
+ history.push(snapshot);
129
+ const max = this.config.maxSnapshots || 90;
130
+ if (history.length > max) history = history.slice(-max);
131
+ try {
132
+ const dir = dirname(TELEMETRY_FILE);
133
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
134
+ writeFileSync(
135
+ TELEMETRY_FILE,
136
+ JSON.stringify({ version: 1, snapshots: history }, null, 2),
137
+ "utf-8"
138
+ );
139
+ } catch (err) {
140
+ this.state.errors.push(String(err));
141
+ if (this.state.errors.length > 5)
142
+ this.state.errors = this.state.errors.slice(-5);
143
+ this.onLog("ERROR", "Failed to save telemetry", { error: String(err) });
144
+ return null;
145
+ }
146
+ this.state.lastSnapshotTime = Date.now();
147
+ this.state.snapshotCount++;
148
+ return snapshot;
149
+ }
150
+ start() {
151
+ if (this.isRunning || this.isOptedOut()) {
152
+ if (this.isOptedOut()) {
153
+ this.onLog("INFO", "Telemetry disabled \u2014 opt-out active");
154
+ }
155
+ return;
156
+ }
157
+ this.isRunning = true;
158
+ const intervalMs = (this.config.interval || 1440) * 60 * 1e3;
159
+ this.onLog("INFO", "Telemetry service started", {
160
+ interval_min: this.config.interval
161
+ });
162
+ setTimeout(() => {
163
+ if (!this.isRunning) return;
164
+ const snap = this.save();
165
+ if (snap)
166
+ this.onLog("INFO", "Telemetry snapshot saved", {
167
+ sessions: snap.sessions.active_now
168
+ });
169
+ }, 3e4);
170
+ this.intervalId = setInterval(() => {
171
+ const snap = this.save();
172
+ if (snap)
173
+ this.onLog("INFO", "Telemetry snapshot saved", {
174
+ sessions: snap.sessions.active_now
175
+ });
176
+ }, intervalMs);
177
+ if (this.intervalId.unref) this.intervalId.unref();
178
+ }
179
+ stop() {
180
+ if (this.intervalId) {
181
+ clearInterval(this.intervalId);
182
+ this.intervalId = void 0;
183
+ }
184
+ this.isRunning = false;
185
+ }
186
+ getState() {
187
+ return { ...this.state };
188
+ }
189
+ }
190
+ export {
191
+ DaemonTelemetryService
192
+ };
@@ -21,6 +21,9 @@ import { DaemonLinearService } from "./services/linear-service.js";
21
21
  import { DaemonGitHubService } from "./services/github-service.js";
22
22
  import { DaemonMaintenanceService } from "./services/maintenance-service.js";
23
23
  import { DaemonMemoryService } from "./services/memory-service.js";
24
+ import { DaemonTelemetryService } from "./services/telemetry-service.js";
25
+ import { DaemonDesirePathService } from "./services/desire-path-service.js";
26
+ import { ResearchStreamService } from "./services/research-stream-service.js";
24
27
  class UnifiedDaemon {
25
28
  config;
26
29
  paths;
@@ -29,6 +32,9 @@ class UnifiedDaemon {
29
32
  githubService;
30
33
  maintenanceService;
31
34
  memoryService;
35
+ telemetryService;
36
+ desirePathService;
37
+ researchStreamService;
32
38
  heartbeatInterval;
33
39
  isShuttingDown = false;
34
40
  startTime = 0;
@@ -55,6 +61,19 @@ class UnifiedDaemon {
55
61
  this.config.memory,
56
62
  (level, msg, data) => this.log(level, "memory", msg, data)
57
63
  );
64
+ this.telemetryService = new DaemonTelemetryService(
65
+ this.config.telemetry,
66
+ (level, msg, data) => this.log(level, "telemetry", msg, data),
67
+ () => this.getStatus()
68
+ );
69
+ this.desirePathService = new DaemonDesirePathService(
70
+ this.config.desirePaths,
71
+ (level, msg, data) => this.log(level, "desire-paths", msg, data)
72
+ );
73
+ this.researchStreamService = new ResearchStreamService(
74
+ this.config.researchStream,
75
+ (level, msg, data) => this.log(level, "research-stream", msg, data)
76
+ );
58
77
  }
59
78
  log(level, service, message, data) {
60
79
  const logLevel = level.toUpperCase();
@@ -234,7 +253,9 @@ class UnifiedDaemon {
234
253
  linearSyncs: this.linearService.getState().syncCount,
235
254
  githubSyncs: this.githubService.getState().syncCount,
236
255
  maintenanceRuns: this.maintenanceService.getState().ftsRebuilds,
237
- memoryTriggers: this.memoryService.getState().triggerCount
256
+ memoryTriggers: this.memoryService.getState().triggerCount,
257
+ telemetrySnapshots: this.telemetryService.getState().snapshotCount,
258
+ researchSignals: this.researchStreamService.getState().signalsCollected
238
259
  });
239
260
  if (this.heartbeatInterval) {
240
261
  clearInterval(this.heartbeatInterval);
@@ -245,6 +266,9 @@ class UnifiedDaemon {
245
266
  this.githubService.stop();
246
267
  this.maintenanceService.stop();
247
268
  this.memoryService.stop();
269
+ this.telemetryService.stop();
270
+ this.desirePathService.stop();
271
+ this.researchStreamService.stop();
248
272
  this.cleanup();
249
273
  const exitCode = reason === "sigterm" || reason === "sigint" || reason === "sighup" ? 0 : 1;
250
274
  process.exit(exitCode);
@@ -273,6 +297,9 @@ class UnifiedDaemon {
273
297
  await this.githubService.start();
274
298
  this.maintenanceService.start();
275
299
  this.memoryService.start();
300
+ this.telemetryService.start();
301
+ this.desirePathService.start();
302
+ this.researchStreamService.start();
276
303
  this.heartbeatInterval = setInterval(() => {
277
304
  this.updateStatus();
278
305
  }, this.config.heartbeatInterval * 1e3);