wispy-cli 2.6.2 → 2.7.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/bin/wispy.mjs +812 -38
- package/lib/commands/skills-cmd.mjs +117 -0
- package/lib/commands/trust.mjs +79 -1
- package/lib/commands/ws.mjs +81 -1
- package/lib/wispy-tui-clean.mjs +82 -0
- package/package.json +1 -1
package/bin/wispy.mjs
CHANGED
|
@@ -820,15 +820,60 @@ if (args[0] === "model") {
|
|
|
820
820
|
const config = await loadConfig();
|
|
821
821
|
const configuredProviders = config.providers ? Object.keys(config.providers) : (config.provider ? [config.provider] : []);
|
|
822
822
|
|
|
823
|
-
// wispy model (no args) —
|
|
823
|
+
// wispy model (no args) — interactive menu
|
|
824
824
|
if (!sub) {
|
|
825
825
|
const defaultP = config.defaultProvider ?? configuredProviders[0];
|
|
826
826
|
const currentModel = config.providers?.[defaultP]?.model ?? config.model ?? PROVIDERS[defaultP]?.defaultModel ?? "unknown";
|
|
827
827
|
console.log(`\n🤖 ${_bold("Current model")}: ${_cyan(defaultP)}:${_bold(currentModel)}\n`);
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
828
|
+
|
|
829
|
+
try {
|
|
830
|
+
const { select } = await import("@inquirer/prompts");
|
|
831
|
+
|
|
832
|
+
// Build choices from configured providers only
|
|
833
|
+
const modelChoices = [];
|
|
834
|
+
for (const p of configuredProviders) {
|
|
835
|
+
const models = KNOWN_MODELS[p] ?? [];
|
|
836
|
+
for (const m of models) {
|
|
837
|
+
const cur = config.providers?.[p]?.model ?? PROVIDERS[p]?.defaultModel;
|
|
838
|
+
modelChoices.push({
|
|
839
|
+
name: `${m} (${p})${cur === m ? _dim(" ✓ current") : ""}`,
|
|
840
|
+
value: `${p}:${m}`,
|
|
841
|
+
short: `${p}:${m}`,
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
if (modelChoices.length === 0) {
|
|
847
|
+
console.log(_dim(" No models available. Run 'wispy setup' to configure a provider.\n"));
|
|
848
|
+
process.exit(0);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
let modelChoice;
|
|
852
|
+
try {
|
|
853
|
+
modelChoice = await select({ message: "Switch model:", choices: modelChoices });
|
|
854
|
+
} catch (e) {
|
|
855
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
856
|
+
throw e;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const colonIdx = modelChoice.indexOf(":");
|
|
860
|
+
const provName = modelChoice.slice(0, colonIdx);
|
|
861
|
+
const modelName = modelChoice.slice(colonIdx + 1);
|
|
862
|
+
|
|
863
|
+
if (!config.providers) config.providers = {};
|
|
864
|
+
if (!config.providers[provName]) config.providers[provName] = {};
|
|
865
|
+
config.providers[provName].model = modelName;
|
|
866
|
+
if (!config.defaultProvider) config.defaultProvider = provName;
|
|
867
|
+
await saveConfig(config);
|
|
868
|
+
console.log(_green(`\n✅ Switched to ${_cyan(provName)}:${_bold(modelName)}\n`));
|
|
869
|
+
} catch (e) {
|
|
870
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
871
|
+
// Fallback — just show current
|
|
872
|
+
if (configuredProviders.length > 1) {
|
|
873
|
+
console.log(` Configured providers: ${configuredProviders.join(", ")}`);
|
|
874
|
+
console.log(` ${_dim("Use 'wispy model list' to see all options")}`);
|
|
875
|
+
console.log(` ${_dim("Use 'wispy model set <provider:model>' to switch")}\n`);
|
|
876
|
+
}
|
|
832
877
|
}
|
|
833
878
|
process.exit(0);
|
|
834
879
|
}
|
|
@@ -890,18 +935,66 @@ if (args[0] === "config") {
|
|
|
890
935
|
const config = await loadConfig();
|
|
891
936
|
|
|
892
937
|
if (!sub || sub === "show") {
|
|
893
|
-
// wispy config —
|
|
894
|
-
const
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
938
|
+
// wispy config — interactive config menu
|
|
939
|
+
const { select: cfgSelect } = await import("@inquirer/prompts");
|
|
940
|
+
|
|
941
|
+
// Show current status first
|
|
942
|
+
const providers = config.providers ? Object.keys(config.providers) : (config.provider ? [config.provider] : []);
|
|
943
|
+
const providerStr = providers.length > 0 ? providers.join(", ") : _dim("not set");
|
|
944
|
+
const defaultProv = config.defaultProvider ?? config.provider ?? _dim("not set");
|
|
945
|
+
const security = config.security ?? config.securityLevel ?? _dim("not set");
|
|
946
|
+
const language = config.language ?? _dim("auto");
|
|
947
|
+
const wsName = config.workstream ?? "default";
|
|
948
|
+
|
|
949
|
+
console.log(`\n${_bold("Wispy Config")}\n`);
|
|
950
|
+
console.log(` Providers: ${providerStr}`);
|
|
951
|
+
console.log(` Default: ${defaultProv}`);
|
|
952
|
+
console.log(` Security: ${security}`);
|
|
953
|
+
console.log(` Language: ${language}`);
|
|
954
|
+
console.log(` Workstream: ${wsName}`);
|
|
955
|
+
console.log(` Config file: ${_dim(CONFIG_PATH)}`);
|
|
956
|
+
console.log("");
|
|
957
|
+
|
|
958
|
+
const action = await cfgSelect({
|
|
959
|
+
message: "What do you want to change?",
|
|
960
|
+
choices: [
|
|
961
|
+
{ name: "Providers — add/remove AI providers", value: "provider" },
|
|
962
|
+
{ name: "Security — change trust level", value: "security" },
|
|
963
|
+
{ name: "Language — set preferred language", value: "language" },
|
|
964
|
+
{ name: "Channels — configure messaging bots", value: "channels" },
|
|
965
|
+
{ name: "Server — cloud/server settings", value: "server" },
|
|
966
|
+
{ name: "View raw config (JSON)", value: "raw" },
|
|
967
|
+
{ name: "Reset everything", value: "reset" },
|
|
968
|
+
{ name: "Done", value: "done" },
|
|
969
|
+
],
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
if (action === "done") {
|
|
973
|
+
process.exit(0);
|
|
974
|
+
} else if (action === "raw") {
|
|
975
|
+
const display = JSON.parse(JSON.stringify(config));
|
|
976
|
+
if (display.providers) {
|
|
977
|
+
for (const [k, v] of Object.entries(display.providers)) {
|
|
978
|
+
if (v.apiKey) v.apiKey = v.apiKey.slice(0, 6) + "..." + v.apiKey.slice(-4);
|
|
979
|
+
}
|
|
899
980
|
}
|
|
981
|
+
console.log(JSON.stringify(display, null, 2));
|
|
982
|
+
} else if (action === "reset") {
|
|
983
|
+
const { confirm: cfgConfirm } = await import("@inquirer/prompts");
|
|
984
|
+
const yes = await cfgConfirm({ message: "Reset all configuration?", default: false });
|
|
985
|
+
if (yes) {
|
|
986
|
+
const { writeFile: wf } = await import("node:fs/promises");
|
|
987
|
+
await wf(CONFIG_PATH, "{}\n");
|
|
988
|
+
console.log(`${_green("✓")} Config reset. Run 'wispy setup' to reconfigure.`);
|
|
989
|
+
}
|
|
990
|
+
} else {
|
|
991
|
+
// Delegate to setup wizard step
|
|
992
|
+
const { OnboardingWizard } = await import(
|
|
993
|
+
path.join(__dirname, "..", "core", "onboarding.mjs")
|
|
994
|
+
);
|
|
995
|
+
const wizard = new OnboardingWizard();
|
|
996
|
+
await wizard.runStep(action);
|
|
900
997
|
}
|
|
901
|
-
if (display.apiKey) display.apiKey = display.apiKey.slice(0, 6) + "..." + display.apiKey.slice(-4);
|
|
902
|
-
console.log(`\n${_bold("Wispy Config")} ${_dim(CONFIG_PATH)}\n`);
|
|
903
|
-
console.log(JSON.stringify(display, null, 2));
|
|
904
|
-
console.log("");
|
|
905
998
|
|
|
906
999
|
} else if (sub === "get") {
|
|
907
1000
|
// wispy config get <key>
|
|
@@ -1128,6 +1221,54 @@ if (args[0] === "sync") {
|
|
|
1128
1221
|
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
1129
1222
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
1130
1223
|
|
|
1224
|
+
// Interactive sync menu when no subcommand
|
|
1225
|
+
if (!sub) {
|
|
1226
|
+
try {
|
|
1227
|
+
const { select, Separator } = await import("@inquirer/prompts");
|
|
1228
|
+
const cfg = await SyncManager.loadConfig();
|
|
1229
|
+
const remoteUrl = cfg.remoteUrl;
|
|
1230
|
+
|
|
1231
|
+
if (!remoteUrl) {
|
|
1232
|
+
console.log(_dim("\nNo remote configured. Connect first: wispy connect <url>\n"));
|
|
1233
|
+
process.exit(0);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
console.log(`\nRemote: ${_cyan(remoteUrl)} ✓\n`);
|
|
1237
|
+
|
|
1238
|
+
let syncAction;
|
|
1239
|
+
try {
|
|
1240
|
+
syncAction = await select({
|
|
1241
|
+
message: "Sync actions:",
|
|
1242
|
+
choices: [
|
|
1243
|
+
{ name: "Sync now (bidirectional)", value: "sync" },
|
|
1244
|
+
{ name: "Push local → remote", value: "push" },
|
|
1245
|
+
{ name: "Pull remote → local", value: "pull" },
|
|
1246
|
+
{ name: "View what would sync (status)", value: "status" },
|
|
1247
|
+
{ name: "Enable auto-sync", value: "auto" },
|
|
1248
|
+
{ name: "Disconnect remote", value: "disconnect" },
|
|
1249
|
+
],
|
|
1250
|
+
});
|
|
1251
|
+
} catch (e) {
|
|
1252
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
1253
|
+
throw e;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
if (["sync", "push", "pull", "status", "auto"].includes(syncAction)) {
|
|
1257
|
+
args[1] = syncAction;
|
|
1258
|
+
} else if (syncAction === "disconnect") {
|
|
1259
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
1260
|
+
let ok;
|
|
1261
|
+
try { ok = await confirm({ message: "Disconnect remote?", default: false }); }
|
|
1262
|
+
catch (e) { if (e.name === "ExitPromptError") { process.exit(130); } throw e; }
|
|
1263
|
+
if (ok) { await SyncManager.disableAuto?.(); console.log(_dim("Remote disconnected.")); }
|
|
1264
|
+
process.exit(0);
|
|
1265
|
+
}
|
|
1266
|
+
} catch (e) {
|
|
1267
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
1268
|
+
// Fall through
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1131
1272
|
// Parse flags
|
|
1132
1273
|
const strategyIdx = args.indexOf("--strategy");
|
|
1133
1274
|
const strategy = strategyIdx !== -1 ? args[strategyIdx + 1] : null;
|
|
@@ -1256,6 +1397,93 @@ if (args[0] === "deploy") {
|
|
|
1256
1397
|
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
1257
1398
|
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
1258
1399
|
|
|
1400
|
+
if (!sub) {
|
|
1401
|
+
// Interactive deploy menu
|
|
1402
|
+
try {
|
|
1403
|
+
const { select, Separator } = await import("@inquirer/prompts");
|
|
1404
|
+
let deployTarget;
|
|
1405
|
+
try {
|
|
1406
|
+
deployTarget = await select({
|
|
1407
|
+
message: "Deploy wispy to:",
|
|
1408
|
+
choices: [
|
|
1409
|
+
{ name: "VPS (SSH + systemd)", value: "vps" },
|
|
1410
|
+
{ name: "Docker (Dockerfile + compose)", value: "docker" },
|
|
1411
|
+
{ name: "Railway", value: "railway" },
|
|
1412
|
+
{ name: "Fly.io", value: "fly" },
|
|
1413
|
+
{ name: "Render", value: "render" },
|
|
1414
|
+
{ name: "Modal (serverless)", value: "modal" },
|
|
1415
|
+
{ name: "Daytona", value: "daytona" },
|
|
1416
|
+
new Separator("──────────"),
|
|
1417
|
+
{ name: "Generate all configs (deploy init)", value: "init" },
|
|
1418
|
+
{ name: "Check remote status", value: "status-check" },
|
|
1419
|
+
],
|
|
1420
|
+
});
|
|
1421
|
+
} catch (e) {
|
|
1422
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
1423
|
+
throw e;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
if (deployTarget === "vps") {
|
|
1427
|
+
const { input } = await import("@inquirer/prompts");
|
|
1428
|
+
let target;
|
|
1429
|
+
try { target = await input({ message: "SSH target (user@host):" }); }
|
|
1430
|
+
catch (e) { if (e.name === "ExitPromptError") { process.exit(130); } throw e; }
|
|
1431
|
+
if (target && target.trim()) {
|
|
1432
|
+
try { await dm.deployVPS({ target: target.trim() }); }
|
|
1433
|
+
catch (err) { console.error(_red(`\n❌ Deploy failed: ${err.message}`)); process.exit(1); }
|
|
1434
|
+
}
|
|
1435
|
+
} else if (deployTarget === "docker") {
|
|
1436
|
+
const created = await dm.init(process.cwd());
|
|
1437
|
+
for (const f of created) console.log(` ${f.includes("skipped") ? "⏭️ " : "✅"} ${f}`);
|
|
1438
|
+
console.log(_dim("\n Next: docker-compose up -d\n"));
|
|
1439
|
+
} else if (deployTarget === "railway") {
|
|
1440
|
+
process.stdout.write(dm.generateRailwayConfig() + "\n");
|
|
1441
|
+
} else if (deployTarget === "fly") {
|
|
1442
|
+
process.stdout.write(dm.generateFlyConfig());
|
|
1443
|
+
} else if (deployTarget === "render") {
|
|
1444
|
+
process.stdout.write(dm.generateRenderConfig());
|
|
1445
|
+
} else if (deployTarget === "modal") {
|
|
1446
|
+
process.stdout.write(dm.generateModalConfig());
|
|
1447
|
+
} else if (deployTarget === "daytona") {
|
|
1448
|
+
const { mkdir: mkdDir, writeFile: wfDir } = await import("node:fs/promises");
|
|
1449
|
+
const daytonaDir = path.join(process.cwd(), ".daytona");
|
|
1450
|
+
await mkdDir(daytonaDir, { recursive: true });
|
|
1451
|
+
const daytonaConfigPath = path.join(daytonaDir, "config.yaml");
|
|
1452
|
+
let exists = false;
|
|
1453
|
+
try { await (await import("node:fs/promises")).access(daytonaConfigPath); exists = true; } catch {}
|
|
1454
|
+
if (!exists) {
|
|
1455
|
+
await wfDir(daytonaConfigPath, dm.generateDaytonaConfig(), "utf8");
|
|
1456
|
+
console.log(_green(`✅ Created .daytona/config.yaml`));
|
|
1457
|
+
} else {
|
|
1458
|
+
console.log(_yellow(`⏭️ .daytona/config.yaml already exists`));
|
|
1459
|
+
}
|
|
1460
|
+
} else if (deployTarget === "init") {
|
|
1461
|
+
console.log("\n🌿 Initializing wispy deploy configs...\n");
|
|
1462
|
+
const created = await dm.init(process.cwd());
|
|
1463
|
+
for (const f of created) console.log(` ${f.includes("skipped") ? "⏭️ " : "✅"} ${f}`);
|
|
1464
|
+
} else if (deployTarget === "status-check") {
|
|
1465
|
+
const { input } = await import("@inquirer/prompts");
|
|
1466
|
+
let url;
|
|
1467
|
+
try { url = await input({ message: "Remote URL to check:" }); }
|
|
1468
|
+
catch (e) { if (e.name === "ExitPromptError") { process.exit(130); } throw e; }
|
|
1469
|
+
if (url && url.trim()) {
|
|
1470
|
+
process.stdout.write(`\n📡 Checking ${_cyan(url.trim())}... `);
|
|
1471
|
+
const status = await dm.checkRemote(url.trim());
|
|
1472
|
+
if (status.alive) {
|
|
1473
|
+
console.log(_green("✓ alive"));
|
|
1474
|
+
if (status.version) console.log(` Version: ${status.version}`);
|
|
1475
|
+
} else {
|
|
1476
|
+
console.log(_red("✗ unreachable"));
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
process.exit(0);
|
|
1481
|
+
} catch (e) {
|
|
1482
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
1483
|
+
// Fall through to help
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1259
1487
|
if (sub === "dockerfile") {
|
|
1260
1488
|
process.stdout.write(dm.generateDockerfile());
|
|
1261
1489
|
process.exit(0);
|
|
@@ -1399,7 +1627,56 @@ if (args[0] === "migrate") {
|
|
|
1399
1627
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
1400
1628
|
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
1401
1629
|
|
|
1402
|
-
if (!sub
|
|
1630
|
+
if (!sub) {
|
|
1631
|
+
// Interactive migrate menu
|
|
1632
|
+
try {
|
|
1633
|
+
const { select } = await import("@inquirer/prompts");
|
|
1634
|
+
const { homedir } = await import("node:os");
|
|
1635
|
+
const { existsSync } = await import("node:fs");
|
|
1636
|
+
const { join } = await import("node:path");
|
|
1637
|
+
|
|
1638
|
+
const hasOpenClaw = existsSync(join(homedir(), ".openclaw"));
|
|
1639
|
+
const choices = [
|
|
1640
|
+
{
|
|
1641
|
+
name: `OpenClaw${hasOpenClaw ? green(" (detected at ~/.openclaw)") : dim(" (not found)")}`,
|
|
1642
|
+
value: "openclaw",
|
|
1643
|
+
},
|
|
1644
|
+
{ name: "Hermes Agent", value: "hermes" },
|
|
1645
|
+
{ name: "Manual import (JSON/YAML)", value: "manual" },
|
|
1646
|
+
];
|
|
1647
|
+
|
|
1648
|
+
let migrateFrom;
|
|
1649
|
+
try {
|
|
1650
|
+
migrateFrom = await select({ message: "Migrate from:", choices });
|
|
1651
|
+
} catch (e) {
|
|
1652
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
1653
|
+
throw e;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
if (migrateFrom === "openclaw") {
|
|
1657
|
+
args[1] = "openclaw";
|
|
1658
|
+
// Fall through
|
|
1659
|
+
} else if (migrateFrom === "hermes") {
|
|
1660
|
+
console.log(dim("\nHermes migration coming soon. Use manual import for now.\n"));
|
|
1661
|
+
process.exit(0);
|
|
1662
|
+
} else if (migrateFrom === "manual") {
|
|
1663
|
+
const { input } = await import("@inquirer/prompts");
|
|
1664
|
+
let filePath;
|
|
1665
|
+
try { filePath = await input({ message: "Path to JSON/YAML file:" }); }
|
|
1666
|
+
catch (e) { if (e.name === "ExitPromptError") { process.exit(130); } throw e; }
|
|
1667
|
+
console.log(dim(`\nManual import from ${filePath} — coming soon.\n`));
|
|
1668
|
+
process.exit(0);
|
|
1669
|
+
}
|
|
1670
|
+
} catch (e) {
|
|
1671
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
1672
|
+
args[1] = "openclaw";
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
const sub2 = args[1]; // re-read after interactive
|
|
1677
|
+
|
|
1678
|
+
if (!sub || sub === "openclaw" || sub2 === "openclaw") {
|
|
1679
|
+
const subToUse = sub || sub2;
|
|
1403
1680
|
console.log(`\n🌿 ${bold("Wispy Migration Tool")} ${dim("— from OpenClaw")}\n`);
|
|
1404
1681
|
if (dryRun) console.log(yellow(" [DRY RUN — no files will be written]\n"));
|
|
1405
1682
|
if (memoryOnly) console.log(dim(" [memory-only mode]\n"));
|
|
@@ -1459,6 +1736,108 @@ if (args[0] === "cron") {
|
|
|
1459
1736
|
const cron = new CronManager(WISPY_DIR, engine);
|
|
1460
1737
|
await cron.init();
|
|
1461
1738
|
|
|
1739
|
+
if (!sub) {
|
|
1740
|
+
// Interactive cron menu
|
|
1741
|
+
try {
|
|
1742
|
+
const { select, Separator } = await import("@inquirer/prompts");
|
|
1743
|
+
const jobs = cron.list();
|
|
1744
|
+
|
|
1745
|
+
const choices = [];
|
|
1746
|
+
if (jobs.length === 0) {
|
|
1747
|
+
choices.push(new Separator(_dim("No jobs configured.")));
|
|
1748
|
+
} else {
|
|
1749
|
+
for (const j of jobs) {
|
|
1750
|
+
const schedStr = j.schedule.kind === "cron" ? j.schedule.expr
|
|
1751
|
+
: j.schedule.kind === "every" ? `every ${j.schedule.ms / 60000}min`
|
|
1752
|
+
: `at ${j.schedule.time}`;
|
|
1753
|
+
const lastRun = j.lastRun ? `last: ${_dim(formatCronRelative(j.lastRun))} ✓` : _dim("never run");
|
|
1754
|
+
choices.push({
|
|
1755
|
+
name: `${j.name} — ${schedStr} — ${lastRun}`,
|
|
1756
|
+
value: { type: "job", id: j.id, name: j.name },
|
|
1757
|
+
short: j.name,
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
choices.push(new Separator("──────────"));
|
|
1762
|
+
choices.push({ name: "Add a new job", value: { type: "add" } });
|
|
1763
|
+
choices.push({ name: "Start scheduler", value: { type: "start" } });
|
|
1764
|
+
|
|
1765
|
+
function formatCronRelative(ts) {
|
|
1766
|
+
if (!ts) return "never";
|
|
1767
|
+
const diffMs = Date.now() - new Date(ts).getTime();
|
|
1768
|
+
const diffMin = Math.floor(diffMs / 60000);
|
|
1769
|
+
const diffH = Math.floor(diffMin / 60);
|
|
1770
|
+
if (diffMin < 1) return "just now";
|
|
1771
|
+
if (diffMin < 60) return `${diffMin}min ago`;
|
|
1772
|
+
if (diffH < 24) return `${diffH}hr ago`;
|
|
1773
|
+
return `${Math.floor(diffH / 24)}d ago`;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
let answer;
|
|
1777
|
+
try {
|
|
1778
|
+
answer = await select({ message: "Cron jobs:", choices });
|
|
1779
|
+
} catch (e) {
|
|
1780
|
+
if (e.name === "ExitPromptError") { engine.destroy?.(); process.exit(130); }
|
|
1781
|
+
throw e;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
if (answer.type === "add") {
|
|
1785
|
+
// fall through to the "add" sub-command handler below
|
|
1786
|
+
args[1] = "add";
|
|
1787
|
+
} else if (answer.type === "start") {
|
|
1788
|
+
args[1] = "start";
|
|
1789
|
+
} else if (answer.type === "job") {
|
|
1790
|
+
// Job sub-menu
|
|
1791
|
+
let jobAction;
|
|
1792
|
+
try {
|
|
1793
|
+
jobAction = await select({
|
|
1794
|
+
message: `${answer.name}:`,
|
|
1795
|
+
choices: [
|
|
1796
|
+
{ name: "Run now", value: "run" },
|
|
1797
|
+
{ name: "Edit (not yet implemented)", value: "edit" },
|
|
1798
|
+
{ name: "Remove", value: "remove" },
|
|
1799
|
+
{ name: "View history", value: "history" },
|
|
1800
|
+
],
|
|
1801
|
+
});
|
|
1802
|
+
} catch (e) {
|
|
1803
|
+
if (e.name === "ExitPromptError") { engine.destroy?.(); process.exit(130); }
|
|
1804
|
+
throw e;
|
|
1805
|
+
}
|
|
1806
|
+
if (jobAction === "run") {
|
|
1807
|
+
console.log(`🌿 Running job: ${answer.name}...`);
|
|
1808
|
+
const result = await cron.runNow(answer.id);
|
|
1809
|
+
console.log(result.output ?? result.error);
|
|
1810
|
+
} else if (jobAction === "remove") {
|
|
1811
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
1812
|
+
let ok;
|
|
1813
|
+
try { ok = await confirm({ message: `Remove job '${answer.name}'?`, default: false }); }
|
|
1814
|
+
catch (e) { if (e.name === "ExitPromptError") { engine.destroy?.(); process.exit(130); } throw e; }
|
|
1815
|
+
if (ok) {
|
|
1816
|
+
await cron.remove(answer.id);
|
|
1817
|
+
console.log(_green(`✅ Removed job: ${answer.name}`));
|
|
1818
|
+
}
|
|
1819
|
+
} else if (jobAction === "history") {
|
|
1820
|
+
const history = await cron.getHistory(answer.id);
|
|
1821
|
+
console.log(`\n📋 History for "${answer.name}" (last ${history.length} runs):\n`);
|
|
1822
|
+
for (const h of history) {
|
|
1823
|
+
const icon = h.status === "success" ? "✅" : "❌";
|
|
1824
|
+
console.log(` ${icon} ${new Date(h.startedAt).toLocaleString()}`);
|
|
1825
|
+
console.log(` ${h.output?.slice(0, 100) ?? h.error ?? ""}`);
|
|
1826
|
+
}
|
|
1827
|
+
if (history.length === 0) console.log(" No runs yet.");
|
|
1828
|
+
} else if (jobAction === "edit") {
|
|
1829
|
+
console.log(_dim("Edit via: wispy cron add (then remove the old one)"));
|
|
1830
|
+
}
|
|
1831
|
+
engine.destroy?.();
|
|
1832
|
+
process.exit(0);
|
|
1833
|
+
}
|
|
1834
|
+
} catch (e) {
|
|
1835
|
+
if (e.name === "ExitPromptError") { engine.destroy?.(); process.exit(130); }
|
|
1836
|
+
// Fallback to list
|
|
1837
|
+
args[1] = "list";
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1462
1841
|
if (!sub || sub === "list") {
|
|
1463
1842
|
const jobs = cron.list();
|
|
1464
1843
|
if (jobs.length === 0) {
|
|
@@ -1605,6 +1984,68 @@ if (args[0] === "audit" || args[0] === "log") {
|
|
|
1605
1984
|
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
1606
1985
|
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
1607
1986
|
|
|
1987
|
+
// Interactive audit menu when no subcommand
|
|
1988
|
+
if (!sub) {
|
|
1989
|
+
try {
|
|
1990
|
+
const { select } = await import("@inquirer/prompts");
|
|
1991
|
+
let auditAction;
|
|
1992
|
+
try {
|
|
1993
|
+
auditAction = await select({
|
|
1994
|
+
message: "Audit log:",
|
|
1995
|
+
choices: [
|
|
1996
|
+
{ name: "View recent events", value: "recent" },
|
|
1997
|
+
{ name: "Filter by tool", value: "by-tool" },
|
|
1998
|
+
{ name: "Filter by session", value: "by-session" },
|
|
1999
|
+
{ name: "Today's events only", value: "today" },
|
|
2000
|
+
{ name: "Replay a session", value: "replay" },
|
|
2001
|
+
{ name: "Export as markdown", value: "export-md" },
|
|
2002
|
+
],
|
|
2003
|
+
});
|
|
2004
|
+
} catch (e) {
|
|
2005
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
2006
|
+
throw e;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
if (auditAction === "recent") {
|
|
2010
|
+
// Fall through with no filter
|
|
2011
|
+
} else if (auditAction === "by-tool") {
|
|
2012
|
+
const { input } = await import("@inquirer/prompts");
|
|
2013
|
+
let tool;
|
|
2014
|
+
try { tool = await input({ message: "Tool name:" }); }
|
|
2015
|
+
catch (e) { if (e.name === "ExitPromptError") { process.exit(130); } throw e; }
|
|
2016
|
+
if (tool && tool.trim()) args.push("--tool", tool.trim());
|
|
2017
|
+
} else if (auditAction === "by-session") {
|
|
2018
|
+
const { input } = await import("@inquirer/prompts");
|
|
2019
|
+
let sid;
|
|
2020
|
+
try { sid = await input({ message: "Session ID:" }); }
|
|
2021
|
+
catch (e) { if (e.name === "ExitPromptError") { process.exit(130); } throw e; }
|
|
2022
|
+
if (sid && sid.trim()) args.push("--session", sid.trim());
|
|
2023
|
+
} else if (auditAction === "today") {
|
|
2024
|
+
args.push("--today");
|
|
2025
|
+
} else if (auditAction === "replay") {
|
|
2026
|
+
const { input } = await import("@inquirer/prompts");
|
|
2027
|
+
let sid;
|
|
2028
|
+
try { sid = await input({ message: "Session ID to replay:" }); }
|
|
2029
|
+
catch (e) { if (e.name === "ExitPromptError") { process.exit(130); } throw e; }
|
|
2030
|
+
if (sid && sid.trim()) {
|
|
2031
|
+
args[1] = "replay";
|
|
2032
|
+
args[2] = sid.trim();
|
|
2033
|
+
}
|
|
2034
|
+
} else if (auditAction === "export-md") {
|
|
2035
|
+
const content = await audit.exportMarkdown();
|
|
2036
|
+
const ts = new Date().toISOString().slice(0, 10);
|
|
2037
|
+
const outFile = `wispy-audit-${ts}.md`;
|
|
2038
|
+
const { writeFile: wf } = await import("node:fs/promises");
|
|
2039
|
+
await wf(outFile, content, "utf8");
|
|
2040
|
+
console.log(green(`✅ Exported to ${outFile}`));
|
|
2041
|
+
process.exit(0);
|
|
2042
|
+
}
|
|
2043
|
+
} catch (e) {
|
|
2044
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
2045
|
+
// Fall through to regular display
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
|
|
1608
2049
|
function formatEvent(evt) {
|
|
1609
2050
|
const ts = new Date(evt.timestamp).toLocaleTimeString();
|
|
1610
2051
|
const icons = {
|
|
@@ -1766,6 +2207,95 @@ if (args[0] === "node") {
|
|
|
1766
2207
|
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
1767
2208
|
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
1768
2209
|
|
|
2210
|
+
if (!sub) {
|
|
2211
|
+
// Interactive node menu
|
|
2212
|
+
try {
|
|
2213
|
+
const { select, Separator } = await import("@inquirer/prompts");
|
|
2214
|
+
const nodeList = await nodes.list();
|
|
2215
|
+
|
|
2216
|
+
const choices = [];
|
|
2217
|
+
if (nodeList.length === 0) {
|
|
2218
|
+
choices.push(new Separator(dim("No nodes connected.")));
|
|
2219
|
+
} else {
|
|
2220
|
+
const results = await nodes.status().catch(() => []);
|
|
2221
|
+
for (const n of nodeList) {
|
|
2222
|
+
const statusInfo = results.find(r => r.id === n.id);
|
|
2223
|
+
const alive = statusInfo?.alive;
|
|
2224
|
+
const statusStr = alive ? green("● online") : dim("● offline");
|
|
2225
|
+
choices.push({
|
|
2226
|
+
name: `${n.name} — ${n.host}:${n.port} — ${statusStr}`,
|
|
2227
|
+
value: { type: "node", id: n.id, name: n.name },
|
|
2228
|
+
short: n.name,
|
|
2229
|
+
});
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
choices.push(new Separator("──────────"));
|
|
2233
|
+
choices.push({ name: "Pair a new node (generate code)", value: { type: "pair" } });
|
|
2234
|
+
choices.push({ name: "Connect to a node", value: { type: "connect" } });
|
|
2235
|
+
|
|
2236
|
+
let answer;
|
|
2237
|
+
try {
|
|
2238
|
+
answer = await select({ message: "Nodes:", choices });
|
|
2239
|
+
} catch (e) {
|
|
2240
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
2241
|
+
throw e;
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
if (answer.type === "pair") {
|
|
2245
|
+
const code = await nodes.generatePairCode();
|
|
2246
|
+
console.log(`\n🔗 Pairing Code: ${bold(green(code))}\n`);
|
|
2247
|
+
console.log(` This code expires in 1 hour.`);
|
|
2248
|
+
console.log(`\n On the remote machine:\n ${cyan(`wispy node connect ${code} --url http://localhost:18790`)}\n`);
|
|
2249
|
+
} else if (answer.type === "connect") {
|
|
2250
|
+
const { input } = await import("@inquirer/prompts");
|
|
2251
|
+
let code, url;
|
|
2252
|
+
try {
|
|
2253
|
+
code = await input({ message: "Pairing code:" });
|
|
2254
|
+
url = await input({ message: "Server URL:", default: "http://localhost:18790" });
|
|
2255
|
+
} catch (e) {
|
|
2256
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
2257
|
+
throw e;
|
|
2258
|
+
}
|
|
2259
|
+
if (code && code.trim()) args[1] = "connect", args[2] = code.trim(), args.push("--url"), args.push(url || "http://localhost:18790");
|
|
2260
|
+
else process.exit(0);
|
|
2261
|
+
} else if (answer.type === "node") {
|
|
2262
|
+
let nodeAction;
|
|
2263
|
+
try {
|
|
2264
|
+
nodeAction = await select({
|
|
2265
|
+
message: `${answer.name}:`,
|
|
2266
|
+
choices: [
|
|
2267
|
+
{ name: "Ping", value: "ping" },
|
|
2268
|
+
{ name: "Remove", value: "remove" },
|
|
2269
|
+
{ name: "Execute command", value: "exec" },
|
|
2270
|
+
],
|
|
2271
|
+
});
|
|
2272
|
+
} catch (e) {
|
|
2273
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
2274
|
+
throw e;
|
|
2275
|
+
}
|
|
2276
|
+
if (nodeAction === "ping") {
|
|
2277
|
+
const results = await nodes.status();
|
|
2278
|
+
const r = results.find(x => x.id === answer.id);
|
|
2279
|
+
if (r?.alive) console.log(green(`● ${answer.name} — alive (${r.latency ?? "?"}ms)`));
|
|
2280
|
+
else console.log(red(`● ${answer.name} — unreachable`));
|
|
2281
|
+
} else if (nodeAction === "remove") {
|
|
2282
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
2283
|
+
let ok;
|
|
2284
|
+
try { ok = await confirm({ message: `Remove node '${answer.name}'?`, default: false }); }
|
|
2285
|
+
catch (e) { if (e.name === "ExitPromptError") { process.exit(130); } throw e; }
|
|
2286
|
+
if (ok) { await nodes.remove(answer.id); console.log(green(`✅ Removed node: ${answer.name}`)); }
|
|
2287
|
+
} else if (nodeAction === "exec") {
|
|
2288
|
+
console.log(dim("Execute via: wispy node exec <id> <command> (coming soon)"));
|
|
2289
|
+
}
|
|
2290
|
+
process.exit(0);
|
|
2291
|
+
}
|
|
2292
|
+
process.exit(0);
|
|
2293
|
+
} catch (e) {
|
|
2294
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
2295
|
+
// Fall through to help
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
|
|
1769
2299
|
if (sub === "pair") {
|
|
1770
2300
|
const code = await nodes.generatePairCode();
|
|
1771
2301
|
console.log(`\n🔗 Pairing Code: ${bold(green(code))}\n`);
|
|
@@ -1896,6 +2426,107 @@ if (args[0] === "channel") {
|
|
|
1896
2426
|
await channelList();
|
|
1897
2427
|
} else if (sub === "test" && name) {
|
|
1898
2428
|
await channelTest(name);
|
|
2429
|
+
} else if (!sub) {
|
|
2430
|
+
// Interactive channel menu
|
|
2431
|
+
try {
|
|
2432
|
+
const { select, Separator } = await import("@inquirer/prompts");
|
|
2433
|
+
const { homedir } = await import("node:os");
|
|
2434
|
+
const { readFile } = await import("node:fs/promises");
|
|
2435
|
+
const { join } = await import("node:path");
|
|
2436
|
+
|
|
2437
|
+
const channelDefs = [
|
|
2438
|
+
{ key: "telegram", label: "Telegram" },
|
|
2439
|
+
{ key: "discord", label: "Discord" },
|
|
2440
|
+
{ key: "slack", label: "Slack" },
|
|
2441
|
+
{ key: "whatsapp", label: "WhatsApp" },
|
|
2442
|
+
{ key: "signal", label: "Signal" },
|
|
2443
|
+
{ key: "email", label: "Email" },
|
|
2444
|
+
];
|
|
2445
|
+
|
|
2446
|
+
// Check which are configured
|
|
2447
|
+
const configPath = join(homedir(), ".wispy", "config.json");
|
|
2448
|
+
let cfg = {};
|
|
2449
|
+
try { cfg = JSON.parse(await readFile(configPath, "utf8")); } catch {}
|
|
2450
|
+
const channels = cfg.channels ?? {};
|
|
2451
|
+
|
|
2452
|
+
const choices = channelDefs.map(ch => {
|
|
2453
|
+
const configured = !!channels[ch.key];
|
|
2454
|
+
const status = configured ? _green("connected ✓") : _dim("not configured");
|
|
2455
|
+
return {
|
|
2456
|
+
name: `${ch.label} — ${status}`,
|
|
2457
|
+
value: { key: ch.key, configured, label: ch.label },
|
|
2458
|
+
short: ch.label,
|
|
2459
|
+
};
|
|
2460
|
+
});
|
|
2461
|
+
choices.push(new Separator("──────────"));
|
|
2462
|
+
choices.push({ name: "Start all bots (--serve)", value: { key: "__serve__" } });
|
|
2463
|
+
|
|
2464
|
+
let answer;
|
|
2465
|
+
try {
|
|
2466
|
+
answer = await select({ message: "Channels:", choices });
|
|
2467
|
+
} catch (e) {
|
|
2468
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
2469
|
+
throw e;
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
if (answer.key === "__serve__") {
|
|
2473
|
+
// Re-run with --serve
|
|
2474
|
+
const { spawn } = await import("node:child_process");
|
|
2475
|
+
spawn(process.execPath, [process.argv[1], "--serve"], { stdio: "inherit" })
|
|
2476
|
+
.on("exit", code => process.exit(code ?? 0));
|
|
2477
|
+
await new Promise(() => {});
|
|
2478
|
+
} else if (!answer.configured) {
|
|
2479
|
+
await channelSetup(answer.key);
|
|
2480
|
+
} else {
|
|
2481
|
+
// Configured channel actions
|
|
2482
|
+
let channelAction;
|
|
2483
|
+
try {
|
|
2484
|
+
channelAction = await select({
|
|
2485
|
+
message: `${answer.label}:`,
|
|
2486
|
+
choices: [
|
|
2487
|
+
{ name: "Test connection", value: "test" },
|
|
2488
|
+
{ name: "Reconfigure", value: "setup" },
|
|
2489
|
+
{ name: "Disconnect", value: "disconnect" },
|
|
2490
|
+
],
|
|
2491
|
+
});
|
|
2492
|
+
} catch (e) {
|
|
2493
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
2494
|
+
throw e;
|
|
2495
|
+
}
|
|
2496
|
+
if (channelAction === "test") {
|
|
2497
|
+
await channelTest(answer.key);
|
|
2498
|
+
} else if (channelAction === "setup") {
|
|
2499
|
+
await channelSetup(answer.key);
|
|
2500
|
+
} else if (channelAction === "disconnect") {
|
|
2501
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
2502
|
+
let ok;
|
|
2503
|
+
try { ok = await confirm({ message: `Disconnect ${answer.label}?`, default: false }); }
|
|
2504
|
+
catch (e) { if (e.name === "ExitPromptError") { process.exit(130); } throw e; }
|
|
2505
|
+
if (ok) {
|
|
2506
|
+
const { loadConfig, saveConfig } = await import(path.join(__dirname, "..", "core", "config.mjs"));
|
|
2507
|
+
const c = await loadConfig();
|
|
2508
|
+
if (c.channels) delete c.channels[answer.key];
|
|
2509
|
+
await saveConfig(c);
|
|
2510
|
+
console.log(_green(`✅ Disconnected ${answer.label}`));
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
} catch (e) {
|
|
2515
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
2516
|
+
// Fallback help
|
|
2517
|
+
console.log(`
|
|
2518
|
+
🌿 Wispy Channel Commands:
|
|
2519
|
+
|
|
2520
|
+
wispy channel setup telegram — interactive Telegram bot setup
|
|
2521
|
+
wispy channel setup discord — interactive Discord bot setup
|
|
2522
|
+
wispy channel setup slack — interactive Slack bot setup
|
|
2523
|
+
wispy channel setup whatsapp — WhatsApp setup
|
|
2524
|
+
wispy channel setup email — Email setup
|
|
2525
|
+
wispy channel list — show configured channels
|
|
2526
|
+
wispy channel test <name> — test a channel connection
|
|
2527
|
+
wispy --serve — start all configured channel bots
|
|
2528
|
+
`);
|
|
2529
|
+
}
|
|
1899
2530
|
} else {
|
|
1900
2531
|
console.log(`
|
|
1901
2532
|
🌿 Wispy Channel Commands:
|
|
@@ -1931,25 +2562,124 @@ if (args[0] === "auth") {
|
|
|
1931
2562
|
|
|
1932
2563
|
const auth = new AuthManager(WISPY_DIR);
|
|
1933
2564
|
|
|
1934
|
-
// wispy auth —
|
|
2565
|
+
// wispy auth — interactive menu
|
|
1935
2566
|
if (!sub) {
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
2567
|
+
try {
|
|
2568
|
+
const { select, Separator } = await import("@inquirer/prompts");
|
|
2569
|
+
const statuses = await auth.allStatus().catch(() => []);
|
|
2570
|
+
|
|
2571
|
+
// All known providers + configured ones
|
|
2572
|
+
const KNOWN_PROVIDERS = ["google", "anthropic", "openai", "xai", "github-copilot"];
|
|
2573
|
+
const configuredMap = {};
|
|
2574
|
+
for (const s of statuses) configuredMap[s.provider] = s;
|
|
2575
|
+
|
|
2576
|
+
const { loadConfig } = await import(path.join(__dirname, "..", "core", "config.mjs"));
|
|
2577
|
+
const cfg = await loadConfig();
|
|
2578
|
+
const activeProviders = cfg.providers ? Object.keys(cfg.providers) : (cfg.provider ? [cfg.provider] : []);
|
|
2579
|
+
|
|
2580
|
+
const allProviders = [...new Set([...activeProviders, ...statuses.map(s => s.provider), ...KNOWN_PROVIDERS])];
|
|
2581
|
+
|
|
2582
|
+
const choices = allProviders.map(p => {
|
|
2583
|
+
const s = configuredMap[p];
|
|
2584
|
+
const status = s ? (s.expired ? _yellow("⚠️ expired") : _green("API key ✓")) : _dim("not configured");
|
|
2585
|
+
return {
|
|
2586
|
+
name: `${p} — ${status}`,
|
|
2587
|
+
value: { provider: p, configured: !!s, expired: s?.expired },
|
|
2588
|
+
short: p,
|
|
2589
|
+
};
|
|
2590
|
+
});
|
|
2591
|
+
choices.push(new Separator("──────────"));
|
|
2592
|
+
choices.push({ name: "Add new provider auth", value: { provider: "__new__" } });
|
|
2593
|
+
choices.push({ name: "Refresh expired tokens", value: { provider: "__refresh_all__" } });
|
|
2594
|
+
|
|
2595
|
+
let answer;
|
|
2596
|
+
try {
|
|
2597
|
+
answer = await select({ message: "Authentication:", choices });
|
|
2598
|
+
} catch (e) {
|
|
2599
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
2600
|
+
throw e;
|
|
1945
2601
|
}
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
2602
|
+
|
|
2603
|
+
if (answer.provider === "__new__") {
|
|
2604
|
+
const { input } = await import("@inquirer/prompts");
|
|
2605
|
+
let prov;
|
|
2606
|
+
try { prov = await input({ message: "Provider name (e.g. github-copilot):" }); }
|
|
2607
|
+
catch (e) { if (e.name === "ExitPromptError") { process.exit(130); } throw e; }
|
|
2608
|
+
if (prov && prov.trim()) {
|
|
2609
|
+
// Treat as wispy auth <provider>
|
|
2610
|
+
const provName = prov.trim();
|
|
2611
|
+
if (provName === "github-copilot") {
|
|
2612
|
+
console.log(`\n🔑 ${_bold("GitHub Copilot")} — OAuth sign-in\n`);
|
|
2613
|
+
try {
|
|
2614
|
+
const result = await auth.oauth("github-copilot");
|
|
2615
|
+
if (result.valid) console.log(_green(`✅ GitHub Copilot authenticated!\n`));
|
|
2616
|
+
} catch (err) { console.error(_red(`\n❌ ${err.message}\n`)); }
|
|
2617
|
+
} else {
|
|
2618
|
+
console.log(_dim(` API key auth: set env var for ${provName} and run 'wispy setup'\n`));
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
} else if (answer.provider === "__refresh_all__") {
|
|
2622
|
+
const expired = statuses.filter(s => s.expired);
|
|
2623
|
+
if (expired.length === 0) {
|
|
2624
|
+
console.log(_dim("No expired tokens found.\n"));
|
|
2625
|
+
} else {
|
|
2626
|
+
for (const s of expired) {
|
|
2627
|
+
process.stdout.write(`Refreshing ${s.provider}... `);
|
|
2628
|
+
try { await auth.refreshToken(s.provider); console.log(_green("✓")); }
|
|
2629
|
+
catch (e) { console.log(_red(`✗ ${e.message}`)); }
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
} else if (answer.configured) {
|
|
2633
|
+
let authAction;
|
|
2634
|
+
try {
|
|
2635
|
+
authAction = await select({
|
|
2636
|
+
message: `${answer.provider}:`,
|
|
2637
|
+
choices: [
|
|
2638
|
+
{ name: "Refresh token", value: "refresh" },
|
|
2639
|
+
{ name: "Revoke / remove", value: "revoke" },
|
|
2640
|
+
],
|
|
2641
|
+
});
|
|
2642
|
+
} catch (e) {
|
|
2643
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
2644
|
+
throw e;
|
|
2645
|
+
}
|
|
2646
|
+
if (authAction === "refresh") {
|
|
2647
|
+
try { await auth.refreshToken(answer.provider); console.log(_green(`✅ Token refreshed for ${answer.provider}\n`)); }
|
|
2648
|
+
catch (err) { console.error(_red(`❌ ${err.message}\n`)); }
|
|
2649
|
+
} else if (authAction === "revoke") {
|
|
2650
|
+
await auth.revokeToken(answer.provider);
|
|
2651
|
+
console.log(_green(`✅ Revoked auth for ${answer.provider}\n`));
|
|
2652
|
+
}
|
|
2653
|
+
} else {
|
|
2654
|
+
// Setup flow for unconfigured provider
|
|
2655
|
+
if (answer.provider === "github-copilot") {
|
|
2656
|
+
console.log(`\n🔑 ${_bold("GitHub Copilot")} — OAuth sign-in\n`);
|
|
2657
|
+
try {
|
|
2658
|
+
const result = await auth.oauth("github-copilot");
|
|
2659
|
+
if (result.valid) console.log(_green(`✅ GitHub Copilot authenticated!\n`));
|
|
2660
|
+
} catch (err) { console.error(_red(`\n❌ ${err.message}\n`)); }
|
|
2661
|
+
} else {
|
|
2662
|
+
console.log(_dim(`\n Set env var for ${answer.provider} and run 'wispy setup provider'\n`));
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
process.exit(0);
|
|
2666
|
+
} catch (e) {
|
|
2667
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
2668
|
+
// Fallback: show status
|
|
2669
|
+
const statuses = await auth.allStatus().catch(() => []);
|
|
2670
|
+
if (statuses.length === 0) {
|
|
2671
|
+
console.log(_dim("\n No saved auth tokens. Run: wispy auth <provider>\n"));
|
|
2672
|
+
} else {
|
|
2673
|
+
console.log(`\n${_bold("🔑 Auth Status")}\n`);
|
|
2674
|
+
for (const s of statuses) {
|
|
2675
|
+
const expiryStr = s.expiresAt ? _dim(` (expires ${new Date(s.expiresAt).toLocaleString()})`) : "";
|
|
2676
|
+
const statusIcon = s.expired ? _yellow("⚠️ expired") : _green("✅ valid");
|
|
2677
|
+
console.log(` ${_cyan(s.provider.padEnd(20))} ${s.type.padEnd(8)} ${statusIcon}${expiryStr}`);
|
|
2678
|
+
}
|
|
2679
|
+
console.log("");
|
|
2680
|
+
}
|
|
2681
|
+
process.exit(0);
|
|
1950
2682
|
}
|
|
1951
|
-
console.log("");
|
|
1952
|
-
process.exit(0);
|
|
1953
2683
|
}
|
|
1954
2684
|
|
|
1955
2685
|
// wispy auth refresh <provider>
|
|
@@ -2132,12 +2862,56 @@ if (isInteractiveStart) {
|
|
|
2132
2862
|
path.join(__dirname, "..", "core", "config.mjs")
|
|
2133
2863
|
);
|
|
2134
2864
|
if (await isFirstRun()) {
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2865
|
+
// Show welcome interactive menu before onboarding
|
|
2866
|
+
if (args.length === 0) {
|
|
2867
|
+
try {
|
|
2868
|
+
const { select } = await import("@inquirer/prompts");
|
|
2869
|
+
let welcomeAction;
|
|
2870
|
+
try {
|
|
2871
|
+
welcomeAction = await select({
|
|
2872
|
+
message: "Welcome to Wispy! What would you like to do?",
|
|
2873
|
+
choices: [
|
|
2874
|
+
{ name: "Start chatting (REPL)", value: "repl" },
|
|
2875
|
+
{ name: "Open workspace (TUI)", value: "tui" },
|
|
2876
|
+
{ name: "Set up wispy", value: "setup" },
|
|
2877
|
+
{ name: "Run diagnostics", value: "doctor" },
|
|
2878
|
+
],
|
|
2879
|
+
});
|
|
2880
|
+
} catch (e) {
|
|
2881
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
2882
|
+
throw e;
|
|
2883
|
+
}
|
|
2884
|
+
if (welcomeAction === "tui") {
|
|
2885
|
+
args.push("tui");
|
|
2886
|
+
} else if (welcomeAction === "setup") {
|
|
2887
|
+
const { OnboardingWizard } = await import(path.join(__dirname, "..", "core", "onboarding.mjs"));
|
|
2888
|
+
const wizard = new OnboardingWizard();
|
|
2889
|
+
await wizard.run();
|
|
2890
|
+
process.exit(0);
|
|
2891
|
+
} else if (welcomeAction === "doctor") {
|
|
2892
|
+
// Re-exec with doctor
|
|
2893
|
+
const { spawn: sp } = await import("node:child_process");
|
|
2894
|
+
sp(process.execPath, [process.argv[1], "doctor"], { stdio: "inherit" })
|
|
2895
|
+
.on("exit", code => process.exit(code ?? 0));
|
|
2896
|
+
await new Promise(() => {});
|
|
2897
|
+
}
|
|
2898
|
+
// "repl" falls through to REPL below
|
|
2899
|
+
} catch (e) {
|
|
2900
|
+
if (e.name !== "ExitPromptError") {
|
|
2901
|
+
// Run normal onboarding if welcome menu fails
|
|
2902
|
+
const { OnboardingWizard } = await import(path.join(__dirname, "..", "core", "onboarding.mjs"));
|
|
2903
|
+
const wizard = new OnboardingWizard();
|
|
2904
|
+
await wizard.run();
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
} else {
|
|
2908
|
+
const { OnboardingWizard } = await import(
|
|
2909
|
+
path.join(__dirname, "..", "core", "onboarding.mjs")
|
|
2910
|
+
);
|
|
2911
|
+
const wizard = new OnboardingWizard();
|
|
2912
|
+
await wizard.run();
|
|
2913
|
+
// After onboarding, continue to REPL or TUI
|
|
2914
|
+
}
|
|
2141
2915
|
}
|
|
2142
2916
|
} catch {
|
|
2143
2917
|
// If onboarding fails for any reason, continue normally
|
|
@@ -204,6 +204,123 @@ export async function cmdImproveSkill(name, feedback) {
|
|
|
204
204
|
export async function handleSkillCommand(args) {
|
|
205
205
|
const sub = args[1];
|
|
206
206
|
|
|
207
|
+
if (!sub) {
|
|
208
|
+
// Interactive menu
|
|
209
|
+
try {
|
|
210
|
+
const { select, Separator, input } = await import("@inquirer/prompts");
|
|
211
|
+
const skills = await listSkills();
|
|
212
|
+
|
|
213
|
+
const choices = [];
|
|
214
|
+
if (skills.length > 0) {
|
|
215
|
+
for (const s of skills) {
|
|
216
|
+
const used = s.timesUsed > 0 ? ` — used ${s.timesUsed} times` : "";
|
|
217
|
+
choices.push({
|
|
218
|
+
name: `${s.name}${used}`,
|
|
219
|
+
value: { type: "skill", name: s.name },
|
|
220
|
+
short: s.name,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
choices.push(new Separator("──────────"));
|
|
225
|
+
choices.push({ name: "Create skill from last conversation", value: { type: "create" } });
|
|
226
|
+
choices.push({ name: "Import skill", value: { type: "import" } });
|
|
227
|
+
|
|
228
|
+
let answer;
|
|
229
|
+
try {
|
|
230
|
+
answer = await select({ message: "Skills:", choices });
|
|
231
|
+
} catch (e) {
|
|
232
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
233
|
+
throw e;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (answer.type === "create") {
|
|
237
|
+
let name;
|
|
238
|
+
try {
|
|
239
|
+
name = await input({ message: "Skill name:" });
|
|
240
|
+
} catch (e) {
|
|
241
|
+
if (e.name === "ExitPromptError") return;
|
|
242
|
+
throw e;
|
|
243
|
+
}
|
|
244
|
+
if (name && name.trim()) await cmdTeach(name.trim());
|
|
245
|
+
} else if (answer.type === "import") {
|
|
246
|
+
console.log(dim("Import via: wispy skill import <path> (coming soon)"));
|
|
247
|
+
} else if (answer.type === "skill") {
|
|
248
|
+
// Sub-menu for selected skill
|
|
249
|
+
let skillAction;
|
|
250
|
+
try {
|
|
251
|
+
skillAction = await select({
|
|
252
|
+
message: `${answer.name}:`,
|
|
253
|
+
choices: [
|
|
254
|
+
{ name: "Run now", value: "run" },
|
|
255
|
+
{ name: "View details", value: "view" },
|
|
256
|
+
{ name: "Improve", value: "improve" },
|
|
257
|
+
{ name: "Delete", value: "delete" },
|
|
258
|
+
],
|
|
259
|
+
});
|
|
260
|
+
} catch (e) {
|
|
261
|
+
if (e.name === "ExitPromptError") return;
|
|
262
|
+
throw e;
|
|
263
|
+
}
|
|
264
|
+
if (skillAction === "run") {
|
|
265
|
+
await cmdSkillRun(answer.name);
|
|
266
|
+
} else if (skillAction === "view") {
|
|
267
|
+
const skill = await (async () => {
|
|
268
|
+
const { readFile } = await import("node:fs/promises");
|
|
269
|
+
const { join } = await import("node:path");
|
|
270
|
+
const { homedir } = await import("node:os");
|
|
271
|
+
const skillsDir = join(homedir(), ".wispy", "skills");
|
|
272
|
+
try {
|
|
273
|
+
return JSON.parse(await readFile(join(skillsDir, `${answer.name}.json`), "utf8"));
|
|
274
|
+
} catch { return null; }
|
|
275
|
+
})();
|
|
276
|
+
if (skill) {
|
|
277
|
+
console.log(`\n${bold(skill.name)} v${skill.version ?? 1}`);
|
|
278
|
+
console.log(dim(` ${skill.description ?? ""}`));
|
|
279
|
+
console.log(dim(` Prompt: ${skill.prompt?.slice(0, 120) ?? ""}`));
|
|
280
|
+
console.log(dim(` Used: ${skill.timesUsed ?? 0}x`));
|
|
281
|
+
if (skill.tags?.length > 0) console.log(dim(` Tags: ${skill.tags.join(", ")}`));
|
|
282
|
+
console.log("");
|
|
283
|
+
}
|
|
284
|
+
} else if (skillAction === "improve") {
|
|
285
|
+
let feedback;
|
|
286
|
+
try {
|
|
287
|
+
const { input: inp } = await import("@inquirer/prompts");
|
|
288
|
+
feedback = await inp({ message: "Improvement notes:" });
|
|
289
|
+
} catch (e) {
|
|
290
|
+
if (e.name === "ExitPromptError") return;
|
|
291
|
+
throw e;
|
|
292
|
+
}
|
|
293
|
+
if (feedback && feedback.trim()) await cmdImproveSkill(answer.name, feedback.trim());
|
|
294
|
+
} else if (skillAction === "delete") {
|
|
295
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
296
|
+
let ok;
|
|
297
|
+
try {
|
|
298
|
+
ok = await confirm({ message: `Delete skill '${answer.name}'?`, default: false });
|
|
299
|
+
} catch (e) {
|
|
300
|
+
if (e.name === "ExitPromptError") return;
|
|
301
|
+
throw e;
|
|
302
|
+
}
|
|
303
|
+
if (ok) {
|
|
304
|
+
const { unlink } = await import("node:fs/promises");
|
|
305
|
+
const { join } = await import("node:path");
|
|
306
|
+
const { homedir } = await import("node:os");
|
|
307
|
+
const skillsDir = join(homedir(), ".wispy", "skills");
|
|
308
|
+
try {
|
|
309
|
+
await unlink(join(skillsDir, `${answer.name}.json`));
|
|
310
|
+
console.log(green(`✅ Deleted skill: ${answer.name}`));
|
|
311
|
+
} catch (e) {
|
|
312
|
+
console.log(red(`Failed to delete: ${e.message}`));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
} catch (e) {
|
|
318
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
319
|
+
await cmdSkillList();
|
|
320
|
+
}
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
207
324
|
if (!sub) return cmdSkillList();
|
|
208
325
|
if (sub === "run") return cmdSkillRun(args[2]);
|
|
209
326
|
if (sub === "list") return cmdSkillList();
|
package/lib/commands/trust.mjs
CHANGED
|
@@ -322,7 +322,85 @@ export async function cmdTrustReceipt(id) {
|
|
|
322
322
|
export async function handleTrustCommand(args) {
|
|
323
323
|
const sub = args[1];
|
|
324
324
|
|
|
325
|
-
if (!sub)
|
|
325
|
+
if (!sub) {
|
|
326
|
+
// Interactive menu
|
|
327
|
+
try {
|
|
328
|
+
const { select } = await import("@inquirer/prompts");
|
|
329
|
+
const level = await getCurrentSecurityLevel();
|
|
330
|
+
const preset = SECURITY_PRESETS[level] ?? SECURITY_PRESETS.balanced;
|
|
331
|
+
|
|
332
|
+
console.log(`\nCurrent level: ${preset.color(bold(preset.label))}`);
|
|
333
|
+
console.log(dim(` ${preset.description}\n`));
|
|
334
|
+
|
|
335
|
+
let action;
|
|
336
|
+
try {
|
|
337
|
+
action = await select({
|
|
338
|
+
message: "Change trust settings:",
|
|
339
|
+
choices: [
|
|
340
|
+
{ name: "Set level (careful / balanced / yolo)", value: "set-level" },
|
|
341
|
+
{ name: "View audit log", value: "log" },
|
|
342
|
+
{ name: "View recent approvals", value: "approvals" },
|
|
343
|
+
{ name: "Replay a session", value: "replay" },
|
|
344
|
+
{ name: "Show permissions by tool", value: "permissions" },
|
|
345
|
+
],
|
|
346
|
+
});
|
|
347
|
+
} catch (e) {
|
|
348
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
349
|
+
throw e;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (action === "set-level") {
|
|
353
|
+
let levelChoice;
|
|
354
|
+
try {
|
|
355
|
+
levelChoice = await select({
|
|
356
|
+
message: "Choose trust level:",
|
|
357
|
+
choices: [
|
|
358
|
+
{ name: `${green("careful")} 🔒 — require approval for most operations`, value: "careful" },
|
|
359
|
+
{ name: `${yellow("balanced")} ⚖️ — approve dangerous ops, notify writes`, value: "balanced" },
|
|
360
|
+
{ name: `${red("yolo")} 🚀 — auto-approve everything (use with care!)`, value: "yolo" },
|
|
361
|
+
],
|
|
362
|
+
});
|
|
363
|
+
} catch (e) {
|
|
364
|
+
if (e.name === "ExitPromptError") return;
|
|
365
|
+
throw e;
|
|
366
|
+
}
|
|
367
|
+
await cmdTrustLevel(levelChoice);
|
|
368
|
+
} else if (action === "log") {
|
|
369
|
+
await cmdTrustLog([]);
|
|
370
|
+
} else if (action === "approvals") {
|
|
371
|
+
await cmdTrustShow();
|
|
372
|
+
} else if (action === "replay") {
|
|
373
|
+
const { input } = await import("@inquirer/prompts");
|
|
374
|
+
let sid;
|
|
375
|
+
try {
|
|
376
|
+
sid = await input({ message: "Session ID to replay:" });
|
|
377
|
+
} catch (e) {
|
|
378
|
+
if (e.name === "ExitPromptError") return;
|
|
379
|
+
throw e;
|
|
380
|
+
}
|
|
381
|
+
if (sid && sid.trim()) await cmdTrustReplay(sid.trim());
|
|
382
|
+
} else if (action === "permissions") {
|
|
383
|
+
const { readJsonOr: rjo } = { readJsonOr: async (p, f) => { try { return JSON.parse(await (await import("node:fs/promises")).readFile(p, "utf8")); } catch { return f; } } };
|
|
384
|
+
const permsData = await rjo(PERMISSIONS_FILE, { policies: {} });
|
|
385
|
+
const policies = permsData.policies ?? {};
|
|
386
|
+
console.log(`\n${bold("🔐 Permissions by tool:")}\n`);
|
|
387
|
+
if (Object.keys(policies).length === 0) {
|
|
388
|
+
console.log(dim(" No custom policies. Using level defaults."));
|
|
389
|
+
} else {
|
|
390
|
+
for (const [tool, pol] of Object.entries(policies)) {
|
|
391
|
+
const icon = pol === "approve" ? "🔐" : pol === "notify" ? "📋" : "✅";
|
|
392
|
+
console.log(` ${icon} ${cyan(tool.padEnd(20))} ${pol}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
console.log("");
|
|
396
|
+
}
|
|
397
|
+
} catch (e) {
|
|
398
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
399
|
+
// Fallback to regular show
|
|
400
|
+
await cmdTrustShow();
|
|
401
|
+
}
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
326
404
|
if (sub === "level") return cmdTrustLevel(args[2]);
|
|
327
405
|
if (sub === "log") return cmdTrustLog(args.slice(2));
|
|
328
406
|
if (sub === "replay") return cmdTrustReplay(args[2]);
|
package/lib/commands/ws.mjs
CHANGED
|
@@ -412,7 +412,87 @@ export async function handleWsCommand(args) {
|
|
|
412
412
|
const sub = args[1];
|
|
413
413
|
|
|
414
414
|
if (!sub) {
|
|
415
|
-
|
|
415
|
+
// Interactive menu
|
|
416
|
+
try {
|
|
417
|
+
const { select, input, Separator } = await import("@inquirer/prompts");
|
|
418
|
+
const workstreams = await listAllWorkstreams();
|
|
419
|
+
|
|
420
|
+
const choices = [];
|
|
421
|
+
if (workstreams.length > 0) {
|
|
422
|
+
for (const ws of workstreams) {
|
|
423
|
+
const marker = ws.isActive ? "● " : " ";
|
|
424
|
+
const last = formatRelative(ws.lastActive);
|
|
425
|
+
const msgs = ws.sessionCount > 0 ? ` · ${ws.sessionCount} msgs` : "";
|
|
426
|
+
choices.push({
|
|
427
|
+
name: `${marker}${ws.name}${ws.isActive ? " (active)" : ""} — ${last}${msgs}`,
|
|
428
|
+
value: { type: "switch", name: ws.name },
|
|
429
|
+
short: ws.name,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
choices.push(new Separator(dim("No workstreams yet")));
|
|
434
|
+
}
|
|
435
|
+
choices.push(new Separator("──────────"));
|
|
436
|
+
choices.push({ name: "Create new workstream", value: { type: "new" }, short: "Create new" });
|
|
437
|
+
choices.push({ name: "Archive a workstream", value: { type: "archive" }, short: "Archive" });
|
|
438
|
+
choices.push({ name: "Delete a workstream", value: { type: "delete" }, short: "Delete" });
|
|
439
|
+
|
|
440
|
+
let answer;
|
|
441
|
+
try {
|
|
442
|
+
answer = await select({ message: "Workstreams:", choices });
|
|
443
|
+
} catch (e) {
|
|
444
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
445
|
+
throw e;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (answer.type === "switch") {
|
|
449
|
+
await cmdWsSwitch(answer.name);
|
|
450
|
+
} else if (answer.type === "new") {
|
|
451
|
+
let name;
|
|
452
|
+
try {
|
|
453
|
+
name = await input({ message: "New workstream name:" });
|
|
454
|
+
} catch (e) {
|
|
455
|
+
if (e.name === "ExitPromptError") return;
|
|
456
|
+
throw e;
|
|
457
|
+
}
|
|
458
|
+
if (name && name.trim()) await cmdWsNew(name.trim());
|
|
459
|
+
} else if (answer.type === "archive") {
|
|
460
|
+
const nonActive = workstreams.filter(w => !w.isActive);
|
|
461
|
+
if (nonActive.length === 0) {
|
|
462
|
+
console.log(dim("No other workstreams to archive."));
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const archiveChoices = nonActive.map(w => ({ name: w.name, value: w.name }));
|
|
466
|
+
let toArchive;
|
|
467
|
+
try {
|
|
468
|
+
toArchive = await select({ message: "Archive which workstream?", choices: archiveChoices });
|
|
469
|
+
} catch (e) {
|
|
470
|
+
if (e.name === "ExitPromptError") return;
|
|
471
|
+
throw e;
|
|
472
|
+
}
|
|
473
|
+
await cmdWsArchive(toArchive);
|
|
474
|
+
} else if (answer.type === "delete") {
|
|
475
|
+
const deletable = workstreams.filter(w => w.name !== "default");
|
|
476
|
+
if (deletable.length === 0) {
|
|
477
|
+
console.log(dim("No workstreams to delete (cannot delete 'default')."));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const deleteChoices = deletable.map(w => ({ name: w.name, value: w.name }));
|
|
481
|
+
let toDelete;
|
|
482
|
+
try {
|
|
483
|
+
toDelete = await select({ message: "Delete which workstream?", choices: deleteChoices });
|
|
484
|
+
} catch (e) {
|
|
485
|
+
if (e.name === "ExitPromptError") return;
|
|
486
|
+
throw e;
|
|
487
|
+
}
|
|
488
|
+
await cmdWsDelete(toDelete);
|
|
489
|
+
}
|
|
490
|
+
} catch (e) {
|
|
491
|
+
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
492
|
+
// Fallback to plain list if inquirer unavailable
|
|
493
|
+
await cmdWsList();
|
|
494
|
+
}
|
|
495
|
+
return;
|
|
416
496
|
}
|
|
417
497
|
|
|
418
498
|
if (sub === "new") return cmdWsNew(args[2]);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* wispy-tui.mjs — Workspace OS TUI for Wispy v2.0
|
|
4
|
+
*
|
|
5
|
+
* Multi-panel workspace interface:
|
|
6
|
+
* - Left sidebar: Workstreams, Agents, Memory, Cron, Sync
|
|
7
|
+
* - Main area: Chat / Overview / Agents / Memory / Audit / Settings
|
|
8
|
+
* - Bottom: Action Timeline bar + Input
|
|
9
|
+
* - Overlays: Approval dialogs, Diff views
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import React, { useState, useEffect, useRef, useCallback, useReducer } from "react";
|
|
13
|
+
import { render, Box, Text, useApp, Newline, useInput, useStdout } from "ink";
|
|
14
|
+
import Spinner from "ink-spinner";
|
|
15
|
+
import TextInput from "ink-text-input";
|
|
16
|
+
|
|
17
|
+
import { COMMANDS, filterCommands } from "./command-registry.mjs";
|
|
18
|
+
|
|
19
|
+
import os from "node:os";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { readFile, writeFile, readdir, stat, mkdir } from "node:fs/promises";
|
|
22
|
+
|
|
23
|
+
import { WispyEngine, CONVERSATIONS_DIR, PROVIDERS, WISPY_DIR, MEMORY_DIR } from "../core/index.mjs";
|
|
24
|
+
|
|
25
|
+
// ─── Parse CLI args ──────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const rawArgs = process.argv.slice(2);
|
|
28
|
+
const wsIdx = rawArgs.findIndex((a) => a === "-w" || a === "--workstream");
|
|
29
|
+
const INITIAL_WORKSTREAM =
|
|
30
|
+
process.env.WISPY_WORKSTREAM ??
|
|
31
|
+
(wsIdx !== -1 ? rawArgs[wsIdx + 1] : null) ??
|
|
32
|
+
"default";
|
|
33
|
+
|
|
34
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
const VIEWS = ["chat", "overview", "agents", "memory", "audit", "settings"];
|
|
37
|
+
const SIDEBAR_WIDTH = 16;
|
|
38
|
+
const TIMELINE_LINES = 3;
|
|
39
|
+
|
|
40
|
+
const TOOL_ICONS = {
|
|
41
|
+
read_file: "⎘", write_file: "✍", file_edit: "✎", run_command: "⚙",
|
|
42
|
+
git: "⧉", web_search: "🔍", web_fetch: "⤓", list_directory: "☰",
|
|
43
|
+
spawn_subagent: "⇝", spawn_agent: "↠", memory_save: "💾",
|
|
44
|
+
memory_search: "🔎", memory_list: "🗂", delete_file: "🗑",
|
|
45
|
+
node_execute: "⨀", update_work_context: "⟳",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ─── Utilities ───────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
function fmtTime(iso) {
|
|
51
|
+
if (!iso) return "";
|
|
52
|
+
try { return new Date(iso).toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" }); }
|
|
53
|
+
catch { return ""; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function fmtRelTime(iso) {
|
|
57
|
+
if (!iso) return "";
|
|
58
|
+
try {
|
|
59
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
60
|
+
if (diff < 60_000) return "just now";
|
|
61
|
+
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}min ago`;
|
|
62
|
+
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}hr ago`;
|
|
63
|
+
return "yesterday";
|
|
64
|
+
} catch { return ""; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function truncate(str, n) {
|
|
68
|
+
if (!str) return "";
|
|
69
|
+
return str.length > n ? str.slice(0, n - 1) + "…" : str;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── Markdown renderer ───────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
function renderMarkdown(text, maxWidth = 60) {
|
|
75
|
+
// Updated renderer function
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Updates incorporate "minimalistic" visuals for agent functionality and correct UX/lint fixes ───
|
|
79
|
+
const agentIcon = (status) => {
|
|
80
|
+
if (status === "running") return "●";
|
|
81
|
+
if (status === "pending") return "○";
|
|
82
|
+
if (status === "" };
|