@phnx-labs/agents-cli 1.20.57 → 1.20.59
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/CHANGELOG.md +30 -0
- package/README.md +40 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/defaults.js +24 -0
- package/dist/commands/exec.js +29 -5
- package/dist/commands/output.d.ts +19 -0
- package/dist/commands/output.js +333 -0
- package/dist/commands/secrets.js +34 -25
- package/dist/commands/versions.js +11 -3
- package/dist/commands/view.js +19 -4
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +21 -0
- package/dist/lib/agents.js +42 -14
- package/dist/lib/daemon.d.ts +5 -5
- package/dist/lib/daemon.js +65 -17
- package/dist/lib/git.d.ts +9 -0
- package/dist/lib/git.js +12 -0
- package/dist/lib/hosts/dispatch.d.ts +21 -0
- package/dist/lib/hosts/dispatch.js +88 -5
- package/dist/lib/hosts/passthrough.js +1 -0
- package/dist/lib/mcp.js +1 -1
- package/dist/lib/output/git-output.d.ts +74 -0
- package/dist/lib/output/git-output.js +213 -0
- package/dist/lib/permissions.d.ts +31 -1
- package/dist/lib/permissions.js +210 -9
- package/dist/lib/project-root.d.ts +65 -0
- package/dist/lib/project-root.js +134 -0
- package/dist/lib/resources/mcp.js +1 -1
- package/dist/lib/resources/permissions.js +5 -1
- package/dist/lib/resources/skills.js +6 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/secrets/agent.d.ts +26 -23
- package/dist/lib/secrets/agent.js +196 -216
- package/dist/lib/secrets/remote.d.ts +7 -2
- package/dist/lib/secrets/remote.js +12 -10
- package/dist/lib/session/active.d.ts +3 -0
- package/dist/lib/session/active.js +1 -0
- package/dist/lib/session/db.d.ts +3 -0
- package/dist/lib/session/db.js +20 -4
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +40 -4
- package/dist/lib/session/parse.js +38 -15
- package/dist/lib/session/state.d.ts +4 -1
- package/dist/lib/session/state.js +18 -1
- package/dist/lib/session/types.d.ts +10 -0
- package/dist/lib/staleness/detectors/permissions.js +64 -1
- package/dist/lib/staleness/detectors/subagents.js +30 -0
- package/dist/lib/staleness/writers/commands.js +3 -3
- package/dist/lib/staleness/writers/subagents.js +13 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/subagents.d.ts +23 -0
- package/dist/lib/subagents.js +161 -12
- package/dist/lib/teams/agents.d.ts +14 -0
- package/dist/lib/teams/agents.js +158 -22
- package/dist/lib/types.d.ts +13 -0
- package/dist/lib/versions.d.ts +39 -0
- package/dist/lib/versions.js +199 -12
- package/package.json +1 -1
- package/scripts/postinstall.js +26 -11
package/dist/commands/secrets.js
CHANGED
|
@@ -12,14 +12,14 @@ import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/widt
|
|
|
12
12
|
import * as fs from 'fs';
|
|
13
13
|
import { SSH_TARGET_RE, assertValidSshTarget, sshExec } from '../lib/ssh-exec.js';
|
|
14
14
|
import { quoteWin32ExecArg, composeWin32CommandLine } from '../lib/platform/index.js';
|
|
15
|
-
import { ensureDaemonStarted } from '../lib/daemon.js';
|
|
15
|
+
import { ensureDaemonStarted, isDaemonRunning } from '../lib/daemon.js';
|
|
16
16
|
import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveSshTarget, } from '../lib/secrets/remote.js';
|
|
17
17
|
import { remoteShellFor, buildWindowsStdinImportCommand } from '../lib/hosts/remote-cmd.js';
|
|
18
18
|
import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
|
|
19
19
|
import { bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, } from '../lib/secrets/bundles.js';
|
|
20
20
|
import { getKeychainToken, getKeychainTokens, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
|
|
21
21
|
import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
|
|
22
|
-
import { DEFAULT_TTL_MS, agentLoad, agentLock, agentStatus, ensureAgentRunning,
|
|
22
|
+
import { DEFAULT_TTL_MS, agentLoad, agentLock, agentPing, agentStatus, ensureAgentRunning, runAgentLoadFromStdin, runSecretsAgent, uninstallSecretsAgentService, } from '../lib/secrets/agent.js';
|
|
23
23
|
import { parseDuration } from '../lib/hooks/cache.js';
|
|
24
24
|
import { emit } from '../lib/events.js';
|
|
25
25
|
import { registerCommandGroups, setHelpSections } from '../lib/help.js';
|
|
@@ -328,12 +328,12 @@ async function browseRemote(targets, args, tty) {
|
|
|
328
328
|
if (tty) {
|
|
329
329
|
for (const t of targets) {
|
|
330
330
|
const target = await resolveSshTarget(t);
|
|
331
|
-
render(t, remoteSecretsRaw(target, args, { tty: true }));
|
|
331
|
+
render(t, remoteSecretsRaw(target, args, { tty: true, osLookupName: t }));
|
|
332
332
|
}
|
|
333
333
|
}
|
|
334
334
|
else {
|
|
335
|
-
const resolved = await Promise.all(targets.map((t) => resolveSshTarget(t)));
|
|
336
|
-
const results = resolved.map((target) => remoteSecretsRaw(target, args));
|
|
335
|
+
const resolved = await Promise.all(targets.map(async (t) => ({ name: t, target: await resolveSshTarget(t) })));
|
|
336
|
+
const results = resolved.map(({ name, target }) => remoteSecretsRaw(target, args, { osLookupName: name }));
|
|
337
337
|
targets.forEach((t, i) => render(t, results[i]));
|
|
338
338
|
}
|
|
339
339
|
if (failures > 0)
|
|
@@ -1476,7 +1476,7 @@ Examples:
|
|
|
1476
1476
|
// engine (BatchMode, ConnectTimeout, keepalive, control-socket
|
|
1477
1477
|
// reuse) via the same path the READ inverse (`remoteResolveEnv`)
|
|
1478
1478
|
// uses. `--from -` reads the .env off ssh stdin.
|
|
1479
|
-
res = remoteSecretsRaw(host, ['import', resolvedBundleName, '--from', '-', ...(opts.force ? ['--force'] : [])], { input: dotenv });
|
|
1479
|
+
res = remoteSecretsRaw(host, ['import', resolvedBundleName, '--from', '-', ...(opts.force ? ['--force'] : [])], { input: dotenv, osLookupName: host });
|
|
1480
1480
|
}
|
|
1481
1481
|
if (res.code === null) {
|
|
1482
1482
|
failures++;
|
|
@@ -1584,7 +1584,7 @@ Examples:
|
|
|
1584
1584
|
// locally instead.
|
|
1585
1585
|
const { assertRemoteBundleFlagsUnsupported } = await import('../lib/secrets/bundles.js');
|
|
1586
1586
|
assertRemoteBundleFlagsUnsupported(bundleName, execOpts.host, { keys: keysSubset, allowExpired: execOpts.allowExpired }, { keysFlag: '--keys', allowExpiredFlag: '--allow-expired' });
|
|
1587
|
-
secretEnv = await remoteResolveEnv(await resolveSshTarget(execOpts.host), bundleName);
|
|
1587
|
+
secretEnv = await remoteResolveEnv(await resolveSshTarget(execOpts.host), bundleName, { osLookupName: execOpts.host });
|
|
1588
1588
|
}
|
|
1589
1589
|
else {
|
|
1590
1590
|
const { readAndResolveBundleEnv } = await import('../lib/secrets/bundles.js');
|
|
@@ -1738,7 +1738,7 @@ Examples:
|
|
|
1738
1738
|
// sees a real TTY, which requires our local terminal to pass straight
|
|
1739
1739
|
// through. The remote's prompt + output stream to this terminal; we get
|
|
1740
1740
|
// back only the exit code.
|
|
1741
|
-
const code = remoteSecretsStream(target, unlockArgs);
|
|
1741
|
+
const code = remoteSecretsStream(target, unlockArgs, { osLookupName: h });
|
|
1742
1742
|
if (code === 0) {
|
|
1743
1743
|
console.log(chalk.green(`${h}: unlocked`));
|
|
1744
1744
|
}
|
|
@@ -1770,13 +1770,13 @@ Examples:
|
|
|
1770
1770
|
ttlMs = secs * 1000;
|
|
1771
1771
|
}
|
|
1772
1772
|
if (!(await ensureAgentRunning())) {
|
|
1773
|
-
console.error(chalk.red('Could not start the secrets
|
|
1773
|
+
console.error(chalk.red('Could not start the secrets broker.'));
|
|
1774
1774
|
process.exit(1);
|
|
1775
1775
|
}
|
|
1776
1776
|
// #415: the daemon should be always-on for any background need, not only
|
|
1777
|
-
// after `routines add`.
|
|
1778
|
-
// the
|
|
1779
|
-
//
|
|
1777
|
+
// after `routines add`. `ensureAgentRunning` prefers the daemon (it hosts
|
|
1778
|
+
// the broker, #416), but can fall back to a one-off broker spawn when the
|
|
1779
|
+
// daemon can't come up — so ensure the daemon is up regardless. Idempotent
|
|
1780
1780
|
// (single-instance start lock, #414) and best-effort — never blocks unlock.
|
|
1781
1781
|
ensureDaemonStarted();
|
|
1782
1782
|
let loaded = 0;
|
|
@@ -1830,13 +1830,14 @@ Examples:
|
|
|
1830
1830
|
console.log(chalk.gray('secrets-agent is macOS-only.'));
|
|
1831
1831
|
return;
|
|
1832
1832
|
}
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1833
|
+
const brokerUp = (await agentPing()).reachable;
|
|
1834
|
+
console.log(chalk.gray('broker: ') +
|
|
1835
|
+
(brokerUp
|
|
1836
|
+
? chalk.green('running') + chalk.gray(isDaemonRunning() ? ' (hosted by the daemon)' : ' (standalone)')
|
|
1837
|
+
: chalk.yellow('not running — starts on demand, or run `agents secrets start` to bring the daemon up now')));
|
|
1837
1838
|
const entries = await agentStatus();
|
|
1838
1839
|
if (entries.length === 0) {
|
|
1839
|
-
console.log(chalk.gray('No bundles unlocked. The secrets
|
|
1840
|
+
console.log(chalk.gray('No bundles unlocked. The secrets broker is idle or not running.'));
|
|
1840
1841
|
console.log(chalk.gray('Try: agents secrets unlock <bundle>'));
|
|
1841
1842
|
return;
|
|
1842
1843
|
}
|
|
@@ -1887,29 +1888,37 @@ Examples:
|
|
|
1887
1888
|
});
|
|
1888
1889
|
cmd
|
|
1889
1890
|
.command('start')
|
|
1890
|
-
.description('
|
|
1891
|
+
.description('Bring up the always-on daemon that hosts the secrets broker (macOS). Survives heavy load; reads connect instantly.')
|
|
1891
1892
|
.action(async () => {
|
|
1892
1893
|
if (process.platform !== 'darwin') {
|
|
1893
|
-
console.error(chalk.red('secrets
|
|
1894
|
+
console.error(chalk.red('The secrets broker is macOS-only.'));
|
|
1894
1895
|
process.exit(1);
|
|
1895
1896
|
}
|
|
1896
|
-
process.stdout.write(chalk.gray('
|
|
1897
|
-
|
|
1898
|
-
|
|
1897
|
+
process.stdout.write(chalk.gray('Starting the daemon…\n'));
|
|
1898
|
+
ensureDaemonStarted();
|
|
1899
|
+
// The daemon hosts the broker socket-first; wait briefly for it to answer.
|
|
1900
|
+
const deadline = Date.now() + 10000;
|
|
1901
|
+
let reachable = (await agentPing()).reachable;
|
|
1902
|
+
while (!reachable && Date.now() < deadline) {
|
|
1903
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
1904
|
+
reachable = (await agentPing()).reachable;
|
|
1905
|
+
}
|
|
1906
|
+
if (reachable) {
|
|
1907
|
+
console.log(chalk.green('secrets broker running.') + chalk.gray(' Hosted by the always-on daemon; unlock/auto-cache now connect instantly.'));
|
|
1899
1908
|
}
|
|
1900
1909
|
else {
|
|
1901
|
-
console.error(chalk.red('
|
|
1910
|
+
console.error(chalk.red('Daemon started but the broker did not become reachable in time (machine may be heavily loaded — it will keep retrying).'));
|
|
1902
1911
|
process.exit(1);
|
|
1903
1912
|
}
|
|
1904
1913
|
});
|
|
1905
1914
|
cmd
|
|
1906
1915
|
.command('stop')
|
|
1907
|
-
.description('
|
|
1916
|
+
.description('Lock all bundles and retire any legacy standalone service. The always-on daemon (which hosts the broker) is left running.')
|
|
1908
1917
|
.action(async () => {
|
|
1909
1918
|
if (process.platform !== 'darwin')
|
|
1910
1919
|
return;
|
|
1911
1920
|
await uninstallSecretsAgentService();
|
|
1912
|
-
console.log(chalk.green('
|
|
1921
|
+
console.log(chalk.green('Locked all bundles.') + chalk.gray(' The broker stays hosted by the always-on daemon; a legacy standalone service, if any, was retired.'));
|
|
1913
1922
|
});
|
|
1914
1923
|
cmd
|
|
1915
1924
|
.command('_agent-run', { hidden: true })
|
|
@@ -4,7 +4,7 @@ import ora from 'ora';
|
|
|
4
4
|
import * as fs from 'fs';
|
|
5
5
|
import * as path from 'path';
|
|
6
6
|
import { select, confirm, checkbox } from '@inquirer/prompts';
|
|
7
|
-
import { AGENTS, ALL_AGENT_IDS, getAccountEmail, getAccountInfo, agentLabel, warnAgentDeprecated, } from '../lib/agents.js';
|
|
7
|
+
import { AGENTS, ALL_AGENT_IDS, getAccountEmail, getAccountInfo, agentLabel, warnAgentDeprecated, isSelfUpdatingAgent, } from '../lib/agents.js';
|
|
8
8
|
import { formatUsageSummary, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
|
|
9
9
|
import { viewAction } from './view.js';
|
|
10
10
|
import { readManifest, writeManifest, createDefaultManifest } from '../lib/manifest.js';
|
|
@@ -67,7 +67,7 @@ async function setDefaultVersion(agent, installedVersion) {
|
|
|
67
67
|
createVersionedAlias(agent, installedVersion);
|
|
68
68
|
const symlinkResult = await switchConfigSymlink(agent, installedVersion);
|
|
69
69
|
if (symlinkResult.success) {
|
|
70
|
-
console.log(chalk.green(' Set as default'));
|
|
70
|
+
console.log(chalk.green(isSelfUpdatingAgent(agent) ? ' Set as active config profile' : ' Set as default'));
|
|
71
71
|
if (symlinkResult.backupPath) {
|
|
72
72
|
console.log(chalk.gray(` Backed up existing config to: ${symlinkResult.backupPath}`));
|
|
73
73
|
}
|
|
@@ -752,7 +752,15 @@ export function registerVersionsCommands(program) {
|
|
|
752
752
|
warnIfShimShadowed(agentId);
|
|
753
753
|
const useEmail = await getAccountEmail(agentId, getVersionHomePath(agentId, finalVersion));
|
|
754
754
|
const useEmailStr = useEmail ? chalk.cyan(` (${useEmail})`) : '';
|
|
755
|
-
|
|
755
|
+
// Self-updating agents are one binary; `use` only swaps the config
|
|
756
|
+
// symlink (the profile), it does not change which binary runs — so say
|
|
757
|
+
// "profile", not "version", to match what actually happened.
|
|
758
|
+
if (isSelfUpdatingAgent(agentId)) {
|
|
759
|
+
console.log(chalk.green(`Switched ${agentLabel(agentConfig.id)} to config profile ${finalVersion}`) + useEmailStr);
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
console.log(chalk.green(`Set ${agentLabel(agentConfig.id)}@${finalVersion} as global default`) + useEmailStr);
|
|
763
|
+
}
|
|
756
764
|
}
|
|
757
765
|
}
|
|
758
766
|
catch (err) {
|
package/dist/commands/view.js
CHANGED
|
@@ -7,7 +7,7 @@ import * as path from 'path';
|
|
|
7
7
|
import { AGENTS, ALL_AGENT_IDS, getAllCliStates, getAccountInfo, resolveAgentName, formatAgentError, agentLabel, colorAgent, } from '../lib/agents.js';
|
|
8
8
|
import { deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
|
|
9
9
|
import { readManifest } from '../lib/manifest.js';
|
|
10
|
-
import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, printTrashFooter, reconcileStaleLatestForAgent, } from '../lib/versions.js';
|
|
10
|
+
import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, printTrashFooter, reconcileStaleLatestForAgent, isGlobalBinaryAgent, getLiveVersion, } from '../lib/versions.js';
|
|
11
11
|
import { ensureVersionedAliasCurrent, removeShim, } from '../lib/shims.js';
|
|
12
12
|
import { getAgentResources } from '../lib/resources.js';
|
|
13
13
|
import { resolveVersionFilter, AgentSpecError } from '../lib/agent-spec/index.js';
|
|
@@ -358,6 +358,19 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
358
358
|
profileOnly.push(agentId);
|
|
359
359
|
}
|
|
360
360
|
}
|
|
361
|
+
// For self-updating global-binary agents (droid) the on-disk version-dir name
|
|
362
|
+
// is a stale label — the real version is whatever `<cli> --version` reports.
|
|
363
|
+
// Resolve it once so every row/width pass shows the live version, while the
|
|
364
|
+
// per-version home + account lookups keep using the real dir name.
|
|
365
|
+
const liveVersionByAgent = new Map();
|
|
366
|
+
await Promise.all(versionManaged
|
|
367
|
+
.filter((agentId) => isGlobalBinaryAgent(agentId))
|
|
368
|
+
.map(async (agentId) => {
|
|
369
|
+
const live = await getLiveVersion(agentId);
|
|
370
|
+
if (live)
|
|
371
|
+
liveVersionByAgent.set(agentId, live);
|
|
372
|
+
}));
|
|
373
|
+
const displayVersion = (agentId, dirVersion) => liveVersionByAgent.get(agentId) ?? dirVersion;
|
|
361
374
|
// Show version-managed agents
|
|
362
375
|
if (versionManaged.length > 0) {
|
|
363
376
|
// Calculate column widths across all agents for alignment
|
|
@@ -370,7 +383,8 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
370
383
|
const versions = listInstalledVersions(agentId);
|
|
371
384
|
const globalDefault = getGlobalDefault(agentId);
|
|
372
385
|
for (const v of versions) {
|
|
373
|
-
const
|
|
386
|
+
const shown = displayVersion(agentId, v);
|
|
387
|
+
const label = v === globalDefault ? `${shown} (default)` : shown;
|
|
374
388
|
maxVerLabel = Math.max(maxVerLabel, label.length);
|
|
375
389
|
const rawInfo = infoMap.get(`${agentId}:${v}`);
|
|
376
390
|
const info = rawInfo ? mergeCanonical(rawInfo) : undefined;
|
|
@@ -423,9 +437,10 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
423
437
|
});
|
|
424
438
|
for (const version of sortedVersions) {
|
|
425
439
|
const isDefault = version === globalDefault;
|
|
426
|
-
const
|
|
440
|
+
const shown = displayVersion(agentId, version);
|
|
441
|
+
const base = isDefault ? `${shown} (default)` : shown;
|
|
427
442
|
const padded = base.padEnd(maxVerLabel);
|
|
428
|
-
const label = isDefault ? `${
|
|
443
|
+
const label = isDefault ? `${shown}${chalk.green(' (default)')}${' '.repeat(maxVerLabel - base.length)}` : padded;
|
|
429
444
|
const rawInfo = infoMap.get(`${agentId}:${version}`);
|
|
430
445
|
const vInfo = rawInfo ? mergeCanonical(rawInfo) : undefined;
|
|
431
446
|
const usageKey = getUsageLookupKey(vInfo);
|
package/dist/index.js
CHANGED
|
@@ -50,7 +50,7 @@ if (IS_DEV_BUILD) {
|
|
|
50
50
|
// module on each invocation (which loaded the whole ~50-module tree before the
|
|
51
51
|
// first byte of output), the registry maps a command name to a thunk that
|
|
52
52
|
// imports only what that command needs. See src/lib/startup/command-registry.ts.
|
|
53
|
-
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadFeed, } from './lib/startup/command-registry.js';
|
|
53
|
+
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadOutput, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadFeed, } from './lib/startup/command-registry.js';
|
|
54
54
|
import { applyGlobalHelpConventions } from './lib/help.js';
|
|
55
55
|
import { renderWhatsNew } from './lib/whats-new.js';
|
|
56
56
|
import { emit, redactArgs } from './lib/events.js';
|
|
@@ -690,6 +690,7 @@ async function registerAllEagerCommands() {
|
|
|
690
690
|
await reg(loadFactory);
|
|
691
691
|
await reg(loadUsage);
|
|
692
692
|
await reg(loadCost);
|
|
693
|
+
await reg(loadOutput);
|
|
693
694
|
await reg(loadBudget);
|
|
694
695
|
await reg(loadAlias);
|
|
695
696
|
await reg(loadPty);
|
package/dist/lib/agents.d.ts
CHANGED
|
@@ -34,6 +34,27 @@ export declare function findInPath(command: string): string | null;
|
|
|
34
34
|
export declare const AGENTS: Record<AgentId, AgentConfig>;
|
|
35
35
|
/** All registered agent IDs derived from the AGENTS registry. */
|
|
36
36
|
export declare const ALL_AGENT_IDS: AgentId[];
|
|
37
|
+
/**
|
|
38
|
+
* A self-updating agent is a single global binary installed by an official
|
|
39
|
+
* `curl … | sh` / `brew install` script that carries NO version token — the
|
|
40
|
+
* installer can only ever fetch the *current* release, and the binary then keeps
|
|
41
|
+
* itself up to date in place (droid, grok, antigravity, cursor, hermes, forge,
|
|
42
|
+
* kiro, goose). There is no semver to pin, so agents-cli must not model these as
|
|
43
|
+
* having multiple installable version-homes the way it does for npm-packaged
|
|
44
|
+
* agents (claude, codex, kimi, …).
|
|
45
|
+
*
|
|
46
|
+
* The predicate is `!npmPackage && installScript && !installScript.includes('VERSION')`:
|
|
47
|
+
* - `npmPackage` empty → not installed from npm, so `agents add x@1.2.3`
|
|
48
|
+
* can't resolve a registry version.
|
|
49
|
+
* - `installScript` present → it IS installed by a script (not unmanaged).
|
|
50
|
+
* - no `VERSION` placeholder → the script has no slot for a pinned version
|
|
51
|
+
* (contrast: an installer templated with `VERSION`
|
|
52
|
+
* could pin, and is NOT self-updating).
|
|
53
|
+
*
|
|
54
|
+
* Route every "is this a pinnable, multi-version agent?" decision through here —
|
|
55
|
+
* never a scattered `agent === 'droid'`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function isSelfUpdatingAgent(agent: AgentId): boolean;
|
|
37
58
|
/** Get the chalk color function for an agent. Works for any AgentId or SessionAgentId. */
|
|
38
59
|
export declare function colorAgent(agentId: string): (s: string) => string;
|
|
39
60
|
/** Return the agent's display name, colored. */
|
package/dist/lib/agents.js
CHANGED
|
@@ -236,7 +236,7 @@ export const AGENTS = {
|
|
|
236
236
|
cloudProvider: 'codex',
|
|
237
237
|
// Subagents: multi-agent plumbing since 0.117.0; custom agents as
|
|
238
238
|
// ~/.codex/agents/*.toml (name, description, developer_instructions).
|
|
239
|
-
capabilities: { hooks: { since: '0.116.0' }, mcp: true, mcpHttp: true, mcpHeaders: false, allowlist:
|
|
239
|
+
capabilities: { hooks: { since: '0.116.0' }, mcp: true, mcpHttp: true, mcpHeaders: false, allowlist: { since: '0.138.0' }, skills: true, commands: { until: '0.117.0' }, plugins: { since: '0.128.0' }, subagents: { since: '0.117.0' }, rules: { file: 'AGENTS.md' }, workflows: false, memory: true, modes: ['plan', 'edit', 'skip'] },
|
|
240
240
|
},
|
|
241
241
|
gemini: {
|
|
242
242
|
id: 'gemini',
|
|
@@ -290,7 +290,7 @@ export const AGENTS = {
|
|
|
290
290
|
format: 'markdown',
|
|
291
291
|
variableSyntax: '$ARGUMENTS',
|
|
292
292
|
supportsHooks: true,
|
|
293
|
-
capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist:
|
|
293
|
+
capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: true, skills: true, commands: true, plugins: true, subagents: false, rules: { file: '.cursorrules' }, workflows: false, memory: false, modes: ['edit', 'skip'] }, // allowlist: ~/.cursor/cli-config.json
|
|
294
294
|
},
|
|
295
295
|
opencode: {
|
|
296
296
|
id: 'opencode',
|
|
@@ -310,7 +310,7 @@ export const AGENTS = {
|
|
|
310
310
|
format: 'markdown',
|
|
311
311
|
variableSyntax: '$ARGUMENTS',
|
|
312
312
|
supportsHooks: false,
|
|
313
|
-
capabilities: { hooks: false, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: { since: '1.1.1' }, skills: true, commands: true, plugins: true, subagents: true, rules: { file: 'AGENTS.md' }, workflows: false, memory: false, modes: ['plan', 'edit'] },
|
|
313
|
+
capabilities: { hooks: false, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: { since: '1.1.1' }, skills: true, commands: true, plugins: true, subagents: true, rules: { file: 'AGENTS.md' }, workflows: false, memory: false, modes: ['plan', 'edit'] },
|
|
314
314
|
},
|
|
315
315
|
openclaw: {
|
|
316
316
|
id: 'openclaw',
|
|
@@ -353,7 +353,7 @@ export const AGENTS = {
|
|
|
353
353
|
format: 'markdown',
|
|
354
354
|
variableSyntax: '$ARGUMENTS',
|
|
355
355
|
supportsHooks: true,
|
|
356
|
-
capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: false, skills: true, commands: true, plugins: true, subagents:
|
|
356
|
+
capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: false, skills: true, commands: true, plugins: true, subagents: { since: '0.0.353' }, rules: { file: 'AGENTS.md' }, workflows: false, memory: false, modes: ['plan', 'edit', 'auto', 'skip'] },
|
|
357
357
|
},
|
|
358
358
|
amp: {
|
|
359
359
|
id: 'amp',
|
|
@@ -393,7 +393,7 @@ export const AGENTS = {
|
|
|
393
393
|
format: 'markdown',
|
|
394
394
|
variableSyntax: '$ARGUMENTS',
|
|
395
395
|
supportsHooks: true,
|
|
396
|
-
capabilities: { hooks: { since: '0.10.0' }, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist:
|
|
396
|
+
capabilities: { hooks: { since: '0.10.0' }, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: { since: '2.8.0' }, skills: true, commands: true, plugins: false, subagents: { since: '1.23.0' }, rules: { file: 'AGENTS.md' }, workflows: false, memory: false, modes: ['edit'] },
|
|
397
397
|
},
|
|
398
398
|
goose: {
|
|
399
399
|
id: 'goose',
|
|
@@ -405,7 +405,10 @@ export const AGENTS = {
|
|
|
405
405
|
configDir: path.join(HOME, '.config', 'goose'),
|
|
406
406
|
commandsDir: path.join(HOME, '.config', 'goose', 'commands'),
|
|
407
407
|
commandsSubdir: 'commands',
|
|
408
|
-
|
|
408
|
+
// Goose reads skills directly from central ~/.agents/skills/ via the Summon
|
|
409
|
+
// extension (block-goose-cli ≥ 1.25.0). No per-version copy is written.
|
|
410
|
+
skillsDir: path.join(HOME, '.agents', 'skills'),
|
|
411
|
+
nativeAgentsSkillsDir: true,
|
|
409
412
|
// Hooks: Open Plugins format — auto-discovered from
|
|
410
413
|
// ~/.agents/plugins/<name>/hooks/hooks.json (shipped block-goose-cli
|
|
411
414
|
// ≥ 1.34.0). See registerHooksForGoose.
|
|
@@ -416,7 +419,7 @@ export const AGENTS = {
|
|
|
416
419
|
supportsHooks: true,
|
|
417
420
|
// Plugins: Open Plugins under ~/.agents/plugins/<name>/ (same layout as
|
|
418
421
|
// agents-cli source). Version isolation copies into versionHome/.agents/plugins/.
|
|
419
|
-
capabilities: { hooks: { since: '1.34.0' }, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: false, skills:
|
|
422
|
+
capabilities: { hooks: { since: '1.34.0' }, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: false, skills: { since: '1.25.0' }, commands: false, plugins: true, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, memory: false, modes: ['edit'] },
|
|
420
423
|
},
|
|
421
424
|
// Google Antigravity CLI (`agy`) — official replacement for Gemini CLI as of IO 2026.
|
|
422
425
|
// configDir nests inside `~/.gemini/` since agy shares the parent dir with the Gemini
|
|
@@ -560,7 +563,7 @@ export const AGENTS = {
|
|
|
560
563
|
authFiles: ['auth.v2.file', 'auth.v2.key'],
|
|
561
564
|
commandsDir: path.join(HOME, '.factory', 'commands'),
|
|
562
565
|
commandsSubdir: 'commands',
|
|
563
|
-
skillsDir: '',
|
|
566
|
+
skillsDir: path.join(HOME, '.factory', 'skills'),
|
|
564
567
|
hooksDir: 'hooks',
|
|
565
568
|
pluginManifestDir: '.factory-plugin',
|
|
566
569
|
instructionsFile: 'AGENTS.md',
|
|
@@ -575,8 +578,8 @@ export const AGENTS = {
|
|
|
575
578
|
mcp: true,
|
|
576
579
|
mcpHttp: false,
|
|
577
580
|
mcpHeaders: false,
|
|
578
|
-
allowlist:
|
|
579
|
-
skills:
|
|
581
|
+
allowlist: { since: '0.57.5' },
|
|
582
|
+
skills: { since: '0.26.0' },
|
|
580
583
|
commands: true,
|
|
581
584
|
plugins: true,
|
|
582
585
|
subagents: true,
|
|
@@ -661,6 +664,30 @@ export const AGENTS = {
|
|
|
661
664
|
};
|
|
662
665
|
/** All registered agent IDs derived from the AGENTS registry. */
|
|
663
666
|
export const ALL_AGENT_IDS = Object.keys(AGENTS);
|
|
667
|
+
/**
|
|
668
|
+
* A self-updating agent is a single global binary installed by an official
|
|
669
|
+
* `curl … | sh` / `brew install` script that carries NO version token — the
|
|
670
|
+
* installer can only ever fetch the *current* release, and the binary then keeps
|
|
671
|
+
* itself up to date in place (droid, grok, antigravity, cursor, hermes, forge,
|
|
672
|
+
* kiro, goose). There is no semver to pin, so agents-cli must not model these as
|
|
673
|
+
* having multiple installable version-homes the way it does for npm-packaged
|
|
674
|
+
* agents (claude, codex, kimi, …).
|
|
675
|
+
*
|
|
676
|
+
* The predicate is `!npmPackage && installScript && !installScript.includes('VERSION')`:
|
|
677
|
+
* - `npmPackage` empty → not installed from npm, so `agents add x@1.2.3`
|
|
678
|
+
* can't resolve a registry version.
|
|
679
|
+
* - `installScript` present → it IS installed by a script (not unmanaged).
|
|
680
|
+
* - no `VERSION` placeholder → the script has no slot for a pinned version
|
|
681
|
+
* (contrast: an installer templated with `VERSION`
|
|
682
|
+
* could pin, and is NOT self-updating).
|
|
683
|
+
*
|
|
684
|
+
* Route every "is this a pinnable, multi-version agent?" decision through here —
|
|
685
|
+
* never a scattered `agent === 'droid'`.
|
|
686
|
+
*/
|
|
687
|
+
export function isSelfUpdatingAgent(agent) {
|
|
688
|
+
const cfg = AGENTS[agent];
|
|
689
|
+
return !cfg.npmPackage && !!cfg.installScript && !cfg.installScript.includes('VERSION');
|
|
690
|
+
}
|
|
664
691
|
// Capability-filtered agent lists used to live here as `*_CAPABLE_AGENTS`
|
|
665
692
|
// constants. They were a frequent source of silent-skip bugs (e.g. grok
|
|
666
693
|
// rules sync gated on `COMMANDS_CAPABLE_AGENTS`). Use `capableAgents(cap)`
|
|
@@ -1802,8 +1829,8 @@ export function getUserMcpConfigPath(agentId) {
|
|
|
1802
1829
|
// Codex uses TOML config
|
|
1803
1830
|
return path.join(agent.configDir, 'config.toml');
|
|
1804
1831
|
case 'opencode':
|
|
1805
|
-
// OpenCode
|
|
1806
|
-
return path.join(
|
|
1832
|
+
// OpenCode loads ~/.config/opencode/opencode.jsonc (not ~/.opencode/)
|
|
1833
|
+
return path.join(HOME, '.config', 'opencode', 'opencode.jsonc');
|
|
1807
1834
|
case 'cursor':
|
|
1808
1835
|
// Cursor uses mcp.json
|
|
1809
1836
|
return path.join(agent.configDir, 'mcp.json');
|
|
@@ -1841,7 +1868,7 @@ export function getMcpConfigPathForHome(agentId, home) {
|
|
|
1841
1868
|
case 'codex':
|
|
1842
1869
|
return path.join(home, '.codex', 'config.toml');
|
|
1843
1870
|
case 'opencode':
|
|
1844
|
-
return path.join(home, '.opencode', 'opencode.jsonc');
|
|
1871
|
+
return path.join(home, '.config', 'opencode', 'opencode.jsonc');
|
|
1845
1872
|
case 'cursor':
|
|
1846
1873
|
return path.join(home, '.cursor', 'mcp.json');
|
|
1847
1874
|
case 'openclaw':
|
|
@@ -1879,7 +1906,8 @@ function getProjectMcpConfigPath(agentId, cwd = process.cwd()) {
|
|
|
1879
1906
|
case 'codex':
|
|
1880
1907
|
return path.join(cwd, `.${agentId}`, 'config.toml');
|
|
1881
1908
|
case 'opencode':
|
|
1882
|
-
|
|
1909
|
+
// Project config is opencode.jsonc at project root (not .opencode/)
|
|
1910
|
+
return path.join(cwd, 'opencode.jsonc');
|
|
1883
1911
|
case 'cursor':
|
|
1884
1912
|
return path.join(cwd, `.${agentId}`, 'mcp.json');
|
|
1885
1913
|
case 'openclaw':
|
package/dist/lib/daemon.d.ts
CHANGED
|
@@ -82,12 +82,12 @@ export declare function readDaemonClaudeOAuthToken(opts?: {
|
|
|
82
82
|
*/
|
|
83
83
|
export declare function writeOwnerOnlyServiceManifest(filePath: string, content: string): void;
|
|
84
84
|
/** Generate a macOS launchd plist for auto-starting the daemon. */
|
|
85
|
-
export declare function generateLaunchdPlist(oauthToken?: string | null): string;
|
|
85
|
+
export declare function generateLaunchdPlist(oauthToken?: string | null, agentsBin?: string): string;
|
|
86
86
|
/** Generate a Linux systemd user unit for auto-starting the daemon. */
|
|
87
|
-
export declare function generateSystemdUnit(oauthToken?: string | null): string;
|
|
87
|
+
export declare function generateSystemdUnit(oauthToken?: string | null, agentsBin?: string): string;
|
|
88
88
|
export declare function getAgentsBinPath(argv1?: string | undefined, execPath?: string): string;
|
|
89
89
|
/** Start the daemon via launchd, systemd, or as a detached process. */
|
|
90
|
-
export declare function startDaemon(): {
|
|
90
|
+
export declare function startDaemon(agentsBin?: string): {
|
|
91
91
|
pid: number | null;
|
|
92
92
|
method: string;
|
|
93
93
|
};
|
|
@@ -129,8 +129,8 @@ export declare function buildDetachedDaemonEnv(baseEnv?: NodeJS.ProcessEnv, oaut
|
|
|
129
129
|
* Going through `process.execPath` means a real PE/binary is spawned with
|
|
130
130
|
* `detached: true` and no console, so nothing signals the daemon after launch.
|
|
131
131
|
*
|
|
132
|
-
* When the entry isn't a
|
|
133
|
-
*
|
|
132
|
+
* When the entry isn't a Node script (e.g. a native compiled launcher), run it
|
|
133
|
+
* directly — it owns its own runtime resolution.
|
|
134
134
|
*/
|
|
135
135
|
export declare function getDaemonLaunch(agentsBin?: string): {
|
|
136
136
|
command: string;
|
package/dist/lib/daemon.js
CHANGED
|
@@ -674,8 +674,7 @@ export function writeOwnerOnlyServiceManifest(filePath, content) {
|
|
|
674
674
|
fs.writeFileSync(filePath, content, { encoding: 'utf-8', mode: 0o600 });
|
|
675
675
|
}
|
|
676
676
|
/** Generate a macOS launchd plist for auto-starting the daemon. */
|
|
677
|
-
export function generateLaunchdPlist(oauthToken = readDaemonClaudeOAuthToken()) {
|
|
678
|
-
const agentsBin = getAgentsBinPath();
|
|
677
|
+
export function generateLaunchdPlist(oauthToken = readDaemonClaudeOAuthToken(), agentsBin = getAgentsBinPath()) {
|
|
679
678
|
const launch = getDaemonLaunch(agentsBin);
|
|
680
679
|
const logPath = getLogPath();
|
|
681
680
|
const oauthEntry = oauthToken
|
|
@@ -704,7 +703,7 @@ ${[launch.command, ...launch.args].map((arg) => ` <string>${xmlEscape(arg)}</
|
|
|
704
703
|
<key>EnvironmentVariables</key>
|
|
705
704
|
<dict>
|
|
706
705
|
<key>PATH</key>
|
|
707
|
-
<string
|
|
706
|
+
<string>${daemonNodeBinDir()}:/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:${os.homedir()}/.bun/bin</string>${oauthEntry}
|
|
708
707
|
</dict>
|
|
709
708
|
</dict>
|
|
710
709
|
</plist>`;
|
|
@@ -714,8 +713,7 @@ function systemdExecArg(value) {
|
|
|
714
713
|
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
715
714
|
}
|
|
716
715
|
/** Generate a Linux systemd user unit for auto-starting the daemon. */
|
|
717
|
-
export function generateSystemdUnit(oauthToken = readDaemonClaudeOAuthToken()) {
|
|
718
|
-
const agentsBin = getAgentsBinPath();
|
|
716
|
+
export function generateSystemdUnit(oauthToken = readDaemonClaudeOAuthToken(), agentsBin = getAgentsBinPath()) {
|
|
719
717
|
const launch = getDaemonLaunch(agentsBin);
|
|
720
718
|
const execStart = [launch.command, ...launch.args].map(systemdExecArg).join(' ');
|
|
721
719
|
const oauthLine = oauthToken
|
|
@@ -730,7 +728,7 @@ Type=simple
|
|
|
730
728
|
ExecStart=${execStart}
|
|
731
729
|
Restart=always
|
|
732
730
|
RestartSec=10
|
|
733
|
-
Environment=PATH
|
|
731
|
+
Environment=PATH=${daemonNodeBinDir()}:/usr/local/bin:/usr/bin:/bin${oauthLine}
|
|
734
732
|
|
|
735
733
|
[Install]
|
|
736
734
|
WantedBy=default.target`;
|
|
@@ -802,7 +800,7 @@ function readServiceManagerPid(platform = os.platform()) {
|
|
|
802
800
|
return null;
|
|
803
801
|
}
|
|
804
802
|
/** Start the daemon via launchd, systemd, or as a detached process. */
|
|
805
|
-
export function startDaemon() {
|
|
803
|
+
export function startDaemon(agentsBin) {
|
|
806
804
|
if (isDaemonRunning()) {
|
|
807
805
|
const pid = readDaemonPid();
|
|
808
806
|
return { pid, method: 'already-running' };
|
|
@@ -814,7 +812,7 @@ export function startDaemon() {
|
|
|
814
812
|
return { pid, method: 'already-starting' };
|
|
815
813
|
}
|
|
816
814
|
try {
|
|
817
|
-
return startDaemonLocked();
|
|
815
|
+
return startDaemonLocked(agentsBin ?? getAgentsBinPath());
|
|
818
816
|
}
|
|
819
817
|
finally {
|
|
820
818
|
releaseLock();
|
|
@@ -838,7 +836,7 @@ export function ensureDaemonStarted() {
|
|
|
838
836
|
return null;
|
|
839
837
|
}
|
|
840
838
|
}
|
|
841
|
-
function startDaemonLocked() {
|
|
839
|
+
function startDaemonLocked(agentsBin) {
|
|
842
840
|
const platform = os.platform();
|
|
843
841
|
if (platform === 'darwin') {
|
|
844
842
|
try {
|
|
@@ -849,7 +847,7 @@ function startDaemonLocked() {
|
|
|
849
847
|
}
|
|
850
848
|
// The plist may embed a long-lived OAuth token in EnvironmentVariables;
|
|
851
849
|
// create owner-only atomically (no world-readable window before chmod).
|
|
852
|
-
writeOwnerOnlyServiceManifest(plistPath, generateLaunchdPlist());
|
|
850
|
+
writeOwnerOnlyServiceManifest(plistPath, generateLaunchdPlist(undefined, agentsBin));
|
|
853
851
|
try {
|
|
854
852
|
execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
855
853
|
}
|
|
@@ -867,7 +865,7 @@ function startDaemonLocked() {
|
|
|
867
865
|
catch {
|
|
868
866
|
// load threw — fall through to detached spawn
|
|
869
867
|
}
|
|
870
|
-
return startDetached();
|
|
868
|
+
return startDetached({ agentsBin });
|
|
871
869
|
}
|
|
872
870
|
if (platform === 'linux') {
|
|
873
871
|
try {
|
|
@@ -877,7 +875,7 @@ function startDaemonLocked() {
|
|
|
877
875
|
fs.mkdirSync(unitDir, { recursive: true });
|
|
878
876
|
}
|
|
879
877
|
// May embed a long-lived OAuth token in an Environment= line; owner-only.
|
|
880
|
-
writeOwnerOnlyServiceManifest(unitPath, generateSystemdUnit());
|
|
878
|
+
writeOwnerOnlyServiceManifest(unitPath, generateSystemdUnit(undefined, agentsBin));
|
|
881
879
|
execFileSync('systemctl', ['--user', 'daemon-reload'], { encoding: 'utf-8' });
|
|
882
880
|
execFileSync('systemctl', ['--user', 'enable', SYSTEMD_UNIT], { encoding: 'utf-8' });
|
|
883
881
|
execFileSync('systemctl', ['--user', 'start', SYSTEMD_UNIT], { encoding: 'utf-8' });
|
|
@@ -890,9 +888,9 @@ function startDaemonLocked() {
|
|
|
890
888
|
catch {
|
|
891
889
|
// start threw — fall through to detached spawn
|
|
892
890
|
}
|
|
893
|
-
return startDetached();
|
|
891
|
+
return startDetached({ agentsBin });
|
|
894
892
|
}
|
|
895
|
-
return startDetached();
|
|
893
|
+
return startDetached({ agentsBin });
|
|
896
894
|
}
|
|
897
895
|
/**
|
|
898
896
|
* Environment for the detached daemon fallback. The launchd/systemd paths
|
|
@@ -925,18 +923,68 @@ export function buildDetachedDaemonEnv(baseEnv = process.env, oauthToken = readD
|
|
|
925
923
|
* Going through `process.execPath` means a real PE/binary is spawned with
|
|
926
924
|
* `detached: true` and no console, so nothing signals the daemon after launch.
|
|
927
925
|
*
|
|
928
|
-
* When the entry isn't a
|
|
929
|
-
*
|
|
926
|
+
* When the entry isn't a Node script (e.g. a native compiled launcher), run it
|
|
927
|
+
* directly — it owns its own runtime resolution.
|
|
930
928
|
*/
|
|
931
929
|
export function getDaemonLaunch(agentsBin = getAgentsBinPath()) {
|
|
932
930
|
const { warnings } = validateDaemonBinary(agentsBin);
|
|
933
931
|
for (const w of warnings)
|
|
934
932
|
process.stderr.write(`[agents] ${w}\n`);
|
|
935
|
-
if (
|
|
933
|
+
if (isNodeScriptEntry(agentsBin)) {
|
|
936
934
|
return { command: process.execPath, args: [agentsBin, 'daemon', '_run'] };
|
|
937
935
|
}
|
|
938
936
|
return { command: agentsBin, args: ['daemon', '_run'] };
|
|
939
937
|
}
|
|
938
|
+
/**
|
|
939
|
+
* A daemon entry must be launched through the Node runtime when it is a Node
|
|
940
|
+
* script — a `.js`/`.cjs`/`.mjs` file, OR a symlink/extension-less shim whose
|
|
941
|
+
* shebang names `node`. Package installs link `bin/agents` to a `dist/index.js`
|
|
942
|
+
* (a symlink) or drop an extension-less `#!/usr/bin/env node` shim, so an
|
|
943
|
+
* extension check alone misses them and they get run directly. Executing such an
|
|
944
|
+
* entry then relies on the shebang resolving `node` off the daemon's PATH — and
|
|
945
|
+
* when that PATH points at a pruned nvm version (or an ancient system node), the
|
|
946
|
+
* daemon crash-loops at import (`node:util` has no `styleText` on Node 18). A
|
|
947
|
+
* real compiled binary (Mach-O/ELF/PE) has no `#!node` shebang, so it takes the
|
|
948
|
+
* direct branch and owns its own runtime resolution.
|
|
949
|
+
*/
|
|
950
|
+
function isNodeScriptEntry(agentsBin) {
|
|
951
|
+
let resolved = agentsBin;
|
|
952
|
+
try {
|
|
953
|
+
resolved = fs.realpathSync(agentsBin);
|
|
954
|
+
}
|
|
955
|
+
catch {
|
|
956
|
+
// Unresolvable (e.g. a template path that does not exist on this box): fall
|
|
957
|
+
// back to the extension check on the path as given.
|
|
958
|
+
}
|
|
959
|
+
if (/\.(c|m)?js$/.test(resolved))
|
|
960
|
+
return true;
|
|
961
|
+
try {
|
|
962
|
+
const fd = fs.openSync(resolved, 'r');
|
|
963
|
+
try {
|
|
964
|
+
const buf = Buffer.alloc(128);
|
|
965
|
+
const n = fs.readSync(fd, buf, 0, 128, 0);
|
|
966
|
+
const firstLine = buf.toString('utf-8', 0, n).split('\n', 1)[0];
|
|
967
|
+
return firstLine.startsWith('#!') && /\bnode\b/.test(firstLine);
|
|
968
|
+
}
|
|
969
|
+
finally {
|
|
970
|
+
fs.closeSync(fd);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
catch {
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* The directory of the Node runtime that generated this service manifest, kept
|
|
979
|
+
* first on the daemon's PATH. Both the shim's shebang and any child routine
|
|
980
|
+
* process then resolve the exact Node that installed the service — never an
|
|
981
|
+
* ancient system node or a pruned nvm version. Replaces the old hardcoded
|
|
982
|
+
* `~/.nvm/versions/node/v24.0.0/bin`, which went stale the moment that patch
|
|
983
|
+
* release was upgraded away and bricked the daemon fleet-wide.
|
|
984
|
+
*/
|
|
985
|
+
function daemonNodeBinDir() {
|
|
986
|
+
return path.dirname(process.execPath);
|
|
987
|
+
}
|
|
940
988
|
/**
|
|
941
989
|
* Build the argv to relaunch the `agents` CLI with the given subcommand args.
|
|
942
990
|
*
|