@phnx-labs/agents-cli 1.22.16 → 1.22.18
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 +33 -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/providers/rush.d.ts +2 -0
- package/dist/lib/channels/providers/rush.js +20 -5
- package/dist/lib/channels/registry.d.ts +2 -0
- package/dist/lib/channels/send.d.ts +4 -3
- package/dist/lib/channels/send.js +15 -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 +8 -7
- package/dist/lib/placement.d.ts +8 -4
- package/dist/lib/placement.js +14 -8
- package/dist/lib/resources.js +124 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/session/active.js +7 -3
- package/dist/lib/settings-manifest.js +7 -1
- package/dist/lib/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -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`). */
|
package/dist/lib/feed-post.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import * as fs from 'fs';
|
|
17
17
|
import * as path from 'path';
|
|
18
18
|
import { spawnSync } from 'child_process';
|
|
19
|
-
import { appendActivityEvent, } from './activity.js';
|
|
19
|
+
import { appendActivityEvent, readRecentActivity, } from './activity.js';
|
|
20
20
|
import { resolveProjectNameForCwd, listProjectDefs } from './projects.js';
|
|
21
21
|
import { getHistoryDir } from './state.js';
|
|
22
22
|
import { machineId } from './machine-id.js';
|
|
@@ -30,7 +30,7 @@ export const STATUS_TITLE_MAX_CHARS = 60;
|
|
|
30
30
|
* Resolve who is posting. Order:
|
|
31
31
|
* 1. Explicit --session flag
|
|
32
32
|
* 2. Env: AGENT_SESSION_ID / AGENTS_SESSION_ID / basename(AGENTS_MAILBOX_DIR)
|
|
33
|
-
* 3. Env AGENT_LAUNCH_ID → match in pid registry
|
|
33
|
+
* 3. Env AGENT_LAUNCH_ID → match in pid registry or activity index
|
|
34
34
|
* 4. Walk parent PIDs from startPid (default process.ppid) through by-pid registry
|
|
35
35
|
*/
|
|
36
36
|
export function resolvePostIdentity(input) {
|
|
@@ -55,8 +55,12 @@ export function resolvePostIdentity(input) {
|
|
|
55
55
|
registry = walkPidRegistry(start, getParent, readEntry);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
const activity = launchId && !envSession && !registry?.sessionId
|
|
59
|
+
? readRecentActivity({ root: input.activityRoot, maxBytesPerSession: 64 * 1024 })
|
|
60
|
+
.find((event) => event.launchId === launchId)
|
|
61
|
+
: undefined;
|
|
58
62
|
// Prefer env session (explicit + managed run), fill gaps from registry.
|
|
59
|
-
const sessionId = envSession ?? registry?.sessionId;
|
|
63
|
+
const sessionId = envSession ?? registry?.sessionId ?? activity?.sessionId;
|
|
60
64
|
if (!sessionId || !isValidMailboxId(sessionId))
|
|
61
65
|
return undefined;
|
|
62
66
|
const mailboxFromEnv = mailboxIdFromEnv(env);
|
|
@@ -66,17 +70,18 @@ export function resolvePostIdentity(input) {
|
|
|
66
70
|
return {
|
|
67
71
|
sessionId,
|
|
68
72
|
mailboxId,
|
|
69
|
-
host: machineIdFromEnv(env),
|
|
70
|
-
runtime: env.AGENTS_RUNTIME?.trim() || 'headless',
|
|
73
|
+
host: activity?.host ?? machineIdFromEnv(env),
|
|
74
|
+
runtime: env.AGENTS_RUNTIME?.trim() || activity?.runtime || 'headless',
|
|
71
75
|
agent: env.AGENTS_AGENT_NAME?.trim()
|
|
72
76
|
|| registry?.agent
|
|
77
|
+
|| activity?.agent
|
|
73
78
|
|| detectAgentKind(env),
|
|
74
79
|
cwd: input.cwd
|
|
75
|
-
?? (env.AGENTS_CWD?.trim() || registry?.cwd || process.cwd()),
|
|
76
|
-
pid: registry?.pid,
|
|
77
|
-
launchId: launchId || registry?.launchId,
|
|
78
|
-
terminalId: env.AGENT_TERMINAL_ID?.trim() || registry?.terminalId,
|
|
79
|
-
tmuxPane: env.TMUX_PANE?.trim() || registry?.tmuxPane,
|
|
80
|
+
?? (env.AGENTS_CWD?.trim() || registry?.cwd || activity?.cwd || process.cwd()),
|
|
81
|
+
pid: registry?.pid ?? activity?.pid,
|
|
82
|
+
launchId: launchId || registry?.launchId || activity?.launchId,
|
|
83
|
+
terminalId: env.AGENT_TERMINAL_ID?.trim() || registry?.terminalId || activity?.terminalId,
|
|
84
|
+
tmuxPane: env.TMUX_PANE?.trim() || registry?.tmuxPane || activity?.tmuxPane,
|
|
80
85
|
};
|
|
81
86
|
}
|
|
82
87
|
function firstValidId(candidates) {
|
|
@@ -118,6 +118,14 @@ export const RUN_OPTION_FORWARDING = {
|
|
|
118
118
|
bare: 'local-only', // skips the local setup-copy push; lease-only concern
|
|
119
119
|
tailscale: 'local-only', // --tailscale/--no-tailscale gate the lease net mode; never forwarded
|
|
120
120
|
copyCreds: 'local-only', // copies creds TO the host before dispatch — local concern only
|
|
121
|
+
// Cloud placement: chosen and dispatched from THIS machine via the provider
|
|
122
|
+
// registry; mutually exclusive with --host (placement conflict dies before
|
|
123
|
+
// dispatch), so these never have a remote argv to ride.
|
|
124
|
+
cloud: 'local-only',
|
|
125
|
+
provider: 'local-only',
|
|
126
|
+
repo: 'local-only',
|
|
127
|
+
branch: 'local-only',
|
|
128
|
+
cloudEnv: 'local-only',
|
|
121
129
|
authCheck: 'local-only', // --no-auth-check gates the local interactive login preflight; --host runs skip that preflight entirely
|
|
122
130
|
// The notification must land on the box the PERSON is at — the one that
|
|
123
131
|
// dispatched — not on a headless worker with no desktop to post to. The local
|
package/dist/lib/humans.d.ts
CHANGED
|
@@ -12,8 +12,9 @@ export declare function readHumans(): HumansConfig | null;
|
|
|
12
12
|
*/
|
|
13
13
|
export declare function writeHumans(config: HumansConfig): void;
|
|
14
14
|
/**
|
|
15
|
-
* Return the effective owner notification
|
|
16
|
-
*
|
|
15
|
+
* Return the effective owner notification destination from humans.yaml.
|
|
16
|
+
* The owner's normal-severity policy selects the channel; when no normal
|
|
17
|
+
* policy is declared, the first addressable channel is the default.
|
|
17
18
|
*/
|
|
18
19
|
export declare function getOwnerNotifyFromHumans(): {
|
|
19
20
|
channel: string;
|
package/dist/lib/humans.js
CHANGED
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
* Schema version: 1
|
|
6
6
|
*
|
|
7
7
|
* This module is the single read/write seam for humans.yaml. All channel/
|
|
8
|
-
* notify consumers should read owner config from here
|
|
9
|
-
* the legacy notify.owner in agents.yaml during the migration window).
|
|
8
|
+
* notify consumers should read owner config from here.
|
|
10
9
|
*/
|
|
11
10
|
import * as fs from 'fs';
|
|
12
11
|
import * as yaml from 'yaml';
|
|
@@ -47,14 +46,23 @@ export function writeHumans(config) {
|
|
|
47
46
|
fs.writeFileSync(filePath, HUMANS_HEADER + body, { encoding: 'utf-8', mode: 0o600 });
|
|
48
47
|
}
|
|
49
48
|
/**
|
|
50
|
-
* Return the effective owner notification
|
|
51
|
-
*
|
|
49
|
+
* Return the effective owner notification destination from humans.yaml.
|
|
50
|
+
* The owner's normal-severity policy selects the channel; when no normal
|
|
51
|
+
* policy is declared, the first addressable channel is the default.
|
|
52
52
|
*/
|
|
53
53
|
export function getOwnerNotifyFromHumans() {
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
const owner = readHumans()?.owner;
|
|
55
|
+
const channels = owner?.channels ?? [];
|
|
56
|
+
const preferredIds = owner?.policy?.normal ?? [];
|
|
57
|
+
const preferred = preferredIds
|
|
58
|
+
.map((id) => channels.find((entry) => entry.id === id))
|
|
59
|
+
.find((entry) => entry?.to);
|
|
60
|
+
const selected = preferred ?? channels.find((entry) => entry.to);
|
|
61
|
+
if (selected?.id && selected.to)
|
|
62
|
+
return { channel: selected.id, to: selected.to };
|
|
63
|
+
const migrated = owner?.notify;
|
|
64
|
+
if (migrated?.channel && migrated.to)
|
|
65
|
+
return migrated;
|
|
58
66
|
return null;
|
|
59
67
|
}
|
|
60
68
|
/**
|
|
Binary file
|
|
Binary file
|
package/dist/lib/notify.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Every human-facing owner notification (feed urgent-block dispatch, monitor
|
|
5
5
|
* `notify` action, `agents notify`) funnels through the single channel seam:
|
|
6
6
|
* `lookupTransport(channel, meta).provider.send(text, opts)`. The recipient comes
|
|
7
|
-
* from `
|
|
7
|
+
* from `humans.yaml` — never a hardcoded chat id — so changing the
|
|
8
8
|
* owner is honoured by every path at once. `notify.transports` picks the actual
|
|
9
9
|
* provider per host (rush telegram on zion, openclaw-telegram on mac-mini).
|
|
10
10
|
* Best-effort: a delivery failure is returned to the caller, never thrown, so a
|
|
@@ -19,9 +19,9 @@ import type { SendResult } from './channels/registry.js';
|
|
|
19
19
|
export interface OwnerNotifyOptions {
|
|
20
20
|
/** Config source (defaults to `readMeta()`); lets callers/tests inject it. */
|
|
21
21
|
meta?: Meta;
|
|
22
|
-
/** Override the owner channel from
|
|
22
|
+
/** Override the owner channel resolved from humans.yaml. */
|
|
23
23
|
channel?: string;
|
|
24
|
-
/** Override the owner target from
|
|
24
|
+
/** Override the owner target resolved from humans.yaml. */
|
|
25
25
|
target?: string;
|
|
26
26
|
/** Resolve + build the delivery but do not actually send. */
|
|
27
27
|
dryRun?: boolean;
|
|
@@ -44,7 +44,7 @@ export declare function buildOpenClawNotifyArgs(text: string, opts: {
|
|
|
44
44
|
}): string[];
|
|
45
45
|
/**
|
|
46
46
|
* Deliver a message to the configured owner through the one channel seam.
|
|
47
|
-
* `channel`/`target` default to
|
|
47
|
+
* `channel`/`target` default to the normal owner channel in humans.yaml; `notify.transports`
|
|
48
48
|
* selects the provider per host. A missing owner config or a delivery failure
|
|
49
49
|
* (e.g. openclaw not on PATH) returns a clean `SendResult` error — never a raw
|
|
50
50
|
* ENOENT — so callers surface a consistent, best-effort failure.
|
package/dist/lib/notify.js
CHANGED
|
@@ -34,17 +34,14 @@ export function buildOpenClawNotifyArgs(text, opts) {
|
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
36
|
* Deliver a message to the configured owner through the one channel seam.
|
|
37
|
-
* `channel`/`target` default to
|
|
37
|
+
* `channel`/`target` default to the normal owner channel in humans.yaml; `notify.transports`
|
|
38
38
|
* selects the provider per host. A missing owner config or a delivery failure
|
|
39
39
|
* (e.g. openclaw not on PATH) returns a clean `SendResult` error — never a raw
|
|
40
40
|
* ENOENT — so callers surface a consistent, best-effort failure.
|
|
41
41
|
*/
|
|
42
42
|
export async function sendToOwner(text, options = {}) {
|
|
43
43
|
const meta = options.meta ?? readMeta();
|
|
44
|
-
|
|
45
|
-
const humansOwner = getOwnerNotifyFromHumans();
|
|
46
|
-
const legacyOwner = meta.notify?.owner;
|
|
47
|
-
const owner = humansOwner ?? legacyOwner;
|
|
44
|
+
const owner = getOwnerNotifyFromHumans() ?? meta.notify?.owner;
|
|
48
45
|
const channel = options.channel ?? owner?.channel;
|
|
49
46
|
const target = options.target ?? owner?.to;
|
|
50
47
|
if (!channel || !target) {
|
|
@@ -52,7 +49,7 @@ export async function sendToOwner(text, options = {}) {
|
|
|
52
49
|
ok: false,
|
|
53
50
|
channel: channel ?? 'unknown',
|
|
54
51
|
id: target ?? '',
|
|
55
|
-
error: '
|
|
52
|
+
error: 'No addressable owner channel configured in humans.yaml or legacy notify.owner',
|
|
56
53
|
};
|
|
57
54
|
}
|
|
58
55
|
registerBuiltinProviders();
|
|
@@ -60,7 +57,11 @@ export async function sendToOwner(text, options = {}) {
|
|
|
60
57
|
if (!provider) {
|
|
61
58
|
return { ok: false, channel, id: target, error };
|
|
62
59
|
}
|
|
63
|
-
return provider.send(text, {
|
|
60
|
+
return provider.send(text, {
|
|
61
|
+
target,
|
|
62
|
+
ownerScoped: options.target === undefined,
|
|
63
|
+
dryRun: options.dryRun,
|
|
64
|
+
});
|
|
64
65
|
}
|
|
65
66
|
export async function notifyUrgentBlock(block, options = {}) {
|
|
66
67
|
if (block.notifiedAt) {
|
package/dist/lib/placement.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Placement — one model for "where does the body run?"
|
|
3
3
|
*
|
|
4
4
|
* The CLI grew several doors that all mean execution target:
|
|
5
|
-
* run --host / --device / --lease / --box
|
|
5
|
+
* run --host / --device / --lease / --box / --cloud
|
|
6
6
|
* routines --placement / --run-on / hostStrategy
|
|
7
7
|
* monitors --run-on (body) vs --device (owner — NOT placement)
|
|
8
8
|
* teams --device (teammate pin)
|
|
@@ -41,6 +41,10 @@ export interface RunPlacementFlags {
|
|
|
41
41
|
computer?: string;
|
|
42
42
|
lease?: string | boolean;
|
|
43
43
|
box?: string;
|
|
44
|
+
/** --cloud: vendor cloud placement (the agent's native cloud provider). */
|
|
45
|
+
cloud?: boolean;
|
|
46
|
+
/** --provider: refines the cloud placement; not a placement on its own. */
|
|
47
|
+
provider?: string;
|
|
44
48
|
}
|
|
45
49
|
export declare class PlacementError extends Error {
|
|
46
50
|
constructor(message: string);
|
|
@@ -68,10 +72,10 @@ export declare function placementFromRunFlags(flags: RunPlacementFlags): Placeme
|
|
|
68
72
|
* Expand a resolved placement into the concrete run option fields the
|
|
69
73
|
* existing dispatch paths already understand. Pure — does not mutate input.
|
|
70
74
|
*
|
|
71
|
-
* `
|
|
72
|
-
*
|
|
75
|
+
* `fleet` is not valid for a bare `agents run` (it is a routines placement);
|
|
76
|
+
* it throws so callers fail loud.
|
|
73
77
|
*/
|
|
74
|
-
export declare function expandPlacementToRunFlags(placement: Placement): Pick<RunPlacementFlags, 'host' | 'device' | 'lease' | 'box'>;
|
|
78
|
+
export declare function expandPlacementToRunFlags(placement: Placement): Pick<RunPlacementFlags, 'host' | 'device' | 'lease' | 'box' | 'cloud' | 'provider'>;
|
|
75
79
|
/** Map routines hostStrategy (+ optional host) onto the shared Placement. */
|
|
76
80
|
export declare function placementFromHostStrategy(strategy: 'local' | 'host' | 'fleet' | 'cloud', host?: string): Placement;
|
|
77
81
|
/** One-line human form for logs / help. */
|
package/dist/lib/placement.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Placement — one model for "where does the body run?"
|
|
3
3
|
*
|
|
4
4
|
* The CLI grew several doors that all mean execution target:
|
|
5
|
-
* run --host / --device / --lease / --box
|
|
5
|
+
* run --host / --device / --lease / --box / --cloud
|
|
6
6
|
* routines --placement / --run-on / hostStrategy
|
|
7
7
|
* monitors --run-on (body) vs --device (owner — NOT placement)
|
|
8
8
|
* teams --device (teammate pin)
|
|
@@ -93,6 +93,7 @@ export function placementFromRunFlags(flags) {
|
|
|
93
93
|
const hostT = hostFamilyTarget(flags);
|
|
94
94
|
const hasLease = flags.lease !== undefined && flags.lease !== false;
|
|
95
95
|
const hasBox = !!flags.box;
|
|
96
|
+
const hasCloud = flags.cloud === true;
|
|
96
97
|
const placementFlags = [];
|
|
97
98
|
if (where)
|
|
98
99
|
placementFlags.push('--where');
|
|
@@ -102,12 +103,16 @@ export function placementFromRunFlags(flags) {
|
|
|
102
103
|
placementFlags.push('--lease');
|
|
103
104
|
if (hasBox)
|
|
104
105
|
placementFlags.push('--box');
|
|
106
|
+
if (hasCloud)
|
|
107
|
+
placementFlags.push('--cloud');
|
|
105
108
|
if (placementFlags.length > 1) {
|
|
106
109
|
throw new PlacementError(`Conflicting placement flags: ${placementFlags.join(' + ')}. ` +
|
|
107
|
-
`Use one door — prefer --where (device:<name> | auto | lease | local).`);
|
|
110
|
+
`Use one door — prefer --where (device:<name> | auto | lease | cloud | local).`);
|
|
108
111
|
}
|
|
109
112
|
if (where)
|
|
110
113
|
return parseWhereSpec(where, '--where');
|
|
114
|
+
if (hasCloud)
|
|
115
|
+
return { kind: 'cloud', target: flags.provider, source: '--cloud' };
|
|
111
116
|
if (hasBox)
|
|
112
117
|
return { kind: 'lease', target: flags.box, source: '--box' };
|
|
113
118
|
if (hasLease) {
|
|
@@ -122,8 +127,8 @@ export function placementFromRunFlags(flags) {
|
|
|
122
127
|
* Expand a resolved placement into the concrete run option fields the
|
|
123
128
|
* existing dispatch paths already understand. Pure — does not mutate input.
|
|
124
129
|
*
|
|
125
|
-
* `
|
|
126
|
-
*
|
|
130
|
+
* `fleet` is not valid for a bare `agents run` (it is a routines placement);
|
|
131
|
+
* it throws so callers fail loud.
|
|
127
132
|
*/
|
|
128
133
|
export function expandPlacementToRunFlags(placement) {
|
|
129
134
|
switch (placement.kind) {
|
|
@@ -140,12 +145,13 @@ export function expandPlacementToRunFlags(placement) {
|
|
|
140
145
|
if (placement.source === '--box')
|
|
141
146
|
return { box: placement.target };
|
|
142
147
|
return placement.target ? { lease: placement.target } : { lease: true };
|
|
148
|
+
case 'cloud':
|
|
149
|
+
// Vendor cloud placement — `--where cloud[:provider]` expands to the
|
|
150
|
+
// --cloud flag (+ --provider refinement) the run action dispatches on.
|
|
151
|
+
return placement.target ? { cloud: true, provider: placement.target } : { cloud: true };
|
|
143
152
|
case 'fleet':
|
|
144
153
|
throw new PlacementError(`fleet placement is for routines (agents routines add … --placement fleet), not agents run. ` +
|
|
145
154
|
`Use --where device:auto for an affinity pick, or --where device:<name>.`);
|
|
146
|
-
case 'cloud':
|
|
147
|
-
throw new PlacementError(`cloud placement is agents cloud run (vendor cloud), not agents run. ` +
|
|
148
|
-
`For a disposable box use --where lease; for your fleet use --where device:<name>.`);
|
|
149
155
|
}
|
|
150
156
|
}
|
|
151
157
|
/** Map routines hostStrategy (+ optional host) onto the shared Placement. */
|
|
@@ -180,9 +186,9 @@ export const PLACEMENT_MATRIX = `
|
|
|
180
186
|
Affinity pick (14d usage) --where auto (= --device auto)
|
|
181
187
|
Disposable cloud box --where lease (= --lease)
|
|
182
188
|
Reuse warm crabbox --box <slug>
|
|
189
|
+
Vendor cloud task --cloud (= --where cloud[:provider])
|
|
183
190
|
Routines: body on one box --run-on <name> / --placement host
|
|
184
191
|
Routines: pick any online --placement fleet
|
|
185
|
-
Vendor cloud task agents cloud run …
|
|
186
192
|
Monitors: who evaluates --device <owner> (NOT body placement)
|
|
187
193
|
Monitors: where action runs --run-on <host>
|
|
188
194
|
`.trim();
|