@stackmemoryai/stackmemory 1.2.2 → 1.2.4
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/dist/src/cli/codex-sm.js +6 -8
- package/dist/src/cli/commands/config.js +0 -81
- package/dist/src/cli/commands/context-rehydrate.js +133 -47
- package/dist/src/cli/commands/db.js +35 -8
- package/dist/src/cli/commands/handoff.js +1 -1
- package/dist/src/cli/commands/linear.js +9 -0
- package/dist/src/cli/commands/ralph.js +2 -2
- package/dist/src/cli/commands/setup.js +2 -2
- package/dist/src/cli/commands/signup.js +3 -1
- package/dist/src/cli/commands/storage-tier.js +26 -8
- package/dist/src/cli/index.js +1 -57
- package/dist/src/core/config/feature-flags.js +0 -4
- package/dist/src/core/context/dual-stack-manager.js +10 -3
- package/dist/src/core/context/frame-database.js +32 -0
- package/dist/src/core/context/frame-handoff-manager.js +2 -2
- package/dist/src/core/context/{refactored-frame-manager.js → frame-manager.js} +3 -3
- package/dist/src/core/context/index.js +2 -2
- package/dist/src/core/database/sqlite-adapter.js +161 -1
- package/dist/src/core/digest/frame-digest-integration.js +1 -1
- package/dist/src/core/digest/index.js +1 -1
- package/dist/src/core/execution/parallel-executor.js +5 -1
- package/dist/src/core/projects/project-isolation.js +18 -4
- package/dist/src/core/security/index.js +2 -0
- package/dist/src/core/security/input-sanitizer.js +23 -0
- package/dist/src/core/utils/update-checker.js +10 -6
- package/dist/src/daemon/daemon-config.js +2 -1
- package/dist/src/daemon/services/auto-save-service.js +121 -0
- package/dist/src/daemon/services/maintenance-service.js +76 -1
- package/dist/src/features/sweep/prompt-builder.js +2 -2
- package/dist/src/integrations/linear/config.js +3 -1
- package/dist/src/integrations/linear/sync.js +18 -5
- package/dist/src/integrations/mcp/handlers/code-execution-handlers.js +33 -7
- package/dist/src/integrations/mcp/handlers/cord-handlers.js +397 -0
- package/dist/src/integrations/mcp/handlers/index.js +55 -1
- package/dist/src/integrations/mcp/handlers/task-handlers.js +55 -12
- package/dist/src/integrations/mcp/handlers/team-handlers.js +211 -0
- package/dist/src/integrations/mcp/handlers/trace-handlers.js +25 -9
- package/dist/src/integrations/mcp/index.js +2 -2
- package/dist/src/integrations/mcp/refactored-server.js +31 -10
- package/dist/src/integrations/mcp/tool-definitions.js +215 -1
- package/dist/src/integrations/ralph/context/context-budget-manager.js +10 -2
- package/dist/src/integrations/ralph/context/stackmemory-context-loader.js +54 -22
- package/dist/src/integrations/ralph/learning/pattern-learner.js +59 -24
- package/dist/src/integrations/ralph/orchestration/multi-loop-orchestrator.js +81 -35
- package/dist/src/integrations/ralph/patterns/compounding-engineering-pattern.js +12 -4
- package/dist/src/integrations/ralph/patterns/extended-coherence-sessions.js +32 -9
- package/dist/src/integrations/ralph/swarm/git-workflow-manager.js +25 -8
- package/dist/src/integrations/ralph/swarm/swarm-coordinator.js +17 -5
- package/dist/src/integrations/ralph/visualization/ralph-debugger.js +73 -22
- package/dist/src/skills/claude-skills.js +0 -102
- package/dist/src/utils/hook-installer.js +8 -0
- package/package.json +5 -5
- package/scripts/install-claude-hooks-auto.js +8 -0
- package/templates/claude-hooks/cord-trace.js +225 -0
- package/dist/src/core/config/storage-config.js +0 -114
- package/dist/src/core/storage/chromadb-adapter.js +0 -379
- package/dist/src/integrations/claude-code/enhanced-pre-clear-hooks.js +0 -458
- package/dist/src/integrations/ralph/coordination/enhanced-coordination.js +0 -409
- package/dist/src/skills/repo-ingestion-skill.js +0 -631
- package/templates/claude-hooks/chromadb-wrapper +0 -21
- /package/dist/src/core/context/{enhanced-rehydration.js → rehydration.js} +0 -0
- /package/dist/src/core/digest/{enhanced-hybrid-digest.js → hybrid-digest.js} +0 -0
- /package/dist/src/core/session/{enhanced-handoff.js → handoff.js} +0 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cord Trace Hook (PostToolUse)
|
|
5
|
+
*
|
|
6
|
+
* Fires on every cord_* MCP tool call, logging metrics to a JSONL trace file
|
|
7
|
+
* and accumulating session metrics in a per-session state file.
|
|
8
|
+
*
|
|
9
|
+
* Trace dir: ~/.stackmemory/cord-traces/
|
|
10
|
+
* cord-trace-{date}.jsonl — append-only trace log
|
|
11
|
+
* cord-state-{session_id}.json — running session metrics
|
|
12
|
+
*
|
|
13
|
+
* Must complete in <50ms — pure file I/O only.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const HOME = process.env.HOME || '/tmp';
|
|
20
|
+
const TRACE_DIR = path.join(HOME, '.stackmemory', 'cord-traces');
|
|
21
|
+
const CORD_TOOLS = new Set([
|
|
22
|
+
'cord_spawn',
|
|
23
|
+
'cord_fork',
|
|
24
|
+
'cord_complete',
|
|
25
|
+
'cord_ask',
|
|
26
|
+
'cord_tree',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
function ensureDir(dir) {
|
|
30
|
+
if (!fs.existsSync(dir)) {
|
|
31
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function safeWriteFile(filePath, content) {
|
|
36
|
+
const tmp = filePath + '.tmp';
|
|
37
|
+
fs.writeFileSync(tmp, content);
|
|
38
|
+
fs.renameSync(tmp, filePath);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Extract the cord tool name from the full MCP tool name.
|
|
43
|
+
* e.g. "mcp__stackmemory-refactored__cord_spawn" -> "cord_spawn"
|
|
44
|
+
*/
|
|
45
|
+
function extractCordTool(toolName) {
|
|
46
|
+
if (!toolName) return null;
|
|
47
|
+
for (const t of CORD_TOOLS) {
|
|
48
|
+
if (toolName.endsWith(t)) return t;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function loadState(sessionId) {
|
|
54
|
+
const stateFile = path.join(TRACE_DIR, `cord-state-${sessionId}.json`);
|
|
55
|
+
try {
|
|
56
|
+
if (fs.existsSync(stateFile)) {
|
|
57
|
+
return JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// Corrupted state, start fresh
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
session_id: sessionId,
|
|
64
|
+
started: new Date().toISOString(),
|
|
65
|
+
tool_counts: {},
|
|
66
|
+
task_ids: [],
|
|
67
|
+
context_modes: {},
|
|
68
|
+
unblocked_total: 0,
|
|
69
|
+
max_depth: 0,
|
|
70
|
+
completion_rate: 0,
|
|
71
|
+
ask_count: 0,
|
|
72
|
+
completed_count: 0,
|
|
73
|
+
spawn_fork_count: 0,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function saveState(sessionId, state) {
|
|
78
|
+
const stateFile = path.join(TRACE_DIR, `cord-state-${sessionId}.json`);
|
|
79
|
+
safeWriteFile(stateFile, JSON.stringify(state, null, 2));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function appendTrace(entry) {
|
|
83
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
84
|
+
const traceFile = path.join(TRACE_DIR, `cord-trace-${date}.jsonl`);
|
|
85
|
+
fs.appendFileSync(traceFile, JSON.stringify(entry) + '\n');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Safely extract metadata from tool_response.
|
|
90
|
+
* MCP tool responses come as { content: [{ type, text }], metadata: {...} }.
|
|
91
|
+
* The hook receives tool_response which may be the raw MCP result or a string.
|
|
92
|
+
*/
|
|
93
|
+
function extractMetadata(toolResponse) {
|
|
94
|
+
if (!toolResponse) return {};
|
|
95
|
+
if (typeof toolResponse === 'string') {
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(toolResponse);
|
|
98
|
+
return parsed.metadata || parsed;
|
|
99
|
+
} catch {
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (toolResponse.metadata) return toolResponse.metadata;
|
|
104
|
+
return toolResponse;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function updateState(state, cordTool, toolInput, toolResponse) {
|
|
108
|
+
// Increment tool count
|
|
109
|
+
state.tool_counts[cordTool] = (state.tool_counts[cordTool] || 0) + 1;
|
|
110
|
+
|
|
111
|
+
const meta = extractMetadata(toolResponse);
|
|
112
|
+
|
|
113
|
+
switch (cordTool) {
|
|
114
|
+
case 'cord_spawn':
|
|
115
|
+
case 'cord_fork': {
|
|
116
|
+
state.spawn_fork_count++;
|
|
117
|
+
|
|
118
|
+
// Track context mode
|
|
119
|
+
const mode = meta.context_mode || cordTool.replace('cord_', '');
|
|
120
|
+
state.context_modes[mode] = (state.context_modes[mode] || 0) + 1;
|
|
121
|
+
|
|
122
|
+
// Track task ID
|
|
123
|
+
if (meta.task_id && !state.task_ids.includes(meta.task_id)) {
|
|
124
|
+
state.task_ids.push(meta.task_id);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Track max depth
|
|
128
|
+
const depth = meta.depth ?? 0;
|
|
129
|
+
if (depth > state.max_depth) {
|
|
130
|
+
state.max_depth = depth;
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
case 'cord_complete': {
|
|
136
|
+
state.completed_count++;
|
|
137
|
+
|
|
138
|
+
// Count unblocked tasks
|
|
139
|
+
const unblocked = meta.unblocked;
|
|
140
|
+
if (Array.isArray(unblocked)) {
|
|
141
|
+
state.unblocked_total += unblocked.length;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Update completion rate
|
|
145
|
+
if (state.spawn_fork_count > 0) {
|
|
146
|
+
state.completion_rate = +(
|
|
147
|
+
state.completed_count / state.spawn_fork_count
|
|
148
|
+
).toFixed(3);
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
case 'cord_ask': {
|
|
154
|
+
state.ask_count++;
|
|
155
|
+
|
|
156
|
+
// Track task ID
|
|
157
|
+
if (meta.task_id && !state.task_ids.includes(meta.task_id)) {
|
|
158
|
+
state.task_ids.push(meta.task_id);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Track context mode
|
|
162
|
+
state.context_modes['ask'] = (state.context_modes['ask'] || 0) + 1;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
case 'cord_tree':
|
|
167
|
+
// Read-only — no state change
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function buildMetricsSummary(state) {
|
|
173
|
+
return {
|
|
174
|
+
task_count: state.task_ids.length,
|
|
175
|
+
active: state.spawn_fork_count - state.completed_count,
|
|
176
|
+
completed: state.completed_count,
|
|
177
|
+
asks: state.ask_count,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function readInput() {
|
|
182
|
+
let input = '';
|
|
183
|
+
for await (const chunk of process.stdin) {
|
|
184
|
+
input += chunk;
|
|
185
|
+
}
|
|
186
|
+
return JSON.parse(input);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function main() {
|
|
190
|
+
try {
|
|
191
|
+
const input = await readInput();
|
|
192
|
+
const { tool_name, tool_input, tool_response, session_id } = input;
|
|
193
|
+
|
|
194
|
+
const cordTool = extractCordTool(tool_name);
|
|
195
|
+
if (!cordTool) return; // Not a cord tool, shouldn't happen given matcher
|
|
196
|
+
|
|
197
|
+
const sid =
|
|
198
|
+
session_id || process.env.CLAUDE_INSTANCE_ID || String(process.ppid);
|
|
199
|
+
|
|
200
|
+
ensureDir(TRACE_DIR);
|
|
201
|
+
|
|
202
|
+
// Load and update state
|
|
203
|
+
const state = loadState(sid);
|
|
204
|
+
updateState(state, cordTool, tool_input, tool_response);
|
|
205
|
+
|
|
206
|
+
// Build trace entry
|
|
207
|
+
const entry = {
|
|
208
|
+
ts: new Date().toISOString(),
|
|
209
|
+
session_id: sid,
|
|
210
|
+
cwd: input.cwd || process.cwd(),
|
|
211
|
+
tool: cordTool,
|
|
212
|
+
input: tool_input || {},
|
|
213
|
+
response: extractMetadata(tool_response),
|
|
214
|
+
metrics: buildMetricsSummary(state),
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Append trace + save state
|
|
218
|
+
appendTrace(entry);
|
|
219
|
+
saveState(sid, state);
|
|
220
|
+
} catch {
|
|
221
|
+
// Silent fail — never block the agent
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
main();
|
|
@@ -1,114 +0,0 @@
|
|
|
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 { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
6
|
-
import { join } from "path";
|
|
7
|
-
import { homedir } from "os";
|
|
8
|
-
const DEFAULT_STORAGE_CONFIG = {
|
|
9
|
-
mode: "sqlite",
|
|
10
|
-
chromadb: {
|
|
11
|
-
enabled: false
|
|
12
|
-
}
|
|
13
|
-
};
|
|
14
|
-
const STACKMEMORY_DIR = join(homedir(), ".stackmemory");
|
|
15
|
-
const CONFIG_FILE = join(STACKMEMORY_DIR, "storage-config.json");
|
|
16
|
-
function loadStorageConfig() {
|
|
17
|
-
try {
|
|
18
|
-
if (existsSync(CONFIG_FILE)) {
|
|
19
|
-
const content = readFileSync(CONFIG_FILE, "utf-8");
|
|
20
|
-
const config = JSON.parse(content);
|
|
21
|
-
return {
|
|
22
|
-
mode: config.mode || DEFAULT_STORAGE_CONFIG.mode,
|
|
23
|
-
chromadb: {
|
|
24
|
-
...DEFAULT_STORAGE_CONFIG.chromadb,
|
|
25
|
-
...config.chromadb
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
} catch (error) {
|
|
30
|
-
console.warn("Failed to load storage config, using defaults:", error);
|
|
31
|
-
}
|
|
32
|
-
return DEFAULT_STORAGE_CONFIG;
|
|
33
|
-
}
|
|
34
|
-
function saveStorageConfig(config) {
|
|
35
|
-
try {
|
|
36
|
-
if (!existsSync(STACKMEMORY_DIR)) {
|
|
37
|
-
mkdirSync(STACKMEMORY_DIR, { recursive: true });
|
|
38
|
-
}
|
|
39
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
|
|
40
|
-
} catch (error) {
|
|
41
|
-
console.error("Failed to save storage config:", error);
|
|
42
|
-
throw error;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
function isChromaDBEnabled() {
|
|
46
|
-
const config = loadStorageConfig();
|
|
47
|
-
if (!config.chromadb.enabled) {
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
const apiKey = config.chromadb.apiKey || process.env["CHROMADB_API_KEY"];
|
|
51
|
-
if (!apiKey) {
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
function getStorageMode() {
|
|
57
|
-
const config = loadStorageConfig();
|
|
58
|
-
if (config.mode === "hybrid" && !isChromaDBEnabled()) {
|
|
59
|
-
return "sqlite";
|
|
60
|
-
}
|
|
61
|
-
return config.mode;
|
|
62
|
-
}
|
|
63
|
-
function getChromaDBConfig() {
|
|
64
|
-
if (!isChromaDBEnabled()) {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
const config = loadStorageConfig();
|
|
68
|
-
const apiKey = config.chromadb.apiKey || process.env["CHROMADB_API_KEY"];
|
|
69
|
-
const apiUrl = config.chromadb.apiUrl || process.env["CHROMADB_API_URL"] || "https://api.trychroma.com";
|
|
70
|
-
return {
|
|
71
|
-
enabled: true,
|
|
72
|
-
apiKey,
|
|
73
|
-
apiUrl,
|
|
74
|
-
tenant: config.chromadb.tenant || process.env["CHROMADB_TENANT"] || "default_tenant",
|
|
75
|
-
database: config.chromadb.database || process.env["CHROMADB_DATABASE"] || "default_database"
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
function enableChromaDB(chromaConfig) {
|
|
79
|
-
const config = loadStorageConfig();
|
|
80
|
-
config.mode = "hybrid";
|
|
81
|
-
config.chromadb = {
|
|
82
|
-
enabled: true,
|
|
83
|
-
apiKey: chromaConfig.apiKey,
|
|
84
|
-
apiUrl: chromaConfig.apiUrl || "https://api.trychroma.com",
|
|
85
|
-
tenant: chromaConfig.tenant || "default_tenant",
|
|
86
|
-
database: chromaConfig.database || "default_database"
|
|
87
|
-
};
|
|
88
|
-
saveStorageConfig(config);
|
|
89
|
-
}
|
|
90
|
-
function disableChromaDB() {
|
|
91
|
-
const config = loadStorageConfig();
|
|
92
|
-
config.mode = "sqlite";
|
|
93
|
-
config.chromadb = {
|
|
94
|
-
enabled: false
|
|
95
|
-
};
|
|
96
|
-
saveStorageConfig(config);
|
|
97
|
-
}
|
|
98
|
-
function getStorageModeDescription() {
|
|
99
|
-
const mode = getStorageMode();
|
|
100
|
-
if (mode === "hybrid") {
|
|
101
|
-
return "Hybrid (SQLite + ChromaDB for semantic search and cloud backup)";
|
|
102
|
-
}
|
|
103
|
-
return "SQLite (local storage only, fast, no external dependencies)";
|
|
104
|
-
}
|
|
105
|
-
export {
|
|
106
|
-
disableChromaDB,
|
|
107
|
-
enableChromaDB,
|
|
108
|
-
getChromaDBConfig,
|
|
109
|
-
getStorageMode,
|
|
110
|
-
getStorageModeDescription,
|
|
111
|
-
isChromaDBEnabled,
|
|
112
|
-
loadStorageConfig,
|
|
113
|
-
saveStorageConfig
|
|
114
|
-
};
|
|
@@ -1,379 +0,0 @@
|
|
|
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 { v4 as uuidv4 } from "uuid";
|
|
6
|
-
import { Logger } from "../monitoring/logger.js";
|
|
7
|
-
let chromadbModule = null;
|
|
8
|
-
async function getChromaDB() {
|
|
9
|
-
if (!chromadbModule) {
|
|
10
|
-
try {
|
|
11
|
-
chromadbModule = await import("chromadb");
|
|
12
|
-
} catch {
|
|
13
|
-
throw new Error(
|
|
14
|
-
"chromadb is not installed. Install it with: npm install chromadb"
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return chromadbModule;
|
|
19
|
-
}
|
|
20
|
-
class ChromaDBAdapter {
|
|
21
|
-
client = null;
|
|
22
|
-
collection = null;
|
|
23
|
-
logger;
|
|
24
|
-
config;
|
|
25
|
-
userId;
|
|
26
|
-
teamId;
|
|
27
|
-
constructor(config, userId, teamId) {
|
|
28
|
-
this.config = config;
|
|
29
|
-
this.userId = userId;
|
|
30
|
-
this.teamId = teamId;
|
|
31
|
-
this.logger = new Logger("ChromaDBAdapter");
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Factory method to create and initialize the adapter
|
|
35
|
-
*/
|
|
36
|
-
static async create(config, userId, teamId) {
|
|
37
|
-
const adapter = new ChromaDBAdapter(config, userId, teamId);
|
|
38
|
-
await adapter.initClient();
|
|
39
|
-
return adapter;
|
|
40
|
-
}
|
|
41
|
-
async initClient() {
|
|
42
|
-
const chromadb = await getChromaDB();
|
|
43
|
-
this.client = new chromadb.CloudClient({
|
|
44
|
-
apiKey: this.config.apiKey,
|
|
45
|
-
tenant: this.config.tenant,
|
|
46
|
-
database: this.config.database
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
async initialize() {
|
|
50
|
-
try {
|
|
51
|
-
if (!this.client) {
|
|
52
|
-
await this.initClient();
|
|
53
|
-
}
|
|
54
|
-
const collectionName = this.config.collectionName || "stackmemory_contexts";
|
|
55
|
-
this.collection = await this.client.getOrCreateCollection({
|
|
56
|
-
name: collectionName,
|
|
57
|
-
metadata: {
|
|
58
|
-
description: "StackMemory context storage",
|
|
59
|
-
version: "1.0.0",
|
|
60
|
-
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
this.logger.info(`ChromaDB collection '${collectionName}' initialized`);
|
|
64
|
-
} catch (error) {
|
|
65
|
-
this.logger.error("Failed to initialize ChromaDB collection", error);
|
|
66
|
-
throw error;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Store a frame in ChromaDB
|
|
71
|
-
*/
|
|
72
|
-
async storeFrame(frame) {
|
|
73
|
-
if (!this.collection) {
|
|
74
|
-
throw new Error("ChromaDB not initialized");
|
|
75
|
-
}
|
|
76
|
-
try {
|
|
77
|
-
const frameMetadata = {
|
|
78
|
-
user_id: this.userId,
|
|
79
|
-
frame_id: frame.frameId,
|
|
80
|
-
session_id: frame.sessionId || "unknown",
|
|
81
|
-
project_name: frame.projectName || "default",
|
|
82
|
-
timestamp: frame.timestamp,
|
|
83
|
-
type: "frame",
|
|
84
|
-
score: frame.score,
|
|
85
|
-
tags: frame.tags || []
|
|
86
|
-
};
|
|
87
|
-
if (this.teamId) {
|
|
88
|
-
frameMetadata.team_id = this.teamId;
|
|
89
|
-
}
|
|
90
|
-
const document = {
|
|
91
|
-
id: `frame_${frame.frameId}_${this.userId}`,
|
|
92
|
-
document: this.frameToDocument(frame),
|
|
93
|
-
metadata: frameMetadata
|
|
94
|
-
};
|
|
95
|
-
await this.collection.add({
|
|
96
|
-
ids: [document.id],
|
|
97
|
-
documents: [document.document],
|
|
98
|
-
metadatas: [document.metadata]
|
|
99
|
-
});
|
|
100
|
-
this.logger.debug(
|
|
101
|
-
`Stored frame ${frame.frameId} for user ${this.userId}`
|
|
102
|
-
);
|
|
103
|
-
} catch (error) {
|
|
104
|
-
this.logger.error(`Failed to store frame ${frame.frameId}`, error);
|
|
105
|
-
throw error;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Store a decision or observation
|
|
110
|
-
*/
|
|
111
|
-
async storeContext(type, content, metadata) {
|
|
112
|
-
if (!this.collection) {
|
|
113
|
-
throw new Error("ChromaDB not initialized");
|
|
114
|
-
}
|
|
115
|
-
try {
|
|
116
|
-
const contextId = `${type}_${uuidv4()}_${this.userId}`;
|
|
117
|
-
const documentMetadata = {
|
|
118
|
-
user_id: this.userId,
|
|
119
|
-
frame_id: metadata?.frame_id || "none",
|
|
120
|
-
session_id: metadata?.session_id || "unknown",
|
|
121
|
-
project_name: metadata?.project_name || "default",
|
|
122
|
-
timestamp: Date.now(),
|
|
123
|
-
type,
|
|
124
|
-
...metadata
|
|
125
|
-
};
|
|
126
|
-
if (this.teamId) {
|
|
127
|
-
documentMetadata.team_id = this.teamId;
|
|
128
|
-
}
|
|
129
|
-
const document = {
|
|
130
|
-
id: contextId,
|
|
131
|
-
document: content,
|
|
132
|
-
metadata: documentMetadata
|
|
133
|
-
};
|
|
134
|
-
await this.collection.add({
|
|
135
|
-
ids: [document.id],
|
|
136
|
-
documents: [document.document],
|
|
137
|
-
metadatas: [document.metadata]
|
|
138
|
-
});
|
|
139
|
-
this.logger.debug(`Stored ${type} for user ${this.userId}`);
|
|
140
|
-
} catch (error) {
|
|
141
|
-
this.logger.error(`Failed to store ${type}`, error);
|
|
142
|
-
throw error;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Query contexts by semantic similarity
|
|
147
|
-
*/
|
|
148
|
-
async queryContexts(query, limit = 10, filters) {
|
|
149
|
-
if (!this.collection) {
|
|
150
|
-
throw new Error("ChromaDB not initialized");
|
|
151
|
-
}
|
|
152
|
-
try {
|
|
153
|
-
const whereClause = {
|
|
154
|
-
user_id: this.userId
|
|
155
|
-
};
|
|
156
|
-
if (this.teamId) {
|
|
157
|
-
whereClause["$or"] = [
|
|
158
|
-
{ team_id: this.teamId },
|
|
159
|
-
{ user_id: this.userId }
|
|
160
|
-
];
|
|
161
|
-
}
|
|
162
|
-
if (filters?.type && filters.type.length > 0) {
|
|
163
|
-
whereClause.type = { $in: filters.type };
|
|
164
|
-
}
|
|
165
|
-
if (filters?.projectName) {
|
|
166
|
-
whereClause.project_name = filters.projectName;
|
|
167
|
-
}
|
|
168
|
-
if (filters?.sessionId) {
|
|
169
|
-
whereClause.session_id = filters.sessionId;
|
|
170
|
-
}
|
|
171
|
-
if (filters?.startTime || filters?.endTime) {
|
|
172
|
-
whereClause.timestamp = {};
|
|
173
|
-
if (filters.startTime) {
|
|
174
|
-
whereClause.timestamp.$gte = filters.startTime;
|
|
175
|
-
}
|
|
176
|
-
if (filters.endTime) {
|
|
177
|
-
whereClause.timestamp.$lte = filters.endTime;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
const results = await this.collection.query({
|
|
181
|
-
queryTexts: [query],
|
|
182
|
-
nResults: limit,
|
|
183
|
-
where: whereClause,
|
|
184
|
-
include: ["documents", "metadatas", "distances"]
|
|
185
|
-
});
|
|
186
|
-
const contexts = [];
|
|
187
|
-
if (results.documents && results.documents[0]) {
|
|
188
|
-
for (let i = 0; i < results.documents[0].length; i++) {
|
|
189
|
-
contexts.push({
|
|
190
|
-
content: results.documents[0][i] || "",
|
|
191
|
-
metadata: results.metadatas?.[0]?.[i] || {},
|
|
192
|
-
distance: results.distances?.[0]?.[i] || 0
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
this.logger.debug(`Found ${contexts.length} contexts for query`);
|
|
197
|
-
return contexts;
|
|
198
|
-
} catch (error) {
|
|
199
|
-
this.logger.error("Failed to query contexts", error);
|
|
200
|
-
throw error;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* Get user's recent contexts
|
|
205
|
-
*/
|
|
206
|
-
async getRecentContexts(limit = 20, type) {
|
|
207
|
-
if (!this.collection) {
|
|
208
|
-
throw new Error("ChromaDB not initialized");
|
|
209
|
-
}
|
|
210
|
-
try {
|
|
211
|
-
const whereClause = {
|
|
212
|
-
user_id: this.userId
|
|
213
|
-
};
|
|
214
|
-
if (type) {
|
|
215
|
-
whereClause.type = type;
|
|
216
|
-
}
|
|
217
|
-
const results = await this.collection.get({
|
|
218
|
-
where: whereClause,
|
|
219
|
-
include: ["documents", "metadatas"]
|
|
220
|
-
});
|
|
221
|
-
const contexts = [];
|
|
222
|
-
if (results.documents) {
|
|
223
|
-
const indexed = results.documents.map((doc, i) => ({
|
|
224
|
-
content: doc || "",
|
|
225
|
-
metadata: results.metadatas?.[i] || {}
|
|
226
|
-
}));
|
|
227
|
-
indexed.sort(
|
|
228
|
-
(a, b) => (b.metadata.timestamp || 0) - (a.metadata.timestamp || 0)
|
|
229
|
-
);
|
|
230
|
-
contexts.push(...indexed.slice(0, limit));
|
|
231
|
-
}
|
|
232
|
-
return contexts;
|
|
233
|
-
} catch (error) {
|
|
234
|
-
this.logger.error("Failed to get recent contexts", error);
|
|
235
|
-
throw error;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
/**
|
|
239
|
-
* Delete old contexts (retention policy)
|
|
240
|
-
*/
|
|
241
|
-
async deleteOldContexts(olderThanDays = 30) {
|
|
242
|
-
if (!this.collection) {
|
|
243
|
-
throw new Error("ChromaDB not initialized");
|
|
244
|
-
}
|
|
245
|
-
try {
|
|
246
|
-
const cutoffTime = Date.now() - olderThanDays * 24 * 60 * 60 * 1e3;
|
|
247
|
-
const results = await this.collection.get({
|
|
248
|
-
where: {
|
|
249
|
-
user_id: this.userId,
|
|
250
|
-
timestamp: { $lt: cutoffTime }
|
|
251
|
-
},
|
|
252
|
-
include: ["ids"]
|
|
253
|
-
});
|
|
254
|
-
if (!results.ids || results.ids.length === 0) {
|
|
255
|
-
return 0;
|
|
256
|
-
}
|
|
257
|
-
await this.collection.delete({
|
|
258
|
-
ids: results.ids
|
|
259
|
-
});
|
|
260
|
-
this.logger.info(`Deleted ${results.ids.length} old contexts`);
|
|
261
|
-
return results.ids.length;
|
|
262
|
-
} catch (error) {
|
|
263
|
-
this.logger.error("Failed to delete old contexts", error);
|
|
264
|
-
throw error;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Get team contexts (if user is part of a team)
|
|
269
|
-
*/
|
|
270
|
-
async getTeamContexts(limit = 20) {
|
|
271
|
-
if (!this.collection || !this.teamId) {
|
|
272
|
-
return [];
|
|
273
|
-
}
|
|
274
|
-
try {
|
|
275
|
-
const results = await this.collection.get({
|
|
276
|
-
where: {
|
|
277
|
-
team_id: this.teamId
|
|
278
|
-
},
|
|
279
|
-
include: ["documents", "metadatas"],
|
|
280
|
-
limit
|
|
281
|
-
});
|
|
282
|
-
const contexts = [];
|
|
283
|
-
if (results.documents) {
|
|
284
|
-
for (let i = 0; i < results.documents.length; i++) {
|
|
285
|
-
contexts.push({
|
|
286
|
-
content: results.documents[i] || "",
|
|
287
|
-
metadata: results.metadatas?.[i] || {}
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
return contexts;
|
|
292
|
-
} catch (error) {
|
|
293
|
-
this.logger.error("Failed to get team contexts", error);
|
|
294
|
-
return [];
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* Convert frame to searchable document
|
|
299
|
-
*/
|
|
300
|
-
frameToDocument(frame) {
|
|
301
|
-
const parts = [
|
|
302
|
-
`Frame: ${frame.title}`,
|
|
303
|
-
`Type: ${frame.type}`,
|
|
304
|
-
`Status: ${frame.status}`
|
|
305
|
-
];
|
|
306
|
-
if (frame.description) {
|
|
307
|
-
parts.push(`Description: ${frame.description}`);
|
|
308
|
-
}
|
|
309
|
-
if (frame.inputs && frame.inputs.length > 0) {
|
|
310
|
-
parts.push(`Inputs: ${frame.inputs.join(", ")}`);
|
|
311
|
-
}
|
|
312
|
-
if (frame.outputs && frame.outputs.length > 0) {
|
|
313
|
-
parts.push(`Outputs: ${frame.outputs.join(", ")}`);
|
|
314
|
-
}
|
|
315
|
-
if (frame.tags && frame.tags.length > 0) {
|
|
316
|
-
parts.push(`Tags: ${frame.tags.join(", ")}`);
|
|
317
|
-
}
|
|
318
|
-
if (frame.digest_json) {
|
|
319
|
-
try {
|
|
320
|
-
const digest = JSON.parse(frame.digest_json);
|
|
321
|
-
if (digest.summary) {
|
|
322
|
-
parts.push(`Summary: ${digest.summary}`);
|
|
323
|
-
}
|
|
324
|
-
if (digest.keyDecisions) {
|
|
325
|
-
parts.push(`Decisions: ${digest.keyDecisions.join(". ")}`);
|
|
326
|
-
}
|
|
327
|
-
} catch {
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
return parts.join("\n");
|
|
331
|
-
}
|
|
332
|
-
/**
|
|
333
|
-
* Update team ID for a user
|
|
334
|
-
*/
|
|
335
|
-
async updateTeamId(newTeamId) {
|
|
336
|
-
this.teamId = newTeamId;
|
|
337
|
-
this.logger.info(`Updated team ID to ${newTeamId} for user ${this.userId}`);
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* Get storage statistics
|
|
341
|
-
*/
|
|
342
|
-
async getStats() {
|
|
343
|
-
if (!this.collection) {
|
|
344
|
-
throw new Error("ChromaDB not initialized");
|
|
345
|
-
}
|
|
346
|
-
try {
|
|
347
|
-
const userResults = await this.collection.get({
|
|
348
|
-
where: { user_id: this.userId },
|
|
349
|
-
include: ["metadatas"]
|
|
350
|
-
});
|
|
351
|
-
const stats = {
|
|
352
|
-
totalDocuments: 0,
|
|
353
|
-
userDocuments: userResults.ids?.length || 0,
|
|
354
|
-
documentsByType: {}
|
|
355
|
-
};
|
|
356
|
-
if (userResults.metadatas) {
|
|
357
|
-
for (const metadata of userResults.metadatas) {
|
|
358
|
-
const type = metadata?.type || "unknown";
|
|
359
|
-
stats.documentsByType[type] = (stats.documentsByType[type] || 0) + 1;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
if (this.teamId) {
|
|
363
|
-
const teamResults = await this.collection.get({
|
|
364
|
-
where: { team_id: this.teamId },
|
|
365
|
-
include: ["ids"]
|
|
366
|
-
});
|
|
367
|
-
stats.teamDocuments = teamResults.ids?.length || 0;
|
|
368
|
-
}
|
|
369
|
-
stats.totalDocuments = stats.userDocuments + (stats.teamDocuments || 0);
|
|
370
|
-
return stats;
|
|
371
|
-
} catch (error) {
|
|
372
|
-
this.logger.error("Failed to get stats", error);
|
|
373
|
-
throw error;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
export {
|
|
378
|
-
ChromaDBAdapter
|
|
379
|
-
};
|