wispy-cli 0.3.1 → 0.3.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/lib/wispy-repl.mjs +121 -2
- package/package.json +1 -1
package/lib/wispy-repl.mjs
CHANGED
|
@@ -1464,8 +1464,20 @@ Be concise. Your output feeds into the next stage.`;
|
|
|
1464
1464
|
return { success: true, iterations: MAX_ITERATIONS, result: lastResult, verified: false, message: "Max iterations reached" };
|
|
1465
1465
|
}
|
|
1466
1466
|
|
|
1467
|
-
default:
|
|
1468
|
-
|
|
1467
|
+
default: {
|
|
1468
|
+
// Unknown tool — try to execute as a skill via run_command
|
|
1469
|
+
// This handles cases where the AI hallucinates tools from skill descriptions
|
|
1470
|
+
const skills = await loadSkills();
|
|
1471
|
+
const matchedSkill = skills.find(s => s.name.toLowerCase() === name.toLowerCase());
|
|
1472
|
+
if (matchedSkill) {
|
|
1473
|
+
return {
|
|
1474
|
+
success: false,
|
|
1475
|
+
error: `"${name}" is a skill, not a tool. Use run_command to execute commands from the ${name} skill guide. Example from the skill: look for curl/bash commands in the skill description.`,
|
|
1476
|
+
skill_hint: matchedSkill.body.slice(0, 500),
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
return { success: false, error: `Unknown tool: ${name}. Available: read_file, write_file, file_edit, file_search, run_command, list_directory, git, web_search, web_fetch, keychain, clipboard, spawn_agent, spawn_async_agent, pipeline, ralph_loop, update_plan, list_agents, get_agent_result` };
|
|
1480
|
+
}
|
|
1469
1481
|
}
|
|
1470
1482
|
} catch (err) {
|
|
1471
1483
|
return { success: false, error: err.message };
|
|
@@ -1493,6 +1505,69 @@ async function loadWorkMd() {
|
|
|
1493
1505
|
return null;
|
|
1494
1506
|
}
|
|
1495
1507
|
|
|
1508
|
+
// ---------------------------------------------------------------------------
|
|
1509
|
+
// Skill loader — loads SKILL.md files from multiple sources
|
|
1510
|
+
// Compatible with OpenClaw and Claude Code skill formats
|
|
1511
|
+
// ---------------------------------------------------------------------------
|
|
1512
|
+
|
|
1513
|
+
async function loadSkills() {
|
|
1514
|
+
const skillDirs = [
|
|
1515
|
+
// OpenClaw built-in skills
|
|
1516
|
+
"/opt/homebrew/lib/node_modules/openclaw/skills",
|
|
1517
|
+
// OpenClaw user skills
|
|
1518
|
+
path.join(os.homedir(), ".openclaw", "workspace", "skills"),
|
|
1519
|
+
// Wispy skills
|
|
1520
|
+
path.join(WISPY_DIR, "skills"),
|
|
1521
|
+
// Project-local skills
|
|
1522
|
+
path.resolve(".wispy", "skills"),
|
|
1523
|
+
// Claude Code skills (if installed)
|
|
1524
|
+
path.join(os.homedir(), ".claude", "skills"),
|
|
1525
|
+
];
|
|
1526
|
+
|
|
1527
|
+
const skills = [];
|
|
1528
|
+
const { readdir: rd, stat: st } = await import("node:fs/promises");
|
|
1529
|
+
|
|
1530
|
+
for (const dir of skillDirs) {
|
|
1531
|
+
try {
|
|
1532
|
+
const entries = await rd(dir);
|
|
1533
|
+
for (const entry of entries) {
|
|
1534
|
+
const skillMdPath = path.join(dir, entry, "SKILL.md");
|
|
1535
|
+
try {
|
|
1536
|
+
const content = await readFile(skillMdPath, "utf8");
|
|
1537
|
+
// Parse frontmatter
|
|
1538
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
1539
|
+
let name = entry;
|
|
1540
|
+
let description = "";
|
|
1541
|
+
let body = content;
|
|
1542
|
+
|
|
1543
|
+
if (fmMatch) {
|
|
1544
|
+
const fm = fmMatch[1];
|
|
1545
|
+
body = fmMatch[2];
|
|
1546
|
+
const nameMatch = fm.match(/name:\s*["']?(.+?)["']?\s*$/m);
|
|
1547
|
+
const descMatch = fm.match(/description:\s*["'](.+?)["']\s*$/m);
|
|
1548
|
+
if (nameMatch) name = nameMatch[1].trim();
|
|
1549
|
+
if (descMatch) description = descMatch[1].trim();
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
skills.push({ name, description, body: body.trim(), path: skillMdPath, source: dir });
|
|
1553
|
+
} catch { /* no SKILL.md */ }
|
|
1554
|
+
}
|
|
1555
|
+
} catch { /* dir doesn't exist */ }
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
return skills;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
function matchSkills(prompt, skills) {
|
|
1562
|
+
const lower = prompt.toLowerCase();
|
|
1563
|
+
return skills.filter(skill => {
|
|
1564
|
+
const nameMatch = lower.includes(skill.name.toLowerCase());
|
|
1565
|
+
const descWords = skill.description.toLowerCase().split(/\s+/);
|
|
1566
|
+
const descMatch = descWords.some(w => w.length > 4 && lower.includes(w));
|
|
1567
|
+
return nameMatch || descMatch;
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1496
1571
|
async function buildSystemPrompt(messages = []) {
|
|
1497
1572
|
// Detect user's language from last message for system prompt hint
|
|
1498
1573
|
const lastUserMsg = messages?.find ? [...messages].reverse().find(m => m.role === "user")?.content ?? "" : "";
|
|
@@ -1558,6 +1633,25 @@ async function buildSystemPrompt(messages = []) {
|
|
|
1558
1633
|
parts.push("");
|
|
1559
1634
|
}
|
|
1560
1635
|
|
|
1636
|
+
// Load and inject matching skills
|
|
1637
|
+
const allSkills = await loadSkills();
|
|
1638
|
+
if (allSkills.length > 0 && lastUserMsg) {
|
|
1639
|
+
const matched = matchSkills(lastUserMsg, allSkills);
|
|
1640
|
+
if (matched.length > 0) {
|
|
1641
|
+
parts.push("## Active Skills (instructions — use run_command/web_fetch to execute)");
|
|
1642
|
+
parts.push("Skills are NOT tools — they are guides. Use run_command to execute the commands described in them.");
|
|
1643
|
+
for (const skill of matched.slice(0, 3)) { // Max 3 skills per turn
|
|
1644
|
+
parts.push(`### ${skill.name}`);
|
|
1645
|
+
parts.push(skill.body.slice(0, 5000));
|
|
1646
|
+
parts.push("");
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
// Always list available skills
|
|
1650
|
+
parts.push(`## Available Skills (${allSkills.length} installed)`);
|
|
1651
|
+
parts.push(allSkills.map(s => `- ${s.name}: ${s.description.slice(0, 60)}`).join("\n"));
|
|
1652
|
+
parts.push("");
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1561
1655
|
return parts.join("\n");
|
|
1562
1656
|
}
|
|
1563
1657
|
|
|
@@ -2131,6 +2225,30 @@ ${bold("Wispy Commands:")}
|
|
|
2131
2225
|
return true;
|
|
2132
2226
|
}
|
|
2133
2227
|
|
|
2228
|
+
if (cmd === "/skills") {
|
|
2229
|
+
const skills = await loadSkills();
|
|
2230
|
+
if (skills.length === 0) {
|
|
2231
|
+
console.log(dim("No skills installed."));
|
|
2232
|
+
console.log(dim("Add skills to ~/.wispy/skills/ or install OpenClaw skills."));
|
|
2233
|
+
} else {
|
|
2234
|
+
console.log(bold(`\n🧩 Skills (${skills.length} installed):\n`));
|
|
2235
|
+
const bySource = {};
|
|
2236
|
+
for (const s of skills) {
|
|
2237
|
+
const src = s.source.includes("openclaw") ? "OpenClaw" : s.source.includes(".wispy") ? "Wispy" : s.source.includes(".claude") ? "Claude" : "Project";
|
|
2238
|
+
if (!bySource[src]) bySource[src] = [];
|
|
2239
|
+
bySource[src].push(s);
|
|
2240
|
+
}
|
|
2241
|
+
for (const [src, sks] of Object.entries(bySource)) {
|
|
2242
|
+
console.log(` ${bold(src)} (${sks.length}):`);
|
|
2243
|
+
for (const s of sks) {
|
|
2244
|
+
console.log(` ${green(s.name.padEnd(20))} ${dim(s.description.slice(0, 50))}`);
|
|
2245
|
+
}
|
|
2246
|
+
console.log("");
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
return true;
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2134
2252
|
if (cmd === "/sessions" || cmd === "/ls") {
|
|
2135
2253
|
const wsList = await listWorkstreams();
|
|
2136
2254
|
if (wsList.length === 0) {
|
|
@@ -2585,6 +2703,7 @@ ${bold("In-session commands:")}
|
|
|
2585
2703
|
/workstreams List all workstreams
|
|
2586
2704
|
/overview Director view — all workstreams at a glance
|
|
2587
2705
|
/search <keyword> Search across all workstreams
|
|
2706
|
+
/skills List installed skills (OpenClaw/Claude compatible)
|
|
2588
2707
|
/sessions List all sessions
|
|
2589
2708
|
/delete <name> Delete a session
|
|
2590
2709
|
/export [md|clipboard] Export conversation
|