@vinaes/succ 1.3.22 → 1.3.31
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/README.md +4 -0
- package/dist/commands/analyze-utils.d.ts +1 -1
- package/dist/commands/analyze-utils.d.ts.map +1 -1
- package/dist/commands/analyze-utils.js +1 -2
- package/dist/commands/analyze-utils.js.map +1 -1
- package/dist/daemon/service.d.ts.map +1 -1
- package/dist/daemon/service.js +34 -1
- package/dist/daemon/service.js.map +1 -1
- package/dist/lib/config-types.d.ts +1 -0
- package/dist/lib/config-types.d.ts.map +1 -1
- package/dist/lib/config.d.ts +6 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +21 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/db/index.d.ts +1 -1
- package/dist/lib/db/index.d.ts.map +1 -1
- package/dist/lib/db/index.js +1 -1
- package/dist/lib/db/index.js.map +1 -1
- package/dist/lib/db/memories.d.ts +6 -0
- package/dist/lib/db/memories.d.ts.map +1 -1
- package/dist/lib/db/memories.js +34 -0
- package/dist/lib/db/memories.js.map +1 -1
- package/dist/lib/graph-export.js +1 -2
- package/dist/lib/graph-export.js.map +1 -1
- package/dist/lib/hook-rules.d.ts +31 -0
- package/dist/lib/hook-rules.d.ts.map +1 -0
- package/dist/lib/hook-rules.js +102 -0
- package/dist/lib/hook-rules.js.map +1 -0
- package/dist/lib/llm.d.ts.map +1 -1
- package/dist/lib/llm.js +4 -4
- package/dist/lib/llm.js.map +1 -1
- package/dist/lib/ort-session.d.ts.map +1 -1
- package/dist/lib/ort-session.js +0 -1
- package/dist/lib/ort-session.js.map +1 -1
- package/dist/lib/session-summary.d.ts +1 -0
- package/dist/lib/session-summary.d.ts.map +1 -1
- package/dist/lib/session-summary.js +3 -0
- package/dist/lib/session-summary.js.map +1 -1
- package/dist/lib/storage/backends/postgresql.d.ts +1 -0
- package/dist/lib/storage/backends/postgresql.d.ts.map +1 -1
- package/dist/lib/storage/backends/postgresql.js +33 -0
- package/dist/lib/storage/backends/postgresql.js.map +1 -1
- package/dist/lib/storage/dispatcher.d.ts +1 -0
- package/dist/lib/storage/dispatcher.d.ts.map +1 -1
- package/dist/lib/storage/dispatcher.js +6 -0
- package/dist/lib/storage/dispatcher.js.map +1 -1
- package/dist/lib/storage/index.d.ts +1 -0
- package/dist/lib/storage/index.d.ts.map +1 -1
- package/dist/lib/storage/index.js +4 -0
- package/dist/lib/storage/index.js.map +1 -1
- package/dist/mcp/tools/memory.d.ts.map +1 -1
- package/dist/mcp/tools/memory.js +20 -3
- package/dist/mcp/tools/memory.js.map +1 -1
- package/dist/prompts/extraction.d.ts +2 -2
- package/dist/prompts/extraction.d.ts.map +1 -1
- package/dist/prompts/extraction.js +6 -2
- package/dist/prompts/extraction.js.map +1 -1
- package/hooks/succ-pre-tool.cjs +304 -76
- package/hooks/succ-session-start.cjs +38 -7
- package/package.json +4 -3
package/hooks/succ-pre-tool.cjs
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* PreToolUse Hook — Command safety guard + commit context
|
|
3
|
+
* PreToolUse Hook — Command safety guard + commit context + file-linked memories
|
|
4
4
|
*
|
|
5
|
-
* Fires before every
|
|
5
|
+
* Fires before every tool call. Four features:
|
|
6
6
|
*
|
|
7
|
-
* 1.
|
|
7
|
+
* 1. File-linked memories — intercepts Edit/Write, queries daemon for related memories,
|
|
8
|
+
* injects them as additionalContext (~10ms, fail-open)
|
|
9
|
+
*
|
|
10
|
+
* 2. Command safety guard — blocks dangerous git/filesystem/database/docker commands
|
|
8
11
|
* Config: commandSafetyGuard.mode = 'deny' | 'ask' | 'off' (default: 'deny')
|
|
9
12
|
* Config: commandSafetyGuard.allowlist = string[]
|
|
10
13
|
* Config: commandSafetyGuard.customPatterns = [{ pattern: "regex", reason: "why" }]
|
|
11
14
|
*
|
|
12
|
-
*
|
|
15
|
+
* 3. Commit guidelines injection — injects co-author format into context
|
|
13
16
|
* when Claude is about to run git commit
|
|
14
17
|
* Config: includeCoAuthoredBy (default: true)
|
|
15
18
|
*
|
|
16
|
-
*
|
|
19
|
+
* 4. Pre-commit diff review reminder — injects reminder to run diff-reviewer
|
|
17
20
|
* Config: preCommitReview (default: false)
|
|
18
21
|
*/
|
|
19
22
|
|
|
@@ -24,72 +27,182 @@ const path = require('path');
|
|
|
24
27
|
|
|
25
28
|
const DANGEROUS_PATTERNS = [
|
|
26
29
|
// ── Git — data loss ──
|
|
27
|
-
{
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
{
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
{
|
|
36
|
-
|
|
30
|
+
{
|
|
31
|
+
pattern: /\bgit\s+reset\s+--hard\b/,
|
|
32
|
+
reason: 'git reset --hard destroys uncommitted changes. Use git stash first.',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
pattern: /\bgit\s+reset\s+--merge\b/,
|
|
36
|
+
reason: 'git reset --merge can destroy uncommitted changes.',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
pattern: /\bgit\s+checkout\s+--\s/,
|
|
40
|
+
reason: 'git checkout -- discards file modifications. Use git stash first.',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
pattern: /\bgit\s+checkout\s+\.\s*($|[;&|])/,
|
|
44
|
+
reason: 'git checkout . discards all modifications. Use git stash first.',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
pattern: /\bgit\s+restore\s+--staged\s+--worktree\b/,
|
|
48
|
+
reason: 'git restore --staged --worktree discards both staged and unstaged changes.',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
pattern: /\bgit\s+clean\s+-[a-zA-Z]*f/,
|
|
52
|
+
reason: 'git clean -f permanently deletes untracked files.',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
pattern: /\bgit\s+push\s+.*--force(?!-with-lease)\b/,
|
|
56
|
+
reason: 'git push --force rewrites remote history. Use --force-with-lease instead.',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
pattern: /\bgit\s+push\s+-f\b/,
|
|
60
|
+
reason: 'git push -f rewrites remote history. Use --force-with-lease instead.',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
pattern: /\bgit\s+branch\s+-D\b/,
|
|
64
|
+
reason: 'git branch -D force-deletes without merge verification. Use -d for safe delete.',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
pattern: /\bgit\s+stash\s+drop\b/,
|
|
68
|
+
reason: 'git stash drop permanently destroys stashed work.',
|
|
69
|
+
},
|
|
37
70
|
{ pattern: /\bgit\s+stash\s+clear\b/, reason: 'git stash clear destroys ALL stashed work.' },
|
|
38
|
-
{
|
|
39
|
-
|
|
71
|
+
{
|
|
72
|
+
pattern: /\bgit\s+rebase\s+-i\b/,
|
|
73
|
+
reason: 'git rebase -i requires interactive terminal (not available in hooks).',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
pattern: /\bgit\s+reflog\s+expire\s+--expire=now\b/,
|
|
77
|
+
reason: 'git reflog expire --expire=now permanently removes recovery points.',
|
|
78
|
+
},
|
|
40
79
|
|
|
41
80
|
// ── Filesystem — data loss ──
|
|
42
|
-
{
|
|
43
|
-
|
|
44
|
-
|
|
81
|
+
{
|
|
82
|
+
pattern: /\brm\s+.*\.succ\b/,
|
|
83
|
+
reason:
|
|
84
|
+
'.succ/ contains your memory, brain vault, and config. This would destroy all succ data.',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
pattern: /\brm\s+-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*\b/,
|
|
88
|
+
reason: 'rm -rf can permanently delete files. Verify the target path.',
|
|
89
|
+
checkPath: true,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
pattern: /\brm\s+-[a-zA-Z]*f[a-zA-Z]*r[a-zA-Z]*\b/,
|
|
93
|
+
reason: 'rm -fr can permanently delete files. Verify the target path.',
|
|
94
|
+
checkPath: true,
|
|
95
|
+
},
|
|
45
96
|
|
|
46
97
|
// ── Docker — container/image/volume destruction ──
|
|
47
|
-
{
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
{
|
|
98
|
+
{
|
|
99
|
+
pattern: /\bdocker\s+system\s+prune\b/,
|
|
100
|
+
reason:
|
|
101
|
+
'docker system prune removes all unused containers, networks, images, and optionally volumes.',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
pattern: /\bdocker\s+volume\s+prune\b/,
|
|
105
|
+
reason: 'docker volume prune removes all unused volumes (potential data loss).',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
pattern: /\bdocker\s+rm\s+-f\b/,
|
|
109
|
+
reason: 'docker rm -f force-removes running containers without graceful shutdown.',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
pattern: /\bdocker\s+rmi\s+-f\b/,
|
|
113
|
+
reason: 'docker rmi -f force-removes images that may be in use.',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
pattern: /\bdocker-compose\s+down\s+-v\b/,
|
|
117
|
+
reason: 'docker-compose down -v removes named volumes (database data loss).',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
pattern: /\bdocker\s+compose\s+down\s+-v\b/,
|
|
121
|
+
reason: 'docker compose down -v removes named volumes (database data loss).',
|
|
122
|
+
},
|
|
53
123
|
|
|
54
124
|
// ── SQLite — database destruction ──
|
|
55
|
-
{
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
125
|
+
{
|
|
126
|
+
pattern: /\bsqlite3?\b.*\bDROP\s+TABLE\b/i,
|
|
127
|
+
reason: 'DROP TABLE permanently deletes a SQLite table and all its data.',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
pattern: /\bsqlite3?\b.*\bDROP\s+DATABASE\b/i,
|
|
131
|
+
reason: 'DROP DATABASE permanently deletes the entire SQLite database.',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
pattern: /\bsqlite3?\b.*\bDELETE\s+FROM\s+\w+\s*;/i,
|
|
135
|
+
reason: 'DELETE FROM without WHERE deletes all rows in a SQLite table.',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
pattern: /\bsqlite3?\b.*\bTRUNCATE\b/i,
|
|
139
|
+
reason: 'TRUNCATE removes all data from a SQLite table.',
|
|
140
|
+
},
|
|
59
141
|
|
|
60
142
|
// ── PostgreSQL — database destruction ──
|
|
61
|
-
{
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
143
|
+
{
|
|
144
|
+
pattern: /\bpsql\b.*\bDROP\s+TABLE\b/i,
|
|
145
|
+
reason: 'DROP TABLE permanently deletes a PostgreSQL table and all its data.',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
pattern: /\bpsql\b.*\bDROP\s+DATABASE\b/i,
|
|
149
|
+
reason: 'DROP DATABASE permanently deletes the entire PostgreSQL database.',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
pattern: /\bpsql\b.*\bDELETE\s+FROM\s+\w+\s*;/i,
|
|
153
|
+
reason: 'DELETE FROM without WHERE deletes all rows in a PostgreSQL table.',
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
pattern: /\bpsql\b.*\bTRUNCATE\b/i,
|
|
157
|
+
reason: 'TRUNCATE removes all data from a PostgreSQL table.',
|
|
158
|
+
},
|
|
65
159
|
{ pattern: /\bdropdb\b/, reason: 'dropdb permanently deletes a PostgreSQL database.' },
|
|
66
160
|
{ pattern: /\bdropuser\b/, reason: 'dropuser permanently deletes a PostgreSQL user/role.' },
|
|
67
161
|
|
|
68
162
|
// ── Qdrant — vector database destruction ──
|
|
69
|
-
{
|
|
70
|
-
|
|
71
|
-
|
|
163
|
+
{
|
|
164
|
+
pattern: /\bcurl\b.*\bqdrant\b.*\bDELETE\b/i,
|
|
165
|
+
reason: 'DELETE on Qdrant API can remove collections or points permanently.',
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
pattern: /\bcurl\b.*\b:6333\b.*\bDELETE\b/i,
|
|
169
|
+
reason: 'DELETE on Qdrant port 6333 can remove collections or points permanently.',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
pattern: /\bcurl\b.*\b:6334\b.*\bDELETE\b/i,
|
|
173
|
+
reason: 'DELETE on Qdrant gRPC port can remove data permanently.',
|
|
174
|
+
},
|
|
72
175
|
];
|
|
73
176
|
|
|
74
177
|
// Paths where rm -rf is considered safe (normalized, lowercase)
|
|
75
178
|
const SAFE_RM_PATHS = [
|
|
76
|
-
'/tmp',
|
|
77
|
-
'
|
|
78
|
-
'
|
|
79
|
-
'
|
|
80
|
-
'
|
|
179
|
+
'/tmp',
|
|
180
|
+
'/var/tmp',
|
|
181
|
+
'node_modules',
|
|
182
|
+
'dist',
|
|
183
|
+
'build',
|
|
184
|
+
'.cache',
|
|
185
|
+
'__pycache__',
|
|
186
|
+
'.pytest_cache',
|
|
187
|
+
'.tox',
|
|
188
|
+
'target/debug',
|
|
189
|
+
'target/release',
|
|
190
|
+
'.next',
|
|
191
|
+
'.nuxt',
|
|
192
|
+
'.turbo',
|
|
193
|
+
'coverage',
|
|
81
194
|
];
|
|
82
195
|
|
|
83
196
|
// Prefixes that indicate the command is data, not execution
|
|
84
197
|
const DATA_PREFIXES = [
|
|
85
|
-
/^\s*(?:#|\/\/)/,
|
|
86
|
-
/^\s*echo\b/,
|
|
87
|
-
/^\s*printf\b/,
|
|
88
|
-
/^\s*cat\s*<</,
|
|
89
|
-
/^\s*grep\b/,
|
|
90
|
-
/^\s*rg\b/,
|
|
91
|
-
/^\s*ag\b/,
|
|
92
|
-
/^\s*(?:"|').*(?:"|')\s
|
|
198
|
+
/^\s*(?:#|\/\/)/, // comments
|
|
199
|
+
/^\s*echo\b/, // echo
|
|
200
|
+
/^\s*printf\b/, // printf
|
|
201
|
+
/^\s*cat\s*<</, // heredoc (cat <<)
|
|
202
|
+
/^\s*grep\b/, // grep
|
|
203
|
+
/^\s*rg\b/, // ripgrep
|
|
204
|
+
/^\s*ag\b/, // silver searcher
|
|
205
|
+
/^\s*(?:"|').*(?:"|')\s*$/, // quoted string
|
|
93
206
|
];
|
|
94
207
|
|
|
95
208
|
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
@@ -143,7 +256,7 @@ function loadConfig(projectDir) {
|
|
|
143
256
|
|
|
144
257
|
function isDataContext(command) {
|
|
145
258
|
const trimmed = command.trim();
|
|
146
|
-
return DATA_PREFIXES.some(prefix => prefix.test(trimmed));
|
|
259
|
+
return DATA_PREFIXES.some((prefix) => prefix.test(trimmed));
|
|
147
260
|
}
|
|
148
261
|
|
|
149
262
|
function isRmPathSafe(command) {
|
|
@@ -153,7 +266,7 @@ function isRmPathSafe(command) {
|
|
|
153
266
|
const target = match[1].trim().replace(/["']/g, '');
|
|
154
267
|
const normalized = target.toLowerCase().replace(/\\/g, '/');
|
|
155
268
|
|
|
156
|
-
return SAFE_RM_PATHS.some(safe => {
|
|
269
|
+
return SAFE_RM_PATHS.some((safe) => {
|
|
157
270
|
if (normalized === safe || normalized.endsWith('/' + safe)) return true;
|
|
158
271
|
if (safe === '/tmp' && normalized.startsWith('/tmp/')) return true;
|
|
159
272
|
if (safe === '/var/tmp' && normalized.startsWith('/var/tmp/')) return true;
|
|
@@ -236,6 +349,58 @@ MEDIUM and below — commit is OK, mention findings in summary.
|
|
|
236
349
|
return parts.join('\n');
|
|
237
350
|
}
|
|
238
351
|
|
|
352
|
+
// ─── Daemon helpers ──────────────────────────────────────────────────
|
|
353
|
+
|
|
354
|
+
function getDaemonPort(projectDir) {
|
|
355
|
+
const portFile = path.join(projectDir, '.succ', '.tmp', 'daemon.port');
|
|
356
|
+
if (!fs.existsSync(portFile)) return null;
|
|
357
|
+
const port = parseInt(fs.readFileSync(portFile, 'utf8').trim(), 10);
|
|
358
|
+
return port && !isNaN(port) ? port : null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function recallFileMemories(projectDir, fileName) {
|
|
362
|
+
const port = getDaemonPort(projectDir);
|
|
363
|
+
if (!port) return [];
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/recall-by-tag`, {
|
|
367
|
+
method: 'POST',
|
|
368
|
+
headers: { 'Content-Type': 'application/json' },
|
|
369
|
+
body: JSON.stringify({ tag: `file:${fileName}`, limit: 5 }),
|
|
370
|
+
signal: AbortSignal.timeout(2000),
|
|
371
|
+
});
|
|
372
|
+
if (!res.ok) return [];
|
|
373
|
+
const data = await res.json();
|
|
374
|
+
return data.results || [];
|
|
375
|
+
} catch {
|
|
376
|
+
return []; // fail-open
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function fetchHookRules(projectDir, toolName, toolInput) {
|
|
381
|
+
const port = getDaemonPort(projectDir);
|
|
382
|
+
if (!port) return [];
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/hook-rules`, {
|
|
386
|
+
method: 'POST',
|
|
387
|
+
headers: { 'Content-Type': 'application/json' },
|
|
388
|
+
body: JSON.stringify({ tool_name: toolName, tool_input: toolInput || {} }),
|
|
389
|
+
signal: AbortSignal.timeout(2000),
|
|
390
|
+
});
|
|
391
|
+
if (!res.ok) return [];
|
|
392
|
+
const data = await res.json();
|
|
393
|
+
return data.rules || [];
|
|
394
|
+
} catch {
|
|
395
|
+
return []; // fail-open
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function formatFileContext(memories, fileName) {
|
|
400
|
+
const lines = memories.map((m) => `- [${m.type || 'observation'}] ${m.content.slice(0, 200)}`);
|
|
401
|
+
return `<file-context file="${fileName}">\nRelated memories:\n${lines.join('\n')}\n</file-context>`;
|
|
402
|
+
}
|
|
403
|
+
|
|
239
404
|
// ─── Main ────────────────────────────────────────────────────────────
|
|
240
405
|
|
|
241
406
|
let input = '';
|
|
@@ -247,7 +412,7 @@ process.stdin.on('readable', () => {
|
|
|
247
412
|
}
|
|
248
413
|
});
|
|
249
414
|
|
|
250
|
-
process.stdin.on('end', () => {
|
|
415
|
+
process.stdin.on('end', async () => {
|
|
251
416
|
try {
|
|
252
417
|
const hookInput = JSON.parse(input);
|
|
253
418
|
let projectDir = hookInput.cwd || process.cwd();
|
|
@@ -267,43 +432,106 @@ process.stdin.on('end', () => {
|
|
|
267
432
|
process.exit(0);
|
|
268
433
|
}
|
|
269
434
|
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
435
|
+
const toolName = hookInput.tool_name || '';
|
|
436
|
+
const toolInput = hookInput.tool_input || {};
|
|
437
|
+
const filePath = toolInput.file_path || '';
|
|
438
|
+
const command = toolInput.command || '';
|
|
439
|
+
const contextParts = [];
|
|
440
|
+
let askReason = null;
|
|
441
|
+
|
|
442
|
+
// 1. Dynamic hook rules from memory (ALL tools)
|
|
443
|
+
// Note: deny/ask exit immediately — any accumulated contextParts are intentionally
|
|
444
|
+
// discarded because the tool call is being blocked or requires confirmation.
|
|
445
|
+
const rules = await fetchHookRules(projectDir, toolName, toolInput);
|
|
446
|
+
for (const rule of rules) {
|
|
447
|
+
if (rule.action === 'deny') {
|
|
448
|
+
const output = {
|
|
449
|
+
hookSpecificOutput: {
|
|
450
|
+
hookEventName: 'PreToolUse',
|
|
451
|
+
permissionDecision: 'deny',
|
|
452
|
+
permissionDecisionReason: `[succ rule] ${rule.content}`,
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
console.log(JSON.stringify(output));
|
|
456
|
+
process.exit(0);
|
|
457
|
+
}
|
|
458
|
+
if (rule.action === 'ask' && !askReason) {
|
|
459
|
+
askReason = rule.content;
|
|
460
|
+
}
|
|
461
|
+
if (rule.action === 'inject') {
|
|
462
|
+
contextParts.push(`<hook-rule>${rule.content}</hook-rule>`);
|
|
463
|
+
}
|
|
273
464
|
}
|
|
274
465
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
hookEventName: 'PreToolUse',
|
|
283
|
-
permissionDecision: dangerousResult.mode === 'ask' ? 'ask' : 'deny',
|
|
284
|
-
permissionDecisionReason: `[succ guard] ${dangerousResult.reason}`,
|
|
285
|
-
},
|
|
286
|
-
};
|
|
287
|
-
console.log(JSON.stringify(output));
|
|
288
|
-
process.exit(0);
|
|
466
|
+
// 2. File-linked memories (Edit/Write only — Read is too frequent, wastes context)
|
|
467
|
+
if ((toolName === 'Edit' || toolName === 'Write') && filePath) {
|
|
468
|
+
const fileName = path.basename(filePath);
|
|
469
|
+
const memories = await recallFileMemories(projectDir, fileName);
|
|
470
|
+
if (memories.length > 0) {
|
|
471
|
+
contextParts.push(formatFileContext(memories, fileName));
|
|
472
|
+
}
|
|
289
473
|
}
|
|
290
474
|
|
|
291
|
-
//
|
|
292
|
-
if (
|
|
293
|
-
const
|
|
294
|
-
|
|
475
|
+
// 3. Command safety guard (Bash only)
|
|
476
|
+
if (command) {
|
|
477
|
+
const config = loadConfig(projectDir);
|
|
478
|
+
const dangerousResult = checkDangerous(command, config);
|
|
479
|
+
if (dangerousResult) {
|
|
480
|
+
const output = {
|
|
481
|
+
hookSpecificOutput: {
|
|
482
|
+
hookEventName: 'PreToolUse',
|
|
483
|
+
permissionDecision: dangerousResult.mode === 'ask' ? 'ask' : 'deny',
|
|
484
|
+
permissionDecisionReason: `[succ guard] ${dangerousResult.reason}`,
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
console.log(JSON.stringify(output));
|
|
488
|
+
process.exit(0);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// 4. Hook rule ask (after safety guard, so deny takes priority)
|
|
492
|
+
if (askReason) {
|
|
295
493
|
const output = {
|
|
296
494
|
hookSpecificOutput: {
|
|
297
495
|
hookEventName: 'PreToolUse',
|
|
298
|
-
|
|
496
|
+
permissionDecision: 'ask',
|
|
497
|
+
permissionDecisionReason: `[succ rule] ${askReason}`,
|
|
299
498
|
},
|
|
300
499
|
};
|
|
301
500
|
console.log(JSON.stringify(output));
|
|
302
501
|
process.exit(0);
|
|
303
502
|
}
|
|
503
|
+
|
|
504
|
+
// 5. Git commit — inject guidelines + diff review reminder
|
|
505
|
+
if (/\bgit\s+commit\b/.test(command)) {
|
|
506
|
+
const commitContext = buildCommitContext(config);
|
|
507
|
+
if (commitContext) {
|
|
508
|
+
contextParts.push(commitContext);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
} else if (askReason) {
|
|
512
|
+
// Non-Bash ask rule
|
|
513
|
+
const output = {
|
|
514
|
+
hookSpecificOutput: {
|
|
515
|
+
hookEventName: 'PreToolUse',
|
|
516
|
+
permissionDecision: 'ask',
|
|
517
|
+
permissionDecisionReason: `[succ rule] ${askReason}`,
|
|
518
|
+
},
|
|
519
|
+
};
|
|
520
|
+
console.log(JSON.stringify(output));
|
|
521
|
+
process.exit(0);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// 6. Emit combined context
|
|
525
|
+
if (contextParts.length > 0) {
|
|
526
|
+
const output = {
|
|
527
|
+
hookSpecificOutput: {
|
|
528
|
+
hookEventName: 'PreToolUse',
|
|
529
|
+
additionalContext: contextParts.join('\n'),
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
console.log(JSON.stringify(output));
|
|
304
533
|
}
|
|
305
534
|
|
|
306
|
-
// No action needed
|
|
307
535
|
process.exit(0);
|
|
308
536
|
} catch {
|
|
309
537
|
// Fail-open: don't block on hook errors
|
|
@@ -63,13 +63,15 @@ process.stdin.on('end', async () => {
|
|
|
63
63
|
process.exit(0);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
// Load config settings
|
|
66
|
+
// Load config settings (project config overrides global, but both are checked)
|
|
67
67
|
let includeCoAuthoredBy = true; // default: true
|
|
68
68
|
let communicationAutoAdapt = true; // default: true
|
|
69
69
|
let communicationTrackHistory = false; // default: false
|
|
70
|
+
let hasOpenRouterKey = !!process.env.OPENROUTER_API_KEY;
|
|
70
71
|
const configPaths = [
|
|
71
|
-
|
|
72
|
+
// Global first, then project overrides
|
|
72
73
|
path.join(require('os').homedir(), '.succ', 'config.json'),
|
|
74
|
+
path.join(succDir, 'config.json'),
|
|
73
75
|
];
|
|
74
76
|
for (const configPath of configPaths) {
|
|
75
77
|
if (fs.existsSync(configPath)) {
|
|
@@ -84,7 +86,14 @@ process.stdin.on('end', async () => {
|
|
|
84
86
|
if (config.communicationTrackHistory === true) {
|
|
85
87
|
communicationTrackHistory = true;
|
|
86
88
|
}
|
|
87
|
-
|
|
89
|
+
// Check for OpenRouter API key: llm.api_key, llm.embeddings.api_key, or web_search.api_key
|
|
90
|
+
if (!hasOpenRouterKey) {
|
|
91
|
+
const keys = [config.llm?.api_key, config.llm?.embeddings?.api_key, config.web_search?.api_key];
|
|
92
|
+
if (keys.some(k => typeof k === 'string' && k.startsWith('sk-or-'))) {
|
|
93
|
+
hasOpenRouterKey = true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// No break — merge both configs (global defaults, project overrides)
|
|
88
97
|
} catch {
|
|
89
98
|
// Ignore parse errors
|
|
90
99
|
}
|
|
@@ -156,6 +165,28 @@ Without it, succ works in global-only mode and can't access project data.
|
|
|
156
165
|
→ Record failed approach (boosted in recall to prevent retrying)
|
|
157
166
|
</memory>
|
|
158
167
|
|
|
168
|
+
<hook-rules hint="Dynamic pre-tool rules from memory. Saved rules auto-fire before matching tool calls.">
|
|
169
|
+
When user asks to remember a rule about tool behavior (e.g., "before deploy run tests",
|
|
170
|
+
"always review before commit", "block rm -rf"), save with hook-rule convention:
|
|
171
|
+
|
|
172
|
+
**succ_remember** content="..." tags=["hook-rule", "tool:{ToolName}", "match:{regex}"] type="decision|error|pattern"
|
|
173
|
+
|
|
174
|
+
Tags:
|
|
175
|
+
- **hook-rule** — required, marks memory as a pre-tool rule
|
|
176
|
+
- **tool:{Name}** — optional, filter by tool (Bash, Edit, Write, Read, Skill, Task). Omit = all tools
|
|
177
|
+
- **match:{regex}** — optional, regex tested against tool input (command, skill name, file basename, prompt)
|
|
178
|
+
|
|
179
|
+
Action via type:
|
|
180
|
+
- **decision/observation/learning** → inject as additionalContext (guide the agent)
|
|
181
|
+
- **error** → deny the tool call (block it)
|
|
182
|
+
- **pattern** → ask user for confirmation before proceeding
|
|
183
|
+
|
|
184
|
+
Examples:
|
|
185
|
+
\`succ_remember content="Run diff-reviewer before deploying" tags=["hook-rule","tool:Skill","match:deploy"] type="decision"\`
|
|
186
|
+
\`succ_remember content="Never force-push to main" tags=["hook-rule","tool:Bash","match:push.*--force.*main"] type="error"\`
|
|
187
|
+
\`succ_remember content="Editing test files — run tests after" tags=["hook-rule","tool:Edit","match:\\\\.test\\\\."] type="decision"\`
|
|
188
|
+
</hook-rules>
|
|
189
|
+
|
|
159
190
|
<ops>
|
|
160
191
|
**succ_index_file** file="doc.md" [force=true] — index doc for succ_search
|
|
161
192
|
**succ_index_code_file** file="src/auth.ts" [force=true] — index code for succ_search_code
|
|
@@ -179,12 +210,12 @@ Without it, succ works in global-only mode and can't access project data.
|
|
|
179
210
|
**succ_prd_export** [prd_id="prd_xxx"] — Obsidian Mermaid export
|
|
180
211
|
</prd>
|
|
181
212
|
|
|
182
|
-
|
|
213
|
+
${hasOpenRouterKey ? `<web-search hint="Perplexity Sonar via OpenRouter.">
|
|
183
214
|
**succ_quick_search** query="..." — cheap & fast, simple facts
|
|
184
215
|
**succ_web_search** query="..." [model="perplexity/sonar-pro"] — quality search, complex queries
|
|
185
216
|
**succ_deep_research** query="..." — multi-step research (30-120s, 30+ sources)
|
|
186
217
|
**succ_web_search_history** [tool_name="..."] [limit=20] — past searches and costs
|
|
187
|
-
</web-search
|
|
218
|
+
</web-search>` : ''}
|
|
188
219
|
|
|
189
220
|
<debug hint="Structured debugging with hypothesis testing. Sessions in .succ/debugs/.">
|
|
190
221
|
**succ_debug** action="create|hypothesis|instrument|result|resolve|abandon|status|list|log|show_log|detect_lang|gen_log"
|
|
@@ -203,8 +234,8 @@ Without it, succ works in global-only mode and can't access project data.
|
|
|
203
234
|
| Multi-step tasks, research | succ-general | general-purpose agent |
|
|
204
235
|
| Code review | succ-code-reviewer | built-in review |
|
|
205
236
|
| Pre-commit review | succ-diff-reviewer | manual diff reading |
|
|
206
|
-
| Web page fetch | succ_fetch | WebFetch
|
|
207
|
-
| Web search | succ_quick_search / succ_web_search | WebSearch / Brave
|
|
237
|
+
| Web page fetch | succ_fetch | WebFetch |${hasOpenRouterKey ? `
|
|
238
|
+
| Web search | succ_quick_search / succ_web_search | WebSearch / Brave |` : ''}
|
|
208
239
|
|
|
209
240
|
Direct file reads (Read/Grep) are fine when you know the exact path — for discovery, always succ agents.
|
|
210
241
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vinaes/succ",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.31",
|
|
4
4
|
"description": "Semantic Understanding for Code Contexts — persistent memory for AI coding assistants (Claude Code, Cursor, Windsurf, Continue.dev)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"succ-mcp": "./dist/mcp-server.js"
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
|
-
"build": "tsc",
|
|
30
|
+
"build": "prettier --check src/ && eslint src/ && tsc",
|
|
31
|
+
"build:compile": "tsc",
|
|
31
32
|
"dev": "tsx src/cli.ts",
|
|
32
33
|
"test": "vitest",
|
|
33
34
|
"test:storage": "vitest --run src/lib/storage/",
|
|
@@ -42,7 +43,7 @@
|
|
|
42
43
|
"lint:fix": "eslint src/ --fix",
|
|
43
44
|
"format": "prettier --write src/",
|
|
44
45
|
"format:check": "prettier --check src/",
|
|
45
|
-
"prepare": "npm run build",
|
|
46
|
+
"prepare": "npm run build:compile",
|
|
46
47
|
"prepublishOnly": "npm run build && npm run lint"
|
|
47
48
|
},
|
|
48
49
|
"keywords": [
|