@phnx-labs/agents-cli 1.20.33 → 1.20.34
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 +7 -0
- package/README.md +28 -2
- package/dist/commands/computer.d.ts +23 -0
- package/dist/commands/computer.js +45 -3
- package/dist/commands/doctor.d.ts +10 -0
- package/dist/commands/doctor.js +49 -0
- package/dist/commands/import.js +1 -1
- package/dist/commands/rules.js +1 -1
- package/dist/commands/secrets-migrate.js +23 -11
- package/dist/commands/secrets.d.ts +20 -0
- package/dist/commands/secrets.js +53 -1
- package/dist/commands/status.d.ts +12 -0
- package/dist/commands/status.js +81 -0
- package/dist/commands/teams.js +70 -6
- package/dist/commands/versions.js +2 -1
- package/dist/commands/view.d.ts +39 -0
- package/dist/commands/view.js +194 -75
- package/dist/index.js +4 -2
- package/dist/lib/acp/harnesses.d.ts +1 -1
- package/dist/lib/acp/harnesses.js +2 -2
- package/dist/lib/agents.d.ts +12 -0
- package/dist/lib/agents.js +115 -32
- package/dist/lib/browser/chrome.js +20 -0
- package/dist/lib/browser/drivers/ssh.d.ts +19 -0
- package/dist/lib/browser/drivers/ssh.js +18 -3
- package/dist/lib/doctor-diff.js +29 -2
- package/dist/lib/drift-sync.d.ts +43 -0
- package/dist/lib/drift-sync.js +179 -0
- package/dist/lib/exec.d.ts +15 -0
- package/dist/lib/exec.js +21 -11
- package/dist/lib/platform/winpath.d.ts +31 -2
- package/dist/lib/platform/winpath.js +133 -24
- package/dist/lib/pwsh.d.ts +11 -0
- package/dist/lib/pwsh.js +13 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +42 -1
- package/dist/lib/secrets/agent.js +89 -11
- package/dist/lib/secrets/bundles.js +40 -9
- package/dist/lib/secrets/filestore.js +31 -1
- package/dist/lib/secrets/index.d.ts +33 -1
- package/dist/lib/secrets/index.js +90 -9
- package/dist/lib/secrets/windows.d.ts +74 -0
- package/dist/lib/secrets/windows.js +440 -0
- package/dist/lib/shims.d.ts +20 -0
- package/dist/lib/shims.js +53 -20
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/sync-status.d.ts +102 -0
- package/dist/lib/sync-status.js +135 -0
- package/dist/lib/teams/agents.d.ts +24 -0
- package/dist/lib/teams/agents.js +30 -1
- package/dist/lib/types.d.ts +20 -1
- package/dist/lib/usage.d.ts +30 -0
- package/dist/lib/usage.js +159 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -51,7 +51,7 @@ if (IS_DEV_BUILD) {
|
|
|
51
51
|
// module on each invocation (which loaded the whole ~50-module tree before the
|
|
52
52
|
// first byte of output), the registry maps a command name to a thunk that
|
|
53
53
|
// imports only what that command needs. See src/lib/startup/command-registry.ts.
|
|
54
|
-
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadBudget, loadAlias, loadPty, loadTmux, loadBrowser, loadComputer, loadHosts, loadSsh, loadPull, loadPush, loadRepo, loadSetup, } from './lib/startup/command-registry.js';
|
|
54
|
+
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadBudget, loadAlias, loadPty, loadTmux, loadBrowser, loadComputer, loadHosts, loadSsh, loadPull, loadPush, loadRepo, loadSetup, } from './lib/startup/command-registry.js';
|
|
55
55
|
import { applyGlobalHelpConventions } from './lib/help.js';
|
|
56
56
|
import { IS_WINDOWS } from './lib/platform/index.js';
|
|
57
57
|
// Transparent shim delegate: the generated Windows `.cmd` shims invoke
|
|
@@ -764,6 +764,7 @@ async function registerAllEagerCommands() {
|
|
|
764
764
|
await reg(loadTrash);
|
|
765
765
|
await reg(loadRestore);
|
|
766
766
|
await reg(loadDoctor);
|
|
767
|
+
await reg(loadStatus);
|
|
767
768
|
registerExecAliasCommand(program);
|
|
768
769
|
await reg(loadProfiles);
|
|
769
770
|
await reg(loadSecrets);
|
|
@@ -894,7 +895,8 @@ if (!helpOrVersionRequested) {
|
|
|
894
895
|
// fire-and-forget the next background sync. System repo gets a real fast-forward
|
|
895
896
|
// pull (read-only locally, safe). User repo and extras get fetch-only + a
|
|
896
897
|
// status marker that we'll print on the *next* invocation.
|
|
897
|
-
const { spawnDetachedSync } = await import('./lib/auto-pull.js');
|
|
898
|
+
const { spawnDetachedSync, printPendingUpdateNotices } = await import('./lib/auto-pull.js');
|
|
899
|
+
printPendingUpdateNotices();
|
|
898
900
|
spawnDetachedSync();
|
|
899
901
|
}
|
|
900
902
|
// First-run experience: no args + no config yet + TTY -> launch interactive setup.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Per-harness spawn configuration for Agent Client Protocol (ACP) mode.
|
|
3
3
|
*
|
|
4
4
|
* Each entry describes how to launch a coding agent CLI as an ACP server
|
|
5
|
-
* (stdio JSON-RPC). Unsupported harnesses (amp,
|
|
5
|
+
* (stdio JSON-RPC). Unsupported harnesses (amp, goose-stdio) are omitted;
|
|
6
6
|
* callers should fall back to the legacy direct-exec path for those.
|
|
7
7
|
*
|
|
8
8
|
* Sources: https://agentclientprotocol.com/get-started/agents + vendor docs.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Per-harness spawn configuration for Agent Client Protocol (ACP) mode.
|
|
3
3
|
*
|
|
4
4
|
* Each entry describes how to launch a coding agent CLI as an ACP server
|
|
5
|
-
* (stdio JSON-RPC). Unsupported harnesses (amp,
|
|
5
|
+
* (stdio JSON-RPC). Unsupported harnesses (amp, goose-stdio) are omitted;
|
|
6
6
|
* callers should fall back to the legacy direct-exec path for those.
|
|
7
7
|
*
|
|
8
8
|
* Sources: https://agentclientprotocol.com/get-started/agents + vendor docs.
|
|
@@ -61,7 +61,7 @@ export const ACP_HARNESSES = {
|
|
|
61
61
|
// goose: ACP over HTTP via `goosed`, not a clean stdio subcommand.
|
|
62
62
|
// copilot, kiro: excluded for now (not installed in the reference environment,
|
|
63
63
|
// no local verification possible).
|
|
64
|
-
// amp
|
|
64
|
+
// amp: not on the ACP agents list.
|
|
65
65
|
};
|
|
66
66
|
/** Returns the ACP spawn spec for an agent, or undefined if the harness does not speak ACP. */
|
|
67
67
|
export function getAcpSpec(agent) {
|
package/dist/lib/agents.d.ts
CHANGED
|
@@ -219,6 +219,18 @@ export declare const AGENT_NAME_ALIASES: Record<string, AgentId>;
|
|
|
219
219
|
export declare function resolveAgentName(input: string): AgentId | null;
|
|
220
220
|
/** Check whether the input string matches any known agent name or alias. */
|
|
221
221
|
export declare function isAgentName(input: string): boolean;
|
|
222
|
+
/**
|
|
223
|
+
* Build the deprecation notice lines for an agent, or null if it isn't
|
|
224
|
+
* deprecated. Split from the printer so tests can assert the content without
|
|
225
|
+
* capturing stdout. Lines are plain (uncolored) text.
|
|
226
|
+
*/
|
|
227
|
+
export declare function deprecationNotice(agent: AgentId): string[] | null;
|
|
228
|
+
/**
|
|
229
|
+
* Print a deprecation warning (yellow) if the agent's registry entry carries a
|
|
230
|
+
* `deprecated` marker; no-op otherwise. Call from any user entry point that
|
|
231
|
+
* acts on a chosen agent — install (`agents add`) and `agents teams add`.
|
|
232
|
+
*/
|
|
233
|
+
export declare function warnAgentDeprecated(agent: AgentId): void;
|
|
222
234
|
/** Format an error message for an unrecognized agent name, listing valid options. */
|
|
223
235
|
export declare function formatAgentError(agentName: string, validAgents?: AgentId[]): string;
|
|
224
236
|
export {};
|
package/dist/lib/agents.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Core agent configuration and detection module.
|
|
3
3
|
*
|
|
4
4
|
* Defines the canonical registry of all supported AI coding agents (Claude, Codex,
|
|
5
|
-
* Gemini, Cursor, OpenCode, OpenClaw, Copilot, Amp, Kiro, Goose,
|
|
5
|
+
* Gemini, Cursor, OpenCode, OpenClaw, Copilot, Amp, Kiro, Goose, Grok) with their
|
|
6
6
|
* CLI commands, config paths, capability flags, and MCP integration points.
|
|
7
7
|
*
|
|
8
8
|
* Provides functions for detecting installed CLIs, resolving version-managed binaries,
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { execFile } from 'child_process';
|
|
12
12
|
import { promisify } from 'util';
|
|
13
|
+
import * as crypto from 'crypto';
|
|
13
14
|
import * as fs from 'fs';
|
|
14
15
|
import * as path from 'path';
|
|
15
16
|
import * as os from 'os';
|
|
@@ -249,6 +250,16 @@ export const AGENTS = {
|
|
|
249
250
|
variableSyntax: '{{args}}',
|
|
250
251
|
supportsHooks: true,
|
|
251
252
|
nativeAgentsSkillsDir: true,
|
|
253
|
+
// Google retired the Gemini CLI (announced at Google I/O 2026, May 19); the `gemini`
|
|
254
|
+
// command stopped serving free/Pro/Ultra requests on June 18, 2026. Antigravity CLI
|
|
255
|
+
// (`agy`) is the official successor. See warnAgentDeprecated() for the surfaced warning.
|
|
256
|
+
deprecated: {
|
|
257
|
+
by: 'Google',
|
|
258
|
+
date: 'June 18, 2026',
|
|
259
|
+
reason: 'The Gemini CLI was retired for free, Pro, and Ultra tiers and no longer serves requests (announced at Google I/O 2026 on May 19).',
|
|
260
|
+
replacement: 'antigravity',
|
|
261
|
+
url: 'https://developers.googleblog.com/an-important-update-transitioning-gemini-cli-to-antigravity-cli/',
|
|
262
|
+
},
|
|
252
263
|
// gemini hooks: shipped in v0.26.0 (Jan 2026); older binaries silently ignore the `hooks` key.
|
|
253
264
|
capabilities: { hooks: { since: '0.26.0' }, mcp: true, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: 'GEMINI.md' }, workflows: false, modes: ['plan', 'edit', 'skip'], rulesImports: true },
|
|
254
265
|
},
|
|
@@ -375,24 +386,6 @@ export const AGENTS = {
|
|
|
375
386
|
supportsHooks: false,
|
|
376
387
|
capabilities: { hooks: false, mcp: true, allowlist: false, skills: false, commands: false, plugins: false, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['edit'] },
|
|
377
388
|
},
|
|
378
|
-
roo: {
|
|
379
|
-
id: 'roo',
|
|
380
|
-
name: 'Roo Code',
|
|
381
|
-
color: 'cyanBright',
|
|
382
|
-
cliCommand: 'roo',
|
|
383
|
-
npmPackage: '',
|
|
384
|
-
installScript: 'curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh',
|
|
385
|
-
configDir: path.join(HOME, '.roo'),
|
|
386
|
-
commandsDir: path.join(HOME, '.roo', 'commands'),
|
|
387
|
-
commandsSubdir: 'commands',
|
|
388
|
-
skillsDir: path.join(HOME, '.roo', 'skills'),
|
|
389
|
-
hooksDir: 'hooks',
|
|
390
|
-
instructionsFile: 'AGENTS.md',
|
|
391
|
-
format: 'markdown',
|
|
392
|
-
variableSyntax: '$ARGUMENTS',
|
|
393
|
-
supportsHooks: false,
|
|
394
|
-
capabilities: { hooks: false, mcp: true, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['plan', 'edit'] },
|
|
395
|
-
},
|
|
396
389
|
// Google Antigravity CLI (`agy`) — official replacement for Gemini CLI as of IO 2026.
|
|
397
390
|
// configDir nests inside `~/.gemini/` since agy shares the parent dir with the Gemini
|
|
398
391
|
// CLI but isolates its own state in the `antigravity-cli/` subdir. Per-version HOME
|
|
@@ -802,6 +795,52 @@ function resolveAccountCredentialPath(base, ...segments) {
|
|
|
802
795
|
}
|
|
803
796
|
return null;
|
|
804
797
|
}
|
|
798
|
+
/**
|
|
799
|
+
* Factory Droid stores its OAuth credential encrypted at ~/.factory/auth.v2.file
|
|
800
|
+
* (AES-256-GCM, format `ivB64:tagB64:ctB64`) with the 32-byte key base64-stored
|
|
801
|
+
* in ~/.factory/auth.v2.key. On the keyfile-v2 source there is no OS-keychain /
|
|
802
|
+
* device binding — the key is on disk — so we can decrypt locally with no
|
|
803
|
+
* network call. The decrypted JSON credential's `access_token` is a WorkOS JWT
|
|
804
|
+
* carrying an `email` claim (plus org_id / role). We decode the claim WITHOUT
|
|
805
|
+
* verifying `exp`: the email is stable identity for display, not an
|
|
806
|
+
* authorization decision, so an expired token still yields the right address.
|
|
807
|
+
* Every failure (missing key file — e.g. a keyring-v2/legacy login with no
|
|
808
|
+
* on-disk key, a bad GCM tag, malformed JSON, or no email claim) returns null so
|
|
809
|
+
* the caller falls back to the file-presence signed-in signal. Never throws.
|
|
810
|
+
*/
|
|
811
|
+
function decryptDroidCredential(base) {
|
|
812
|
+
const filePath = resolveAccountCredentialPath(base, '.factory', 'auth.v2.file');
|
|
813
|
+
const keyPath = resolveAccountCredentialPath(base, '.factory', 'auth.v2.key');
|
|
814
|
+
if (!filePath || !keyPath)
|
|
815
|
+
return null;
|
|
816
|
+
try {
|
|
817
|
+
const blob = fs.readFileSync(filePath, 'utf-8').trim();
|
|
818
|
+
const key = Buffer.from(fs.readFileSync(keyPath, 'utf-8').trim(), 'base64');
|
|
819
|
+
if (key.length !== 32)
|
|
820
|
+
return null;
|
|
821
|
+
const [ivB64, tagB64, ctB64] = blob.split(':');
|
|
822
|
+
if (!ivB64 || !tagB64 || !ctB64)
|
|
823
|
+
return null;
|
|
824
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(ivB64, 'base64'));
|
|
825
|
+
decipher.setAuthTag(Buffer.from(tagB64, 'base64'));
|
|
826
|
+
const plaintext = Buffer.concat([
|
|
827
|
+
decipher.update(Buffer.from(ctB64, 'base64')),
|
|
828
|
+
decipher.final(),
|
|
829
|
+
]).toString('utf-8');
|
|
830
|
+
const cred = JSON.parse(plaintext);
|
|
831
|
+
const claims = typeof cred?.access_token === 'string' ? decodeJwtPayload(cred.access_token) : null;
|
|
832
|
+
if (!claims)
|
|
833
|
+
return null;
|
|
834
|
+
return {
|
|
835
|
+
email: typeof claims.email === 'string' ? claims.email : null,
|
|
836
|
+
orgId: normalizeIdentityPart(claims.org_id ?? cred.active_organization_id),
|
|
837
|
+
role: typeof claims.role === 'string' ? claims.role : null,
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
catch {
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
805
844
|
let cachedAgyKeychainSignedIn;
|
|
806
845
|
/**
|
|
807
846
|
* Antigravity (`agy`, a Codeium/Windsurf-based CLI) stores its OAuth token in
|
|
@@ -1034,12 +1073,29 @@ export async function getAccountInfo(agentId, home) {
|
|
|
1034
1073
|
return { ...empty, signedIn: true, accountId: userId, accountKey, lastActive };
|
|
1035
1074
|
}
|
|
1036
1075
|
case 'droid': {
|
|
1037
|
-
// Factory Droid stores auth at ~/.factory/auth.v2.file (
|
|
1038
|
-
//
|
|
1039
|
-
//
|
|
1040
|
-
//
|
|
1041
|
-
//
|
|
1042
|
-
//
|
|
1076
|
+
// Factory Droid stores auth at ~/.factory/auth.v2.file (AES-256-GCM,
|
|
1077
|
+
// decrypted with the on-disk ~/.factory/auth.v2.key). We decrypt locally
|
|
1078
|
+
// — no network — and surface the email/org/role from the WorkOS
|
|
1079
|
+
// access-token JWT, same as claude/codex/grok. If the credential can't be
|
|
1080
|
+
// decrypted (a keyring-v2/legacy login with no on-disk key, or a decrypt
|
|
1081
|
+
// failure) we fall back to the file-presence signed-in signal so the row
|
|
1082
|
+
// still reads as logged in — the conservative floor antigravity/kimi use.
|
|
1083
|
+
// `.factory` is the config dir on every platform (macOS/Linux
|
|
1084
|
+
// ~/.factory, Windows %USERPROFILE%\.factory).
|
|
1085
|
+
const decoded = decryptDroidCredential(base);
|
|
1086
|
+
if (decoded?.email) {
|
|
1087
|
+
const organizationId = decoded.orgId;
|
|
1088
|
+
const accountKey = buildIdentityKey(agentId, [['org', organizationId]]);
|
|
1089
|
+
return {
|
|
1090
|
+
...empty,
|
|
1091
|
+
email: decoded.email,
|
|
1092
|
+
organizationId,
|
|
1093
|
+
accountId: organizationId,
|
|
1094
|
+
accountKey,
|
|
1095
|
+
signedIn: true,
|
|
1096
|
+
lastActive,
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1043
1099
|
const authPath = resolveAccountCredentialPath(base, '.factory', 'auth.v2.file');
|
|
1044
1100
|
if (!authPath)
|
|
1045
1101
|
return { ...empty, lastActive };
|
|
@@ -1574,8 +1630,6 @@ export function getMcpConfigPathForHome(agentId, home) {
|
|
|
1574
1630
|
return path.join(home, '.kiro', 'settings', 'mcp.json');
|
|
1575
1631
|
case 'goose':
|
|
1576
1632
|
return path.join(home, '.config', 'goose', 'config.yaml');
|
|
1577
|
-
case 'roo':
|
|
1578
|
-
return path.join(home, '.roo', 'mcp.json');
|
|
1579
1633
|
case 'antigravity':
|
|
1580
1634
|
return path.join(home, '.gemini', 'antigravity-cli', 'mcp_config.json');
|
|
1581
1635
|
case 'grok':
|
|
@@ -1612,8 +1666,6 @@ function getProjectMcpConfigPath(agentId, cwd = process.cwd()) {
|
|
|
1612
1666
|
return path.join(cwd, '.kiro', 'settings', 'mcp.json');
|
|
1613
1667
|
case 'goose':
|
|
1614
1668
|
return path.join(cwd, '.goose', 'config.yaml');
|
|
1615
|
-
case 'roo':
|
|
1616
|
-
return path.join(cwd, '.roo', 'mcp.json');
|
|
1617
1669
|
case 'antigravity':
|
|
1618
1670
|
return path.join(cwd, '.gemini', 'antigravity-cli', 'mcp_config.json');
|
|
1619
1671
|
case 'grok':
|
|
@@ -1746,9 +1798,6 @@ export const AGENT_NAME_ALIASES = {
|
|
|
1746
1798
|
'kiro-cli': 'kiro',
|
|
1747
1799
|
goose: 'goose',
|
|
1748
1800
|
'block-goose': 'goose',
|
|
1749
|
-
roo: 'roo',
|
|
1750
|
-
'roo-code': 'roo',
|
|
1751
|
-
roocode: 'roo',
|
|
1752
1801
|
antigravity: 'antigravity',
|
|
1753
1802
|
'google-antigravity': 'antigravity',
|
|
1754
1803
|
agy: 'antigravity',
|
|
@@ -1789,6 +1838,40 @@ export function resolveAgentName(input) {
|
|
|
1789
1838
|
export function isAgentName(input) {
|
|
1790
1839
|
return resolveAgentName(input) !== null;
|
|
1791
1840
|
}
|
|
1841
|
+
/**
|
|
1842
|
+
* Build the deprecation notice lines for an agent, or null if it isn't
|
|
1843
|
+
* deprecated. Split from the printer so tests can assert the content without
|
|
1844
|
+
* capturing stdout. Lines are plain (uncolored) text.
|
|
1845
|
+
*/
|
|
1846
|
+
export function deprecationNotice(agent) {
|
|
1847
|
+
const dep = AGENTS[agent].deprecated;
|
|
1848
|
+
if (!dep)
|
|
1849
|
+
return null;
|
|
1850
|
+
const name = AGENTS[agent].name;
|
|
1851
|
+
const lines = [
|
|
1852
|
+
`Warning: ${name} was deprecated by ${dep.by} (${dep.date}).`,
|
|
1853
|
+
` ${dep.reason}`,
|
|
1854
|
+
];
|
|
1855
|
+
if (dep.replacement) {
|
|
1856
|
+
const rep = AGENTS[dep.replacement];
|
|
1857
|
+
lines.push(` Consider using ${rep.name} instead: agents add ${rep.id}`);
|
|
1858
|
+
}
|
|
1859
|
+
if (dep.url)
|
|
1860
|
+
lines.push(` ${dep.url}`);
|
|
1861
|
+
return lines;
|
|
1862
|
+
}
|
|
1863
|
+
/**
|
|
1864
|
+
* Print a deprecation warning (yellow) if the agent's registry entry carries a
|
|
1865
|
+
* `deprecated` marker; no-op otherwise. Call from any user entry point that
|
|
1866
|
+
* acts on a chosen agent — install (`agents add`) and `agents teams add`.
|
|
1867
|
+
*/
|
|
1868
|
+
export function warnAgentDeprecated(agent) {
|
|
1869
|
+
const lines = deprecationNotice(agent);
|
|
1870
|
+
if (!lines)
|
|
1871
|
+
return;
|
|
1872
|
+
for (const line of lines)
|
|
1873
|
+
console.log(chalk.yellow(line));
|
|
1874
|
+
}
|
|
1792
1875
|
/** Format an error message for an unrecognized agent name, listing valid options. */
|
|
1793
1876
|
export function formatAgentError(agentName, validAgents = ALL_AGENT_IDS) {
|
|
1794
1877
|
return `Unknown agent '${agentName}'. Valid agents: ${validAgents.join(', ')}`;
|
|
@@ -51,6 +51,7 @@ const BROWSER_PATHS = {
|
|
|
51
51
|
edge: [
|
|
52
52
|
`${WIN_PROGRAMFILES}\\Microsoft\\Edge\\Application\\msedge.exe`,
|
|
53
53
|
`${WIN_PROGRAMFILES_X86}\\Microsoft\\Edge\\Application\\msedge.exe`,
|
|
54
|
+
`${WIN_LOCALAPPDATA}\\Microsoft\\Edge\\Application\\msedge.exe`,
|
|
54
55
|
],
|
|
55
56
|
custom: [],
|
|
56
57
|
},
|
|
@@ -282,6 +283,25 @@ export async function attachToChrome(port) {
|
|
|
282
283
|
return wsUrl;
|
|
283
284
|
}
|
|
284
285
|
export function killChrome(pid) {
|
|
286
|
+
if (process.platform === 'win32') {
|
|
287
|
+
// On Windows `process.kill(pid, 'SIGINT')` maps to TerminateProcess — a
|
|
288
|
+
// hard kill that skips Chromium's on-exit profile flush (leaving a dirty
|
|
289
|
+
// "Chrome didn't shut down correctly" state). Ask taskkill for a graceful
|
|
290
|
+
// stop first (no /F → posts WM_CLOSE), and only force-kill the tree if that
|
|
291
|
+
// fails or the process is still around.
|
|
292
|
+
try {
|
|
293
|
+
execFileSync('taskkill', ['/pid', String(pid)], { stdio: 'ignore' });
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
try {
|
|
297
|
+
execFileSync('taskkill', ['/pid', String(pid), '/t', '/f'], { stdio: 'ignore' });
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
// Process already dead
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
285
305
|
try {
|
|
286
306
|
process.kill(pid, 'SIGINT');
|
|
287
307
|
}
|
|
@@ -54,3 +54,22 @@ export declare function buildLaunchCmd(remoteOs: RemoteOs, browserType: string,
|
|
|
54
54
|
*/
|
|
55
55
|
export declare function buildKillCmd(remoteOs: RemoteOs, port: number): string;
|
|
56
56
|
export declare function restartRemoteBrowser(user: string, host: string, browserType: string, port: number, remoteOs: RemoteOs, customBinary?: string): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Identify whether a pid listening on our target local port is an SSH
|
|
59
|
+
* tunnel WE would have spawned for `host:remotePort`. Used so that two
|
|
60
|
+
* agents-browser invocations of the same SSH profile share a tunnel
|
|
61
|
+
* rather than failing the second one with "port in use".
|
|
62
|
+
*
|
|
63
|
+
* Best-effort match against the ssh -L command line via `ps`. If we
|
|
64
|
+
* can't read the cmd or the args don't look like ours, treat as not-ours.
|
|
65
|
+
*
|
|
66
|
+
* Windows has no `ps`, so the POSIX branch below always throws there and every
|
|
67
|
+
* reuse check returned false — the second invocation to the same host:port then
|
|
68
|
+
* failed "port in use". On win32 we instead reuse the netstat -ano + tasklist
|
|
69
|
+
* occupant lookup (getPortOccupant): the tunnel binds `remotePort` locally
|
|
70
|
+
* (localPort === remotePort in the caller), so the pid holding that port is our
|
|
71
|
+
* ssh.exe. tasklist only exposes the image name, not the full command line, so
|
|
72
|
+
* we can't match host/remotePort as tightly as the POSIX branch — an ssh
|
|
73
|
+
* process on our exact local port is our tunnel in practice.
|
|
74
|
+
*/
|
|
75
|
+
export declare function isOwnTunnel(pid: number, host: string, remotePort: number): boolean;
|
|
@@ -12,6 +12,7 @@ export { shellQuote };
|
|
|
12
12
|
// the single ssh-tunnel helper. Calling it with no options preserves this
|
|
13
13
|
// driver's original foreground, stderr-captured behavior exactly.
|
|
14
14
|
import { startSSHTunnel } from '../../ssh-tunnel.js';
|
|
15
|
+
import { encodePwshBase64 } from '../../pwsh.js';
|
|
15
16
|
export async function connectSSH(endpoint, profile) {
|
|
16
17
|
const url = new URL(endpoint);
|
|
17
18
|
if (url.protocol !== 'ssh:') {
|
|
@@ -159,8 +160,7 @@ function psSingleQuote(s) {
|
|
|
159
160
|
* `powershell -Command "…"` is fragile the moment a path or URL is involved).
|
|
160
161
|
*/
|
|
161
162
|
export function encodePowerShell(script) {
|
|
162
|
-
|
|
163
|
-
return `powershell -NoProfile -EncodedCommand ${b64}`;
|
|
163
|
+
return `powershell -NoProfile -EncodedCommand ${encodePwshBase64(script)}`;
|
|
164
164
|
}
|
|
165
165
|
/**
|
|
166
166
|
* The PowerShell that launches the browser on a Windows remote. Two hard
|
|
@@ -290,8 +290,23 @@ function sleep(ms) {
|
|
|
290
290
|
*
|
|
291
291
|
* Best-effort match against the ssh -L command line via `ps`. If we
|
|
292
292
|
* can't read the cmd or the args don't look like ours, treat as not-ours.
|
|
293
|
+
*
|
|
294
|
+
* Windows has no `ps`, so the POSIX branch below always throws there and every
|
|
295
|
+
* reuse check returned false — the second invocation to the same host:port then
|
|
296
|
+
* failed "port in use". On win32 we instead reuse the netstat -ano + tasklist
|
|
297
|
+
* occupant lookup (getPortOccupant): the tunnel binds `remotePort` locally
|
|
298
|
+
* (localPort === remotePort in the caller), so the pid holding that port is our
|
|
299
|
+
* ssh.exe. tasklist only exposes the image name, not the full command line, so
|
|
300
|
+
* we can't match host/remotePort as tightly as the POSIX branch — an ssh
|
|
301
|
+
* process on our exact local port is our tunnel in practice.
|
|
293
302
|
*/
|
|
294
|
-
function isOwnTunnel(pid, host, remotePort) {
|
|
303
|
+
export function isOwnTunnel(pid, host, remotePort) {
|
|
304
|
+
if (process.platform === 'win32') {
|
|
305
|
+
const occupant = getPortOccupant(remotePort);
|
|
306
|
+
if (!occupant || occupant.pid !== pid)
|
|
307
|
+
return false;
|
|
308
|
+
return occupant.command.toLowerCase().startsWith('ssh');
|
|
309
|
+
}
|
|
295
310
|
try {
|
|
296
311
|
const out = execFileSync('ps', ['-p', String(pid), '-o', 'command='], {
|
|
297
312
|
encoding: 'utf-8',
|
package/dist/lib/doctor-diff.js
CHANGED
|
@@ -29,6 +29,8 @@ import { pluginInstallDir, repairableManifestFields } from './plugin-marketplace
|
|
|
29
29
|
import { markdownToToml } from './convert.js';
|
|
30
30
|
import { resolveImports, supportsRulesImports } from './rules/compile.js';
|
|
31
31
|
import { listCommandsInVersionHome, getVersionCommandsDir } from './commands.js';
|
|
32
|
+
import { shouldInstallCommandAsSkill, commandSkillMatches, commandSkillName } from './command-skills.js';
|
|
33
|
+
import { supports } from './capabilities.js';
|
|
32
34
|
import { listSkillsInVersionHome, getVersionSkillsDir } from './skills.js';
|
|
33
35
|
import { listHooksInVersionHome, listHookEntriesFromDir } from './hooks.js';
|
|
34
36
|
const RULES_DOC_FILENAME = 'README.md';
|
|
@@ -87,6 +89,15 @@ function diffCommands(agent, version, cwd, excludeProject = false) {
|
|
|
87
89
|
const isToml = agentConfig.format === 'toml';
|
|
88
90
|
const ext = isToml ? '.toml' : '.md';
|
|
89
91
|
const homeDir = getVersionCommandsDir(agent, version);
|
|
92
|
+
// Command-as-skill agents (kimi, codex>=0.117, grok) install every command as a
|
|
93
|
+
// SKILL wrapper at <agentDir>/skills/<cmd>/SKILL.md, not a native command file.
|
|
94
|
+
// The compare must follow that or every command false-reports as drifted.
|
|
95
|
+
const asSkill = shouldInstallCommandAsSkill(agent, version);
|
|
96
|
+
// Agents that hold commands neither natively nor as skills (e.g. goose) must not
|
|
97
|
+
// report source commands as "missing" — they structurally can't take them.
|
|
98
|
+
if (!asSkill && !supports(agent, 'commands', version).ok)
|
|
99
|
+
return [];
|
|
100
|
+
const agentDir = path.join(getVersionHomePath(agent, version), agentConfigDirName(agent));
|
|
90
101
|
const installed = new Set(listCommandsInVersionHome(agent, version));
|
|
91
102
|
const layerBases = buildLayerBases(cwd, 'commands', { excludeProject });
|
|
92
103
|
const sourceByName = new Map();
|
|
@@ -113,11 +124,24 @@ function diffCommands(agent, version, cwd, excludeProject = false) {
|
|
|
113
124
|
const seen = new Set();
|
|
114
125
|
for (const [name, src] of sourceByName) {
|
|
115
126
|
seen.add(name);
|
|
116
|
-
const homePath = path.join(homeDir, `${name}${ext}`);
|
|
117
127
|
if (!installed.has(name)) {
|
|
118
128
|
rows.push({ kind: 'commands', name, status: 'missing', source: src.layer, sourcePath: src.path });
|
|
119
129
|
continue;
|
|
120
130
|
}
|
|
131
|
+
if (asSkill) {
|
|
132
|
+
// Compare against the installed command-skill wrapper, not a native path.
|
|
133
|
+
const matches = commandSkillMatches(agentDir, name, src.path);
|
|
134
|
+
rows.push({
|
|
135
|
+
kind: 'commands',
|
|
136
|
+
name,
|
|
137
|
+
status: matches ? 'ok' : 'diff',
|
|
138
|
+
source: src.layer,
|
|
139
|
+
sourcePath: src.path,
|
|
140
|
+
homePath: path.join(agentDir, 'skills', commandSkillName(name), 'SKILL.md'),
|
|
141
|
+
});
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const homePath = path.join(homeDir, `${name}${ext}`);
|
|
121
145
|
const installedContent = readSafe(homePath);
|
|
122
146
|
const sourceContent = readSafe(src.path);
|
|
123
147
|
if (installedContent == null || sourceContent == null) {
|
|
@@ -138,7 +162,10 @@ function diffCommands(agent, version, cwd, excludeProject = false) {
|
|
|
138
162
|
for (const name of installed) {
|
|
139
163
|
if (seen.has(name))
|
|
140
164
|
continue;
|
|
141
|
-
|
|
165
|
+
const extraHome = asSkill
|
|
166
|
+
? path.join(agentDir, 'skills', commandSkillName(name), 'SKILL.md')
|
|
167
|
+
: path.join(homeDir, `${name}${ext}`);
|
|
168
|
+
rows.push({ kind: 'commands', name, status: 'extra', homePath: extraHome });
|
|
142
169
|
}
|
|
143
170
|
return rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
144
171
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive drift-sync flow — the single "we detected drift, want to fix it?"
|
|
3
|
+
* action, shared by `agents status`, `agents doctor`, and the menu-bar "NEEDS
|
|
4
|
+
* SYNC" row.
|
|
5
|
+
*
|
|
6
|
+
* It composes existing pieces, re-implementing nothing:
|
|
7
|
+
* - computeSyncStatus() — the unified detection engine (sync-status.ts)
|
|
8
|
+
* - pullRepo() — fast-forward the `.system` repo (git.ts)
|
|
9
|
+
* - promptAgentVersionSelection() — the "which agent types / versions?" picker
|
|
10
|
+
* - heal({ mode: 'full' }) — the reconcile engine `doctor --fix` uses
|
|
11
|
+
*
|
|
12
|
+
* Combined flow (one confirmation): if `.system` is behind AND resources drifted,
|
|
13
|
+
* a single "Sync all detected" both pulls `.system` and reconciles the chosen
|
|
14
|
+
* version homes. The security posture is preserved — the `.system` pull only ever
|
|
15
|
+
* happens on an explicit user choice here, never silently (see auto-pull-worker.ts
|
|
16
|
+
* for why system auto-pull is off by default).
|
|
17
|
+
*/
|
|
18
|
+
import { type VersionHealResult } from './heal.js';
|
|
19
|
+
import { type UnifiedSyncStatus } from './sync-status.js';
|
|
20
|
+
export interface DriftSyncOptions {
|
|
21
|
+
cwd?: string;
|
|
22
|
+
/** Reconcile everything detected with no prompts — the "kick it" path, also the
|
|
23
|
+
* non-TTY / menu-bar-launched-with-flag behavior. Pulls `.system` if behind. */
|
|
24
|
+
yes?: boolean;
|
|
25
|
+
/** Pre-computed status, to avoid a second scan when the caller already has one. */
|
|
26
|
+
status?: UnifiedSyncStatus;
|
|
27
|
+
/** Skip the drift summary — set by callers that already printed their own. */
|
|
28
|
+
quiet?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface DriftSyncResult {
|
|
31
|
+
systemBehindBefore: number;
|
|
32
|
+
systemPulled: boolean;
|
|
33
|
+
healed: VersionHealResult[];
|
|
34
|
+
/** True when the user declined at the prompt (nothing was changed). */
|
|
35
|
+
cancelled: boolean;
|
|
36
|
+
/** True when there was no drift to act on in the first place. */
|
|
37
|
+
nothingToDo: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The unified "drift detected — sync now?" flow. Returns a structured result so
|
|
41
|
+
* callers (menu-bar, doctor) can report without re-scanning.
|
|
42
|
+
*/
|
|
43
|
+
export declare function promptDriftSync(opts?: DriftSyncOptions): Promise<DriftSyncResult>;
|