fellow-agents 0.0.21 → 0.0.22
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 +14 -0
- package/dist/cli.js +13 -3
- package/dist/commands/clean.js +1 -0
- package/dist/commands/config.js +109 -0
- package/dist/commands/start.js +100 -0
- package/dist/lib/preferences.js +97 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,20 @@ fellow-agents
|
|
|
12
12
|
|
|
13
13
|
That's it. Creates a workspace, downloads what it needs, opens your browser — three agents ready to collaborate.
|
|
14
14
|
|
|
15
|
+
## What you get
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
A team of AI agents working together in one browser. Each session is an autonomous agent — they message each other, coordinate on tasks, and track shared work. All running locally, all visible at once.
|
|
20
|
+
|
|
21
|
+
- **(1) CLI sessions are agents.** Each one has its own identity, system prompt, and workspace.
|
|
22
|
+
- **(2) Bring your own AI.** Claude Code, GitHub Copilot CLI, pi, or any compatible CLI — mix and match per agent.
|
|
23
|
+
- **(3) Agents talk to each other.** Built-in async messaging (emcom) lets agents hand off tasks, request reviews, escalate questions — no shared context window, no token limits.
|
|
24
|
+
- **(4) Run one or many per tab.** Group agents by team or project. Switch between tabs like browser tabs.
|
|
25
|
+
- **(5) Built-in work tracker.** Every agent sees the same queue. File items, assign work, track status across sessions.
|
|
26
|
+
|
|
27
|
+
If you've ever wished Claude could send a sub-task to another instance and get an answer back while you keep working — this is that.
|
|
28
|
+
|
|
15
29
|
---
|
|
16
30
|
|
|
17
31
|
## What happens when you run it
|
package/dist/cli.js
CHANGED
|
@@ -4,8 +4,9 @@ const args = process.argv.slice(2);
|
|
|
4
4
|
const command = args[0] === "stop" ? "stop"
|
|
5
5
|
: args[0] === "clean" ? "clean"
|
|
6
6
|
: args[0] === "uninstall" ? "uninstall"
|
|
7
|
-
:
|
|
8
|
-
: "
|
|
7
|
+
: args[0] === "config" ? "config"
|
|
8
|
+
: (args[0] === "--help" || args[0] === "-h") ? "help"
|
|
9
|
+
: "start";
|
|
9
10
|
function getFlag(name, fallback) {
|
|
10
11
|
const idx = args.indexOf(name);
|
|
11
12
|
return idx !== -1 && args[idx + 1] ? args[idx + 1] : fallback;
|
|
@@ -38,6 +39,10 @@ else if (command === "uninstall") {
|
|
|
38
39
|
yes: hasFlag("--yes"),
|
|
39
40
|
});
|
|
40
41
|
}
|
|
42
|
+
else if (command === "config") {
|
|
43
|
+
const { config } = await import("./commands/config.js");
|
|
44
|
+
config(args.slice(1));
|
|
45
|
+
}
|
|
41
46
|
else {
|
|
42
47
|
console.log(`fellow-agents — multi-agent system for Claude Code, Copilot CLI, and pi
|
|
43
48
|
|
|
@@ -49,8 +54,9 @@ else {
|
|
|
49
54
|
Commands:
|
|
50
55
|
fellow-agents [options] Start services (the usual command)
|
|
51
56
|
fellow-agents stop Stop running services
|
|
52
|
-
fellow-agents clean Wipe cached binaries + pty-win install (preserves logs)
|
|
57
|
+
fellow-agents clean Wipe cached binaries + pty-win install (preserves logs and preferences)
|
|
53
58
|
fellow-agents uninstall [--yes] Remove all state, including scaffolded workspaces
|
|
59
|
+
fellow-agents config <get|set> Read or write user preferences (see 'config --help')
|
|
54
60
|
|
|
55
61
|
Start options:
|
|
56
62
|
--port <number> pty-win port (default: 3700)
|
|
@@ -63,6 +69,10 @@ Uninstall options:
|
|
|
63
69
|
--yes Actually perform the uninstall (default is dry-run preview)
|
|
64
70
|
--dir <path> Workspace location (default: current — use if you ran start elsewhere)
|
|
65
71
|
|
|
72
|
+
Config:
|
|
73
|
+
fellow-agents config get [key] Print all preferences, or one value
|
|
74
|
+
fellow-agents config set <key> <value> Write a preference (e.g. cliPreference claude)
|
|
75
|
+
|
|
66
76
|
General:
|
|
67
77
|
-h, --help Show this help
|
|
68
78
|
|
package/dist/commands/clean.js
CHANGED
|
@@ -67,6 +67,7 @@ export function clean() {
|
|
|
67
67
|
console.log("");
|
|
68
68
|
console.log(` Cleaned ${formatBytes(totalFreed)} from ${dataDir}`);
|
|
69
69
|
console.log(` Logs preserved at ${join(dataDir, "logs")}`);
|
|
70
|
+
console.log(` Preferences preserved at ${join(dataDir, "preferences.json")} (if set)`);
|
|
70
71
|
console.log(` Run 'fellow-agents' to reinstall.`);
|
|
71
72
|
console.log("");
|
|
72
73
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { readPreferences, writePreferences, lookupCli, preferencesFile, KNOWN_KEYS, } from "../lib/preferences.js";
|
|
2
|
+
function printHelp() {
|
|
3
|
+
console.log(`fellow-agents config — read or write user preferences
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
fellow-agents config get Print all preferences as JSON
|
|
7
|
+
fellow-agents config get <key> Print the value of a single key
|
|
8
|
+
fellow-agents config set <key> <value> Write a value (creates the file if missing)
|
|
9
|
+
|
|
10
|
+
Known keys:
|
|
11
|
+
cliPreference The CLI launched by pty-win's play button.
|
|
12
|
+
Bare command (claude | copilot | pi) or full path to an executable.
|
|
13
|
+
|
|
14
|
+
File: ${preferencesFile}
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
fellow-agents config set cliPreference claude
|
|
18
|
+
fellow-agents config set cliPreference "C:\\Program Files\\claude\\claude.exe"
|
|
19
|
+
fellow-agents config get cliPreference`);
|
|
20
|
+
}
|
|
21
|
+
function isKnownKey(key) {
|
|
22
|
+
return KNOWN_KEYS.includes(key);
|
|
23
|
+
}
|
|
24
|
+
export function config(args) {
|
|
25
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
26
|
+
printHelp();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const sub = args[0];
|
|
30
|
+
if (sub === "get") {
|
|
31
|
+
handleGet(args.slice(1));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (sub === "set") {
|
|
35
|
+
handleSet(args.slice(1));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
console.error(`Unknown config subcommand: ${sub}`);
|
|
39
|
+
console.error(`Run 'fellow-agents config --help' for usage.`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
function handleGet(args) {
|
|
43
|
+
const prefs = readPreferences();
|
|
44
|
+
if (args.length === 0) {
|
|
45
|
+
if (prefs === null) {
|
|
46
|
+
console.log("No preferences set.");
|
|
47
|
+
console.log(`Run 'fellow-agents' or 'fellow-agents config set <key> <value>' to create them.`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
console.log(JSON.stringify(prefs, null, 2));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const key = args[0];
|
|
54
|
+
if (!isKnownKey(key)) {
|
|
55
|
+
console.error(`Unknown key: ${key}`);
|
|
56
|
+
console.error(`Known keys: ${KNOWN_KEYS.join(", ")}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
if (prefs === null) {
|
|
60
|
+
console.log("No preferences set.");
|
|
61
|
+
console.log(`Run 'fellow-agents' or 'fellow-agents config set ${key} <value>' to set it.`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const value = prefs[key];
|
|
65
|
+
if (value === undefined || value === null || value === "") {
|
|
66
|
+
console.log(`${key} is not set.`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
console.log(String(value));
|
|
70
|
+
}
|
|
71
|
+
function handleSet(args) {
|
|
72
|
+
if (args.length < 2) {
|
|
73
|
+
console.error("Usage: fellow-agents config set <key> <value>");
|
|
74
|
+
console.error(`Known keys: ${KNOWN_KEYS.join(", ")}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
const key = args[0];
|
|
78
|
+
const value = args.slice(1).join(" ");
|
|
79
|
+
if (!isKnownKey(key)) {
|
|
80
|
+
console.error(`Unknown key: ${key}`);
|
|
81
|
+
console.error(`Known keys: ${KNOWN_KEYS.join(", ")}`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
if (!value.trim()) {
|
|
85
|
+
console.error(`Value for '${key}' cannot be empty.`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
// cliPreference: warn (don't fail) if where.exe / which doesn't find it.
|
|
89
|
+
// Real use case: user pre-configures before installing the CLI, or the CLI
|
|
90
|
+
// is in a non-default location PATH doesn't see yet. Reject would block valid flows.
|
|
91
|
+
if (key === "cliPreference") {
|
|
92
|
+
const looksLikePath = value.includes("\\") || value.includes("/");
|
|
93
|
+
const resolved = lookupCli(value);
|
|
94
|
+
if (resolved === null && !looksLikePath) {
|
|
95
|
+
const tool = process.platform === "win32" ? "where.exe" : "which";
|
|
96
|
+
console.error(` WARNING: '${tool} ${value}' returned no matches.`);
|
|
97
|
+
console.error(` Writing the preference anyway — pty-win will fall back if the CLI is missing at launch time.`);
|
|
98
|
+
console.error(` Fix later with: fellow-agents config set cliPreference <name-or-path>`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const existing = readPreferences();
|
|
102
|
+
const written = writePreferences({
|
|
103
|
+
...(existing ?? {}),
|
|
104
|
+
[key]: value,
|
|
105
|
+
updatedBy: "config-set",
|
|
106
|
+
});
|
|
107
|
+
console.log(`Set ${key} = ${value}`);
|
|
108
|
+
console.log(`Wrote ${preferencesFile} (schema ${written.schema}, updatedAt ${written.updatedAt}).`);
|
|
109
|
+
}
|
package/dist/commands/start.js
CHANGED
|
@@ -2,12 +2,15 @@ import { existsSync, readFileSync, mkdirSync, writeFileSync } from "fs";
|
|
|
2
2
|
import http from "http";
|
|
3
3
|
import { join, resolve } from "path";
|
|
4
4
|
import { execSync } from "child_process";
|
|
5
|
+
import * as readline from "node:readline/promises";
|
|
6
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
5
7
|
import { binDir, ptyWinDir, logsDir, dataDir } from "../lib/paths.js";
|
|
6
8
|
import { downloadBinaries } from "../lib/download.js";
|
|
7
9
|
import { startEmcomServer, startPtyWin, stopAll, logPath } from "../lib/services.js";
|
|
8
10
|
import { scaffoldWorkspaces, registerAgents, writeHooks } from "../lib/workspaces.js";
|
|
9
11
|
import { installSkills } from "../lib/skills.js";
|
|
10
12
|
import { binarySuffix } from "../lib/platform.js";
|
|
13
|
+
import { readPreferences, writePreferences, autoDetectClis, lookupCli, } from "../lib/preferences.js";
|
|
11
14
|
// Minimal engines.node range check — handles ">=N", "<N", and combinations.
|
|
12
15
|
function nodeInRange(version, range) {
|
|
13
16
|
const major = parseInt(version.split(".")[0], 10);
|
|
@@ -17,6 +20,75 @@ function nodeInRange(version, range) {
|
|
|
17
20
|
const max = maxMatch ? parseInt(maxMatch[1], 10) : Infinity;
|
|
18
21
|
return major >= min && major < max;
|
|
19
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* First-run CLI preference prompt. Returns the chosen value (bare command or full path),
|
|
25
|
+
* or null if we should skip (non-interactive, or user declined).
|
|
26
|
+
*
|
|
27
|
+
* Design (locked 5/27 with milo):
|
|
28
|
+
* - Native readline (no inquirer dep — start.ts is a setup pipeline, not a chat UI)
|
|
29
|
+
* - Auto-detected CLIs from PATH shown as numbered choices, plus a "Custom path" option
|
|
30
|
+
* - If user types a custom value not on PATH, single confirm: "Use it anyway? [y/N]"
|
|
31
|
+
* - Non-interactive stdin (CI, piped) → return null, caller logs a hint
|
|
32
|
+
*/
|
|
33
|
+
async function promptForCliPreference() {
|
|
34
|
+
if (!input.isTTY)
|
|
35
|
+
return null;
|
|
36
|
+
const detected = autoDetectClis();
|
|
37
|
+
const rl = readline.createInterface({ input, output });
|
|
38
|
+
try {
|
|
39
|
+
console.log("");
|
|
40
|
+
console.log(" Pick your preferred CLI — pty-win's play button will launch this in each new tab.");
|
|
41
|
+
console.log(" (You can change it later with 'fellow-agents config set cliPreference <name>'.)");
|
|
42
|
+
console.log("");
|
|
43
|
+
const choices = [...detected];
|
|
44
|
+
if (detected.length > 0) {
|
|
45
|
+
for (let i = 0; i < detected.length; i++) {
|
|
46
|
+
console.log(` [${i + 1}] ${detected[i]}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.log(" (None of claude/copilot/pi found on PATH — pick Custom path to specify your own)");
|
|
51
|
+
}
|
|
52
|
+
const customIdx = choices.length + 1;
|
|
53
|
+
console.log(` [${customIdx}] Custom path or other command`);
|
|
54
|
+
console.log(` [s] Skip for now`);
|
|
55
|
+
console.log("");
|
|
56
|
+
const answer = (await rl.question(" Choice: ")).trim().toLowerCase();
|
|
57
|
+
if (answer === "s" || answer === "skip" || answer === "") {
|
|
58
|
+
console.log(" Skipped — pty-win will pick a default until you set one.");
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const num = parseInt(answer, 10);
|
|
62
|
+
if (!isNaN(num) && num >= 1 && num <= detected.length) {
|
|
63
|
+
return detected[num - 1];
|
|
64
|
+
}
|
|
65
|
+
if (!isNaN(num) && num === customIdx) {
|
|
66
|
+
const value = (await rl.question(" Enter command or full path: ")).trim();
|
|
67
|
+
if (!value) {
|
|
68
|
+
console.log(" Empty input — skipped.");
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
// Confirm step if value doesn't resolve and doesn't look like a path
|
|
72
|
+
const looksLikePath = value.includes("\\") || value.includes("/");
|
|
73
|
+
const resolved = lookupCli(value);
|
|
74
|
+
if (resolved === null && !looksLikePath) {
|
|
75
|
+
const tool = process.platform === "win32" ? "where.exe" : "which";
|
|
76
|
+
console.log(` '${tool} ${value}' returned no matches.`);
|
|
77
|
+
const confirm = (await rl.question(" Use it anyway? [y/N]: ")).trim().toLowerCase();
|
|
78
|
+
if (confirm !== "y" && confirm !== "yes") {
|
|
79
|
+
console.log(" Skipped.");
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
console.log(` Unrecognised choice '${answer}' — skipped.`);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
rl.close();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
20
92
|
export async function start(opts) {
|
|
21
93
|
console.log("");
|
|
22
94
|
console.log(" fellow-agents");
|
|
@@ -43,6 +115,34 @@ export async function start(opts) {
|
|
|
43
115
|
}
|
|
44
116
|
catch { }
|
|
45
117
|
}
|
|
118
|
+
// CLI preference prompt — fires when preferences.json is missing or has no cliPreference.
|
|
119
|
+
// Independent of the first-run marker so a user who wipes preferences.json (or upgrades from
|
|
120
|
+
// a pre-0.0.22 install) gets prompted on the next run. Non-interactive sessions skip silently.
|
|
121
|
+
const existingPrefs = readPreferences();
|
|
122
|
+
if (existingPrefs === null || !existingPrefs.cliPreference) {
|
|
123
|
+
const chosen = await promptForCliPreference();
|
|
124
|
+
if (chosen) {
|
|
125
|
+
try {
|
|
126
|
+
writePreferences({
|
|
127
|
+
...(existingPrefs ?? {}),
|
|
128
|
+
cliPreference: chosen,
|
|
129
|
+
updatedBy: "first-run-prompt",
|
|
130
|
+
});
|
|
131
|
+
console.log(` CLI preference set: ${chosen}`);
|
|
132
|
+
console.log("");
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
console.error(` Failed to write preferences: ${err.message}`);
|
|
136
|
+
console.error(` Continuing without a stored preference.`);
|
|
137
|
+
console.log("");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else if (!input.isTTY) {
|
|
141
|
+
console.log(" (non-interactive — skipping CLI preference setup)");
|
|
142
|
+
console.log(` Set later with: fellow-agents config set cliPreference <name-or-path>`);
|
|
143
|
+
console.log("");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
46
146
|
// Auto-create fellow-agents/ subdirectory if CWD doesn't already have workspaces/
|
|
47
147
|
let workDir = resolve(opts.dir);
|
|
48
148
|
if (!existsSync(join(workDir, "workspaces"))) {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync, unlinkSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import { dataDir } from "./paths.js";
|
|
5
|
+
/** ~/.fellow-agents/preferences.json */
|
|
6
|
+
export const preferencesFile = join(dataDir, "preferences.json");
|
|
7
|
+
export const CURRENT_SCHEMA = 1;
|
|
8
|
+
/** Known CLI names — used by autoDetectClis() and as the default preset list for first-run prompt. */
|
|
9
|
+
export const KNOWN_CLIS = ["claude", "copilot", "pi"];
|
|
10
|
+
/** Known preference keys (for config get/set validation + --help listing). */
|
|
11
|
+
export const KNOWN_KEYS = ["cliPreference"];
|
|
12
|
+
export const KEY_SCHEMAS = {
|
|
13
|
+
cliPreference: {
|
|
14
|
+
type: "select",
|
|
15
|
+
label: "Default CLI",
|
|
16
|
+
description: "The CLI launched by pty-win's play button in each new tab.",
|
|
17
|
+
options: [...KNOWN_CLIS],
|
|
18
|
+
allowCustom: true,
|
|
19
|
+
customLabel: "Custom path…",
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Read preferences from disk.
|
|
24
|
+
* Returns null if file is missing.
|
|
25
|
+
* Returns { schema: CURRENT_SCHEMA } with a warning on the console if file exists but is malformed —
|
|
26
|
+
* caller can decide whether to re-prompt.
|
|
27
|
+
*/
|
|
28
|
+
export function readPreferences() {
|
|
29
|
+
if (!existsSync(preferencesFile))
|
|
30
|
+
return null;
|
|
31
|
+
try {
|
|
32
|
+
const raw = readFileSync(preferencesFile, "utf-8");
|
|
33
|
+
const parsed = JSON.parse(raw);
|
|
34
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
35
|
+
throw new Error("not an object");
|
|
36
|
+
if (typeof parsed.schema !== "number")
|
|
37
|
+
throw new Error("missing schema");
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
console.error(` WARNING: ${preferencesFile} is malformed (${err.message}). Treating as unset.`);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Write preferences atomically (temp + renameSync — atomic on Windows from Node 10+).
|
|
47
|
+
* Always stamps `updatedAt` to now and `updatedBy` to the supplied value.
|
|
48
|
+
* Ensures dataDir exists. Cleans up the temp file on failure.
|
|
49
|
+
*/
|
|
50
|
+
export function writePreferences(prefs) {
|
|
51
|
+
mkdirSync(dataDir, { recursive: true });
|
|
52
|
+
const next = {
|
|
53
|
+
schema: prefs.schema ?? CURRENT_SCHEMA,
|
|
54
|
+
cliPreference: prefs.cliPreference,
|
|
55
|
+
updatedAt: new Date().toISOString(),
|
|
56
|
+
updatedBy: prefs.updatedBy,
|
|
57
|
+
};
|
|
58
|
+
const tmp = `${preferencesFile}.tmp`;
|
|
59
|
+
try {
|
|
60
|
+
writeFileSync(tmp, JSON.stringify(next, null, 2) + "\n", "utf-8");
|
|
61
|
+
renameSync(tmp, preferencesFile);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
try {
|
|
65
|
+
if (existsSync(tmp))
|
|
66
|
+
unlinkSync(tmp);
|
|
67
|
+
}
|
|
68
|
+
catch { }
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
return next;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Look up a CLI on PATH. Returns the resolved full path (first hit), or null if not found.
|
|
75
|
+
* Uses `where.exe` on Windows and `which -a` on Unix. Never throws.
|
|
76
|
+
*/
|
|
77
|
+
export function lookupCli(name) {
|
|
78
|
+
const cmd = process.platform === "win32" ? `where.exe ${name}` : `which ${name}`;
|
|
79
|
+
try {
|
|
80
|
+
const out = execSync(cmd, { stdio: ["ignore", "pipe", "ignore"] })
|
|
81
|
+
.toString()
|
|
82
|
+
.split(/\r?\n/)
|
|
83
|
+
.map((s) => s.trim())
|
|
84
|
+
.filter(Boolean);
|
|
85
|
+
return out.length > 0 ? out[0] : null;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Auto-detect which of the KNOWN_CLIS are on PATH.
|
|
93
|
+
* Returns the subset that resolved, preserving the KNOWN_CLIS priority order.
|
|
94
|
+
*/
|
|
95
|
+
export function autoDetectClis() {
|
|
96
|
+
return KNOWN_CLIS.filter((name) => lookupCli(name) !== null);
|
|
97
|
+
}
|