@stackmemoryai/stackmemory 1.12.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +131 -64
- package/README.md +3 -1
- package/bin/claude-sm +16 -1
- package/bin/claude-smd +16 -1
- package/bin/codex-smd +16 -1
- package/bin/gemini-sm +16 -1
- package/bin/hermes-sm +21 -0
- package/bin/hermes-smd +21 -0
- package/bin/opencode-sm +16 -1
- package/dist/src/cli/codex-sm.js +51 -11
- package/dist/src/cli/commands/brain.js +206 -0
- package/dist/src/cli/commands/company-os.js +184 -0
- package/dist/src/cli/commands/context.js +5 -0
- package/dist/src/cli/commands/operator.js +127 -0
- package/dist/src/cli/commands/orchestrate.js +2 -0
- package/dist/src/cli/commands/orchestrator.js +3 -2
- package/dist/src/cli/commands/patterns.js +254 -0
- package/dist/src/cli/commands/portal.js +161 -0
- package/dist/src/cli/commands/scaffold.js +92 -0
- package/dist/src/cli/commands/setup.js +1 -4
- package/dist/src/cli/commands/sync.js +253 -0
- package/dist/src/cli/commands/tasks.js +130 -1
- package/dist/src/cli/commands/vision.js +221 -0
- package/dist/src/cli/hermes-sm.js +224 -0
- package/dist/src/cli/index.js +15 -10
- package/dist/src/cli/utils/real-cli-bin.js +72 -0
- package/dist/src/core/brain/brain-store.js +187 -0
- package/dist/src/core/brain/brain-sync.js +193 -0
- package/dist/src/core/brain/index.js +78 -0
- package/dist/src/core/brain/types.js +10 -0
- package/dist/src/core/cache/token-estimator.js +24 -1
- package/dist/src/core/config/feature-flags.js +2 -6
- package/dist/src/core/context/frame-database.js +44 -0
- package/dist/src/core/context/recursive-context-manager.js +1 -1
- package/dist/src/core/context/rehydration.js +2 -1
- package/dist/src/core/database/sqlite-adapter.js +14 -1
- package/dist/src/core/models/model-router.js +33 -1
- package/dist/src/core/models/provider-pricing.js +58 -4
- package/dist/src/core/patterns/index.js +22 -0
- package/dist/src/core/patterns/pattern-applier.js +39 -0
- package/dist/src/core/patterns/pattern-observer.js +157 -0
- package/dist/src/core/patterns/pattern-store.js +259 -0
- package/dist/src/core/patterns/types.js +19 -0
- package/dist/src/core/retrieval/llm-context-retrieval.js +5 -4
- package/dist/src/core/retrieval/unified-context-assembler.js +11 -66
- package/dist/src/core/skill-packs/types.js +14 -1
- package/dist/src/core/storage/cloud-sync-manager.js +116 -0
- package/dist/src/core/storage/cloud-sync.js +574 -0
- package/dist/src/core/storage/two-tier-storage.js +5 -1
- package/dist/src/core/tasks/master-tasks-template.js +43 -0
- package/dist/src/core/tasks/md-task-parser.js +138 -0
- package/dist/src/core/vision/index.js +27 -0
- package/dist/src/core/vision/signals.js +79 -0
- package/dist/src/core/vision/types.js +22 -0
- package/dist/src/core/vision/vision-file.js +146 -0
- package/dist/src/core/vision/vision-loop.js +220 -0
- package/dist/src/core/wiki/wiki-compiler.js +103 -1
- package/dist/src/daemon/daemon-config.js +45 -0
- package/dist/src/daemon/services/desire-path-service.js +566 -0
- package/dist/src/daemon/services/research-stream-service.js +320 -0
- package/dist/src/daemon/services/telemetry-service.js +192 -0
- package/dist/src/daemon/unified-daemon.js +28 -1
- package/dist/src/features/browser/cli-browser-agent.js +417 -0
- package/dist/src/features/browser/stagehand-workflows.js +578 -0
- package/dist/src/features/operator/adapter-factory.js +62 -0
- package/dist/src/features/operator/browser-adapter.js +109 -0
- package/dist/src/features/operator/desktop-adapter.js +125 -0
- package/dist/src/features/operator/index.js +39 -0
- package/dist/src/features/operator/llm-decision.js +137 -0
- package/dist/src/features/operator/operator-logger.js +92 -0
- package/dist/src/features/operator/overnight-runner.js +327 -0
- package/dist/src/features/operator/screen-adapter.js +91 -0
- package/dist/src/features/operator/session-manager.js +127 -0
- package/dist/src/features/operator/state-machine.js +227 -0
- package/dist/src/features/operator/task-queue.js +81 -0
- package/dist/src/features/portal/index.js +26 -0
- package/dist/src/features/portal/server.js +240 -0
- package/dist/src/features/portal/types.js +14 -0
- package/dist/src/features/portal/ui.js +195 -0
- package/dist/src/features/tasks/task-aware-context.js +2 -1
- package/dist/src/features/tui/simple-monitor.js +0 -23
- package/dist/src/features/tui/swarm-monitor.js +8 -66
- package/dist/src/{integrations/diffmem/index.js → features/web/client/hooks/use-socket.js} +6 -5
- package/dist/src/features/web/client/lib/utils.js +12 -0
- package/dist/src/features/web/client/stores/session-store.js +12 -0
- package/dist/src/features/web/server/gcp-billing.js +76 -0
- package/dist/src/features/web/server/index.js +10 -0
- package/dist/src/features/web/server/spend-calculator.js +228 -0
- package/dist/src/hooks/schemas.js +4 -1
- package/dist/src/integrations/anthropic/client.js +3 -2
- package/dist/src/integrations/claude-code/agent-bridge.js +0 -3
- package/dist/src/integrations/claude-code/subagent-client.js +218 -11
- package/dist/src/integrations/claude-code/task-coordinator.js +2 -1
- package/dist/src/integrations/linear/webhook-retry.js +196 -0
- package/dist/src/integrations/linear/webhook-server.js +18 -22
- package/dist/src/integrations/mcp/handlers/cloud-sync-handlers.js +101 -0
- package/dist/src/integrations/mcp/handlers/index.js +27 -52
- package/dist/src/integrations/mcp/server.js +122 -335
- package/dist/src/integrations/mcp/tool-alias-registry.js +0 -73
- package/dist/src/integrations/mcp/tool-definitions.js +111 -510
- package/dist/src/mcp/stackmemory-mcp-server.js +404 -379
- package/dist/src/orchestrators/multimodal/determinism.js +2 -1
- package/dist/src/orchestrators/multimodal/harness.js +2 -1
- package/dist/src/skills/recursive-agent-orchestrator.js +2 -4
- package/dist/src/utils/process-cleanup.js +1 -7
- package/docs/README.md +42 -0
- package/docs/guides/README_INSTALL.md +208 -0
- package/package.json +18 -9
- package/scripts/claude-code-wrapper.sh +11 -0
- package/scripts/claude-sm-setup.sh +12 -1
- package/scripts/codex-wrapper.sh +11 -0
- package/scripts/git-hooks/branch-context-manager.sh +11 -0
- package/scripts/git-hooks/post-checkout-stackmemory.sh +11 -0
- package/scripts/git-hooks/post-commit-stackmemory.sh +11 -0
- package/scripts/git-hooks/pre-commit-stackmemory.sh +11 -0
- package/scripts/hooks/cleanup-shell.sh +12 -1
- package/scripts/hooks/task-complete.sh +12 -1
- package/scripts/install-code-execution-hooks.sh +12 -1
- package/scripts/install-sweep-hook.sh +12 -0
- package/scripts/install.sh +11 -0
- package/scripts/opencode-wrapper.sh +11 -0
- package/scripts/portal/cloud-init.yaml +69 -0
- package/scripts/portal/setup.sh +69 -0
- package/scripts/portal/stackmemory-portal.service +34 -0
- package/scripts/setup-claude-integration.sh +12 -1
- package/scripts/smoke-init-db.sh +23 -0
- package/scripts/stackmemory-daemon.sh +11 -0
- package/scripts/verify-dist.cjs +11 -4
- package/dist/src/cli/commands/ralph.js +0 -1053
- package/dist/src/hooks/diffmem-hooks.js +0 -376
- package/dist/src/integrations/diffmem/client.js +0 -208
- package/dist/src/integrations/diffmem/config.js +0 -14
- package/dist/src/integrations/greptile/client.js +0 -101
- package/dist/src/integrations/greptile/config.js +0 -14
- package/dist/src/integrations/greptile/index.js +0 -11
- package/dist/src/integrations/mcp/handlers/cross-search-handlers.js +0 -188
- package/dist/src/integrations/mcp/handlers/diffmem-handlers.js +0 -455
- package/dist/src/integrations/mcp/handlers/greptile-handlers.js +0 -456
- package/dist/src/integrations/mcp/handlers/provider-handlers.js +0 -227
- package/dist/src/integrations/ralph/bridge/ralph-stackmemory-bridge.js +0 -863
- package/dist/src/integrations/ralph/context/context-budget-manager.js +0 -308
- package/dist/src/integrations/ralph/context/stackmemory-context-loader.js +0 -354
- package/dist/src/integrations/ralph/index.js +0 -17
- package/dist/src/integrations/ralph/learning/pattern-learner.js +0 -416
- package/dist/src/integrations/ralph/lifecycle/iteration-lifecycle.js +0 -448
- package/dist/src/integrations/ralph/loopmax.js +0 -488
- package/dist/src/integrations/ralph/monitoring/swarm-dashboard.js +0 -293
- package/dist/src/integrations/ralph/monitoring/swarm-registry.js +0 -107
- package/dist/src/integrations/ralph/orchestration/multi-loop-orchestrator.js +0 -508
- package/dist/src/integrations/ralph/patterns/compounding-engineering-pattern.js +0 -407
- package/dist/src/integrations/ralph/patterns/extended-coherence-sessions.js +0 -495
- package/dist/src/integrations/ralph/patterns/oracle-worker-pattern.js +0 -387
- package/dist/src/integrations/ralph/performance/performance-optimizer.js +0 -357
- package/dist/src/integrations/ralph/recovery/crash-recovery.js +0 -461
- package/dist/src/integrations/ralph/state/state-reconciler.js +0 -420
- package/dist/src/integrations/ralph/swarm/git-workflow-manager.js +0 -444
- package/dist/src/integrations/ralph/swarm/swarm-coordinator.js +0 -1005
- package/dist/src/integrations/ralph/visualization/ralph-debugger.js +0 -635
- package/scripts/ralph-loop-implementation.js +0 -404
- /package/dist/src/{integrations/diffmem/types.js → core/storage/cloud-sync-types.js} +0 -0
- /package/dist/src/{integrations/greptile → features/operator}/types.js +0 -0
- /package/dist/src/{integrations/ralph/types.js → features/web/client/next-env.d.js} +0 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
import { randomUUID } from "crypto";
|
|
6
|
+
import {
|
|
7
|
+
BRAIN_TABLE,
|
|
8
|
+
DEFAULT_BRAIN_LIMIT
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
class BrainStore {
|
|
11
|
+
db;
|
|
12
|
+
workspaceId;
|
|
13
|
+
projectId;
|
|
14
|
+
constructor(db, scope) {
|
|
15
|
+
this.db = db;
|
|
16
|
+
this.projectId = scope.projectId;
|
|
17
|
+
this.workspaceId = scope.workspaceId ?? "";
|
|
18
|
+
this.ensureTable();
|
|
19
|
+
}
|
|
20
|
+
/** Create the brain_entries table + indexes if they don't exist. */
|
|
21
|
+
ensureTable() {
|
|
22
|
+
this.db.exec(`
|
|
23
|
+
CREATE TABLE IF NOT EXISTS ${BRAIN_TABLE} (
|
|
24
|
+
entry_id TEXT PRIMARY KEY,
|
|
25
|
+
workspace_id TEXT NOT NULL DEFAULT '',
|
|
26
|
+
project_id TEXT NOT NULL,
|
|
27
|
+
agent TEXT NOT NULL DEFAULT 'claude',
|
|
28
|
+
kind TEXT NOT NULL DEFAULT 'note',
|
|
29
|
+
title TEXT NOT NULL,
|
|
30
|
+
summary TEXT NOT NULL DEFAULT '',
|
|
31
|
+
conclusion TEXT NOT NULL DEFAULT '',
|
|
32
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
33
|
+
refs TEXT NOT NULL DEFAULT '[]',
|
|
34
|
+
confidence REAL NOT NULL DEFAULT 0.7,
|
|
35
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
36
|
+
superseded_by TEXT,
|
|
37
|
+
created_at INTEGER NOT NULL,
|
|
38
|
+
updated_at INTEGER NOT NULL
|
|
39
|
+
);
|
|
40
|
+
CREATE INDEX IF NOT EXISTS idx_brain_project ON ${BRAIN_TABLE}(project_id, created_at DESC);
|
|
41
|
+
CREATE INDEX IF NOT EXISTS idx_brain_workspace ON ${BRAIN_TABLE}(workspace_id, created_at DESC);
|
|
42
|
+
`);
|
|
43
|
+
}
|
|
44
|
+
/** Record (or upsert by entryId) a brain entry. */
|
|
45
|
+
record(input) {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
const entry = {
|
|
48
|
+
entryId: input.entryId ?? randomUUID(),
|
|
49
|
+
workspaceId: this.workspaceId,
|
|
50
|
+
projectId: this.projectId,
|
|
51
|
+
agent: input.agent ?? "claude",
|
|
52
|
+
kind: input.kind ?? "note",
|
|
53
|
+
title: input.title,
|
|
54
|
+
summary: input.summary ?? "",
|
|
55
|
+
conclusion: input.conclusion ?? "",
|
|
56
|
+
tags: input.tags ?? [],
|
|
57
|
+
refs: input.refs ?? [],
|
|
58
|
+
confidence: clamp01(input.confidence ?? 0.7),
|
|
59
|
+
status: "active",
|
|
60
|
+
createdAt: input.createdAt ?? now,
|
|
61
|
+
updatedAt: input.updatedAt ?? now
|
|
62
|
+
};
|
|
63
|
+
this.db.prepare(
|
|
64
|
+
`INSERT INTO ${BRAIN_TABLE}
|
|
65
|
+
(entry_id, workspace_id, project_id, agent, kind, title, summary,
|
|
66
|
+
conclusion, tags, refs, confidence, status, superseded_by,
|
|
67
|
+
created_at, updated_at)
|
|
68
|
+
VALUES (@entryId, @workspaceId, @projectId, @agent, @kind, @title,
|
|
69
|
+
@summary, @conclusion, @tags, @refs, @confidence, @status,
|
|
70
|
+
NULL, @createdAt, @updatedAt)
|
|
71
|
+
ON CONFLICT(entry_id) DO UPDATE SET
|
|
72
|
+
agent = excluded.agent,
|
|
73
|
+
kind = excluded.kind,
|
|
74
|
+
title = excluded.title,
|
|
75
|
+
summary = excluded.summary,
|
|
76
|
+
conclusion = excluded.conclusion,
|
|
77
|
+
tags = excluded.tags,
|
|
78
|
+
refs = excluded.refs,
|
|
79
|
+
confidence = excluded.confidence,
|
|
80
|
+
updated_at = excluded.updated_at`
|
|
81
|
+
).run({
|
|
82
|
+
...entry,
|
|
83
|
+
tags: JSON.stringify(entry.tags),
|
|
84
|
+
refs: JSON.stringify(entry.refs)
|
|
85
|
+
});
|
|
86
|
+
return entry;
|
|
87
|
+
}
|
|
88
|
+
/** Fetch a single entry by id (or unique prefix). */
|
|
89
|
+
get(entryId) {
|
|
90
|
+
const row = this.db.prepare(
|
|
91
|
+
`SELECT * FROM ${BRAIN_TABLE} WHERE entry_id = ? OR entry_id LIKE ? LIMIT 1`
|
|
92
|
+
).get(entryId, `${entryId}%`);
|
|
93
|
+
return row ? rowToEntry(row) : null;
|
|
94
|
+
}
|
|
95
|
+
/** Search entries by scope + free text, newest first. */
|
|
96
|
+
recall(query = {}) {
|
|
97
|
+
const where = [];
|
|
98
|
+
const params = [];
|
|
99
|
+
if (query.org) {
|
|
100
|
+
where.push("workspace_id = ?");
|
|
101
|
+
params.push(this.workspaceId);
|
|
102
|
+
} else {
|
|
103
|
+
where.push("project_id = ?");
|
|
104
|
+
params.push(query.projectId ?? this.projectId);
|
|
105
|
+
}
|
|
106
|
+
if (!query.includeSuperseded) {
|
|
107
|
+
where.push("status = 'active'");
|
|
108
|
+
}
|
|
109
|
+
if (query.agent) {
|
|
110
|
+
where.push("agent = ?");
|
|
111
|
+
params.push(query.agent);
|
|
112
|
+
}
|
|
113
|
+
if (query.kind) {
|
|
114
|
+
where.push("kind = ?");
|
|
115
|
+
params.push(query.kind);
|
|
116
|
+
}
|
|
117
|
+
if (query.since) {
|
|
118
|
+
where.push("created_at >= ?");
|
|
119
|
+
params.push(query.since);
|
|
120
|
+
}
|
|
121
|
+
if (query.text) {
|
|
122
|
+
where.push(
|
|
123
|
+
"(title LIKE ? OR summary LIKE ? OR conclusion LIKE ? OR tags LIKE ?)"
|
|
124
|
+
);
|
|
125
|
+
const like = `%${query.text}%`;
|
|
126
|
+
params.push(like, like, like, like);
|
|
127
|
+
}
|
|
128
|
+
const limit = Math.max(1, query.limit ?? DEFAULT_BRAIN_LIMIT);
|
|
129
|
+
const rows = this.db.prepare(
|
|
130
|
+
`SELECT * FROM ${BRAIN_TABLE}
|
|
131
|
+
WHERE ${where.join(" AND ")}
|
|
132
|
+
ORDER BY created_at DESC
|
|
133
|
+
LIMIT ?`
|
|
134
|
+
).all(...params, limit);
|
|
135
|
+
return rows.map(rowToEntry);
|
|
136
|
+
}
|
|
137
|
+
/** Mark `oldId` superseded by `newId`. */
|
|
138
|
+
supersede(oldId, newId) {
|
|
139
|
+
this.db.prepare(
|
|
140
|
+
`UPDATE ${BRAIN_TABLE}
|
|
141
|
+
SET status = 'superseded', superseded_by = ?, updated_at = ?
|
|
142
|
+
WHERE entry_id = ?`
|
|
143
|
+
).run(newId, Date.now(), oldId);
|
|
144
|
+
}
|
|
145
|
+
/** Count entries in scope (for status output). */
|
|
146
|
+
count(org = false) {
|
|
147
|
+
const col = org ? "workspace_id" : "project_id";
|
|
148
|
+
const val = org ? this.workspaceId : this.projectId;
|
|
149
|
+
const row = this.db.prepare(`SELECT COUNT(*) AS n FROM ${BRAIN_TABLE} WHERE ${col} = ?`).get(val);
|
|
150
|
+
return row.n;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function rowToEntry(row) {
|
|
154
|
+
const entry = {
|
|
155
|
+
entryId: row.entry_id,
|
|
156
|
+
workspaceId: row.workspace_id,
|
|
157
|
+
projectId: row.project_id,
|
|
158
|
+
agent: row.agent,
|
|
159
|
+
kind: row.kind,
|
|
160
|
+
title: row.title,
|
|
161
|
+
summary: row.summary,
|
|
162
|
+
conclusion: row.conclusion,
|
|
163
|
+
tags: safeParse(row.tags),
|
|
164
|
+
refs: safeParse(row.refs),
|
|
165
|
+
confidence: row.confidence,
|
|
166
|
+
status: row.status,
|
|
167
|
+
createdAt: row.created_at,
|
|
168
|
+
updatedAt: row.updated_at
|
|
169
|
+
};
|
|
170
|
+
if (row.superseded_by) entry.supersededBy = row.superseded_by;
|
|
171
|
+
return entry;
|
|
172
|
+
}
|
|
173
|
+
function safeParse(json) {
|
|
174
|
+
try {
|
|
175
|
+
const v = JSON.parse(json);
|
|
176
|
+
return Array.isArray(v) ? v.map(String) : [];
|
|
177
|
+
} catch {
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function clamp01(n) {
|
|
182
|
+
if (Number.isNaN(n)) return 0.7;
|
|
183
|
+
return Math.max(0, Math.min(1, n));
|
|
184
|
+
}
|
|
185
|
+
export {
|
|
186
|
+
BrainStore
|
|
187
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
const BRAIN_TABLE = "brain_entries";
|
|
6
|
+
class BrainSync {
|
|
7
|
+
db;
|
|
8
|
+
store;
|
|
9
|
+
config;
|
|
10
|
+
constructor(db, store, config) {
|
|
11
|
+
this.db = db;
|
|
12
|
+
this.store = store;
|
|
13
|
+
this.config = {
|
|
14
|
+
timeoutMs: 3e4,
|
|
15
|
+
batchSize: 200,
|
|
16
|
+
...config
|
|
17
|
+
};
|
|
18
|
+
this.ensureMeta();
|
|
19
|
+
}
|
|
20
|
+
ensureMeta() {
|
|
21
|
+
this.db.exec(`
|
|
22
|
+
CREATE TABLE IF NOT EXISTS brain_sync_meta (
|
|
23
|
+
direction TEXT PRIMARY KEY,
|
|
24
|
+
cursor INTEGER NOT NULL DEFAULT 0
|
|
25
|
+
);
|
|
26
|
+
`);
|
|
27
|
+
}
|
|
28
|
+
getCursor(direction) {
|
|
29
|
+
const row = this.db.prepare("SELECT cursor FROM brain_sync_meta WHERE direction = ?").get(direction);
|
|
30
|
+
return row?.cursor ?? 0;
|
|
31
|
+
}
|
|
32
|
+
setCursor(direction, cursor) {
|
|
33
|
+
this.db.prepare(
|
|
34
|
+
`INSERT INTO brain_sync_meta (direction, cursor) VALUES (?, ?)
|
|
35
|
+
ON CONFLICT(direction) DO UPDATE SET cursor = excluded.cursor`
|
|
36
|
+
).run(direction, cursor);
|
|
37
|
+
}
|
|
38
|
+
/** Push locally-updated entries to the cloud. */
|
|
39
|
+
async push() {
|
|
40
|
+
const since = this.getCursor("push");
|
|
41
|
+
const rows = this.db.prepare(
|
|
42
|
+
`SELECT * FROM ${BRAIN_TABLE} WHERE updated_at > ? ORDER BY updated_at ASC LIMIT ?`
|
|
43
|
+
).all(since, this.config.batchSize);
|
|
44
|
+
if (rows.length === 0) {
|
|
45
|
+
return { success: true, pushed: 0, pulled: 0, applied: 0 };
|
|
46
|
+
}
|
|
47
|
+
const entries = rows.map(toWireEntry);
|
|
48
|
+
const maxUpdated = Math.max(...entries.map((e) => e.updatedAt));
|
|
49
|
+
try {
|
|
50
|
+
const res = await this.post("/v1/brain/push", {
|
|
51
|
+
protocolVersion: 1,
|
|
52
|
+
clientId: this.config.clientId,
|
|
53
|
+
workspaceId: this.config.workspaceId,
|
|
54
|
+
projectId: this.config.projectId,
|
|
55
|
+
since,
|
|
56
|
+
entries
|
|
57
|
+
});
|
|
58
|
+
this.setCursor("push", Math.max(maxUpdated, res.serverCursor ?? 0));
|
|
59
|
+
return {
|
|
60
|
+
success: true,
|
|
61
|
+
pushed: res.accepted ?? entries.length,
|
|
62
|
+
pulled: 0,
|
|
63
|
+
applied: 0
|
|
64
|
+
};
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
pushed: 0,
|
|
69
|
+
pulled: 0,
|
|
70
|
+
applied: 0,
|
|
71
|
+
error: errMsg(err)
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Pull remote entries and apply them locally (newest-wins). */
|
|
76
|
+
async pull() {
|
|
77
|
+
const since = this.getCursor("pull");
|
|
78
|
+
try {
|
|
79
|
+
const res = await this.post("/v1/brain/pull", {
|
|
80
|
+
protocolVersion: 1,
|
|
81
|
+
clientId: this.config.clientId,
|
|
82
|
+
workspaceId: this.config.workspaceId,
|
|
83
|
+
projectId: this.config.projectId,
|
|
84
|
+
since,
|
|
85
|
+
limit: this.config.batchSize
|
|
86
|
+
});
|
|
87
|
+
const entries = res.entries ?? [];
|
|
88
|
+
let applied = 0;
|
|
89
|
+
let maxUpdated = since;
|
|
90
|
+
for (const remote of entries) {
|
|
91
|
+
maxUpdated = Math.max(maxUpdated, remote.updatedAt ?? 0);
|
|
92
|
+
const local = this.store.get(remote.entryId);
|
|
93
|
+
if (local && local.updatedAt >= (remote.updatedAt ?? 0)) continue;
|
|
94
|
+
this.store.record({
|
|
95
|
+
entryId: remote.entryId,
|
|
96
|
+
agent: remote.agent,
|
|
97
|
+
kind: remote.kind,
|
|
98
|
+
title: remote.title,
|
|
99
|
+
summary: remote.summary,
|
|
100
|
+
conclusion: remote.conclusion,
|
|
101
|
+
tags: remote.tags,
|
|
102
|
+
refs: remote.refs,
|
|
103
|
+
confidence: remote.confidence,
|
|
104
|
+
createdAt: remote.createdAt,
|
|
105
|
+
updatedAt: remote.updatedAt
|
|
106
|
+
});
|
|
107
|
+
applied++;
|
|
108
|
+
}
|
|
109
|
+
this.setCursor("pull", Math.max(maxUpdated, res.serverCursor ?? 0));
|
|
110
|
+
return { success: true, pushed: 0, pulled: entries.length, applied };
|
|
111
|
+
} catch (err) {
|
|
112
|
+
return {
|
|
113
|
+
success: false,
|
|
114
|
+
pushed: 0,
|
|
115
|
+
pulled: 0,
|
|
116
|
+
applied: 0,
|
|
117
|
+
error: errMsg(err)
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Push then pull in one shot. */
|
|
122
|
+
async sync() {
|
|
123
|
+
const pushed = await this.push();
|
|
124
|
+
const pulled = await this.pull();
|
|
125
|
+
const error = pushed.error ?? pulled.error;
|
|
126
|
+
return {
|
|
127
|
+
success: pushed.success && pulled.success,
|
|
128
|
+
pushed: pushed.pushed,
|
|
129
|
+
pulled: pulled.pulled,
|
|
130
|
+
applied: pulled.applied,
|
|
131
|
+
...error ? { error } : {}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async post(path, body) {
|
|
135
|
+
const controller = new AbortController();
|
|
136
|
+
const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
137
|
+
try {
|
|
138
|
+
const res = await fetch(`${this.config.endpoint}${path}`, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: {
|
|
141
|
+
"Content-Type": "application/json",
|
|
142
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
143
|
+
"X-Client-Id": this.config.clientId
|
|
144
|
+
},
|
|
145
|
+
body: JSON.stringify(body),
|
|
146
|
+
signal: controller.signal
|
|
147
|
+
});
|
|
148
|
+
if (!res.ok) {
|
|
149
|
+
throw new Error(`${res.status} ${res.statusText}`);
|
|
150
|
+
}
|
|
151
|
+
return await res.json();
|
|
152
|
+
} finally {
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function toWireEntry(row) {
|
|
158
|
+
const parse = (v) => {
|
|
159
|
+
try {
|
|
160
|
+
const a = JSON.parse(String(v ?? "[]"));
|
|
161
|
+
return Array.isArray(a) ? a.map(String) : [];
|
|
162
|
+
} catch {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
const entry = {
|
|
167
|
+
entryId: String(row["entry_id"]),
|
|
168
|
+
workspaceId: String(row["workspace_id"] ?? ""),
|
|
169
|
+
projectId: String(row["project_id"]),
|
|
170
|
+
agent: String(row["agent"]),
|
|
171
|
+
kind: String(row["kind"]),
|
|
172
|
+
title: String(row["title"]),
|
|
173
|
+
summary: String(row["summary"] ?? ""),
|
|
174
|
+
conclusion: String(row["conclusion"] ?? ""),
|
|
175
|
+
tags: parse(row["tags"]),
|
|
176
|
+
refs: parse(row["refs"]),
|
|
177
|
+
confidence: Number(row["confidence"] ?? 0.7),
|
|
178
|
+
status: String(row["status"] ?? "active"),
|
|
179
|
+
createdAt: Number(row["created_at"]),
|
|
180
|
+
updatedAt: Number(row["updated_at"])
|
|
181
|
+
};
|
|
182
|
+
if (row["superseded_by"]) entry.supersededBy = String(row["superseded_by"]);
|
|
183
|
+
return entry;
|
|
184
|
+
}
|
|
185
|
+
function errMsg(err) {
|
|
186
|
+
if (err instanceof Error) {
|
|
187
|
+
return err.name === "AbortError" ? "request timed out" : err.message;
|
|
188
|
+
}
|
|
189
|
+
return String(err);
|
|
190
|
+
}
|
|
191
|
+
export {
|
|
192
|
+
BrainSync
|
|
193
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
import { homedir, hostname } from "os";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { existsSync, readFileSync, mkdirSync } from "fs";
|
|
8
|
+
import { createHash } from "crypto";
|
|
9
|
+
import Database from "better-sqlite3";
|
|
10
|
+
import { BrainStore } from "./brain-store.js";
|
|
11
|
+
import { BrainSync } from "./brain-sync.js";
|
|
12
|
+
import { BrainStore as BrainStore2 } from "./brain-store.js";
|
|
13
|
+
import { BrainSync as BrainSync2 } from "./brain-sync.js";
|
|
14
|
+
export * from "./types.js";
|
|
15
|
+
const DEFAULT_ENDPOINT = "https://provenant-api.jpwu03.workers.dev";
|
|
16
|
+
function readAuth() {
|
|
17
|
+
const cfgPath = join(homedir(), ".stackmemory", "config.json");
|
|
18
|
+
if (!existsSync(cfgPath)) return {};
|
|
19
|
+
try {
|
|
20
|
+
const cfg = JSON.parse(readFileSync(cfgPath, "utf8"));
|
|
21
|
+
return cfg.auth ?? {};
|
|
22
|
+
} catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function hashId(input) {
|
|
27
|
+
return createHash("sha256").update(input).digest("hex").slice(0, 16);
|
|
28
|
+
}
|
|
29
|
+
function resolveScope(projectDir) {
|
|
30
|
+
const auth = readAuth();
|
|
31
|
+
const projectId = process.env["PROVENANT_PROJECT_ID"] || auth.projectId || hashId(projectDir);
|
|
32
|
+
const workspaceId = process.env["PROVENANT_WORKSPACE_ID"] || auth.workspaceId || "";
|
|
33
|
+
return { projectId, workspaceId };
|
|
34
|
+
}
|
|
35
|
+
function resolveDbPath(projectDir) {
|
|
36
|
+
const contextDb = join(projectDir, ".stackmemory", "context.db");
|
|
37
|
+
if (existsSync(contextDb)) return contextDb;
|
|
38
|
+
const localDb = join(projectDir, ".stackmemory", "stackmemory.db");
|
|
39
|
+
if (existsSync(localDb)) return localDb;
|
|
40
|
+
const globalDb = join(homedir(), ".stackmemory", "stackmemory.db");
|
|
41
|
+
if (existsSync(globalDb)) return globalDb;
|
|
42
|
+
mkdirSync(join(projectDir, ".stackmemory"), { recursive: true });
|
|
43
|
+
return contextDb;
|
|
44
|
+
}
|
|
45
|
+
function openBrain(projectDir = process.cwd()) {
|
|
46
|
+
const { projectId, workspaceId } = resolveScope(projectDir);
|
|
47
|
+
const dbPath = resolveDbPath(projectDir);
|
|
48
|
+
const db = new Database(dbPath);
|
|
49
|
+
const store = new BrainStore(db, { projectId, workspaceId });
|
|
50
|
+
const auth = readAuth();
|
|
51
|
+
const apiKey = process.env["PROVENANT_API_KEY"] || auth.apiKey;
|
|
52
|
+
let sync = null;
|
|
53
|
+
if (apiKey) {
|
|
54
|
+
const syncConfig = {
|
|
55
|
+
endpoint: process.env["PROVENANT_API_URL"] || auth.apiUrl || DEFAULT_ENDPOINT,
|
|
56
|
+
apiKey,
|
|
57
|
+
workspaceId,
|
|
58
|
+
projectId,
|
|
59
|
+
clientId: hashId(hostname() + projectDir)
|
|
60
|
+
};
|
|
61
|
+
sync = new BrainSync(db, store, syncConfig);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
db,
|
|
65
|
+
store,
|
|
66
|
+
projectId,
|
|
67
|
+
workspaceId,
|
|
68
|
+
sync,
|
|
69
|
+
close: () => db.close()
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
BrainStore2 as BrainStore,
|
|
74
|
+
BrainSync2 as BrainSync,
|
|
75
|
+
openBrain,
|
|
76
|
+
resolveDbPath,
|
|
77
|
+
resolveScope
|
|
78
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
const BRAIN_TABLE = "brain_entries";
|
|
6
|
+
const DEFAULT_BRAIN_LIMIT = 20;
|
|
7
|
+
export {
|
|
8
|
+
BRAIN_TABLE,
|
|
9
|
+
DEFAULT_BRAIN_LIMIT
|
|
10
|
+
};
|
|
@@ -3,14 +3,37 @@ import { dirname as __pathDirname } from 'path';
|
|
|
3
3
|
const __filename = __fileURLToPath(import.meta.url);
|
|
4
4
|
const __dirname = __pathDirname(__filename);
|
|
5
5
|
import { createHash } from "crypto";
|
|
6
|
+
import { createRequire } from "module";
|
|
7
|
+
let encoder = null;
|
|
8
|
+
let initAttempted = false;
|
|
9
|
+
function getEncoder() {
|
|
10
|
+
if (initAttempted) return encoder;
|
|
11
|
+
initAttempted = true;
|
|
12
|
+
try {
|
|
13
|
+
const require2 = createRequire(import.meta.url);
|
|
14
|
+
const tiktoken = require2("js-tiktoken");
|
|
15
|
+
encoder = tiktoken.getEncoding("cl100k_base");
|
|
16
|
+
} catch {
|
|
17
|
+
encoder = null;
|
|
18
|
+
}
|
|
19
|
+
return encoder;
|
|
20
|
+
}
|
|
6
21
|
function estimateTokens(content) {
|
|
7
22
|
if (!content) return 0;
|
|
23
|
+
const enc = getEncoder();
|
|
24
|
+
if (enc) {
|
|
25
|
+
return enc.encode(content).length;
|
|
26
|
+
}
|
|
8
27
|
return Math.ceil(content.length / 4);
|
|
9
28
|
}
|
|
29
|
+
function isTiktokenActive() {
|
|
30
|
+
return getEncoder() !== null;
|
|
31
|
+
}
|
|
10
32
|
function hashContent(content) {
|
|
11
33
|
return createHash("sha256").update(content).digest("hex");
|
|
12
34
|
}
|
|
13
35
|
export {
|
|
14
36
|
estimateTokens,
|
|
15
|
-
hashContent
|
|
37
|
+
hashContent,
|
|
38
|
+
isTiktokenActive
|
|
16
39
|
};
|
|
@@ -15,8 +15,7 @@ function isFeatureEnabled(feature) {
|
|
|
15
15
|
return process.env["STACKMEMORY_AI"] !== "false" && (!!process.env["ANTHROPIC_API_KEY"] || !!process.env["OPENAI_API_KEY"]);
|
|
16
16
|
case "skills":
|
|
17
17
|
return process.env["STACKMEMORY_SKILLS"] === "true" || process.env["STACKMEMORY_SKILLS"] === "1";
|
|
18
|
-
|
|
19
|
-
return process.env["STACKMEMORY_RALPH"] !== "false";
|
|
18
|
+
// For npm package users, must be explicitly enabled
|
|
20
19
|
case "multiProvider":
|
|
21
20
|
return process.env["STACKMEMORY_MULTI_PROVIDER"] === "true" || process.env["STACKMEMORY_MULTI_PROVIDER"] === "1";
|
|
22
21
|
default:
|
|
@@ -29,7 +28,6 @@ function getFeatureFlags() {
|
|
|
29
28
|
linear: isFeatureEnabled("linear"),
|
|
30
29
|
aiSummaries: isFeatureEnabled("aiSummaries"),
|
|
31
30
|
skills: isFeatureEnabled("skills"),
|
|
32
|
-
ralph: isFeatureEnabled("ralph"),
|
|
33
31
|
multiProvider: isFeatureEnabled("multiProvider")
|
|
34
32
|
};
|
|
35
33
|
}
|
|
@@ -49,9 +47,7 @@ function logFeatureStatus() {
|
|
|
49
47
|
console.log(
|
|
50
48
|
` Skills: ${flags.skills ? "enabled" : "disabled (set STACKMEMORY_SKILLS=true)"}`
|
|
51
49
|
);
|
|
52
|
-
console.log(
|
|
53
|
-
` Ralph: ${flags.ralph ? "enabled" : "disabled (set STACKMEMORY_RALPH=true)"}`
|
|
54
|
-
);
|
|
50
|
+
console.log();
|
|
55
51
|
console.log(
|
|
56
52
|
` MultiProvider: ${flags.multiProvider ? "enabled" : "disabled (set STACKMEMORY_MULTI_PROVIDER=true)"}`
|
|
57
53
|
);
|
|
@@ -208,6 +208,50 @@ class FrameDatabase {
|
|
|
208
208
|
CREATE INDEX IF NOT EXISTS idx_entity_temporal
|
|
209
209
|
ON entity_states(entity_name, valid_from DESC);
|
|
210
210
|
`);
|
|
211
|
+
this.db.exec(`
|
|
212
|
+
CREATE TABLE IF NOT EXISTS cloud_sync_state (
|
|
213
|
+
table_name TEXT NOT NULL,
|
|
214
|
+
row_id TEXT NOT NULL,
|
|
215
|
+
last_pushed_at INTEGER,
|
|
216
|
+
last_pushed_version INTEGER,
|
|
217
|
+
last_pulled_at INTEGER,
|
|
218
|
+
last_pulled_version INTEGER,
|
|
219
|
+
sync_status TEXT NOT NULL DEFAULT 'pending',
|
|
220
|
+
push_error TEXT,
|
|
221
|
+
push_attempts INTEGER DEFAULT 0,
|
|
222
|
+
PRIMARY KEY (table_name, row_id)
|
|
223
|
+
);
|
|
224
|
+
CREATE INDEX IF NOT EXISTS idx_cloud_sync_status ON cloud_sync_state(sync_status);
|
|
225
|
+
CREATE INDEX IF NOT EXISTS idx_cloud_sync_pushed ON cloud_sync_state(last_pushed_at);
|
|
226
|
+
|
|
227
|
+
CREATE TABLE IF NOT EXISTS cloud_sync_cursors (
|
|
228
|
+
direction TEXT PRIMARY KEY,
|
|
229
|
+
cursor_value TEXT NOT NULL,
|
|
230
|
+
updated_at INTEGER NOT NULL
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
-- Learned behavioral patterns (observe \u2192 learn \u2192 apply)
|
|
234
|
+
CREATE TABLE IF NOT EXISTS patterns (
|
|
235
|
+
id TEXT PRIMARY KEY,
|
|
236
|
+
domain TEXT NOT NULL,
|
|
237
|
+
trigger TEXT NOT NULL,
|
|
238
|
+
action TEXT NOT NULL,
|
|
239
|
+
evidence TEXT DEFAULT '[]',
|
|
240
|
+
confidence REAL DEFAULT 0.3,
|
|
241
|
+
observation_count INTEGER DEFAULT 0,
|
|
242
|
+
scope TEXT DEFAULT 'project',
|
|
243
|
+
project_id TEXT,
|
|
244
|
+
status TEXT DEFAULT 'pending',
|
|
245
|
+
source TEXT DEFAULT 'observed',
|
|
246
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
|
|
247
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
|
|
248
|
+
last_matched_at INTEGER,
|
|
249
|
+
superseded_by TEXT
|
|
250
|
+
);
|
|
251
|
+
CREATE INDEX IF NOT EXISTS idx_patterns_project ON patterns(project_id, status);
|
|
252
|
+
CREATE INDEX IF NOT EXISTS idx_patterns_domain ON patterns(domain, confidence DESC);
|
|
253
|
+
CREATE INDEX IF NOT EXISTS idx_patterns_status ON patterns(status, confidence DESC);
|
|
254
|
+
`);
|
|
211
255
|
logger.info("Frame database schema initialized");
|
|
212
256
|
} catch (error) {
|
|
213
257
|
throw new DatabaseError(
|
|
@@ -3,6 +3,7 @@ import { dirname as __pathDirname } from 'path';
|
|
|
3
3
|
const __filename = __fileURLToPath(import.meta.url);
|
|
4
4
|
const __dirname = __pathDirname(__filename);
|
|
5
5
|
import { logger } from "../monitoring/logger.js";
|
|
6
|
+
import { estimateTokens } from "../cache/token-estimator.js";
|
|
6
7
|
import { ValidationError, ErrorCode } from "../errors/index.js";
|
|
7
8
|
import * as fs from "fs";
|
|
8
9
|
import * as path from "path";
|
|
@@ -381,7 +382,6 @@ class RecursiveContextManager {
|
|
|
381
382
|
fitChunksToTokenBudget(chunks, _maxTokens) {
|
|
382
383
|
const selected = [];
|
|
383
384
|
let totalTokens = 0;
|
|
384
|
-
const estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
385
385
|
for (const chunk of chunks) {
|
|
386
386
|
const chunkTokens = estimateTokens(chunk.content);
|
|
387
387
|
if (totalTokens + chunkTokens <= maxTokens) {
|
|
@@ -5,6 +5,7 @@ const __dirname = __pathDirname(__filename);
|
|
|
5
5
|
import * as fs from "fs/promises";
|
|
6
6
|
import * as path from "path";
|
|
7
7
|
import { logger } from "../monitoring/logger.js";
|
|
8
|
+
import { estimateTokens } from "../cache/token-estimator.js";
|
|
8
9
|
import {
|
|
9
10
|
getModelTokenLimit
|
|
10
11
|
} from "../models/model-router.js";
|
|
@@ -44,7 +45,7 @@ class CompactionHandler {
|
|
|
44
45
|
* Track token usage from a message
|
|
45
46
|
*/
|
|
46
47
|
trackTokens(content) {
|
|
47
|
-
const estimatedTokens =
|
|
48
|
+
const estimatedTokens = estimateTokens(content);
|
|
48
49
|
this.tokenAccumulator += estimatedTokens;
|
|
49
50
|
this.metrics.estimatedTokens += estimatedTokens;
|
|
50
51
|
if (this.isApproachingCompaction()) {
|
|
@@ -44,7 +44,20 @@ class SQLiteAdapter extends FeatureAwareDatabaseAdapter {
|
|
|
44
44
|
const config = this.config;
|
|
45
45
|
const dir = path.dirname(this.dbPath);
|
|
46
46
|
await fs.mkdir(dir, { recursive: true });
|
|
47
|
-
|
|
47
|
+
try {
|
|
48
|
+
this.db = new Database(this.dbPath);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
51
|
+
if (msg.includes("NODE_MODULE_VERSION") || msg.includes("was compiled against a different Node.js version")) {
|
|
52
|
+
const nodeVersion = process.version;
|
|
53
|
+
throw new Error(
|
|
54
|
+
`better-sqlite3 was compiled for a different Node.js version than the one currently running (${nodeVersion}).
|
|
55
|
+
Fix: cd ${process.cwd()} && npm rebuild better-sqlite3
|
|
56
|
+
If that fails: npm install`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
48
61
|
this.db.pragma("foreign_keys = ON");
|
|
49
62
|
if (config.walMode !== false) {
|
|
50
63
|
this.db.pragma("journal_mode = WAL");
|