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.
@@ -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();
@@ -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) return cmdTrustShow();
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]);
@@ -412,7 +412,87 @@ export async function handleWsCommand(args) {
412
412
  const sub = args[1];
413
413
 
414
414
  if (!sub) {
415
- return cmdWsList();
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.6.3",
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
  },