@stackmemoryai/stackmemory 1.10.5 → 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 (225) hide show
  1. package/LICENSE +131 -64
  2. package/README.md +107 -24
  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/claude-sm.js +266 -84
  11. package/dist/src/cli/codex-sm.js +225 -33
  12. package/dist/src/cli/commands/bench.js +209 -2
  13. package/dist/src/cli/commands/brain.js +206 -0
  14. package/dist/src/cli/commands/cache.js +126 -0
  15. package/dist/src/cli/commands/company-os.js +184 -0
  16. package/dist/src/cli/commands/context.js +5 -0
  17. package/dist/src/cli/commands/daemon.js +41 -0
  18. package/dist/src/cli/commands/handoff.js +40 -9
  19. package/dist/src/cli/commands/onboard.js +70 -3
  20. package/dist/src/cli/commands/operator.js +127 -0
  21. package/dist/src/cli/commands/optimize.js +117 -0
  22. package/dist/src/cli/commands/orchestrate.js +232 -5
  23. package/dist/src/cli/commands/orchestrator.js +315 -26
  24. package/dist/src/cli/commands/pack.js +322 -0
  25. package/dist/src/cli/commands/patterns.js +254 -0
  26. package/dist/src/cli/commands/portal.js +161 -0
  27. package/dist/src/cli/commands/scaffold.js +92 -0
  28. package/dist/src/cli/commands/search.js +40 -1
  29. package/dist/src/cli/commands/setup.js +178 -11
  30. package/dist/src/cli/commands/skills.js +10 -1
  31. package/dist/src/cli/commands/state.js +265 -0
  32. package/dist/src/cli/commands/sync.js +253 -0
  33. package/dist/src/cli/commands/tasks.js +130 -1
  34. package/dist/src/cli/commands/vision.js +221 -0
  35. package/dist/src/cli/gemini-sm.js +19 -29
  36. package/dist/src/cli/hermes-sm.js +224 -0
  37. package/dist/src/cli/index.js +105 -39
  38. package/dist/src/cli/opencode-sm.js +38 -21
  39. package/dist/src/cli/utils/determinism-watcher.js +66 -0
  40. package/dist/src/cli/utils/real-cli-bin.js +116 -0
  41. package/dist/src/core/brain/brain-store.js +187 -0
  42. package/dist/src/core/brain/brain-sync.js +193 -0
  43. package/dist/src/core/brain/index.js +78 -0
  44. package/dist/src/core/brain/types.js +10 -0
  45. package/dist/src/core/cache/content-cache.js +238 -0
  46. package/dist/src/{integrations/diffmem → core/cache}/index.js +5 -5
  47. package/dist/src/core/cache/token-estimator.js +39 -0
  48. package/dist/src/core/config/feature-flags.js +2 -6
  49. package/dist/src/core/context/frame-database.js +79 -27
  50. package/dist/src/core/context/recursive-context-manager.js +1 -1
  51. package/dist/src/core/context/rehydration.js +2 -1
  52. package/dist/src/core/cross-search/cross-project-search.js +269 -0
  53. package/dist/src/core/cross-search/index.js +10 -0
  54. package/dist/src/core/database/sqlite-adapter.js +14 -84
  55. package/dist/src/core/extensions/provider-adapter.js +5 -0
  56. package/dist/src/core/models/model-router.js +54 -2
  57. package/dist/src/core/models/provider-pricing.js +58 -4
  58. package/dist/src/core/monitoring/logger.js +2 -1
  59. package/dist/src/core/optimization/trace-optimizer.js +413 -0
  60. package/dist/src/core/patterns/index.js +22 -0
  61. package/dist/src/core/patterns/pattern-applier.js +39 -0
  62. package/dist/src/core/patterns/pattern-observer.js +157 -0
  63. package/dist/src/core/patterns/pattern-store.js +259 -0
  64. package/dist/src/core/patterns/types.js +19 -0
  65. package/dist/src/core/provenance/confidence-scorer.js +128 -0
  66. package/dist/src/core/provenance/index.js +40 -0
  67. package/dist/src/core/provenance/provenance-store.js +194 -0
  68. package/dist/src/core/provenance/types.js +82 -0
  69. package/dist/src/core/retrieval/llm-context-retrieval.js +5 -4
  70. package/dist/src/core/retrieval/unified-context-assembler.js +11 -66
  71. package/dist/src/core/session/project-handoff.js +64 -0
  72. package/dist/src/core/session/session-manager.js +28 -0
  73. package/dist/src/core/shared-state/canonical-store.js +564 -0
  74. package/dist/src/core/skill-packs/index.js +18 -0
  75. package/dist/src/core/skill-packs/parser.js +42 -0
  76. package/dist/src/core/skill-packs/registry.js +224 -0
  77. package/dist/src/core/skill-packs/types.js +79 -0
  78. package/dist/src/core/storage/cloud-sync-manager.js +116 -0
  79. package/dist/src/core/storage/cloud-sync.js +574 -0
  80. package/dist/src/core/storage/two-tier-storage.js +5 -1
  81. package/dist/src/core/tasks/master-tasks-template.js +43 -0
  82. package/dist/src/core/tasks/md-task-parser.js +138 -0
  83. package/dist/src/core/trace/trace-event-store.js +282 -0
  84. package/dist/src/core/vision/index.js +27 -0
  85. package/dist/src/core/vision/signals.js +79 -0
  86. package/dist/src/core/vision/types.js +22 -0
  87. package/dist/src/core/vision/vision-file.js +146 -0
  88. package/dist/src/core/vision/vision-loop.js +220 -0
  89. package/dist/src/core/wiki/wiki-compiler.js +103 -1
  90. package/dist/src/daemon/daemon-config.js +52 -0
  91. package/dist/src/daemon/services/desire-path-service.js +566 -0
  92. package/dist/src/daemon/services/github-service.js +126 -0
  93. package/dist/src/daemon/services/research-stream-service.js +320 -0
  94. package/dist/src/daemon/services/telemetry-service.js +192 -0
  95. package/dist/src/daemon/unified-daemon.js +58 -1
  96. package/dist/src/features/browser/cli-browser-agent.js +417 -0
  97. package/dist/src/features/browser/stagehand-workflows.js +578 -0
  98. package/dist/src/features/operator/adapter-factory.js +62 -0
  99. package/dist/src/features/operator/browser-adapter.js +109 -0
  100. package/dist/src/features/operator/desktop-adapter.js +125 -0
  101. package/dist/src/features/operator/index.js +39 -0
  102. package/dist/src/features/operator/llm-decision.js +137 -0
  103. package/dist/src/features/operator/operator-logger.js +92 -0
  104. package/dist/src/features/operator/overnight-runner.js +327 -0
  105. package/dist/src/features/operator/screen-adapter.js +91 -0
  106. package/dist/src/features/operator/session-manager.js +127 -0
  107. package/dist/src/features/operator/state-machine.js +227 -0
  108. package/dist/src/features/operator/task-queue.js +81 -0
  109. package/dist/src/features/portal/index.js +26 -0
  110. package/dist/src/features/portal/server.js +240 -0
  111. package/dist/src/features/portal/types.js +14 -0
  112. package/dist/src/features/portal/ui.js +195 -0
  113. package/dist/src/features/sweep/pty-wrapper.js +13 -5
  114. package/dist/src/features/tasks/task-aware-context.js +2 -1
  115. package/dist/src/features/tui/simple-monitor.js +0 -23
  116. package/dist/src/features/tui/swarm-monitor.js +8 -66
  117. package/dist/src/features/web/client/hooks/use-socket.js +12 -0
  118. package/dist/src/{core/merge/index.js → features/web/client/lib/utils.js} +8 -4
  119. package/dist/src/features/web/client/next-env.d.js +4 -0
  120. package/dist/src/features/web/client/stores/session-store.js +12 -0
  121. package/dist/src/features/web/server/gcp-billing.js +76 -0
  122. package/dist/src/features/web/server/index.js +10 -0
  123. package/dist/src/features/web/server/spend-calculator.js +228 -0
  124. package/dist/src/hooks/schemas.js +6 -1
  125. package/dist/src/integrations/anthropic/client.js +3 -2
  126. package/dist/src/integrations/claude-code/agent-bridge.js +0 -3
  127. package/dist/src/integrations/claude-code/subagent-client.js +307 -11
  128. package/dist/src/integrations/claude-code/task-coordinator.js +2 -1
  129. package/dist/src/integrations/github/pr-state.js +158 -0
  130. package/dist/src/integrations/linear/client.js +4 -1
  131. package/dist/src/integrations/linear/webhook-retry.js +196 -0
  132. package/dist/src/integrations/linear/webhook-server.js +18 -22
  133. package/dist/src/integrations/mcp/handlers/cloud-sync-handlers.js +101 -0
  134. package/dist/src/integrations/mcp/handlers/index.js +40 -84
  135. package/dist/src/integrations/mcp/server.js +542 -641
  136. package/dist/src/integrations/mcp/tool-alias-registry.js +297 -0
  137. package/dist/src/integrations/mcp/tool-definitions.js +152 -682
  138. package/dist/src/mcp/stackmemory-mcp-server.js +571 -231
  139. package/dist/src/orchestrators/multimodal/determinism.js +244 -0
  140. package/dist/src/orchestrators/multimodal/harness.js +149 -78
  141. package/dist/src/orchestrators/multimodal/providers.js +44 -3
  142. package/dist/src/skills/recursive-agent-orchestrator.js +2 -4
  143. package/dist/src/utils/hook-installer.js +0 -8
  144. package/dist/src/utils/process-cleanup.js +1 -7
  145. package/docs/README.md +42 -0
  146. package/docs/guides/README_INSTALL.md +208 -0
  147. package/package.json +27 -9
  148. package/packs/coding/python-fastapi/instructions.md +60 -0
  149. package/packs/coding/python-fastapi/pack.yaml +28 -0
  150. package/packs/coding/typescript-react/instructions.md +47 -0
  151. package/packs/coding/typescript-react/pack.yaml +28 -0
  152. package/packs/core/commands/capture.md +32 -0
  153. package/packs/core/commands/learn.md +73 -0
  154. package/packs/core/commands/next.md +36 -0
  155. package/packs/core/commands/restart.md +58 -0
  156. package/packs/core/commands/restore.md +29 -0
  157. package/packs/core/commands/start.md +57 -0
  158. package/packs/core/commands/stop.md +65 -0
  159. package/packs/core/commands/summary.md +40 -0
  160. package/packs/core/manifest.json +24 -0
  161. package/packs/ops/decision-recovery/instructions.md +65 -0
  162. package/packs/ops/decision-recovery/pack.yaml +89 -0
  163. package/scripts/claude-code-wrapper.sh +11 -0
  164. package/scripts/claude-sm-setup.sh +12 -1
  165. package/scripts/codex-wrapper.sh +11 -0
  166. package/scripts/git-hooks/branch-context-manager.sh +11 -0
  167. package/scripts/git-hooks/post-checkout-stackmemory.sh +11 -0
  168. package/scripts/git-hooks/post-commit-stackmemory.sh +11 -0
  169. package/scripts/git-hooks/pre-commit-stackmemory.sh +11 -0
  170. package/scripts/hooks/cleanup-shell.sh +12 -1
  171. package/scripts/hooks/task-complete.sh +12 -1
  172. package/scripts/install-code-execution-hooks.sh +12 -1
  173. package/scripts/install-sweep-hook.sh +12 -0
  174. package/scripts/install.sh +11 -0
  175. package/scripts/opencode-wrapper.sh +11 -0
  176. package/scripts/portal/cloud-init.yaml +69 -0
  177. package/scripts/portal/setup.sh +69 -0
  178. package/scripts/portal/stackmemory-portal.service +34 -0
  179. package/scripts/setup-claude-integration.sh +12 -1
  180. package/scripts/smoke-init-db.sh +23 -0
  181. package/scripts/stackmemory-daemon.sh +11 -0
  182. package/scripts/verify-dist.cjs +11 -4
  183. package/dist/src/cli/commands/ralph.js +0 -1053
  184. package/dist/src/cli/commands/team.js +0 -168
  185. package/dist/src/core/context/shared-context-layer.js +0 -620
  186. package/dist/src/core/context/stack-merge-resolver.js +0 -748
  187. package/dist/src/core/merge/conflict-detector.js +0 -430
  188. package/dist/src/core/merge/resolution-engine.js +0 -557
  189. package/dist/src/core/merge/stack-diff.js +0 -531
  190. package/dist/src/core/merge/unified-merge-resolver.js +0 -302
  191. package/dist/src/hooks/diffmem-hooks.js +0 -376
  192. package/dist/src/integrations/diffmem/client.js +0 -208
  193. package/dist/src/integrations/diffmem/config.js +0 -14
  194. package/dist/src/integrations/greptile/client.js +0 -101
  195. package/dist/src/integrations/greptile/config.js +0 -14
  196. package/dist/src/integrations/greptile/index.js +0 -11
  197. package/dist/src/integrations/mcp/handlers/cord-handlers.js +0 -397
  198. package/dist/src/integrations/mcp/handlers/diffmem-handlers.js +0 -455
  199. package/dist/src/integrations/mcp/handlers/greptile-handlers.js +0 -456
  200. package/dist/src/integrations/mcp/handlers/provider-handlers.js +0 -227
  201. package/dist/src/integrations/mcp/handlers/team-handlers.js +0 -211
  202. package/dist/src/integrations/ralph/bridge/ralph-stackmemory-bridge.js +0 -863
  203. package/dist/src/integrations/ralph/context/context-budget-manager.js +0 -308
  204. package/dist/src/integrations/ralph/context/stackmemory-context-loader.js +0 -391
  205. package/dist/src/integrations/ralph/index.js +0 -17
  206. package/dist/src/integrations/ralph/learning/pattern-learner.js +0 -435
  207. package/dist/src/integrations/ralph/lifecycle/iteration-lifecycle.js +0 -448
  208. package/dist/src/integrations/ralph/loopmax.js +0 -488
  209. package/dist/src/integrations/ralph/monitoring/swarm-dashboard.js +0 -293
  210. package/dist/src/integrations/ralph/monitoring/swarm-registry.js +0 -107
  211. package/dist/src/integrations/ralph/orchestration/multi-loop-orchestrator.js +0 -508
  212. package/dist/src/integrations/ralph/patterns/compounding-engineering-pattern.js +0 -407
  213. package/dist/src/integrations/ralph/patterns/extended-coherence-sessions.js +0 -495
  214. package/dist/src/integrations/ralph/patterns/oracle-worker-pattern.js +0 -387
  215. package/dist/src/integrations/ralph/performance/performance-optimizer.js +0 -357
  216. package/dist/src/integrations/ralph/recovery/crash-recovery.js +0 -461
  217. package/dist/src/integrations/ralph/state/state-reconciler.js +0 -420
  218. package/dist/src/integrations/ralph/swarm/git-workflow-manager.js +0 -444
  219. package/dist/src/integrations/ralph/swarm/swarm-coordinator.js +0 -1007
  220. package/dist/src/integrations/ralph/visualization/ralph-debugger.js +0 -635
  221. package/scripts/ralph-loop-implementation.js +0 -404
  222. /package/dist/src/core/{merge → cache}/types.js +0 -0
  223. /package/dist/src/{integrations/diffmem/types.js → core/storage/cloud-sync-types.js} +0 -0
  224. /package/dist/src/{integrations/greptile/types.js → core/trace/trace-event.js} +0 -0
  225. /package/dist/src/{integrations/ralph → features/operator}/types.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
+ };
@@ -18,15 +18,23 @@ import {
18
18
  } from "./daemon-config.js";
19
19
  import { DaemonContextService } from "./services/context-service.js";
20
20
  import { DaemonLinearService } from "./services/linear-service.js";
21
+ import { DaemonGitHubService } from "./services/github-service.js";
21
22
  import { DaemonMaintenanceService } from "./services/maintenance-service.js";
22
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";
23
27
  class UnifiedDaemon {
24
28
  config;
25
29
  paths;
26
30
  contextService;
27
31
  linearService;
32
+ githubService;
28
33
  maintenanceService;
29
34
  memoryService;
35
+ telemetryService;
36
+ desirePathService;
37
+ researchStreamService;
30
38
  heartbeatInterval;
31
39
  isShuttingDown = false;
32
40
  startTime = 0;
@@ -41,6 +49,10 @@ class UnifiedDaemon {
41
49
  this.config.linear,
42
50
  (level, msg, data) => this.log(level, "linear", msg, data)
43
51
  );
52
+ this.githubService = new DaemonGitHubService(
53
+ this.config.github,
54
+ (level, msg, data) => this.log(level, "github", msg, data)
55
+ );
44
56
  this.maintenanceService = new DaemonMaintenanceService(
45
57
  this.config.maintenance,
46
58
  (level, msg, data) => this.log(level, "maintenance", msg, data)
@@ -49,6 +61,19 @@ class UnifiedDaemon {
49
61
  this.config.memory,
50
62
  (level, msg, data) => this.log(level, "memory", msg, data)
51
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
+ );
52
77
  }
53
78
  log(level, service, message, data) {
54
79
  const logLevel = level.toUpperCase();
@@ -101,6 +126,7 @@ class UnifiedDaemon {
101
126
  updateStatus() {
102
127
  const maintenanceState = this.maintenanceService.getState();
103
128
  const memoryState = this.memoryService.getState();
129
+ const githubState = this.githubService.getState();
104
130
  const status = {
105
131
  running: true,
106
132
  pid: process.pid,
@@ -117,6 +143,12 @@ class UnifiedDaemon {
117
143
  lastRun: this.linearService.getState().lastSyncTime || void 0,
118
144
  syncCount: this.linearService.getState().syncCount
119
145
  },
146
+ github: {
147
+ enabled: this.config.github.enabled,
148
+ lastRun: githubState.lastSyncTime || void 0,
149
+ syncCount: githubState.syncCount,
150
+ lastProjectionState: githubState.lastProjectionState
151
+ },
120
152
  maintenance: {
121
153
  enabled: this.config.maintenance.enabled,
122
154
  lastRun: maintenanceState.lastRunTime || void 0,
@@ -139,6 +171,7 @@ class UnifiedDaemon {
139
171
  errors: [
140
172
  ...this.contextService.getState().errors.slice(-5),
141
173
  ...this.linearService.getState().errors.slice(-5),
174
+ ...githubState.errors.slice(-5),
142
175
  ...maintenanceState.errors.slice(-5),
143
176
  ...memoryState.errors.slice(-5)
144
177
  ]
@@ -190,6 +223,11 @@ class UnifiedDaemon {
190
223
  enabled: false,
191
224
  syncCount: this.linearService.getState().syncCount
192
225
  },
226
+ github: {
227
+ enabled: false,
228
+ syncCount: this.githubService.getState().syncCount,
229
+ lastProjectionState: this.githubService.getState().lastProjectionState
230
+ },
193
231
  maintenance: {
194
232
  enabled: false,
195
233
  staleFramesCleaned: this.maintenanceService.getState().staleFramesCleaned,
@@ -213,8 +251,11 @@ class UnifiedDaemon {
213
251
  uptime: Date.now() - this.startTime,
214
252
  contextSaves: this.contextService.getState().saveCount,
215
253
  linearSyncs: this.linearService.getState().syncCount,
254
+ githubSyncs: this.githubService.getState().syncCount,
216
255
  maintenanceRuns: this.maintenanceService.getState().ftsRebuilds,
217
- memoryTriggers: this.memoryService.getState().triggerCount
256
+ memoryTriggers: this.memoryService.getState().triggerCount,
257
+ telemetrySnapshots: this.telemetryService.getState().snapshotCount,
258
+ researchSignals: this.researchStreamService.getState().signalsCollected
218
259
  });
219
260
  if (this.heartbeatInterval) {
220
261
  clearInterval(this.heartbeatInterval);
@@ -222,8 +263,12 @@ class UnifiedDaemon {
222
263
  }
223
264
  this.contextService.stop();
224
265
  this.linearService.stop();
266
+ this.githubService.stop();
225
267
  this.maintenanceService.stop();
226
268
  this.memoryService.stop();
269
+ this.telemetryService.stop();
270
+ this.desirePathService.stop();
271
+ this.researchStreamService.stop();
227
272
  this.cleanup();
228
273
  const exitCode = reason === "sigterm" || reason === "sigint" || reason === "sighup" ? 0 : 1;
229
274
  process.exit(exitCode);
@@ -241,6 +286,7 @@ class UnifiedDaemon {
241
286
  config: {
242
287
  context: this.config.context.enabled,
243
288
  linear: this.config.linear.enabled,
289
+ github: this.config.github.enabled,
244
290
  maintenance: this.config.maintenance.enabled,
245
291
  memory: this.config.memory.enabled,
246
292
  fileWatch: this.config.fileWatch.enabled
@@ -248,8 +294,12 @@ class UnifiedDaemon {
248
294
  });
249
295
  this.contextService.start();
250
296
  await this.linearService.start();
297
+ await this.githubService.start();
251
298
  this.maintenanceService.start();
252
299
  this.memoryService.start();
300
+ this.telemetryService.start();
301
+ this.desirePathService.start();
302
+ this.researchStreamService.start();
253
303
  this.heartbeatInterval = setInterval(() => {
254
304
  this.updateStatus();
255
305
  }, this.config.heartbeatInterval * 1e3);
@@ -258,6 +308,7 @@ class UnifiedDaemon {
258
308
  getStatus() {
259
309
  const maintenanceState = this.maintenanceService.getState();
260
310
  const memoryState = this.memoryService.getState();
311
+ const githubState = this.githubService.getState();
261
312
  return {
262
313
  running: !this.isShuttingDown,
263
314
  pid: process.pid,
@@ -274,6 +325,12 @@ class UnifiedDaemon {
274
325
  lastRun: this.linearService.getState().lastSyncTime || void 0,
275
326
  syncCount: this.linearService.getState().syncCount
276
327
  },
328
+ github: {
329
+ enabled: this.config.github.enabled,
330
+ lastRun: githubState.lastSyncTime || void 0,
331
+ syncCount: githubState.syncCount,
332
+ lastProjectionState: githubState.lastProjectionState
333
+ },
277
334
  maintenance: {
278
335
  enabled: this.config.maintenance.enabled,
279
336
  lastRun: maintenanceState.lastRunTime || void 0,