@stackmemoryai/stackmemory 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +66 -270
  2. package/dist/src/cli/claude-sm.js +9 -0
  3. package/dist/src/cli/codex-sm.js +9 -0
  4. package/dist/src/cli/commands/audit.js +134 -0
  5. package/dist/src/cli/commands/bench.js +252 -0
  6. package/dist/src/cli/commands/dashboard.js +2 -1
  7. package/dist/src/cli/commands/stats.js +118 -0
  8. package/dist/src/cli/index.js +6 -0
  9. package/dist/src/core/config/feature-flags.js +7 -1
  10. package/dist/src/core/context/enhanced-rehydration.js +24 -5
  11. package/dist/src/core/extensions/cerebras-adapter.js +28 -0
  12. package/dist/src/core/extensions/deepinfra-adapter.js +32 -0
  13. package/dist/src/core/extensions/provider-adapter.js +33 -240
  14. package/dist/src/core/models/complexity-scorer.js +154 -0
  15. package/dist/src/core/models/model-router.js +230 -36
  16. package/dist/src/core/models/provider-pricing.js +63 -0
  17. package/dist/src/core/models/sensitive-guard.js +112 -0
  18. package/dist/src/core/monitoring/feedback-loops.js +88 -0
  19. package/dist/src/features/sweep/pty-wrapper.js +9 -0
  20. package/dist/src/hooks/daemon.js +8 -0
  21. package/dist/src/hooks/graphiti-hooks.js +104 -0
  22. package/dist/src/hooks/schemas.js +12 -1
  23. package/dist/src/integrations/anthropic/batch-client.js +256 -0
  24. package/dist/src/integrations/anthropic/client.js +87 -72
  25. package/dist/src/integrations/claude-code/subagent-client.js +133 -12
  26. package/dist/src/integrations/graphiti/client.js +115 -0
  27. package/dist/src/integrations/graphiti/config.js +17 -0
  28. package/dist/src/integrations/graphiti/types.js +4 -0
  29. package/dist/src/integrations/mcp/handlers/index.js +25 -1
  30. package/dist/src/integrations/mcp/handlers/provider-handlers.js +227 -0
  31. package/dist/src/integrations/mcp/server.js +207 -1
  32. package/dist/src/integrations/mcp/tool-definitions.js +38 -1
  33. package/dist/src/orchestrators/multimodal/baselines.js +128 -0
  34. package/dist/src/orchestrators/multimodal/constants.js +9 -1
  35. package/dist/src/orchestrators/multimodal/harness.js +86 -6
  36. package/dist/src/orchestrators/multimodal/providers.js +113 -2
  37. package/dist/src/skills/recursive-agent-orchestrator.js +15 -10
  38. package/dist/src/utils/fuzzy-edit.js +162 -0
  39. package/package.json +5 -1
@@ -0,0 +1,104 @@
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 { logger } from "../core/monitoring/logger.js";
6
+ import { GraphitiClient } from "../integrations/graphiti/client.js";
7
+ import { DEFAULT_GRAPHITI_CONFIG } from "../integrations/graphiti/config.js";
8
+ class GraphitiHooks {
9
+ client;
10
+ config;
11
+ constructor(config = {}) {
12
+ this.config = { ...DEFAULT_GRAPHITI_CONFIG, ...config };
13
+ this.client = new GraphitiClient(this.config);
14
+ }
15
+ register(emitter) {
16
+ if (!this.config.enabled) {
17
+ logger.debug("Graphiti hooks disabled");
18
+ return;
19
+ }
20
+ emitter.registerHandler("session_start", this.onSessionStart.bind(this));
21
+ emitter.registerHandler("file_change", this.onFileChange.bind(this));
22
+ emitter.registerHandler("session_end", this.onSessionEnd.bind(this));
23
+ logger.info("Graphiti hooks registered", {
24
+ endpoint: this.config.endpoint,
25
+ backend: this.config.backend,
26
+ maxHops: this.config.maxHops
27
+ });
28
+ }
29
+ async onSessionStart(event) {
30
+ try {
31
+ const status = await this.client.getStatus();
32
+ if (!status.connected) {
33
+ logger.warn("Graphiti not available - operating in degraded mode");
34
+ return;
35
+ }
36
+ const episode = {
37
+ type: "session_start",
38
+ content: event.data || {},
39
+ timestamp: Date.now(),
40
+ source: "stackmemory",
41
+ metadata: { severity: "info" }
42
+ };
43
+ await this.client.upsertEpisode(episode);
44
+ } catch (error) {
45
+ logger.debug("Graphiti session_start failed", {
46
+ error: error instanceof Error ? error.message : String(error)
47
+ });
48
+ }
49
+ }
50
+ async onFileChange(event) {
51
+ const fileEvent = event;
52
+ try {
53
+ const episode = {
54
+ type: "file_change",
55
+ content: {
56
+ path: fileEvent.data.path,
57
+ changeType: fileEvent.data.changeType,
58
+ size: typeof fileEvent.data.content === "string" ? fileEvent.data.content.length : void 0
59
+ },
60
+ timestamp: Date.now(),
61
+ source: "stackmemory"
62
+ };
63
+ await this.client.upsertEpisode(episode);
64
+ } catch (error) {
65
+ logger.debug("Graphiti file_change episode failed", {
66
+ error: error instanceof Error ? error.message : String(error)
67
+ });
68
+ }
69
+ }
70
+ async onSessionEnd(event) {
71
+ try {
72
+ const episode = {
73
+ type: "session_end",
74
+ content: event.data || {},
75
+ timestamp: Date.now(),
76
+ source: "stackmemory"
77
+ };
78
+ await this.client.upsertEpisode(episode);
79
+ } catch (error) {
80
+ logger.debug("Graphiti session_end failed", {
81
+ error: error instanceof Error ? error.message : String(error)
82
+ });
83
+ }
84
+ }
85
+ // Expose a simple temporal query helper for future MCP tooling
86
+ async buildTemporalContext(query = {}) {
87
+ const now = Date.now();
88
+ const q = {
89
+ query: query.query || void 0,
90
+ entityTypes: query.entityTypes || void 0,
91
+ relationTypes: query.relationTypes || void 0,
92
+ validFrom: query.validFrom ?? now - 1e3 * 60 * 60 * 24 * 30,
93
+ // 30d default
94
+ validTo: query.validTo ?? now,
95
+ maxHops: query.maxHops ?? this.config.maxHops,
96
+ k: query.k ?? 20,
97
+ rerank: query.rerank ?? true
98
+ };
99
+ return this.client.queryTemporal(q);
100
+ }
101
+ }
102
+ export {
103
+ GraphitiHooks
104
+ };
@@ -16,6 +16,10 @@ const ModelProviderSchema = z.enum([
16
16
  "qwen",
17
17
  "openai",
18
18
  "ollama",
19
+ "cerebras",
20
+ "deepinfra",
21
+ "openrouter",
22
+ "anthropic-batch",
19
23
  "custom"
20
24
  ]);
21
25
  const ModelConfigSchema = z.object({
@@ -33,7 +37,10 @@ const ModelRouterConfigSchema = z.object({
33
37
  plan: ModelProviderSchema.optional(),
34
38
  think: ModelProviderSchema.optional(),
35
39
  code: ModelProviderSchema.optional(),
36
- review: ModelProviderSchema.optional()
40
+ review: ModelProviderSchema.optional(),
41
+ linting: ModelProviderSchema.optional(),
42
+ context: ModelProviderSchema.optional(),
43
+ testing: ModelProviderSchema.optional()
37
44
  }).optional().default({}),
38
45
  fallback: z.object({
39
46
  enabled: z.boolean(),
@@ -49,6 +56,10 @@ const ModelRouterConfigSchema = z.object({
49
56
  qwen: ModelConfigSchema.optional(),
50
57
  openai: ModelConfigSchema.optional(),
51
58
  ollama: ModelConfigSchema.optional(),
59
+ cerebras: ModelConfigSchema.optional(),
60
+ deepinfra: ModelConfigSchema.optional(),
61
+ openrouter: ModelConfigSchema.optional(),
62
+ "anthropic-batch": ModelConfigSchema.optional(),
52
63
  custom: ModelConfigSchema.optional()
53
64
  }).optional().default({}),
54
65
  thinkingMode: z.object({
@@ -0,0 +1,256 @@
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
+ import { logger } from "../../core/monitoring/logger.js";
9
+ const BATCH_JOBS_PATH = join(homedir(), ".stackmemory", "batch-jobs.json");
10
+ const DEFAULT_POLL_INTERVAL_MS = 3e4;
11
+ const DEFAULT_TIMEOUT_MS = 2 * 60 * 60 * 1e3;
12
+ class AnthropicBatchClient {
13
+ apiKey;
14
+ baseUrl;
15
+ mockMode;
16
+ constructor(config) {
17
+ this.apiKey = config?.apiKey !== void 0 ? config.apiKey : process.env["ANTHROPIC_API_KEY"] || "";
18
+ this.baseUrl = config?.baseUrl || "https://api.anthropic.com";
19
+ this.mockMode = config?.mockMode ?? !this.apiKey;
20
+ if (this.mockMode) {
21
+ logger.warn("AnthropicBatchClient: no API key, using mock mode");
22
+ }
23
+ }
24
+ /**
25
+ * Submit a batch of requests
26
+ */
27
+ async submit(requests, description) {
28
+ if (this.mockMode) {
29
+ const batchId = `batch_mock_${Date.now()}`;
30
+ this.persistJob({
31
+ batchId,
32
+ status: "ended",
33
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
34
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
35
+ requestCount: requests.length,
36
+ description
37
+ });
38
+ return batchId;
39
+ }
40
+ const response = await fetch(`${this.baseUrl}/v1/messages/batches`, {
41
+ method: "POST",
42
+ headers: {
43
+ "Content-Type": "application/json",
44
+ "x-api-key": this.apiKey,
45
+ "anthropic-version": "2023-06-01"
46
+ },
47
+ body: JSON.stringify({ requests })
48
+ });
49
+ if (!response.ok) {
50
+ const errText = await response.text();
51
+ throw new Error(`Batch submit failed: ${response.status} ${errText}`);
52
+ }
53
+ const job = await response.json();
54
+ this.persistJob({
55
+ batchId: job.id,
56
+ status: job.processing_status,
57
+ createdAt: job.created_at,
58
+ requestCount: requests.length,
59
+ description
60
+ });
61
+ logger.info("Batch submitted", { batchId: job.id, count: requests.length });
62
+ return job.id;
63
+ }
64
+ /**
65
+ * Poll batch job status
66
+ */
67
+ async poll(batchId) {
68
+ if (this.mockMode) {
69
+ return this.mockBatchJob(batchId);
70
+ }
71
+ const response = await fetch(
72
+ `${this.baseUrl}/v1/messages/batches/${batchId}`,
73
+ {
74
+ headers: {
75
+ "x-api-key": this.apiKey,
76
+ "anthropic-version": "2023-06-01"
77
+ }
78
+ }
79
+ );
80
+ if (!response.ok) {
81
+ const errText = await response.text();
82
+ throw new Error(`Batch poll failed: ${response.status} ${errText}`);
83
+ }
84
+ const job = await response.json();
85
+ this.updateJobStatus(batchId, job.processing_status, job.ended_at);
86
+ return job;
87
+ }
88
+ /**
89
+ * Retrieve batch results
90
+ */
91
+ async retrieve(batchId) {
92
+ if (this.mockMode) {
93
+ return this.mockBatchResults(batchId);
94
+ }
95
+ const job = await this.poll(batchId);
96
+ if (job.processing_status !== "ended") {
97
+ throw new Error(
98
+ `Batch ${batchId} not finished: ${job.processing_status}`
99
+ );
100
+ }
101
+ if (!job.results_url) {
102
+ throw new Error(`Batch ${batchId} has no results URL`);
103
+ }
104
+ const response = await fetch(job.results_url, {
105
+ headers: {
106
+ "x-api-key": this.apiKey,
107
+ "anthropic-version": "2023-06-01"
108
+ }
109
+ });
110
+ if (!response.ok) {
111
+ throw new Error(`Batch retrieve failed: ${response.status}`);
112
+ }
113
+ const text = await response.text();
114
+ return text.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
115
+ }
116
+ /**
117
+ * Poll until batch completes or times out
118
+ */
119
+ async waitForCompletion(batchId, timeoutMs = DEFAULT_TIMEOUT_MS, pollIntervalMs = DEFAULT_POLL_INTERVAL_MS) {
120
+ const deadline = Date.now() + timeoutMs;
121
+ while (Date.now() < deadline) {
122
+ const job = await this.poll(batchId);
123
+ if (job.processing_status === "ended") {
124
+ return job;
125
+ }
126
+ const remaining = deadline - Date.now();
127
+ const waitTime = Math.min(pollIntervalMs, remaining);
128
+ if (waitTime <= 0) break;
129
+ await new Promise((resolve) => {
130
+ const timer = setTimeout(() => resolve(), waitTime);
131
+ if (typeof timer === "object" && "unref" in timer) {
132
+ timer.unref();
133
+ }
134
+ });
135
+ }
136
+ throw new Error(`Batch ${batchId} timed out after ${timeoutMs}ms`);
137
+ }
138
+ /**
139
+ * Submit and wait for results (convenience)
140
+ */
141
+ async submitAndWait(requests, timeoutMs = DEFAULT_TIMEOUT_MS, description) {
142
+ const batchId = await this.submit(requests, description);
143
+ await this.waitForCompletion(batchId, timeoutMs);
144
+ return this.retrieve(batchId);
145
+ }
146
+ /**
147
+ * Cancel a batch job
148
+ */
149
+ async cancel(batchId) {
150
+ if (this.mockMode) {
151
+ return this.mockBatchJob(batchId, "canceling");
152
+ }
153
+ const response = await fetch(
154
+ `${this.baseUrl}/v1/messages/batches/${batchId}/cancel`,
155
+ {
156
+ method: "POST",
157
+ headers: {
158
+ "x-api-key": this.apiKey,
159
+ "anthropic-version": "2023-06-01"
160
+ }
161
+ }
162
+ );
163
+ if (!response.ok) {
164
+ throw new Error(`Batch cancel failed: ${response.status}`);
165
+ }
166
+ const job = await response.json();
167
+ this.updateJobStatus(batchId, job.processing_status);
168
+ return job;
169
+ }
170
+ /**
171
+ * List stored batch jobs
172
+ */
173
+ listJobs() {
174
+ return this.loadStoredJobs();
175
+ }
176
+ // ── Persistence ───────────────────────────────────────────────────────
177
+ persistJob(job) {
178
+ const jobs = this.loadStoredJobs();
179
+ const existing = jobs.findIndex((j) => j.batchId === job.batchId);
180
+ if (existing >= 0) {
181
+ jobs[existing] = job;
182
+ } else {
183
+ jobs.push(job);
184
+ }
185
+ const trimmed = jobs.slice(-50);
186
+ this.saveStoredJobs(trimmed);
187
+ }
188
+ updateJobStatus(batchId, status, endedAt) {
189
+ const jobs = this.loadStoredJobs();
190
+ const job = jobs.find((j) => j.batchId === batchId);
191
+ if (job) {
192
+ job.status = status;
193
+ if (endedAt) job.endedAt = endedAt;
194
+ this.saveStoredJobs(jobs);
195
+ }
196
+ }
197
+ loadStoredJobs() {
198
+ try {
199
+ if (existsSync(BATCH_JOBS_PATH)) {
200
+ return JSON.parse(readFileSync(BATCH_JOBS_PATH, "utf8"));
201
+ }
202
+ } catch {
203
+ }
204
+ return [];
205
+ }
206
+ saveStoredJobs(jobs) {
207
+ try {
208
+ const dir = join(homedir(), ".stackmemory");
209
+ if (!existsSync(dir)) {
210
+ mkdirSync(dir, { recursive: true });
211
+ }
212
+ writeFileSync(BATCH_JOBS_PATH, JSON.stringify(jobs, null, 2));
213
+ } catch {
214
+ }
215
+ }
216
+ // ── Mock mode ─────────────────────────────────────────────────────────
217
+ mockBatchJob(batchId, status = "ended") {
218
+ return {
219
+ id: batchId,
220
+ type: "message_batch",
221
+ processing_status: status,
222
+ request_counts: {
223
+ processing: 0,
224
+ succeeded: 1,
225
+ errored: 0,
226
+ canceled: 0,
227
+ expired: 0
228
+ },
229
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
230
+ ended_at: status === "ended" ? (/* @__PURE__ */ new Date()).toISOString() : void 0
231
+ };
232
+ }
233
+ mockBatchResults(batchId) {
234
+ const jobs = this.loadStoredJobs();
235
+ const job = jobs.find((j) => j.batchId === batchId);
236
+ const count = job?.requestCount || 1;
237
+ return Array.from({ length: count }, (_, i) => ({
238
+ custom_id: `req_${i}`,
239
+ result: {
240
+ type: "succeeded",
241
+ message: {
242
+ id: `msg_mock_${i}`,
243
+ content: [
244
+ { type: "text", text: `Mock batch response for request ${i}` }
245
+ ],
246
+ model: "claude-sonnet-4-5-20250929",
247
+ stop_reason: "end_turn",
248
+ usage: { input_tokens: 100, output_tokens: 50 }
249
+ }
250
+ }
251
+ }));
252
+ }
253
+ }
254
+ export {
255
+ AnthropicBatchClient
256
+ };
@@ -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 "../../core/monitoring/logger.js";
6
+ import { STRUCTURED_RESPONSE_SUFFIX } from "../../orchestrators/multimodal/constants.js";
6
7
  class AnthropicClient {
7
8
  apiKey;
8
9
  baseURL;
@@ -31,17 +32,21 @@ class AnthropicClient {
31
32
  * Send completion request to Claude
32
33
  */
33
34
  async complete(request) {
35
+ const enhancedRequest = {
36
+ ...request,
37
+ systemPrompt: request.systemPrompt + STRUCTURED_RESPONSE_SUFFIX
38
+ };
34
39
  await this.checkRateLimit();
35
40
  logger.debug("Sending completion request", {
36
- model: request.model,
37
- promptLength: request.prompt.length,
38
- maxTokens: request.maxTokens
41
+ model: enhancedRequest.model,
42
+ promptLength: enhancedRequest.prompt.length,
43
+ maxTokens: enhancedRequest.maxTokens
39
44
  });
40
45
  if (!this.apiKey) {
41
- return this.mockComplete(request);
46
+ return this.mockComplete(enhancedRequest);
42
47
  }
43
48
  try {
44
- const response = await this.sendRequest(request);
49
+ const response = await this.sendRequest(enhancedRequest);
45
50
  return response.content;
46
51
  } catch (error) {
47
52
  logger.error("Anthropic API error", { error });
@@ -57,7 +62,9 @@ class AnthropicClient {
57
62
  } catch (error) {
58
63
  if (attempt < this.maxRetries) {
59
64
  const delay = Math.pow(2, attempt) * 1e3;
60
- logger.warn(`Retrying after ${delay}ms (attempt ${attempt}/${this.maxRetries})`);
65
+ logger.warn(
66
+ `Retrying after ${delay}ms (attempt ${attempt}/${this.maxRetries})`
67
+ );
61
68
  await this.delay(delay);
62
69
  return this.sendRequest(request, attempt + 1);
63
70
  }
@@ -85,44 +92,48 @@ class AnthropicClient {
85
92
  * Mock response generators
86
93
  */
87
94
  mockPlanningResponse(prompt) {
88
- return JSON.stringify({
89
- plan: {
90
- type: "sequential",
91
- tasks: [
92
- {
93
- id: "task-1",
94
- description: "Analyze requirements",
95
- agent: "context",
96
- dependencies: []
97
- },
98
- {
99
- id: "task-2",
100
- type: "parallel",
101
- description: "Implementation phase",
102
- children: [
103
- {
104
- id: "task-2a",
105
- description: "Write core logic",
106
- agent: "code",
107
- dependencies: ["task-1"]
108
- },
109
- {
110
- id: "task-2b",
111
- description: "Write tests",
112
- agent: "testing",
113
- dependencies: ["task-1"]
114
- }
115
- ]
116
- },
117
- {
118
- id: "task-3",
119
- description: "Review and improve",
120
- agent: "review",
121
- dependencies: ["task-2"]
122
- }
123
- ]
124
- }
125
- }, null, 2);
95
+ return JSON.stringify(
96
+ {
97
+ plan: {
98
+ type: "sequential",
99
+ tasks: [
100
+ {
101
+ id: "task-1",
102
+ description: "Analyze requirements",
103
+ agent: "context",
104
+ dependencies: []
105
+ },
106
+ {
107
+ id: "task-2",
108
+ type: "parallel",
109
+ description: "Implementation phase",
110
+ children: [
111
+ {
112
+ id: "task-2a",
113
+ description: "Write core logic",
114
+ agent: "code",
115
+ dependencies: ["task-1"]
116
+ },
117
+ {
118
+ id: "task-2b",
119
+ description: "Write tests",
120
+ agent: "testing",
121
+ dependencies: ["task-1"]
122
+ }
123
+ ]
124
+ },
125
+ {
126
+ id: "task-3",
127
+ description: "Review and improve",
128
+ agent: "review",
129
+ dependencies: ["task-2"]
130
+ }
131
+ ]
132
+ }
133
+ },
134
+ null,
135
+ 2
136
+ );
126
137
  }
127
138
  mockCodeResponse(prompt) {
128
139
  return `
@@ -166,34 +177,38 @@ describe('validateInput', () => {
166
177
  `.trim();
167
178
  }
168
179
  mockReviewResponse(prompt) {
169
- return JSON.stringify({
170
- quality: 0.75,
171
- issues: [
172
- "Missing error handling in processTask function",
173
- "No input validation before processing",
174
- "Tests could cover more edge cases"
175
- ],
176
- suggestions: [
177
- "Add try-catch block in processTask",
178
- "Validate input length and type",
179
- "Add tests for special characters and long inputs",
180
- "Consider adding performance tests"
181
- ],
182
- improvements: [
183
- {
184
- file: "implementation.ts",
185
- line: 3,
186
- suggestion: "Add input validation",
187
- priority: "high"
188
- },
189
- {
190
- file: "tests.ts",
191
- line: 15,
192
- suggestion: "Add edge case tests",
193
- priority: "medium"
194
- }
195
- ]
196
- }, null, 2);
180
+ return JSON.stringify(
181
+ {
182
+ quality: 0.75,
183
+ issues: [
184
+ "Missing error handling in processTask function",
185
+ "No input validation before processing",
186
+ "Tests could cover more edge cases"
187
+ ],
188
+ suggestions: [
189
+ "Add try-catch block in processTask",
190
+ "Validate input length and type",
191
+ "Add tests for special characters and long inputs",
192
+ "Consider adding performance tests"
193
+ ],
194
+ improvements: [
195
+ {
196
+ file: "implementation.ts",
197
+ line: 3,
198
+ suggestion: "Add input validation",
199
+ priority: "high"
200
+ },
201
+ {
202
+ file: "tests.ts",
203
+ line: 15,
204
+ suggestion: "Add edge case tests",
205
+ priority: "medium"
206
+ }
207
+ ]
208
+ },
209
+ null,
210
+ 2
211
+ );
197
212
  }
198
213
  /**
199
214
  * Create mock response object