wispy-cli 0.3.1 → 0.4.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/bin/wispy-tui.mjs +14 -0
- package/bin/wispy.mjs +22 -4
- package/lib/mcp-client.mjs +381 -0
- package/lib/wispy-repl.mjs +287 -6
- package/lib/wispy-tui.mjs +812 -0
- package/package.json +12 -3
package/lib/wispy-repl.mjs
CHANGED
|
@@ -15,6 +15,8 @@ import os from "node:os";
|
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
import { createInterface } from "node:readline";
|
|
17
17
|
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
18
|
+
import { fileURLToPath as _fileURLToPath } from "node:url";
|
|
19
|
+
import { MCPManager, ensureDefaultMcpConfig } from "./mcp-client.mjs";
|
|
18
20
|
|
|
19
21
|
// ---------------------------------------------------------------------------
|
|
20
22
|
// Config
|
|
@@ -22,6 +24,10 @@ import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
22
24
|
|
|
23
25
|
const WISPY_DIR = path.join(os.homedir(), ".wispy");
|
|
24
26
|
const MEMORY_DIR = path.join(WISPY_DIR, "memory");
|
|
27
|
+
const MCP_CONFIG_PATH = path.join(WISPY_DIR, "mcp.json");
|
|
28
|
+
|
|
29
|
+
// Global MCP manager — initialized at startup
|
|
30
|
+
const mcpManager = new MCPManager(MCP_CONFIG_PATH);
|
|
25
31
|
|
|
26
32
|
// Workstream-aware conversation storage
|
|
27
33
|
// wispy -w "project-name" → separate conversation per workstream
|
|
@@ -619,6 +625,11 @@ function optimizeContext(messages, maxTokens = 30_000) {
|
|
|
619
625
|
// Tool definitions (Gemini function calling format)
|
|
620
626
|
// ---------------------------------------------------------------------------
|
|
621
627
|
|
|
628
|
+
// Returns merged static + dynamically registered MCP tool definitions
|
|
629
|
+
function getAllToolDefinitions() {
|
|
630
|
+
return [...TOOL_DEFINITIONS, ...mcpManager.getToolDefinitions()];
|
|
631
|
+
}
|
|
632
|
+
|
|
622
633
|
const TOOL_DEFINITIONS = [
|
|
623
634
|
{
|
|
624
635
|
name: "read_file",
|
|
@@ -1464,8 +1475,36 @@ Be concise. Your output feeds into the next stage.`;
|
|
|
1464
1475
|
return { success: true, iterations: MAX_ITERATIONS, result: lastResult, verified: false, message: "Max iterations reached" };
|
|
1465
1476
|
}
|
|
1466
1477
|
|
|
1467
|
-
default:
|
|
1468
|
-
|
|
1478
|
+
default: {
|
|
1479
|
+
// Check MCP tools first
|
|
1480
|
+
if (mcpManager.hasTool(name)) {
|
|
1481
|
+
try {
|
|
1482
|
+
const result = await mcpManager.callTool(name, args);
|
|
1483
|
+
// MCP tools/call returns { content: [{type, text}], isError? }
|
|
1484
|
+
if (result?.isError) {
|
|
1485
|
+
const errText = result.content?.map(c => c.text ?? "").join("") ?? "MCP tool error";
|
|
1486
|
+
return { success: false, error: errText };
|
|
1487
|
+
}
|
|
1488
|
+
const output = result?.content?.map(c => c.text ?? c.data ?? JSON.stringify(c)).join("\n") ?? JSON.stringify(result);
|
|
1489
|
+
return { success: true, output };
|
|
1490
|
+
} catch (err) {
|
|
1491
|
+
return { success: false, error: `MCP tool error: ${err.message}` };
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
// Unknown tool — try to execute as a skill via run_command
|
|
1496
|
+
// This handles cases where the AI hallucinates tools from skill descriptions
|
|
1497
|
+
const skills = await loadSkills();
|
|
1498
|
+
const matchedSkill = skills.find(s => s.name.toLowerCase() === name.toLowerCase());
|
|
1499
|
+
if (matchedSkill) {
|
|
1500
|
+
return {
|
|
1501
|
+
success: false,
|
|
1502
|
+
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.`,
|
|
1503
|
+
skill_hint: matchedSkill.body.slice(0, 500),
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
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, and MCP tools (see /mcp list)` };
|
|
1507
|
+
}
|
|
1469
1508
|
}
|
|
1470
1509
|
} catch (err) {
|
|
1471
1510
|
return { success: false, error: err.message };
|
|
@@ -1493,6 +1532,69 @@ async function loadWorkMd() {
|
|
|
1493
1532
|
return null;
|
|
1494
1533
|
}
|
|
1495
1534
|
|
|
1535
|
+
// ---------------------------------------------------------------------------
|
|
1536
|
+
// Skill loader — loads SKILL.md files from multiple sources
|
|
1537
|
+
// Compatible with OpenClaw and Claude Code skill formats
|
|
1538
|
+
// ---------------------------------------------------------------------------
|
|
1539
|
+
|
|
1540
|
+
async function loadSkills() {
|
|
1541
|
+
const skillDirs = [
|
|
1542
|
+
// OpenClaw built-in skills
|
|
1543
|
+
"/opt/homebrew/lib/node_modules/openclaw/skills",
|
|
1544
|
+
// OpenClaw user skills
|
|
1545
|
+
path.join(os.homedir(), ".openclaw", "workspace", "skills"),
|
|
1546
|
+
// Wispy skills
|
|
1547
|
+
path.join(WISPY_DIR, "skills"),
|
|
1548
|
+
// Project-local skills
|
|
1549
|
+
path.resolve(".wispy", "skills"),
|
|
1550
|
+
// Claude Code skills (if installed)
|
|
1551
|
+
path.join(os.homedir(), ".claude", "skills"),
|
|
1552
|
+
];
|
|
1553
|
+
|
|
1554
|
+
const skills = [];
|
|
1555
|
+
const { readdir: rd, stat: st } = await import("node:fs/promises");
|
|
1556
|
+
|
|
1557
|
+
for (const dir of skillDirs) {
|
|
1558
|
+
try {
|
|
1559
|
+
const entries = await rd(dir);
|
|
1560
|
+
for (const entry of entries) {
|
|
1561
|
+
const skillMdPath = path.join(dir, entry, "SKILL.md");
|
|
1562
|
+
try {
|
|
1563
|
+
const content = await readFile(skillMdPath, "utf8");
|
|
1564
|
+
// Parse frontmatter
|
|
1565
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
1566
|
+
let name = entry;
|
|
1567
|
+
let description = "";
|
|
1568
|
+
let body = content;
|
|
1569
|
+
|
|
1570
|
+
if (fmMatch) {
|
|
1571
|
+
const fm = fmMatch[1];
|
|
1572
|
+
body = fmMatch[2];
|
|
1573
|
+
const nameMatch = fm.match(/name:\s*["']?(.+?)["']?\s*$/m);
|
|
1574
|
+
const descMatch = fm.match(/description:\s*["'](.+?)["']\s*$/m);
|
|
1575
|
+
if (nameMatch) name = nameMatch[1].trim();
|
|
1576
|
+
if (descMatch) description = descMatch[1].trim();
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
skills.push({ name, description, body: body.trim(), path: skillMdPath, source: dir });
|
|
1580
|
+
} catch { /* no SKILL.md */ }
|
|
1581
|
+
}
|
|
1582
|
+
} catch { /* dir doesn't exist */ }
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
return skills;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
function matchSkills(prompt, skills) {
|
|
1589
|
+
const lower = prompt.toLowerCase();
|
|
1590
|
+
return skills.filter(skill => {
|
|
1591
|
+
const nameMatch = lower.includes(skill.name.toLowerCase());
|
|
1592
|
+
const descWords = skill.description.toLowerCase().split(/\s+/);
|
|
1593
|
+
const descMatch = descWords.some(w => w.length > 4 && lower.includes(w));
|
|
1594
|
+
return nameMatch || descMatch;
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1496
1598
|
async function buildSystemPrompt(messages = []) {
|
|
1497
1599
|
// Detect user's language from last message for system prompt hint
|
|
1498
1600
|
const lastUserMsg = messages?.find ? [...messages].reverse().find(m => m.role === "user")?.content ?? "" : "";
|
|
@@ -1524,7 +1626,7 @@ async function buildSystemPrompt(messages = []) {
|
|
|
1524
1626
|
" - NEVER reply in Korean when the user wrote in English.",
|
|
1525
1627
|
"",
|
|
1526
1628
|
"## Tools",
|
|
1527
|
-
|
|
1629
|
+
`You have ${18 + mcpManager.getAllTools().length} tools: 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${mcpManager.getAllTools().length > 0 ? ", and MCP tools: " + mcpManager.getAllTools().map(t => t.wispyName).join(", ") : ""}.`,
|
|
1528
1630
|
"- file_edit: for targeted text replacement (prefer over write_file for edits)",
|
|
1529
1631
|
"- file_search: grep across codebase",
|
|
1530
1632
|
"- git: any git command",
|
|
@@ -1558,6 +1660,25 @@ async function buildSystemPrompt(messages = []) {
|
|
|
1558
1660
|
parts.push("");
|
|
1559
1661
|
}
|
|
1560
1662
|
|
|
1663
|
+
// Load and inject matching skills
|
|
1664
|
+
const allSkills = await loadSkills();
|
|
1665
|
+
if (allSkills.length > 0 && lastUserMsg) {
|
|
1666
|
+
const matched = matchSkills(lastUserMsg, allSkills);
|
|
1667
|
+
if (matched.length > 0) {
|
|
1668
|
+
parts.push("## Active Skills (instructions — use run_command/web_fetch to execute)");
|
|
1669
|
+
parts.push("Skills are NOT tools — they are guides. Use run_command to execute the commands described in them.");
|
|
1670
|
+
for (const skill of matched.slice(0, 3)) { // Max 3 skills per turn
|
|
1671
|
+
parts.push(`### ${skill.name}`);
|
|
1672
|
+
parts.push(skill.body.slice(0, 5000));
|
|
1673
|
+
parts.push("");
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
// Always list available skills
|
|
1677
|
+
parts.push(`## Available Skills (${allSkills.length} installed)`);
|
|
1678
|
+
parts.push(allSkills.map(s => `- ${s.name}: ${s.description.slice(0, 60)}`).join("\n"));
|
|
1679
|
+
parts.push("");
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1561
1682
|
return parts.join("\n");
|
|
1562
1683
|
}
|
|
1563
1684
|
|
|
@@ -1617,7 +1738,7 @@ async function chatGeminiWithTools(messages, onChunk) {
|
|
|
1617
1738
|
sessionTokens.input += estimateTokens(systemInstruction + inputText);
|
|
1618
1739
|
|
|
1619
1740
|
const geminiTools = [{
|
|
1620
|
-
functionDeclarations:
|
|
1741
|
+
functionDeclarations: getAllToolDefinitions().map(t => ({
|
|
1621
1742
|
name: t.name,
|
|
1622
1743
|
description: t.description,
|
|
1623
1744
|
parameters: t.parameters,
|
|
@@ -1707,7 +1828,7 @@ async function chatOpenAIWithTools(messages, onChunk) {
|
|
|
1707
1828
|
return { role: m.role === "assistant" ? "assistant" : m.role, content: m.content };
|
|
1708
1829
|
});
|
|
1709
1830
|
|
|
1710
|
-
const openaiTools =
|
|
1831
|
+
const openaiTools = getAllToolDefinitions().map(t => ({
|
|
1711
1832
|
type: "function",
|
|
1712
1833
|
function: { name: t.name, description: t.description, parameters: t.parameters },
|
|
1713
1834
|
}));
|
|
@@ -1785,7 +1906,7 @@ async function chatAnthropicWithTools(messages, onChunk) {
|
|
|
1785
1906
|
const inputText = anthropicMessages.map(m => typeof m.content === "string" ? m.content : JSON.stringify(m.content)).join("");
|
|
1786
1907
|
sessionTokens.input += estimateTokens(systemPrompt + inputText);
|
|
1787
1908
|
|
|
1788
|
-
const anthropicTools =
|
|
1909
|
+
const anthropicTools = getAllToolDefinitions().map(t => ({
|
|
1789
1910
|
name: t.name,
|
|
1790
1911
|
description: t.description,
|
|
1791
1912
|
input_schema: t.parameters,
|
|
@@ -2131,6 +2252,30 @@ ${bold("Wispy Commands:")}
|
|
|
2131
2252
|
return true;
|
|
2132
2253
|
}
|
|
2133
2254
|
|
|
2255
|
+
if (cmd === "/skills") {
|
|
2256
|
+
const skills = await loadSkills();
|
|
2257
|
+
if (skills.length === 0) {
|
|
2258
|
+
console.log(dim("No skills installed."));
|
|
2259
|
+
console.log(dim("Add skills to ~/.wispy/skills/ or install OpenClaw skills."));
|
|
2260
|
+
} else {
|
|
2261
|
+
console.log(bold(`\n🧩 Skills (${skills.length} installed):\n`));
|
|
2262
|
+
const bySource = {};
|
|
2263
|
+
for (const s of skills) {
|
|
2264
|
+
const src = s.source.includes("openclaw") ? "OpenClaw" : s.source.includes(".wispy") ? "Wispy" : s.source.includes(".claude") ? "Claude" : "Project";
|
|
2265
|
+
if (!bySource[src]) bySource[src] = [];
|
|
2266
|
+
bySource[src].push(s);
|
|
2267
|
+
}
|
|
2268
|
+
for (const [src, sks] of Object.entries(bySource)) {
|
|
2269
|
+
console.log(` ${bold(src)} (${sks.length}):`);
|
|
2270
|
+
for (const s of sks) {
|
|
2271
|
+
console.log(` ${green(s.name.padEnd(20))} ${dim(s.description.slice(0, 50))}`);
|
|
2272
|
+
}
|
|
2273
|
+
console.log("");
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
return true;
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2134
2279
|
if (cmd === "/sessions" || cmd === "/ls") {
|
|
2135
2280
|
const wsList = await listWorkstreams();
|
|
2136
2281
|
if (wsList.length === 0) {
|
|
@@ -2203,6 +2348,119 @@ ${bold("Wispy Commands:")}
|
|
|
2203
2348
|
process.exit(0);
|
|
2204
2349
|
}
|
|
2205
2350
|
|
|
2351
|
+
// ---------------------------------------------------------------------------
|
|
2352
|
+
// /mcp — MCP server management
|
|
2353
|
+
// ---------------------------------------------------------------------------
|
|
2354
|
+
if (cmd === "/mcp") {
|
|
2355
|
+
const sub = parts[1] ?? "list";
|
|
2356
|
+
|
|
2357
|
+
if (sub === "list") {
|
|
2358
|
+
const status = mcpManager.getStatus();
|
|
2359
|
+
const allTools = mcpManager.getAllTools();
|
|
2360
|
+
if (status.length === 0) {
|
|
2361
|
+
console.log(dim("No MCP servers connected."));
|
|
2362
|
+
console.log(dim(`Config: ${MCP_CONFIG_PATH}`));
|
|
2363
|
+
console.log(dim("Use /mcp connect <name> to connect a server."));
|
|
2364
|
+
} else {
|
|
2365
|
+
console.log(bold(`\n🔌 MCP Servers (${status.length}):\n`));
|
|
2366
|
+
for (const s of status) {
|
|
2367
|
+
const icon = s.connected ? green("●") : red("○");
|
|
2368
|
+
const toolList = s.tools.length > 0
|
|
2369
|
+
? dim(` [${s.tools.slice(0, 5).join(", ")}${s.tools.length > 5 ? "..." : ""}]`)
|
|
2370
|
+
: "";
|
|
2371
|
+
console.log(` ${icon} ${bold(s.name.padEnd(18))} ${s.toolCount} tools${toolList}`);
|
|
2372
|
+
if (s.serverInfo?.name) console.log(dim(` server: ${s.serverInfo.name} v${s.serverInfo.version ?? "?"}`));
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
if (allTools.length > 0) {
|
|
2376
|
+
console.log(bold(`\n🧰 MCP Tools (${allTools.length} total):\n`));
|
|
2377
|
+
for (const t of allTools) {
|
|
2378
|
+
console.log(` ${cyan(t.wispyName.padEnd(30))} ${dim(t.description.slice(0, 60))}`);
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
console.log("");
|
|
2382
|
+
return true;
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
if (sub === "connect") {
|
|
2386
|
+
const serverName = parts[2];
|
|
2387
|
+
if (!serverName) {
|
|
2388
|
+
console.log(yellow("Usage: /mcp connect <server-name>"));
|
|
2389
|
+
return true;
|
|
2390
|
+
}
|
|
2391
|
+
const config = await mcpManager.loadConfig();
|
|
2392
|
+
const serverConfig = config.mcpServers?.[serverName];
|
|
2393
|
+
if (!serverConfig) {
|
|
2394
|
+
console.log(red(`Server "${serverName}" not found in ${MCP_CONFIG_PATH}`));
|
|
2395
|
+
console.log(dim(`Available: ${Object.keys(config.mcpServers ?? {}).join(", ") || "none"}`));
|
|
2396
|
+
return true;
|
|
2397
|
+
}
|
|
2398
|
+
process.stdout.write(dim(` Connecting to "${serverName}"...`));
|
|
2399
|
+
try {
|
|
2400
|
+
const client = await mcpManager.connect(serverName, serverConfig);
|
|
2401
|
+
console.log(green(` ✓ connected (${client.tools.length} tools)`));
|
|
2402
|
+
if (client.tools.length > 0) {
|
|
2403
|
+
console.log(dim(` Tools: ${client.tools.map(t => t.name).join(", ")}`));
|
|
2404
|
+
}
|
|
2405
|
+
} catch (err) {
|
|
2406
|
+
console.log(red(` ✗ failed: ${err.message.slice(0, 120)}`));
|
|
2407
|
+
}
|
|
2408
|
+
return true;
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
if (sub === "disconnect") {
|
|
2412
|
+
const serverName = parts[2];
|
|
2413
|
+
if (!serverName) {
|
|
2414
|
+
console.log(yellow("Usage: /mcp disconnect <server-name>"));
|
|
2415
|
+
return true;
|
|
2416
|
+
}
|
|
2417
|
+
const ok = mcpManager.disconnect(serverName);
|
|
2418
|
+
console.log(ok ? green(`✓ Disconnected "${serverName}"`) : yellow(`"${serverName}" was not connected`));
|
|
2419
|
+
return true;
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
if (sub === "config") {
|
|
2423
|
+
console.log(dim(`Config file: ${MCP_CONFIG_PATH}`));
|
|
2424
|
+
try {
|
|
2425
|
+
const cfg = await mcpManager.loadConfig();
|
|
2426
|
+
const servers = cfg.mcpServers ?? {};
|
|
2427
|
+
console.log(bold(`\nDefined servers (${Object.keys(servers).length}):\n`));
|
|
2428
|
+
for (const [name, s] of Object.entries(servers)) {
|
|
2429
|
+
const status = s.disabled ? dim("disabled") : green("enabled");
|
|
2430
|
+
console.log(` ${name.padEnd(20)} ${status}`);
|
|
2431
|
+
console.log(dim(` cmd: ${s.command} ${(s.args ?? []).join(" ")}`));
|
|
2432
|
+
}
|
|
2433
|
+
} catch {
|
|
2434
|
+
console.log(dim("No config found."));
|
|
2435
|
+
}
|
|
2436
|
+
return true;
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
if (sub === "reload") {
|
|
2440
|
+
console.log(dim("Reloading MCP servers..."));
|
|
2441
|
+
mcpManager.disconnectAll();
|
|
2442
|
+
const results = await mcpManager.autoConnect();
|
|
2443
|
+
for (const r of results) {
|
|
2444
|
+
if (r.status === "connected") console.log(green(` ✓ ${r.name} (${r.tools} tools)`));
|
|
2445
|
+
else if (r.status === "disabled") console.log(dim(` ○ ${r.name} (disabled)`));
|
|
2446
|
+
else console.log(red(` ✗ ${r.name}: ${r.error?.slice(0, 80)}`));
|
|
2447
|
+
}
|
|
2448
|
+
return true;
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
// Unknown subcommand
|
|
2452
|
+
console.log(`
|
|
2453
|
+
${bold("/mcp commands:")}
|
|
2454
|
+
${cyan("/mcp list")} List connected servers and tools
|
|
2455
|
+
${cyan("/mcp connect <name>")} Connect a server from config
|
|
2456
|
+
${cyan("/mcp disconnect <name>")} Disconnect a server
|
|
2457
|
+
${cyan("/mcp config")} Show mcp.json config
|
|
2458
|
+
${cyan("/mcp reload")} Reconnect all servers
|
|
2459
|
+
${dim(`Config: ${MCP_CONFIG_PATH}`)}
|
|
2460
|
+
`);
|
|
2461
|
+
return true;
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2206
2464
|
return false;
|
|
2207
2465
|
}
|
|
2208
2466
|
|
|
@@ -2500,6 +2758,28 @@ if (!API_KEY && PROVIDER !== "ollama") {
|
|
|
2500
2758
|
}
|
|
2501
2759
|
}
|
|
2502
2760
|
|
|
2761
|
+
// Auto-connect MCP servers from ~/.wispy/mcp.json
|
|
2762
|
+
await ensureDefaultMcpConfig(MCP_CONFIG_PATH);
|
|
2763
|
+
{
|
|
2764
|
+
const mcpResults = await mcpManager.autoConnect();
|
|
2765
|
+
const connected = mcpResults.filter(r => r.status === "connected");
|
|
2766
|
+
const failed = mcpResults.filter(r => r.status === "failed");
|
|
2767
|
+
if (connected.length > 0) {
|
|
2768
|
+
// Quiet success — only show if verbose
|
|
2769
|
+
// console.log(dim(`🔌 MCP: ${connected.map(r => `${r.name}(${r.tools})`).join(", ")}`));
|
|
2770
|
+
}
|
|
2771
|
+
if (failed.length > 0 && process.env.WISPY_DEBUG) {
|
|
2772
|
+
for (const r of failed) {
|
|
2773
|
+
console.error(dim(`⚠ MCP "${r.name}" failed: ${r.error?.slice(0, 80)}`));
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
// Graceful MCP cleanup on exit
|
|
2779
|
+
process.on("exit", () => { try { mcpManager.disconnectAll(); } catch {} });
|
|
2780
|
+
process.on("SIGINT", () => { mcpManager.disconnectAll(); process.exit(0); });
|
|
2781
|
+
process.on("SIGTERM", () => { mcpManager.disconnectAll(); process.exit(0); });
|
|
2782
|
+
|
|
2503
2783
|
// Auto-start server before entering REPL or one-shot
|
|
2504
2784
|
const serverStatus = await startServerIfNeeded();
|
|
2505
2785
|
if (serverStatus.started) {
|
|
@@ -2585,6 +2865,7 @@ ${bold("In-session commands:")}
|
|
|
2585
2865
|
/workstreams List all workstreams
|
|
2586
2866
|
/overview Director view — all workstreams at a glance
|
|
2587
2867
|
/search <keyword> Search across all workstreams
|
|
2868
|
+
/skills List installed skills (OpenClaw/Claude compatible)
|
|
2588
2869
|
/sessions List all sessions
|
|
2589
2870
|
/delete <name> Delete a session
|
|
2590
2871
|
/export [md|clipboard] Export conversation
|