@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,187 @@
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 { randomUUID } from "crypto";
6
+ import {
7
+ BRAIN_TABLE,
8
+ DEFAULT_BRAIN_LIMIT
9
+ } from "./types.js";
10
+ class BrainStore {
11
+ db;
12
+ workspaceId;
13
+ projectId;
14
+ constructor(db, scope) {
15
+ this.db = db;
16
+ this.projectId = scope.projectId;
17
+ this.workspaceId = scope.workspaceId ?? "";
18
+ this.ensureTable();
19
+ }
20
+ /** Create the brain_entries table + indexes if they don't exist. */
21
+ ensureTable() {
22
+ this.db.exec(`
23
+ CREATE TABLE IF NOT EXISTS ${BRAIN_TABLE} (
24
+ entry_id TEXT PRIMARY KEY,
25
+ workspace_id TEXT NOT NULL DEFAULT '',
26
+ project_id TEXT NOT NULL,
27
+ agent TEXT NOT NULL DEFAULT 'claude',
28
+ kind TEXT NOT NULL DEFAULT 'note',
29
+ title TEXT NOT NULL,
30
+ summary TEXT NOT NULL DEFAULT '',
31
+ conclusion TEXT NOT NULL DEFAULT '',
32
+ tags TEXT NOT NULL DEFAULT '[]',
33
+ refs TEXT NOT NULL DEFAULT '[]',
34
+ confidence REAL NOT NULL DEFAULT 0.7,
35
+ status TEXT NOT NULL DEFAULT 'active',
36
+ superseded_by TEXT,
37
+ created_at INTEGER NOT NULL,
38
+ updated_at INTEGER NOT NULL
39
+ );
40
+ CREATE INDEX IF NOT EXISTS idx_brain_project ON ${BRAIN_TABLE}(project_id, created_at DESC);
41
+ CREATE INDEX IF NOT EXISTS idx_brain_workspace ON ${BRAIN_TABLE}(workspace_id, created_at DESC);
42
+ `);
43
+ }
44
+ /** Record (or upsert by entryId) a brain entry. */
45
+ record(input) {
46
+ const now = Date.now();
47
+ const entry = {
48
+ entryId: input.entryId ?? randomUUID(),
49
+ workspaceId: this.workspaceId,
50
+ projectId: this.projectId,
51
+ agent: input.agent ?? "claude",
52
+ kind: input.kind ?? "note",
53
+ title: input.title,
54
+ summary: input.summary ?? "",
55
+ conclusion: input.conclusion ?? "",
56
+ tags: input.tags ?? [],
57
+ refs: input.refs ?? [],
58
+ confidence: clamp01(input.confidence ?? 0.7),
59
+ status: "active",
60
+ createdAt: input.createdAt ?? now,
61
+ updatedAt: input.updatedAt ?? now
62
+ };
63
+ this.db.prepare(
64
+ `INSERT INTO ${BRAIN_TABLE}
65
+ (entry_id, workspace_id, project_id, agent, kind, title, summary,
66
+ conclusion, tags, refs, confidence, status, superseded_by,
67
+ created_at, updated_at)
68
+ VALUES (@entryId, @workspaceId, @projectId, @agent, @kind, @title,
69
+ @summary, @conclusion, @tags, @refs, @confidence, @status,
70
+ NULL, @createdAt, @updatedAt)
71
+ ON CONFLICT(entry_id) DO UPDATE SET
72
+ agent = excluded.agent,
73
+ kind = excluded.kind,
74
+ title = excluded.title,
75
+ summary = excluded.summary,
76
+ conclusion = excluded.conclusion,
77
+ tags = excluded.tags,
78
+ refs = excluded.refs,
79
+ confidence = excluded.confidence,
80
+ updated_at = excluded.updated_at`
81
+ ).run({
82
+ ...entry,
83
+ tags: JSON.stringify(entry.tags),
84
+ refs: JSON.stringify(entry.refs)
85
+ });
86
+ return entry;
87
+ }
88
+ /** Fetch a single entry by id (or unique prefix). */
89
+ get(entryId) {
90
+ const row = this.db.prepare(
91
+ `SELECT * FROM ${BRAIN_TABLE} WHERE entry_id = ? OR entry_id LIKE ? LIMIT 1`
92
+ ).get(entryId, `${entryId}%`);
93
+ return row ? rowToEntry(row) : null;
94
+ }
95
+ /** Search entries by scope + free text, newest first. */
96
+ recall(query = {}) {
97
+ const where = [];
98
+ const params = [];
99
+ if (query.org) {
100
+ where.push("workspace_id = ?");
101
+ params.push(this.workspaceId);
102
+ } else {
103
+ where.push("project_id = ?");
104
+ params.push(query.projectId ?? this.projectId);
105
+ }
106
+ if (!query.includeSuperseded) {
107
+ where.push("status = 'active'");
108
+ }
109
+ if (query.agent) {
110
+ where.push("agent = ?");
111
+ params.push(query.agent);
112
+ }
113
+ if (query.kind) {
114
+ where.push("kind = ?");
115
+ params.push(query.kind);
116
+ }
117
+ if (query.since) {
118
+ where.push("created_at >= ?");
119
+ params.push(query.since);
120
+ }
121
+ if (query.text) {
122
+ where.push(
123
+ "(title LIKE ? OR summary LIKE ? OR conclusion LIKE ? OR tags LIKE ?)"
124
+ );
125
+ const like = `%${query.text}%`;
126
+ params.push(like, like, like, like);
127
+ }
128
+ const limit = Math.max(1, query.limit ?? DEFAULT_BRAIN_LIMIT);
129
+ const rows = this.db.prepare(
130
+ `SELECT * FROM ${BRAIN_TABLE}
131
+ WHERE ${where.join(" AND ")}
132
+ ORDER BY created_at DESC
133
+ LIMIT ?`
134
+ ).all(...params, limit);
135
+ return rows.map(rowToEntry);
136
+ }
137
+ /** Mark `oldId` superseded by `newId`. */
138
+ supersede(oldId, newId) {
139
+ this.db.prepare(
140
+ `UPDATE ${BRAIN_TABLE}
141
+ SET status = 'superseded', superseded_by = ?, updated_at = ?
142
+ WHERE entry_id = ?`
143
+ ).run(newId, Date.now(), oldId);
144
+ }
145
+ /** Count entries in scope (for status output). */
146
+ count(org = false) {
147
+ const col = org ? "workspace_id" : "project_id";
148
+ const val = org ? this.workspaceId : this.projectId;
149
+ const row = this.db.prepare(`SELECT COUNT(*) AS n FROM ${BRAIN_TABLE} WHERE ${col} = ?`).get(val);
150
+ return row.n;
151
+ }
152
+ }
153
+ function rowToEntry(row) {
154
+ const entry = {
155
+ entryId: row.entry_id,
156
+ workspaceId: row.workspace_id,
157
+ projectId: row.project_id,
158
+ agent: row.agent,
159
+ kind: row.kind,
160
+ title: row.title,
161
+ summary: row.summary,
162
+ conclusion: row.conclusion,
163
+ tags: safeParse(row.tags),
164
+ refs: safeParse(row.refs),
165
+ confidence: row.confidence,
166
+ status: row.status,
167
+ createdAt: row.created_at,
168
+ updatedAt: row.updated_at
169
+ };
170
+ if (row.superseded_by) entry.supersededBy = row.superseded_by;
171
+ return entry;
172
+ }
173
+ function safeParse(json) {
174
+ try {
175
+ const v = JSON.parse(json);
176
+ return Array.isArray(v) ? v.map(String) : [];
177
+ } catch {
178
+ return [];
179
+ }
180
+ }
181
+ function clamp01(n) {
182
+ if (Number.isNaN(n)) return 0.7;
183
+ return Math.max(0, Math.min(1, n));
184
+ }
185
+ export {
186
+ BrainStore
187
+ };
@@ -0,0 +1,193 @@
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
+ const BRAIN_TABLE = "brain_entries";
6
+ class BrainSync {
7
+ db;
8
+ store;
9
+ config;
10
+ constructor(db, store, config) {
11
+ this.db = db;
12
+ this.store = store;
13
+ this.config = {
14
+ timeoutMs: 3e4,
15
+ batchSize: 200,
16
+ ...config
17
+ };
18
+ this.ensureMeta();
19
+ }
20
+ ensureMeta() {
21
+ this.db.exec(`
22
+ CREATE TABLE IF NOT EXISTS brain_sync_meta (
23
+ direction TEXT PRIMARY KEY,
24
+ cursor INTEGER NOT NULL DEFAULT 0
25
+ );
26
+ `);
27
+ }
28
+ getCursor(direction) {
29
+ const row = this.db.prepare("SELECT cursor FROM brain_sync_meta WHERE direction = ?").get(direction);
30
+ return row?.cursor ?? 0;
31
+ }
32
+ setCursor(direction, cursor) {
33
+ this.db.prepare(
34
+ `INSERT INTO brain_sync_meta (direction, cursor) VALUES (?, ?)
35
+ ON CONFLICT(direction) DO UPDATE SET cursor = excluded.cursor`
36
+ ).run(direction, cursor);
37
+ }
38
+ /** Push locally-updated entries to the cloud. */
39
+ async push() {
40
+ const since = this.getCursor("push");
41
+ const rows = this.db.prepare(
42
+ `SELECT * FROM ${BRAIN_TABLE} WHERE updated_at > ? ORDER BY updated_at ASC LIMIT ?`
43
+ ).all(since, this.config.batchSize);
44
+ if (rows.length === 0) {
45
+ return { success: true, pushed: 0, pulled: 0, applied: 0 };
46
+ }
47
+ const entries = rows.map(toWireEntry);
48
+ const maxUpdated = Math.max(...entries.map((e) => e.updatedAt));
49
+ try {
50
+ const res = await this.post("/v1/brain/push", {
51
+ protocolVersion: 1,
52
+ clientId: this.config.clientId,
53
+ workspaceId: this.config.workspaceId,
54
+ projectId: this.config.projectId,
55
+ since,
56
+ entries
57
+ });
58
+ this.setCursor("push", Math.max(maxUpdated, res.serverCursor ?? 0));
59
+ return {
60
+ success: true,
61
+ pushed: res.accepted ?? entries.length,
62
+ pulled: 0,
63
+ applied: 0
64
+ };
65
+ } catch (err) {
66
+ return {
67
+ success: false,
68
+ pushed: 0,
69
+ pulled: 0,
70
+ applied: 0,
71
+ error: errMsg(err)
72
+ };
73
+ }
74
+ }
75
+ /** Pull remote entries and apply them locally (newest-wins). */
76
+ async pull() {
77
+ const since = this.getCursor("pull");
78
+ try {
79
+ const res = await this.post("/v1/brain/pull", {
80
+ protocolVersion: 1,
81
+ clientId: this.config.clientId,
82
+ workspaceId: this.config.workspaceId,
83
+ projectId: this.config.projectId,
84
+ since,
85
+ limit: this.config.batchSize
86
+ });
87
+ const entries = res.entries ?? [];
88
+ let applied = 0;
89
+ let maxUpdated = since;
90
+ for (const remote of entries) {
91
+ maxUpdated = Math.max(maxUpdated, remote.updatedAt ?? 0);
92
+ const local = this.store.get(remote.entryId);
93
+ if (local && local.updatedAt >= (remote.updatedAt ?? 0)) continue;
94
+ this.store.record({
95
+ entryId: remote.entryId,
96
+ agent: remote.agent,
97
+ kind: remote.kind,
98
+ title: remote.title,
99
+ summary: remote.summary,
100
+ conclusion: remote.conclusion,
101
+ tags: remote.tags,
102
+ refs: remote.refs,
103
+ confidence: remote.confidence,
104
+ createdAt: remote.createdAt,
105
+ updatedAt: remote.updatedAt
106
+ });
107
+ applied++;
108
+ }
109
+ this.setCursor("pull", Math.max(maxUpdated, res.serverCursor ?? 0));
110
+ return { success: true, pushed: 0, pulled: entries.length, applied };
111
+ } catch (err) {
112
+ return {
113
+ success: false,
114
+ pushed: 0,
115
+ pulled: 0,
116
+ applied: 0,
117
+ error: errMsg(err)
118
+ };
119
+ }
120
+ }
121
+ /** Push then pull in one shot. */
122
+ async sync() {
123
+ const pushed = await this.push();
124
+ const pulled = await this.pull();
125
+ const error = pushed.error ?? pulled.error;
126
+ return {
127
+ success: pushed.success && pulled.success,
128
+ pushed: pushed.pushed,
129
+ pulled: pulled.pulled,
130
+ applied: pulled.applied,
131
+ ...error ? { error } : {}
132
+ };
133
+ }
134
+ async post(path, body) {
135
+ const controller = new AbortController();
136
+ const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
137
+ try {
138
+ const res = await fetch(`${this.config.endpoint}${path}`, {
139
+ method: "POST",
140
+ headers: {
141
+ "Content-Type": "application/json",
142
+ Authorization: `Bearer ${this.config.apiKey}`,
143
+ "X-Client-Id": this.config.clientId
144
+ },
145
+ body: JSON.stringify(body),
146
+ signal: controller.signal
147
+ });
148
+ if (!res.ok) {
149
+ throw new Error(`${res.status} ${res.statusText}`);
150
+ }
151
+ return await res.json();
152
+ } finally {
153
+ clearTimeout(timer);
154
+ }
155
+ }
156
+ }
157
+ function toWireEntry(row) {
158
+ const parse = (v) => {
159
+ try {
160
+ const a = JSON.parse(String(v ?? "[]"));
161
+ return Array.isArray(a) ? a.map(String) : [];
162
+ } catch {
163
+ return [];
164
+ }
165
+ };
166
+ const entry = {
167
+ entryId: String(row["entry_id"]),
168
+ workspaceId: String(row["workspace_id"] ?? ""),
169
+ projectId: String(row["project_id"]),
170
+ agent: String(row["agent"]),
171
+ kind: String(row["kind"]),
172
+ title: String(row["title"]),
173
+ summary: String(row["summary"] ?? ""),
174
+ conclusion: String(row["conclusion"] ?? ""),
175
+ tags: parse(row["tags"]),
176
+ refs: parse(row["refs"]),
177
+ confidence: Number(row["confidence"] ?? 0.7),
178
+ status: String(row["status"] ?? "active"),
179
+ createdAt: Number(row["created_at"]),
180
+ updatedAt: Number(row["updated_at"])
181
+ };
182
+ if (row["superseded_by"]) entry.supersededBy = String(row["superseded_by"]);
183
+ return entry;
184
+ }
185
+ function errMsg(err) {
186
+ if (err instanceof Error) {
187
+ return err.name === "AbortError" ? "request timed out" : err.message;
188
+ }
189
+ return String(err);
190
+ }
191
+ export {
192
+ BrainSync
193
+ };
@@ -0,0 +1,78 @@
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 { homedir, hostname } from "os";
6
+ import { join } from "path";
7
+ import { existsSync, readFileSync, mkdirSync } from "fs";
8
+ import { createHash } from "crypto";
9
+ import Database from "better-sqlite3";
10
+ import { BrainStore } from "./brain-store.js";
11
+ import { BrainSync } from "./brain-sync.js";
12
+ import { BrainStore as BrainStore2 } from "./brain-store.js";
13
+ import { BrainSync as BrainSync2 } from "./brain-sync.js";
14
+ export * from "./types.js";
15
+ const DEFAULT_ENDPOINT = "https://provenant-api.jpwu03.workers.dev";
16
+ function readAuth() {
17
+ const cfgPath = join(homedir(), ".stackmemory", "config.json");
18
+ if (!existsSync(cfgPath)) return {};
19
+ try {
20
+ const cfg = JSON.parse(readFileSync(cfgPath, "utf8"));
21
+ return cfg.auth ?? {};
22
+ } catch {
23
+ return {};
24
+ }
25
+ }
26
+ function hashId(input) {
27
+ return createHash("sha256").update(input).digest("hex").slice(0, 16);
28
+ }
29
+ function resolveScope(projectDir) {
30
+ const auth = readAuth();
31
+ const projectId = process.env["PROVENANT_PROJECT_ID"] || auth.projectId || hashId(projectDir);
32
+ const workspaceId = process.env["PROVENANT_WORKSPACE_ID"] || auth.workspaceId || "";
33
+ return { projectId, workspaceId };
34
+ }
35
+ function resolveDbPath(projectDir) {
36
+ const contextDb = join(projectDir, ".stackmemory", "context.db");
37
+ if (existsSync(contextDb)) return contextDb;
38
+ const localDb = join(projectDir, ".stackmemory", "stackmemory.db");
39
+ if (existsSync(localDb)) return localDb;
40
+ const globalDb = join(homedir(), ".stackmemory", "stackmemory.db");
41
+ if (existsSync(globalDb)) return globalDb;
42
+ mkdirSync(join(projectDir, ".stackmemory"), { recursive: true });
43
+ return contextDb;
44
+ }
45
+ function openBrain(projectDir = process.cwd()) {
46
+ const { projectId, workspaceId } = resolveScope(projectDir);
47
+ const dbPath = resolveDbPath(projectDir);
48
+ const db = new Database(dbPath);
49
+ const store = new BrainStore(db, { projectId, workspaceId });
50
+ const auth = readAuth();
51
+ const apiKey = process.env["PROVENANT_API_KEY"] || auth.apiKey;
52
+ let sync = null;
53
+ if (apiKey) {
54
+ const syncConfig = {
55
+ endpoint: process.env["PROVENANT_API_URL"] || auth.apiUrl || DEFAULT_ENDPOINT,
56
+ apiKey,
57
+ workspaceId,
58
+ projectId,
59
+ clientId: hashId(hostname() + projectDir)
60
+ };
61
+ sync = new BrainSync(db, store, syncConfig);
62
+ }
63
+ return {
64
+ db,
65
+ store,
66
+ projectId,
67
+ workspaceId,
68
+ sync,
69
+ close: () => db.close()
70
+ };
71
+ }
72
+ export {
73
+ BrainStore2 as BrainStore,
74
+ BrainSync2 as BrainSync,
75
+ openBrain,
76
+ resolveDbPath,
77
+ resolveScope
78
+ };
@@ -0,0 +1,10 @@
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
+ const BRAIN_TABLE = "brain_entries";
6
+ const DEFAULT_BRAIN_LIMIT = 20;
7
+ export {
8
+ BRAIN_TABLE,
9
+ DEFAULT_BRAIN_LIMIT
10
+ };
@@ -0,0 +1,238 @@
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 { logger } from "../monitoring/logger.js";
6
+ import { estimateTokens, hashContent } from "./token-estimator.js";
7
+ class ContentCache {
8
+ db;
9
+ constructor(db) {
10
+ this.db = db;
11
+ this.initializeSchema();
12
+ }
13
+ // ------------------------------------------------------------------
14
+ // Schema
15
+ // ------------------------------------------------------------------
16
+ initializeSchema() {
17
+ this.db.exec(`
18
+ CREATE TABLE IF NOT EXISTS content_cache (
19
+ hash TEXT PRIMARY KEY,
20
+ content TEXT NOT NULL,
21
+ token_count INTEGER NOT NULL,
22
+ hit_count INTEGER NOT NULL DEFAULT 0,
23
+ first_seen INTEGER NOT NULL,
24
+ last_seen INTEGER NOT NULL,
25
+ source TEXT NOT NULL DEFAULT '',
26
+ metadata TEXT
27
+ );
28
+
29
+ CREATE INDEX IF NOT EXISTS idx_cache_source ON content_cache(source);
30
+ CREATE INDEX IF NOT EXISTS idx_cache_last_seen ON content_cache(last_seen);
31
+ `);
32
+ const hasFts = this.db.prepare(
33
+ `SELECT name FROM sqlite_master WHERE type='table' AND name='content_cache_fts'`
34
+ ).get();
35
+ if (!hasFts) {
36
+ this.db.exec(`
37
+ CREATE VIRTUAL TABLE content_cache_fts
38
+ USING fts5(content, hash UNINDEXED, content_rowid='rowid');
39
+ `);
40
+ }
41
+ logger.debug("ContentCache: schema initialized");
42
+ }
43
+ // ------------------------------------------------------------------
44
+ // Core operations
45
+ // ------------------------------------------------------------------
46
+ /**
47
+ * Look up content by hash. If it exists, increments hit_count and
48
+ * returns the saved tokens. Otherwise returns a miss.
49
+ */
50
+ lookup(content, source) {
51
+ const hash = hashContent(content);
52
+ const row = this.db.prepare("SELECT * FROM content_cache WHERE hash = ?").get(hash);
53
+ if (!row) {
54
+ return { hit: false, hash, tokensSaved: 0 };
55
+ }
56
+ const now = Math.floor(Date.now() / 1e3);
57
+ this.db.prepare(
58
+ "UPDATE content_cache SET hit_count = hit_count + 1, last_seen = ? WHERE hash = ?"
59
+ ).run(now, hash);
60
+ if (source && source !== row.source) {
61
+ this.db.prepare("UPDATE content_cache SET source = ? WHERE hash = ?").run(source, hash);
62
+ }
63
+ const entry = this.rowToEntry({
64
+ ...row,
65
+ hit_count: row.hit_count + 1,
66
+ last_seen: now,
67
+ source: source ?? row.source
68
+ });
69
+ return {
70
+ hit: true,
71
+ hash,
72
+ entry,
73
+ tokensSaved: entry.tokenCount
74
+ };
75
+ }
76
+ /**
77
+ * Insert or update a cache entry. Returns the entry.
78
+ */
79
+ put(content, source, metadata) {
80
+ const hash = hashContent(content);
81
+ const tokenCount = estimateTokens(content);
82
+ const now = Math.floor(Date.now() / 1e3);
83
+ const src = source ?? "";
84
+ const meta = metadata ? JSON.stringify(metadata) : null;
85
+ const existing = this.db.prepare("SELECT hash FROM content_cache WHERE hash = ?").get(hash);
86
+ if (existing) {
87
+ this.db.prepare(
88
+ "UPDATE content_cache SET hit_count = hit_count + 1, last_seen = ?, source = ?, metadata = ? WHERE hash = ?"
89
+ ).run(now, src, meta, hash);
90
+ } else {
91
+ this.db.prepare(
92
+ `INSERT INTO content_cache (hash, content, token_count, hit_count, first_seen, last_seen, source, metadata)
93
+ VALUES (?, ?, ?, 0, ?, ?, ?, ?)`
94
+ ).run(hash, content, tokenCount, now, now, src, meta);
95
+ this.db.prepare(`INSERT INTO content_cache_fts (content, hash) VALUES (?, ?)`).run(content, hash);
96
+ }
97
+ return this.getEntry(hash);
98
+ }
99
+ /**
100
+ * Retrieve a single entry by hash.
101
+ */
102
+ getEntry(hash) {
103
+ const row = this.db.prepare("SELECT * FROM content_cache WHERE hash = ?").get(hash);
104
+ return row ? this.rowToEntry(row) : void 0;
105
+ }
106
+ /**
107
+ * Aggregate cache statistics.
108
+ */
109
+ getStats() {
110
+ const agg = this.db.prepare(
111
+ `SELECT
112
+ COUNT(*) as total_entries,
113
+ COALESCE(SUM(token_count), 0) as total_tokens_cached,
114
+ COALESCE(SUM(hit_count * token_count), 0) as total_tokens_saved,
115
+ COALESCE(SUM(hit_count), 0) as total_hits,
116
+ COUNT(*) as total_lookups
117
+ FROM content_cache`
118
+ ).get();
119
+ const totalHits = agg.total_hits;
120
+ const totalEntries = agg.total_entries;
121
+ const hitRate = totalHits + totalEntries > 0 ? totalHits / (totalHits + totalEntries) : 0;
122
+ const topRows = this.db.prepare(
123
+ `SELECT source, SUM(hit_count * token_count) as tokens_saved
124
+ FROM content_cache
125
+ WHERE source != ''
126
+ GROUP BY source
127
+ ORDER BY tokens_saved DESC
128
+ LIMIT 10`
129
+ ).all();
130
+ return {
131
+ totalEntries,
132
+ totalTokensCached: agg.total_tokens_cached,
133
+ totalTokensSaved: agg.total_tokens_saved,
134
+ hitRate,
135
+ topSources: topRows.map((r) => ({
136
+ source: r.source,
137
+ tokensSaved: r.tokens_saved
138
+ }))
139
+ };
140
+ }
141
+ /**
142
+ * Remove entries older than the given unix timestamp.
143
+ * Returns the number of evicted entries.
144
+ */
145
+ evict(olderThan) {
146
+ const cutoff = olderThan ?? Math.floor(Date.now() / 1e3);
147
+ this.db.prepare(
148
+ `DELETE FROM content_cache_fts
149
+ WHERE hash IN (SELECT hash FROM content_cache WHERE last_seen < ?)`
150
+ ).run(cutoff);
151
+ const result = this.db.prepare("DELETE FROM content_cache WHERE last_seen < ?").run(cutoff);
152
+ if (result.changes > 0) {
153
+ logger.debug(`ContentCache: evicted ${result.changes} entries`);
154
+ }
155
+ return result.changes;
156
+ }
157
+ /**
158
+ * Search cached content via FTS5.
159
+ */
160
+ search(query, limit = 20) {
161
+ if (!query.trim()) return [];
162
+ const sanitized = this.sanitizeFtsQuery(query);
163
+ const rows = this.db.prepare(
164
+ `SELECT cc.*
165
+ FROM content_cache_fts fts
166
+ JOIN content_cache cc ON cc.hash = fts.hash
167
+ WHERE content_cache_fts MATCH ?
168
+ LIMIT ?`
169
+ ).all(sanitized, limit);
170
+ return rows.map((r) => this.rowToEntry(r));
171
+ }
172
+ /**
173
+ * Remove all entries.
174
+ */
175
+ clear() {
176
+ this.db.exec("DELETE FROM content_cache_fts");
177
+ this.db.exec("DELETE FROM content_cache");
178
+ logger.debug("ContentCache: cleared");
179
+ }
180
+ // ------------------------------------------------------------------
181
+ // Key-based operations (for input-addressed caching, e.g., tool+args → result)
182
+ // ------------------------------------------------------------------
183
+ /**
184
+ * Look up cached result by an explicit key (e.g., "tool:args-hash").
185
+ * The key is hashed to produce the cache entry hash.
186
+ */
187
+ lookupByKey(key, source) {
188
+ return this.lookup(key, source);
189
+ }
190
+ /**
191
+ * Store a result under an explicit key.
192
+ * The key is hashed for addressing; the value is stored as content.
193
+ */
194
+ putByKey(key, value, source, metadata) {
195
+ const hash = hashContent(key);
196
+ const tokenCount = estimateTokens(value);
197
+ const now = Math.floor(Date.now() / 1e3);
198
+ const src = source ?? "";
199
+ const meta = metadata ? JSON.stringify(metadata) : null;
200
+ const existing = this.db.prepare("SELECT hash FROM content_cache WHERE hash = ?").get(hash);
201
+ if (existing) {
202
+ this.db.prepare(
203
+ "UPDATE content_cache SET content = ?, token_count = ?, hit_count = hit_count + 1, last_seen = ?, source = ?, metadata = ? WHERE hash = ?"
204
+ ).run(value, tokenCount, now, src, meta, hash);
205
+ } else {
206
+ this.db.prepare(
207
+ `INSERT INTO content_cache (hash, content, token_count, hit_count, first_seen, last_seen, source, metadata)
208
+ VALUES (?, ?, ?, 0, ?, ?, ?, ?)`
209
+ ).run(hash, value, tokenCount, now, now, src, meta);
210
+ this.db.prepare(`INSERT INTO content_cache_fts (content, hash) VALUES (?, ?)`).run(value, hash);
211
+ }
212
+ return this.getEntry(hash);
213
+ }
214
+ // ------------------------------------------------------------------
215
+ // Helpers
216
+ // ------------------------------------------------------------------
217
+ rowToEntry(row) {
218
+ return {
219
+ hash: row.hash,
220
+ content: row.content,
221
+ tokenCount: row.token_count,
222
+ hitCount: row.hit_count,
223
+ firstSeen: row.first_seen,
224
+ lastSeen: row.last_seen,
225
+ source: row.source,
226
+ metadata: row.metadata ? JSON.parse(row.metadata) : void 0
227
+ };
228
+ }
229
+ sanitizeFtsQuery(query) {
230
+ const cleaned = query.replace(/['"()*~^{}\[\]]/g, "");
231
+ const terms = cleaned.split(/\s+/).filter((t) => t && !/^(AND|OR|NOT|NEAR)$/i.test(t));
232
+ if (terms.length === 0) return '""';
233
+ return terms.map((t) => `"${t}"`).join(" ");
234
+ }
235
+ }
236
+ export {
237
+ ContentCache
238
+ };