@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.
Files changed (37) hide show
  1. package/bin/claude-sm +6 -0
  2. package/bin/claude-smd +6 -0
  3. package/dist/cli/claude-sm-danger.js +20 -0
  4. package/dist/cli/claude-sm-danger.js.map +7 -0
  5. package/dist/cli/commands/api.js +228 -0
  6. package/dist/cli/commands/api.js.map +7 -0
  7. package/dist/cli/commands/cleanup-processes.js +64 -0
  8. package/dist/cli/commands/cleanup-processes.js.map +7 -0
  9. package/dist/cli/commands/hooks.js +294 -0
  10. package/dist/cli/commands/hooks.js.map +7 -0
  11. package/dist/cli/commands/shell.js +248 -0
  12. package/dist/cli/commands/shell.js.map +7 -0
  13. package/dist/cli/commands/sweep.js +173 -5
  14. package/dist/cli/commands/sweep.js.map +3 -3
  15. package/dist/cli/index.js +9 -1
  16. package/dist/cli/index.js.map +2 -2
  17. package/dist/hooks/config.js +146 -0
  18. package/dist/hooks/config.js.map +7 -0
  19. package/dist/hooks/daemon.js +360 -0
  20. package/dist/hooks/daemon.js.map +7 -0
  21. package/dist/hooks/events.js +51 -0
  22. package/dist/hooks/events.js.map +7 -0
  23. package/dist/hooks/index.js +4 -0
  24. package/dist/hooks/index.js.map +7 -0
  25. package/dist/skills/api-discovery.js +349 -0
  26. package/dist/skills/api-discovery.js.map +7 -0
  27. package/dist/skills/api-skill.js +471 -0
  28. package/dist/skills/api-skill.js.map +7 -0
  29. package/dist/skills/claude-skills.js +49 -1
  30. package/dist/skills/claude-skills.js.map +2 -2
  31. package/dist/utils/process-cleanup.js +132 -0
  32. package/dist/utils/process-cleanup.js.map +7 -0
  33. package/package.json +4 -2
  34. package/scripts/install-sweep-hook.sh +89 -0
  35. package/templates/claude-hooks/post-edit-sweep.js +437 -0
  36. package/templates/shell/sweep-complete.zsh +116 -0
  37. 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));