wispy-cli 2.7.13 → 2.7.14

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
@@ -60,9 +60,24 @@ const globalPersonality = extractFlag(["--personality"], true);
60
60
  const globalJsonMode = hasFlag("--json");
61
61
  if (globalJsonMode) { args.splice(args.indexOf("--json"), 1); }
62
62
 
63
+ // Parse image flags: -i <path> or --image <path> (multiple allowed)
64
+ const imagePaths = [];
65
+ {
66
+ let i = 0;
67
+ while (i < args.length) {
68
+ if ((args[i] === "-i" || args[i] === "--image") && i + 1 < args.length) {
69
+ imagePaths.push(args[i + 1]);
70
+ args.splice(i, 2);
71
+ } else {
72
+ i++;
73
+ }
74
+ }
75
+ }
76
+
63
77
  // Expose for submodules via env
64
78
  if (globalProfile) process.env.WISPY_PROFILE = globalProfile;
65
79
  if (globalPersonality) process.env.WISPY_PERSONALITY = globalPersonality;
80
+ if (imagePaths.length > 0) process.env.WISPY_IMAGES = JSON.stringify(imagePaths);
66
81
 
67
82
  // ── Flags ─────────────────────────────────────────────────────────────────────
68
83
 
@@ -110,10 +125,20 @@ Usage:
110
125
  Manage HTTP/WS server
111
126
  wispy tui Launch full terminal UI
112
127
  wispy overview Director view of workstreams
128
+ wispy sessions [--all] List all sessions
129
+ wispy resume [session-id] Resume a previous session
130
+ wispy resume --last Resume the most recent session
131
+ wispy fork [session-id] Fork (branch) from a session
132
+ wispy fork --last Fork the most recent session
133
+ wispy review Review uncommitted changes
134
+ wispy review --base <branch> Review changes against branch
135
+ wispy review --commit <sha> Review a specific commit
136
+ wispy review --json Output review as JSON
113
137
 
114
138
  Options:
115
139
  -w, --workstream <name> Set active workstream
116
140
  -p, --profile <name> Use a named config profile
141
+ -i, --image <path> Attach image (can use multiple times)
117
142
  --session <id> Resume a session
118
143
  --model <name> Override AI model
119
144
  --provider <name> Override AI provider
@@ -833,6 +858,196 @@ if (command === "model") {
833
858
  process.exit(0);
834
859
  }
835
860
 
861
+ // ── Review ────────────────────────────────────────────────────────────────────
862
+
863
+ if (command === "review") {
864
+ try {
865
+ const { handleReviewCommand } = await import(join(rootDir, "lib/commands/review.mjs"));
866
+ await handleReviewCommand(args.slice(1));
867
+ } catch (err) {
868
+ console.error("Review error:", err.message);
869
+ process.exit(1);
870
+ }
871
+ process.exit(0);
872
+ }
873
+
874
+ // ── Sessions list ─────────────────────────────────────────────────────────────
875
+
876
+ if (command === "sessions") {
877
+ try {
878
+ const { SessionManager } = await import(join(rootDir, "core/session.mjs"));
879
+ const { select } = await import("@inquirer/prompts");
880
+ const mgr = new SessionManager();
881
+ const showAll = args.includes("--all") || args.includes("-a");
882
+
883
+ const sessions = await mgr.listSessions({ all: showAll });
884
+ if (sessions.length === 0) {
885
+ console.log(" No sessions found.");
886
+ process.exit(0);
887
+ }
888
+
889
+ console.log(`\n Sessions (${sessions.length})${showAll ? "" : " — use --all to show all workstreams"}:\n`);
890
+ for (const s of sessions) {
891
+ const ts = new Date(s.updatedAt).toLocaleString();
892
+ const preview = s.firstMessage ? ` "${s.firstMessage.slice(0, 60)}${s.firstMessage.length > 60 ? "…" : ""}"` : "";
893
+ console.log(` ${s.id}`);
894
+ console.log(` ${ts} · ${s.workstream} · ${s.messageCount} msgs${s.model ? ` · ${s.model}` : ""}`);
895
+ if (preview) console.log(` ${preview}`);
896
+ console.log("");
897
+ }
898
+
899
+ // Interactive: if TTY, offer to pick a session
900
+ if (process.stdout.isTTY && !args.includes("--no-interactive")) {
901
+ const choices = [
902
+ { name: "(exit)", value: null },
903
+ ...sessions.map(s => ({
904
+ name: `${s.id} ${new Date(s.updatedAt).toLocaleString()} "${(s.firstMessage ?? "").slice(0, 50)}"`,
905
+ value: s.id,
906
+ })),
907
+ ];
908
+
909
+ let selectedId;
910
+ try {
911
+ selectedId = await select({ message: "Select session:", choices });
912
+ } catch {
913
+ process.exit(0);
914
+ }
915
+
916
+ if (!selectedId) process.exit(0);
917
+
918
+ let action;
919
+ try {
920
+ action = await select({
921
+ message: `Session ${selectedId}:`,
922
+ choices: [
923
+ { name: "resume — continue this session", value: "resume" },
924
+ { name: "fork — start a new branch from this session", value: "fork" },
925
+ { name: "cancel", value: null },
926
+ ],
927
+ });
928
+ } catch {
929
+ process.exit(0);
930
+ }
931
+
932
+ if (!action) process.exit(0);
933
+
934
+ // Delegate to REPL with the appropriate action
935
+ process.env.WISPY_SESSION_ACTION = action;
936
+ process.env.WISPY_SESSION_ID = selectedId;
937
+ await import(join(rootDir, "lib/wispy-repl.mjs"));
938
+ }
939
+ } catch (err) {
940
+ console.error("Sessions error:", err.message);
941
+ process.exit(1);
942
+ }
943
+ process.exit(0);
944
+ }
945
+
946
+ // ── Resume session ────────────────────────────────────────────────────────────
947
+
948
+ if (command === "resume") {
949
+ try {
950
+ const { SessionManager } = await import(join(rootDir, "core/session.mjs"));
951
+ const { select } = await import("@inquirer/prompts");
952
+ const mgr = new SessionManager();
953
+
954
+ let sessionId = args[1]; // may be undefined
955
+
956
+ if (args.includes("--last") || args[1] === "--last") {
957
+ // Resume most recent
958
+ const sessions = await mgr.listSessions({ all: true, limit: 1 });
959
+ if (!sessions.length) {
960
+ console.error("No sessions found.");
961
+ process.exit(1);
962
+ }
963
+ sessionId = sessions[0].id;
964
+ } else if (!sessionId || sessionId.startsWith("--")) {
965
+ // Interactive picker
966
+ const sessions = await mgr.listSessions({ all: true });
967
+ if (!sessions.length) {
968
+ console.error("No sessions found.");
969
+ process.exit(1);
970
+ }
971
+
972
+ const choices = sessions.map(s => ({
973
+ name: `${new Date(s.updatedAt).toLocaleString()} [${s.workstream}] ${s.messageCount}msgs "${(s.firstMessage ?? "").slice(0, 50)}"`,
974
+ value: s.id,
975
+ }));
976
+
977
+ try {
978
+ sessionId = await select({ message: "Resume which session?", choices });
979
+ } catch {
980
+ process.exit(0);
981
+ }
982
+ }
983
+
984
+ if (!sessionId) process.exit(0);
985
+
986
+ console.log(` Resuming session ${sessionId}...`);
987
+ process.env.WISPY_SESSION_ACTION = "resume";
988
+ process.env.WISPY_SESSION_ID = sessionId;
989
+ await import(join(rootDir, "lib/wispy-repl.mjs"));
990
+ } catch (err) {
991
+ console.error("Resume error:", err.message);
992
+ process.exit(1);
993
+ }
994
+ // REPL handles lifecycle
995
+ }
996
+
997
+ // ── Fork session ──────────────────────────────────────────────────────────────
998
+
999
+ if (command === "fork") {
1000
+ try {
1001
+ const { SessionManager } = await import(join(rootDir, "core/session.mjs"));
1002
+ const { select } = await import("@inquirer/prompts");
1003
+ const mgr = new SessionManager();
1004
+
1005
+ let sessionId = args[1];
1006
+
1007
+ if (args.includes("--last") || args[1] === "--last") {
1008
+ const sessions = await mgr.listSessions({ all: true, limit: 1 });
1009
+ if (!sessions.length) {
1010
+ console.error("No sessions found.");
1011
+ process.exit(1);
1012
+ }
1013
+ sessionId = sessions[0].id;
1014
+ } else if (!sessionId || sessionId.startsWith("--")) {
1015
+ const sessions = await mgr.listSessions({ all: true });
1016
+ if (!sessions.length) {
1017
+ console.error("No sessions found.");
1018
+ process.exit(1);
1019
+ }
1020
+
1021
+ const choices = sessions.map(s => ({
1022
+ name: `${new Date(s.updatedAt).toLocaleString()} [${s.workstream}] ${s.messageCount}msgs "${(s.firstMessage ?? "").slice(0, 50)}"`,
1023
+ value: s.id,
1024
+ }));
1025
+
1026
+ try {
1027
+ sessionId = await select({ message: "Fork which session?", choices });
1028
+ } catch {
1029
+ process.exit(0);
1030
+ }
1031
+ }
1032
+
1033
+ if (!sessionId) process.exit(0);
1034
+
1035
+ // Actually fork
1036
+ const forked = await mgr.forkSession(sessionId);
1037
+ console.log(` ✓ Forked from ${sessionId}`);
1038
+ console.log(` New session: ${forked.id} (${forked.messages.length} messages copied)`);
1039
+ console.log(` Starting REPL with forked session...`);
1040
+
1041
+ process.env.WISPY_SESSION_ACTION = "fork";
1042
+ process.env.WISPY_SESSION_ID = forked.id;
1043
+ await import(join(rootDir, "lib/wispy-repl.mjs"));
1044
+ } catch (err) {
1045
+ console.error("Fork error:", err.message);
1046
+ process.exit(1);
1047
+ }
1048
+ // REPL handles lifecycle
1049
+ }
1050
+
836
1051
  // ── TUI ───────────────────────────────────────────────────────────────────────
837
1052
 
838
1053
  if (command === "tui") {
@@ -0,0 +1,133 @@
1
+ /**
2
+ * core/agents.mjs — Custom Agent Definitions for Wispy
3
+ *
4
+ * Allows users to define named agents with custom system prompts and models,
5
+ * similar to Claude Code's --agents feature.
6
+ *
7
+ * Usage:
8
+ * const mgr = new AgentManager(config);
9
+ * const agent = mgr.get("reviewer");
10
+ * // agent = { name, description, prompt, model }
11
+ */
12
+
13
+ // ── Built-in agents ────────────────────────────────────────────────────────────
14
+
15
+ export const BUILTIN_AGENTS = {
16
+ default: {
17
+ description: "General-purpose assistant",
18
+ prompt: null, // uses default Wispy system prompt
19
+ model: null, // uses default model
20
+ },
21
+ reviewer: {
22
+ description: "Code reviewer — bugs, security, performance, best practices",
23
+ prompt: "You are a senior code reviewer. Focus on bugs, security, performance, and best practices. Be constructive and specific. Provide line-level feedback where relevant. End your review with a summary of key findings.",
24
+ model: null,
25
+ },
26
+ planner: {
27
+ description: "Project planner — breaks tasks into concrete steps",
28
+ prompt: "You are a project planner. Break tasks into concrete steps. Create actionable plans with clear priorities and dependencies. Use numbered lists for steps. Estimate effort where possible.",
29
+ model: null,
30
+ },
31
+ explorer: {
32
+ description: "Codebase explorer — navigates files, understands architecture",
33
+ prompt: "You are a codebase explorer. Navigate files, understand architecture, find patterns. Report findings clearly with file paths and code snippets. Summarize what you find at the end.",
34
+ model: null,
35
+ },
36
+ };
37
+
38
+ // ── AgentManager ───────────────────────────────────────────────────────────────
39
+
40
+ export class AgentManager {
41
+ /**
42
+ * @param {object} config - Wispy config object (may have config.agents)
43
+ */
44
+ constructor(config = {}) {
45
+ this._agents = {};
46
+
47
+ // Load built-in agents
48
+ for (const [name, def] of Object.entries(BUILTIN_AGENTS)) {
49
+ this._agents[name] = { name, builtin: true, ...def };
50
+ }
51
+
52
+ // Load custom agents from config
53
+ if (config.agents && typeof config.agents === "object") {
54
+ for (const [name, def] of Object.entries(config.agents)) {
55
+ if (typeof def !== "object" || def === null) continue;
56
+ this._agents[name] = {
57
+ name,
58
+ builtin: false,
59
+ description: def.description ?? `Custom agent: ${name}`,
60
+ prompt: def.prompt ?? null,
61
+ model: def.model ?? null,
62
+ };
63
+ }
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Get an agent by name. Returns null if not found.
69
+ * @param {string} name
70
+ * @returns {{ name: string, description: string, prompt: string|null, model: string|null, builtin: boolean }|null}
71
+ */
72
+ get(name) {
73
+ return this._agents[name] ?? null;
74
+ }
75
+
76
+ /**
77
+ * List all agents (built-in + custom).
78
+ * @returns {Array<{ name: string, description: string, prompt: string|null, model: string|null, builtin: boolean }>}
79
+ */
80
+ list() {
81
+ return Object.values(this._agents);
82
+ }
83
+
84
+ /**
85
+ * Check if an agent exists.
86
+ * @param {string} name
87
+ */
88
+ has(name) {
89
+ return name in this._agents;
90
+ }
91
+
92
+ /**
93
+ * Get all agents as a plain object.
94
+ */
95
+ getAllAgents() {
96
+ return { ...this._agents };
97
+ }
98
+
99
+ /**
100
+ * Format agents for CLI display.
101
+ */
102
+ formatList() {
103
+ const lines = [];
104
+ const builtins = Object.values(this._agents).filter(a => a.builtin);
105
+ const custom = Object.values(this._agents).filter(a => !a.builtin);
106
+
107
+ lines.push("\n Built-in agents:\n");
108
+ for (const a of builtins) {
109
+ lines.push(` \x1b[32m${a.name.padEnd(12)}\x1b[0m ${a.description}`);
110
+ if (a.model) lines.push(` ${" ".repeat(12)} model: ${a.model}`);
111
+ }
112
+
113
+ if (custom.length > 0) {
114
+ lines.push("\n Custom agents:\n");
115
+ for (const a of custom) {
116
+ lines.push(` \x1b[33m${a.name.padEnd(12)}\x1b[0m ${a.description}`);
117
+ if (a.model) lines.push(` ${" ".repeat(12)} model: ${a.model}`);
118
+ if (a.prompt) lines.push(` ${" ".repeat(12)} prompt: ${a.prompt.slice(0, 60)}...`);
119
+ }
120
+ } else {
121
+ lines.push("\n \x1b[2mNo custom agents. Add them in ~/.wispy/config.json under \"agents\".\x1b[0m");
122
+ }
123
+
124
+ lines.push(
125
+ "\n Usage:",
126
+ " wispy --agent reviewer \"review this code\"",
127
+ " wispy --agent planner \"build a todo app\"",
128
+ "",
129
+ );
130
+
131
+ return lines.join("\n");
132
+ }
133
+ }
package/core/engine.mjs CHANGED
@@ -214,8 +214,15 @@ export class WispyEngine {
214
214
  }
215
215
  }
216
216
 
217
- // Add user message
218
- messages.push({ role: "user", content: userMessage });
217
+ // Add user message (with optional image attachments)
218
+ const userMsg = { role: "user", content: userMessage };
219
+ if (opts.images && opts.images.length > 0) {
220
+ const imageData = await this._loadImages(opts.images);
221
+ if (imageData.length > 0) {
222
+ userMsg.images = imageData;
223
+ }
224
+ }
225
+ messages.push(userMsg);
219
226
  this.sessions.addMessage(session.id, { role: "user", content: userMessage });
220
227
 
221
228
  // Audit: log incoming message
@@ -1525,6 +1532,45 @@ export class WispyEngine {
1525
1532
  }
1526
1533
  }
1527
1534
 
1535
+ // ── Image handling ────────────────────────────────────────────────────────────
1536
+
1537
+ /**
1538
+ * Load images from file paths and return base64-encoded data with MIME types.
1539
+ * @param {string[]} imagePaths - Array of file paths
1540
+ * @returns {Array<{data: string, mimeType: string, path: string}>}
1541
+ */
1542
+ async _loadImages(imagePaths) {
1543
+ const MIME_TYPES = {
1544
+ ".png": "image/png",
1545
+ ".jpg": "image/jpeg",
1546
+ ".jpeg": "image/jpeg",
1547
+ ".gif": "image/gif",
1548
+ ".webp": "image/webp",
1549
+ ".bmp": "image/bmp",
1550
+ ".svg": "image/svg+xml",
1551
+ };
1552
+
1553
+ const results = [];
1554
+ for (const imgPath of imagePaths) {
1555
+ try {
1556
+ const resolvedPath = path.resolve(imgPath);
1557
+ const ext = path.extname(resolvedPath).toLowerCase();
1558
+ const mimeType = MIME_TYPES[ext] ?? "image/jpeg";
1559
+ const buffer = await readFile(resolvedPath);
1560
+ results.push({
1561
+ data: buffer.toString("base64"),
1562
+ mimeType,
1563
+ path: resolvedPath,
1564
+ });
1565
+ } catch (err) {
1566
+ if (process.env.WISPY_DEBUG) {
1567
+ console.error(`[wispy] Failed to load image ${imgPath}: ${err.message}`);
1568
+ }
1569
+ }
1570
+ }
1571
+ return results;
1572
+ }
1573
+
1528
1574
  // ── Cleanup ──────────────────────────────────────────────────────────────────
1529
1575
 
1530
1576
  destroy() {
@@ -893,6 +893,21 @@ ${bold("Permissions & Audit (v1.1):")}
893
893
  return true;
894
894
  }
895
895
 
896
+ // ── Image attachment ─────────────────────────────────────────────────────────
897
+
898
+ if (cmd === "/image") {
899
+ const imagePath = parts.slice(1).join(" ");
900
+ if (!imagePath) {
901
+ console.log(yellow("Usage: /image <path> — attach image to next message"));
902
+ return true;
903
+ }
904
+ // Store pending image on engine for next message
905
+ if (!engine._pendingImages) engine._pendingImages = [];
906
+ engine._pendingImages.push(imagePath);
907
+ console.log(green(`📎 Image queued: ${imagePath} — send your message to include it`));
908
+ return true;
909
+ }
910
+
896
911
  if (cmd === "/recall") {
897
912
  const query = parts.slice(1).join(" ");
898
913
  if (!query) { console.log(yellow("Usage: /recall <query>")); return true; }
@@ -1262,6 +1277,10 @@ async function runRepl(engine) {
1262
1277
 
1263
1278
  conversation.push({ role: "user", content: input });
1264
1279
 
1280
+ // Consume pending image attachments (from /image command)
1281
+ const pendingImages = engine._pendingImages ?? [];
1282
+ engine._pendingImages = [];
1283
+
1265
1284
  process.stdout.write(cyan("🌿 "));
1266
1285
  try {
1267
1286
  // Build messages from conversation history (keep system prompt + history)
@@ -1273,6 +1292,7 @@ async function runRepl(engine) {
1273
1292
  systemPrompt: await engine._buildSystemPrompt(input),
1274
1293
  noSave: true,
1275
1294
  dryRun: engine.dryRunMode ?? false,
1295
+ images: pendingImages,
1276
1296
  onSkillLearned: (skill) => {
1277
1297
  console.log(cyan(`\n💡 Learned new skill: '${skill.name}' — use /${skill.name} next time`));
1278
1298
  },
@@ -1307,14 +1327,87 @@ async function runRepl(engine) {
1307
1327
  });
1308
1328
  }
1309
1329
 
1330
+ // ---------------------------------------------------------------------------
1331
+ // REPL with pre-populated history (for resume/fork)
1332
+ // ---------------------------------------------------------------------------
1333
+
1334
+ async function runReplWithHistory(engine, existingConversation) {
1335
+ const wsLabel = ACTIVE_WORKSTREAM === "default" ? "" : ` ${dim("·")} ${cyan(ACTIVE_WORKSTREAM)}`;
1336
+ console.log(`
1337
+ ${bold("🌿 Wispy")}${wsLabel} ${dim(`· ${engine.model}`)}
1338
+ ${dim(`${engine.provider} · /help for commands · Ctrl+C to exit`)}
1339
+ `);
1340
+
1341
+ const conversation = [...existingConversation];
1342
+
1343
+ const rl = createInterface({
1344
+ input: process.stdin,
1345
+ output: process.stdout,
1346
+ prompt: green("› "),
1347
+ historySize: 100,
1348
+ completer: makeCompleter(engine),
1349
+ });
1350
+
1351
+ function updatePrompt() {
1352
+ const ws = ACTIVE_WORKSTREAM !== "default" ? `${cyan(ACTIVE_WORKSTREAM)} ` : "";
1353
+ const dry = engine.dryRunMode ? yellow("(dry) ") : "";
1354
+ rl.setPrompt(ws + dry + green("› "));
1355
+ }
1356
+ updatePrompt();
1357
+ rl.prompt();
1358
+
1359
+ rl.on("line", async (line) => {
1360
+ const input = line.trim();
1361
+ if (!input) { rl.prompt(); return; }
1362
+
1363
+ if (input.startsWith("/")) {
1364
+ const handled = await handleSlashCommand(input, engine, conversation);
1365
+ if (handled) { updatePrompt(); rl.prompt(); return; }
1366
+ }
1367
+
1368
+ conversation.push({ role: "user", content: input });
1369
+
1370
+ // Check for pending image attachment from /image command
1371
+ const pendingImages = engine._pendingImages ?? [];
1372
+ engine._pendingImages = [];
1373
+
1374
+ process.stdout.write(cyan("🌿 "));
1375
+ try {
1376
+ const systemPrompt = await engine._buildSystemPrompt(input);
1377
+ const response = await engine.processMessage(null, input, {
1378
+ onChunk: (chunk) => process.stdout.write(chunk),
1379
+ systemPrompt,
1380
+ noSave: true,
1381
+ images: pendingImages,
1382
+ });
1383
+ console.log("\n");
1384
+ conversation.push({ role: "assistant", content: response.content });
1385
+ if (conversation.length > 50) conversation.splice(0, conversation.length - 50);
1386
+ await saveConversation(conversation);
1387
+ console.log(dim(` ${engine.providers.formatCost()}`));
1388
+ } catch (err) {
1389
+ console.log(red(`\n\n❌ Error: ${err.message.slice(0, 200)}`));
1390
+ }
1391
+
1392
+ rl.prompt();
1393
+ });
1394
+
1395
+ rl.on("close", () => {
1396
+ console.log(dim(`\n🌿 Bye! (${engine.providers.formatCost()})`));
1397
+ try { engine.destroy(); } catch {}
1398
+ process.exit(0);
1399
+ });
1400
+ }
1401
+
1310
1402
  // ---------------------------------------------------------------------------
1311
1403
  // One-shot mode
1312
1404
  // ---------------------------------------------------------------------------
1313
1405
 
1314
- async function runOneShot(engine, message) {
1406
+ async function runOneShot(engine, message, opts = {}) {
1315
1407
  try {
1316
1408
  const response = await engine.processMessage(null, message, {
1317
1409
  onChunk: (chunk) => process.stdout.write(chunk),
1410
+ images: opts.images ?? [],
1318
1411
  });
1319
1412
  console.log("");
1320
1413
  console.log(dim(engine.providers.formatCost()));
@@ -1429,9 +1522,54 @@ if (serverStatus.started && !serverStatus.noBinary) {
1429
1522
  if (args[0] === "overview" || args[0] === "dashboard") { await showOverview(); process.exit(0); }
1430
1523
  if (args[0] === "search" && args[1]) { await searchAcrossWorkstreams(args.slice(1).join(" ")); process.exit(0); }
1431
1524
 
1525
+ // Handle session resume/fork from env (set by wispy resume/fork/sessions commands)
1526
+ const sessionAction = process.env.WISPY_SESSION_ACTION;
1527
+ const sessionActionId = process.env.WISPY_SESSION_ID;
1528
+ if (sessionAction && sessionActionId) {
1529
+ delete process.env.WISPY_SESSION_ACTION;
1530
+ delete process.env.WISPY_SESSION_ID;
1531
+
1532
+ if (sessionAction === "resume") {
1533
+ // Load session and start REPL with existing conversation
1534
+ const { SessionManager } = await import("../core/session.mjs");
1535
+ const mgr = new SessionManager();
1536
+ const session = await mgr.load(sessionActionId);
1537
+ if (!session) {
1538
+ console.error(red(` Session not found: ${sessionActionId}`));
1539
+ process.exit(1);
1540
+ }
1541
+ const conversation = session.messages.filter(m => m.role !== "system");
1542
+ console.log(green(` ▶ Resuming session ${sessionActionId} (${conversation.length} messages)`));
1543
+ console.log(dim(` First message: "${(conversation.find(m => m.role === "user")?.content ?? "").slice(0, 60)}"`));
1544
+ console.log("");
1545
+ // Override loadConversation by starting REPL with pre-populated history
1546
+ await runReplWithHistory(engine, conversation);
1547
+ } else if (sessionAction === "fork") {
1548
+ // Fork already done — just start REPL with forked session
1549
+ const { SessionManager } = await import("../core/session.mjs");
1550
+ const mgr = new SessionManager();
1551
+ const session = await mgr.load(sessionActionId);
1552
+ if (!session) {
1553
+ console.error(red(` Forked session not found: ${sessionActionId}`));
1554
+ process.exit(1);
1555
+ }
1556
+ const conversation = session.messages.filter(m => m.role !== "system");
1557
+ console.log(green(` ⑂ Forked session ${sessionActionId} (${conversation.length} messages)`));
1558
+ console.log("");
1559
+ await runReplWithHistory(engine, conversation);
1560
+ }
1561
+ }
1562
+
1563
+ // Parse image flags from env (set by wispy.mjs -i flag parsing)
1564
+ const globalImages = (() => {
1565
+ try {
1566
+ return process.env.WISPY_IMAGES ? JSON.parse(process.env.WISPY_IMAGES) : [];
1567
+ } catch { return []; }
1568
+ })();
1569
+
1432
1570
  if (args.length > 0 && args[0] !== "--help" && args[0] !== "-h") {
1433
- // One-shot mode
1434
- await runOneShot(engine, args.join(" "));
1571
+ // One-shot mode — with optional image attachments
1572
+ await runOneShot(engine, args.join(" "), { images: globalImages });
1435
1573
  } else if (args[0] === "--help" || args[0] === "-h") {
1436
1574
  console.log(`
1437
1575
  ${bold("🌿 Wispy")} — AI workspace assistant
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wispy-cli",
3
- "version": "2.7.13",
3
+ "version": "2.7.14",
4
4
  "description": "🌿 Wispy — AI workspace assistant with trustworthy execution (harness, receipts, approvals, diffs)",
5
5
  "license": "MIT",
6
6
  "author": "Minseo & Poropo",