@phnx-labs/agents-cli 1.20.29 → 1.20.30
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/dist/commands/computer-actions.js +6 -2
- package/dist/commands/computer.d.ts +12 -0
- package/dist/commands/computer.js +88 -13
- package/dist/commands/inspect.js +1 -1
- package/dist/commands/models.js +8 -2
- package/dist/commands/sessions.js +156 -44
- package/dist/commands/sync.js +70 -14
- package/dist/lib/agents.d.ts +0 -4
- package/dist/lib/agents.js +54 -5
- package/dist/lib/browser/drivers/ssh.js +4 -35
- package/dist/lib/computer-rpc.d.ts +6 -1
- package/dist/lib/computer-rpc.js +86 -3
- package/dist/lib/exec.js +14 -0
- package/dist/lib/models.js +138 -5
- package/dist/lib/runner.js +7 -7
- package/dist/lib/session/active.d.ts +13 -0
- package/dist/lib/session/active.js +79 -18
- package/dist/lib/session/cloud.js +2 -0
- package/dist/lib/session/db.d.ts +11 -0
- package/dist/lib/session/db.js +62 -5
- package/dist/lib/session/discover.d.ts +5 -0
- package/dist/lib/session/discover.js +81 -0
- package/dist/lib/session/parse.d.ts +15 -0
- package/dist/lib/session/parse.js +22 -2
- package/dist/lib/session/remote.d.ts +1 -1
- package/dist/lib/session/remote.js +8 -3
- package/dist/lib/session/state.d.ts +82 -0
- package/dist/lib/session/state.js +221 -0
- package/dist/lib/session/tail.d.ts +18 -0
- package/dist/lib/session/tail.js +57 -0
- package/dist/lib/session/types.d.ts +9 -0
- package/dist/lib/session/width.d.ts +29 -0
- package/dist/lib/session/width.js +91 -0
- package/dist/lib/shims.d.ts +17 -1
- package/dist/lib/shims.js +130 -6
- package/dist/lib/ssh-tunnel.d.ts +127 -0
- package/dist/lib/ssh-tunnel.js +346 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +17 -1
- package/dist/lib/teams/agents.d.ts +11 -1
- package/dist/lib/teams/agents.js +16 -2
- package/dist/lib/types.d.ts +1 -0
- package/dist/lib/versions.d.ts +19 -0
- package/dist/lib/versions.js +84 -24
- package/package.json +1 -1
package/dist/commands/sync.js
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
import * as path from 'path';
|
|
31
31
|
import chalk from 'chalk';
|
|
32
32
|
import { agentLabel, resolveAgentName } from '../lib/agents.js';
|
|
33
|
-
import { isVersionInstalled, syncResourcesToVersion, parseAgentSpec, resolveVersion, resolveVersionAlias, listInstalledVersions, getAvailableResources, getActuallySyncedResources, getProjectOnlyResources, getNewResources, hasNewResources, promptResourceSelection, promptNewResourceSelection, } from '../lib/versions.js';
|
|
33
|
+
import { isVersionInstalled, syncResourcesToVersion, parseAgentSpec, resolveVersion, resolveVersionAlias, listInstalledVersions, getAvailableResources, getActuallySyncedResources, getProjectOnlyResources, getNewResources, hasNewResources, promptResourceSelection, promptNewResourceSelection, buildRepoScopedSelection, listRepoNames, } from '../lib/versions.js';
|
|
34
34
|
import { compileRulesForProject } from '../lib/rules/compile.js';
|
|
35
35
|
import { runLaunchSync } from '../lib/project-launch.js';
|
|
36
36
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
@@ -38,11 +38,12 @@ import { runUmbrellaSync } from '../lib/sync-umbrella.js';
|
|
|
38
38
|
/** Register the `agents sync` command. */
|
|
39
39
|
export function registerSyncCommand(program) {
|
|
40
40
|
program
|
|
41
|
-
.command('sync [agentSpec]')
|
|
41
|
+
.command('sync [agentSpec] [repo]')
|
|
42
42
|
.summary('Make this machine current, or sync resources into one agent')
|
|
43
|
-
.description('With an [agentSpec], syncs resources (commands, skills, hooks, rules, MCPs, plugins, etc.) into that installed agent version — previews changes and lets you pick. e.g. "claude", "claude@2.1.142",
|
|
43
|
+
.description('With an [agentSpec], syncs resources (commands, skills, hooks, rules, MCPs, plugins, etc.) into that installed agent version — previews changes and lets you pick. e.g. "claude", "claude@2.1.142", a selector: @latest / @oldest / @pinned (= @default), or @all for every installed version.\n\nAppend a [repo] (or pass --repo) to scope the sync to a single DotAgent repo — system / user / project / <alias>. e.g. "agents sync claude@all system" reconciles only the system repo\'s resources into every installed Claude.\n\nWith NO agent, runs the umbrella verb: fetch remote state (config repos + secrets + sessions) then reconcile it into every installed agent. Scope it with --repos / --secrets / --sessions, --cloud (fetch only), or --local (reconcile only).')
|
|
44
44
|
.option('--agent <agent>', 'Agent identifier (legacy form; prefer the positional spec)')
|
|
45
45
|
.option('--agent-version <version>', 'Version to sync into (legacy form; prefer "agent@version")')
|
|
46
|
+
.option('--repo <name>', 'Scope the sync to a single DotAgent repo: system / user / project / <alias> (also accepted as a positional)')
|
|
46
47
|
.option('--project-dir <path>', 'Path to project-level .agents/ directory containing project-scoped resources')
|
|
47
48
|
.option('--cwd <path>', 'Working directory for discovering project manifest and resources')
|
|
48
49
|
.option('--launch', 'Hot-path mode (shim only): skip version-home reconciliation, run project-scoped compile + workspace mirror + plugin marketplaces', false)
|
|
@@ -55,8 +56,8 @@ export function registerSyncCommand(program) {
|
|
|
55
56
|
.option('--sessions', 'Umbrella: sync session transcripts across machines', false)
|
|
56
57
|
.option('--cloud', 'Umbrella: fetch all remote state but skip the local reconcile', false)
|
|
57
58
|
.option('--local', "Umbrella: reconcile resources into installed agents only (no fetch)", false)
|
|
58
|
-
.action(async (agentSpec, opts) => {
|
|
59
|
-
await runSync(agentSpec, opts);
|
|
59
|
+
.action(async (agentSpec, repo, opts) => {
|
|
60
|
+
await runSync(agentSpec, repo, opts);
|
|
60
61
|
});
|
|
61
62
|
}
|
|
62
63
|
/**
|
|
@@ -109,7 +110,7 @@ async function runUmbrella(opts, quiet, outLog, errLog) {
|
|
|
109
110
|
process.exitCode = 1;
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
|
-
async function runSync(agentSpec, opts) {
|
|
113
|
+
async function runSync(agentSpec, repoArg, opts) {
|
|
113
114
|
const quiet = !!opts.quiet;
|
|
114
115
|
const errLog = (msg) => { if (!quiet)
|
|
115
116
|
console.error(msg); };
|
|
@@ -119,16 +120,17 @@ async function runSync(agentSpec, opts) {
|
|
|
119
120
|
let agentId;
|
|
120
121
|
let version;
|
|
121
122
|
// A positional @selector typed by the user (latest/oldest/pinned/default/
|
|
122
|
-
// explicit). parseAgentSpec defaults a missing version to 'latest', so a
|
|
123
|
-
// `agents sync claude` and `agents sync claude@latest` are
|
|
124
|
-
// after parsing — we only treat the version as a selector
|
|
125
|
-
// actually typed, keeping bare `claude` on the
|
|
123
|
+
// all/explicit). parseAgentSpec defaults a missing version to 'latest', so a
|
|
124
|
+
// bare `agents sync claude` and `agents sync claude@latest` are
|
|
125
|
+
// indistinguishable after parsing — we only treat the version as a selector
|
|
126
|
+
// when an '@' was actually typed, keeping bare `claude` on the
|
|
127
|
+
// default-version path.
|
|
126
128
|
let selector;
|
|
127
129
|
if (agentSpec) {
|
|
128
130
|
const parsed = parseAgentSpec(agentSpec);
|
|
129
131
|
if (!parsed) {
|
|
130
132
|
errLog(chalk.red(`Invalid agent spec '${agentSpec}'.`));
|
|
131
|
-
errLog(chalk.gray('Examples: claude, claude@2.1.142, claude@latest, claude@oldest, claude@pinned'));
|
|
133
|
+
errLog(chalk.gray('Examples: claude, claude@2.1.142, claude@latest, claude@oldest, claude@pinned, claude@all'));
|
|
132
134
|
process.exitCode = 1;
|
|
133
135
|
return;
|
|
134
136
|
}
|
|
@@ -136,6 +138,18 @@ async function runSync(agentSpec, opts) {
|
|
|
136
138
|
if (agentSpec.includes('@'))
|
|
137
139
|
selector = parsed.version;
|
|
138
140
|
}
|
|
141
|
+
// Repo scope: --repo flag wins over the positional. Validate against the
|
|
142
|
+
// known DotAgent repos so a typo fails loudly instead of syncing nothing.
|
|
143
|
+
const repoScope = opts.repo || repoArg;
|
|
144
|
+
if (repoScope !== undefined) {
|
|
145
|
+
const known = listRepoNames();
|
|
146
|
+
if (!known.includes(repoScope)) {
|
|
147
|
+
errLog(chalk.red(`Unknown repo '${repoScope}'.`));
|
|
148
|
+
errLog(chalk.gray(`Known repos: ${known.join(', ')}`));
|
|
149
|
+
process.exitCode = 1;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
139
153
|
if (opts.agent) {
|
|
140
154
|
const resolved = resolveAgentName(opts.agent);
|
|
141
155
|
if (!resolved) {
|
|
@@ -156,6 +170,37 @@ async function runSync(agentSpec, opts) {
|
|
|
156
170
|
await runUmbrella(opts, quiet, outLog, errLog);
|
|
157
171
|
return;
|
|
158
172
|
}
|
|
173
|
+
const projectDir = opts.projectDir;
|
|
174
|
+
const cwd = opts.cwd || process.cwd();
|
|
175
|
+
const force = !!opts.force;
|
|
176
|
+
// ---------- 2a. @all: reconcile every installed version of this agent ----------
|
|
177
|
+
// Non-interactive by design — fanning an interactive preview across N
|
|
178
|
+
// versions is unusable. Honors an optional repo scope.
|
|
179
|
+
if (selector === 'all') {
|
|
180
|
+
const installed = listInstalledVersions(agentId);
|
|
181
|
+
if (installed.length === 0) {
|
|
182
|
+
errLog(chalk.red(`No ${agentLabel(agentId)} versions installed.`));
|
|
183
|
+
errLog(chalk.gray(`Install one: agents add ${agentId}@latest`));
|
|
184
|
+
process.exitCode = 1;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
let selection;
|
|
188
|
+
if (repoScope) {
|
|
189
|
+
selection = buildRepoScopedSelection(repoScope, cwd);
|
|
190
|
+
if (Object.keys(selection).length === 0) {
|
|
191
|
+
outLog(chalk.gray(`Nothing from repo '${repoScope}' to sync.`));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const scopeLabel = repoScope ? chalk.gray(` (repo: ${repoScope})`) : '';
|
|
196
|
+
outLog(chalk.cyan(`Syncing ${installed.length} ${agentLabel(agentId)} version(s)${scopeLabel}.`));
|
|
197
|
+
for (const v of installed) {
|
|
198
|
+
const result = syncResourcesToVersion(agentId, v, selection, { projectDir, cwd, force });
|
|
199
|
+
if (!quiet)
|
|
200
|
+
printSyncDetail(result, agentId, v, cwd);
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
159
204
|
// ---------- 2. Resolve version (project pin → global default → sole installed) ----------
|
|
160
205
|
// A positional @selector wins over the default-resolution below.
|
|
161
206
|
// @latest / @oldest → newest / oldest installed (process.exit if none)
|
|
@@ -197,15 +242,26 @@ async function runSync(agentSpec, opts) {
|
|
|
197
242
|
process.exitCode = 1;
|
|
198
243
|
return;
|
|
199
244
|
}
|
|
200
|
-
const projectDir = opts.projectDir;
|
|
201
|
-
const cwd = opts.cwd || process.cwd();
|
|
202
245
|
// ---------- 3. --launch mode bypasses everything below ----------
|
|
203
246
|
if (opts.launch) {
|
|
204
247
|
runLaunchMode(agentId, version, cwd, quiet);
|
|
205
248
|
return;
|
|
206
249
|
}
|
|
250
|
+
// ---------- 3b. Repo-scoped single-version sync ----------
|
|
251
|
+
// An explicit --repo / positional repo is a targeted request, so skip the
|
|
252
|
+
// interactive preview and reconcile just that repo's resources.
|
|
253
|
+
if (repoScope) {
|
|
254
|
+
const scoped = buildRepoScopedSelection(repoScope, cwd);
|
|
255
|
+
if (Object.keys(scoped).length === 0) {
|
|
256
|
+
outLog(chalk.gray(`Nothing from repo '${repoScope}' to sync into ${agentLabel(agentId)}@${version}.`));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const result = syncResourcesToVersion(agentId, version, scoped, { projectDir, cwd, force });
|
|
260
|
+
if (!quiet)
|
|
261
|
+
printSyncDetail(result, agentId, version, cwd);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
207
264
|
// ---------- 4. Decide selection (interactive preview vs auto) ----------
|
|
208
|
-
const force = !!opts.force;
|
|
209
265
|
const yes = !!opts.yes;
|
|
210
266
|
const interactive = !quiet && !yes && isInteractiveTerminal();
|
|
211
267
|
let selection;
|
package/dist/lib/agents.d.ts
CHANGED
|
@@ -111,10 +111,6 @@ export interface AccountInfo {
|
|
|
111
111
|
}
|
|
112
112
|
/** Return the email address associated with the agent's auth config, or null. */
|
|
113
113
|
export declare function getAccountEmail(agentId: AgentId, home?: string): Promise<string | null>;
|
|
114
|
-
/**
|
|
115
|
-
* Extract full account information (identity, plan, usage status, credits) from
|
|
116
|
-
* the agent's local auth/config files. Supports Claude, Codex, and Gemini.
|
|
117
|
-
*/
|
|
118
114
|
export declare function getAccountInfo(agentId: AgentId, home?: string): Promise<AccountInfo>;
|
|
119
115
|
/**
|
|
120
116
|
* Determine when the agent was last used by checking session file mtimes,
|
package/dist/lib/agents.js
CHANGED
|
@@ -410,6 +410,7 @@ export const AGENTS = {
|
|
|
410
410
|
npmPackage: '',
|
|
411
411
|
installScript: 'curl -fsSL https://antigravity.google/cli/install.sh | bash',
|
|
412
412
|
configDir: path.join(HOME, '.gemini', 'antigravity-cli'),
|
|
413
|
+
authFiles: ['antigravity-oauth-token'],
|
|
413
414
|
commandsDir: path.join(HOME, '.gemini', 'antigravity-cli', 'commands'),
|
|
414
415
|
commandsSubdir: 'commands',
|
|
415
416
|
skillsDir: path.join(HOME, '.gemini', 'antigravity-cli', 'skills'),
|
|
@@ -470,6 +471,7 @@ export const AGENTS = {
|
|
|
470
471
|
npmPackage: '@moonshot-ai/kimi-code',
|
|
471
472
|
installScript: 'curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash',
|
|
472
473
|
configDir: path.join(HOME, '.kimi-code'),
|
|
474
|
+
authFiles: ['credentials/kimi-code.json'],
|
|
473
475
|
commandsDir: '',
|
|
474
476
|
commandsSubdir: '',
|
|
475
477
|
skillsDir: path.join(HOME, '.kimi-code', 'skills'),
|
|
@@ -494,7 +496,8 @@ export const AGENTS = {
|
|
|
494
496
|
},
|
|
495
497
|
// Factory AI Droid CLI (`droid`) — agentic coding CLI from factory.ai.
|
|
496
498
|
// Install: `curl -fsSL https://app.factory.ai/cli | sh` (no npm package).
|
|
497
|
-
// Binary is NOT in node_modules/.bin —
|
|
499
|
+
// Binary is NOT in node_modules/.bin — the shim resolves the fixed install
|
|
500
|
+
// path ~/.local/bin/droid directly (see the droid branch in shims.ts).
|
|
498
501
|
// Config: `~/.factory/` (settings.json, mcp.json, droids/, commands/).
|
|
499
502
|
// Memory: native AGENTS.md. Subagents = custom droids (top-level .md files
|
|
500
503
|
// in ~/.factory/droids/). Config isolation rides the ~/.factory symlink
|
|
@@ -508,6 +511,7 @@ export const AGENTS = {
|
|
|
508
511
|
npmPackage: '',
|
|
509
512
|
installScript: 'curl -fsSL https://app.factory.ai/cli | sh',
|
|
510
513
|
configDir: path.join(HOME, '.factory'),
|
|
514
|
+
authFiles: ['auth.v2.file', 'auth.v2.key'],
|
|
511
515
|
commandsDir: path.join(HOME, '.factory', 'commands'),
|
|
512
516
|
commandsSubdir: 'commands',
|
|
513
517
|
skillsDir: '', // no skills concept
|
|
@@ -707,6 +711,7 @@ export const UNMANAGED_DETECTION_CANDIDATES = [
|
|
|
707
711
|
'gemini',
|
|
708
712
|
'grok',
|
|
709
713
|
'copilot',
|
|
714
|
+
'droid',
|
|
710
715
|
];
|
|
711
716
|
/**
|
|
712
717
|
* Detect existing agent installations that are NOT yet managed by agents-cli.
|
|
@@ -770,6 +775,33 @@ export async function getAccountEmail(agentId, home) {
|
|
|
770
775
|
* Extract full account information (identity, plan, usage status, credits) from
|
|
771
776
|
* the agent's local auth/config files. Supports Claude, Codex, and Gemini.
|
|
772
777
|
*/
|
|
778
|
+
/**
|
|
779
|
+
* Resolve a file-auth agent's credential file. Sign-in is account-global, but
|
|
780
|
+
* each installed version gets an isolated home; the credential physically lives
|
|
781
|
+
* only in the home the user logged in under (the one the `~/.<config>` symlink
|
|
782
|
+
* targets). Check the per-version `base` first, then fall back to the active
|
|
783
|
+
* config location under the real HOME so every installed version reflects the
|
|
784
|
+
* true account state (droid/antigravity/kimi all stored login per-version-home
|
|
785
|
+
* and showed non-active versions as "not signed in"). Returns the first
|
|
786
|
+
* existing path, or null.
|
|
787
|
+
*/
|
|
788
|
+
function resolveAccountCredentialPath(base, ...segments) {
|
|
789
|
+
const perVersion = path.join(base, ...segments);
|
|
790
|
+
try {
|
|
791
|
+
if (fs.existsSync(perVersion))
|
|
792
|
+
return perVersion;
|
|
793
|
+
}
|
|
794
|
+
catch { /* unreadable */ }
|
|
795
|
+
const active = path.join(process.env.AGENTS_REAL_HOME || os.homedir(), ...segments);
|
|
796
|
+
if (active !== perVersion) {
|
|
797
|
+
try {
|
|
798
|
+
if (fs.existsSync(active))
|
|
799
|
+
return active;
|
|
800
|
+
}
|
|
801
|
+
catch { /* unreadable */ }
|
|
802
|
+
}
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
773
805
|
export async function getAccountInfo(agentId, home) {
|
|
774
806
|
const base = home || os.homedir();
|
|
775
807
|
const empty = {
|
|
@@ -924,8 +956,8 @@ export async function getAccountInfo(agentId, home) {
|
|
|
924
956
|
// OAuth grant (access + refresh token, no id_token), so there's no email
|
|
925
957
|
// claim to read locally — presence of a refresh token is the only
|
|
926
958
|
// signed-in signal we can derive without a network call.
|
|
927
|
-
const tokenPath =
|
|
928
|
-
if (!
|
|
959
|
+
const tokenPath = resolveAccountCredentialPath(base, '.gemini', 'antigravity-cli', 'antigravity-oauth-token');
|
|
960
|
+
if (!tokenPath)
|
|
929
961
|
return { ...empty, lastActive };
|
|
930
962
|
const data = JSON.parse(await fs.promises.readFile(tokenPath, 'utf-8'));
|
|
931
963
|
const hasToken = typeof data?.token?.refresh_token === 'string' && !!data.token.refresh_token;
|
|
@@ -938,8 +970,8 @@ export async function getAccountInfo(agentId, home) {
|
|
|
938
970
|
// ~/.kimi-code/credentials/kimi-code.json. The access token is a JWT
|
|
939
971
|
// whose payload carries an opaque user_id (no email), so we report
|
|
940
972
|
// signed-in state plus a stable account key for usage dedup.
|
|
941
|
-
const credPath =
|
|
942
|
-
if (!
|
|
973
|
+
const credPath = resolveAccountCredentialPath(base, '.kimi-code', 'credentials', 'kimi-code.json');
|
|
974
|
+
if (!credPath)
|
|
943
975
|
return { ...empty, lastActive };
|
|
944
976
|
const data = JSON.parse(await fs.promises.readFile(credPath, 'utf-8'));
|
|
945
977
|
const accessToken = data?.access_token;
|
|
@@ -950,6 +982,18 @@ export async function getAccountInfo(agentId, home) {
|
|
|
950
982
|
const accountKey = buildIdentityKey(agentId, [['user', userId]]);
|
|
951
983
|
return { ...empty, signedIn: true, accountId: userId, accountKey, lastActive };
|
|
952
984
|
}
|
|
985
|
+
case 'droid': {
|
|
986
|
+
// Factory Droid stores auth at ~/.factory/auth.v2.file (+ auth.v2.key,
|
|
987
|
+
// an encrypted blob). No email/JWT is readable locally, so presence of
|
|
988
|
+
// the auth file is the only signed-in signal we can derive without a
|
|
989
|
+
// network call — same pattern as antigravity/kimi. `.factory` is the
|
|
990
|
+
// config dir on every platform (macOS/Linux ~/.factory, Windows
|
|
991
|
+
// %USERPROFILE%\.factory), so path.join keeps this cross-platform.
|
|
992
|
+
const authPath = resolveAccountCredentialPath(base, '.factory', 'auth.v2.file');
|
|
993
|
+
if (!authPath)
|
|
994
|
+
return { ...empty, lastActive };
|
|
995
|
+
return { ...empty, signedIn: true, lastActive };
|
|
996
|
+
}
|
|
953
997
|
default:
|
|
954
998
|
return { ...empty, lastActive };
|
|
955
999
|
}
|
|
@@ -1051,6 +1095,8 @@ function getSessionDir(agentId, base) {
|
|
|
1051
1095
|
// Copilot persists sessions at ~/.copilot/session-state/<id>/events.jsonl.
|
|
1052
1096
|
// The events.jsonl is the canonical NDJSON event stream per session.
|
|
1053
1097
|
return path.join(base, '.copilot', 'session-state');
|
|
1098
|
+
case 'droid':
|
|
1099
|
+
return path.join(base, '.factory', 'sessions');
|
|
1054
1100
|
default:
|
|
1055
1101
|
return null;
|
|
1056
1102
|
}
|
|
@@ -1061,6 +1107,7 @@ function getSessionExtension(agentId) {
|
|
|
1061
1107
|
case 'claude':
|
|
1062
1108
|
case 'codex':
|
|
1063
1109
|
case 'copilot':
|
|
1110
|
+
case 'droid':
|
|
1064
1111
|
return '.jsonl';
|
|
1065
1112
|
case 'gemini':
|
|
1066
1113
|
return '.json';
|
|
@@ -1661,6 +1708,8 @@ export const AGENT_NAME_ALIASES = {
|
|
|
1661
1708
|
gk: 'grok',
|
|
1662
1709
|
kimi: 'kimi',
|
|
1663
1710
|
'kimi-code': 'kimi',
|
|
1711
|
+
factory: 'droid',
|
|
1712
|
+
'factory-ai': 'droid',
|
|
1664
1713
|
};
|
|
1665
1714
|
/**
|
|
1666
1715
|
* Resolve a user-provided agent name (alias, shorthand, or canonical) to its AgentId.
|
|
@@ -8,6 +8,10 @@ import { writeProfileRuntime, clearProfileRuntime } from '../runtime-state.js';
|
|
|
8
8
|
// so existing importers of `shellQuote` from this module keep working.
|
|
9
9
|
import { shellQuote } from '../../ssh-exec.js';
|
|
10
10
|
export { shellQuote };
|
|
11
|
+
// The `ssh -L` tunnel spawn is shared with `agents computer --host`; it lives in
|
|
12
|
+
// the single ssh-tunnel helper. Calling it with no options preserves this
|
|
13
|
+
// driver's original foreground, stderr-captured behavior exactly.
|
|
14
|
+
import { startSSHTunnel } from '../../ssh-tunnel.js';
|
|
11
15
|
export async function connectSSH(endpoint, profile) {
|
|
12
16
|
const url = new URL(endpoint);
|
|
13
17
|
if (url.protocol !== 'ssh:') {
|
|
@@ -102,41 +106,6 @@ export async function connectSSH(endpoint, profile) {
|
|
|
102
106
|
},
|
|
103
107
|
};
|
|
104
108
|
}
|
|
105
|
-
function startSSHTunnel(user, host, localPort, remotePort) {
|
|
106
|
-
return new Promise((resolve, reject) => {
|
|
107
|
-
const args = [
|
|
108
|
-
'-L',
|
|
109
|
-
`${localPort}:127.0.0.1:${remotePort}`,
|
|
110
|
-
`${user}@${host}`,
|
|
111
|
-
'-N',
|
|
112
|
-
'-o',
|
|
113
|
-
'StrictHostKeyChecking=accept-new',
|
|
114
|
-
'-o',
|
|
115
|
-
'BatchMode=yes',
|
|
116
|
-
'-o',
|
|
117
|
-
'ConnectTimeout=10',
|
|
118
|
-
];
|
|
119
|
-
const tunnel = spawn('ssh', args, {
|
|
120
|
-
stdio: ['ignore', 'ignore', 'pipe'],
|
|
121
|
-
detached: false,
|
|
122
|
-
});
|
|
123
|
-
let stderr = '';
|
|
124
|
-
tunnel.stderr?.on('data', (data) => {
|
|
125
|
-
stderr += data.toString();
|
|
126
|
-
});
|
|
127
|
-
tunnel.on('error', (err) => {
|
|
128
|
-
reject(new Error(`SSH tunnel failed: ${err.message}`));
|
|
129
|
-
});
|
|
130
|
-
setTimeout(() => {
|
|
131
|
-
if (tunnel.killed) {
|
|
132
|
-
reject(new Error(`SSH tunnel died: ${stderr}`));
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
resolve(tunnel);
|
|
136
|
-
}
|
|
137
|
-
}, 500);
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
109
|
async function waitForPort(port, timeoutMs) {
|
|
141
110
|
const start = Date.now();
|
|
142
111
|
while (Date.now() - start < timeoutMs) {
|
|
@@ -20,10 +20,15 @@ export declare function loadDefaultPeers(): string[];
|
|
|
20
20
|
export declare function writeComputerPeers(allowedExecPaths: string[]): void;
|
|
21
21
|
export declare function resolveHelperExec(): string | null;
|
|
22
22
|
export declare function resolveHelperApp(): string | null;
|
|
23
|
+
export declare function resolveTcpEndpoint(): {
|
|
24
|
+
host: string;
|
|
25
|
+
port: number;
|
|
26
|
+
token: string | null;
|
|
27
|
+
} | null;
|
|
23
28
|
export declare function openComputerClient(): ComputerClient;
|
|
24
29
|
export declare const RPC_TIMEOUT_MS = 30000;
|
|
25
30
|
export declare function resolveRpcTimeoutMs(env: string | undefined): number;
|
|
26
31
|
export declare function describeTransport(): {
|
|
27
|
-
kind: 'socket' | 'stdio' | 'none';
|
|
32
|
+
kind: 'socket' | 'stdio' | 'tcp' | 'none';
|
|
28
33
|
path: string | null;
|
|
29
34
|
};
|
package/dist/lib/computer-rpc.js
CHANGED
|
@@ -187,9 +187,31 @@ export function resolveHelperApp() {
|
|
|
187
187
|
// exec = <bundle>/Contents/MacOS/ComputerHelper
|
|
188
188
|
return path.resolve(exec, '..', '..', '..');
|
|
189
189
|
}
|
|
190
|
-
//
|
|
191
|
-
//
|
|
190
|
+
// Resolve the TCP endpoint for the Windows daemon (computer-helper-win), if
|
|
191
|
+
// configured. The Windows helper binds loopback TCP (Program.cs) and the CLI
|
|
192
|
+
// reaches it over an `ssh -L` tunnel, so the endpoint is a local forwarded
|
|
193
|
+
// port. COMPUTER_HELPER_TCP is "host:port" (host defaults to 127.0.0.1);
|
|
194
|
+
// COMPUTER_HELPER_TOKEN is the shared secret sent in the first `auth` frame.
|
|
195
|
+
export function resolveTcpEndpoint() {
|
|
196
|
+
const raw = process.env.COMPUTER_HELPER_TCP;
|
|
197
|
+
if (!raw || raw.length === 0)
|
|
198
|
+
return null;
|
|
199
|
+
const [hostPart, portPart] = raw.includes(':') ? raw.split(':') : ['127.0.0.1', raw];
|
|
200
|
+
const port = Number(portPart);
|
|
201
|
+
if (!Number.isInteger(port) || port <= 0)
|
|
202
|
+
return null;
|
|
203
|
+
const token = process.env.COMPUTER_HELPER_TOKEN;
|
|
204
|
+
return { host: hostPart || '127.0.0.1', port, token: token && token.length > 0 ? token : null };
|
|
205
|
+
}
|
|
206
|
+
// Pick the best transport. Precedence:
|
|
207
|
+
// 1. COMPUTER_HELPER_TCP -> the Windows daemon over a (tunneled) TCP port.
|
|
208
|
+
// 2. the macOS launchd socket if it exists.
|
|
209
|
+
// 3. spawning the helper as a subprocess (legacy/dev fallback).
|
|
192
210
|
export function openComputerClient() {
|
|
211
|
+
const tcp = resolveTcpEndpoint();
|
|
212
|
+
if (tcp) {
|
|
213
|
+
return new TcpClient(tcp.host, tcp.port, tcp.token);
|
|
214
|
+
}
|
|
193
215
|
const sockPath = resolveSocketPath();
|
|
194
216
|
if (fs.existsSync(sockPath)) {
|
|
195
217
|
return new SocketClient(sockPath);
|
|
@@ -297,6 +319,61 @@ class SocketClient extends BaseClient {
|
|
|
297
319
|
});
|
|
298
320
|
}
|
|
299
321
|
}
|
|
322
|
+
// TCP transport for the Windows daemon (computer-helper-win). The daemon
|
|
323
|
+
// binds loopback only (Program.cs); the CLI reaches it over an `ssh -L`
|
|
324
|
+
// tunnel, so `host` is typically 127.0.0.1 + a forwarded port. When a token
|
|
325
|
+
// is configured the daemon accepts only an `auth` frame until authenticated,
|
|
326
|
+
// so we send that first and gate every other call on it.
|
|
327
|
+
class TcpClient extends BaseClient {
|
|
328
|
+
sock;
|
|
329
|
+
authReady;
|
|
330
|
+
constructor(host, port, token) {
|
|
331
|
+
super();
|
|
332
|
+
this.sock = createConnection({ host, port });
|
|
333
|
+
this.sock.setEncoding('utf8');
|
|
334
|
+
this.sock.on('data', (chunk) => this.handleChunk(chunk));
|
|
335
|
+
this.sock.on('error', (err) => {
|
|
336
|
+
this.closed = true;
|
|
337
|
+
this.failPending('socket_error', err.message);
|
|
338
|
+
});
|
|
339
|
+
this.sock.on('close', () => {
|
|
340
|
+
this.closed = true;
|
|
341
|
+
this.failPending('helper_exited', 'tcp connection closed before reply');
|
|
342
|
+
});
|
|
343
|
+
// Kick off the auth handshake synchronously so its frame (id 1) is the
|
|
344
|
+
// first thing written. No token → daemon is open (tunnel-gated).
|
|
345
|
+
this.authReady = token ? this.authenticate(token) : Promise.resolve();
|
|
346
|
+
}
|
|
347
|
+
async authenticate(token) {
|
|
348
|
+
const res = await super.call('auth', { token });
|
|
349
|
+
if (res.error)
|
|
350
|
+
throw new Error(`computer-helper auth failed: ${res.error.code}`);
|
|
351
|
+
}
|
|
352
|
+
async call(method, params) {
|
|
353
|
+
if (method !== 'auth') {
|
|
354
|
+
try {
|
|
355
|
+
await this.authReady;
|
|
356
|
+
}
|
|
357
|
+
catch (e) {
|
|
358
|
+
return { id: null, error: { code: 'auth_failed', message: e.message } };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return super.call(method, params);
|
|
362
|
+
}
|
|
363
|
+
send(payload) {
|
|
364
|
+
this.sock.write(payload);
|
|
365
|
+
}
|
|
366
|
+
async close() {
|
|
367
|
+
if (this.closed)
|
|
368
|
+
return;
|
|
369
|
+
this.sock.end();
|
|
370
|
+
await new Promise((resolve) => {
|
|
371
|
+
if (this.closed)
|
|
372
|
+
return resolve();
|
|
373
|
+
this.sock.on('close', () => resolve());
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
300
377
|
class StdioClient extends BaseClient {
|
|
301
378
|
proc;
|
|
302
379
|
constructor(helperPath) {
|
|
@@ -324,8 +401,14 @@ class StdioClient extends BaseClient {
|
|
|
324
401
|
}
|
|
325
402
|
}
|
|
326
403
|
// Describe which transport is currently in use. Useful for diagnostics
|
|
327
|
-
// like `agents computer status`.
|
|
404
|
+
// like `agents computer status`. TCP takes precedence to match
|
|
405
|
+
// openComputerClient() — when COMPUTER_HELPER_TCP is set we drive a remote
|
|
406
|
+
// (Windows) daemon over a tunnel, so callers off macOS (no socket, no local
|
|
407
|
+
// .app) must not be told "no transport". `path` is null: the endpoint is an
|
|
408
|
+
// env-configured host:port, not an on-disk path.
|
|
328
409
|
export function describeTransport() {
|
|
410
|
+
if (resolveTcpEndpoint())
|
|
411
|
+
return { kind: 'tcp', path: null };
|
|
329
412
|
const sockPath = resolveSocketPath();
|
|
330
413
|
if (fs.existsSync(sockPath))
|
|
331
414
|
return { kind: 'socket', path: sockPath };
|
package/dist/lib/exec.js
CHANGED
|
@@ -534,6 +534,20 @@ export function buildExecCommand(options) {
|
|
|
534
534
|
cmd.push('--dangerously-bypass-approvals-and-sandbox');
|
|
535
535
|
}
|
|
536
536
|
}
|
|
537
|
+
else if (options.agent === 'kimi' && !interactive) {
|
|
538
|
+
// kimi's headless prompt mode (`-p`/`--prompt`) is self-contained and REFUSES
|
|
539
|
+
// to be combined with any startup-mode flag: `--plan`, `--auto`, and `--yolo`
|
|
540
|
+
// all abort with "Cannot combine --prompt with --X" (verified against the live
|
|
541
|
+
// kimi CLI). The write-capable modes (edit/auto/skip) all collapse to kimi's
|
|
542
|
+
// default `-p` behavior, which already auto-approves tool calls, so we emit no
|
|
543
|
+
// mode flag. Plan (read-only) has no headless equivalent, so fail closed rather
|
|
544
|
+
// than silently letting a plan-mode run mutate the workspace.
|
|
545
|
+
if (resolvedMode === 'plan') {
|
|
546
|
+
throw new Error('kimi has no headless read-only mode: `--prompt` cannot be combined with `--plan`. ' +
|
|
547
|
+
'Run kimi in plan mode interactively (omit the prompt), or use --mode edit, auto, or skip.');
|
|
548
|
+
}
|
|
549
|
+
// edit/auto/skip: emit no mode flag — `kimi -p` auto-runs.
|
|
550
|
+
}
|
|
537
551
|
else {
|
|
538
552
|
cmd.push(...modeFlags);
|
|
539
553
|
}
|