wispy-cli 2.7.2 โ†’ 2.7.4

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
@@ -45,7 +45,7 @@
45
45
  const { handleWsCommand } = await import(baseDir + '/../lib/commands/ws.mjs');
46
46
  return await handleWsCommand(args);
47
47
  case "help":
48
- 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`);
49
49
  break;
50
50
  case null:
51
51
  console.log("Goodbye!");
@@ -0,0 +1,2 @@
1
+ import { inquirer } from 'inquirer';
2
+ /** Designed by EE interactivity wsMode replaces args[].CORE */
@@ -1,522 +1,32 @@
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
11
- */
1
+ import { select } from '@inquirer/prompts';
12
2
 
13
- import { readFile, writeFile, mkdir, readdir, stat, rename, rm } from "node:fs/promises";
14
- import { existsSync } from "node:fs";
15
- import path from "node:path";
16
- import os from "node:os";
17
-
18
- const WISPY_DIR = path.join(os.homedir(), ".wispy");
19
- 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
- const red = (s) => `\x1b[31m${s}\x1b[0m`;
31
-
32
- async function readJsonOr(filePath, fallback = null) {
3
+ export async function handleWsCommand() {
33
4
  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
- }
55
-
56
- /**
57
- * Get all workstreams by scanning conversations/ dir (legacy) and workstreams/ dir
58
- */
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,
5
+ const action = await select({
6
+ message: 'What WebSocket action would you like to perform?',
7
+ choices: [
8
+ { name: 'Start WebSocket Client', value: 'startClient' },
9
+ { name: 'Run WebSocket Server Debug Mode', value: 'runServerDebug' },
10
+ { name: 'Exit', value: 'exit' }
11
+ ]
111
12
  });
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
- }
223
-
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
13
 
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 {}
14
+ switch (action) {
15
+ case 'startClient':
16
+ console.log('Starting WebSocket client...');
17
+ break;
18
+ case 'runServerDebug':
19
+ console.log('Running server in debug mode...');
20
+ break;
21
+ case 'exit':
22
+ console.log('Exiting WebSocket command.');
23
+ return;
297
24
  }
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;
398
- }
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"));
406
- }
407
-
408
- console.log(green(`๐Ÿ—‘๏ธ Deleted workstream: ${name}`));
409
- }
410
-
411
- 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();
25
+ } catch (err) {
26
+ if (err.name === 'ExitPromptError') {
27
+ console.log('Prompt closed. Exiting command gracefully.');
28
+ } else {
29
+ console.error('Unexpected error:', err);
494
30
  }
495
- return;
496
31
  }
497
-
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);
508
- }
509
-
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
- }
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wispy-cli",
3
- "version": "2.7.2",
3
+ "version": "2.7.4",
4
4
  "description": "๐ŸŒฟ Wispy โ€” AI workspace assistant with trustworthy execution (harness, receipts, approvals, diffs)",
5
5
  "license": "MIT",
6
6
  "author": "Minseo & Poropo",