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/README.md +28 -2
- package/package.json +2 -2
- package/scripts/demo/record.py +196 -0
- package/scripts/demo/scenes.html +913 -0
- package/skills/media-video-compose.md +320 -0
- package/skills/media-video-script.md +204 -0
- package/skills/media-video-voice.md +184 -0
- package/src/agent-overview.js +320 -0
- package/src/agent-roster.js +53 -0
- package/src/agent.js +178 -18
- package/src/cli.js +193 -86
- package/src/completions.js +3 -1
- package/src/correction.js +11 -4
- package/src/endpoints.js +94 -31
- package/src/guard.js +101 -0
- package/src/index.js +19 -5
- package/src/llm.js +462 -52
- package/src/markdown.js +217 -0
- package/src/notify.js +34 -0
- package/src/pty.js +1 -1
- package/src/review.js +8 -1
- package/src/self-commands.js +108 -2
- package/src/session.js +58 -2
- package/src/subagent.js +12 -1
- package/src/taskClassifier.js +2 -2
- package/src/team.js +22 -0
- package/src/tools.js +408 -1
- package/src/workflows.js +32 -0
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
|
-
|
|
31
|
-
|
|
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|
|
|
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
|
|
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
|
|
2
|
-
// Loads ~/.config/shmakk/endpoints.
|
|
3
|
-
// Can switch
|
|
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
|
-
//
|
|
5
|
+
// Preferred format (~/.config/shmakk/endpoints.json):
|
|
6
6
|
// {
|
|
7
|
-
// "
|
|
8
|
-
//
|
|
9
|
-
// "
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
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
|
-
//
|
|
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;
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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,
|