@stackmemoryai/stackmemory 1.2.0 → 1.2.2
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/claude-sm.js +65 -0
- package/dist/src/cli/commands/skills.js +123 -1
- package/dist/src/hooks/graphiti-hooks.js +149 -0
- package/dist/src/hooks/session-summary.js +30 -0
- package/dist/src/integrations/graphiti/linear-graphiti-bridge.js +115 -0
- package/dist/src/integrations/greptile/client.js +101 -0
- package/dist/src/integrations/greptile/config.js +14 -0
- package/dist/src/integrations/greptile/index.js +11 -0
- package/dist/src/integrations/greptile/types.js +4 -0
- package/dist/src/integrations/linear/webhook.js +16 -0
- package/dist/src/integrations/mcp/handlers/greptile-handlers.js +456 -0
- package/dist/src/integrations/mcp/server.js +136 -0
- package/dist/src/integrations/mcp/tool-definitions.js +53 -1
- package/dist/src/skills/claude-skills.js +46 -1
- package/dist/src/skills/parallel-agent-skill.js +514 -0
- package/dist/src/utils/hook-installer.js +155 -0
- package/package.json +2 -2
- package/scripts/gepa/.before-optimize.md +140 -0
- package/scripts/gepa/config.json +7 -1
- package/scripts/gepa/evals/fixtures/api-endpoint.ts +31 -0
- package/scripts/gepa/evals/fixtures/brittle-integration.ts +38 -0
- package/scripts/gepa/evals/fixtures/fts5-triggers.sql +23 -0
- package/scripts/gepa/evals/fixtures/leaky-service.ts +70 -0
- package/scripts/gepa/evals/fixtures/mcp-dispatch-stub.ts +39 -0
- package/scripts/gepa/evals/fixtures/pr-diff.txt +24 -0
- package/scripts/gepa/evals/fixtures/unsafe-webhook.ts +34 -0
- package/scripts/gepa/evals/fixtures/unwrapped-db-op.ts +42 -0
- package/scripts/gepa/evals/stackmemory-tasks.jsonl +8 -0
- package/scripts/gepa/generations/gen-000/baseline.md +48 -0
- package/scripts/gepa/generations/gen-001/baseline.md +172 -0
- package/scripts/gepa/generations/gen-001/variant-a.md +176 -0
- package/scripts/gepa/generations/gen-001/variant-b.md +266 -0
- package/scripts/gepa/generations/gen-001/variant-c.md +142 -0
- package/scripts/gepa/generations/gen-001/variant-d.md +172 -0
- package/scripts/gepa/hooks/reflect.js +44 -5
- package/scripts/gepa/optimize.js +281 -39
- package/scripts/gepa/results/eval-1-baseline.json +218 -0
- package/scripts/gepa/results/eval-1-variant-a.json +218 -0
- package/scripts/gepa/results/eval-1-variant-b.json +218 -0
- package/scripts/gepa/results/eval-1-variant-c.json +198 -0
- package/scripts/gepa/results/eval-1-variant-d.json +198 -0
- package/scripts/gepa/state.json +44 -5
- package/scripts/install-claude-hooks-auto.js +176 -44
- package/templates/claude-hooks/auto-checkpoint.js +174 -0
- package/templates/claude-hooks/chime-on-stop.sh +22 -0
- package/templates/claude-hooks/session-rescue.sh +15 -0
- package/templates/claude-hooks/stop-checkpoint.js +120 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Auto-Checkpoint Hook (PostToolUse)
|
|
5
|
+
*
|
|
6
|
+
* Counts tool calls per project session. Every 25 calls, writes a lightweight
|
|
7
|
+
* checkpoint to ~/.stackmemory/checkpoints/{project-hash}/latest.json.
|
|
8
|
+
* Must complete in <50ms -- pure file I/O only.
|
|
9
|
+
*
|
|
10
|
+
* State is keyed per-session (process.ppid) to avoid race conditions
|
|
11
|
+
* when multiple Claude instances run concurrently.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
|
|
18
|
+
const CHECKPOINT_INTERVAL = 25;
|
|
19
|
+
const SAVE_INTERVAL = 5;
|
|
20
|
+
const MAX_RECENT_TOOLS = 20;
|
|
21
|
+
const MAX_FILES_TRACKED = 50;
|
|
22
|
+
const TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
23
|
+
|
|
24
|
+
const HOME = process.env.HOME || '/tmp';
|
|
25
|
+
const SESSION_ID = process.env.CLAUDE_INSTANCE_ID || String(process.ppid);
|
|
26
|
+
const STATE_FILE = path.join(
|
|
27
|
+
HOME,
|
|
28
|
+
'.stackmemory',
|
|
29
|
+
`checkpoint-state-${SESSION_ID}.json`
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
function projectHash(cwd) {
|
|
33
|
+
return crypto.createHash('md5').update(cwd).digest('hex').slice(0, 12);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ensureDir(dir) {
|
|
37
|
+
if (!fs.existsSync(dir)) {
|
|
38
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function safeWriteFile(filePath, content) {
|
|
43
|
+
const tmp = filePath + '.tmp';
|
|
44
|
+
fs.writeFileSync(tmp, content);
|
|
45
|
+
fs.renameSync(tmp, filePath);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function loadState() {
|
|
49
|
+
try {
|
|
50
|
+
if (fs.existsSync(STATE_FILE)) {
|
|
51
|
+
const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
|
|
52
|
+
const cutoff = Date.now() - TTL_MS;
|
|
53
|
+
for (const [cwd, proj] of Object.entries(state.projects)) {
|
|
54
|
+
if (
|
|
55
|
+
proj.sessionStart &&
|
|
56
|
+
new Date(proj.sessionStart).getTime() < cutoff
|
|
57
|
+
) {
|
|
58
|
+
delete state.projects[cwd];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return state;
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
// Corrupted state, start fresh
|
|
65
|
+
}
|
|
66
|
+
return { projects: {} };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function saveState(state) {
|
|
70
|
+
try {
|
|
71
|
+
ensureDir(path.dirname(STATE_FILE));
|
|
72
|
+
safeWriteFile(STATE_FILE, JSON.stringify(state));
|
|
73
|
+
} catch {
|
|
74
|
+
// Best-effort
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getProjectState(state, cwd) {
|
|
79
|
+
if (!state.projects[cwd]) {
|
|
80
|
+
state.projects[cwd] = {
|
|
81
|
+
toolCount: 0,
|
|
82
|
+
checkpointCount: 0,
|
|
83
|
+
lastCheckpoint: null,
|
|
84
|
+
sessionStart: new Date().toISOString(),
|
|
85
|
+
filesModified: [],
|
|
86
|
+
recentTools: [],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return state.projects[cwd];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function writeCheckpoint(proj, cwd) {
|
|
93
|
+
const hash = projectHash(cwd);
|
|
94
|
+
const cpDir = path.join(HOME, '.stackmemory', 'checkpoints', hash);
|
|
95
|
+
ensureDir(cpDir);
|
|
96
|
+
|
|
97
|
+
const checkpoint = {
|
|
98
|
+
toolCount: proj.toolCount,
|
|
99
|
+
checkpointNumber: proj.checkpointCount,
|
|
100
|
+
timestamp: new Date().toISOString(),
|
|
101
|
+
filesModified: proj.filesModified.slice(-MAX_FILES_TRACKED),
|
|
102
|
+
recentTools: proj.recentTools.slice(-MAX_RECENT_TOOLS),
|
|
103
|
+
cwd,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
safeWriteFile(
|
|
107
|
+
path.join(cpDir, 'latest.json'),
|
|
108
|
+
JSON.stringify(checkpoint, null, 2)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function trackFile(proj, toolInput) {
|
|
113
|
+
const filePath = toolInput?.file_path;
|
|
114
|
+
if (filePath && !proj.filesModified.includes(filePath)) {
|
|
115
|
+
proj.filesModified.push(filePath);
|
|
116
|
+
if (proj.filesModified.length > MAX_FILES_TRACKED) {
|
|
117
|
+
proj.filesModified = proj.filesModified.slice(-MAX_FILES_TRACKED);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function readInput() {
|
|
123
|
+
let input = '';
|
|
124
|
+
for await (const chunk of process.stdin) {
|
|
125
|
+
input += chunk;
|
|
126
|
+
}
|
|
127
|
+
return JSON.parse(input);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function main() {
|
|
131
|
+
try {
|
|
132
|
+
const input = await readInput();
|
|
133
|
+
const { tool_name, tool_input } = input;
|
|
134
|
+
|
|
135
|
+
const cwd = process.cwd();
|
|
136
|
+
const state = loadState();
|
|
137
|
+
const proj = getProjectState(state, cwd);
|
|
138
|
+
|
|
139
|
+
// Increment counter
|
|
140
|
+
proj.toolCount++;
|
|
141
|
+
|
|
142
|
+
// Track tool name
|
|
143
|
+
proj.recentTools.push(tool_name);
|
|
144
|
+
if (proj.recentTools.length > MAX_RECENT_TOOLS) {
|
|
145
|
+
proj.recentTools = proj.recentTools.slice(-MAX_RECENT_TOOLS);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Track modified files from Edit/Write
|
|
149
|
+
if (
|
|
150
|
+
tool_name === 'Edit' ||
|
|
151
|
+
tool_name === 'Write' ||
|
|
152
|
+
tool_name === 'MultiEdit'
|
|
153
|
+
) {
|
|
154
|
+
trackFile(proj, tool_input);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Checkpoint every N calls
|
|
158
|
+
const isCheckpoint = proj.toolCount % CHECKPOINT_INTERVAL === 0;
|
|
159
|
+
if (isCheckpoint) {
|
|
160
|
+
proj.checkpointCount++;
|
|
161
|
+
proj.lastCheckpoint = new Date().toISOString();
|
|
162
|
+
writeCheckpoint(proj, cwd);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Only persist state every SAVE_INTERVAL calls or on checkpoint boundaries
|
|
166
|
+
if (isCheckpoint || proj.toolCount % SAVE_INTERVAL === 0) {
|
|
167
|
+
saveState(state);
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
// Silent fail -- never block the agent
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
main();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Chime once when Claude Code needs user input
|
|
3
|
+
# Uses macOS system sound
|
|
4
|
+
|
|
5
|
+
# Prevent multiple chimes by checking if we recently played
|
|
6
|
+
CHIME_LOCK="/tmp/claude-chime-lock"
|
|
7
|
+
CHIME_COOLDOWN=2 # seconds
|
|
8
|
+
|
|
9
|
+
if [ -f "$CHIME_LOCK" ]; then
|
|
10
|
+
LAST_CHIME=$(cat "$CHIME_LOCK")
|
|
11
|
+
NOW=$(date +%s)
|
|
12
|
+
DIFF=$((NOW - LAST_CHIME))
|
|
13
|
+
if [ "$DIFF" -lt "$CHIME_COOLDOWN" ]; then
|
|
14
|
+
exit 0 # Skip chime if too recent
|
|
15
|
+
fi
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
# Record chime time
|
|
19
|
+
date +%s > "$CHIME_LOCK"
|
|
20
|
+
|
|
21
|
+
# Play system sound (Glass is a nice subtle chime)
|
|
22
|
+
afplay /System/Library/Sounds/Glass.aiff &
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# session-rescue.sh — Auto-capture stackmemory handoff on session close
|
|
3
|
+
# Runs as a Stop hook. Silent on failure. Skips non-stackmemory projects.
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
# Only run in stackmemory-initialized projects
|
|
8
|
+
[ -d ".stackmemory" ] || exit 0
|
|
9
|
+
|
|
10
|
+
# Require stackmemory CLI
|
|
11
|
+
command -v stackmemory >/dev/null 2>&1 || exit 0
|
|
12
|
+
|
|
13
|
+
# Capture without committing, compact format, 10s timeout
|
|
14
|
+
timeout 10 stackmemory capture --no-commit --format compact \
|
|
15
|
+
-m "auto-rescue on session close" >/dev/null 2>&1 || true
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stop-Checkpoint Hook
|
|
5
|
+
*
|
|
6
|
+
* Fires on session stop. Reads checkpoint state for current project.
|
|
7
|
+
* If 3+ checkpoints accumulated, outputs a "context heavy" message.
|
|
8
|
+
* Writes a final checkpoint with session stop metadata.
|
|
9
|
+
*
|
|
10
|
+
* State is keyed per-session (process.ppid) to avoid race conditions
|
|
11
|
+
* when multiple Claude instances run concurrently.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
|
|
18
|
+
const HOME = process.env.HOME || '/tmp';
|
|
19
|
+
const SESSION_ID = process.env.CLAUDE_INSTANCE_ID || String(process.ppid);
|
|
20
|
+
const STATE_FILE = path.join(
|
|
21
|
+
HOME,
|
|
22
|
+
'.stackmemory',
|
|
23
|
+
`checkpoint-state-${SESSION_ID}.json`
|
|
24
|
+
);
|
|
25
|
+
const MAX_FILES_TRACKED = 50;
|
|
26
|
+
const MAX_RECENT_TOOLS = 20;
|
|
27
|
+
|
|
28
|
+
function projectHash(cwd) {
|
|
29
|
+
return crypto.createHash('md5').update(cwd).digest('hex').slice(0, 12);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ensureDir(dir) {
|
|
33
|
+
if (!fs.existsSync(dir)) {
|
|
34
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function safeWriteFile(filePath, content) {
|
|
39
|
+
const tmp = filePath + '.tmp';
|
|
40
|
+
fs.writeFileSync(tmp, content);
|
|
41
|
+
fs.renameSync(tmp, filePath);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function loadState() {
|
|
45
|
+
try {
|
|
46
|
+
if (fs.existsSync(STATE_FILE)) {
|
|
47
|
+
const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
|
|
48
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
49
|
+
for (const [cwd, proj] of Object.entries(state.projects)) {
|
|
50
|
+
if (
|
|
51
|
+
proj.sessionStart &&
|
|
52
|
+
new Date(proj.sessionStart).getTime() < cutoff
|
|
53
|
+
) {
|
|
54
|
+
delete state.projects[cwd];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return state;
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// Corrupted state, start fresh
|
|
61
|
+
}
|
|
62
|
+
return { projects: {} };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function main() {
|
|
66
|
+
try {
|
|
67
|
+
const cwd = process.cwd();
|
|
68
|
+
const state = loadState();
|
|
69
|
+
const proj = state.projects?.[cwd];
|
|
70
|
+
|
|
71
|
+
if (!proj) return;
|
|
72
|
+
|
|
73
|
+
const { toolCount, checkpointCount, sessionStart, filesModified } = proj;
|
|
74
|
+
|
|
75
|
+
// Write final checkpoint
|
|
76
|
+
const hash = projectHash(cwd);
|
|
77
|
+
const cpDir = path.join(HOME, '.stackmemory', 'checkpoints', hash);
|
|
78
|
+
ensureDir(cpDir);
|
|
79
|
+
|
|
80
|
+
// NaN duration guard
|
|
81
|
+
const startMs = sessionStart ? new Date(sessionStart).getTime() : 0;
|
|
82
|
+
const duration =
|
|
83
|
+
startMs > 0 ? Math.round((Date.now() - startMs) / 1000) : 0;
|
|
84
|
+
|
|
85
|
+
const finalCheckpoint = {
|
|
86
|
+
reason: 'session_stop',
|
|
87
|
+
toolCount,
|
|
88
|
+
checkpointNumber: checkpointCount,
|
|
89
|
+
totalFiles: filesModified?.length || 0,
|
|
90
|
+
duration,
|
|
91
|
+
timestamp: new Date().toISOString(),
|
|
92
|
+
filesModified: (filesModified || []).slice(-MAX_FILES_TRACKED),
|
|
93
|
+
recentTools: (proj.recentTools || []).slice(-MAX_RECENT_TOOLS),
|
|
94
|
+
cwd,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
safeWriteFile(
|
|
98
|
+
path.join(cpDir, 'latest.json'),
|
|
99
|
+
JSON.stringify(finalCheckpoint, null, 2)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Output context-heavy message if 3+ checkpoints
|
|
103
|
+
if (checkpointCount >= 3) {
|
|
104
|
+
console.error(
|
|
105
|
+
`\nContext heavy (${checkpointCount} checkpoints, ${toolCount} tool calls). State saved. Consider /clear to reload from checkpoint.\n`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Clean up session state file since session is ending
|
|
110
|
+
try {
|
|
111
|
+
fs.unlinkSync(STATE_FILE);
|
|
112
|
+
} catch {
|
|
113
|
+
// Best-effort cleanup
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// Silent fail -- never block the agent
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
main();
|