@phnx-labs/agents-cli 1.20.92 → 1.21.0
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 +385 -0
- package/README.md +3 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/beta.js +3 -5
- package/dist/commands/doctor.js +23 -0
- package/dist/commands/events.js +91 -1
- package/dist/commands/exec.js +13 -0
- package/dist/commands/mailboxes.js +39 -1
- package/dist/commands/message.js +12 -1
- package/dist/commands/packages.js +14 -5
- package/dist/commands/projects.d.ts +38 -0
- package/dist/commands/projects.js +355 -48
- package/dist/commands/routines.js +27 -1
- package/dist/commands/rules.js +6 -0
- package/dist/commands/secrets.d.ts +17 -0
- package/dist/commands/secrets.js +198 -7
- package/dist/commands/send.d.ts +14 -12
- package/dist/commands/send.js +105 -35
- 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 +13 -12
- package/dist/commands/view.js +4 -0
- package/dist/commands/watchdog.js +26 -3
- package/dist/index.js +38 -6
- package/dist/lib/activity.d.ts +15 -0
- package/dist/lib/activity.js +26 -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/send.d.ts +83 -0
- package/dist/lib/channels/send.js +112 -0
- 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-ingest.d.ts +46 -0
- package/dist/lib/events-ingest.js +182 -0
- package/dist/lib/events.d.ts +17 -5
- package/dist/lib/events.js +59 -56
- package/dist/lib/feed.d.ts +1 -1
- package/dist/lib/feed.js +2 -0
- package/dist/lib/git.d.ts +13 -2
- package/dist/lib/git.js +38 -6
- package/dist/lib/linear-project-counts.d.ts +129 -0
- package/dist/lib/linear-project-counts.js +206 -0
- package/dist/lib/linear-projects.d.ts +62 -0
- package/dist/lib/linear-projects.js +144 -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/menubar/notify-desktop.d.ts +17 -2
- package/dist/lib/menubar/notify-desktop.js +8 -2
- package/dist/lib/migrate.js +1 -1
- package/dist/lib/models.js +21 -11
- 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/project-probe.d.ts +75 -0
- package/dist/lib/project-probe.js +160 -0
- package/dist/lib/project-resources.d.ts +8 -0
- package/dist/lib/project-resources.js +31 -3
- package/dist/lib/project-status.d.ts +32 -1
- package/dist/lib/project-status.js +82 -1
- package/dist/lib/projects.d.ts +6 -0
- package/dist/lib/projects.js +12 -0
- package/dist/lib/remote-agents-json.d.ts +14 -1
- package/dist/lib/remote-agents-json.js +21 -3
- package/dist/lib/routine-notify.d.ts +11 -0
- package/dist/lib/routine-notify.js +22 -0
- 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/run-notify.js +3 -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/audit.d.ts +1 -1
- package/dist/lib/secrets/audit.js +53 -10
- package/dist/lib/secrets/index.d.ts +4 -5
- package/dist/lib/secrets/index.js +4 -5
- package/dist/lib/secrets/list-filter.d.ts +20 -5
- package/dist/lib/secrets/list-filter.js +22 -6
- package/dist/lib/secrets/usage-db.d.ts +106 -0
- package/dist/lib/secrets/usage-db.js +236 -0
- 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-active.d.ts +5 -1
- package/dist/lib/session/remote-active.js +4 -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/sqlite.js +28 -1
- 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 +19 -2
- package/dist/lib/state.js +99 -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 +22 -5
- package/dist/lib/versions.d.ts +6 -0
- package/dist/lib/versions.js +6 -4
- 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
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** The minimal Linear project shape the link flow needs (id + name + url). */
|
|
2
|
+
export interface LinearProjectLite {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
/** Project URL, when the `linear` CLI JSON carries one. Never fabricated. */
|
|
6
|
+
url?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Collapse a Linear name / repo slug / folder path to one comparison key:
|
|
10
|
+
* lowercase, keep only the last path segment, strip separators.
|
|
11
|
+
* "Agents CLI" -> "agentscli"
|
|
12
|
+
* "phnx-labs/agents-cli" -> "agentscli"
|
|
13
|
+
* "~/src/.../agents-cli" -> "agentscli"
|
|
14
|
+
*/
|
|
15
|
+
export declare function normalizeProjectKey(s: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Find the Linear project that best matches a repo slug or folder name.
|
|
18
|
+
* Exact normalized match first, then a containment fallback (either direction),
|
|
19
|
+
* so "agents-cli-web" still suggests "Agents CLI" when no exact peer exists.
|
|
20
|
+
*
|
|
21
|
+
* Kept for parity with the Factory original — the `link` command uses
|
|
22
|
+
* {@link pickLinearProject} instead: this one returns the FIRST match (silent
|
|
23
|
+
* on duplicate names), which is fine for a UI suggestion but never for a write
|
|
24
|
+
* path.
|
|
25
|
+
*/
|
|
26
|
+
export declare function matchLinearProject(slugOrName: string, projects: LinearProjectLite[]): LinearProjectLite | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Find the local checkout directory a Linear project name refers to, on EXACT
|
|
29
|
+
* key equality only — never containment. `matchLinearProject`'s containment
|
|
30
|
+
* fallback is right for suggesting a link a human then confirms; this backs
|
|
31
|
+
* `projects import --from-linear`, which writes `root`/`repo` with nobody
|
|
32
|
+
* looking, and "Agents CLI" must not silently bind `agents-cli-web`.
|
|
33
|
+
*
|
|
34
|
+
* Returns `undefined` when nothing matches, and also when SEVERAL dirs key to
|
|
35
|
+
* the same value (`agents-cli` and `agents_cli`) — an ambiguous match is not a
|
|
36
|
+
* match.
|
|
37
|
+
*/
|
|
38
|
+
export declare function matchLocalCheckoutExact(name: string, dirNames: string[]): string | undefined;
|
|
39
|
+
/** The outcome of picking one Linear project out of the workspace list. */
|
|
40
|
+
export type LinearPick = {
|
|
41
|
+
kind: 'match';
|
|
42
|
+
project: LinearProjectLite;
|
|
43
|
+
} | {
|
|
44
|
+
kind: 'candidates';
|
|
45
|
+
projects: LinearProjectLite[];
|
|
46
|
+
} | {
|
|
47
|
+
kind: 'none';
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Pick the Linear project a query refers to. An exact id or exact normalized
|
|
51
|
+
* name match is confident enough to write; anything weaker (several exact-name
|
|
52
|
+
* peers, or only containment matches) comes back as a candidate LIST for the
|
|
53
|
+
* user to disambiguate — the link command never guesses on a weak signal.
|
|
54
|
+
*/
|
|
55
|
+
export declare function pickLinearProject(query: string, projects: LinearProjectLite[]): LinearPick;
|
|
56
|
+
/**
|
|
57
|
+
* List the workspace's Linear projects via the `linear` CLI on PATH. Throws a
|
|
58
|
+
* clear error when the binary is missing, errors, or returns a shape we can't
|
|
59
|
+
* use — this backs an explicit user command, so a silent empty list would send
|
|
60
|
+
* the user down a wrong "no matches" path.
|
|
61
|
+
*/
|
|
62
|
+
export declare function listLinearProjects(): LinearProjectLite[];
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Linear project matching for `agents projects link --linear`.
|
|
2
|
+
//
|
|
3
|
+
// A project's identity shows up three ways — a Linear project name ("Agents CLI"),
|
|
4
|
+
// a GitHub repo slug ("phnx-labs/agents-cli"), and a filesystem folder
|
|
5
|
+
// (".../agents-cli"). normalizeProjectKey() collapses all three to one comparison
|
|
6
|
+
// key so they compare equal, and matchLinearProject() binds a repo/folder to the
|
|
7
|
+
// Linear project the user most likely means.
|
|
8
|
+
//
|
|
9
|
+
// Ported from apps/factory/src/core/linearProjects.ts (no cross-package imports —
|
|
10
|
+
// repo rule); keep the two in sync. The matcher half is PURE so it unit-tests
|
|
11
|
+
// without a live `linear` binary; the `linear projects --json` shell-out lives at
|
|
12
|
+
// the bottom (listLinearProjects) and fails LOUD — it's behind an explicit user
|
|
13
|
+
// command, not a best-effort card enrichment.
|
|
14
|
+
import { execFileSync } from 'child_process';
|
|
15
|
+
/**
|
|
16
|
+
* Collapse a Linear name / repo slug / folder path to one comparison key:
|
|
17
|
+
* lowercase, keep only the last path segment, strip separators.
|
|
18
|
+
* "Agents CLI" -> "agentscli"
|
|
19
|
+
* "phnx-labs/agents-cli" -> "agentscli"
|
|
20
|
+
* "~/src/.../agents-cli" -> "agentscli"
|
|
21
|
+
*/
|
|
22
|
+
export function normalizeProjectKey(s) {
|
|
23
|
+
const last = s.toLowerCase().split('/').filter(Boolean).pop() ?? '';
|
|
24
|
+
return last.replace(/[-_\s.]/g, '');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Find the Linear project that best matches a repo slug or folder name.
|
|
28
|
+
* Exact normalized match first, then a containment fallback (either direction),
|
|
29
|
+
* so "agents-cli-web" still suggests "Agents CLI" when no exact peer exists.
|
|
30
|
+
*
|
|
31
|
+
* Kept for parity with the Factory original — the `link` command uses
|
|
32
|
+
* {@link pickLinearProject} instead: this one returns the FIRST match (silent
|
|
33
|
+
* on duplicate names), which is fine for a UI suggestion but never for a write
|
|
34
|
+
* path.
|
|
35
|
+
*/
|
|
36
|
+
export function matchLinearProject(slugOrName, projects) {
|
|
37
|
+
const key = normalizeProjectKey(slugOrName);
|
|
38
|
+
if (!key)
|
|
39
|
+
return undefined;
|
|
40
|
+
const exact = projects.find((p) => normalizeProjectKey(p.name) === key);
|
|
41
|
+
if (exact)
|
|
42
|
+
return exact;
|
|
43
|
+
return projects.find((p) => {
|
|
44
|
+
const pk = normalizeProjectKey(p.name);
|
|
45
|
+
return pk.length > 0 && (pk.includes(key) || key.includes(pk));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Collapse a Linear **display name** or a directory basename to one comparison
|
|
50
|
+
* key. Deliberately NOT `normalizeProjectKey`: that one keeps only the segment
|
|
51
|
+
* after the last `/`, which is right for an `owner/repo` slug or a filesystem
|
|
52
|
+
* path and wrong for a display name, where `/` is ordinary punctuation. Keying
|
|
53
|
+
* "Rush / Web" the path way yields `web`, which exact-matches an unrelated
|
|
54
|
+
* `web/` checkout — the precise silent mis-binding this whole match path exists
|
|
55
|
+
* to prevent.
|
|
56
|
+
*/
|
|
57
|
+
function checkoutMatchKey(s) {
|
|
58
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, '');
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Find the local checkout directory a Linear project name refers to, on EXACT
|
|
62
|
+
* key equality only — never containment. `matchLinearProject`'s containment
|
|
63
|
+
* fallback is right for suggesting a link a human then confirms; this backs
|
|
64
|
+
* `projects import --from-linear`, which writes `root`/`repo` with nobody
|
|
65
|
+
* looking, and "Agents CLI" must not silently bind `agents-cli-web`.
|
|
66
|
+
*
|
|
67
|
+
* Returns `undefined` when nothing matches, and also when SEVERAL dirs key to
|
|
68
|
+
* the same value (`agents-cli` and `agents_cli`) — an ambiguous match is not a
|
|
69
|
+
* match.
|
|
70
|
+
*/
|
|
71
|
+
export function matchLocalCheckoutExact(name, dirNames) {
|
|
72
|
+
const key = checkoutMatchKey(name);
|
|
73
|
+
if (!key)
|
|
74
|
+
return undefined;
|
|
75
|
+
const hits = dirNames.filter((d) => checkoutMatchKey(d) === key);
|
|
76
|
+
return hits.length === 1 ? hits[0] : undefined;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Pick the Linear project a query refers to. An exact id or exact normalized
|
|
80
|
+
* name match is confident enough to write; anything weaker (several exact-name
|
|
81
|
+
* peers, or only containment matches) comes back as a candidate LIST for the
|
|
82
|
+
* user to disambiguate — the link command never guesses on a weak signal.
|
|
83
|
+
*/
|
|
84
|
+
export function pickLinearProject(query, projects) {
|
|
85
|
+
const q = query.trim();
|
|
86
|
+
if (!q)
|
|
87
|
+
return { kind: 'none' };
|
|
88
|
+
const byId = projects.find((p) => p.id === q);
|
|
89
|
+
if (byId)
|
|
90
|
+
return { kind: 'match', project: byId };
|
|
91
|
+
const key = normalizeProjectKey(q);
|
|
92
|
+
if (!key)
|
|
93
|
+
return { kind: 'none' };
|
|
94
|
+
const exact = projects.filter((p) => normalizeProjectKey(p.name) === key);
|
|
95
|
+
if (exact.length === 1)
|
|
96
|
+
return { kind: 'match', project: exact[0] };
|
|
97
|
+
if (exact.length > 1)
|
|
98
|
+
return { kind: 'candidates', projects: exact };
|
|
99
|
+
const containment = projects.filter((p) => {
|
|
100
|
+
const pk = normalizeProjectKey(p.name);
|
|
101
|
+
return pk.length > 0 && (pk.includes(key) || key.includes(pk));
|
|
102
|
+
});
|
|
103
|
+
return containment.length > 0 ? { kind: 'candidates', projects: containment } : { kind: 'none' };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* List the workspace's Linear projects via the `linear` CLI on PATH. Throws a
|
|
107
|
+
* clear error when the binary is missing, errors, or returns a shape we can't
|
|
108
|
+
* use — this backs an explicit user command, so a silent empty list would send
|
|
109
|
+
* the user down a wrong "no matches" path.
|
|
110
|
+
*/
|
|
111
|
+
export function listLinearProjects() {
|
|
112
|
+
let out;
|
|
113
|
+
try {
|
|
114
|
+
out = execFileSync('linear', ['projects', '--json'], {
|
|
115
|
+
encoding: 'utf8',
|
|
116
|
+
timeout: 8000,
|
|
117
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
throw new Error('Could not list Linear projects — is the `linear` CLI installed and logged in? (`brew install linear-cli`, `linear auth login`)');
|
|
122
|
+
}
|
|
123
|
+
let parsed;
|
|
124
|
+
try {
|
|
125
|
+
parsed = JSON.parse(out);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
throw new Error('`linear projects --json` returned invalid JSON');
|
|
129
|
+
}
|
|
130
|
+
if (!Array.isArray(parsed))
|
|
131
|
+
throw new Error('`linear projects --json` did not return a list');
|
|
132
|
+
return parsed.flatMap((x) => {
|
|
133
|
+
if (x && typeof x === 'object' && !Array.isArray(x)) {
|
|
134
|
+
const o = x;
|
|
135
|
+
if (typeof o.id === 'string' && typeof o.name === 'string') {
|
|
136
|
+
const p = { id: o.id, name: o.name };
|
|
137
|
+
if (typeof o.url === 'string')
|
|
138
|
+
p.url = o.url;
|
|
139
|
+
return [p];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return [];
|
|
143
|
+
});
|
|
144
|
+
}
|
package/dist/lib/mailbox-gc.js
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
|
-
import { getMailboxRootDir } from './state.js';
|
|
11
|
+
import { getFeedDir, getMailboxRootDir } from './state.js';
|
|
12
12
|
import { mailboxDir, isValidMailboxId, readMessage, sweepExpired, } from './mailbox.js';
|
|
13
|
-
import { listBlocks, removeBlock } from './feed.js';
|
|
13
|
+
import { listBlocks, removeBlock, recordMessageReceipt } from './feed.js';
|
|
14
14
|
const DEFAULT_MAX_CONSUMED_AGE_MINUTES = 24 * 60;
|
|
15
15
|
function consumedAgeMinutes(file, now) {
|
|
16
16
|
try {
|
|
@@ -31,7 +31,7 @@ function jsonFiles(dir) {
|
|
|
31
31
|
}
|
|
32
32
|
return names.filter((n) => n.endsWith('.json')).sort();
|
|
33
33
|
}
|
|
34
|
-
function archiveAllPending(boxDir, reason) {
|
|
34
|
+
function archiveAllPending(boxDir, reason, feedRoot) {
|
|
35
35
|
let n = 0;
|
|
36
36
|
for (const dir of [path.join(boxDir, 'inbox'), path.join(boxDir, 'processing')]) {
|
|
37
37
|
for (const name of jsonFiles(dir)) {
|
|
@@ -45,6 +45,14 @@ function archiveAllPending(boxDir, reason) {
|
|
|
45
45
|
fs.writeFileSync(tmp, JSON.stringify(msg, null, 2), 'utf-8');
|
|
46
46
|
fs.renameSync(tmp, dest);
|
|
47
47
|
fs.unlinkSync(src);
|
|
48
|
+
if (msg.blockId) {
|
|
49
|
+
try {
|
|
50
|
+
recordMessageReceipt(msg.blockId, { msgId: msg.msgId, status: reason === 'expired' ? 'expired' : 'dropped', at: new Date().toISOString(), from: msg.from }, feedRoot);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Receipt surfacing is best-effort; never stall GC.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
48
56
|
}
|
|
49
57
|
else {
|
|
50
58
|
fs.renameSync(src, dest);
|
|
@@ -58,6 +66,16 @@ function archiveAllPending(boxDir, reason) {
|
|
|
58
66
|
}
|
|
59
67
|
return n;
|
|
60
68
|
}
|
|
69
|
+
function blockAgeMinutes(blockId, feedRoot, now) {
|
|
70
|
+
const file = path.join(feedRoot ?? getFeedDir(), `${blockId}.json`);
|
|
71
|
+
try {
|
|
72
|
+
const stat = fs.statSync(file);
|
|
73
|
+
return (now.getTime() - stat.mtimeMs) / 60_000;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return Number.POSITIVE_INFINITY;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
61
79
|
function pruneConsumed(boxDir, maxAgeMinutes, now) {
|
|
62
80
|
let n = 0;
|
|
63
81
|
const consumed = path.join(boxDir, 'consumed');
|
|
@@ -115,7 +133,7 @@ export function gcMailbox(activeBoxIds, options = {}) {
|
|
|
115
133
|
const boxDir = mailboxDir(name, root);
|
|
116
134
|
if (!activeBoxIds.has(name)) {
|
|
117
135
|
result.deadBoxes++;
|
|
118
|
-
result.messagesDroppedDead += archiveAllPending(boxDir, 'dead');
|
|
136
|
+
result.messagesDroppedDead += archiveAllPending(boxDir, 'dead', feedRoot);
|
|
119
137
|
result.consumedPruned += pruneConsumed(boxDir, maxConsumedAgeMinutes, now);
|
|
120
138
|
// Also prune the empty box dir if it is now empty.
|
|
121
139
|
try {
|
|
@@ -136,13 +154,18 @@ export function gcMailbox(activeBoxIds, options = {}) {
|
|
|
136
154
|
else {
|
|
137
155
|
// Live box: archive expired messages (same path as drain/peek) so GC does
|
|
138
156
|
// not leave expired files in inbox/processing while only bumping metrics.
|
|
139
|
-
result.messagesDroppedExpired += sweepExpired(boxDir, name, now);
|
|
157
|
+
result.messagesDroppedExpired += sweepExpired(boxDir, name, now, feedRoot);
|
|
140
158
|
result.consumedPruned += pruneConsumed(boxDir, maxConsumedAgeMinutes, now);
|
|
141
159
|
}
|
|
142
160
|
}
|
|
143
161
|
for (const blockId of blocksToRemove) {
|
|
144
|
-
|
|
145
|
-
|
|
162
|
+
// A dead box's block is kept for up to maxConsumedAgeMinutes after a bounce
|
|
163
|
+
// receipt is written so the operator/sender can see the failure. Stale blocks
|
|
164
|
+
// (older than the prune window) are removed.
|
|
165
|
+
if (blockAgeMinutes(blockId, feedRoot, now) >= maxConsumedAgeMinutes) {
|
|
166
|
+
if (removeBlock(blockId, feedRoot)) {
|
|
167
|
+
result.blocksRemoved++;
|
|
168
|
+
}
|
|
146
169
|
}
|
|
147
170
|
}
|
|
148
171
|
return result;
|
package/dist/lib/mailbox.d.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/** Default delivery TTL for messages enqueued without an explicit one (24 hours). */
|
|
2
|
+
export declare const DEFAULT_TTL_SECONDS: number;
|
|
3
|
+
/** Env var that overrides {@link DEFAULT_TTL_SECONDS} with a duration like "24h" or "3600". */
|
|
4
|
+
export declare const MAILBOX_TTL_ENV = "AGENTS_MAILBOX_TTL";
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the default mailbox TTL in seconds. Honors `AGENTS_MAILBOX_TTL`
|
|
7
|
+
* (parsed by {@link parseDuration}); falls back to 24h. A malformed env value
|
|
8
|
+
* fails loud instead of silently disabling expiry.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveDefaultTtlSeconds(): number;
|
|
1
11
|
/** A single mailbox message. `text` may embed `host:/path` clip tokens. */
|
|
2
12
|
export interface MailboxMessage {
|
|
3
13
|
/** Unique, time-sortable id. Also the on-disk filename stem. */
|
|
@@ -54,8 +64,11 @@ export declare function isExpired(msg: MailboxMessage, now?: Date): boolean;
|
|
|
54
64
|
/**
|
|
55
65
|
* Move expired messages from inbox/ and processing/ into consumed/ with a
|
|
56
66
|
* `dropped: expired` marker. Called by drain/peek before returning messages.
|
|
67
|
+
*
|
|
68
|
+
* When a dropped message carries a `blockId`, a failure receipt is surfaced
|
|
69
|
+
* back to the feed store so the sender sees the bounce.
|
|
57
70
|
*/
|
|
58
|
-
export declare function sweepExpired(boxDir: string, boxId?: string, now?: Date): number;
|
|
71
|
+
export declare function sweepExpired(boxDir: string, boxId?: string, now?: Date, feedRoot?: string): number;
|
|
59
72
|
/**
|
|
60
73
|
* Drain the box: return every pending message addressed to it, in FIFO order,
|
|
61
74
|
* removing them from the queue. Claim-first (inbox → processing → consumed) so
|
package/dist/lib/mailbox.js
CHANGED
|
@@ -20,6 +20,26 @@ import * as path from 'path';
|
|
|
20
20
|
import { randomUUID } from 'crypto';
|
|
21
21
|
import { getMailboxRootDir } from './state.js';
|
|
22
22
|
import { recordMessageReceipt } from './feed.js';
|
|
23
|
+
import { parseDuration } from './hooks/cache.js';
|
|
24
|
+
/** Default delivery TTL for messages enqueued without an explicit one (24 hours). */
|
|
25
|
+
export const DEFAULT_TTL_SECONDS = 24 * 60 * 60;
|
|
26
|
+
/** Env var that overrides {@link DEFAULT_TTL_SECONDS} with a duration like "24h" or "3600". */
|
|
27
|
+
export const MAILBOX_TTL_ENV = 'AGENTS_MAILBOX_TTL';
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the default mailbox TTL in seconds. Honors `AGENTS_MAILBOX_TTL`
|
|
30
|
+
* (parsed by {@link parseDuration}); falls back to 24h. A malformed env value
|
|
31
|
+
* fails loud instead of silently disabling expiry.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveDefaultTtlSeconds() {
|
|
34
|
+
const raw = process.env[MAILBOX_TTL_ENV];
|
|
35
|
+
if (raw == null || raw === '')
|
|
36
|
+
return DEFAULT_TTL_SECONDS;
|
|
37
|
+
const parsed = parseDuration(raw);
|
|
38
|
+
if (parsed == null || parsed <= 0) {
|
|
39
|
+
throw new Error(`Invalid ${MAILBOX_TTL_ENV}=${JSON.stringify(raw)}: expected a positive duration (e.g. 24h, 30m, 3600).`);
|
|
40
|
+
}
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
23
43
|
/**
|
|
24
44
|
* A mailboxId must be a single, separator-free path segment. Real ids (session
|
|
25
45
|
* UUID / teams agentId / loop runId) already satisfy this; rejecting anything
|
|
@@ -78,8 +98,9 @@ export function enqueue(boxDir, msg) {
|
|
|
78
98
|
text: msg.text,
|
|
79
99
|
blockId: msg.blockId,
|
|
80
100
|
};
|
|
81
|
-
|
|
82
|
-
|
|
101
|
+
const ttlSeconds = msg.ttlSeconds ?? resolveDefaultTtlSeconds();
|
|
102
|
+
if (ttlSeconds > 0) {
|
|
103
|
+
record.expiresAt = new Date(now.getTime() + ttlSeconds * 1000).toISOString();
|
|
83
104
|
}
|
|
84
105
|
const target = path.join(inboxDir(boxDir), `${msgId}.json`);
|
|
85
106
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
@@ -139,8 +160,11 @@ function archiveDropped(boxDir, name, reason) {
|
|
|
139
160
|
/**
|
|
140
161
|
* Move expired messages from inbox/ and processing/ into consumed/ with a
|
|
141
162
|
* `dropped: expired` marker. Called by drain/peek before returning messages.
|
|
163
|
+
*
|
|
164
|
+
* When a dropped message carries a `blockId`, a failure receipt is surfaced
|
|
165
|
+
* back to the feed store so the sender sees the bounce.
|
|
142
166
|
*/
|
|
143
|
-
export function sweepExpired(boxDir, boxId = path.basename(boxDir), now = new Date()) {
|
|
167
|
+
export function sweepExpired(boxDir, boxId = path.basename(boxDir), now = new Date(), feedRoot) {
|
|
144
168
|
ensureDirs(boxDir);
|
|
145
169
|
let n = 0;
|
|
146
170
|
for (const dir of [inboxDir(boxDir), processingDir(boxDir)]) {
|
|
@@ -154,6 +178,14 @@ export function sweepExpired(boxDir, boxId = path.basename(boxDir), now = new Da
|
|
|
154
178
|
fs.writeFileSync(tmp, JSON.stringify(msg, null, 2), 'utf-8');
|
|
155
179
|
fs.renameSync(tmp, dest);
|
|
156
180
|
fs.unlinkSync(path.join(dir, name));
|
|
181
|
+
if (msg.blockId) {
|
|
182
|
+
try {
|
|
183
|
+
recordMessageReceipt(msg.blockId, { msgId: msg.msgId, status: 'expired', at: new Date().toISOString(), from: msg.from }, feedRoot ?? process.env.AGENTS_FEED_DIR);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Receipt surfacing is best-effort; never stall expiry.
|
|
187
|
+
}
|
|
188
|
+
}
|
|
157
189
|
n++;
|
|
158
190
|
}
|
|
159
191
|
catch {
|
|
Binary file
|
|
Binary file
|
|
@@ -34,29 +34,12 @@ export declare function codesignVerifies(appPath: string): boolean;
|
|
|
34
34
|
* A Developer-ID-signed but un-notarized app is rejected by `spctl --assess`,
|
|
35
35
|
* which macOS surfaces as "the app is damaged" and can crash AppKit during
|
|
36
36
|
* launch. This is separate from `codesign --verify`: a signature can be valid
|
|
37
|
-
* while Gatekeeper still refuses to run it.
|
|
37
|
+
* while Gatekeeper still refuses to run it. The release notarizes + staples the
|
|
38
|
+
* helper (menubar/scripts/build.sh, gated by verify-menubar-helper.sh), so a
|
|
39
|
+
* shipped bundle passes this; the launch guards use it to fail loud rather than
|
|
40
|
+
* bootstrap a helper macOS would reject.
|
|
38
41
|
*/
|
|
39
42
|
export declare function gatekeeperAssesses(appPath: string): boolean;
|
|
40
|
-
/**
|
|
41
|
-
* Guarantee the installed bundle has a signature Gatekeeper will accept on THIS
|
|
42
|
-
* machine.
|
|
43
|
-
*
|
|
44
|
-
* npm's pack/extract strips the ad-hoc/linker signature the release baked into
|
|
45
|
-
* the helper, leaving `code object is not signed at all`. On macOS 26+ the
|
|
46
|
-
* kernel's code-signing monitor SIGKILLs an unsigned/invalid binary at launch
|
|
47
|
-
* (`SIGKILL (Code Signature Invalid)`), so under the launchd `KeepAlive` service
|
|
48
|
-
* an ad-hoc release helper crash-loops forever and its unstable identity makes
|
|
49
|
-
* the Accessibility grant (needed for the clip→paste keystroke in Clip.swift)
|
|
50
|
-
* re-prompt every time. A fresh ad-hoc re-sign gives the on-disk bytes a
|
|
51
|
-
* matching cdhash, which the kernel accepts.
|
|
52
|
-
*
|
|
53
|
-
* A Developer-ID-signed helper survives npm untouched — its embedded signature
|
|
54
|
-
* still verifies — but if the release was not notarized, Gatekeeper rejects it.
|
|
55
|
-
* In that case we strip the quarantine xattr and re-sign ad-hoc so the helper
|
|
56
|
-
* can launch locally. The stable fix is to notarize the release build; this
|
|
57
|
-
* fallback just prevents a crash-loop while the user is on an un-notarized cut.
|
|
58
|
-
*/
|
|
59
|
-
export declare function ensureValidSignature(appPath: string): boolean;
|
|
60
43
|
/**
|
|
61
44
|
* Copy the bundled `.app` to the stable user path (idempotent unless forced).
|
|
62
45
|
* Returns the installed executable path, or null if no source bundle ships
|
|
@@ -65,6 +48,7 @@ export declare function ensureValidSignature(appPath: string): boolean;
|
|
|
65
48
|
export declare function ensureMenubarAppInstalled(opts?: {
|
|
66
49
|
forceReinstall?: boolean;
|
|
67
50
|
}): string | null;
|
|
51
|
+
export declare function generateServicePlist(execPath: string): string;
|
|
68
52
|
/**
|
|
69
53
|
* Restart the menu-bar launchd agent from a clean state.
|
|
70
54
|
*
|
|
@@ -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();
|
|
@@ -4,8 +4,12 @@
|
|
|
4
4
|
* The one place the daemon (overdue routines, heal, routine start/finish/output)
|
|
5
5
|
* emits a native desktop notification. On macOS it routes through the installed
|
|
6
6
|
* `MenubarHelper.app` companion — a one-shot `MenubarHelper --notify` invocation —
|
|
7
|
-
* so the notification is attributed to that bundle and carries the agents-cli
|
|
8
|
-
*
|
|
7
|
+
* so the notification is attributed to that bundle and carries the agents-cli
|
|
8
|
+
* mark instead of the generic AppleScript icon. A banner carries two images: the
|
|
9
|
+
* agents-cli app icon on the LEFT (the sender) and, when the event belongs to one
|
|
10
|
+
* harness, that agent's avatar on the RIGHT (`agent`, rendered by
|
|
11
|
+
* AgentAvatar.swift) — the same layout macOS gives a YouTube notification, app on
|
|
12
|
+
* the left and the channel on the right. When the companion
|
|
9
13
|
* app is not installed (menu bar disabled, a Linux package, a dev checkout), it
|
|
10
14
|
* degrades to `osascript` so an overdue/heal notice is never silently lost — the
|
|
11
15
|
* generic icon is the acceptable cost of preserving delivery, not a bug hidden by
|
|
@@ -32,6 +36,17 @@ export interface DesktopNotification {
|
|
|
32
36
|
* macOS-only (osascript / notify-send have no click target). See routine-notify.ts.
|
|
33
37
|
*/
|
|
34
38
|
action?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Harness the notification is ABOUT (`claude`, `codex`, … — an `AgentId`).
|
|
41
|
+
* macOS draws two images on a banner: the sending bundle's app icon on the
|
|
42
|
+
* LEFT and `contentImage` on the RIGHT. The companion renders this id as the
|
|
43
|
+
* right-hand avatar (AgentAvatar.swift), so a banner reads "agents-cli, about
|
|
44
|
+
* Claude" the way a YouTube notification reads "YouTube, from this channel".
|
|
45
|
+
* Omit it when no single harness owns the event (a daemon heal, a fan-out
|
|
46
|
+
* across several agents) — the right slot then stays empty rather than
|
|
47
|
+
* repeating the left one. macOS-only; osascript / notify-send carry no image.
|
|
48
|
+
*/
|
|
49
|
+
agent?: string;
|
|
35
50
|
}
|
|
36
51
|
/** Argv for the MenubarHelper one-shot notify mode. Exported for tests. */
|
|
37
52
|
export declare function buildMenubarNotifyArgs(n: DesktopNotification): string[];
|
|
@@ -4,8 +4,12 @@
|
|
|
4
4
|
* The one place the daemon (overdue routines, heal, routine start/finish/output)
|
|
5
5
|
* emits a native desktop notification. On macOS it routes through the installed
|
|
6
6
|
* `MenubarHelper.app` companion — a one-shot `MenubarHelper --notify` invocation —
|
|
7
|
-
* so the notification is attributed to that bundle and carries the agents-cli
|
|
8
|
-
*
|
|
7
|
+
* so the notification is attributed to that bundle and carries the agents-cli
|
|
8
|
+
* mark instead of the generic AppleScript icon. A banner carries two images: the
|
|
9
|
+
* agents-cli app icon on the LEFT (the sender) and, when the event belongs to one
|
|
10
|
+
* harness, that agent's avatar on the RIGHT (`agent`, rendered by
|
|
11
|
+
* AgentAvatar.swift) — the same layout macOS gives a YouTube notification, app on
|
|
12
|
+
* the left and the channel on the right. When the companion
|
|
9
13
|
* app is not installed (menu bar disabled, a Linux package, a dev checkout), it
|
|
10
14
|
* degrades to `osascript` so an overdue/heal notice is never silently lost — the
|
|
11
15
|
* generic icon is the acceptable cost of preserving delivery, not a bug hidden by
|
|
@@ -35,6 +39,8 @@ export function buildMenubarNotifyArgs(n) {
|
|
|
35
39
|
args.push('--subtitle', n.subtitle);
|
|
36
40
|
if (n.action)
|
|
37
41
|
args.push('--action', n.action);
|
|
42
|
+
if (n.agent)
|
|
43
|
+
args.push('--agent', n.agent);
|
|
38
44
|
return args;
|
|
39
45
|
}
|
|
40
46
|
/** AppleScript for the osascript degradation path. Exported for tests. */
|
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
|
}
|