@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,158 @@
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 { execFileSync } from "child_process";
6
+ import {
7
+ canonicalStateStore
8
+ } from "../../core/shared-state/canonical-store.js";
9
+ import { projectIdFromIdentifier } from "../../core/shared-state/canonical-store.js";
10
+ function runGit(args, cwd) {
11
+ return execFileSync("git", args, {
12
+ cwd,
13
+ encoding: "utf8",
14
+ stdio: ["ignore", "pipe", "pipe"]
15
+ }).trim();
16
+ }
17
+ function runGh(args, cwd) {
18
+ return execFileSync("gh", args, {
19
+ cwd,
20
+ encoding: "utf8",
21
+ stdio: ["ignore", "pipe", "pipe"]
22
+ }).trim();
23
+ }
24
+ function normalizeRemoteToRepo(remote) {
25
+ const cleaned = remote.replace(/\.git$/, "").trim();
26
+ if (cleaned.startsWith("git@github.com:")) {
27
+ return cleaned.replace("git@github.com:", "");
28
+ }
29
+ if (cleaned.startsWith("https://github.com/")) {
30
+ return cleaned.replace("https://github.com/", "");
31
+ }
32
+ if (cleaned.startsWith("http://github.com/")) {
33
+ return cleaned.replace("http://github.com/", "");
34
+ }
35
+ throw new Error(`Unsupported GitHub remote: ${remote}`);
36
+ }
37
+ function summarizeStatusCheckRollup(rollup) {
38
+ if (!rollup || rollup.length === 0) {
39
+ return void 0;
40
+ }
41
+ const states = rollup.map((item) => item.conclusion || item.status || item.state).filter(Boolean);
42
+ if (states.length === 0) {
43
+ return void 0;
44
+ }
45
+ if (states.every((state) => state === "SUCCESS")) {
46
+ return "SUCCESS";
47
+ }
48
+ if (states.some((state) => state === "FAILURE" || state === "ERROR")) {
49
+ return "FAILURE";
50
+ }
51
+ if (states.some(
52
+ (state) => state === "PENDING" || state === "IN_PROGRESS" || state === "EXPECTED"
53
+ )) {
54
+ return "PENDING";
55
+ }
56
+ return states[0];
57
+ }
58
+ function getCurrentRepoGitHubInfo(cwd = process.cwd()) {
59
+ try {
60
+ const projectPath = runGit(["rev-parse", "--show-toplevel"], cwd);
61
+ const branch = runGit(["rev-parse", "--abbrev-ref", "HEAD"], projectPath);
62
+ const remote = runGit(
63
+ ["config", "--get", "remote.origin.url"],
64
+ projectPath
65
+ );
66
+ const repo = normalizeRemoteToRepo(remote);
67
+ return {
68
+ repo,
69
+ branch,
70
+ projectPath,
71
+ projectId: projectIdFromIdentifier(remote)
72
+ };
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+ async function refreshCurrentRepoPullRequestState(cwd = process.cwd()) {
78
+ const info = getCurrentRepoGitHubInfo(cwd);
79
+ if (!info) {
80
+ return null;
81
+ }
82
+ try {
83
+ const output = runGh(
84
+ [
85
+ "pr",
86
+ "view",
87
+ "--repo",
88
+ info.repo,
89
+ "--json",
90
+ [
91
+ "number",
92
+ "title",
93
+ "state",
94
+ "isDraft",
95
+ "url",
96
+ "baseRefName",
97
+ "headRefName",
98
+ "headRefOid",
99
+ "mergedAt",
100
+ "updatedAt",
101
+ "reviewDecision",
102
+ "statusCheckRollup"
103
+ ].join(",")
104
+ ],
105
+ info.projectPath
106
+ );
107
+ const parsed = JSON.parse(output);
108
+ const projection = {
109
+ repo: info.repo,
110
+ branch: info.branch,
111
+ projectId: info.projectId,
112
+ projectPath: info.projectPath,
113
+ prNumber: parsed.number,
114
+ title: parsed.title,
115
+ state: parsed.mergedAt && parsed.state === "MERGED" ? "MERGED" : parsed.state,
116
+ isDraft: parsed.isDraft,
117
+ url: parsed.url,
118
+ baseRefName: parsed.baseRefName,
119
+ headRefName: parsed.headRefName,
120
+ headRefOid: parsed.headRefOid,
121
+ mergedAt: parsed.mergedAt || void 0,
122
+ updatedAt: parsed.updatedAt,
123
+ reviewDecision: parsed.reviewDecision || void 0,
124
+ statusCheckRollup: summarizeStatusCheckRollup(parsed.statusCheckRollup),
125
+ lastSyncedAt: Date.now()
126
+ };
127
+ await canonicalStateStore.saveGitHubPullRequest(projection);
128
+ if (projection.state === "MERGED" || projection.state === "CLOSED") {
129
+ await canonicalStateStore.releaseClaims({
130
+ projectId: info.projectId,
131
+ projectPath: info.projectPath,
132
+ branch: info.branch,
133
+ reason: `github_pr_${projection.state.toLowerCase()}`
134
+ });
135
+ }
136
+ await canonicalStateStore.appendEvent({
137
+ type: "github_pr_refreshed",
138
+ tool: "stackmemory",
139
+ projectId: info.projectId,
140
+ projectPath: info.projectPath,
141
+ branch: info.branch,
142
+ payload: {
143
+ repo: info.repo,
144
+ prNumber: projection.prNumber,
145
+ state: projection.state,
146
+ reviewDecision: projection.reviewDecision,
147
+ statusCheckRollup: projection.statusCheckRollup
148
+ }
149
+ });
150
+ return projection;
151
+ } catch {
152
+ return null;
153
+ }
154
+ }
155
+ export {
156
+ getCurrentRepoGitHubInfo,
157
+ refreshCurrentRepoPullRequestState
158
+ };
@@ -496,7 +496,10 @@ class LinearClient {
496
496
  filter: Object.keys(filter).length > 0 ? filter : void 0,
497
497
  first: options?.limit || 50
498
498
  });
499
- return result.issues.nodes;
499
+ return result.issues.nodes.map((issue) => ({
500
+ ...issue,
501
+ labels: Array.isArray(issue.labels) ? issue.labels : issue.labels?.nodes || []
502
+ }));
500
503
  }
501
504
  /**
502
505
  * Assign an issue to a user
@@ -0,0 +1,196 @@
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 Database from "better-sqlite3";
6
+ import { randomUUID } from "crypto";
7
+ import { calculateBackoff } from "../../core/errors/recovery.js";
8
+ import { logger } from "../../core/monitoring/logger.js";
9
+ const DEFAULT_CONFIG = {
10
+ maxAttempts: 5,
11
+ initialDelay: 1e3,
12
+ maxDelay: 3e5,
13
+ backoffFactor: 2,
14
+ workerIntervalMs: 5e3
15
+ };
16
+ class WebhookDeliveryQueue {
17
+ db;
18
+ config;
19
+ workerTimer = null;
20
+ handler = null;
21
+ constructor(dbPath, config) {
22
+ this.config = { ...DEFAULT_CONFIG, ...config };
23
+ this.db = new Database(dbPath);
24
+ this.db.pragma("journal_mode = WAL");
25
+ this.ensureSchema();
26
+ }
27
+ ensureSchema() {
28
+ this.db.exec(`
29
+ CREATE TABLE IF NOT EXISTS webhook_deliveries (
30
+ id TEXT PRIMARY KEY,
31
+ event_type TEXT NOT NULL,
32
+ payload TEXT NOT NULL,
33
+ status TEXT NOT NULL DEFAULT 'pending',
34
+ attempts INTEGER NOT NULL DEFAULT 0,
35
+ max_attempts INTEGER NOT NULL DEFAULT 5,
36
+ next_retry_at INTEGER,
37
+ last_error TEXT,
38
+ created_at INTEGER NOT NULL,
39
+ updated_at INTEGER NOT NULL
40
+ );
41
+ CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_status_retry
42
+ ON webhook_deliveries(status, next_retry_at);
43
+ `);
44
+ }
45
+ /**
46
+ * Set the handler function that processes webhook events.
47
+ */
48
+ setHandler(fn) {
49
+ this.handler = fn;
50
+ }
51
+ /**
52
+ * Enqueue a webhook event for processing.
53
+ */
54
+ enqueue(eventType, payload) {
55
+ const id = randomUUID();
56
+ const now = Date.now();
57
+ this.db.prepare(
58
+ `INSERT INTO webhook_deliveries
59
+ (id, event_type, payload, status, attempts, max_attempts, next_retry_at, created_at, updated_at)
60
+ VALUES (?, ?, ?, 'pending', 0, ?, ?, ?, ?)`
61
+ ).run(
62
+ id,
63
+ eventType,
64
+ JSON.stringify(payload),
65
+ this.config.maxAttempts,
66
+ now,
67
+ now,
68
+ now
69
+ );
70
+ return id;
71
+ }
72
+ /**
73
+ * Process the next eligible delivery. Returns true if a delivery was processed.
74
+ */
75
+ async processNext() {
76
+ if (!this.handler) {
77
+ return false;
78
+ }
79
+ const now = Date.now();
80
+ const delivery = this.db.prepare(
81
+ `UPDATE webhook_deliveries
82
+ SET status = 'processing', updated_at = ?
83
+ WHERE id = (
84
+ SELECT id FROM webhook_deliveries
85
+ WHERE (status = 'pending' OR (status = 'failed' AND next_retry_at <= ?))
86
+ ORDER BY next_retry_at ASC, created_at ASC
87
+ LIMIT 1
88
+ )
89
+ RETURNING *`
90
+ ).get(now, now);
91
+ if (!delivery) {
92
+ return false;
93
+ }
94
+ const attempt = delivery.attempts + 1;
95
+ try {
96
+ const payload = JSON.parse(delivery.payload);
97
+ await this.handler(delivery.event_type, payload);
98
+ this.db.prepare(
99
+ `UPDATE webhook_deliveries
100
+ SET status = 'completed', attempts = ?, last_error = NULL, updated_at = ?
101
+ WHERE id = ?`
102
+ ).run(attempt, Date.now(), delivery.id);
103
+ logger.info(
104
+ `Webhook delivery ${delivery.id} completed on attempt ${attempt}`
105
+ );
106
+ return true;
107
+ } catch (error) {
108
+ const errorMsg = error instanceof Error ? error.message : String(error);
109
+ if (attempt >= delivery.max_attempts) {
110
+ this.db.prepare(
111
+ `UPDATE webhook_deliveries
112
+ SET status = 'dead', attempts = ?, last_error = ?, updated_at = ?
113
+ WHERE id = ?`
114
+ ).run(attempt, errorMsg, Date.now(), delivery.id);
115
+ logger.error(
116
+ `Webhook delivery ${delivery.id} dead after ${attempt} attempts: ${errorMsg}`
117
+ );
118
+ } else {
119
+ const delay = calculateBackoff(
120
+ attempt,
121
+ this.config.initialDelay,
122
+ this.config.maxDelay,
123
+ this.config.backoffFactor
124
+ );
125
+ const nextRetry = Date.now() + delay;
126
+ this.db.prepare(
127
+ `UPDATE webhook_deliveries
128
+ SET status = 'failed', attempts = ?, last_error = ?, next_retry_at = ?, updated_at = ?
129
+ WHERE id = ?`
130
+ ).run(attempt, errorMsg, nextRetry, Date.now(), delivery.id);
131
+ logger.warn(
132
+ `Webhook delivery ${delivery.id} failed (attempt ${attempt}/${delivery.max_attempts}), retry in ${delay}ms`
133
+ );
134
+ }
135
+ return true;
136
+ }
137
+ }
138
+ /**
139
+ * Start the background worker that polls for deliveries.
140
+ */
141
+ startWorker() {
142
+ if (this.workerTimer) return;
143
+ this.workerTimer = setInterval(async () => {
144
+ try {
145
+ while (await this.processNext()) {
146
+ }
147
+ } catch (error) {
148
+ logger.error("Webhook retry worker error", error);
149
+ }
150
+ }, this.config.workerIntervalMs);
151
+ }
152
+ /**
153
+ * Stop the background worker.
154
+ */
155
+ stopWorker() {
156
+ if (this.workerTimer) {
157
+ clearInterval(this.workerTimer);
158
+ this.workerTimer = null;
159
+ }
160
+ }
161
+ /**
162
+ * Get delivery counts by status.
163
+ */
164
+ getStats() {
165
+ const rows = this.db.prepare(
166
+ `SELECT status, COUNT(*) as count FROM webhook_deliveries GROUP BY status`
167
+ ).all();
168
+ const stats = {
169
+ pending: 0,
170
+ processing: 0,
171
+ completed: 0,
172
+ failed: 0,
173
+ dead: 0
174
+ };
175
+ for (const row of rows) {
176
+ stats[row.status] = row.count;
177
+ }
178
+ return stats;
179
+ }
180
+ /**
181
+ * Get a single delivery by ID.
182
+ */
183
+ getDelivery(id) {
184
+ return this.db.prepare(`SELECT * FROM webhook_deliveries WHERE id = ?`).get(id);
185
+ }
186
+ /**
187
+ * Close the database connection and stop the worker.
188
+ */
189
+ close() {
190
+ this.stopWorker();
191
+ this.db.close();
192
+ }
193
+ }
194
+ export {
195
+ WebhookDeliveryQueue
196
+ };
@@ -8,7 +8,10 @@ import crypto from "crypto";
8
8
  import { LinearSyncService } from "./sync-service.js";
9
9
  import { IntegrationError, ErrorCode } from "../../core/errors/index.js";
10
10
  import { logger } from "../../core/monitoring/logger.js";
11
+ import { WebhookDeliveryQueue } from "./webhook-retry.js";
11
12
  import chalk from "chalk";
13
+ import { join } from "path";
14
+ import { homedir } from "os";
12
15
  function _getEnv(key, defaultValue) {
13
16
  const value = process.env[key];
14
17
  if (value === void 0) {
@@ -29,21 +32,28 @@ class LinearWebhookServer {
29
32
  // Using singleton logger from monitoring
30
33
  syncService;
31
34
  config;
32
- eventQueue = [];
33
- isProcessing = false;
35
+ deliveryQueue;
34
36
  constructor(config) {
35
37
  this.app = express();
36
38
  this.syncService = new LinearSyncService();
39
+ const dbPath = config?.dbPath || join(homedir(), ".stackmemory", "webhook-deliveries.db");
37
40
  this.config = {
38
41
  port: config?.port || parseInt(process.env["WEBHOOK_PORT"] || "3456"),
39
42
  host: config?.host || process.env["WEBHOOK_HOST"] || "localhost",
40
43
  webhookSecret: config?.webhookSecret || process.env["LINEAR_WEBHOOK_SECRET"],
41
44
  maxPayloadSize: config?.maxPayloadSize || "10mb",
45
+ dbPath,
42
46
  rateLimit: {
43
47
  windowMs: config?.rateLimit?.windowMs || 6e4,
44
48
  max: config?.rateLimit?.max || 100
45
49
  }
46
50
  };
51
+ this.deliveryQueue = new WebhookDeliveryQueue(dbPath);
52
+ this.deliveryQueue.setHandler(
53
+ async (eventType, payload) => {
54
+ await this.handleWebhookEvent(payload);
55
+ }
56
+ );
47
57
  this.setupMiddleware();
48
58
  this.setupRoutes();
49
59
  }
@@ -61,12 +71,12 @@ class LinearWebhookServer {
61
71
  }
62
72
  setupRoutes() {
63
73
  this.app.get("/health", (req, res) => {
74
+ const stats = this.deliveryQueue.getStats();
64
75
  res.json({
65
76
  status: "healthy",
66
77
  service: "linear-webhook",
67
78
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
68
- queue: this.eventQueue.length,
69
- processing: this.isProcessing
79
+ deliveries: stats
70
80
  });
71
81
  });
72
82
  this.app.post("/webhook/linear", async (req, res) => {
@@ -77,11 +87,10 @@ class LinearWebhookServer {
77
87
  }
78
88
  const payload = JSON.parse(req.body.toString());
79
89
  logger.info(`Received webhook: ${payload.type} - ${payload.action}`);
80
- this.eventQueue.push(payload);
81
- this.processQueue();
90
+ const deliveryId = this.deliveryQueue.enqueue(payload.type, payload);
82
91
  return res.status(200).json({
83
92
  status: "accepted",
84
- queued: true
93
+ deliveryId
85
94
  });
86
95
  } catch (error) {
87
96
  logger.error("Webhook processing error:", error);
@@ -104,21 +113,6 @@ class LinearWebhookServer {
104
113
  const hash = crypto.createHmac("sha256", this.config.webhookSecret).update(req.body).digest("hex");
105
114
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(hash));
106
115
  }
107
- async processQueue() {
108
- if (this.isProcessing || this.eventQueue.length === 0) {
109
- return;
110
- }
111
- this.isProcessing = true;
112
- while (this.eventQueue.length > 0) {
113
- const event = this.eventQueue.shift();
114
- try {
115
- await this.handleWebhookEvent(event);
116
- } catch (error) {
117
- logger.error(`Failed to process event: ${event.type}`, error);
118
- }
119
- }
120
- this.isProcessing = false;
121
- }
122
116
  async handleWebhookEvent(payload) {
123
117
  const { type, action, data } = payload;
124
118
  switch (type) {
@@ -166,6 +160,7 @@ class LinearWebhookServer {
166
160
  this.config.port,
167
161
  this.config.host,
168
162
  () => {
163
+ this.deliveryQueue.startWorker();
169
164
  console.log(
170
165
  chalk.green("\u2713") + chalk.bold(" Linear Webhook Server Started")
171
166
  );
@@ -188,6 +183,7 @@ class LinearWebhookServer {
188
183
  });
189
184
  }
190
185
  async stop() {
186
+ this.deliveryQueue.close();
191
187
  return new Promise((resolve) => {
192
188
  if (this.server) {
193
189
  this.server.close(() => {
@@ -0,0 +1,101 @@
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 VALID_SYNC_TABLES = /* @__PURE__ */ new Set([
6
+ "frames",
7
+ "events",
8
+ "anchors",
9
+ "trace_events",
10
+ "entity_states"
11
+ ]);
12
+ class CloudSyncHandlers {
13
+ syncManager;
14
+ constructor(deps) {
15
+ this.syncManager = deps.syncManager;
16
+ }
17
+ ensureManager() {
18
+ if (!this.syncManager) {
19
+ throw new Error(
20
+ "Cloud sync not configured. Run `stackmemory login` to connect to Provenant."
21
+ );
22
+ }
23
+ return this.syncManager;
24
+ }
25
+ async handlePush(args) {
26
+ const manager = this.ensureManager();
27
+ const result = await manager.performPush("manual", args.force);
28
+ return {
29
+ content: [
30
+ {
31
+ type: "text",
32
+ text: JSON.stringify(
33
+ {
34
+ success: result.success,
35
+ pushed: result.pushed,
36
+ rejected: result.rejected,
37
+ conflicts: result.conflicts,
38
+ error: result.error
39
+ },
40
+ null,
41
+ 2
42
+ )
43
+ }
44
+ ]
45
+ };
46
+ }
47
+ async handlePull(args) {
48
+ const manager = this.ensureManager();
49
+ const tables = args.tables?.filter((t) => VALID_SYNC_TABLES.has(t));
50
+ const result = await manager.performPull("manual", tables);
51
+ return {
52
+ content: [
53
+ {
54
+ type: "text",
55
+ text: JSON.stringify(
56
+ {
57
+ success: result.success,
58
+ pulled: result.pulled,
59
+ applied: result.applied,
60
+ conflicts: result.conflicts,
61
+ error: result.error
62
+ },
63
+ null,
64
+ 2
65
+ )
66
+ }
67
+ ]
68
+ };
69
+ }
70
+ async handleStatus() {
71
+ if (!this.syncManager) {
72
+ return {
73
+ content: [
74
+ {
75
+ type: "text",
76
+ text: JSON.stringify(
77
+ {
78
+ connected: false,
79
+ message: "Cloud sync not configured. Run `stackmemory login` to connect."
80
+ },
81
+ null,
82
+ 2
83
+ )
84
+ }
85
+ ]
86
+ };
87
+ }
88
+ const status = this.syncManager.getStatus();
89
+ return {
90
+ content: [
91
+ {
92
+ type: "text",
93
+ text: JSON.stringify(status, null, 2)
94
+ }
95
+ ]
96
+ };
97
+ }
98
+ }
99
+ export {
100
+ CloudSyncHandlers
101
+ };