swarmlancer 0.1.4 → 0.1.5
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 +1 -0
- package/dist/agent.js +105 -3
- package/dist/app.js +167 -11
- package/dist/config.d.ts +26 -0
- package/dist/config.js +34 -0
- package/dist/screening.d.ts +20 -0
- package/dist/screening.js +101 -0
- package/dist/screens/agent-running.d.ts +4 -1
- package/dist/screens/agent-running.js +13 -7
- package/dist/screens/dashboard.d.ts +1 -1
- package/dist/screens/dashboard.js +3 -1
- package/dist/screens/discovery-settings.d.ts +17 -0
- package/dist/screens/discovery-settings.js +189 -0
- package/dist/screens/session-goal.d.ts +13 -0
- package/dist/screens/session-goal.js +61 -0
- package/dist/screens/settings.d.ts +15 -0
- package/dist/screens/settings.js +126 -0
- package/package.json +1 -1
package/dist/agent.d.ts
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
219
|
+
* Agent limits settings screen.
|
|
215
220
|
*/
|
|
216
|
-
function
|
|
221
|
+
function runSettings() {
|
|
217
222
|
return new Promise((resolve) => {
|
|
218
|
-
const
|
|
219
|
-
const screen = new
|
|
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.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
this.
|
|
21
|
-
this.
|
|
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
|
|
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: "
|
|
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
|
+
}
|