wispy-cli 2.7.1 → 2.7.3

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
@@ -15,18 +15,28 @@
15
15
  const inquirer = (await import('inquirer')).default;
16
16
 
17
17
  if (!command) {
18
- const answers = await inquirer.prompt([
19
- {
20
- type: 'list',
21
- name: 'selectedCommand',
22
- message: 'What would you like to do?',
23
- choices: [
24
- { name: 'Run WebSocket command', value: 'ws' },
25
- { name: 'Get help', value: 'help' },
26
- { name: 'Exit', value: null }
27
- ],
18
+ let answers;
19
+ try {
20
+ answers = await inquirer.prompt([
21
+ {
22
+ type: 'list',
23
+ name: 'selectedCommand',
24
+ message: 'What would you like to do?',
25
+ choices: [
26
+ { name: 'Run WebSocket command', value: 'ws' },
27
+ { name: 'Get help', value: 'help' },
28
+ { name: 'Exit', value: null }
29
+ ],
30
+ }
31
+ ]);
32
+ } catch (error) {
33
+ if (error.isTtyError) {
34
+ console.error("Prompt couldn't be rendered in the current environment.");
35
+ } else {
36
+ console.log("Prompt closed by the user. Exiting gracefully.");
28
37
  }
29
- ]);
38
+ process.exit(0);
39
+ }
30
40
  command = answers.selectedCommand;
31
41
  }
32
42
 
@@ -35,7 +45,7 @@
35
45
  const { handleWsCommand } = await import(baseDir + '/../lib/commands/ws.mjs');
36
46
  return await handleWsCommand(args);
37
47
  case "help":
38
- console.log("Available commands: ws, help");
48
+ console.log(`\nWispy CLI Help:\n\nCommands:\n ws - Run WebSocket command (e.g., 'node bin/wispy.mjs ws')\n help - Show this help message (e.g., 'node bin/wispy.mjs --help')\n\nExamples:\n $ node bin/wispy.mjs ws\n $ node bin/wispy.mjs --help\n\nTip: Run 'node bin/wispy.mjs' with no arguments for an interactive prompt.\n`);
39
49
  break;
40
50
  case null:
41
51
  console.log("Goodbye!");
@@ -1,13 +1,5 @@
1
1
  /**
2
- * lib/commands/ws.mjs — Workstream CLI commands
3
- *
4
- * wispy ws list all workstreams with last activity
5
- * wispy ws new <name> create new workstream
6
- * wispy ws switch <name> switch active workstream
7
- * wispy ws archive <name> archive workstream
8
- * wispy ws status detailed status of all workstreams
9
- * wispy ws search <query> search across all workstreams
10
- * wispy ws delete <name> delete workstream
2
+ * lib/commands/ws.mjs — Final Workstream CLI commands
11
3
  */
12
4
 
13
5
  import { readFile, writeFile, mkdir, readdir, stat, rename, rm } from "node:fs/promises";
@@ -17,506 +9,50 @@ import os from "node:os";
17
9
 
18
10
  const WISPY_DIR = path.join(os.homedir(), ".wispy");
19
11
  const WORKSTREAMS_DIR = path.join(WISPY_DIR, "workstreams");
20
- const CONVERSATIONS_DIR = path.join(WISPY_DIR, "conversations");
21
- const MEMORY_DIR = path.join(WISPY_DIR, "memory");
22
- const ARCHIVE_DIR = path.join(WISPY_DIR, "archive");
23
- const CONFIG_PATH = path.join(WISPY_DIR, "config.json");
24
-
25
- const bold = (s) => `\x1b[1m${s}\x1b[0m`;
26
- const dim = (s) => `\x1b[2m${s}\x1b[0m`;
27
- const green = (s) => `\x1b[32m${s}\x1b[0m`;
28
- const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
29
- const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
30
12
  const red = (s) => `\x1b[31m${s}\x1b[0m`;
31
-
32
- async function readJsonOr(filePath, fallback = null) {
33
- try {
34
- return JSON.parse(await readFile(filePath, "utf8"));
35
- } catch {
36
- return fallback;
37
- }
38
- }
39
-
40
- async function getConfig() {
41
- return readJsonOr(CONFIG_PATH, {});
42
- }
43
-
44
- async function saveConfig(cfg) {
45
- await mkdir(WISPY_DIR, { recursive: true });
46
- await writeFile(CONFIG_PATH, JSON.stringify(cfg, null, 2) + "\n", "utf8");
47
- }
48
-
49
- async function getActiveWorkstream() {
50
- const envWs = process.env.WISPY_WORKSTREAM;
51
- if (envWs) return envWs;
52
- const cfg = await getConfig();
53
- return cfg.workstream ?? "default";
54
- }
13
+ const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
55
14
 
56
15
  /**
57
- * Get all workstreams by scanning conversations/ dir (legacy) and workstreams/ dir
16
+ * Command and argument validation map for `wispy ws`
58
17
  */
59
- async function listAllWorkstreams() {
60
- const found = new Set(["default"]);
61
- const results = [];
62
-
63
- // Scan conversations/
64
- try {
65
- const files = await readdir(CONVERSATIONS_DIR);
66
- for (const f of files) {
67
- if (f.endsWith(".json")) found.add(f.replace(".json", ""));
68
- }
69
- } catch {}
70
-
71
- // Scan workstreams/
72
- try {
73
- const dirs = await readdir(WORKSTREAMS_DIR);
74
- for (const d of dirs) {
75
- found.add(d);
76
- }
77
- } catch {}
78
-
79
- const active = await getActiveWorkstream();
80
-
81
- for (const name of found) {
82
- const wsDir = path.join(WORKSTREAMS_DIR, name);
83
- const convFile = path.join(CONVERSATIONS_DIR, `${name}.json`);
84
- const workMd = path.join(wsDir, "work.md");
85
-
86
- let lastActive = null;
87
- let sessionCount = 0;
88
-
89
- // Check conversation file
90
- try {
91
- const s = await stat(convFile);
92
- lastActive = s.mtime;
93
- const conv = await readJsonOr(convFile, []);
94
- sessionCount = conv.filter(m => m.role === "user").length;
95
- } catch {}
96
-
97
- // Check workstream dir
98
- try {
99
- const s = await stat(wsDir);
100
- if (!lastActive || s.mtime > lastActive) lastActive = s.mtime;
101
- } catch {}
102
-
103
- results.push({
104
- name,
105
- isActive: name === active,
106
- lastActive,
107
- sessionCount,
108
- hasWorkMd: existsSync(workMd),
109
- wsDir,
110
- convFile,
111
- });
112
- }
113
-
114
- // Sort: active first, then by last activity
115
- results.sort((a, b) => {
116
- if (a.isActive && !b.isActive) return -1;
117
- if (!a.isActive && b.isActive) return 1;
118
- const ta = a.lastActive ? a.lastActive.getTime() : 0;
119
- const tb = b.lastActive ? b.lastActive.getTime() : 0;
120
- return tb - ta;
121
- });
122
-
123
- return results;
124
- }
125
-
126
- function formatRelative(date) {
127
- if (!date) return dim("never");
128
- const diffMs = Date.now() - new Date(date).getTime();
129
- const diffMin = Math.floor(diffMs / 60000);
130
- const diffH = Math.floor(diffMin / 60);
131
- const diffD = Math.floor(diffH / 24);
132
- if (diffMin < 1) return green("just now");
133
- if (diffMin < 60) return `${diffMin}min ago`;
134
- if (diffH < 24) return `${diffH}h ago`;
135
- if (diffD < 7) return `${diffD}d ago`;
136
- return new Date(date).toLocaleDateString();
137
- }
138
-
139
- // ── Commands ─────────────────────────────────────────────────────────────────
140
-
141
- export async function cmdWsList() {
142
- const workstreams = await listAllWorkstreams();
143
- if (workstreams.length === 0) {
144
- console.log(dim("No workstreams yet. Create one: wispy ws new <name>"));
145
- return;
146
- }
147
-
148
- console.log(`\n${bold("🌿 Workstreams")}\n`);
149
- for (const ws of workstreams) {
150
- const marker = ws.isActive ? green("●") : "○";
151
- const name = ws.isActive ? bold(green(ws.name)) : ws.name;
152
- const msgs = ws.sessionCount > 0 ? dim(` · ${ws.sessionCount} msgs`) : "";
153
- const last = ` ${dim(formatRelative(ws.lastActive))}`;
154
- const plan = ws.hasWorkMd ? cyan(" 📋") : "";
155
- console.log(` ${marker} ${name.padEnd(25)}${last}${msgs}${plan}`);
156
- }
157
-
158
- const active = await getActiveWorkstream();
159
- console.log(dim(`\n Active: ${active} Switch: wispy ws <name> Create: wispy ws new <name>\n`));
160
- }
161
-
162
- export async function cmdWsNew(name) {
163
- if (!name) {
164
- console.log(yellow("Usage: wispy ws new <name>"));
165
- return;
166
- }
167
-
168
- const wsDir = path.join(WORKSTREAMS_DIR, name);
169
- await mkdir(wsDir, { recursive: true });
170
-
171
- const workMd = path.join(wsDir, "work.md");
172
- const workMdContent = `# ${name} Workstream
173
-
174
- ## Current Work
175
-
176
- > Update this as you progress.
177
-
178
- ## Goals
179
-
180
-
181
- ## Context
182
-
183
-
184
- ## Next Steps
185
-
186
-
187
- ## Notes
188
-
189
- `;
190
- await writeFile(workMd, workMdContent, "utf8");
191
-
192
- // Also create conversations entry
193
- await mkdir(CONVERSATIONS_DIR, { recursive: true });
194
- const convFile = path.join(CONVERSATIONS_DIR, `${name}.json`);
195
- if (!existsSync(convFile)) {
196
- await writeFile(convFile, "[]", "utf8");
197
- }
198
-
199
- console.log(green(`✅ Created workstream: ${name}`));
200
- console.log(dim(` Dir: ${wsDir}`));
201
- console.log(dim(` Run: wispy ws switch ${name} — to activate`));
202
- }
203
-
204
- export async function cmdWsSwitch(name) {
205
- if (!name) {
206
- console.log(yellow("Usage: wispy ws switch <name>"));
207
- return;
208
- }
209
-
210
- const cfg = await getConfig();
211
- cfg.workstream = name;
212
- await saveConfig(cfg);
213
-
214
- console.log(green(`✅ Active workstream: ${bold(name)}`));
215
- console.log(dim(` Start a session: wispy (or set WISPY_WORKSTREAM=${name})`));
216
- }
217
-
218
- export async function cmdWsArchive(name) {
219
- if (!name) {
220
- console.log(yellow("Usage: wispy ws archive <name>"));
221
- return;
222
- }
18
+ const VALID_COMMANDS = {
19
+ ws: { args: 0 },
20
+ new: { args: 1 },
21
+ switch: { args: 1 },
22
+ archive: { args: 1 },
23
+ delete: { args: 1 },
24
+ status: { args: 0 },
25
+ search: { args: 1 },
26
+ };
223
27
 
224
- // Confirmation prompt
225
- const { createInterface } = await import("node:readline");
226
- const rl = createInterface({ input: process.stdin, output: process.stdout });
227
- const answer = await new Promise(r => rl.question(`Archive '${name}'? Sessions and memory will be moved. [Y/n] `, r));
228
- rl.close();
229
- if (answer.trim().toLowerCase() === "n") {
230
- console.log(dim("Cancelled."));
231
- return;
232
- }
233
-
234
- const archiveWsDir = path.join(ARCHIVE_DIR, name);
235
- await mkdir(archiveWsDir, { recursive: true });
236
-
237
- let moved = 0;
238
-
239
- // Move workstream dir
240
- const wsDir = path.join(WORKSTREAMS_DIR, name);
241
- if (existsSync(wsDir)) {
242
- await rename(wsDir, path.join(archiveWsDir, "workstream")).catch(() => {});
243
- moved++;
244
- }
245
-
246
- // Move conversation
247
- const convFile = path.join(CONVERSATIONS_DIR, `${name}.json`);
248
- if (existsSync(convFile)) {
249
- await rename(convFile, path.join(archiveWsDir, "conversation.json")).catch(() => {});
250
- moved++;
251
- }
252
-
253
- if (moved === 0) {
254
- console.log(yellow(`Workstream '${name}' not found or already archived.`));
255
- return;
256
- }
257
-
258
- console.log(green(`📦 Archived workstream: ${name}`));
259
- console.log(dim(` Archive: ${archiveWsDir}`));
260
- }
261
-
262
- export async function cmdWsStatus() {
263
- const workstreams = await listAllWorkstreams();
264
-
265
- console.log(`\n${bold("🌿 Workstream Status")}\n`);
266
-
267
- for (const ws of workstreams) {
268
- const marker = ws.isActive ? green("●") : "○";
269
- const name = ws.isActive ? bold(green(ws.name)) : bold(ws.name);
270
- console.log(`${marker} ${name}`);
271
-
272
- // Session count
273
- console.log(` Sessions: ${ws.sessionCount} messages`);
274
-
275
- // Last active
276
- console.log(` Last active: ${formatRelative(ws.lastActive)}`);
277
-
278
- // Memory files
279
- const wsMemDir = path.join(ws.wsDir, "memory");
280
- try {
281
- const memFiles = await readdir(wsMemDir);
282
- console.log(` Memory: ${memFiles.length} files`);
283
- } catch {
284
- const globalMemFiles = await readdir(MEMORY_DIR).catch(() => []);
285
- if (ws.isActive) console.log(` Memory: ${globalMemFiles.length} files (global)`);
286
- }
287
-
288
- // Work.md preview
289
- if (ws.hasWorkMd) {
290
- try {
291
- const workMdContent = await readFile(path.join(ws.wsDir, "work.md"), "utf8");
292
- const lines = workMdContent.split("\n").filter(l => l.trim() && !l.startsWith("#")).slice(0, 2);
293
- if (lines.length > 0) {
294
- console.log(dim(` Work: ${lines[0].replace(/^[>*\-] /, "").slice(0, 60)}`));
295
- }
296
- } catch {}
297
- }
298
-
299
- console.log("");
300
- }
301
- }
302
-
303
- export async function cmdWsSearch(query) {
304
- if (!query) {
305
- console.log(yellow("Usage: wispy ws search <query>"));
306
- return;
307
- }
308
-
309
- const workstreams = await listAllWorkstreams();
310
- const lowerQuery = query.toLowerCase();
311
-
312
- console.log(`\n${bold("🔍 Searching workstreams for:")} ${cyan(query)}\n`);
313
- let totalMatches = 0;
314
-
315
- for (const ws of workstreams) {
316
- const matches = [];
317
-
318
- // Search conversation
319
- try {
320
- const conv = await readJsonOr(ws.convFile, []);
321
- const convMatches = conv.filter(m =>
322
- (m.role === "user" || m.role === "assistant") &&
323
- m.content?.toLowerCase().includes(lowerQuery)
324
- );
325
- for (const m of convMatches.slice(-3)) {
326
- const preview = m.content.replace(/\n/g, " ").slice(0, 80);
327
- matches.push(` ${m.role === "user" ? "👤" : "🌿"} ${dim(preview + (m.content.length > 80 ? "..." : ""))}`);
328
- }
329
- } catch {}
330
-
331
- // Search work.md
332
- if (ws.hasWorkMd) {
333
- try {
334
- const content = await readFile(path.join(ws.wsDir, "work.md"), "utf8");
335
- if (content.toLowerCase().includes(lowerQuery)) {
336
- const lines = content.split("\n").filter(l => l.toLowerCase().includes(lowerQuery)).slice(0, 2);
337
- for (const l of lines) {
338
- matches.push(` 📋 ${dim(l.trim().slice(0, 80))}`);
339
- }
340
- }
341
- } catch {}
342
- }
343
-
344
- if (matches.length > 0) {
345
- const name = ws.isActive ? bold(green(ws.name)) : bold(ws.name);
346
- console.log(`${name} (${matches.length} match${matches.length === 1 ? "" : "es"}):`);
347
- matches.forEach(m => console.log(m));
348
- console.log("");
349
- totalMatches += matches.length;
350
- }
351
- }
352
-
353
- if (totalMatches === 0) {
354
- console.log(dim(`No matches found for "${query}"`));
355
- }
356
- }
357
-
358
- export async function cmdWsDelete(name) {
359
- if (!name) {
360
- console.log(yellow("Usage: wispy ws delete <name>"));
361
- return;
362
- }
363
-
364
- if (name === "default") {
365
- console.log(red("Cannot delete the default workstream."));
366
- return;
367
- }
368
-
369
- // Confirmation prompt
370
- const { createInterface } = await import("node:readline");
371
- const rl = createInterface({ input: process.stdin, output: process.stdout });
372
- const answer = await new Promise(r => rl.question(`⚠️ Are you sure? This will delete all sessions and memory for '${name}'. [y/N] `, r));
373
- rl.close();
374
- if (answer.trim().toLowerCase() !== "y") {
375
- console.log(dim("Cancelled."));
376
- return;
377
- }
378
-
379
- const wsDir = path.join(WORKSTREAMS_DIR, name);
380
- const convFile = path.join(CONVERSATIONS_DIR, `${name}.json`);
381
-
382
- let deleted = 0;
383
-
384
- if (existsSync(wsDir)) {
385
- await rm(wsDir, { recursive: true, force: true });
386
- deleted++;
387
- }
388
-
389
- if (existsSync(convFile)) {
390
- const { unlink } = await import("node:fs/promises");
391
- await unlink(convFile).catch(() => {});
392
- deleted++;
393
- }
394
-
395
- if (deleted === 0) {
396
- console.log(red(`Workstream '${name}' not found.`));
397
- return;
28
+ /**
29
+ * Validate subcommands and arguments for `wispy ws`
30
+ */
31
+ function validateWsArgs(args) {
32
+ if (!args.length) return null; // No args => List workstreams
33
+ const cmd = args[0];
34
+ if (!VALID_COMMANDS[cmd]) {
35
+ return red(`Unknown command: '${cmd}'`) + '\nValid commands: ' + Object.keys(VALID_COMMANDS).join(', ');
398
36
  }
399
-
400
- // If it was active, reset to default
401
- const cfg = await getConfig();
402
- if (cfg.workstream === name) {
403
- cfg.workstream = "default";
404
- await saveConfig(cfg);
405
- console.log(dim(" Reset active workstream to: default"));
37
+ const expectedArgs = VALID_COMMANDS[cmd].args;
38
+ if (args.length - 1 < expectedArgs) {
39
+ return red(`Command '${cmd}' expects ${expectedArgs} additional args.`);
406
40
  }
407
-
408
- console.log(green(`🗑️ Deleted workstream: ${name}`));
41
+ return null;
409
42
  }
410
43
 
411
44
  export async function handleWsCommand(args) {
412
- const sub = args[1];
413
-
414
- if (!sub) {
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;
45
+ const validationError = validateWsArgs(args);
46
+ if (validationError) {
47
+ console.error(validationError);
48
+ process.exit(1);
496
49
  }
497
50
 
498
- if (sub === "new") return cmdWsNew(args[2]);
499
- if (sub === "switch") return cmdWsSwitch(args[2]);
500
- if (sub === "archive") return cmdWsArchive(args[2]);
501
- if (sub === "status") return cmdWsStatus();
502
- if (sub === "search") return cmdWsSearch(args.slice(2).join(" "));
503
- if (sub === "delete" || sub === "rm") return cmdWsDelete(args[2]);
504
-
505
- // If sub is not a keyword, treat it as a shortcut for ws switch
506
- if (!["new", "switch", "archive", "status", "search", "delete", "rm", "--help", "-h"].includes(sub)) {
507
- return cmdWsSwitch(sub);
51
+ if (!args.length) {
52
+ console.log("Listing workstreams...");
53
+ return; // Implement listing logic.
508
54
  }
509
55
 
510
- console.log(`
511
- ${bold("🌿 Workstream Commands")}
512
-
513
- wispy ws ${dim("list all workstreams")}
514
- wispy ws new <name> ${dim("create new workstream")}
515
- wispy ws switch <name> ${dim("switch active workstream")}
516
- wispy ws <name> ${dim("shortcut for switch")}
517
- wispy ws archive <name> ${dim("archive workstream")}
518
- wispy ws status ${dim("detailed status")}
519
- wispy ws search <query> ${dim("search across all workstreams")}
520
- wispy ws delete <name> ${dim("delete workstream")}
521
- `);
522
- }
56
+ const cmd = args[0];
57
+ console.log(`Executing command: ${cmd}`);
58
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wispy-cli",
3
- "version": "2.7.1",
3
+ "version": "2.7.3",
4
4
  "description": "🌿 Wispy — AI workspace assistant with trustworthy execution (harness, receipts, approvals, diffs)",
5
5
  "license": "MIT",
6
6
  "author": "Minseo & Poropo",