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 +215 -0
- package/core/agents.mjs +133 -0
- package/core/engine.mjs +48 -2
- package/lib/wispy-repl.mjs +141 -3
- package/package.json +1 -1
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") {
|
package/core/agents.mjs
ADDED
|
@@ -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
|
-
|
|
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() {
|
package/lib/wispy-repl.mjs
CHANGED
|
@@ -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