shmakk 1.2.3 → 1.2.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/.env.example +11 -0
- package/README.md +75 -1
- package/docs/index.html +154 -16
- package/docs/mcp.md +78 -0
- package/docs/ssh.md +82 -0
- package/docs/vibedit-analysis.md +375 -0
- package/docs/vim.md +110 -0
- package/docs/voice.md +4 -0
- package/package.json +9 -5
- package/scripts/test-vibedit.js +45 -0
- package/scripts/vibedit-demo.sh +52 -0
- package/skills/shmakk-skill-creator.md +269 -0
- package/src/_check.js +7 -0
- package/src/_check_schema.js +5 -0
- package/src/_cleanup.js +18 -0
- package/src/_fix.js +9 -0
- package/src/_test_import.js +15 -0
- package/src/agent.js +11 -4
- package/src/browser-daemon.js +209 -0
- package/src/browser.js +10 -0
- package/src/cli/browserDaemon.js +60 -0
- package/src/cli/connectBrowser.js +137 -0
- package/src/cli.js +235 -8
- package/src/completions.js +8 -0
- package/src/control.js +273 -1
- package/src/core/browserConnector.js +523 -0
- package/src/correction.js +6 -0
- package/src/electron.js +305 -0
- package/src/endpoints.js +74 -9
- package/src/index.js +24 -1
- package/src/llm.js +501 -61
- package/src/mobile.js +307 -0
- package/src/notify.js +51 -3
- package/src/orchestrator.js +35 -1
- package/src/pty.js +11 -6
- package/src/review.js +45 -11
- package/src/self-commands.js +153 -0
- package/src/session-convert.js +508 -0
- package/src/session-search.js +31 -0
- package/src/session.js +392 -46
- package/src/skills/browserActions.ts +984 -0
- package/src/skills.js +451 -24
- package/src/system-prompt.js +31 -25
- package/src/tools.js +81 -0
- package/src/vibedit/control.js +534 -0
- package/src/vibedit/electron.js +108 -0
- package/src/vibedit/files.js +171 -0
- package/src/vibedit/index.js +298 -0
- package/src/vibedit/overlay.js +1482 -0
- package/src/vibedit/prompts.js +245 -0
- package/src/vibedit/state.js +32 -0
- package/src/vim.js +410 -0
package/src/vim.js
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { spawnSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
function binPath() {
|
|
7
|
+
return path.resolve(__dirname, '..', 'bin', 'shmakk.js');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function splitPath(value) {
|
|
11
|
+
return String(value || '').split(path.delimiter).filter(Boolean);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function withoutDir(pathValue, dir) {
|
|
15
|
+
const resolved = path.resolve(dir);
|
|
16
|
+
return splitPath(pathValue).filter((p) => {
|
|
17
|
+
try { return path.resolve(p) !== resolved; } catch { return true; }
|
|
18
|
+
}).join(path.delimiter);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function findExecutable(name, pathValue = process.env.PATH) {
|
|
22
|
+
for (const dir of splitPath(pathValue)) {
|
|
23
|
+
const candidate = path.join(dir, name);
|
|
24
|
+
try {
|
|
25
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
26
|
+
return candidate;
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function prepareVimEnvironment(mode = 'vim') {
|
|
33
|
+
if (mode === 'disable') return { env: {}, cleanup: () => {} };
|
|
34
|
+
const command = mode === 'vi' ? 'vi' : 'vim';
|
|
35
|
+
const currentPath = process.env.PATH || '';
|
|
36
|
+
const real = findExecutable(command, currentPath);
|
|
37
|
+
if (!real) {
|
|
38
|
+
process.stderr.write(`[shmakk] warning: --vim ${command} requested, but ${command} was not found in PATH\n`);
|
|
39
|
+
return { env: {}, cleanup: () => {} };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'shmakk-vim-'));
|
|
43
|
+
const wrapper = path.join(dir, command);
|
|
44
|
+
const script = [
|
|
45
|
+
'#!/usr/bin/env sh',
|
|
46
|
+
`exec "${process.execPath}" "${binPath()}" --vim-editor "${command}" --vim-real "${real}" -- "$@"`,
|
|
47
|
+
'',
|
|
48
|
+
].join('\n');
|
|
49
|
+
fs.writeFileSync(wrapper, script, { mode: 0o755 });
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
env: {
|
|
53
|
+
SHMAKK_REAL_PATH: currentPath,
|
|
54
|
+
SHMAKK_VIM_SHIM_DIR: dir,
|
|
55
|
+
PATH: `${dir}${path.delimiter}${currentPath}`,
|
|
56
|
+
},
|
|
57
|
+
cleanup: () => {
|
|
58
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function writePlugin() {
|
|
64
|
+
const p = path.join(os.tmpdir(), `shmakk-vim-plugin-${process.pid}-${Date.now()}.vim`);
|
|
65
|
+
const node = process.execPath;
|
|
66
|
+
const shmakk = binPath();
|
|
67
|
+
const lines = [];
|
|
68
|
+
lines.push('if exists("g:loaded_shmakk_vim") | finish | endif');
|
|
69
|
+
lines.push('let g:loaded_shmakk_vim = 1');
|
|
70
|
+
lines.push(`let s:shmakk_node = ${JSON.stringify(node)}`);
|
|
71
|
+
lines.push(`let s:shmakk_bin = ${JSON.stringify(shmakk)}`);
|
|
72
|
+
lines.push('');
|
|
73
|
+
lines.push('function! s:Run(mode, payload) abort');
|
|
74
|
+
lines.push(' let out = system([s:shmakk_node, s:shmakk_bin, "--vim-ai", a:mode], json_encode(a:payload))');
|
|
75
|
+
lines.push(' if v:shell_error != 0');
|
|
76
|
+
lines.push(' echohl ErrorMsg | echom "[shmakk] " . out | echohl None');
|
|
77
|
+
lines.push(' return {"ok": v:false, "error": out}');
|
|
78
|
+
lines.push(' endif');
|
|
79
|
+
lines.push(' let decoded = json_decode(out)');
|
|
80
|
+
lines.push(' if type(decoded) != v:t_dict');
|
|
81
|
+
lines.push(' return {"ok": v:false, "error": "invalid shmakk response"}');
|
|
82
|
+
lines.push(' endif');
|
|
83
|
+
lines.push(' return decoded');
|
|
84
|
+
lines.push('endfunction');
|
|
85
|
+
lines.push('');
|
|
86
|
+
lines.push('let s:auto_jobs = {}');
|
|
87
|
+
lines.push('');
|
|
88
|
+
lines.push('function! s:StartAsyncSuggest(payload) abort');
|
|
89
|
+
lines.push(' if !exists("*job_start") | return 0 | endif');
|
|
90
|
+
lines.push(' let in_file = tempname()');
|
|
91
|
+
lines.push(' let out_file = tempname()');
|
|
92
|
+
lines.push(' let err_file = tempname()');
|
|
93
|
+
lines.push(' call writefile([json_encode(a:payload)], in_file)');
|
|
94
|
+
lines.push(' let job = job_start([s:shmakk_node, s:shmakk_bin, "--vim-ai", "suggest"], {"in_io": "file", "in_name": in_file, "out_io": "file", "out_name": out_file, "err_io": "file", "err_name": err_file})');
|
|
95
|
+
lines.push(' if job <= 0');
|
|
96
|
+
lines.push(' call delete(in_file) | call delete(out_file) | call delete(err_file)');
|
|
97
|
+
lines.push(' return 0');
|
|
98
|
+
lines.push(' endif');
|
|
99
|
+
lines.push(' let s:auto_jobs[job] = {"out": out_file, "err": err_file, "in": in_file, "buf": bufnr("%"), "line": line("."), "col": col(".") }');
|
|
100
|
+
lines.push(' call timer_start(500, function("s:PollAsyncSuggest", [job]))');
|
|
101
|
+
lines.push(' return 1');
|
|
102
|
+
lines.push('endfunction');
|
|
103
|
+
lines.push('');
|
|
104
|
+
lines.push('function! s:PollAsyncSuggest(job, timer) abort');
|
|
105
|
+
lines.push(' if !has_key(s:auto_jobs, a:job) | return | endif');
|
|
106
|
+
lines.push(' if job_status(a:job) ==# "run"');
|
|
107
|
+
lines.push(' call timer_start(500, function("s:PollAsyncSuggest", [a:job]))');
|
|
108
|
+
lines.push(' return');
|
|
109
|
+
lines.push(' endif');
|
|
110
|
+
lines.push(' let meta = remove(s:auto_jobs, a:job)');
|
|
111
|
+
lines.push(' let raw = join(readfile(meta.out), "\\n")');
|
|
112
|
+
lines.push(' let err = join(readfile(meta.err), "\\n")');
|
|
113
|
+
lines.push(' call delete(meta.in) | call delete(meta.out) | call delete(meta.err)');
|
|
114
|
+
lines.push(' if bufnr("%") != meta.buf | return | endif');
|
|
115
|
+
lines.push(' let decoded = json_decode(raw)');
|
|
116
|
+
lines.push(' if type(decoded) != v:t_dict || !get(decoded, "ok", v:false)');
|
|
117
|
+
lines.push(' if err !=# "" | echom "[shmakk] auto-suggest failed: " . err | endif');
|
|
118
|
+
lines.push(' return');
|
|
119
|
+
lines.push(' endif');
|
|
120
|
+
lines.push(' let text = get(decoded, "text", "")');
|
|
121
|
+
lines.push(' if text ==# "" | return | endif');
|
|
122
|
+
lines.push(' let b:shmakk_pending_suggestion = {"text": text, "line": meta.line, "col": meta.col}');
|
|
123
|
+
lines.push(' echo "[shmakk] suggestion ready: :ShmakkAccept, :ShmakkPreview, or :ShmakkDeny"');
|
|
124
|
+
lines.push('endfunction');
|
|
125
|
+
lines.push('');
|
|
126
|
+
lines.push('function! s:Context(prompt) abort');
|
|
127
|
+
lines.push(' return {"file": expand("%:p"), "line": line("."), "col": col("."), "prompt": a:prompt, "buffer": join(getline(1, "$"), "\\n")}');
|
|
128
|
+
lines.push('endfunction');
|
|
129
|
+
lines.push('');
|
|
130
|
+
lines.push('function! s:InsertAtCursor(text) abort');
|
|
131
|
+
lines.push(' let parts = split(a:text, "\\n", v:true)');
|
|
132
|
+
lines.push(' if empty(parts) | return | endif');
|
|
133
|
+
lines.push(' let lnum = line(".")');
|
|
134
|
+
lines.push(' let c = col(".")');
|
|
135
|
+
lines.push(' let cur = getline(lnum)');
|
|
136
|
+
lines.push(' let before = strpart(cur, 0, c - 1)');
|
|
137
|
+
lines.push(' let after = strpart(cur, c - 1)');
|
|
138
|
+
lines.push(' if len(parts) == 1');
|
|
139
|
+
lines.push(' call setline(lnum, before . parts[0] . after)');
|
|
140
|
+
lines.push(' else');
|
|
141
|
+
lines.push(' call setline(lnum, before . parts[0])');
|
|
142
|
+
lines.push(' call append(lnum, parts[1:-2] + [parts[-1] . after])');
|
|
143
|
+
lines.push(' endif');
|
|
144
|
+
lines.push('endfunction');
|
|
145
|
+
lines.push('');
|
|
146
|
+
lines.push('function! ShmakkGenerate(prompt) abort');
|
|
147
|
+
lines.push(' let prompt = a:prompt');
|
|
148
|
+
lines.push(' echo "[shmakk] generating..." | redraw');
|
|
149
|
+
lines.push(' let r = s:Run("generate", s:Context(prompt))');
|
|
150
|
+
lines.push(' if get(r, "ok", v:false)');
|
|
151
|
+
lines.push(' call s:InsertAtCursor(get(r, "text", ""))');
|
|
152
|
+
lines.push(' else');
|
|
153
|
+
lines.push(' echohl ErrorMsg | echom "[shmakk] " . get(r, "error", "generation failed") | echohl None');
|
|
154
|
+
lines.push(' endif');
|
|
155
|
+
lines.push('endfunction');
|
|
156
|
+
lines.push('');
|
|
157
|
+
lines.push('function! ShmakkTypeWriter(prompt) abort');
|
|
158
|
+
lines.push(' let prompt = a:prompt');
|
|
159
|
+
lines.push(' echo "[shmakk] writing..." | redraw');
|
|
160
|
+
lines.push(' let r = s:Run("typewriter", s:Context(prompt))');
|
|
161
|
+
lines.push(' if get(r, "ok", v:false)');
|
|
162
|
+
lines.push(' call s:InsertAtCursor(get(r, "text", ""))');
|
|
163
|
+
lines.push(' else');
|
|
164
|
+
lines.push(' echohl ErrorMsg | echom "[shmakk] " . get(r, "error", "typewriter failed") | echohl None');
|
|
165
|
+
lines.push(' endif');
|
|
166
|
+
lines.push('endfunction');
|
|
167
|
+
lines.push('');
|
|
168
|
+
lines.push('function! ShmakkSuggest() abort');
|
|
169
|
+
lines.push(' echo "[shmakk] suggesting..." | redraw');
|
|
170
|
+
lines.push(' let r = s:Run("suggest", s:Context(""))');
|
|
171
|
+
lines.push(' if !get(r, "ok", v:false)');
|
|
172
|
+
lines.push(' echohl ErrorMsg | echom "[shmakk] " . get(r, "error", "suggestion failed") | echohl None');
|
|
173
|
+
lines.push(' return');
|
|
174
|
+
lines.push(' endif');
|
|
175
|
+
lines.push(' let text = get(r, "text", "")');
|
|
176
|
+
lines.push(' if text ==# "" | return | endif');
|
|
177
|
+
lines.push(' botright new');
|
|
178
|
+
lines.push(' setlocal buftype=nofile bufhidden=wipe noswapfile nobuflisted readonly');
|
|
179
|
+
lines.push(' file [shmakk-suggestion]');
|
|
180
|
+
lines.push(' call setline(1, split(text, "\\n", v:true))');
|
|
181
|
+
lines.push(' normal! gg');
|
|
182
|
+
lines.push(' let choice = confirm("Accept shmakk suggestion?", "&Accept\\n&Deny", 2)');
|
|
183
|
+
lines.push(' bdelete!');
|
|
184
|
+
lines.push(' if choice == 1');
|
|
185
|
+
lines.push(' call s:InsertAtCursor(text)');
|
|
186
|
+
lines.push(' endif');
|
|
187
|
+
lines.push('endfunction');
|
|
188
|
+
lines.push('');
|
|
189
|
+
lines.push('function! ShmakkAccept() abort');
|
|
190
|
+
lines.push(' if !exists("b:shmakk_pending_suggestion")');
|
|
191
|
+
lines.push(' echo "[shmakk] no pending suggestion"');
|
|
192
|
+
lines.push(' return');
|
|
193
|
+
lines.push(' endif');
|
|
194
|
+
lines.push(' let text = b:shmakk_pending_suggestion.text');
|
|
195
|
+
lines.push(' let source_buf = bufnr("%")');
|
|
196
|
+
lines.push(' botright new');
|
|
197
|
+
lines.push(' setlocal buftype=nofile bufhidden=wipe noswapfile nobuflisted readonly');
|
|
198
|
+
lines.push(' file [shmakk-suggestion]');
|
|
199
|
+
lines.push(' call setline(1, split(text, "\\n", v:true))');
|
|
200
|
+
lines.push(' normal! gg');
|
|
201
|
+
lines.push(' let choice = confirm("Accept shmakk suggestion?", "&Accept\\n&Deny", 2)');
|
|
202
|
+
lines.push(' bdelete!');
|
|
203
|
+
lines.push(' if choice == 1');
|
|
204
|
+
lines.push(' if bufnr("%") != source_buf | execute "buffer " . source_buf | endif');
|
|
205
|
+
lines.push(' unlet b:shmakk_pending_suggestion');
|
|
206
|
+
lines.push(' call s:InsertAtCursor(text)');
|
|
207
|
+
lines.push(' endif');
|
|
208
|
+
lines.push('endfunction');
|
|
209
|
+
lines.push('');
|
|
210
|
+
lines.push('function! ShmakkDeny() abort');
|
|
211
|
+
lines.push(' if exists("b:shmakk_pending_suggestion") | unlet b:shmakk_pending_suggestion | endif');
|
|
212
|
+
lines.push(' echo "[shmakk] suggestion cleared"');
|
|
213
|
+
lines.push('endfunction');
|
|
214
|
+
lines.push('');
|
|
215
|
+
lines.push('function! ShmakkPreview() abort');
|
|
216
|
+
lines.push(' if !exists("b:shmakk_pending_suggestion")');
|
|
217
|
+
lines.push(' echo "[shmakk] no pending suggestion"');
|
|
218
|
+
lines.push(' return');
|
|
219
|
+
lines.push(' endif');
|
|
220
|
+
lines.push(' let text = b:shmakk_pending_suggestion.text');
|
|
221
|
+
lines.push(' botright new');
|
|
222
|
+
lines.push(' setlocal buftype=nofile bufhidden=wipe noswapfile nobuflisted readonly');
|
|
223
|
+
lines.push(' file [shmakk-suggestion]');
|
|
224
|
+
lines.push(' call setline(1, split(text, "\\n", v:true))');
|
|
225
|
+
lines.push(' normal! gg');
|
|
226
|
+
lines.push('endfunction');
|
|
227
|
+
lines.push('');
|
|
228
|
+
lines.push('function! s:MaybeAutoSuggest(timer) abort');
|
|
229
|
+
lines.push(' if !get(g:, "shmakk_auto_suggest", 0) | return | endif');
|
|
230
|
+
lines.push(' if mode() !=# "i" || exists("b:shmakk_pending_suggestion") | return | endif');
|
|
231
|
+
lines.push(' let min_chars = get(g:, "shmakk_auto_suggest_min_chars", 20)');
|
|
232
|
+
lines.push(' let before = strpart(getline("."), 0, col(".") - 1)');
|
|
233
|
+
lines.push(' if strlen(before) < min_chars | return | endif');
|
|
234
|
+
lines.push(' echo "[shmakk] auto-suggesting..."');
|
|
235
|
+
lines.push(' call s:StartAsyncSuggest(s:Context(""))');
|
|
236
|
+
lines.push('endfunction');
|
|
237
|
+
lines.push('');
|
|
238
|
+
lines.push('function! s:ScheduleAutoSuggest() abort');
|
|
239
|
+
lines.push(' if !get(g:, "shmakk_auto_suggest", 0) | return | endif');
|
|
240
|
+
lines.push(' if exists("b:shmakk_auto_timer") | call timer_stop(b:shmakk_auto_timer) | endif');
|
|
241
|
+
lines.push(' let b:shmakk_auto_timer = timer_start(get(g:, "shmakk_auto_suggest_delay_ms", 2000), function("s:MaybeAutoSuggest"))');
|
|
242
|
+
lines.push('endfunction');
|
|
243
|
+
lines.push('');
|
|
244
|
+
lines.push('function! ShmakkCommand(cmd) abort');
|
|
245
|
+
lines.push(' let r = s:Run("cmd", {"cmd": a:cmd, "cwd": getcwd()})');
|
|
246
|
+
lines.push(' botright new');
|
|
247
|
+
lines.push(' setlocal buftype=nofile bufhidden=wipe noswapfile nobuflisted');
|
|
248
|
+
lines.push(' file [shmakk-cmd]');
|
|
249
|
+
lines.push(' let lines = ["$ " . a:cmd, "exit " . string(get(r, "code", 1)), ""]');
|
|
250
|
+
lines.push(' if get(r, "stdout", "") != "" | call extend(lines, split(r.stdout, "\\n", v:true)) | endif');
|
|
251
|
+
lines.push(' if get(r, "stderr", "") != "" | call extend(lines, ["", "stderr:"]) | call extend(lines, split(r.stderr, "\\n", v:true)) | endif');
|
|
252
|
+
lines.push(' call setline(1, lines)');
|
|
253
|
+
lines.push(' normal! gg');
|
|
254
|
+
lines.push('endfunction');
|
|
255
|
+
lines.push('');
|
|
256
|
+
lines.push('nnoremap <silent> <C-Space> :call ShmakkSuggest()<CR>');
|
|
257
|
+
lines.push('inoremap <silent> <C-Space> <Esc>:call ShmakkSuggest()<CR>a');
|
|
258
|
+
lines.push('nnoremap <silent> <leader>sa :call ShmakkAccept()<CR>');
|
|
259
|
+
lines.push('nnoremap <silent> <leader>sd :call ShmakkDeny()<CR>');
|
|
260
|
+
lines.push('nnoremap <silent> <leader>sp :call ShmakkPreview()<CR>');
|
|
261
|
+
lines.push('augroup ShmakkVimAI');
|
|
262
|
+
lines.push(' autocmd! * <buffer>');
|
|
263
|
+
lines.push(' autocmd TextChangedI <buffer> call s:ScheduleAutoSuggest()');
|
|
264
|
+
lines.push('augroup END');
|
|
265
|
+
lines.push('command! -nargs=* ShmakkGenerate call ShmakkGenerate(<q-args>)');
|
|
266
|
+
lines.push('command! -nargs=* G call ShmakkGenerate(<q-args>)');
|
|
267
|
+
lines.push('command! -nargs=* ShmakkTypeWriter call ShmakkTypeWriter(<q-args>)');
|
|
268
|
+
lines.push('command! -nargs=* Tw call ShmakkTypeWriter(<q-args>)');
|
|
269
|
+
lines.push('command! -nargs=* ShmakkCommand call ShmakkCommand(<q-args>)');
|
|
270
|
+
lines.push('command! -nargs=* Cmd call ShmakkCommand(<q-args>)');
|
|
271
|
+
lines.push('command! -nargs=0 ShmakkSuggest call ShmakkSuggest()');
|
|
272
|
+
lines.push('command! -nargs=0 ShmakkAccept call ShmakkAccept()');
|
|
273
|
+
lines.push('command! -nargs=0 ShmakkDeny call ShmakkDeny()');
|
|
274
|
+
lines.push('command! -nargs=0 ShmakkPreview call ShmakkPreview()');
|
|
275
|
+
fs.writeFileSync(p, lines.join('\n') + '\n', 'utf8');
|
|
276
|
+
return p;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function runEditor(realEditor, args = []) {
|
|
280
|
+
const script = writePlugin();
|
|
281
|
+
const cleanPath = process.env.SHMAKK_REAL_PATH || withoutDir(process.env.PATH || '', process.env.SHMAKK_VIM_SHIM_DIR || '');
|
|
282
|
+
const env = { ...process.env, PATH: cleanPath };
|
|
283
|
+
const childArgs = [...args, '-S', script];
|
|
284
|
+
const res = spawnSync(realEditor, childArgs, { stdio: 'inherit', env });
|
|
285
|
+
try { fs.rmSync(script, { force: true }); } catch {}
|
|
286
|
+
return res.status ?? (res.signal ? 1 : 0);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function stripFence(text) {
|
|
290
|
+
const s = String(text || '').trim();
|
|
291
|
+
const m = s.match(/^```[^\n]*\n([\s\S]*?)\n```$/);
|
|
292
|
+
return m ? m[1] : s;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function contextWindow(payload, mode) {
|
|
296
|
+
const buffer = String(payload.buffer || '');
|
|
297
|
+
if (mode !== 'suggest') return buffer.slice(0, 30000);
|
|
298
|
+
|
|
299
|
+
const lines = buffer.split('\n');
|
|
300
|
+
const cursorLine = Math.max(1, Number(payload.line) || 1);
|
|
301
|
+
const beforeLines = Math.max(10, Number(process.env.SHMAKK_VIM_SUGGEST_BEFORE_LINES) || 80);
|
|
302
|
+
const afterLines = Math.max(5, Number(process.env.SHMAKK_VIM_SUGGEST_AFTER_LINES) || 40);
|
|
303
|
+
const start = Math.max(0, cursorLine - beforeLines - 1);
|
|
304
|
+
const end = Math.min(lines.length, cursorLine + afterLines);
|
|
305
|
+
let windowText = lines.slice(start, end).join('\n');
|
|
306
|
+
const maxChars = Math.max(2000, Number(process.env.SHMAKK_VIM_SUGGEST_MAX_CHARS) || 12000);
|
|
307
|
+
if (windowText.length > maxChars) {
|
|
308
|
+
windowText = windowText.slice(Math.max(0, windowText.length - maxChars));
|
|
309
|
+
}
|
|
310
|
+
const prefix = start > 0 ? `[Earlier lines omitted: 1-${start}]\n` : '';
|
|
311
|
+
const suffix = end < lines.length ? `\n[Later lines omitted: ${end + 1}-${lines.length}]` : '';
|
|
312
|
+
return prefix + windowText + suffix;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function suggestEndpointName() {
|
|
316
|
+
return process.env.SHMAKK_VIM_SUGGEST_ENDPOINT || process.env.SHMAKK_FAST_ENDPOINT || 'fast';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function callModel(mode, payload) {
|
|
320
|
+
const { isConfigured, makeClient, makeClientForEndpoint, modelFor } = require('./llm');
|
|
321
|
+
if (!isConfigured()) {
|
|
322
|
+
return { ok: false, error: 'LLM is not configured. Set SHMAKK_BASE_URL or an endpoint first.' };
|
|
323
|
+
}
|
|
324
|
+
let client = makeClient('vim');
|
|
325
|
+
let model = modelFor('vim');
|
|
326
|
+
if (mode === 'suggest') {
|
|
327
|
+
const fast = makeClientForEndpoint(suggestEndpointName());
|
|
328
|
+
if (fast) {
|
|
329
|
+
client = fast.client;
|
|
330
|
+
model = fast.model;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const intent = mode === 'typewriter'
|
|
334
|
+
? 'Write prose or documentation at the cursor.'
|
|
335
|
+
: mode === 'suggest'
|
|
336
|
+
? 'Predict the best next code block at the cursor. Prefer complete functions, methods, classes, or cohesive multi-line edits when appropriate.'
|
|
337
|
+
: 'Generate or edit code at the cursor.';
|
|
338
|
+
const messages = [
|
|
339
|
+
{ role: 'system', content: 'You are shmakk inside Vim. Return only the requested text. Do not wrap code in markdown fences unless the user explicitly asks for markdown.' },
|
|
340
|
+
{ role: 'user', content: `${intent}\nFile: ${payload.file || '(unnamed)'}\nCursor: ${payload.line || 1}:${payload.col || 1}\nPrompt/base: ${payload.prompt || ''}\n\nBuffer/context:\n${contextWindow(payload, mode)}` },
|
|
341
|
+
];
|
|
342
|
+
const resp = await client.chat.completions.create({
|
|
343
|
+
model,
|
|
344
|
+
temperature: mode === 'suggest' ? 0.1 : 0.2,
|
|
345
|
+
max_tokens: mode === 'suggest' ? 1800 : 2400,
|
|
346
|
+
messages,
|
|
347
|
+
});
|
|
348
|
+
const text = resp.choices?.[0]?.message?.content || '';
|
|
349
|
+
return { ok: true, text: stripFence(text) };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function readsStdin() {
|
|
353
|
+
return fs.readFileSync(0, 'utf8');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function commandUsesShmakk(cmd) {
|
|
357
|
+
return /(^|[;&|()]\s*)shmakk(\s|$)/.test(String(cmd || ''));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function runShellCommand(cmd, cwd) {
|
|
361
|
+
if (commandUsesShmakk(cmd)) {
|
|
362
|
+
return { ok: false, code: 127, stdout: '', stderr: 'shmakk is not available inside :cmd\n' };
|
|
363
|
+
}
|
|
364
|
+
const shell = process.env.SHELL || '/bin/sh';
|
|
365
|
+
const env = { ...process.env };
|
|
366
|
+
delete env.SHMAKK;
|
|
367
|
+
delete env.SHMAKK_PID;
|
|
368
|
+
delete env.SHMAKK_SESSION_ID;
|
|
369
|
+
env.PATH = env.SHMAKK_REAL_PATH || withoutDir(env.PATH || '', env.SHMAKK_VIM_SHIM_DIR || '');
|
|
370
|
+
const spawnOpts = {
|
|
371
|
+
cwd: cwd || process.cwd(),
|
|
372
|
+
env,
|
|
373
|
+
encoding: 'utf8',
|
|
374
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
375
|
+
};
|
|
376
|
+
let res = spawnSync(shell, ['-lc', String(cmd || '')], spawnOpts);
|
|
377
|
+
let fallbackNote = '';
|
|
378
|
+
if (res.error && shell !== '/bin/sh') {
|
|
379
|
+
fallbackNote = `${res.error.message}; retried with /bin/sh\n`;
|
|
380
|
+
res = spawnSync('/bin/sh', ['-lc', String(cmd || '')], spawnOpts);
|
|
381
|
+
}
|
|
382
|
+
const code = res.error ? 1 : (res.status ?? (res.signal ? 1 : 0));
|
|
383
|
+
return {
|
|
384
|
+
ok: !res.error && code === 0,
|
|
385
|
+
code,
|
|
386
|
+
stdout: res.stdout || '',
|
|
387
|
+
stderr: fallbackNote + (res.stderr || (res.error ? `${res.error.message}\n` : '')),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function runAi(mode) {
|
|
392
|
+
let payload = {};
|
|
393
|
+
try { payload = JSON.parse(readsStdin() || '{}'); } catch (e) {
|
|
394
|
+
process.stdout.write(JSON.stringify({ ok: false, error: `invalid JSON: ${e.message}` }) + '\n');
|
|
395
|
+
return 1;
|
|
396
|
+
}
|
|
397
|
+
const result = mode === 'cmd'
|
|
398
|
+
? runShellCommand(payload.cmd, payload.cwd)
|
|
399
|
+
: await callModel(mode, payload);
|
|
400
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
401
|
+
return result.ok === false ? 1 : 0;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
module.exports = {
|
|
405
|
+
prepareVimEnvironment,
|
|
406
|
+
runEditor,
|
|
407
|
+
runAi,
|
|
408
|
+
commandUsesShmakk,
|
|
409
|
+
_test: { findExecutable, withoutDir, commandUsesShmakk, stripFence, writePlugin, contextWindow, suggestEndpointName },
|
|
410
|
+
};
|