swarmlancer 0.1.4 → 0.1.6

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/agent.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  type LogFn = (line: string) => void;
2
2
  export declare function startAgent(log?: LogFn): void;
3
+ export declare function sendToServer(msg: Record<string, unknown>): void;
3
4
  export declare function stopAgent(): void;
4
5
  export {};
package/dist/agent.js CHANGED
@@ -1,22 +1,48 @@
1
1
  import WebSocket from "ws";
2
- import { getConfig, getAgentFilePath } from "./config.js";
2
+ import { getConfig, getAgentFilePath, getLimits } from "./config.js";
3
3
  import { runInference } from "./inference.js";
4
4
  import { colors } from "./theme.js";
5
5
  let activeWs = null;
6
6
  let alive = false;
7
7
  let reconnectTimer = null;
8
+ let idleTimer = null;
9
+ // Session counters
10
+ let activeConversations = new Set();
11
+ let conversationMessageCounts = new Map();
12
+ let totalConversations = 0;
13
+ let lastConversationTime = 0;
14
+ function resetCounters() {
15
+ activeConversations.clear();
16
+ conversationMessageCounts.clear();
17
+ totalConversations = 0;
18
+ lastConversationTime = 0;
19
+ }
20
+ function resetIdleTimer(limits, log) {
21
+ if (idleTimer)
22
+ clearTimeout(idleTimer);
23
+ idleTimer = setTimeout(() => {
24
+ log(colors.yellow(`⏱ Auto-stopping: idle for ${limits.autoStopIdleMinutes} minutes`));
25
+ stopAgent();
26
+ }, limits.autoStopIdleMinutes * 60 * 1000);
27
+ }
8
28
  export function startAgent(log = console.log) {
9
29
  const config = getConfig();
30
+ const limits = getLimits();
10
31
  if (!config.token) {
11
32
  log(colors.red("Not logged in. Run: swarmlancer login"));
12
33
  return;
13
34
  }
35
+ resetCounters();
14
36
  const wsUrl = config.serverUrl.replace(/^http/, "ws") + "/ws";
15
37
  alive = true;
38
+ // Log limits
39
+ log(colors.gray(`Limits: ${limits.maxConcurrentConversations} concurrent, ${limits.maxMessagesPerConversation} msgs/convo, ${limits.cooldownSeconds}s cooldown, ${limits.maxConversationsPerSession} max/session, ${limits.autoStopIdleMinutes}m idle`));
16
40
  function connect() {
17
41
  log(colors.cyan(`Connecting to ${wsUrl}...`));
18
42
  const ws = new WebSocket(wsUrl);
19
43
  activeWs = ws;
44
+ // Start idle timer
45
+ resetIdleTimer(limits, log);
20
46
  ws.on("open", () => {
21
47
  ws.send(JSON.stringify({ type: "auth", token: config.token }));
22
48
  });
@@ -42,9 +68,69 @@ export function startAgent(log = console.log) {
42
68
  }
43
69
  case "inference_request": {
44
70
  const { requestId, conversationId, systemPrompt, messages } = msg;
45
- log(colors.yellow(`💬 Inference request for conversation ${conversationId.slice(0, 8)}...`));
71
+ // Reset idle timer on activity
72
+ resetIdleTimer(limits, log);
73
+ // Check concurrent conversation limit
74
+ if (!activeConversations.has(conversationId) &&
75
+ activeConversations.size >= limits.maxConcurrentConversations) {
76
+ log(colors.yellow(`⚠ Skipping: ${limits.maxConcurrentConversations} concurrent conversations active`));
77
+ ws.send(JSON.stringify({
78
+ type: "inference_error",
79
+ requestId,
80
+ error: "Agent is at max concurrent conversations. Try again later.",
81
+ }));
82
+ break;
83
+ }
84
+ // Check session total limit
85
+ if (!activeConversations.has(conversationId) &&
86
+ totalConversations >= limits.maxConversationsPerSession) {
87
+ log(colors.yellow(`⚠ Session limit reached (${limits.maxConversationsPerSession} conversations). Stopping.`));
88
+ stopAgent();
89
+ break;
90
+ }
91
+ // Check cooldown
92
+ if (!activeConversations.has(conversationId) &&
93
+ limits.cooldownSeconds > 0) {
94
+ const elapsed = (Date.now() - lastConversationTime) / 1000;
95
+ if (lastConversationTime > 0 && elapsed < limits.cooldownSeconds) {
96
+ const wait = Math.ceil(limits.cooldownSeconds - elapsed);
97
+ log(colors.yellow(`⚠ Cooldown: ${wait}s remaining`));
98
+ ws.send(JSON.stringify({
99
+ type: "inference_error",
100
+ requestId,
101
+ error: `Agent is in cooldown. Try again in ${wait} seconds.`,
102
+ }));
103
+ break;
104
+ }
105
+ }
106
+ // Track conversation
107
+ if (!activeConversations.has(conversationId)) {
108
+ activeConversations.add(conversationId);
109
+ conversationMessageCounts.set(conversationId, 0);
110
+ totalConversations++;
111
+ lastConversationTime = Date.now();
112
+ }
113
+ // Check per-conversation message limit
114
+ const msgCount = (conversationMessageCounts.get(conversationId) ?? 0) + 1;
115
+ conversationMessageCounts.set(conversationId, msgCount);
116
+ if (msgCount > limits.maxMessagesPerConversation) {
117
+ log(colors.yellow(`⚠ Conversation ${conversationId.slice(0, 8)} hit ${limits.maxMessagesPerConversation} message limit`));
118
+ activeConversations.delete(conversationId);
119
+ ws.send(JSON.stringify({
120
+ type: "inference_response",
121
+ requestId,
122
+ content: "I've reached my message limit for this conversation. It was great talking! Feel free to connect on GitHub if you'd like to continue.",
123
+ }));
124
+ break;
125
+ }
126
+ log(colors.yellow(`💬 [${conversationId.slice(0, 8)}] msg ${msgCount}/${limits.maxMessagesPerConversation} (${activeConversations.size}/${limits.maxConcurrentConversations} active, ${totalConversations}/${limits.maxConversationsPerSession} total)`));
46
127
  try {
47
- const response = await runInference(systemPrompt, messages);
128
+ let response = await runInference(systemPrompt, messages);
129
+ // Enforce response length limit
130
+ if (response.length > limits.maxResponseLength) {
131
+ response = response.slice(0, limits.maxResponseLength);
132
+ log(colors.gray(` (truncated to ${limits.maxResponseLength} chars)`));
133
+ }
48
134
  const preview = response.length > 80 ? response.slice(0, 80) + "..." : response;
49
135
  log(colors.green(`✓ Response: ${preview}`));
50
136
  ws.send(JSON.stringify({
@@ -67,6 +153,14 @@ export function startAgent(log = console.log) {
67
153
  case "conversation_started": {
68
154
  const { conversationId, withUser } = msg;
69
155
  log(colors.brightGreen(`🤝 Conversation started with ${withUser.displayName} (@${withUser.githubUsername})`));
156
+ resetIdleTimer(limits, log);
157
+ break;
158
+ }
159
+ case "conversation_ended": {
160
+ const endedId = msg.conversationId;
161
+ activeConversations.delete(endedId);
162
+ conversationMessageCounts.delete(endedId);
163
+ log(colors.gray(` Conversation ${endedId?.slice(0, 8)} ended (${activeConversations.size} active)`));
70
164
  break;
71
165
  }
72
166
  case "error":
@@ -86,10 +180,18 @@ export function startAgent(log = console.log) {
86
180
  }
87
181
  connect();
88
182
  }
183
+ export function sendToServer(msg) {
184
+ if (activeWs && activeWs.readyState === WebSocket.OPEN) {
185
+ activeWs.send(JSON.stringify(msg));
186
+ }
187
+ }
89
188
  export function stopAgent() {
90
189
  alive = false;
91
190
  if (reconnectTimer)
92
191
  clearTimeout(reconnectTimer);
192
+ if (idleTimer)
193
+ clearTimeout(idleTimer);
93
194
  activeWs?.close();
94
195
  activeWs = null;
196
+ resetCounters();
95
197
  }
package/dist/app.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { ProcessTerminal, TUI, Container } from "@mariozechner/pi-tui";
2
- import { getConfig, ensureAgentFile, getAgentFilePath, getAgentInstructions, saveAgentInstructions, } from "./config.js";
2
+ import { getConfig, ensureAgentFile, getAgentFilePath, getAgentInstructions, saveAgentInstructions, getLimits, saveLimits, getDiscovery, saveDiscovery, } from "./config.js";
3
3
  import { login } from "./login.js";
4
4
  import { initInference, getAvailableModels } from "./inference.js";
5
5
  import { getProfile, isProfileComplete, saveProfile, } from "./profile.js";
6
- import { startAgent, stopAgent } from "./agent.js";
6
+ import { startAgent, stopAgent, sendToServer } from "./agent.js";
7
+ import { colors } from "./theme.js";
7
8
  import { BannerComponent } from "./screens/banner.js";
8
9
  import { SetupWizardScreen } from "./screens/setup-wizard.js";
9
10
  import { DashboardScreen } from "./screens/dashboard.js";
@@ -12,7 +13,11 @@ import { ProfileEditorScreen } from "./screens/profile-editor.js";
12
13
  import { ProfileViewScreen } from "./screens/profile-view.js";
13
14
  import { AgentEditorScreen } from "./screens/agent-editor.js";
14
15
  import { AgentRunningScreen } from "./screens/agent-running.js";
16
+ import { SettingsScreen } from "./screens/settings.js";
17
+ import { DiscoverySettingsScreen } from "./screens/discovery-settings.js";
18
+ import { SessionGoalScreen } from "./screens/session-goal.js";
15
19
  import { MessageScreen } from "./screens/message.js";
20
+ import { screenProfiles } from "./screening.js";
16
21
  let terminal;
17
22
  let tui;
18
23
  let selectedModel;
@@ -211,22 +216,167 @@ function runProfileView() {
211
216
  });
212
217
  }
213
218
  /**
214
- * Start the agent and show the live log screen.
219
+ * Agent limits settings screen.
215
220
  */
216
- function runAgent() {
221
+ function runSettings() {
217
222
  return new Promise((resolve) => {
218
- const config = getConfig();
219
- const screen = new AgentRunningScreen(tui, selectedModel, config.serverUrl, getAgentFilePath());
223
+ const limits = getLimits();
224
+ const screen = new SettingsScreen(tui, limits);
225
+ screen.onSave = async (newLimits) => {
226
+ try {
227
+ saveLimits(newLimits);
228
+ await showMessage("Saved", ["Agent limits updated."], "success");
229
+ }
230
+ catch (err) {
231
+ await showMessage("Error", [err instanceof Error ? err.message : "Failed to save"], "error");
232
+ }
233
+ resolve();
234
+ };
235
+ screen.onCancel = () => resolve();
236
+ setScreen(screen);
237
+ });
238
+ }
239
+ /**
240
+ * Discovery settings screen.
241
+ */
242
+ function runDiscoverySettings() {
243
+ return new Promise((resolve) => {
244
+ const discovery = getDiscovery();
245
+ const screen = new DiscoverySettingsScreen(tui, discovery);
246
+ screen.onSave = async (newSettings) => {
247
+ try {
248
+ saveDiscovery(newSettings);
249
+ await showMessage("Saved", ["Discovery settings updated."], "success");
250
+ }
251
+ catch (err) {
252
+ await showMessage("Error", [err instanceof Error ? err.message : "Failed to save"], "error");
253
+ }
254
+ resolve();
255
+ };
256
+ screen.onCancel = () => resolve();
257
+ setScreen(screen);
258
+ });
259
+ }
260
+ /**
261
+ * Ask for an optional session goal before starting.
262
+ */
263
+ function askSessionGoal() {
264
+ return new Promise((resolve) => {
265
+ const screen = new SessionGoalScreen(tui);
266
+ screen.onSubmit = (goal) => resolve(goal.trim());
267
+ screen.onSkip = () => resolve("");
268
+ // Wire the editor's onSubmit
269
+ // The SessionGoalScreen's editor fires onSubmit on Enter
270
+ // We need to capture it from the screen level
271
+ setScreen(screen);
272
+ });
273
+ }
274
+ /**
275
+ * Fetch candidate profiles from the server's /api/discover endpoint.
276
+ */
277
+ async function fetchCandidates() {
278
+ const config = getConfig();
279
+ const discovery = getDiscovery();
280
+ const params = new URLSearchParams();
281
+ if (discovery.onlineOnly)
282
+ params.set("onlineOnly", "true");
283
+ params.set("notContactedInDays", String(discovery.recontactAfterDays));
284
+ if (discovery.includeKeywords.length > 0) {
285
+ params.set("keywords", discovery.includeKeywords.join(","));
286
+ }
287
+ if (discovery.excludeKeywords.length > 0) {
288
+ params.set("excludeKeywords", discovery.excludeKeywords.join(","));
289
+ }
290
+ if (discovery.excludeUsers.length > 0) {
291
+ params.set("excludeUsers", discovery.excludeUsers.join(","));
292
+ }
293
+ params.set("limit", String(discovery.maxScreenPerSession));
294
+ const res = await fetch(`${config.serverUrl}/api/discover?${params}`, {
295
+ headers: { Authorization: `Bearer ${config.token}` },
296
+ });
297
+ if (!res.ok)
298
+ throw new Error(`Discover API error: ${res.status}`);
299
+ const data = (await res.json());
300
+ return data.profiles;
301
+ }
302
+ /**
303
+ * Start the agent: session goal → discover → screen → connect → run.
304
+ */
305
+ async function runAgent() {
306
+ // 1. Ask for session goal
307
+ const sessionGoal = await askSessionGoal();
308
+ const config = getConfig();
309
+ const discovery = getDiscovery();
310
+ // 2. Show the running screen
311
+ const screen = new AgentRunningScreen(tui, selectedModel, config.serverUrl, getAgentFilePath(), sessionGoal);
312
+ const done = new Promise((resolve) => {
220
313
  screen.onStop = () => {
221
314
  stopAgent();
222
315
  resolve();
223
316
  };
224
- setScreen(screen);
225
- // Start the agent with the log callback
226
- startAgent((line) => {
227
- screen.addLog(line);
228
- });
229
317
  });
318
+ setScreen(screen);
319
+ const log = (line) => screen.addLog(line);
320
+ // 3. Start the WebSocket agent (for incoming requests)
321
+ startAgent(log);
322
+ // 4. Discovery + screening (outbound)
323
+ screen.setStatus("discovering candidates...");
324
+ try {
325
+ const candidates = await fetchCandidates();
326
+ if (candidates.length === 0) {
327
+ log(colors.gray("No candidates match your discovery filters."));
328
+ log(colors.gray("Waiting for incoming conversations..."));
329
+ screen.setStatus("waiting for conversations...");
330
+ }
331
+ else {
332
+ log(colors.cyan(`📡 Found ${candidates.length} candidates, screening...`));
333
+ screen.setStatus(`screening ${candidates.length} profiles...`);
334
+ const results = await screenProfiles(candidates, sessionGoal, discovery.matchThreshold, discovery.maxScreenPerSession, (screened, total, result) => {
335
+ const icon = result.score >= discovery.matchThreshold
336
+ ? colors.green("✓")
337
+ : colors.gray("⊘");
338
+ const name = `@${result.profile.githubUsername}`;
339
+ const scoreStr = result.score >= discovery.matchThreshold
340
+ ? colors.green(`${result.score}/10`)
341
+ : colors.gray(`${result.score}/10`);
342
+ log(` ${icon} ${name} ${scoreStr} — ${colors.gray(result.reason)}`);
343
+ screen.setStatus(`screening ${screened}/${total}...`);
344
+ });
345
+ const matches = results.filter((r) => r.score >= discovery.matchThreshold);
346
+ log("");
347
+ if (matches.length === 0) {
348
+ log(colors.yellow("No strong matches found. Waiting for incoming conversations..."));
349
+ }
350
+ else {
351
+ log(colors.green(`Found ${matches.length} match${matches.length === 1 ? "" : "es"}. Starting conversations...`));
352
+ log("");
353
+ // Initiate conversations with matches
354
+ for (const match of matches) {
355
+ if (!match.profile.online) {
356
+ log(colors.gray(` ⊘ @${match.profile.githubUsername} is offline, skipping`));
357
+ continue;
358
+ }
359
+ log(colors.cyan(` 🤝 Reaching out to @${match.profile.githubUsername} (${match.score}/10: ${match.reason})`));
360
+ // Send start_conversation via WebSocket
361
+ sendToServer({
362
+ type: "start_conversation",
363
+ withUserId: match.profile.id,
364
+ });
365
+ // Respect cooldown between outreach
366
+ const limits = getLimits();
367
+ if (limits.cooldownSeconds > 0) {
368
+ await new Promise((r) => setTimeout(r, limits.cooldownSeconds * 1000));
369
+ }
370
+ }
371
+ }
372
+ screen.setStatus(`${matches.length} matches • waiting for conversations...`);
373
+ }
374
+ }
375
+ catch (err) {
376
+ log(colors.red(`Discovery failed: ${err instanceof Error ? err.message : err}`));
377
+ screen.setStatus("waiting for conversations...");
378
+ }
379
+ return done;
230
380
  }
231
381
  /**
232
382
  * Main dashboard loop.
@@ -271,6 +421,12 @@ async function runDashboard() {
271
421
  case "profile-view":
272
422
  await runProfileView();
273
423
  break;
424
+ case "settings":
425
+ await runSettings();
426
+ break;
427
+ case "discovery":
428
+ await runDiscoverySettings();
429
+ break;
274
430
  case "quit":
275
431
  shutdown();
276
432
  return;
package/dist/config.d.ts CHANGED
@@ -1,12 +1,38 @@
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;
1
21
  export type Config = {
2
22
  token?: string;
3
23
  serverUrl: string;
4
24
  userId?: string;
25
+ limits?: Partial<AgentLimits>;
26
+ discovery?: Partial<DiscoverySettings>;
5
27
  };
6
28
  export declare function getConfigDir(): string;
7
29
  export declare function getConfig(): Config;
8
30
  export declare function saveConfig(config: Config): void;
9
31
  export declare function getAgentInstructions(): string;
10
32
  export declare function getAgentFilePath(): string;
33
+ export declare function getLimits(): AgentLimits;
34
+ export declare function saveLimits(limits: AgentLimits): void;
35
+ export declare function getDiscovery(): DiscoverySettings;
36
+ export declare function saveDiscovery(discovery: DiscoverySettings): void;
11
37
  export declare function saveAgentInstructions(content: string): void;
12
38
  export declare function ensureAgentFile(): void;
package/dist/config.js CHANGED
@@ -4,6 +4,24 @@ import { homedir } from "os";
4
4
  const CONFIG_DIR = join(homedir(), ".swarmlancer");
5
5
  const CONFIG_FILE = join(CONFIG_DIR, "config.json");
6
6
  const AGENT_FILE = join(CONFIG_DIR, "agent.md");
7
+ export const DEFAULT_LIMITS = {
8
+ maxConcurrentConversations: 2,
9
+ maxMessagesPerConversation: 10,
10
+ maxResponseLength: 2000,
11
+ cooldownSeconds: 30,
12
+ maxConversationsPerSession: 50,
13
+ autoStopIdleMinutes: 60,
14
+ };
15
+ export const DEFAULT_DISCOVERY = {
16
+ recontactAfterDays: 30,
17
+ onlineOnly: true,
18
+ includeKeywords: [],
19
+ excludeKeywords: [],
20
+ excludeUsers: [],
21
+ priorityUsers: [],
22
+ matchThreshold: 7,
23
+ maxScreenPerSession: 50,
24
+ };
7
25
  const DEFAULT_CONFIG = {
8
26
  serverUrl: "https://swarmlancer.com",
9
27
  };
@@ -35,6 +53,22 @@ export function getAgentInstructions() {
35
53
  export function getAgentFilePath() {
36
54
  return AGENT_FILE;
37
55
  }
56
+ export function getLimits() {
57
+ const config = getConfig();
58
+ return { ...DEFAULT_LIMITS, ...config.limits };
59
+ }
60
+ export function saveLimits(limits) {
61
+ const config = getConfig();
62
+ saveConfig({ ...config, limits });
63
+ }
64
+ export function getDiscovery() {
65
+ const config = getConfig();
66
+ return { ...DEFAULT_DISCOVERY, ...config.discovery };
67
+ }
68
+ export function saveDiscovery(discovery) {
69
+ const config = getConfig();
70
+ saveConfig({ ...config, discovery });
71
+ }
38
72
  export function saveAgentInstructions(content) {
39
73
  mkdirSync(CONFIG_DIR, { recursive: true });
40
74
  writeFileSync(AGENT_FILE, content);
@@ -0,0 +1,20 @@
1
+ export interface CandidateProfile {
2
+ id: string;
3
+ githubUsername: string;
4
+ displayName: string;
5
+ bio: string | null;
6
+ lookingFor: string | null;
7
+ skills: string | null;
8
+ projects: string | null;
9
+ online: boolean;
10
+ }
11
+ export interface ScreeningResult {
12
+ profile: CandidateProfile;
13
+ score: number;
14
+ reason: string;
15
+ }
16
+ /**
17
+ * Screen a batch of profiles using the LLM.
18
+ * Returns scored results sorted by score descending.
19
+ */
20
+ export declare function screenProfiles(profiles: CandidateProfile[], sessionGoal: string, threshold: number, maxToScreen: number, onProgress?: (screened: number, total: number, result: ScreeningResult) => void): Promise<ScreeningResult[]>;
@@ -0,0 +1,101 @@
1
+ import { runInference } from "./inference.js";
2
+ import { getAgentInstructions } from "./config.js";
3
+ const BATCH_SIZE = 10;
4
+ /**
5
+ * Screen a batch of profiles using the LLM.
6
+ * Returns scored results sorted by score descending.
7
+ */
8
+ export async function screenProfiles(profiles, sessionGoal, threshold, maxToScreen, onProgress) {
9
+ const agentMd = getAgentInstructions();
10
+ const toScreen = profiles.slice(0, maxToScreen);
11
+ const results = [];
12
+ // Process in batches
13
+ for (let i = 0; i < toScreen.length; i += BATCH_SIZE) {
14
+ const batch = toScreen.slice(i, i + BATCH_SIZE);
15
+ const batchResults = await screenBatch(batch, agentMd, sessionGoal);
16
+ for (const r of batchResults) {
17
+ results.push(r);
18
+ onProgress?.(results.length, toScreen.length, r);
19
+ }
20
+ }
21
+ // Sort by score descending
22
+ results.sort((a, b) => b.score - a.score);
23
+ return results;
24
+ }
25
+ async function screenBatch(profiles, agentMd, sessionGoal) {
26
+ const profileList = profiles
27
+ .map((p, i) => {
28
+ const parts = [`${i + 1}. ${p.displayName} (@${p.githubUsername})`];
29
+ if (p.bio)
30
+ parts.push(` Bio: ${p.bio}`);
31
+ if (p.skills)
32
+ parts.push(` Skills: ${p.skills}`);
33
+ if (p.projects)
34
+ parts.push(` Projects: ${p.projects}`);
35
+ if (p.lookingFor)
36
+ parts.push(` Looking for: ${p.lookingFor}`);
37
+ return parts.join("\n");
38
+ })
39
+ .join("\n\n");
40
+ const systemPrompt = `You are a networking assistant. Your job is to evaluate how relevant each person is to the user based on their profile and what the user is looking for.
41
+
42
+ ## About the user
43
+ ${agentMd}
44
+ ${sessionGoal ? `\n## What the user is looking for TODAY\n${sessionGoal}` : ""}
45
+
46
+ ## Instructions
47
+ For each person below, respond with EXACTLY this format (one line per person):
48
+ NUMBER|SCORE|REASON
49
+
50
+ Where:
51
+ - NUMBER is the person's number (1, 2, 3...)
52
+ - SCORE is 1-10 (1=no overlap, 10=perfect match)
53
+ - REASON is a brief explanation (one sentence)
54
+
55
+ Be honest. Most people won't be a match. Only score 7+ if there's genuine relevance.`;
56
+ const userMessage = `Rate these ${profiles.length} profiles:\n\n${profileList}`;
57
+ try {
58
+ const response = await runInference(systemPrompt, [
59
+ { role: "user", content: userMessage },
60
+ ]);
61
+ return parseScreeningResponse(response, profiles);
62
+ }
63
+ catch (err) {
64
+ // If LLM fails, return all with score 0
65
+ return profiles.map((p) => ({
66
+ profile: p,
67
+ score: 0,
68
+ reason: "screening failed",
69
+ }));
70
+ }
71
+ }
72
+ function parseScreeningResponse(response, profiles) {
73
+ const results = [];
74
+ const lines = response.split("\n").filter((l) => l.includes("|"));
75
+ for (const line of lines) {
76
+ const parts = line.split("|").map((s) => s.trim());
77
+ if (parts.length < 3)
78
+ continue;
79
+ const num = parseInt(parts[0]) - 1;
80
+ const score = Math.max(1, Math.min(10, parseInt(parts[1]) || 1));
81
+ const reason = parts.slice(2).join("|").trim();
82
+ if (num >= 0 && num < profiles.length) {
83
+ results.push({
84
+ profile: profiles[num],
85
+ score,
86
+ reason,
87
+ });
88
+ }
89
+ }
90
+ // Add any missing profiles with score 0
91
+ for (let i = 0; i < profiles.length; i++) {
92
+ if (!results.find((r) => r.profile.id === profiles[i].id)) {
93
+ results.push({
94
+ profile: profiles[i],
95
+ score: 0,
96
+ reason: "not evaluated",
97
+ });
98
+ }
99
+ }
100
+ return results;
101
+ }
@@ -8,8 +8,11 @@ export declare class AgentRunningScreen implements Component {
8
8
  private model;
9
9
  private serverUrl;
10
10
  private agentPath;
11
+ private sessionGoal;
12
+ private statusLine;
11
13
  onStop?: () => void;
12
- constructor(tui: TUI, model: Model<Api>, serverUrl: string, agentPath: string);
14
+ constructor(tui: TUI, model: Model<Api>, serverUrl: string, agentPath: string, sessionGoal?: string);
15
+ setStatus(status: string): void;
13
16
  addLog(line: string): void;
14
17
  handleInput(data: string): void;
15
18
  render(width: number): string[];
@@ -8,17 +8,20 @@ export class AgentRunningScreen {
8
8
  model;
9
9
  serverUrl;
10
10
  agentPath;
11
+ sessionGoal;
12
+ statusLine = "starting...";
11
13
  onStop;
12
- constructor(tui, model, serverUrl, agentPath) {
14
+ constructor(tui, model, serverUrl, agentPath, sessionGoal = "") {
13
15
  this.tui = tui;
14
16
  this.model = model;
15
17
  this.serverUrl = serverUrl;
16
18
  this.agentPath = agentPath;
17
- this.addLog(theme.accent("Agent starting..."));
18
- this.addLog(` Model: ${colors.green(model.provider + "/" + model.id)}`);
19
- this.addLog(` Server: ${colors.green(serverUrl)}`);
20
- this.addLog(` Agent: ${colors.green(agentPath)}`);
21
- this.addLog("");
19
+ this.sessionGoal = sessionGoal;
20
+ }
21
+ setStatus(status) {
22
+ this.statusLine = status;
23
+ this.cachedRender = undefined;
24
+ this.tui.requestRender();
22
25
  }
23
26
  addLog(line) {
24
27
  this.logLines.push(line);
@@ -43,6 +46,9 @@ export class AgentRunningScreen {
43
46
  lines.push(theme.border("─".repeat(width)));
44
47
  const modelInfo = `${this.model.provider}/${this.model.id}`;
45
48
  lines.push(truncateToWidth(` ${theme.title("⚡ AGENT ONLINE")} ${colors.gray("model:")} ${colors.green(modelInfo)}`, width));
49
+ if (this.sessionGoal) {
50
+ lines.push(truncateToWidth(` ${colors.gray("goal:")} ${colors.cyan(this.sessionGoal)}`, width));
51
+ }
46
52
  lines.push(theme.border("─".repeat(width)));
47
53
  lines.push("");
48
54
  // Log
@@ -52,7 +58,7 @@ export class AgentRunningScreen {
52
58
  // Footer
53
59
  lines.push("");
54
60
  lines.push(theme.border("─".repeat(width)));
55
- lines.push(truncateToWidth(colors.gray(" esc/q stop agent • waiting for conversations..."), width));
61
+ lines.push(truncateToWidth(` ${colors.gray(this.statusLine)} ${colors.gray(" esc/q stop")}`, width));
56
62
  this.cachedRender = lines;
57
63
  return lines;
58
64
  }
@@ -1,7 +1,7 @@
1
1
  import { type Component } from "@mariozechner/pi-tui";
2
2
  import type { TUI } from "@mariozechner/pi-tui";
3
3
  import { type StatusInfo } from "./status-panel.js";
4
- export type MenuAction = "start" | "profile-edit" | "agent-edit" | "model-pick" | "profile-view" | "quit";
4
+ export type MenuAction = "start" | "profile-edit" | "agent-edit" | "settings" | "discovery" | "model-pick" | "profile-view" | "quit";
5
5
  export declare class DashboardScreen implements Component {
6
6
  private container;
7
7
  private statusPanel;
@@ -3,9 +3,11 @@ import { colors, theme } from "../theme.js";
3
3
  import { StatusPanel } from "./status-panel.js";
4
4
  import { BannerComponent } from "./banner.js";
5
5
  const MENU_ITEMS = [
6
- { value: "start", label: "Start agent", description: "Connect and start conversations" },
6
+ { value: "start", label: "Start agent", description: "Set goal discover → screen → connect" },
7
7
  { value: "profile-edit", label: "Edit profile", description: "Update your public profile" },
8
8
  { value: "agent-edit", label: "Edit agent instructions", description: "Configure how your agent behaves" },
9
+ { value: "discovery", label: "Discovery settings", description: "Keywords, match threshold, filters" },
10
+ { value: "settings", label: "Agent limits", description: "Max conversations, cooldown, auto-stop" },
9
11
  { value: "model-pick", label: "Change model", description: "Pick a different LLM model" },
10
12
  { value: "profile-view", label: "View profile", description: "See your current public profile" },
11
13
  { value: "quit", label: "Quit", description: "Exit swarmlancer" },
@@ -0,0 +1,17 @@
1
+ import { type Component } from "@mariozechner/pi-tui";
2
+ import type { TUI } from "@mariozechner/pi-tui";
3
+ import type { DiscoverySettings } from "../config.js";
4
+ export declare class DiscoverySettingsScreen implements Component {
5
+ private tui;
6
+ private settings;
7
+ private selectedIndex;
8
+ private editingKeywords;
9
+ private keywordInput;
10
+ private cachedRender?;
11
+ onSave?: (settings: DiscoverySettings) => void;
12
+ onCancel?: () => void;
13
+ constructor(tui: TUI, settings: DiscoverySettings);
14
+ handleInput(data: string): void;
15
+ render(width: number): string[];
16
+ invalidate(): void;
17
+ }
@@ -0,0 +1,189 @@
1
+ import { Input, matchesKey, Key, truncateToWidth, } from "@mariozechner/pi-tui";
2
+ import { colors, theme } from "../theme.js";
3
+ const FIELDS = [
4
+ {
5
+ key: "matchThreshold",
6
+ label: "Match threshold",
7
+ description: "1-10 — how picky your agent is (higher = fewer, better matches)",
8
+ type: "number", min: 1, max: 10, step: 1, suffix: "/10",
9
+ },
10
+ {
11
+ key: "maxScreenPerSession",
12
+ label: "Max profiles to screen",
13
+ description: "Don't burn tokens evaluating more than this per session",
14
+ type: "number", min: 5, max: 200, step: 5, suffix: "",
15
+ },
16
+ {
17
+ key: "recontactAfterDays",
18
+ label: "Re-contact after days",
19
+ description: "0 = only new people, 30 = re-engage after a month",
20
+ type: "number", min: 0, max: 365, step: 5, suffix: "d",
21
+ },
22
+ {
23
+ key: "onlineOnly",
24
+ label: "Online only",
25
+ description: "Only discover users with agents currently running",
26
+ type: "boolean",
27
+ },
28
+ {
29
+ key: "includeKeywords",
30
+ label: "Include keywords",
31
+ description: "Only profiles mentioning these (comma-separated, empty = all)",
32
+ type: "keywords",
33
+ },
34
+ {
35
+ key: "excludeKeywords",
36
+ label: "Exclude keywords",
37
+ description: "Skip profiles mentioning these (comma-separated)",
38
+ type: "keywords",
39
+ },
40
+ ];
41
+ export class DiscoverySettingsScreen {
42
+ tui;
43
+ settings;
44
+ selectedIndex = 0;
45
+ editingKeywords = false;
46
+ keywordInput;
47
+ cachedRender;
48
+ onSave;
49
+ onCancel;
50
+ constructor(tui, settings) {
51
+ this.tui = tui;
52
+ this.settings = { ...settings };
53
+ this.keywordInput = new Input();
54
+ }
55
+ handleInput(data) {
56
+ // Keyword editing mode
57
+ if (this.editingKeywords) {
58
+ if (matchesKey(data, Key.enter)) {
59
+ const field = FIELDS[this.selectedIndex];
60
+ const val = this.keywordInput.getValue().split(",").map((s) => s.trim()).filter(Boolean);
61
+ this.settings[field.key] = val;
62
+ this.editingKeywords = false;
63
+ this.cachedRender = undefined;
64
+ this.tui.requestRender();
65
+ return;
66
+ }
67
+ if (matchesKey(data, Key.escape)) {
68
+ this.editingKeywords = false;
69
+ this.cachedRender = undefined;
70
+ this.tui.requestRender();
71
+ return;
72
+ }
73
+ this.keywordInput.handleInput(data);
74
+ this.cachedRender = undefined;
75
+ this.tui.requestRender();
76
+ return;
77
+ }
78
+ if (matchesKey(data, Key.escape)) {
79
+ this.onCancel?.();
80
+ return;
81
+ }
82
+ if (matchesKey(data, Key.ctrl("s"))) {
83
+ this.onSave?.({ ...this.settings });
84
+ return;
85
+ }
86
+ // Navigate
87
+ if (matchesKey(data, Key.up)) {
88
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
89
+ this.cachedRender = undefined;
90
+ this.tui.requestRender();
91
+ return;
92
+ }
93
+ if (matchesKey(data, Key.down)) {
94
+ this.selectedIndex = Math.min(FIELDS.length - 1, this.selectedIndex + 1);
95
+ this.cachedRender = undefined;
96
+ this.tui.requestRender();
97
+ return;
98
+ }
99
+ const field = FIELDS[this.selectedIndex];
100
+ // Enter to edit keywords or save
101
+ if (matchesKey(data, Key.enter)) {
102
+ if (field.type === "keywords") {
103
+ this.editingKeywords = true;
104
+ this.keywordInput.setValue(this.settings[field.key].join(", "));
105
+ this.cachedRender = undefined;
106
+ this.tui.requestRender();
107
+ return;
108
+ }
109
+ this.onSave?.({ ...this.settings });
110
+ return;
111
+ }
112
+ // Adjust values with left/right
113
+ if (matchesKey(data, Key.right) || matchesKey(data, Key.left)) {
114
+ const dir = matchesKey(data, Key.right) ? 1 : -1;
115
+ if (field.type === "boolean") {
116
+ this.settings[field.key] = !this.settings[field.key];
117
+ }
118
+ else if (field.type === "number") {
119
+ const step = field.step ?? 1;
120
+ const min = field.min ?? 0;
121
+ const max = field.max ?? 999;
122
+ const cur = this.settings[field.key];
123
+ this.settings[field.key] = Math.max(min, Math.min(max, cur + dir * step));
124
+ }
125
+ this.cachedRender = undefined;
126
+ this.tui.requestRender();
127
+ }
128
+ }
129
+ render(width) {
130
+ if (this.cachedRender)
131
+ return this.cachedRender;
132
+ const lines = [];
133
+ lines.push("");
134
+ lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
135
+ lines.push(truncateToWidth(theme.title(" Discovery Settings"), width));
136
+ lines.push(truncateToWidth(colors.gray(" Control how your agent finds people to talk to."), width));
137
+ lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
138
+ lines.push("");
139
+ for (let i = 0; i < FIELDS.length; i++) {
140
+ const f = FIELDS[i];
141
+ const isActive = i === this.selectedIndex;
142
+ const prefix = isActive ? theme.accent("▸ ") : " ";
143
+ const label = isActive ? theme.accent(f.label) : f.label;
144
+ if (f.type === "number") {
145
+ const value = this.settings[f.key];
146
+ const pct = ((value - (f.min ?? 0)) / ((f.max ?? 100) - (f.min ?? 0)));
147
+ const barW = 16;
148
+ const filled = Math.round(pct * barW);
149
+ const bar = colors.cyan("█".repeat(filled)) + colors.gray("░".repeat(barW - filled));
150
+ const valStr = `${value}${f.suffix ?? ""}`;
151
+ lines.push(truncateToWidth(`${prefix}${label}`, width));
152
+ lines.push(truncateToWidth(` ${colors.gray("◀")} ${bar} ${colors.gray("▶")} ${isActive ? colors.brightCyan(valStr) : colors.gray(valStr)}`, width));
153
+ }
154
+ else if (f.type === "boolean") {
155
+ const value = this.settings[f.key];
156
+ const valStr = value ? colors.green("ON") : colors.red("OFF");
157
+ lines.push(truncateToWidth(`${prefix}${label} ${colors.gray("◀")} ${valStr} ${colors.gray("▶")}`, width));
158
+ }
159
+ else if (f.type === "keywords") {
160
+ const value = this.settings[f.key];
161
+ const display = value.length > 0 ? value.join(", ") : colors.gray("(none)");
162
+ lines.push(truncateToWidth(`${prefix}${label}`, width));
163
+ if (this.editingKeywords && isActive) {
164
+ const inputLines = this.keywordInput.render(width - 4);
165
+ for (const il of inputLines) {
166
+ lines.push(truncateToWidth(` ${il}`, width));
167
+ }
168
+ }
169
+ else {
170
+ lines.push(truncateToWidth(` ${display}`, width));
171
+ }
172
+ }
173
+ if (isActive) {
174
+ lines.push(truncateToWidth(` ${colors.gray(f.description)}`, width));
175
+ }
176
+ lines.push("");
177
+ }
178
+ lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
179
+ const help = this.editingKeywords
180
+ ? colors.gray(" type keywords, comma-separated • enter confirm • esc cancel")
181
+ : colors.gray(" ↑↓ select • ←→ adjust • enter edit/save • ctrl+s save • esc back");
182
+ lines.push(truncateToWidth(help, width));
183
+ this.cachedRender = lines;
184
+ return lines;
185
+ }
186
+ invalidate() {
187
+ this.cachedRender = undefined;
188
+ }
189
+ }
@@ -0,0 +1,13 @@
1
+ import { type Component } from "@mariozechner/pi-tui";
2
+ import type { TUI } from "@mariozechner/pi-tui";
3
+ export declare class SessionGoalScreen implements Component {
4
+ private tui;
5
+ private editor;
6
+ private cachedRender?;
7
+ onSubmit?: (goal: string) => void;
8
+ onSkip?: () => void;
9
+ constructor(tui: TUI);
10
+ handleInput(data: string): void;
11
+ render(width: number): string[];
12
+ invalidate(): void;
13
+ }
@@ -0,0 +1,61 @@
1
+ import { Editor, matchesKey, Key, truncateToWidth, } from "@mariozechner/pi-tui";
2
+ import { colors, theme } from "../theme.js";
3
+ const EDITOR_THEME = {
4
+ borderColor: (s) => colors.cyan(s),
5
+ selectList: {
6
+ selectedPrefix: (t) => colors.cyan(t),
7
+ selectedText: (t) => colors.cyan(t),
8
+ description: (t) => colors.gray(t),
9
+ scrollInfo: (t) => colors.gray(t),
10
+ noMatch: (t) => colors.yellow(t),
11
+ },
12
+ };
13
+ export class SessionGoalScreen {
14
+ tui;
15
+ editor;
16
+ cachedRender;
17
+ onSubmit;
18
+ onSkip;
19
+ constructor(tui) {
20
+ this.tui = tui;
21
+ this.editor = new Editor(tui, EDITOR_THEME, { paddingX: 1 });
22
+ this.editor.disableSubmit = false; // Enter submits
23
+ this.editor.onSubmit = (text) => {
24
+ this.onSubmit?.(text);
25
+ };
26
+ }
27
+ handleInput(data) {
28
+ if (matchesKey(data, Key.escape)) {
29
+ this.onSkip?.();
30
+ return;
31
+ }
32
+ this.editor.handleInput(data);
33
+ this.cachedRender = undefined;
34
+ this.tui.requestRender();
35
+ }
36
+ render(width) {
37
+ if (this.cachedRender)
38
+ return this.cachedRender;
39
+ const lines = [];
40
+ lines.push("");
41
+ lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
42
+ lines.push(truncateToWidth(theme.title(" What are you looking for today?"), width));
43
+ lines.push(truncateToWidth(colors.gray(" Optional — sharpens who your agent reaches out to. Leave empty to use defaults from agent.md."), width));
44
+ lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
45
+ lines.push("");
46
+ lines.push(truncateToWidth(colors.gray(' Examples: "Rust developers for CLI collaboration" or "Anyone doing AI agents"'), width));
47
+ lines.push("");
48
+ for (const line of this.editor.render(width)) {
49
+ lines.push(line);
50
+ }
51
+ lines.push("");
52
+ lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
53
+ lines.push(truncateToWidth(colors.gray(" enter submit • esc skip (use defaults)"), width));
54
+ this.cachedRender = lines;
55
+ return lines;
56
+ }
57
+ invalidate() {
58
+ this.cachedRender = undefined;
59
+ this.editor.invalidate();
60
+ }
61
+ }
@@ -0,0 +1,15 @@
1
+ import { type Component } from "@mariozechner/pi-tui";
2
+ import type { TUI } from "@mariozechner/pi-tui";
3
+ import type { AgentLimits } from "../config.js";
4
+ export declare class SettingsScreen implements Component {
5
+ private tui;
6
+ private limits;
7
+ private selectedIndex;
8
+ private cachedRender?;
9
+ onSave?: (limits: AgentLimits) => void;
10
+ onCancel?: () => void;
11
+ constructor(tui: TUI, limits: AgentLimits);
12
+ handleInput(data: string): void;
13
+ render(width: number): string[];
14
+ invalidate(): void;
15
+ }
@@ -0,0 +1,126 @@
1
+ import { matchesKey, Key, truncateToWidth, } from "@mariozechner/pi-tui";
2
+ import { colors, theme } from "../theme.js";
3
+ const SETTINGS = [
4
+ {
5
+ key: "maxConcurrentConversations",
6
+ label: "Max concurrent conversations",
7
+ description: "How many conversations at once",
8
+ min: 1, max: 10, step: 1, suffix: "",
9
+ },
10
+ {
11
+ key: "maxMessagesPerConversation",
12
+ label: "Max messages per conversation",
13
+ description: "Stop a conversation after this many messages",
14
+ min: 2, max: 50, step: 2, suffix: " msgs",
15
+ },
16
+ {
17
+ key: "maxResponseLength",
18
+ label: "Max response length",
19
+ description: "Truncate agent responses beyond this",
20
+ min: 200, max: 10000, step: 200, suffix: " chars",
21
+ },
22
+ {
23
+ key: "cooldownSeconds",
24
+ label: "Cooldown between conversations",
25
+ description: "Wait before starting the next one",
26
+ min: 0, max: 300, step: 10, suffix: "s",
27
+ },
28
+ {
29
+ key: "maxConversationsPerSession",
30
+ label: "Max conversations per session",
31
+ description: "Agent stops after this many total",
32
+ min: 1, max: 500, step: 10, suffix: "",
33
+ },
34
+ {
35
+ key: "autoStopIdleMinutes",
36
+ label: "Auto-stop after idle",
37
+ description: "Disconnect if no conversations for this long",
38
+ min: 5, max: 1440, step: 5, suffix: " min",
39
+ },
40
+ ];
41
+ export class SettingsScreen {
42
+ tui;
43
+ limits;
44
+ selectedIndex = 0;
45
+ cachedRender;
46
+ onSave;
47
+ onCancel;
48
+ constructor(tui, limits) {
49
+ this.tui = tui;
50
+ this.limits = { ...limits };
51
+ }
52
+ handleInput(data) {
53
+ if (matchesKey(data, Key.escape)) {
54
+ this.onCancel?.();
55
+ return;
56
+ }
57
+ if (matchesKey(data, Key.ctrl("s")) || matchesKey(data, Key.enter)) {
58
+ this.onSave?.({ ...this.limits });
59
+ return;
60
+ }
61
+ // Navigate settings
62
+ if (matchesKey(data, Key.up)) {
63
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
64
+ this.cachedRender = undefined;
65
+ this.tui.requestRender();
66
+ return;
67
+ }
68
+ if (matchesKey(data, Key.down)) {
69
+ this.selectedIndex = Math.min(SETTINGS.length - 1, this.selectedIndex + 1);
70
+ this.cachedRender = undefined;
71
+ this.tui.requestRender();
72
+ return;
73
+ }
74
+ // Adjust value
75
+ const setting = SETTINGS[this.selectedIndex];
76
+ if (matchesKey(data, Key.right)) {
77
+ this.limits[setting.key] = Math.min(setting.max, this.limits[setting.key] + setting.step);
78
+ this.cachedRender = undefined;
79
+ this.tui.requestRender();
80
+ return;
81
+ }
82
+ if (matchesKey(data, Key.left)) {
83
+ this.limits[setting.key] = Math.max(setting.min, this.limits[setting.key] - setting.step);
84
+ this.cachedRender = undefined;
85
+ this.tui.requestRender();
86
+ return;
87
+ }
88
+ }
89
+ render(width) {
90
+ if (this.cachedRender)
91
+ return this.cachedRender;
92
+ const lines = [];
93
+ lines.push("");
94
+ lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
95
+ lines.push(truncateToWidth(theme.title(" Agent Limits"), width));
96
+ lines.push(truncateToWidth(colors.gray(" Control how your agent behaves in conversations."), width));
97
+ lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
98
+ lines.push("");
99
+ for (let i = 0; i < SETTINGS.length; i++) {
100
+ const s = SETTINGS[i];
101
+ const isActive = i === this.selectedIndex;
102
+ const value = this.limits[s.key];
103
+ const prefix = isActive ? theme.accent("▸ ") : " ";
104
+ const label = isActive ? theme.accent(s.label) : s.label;
105
+ const valueStr = `${value}${s.suffix}`;
106
+ // Slider bar
107
+ const pct = (value - s.min) / (s.max - s.min);
108
+ const barWidth = 20;
109
+ const filled = Math.round(pct * barWidth);
110
+ const bar = colors.cyan("█".repeat(filled)) + colors.gray("░".repeat(barWidth - filled));
111
+ lines.push(truncateToWidth(`${prefix}${label}`, width));
112
+ lines.push(truncateToWidth(` ${colors.gray("◀")} ${bar} ${colors.gray("▶")} ${isActive ? colors.brightCyan(valueStr) : colors.gray(valueStr)}`, width));
113
+ if (isActive) {
114
+ lines.push(truncateToWidth(` ${colors.gray(s.description)}`, width));
115
+ }
116
+ lines.push("");
117
+ }
118
+ lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
119
+ lines.push(truncateToWidth(colors.gray(" ↑↓ select • ←→ adjust • enter save • esc cancel"), width));
120
+ this.cachedRender = lines;
121
+ return lines;
122
+ }
123
+ invalidate() {
124
+ this.cachedRender = undefined;
125
+ }
126
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swarmlancer",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Swarmlancer CLI — your agent, your rules. Connect your AI agent to a network of other agents.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",