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.
- package/README.md +15 -0
- package/dist/agent.d.ts +13 -0
- package/dist/agent.js +202 -0
- package/dist/app.d.ts +4 -0
- package/dist/app.js +496 -0
- package/dist/config.d.ts +49 -0
- package/dist/config.js +175 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +129 -0
- package/dist/inference.d.ts +13 -0
- package/dist/inference.js +105 -0
- package/dist/login.d.ts +1 -0
- package/dist/login.js +57 -0
- package/dist/screening.d.ts +22 -0
- package/dist/screening.js +101 -0
- package/dist/screens/agent-config.d.ts +14 -0
- package/dist/screens/agent-config.js +64 -0
- package/dist/screens/agent-editor.d.ts +13 -0
- package/dist/screens/agent-editor.js +64 -0
- package/dist/screens/agent-list.d.ts +22 -0
- package/dist/screens/agent-list.js +73 -0
- package/dist/screens/agent-picker.d.ts +15 -0
- package/dist/screens/agent-picker.js +51 -0
- package/dist/screens/agent-running.d.ts +20 -0
- package/dist/screens/agent-running.js +68 -0
- package/dist/screens/banner.d.ts +6 -0
- package/dist/screens/banner.js +27 -0
- package/dist/screens/dashboard.d.ts +16 -0
- package/dist/screens/dashboard.js +59 -0
- package/dist/screens/discovery-settings.d.ts +17 -0
- package/dist/screens/discovery-settings.js +189 -0
- package/dist/screens/message.d.ts +15 -0
- package/dist/screens/message.js +39 -0
- package/dist/screens/model-picker.d.ts +14 -0
- package/dist/screens/model-picker.js +49 -0
- package/dist/screens/name-editor.d.ts +15 -0
- package/dist/screens/name-editor.js +67 -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/dist/screens/setup-wizard.d.ts +20 -0
- package/dist/screens/setup-wizard.js +120 -0
- package/dist/screens/status-panel.d.ts +15 -0
- package/dist/screens/status-panel.js +37 -0
- package/dist/theme.d.ts +42 -0
- package/dist/theme.js +56 -0
- 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
|
+
`;
|
package/dist/index.d.ts
ADDED
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
|
+
}
|
package/dist/login.d.ts
ADDED
|
@@ -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
|
+
}
|