@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
@@ -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 { spawnSync } from "child_process";
6
+ import { STRUCTURED_RESPONSE_SUFFIX } from "./constants.js";
6
7
  async function callClaude(prompt, options) {
7
8
  const apiKey = process.env["ANTHROPIC_API_KEY"];
8
9
  if (!apiKey) {
@@ -15,7 +16,7 @@ async function callClaude(prompt, options) {
15
16
  const { Anthropic } = await import("@anthropic-ai/sdk");
16
17
  const client = new Anthropic({ apiKey });
17
18
  const model = options.model || "claude-sonnet-4-20250514";
18
- const system = options.system || "You are a precise software planning assistant.";
19
+ const system = (options.system || "You are a precise software planning assistant.") + STRUCTURED_RESPONSE_SUFFIX;
19
20
  try {
20
21
  const msg = await client.messages.create({
21
22
  model,
@@ -79,6 +80,113 @@ function callCodexCLI(prompt, args = [], dryRun = true, cwd) {
79
80
  return { ok: false, output: e?.message || String(e), command: printable };
80
81
  }
81
82
  }
83
+ function captureGitDiff(cwd, maxLen = 12e3) {
84
+ try {
85
+ const unstaged = spawnSync("git", ["diff"], {
86
+ cwd,
87
+ encoding: "utf8",
88
+ timeout: 1e4
89
+ });
90
+ const staged = spawnSync("git", ["diff", "--cached"], {
91
+ cwd,
92
+ encoding: "utf8",
93
+ timeout: 1e4
94
+ });
95
+ const untracked = spawnSync(
96
+ "git",
97
+ ["ls-files", "--others", "--exclude-standard"],
98
+ { cwd, encoding: "utf8", timeout: 1e4 }
99
+ );
100
+ let diff = "";
101
+ if (staged.stdout?.trim()) diff += staged.stdout;
102
+ if (unstaged.stdout?.trim()) diff += (diff ? "\n" : "") + unstaged.stdout;
103
+ if (untracked.stdout?.trim()) {
104
+ const newFiles = untracked.stdout.trim().split("\n").slice(0, 10);
105
+ diff += (diff ? "\n" : "") + `New untracked files:
106
+ ${newFiles.join("\n")}`;
107
+ }
108
+ if (!diff.trim()) return "(no changes detected)";
109
+ if (diff.length > maxLen) {
110
+ return diff.slice(0, maxLen) + `
111
+ ... (truncated, ${diff.length} total chars)`;
112
+ }
113
+ return diff;
114
+ } catch {
115
+ return "(git diff failed)";
116
+ }
117
+ }
118
+ function runPostImplChecks(cwd) {
119
+ const maxOutput = 2e3;
120
+ function truncate(s) {
121
+ if (s.length <= maxOutput) return s;
122
+ return s.slice(0, maxOutput) + `
123
+ ... (truncated, ${s.length} total chars)`;
124
+ }
125
+ let lintOk = false;
126
+ let lintOutput = "";
127
+ try {
128
+ const lint = spawnSync("npm", ["run", "lint"], {
129
+ cwd,
130
+ encoding: "utf8",
131
+ timeout: 3e4
132
+ });
133
+ lintOk = lint.status === 0;
134
+ lintOutput = truncate((lint.stdout || "") + (lint.stderr || ""));
135
+ } catch (e) {
136
+ lintOutput = truncate(e instanceof Error ? e.message : String(e));
137
+ }
138
+ let testsOk = false;
139
+ let testOutput = "";
140
+ try {
141
+ const tests = spawnSync(
142
+ "npx",
143
+ ["vitest", "run", "--reporter=dot", "--bail=1"],
144
+ {
145
+ cwd,
146
+ encoding: "utf8",
147
+ timeout: 12e4
148
+ }
149
+ );
150
+ testsOk = tests.status === 0;
151
+ testOutput = truncate((tests.stdout || "") + (tests.stderr || ""));
152
+ } catch (e) {
153
+ testOutput = truncate(e instanceof Error ? e.message : String(e));
154
+ }
155
+ return { lintOk, lintOutput, testsOk, testOutput };
156
+ }
157
+ function parseEditMetrics(diff) {
158
+ if (!diff || diff.startsWith("(")) {
159
+ return { editAttempts: 0, editSuccesses: 0, editFuzzyFallbacks: 0 };
160
+ }
161
+ const lines = diff.split("\n");
162
+ let currentFileHunks = 0;
163
+ let currentFileHasConflict = false;
164
+ let totalAttempts = 0;
165
+ let totalSuccesses = 0;
166
+ const flushFile = () => {
167
+ totalAttempts += currentFileHunks;
168
+ if (!currentFileHasConflict) {
169
+ totalSuccesses += currentFileHunks;
170
+ }
171
+ currentFileHunks = 0;
172
+ currentFileHasConflict = false;
173
+ };
174
+ for (const line of lines) {
175
+ if (/^diff --git /.test(line)) {
176
+ flushFile();
177
+ } else if (/^@@ /.test(line)) {
178
+ currentFileHunks++;
179
+ } else if (/^[<>=]{7}/.test(line)) {
180
+ currentFileHasConflict = true;
181
+ }
182
+ }
183
+ flushFile();
184
+ return {
185
+ editAttempts: totalAttempts,
186
+ editSuccesses: totalSuccesses,
187
+ editFuzzyFallbacks: 0
188
+ };
189
+ }
82
190
  async function implementWithClaude(prompt, options) {
83
191
  try {
84
192
  const out = await callClaude(prompt, {
@@ -93,5 +201,8 @@ async function implementWithClaude(prompt, options) {
93
201
  export {
94
202
  callClaude,
95
203
  callCodexCLI,
96
- implementWithClaude
204
+ captureGitDiff,
205
+ implementWithClaude,
206
+ parseEditMetrics,
207
+ runPostImplChecks
97
208
  };
@@ -8,6 +8,7 @@ import { logger } from "../core/monitoring/logger.js";
8
8
  import { ParallelExecutor } from "../core/execution/parallel-executor.js";
9
9
  import { RecursiveContextManager } from "../core/context/recursive-context-manager.js";
10
10
  import { ClaudeCodeSubagentClient } from "../integrations/claude-code/subagent-client.js";
11
+ import { STRUCTURED_RESPONSE_SUFFIX } from "../orchestrators/multimodal/constants.js";
11
12
  class RecursiveAgentOrchestrator {
12
13
  frameManager;
13
14
  contextRetriever;
@@ -72,7 +73,7 @@ Rules:
72
73
  - Maximize parallelism \u2014 independent tasks run concurrently
73
74
  - Each subtask names its agent type: planning, code, testing, linting, review, improve, context, publish
74
75
  - Include failure modes and rollback steps for risky operations
75
- - Keep subtask descriptions actionable (verb + object + constraint)`,
76
+ - Keep subtask descriptions actionable (verb + object + constraint)` + STRUCTURED_RESPONSE_SUFFIX,
76
77
  capabilities: ["decompose", "analyze", "strategize", "prioritize"]
77
78
  });
78
79
  configs.set("code", {
@@ -87,7 +88,7 @@ Rules:
87
88
  - Add .js extensions to relative TypeScript imports (ESM)
88
89
  - Return undefined over throwing; log+continue over crash
89
90
  - No emojis, no unnecessary comments, functions under 20 lines
90
- - Validate inputs at system boundaries only`,
91
+ - Validate inputs at system boundaries only` + STRUCTURED_RESPONSE_SUFFIX,
91
92
  capabilities: ["implement", "refactor", "optimize", "document"]
92
93
  });
93
94
  configs.set("testing", {
@@ -102,7 +103,7 @@ Rules:
102
103
  - Prioritize: critical paths > edge cases > happy paths
103
104
  - Each test should assert meaningful behavior, not implementation details
104
105
  - Use parameterized tests (it.each) to consolidate similar cases
105
- - Run tests after writing: npm run test:run`,
106
+ - Run tests after writing: npm run test:run` + STRUCTURED_RESPONSE_SUFFIX,
106
107
  capabilities: [
107
108
  "generate-tests",
108
109
  "validate",
@@ -121,7 +122,7 @@ Rules:
121
122
  - Run: npm run lint (ESLint + Prettier)
122
123
  - Auto-fix: npm run lint:fix
123
124
  - ESM imports require .js extension on relative paths
124
- - Report unfixable issues with file:line format`,
125
+ - Report unfixable issues with file:line format` + STRUCTURED_RESPONSE_SUFFIX,
125
126
  capabilities: ["lint", "format", "type-check", "security-scan"]
126
127
  });
127
128
  configs.set("review", {
@@ -136,7 +137,7 @@ Rules:
136
137
  - Flag: SQL injection, XSS, secret exposure, command injection
137
138
  - Flag: functions > 20 lines, cyclomatic complexity > 5
138
139
  - Flag: missing error handling at system boundaries
139
- - Suggest specific fixes, not vague improvements`,
140
+ - Suggest specific fixes, not vague improvements` + STRUCTURED_RESPONSE_SUFFIX,
140
141
  capabilities: [
141
142
  "review",
142
143
  "critique",
@@ -155,7 +156,7 @@ Rules:
155
156
  - Apply only the specific improvements requested \u2014 no scope creep
156
157
  - Maintain backward compatibility unless explicitly breaking
157
158
  - Run lint + tests after changes to verify nothing regressed
158
- - Keep changes minimal and focused`,
159
+ - Keep changes minimal and focused` + STRUCTURED_RESPONSE_SUFFIX,
159
160
  capabilities: ["enhance", "refactor", "optimize", "polish"]
160
161
  });
161
162
  configs.set("context", {
@@ -169,7 +170,7 @@ Rules:
169
170
  - Check docs/specs/ for ONE_PAGER.md, DEV_SPEC.md, PROMPT_PLAN.md
170
171
  - Check CLAUDE.md and AGENTS.md for project conventions
171
172
  - Search src/ for relevant implementations
172
- - Return concise summaries, not full file contents`,
173
+ - Return concise summaries, not full file contents` + STRUCTURED_RESPONSE_SUFFIX,
173
174
  capabilities: ["search", "retrieve", "summarize", "contextualize"]
174
175
  });
175
176
  configs.set("publish", {
@@ -183,7 +184,7 @@ Rules:
183
184
  - Verify lint + tests + build pass before any publish
184
185
  - Follow semver: breaking=major, feature=minor, fix=patch
185
186
  - Generate changelog from git log since last tag
186
- - Never force-push or skip pre-publish hooks`,
187
+ - Never force-push or skip pre-publish hooks` + STRUCTURED_RESPONSE_SUFFIX,
187
188
  capabilities: ["publish-npm", "github-release", "deploy", "document"]
188
189
  });
189
190
  return configs;
@@ -557,9 +558,13 @@ ${sections.join("\n\n")}` : "";
557
558
  const pricing = {
558
559
  "claude-sonnet-4-5-20250929": 15,
559
560
  "claude-haiku-4-5-20251001": 1,
560
- "claude-opus-4-6": 75
561
+ "claude-opus-4-6": 75,
562
+ // External providers — much cheaper
563
+ "llama-4-scout-17b-16e-instruct": 0.35,
564
+ "THUDM/glm-4-9b-chat": 0.06
561
565
  };
562
- return tokens / 1e6 * (pricing[model] || 10);
566
+ const modelName = typeof model === "string" ? model : model.model;
567
+ return tokens / 1e6 * (pricing[modelName] || 10);
563
568
  }
564
569
  countGeneratedTests(node) {
565
570
  let count = 0;
@@ -0,0 +1,162 @@
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
+ function levenshtein(a, b) {
6
+ const m = a.length;
7
+ const n = b.length;
8
+ const dp = Array.from(
9
+ { length: m + 1 },
10
+ () => new Array(n + 1).fill(0)
11
+ );
12
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
13
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
14
+ for (let i = 1; i <= m; i++) {
15
+ for (let j = 1; j <= n; j++) {
16
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
17
+ dp[i][j] = Math.min(
18
+ dp[i - 1][j] + 1,
19
+ dp[i][j - 1] + 1,
20
+ dp[i - 1][j - 1] + cost
21
+ );
22
+ }
23
+ }
24
+ return dp[m][n];
25
+ }
26
+ function normalizeWhitespace(text) {
27
+ return text.split("\n").map((line) => line.trim().replace(/\s+/g, " ")).join("\n");
28
+ }
29
+ function stripIndentation(text) {
30
+ return text.split("\n").map((line) => line.trimStart()).join("\n");
31
+ }
32
+ function fuzzyMatch(content, oldString, threshold = 0.85) {
33
+ if (!oldString) return null;
34
+ const exactIdx = content.indexOf(oldString);
35
+ if (exactIdx !== -1) {
36
+ return {
37
+ found: true,
38
+ startIndex: exactIdx,
39
+ endIndex: exactIdx + oldString.length,
40
+ confidence: 1,
41
+ matchedText: oldString,
42
+ method: "exact"
43
+ };
44
+ }
45
+ const normContent = normalizeWhitespace(content);
46
+ const normOld = normalizeWhitespace(oldString);
47
+ const normIdx = normContent.indexOf(normOld);
48
+ if (normIdx !== -1) {
49
+ const match = mapNormalizedToOriginal(
50
+ content,
51
+ normContent,
52
+ normOld,
53
+ normIdx
54
+ );
55
+ if (match) {
56
+ return {
57
+ found: true,
58
+ startIndex: match.start,
59
+ endIndex: match.end,
60
+ confidence: 0.95,
61
+ matchedText: content.slice(match.start, match.end),
62
+ method: "whitespace-normalized"
63
+ };
64
+ }
65
+ }
66
+ const stripContent = stripIndentation(content);
67
+ const stripOld = stripIndentation(oldString);
68
+ const stripIdx = stripContent.indexOf(stripOld);
69
+ if (stripIdx !== -1) {
70
+ const match = mapStrippedToOriginal(
71
+ content,
72
+ stripContent,
73
+ stripOld,
74
+ stripIdx
75
+ );
76
+ if (match) {
77
+ return {
78
+ found: true,
79
+ startIndex: match.start,
80
+ endIndex: match.end,
81
+ confidence: 0.9,
82
+ matchedText: content.slice(match.start, match.end),
83
+ method: "indentation-insensitive"
84
+ };
85
+ }
86
+ }
87
+ const contentLines = content.split("\n");
88
+ const oldLines = oldString.split("\n");
89
+ const windowSize = oldLines.length;
90
+ if (windowSize === 0 || contentLines.length < windowSize) return null;
91
+ let bestScore = 0;
92
+ let bestStart = -1;
93
+ let bestEnd = -1;
94
+ for (let i = 0; i <= contentLines.length - windowSize; i++) {
95
+ const windowText = contentLines.slice(i, i + windowSize).join("\n");
96
+ const maxLen = Math.max(windowText.length, oldString.length);
97
+ if (maxLen === 0) continue;
98
+ const dist = levenshtein(windowText, oldString);
99
+ const similarity = 1 - dist / maxLen;
100
+ if (similarity > bestScore) {
101
+ bestScore = similarity;
102
+ bestStart = i;
103
+ bestEnd = i + windowSize;
104
+ }
105
+ }
106
+ if (bestScore >= threshold && bestStart >= 0) {
107
+ let startCharIdx = 0;
108
+ for (let i = 0; i < bestStart; i++) {
109
+ startCharIdx += contentLines[i].length + 1;
110
+ }
111
+ let endCharIdx = startCharIdx;
112
+ for (let i = bestStart; i < bestEnd; i++) {
113
+ endCharIdx += contentLines[i].length + (i < bestEnd - 1 ? 1 : 0);
114
+ }
115
+ return {
116
+ found: true,
117
+ startIndex: startCharIdx,
118
+ endIndex: endCharIdx,
119
+ confidence: Math.round(bestScore * 100) / 100,
120
+ matchedText: contentLines.slice(bestStart, bestEnd).join("\n"),
121
+ method: "line-fuzzy"
122
+ };
123
+ }
124
+ return null;
125
+ }
126
+ function fuzzyEdit(content, oldString, newString, threshold = 0.85) {
127
+ const match = fuzzyMatch(content, oldString, threshold);
128
+ if (!match) return null;
129
+ const result = content.slice(0, match.startIndex) + newString + content.slice(match.endIndex);
130
+ return { content: result, match };
131
+ }
132
+ function mapNormalizedToOriginal(original, _normalized, normNeedle, normIdx) {
133
+ const normLines = _normalized.split("\n");
134
+ const origLines = original.split("\n");
135
+ let charCount = 0;
136
+ let startLine = 0;
137
+ for (let i = 0; i < normLines.length; i++) {
138
+ if (charCount + normLines[i].length >= normIdx) {
139
+ startLine = i;
140
+ break;
141
+ }
142
+ charCount += normLines[i].length + 1;
143
+ }
144
+ const needleLineCount = normNeedle.split("\n").length;
145
+ const endLine = startLine + needleLineCount;
146
+ let startChar = 0;
147
+ for (let i = 0; i < startLine; i++) {
148
+ startChar += origLines[i].length + 1;
149
+ }
150
+ let endChar = startChar;
151
+ for (let i = startLine; i < endLine && i < origLines.length; i++) {
152
+ endChar += origLines[i].length + (i < endLine - 1 ? 1 : 0);
153
+ }
154
+ return { start: startChar, end: endChar };
155
+ }
156
+ function mapStrippedToOriginal(original, stripped, stripNeedle, stripIdx) {
157
+ return mapNormalizedToOriginal(original, stripped, stripNeedle, stripIdx);
158
+ }
159
+ export {
160
+ fuzzyEdit,
161
+ fuzzyMatch
162
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackmemoryai/stackmemory",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.",
5
5
  "engines": {
6
6
  "node": ">=20.0.0",
@@ -93,6 +93,10 @@
93
93
  "format": "prettier --write src/**/*.ts scripts/**/*.ts",
94
94
  "test": "vitest",
95
95
  "test:ui": "vitest --ui",
96
+ "test:unit": "vitest run --project unit",
97
+ "test:integration": "vitest run --project integration",
98
+ "test:live": "vitest run --project live",
99
+ "test:all": "vitest run",
96
100
  "test:run": "vitest run",
97
101
  "test:pre-publish": "./scripts/test-pre-publish-quick.sh",
98
102
  "test:pre-commit": "vitest related --run --reporter=dot --silent --bail=1",