swarmlancer-cli 0.2.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.
Files changed (48) hide show
  1. package/README.md +15 -0
  2. package/dist/agent.d.ts +13 -0
  3. package/dist/agent.js +202 -0
  4. package/dist/app.d.ts +4 -0
  5. package/dist/app.js +496 -0
  6. package/dist/config.d.ts +49 -0
  7. package/dist/config.js +175 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +129 -0
  10. package/dist/inference.d.ts +13 -0
  11. package/dist/inference.js +105 -0
  12. package/dist/login.d.ts +1 -0
  13. package/dist/login.js +57 -0
  14. package/dist/screening.d.ts +22 -0
  15. package/dist/screening.js +101 -0
  16. package/dist/screens/agent-config.d.ts +14 -0
  17. package/dist/screens/agent-config.js +64 -0
  18. package/dist/screens/agent-editor.d.ts +13 -0
  19. package/dist/screens/agent-editor.js +64 -0
  20. package/dist/screens/agent-list.d.ts +22 -0
  21. package/dist/screens/agent-list.js +73 -0
  22. package/dist/screens/agent-picker.d.ts +15 -0
  23. package/dist/screens/agent-picker.js +51 -0
  24. package/dist/screens/agent-running.d.ts +20 -0
  25. package/dist/screens/agent-running.js +68 -0
  26. package/dist/screens/banner.d.ts +6 -0
  27. package/dist/screens/banner.js +27 -0
  28. package/dist/screens/dashboard.d.ts +16 -0
  29. package/dist/screens/dashboard.js +59 -0
  30. package/dist/screens/discovery-settings.d.ts +17 -0
  31. package/dist/screens/discovery-settings.js +189 -0
  32. package/dist/screens/message.d.ts +15 -0
  33. package/dist/screens/message.js +39 -0
  34. package/dist/screens/model-picker.d.ts +14 -0
  35. package/dist/screens/model-picker.js +49 -0
  36. package/dist/screens/name-editor.d.ts +15 -0
  37. package/dist/screens/name-editor.js +67 -0
  38. package/dist/screens/session-goal.d.ts +13 -0
  39. package/dist/screens/session-goal.js +61 -0
  40. package/dist/screens/settings.d.ts +15 -0
  41. package/dist/screens/settings.js +126 -0
  42. package/dist/screens/setup-wizard.d.ts +20 -0
  43. package/dist/screens/setup-wizard.js +120 -0
  44. package/dist/screens/status-panel.d.ts +15 -0
  45. package/dist/screens/status-panel.js +37 -0
  46. package/dist/theme.d.ts +42 -0
  47. package/dist/theme.js +56 -0
  48. package/package.json +49 -0
package/dist/app.js ADDED
@@ -0,0 +1,496 @@
1
+ import { ProcessTerminal, TUI, Container } from "@mariozechner/pi-tui";
2
+ import { getConfig, getAgents, getAgent, saveAgent, deleteAgent, createAgent, migrateLegacyAgent, } from "./config.js";
3
+ import { login } from "./login.js";
4
+ import { initInference, getAvailableModels, setAgentInstructions } from "./inference.js";
5
+ import { startAgent, stopAgent, sendToServer } from "./agent.js";
6
+ import { colors } from "./theme.js";
7
+ import { BannerComponent } from "./screens/banner.js";
8
+ import { SetupWizardScreen } from "./screens/setup-wizard.js";
9
+ import { DashboardScreen } from "./screens/dashboard.js";
10
+ import { AgentListScreen } from "./screens/agent-list.js";
11
+ import { AgentConfigScreen } from "./screens/agent-config.js";
12
+ import { AgentPickerScreen } from "./screens/agent-picker.js";
13
+ import { AgentEditorScreen } from "./screens/agent-editor.js";
14
+ import { AgentRunningScreen } from "./screens/agent-running.js";
15
+ import { SettingsScreen } from "./screens/settings.js";
16
+ import { DiscoverySettingsScreen } from "./screens/discovery-settings.js";
17
+ import { ModelPickerScreen } from "./screens/model-picker.js";
18
+ import { NameEditorScreen } from "./screens/name-editor.js";
19
+ import { SessionGoalScreen } from "./screens/session-goal.js";
20
+ import { MessageScreen } from "./screens/message.js";
21
+ import { screenProfiles } from "./screening.js";
22
+ let terminal;
23
+ let tui;
24
+ let detectedModel;
25
+ /**
26
+ * Switch the active screen by replacing the TUI's child + focus.
27
+ */
28
+ function setScreen(component) {
29
+ tui.clear();
30
+ tui.addChild(component);
31
+ tui.setFocus(component);
32
+ tui.requestRender(true);
33
+ }
34
+ /**
35
+ * Show a temporary message and wait for any key.
36
+ */
37
+ function showMessage(title, lines, style = "info") {
38
+ return new Promise((resolve) => {
39
+ const screen = new MessageScreen(tui, title, lines, style);
40
+ screen.onClose = () => resolve();
41
+ setScreen(screen);
42
+ });
43
+ }
44
+ /**
45
+ * Run the setup wizard: check auth, model, agents.
46
+ */
47
+ async function runSetup() {
48
+ // Migrate legacy single-agent setup
49
+ migrateLegacyAgent();
50
+ const banner = new BannerComponent();
51
+ const wizard = new SetupWizardScreen(tui);
52
+ // Show banner + wizard together
53
+ const wrapper = new Container();
54
+ wrapper.addChild(banner);
55
+ wrapper.addChild(wizard);
56
+ tui.clear();
57
+ tui.addChild(wrapper);
58
+ tui.setFocus(wizard);
59
+ tui.requestRender(true);
60
+ // Step 1: Auth
61
+ const config = getConfig();
62
+ wizard.setStep(0, "running");
63
+ if (config.token) {
64
+ wizard.setStep(0, "done", "logged in");
65
+ }
66
+ else {
67
+ wizard.setStep(0, "running", "not logged in — opening browser...");
68
+ try {
69
+ await login();
70
+ wizard.setStep(0, "done", "logged in");
71
+ }
72
+ catch (err) {
73
+ wizard.setStep(0, "failed", err instanceof Error ? err.message : "login failed");
74
+ }
75
+ }
76
+ // Step 2: Model
77
+ wizard.setStep(1, "running", "detecting pi credentials...");
78
+ try {
79
+ const { model } = await initInference();
80
+ detectedModel = model;
81
+ wizard.setStep(1, "done", `${model.provider}/${model.id}`);
82
+ }
83
+ catch (err) {
84
+ wizard.setStep(1, "failed", err instanceof Error ? err.message : "no models found");
85
+ }
86
+ // Step 3: Agents
87
+ wizard.setStep(2, "running");
88
+ const agents = getAgents();
89
+ if (agents.length > 0) {
90
+ wizard.setStep(2, "done", `${agents.length} agent${agents.length === 1 ? "" : "s"}`);
91
+ }
92
+ else {
93
+ wizard.setStep(2, "skipped", "no agents yet");
94
+ const shouldCreate = await wizard.askConfirm("You have no agents. Create one now?");
95
+ if (shouldCreate) {
96
+ const name = await askName("New Agent Name", "Give your first agent a name.");
97
+ if (name) {
98
+ createAgent(name);
99
+ wizard.setStep(2, "done", "1 agent created");
100
+ }
101
+ else {
102
+ wizard.setStep(2, "skipped", "skipped");
103
+ }
104
+ // Re-show the wizard
105
+ tui.clear();
106
+ tui.addChild(wrapper);
107
+ tui.setFocus(wizard);
108
+ tui.requestRender(true);
109
+ }
110
+ }
111
+ // Short pause then go to dashboard
112
+ await new Promise((r) => setTimeout(r, 800));
113
+ }
114
+ // ── Sub-screens that return promises ──────────────────────
115
+ /**
116
+ * Ask the user for a name via the NameEditorScreen.
117
+ */
118
+ function askName(title, subtitle, current = "") {
119
+ return new Promise((resolve) => {
120
+ const screen = new NameEditorScreen(tui, current, title, subtitle);
121
+ screen.onSave = (name) => resolve(name);
122
+ screen.onCancel = () => resolve(null);
123
+ setScreen(screen);
124
+ });
125
+ }
126
+ /**
127
+ * Inline agent instructions editor.
128
+ */
129
+ function editInstructions(agent) {
130
+ return new Promise((resolve) => {
131
+ const screen = new AgentEditorScreen(tui, agent.instructions);
132
+ screen.onSave = async (newContent) => {
133
+ try {
134
+ agent.instructions = newContent;
135
+ saveAgent(agent);
136
+ await showMessage("Saved", ["Agent instructions updated."], "success");
137
+ }
138
+ catch (err) {
139
+ await showMessage("Error", [err instanceof Error ? err.message : "Failed to save"], "error");
140
+ }
141
+ resolve();
142
+ };
143
+ screen.onCancel = () => resolve();
144
+ setScreen(screen);
145
+ });
146
+ }
147
+ /**
148
+ * Agent limits settings screen.
149
+ */
150
+ function editLimits(agent) {
151
+ return new Promise((resolve) => {
152
+ const screen = new SettingsScreen(tui, agent.limits);
153
+ screen.onSave = async (newLimits) => {
154
+ try {
155
+ agent.limits = newLimits;
156
+ saveAgent(agent);
157
+ await showMessage("Saved", ["Agent limits updated."], "success");
158
+ }
159
+ catch (err) {
160
+ await showMessage("Error", [err instanceof Error ? err.message : "Failed to save"], "error");
161
+ }
162
+ resolve();
163
+ };
164
+ screen.onCancel = () => resolve();
165
+ setScreen(screen);
166
+ });
167
+ }
168
+ /**
169
+ * Discovery settings screen.
170
+ */
171
+ function editDiscovery(agent) {
172
+ return new Promise((resolve) => {
173
+ const screen = new DiscoverySettingsScreen(tui, agent.discovery);
174
+ screen.onSave = async (newSettings) => {
175
+ try {
176
+ agent.discovery = newSettings;
177
+ saveAgent(agent);
178
+ await showMessage("Saved", ["Discovery settings updated."], "success");
179
+ }
180
+ catch (err) {
181
+ await showMessage("Error", [err instanceof Error ? err.message : "Failed to save"], "error");
182
+ }
183
+ resolve();
184
+ };
185
+ screen.onCancel = () => resolve();
186
+ setScreen(screen);
187
+ });
188
+ }
189
+ /**
190
+ * Model picker screen — saves the model pattern to the agent.
191
+ */
192
+ function editModel(agent) {
193
+ return new Promise(async (resolve) => {
194
+ try {
195
+ await initInference();
196
+ const models = await getAvailableModels();
197
+ if (models.length === 0) {
198
+ await showMessage("No models", ["No models available.", "Run `pi` and /login to authenticate a provider."], "error");
199
+ resolve();
200
+ return;
201
+ }
202
+ const screen = new ModelPickerScreen(tui, models);
203
+ screen.onSelect = async (model) => {
204
+ agent.modelPattern = model.id;
205
+ saveAgent(agent);
206
+ await showMessage("Model selected", [`${model.provider}/${model.id}`], "success");
207
+ resolve();
208
+ };
209
+ screen.onCancel = () => resolve();
210
+ setScreen(screen);
211
+ }
212
+ catch (err) {
213
+ await showMessage("Error", [err instanceof Error ? err.message : "Failed to load models"], "error");
214
+ resolve();
215
+ }
216
+ });
217
+ }
218
+ /**
219
+ * Ask for an optional session goal before starting.
220
+ */
221
+ function askSessionGoal() {
222
+ return new Promise((resolve) => {
223
+ const screen = new SessionGoalScreen(tui);
224
+ screen.onSubmit = (goal) => resolve(goal.trim());
225
+ screen.onSkip = () => resolve("");
226
+ setScreen(screen);
227
+ });
228
+ }
229
+ /**
230
+ * Pick which agent to start (or auto-select if only one).
231
+ */
232
+ function pickAgent() {
233
+ const agents = getAgents();
234
+ if (agents.length === 0) {
235
+ return showMessage("No agents", ["Create an agent first in Manage agents."], "error").then(() => null);
236
+ }
237
+ if (agents.length === 1) {
238
+ return Promise.resolve(agents[0]);
239
+ }
240
+ return new Promise((resolve) => {
241
+ const screen = new AgentPickerScreen(tui, agents);
242
+ screen.onSelect = (agent) => resolve(agent);
243
+ screen.onCancel = () => resolve(null);
244
+ setScreen(screen);
245
+ });
246
+ }
247
+ // ── Agent config sub-menu ─────────────────────────────────
248
+ async function runAgentConfig(agentId) {
249
+ while (true) {
250
+ const agent = getAgent(agentId);
251
+ if (!agent)
252
+ return; // deleted
253
+ const action = await new Promise((resolve) => {
254
+ const screen = new AgentConfigScreen(tui, agent);
255
+ screen.onAction = resolve;
256
+ setScreen(screen);
257
+ });
258
+ switch (action) {
259
+ case "edit-name": {
260
+ const newName = await askName("Rename Agent", "Enter a new name for this agent.", agent.name);
261
+ if (newName) {
262
+ agent.name = newName;
263
+ saveAgent(agent);
264
+ }
265
+ break;
266
+ }
267
+ case "edit-instructions":
268
+ await editInstructions(agent);
269
+ break;
270
+ case "edit-discovery":
271
+ await editDiscovery(agent);
272
+ break;
273
+ case "edit-limits":
274
+ await editLimits(agent);
275
+ break;
276
+ case "edit-model":
277
+ await editModel(agent);
278
+ break;
279
+ case "delete": {
280
+ await showMessage("Deleted", [`Agent "${agent.name}" has been deleted.`], "info");
281
+ deleteAgent(agent.id);
282
+ return;
283
+ }
284
+ case "back":
285
+ return;
286
+ }
287
+ }
288
+ }
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
+ // ── Running the agent ─────────────────────────────────────
317
+ /**
318
+ * Fetch candidate profiles from the server's /api/discover endpoint.
319
+ */
320
+ async function fetchCandidates(discovery) {
321
+ const config = getConfig();
322
+ const params = new URLSearchParams();
323
+ if (discovery.onlineOnly)
324
+ params.set("onlineOnly", "true");
325
+ params.set("notContactedInDays", String(discovery.recontactAfterDays));
326
+ if (discovery.includeKeywords.length > 0) {
327
+ params.set("keywords", discovery.includeKeywords.join(","));
328
+ }
329
+ if (discovery.excludeKeywords.length > 0) {
330
+ params.set("excludeKeywords", discovery.excludeKeywords.join(","));
331
+ }
332
+ if (discovery.excludeUsers.length > 0) {
333
+ params.set("excludeUsers", discovery.excludeUsers.join(","));
334
+ }
335
+ params.set("limit", String(discovery.maxScreenPerSession));
336
+ const res = await fetch(`${config.serverUrl}/api/discover?${params}`, {
337
+ headers: { Authorization: `Bearer ${config.token}` },
338
+ });
339
+ if (!res.ok)
340
+ throw new Error(`Discover API error: ${res.status}`);
341
+ const data = (await res.json());
342
+ return data.profiles;
343
+ }
344
+ /**
345
+ * Start the agent: pick agent → session goal → discover → screen → connect → run.
346
+ */
347
+ async function runAgentSession() {
348
+ // 1. Pick agent
349
+ const agent = await pickAgent();
350
+ if (!agent)
351
+ return;
352
+ // 2. Init model for this agent
353
+ let activeModel;
354
+ try {
355
+ const { model } = await initInference(agent.modelPattern);
356
+ activeModel = model;
357
+ }
358
+ catch (err) {
359
+ await showMessage("Model error", [err instanceof Error ? err.message : "Failed to initialize model"], "error");
360
+ return;
361
+ }
362
+ // 3. Set agent instructions for inference
363
+ setAgentInstructions(agent.instructions);
364
+ // 4. Ask for session goal
365
+ const sessionGoal = await askSessionGoal();
366
+ const config = getConfig();
367
+ const discovery = agent.discovery;
368
+ const limits = agent.limits;
369
+ // 5. Show the running screen
370
+ const screen = new AgentRunningScreen(tui, activeModel, config.serverUrl, agent.name, sessionGoal);
371
+ const done = new Promise((resolve) => {
372
+ screen.onStop = () => {
373
+ stopAgent();
374
+ resolve();
375
+ };
376
+ });
377
+ setScreen(screen);
378
+ const log = (line) => screen.addLog(line);
379
+ // 6. Start the WebSocket agent (for incoming requests) with per-agent limits
380
+ startAgent(limits, agent.id, agent.name, log);
381
+ // 7. Discovery + screening (outbound)
382
+ screen.setStatus("discovering candidates...");
383
+ try {
384
+ const candidates = await fetchCandidates(discovery);
385
+ if (candidates.length === 0) {
386
+ log(colors.gray("No candidates match your discovery filters."));
387
+ log(colors.gray("Waiting for incoming conversations..."));
388
+ screen.setStatus("waiting for conversations...");
389
+ }
390
+ else {
391
+ log(colors.cyan(`📡 Found ${candidates.length} candidates, screening...`));
392
+ screen.setStatus(`screening ${candidates.length} profiles...`);
393
+ const results = await screenProfiles(candidates, agent.instructions, sessionGoal, discovery.matchThreshold, discovery.maxScreenPerSession, (screened, total, result) => {
394
+ const icon = result.score >= discovery.matchThreshold
395
+ ? colors.green("✓")
396
+ : colors.gray("⊘");
397
+ const name = `@${result.profile.githubUsername}`;
398
+ const scoreStr = result.score >= discovery.matchThreshold
399
+ ? colors.green(`${result.score}/10`)
400
+ : colors.gray(`${result.score}/10`);
401
+ log(` ${icon} ${name} ${scoreStr} — ${colors.gray(result.reason)}`);
402
+ screen.setStatus(`screening ${screened}/${total}...`);
403
+ });
404
+ const matches = results.filter((r) => r.score >= discovery.matchThreshold);
405
+ log("");
406
+ if (matches.length === 0) {
407
+ log(colors.yellow("No strong matches found. Waiting for incoming conversations..."));
408
+ }
409
+ else {
410
+ log(colors.green(`Found ${matches.length} match${matches.length === 1 ? "" : "es"}. Starting conversations...`));
411
+ log("");
412
+ // Initiate conversations with matches
413
+ for (const match of matches) {
414
+ if (!match.profile.online) {
415
+ log(colors.gray(` ⊘ @${match.profile.githubUsername} is offline, skipping`));
416
+ continue;
417
+ }
418
+ log(colors.cyan(` 🤝 Reaching out to @${match.profile.githubUsername} (${match.score}/10: ${match.reason})`));
419
+ // Send start_conversation via WebSocket
420
+ sendToServer({
421
+ type: "start_conversation",
422
+ withUserId: match.profile.id,
423
+ });
424
+ // Respect cooldown between outreach
425
+ if (limits.cooldownSeconds > 0) {
426
+ await new Promise((r) => setTimeout(r, limits.cooldownSeconds * 1000));
427
+ }
428
+ }
429
+ }
430
+ screen.setStatus(`${matches.length} matches • waiting for conversations...`);
431
+ }
432
+ }
433
+ catch (err) {
434
+ log(colors.red(`Discovery failed: ${err instanceof Error ? err.message : err}`));
435
+ screen.setStatus("waiting for conversations...");
436
+ }
437
+ return done;
438
+ }
439
+ // ── Dashboard loop ────────────────────────────────────────
440
+ async function runDashboard() {
441
+ while (true) {
442
+ const config = getConfig();
443
+ const agents = getAgents();
444
+ const action = await new Promise((resolve) => {
445
+ const dashboard = new DashboardScreen(tui, {
446
+ loggedIn: !!config.token,
447
+ model: detectedModel,
448
+ agentCount: agents.length,
449
+ serverUrl: config.serverUrl,
450
+ });
451
+ dashboard.onAction = resolve;
452
+ setScreen(dashboard);
453
+ });
454
+ switch (action) {
455
+ case "start": {
456
+ const conf = getConfig();
457
+ if (!conf.token) {
458
+ await showMessage("Not logged in", ["Run login first."], "error");
459
+ break;
460
+ }
461
+ await runAgentSession();
462
+ break;
463
+ }
464
+ case "agents":
465
+ await runAgentList();
466
+ break;
467
+ case "quit":
468
+ shutdown();
469
+ return;
470
+ }
471
+ }
472
+ }
473
+ function shutdown() {
474
+ tui.stop();
475
+ terminal.clearScreen();
476
+ process.exit(0);
477
+ }
478
+ /**
479
+ * Main entry point for interactive TUI mode.
480
+ */
481
+ export async function runInteractive() {
482
+ terminal = new ProcessTerminal();
483
+ tui = new TUI(terminal, true);
484
+ tui.start();
485
+ // Handle Ctrl+C globally
486
+ tui.addInputListener((data) => {
487
+ if (data === "\x03") {
488
+ // Ctrl+C
489
+ shutdown();
490
+ return { consume: true };
491
+ }
492
+ return undefined;
493
+ });
494
+ await runSetup();
495
+ await runDashboard();
496
+ }
@@ -0,0 +1,49 @@
1
+ export type AgentLimits = {
2
+ maxConcurrentConversations: number;
3
+ maxMessagesPerConversation: number;
4
+ maxResponseLength: number;
5
+ cooldownSeconds: number;
6
+ maxConversationsPerSession: number;
7
+ autoStopIdleMinutes: number;
8
+ };
9
+ export declare const DEFAULT_LIMITS: AgentLimits;
10
+ export type DiscoverySettings = {
11
+ recontactAfterDays: number;
12
+ onlineOnly: boolean;
13
+ includeKeywords: string[];
14
+ excludeKeywords: string[];
15
+ excludeUsers: string[];
16
+ priorityUsers: string[];
17
+ matchThreshold: number;
18
+ maxScreenPerSession: number;
19
+ };
20
+ export declare const DEFAULT_DISCOVERY: DiscoverySettings;
21
+ /**
22
+ * A single agent profile with all its settings.
23
+ */
24
+ export type AgentProfile = {
25
+ id: string;
26
+ name: string;
27
+ instructions: string;
28
+ discovery: DiscoverySettings;
29
+ limits: AgentLimits;
30
+ modelPattern?: string;
31
+ };
32
+ export type Config = {
33
+ token?: string;
34
+ serverUrl: string;
35
+ userId?: string;
36
+ };
37
+ export declare function getConfigDir(): string;
38
+ export declare function getConfig(): Config;
39
+ export declare function saveConfig(config: Config): void;
40
+ /**
41
+ * Migrate legacy single-agent setup to new multi-agent format.
42
+ * Runs once: if ~/.swarmlancer/agent.md exists and no agents dir.
43
+ */
44
+ export declare function migrateLegacyAgent(): void;
45
+ export declare function getAgents(): AgentProfile[];
46
+ export declare function getAgent(id: string): AgentProfile | undefined;
47
+ export declare function saveAgent(agent: AgentProfile): void;
48
+ export declare function deleteAgent(id: string): void;
49
+ export declare function createAgent(name: string): AgentProfile;