wispy-cli 2.7.13 → 2.7.15

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,42 @@ const globalPersonality = extractFlag(["--personality"], true);
60
60
  const globalJsonMode = hasFlag("--json");
61
61
  if (globalJsonMode) { args.splice(args.indexOf("--json"), 1); }
62
62
 
63
+ // New flags: system prompt, session name, effort, agent, budget, tool allow/deny
64
+ const globalSystemPrompt = extractFlag(["--system-prompt"], true);
65
+ const globalAppendSystemPrompt = extractFlag(["--append-system-prompt"], true);
66
+ const globalSessionName = extractFlag(["--name"], true);
67
+ const globalAgent = extractFlag(["--agent"], true);
68
+ const globalEffort = extractFlag(["--effort"], true);
69
+ const globalMaxBudget = extractFlag(["--max-budget-usd"], true);
70
+ const globalAllowedTools = extractFlag(["--allowedTools", "--allowed-tools"], true);
71
+ const globalDisallowedTools = extractFlag(["--disallowedTools", "--disallowed-tools"], true);
72
+
73
+ // Parse image flags: -i <path> or --image <path> (multiple allowed)
74
+ const imagePaths = [];
75
+ {
76
+ let i = 0;
77
+ while (i < args.length) {
78
+ if ((args[i] === "-i" || args[i] === "--image") && i + 1 < args.length) {
79
+ imagePaths.push(args[i + 1]);
80
+ args.splice(i, 2);
81
+ } else {
82
+ i++;
83
+ }
84
+ }
85
+ }
86
+
63
87
  // Expose for submodules via env
64
88
  if (globalProfile) process.env.WISPY_PROFILE = globalProfile;
65
89
  if (globalPersonality) process.env.WISPY_PERSONALITY = globalPersonality;
90
+ if (imagePaths.length > 0) process.env.WISPY_IMAGES = JSON.stringify(imagePaths);
91
+ if (globalSystemPrompt) process.env.WISPY_SYSTEM_PROMPT = globalSystemPrompt;
92
+ if (globalAppendSystemPrompt) process.env.WISPY_APPEND_SYSTEM_PROMPT = globalAppendSystemPrompt;
93
+ if (globalSessionName) process.env.WISPY_SESSION_NAME = globalSessionName;
94
+ if (globalAgent) process.env.WISPY_AGENT = globalAgent;
95
+ if (globalEffort) process.env.WISPY_EFFORT = globalEffort;
96
+ if (globalMaxBudget) process.env.WISPY_MAX_BUDGET_USD = globalMaxBudget;
97
+ if (globalAllowedTools) process.env.WISPY_ALLOWED_TOOLS = globalAllowedTools;
98
+ if (globalDisallowedTools) process.env.WISPY_DISALLOWED_TOOLS = globalDisallowedTools;
66
99
 
67
100
  // ── Flags ─────────────────────────────────────────────────────────────────────
68
101
 
@@ -110,21 +143,84 @@ Usage:
110
143
  Manage HTTP/WS server
111
144
  wispy tui Launch full terminal UI
112
145
  wispy overview Director view of workstreams
146
+ wispy sessions [--all] List all sessions
147
+ wispy resume [session-id] Resume a previous session
148
+ wispy resume --last Resume the most recent session
149
+ wispy fork [session-id] Fork (branch) from a session
150
+ wispy fork --last Fork the most recent session
151
+ wispy review Review uncommitted changes
152
+ wispy review --base <branch> Review changes against branch
153
+ wispy review --commit <sha> Review a specific commit
154
+ wispy review --json Output review as JSON
155
+ wispy agents [list] List available agents (built-in + custom)
156
+ wispy cost Show API spending report
113
157
 
114
158
  Options:
115
159
  -w, --workstream <name> Set active workstream
116
160
  -p, --profile <name> Use a named config profile
161
+ -i, --image <path> Attach image (can use multiple times)
117
162
  --session <id> Resume a session
163
+ --name <name> Give this session a display name (e.g. --name "refactor-auth")
118
164
  --model <name> Override AI model
119
165
  --provider <name> Override AI provider
120
166
  --personality <name> Set personality (pragmatic|concise|explanatory|friendly|strict)
167
+ --agent <name> Use a named agent (reviewer|planner|explorer|custom)
168
+ --effort <level> Effort level: low|medium|high|max (default: medium)
169
+ --system-prompt <prompt> Replace the default system prompt entirely
170
+ --append-system-prompt <p> Append text to the default system prompt
171
+ --max-budget-usd <amount> Session budget cap in USD (e.g. 5.00)
172
+ --allowedTools <patterns> Only allow specified tools (e.g. "read_file Bash(git:*)")
173
+ --disallowedTools <patterns> Block specified tools (e.g. "write_file delete_file")
121
174
  --json Output JSONL events (for exec command, CI/pipeline use)
122
175
  --help, -h Show this help
123
176
  --version, -v Show version
177
+
178
+ Project settings:
179
+ .wispy/settings.json Per-project config (model, personality, tools, agents, etc.)
180
+ Wispy walks up from cwd to find this file.
124
181
  `);
125
182
  process.exit(0);
126
183
  }
127
184
 
185
+ // ── Agents ────────────────────────────────────────────────────────────────────
186
+
187
+ if (command === "agents" || command === "agent") {
188
+ try {
189
+ const { loadConfig } = await import(join(rootDir, "core/config.mjs"));
190
+ const { AgentManager } = await import(join(rootDir, "core/agents.mjs"));
191
+
192
+ const sub = args[1];
193
+ const config = await loadConfig();
194
+ const mgr = new AgentManager(config);
195
+
196
+ if (!sub || sub === "list") {
197
+ console.log(mgr.formatList());
198
+ } else {
199
+ console.error(`Unknown subcommand: ${sub}`);
200
+ console.log("Available: list");
201
+ process.exit(1);
202
+ }
203
+ } catch (err) {
204
+ console.error("Agents error:", err.message);
205
+ process.exit(1);
206
+ }
207
+ process.exit(0);
208
+ }
209
+
210
+ // ── Cost / Budget ──────────────────────────────────────────────────────────────
211
+
212
+ if (command === "cost" || command === "budget") {
213
+ try {
214
+ const { BudgetManager } = await import(join(rootDir, "core/budget.mjs"));
215
+ const budget = new BudgetManager();
216
+ console.log(await budget.formatReport());
217
+ } catch (err) {
218
+ console.error("Cost error:", err.message);
219
+ process.exit(1);
220
+ }
221
+ process.exit(0);
222
+ }
223
+
128
224
  // ── Setup (first-run wizard) ──────────────────────────────────────────────────
129
225
 
130
226
  if (command === "setup") {
@@ -655,9 +751,18 @@ if (command === "exec") {
655
751
  if (profileConfig.model) process.env.WISPY_MODEL = process.env.WISPY_MODEL || profileConfig.model;
656
752
 
657
753
  const personality = globalPersonality || profileConfig.personality || null;
754
+ const effort = globalEffort || profileConfig.effort || null;
755
+ const maxBudgetUsd = globalMaxBudget ? parseFloat(globalMaxBudget) : null;
756
+ const allowedTools = globalAllowedTools || null;
757
+ const disallowedTools = globalDisallowedTools || null;
758
+ const agent = globalAgent || null;
658
759
 
659
760
  const engine = new WispyEngine({
660
761
  personality,
762
+ effort,
763
+ maxBudgetUsd,
764
+ allowedTools,
765
+ disallowedTools,
661
766
  workstream: process.env.WISPY_WORKSTREAM ?? "default",
662
767
  });
663
768
 
@@ -673,11 +778,17 @@ if (command === "exec") {
673
778
  process.exit(1);
674
779
  }
675
780
 
781
+ // Apply tool allow/deny to harness
782
+ if (allowedTools) engine.harness.setAllowedTools(allowedTools);
783
+ if (disallowedTools) engine.harness.setDisallowedTools(disallowedTools);
784
+
676
785
  const emitter = createEmitter(globalJsonMode);
677
786
 
678
787
  const result = await engine.processMessage(null, message, {
679
788
  emitter,
680
789
  personality,
790
+ effort,
791
+ agent,
681
792
  skipSkillCapture: true,
682
793
  skipUserModel: true,
683
794
  });
@@ -833,6 +944,196 @@ if (command === "model") {
833
944
  process.exit(0);
834
945
  }
835
946
 
947
+ // ── Review ────────────────────────────────────────────────────────────────────
948
+
949
+ if (command === "review") {
950
+ try {
951
+ const { handleReviewCommand } = await import(join(rootDir, "lib/commands/review.mjs"));
952
+ await handleReviewCommand(args.slice(1));
953
+ } catch (err) {
954
+ console.error("Review error:", err.message);
955
+ process.exit(1);
956
+ }
957
+ process.exit(0);
958
+ }
959
+
960
+ // ── Sessions list ─────────────────────────────────────────────────────────────
961
+
962
+ if (command === "sessions") {
963
+ try {
964
+ const { SessionManager } = await import(join(rootDir, "core/session.mjs"));
965
+ const { select } = await import("@inquirer/prompts");
966
+ const mgr = new SessionManager();
967
+ const showAll = args.includes("--all") || args.includes("-a");
968
+
969
+ const sessions = await mgr.listSessions({ all: showAll });
970
+ if (sessions.length === 0) {
971
+ console.log(" No sessions found.");
972
+ process.exit(0);
973
+ }
974
+
975
+ console.log(`\n Sessions (${sessions.length})${showAll ? "" : " — use --all to show all workstreams"}:\n`);
976
+ for (const s of sessions) {
977
+ const ts = new Date(s.updatedAt).toLocaleString();
978
+ const preview = s.firstMessage ? ` "${s.firstMessage.slice(0, 60)}${s.firstMessage.length > 60 ? "…" : ""}"` : "";
979
+ console.log(` ${s.id}`);
980
+ console.log(` ${ts} · ${s.workstream} · ${s.messageCount} msgs${s.model ? ` · ${s.model}` : ""}`);
981
+ if (preview) console.log(` ${preview}`);
982
+ console.log("");
983
+ }
984
+
985
+ // Interactive: if TTY, offer to pick a session
986
+ if (process.stdout.isTTY && !args.includes("--no-interactive")) {
987
+ const choices = [
988
+ { name: "(exit)", value: null },
989
+ ...sessions.map(s => ({
990
+ name: `${s.id} ${new Date(s.updatedAt).toLocaleString()} "${(s.firstMessage ?? "").slice(0, 50)}"`,
991
+ value: s.id,
992
+ })),
993
+ ];
994
+
995
+ let selectedId;
996
+ try {
997
+ selectedId = await select({ message: "Select session:", choices });
998
+ } catch {
999
+ process.exit(0);
1000
+ }
1001
+
1002
+ if (!selectedId) process.exit(0);
1003
+
1004
+ let action;
1005
+ try {
1006
+ action = await select({
1007
+ message: `Session ${selectedId}:`,
1008
+ choices: [
1009
+ { name: "resume — continue this session", value: "resume" },
1010
+ { name: "fork — start a new branch from this session", value: "fork" },
1011
+ { name: "cancel", value: null },
1012
+ ],
1013
+ });
1014
+ } catch {
1015
+ process.exit(0);
1016
+ }
1017
+
1018
+ if (!action) process.exit(0);
1019
+
1020
+ // Delegate to REPL with the appropriate action
1021
+ process.env.WISPY_SESSION_ACTION = action;
1022
+ process.env.WISPY_SESSION_ID = selectedId;
1023
+ await import(join(rootDir, "lib/wispy-repl.mjs"));
1024
+ }
1025
+ } catch (err) {
1026
+ console.error("Sessions error:", err.message);
1027
+ process.exit(1);
1028
+ }
1029
+ process.exit(0);
1030
+ }
1031
+
1032
+ // ── Resume session ────────────────────────────────────────────────────────────
1033
+
1034
+ if (command === "resume") {
1035
+ try {
1036
+ const { SessionManager } = await import(join(rootDir, "core/session.mjs"));
1037
+ const { select } = await import("@inquirer/prompts");
1038
+ const mgr = new SessionManager();
1039
+
1040
+ let sessionId = args[1]; // may be undefined
1041
+
1042
+ if (args.includes("--last") || args[1] === "--last") {
1043
+ // Resume most recent
1044
+ const sessions = await mgr.listSessions({ all: true, limit: 1 });
1045
+ if (!sessions.length) {
1046
+ console.error("No sessions found.");
1047
+ process.exit(1);
1048
+ }
1049
+ sessionId = sessions[0].id;
1050
+ } else if (!sessionId || sessionId.startsWith("--")) {
1051
+ // Interactive picker
1052
+ const sessions = await mgr.listSessions({ all: true });
1053
+ if (!sessions.length) {
1054
+ console.error("No sessions found.");
1055
+ process.exit(1);
1056
+ }
1057
+
1058
+ const choices = sessions.map(s => ({
1059
+ name: `${new Date(s.updatedAt).toLocaleString()} [${s.workstream}] ${s.messageCount}msgs "${(s.firstMessage ?? "").slice(0, 50)}"`,
1060
+ value: s.id,
1061
+ }));
1062
+
1063
+ try {
1064
+ sessionId = await select({ message: "Resume which session?", choices });
1065
+ } catch {
1066
+ process.exit(0);
1067
+ }
1068
+ }
1069
+
1070
+ if (!sessionId) process.exit(0);
1071
+
1072
+ console.log(` Resuming session ${sessionId}...`);
1073
+ process.env.WISPY_SESSION_ACTION = "resume";
1074
+ process.env.WISPY_SESSION_ID = sessionId;
1075
+ await import(join(rootDir, "lib/wispy-repl.mjs"));
1076
+ } catch (err) {
1077
+ console.error("Resume error:", err.message);
1078
+ process.exit(1);
1079
+ }
1080
+ // REPL handles lifecycle
1081
+ }
1082
+
1083
+ // ── Fork session ──────────────────────────────────────────────────────────────
1084
+
1085
+ if (command === "fork") {
1086
+ try {
1087
+ const { SessionManager } = await import(join(rootDir, "core/session.mjs"));
1088
+ const { select } = await import("@inquirer/prompts");
1089
+ const mgr = new SessionManager();
1090
+
1091
+ let sessionId = args[1];
1092
+
1093
+ if (args.includes("--last") || args[1] === "--last") {
1094
+ const sessions = await mgr.listSessions({ all: true, limit: 1 });
1095
+ if (!sessions.length) {
1096
+ console.error("No sessions found.");
1097
+ process.exit(1);
1098
+ }
1099
+ sessionId = sessions[0].id;
1100
+ } else if (!sessionId || sessionId.startsWith("--")) {
1101
+ const sessions = await mgr.listSessions({ all: true });
1102
+ if (!sessions.length) {
1103
+ console.error("No sessions found.");
1104
+ process.exit(1);
1105
+ }
1106
+
1107
+ const choices = sessions.map(s => ({
1108
+ name: `${new Date(s.updatedAt).toLocaleString()} [${s.workstream}] ${s.messageCount}msgs "${(s.firstMessage ?? "").slice(0, 50)}"`,
1109
+ value: s.id,
1110
+ }));
1111
+
1112
+ try {
1113
+ sessionId = await select({ message: "Fork which session?", choices });
1114
+ } catch {
1115
+ process.exit(0);
1116
+ }
1117
+ }
1118
+
1119
+ if (!sessionId) process.exit(0);
1120
+
1121
+ // Actually fork
1122
+ const forked = await mgr.forkSession(sessionId);
1123
+ console.log(` ✓ Forked from ${sessionId}`);
1124
+ console.log(` New session: ${forked.id} (${forked.messages.length} messages copied)`);
1125
+ console.log(` Starting REPL with forked session...`);
1126
+
1127
+ process.env.WISPY_SESSION_ACTION = "fork";
1128
+ process.env.WISPY_SESSION_ID = forked.id;
1129
+ await import(join(rootDir, "lib/wispy-repl.mjs"));
1130
+ } catch (err) {
1131
+ console.error("Fork error:", err.message);
1132
+ process.exit(1);
1133
+ }
1134
+ // REPL handles lifecycle
1135
+ }
1136
+
836
1137
  // ── TUI ───────────────────────────────────────────────────────────────────────
837
1138
 
838
1139
  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
+ }