@phnx-labs/agents-cli 1.22.16 → 1.22.17
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 +12 -0
- package/README.md +10 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/cloud.d.ts +0 -1
- package/dist/commands/cloud.js +19 -185
- package/dist/commands/doctor.js +16 -14
- package/dist/commands/exec.js +57 -5
- package/dist/commands/feed.d.ts +1 -0
- package/dist/commands/feed.js +36 -32
- package/dist/commands/run-cloud.d.ts +26 -0
- package/dist/commands/run-cloud.js +162 -0
- package/dist/commands/versions.js +4 -2
- package/dist/commands/view.js +1 -54
- package/dist/lib/agents.js +4 -2
- package/dist/lib/channels/send.d.ts +3 -3
- package/dist/lib/channels/send.js +13 -18
- package/dist/lib/cloud/dispatch.d.ts +27 -0
- package/dist/lib/cloud/dispatch.js +214 -0
- package/dist/lib/feed-post.d.ts +2 -2
- package/dist/lib/feed-post.js +15 -10
- package/dist/lib/hosts/remote-cmd.js +8 -0
- package/dist/lib/humans.d.ts +3 -2
- package/dist/lib/humans.js +16 -8
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/notify.d.ts +4 -4
- package/dist/lib/notify.js +3 -6
- package/dist/lib/placement.d.ts +8 -4
- package/dist/lib/placement.js +14 -8
- 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/settings-manifest.js +7 -1
- package/dist/lib/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents run <agent> [prompt] --cloud` — the vendor-cloud placement for a run.
|
|
3
|
+
*
|
|
4
|
+
* One of three run placements: local (default), machine (--host/--device,
|
|
5
|
+
* --lease), cloud (--cloud). Routing goes through the cloud provider registry
|
|
6
|
+
* exactly like `agents cloud run --agent <agent>`: the agent's native
|
|
7
|
+
* `cloudProvider` wins, `--provider` overrides. The dispatch itself is the
|
|
8
|
+
* shared core in lib/cloud/dispatch.ts — this module only validates the run
|
|
9
|
+
* surface and translates run flags into a DispatchOptions.
|
|
10
|
+
*/
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import { AGENTS, resolveAgentName, isAgentHardDeprecated, hardDeprecationError } from '../lib/agents.js';
|
|
13
|
+
import { RUN_AUTO_KEYWORD } from '../lib/types.js';
|
|
14
|
+
import { resolveProvider, nativeProviderForAgent } from '../lib/cloud/registry.js';
|
|
15
|
+
import { resolveCloudPrompt, executeCloudDispatch } from '../lib/cloud/dispatch.js';
|
|
16
|
+
/** Error type for run --cloud validation failures (exit 1, red message). */
|
|
17
|
+
export class RunCloudError extends Error {
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'RunCloudError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Run flags that are meaningless on a cloud placement. Each entry maps the
|
|
25
|
+
* option field to the flag the user typed. Cloud tasks run in the provider's
|
|
26
|
+
* workspace on the provider's accounts, so local-run knobs (account strategy,
|
|
27
|
+
* secrets injection, loop guards, resume, terminal handoff, cwd) have nothing
|
|
28
|
+
* to act on — passing one is an error, never a silent ignore.
|
|
29
|
+
*/
|
|
30
|
+
const RUN_CLOUD_CONFLICTS = [
|
|
31
|
+
{ field: 'terminal', flag: '--terminal', set: (v) => v !== undefined && v !== false },
|
|
32
|
+
{ field: 'interactive', flag: '--interactive', set: (v) => v === true },
|
|
33
|
+
{ field: 'acp', flag: '--acp', set: (v) => v === true },
|
|
34
|
+
{ field: 'loop', flag: '--loop', set: (v) => v === true },
|
|
35
|
+
{ field: 'resumeCheckpoint', flag: '--resume-checkpoint', set: (v) => v !== undefined },
|
|
36
|
+
{ field: 'maxIterations', flag: '--max-iterations', set: (v) => v !== undefined },
|
|
37
|
+
{ field: 'budget', flag: '--budget', set: (v) => v !== undefined },
|
|
38
|
+
{ field: 'until', flag: '--until', set: (v) => v !== undefined },
|
|
39
|
+
{ field: 'interval', flag: '--interval', set: (v) => v !== undefined },
|
|
40
|
+
{ field: 'resume', flag: '--resume', set: (v) => v !== undefined && v !== false },
|
|
41
|
+
{ field: 'sessionId', flag: '--session-id', set: (v) => v !== undefined },
|
|
42
|
+
{ field: 'secrets', flag: '--secrets', set: (v) => Array.isArray(v) && v.length > 0 },
|
|
43
|
+
{ field: 'secretsKeys', flag: '--secrets-keys', set: (v) => v !== undefined },
|
|
44
|
+
{ field: 'allowExpired', flag: '--allow-expired', set: (v) => v === true },
|
|
45
|
+
{ field: 'autoSecrets', flag: '--no-auto-secrets', set: (v) => v === false },
|
|
46
|
+
{ field: 'copyCreds', flag: '--copy-creds', set: (v) => v === true },
|
|
47
|
+
{ field: 'fallback', flag: '--fallback', set: (v) => v !== undefined },
|
|
48
|
+
{ field: 'strategy', flag: '--strategy', set: (v) => v !== undefined },
|
|
49
|
+
{ field: 'balanced', flag: '--balanced', set: (v) => v === true },
|
|
50
|
+
{ field: 'cwd', flag: '--cwd', set: (v) => v !== undefined },
|
|
51
|
+
{ field: 'project', flag: '--project', set: (v) => v !== undefined },
|
|
52
|
+
{ field: 'addDir', flag: '--add-dir', set: (v) => Array.isArray(v) && v.length > 0 },
|
|
53
|
+
{ field: 'remoteCwd', flag: '--remote-cwd', set: (v) => v !== undefined },
|
|
54
|
+
{ field: 'env', flag: '--env', set: (v) => Array.isArray(v) && v.length > 0 },
|
|
55
|
+
{ field: 'notify', flag: '--notify', set: (v) => v === true },
|
|
56
|
+
// effort defaults to 'auto' on run; only an explicit non-default value is a
|
|
57
|
+
// conflict (no provider consumes an effort option — it would be dropped).
|
|
58
|
+
{ field: 'effort', flag: '--effort', set: (v) => v !== undefined && v !== 'auto' },
|
|
59
|
+
// --name seeds the LOCAL session label; a cloud task has no local session.
|
|
60
|
+
{ field: 'name', flag: '--name', set: (v) => v !== undefined },
|
|
61
|
+
];
|
|
62
|
+
/** Flags that refine a cloud dispatch; meaningless without the placement. */
|
|
63
|
+
const CLOUD_ONLY_FLAGS = [
|
|
64
|
+
{ field: 'provider', flag: '--provider', set: (v) => v !== undefined },
|
|
65
|
+
{ field: 'repo', flag: '--repo', set: (v) => Array.isArray(v) && v.length > 0 },
|
|
66
|
+
{ field: 'branch', flag: '--branch', set: (v) => v !== undefined },
|
|
67
|
+
{ field: 'cloudEnv', flag: '--cloud-env', set: (v) => v !== undefined },
|
|
68
|
+
];
|
|
69
|
+
/** Flags the user passed that cannot ride a cloud placement. */
|
|
70
|
+
export function runCloudConflicts(options) {
|
|
71
|
+
return RUN_CLOUD_CONFLICTS.filter((c) => c.set(options[c.field])).map((c) => c.flag);
|
|
72
|
+
}
|
|
73
|
+
/** Cloud-only flags passed without the --cloud placement. */
|
|
74
|
+
export function cloudFlagsWithoutCloud(options) {
|
|
75
|
+
return CLOUD_ONLY_FLAGS.filter((c) => c.set(options[c.field])).map((c) => c.flag);
|
|
76
|
+
}
|
|
77
|
+
/** Agents that route to a native cloud provider (the registry's truth). */
|
|
78
|
+
export function cloudCapableAgentIds() {
|
|
79
|
+
return Object.values(AGENTS)
|
|
80
|
+
.filter((a) => a.cloudProvider)
|
|
81
|
+
.map((a) => a.id)
|
|
82
|
+
.sort();
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Resolve the cloud provider for a run: explicit --provider wins, else the
|
|
86
|
+
* agent's native cloud. An agent with no native cloud fails loud with the
|
|
87
|
+
* capable list — never a silent ride onto the configured default.
|
|
88
|
+
*/
|
|
89
|
+
export function resolveRunCloudProvider(agentId, explicitProvider) {
|
|
90
|
+
if (explicitProvider)
|
|
91
|
+
return resolveProvider(explicitProvider);
|
|
92
|
+
if (!nativeProviderForAgent(agentId)) {
|
|
93
|
+
throw new RunCloudError(`${agentId} has no native cloud. Cloud-capable agents: ${cloudCapableAgentIds().join(', ')}. ` +
|
|
94
|
+
`Override with --provider <id> (rush | codex | factory | antigravity | host).`);
|
|
95
|
+
}
|
|
96
|
+
return resolveProvider(undefined, agentId);
|
|
97
|
+
}
|
|
98
|
+
/** Validate the agent half of `agents run <agent> --cloud` and return the registry id. */
|
|
99
|
+
export function resolveRunCloudAgent(agentSpec) {
|
|
100
|
+
if (agentSpec === RUN_AUTO_KEYWORD || agentSpec.startsWith(`${RUN_AUTO_KEYWORD}@`)) {
|
|
101
|
+
throw new RunCloudError(`agents run auto --cloud: auto harness-pick is a local-run feature. ` +
|
|
102
|
+
`Name a cloud-capable agent: ${cloudCapableAgentIds().join(', ')}.`);
|
|
103
|
+
}
|
|
104
|
+
if (agentSpec.includes('@')) {
|
|
105
|
+
throw new RunCloudError(`Version pins (<agent>@<version>) do not apply to --cloud — the provider runs its own agent version. ` +
|
|
106
|
+
`Drop the pin: agents run ${agentSpec.split('@')[0]} "<task>" --cloud.`);
|
|
107
|
+
}
|
|
108
|
+
const agentId = resolveAgentName(agentSpec);
|
|
109
|
+
if (!agentId) {
|
|
110
|
+
throw new RunCloudError(`Unknown agent: ${agentSpec}. Cloud-capable agents: ${cloudCapableAgentIds().join(', ')}.`);
|
|
111
|
+
}
|
|
112
|
+
if (isAgentHardDeprecated(agentId)) {
|
|
113
|
+
throw new RunCloudError(hardDeprecationError(agentId));
|
|
114
|
+
}
|
|
115
|
+
return agentId;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Handle `agents run <agent> [prompt] --cloud`. Validation lives in the
|
|
119
|
+
* exported helpers above (unit-tested); this wires them to the shared
|
|
120
|
+
* dispatch core and dies on any validation failure.
|
|
121
|
+
*/
|
|
122
|
+
export async function handleRunCloud(agentSpec, prompt, options, command) {
|
|
123
|
+
const json = options.json === true;
|
|
124
|
+
try {
|
|
125
|
+
const agentId = resolveRunCloudAgent(agentSpec);
|
|
126
|
+
const provider = resolveRunCloudProvider(agentId, options.provider);
|
|
127
|
+
const resolvedPrompt = resolveCloudPrompt(prompt, {
|
|
128
|
+
json,
|
|
129
|
+
hint: `agents run ${agentId} "<task>" --cloud${agentId === 'claude' ? ' --repo <owner/repo>' : ''}`,
|
|
130
|
+
});
|
|
131
|
+
const repoValues = Array.isArray(options.repo) ? options.repo : [];
|
|
132
|
+
const dispatchOptions = {
|
|
133
|
+
prompt: resolvedPrompt,
|
|
134
|
+
agent: agentId,
|
|
135
|
+
repo: repoValues[0],
|
|
136
|
+
repos: repoValues.length > 0 ? repoValues : undefined,
|
|
137
|
+
branch: options.branch,
|
|
138
|
+
timeout: options.timeout,
|
|
139
|
+
model: options.model,
|
|
140
|
+
providerOptions: {},
|
|
141
|
+
};
|
|
142
|
+
if (options.cloudEnv)
|
|
143
|
+
dispatchOptions.providerOptions.env = options.cloudEnv;
|
|
144
|
+
// --mode defaults to 'plan' on run; forward it only when the user set it.
|
|
145
|
+
if (command.getOptionValueSource('mode') === 'cli') {
|
|
146
|
+
dispatchOptions.providerOptions.mode = options.mode;
|
|
147
|
+
}
|
|
148
|
+
await executeCloudDispatch({
|
|
149
|
+
provider,
|
|
150
|
+
dispatchOptions,
|
|
151
|
+
follow: options.follow !== false,
|
|
152
|
+
json,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
if (err instanceof RunCloudError) {
|
|
157
|
+
console.error(chalk.red(err.message));
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -438,8 +438,10 @@ export function registerVersionsCommands(program) {
|
|
|
438
438
|
console.log(chalk.gray(` Created shim: ${getShimsDir()}/${agentConfig.cliCommand}`));
|
|
439
439
|
}
|
|
440
440
|
// Seed the fresh version home with user settings from the current
|
|
441
|
-
// default version (settings.json, keybindings, codex config
|
|
442
|
-
//
|
|
441
|
+
// default version (settings.json, keybindings, codex config).
|
|
442
|
+
// Credentials are deliberately excluded so each version keeps its own
|
|
443
|
+
// login (see SETTINGS_MANIFEST). Gap-filling only — never overwrites
|
|
444
|
+
// what the new home has.
|
|
443
445
|
const carrySource = getGlobalDefault(agent);
|
|
444
446
|
if (carrySource && carrySource !== installedVersion) {
|
|
445
447
|
const carried = carryForwardSettings(agent, getVersionHomePath(agent, carrySource), getVersionHomePath(agent, installedVersion));
|
package/dist/commands/view.js
CHANGED
|
@@ -10,8 +10,7 @@ import { machineId } from '../lib/machine-id.js';
|
|
|
10
10
|
import { authCacheKey, formatCheckedAge, readAuthHealthCache } from '../lib/auth-health.js';
|
|
11
11
|
import { agentReportsUsage, deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
|
|
12
12
|
import { readManifest } from '../lib/manifest.js';
|
|
13
|
-
import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir,
|
|
14
|
-
import { formatKeptProjectResources } from '../lib/project-resources.js';
|
|
13
|
+
import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, removeVersion, printTrashFooter, reconcileStaleLatestForAgent, isGlobalBinaryAgent, getLiveVersion, isVersionIsolated, getIsolatedDefault, } from '../lib/versions.js';
|
|
15
14
|
import { ensureVersionedAliasCurrent, removeShim, } from '../lib/shims.js';
|
|
16
15
|
import { getAgentResources } from '../lib/resources.js';
|
|
17
16
|
import { renderMergedResources } from '../lib/merged-resources.js';
|
|
@@ -29,7 +28,6 @@ import { resolveConfiguredModel } from '../lib/models.js';
|
|
|
29
28
|
import { listProfiles, profileExists, profileSummary, readProfile } from '../lib/profiles.js';
|
|
30
29
|
import { getByokUsageForHarness, hasByokProvider, renderByokBar } from '../lib/byok-usage.js';
|
|
31
30
|
import { renderHarnessDetail } from './harness.js';
|
|
32
|
-
import { loadManifest, isStale } from '../lib/staleness/index.js';
|
|
33
31
|
import { confirm } from '@inquirer/prompts';
|
|
34
32
|
import { formatPath, isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
35
33
|
import { terminalWidth, truncateToWidth, stringWidth, padToWidth } from '../lib/session/width.js';
|
|
@@ -781,57 +779,6 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
781
779
|
if (!filterAgentId) {
|
|
782
780
|
renderHostClisSection(process.cwd());
|
|
783
781
|
}
|
|
784
|
-
// Check for new resources when viewing a specific agent
|
|
785
|
-
if (filterAgentId && versionManaged.length > 0) {
|
|
786
|
-
const defaultVersion = getGlobalDefault(filterAgentId);
|
|
787
|
-
if (defaultVersion) {
|
|
788
|
-
const manifest = loadManifest(filterAgentId, defaultVersion);
|
|
789
|
-
const cwd = process.cwd();
|
|
790
|
-
if (manifest && !isStale(manifest, filterAgentId, defaultVersion, cwd)) {
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
const available = getAvailableResources();
|
|
794
|
-
const synced = getActuallySyncedResources(filterAgentId, defaultVersion);
|
|
795
|
-
const projectOnly = getProjectOnlyResources();
|
|
796
|
-
const newResources = getNewResources(available, synced, projectOnly);
|
|
797
|
-
if (hasNewResources(newResources, filterAgentId, defaultVersion)) {
|
|
798
|
-
try {
|
|
799
|
-
const selection = await promptNewResourceSelection(filterAgentId, newResources, defaultVersion);
|
|
800
|
-
if (selection && Object.keys(selection).length > 0) {
|
|
801
|
-
const result = syncResourcesToVersion(filterAgentId, defaultVersion, selection);
|
|
802
|
-
const synced = [];
|
|
803
|
-
if (result.commands)
|
|
804
|
-
synced.push('commands');
|
|
805
|
-
if (result.skills)
|
|
806
|
-
synced.push('skills');
|
|
807
|
-
if (result.hooks)
|
|
808
|
-
synced.push('hooks');
|
|
809
|
-
if (result.memory.length > 0)
|
|
810
|
-
synced.push('memory');
|
|
811
|
-
if (result.permissions)
|
|
812
|
-
synced.push('permissions');
|
|
813
|
-
if (result.mcp.length > 0)
|
|
814
|
-
synced.push('mcp');
|
|
815
|
-
if (result.plugins.length > 0)
|
|
816
|
-
synced.push('plugins');
|
|
817
|
-
if (result.workflows.length > 0)
|
|
818
|
-
synced.push('workflows');
|
|
819
|
-
if (synced.length > 0) {
|
|
820
|
-
console.log(chalk.green(`\nSynced to ${agentLabel(filterAgentId)}@${defaultVersion}: ${synced.join(', ')}`));
|
|
821
|
-
}
|
|
822
|
-
const kept = formatKeptProjectResources(result.projectSkipped);
|
|
823
|
-
if (kept)
|
|
824
|
-
console.log(chalk.gray(kept));
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
catch (err) {
|
|
828
|
-
if (isPromptCancelled(err))
|
|
829
|
-
return;
|
|
830
|
-
throw err;
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
782
|
}
|
|
836
783
|
/**
|
|
837
784
|
* Show detailed resources for a specific agent version.
|
package/dist/lib/agents.js
CHANGED
|
@@ -212,8 +212,10 @@ export const AGENTS = {
|
|
|
212
212
|
format: 'markdown',
|
|
213
213
|
variableSyntax: '$ARGUMENTS',
|
|
214
214
|
supportsHooks: true,
|
|
215
|
-
// Claude Code
|
|
216
|
-
//
|
|
215
|
+
// Claude Code grew a native `claude --cloud "<prompt>"` (Anthropic-managed
|
|
216
|
+
// infra, claude.ai/code; requires claude.ai subscription auth). Routing
|
|
217
|
+
// still goes to Rush Cloud deliberately — it keeps cloud tasks in one
|
|
218
|
+
// tracked fleet (agents cloud list/status/logs) regardless of harness.
|
|
217
219
|
cloudProvider: 'rush',
|
|
218
220
|
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, memory: true, modes: ['plan', 'edit', 'auto', 'skip'], rulesImports: true, interactiveRepl: true },
|
|
219
221
|
},
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* One primitive: resolve a destination (channel + target), compose text + urls +
|
|
5
5
|
* attachments, hand off to a channel provider. `notify` is the same path with
|
|
6
|
-
* destination defaulted
|
|
6
|
+
* destination defaulted from `humans.yaml` — owner is an address
|
|
7
7
|
* alias (`--to owner`), not a separate stack.
|
|
8
8
|
*
|
|
9
9
|
* Agent control (`agents message`, `sessions inject`) stays outside this module.
|
|
@@ -60,8 +60,8 @@ export declare function isOwnerAlias(to: string | undefined): boolean;
|
|
|
60
60
|
/** Compose body + optional URL lines (skip urls already present in the body). */
|
|
61
61
|
export declare function composeSendText(text: string, urls?: string[]): string;
|
|
62
62
|
/**
|
|
63
|
-
* Read the owner destination
|
|
64
|
-
*
|
|
63
|
+
* Read the owner destination. humans.yaml is canonical; notify.owner remains a
|
|
64
|
+
* migration fallback for installations that have not run the schema migration.
|
|
65
65
|
*/
|
|
66
66
|
export declare function readOwnerDest(meta: Meta): {
|
|
67
67
|
channel: string;
|
|
@@ -18,19 +18,16 @@ export function composeSendText(text, urls) {
|
|
|
18
18
|
return body ? `${body}\n${extra.join('\n')}` : extra.join('\n');
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
-
* Read the owner destination
|
|
22
|
-
*
|
|
21
|
+
* Read the owner destination. humans.yaml is canonical; notify.owner remains a
|
|
22
|
+
* migration fallback for installations that have not run the schema migration.
|
|
23
23
|
*/
|
|
24
24
|
export function readOwnerDest(meta) {
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
27
|
-
return
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
if (!channel || !to)
|
|
32
|
-
return null;
|
|
33
|
-
return { channel, to };
|
|
25
|
+
const canonical = getOwnerNotifyFromHumans();
|
|
26
|
+
if (canonical)
|
|
27
|
+
return canonical;
|
|
28
|
+
const channel = meta.notify?.owner?.channel?.trim();
|
|
29
|
+
const to = meta.notify?.owner?.to?.trim();
|
|
30
|
+
return channel && to ? { channel, to } : null;
|
|
34
31
|
}
|
|
35
32
|
/**
|
|
36
33
|
* Resolve CLI/config into a send envelope. Pure except for reading `meta` —
|
|
@@ -55,16 +52,14 @@ export function resolveSendEnvelope(input, meta) {
|
|
|
55
52
|
};
|
|
56
53
|
}
|
|
57
54
|
// Owner defaults fill only missing fields (and expand the bare "owner" alias).
|
|
58
|
-
// humans.yaml is the
|
|
59
|
-
// fallback for the migration window. Explicit --channel/--to always win.
|
|
55
|
+
// humans.yaml is the canonical source. Explicit --channel/--to always win.
|
|
60
56
|
let channel = (input.channel ?? '').trim();
|
|
61
57
|
let to = (input.to ?? '').trim();
|
|
62
58
|
const usedOwnerAlias = isOwnerAlias(to);
|
|
63
59
|
if (input.ownerMode || usedOwnerAlias) {
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const ownerTo = humansOwner?.to ?? fallbackOwner?.to?.trim() ?? '';
|
|
60
|
+
const owner = getOwnerNotifyFromHumans() ?? meta.notify?.owner;
|
|
61
|
+
const ownerChannel = owner?.channel ?? '';
|
|
62
|
+
const ownerTo = owner?.to ?? '';
|
|
68
63
|
if (!channel)
|
|
69
64
|
channel = ownerChannel;
|
|
70
65
|
if (!to || usedOwnerAlias)
|
|
@@ -72,7 +67,7 @@ export function resolveSendEnvelope(input, meta) {
|
|
|
72
67
|
}
|
|
73
68
|
if (!channel || !to) {
|
|
74
69
|
const hint = input.ownerMode || usedOwnerAlias
|
|
75
|
-
? 'Set
|
|
70
|
+
? 'Set owner.channels and owner.policy.normal in humans.yaml, or pass --channel and --to explicitly.'
|
|
76
71
|
: 'Need --channel and --to (or --to owner with notify.owner configured). ' +
|
|
77
72
|
'Example: agents send --channel desktop --to local --text "hi"';
|
|
78
73
|
return { ok: false, error: hint };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CloudProvider, DispatchOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the prompt for a cloud dispatch: the raw value, or the contents of
|
|
4
|
+
* the file it points at (with a dim note on TTY). Dies when empty.
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolveCloudPrompt(raw: string | undefined, opts: {
|
|
7
|
+
json: boolean;
|
|
8
|
+
hint: string;
|
|
9
|
+
}): string;
|
|
10
|
+
export interface ExecuteCloudDispatchParams {
|
|
11
|
+
provider: CloudProvider;
|
|
12
|
+
dispatchOptions: DispatchOptions;
|
|
13
|
+
/** Image file paths for vision dispatch (checked against provider capability). */
|
|
14
|
+
imagePaths?: string[];
|
|
15
|
+
/** Raw skill refs (`id` or `id@version`) for ride-along skills. */
|
|
16
|
+
skillIds?: string[];
|
|
17
|
+
/** Stream the task output after dispatch; false = fire-and-forget. */
|
|
18
|
+
follow: boolean;
|
|
19
|
+
json: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Dispatch a cloud task and (unless follow=false) stream it to completion.
|
|
23
|
+
* Owns: share-env injection, capability checks, the dispatch spinner +
|
|
24
|
+
* missing-target picker, local persistence, event emission, and the budget
|
|
25
|
+
* kill-switch. Dies on any dispatch failure — callers never see a partial.
|
|
26
|
+
*/
|
|
27
|
+
export declare function executeCloudDispatch(params: ExecuteCloudDispatchParams): Promise<void>;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared cloud dispatch core — the ONE path every cloud dispatch goes through.
|
|
3
|
+
*
|
|
4
|
+
* Both `agents cloud run` (commands/cloud.ts) and `agents run <agent> --cloud`
|
|
5
|
+
* (commands/run-cloud.ts) build a DispatchOptions + resolve a provider, then
|
|
6
|
+
* call executeCloudDispatch here. Behavior — capability checks, the missing-
|
|
7
|
+
* target picker, local persistence, event emission, streaming, and the live
|
|
8
|
+
* budget kill-switch — must not diverge between the two surfaces.
|
|
9
|
+
*/
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import ora from 'ora';
|
|
14
|
+
import { die } from '../format.js';
|
|
15
|
+
import { insertTask, updateTaskStatus } from './store.js';
|
|
16
|
+
import { renderStream } from './stream.js';
|
|
17
|
+
import { MissingTargetError, MAX_IMAGES_PER_DISPATCH } from './types.js';
|
|
18
|
+
import { emit } from '../events.js';
|
|
19
|
+
import { shareRuntimeEnv } from '../share/config.js';
|
|
20
|
+
/** Map a supported image file extension to its wire mimeType. Rejects anything else. */
|
|
21
|
+
function imageMimeFromPath(file) {
|
|
22
|
+
const ext = path.extname(file).toLowerCase();
|
|
23
|
+
if (ext === '.png')
|
|
24
|
+
return 'image/png';
|
|
25
|
+
if (ext === '.jpg' || ext === '.jpeg')
|
|
26
|
+
return 'image/jpeg';
|
|
27
|
+
if (ext === '.webp')
|
|
28
|
+
return 'image/webp';
|
|
29
|
+
die(`Unsupported image type ${JSON.stringify(ext || file)}. Use .png, .jpg/.jpeg, or .webp.`);
|
|
30
|
+
}
|
|
31
|
+
/** Read one image file into a base64 ImageAttachment, dying with a clear error if it's missing. */
|
|
32
|
+
function readImageAttachment(file) {
|
|
33
|
+
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
|
|
34
|
+
die(`Image not found: ${file}`);
|
|
35
|
+
}
|
|
36
|
+
const mimeType = imageMimeFromPath(file);
|
|
37
|
+
return { data: fs.readFileSync(file).toString('base64'), mimeType };
|
|
38
|
+
}
|
|
39
|
+
/** Parse a `--skill <id>` value (`id` or `id@version`) into a SkillRef. */
|
|
40
|
+
function parseSkillRef(raw) {
|
|
41
|
+
const at = raw.lastIndexOf('@');
|
|
42
|
+
if (at > 0) {
|
|
43
|
+
return { id: raw.slice(0, at), version: raw.slice(at + 1) };
|
|
44
|
+
}
|
|
45
|
+
return { id: raw };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the prompt for a cloud dispatch: the raw value, or the contents of
|
|
49
|
+
* the file it points at (with a dim note on TTY). Dies when empty.
|
|
50
|
+
*/
|
|
51
|
+
export function resolveCloudPrompt(raw, opts) {
|
|
52
|
+
let prompt = raw;
|
|
53
|
+
if (!prompt)
|
|
54
|
+
die('Prompt is required. Pass it as an argument or with --prompt.', 1, { json: opts.json, hint: opts.hint });
|
|
55
|
+
// If prompt is a file path, read it and tell the user
|
|
56
|
+
if (fs.existsSync(prompt) && fs.statSync(prompt).isFile()) {
|
|
57
|
+
const filePath = prompt;
|
|
58
|
+
const stat = fs.statSync(filePath);
|
|
59
|
+
const sizeKB = (stat.size / 1024).toFixed(1);
|
|
60
|
+
prompt = fs.readFileSync(filePath, 'utf-8').trim();
|
|
61
|
+
if (process.stderr.isTTY) {
|
|
62
|
+
process.stderr.write(chalk.dim(`Reading prompt from ${filePath} (${sizeKB} KB)\n`));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return prompt;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* After a `MissingTargetError`, try to resolve the target interactively.
|
|
69
|
+
* Returns the chosen id, or undefined when no interactive resolution is
|
|
70
|
+
* possible (non-TTY/JSON, provider can't enumerate, or user cancels) — the
|
|
71
|
+
* caller then prints the error's guidance.
|
|
72
|
+
*
|
|
73
|
+
* Codex has no `listTargets` (no list-environments CLI), so it always returns
|
|
74
|
+
* undefined here and the user sees the `codex cloud` guidance. Factory lists
|
|
75
|
+
* Droid Computers; if listing fails (not signed in) or parses to nothing, we
|
|
76
|
+
* fall back to a free-text prompt so a dispatch is never hard-blocked.
|
|
77
|
+
*/
|
|
78
|
+
async function pickMissingTarget(provider, err, json) {
|
|
79
|
+
if (json || !process.stdout.isTTY)
|
|
80
|
+
return undefined;
|
|
81
|
+
if (!provider.listTargets)
|
|
82
|
+
return undefined;
|
|
83
|
+
const { select, input } = await import('@inquirer/prompts');
|
|
84
|
+
const promptName = err.kind === 'env' ? 'environment' : 'computer';
|
|
85
|
+
let targets;
|
|
86
|
+
try {
|
|
87
|
+
targets = await provider.listTargets();
|
|
88
|
+
}
|
|
89
|
+
catch (listErr) {
|
|
90
|
+
process.stderr.write(chalk.dim(`Could not list ${promptName}s: ${listErr.message}\n`));
|
|
91
|
+
targets = [];
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
if (targets.length > 0) {
|
|
95
|
+
return await select({
|
|
96
|
+
message: `Select a ${promptName}`,
|
|
97
|
+
choices: targets.map((t) => ({ value: t.id, name: t.label ? `${t.id} ${chalk.dim(t.label)}` : t.id })),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const typed = (await input({ message: `No ${promptName}s found. Enter a ${promptName} name (blank to cancel):` })).trim();
|
|
101
|
+
return typed || undefined;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// User hit Ctrl-C / Esc on the prompt.
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Dispatch a cloud task and (unless follow=false) stream it to completion.
|
|
110
|
+
* Owns: share-env injection, capability checks, the dispatch spinner +
|
|
111
|
+
* missing-target picker, local persistence, event emission, and the budget
|
|
112
|
+
* kill-switch. Dies on any dispatch failure — callers never see a partial.
|
|
113
|
+
*/
|
|
114
|
+
export async function executeCloudDispatch(params) {
|
|
115
|
+
const { provider, dispatchOptions, follow, json } = params;
|
|
116
|
+
const imagePaths = params.imagePaths ?? [];
|
|
117
|
+
const skillIds = params.skillIds ?? [];
|
|
118
|
+
const shareEnv = shareRuntimeEnv();
|
|
119
|
+
if (shareEnv)
|
|
120
|
+
dispatchOptions.env = shareEnv;
|
|
121
|
+
// Vision attachments + ride-along skills. Only wire them when the resolved
|
|
122
|
+
// provider advertises support — otherwise fail loud rather than silently
|
|
123
|
+
// drop the flags the user passed.
|
|
124
|
+
const caps = provider.capabilities();
|
|
125
|
+
if (imagePaths.length > 0) {
|
|
126
|
+
if (!caps.images)
|
|
127
|
+
die(`${provider.name} does not support image attachments.`, 1, { json });
|
|
128
|
+
if (imagePaths.length > MAX_IMAGES_PER_DISPATCH) {
|
|
129
|
+
die(`Too many images: ${imagePaths.length}. Max is ${MAX_IMAGES_PER_DISPATCH} per dispatch.`, 1, { json });
|
|
130
|
+
}
|
|
131
|
+
dispatchOptions.images = imagePaths.map(readImageAttachment);
|
|
132
|
+
}
|
|
133
|
+
if (skillIds.length > 0) {
|
|
134
|
+
if (!caps.skills)
|
|
135
|
+
die(`${provider.name} does not support ride-along skills.`, 1, { json });
|
|
136
|
+
dispatchOptions.skills = skillIds.map(parseSkillRef);
|
|
137
|
+
}
|
|
138
|
+
// Dispatch. On a missing pre-provisioned target (Codex env / Factory
|
|
139
|
+
// computer), offer an interactive picker instead of a raw error.
|
|
140
|
+
const dispatchOnce = async () => {
|
|
141
|
+
const spinner = ora({ text: `Dispatching to ${provider.name}...`, stream: process.stderr }).start();
|
|
142
|
+
try {
|
|
143
|
+
const t = await provider.dispatch(dispatchOptions);
|
|
144
|
+
spinner.succeed(`Task ${t.id} dispatched to ${provider.name}`);
|
|
145
|
+
return t;
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
spinner.fail('Dispatch failed');
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
let task;
|
|
153
|
+
try {
|
|
154
|
+
task = await dispatchOnce();
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
if (err instanceof MissingTargetError) {
|
|
158
|
+
const picked = await pickMissingTarget(provider, err, json);
|
|
159
|
+
if (!picked) {
|
|
160
|
+
die(err.guidance ? `${err.message}\n\n${err.guidance}` : err.message, 1, { json });
|
|
161
|
+
}
|
|
162
|
+
dispatchOptions.providerOptions[err.kind] = picked;
|
|
163
|
+
try {
|
|
164
|
+
task = await dispatchOnce();
|
|
165
|
+
}
|
|
166
|
+
catch (err2) {
|
|
167
|
+
die(err2.message, 1, { json });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
die(err.message, 1, { json });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Persist locally
|
|
175
|
+
insertTask(task);
|
|
176
|
+
emit('cloud.dispatch', { module: 'cloud', taskId: task.id, agent: task.agent, provider: task.provider, status: task.status });
|
|
177
|
+
if (json) {
|
|
178
|
+
process.stdout.write(JSON.stringify(task) + '\n');
|
|
179
|
+
}
|
|
180
|
+
// Stream output unless --no-follow
|
|
181
|
+
if (!follow)
|
|
182
|
+
return;
|
|
183
|
+
try {
|
|
184
|
+
// Live budget kill-switch (issue #399). Reuses makeLiveSpendWatcher to
|
|
185
|
+
// feed the provider's `usage` events into a shared watcher; on a cap
|
|
186
|
+
// breach we call provider.cancel(task.id) mid-stream. Dormant (returns
|
|
187
|
+
// null) when no caps are configured, so the raw stream flows unchanged.
|
|
188
|
+
const { wrapStreamWithBudgetGate } = await import('../budget/live-cloud.js');
|
|
189
|
+
const gated = wrapStreamWithBudgetGate({
|
|
190
|
+
provider,
|
|
191
|
+
taskId: task.id,
|
|
192
|
+
project: task.repo ?? task.repos?.[0] ?? process.cwd(),
|
|
193
|
+
agent: task.agent ?? 'cloud',
|
|
194
|
+
cwd: process.cwd(),
|
|
195
|
+
});
|
|
196
|
+
const eventSource = gated ? gated.wrap(provider.stream(task.id)) : provider.stream(task.id);
|
|
197
|
+
const result = await renderStream(eventSource, { json });
|
|
198
|
+
updateTaskStatus(task.id, result.status, {
|
|
199
|
+
summary: result.summary,
|
|
200
|
+
prUrl: result.prUrl,
|
|
201
|
+
});
|
|
202
|
+
emit('cloud.complete', { module: 'cloud', taskId: task.id, status: result.status, prUrl: result.prUrl });
|
|
203
|
+
if (gated?.gate.breached()) {
|
|
204
|
+
const b = gated.gate.breach();
|
|
205
|
+
process.stderr.write(`[budget] cap ${b?.cap} exceeded — cancelled cloud task ${task.id}\n`);
|
|
206
|
+
process.exitCode = 7; // Mirrors BUDGET_KILL_EXIT_CODE for CI/headless.
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
// Stream disconnect is OK — task keeps running
|
|
211
|
+
process.stderr.write(chalk.dim(`\nStream disconnected. Task ${task.id} continues running.\n`));
|
|
212
|
+
process.stderr.write(chalk.dim(`Check status: agents cloud status ${task.id}\n`));
|
|
213
|
+
}
|
|
214
|
+
}
|
package/dist/lib/feed-post.d.ts
CHANGED
|
@@ -72,10 +72,10 @@ export interface PostIdentity {
|
|
|
72
72
|
* Resolve who is posting. Order:
|
|
73
73
|
* 1. Explicit --session flag
|
|
74
74
|
* 2. Env: AGENT_SESSION_ID / AGENTS_SESSION_ID / basename(AGENTS_MAILBOX_DIR)
|
|
75
|
-
* 3. Env AGENT_LAUNCH_ID → match in pid registry
|
|
75
|
+
* 3. Env AGENT_LAUNCH_ID → match in pid registry or activity index
|
|
76
76
|
* 4. Walk parent PIDs from startPid (default process.ppid) through by-pid registry
|
|
77
77
|
*/
|
|
78
|
-
export declare function resolvePostIdentity(input: Pick<FeedPostInput, 'sessionId' | 'env' | 'cwd' | 'startPid' | 'getParentPid' | 'readEntry' | 'listEntries'>): PostIdentity | undefined;
|
|
78
|
+
export declare function resolvePostIdentity(input: Pick<FeedPostInput, 'sessionId' | 'env' | 'cwd' | 'activityRoot' | 'startPid' | 'getParentPid' | 'readEntry' | 'listEntries'>): PostIdentity | undefined;
|
|
79
79
|
/** Walk up to 16 ancestors looking for a by-pid registry entry with a session. */
|
|
80
80
|
export declare function walkPidRegistry(startPid: number, getParent: (pid: number) => number | undefined, readEntry: (pid: number) => PidSessionEntry | undefined): PidSessionEntry | undefined;
|
|
81
81
|
/** Best-effort parent pid of `pid` (Linux /proc, else `ps`). */
|