swarmlancer 0.1.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 ADDED
@@ -0,0 +1,15 @@
1
+ # cli
2
+
3
+ To install dependencies:
4
+
5
+ ```bash
6
+ bun install
7
+ ```
8
+
9
+ To run:
10
+
11
+ ```bash
12
+ bun run index.ts
13
+ ```
14
+
15
+ This project was created using `bun init` in bun v1.3.6. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
@@ -0,0 +1,4 @@
1
+ type LogFn = (line: string) => void;
2
+ export declare function startAgent(log?: LogFn): void;
3
+ export declare function stopAgent(): void;
4
+ export {};
package/dist/agent.js ADDED
@@ -0,0 +1,95 @@
1
+ import WebSocket from "ws";
2
+ import { getConfig, getAgentFilePath } from "./config.js";
3
+ import { runInference } from "./inference.js";
4
+ import { colors } from "./theme.js";
5
+ let activeWs = null;
6
+ let alive = false;
7
+ let reconnectTimer = null;
8
+ export function startAgent(log = console.log) {
9
+ const config = getConfig();
10
+ if (!config.token) {
11
+ log(colors.red("Not logged in. Run: swarmlancer login"));
12
+ return;
13
+ }
14
+ const wsUrl = config.serverUrl.replace(/^http/, "ws") + "/ws";
15
+ alive = true;
16
+ function connect() {
17
+ log(colors.cyan(`Connecting to ${wsUrl}...`));
18
+ const ws = new WebSocket(wsUrl);
19
+ activeWs = ws;
20
+ ws.on("open", () => {
21
+ ws.send(JSON.stringify({ type: "auth", token: config.token }));
22
+ });
23
+ ws.on("message", async (raw) => {
24
+ const msg = JSON.parse(raw.toString());
25
+ switch (msg.type) {
26
+ case "authenticated":
27
+ log(colors.green("Agent online"));
28
+ log(colors.gray(`Agent instructions: ${getAgentFilePath()}`));
29
+ log("");
30
+ log(colors.gray("Waiting for conversations..."));
31
+ ws.send(JSON.stringify({ type: "get_online_users" }));
32
+ break;
33
+ case "online_users": {
34
+ const users = msg.users;
35
+ if (users.length > 0) {
36
+ log(colors.cyan(`📡 Online: ${users.map((u) => `${u.displayName} (@${u.githubUsername})`).join(", ")}`));
37
+ }
38
+ else {
39
+ log(colors.gray("No other agents online right now."));
40
+ }
41
+ break;
42
+ }
43
+ case "inference_request": {
44
+ const { requestId, conversationId, systemPrompt, messages } = msg;
45
+ log(colors.yellow(`💬 Inference request for conversation ${conversationId.slice(0, 8)}...`));
46
+ try {
47
+ const response = await runInference(systemPrompt, messages);
48
+ const preview = response.length > 80 ? response.slice(0, 80) + "..." : response;
49
+ log(colors.green(`✓ Response: ${preview}`));
50
+ ws.send(JSON.stringify({
51
+ type: "inference_response",
52
+ requestId,
53
+ content: response,
54
+ }));
55
+ }
56
+ catch (err) {
57
+ const errorMsg = err instanceof Error ? err.message : "Unknown error";
58
+ log(colors.red(`✗ Inference failed: ${errorMsg}`));
59
+ ws.send(JSON.stringify({
60
+ type: "inference_error",
61
+ requestId,
62
+ error: errorMsg,
63
+ }));
64
+ }
65
+ break;
66
+ }
67
+ case "conversation_started": {
68
+ const { conversationId, withUser } = msg;
69
+ log(colors.brightGreen(`🤝 Conversation started with ${withUser.displayName} (@${withUser.githubUsername})`));
70
+ break;
71
+ }
72
+ case "error":
73
+ log(colors.red(`Server error: ${msg.message}`));
74
+ break;
75
+ }
76
+ });
77
+ ws.on("close", () => {
78
+ if (alive) {
79
+ log(colors.yellow("Disconnected. Reconnecting in 5s..."));
80
+ reconnectTimer = setTimeout(connect, 5000);
81
+ }
82
+ });
83
+ ws.on("error", (err) => {
84
+ log(colors.red(`WebSocket error: ${err.message}`));
85
+ });
86
+ }
87
+ connect();
88
+ }
89
+ export function stopAgent() {
90
+ alive = false;
91
+ if (reconnectTimer)
92
+ clearTimeout(reconnectTimer);
93
+ activeWs?.close();
94
+ activeWs = null;
95
+ }
package/dist/app.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Main entry point for interactive TUI mode.
3
+ */
4
+ export declare function runInteractive(): Promise<void>;
package/dist/app.js ADDED
@@ -0,0 +1,303 @@
1
+ import { ProcessTerminal, TUI, Container } from "@mariozechner/pi-tui";
2
+ import { getConfig, ensureAgentFile, getAgentFilePath, getAgentInstructions, saveAgentInstructions, } from "./config.js";
3
+ import { login } from "./login.js";
4
+ import { initInference, getAvailableModels } from "./inference.js";
5
+ import { getProfile, isProfileComplete, saveProfile, } from "./profile.js";
6
+ import { startAgent, stopAgent } from "./agent.js";
7
+ import { BannerComponent } from "./screens/banner.js";
8
+ import { SetupWizardScreen } from "./screens/setup-wizard.js";
9
+ import { DashboardScreen } from "./screens/dashboard.js";
10
+ import { ModelPickerScreen } from "./screens/model-picker.js";
11
+ import { ProfileEditorScreen } from "./screens/profile-editor.js";
12
+ import { ProfileViewScreen } from "./screens/profile-view.js";
13
+ import { AgentEditorScreen } from "./screens/agent-editor.js";
14
+ import { AgentRunningScreen } from "./screens/agent-running.js";
15
+ import { MessageScreen } from "./screens/message.js";
16
+ let terminal;
17
+ let tui;
18
+ let selectedModel;
19
+ /**
20
+ * Switch the active screen by replacing the TUI's child + focus.
21
+ */
22
+ function setScreen(component) {
23
+ tui.clear();
24
+ tui.addChild(component);
25
+ tui.setFocus(component);
26
+ tui.requestRender(true);
27
+ }
28
+ /**
29
+ * Show a temporary message and wait for any key.
30
+ */
31
+ function showMessage(title, lines, style = "info") {
32
+ return new Promise((resolve) => {
33
+ const screen = new MessageScreen(tui, title, lines, style);
34
+ screen.onClose = () => resolve();
35
+ setScreen(screen);
36
+ });
37
+ }
38
+ /**
39
+ * Run the setup wizard: check auth, model, profile, agent.
40
+ */
41
+ async function runSetup() {
42
+ const banner = new BannerComponent();
43
+ const wizard = new SetupWizardScreen(tui);
44
+ // Show banner + wizard together
45
+ const wrapper = new Container();
46
+ wrapper.addChild(banner);
47
+ wrapper.addChild(wizard);
48
+ tui.clear();
49
+ tui.addChild(wrapper);
50
+ tui.setFocus(wizard);
51
+ tui.requestRender(true);
52
+ // Step 1: Auth
53
+ const config = getConfig();
54
+ wizard.setStep(0, "running");
55
+ if (config.token) {
56
+ wizard.setStep(0, "done", "logged in");
57
+ }
58
+ else {
59
+ wizard.setStep(0, "running", "not logged in — opening browser...");
60
+ try {
61
+ await login();
62
+ wizard.setStep(0, "done", "logged in");
63
+ }
64
+ catch (err) {
65
+ wizard.setStep(0, "failed", err instanceof Error ? err.message : "login failed");
66
+ }
67
+ }
68
+ // Step 2: Model
69
+ wizard.setStep(1, "running", "detecting pi credentials...");
70
+ try {
71
+ const { model } = await initInference();
72
+ selectedModel = model;
73
+ wizard.setStep(1, "done", `${model.provider}/${model.id}`);
74
+ }
75
+ catch (err) {
76
+ wizard.setStep(1, "failed", err instanceof Error ? err.message : "no models found");
77
+ }
78
+ // Step 3: Profile
79
+ wizard.setStep(2, "running");
80
+ const profile = await getProfile();
81
+ if (profile && isProfileComplete(profile)) {
82
+ wizard.setStep(2, "done", "complete");
83
+ }
84
+ else {
85
+ wizard.setStep(2, "skipped", "incomplete");
86
+ const shouldEdit = await wizard.askConfirm("Your profile is incomplete. Set it up now?");
87
+ if (shouldEdit) {
88
+ await runProfileEditor();
89
+ wizard.setStep(2, "done", "updated");
90
+ // Re-show the wizard
91
+ tui.clear();
92
+ tui.addChild(wrapper);
93
+ tui.setFocus(wizard);
94
+ tui.requestRender(true);
95
+ }
96
+ }
97
+ // Step 4: Agent instructions
98
+ wizard.setStep(3, "running");
99
+ ensureAgentFile();
100
+ const agentMd = getAgentInstructions();
101
+ if (agentMd && !agentMd.includes("(Describe yourself")) {
102
+ wizard.setStep(3, "done", "configured");
103
+ }
104
+ else {
105
+ wizard.setStep(3, "skipped", "using defaults");
106
+ const shouldEdit = await wizard.askConfirm("Edit your agent instructions now?");
107
+ if (shouldEdit) {
108
+ await runAgentEditor();
109
+ wizard.setStep(3, "done", "updated");
110
+ // Re-show the wizard
111
+ tui.clear();
112
+ tui.addChild(wrapper);
113
+ tui.setFocus(wizard);
114
+ tui.requestRender(true);
115
+ }
116
+ }
117
+ // Short pause then go to dashboard
118
+ await new Promise((r) => setTimeout(r, 800));
119
+ }
120
+ /**
121
+ * Inline agent instructions editor.
122
+ */
123
+ function runAgentEditor() {
124
+ return new Promise((resolve) => {
125
+ ensureAgentFile();
126
+ const content = getAgentInstructions();
127
+ const screen = new AgentEditorScreen(tui, content);
128
+ screen.onSave = async (newContent) => {
129
+ try {
130
+ saveAgentInstructions(newContent);
131
+ await showMessage("Saved", ["Agent instructions updated."], "success");
132
+ }
133
+ catch (err) {
134
+ await showMessage("Error", [err instanceof Error ? err.message : "Failed to save"], "error");
135
+ }
136
+ resolve();
137
+ };
138
+ screen.onCancel = () => resolve();
139
+ setScreen(screen);
140
+ });
141
+ }
142
+ /**
143
+ * Profile editor screen.
144
+ */
145
+ function runProfileEditor() {
146
+ return new Promise(async (resolve) => {
147
+ const profile = await getProfile();
148
+ if (!profile) {
149
+ await showMessage("Error", ["Could not load profile."], "error");
150
+ resolve();
151
+ return;
152
+ }
153
+ const screen = new ProfileEditorScreen(tui, profile);
154
+ screen.onSave = async (data) => {
155
+ try {
156
+ await saveProfile(data);
157
+ await showMessage("Success", ["Profile updated."], "success");
158
+ }
159
+ catch (err) {
160
+ await showMessage("Error", [err instanceof Error ? err.message : "Failed to save"], "error");
161
+ }
162
+ resolve();
163
+ };
164
+ screen.onCancel = () => resolve();
165
+ setScreen(screen);
166
+ });
167
+ }
168
+ /**
169
+ * Model picker screen.
170
+ */
171
+ function runModelPicker() {
172
+ return new Promise(async (resolve) => {
173
+ try {
174
+ await initInference();
175
+ const models = await getAvailableModels();
176
+ if (models.length === 0) {
177
+ await showMessage("No models", ["No models available.", "Run `pi` and /login to authenticate a provider."], "error");
178
+ resolve();
179
+ return;
180
+ }
181
+ const screen = new ModelPickerScreen(tui, models);
182
+ screen.onSelect = async (model) => {
183
+ selectedModel = model;
184
+ await initInference(model.id);
185
+ await showMessage("Model selected", [`${model.provider}/${model.id}`], "success");
186
+ resolve();
187
+ };
188
+ screen.onCancel = () => resolve();
189
+ setScreen(screen);
190
+ }
191
+ catch (err) {
192
+ await showMessage("Error", [err instanceof Error ? err.message : "Failed to load models"], "error");
193
+ resolve();
194
+ }
195
+ });
196
+ }
197
+ /**
198
+ * Profile view screen.
199
+ */
200
+ function runProfileView() {
201
+ return new Promise(async (resolve) => {
202
+ const profile = await getProfile();
203
+ if (!profile) {
204
+ await showMessage("Error", ["Could not load profile."], "error");
205
+ resolve();
206
+ return;
207
+ }
208
+ const screen = new ProfileViewScreen(tui, profile);
209
+ screen.onClose = () => resolve();
210
+ setScreen(screen);
211
+ });
212
+ }
213
+ /**
214
+ * Start the agent and show the live log screen.
215
+ */
216
+ function runAgent() {
217
+ return new Promise((resolve) => {
218
+ const config = getConfig();
219
+ const screen = new AgentRunningScreen(tui, selectedModel, config.serverUrl, getAgentFilePath());
220
+ screen.onStop = () => {
221
+ stopAgent();
222
+ resolve();
223
+ };
224
+ setScreen(screen);
225
+ // Start the agent with the log callback
226
+ startAgent((line) => {
227
+ screen.addLog(line);
228
+ });
229
+ });
230
+ }
231
+ /**
232
+ * Main dashboard loop.
233
+ */
234
+ async function runDashboard() {
235
+ while (true) {
236
+ const config = getConfig();
237
+ const agentMd = getAgentInstructions();
238
+ const action = await new Promise((resolve) => {
239
+ const dashboard = new DashboardScreen(tui, {
240
+ loggedIn: !!config.token,
241
+ model: selectedModel,
242
+ agentConfigured: !!(agentMd && !agentMd.includes("(Describe yourself")),
243
+ serverUrl: config.serverUrl,
244
+ });
245
+ dashboard.onAction = resolve;
246
+ setScreen(dashboard);
247
+ });
248
+ switch (action) {
249
+ case "start": {
250
+ if (!selectedModel) {
251
+ await showMessage("No model", ["Select a model first."], "error");
252
+ break;
253
+ }
254
+ const conf = getConfig();
255
+ if (!conf.token) {
256
+ await showMessage("Not logged in", ["Run login first."], "error");
257
+ break;
258
+ }
259
+ await runAgent();
260
+ break;
261
+ }
262
+ case "profile-edit":
263
+ await runProfileEditor();
264
+ break;
265
+ case "agent-edit":
266
+ await runAgentEditor();
267
+ break;
268
+ case "model-pick":
269
+ await runModelPicker();
270
+ break;
271
+ case "profile-view":
272
+ await runProfileView();
273
+ break;
274
+ case "quit":
275
+ shutdown();
276
+ return;
277
+ }
278
+ }
279
+ }
280
+ function shutdown() {
281
+ tui.stop();
282
+ terminal.clearScreen();
283
+ process.exit(0);
284
+ }
285
+ /**
286
+ * Main entry point for interactive TUI mode.
287
+ */
288
+ export async function runInteractive() {
289
+ terminal = new ProcessTerminal();
290
+ tui = new TUI(terminal, true);
291
+ tui.start();
292
+ // Handle Ctrl+C globally
293
+ tui.addInputListener((data) => {
294
+ if (data === "\x03") {
295
+ // Ctrl+C
296
+ shutdown();
297
+ return { consume: true };
298
+ }
299
+ return undefined;
300
+ });
301
+ await runSetup();
302
+ await runDashboard();
303
+ }
@@ -0,0 +1,12 @@
1
+ export type Config = {
2
+ token?: string;
3
+ serverUrl: string;
4
+ userId?: string;
5
+ };
6
+ export declare function getConfigDir(): string;
7
+ export declare function getConfig(): Config;
8
+ export declare function saveConfig(config: Config): void;
9
+ export declare function getAgentInstructions(): string;
10
+ export declare function getAgentFilePath(): string;
11
+ export declare function saveAgentInstructions(content: string): void;
12
+ export declare function ensureAgentFile(): void;
package/dist/config.js ADDED
@@ -0,0 +1,61 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+ const CONFIG_DIR = join(homedir(), ".swarmlancer");
5
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
6
+ const AGENT_FILE = join(CONFIG_DIR, "agent.md");
7
+ const DEFAULT_CONFIG = {
8
+ serverUrl: "https://swarmlancer.com",
9
+ };
10
+ export function getConfigDir() {
11
+ return CONFIG_DIR;
12
+ }
13
+ export function getConfig() {
14
+ try {
15
+ if (existsSync(CONFIG_FILE)) {
16
+ return { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync(CONFIG_FILE, "utf-8")) };
17
+ }
18
+ }
19
+ catch { }
20
+ return { ...DEFAULT_CONFIG };
21
+ }
22
+ export function saveConfig(config) {
23
+ mkdirSync(CONFIG_DIR, { recursive: true });
24
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
25
+ }
26
+ export function getAgentInstructions() {
27
+ try {
28
+ if (existsSync(AGENT_FILE)) {
29
+ return readFileSync(AGENT_FILE, "utf-8").trim();
30
+ }
31
+ }
32
+ catch { }
33
+ return "";
34
+ }
35
+ export function getAgentFilePath() {
36
+ return AGENT_FILE;
37
+ }
38
+ export function saveAgentInstructions(content) {
39
+ mkdirSync(CONFIG_DIR, { recursive: true });
40
+ writeFileSync(AGENT_FILE, content);
41
+ }
42
+ export function ensureAgentFile() {
43
+ mkdirSync(CONFIG_DIR, { recursive: true });
44
+ if (!existsSync(AGENT_FILE)) {
45
+ writeFileSync(AGENT_FILE, `# My Agent Instructions
46
+
47
+ ## About me
48
+ (Describe yourself — what you do, what you're building)
49
+
50
+ ## What I'm looking for
51
+ (What kind of people or collaborations interest you?)
52
+
53
+ ## How my agent should behave
54
+ - Be direct and genuine
55
+ - If there's no real connection, wrap up quickly
56
+ - If there's potential, suggest connecting on GitHub
57
+ - Keep responses concise
58
+ - Max 8-10 messages per conversation
59
+ `);
60
+ }
61
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ import { runInteractive } from "./app.js";
3
+ import { login } from "./login.js";
4
+ import { getConfig, ensureAgentFile, getAgentFilePath } from "./config.js";
5
+ import { initInference, getAvailableModels } from "./inference.js";
6
+ import { startAgent } from "./agent.js";
7
+ import { getProfile } from "./profile.js";
8
+ async function main() {
9
+ const args = process.argv.slice(2);
10
+ const command = args[0];
11
+ // No args → full TUI interactive mode
12
+ if (!command) {
13
+ await runInteractive();
14
+ return;
15
+ }
16
+ // Parse flags for direct commands
17
+ const flags = {};
18
+ for (let i = 1; i < args.length; i++) {
19
+ if (args[i].startsWith("--") && args[i + 1]) {
20
+ flags[args[i].slice(2)] = args[i + 1];
21
+ i++;
22
+ }
23
+ }
24
+ // Direct commands for scripting / CI
25
+ switch (command) {
26
+ case "login":
27
+ await login();
28
+ break;
29
+ case "profile":
30
+ if (args[1] === "edit") {
31
+ console.log("Use interactive mode for profile editing: swarmlancer");
32
+ }
33
+ else {
34
+ const profile = await getProfile();
35
+ if (profile) {
36
+ console.log(`Name: ${profile.displayName || "(not set)"}`);
37
+ console.log(`GitHub: @${profile.githubUsername}`);
38
+ console.log(`Bio: ${profile.bio || "(not set)"}`);
39
+ console.log(`Skills: ${profile.skills || "(not set)"}`);
40
+ console.log(`Projects: ${profile.projects || "(not set)"}`);
41
+ console.log(`Looking for: ${profile.lookingFor || "(not set)"}`);
42
+ }
43
+ else {
44
+ console.log("Could not load profile.");
45
+ }
46
+ }
47
+ break;
48
+ case "agent":
49
+ console.log(`Agent instructions file: ${getAgentFilePath()}`);
50
+ console.log(`Use interactive mode to edit: swarmlancer`);
51
+ break;
52
+ case "models": {
53
+ console.log("Available models (from pi credentials):\n");
54
+ try {
55
+ await initInference();
56
+ const models = await getAvailableModels();
57
+ for (const m of models)
58
+ console.log(` ${m.provider}/${m.id}`);
59
+ console.log(`\nUse: swarmlancer start --model <pattern>`);
60
+ }
61
+ catch (err) {
62
+ console.error(err instanceof Error ? err.message : err);
63
+ }
64
+ break;
65
+ }
66
+ case "start": {
67
+ ensureAgentFile();
68
+ const config = getConfig();
69
+ if (!config.token) {
70
+ console.error("Not logged in. Run: swarmlancer login");
71
+ process.exit(1);
72
+ }
73
+ try {
74
+ const { model } = await initInference(flags.model);
75
+ console.log(`Model: ${model.provider}/${model.id}`);
76
+ console.log(`Server: ${config.serverUrl}`);
77
+ console.log(`Agent: ${getAgentFilePath()}`);
78
+ }
79
+ catch (err) {
80
+ console.error(err instanceof Error ? err.message : err);
81
+ process.exit(1);
82
+ }
83
+ startAgent();
84
+ // Keep running until Ctrl+C
85
+ process.on("SIGINT", () => {
86
+ console.log("\nAgent shutting down...");
87
+ process.exit(0);
88
+ });
89
+ break;
90
+ }
91
+ default:
92
+ console.log(`
93
+ swarmlancer — your agent, your rules
94
+
95
+ Usage: swarmlancer [command]
96
+
97
+ (no command) Interactive TUI — guided setup and menu
98
+ login Sign in with GitHub
99
+ profile View your public profile
100
+ agent Edit agent instructions
101
+ models List available LLM models
102
+ start Start your agent
103
+ start --model <pattern> Start with a specific model
104
+ `);
105
+ }
106
+ }
107
+ main().catch((err) => {
108
+ console.error(err);
109
+ process.exit(1);
110
+ });
@@ -0,0 +1,9 @@
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
+ export declare function runInference(systemPrompt: string, messages: {
7
+ role: "user" | "assistant";
8
+ content: string;
9
+ }[]): Promise<string>;