@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,574 @@
|
|
|
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 { createHash } from "crypto";
|
|
6
|
+
import { AsyncMutex } from "../utils/async-mutex.js";
|
|
7
|
+
import { logger } from "../monitoring/logger.js";
|
|
8
|
+
const SYNCABLE_TABLES = [
|
|
9
|
+
"frames",
|
|
10
|
+
"events",
|
|
11
|
+
"anchors",
|
|
12
|
+
"trace_events",
|
|
13
|
+
"entity_states"
|
|
14
|
+
];
|
|
15
|
+
const VERSION_COLUMN = {
|
|
16
|
+
frames: "created_at",
|
|
17
|
+
events: "ts",
|
|
18
|
+
anchors: "created_at",
|
|
19
|
+
trace_events: "timestamp",
|
|
20
|
+
entity_states: "valid_from"
|
|
21
|
+
};
|
|
22
|
+
const PK_COLUMN = {
|
|
23
|
+
frames: "frame_id",
|
|
24
|
+
events: "event_id",
|
|
25
|
+
anchors: "anchor_id",
|
|
26
|
+
trace_events: "id",
|
|
27
|
+
entity_states: "id"
|
|
28
|
+
};
|
|
29
|
+
const VALID_COLUMNS = {
|
|
30
|
+
frames: /* @__PURE__ */ new Set([
|
|
31
|
+
"frame_id",
|
|
32
|
+
"run_id",
|
|
33
|
+
"project_id",
|
|
34
|
+
"parent_frame_id",
|
|
35
|
+
"depth",
|
|
36
|
+
"type",
|
|
37
|
+
"name",
|
|
38
|
+
"state",
|
|
39
|
+
"inputs",
|
|
40
|
+
"outputs",
|
|
41
|
+
"digest_text",
|
|
42
|
+
"digest_json",
|
|
43
|
+
"created_at",
|
|
44
|
+
"closed_at",
|
|
45
|
+
"retention_policy",
|
|
46
|
+
"importance_score",
|
|
47
|
+
"prov_source",
|
|
48
|
+
"prov_derivation",
|
|
49
|
+
"prov_confidence",
|
|
50
|
+
"prov_superseded_by",
|
|
51
|
+
"prov_program_version",
|
|
52
|
+
"access_count",
|
|
53
|
+
"last_accessed"
|
|
54
|
+
]),
|
|
55
|
+
events: /* @__PURE__ */ new Set([
|
|
56
|
+
"event_id",
|
|
57
|
+
"frame_id",
|
|
58
|
+
"run_id",
|
|
59
|
+
"seq",
|
|
60
|
+
"event_type",
|
|
61
|
+
"payload",
|
|
62
|
+
"ts"
|
|
63
|
+
]),
|
|
64
|
+
anchors: /* @__PURE__ */ new Set([
|
|
65
|
+
"anchor_id",
|
|
66
|
+
"frame_id",
|
|
67
|
+
"project_id",
|
|
68
|
+
"type",
|
|
69
|
+
"text",
|
|
70
|
+
"priority",
|
|
71
|
+
"created_at",
|
|
72
|
+
"metadata",
|
|
73
|
+
"prov_source",
|
|
74
|
+
"prov_confidence",
|
|
75
|
+
"prov_superseded_by"
|
|
76
|
+
]),
|
|
77
|
+
trace_events: /* @__PURE__ */ new Set([
|
|
78
|
+
"id",
|
|
79
|
+
"timestamp",
|
|
80
|
+
"session_id",
|
|
81
|
+
"trace_id",
|
|
82
|
+
"parent_trace_id",
|
|
83
|
+
"tenant_id",
|
|
84
|
+
"actor_host",
|
|
85
|
+
"actor_agent",
|
|
86
|
+
"actor_user",
|
|
87
|
+
"operation",
|
|
88
|
+
"inputs",
|
|
89
|
+
"outputs",
|
|
90
|
+
"tokens_in",
|
|
91
|
+
"tokens_out",
|
|
92
|
+
"cost_usd",
|
|
93
|
+
"duration_ms",
|
|
94
|
+
"score",
|
|
95
|
+
"feedback",
|
|
96
|
+
"provenance",
|
|
97
|
+
"error",
|
|
98
|
+
"tags"
|
|
99
|
+
]),
|
|
100
|
+
entity_states: /* @__PURE__ */ new Set([
|
|
101
|
+
"id",
|
|
102
|
+
"project_id",
|
|
103
|
+
"entity_name",
|
|
104
|
+
"relation",
|
|
105
|
+
"value",
|
|
106
|
+
"context",
|
|
107
|
+
"source_frame_id",
|
|
108
|
+
"valid_from",
|
|
109
|
+
"superseded_at"
|
|
110
|
+
])
|
|
111
|
+
};
|
|
112
|
+
class CloudSyncEngine {
|
|
113
|
+
db;
|
|
114
|
+
config;
|
|
115
|
+
mutex;
|
|
116
|
+
fetchFn;
|
|
117
|
+
constructor(db, config, fetchFn) {
|
|
118
|
+
this.db = db;
|
|
119
|
+
this.config = config;
|
|
120
|
+
this.mutex = new AsyncMutex(3e5);
|
|
121
|
+
this.fetchFn = fetchFn ?? globalThis.fetch;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Push local changes to cloud
|
|
125
|
+
*/
|
|
126
|
+
async push(force = false) {
|
|
127
|
+
return this.mutex.withLock(async () => {
|
|
128
|
+
try {
|
|
129
|
+
this.ensureSyncTables();
|
|
130
|
+
if (force) {
|
|
131
|
+
this.db.prepare(`DELETE FROM cloud_sync_state`).run();
|
|
132
|
+
this.db.prepare(`DELETE FROM cloud_sync_cursors WHERE direction = 'push'`).run();
|
|
133
|
+
}
|
|
134
|
+
const pending = this.collectPendingEntities();
|
|
135
|
+
if (pending.length === 0) {
|
|
136
|
+
return { success: true, pushed: 0, rejected: 0, conflicts: 0 };
|
|
137
|
+
}
|
|
138
|
+
let totalPushed = 0;
|
|
139
|
+
let totalRejected = 0;
|
|
140
|
+
let totalConflicts = 0;
|
|
141
|
+
for (let i = 0; i < pending.length; i += this.config.batchSize) {
|
|
142
|
+
const batch = pending.slice(i, i + this.config.batchSize);
|
|
143
|
+
const cursor = this.getCursor("push");
|
|
144
|
+
const request = {
|
|
145
|
+
protocolVersion: 1,
|
|
146
|
+
clientId: this.config.clientId,
|
|
147
|
+
projectId: this.config.projectId,
|
|
148
|
+
syncCursor: cursor,
|
|
149
|
+
entities: batch,
|
|
150
|
+
checksum: this.computeChecksum(batch)
|
|
151
|
+
};
|
|
152
|
+
const response = await this.sendPush(request);
|
|
153
|
+
if (!response) {
|
|
154
|
+
return {
|
|
155
|
+
success: false,
|
|
156
|
+
pushed: totalPushed,
|
|
157
|
+
rejected: totalRejected,
|
|
158
|
+
conflicts: totalConflicts,
|
|
159
|
+
error: "Network error"
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const rejectedIds = new Set(response.rejected.map((r) => r.id));
|
|
163
|
+
const conflictIds = new Set(
|
|
164
|
+
(response.conflicts ?? []).map((c) => c.id)
|
|
165
|
+
);
|
|
166
|
+
for (const entity of batch) {
|
|
167
|
+
if (conflictIds.has(entity.id)) {
|
|
168
|
+
this.updateSyncState(entity.table, entity.id, "conflict");
|
|
169
|
+
} else if (!rejectedIds.has(entity.id)) {
|
|
170
|
+
this.markPushed(entity.table, entity.id, entity.version);
|
|
171
|
+
} else {
|
|
172
|
+
this.recordPushError(
|
|
173
|
+
entity.table,
|
|
174
|
+
entity.id,
|
|
175
|
+
response.rejected.find((r) => r.id === entity.id)?.reason ?? "unknown"
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
this.setCursor("push", response.serverCursor);
|
|
180
|
+
totalPushed += response.accepted;
|
|
181
|
+
totalRejected += response.rejected.length;
|
|
182
|
+
totalConflicts += (response.conflicts ?? []).length;
|
|
183
|
+
}
|
|
184
|
+
logger.info("Cloud sync push completed", {
|
|
185
|
+
pushed: totalPushed,
|
|
186
|
+
rejected: totalRejected,
|
|
187
|
+
conflicts: totalConflicts
|
|
188
|
+
});
|
|
189
|
+
return {
|
|
190
|
+
success: true,
|
|
191
|
+
pushed: totalPushed,
|
|
192
|
+
rejected: totalRejected,
|
|
193
|
+
conflicts: totalConflicts
|
|
194
|
+
};
|
|
195
|
+
} catch (error) {
|
|
196
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
197
|
+
logger.error("Cloud sync push failed", { error: msg });
|
|
198
|
+
return {
|
|
199
|
+
success: false,
|
|
200
|
+
pushed: 0,
|
|
201
|
+
rejected: 0,
|
|
202
|
+
conflicts: 0,
|
|
203
|
+
error: msg
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}, "cloud-sync-push");
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Pull remote changes to local
|
|
210
|
+
*/
|
|
211
|
+
async pull(tables) {
|
|
212
|
+
return this.mutex.withLock(async () => {
|
|
213
|
+
try {
|
|
214
|
+
this.ensureSyncTables();
|
|
215
|
+
let totalPulled = 0;
|
|
216
|
+
let totalApplied = 0;
|
|
217
|
+
let totalConflicts = 0;
|
|
218
|
+
let hasMore = true;
|
|
219
|
+
let cursor = this.getCursor("pull");
|
|
220
|
+
while (hasMore) {
|
|
221
|
+
const request = {
|
|
222
|
+
protocolVersion: 1,
|
|
223
|
+
clientId: this.config.clientId,
|
|
224
|
+
projectId: this.config.projectId,
|
|
225
|
+
since: cursor,
|
|
226
|
+
tables,
|
|
227
|
+
limit: this.config.batchSize
|
|
228
|
+
};
|
|
229
|
+
const response = await this.sendPull(request);
|
|
230
|
+
if (!response) {
|
|
231
|
+
return {
|
|
232
|
+
success: false,
|
|
233
|
+
pulled: totalPulled,
|
|
234
|
+
applied: totalApplied,
|
|
235
|
+
conflicts: totalConflicts,
|
|
236
|
+
error: "Network error"
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const applied = this.applyPulledEntities(response.entities);
|
|
240
|
+
totalPulled += response.entities.length;
|
|
241
|
+
totalApplied += applied.applied;
|
|
242
|
+
totalConflicts += applied.conflicts;
|
|
243
|
+
cursor = response.serverCursor;
|
|
244
|
+
hasMore = response.hasMore;
|
|
245
|
+
}
|
|
246
|
+
this.setCursor("pull", cursor);
|
|
247
|
+
logger.info("Cloud sync pull completed", {
|
|
248
|
+
pulled: totalPulled,
|
|
249
|
+
applied: totalApplied,
|
|
250
|
+
conflicts: totalConflicts
|
|
251
|
+
});
|
|
252
|
+
return {
|
|
253
|
+
success: true,
|
|
254
|
+
pulled: totalPulled,
|
|
255
|
+
applied: totalApplied,
|
|
256
|
+
conflicts: totalConflicts
|
|
257
|
+
};
|
|
258
|
+
} catch (error) {
|
|
259
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
260
|
+
logger.error("Cloud sync pull failed", { error: msg });
|
|
261
|
+
return {
|
|
262
|
+
success: false,
|
|
263
|
+
pulled: 0,
|
|
264
|
+
applied: 0,
|
|
265
|
+
conflicts: 0,
|
|
266
|
+
error: msg
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}, "cloud-sync-pull");
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Get current sync status
|
|
273
|
+
*/
|
|
274
|
+
status() {
|
|
275
|
+
this.ensureSyncTables();
|
|
276
|
+
const pendingPush = this.db.prepare(
|
|
277
|
+
`SELECT COUNT(*) as count FROM cloud_sync_state WHERE sync_status = 'pending'`
|
|
278
|
+
).get();
|
|
279
|
+
const conflicts = this.db.prepare(
|
|
280
|
+
`SELECT COUNT(*) as count FROM cloud_sync_state WHERE sync_status = 'conflict'`
|
|
281
|
+
).get();
|
|
282
|
+
const pushCursor = this.db.prepare(
|
|
283
|
+
`SELECT cursor_value FROM cloud_sync_cursors WHERE direction = 'push'`
|
|
284
|
+
).get();
|
|
285
|
+
const pullCursor = this.db.prepare(
|
|
286
|
+
`SELECT cursor_value FROM cloud_sync_cursors WHERE direction = 'pull'`
|
|
287
|
+
).get();
|
|
288
|
+
let untrackedCount = 0;
|
|
289
|
+
for (const table of SYNCABLE_TABLES) {
|
|
290
|
+
if (!this.tableExists(table)) continue;
|
|
291
|
+
const pk = PK_COLUMN[table];
|
|
292
|
+
const row = this.db.prepare(
|
|
293
|
+
`SELECT COUNT(*) as count FROM ${table} t
|
|
294
|
+
WHERE NOT EXISTS (
|
|
295
|
+
SELECT 1 FROM cloud_sync_state s
|
|
296
|
+
WHERE s.table_name = ? AND s.row_id = t.${pk}
|
|
297
|
+
)`
|
|
298
|
+
).get(table);
|
|
299
|
+
untrackedCount += row?.count ?? 0;
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
connected: this.config.enabled,
|
|
303
|
+
lastPushAt: pushCursor?.cursor_value ?? null,
|
|
304
|
+
lastPullAt: pullCursor?.cursor_value ?? null,
|
|
305
|
+
pendingPushCount: (pendingPush?.count ?? 0) + untrackedCount,
|
|
306
|
+
pendingPullCount: 0,
|
|
307
|
+
// Can't know without asking the server
|
|
308
|
+
conflictCount: conflicts?.count ?? 0,
|
|
309
|
+
endpoint: this.config.endpoint
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
// --- Internal: Table helpers ---
|
|
313
|
+
tableExists(name) {
|
|
314
|
+
const row = this.db.prepare(`SELECT 1 FROM sqlite_master WHERE type='table' AND name=?`).get(name);
|
|
315
|
+
return !!row;
|
|
316
|
+
}
|
|
317
|
+
ensureSyncTables() {
|
|
318
|
+
this.db.exec(`
|
|
319
|
+
CREATE TABLE IF NOT EXISTS cloud_sync_state (
|
|
320
|
+
table_name TEXT NOT NULL,
|
|
321
|
+
row_id TEXT NOT NULL,
|
|
322
|
+
last_pushed_at INTEGER,
|
|
323
|
+
last_pushed_version INTEGER,
|
|
324
|
+
last_pulled_at INTEGER,
|
|
325
|
+
last_pulled_version INTEGER,
|
|
326
|
+
sync_status TEXT NOT NULL DEFAULT 'pending',
|
|
327
|
+
push_error TEXT,
|
|
328
|
+
push_attempts INTEGER NOT NULL DEFAULT 0,
|
|
329
|
+
PRIMARY KEY (table_name, row_id)
|
|
330
|
+
);
|
|
331
|
+
CREATE TABLE IF NOT EXISTS cloud_sync_cursors (
|
|
332
|
+
direction TEXT NOT NULL PRIMARY KEY,
|
|
333
|
+
cursor_value TEXT NOT NULL,
|
|
334
|
+
updated_at INTEGER NOT NULL
|
|
335
|
+
);
|
|
336
|
+
`);
|
|
337
|
+
}
|
|
338
|
+
// --- Internal: Collect pending entities ---
|
|
339
|
+
collectPendingEntities() {
|
|
340
|
+
this.ensureSyncTables();
|
|
341
|
+
const entities = [];
|
|
342
|
+
for (const table of SYNCABLE_TABLES) {
|
|
343
|
+
if (!this.tableExists(table)) continue;
|
|
344
|
+
const pk = PK_COLUMN[table];
|
|
345
|
+
const versionCol = VERSION_COLUMN[table];
|
|
346
|
+
const rows = this.db.prepare(
|
|
347
|
+
`SELECT t.* FROM ${table} t
|
|
348
|
+
WHERE NOT EXISTS (
|
|
349
|
+
SELECT 1 FROM cloud_sync_state s
|
|
350
|
+
WHERE s.table_name = ? AND s.row_id = t.${pk} AND s.sync_status = 'synced'
|
|
351
|
+
)
|
|
352
|
+
LIMIT ?`
|
|
353
|
+
).all(table, this.config.batchSize * 2);
|
|
354
|
+
for (const row of rows) {
|
|
355
|
+
const id = String(row[pk]);
|
|
356
|
+
const version = Number(row[versionCol] ?? 0);
|
|
357
|
+
const tier = this.getRowTier(table, row);
|
|
358
|
+
const data = this.projectByTier(table, row, tier);
|
|
359
|
+
entities.push({ table, id, version, tier, data });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return entities;
|
|
363
|
+
}
|
|
364
|
+
// --- Internal: Generational projection ---
|
|
365
|
+
getRowTier(table, row) {
|
|
366
|
+
if (table !== "frames") return "young";
|
|
367
|
+
const createdAt = Number(row.created_at ?? 0);
|
|
368
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
369
|
+
const ageDays = (now - createdAt) / 86400;
|
|
370
|
+
if (ageDays <= this.config.generationalPolicy.youngMaxAgeDays) {
|
|
371
|
+
return "young";
|
|
372
|
+
}
|
|
373
|
+
if (ageDays <= this.config.generationalPolicy.matureMaxAgeDays) {
|
|
374
|
+
return "mature";
|
|
375
|
+
}
|
|
376
|
+
return "old";
|
|
377
|
+
}
|
|
378
|
+
projectByTier(table, row, tier) {
|
|
379
|
+
if (table !== "frames") return row;
|
|
380
|
+
if (tier === "young") return row;
|
|
381
|
+
if (tier === "mature") {
|
|
382
|
+
return {
|
|
383
|
+
frame_id: row.frame_id,
|
|
384
|
+
run_id: row.run_id,
|
|
385
|
+
project_id: row.project_id,
|
|
386
|
+
name: row.name,
|
|
387
|
+
state: row.state,
|
|
388
|
+
digest_text: row.digest_text,
|
|
389
|
+
digest_json: row.digest_json,
|
|
390
|
+
importance_score: row.importance_score,
|
|
391
|
+
created_at: row.created_at,
|
|
392
|
+
closed_at: row.closed_at
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
frame_id: row.frame_id,
|
|
397
|
+
project_id: row.project_id,
|
|
398
|
+
name: row.name,
|
|
399
|
+
state: row.state,
|
|
400
|
+
created_at: row.created_at,
|
|
401
|
+
closed_at: row.closed_at
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
// --- Internal: Apply pulled entities ---
|
|
405
|
+
applyPulledEntities(entities) {
|
|
406
|
+
let applied = 0;
|
|
407
|
+
let conflicts = 0;
|
|
408
|
+
const applyAll = this.db.transaction(() => {
|
|
409
|
+
for (const entity of entities) {
|
|
410
|
+
const pk = PK_COLUMN[entity.table];
|
|
411
|
+
const existing = this.db.prepare(
|
|
412
|
+
`SELECT ${pk} as id, ${VERSION_COLUMN[entity.table]} as version FROM ${entity.table} WHERE ${pk} = ?`
|
|
413
|
+
).get(String(entity.data[pk]));
|
|
414
|
+
if (existing) {
|
|
415
|
+
if (entity.version > existing.version) {
|
|
416
|
+
this.upsertRow(entity.table, entity.data, pk);
|
|
417
|
+
this.markPulled(
|
|
418
|
+
entity.table,
|
|
419
|
+
String(entity.data[pk]),
|
|
420
|
+
entity.version
|
|
421
|
+
);
|
|
422
|
+
applied++;
|
|
423
|
+
} else {
|
|
424
|
+
conflicts++;
|
|
425
|
+
this.updateSyncState(
|
|
426
|
+
entity.table,
|
|
427
|
+
String(entity.data[pk]),
|
|
428
|
+
"conflict"
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
} else {
|
|
432
|
+
this.upsertRow(entity.table, entity.data, pk);
|
|
433
|
+
this.markPulled(
|
|
434
|
+
entity.table,
|
|
435
|
+
String(entity.data[pk]),
|
|
436
|
+
entity.version
|
|
437
|
+
);
|
|
438
|
+
applied++;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
applyAll();
|
|
443
|
+
return { applied, conflicts };
|
|
444
|
+
}
|
|
445
|
+
upsertRow(table, data, pk) {
|
|
446
|
+
const valid = VALID_COLUMNS[table];
|
|
447
|
+
const safeEntries = Object.entries(data).filter(([k]) => valid.has(k));
|
|
448
|
+
if (safeEntries.length === 0) return;
|
|
449
|
+
const keys = safeEntries.map(([k]) => k);
|
|
450
|
+
const values = safeEntries.map(([, v]) => v);
|
|
451
|
+
const placeholders = keys.map(() => "?").join(", ");
|
|
452
|
+
const columns = keys.join(", ");
|
|
453
|
+
const updates = keys.filter((k) => k !== pk).map((k) => `${k} = excluded.${k}`).join(", ");
|
|
454
|
+
this.db.prepare(
|
|
455
|
+
`INSERT INTO ${table} (${columns}) VALUES (${placeholders})
|
|
456
|
+
ON CONFLICT(${pk}) DO UPDATE SET ${updates}`
|
|
457
|
+
).run(...values);
|
|
458
|
+
}
|
|
459
|
+
// --- Internal: Sync state management ---
|
|
460
|
+
markPushed(table, rowId, version) {
|
|
461
|
+
const now = Date.now();
|
|
462
|
+
this.db.prepare(
|
|
463
|
+
`INSERT INTO cloud_sync_state (table_name, row_id, last_pushed_at, last_pushed_version, sync_status, push_attempts)
|
|
464
|
+
VALUES (?, ?, ?, ?, 'synced', 0)
|
|
465
|
+
ON CONFLICT(table_name, row_id) DO UPDATE SET
|
|
466
|
+
last_pushed_at = ?, last_pushed_version = ?, sync_status = 'synced', push_error = NULL, push_attempts = 0`
|
|
467
|
+
).run(table, rowId, now, version, now, version);
|
|
468
|
+
}
|
|
469
|
+
markPulled(table, rowId, version) {
|
|
470
|
+
const now = Date.now();
|
|
471
|
+
this.db.prepare(
|
|
472
|
+
`INSERT INTO cloud_sync_state (table_name, row_id, last_pulled_at, last_pulled_version, sync_status)
|
|
473
|
+
VALUES (?, ?, ?, ?, 'synced')
|
|
474
|
+
ON CONFLICT(table_name, row_id) DO UPDATE SET
|
|
475
|
+
last_pulled_at = ?, last_pulled_version = ?, sync_status = 'synced'`
|
|
476
|
+
).run(table, rowId, now, version, now, version);
|
|
477
|
+
}
|
|
478
|
+
updateSyncState(table, rowId, status) {
|
|
479
|
+
this.db.prepare(
|
|
480
|
+
`INSERT INTO cloud_sync_state (table_name, row_id, sync_status)
|
|
481
|
+
VALUES (?, ?, ?)
|
|
482
|
+
ON CONFLICT(table_name, row_id) DO UPDATE SET sync_status = ?`
|
|
483
|
+
).run(table, rowId, status, status);
|
|
484
|
+
}
|
|
485
|
+
recordPushError(table, rowId, error) {
|
|
486
|
+
this.db.prepare(
|
|
487
|
+
`INSERT INTO cloud_sync_state (table_name, row_id, sync_status, push_error, push_attempts)
|
|
488
|
+
VALUES (?, ?, 'pending', ?, 1)
|
|
489
|
+
ON CONFLICT(table_name, row_id) DO UPDATE SET
|
|
490
|
+
push_error = ?, push_attempts = push_attempts + 1`
|
|
491
|
+
).run(table, rowId, error, error);
|
|
492
|
+
}
|
|
493
|
+
// --- Internal: Cursor management ---
|
|
494
|
+
getCursor(direction) {
|
|
495
|
+
const row = this.db.prepare(
|
|
496
|
+
`SELECT cursor_value FROM cloud_sync_cursors WHERE direction = ?`
|
|
497
|
+
).get(direction);
|
|
498
|
+
return row?.cursor_value ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
499
|
+
}
|
|
500
|
+
setCursor(direction, cursor) {
|
|
501
|
+
this.db.prepare(
|
|
502
|
+
`INSERT INTO cloud_sync_cursors (direction, cursor_value, updated_at)
|
|
503
|
+
VALUES (?, ?, ?)
|
|
504
|
+
ON CONFLICT(direction) DO UPDATE SET cursor_value = ?, updated_at = ?`
|
|
505
|
+
).run(direction, cursor, Date.now(), cursor, Date.now());
|
|
506
|
+
}
|
|
507
|
+
// --- Internal: HTTP ---
|
|
508
|
+
async sendPush(request) {
|
|
509
|
+
return this.httpPost("/v1/sync/push", request);
|
|
510
|
+
}
|
|
511
|
+
async sendPull(request) {
|
|
512
|
+
return this.httpPost("/v1/sync/pull", request);
|
|
513
|
+
}
|
|
514
|
+
async httpPost(path, body) {
|
|
515
|
+
let lastError;
|
|
516
|
+
for (let attempt = 0; attempt < this.config.retryAttempts; attempt++) {
|
|
517
|
+
try {
|
|
518
|
+
const controller = new AbortController();
|
|
519
|
+
const timeout = setTimeout(
|
|
520
|
+
() => controller.abort(),
|
|
521
|
+
this.config.timeoutMs
|
|
522
|
+
);
|
|
523
|
+
try {
|
|
524
|
+
const response = await this.fetchFn(
|
|
525
|
+
`${this.config.endpoint}${path}`,
|
|
526
|
+
{
|
|
527
|
+
method: "POST",
|
|
528
|
+
headers: {
|
|
529
|
+
"Content-Type": "application/json",
|
|
530
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
531
|
+
"X-Client-Id": this.config.clientId
|
|
532
|
+
},
|
|
533
|
+
body: JSON.stringify(body),
|
|
534
|
+
signal: controller.signal
|
|
535
|
+
}
|
|
536
|
+
);
|
|
537
|
+
if (!response.ok) {
|
|
538
|
+
lastError = `HTTP ${response.status}: ${response.statusText}`;
|
|
539
|
+
if (response.status >= 400 && response.status < 500) {
|
|
540
|
+
logger.error("Cloud sync client error", {
|
|
541
|
+
path,
|
|
542
|
+
status: response.status
|
|
543
|
+
});
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
return await response.json();
|
|
549
|
+
} finally {
|
|
550
|
+
clearTimeout(timeout);
|
|
551
|
+
}
|
|
552
|
+
} catch (error) {
|
|
553
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
554
|
+
if (attempt < this.config.retryAttempts - 1) {
|
|
555
|
+
const delay = this.config.retryBaseDelayMs * Math.pow(2, attempt);
|
|
556
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
logger.warn("Cloud sync HTTP failed after retries", {
|
|
561
|
+
path,
|
|
562
|
+
error: lastError
|
|
563
|
+
});
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
// --- Internal: Checksum ---
|
|
567
|
+
computeChecksum(entities) {
|
|
568
|
+
const ids = entities.map((e) => `${e.table}:${e.id}`).sort().join(",");
|
|
569
|
+
return createHash("sha256").update(ids).digest("hex");
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
export {
|
|
573
|
+
CloudSyncEngine
|
|
574
|
+
};
|
|
@@ -8,7 +8,11 @@ import {
|
|
|
8
8
|
PutObjectCommand,
|
|
9
9
|
GetObjectCommand
|
|
10
10
|
} from "@aws-sdk/client-s3";
|
|
11
|
-
|
|
11
|
+
let createRedisClient;
|
|
12
|
+
try {
|
|
13
|
+
createRedisClient = require("redis").createClient;
|
|
14
|
+
} catch {
|
|
15
|
+
}
|
|
12
16
|
import { Pool } from "pg";
|
|
13
17
|
import * as zlib from "zlib";
|
|
14
18
|
import { promisify } from "util";
|
|
@@ -0,0 +1,43 @@
|
|
|
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 MASTER_TASKS_TEMPLATE = `# Master Tasks
|
|
6
|
+
|
|
7
|
+
> Single source of truth for what to build. Local-first, optionally syncs to Linear/GitHub.
|
|
8
|
+
> Powers \\\`/next\\\` task selection. Referenced by CLAUDE.md and AGENTS.md.
|
|
9
|
+
|
|
10
|
+
## Rules
|
|
11
|
+
|
|
12
|
+
1. **Local-first**: This file is canonical. Linear/GH are downstream mirrors, not sources.
|
|
13
|
+
2. **One owner**: Every task has exactly one owner. \\\`@me\\\` = you, \\\`@agent\\\` = dispatch to sub-agent, \\\`@defer\\\` = not assigned.
|
|
14
|
+
3. **Priority tiers**: \\\`P0\\\` now (blocking), \\\`P1\\\` this week, \\\`P2\\\` next sprint, \\\`P3\\\` someday.
|
|
15
|
+
4. **Status flow**: \\\`todo\\\` \u2192 \\\`active\\\` \u2192 \\\`done\\\` | \\\`blocked\\\` | \\\`cut\\\`.
|
|
16
|
+
5. **Sync targets**: \\\`local\\\` (stays here), \\\`linear\\\` (create/update Linear issue), \\\`gh\\\` (GitHub issue/PR).
|
|
17
|
+
6. **Agent /next reads P0 first, then P1**: Skip blocked, done, cut. Prefer @agent tasks unless @me explicitly set.
|
|
18
|
+
7. **Keep it scannable**: One line per task in the table. Details go in notes column or linked doc.
|
|
19
|
+
8. **Update on completion**: Mark done with date. Don't delete \u2014 move to Done section monthly.
|
|
20
|
+
|
|
21
|
+
## Active Tasks
|
|
22
|
+
|
|
23
|
+
| id | P | status | owner | sync | task | branch/PR | notes |
|
|
24
|
+
|----|---|--------|-------|------|------|-----------|-------|
|
|
25
|
+
|
|
26
|
+
## Done (archive monthly)
|
|
27
|
+
|
|
28
|
+
_Move completed tasks here at end of month._
|
|
29
|
+
|
|
30
|
+
<!-- Template row:
|
|
31
|
+
| Txx | Px | todo | @me/@agent/@defer | local/linear/gh | description | branch/PR | notes |
|
|
32
|
+
-->
|
|
33
|
+
`;
|
|
34
|
+
const TASKS_CONFIG_TEMPLATE = {
|
|
35
|
+
linear: { team: "", project: "" },
|
|
36
|
+
github: { repo: "" },
|
|
37
|
+
defaultSync: "local",
|
|
38
|
+
defaultOwner: "@me"
|
|
39
|
+
};
|
|
40
|
+
export {
|
|
41
|
+
MASTER_TASKS_TEMPLATE,
|
|
42
|
+
TASKS_CONFIG_TEMPLATE
|
|
43
|
+
};
|