@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.
- package/LICENSE +131 -64
- package/README.md +107 -24
- 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/claude-sm.js +266 -84
- package/dist/src/cli/codex-sm.js +225 -33
- package/dist/src/cli/commands/bench.js +209 -2
- package/dist/src/cli/commands/brain.js +206 -0
- package/dist/src/cli/commands/cache.js +126 -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/daemon.js +41 -0
- package/dist/src/cli/commands/handoff.js +40 -9
- package/dist/src/cli/commands/onboard.js +70 -3
- package/dist/src/cli/commands/operator.js +127 -0
- package/dist/src/cli/commands/optimize.js +117 -0
- package/dist/src/cli/commands/orchestrate.js +232 -5
- package/dist/src/cli/commands/orchestrator.js +315 -26
- package/dist/src/cli/commands/pack.js +322 -0
- 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/search.js +40 -1
- package/dist/src/cli/commands/setup.js +178 -11
- package/dist/src/cli/commands/skills.js +10 -1
- package/dist/src/cli/commands/state.js +265 -0
- 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/gemini-sm.js +19 -29
- package/dist/src/cli/hermes-sm.js +224 -0
- package/dist/src/cli/index.js +105 -39
- package/dist/src/cli/opencode-sm.js +38 -21
- package/dist/src/cli/utils/determinism-watcher.js +66 -0
- package/dist/src/cli/utils/real-cli-bin.js +116 -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/content-cache.js +238 -0
- package/dist/src/{integrations/diffmem → core/cache}/index.js +5 -5
- package/dist/src/core/cache/token-estimator.js +39 -0
- package/dist/src/core/config/feature-flags.js +2 -6
- package/dist/src/core/context/frame-database.js +79 -27
- package/dist/src/core/context/recursive-context-manager.js +1 -1
- package/dist/src/core/context/rehydration.js +2 -1
- package/dist/src/core/cross-search/cross-project-search.js +269 -0
- package/dist/src/core/cross-search/index.js +10 -0
- package/dist/src/core/database/sqlite-adapter.js +14 -84
- package/dist/src/core/extensions/provider-adapter.js +5 -0
- package/dist/src/core/models/model-router.js +54 -2
- package/dist/src/core/models/provider-pricing.js +58 -4
- package/dist/src/core/monitoring/logger.js +2 -1
- package/dist/src/core/optimization/trace-optimizer.js +413 -0
- 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/provenance/confidence-scorer.js +128 -0
- package/dist/src/core/provenance/index.js +40 -0
- package/dist/src/core/provenance/provenance-store.js +194 -0
- package/dist/src/core/provenance/types.js +82 -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/session/project-handoff.js +64 -0
- package/dist/src/core/session/session-manager.js +28 -0
- package/dist/src/core/shared-state/canonical-store.js +564 -0
- package/dist/src/core/skill-packs/index.js +18 -0
- package/dist/src/core/skill-packs/parser.js +42 -0
- package/dist/src/core/skill-packs/registry.js +224 -0
- package/dist/src/core/skill-packs/types.js +79 -0
- 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/trace/trace-event-store.js +282 -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 +52 -0
- package/dist/src/daemon/services/desire-path-service.js +566 -0
- package/dist/src/daemon/services/github-service.js +126 -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 +58 -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/sweep/pty-wrapper.js +13 -5
- 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/features/web/client/hooks/use-socket.js +12 -0
- package/dist/src/{core/merge/index.js → features/web/client/lib/utils.js} +8 -4
- package/dist/src/features/web/client/next-env.d.js +4 -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 +6 -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 +307 -11
- package/dist/src/integrations/claude-code/task-coordinator.js +2 -1
- package/dist/src/integrations/github/pr-state.js +158 -0
- package/dist/src/integrations/linear/client.js +4 -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 +40 -84
- package/dist/src/integrations/mcp/server.js +542 -641
- package/dist/src/integrations/mcp/tool-alias-registry.js +297 -0
- package/dist/src/integrations/mcp/tool-definitions.js +152 -682
- package/dist/src/mcp/stackmemory-mcp-server.js +571 -231
- package/dist/src/orchestrators/multimodal/determinism.js +244 -0
- package/dist/src/orchestrators/multimodal/harness.js +149 -78
- package/dist/src/orchestrators/multimodal/providers.js +44 -3
- package/dist/src/skills/recursive-agent-orchestrator.js +2 -4
- package/dist/src/utils/hook-installer.js +0 -8
- 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 +27 -9
- package/packs/coding/python-fastapi/instructions.md +60 -0
- package/packs/coding/python-fastapi/pack.yaml +28 -0
- package/packs/coding/typescript-react/instructions.md +47 -0
- package/packs/coding/typescript-react/pack.yaml +28 -0
- package/packs/core/commands/capture.md +32 -0
- package/packs/core/commands/learn.md +73 -0
- package/packs/core/commands/next.md +36 -0
- package/packs/core/commands/restart.md +58 -0
- package/packs/core/commands/restore.md +29 -0
- package/packs/core/commands/start.md +57 -0
- package/packs/core/commands/stop.md +65 -0
- package/packs/core/commands/summary.md +40 -0
- package/packs/core/manifest.json +24 -0
- package/packs/ops/decision-recovery/instructions.md +65 -0
- package/packs/ops/decision-recovery/pack.yaml +89 -0
- 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/cli/commands/team.js +0 -168
- package/dist/src/core/context/shared-context-layer.js +0 -620
- package/dist/src/core/context/stack-merge-resolver.js +0 -748
- package/dist/src/core/merge/conflict-detector.js +0 -430
- package/dist/src/core/merge/resolution-engine.js +0 -557
- package/dist/src/core/merge/stack-diff.js +0 -531
- package/dist/src/core/merge/unified-merge-resolver.js +0 -302
- 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/cord-handlers.js +0 -397
- 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/mcp/handlers/team-handlers.js +0 -211
- 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 -391
- package/dist/src/integrations/ralph/index.js +0 -17
- package/dist/src/integrations/ralph/learning/pattern-learner.js +0 -435
- 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 -1007
- package/dist/src/integrations/ralph/visualization/ralph-debugger.js +0 -635
- package/scripts/ralph-loop-implementation.js +0 -404
- /package/dist/src/core/{merge → cache}/types.js +0 -0
- /package/dist/src/{integrations/diffmem/types.js → core/storage/cloud-sync-types.js} +0 -0
- /package/dist/src/{integrations/greptile/types.js → core/trace/trace-event.js} +0 -0
- /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
|
-
|
|
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
|
-
|
|
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.
|
|
81
|
-
this.processQueue();
|
|
90
|
+
const deliveryId = this.deliveryQueue.enqueue(payload.type, payload);
|
|
82
91
|
return res.status(200).json({
|
|
83
92
|
status: "accepted",
|
|
84
|
-
|
|
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
|
+
};
|