wispy-cli 2.0.1 → 2.0.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/bin/wispy.mjs CHANGED
@@ -4,8 +4,8 @@
4
4
  * Wispy CLI entry point
5
5
  *
6
6
  * Flags:
7
- * ui Launch Ink-based TUI mode (primary)
8
- * --tui Launch Ink-based TUI mode (alias, kept for compatibility)
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,7 @@ 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", "ui"].includes(a)
1105
+ "cron", "audit", "log", "server", "node", "channel", "sync", "tui"].includes(a)
1028
1106
  );
1029
1107
 
1030
1108
  if (isInteractiveStart) {
@@ -1047,10 +1125,10 @@ if (isInteractiveStart) {
1047
1125
 
1048
1126
  // ── TUI mode ──────────────────────────────────────────────────────────────────
1049
1127
  // `wispy ui` is the primary way; `--tui` is kept as a hidden backwards-compat alias
1050
- const tuiMode = args[0] === "ui" || args.includes("--tui");
1128
+ const tuiMode = args[0] === "tui" || args.includes("--tui");
1051
1129
 
1052
1130
  if (tuiMode) {
1053
- const newArgs = args.filter(a => a !== "--tui" && a !== "ui");
1131
+ const newArgs = args.filter(a => a !== "--tui" && a !== "tui");
1054
1132
  process.argv = [process.argv[0], process.argv[1], ...newArgs];
1055
1133
  const tuiScript = path.join(__dirname, "..", "lib", "wispy-tui.mjs");
1056
1134
  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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wispy-cli",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "description": "🌿 Wispy — AI workspace assistant with trustworthy execution (harness, receipts, approvals, diffs)",
5
5
  "license": "MIT",
6
6
  "author": "Minseo & Poropo",