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 +301 -0
- package/core/agents.mjs +133 -0
- package/core/budget.mjs +277 -0
- package/core/engine.mjs +216 -10
- package/core/harness.mjs +162 -0
- package/core/project-settings.mjs +122 -0
- package/core/session.mjs +27 -3
- package/lib/wispy-repl.mjs +141 -3
- package/package.json +1 -1
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") {
|
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
|
+
}
|