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.
@@ -0,0 +1,167 @@
1
+ // Shell tab-completion scripts for shmakk.
2
+ //
3
+ // Generate with:
4
+ // shmakk --completion bash > ~/.shmakk-completion.bash && source ~/.shmakk-completion.bash
5
+ // shmakk --completion zsh > ~/.shmakk-completion.zsh && source ~/.shmakk-completion.zsh
6
+ // shmakk --completion fish > ~/.config/fish/completions/shmakk.fish
7
+ //
8
+ // For permanent install:
9
+ // bash: echo 'source ~/.shmakk-completion.bash' >> ~/.bashrc
10
+ // zsh: echo 'source ~/.shmakk-completion.zsh' >> ~/.zshrc
11
+
12
+ // All known flags in one place so the completion scripts stay in sync.
13
+ const FLAGS = [
14
+ // booleans (no argument)
15
+ { flag: '--review', arg: false, desc: 'Review mode (confirm every AI action)' },
16
+ { flag: '--yes-files', arg: false, desc: 'Auto-accept AI file writes/edits' },
17
+ { flag: '--no-ai', arg: false, desc: 'Disable AI entirely (pure passthrough)' },
18
+ { flag: '--no-correction', arg: false, desc: 'Disable command correction' },
19
+ { flag: '--help', arg: false, desc: 'Show help' },
20
+ { flag: '--debug', arg: false, desc: 'Verbose logging' },
21
+ { flag: '--print-config', arg: false, desc: 'Print resolved config and exit' },
22
+ { flag: '--update-command-glossary', arg: false, desc: 'Scan PATH and build command glossary' },
23
+ { flag: '--status', arg: false, desc: 'Show whether inside shmakk' },
24
+ { flag: '--stats', arg: false, desc: 'Show session/task stats' },
25
+ { flag: '--compact', arg: false, desc: 'Clear conversation + task journal' },
26
+ { flag: '--list-skills', arg: false, desc: 'List registered local skills' },
27
+ { flag: '--skill-status', arg: false, desc: 'Show active skill and registry status' },
28
+ { flag: '--resume-status', arg: false, desc: 'Show task journal summary' },
29
+ { flag: '--exit', arg: false, desc: 'Cleanly exit parent shmakk' },
30
+ { flag: '--restart', arg: false, desc: 'Restart inner shell' },
31
+ { flag: '--reset', arg: false, desc: 'Clear AI conversation history' },
32
+ { flag: '--stt', arg: false, desc: 'Speech-to-Text: mic → text input' },
33
+ { flag: '--tts', arg: false, desc: 'Text-to-Speech: spoken responses' },
34
+ { flag: '--sts', arg: false, desc: 'Speech-to-Speech: always-on mic + TTS' },
35
+ { flag: '--voice', arg: false, desc: 'Enable voice input (stt shortcut)' },
36
+
37
+ // flags with arguments
38
+ { flag: '--workspace', arg: '<path>', desc: 'Override workspace root' },
39
+ { flag: '--profile', arg: '<name>', desc: 'Startup profile (tiny|balanced|deep|builder|large-app)' },
40
+ { flag: '--profile-set', arg: '<name>', desc: 'Switch profile and restart' },
41
+ { flag: '--endpoint', arg: '<name>', desc: 'Use endpoint preset from .shmakk/endpoints.json' },
42
+ { flag: '--colors', arg: '<true|false>', desc: 'Toggle ANSI colors' },
43
+ { flag: '--load-skill', arg: '<name>', desc: 'Load a skill into workspace state' },
44
+ { flag: '--unload-skill', arg: '<name>', desc: 'Remove skill from registry' },
45
+ { flag: '--install-skill', arg: '<url>', desc: 'Download and install skill from URL' },
46
+ { flag: '--build-history', arg: '[files...]', desc: 'Parse shell history for frequency map' },
47
+ { flag: '--completion', arg: '<bash|zsh|fish>', desc: 'Output shell completion script' },
48
+ // voice tunables
49
+ { flag: '--voice-language', arg: '<code>', desc: 'Language hint (en, es, fr)' },
50
+ { flag: '--voice-max-sec', arg: '<sec>', desc: 'Max recording seconds' },
51
+ { flag: '--voice-silence-sec', arg: '<sec>', desc: 'VAD silence before stopping' },
52
+ { flag: '--voice-silence-threshold', arg: '<%>', desc: 'VAD amplitude threshold' },
53
+ { flag: '--voice-silence-start-sec', arg: '<sec>', desc: 'Sound before recording starts' },
54
+ { flag: '--voice-pad-start-sec', arg: '<sec>', desc: 'Padding before recording' },
55
+ { flag: '--tts-voice', arg: '<name>', desc: 'Override Kokoro voice' },
56
+ ];
57
+
58
+ function bash() {
59
+ const lines = [];
60
+ lines.push('# shmakk bash completion');
61
+ lines.push('_shmakk_completion() {');
62
+ lines.push(' local cur prev words cword');
63
+ lines.push(' _init_completion || return');
64
+ lines.push('');
65
+ lines.push(' case $prev in');
66
+
67
+ for (const f of FLAGS) {
68
+ if (!f.arg) continue;
69
+ // flags that take an arg
70
+ lines.push(` ${f.flag})`);
71
+ if (f.flag === '--profile' || f.flag === '--profile-set') {
72
+ lines.push(' COMPREPLY=($(compgen -W "tiny balanced deep builder large-app" -- "$cur"))');
73
+ lines.push(' return');
74
+ } else if (f.flag === '--completion') {
75
+ lines.push(' COMPREPLY=($(compgen -W "bash zsh fish" -- "$cur"))');
76
+ lines.push(' return');
77
+ } else if (f.flag === '--colors') {
78
+ lines.push(' COMPREPLY=($(compgen -W "true false" -- "$cur"))');
79
+ lines.push(' return');
80
+ } else {
81
+ lines.push(' COMPREPLY=()');
82
+ lines.push(' return');
83
+ }
84
+ lines.push(' ;;');
85
+ }
86
+
87
+ lines.push(' esac');
88
+ lines.push('');
89
+ lines.push(' # complete flags');
90
+ const flagNames = FLAGS.map((f) => f.flag).join(' ');
91
+ lines.push(` COMPREPLY=($(compgen -W "${flagNames}" -- "$cur"))`);
92
+ lines.push('}');
93
+ lines.push('');
94
+ lines.push('complete -F _shmakk_completion shmakk');
95
+ return lines.join('\n') + '\n';
96
+ }
97
+
98
+ function zsh() {
99
+ const lines = [];
100
+ lines.push('#compdef shmakk');
101
+ lines.push('');
102
+ lines.push('_shmakk() {');
103
+ lines.push(' local -a flags_bool flags_arg');
104
+ lines.push('');
105
+
106
+ const bools = FLAGS.filter((f) => !f.arg).map((f) => f.flag);
107
+ lines.push(` flags_bool=(${bools.map((f) => `"${f}"`).join(' ')})`);
108
+
109
+ const withArgs = FLAGS.filter((f) => f.arg);
110
+ lines.push(` flags_arg=(${withArgs.map((f) => `"${f.flag}"`).join(' ')})`);
111
+
112
+ lines.push('');
113
+ lines.push(' _arguments -s \\');
114
+ for (const f of FLAGS) {
115
+ if (!f.arg) {
116
+ lines.push(` "${f.flag}[${f.desc}]" \\`);
117
+ } else if (f.flag === '--profile' || f.flag === '--profile-set') {
118
+ lines.push(` "${f.flag}[${f.desc}]:profile:(tiny balanced deep builder large-app)" \\`);
119
+ } else if (f.flag === '--completion') {
120
+ lines.push(` "${f.flag}[${f.desc}]:shell:(bash zsh fish)" \\`);
121
+ } else if (f.flag === '--colors') {
122
+ lines.push(` "${f.flag}[${f.desc}]:value:(true false)" \\`);
123
+ } else {
124
+ lines.push(` "${f.flag}[${f.desc}]: :" \\`);
125
+ }
126
+ }
127
+
128
+ lines.push(' && return 0');
129
+ lines.push('}');
130
+ lines.push('');
131
+ lines.push('_shmakk');
132
+ return lines.join('\n') + '\n';
133
+ }
134
+
135
+ function fish() {
136
+ const lines = [];
137
+ lines.push('# shmakk fish completion');
138
+ lines.push('');
139
+
140
+ for (const f of FLAGS) {
141
+ if (!f.arg) {
142
+ lines.push(`complete -c shmakk -l ${f.flag.slice(2)} -d '${f.desc}'`);
143
+ } else if (f.flag === '--profile' || f.flag === '--profile-set') {
144
+ lines.push(`complete -c shmakk -l ${f.flag.slice(2)} -d '${f.desc}' -xa 'tiny balanced deep builder large-app'`);
145
+ } else if (f.flag === '--completion') {
146
+ lines.push(`complete -c shmakk -l ${f.flag.slice(2)} -d '${f.desc}' -xa 'bash zsh fish'`);
147
+ } else if (f.flag === '--colors') {
148
+ lines.push(`complete -c shmakk -l ${f.flag.slice(2)} -d '${f.desc}' -xa 'true false'`);
149
+ } else {
150
+ // arg but no specific completions
151
+ lines.push(`complete -c shmakk -l ${f.flag.slice(2)} -d '${f.desc}' -r`);
152
+ }
153
+ }
154
+
155
+ return lines.join('\n') + '\n';
156
+ }
157
+
158
+ function generate(shell) {
159
+ switch (shell) {
160
+ case 'bash': return bash();
161
+ case 'zsh': return zsh();
162
+ case 'fish': return fish();
163
+ default: throw new Error(`unknown shell: ${shell}. Use: bash, zsh, fish`);
164
+ }
165
+ }
166
+
167
+ module.exports = { generate, FLAGS };
package/src/control.js ADDED
@@ -0,0 +1,250 @@
1
+ // Control commands run from *inside* an shmakk session (the orchestrator
2
+ // puts its own PID in SHMAKK_PID for the child shell's environment).
3
+
4
+ function getParentPid() {
5
+ const pid = parseInt(process.env.SHMAKK_PID || '0', 10);
6
+ return pid > 0 ? pid : 0;
7
+ }
8
+
9
+ function profileSignalPath(pid) {
10
+ return `/tmp/shmakk-profile-${pid}.txt`;
11
+ }
12
+
13
+ function taskJournalPath(cwd = process.cwd()) {
14
+ return require('path').join(cwd, '.shmakk', 'state', 'task-journal.json');
15
+ }
16
+
17
+ function activeSkillMetaPath(cwd = process.cwd()) {
18
+ return require('path').join(cwd, '.shmakk', 'state', 'active-skill.json');
19
+ }
20
+
21
+ function isAlive(pid) {
22
+ if (!pid) return false;
23
+ try { process.kill(pid, 0); return true; } catch { return false; }
24
+ }
25
+
26
+ function status() {
27
+ const pid = getParentPid();
28
+ if (!pid) {
29
+ process.stdout.write('shmakk: not running (this terminal is not inside shmakk)\n');
30
+ return 1;
31
+ }
32
+ if (!isAlive(pid)) {
33
+ process.stdout.write(`shmakk: stale SHMAKK_PID=${pid} (parent not alive)\n`);
34
+ return 2;
35
+ }
36
+ process.stdout.write(`shmakk: running, parent pid ${pid}\n`);
37
+ return 0;
38
+ }
39
+
40
+ function exitParent() {
41
+ const pid = getParentPid();
42
+ if (!pid || !isAlive(pid)) {
43
+ process.stderr.write('shmakk --exit: not inside an shmakk session\n');
44
+ return 1;
45
+ }
46
+ try { process.kill(pid, 'SIGTERM'); } catch (e) {
47
+ process.stderr.write(`shmakk --exit: ${e.message}\n`); return 1;
48
+ }
49
+ return 0;
50
+ }
51
+
52
+ function restartParent() {
53
+ const pid = getParentPid();
54
+ if (!pid || !isAlive(pid)) {
55
+ process.stderr.write('shmakk --restart: not inside an shmakk session\n');
56
+ return 1;
57
+ }
58
+ try { process.kill(pid, 'SIGUSR1'); } catch (e) {
59
+ process.stderr.write(`shmakk --restart: ${e.message}\n`); return 1;
60
+ }
61
+ return 0;
62
+ }
63
+
64
+ function resetConversation() {
65
+ const pid = getParentPid();
66
+ if (!pid || !isAlive(pid)) {
67
+ process.stderr.write('shmakk --reset: not inside an shmakk session\n');
68
+ return 1;
69
+ }
70
+ try { process.kill(pid, 'SIGUSR2'); } catch (e) {
71
+ process.stderr.write(`shmakk --reset: ${e.message}\n`); return 1;
72
+ }
73
+ return 0;
74
+ }
75
+
76
+ function setProfileAndRestart(profileName) {
77
+ const pid = getParentPid();
78
+ if (!pid || !isAlive(pid)) {
79
+ process.stderr.write('shmakk --profile-set: not inside an shmakk session\n');
80
+ return 1;
81
+ }
82
+ const name = String(profileName || '').trim().toLowerCase();
83
+ if (!name) {
84
+ process.stderr.write('shmakk --profile-set: missing profile name\n');
85
+ return 1;
86
+ }
87
+ try {
88
+ require('fs').writeFileSync(profileSignalPath(pid), name + '\n', 'utf8');
89
+ process.kill(pid, 'SIGUSR1');
90
+ } catch (e) {
91
+ process.stderr.write(`shmakk --profile-set: ${e.message}\n`);
92
+ return 1;
93
+ }
94
+ return 0;
95
+ }
96
+
97
+ function resumeStatus() {
98
+ const p = taskJournalPath();
99
+ try {
100
+ const fs = require('fs');
101
+ if (!fs.existsSync(p)) {
102
+ process.stdout.write('shmakk: no resume journal found\n');
103
+ return 0;
104
+ }
105
+ const j = JSON.parse(fs.readFileSync(p, 'utf8'));
106
+ process.stdout.write('shmakk resume status\n');
107
+ process.stdout.write('--------------------\n');
108
+ process.stdout.write(`status: ${j.status || 'unknown'}\n`);
109
+ process.stdout.write(`profile: ${j.profile || 'unknown'}\n`);
110
+ process.stdout.write(`updated: ${j.updatedAt || 'unknown'}\n`);
111
+ process.stdout.write(`input: ${String(j.input || '').slice(0, 120)}\n`);
112
+ process.stdout.write(`touched_files: ${Array.isArray(j.touchedFiles) ? j.touchedFiles.length : 0}\n`);
113
+ return 0;
114
+ } catch (e) {
115
+ process.stderr.write(`shmakk --resume-status: ${e.message}\n`);
116
+ return 1;
117
+ }
118
+ }
119
+
120
+ function compactContext() {
121
+ const pid = getParentPid();
122
+ if (pid && isAlive(pid)) {
123
+ try { process.kill(pid, 'SIGUSR2'); } catch (e) {
124
+ process.stderr.write(`shmakk --compact: ${e.message}\n`);
125
+ return 1;
126
+ }
127
+ process.stdout.write('shmakk: compact requested (conversation + task journal cleared)\n');
128
+ return 0;
129
+ }
130
+
131
+ try {
132
+ const fs = require('fs');
133
+ fs.rmSync(taskJournalPath(), { force: true });
134
+ process.stdout.write('shmakk: compacted local task journal (no active session)\n');
135
+ return 0;
136
+ } catch (e) {
137
+ process.stderr.write(`shmakk --compact: ${e.message}\n`);
138
+ return 1;
139
+ }
140
+ }
141
+
142
+ function stats() {
143
+ const fs = require('fs');
144
+ const audit = require('./audit');
145
+ const pid = getParentPid();
146
+ const running = !!(pid && isAlive(pid));
147
+ let journal = null;
148
+ let activeSkill = null;
149
+ let auditLines = 0;
150
+
151
+ try {
152
+ const p = taskJournalPath();
153
+ if (fs.existsSync(p)) journal = JSON.parse(fs.readFileSync(p, 'utf8'));
154
+ } catch {}
155
+ try {
156
+ const p = activeSkillMetaPath();
157
+ if (fs.existsSync(p)) activeSkill = JSON.parse(fs.readFileSync(p, 'utf8'));
158
+ } catch {}
159
+ try {
160
+ const p = audit.logPath();
161
+ if (fs.existsSync(p)) auditLines = fs.readFileSync(p, 'utf8').split(/\r?\n/).filter(Boolean).length;
162
+ } catch {}
163
+
164
+ process.stdout.write('shmakk stats\n');
165
+ process.stdout.write('-----------\n');
166
+ process.stdout.write(`session_running: ${running ? 'yes' : 'no'}\n`);
167
+ process.stdout.write(`session_pid: ${running ? pid : 'n/a'}\n`);
168
+ process.stdout.write(`resume_status: ${journal?.status || 'none'}\n`);
169
+ process.stdout.write(`resume_updated: ${journal?.updatedAt || 'n/a'}\n`);
170
+ process.stdout.write(`resume_touched_files: ${Array.isArray(journal?.touchedFiles) ? journal.touchedFiles.length : 0}\n`);
171
+ process.stdout.write(`profile: ${journal?.profile || 'n/a'}\n`);
172
+ process.stdout.write(`active_skill: ${activeSkill?.name || 'none'}\n`);
173
+ process.stdout.write(`active_skill_loaded_at: ${activeSkill?.loadedAt || 'n/a'}\n`);
174
+ process.stdout.write(`audit_events_total: ${auditLines}\n`);
175
+ process.stdout.write('token_stats: unavailable (provider usage streaming not persisted yet)\n');
176
+ return 0;
177
+ }
178
+
179
+ function loadSkill(name) {
180
+ const { loadSkillToWorkspace } = require('./skills');
181
+ const res = loadSkillToWorkspace(name, process.cwd());
182
+ if (!res.ok) {
183
+ process.stderr.write(`shmakk --load-skill: ${res.error}\n`);
184
+ if (res.searched) process.stderr.write(`searched:\n- ${res.searched.join('\n- ')}\n`);
185
+ return 1;
186
+ }
187
+ process.stdout.write(`shmakk: loaded skill '${res.name}'\n`);
188
+ process.stdout.write(`source: ${res.source}\n`);
189
+ process.stdout.write(`local: ${res.localPath}\n`);
190
+ return 0;
191
+ }
192
+
193
+ function listSkills() {
194
+ const { listSkills: ls } = require('./skills');
195
+ const all = ls(process.cwd());
196
+ if (!all.length) {
197
+ process.stdout.write('shmakk: no skills loaded in registry\n');
198
+ return 0;
199
+ }
200
+ process.stdout.write('shmakk skills\n');
201
+ process.stdout.write('------------\n');
202
+ for (const s of all) {
203
+ process.stdout.write(`- ${s.name}${s.version ? ` v${s.version}` : ''}${s.active ? ' [active]' : ''}\n`);
204
+ }
205
+ return 0;
206
+ }
207
+
208
+ function skillStatus() {
209
+ const { skillStatus: ss } = require('./skills');
210
+ const st = ss(process.cwd());
211
+ process.stdout.write('shmakk skill status\n');
212
+ process.stdout.write('------------------\n');
213
+ process.stdout.write(`total: ${st.total}\n`);
214
+ if (!st.active) {
215
+ process.stdout.write('active: none\n');
216
+ return 0;
217
+ }
218
+ process.stdout.write(`active: ${st.active.name}\n`);
219
+ process.stdout.write(`version: ${st.active.version}\n`);
220
+ process.stdout.write(`loaded_at: ${st.active.loadedAt || 'n/a'}\n`);
221
+ process.stdout.write(`bytes: ${st.active.bytes || 0}\n`);
222
+ process.stdout.write(`source: ${st.active.source || 'n/a'}\n`);
223
+ return 0;
224
+ }
225
+
226
+ function unloadSkill(name) {
227
+ const { unloadSkill: us } = require('./skills');
228
+ const res = us(name, process.cwd());
229
+ if (!res.ok) {
230
+ process.stderr.write(`shmakk --unload-skill: ${res.error}\n`);
231
+ return 1;
232
+ }
233
+ process.stdout.write(`shmakk: unloaded skill '${res.name}'\n`);
234
+ return 0;
235
+ }
236
+
237
+ async function installSkill(url) {
238
+ const { installSkillFromUrl } = require('./skills');
239
+ const res = await installSkillFromUrl(url, process.cwd());
240
+ if (!res.ok) {
241
+ process.stderr.write(`shmakk --install-skill: ${res.error}\n`);
242
+ return 1;
243
+ }
244
+ process.stdout.write(`shmakk: installed + loaded skill '${res.name}'\n`);
245
+ process.stdout.write(`source: ${res.source}\n`);
246
+ process.stdout.write(`local: ${res.localPath}\n`);
247
+ return 0;
248
+ }
249
+
250
+ module.exports = { status, exitParent, restartParent, resetConversation, setProfileAndRestart, profileSignalPath, resumeStatus, compactContext, stats, loadSkill, listSkills, skillStatus, unloadSkill, installSkill };
@@ -0,0 +1,159 @@
1
+ // Deterministic command correction with frequency-weighted distance matching.
2
+ // No LLM, no API calls, sub-millisecond.
3
+ //
4
+ // Only fires when a shell command exits with non-zero.
5
+ // First token matched against glossary commands; subsequent tokens matched
6
+ // against the corrected command's subcommands. Ties broken by usage frequency
7
+ // from the user's shell history.
8
+
9
+ const { loadFreqMap } = require('./history-parser');
10
+
11
+ function levenshtein(a, b) {
12
+ if (a === b) return 0;
13
+ const m = a.length, n = b.length;
14
+ if (!m) return n; if (!n) return m;
15
+ const dp = new Array(n + 1);
16
+ for (let j = 0; j <= n; j++) dp[j] = j;
17
+ for (let i = 1; i <= m; i++) {
18
+ let prev = dp[0]; dp[0] = i;
19
+ for (let j = 1; j <= n; j++) {
20
+ const tmp = dp[j];
21
+ dp[j] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j], dp[j - 1]);
22
+ prev = tmp;
23
+ }
24
+ }
25
+ return dp[n];
26
+ }
27
+
28
+ // Natural-language pre-filter. If the input reads like a sentence or question,
29
+ // skip correction entirely and route to the task agent.
30
+ const NL_WORDS = new RegExp(
31
+ '\\b(' +
32
+ 'I|me|my|you|your|the|this|that|these|those|a|an|is|are|was|were|do|does|did|' +
33
+ 'can|could|would|should|please|why|what|how|where|when|who|which|' +
34
+ 'fix|tell|show|explain|help|find|look|check|run|make|build|install|create|update|' +
35
+ 'add|remove|delete|change|setup|set\\s+up|debug' +
36
+ ')\\b',
37
+ 'i'
38
+ );
39
+
40
+ function looksLikeNaturalLanguage(input) {
41
+ if (!input) return false;
42
+ const trimmed = input.trim();
43
+ if (trimmed.includes('?')) return true;
44
+ const tokens = trimmed.split(/\s+/);
45
+ if (tokens.length > 5) return true;
46
+ if (tokens.length > 2 && NL_WORDS.test(trimmed)) return true;
47
+ return false;
48
+ }
49
+
50
+ // Score candidates: lower distance = better, higher frequency = better.
51
+ // Returns the best match or null if none are close enough.
52
+ // Threshold scales with word length to avoid false positives on short tokens.
53
+ function bestMatch(word, candidates, freqMap) {
54
+ if (!candidates || !candidates.length) return null;
55
+ if (candidates.includes(word)) return word; // exact match
56
+
57
+ const wlen = word.length;
58
+ // Max distance scales with word length to catch transpositions on short
59
+ // words (e.g. gti→git dist 2) while avoiding false matches on 1-char tokens.
60
+ // wlen=1 → maxDist=1, wlen=3 → maxDist=2, wlen=5+ → maxDist=3
61
+ const maxDist = Math.max(1, Math.min(3, Math.floor(wlen / 2) + 1));
62
+
63
+ const scored = candidates.map((c) => ({
64
+ name: c,
65
+ dist: levenshtein(word.toLowerCase(), c.toLowerCase()),
66
+ freq: freqMap[c] || 0,
67
+ }));
68
+
69
+ // Filter: only keep within distance threshold
70
+ const withinThreshold = scored.filter((s) => s.dist <= maxDist && s.dist > 0);
71
+ if (!withinThreshold.length) return null;
72
+
73
+ // Sort: distance ASC, then frequency DESC, then alphabetically for stability
74
+ withinThreshold.sort((a, b) => {
75
+ if (a.dist !== b.dist) return a.dist - b.dist;
76
+ if (b.freq !== a.freq) return b.freq - a.freq;
77
+ return a.name.localeCompare(b.name);
78
+ });
79
+
80
+ return withinThreshold[0].name;
81
+ }
82
+
83
+ // Should a token be left as-is? (flags, paths, shell expansions, etc.)
84
+ function isStaticToken(tok) {
85
+ return tok === '.'
86
+ || tok === '..'
87
+ || tok.startsWith('-')
88
+ || tok.startsWith('$')
89
+ || tok.startsWith('/')
90
+ || tok.startsWith('~')
91
+ || tok.startsWith('--');
92
+ }
93
+
94
+ async function correct({ input, glossary, signal: _unused }) {
95
+ // Pre-filter natural language
96
+ if (looksLikeNaturalLanguage(input)) {
97
+ return { category: 'not_a_correction', proposed: null, safety: 'uncertain', reason: 'looks like natural language' };
98
+ }
99
+
100
+ // No glossary? Can't correct anything.
101
+ if (!glossary || !glossary.commands) {
102
+ return { category: 'not_a_correction', proposed: null, safety: 'uncertain', reason: 'no glossary available' };
103
+ }
104
+
105
+ const freqMap = loadFreqMap();
106
+ const tokens = input.trim().split(/\s+/);
107
+ if (!tokens.length) {
108
+ return { category: 'not_a_correction', proposed: null, safety: 'uncertain', reason: 'empty input' };
109
+ }
110
+
111
+ // ── Token 0: correct the command name ──
112
+ const cmd = tokens[0];
113
+ const allCommandNames = Object.keys(glossary.commands);
114
+ const correctedCmd = bestMatch(cmd, allCommandNames, freqMap);
115
+
116
+ // No close match for the command — pass through to task agent
117
+ if (!correctedCmd) {
118
+ return { category: 'not_a_correction', proposed: null, safety: 'uncertain', reason: 'no close command match' };
119
+ }
120
+
121
+ const fixedTokens = [correctedCmd];
122
+ const cmdEntry = glossary.commands[correctedCmd];
123
+ const subcommands = cmdEntry?.subcommands || [];
124
+
125
+ // ── Tokens 1+: correct subcommands and known arguments ──
126
+ for (let i = 1; i < tokens.length; i++) {
127
+ const tok = tokens[i];
128
+ if (isStaticToken(tok)) {
129
+ fixedTokens.push(tok);
130
+ continue;
131
+ }
132
+ // Already a known subcommand? Keep it.
133
+ if (subcommands.includes(tok)) {
134
+ fixedTokens.push(tok);
135
+ continue;
136
+ }
137
+ // Try to match against subcommands
138
+ const bestSub = bestMatch(tok, subcommands, freqMap);
139
+ if (bestSub) {
140
+ fixedTokens.push(bestSub);
141
+ } else {
142
+ fixedTokens.push(tok); // keep original
143
+ }
144
+ }
145
+
146
+ const proposed = fixedTokens.join(' ');
147
+ if (proposed === input.trim()) {
148
+ return { category: 'not_a_correction', proposed: null, safety: 'uncertain', reason: 'no correction needed' };
149
+ }
150
+
151
+ return {
152
+ category: 'command_correction',
153
+ proposed,
154
+ safety: 'safe',
155
+ reason: '',
156
+ };
157
+ }
158
+
159
+ module.exports = { correct, looksLikeNaturalLanguage };
@@ -0,0 +1,52 @@
1
+ // Named endpoint presets. Loads .shmakk/endpoints.json from the workspace
2
+ // root (or the nearest ancestor) and applies the selected preset by setting
3
+ // process.env.SHMAKK_* variables before any other module reads them.
4
+ //
5
+ // Format (.shmakk/endpoints.json):
6
+ // {
7
+ // "makkorch": {
8
+ // "base_url": "https://api.example.com/v1",
9
+ // "api_key": "sk-...",
10
+ // "model": "gpt-4o-mini",
11
+ // "headers": "x-custom=value"
12
+ // }
13
+ // }
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ function configPath(cwd) {
19
+ // Look in the workspace root (usually cwd). The endpoint config is
20
+ // a user-local file and doesn't need ancestor traversal like state.
21
+ return path.join(cwd, '.shmakk', 'endpoints.json');
22
+ }
23
+
24
+ function loadEndpoints(cwd) {
25
+ try {
26
+ const raw = fs.readFileSync(configPath(cwd || process.cwd()), 'utf8');
27
+ return JSON.parse(raw);
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ function applyEndpoint(name, cwd) {
34
+ const endpoints = loadEndpoints(cwd);
35
+ if (!endpoints || !endpoints[name]) return false;
36
+
37
+ const cfg = endpoints[name];
38
+ if (cfg.base_url) process.env.SHMAKK_BASE_URL = cfg.base_url;
39
+ if (cfg.api_key) process.env.SHMAKK_API_KEY = cfg.api_key;
40
+ if (cfg.model) process.env.SHMAKK_MODEL = cfg.model;
41
+ if (cfg.headers) process.env.SHMAKK_HEADERS = cfg.headers;
42
+
43
+ return true;
44
+ }
45
+
46
+ function listEndpoints(cwd) {
47
+ const endpoints = loadEndpoints(cwd);
48
+ if (!endpoints) return [];
49
+ return Object.keys(endpoints);
50
+ }
51
+
52
+ module.exports = { applyEndpoint, listEndpoints };
@@ -0,0 +1,33 @@
1
+ const { execSync } = require('child_process');
2
+
3
+ function run(cmd) {
4
+ try { return execSync(cmd, { encoding: 'utf8' }).trim(); }
5
+ catch (e) { return (e.stdout || e.message || '').toString().trim(); }
6
+ }
7
+
8
+ const prefix = run('npm config get prefix');
9
+ const root = run('npm root -g');
10
+ const whichShmakk = run('which shmakk || true');
11
+ const pathValue = process.env.PATH || '';
12
+
13
+ console.log('Global doctor');
14
+ console.log('-------------');
15
+ console.log('npm prefix :', prefix);
16
+ console.log('npm root :', root);
17
+ console.log('which shmakk:', whichShmakk || '(not found)');
18
+
19
+ if (!whichShmakk) {
20
+ console.log('\n`shmakk` is not on PATH.');
21
+ console.log('Run: npm run global:setup');
22
+ console.log('Then open a new terminal and run:');
23
+ console.log(' shmakk --help');
24
+ }
25
+
26
+ if (whichShmakk) {
27
+ console.log('\nLooks good. Try:');
28
+ console.log(' shmakk --help');
29
+ }
30
+
31
+ if (!pathValue.includes('/bin')) {
32
+ console.log('\nPATH looks unusual; verify your shell init files.');
33
+ }