swarmlancer-cli 0.3.0 → 0.4.0
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/dist/app.d.ts +0 -3
- package/dist/app.js +90 -134
- package/dist/index.js +22 -33
- package/dist/inference.d.ts +6 -14
- package/dist/inference.js +88 -103
- package/dist/providers.d.ts +30 -0
- package/dist/providers.js +119 -0
- package/dist/screens/agent-running.d.ts +2 -2
- package/dist/screens/banner.js +10 -6
- package/dist/screens/dashboard.d.ts +5 -3
- package/dist/screens/dashboard.js +92 -60
- package/dist/screens/model-picker.d.ts +3 -3
- package/dist/screens/model-picker.js +3 -3
- package/dist/screens/setup-wizard.js +1 -1
- package/dist/screens/status-panel.d.ts +2 -2
- package/package.json +1 -2
package/dist/app.d.ts
CHANGED
package/dist/app.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { ProcessTerminal, TUI, Container } from "@mariozechner/pi-tui";
|
|
2
2
|
import { getConfig, getAgents, getAgent, saveAgent, deleteAgent, createAgent, migrateLegacyAgent, } from "./config.js";
|
|
3
3
|
import { login } from "./login.js";
|
|
4
|
-
import {
|
|
4
|
+
import { setActiveModel, setAgentInstructions } from "./inference.js";
|
|
5
|
+
import { resolveModel, getAvailableModels, isProviderAuthenticated, saveProviderKey, PROVIDERS, } from "./providers.js";
|
|
5
6
|
import { startAgent, stopAgent, sendToServer } from "./agent.js";
|
|
6
7
|
import { colors } from "./theme.js";
|
|
7
8
|
import { BannerComponent } from "./screens/banner.js";
|
|
@@ -20,19 +21,12 @@ import { MessageScreen } from "./screens/message.js";
|
|
|
20
21
|
import { screenProfiles } from "./screening.js";
|
|
21
22
|
let terminal;
|
|
22
23
|
let tui;
|
|
23
|
-
let detectedModel;
|
|
24
|
-
/**
|
|
25
|
-
* Switch the active screen by replacing the TUI's child + focus.
|
|
26
|
-
*/
|
|
27
24
|
function setScreen(component) {
|
|
28
25
|
tui.clear();
|
|
29
26
|
tui.addChild(component);
|
|
30
27
|
tui.setFocus(component);
|
|
31
28
|
tui.requestRender(true);
|
|
32
29
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Show a temporary message and wait for any key.
|
|
35
|
-
*/
|
|
36
30
|
function showMessage(title, lines, style = "info") {
|
|
37
31
|
return new Promise((resolve) => {
|
|
38
32
|
const screen = new MessageScreen(tui, title, lines, style);
|
|
@@ -40,15 +34,11 @@ function showMessage(title, lines, style = "info") {
|
|
|
40
34
|
setScreen(screen);
|
|
41
35
|
});
|
|
42
36
|
}
|
|
43
|
-
|
|
44
|
-
* Run the setup wizard: check auth, model, agents.
|
|
45
|
-
*/
|
|
37
|
+
// ── Setup ─────────────────────────────────────────────────
|
|
46
38
|
async function runSetup() {
|
|
47
|
-
// Migrate legacy single-agent setup
|
|
48
39
|
migrateLegacyAgent();
|
|
49
40
|
const banner = new BannerComponent();
|
|
50
41
|
const wizard = new SetupWizardScreen(tui);
|
|
51
|
-
// Show banner + wizard together
|
|
52
42
|
const wrapper = new Container();
|
|
53
43
|
wrapper.addChild(banner);
|
|
54
44
|
wrapper.addChild(wizard);
|
|
@@ -72,45 +62,43 @@ async function runSetup() {
|
|
|
72
62
|
wizard.setStep(0, "failed", err instanceof Error ? err.message : "login failed");
|
|
73
63
|
}
|
|
74
64
|
}
|
|
75
|
-
// Step 2:
|
|
76
|
-
wizard.setStep(1, "running"
|
|
77
|
-
try {
|
|
78
|
-
const { model } = await initInference();
|
|
79
|
-
detectedModel = model;
|
|
80
|
-
wizard.setStep(1, "done", `${model.provider}/${model.id}`);
|
|
81
|
-
}
|
|
82
|
-
catch (err) {
|
|
83
|
-
wizard.setStep(1, "failed", err instanceof Error ? err.message : "no models found");
|
|
84
|
-
}
|
|
85
|
-
// Step 3: Agents
|
|
86
|
-
wizard.setStep(2, "running");
|
|
65
|
+
// Step 2: Agents
|
|
66
|
+
wizard.setStep(1, "running");
|
|
87
67
|
const agents = getAgents();
|
|
88
68
|
if (agents.length > 0) {
|
|
89
|
-
wizard.setStep(
|
|
69
|
+
wizard.setStep(1, "done", `${agents.length} agent${agents.length === 1 ? "" : "s"}`);
|
|
90
70
|
}
|
|
91
71
|
else {
|
|
92
|
-
wizard.setStep(
|
|
72
|
+
wizard.setStep(1, "skipped", "no agents yet");
|
|
93
73
|
const shouldCreate = await wizard.askConfirm("You have no agents. Create one now?");
|
|
94
74
|
if (shouldCreate) {
|
|
95
75
|
const name = await askName("New Agent Name", "Give your first agent a name.");
|
|
96
76
|
if (name) {
|
|
97
77
|
createAgent(name);
|
|
98
|
-
wizard.setStep(
|
|
78
|
+
wizard.setStep(1, "done", "1 agent created");
|
|
99
79
|
}
|
|
100
80
|
else {
|
|
101
|
-
wizard.setStep(
|
|
81
|
+
wizard.setStep(1, "skipped", "skipped");
|
|
102
82
|
}
|
|
103
|
-
// Re-show the wizard
|
|
104
83
|
tui.clear();
|
|
105
84
|
tui.addChild(wrapper);
|
|
106
85
|
tui.setFocus(wizard);
|
|
107
86
|
tui.requestRender(true);
|
|
108
87
|
}
|
|
109
88
|
}
|
|
110
|
-
//
|
|
89
|
+
// Step 3: Providers
|
|
90
|
+
wizard.setStep(2, "running");
|
|
91
|
+
const models = getAvailableModels();
|
|
92
|
+
if (models.length > 0) {
|
|
93
|
+
const providers = [...new Set(models.map((m) => m.provider))];
|
|
94
|
+
wizard.setStep(2, "done", providers.join(", "));
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
wizard.setStep(2, "skipped", "no API keys — add in Swarm → Providers");
|
|
98
|
+
}
|
|
111
99
|
await new Promise((r) => setTimeout(r, 800));
|
|
112
100
|
}
|
|
113
|
-
// ── Sub-screens
|
|
101
|
+
// ── Sub-screens ───────────────────────────────────────────
|
|
114
102
|
function askName(title, subtitle, current = "") {
|
|
115
103
|
return new Promise((resolve) => {
|
|
116
104
|
const screen = new NameEditorScreen(tui, current, title, subtitle);
|
|
@@ -123,14 +111,9 @@ function editInstructions(agent) {
|
|
|
123
111
|
return new Promise((resolve) => {
|
|
124
112
|
const screen = new AgentEditorScreen(tui, agent.instructions);
|
|
125
113
|
screen.onSave = async (newContent) => {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
await showMessage("Saved", ["Agent instructions updated."], "success");
|
|
130
|
-
}
|
|
131
|
-
catch (err) {
|
|
132
|
-
await showMessage("Error", [err instanceof Error ? err.message : "Failed to save"], "error");
|
|
133
|
-
}
|
|
114
|
+
agent.instructions = newContent;
|
|
115
|
+
saveAgent(agent);
|
|
116
|
+
await showMessage("Saved", ["Agent instructions updated."], "success");
|
|
134
117
|
resolve();
|
|
135
118
|
};
|
|
136
119
|
screen.onCancel = () => resolve();
|
|
@@ -141,14 +124,9 @@ function editLimits(agent) {
|
|
|
141
124
|
return new Promise((resolve) => {
|
|
142
125
|
const screen = new SettingsScreen(tui, agent.limits);
|
|
143
126
|
screen.onSave = async (newLimits) => {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
await showMessage("Saved", ["Agent limits updated."], "success");
|
|
148
|
-
}
|
|
149
|
-
catch (err) {
|
|
150
|
-
await showMessage("Error", [err instanceof Error ? err.message : "Failed to save"], "error");
|
|
151
|
-
}
|
|
127
|
+
agent.limits = newLimits;
|
|
128
|
+
saveAgent(agent);
|
|
129
|
+
await showMessage("Saved", ["Agent limits updated."], "success");
|
|
152
130
|
resolve();
|
|
153
131
|
};
|
|
154
132
|
screen.onCancel = () => resolve();
|
|
@@ -159,14 +137,9 @@ function editDiscovery(agent) {
|
|
|
159
137
|
return new Promise((resolve) => {
|
|
160
138
|
const screen = new DiscoverySettingsScreen(tui, agent.discovery);
|
|
161
139
|
screen.onSave = async (newSettings) => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
await showMessage("Saved", ["Discovery settings updated."], "success");
|
|
166
|
-
}
|
|
167
|
-
catch (err) {
|
|
168
|
-
await showMessage("Error", [err instanceof Error ? err.message : "Failed to save"], "error");
|
|
169
|
-
}
|
|
140
|
+
agent.discovery = newSettings;
|
|
141
|
+
saveAgent(agent);
|
|
142
|
+
await showMessage("Saved", ["Discovery settings updated."], "success");
|
|
170
143
|
resolve();
|
|
171
144
|
};
|
|
172
145
|
screen.onCancel = () => resolve();
|
|
@@ -175,28 +148,21 @@ function editDiscovery(agent) {
|
|
|
175
148
|
}
|
|
176
149
|
function editModel(agent) {
|
|
177
150
|
return new Promise(async (resolve) => {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (models.length === 0) {
|
|
182
|
-
await showMessage("No models", ["No models available.", "Select a provider to authenticate."], "error");
|
|
183
|
-
resolve();
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
const screen = new ModelPickerScreen(tui, models);
|
|
187
|
-
screen.onSelect = async (model) => {
|
|
188
|
-
agent.modelPattern = model.id;
|
|
189
|
-
saveAgent(agent);
|
|
190
|
-
await showMessage("Model selected", [`${model.provider}/${model.id}`], "success");
|
|
191
|
-
resolve();
|
|
192
|
-
};
|
|
193
|
-
screen.onCancel = () => resolve();
|
|
194
|
-
setScreen(screen);
|
|
195
|
-
}
|
|
196
|
-
catch (err) {
|
|
197
|
-
await showMessage("Error", [err instanceof Error ? err.message : "Failed to load models"], "error");
|
|
151
|
+
const models = getAvailableModels();
|
|
152
|
+
if (models.length === 0) {
|
|
153
|
+
await showMessage("No models", ["No provider API keys configured.", "Add one in Swarm → Providers."], "error");
|
|
198
154
|
resolve();
|
|
155
|
+
return;
|
|
199
156
|
}
|
|
157
|
+
const screen = new ModelPickerScreen(tui, models);
|
|
158
|
+
screen.onSelect = async (model) => {
|
|
159
|
+
agent.modelPattern = `${model.provider}/${model.id}`;
|
|
160
|
+
saveAgent(agent);
|
|
161
|
+
await showMessage("Model selected", [`${model.provider}/${model.id}`], "success");
|
|
162
|
+
resolve();
|
|
163
|
+
};
|
|
164
|
+
screen.onCancel = () => resolve();
|
|
165
|
+
setScreen(screen);
|
|
200
166
|
});
|
|
201
167
|
}
|
|
202
168
|
function askSessionGoal() {
|
|
@@ -212,9 +178,8 @@ function pickAgent() {
|
|
|
212
178
|
if (agents.length === 0) {
|
|
213
179
|
return showMessage("No agents", ["Create an agent first."], "error").then(() => null);
|
|
214
180
|
}
|
|
215
|
-
if (agents.length === 1)
|
|
181
|
+
if (agents.length === 1)
|
|
216
182
|
return Promise.resolve(agents[0]);
|
|
217
|
-
}
|
|
218
183
|
return new Promise((resolve) => {
|
|
219
184
|
const screen = new AgentPickerScreen(tui, agents);
|
|
220
185
|
screen.onSelect = (agent) => resolve(agent);
|
|
@@ -222,7 +187,25 @@ function pickAgent() {
|
|
|
222
187
|
setScreen(screen);
|
|
223
188
|
});
|
|
224
189
|
}
|
|
225
|
-
// ──
|
|
190
|
+
// ── Provider login ────────────────────────────────────────
|
|
191
|
+
async function handleProviderLogin(providerId) {
|
|
192
|
+
const provider = PROVIDERS.find((p) => p.id === providerId);
|
|
193
|
+
if (!provider)
|
|
194
|
+
return;
|
|
195
|
+
if (!provider.keyBased) {
|
|
196
|
+
await showMessage("Coming soon", [`${provider.label} is not yet supported.`], "info");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (isProviderAuthenticated(provider.id)) {
|
|
200
|
+
await showMessage("Already configured", [`${provider.label} is already logged in.`, "", "To change the API key, re-enter it below."], "info");
|
|
201
|
+
}
|
|
202
|
+
const key = await askName(`${provider.label}`, "Paste your API key:");
|
|
203
|
+
if (key) {
|
|
204
|
+
saveProviderKey(provider.id, key);
|
|
205
|
+
await showMessage("Saved", [`${provider.label} API key saved.`], "success");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// ── Agent config ──────────────────────────────────────────
|
|
226
209
|
async function runAgentConfig(agentId) {
|
|
227
210
|
while (true) {
|
|
228
211
|
const agent = getAgent(agentId);
|
|
@@ -259,27 +242,23 @@ async function runAgentConfig(agentId) {
|
|
|
259
242
|
deleteAgent(agent.id);
|
|
260
243
|
return;
|
|
261
244
|
}
|
|
262
|
-
case "back":
|
|
263
|
-
return;
|
|
245
|
+
case "back": return;
|
|
264
246
|
}
|
|
265
247
|
}
|
|
266
248
|
}
|
|
267
|
-
// ── Running
|
|
249
|
+
// ── Running agent ─────────────────────────────────────────
|
|
268
250
|
async function fetchCandidates(discovery) {
|
|
269
251
|
const config = getConfig();
|
|
270
252
|
const params = new URLSearchParams();
|
|
271
253
|
if (discovery.onlineOnly)
|
|
272
254
|
params.set("onlineOnly", "true");
|
|
273
255
|
params.set("notContactedInDays", String(discovery.recontactAfterDays));
|
|
274
|
-
if (discovery.includeKeywords.length > 0)
|
|
256
|
+
if (discovery.includeKeywords.length > 0)
|
|
275
257
|
params.set("keywords", discovery.includeKeywords.join(","));
|
|
276
|
-
|
|
277
|
-
if (discovery.excludeKeywords.length > 0) {
|
|
258
|
+
if (discovery.excludeKeywords.length > 0)
|
|
278
259
|
params.set("excludeKeywords", discovery.excludeKeywords.join(","));
|
|
279
|
-
|
|
280
|
-
if (discovery.excludeUsers.length > 0) {
|
|
260
|
+
if (discovery.excludeUsers.length > 0)
|
|
281
261
|
params.set("excludeUsers", discovery.excludeUsers.join(","));
|
|
282
|
-
}
|
|
283
262
|
params.set("limit", String(discovery.maxScreenPerSession));
|
|
284
263
|
const res = await fetch(`${config.serverUrl}/api/discover?${params}`, {
|
|
285
264
|
headers: { Authorization: `Bearer ${config.token}` },
|
|
@@ -293,73 +272,65 @@ async function runAgentSession() {
|
|
|
293
272
|
const agent = await pickAgent();
|
|
294
273
|
if (!agent)
|
|
295
274
|
return;
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
catch (err) {
|
|
302
|
-
await showMessage("Model error", [err instanceof Error ? err.message : "Failed to initialize model"], "error");
|
|
275
|
+
// Resolve model
|
|
276
|
+
const resolved = resolveModel(agent.modelPattern);
|
|
277
|
+
if (!resolved) {
|
|
278
|
+
await showMessage("No model", ["No provider API keys configured.", "Add one in Swarm → Providers."], "error");
|
|
303
279
|
return;
|
|
304
280
|
}
|
|
281
|
+
setActiveModel(resolved.provider, resolved.model);
|
|
305
282
|
setAgentInstructions(agent.instructions);
|
|
283
|
+
const modelInfo = {
|
|
284
|
+
provider: resolved.provider,
|
|
285
|
+
id: resolved.model,
|
|
286
|
+
name: resolved.model,
|
|
287
|
+
};
|
|
306
288
|
const sessionGoal = await askSessionGoal();
|
|
307
289
|
const config = getConfig();
|
|
308
290
|
const discovery = agent.discovery;
|
|
309
291
|
const limits = agent.limits;
|
|
310
|
-
const screen = new AgentRunningScreen(tui,
|
|
292
|
+
const screen = new AgentRunningScreen(tui, modelInfo, config.serverUrl, agent.name, sessionGoal);
|
|
311
293
|
const done = new Promise((resolve) => {
|
|
312
|
-
screen.onStop = () => {
|
|
313
|
-
stopAgent();
|
|
314
|
-
resolve();
|
|
315
|
-
};
|
|
294
|
+
screen.onStop = () => { stopAgent(); resolve(); };
|
|
316
295
|
});
|
|
317
296
|
setScreen(screen);
|
|
318
297
|
const log = (line) => screen.addLog(line);
|
|
319
298
|
startAgent(limits, agent.id, agent.name, log);
|
|
299
|
+
// Discovery
|
|
320
300
|
screen.setStatus("discovering candidates...");
|
|
321
301
|
try {
|
|
322
302
|
const candidates = await fetchCandidates(discovery);
|
|
323
303
|
if (candidates.length === 0) {
|
|
324
304
|
log(colors.gray("No candidates match your discovery filters."));
|
|
325
|
-
log(colors.gray("Waiting for incoming conversations..."));
|
|
326
305
|
screen.setStatus("waiting for conversations...");
|
|
327
306
|
}
|
|
328
307
|
else {
|
|
329
308
|
log(colors.lime(`📡 Found ${candidates.length} candidates, screening...`));
|
|
330
309
|
screen.setStatus(`screening ${candidates.length} profiles...`);
|
|
331
310
|
const results = await screenProfiles(candidates, agent.instructions, sessionGoal, discovery.matchThreshold, discovery.maxScreenPerSession, (screened, total, result) => {
|
|
332
|
-
const icon = result.score >= discovery.matchThreshold
|
|
333
|
-
? colors.lime("✓")
|
|
334
|
-
: colors.gray("⊘");
|
|
335
|
-
const name = `@${result.profile.githubUsername}`;
|
|
311
|
+
const icon = result.score >= discovery.matchThreshold ? colors.lime("✓") : colors.gray("⊘");
|
|
336
312
|
const scoreStr = result.score >= discovery.matchThreshold
|
|
337
|
-
? colors.lime(`${result.score}/10`)
|
|
338
|
-
|
|
339
|
-
log(` ${icon} ${name} ${scoreStr} — ${colors.gray(result.reason)}`);
|
|
313
|
+
? colors.lime(`${result.score}/10`) : colors.gray(`${result.score}/10`);
|
|
314
|
+
log(` ${icon} @${result.profile.githubUsername} ${scoreStr} — ${colors.gray(result.reason)}`);
|
|
340
315
|
screen.setStatus(`screening ${screened}/${total}...`);
|
|
341
316
|
});
|
|
342
317
|
const matches = results.filter((r) => r.score >= discovery.matchThreshold);
|
|
343
318
|
log("");
|
|
344
319
|
if (matches.length === 0) {
|
|
345
|
-
log(colors.lime("No strong matches
|
|
320
|
+
log(colors.lime("No strong matches. Waiting for incoming conversations..."));
|
|
346
321
|
}
|
|
347
322
|
else {
|
|
348
323
|
log(colors.lime(`Found ${matches.length} match${matches.length === 1 ? "" : "es"}. Starting conversations...`));
|
|
349
324
|
log("");
|
|
350
325
|
for (const match of matches) {
|
|
351
326
|
if (!match.profile.online) {
|
|
352
|
-
log(colors.gray(` ⊘ @${match.profile.githubUsername}
|
|
327
|
+
log(colors.gray(` ⊘ @${match.profile.githubUsername} offline`));
|
|
353
328
|
continue;
|
|
354
329
|
}
|
|
355
|
-
log(colors.lime(` 🤝
|
|
356
|
-
sendToServer({
|
|
357
|
-
|
|
358
|
-
withUserId: match.profile.id,
|
|
359
|
-
});
|
|
360
|
-
if (limits.cooldownSeconds > 0) {
|
|
330
|
+
log(colors.lime(` 🤝 @${match.profile.githubUsername} (${match.score}/10: ${match.reason})`));
|
|
331
|
+
sendToServer({ type: "start_conversation", withUserId: match.profile.id });
|
|
332
|
+
if (limits.cooldownSeconds > 0)
|
|
361
333
|
await new Promise((r) => setTimeout(r, limits.cooldownSeconds * 1000));
|
|
362
|
-
}
|
|
363
334
|
}
|
|
364
335
|
}
|
|
365
336
|
screen.setStatus(`${matches.length} matches • waiting for conversations...`);
|
|
@@ -375,12 +346,8 @@ async function runAgentSession() {
|
|
|
375
346
|
async function runDashboard() {
|
|
376
347
|
while (true) {
|
|
377
348
|
const agents = getAgents();
|
|
378
|
-
const authenticatedProviders = await getAuthenticatedProviders();
|
|
379
349
|
const action = await new Promise((resolve) => {
|
|
380
|
-
const dashboard = new DashboardScreen(tui, {
|
|
381
|
-
agents,
|
|
382
|
-
authenticatedProviders,
|
|
383
|
-
});
|
|
350
|
+
const dashboard = new DashboardScreen(tui, { agents });
|
|
384
351
|
dashboard.onAction = resolve;
|
|
385
352
|
setScreen(dashboard);
|
|
386
353
|
});
|
|
@@ -406,15 +373,7 @@ async function runDashboard() {
|
|
|
406
373
|
await runAgentConfig(action.agentId);
|
|
407
374
|
break;
|
|
408
375
|
case "provider":
|
|
409
|
-
await
|
|
410
|
-
`To authenticate with this provider, run:`,
|
|
411
|
-
``,
|
|
412
|
-
` pi`,
|
|
413
|
-
` /login`,
|
|
414
|
-
``,
|
|
415
|
-
`Then select the provider in pi's login flow.`,
|
|
416
|
-
`Your credentials are stored locally in ~/.pi/agent/auth.json`,
|
|
417
|
-
], "info");
|
|
376
|
+
await handleProviderLogin(action.providerId);
|
|
418
377
|
break;
|
|
419
378
|
case "quit":
|
|
420
379
|
shutdown();
|
|
@@ -427,9 +386,6 @@ function shutdown() {
|
|
|
427
386
|
terminal.clearScreen();
|
|
428
387
|
process.exit(0);
|
|
429
388
|
}
|
|
430
|
-
/**
|
|
431
|
-
* Main entry point for interactive TUI mode.
|
|
432
|
-
*/
|
|
433
389
|
export async function runInteractive() {
|
|
434
390
|
terminal = new ProcessTerminal();
|
|
435
391
|
tui = new TUI(terminal, true);
|
package/dist/index.js
CHANGED
|
@@ -2,17 +2,16 @@
|
|
|
2
2
|
import { runInteractive } from "./app.js";
|
|
3
3
|
import { login } from "./login.js";
|
|
4
4
|
import { getConfig, getAgents, migrateLegacyAgent } from "./config.js";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveModel, getAvailableModels } from "./providers.js";
|
|
6
|
+
import { setActiveModel, setAgentInstructions } from "./inference.js";
|
|
6
7
|
import { startAgent } from "./agent.js";
|
|
7
8
|
async function main() {
|
|
8
9
|
const args = process.argv.slice(2);
|
|
9
10
|
const command = args[0];
|
|
10
|
-
// No args → full TUI interactive mode
|
|
11
11
|
if (!command) {
|
|
12
12
|
await runInteractive();
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
|
-
// Parse flags for direct commands
|
|
16
15
|
const flags = {};
|
|
17
16
|
for (let i = 1; i < args.length; i++) {
|
|
18
17
|
if (args[i].startsWith("--") && args[i + 1]) {
|
|
@@ -20,7 +19,6 @@ async function main() {
|
|
|
20
19
|
i++;
|
|
21
20
|
}
|
|
22
21
|
}
|
|
23
|
-
// Direct commands for scripting / CI
|
|
24
22
|
switch (command) {
|
|
25
23
|
case "login":
|
|
26
24
|
await login();
|
|
@@ -49,16 +47,14 @@ async function main() {
|
|
|
49
47
|
break;
|
|
50
48
|
}
|
|
51
49
|
case "models": {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const models = await getAvailableModels();
|
|
56
|
-
for (const m of models)
|
|
57
|
-
console.log(` ${m.provider}/${m.id}`);
|
|
58
|
-
console.log(`\nUse: swarmlancer start --model <pattern>`);
|
|
50
|
+
const models = getAvailableModels();
|
|
51
|
+
if (models.length === 0) {
|
|
52
|
+
console.log("No models available. Add a provider API key: swarmlancer");
|
|
59
53
|
}
|
|
60
|
-
|
|
61
|
-
console.
|
|
54
|
+
else {
|
|
55
|
+
console.log("Available models:\n");
|
|
56
|
+
for (const m of models)
|
|
57
|
+
console.log(` ${m.provider}/${m.id} (${m.name})`);
|
|
62
58
|
}
|
|
63
59
|
break;
|
|
64
60
|
}
|
|
@@ -74,7 +70,6 @@ async function main() {
|
|
|
74
70
|
console.error("No agents configured. Run `swarmlancer` to create one.");
|
|
75
71
|
process.exit(1);
|
|
76
72
|
}
|
|
77
|
-
// Pick agent by name or use first
|
|
78
73
|
let agent = agents[0];
|
|
79
74
|
if (flags.agent) {
|
|
80
75
|
const match = agents.find((a) => a.name.toLowerCase().includes(flags.agent.toLowerCase()));
|
|
@@ -86,25 +81,19 @@ async function main() {
|
|
|
86
81
|
}
|
|
87
82
|
agent = match;
|
|
88
83
|
}
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
setAgentInstructions(agent.instructions);
|
|
94
|
-
console.log(`Agent: ${agent.name}`);
|
|
95
|
-
console.log(`Model: ${model.provider}/${model.id}`);
|
|
96
|
-
console.log(`Server: ${config.serverUrl}`);
|
|
97
|
-
}
|
|
98
|
-
catch (err) {
|
|
99
|
-
console.error(err instanceof Error ? err.message : err);
|
|
84
|
+
const modelPattern = flags.model || agent.modelPattern;
|
|
85
|
+
const resolved = resolveModel(modelPattern);
|
|
86
|
+
if (!resolved) {
|
|
87
|
+
console.error("No model available. Add a provider API key: swarmlancer");
|
|
100
88
|
process.exit(1);
|
|
101
89
|
}
|
|
90
|
+
setActiveModel(resolved.provider, resolved.model);
|
|
91
|
+
setAgentInstructions(agent.instructions);
|
|
92
|
+
console.log(`Agent: ${agent.name}`);
|
|
93
|
+
console.log(`Model: ${resolved.provider}/${resolved.model}`);
|
|
94
|
+
console.log(`Server: ${config.serverUrl}`);
|
|
102
95
|
startAgent(agent.limits, agent.id, agent.name);
|
|
103
|
-
|
|
104
|
-
process.on("SIGINT", () => {
|
|
105
|
-
console.log("\nAgent shutting down...");
|
|
106
|
-
process.exit(0);
|
|
107
|
-
});
|
|
96
|
+
process.on("SIGINT", () => { console.log("\nAgent shutting down..."); process.exit(0); });
|
|
108
97
|
break;
|
|
109
98
|
}
|
|
110
99
|
default:
|
|
@@ -113,13 +102,13 @@ swarmlancer — let the swarm begin
|
|
|
113
102
|
|
|
114
103
|
Usage: swarmlancer [command]
|
|
115
104
|
|
|
116
|
-
(no command) Interactive TUI
|
|
105
|
+
(no command) Interactive TUI
|
|
117
106
|
login Sign in with GitHub
|
|
118
107
|
agents List configured agents
|
|
119
|
-
models List available
|
|
108
|
+
models List available models
|
|
120
109
|
start Start first agent
|
|
121
110
|
start --agent <name> Start a specific agent
|
|
122
|
-
start --model <pattern> Override model
|
|
111
|
+
start --model <pattern> Override model
|
|
123
112
|
`);
|
|
124
113
|
}
|
|
125
114
|
}
|
package/dist/inference.d.ts
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
export declare function initInference(modelPattern?: string): Promise<{
|
|
3
|
-
model: Model<Api>;
|
|
4
|
-
}>;
|
|
5
|
-
export declare function getAvailableModels(): Model<Api>[];
|
|
6
|
-
/**
|
|
7
|
-
* Return unique provider names that have at least one authenticated model.
|
|
8
|
-
*/
|
|
9
|
-
export declare function getAuthenticatedProviders(): Promise<string[]>;
|
|
10
|
-
/**
|
|
11
|
-
* Set the agent instructions that will be prepended to every inference call.
|
|
12
|
-
*/
|
|
1
|
+
import { type ProviderId } from "./providers.js";
|
|
13
2
|
export declare function setAgentInstructions(instructions: string): void;
|
|
14
|
-
export declare function
|
|
3
|
+
export declare function setActiveModel(provider: ProviderId, model: string): void;
|
|
4
|
+
type Message = {
|
|
15
5
|
role: "user" | "assistant";
|
|
16
6
|
content: string;
|
|
17
|
-
}
|
|
7
|
+
};
|
|
8
|
+
export declare function runInference(systemPrompt: string, messages: Message[]): Promise<string>;
|
|
9
|
+
export {};
|
package/dist/inference.js
CHANGED
|
@@ -1,118 +1,103 @@
|
|
|
1
|
-
import {
|
|
2
|
-
let
|
|
3
|
-
let modelRegistry;
|
|
1
|
+
import { getProviderKey } from "./providers.js";
|
|
2
|
+
let currentProvider;
|
|
4
3
|
let currentModel;
|
|
5
4
|
let currentAgentInstructions = "";
|
|
6
|
-
export async function initInference(modelPattern) {
|
|
7
|
-
authStorage = AuthStorage.create(); // reads ~/.pi/agent/auth.json
|
|
8
|
-
modelRegistry = new ModelRegistry(authStorage);
|
|
9
|
-
const available = await modelRegistry.getAvailable();
|
|
10
|
-
if (available.length === 0) {
|
|
11
|
-
throw new Error("No models available. Run `pi` first and authenticate with a provider (Anthropic, OpenAI, Google, Ollama, etc.)");
|
|
12
|
-
}
|
|
13
|
-
if (modelPattern) {
|
|
14
|
-
const match = available.find((m) => m.id.includes(modelPattern) ||
|
|
15
|
-
m.name.toLowerCase().includes(modelPattern.toLowerCase()) ||
|
|
16
|
-
`${m.provider}/${m.id}`.includes(modelPattern));
|
|
17
|
-
if (!match) {
|
|
18
|
-
console.error(` Model "${modelPattern}" not found. Available:`);
|
|
19
|
-
for (const m of available)
|
|
20
|
-
console.error(` ${m.provider}/${m.id}`);
|
|
21
|
-
process.exit(1);
|
|
22
|
-
}
|
|
23
|
-
currentModel = match;
|
|
24
|
-
}
|
|
25
|
-
else {
|
|
26
|
-
// Pick cheapest reasonable model — prefer latest small models
|
|
27
|
-
const preferences = ["haiku-4", "flash", "gpt-4o-mini", "haiku"];
|
|
28
|
-
for (const pref of preferences) {
|
|
29
|
-
const match = available.find((m) => m.id.includes(pref));
|
|
30
|
-
if (match) {
|
|
31
|
-
currentModel = match;
|
|
32
|
-
break;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
if (!currentModel)
|
|
36
|
-
currentModel = available[0];
|
|
37
|
-
}
|
|
38
|
-
return { model: currentModel };
|
|
39
|
-
}
|
|
40
|
-
export function getAvailableModels() {
|
|
41
|
-
return modelRegistry?.getAvailable() ?? Promise.resolve([]);
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Return unique provider names that have at least one authenticated model.
|
|
45
|
-
*/
|
|
46
|
-
export async function getAuthenticatedProviders() {
|
|
47
|
-
try {
|
|
48
|
-
const models = await getAvailableModels();
|
|
49
|
-
const providers = new Set(models.map((m) => m.provider.toLowerCase()));
|
|
50
|
-
return Array.from(providers);
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
return [];
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Set the agent instructions that will be prepended to every inference call.
|
|
58
|
-
*/
|
|
59
5
|
export function setAgentInstructions(instructions) {
|
|
60
6
|
currentAgentInstructions = instructions;
|
|
61
7
|
}
|
|
8
|
+
export function setActiveModel(provider, model) {
|
|
9
|
+
currentProvider = provider;
|
|
10
|
+
currentModel = model;
|
|
11
|
+
}
|
|
62
12
|
export async function runInference(systemPrompt, messages) {
|
|
63
|
-
if (!
|
|
64
|
-
throw new Error("
|
|
13
|
+
if (!currentProvider || !currentModel) {
|
|
14
|
+
throw new Error("No model configured. Add a provider API key and select a model.");
|
|
65
15
|
}
|
|
66
|
-
// Prepend agent instructions to server-provided system prompt
|
|
67
16
|
const fullSystemPrompt = currentAgentInstructions
|
|
68
17
|
? `${currentAgentInstructions}\n\n---\n\n${systemPrompt}`
|
|
69
18
|
: systemPrompt;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
19
|
+
switch (currentProvider) {
|
|
20
|
+
case "anthropic":
|
|
21
|
+
return callAnthropic(fullSystemPrompt, messages, currentModel);
|
|
22
|
+
case "openai":
|
|
23
|
+
return callOpenAI(fullSystemPrompt, messages, currentModel);
|
|
24
|
+
case "google":
|
|
25
|
+
return callGoogle(fullSystemPrompt, messages, currentModel);
|
|
26
|
+
default:
|
|
27
|
+
throw new Error(`Provider "${currentProvider}" does not support direct API calls yet.`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// ── Anthropic ─────────────────────────────────────────────
|
|
31
|
+
async function callAnthropic(system, messages, model) {
|
|
32
|
+
const apiKey = getProviderKey("anthropic");
|
|
33
|
+
if (!apiKey)
|
|
34
|
+
throw new Error("Anthropic API key not configured");
|
|
35
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: {
|
|
38
|
+
"Content-Type": "application/json",
|
|
39
|
+
"x-api-key": apiKey,
|
|
40
|
+
"anthropic-version": "2023-06-01",
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify({
|
|
43
|
+
model,
|
|
44
|
+
max_tokens: 2048,
|
|
45
|
+
system,
|
|
46
|
+
messages,
|
|
93
47
|
}),
|
|
94
48
|
});
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
session.agent.state.messages.push(msg.role === "user"
|
|
99
|
-
? { role: "user", content: [{ type: "text", text: msg.content }], timestamp: Date.now() }
|
|
100
|
-
: {
|
|
101
|
-
role: "assistant",
|
|
102
|
-
content: [{ type: "text", text: msg.content }],
|
|
103
|
-
timestamp: Date.now(),
|
|
104
|
-
});
|
|
105
|
-
}
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
const body = await res.text();
|
|
51
|
+
throw new Error(`Anthropic ${res.status}: ${body.slice(0, 200)}`);
|
|
106
52
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
53
|
+
const data = (await res.json());
|
|
54
|
+
return data.content?.[0]?.text || "(no response)";
|
|
55
|
+
}
|
|
56
|
+
// ── OpenAI ────────────────────────────────────────────────
|
|
57
|
+
async function callOpenAI(system, messages, model) {
|
|
58
|
+
const apiKey = getProviderKey("openai");
|
|
59
|
+
if (!apiKey)
|
|
60
|
+
throw new Error("OpenAI API key not configured");
|
|
61
|
+
const res = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: {
|
|
64
|
+
"Content-Type": "application/json",
|
|
65
|
+
Authorization: `Bearer ${apiKey}`,
|
|
66
|
+
},
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
model,
|
|
69
|
+
messages: [{ role: "system", content: system }, ...messages],
|
|
70
|
+
}),
|
|
112
71
|
});
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
72
|
+
if (!res.ok) {
|
|
73
|
+
const body = await res.text();
|
|
74
|
+
throw new Error(`OpenAI ${res.status}: ${body.slice(0, 200)}`);
|
|
75
|
+
}
|
|
76
|
+
const data = (await res.json());
|
|
77
|
+
return data.choices?.[0]?.message?.content || "(no response)";
|
|
78
|
+
}
|
|
79
|
+
// ── Google Gemini ─────────────────────────────────────────
|
|
80
|
+
async function callGoogle(system, messages, model) {
|
|
81
|
+
const apiKey = getProviderKey("google");
|
|
82
|
+
if (!apiKey)
|
|
83
|
+
throw new Error("Google API key not configured");
|
|
84
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
|
|
85
|
+
const contents = messages.map((m) => ({
|
|
86
|
+
role: m.role === "assistant" ? "model" : "user",
|
|
87
|
+
parts: [{ text: m.content }],
|
|
88
|
+
}));
|
|
89
|
+
const res = await fetch(url, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: { "Content-Type": "application/json" },
|
|
92
|
+
body: JSON.stringify({
|
|
93
|
+
systemInstruction: { parts: [{ text: system }] },
|
|
94
|
+
contents,
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
if (!res.ok) {
|
|
98
|
+
const body = await res.text();
|
|
99
|
+
throw new Error(`Google ${res.status}: ${body.slice(0, 200)}`);
|
|
100
|
+
}
|
|
101
|
+
const data = (await res.json());
|
|
102
|
+
return data.candidates?.[0]?.content?.parts?.[0]?.text || "(no response)";
|
|
118
103
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type ModelInfo = {
|
|
2
|
+
provider: string;
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
};
|
|
6
|
+
export type ProviderId = "anthropic" | "openai" | "google" | "copilot" | "antigravity";
|
|
7
|
+
export type ProviderDef = {
|
|
8
|
+
id: ProviderId;
|
|
9
|
+
label: string;
|
|
10
|
+
keyBased: boolean;
|
|
11
|
+
models: {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
}[];
|
|
15
|
+
};
|
|
16
|
+
export declare const PROVIDERS: ProviderDef[];
|
|
17
|
+
export declare function getProviderKey(id: ProviderId): string | undefined;
|
|
18
|
+
export declare function saveProviderKey(id: ProviderId, apiKey: string): void;
|
|
19
|
+
export declare function removeProviderKey(id: ProviderId): void;
|
|
20
|
+
export declare function getAuthenticatedProviderIds(): ProviderId[];
|
|
21
|
+
export declare function isProviderAuthenticated(id: ProviderId): boolean;
|
|
22
|
+
export declare function getAvailableModels(): ModelInfo[];
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a model pattern like "anthropic/claude-haiku-4-20250514" or just "claude-haiku-4-20250514"
|
|
25
|
+
* into a { provider, model } pair. Falls back to the cheapest available model.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveModel(modelPattern?: string): {
|
|
28
|
+
provider: ProviderId;
|
|
29
|
+
model: string;
|
|
30
|
+
} | undefined;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
const CONFIG_DIR = join(homedir(), ".swarmlancer");
|
|
5
|
+
const PROVIDERS_FILE = join(CONFIG_DIR, "providers.json");
|
|
6
|
+
// ── Known providers ───────────────────────────────────────
|
|
7
|
+
export const PROVIDERS = [
|
|
8
|
+
{
|
|
9
|
+
id: "anthropic",
|
|
10
|
+
label: "Anthropic (Claude Pro/Max)",
|
|
11
|
+
keyBased: true,
|
|
12
|
+
models: [
|
|
13
|
+
{ id: "claude-sonnet-4-20250514", name: "Claude Sonnet 4" },
|
|
14
|
+
{ id: "claude-haiku-4-20250514", name: "Claude Haiku 4" },
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "openai",
|
|
19
|
+
label: "ChatGPT Plus/Pro (Codex Subscription)",
|
|
20
|
+
keyBased: true,
|
|
21
|
+
models: [
|
|
22
|
+
{ id: "gpt-4o", name: "GPT-4o" },
|
|
23
|
+
{ id: "gpt-4o-mini", name: "GPT-4o Mini" },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "google",
|
|
28
|
+
label: "Google Cloud Code Assist (Gemini CLI)",
|
|
29
|
+
keyBased: true,
|
|
30
|
+
models: [
|
|
31
|
+
{ id: "gemini-2.0-flash", name: "Gemini 2.0 Flash" },
|
|
32
|
+
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro" },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "copilot",
|
|
37
|
+
label: "GitHub Copilot",
|
|
38
|
+
keyBased: false,
|
|
39
|
+
models: [],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "antigravity",
|
|
43
|
+
label: "Antigravity (Gemini 3, Claude, GPT-OSS)",
|
|
44
|
+
keyBased: false,
|
|
45
|
+
models: [],
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
function readAll() {
|
|
49
|
+
try {
|
|
50
|
+
if (existsSync(PROVIDERS_FILE)) {
|
|
51
|
+
return JSON.parse(readFileSync(PROVIDERS_FILE, "utf-8"));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch { }
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
function writeAll(data) {
|
|
58
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
59
|
+
writeFileSync(PROVIDERS_FILE, JSON.stringify(data, null, 2));
|
|
60
|
+
}
|
|
61
|
+
export function getProviderKey(id) {
|
|
62
|
+
return readAll()[id]?.apiKey || undefined;
|
|
63
|
+
}
|
|
64
|
+
export function saveProviderKey(id, apiKey) {
|
|
65
|
+
const all = readAll();
|
|
66
|
+
all[id] = { apiKey };
|
|
67
|
+
writeAll(all);
|
|
68
|
+
}
|
|
69
|
+
export function removeProviderKey(id) {
|
|
70
|
+
const all = readAll();
|
|
71
|
+
delete all[id];
|
|
72
|
+
writeAll(all);
|
|
73
|
+
}
|
|
74
|
+
// ── Queries ───────────────────────────────────────────────
|
|
75
|
+
export function getAuthenticatedProviderIds() {
|
|
76
|
+
const all = readAll();
|
|
77
|
+
return Object.keys(all).filter((k) => all[k]?.apiKey);
|
|
78
|
+
}
|
|
79
|
+
export function isProviderAuthenticated(id) {
|
|
80
|
+
return !!getProviderKey(id);
|
|
81
|
+
}
|
|
82
|
+
export function getAvailableModels() {
|
|
83
|
+
const authed = getAuthenticatedProviderIds();
|
|
84
|
+
const models = [];
|
|
85
|
+
for (const p of PROVIDERS) {
|
|
86
|
+
if (authed.includes(p.id)) {
|
|
87
|
+
for (const m of p.models) {
|
|
88
|
+
models.push({ provider: p.id, id: m.id, name: m.name });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return models;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Resolve a model pattern like "anthropic/claude-haiku-4-20250514" or just "claude-haiku-4-20250514"
|
|
96
|
+
* into a { provider, model } pair. Falls back to the cheapest available model.
|
|
97
|
+
*/
|
|
98
|
+
export function resolveModel(modelPattern) {
|
|
99
|
+
const models = getAvailableModels();
|
|
100
|
+
if (models.length === 0)
|
|
101
|
+
return undefined;
|
|
102
|
+
if (modelPattern) {
|
|
103
|
+
// Try "provider/model" format
|
|
104
|
+
if (modelPattern.includes("/")) {
|
|
105
|
+
const [prov, mod] = modelPattern.split("/", 2);
|
|
106
|
+
const match = models.find((m) => m.provider === prov && m.id === mod);
|
|
107
|
+
if (match)
|
|
108
|
+
return { provider: match.provider, model: match.id };
|
|
109
|
+
}
|
|
110
|
+
// Fuzzy match
|
|
111
|
+
const match = models.find((m) => m.id.includes(modelPattern) ||
|
|
112
|
+
m.name.toLowerCase().includes(modelPattern.toLowerCase()));
|
|
113
|
+
if (match)
|
|
114
|
+
return { provider: match.provider, model: match.id };
|
|
115
|
+
}
|
|
116
|
+
// Default: first available model (cheapest tends to be listed last per provider, but first model overall)
|
|
117
|
+
const m = models[0];
|
|
118
|
+
return { provider: m.provider, model: m.id };
|
|
119
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Component } from "@mariozechner/pi-tui";
|
|
2
2
|
import type { TUI } from "@mariozechner/pi-tui";
|
|
3
|
-
import type {
|
|
3
|
+
import type { ModelInfo } from "../providers.js";
|
|
4
4
|
export declare class AgentRunningScreen implements Component {
|
|
5
5
|
private tui;
|
|
6
6
|
private logLines;
|
|
@@ -11,7 +11,7 @@ export declare class AgentRunningScreen implements Component {
|
|
|
11
11
|
private sessionGoal;
|
|
12
12
|
private statusLine;
|
|
13
13
|
onStop?: () => void;
|
|
14
|
-
constructor(tui: TUI, model:
|
|
14
|
+
constructor(tui: TUI, model: ModelInfo, serverUrl: string, agentName: string, sessionGoal?: string);
|
|
15
15
|
setStatus(status: string): void;
|
|
16
16
|
addLog(line: string): void;
|
|
17
17
|
handleInput(data: string): void;
|
package/dist/screens/banner.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { Container, Text, Spacer } from "@mariozechner/pi-tui";
|
|
2
2
|
import { colors } from "../theme.js";
|
|
3
3
|
const BANNER_ART = [
|
|
4
|
-
`
|
|
5
|
-
`
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
` █████████ ███`,
|
|
5
|
+
` ███░░░░░███ ███░ `,
|
|
6
|
+
`░███ ░░░ ███░ `,
|
|
7
|
+
`░░█████████ ███░ `,
|
|
8
|
+
` ░░░░░░░░███░░░███ `,
|
|
9
|
+
` ███ ░███ ░░░███ `,
|
|
10
|
+
`░░█████████ ░░░███`,
|
|
11
|
+
` ░░░░░░░░░ ░░░ `,
|
|
8
12
|
];
|
|
9
13
|
export class BannerComponent extends Container {
|
|
10
14
|
constructor() {
|
|
@@ -15,10 +19,10 @@ export class BannerComponent extends Container {
|
|
|
15
19
|
this.clear();
|
|
16
20
|
this.addChild(new Spacer(1));
|
|
17
21
|
for (const line of BANNER_ART) {
|
|
18
|
-
this.addChild(new Text(colors.limeBold(line), 1, 0));
|
|
22
|
+
this.addChild(new Text(colors.limeBold(` ${line}`), 1, 0));
|
|
19
23
|
}
|
|
20
24
|
this.addChild(new Spacer(0));
|
|
21
|
-
this.addChild(new Text(colors.gray("
|
|
25
|
+
this.addChild(new Text(colors.gray(" LET THE SWARM BEGIN"), 1, 0));
|
|
22
26
|
this.addChild(new Spacer(1));
|
|
23
27
|
}
|
|
24
28
|
invalidate() {
|
|
@@ -16,18 +16,20 @@ export type MenuAction = {
|
|
|
16
16
|
};
|
|
17
17
|
export interface DashboardData {
|
|
18
18
|
agents: AgentProfile[];
|
|
19
|
-
authenticatedProviders: string[];
|
|
20
19
|
}
|
|
21
20
|
export declare class DashboardScreen implements Component {
|
|
22
21
|
private tui;
|
|
23
22
|
private banner;
|
|
24
|
-
private
|
|
23
|
+
private tree;
|
|
24
|
+
private flat;
|
|
25
25
|
private selectableIndices;
|
|
26
26
|
private cursor;
|
|
27
27
|
private cachedRender?;
|
|
28
28
|
onAction?: (action: MenuAction) => void;
|
|
29
29
|
constructor(tui: TUI, data: DashboardData);
|
|
30
|
-
private
|
|
30
|
+
private buildTree;
|
|
31
|
+
private rebuildFlat;
|
|
32
|
+
private flattenTree;
|
|
31
33
|
handleInput(data: string): void;
|
|
32
34
|
render(width: number): string[];
|
|
33
35
|
invalidate(): void;
|
|
@@ -1,64 +1,93 @@
|
|
|
1
1
|
import { matchesKey, Key, truncateToWidth, } from "@mariozechner/pi-tui";
|
|
2
2
|
import { colors, theme } from "../theme.js";
|
|
3
3
|
import { BannerComponent } from "./banner.js";
|
|
4
|
-
|
|
5
|
-
{ id: "anthropic", label: "Anthropic (Claude Pro/Max)", match: ["anthropic"] },
|
|
6
|
-
{ id: "copilot", label: "GitHub Copilot", match: ["copilot", "github"] },
|
|
7
|
-
{ id: "google", label: "Google Cloud Code Assist (Gemini CLI)", match: ["google", "vertex"] },
|
|
8
|
-
{ id: "antigravity", label: "Antigravity (Gemini 3, Claude, GPT-OSS)", match: ["antigravity"] },
|
|
9
|
-
{ id: "openai", label: "ChatGPT Plus/Pro (Codex Subscription)", match: ["openai", "chatgpt"] },
|
|
10
|
-
];
|
|
4
|
+
import { PROVIDERS, isProviderAuthenticated } from "../providers.js";
|
|
11
5
|
export class DashboardScreen {
|
|
12
6
|
tui;
|
|
13
7
|
banner;
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
tree;
|
|
9
|
+
flat = [];
|
|
10
|
+
selectableIndices = [];
|
|
16
11
|
cursor = 0;
|
|
17
12
|
cachedRender;
|
|
18
13
|
onAction;
|
|
19
14
|
constructor(tui, data) {
|
|
20
15
|
this.tui = tui;
|
|
21
16
|
this.banner = new BannerComponent();
|
|
22
|
-
this.
|
|
23
|
-
this.
|
|
24
|
-
.map((item, i) => (item.kind === "action" ? i : -1))
|
|
25
|
-
.filter((i) => i >= 0);
|
|
17
|
+
this.tree = this.buildTree(data);
|
|
18
|
+
this.rebuildFlat();
|
|
26
19
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// Manage
|
|
33
|
-
items.push({ kind: "header", text: "Manage", indent: 1 });
|
|
34
|
-
items.push({ kind: "action", text: "+ Create", indent: 2, action: { type: "create-agent" } });
|
|
20
|
+
buildTree(data) {
|
|
21
|
+
// Manage children
|
|
22
|
+
const manageChildren = [
|
|
23
|
+
{ label: "+ Create", indent: 0, action: { type: "create-agent" }, expanded: false },
|
|
24
|
+
];
|
|
35
25
|
for (const agent of data.agents) {
|
|
36
26
|
const ready = agent.instructions.length > 0;
|
|
37
27
|
const suffix = ready ? " [⚡]" : "";
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
indent: 2,
|
|
28
|
+
manageChildren.push({
|
|
29
|
+
label: `${agent.name}${suffix}`,
|
|
30
|
+
indent: 0,
|
|
42
31
|
action: { type: "edit-agent", agentId: agent.id },
|
|
32
|
+
expanded: false,
|
|
43
33
|
});
|
|
44
34
|
}
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
35
|
+
// Provider children
|
|
36
|
+
const providerChildren = [
|
|
37
|
+
{ label: "Select provider to login:", indent: 0, isLabel: true, expanded: false },
|
|
38
|
+
];
|
|
39
|
+
for (const p of PROVIDERS) {
|
|
40
|
+
const loggedIn = isProviderAuthenticated(p.id);
|
|
50
41
|
const prefix = loggedIn ? "→ " : " ";
|
|
51
42
|
const suffix = loggedIn ? " ✓ logged in" : "";
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
indent: 3,
|
|
43
|
+
providerChildren.push({
|
|
44
|
+
label: `${prefix}${p.label}${suffix}`,
|
|
45
|
+
indent: 0,
|
|
56
46
|
action: { type: "provider", providerId: p.id },
|
|
47
|
+
expanded: false,
|
|
57
48
|
});
|
|
58
49
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
50
|
+
return [
|
|
51
|
+
{ label: "Go online", indent: 0, action: { type: "start" }, expanded: false },
|
|
52
|
+
{
|
|
53
|
+
label: "Swarm",
|
|
54
|
+
indent: 0,
|
|
55
|
+
expanded: false,
|
|
56
|
+
children: [
|
|
57
|
+
{
|
|
58
|
+
label: "Manage",
|
|
59
|
+
indent: 0,
|
|
60
|
+
expanded: false,
|
|
61
|
+
children: manageChildren,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
label: "Providers",
|
|
65
|
+
indent: 0,
|
|
66
|
+
expanded: false,
|
|
67
|
+
children: providerChildren,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
{ label: "Exit", indent: 0, action: { type: "quit" }, expanded: false },
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
rebuildFlat() {
|
|
75
|
+
this.flat = [];
|
|
76
|
+
this.flattenTree(this.tree, 0);
|
|
77
|
+
this.selectableIndices = this.flat
|
|
78
|
+
.map((item, i) => (!item.node.isLabel ? i : -1))
|
|
79
|
+
.filter((i) => i >= 0);
|
|
80
|
+
if (this.cursor >= this.selectableIndices.length) {
|
|
81
|
+
this.cursor = Math.max(0, this.selectableIndices.length - 1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
flattenTree(nodes, indent) {
|
|
85
|
+
for (const node of nodes) {
|
|
86
|
+
this.flat.push({ node, indent });
|
|
87
|
+
if (node.children && node.expanded) {
|
|
88
|
+
this.flattenTree(node.children, indent + 1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
62
91
|
}
|
|
63
92
|
handleInput(data) {
|
|
64
93
|
if (matchesKey(data, "q")) {
|
|
@@ -80,10 +109,18 @@ export class DashboardScreen {
|
|
|
80
109
|
return;
|
|
81
110
|
}
|
|
82
111
|
if (matchesKey(data, Key.enter)) {
|
|
83
|
-
const
|
|
84
|
-
const item = this.
|
|
85
|
-
if (item.
|
|
86
|
-
|
|
112
|
+
const itemIdx = this.selectableIndices[this.cursor];
|
|
113
|
+
const item = this.flat[itemIdx];
|
|
114
|
+
if (item.node.children) {
|
|
115
|
+
// Branch: toggle expand/collapse
|
|
116
|
+
item.node.expanded = !item.node.expanded;
|
|
117
|
+
this.rebuildFlat();
|
|
118
|
+
this.cachedRender = undefined;
|
|
119
|
+
this.tui.requestRender();
|
|
120
|
+
}
|
|
121
|
+
else if (item.node.action) {
|
|
122
|
+
// Leaf: trigger action
|
|
123
|
+
this.onAction?.(item.node.action);
|
|
87
124
|
}
|
|
88
125
|
return;
|
|
89
126
|
}
|
|
@@ -92,33 +129,28 @@ export class DashboardScreen {
|
|
|
92
129
|
if (this.cachedRender)
|
|
93
130
|
return this.cachedRender;
|
|
94
131
|
const lines = this.banner.render(width);
|
|
95
|
-
const
|
|
96
|
-
for (let i = 0; i < this.
|
|
97
|
-
const
|
|
98
|
-
const pad = " ".repeat(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (item.kind === "header") {
|
|
104
|
-
lines.push(truncateToWidth(` ${pad}${colors.bold(item.text)}`, width));
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
if (item.kind === "label") {
|
|
108
|
-
lines.push(truncateToWidth(` ${pad}${colors.gray(item.text)}`, width));
|
|
132
|
+
const selectedFlatIdx = this.selectableIndices[this.cursor];
|
|
133
|
+
for (let i = 0; i < this.flat.length; i++) {
|
|
134
|
+
const { node, indent } = this.flat[i];
|
|
135
|
+
const pad = " ".repeat(indent + 1);
|
|
136
|
+
const isSelected = i === selectedFlatIdx;
|
|
137
|
+
if (node.isLabel) {
|
|
138
|
+
// Non-selectable label
|
|
139
|
+
lines.push(truncateToWidth(`${pad} ${colors.gray(node.label)}`, width));
|
|
109
140
|
continue;
|
|
110
141
|
}
|
|
111
|
-
|
|
112
|
-
const
|
|
142
|
+
const isBranch = !!node.children;
|
|
143
|
+
const arrow = isBranch ? (node.expanded ? " ▾" : " ▸") : "";
|
|
144
|
+
const label = `${node.label}${arrow}`;
|
|
113
145
|
if (isSelected) {
|
|
114
|
-
lines.push(truncateToWidth(
|
|
146
|
+
lines.push(truncateToWidth(`${pad}${theme.accent(`▸ ${label}`)}`, width));
|
|
115
147
|
}
|
|
116
148
|
else {
|
|
117
|
-
lines.push(truncateToWidth(
|
|
149
|
+
lines.push(truncateToWidth(`${pad} ${label}`, width));
|
|
118
150
|
}
|
|
119
151
|
}
|
|
120
152
|
lines.push("");
|
|
121
|
-
lines.push(truncateToWidth(colors.gray(" ↑↓ navigate • enter select • q quit"), width));
|
|
153
|
+
lines.push(truncateToWidth(colors.gray(" ↑↓ navigate • enter expand/select • q quit"), width));
|
|
122
154
|
this.cachedRender = lines;
|
|
123
155
|
return lines;
|
|
124
156
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { type Component } from "@mariozechner/pi-tui";
|
|
2
2
|
import type { TUI } from "@mariozechner/pi-tui";
|
|
3
|
-
import type {
|
|
3
|
+
import type { ModelInfo } from "../providers.js";
|
|
4
4
|
export declare class ModelPickerScreen implements Component {
|
|
5
5
|
private container;
|
|
6
6
|
private selectList;
|
|
7
7
|
private tui;
|
|
8
|
-
onSelect?: (model:
|
|
8
|
+
onSelect?: (model: ModelInfo) => void;
|
|
9
9
|
onCancel?: () => void;
|
|
10
|
-
constructor(tui: TUI, models:
|
|
10
|
+
constructor(tui: TUI, models: ModelInfo[]);
|
|
11
11
|
handleInput(data: string): void;
|
|
12
12
|
render(width: number): string[];
|
|
13
13
|
invalidate(): void;
|
|
@@ -14,8 +14,8 @@ export class ModelPickerScreen {
|
|
|
14
14
|
this.container.addChild(new Text(theme.title(" Pick a model"), 1, 0));
|
|
15
15
|
this.container.addChild(new Spacer(1));
|
|
16
16
|
const items = models.map((m) => ({
|
|
17
|
-
value: m.id
|
|
18
|
-
label:
|
|
17
|
+
value: `${m.provider}/${m.id}`,
|
|
18
|
+
label: m.name,
|
|
19
19
|
description: `${m.provider}/${m.id}`,
|
|
20
20
|
}));
|
|
21
21
|
this.selectList = new SelectList(items, Math.min(items.length, 15), {
|
|
@@ -26,7 +26,7 @@ export class ModelPickerScreen {
|
|
|
26
26
|
noMatch: (t) => colors.gray(t),
|
|
27
27
|
});
|
|
28
28
|
this.selectList.onSelect = (item) => {
|
|
29
|
-
const model = models.find((m) => m.id === item.value);
|
|
29
|
+
const model = models.find((m) => `${m.provider}/${m.id}` === item.value);
|
|
30
30
|
if (model)
|
|
31
31
|
this.onSelect?.(model);
|
|
32
32
|
};
|
|
@@ -13,8 +13,8 @@ export class SetupWizardScreen {
|
|
|
13
13
|
this.tui = tui;
|
|
14
14
|
this.steps = [
|
|
15
15
|
{ label: "Authentication", status: "pending" },
|
|
16
|
-
{ label: "Model detection", status: "pending" },
|
|
17
16
|
{ label: "Agents", status: "pending" },
|
|
17
|
+
{ label: "Providers", status: "pending" },
|
|
18
18
|
];
|
|
19
19
|
}
|
|
20
20
|
setStep(index, status, detail) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Container } from "@mariozechner/pi-tui";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ModelInfo } from "../providers.js";
|
|
3
3
|
export interface StatusInfo {
|
|
4
4
|
loggedIn: boolean;
|
|
5
|
-
model:
|
|
5
|
+
model: ModelInfo | undefined;
|
|
6
6
|
agentCount: number;
|
|
7
7
|
serverUrl: string;
|
|
8
8
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "swarmlancer-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Swarmlancer CLI — let the swarm begin. Connect your AI agent to a network of other agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -41,7 +41,6 @@
|
|
|
41
41
|
"typescript": "^5.8.0"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@mariozechner/pi-coding-agent": "^0.58.4",
|
|
45
44
|
"@mariozechner/pi-tui": "^0.58.4",
|
|
46
45
|
"open": "^11.0.0",
|
|
47
46
|
"ws": "^8.19.0"
|