shmakk 1.2.2 → 1.2.4
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/package.json +1 -1
- package/src/agent.js +54 -9
- package/src/cli.js +167 -79
- package/src/correction.js +12 -0
- package/src/endpoints.js +6 -0
- package/src/hooks/bash.js +17 -2
- package/src/hooks/fish.js +21 -2
- package/src/hooks/zsh.js +31 -2
- package/src/index.js +11 -2
- package/src/llm.js +2 -2
- package/src/mcp-client.js +7 -1
- package/src/notify.js +6 -3
- package/src/pty.js +2 -2
- package/src/review.js +3 -3
- package/src/self-commands.js +96 -16
- package/src/session.js +22 -5
- package/src/shell.js +39 -19
- package/src/system-prompt.js +3 -1
- package/src/tools.js +26 -1
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
|
-
const { makeClient, modelFor, isConfigured, getDeepSeekOptions } = require('./llm');
|
|
10
|
+
const { makeClient, modelFor, isConfigured, getDeepSeekOptions, supportsVision } = require('./llm');
|
|
11
11
|
const {
|
|
12
12
|
sanitizeAssistantContent,
|
|
13
13
|
isLeakedToolMarkup,
|
|
@@ -87,13 +87,16 @@ function clearTaskJournal(root) {
|
|
|
87
87
|
const { renderBlock } = require('./markdown');
|
|
88
88
|
|
|
89
89
|
// Tiny spinner so the user sees "the agent is thinking" while we wait on
|
|
90
|
-
// the model. Erased when stop() is called.
|
|
90
|
+
// the model. Erased when stop() is called. Also updates the terminal tab
|
|
91
|
+
// title so you can see agent activity even when looking at another tab.
|
|
91
92
|
function startSpinner(write, label = 'thinking') {
|
|
92
93
|
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
93
94
|
let i = 0; let line = '';
|
|
94
95
|
const draw = () => {
|
|
95
96
|
line = `\x1b[2m${frames[i % frames.length]} ${label}…\x1b[0m`;
|
|
96
97
|
write('\r' + line);
|
|
98
|
+
// Update terminal tab/window title so activity is visible from other tabs
|
|
99
|
+
write(`\x1b]0;${frames[i % frames.length]} ${label} — shmakk\x07`);
|
|
97
100
|
i++;
|
|
98
101
|
};
|
|
99
102
|
draw();
|
|
@@ -101,6 +104,8 @@ function startSpinner(write, label = 'thinking') {
|
|
|
101
104
|
return () => {
|
|
102
105
|
clearInterval(tm);
|
|
103
106
|
write('\r' + ' '.repeat(line.replace(/\x1b\[[0-9;]*m/g, '').length + 2) + '\r\r');
|
|
107
|
+
// Clear the terminal title — shell will restore normal title on next prompt
|
|
108
|
+
write('\x1b]0;\x07');
|
|
104
109
|
};
|
|
105
110
|
}
|
|
106
111
|
|
|
@@ -251,6 +256,7 @@ async function runAgent({ input, roots, glossary, confirmTool, write, signal, hi
|
|
|
251
256
|
mcpToolHint,
|
|
252
257
|
userRulesText,
|
|
253
258
|
userMemoryText,
|
|
259
|
+
supportsVision: supportsVision(),
|
|
254
260
|
});
|
|
255
261
|
|
|
256
262
|
const boundedHistory = trimForContext(history, runtimeProfile.historyEntries);
|
|
@@ -292,14 +298,22 @@ async function runAgent({ input, roots, glossary, confirmTool, write, signal, hi
|
|
|
292
298
|
// Tool loop. Streams content as it arrives; prints each tool call.
|
|
293
299
|
let producedAnything = false;
|
|
294
300
|
const runState = { _dsmlLeakRetries: 0 };
|
|
301
|
+
let taskSpinnerStop = null;
|
|
302
|
+
const ensureSpinner = () => {
|
|
303
|
+
if (!taskSpinnerStop) taskSpinnerStop = startSpinner(write, 'thinking');
|
|
304
|
+
};
|
|
305
|
+
const stopTaskSpinner = () => {
|
|
306
|
+
if (taskSpinnerStop) { taskSpinnerStop(); taskSpinnerStop = null; }
|
|
307
|
+
};
|
|
295
308
|
for (let i = 0; i < dynamicToolBudget; i++) {
|
|
296
|
-
if (signal && signal.aborted) return messages.slice(1);
|
|
309
|
+
if (signal && signal.aborted) { stopTaskSpinner(); return messages.slice(1); }
|
|
297
310
|
|
|
298
311
|
// Stream the response so the user sees text as it generates.
|
|
299
312
|
const cacheKey = promptCacheEnabled ? promptCache.makeKey({ model: modelFor('agent'), messages, toolChoice: 'auto' }) : null;
|
|
300
313
|
if (promptCacheEnabled && cacheKey) {
|
|
301
314
|
const hit = promptCache.get(roots[0], cacheKey);
|
|
302
315
|
if (hit && hit.content) {
|
|
316
|
+
stopTaskSpinner();
|
|
303
317
|
write(dim('[shmakk] prompt cache hit', colors) + '\n');
|
|
304
318
|
write(highlightCodeBlocks(hit.content, colors));
|
|
305
319
|
if (!hit.content.endsWith('\n')) write('\n');
|
|
@@ -309,7 +323,7 @@ async function runAgent({ input, roots, glossary, confirmTool, write, signal, hi
|
|
|
309
323
|
}
|
|
310
324
|
}
|
|
311
325
|
|
|
312
|
-
|
|
326
|
+
ensureSpinner();
|
|
313
327
|
const allTools = mcpManager ? [...TOOLS, ...mcpManager.getToolDefinitions()] : TOOLS;
|
|
314
328
|
let stream;
|
|
315
329
|
try {
|
|
@@ -324,14 +338,13 @@ async function runAgent({ input, roots, glossary, confirmTool, write, signal, hi
|
|
|
324
338
|
...getDeepSeekOptions('tool_loop'),
|
|
325
339
|
}, { signal });
|
|
326
340
|
} catch (e) {
|
|
327
|
-
|
|
341
|
+
stopTaskSpinner();
|
|
328
342
|
throw e;
|
|
329
343
|
}
|
|
330
344
|
|
|
331
345
|
let content = '';
|
|
332
346
|
let reasoningContent = '';
|
|
333
347
|
const toolCalls = []; // [{id, type:'function', function:{name, arguments}}]
|
|
334
|
-
let spinnerStopped = false;
|
|
335
348
|
let streamingContentOk = true; // flipped to false on leak
|
|
336
349
|
try {
|
|
337
350
|
for await (const chunk of stream) {
|
|
@@ -370,7 +383,6 @@ async function runAgent({ input, roots, glossary, confirmTool, write, signal, hi
|
|
|
370
383
|
reasoningContent += delta.reasoning_content;
|
|
371
384
|
}
|
|
372
385
|
if (delta.tool_calls) {
|
|
373
|
-
if (!spinnerStopped) { stop(); spinnerStopped = true; }
|
|
374
386
|
for (const tc of delta.tool_calls) {
|
|
375
387
|
const idx = tc.index ?? 0;
|
|
376
388
|
if (!toolCalls[idx]) toolCalls[idx] = { id: tc.id || '', type: 'function', function: { name: '', arguments: '' } };
|
|
@@ -382,7 +394,8 @@ async function runAgent({ input, roots, glossary, confirmTool, write, signal, hi
|
|
|
382
394
|
}
|
|
383
395
|
}
|
|
384
396
|
} finally {
|
|
385
|
-
|
|
397
|
+
// Spinner runs continuously from loop start — stopped below when
|
|
398
|
+
// we're about to show output or dispatch tools that may need input.
|
|
386
399
|
}
|
|
387
400
|
|
|
388
401
|
// ── DSML leak detection (after stream completes) ────────────────────
|
|
@@ -427,6 +440,7 @@ async function runAgent({ input, roots, glossary, confirmTool, write, signal, hi
|
|
|
427
440
|
// Max retries exceeded — strip and show what we can.
|
|
428
441
|
content = sanitized.visibleText;
|
|
429
442
|
if (!content) {
|
|
443
|
+
stopTaskSpinner();
|
|
430
444
|
write(dim('[shmakk] response contained only leaked tool markup — blocked.', colors) + '\n');
|
|
431
445
|
clearTaskJournal(roots[0]);
|
|
432
446
|
return messages.slice(1);
|
|
@@ -477,6 +491,7 @@ async function runAgent({ input, roots, glossary, confirmTool, write, signal, hi
|
|
|
477
491
|
|
|
478
492
|
// No tools → done.
|
|
479
493
|
if (!normalizedToolCalls.length) {
|
|
494
|
+
stopTaskSpinner();
|
|
480
495
|
if (content) {
|
|
481
496
|
write(renderBlock(content, { enabled: markdown, colors }));
|
|
482
497
|
if (!content.endsWith('\n')) write('\n');
|
|
@@ -497,6 +512,7 @@ async function runAgent({ input, roots, glossary, confirmTool, write, signal, hi
|
|
|
497
512
|
// Dispatch tool calls.
|
|
498
513
|
// Reads/lists are noisy — collect them silently and show a single dim summary.
|
|
499
514
|
// Writes, edits, runs, and errors always print clearly.
|
|
515
|
+
stopTaskSpinner();
|
|
500
516
|
const SILENT_TOOLS = new Set(['read_file', 'list_dir', 'web_search', 'fetch_url']);
|
|
501
517
|
let iterProgress = false;
|
|
502
518
|
let silentReads = [];
|
|
@@ -534,7 +550,34 @@ async function runAgent({ input, roots, glossary, confirmTool, write, signal, hi
|
|
|
534
550
|
write(dim(`→ ${c.function.name}(${shortArgs(args)})${summary ? ' — ' + summary : ''}`, colors) + '\n');
|
|
535
551
|
}
|
|
536
552
|
|
|
537
|
-
|
|
553
|
+
// Build tool result message. If the result includes images with real
|
|
554
|
+
// base64 data AND the current endpoint has `vision: true`, send them as
|
|
555
|
+
// vision content blocks in a follow-up user message. Otherwise just
|
|
556
|
+
// include image metadata in the text. Keep the tool message text-only.
|
|
557
|
+
const toolImages = Array.isArray(result.images) ? result.images.filter((img) => img.data) : [];
|
|
558
|
+
const toolText = result.content || '';
|
|
559
|
+
const toolMeta = Object.fromEntries(
|
|
560
|
+
Object.entries(result || {}).filter(([k]) => !['content', 'images', 'error', 'isRetryable'].includes(k)),
|
|
561
|
+
);
|
|
562
|
+
let toolContent = (toolText + (Object.keys(toolMeta).length ? ' ' + JSON.stringify(toolMeta) : '')).trim();
|
|
563
|
+
if (toolImages.length > 0 && !supportsVision()) {
|
|
564
|
+
// Endpoint doesn't support vision — include image metadata as text
|
|
565
|
+
const imgDesc = toolImages.map((img, i) => `[Image #${i + 1}: ${img.mimeType}, base64=${img.dataLength} chars${img.truncated ? ', truncated' : ''}]`).join(', ');
|
|
566
|
+
toolContent = toolContent ? `${toolContent} ${imgDesc}` : imgDesc;
|
|
567
|
+
}
|
|
568
|
+
messages.push({ role: 'tool', tool_call_id: c.id, content: toolContent.slice(0, 8000) });
|
|
569
|
+
if (toolImages.length > 0 && supportsVision()) {
|
|
570
|
+
messages.push({
|
|
571
|
+
role: 'user',
|
|
572
|
+
content: [
|
|
573
|
+
{ type: 'text', text: `[Images returned by ${c.function.name}${toolImages.some((img) => img.truncated) ? ' (some truncated to ~2MB base64)' : ''}]` },
|
|
574
|
+
...toolImages.map((img) => ({
|
|
575
|
+
type: 'image_url',
|
|
576
|
+
image_url: { url: `data:${img.mimeType};base64,${img.data}`, detail: 'auto' },
|
|
577
|
+
})),
|
|
578
|
+
],
|
|
579
|
+
});
|
|
580
|
+
}
|
|
538
581
|
producedAnything = true;
|
|
539
582
|
persistJournal('running');
|
|
540
583
|
if (signal && signal.aborted) return messages.slice(1);
|
|
@@ -553,6 +596,7 @@ async function runAgent({ input, roots, glossary, confirmTool, write, signal, hi
|
|
|
553
596
|
}
|
|
554
597
|
|
|
555
598
|
if (repeatedSignatureCount >= runtimeProfile.stallRepeat && noProgressRepeats >= 2) {
|
|
599
|
+
stopTaskSpinner();
|
|
556
600
|
break;
|
|
557
601
|
}
|
|
558
602
|
}
|
|
@@ -589,6 +633,7 @@ async function runAgent({ input, roots, glossary, confirmTool, write, signal, hi
|
|
|
589
633
|
}
|
|
590
634
|
} catch {}
|
|
591
635
|
|
|
636
|
+
stopTaskSpinner();
|
|
592
637
|
write(dim('[shmakk] paused after several tool rounds. Resume later continues from task journal; try `shmakk --reset` to clear.', colors) + '\n');
|
|
593
638
|
persistJournal('paused');
|
|
594
639
|
return messages.slice(1);
|
package/src/cli.js
CHANGED
|
@@ -41,8 +41,10 @@ function parseArgs(argv) {
|
|
|
41
41
|
voiceSilenceStartSec: null,
|
|
42
42
|
voicePadStartSec: null,
|
|
43
43
|
ttsVoice: null,
|
|
44
|
-
notify:
|
|
44
|
+
notify: true,
|
|
45
45
|
completion: null,
|
|
46
|
+
helpCategory: null,
|
|
47
|
+
shell: null,
|
|
46
48
|
unknown: [],
|
|
47
49
|
};
|
|
48
50
|
|
|
@@ -53,7 +55,13 @@ function parseArgs(argv) {
|
|
|
53
55
|
case '--yes-files': opts.yesFiles = true; break;
|
|
54
56
|
case '--update-command-glossary': opts.updateGlossary = true; break;
|
|
55
57
|
case '-h':
|
|
56
|
-
case '--help':
|
|
58
|
+
case '--help':
|
|
59
|
+
opts.help = true;
|
|
60
|
+
// Capture optional category: shmakk --help voice
|
|
61
|
+
if (i + 1 < argv.length && !argv[i + 1].startsWith('-')) {
|
|
62
|
+
opts.helpCategory = argv[++i];
|
|
63
|
+
}
|
|
64
|
+
break;
|
|
57
65
|
case '--debug': opts.debug = true; break;
|
|
58
66
|
case '--no-ai': opts.noAi = true; break;
|
|
59
67
|
case '--no-correction': opts.noCorrection = true; break;
|
|
@@ -102,34 +110,67 @@ function parseArgs(argv) {
|
|
|
102
110
|
case '--markdown': opts.markdown = argv[++i] || null; break;
|
|
103
111
|
case '--endpoint': opts.endpoint = argv[++i] || null; break;
|
|
104
112
|
case '--model-recommendation': opts.modelRecommendation = true; break;
|
|
113
|
+
case '--shell':
|
|
114
|
+
{
|
|
115
|
+
const v = argv[++i];
|
|
116
|
+
if (!v || !['fish', 'bash', 'zsh'].includes(v)) {
|
|
117
|
+
process.stderr.write('[shmakk] invalid --shell. Use: fish|bash|zsh\n');
|
|
118
|
+
process.exit(2);
|
|
119
|
+
}
|
|
120
|
+
opts.shell = v;
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
105
123
|
default: opts.unknown.push(a);
|
|
106
124
|
}
|
|
107
125
|
}
|
|
108
126
|
return opts;
|
|
109
127
|
}
|
|
110
128
|
|
|
111
|
-
|
|
129
|
+
// ── Help: category-based navigation ──────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
const HELP_SUMMARY = `shmakk - AI-supervised terminal wrapper
|
|
112
132
|
|
|
113
133
|
Launch shmakk, then type commands as usual. shmakk watches the shell, catches
|
|
114
134
|
failures, and calls an LLM to fix them, plan tasks, and edit files.
|
|
115
135
|
|
|
116
|
-
You can also type natural-language self-commands directly into the session
|
|
117
|
-
(e.g. "list skills", "agent overview", "compact"). See SELF-COMMANDS below.
|
|
118
|
-
|
|
119
136
|
Type "help" inside a session to see this same text.
|
|
120
137
|
|
|
121
|
-
|
|
122
|
-
|
|
138
|
+
Usage: shmakk [--flag ...]
|
|
139
|
+
shmakk --help [category]
|
|
140
|
+
|
|
141
|
+
Categories (shmakk --help <name> for details):
|
|
142
|
+
|
|
143
|
+
launch Startup modes, profiles, tuning flags
|
|
144
|
+
session Status, stats, restart, exit, control signals
|
|
145
|
+
skills Skill discovery, loading, listing, management
|
|
146
|
+
models Provider configuration, endpoint presets
|
|
147
|
+
voice Speech-to-Text / Text-to-Speech options
|
|
148
|
+
env Environment variable reference
|
|
149
|
+
mcp MCP servers and browser automation
|
|
150
|
+
ssh Remote host execution
|
|
151
|
+
self Natural-language self-commands (inside a session)
|
|
152
|
+
|
|
153
|
+
`;
|
|
154
|
+
|
|
155
|
+
const HELP_SECTIONS = {};
|
|
156
|
+
|
|
157
|
+
HELP_SECTIONS.launch = `═══════════════════════════════════════════════════════════════════════════
|
|
158
|
+
LAUNCH OPTIONS (shmakk --flag from outside a session)
|
|
123
159
|
═══════════════════════════════════════════════════════════════════════════
|
|
124
160
|
|
|
161
|
+
These flags only apply when starting a new shmakk session. They are
|
|
162
|
+
ignored if you are already inside shmakk (SHMAKK=1).
|
|
163
|
+
|
|
125
164
|
shmakk Launch in auto mode (AI acts on failures)
|
|
126
165
|
shmakk --review Launch in review mode (confirm every AI action)
|
|
127
166
|
shmakk --yes-files Auto-accept file writes, edits, mkdir
|
|
128
167
|
|
|
129
|
-
shmakk --help Show this
|
|
168
|
+
shmakk --help Show overview (this text)
|
|
169
|
+
shmakk --help <category> Show detailed help for a category
|
|
130
170
|
shmakk --build-history [files] Parse shell history for better corrections
|
|
131
171
|
shmakk --update-command-glossary Scan PATH and build local command glossary
|
|
132
172
|
|
|
173
|
+
--shell <fish|bash|zsh> Use a specific shell (default: current $SHELL)
|
|
133
174
|
--no-ai Disable AI entirely (pure passthrough)
|
|
134
175
|
--no-correction Disable command correction
|
|
135
176
|
--debug Verbose logging to stderr
|
|
@@ -140,8 +181,9 @@ const HELP = `shmakk - AI-supervised terminal wrapper
|
|
|
140
181
|
--colors <true|false> Enable or disable ANSI colors
|
|
141
182
|
--markdown <true|false> Enable or disable markdown rendering
|
|
142
183
|
--notify Desktop notifications for Y/n prompts
|
|
184
|
+
`;
|
|
143
185
|
|
|
144
|
-
|
|
186
|
+
HELP_SECTIONS.models = `═══════════════════════════════════════════════════════════════════════════
|
|
145
187
|
MODEL PROVIDERS
|
|
146
188
|
═══════════════════════════════════════════════════════════════════════════
|
|
147
189
|
|
|
@@ -159,8 +201,9 @@ const HELP = `shmakk - AI-supervised terminal wrapper
|
|
|
159
201
|
"model":"qwen/qwen3.5-9b" }
|
|
160
202
|
}
|
|
161
203
|
}
|
|
204
|
+
`;
|
|
162
205
|
|
|
163
|
-
|
|
206
|
+
HELP_SECTIONS.session = `═══════════════════════════════════════════════════════════════════════════
|
|
164
207
|
SESSION CONTROL (shmakk --flag from another terminal)
|
|
165
208
|
═══════════════════════════════════════════════════════════════════════════
|
|
166
209
|
|
|
@@ -176,6 +219,11 @@ const HELP = `shmakk - AI-supervised terminal wrapper
|
|
|
176
219
|
shmakk --exit Cleanly exit the parent shmakk
|
|
177
220
|
|
|
178
221
|
shmakk --profile-set <name> Switch profile and restart
|
|
222
|
+
`;
|
|
223
|
+
|
|
224
|
+
HELP_SECTIONS.skills = `═══════════════════════════════════════════════════════════════════════════
|
|
225
|
+
SKILLS
|
|
226
|
+
═══════════════════════════════════════════════════════════════════════════
|
|
179
227
|
|
|
180
228
|
shmakk --load-skill <name> Load a skill into workspace state
|
|
181
229
|
shmakk --install-skill <url> Download skill markdown from URL, validate, load
|
|
@@ -183,71 +231,9 @@ const HELP = `shmakk - AI-supervised terminal wrapper
|
|
|
183
231
|
shmakk --list-skills List all registered skills (workspace + global)
|
|
184
232
|
shmakk --skill-status Active skill and registry status
|
|
185
233
|
shmakk --unload-skill <name> Remove skill from whichever registry has it
|
|
234
|
+
`;
|
|
186
235
|
|
|
187
|
-
|
|
188
|
-
SELF-COMMANDS (type inside an shmakk session)
|
|
189
|
-
═══════════════════════════════════════════════════════════════════════════
|
|
190
|
-
|
|
191
|
-
── Skills ──
|
|
192
|
-
list skills List all registered skills
|
|
193
|
-
list skills <category> List skills in a specific category
|
|
194
|
-
list skill categories Show available skill categories
|
|
195
|
-
find skills <query> Search skills by name/description
|
|
196
|
-
load skill <name> Load a skill into the active workspace
|
|
197
|
-
unload skill <name> Remove a skill from its registry
|
|
198
|
-
skill status Show active skill and registry state
|
|
199
|
-
|
|
200
|
-
── Agents & Team ──
|
|
201
|
-
agent overview Show all agents and their specialisms
|
|
202
|
-
agent skills List all agent skills
|
|
203
|
-
agent <name> Show detail for a specific agent
|
|
204
|
-
list agents Alias for agent overview
|
|
205
|
-
|
|
206
|
-
── Context & Session ──
|
|
207
|
-
status Show session status
|
|
208
|
-
stats Show session/task statistics
|
|
209
|
-
resume status Show task journal for resume continuity
|
|
210
|
-
show plan Display current plan and progress
|
|
211
|
-
compact Clear conversation + task journal
|
|
212
|
-
reset Clear AI conversation history
|
|
213
|
-
|
|
214
|
-
── Memory & Search ──
|
|
215
|
-
recall <query> Search past sessions by content
|
|
216
|
-
find session <query> Find a session by topic
|
|
217
|
-
last sessions Show recent sessions
|
|
218
|
-
search db status Display session search DB info
|
|
219
|
-
show memory List stored memories
|
|
220
|
-
forget <query> Remove matching memories
|
|
221
|
-
|
|
222
|
-
── Configuration ──
|
|
223
|
-
show config Print resolved configuration
|
|
224
|
-
mcp status Show MCP servers and tools
|
|
225
|
-
show rules Display active workspace rules
|
|
226
|
-
list endpoints List configured model endpoints
|
|
227
|
-
use endpoint <name> Switch to a named model endpoint
|
|
228
|
-
set model to <name> Change the active model
|
|
229
|
-
set url to <url> Change the base URL
|
|
230
|
-
set api key to <key> Change the API key
|
|
231
|
-
|
|
232
|
-
── Toggles ──
|
|
233
|
-
enable review | disable review
|
|
234
|
-
enable correction | disable correction
|
|
235
|
-
enable yes-files | disable yes-files
|
|
236
|
-
enable colors | disable colors
|
|
237
|
-
enable debug | disable debug
|
|
238
|
-
|
|
239
|
-
── Workflows ──
|
|
240
|
-
list workflows Show available automation workflows
|
|
241
|
-
run workflow <name> Execute a named workflow
|
|
242
|
-
|
|
243
|
-
── Edits ──
|
|
244
|
-
review edits Step through pending file changes
|
|
245
|
-
|
|
246
|
-
── Meta ──
|
|
247
|
-
sidebar <query> Out-of-band agent query (not added to history)
|
|
248
|
-
help Show this help
|
|
249
|
-
|
|
250
|
-
═══════════════════════════════════════════════════════════════════════════
|
|
236
|
+
HELP_SECTIONS.voice = `═══════════════════════════════════════════════════════════════════════════
|
|
251
237
|
VOICE (Speech-to-Text / Text-to-Speech)
|
|
252
238
|
═══════════════════════════════════════════════════════════════════════════
|
|
253
239
|
|
|
@@ -266,8 +252,9 @@ const HELP = `shmakk - AI-supervised terminal wrapper
|
|
|
266
252
|
STT: Whisper-base ONNX in-process. No Python, no server, no API key.
|
|
267
253
|
TTS: kokoro-js (Kokoro-82M ONNX, ~334MB fp16). Auto-download on first use.
|
|
268
254
|
Requires aplay, paplay, or afplay for audio. 28 voices, rotated daily.
|
|
255
|
+
`;
|
|
269
256
|
|
|
270
|
-
|
|
257
|
+
HELP_SECTIONS.env = `═══════════════════════════════════════════════════════════════════════════
|
|
271
258
|
ENVIRONMENT
|
|
272
259
|
═══════════════════════════════════════════════════════════════════════════
|
|
273
260
|
|
|
@@ -287,8 +274,9 @@ const HELP = `shmakk - AI-supervised terminal wrapper
|
|
|
287
274
|
SHMAKK_VOICE_SILENCE_SEC VAD silence threshold seconds
|
|
288
275
|
SHMAKK_VOICE_SILENCE_THRESHOLD VAD amplitude threshold
|
|
289
276
|
SHMAKK_VOICE_PAD_START_SEC Start-of-recording padding
|
|
277
|
+
`;
|
|
290
278
|
|
|
291
|
-
|
|
279
|
+
HELP_SECTIONS.mcp = `═══════════════════════════════════════════════════════════════════════════
|
|
292
280
|
MCP & BROWSER
|
|
293
281
|
═══════════════════════════════════════════════════════════════════════════
|
|
294
282
|
|
|
@@ -299,8 +287,9 @@ const HELP = `shmakk - AI-supervised terminal wrapper
|
|
|
299
287
|
npm install playwright && npx playwright install chromium
|
|
300
288
|
Tools: navigate, click, type, read_page, screenshot, evaluate, select,
|
|
301
289
|
wait, scroll, close.
|
|
290
|
+
`;
|
|
302
291
|
|
|
303
|
-
|
|
292
|
+
HELP_SECTIONS.ssh = `═══════════════════════════════════════════════════════════════════════════
|
|
304
293
|
REMOTE HOSTS (SSH)
|
|
305
294
|
═══════════════════════════════════════════════════════════════════════════
|
|
306
295
|
|
|
@@ -326,7 +315,106 @@ const HELP = `shmakk - AI-supervised terminal wrapper
|
|
|
326
315
|
ControlMaster auto
|
|
327
316
|
ControlPath ~/.ssh/controlmasters/%r@%h:%p
|
|
328
317
|
ControlPersist 600
|
|
318
|
+
`;
|
|
319
|
+
|
|
320
|
+
HELP_SECTIONS.self = `═══════════════════════════════════════════════════════════════════════════
|
|
321
|
+
SELF-COMMANDS (type inside an shmakk session)
|
|
322
|
+
═══════════════════════════════════════════════════════════════════════════
|
|
323
|
+
|
|
324
|
+
Self-commands work with a prefix — /cmd or shmakk cmd.
|
|
325
|
+
Bare words like "status" or "stats" go to the shell, not shmakk.
|
|
326
|
+
|
|
327
|
+
-- Session --
|
|
328
|
+
/status | shmakk status Show session status
|
|
329
|
+
/stats | shmakk stats Show session/task statistics
|
|
330
|
+
/sessions | shmakk sessions Show recent sessions
|
|
331
|
+
show sessions | last sessions (same as /sessions)
|
|
332
|
+
resume status Show task journal for resume continuity
|
|
333
|
+
show plan Display current plan and progress
|
|
334
|
+
/compact | shmakk compact Clear conversation + task journal
|
|
335
|
+
/reset | shmakk reset Clear AI conversation history
|
|
336
|
+
|
|
337
|
+
-- Skills --
|
|
338
|
+
list skills List all registered skills
|
|
339
|
+
list skills <category> List skills in a specific category
|
|
340
|
+
list skill categories Show available skill categories
|
|
341
|
+
find skills <query> Search skills by name/description
|
|
342
|
+
load skill <name> Load a skill into the active workspace
|
|
343
|
+
unload skill <name> Remove a skill from its registry
|
|
344
|
+
skill status Show active skill and registry state
|
|
345
|
+
|
|
346
|
+
-- Agents & Team --
|
|
347
|
+
agent overview Show all agents and their specialisms
|
|
348
|
+
agent skills List all agent skills
|
|
349
|
+
agent <name> Show detail for a specific agent
|
|
350
|
+
list agents Alias for agent overview
|
|
351
|
+
|
|
352
|
+
-- Memory & Search --
|
|
353
|
+
recall <query> Search past sessions by content
|
|
354
|
+
find session <query> Find a session by topic
|
|
355
|
+
search db status Display session search DB info
|
|
356
|
+
show memory List stored memories
|
|
357
|
+
forget <query> Remove matching memories
|
|
358
|
+
|
|
359
|
+
-- Configuration --
|
|
360
|
+
show config Print resolved configuration
|
|
361
|
+
mcp status Show MCP servers and tools
|
|
362
|
+
show rules Display active workspace rules
|
|
363
|
+
list endpoints List configured model endpoints
|
|
364
|
+
use endpoint <name> Switch to a named model endpoint
|
|
365
|
+
set model to <name> Change the active model
|
|
366
|
+
set url to <url> Change the base URL
|
|
367
|
+
set api key to <key> Change the API key
|
|
368
|
+
|
|
369
|
+
-- Toggles --
|
|
370
|
+
enable review | disable review
|
|
371
|
+
enable correction | disable correction
|
|
372
|
+
enable yes-files | disable yes-files
|
|
373
|
+
enable colors | disable colors
|
|
374
|
+
enable debug | disable debug
|
|
375
|
+
|
|
376
|
+
-- Workflows --
|
|
377
|
+
list workflows Show available automation workflows
|
|
378
|
+
run workflow <name> Execute a named workflow
|
|
379
|
+
|
|
380
|
+
-- Edits --
|
|
381
|
+
review edits Step through pending file changes
|
|
382
|
+
|
|
383
|
+
-- Meta --
|
|
384
|
+
sidebar <query> Out-of-band agent query (not added to history)
|
|
385
|
+
help Show this help
|
|
386
|
+
`;
|
|
387
|
+
|
|
388
|
+
// Resolve: returns the full old HELP string for backward compat, or the category
|
|
389
|
+
// text, or the summary.
|
|
390
|
+
function resolveHelp(category) {
|
|
391
|
+
if (category) {
|
|
392
|
+
const key = category.toLowerCase();
|
|
393
|
+
if (HELP_SECTIONS[key]) return HELP_SECTIONS[key];
|
|
394
|
+
// Unknown category: show summary + available categories
|
|
395
|
+
const available = Object.keys(HELP_SECTIONS).join(', ');
|
|
396
|
+
return HELP_SUMMARY + `Unknown category "${category}". Available: ${available}\n`;
|
|
397
|
+
}
|
|
398
|
+
return HELP_SUMMARY;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Help text shown INSIDE a session (no launch flags — you're already running)
|
|
402
|
+
const HELP_SESSION_SUMMARY = `shmakk — inside a session
|
|
403
|
+
|
|
404
|
+
Type commands as usual. shmakk watches the shell, catches failures, and
|
|
405
|
+
calls an LLM to fix them, plan tasks, and edit files.
|
|
406
|
+
|
|
407
|
+
Self-commands use a prefix: /cmd or shmakk cmd
|
|
408
|
+
Examples: /status /sessions shmakk status shmakk show sessions
|
|
409
|
+
|
|
410
|
+
Multi-word natural language also works: "show help" "list skills"
|
|
411
|
+
|
|
412
|
+
For the full reference: shmakk --help self (from outside the session)
|
|
413
|
+
For launch flags: shmakk --help launch
|
|
329
414
|
|
|
330
415
|
`;
|
|
331
416
|
|
|
332
|
-
|
|
417
|
+
// Full legacy HELP for backward compat
|
|
418
|
+
const HELP = HELP_SUMMARY + Object.values(HELP_SECTIONS).join('\n');
|
|
419
|
+
|
|
420
|
+
module.exports = { parseArgs, HELP, resolveHelp, HELP_SUMMARY, HELP_SESSION_SUMMARY };
|
package/src/correction.js
CHANGED
|
@@ -115,6 +115,18 @@ function preserveCase(original, corrected) {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
async function correct({ input, glossary, signal: _unused }) {
|
|
118
|
+
// Null/empty input: can happen at shell startup when precmd fires before
|
|
119
|
+
// any command is executed (especially in zsh). Nothing to correct.
|
|
120
|
+
if (!input || !input.trim()) {
|
|
121
|
+
return { category: 'not_a_correction', proposed: null, safety: 'uncertain', reason: 'empty input' };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// /-prefixed and "shmakk ..." commands are shmakk self-commands.
|
|
125
|
+
// They should never reach the correction engine — bail out immediately.
|
|
126
|
+
if (/^\//.test(input) || /^shmakk\s/i.test(input)) {
|
|
127
|
+
return { category: 'not_a_correction', proposed: null, safety: 'uncertain', reason: 'shmakk self-command prefix — not a shell command' };
|
|
128
|
+
}
|
|
129
|
+
|
|
118
130
|
// Pre-filter natural language
|
|
119
131
|
if (looksLikeNaturalLanguage(input)) {
|
|
120
132
|
return { category: 'not_a_correction', proposed: null, safety: 'uncertain', reason: 'looks like natural language' };
|
package/src/endpoints.js
CHANGED
|
@@ -73,6 +73,7 @@ function normalizeModelConfig(name, cfg) {
|
|
|
73
73
|
headers: cfg.headers || cfg.headears || null,
|
|
74
74
|
registry: cfg.registry || null,
|
|
75
75
|
main: !!cfg.main,
|
|
76
|
+
vision: !!cfg.vision,
|
|
76
77
|
};
|
|
77
78
|
}
|
|
78
79
|
|
|
@@ -143,6 +144,10 @@ function getCurrentEndpointName() {
|
|
|
143
144
|
return currentEndpointName;
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
function supportsVision() {
|
|
148
|
+
return !!(currentEndpointConfig && currentEndpointConfig.vision);
|
|
149
|
+
}
|
|
150
|
+
|
|
146
151
|
function listEndpoints(cwd) {
|
|
147
152
|
return Object.keys(loadModelRegistry(cwd).models);
|
|
148
153
|
}
|
|
@@ -160,5 +165,6 @@ module.exports = {
|
|
|
160
165
|
listEndpoints,
|
|
161
166
|
getCurrentEndpoint,
|
|
162
167
|
getCurrentEndpointName,
|
|
168
|
+
supportsVision,
|
|
163
169
|
getModelRegistry,
|
|
164
170
|
};
|
package/src/hooks/bash.js
CHANGED
|
@@ -3,6 +3,21 @@ const os = require('os');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
|
|
5
5
|
const INIT = `
|
|
6
|
+
# Source login scripts first (equivalent to bash -l).
|
|
7
|
+
# When --rcfile is used, bash skips the normal login sequence, so we must
|
|
8
|
+
# replicate it manually. Order matters: profile.d → /etc/profile, then
|
|
9
|
+
# first found of: ~/.bash_profile, ~/.bash_login, ~/.profile, ~/.bashrc.
|
|
10
|
+
[ -d /etc/profile.d ] && for f in /etc/profile.d/*.sh; do [ -r "$f" ] && . "$f"; done
|
|
11
|
+
[ -f /etc/profile ] && . /etc/profile
|
|
12
|
+
if [ -f "$HOME/.bash_profile" ]; then
|
|
13
|
+
. "$HOME/.bash_profile"
|
|
14
|
+
elif [ -f "$HOME/.bash_login" ]; then
|
|
15
|
+
. "$HOME/.bash_login"
|
|
16
|
+
elif [ -f "$HOME/.profile" ]; then
|
|
17
|
+
. "$HOME/.profile"
|
|
18
|
+
fi
|
|
19
|
+
# Then interactive rc files (these may be already sourced above, but
|
|
20
|
+
# sourcing twice is harmless for well-behaved scripts).
|
|
6
21
|
[ -f /etc/bash.bashrc ] && . /etc/bash.bashrc
|
|
7
22
|
[ -f "$HOME/.bashrc" ] && . "$HOME/.bashrc"
|
|
8
23
|
|
|
@@ -13,13 +28,13 @@ __shmakk_preexec() {
|
|
|
13
28
|
[ "$BASH_COMMAND" = "$PROMPT_COMMAND" ] && return
|
|
14
29
|
__shmakk_armed=
|
|
15
30
|
local cmd
|
|
16
|
-
cmd=$(printf '%s' "$BASH_COMMAND" | base64 -w0 2>/dev/null ||
|
|
31
|
+
cmd=$(printf '%s' "$BASH_COMMAND" | base64 -w0 2>/dev/null || base64 -b 0 2>/dev/null || base64 | tr -d '\n')
|
|
17
32
|
printf '\\e]6973;B;%s\\a' "$cmd"
|
|
18
33
|
}
|
|
19
34
|
__shmakk_precmd() {
|
|
20
35
|
local ec=$?
|
|
21
36
|
local p
|
|
22
|
-
p=$(printf '%s' "$PWD" | base64 -w0 2>/dev/null ||
|
|
37
|
+
p=$(printf '%s' "$PWD" | base64 -w0 2>/dev/null || base64 -b 0 2>/dev/null || base64 | tr -d '\n')
|
|
23
38
|
printf '\\e]6973;C;%s\\a' "$ec"
|
|
24
39
|
printf '\\e]6973;D;%s\\a' "$p"
|
|
25
40
|
__shmakk_armed=1
|
package/src/hooks/fish.js
CHANGED
|
@@ -1,17 +1,36 @@
|
|
|
1
1
|
// Returns { args, env, cleanup } for spawning fish with markers wired up.
|
|
2
2
|
// fish supports `-C COMMAND` to run init code after config.fish.
|
|
3
|
+
//
|
|
4
|
+
// base64 encoding: try `-w0` (GNU coreutils), fall back to `-b 0` (BSD/macOS),
|
|
5
|
+
// then plain `base64` as last resort. `tr -d '\n'` strips any line wrapping
|
|
6
|
+
// so the OSC marker payload stays on one line.
|
|
3
7
|
|
|
4
8
|
const INIT = `
|
|
5
9
|
function __shmakk_pre --on-event fish_preexec
|
|
6
|
-
set -l c (printf '%s' "$argv" | base64 -w0 2>/dev/null
|
|
10
|
+
set -l c (printf '%s' "$argv" | base64 -w0 2>/dev/null || base64 -b 0 2>/dev/null || base64 | tr -d '\n')
|
|
7
11
|
printf '\\e]6973;B;%s\\a' "$c"
|
|
8
12
|
end
|
|
9
13
|
function __shmakk_post --on-event fish_postexec
|
|
10
14
|
set -l ec $status
|
|
11
|
-
set -l p (printf '%s' "$PWD" | base64 -w0 2>/dev/null
|
|
15
|
+
set -l p (printf '%s' "$PWD" | base64 -w0 2>/dev/null || base64 -b 0 2>/dev/null || base64 | tr -d '\n')
|
|
12
16
|
printf '\\e]6973;C;%s\\a' $ec
|
|
13
17
|
printf '\\e]6973;D;%s\\a' "$p"
|
|
14
18
|
end
|
|
19
|
+
# Override shmakk binary inside a session so "shmakk <cmd>" routes to
|
|
20
|
+
# local self-commands instead of forking a nested shmakk process.
|
|
21
|
+
# Passes through --flags to the real shmakk binary.
|
|
22
|
+
function shmakk
|
|
23
|
+
if set -q argv[1]; and string match -qr '^--' -- "$argv[1]"
|
|
24
|
+
command shmakk $argv
|
|
25
|
+
return $status
|
|
26
|
+
end
|
|
27
|
+
set -l raw (printf '%s' "shmakk $argv" | base64 -w0 2>/dev/null || base64 -b 0 2>/dev/null || base64 | tr -d '\n')
|
|
28
|
+
set -l pwd_b64 (printf '%s' "$PWD" | base64 -w0 2>/dev/null || base64 -b 0 2>/dev/null || base64 | tr -d '\n')
|
|
29
|
+
printf '\\e]6973;B;%s\\a' "$raw"
|
|
30
|
+
printf '\\e]6973;C;127\\a'
|
|
31
|
+
printf '\\e]6973;D;%s\\a' "$pwd_b64"
|
|
32
|
+
return 127
|
|
33
|
+
end
|
|
15
34
|
`.trim();
|
|
16
35
|
|
|
17
36
|
function configure() {
|
package/src/hooks/zsh.js
CHANGED
|
@@ -2,6 +2,28 @@ const fs = require('fs');
|
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
|
|
5
|
+
// zsh under a custom ZDOTDIR sources .zshenv, .zprofile, .zshrc, .zlogin from
|
|
6
|
+
// that directory. We create all four so nothing from the user's real ZDOTDIR
|
|
7
|
+
// is skipped.
|
|
8
|
+
|
|
9
|
+
const ZSHENV = `
|
|
10
|
+
# Source the real .zshenv so PATH and env vars are available.
|
|
11
|
+
if [ -n "$SHMAKK_REAL_ZDOTDIR" ] && [ -f "$SHMAKK_REAL_ZDOTDIR/.zshenv" ]; then
|
|
12
|
+
source "$SHMAKK_REAL_ZDOTDIR/.zshenv"
|
|
13
|
+
elif [ -f "$HOME/.zshenv" ]; then
|
|
14
|
+
source "$HOME/.zshenv"
|
|
15
|
+
fi
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
const ZPROFILE = `
|
|
19
|
+
# Source the real .zprofile (login shell initialization).
|
|
20
|
+
if [ -n "$SHMAKK_REAL_ZDOTDIR" ] && [ -f "$SHMAKK_REAL_ZDOTDIR/.zprofile" ]; then
|
|
21
|
+
source "$SHMAKK_REAL_ZDOTDIR/.zprofile"
|
|
22
|
+
elif [ -f "$HOME/.zprofile" ]; then
|
|
23
|
+
source "$HOME/.zprofile"
|
|
24
|
+
fi
|
|
25
|
+
`;
|
|
26
|
+
|
|
5
27
|
const ZSHRC = `
|
|
6
28
|
# preserve real ZDOTDIR so user config is sourced
|
|
7
29
|
if [ -n "$SHMAKK_REAL_ZDOTDIR" ]; then
|
|
@@ -12,13 +34,13 @@ fi
|
|
|
12
34
|
|
|
13
35
|
__shmakk_preexec() {
|
|
14
36
|
local cmd
|
|
15
|
-
cmd=$(printf '%s' "$1" | base64 -w0 2>/dev/null ||
|
|
37
|
+
cmd=$(printf '%s' "$1" | base64 -w0 2>/dev/null || base64 -b 0 2>/dev/null || base64 | tr -d '\n')
|
|
16
38
|
printf '\\e]6973;B;%s\\a' "$cmd"
|
|
17
39
|
}
|
|
18
40
|
__shmakk_precmd() {
|
|
19
41
|
local ec=$?
|
|
20
42
|
local p
|
|
21
|
-
p=$(printf '%s' "$PWD" | base64 -w0 2>/dev/null ||
|
|
43
|
+
p=$(printf '%s' "$PWD" | base64 -w0 2>/dev/null || base64 -b 0 2>/dev/null || base64 | tr -d '\n')
|
|
22
44
|
printf '\\e]6973;C;%s\\a' "$ec"
|
|
23
45
|
printf '\\e]6973;D;%s\\a' "$p"
|
|
24
46
|
}
|
|
@@ -30,7 +52,14 @@ precmd_functions+=(__shmakk_precmd)
|
|
|
30
52
|
function configure() {
|
|
31
53
|
const dir = path.join(os.tmpdir(), `shmakk-zsh-${process.pid}`);
|
|
32
54
|
fs.mkdirSync(dir, { recursive: true });
|
|
55
|
+
// zsh under a custom ZDOTDIR sources .zshenv, .zprofile, .zshrc, .zlogin
|
|
56
|
+
// from that directory. We must provide all four so the user's environment
|
|
57
|
+
// is complete.
|
|
58
|
+
fs.writeFileSync(path.join(dir, '.zshenv'), ZSHENV, { mode: 0o600 });
|
|
59
|
+
fs.writeFileSync(path.join(dir, '.zprofile'), ZPROFILE, { mode: 0o600 });
|
|
33
60
|
fs.writeFileSync(path.join(dir, '.zshrc'), ZSHRC, { mode: 0o600 });
|
|
61
|
+
// No .zlogin needed — zsh docs say .zlogin is for commands to run at the
|
|
62
|
+
// start of an interactive login shell; .zprofile already covers env setup.
|
|
34
63
|
const realZ = process.env.ZDOTDIR || '';
|
|
35
64
|
return {
|
|
36
65
|
args: ['-i'],
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { parseArgs, HELP } = require('./cli');
|
|
1
|
+
const { parseArgs, HELP, resolveHelp } = require('./cli');
|
|
2
2
|
const { normalizeProfile, resolveProfile } = require('./profiles');
|
|
3
3
|
const { applyEndpoint, getCurrentEndpoint, getCurrentEndpointName } = require('./endpoints');
|
|
4
4
|
const { ensureModelRuntime } = require('./llm');
|
|
@@ -66,7 +66,7 @@ async function main() {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
if (opts.help) {
|
|
69
|
-
process.stdout.write(
|
|
69
|
+
process.stdout.write(resolveHelp(opts.helpCategory));
|
|
70
70
|
process.exit(0);
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -204,6 +204,15 @@ async function main() {
|
|
|
204
204
|
if (opts.ttsVoice) process.env.SHMAKK_TTS_VOICE = opts.ttsVoice;
|
|
205
205
|
|
|
206
206
|
const { start } = require('./orchestrator');
|
|
207
|
+
|
|
208
|
+
// Refuse to nest sessions: launching shmakk inside shmakk would
|
|
209
|
+
// create a recursive PTY tree with no benefit.
|
|
210
|
+
if (process.env.SHMAKK === '1') {
|
|
211
|
+
process.stderr.write('[shmakk] already inside an shmakk session (SHMAKK=1).\n');
|
|
212
|
+
process.stderr.write('[shmakk] use --help to see in-session commands, or exit the current session first.\n');
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
|
|
207
216
|
const exitCode = await start(opts);
|
|
208
217
|
process.exit(exitCode);
|
|
209
218
|
}
|
package/src/llm.js
CHANGED
|
@@ -4,7 +4,7 @@ try { OpenAI = require('openai'); } catch { OpenAI = null; }
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const fs = require('fs');
|
|
7
|
-
const { getCurrentEndpoint, getCurrentEndpointName, getModelRegistry } = require('./endpoints');
|
|
7
|
+
const { getCurrentEndpoint, getCurrentEndpointName, getModelRegistry, supportsVision } = require('./endpoints');
|
|
8
8
|
|
|
9
9
|
function parseHeaders(s) {
|
|
10
10
|
const out = {};
|
|
@@ -536,4 +536,4 @@ function getDeepSeekOptions(taskType) {
|
|
|
536
536
|
};
|
|
537
537
|
}
|
|
538
538
|
|
|
539
|
-
module.exports = { makeClient, modelFor, isConfigured, ensureModelRuntime, getDeepSeekOptions, isDeepSeekProvider };
|
|
539
|
+
module.exports = { makeClient, modelFor, isConfigured, ensureModelRuntime, getDeepSeekOptions, isDeepSeekProvider, supportsVision };
|
package/src/mcp-client.js
CHANGED
|
@@ -212,9 +212,15 @@ class MCPServer {
|
|
|
212
212
|
if (item.type === 'text') {
|
|
213
213
|
texts.push(item.text);
|
|
214
214
|
} else if (item.type === 'image') {
|
|
215
|
+
// Preserve base64 image data so vision-capable providers can process it.
|
|
216
|
+
// Cap at ~2MB of base64 to avoid blowing out context windows.
|
|
217
|
+
const raw = String(item.data || '');
|
|
218
|
+
const capped = raw.length > 2_000_000 ? raw.slice(0, 2_000_000) : raw;
|
|
215
219
|
images.push({
|
|
216
220
|
mimeType: item.mimeType || 'image/png',
|
|
217
|
-
|
|
221
|
+
data: capped,
|
|
222
|
+
dataLength: raw.length,
|
|
223
|
+
truncated: raw.length > 2_000_000,
|
|
218
224
|
});
|
|
219
225
|
} else if (item.type === 'resource') {
|
|
220
226
|
texts.push(`[resource: ${item.resource?.uri || 'unknown'}]`);
|
package/src/notify.js
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
// Falls back silently if notify-send is not available or no notification
|
|
3
3
|
// daemon is running.
|
|
4
4
|
|
|
5
|
-
const { execFile } = require('child_process');
|
|
5
|
+
const { execFile, execFileSync } = require('child_process');
|
|
6
|
+
const { existsSync } = require('fs');
|
|
6
7
|
|
|
7
8
|
const NOTIFY_BIN = 'notify-send';
|
|
8
9
|
|
|
9
10
|
function available() {
|
|
10
11
|
try {
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
// Prefer direct path check; fall back to `command -v` if not at known paths
|
|
13
|
+
if (existsSync('/usr/bin/notify-send')) return true;
|
|
14
|
+
if (existsSync('/usr/local/bin/notify-send')) return true;
|
|
15
|
+
execFileSync('command', ['-v', NOTIFY_BIN], { stdio: 'ignore' });
|
|
13
16
|
return true;
|
|
14
17
|
} catch {
|
|
15
18
|
return false;
|
package/src/pty.js
CHANGED
|
@@ -13,8 +13,8 @@ function getSize() {
|
|
|
13
13
|
|
|
14
14
|
const VOICE_HOTKEY = 0x0f; // Ctrl+O — triggers voice recording
|
|
15
15
|
|
|
16
|
-
function startSession({ debug = false, voiceEnabled = false } = {}) {
|
|
17
|
-
const shell = detectShell();
|
|
16
|
+
function startSession({ debug = false, voiceEnabled = false, shellOverride = null } = {}) {
|
|
17
|
+
const shell = detectShell(shellOverride);
|
|
18
18
|
const cfg = configureForShell(shell.name);
|
|
19
19
|
const { cols, rows } = getSize();
|
|
20
20
|
|
package/src/review.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
// optional `{ onCancel }` callback that fires when the user hits Ctrl-C.
|
|
3
3
|
|
|
4
4
|
function makePrompter(pty, write, opts = {}) {
|
|
5
|
-
const { onNotify } = opts;
|
|
6
5
|
return function ask(question, defaultYes, { onCancel, onWhy } = {}) {
|
|
7
6
|
return new Promise((resolve) => {
|
|
8
|
-
if (
|
|
7
|
+
if (opts.notify) {
|
|
9
8
|
try {
|
|
9
|
+
const { notify } = require('./notify');
|
|
10
10
|
const body = typeof question === 'string' ? question.replace(/\x1b\[[0-9;]*m/g, '') : String(question || '');
|
|
11
|
-
|
|
11
|
+
notify('shmakk needs your attention', body.slice(0, 120));
|
|
12
12
|
} catch {}
|
|
13
13
|
}
|
|
14
14
|
const tag = defaultYes ? '[Y/n/?]' : '[y/N/?]';
|
package/src/self-commands.js
CHANGED
|
@@ -110,12 +110,14 @@ const SELF_COMMANDS = [
|
|
|
110
110
|
},
|
|
111
111
|
|
|
112
112
|
// ── Session ──
|
|
113
|
+
// NOTE: Bare "status", "stats" etc. are NOT intercepted — they may be
|
|
114
|
+
// real shell commands. Use /status, /stats, shmakk status, etc. instead.
|
|
113
115
|
{
|
|
114
|
-
patterns: [/^
|
|
116
|
+
patterns: [/^status$/i],
|
|
115
117
|
action: 'status',
|
|
116
118
|
},
|
|
117
119
|
{
|
|
118
|
-
patterns: [/^
|
|
120
|
+
patterns: [/^stats$/i, /^session\s+stats$/i],
|
|
119
121
|
action: 'stats',
|
|
120
122
|
},
|
|
121
123
|
{
|
|
@@ -164,8 +166,8 @@ const SELF_COMMANDS = [
|
|
|
164
166
|
},
|
|
165
167
|
{
|
|
166
168
|
patterns: [
|
|
167
|
-
/^(?:show\s+)?last\s+sessions?$/i,
|
|
168
|
-
/^
|
|
169
|
+
/^(?:show\s+)?(?:last\s+|recent\s+)?sessions?$/i,
|
|
170
|
+
/^sessions?$/i,
|
|
169
171
|
],
|
|
170
172
|
action: 'last-sessions',
|
|
171
173
|
},
|
|
@@ -320,6 +322,22 @@ const SELF_COMMANDS = [
|
|
|
320
322
|
action: 'disable-yes-files',
|
|
321
323
|
},
|
|
322
324
|
|
|
325
|
+
// ── Notify ──
|
|
326
|
+
{
|
|
327
|
+
patterns: [
|
|
328
|
+
/^(?:enable|turn\s+on)\s+notify$/i,
|
|
329
|
+
/^notify\s+on$/i,
|
|
330
|
+
],
|
|
331
|
+
action: 'enable-notify',
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
patterns: [
|
|
335
|
+
/^(?:disable|turn\s+off|no)\s+notify$/i,
|
|
336
|
+
/^notify\s+off$/i,
|
|
337
|
+
],
|
|
338
|
+
action: 'disable-notify',
|
|
339
|
+
},
|
|
340
|
+
|
|
323
341
|
// ── Colors ──
|
|
324
342
|
{
|
|
325
343
|
patterns: [
|
|
@@ -399,20 +417,65 @@ const SELF_COMMANDS = [
|
|
|
399
417
|
},
|
|
400
418
|
];
|
|
401
419
|
|
|
420
|
+
// Self-command prefixes accepted by the shell:
|
|
421
|
+
// /cmd — e.g. /status, /sessions, /compact
|
|
422
|
+
// shmakk cmd — e.g. shmakk status, shmakk show sessions
|
|
423
|
+
// Bare words like "status" are NOT intercepted (they go to the shell).
|
|
424
|
+
const SELF_PREFIX_RE = /^\/(.+)$/;
|
|
425
|
+
const SHMAKK_PREFIX_RE = /^shmakk\s+(.+)$/i;
|
|
426
|
+
|
|
427
|
+
function hasSelfCommandPrefix(input) {
|
|
428
|
+
const text = String(input || '').trim();
|
|
429
|
+
return SELF_PREFIX_RE.test(text) || SHMAKK_PREFIX_RE.test(text);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function stripSelfCommandPrefix(input) {
|
|
433
|
+
const text = String(input || '').trim();
|
|
434
|
+
let m = SELF_PREFIX_RE.exec(text);
|
|
435
|
+
if (m) return m[1].trim();
|
|
436
|
+
m = SHMAKK_PREFIX_RE.exec(text);
|
|
437
|
+
if (m) return m[1].trim();
|
|
438
|
+
return text;
|
|
439
|
+
}
|
|
440
|
+
|
|
402
441
|
function matchSelfCommand(input) {
|
|
403
442
|
const text = String(input || '').trim();
|
|
404
443
|
if (!text) return { matched: false };
|
|
405
444
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
445
|
+
// Try matching with prefix stripped first (for /status, shmakk status, etc.)
|
|
446
|
+
const stripped = stripSelfCommandPrefix(text);
|
|
447
|
+
if (stripped !== text) {
|
|
448
|
+
for (const entry of SELF_COMMANDS) {
|
|
449
|
+
for (const pattern of entry.patterns) {
|
|
450
|
+
const m = pattern.exec(stripped);
|
|
451
|
+
if (m) {
|
|
452
|
+
return {
|
|
453
|
+
matched: true,
|
|
454
|
+
action: entry.action,
|
|
455
|
+
arg: entry.needsArg && m[1] ? m[1].trim() : null,
|
|
456
|
+
confirm: !!entry.confirm,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return { matched: false };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Multi-word natural-language commands (no prefix needed).
|
|
465
|
+
// Single bare words are NOT matched — they could be real shell commands.
|
|
466
|
+
const wordCount = text.split(/\s+/).length;
|
|
467
|
+
if (wordCount >= 2) {
|
|
468
|
+
for (const entry of SELF_COMMANDS) {
|
|
469
|
+
for (const pattern of entry.patterns) {
|
|
470
|
+
const m = pattern.exec(text);
|
|
471
|
+
if (m) {
|
|
472
|
+
return {
|
|
473
|
+
matched: true,
|
|
474
|
+
action: entry.action,
|
|
475
|
+
arg: entry.needsArg && m[1] ? m[1].trim() : null,
|
|
476
|
+
confirm: !!entry.confirm,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
416
479
|
}
|
|
417
480
|
}
|
|
418
481
|
}
|
|
@@ -422,6 +485,9 @@ function matchSelfCommand(input) {
|
|
|
422
485
|
|
|
423
486
|
// ctx is optional: { opts, HELP, setColors }
|
|
424
487
|
function executeSelfCommand(match, write, ctx = {}) {
|
|
488
|
+
// Update terminal tab title so self-command activity is visible from other tabs
|
|
489
|
+
const label = match.action.replace(/-/g, ' ');
|
|
490
|
+
write(`\x1b]0;${label} — shmakk\x07`);
|
|
425
491
|
const ctl = require('./control');
|
|
426
492
|
const opts = ctx.opts || {};
|
|
427
493
|
|
|
@@ -429,7 +495,7 @@ function executeSelfCommand(match, write, ctx = {}) {
|
|
|
429
495
|
|
|
430
496
|
// ── Help ──
|
|
431
497
|
case 'show-help': {
|
|
432
|
-
const helpText = ctx.HELP || '[shmakk] help text not available';
|
|
498
|
+
const helpText = ctx.HELP_SESSION_SUMMARY || ctx.HELP_SUMMARY || ctx.HELP || '[shmakk] help text not available';
|
|
433
499
|
write(helpText.replace(/\n/g, '\r\n'));
|
|
434
500
|
break;
|
|
435
501
|
}
|
|
@@ -803,6 +869,18 @@ function executeSelfCommand(match, write, ctx = {}) {
|
|
|
803
869
|
break;
|
|
804
870
|
}
|
|
805
871
|
|
|
872
|
+
// ── Notify ──
|
|
873
|
+
case 'enable-notify': {
|
|
874
|
+
if (ctx.opts) ctx.opts.notify = true;
|
|
875
|
+
write('[shmakk] desktop notifications enabled\r\n');
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
case 'disable-notify': {
|
|
879
|
+
if (ctx.opts) ctx.opts.notify = false;
|
|
880
|
+
write('[shmakk] desktop notifications disabled\r\n');
|
|
881
|
+
break;
|
|
882
|
+
}
|
|
883
|
+
|
|
806
884
|
// ── Colors ──
|
|
807
885
|
case 'enable-colors': {
|
|
808
886
|
if (ctx.opts) ctx.opts.colors = true;
|
|
@@ -858,6 +936,8 @@ function executeSelfCommand(match, write, ctx = {}) {
|
|
|
858
936
|
default:
|
|
859
937
|
write(`[shmakk] unknown self-command: ${match.action}\r\n`);
|
|
860
938
|
}
|
|
939
|
+
// Clear terminal title — shell will restore normal title on next prompt
|
|
940
|
+
write('\x1b]0;\x07');
|
|
861
941
|
}
|
|
862
942
|
|
|
863
|
-
module.exports = { matchSelfCommand, executeSelfCommand, SELF_COMMANDS };
|
|
943
|
+
module.exports = { matchSelfCommand, executeSelfCommand, hasSelfCommandPrefix, stripSelfCommandPrefix, SELF_COMMANDS };
|
package/src/session.js
CHANGED
|
@@ -20,7 +20,7 @@ const { runTeam, looksMultiDomain } = require('./team');
|
|
|
20
20
|
const { addPlanTasks, markTaskComplete, markTaskSkipped } = require('./task-file');
|
|
21
21
|
const { captureGitSha, runPostPlanReview } = require('./code-reviewer');
|
|
22
22
|
const sessionSearch = require('./session-search');
|
|
23
|
-
const { HELP } = require('./cli');
|
|
23
|
+
const { HELP, HELP_SUMMARY, HELP_SESSION_SUMMARY } = require('./cli');
|
|
24
24
|
const audit = require('./audit');
|
|
25
25
|
const { setMaxListeners } = require('events');
|
|
26
26
|
|
|
@@ -163,13 +163,11 @@ function makeToolConfirm(opts, ask, out, getAbort) {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
async function runOneSession(opts, registerSession) {
|
|
166
|
-
const session = startSession({ debug: opts.debug, voiceEnabled: !!opts.voice });
|
|
166
|
+
const session = startSession({ debug: opts.debug, voiceEnabled: !!opts.voice, shellOverride: opts.shell });
|
|
167
167
|
let colorsEnabled = opts.colors !== false;
|
|
168
168
|
let markdownEnabled = opts.markdown !== false;
|
|
169
169
|
const out = (s) => session.stdoutWrite(colorsEnabled ? s : stripAnsi(s));
|
|
170
|
-
const ask = makePrompter(session, out,
|
|
171
|
-
onNotify: opts.notify ? (summary, body) => notify(summary, body) : null,
|
|
172
|
-
});
|
|
170
|
+
const ask = makePrompter(session, out, opts);
|
|
173
171
|
const glossary = loadGlossary();
|
|
174
172
|
// Workspace tracking: explicit --workspace is "pinned"; otherwise cwd
|
|
175
173
|
// floats with the inner shell's `cd`. When both pinned and cwd differ,
|
|
@@ -373,6 +371,8 @@ async function runOneSession(opts, registerSession) {
|
|
|
373
371
|
executeSelfCommand(voiceSelfCmd, out, {
|
|
374
372
|
opts,
|
|
375
373
|
HELP,
|
|
374
|
+
HELP_SUMMARY,
|
|
375
|
+
HELP_SESSION_SUMMARY,
|
|
376
376
|
setColors: (v) => { colorsEnabled = v; },
|
|
377
377
|
});
|
|
378
378
|
return;
|
|
@@ -613,6 +613,14 @@ async function runOneSession(opts, registerSession) {
|
|
|
613
613
|
lastCommand = null;
|
|
614
614
|
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
|
|
615
615
|
|
|
616
|
+
// No command was tracked — precmd can fire at shell startup (especially
|
|
617
|
+
// in zsh) before any command executes. There's nothing to correct or
|
|
618
|
+
// route to the agent.
|
|
619
|
+
if (!lastCmd) {
|
|
620
|
+
discardPending();
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
|
|
616
624
|
// ── Self-command detection (FIRST — before ANY other processing) ──
|
|
617
625
|
// Self-commands are pure local execution. They MUST bypass:
|
|
618
626
|
// - the noAi early-return (they don't need an LLM)
|
|
@@ -672,11 +680,20 @@ async function runOneSession(opts, registerSession) {
|
|
|
672
680
|
executeSelfCommand(selfCmd, out, {
|
|
673
681
|
opts,
|
|
674
682
|
HELP,
|
|
683
|
+
HELP_SUMMARY,
|
|
684
|
+
HELP_SESSION_SUMMARY,
|
|
675
685
|
setColors: (v) => { colorsEnabled = v; },
|
|
676
686
|
});
|
|
677
687
|
session.childWrite('\r');
|
|
678
688
|
return;
|
|
679
689
|
}
|
|
690
|
+
// /-prefixed and "shmakk ..." commands that didn't match a known
|
|
691
|
+
// self-command are invalid shmakk commands. Don't send them to the
|
|
692
|
+
// correction engine — the user was explicitly addressing shmakk.
|
|
693
|
+
if (/^\//.test(lastCmd) || /^shmakk\s/i.test(lastCmd)) {
|
|
694
|
+
flushPending();
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
680
697
|
}
|
|
681
698
|
|
|
682
699
|
// Determine the command to feed forward.
|
package/src/shell.js
CHANGED
|
@@ -1,32 +1,52 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// Map shell name to the preferred executable path.
|
|
5
|
+
// Resolve's dctl paths on Arch-like systems can put bash in /usr/bin instead of /bin.
|
|
6
|
+
// Map shell name to candidate executable paths.
|
|
7
|
+
// Ordered by likelihood on the current platform; first existing path wins.
|
|
8
|
+
const SHELL_PATH_CANDIDATES = {
|
|
9
|
+
fish: ['/usr/bin/fish', '/opt/homebrew/bin/fish', '/usr/local/bin/fish', '/bin/fish'],
|
|
10
|
+
bash: ['/usr/bin/bash', '/bin/bash', '/opt/homebrew/bin/bash', '/usr/local/bin/bash'],
|
|
11
|
+
zsh: ['/usr/bin/zsh', '/bin/zsh', '/opt/homebrew/bin/zsh', '/usr/local/bin/zsh'],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function shellPath(name) {
|
|
15
|
+
// Name given explicitly (--shell flag): try known paths first, then PATH.
|
|
16
|
+
const candidates = SHELL_PATH_CANDIDATES[name];
|
|
17
|
+
if (candidates) {
|
|
18
|
+
for (const candidate of candidates) {
|
|
19
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Fall back to PATH search for the requested shell.
|
|
24
|
+
const { execSync } = require('child_process');
|
|
25
|
+
try {
|
|
26
|
+
const p = execSync(`command -v ${name}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
|
|
27
|
+
if (p && fs.existsSync(p)) return p;
|
|
28
|
+
} catch {}
|
|
29
|
+
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function detectShell(shellOverride) {
|
|
34
|
+
// Explicit --shell flag overrides everything.
|
|
35
|
+
if (shellOverride) {
|
|
36
|
+
const p = shellPath(shellOverride);
|
|
37
|
+
if (p) return { path: p, name: shellOverride };
|
|
38
|
+
process.stderr.write(`[shmakk] shell "${shellOverride}" not found, falling back to default\n`);
|
|
39
|
+
}
|
|
40
|
+
|
|
5
41
|
const env = process.env.SHELL;
|
|
6
42
|
if (env && fs.existsSync(env)) {
|
|
7
43
|
return { path: env, name: path.basename(env) };
|
|
8
44
|
}
|
|
9
|
-
const fallbacks = ['/bin/bash', '/usr/bin/bash', '/bin/sh'];
|
|
45
|
+
const fallbacks = ['/bin/bash', '/usr/bin/bash', '/opt/homebrew/bin/bash', '/usr/local/bin/bash', '/bin/sh'];
|
|
10
46
|
for (const f of fallbacks) {
|
|
11
47
|
if (fs.existsSync(f)) return { path: f, name: path.basename(f) };
|
|
12
48
|
}
|
|
13
49
|
return { path: '/bin/sh', name: 'sh' };
|
|
14
50
|
}
|
|
15
51
|
|
|
16
|
-
|
|
17
|
-
// Login + interactive so the user's normal init runs.
|
|
18
|
-
// We deliberately keep this minimal: do NOT inject rc files,
|
|
19
|
-
// do NOT alter prompt. Phase 2 will add hooks for command metadata.
|
|
20
|
-
switch (name) {
|
|
21
|
-
case 'fish':
|
|
22
|
-
return ['-i', '-l'];
|
|
23
|
-
case 'zsh':
|
|
24
|
-
return ['-i', '-l'];
|
|
25
|
-
case 'bash':
|
|
26
|
-
return ['-i', '-l'];
|
|
27
|
-
default:
|
|
28
|
-
return ['-i'];
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
module.exports = { detectShell, shellArgs };
|
|
52
|
+
module.exports = { detectShell, shellPath };
|
package/src/system-prompt.js
CHANGED
|
@@ -15,6 +15,7 @@ function buildSystemPrompt({
|
|
|
15
15
|
mcpToolHint = null,
|
|
16
16
|
userRulesText = null,
|
|
17
17
|
userMemoryText = null,
|
|
18
|
+
supportsVision = false,
|
|
18
19
|
}) {
|
|
19
20
|
return `You are an expert AI coding assistant running inside shmakk.
|
|
20
21
|
|
|
@@ -65,12 +66,13 @@ Tool Call Format:
|
|
|
65
66
|
|
|
66
67
|
Available Tools:
|
|
67
68
|
- list_dir: list files/directories
|
|
68
|
-
- read_file: read file contents
|
|
69
|
+
- read_file: read file contents (text or images)
|
|
69
70
|
- write_file: create or overwrite a file
|
|
70
71
|
- make_dir: create a directory
|
|
71
72
|
- run: execute shell commands
|
|
72
73
|
- web_search: search the web
|
|
73
74
|
- fetch_url: fetch a URL
|
|
75
|
+
${supportsVision ? '\nIf images are included in tool results (e.g. read_file on a PNG, or MCP tools that return visuals), you can describe and analyze what you see in them.\n' : ''}
|
|
74
76
|
|
|
75
77
|
Path Rules:
|
|
76
78
|
- Always use relative paths resolved against \`${roots[0]}\`.
|
package/src/tools.js
CHANGED
|
@@ -81,7 +81,7 @@ function within(roots, p) {
|
|
|
81
81
|
const TOOLS = [
|
|
82
82
|
{ type: 'function', function: {
|
|
83
83
|
name: 'read_file',
|
|
84
|
-
description: 'Read a
|
|
84
|
+
description: 'Read a file inside the workspace. Text files support partial reads (head, tail, grep, imports, exports, symbol). Image files (.png/.jpg/.gif/.webp/.bmp/.svg) are returned as base64 for vision analysis.',
|
|
85
85
|
parameters: {
|
|
86
86
|
type: 'object',
|
|
87
87
|
required: ['path'],
|
|
@@ -397,6 +397,31 @@ async function dispatchTool(name, args, roots, confirmTool, signal, mcpManager)
|
|
|
397
397
|
if (!p) return { error: 'path outside workspace' };
|
|
398
398
|
try {
|
|
399
399
|
const buf = fs.readFileSync(p);
|
|
400
|
+
const ext = path.extname(p).toLowerCase();
|
|
401
|
+
|
|
402
|
+
// Image files: return as base64 for vision-capable providers.
|
|
403
|
+
// Mode-specific sub-reads don't apply to images — always return full.
|
|
404
|
+
const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg']);
|
|
405
|
+
const MIME_MAP = {
|
|
406
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
407
|
+
'.gif': 'image/gif', '.webp': 'image/webp', '.bmp': 'image/bmp',
|
|
408
|
+
'.svg': 'image/svg+xml',
|
|
409
|
+
};
|
|
410
|
+
if (IMAGE_EXTS.has(ext)) {
|
|
411
|
+
const maxImageBytes = 2 * 1024 * 1024; // 2 MB binary (~2.7 MB base64)
|
|
412
|
+
const slice = buf.length > maxImageBytes ? buf.subarray(0, maxImageBytes) : buf;
|
|
413
|
+
const b64 = slice.toString('base64');
|
|
414
|
+
return {
|
|
415
|
+
content: `[Image: ${path.basename(p)} — ${buf.length} bytes${buf.length > maxImageBytes ? ' (truncated to 2MB for display)' : ''}]`,
|
|
416
|
+
images: [{
|
|
417
|
+
mimeType: MIME_MAP[ext],
|
|
418
|
+
data: b64,
|
|
419
|
+
dataLength: b64.length,
|
|
420
|
+
truncated: buf.length > maxImageBytes,
|
|
421
|
+
}],
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
400
425
|
const text = buf.slice(0, MAX_FILE_BYTES).toString('utf8');
|
|
401
426
|
const lines = text.split(/\r?\n/);
|
|
402
427
|
const mode = args.mode || 'full';
|