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/config.js ADDED
@@ -0,0 +1,175 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+ import { randomUUID } from "crypto";
5
+ const CONFIG_DIR = join(homedir(), ".swarmlancer");
6
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
7
+ const AGENTS_DIR = join(CONFIG_DIR, "agents");
8
+ // Legacy path — used only for migration
9
+ const LEGACY_AGENT_FILE = join(CONFIG_DIR, "agent.md");
10
+ export const DEFAULT_LIMITS = {
11
+ maxConcurrentConversations: 2,
12
+ maxMessagesPerConversation: 10,
13
+ maxResponseLength: 2000,
14
+ cooldownSeconds: 30,
15
+ maxConversationsPerSession: 50,
16
+ autoStopIdleMinutes: 60,
17
+ };
18
+ export const DEFAULT_DISCOVERY = {
19
+ recontactAfterDays: 30,
20
+ onlineOnly: true,
21
+ includeKeywords: [],
22
+ excludeKeywords: [],
23
+ excludeUsers: [],
24
+ priorityUsers: [],
25
+ matchThreshold: 7,
26
+ maxScreenPerSession: 50,
27
+ };
28
+ const DEFAULT_CONFIG = {
29
+ serverUrl: "https://swarmlancer.com",
30
+ };
31
+ export function getConfigDir() {
32
+ return CONFIG_DIR;
33
+ }
34
+ export function getConfig() {
35
+ try {
36
+ if (existsSync(CONFIG_FILE)) {
37
+ const raw = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
38
+ // Only take the global fields, ignore legacy limits/discovery
39
+ return {
40
+ ...DEFAULT_CONFIG,
41
+ token: raw.token,
42
+ serverUrl: raw.serverUrl ?? DEFAULT_CONFIG.serverUrl,
43
+ userId: raw.userId,
44
+ };
45
+ }
46
+ }
47
+ catch { }
48
+ return { ...DEFAULT_CONFIG };
49
+ }
50
+ export function saveConfig(config) {
51
+ mkdirSync(CONFIG_DIR, { recursive: true });
52
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
53
+ }
54
+ // ── Agent CRUD ────────────────────────────────────────────
55
+ function ensureAgentsDir() {
56
+ mkdirSync(AGENTS_DIR, { recursive: true });
57
+ }
58
+ function agentFilePath(id) {
59
+ return join(AGENTS_DIR, `${id}.json`);
60
+ }
61
+ /**
62
+ * Migrate legacy single-agent setup to new multi-agent format.
63
+ * Runs once: if ~/.swarmlancer/agent.md exists and no agents dir.
64
+ */
65
+ export function migrateLegacyAgent() {
66
+ ensureAgentsDir();
67
+ // Already have agents? Skip.
68
+ const existing = readdirSync(AGENTS_DIR).filter((f) => f.endsWith(".json"));
69
+ if (existing.length > 0)
70
+ return;
71
+ // Check for legacy agent.md
72
+ if (!existsSync(LEGACY_AGENT_FILE))
73
+ return;
74
+ const instructions = readFileSync(LEGACY_AGENT_FILE, "utf-8").trim();
75
+ if (!instructions)
76
+ return;
77
+ // Read legacy limits/discovery from config.json if present
78
+ let legacyLimits = {};
79
+ let legacyDiscovery = {};
80
+ try {
81
+ if (existsSync(CONFIG_FILE)) {
82
+ const raw = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
83
+ if (raw.limits)
84
+ legacyLimits = raw.limits;
85
+ if (raw.discovery)
86
+ legacyDiscovery = raw.discovery;
87
+ }
88
+ }
89
+ catch { }
90
+ const agent = {
91
+ id: randomUUID(),
92
+ name: "My Agent",
93
+ instructions,
94
+ discovery: { ...DEFAULT_DISCOVERY, ...legacyDiscovery },
95
+ limits: { ...DEFAULT_LIMITS, ...legacyLimits },
96
+ };
97
+ saveAgent(agent);
98
+ }
99
+ export function getAgents() {
100
+ ensureAgentsDir();
101
+ const files = readdirSync(AGENTS_DIR).filter((f) => f.endsWith(".json"));
102
+ const agents = [];
103
+ for (const file of files) {
104
+ try {
105
+ const raw = JSON.parse(readFileSync(join(AGENTS_DIR, file), "utf-8"));
106
+ agents.push({
107
+ id: raw.id,
108
+ name: raw.name || "Unnamed",
109
+ instructions: raw.instructions || "",
110
+ discovery: { ...DEFAULT_DISCOVERY, ...raw.discovery },
111
+ limits: { ...DEFAULT_LIMITS, ...raw.limits },
112
+ modelPattern: raw.modelPattern,
113
+ });
114
+ }
115
+ catch { }
116
+ }
117
+ // Sort by name
118
+ agents.sort((a, b) => a.name.localeCompare(b.name));
119
+ return agents;
120
+ }
121
+ export function getAgent(id) {
122
+ const filePath = agentFilePath(id);
123
+ if (!existsSync(filePath))
124
+ return undefined;
125
+ try {
126
+ const raw = JSON.parse(readFileSync(filePath, "utf-8"));
127
+ return {
128
+ id: raw.id,
129
+ name: raw.name || "Unnamed",
130
+ instructions: raw.instructions || "",
131
+ discovery: { ...DEFAULT_DISCOVERY, ...raw.discovery },
132
+ limits: { ...DEFAULT_LIMITS, ...raw.limits },
133
+ modelPattern: raw.modelPattern,
134
+ };
135
+ }
136
+ catch {
137
+ return undefined;
138
+ }
139
+ }
140
+ export function saveAgent(agent) {
141
+ ensureAgentsDir();
142
+ writeFileSync(agentFilePath(agent.id), JSON.stringify(agent, null, 2));
143
+ }
144
+ export function deleteAgent(id) {
145
+ const filePath = agentFilePath(id);
146
+ if (existsSync(filePath)) {
147
+ unlinkSync(filePath);
148
+ }
149
+ }
150
+ export function createAgent(name) {
151
+ const agent = {
152
+ id: randomUUID(),
153
+ name,
154
+ instructions: DEFAULT_AGENT_INSTRUCTIONS,
155
+ discovery: { ...DEFAULT_DISCOVERY },
156
+ limits: { ...DEFAULT_LIMITS },
157
+ };
158
+ saveAgent(agent);
159
+ return agent;
160
+ }
161
+ const DEFAULT_AGENT_INSTRUCTIONS = `# My Agent Instructions
162
+
163
+ ## About me
164
+ (Describe yourself — what you do, what you're building)
165
+
166
+ ## What I'm looking for
167
+ (What kind of people or collaborations interest you?)
168
+
169
+ ## How my agent should behave
170
+ - Be direct and genuine
171
+ - If there's no real connection, wrap up quickly
172
+ - If there's potential, suggest connecting on GitHub
173
+ - Keep responses concise
174
+ - Max 8-10 messages per conversation
175
+ `;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+ import { runInteractive } from "./app.js";
3
+ import { login } from "./login.js";
4
+ import { getConfig, getAgents, migrateLegacyAgent } from "./config.js";
5
+ import { initInference, getAvailableModels, setAgentInstructions } from "./inference.js";
6
+ import { startAgent } from "./agent.js";
7
+ async function main() {
8
+ const args = process.argv.slice(2);
9
+ const command = args[0];
10
+ // No args → full TUI interactive mode
11
+ if (!command) {
12
+ await runInteractive();
13
+ return;
14
+ }
15
+ // Parse flags for direct commands
16
+ const flags = {};
17
+ for (let i = 1; i < args.length; i++) {
18
+ if (args[i].startsWith("--") && args[i + 1]) {
19
+ flags[args[i].slice(2)] = args[i + 1];
20
+ i++;
21
+ }
22
+ }
23
+ // Direct commands for scripting / CI
24
+ switch (command) {
25
+ case "login":
26
+ await login();
27
+ break;
28
+ case "agents": {
29
+ migrateLegacyAgent();
30
+ const agents = getAgents();
31
+ if (agents.length === 0) {
32
+ console.log("No agents configured. Run `swarmlancer` to create one.");
33
+ }
34
+ else {
35
+ console.log(`${agents.length} agent${agents.length === 1 ? "" : "s"}:\n`);
36
+ for (const a of agents) {
37
+ const model = a.modelPattern || "(auto)";
38
+ const instr = a.instructions.length > 0
39
+ ? `${a.instructions.split("\n").length} lines`
40
+ : "empty";
41
+ console.log(` ${a.name}`);
42
+ console.log(` Model: ${model}`);
43
+ console.log(` Instructions: ${instr}`);
44
+ console.log(` Discovery: threshold ${a.discovery.matchThreshold}/10`);
45
+ console.log(` Limits: ${a.limits.maxConcurrentConversations} concurrent, ${a.limits.maxMessagesPerConversation} msgs`);
46
+ console.log("");
47
+ }
48
+ }
49
+ break;
50
+ }
51
+ case "models": {
52
+ console.log("Available models (from pi credentials):\n");
53
+ try {
54
+ await initInference();
55
+ const models = await getAvailableModels();
56
+ for (const m of models)
57
+ console.log(` ${m.provider}/${m.id}`);
58
+ console.log(`\nUse: swarmlancer start --model <pattern>`);
59
+ }
60
+ catch (err) {
61
+ console.error(err instanceof Error ? err.message : err);
62
+ }
63
+ break;
64
+ }
65
+ case "start": {
66
+ migrateLegacyAgent();
67
+ const config = getConfig();
68
+ if (!config.token) {
69
+ console.error("Not logged in. Run: swarmlancer login");
70
+ process.exit(1);
71
+ }
72
+ const agents = getAgents();
73
+ if (agents.length === 0) {
74
+ console.error("No agents configured. Run `swarmlancer` to create one.");
75
+ process.exit(1);
76
+ }
77
+ // Pick agent by name or use first
78
+ let agent = agents[0];
79
+ if (flags.agent) {
80
+ const match = agents.find((a) => a.name.toLowerCase().includes(flags.agent.toLowerCase()));
81
+ if (!match) {
82
+ console.error(`Agent "${flags.agent}" not found. Available:`);
83
+ for (const a of agents)
84
+ console.error(` ${a.name}`);
85
+ process.exit(1);
86
+ }
87
+ agent = match;
88
+ }
89
+ // Init model (agent's model pattern takes priority, then --model flag)
90
+ const modelPattern = agent.modelPattern || flags.model;
91
+ try {
92
+ const { model } = await initInference(modelPattern);
93
+ setAgentInstructions(agent.instructions);
94
+ console.log(`Agent: ${agent.name}`);
95
+ console.log(`Model: ${model.provider}/${model.id}`);
96
+ console.log(`Server: ${config.serverUrl}`);
97
+ }
98
+ catch (err) {
99
+ console.error(err instanceof Error ? err.message : err);
100
+ process.exit(1);
101
+ }
102
+ startAgent(agent.limits, agent.id, agent.name);
103
+ // Keep running until Ctrl+C
104
+ process.on("SIGINT", () => {
105
+ console.log("\nAgent shutting down...");
106
+ process.exit(0);
107
+ });
108
+ break;
109
+ }
110
+ default:
111
+ console.log(`
112
+ swarmlancer — let the swarm begin
113
+
114
+ Usage: swarmlancer [command]
115
+
116
+ (no command) Interactive TUI — guided setup and menu
117
+ login Sign in with GitHub
118
+ agents List configured agents
119
+ models List available LLM models
120
+ start Start first agent
121
+ start --agent <name> Start a specific agent
122
+ start --model <pattern> Override model for this session
123
+ `);
124
+ }
125
+ }
126
+ main().catch((err) => {
127
+ console.error(err);
128
+ process.exit(1);
129
+ });
@@ -0,0 +1,13 @@
1
+ import type { Model, Api } from "@mariozechner/pi-ai";
2
+ export declare function initInference(modelPattern?: string): Promise<{
3
+ model: Model<Api>;
4
+ }>;
5
+ export declare function getAvailableModels(): Model<Api>[];
6
+ /**
7
+ * Set the agent instructions that will be prepended to every inference call.
8
+ */
9
+ export declare function setAgentInstructions(instructions: string): void;
10
+ export declare function runInference(systemPrompt: string, messages: {
11
+ role: "user" | "assistant";
12
+ content: string;
13
+ }[]): Promise<string>;
@@ -0,0 +1,105 @@
1
+ import { AuthStorage, ModelRegistry, createAgentSession, SessionManager, SettingsManager, createExtensionRuntime, } from "@mariozechner/pi-coding-agent";
2
+ let authStorage;
3
+ let modelRegistry;
4
+ let currentModel;
5
+ let currentAgentInstructions = "";
6
+ export async function initInference(modelPattern) {
7
+ authStorage = AuthStorage.create(); // reads ~/.pi/agent/auth.json
8
+ modelRegistry = new ModelRegistry(authStorage);
9
+ const available = await modelRegistry.getAvailable();
10
+ if (available.length === 0) {
11
+ throw new Error("No models available. Run `pi` first and authenticate with a provider (Anthropic, OpenAI, Google, Ollama, etc.)");
12
+ }
13
+ if (modelPattern) {
14
+ const match = available.find((m) => m.id.includes(modelPattern) ||
15
+ m.name.toLowerCase().includes(modelPattern.toLowerCase()) ||
16
+ `${m.provider}/${m.id}`.includes(modelPattern));
17
+ if (!match) {
18
+ console.error(` Model "${modelPattern}" not found. Available:`);
19
+ for (const m of available)
20
+ console.error(` ${m.provider}/${m.id}`);
21
+ process.exit(1);
22
+ }
23
+ currentModel = match;
24
+ }
25
+ else {
26
+ // Pick cheapest reasonable model — prefer latest small models
27
+ const preferences = ["haiku-4", "flash", "gpt-4o-mini", "haiku"];
28
+ for (const pref of preferences) {
29
+ const match = available.find((m) => m.id.includes(pref));
30
+ if (match) {
31
+ currentModel = match;
32
+ break;
33
+ }
34
+ }
35
+ if (!currentModel)
36
+ currentModel = available[0];
37
+ }
38
+ return { model: currentModel };
39
+ }
40
+ export function getAvailableModels() {
41
+ return modelRegistry?.getAvailable() ?? Promise.resolve([]);
42
+ }
43
+ /**
44
+ * Set the agent instructions that will be prepended to every inference call.
45
+ */
46
+ export function setAgentInstructions(instructions) {
47
+ currentAgentInstructions = instructions;
48
+ }
49
+ export async function runInference(systemPrompt, messages) {
50
+ if (!currentModel || !authStorage) {
51
+ throw new Error("Inference not initialized. Call initInference() first.");
52
+ }
53
+ // Prepend agent instructions to server-provided system prompt
54
+ const fullSystemPrompt = currentAgentInstructions
55
+ ? `${currentAgentInstructions}\n\n---\n\n${systemPrompt}`
56
+ : systemPrompt;
57
+ const resourceLoader = {
58
+ getExtensions: () => ({ extensions: [], errors: [], runtime: createExtensionRuntime() }),
59
+ getSkills: () => ({ skills: [], diagnostics: [] }),
60
+ getPrompts: () => ({ prompts: [], diagnostics: [] }),
61
+ getThemes: () => ({ themes: [], diagnostics: [] }),
62
+ getAgentsFiles: () => ({ agentsFiles: [] }),
63
+ getSystemPrompt: () => fullSystemPrompt,
64
+ getAppendSystemPrompt: () => [],
65
+ getPathMetadata: () => new Map(),
66
+ extendResources: () => { },
67
+ reload: async () => { },
68
+ };
69
+ const { session } = await createAgentSession({
70
+ model: currentModel,
71
+ thinkingLevel: "off",
72
+ tools: [],
73
+ authStorage,
74
+ modelRegistry,
75
+ resourceLoader,
76
+ sessionManager: SessionManager.inMemory(),
77
+ settingsManager: SettingsManager.inMemory({
78
+ compaction: { enabled: false },
79
+ retry: { enabled: true, maxRetries: 2 },
80
+ }),
81
+ });
82
+ // Inject conversation history (all but last message)
83
+ if (messages.length > 1) {
84
+ for (const msg of messages.slice(0, -1)) {
85
+ session.agent.state.messages.push(msg.role === "user"
86
+ ? { role: "user", content: [{ type: "text", text: msg.content }], timestamp: Date.now() }
87
+ : {
88
+ role: "assistant",
89
+ content: [{ type: "text", text: msg.content }],
90
+ timestamp: Date.now(),
91
+ });
92
+ }
93
+ }
94
+ let responseText = "";
95
+ session.subscribe((event) => {
96
+ if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
97
+ responseText += event.assistantMessageEvent.delta;
98
+ }
99
+ });
100
+ // Prompt with the last message
101
+ const lastMessage = messages[messages.length - 1];
102
+ await session.prompt(lastMessage.content);
103
+ session.dispose();
104
+ return responseText || "(no response)";
105
+ }
@@ -0,0 +1 @@
1
+ export declare function login(): Promise<void>;
package/dist/login.js ADDED
@@ -0,0 +1,57 @@
1
+ import { getConfig, saveConfig } from "./config.js";
2
+ import open from "open";
3
+ import http from "http";
4
+ export async function login() {
5
+ const config = getConfig();
6
+ // Request GitHub OAuth URL from server, telling it this is a CLI login
7
+ const res = await fetch(`${config.serverUrl}/api/auth/github?source=cli`);
8
+ const { url } = (await res.json());
9
+ console.log("\n Opening GitHub in your browser...\n");
10
+ await open(url);
11
+ // Start a temporary local server to catch the token callback
12
+ // The server-side OAuth callback will try to POST the token here
13
+ const callbackPort = 9876;
14
+ return new Promise((resolve, reject) => {
15
+ const server = http.createServer(async (req, res) => {
16
+ const reqUrl = new URL(req.url || "/", `http://localhost:${callbackPort}`);
17
+ if (reqUrl.pathname === "/callback") {
18
+ const token = reqUrl.searchParams.get("token");
19
+ const userId = reqUrl.searchParams.get("userId");
20
+ if (token && userId) {
21
+ // CORS headers so the browser page can reach us
22
+ res.writeHead(200, {
23
+ "Content-Type": "text/plain",
24
+ "Access-Control-Allow-Origin": "*",
25
+ });
26
+ res.end("ok");
27
+ saveConfig({ ...config, token, userId });
28
+ server.close();
29
+ console.log(" ✓ Logged in successfully!");
30
+ console.log(` ✓ Token saved to ~/.swarmlancer/config.json\n`);
31
+ resolve();
32
+ return;
33
+ }
34
+ }
35
+ // CORS preflight
36
+ if (req.method === "OPTIONS") {
37
+ res.writeHead(200, {
38
+ "Access-Control-Allow-Origin": "*",
39
+ "Access-Control-Allow-Methods": "GET",
40
+ });
41
+ res.end();
42
+ return;
43
+ }
44
+ res.writeHead(404);
45
+ res.end("Not found");
46
+ });
47
+ server.listen(callbackPort, () => {
48
+ console.log(" Waiting for GitHub authorization...");
49
+ console.log(" (If the browser doesn't open, visit the URL above manually)\n");
50
+ });
51
+ // Timeout after 2 minutes
52
+ setTimeout(() => {
53
+ server.close();
54
+ reject(new Error("Login timed out. Try again."));
55
+ }, 120_000);
56
+ });
57
+ }
@@ -0,0 +1,22 @@
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
+ * @param agentInstructions - The agent's instructions (from the selected agent profile)
21
+ */
22
+ export declare function screenProfiles(profiles: CandidateProfile[], agentInstructions: string, 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
+ const BATCH_SIZE = 10;
3
+ /**
4
+ * Screen a batch of profiles using the LLM.
5
+ * Returns scored results sorted by score descending.
6
+ *
7
+ * @param agentInstructions - The agent's instructions (from the selected agent profile)
8
+ */
9
+ export async function screenProfiles(profiles, agentInstructions, sessionGoal, threshold, maxToScreen, onProgress) {
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, agentInstructions, 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, agentInstructions, 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
+ ${agentInstructions}
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
+ }
@@ -0,0 +1,14 @@
1
+ import { type Component } from "@mariozechner/pi-tui";
2
+ import type { TUI } from "@mariozechner/pi-tui";
3
+ import type { AgentProfile } from "../config.js";
4
+ export type AgentConfigAction = "edit-name" | "edit-instructions" | "edit-discovery" | "edit-limits" | "edit-model" | "delete" | "back";
5
+ export declare class AgentConfigScreen implements Component {
6
+ private container;
7
+ private selectList;
8
+ private tui;
9
+ onAction?: (action: AgentConfigAction) => void;
10
+ constructor(tui: TUI, agent: AgentProfile);
11
+ handleInput(data: string): void;
12
+ render(width: number): string[];
13
+ invalidate(): void;
14
+ }