@phnx-labs/agents-cli 1.20.49 → 1.20.51
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 +31 -0
- package/README.md +3 -0
- package/dist/commands/browser-picker.js +1 -18
- package/dist/commands/cloud.js +1 -25
- package/dist/commands/computer.d.ts +1 -0
- package/dist/commands/computer.js +129 -8
- package/dist/commands/doctor.js +133 -2
- package/dist/commands/exec.js +49 -6
- package/dist/commands/factory.js +1 -4
- package/dist/commands/inspect.js +1 -11
- package/dist/commands/mcp.js +2 -6
- package/dist/commands/message.js +1 -4
- package/dist/commands/profiles.js +1 -18
- package/dist/commands/repo.js +33 -14
- package/dist/commands/resource-view.d.ts +1 -0
- package/dist/commands/resource-view.js +5 -17
- package/dist/commands/secrets.d.ts +1 -0
- package/dist/commands/secrets.js +1 -28
- package/dist/commands/sessions-picker.js +1 -18
- package/dist/commands/sessions.js +6 -8
- package/dist/commands/teams-picker.js +1 -32
- package/dist/commands/teams.js +218 -97
- package/dist/commands/tmux.js +1 -3
- package/dist/commands/view.js +1 -9
- package/dist/commands/worktree.js +1 -4
- package/dist/lib/agents.d.ts +0 -4
- package/dist/lib/agents.js +20 -33
- package/dist/lib/auto-dispatch-linear.d.ts +18 -0
- package/dist/lib/auto-dispatch-linear.js +107 -0
- package/dist/lib/auto-dispatch-provider.d.ts +10 -0
- package/dist/lib/auto-dispatch-provider.js +25 -0
- package/dist/lib/auto-dispatch.d.ts +87 -0
- package/dist/lib/auto-dispatch.js +142 -0
- package/dist/lib/browser/cdp.js +11 -2
- package/dist/lib/browser/drivers/ssh.d.ts +28 -10
- package/dist/lib/browser/drivers/ssh.js +57 -18
- package/dist/lib/browser/refs.js +1 -5
- package/dist/lib/cli-resources.d.ts +0 -2
- package/dist/lib/cli-resources.js +30 -13
- package/dist/lib/cloud/rush.d.ts +0 -24
- package/dist/lib/cloud/rush.js +0 -31
- package/dist/lib/crabbox/cli.js +4 -1
- package/dist/lib/crabbox/lease.js +29 -1
- package/dist/lib/daemon.js +41 -0
- package/dist/lib/exec.js +43 -18
- package/dist/lib/format.d.ts +38 -0
- package/dist/lib/format.js +108 -0
- package/dist/lib/git.d.ts +21 -0
- package/dist/lib/git.js +92 -0
- package/dist/lib/hooks/cache.d.ts +9 -2
- package/dist/lib/hooks/cache.js +220 -8
- package/dist/lib/hooks.js +17 -8
- package/dist/lib/hosts/passthrough.js +30 -1
- package/dist/lib/hosts/progress.d.ts +31 -0
- package/dist/lib/hosts/progress.js +35 -0
- package/dist/lib/hosts/remote-cmd.d.ts +1 -1
- package/dist/lib/hosts/remote-cmd.js +13 -3
- package/dist/lib/platform/exec.d.ts +4 -1
- package/dist/lib/platform/exec.js +8 -2
- package/dist/lib/resources.d.ts +0 -8
- package/dist/lib/resources.js +0 -10
- package/dist/lib/runner.js +10 -2
- 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/session/active.d.ts +11 -1
- package/dist/lib/session/active.js +3 -0
- package/dist/lib/session/db.d.ts +1 -4
- package/dist/lib/session/db.js +20 -25
- package/dist/lib/session/discover.d.ts +2 -2
- package/dist/lib/session/discover.js +61 -48
- package/dist/lib/session/parse.js +35 -34
- package/dist/lib/session/render.d.ts +7 -3
- package/dist/lib/session/render.js +15 -9
- package/dist/lib/session/state.d.ts +55 -0
- package/dist/lib/session/state.js +87 -10
- package/dist/lib/session/types.d.ts +9 -0
- package/dist/lib/shims.d.ts +25 -2
- package/dist/lib/shims.js +73 -8
- package/dist/lib/ssh-tunnel.d.ts +33 -2
- package/dist/lib/ssh-tunnel.js +94 -7
- package/dist/lib/staleness/types.d.ts +0 -1
- package/dist/lib/teams/agents.d.ts +108 -1
- package/dist/lib/teams/agents.js +511 -11
- package/dist/lib/teams/api.d.ts +7 -1
- package/dist/lib/teams/api.js +5 -2
- package/dist/lib/teams/registry.d.ts +17 -0
- package/dist/lib/teams/registry.js +2 -0
- package/dist/lib/teams/remoteWorktree.d.ts +57 -0
- package/dist/lib/teams/remoteWorktree.js +213 -0
- package/dist/lib/teams/scheduler.d.ts +29 -0
- package/dist/lib/teams/scheduler.js +78 -0
- package/dist/lib/teams/supervisor.js +7 -0
- package/dist/lib/types.d.ts +14 -1
- package/dist/lib/versions.d.ts +7 -26
- package/dist/lib/versions.js +44 -146
- package/dist/lib/warn-unpushed.d.ts +40 -0
- package/dist/lib/warn-unpushed.js +128 -0
- package/package.json +3 -1
- package/dist/lib/resources/index.d.ts +0 -53
- package/dist/lib/resources/index.js +0 -76
package/dist/lib/agents.js
CHANGED
|
@@ -21,6 +21,7 @@ import { latestFileMtimeMs } from './fs-walk.js';
|
|
|
21
21
|
import { damerauLevenshtein } from './fuzzy.js';
|
|
22
22
|
import { getCacheDir, getVersionsDir, getShimsDir, getCliVersionCachePath } from './state.js';
|
|
23
23
|
import { resolveVersion, getVersionHomePath, getBinaryPath } from './versions.js';
|
|
24
|
+
import { supports } from './capabilities.js';
|
|
24
25
|
const execFileAsync = promisify(execFile);
|
|
25
26
|
const HOME = os.homedir();
|
|
26
27
|
/**
|
|
@@ -212,7 +213,7 @@ export const AGENTS = {
|
|
|
212
213
|
// Claude Code has no headless Anthropic-hosted dispatch CLI (only
|
|
213
214
|
// --remote-control, which bridges a *local* session). Its cloud is Rush.
|
|
214
215
|
cloudProvider: 'rush',
|
|
215
|
-
capabilities: { hooks: true, mcp: true, allowlist: true, skills: true, commands: true, plugins: true, subagents: true, rules: { file: 'CLAUDE.md' }, workflows: true, modes: ['plan', 'edit', 'auto', 'skip'], rulesImports: true },
|
|
216
|
+
capabilities: { hooks: true, mcp: true, mcpHttp: true, mcpHeaders: true, allowlist: true, skills: true, commands: true, plugins: true, subagents: true, rules: { file: 'CLAUDE.md' }, workflows: true, modes: ['plan', 'edit', 'auto', 'skip'], rulesImports: true },
|
|
216
217
|
},
|
|
217
218
|
// codex hooks: gated to >= 0.116.0 (introduced [features] codex_hooks flag).
|
|
218
219
|
codex: {
|
|
@@ -232,7 +233,7 @@ export const AGENTS = {
|
|
|
232
233
|
variableSyntax: '$ARGUMENTS',
|
|
233
234
|
supportsHooks: true,
|
|
234
235
|
cloudProvider: 'codex',
|
|
235
|
-
capabilities: { hooks: { since: '0.116.0' }, mcp: true, allowlist: false, skills: true, commands: { until: '0.117.0' }, plugins: { since: '0.128.0' }, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['plan', 'edit', 'skip'] },
|
|
236
|
+
capabilities: { hooks: { since: '0.116.0' }, mcp: true, mcpHttp: true, mcpHeaders: false, allowlist: false, skills: true, commands: { until: '0.117.0' }, plugins: { since: '0.128.0' }, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['plan', 'edit', 'skip'] },
|
|
236
237
|
},
|
|
237
238
|
gemini: {
|
|
238
239
|
id: 'gemini',
|
|
@@ -261,7 +262,7 @@ export const AGENTS = {
|
|
|
261
262
|
url: 'https://developers.googleblog.com/an-important-update-transitioning-gemini-cli-to-antigravity-cli/',
|
|
262
263
|
},
|
|
263
264
|
// gemini hooks: shipped in v0.26.0 (Jan 2026); older binaries silently ignore the `hooks` key.
|
|
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 },
|
|
265
|
+
capabilities: { hooks: { since: '0.26.0' }, mcp: true, mcpHttp: true, mcpHeaders: false, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: 'GEMINI.md' }, workflows: false, modes: ['plan', 'edit', 'skip'], rulesImports: true },
|
|
265
266
|
},
|
|
266
267
|
cursor: {
|
|
267
268
|
id: 'cursor',
|
|
@@ -279,7 +280,7 @@ export const AGENTS = {
|
|
|
279
280
|
format: 'markdown',
|
|
280
281
|
variableSyntax: '$ARGUMENTS',
|
|
281
282
|
supportsHooks: false,
|
|
282
|
-
capabilities: { hooks: false, mcp: true, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: '.cursorrules' }, workflows: false, modes: ['edit', 'skip'] },
|
|
283
|
+
capabilities: { hooks: false, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: '.cursorrules' }, workflows: false, modes: ['edit', 'skip'] },
|
|
283
284
|
},
|
|
284
285
|
opencode: {
|
|
285
286
|
id: 'opencode',
|
|
@@ -296,7 +297,7 @@ export const AGENTS = {
|
|
|
296
297
|
format: 'markdown',
|
|
297
298
|
variableSyntax: '$ARGUMENTS',
|
|
298
299
|
supportsHooks: false,
|
|
299
|
-
capabilities: { hooks: false, mcp: true, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['plan', 'edit'] },
|
|
300
|
+
capabilities: { hooks: false, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['plan', 'edit'] },
|
|
300
301
|
},
|
|
301
302
|
openclaw: {
|
|
302
303
|
id: 'openclaw',
|
|
@@ -314,7 +315,7 @@ export const AGENTS = {
|
|
|
314
315
|
format: 'markdown',
|
|
315
316
|
variableSyntax: '{{ARGUMENTS}}',
|
|
316
317
|
supportsHooks: true,
|
|
317
|
-
capabilities: { hooks: true, mcp: true, allowlist: false, skills: true, commands: false, plugins: true, subagents: true, rules: { file: 'workspace/AGENTS.md' }, workflows: false, modes: ['plan', 'edit', 'skip'] },
|
|
318
|
+
capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: false, skills: true, commands: false, plugins: true, subagents: true, rules: { file: 'workspace/AGENTS.md' }, workflows: false, modes: ['plan', 'edit', 'skip'] },
|
|
318
319
|
},
|
|
319
320
|
copilot: {
|
|
320
321
|
id: 'copilot',
|
|
@@ -336,7 +337,7 @@ export const AGENTS = {
|
|
|
336
337
|
format: 'markdown',
|
|
337
338
|
variableSyntax: '$ARGUMENTS',
|
|
338
339
|
supportsHooks: false,
|
|
339
|
-
capabilities: { hooks: false, mcp: true, allowlist: false, skills: true, commands: true, plugins: true, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['plan', 'edit', 'auto', 'skip'] },
|
|
340
|
+
capabilities: { hooks: false, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: false, skills: true, commands: true, plugins: true, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['plan', 'edit', 'auto', 'skip'] },
|
|
340
341
|
},
|
|
341
342
|
amp: {
|
|
342
343
|
id: 'amp',
|
|
@@ -353,7 +354,7 @@ export const AGENTS = {
|
|
|
353
354
|
format: 'markdown',
|
|
354
355
|
variableSyntax: '$ARGUMENTS',
|
|
355
356
|
supportsHooks: false,
|
|
356
|
-
capabilities: { hooks: false, mcp: true, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['plan', 'edit'] },
|
|
357
|
+
capabilities: { hooks: false, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['plan', 'edit'] },
|
|
357
358
|
},
|
|
358
359
|
kiro: {
|
|
359
360
|
id: 'kiro',
|
|
@@ -371,7 +372,7 @@ export const AGENTS = {
|
|
|
371
372
|
format: 'markdown',
|
|
372
373
|
variableSyntax: '$ARGUMENTS',
|
|
373
374
|
supportsHooks: false,
|
|
374
|
-
capabilities: { hooks: false, mcp: true, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['edit'] },
|
|
375
|
+
capabilities: { hooks: false, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['edit'] },
|
|
375
376
|
},
|
|
376
377
|
goose: {
|
|
377
378
|
id: 'goose',
|
|
@@ -389,7 +390,7 @@ export const AGENTS = {
|
|
|
389
390
|
format: 'markdown',
|
|
390
391
|
variableSyntax: '$ARGUMENTS',
|
|
391
392
|
supportsHooks: false,
|
|
392
|
-
capabilities: { hooks: false, mcp: true, allowlist: false, skills: false, commands: false, plugins: false, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['edit'] },
|
|
393
|
+
capabilities: { hooks: false, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: false, skills: false, commands: false, plugins: false, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['edit'] },
|
|
393
394
|
},
|
|
394
395
|
// Google Antigravity CLI (`agy`) — official replacement for Gemini CLI as of IO 2026.
|
|
395
396
|
// configDir nests inside `~/.gemini/` since agy shares the parent dir with the Gemini
|
|
@@ -418,7 +419,7 @@ export const AGENTS = {
|
|
|
418
419
|
variableSyntax: '{{args}}',
|
|
419
420
|
supportsHooks: true,
|
|
420
421
|
cloudProvider: 'antigravity',
|
|
421
|
-
capabilities: { hooks: true, mcp: true, allowlist: true, skills: true, commands: true, plugins: true, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['edit', 'skip'], rulesImports: false },
|
|
422
|
+
capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: true, skills: true, commands: true, plugins: true, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['edit', 'skip'], rulesImports: false },
|
|
422
423
|
},
|
|
423
424
|
// xAI Grok Build CLI (`grok`) — early beta, SuperGrok Heavy. Auth via OAuth on
|
|
424
425
|
// first launch, or XAI_API_KEY env var for headless. MCP servers configured inline
|
|
@@ -445,6 +446,8 @@ export const AGENTS = {
|
|
|
445
446
|
capabilities: {
|
|
446
447
|
hooks: true,
|
|
447
448
|
mcp: true,
|
|
449
|
+
mcpHttp: false,
|
|
450
|
+
mcpHeaders: false,
|
|
448
451
|
allowlist: true, // maps to Grok's granular Bash/Edit/Write/Read/Grep/WebFetch/MCPTool rules
|
|
449
452
|
skills: true,
|
|
450
453
|
commands: false, // covered by skills
|
|
@@ -481,6 +484,8 @@ export const AGENTS = {
|
|
|
481
484
|
capabilities: {
|
|
482
485
|
hooks: true,
|
|
483
486
|
mcp: true,
|
|
487
|
+
mcpHttp: false,
|
|
488
|
+
mcpHeaders: false,
|
|
484
489
|
allowlist: true,
|
|
485
490
|
skills: true,
|
|
486
491
|
commands: false,
|
|
@@ -540,6 +545,8 @@ export const AGENTS = {
|
|
|
540
545
|
capabilities: {
|
|
541
546
|
hooks: true,
|
|
542
547
|
mcp: true,
|
|
548
|
+
mcpHttp: false,
|
|
549
|
+
mcpHeaders: false,
|
|
543
550
|
allowlist: false,
|
|
544
551
|
skills: false,
|
|
545
552
|
commands: true,
|
|
@@ -708,11 +715,6 @@ export async function getAllCliStates() {
|
|
|
708
715
|
}
|
|
709
716
|
return states;
|
|
710
717
|
}
|
|
711
|
-
/** Check whether the agent's config directory exists on disk. */
|
|
712
|
-
export function isConfigured(agentId) {
|
|
713
|
-
const agent = AGENTS[agentId];
|
|
714
|
-
return fs.existsSync(agent.configDir);
|
|
715
|
-
}
|
|
716
718
|
/**
|
|
717
719
|
* Agents that `agents setup` probes for pre-existing native installations
|
|
718
720
|
* (i.e., a config dir present before agents-cli took over). Add an agent here
|
|
@@ -1310,31 +1312,16 @@ function buildIdentityKey(agentId, parts) {
|
|
|
1310
1312
|
return null;
|
|
1311
1313
|
return `${agentId}:${encoded.join(':')}`;
|
|
1312
1314
|
}
|
|
1313
|
-
/** Check whether a named MCP server is registered with the agent's CLI. */
|
|
1314
|
-
export async function isMcpRegistered(agentId, mcpName) {
|
|
1315
|
-
const agent = AGENTS[agentId];
|
|
1316
|
-
if (!agent.capabilities.mcp || !(await isCliInstalled(agentId))) {
|
|
1317
|
-
return false;
|
|
1318
|
-
}
|
|
1319
|
-
try {
|
|
1320
|
-
const { stdout } = await execFileAsync(agent.cliCommand, ['mcp', 'list']);
|
|
1321
|
-
return stdout.toLowerCase().includes(mcpName.toLowerCase());
|
|
1322
|
-
}
|
|
1323
|
-
catch {
|
|
1324
|
-
/* mcp list command failed */
|
|
1325
|
-
return false;
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
1315
|
/** Register an MCP server with an agent's CLI via `mcp add`. */
|
|
1329
1316
|
export async function registerMcp(agentId, name, command, scope = 'user', transport = 'stdio', options) {
|
|
1330
1317
|
const agent = AGENTS[agentId];
|
|
1331
1318
|
if (!agent.capabilities.mcp) {
|
|
1332
1319
|
return { success: false, error: 'Agent does not support MCP' };
|
|
1333
1320
|
}
|
|
1334
|
-
if (transport === 'http' && agentId
|
|
1321
|
+
if (transport === 'http' && !supports(agentId, 'mcpHttp').ok) {
|
|
1335
1322
|
return { success: false, error: 'skipped: agent does not support HTTP MCP registration' };
|
|
1336
1323
|
}
|
|
1337
|
-
if (transport === 'http' && options?.headers && Object.keys(options.headers).length > 0 && agentId
|
|
1324
|
+
if (transport === 'http' && options?.headers && Object.keys(options.headers).length > 0 && !supports(agentId, 'mcpHeaders').ok) {
|
|
1338
1325
|
return { success: false, error: 'skipped: HTTP MCP headers are only supported for Claude registration' };
|
|
1339
1326
|
}
|
|
1340
1327
|
if (!options?.binary && !(await isCliInstalled(agentId))) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concrete Linear gateway for auto-dispatch.
|
|
3
|
+
*
|
|
4
|
+
* Talks to the Linear GraphQL API. The API key is read from the LINEAR_API_KEY
|
|
5
|
+
* env var, falling back to the macOS keychain generic-password `linear-api-key`
|
|
6
|
+
* (how the rest of the stack stores it). If no key is resolvable, the gateway is
|
|
7
|
+
* absent and the daemon skips auto-dispatch entirely.
|
|
8
|
+
*
|
|
9
|
+
* "Started" (Doing) and "unstarted" (Todo) are Linear state *types*, so we never
|
|
10
|
+
* hardcode a team's custom state names. `startIssue` resolves the team's first
|
|
11
|
+
* started-type state (cached per team) and moves the issue there — the existing
|
|
12
|
+
* factory webhook turns that transition into a real run.
|
|
13
|
+
*/
|
|
14
|
+
import type { LinearGateway } from './auto-dispatch.js';
|
|
15
|
+
/** Resolve the Linear API key from env or (on macOS) the keychain. Null if absent. */
|
|
16
|
+
export declare function resolveLinearApiKey(): string | null;
|
|
17
|
+
/** Build a real Linear gateway, or null when no API key is configured. */
|
|
18
|
+
export declare function createLinearGateway(): LinearGateway | null;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concrete Linear gateway for auto-dispatch.
|
|
3
|
+
*
|
|
4
|
+
* Talks to the Linear GraphQL API. The API key is read from the LINEAR_API_KEY
|
|
5
|
+
* env var, falling back to the macOS keychain generic-password `linear-api-key`
|
|
6
|
+
* (how the rest of the stack stores it). If no key is resolvable, the gateway is
|
|
7
|
+
* absent and the daemon skips auto-dispatch entirely.
|
|
8
|
+
*
|
|
9
|
+
* "Started" (Doing) and "unstarted" (Todo) are Linear state *types*, so we never
|
|
10
|
+
* hardcode a team's custom state names. `startIssue` resolves the team's first
|
|
11
|
+
* started-type state (cached per team) and moves the issue there — the existing
|
|
12
|
+
* factory webhook turns that transition into a real run.
|
|
13
|
+
*/
|
|
14
|
+
import { execFileSync } from 'child_process';
|
|
15
|
+
const LINEAR_API = 'https://api.linear.app/graphql';
|
|
16
|
+
/** Resolve the Linear API key from env or (on macOS) the keychain. Null if absent. */
|
|
17
|
+
export function resolveLinearApiKey() {
|
|
18
|
+
const fromEnv = process.env.LINEAR_API_KEY?.trim();
|
|
19
|
+
if (fromEnv)
|
|
20
|
+
return fromEnv;
|
|
21
|
+
if (process.platform === 'darwin') {
|
|
22
|
+
try {
|
|
23
|
+
const out = execFileSync('security', ['find-generic-password', '-s', 'linear-api-key', '-w'], {
|
|
24
|
+
encoding: 'utf-8',
|
|
25
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
26
|
+
});
|
|
27
|
+
const key = out.trim();
|
|
28
|
+
if (key)
|
|
29
|
+
return key;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// not in keychain — fall through
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
async function gql(apiKey, query, variables) {
|
|
38
|
+
const res = await fetch(LINEAR_API, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: { Authorization: apiKey, 'Content-Type': 'application/json' },
|
|
41
|
+
body: JSON.stringify({ query, variables }),
|
|
42
|
+
});
|
|
43
|
+
if (!res.ok)
|
|
44
|
+
throw new Error(`Linear API HTTP ${res.status}`);
|
|
45
|
+
const json = (await res.json());
|
|
46
|
+
if (json.errors?.length)
|
|
47
|
+
throw new Error(`Linear API: ${json.errors.map((e) => e.message).join('; ')}`);
|
|
48
|
+
if (!json.data)
|
|
49
|
+
throw new Error('Linear API: empty response');
|
|
50
|
+
return json.data;
|
|
51
|
+
}
|
|
52
|
+
/** Build a real Linear gateway, or null when no API key is configured. */
|
|
53
|
+
export function createLinearGateway() {
|
|
54
|
+
const apiKey = resolveLinearApiKey();
|
|
55
|
+
if (!apiKey)
|
|
56
|
+
return null;
|
|
57
|
+
// Cache the resolved started-state id per team so we don't re-query each dispatch.
|
|
58
|
+
const startedStateByTeam = new Map();
|
|
59
|
+
async function resolveStartedState(teamId) {
|
|
60
|
+
const cached = startedStateByTeam.get(teamId);
|
|
61
|
+
if (cached)
|
|
62
|
+
return cached;
|
|
63
|
+
const data = await gql(apiKey, `query($t:String!){ workflowStates(filter:{ team:{ id:{ eq:$t } }, type:{ eq:"started" } }, first:10){ nodes{ id type position } } }`, { t: teamId });
|
|
64
|
+
const nodes = data.workflowStates.nodes.slice().sort((a, b) => a.position - b.position);
|
|
65
|
+
const state = nodes[0];
|
|
66
|
+
if (!state)
|
|
67
|
+
throw new Error(`no started-type workflow state for team ${teamId}`);
|
|
68
|
+
startedStateByTeam.set(teamId, state.id);
|
|
69
|
+
return state.id;
|
|
70
|
+
}
|
|
71
|
+
// team id per issue, captured during fetchDelegatedTodo, so startIssue can resolve state.
|
|
72
|
+
const teamByIssue = new Map();
|
|
73
|
+
return {
|
|
74
|
+
async countInFlight(linearProjectId) {
|
|
75
|
+
const data = await gql(apiKey, `query($p:ID!){ issues(filter:{ project:{ id:{ eq:$p } }, state:{ type:{ eq:"started" } }, delegate:{ null:false } }, first:250){ nodes{ id } } }`, { p: linearProjectId });
|
|
76
|
+
return data.issues.nodes.length;
|
|
77
|
+
},
|
|
78
|
+
async fetchDelegatedTodo(linearProjectId) {
|
|
79
|
+
const data = await gql(apiKey, `query($p:ID!){ issues(filter:{ project:{ id:{ eq:$p } }, state:{ type:{ eq:"unstarted" } }, delegate:{ null:false } }, first:50){ nodes{ id identifier title priority delegate{ name } team{ id } } } }`, { p: linearProjectId });
|
|
80
|
+
const out = [];
|
|
81
|
+
for (const n of data.issues.nodes) {
|
|
82
|
+
if (!n.delegate?.name)
|
|
83
|
+
continue;
|
|
84
|
+
if (n.team?.id)
|
|
85
|
+
teamByIssue.set(n.id, n.team.id);
|
|
86
|
+
out.push({ id: n.id, identifier: n.identifier, title: n.title ?? '', priority: n.priority ?? 0, delegateName: n.delegate.name });
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
},
|
|
90
|
+
async markStarted(issueId, delegateName) {
|
|
91
|
+
const teamId = teamByIssue.get(issueId);
|
|
92
|
+
if (!teamId)
|
|
93
|
+
throw new Error(`unknown team for issue ${issueId} (fetch it first)`);
|
|
94
|
+
const stateId = await resolveStartedState(teamId);
|
|
95
|
+
const data = await gql(apiKey, `mutation($id:String!,$s:String!){ issueUpdate(id:$id, input:{ stateId:$s }){ success } }`, { id: issueId, s: stateId });
|
|
96
|
+
if (!data.issueUpdate.success)
|
|
97
|
+
throw new Error(`issueUpdate failed for ${issueId}`);
|
|
98
|
+
// Leave a trail so the auto-dispatch is visible in the ticket history.
|
|
99
|
+
try {
|
|
100
|
+
await gql(apiKey, `mutation($i:String!,$b:String!){ commentCreate(input:{ issueId:$i, body:$b }){ success } }`, { i: issueId, b: `Auto-dispatched to **${delegateName}** by the factory (moved Todo → Doing).` });
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// comment is best-effort — the state change is what matters
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concrete Dispatcher for auto-dispatch — runs a ticket through agents-cli's own
|
|
3
|
+
* cloud-provider abstraction (`resolveProvider().dispatch()`), the same layer as
|
|
4
|
+
* `agents cloud run`. Rush (Prix) is one provider among rush/codex/factory; a
|
|
5
|
+
* project pins its provider via `provider` in projects.json, else the delegate
|
|
6
|
+
* agent's native cloud is used. This is what removes the hidden Prix coupling —
|
|
7
|
+
* auto-dispatch no longer depends on the prix/api webhook.
|
|
8
|
+
*/
|
|
9
|
+
import type { Dispatcher } from './auto-dispatch.js';
|
|
10
|
+
export declare function createProviderDispatcher(): Dispatcher;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concrete Dispatcher for auto-dispatch — runs a ticket through agents-cli's own
|
|
3
|
+
* cloud-provider abstraction (`resolveProvider().dispatch()`), the same layer as
|
|
4
|
+
* `agents cloud run`. Rush (Prix) is one provider among rush/codex/factory; a
|
|
5
|
+
* project pins its provider via `provider` in projects.json, else the delegate
|
|
6
|
+
* agent's native cloud is used. This is what removes the hidden Prix coupling —
|
|
7
|
+
* auto-dispatch no longer depends on the prix/api webhook.
|
|
8
|
+
*/
|
|
9
|
+
import { resolveProvider } from './cloud/registry.js';
|
|
10
|
+
export function createProviderDispatcher() {
|
|
11
|
+
return {
|
|
12
|
+
async dispatch({ agent, prompt, repo, provider }) {
|
|
13
|
+
const p = resolveProvider(provider, agent);
|
|
14
|
+
const caps = p.capabilities();
|
|
15
|
+
if (!caps.available) {
|
|
16
|
+
throw new Error(`cloud provider '${p.id}' is not available (auth/binary missing)`);
|
|
17
|
+
}
|
|
18
|
+
if (!caps.dispatch) {
|
|
19
|
+
throw new Error(`cloud provider '${p.id}' does not support dispatch`);
|
|
20
|
+
}
|
|
21
|
+
const task = await p.dispatch({ agent, prompt, repo });
|
|
22
|
+
return { id: task.id };
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-dispatch — pull side of the factory dispatch loop.
|
|
3
|
+
*
|
|
4
|
+
* Polls Linear for issues that are delegated to an agent and still in Todo for a
|
|
5
|
+
* managed project, and — up to a per-project concurrency cap — DISPATCHES each
|
|
6
|
+
* through agents-cli's own cloud-provider abstraction (`resolveProvider().dispatch()`),
|
|
7
|
+
* then marks the ticket Doing so it isn't picked up twice.
|
|
8
|
+
*
|
|
9
|
+
* The dispatch goes through the same provider layer as `agents cloud run`, so Rush
|
|
10
|
+
* (Prix) is just one provider among rush/codex/factory — NOT a hidden requirement.
|
|
11
|
+
* A project may pin its provider via `provider` in ~/.agents/factory/projects.json.
|
|
12
|
+
*
|
|
13
|
+
* OPT-IN: a project auto-dispatches ONLY when `autoDispatch: true` AND `maxAgents > 0`.
|
|
14
|
+
* Nothing global is on by default.
|
|
15
|
+
*/
|
|
16
|
+
/** The subset of a managed project this module needs (mirror of the shared JSON). */
|
|
17
|
+
export interface AutoDispatchProject {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
linearProjectId?: string;
|
|
21
|
+
repoSlug?: string;
|
|
22
|
+
autoDispatch?: boolean;
|
|
23
|
+
maxAgents?: number;
|
|
24
|
+
provider?: string;
|
|
25
|
+
}
|
|
26
|
+
/** A Linear issue that is a candidate for dispatch. */
|
|
27
|
+
export interface DelegatedIssue {
|
|
28
|
+
id: string;
|
|
29
|
+
identifier: string;
|
|
30
|
+
title: string;
|
|
31
|
+
delegateName: string;
|
|
32
|
+
priority: number;
|
|
33
|
+
}
|
|
34
|
+
/** One planned dispatch: which issue, to which agent, in which project. */
|
|
35
|
+
export interface PlannedDispatch {
|
|
36
|
+
projectId: string;
|
|
37
|
+
repoSlug?: string;
|
|
38
|
+
provider?: string;
|
|
39
|
+
issueId: string;
|
|
40
|
+
identifier: string;
|
|
41
|
+
title: string;
|
|
42
|
+
delegateName: string;
|
|
43
|
+
}
|
|
44
|
+
/** Path to the shared factory project registry (written by the factory UI). */
|
|
45
|
+
export declare function factoryProjectsPath(): string;
|
|
46
|
+
/** Read the project registry rows this module cares about. */
|
|
47
|
+
export declare function readAutoDispatchProjects(): AutoDispatchProject[];
|
|
48
|
+
/** A project is eligible only when it has explicitly opted in with a positive cap. */
|
|
49
|
+
export declare function isEligible(p: AutoDispatchProject): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* PURE planner. Given eligible projects, in-flight counts, and delegated-Todo
|
|
52
|
+
* issues per project, decide which issues to dispatch — never exceeding
|
|
53
|
+
* `maxAgents` in-flight. Highest Linear priority first.
|
|
54
|
+
*/
|
|
55
|
+
export declare function planAutoDispatch(projects: AutoDispatchProject[], inFlightByProject: Record<string, number>, delegatedTodoByProject: Record<string, DelegatedIssue[]>): PlannedDispatch[];
|
|
56
|
+
/** Map Linear priority to a sortable rank (urgent first, none last). */
|
|
57
|
+
export declare function priorityRank(priority: number): number;
|
|
58
|
+
/** Build the prompt an auto-dispatched agent receives for a ticket. */
|
|
59
|
+
export declare function dispatchPrompt(identifier: string, title: string): string;
|
|
60
|
+
/** Linear I/O surface — discovery + bookkeeping (NOT the dispatch trigger). */
|
|
61
|
+
export interface LinearGateway {
|
|
62
|
+
/** Count issues in a started ("Doing") state whose delegate is set, per project. */
|
|
63
|
+
countInFlight(linearProjectId: string): Promise<number>;
|
|
64
|
+
/** Fetch delegated issues in an unstarted ("Todo") state for a project. */
|
|
65
|
+
fetchDelegatedTodo(linearProjectId: string): Promise<DelegatedIssue[]>;
|
|
66
|
+
/** After a successful dispatch: move the issue Todo -> Doing so it isn't re-picked. */
|
|
67
|
+
markStarted(issueId: string, delegateName: string): Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
/** Dispatch surface — runs a ticket through agents-cli's cloud/local provider layer. */
|
|
70
|
+
export interface Dispatcher {
|
|
71
|
+
dispatch(opts: {
|
|
72
|
+
agent: string;
|
|
73
|
+
prompt: string;
|
|
74
|
+
repo?: string;
|
|
75
|
+
provider?: string;
|
|
76
|
+
}): Promise<{
|
|
77
|
+
id: string;
|
|
78
|
+
}>;
|
|
79
|
+
}
|
|
80
|
+
export interface AutoDispatchDeps {
|
|
81
|
+
projects: AutoDispatchProject[];
|
|
82
|
+
linear: LinearGateway;
|
|
83
|
+
dispatcher: Dispatcher;
|
|
84
|
+
log?: (level: 'INFO' | 'WARN' | 'ERROR', msg: string) => void;
|
|
85
|
+
}
|
|
86
|
+
/** One tick: read state per eligible project, plan, dispatch, then mark started. */
|
|
87
|
+
export declare function autoDispatchTick(deps: AutoDispatchDeps): Promise<PlannedDispatch[]>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-dispatch — pull side of the factory dispatch loop.
|
|
3
|
+
*
|
|
4
|
+
* Polls Linear for issues that are delegated to an agent and still in Todo for a
|
|
5
|
+
* managed project, and — up to a per-project concurrency cap — DISPATCHES each
|
|
6
|
+
* through agents-cli's own cloud-provider abstraction (`resolveProvider().dispatch()`),
|
|
7
|
+
* then marks the ticket Doing so it isn't picked up twice.
|
|
8
|
+
*
|
|
9
|
+
* The dispatch goes through the same provider layer as `agents cloud run`, so Rush
|
|
10
|
+
* (Prix) is just one provider among rush/codex/factory — NOT a hidden requirement.
|
|
11
|
+
* A project may pin its provider via `provider` in ~/.agents/factory/projects.json.
|
|
12
|
+
*
|
|
13
|
+
* OPT-IN: a project auto-dispatches ONLY when `autoDispatch: true` AND `maxAgents > 0`.
|
|
14
|
+
* Nothing global is on by default.
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import { homedir } from 'os';
|
|
19
|
+
/** Path to the shared factory project registry (written by the factory UI). */
|
|
20
|
+
export function factoryProjectsPath() {
|
|
21
|
+
return path.join(homedir(), '.agents', 'factory', 'projects.json');
|
|
22
|
+
}
|
|
23
|
+
/** Read the project registry rows this module cares about. */
|
|
24
|
+
export function readAutoDispatchProjects() {
|
|
25
|
+
let raw;
|
|
26
|
+
try {
|
|
27
|
+
raw = JSON.parse(fs.readFileSync(factoryProjectsPath(), 'utf-8'));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
if (!Array.isArray(raw))
|
|
33
|
+
return [];
|
|
34
|
+
const out = [];
|
|
35
|
+
for (const r of raw) {
|
|
36
|
+
if (!r || typeof r !== 'object')
|
|
37
|
+
continue;
|
|
38
|
+
const o = r;
|
|
39
|
+
if (typeof o.id !== 'string' || typeof o.name !== 'string')
|
|
40
|
+
continue;
|
|
41
|
+
out.push({
|
|
42
|
+
id: o.id,
|
|
43
|
+
name: o.name,
|
|
44
|
+
linearProjectId: typeof o.linearProjectId === 'string' ? o.linearProjectId : undefined,
|
|
45
|
+
repoSlug: typeof o.repoSlug === 'string' ? o.repoSlug : undefined,
|
|
46
|
+
autoDispatch: o.autoDispatch === true,
|
|
47
|
+
maxAgents: typeof o.maxAgents === 'number' ? o.maxAgents : undefined,
|
|
48
|
+
provider: typeof o.provider === 'string' ? o.provider : undefined,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/** A project is eligible only when it has explicitly opted in with a positive cap. */
|
|
54
|
+
export function isEligible(p) {
|
|
55
|
+
return p.autoDispatch === true && typeof p.maxAgents === 'number' && p.maxAgents > 0 && !!p.linearProjectId;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* PURE planner. Given eligible projects, in-flight counts, and delegated-Todo
|
|
59
|
+
* issues per project, decide which issues to dispatch — never exceeding
|
|
60
|
+
* `maxAgents` in-flight. Highest Linear priority first.
|
|
61
|
+
*/
|
|
62
|
+
export function planAutoDispatch(projects, inFlightByProject, delegatedTodoByProject) {
|
|
63
|
+
const plan = [];
|
|
64
|
+
for (const p of projects) {
|
|
65
|
+
if (!isEligible(p))
|
|
66
|
+
continue;
|
|
67
|
+
const cap = p.maxAgents;
|
|
68
|
+
const slots = cap - (inFlightByProject[p.id] ?? 0);
|
|
69
|
+
if (slots <= 0)
|
|
70
|
+
continue;
|
|
71
|
+
const candidates = (delegatedTodoByProject[p.id] ?? [])
|
|
72
|
+
.slice()
|
|
73
|
+
.sort((a, b) => priorityRank(a.priority) - priorityRank(b.priority));
|
|
74
|
+
for (const issue of candidates.slice(0, slots)) {
|
|
75
|
+
plan.push({
|
|
76
|
+
projectId: p.id,
|
|
77
|
+
repoSlug: p.repoSlug,
|
|
78
|
+
provider: p.provider,
|
|
79
|
+
issueId: issue.id,
|
|
80
|
+
identifier: issue.identifier,
|
|
81
|
+
title: issue.title,
|
|
82
|
+
delegateName: issue.delegateName,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return plan;
|
|
87
|
+
}
|
|
88
|
+
/** Map Linear priority to a sortable rank (urgent first, none last). */
|
|
89
|
+
export function priorityRank(priority) {
|
|
90
|
+
return priority === 0 ? 5 : priority;
|
|
91
|
+
}
|
|
92
|
+
/** Build the prompt an auto-dispatched agent receives for a ticket. */
|
|
93
|
+
export function dispatchPrompt(identifier, title) {
|
|
94
|
+
return `Work on Linear ticket ${identifier}: ${title}. Read the ticket for full context, implement it, and open a PR when done.`;
|
|
95
|
+
}
|
|
96
|
+
/** One tick: read state per eligible project, plan, dispatch, then mark started. */
|
|
97
|
+
export async function autoDispatchTick(deps) {
|
|
98
|
+
const log = deps.log ?? (() => { });
|
|
99
|
+
const eligible = deps.projects.filter(isEligible);
|
|
100
|
+
if (eligible.length === 0)
|
|
101
|
+
return [];
|
|
102
|
+
const inFlight = {};
|
|
103
|
+
const todo = {};
|
|
104
|
+
for (const p of eligible) {
|
|
105
|
+
const pid = p.linearProjectId;
|
|
106
|
+
try {
|
|
107
|
+
inFlight[p.id] = await deps.linear.countInFlight(pid);
|
|
108
|
+
todo[p.id] = await deps.linear.fetchDelegatedTodo(pid);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
log('WARN', `auto-dispatch: failed to read Linear state for '${p.name}': ${err.message}`);
|
|
112
|
+
inFlight[p.id] = Number.MAX_SAFE_INTEGER; // fail closed for this project
|
|
113
|
+
todo[p.id] = [];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const plan = planAutoDispatch(eligible, inFlight, todo);
|
|
117
|
+
const dispatched = [];
|
|
118
|
+
for (const d of plan) {
|
|
119
|
+
try {
|
|
120
|
+
const task = await deps.dispatcher.dispatch({
|
|
121
|
+
agent: d.delegateName.trim().toLowerCase(),
|
|
122
|
+
prompt: dispatchPrompt(d.identifier, d.title),
|
|
123
|
+
repo: d.repoSlug,
|
|
124
|
+
provider: d.provider,
|
|
125
|
+
});
|
|
126
|
+
// Bookkeeping only — dispatch already happened; moving to Doing keeps the
|
|
127
|
+
// ticket out of the next Todo poll. A failure here is non-fatal.
|
|
128
|
+
try {
|
|
129
|
+
await deps.linear.markStarted(d.issueId, d.delegateName);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
log('WARN', `auto-dispatch: dispatched ${d.identifier} but failed to mark Doing: ${err.message}`);
|
|
133
|
+
}
|
|
134
|
+
dispatched.push(d);
|
|
135
|
+
log('INFO', `auto-dispatch: dispatched ${d.identifier} to ${d.delegateName} (task ${task.id})`);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
log('WARN', `auto-dispatch: failed to dispatch ${d.identifier}: ${err.message}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return dispatched;
|
|
142
|
+
}
|
package/dist/lib/browser/cdp.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
// The `ws` client, NOT the platform (undici) WebSocket: undici enforces a
|
|
2
|
+
// non-configurable max decompressed message size, and a CDP response carrying
|
|
3
|
+
// a base64 screenshot of a content-rich page blows past it — the socket dies
|
|
4
|
+
// with code 1006 ("Max decompressed message size exceeded") while the command
|
|
5
|
+
// is pending. `ws` offers no permessage-deflate by default and takes an
|
|
6
|
+
// explicit payload cap. (Reproduced live against a remote Edge over
|
|
7
|
+
// `browser --host`: fresh blank newtab passed, reused content-rich newtab
|
|
8
|
+
// failed on every Page.captureScreenshot.)
|
|
9
|
+
import WSWebSocket from 'ws';
|
|
1
10
|
const pipeTransports = new Map();
|
|
2
11
|
export function registerPipeTransport(transport) {
|
|
3
12
|
const id = `pipe://${process.pid}/${pipeTransports.size + 1}`;
|
|
@@ -26,7 +35,7 @@ export class CDPClient {
|
|
|
26
35
|
return;
|
|
27
36
|
}
|
|
28
37
|
return new Promise((resolve, reject) => {
|
|
29
|
-
this.ws = new
|
|
38
|
+
this.ws = new WSWebSocket(endpoint, { maxPayload: 256 * 1024 * 1024 });
|
|
30
39
|
this.transport = 'websocket';
|
|
31
40
|
this.ws.onopen = () => resolve();
|
|
32
41
|
this.ws.onerror = () => reject(new Error('WebSocket error'));
|
|
@@ -102,7 +111,7 @@ export class CDPClient {
|
|
|
102
111
|
this.transport = null;
|
|
103
112
|
}
|
|
104
113
|
get connected() {
|
|
105
|
-
return ((this.ws !== null && this.ws.readyState ===
|
|
114
|
+
return ((this.ws !== null && this.ws.readyState === WSWebSocket.OPEN) ||
|
|
106
115
|
(this.pipe !== null && !this.pipe.write.destroyed));
|
|
107
116
|
}
|
|
108
117
|
get isOpen() {
|