shmakk 1.2.0 → 1.2.2

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.
@@ -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() {
package/src/correction.js CHANGED
@@ -27,12 +27,19 @@ function levenshtein(a, b) {
27
27
 
28
28
  // Natural-language pre-filter. If the input reads like a sentence or question,
29
29
  // skip correction entirely and route to the task agent.
30
- const NL_WORDS = new RegExp(
31
- '\\b(' +
30
+ // Only check the FIRST token — if it looks like a natural-language opener
31
+ // ("Fix ...", "I ...", "Please ...") skip correction. This avoids matching
32
+ // common action words that appear as command arguments (e.g. "install" in
33
+ // "npm install").
34
+ const NL_FIRST_WORD = new RegExp(
35
+ '^(' +
32
36
  'I|me|my|you|your|the|this|that|these|those|a|an|is|are|was|were|do|does|did|' +
33
37
  'can|could|would|should|please|why|what|how|where|when|who|which|' +
34
38
  'fix|tell|show|explain|help|find|look|check|run|make|build|install|create|update|' +
35
- 'add|remove|delete|change|setup|set\\s+up|debug' +
39
+ 'add|remove|delete|change|setup|debug|' +
40
+ 'hello|hi|hey|ok|okay|sure|yes|no|maybe|thanks|thank|sorry|' +
41
+ 'let|lets|shall|will|might|must|need|want|got|get|give|take|' +
42
+ 'here|there|now|then|just|also|only|very|really' +
36
43
  ')\\b',
37
44
  'i'
38
45
  );
@@ -43,7 +50,7 @@ function looksLikeNaturalLanguage(input) {
43
50
  if (trimmed.includes('?')) return true;
44
51
  const tokens = trimmed.split(/\s+/);
45
52
  if (tokens.length > 5) return true;
46
- if (tokens.length > 2 && NL_WORDS.test(trimmed)) return true;
53
+ if (tokens.length >= 2 && NL_FIRST_WORD.test(trimmed)) return true;
47
54
  return false;
48
55
  }
49
56
 
package/src/endpoints.js CHANGED
@@ -1,15 +1,26 @@
1
- // Named endpoint presets with hotswap support.
2
- // Loads ~/.config/shmakk/endpoints.js (or .json for backwards compat).
3
- // Can switch endpoints mid-session without restarting.
1
+ // Named model endpoints with hotswap support.
2
+ // Loads ~/.config/shmakk/endpoints.json (or .js for backwards compat).
3
+ // Can switch models mid-session without restarting.
4
4
  //
5
- // Format (~/.config/shmakk/endpoints.js):
5
+ // Preferred format (~/.config/shmakk/endpoints.json):
6
6
  // {
7
- // "makkorch": {
8
- // "base_url": "https://api.example.com/v1",
9
- // "api_key": "sk-...",
10
- // "model": "gpt-4o-mini",
11
- // "headers": "x-custom=value",
12
- // "registry": "claudeHaiku,claudeSonnet,ministral"
7
+ // "main": "gpt-5",
8
+ // "models": {
9
+ // "gpt-5": {
10
+ // "provider": "codex",
11
+ // "model": "gpt-5-codex",
12
+ // "api_key": "sk-..."
13
+ // },
14
+ // "claude": {
15
+ // "provider": "anthropic",
16
+ // "model": "claude-sonnet-4-5-20250929",
17
+ // "api_key": "sk-ant-..."
18
+ // },
19
+ // "local": {
20
+ // "provider": "openai-compatible",
21
+ // "base_url": "http://127.0.0.1:1234/v1",
22
+ // "model": "qwen/qwen3.5-9b"
23
+ // }
13
24
  // }
14
25
  // }
15
26
 
@@ -23,13 +34,13 @@ let endpointsCwd = null;
23
34
 
24
35
  function configPath(cwd) {
25
36
  const configDir = path.join(os.homedir(), '.config', 'shmakk');
26
- const jsPath = path.join(configDir, 'endpoints.js');
27
37
  const jsonPath = path.join(configDir, 'endpoints.json');
38
+ const jsPath = path.join(configDir, 'endpoints.js');
28
39
 
29
- // Try .js first, fall back to .json for backwards compatibility
30
- if (fs.existsSync(jsPath)) return jsPath;
40
+ // Prefer JSON for user-editable model registries; keep .js compatibility.
31
41
  if (fs.existsSync(jsonPath)) return jsonPath;
32
- return jsPath; // Default to .js even if neither exists
42
+ if (fs.existsSync(jsPath)) return jsPath;
43
+ return jsonPath;
33
44
  }
34
45
 
35
46
  function loadEndpoints(cwd) {
@@ -49,26 +60,71 @@ function loadEndpoints(cwd) {
49
60
  }
50
61
  }
51
62
 
52
- function applyEndpoint(name, cwd) {
53
- const endpoints = loadEndpoints(cwd);
54
- if (!endpoints || !endpoints[name]) return false;
55
-
56
- const cfg = endpoints[name];
57
- currentEndpointName = name;
58
-
59
- // Normalize: accept both camelCase and snake_case
60
- const normalized = {
61
- base_url: cfg.base_url || cfg.baseUrl,
62
- api_key: cfg.api_key || cfg.apiKey,
63
- model: cfg.model,
64
- headers: cfg.headers,
65
- registry: cfg.registry,
63
+ function normalizeModelConfig(name, cfg) {
64
+ if (!cfg || typeof cfg !== 'object') return null;
65
+
66
+ const provider = String(cfg.provider || cfg.type || 'openai-compatible').toLowerCase();
67
+ return {
68
+ name,
69
+ provider,
70
+ base_url: cfg.base_url || cfg.baseUrl || cfg.host || cfg.url || null,
71
+ api_key: cfg.api_key || cfg.apiKey || cfg.key || null,
72
+ model: cfg.model || name,
73
+ headers: cfg.headers || cfg.headears || null,
74
+ registry: cfg.registry || null,
75
+ main: !!cfg.main,
66
76
  };
77
+ }
78
+
79
+ function normalizeRegistry(raw) {
80
+ if (!raw || typeof raw !== 'object') {
81
+ return { main: null, models: {} };
82
+ }
83
+
84
+ const explicitModels = raw.models || raw.endpoints;
85
+ const source = explicitModels && typeof explicitModels === 'object'
86
+ ? explicitModels
87
+ : Object.fromEntries(
88
+ Object.entries(raw).filter(([key, value]) => {
89
+ return key !== 'main' && value && typeof value === 'object' && !Array.isArray(value);
90
+ }),
91
+ );
92
+
93
+ const models = {};
94
+ for (const [name, cfg] of Object.entries(source)) {
95
+ const normalized = normalizeModelConfig(name, cfg);
96
+ if (normalized) models[name] = normalized;
97
+ }
98
+
99
+ let main = typeof raw.main === 'string' ? raw.main : null;
100
+ if (!main) {
101
+ const marked = Object.values(models).find((cfg) => cfg.main);
102
+ if (marked) main = marked.name;
103
+ }
104
+ if (!main && Object.keys(models).length === 1) {
105
+ main = Object.keys(models)[0];
106
+ }
107
+
108
+ return { main, models };
109
+ }
110
+
111
+ function loadModelRegistry(cwd) {
112
+ return normalizeRegistry(loadEndpoints(cwd));
113
+ }
114
+
115
+ function applyEndpoint(name, cwd) {
116
+ const registry = loadModelRegistry(cwd);
117
+ const selected = name === 'main' ? registry.main : name;
118
+ if (!selected || !registry.models[selected]) return false;
119
+
120
+ const normalized = registry.models[selected];
121
+ currentEndpointName = selected;
67
122
 
68
123
  currentEndpointConfig = normalized;
69
124
  endpointsCwd = cwd;
70
125
 
71
126
  // Also update env vars for backwards compatibility
127
+ if (normalized.provider) process.env.SHMAKK_PROVIDER = normalized.provider;
72
128
  if (normalized.base_url) process.env.SHMAKK_BASE_URL = normalized.base_url;
73
129
  if (normalized.api_key) process.env.SHMAKK_API_KEY = normalized.api_key;
74
130
  if (normalized.model) process.env.SHMAKK_MODEL = normalized.model;
@@ -88,9 +144,15 @@ function getCurrentEndpointName() {
88
144
  }
89
145
 
90
146
  function listEndpoints(cwd) {
91
- const endpoints = loadEndpoints(cwd);
92
- if (!endpoints) return [];
93
- return Object.keys(endpoints);
147
+ return Object.keys(loadModelRegistry(cwd).models);
148
+ }
149
+
150
+ function getModelRegistry(cwd) {
151
+ const registry = loadModelRegistry(cwd || endpointsCwd || process.cwd());
152
+ return {
153
+ main: registry.main,
154
+ models: Object.fromEntries(Object.entries(registry.models).map(([name, cfg]) => [name, { ...cfg }])),
155
+ };
94
156
  }
95
157
 
96
158
  module.exports = {
@@ -98,4 +160,5 @@ module.exports = {
98
160
  listEndpoints,
99
161
  getCurrentEndpoint,
100
162
  getCurrentEndpointName,
163
+ getModelRegistry,
101
164
  };
package/src/guard.js ADDED
@@ -0,0 +1,101 @@
1
+ // DSML leak detection, sanitization, streaming guard, and mutation-tool
2
+ // approval helpers. Used by agent.js and tools.js.
3
+ //
4
+ // See BUGFUXPAND.md for the full rationale — this module is the runtime
5
+ // enforcement of every item in that document.
6
+
7
+ // ── DSML leak detection ─────────────────────────────────────────────────────
8
+
9
+ // DSML uses fullwidth vertical bars (U+FF5C): <|DSML|tool_calls>
10
+ // Also catches double-bar variants and <function=name> format.
11
+ const DSML_LEAK_RE =
12
+ /<(?:\s*\/?\s*(?:[||]+\s*(?:DSML\s*)?[||]+\s*(?:tool_calls|invoke|parameter)\b|[||]+(?:\s*tool_calls|\s*invoke|\s*parameter)\b)|\s*function\s*=\s*[a-zA-Z0-9_]+)/i;
13
+
14
+ const DSML_TOOL_CALL_BLOCK_RE =
15
+ /<(?:\s*[||]+\s*(?:DSML\s*)?[||]+\s*tool_calls\s*>|\s*[||]+\s*tool_calls\s*[||]*\s*>)[\s\S]*?<\s*\/\s*(?:[||]+\s*(?:DSML\s*)?[||]+|[||]+)\s*tool_calls\s*[||]*\s*>/gi;
16
+
17
+ /** Returns true when visible assistant text contains leaked internal tool
18
+ * markup that should never reach the user. */
19
+ function isLeakedToolMarkup(text) {
20
+ return DSML_LEAK_RE.test(text);
21
+ }
22
+
23
+ /** Remove complete leaked DSML blocks so remaining visible text can be used
24
+ * if we decide to strip rather than block. */
25
+ function stripInternalToolMarkup(text) {
26
+ return text.replace(DSML_TOOL_CALL_BLOCK_RE, "").trim();
27
+ }
28
+
29
+ /** One-stop sanitizer: returns clean visible text + a leak flag. */
30
+ function sanitizeAssistantContent(raw) {
31
+ const hadInternalLeak = isLeakedToolMarkup(raw);
32
+ const visibleText = stripInternalToolMarkup(raw);
33
+ return { visibleText, hadInternalLeak };
34
+ }
35
+
36
+ // ── Streaming guard: lookbehind buffer ──────────────────────────────────────
37
+
38
+ const PARTIAL_INTERNAL_PREFIXES = [
39
+ "<|",
40
+ "<|",
41
+ "<||",
42
+ "<||",
43
+ "<||DSML",
44
+ "<||DSML",
45
+ ];
46
+
47
+ /** Returns true when `tail` could be the beginning of an internal markup
48
+ * string that hasn't finished streaming yet. */
49
+ function mightBecomeInternalMarkup(tail) {
50
+ return PARTIAL_INTERNAL_PREFIXES.some(
51
+ (prefix) => tail.endsWith(prefix) || prefix.startsWith(tail),
52
+ );
53
+ }
54
+
55
+ // ── Mutation tool classification ───────────────────────────────────────────
56
+
57
+ const MUTATION_TOOLS = new Set([
58
+ "edit_file",
59
+ "write_file",
60
+ "delete_file",
61
+ "make_dir",
62
+ "run",
63
+ ]);
64
+
65
+ /** True when the named tool mutates the workspace or runs an external process. */
66
+ function isMutationTool(name) {
67
+ return MUTATION_TOOLS.has(name);
68
+ }
69
+
70
+ // ── Args hashing (for approval validation) ──────────────────────────────────
71
+
72
+ const crypto = require("crypto");
73
+
74
+ function hashArgs(args) {
75
+ const raw = typeof args === "string" ? args : JSON.stringify(args || {});
76
+ return crypto.createHash("sha256").update(raw).digest("hex").slice(0, 16);
77
+ }
78
+
79
+ // ── Retry message ───────────────────────────────────────────────────────────
80
+
81
+ const DSML_RETRY_USER_MESSAGE = {
82
+ role: "user",
83
+ content:
84
+ "Your previous response emitted internal tool markup as visible text. " +
85
+ "Do not print DSML/XML/tool markup. Use only native structured tool " +
86
+ "calls or normal user-visible text.",
87
+ };
88
+
89
+ module.exports = {
90
+ DSML_LEAK_RE,
91
+ DSML_TOOL_CALL_BLOCK_RE,
92
+ PARTIAL_INTERNAL_PREFIXES,
93
+ isLeakedToolMarkup,
94
+ stripInternalToolMarkup,
95
+ sanitizeAssistantContent,
96
+ mightBecomeInternalMarkup,
97
+ MUTATION_TOOLS,
98
+ isMutationTool,
99
+ hashArgs,
100
+ DSML_RETRY_USER_MESSAGE,
101
+ };
package/src/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const { parseArgs, HELP } = require('./cli');
2
2
  const { normalizeProfile, resolveProfile } = require('./profiles');
3
3
  const { applyEndpoint, getCurrentEndpoint, getCurrentEndpointName } = require('./endpoints');
4
- const { ensureMakkorch } = require('./llm');
4
+ const { ensureModelRuntime } = require('./llm');
5
5
  const { spawn } = require('child_process');
6
6
  const path = require('path');
7
7
  const fs = require('fs');
@@ -33,15 +33,18 @@ async function main() {
33
33
  }
34
34
  }
35
35
 
36
- // Apply named endpoint preset from ~/.config/shmakk/endpoints.js before
36
+ if (opts.modelRecommendation) {
37
+ process.env.SHMAKK_MODEL_RECOMMENDATION = '1';
38
+ }
39
+
40
+ // Apply named model preset from ~/.config/shmakk/endpoints.json before
37
41
  // any other module reads SHMAKK_* environment variables.
38
42
  if (opts.endpoint) {
39
- const configDir = path.join(require('os').homedir(), '.config', 'shmakk', 'endpoints.js');
43
+ const configDir = path.join(require('os').homedir(), '.config', 'shmakk', 'endpoints.json');
40
44
  if (!applyEndpoint(opts.endpoint, opts.workspace || process.cwd())) {
41
45
  process.stderr.write(`[shmakk] endpoint "${opts.endpoint}" not found in ${configDir}\n`);
42
46
  }
43
- // Auto-start makkorch if needed
44
- await ensureMakkorch();
47
+ await ensureModelRuntime();
45
48
  }
46
49
 
47
50
  if (opts.colors !== null) {
@@ -53,6 +56,15 @@ async function main() {
53
56
  opts.colors = v === 'true';
54
57
  }
55
58
 
59
+ if (opts.markdown !== null) {
60
+ const v = String(opts.markdown).toLowerCase();
61
+ if (v !== 'true' && v !== 'false') {
62
+ process.stderr.write('[shmakk] invalid --markdown. Use: true|false\n');
63
+ process.exit(2);
64
+ }
65
+ opts.markdown = v === 'true';
66
+ }
67
+
56
68
  if (opts.help) {
57
69
  process.stdout.write(HELP);
58
70
  process.exit(0);
@@ -86,6 +98,8 @@ async function main() {
86
98
  baseUrl: activeEndpoint?.base_url || process.env.SHMAKK_BASE_URL || null,
87
99
  apiKey: activeEndpoint?.api_key ? (activeEndpoint.api_key.slice(0, 8) + '...' + activeEndpoint.api_key.slice(-4)) : null,
88
100
  model: activeEndpoint?.model || process.env.SHMAKK_MODEL || null,
101
+ provider: activeEndpoint?.provider || process.env.SHMAKK_PROVIDER || null,
102
+ modelRecommendation: process.env.SHMAKK_MODEL_RECOMMENDATION === '1',
89
103
  registry: activeEndpoint?.registry || process.env.SHMAKK_REGISTRY || null,
90
104
  profile: profile.name,
91
105
  colors: opts.colors,