shmakk 1.2.0 → 1.2.1

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/src/agent.js CHANGED
@@ -7,13 +7,23 @@
7
7
 
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
- const { makeClient, modelFor, isConfigured } = require('./llm');
10
+ const { makeClient, modelFor, isConfigured, getDeepSeekOptions } = require('./llm');
11
+ const {
12
+ sanitizeAssistantContent,
13
+ isLeakedToolMarkup,
14
+ mightBecomeInternalMarkup,
15
+ MUTATION_TOOLS,
16
+ isMutationTool,
17
+ hashArgs,
18
+ DSML_RETRY_USER_MESSAGE,
19
+ } = require('./guard');
11
20
  const { buildOrRefreshIndex, relevantSubgraph } = require('./workspace-index');
12
21
  const { renderActiveSkillForPrompt } = require('./skills');
13
22
  const { renderRulesForPrompt } = require('./rules');
14
23
  const { renderMemoryForPrompt } = require('./memory');
15
24
  const sessionSearch = require('./session-search');
16
25
  const promptCache = require('./prompt-cache');
26
+ const audit = require('./audit');
17
27
  const { buildSystemPrompt } = require('./system-prompt');
18
28
  const {
19
29
  TOOLS,
@@ -74,6 +84,8 @@ function clearTaskJournal(root) {
74
84
  try { fs.rmSync(journalPath(root), { force: true }); } catch {}
75
85
  }
76
86
 
87
+ const { renderBlock } = require('./markdown');
88
+
77
89
  // Tiny spinner so the user sees "the agent is thinking" while we wait on
78
90
  // the model. Erased when stop() is called.
79
91
  function startSpinner(write, label = 'thinking') {
@@ -94,19 +106,9 @@ function startSpinner(write, label = 'thinking') {
94
106
 
95
107
  function dim(s, enabled = true) { return enabled ? `\x1b[2m${s}\x1b[0m` : s; }
96
108
 
97
- function highlightCodeBlocks(text, enabled = true) {
98
- const src = String(text || '');
99
- if (!enabled) return src;
100
- return src.replace(/```([a-zA-Z0-9_-]*)\n([\s\S]*?)```/g, (_m, lang, code) => {
101
- const head = `\x1b[36m\x1b[1m${lang || 'code'}\x1b[0m`;
102
- const body = `\x1b[90m${code.replace(/\n$/, '')}\x1b[0m`;
103
- return `${head}\n${body}`;
104
- }).replace(/\u0000\u0000\u0000/g, '```');
105
- }
106
-
107
109
  // ── Main agent entry point ──────────────────────────────────────────────────
108
110
 
109
- async function runAgent({ input, roots, glossary, confirmTool, write, signal, history = [], profile = 'balanced', colors = true, voiceMode = false, specialistHint = null, mcpManager = null, requireToolUse = false }) {
111
+ async function runAgent({ input, roots, glossary, confirmTool, write, signal, history = [], profile = 'balanced', colors = true, markdown = true, voiceMode = false, specialistHint = null, mcpManager = null, requireToolUse = false }) {
110
112
  // roots: array of allowed workspace roots (first is the primary cwd).
111
113
  // history: prior conversation turns (assistant/user/tool). System prompt
112
114
  // is rebuilt fresh each call so the current cwd is always accurate.
@@ -140,6 +142,40 @@ async function runAgent({ input, roots, glossary, confirmTool, write, signal, hi
140
142
 
141
143
  persistJournal('running');
142
144
  const promptCacheEnabled = String(process.env.SHMAKK_PROMPT_CACHE || '1') !== '0';
145
+
146
+ // ── Per-turn mutation-approval state ───────────────────────────────────
147
+ // Every mutation tool call must be individually approved. If the user
148
+ // denies ANY mutation in a turn, ALL pending mutations from that turn are
149
+ // invalidated. This prevents the agent from executing an edit after the
150
+ // user said no.
151
+ const turnApprovals = new Map(); // toolCallId → { approved, argsHash, expiresAt }
152
+ let turnDenied = false;
153
+
154
+ function clearTurnApprovals() {
155
+ turnApprovals.clear();
156
+ turnDenied = false;
157
+ }
158
+
159
+ function wrapConfirmTool(baseConfirm) {
160
+ if (!baseConfirm) return null;
161
+ return async ({ name, args, safety, description }) => {
162
+ // If this turn has already been denied, reject all mutation tools.
163
+ if (turnDenied && isMutationTool(name)) {
164
+ return false;
165
+ }
166
+ const ok = await baseConfirm({ name, args, safety, description });
167
+ if (!ok && isMutationTool(name)) {
168
+ turnDenied = true;
169
+ // Invalidate any pre-approved mutation calls from this turn.
170
+ for (const [id, a] of turnApprovals) {
171
+ if (isMutationTool(a.toolName)) a.approved = false;
172
+ }
173
+ }
174
+ return ok;
175
+ };
176
+ }
177
+
178
+ const guardedConfirm = wrapConfirmTool(confirmTool);
143
179
  const maxDiscoveryCallsPerRound = Math.max(
144
180
  1,
145
181
  Number(process.env.SHMAKK_MAX_DISCOVERY_CALLS_PER_ROUND)
@@ -152,8 +188,44 @@ async function runAgent({ input, roots, glossary, confirmTool, write, signal, hi
152
188
  const graph = relevantSubgraph(idx, input, 12, 1);
153
189
  if (graph.length) {
154
190
  indexHint = `\n\nCompact relevant subgraph for this task:\n${graph.map((n) => `- ${n.path} [role=${n.role}] symbols=${n.symbols.slice(0, 4).join(', ') || '-'} edges=${n.edges.slice(0, 4).join(', ') || '-'} snippet=${(n.snippet || []).slice(0, 3).join(' | ') || '-'}`).join('\n')}\nStart with these files and their immediate dependencies before broad exploration. Prefer these snippet cues before reading full files.`;
191
+ } else {
192
+ // Fallback: no query hits — give the agent a top-level map so it can
193
+ // start exploring without waiting for the user to say "read the dir".
194
+ const files = idx.files || {};
195
+ const configHints = [];
196
+ const topDirs = new Set();
197
+ const topFiles = new Set();
198
+ const allKeys = Object.keys(files);
199
+ // Determine which top-level names are directories vs files by checking
200
+ // whether they appear as a prefix for deeper entries.
201
+ const hasSlash = new Map(); // topName -> true if dir, false if top-level file
202
+ for (const rel of allKeys) {
203
+ const top = rel.split('/')[0];
204
+ if (!top || top.startsWith('.') || top === 'node_modules') continue;
205
+ if (rel === top) {
206
+ // A top-level file (no / in path) — only mark if not already known as dir
207
+ if (!hasSlash.has(top)) hasSlash.set(top, false);
208
+ } else {
209
+ // e.g. "src/agent.js" — top="src", this is a directory
210
+ hasSlash.set(top, true);
211
+ }
212
+ const base = rel.split('/').pop();
213
+ if (base === 'package.json' || base === 'README.md' || base === 'tsconfig.json') {
214
+ const f = files[rel];
215
+ configHints.push(`- ${rel} [role=${f.role}] snippet=${(f.snippet || []).slice(0, 1).join(' | ') || '-'}`);
216
+ }
217
+ }
218
+ for (const [name, isDir] of hasSlash) {
219
+ if (isDir) topDirs.add(name); else topFiles.add(name);
220
+ }
221
+ const topLines = [];
222
+ if (topDirs.size) topLines.push(`Top-level dirs: ${[...topDirs].sort().join(', ')}`);
223
+ if (topFiles.size) topLines.push(`Top-level files: ${[...topFiles].sort().join(', ')}`);
224
+ indexHint = `\n\nWorkspace structure (no query hits — start by exploring relevant directories):\n${topLines.join('\n')}${configHints.length ? '\n' + configHints.join('\n') : ''}\nUse list_dir to explore further.`;
155
225
  }
156
- } catch {}
226
+ } catch (e) {
227
+ indexHint = `\n\nWorkspace index unavailable (${e.message || 'unknown error'}). Start with list_dir of the root directory to discover the project structure.`;
228
+ }
157
229
 
158
230
  // Build MCP tool hint for system prompt if MCP tools are available
159
231
  let mcpToolHint = null;
@@ -219,6 +291,7 @@ async function runAgent({ input, roots, glossary, confirmTool, write, signal, hi
219
291
 
220
292
  // Tool loop. Streams content as it arrives; prints each tool call.
221
293
  let producedAnything = false;
294
+ const runState = { _dsmlLeakRetries: 0 };
222
295
  for (let i = 0; i < dynamicToolBudget; i++) {
223
296
  if (signal && signal.aborted) return messages.slice(1);
224
297
 
@@ -248,6 +321,7 @@ async function runAgent({ input, roots, glossary, confirmTool, write, signal, hi
248
321
  model: modelFor('agent'),
249
322
  messages, tools: allTools, tool_choice: toolChoiceForThisIter,
250
323
  temperature: 0, stream: true,
324
+ ...getDeepSeekOptions('tool_loop'),
251
325
  }, { signal });
252
326
  } catch (e) {
253
327
  stop();
@@ -258,12 +332,39 @@ async function runAgent({ input, roots, glossary, confirmTool, write, signal, hi
258
332
  let reasoningContent = '';
259
333
  const toolCalls = []; // [{id, type:'function', function:{name, arguments}}]
260
334
  let spinnerStopped = false;
335
+ let streamingContentOk = true; // flipped to false on leak
261
336
  try {
262
337
  for await (const chunk of stream) {
338
+ // Check for abort between chunks.
339
+ if (signal && signal.aborted) {
340
+ streamingContentOk = false;
341
+ break;
342
+ }
263
343
  const delta = chunk.choices?.[0]?.delta;
264
344
  if (!delta) continue;
265
345
  if (delta.content) {
266
- content += delta.content;
346
+ // ── Streaming guard: buffer tokens before flushing ──────────
347
+ // We never append tokens directly to visible chat state.
348
+ // Small lookbehind buffer so partial strings like "<||DSML"
349
+ // are not flushed before we can detect them.
350
+ const token = delta.content;
351
+ content += token;
352
+
353
+ // Check the trailing portion for partial DSML prefixes.
354
+ // Only check the last ~80 chars — enough to catch any prefix.
355
+ const tail = content.slice(-80);
356
+ if (mightBecomeInternalMarkup(tail)) {
357
+ // Hold back — don't flush yet, the next token may complete
358
+ // a benign string or reveal leaked markup.
359
+ continue;
360
+ }
361
+
362
+ // If we have accumulated content and there's no dangerous
363
+ // prefix in the tail, flush it now.
364
+ if (content.length > 0 && !mightBecomeInternalMarkup(content.slice(-80))) {
365
+ // Nothing to flush separately here — the spinner handles
366
+ // the "thinking" display. We just keep accumulating.
367
+ }
267
368
  }
268
369
  if (typeof delta.reasoning_content === 'string' && delta.reasoning_content.length) {
269
370
  reasoningContent += delta.reasoning_content;
@@ -284,6 +385,54 @@ async function runAgent({ input, roots, glossary, confirmTool, write, signal, hi
284
385
  if (!spinnerStopped) stop();
285
386
  }
286
387
 
388
+ // ── DSML leak detection (after stream completes) ────────────────────
389
+ if (content && isLeakedToolMarkup(content)) {
390
+ const sanitized = sanitizeAssistantContent(content);
391
+ // Log the leak but do NOT persist raw content.
392
+ audit.append({
393
+ kind: 'dsml-leak',
394
+ model: modelFor('agent'),
395
+ leakedMarkupDetected: true,
396
+ retried: false,
397
+ });
398
+
399
+ // If this is the first leak in this turn, retry once with safer settings.
400
+ // We inject a user message telling the model not to emit DSML and re-run
401
+ // the current iteration.
402
+ const RETRY_MAX = 1;
403
+ if (!runState._dsmlLeakRetries) runState._dsmlLeakRetries = 0;
404
+
405
+ if (runState._dsmlLeakRetries < RETRY_MAX) {
406
+ runState._dsmlLeakRetries += 1;
407
+
408
+ // Remove the last user message (the one that triggered this turn)
409
+ // and replace it with the retry instruction so we don't grow history.
410
+ // Find the last user message index.
411
+ let lastUserIdx = -1;
412
+ for (let mi = messages.length - 1; mi >= 0; mi--) {
413
+ if (messages[mi].role === 'user') { lastUserIdx = mi; break; }
414
+ }
415
+ if (lastUserIdx >= 0) {
416
+ messages.splice(lastUserIdx, 1);
417
+ }
418
+ messages.push(DSML_RETRY_USER_MESSAGE);
419
+
420
+ // Disable thinking for this retry.
421
+ process.env._SHMAKK_FORCE_NO_THINKING = '1';
422
+ i--; // re-spend this iteration
423
+ audit.append({ kind: 'dsml-leak-retry', model: modelFor('agent') });
424
+ continue; // re-enter the tool loop
425
+ }
426
+
427
+ // Max retries exceeded — strip and show what we can.
428
+ content = sanitized.visibleText;
429
+ if (!content) {
430
+ write(dim('[shmakk] response contained only leaked tool markup — blocked.', colors) + '\n');
431
+ clearTaskJournal(roots[0]);
432
+ return messages.slice(1);
433
+ }
434
+ }
435
+
287
436
  const fallbackActions = toolCalls.length ? [] : [
288
437
  ...parseFallbackActions(content),
289
438
  ...parseXmlFallbackActions(content),
@@ -329,7 +478,7 @@ async function runAgent({ input, roots, glossary, confirmTool, write, signal, hi
329
478
  // No tools → done.
330
479
  if (!normalizedToolCalls.length) {
331
480
  if (content) {
332
- write(highlightCodeBlocks(content, colors));
481
+ write(renderBlock(content, { enabled: markdown, colors }));
333
482
  if (!content.endsWith('\n')) write('\n');
334
483
  producedAnything = true;
335
484
  if (promptCacheEnabled && cacheKey) {
@@ -368,7 +517,7 @@ async function runAgent({ input, roots, glossary, confirmTool, write, signal, hi
368
517
  if (canUseCache && toolResultCache.has(cacheKey)) {
369
518
  result = toolResultCache.get(cacheKey);
370
519
  } else {
371
- result = await dispatchTool(c.function.name, args, roots, confirmTool, signal, mcpManager);
520
+ result = await dispatchTool(c.function.name, args, roots, guardedConfirm, signal, mcpManager);
372
521
  if (canUseCache && !result?.error) toolResultCache.set(cacheKey, result);
373
522
  iterProgress = true;
374
523
  }
@@ -419,11 +568,22 @@ async function runAgent({ input, roots, glossary, confirmTool, write, signal, hi
419
568
  temperature: 0,
420
569
  tool_choice: 'none',
421
570
  stream: false,
571
+ ...getDeepSeekOptions('tool_loop'),
422
572
  }, { signal });
423
573
  const finalText = final.choices?.[0]?.message?.content || '';
424
574
  if (finalText) {
425
- write(finalText);
426
- if (!finalText.endsWith('\n')) write('\n');
575
+ // ── DSML leak guard (same as the main loop) ──────────────────────
576
+ const finalSanitized = sanitizeAssistantContent(finalText);
577
+ const displayText = finalSanitized.hadInternalLeak
578
+ ? finalSanitized.visibleText
579
+ : finalText;
580
+ if (displayText) {
581
+ write(renderBlock(displayText, { enabled: markdown, colors }));
582
+ if (!displayText.endsWith('\n')) write('\n');
583
+ }
584
+ if (finalSanitized.hadInternalLeak) {
585
+ audit.append({ kind: 'dsml-leak-finalize', model: modelFor('agent'), leakedMarkupDetected: true });
586
+ }
427
587
  clearTaskJournal(roots[0]);
428
588
  return messages.slice(1);
429
589
  }
package/src/cli.js CHANGED
@@ -27,7 +27,9 @@ function parseArgs(argv) {
27
27
  profile: null,
28
28
  profileSet: null,
29
29
  colors: null,
30
+ markdown: null,
30
31
  endpoint: null,
32
+ modelRecommendation: false,
31
33
  voice: false,
32
34
  stt: false,
33
35
  tts: false,
@@ -39,6 +41,7 @@ function parseArgs(argv) {
39
41
  voiceSilenceStartSec: null,
40
42
  voicePadStartSec: null,
41
43
  ttsVoice: null,
44
+ notify: false,
42
45
  completion: null,
43
46
  unknown: [],
44
47
  };
@@ -93,9 +96,12 @@ function parseArgs(argv) {
93
96
  case '--voice-silence-start-sec': opts.voiceSilenceStartSec = argv[++i] || null; break;
94
97
  case '--voice-pad-start-sec': opts.voicePadStartSec = argv[++i] || null; break;
95
98
  case '--tts-voice': opts.ttsVoice = argv[++i] || null; break;
99
+ case '--notify': opts.notify = true; break;
96
100
  case '--completion': opts.completion = argv[++i] || null; break;
97
101
  case '--colors': opts.colors = argv[++i] || null; break;
102
+ case '--markdown': opts.markdown = argv[++i] || null; break;
98
103
  case '--endpoint': opts.endpoint = argv[++i] || null; break;
104
+ case '--model-recommendation': opts.modelRecommendation = true; break;
99
105
  default: opts.unknown.push(a);
100
106
  }
101
107
  }
@@ -104,95 +110,196 @@ function parseArgs(argv) {
104
110
 
105
111
  const HELP = `shmakk - AI-supervised terminal wrapper
106
112
 
107
- Usage:
108
- shmakk Launch in auto mode
109
- shmakk --review Launch in review mode (confirm every AI action)
110
- shmakk --yes-files Auto-accept AI file writes, edits, and directory creation
111
- shmakk --update-command-glossary
112
- Scan PATH and build local command glossary
113
- shmakk --help Show this help
114
- shmakk --build-history [files...]
115
- Parse shell history files and build command
116
- frequency map for better corrections.
117
- Auto-detects bash/zsh/fish history if no
118
- files given.
119
-
120
- Control (run from inside an shmakk session):
121
- shmakk --status Show whether this terminal is inside shmakk
122
- shmakk --stats Show session/task stats (journal, audit, active skill)
123
- shmakk --compact Compact context by clearing conversation + task journal
124
- shmakk --load-skill <name> Load a Claude/Codex-style skill into shmakk workspace state
125
- shmakk --install-skill <url> Download skill markdown from URL, validate, and load
126
- shmakk -G, --global With --load-skill or --install-skill, use global (~/.config/shmakk) instead of workspace
127
- shmakk --list-skills List all registered skills (workspace + global)
128
- shmakk --skill-status Show active skill and registry status (workspace + global)
129
- shmakk --unload-skill <name> Remove skill from whichever registry has it
130
- shmakk --show-plan Show current plan status (tasks and progress)
131
- shmakk --mcp-status Show configured MCP servers and their tools
132
- shmakk --resume-status Show task journal summary for resume continuity
133
- shmakk --exit Cleanly exit the parent shmakk
134
- shmakk --restart Restart the inner shell (preserves window)
135
- shmakk --reset Clear the AI conversation history (keep session)
136
- shmakk --profile-set <name> Switch profile and restart (tiny|balanced|deep|builder|large-app)
137
- shmakk --colors <true|false> Enable or disable ANSI colors + code highlighting
138
-
139
- Optional:
140
- --no-ai Disable AI entirely (pure passthrough)
141
- --no-correction Disable command correction
142
- --yes-files Auto-accept write_file, edit_file, and make_dir in auto mode
143
- --workspace <path> Override workspace root
144
- --profile <name> Startup profile: tiny|balanced|deep|builder|large-app
145
- --endpoint <name> Use endpoint preset from ~/.config/shmakk/endpoints.js
146
- --colors <true|false> Toggle colored logs and code-block highlighting
147
- --debug Verbose logging to stderr
148
- --print-config Print resolved configuration and exit
149
-
150
- Speech-to-Text / Text-to-Speech (VAD-based, no hotkeys):
151
- --sts Speech-to-Speech: always-on mic + TTS responses
152
- --stt Speech-to-Text: mic → text input (no TTS)
153
- --tts Text-to-Speech: text input → spoken responses
154
- --voice-language <code> Language hint (e.g., en, es, fr)
155
- --voice-max-sec <sec> Max recording duration (default: 30)
156
- --voice-silence-sec <sec> VAD silence before stopping (default: 1.0)
157
- --voice-silence-threshold <%> VAD amplitude threshold (default: 1%)
158
- --voice-silence-start-sec <sec> Seconds of sound before starting (default: 0.5)
159
- --voice-pad-start-sec <sec> Padding added to start of recording (default: 0.3)
160
- --tts-voice <name> Override rotated voice schedule (default: af_heart)
161
- --completion <bash|zsh|fish> Output shell tab-completion script
162
-
163
- Browser Automation:
164
- The agent has a built-in browser tool (navigate, click, type, read_page,
165
- screenshot, evaluate, select, wait, scroll, close). Requires playwright:
166
- npm install playwright && npx playwright install chromium
113
+ Launch shmakk, then type commands as usual. shmakk watches the shell, catches
114
+ failures, and calls an LLM to fix them, plan tasks, and edit files.
115
+
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
+ Type "help" inside a session to see this same text.
120
+
121
+ ═══════════════════════════════════════════════════════════════════════════
122
+ LAUNCH OPTIONS
123
+ ═══════════════════════════════════════════════════════════════════════════
124
+
125
+ shmakk Launch in auto mode (AI acts on failures)
126
+ shmakk --review Launch in review mode (confirm every AI action)
127
+ shmakk --yes-files Auto-accept file writes, edits, mkdir
128
+
129
+ shmakk --help Show this help
130
+ shmakk --build-history [files] Parse shell history for better corrections
131
+ shmakk --update-command-glossary Scan PATH and build local command glossary
132
+
133
+ --no-ai Disable AI entirely (pure passthrough)
134
+ --no-correction Disable command correction
135
+ --debug Verbose logging to stderr
136
+ --print-config Print resolved configuration and exit
137
+
138
+ --workspace <path> Override workspace root
139
+ --profile <name> Startup profile: tiny|balanced|deep|builder|large-app
140
+ --colors <true|false> Enable or disable ANSI colors
141
+ --markdown <true|false> Enable or disable markdown rendering
142
+ --notify Desktop notifications for Y/n prompts
143
+
144
+ ═══════════════════════════════════════════════════════════════════════════
145
+ MODEL PROVIDERS
146
+ ═══════════════════════════════════════════════════════════════════════════
147
+
148
+ --endpoint <name> Use model preset from ~/.config/shmakk/endpoints.json
149
+ --model-recommendation Main model chooses best model per call
150
+
151
+ Providers: openai-compatible | codex | anthropic | google
152
+ Configure in ~/.config/shmakk/endpoints.json:
153
+ {
154
+ "main": "claude",
155
+ "models": {
156
+ "claude": { "provider":"anthropic", "model":"claude-sonnet-4-5-...", "api_key":"..." },
157
+ "gpt5": { "provider":"codex", "model":"gpt-5-codex", "api_key":"..." },
158
+ "local-qwen": { "provider":"openai-compatible", "base_url":"http://127.0.0.1:1234/v1",
159
+ "model":"qwen/qwen3.5-9b" }
160
+ }
161
+ }
162
+
163
+ ═══════════════════════════════════════════════════════════════════════════
164
+ SESSION CONTROL (shmakk --flag from another terminal)
165
+ ═══════════════════════════════════════════════════════════════════════════
166
+
167
+ shmakk --status Is this terminal inside shmakk?
168
+ shmakk --stats Session/task stats (journal, audit, skill)
169
+ shmakk --show-plan Current plan: tasks and progress
170
+ shmakk --resume-status Task journal summary for continuity
171
+ shmakk --mcp-status MCP servers and their tools
172
+
173
+ shmakk --compact Clear conversation + task journal
174
+ shmakk --reset Clear AI conversation history (keep session)
175
+ shmakk --restart Restart the inner shell (keeps window)
176
+ shmakk --exit Cleanly exit the parent shmakk
177
+
178
+ shmakk --profile-set <name> Switch profile and restart
179
+
180
+ shmakk --load-skill <name> Load a skill into workspace state
181
+ shmakk --install-skill <url> Download skill markdown from URL, validate, load
182
+ shmakk -G, --global Use global (~/.config/shmakk) with --load-skill / --install-skill
183
+ shmakk --list-skills List all registered skills (workspace + global)
184
+ shmakk --skill-status Active skill and registry status
185
+ shmakk --unload-skill <name> Remove skill from whichever registry has it
186
+
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
167
199
 
168
- MCP (Model Context Protocol):
169
- shmakk --mcp-status Show configured MCP servers and their tools
170
- Configure in ~/.config/shmakk/mcp.json or .shmakk/mcp.json:
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
+ ═══════════════════════════════════════════════════════════════════════════
251
+ VOICE (Speech-to-Text / Text-to-Speech)
252
+ ═══════════════════════════════════════════════════════════════════════════
253
+
254
+ --sts Speech-to-Speech: always-on mic + TTS
255
+ --stt Speech-to-Text: mic input, text output
256
+ --tts Text-to-Speech: text input, spoken output
257
+
258
+ --voice-language <code> Language hint (e.g. en, es, fr)
259
+ --voice-max-sec <sec> Max recording seconds (default: 30)
260
+ --voice-silence-sec <sec> Silence before stopping (default: 1.0)
261
+ --voice-silence-threshold <%> VAD amplitude threshold (default: 1%)
262
+ --voice-silence-start-sec <sec> Sound required before start (default: 0.5)
263
+ --voice-pad-start-sec <sec> Padding at start of recording (default: 0.3)
264
+ --tts-voice <name> Override daily voice rotation
265
+
266
+ STT: Whisper-base ONNX in-process. No Python, no server, no API key.
267
+ TTS: kokoro-js (Kokoro-82M ONNX, ~334MB fp16). Auto-download on first use.
268
+ Requires aplay, paplay, or afplay for audio. 28 voices, rotated daily.
269
+
270
+ ═══════════════════════════════════════════════════════════════════════════
271
+ ENVIRONMENT
272
+ ═══════════════════════════════════════════════════════════════════════════
273
+
274
+ SHMAKK_BASE_URL OpenAI-compatible base URL
275
+ SHMAKK_API_KEY API key
276
+ SHMAKK_MODEL Default model
277
+ SHMAKK_PROVIDER Provider: openai-compatible|codex|anthropic|google
278
+ SHMAKK_HEADERS Extra headers: k=v,k=v
279
+ SHMAKK_REGISTRY Model registry filter (comma-separated)
280
+ SHMAKK_MODEL_RECOMMENDATION Set to 1 to let main model choose per call
281
+
282
+ SHMAKK_HF_CACHE HuggingFace cache directory (voice models)
283
+ SHMAKK_TTS_VOICE Pin a specific TTS voice
284
+ SHMAKK_TTS_DTYPE Kokoro dtype: fp32|fp16|q8|q4|q4f16 (default: fp16)
285
+ SHMAKK_VOICE_LANGUAGE Language hint for STT
286
+ SHMAKK_VOICE_MAX_SEC Max recording seconds
287
+ SHMAKK_VOICE_SILENCE_SEC VAD silence threshold seconds
288
+ SHMAKK_VOICE_SILENCE_THRESHOLD VAD amplitude threshold
289
+ SHMAKK_VOICE_PAD_START_SEC Start-of-recording padding
290
+
291
+ ═══════════════════════════════════════════════════════════════════════════
292
+ MCP & BROWSER
293
+ ═══════════════════════════════════════════════════════════════════════════
294
+
295
+ MCP servers: configure in ~/.config/shmakk/mcp.json or .shmakk/mcp.json
171
296
  { "mcpServers": { "name": { "command": "...", "args": [...] } } }
172
297
 
173
- Voice uses Whisper-base ONNX in-process. No Python, no server, no API key.
174
- Model auto-downloads on first use.
175
-
176
- TTS uses kokoro-js (Kokoro-82M ONNX, ~334MB fp16). Model auto-downloads on first use.
177
- Requires: aplay, paplay, or afplay for audio playback.
178
- All 28 Kokoro voices rotate automatically on a daily schedule.
179
-
180
- Voice environment:
181
- SHMAKK_HF_CACHE HuggingFace cache directory override
182
- SHMAKK_TTS_VOICE Pin a specific TTS voice (default: auto-rotated)
183
- SHMAKK_TTS_DTYPE Kokoro dtype: fp32, fp16, q8, q4, q4f16 (default: fp16)
184
- SHMAKK_VOICE_LANGUAGE Language hint for STT (e.g., en, es, fr)
185
- SHMAKK_VOICE_MAX_SEC Max recording seconds (default: 30)
186
- SHMAKK_VOICE_SILENCE_SEC VAD silence threshold seconds (default: 1.0)
187
- SHMAKK_VOICE_SILENCE_THRESHOLD VAD amplitude threshold (default: 1%)
188
- SHMAKK_VOICE_PAD_START_SEC Padding added to start of recording (default: 0.3)
189
-
190
- Environment:
191
- SHMAKK_BASE_URL OpenAI-compatible base URL
192
- SHMAKK_API_KEY API key
193
- SHMAKK_MODEL Default model
194
- SHMAKK_HEADERS Comma-separated extra headers (k=v,k=v)
195
- SHMAKK_REGISTRY Comma-separated model registry filter (for makkorch)
298
+ Browser automation: requires playwright
299
+ npm install playwright && npx playwright install chromium
300
+ Tools: navigate, click, type, read_page, screenshot, evaluate, select,
301
+ wait, scroll, close.
302
+
196
303
  `;
197
304
 
198
305
  module.exports = { parseArgs, HELP };
@@ -33,12 +33,13 @@ const FLAGS = [
33
33
  { flag: '--tts', arg: false, desc: 'Text-to-Speech: spoken responses' },
34
34
  { flag: '--sts', arg: false, desc: 'Speech-to-Speech: always-on mic + TTS' },
35
35
  { flag: '--voice', arg: false, desc: 'Enable voice input (stt shortcut)' },
36
+ { flag: '--model-recommendation', arg: false, desc: 'Route each model call via main model recommendation' },
36
37
 
37
38
  // flags with arguments
38
39
  { flag: '--workspace', arg: '<path>', desc: 'Override workspace root' },
39
40
  { flag: '--profile', arg: '<name>', desc: 'Startup profile (tiny|balanced|deep|builder|large-app)' },
40
41
  { flag: '--profile-set', arg: '<name>', desc: 'Switch profile and restart' },
41
- { flag: '--endpoint', arg: '<name>', desc: 'Use endpoint preset from ~/.config/shmakk/endpoints.js' },
42
+ { flag: '--endpoint', arg: '<name>', desc: 'Use model preset from ~/.config/shmakk/endpoints.json' },
42
43
  { flag: '--colors', arg: '<true|false>', desc: 'Toggle ANSI colors' },
43
44
  { flag: '--load-skill', arg: '<name>', desc: 'Load a skill into workspace state' },
44
45
  { flag: '--unload-skill', arg: '<name>', desc: 'Remove skill from registry' },
@@ -53,6 +54,7 @@ const FLAGS = [
53
54
  { flag: '--voice-silence-start-sec', arg: '<sec>', desc: 'Sound before recording starts' },
54
55
  { flag: '--voice-pad-start-sec', arg: '<sec>', desc: 'Padding before recording' },
55
56
  { flag: '--tts-voice', arg: '<name>', desc: 'Override Kokoro voice' },
57
+ { flag: '--notify', arg: false, desc: 'Send desktop notifications when shmakk needs your attention' },
56
58
  ];
57
59
 
58
60
  function bash() {