@phnx-labs/agents-cli 1.20.93 → 1.21.1
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 +290 -0
- package/README.md +4 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/beta.js +3 -5
- package/dist/commands/doctor.js +23 -0
- package/dist/commands/exec.js +13 -0
- package/dist/commands/feed.js +37 -23
- package/dist/commands/mailboxes.js +39 -1
- package/dist/commands/message.js +12 -1
- package/dist/commands/monitors.js +8 -6
- package/dist/commands/packages.js +14 -5
- package/dist/commands/projects.d.ts +28 -0
- package/dist/commands/projects.js +173 -47
- package/dist/commands/routines.js +27 -1
- package/dist/commands/rules.js +6 -0
- package/dist/commands/sessions.d.ts +17 -0
- package/dist/commands/sessions.js +16 -8
- package/dist/commands/setup-browser.js +5 -1
- package/dist/commands/setup-fleet.js +5 -0
- package/dist/commands/setup-preferences.d.ts +53 -0
- package/dist/commands/setup-preferences.js +142 -0
- package/dist/commands/setup.js +8 -9
- package/dist/commands/ssh.js +220 -10
- package/dist/commands/sync.d.ts +2 -2
- package/dist/commands/sync.js +4 -9
- package/dist/commands/watchdog.js +26 -3
- package/dist/index.js +22 -6
- package/dist/lib/activity.d.ts +13 -0
- package/dist/lib/activity.js +22 -2
- package/dist/lib/auto-dispatch.d.ts +6 -1
- package/dist/lib/auto-dispatch.js +7 -2
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/channels/providers/openclaw-telegram.js +1 -1
- package/dist/lib/channels/resolve.d.ts +18 -1
- package/dist/lib/channels/resolve.js +15 -5
- package/dist/lib/daemon.d.ts +13 -0
- package/dist/lib/daemon.js +80 -60
- package/dist/lib/device-config.d.ts +82 -0
- package/dist/lib/device-config.js +296 -0
- package/dist/lib/event-provenance.d.ts +19 -0
- package/dist/lib/event-provenance.js +48 -0
- package/dist/lib/events.d.ts +2 -2
- package/dist/lib/events.js +4 -53
- package/dist/lib/feed-broadcast.d.ts +41 -5
- package/dist/lib/feed-broadcast.js +110 -12
- package/dist/lib/feed-post.d.ts +16 -2
- package/dist/lib/feed-post.js +35 -7
- package/dist/lib/feed.d.ts +1 -1
- package/dist/lib/feed.js +3 -1
- package/dist/lib/git.d.ts +13 -2
- package/dist/lib/git.js +38 -6
- package/dist/lib/hooks.d.ts +16 -1
- package/dist/lib/hooks.js +61 -1
- package/dist/lib/linear-project-counts.d.ts +72 -5
- package/dist/lib/linear-project-counts.js +90 -6
- package/dist/lib/linear-projects.d.ts +12 -0
- package/dist/lib/linear-projects.js +30 -0
- package/dist/lib/mailbox-gc.js +30 -7
- package/dist/lib/mailbox.d.ts +14 -1
- package/dist/lib/mailbox.js +35 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +5 -21
- package/dist/lib/menubar/install-menubar.js +35 -44
- package/dist/lib/migrate.js +1 -1
- package/dist/lib/models.js +21 -11
- package/dist/lib/monitors/config.d.ts +1 -1
- package/dist/lib/monitors/dispatch.d.ts +6 -2
- package/dist/lib/monitors/dispatch.js +10 -15
- package/dist/lib/notify.d.ts +42 -6
- package/dist/lib/notify.js +41 -32
- package/dist/lib/overdue.d.ts +1 -1
- package/dist/lib/overdue.js +54 -19
- package/dist/lib/project-import.d.ts +96 -0
- package/dist/lib/project-import.js +171 -0
- package/dist/lib/remote-agents-json.d.ts +14 -1
- package/dist/lib/remote-agents-json.js +21 -3
- package/dist/lib/routines.d.ts +53 -0
- package/dist/lib/routines.js +105 -4
- package/dist/lib/rules/run-sync.d.ts +18 -0
- package/dist/lib/rules/run-sync.js +92 -0
- package/dist/lib/scheduler.js +12 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/index.d.ts +4 -5
- package/dist/lib/secrets/index.js +4 -5
- package/dist/lib/session/active.d.ts +16 -2
- package/dist/lib/session/active.js +8 -4
- package/dist/lib/session/bundle.js +5 -1
- package/dist/lib/session/remote-list.d.ts +1 -15
- package/dist/lib/session/remote-list.js +22 -111
- package/dist/lib/session/sync/config.d.ts +13 -5
- package/dist/lib/session/sync/config.js +21 -13
- package/dist/lib/startup/command-registry.d.ts +0 -2
- package/dist/lib/startup/command-registry.js +1 -5
- package/dist/lib/state.d.ts +7 -2
- package/dist/lib/state.js +85 -10
- package/dist/lib/sync-umbrella.d.ts +11 -23
- package/dist/lib/sync-umbrella.js +14 -45
- package/dist/lib/teams/agents.d.ts +35 -3
- package/dist/lib/teams/agents.js +52 -8
- package/dist/lib/teams/scheduler.d.ts +29 -2
- package/dist/lib/teams/scheduler.js +60 -16
- package/dist/lib/types.d.ts +23 -1
- package/package.json +1 -2
- package/dist/commands/drive.d.ts +0 -10
- package/dist/commands/drive.js +0 -183
- package/dist/commands/hq.d.ts +0 -2
- package/dist/commands/hq.js +0 -58
- package/dist/commands/sessions-sync.d.ts +0 -17
- package/dist/commands/sessions-sync.js +0 -135
- package/dist/commands/sync-provision.d.ts +0 -23
- package/dist/commands/sync-provision.js +0 -107
- package/dist/lib/drive-sync.d.ts +0 -45
- package/dist/lib/drive-sync.js +0 -238
- package/dist/lib/hq/floor.d.ts +0 -87
- package/dist/lib/hq/floor.js +0 -243
- package/dist/lib/session/sync/crdt.d.ts +0 -44
- package/dist/lib/session/sync/crdt.js +0 -119
- package/dist/lib/session/sync/manifest.d.ts +0 -62
- package/dist/lib/session/sync/manifest.js +0 -100
- package/dist/lib/session/sync/provision.d.ts +0 -49
- package/dist/lib/session/sync/provision.js +0 -91
- package/dist/lib/session/sync/r2.d.ts +0 -32
- package/dist/lib/session/sync/r2.js +0 -121
- package/dist/lib/session/sync/sync.d.ts +0 -106
- package/dist/lib/session/sync/sync.js +0 -374
|
@@ -25,6 +25,21 @@ import { getCliVersion, resolveAgentsBin, resolveInstalledLayout } from '../vers
|
|
|
25
25
|
const APP_BUNDLE_NAME = 'MenubarHelper.app';
|
|
26
26
|
const INSTALL_DIR_NAME = 'agents-cli';
|
|
27
27
|
const SERVICE_LABEL = 'com.phnx-labs.agents-menubar';
|
|
28
|
+
/**
|
|
29
|
+
* Minimum seconds between launchd restarts of the helper (`ThrottleInterval`).
|
|
30
|
+
*
|
|
31
|
+
* The helper can crash at startup on a loaded machine: `NSApplication.shared`
|
|
32
|
+
* segfaults inside `SLSNewConnection` when WindowServer is too starved to hand
|
|
33
|
+
* out a connection. With `KeepAlive` and no throttle, launchd relaunches on its
|
|
34
|
+
* 10s default, and each attempt spawns a fresh `agents doctor --json` before
|
|
35
|
+
* dying — so a starved box gets hit harder the worse it gets. 30s bounds that
|
|
36
|
+
* respawn rate while staying well inside "the menu bar came back on its own".
|
|
37
|
+
*
|
|
38
|
+
* This only paces the restarts. What actually stops the pile-up is the helper
|
|
39
|
+
* bounding and group-killing its own children (menubar/Sources/MenubarHelper/
|
|
40
|
+
* ChildProcess.swift); the two are complementary, not alternatives.
|
|
41
|
+
*/
|
|
42
|
+
const MENUBAR_THROTTLE_SECONDS = 30;
|
|
28
43
|
function onDarwin() {
|
|
29
44
|
return process.platform === 'darwin';
|
|
30
45
|
}
|
|
@@ -181,41 +196,15 @@ export function codesignVerifies(appPath) {
|
|
|
181
196
|
* A Developer-ID-signed but un-notarized app is rejected by `spctl --assess`,
|
|
182
197
|
* which macOS surfaces as "the app is damaged" and can crash AppKit during
|
|
183
198
|
* launch. This is separate from `codesign --verify`: a signature can be valid
|
|
184
|
-
* while Gatekeeper still refuses to run it.
|
|
199
|
+
* while Gatekeeper still refuses to run it. The release notarizes + staples the
|
|
200
|
+
* helper (menubar/scripts/build.sh, gated by verify-menubar-helper.sh), so a
|
|
201
|
+
* shipped bundle passes this; the launch guards use it to fail loud rather than
|
|
202
|
+
* bootstrap a helper macOS would reject.
|
|
185
203
|
*/
|
|
186
204
|
export function gatekeeperAssesses(appPath) {
|
|
187
205
|
const r = spawnSync('spctl', ['--assess', '--type', 'exec', appPath], { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
188
206
|
return r.status === 0;
|
|
189
207
|
}
|
|
190
|
-
/**
|
|
191
|
-
* Guarantee the installed bundle has a signature Gatekeeper will accept on THIS
|
|
192
|
-
* machine.
|
|
193
|
-
*
|
|
194
|
-
* npm's pack/extract strips the ad-hoc/linker signature the release baked into
|
|
195
|
-
* the helper, leaving `code object is not signed at all`. On macOS 26+ the
|
|
196
|
-
* kernel's code-signing monitor SIGKILLs an unsigned/invalid binary at launch
|
|
197
|
-
* (`SIGKILL (Code Signature Invalid)`), so under the launchd `KeepAlive` service
|
|
198
|
-
* an ad-hoc release helper crash-loops forever and its unstable identity makes
|
|
199
|
-
* the Accessibility grant (needed for the clip→paste keystroke in Clip.swift)
|
|
200
|
-
* re-prompt every time. A fresh ad-hoc re-sign gives the on-disk bytes a
|
|
201
|
-
* matching cdhash, which the kernel accepts.
|
|
202
|
-
*
|
|
203
|
-
* A Developer-ID-signed helper survives npm untouched — its embedded signature
|
|
204
|
-
* still verifies — but if the release was not notarized, Gatekeeper rejects it.
|
|
205
|
-
* In that case we strip the quarantine xattr and re-sign ad-hoc so the helper
|
|
206
|
-
* can launch locally. The stable fix is to notarize the release build; this
|
|
207
|
-
* fallback just prevents a crash-loop while the user is on an un-notarized cut.
|
|
208
|
-
*/
|
|
209
|
-
export function ensureValidSignature(appPath) {
|
|
210
|
-
if (codesignVerifies(appPath) && gatekeeperAssesses(appPath))
|
|
211
|
-
return true;
|
|
212
|
-
// Drop any quarantine/xattrs the tarball round-trip added (they can break
|
|
213
|
-
// both codesign and Gatekeeper), then re-sign ad-hoc under the helper's
|
|
214
|
-
// stable bundle identifier.
|
|
215
|
-
spawnSync('xattr', ['-cr', appPath], { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
216
|
-
spawnSync('codesign', ['--force', '--sign', '-', '--identifier', SERVICE_LABEL, appPath], { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
217
|
-
return codesignVerifies(appPath);
|
|
218
|
-
}
|
|
219
208
|
/**
|
|
220
209
|
* Copy the bundled `.app` to the stable user path (idempotent unless forced).
|
|
221
210
|
* Returns the installed executable path, or null if no source bundle ships
|
|
@@ -229,13 +218,9 @@ export function ensureMenubarAppInstalled(opts = {}) {
|
|
|
229
218
|
return null;
|
|
230
219
|
const dest = installedAppPath();
|
|
231
220
|
if (!opts.forceReinstall && fs.existsSync(dest)) {
|
|
232
|
-
// Self-heal an already-installed bundle whose signature npm stripped on a
|
|
233
|
-
// prior upgrade (macOS 26+ SIGKILLs it otherwise) without a forced recopy.
|
|
234
|
-
ensureValidSignature(dest);
|
|
235
221
|
return installedExecutablePath();
|
|
236
222
|
}
|
|
237
223
|
copyAppBundle(src, dest);
|
|
238
|
-
ensureValidSignature(dest);
|
|
239
224
|
// A fresh copy is exactly when the bundle's icon can be new (first install) or
|
|
240
225
|
// superseded (upgrade) — register it so LaunchServices knows the bundle and can
|
|
241
226
|
// resolve its AppIcon for the left-hand slot of daemon notifications.
|
|
@@ -245,7 +230,7 @@ export function ensureMenubarAppInstalled(opts = {}) {
|
|
|
245
230
|
function xmlEscape(s) {
|
|
246
231
|
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
247
232
|
}
|
|
248
|
-
function generateServicePlist(execPath) {
|
|
233
|
+
export function generateServicePlist(execPath) {
|
|
249
234
|
const home = os.homedir();
|
|
250
235
|
const logPath = path.join(getHelpersDir(), 'menubar', 'menubar.log');
|
|
251
236
|
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
@@ -281,6 +266,8 @@ function generateServicePlist(execPath) {
|
|
|
281
266
|
<true/>
|
|
282
267
|
<key>KeepAlive</key>
|
|
283
268
|
<true/>
|
|
269
|
+
<key>ThrottleInterval</key>
|
|
270
|
+
<integer>${MENUBAR_THROTTLE_SECONDS}</integer>
|
|
284
271
|
<key>ProcessType</key>
|
|
285
272
|
<string>Interactive</string>
|
|
286
273
|
<key>StandardOutPath</key>
|
|
@@ -331,12 +318,15 @@ export function enableMenubarService(opts = { clearOptOut: true }) {
|
|
|
331
318
|
const exec = ensureMenubarAppInstalled({ forceReinstall: true });
|
|
332
319
|
if (!exec)
|
|
333
320
|
return false;
|
|
334
|
-
// Never bootstrap a helper
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
if
|
|
339
|
-
|
|
321
|
+
// Never bootstrap a helper macOS will reject at launch: an invalid signature
|
|
322
|
+
// under launchd KeepAlive crash-loops forever, and an un-notarized bundle is
|
|
323
|
+
// rejected by Gatekeeper as "damaged". A shipped helper is Developer-ID signed
|
|
324
|
+
// AND notarized (release build + the verify-menubar-helper.sh prepack gate), so
|
|
325
|
+
// this passes; if it ever doesn't, skip the service and point at the upgrade
|
|
326
|
+
// rather than re-signing over it (an ad-hoc re-sign never satisfies Gatekeeper).
|
|
327
|
+
if (!(codesignVerifies(installedAppPath()) && gatekeeperAssesses(installedAppPath()))) {
|
|
328
|
+
process.stderr.write('agents: menu-bar helper is not notarized/valid on this machine; skipping launch. ' +
|
|
329
|
+
'Upgrade to a notarized build (npm i -g @phnx-labs/agents-cli), then `agents menubar setup`.\n');
|
|
340
330
|
return false;
|
|
341
331
|
}
|
|
342
332
|
if (opts.clearOptOut)
|
|
@@ -560,11 +550,12 @@ export function runMenubarSetup() {
|
|
|
560
550
|
return { steps, configured: false, status: getMenubarStatus() };
|
|
561
551
|
}
|
|
562
552
|
step('bundle', before.installedVersion === getCliVersion() ? 'ok' : 'changed', `${installedAppPath()} (${getCliVersion()})`);
|
|
563
|
-
if (!codesignVerifies(installedAppPath())) {
|
|
564
|
-
step('signature', 'failed', '
|
|
553
|
+
if (!(codesignVerifies(installedAppPath()) && gatekeeperAssesses(installedAppPath()))) {
|
|
554
|
+
step('signature', 'failed', 'not notarized/valid on this machine — refusing to start it (Gatekeeper rejects an ' +
|
|
555
|
+
'un-notarized helper as "damaged"). Upgrade to a notarized build of agents-cli.');
|
|
565
556
|
return { steps, configured: false, status: getMenubarStatus() };
|
|
566
557
|
}
|
|
567
|
-
step('signature', 'ok', 'valid');
|
|
558
|
+
step('signature', 'ok', 'valid + notarized');
|
|
568
559
|
// Clear the sticky opt-out: running `setup` is an explicit request for the
|
|
569
560
|
// menu bar, so a stale `menubar disable` must not silently win.
|
|
570
561
|
clearMenubarOptOut();
|
package/dist/lib/migrate.js
CHANGED
|
@@ -1708,7 +1708,7 @@ function migrateVersionResourcesToPatterns() {
|
|
|
1708
1708
|
}
|
|
1709
1709
|
}
|
|
1710
1710
|
if (changed) {
|
|
1711
|
-
const META_HEADER = '# agents-cli metadata\n# Auto-generated - do not edit manually\n# https://github.com/phnx-labs/agents-cli\n\n';
|
|
1711
|
+
const META_HEADER = '# agents-cli metadata\n# Auto-generated - do not edit manually\n# https://github.com/phnx-labs/agents-cli\n# yaml-language-server: $schema=https://raw.githubusercontent.com/phnx-labs/agents-cli/main/apps/cli/schema/agents-yaml.schema.json\n\n';
|
|
1712
1712
|
fs.writeFileSync(metaFile, META_HEADER + yaml.stringify(meta), 'utf-8');
|
|
1713
1713
|
console.error('Migrated agents.yaml versions: entries to pattern format');
|
|
1714
1714
|
}
|
package/dist/lib/models.js
CHANGED
|
@@ -20,7 +20,14 @@ const CACHE_PATH = getModelsCachePath();
|
|
|
20
20
|
* Bump when the extractor logic changes shape in an incompatible way so cached
|
|
21
21
|
* catalogs from older agents-cli builds are re-extracted.
|
|
22
22
|
*/
|
|
23
|
-
const CACHE_SCHEMA_VERSION =
|
|
23
|
+
const CACHE_SCHEMA_VERSION = 3;
|
|
24
|
+
/**
|
|
25
|
+
* How long a cached 0-model extraction is trusted before we retry it. Bounds
|
|
26
|
+
* the self-healing window for a transient failure (mid-install, a broken
|
|
27
|
+
* extractor regex fixed in a later agents-cli release) without falling back
|
|
28
|
+
* to re-extracting -- and re-scanning the whole binary -- on every call.
|
|
29
|
+
*/
|
|
30
|
+
const EMPTY_CATALOG_RETRY_MS = 24 * 60 * 60 * 1000;
|
|
24
31
|
let memoryCache = null;
|
|
25
32
|
function cacheKey(agent, version) {
|
|
26
33
|
return `${agent}@${version}`;
|
|
@@ -884,7 +891,10 @@ export function getModelCatalog(agent, version) {
|
|
|
884
891
|
const key = cacheKey(agent, version);
|
|
885
892
|
const cached = cache.entries[key];
|
|
886
893
|
if (cached && cached.sourcePath === src.path && cached.mtime === mtime) {
|
|
887
|
-
|
|
894
|
+
const isFresh = cached.catalog.models.length > 0 ||
|
|
895
|
+
Date.now() - (cached.attemptedAt ?? 0) < EMPTY_CATALOG_RETRY_MS;
|
|
896
|
+
if (isFresh)
|
|
897
|
+
return cached.catalog;
|
|
888
898
|
}
|
|
889
899
|
let models = [];
|
|
890
900
|
let aliases = {};
|
|
@@ -927,15 +937,15 @@ export function getModelCatalog(agent, version) {
|
|
|
927
937
|
models,
|
|
928
938
|
aliases,
|
|
929
939
|
};
|
|
930
|
-
//
|
|
931
|
-
//
|
|
932
|
-
//
|
|
933
|
-
//
|
|
934
|
-
//
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
940
|
+
// Cache a 0-model extraction too, stamped with when it was attempted, so a
|
|
941
|
+
// broken/mid-install extractor doesn't force a full re-scan of the source
|
|
942
|
+
// binary (up to ~1.85s each for a 230-270MB Claude binary) on every call --
|
|
943
|
+
// `getModelCatalog` runs once per installed version per invocation of
|
|
944
|
+
// commands like `agents view`. It self-heals: the read site above re-tries
|
|
945
|
+
// extraction once EMPTY_CATALOG_RETRY_MS has elapsed, or immediately once
|
|
946
|
+
// the source file's mtime changes (an upgrade/reinstall).
|
|
947
|
+
cache.entries[key] = { sourcePath: src.path, mtime, catalog, attemptedAt: Date.now() };
|
|
948
|
+
saveCache();
|
|
939
949
|
return catalog;
|
|
940
950
|
}
|
|
941
951
|
/**
|
|
@@ -80,7 +80,7 @@ export interface ActionConfig {
|
|
|
80
80
|
timeout?: string;
|
|
81
81
|
/** routine: name of an existing routine to fire. */
|
|
82
82
|
routine?: string;
|
|
83
|
-
/** notify:
|
|
83
|
+
/** notify: override the owner channel (defaults to `notify.owner.channel`). */
|
|
84
84
|
notifyChannel?: string;
|
|
85
85
|
/** webhook-out: URL to POST the event to. */
|
|
86
86
|
url?: string;
|
|
@@ -5,8 +5,12 @@
|
|
|
5
5
|
* action goes through the *same* detached spawn cron and webhook fires use
|
|
6
6
|
* (executeJobDetached, lib/runner.ts) — a monitor never duplicates spawn logic,
|
|
7
7
|
* it synthesizes a JobConfig and hands it to the one dispatch seam. `notify`
|
|
8
|
-
*
|
|
8
|
+
* routes the owner through the one channel seam (sendToOwner → lookupTransport,
|
|
9
|
+
* lib/notify.ts) — recipient from notify.owner, no hardcoded chat id, and an
|
|
10
|
+
* unresolvable channel comes back as `ok: false` instead of exiting the daemon;
|
|
11
|
+
* `webhook-out` POSTs the event.
|
|
9
12
|
*/
|
|
13
|
+
import type { Meta } from '../types.js';
|
|
10
14
|
import type { ActionConfig, MonitorConfig, MonitorEvent } from './config.js';
|
|
11
15
|
/** Outcome of a dispatched action. */
|
|
12
16
|
export interface DispatchResult {
|
|
@@ -25,4 +29,4 @@ export declare function injectEvent(prompt: string, event: MonitorEvent): string
|
|
|
25
29
|
* use. `routine` fires an existing routine with the event injected. `notify` and
|
|
26
30
|
* `webhook-out` are terminal side-effects.
|
|
27
31
|
*/
|
|
28
|
-
export declare function dispatchAction(monitor: MonitorConfig, event: MonitorEvent): Promise<DispatchResult>;
|
|
32
|
+
export declare function dispatchAction(monitor: MonitorConfig, event: MonitorEvent, meta?: Meta): Promise<DispatchResult>;
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
* action goes through the *same* detached spawn cron and webhook fires use
|
|
6
6
|
* (executeJobDetached, lib/runner.ts) — a monitor never duplicates spawn logic,
|
|
7
7
|
* it synthesizes a JobConfig and hands it to the one dispatch seam. `notify`
|
|
8
|
-
*
|
|
8
|
+
* routes the owner through the one channel seam (sendToOwner → lookupTransport,
|
|
9
|
+
* lib/notify.ts) — recipient from notify.owner, no hardcoded chat id, and an
|
|
10
|
+
* unresolvable channel comes back as `ok: false` instead of exiting the daemon;
|
|
11
|
+
* `webhook-out` POSTs the event.
|
|
9
12
|
*/
|
|
10
|
-
import { execFile } from 'child_process';
|
|
11
|
-
import { promisify } from 'util';
|
|
12
13
|
import { executeJobDetached } from '../runner.js';
|
|
13
14
|
import { readJob } from '../routines.js';
|
|
14
|
-
import {
|
|
15
|
-
const execFileAsync = promisify(execFile);
|
|
15
|
+
import { sendToOwner } from '../notify.js';
|
|
16
16
|
/** Replace `{event}` in a prompt with the fired event summary. */
|
|
17
17
|
export function injectEvent(prompt, event) {
|
|
18
18
|
return prompt.replace(/\{event\}/g, event.summary);
|
|
@@ -24,7 +24,7 @@ export function injectEvent(prompt, event) {
|
|
|
24
24
|
* use. `routine` fires an existing routine with the event injected. `notify` and
|
|
25
25
|
* `webhook-out` are terminal side-effects.
|
|
26
26
|
*/
|
|
27
|
-
export async function dispatchAction(monitor, event) {
|
|
27
|
+
export async function dispatchAction(monitor, event, meta) {
|
|
28
28
|
const action = monitor.action;
|
|
29
29
|
if (action.type === 'run') {
|
|
30
30
|
const job = {
|
|
@@ -63,16 +63,11 @@ export async function dispatchAction(monitor, event) {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
if (action.type === 'notify') {
|
|
66
|
-
const
|
|
67
|
-
|
|
66
|
+
const result = await sendToOwner(event.summary, {
|
|
67
|
+
...(meta ? { meta } : {}),
|
|
68
|
+
...(action.notifyChannel ? { channel: action.notifyChannel } : {}),
|
|
68
69
|
});
|
|
69
|
-
|
|
70
|
-
await execFileAsync('openclaw', args);
|
|
71
|
-
return { kind: 'notify', ok: true };
|
|
72
|
-
}
|
|
73
|
-
catch (err) {
|
|
74
|
-
return { kind: 'notify', ok: false, error: err.message };
|
|
75
|
-
}
|
|
70
|
+
return { kind: 'notify', ok: result.ok, ...(result.ok ? {} : { error: result.error }) };
|
|
76
71
|
}
|
|
77
72
|
// webhook-out
|
|
78
73
|
if (!action.url)
|
package/dist/lib/notify.d.ts
CHANGED
|
@@ -1,9 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner notifier — the one seam for "ping the human" messages.
|
|
3
|
+
*
|
|
4
|
+
* Every human-facing owner notification (feed urgent-block dispatch, monitor
|
|
5
|
+
* `notify` action, `agents notify`) funnels through the single channel seam:
|
|
6
|
+
* `lookupTransport(channel, meta).provider.send(text, opts)`. The recipient comes
|
|
7
|
+
* from `notify.owner` in agents.yaml — never a hardcoded chat id — so changing the
|
|
8
|
+
* owner is honoured by every path at once. `notify.transports` picks the actual
|
|
9
|
+
* provider per host (rush telegram on zion, openclaw-telegram on mac-mini).
|
|
10
|
+
* Best-effort: a delivery failure is returned to the caller, never thrown, so a
|
|
11
|
+
* notification hiccup never blocks the agent. That is why this module resolves
|
|
12
|
+
* with `lookupTransport` and not the `die()`-capable `resolveTransport` — the
|
|
13
|
+
* monitor daemon and the feed-dispatch loop call in here, and `process.exit()`
|
|
14
|
+
* would take them down on a typo'd channel name, bypassing their try/catch.
|
|
15
|
+
*/
|
|
1
16
|
import type { OpenBlock } from './feed.js';
|
|
2
|
-
|
|
17
|
+
import type { Meta } from './types.js';
|
|
18
|
+
import type { SendResult } from './channels/registry.js';
|
|
19
|
+
export interface OwnerNotifyOptions {
|
|
20
|
+
/** Config source (defaults to `readMeta()`); lets callers/tests inject it. */
|
|
21
|
+
meta?: Meta;
|
|
22
|
+
/** Override the owner channel from `notify.owner.channel`. */
|
|
3
23
|
channel?: string;
|
|
4
|
-
|
|
5
|
-
/** OpenClaw destination (Telegram chat id). Defaults to Muqsit's chat. */
|
|
24
|
+
/** Override the owner target from `notify.owner.to`. */
|
|
6
25
|
target?: string;
|
|
26
|
+
/** Resolve + build the delivery but do not actually send. */
|
|
7
27
|
dryRun?: boolean;
|
|
8
28
|
}
|
|
9
29
|
export interface NotifyResult {
|
|
@@ -12,6 +32,22 @@ export interface NotifyResult {
|
|
|
12
32
|
error?: string;
|
|
13
33
|
}
|
|
14
34
|
export declare function formatUrgentBlockMessage(block: OpenBlock): string;
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Build openclaw argv for a Telegram send (used by the openclaw-telegram
|
|
37
|
+
* provider and its tests). `target` is required — the recipient is always
|
|
38
|
+
* resolved by the caller, never defaulted to a hardcoded number here.
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildOpenClawNotifyArgs(text: string, opts: {
|
|
41
|
+
target: string;
|
|
42
|
+
channel?: string;
|
|
43
|
+
account?: string;
|
|
44
|
+
}): string[];
|
|
45
|
+
/**
|
|
46
|
+
* Deliver a message to the configured owner through the one channel seam.
|
|
47
|
+
* `channel`/`target` default to `notify.owner.{channel,to}`; `notify.transports`
|
|
48
|
+
* selects the provider per host. A missing owner config or a delivery failure
|
|
49
|
+
* (e.g. openclaw not on PATH) returns a clean `SendResult` error — never a raw
|
|
50
|
+
* ENOENT — so callers surface a consistent, best-effort failure.
|
|
51
|
+
*/
|
|
52
|
+
export declare function sendToOwner(text: string, options?: OwnerNotifyOptions): Promise<SendResult>;
|
|
53
|
+
export declare function notifyUrgentBlock(block: OpenBlock, options?: OwnerNotifyOptions): Promise<NotifyResult>;
|
package/dist/lib/notify.js
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Reuses the OpenClaw Telegram gateway on the local mac-mini (Jeff/`default` bot)
|
|
5
|
-
* instead of raw bot tokens. Notifies once per block (tracked by `notifiedAt`).
|
|
6
|
-
* Best-effort: any openclaw failure is surfaced as a warning, not a hard error,
|
|
7
|
-
* so a notification hiccup never blocks the agent.
|
|
8
|
-
*/
|
|
9
|
-
import { execFile } from 'child_process';
|
|
10
|
-
import { promisify } from 'util';
|
|
11
|
-
const execFileAsync = promisify(execFile);
|
|
1
|
+
import { readMeta } from './state.js';
|
|
2
|
+
import { registerBuiltinProviders } from './channels/providers/index.js';
|
|
3
|
+
import { lookupTransport } from './channels/resolve.js';
|
|
12
4
|
export function formatUrgentBlockMessage(block) {
|
|
13
5
|
const q = block.questions[0];
|
|
14
6
|
const header = q?.header ? `[${q.header}] ` : '';
|
|
@@ -18,11 +10,14 @@ export function formatUrgentBlockMessage(block) {
|
|
|
18
10
|
const cost = block.costOfDelay ?? 'low';
|
|
19
11
|
return `URGENT ${cls.toUpperCase()}${host}: ${header}${text} (cost: ${cost}, id: ${block.blockId})`;
|
|
20
12
|
}
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Build openclaw argv for a Telegram send (used by the openclaw-telegram
|
|
15
|
+
* provider and its tests). `target` is required — the recipient is always
|
|
16
|
+
* resolved by the caller, never defaulted to a hardcoded number here.
|
|
17
|
+
*/
|
|
18
|
+
export function buildOpenClawNotifyArgs(text, opts) {
|
|
19
|
+
const channel = opts.channel ?? 'telegram';
|
|
20
|
+
const account = opts.account ?? 'default';
|
|
26
21
|
return [
|
|
27
22
|
'message',
|
|
28
23
|
'send',
|
|
@@ -31,11 +26,38 @@ export function buildOpenClawNotifyArgs(text, options = {}) {
|
|
|
31
26
|
'--account',
|
|
32
27
|
account,
|
|
33
28
|
'--target',
|
|
34
|
-
target,
|
|
29
|
+
opts.target,
|
|
35
30
|
'--message',
|
|
36
31
|
text,
|
|
37
32
|
];
|
|
38
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Deliver a message to the configured owner through the one channel seam.
|
|
36
|
+
* `channel`/`target` default to `notify.owner.{channel,to}`; `notify.transports`
|
|
37
|
+
* selects the provider per host. A missing owner config or a delivery failure
|
|
38
|
+
* (e.g. openclaw not on PATH) returns a clean `SendResult` error — never a raw
|
|
39
|
+
* ENOENT — so callers surface a consistent, best-effort failure.
|
|
40
|
+
*/
|
|
41
|
+
export async function sendToOwner(text, options = {}) {
|
|
42
|
+
const meta = options.meta ?? readMeta();
|
|
43
|
+
const owner = meta.notify?.owner;
|
|
44
|
+
const channel = options.channel ?? owner?.channel;
|
|
45
|
+
const target = options.target ?? owner?.to;
|
|
46
|
+
if (!channel || !target) {
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
channel: channel ?? 'unknown',
|
|
50
|
+
id: target ?? '',
|
|
51
|
+
error: 'notify.owner.{channel,to} not set in agents.yaml',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
registerBuiltinProviders();
|
|
55
|
+
const { provider, error } = lookupTransport(channel, meta);
|
|
56
|
+
if (!provider) {
|
|
57
|
+
return { ok: false, channel, id: target, error };
|
|
58
|
+
}
|
|
59
|
+
return provider.send(text, { target, dryRun: options.dryRun });
|
|
60
|
+
}
|
|
39
61
|
export async function notifyUrgentBlock(block, options = {}) {
|
|
40
62
|
if (block.notifiedAt) {
|
|
41
63
|
return { ok: true, skipped: true };
|
|
@@ -43,19 +65,6 @@ export async function notifyUrgentBlock(block, options = {}) {
|
|
|
43
65
|
if (options.dryRun) {
|
|
44
66
|
return { ok: true, skipped: true };
|
|
45
67
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
await execFileAsync('which', ['openclaw']);
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
return { ok: false, error: 'openclaw CLI not found on PATH' };
|
|
52
|
-
}
|
|
53
|
-
const text = formatUrgentBlockMessage(block);
|
|
54
|
-
try {
|
|
55
|
-
await execFileAsync('openclaw', buildOpenClawNotifyArgs(text, options));
|
|
56
|
-
return { ok: true };
|
|
57
|
-
}
|
|
58
|
-
catch (err) {
|
|
59
|
-
return { ok: false, error: err.message };
|
|
60
|
-
}
|
|
68
|
+
const result = await sendToOwner(formatUrgentBlockMessage(block), options);
|
|
69
|
+
return result.ok ? { ok: true } : { ok: false, error: result.error };
|
|
61
70
|
}
|
package/dist/lib/overdue.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export interface OverdueJob {
|
|
|
31
31
|
* Returns null when neither is available, which leaves the routine unfloored
|
|
32
32
|
* (previous behaviour) rather than silently skipping it.
|
|
33
33
|
*/
|
|
34
|
-
export declare function routineEffectiveStart(job: JobConfig): Date | null;
|
|
34
|
+
export declare function routineEffectiveStart(job: JobConfig, now?: Date): Date | null;
|
|
35
35
|
/** Return every enabled, recurring job whose most recent expected fire was
|
|
36
36
|
* missed. One-shot jobs are excluded — they fire at most once. */
|
|
37
37
|
export declare function detectOverdueJobs(now?: Date): OverdueJob[];
|
package/dist/lib/overdue.js
CHANGED
|
@@ -13,29 +13,48 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import * as fs from 'fs';
|
|
15
15
|
import { Cron } from 'croner';
|
|
16
|
-
import { listJobs, getLatestRun,
|
|
16
|
+
import { listJobs, getLatestRun, resolveJobFilePath, isPastEndAt, isOneShotRoutine, jobRunsOnThisDevice } from './routines.js';
|
|
17
17
|
import { notifyDesktop } from './menubar/notify-desktop.js';
|
|
18
18
|
// Tolerance between "expected fire" and "recorded run start" — accounts for
|
|
19
19
|
// the small gap between the cron tick and when the runner writes meta.json.
|
|
20
20
|
const GRACE_MS = 60_000;
|
|
21
|
-
const
|
|
21
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
22
|
+
/**
|
|
23
|
+
* Lookback windows, narrowest first. A fixed one-week window silently blinded
|
|
24
|
+
* detection to any cron whose gap exceeds it: `0 9 1,13,25 * *` has 12-day gaps,
|
|
25
|
+
* so `nextRun(now - 7d)` jumped past `now`, the walk returned null, and the
|
|
26
|
+
* routine was never flagged overdue on any device — no missed record, no
|
|
27
|
+
* catch-up, permanently. Monthly, quarterly and annual routines were all in that
|
|
28
|
+
* class.
|
|
29
|
+
*
|
|
30
|
+
* A wider window is only tried when the narrower one found nothing, so a dense
|
|
31
|
+
* schedule (every minute, hourly, daily) never walks more than a week of
|
|
32
|
+
* occurrences. A sparse schedule has few occurrences to walk by definition.
|
|
33
|
+
*/
|
|
34
|
+
const LOOKBACK_WINDOWS_MS = [7 * DAY_MS, 32 * DAY_MS, 93 * DAY_MS, 400 * DAY_MS];
|
|
22
35
|
/** Compute the most recent fire of `pattern` at or before `now`. Croner's
|
|
23
36
|
* `previousRun()` returns the cron instance's own last fire, which is null
|
|
24
37
|
* on a freshly-constructed instance — so we walk `nextRun(cursor)` forward
|
|
25
38
|
* from a week ago and keep the last fire still ≤ now. */
|
|
26
39
|
function previousExpectedFire(cron, now) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
for (const window of LOOKBACK_WINDOWS_MS) {
|
|
41
|
+
let cursor = new Date(now.getTime() - window);
|
|
42
|
+
let last = null;
|
|
43
|
+
// Cap iterations: an every-minute schedule yields ≤ 10080 steps over a week;
|
|
44
|
+
// 20k is a paranoia bound against pathological patterns. Only a schedule
|
|
45
|
+
// that found nothing in the narrower window reaches a wider one, and such a
|
|
46
|
+
// schedule is sparse, so the cap is never the binding constraint.
|
|
47
|
+
for (let i = 0; i < 20000; i++) {
|
|
48
|
+
const next = cron.nextRun(cursor);
|
|
49
|
+
if (!next || next.getTime() > now.getTime())
|
|
50
|
+
break;
|
|
51
|
+
last = next;
|
|
52
|
+
cursor = next;
|
|
53
|
+
}
|
|
54
|
+
if (last)
|
|
55
|
+
return last;
|
|
37
56
|
}
|
|
38
|
-
return
|
|
57
|
+
return null;
|
|
39
58
|
}
|
|
40
59
|
/**
|
|
41
60
|
* When a routine started existing, and therefore the earliest fire it can
|
|
@@ -49,13 +68,17 @@ function previousExpectedFire(cron, now) {
|
|
|
49
68
|
* Returns null when neither is available, which leaves the routine unfloored
|
|
50
69
|
* (previous behaviour) rather than silently skipping it.
|
|
51
70
|
*/
|
|
52
|
-
export function routineEffectiveStart(job) {
|
|
71
|
+
export function routineEffectiveStart(job, now = new Date()) {
|
|
53
72
|
if (job.createdAt) {
|
|
54
73
|
const stamped = new Date(job.createdAt);
|
|
55
|
-
|
|
56
|
-
|
|
74
|
+
// Clamp a future stamp (clock skew, a hand-edited year) to now. Left
|
|
75
|
+
// unclamped it sits after every possible expected fire, so the routine can
|
|
76
|
+
// never be flagged overdue until wall-clock time catches up.
|
|
77
|
+
if (!isNaN(stamped.getTime())) {
|
|
78
|
+
return stamped.getTime() > now.getTime() ? now : stamped;
|
|
79
|
+
}
|
|
57
80
|
}
|
|
58
|
-
const path =
|
|
81
|
+
const path = resolveJobFilePath(job.name);
|
|
59
82
|
if (!path)
|
|
60
83
|
return null;
|
|
61
84
|
try {
|
|
@@ -70,7 +93,19 @@ export function routineEffectiveStart(job) {
|
|
|
70
93
|
export function detectOverdueJobs(now = new Date()) {
|
|
71
94
|
const overdue = [];
|
|
72
95
|
for (const job of listJobs()) {
|
|
73
|
-
if (!job.enabled
|
|
96
|
+
if (!job.enabled)
|
|
97
|
+
continue;
|
|
98
|
+
// One-shot: fires at most once, so a missed slot is not a backlog to replay.
|
|
99
|
+
// Use the same predicate the scheduler does — the raw `runOnce` flag alone
|
|
100
|
+
// missed a one-shot-LIKE schedule (a fixed minute/hour/day/month) that never
|
|
101
|
+
// carried the flag.
|
|
102
|
+
if (isOneShotRoutine(job))
|
|
103
|
+
continue;
|
|
104
|
+
// Past its configured end: catch-up must not resurrect a routine the author
|
|
105
|
+
// already retired. The scheduler only auto-disables lazily, inside a live
|
|
106
|
+
// cron tick, so a routine whose endAt elapsed while the daemon was down is
|
|
107
|
+
// still enabled on disk when the catch-up pass runs.
|
|
108
|
+
if (isPastEndAt(job, now))
|
|
74
109
|
continue;
|
|
75
110
|
// Trigger-only jobs (no cron schedule) never have an expected fire time.
|
|
76
111
|
if (!job.schedule)
|
|
@@ -99,7 +134,7 @@ export function detectOverdueJobs(now = new Date()) {
|
|
|
99
134
|
// a miss. Without this, any newly created routine on a daily/weekly cron is
|
|
100
135
|
// instantly "overdue" for the previous occurrence — and with auto-catchup
|
|
101
136
|
// that means `agents routines add` runs the routine once, immediately.
|
|
102
|
-
const start = routineEffectiveStart(job);
|
|
137
|
+
const start = routineEffectiveStart(job, now);
|
|
103
138
|
if (start && expected.getTime() < start.getTime())
|
|
104
139
|
continue;
|
|
105
140
|
const latest = getLatestRun(job.name);
|