shmakk 1.1.0
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/.env.example +23 -0
- package/LICENSE +21 -0
- package/README.md +138 -0
- package/bin/shmakk.js +2 -0
- package/docs/index.html +581 -0
- package/docs/voice.md +181 -0
- package/package.json +58 -0
- package/scripts/patch-onnxruntime.js +82 -0
- package/src/agent.js +0 -0
- package/src/audit.js +18 -0
- package/src/cli.js +177 -0
- package/src/completions.js +167 -0
- package/src/control.js +250 -0
- package/src/correction.js +159 -0
- package/src/endpoints.js +52 -0
- package/src/global-doctor.js +33 -0
- package/src/global-setup.js +62 -0
- package/src/glossary.js +235 -0
- package/src/history-parser.js +166 -0
- package/src/hooks/bash.js +43 -0
- package/src/hooks/fish.js +25 -0
- package/src/hooks/index.js +14 -0
- package/src/hooks/zsh.js +42 -0
- package/src/index.js +166 -0
- package/src/llm.js +45 -0
- package/src/markers.js +113 -0
- package/src/orchestrator.js +61 -0
- package/src/profiles.js +19 -0
- package/src/prompt-cache.js +83 -0
- package/src/pty.js +107 -0
- package/src/review.js +75 -0
- package/src/safety.js +77 -0
- package/src/services/stt.js +131 -0
- package/src/services/tts.js +307 -0
- package/src/services/voice.js +362 -0
- package/src/session.js +604 -0
- package/src/setup-voice.js +108 -0
- package/src/shell.js +32 -0
- package/src/skills.js +309 -0
- package/src/subagent.js +42 -0
- package/src/system-prompt.js +261 -0
- package/src/tools.js +386 -0
- package/src/web.js +228 -0
- package/src/workspace-index.js +213 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// shmakk voice setup — checks all dependencies for --stt / --tts / --sts
|
|
3
|
+
// Run via: npm run setup:voice or node src/setup-voice.js
|
|
4
|
+
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const ok = (s) => process.stdout.write(` \x1b[32m✓\x1b[0m ${s}\n`);
|
|
8
|
+
const warn = (s) => process.stdout.write(` \x1b[33m⚠\x1b[0m ${s}\n`);
|
|
9
|
+
const fail = (s) => process.stdout.write(` \x1b[31m✗\x1b[0m ${s}\n`);
|
|
10
|
+
const title = (s) => process.stdout.write(`\n\x1b[1m${s}\x1b[0m\n`);
|
|
11
|
+
|
|
12
|
+
let allGood = true;
|
|
13
|
+
|
|
14
|
+
function cmd(c) {
|
|
15
|
+
try {
|
|
16
|
+
return execSync(c, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000 }).trim();
|
|
17
|
+
} catch { return null; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
title('shmakk voice setup check');
|
|
21
|
+
|
|
22
|
+
// ── Node deps ────────────────────────────────────────────────────────────────
|
|
23
|
+
title('Node.js optional dependencies');
|
|
24
|
+
|
|
25
|
+
for (const dep of ['@huggingface/transformers', 'kokoro-js', 'wavefile']) {
|
|
26
|
+
try {
|
|
27
|
+
require.resolve(dep);
|
|
28
|
+
ok(dep);
|
|
29
|
+
} catch {
|
|
30
|
+
fail(`${dep} — not installed`);
|
|
31
|
+
allGood = false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── System audio recorder ────────────────────────────────────────────────────
|
|
36
|
+
title('Audio recorder (microphone input)');
|
|
37
|
+
|
|
38
|
+
const recorders = ['rec', 'sox', 'ffmpeg', 'arecord'];
|
|
39
|
+
let recorderFound = false;
|
|
40
|
+
for (const r of recorders) {
|
|
41
|
+
if (cmd(`which ${r}`)) {
|
|
42
|
+
ok(`${r} found`);
|
|
43
|
+
recorderFound = true;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!recorderFound) {
|
|
48
|
+
fail('No audio recorder found');
|
|
49
|
+
warn('Install sox: sudo pacman -S sox (Arch/EndeavourOS)');
|
|
50
|
+
warn(' sudo apt install sox (Debian/Ubuntu)');
|
|
51
|
+
warn(' brew install sox (macOS)');
|
|
52
|
+
allGood = false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── System audio player ──────────────────────────────────────────────────────
|
|
56
|
+
title('Audio player (TTS playback)');
|
|
57
|
+
|
|
58
|
+
const players = ['paplay', 'aplay', 'afplay'];
|
|
59
|
+
let playerFound = false;
|
|
60
|
+
for (const p of players) {
|
|
61
|
+
if (cmd(`which ${p}`)) {
|
|
62
|
+
ok(`${p} found`);
|
|
63
|
+
playerFound = true;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!playerFound) {
|
|
68
|
+
fail('No audio player found');
|
|
69
|
+
warn('Install: sudo pacman -S libpulse (Arch/EndeavourOS)');
|
|
70
|
+
allGood = false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Audio server ─────────────────────────────────────────────────────────────
|
|
74
|
+
title('Audio server');
|
|
75
|
+
|
|
76
|
+
const paInfo = cmd('pactl info');
|
|
77
|
+
if (paInfo) {
|
|
78
|
+
const server = paInfo.match(/Server Name:\s*(.+)/)?.[1] || 'unknown';
|
|
79
|
+
ok(`Running: ${server}`);
|
|
80
|
+
} else {
|
|
81
|
+
fail('PulseAudio/PipeWire not running');
|
|
82
|
+
warn('Start: systemctl --user start pipewire-pulse');
|
|
83
|
+
allGood = false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Microphone ───────────────────────────────────────────────────────────────
|
|
87
|
+
title('Microphone sources');
|
|
88
|
+
|
|
89
|
+
const sources = cmd('pactl list sources short');
|
|
90
|
+
if (sources) {
|
|
91
|
+
sources.split('\n').filter(Boolean).forEach(s => {
|
|
92
|
+
ok(s.trim().split('\t')[1] || s.trim());
|
|
93
|
+
});
|
|
94
|
+
} else {
|
|
95
|
+
warn('Could not list audio sources');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Summary ──────────────────────────────────────────────────────────────────
|
|
99
|
+
process.stdout.write('\n');
|
|
100
|
+
if (allGood) {
|
|
101
|
+
process.stdout.write('\x1b[32m\x1b[1m✓ All good! Try: shmakk --sts\x1b[0m\n\n');
|
|
102
|
+
} else {
|
|
103
|
+
process.stdout.write('\x1b[33m\x1b[1m⚠ Issues found above. Fix them then re-run: npm run setup:voice\x1b[0m\n\n');
|
|
104
|
+
process.stdout.write('Quick fix (Arch/EndeavourOS):\n');
|
|
105
|
+
process.stdout.write(' sudo pacman -S sox\n');
|
|
106
|
+
process.stdout.write(' npm install --include=optional\n\n');
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
package/src/shell.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function detectShell() {
|
|
5
|
+
const env = process.env.SHELL;
|
|
6
|
+
if (env && fs.existsSync(env)) {
|
|
7
|
+
return { path: env, name: path.basename(env) };
|
|
8
|
+
}
|
|
9
|
+
const fallbacks = ['/bin/bash', '/usr/bin/bash', '/bin/sh'];
|
|
10
|
+
for (const f of fallbacks) {
|
|
11
|
+
if (fs.existsSync(f)) return { path: f, name: path.basename(f) };
|
|
12
|
+
}
|
|
13
|
+
return { path: '/bin/sh', name: 'sh' };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function shellArgs(name) {
|
|
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 };
|
package/src/skills.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const MAX_SKILL_BYTES = 64 * 1024;
|
|
6
|
+
const DEFAULT_RENDER_BYTES = 12 * 1024;
|
|
7
|
+
|
|
8
|
+
function safeName(name) {
|
|
9
|
+
return String(name || '').trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function candidatePaths(name, cwd = process.cwd()) {
|
|
13
|
+
const n = safeName(name);
|
|
14
|
+
const home = os.homedir();
|
|
15
|
+
return [
|
|
16
|
+
path.join(cwd, '.claude', 'skills', `${n}.md`),
|
|
17
|
+
path.join(cwd, '.claude', 'skills', n, 'SKILL.md'),
|
|
18
|
+
path.join(cwd, '.codex', 'skills', `${n}.md`),
|
|
19
|
+
path.join(cwd, '.codex', 'skills', n, 'SKILL.md'),
|
|
20
|
+
path.join(home, '.claude', 'skills', `${n}.md`),
|
|
21
|
+
path.join(home, '.claude', 'skills', n, 'SKILL.md'),
|
|
22
|
+
path.join(home, '.codex', 'skills', `${n}.md`),
|
|
23
|
+
path.join(home, '.codex', 'skills', n, 'SKILL.md'),
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function stateDir(cwd = process.cwd()) {
|
|
28
|
+
return path.join(cwd, '.shmakk', 'state');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function skillsDir(cwd = process.cwd()) {
|
|
32
|
+
return path.join(cwd, '.shmakk', 'skills');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function registryPath(cwd = process.cwd()) {
|
|
36
|
+
return path.join(stateDir(cwd), 'skills-registry.json');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function activeSkillPath(cwd = process.cwd()) {
|
|
40
|
+
return path.join(stateDir(cwd), 'active-skill.json');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function ensureDirs(cwd = process.cwd()) {
|
|
44
|
+
fs.mkdirSync(stateDir(cwd), { recursive: true });
|
|
45
|
+
fs.mkdirSync(skillsDir(cwd), { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sha256(s) {
|
|
49
|
+
return require('crypto').createHash('sha256').update(String(s || ''), 'utf8').digest('hex');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseFrontmatter(raw) {
|
|
53
|
+
const m = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/m.exec(String(raw || ''));
|
|
54
|
+
if (!m) return { meta: {}, body: String(raw || '') };
|
|
55
|
+
const meta = {};
|
|
56
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
57
|
+
const mm = /^([a-zA-Z0-9_-]+)\s*:\s*(.+)$/.exec(line.trim());
|
|
58
|
+
if (!mm) continue;
|
|
59
|
+
meta[mm[1].toLowerCase()] = mm[2].trim();
|
|
60
|
+
}
|
|
61
|
+
return { meta, body: m[2] };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function validateSkill(raw, sourcePath = '') {
|
|
65
|
+
const text = String(raw || '');
|
|
66
|
+
const issues = [];
|
|
67
|
+
if (!text.trim()) issues.push('empty skill content');
|
|
68
|
+
if (Buffer.byteLength(text, 'utf8') > MAX_SKILL_BYTES) issues.push(`skill exceeds ${MAX_SKILL_BYTES} bytes`);
|
|
69
|
+
if (!/^#\s+/m.test(text)) issues.push('missing markdown heading');
|
|
70
|
+
if (!/(instruction|rule|guideline|workflow|steps?|when to use|pattern|quick start|core concepts?)/i.test(text)) {
|
|
71
|
+
issues.push('no obvious instruction sections found');
|
|
72
|
+
}
|
|
73
|
+
if (/\b(ignore previous|bypass safety|exfiltrate|leak secret|disable security)\b/i.test(text)) {
|
|
74
|
+
issues.push('contains potentially unsafe instruction phrases');
|
|
75
|
+
}
|
|
76
|
+
const fm = parseFrontmatter(text);
|
|
77
|
+
const name = safeName(fm.meta.name || path.basename(sourcePath || '', path.extname(sourcePath || '')) || 'skill');
|
|
78
|
+
const version = String(fm.meta.version || '1').trim();
|
|
79
|
+
return {
|
|
80
|
+
ok: issues.length === 0,
|
|
81
|
+
issues,
|
|
82
|
+
normalizedName: name,
|
|
83
|
+
version,
|
|
84
|
+
body: fm.body,
|
|
85
|
+
raw: text,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function loadRegistry(cwd = process.cwd()) {
|
|
90
|
+
try {
|
|
91
|
+
const p = registryPath(cwd);
|
|
92
|
+
if (!fs.existsSync(p)) return { skills: {}, updatedAt: null };
|
|
93
|
+
const j = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
94
|
+
return { skills: j.skills || {}, updatedAt: j.updatedAt || null };
|
|
95
|
+
} catch {
|
|
96
|
+
return { skills: {}, updatedAt: null };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function saveRegistry(cwd, registry) {
|
|
101
|
+
ensureDirs(cwd);
|
|
102
|
+
const p = registryPath(cwd);
|
|
103
|
+
fs.writeFileSync(p, JSON.stringify({
|
|
104
|
+
skills: registry.skills || {},
|
|
105
|
+
updatedAt: new Date().toISOString(),
|
|
106
|
+
}, null, 2));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function loadSkillToWorkspace(name, cwd = process.cwd()) {
|
|
110
|
+
const n = safeName(name);
|
|
111
|
+
if (!n) return { ok: false, error: 'missing skill name' };
|
|
112
|
+
const found = candidatePaths(n, cwd).find((p) => fs.existsSync(p));
|
|
113
|
+
if (!found) {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
error: `skill not found: ${n}`,
|
|
117
|
+
searched: candidatePaths(n, cwd),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const raw = fs.readFileSync(found, 'utf8');
|
|
122
|
+
const validation = validateSkill(raw, found);
|
|
123
|
+
if (!validation.ok) {
|
|
124
|
+
return { ok: false, error: `skill failed validation: ${validation.issues.join('; ')}` };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
ensureDirs(cwd);
|
|
128
|
+
const localSkillPath = path.join(skillsDir(cwd), `${validation.normalizedName}.md`);
|
|
129
|
+
fs.writeFileSync(localSkillPath, validation.raw, 'utf8');
|
|
130
|
+
|
|
131
|
+
const registry = loadRegistry(cwd);
|
|
132
|
+
const checksum = sha256(validation.raw);
|
|
133
|
+
registry.skills[validation.normalizedName] = {
|
|
134
|
+
name: validation.normalizedName,
|
|
135
|
+
version: validation.version,
|
|
136
|
+
source: found,
|
|
137
|
+
localPath: localSkillPath,
|
|
138
|
+
checksum,
|
|
139
|
+
bytes: Buffer.byteLength(validation.raw, 'utf8'),
|
|
140
|
+
loadedAt: new Date().toISOString(),
|
|
141
|
+
active: true,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
for (const k of Object.keys(registry.skills)) {
|
|
145
|
+
if (k !== validation.normalizedName) registry.skills[k].active = false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
saveRegistry(cwd, registry);
|
|
149
|
+
fs.writeFileSync(activeSkillPath(cwd), JSON.stringify(registry.skills[validation.normalizedName], null, 2));
|
|
150
|
+
|
|
151
|
+
return { ok: true, name: validation.normalizedName, source: found, localPath: localSkillPath, version: validation.version };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function importSkillContent(raw, sourceLabel, cwd = process.cwd(), fallbackName = 'downloaded-skill') {
|
|
155
|
+
const validation = validateSkill(raw, sourceLabel);
|
|
156
|
+
if (!validation.ok) {
|
|
157
|
+
return { ok: false, error: `skill failed validation: ${validation.issues.join('; ')}` };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const name = validation.normalizedName || safeName(fallbackName) || 'downloaded-skill';
|
|
161
|
+
ensureDirs(cwd);
|
|
162
|
+
const localSkillPath = path.join(skillsDir(cwd), `${name}.md`);
|
|
163
|
+
fs.writeFileSync(localSkillPath, validation.raw, 'utf8');
|
|
164
|
+
|
|
165
|
+
const registry = loadRegistry(cwd);
|
|
166
|
+
registry.skills[name] = {
|
|
167
|
+
name,
|
|
168
|
+
version: validation.version,
|
|
169
|
+
source: sourceLabel,
|
|
170
|
+
localPath: localSkillPath,
|
|
171
|
+
checksum: sha256(validation.raw),
|
|
172
|
+
bytes: Buffer.byteLength(validation.raw, 'utf8'),
|
|
173
|
+
loadedAt: new Date().toISOString(),
|
|
174
|
+
active: true,
|
|
175
|
+
};
|
|
176
|
+
for (const k of Object.keys(registry.skills)) {
|
|
177
|
+
if (k !== name) registry.skills[k].active = false;
|
|
178
|
+
}
|
|
179
|
+
saveRegistry(cwd, registry);
|
|
180
|
+
fs.writeFileSync(activeSkillPath(cwd), JSON.stringify(registry.skills[name], null, 2));
|
|
181
|
+
return { ok: true, name, source: sourceLabel, localPath: localSkillPath, version: validation.version };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function readActiveSkill(cwd = process.cwd()) {
|
|
185
|
+
try {
|
|
186
|
+
const p = activeSkillPath(cwd);
|
|
187
|
+
if (!fs.existsSync(p)) return null;
|
|
188
|
+
const meta = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
189
|
+
if (!meta || !meta.localPath || !fs.existsSync(meta.localPath)) return null;
|
|
190
|
+
const content = fs.readFileSync(meta.localPath, 'utf8');
|
|
191
|
+
return { ...meta, content };
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function renderActiveSkillForPrompt(cwd = process.cwd(), maxBytes = DEFAULT_RENDER_BYTES) {
|
|
198
|
+
const skill = readActiveSkill(cwd);
|
|
199
|
+
if (!skill || !skill.content) return '';
|
|
200
|
+
const body = String(skill.content || '').slice(0, Math.max(1000, Number(maxBytes) || DEFAULT_RENDER_BYTES));
|
|
201
|
+
return `Active loaded skill (${skill.name}${skill.version ? ` v${skill.version}` : ''}) instructions:\n${body}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function listSkills(cwd = process.cwd()) {
|
|
205
|
+
const r = loadRegistry(cwd);
|
|
206
|
+
return Object.values(r.skills || {}).sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function unloadSkill(name, cwd = process.cwd()) {
|
|
210
|
+
const n = safeName(name);
|
|
211
|
+
const registry = loadRegistry(cwd);
|
|
212
|
+
const entry = registry.skills[n];
|
|
213
|
+
if (!entry) return { ok: false, error: `skill not found in registry: ${n}` };
|
|
214
|
+
delete registry.skills[n];
|
|
215
|
+
if (entry.localPath) {
|
|
216
|
+
try { fs.rmSync(entry.localPath, { force: true }); } catch {}
|
|
217
|
+
}
|
|
218
|
+
const active = readActiveSkill(cwd);
|
|
219
|
+
if (active && safeName(active.name) === n) {
|
|
220
|
+
try { fs.rmSync(activeSkillPath(cwd), { force: true }); } catch {}
|
|
221
|
+
}
|
|
222
|
+
saveRegistry(cwd, registry);
|
|
223
|
+
return { ok: true, name: n };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function skillStatus(cwd = process.cwd()) {
|
|
227
|
+
const active = readActiveSkill(cwd);
|
|
228
|
+
const all = listSkills(cwd);
|
|
229
|
+
return {
|
|
230
|
+
active: active ? {
|
|
231
|
+
name: active.name,
|
|
232
|
+
version: active.version || '1',
|
|
233
|
+
source: active.source,
|
|
234
|
+
loadedAt: active.loadedAt,
|
|
235
|
+
bytes: active.bytes || Buffer.byteLength(String(active.content || ''), 'utf8'),
|
|
236
|
+
} : null,
|
|
237
|
+
total: all.length,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function installSkillFromUrl(url, cwd = process.cwd()) {
|
|
242
|
+
let u;
|
|
243
|
+
try { u = new URL(String(url || '')); } catch { return { ok: false, error: 'invalid URL' }; }
|
|
244
|
+
if (!/^https?:$/.test(u.protocol)) return { ok: false, error: 'only http(s) URLs are supported' };
|
|
245
|
+
|
|
246
|
+
async function resolveGitHubUrl(inputUrl) {
|
|
247
|
+
try {
|
|
248
|
+
const gu = new URL(inputUrl);
|
|
249
|
+
if (!/^(www\.)?github\.com$/i.test(gu.host)) return inputUrl;
|
|
250
|
+
const parts = gu.pathname.split('/').filter(Boolean);
|
|
251
|
+
// /owner/repo/tree/ref/path...
|
|
252
|
+
if (parts.length >= 5 && (parts[2] === 'tree' || parts[2] === 'blob')) {
|
|
253
|
+
const owner = parts[0];
|
|
254
|
+
const repo = parts[1];
|
|
255
|
+
const ref = parts[3];
|
|
256
|
+
const relPath = parts.slice(4).join('/');
|
|
257
|
+
if (parts[2] === 'blob') {
|
|
258
|
+
return `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${relPath}`;
|
|
259
|
+
}
|
|
260
|
+
// tree: if direct markdown path, convert to raw
|
|
261
|
+
if (/\.(md|markdown)$/i.test(relPath)) {
|
|
262
|
+
return `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${relPath}`;
|
|
263
|
+
}
|
|
264
|
+
// tree directory: discover SKILL.md first, then first markdown fallback
|
|
265
|
+
const api = `https://api.github.com/repos/${owner}/${repo}/contents/${relPath}?ref=${encodeURIComponent(ref)}`;
|
|
266
|
+
const resp = await fetch(api, { headers: { 'user-agent': 'shmakk-skill-installer/1.0' } });
|
|
267
|
+
if (!resp.ok) return inputUrl;
|
|
268
|
+
const arr = await resp.json();
|
|
269
|
+
if (!Array.isArray(arr)) return inputUrl;
|
|
270
|
+
const skillFile = arr.find((x) => x && x.type === 'file' && /^SKILL\.md$/i.test(String(x.name || '')) && x.download_url)
|
|
271
|
+
|| arr.find((x) => x && x.type === 'file' && /\.(md|markdown)$/i.test(String(x.name || '')) && x.download_url);
|
|
272
|
+
return skillFile?.download_url || inputUrl;
|
|
273
|
+
}
|
|
274
|
+
return inputUrl;
|
|
275
|
+
} catch {
|
|
276
|
+
return inputUrl;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const resolvedUrl = await resolveGitHubUrl(u.href);
|
|
281
|
+
let finalUrl;
|
|
282
|
+
try { finalUrl = new URL(resolvedUrl); } catch { finalUrl = u; }
|
|
283
|
+
|
|
284
|
+
let text = '';
|
|
285
|
+
try {
|
|
286
|
+
const resp = await fetch(finalUrl.href, {
|
|
287
|
+
headers: { 'user-agent': 'shmakk-skill-installer/1.0' },
|
|
288
|
+
});
|
|
289
|
+
if (!resp.ok) return { ok: false, error: `download failed: HTTP ${resp.status}` };
|
|
290
|
+
text = await resp.text();
|
|
291
|
+
} catch (e) {
|
|
292
|
+
return { ok: false, error: `download failed: ${e.message}` };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const derived = safeName(path.basename(finalUrl.pathname || '', path.extname(finalUrl.pathname || '')) || 'downloaded-skill');
|
|
296
|
+
return importSkillContent(text, finalUrl.href, cwd, derived);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
module.exports = {
|
|
300
|
+
loadSkillToWorkspace,
|
|
301
|
+
importSkillContent,
|
|
302
|
+
readActiveSkill,
|
|
303
|
+
renderActiveSkillForPrompt,
|
|
304
|
+
listSkills,
|
|
305
|
+
unloadSkill,
|
|
306
|
+
skillStatus,
|
|
307
|
+
installSkillFromUrl,
|
|
308
|
+
safeName,
|
|
309
|
+
};
|
package/src/subagent.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Auto-subagent planning pass. Runs short read-only sub-calls before the
|
|
2
|
+
// main agent loop to scope work, identify risks, and outline a plan.
|
|
3
|
+
// Extracted from agent.js.
|
|
4
|
+
|
|
5
|
+
function shouldUseAutoSubagents(input, roots) {
|
|
6
|
+
if (String(process.env.SHMAKK_AUTO_SUBAGENTS || '1') === '0') return false;
|
|
7
|
+
const minLen = Math.max(40, Number(process.env.SHMAKK_AUTO_SUBAGENTS_MIN_INPUT_LEN) || 140);
|
|
8
|
+
const maxRoots = Math.max(1, Number(process.env.SHMAKK_AUTO_SUBAGENTS_MAX_ROOTS) || 2);
|
|
9
|
+
const s = String(input || '');
|
|
10
|
+
const broadSignals = /(large|across|multiple|refactor|architecture|investigate|analyz|codebase|project-wide|compare)/i.test(s);
|
|
11
|
+
return (s.length >= minLen && broadSignals) || (Array.isArray(roots) && roots.length >= maxRoots && s.length >= Math.floor(minLen * 0.7));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function runAutoSubagents({ client, input, roots, signal }) {
|
|
15
|
+
const n = Math.max(1, Math.min(3, Number(process.env.SHMAKK_AUTO_SUBAGENTS_COUNT) || 2));
|
|
16
|
+
const focuses = [
|
|
17
|
+
'Scope and likely target files/modules',
|
|
18
|
+
'Risks, edge cases, and verification strategy',
|
|
19
|
+
'Concrete step-by-step implementation plan with minimal reads',
|
|
20
|
+
].slice(0, n);
|
|
21
|
+
|
|
22
|
+
const out = [];
|
|
23
|
+
for (let i = 0; i < focuses.length; i++) {
|
|
24
|
+
try {
|
|
25
|
+
const r = await client.chat.completions.create({
|
|
26
|
+
model: process.env.SHMAKK_MODEL || 'gpt-4o-mini',
|
|
27
|
+
temperature: 0,
|
|
28
|
+
stream: false,
|
|
29
|
+
tool_choice: 'none',
|
|
30
|
+
messages: [
|
|
31
|
+
{ role: 'system', content: `You are subagent ${i + 1}. Read-only planning only. No tool calls. Be concise.` },
|
|
32
|
+
{ role: 'user', content: `Workspace roots: ${roots.join(', ')}\nTask: ${input}\nFocus: ${focuses[i]}\nReturn bullet points only.` },
|
|
33
|
+
],
|
|
34
|
+
}, { signal });
|
|
35
|
+
const text = String(r.choices?.[0]?.message?.content || '').trim();
|
|
36
|
+
if (text) out.push(`Subagent ${i + 1} (${focuses[i]}):\n${text}`);
|
|
37
|
+
} catch {}
|
|
38
|
+
}
|
|
39
|
+
return out.join('\n\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { shouldUseAutoSubagents, runAutoSubagents };
|