pilotswarm-cli 0.1.11 → 0.1.13
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/bin/tui.js +4 -276
- package/package.json +16 -11
- package/src/app.js +595 -0
- package/src/bootstrap-env.js +187 -0
- package/src/embedded-workers.js +65 -0
- package/src/index.js +152 -0
- package/src/node-sdk-transport.js +702 -0
- package/src/platform.js +899 -0
- package/tui-splash.txt +11 -0
- package/cli/tui.js +0 -7279
package/bin/tui.js
CHANGED
|
@@ -1,279 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
*
|
|
6
|
-
* Modes:
|
|
7
|
-
* local Embedded workers + TUI in one process (default)
|
|
8
|
-
* remote Client-only TUI, workers run elsewhere (AKS, separate process)
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
* npx pilotswarm local --plugin ./plugin
|
|
12
|
-
* npx pilotswarm remote --store postgresql://... --context toygres-aks
|
|
13
|
-
* npx pilotswarm --env .env --plugin ./my-plugin --workers 4
|
|
14
|
-
*
|
|
15
|
-
* All flags can also be set via environment variables (CLI flags take precedence).
|
|
16
|
-
*/
|
|
3
|
+
import { parseCliIntoEnv } from "../src/bootstrap-env.js";
|
|
4
|
+
import { startTuiApp } from "../src/index.js";
|
|
17
5
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
import path from "node:path";
|
|
21
|
-
import { fileURLToPath } from "node:url";
|
|
22
|
-
|
|
23
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
24
|
-
const pkgRoot = path.resolve(__dirname, "..");
|
|
25
|
-
const defaultTuiSplashPath = path.join(pkgRoot, "cli", "tui-splash.txt");
|
|
26
|
-
|
|
27
|
-
function readPluginMetadata(pluginDir) {
|
|
28
|
-
if (!pluginDir) return null;
|
|
29
|
-
const pluginJsonPath = path.join(pluginDir, "plugin.json");
|
|
30
|
-
if (!fs.existsSync(pluginJsonPath)) return null;
|
|
31
|
-
try {
|
|
32
|
-
return JSON.parse(fs.readFileSync(pluginJsonPath, "utf-8"));
|
|
33
|
-
} catch (err) {
|
|
34
|
-
console.error(`Failed to parse plugin metadata: ${pluginJsonPath}: ${err.message}`);
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// ─── Parse CLI args ──────────────────────────────────────────────
|
|
40
|
-
|
|
41
|
-
const { values: flags, positionals } = parseArgs({
|
|
42
|
-
options: {
|
|
43
|
-
// Connection
|
|
44
|
-
store: { type: "string", short: "s" },
|
|
45
|
-
env: { type: "string", short: "e" },
|
|
46
|
-
|
|
47
|
-
// Local mode
|
|
48
|
-
plugin: { type: "string", short: "p" },
|
|
49
|
-
worker: { type: "string", short: "w" },
|
|
50
|
-
workers: { type: "string", short: "n" },
|
|
51
|
-
model: { type: "string", short: "m" },
|
|
52
|
-
system: { type: "string" },
|
|
53
|
-
|
|
54
|
-
// Remote mode
|
|
55
|
-
context: { type: "string", short: "c" },
|
|
56
|
-
namespace: { type: "string" },
|
|
57
|
-
label: { type: "string" },
|
|
58
|
-
|
|
59
|
-
// General
|
|
60
|
-
"log-level": { type: "string" },
|
|
61
|
-
help: { type: "boolean", short: "h" },
|
|
62
|
-
},
|
|
63
|
-
allowPositionals: true,
|
|
64
|
-
strict: false,
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
if (flags.help) {
|
|
68
|
-
console.log(`
|
|
69
|
-
pilotswarm — TUI for pilotswarm apps
|
|
70
|
-
|
|
71
|
-
USAGE
|
|
72
|
-
npx pilotswarm [local|remote] [flags]
|
|
73
|
-
|
|
74
|
-
MODES
|
|
75
|
-
local Embed workers in the TUI process (default)
|
|
76
|
-
remote Client-only — connect to remote workers via kubectl logs
|
|
77
|
-
|
|
78
|
-
FLAGS ENV VAR EQUIVALENT
|
|
79
|
-
-s, --store <url> Database URL DATABASE_URL
|
|
80
|
-
-e, --env <file> Env file (default: .env / .env.remote)
|
|
81
|
-
|
|
82
|
-
-p, --plugin <dir> Plugin dir PLUGIN_DIRS
|
|
83
|
-
-w, --worker <module> Tool module WORKER_MODULE
|
|
84
|
-
-n, --workers <count> Worker count WORKERS (default: 4)
|
|
85
|
-
-m, --model <name> LLM model COPILOT_MODEL
|
|
86
|
-
--system <msg|file> System msg SYSTEM_MESSAGE (or plugin/system.md)
|
|
87
|
-
|
|
88
|
-
-c, --context <ctx> K8s context K8S_CONTEXT
|
|
89
|
-
--namespace <ns> K8s namespace K8S_NAMESPACE (default: copilot-runtime)
|
|
90
|
-
--label <selector> Pod label K8S_POD_LABEL
|
|
91
|
-
--log-level <level> Trace level LOG_LEVEL
|
|
92
|
-
-h, --help Show help
|
|
93
|
-
|
|
94
|
-
All flags can be set via the corresponding env var (in .env or exported).
|
|
95
|
-
CLI flags take precedence over env vars.
|
|
96
|
-
|
|
97
|
-
EXAMPLES
|
|
98
|
-
# Plugin-only (no code), env from file
|
|
99
|
-
npx pilotswarm --env .env --plugin ./my-plugin
|
|
100
|
-
|
|
101
|
-
# Custom tools via worker module
|
|
102
|
-
npx pilotswarm --env .env --plugin ./plugin --worker ./tools.js
|
|
103
|
-
|
|
104
|
-
# All config in .env (zero flags)
|
|
105
|
-
echo "DATABASE_URL=postgresql://..." >> .env
|
|
106
|
-
echo "GITHUB_TOKEN=ghu_..." >> .env
|
|
107
|
-
echo "PLUGIN_DIRS=./plugin" >> .env
|
|
108
|
-
npx pilotswarm
|
|
109
|
-
|
|
110
|
-
# Client-only, workers on AKS
|
|
111
|
-
npx pilotswarm remote --store postgresql://... --namespace my-app
|
|
112
|
-
`.trim());
|
|
113
|
-
process.exit(0);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ─── Determine mode ─────────────────────────────────────────────
|
|
117
|
-
|
|
118
|
-
const mode = positionals[0] === "remote" ? "remote" : "local";
|
|
119
|
-
|
|
120
|
-
// ─── Load env file ───────────────────────────────────────────────
|
|
121
|
-
|
|
122
|
-
const envFile = flags.env
|
|
123
|
-
|| (mode === "remote" ? ".env.remote" : ".env");
|
|
124
|
-
if (fs.existsSync(envFile)) {
|
|
125
|
-
// Parse env file manually (KEY=VALUE lines)
|
|
126
|
-
const envContent = fs.readFileSync(envFile, "utf-8");
|
|
127
|
-
for (const line of envContent.split("\n")) {
|
|
128
|
-
const trimmed = line.trim();
|
|
129
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
130
|
-
const eqIdx = trimmed.indexOf("=");
|
|
131
|
-
if (eqIdx === -1) continue;
|
|
132
|
-
const key = trimmed.slice(0, eqIdx).trim();
|
|
133
|
-
const value = trimmed.slice(eqIdx + 1).trim();
|
|
134
|
-
// CLI flags take precedence, env file fills in gaps
|
|
135
|
-
if (!process.env[key]) {
|
|
136
|
-
process.env[key] = value;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// ─── Resolve system message ──────────────────────────────────────
|
|
142
|
-
|
|
143
|
-
const DEFAULT_SYSTEM_MESSAGE = `You are a helpful assistant running in a durable execution environment. Be concise.
|
|
144
|
-
|
|
145
|
-
CRITICAL RULES:
|
|
146
|
-
1. You have a 'wait' tool. You MUST use it whenever you need to wait, pause, sleep, delay, poll, check back later, schedule a future action, or implement any recurring/periodic task.
|
|
147
|
-
2. NEVER say you cannot wait or set timers. You CAN — use the 'wait' tool.
|
|
148
|
-
3. NEVER use bash sleep, setTimeout, setInterval, cron, or any other timing mechanism.
|
|
149
|
-
4. The 'wait' tool enables durable timers that survive process restarts and node migrations.
|
|
150
|
-
5. For recurring tasks: use the 'wait' tool in a loop — complete the action, then call wait(seconds), then repeat.`;
|
|
151
|
-
|
|
152
|
-
function resolveSystemMessage() {
|
|
153
|
-
// 1. CLI flag (string or file path)
|
|
154
|
-
if (flags.system) {
|
|
155
|
-
if (fs.existsSync(flags.system)) {
|
|
156
|
-
return fs.readFileSync(flags.system, "utf-8").trim();
|
|
157
|
-
}
|
|
158
|
-
return flags.system;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// 2. plugin/system.md (convention)
|
|
162
|
-
const pluginDir = resolvePluginDir();
|
|
163
|
-
if (pluginDir) {
|
|
164
|
-
const systemMd = path.join(pluginDir, "system.md");
|
|
165
|
-
if (fs.existsSync(systemMd)) {
|
|
166
|
-
return fs.readFileSync(systemMd, "utf-8").trim();
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// 3. SYSTEM_MESSAGE env var
|
|
171
|
-
if (process.env.SYSTEM_MESSAGE) {
|
|
172
|
-
return process.env.SYSTEM_MESSAGE;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// 4. Default
|
|
176
|
-
return DEFAULT_SYSTEM_MESSAGE;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function resolvePluginDir() {
|
|
180
|
-
if (flags.plugin) return path.resolve(flags.plugin);
|
|
181
|
-
if (process.env.PLUGIN_DIRS) {
|
|
182
|
-
const dirs = process.env.PLUGIN_DIRS.split(",").map(d => d.trim()).filter(Boolean);
|
|
183
|
-
return dirs[0] || null;
|
|
184
|
-
}
|
|
185
|
-
// Auto-detect: ./plugins in cwd, then bundled plugins in CLI package
|
|
186
|
-
const cwdPlugin = path.resolve("plugins");
|
|
187
|
-
if (fs.existsSync(cwdPlugin)) return cwdPlugin;
|
|
188
|
-
const bundledPlugin = path.join(pkgRoot, "plugins");
|
|
189
|
-
if (fs.existsSync(bundledPlugin)) return bundledPlugin;
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function resolveTuiBranding(pluginDir) {
|
|
194
|
-
const pluginMeta = readPluginMetadata(pluginDir);
|
|
195
|
-
const tui = pluginMeta?.tui;
|
|
196
|
-
let defaultSplash = "";
|
|
197
|
-
if (fs.existsSync(defaultTuiSplashPath)) {
|
|
198
|
-
defaultSplash = fs.readFileSync(defaultTuiSplashPath, "utf-8").trimEnd();
|
|
199
|
-
}
|
|
200
|
-
if (!tui || typeof tui !== "object") {
|
|
201
|
-
return {
|
|
202
|
-
title: "PilotSwarm",
|
|
203
|
-
splash: defaultSplash,
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const title = typeof tui.title === "string" && tui.title.trim()
|
|
208
|
-
? tui.title.trim()
|
|
209
|
-
: "PilotSwarm";
|
|
210
|
-
|
|
211
|
-
let splash = defaultSplash;
|
|
212
|
-
if (typeof tui.splash === "string" && tui.splash.trim()) {
|
|
213
|
-
splash = tui.splash;
|
|
214
|
-
} else if (typeof tui.splashFile === "string" && tui.splashFile.trim()) {
|
|
215
|
-
const splashPath = path.resolve(pluginDir, tui.splashFile);
|
|
216
|
-
if (!fs.existsSync(splashPath)) {
|
|
217
|
-
console.error(`TUI splash file not found: ${splashPath}`);
|
|
218
|
-
process.exit(1);
|
|
219
|
-
}
|
|
220
|
-
splash = fs.readFileSync(splashPath, "utf-8").trimEnd();
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return { title, splash };
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// ─── Build TUI config and set env vars ───────────────────────────
|
|
227
|
-
|
|
228
|
-
// Store
|
|
229
|
-
const store = flags.store || process.env.DATABASE_URL || "sqlite::memory:";
|
|
230
|
-
process.env.DATABASE_URL = store;
|
|
231
|
-
|
|
232
|
-
// Workers
|
|
233
|
-
if (mode === "remote") {
|
|
234
|
-
process.env.WORKERS = "0";
|
|
235
|
-
} else {
|
|
236
|
-
process.env.WORKERS = flags.workers ?? process.env.WORKERS ?? "4";
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Plugin dirs
|
|
240
|
-
const pluginDir = resolvePluginDir();
|
|
241
|
-
if (pluginDir) {
|
|
242
|
-
process.env.PLUGIN_DIRS = pluginDir;
|
|
243
|
-
}
|
|
244
|
-
const tuiBranding = resolveTuiBranding(pluginDir);
|
|
245
|
-
|
|
246
|
-
// Model
|
|
247
|
-
process.env.COPILOT_MODEL = flags.model || process.env.COPILOT_MODEL || "";
|
|
248
|
-
|
|
249
|
-
// Log level
|
|
250
|
-
process.env.LOG_LEVEL = flags["log-level"] || process.env.LOG_LEVEL || "";
|
|
251
|
-
|
|
252
|
-
// Context, namespace and label for remote mode (kubectl log streaming)
|
|
253
|
-
process.env.K8S_CONTEXT = flags.context || process.env.K8S_CONTEXT || "";
|
|
254
|
-
process.env.K8S_NAMESPACE = flags.namespace || process.env.K8S_NAMESPACE || "copilot-runtime";
|
|
255
|
-
process.env.K8S_POD_LABEL = flags.label || process.env.K8S_POD_LABEL || "app.kubernetes.io/component=worker";
|
|
256
|
-
|
|
257
|
-
// System message
|
|
258
|
-
process.env._TUI_SYSTEM_MESSAGE = resolveSystemMessage();
|
|
259
|
-
process.env._TUI_TITLE = tuiBranding.title;
|
|
260
|
-
process.env._TUI_SPLASH = tuiBranding.splash;
|
|
261
|
-
|
|
262
|
-
// ─── Load custom worker module (local mode only) ─────────────────
|
|
263
|
-
|
|
264
|
-
const workerModulePath = flags.worker || process.env.WORKER_MODULE || "";
|
|
265
|
-
if (mode === "local" && workerModulePath) {
|
|
266
|
-
const resolved = path.resolve(workerModulePath);
|
|
267
|
-
if (!fs.existsSync(resolved)) {
|
|
268
|
-
console.error(`Worker module not found: ${resolved}`);
|
|
269
|
-
process.exit(1);
|
|
270
|
-
}
|
|
271
|
-
process.env._TUI_WORKER_MODULE = resolved;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// ─── Launch TUI ──────────────────────────────────────────────────
|
|
275
|
-
|
|
276
|
-
// The TUI is the same file, but now reads config from env vars set above
|
|
277
|
-
// instead of relying on the user to set them manually.
|
|
278
|
-
const tuiPath = path.join(pkgRoot, "cli", "tui.js");
|
|
279
|
-
await import(tuiPath);
|
|
6
|
+
const config = parseCliIntoEnv(process.argv.slice(2));
|
|
7
|
+
await startTuiApp(config);
|
package/package.json
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pilotswarm-cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Terminal UI for
|
|
3
|
+
"version": "0.1.13",
|
|
4
|
+
"description": "Terminal UI for PilotSwarm.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"pilotswarm": "./bin/tui.js",
|
|
8
8
|
"pilotswarm-cli": "./bin/tui.js"
|
|
9
9
|
},
|
|
10
|
+
"main": "./src/index.js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./src/index.js"
|
|
13
|
+
},
|
|
10
14
|
"scripts": {
|
|
11
15
|
"build": "echo 'pilotswarm-cli: no build step (plain JS)'",
|
|
12
|
-
"
|
|
13
|
-
"
|
|
16
|
+
"start": "node ./bin/tui.js local --env ../../.env.remote",
|
|
17
|
+
"start:remote": "node ./bin/tui.js remote --env ../../.env.remote",
|
|
18
|
+
"clean": "true"
|
|
14
19
|
},
|
|
15
20
|
"keywords": [
|
|
16
21
|
"copilot",
|
|
@@ -30,19 +35,19 @@
|
|
|
30
35
|
"url": "https://github.com/affandar/PilotSwarm/issues"
|
|
31
36
|
},
|
|
32
37
|
"dependencies": {
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"neo-blessed": "^0.1.81"
|
|
38
|
+
"ink": "^6.8.0",
|
|
39
|
+
"pilotswarm-sdk": "^0.1.12",
|
|
40
|
+
"pilotswarm-ui-core": "0.1.0",
|
|
41
|
+
"pilotswarm-ui-react": "0.1.0",
|
|
42
|
+
"react": "^19.2.4"
|
|
39
43
|
},
|
|
40
44
|
"engines": {
|
|
41
45
|
"node": ">=24.0.0"
|
|
42
46
|
},
|
|
43
47
|
"files": [
|
|
44
48
|
"bin/**/*",
|
|
45
|
-
"
|
|
49
|
+
"src/**/*",
|
|
50
|
+
"tui-splash.txt",
|
|
46
51
|
"plugins/**/*",
|
|
47
52
|
"README.md"
|
|
48
53
|
]
|