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.
- package/README.md +68 -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 +220 -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/ssh.js +255 -0
- package/src/subagent.js +12 -1
- package/src/taskClassifier.js +2 -2
- package/src/team.js +22 -0
- package/src/tools.js +487 -1
- package/src/workflows.js +32 -0
package/src/ssh.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// SSH remote execution and file transfer for shmakk.
|
|
2
|
+
//
|
|
3
|
+
// Hosts are defined in .shmakk/hosts.json (project-local) or
|
|
4
|
+
// ~/.config/shmakk/hosts.json (global). If a host config has
|
|
5
|
+
// allow_ssh_config: true, ~/.ssh/config Host entries are also
|
|
6
|
+
// available as targets.
|
|
7
|
+
//
|
|
8
|
+
// Schema (hosts.json):
|
|
9
|
+
// {
|
|
10
|
+
// "hosts": {
|
|
11
|
+
// "devbox": {
|
|
12
|
+
// "host": "user@192.168.1.100",
|
|
13
|
+
// "port": 22,
|
|
14
|
+
// "auto_approve": false,
|
|
15
|
+
// "timeout_sec": 30
|
|
16
|
+
// }
|
|
17
|
+
// },
|
|
18
|
+
// "allow_ssh_config": false,
|
|
19
|
+
// "default_timeout_sec": 30
|
|
20
|
+
// }
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const os = require('os');
|
|
25
|
+
const { execFile } = require('child_process');
|
|
26
|
+
|
|
27
|
+
// ── Config loading ─────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
function loadHostConfig(workspaceRoot) {
|
|
30
|
+
const candidates = [];
|
|
31
|
+
// Project-local config
|
|
32
|
+
if (workspaceRoot) {
|
|
33
|
+
candidates.push(path.join(workspaceRoot, '.shmakk', 'hosts.json'));
|
|
34
|
+
}
|
|
35
|
+
// Global config
|
|
36
|
+
candidates.push(path.join(os.homedir(), '.config', 'shmakk', 'hosts.json'));
|
|
37
|
+
|
|
38
|
+
let merged = { hosts: {}, allow_ssh_config: false, default_timeout_sec: 30 };
|
|
39
|
+
|
|
40
|
+
for (const p of candidates) {
|
|
41
|
+
try {
|
|
42
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
43
|
+
const cfg = JSON.parse(raw);
|
|
44
|
+
if (cfg.hosts && typeof cfg.hosts === 'object') {
|
|
45
|
+
Object.assign(merged.hosts, cfg.hosts);
|
|
46
|
+
}
|
|
47
|
+
if (typeof cfg.allow_ssh_config === 'boolean') {
|
|
48
|
+
merged.allow_ssh_config = merged.allow_ssh_config || cfg.allow_ssh_config;
|
|
49
|
+
}
|
|
50
|
+
if (typeof cfg.default_timeout_sec === 'number') {
|
|
51
|
+
merged.default_timeout_sec = cfg.default_timeout_sec;
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// Missing or malformed — skip
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Optionally import ~/.ssh/config hosts
|
|
59
|
+
if (merged.allow_ssh_config) {
|
|
60
|
+
const sshConfigHosts = parseSSHConfig();
|
|
61
|
+
for (const [name, entry] of Object.entries(sshConfigHosts)) {
|
|
62
|
+
if (!merged.hosts[name]) {
|
|
63
|
+
merged.hosts[name] = entry;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return merged;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseSSHConfig() {
|
|
72
|
+
const configPath = path.join(os.homedir(), '.ssh', 'config');
|
|
73
|
+
const hosts = {};
|
|
74
|
+
try {
|
|
75
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
76
|
+
const lines = content.split(/\r?\n/);
|
|
77
|
+
let currentHost = null;
|
|
78
|
+
let currentEntry = null;
|
|
79
|
+
|
|
80
|
+
for (const raw of lines) {
|
|
81
|
+
const line = raw.trim();
|
|
82
|
+
if (!line || line.startsWith('#')) continue;
|
|
83
|
+
|
|
84
|
+
const m = line.match(/^(\S+)\s+(.+)$/);
|
|
85
|
+
if (!m) continue;
|
|
86
|
+
|
|
87
|
+
const key = m[1].toLowerCase();
|
|
88
|
+
const value = m[2].trim();
|
|
89
|
+
|
|
90
|
+
if (key === 'host') {
|
|
91
|
+
// Save previous entry
|
|
92
|
+
if (currentHost && currentEntry) {
|
|
93
|
+
currentEntry._aliases = currentHost.split(/\s+/);
|
|
94
|
+
for (const alias of currentEntry._aliases) {
|
|
95
|
+
if (alias !== '*') hosts[alias] = currentEntry;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
currentHost = value;
|
|
99
|
+
currentEntry = { host: null, port: 22, auto_approve: false, _from_ssh_config: true };
|
|
100
|
+
} else if (currentEntry) {
|
|
101
|
+
if (key === 'hostname') currentEntry.host = value;
|
|
102
|
+
else if (key === 'port') currentEntry.port = parseInt(value, 10) || 22;
|
|
103
|
+
else if (key === 'user') {
|
|
104
|
+
// Merge user into the host string
|
|
105
|
+
const h = currentEntry.host || value;
|
|
106
|
+
currentEntry.host = `${value}@${h.replace(/^[^@]+@/, '')}`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Save last entry
|
|
111
|
+
if (currentHost && currentEntry) {
|
|
112
|
+
currentEntry._aliases = currentHost.split(/\s+/);
|
|
113
|
+
for (const alias of currentEntry._aliases) {
|
|
114
|
+
if (alias !== '*') hosts[alias] = currentEntry;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// No config or unreadable
|
|
119
|
+
}
|
|
120
|
+
return hosts;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function resolveHost(cfg, name) {
|
|
124
|
+
const entry = cfg.hosts[name];
|
|
125
|
+
if (!entry) return null;
|
|
126
|
+
if (!entry.host) return null;
|
|
127
|
+
return entry;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── SSH command builder ─────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
function buildSSHArgs(entry, cmd) {
|
|
133
|
+
const args = ['ssh'];
|
|
134
|
+
if (entry.port && entry.port !== 22) {
|
|
135
|
+
args.push('-p', String(entry.port));
|
|
136
|
+
}
|
|
137
|
+
// Common options for non-interactive remote execution
|
|
138
|
+
args.push('-o', 'BatchMode=yes');
|
|
139
|
+
args.push('-o', 'StrictHostKeyChecking=accept-new');
|
|
140
|
+
args.push('-o', 'ConnectTimeout=10');
|
|
141
|
+
args.push(entry.host);
|
|
142
|
+
args.push(cmd);
|
|
143
|
+
return args;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function buildSCPArgs(entry, src, dest, direction) {
|
|
147
|
+
// direction: 'push' → local src → remote dest
|
|
148
|
+
// 'pull' → remote src → local dest
|
|
149
|
+
const args = ['scp'];
|
|
150
|
+
if (entry.port && entry.port !== 22) {
|
|
151
|
+
args.push('-P', String(entry.port));
|
|
152
|
+
}
|
|
153
|
+
args.push('-o', 'BatchMode=yes');
|
|
154
|
+
args.push('-o', 'StrictHostKeyChecking=accept-new');
|
|
155
|
+
args.push('-o', 'ConnectTimeout=10');
|
|
156
|
+
|
|
157
|
+
if (direction === 'push') {
|
|
158
|
+
args.push(src, `${entry.host}:${dest}`);
|
|
159
|
+
} else {
|
|
160
|
+
args.push(`${entry.host}:${src}`, dest);
|
|
161
|
+
}
|
|
162
|
+
return args;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── Execution ───────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
function sshRun(entry, cmd, signal) {
|
|
168
|
+
const timeout = (entry.timeout_sec || 30) * 1000;
|
|
169
|
+
const args = buildSSHArgs(entry, cmd);
|
|
170
|
+
|
|
171
|
+
return new Promise((resolve) => {
|
|
172
|
+
const child = execFile('ssh', args, {
|
|
173
|
+
timeout,
|
|
174
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
175
|
+
}, (err, stdout, stderr) => {
|
|
176
|
+
if (err) {
|
|
177
|
+
const msg = (stderr || '').toString().trim() || err.message;
|
|
178
|
+
// Distinguish known SSH errors
|
|
179
|
+
if (err.killed) {
|
|
180
|
+
resolve({ error: `SSH timed out after ${timeout / 1000}s`, exitCode: null, stderr: msg });
|
|
181
|
+
} else {
|
|
182
|
+
resolve({
|
|
183
|
+
error: `SSH failed (exit ${err.code}): ${msg}`,
|
|
184
|
+
exitCode: err.code,
|
|
185
|
+
stderr: msg,
|
|
186
|
+
stdout: (stdout || '').toString().trim(),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
resolve({
|
|
192
|
+
ok: true,
|
|
193
|
+
stdout: (stdout || '').toString().trim(),
|
|
194
|
+
stderr: (stderr || '').toString().trim(),
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (signal) {
|
|
199
|
+
const onAbort = () => { try { child.kill('SIGINT'); } catch {} };
|
|
200
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function sshTransfer(entry, src, dest, direction, signal) {
|
|
206
|
+
const timeout = (entry.timeout_sec || 60) * 1000;
|
|
207
|
+
const args = buildSCPArgs(entry, src, dest, direction);
|
|
208
|
+
|
|
209
|
+
return new Promise((resolve) => {
|
|
210
|
+
const child = execFile('scp', args, {
|
|
211
|
+
timeout,
|
|
212
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
213
|
+
}, (err, stdout, stderr) => {
|
|
214
|
+
if (err) {
|
|
215
|
+
const msg = (stderr || '').toString().trim() || err.message;
|
|
216
|
+
if (err.killed) {
|
|
217
|
+
resolve({ error: `SCP timed out after ${timeout / 1000}s`, stderr: msg });
|
|
218
|
+
} else {
|
|
219
|
+
resolve({
|
|
220
|
+
error: `SCP failed (exit ${err.code}): ${msg}`,
|
|
221
|
+
exitCode: err.code,
|
|
222
|
+
stderr: msg,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
resolve({ ok: true, stdout: (stdout || '').toString().trim() });
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (signal) {
|
|
231
|
+
const onAbort = () => { try { child.kill('SIGINT'); } catch {} };
|
|
232
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Host listing ────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
function listHosts(cfg) {
|
|
240
|
+
return Object.entries(cfg.hosts).map(([name, entry]) => ({
|
|
241
|
+
name,
|
|
242
|
+
host: entry.host,
|
|
243
|
+
port: entry.port || 22,
|
|
244
|
+
auto_approve: !!entry.auto_approve,
|
|
245
|
+
from_ssh_config: !!entry._from_ssh_config,
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
module.exports = {
|
|
250
|
+
loadHostConfig,
|
|
251
|
+
resolveHost,
|
|
252
|
+
sshRun,
|
|
253
|
+
sshTransfer,
|
|
254
|
+
listHosts,
|
|
255
|
+
};
|
package/src/subagent.js
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
|
+
const { modelFor } = require('./llm');
|
|
2
|
+
|
|
1
3
|
// Auto-subagent planning pass. Runs short read-only sub-calls before the
|
|
2
4
|
// main agent loop to scope work, identify risks, and outline a plan.
|
|
3
5
|
// Extracted from agent.js.
|
|
6
|
+
//
|
|
7
|
+
// Force triggers: phrases like "use agent team", "team mode", "pm mode",
|
|
8
|
+
// "agent mode", "multi-agent", or "subagent mode" always enable subagents
|
|
9
|
+
// regardless of heuristics or SHMAKK_AUTO_SUBAGENTS=0.
|
|
4
10
|
|
|
5
11
|
function shouldUseAutoSubagents(input, roots) {
|
|
12
|
+
// Force team/PM mode when the user explicitly requests it, regardless of
|
|
13
|
+
// auto-detection heuristics or SHMAKK_AUTO_SUBAGENTS env override.
|
|
14
|
+
if (/(\buse\s+agent\s+team\b|\bteam\s+mode\b|\bpm\s+mode\b|\bagent\s+mode\b|\bmulti.?agent\b|\bsub.?agent\s+mode\b)/i.test(String(input || ''))) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
6
17
|
if (String(process.env.SHMAKK_AUTO_SUBAGENTS || '1') === '0') return false;
|
|
7
18
|
const minLen = Math.max(40, Number(process.env.SHMAKK_AUTO_SUBAGENTS_MIN_INPUT_LEN) || 140);
|
|
8
19
|
const maxRoots = Math.max(1, Number(process.env.SHMAKK_AUTO_SUBAGENTS_MAX_ROOTS) || 2);
|
|
@@ -26,7 +37,7 @@ async function runAutoSubagents({ client, input, roots, signal }) {
|
|
|
26
37
|
for (let i = 0; i < focuses.length; i++) {
|
|
27
38
|
try {
|
|
28
39
|
const r = await client.chat.completions.create({
|
|
29
|
-
model:
|
|
40
|
+
model: modelFor('subagent-planning'),
|
|
30
41
|
temperature: 0,
|
|
31
42
|
stream: false,
|
|
32
43
|
tool_choice: 'none',
|
package/src/taskClassifier.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Task classifier —
|
|
1
|
+
// Task classifier — keeps routing consistent across model providers.
|
|
2
2
|
// Analyzes message content to detect task type (architecture, implementation, debugging, etc).
|
|
3
3
|
|
|
4
4
|
function normalizeContent(content) {
|
|
@@ -64,7 +64,7 @@ function makeProfile(taskType, scope, reasoning, urgency, risk) {
|
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
// Classify task type from messages
|
|
67
|
+
// Classify task type from messages.
|
|
68
68
|
function classifyTask(messages) {
|
|
69
69
|
const text = messagesToText(messages);
|
|
70
70
|
|
package/src/team.js
CHANGED
|
@@ -21,6 +21,7 @@ const os = require('os');
|
|
|
21
21
|
const { makeClient, modelFor, isConfigured } = require('./llm');
|
|
22
22
|
const { runAgent } = require('./agent');
|
|
23
23
|
const { matchWorkflow, expandWorkflow } = require('./workflows');
|
|
24
|
+
const agentOverview = require('./agent-overview');
|
|
24
25
|
|
|
25
26
|
// Role → preferred skill name mapping. Most agents can be powered by a real
|
|
26
27
|
// skill file from ~/.config/shmakk/skills/. When a skill file exists, its
|
|
@@ -422,6 +423,16 @@ async function runSubAgent({
|
|
|
422
423
|
// Step 2: try to load the skill content from the catalog.
|
|
423
424
|
const loaded = loadSkillContent(wantedSkill, roots);
|
|
424
425
|
|
|
426
|
+
// Register agent in the overview tracker.
|
|
427
|
+
const agentId = agentOverview.register(null, {
|
|
428
|
+
role,
|
|
429
|
+
skill: wantedSkill,
|
|
430
|
+
skillSource: loaded ? loaded.source : null,
|
|
431
|
+
task,
|
|
432
|
+
fileScope: fileScope || null,
|
|
433
|
+
topology,
|
|
434
|
+
});
|
|
435
|
+
|
|
425
436
|
// Step 3: fall back to AGENT_ROSTER if no skill file found.
|
|
426
437
|
const roster = AGENT_ROSTER[role];
|
|
427
438
|
if (!loaded && !roster) {
|
|
@@ -512,6 +523,7 @@ async function runSubAgent({
|
|
|
512
523
|
};
|
|
513
524
|
|
|
514
525
|
try {
|
|
526
|
+
agentOverview.markRunning(agentId);
|
|
515
527
|
await runOnce(subInput);
|
|
516
528
|
|
|
517
529
|
// Retry once if the agent produced 0 tool calls — it likely got stuck in
|
|
@@ -540,6 +552,13 @@ async function runSubAgent({
|
|
|
540
552
|
await runOnce(retryInput, { hintOverride: retryHint, requireTool: true });
|
|
541
553
|
}
|
|
542
554
|
|
|
555
|
+
agentOverview.markDone(agentId, {
|
|
556
|
+
toolCount,
|
|
557
|
+
output: lines.join(''),
|
|
558
|
+
skill: loaded ? wantedSkill : null,
|
|
559
|
+
skillSource: loaded ? loaded.source : null,
|
|
560
|
+
});
|
|
561
|
+
|
|
543
562
|
return {
|
|
544
563
|
role, task,
|
|
545
564
|
output: lines.join(''),
|
|
@@ -548,6 +567,7 @@ async function runSubAgent({
|
|
|
548
567
|
skillUsed: loaded ? wantedSkill : null,
|
|
549
568
|
};
|
|
550
569
|
} catch (e) {
|
|
570
|
+
agentOverview.markError(agentId, e.message);
|
|
551
571
|
return {
|
|
552
572
|
role, task,
|
|
553
573
|
output: lines.join(''),
|
|
@@ -735,6 +755,7 @@ async function runTeam({ input, roots, write, signal, mcpManager }) {
|
|
|
735
755
|
write('\r\n');
|
|
736
756
|
|
|
737
757
|
// Phase 2: Execute based on topology
|
|
758
|
+
agentOverview.startTeamRun(`team-${Date.now()}`);
|
|
738
759
|
const agentResults = topology === 'pipeline'
|
|
739
760
|
? await runPipelineAgents({ agents, overallInput: input, roots, signal, mcpManager, write })
|
|
740
761
|
: await runParallelAgents({ agents, overallInput: input, roots, signal, mcpManager, write });
|
|
@@ -746,6 +767,7 @@ async function runTeam({ input, roots, write, signal, mcpManager }) {
|
|
|
746
767
|
const summary = await synthesizeResults({ overallInput: input, agentResults, client, signal });
|
|
747
768
|
write(summary + '\r\n');
|
|
748
769
|
|
|
770
|
+
agentOverview.endTeamRun();
|
|
749
771
|
return true;
|
|
750
772
|
}
|
|
751
773
|
|