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.
@@ -0,0 +1,129 @@
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 ProfileEditorScreen {
14
+ tui;
15
+ fields;
16
+ editors;
17
+ currentField = 0;
18
+ cachedLines;
19
+ onSave;
20
+ onCancel;
21
+ constructor(tui, profile) {
22
+ this.tui = tui;
23
+ this.fields = [
24
+ { key: "displayName", label: "Display name", value: profile.displayName || "" },
25
+ { key: "bio", label: "Bio", value: profile.bio || "" },
26
+ { key: "skills", label: "Skills", value: profile.skills || "" },
27
+ { key: "projects", label: "Projects", value: profile.projects || "" },
28
+ { key: "lookingFor", label: "Looking for", value: profile.lookingFor || "" },
29
+ ];
30
+ this.editors = this.fields.map((f) => {
31
+ const editor = new Editor(tui, EDITOR_THEME, { paddingX: 1 });
32
+ editor.setText(f.value);
33
+ // Enter moves to next field (via onSubmit), Shift+Enter = new line
34
+ editor.disableSubmit = false;
35
+ return editor;
36
+ });
37
+ // Wire up Enter on each field → advance to next / save
38
+ this.editors.forEach((ed, i) => {
39
+ ed.onSubmit = () => {
40
+ if (i < this.fields.length - 1) {
41
+ this.currentField = i + 1;
42
+ }
43
+ else {
44
+ this.onSave?.(this.getResult());
45
+ return;
46
+ }
47
+ this.cachedLines = undefined;
48
+ this.tui.requestRender();
49
+ };
50
+ });
51
+ }
52
+ getResult() {
53
+ const result = {};
54
+ for (let i = 0; i < this.fields.length; i++) {
55
+ result[this.fields[i].key] = this.editors[i].getText();
56
+ }
57
+ return result;
58
+ }
59
+ handleInput(data) {
60
+ if (matchesKey(data, Key.escape)) {
61
+ this.onCancel?.();
62
+ return;
63
+ }
64
+ // Ctrl+S to save from anywhere
65
+ if (matchesKey(data, Key.ctrl("s"))) {
66
+ this.onSave?.(this.getResult());
67
+ return;
68
+ }
69
+ // Tab → next field, Shift+Tab → prev field
70
+ if (matchesKey(data, Key.tab)) {
71
+ if (this.currentField < this.fields.length - 1) {
72
+ this.currentField++;
73
+ }
74
+ else {
75
+ this.onSave?.(this.getResult());
76
+ return;
77
+ }
78
+ this.cachedLines = undefined;
79
+ this.tui.requestRender();
80
+ return;
81
+ }
82
+ if (matchesKey(data, Key.shift("tab"))) {
83
+ if (this.currentField > 0) {
84
+ this.currentField--;
85
+ this.cachedLines = undefined;
86
+ this.tui.requestRender();
87
+ }
88
+ return;
89
+ }
90
+ // Forward everything else to the active editor
91
+ this.editors[this.currentField].handleInput(data);
92
+ this.cachedLines = undefined;
93
+ this.tui.requestRender();
94
+ }
95
+ render(width) {
96
+ if (this.cachedLines)
97
+ return this.cachedLines;
98
+ const lines = [];
99
+ lines.push("");
100
+ lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
101
+ lines.push(truncateToWidth(theme.title(" Edit Profile"), width));
102
+ lines.push(truncateToWidth(colors.gray(" Other agents use your profile to decide who to talk to."), width));
103
+ lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
104
+ lines.push("");
105
+ for (let i = 0; i < this.fields.length; i++) {
106
+ const field = this.fields[i];
107
+ const isActive = i === this.currentField;
108
+ const label = isActive
109
+ ? theme.accent(`▸ ${field.label}`)
110
+ : colors.gray(` ${field.label}`);
111
+ lines.push(truncateToWidth(label, width));
112
+ // Render the Editor component
113
+ const editorLines = this.editors[i].render(width - 2);
114
+ for (const line of editorLines) {
115
+ lines.push(truncateToWidth(` ${line}`, width));
116
+ }
117
+ lines.push("");
118
+ }
119
+ lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
120
+ lines.push(truncateToWidth(colors.gray(" tab next • shift+tab prev • enter next/save • shift+enter new line • ctrl+s save • esc cancel"), width));
121
+ this.cachedLines = lines;
122
+ return lines;
123
+ }
124
+ invalidate() {
125
+ this.cachedLines = undefined;
126
+ for (const editor of this.editors)
127
+ editor.invalidate();
128
+ }
129
+ }
@@ -0,0 +1,11 @@
1
+ import { type Component } from "@mariozechner/pi-tui";
2
+ import type { TUI } from "@mariozechner/pi-tui";
3
+ export declare class ProfileViewScreen implements Component {
4
+ private container;
5
+ private tui;
6
+ onClose?: () => void;
7
+ constructor(tui: TUI, profile: Record<string, string>);
8
+ handleInput(_data: string): void;
9
+ render(width: number): string[];
10
+ invalidate(): void;
11
+ }
@@ -0,0 +1,38 @@
1
+ import { Container, Text, Spacer, } from "@mariozechner/pi-tui";
2
+ import { colors, theme } from "../theme.js";
3
+ export class ProfileViewScreen {
4
+ container;
5
+ tui;
6
+ onClose;
7
+ constructor(tui, profile) {
8
+ this.tui = tui;
9
+ this.container = new Container();
10
+ this.container.addChild(new Spacer(1));
11
+ this.container.addChild(new Text(theme.border("─".repeat(50)), 0, 0));
12
+ this.container.addChild(new Text(theme.title(" Your Public Profile"), 1, 0));
13
+ this.container.addChild(new Spacer(1));
14
+ const row = (label, value) => {
15
+ const lbl = colors.gray(label.padEnd(14));
16
+ const val = value || colors.gray("(not set)");
17
+ return new Text(` ${lbl} ${val}`, 0, 0);
18
+ };
19
+ this.container.addChild(row("Name", profile.displayName));
20
+ this.container.addChild(row("GitHub", `@${profile.githubUsername || "unknown"}`));
21
+ this.container.addChild(row("Bio", profile.bio));
22
+ this.container.addChild(row("Skills", profile.skills));
23
+ this.container.addChild(row("Projects", profile.projects));
24
+ this.container.addChild(row("Looking for", profile.lookingFor));
25
+ this.container.addChild(new Spacer(1));
26
+ this.container.addChild(new Text(colors.gray(" Press any key to go back"), 1, 0));
27
+ this.container.addChild(new Text(theme.border("─".repeat(50)), 0, 0));
28
+ }
29
+ handleInput(_data) {
30
+ this.onClose?.();
31
+ }
32
+ render(width) {
33
+ return this.container.render(width);
34
+ }
35
+ invalidate() {
36
+ this.container.invalidate();
37
+ }
38
+ }
@@ -0,0 +1,20 @@
1
+ import { type Component } from "@mariozechner/pi-tui";
2
+ import type { TUI } from "@mariozechner/pi-tui";
3
+ type StepStatus = "pending" | "running" | "done" | "failed" | "skipped";
4
+ export declare class SetupWizardScreen implements Component {
5
+ private tui;
6
+ private steps;
7
+ private cachedRender?;
8
+ private promptActive;
9
+ private promptText;
10
+ private promptList?;
11
+ private promptResolve?;
12
+ constructor(tui: TUI);
13
+ setStep(index: number, status: StepStatus, detail?: string): void;
14
+ /** Show a yes/no prompt and return the user's choice */
15
+ askConfirm(question: string): Promise<boolean>;
16
+ handleInput(data: string): void;
17
+ render(width: number): string[];
18
+ invalidate(): void;
19
+ }
20
+ export {};
@@ -0,0 +1,120 @@
1
+ import { SelectList, truncateToWidth, } from "@mariozechner/pi-tui";
2
+ import { colors, theme } from "../theme.js";
3
+ export class SetupWizardScreen {
4
+ tui;
5
+ steps;
6
+ cachedRender;
7
+ // For confirmation prompts
8
+ promptActive = false;
9
+ promptText = "";
10
+ promptList;
11
+ promptResolve;
12
+ constructor(tui) {
13
+ this.tui = tui;
14
+ this.steps = [
15
+ { label: "Authentication", status: "pending" },
16
+ { label: "Model detection", status: "pending" },
17
+ { label: "Profile check", status: "pending" },
18
+ { label: "Agent instructions", status: "pending" },
19
+ ];
20
+ }
21
+ setStep(index, status, detail) {
22
+ this.steps[index].status = status;
23
+ if (detail !== undefined)
24
+ this.steps[index].detail = detail;
25
+ this.cachedRender = undefined;
26
+ this.tui.requestRender();
27
+ }
28
+ /** Show a yes/no prompt and return the user's choice */
29
+ askConfirm(question) {
30
+ return new Promise((resolve) => {
31
+ this.promptActive = true;
32
+ this.promptText = question;
33
+ this.promptResolve = resolve;
34
+ const items = [
35
+ { value: "yes", label: "Yes" },
36
+ { value: "no", label: "Later" },
37
+ ];
38
+ this.promptList = new SelectList(items, 2, {
39
+ selectedPrefix: (t) => theme.accent(t),
40
+ selectedText: (t) => theme.accent(t),
41
+ description: (t) => colors.gray(t),
42
+ scrollInfo: (t) => colors.gray(t),
43
+ noMatch: (t) => colors.yellow(t),
44
+ });
45
+ this.promptList.onSelect = (item) => {
46
+ this.promptActive = false;
47
+ this.promptList = undefined;
48
+ this.cachedRender = undefined;
49
+ this.tui.requestRender();
50
+ resolve(item.value === "yes");
51
+ };
52
+ this.promptList.onCancel = () => {
53
+ this.promptActive = false;
54
+ this.promptList = undefined;
55
+ this.cachedRender = undefined;
56
+ this.tui.requestRender();
57
+ resolve(false);
58
+ };
59
+ this.cachedRender = undefined;
60
+ this.tui.requestRender();
61
+ });
62
+ }
63
+ handleInput(data) {
64
+ if (this.promptActive && this.promptList) {
65
+ this.promptList.handleInput(data);
66
+ this.tui.requestRender();
67
+ }
68
+ }
69
+ render(width) {
70
+ if (this.cachedRender)
71
+ return this.cachedRender;
72
+ const lines = [];
73
+ // Steps
74
+ for (let si = 0; si < this.steps.length; si++) {
75
+ const step = this.steps[si];
76
+ let icon;
77
+ let line;
78
+ switch (step.status) {
79
+ case "pending":
80
+ icon = colors.gray("○");
81
+ line = colors.gray(step.label);
82
+ break;
83
+ case "running":
84
+ icon = colors.cyan("◉");
85
+ line = colors.cyan(step.label + "...");
86
+ break;
87
+ case "done":
88
+ icon = colors.green("✓");
89
+ line = colors.green(step.label);
90
+ break;
91
+ case "failed":
92
+ icon = colors.red("✗");
93
+ line = colors.red(step.label);
94
+ break;
95
+ case "skipped":
96
+ icon = colors.yellow("⊘");
97
+ line = colors.yellow(step.label);
98
+ break;
99
+ }
100
+ lines.push(truncateToWidth(` ${icon} ${line}`, width));
101
+ if (step.detail) {
102
+ lines.push(truncateToWidth(` ${colors.gray(step.detail)}`, width));
103
+ }
104
+ }
105
+ // Prompt
106
+ if (this.promptActive && this.promptList) {
107
+ lines.push("");
108
+ lines.push(truncateToWidth(` ${colors.bold(this.promptText)}`, width));
109
+ lines.push("");
110
+ for (const pLine of this.promptList.render(width - 2)) {
111
+ lines.push(truncateToWidth(` ${pLine}`, width));
112
+ }
113
+ }
114
+ this.cachedRender = lines;
115
+ return lines;
116
+ }
117
+ invalidate() {
118
+ this.cachedRender = undefined;
119
+ }
120
+ }
@@ -0,0 +1,15 @@
1
+ import { Container } from "@mariozechner/pi-tui";
2
+ import type { Model, Api } from "@mariozechner/pi-ai";
3
+ export interface StatusInfo {
4
+ loggedIn: boolean;
5
+ model: Model<Api> | undefined;
6
+ agentConfigured: boolean;
7
+ serverUrl: string;
8
+ }
9
+ export declare class StatusPanel extends Container {
10
+ private info;
11
+ constructor(info: StatusInfo);
12
+ update(info: Partial<StatusInfo>): void;
13
+ private rebuild;
14
+ invalidate(): void;
15
+ }
@@ -0,0 +1,34 @@
1
+ import { Container, Text } from "@mariozechner/pi-tui";
2
+ import { colors, theme } from "../theme.js";
3
+ export class StatusPanel extends Container {
4
+ info;
5
+ constructor(info) {
6
+ super();
7
+ this.info = info;
8
+ this.rebuild();
9
+ }
10
+ update(info) {
11
+ Object.assign(this.info, info);
12
+ this.rebuild();
13
+ }
14
+ rebuild() {
15
+ this.clear();
16
+ const line = (label, value, ok) => {
17
+ const icon = ok ? colors.green("✓") : colors.red("✗");
18
+ const labelStr = colors.gray(label.padEnd(10));
19
+ return ` ${icon} ${labelStr} ${value}`;
20
+ };
21
+ this.addChild(new Text(theme.border("─".repeat(50)), 0, 0));
22
+ this.addChild(new Text(line("Auth", this.info.loggedIn ? "logged in" : "not logged in", this.info.loggedIn), 0, 0));
23
+ this.addChild(new Text(line("Model", this.info.model
24
+ ? `${this.info.model.provider}/${this.info.model.id}`
25
+ : "(not selected)", !!this.info.model), 0, 0));
26
+ this.addChild(new Text(line("Agent", this.info.agentConfigured ? "configured" : "empty — edit agent instructions", this.info.agentConfigured), 0, 0));
27
+ this.addChild(new Text(line("Server", this.info.serverUrl, true), 0, 0));
28
+ this.addChild(new Text(theme.border("─".repeat(50)), 0, 0));
29
+ }
30
+ invalidate() {
31
+ super.invalidate();
32
+ this.rebuild();
33
+ }
34
+ }
@@ -0,0 +1,39 @@
1
+ export declare const colors: {
2
+ reset: string;
3
+ bold: (s: string) => string;
4
+ dim: (s: string) => string;
5
+ italic: (s: string) => string;
6
+ cyan: (s: string) => string;
7
+ green: (s: string) => string;
8
+ red: (s: string) => string;
9
+ yellow: (s: string) => string;
10
+ magenta: (s: string) => string;
11
+ blue: (s: string) => string;
12
+ white: (s: string) => string;
13
+ gray: (s: string) => string;
14
+ brightCyan: (s: string) => string;
15
+ brightGreen: (s: string) => string;
16
+ brightYellow: (s: string) => string;
17
+ brightWhite: (s: string) => string;
18
+ bgGray: (s: string) => string;
19
+ bgCyan: (s: string) => string;
20
+ bgBlue: (s: string) => string;
21
+ inverse: (s: string) => string;
22
+ };
23
+ export declare const theme: {
24
+ accent: (s: string) => string;
25
+ success: (s: string) => string;
26
+ error: (s: string) => string;
27
+ warning: (s: string) => string;
28
+ muted: (s: string) => string;
29
+ highlight: (s: string) => string;
30
+ border: (s: string) => string;
31
+ title: (s: string) => string;
32
+ subtitle: (s: string) => string;
33
+ status: {
34
+ ok: (s: string) => string;
35
+ fail: (s: string) => string;
36
+ warn: (s: string) => string;
37
+ info: (s: string) => string;
38
+ };
39
+ };
package/dist/theme.js ADDED
@@ -0,0 +1,47 @@
1
+ // ANSI color helpers for standalone TUI (no pi theme system)
2
+ const ESC = "\x1b[";
3
+ export const colors = {
4
+ reset: `${ESC}0m`,
5
+ // Foreground
6
+ bold: (s) => `${ESC}1m${s}${ESC}22m`,
7
+ dim: (s) => `${ESC}2m${s}${ESC}22m`,
8
+ italic: (s) => `${ESC}3m${s}${ESC}23m`,
9
+ // Named colors
10
+ cyan: (s) => `${ESC}36m${s}${ESC}39m`,
11
+ green: (s) => `${ESC}32m${s}${ESC}39m`,
12
+ red: (s) => `${ESC}31m${s}${ESC}39m`,
13
+ yellow: (s) => `${ESC}33m${s}${ESC}39m`,
14
+ magenta: (s) => `${ESC}35m${s}${ESC}39m`,
15
+ blue: (s) => `${ESC}34m${s}${ESC}39m`,
16
+ white: (s) => `${ESC}37m${s}${ESC}39m`,
17
+ gray: (s) => `${ESC}90m${s}${ESC}39m`,
18
+ // Bright
19
+ brightCyan: (s) => `${ESC}96m${s}${ESC}39m`,
20
+ brightGreen: (s) => `${ESC}92m${s}${ESC}39m`,
21
+ brightYellow: (s) => `${ESC}93m${s}${ESC}39m`,
22
+ brightWhite: (s) => `${ESC}97m${s}${ESC}39m`,
23
+ // Background
24
+ bgGray: (s) => `${ESC}48;5;236m${s}${ESC}49m`,
25
+ bgCyan: (s) => `${ESC}46m${s}${ESC}49m`,
26
+ bgBlue: (s) => `${ESC}44m${s}${ESC}49m`,
27
+ // Inverse
28
+ inverse: (s) => `${ESC}7m${s}${ESC}27m`,
29
+ };
30
+ // Semantic aliases
31
+ export const theme = {
32
+ accent: colors.cyan,
33
+ success: colors.green,
34
+ error: colors.red,
35
+ warning: colors.yellow,
36
+ muted: colors.gray,
37
+ highlight: colors.brightCyan,
38
+ border: (s) => colors.cyan(s),
39
+ title: (s) => colors.bold(colors.brightCyan(s)),
40
+ subtitle: (s) => colors.bold(colors.white(s)),
41
+ status: {
42
+ ok: (s) => colors.green(`✓ ${s}`),
43
+ fail: (s) => colors.red(`✗ ${s}`),
44
+ warn: (s) => colors.yellow(`⚠ ${s}`),
45
+ info: (s) => colors.cyan(`▸ ${s}`),
46
+ },
47
+ };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "swarmlancer",
3
+ "version": "0.1.0",
4
+ "description": "Swarmlancer CLI — your agent, your rules. Connect your AI agent to a network of other agents.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "swarmlancer": "dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.build.json",
16
+ "prepublishOnly": "rm -rf dist && npm run build",
17
+ "start": "bun run src/index.ts start",
18
+ "dev": "bun run src/index.ts"
19
+ },
20
+ "keywords": [
21
+ "ai",
22
+ "agent",
23
+ "networking",
24
+ "cli",
25
+ "llm",
26
+ "autonomous"
27
+ ],
28
+ "author": "nevertypeit",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/nevertypeit/swarmlancer-cli"
33
+ },
34
+ "homepage": "https://swarmlancer.com",
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^25.5.0",
40
+ "@types/ws": "^8.18.1",
41
+ "typescript": "^5.8.0"
42
+ },
43
+ "dependencies": {
44
+ "@mariozechner/pi-coding-agent": "^0.58.4",
45
+ "@mariozechner/pi-tui": "^0.58.4",
46
+ "open": "^11.0.0",
47
+ "ws": "^8.19.0"
48
+ }
49
+ }