pilotswarm-cli 0.1.13 → 0.1.15
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 +3 -0
- package/bin/tui.js +5 -2
- package/package.json +4 -3
- package/src/app.js +55 -0
- package/src/bootstrap-env.js +1 -37
- package/src/index.js +67 -0
- package/src/node-sdk-transport.js +422 -61
- package/src/platform.js +90 -35
- package/src/plugin-config.js +239 -0
- package/src/portal.js +7 -0
package/README.md
CHANGED
|
@@ -22,6 +22,9 @@ npx pilotswarm local --env .env --plugin ./plugin --worker ./worker-module.js
|
|
|
22
22
|
|
|
23
23
|
`pilotswarm-cli` provides the shipped TUI. Your app customizes it with `plugin/plugin.json`, `plugin/agents/*.agent.md`, `plugin/skills/*/SKILL.md`, and optional worker-side tools.
|
|
24
24
|
|
|
25
|
+
Portal/runtime helpers that are intentionally shared with `pilotswarm-web`
|
|
26
|
+
are exported from `pilotswarm-cli/portal`.
|
|
27
|
+
|
|
25
28
|
Common docs:
|
|
26
29
|
|
|
27
30
|
- CLI apps: `https://github.com/affandar/PilotSwarm/blob/main/docs/cli/building-cli-apps.md`
|
package/bin/tui.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
// Force the shipped TUI onto production React/Ink unless the caller
|
|
4
|
+
// explicitly opts into another environment for debugging.
|
|
5
|
+
process.env.NODE_ENV ??= "production";
|
|
5
6
|
|
|
7
|
+
const { parseCliIntoEnv } = await import("../src/bootstrap-env.js");
|
|
6
8
|
const config = parseCliIntoEnv(process.argv.slice(2));
|
|
9
|
+
const { startTuiApp } = await import("../src/index.js");
|
|
7
10
|
await startTuiApp(config);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pilotswarm-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Terminal UI for PilotSwarm.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"main": "./src/index.js",
|
|
11
11
|
"exports": {
|
|
12
|
-
".": "./src/index.js"
|
|
12
|
+
".": "./src/index.js",
|
|
13
|
+
"./portal": "./src/portal.js"
|
|
13
14
|
},
|
|
14
15
|
"scripts": {
|
|
15
16
|
"build": "echo 'pilotswarm-cli: no build step (plain JS)'",
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {
|
|
38
39
|
"ink": "^6.8.0",
|
|
39
|
-
"pilotswarm-sdk": "^0.1.
|
|
40
|
+
"pilotswarm-sdk": "^0.1.15",
|
|
40
41
|
"pilotswarm-ui-core": "0.1.0",
|
|
41
42
|
"pilotswarm-ui-react": "0.1.0",
|
|
42
43
|
"react": "^19.2.4"
|
package/src/app.js
CHANGED
|
@@ -231,6 +231,7 @@ export function PilotSwarmTuiApp({ controller, platform, onRequestExit }) {
|
|
|
231
231
|
const focus = controller.getState().ui.focusRegion;
|
|
232
232
|
const modal = controller.getState().ui.modal;
|
|
233
233
|
const inspectorTab = controller.getState().ui.inspectorTab;
|
|
234
|
+
const fullscreenPane = controller.getState().ui.fullscreenPane || null;
|
|
234
235
|
const plainShortcut = isPlainShortcutKey(key);
|
|
235
236
|
const matchesCtrlKey = (name, controlChar) => key.ctrl
|
|
236
237
|
&& (key.name === name || input === name || input === controlChar);
|
|
@@ -241,6 +242,7 @@ export function PilotSwarmTuiApp({ controller, platform, onRequestExit }) {
|
|
|
241
242
|
const isCtrlA = matchesCtrlKey("a", "\u0001");
|
|
242
243
|
const isShiftN = input === "N" || (key.shift && key.name === "n");
|
|
243
244
|
const isShiftD = input === "D" || (key.shift && key.name === "d");
|
|
245
|
+
const isShiftT = !key.ctrl && !key.meta && !key.alt && (input === "T" || (key.shift && key.name === "t"));
|
|
244
246
|
const isAltBackspace = key.meta && (key.backspace || key.delete || key.name === "backspace" || key.name === "delete");
|
|
245
247
|
const isAltLeftWord = key.meta && (key.leftArrow || key.name === "left" || input === "b" || input === "B");
|
|
246
248
|
const isAltRightWord = key.meta && (key.rightArrow || key.name === "right" || input === "f" || input === "F");
|
|
@@ -275,6 +277,10 @@ export function PilotSwarmTuiApp({ controller, platform, onRequestExit }) {
|
|
|
275
277
|
}
|
|
276
278
|
|
|
277
279
|
if (modal) {
|
|
280
|
+
if (isShiftT && modal.type === "themePicker") {
|
|
281
|
+
controller.handleCommand(UI_COMMANDS.CLOSE_MODAL).catch(() => {});
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
278
284
|
if (modal.type === "renameSession" || modal.type === "artifactUpload") {
|
|
279
285
|
if (key.escape) {
|
|
280
286
|
controller.handleCommand(UI_COMMANDS.CLOSE_MODAL).catch(() => {});
|
|
@@ -342,11 +348,19 @@ export function PilotSwarmTuiApp({ controller, platform, onRequestExit }) {
|
|
|
342
348
|
return;
|
|
343
349
|
}
|
|
344
350
|
|
|
351
|
+
if (focus !== "prompt" && isShiftT) {
|
|
352
|
+
controller.handleCommand(UI_COMMANDS.OPEN_THEME_PICKER).catch(() => {});
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
345
356
|
if (key.tab && key.shift) {
|
|
346
357
|
controller.handleCommand(UI_COMMANDS.FOCUS_PREV).catch(() => {});
|
|
347
358
|
return;
|
|
348
359
|
}
|
|
349
360
|
if (key.tab) {
|
|
361
|
+
if (focus === "prompt" && controller.acceptPromptReferenceAutocomplete()) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
350
364
|
controller.handleCommand(UI_COMMANDS.FOCUS_NEXT).catch(() => {});
|
|
351
365
|
return;
|
|
352
366
|
}
|
|
@@ -354,6 +368,15 @@ export function PilotSwarmTuiApp({ controller, platform, onRequestExit }) {
|
|
|
354
368
|
controller.handleCommand(UI_COMMANDS.TOGGLE_FILE_PREVIEW_FULLSCREEN).catch(() => {});
|
|
355
369
|
return;
|
|
356
370
|
}
|
|
371
|
+
if (key.escape && fullscreenPane) {
|
|
372
|
+
if (focus === "prompt") {
|
|
373
|
+
controller.setPrompt("");
|
|
374
|
+
controller.setFocus(fullscreenPane);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
controller.handleCommand(UI_COMMANDS.TOGGLE_PANE_FULLSCREEN).catch(() => {});
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
357
380
|
if (key.escape) {
|
|
358
381
|
if (focus === "prompt") {
|
|
359
382
|
controller.setPrompt("");
|
|
@@ -387,10 +410,34 @@ export function PilotSwarmTuiApp({ controller, platform, onRequestExit }) {
|
|
|
387
410
|
controller.handleCommand(UI_COMMANDS.OPEN_FILES_FILTER).catch(() => {});
|
|
388
411
|
return;
|
|
389
412
|
}
|
|
413
|
+
if (focus === "inspector" && inspectorTab === "files" && input === "u") {
|
|
414
|
+
controller.handleCommand(UI_COMMANDS.OPEN_ARTIFACT_UPLOAD).catch(() => {});
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (focus === "inspector" && inspectorTab === "files" && input === "a") {
|
|
418
|
+
controller.handleCommand(UI_COMMANDS.DOWNLOAD_SELECTED_FILE).catch(() => {});
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (focus === "inspector" && inspectorTab === "history" && input === "f") {
|
|
422
|
+
controller.handleCommand(UI_COMMANDS.OPEN_HISTORY_FORMAT).catch(() => {});
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (focus === "inspector" && inspectorTab === "history" && input === "r") {
|
|
426
|
+
controller.handleCommand(UI_COMMANDS.REFRESH_EXECUTION_HISTORY).catch(() => {});
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (focus === "inspector" && inspectorTab === "history" && input === "a") {
|
|
430
|
+
controller.handleCommand(UI_COMMANDS.EXPORT_EXECUTION_HISTORY).catch(() => {});
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
390
433
|
if (focus === "inspector" && inspectorTab === "files" && input === "v") {
|
|
391
434
|
controller.handleCommand(UI_COMMANDS.TOGGLE_FILE_PREVIEW_FULLSCREEN).catch(() => {});
|
|
392
435
|
return;
|
|
393
436
|
}
|
|
437
|
+
if (focus !== "prompt" && input === "v") {
|
|
438
|
+
controller.handleCommand(UI_COMMANDS.TOGGLE_PANE_FULLSCREEN).catch(() => {});
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
394
441
|
if (focus === "inspector" && inspectorTab === "files" && plainShortcut && input === "o") {
|
|
395
442
|
controller.handleCommand(UI_COMMANDS.OPEN_SELECTED_FILE).catch(() => {});
|
|
396
443
|
return;
|
|
@@ -414,6 +461,14 @@ export function PilotSwarmTuiApp({ controller, platform, onRequestExit }) {
|
|
|
414
461
|
controller.handleCommand(UI_COMMANDS.GROW_RIGHT_PANE).catch(() => {});
|
|
415
462
|
return;
|
|
416
463
|
}
|
|
464
|
+
if (focus !== "prompt" && input === "{") {
|
|
465
|
+
controller.handleCommand(UI_COMMANDS.SHRINK_SESSION_PANE).catch(() => {});
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (focus !== "prompt" && input === "}") {
|
|
469
|
+
controller.handleCommand(UI_COMMANDS.GROW_SESSION_PANE).catch(() => {});
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
417
472
|
|
|
418
473
|
if (focus === "prompt") {
|
|
419
474
|
if (isCtrlA) {
|
package/src/bootstrap-env.js
CHANGED
|
@@ -3,21 +3,10 @@ import { execFileSync } from "node:child_process";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { resolveTuiBranding } from "./plugin-config.js";
|
|
6
7
|
|
|
7
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
9
|
const pkgRoot = path.resolve(__dirname, "..");
|
|
9
|
-
const defaultTuiSplashPath = path.join(pkgRoot, "tui-splash.txt");
|
|
10
|
-
|
|
11
|
-
function readPluginMetadata(pluginDir) {
|
|
12
|
-
if (!pluginDir) return null;
|
|
13
|
-
const pluginJsonPath = path.join(pluginDir, "plugin.json");
|
|
14
|
-
if (!fs.existsSync(pluginJsonPath)) return null;
|
|
15
|
-
try {
|
|
16
|
-
return JSON.parse(fs.readFileSync(pluginJsonPath, "utf-8"));
|
|
17
|
-
} catch (error) {
|
|
18
|
-
throw new Error(`Failed to parse plugin metadata: ${pluginJsonPath}: ${error.message}`);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
10
|
|
|
22
11
|
function resolvePluginDir(flags) {
|
|
23
12
|
if (flags.plugin) return path.resolve(flags.plugin);
|
|
@@ -52,31 +41,6 @@ function resolveSystemMessage(flags) {
|
|
|
52
41
|
return undefined;
|
|
53
42
|
}
|
|
54
43
|
|
|
55
|
-
function resolveTuiBranding(pluginDir) {
|
|
56
|
-
const pluginMeta = readPluginMetadata(pluginDir);
|
|
57
|
-
const tui = pluginMeta?.tui;
|
|
58
|
-
let defaultSplash = "{bold}{cyan-fg}PilotSwarm{/cyan-fg}{/bold}";
|
|
59
|
-
if (fs.existsSync(defaultTuiSplashPath)) {
|
|
60
|
-
defaultSplash = fs.readFileSync(defaultTuiSplashPath, "utf-8").trimEnd();
|
|
61
|
-
}
|
|
62
|
-
if (!tui || typeof tui !== "object") {
|
|
63
|
-
return { title: "PilotSwarm", splash: defaultSplash };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const title = typeof tui.title === "string" && tui.title.trim() ? tui.title.trim() : "PilotSwarm";
|
|
67
|
-
let splash = defaultSplash;
|
|
68
|
-
if (typeof tui.splash === "string" && tui.splash.trim()) {
|
|
69
|
-
splash = tui.splash;
|
|
70
|
-
} else if (typeof tui.splashFile === "string" && tui.splashFile.trim()) {
|
|
71
|
-
const splashPath = path.resolve(pluginDir, tui.splashFile);
|
|
72
|
-
if (!fs.existsSync(splashPath)) {
|
|
73
|
-
throw new Error(`TUI splash file not found: ${splashPath}`);
|
|
74
|
-
}
|
|
75
|
-
splash = fs.readFileSync(splashPath, "utf-8").trimEnd();
|
|
76
|
-
}
|
|
77
|
-
return { title, splash };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
44
|
function loadEnvFile(envFile) {
|
|
81
45
|
if (!fs.existsSync(envFile)) return;
|
|
82
46
|
const envContent = fs.readFileSync(envFile, "utf-8");
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
3
5
|
import { createRequire } from "node:module";
|
|
4
6
|
import { render } from "ink";
|
|
5
7
|
import {
|
|
@@ -14,6 +16,26 @@ import { NodeSdkTransport } from "./node-sdk-transport.js";
|
|
|
14
16
|
|
|
15
17
|
const require = createRequire(import.meta.url);
|
|
16
18
|
|
|
19
|
+
const CONFIG_DIR = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "pilotswarm");
|
|
20
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
21
|
+
|
|
22
|
+
function readConfig() {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
|
|
25
|
+
} catch {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function writeConfig(patch) {
|
|
31
|
+
try {
|
|
32
|
+
const existing = readConfig();
|
|
33
|
+
const merged = { ...existing, ...patch };
|
|
34
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
35
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
36
|
+
} catch {}
|
|
37
|
+
}
|
|
38
|
+
|
|
17
39
|
function setupTuiHostRuntime() {
|
|
18
40
|
const logFile = "/tmp/duroxide-tui.log";
|
|
19
41
|
const originalConsole = {
|
|
@@ -90,9 +112,11 @@ export async function startTuiApp(config) {
|
|
|
90
112
|
store: config.store,
|
|
91
113
|
mode: config.mode,
|
|
92
114
|
});
|
|
115
|
+
const userConfig = readConfig();
|
|
93
116
|
const store = createStore(appReducer, createInitialState({
|
|
94
117
|
mode: config.mode,
|
|
95
118
|
branding: config.branding,
|
|
119
|
+
themeId: userConfig.themeId,
|
|
96
120
|
}));
|
|
97
121
|
const controller = new PilotSwarmUiController({ store, transport });
|
|
98
122
|
let tuiApp;
|
|
@@ -144,6 +168,49 @@ export async function startTuiApp(config) {
|
|
|
144
168
|
exitOnCtrlC: false,
|
|
145
169
|
});
|
|
146
170
|
|
|
171
|
+
// Listen for portal theme-change OSC sequences on stdin:
|
|
172
|
+
// \x1b]777;theme;<themeId>\x07
|
|
173
|
+
let oscBuffer = "";
|
|
174
|
+
const OSC_PREFIX = "\x1b]777;theme;";
|
|
175
|
+
const OSC_SUFFIX = "\x07";
|
|
176
|
+
const stdinThemeHandler = (data) => {
|
|
177
|
+
const str = typeof data === "string" ? data : data.toString("utf8");
|
|
178
|
+
oscBuffer += str;
|
|
179
|
+
while (oscBuffer.includes(OSC_PREFIX) && oscBuffer.includes(OSC_SUFFIX)) {
|
|
180
|
+
const start = oscBuffer.indexOf(OSC_PREFIX);
|
|
181
|
+
const end = oscBuffer.indexOf(OSC_SUFFIX, start);
|
|
182
|
+
if (end < 0) break;
|
|
183
|
+
const themeId = oscBuffer.slice(start + OSC_PREFIX.length, end);
|
|
184
|
+
oscBuffer = oscBuffer.slice(end + OSC_SUFFIX.length);
|
|
185
|
+
if (themeId) {
|
|
186
|
+
store.dispatch({ type: "ui/theme", themeId });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Prevent buffer from growing unbounded
|
|
190
|
+
if (oscBuffer.length > 1024) oscBuffer = oscBuffer.slice(-256);
|
|
191
|
+
};
|
|
192
|
+
process.stdin.on("data", stdinThemeHandler);
|
|
193
|
+
|
|
194
|
+
// Sync viewport on terminal resize (SIGWINCH)
|
|
195
|
+
const syncViewport = () => {
|
|
196
|
+
controller.setViewport({
|
|
197
|
+
width: process.stdout.columns || 120,
|
|
198
|
+
height: process.stdout.rows || 40,
|
|
199
|
+
});
|
|
200
|
+
};
|
|
201
|
+
syncViewport();
|
|
202
|
+
process.stdout.on("resize", syncViewport);
|
|
203
|
+
|
|
204
|
+
// Persist theme changes to config file
|
|
205
|
+
let lastPersistedThemeId = store.getState().ui.themeId;
|
|
206
|
+
store.subscribe(() => {
|
|
207
|
+
const currentThemeId = store.getState().ui.themeId;
|
|
208
|
+
if (currentThemeId && currentThemeId !== lastPersistedThemeId) {
|
|
209
|
+
lastPersistedThemeId = currentThemeId;
|
|
210
|
+
writeConfig({ themeId: currentThemeId });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
147
214
|
try {
|
|
148
215
|
await exitPromise;
|
|
149
216
|
} finally {
|