swarmlancer-cli 0.2.1 → 0.3.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.js CHANGED
@@ -1,13 +1,12 @@
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 } from "./inference.js";
4
+ import { initInference, getAvailableModels, setAgentInstructions, getAuthenticatedProviders } from "./inference.js";
5
5
  import { startAgent, stopAgent, sendToServer } from "./agent.js";
6
6
  import { colors } from "./theme.js";
7
7
  import { BannerComponent } from "./screens/banner.js";
8
8
  import { SetupWizardScreen } from "./screens/setup-wizard.js";
9
9
  import { DashboardScreen } from "./screens/dashboard.js";
10
- import { AgentListScreen } from "./screens/agent-list.js";
11
10
  import { AgentConfigScreen } from "./screens/agent-config.js";
12
11
  import { AgentPickerScreen } from "./screens/agent-picker.js";
13
12
  import { AgentEditorScreen } from "./screens/agent-editor.js";
@@ -112,9 +111,6 @@ async function runSetup() {
112
111
  await new Promise((r) => setTimeout(r, 800));
113
112
  }
114
113
  // ── Sub-screens that return promises ──────────────────────
115
- /**
116
- * Ask the user for a name via the NameEditorScreen.
117
- */
118
114
  function askName(title, subtitle, current = "") {
119
115
  return new Promise((resolve) => {
120
116
  const screen = new NameEditorScreen(tui, current, title, subtitle);
@@ -123,9 +119,6 @@ function askName(title, subtitle, current = "") {
123
119
  setScreen(screen);
124
120
  });
125
121
  }
126
- /**
127
- * Inline agent instructions editor.
128
- */
129
122
  function editInstructions(agent) {
130
123
  return new Promise((resolve) => {
131
124
  const screen = new AgentEditorScreen(tui, agent.instructions);
@@ -144,9 +137,6 @@ function editInstructions(agent) {
144
137
  setScreen(screen);
145
138
  });
146
139
  }
147
- /**
148
- * Agent limits settings screen.
149
- */
150
140
  function editLimits(agent) {
151
141
  return new Promise((resolve) => {
152
142
  const screen = new SettingsScreen(tui, agent.limits);
@@ -165,9 +155,6 @@ function editLimits(agent) {
165
155
  setScreen(screen);
166
156
  });
167
157
  }
168
- /**
169
- * Discovery settings screen.
170
- */
171
158
  function editDiscovery(agent) {
172
159
  return new Promise((resolve) => {
173
160
  const screen = new DiscoverySettingsScreen(tui, agent.discovery);
@@ -186,16 +173,13 @@ function editDiscovery(agent) {
186
173
  setScreen(screen);
187
174
  });
188
175
  }
189
- /**
190
- * Model picker screen — saves the model pattern to the agent.
191
- */
192
176
  function editModel(agent) {
193
177
  return new Promise(async (resolve) => {
194
178
  try {
195
179
  await initInference();
196
180
  const models = await getAvailableModels();
197
181
  if (models.length === 0) {
198
- await showMessage("No models", ["No models available.", "Run `pi` and /login to authenticate a provider."], "error");
182
+ await showMessage("No models", ["No models available.", "Select a provider to authenticate."], "error");
199
183
  resolve();
200
184
  return;
201
185
  }
@@ -215,9 +199,6 @@ function editModel(agent) {
215
199
  }
216
200
  });
217
201
  }
218
- /**
219
- * Ask for an optional session goal before starting.
220
- */
221
202
  function askSessionGoal() {
222
203
  return new Promise((resolve) => {
223
204
  const screen = new SessionGoalScreen(tui);
@@ -226,13 +207,10 @@ function askSessionGoal() {
226
207
  setScreen(screen);
227
208
  });
228
209
  }
229
- /**
230
- * Pick which agent to start (or auto-select if only one).
231
- */
232
210
  function pickAgent() {
233
211
  const agents = getAgents();
234
212
  if (agents.length === 0) {
235
- return showMessage("No agents", ["Create an agent first in Manage agents."], "error").then(() => null);
213
+ return showMessage("No agents", ["Create an agent first."], "error").then(() => null);
236
214
  }
237
215
  if (agents.length === 1) {
238
216
  return Promise.resolve(agents[0]);
@@ -249,7 +227,7 @@ async function runAgentConfig(agentId) {
249
227
  while (true) {
250
228
  const agent = getAgent(agentId);
251
229
  if (!agent)
252
- return; // deleted
230
+ return;
253
231
  const action = await new Promise((resolve) => {
254
232
  const screen = new AgentConfigScreen(tui, agent);
255
233
  screen.onAction = resolve;
@@ -286,37 +264,7 @@ async function runAgentConfig(agentId) {
286
264
  }
287
265
  }
288
266
  }
289
- // ── Agent list ────────────────────────────────────────────
290
- async function runAgentList() {
291
- while (true) {
292
- const agents = getAgents();
293
- const action = await new Promise((resolve) => {
294
- const screen = new AgentListScreen(tui, agents);
295
- screen.onAction = resolve;
296
- setScreen(screen);
297
- });
298
- switch (action.type) {
299
- case "create": {
300
- const name = await askName("New Agent", "Give your agent a name.");
301
- if (name) {
302
- const agent = createAgent(name);
303
- // Go straight into editing the new agent
304
- await runAgentConfig(agent.id);
305
- }
306
- break;
307
- }
308
- case "select":
309
- await runAgentConfig(action.agent.id);
310
- break;
311
- case "back":
312
- return;
313
- }
314
- }
315
- }
316
267
  // ── Running the agent ─────────────────────────────────────
317
- /**
318
- * Fetch candidate profiles from the server's /api/discover endpoint.
319
- */
320
268
  async function fetchCandidates(discovery) {
321
269
  const config = getConfig();
322
270
  const params = new URLSearchParams();
@@ -341,15 +289,10 @@ async function fetchCandidates(discovery) {
341
289
  const data = (await res.json());
342
290
  return data.profiles;
343
291
  }
344
- /**
345
- * Start the agent: pick agent → session goal → discover → screen → connect → run.
346
- */
347
292
  async function runAgentSession() {
348
- // 1. Pick agent
349
293
  const agent = await pickAgent();
350
294
  if (!agent)
351
295
  return;
352
- // 2. Init model for this agent
353
296
  let activeModel;
354
297
  try {
355
298
  const { model } = await initInference(agent.modelPattern);
@@ -359,14 +302,11 @@ async function runAgentSession() {
359
302
  await showMessage("Model error", [err instanceof Error ? err.message : "Failed to initialize model"], "error");
360
303
  return;
361
304
  }
362
- // 3. Set agent instructions for inference
363
305
  setAgentInstructions(agent.instructions);
364
- // 4. Ask for session goal
365
306
  const sessionGoal = await askSessionGoal();
366
307
  const config = getConfig();
367
308
  const discovery = agent.discovery;
368
309
  const limits = agent.limits;
369
- // 5. Show the running screen
370
310
  const screen = new AgentRunningScreen(tui, activeModel, config.serverUrl, agent.name, sessionGoal);
371
311
  const done = new Promise((resolve) => {
372
312
  screen.onStop = () => {
@@ -376,9 +316,7 @@ async function runAgentSession() {
376
316
  });
377
317
  setScreen(screen);
378
318
  const log = (line) => screen.addLog(line);
379
- // 6. Start the WebSocket agent (for incoming requests) with per-agent limits
380
319
  startAgent(limits, agent.id, agent.name, log);
381
- // 7. Discovery + screening (outbound)
382
320
  screen.setStatus("discovering candidates...");
383
321
  try {
384
322
  const candidates = await fetchCandidates(discovery);
@@ -409,19 +347,16 @@ async function runAgentSession() {
409
347
  else {
410
348
  log(colors.lime(`Found ${matches.length} match${matches.length === 1 ? "" : "es"}. Starting conversations...`));
411
349
  log("");
412
- // Initiate conversations with matches
413
350
  for (const match of matches) {
414
351
  if (!match.profile.online) {
415
352
  log(colors.gray(` ⊘ @${match.profile.githubUsername} is offline, skipping`));
416
353
  continue;
417
354
  }
418
355
  log(colors.lime(` 🤝 Reaching out to @${match.profile.githubUsername} (${match.score}/10: ${match.reason})`));
419
- // Send start_conversation via WebSocket
420
356
  sendToServer({
421
357
  type: "start_conversation",
422
358
  withUserId: match.profile.id,
423
359
  });
424
- // Respect cooldown between outreach
425
360
  if (limits.cooldownSeconds > 0) {
426
361
  await new Promise((r) => setTimeout(r, limits.cooldownSeconds * 1000));
427
362
  }
@@ -439,19 +374,17 @@ async function runAgentSession() {
439
374
  // ── Dashboard loop ────────────────────────────────────────
440
375
  async function runDashboard() {
441
376
  while (true) {
442
- const config = getConfig();
443
377
  const agents = getAgents();
378
+ const authenticatedProviders = await getAuthenticatedProviders();
444
379
  const action = await new Promise((resolve) => {
445
380
  const dashboard = new DashboardScreen(tui, {
446
- loggedIn: !!config.token,
447
- model: detectedModel,
448
- agentCount: agents.length,
449
- serverUrl: config.serverUrl,
381
+ agents,
382
+ authenticatedProviders,
450
383
  });
451
384
  dashboard.onAction = resolve;
452
385
  setScreen(dashboard);
453
386
  });
454
- switch (action) {
387
+ switch (action.type) {
455
388
  case "start": {
456
389
  const conf = getConfig();
457
390
  if (!conf.token) {
@@ -461,8 +394,27 @@ async function runDashboard() {
461
394
  await runAgentSession();
462
395
  break;
463
396
  }
464
- case "agents":
465
- await runAgentList();
397
+ case "create-agent": {
398
+ const name = await askName("New Agent", "Give your agent a name.");
399
+ if (name) {
400
+ const agent = createAgent(name);
401
+ await runAgentConfig(agent.id);
402
+ }
403
+ break;
404
+ }
405
+ case "edit-agent":
406
+ await runAgentConfig(action.agentId);
407
+ break;
408
+ 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");
466
418
  break;
467
419
  case "quit":
468
420
  shutdown();
@@ -482,10 +434,8 @@ export async function runInteractive() {
482
434
  terminal = new ProcessTerminal();
483
435
  tui = new TUI(terminal, true);
484
436
  tui.start();
485
- // Handle Ctrl+C globally
486
437
  tui.addInputListener((data) => {
487
438
  if (data === "\x03") {
488
- // Ctrl+C
489
439
  shutdown();
490
440
  return { consume: true };
491
441
  }
@@ -3,6 +3,10 @@ export declare function initInference(modelPattern?: string): Promise<{
3
3
  model: Model<Api>;
4
4
  }>;
5
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[]>;
6
10
  /**
7
11
  * Set the agent instructions that will be prepended to every inference call.
8
12
  */
package/dist/inference.js CHANGED
@@ -40,6 +40,19 @@ export async function initInference(modelPattern) {
40
40
  export function getAvailableModels() {
41
41
  return modelRegistry?.getAvailable() ?? Promise.resolve([]);
42
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
+ }
43
56
  /**
44
57
  * Set the agent instructions that will be prepended to every inference call.
45
58
  */
@@ -1,15 +1,33 @@
1
1
  import { type Component } from "@mariozechner/pi-tui";
2
2
  import type { TUI } from "@mariozechner/pi-tui";
3
- import { type StatusInfo } from "./status-panel.js";
4
- export type MenuAction = "start" | "agents" | "quit";
3
+ import type { AgentProfile } from "../config.js";
4
+ export type MenuAction = {
5
+ type: "start";
6
+ } | {
7
+ type: "create-agent";
8
+ } | {
9
+ type: "edit-agent";
10
+ agentId: string;
11
+ } | {
12
+ type: "provider";
13
+ providerId: string;
14
+ } | {
15
+ type: "quit";
16
+ };
17
+ export interface DashboardData {
18
+ agents: AgentProfile[];
19
+ authenticatedProviders: string[];
20
+ }
5
21
  export declare class DashboardScreen implements Component {
6
- private container;
7
- private statusPanel;
8
- private selectList;
9
22
  private tui;
23
+ private banner;
24
+ private items;
25
+ private selectableIndices;
26
+ private cursor;
27
+ private cachedRender?;
10
28
  onAction?: (action: MenuAction) => void;
11
- constructor(tui: TUI, status: StatusInfo);
12
- updateStatus(info: Partial<StatusInfo>): void;
29
+ constructor(tui: TUI, data: DashboardData);
30
+ private buildItems;
13
31
  handleInput(data: string): void;
14
32
  render(width: number): string[];
15
33
  invalidate(): void;
@@ -1,56 +1,128 @@
1
- import { Container, Text, Spacer, SelectList, matchesKey, } from "@mariozechner/pi-tui";
1
+ import { matchesKey, Key, truncateToWidth, } from "@mariozechner/pi-tui";
2
2
  import { colors, theme } from "../theme.js";
3
- import { StatusPanel } from "./status-panel.js";
4
3
  import { BannerComponent } from "./banner.js";
5
- const MENU_ITEMS = [
6
- { value: "start", label: "Start", description: "Pick an agent → set goal → discover → connect" },
7
- { value: "agents", label: "Agents", description: "Create, edit, or delete agents" },
8
- { value: "quit", label: "Exit", description: "" },
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"] },
9
10
  ];
10
11
  export class DashboardScreen {
11
- container;
12
- statusPanel;
13
- selectList;
14
12
  tui;
13
+ banner;
14
+ items;
15
+ selectableIndices;
16
+ cursor = 0;
17
+ cachedRender;
15
18
  onAction;
16
- constructor(tui, status) {
19
+ constructor(tui, data) {
17
20
  this.tui = tui;
18
- this.container = new Container();
19
- // Banner
20
- this.container.addChild(new BannerComponent());
21
- // Status
22
- this.statusPanel = new StatusPanel(status);
23
- this.container.addChild(this.statusPanel);
24
- this.container.addChild(new Spacer(1));
25
- this.selectList = new SelectList(MENU_ITEMS, MENU_ITEMS.length, {
26
- selectedPrefix: (t) => theme.accent(t),
27
- selectedText: (t) => theme.accent(t),
28
- description: (t) => colors.gray(t),
29
- scrollInfo: (t) => colors.gray(t),
30
- noMatch: (t) => colors.gray(t),
31
- });
32
- this.selectList.onSelect = (item) => {
33
- this.onAction?.(item.value);
34
- };
35
- this.container.addChild(this.selectList);
36
- this.container.addChild(new Spacer(1));
37
- this.container.addChild(new Text(colors.gray(" ↑↓ navigate • enter select • q quit"), 1, 0));
21
+ 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);
38
26
  }
39
- updateStatus(info) {
40
- this.statusPanel.update(info);
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" } });
35
+ for (const agent of data.agents) {
36
+ const ready = agent.instructions.length > 0;
37
+ const suffix = ready ? " [⚡]" : "";
38
+ items.push({
39
+ kind: "action",
40
+ text: `${agent.name}${suffix}`,
41
+ indent: 2,
42
+ action: { type: "edit-agent", agentId: agent.id },
43
+ });
44
+ }
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)));
50
+ const prefix = loggedIn ? "→ " : " ";
51
+ const suffix = loggedIn ? " ✓ logged in" : "";
52
+ items.push({
53
+ kind: "action",
54
+ text: `${prefix}${p.label}${suffix}`,
55
+ indent: 3,
56
+ action: { type: "provider", providerId: p.id },
57
+ });
58
+ }
59
+ items.push({ kind: "spacer", text: "", indent: 0 });
60
+ items.push({ kind: "action", text: "Exit", indent: 0, action: { type: "quit" } });
61
+ return items;
41
62
  }
42
63
  handleInput(data) {
43
64
  if (matchesKey(data, "q")) {
44
- this.onAction?.("quit");
65
+ this.onAction?.({ type: "quit" });
66
+ return;
67
+ }
68
+ if (matchesKey(data, Key.up)) {
69
+ if (this.cursor > 0)
70
+ this.cursor--;
71
+ this.cachedRender = undefined;
72
+ this.tui.requestRender();
73
+ return;
74
+ }
75
+ if (matchesKey(data, Key.down)) {
76
+ if (this.cursor < this.selectableIndices.length - 1)
77
+ this.cursor++;
78
+ this.cachedRender = undefined;
79
+ this.tui.requestRender();
80
+ return;
81
+ }
82
+ 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);
87
+ }
45
88
  return;
46
89
  }
47
- this.selectList.handleInput(data);
48
- this.tui.requestRender();
49
90
  }
50
91
  render(width) {
51
- return this.container.render(width);
92
+ if (this.cachedRender)
93
+ return this.cachedRender;
94
+ 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));
109
+ continue;
110
+ }
111
+ // Selectable action
112
+ const isSelected = i === selectedItemIndex;
113
+ if (isSelected) {
114
+ lines.push(truncateToWidth(` ${pad}${theme.accent(`▸ ${item.text}`)}`, width));
115
+ }
116
+ else {
117
+ lines.push(truncateToWidth(` ${pad} ${item.text}`, width));
118
+ }
119
+ }
120
+ lines.push("");
121
+ lines.push(truncateToWidth(colors.gray(" ↑↓ navigate • enter select • q quit"), width));
122
+ this.cachedRender = lines;
123
+ return lines;
52
124
  }
53
125
  invalidate() {
54
- this.container.invalidate();
126
+ this.cachedRender = undefined;
55
127
  }
56
128
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swarmlancer-cli",
3
- "version": "0.2.1",
3
+ "version": "0.3.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",