shmakk 1.1.0
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/.env.example +23 -0
- package/LICENSE +21 -0
- package/README.md +138 -0
- package/bin/shmakk.js +2 -0
- package/docs/index.html +581 -0
- package/docs/voice.md +181 -0
- package/package.json +58 -0
- package/scripts/patch-onnxruntime.js +82 -0
- package/src/agent.js +0 -0
- package/src/audit.js +18 -0
- package/src/cli.js +177 -0
- package/src/completions.js +167 -0
- package/src/control.js +250 -0
- package/src/correction.js +159 -0
- package/src/endpoints.js +52 -0
- package/src/global-doctor.js +33 -0
- package/src/global-setup.js +62 -0
- package/src/glossary.js +235 -0
- package/src/history-parser.js +166 -0
- package/src/hooks/bash.js +43 -0
- package/src/hooks/fish.js +25 -0
- package/src/hooks/index.js +14 -0
- package/src/hooks/zsh.js +42 -0
- package/src/index.js +166 -0
- package/src/llm.js +45 -0
- package/src/markers.js +113 -0
- package/src/orchestrator.js +61 -0
- package/src/profiles.js +19 -0
- package/src/prompt-cache.js +83 -0
- package/src/pty.js +107 -0
- package/src/review.js +75 -0
- package/src/safety.js +77 -0
- package/src/services/stt.js +131 -0
- package/src/services/tts.js +307 -0
- package/src/services/voice.js +362 -0
- package/src/session.js +604 -0
- package/src/setup-voice.js +108 -0
- package/src/shell.js +32 -0
- package/src/skills.js +309 -0
- package/src/subagent.js +42 -0
- package/src/system-prompt.js +261 -0
- package/src/tools.js +386 -0
- package/src/web.js +228 -0
- package/src/workspace-index.js +213 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
function run(cmd) {
|
|
7
|
+
try { return execSync(cmd, { encoding: 'utf8' }).trim(); }
|
|
8
|
+
catch { return ''; }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function ensureLine(filePath, line) {
|
|
12
|
+
let content = '';
|
|
13
|
+
try { content = fs.readFileSync(filePath, 'utf8'); } catch {}
|
|
14
|
+
if (content.includes(line)) return false;
|
|
15
|
+
const next = content.endsWith('\n') || content.length === 0 ? content : `${content}\n`;
|
|
16
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
17
|
+
fs.writeFileSync(filePath, `${next}${line}\n`, 'utf8');
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function main() {
|
|
22
|
+
const home = os.homedir();
|
|
23
|
+
const prefix = run('npm config get prefix') || '/usr/local';
|
|
24
|
+
const npmBin = path.join(prefix, 'bin');
|
|
25
|
+
const shell = process.env.SHELL || '';
|
|
26
|
+
|
|
27
|
+
const fishConf = path.join(home, '.config/fish/config.fish');
|
|
28
|
+
const bashrc = path.join(home, '.bashrc');
|
|
29
|
+
const zshrc = path.join(home, '.zshrc');
|
|
30
|
+
|
|
31
|
+
const fishLine = `fish_add_path -g ${npmBin}`;
|
|
32
|
+
const shLine = `export PATH="${npmBin}:$PATH"`;
|
|
33
|
+
|
|
34
|
+
let changed = false;
|
|
35
|
+
|
|
36
|
+
if (shell.includes('fish')) {
|
|
37
|
+
changed = ensureLine(fishConf, fishLine) || changed;
|
|
38
|
+
} else if (shell.includes('zsh')) {
|
|
39
|
+
changed = ensureLine(zshrc, shLine) || changed;
|
|
40
|
+
} else {
|
|
41
|
+
changed = ensureLine(bashrc, shLine) || changed;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Also add fish config as many users launch fish from other login shells.
|
|
45
|
+
changed = ensureLine(fishConf, fishLine) || changed;
|
|
46
|
+
|
|
47
|
+
console.log('Global PATH setup');
|
|
48
|
+
console.log('-----------------');
|
|
49
|
+
console.log('npm prefix :', prefix);
|
|
50
|
+
console.log('npm bin :', npmBin);
|
|
51
|
+
console.log('shell :', shell || '(unknown)');
|
|
52
|
+
if (changed) {
|
|
53
|
+
console.log('\nUpdated shell config. Open a new terminal, then run:');
|
|
54
|
+
console.log(' shmakk --help');
|
|
55
|
+
} else {
|
|
56
|
+
console.log('\nShell config already contains npm global bin path.');
|
|
57
|
+
console.log('Open a new terminal, then run:');
|
|
58
|
+
console.log(' shmakk --help');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
main();
|
package/src/glossary.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// Static, no-execution command glossary.
|
|
2
|
+
//
|
|
3
|
+
// We never run binaries to extract help. Instead we:
|
|
4
|
+
// 1. List executables on PATH (just names + paths).
|
|
5
|
+
// 2. Parse fish completion files: `complete -c CMD -s X -l LONG -a '...'`
|
|
6
|
+
// 3. Parse bash-completion files for `--long-flag` tokens (best-effort).
|
|
7
|
+
//
|
|
8
|
+
// This is intentionally less rich than running `--help`, but it is safe:
|
|
9
|
+
// no programs are launched, no TTY/X/Wayland/dbus interaction occurs, and
|
|
10
|
+
// nothing on the user's system can be modified by the scan.
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const HELP_KEEP_BYTES = 4 * 1024;
|
|
17
|
+
|
|
18
|
+
function defaultGlossaryPath() {
|
|
19
|
+
const base = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
|
|
20
|
+
return path.join(base, 'shmakk', 'command-glossary.json');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ── PATH enumeration ───────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function listPathBinaries() {
|
|
26
|
+
const seen = new Map();
|
|
27
|
+
const dirs = (process.env.PATH || '').split(':').filter(Boolean);
|
|
28
|
+
for (const dir of dirs) {
|
|
29
|
+
let entries;
|
|
30
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
31
|
+
for (const e of entries) {
|
|
32
|
+
if (e.isDirectory()) continue;
|
|
33
|
+
const full = path.join(dir, e.name);
|
|
34
|
+
try {
|
|
35
|
+
const st = fs.statSync(full);
|
|
36
|
+
if (!(st.mode & 0o111)) continue;
|
|
37
|
+
} catch { continue; }
|
|
38
|
+
if (!seen.has(e.name)) seen.set(e.name, []);
|
|
39
|
+
const list = seen.get(e.name);
|
|
40
|
+
if (!list.includes(full)) list.push(full);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return seen;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── fish completions ───────────────────────────────────────────────────────
|
|
47
|
+
//
|
|
48
|
+
// Lines look like:
|
|
49
|
+
// complete -c git -n '__fish_git_using_command status' -l short -d 'short fmt'
|
|
50
|
+
// complete -c git -a 'add commit push' -d 'commands'
|
|
51
|
+
// complete -c npm -s g -l global
|
|
52
|
+
|
|
53
|
+
function fishCompletionDirs() {
|
|
54
|
+
const dirs = [];
|
|
55
|
+
const home = os.homedir();
|
|
56
|
+
if (process.env.XDG_DATA_HOME) {
|
|
57
|
+
dirs.push(path.join(process.env.XDG_DATA_HOME, 'fish', 'vendor_completions.d'));
|
|
58
|
+
}
|
|
59
|
+
dirs.push(
|
|
60
|
+
path.join(home, '.config', 'fish', 'completions'),
|
|
61
|
+
path.join(home, '.local', 'share', 'fish', 'vendor_completions.d'),
|
|
62
|
+
'/usr/share/fish/completions',
|
|
63
|
+
'/usr/share/fish/vendor_completions.d',
|
|
64
|
+
'/usr/local/share/fish/completions',
|
|
65
|
+
'/usr/local/share/fish/vendor_completions.d',
|
|
66
|
+
);
|
|
67
|
+
return dirs.filter((d) => { try { return fs.statSync(d).isDirectory(); } catch { return false; } });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const FISH_COMPLETE = /^\s*complete\b(.*)$/;
|
|
71
|
+
// extract value of -c / -s / -l / -a / -d, supporting both quoted and bare
|
|
72
|
+
function parseCompleteArgs(line) {
|
|
73
|
+
const out = { c: null, s: [], l: [], a: [] };
|
|
74
|
+
// Tokenize respecting single/double quotes. Simple split is unsafe.
|
|
75
|
+
const toks = tokenize(line);
|
|
76
|
+
for (let i = 0; i < toks.length; i++) {
|
|
77
|
+
const t = toks[i];
|
|
78
|
+
if (t === '-c' || t === '--command') out.c = toks[++i];
|
|
79
|
+
else if (t === '-s' || t === '--short-option') out.s.push(toks[++i]);
|
|
80
|
+
else if (t === '-l' || t === '--long-option') out.l.push(toks[++i]);
|
|
81
|
+
else if (t === '-o' || t === '--old-option') out.l.push(toks[++i]);
|
|
82
|
+
else if (t === '-a' || t === '--arguments') out.a.push(toks[++i]);
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function tokenize(s) {
|
|
88
|
+
const out = [];
|
|
89
|
+
let i = 0; const n = s.length;
|
|
90
|
+
while (i < n) {
|
|
91
|
+
while (i < n && /\s/.test(s[i])) i++;
|
|
92
|
+
if (i >= n) break;
|
|
93
|
+
const ch = s[i];
|
|
94
|
+
if (ch === '"' || ch === "'") {
|
|
95
|
+
const q = ch; i++;
|
|
96
|
+
let v = '';
|
|
97
|
+
while (i < n && s[i] !== q) { v += s[i++]; }
|
|
98
|
+
i++; // closing
|
|
99
|
+
out.push(v);
|
|
100
|
+
} else {
|
|
101
|
+
let v = '';
|
|
102
|
+
while (i < n && !/\s/.test(s[i])) { v += s[i++]; }
|
|
103
|
+
out.push(v);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function parseFishCompletions(commands) {
|
|
110
|
+
for (const dir of fishCompletionDirs()) {
|
|
111
|
+
let files;
|
|
112
|
+
try { files = fs.readdirSync(dir); } catch { continue; }
|
|
113
|
+
for (const f of files) {
|
|
114
|
+
if (!f.endsWith('.fish')) continue;
|
|
115
|
+
const cmdName = f.replace(/\.fish$/, '');
|
|
116
|
+
const entry = ensureEntry(commands, cmdName);
|
|
117
|
+
let text;
|
|
118
|
+
try { text = fs.readFileSync(path.join(dir, f), 'utf8'); } catch { continue; }
|
|
119
|
+
for (const rawLine of text.split('\n')) {
|
|
120
|
+
const m = FISH_COMPLETE.exec(rawLine);
|
|
121
|
+
if (!m) continue;
|
|
122
|
+
const args = parseCompleteArgs(m[1]);
|
|
123
|
+
const target = args.c || cmdName;
|
|
124
|
+
const e = ensureEntry(commands, target);
|
|
125
|
+
for (const sh of args.s) if (sh) e.flags.add('-' + sh);
|
|
126
|
+
for (const lo of args.l) if (lo) e.flags.add('--' + lo);
|
|
127
|
+
for (const a of args.a) {
|
|
128
|
+
for (const sub of String(a).split(/\s+/)) {
|
|
129
|
+
if (sub && /^[a-z][a-z0-9_-]{0,30}$/i.test(sub)) e.subcommands.add(sub);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
e.sources.add('fish:' + path.basename(dir));
|
|
133
|
+
if (entry !== e) entry.aliasOf = target;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── bash completions ───────────────────────────────────────────────────────
|
|
140
|
+
// Best-effort: extract long flags that appear after the command's COMPREPLY
|
|
141
|
+
// generation. We just regex `--[a-z][\w-]*` from the file.
|
|
142
|
+
|
|
143
|
+
function bashCompletionDirs() {
|
|
144
|
+
return [
|
|
145
|
+
'/usr/share/bash-completion/completions',
|
|
146
|
+
'/usr/local/share/bash-completion/completions',
|
|
147
|
+
'/etc/bash_completion.d',
|
|
148
|
+
].filter((d) => { try { return fs.statSync(d).isDirectory(); } catch { return false; } });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const FLAG_RE = /(--[a-zA-Z][\w-]*)/g;
|
|
152
|
+
|
|
153
|
+
function parseBashCompletions(commands) {
|
|
154
|
+
for (const dir of bashCompletionDirs()) {
|
|
155
|
+
let files;
|
|
156
|
+
try { files = fs.readdirSync(dir); } catch { continue; }
|
|
157
|
+
for (const f of files) {
|
|
158
|
+
const cmdName = f; // bash-completion files are typically named after the command
|
|
159
|
+
let text;
|
|
160
|
+
try { text = fs.readFileSync(path.join(dir, f), 'utf8'); } catch { continue; }
|
|
161
|
+
const e = ensureEntry(commands, cmdName);
|
|
162
|
+
let m; let count = 0;
|
|
163
|
+
while ((m = FLAG_RE.exec(text)) !== null) {
|
|
164
|
+
e.flags.add(m[1]);
|
|
165
|
+
if (++count > 200) break;
|
|
166
|
+
}
|
|
167
|
+
if (count) e.sources.add('bash:' + path.basename(dir));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── shape & write ──────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
function ensureEntry(commands, name) {
|
|
175
|
+
if (!commands[name]) {
|
|
176
|
+
commands[name] = {
|
|
177
|
+
paths: [],
|
|
178
|
+
flags: new Set(),
|
|
179
|
+
subcommands: new Set(),
|
|
180
|
+
sources: new Set(),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return commands[name];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function freezeEntry(e) {
|
|
187
|
+
return {
|
|
188
|
+
paths: e.paths,
|
|
189
|
+
flags: Array.from(e.flags).sort().slice(0, 200),
|
|
190
|
+
subcommands: Array.from(e.subcommands).sort().slice(0, 100),
|
|
191
|
+
sources: Array.from(e.sources).sort(),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function buildGlossary({ onProgress } = {}) {
|
|
196
|
+
const bins = listPathBinaries();
|
|
197
|
+
const commands = {};
|
|
198
|
+
|
|
199
|
+
let i = 0; const total = bins.size;
|
|
200
|
+
for (const [name, paths] of bins) {
|
|
201
|
+
const e = ensureEntry(commands, name);
|
|
202
|
+
e.paths = paths;
|
|
203
|
+
if (onProgress && ++i % 200 === 0) onProgress(i, total);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
parseFishCompletions(commands);
|
|
207
|
+
parseBashCompletions(commands);
|
|
208
|
+
|
|
209
|
+
const out = {};
|
|
210
|
+
for (const [name, e] of Object.entries(commands)) out[name] = freezeEntry(e);
|
|
211
|
+
return { generatedAt: new Date().toISOString(), commands: out };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function updateGlossary() {
|
|
215
|
+
const out = defaultGlossaryPath();
|
|
216
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
217
|
+
process.stderr.write('[shmakk] scanning PATH and completion files (no programs are executed)...\n');
|
|
218
|
+
const data = await buildGlossary({
|
|
219
|
+
onProgress: (d, t) => process.stderr.write(`[shmakk] ${d}/${t} binaries\r`),
|
|
220
|
+
});
|
|
221
|
+
fs.writeFileSync(out, JSON.stringify(data, null, 2));
|
|
222
|
+
const n = Object.keys(data.commands).length;
|
|
223
|
+
const withFlags = Object.values(data.commands).filter((e) => e.flags.length).length;
|
|
224
|
+
process.stderr.write(`\n[shmakk] wrote ${n} commands (${withFlags} with completion data) → ${out}\n`);
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function loadGlossary() {
|
|
229
|
+
try {
|
|
230
|
+
const txt = fs.readFileSync(defaultGlossaryPath(), 'utf8');
|
|
231
|
+
return JSON.parse(txt);
|
|
232
|
+
} catch { return null; }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = { updateGlossary, loadGlossary, defaultGlossaryPath, buildGlossary };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// History parser — reads bash, zsh, fish history files and builds a
|
|
2
|
+
// frequency map of command usage for tie-breaking in corrections.
|
|
3
|
+
//
|
|
4
|
+
// Frequency map format:
|
|
5
|
+
// { "git": 4521, "ls": 10982, "npm": 893, "cat": 542, ... }
|
|
6
|
+
//
|
|
7
|
+
// Stored at: .shmakk/state/command-freq.json
|
|
8
|
+
//
|
|
9
|
+
// The user controls what goes in — no auto-learning from corrections.
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
const STATE_DIR = path.join(process.cwd(), '.shmakk', 'state');
|
|
15
|
+
const FREQ_FILE = path.join(STATE_DIR, 'command-freq.json');
|
|
16
|
+
|
|
17
|
+
// ── Parsers per shell format ──────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
// Bash: one command per line, no timestamps.
|
|
20
|
+
function parseBashHistory(content) {
|
|
21
|
+
const freq = {};
|
|
22
|
+
for (const line of content.split(/\r?\n/)) {
|
|
23
|
+
const trimmed = line.trim();
|
|
24
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
25
|
+
const cmd = trimmed.split(/\s+/)[0];
|
|
26
|
+
if (cmd) freq[cmd] = (freq[cmd] || 0) + 1;
|
|
27
|
+
}
|
|
28
|
+
return freq;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Zsh: : <timestamp>:<duration>;<command>
|
|
32
|
+
// Example: : 1776037585:0;ll
|
|
33
|
+
function parseZshHistory(content) {
|
|
34
|
+
const freq = {};
|
|
35
|
+
for (const line of content.split(/\r?\n/)) {
|
|
36
|
+
const trimmed = line.trim();
|
|
37
|
+
if (!trimmed) continue;
|
|
38
|
+
// Format: : ts:duration;command
|
|
39
|
+
const semi = trimmed.indexOf(';');
|
|
40
|
+
if (trimmed.startsWith(':') && semi !== -1) {
|
|
41
|
+
const cmd = trimmed.slice(semi + 1).trim().split(/\s+/)[0];
|
|
42
|
+
if (cmd) freq[cmd] = (freq[cmd] || 0) + 1;
|
|
43
|
+
} else {
|
|
44
|
+
// Fallback: treat as bare command
|
|
45
|
+
const cmd = trimmed.split(/\s+/)[0];
|
|
46
|
+
if (cmd) freq[cmd] = (freq[cmd] || 0) + 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return freq;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fish: YAML-like format
|
|
53
|
+
// - cmd: <command>
|
|
54
|
+
// when: <timestamp>
|
|
55
|
+
function parseFishHistory(content) {
|
|
56
|
+
const freq = {};
|
|
57
|
+
let inEntry = false;
|
|
58
|
+
let lastCmd = null;
|
|
59
|
+
for (const line of content.split(/\r?\n/)) {
|
|
60
|
+
const trimmed = line.trim();
|
|
61
|
+
if (trimmed.startsWith('- cmd:')) {
|
|
62
|
+
// flush previous
|
|
63
|
+
if (lastCmd) freq[lastCmd] = (freq[lastCmd] || 0) + 1;
|
|
64
|
+
lastCmd = trimmed.slice(6).trim();
|
|
65
|
+
inEntry = true;
|
|
66
|
+
} else if (inEntry && trimmed.startsWith('when:')) {
|
|
67
|
+
// end of entry — next line will start a new one or EOF
|
|
68
|
+
inEntry = false;
|
|
69
|
+
} else if (!trimmed) {
|
|
70
|
+
inEntry = false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Flush last entry
|
|
74
|
+
if (lastCmd) freq[lastCmd] = (freq[lastCmd] || 0) + 1;
|
|
75
|
+
return freq;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Detect format and parse a single history file.
|
|
79
|
+
// Returns a frequency map for that file.
|
|
80
|
+
function parseHistoryFile(filePath) {
|
|
81
|
+
let content;
|
|
82
|
+
try {
|
|
83
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
84
|
+
} catch (e) {
|
|
85
|
+
process.stderr.write(`[shmakk] warning: cannot read ${filePath}: ${e.message}\n`);
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
if (!content.trim()) return {};
|
|
89
|
+
|
|
90
|
+
const name = path.basename(filePath);
|
|
91
|
+
|
|
92
|
+
// Fish history: YAML-like with "- cmd:" entries
|
|
93
|
+
if (name === 'fish_history' || content.includes('- cmd:')) {
|
|
94
|
+
return parseFishHistory(content);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Zsh history: lines start with ": <timestamp>:"
|
|
98
|
+
if (content.match(/^:\s+\d+:\d+;/m)) {
|
|
99
|
+
return parseZshHistory(content);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Bash history: one command per line (default fallback)
|
|
103
|
+
return parseBashHistory(content);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Auto-detect common history files on this system.
|
|
107
|
+
function autoDetectHistoryFiles() {
|
|
108
|
+
const home = process.env.HOME || '/home/' + (process.env.USER || 'unknown');
|
|
109
|
+
const candidates = [
|
|
110
|
+
path.join(home, '.bash_history'),
|
|
111
|
+
path.join(home, '.zsh_history'),
|
|
112
|
+
path.join(home, '.local/share/fish/fish_history'),
|
|
113
|
+
path.join(home, '.config/fish/fish_history'),
|
|
114
|
+
];
|
|
115
|
+
return candidates.filter((f) => {
|
|
116
|
+
try { return fs.statSync(f).isFile(); } catch { return false; }
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Parse one or more history files and merge into a single frequency map.
|
|
121
|
+
function buildFreqMap(filePaths) {
|
|
122
|
+
const merged = {};
|
|
123
|
+
for (const fp of filePaths) {
|
|
124
|
+
const freq = parseHistoryFile(fp);
|
|
125
|
+
for (const [cmd, count] of Object.entries(freq)) {
|
|
126
|
+
merged[cmd] = (merged[cmd] || 0) + count;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return merged;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Load / save the frequency map ─────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
function freqMapPath(baseDir) {
|
|
135
|
+
return path.join(baseDir || process.cwd(), '.shmakk', 'state', 'command-freq.json');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function loadFreqMap(baseDir) {
|
|
139
|
+
const fp = freqMapPath(baseDir);
|
|
140
|
+
try {
|
|
141
|
+
const raw = fs.readFileSync(fp, 'utf8');
|
|
142
|
+
const parsed = JSON.parse(raw);
|
|
143
|
+
return typeof parsed === 'object' && parsed !== null ? parsed : {};
|
|
144
|
+
} catch {
|
|
145
|
+
return {};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function saveFreqMap(freqMap, baseDir) {
|
|
150
|
+
const dir = path.join(baseDir || process.cwd(), '.shmakk', 'state');
|
|
151
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
152
|
+
const fp = path.join(dir, 'command-freq.json');
|
|
153
|
+
fs.writeFileSync(fp, JSON.stringify(freqMap, null, 2) + '\n');
|
|
154
|
+
return fp;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
parseBashHistory,
|
|
159
|
+
parseZshHistory,
|
|
160
|
+
parseFishHistory,
|
|
161
|
+
parseHistoryFile,
|
|
162
|
+
autoDetectHistoryFiles,
|
|
163
|
+
buildFreqMap,
|
|
164
|
+
loadFreqMap,
|
|
165
|
+
saveFreqMap,
|
|
166
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const INIT = `
|
|
6
|
+
[ -f /etc/bash.bashrc ] && . /etc/bash.bashrc
|
|
7
|
+
[ -f "$HOME/.bashrc" ] && . "$HOME/.bashrc"
|
|
8
|
+
|
|
9
|
+
__shmakk_armed=1
|
|
10
|
+
__shmakk_preexec() {
|
|
11
|
+
[ -n "$COMP_LINE" ] && return
|
|
12
|
+
[ -z "$__shmakk_armed" ] && return
|
|
13
|
+
[ "$BASH_COMMAND" = "$PROMPT_COMMAND" ] && return
|
|
14
|
+
__shmakk_armed=
|
|
15
|
+
local cmd
|
|
16
|
+
cmd=$(printf '%s' "$BASH_COMMAND" | base64 -w0 2>/dev/null || printf '%s' "$BASH_COMMAND" | base64)
|
|
17
|
+
printf '\\e]6973;B;%s\\a' "$cmd"
|
|
18
|
+
}
|
|
19
|
+
__shmakk_precmd() {
|
|
20
|
+
local ec=$?
|
|
21
|
+
local p
|
|
22
|
+
p=$(printf '%s' "$PWD" | base64 -w0 2>/dev/null || printf '%s' "$PWD" | base64)
|
|
23
|
+
printf '\\e]6973;C;%s\\a' "$ec"
|
|
24
|
+
printf '\\e]6973;D;%s\\a' "$p"
|
|
25
|
+
__shmakk_armed=1
|
|
26
|
+
}
|
|
27
|
+
trap '__shmakk_preexec' DEBUG
|
|
28
|
+
PROMPT_COMMAND="__shmakk_precmd\${PROMPT_COMMAND:+;\$PROMPT_COMMAND}"
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
function configure() {
|
|
32
|
+
const dir = path.join(os.tmpdir(), `shmakk-bash-${process.pid}`);
|
|
33
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
34
|
+
const rcfile = path.join(dir, 'bashrc');
|
|
35
|
+
fs.writeFileSync(rcfile, INIT, { mode: 0o600 });
|
|
36
|
+
return {
|
|
37
|
+
args: ['--rcfile', rcfile, '-i'],
|
|
38
|
+
env: {},
|
|
39
|
+
cleanup: () => { try { fs.rmSync(dir, { recursive: true, force: true }); } catch {} },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { configure };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Returns { args, env, cleanup } for spawning fish with markers wired up.
|
|
2
|
+
// fish supports `-C COMMAND` to run init code after config.fish.
|
|
3
|
+
|
|
4
|
+
const INIT = `
|
|
5
|
+
function __shmakk_pre --on-event fish_preexec
|
|
6
|
+
set -l c (printf '%s' "$argv" | base64 -w0 2>/dev/null; or printf '%s' "$argv" | base64)
|
|
7
|
+
printf '\\e]6973;B;%s\\a' "$c"
|
|
8
|
+
end
|
|
9
|
+
function __shmakk_post --on-event fish_postexec
|
|
10
|
+
set -l ec $status
|
|
11
|
+
set -l p (printf '%s' "$PWD" | base64 -w0 2>/dev/null; or printf '%s' "$PWD" | base64)
|
|
12
|
+
printf '\\e]6973;C;%s\\a' $ec
|
|
13
|
+
printf '\\e]6973;D;%s\\a' "$p"
|
|
14
|
+
end
|
|
15
|
+
`.trim();
|
|
16
|
+
|
|
17
|
+
function configure() {
|
|
18
|
+
return {
|
|
19
|
+
args: ['-i', '-l', '-C', INIT],
|
|
20
|
+
env: {},
|
|
21
|
+
cleanup: () => {},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = { configure };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const fish = require('./fish');
|
|
2
|
+
const bash = require('./bash');
|
|
3
|
+
const zsh = require('./zsh');
|
|
4
|
+
|
|
5
|
+
function configureForShell(name) {
|
|
6
|
+
switch (name) {
|
|
7
|
+
case 'fish': return fish.configure();
|
|
8
|
+
case 'bash': return bash.configure();
|
|
9
|
+
case 'zsh': return zsh.configure();
|
|
10
|
+
default: return { args: ['-i'], env: {}, cleanup: () => {} };
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = { configureForShell };
|
package/src/hooks/zsh.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const ZSHRC = `
|
|
6
|
+
# preserve real ZDOTDIR so user config is sourced
|
|
7
|
+
if [ -n "$SHMAKK_REAL_ZDOTDIR" ]; then
|
|
8
|
+
[ -f "$SHMAKK_REAL_ZDOTDIR/.zshrc" ] && source "$SHMAKK_REAL_ZDOTDIR/.zshrc"
|
|
9
|
+
elif [ -f "$HOME/.zshrc" ]; then
|
|
10
|
+
source "$HOME/.zshrc"
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
__shmakk_preexec() {
|
|
14
|
+
local cmd
|
|
15
|
+
cmd=$(printf '%s' "$1" | base64 -w0 2>/dev/null || printf '%s' "$1" | base64)
|
|
16
|
+
printf '\\e]6973;B;%s\\a' "$cmd"
|
|
17
|
+
}
|
|
18
|
+
__shmakk_precmd() {
|
|
19
|
+
local ec=$?
|
|
20
|
+
local p
|
|
21
|
+
p=$(printf '%s' "$PWD" | base64 -w0 2>/dev/null || printf '%s' "$PWD" | base64)
|
|
22
|
+
printf '\\e]6973;C;%s\\a' "$ec"
|
|
23
|
+
printf '\\e]6973;D;%s\\a' "$p"
|
|
24
|
+
}
|
|
25
|
+
typeset -ag preexec_functions precmd_functions
|
|
26
|
+
preexec_functions+=(__shmakk_preexec)
|
|
27
|
+
precmd_functions+=(__shmakk_precmd)
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
function configure() {
|
|
31
|
+
const dir = path.join(os.tmpdir(), `shmakk-zsh-${process.pid}`);
|
|
32
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
33
|
+
fs.writeFileSync(path.join(dir, '.zshrc'), ZSHRC, { mode: 0o600 });
|
|
34
|
+
const realZ = process.env.ZDOTDIR || '';
|
|
35
|
+
return {
|
|
36
|
+
args: ['-i'],
|
|
37
|
+
env: { ZDOTDIR: dir, SHMAKK_REAL_ZDOTDIR: realZ },
|
|
38
|
+
cleanup: () => { try { fs.rmSync(dir, { recursive: true, force: true }); } catch {} },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { configure };
|