swarmlancer-cli 0.3.1 → 0.4.1

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 CHANGED
@@ -1,4 +1 @@
1
- /**
2
- * Main entry point for interactive TUI mode.
3
- */
4
1
  export declare function runInteractive(): Promise<void>;
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 { initInference, getAvailableModels, setAgentInstructions, getAuthenticatedProviders } from "./inference.js";
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: Model
76
- wizard.setStep(1, "running", "detecting pi credentials...");
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(2, "done", `${agents.length} agent${agents.length === 1 ? "" : "s"}`);
69
+ wizard.setStep(1, "done", `${agents.length} agent${agents.length === 1 ? "" : "s"}`);
90
70
  }
91
71
  else {
92
- wizard.setStep(2, "skipped", "no agents yet");
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(2, "done", "1 agent created");
78
+ wizard.setStep(1, "done", "1 agent created");
99
79
  }
100
80
  else {
101
- wizard.setStep(2, "skipped", "skipped");
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
- // Short pause then go to dashboard
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 that return promises ──────────────────────
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
- try {
127
- agent.instructions = newContent;
128
- saveAgent(agent);
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
- try {
145
- agent.limits = newLimits;
146
- saveAgent(agent);
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
- try {
163
- agent.discovery = newSettings;
164
- saveAgent(agent);
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
- try {
179
- await initInference();
180
- const models = await getAvailableModels();
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
- // ── Agent config sub-menu ─────────────────────────────────
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 the agent ─────────────────────────────────────
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
- let activeModel;
297
- try {
298
- const { model } = await initInference(agent.modelPattern);
299
- activeModel = model;
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);
306
- const sessionGoal = await askSessionGoal();
283
+ const modelInfo = {
284
+ provider: resolved.provider,
285
+ id: resolved.model,
286
+ name: resolved.model,
287
+ };
288
+ const sessionGoal = "";
307
289
  const config = getConfig();
308
290
  const discovery = agent.discovery;
309
291
  const limits = agent.limits;
310
- const screen = new AgentRunningScreen(tui, activeModel, config.serverUrl, agent.name, sessionGoal);
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
- : colors.gray(`${result.score}/10`);
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 found. Waiting for incoming conversations..."));
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} is offline, skipping`));
327
+ log(colors.gray(` ⊘ @${match.profile.githubUsername} offline`));
353
328
  continue;
354
329
  }
355
- log(colors.lime(` 🤝 Reaching out to @${match.profile.githubUsername} (${match.score}/10: ${match.reason})`));
356
- sendToServer({
357
- type: "start_conversation",
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 showMessage("Provider Login", [
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 { initInference, getAvailableModels, setAgentInstructions } from "./inference.js";
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
- console.log("Available models (from pi credentials):\n");
53
- try {
54
- await initInference();
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
- catch (err) {
61
- console.error(err instanceof Error ? err.message : err);
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
- // Init model (agent's model pattern takes priority, then --model flag)
90
- const modelPattern = agent.modelPattern || flags.model;
91
- try {
92
- const { model } = await initInference(modelPattern);
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
- // Keep running until Ctrl+C
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 — guided setup and menu
105
+ (no command) Interactive TUI
117
106
  login Sign in with GitHub
118
107
  agents List configured agents
119
- models List available LLM models
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 for this session
111
+ start --model <pattern> Override model
123
112
  `);
124
113
  }
125
114
  }
@@ -1,17 +1,9 @@
1
- import type { Model, Api } from "@mariozechner/pi-ai";
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 runInference(systemPrompt: string, messages: {
3
+ export declare function setActiveModel(provider: ProviderId, model: string): void;
4
+ type Message = {
15
5
  role: "user" | "assistant";
16
6
  content: string;
17
- }[]): Promise<string>;
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 { AuthStorage, ModelRegistry, createAgentSession, SessionManager, SettingsManager, createExtensionRuntime, } from "@mariozechner/pi-coding-agent";
2
- let authStorage;
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 (!currentModel || !authStorage) {
64
- throw new Error("Inference not initialized. Call initInference() first.");
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
- const resourceLoader = {
71
- getExtensions: () => ({ extensions: [], errors: [], runtime: createExtensionRuntime() }),
72
- getSkills: () => ({ skills: [], diagnostics: [] }),
73
- getPrompts: () => ({ prompts: [], diagnostics: [] }),
74
- getThemes: () => ({ themes: [], diagnostics: [] }),
75
- getAgentsFiles: () => ({ agentsFiles: [] }),
76
- getSystemPrompt: () => fullSystemPrompt,
77
- getAppendSystemPrompt: () => [],
78
- getPathMetadata: () => new Map(),
79
- extendResources: () => { },
80
- reload: async () => { },
81
- };
82
- const { session } = await createAgentSession({
83
- model: currentModel,
84
- thinkingLevel: "off",
85
- tools: [],
86
- authStorage,
87
- modelRegistry,
88
- resourceLoader,
89
- sessionManager: SessionManager.inMemory(),
90
- settingsManager: SettingsManager.inMemory({
91
- compaction: { enabled: false },
92
- retry: { enabled: true, maxRetries: 2 },
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
- // Inject conversation history (all but last message)
96
- if (messages.length > 1) {
97
- for (const msg of messages.slice(0, -1)) {
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
- let responseText = "";
108
- session.subscribe((event) => {
109
- if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
110
- responseText += event.assistantMessageEvent.delta;
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
- // Prompt with the last message
114
- const lastMessage = messages[messages.length - 1];
115
- await session.prompt(lastMessage.content);
116
- session.dispose();
117
- return responseText || "(no response)";
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 { Model, Api } from "@mariozechner/pi-ai";
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: Model<Api>, serverUrl: string, agentName: string, sessionGoal?: string);
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;
@@ -2,13 +2,12 @@ import { Container, Text, Spacer } from "@mariozechner/pi-tui";
2
2
  import { colors } from "../theme.js";
3
3
  const BANNER_ART = [
4
4
  ` █████████ ███`,
5
- ` ███░░░░░███ ███░ `,
6
- `░███ ░░░ ███░ `,
7
- `░░█████████ ███░ `,
8
- ` ░░░░░░░░███░░░███ `,
9
- ` ███ ░███ ░░░███ `,
10
- `░░█████████ ░░░███`,
11
- ` ░░░░░░░░░ ░░░ `,
5
+ ` ███ ███ ███ `,
6
+ ` ███ ███ `,
7
+ ` █████████ ███ `,
8
+ ` ███ ███ `,
9
+ ` ███ ███ ███ `,
10
+ ` █████████ ███`,
12
11
  ];
13
12
  export class BannerComponent extends Container {
14
13
  constructor() {
@@ -22,7 +21,7 @@ export class BannerComponent extends Container {
22
21
  this.addChild(new Text(colors.limeBold(` ${line}`), 1, 0));
23
22
  }
24
23
  this.addChild(new Spacer(0));
25
- this.addChild(new Text(colors.gray(" LET THE SWARM BEGIN"), 1, 0));
24
+ this.addChild(new Text(colors.gray(" LET THE SWARM BEGIN!"), 1, 0));
26
25
  this.addChild(new Spacer(1));
27
26
  }
28
27
  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 items;
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 buildItems;
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
- const KNOWN_PROVIDERS = [
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
- items;
15
- selectableIndices;
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.items = this.buildItems(data);
23
- this.selectableIndices = this.items
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
- buildItems(data) {
28
- const items = [];
29
- items.push({ kind: "action", text: "Go online", indent: 0, action: { type: "start" } });
30
- items.push({ kind: "spacer", text: "", indent: 0 });
31
- items.push({ kind: "header", text: "Swarm", indent: 0 });
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
- items.push({
39
- kind: "action",
40
- text: `${agent.name}${suffix}`,
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
- // Providers
46
- items.push({ kind: "header", text: "Providers", indent: 1 });
47
- items.push({ kind: "label", text: "Select provider to login:", indent: 2 });
48
- for (const p of KNOWN_PROVIDERS) {
49
- const loggedIn = p.match.some((m) => data.authenticatedProviders.some((ap) => ap.includes(m)));
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
- items.push({
53
- kind: "action",
54
- text: `${prefix}${p.label}${suffix}`,
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
- items.push({ kind: "spacer", text: "", indent: 0 });
60
- items.push({ kind: "action", text: "Exit", indent: 0, action: { type: "quit" } });
61
- return items;
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 itemIndex = this.selectableIndices[this.cursor];
84
- const item = this.items[itemIndex];
85
- if (item.action) {
86
- this.onAction?.(item.action);
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 selectedItemIndex = this.selectableIndices[this.cursor];
96
- for (let i = 0; i < this.items.length; i++) {
97
- const item = this.items[i];
98
- const pad = " ".repeat(item.indent);
99
- if (item.kind === "spacer") {
100
- lines.push("");
101
- continue;
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
- // Selectable action
112
- const isSelected = i === selectedItemIndex;
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(` ${pad}${theme.accent(`▸ ${item.text}`)}`, width));
146
+ lines.push(truncateToWidth(`${pad}${theme.accent(`▸ ${label}`)}`, width));
115
147
  }
116
148
  else {
117
- lines.push(truncateToWidth(` ${pad} ${item.text}`, width));
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 { Model, Api } from "@mariozechner/pi-ai";
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: Model<Api>) => void;
8
+ onSelect?: (model: ModelInfo) => void;
9
9
  onCancel?: () => void;
10
- constructor(tui: TUI, models: Model<Api>[]);
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: `${m.name}`,
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 { Model, Api } from "@mariozechner/pi-ai";
2
+ import type { ModelInfo } from "../providers.js";
3
3
  export interface StatusInfo {
4
4
  loggedIn: boolean;
5
- model: Model<Api> | undefined;
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.1",
3
+ "version": "0.4.1",
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"