wispy-cli 2.6.3 → 2.7.1
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 +40 -2209
- package/lib/commands/skills-cmd.mjs +117 -0
- package/lib/commands/trust.mjs +79 -1
- package/lib/commands/ws.mjs +81 -1
- package/lib/wispy-tui-clean.mjs +82 -0
- package/package.json +2 -1
|
@@ -204,6 +204,123 @@ export async function cmdImproveSkill(name, feedback) {
|
|
|
204
204
|
export async function handleSkillCommand(args) {
|
|
205
205
|
const sub = args[1];
|
|
206
206
|
|
|
207
|
+
if (!sub) {
|
|
208
|
+
// Interactive menu
|
|
209
|
+
try {
|
|
210
|
+
const { select, Separator, input } = await import("@inquirer/prompts");
|
|
211
|
+
const skills = await listSkills();
|
|
212
|
+
|
|
213
|
+
const choices = [];
|
|
214
|
+
if (skills.length > 0) {
|
|
215
|
+
for (const s of skills) {
|
|
216
|
+
const used = s.timesUsed > 0 ? ` — used ${s.timesUsed} times` : "";
|
|
217
|
+
choices.push({
|
|
218
|
+
name: `${s.name}${used}`,
|
|
219
|
+
value: { type: "skill", name: s.name },
|
|
220
|
+
short: s.name,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
choices.push(new Separator("──────────"));
|
|
225
|
+
choices.push({ name: "Create skill from last conversation", value: { type: "create" } });
|
|
226
|
+
choices.push({ name: "Import skill", value: { type: "import" } });
|
|
227
|
+
|
|
228
|
+
let answer;
|
|
229
|
+
try {
|
|
230
|
+
answer = await select({ message: "Skills:", choices });
|
|
231
|
+
} catch (e) {
|
|
232
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
233
|
+
throw e;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (answer.type === "create") {
|
|
237
|
+
let name;
|
|
238
|
+
try {
|
|
239
|
+
name = await input({ message: "Skill name:" });
|
|
240
|
+
} catch (e) {
|
|
241
|
+
if (e.name === "ExitPromptError") return;
|
|
242
|
+
throw e;
|
|
243
|
+
}
|
|
244
|
+
if (name && name.trim()) await cmdTeach(name.trim());
|
|
245
|
+
} else if (answer.type === "import") {
|
|
246
|
+
console.log(dim("Import via: wispy skill import <path> (coming soon)"));
|
|
247
|
+
} else if (answer.type === "skill") {
|
|
248
|
+
// Sub-menu for selected skill
|
|
249
|
+
let skillAction;
|
|
250
|
+
try {
|
|
251
|
+
skillAction = await select({
|
|
252
|
+
message: `${answer.name}:`,
|
|
253
|
+
choices: [
|
|
254
|
+
{ name: "Run now", value: "run" },
|
|
255
|
+
{ name: "View details", value: "view" },
|
|
256
|
+
{ name: "Improve", value: "improve" },
|
|
257
|
+
{ name: "Delete", value: "delete" },
|
|
258
|
+
],
|
|
259
|
+
});
|
|
260
|
+
} catch (e) {
|
|
261
|
+
if (e.name === "ExitPromptError") return;
|
|
262
|
+
throw e;
|
|
263
|
+
}
|
|
264
|
+
if (skillAction === "run") {
|
|
265
|
+
await cmdSkillRun(answer.name);
|
|
266
|
+
} else if (skillAction === "view") {
|
|
267
|
+
const skill = await (async () => {
|
|
268
|
+
const { readFile } = await import("node:fs/promises");
|
|
269
|
+
const { join } = await import("node:path");
|
|
270
|
+
const { homedir } = await import("node:os");
|
|
271
|
+
const skillsDir = join(homedir(), ".wispy", "skills");
|
|
272
|
+
try {
|
|
273
|
+
return JSON.parse(await readFile(join(skillsDir, `${answer.name}.json`), "utf8"));
|
|
274
|
+
} catch { return null; }
|
|
275
|
+
})();
|
|
276
|
+
if (skill) {
|
|
277
|
+
console.log(`\n${bold(skill.name)} v${skill.version ?? 1}`);
|
|
278
|
+
console.log(dim(` ${skill.description ?? ""}`));
|
|
279
|
+
console.log(dim(` Prompt: ${skill.prompt?.slice(0, 120) ?? ""}`));
|
|
280
|
+
console.log(dim(` Used: ${skill.timesUsed ?? 0}x`));
|
|
281
|
+
if (skill.tags?.length > 0) console.log(dim(` Tags: ${skill.tags.join(", ")}`));
|
|
282
|
+
console.log("");
|
|
283
|
+
}
|
|
284
|
+
} else if (skillAction === "improve") {
|
|
285
|
+
let feedback;
|
|
286
|
+
try {
|
|
287
|
+
const { input: inp } = await import("@inquirer/prompts");
|
|
288
|
+
feedback = await inp({ message: "Improvement notes:" });
|
|
289
|
+
} catch (e) {
|
|
290
|
+
if (e.name === "ExitPromptError") return;
|
|
291
|
+
throw e;
|
|
292
|
+
}
|
|
293
|
+
if (feedback && feedback.trim()) await cmdImproveSkill(answer.name, feedback.trim());
|
|
294
|
+
} else if (skillAction === "delete") {
|
|
295
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
296
|
+
let ok;
|
|
297
|
+
try {
|
|
298
|
+
ok = await confirm({ message: `Delete skill '${answer.name}'?`, default: false });
|
|
299
|
+
} catch (e) {
|
|
300
|
+
if (e.name === "ExitPromptError") return;
|
|
301
|
+
throw e;
|
|
302
|
+
}
|
|
303
|
+
if (ok) {
|
|
304
|
+
const { unlink } = await import("node:fs/promises");
|
|
305
|
+
const { join } = await import("node:path");
|
|
306
|
+
const { homedir } = await import("node:os");
|
|
307
|
+
const skillsDir = join(homedir(), ".wispy", "skills");
|
|
308
|
+
try {
|
|
309
|
+
await unlink(join(skillsDir, `${answer.name}.json`));
|
|
310
|
+
console.log(green(`✅ Deleted skill: ${answer.name}`));
|
|
311
|
+
} catch (e) {
|
|
312
|
+
console.log(red(`Failed to delete: ${e.message}`));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
} catch (e) {
|
|
318
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
319
|
+
await cmdSkillList();
|
|
320
|
+
}
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
207
324
|
if (!sub) return cmdSkillList();
|
|
208
325
|
if (sub === "run") return cmdSkillRun(args[2]);
|
|
209
326
|
if (sub === "list") return cmdSkillList();
|
package/lib/commands/trust.mjs
CHANGED
|
@@ -322,7 +322,85 @@ export async function cmdTrustReceipt(id) {
|
|
|
322
322
|
export async function handleTrustCommand(args) {
|
|
323
323
|
const sub = args[1];
|
|
324
324
|
|
|
325
|
-
if (!sub)
|
|
325
|
+
if (!sub) {
|
|
326
|
+
// Interactive menu
|
|
327
|
+
try {
|
|
328
|
+
const { select } = await import("@inquirer/prompts");
|
|
329
|
+
const level = await getCurrentSecurityLevel();
|
|
330
|
+
const preset = SECURITY_PRESETS[level] ?? SECURITY_PRESETS.balanced;
|
|
331
|
+
|
|
332
|
+
console.log(`\nCurrent level: ${preset.color(bold(preset.label))}`);
|
|
333
|
+
console.log(dim(` ${preset.description}\n`));
|
|
334
|
+
|
|
335
|
+
let action;
|
|
336
|
+
try {
|
|
337
|
+
action = await select({
|
|
338
|
+
message: "Change trust settings:",
|
|
339
|
+
choices: [
|
|
340
|
+
{ name: "Set level (careful / balanced / yolo)", value: "set-level" },
|
|
341
|
+
{ name: "View audit log", value: "log" },
|
|
342
|
+
{ name: "View recent approvals", value: "approvals" },
|
|
343
|
+
{ name: "Replay a session", value: "replay" },
|
|
344
|
+
{ name: "Show permissions by tool", value: "permissions" },
|
|
345
|
+
],
|
|
346
|
+
});
|
|
347
|
+
} catch (e) {
|
|
348
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
349
|
+
throw e;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (action === "set-level") {
|
|
353
|
+
let levelChoice;
|
|
354
|
+
try {
|
|
355
|
+
levelChoice = await select({
|
|
356
|
+
message: "Choose trust level:",
|
|
357
|
+
choices: [
|
|
358
|
+
{ name: `${green("careful")} 🔒 — require approval for most operations`, value: "careful" },
|
|
359
|
+
{ name: `${yellow("balanced")} ⚖️ — approve dangerous ops, notify writes`, value: "balanced" },
|
|
360
|
+
{ name: `${red("yolo")} 🚀 — auto-approve everything (use with care!)`, value: "yolo" },
|
|
361
|
+
],
|
|
362
|
+
});
|
|
363
|
+
} catch (e) {
|
|
364
|
+
if (e.name === "ExitPromptError") return;
|
|
365
|
+
throw e;
|
|
366
|
+
}
|
|
367
|
+
await cmdTrustLevel(levelChoice);
|
|
368
|
+
} else if (action === "log") {
|
|
369
|
+
await cmdTrustLog([]);
|
|
370
|
+
} else if (action === "approvals") {
|
|
371
|
+
await cmdTrustShow();
|
|
372
|
+
} else if (action === "replay") {
|
|
373
|
+
const { input } = await import("@inquirer/prompts");
|
|
374
|
+
let sid;
|
|
375
|
+
try {
|
|
376
|
+
sid = await input({ message: "Session ID to replay:" });
|
|
377
|
+
} catch (e) {
|
|
378
|
+
if (e.name === "ExitPromptError") return;
|
|
379
|
+
throw e;
|
|
380
|
+
}
|
|
381
|
+
if (sid && sid.trim()) await cmdTrustReplay(sid.trim());
|
|
382
|
+
} else if (action === "permissions") {
|
|
383
|
+
const { readJsonOr: rjo } = { readJsonOr: async (p, f) => { try { return JSON.parse(await (await import("node:fs/promises")).readFile(p, "utf8")); } catch { return f; } } };
|
|
384
|
+
const permsData = await rjo(PERMISSIONS_FILE, { policies: {} });
|
|
385
|
+
const policies = permsData.policies ?? {};
|
|
386
|
+
console.log(`\n${bold("🔐 Permissions by tool:")}\n`);
|
|
387
|
+
if (Object.keys(policies).length === 0) {
|
|
388
|
+
console.log(dim(" No custom policies. Using level defaults."));
|
|
389
|
+
} else {
|
|
390
|
+
for (const [tool, pol] of Object.entries(policies)) {
|
|
391
|
+
const icon = pol === "approve" ? "🔐" : pol === "notify" ? "📋" : "✅";
|
|
392
|
+
console.log(` ${icon} ${cyan(tool.padEnd(20))} ${pol}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
console.log("");
|
|
396
|
+
}
|
|
397
|
+
} catch (e) {
|
|
398
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
399
|
+
// Fallback to regular show
|
|
400
|
+
await cmdTrustShow();
|
|
401
|
+
}
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
326
404
|
if (sub === "level") return cmdTrustLevel(args[2]);
|
|
327
405
|
if (sub === "log") return cmdTrustLog(args.slice(2));
|
|
328
406
|
if (sub === "replay") return cmdTrustReplay(args[2]);
|
package/lib/commands/ws.mjs
CHANGED
|
@@ -412,7 +412,87 @@ export async function handleWsCommand(args) {
|
|
|
412
412
|
const sub = args[1];
|
|
413
413
|
|
|
414
414
|
if (!sub) {
|
|
415
|
-
|
|
415
|
+
// Interactive menu
|
|
416
|
+
try {
|
|
417
|
+
const { select, input, Separator } = await import("@inquirer/prompts");
|
|
418
|
+
const workstreams = await listAllWorkstreams();
|
|
419
|
+
|
|
420
|
+
const choices = [];
|
|
421
|
+
if (workstreams.length > 0) {
|
|
422
|
+
for (const ws of workstreams) {
|
|
423
|
+
const marker = ws.isActive ? "● " : " ";
|
|
424
|
+
const last = formatRelative(ws.lastActive);
|
|
425
|
+
const msgs = ws.sessionCount > 0 ? ` · ${ws.sessionCount} msgs` : "";
|
|
426
|
+
choices.push({
|
|
427
|
+
name: `${marker}${ws.name}${ws.isActive ? " (active)" : ""} — ${last}${msgs}`,
|
|
428
|
+
value: { type: "switch", name: ws.name },
|
|
429
|
+
short: ws.name,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
choices.push(new Separator(dim("No workstreams yet")));
|
|
434
|
+
}
|
|
435
|
+
choices.push(new Separator("──────────"));
|
|
436
|
+
choices.push({ name: "Create new workstream", value: { type: "new" }, short: "Create new" });
|
|
437
|
+
choices.push({ name: "Archive a workstream", value: { type: "archive" }, short: "Archive" });
|
|
438
|
+
choices.push({ name: "Delete a workstream", value: { type: "delete" }, short: "Delete" });
|
|
439
|
+
|
|
440
|
+
let answer;
|
|
441
|
+
try {
|
|
442
|
+
answer = await select({ message: "Workstreams:", choices });
|
|
443
|
+
} catch (e) {
|
|
444
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
445
|
+
throw e;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (answer.type === "switch") {
|
|
449
|
+
await cmdWsSwitch(answer.name);
|
|
450
|
+
} else if (answer.type === "new") {
|
|
451
|
+
let name;
|
|
452
|
+
try {
|
|
453
|
+
name = await input({ message: "New workstream name:" });
|
|
454
|
+
} catch (e) {
|
|
455
|
+
if (e.name === "ExitPromptError") return;
|
|
456
|
+
throw e;
|
|
457
|
+
}
|
|
458
|
+
if (name && name.trim()) await cmdWsNew(name.trim());
|
|
459
|
+
} else if (answer.type === "archive") {
|
|
460
|
+
const nonActive = workstreams.filter(w => !w.isActive);
|
|
461
|
+
if (nonActive.length === 0) {
|
|
462
|
+
console.log(dim("No other workstreams to archive."));
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const archiveChoices = nonActive.map(w => ({ name: w.name, value: w.name }));
|
|
466
|
+
let toArchive;
|
|
467
|
+
try {
|
|
468
|
+
toArchive = await select({ message: "Archive which workstream?", choices: archiveChoices });
|
|
469
|
+
} catch (e) {
|
|
470
|
+
if (e.name === "ExitPromptError") return;
|
|
471
|
+
throw e;
|
|
472
|
+
}
|
|
473
|
+
await cmdWsArchive(toArchive);
|
|
474
|
+
} else if (answer.type === "delete") {
|
|
475
|
+
const deletable = workstreams.filter(w => w.name !== "default");
|
|
476
|
+
if (deletable.length === 0) {
|
|
477
|
+
console.log(dim("No workstreams to delete (cannot delete 'default')."));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const deleteChoices = deletable.map(w => ({ name: w.name, value: w.name }));
|
|
481
|
+
let toDelete;
|
|
482
|
+
try {
|
|
483
|
+
toDelete = await select({ message: "Delete which workstream?", choices: deleteChoices });
|
|
484
|
+
} catch (e) {
|
|
485
|
+
if (e.name === "ExitPromptError") return;
|
|
486
|
+
throw e;
|
|
487
|
+
}
|
|
488
|
+
await cmdWsDelete(toDelete);
|
|
489
|
+
}
|
|
490
|
+
} catch (e) {
|
|
491
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
492
|
+
// Fallback to plain list if inquirer unavailable
|
|
493
|
+
await cmdWsList();
|
|
494
|
+
}
|
|
495
|
+
return;
|
|
416
496
|
}
|
|
417
497
|
|
|
418
498
|
if (sub === "new") return cmdWsNew(args[2]);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* wispy-tui.mjs — Workspace OS TUI for Wispy v2.0
|
|
4
|
+
*
|
|
5
|
+
* Multi-panel workspace interface:
|
|
6
|
+
* - Left sidebar: Workstreams, Agents, Memory, Cron, Sync
|
|
7
|
+
* - Main area: Chat / Overview / Agents / Memory / Audit / Settings
|
|
8
|
+
* - Bottom: Action Timeline bar + Input
|
|
9
|
+
* - Overlays: Approval dialogs, Diff views
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import React, { useState, useEffect, useRef, useCallback, useReducer } from "react";
|
|
13
|
+
import { render, Box, Text, useApp, Newline, useInput, useStdout } from "ink";
|
|
14
|
+
import Spinner from "ink-spinner";
|
|
15
|
+
import TextInput from "ink-text-input";
|
|
16
|
+
|
|
17
|
+
import { COMMANDS, filterCommands } from "./command-registry.mjs";
|
|
18
|
+
|
|
19
|
+
import os from "node:os";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { readFile, writeFile, readdir, stat, mkdir } from "node:fs/promises";
|
|
22
|
+
|
|
23
|
+
import { WispyEngine, CONVERSATIONS_DIR, PROVIDERS, WISPY_DIR, MEMORY_DIR } from "../core/index.mjs";
|
|
24
|
+
|
|
25
|
+
// ─── Parse CLI args ──────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const rawArgs = process.argv.slice(2);
|
|
28
|
+
const wsIdx = rawArgs.findIndex((a) => a === "-w" || a === "--workstream");
|
|
29
|
+
const INITIAL_WORKSTREAM =
|
|
30
|
+
process.env.WISPY_WORKSTREAM ??
|
|
31
|
+
(wsIdx !== -1 ? rawArgs[wsIdx + 1] : null) ??
|
|
32
|
+
"default";
|
|
33
|
+
|
|
34
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
const VIEWS = ["chat", "overview", "agents", "memory", "audit", "settings"];
|
|
37
|
+
const SIDEBAR_WIDTH = 16;
|
|
38
|
+
const TIMELINE_LINES = 3;
|
|
39
|
+
|
|
40
|
+
const TOOL_ICONS = {
|
|
41
|
+
read_file: "⎘", write_file: "✍", file_edit: "✎", run_command: "⚙",
|
|
42
|
+
git: "⧉", web_search: "🔍", web_fetch: "⤓", list_directory: "☰",
|
|
43
|
+
spawn_subagent: "⇝", spawn_agent: "↠", memory_save: "💾",
|
|
44
|
+
memory_search: "🔎", memory_list: "🗂", delete_file: "🗑",
|
|
45
|
+
node_execute: "⨀", update_work_context: "⟳",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ─── Utilities ───────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
function fmtTime(iso) {
|
|
51
|
+
if (!iso) return "";
|
|
52
|
+
try { return new Date(iso).toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" }); }
|
|
53
|
+
catch { return ""; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function fmtRelTime(iso) {
|
|
57
|
+
if (!iso) return "";
|
|
58
|
+
try {
|
|
59
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
60
|
+
if (diff < 60_000) return "just now";
|
|
61
|
+
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}min ago`;
|
|
62
|
+
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}hr ago`;
|
|
63
|
+
return "yesterday";
|
|
64
|
+
} catch { return ""; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function truncate(str, n) {
|
|
68
|
+
if (!str) return "";
|
|
69
|
+
return str.length > n ? str.slice(0, n - 1) + "…" : str;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── Markdown renderer ───────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
function renderMarkdown(text, maxWidth = 60) {
|
|
75
|
+
// Updated renderer function
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Updates incorporate "minimalistic" visuals for agent functionality and correct UX/lint fixes ───
|
|
79
|
+
const agentIcon = (status) => {
|
|
80
|
+
if (status === "running") return "●";
|
|
81
|
+
if (status === "pending") return "○";
|
|
82
|
+
if (status === "" };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wispy-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.1",
|
|
4
4
|
"description": "🌿 Wispy — AI workspace assistant with trustworthy execution (harness, receipts, approvals, diffs)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Minseo & Poropo",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"ink": "^5.2.1",
|
|
68
68
|
"ink-spinner": "^5.0.0",
|
|
69
69
|
"ink-text-input": "^6.0.0",
|
|
70
|
+
"inquirer": "^13.3.2",
|
|
70
71
|
"react": "^18.3.1",
|
|
71
72
|
"uuid": "^13.0.0"
|
|
72
73
|
},
|