@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.
- package/LICENSE +131 -64
- package/README.md +3 -1
- package/bin/claude-sm +16 -1
- package/bin/claude-smd +16 -1
- package/bin/codex-smd +16 -1
- package/bin/gemini-sm +16 -1
- package/bin/hermes-sm +21 -0
- package/bin/hermes-smd +21 -0
- package/bin/opencode-sm +16 -1
- package/dist/src/cli/codex-sm.js +51 -11
- package/dist/src/cli/commands/brain.js +206 -0
- package/dist/src/cli/commands/company-os.js +184 -0
- package/dist/src/cli/commands/context.js +5 -0
- package/dist/src/cli/commands/operator.js +127 -0
- package/dist/src/cli/commands/orchestrate.js +2 -0
- package/dist/src/cli/commands/orchestrator.js +3 -2
- package/dist/src/cli/commands/patterns.js +254 -0
- package/dist/src/cli/commands/portal.js +161 -0
- package/dist/src/cli/commands/scaffold.js +92 -0
- package/dist/src/cli/commands/setup.js +1 -4
- package/dist/src/cli/commands/sync.js +253 -0
- package/dist/src/cli/commands/tasks.js +130 -1
- package/dist/src/cli/commands/vision.js +221 -0
- package/dist/src/cli/hermes-sm.js +224 -0
- package/dist/src/cli/index.js +15 -10
- package/dist/src/cli/utils/real-cli-bin.js +72 -0
- package/dist/src/core/brain/brain-store.js +187 -0
- package/dist/src/core/brain/brain-sync.js +193 -0
- package/dist/src/core/brain/index.js +78 -0
- package/dist/src/core/brain/types.js +10 -0
- package/dist/src/core/cache/token-estimator.js +24 -1
- package/dist/src/core/config/feature-flags.js +2 -6
- package/dist/src/core/context/frame-database.js +44 -0
- package/dist/src/core/context/recursive-context-manager.js +1 -1
- package/dist/src/core/context/rehydration.js +2 -1
- package/dist/src/core/database/sqlite-adapter.js +14 -1
- package/dist/src/core/models/model-router.js +33 -1
- package/dist/src/core/models/provider-pricing.js +58 -4
- package/dist/src/core/patterns/index.js +22 -0
- package/dist/src/core/patterns/pattern-applier.js +39 -0
- package/dist/src/core/patterns/pattern-observer.js +157 -0
- package/dist/src/core/patterns/pattern-store.js +259 -0
- package/dist/src/core/patterns/types.js +19 -0
- package/dist/src/core/retrieval/llm-context-retrieval.js +5 -4
- package/dist/src/core/retrieval/unified-context-assembler.js +11 -66
- package/dist/src/core/skill-packs/types.js +14 -1
- package/dist/src/core/storage/cloud-sync-manager.js +116 -0
- package/dist/src/core/storage/cloud-sync.js +574 -0
- package/dist/src/core/storage/two-tier-storage.js +5 -1
- package/dist/src/core/tasks/master-tasks-template.js +43 -0
- package/dist/src/core/tasks/md-task-parser.js +138 -0
- package/dist/src/core/vision/index.js +27 -0
- package/dist/src/core/vision/signals.js +79 -0
- package/dist/src/core/vision/types.js +22 -0
- package/dist/src/core/vision/vision-file.js +146 -0
- package/dist/src/core/vision/vision-loop.js +220 -0
- package/dist/src/core/wiki/wiki-compiler.js +103 -1
- package/dist/src/daemon/daemon-config.js +45 -0
- package/dist/src/daemon/services/desire-path-service.js +566 -0
- package/dist/src/daemon/services/research-stream-service.js +320 -0
- package/dist/src/daemon/services/telemetry-service.js +192 -0
- package/dist/src/daemon/unified-daemon.js +28 -1
- package/dist/src/features/browser/cli-browser-agent.js +417 -0
- package/dist/src/features/browser/stagehand-workflows.js +578 -0
- package/dist/src/features/operator/adapter-factory.js +62 -0
- package/dist/src/features/operator/browser-adapter.js +109 -0
- package/dist/src/features/operator/desktop-adapter.js +125 -0
- package/dist/src/features/operator/index.js +39 -0
- package/dist/src/features/operator/llm-decision.js +137 -0
- package/dist/src/features/operator/operator-logger.js +92 -0
- package/dist/src/features/operator/overnight-runner.js +327 -0
- package/dist/src/features/operator/screen-adapter.js +91 -0
- package/dist/src/features/operator/session-manager.js +127 -0
- package/dist/src/features/operator/state-machine.js +227 -0
- package/dist/src/features/operator/task-queue.js +81 -0
- package/dist/src/features/portal/index.js +26 -0
- package/dist/src/features/portal/server.js +240 -0
- package/dist/src/features/portal/types.js +14 -0
- package/dist/src/features/portal/ui.js +195 -0
- package/dist/src/features/tasks/task-aware-context.js +2 -1
- package/dist/src/features/tui/simple-monitor.js +0 -23
- package/dist/src/features/tui/swarm-monitor.js +8 -66
- package/dist/src/{integrations/diffmem/index.js → features/web/client/hooks/use-socket.js} +6 -5
- package/dist/src/features/web/client/lib/utils.js +12 -0
- package/dist/src/features/web/client/stores/session-store.js +12 -0
- package/dist/src/features/web/server/gcp-billing.js +76 -0
- package/dist/src/features/web/server/index.js +10 -0
- package/dist/src/features/web/server/spend-calculator.js +228 -0
- package/dist/src/hooks/schemas.js +4 -1
- package/dist/src/integrations/anthropic/client.js +3 -2
- package/dist/src/integrations/claude-code/agent-bridge.js +0 -3
- package/dist/src/integrations/claude-code/subagent-client.js +218 -11
- package/dist/src/integrations/claude-code/task-coordinator.js +2 -1
- package/dist/src/integrations/linear/webhook-retry.js +196 -0
- package/dist/src/integrations/linear/webhook-server.js +18 -22
- package/dist/src/integrations/mcp/handlers/cloud-sync-handlers.js +101 -0
- package/dist/src/integrations/mcp/handlers/index.js +27 -52
- package/dist/src/integrations/mcp/server.js +122 -335
- package/dist/src/integrations/mcp/tool-alias-registry.js +0 -73
- package/dist/src/integrations/mcp/tool-definitions.js +111 -510
- package/dist/src/mcp/stackmemory-mcp-server.js +404 -379
- package/dist/src/orchestrators/multimodal/determinism.js +2 -1
- package/dist/src/orchestrators/multimodal/harness.js +2 -1
- package/dist/src/skills/recursive-agent-orchestrator.js +2 -4
- package/dist/src/utils/process-cleanup.js +1 -7
- package/docs/README.md +42 -0
- package/docs/guides/README_INSTALL.md +208 -0
- package/package.json +18 -9
- package/scripts/claude-code-wrapper.sh +11 -0
- package/scripts/claude-sm-setup.sh +12 -1
- package/scripts/codex-wrapper.sh +11 -0
- package/scripts/git-hooks/branch-context-manager.sh +11 -0
- package/scripts/git-hooks/post-checkout-stackmemory.sh +11 -0
- package/scripts/git-hooks/post-commit-stackmemory.sh +11 -0
- package/scripts/git-hooks/pre-commit-stackmemory.sh +11 -0
- package/scripts/hooks/cleanup-shell.sh +12 -1
- package/scripts/hooks/task-complete.sh +12 -1
- package/scripts/install-code-execution-hooks.sh +12 -1
- package/scripts/install-sweep-hook.sh +12 -0
- package/scripts/install.sh +11 -0
- package/scripts/opencode-wrapper.sh +11 -0
- package/scripts/portal/cloud-init.yaml +69 -0
- package/scripts/portal/setup.sh +69 -0
- package/scripts/portal/stackmemory-portal.service +34 -0
- package/scripts/setup-claude-integration.sh +12 -1
- package/scripts/smoke-init-db.sh +23 -0
- package/scripts/stackmemory-daemon.sh +11 -0
- package/scripts/verify-dist.cjs +11 -4
- package/dist/src/cli/commands/ralph.js +0 -1053
- package/dist/src/hooks/diffmem-hooks.js +0 -376
- package/dist/src/integrations/diffmem/client.js +0 -208
- package/dist/src/integrations/diffmem/config.js +0 -14
- package/dist/src/integrations/greptile/client.js +0 -101
- package/dist/src/integrations/greptile/config.js +0 -14
- package/dist/src/integrations/greptile/index.js +0 -11
- package/dist/src/integrations/mcp/handlers/cross-search-handlers.js +0 -188
- package/dist/src/integrations/mcp/handlers/diffmem-handlers.js +0 -455
- package/dist/src/integrations/mcp/handlers/greptile-handlers.js +0 -456
- package/dist/src/integrations/mcp/handlers/provider-handlers.js +0 -227
- package/dist/src/integrations/ralph/bridge/ralph-stackmemory-bridge.js +0 -863
- package/dist/src/integrations/ralph/context/context-budget-manager.js +0 -308
- package/dist/src/integrations/ralph/context/stackmemory-context-loader.js +0 -354
- package/dist/src/integrations/ralph/index.js +0 -17
- package/dist/src/integrations/ralph/learning/pattern-learner.js +0 -416
- package/dist/src/integrations/ralph/lifecycle/iteration-lifecycle.js +0 -448
- package/dist/src/integrations/ralph/loopmax.js +0 -488
- package/dist/src/integrations/ralph/monitoring/swarm-dashboard.js +0 -293
- package/dist/src/integrations/ralph/monitoring/swarm-registry.js +0 -107
- package/dist/src/integrations/ralph/orchestration/multi-loop-orchestrator.js +0 -508
- package/dist/src/integrations/ralph/patterns/compounding-engineering-pattern.js +0 -407
- package/dist/src/integrations/ralph/patterns/extended-coherence-sessions.js +0 -495
- package/dist/src/integrations/ralph/patterns/oracle-worker-pattern.js +0 -387
- package/dist/src/integrations/ralph/performance/performance-optimizer.js +0 -357
- package/dist/src/integrations/ralph/recovery/crash-recovery.js +0 -461
- package/dist/src/integrations/ralph/state/state-reconciler.js +0 -420
- package/dist/src/integrations/ralph/swarm/git-workflow-manager.js +0 -444
- package/dist/src/integrations/ralph/swarm/swarm-coordinator.js +0 -1005
- package/dist/src/integrations/ralph/visualization/ralph-debugger.js +0 -635
- package/scripts/ralph-loop-implementation.js +0 -404
- /package/dist/src/{integrations/diffmem/types.js → core/storage/cloud-sync-types.js} +0 -0
- /package/dist/src/{integrations/greptile → features/operator}/types.js +0 -0
- /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);
|