@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,198 @@
|
|
|
1
|
+
{
|
|
2
|
+
"variant": "variant-d",
|
|
3
|
+
"generation": 1,
|
|
4
|
+
"results": [
|
|
5
|
+
{
|
|
6
|
+
"taskId": "eval-001",
|
|
7
|
+
"taskName": "simple_function",
|
|
8
|
+
"weight": 1,
|
|
9
|
+
"passed": true,
|
|
10
|
+
"passRate": 0.6666666666666666,
|
|
11
|
+
"criteria": {
|
|
12
|
+
"has_function": {
|
|
13
|
+
"passed": true,
|
|
14
|
+
"reason": "A complete, working function `isPalindrome` is defined in TypeScript with proper syntax, parameters, return type, and implementation."
|
|
15
|
+
},
|
|
16
|
+
"handles_edge_cases": {
|
|
17
|
+
"passed": true,
|
|
18
|
+
"reason": "The code handles edge cases: empty strings (returns true), mixed case (lowercase normalization), and special characters (regex removal of non-alphanumeric)."
|
|
19
|
+
},
|
|
20
|
+
"is_tested": {
|
|
21
|
+
"passed": false,
|
|
22
|
+
"reason": "The output shows example outputs but provides no actual test cases, unit tests, or test function definitions. Examples are illustrative documentation, not executable tests."
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"judgeMode": "llm",
|
|
26
|
+
"duration": 26370,
|
|
27
|
+
"output": "I've written a palindrome checker function at `/private/tmp/gepa-eval-X4niM9/isPalindrome.ts`:\n\n```typescript\nexport function isPalindrome(str: string): boolean {\n if (!str) return true;\n\n const normalized = str.toLowerCase().replace(/[^a-z0-9]/g, '');\n const reversed = normalized.split('').reverse().join('');\n\n return normalized === reversed;\n}\n```\n\n**Features**:\n- Returns `true` for empty strings\n- Normalizes by converting to lowercase and removing non-alphanumeric characters\n- Compares the normalized string with its reverse\n\n**Examples**:\n- `isPalindrome(\"racecar\")` → `true`\n- `isPalindrome(\"A man, a plan, a canal: Panama\")` → `true`\n- `isPalindrome(\"hello\")` → `false`\n"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"taskId": "eval-002",
|
|
31
|
+
"taskName": "refactor_code",
|
|
32
|
+
"weight": 1.5,
|
|
33
|
+
"passed": false,
|
|
34
|
+
"passRate": 0,
|
|
35
|
+
"criteria": {
|
|
36
|
+
"uses_async": {
|
|
37
|
+
"passed": false,
|
|
38
|
+
"reason": "No async/await code is provided; AI only requests the original code instead of demonstrating the refactored solution"
|
|
39
|
+
},
|
|
40
|
+
"preserves_behavior": {
|
|
41
|
+
"passed": false,
|
|
42
|
+
"reason": "No refactored code is shown, so behavior preservation cannot be verified"
|
|
43
|
+
},
|
|
44
|
+
"no_nested_callbacks": {
|
|
45
|
+
"passed": false,
|
|
46
|
+
"reason": "No refactored code is demonstrated; the output is a request for input rather than a solution"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"judgeMode": "llm",
|
|
50
|
+
"duration": 8335,
|
|
51
|
+
"output": "I need to see the code you want to refactor. Could you please provide the file path or share the code that uses callbacks?\n"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"taskId": "eval-003",
|
|
55
|
+
"taskName": "fix_bug",
|
|
56
|
+
"weight": 1.2,
|
|
57
|
+
"passed": true,
|
|
58
|
+
"passRate": 0.6666666666666666,
|
|
59
|
+
"criteria": {
|
|
60
|
+
"bug_fixed": {
|
|
61
|
+
"passed": false,
|
|
62
|
+
"reason": "The output only describes the fix but does not show the actual fixed code. No corrected code snippet is provided to verify the bug is actually fixed."
|
|
63
|
+
},
|
|
64
|
+
"no_new_bugs": {
|
|
65
|
+
"passed": true,
|
|
66
|
+
"reason": "The suggested fix (changing `i <= items.length` to `i < items.length`) is a standard, correct solution for off-by-one errors and introduces no new bugs."
|
|
67
|
+
},
|
|
68
|
+
"explains_fix": {
|
|
69
|
+
"passed": true,
|
|
70
|
+
"reason": "The output clearly explains what was wrong (accessing undefined index 3 on a 3-element array) and why the fix works (using < instead of <=)."
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"judgeMode": "llm",
|
|
74
|
+
"duration": 25447,
|
|
75
|
+
"output": "Fixed buggy-loop.js:6 - changed `i <= items.length` to `i < items.length` to prevent accessing `items[3]` (undefined) on a 3-element array.\n"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"taskId": "eval-004",
|
|
79
|
+
"taskName": "add_feature",
|
|
80
|
+
"weight": 1.5,
|
|
81
|
+
"passed": false,
|
|
82
|
+
"passRate": 0,
|
|
83
|
+
"criteria": {
|
|
84
|
+
"has_pagination": {
|
|
85
|
+
"passed": false,
|
|
86
|
+
"reason": "No code provided in output - empty code block"
|
|
87
|
+
},
|
|
88
|
+
"has_limit": {
|
|
89
|
+
"passed": false,
|
|
90
|
+
"reason": "No code provided in output - empty code block"
|
|
91
|
+
},
|
|
92
|
+
"has_offset": {
|
|
93
|
+
"passed": false,
|
|
94
|
+
"reason": "No code provided in output - empty code block"
|
|
95
|
+
},
|
|
96
|
+
"backwards_compatible": {
|
|
97
|
+
"passed": false,
|
|
98
|
+
"reason": "No code provided in output - empty code block"
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"judgeMode": "llm",
|
|
102
|
+
"duration": 20951,
|
|
103
|
+
"output": "\n"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"taskId": "eval-005",
|
|
107
|
+
"taskName": "code_review",
|
|
108
|
+
"weight": 1.3,
|
|
109
|
+
"passed": true,
|
|
110
|
+
"passRate": 0.6666666666666666,
|
|
111
|
+
"criteria": {
|
|
112
|
+
"identifies_security_issue": {
|
|
113
|
+
"passed": true,
|
|
114
|
+
"reason": "Output correctly identifies 5 legitimate security vulnerabilities: SQL injection, plaintext password comparison, sensitive data logging, insecure session tokens, and missing HttpOnly cookie flags with clear code examples and impact analysis"
|
|
115
|
+
},
|
|
116
|
+
"identifies_performance_issue": {
|
|
117
|
+
"passed": false,
|
|
118
|
+
"reason": "Output does not identify any performance issues. The review focuses exclusively on security vulnerabilities with no mention of performance, scalability, or optimization concerns"
|
|
119
|
+
},
|
|
120
|
+
"actionable_feedback": {
|
|
121
|
+
"passed": true,
|
|
122
|
+
"reason": "Output provides specific, actionable fixes for each issue including exact code examples (e.g., parameterized queries, bcrypt.compare, crypto.randomBytes, cookie options) that developers can immediately implement"
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
"judgeMode": "llm",
|
|
126
|
+
"duration": 29814,
|
|
127
|
+
"output": "## Critical Security Issues Found\n\nThis PR contains **MULTIPLE SEVERE SECURITY VULNERABILITIES** that must be rejected:\n\n### 🔴 CRITICAL: SQL Injection (Line 10)\n```typescript\nconst user = await db.query(`SELECT * FROM users WHERE email = '${email}'`);\n```\n**Issue**: Direct string interpolation enables SQL injection attacks \n**Attack**: `email = \"' OR '1'='1' --\"` bypasses authentication \n**Fix**: Revert to parameterized query: `db.query('SELECT * FROM users WHERE email = $1', [email])`\n\n### 🔴 CRITICAL: Plaintext Password Comparison (Line 14)\n```typescript\nconst valid = password === user.passwordHash;\n```\n**Issue**: Removes bcrypt password hashing, comparing plaintext to hash \n**Impact**: Authentication always fails OR passwords stored in plaintext \n**Fix**: Revert to `await bcrypt.compare(password, user.passwordHash)`\n\n### 🔴 CRITICAL: Sensitive Data Logging (Line 22)\n```typescript\nconsole.log(`Login: ${email} / ${password}`);\n```\n**Issue**: Logs plaintext passwords to console/logs \n**Impact**: Password exposure in log files, GDPR/compliance violation \n**Fix**: Remove this line entirely or log only: `console.log(\\`Login attempt: ${email}\\`)`\n\n### 🔴 HIGH: Insecure Session Token (Line 18)\n```typescript\nconst token = email + ':' + Date.now();\n```\n**Issue**: Predictable token generation (email + timestamp) \n**Impact**: Session hijacking via token guessing \n**Fix**: Use cryptographically secure random tokens: `crypto.randomBytes(32).toString('hex')`\n\n### 🟡 MEDIUM: Missing HttpOnly Cookie Flag\n```typescript\nres.cookie('session', token);\n```\n**Issue**: Cookie accessible via JavaScript (XSS vulnerability) \n**Fix**: `res.cookie('session', token, { httpOnly: true, secure: true, sameSite: 'strict' })`\n\n## Recommendation\n\n**REJECT THIS PR IMMEDIATELY** - Contains multiple critical security vulnerabilities including SQL injection and plaintext password exposure. The \"quick fix\" comment on line 9 is a red flag for introducing security issues.\n\nAll changes should be *"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"taskId": "sm-001",
|
|
131
|
+
"taskName": "add_mcp_tool_handler",
|
|
132
|
+
"weight": 1.5,
|
|
133
|
+
"passed": true,
|
|
134
|
+
"passRate": 1,
|
|
135
|
+
"criteria": {
|
|
136
|
+
"has_switch_case": {
|
|
137
|
+
"passed": true,
|
|
138
|
+
"reason": "Code shows a case statement 'get_frame_summary' within a switch/case structure, following the dispatch pattern"
|
|
139
|
+
},
|
|
140
|
+
"has_zod_schema": {
|
|
141
|
+
"passed": true,
|
|
142
|
+
"reason": "Zod schema is defined inline: z.object({ frameId: z.string() }), validating the required frameId parameter"
|
|
143
|
+
},
|
|
144
|
+
"validates_input": {
|
|
145
|
+
"passed": true,
|
|
146
|
+
"reason": "Input validation occurs via .parse(args) which will throw if frameId is missing or not a string"
|
|
147
|
+
},
|
|
148
|
+
"returns_typed_response": {
|
|
149
|
+
"passed": true,
|
|
150
|
+
"reason": "Return statement explicitly returns an object with the four required properties: frameId, name, status, eventCount"
|
|
151
|
+
},
|
|
152
|
+
"handles_not_found": {
|
|
153
|
+
"passed": true,
|
|
154
|
+
"reason": "Code includes explicit null check (if (!frame)) with error thrown when frame is not found in the frames map"
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
"judgeMode": "llm",
|
|
158
|
+
"duration": 33763,
|
|
159
|
+
"output": "I've added the `get_frame_summary` handler to the MCP server dispatch pattern. The implementation:\n\n**Added case** (mcp-dispatch-stub.ts:32-42):\n```typescript\ncase 'get_frame_summary': {\n const input = z.object({ frameId: z.string() }).parse(args);\n const frame = frames.get(input.frameId);\n if (!frame) throw new Error(`Frame not found: ${input.frameId}`);\n return {\n frameId: frame.id,\n name: frame.name,\n status: frame.status,\n eventCount: frame.events.length\n };\n}\n```\n\n**Features**:\n- ✓ Zod validation for required `frameId` string parameter\n- ✓ Follows existing switch/case dispatch pattern\n- ✓ Returns `{ frameId, name, status, eventCount }` as specified\n- ✓ Error handling for missing frames (consistent with `close_frame`)\n- ✓ Calculates `eventCount` from `frame.events.length`\n\nThe handler integrates seamlessly with the existing `start_frame` and `close_frame` tools.\n"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"taskId": "sm-002",
|
|
163
|
+
"taskName": "fix_fts5_trigger_bug",
|
|
164
|
+
"weight": 1.8,
|
|
165
|
+
"passed": true,
|
|
166
|
+
"passRate": 1,
|
|
167
|
+
"criteria": {
|
|
168
|
+
"has_delete_trigger": {
|
|
169
|
+
"passed": true,
|
|
170
|
+
"reason": "A DELETE trigger named 'frames_ad' is defined with AFTER DELETE ON frames syntax"
|
|
171
|
+
},
|
|
172
|
+
"uses_fts_delete_syntax": {
|
|
173
|
+
"passed": true,
|
|
174
|
+
"reason": "The trigger uses FTS5 special syntax with INSERT INTO frames_fts(frames_fts, ...) VALUES ('delete', ...) which is the correct way to delete from FTS index"
|
|
175
|
+
},
|
|
176
|
+
"references_old_row": {
|
|
177
|
+
"passed": true,
|
|
178
|
+
"reason": "The trigger references old.rowid, old.name, old.digest_text, and old.inputs, old.outputs to access the deleted row values"
|
|
179
|
+
},
|
|
180
|
+
"matches_column_list": {
|
|
181
|
+
"passed": true,
|
|
182
|
+
"reason": "The column list (rowid, name, digest_text, inputs, outputs) matches the FTS index structure referenced in the INSERT and UPDATE triggers"
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
"judgeMode": "llm",
|
|
186
|
+
"duration": 34003,
|
|
187
|
+
"output": "I've added the missing DELETE trigger following the exact pattern of the existing triggers:\n\n**Pattern analysis:**\n- INSERT trigger (`frames_ai`): Adds new row to FTS index\n- UPDATE trigger (`frames_au`): Deletes old row, inserts new row\n- DELETE trigger (`frames_ad`): Deletes row from FTS index\n\n**Implementation:**\n```sql\nCREATE TRIGGER IF NOT EXISTS frames_ad AFTER DELETE ON frames BEGIN\n INSERT INTO frames_fts(frames_fts, rowid, name, digest_text, inputs, outputs)\n VALUES ('delete', old.rowid, old.name, old.digest_text, old.inputs, old.outputs);\nEND;\n```\n\nThe trigger uses the FTS5 special command syntax where `INSERT INTO frames_fts(frames_fts, ...)` with `'delete'` as the first value removes the entry from the full-text index.\n"
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"taskId": "sm-003",
|
|
191
|
+
"taskName": "daemon_service_lifecycle",
|
|
192
|
+
"weight": 1.5,
|
|
193
|
+
"passed": false,
|
|
194
|
+
"duration": 120042,
|
|
195
|
+
"error": "claude timed out after 120000ms"
|
|
196
|
+
}
|
|
197
|
+
]
|
|
198
|
+
}
|
package/scripts/gepa/state.json
CHANGED
|
@@ -1,14 +1,53 @@
|
|
|
1
1
|
{
|
|
2
|
-
"currentGeneration":
|
|
3
|
-
"bestVariant": "
|
|
4
|
-
"bestScore": 0,
|
|
5
|
-
"targetPath": "
|
|
2
|
+
"currentGeneration": 1,
|
|
3
|
+
"bestVariant": "variant-d",
|
|
4
|
+
"bestScore": 0.4985250737463126,
|
|
5
|
+
"targetPath": "/Users/jwu/Dev/stackmemory/CLAUDE.md",
|
|
6
6
|
"history": [
|
|
7
7
|
{
|
|
8
8
|
"generation": 0,
|
|
9
9
|
"variant": "baseline",
|
|
10
10
|
"action": "init",
|
|
11
|
-
"timestamp": "2026-02-
|
|
11
|
+
"timestamp": "2026-02-18T22:59:36.437Z"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"generation": 1,
|
|
15
|
+
"action": "mutate",
|
|
16
|
+
"variants": [
|
|
17
|
+
"variant-a",
|
|
18
|
+
"variant-b",
|
|
19
|
+
"variant-c",
|
|
20
|
+
"variant-d"
|
|
21
|
+
],
|
|
22
|
+
"timestamp": "2026-02-18T23:03:13.997Z"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"generation": 1,
|
|
26
|
+
"action": "select",
|
|
27
|
+
"scores": [
|
|
28
|
+
{
|
|
29
|
+
"variant": "variant-d",
|
|
30
|
+
"score": 0.4985250737463126
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"variant": "variant-b",
|
|
34
|
+
"score": 0.3938053097345133
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"variant": "variant-c",
|
|
38
|
+
"score": 0.23230088495575218
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"variant": "baseline",
|
|
42
|
+
"score": 0.21165191740412978
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"variant": "variant-a",
|
|
46
|
+
"score": 0.1762536873156342
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
"best": "variant-d",
|
|
50
|
+
"timestamp": "2026-02-18T23:23:18.544Z"
|
|
12
51
|
}
|
|
13
52
|
]
|
|
14
53
|
}
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* This runs as a postinstall script to set up tracing hooks and daemon
|
|
6
6
|
*
|
|
7
7
|
* INTERACTIVE: Asks for user consent before modifying ~/.claude
|
|
8
|
+
*
|
|
9
|
+
* Writes to ~/.claude/settings.json (the current Claude Code config format).
|
|
10
|
+
* Uses CANONICAL_HOOKS from hook-installer when dist is available, otherwise
|
|
11
|
+
* falls back to an inline definition.
|
|
8
12
|
*/
|
|
9
13
|
|
|
10
14
|
import {
|
|
@@ -13,6 +17,8 @@ import {
|
|
|
13
17
|
copyFileSync,
|
|
14
18
|
readFileSync,
|
|
15
19
|
writeFileSync,
|
|
20
|
+
renameSync,
|
|
21
|
+
chmodSync,
|
|
16
22
|
} from 'fs';
|
|
17
23
|
import { join } from 'path';
|
|
18
24
|
import { homedir } from 'os';
|
|
@@ -25,11 +31,144 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
25
31
|
const __dirname = dirname(__filename);
|
|
26
32
|
|
|
27
33
|
const claudeHooksDir = join(homedir(), '.claude', 'hooks');
|
|
28
|
-
const
|
|
34
|
+
const claudeSettingsFile = join(homedir(), '.claude', 'settings.json');
|
|
29
35
|
const templatesDir = join(__dirname, '..', 'templates', 'claude-hooks');
|
|
30
36
|
const stackmemoryBinDir = join(homedir(), '.stackmemory', 'bin');
|
|
31
37
|
const distDir = join(__dirname, '..', 'dist');
|
|
32
38
|
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Try to import from compiled hook-installer; fall back to inline definitions
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/** @type {Array<{scriptName: string, eventType: string, matcher?: string, timeout?: number, commandPrefix?: string, required: boolean}>} */
|
|
44
|
+
let CANONICAL_HOOKS;
|
|
45
|
+
let mergeSettingsFn;
|
|
46
|
+
let readSettingsFn;
|
|
47
|
+
let writeSettingsAtomicFn;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const mod = await import('../dist/src/utils/hook-installer.js');
|
|
51
|
+
CANONICAL_HOOKS = mod.CANONICAL_HOOKS;
|
|
52
|
+
mergeSettingsFn = mod.mergeSettings;
|
|
53
|
+
readSettingsFn = mod.readSettings;
|
|
54
|
+
writeSettingsAtomicFn = mod.writeSettingsAtomic;
|
|
55
|
+
} catch {
|
|
56
|
+
// dist not built yet — use inline fallback
|
|
57
|
+
CANONICAL_HOOKS = [
|
|
58
|
+
{
|
|
59
|
+
scriptName: 'session-rescue.sh',
|
|
60
|
+
eventType: 'Stop',
|
|
61
|
+
timeout: 12,
|
|
62
|
+
required: true,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
scriptName: 'stop-checkpoint.js',
|
|
66
|
+
eventType: 'Stop',
|
|
67
|
+
timeout: 5,
|
|
68
|
+
commandPrefix: 'node',
|
|
69
|
+
required: true,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
scriptName: 'chime-on-stop.sh',
|
|
73
|
+
eventType: 'Stop',
|
|
74
|
+
timeout: 2,
|
|
75
|
+
required: true,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
scriptName: 'auto-checkpoint.js',
|
|
79
|
+
eventType: 'PostToolUse',
|
|
80
|
+
timeout: 2,
|
|
81
|
+
commandPrefix: 'node',
|
|
82
|
+
required: true,
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const DEAD_HOOKS = ['sms-response-handler.js'];
|
|
87
|
+
|
|
88
|
+
readSettingsFn = (p) => {
|
|
89
|
+
try {
|
|
90
|
+
if (existsSync(p)) return JSON.parse(readFileSync(p, 'utf8'));
|
|
91
|
+
} catch {
|
|
92
|
+
/* parse error */
|
|
93
|
+
}
|
|
94
|
+
return {};
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
writeSettingsAtomicFn = (settings, p) => {
|
|
98
|
+
const dir = dirname(p);
|
|
99
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
100
|
+
const tmp = p + '.tmp';
|
|
101
|
+
writeFileSync(tmp, JSON.stringify(settings, null, 2) + '\n');
|
|
102
|
+
renameSync(tmp, p);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
mergeSettingsFn = (existing, hooksDir) => {
|
|
106
|
+
const merged = JSON.parse(JSON.stringify(existing));
|
|
107
|
+
if (!merged.hooks) merged.hooks = {};
|
|
108
|
+
|
|
109
|
+
// Remove dead hooks
|
|
110
|
+
for (const eventType of Object.keys(merged.hooks)) {
|
|
111
|
+
const groups = merged.hooks[eventType];
|
|
112
|
+
for (const group of groups) {
|
|
113
|
+
group.hooks = group.hooks.filter((h) => {
|
|
114
|
+
for (const dead of DEAD_HOOKS) {
|
|
115
|
+
if (h.command.includes(dead)) return false;
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
merged.hooks[eventType] = groups.filter((g) => g.hooks.length > 0);
|
|
121
|
+
if (merged.hooks[eventType].length === 0) delete merged.hooks[eventType];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Add missing canonical hooks
|
|
125
|
+
for (const entry of CANONICAL_HOOKS) {
|
|
126
|
+
const exists = (merged.hooks[entry.eventType] || []).some((group) =>
|
|
127
|
+
group.hooks.some((h) => h.command.includes(entry.scriptName))
|
|
128
|
+
);
|
|
129
|
+
if (exists) continue;
|
|
130
|
+
|
|
131
|
+
const scriptPath = join(hooksDir, entry.scriptName);
|
|
132
|
+
const command = entry.commandPrefix
|
|
133
|
+
? `${entry.commandPrefix} ${scriptPath}`
|
|
134
|
+
: scriptPath;
|
|
135
|
+
const hookCmd = { type: 'command', command };
|
|
136
|
+
if (entry.timeout) hookCmd.timeout = entry.timeout;
|
|
137
|
+
|
|
138
|
+
const eventGroups = merged.hooks[entry.eventType] || [];
|
|
139
|
+
const matcherValue = entry.matcher;
|
|
140
|
+
let targetGroup = eventGroups.find((g) =>
|
|
141
|
+
matcherValue ? g.matcher === matcherValue : !g.matcher
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if (targetGroup) {
|
|
145
|
+
targetGroup.hooks.push(hookCmd);
|
|
146
|
+
} else {
|
|
147
|
+
const newGroup = { hooks: [hookCmd] };
|
|
148
|
+
if (matcherValue) newGroup.matcher = matcherValue;
|
|
149
|
+
eventGroups.push(newGroup);
|
|
150
|
+
}
|
|
151
|
+
merged.hooks[entry.eventType] = eventGroups;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return merged;
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Legacy hook files (still installed for backward-compat tooling hooks)
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
const LEGACY_HOOK_FILES = [
|
|
163
|
+
'tool-use-trace.js',
|
|
164
|
+
'on-startup.js',
|
|
165
|
+
'on-clear.js',
|
|
166
|
+
'on-task-complete.js',
|
|
167
|
+
'skill-eval.sh',
|
|
168
|
+
'skill-eval.cjs',
|
|
169
|
+
];
|
|
170
|
+
const LEGACY_DATA_FILES = ['skill-rules.json'];
|
|
171
|
+
|
|
33
172
|
/**
|
|
34
173
|
* Ask user for confirmation before installing hooks
|
|
35
174
|
* Returns true if user consents, false otherwise
|
|
@@ -51,14 +190,13 @@ async function askForConsent() {
|
|
|
51
190
|
return false;
|
|
52
191
|
}
|
|
53
192
|
|
|
54
|
-
console.log('\
|
|
193
|
+
console.log('\nStackMemory Post-Install\n');
|
|
55
194
|
console.log(
|
|
56
195
|
'StackMemory can integrate with Claude Code by installing hooks that:'
|
|
57
196
|
);
|
|
58
197
|
console.log(' - Track tool usage for better context');
|
|
59
198
|
console.log(' - Enable session persistence across restarts');
|
|
60
|
-
console.log(' -
|
|
61
|
-
console.log(' - Auto-update PROMPT_PLAN on task completion');
|
|
199
|
+
console.log(' - Auto-checkpoint and session rescue');
|
|
62
200
|
console.log(' - Recommend relevant skills based on your prompts');
|
|
63
201
|
console.log('\nThis will modify files in ~/.claude/\n');
|
|
64
202
|
|
|
@@ -91,24 +229,38 @@ async function installClaudeHooks() {
|
|
|
91
229
|
console.log('Created ~/.claude/hooks directory');
|
|
92
230
|
}
|
|
93
231
|
|
|
94
|
-
// Copy hook files (scripts + data files)
|
|
95
|
-
const hookFiles = [
|
|
96
|
-
'tool-use-trace.js',
|
|
97
|
-
'on-startup.js',
|
|
98
|
-
'on-clear.js',
|
|
99
|
-
'on-task-complete.js',
|
|
100
|
-
'skill-eval.sh',
|
|
101
|
-
'skill-eval.cjs',
|
|
102
|
-
];
|
|
103
|
-
const dataFiles = ['skill-rules.json'];
|
|
104
232
|
let installed = 0;
|
|
105
233
|
|
|
106
|
-
|
|
234
|
+
// --- Install canonical session-persistence hooks ---
|
|
235
|
+
for (const entry of CANONICAL_HOOKS) {
|
|
236
|
+
const srcPath = join(templatesDir, entry.scriptName);
|
|
237
|
+
const destPath = join(claudeHooksDir, entry.scriptName);
|
|
238
|
+
|
|
239
|
+
if (existsSync(srcPath)) {
|
|
240
|
+
// Backup existing hook if it exists
|
|
241
|
+
if (existsSync(destPath)) {
|
|
242
|
+
const backupPath = `${destPath}.backup-${Date.now()}`;
|
|
243
|
+
copyFileSync(destPath, backupPath);
|
|
244
|
+
console.log(` Backed up: ${entry.scriptName}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
copyFileSync(srcPath, destPath);
|
|
248
|
+
try {
|
|
249
|
+
chmodSync(destPath, 0o755);
|
|
250
|
+
} catch {
|
|
251
|
+
// Silent fail on chmod
|
|
252
|
+
}
|
|
253
|
+
installed++;
|
|
254
|
+
console.log(` Installed: ${entry.scriptName}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// --- Install legacy tooling hooks ---
|
|
259
|
+
for (const hookFile of [...LEGACY_HOOK_FILES, ...LEGACY_DATA_FILES]) {
|
|
107
260
|
const srcPath = join(templatesDir, hookFile);
|
|
108
261
|
const destPath = join(claudeHooksDir, hookFile);
|
|
109
262
|
|
|
110
263
|
if (existsSync(srcPath)) {
|
|
111
|
-
// Backup existing hook if it exists
|
|
112
264
|
if (existsSync(destPath)) {
|
|
113
265
|
const backupPath = `${destPath}.backup-${Date.now()}`;
|
|
114
266
|
copyFileSync(destPath, backupPath);
|
|
@@ -117,11 +269,9 @@ async function installClaudeHooks() {
|
|
|
117
269
|
|
|
118
270
|
copyFileSync(srcPath, destPath);
|
|
119
271
|
|
|
120
|
-
|
|
121
|
-
if (!dataFiles.includes(hookFile)) {
|
|
272
|
+
if (!LEGACY_DATA_FILES.includes(hookFile)) {
|
|
122
273
|
try {
|
|
123
|
-
|
|
124
|
-
execSync(`chmod +x "${destPath}"`, { stdio: 'ignore' });
|
|
274
|
+
chmodSync(destPath, 0o755);
|
|
125
275
|
} catch {
|
|
126
276
|
// Silent fail on chmod
|
|
127
277
|
}
|
|
@@ -132,30 +282,14 @@ async function installClaudeHooks() {
|
|
|
132
282
|
}
|
|
133
283
|
}
|
|
134
284
|
|
|
135
|
-
// Update hooks.json
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
hooksConfig = JSON.parse(readFileSync(claudeConfigFile, 'utf8'));
|
|
140
|
-
} catch {
|
|
141
|
-
// Start fresh if parse fails
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Add our hooks (don't overwrite existing hooks unless they're ours)
|
|
146
|
-
const newHooksConfig = {
|
|
147
|
-
...hooksConfig,
|
|
148
|
-
'tool-use-approval': join(claudeHooksDir, 'tool-use-trace.js'),
|
|
149
|
-
'on-startup': join(claudeHooksDir, 'on-startup.js'),
|
|
150
|
-
'on-clear': join(claudeHooksDir, 'on-clear.js'),
|
|
151
|
-
'on-task-complete': join(claudeHooksDir, 'on-task-complete.js'),
|
|
152
|
-
'user-prompt-submit': join(claudeHooksDir, 'skill-eval.sh'),
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
writeFileSync(claudeConfigFile, JSON.stringify(newHooksConfig, null, 2));
|
|
285
|
+
// --- Update settings.json (not the deprecated hooks.json) ---
|
|
286
|
+
const currentSettings = readSettingsFn(claudeSettingsFile);
|
|
287
|
+
const merged = mergeSettingsFn(currentSettings, claudeHooksDir);
|
|
288
|
+
writeSettingsAtomicFn(merged, claudeSettingsFile);
|
|
156
289
|
|
|
157
290
|
if (installed > 0) {
|
|
158
291
|
console.log(`\n[OK] Installed ${installed} Claude hooks`);
|
|
292
|
+
console.log(` Config: ~/.claude/settings.json`);
|
|
159
293
|
console.log(` Traces: ~/.stackmemory/traces/`);
|
|
160
294
|
console.log(' To disable: set DEBUG_TRACE=false in .env');
|
|
161
295
|
}
|
|
@@ -189,10 +323,8 @@ async function installSessionDaemon() {
|
|
|
189
323
|
if (existsSync(daemonSrc)) {
|
|
190
324
|
copyFileSync(daemonSrc, daemonDest);
|
|
191
325
|
|
|
192
|
-
// Make executable
|
|
193
326
|
try {
|
|
194
|
-
|
|
195
|
-
execSync(`chmod +x "${daemonDest}"`, { stdio: 'ignore' });
|
|
327
|
+
chmodSync(daemonDest, 0o755);
|
|
196
328
|
} catch {
|
|
197
329
|
// Silent fail on chmod
|
|
198
330
|
}
|