@phnx-labs/agents-cli 1.20.74 → 1.20.77
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 +1055 -0
- package/README.md +36 -10
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.js +21 -3
- package/dist/commands/attach.d.ts +10 -0
- package/dist/commands/attach.js +41 -0
- package/dist/commands/computer.js +3 -3
- package/dist/commands/detach-core.d.ts +46 -0
- package/dist/commands/detach-core.js +61 -0
- package/dist/commands/detach.d.ts +5 -0
- package/dist/commands/detach.js +171 -0
- package/dist/commands/doctor.js +18 -0
- package/dist/commands/exec.js +83 -13
- package/dist/commands/feed.d.ts +7 -1
- package/dist/commands/feed.js +47 -4
- package/dist/commands/go.d.ts +9 -2
- package/dist/commands/go.js +16 -6
- package/dist/commands/harness.d.ts +14 -0
- package/dist/commands/harness.js +145 -0
- package/dist/commands/import.js +38 -8
- package/dist/commands/inspect.js +5 -2
- package/dist/commands/profiles.d.ts +20 -0
- package/dist/commands/profiles.js +68 -21
- package/dist/commands/repo.js +14 -0
- package/dist/commands/routines.js +286 -15
- package/dist/commands/secrets.js +102 -37
- package/dist/commands/sessions-browser.js +12 -2
- package/dist/commands/sessions-export.js +19 -5
- package/dist/commands/sessions-migrate.d.ts +29 -0
- package/dist/commands/sessions-migrate.js +596 -0
- package/dist/commands/sessions-picker.d.ts +14 -1
- package/dist/commands/sessions-picker.js +184 -16
- package/dist/commands/sessions.d.ts +56 -14
- package/dist/commands/sessions.js +322 -62
- package/dist/commands/setup-computer.js +2 -2
- package/dist/commands/ssh.d.ts +13 -0
- package/dist/commands/ssh.js +110 -37
- package/dist/commands/status.js +10 -2
- package/dist/commands/versions.js +15 -6
- package/dist/commands/view.d.ts +5 -0
- package/dist/commands/view.js +24 -4
- package/dist/commands/watchdog.d.ts +7 -2
- package/dist/commands/watchdog.js +73 -57
- package/dist/index.js +59 -7
- package/dist/lib/activity.d.ts +17 -6
- package/dist/lib/activity.js +466 -40
- package/dist/lib/actor.d.ts +50 -0
- package/dist/lib/actor.js +166 -0
- package/dist/lib/agent-spec/provider.js +2 -1
- package/dist/lib/agent-spec/resolve.js +19 -5
- package/dist/lib/agent-spec/types.d.ts +9 -1
- package/dist/lib/agents.d.ts +21 -0
- package/dist/lib/agents.js +70 -6
- package/dist/lib/cloud/codex.d.ts +2 -0
- package/dist/lib/cloud/codex.js +14 -3
- package/dist/lib/cloud/session-index.d.ts +32 -0
- package/dist/lib/cloud/session-index.js +58 -0
- package/dist/lib/cloud/store.d.ts +7 -0
- package/dist/lib/cloud/store.js +25 -0
- package/dist/lib/config-transfer.js +4 -0
- package/dist/lib/daemon.d.ts +10 -1
- package/dist/lib/daemon.js +114 -11
- package/dist/lib/devices/connect.d.ts +15 -1
- package/dist/lib/devices/connect.js +15 -1
- package/dist/lib/devices/fleet.d.ts +37 -1
- package/dist/lib/devices/fleet.js +36 -2
- package/dist/lib/devices/health-report.d.ts +38 -0
- package/dist/lib/devices/health-report.js +214 -0
- package/dist/lib/devices/health.js +4 -1
- package/dist/lib/devices/reachability.d.ts +31 -0
- package/dist/lib/devices/reachability.js +40 -0
- package/dist/lib/devices/registry.d.ts +33 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/devices/resolve-target.d.ts +15 -27
- package/dist/lib/devices/resolve-target.js +63 -102
- package/dist/lib/devices/sync.d.ts +18 -0
- package/dist/lib/devices/sync.js +23 -1
- package/dist/lib/devices/tailscale.d.ts +3 -0
- package/dist/lib/devices/tailscale.js +1 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +82 -0
- package/dist/lib/exec.js +218 -6
- package/dist/lib/feed-post.d.ts +63 -0
- package/dist/lib/feed-post.js +204 -0
- package/dist/lib/feed.d.ts +7 -1
- package/dist/lib/feed.js +96 -6
- package/dist/lib/fleet/apply.d.ts +32 -2
- package/dist/lib/fleet/apply.js +97 -10
- package/dist/lib/fleet/types.d.ts +11 -0
- package/dist/lib/fs-walk.d.ts +13 -0
- package/dist/lib/fs-walk.js +16 -7
- package/dist/lib/heal.d.ts +7 -4
- package/dist/lib/heal.js +10 -22
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +156 -0
- package/dist/lib/hosts/dispatch.d.ts +16 -0
- package/dist/lib/hosts/dispatch.js +26 -4
- package/dist/lib/hosts/passthrough.js +9 -2
- package/dist/lib/hosts/reconnect.d.ts +78 -0
- package/dist/lib/hosts/reconnect.js +127 -0
- package/dist/lib/hosts/registry.d.ts +75 -14
- package/dist/lib/hosts/registry.js +205 -30
- package/dist/lib/hosts/remote-cmd.d.ts +11 -0
- package/dist/lib/hosts/remote-cmd.js +32 -8
- package/dist/lib/hosts/remote-session-id.d.ts +45 -0
- package/dist/lib/hosts/remote-session-id.js +84 -0
- package/dist/lib/hosts/run-target.d.ts +17 -5
- package/dist/lib/hosts/run-target.js +30 -10
- package/dist/lib/hosts/session-index.d.ts +18 -1
- package/dist/lib/hosts/session-index.js +40 -5
- package/dist/lib/hosts/session-marker.d.ts +33 -0
- package/dist/lib/hosts/session-marker.js +51 -0
- package/dist/lib/hosts/tasks.d.ts +8 -4
- package/dist/lib/import.d.ts +2 -0
- package/dist/lib/import.js +35 -2
- package/dist/lib/menubar/install-menubar.d.ts +29 -0
- package/dist/lib/menubar/install-menubar.js +59 -1
- package/dist/lib/menubar/notify-desktop.d.ts +44 -0
- package/dist/lib/menubar/notify-desktop.js +78 -0
- package/dist/lib/migrate.d.ts +16 -0
- package/dist/lib/migrate.js +36 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +54 -1
- package/dist/lib/overdue.d.ts +6 -2
- package/dist/lib/overdue.js +10 -36
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/process.d.ts +17 -0
- package/dist/lib/platform/process.js +70 -0
- package/dist/lib/profiles-presets.js +9 -7
- package/dist/lib/profiles.d.ts +31 -0
- package/dist/lib/profiles.js +70 -0
- package/dist/lib/pty-server.d.ts +2 -10
- package/dist/lib/pty-server.js +4 -38
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +48 -8
- package/dist/lib/rotate.d.ts +18 -0
- package/dist/lib/rotate.js +28 -0
- package/dist/lib/routine-notify.d.ts +76 -0
- package/dist/lib/routine-notify.js +190 -0
- package/dist/lib/routines-placement.d.ts +38 -0
- package/dist/lib/routines-placement.js +79 -0
- package/dist/lib/routines-project.d.ts +97 -0
- package/dist/lib/routines-project.js +349 -0
- package/dist/lib/routines.d.ts +94 -0
- package/dist/lib/routines.js +93 -9
- package/dist/lib/runner.d.ts +12 -2
- package/dist/lib/runner.js +242 -20
- package/dist/lib/sandbox.js +10 -0
- package/dist/lib/secrets/account-token.d.ts +20 -0
- package/dist/lib/secrets/account-token.js +64 -0
- package/dist/lib/secrets/agent.d.ts +74 -4
- package/dist/lib/secrets/agent.js +181 -107
- package/dist/lib/secrets/bundles.d.ts +54 -6
- package/dist/lib/secrets/bundles.js +230 -34
- package/dist/lib/secrets/index.d.ts +26 -3
- package/dist/lib/secrets/index.js +42 -12
- package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
- package/dist/lib/secrets/rc-hygiene.js +154 -0
- package/dist/lib/secrets/remote.d.ts +7 -4
- package/dist/lib/secrets/remote.js +7 -4
- package/dist/lib/secrets/session-store.d.ts +6 -2
- package/dist/lib/secrets/session-store.js +65 -15
- package/dist/lib/secrets/sync-commands.d.ts +21 -0
- package/dist/lib/secrets/sync-commands.js +21 -0
- package/dist/lib/secrets/unlock-hints.d.ts +27 -0
- package/dist/lib/secrets/unlock-hints.js +36 -0
- package/dist/lib/self-update.d.ts +23 -2
- package/dist/lib/self-update.js +86 -5
- package/dist/lib/session/active.d.ts +124 -7
- package/dist/lib/session/active.js +390 -56
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +54 -0
- package/dist/lib/session/db.js +320 -19
- package/dist/lib/session/detached.d.ts +30 -0
- package/dist/lib/session/detached.js +92 -0
- package/dist/lib/session/discover.d.ts +478 -3
- package/dist/lib/session/discover.js +1740 -472
- package/dist/lib/session/fork.js +2 -1
- package/dist/lib/session/hook-sessions.d.ts +43 -0
- package/dist/lib/session/hook-sessions.js +135 -0
- package/dist/lib/session/linear.d.ts +6 -0
- package/dist/lib/session/linear.js +55 -0
- package/dist/lib/session/migrate-targets.d.ts +65 -0
- package/dist/lib/session/migrate-targets.js +94 -0
- package/dist/lib/session/migrations.d.ts +37 -0
- package/dist/lib/session/migrations.js +60 -0
- package/dist/lib/session/parse.d.ts +18 -0
- package/dist/lib/session/parse.js +141 -32
- package/dist/lib/session/pid-registry.d.ts +34 -2
- package/dist/lib/session/pid-registry.js +49 -2
- package/dist/lib/session/prompt.d.ts +7 -0
- package/dist/lib/session/prompt.js +37 -0
- package/dist/lib/session/provenance.d.ts +7 -0
- package/dist/lib/session/render.d.ts +7 -2
- package/dist/lib/session/render.js +40 -26
- package/dist/lib/session/short-id.d.ts +17 -0
- package/dist/lib/session/short-id.js +20 -0
- package/dist/lib/session/state.d.ts +4 -0
- package/dist/lib/session/state.js +99 -13
- package/dist/lib/session/types.d.ts +25 -1
- package/dist/lib/session/types.js +8 -0
- package/dist/lib/shims.d.ts +1 -1
- package/dist/lib/shims.js +20 -6
- package/dist/lib/sqlite.d.ts +3 -2
- package/dist/lib/sqlite.js +27 -4
- package/dist/lib/staleness/detectors/commands.js +1 -1
- package/dist/lib/staleness/detectors/workflows.js +13 -2
- package/dist/lib/staleness/writers/commands.js +7 -7
- package/dist/lib/staleness/writers/workflows.d.ts +4 -2
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +3 -0
- package/dist/lib/state.d.ts +8 -4
- package/dist/lib/state.js +21 -37
- package/dist/lib/teams/agents.d.ts +19 -10
- package/dist/lib/teams/agents.js +40 -39
- package/dist/lib/types.d.ts +47 -2
- package/dist/lib/usage.d.ts +33 -1
- package/dist/lib/usage.js +172 -12
- package/dist/lib/versions.d.ts +9 -2
- package/dist/lib/versions.js +37 -8
- package/dist/lib/watchdog/log.d.ts +43 -0
- package/dist/lib/watchdog/log.js +69 -0
- package/dist/lib/watchdog/routine.d.ts +44 -0
- package/dist/lib/watchdog/routine.js +69 -0
- package/dist/lib/watchdog/runner.d.ts +51 -7
- package/dist/lib/watchdog/runner.js +239 -64
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +31 -16
- package/dist/lib/workflows.d.ts +16 -0
- package/dist/lib/workflows.js +110 -1
- package/package.json +1 -1
package/dist/lib/profiles.js
CHANGED
|
@@ -223,11 +223,75 @@ export function profileFromPreset(profileName, preset, version) {
|
|
|
223
223
|
envVar: preset.authEnvVar,
|
|
224
224
|
keychainItem: keychainItemName(preset.provider),
|
|
225
225
|
},
|
|
226
|
+
authOptional: preset.authOptional,
|
|
226
227
|
description: preset.description,
|
|
227
228
|
preset: preset.name,
|
|
228
229
|
provider: preset.provider,
|
|
229
230
|
};
|
|
230
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Env var each host CLI reads to override its model. Mirror of the read-side
|
|
234
|
+
* `MODEL_ENV_KEYS` above, keyed by host so a one-shot `--host <agent> --model
|
|
235
|
+
* <id>` writes the model onto the var that host actually honors.
|
|
236
|
+
*/
|
|
237
|
+
const MODEL_ENV_KEY_BY_HOST = {
|
|
238
|
+
claude: 'ANTHROPIC_MODEL',
|
|
239
|
+
opencode: 'OPENCODE_MODEL',
|
|
240
|
+
grok: 'GROK_MODEL',
|
|
241
|
+
gemini: 'GEMINI_MODEL',
|
|
242
|
+
codex: 'OPENAI_MODEL',
|
|
243
|
+
};
|
|
244
|
+
/** Base-URL env var per host, for the OpenAI/Anthropic-compatible hosts where it applies. */
|
|
245
|
+
const BASE_URL_ENV_KEY_BY_HOST = {
|
|
246
|
+
claude: 'ANTHROPIC_BASE_URL',
|
|
247
|
+
codex: 'OPENAI_BASE_URL',
|
|
248
|
+
};
|
|
249
|
+
/** Return the model-override env var for a host (known hosts mapped; else `<HOST>_MODEL`). */
|
|
250
|
+
export function modelEnvKeyForHost(host) {
|
|
251
|
+
return MODEL_ENV_KEY_BY_HOST[host] ?? `${host.toUpperCase()}_MODEL`;
|
|
252
|
+
}
|
|
253
|
+
/** Return the base-URL env var for a host, or null when the host has no known override var. */
|
|
254
|
+
export function baseUrlEnvKeyForHost(host) {
|
|
255
|
+
return BASE_URL_ENV_KEY_BY_HOST[host] ?? null;
|
|
256
|
+
}
|
|
257
|
+
/** Env var each host reads its auth token from, for custom-endpoint (`--auth-provider`) harnesses. */
|
|
258
|
+
const AUTH_ENV_KEY_BY_HOST = {
|
|
259
|
+
claude: 'ANTHROPIC_AUTH_TOKEN',
|
|
260
|
+
codex: 'OPENAI_API_KEY',
|
|
261
|
+
gemini: 'GEMINI_API_KEY',
|
|
262
|
+
grok: 'XAI_API_KEY',
|
|
263
|
+
opencode: 'OPENCODE_API_KEY',
|
|
264
|
+
};
|
|
265
|
+
/** Return the auth-token env var for a host, or null when unknown. */
|
|
266
|
+
export function authEnvKeyForHost(host) {
|
|
267
|
+
return AUTH_ENV_KEY_BY_HOST[host] ?? null;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Build a custom-harness profile from a host CLI + model in one shot, without a
|
|
271
|
+
* preset. The model lands on the host's model env var ({@link modelEnvKeyForHost});
|
|
272
|
+
* auth is attached only when both `provider` and `authEnvVar` are supplied
|
|
273
|
+
* (hosts that manage their own login — e.g. opencode — need neither).
|
|
274
|
+
*/
|
|
275
|
+
export function profileFromHostModel(name, host, model, opts = {}) {
|
|
276
|
+
const env = { [modelEnvKeyForHost(host)]: model };
|
|
277
|
+
if (opts.baseUrl) {
|
|
278
|
+
const key = baseUrlEnvKeyForHost(host);
|
|
279
|
+
if (key)
|
|
280
|
+
env[key] = opts.baseUrl;
|
|
281
|
+
}
|
|
282
|
+
const profile = {
|
|
283
|
+
name,
|
|
284
|
+
host: { agent: host, version: opts.version },
|
|
285
|
+
env,
|
|
286
|
+
description: opts.description ?? `Custom harness: ${host} + ${model}`,
|
|
287
|
+
provider: opts.provider ?? host,
|
|
288
|
+
};
|
|
289
|
+
if (opts.provider && opts.authEnvVar) {
|
|
290
|
+
profile.auth = { envVar: opts.authEnvVar, keychainItem: keychainItemName(opts.provider) };
|
|
291
|
+
profile.authOptional = false;
|
|
292
|
+
}
|
|
293
|
+
return profile;
|
|
294
|
+
}
|
|
231
295
|
/**
|
|
232
296
|
* Resolve a profile into the env block that should be injected into the
|
|
233
297
|
* spawned agent process. Reads the token from keychain at exec time so the
|
|
@@ -236,6 +300,12 @@ export function profileFromPreset(profileName, preset, version) {
|
|
|
236
300
|
export function resolveProfileEnv(profile) {
|
|
237
301
|
const env = { ...profile.env };
|
|
238
302
|
if (profile.auth) {
|
|
303
|
+
// Optional auth (host manages its own login) with no stored token: inject
|
|
304
|
+
// nothing and let the host use its own credentials. Only required auth
|
|
305
|
+
// hard-fails on a missing keychain item.
|
|
306
|
+
if (profile.authOptional && !hasKeychainToken(profile.auth.keychainItem)) {
|
|
307
|
+
return env;
|
|
308
|
+
}
|
|
239
309
|
const token = getKeychainToken(profile.auth.keychainItem);
|
|
240
310
|
env[profile.auth.envVar] = token;
|
|
241
311
|
}
|
package/dist/lib/pty-server.d.ts
CHANGED
|
@@ -8,16 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Protocol: newline-delimited JSON over ~/.agents/.system/pty.sock
|
|
10
10
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* Used to defeat PID reuse: a kill(pid, ...) is only safe when the process
|
|
14
|
-
* still occupies the PID we observed at spawn time.
|
|
15
|
-
*
|
|
16
|
-
* Linux: field 22 of /proc/<pid>/stat (starttime in clock ticks since boot).
|
|
17
|
-
* macOS: output of `ps -o lstart= -p <pid>` (start time in human format).
|
|
18
|
-
* Returns null on any error so callers can skip the guard rather than crash.
|
|
19
|
-
*/
|
|
20
|
-
export declare function captureProcessStartTime(pid: number): string | null;
|
|
11
|
+
import { captureProcessStartTime } from './platform/index.js';
|
|
12
|
+
export { captureProcessStartTime };
|
|
21
13
|
/**
|
|
22
14
|
* Wrap a user command so a `__SENTINEL__:<exit>` line is printed after it
|
|
23
15
|
* finishes — that line drives completion detection in the exec/read flow.
|
package/dist/lib/pty-server.js
CHANGED
|
@@ -12,46 +12,12 @@ import * as net from 'net';
|
|
|
12
12
|
import * as fs from 'fs';
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import * as crypto from 'crypto';
|
|
15
|
-
import { execFileSync } from 'child_process';
|
|
16
15
|
import { fileURLToPath } from 'url';
|
|
17
16
|
import { getPtyDir as getPtyDirRoot } from './state.js';
|
|
18
|
-
import { isAlive } from './platform/index.js';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
* still occupies the PID we observed at spawn time.
|
|
23
|
-
*
|
|
24
|
-
* Linux: field 22 of /proc/<pid>/stat (starttime in clock ticks since boot).
|
|
25
|
-
* macOS: output of `ps -o lstart= -p <pid>` (start time in human format).
|
|
26
|
-
* Returns null on any error so callers can skip the guard rather than crash.
|
|
27
|
-
*/
|
|
28
|
-
export function captureProcessStartTime(pid) {
|
|
29
|
-
if (!pid || pid <= 0)
|
|
30
|
-
return null;
|
|
31
|
-
try {
|
|
32
|
-
if (process.platform === 'linux') {
|
|
33
|
-
const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf-8');
|
|
34
|
-
// The comm field (#2) is wrapped in parens and may contain spaces, so
|
|
35
|
-
// split off everything after the last `)` to get a clean field list.
|
|
36
|
-
const lastParen = stat.lastIndexOf(')');
|
|
37
|
-
if (lastParen < 0)
|
|
38
|
-
return null;
|
|
39
|
-
const tail = stat.slice(lastParen + 2);
|
|
40
|
-
const fields = tail.split(' ');
|
|
41
|
-
// After comm we are at field 3; starttime is field 22, so index 19 here.
|
|
42
|
-
return fields[19] || null;
|
|
43
|
-
}
|
|
44
|
-
const out = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {
|
|
45
|
-
encoding: 'utf-8',
|
|
46
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
47
|
-
});
|
|
48
|
-
const trimmed = out.trim();
|
|
49
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
17
|
+
import { isAlive, captureProcessStartTime } from './platform/index.js';
|
|
18
|
+
// Re-exported for the existing importers of this module; the implementation is
|
|
19
|
+
// in platform/process.ts, shared with teams/agents.ts so the two cannot drift.
|
|
20
|
+
export { captureProcessStartTime };
|
|
55
21
|
// --- Constants ---
|
|
56
22
|
const IS_WINDOWS = process.platform === 'win32';
|
|
57
23
|
const SENTINEL = '__AGENTS_PTY_DONE__';
|
package/dist/lib/registry.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export declare function getEnabledRegistries(type: RegistryType): Array<{
|
|
|
17
17
|
}>;
|
|
18
18
|
/** Add or update a registry configuration in agents.yaml. */
|
|
19
19
|
export declare function setRegistry(type: RegistryType, name: string, config: Partial<RegistryConfig>): void;
|
|
20
|
-
/** Remove a user-configured registry. Returns false if it did not exist. */
|
|
20
|
+
/** Remove a user-configured or seeded registry. Returns false if it did not exist. */
|
|
21
21
|
export declare function removeRegistry(type: RegistryType, name: string): boolean;
|
|
22
22
|
/** Search MCP registries for servers matching a query string. */
|
|
23
23
|
export declare function searchMcpRegistries(query: string, options?: {
|
package/dist/lib/registry.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import { createHash } from 'crypto';
|
|
11
|
-
import { DEFAULT_REGISTRIES } from './types.js';
|
|
11
|
+
import { DEFAULT_REGISTRIES, SEEDED_REGISTRIES } from './types.js';
|
|
12
12
|
import { readMeta, writeMeta } from './state.js';
|
|
13
13
|
import { discoverSkillsFromRepo } from './skills.js';
|
|
14
14
|
const UNSAFE_PACKAGE_SPEC_CHARS = /[;&|`$\s\x00-\x1f\x7f]/;
|
|
@@ -26,13 +26,38 @@ export function validatedPyPISpec(spec) {
|
|
|
26
26
|
}
|
|
27
27
|
return spec;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Seeded presets offered for `type`, minus any the user has removed.
|
|
31
|
+
*
|
|
32
|
+
* These are resolved here rather than written into agents.yaml. Seeding used to
|
|
33
|
+
* happen on the state READ path and persisted the entry — but agents.yaml is
|
|
34
|
+
* git-tracked in the user's DotAgents repo, so that write left the working tree
|
|
35
|
+
* dirty and every `agents repo pull` aborted with "Working tree has uncommitted
|
|
36
|
+
* changes", naming neither the file nor the cause. Since every `agents`
|
|
37
|
+
* invocation reads state, the dirt returned the instant it was cleared, and the
|
|
38
|
+
* loop could not be escaped through the CLI at all (RUSH-1925).
|
|
39
|
+
*
|
|
40
|
+
* `seededPresets` is the removal tombstone: a key listed there means the user
|
|
41
|
+
* removed that preset, so it is not offered again. Users who were seeded under
|
|
42
|
+
* the old behaviour have the key listed *and* the entry in their own
|
|
43
|
+
* `registries:` block, which still wins below — so nothing disappears for them.
|
|
44
|
+
*/
|
|
45
|
+
function offeredSeeds(type, meta) {
|
|
46
|
+
const removed = new Set(meta.seededPresets || []);
|
|
47
|
+
const offered = {};
|
|
48
|
+
for (const [name, config] of Object.entries(SEEDED_REGISTRIES[type] || {})) {
|
|
49
|
+
if (!removed.has(`${type}.${name}`))
|
|
50
|
+
offered[name] = { ...config };
|
|
51
|
+
}
|
|
52
|
+
return offered;
|
|
53
|
+
}
|
|
29
54
|
/** Get all registries of a given type, merging defaults with user overrides. */
|
|
30
55
|
export function getRegistries(type) {
|
|
31
56
|
const meta = readMeta();
|
|
32
57
|
const defaultRegs = DEFAULT_REGISTRIES[type] || {};
|
|
33
58
|
const userRegs = meta.registries?.[type] || {};
|
|
34
59
|
// Merge defaults with user config (user overrides defaults)
|
|
35
|
-
return { ...defaultRegs, ...userRegs };
|
|
60
|
+
return { ...defaultRegs, ...offeredSeeds(type, meta), ...userRegs };
|
|
36
61
|
}
|
|
37
62
|
/** Get only the enabled registries of a given type. */
|
|
38
63
|
export function getEnabledRegistries(type) {
|
|
@@ -50,19 +75,34 @@ export function setRegistry(type, name, config) {
|
|
|
50
75
|
if (!meta.registries[type]) {
|
|
51
76
|
meta.registries[type] = {};
|
|
52
77
|
}
|
|
53
|
-
|
|
78
|
+
// Seeded presets are resolved in memory and never materialized in agents.yaml
|
|
79
|
+
// (see offeredSeeds), so a partial update — `registry disable`, `enable`, or
|
|
80
|
+
// `config --api-key` — has nothing stored to merge with. Without the
|
|
81
|
+
// SEEDED_REGISTRIES fallback it would persist only the changed fields and drop
|
|
82
|
+
// `url`, and because userRegs wins over the in-memory seed in getRegistries the
|
|
83
|
+
// preset would stay broken even after re-enabling.
|
|
84
|
+
const existing = meta.registries[type][name]
|
|
85
|
+
|| DEFAULT_REGISTRIES[type]?.[name]
|
|
86
|
+
|| SEEDED_REGISTRIES[type]?.[name];
|
|
54
87
|
meta.registries[type][name] = { ...existing, ...config };
|
|
55
88
|
writeMeta(meta);
|
|
56
89
|
}
|
|
57
|
-
/** Remove a user-configured registry. Returns false if it did not exist. */
|
|
90
|
+
/** Remove a user-configured or seeded registry. Returns false if it did not exist. */
|
|
58
91
|
export function removeRegistry(type, name) {
|
|
59
92
|
const meta = readMeta();
|
|
60
|
-
|
|
93
|
+
const inUserConfig = !!meta.registries?.[type]?.[name];
|
|
94
|
+
// A seeded preset has no entry in agents.yaml until the user edits it, so
|
|
95
|
+
// removal is recorded as a tombstone in seededPresets instead of a deletion.
|
|
96
|
+
const isOfferedSeed = !!offeredSeeds(type, meta)[name];
|
|
97
|
+
if (!inUserConfig && !isOfferedSeed)
|
|
98
|
+
return false;
|
|
99
|
+
if (inUserConfig)
|
|
61
100
|
delete meta.registries[type][name];
|
|
62
|
-
|
|
63
|
-
|
|
101
|
+
if (SEEDED_REGISTRIES[type]?.[name]) {
|
|
102
|
+
meta.seededPresets = [...new Set([...(meta.seededPresets || []), `${type}.${name}`])];
|
|
64
103
|
}
|
|
65
|
-
|
|
104
|
+
writeMeta(meta);
|
|
105
|
+
return true;
|
|
66
106
|
}
|
|
67
107
|
/**
|
|
68
108
|
* Cap every registry network call. Without this a slow or unreachable registry
|
package/dist/lib/rotate.d.ts
CHANGED
|
@@ -126,6 +126,24 @@ export declare function pickBalancedCandidate(candidates: RotateCandidate[]): Ro
|
|
|
126
126
|
*/
|
|
127
127
|
export declare function pickAvailableCandidate(candidates: RotateCandidate[], preferredVersion?: string | null): RotateResult | null;
|
|
128
128
|
export declare function collectRunCandidates(agent: AgentId): Promise<RotateCandidate[]>;
|
|
129
|
+
/**
|
|
130
|
+
* Resolve an account identity to the installed version slot that holds it, over
|
|
131
|
+
* an already-collected candidate list. Pure — no I/O — so it is unit-tested
|
|
132
|
+
* directly. Matches, case-insensitively, against a candidate's login `email`
|
|
133
|
+
* (the usual form) or its `accountKey`, and only ever returns a signed-in slot.
|
|
134
|
+
* Returns null when nothing matches, so the caller can fall back and warn.
|
|
135
|
+
*/
|
|
136
|
+
export declare function matchAccountVersion(candidates: RotateCandidate[], account: string): string | null;
|
|
137
|
+
/**
|
|
138
|
+
* Resolve a routine's `account:` pin (login email or account key) to the
|
|
139
|
+
* installed version currently holding that account. Thin I/O wrapper over
|
|
140
|
+
* {@link collectRunCandidates} + {@link matchAccountVersion}; returns null when
|
|
141
|
+
* no signed-in version matches. Pinning a routine to a distinct account is the
|
|
142
|
+
* durable cure for the shared-single-use-refresh-token revocation storm
|
|
143
|
+
* (RUSH-1957): the pinned run never rotates and never lands on another
|
|
144
|
+
* routine's credential.
|
|
145
|
+
*/
|
|
146
|
+
export declare function resolveAccountVersion(agent: AgentId, account: string): Promise<string | null>;
|
|
129
147
|
/**
|
|
130
148
|
* Pick a healthy version for `agent` using weighted random by remaining
|
|
131
149
|
* capacity. See `pickBalancedCandidate` for algorithm details.
|
package/dist/lib/rotate.js
CHANGED
|
@@ -316,6 +316,34 @@ export async function collectRunCandidates(agent) {
|
|
|
316
316
|
};
|
|
317
317
|
});
|
|
318
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* Resolve an account identity to the installed version slot that holds it, over
|
|
321
|
+
* an already-collected candidate list. Pure — no I/O — so it is unit-tested
|
|
322
|
+
* directly. Matches, case-insensitively, against a candidate's login `email`
|
|
323
|
+
* (the usual form) or its `accountKey`, and only ever returns a signed-in slot.
|
|
324
|
+
* Returns null when nothing matches, so the caller can fall back and warn.
|
|
325
|
+
*/
|
|
326
|
+
export function matchAccountVersion(candidates, account) {
|
|
327
|
+
const needle = account.trim().toLowerCase();
|
|
328
|
+
if (!needle)
|
|
329
|
+
return null;
|
|
330
|
+
const match = candidates.find((c) => c.signedIn &&
|
|
331
|
+
(c.email?.toLowerCase() === needle || c.accountKey?.toLowerCase() === needle));
|
|
332
|
+
return match?.version ?? null;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Resolve a routine's `account:` pin (login email or account key) to the
|
|
336
|
+
* installed version currently holding that account. Thin I/O wrapper over
|
|
337
|
+
* {@link collectRunCandidates} + {@link matchAccountVersion}; returns null when
|
|
338
|
+
* no signed-in version matches. Pinning a routine to a distinct account is the
|
|
339
|
+
* durable cure for the shared-single-use-refresh-token revocation storm
|
|
340
|
+
* (RUSH-1957): the pinned run never rotates and never lands on another
|
|
341
|
+
* routine's credential.
|
|
342
|
+
*/
|
|
343
|
+
export async function resolveAccountVersion(agent, account) {
|
|
344
|
+
const candidates = await collectRunCandidates(agent);
|
|
345
|
+
return matchAccountVersion(candidates, account);
|
|
346
|
+
}
|
|
319
347
|
/**
|
|
320
348
|
* Pick a healthy version for `agent` using weighted random by remaining
|
|
321
349
|
* capacity. See `pickBalancedCandidate` for algorithm details.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routine lifecycle desktop notifications (RUSH-2030).
|
|
3
|
+
*
|
|
4
|
+
* The daemon fires a branded notification when a scheduled routine starts and
|
|
5
|
+
* when it finishes (success or failure), routed through the MenubarHelper
|
|
6
|
+
* companion (notify-desktop.ts) so it carries the agents-cli mark.
|
|
7
|
+
*
|
|
8
|
+
* Anti-spam threshold (Acceptance Criteria #4 — "define a sensible threshold so
|
|
9
|
+
* users are not spammed"):
|
|
10
|
+
* - Agent / workflow routines: notify on BOTH start and finish. These are the
|
|
11
|
+
* runs whose output a user actually wants surfaced.
|
|
12
|
+
* - Command routines (deterministic housekeeping — version checks, `git pull`,
|
|
13
|
+
* notify shims that can fire every minute): notify only on FAILURE. A green
|
|
14
|
+
* housekeeping run is noise; a broken one is worth a ping. No start ping.
|
|
15
|
+
* - "Notable output" is folded into the single finish notification rather than
|
|
16
|
+
* sent as a third message: on failure the error reason, on success the first
|
|
17
|
+
* line of the run's report (the user-facing result) when the routine produced
|
|
18
|
+
* one. One start + one finish per run — never a stream.
|
|
19
|
+
*
|
|
20
|
+
* The pure builders (`routineStartNotification` / `routineFinishNotification`)
|
|
21
|
+
* return `null` when the threshold says "don't notify", and are unit-tested; the
|
|
22
|
+
* `notifyRoutine*` wrappers do the filesystem read + dispatch for the daemon.
|
|
23
|
+
*/
|
|
24
|
+
import type { JobConfig, RunMeta } from './routines.js';
|
|
25
|
+
import { type DesktopNotification } from './menubar/notify-desktop.js';
|
|
26
|
+
type RoutineKind = 'agent' | 'workflow' | 'command';
|
|
27
|
+
/** Which flavor of routine a config/meta describes — drives the notify threshold. */
|
|
28
|
+
export declare function routineKind(r: Pick<JobConfig, 'agent' | 'workflow' | 'command'>): RoutineKind;
|
|
29
|
+
/** "1m 20s" / "45s" / "2h 3m" from a millisecond duration, or null when unknown. */
|
|
30
|
+
export declare function formatDuration(ms: number | undefined): string | null;
|
|
31
|
+
/**
|
|
32
|
+
* First non-empty line of a run report, trimmed to a notification-sized snippet.
|
|
33
|
+
* This is the "notable output" surfaced on a successful finish — the routine's
|
|
34
|
+
* own user-facing result. Returns null for an empty/whitespace report so the
|
|
35
|
+
* caller falls back to a plain "Completed" body.
|
|
36
|
+
*/
|
|
37
|
+
export declare function notableSnippet(report: string | null | undefined, maxLen?: number): string | null;
|
|
38
|
+
/**
|
|
39
|
+
* Notification for a routine START, or null when the threshold suppresses it
|
|
40
|
+
* (command-mode housekeeping). Clicking opens the runs folder
|
|
41
|
+
* (~/.agents/.history/runs).
|
|
42
|
+
*/
|
|
43
|
+
export declare function routineStartNotification(config: Pick<JobConfig, 'name' | 'agent' | 'workflow' | 'command'>): DesktopNotification | null;
|
|
44
|
+
/**
|
|
45
|
+
* Notification for a routine that failed to even START — `executeJobDetached`
|
|
46
|
+
* threw before the child was spawned, so no run record and no finish will ever
|
|
47
|
+
* exist. The daemon fires the START notification unconditionally, so this closes
|
|
48
|
+
* the "exactly one start + one finish" invariant: the orphaned start gets its
|
|
49
|
+
* matching failure banner (RUSH-2030). Unlike a green finish this is never
|
|
50
|
+
* suppressed — a broken start is worth a ping for every routine kind, including
|
|
51
|
+
* command housekeeping. Clicking opens the runs folder (~/.agents/.history/runs)
|
|
52
|
+
* since there is no run report to open.
|
|
53
|
+
*/
|
|
54
|
+
export declare function routineStartFailedNotification(config: Pick<JobConfig, 'name' | 'agent' | 'workflow' | 'command'>, error: string): DesktopNotification;
|
|
55
|
+
/**
|
|
56
|
+
* Notification for a routine FINISH, or null when the threshold suppresses it
|
|
57
|
+
* (a successful command-mode housekeeping run). Success carries the report's
|
|
58
|
+
* first line when present (the notable output); failure carries the reason.
|
|
59
|
+
* Clicking opens the run report/log when one is available, else the runs folder
|
|
60
|
+
* (~/.agents/.history/runs).
|
|
61
|
+
*/
|
|
62
|
+
export declare function routineFinishNotification(meta: Pick<RunMeta, 'jobName' | 'status' | 'exitCode' | 'errorMessage' | 'duration' | 'agent' | 'workflow' | 'command'>, opts?: {
|
|
63
|
+
report?: string | null;
|
|
64
|
+
artifactPath?: string | null;
|
|
65
|
+
}): DesktopNotification | null;
|
|
66
|
+
/** Daemon glue: fire the START notification for a triggered routine. Best-effort. */
|
|
67
|
+
export declare function notifyRoutineStart(config: JobConfig): void;
|
|
68
|
+
/**
|
|
69
|
+
* Daemon glue: fire the "failed to start" notification when a routine trigger
|
|
70
|
+
* threw before spawning a child. Pairs with the unconditional START ping so a
|
|
71
|
+
* pre-spawn failure never leaves an orphaned "Routine started". Best-effort.
|
|
72
|
+
*/
|
|
73
|
+
export declare function notifyRoutineStartFailed(config: JobConfig, error: string): void;
|
|
74
|
+
/** Daemon glue: fire the FINISH notification for a completed run. Best-effort. */
|
|
75
|
+
export declare function notifyRoutineFinish(meta: RunMeta): void;
|
|
76
|
+
export {};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routine lifecycle desktop notifications (RUSH-2030).
|
|
3
|
+
*
|
|
4
|
+
* The daemon fires a branded notification when a scheduled routine starts and
|
|
5
|
+
* when it finishes (success or failure), routed through the MenubarHelper
|
|
6
|
+
* companion (notify-desktop.ts) so it carries the agents-cli mark.
|
|
7
|
+
*
|
|
8
|
+
* Anti-spam threshold (Acceptance Criteria #4 — "define a sensible threshold so
|
|
9
|
+
* users are not spammed"):
|
|
10
|
+
* - Agent / workflow routines: notify on BOTH start and finish. These are the
|
|
11
|
+
* runs whose output a user actually wants surfaced.
|
|
12
|
+
* - Command routines (deterministic housekeeping — version checks, `git pull`,
|
|
13
|
+
* notify shims that can fire every minute): notify only on FAILURE. A green
|
|
14
|
+
* housekeeping run is noise; a broken one is worth a ping. No start ping.
|
|
15
|
+
* - "Notable output" is folded into the single finish notification rather than
|
|
16
|
+
* sent as a third message: on failure the error reason, on success the first
|
|
17
|
+
* line of the run's report (the user-facing result) when the routine produced
|
|
18
|
+
* one. One start + one finish per run — never a stream.
|
|
19
|
+
*
|
|
20
|
+
* The pure builders (`routineStartNotification` / `routineFinishNotification`)
|
|
21
|
+
* return `null` when the threshold says "don't notify", and are unit-tested; the
|
|
22
|
+
* `notifyRoutine*` wrappers do the filesystem read + dispatch for the daemon.
|
|
23
|
+
*/
|
|
24
|
+
import * as fs from 'fs';
|
|
25
|
+
import * as path from 'path';
|
|
26
|
+
import { getRunDir } from './routines.js';
|
|
27
|
+
import { notifyDesktop } from './menubar/notify-desktop.js';
|
|
28
|
+
/** Which flavor of routine a config/meta describes — drives the notify threshold. */
|
|
29
|
+
export function routineKind(r) {
|
|
30
|
+
if (r.command)
|
|
31
|
+
return 'command';
|
|
32
|
+
if (r.workflow)
|
|
33
|
+
return 'workflow';
|
|
34
|
+
return 'agent';
|
|
35
|
+
}
|
|
36
|
+
/** Human label for the routine body ("agent claude", "workflow deploy", "command"). */
|
|
37
|
+
function routineLabel(r) {
|
|
38
|
+
if (r.command)
|
|
39
|
+
return 'command';
|
|
40
|
+
if (r.workflow)
|
|
41
|
+
return `workflow ${r.workflow}`;
|
|
42
|
+
return `agent ${r.agent ?? 'unknown'}`;
|
|
43
|
+
}
|
|
44
|
+
/** "1m 20s" / "45s" / "2h 3m" from a millisecond duration, or null when unknown. */
|
|
45
|
+
export function formatDuration(ms) {
|
|
46
|
+
if (ms === undefined || !Number.isFinite(ms) || ms < 0)
|
|
47
|
+
return null;
|
|
48
|
+
const totalSec = Math.round(ms / 1000);
|
|
49
|
+
if (totalSec < 60)
|
|
50
|
+
return `${totalSec}s`;
|
|
51
|
+
const min = Math.floor(totalSec / 60);
|
|
52
|
+
const sec = totalSec % 60;
|
|
53
|
+
if (min < 60)
|
|
54
|
+
return sec ? `${min}m ${sec}s` : `${min}m`;
|
|
55
|
+
const hr = Math.floor(min / 60);
|
|
56
|
+
const remMin = min % 60;
|
|
57
|
+
return remMin ? `${hr}h ${remMin}m` : `${hr}h`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* First non-empty line of a run report, trimmed to a notification-sized snippet.
|
|
61
|
+
* This is the "notable output" surfaced on a successful finish — the routine's
|
|
62
|
+
* own user-facing result. Returns null for an empty/whitespace report so the
|
|
63
|
+
* caller falls back to a plain "Completed" body.
|
|
64
|
+
*/
|
|
65
|
+
export function notableSnippet(report, maxLen = 140) {
|
|
66
|
+
if (!report)
|
|
67
|
+
return null;
|
|
68
|
+
const firstLine = report
|
|
69
|
+
.split('\n')
|
|
70
|
+
.map((l) => l.trim())
|
|
71
|
+
.find((l) => l.length > 0);
|
|
72
|
+
if (!firstLine)
|
|
73
|
+
return null;
|
|
74
|
+
return firstLine.length > maxLen ? `${firstLine.slice(0, maxLen - 1).trimEnd()}…` : firstLine;
|
|
75
|
+
}
|
|
76
|
+
/** Encode a click action that opens a file (report/log) in the default app. */
|
|
77
|
+
function openAction(filePath) {
|
|
78
|
+
return filePath ? `open:${filePath}` : undefined;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Notification for a routine START, or null when the threshold suppresses it
|
|
82
|
+
* (command-mode housekeeping). Clicking opens the runs folder
|
|
83
|
+
* (~/.agents/.history/runs).
|
|
84
|
+
*/
|
|
85
|
+
export function routineStartNotification(config) {
|
|
86
|
+
if (routineKind(config) === 'command')
|
|
87
|
+
return null;
|
|
88
|
+
return {
|
|
89
|
+
title: 'Routine started',
|
|
90
|
+
subtitle: config.name,
|
|
91
|
+
body: `Running ${routineLabel(config)}`,
|
|
92
|
+
action: 'routines:list',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Notification for a routine that failed to even START — `executeJobDetached`
|
|
97
|
+
* threw before the child was spawned, so no run record and no finish will ever
|
|
98
|
+
* exist. The daemon fires the START notification unconditionally, so this closes
|
|
99
|
+
* the "exactly one start + one finish" invariant: the orphaned start gets its
|
|
100
|
+
* matching failure banner (RUSH-2030). Unlike a green finish this is never
|
|
101
|
+
* suppressed — a broken start is worth a ping for every routine kind, including
|
|
102
|
+
* command housekeeping. Clicking opens the runs folder (~/.agents/.history/runs)
|
|
103
|
+
* since there is no run report to open.
|
|
104
|
+
*/
|
|
105
|
+
export function routineStartFailedNotification(config, error) {
|
|
106
|
+
return {
|
|
107
|
+
title: 'Routine failed',
|
|
108
|
+
subtitle: config.name,
|
|
109
|
+
body: `Failed to start: ${error}`,
|
|
110
|
+
action: 'routines:list',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Notification for a routine FINISH, or null when the threshold suppresses it
|
|
115
|
+
* (a successful command-mode housekeeping run). Success carries the report's
|
|
116
|
+
* first line when present (the notable output); failure carries the reason.
|
|
117
|
+
* Clicking opens the run report/log when one is available, else the runs folder
|
|
118
|
+
* (~/.agents/.history/runs).
|
|
119
|
+
*/
|
|
120
|
+
export function routineFinishNotification(meta, opts = {}) {
|
|
121
|
+
const kind = routineKind(meta);
|
|
122
|
+
const ok = meta.status === 'completed';
|
|
123
|
+
if (kind === 'command' && ok)
|
|
124
|
+
return null; // green housekeeping is noise
|
|
125
|
+
const action = openAction(opts.artifactPath) ?? 'routines:list';
|
|
126
|
+
if (ok) {
|
|
127
|
+
const snippet = notableSnippet(opts.report);
|
|
128
|
+
const dur = formatDuration(meta.duration);
|
|
129
|
+
return {
|
|
130
|
+
title: 'Routine finished',
|
|
131
|
+
subtitle: meta.jobName,
|
|
132
|
+
body: snippet ?? (dur ? `Completed in ${dur}` : 'Completed'),
|
|
133
|
+
action,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
// failed | timeout
|
|
137
|
+
const reason = meta.status === 'timeout'
|
|
138
|
+
? 'Timed out'
|
|
139
|
+
: meta.errorMessage
|
|
140
|
+
? meta.errorMessage
|
|
141
|
+
: `Exited with code ${meta.exitCode ?? '?'}`;
|
|
142
|
+
return {
|
|
143
|
+
title: 'Routine failed',
|
|
144
|
+
subtitle: meta.jobName,
|
|
145
|
+
body: reason,
|
|
146
|
+
action,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** Read a finished run's report text + the best artifact to open on click. */
|
|
150
|
+
function loadRunArtifacts(meta) {
|
|
151
|
+
try {
|
|
152
|
+
const runDir = getRunDir(meta.jobName, meta.runId);
|
|
153
|
+
const reportPath = path.join(runDir, 'report.md');
|
|
154
|
+
const stdoutPath = path.join(runDir, 'stdout.log');
|
|
155
|
+
let report = null;
|
|
156
|
+
let artifactPath = null;
|
|
157
|
+
if (fs.existsSync(reportPath)) {
|
|
158
|
+
report = fs.readFileSync(reportPath, 'utf-8');
|
|
159
|
+
artifactPath = reportPath;
|
|
160
|
+
}
|
|
161
|
+
else if (fs.existsSync(stdoutPath)) {
|
|
162
|
+
artifactPath = stdoutPath;
|
|
163
|
+
}
|
|
164
|
+
return { report, artifactPath };
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return { report: null, artifactPath: null };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** Daemon glue: fire the START notification for a triggered routine. Best-effort. */
|
|
171
|
+
export function notifyRoutineStart(config) {
|
|
172
|
+
const n = routineStartNotification(config);
|
|
173
|
+
if (n)
|
|
174
|
+
notifyDesktop(n);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Daemon glue: fire the "failed to start" notification when a routine trigger
|
|
178
|
+
* threw before spawning a child. Pairs with the unconditional START ping so a
|
|
179
|
+
* pre-spawn failure never leaves an orphaned "Routine started". Best-effort.
|
|
180
|
+
*/
|
|
181
|
+
export function notifyRoutineStartFailed(config, error) {
|
|
182
|
+
notifyDesktop(routineStartFailedNotification(config, error));
|
|
183
|
+
}
|
|
184
|
+
/** Daemon glue: fire the FINISH notification for a completed run. Best-effort. */
|
|
185
|
+
export function notifyRoutineFinish(meta) {
|
|
186
|
+
const { report, artifactPath } = loadRunArtifacts(meta);
|
|
187
|
+
const n = routineFinishNotification(meta, { report, artifactPath });
|
|
188
|
+
if (n)
|
|
189
|
+
notifyDesktop(n);
|
|
190
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime placement resolution for routines: local / host / fleet / cloud.
|
|
3
|
+
*
|
|
4
|
+
* The scheduler only decides *whether* this machine may fire the job
|
|
5
|
+
* (`devices` + `jobRunsOnThisDevice`). This module decides *where the job
|
|
6
|
+
* body runs* once it has been fired, using `hostStrategy`.
|
|
7
|
+
*
|
|
8
|
+
* Fleet semantics (RUSH-2035 / RUSH-1980): pick exactly one online device per
|
|
9
|
+
* fire. Cross-device double-fire is prevented by requiring a `devices` firing
|
|
10
|
+
* pin for fleet/host/cloud strategies (applied at add/sync time).
|
|
11
|
+
*/
|
|
12
|
+
import type { JobConfig } from './routines.js';
|
|
13
|
+
export type PlacementTarget = {
|
|
14
|
+
mode: 'local';
|
|
15
|
+
} | {
|
|
16
|
+
mode: 'host';
|
|
17
|
+
host: string;
|
|
18
|
+
} | {
|
|
19
|
+
mode: 'cloud';
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Pick one online fleet device for `hostStrategy: fleet`.
|
|
23
|
+
*
|
|
24
|
+
* Preference order:
|
|
25
|
+
* 1. This machine, if online and eligible (avoids needless SSH)
|
|
26
|
+
* 2. First eligible online device by name (stable / deterministic)
|
|
27
|
+
*
|
|
28
|
+
* `config.devices` is the *firing* allowlist only (which daemon may fire the
|
|
29
|
+
* job). It is intentionally NOT used as an execution pool filter — otherwise
|
|
30
|
+
* the double-fire pin (`devices: [self]`) would collapse fleet placement to
|
|
31
|
+
* always-local. Control / offline / no-address devices are never chosen.
|
|
32
|
+
*/
|
|
33
|
+
export declare function pickFleetDevice(_config?: Pick<JobConfig, 'devices'>): string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Resolve where a fired job's body should execute.
|
|
36
|
+
* Throws a human-readable Error when placement cannot be satisfied.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolvePlacementTarget(config: JobConfig): PlacementTarget;
|