wispy-cli 2.0.1 → 2.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/bin/wispy.mjs +84 -5
- package/lib/commands/skills-cmd.mjs +219 -0
- package/lib/wispy-repl.mjs +245 -3
- package/package.json +1 -1
package/bin/wispy.mjs
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Wispy CLI entry point
|
|
5
5
|
*
|
|
6
6
|
* Flags:
|
|
7
|
-
* ui Launch
|
|
8
|
-
* --tui
|
|
7
|
+
* ui Launch workspace TUI
|
|
8
|
+
* --tui Alias for tui (kept for compat)
|
|
9
9
|
* --serve Start all configured channel bots
|
|
10
10
|
* --telegram Start Telegram bot only
|
|
11
11
|
* --discord Start Discord bot only
|
|
@@ -23,6 +23,84 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
23
23
|
|
|
24
24
|
const args = process.argv.slice(2);
|
|
25
25
|
|
|
26
|
+
// ── ws sub-command ────────────────────────────────────────────────────────────
|
|
27
|
+
if (args[0] === "ws") {
|
|
28
|
+
const { handleWsCommand } = await import(
|
|
29
|
+
path.join(__dirname, "..", "lib", "commands", "ws.mjs")
|
|
30
|
+
);
|
|
31
|
+
await handleWsCommand(args);
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── trust sub-command ─────────────────────────────────────────────────────────
|
|
36
|
+
if (args[0] === "trust") {
|
|
37
|
+
const { handleTrustCommand } = await import(
|
|
38
|
+
path.join(__dirname, "..", "lib", "commands", "trust.mjs")
|
|
39
|
+
);
|
|
40
|
+
await handleTrustCommand(args);
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── where sub-command ─────────────────────────────────────────────────────────
|
|
45
|
+
if (args[0] === "where") {
|
|
46
|
+
const { cmdWhere } = await import(
|
|
47
|
+
path.join(__dirname, "..", "lib", "commands", "continuity.mjs")
|
|
48
|
+
);
|
|
49
|
+
await cmdWhere();
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── handoff sub-command ───────────────────────────────────────────────────────
|
|
54
|
+
if (args[0] === "handoff") {
|
|
55
|
+
const { handleContinuityCommand } = await import(
|
|
56
|
+
path.join(__dirname, "..", "lib", "commands", "continuity.mjs")
|
|
57
|
+
);
|
|
58
|
+
await handleContinuityCommand(args);
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── skill sub-command ─────────────────────────────────────────────────────────
|
|
63
|
+
if (args[0] === "skill") {
|
|
64
|
+
const { handleSkillCommand } = await import(
|
|
65
|
+
path.join(__dirname, "..", "lib", "commands", "skills-cmd.mjs")
|
|
66
|
+
);
|
|
67
|
+
await handleSkillCommand(args);
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── teach sub-command ─────────────────────────────────────────────────────────
|
|
72
|
+
if (args[0] === "teach") {
|
|
73
|
+
const { cmdTeach } = await import(
|
|
74
|
+
path.join(__dirname, "..", "lib", "commands", "skills-cmd.mjs")
|
|
75
|
+
);
|
|
76
|
+
await cmdTeach(args[1]);
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── improve sub-command ───────────────────────────────────────────────────────
|
|
81
|
+
if (args[0] === "improve") {
|
|
82
|
+
const { cmdImproveSkill } = await import(
|
|
83
|
+
path.join(__dirname, "..", "lib", "commands", "skills-cmd.mjs")
|
|
84
|
+
);
|
|
85
|
+
const name = args[1];
|
|
86
|
+
const feedback = args.slice(2).join(" ").replace(/^["']|["']$/g, "");
|
|
87
|
+
await cmdImproveSkill(name, feedback);
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── dry sub-command ───────────────────────────────────────────────────────────
|
|
92
|
+
if (args[0] === "dry") {
|
|
93
|
+
// Re-launch wispy with DRY_RUN env set, passing remaining args
|
|
94
|
+
const { spawn } = await import("node:child_process");
|
|
95
|
+
const remaining = args.slice(1);
|
|
96
|
+
const child = spawn(process.execPath, [process.argv[1], ...remaining], {
|
|
97
|
+
stdio: "inherit",
|
|
98
|
+
env: { ...process.env, WISPY_DRY_RUN: "1" },
|
|
99
|
+
});
|
|
100
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
101
|
+
await new Promise(() => {}); // keep alive until child exits
|
|
102
|
+
}
|
|
103
|
+
|
|
26
104
|
// ── setup / init sub-command ──────────────────────────────────────────────────
|
|
27
105
|
if (args[0] === "setup" || args[0] === "init") {
|
|
28
106
|
const { OnboardingWizard } = await import(
|
|
@@ -1024,7 +1102,8 @@ if (serveMode || telegramMode || discordMode || slackMode) {
|
|
|
1024
1102
|
const isInteractiveStart = !args.some(a =>
|
|
1025
1103
|
["--serve", "--telegram", "--discord", "--slack", "--server",
|
|
1026
1104
|
"status", "setup", "init", "connect", "disconnect", "deploy",
|
|
1027
|
-
"cron", "audit", "log", "server", "node", "channel", "sync", "
|
|
1105
|
+
"cron", "audit", "log", "server", "node", "channel", "sync", "tui",
|
|
1106
|
+
"ws", "trust", "where", "handoff", "skill", "teach", "improve", "dry"].includes(a)
|
|
1028
1107
|
);
|
|
1029
1108
|
|
|
1030
1109
|
if (isInteractiveStart) {
|
|
@@ -1047,10 +1126,10 @@ if (isInteractiveStart) {
|
|
|
1047
1126
|
|
|
1048
1127
|
// ── TUI mode ──────────────────────────────────────────────────────────────────
|
|
1049
1128
|
// `wispy ui` is the primary way; `--tui` is kept as a hidden backwards-compat alias
|
|
1050
|
-
const tuiMode = args[0] === "
|
|
1129
|
+
const tuiMode = args[0] === "tui" || args.includes("--tui");
|
|
1051
1130
|
|
|
1052
1131
|
if (tuiMode) {
|
|
1053
|
-
const newArgs = args.filter(a => a !== "--tui" && a !== "
|
|
1132
|
+
const newArgs = args.filter(a => a !== "--tui" && a !== "tui");
|
|
1054
1133
|
process.argv = [process.argv[0], process.argv[1], ...newArgs];
|
|
1055
1134
|
const tuiScript = path.join(__dirname, "..", "lib", "wispy-tui.mjs");
|
|
1056
1135
|
await import(tuiScript);
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/commands/skills-cmd.mjs — Skill CLI commands
|
|
3
|
+
*
|
|
4
|
+
* wispy skill list learned skills
|
|
5
|
+
* wispy skill run <name> run a skill
|
|
6
|
+
* wispy teach <name> create skill from last conversation
|
|
7
|
+
* wispy improve <name> "..." improve a skill with feedback
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
|
|
14
|
+
const WISPY_DIR = path.join(os.homedir(), ".wispy");
|
|
15
|
+
const SKILLS_DIR = path.join(WISPY_DIR, "skills");
|
|
16
|
+
const CONVERSATIONS_DIR = path.join(WISPY_DIR, "conversations");
|
|
17
|
+
|
|
18
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
19
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
20
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
21
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
22
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
23
|
+
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
24
|
+
|
|
25
|
+
async function readJsonOr(filePath, fallback = null) {
|
|
26
|
+
try { return JSON.parse(await readFile(filePath, "utf8")); } catch { return fallback; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function getActiveWorkstream() {
|
|
30
|
+
const envWs = process.env.WISPY_WORKSTREAM;
|
|
31
|
+
if (envWs) return envWs;
|
|
32
|
+
const cfg = await readJsonOr(path.join(WISPY_DIR, "config.json"), {});
|
|
33
|
+
return cfg.workstream ?? "default";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function listSkills() {
|
|
37
|
+
try {
|
|
38
|
+
const files = await readdir(SKILLS_DIR);
|
|
39
|
+
const skills = [];
|
|
40
|
+
for (const f of files.filter(f => f.endsWith(".json"))) {
|
|
41
|
+
const skill = await readJsonOr(path.join(SKILLS_DIR, f), null);
|
|
42
|
+
if (skill) skills.push(skill);
|
|
43
|
+
}
|
|
44
|
+
return skills.sort((a, b) => (b.timesUsed ?? 0) - (a.timesUsed ?? 0));
|
|
45
|
+
} catch { return []; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Commands ─────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
export async function cmdSkillList() {
|
|
51
|
+
const skills = await listSkills();
|
|
52
|
+
|
|
53
|
+
if (skills.length === 0) {
|
|
54
|
+
console.log(dim("\n🧠 No learned skills yet."));
|
|
55
|
+
console.log(dim(" Wispy auto-learns from complex multi-tool tasks."));
|
|
56
|
+
console.log(dim(" Or create one: wispy teach <name>\n"));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log(`\n${bold(`🧠 Learned Skills (${skills.length})`)}\n`);
|
|
61
|
+
|
|
62
|
+
for (const s of skills) {
|
|
63
|
+
const used = s.timesUsed > 0 ? dim(` · used ${s.timesUsed}x`) : "";
|
|
64
|
+
const version = s.version > 1 ? dim(` · v${s.version}`) : "";
|
|
65
|
+
const tags = s.tags?.length > 0 ? dim(` [${s.tags.join(", ")}]`) : "";
|
|
66
|
+
const lastUsed = s.lastUsed ? dim(` · last used ${new Date(s.lastUsed).toLocaleDateString()}`) : "";
|
|
67
|
+
|
|
68
|
+
console.log(` ${green("/" + s.name.padEnd(25))} ${s.description ?? ""}${used}${version}${lastUsed}`);
|
|
69
|
+
if (tags) console.log(` ${" ".repeat(27)}${tags}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log(dim("\n Run: wispy skill run <name> or /<skill-name> in REPL"));
|
|
73
|
+
console.log(dim(" Create: wispy teach <name>"));
|
|
74
|
+
console.log(dim(" Improve: wispy improve <name> \"feedback\"\n"));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function cmdSkillRun(name) {
|
|
78
|
+
if (!name) {
|
|
79
|
+
console.log(yellow("Usage: wispy skill run <name>"));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const { WispyEngine } = await import("../../core/engine.mjs");
|
|
84
|
+
const engine = new WispyEngine();
|
|
85
|
+
const ok = await engine.init({ skipMcp: false });
|
|
86
|
+
if (!ok) {
|
|
87
|
+
console.log(red("❌ No AI provider configured. Run: wispy setup"));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const skill = await engine.skills.get(name);
|
|
92
|
+
if (!skill) {
|
|
93
|
+
console.log(red(`Skill '${name}' not found.`));
|
|
94
|
+
const allSkills = await listSkills();
|
|
95
|
+
if (allSkills.length > 0) {
|
|
96
|
+
console.log(dim(`Available: ${allSkills.map(s => s.name).join(", ")}`));
|
|
97
|
+
}
|
|
98
|
+
engine.destroy();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(dim(`\n🧠 Running skill: ${skill.name} (v${skill.version ?? 1})`));
|
|
103
|
+
console.log(dim(` ${skill.description ?? skill.prompt.slice(0, 80)}\n`));
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
process.stdout.write(cyan("🌿 "));
|
|
107
|
+
const result = await engine.skills.execute(name, {}, null);
|
|
108
|
+
if (result?.content) {
|
|
109
|
+
console.log(result.content);
|
|
110
|
+
}
|
|
111
|
+
console.log("");
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.log(red(`\n✗ Skill error: ${err.message}`));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
engine.destroy();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function cmdTeach(name) {
|
|
120
|
+
if (!name) {
|
|
121
|
+
console.log(yellow("Usage: wispy teach <name>"));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const { WispyEngine } = await import("../../core/engine.mjs");
|
|
126
|
+
const engine = new WispyEngine();
|
|
127
|
+
const ok = await engine.init({ skipMcp: true });
|
|
128
|
+
if (!ok) {
|
|
129
|
+
console.log(red("❌ No AI provider configured."));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Load last conversation
|
|
134
|
+
const workstream = await getActiveWorkstream();
|
|
135
|
+
const convFile = path.join(CONVERSATIONS_DIR, `${workstream}.json`);
|
|
136
|
+
const conv = await readJsonOr(convFile, []);
|
|
137
|
+
|
|
138
|
+
if (conv.length === 0) {
|
|
139
|
+
console.log(yellow("No conversation found in current workstream to create skill from."));
|
|
140
|
+
engine.destroy();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const userMessages = conv.filter(m => m.role === "user").slice(-5).map(m => m.content);
|
|
145
|
+
const taskSummary = userMessages.join(" / ").slice(0, 500);
|
|
146
|
+
|
|
147
|
+
console.log(`\n${bold("🧠 Teaching skill:")} ${cyan(name)}`);
|
|
148
|
+
console.log(dim(` Based on last ${userMessages.length} messages in '${workstream}'`));
|
|
149
|
+
process.stdout.write(dim(" Analyzing conversation..."));
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const skill = await engine.skills.create({
|
|
153
|
+
name,
|
|
154
|
+
description: `Created from '${workstream}' conversation`,
|
|
155
|
+
prompt: taskSummary,
|
|
156
|
+
tools: [],
|
|
157
|
+
tags: [workstream],
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
console.log(green(" ✓"));
|
|
161
|
+
console.log(green(`\n✅ Skill '${name}' created!`));
|
|
162
|
+
console.log(dim(` Prompt: ${skill.prompt.slice(0, 100)}...`));
|
|
163
|
+
console.log(dim(` Run: /${name} or wispy skill run ${name}\n`));
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.log(red(`\n✗ Failed: ${err.message}`));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
engine.destroy();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function cmdImproveSkill(name, feedback) {
|
|
172
|
+
if (!name || !feedback) {
|
|
173
|
+
console.log(yellow('Usage: wispy improve <name> "feedback"'));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const { WispyEngine } = await import("../../core/engine.mjs");
|
|
178
|
+
const engine = new WispyEngine();
|
|
179
|
+
const ok = await engine.init({ skipMcp: true });
|
|
180
|
+
if (!ok) {
|
|
181
|
+
console.log(red("❌ No AI provider configured."));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const skill = await engine.skills.get(name);
|
|
186
|
+
if (!skill) {
|
|
187
|
+
console.log(red(`Skill '${name}' not found.`));
|
|
188
|
+
engine.destroy();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
process.stdout.write(`${dim("🧠 Improving skill '")}${name}${dim("'...")} `);
|
|
193
|
+
try {
|
|
194
|
+
const updated = await engine.skills.improve(name, feedback);
|
|
195
|
+
console.log(green(`✓ done (v${updated.version})`));
|
|
196
|
+
console.log(dim(` New prompt: ${updated.prompt.slice(0, 100)}...`));
|
|
197
|
+
} catch (err) {
|
|
198
|
+
console.log(red(`\n✗ ${err.message}`));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
engine.destroy();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function handleSkillCommand(args) {
|
|
205
|
+
const sub = args[1];
|
|
206
|
+
|
|
207
|
+
if (!sub) return cmdSkillList();
|
|
208
|
+
if (sub === "run") return cmdSkillRun(args[2]);
|
|
209
|
+
if (sub === "list") return cmdSkillList();
|
|
210
|
+
|
|
211
|
+
console.log(`
|
|
212
|
+
${bold("🧠 Skill Commands")}
|
|
213
|
+
|
|
214
|
+
wispy skill ${dim("list learned skills")}
|
|
215
|
+
wispy skill run <name> ${dim("run a skill")}
|
|
216
|
+
wispy teach <name> ${dim("create skill from last conversation")}
|
|
217
|
+
wispy improve <name> "..." ${dim("improve a skill with feedback")}
|
|
218
|
+
`);
|
|
219
|
+
}
|
package/lib/wispy-repl.mjs
CHANGED
|
@@ -331,6 +331,41 @@ ${bold("Wispy Commands:")}
|
|
|
331
331
|
${cyan("/memories")} List all memory files
|
|
332
332
|
${cyan("/recall")} <query> Search memories
|
|
333
333
|
|
|
334
|
+
${bold("Workstream (v1.4):")}
|
|
335
|
+
${cyan("/ws")} List workstreams
|
|
336
|
+
${cyan("/ws")} <name> Switch to workstream
|
|
337
|
+
${cyan("/ws new")} <name> Create new workstream
|
|
338
|
+
${cyan("/ws archive")} <name> Archive workstream
|
|
339
|
+
${cyan("/ws status")} All workstreams status
|
|
340
|
+
${cyan("/ws search")} <q> Cross-workstream search
|
|
341
|
+
|
|
342
|
+
${bold("Trust (v1.4):")}
|
|
343
|
+
${cyan("/trust")} Show security + recent approvals
|
|
344
|
+
${cyan("/trust")} <level> Change level (careful/balanced/yolo)
|
|
345
|
+
${cyan("/trust log")} Audit log
|
|
346
|
+
${cyan("/receipt")} Show last receipt
|
|
347
|
+
${cyan("/dry")} Toggle dry-run mode
|
|
348
|
+
|
|
349
|
+
${bold("Continuity (v1.4):")}
|
|
350
|
+
${cyan("/where")} Show current mode
|
|
351
|
+
${cyan("/handoff")} Sync + context summary
|
|
352
|
+
${cyan("/sync")} Trigger sync
|
|
353
|
+
|
|
354
|
+
${bold("Skills (v1.4):")}
|
|
355
|
+
${cyan("/skills")} List skills
|
|
356
|
+
${cyan("/teach")} <name> Create skill from this conversation
|
|
357
|
+
${cyan("/improve")} <name> Improve skill
|
|
358
|
+
${cyan("/<skill-name>")} Run any learned skill directly
|
|
359
|
+
|
|
360
|
+
${bold("Quick shortcuts:")}
|
|
361
|
+
${cyan("/o")} Overview (alias for /ws status)
|
|
362
|
+
${cyan("/w")} Workstream list
|
|
363
|
+
${cyan("/a")} Agents list
|
|
364
|
+
${cyan("/m")} Memories
|
|
365
|
+
${cyan("/t")} Timeline (recent actions)
|
|
366
|
+
${cyan("/s")} Sync status
|
|
367
|
+
${cyan("/d")} Toggle dry-run
|
|
368
|
+
|
|
334
369
|
${bold("Sub-agent Commands (v0.9):")}
|
|
335
370
|
${cyan("/agents")} List active/recent sub-agents
|
|
336
371
|
${cyan("/agent")} <id> Show sub-agent details and result
|
|
@@ -399,7 +434,200 @@ ${bold("Permissions & Audit (v1.1):")}
|
|
|
399
434
|
return true;
|
|
400
435
|
}
|
|
401
436
|
|
|
402
|
-
|
|
437
|
+
// ── Quick single-letter shortcuts ────────────────────────────────────────────
|
|
438
|
+
if (cmd === "/o") {
|
|
439
|
+
// Overview — alias for /ws status
|
|
440
|
+
const { cmdWsStatus } = await import("./commands/ws.mjs");
|
|
441
|
+
await cmdWsStatus();
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (cmd === "/w") {
|
|
446
|
+
// Workstream list
|
|
447
|
+
const { cmdWsList } = await import("./commands/ws.mjs");
|
|
448
|
+
await cmdWsList();
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (cmd === "/a") {
|
|
453
|
+
// Agents — handled below (delegates to /agents)
|
|
454
|
+
const inMemory = engine.subagents.list();
|
|
455
|
+
const history = await engine.subagents.listHistory(10);
|
|
456
|
+
const inMemoryIds = new Set(inMemory.map(a => a.id));
|
|
457
|
+
const all = [
|
|
458
|
+
...inMemory.map(a => a.toJSON()),
|
|
459
|
+
...history.filter(h => !inMemoryIds.has(h.id)),
|
|
460
|
+
].slice(0, 20);
|
|
461
|
+
if (all.length === 0) {
|
|
462
|
+
console.log(dim("No sub-agents yet."));
|
|
463
|
+
} else {
|
|
464
|
+
console.log(bold(`\n🤖 Agents (${all.length}):\n`));
|
|
465
|
+
for (const a of all) {
|
|
466
|
+
const icon = { completed: green("✓"), failed: red("✗"), running: cyan("⟳"), killed: dim("✕") }[a.status] ?? "?";
|
|
467
|
+
console.log(` ${icon} ${bold(a.id)} ${dim("·")} ${a.label}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (cmd === "/m") {
|
|
474
|
+
// Memories — alias for /memories
|
|
475
|
+
const keys = await engine.memory.list();
|
|
476
|
+
if (keys.length === 0) {
|
|
477
|
+
console.log(dim("No memories stored yet. Use /remember <text>"));
|
|
478
|
+
} else {
|
|
479
|
+
console.log(bold(`\n🧠 Memories (${keys.length}):\n`));
|
|
480
|
+
for (const k of keys) {
|
|
481
|
+
console.log(` ${cyan(k.key.padEnd(30))} ${dim(k.preview ?? "")}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (cmd === "/t") {
|
|
488
|
+
// Timeline — recent audit events
|
|
489
|
+
const events = await engine.audit.getRecent(15);
|
|
490
|
+
if (events.length === 0) {
|
|
491
|
+
console.log(dim("No recent activity."));
|
|
492
|
+
} else {
|
|
493
|
+
console.log(bold(`\n⏱ Timeline (last ${events.length} actions):\n`));
|
|
494
|
+
for (const evt of events) {
|
|
495
|
+
const ts = new Date(evt.timestamp).toLocaleTimeString();
|
|
496
|
+
const icons = {
|
|
497
|
+
tool_call: "🔧", tool_result: "✅", approval_requested: "⚠️ ",
|
|
498
|
+
approval_granted: "✅", approval_denied: "❌", message_sent: "🌿",
|
|
499
|
+
subagent_spawned: "🤖", error: "🚨",
|
|
500
|
+
};
|
|
501
|
+
const icon = icons[evt.type] ?? "•";
|
|
502
|
+
const detail = evt.tool ? ` ${cyan(evt.tool)}` : evt.content ? ` ${dim(evt.content.slice(0, 50))}` : "";
|
|
503
|
+
console.log(` ${dim(ts)} ${icon} ${evt.type}${detail}`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (cmd === "/s") {
|
|
510
|
+
// Sync status
|
|
511
|
+
try {
|
|
512
|
+
const { SyncManager } = await import("../core/sync.mjs");
|
|
513
|
+
const cfg = await SyncManager.loadConfig();
|
|
514
|
+
if (!cfg.remoteUrl) {
|
|
515
|
+
console.log(dim("No remote sync configured. Use: wispy sync auto --remote <url>"));
|
|
516
|
+
} else {
|
|
517
|
+
console.log(bold(`\n🔄 Sync Status\n`));
|
|
518
|
+
console.log(` Remote: ${cyan(cfg.remoteUrl)}`);
|
|
519
|
+
const lastSync = cfg.lastSync ? new Date(cfg.lastSync).toLocaleString() : "never";
|
|
520
|
+
console.log(` Last sync: ${dim(lastSync)}`);
|
|
521
|
+
console.log(` Auto-sync: ${cfg.autoSync ? green("enabled") : dim("disabled")}`);
|
|
522
|
+
}
|
|
523
|
+
} catch {
|
|
524
|
+
console.log(dim("Sync not available."));
|
|
525
|
+
}
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (cmd === "/d") {
|
|
530
|
+
// Toggle dry-run
|
|
531
|
+
if (engine.dryRunMode) {
|
|
532
|
+
engine.dryRunMode = false;
|
|
533
|
+
console.log(green("🌿 Dry-run OFF — tools will execute normally"));
|
|
534
|
+
} else {
|
|
535
|
+
engine.dryRunMode = true;
|
|
536
|
+
console.log(yellow("🔍 Dry-run ON — all tool calls will be previewed only"));
|
|
537
|
+
}
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ── /ws <subcommand> ─────────────────────────────────────────────────────────
|
|
542
|
+
if (cmd === "/ws") {
|
|
543
|
+
const { handleWsCommand } = await import("./commands/ws.mjs");
|
|
544
|
+
// Convert from /ws args to ws args format
|
|
545
|
+
const wsArgs = ["ws", ...parts.slice(1)];
|
|
546
|
+
await handleWsCommand(wsArgs);
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// ── /trust <subcommand> ──────────────────────────────────────────────────────
|
|
551
|
+
if (cmd === "/trust") {
|
|
552
|
+
const { handleTrustCommand } = await import("./commands/trust.mjs");
|
|
553
|
+
const trustArgs = ["trust", ...parts.slice(1)];
|
|
554
|
+
await handleTrustCommand(trustArgs);
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// ── /receipt ─────────────────────────────────────────────────────────────────
|
|
559
|
+
if (cmd === "/receipt") {
|
|
560
|
+
const { cmdTrustReceipt } = await import("./commands/trust.mjs");
|
|
561
|
+
const sessionId = parts[1] ?? engine.sessions?.list?.()?.find(Boolean)?.id;
|
|
562
|
+
await cmdTrustReceipt(sessionId);
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// ── /dry (toggle dry-run) ────────────────────────────────────────────────────
|
|
567
|
+
if (cmd === "/dry") {
|
|
568
|
+
engine.dryRunMode = !engine.dryRunMode;
|
|
569
|
+
if (engine.dryRunMode) {
|
|
570
|
+
console.log(yellow("🔍 Dry-run ON — all tool calls will be previewed only"));
|
|
571
|
+
} else {
|
|
572
|
+
console.log(green("🌿 Dry-run OFF — tools will execute normally"));
|
|
573
|
+
}
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ── /where ────────────────────────────────────────────────────────────────────
|
|
578
|
+
if (cmd === "/where") {
|
|
579
|
+
const { cmdWhere } = await import("./commands/continuity.mjs");
|
|
580
|
+
await cmdWhere();
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ── /handoff ──────────────────────────────────────────────────────────────────
|
|
585
|
+
if (cmd === "/handoff") {
|
|
586
|
+
const { handleContinuityCommand } = await import("./commands/continuity.mjs");
|
|
587
|
+
const sub = parts[1] ?? "cloud";
|
|
588
|
+
await handleContinuityCommand(["handoff", sub]);
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// ── /sync ─────────────────────────────────────────────────────────────────────
|
|
593
|
+
if (cmd === "/sync") {
|
|
594
|
+
try {
|
|
595
|
+
const { SyncManager } = await import("../core/sync.mjs");
|
|
596
|
+
const cfg = await SyncManager.loadConfig();
|
|
597
|
+
if (!cfg.remoteUrl) {
|
|
598
|
+
console.log(dim("No remote sync configured. Run: wispy sync auto --remote <url>"));
|
|
599
|
+
} else {
|
|
600
|
+
process.stdout.write(dim(`🔄 Syncing with ${cfg.remoteUrl}...`));
|
|
601
|
+
const mgr = new SyncManager({ remoteUrl: cfg.remoteUrl, token: cfg.token, strategy: "newer-wins" });
|
|
602
|
+
const result = await mgr.sync(cfg.remoteUrl, cfg.token, {});
|
|
603
|
+
console.log(green(` ✓ pushed ${result.pushed}, pulled ${result.pulled}`));
|
|
604
|
+
}
|
|
605
|
+
} catch (err) {
|
|
606
|
+
console.log(red(`✗ Sync failed: ${err.message.slice(0, 80)}`));
|
|
607
|
+
}
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// ── /teach <name> ─────────────────────────────────────────────────────────────
|
|
612
|
+
if (cmd === "/teach") {
|
|
613
|
+
const skillName = parts[1];
|
|
614
|
+
if (!skillName) { console.log(yellow("Usage: /teach <name>")); return true; }
|
|
615
|
+
const { cmdTeach } = await import("./commands/skills-cmd.mjs");
|
|
616
|
+
await cmdTeach(skillName);
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ── /improve <name> ───────────────────────────────────────────────────────────
|
|
621
|
+
if (cmd === "/improve") {
|
|
622
|
+
const skillName = parts[1];
|
|
623
|
+
const feedback = parts.slice(2).join(" ").replace(/^["']|["']$/g, "");
|
|
624
|
+
if (!skillName || !feedback) { console.log(yellow('Usage: /improve <name> "feedback"')); return true; }
|
|
625
|
+
const { cmdImproveSkill } = await import("./commands/skills-cmd.mjs");
|
|
626
|
+
await cmdImproveSkill(skillName, feedback);
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (cmd === "/workstreams" || (cmd === "/ws" && !parts[1])) {
|
|
403
631
|
const wsList = await listWorkstreams();
|
|
404
632
|
if (wsList.length === 0) { console.log(dim("No workstreams yet.")); return true; }
|
|
405
633
|
console.log(bold("\n📋 Workstreams:\n"));
|
|
@@ -857,9 +1085,15 @@ ${bold("Permissions & Audit (v1.1):")}
|
|
|
857
1085
|
// ---------------------------------------------------------------------------
|
|
858
1086
|
|
|
859
1087
|
async function runRepl(engine) {
|
|
1088
|
+
// Initialize dry-run from env flag (set by `wispy dry "..."`)
|
|
1089
|
+
if (process.env.WISPY_DRY_RUN === "1") {
|
|
1090
|
+
engine.dryRunMode = true;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
860
1093
|
const wsLabel = ACTIVE_WORKSTREAM === "default" ? "" : ` ${dim("·")} ${cyan(ACTIVE_WORKSTREAM)}`;
|
|
1094
|
+
const dryLabel = engine.dryRunMode ? ` ${yellow("· dry-run")}` : "";
|
|
861
1095
|
console.log(`
|
|
862
|
-
${bold("🌿 Wispy")}${wsLabel} ${dim(`· ${engine.model}`)}
|
|
1096
|
+
${bold("🌿 Wispy")}${wsLabel}${dryLabel} ${dim(`· ${engine.model}`)}
|
|
863
1097
|
${dim(`${engine.provider} · /help for commands · Ctrl+C to exit`)}
|
|
864
1098
|
`);
|
|
865
1099
|
|
|
@@ -872,6 +1106,13 @@ async function runRepl(engine) {
|
|
|
872
1106
|
historySize: 100,
|
|
873
1107
|
});
|
|
874
1108
|
|
|
1109
|
+
// Dynamic prompt — shows workstream and dry-run status
|
|
1110
|
+
function updatePrompt() {
|
|
1111
|
+
const ws = ACTIVE_WORKSTREAM !== "default" ? `${cyan(ACTIVE_WORKSTREAM)} ` : "";
|
|
1112
|
+
const dry = engine.dryRunMode ? yellow("(dry) ") : "";
|
|
1113
|
+
rl.setPrompt(ws + dry + green("› "));
|
|
1114
|
+
}
|
|
1115
|
+
updatePrompt();
|
|
875
1116
|
rl.prompt();
|
|
876
1117
|
|
|
877
1118
|
rl.on("line", async (line) => {
|
|
@@ -880,7 +1121,7 @@ async function runRepl(engine) {
|
|
|
880
1121
|
|
|
881
1122
|
if (input.startsWith("/")) {
|
|
882
1123
|
const handled = await handleSlashCommand(input, engine, conversation);
|
|
883
|
-
if (handled) { rl.prompt(); return; }
|
|
1124
|
+
if (handled) { updatePrompt(); rl.prompt(); return; }
|
|
884
1125
|
}
|
|
885
1126
|
|
|
886
1127
|
conversation.push({ role: "user", content: input });
|
|
@@ -895,6 +1136,7 @@ async function runRepl(engine) {
|
|
|
895
1136
|
onChunk: (chunk) => process.stdout.write(chunk),
|
|
896
1137
|
systemPrompt: await engine._buildSystemPrompt(input),
|
|
897
1138
|
noSave: true,
|
|
1139
|
+
dryRun: engine.dryRunMode ?? false,
|
|
898
1140
|
onSkillLearned: (skill) => {
|
|
899
1141
|
console.log(cyan(`\n💡 Learned new skill: '${skill.name}' — use /${skill.name} next time`));
|
|
900
1142
|
},
|