@stackmemoryai/stackmemory 0.5.3 → 0.5.5
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/bin/claude-sm +6 -0
- package/bin/claude-smd +6 -0
- package/dist/cli/claude-sm-danger.js +20 -0
- package/dist/cli/claude-sm-danger.js.map +7 -0
- package/dist/cli/commands/api.js +228 -0
- package/dist/cli/commands/api.js.map +7 -0
- package/dist/cli/commands/cleanup-processes.js +64 -0
- package/dist/cli/commands/cleanup-processes.js.map +7 -0
- package/dist/cli/commands/hooks.js +294 -0
- package/dist/cli/commands/hooks.js.map +7 -0
- package/dist/cli/commands/shell.js +248 -0
- package/dist/cli/commands/shell.js.map +7 -0
- package/dist/cli/commands/sweep.js +173 -5
- package/dist/cli/commands/sweep.js.map +3 -3
- package/dist/cli/index.js +9 -1
- package/dist/cli/index.js.map +2 -2
- package/dist/hooks/config.js +146 -0
- package/dist/hooks/config.js.map +7 -0
- package/dist/hooks/daemon.js +360 -0
- package/dist/hooks/daemon.js.map +7 -0
- package/dist/hooks/events.js +51 -0
- package/dist/hooks/events.js.map +7 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/index.js.map +7 -0
- package/dist/skills/api-discovery.js +349 -0
- package/dist/skills/api-discovery.js.map +7 -0
- package/dist/skills/api-skill.js +471 -0
- package/dist/skills/api-skill.js.map +7 -0
- package/dist/skills/claude-skills.js +49 -1
- package/dist/skills/claude-skills.js.map +2 -2
- package/dist/utils/process-cleanup.js +132 -0
- package/dist/utils/process-cleanup.js.map +7 -0
- package/package.json +4 -2
- package/scripts/install-sweep-hook.sh +89 -0
- package/templates/claude-hooks/post-edit-sweep.js +437 -0
- package/templates/shell/sweep-complete.zsh +116 -0
- package/templates/shell/sweep-suggest.js +161 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Post-Edit Sweep Hook for Claude Code
|
|
5
|
+
*
|
|
6
|
+
* Runs Sweep 1.5B predictions after file edits to suggest next changes.
|
|
7
|
+
* Tracks recent diffs and provides context-aware predictions.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { spawn } from 'child_process';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
|
|
18
|
+
const CONFIG = {
|
|
19
|
+
enabled: process.env.SWEEP_ENABLED !== 'false',
|
|
20
|
+
maxRecentDiffs: 5,
|
|
21
|
+
predictionTimeout: 30000,
|
|
22
|
+
minEditSize: 10,
|
|
23
|
+
debounceMs: 2000,
|
|
24
|
+
minDiffsForPrediction: 2,
|
|
25
|
+
cooldownMs: 10000,
|
|
26
|
+
codeExtensions: [
|
|
27
|
+
'.ts',
|
|
28
|
+
'.tsx',
|
|
29
|
+
'.js',
|
|
30
|
+
'.jsx',
|
|
31
|
+
'.py',
|
|
32
|
+
'.go',
|
|
33
|
+
'.rs',
|
|
34
|
+
'.java',
|
|
35
|
+
'.c',
|
|
36
|
+
'.cpp',
|
|
37
|
+
'.h',
|
|
38
|
+
'.hpp',
|
|
39
|
+
'.cs',
|
|
40
|
+
'.rb',
|
|
41
|
+
'.php',
|
|
42
|
+
'.swift',
|
|
43
|
+
'.kt',
|
|
44
|
+
'.scala',
|
|
45
|
+
'.vue',
|
|
46
|
+
'.svelte',
|
|
47
|
+
'.astro',
|
|
48
|
+
],
|
|
49
|
+
stateFile: path.join(
|
|
50
|
+
process.env.HOME || '/tmp',
|
|
51
|
+
'.stackmemory',
|
|
52
|
+
'sweep-state.json'
|
|
53
|
+
),
|
|
54
|
+
logFile: path.join(
|
|
55
|
+
process.env.HOME || '/tmp',
|
|
56
|
+
'.stackmemory',
|
|
57
|
+
'sweep-predictions.log'
|
|
58
|
+
),
|
|
59
|
+
pythonScript: path.join(
|
|
60
|
+
process.env.HOME || '/tmp',
|
|
61
|
+
'.stackmemory',
|
|
62
|
+
'sweep',
|
|
63
|
+
'sweep_predict.py'
|
|
64
|
+
),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Fallback locations for sweep_predict.py
|
|
68
|
+
const SCRIPT_LOCATIONS = [
|
|
69
|
+
CONFIG.pythonScript,
|
|
70
|
+
path.join(
|
|
71
|
+
process.cwd(),
|
|
72
|
+
'packages',
|
|
73
|
+
'sweep-addon',
|
|
74
|
+
'python',
|
|
75
|
+
'sweep_predict.py'
|
|
76
|
+
),
|
|
77
|
+
path.join(
|
|
78
|
+
process.cwd(),
|
|
79
|
+
'node_modules',
|
|
80
|
+
'@stackmemoryai',
|
|
81
|
+
'sweep-addon',
|
|
82
|
+
'python',
|
|
83
|
+
'sweep_predict.py'
|
|
84
|
+
),
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
function findPythonScript() {
|
|
88
|
+
for (const loc of SCRIPT_LOCATIONS) {
|
|
89
|
+
if (fs.existsSync(loc)) {
|
|
90
|
+
return loc;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function loadState() {
|
|
97
|
+
try {
|
|
98
|
+
if (fs.existsSync(CONFIG.stateFile)) {
|
|
99
|
+
return JSON.parse(fs.readFileSync(CONFIG.stateFile, 'utf-8'));
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
// Ignore errors
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
recentDiffs: [],
|
|
106
|
+
lastPrediction: null,
|
|
107
|
+
pendingPrediction: null,
|
|
108
|
+
fileContents: {},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function saveState(state) {
|
|
113
|
+
try {
|
|
114
|
+
const dir = path.dirname(CONFIG.stateFile);
|
|
115
|
+
if (!fs.existsSync(dir)) {
|
|
116
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
117
|
+
}
|
|
118
|
+
fs.writeFileSync(CONFIG.stateFile, JSON.stringify(state, null, 2));
|
|
119
|
+
} catch {
|
|
120
|
+
// Ignore errors
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function log(message, data = {}) {
|
|
125
|
+
try {
|
|
126
|
+
const dir = path.dirname(CONFIG.logFile);
|
|
127
|
+
if (!fs.existsSync(dir)) {
|
|
128
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
129
|
+
}
|
|
130
|
+
const entry = {
|
|
131
|
+
timestamp: new Date().toISOString(),
|
|
132
|
+
message,
|
|
133
|
+
...data,
|
|
134
|
+
};
|
|
135
|
+
fs.appendFileSync(CONFIG.logFile, JSON.stringify(entry) + '\n');
|
|
136
|
+
} catch {
|
|
137
|
+
// Ignore
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function runPrediction(filePath, currentContent, recentDiffs) {
|
|
142
|
+
const scriptPath = findPythonScript();
|
|
143
|
+
if (!scriptPath) {
|
|
144
|
+
log('Sweep script not found');
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const input = {
|
|
149
|
+
file_path: filePath,
|
|
150
|
+
current_content: currentContent,
|
|
151
|
+
recent_diffs: recentDiffs,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
return new Promise((resolve) => {
|
|
155
|
+
const proc = spawn('python3', [scriptPath], {
|
|
156
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
157
|
+
timeout: CONFIG.predictionTimeout,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
let stdout = '';
|
|
161
|
+
let stderr = '';
|
|
162
|
+
|
|
163
|
+
proc.stdout.on('data', (data) => (stdout += data));
|
|
164
|
+
proc.stderr.on('data', (data) => (stderr += data));
|
|
165
|
+
|
|
166
|
+
const timeout = setTimeout(() => {
|
|
167
|
+
proc.kill();
|
|
168
|
+
resolve(null);
|
|
169
|
+
}, CONFIG.predictionTimeout);
|
|
170
|
+
|
|
171
|
+
proc.on('close', (code) => {
|
|
172
|
+
clearTimeout(timeout);
|
|
173
|
+
try {
|
|
174
|
+
if (stdout.trim()) {
|
|
175
|
+
const result = JSON.parse(stdout.trim());
|
|
176
|
+
resolve(result);
|
|
177
|
+
} else {
|
|
178
|
+
resolve(null);
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
resolve(null);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
proc.on('error', () => {
|
|
186
|
+
clearTimeout(timeout);
|
|
187
|
+
resolve(null);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
proc.stdin.write(JSON.stringify(input));
|
|
191
|
+
proc.stdin.end();
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function readInput() {
|
|
196
|
+
let input = '';
|
|
197
|
+
for await (const chunk of process.stdin) {
|
|
198
|
+
input += chunk;
|
|
199
|
+
}
|
|
200
|
+
return JSON.parse(input);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isCodeFile(filePath) {
|
|
204
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
205
|
+
return CONFIG.codeExtensions.includes(ext);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function shouldRunPrediction(state, filePath) {
|
|
209
|
+
if (state.recentDiffs.length < CONFIG.minDiffsForPrediction) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (state.lastPrediction) {
|
|
214
|
+
const timeSince = Date.now() - state.lastPrediction.timestamp;
|
|
215
|
+
if (timeSince < CONFIG.cooldownMs) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (state.pendingPrediction) {
|
|
221
|
+
const timeSince = Date.now() - state.pendingPrediction;
|
|
222
|
+
if (timeSince < CONFIG.debounceMs) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function handleEdit(toolInput, toolResult) {
|
|
231
|
+
if (!CONFIG.enabled) return;
|
|
232
|
+
|
|
233
|
+
const { file_path, old_string, new_string } = toolInput;
|
|
234
|
+
if (!file_path || !old_string || !new_string) return;
|
|
235
|
+
|
|
236
|
+
if (!isCodeFile(file_path)) {
|
|
237
|
+
log('Skipping non-code file', { file_path });
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (
|
|
242
|
+
new_string.length < CONFIG.minEditSize &&
|
|
243
|
+
old_string.length < CONFIG.minEditSize
|
|
244
|
+
) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const state = loadState();
|
|
249
|
+
|
|
250
|
+
const diff = {
|
|
251
|
+
file_path,
|
|
252
|
+
original: old_string,
|
|
253
|
+
updated: new_string,
|
|
254
|
+
timestamp: Date.now(),
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
state.recentDiffs.unshift(diff);
|
|
258
|
+
state.recentDiffs = state.recentDiffs.slice(0, CONFIG.maxRecentDiffs);
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
if (fs.existsSync(file_path)) {
|
|
262
|
+
state.fileContents[file_path] = fs.readFileSync(file_path, 'utf-8');
|
|
263
|
+
}
|
|
264
|
+
} catch {
|
|
265
|
+
// Ignore
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
saveState(state);
|
|
269
|
+
log('Edit recorded', { file_path, diffSize: new_string.length });
|
|
270
|
+
|
|
271
|
+
if (shouldRunPrediction(state, file_path)) {
|
|
272
|
+
state.pendingPrediction = Date.now();
|
|
273
|
+
saveState(state);
|
|
274
|
+
|
|
275
|
+
setTimeout(() => {
|
|
276
|
+
runPredictionAsync(file_path, loadState());
|
|
277
|
+
}, CONFIG.debounceMs);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function runPredictionAsync(filePath, state) {
|
|
282
|
+
try {
|
|
283
|
+
const currentContent = state.fileContents[filePath] || '';
|
|
284
|
+
if (!currentContent) {
|
|
285
|
+
state.pendingPrediction = null;
|
|
286
|
+
saveState(state);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const result = await runPrediction(
|
|
291
|
+
filePath,
|
|
292
|
+
currentContent,
|
|
293
|
+
state.recentDiffs
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
state.pendingPrediction = null;
|
|
297
|
+
|
|
298
|
+
if (result && result.success && result.predicted_content) {
|
|
299
|
+
state.lastPrediction = {
|
|
300
|
+
file_path: filePath,
|
|
301
|
+
prediction: result.predicted_content,
|
|
302
|
+
latency_ms: result.latency_ms,
|
|
303
|
+
timestamp: Date.now(),
|
|
304
|
+
};
|
|
305
|
+
saveState(state);
|
|
306
|
+
|
|
307
|
+
log('Prediction complete', {
|
|
308
|
+
file_path: filePath,
|
|
309
|
+
latency_ms: result.latency_ms,
|
|
310
|
+
tokens: result.tokens_generated,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const hint = formatPredictionHint(result);
|
|
314
|
+
if (hint) {
|
|
315
|
+
console.error(hint);
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
saveState(state);
|
|
319
|
+
}
|
|
320
|
+
} catch (error) {
|
|
321
|
+
state.pendingPrediction = null;
|
|
322
|
+
saveState(state);
|
|
323
|
+
log('Prediction error', { error: error.message });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function formatPredictionHint(result) {
|
|
328
|
+
if (!result.predicted_content || result.predicted_content.trim().length < 5) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const preview = result.predicted_content
|
|
333
|
+
.trim()
|
|
334
|
+
.split('\n')
|
|
335
|
+
.slice(0, 3)
|
|
336
|
+
.join('\n');
|
|
337
|
+
const truncated = result.predicted_content.length > 200;
|
|
338
|
+
|
|
339
|
+
return `
|
|
340
|
+
[Sweep Prediction] Next edit suggestion (${result.latency_ms}ms):
|
|
341
|
+
${preview}${truncated ? '\n...' : ''}
|
|
342
|
+
`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function handleWrite(toolInput, toolResult) {
|
|
346
|
+
if (!CONFIG.enabled) return;
|
|
347
|
+
|
|
348
|
+
const { file_path, content } = toolInput;
|
|
349
|
+
if (!file_path || !content) return;
|
|
350
|
+
|
|
351
|
+
if (!isCodeFile(file_path)) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const state = loadState();
|
|
356
|
+
state.fileContents[file_path] = content;
|
|
357
|
+
saveState(state);
|
|
358
|
+
|
|
359
|
+
log('Write recorded', { file_path, size: content.length });
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function main() {
|
|
363
|
+
try {
|
|
364
|
+
const input = await readInput();
|
|
365
|
+
const { tool_name, tool_input, tool_result, event_type } = input;
|
|
366
|
+
|
|
367
|
+
// Only handle post-tool-use events
|
|
368
|
+
if (event_type !== 'post_tool_use') {
|
|
369
|
+
process.exit(0);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Handle different tools
|
|
373
|
+
switch (tool_name) {
|
|
374
|
+
case 'Edit':
|
|
375
|
+
await handleEdit(tool_input, tool_result);
|
|
376
|
+
break;
|
|
377
|
+
case 'Write':
|
|
378
|
+
await handleWrite(tool_input, tool_result);
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Success
|
|
383
|
+
console.log(JSON.stringify({ status: 'ok' }));
|
|
384
|
+
} catch (error) {
|
|
385
|
+
log('Hook error', { error: error.message });
|
|
386
|
+
console.log(JSON.stringify({ status: 'error', message: error.message }));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Handle info request
|
|
391
|
+
if (process.argv.includes('--info')) {
|
|
392
|
+
console.log(
|
|
393
|
+
JSON.stringify({
|
|
394
|
+
hook: 'post-edit-sweep',
|
|
395
|
+
version: '1.0.0',
|
|
396
|
+
description: 'Runs Sweep 1.5B predictions after file edits',
|
|
397
|
+
config: {
|
|
398
|
+
enabled: CONFIG.enabled,
|
|
399
|
+
maxRecentDiffs: CONFIG.maxRecentDiffs,
|
|
400
|
+
predictionTimeout: CONFIG.predictionTimeout,
|
|
401
|
+
},
|
|
402
|
+
})
|
|
403
|
+
);
|
|
404
|
+
process.exit(0);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Handle status request
|
|
408
|
+
if (process.argv.includes('--status')) {
|
|
409
|
+
const state = loadState();
|
|
410
|
+
const scriptPath = findPythonScript();
|
|
411
|
+
console.log(
|
|
412
|
+
JSON.stringify(
|
|
413
|
+
{
|
|
414
|
+
enabled: CONFIG.enabled,
|
|
415
|
+
scriptFound: !!scriptPath,
|
|
416
|
+
scriptPath,
|
|
417
|
+
recentDiffs: state.recentDiffs.length,
|
|
418
|
+
lastPrediction: state.lastPrediction,
|
|
419
|
+
},
|
|
420
|
+
null,
|
|
421
|
+
2
|
|
422
|
+
)
|
|
423
|
+
);
|
|
424
|
+
process.exit(0);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Handle clear request
|
|
428
|
+
if (process.argv.includes('--clear')) {
|
|
429
|
+
saveState({ recentDiffs: [], lastPrediction: null, fileContents: {} });
|
|
430
|
+
console.log('Sweep state cleared');
|
|
431
|
+
process.exit(0);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
main().catch((error) => {
|
|
435
|
+
console.error(JSON.stringify({ status: 'error', message: error.message }));
|
|
436
|
+
process.exit(1);
|
|
437
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env zsh
|
|
2
|
+
# StackMemory Sweep Completion for ZSH
|
|
3
|
+
# Non-intrusive: shows context in RPROMPT, no input hijacking
|
|
4
|
+
|
|
5
|
+
# Configuration
|
|
6
|
+
SWEEP_COMPLETE_ENABLED=${SWEEP_COMPLETE_ENABLED:-true}
|
|
7
|
+
SWEEP_STATE_FILE="${HOME}/.stackmemory/sweep-state.json"
|
|
8
|
+
SWEEP_SUGGEST_SCRIPT="${HOME}/.stackmemory/shell/sweep-suggest.js"
|
|
9
|
+
|
|
10
|
+
# State
|
|
11
|
+
typeset -g _sweep_suggestion=""
|
|
12
|
+
typeset -g _sweep_last_check=0
|
|
13
|
+
|
|
14
|
+
# Get suggestion (called on-demand only)
|
|
15
|
+
_sweep_get_suggestion() {
|
|
16
|
+
[[ "$SWEEP_COMPLETE_ENABLED" != "true" ]] && return 1
|
|
17
|
+
[[ ${#BUFFER} -lt 3 ]] && return 1
|
|
18
|
+
|
|
19
|
+
if [[ -f "$SWEEP_SUGGEST_SCRIPT" ]]; then
|
|
20
|
+
_sweep_suggestion=$(echo "$BUFFER" | timeout 0.5 node "$SWEEP_SUGGEST_SCRIPT" 2>/dev/null)
|
|
21
|
+
[[ -n "$_sweep_suggestion" ]] && return 0
|
|
22
|
+
fi
|
|
23
|
+
return 1
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Accept current suggestion
|
|
27
|
+
_sweep_accept() {
|
|
28
|
+
if [[ -n "$_sweep_suggestion" ]]; then
|
|
29
|
+
BUFFER="${BUFFER}${_sweep_suggestion}"
|
|
30
|
+
CURSOR=${#BUFFER}
|
|
31
|
+
_sweep_suggestion=""
|
|
32
|
+
RPROMPT="$_sweep_saved_rprompt"
|
|
33
|
+
zle redisplay
|
|
34
|
+
else
|
|
35
|
+
# Fall through to normal tab completion
|
|
36
|
+
zle expand-or-complete
|
|
37
|
+
fi
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Request suggestion manually (Ctrl+])
|
|
41
|
+
_sweep_request() {
|
|
42
|
+
if _sweep_get_suggestion; then
|
|
43
|
+
_sweep_saved_rprompt="$RPROMPT"
|
|
44
|
+
RPROMPT="%F{240}[${_sweep_suggestion}]%f"
|
|
45
|
+
zle redisplay
|
|
46
|
+
else
|
|
47
|
+
zle -M "No suggestion available"
|
|
48
|
+
fi
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Clear suggestion
|
|
52
|
+
_sweep_clear() {
|
|
53
|
+
_sweep_suggestion=""
|
|
54
|
+
RPROMPT="$_sweep_saved_rprompt"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Widget definitions
|
|
58
|
+
zle -N sweep-accept _sweep_accept
|
|
59
|
+
zle -N sweep-request _sweep_request
|
|
60
|
+
zle -N sweep-clear _sweep_clear
|
|
61
|
+
|
|
62
|
+
# Key bindings - ONLY these, no input hijacking
|
|
63
|
+
bindkey '^[[Z' sweep-request # Shift+Tab to request suggestion
|
|
64
|
+
bindkey '^I' sweep-accept # Tab to accept (falls through to normal completion if no suggestion)
|
|
65
|
+
|
|
66
|
+
# Show recent file context in RPROMPT (passive, after each command)
|
|
67
|
+
_sweep_show_context() {
|
|
68
|
+
[[ "$SWEEP_COMPLETE_ENABLED" != "true" ]] && return
|
|
69
|
+
|
|
70
|
+
if [[ -f "$SWEEP_STATE_FILE" ]]; then
|
|
71
|
+
local recent_file=$(grep -o '"file_path":"[^"]*"' "$SWEEP_STATE_FILE" 2>/dev/null | head -1 | cut -d'"' -f4)
|
|
72
|
+
if [[ -n "$recent_file" ]]; then
|
|
73
|
+
local filename=$(basename "$recent_file")
|
|
74
|
+
_sweep_saved_rprompt="%F{240}[${filename}]%f"
|
|
75
|
+
RPROMPT="$_sweep_saved_rprompt"
|
|
76
|
+
fi
|
|
77
|
+
fi
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Hook into prompt refresh (runs after each command, not during typing)
|
|
81
|
+
autoload -Uz add-zsh-hook
|
|
82
|
+
add-zsh-hook precmd _sweep_show_context
|
|
83
|
+
|
|
84
|
+
# Status
|
|
85
|
+
sweep_status() {
|
|
86
|
+
echo "Sweep Shell Integration"
|
|
87
|
+
echo " Enabled: $SWEEP_COMPLETE_ENABLED"
|
|
88
|
+
echo " Current suggestion: ${_sweep_suggestion:-none}"
|
|
89
|
+
echo ""
|
|
90
|
+
if [[ -f "$SWEEP_STATE_FILE" ]]; then
|
|
91
|
+
local count=$(grep -c '"file_path"' "$SWEEP_STATE_FILE" 2>/dev/null || echo 0)
|
|
92
|
+
echo " Recent edits tracked: $count"
|
|
93
|
+
fi
|
|
94
|
+
echo ""
|
|
95
|
+
echo "Usage:"
|
|
96
|
+
echo " Shift+Tab Request suggestion based on input"
|
|
97
|
+
echo " Tab Accept suggestion (or normal completion)"
|
|
98
|
+
echo ""
|
|
99
|
+
echo "The right prompt shows your most recently edited file."
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# Toggle
|
|
103
|
+
sweep_toggle() {
|
|
104
|
+
if [[ "$SWEEP_COMPLETE_ENABLED" == "true" ]]; then
|
|
105
|
+
SWEEP_COMPLETE_ENABLED=false
|
|
106
|
+
RPROMPT=""
|
|
107
|
+
echo "Sweep disabled"
|
|
108
|
+
else
|
|
109
|
+
SWEEP_COMPLETE_ENABLED=true
|
|
110
|
+
_sweep_show_context
|
|
111
|
+
echo "Sweep enabled"
|
|
112
|
+
fi
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
alias sweep-on='SWEEP_COMPLETE_ENABLED=true; _sweep_show_context; echo "Sweep enabled"'
|
|
116
|
+
alias sweep-off='SWEEP_COMPLETE_ENABLED=false; RPROMPT=""; echo "Sweep disabled"'
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Sweep Suggestion Script for Shell Integration
|
|
4
|
+
* Reads input from stdin and returns a suggestion
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const STATE_FILE = path.join(
|
|
11
|
+
process.env.HOME || '/tmp',
|
|
12
|
+
'.stackmemory',
|
|
13
|
+
'sweep-state.json'
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
function loadState() {
|
|
17
|
+
try {
|
|
18
|
+
if (fs.existsSync(STATE_FILE)) {
|
|
19
|
+
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
// Ignore
|
|
23
|
+
}
|
|
24
|
+
return { recentDiffs: [], fileContents: {} };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getRecentFile(state) {
|
|
28
|
+
if (!state.recentDiffs || state.recentDiffs.length === 0) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return state.recentDiffs[0]?.file_path;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getFilename(filepath) {
|
|
35
|
+
if (!filepath) return null;
|
|
36
|
+
return path.basename(filepath);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getSuggestion(userInput) {
|
|
40
|
+
const state = loadState();
|
|
41
|
+
const recentFile = getRecentFile(state);
|
|
42
|
+
const filename = getFilename(recentFile);
|
|
43
|
+
|
|
44
|
+
if (!filename) return null;
|
|
45
|
+
|
|
46
|
+
const input = userInput.toLowerCase().trim();
|
|
47
|
+
|
|
48
|
+
// Git commands - suggest based on recent file
|
|
49
|
+
if (input.startsWith('git commit')) {
|
|
50
|
+
if (input === 'git commit') {
|
|
51
|
+
return ` -m "Update ${filename}"`;
|
|
52
|
+
}
|
|
53
|
+
if (input === 'git commit -m') {
|
|
54
|
+
return ` "Update ${filename}"`;
|
|
55
|
+
}
|
|
56
|
+
if (input === 'git commit -m "') {
|
|
57
|
+
return `Update ${filename}"`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (input === 'git add') {
|
|
62
|
+
return ` ${recentFile}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (input === 'git diff') {
|
|
66
|
+
return ` ${recentFile}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (input === 'git log') {
|
|
70
|
+
return ` --oneline -10`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Action keywords at end
|
|
74
|
+
const actionPatterns = {
|
|
75
|
+
fix: ` the bug in ${filename}`,
|
|
76
|
+
add: ` feature to ${filename}`,
|
|
77
|
+
update: ` ${filename}`,
|
|
78
|
+
refactor: ` ${filename}`,
|
|
79
|
+
test: ` ${filename}`,
|
|
80
|
+
implement: ` in ${filename}`,
|
|
81
|
+
create: ` new function in ${filename}`,
|
|
82
|
+
delete: ` from ${filename}`,
|
|
83
|
+
remove: ` from ${filename}`,
|
|
84
|
+
edit: ` ${filename}`,
|
|
85
|
+
open: ` ${recentFile}`,
|
|
86
|
+
check: ` ${filename}`,
|
|
87
|
+
review: ` ${filename}`,
|
|
88
|
+
debug: ` ${filename}`,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
for (const [keyword, suffix] of Object.entries(actionPatterns)) {
|
|
92
|
+
if (input.endsWith(keyword)) {
|
|
93
|
+
return suffix;
|
|
94
|
+
}
|
|
95
|
+
if (input.endsWith(keyword + ' ')) {
|
|
96
|
+
return suffix.trim();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Preposition patterns
|
|
101
|
+
if (
|
|
102
|
+
input.endsWith(' in ') ||
|
|
103
|
+
input.endsWith(' to ') ||
|
|
104
|
+
input.endsWith(' for ')
|
|
105
|
+
) {
|
|
106
|
+
return filename;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (input.endsWith(' file ') || input.endsWith(' the ')) {
|
|
110
|
+
return filename;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// npm/node commands
|
|
114
|
+
if (input === 'npm run') {
|
|
115
|
+
return ' build';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (input === 'npm test') {
|
|
119
|
+
return ` -- ${filename.replace(/\.[^/.]+$/, '')}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (input === 'node') {
|
|
123
|
+
return ` ${recentFile}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Cat/less/vim
|
|
127
|
+
if (
|
|
128
|
+
input === 'cat' ||
|
|
129
|
+
input === 'less' ||
|
|
130
|
+
input === 'vim' ||
|
|
131
|
+
input === 'code'
|
|
132
|
+
) {
|
|
133
|
+
return ` ${recentFile}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function main() {
|
|
140
|
+
let data = '';
|
|
141
|
+
|
|
142
|
+
process.stdin.setEncoding('utf8');
|
|
143
|
+
|
|
144
|
+
for await (const chunk of process.stdin) {
|
|
145
|
+
data += chunk;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const userInput = data.trim();
|
|
149
|
+
|
|
150
|
+
if (!userInput || userInput.length < 2) {
|
|
151
|
+
process.exit(0);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const suggestion = getSuggestion(userInput);
|
|
155
|
+
|
|
156
|
+
if (suggestion) {
|
|
157
|
+
console.log(suggestion);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
main().catch(() => process.exit(0));
|