@phnx-labs/agents-cli 1.20.91 → 1.20.93
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 +276 -0
- package/README.md +1 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +72 -6
- package/dist/commands/activity.js +198 -49
- package/dist/commands/beta.js +1 -0
- package/dist/commands/doctor.js +4 -2
- package/dist/commands/events.js +91 -1
- package/dist/commands/exec.d.ts +14 -0
- package/dist/commands/exec.js +144 -14
- package/dist/commands/projects.d.ts +22 -0
- package/dist/commands/projects.js +539 -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-picker.d.ts +15 -0
- package/dist/commands/sessions-picker.js +37 -12
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +9 -1
- package/dist/commands/sessions.d.ts +10 -5
- package/dist/commands/sessions.js +65 -27
- package/dist/commands/sync.js +9 -3
- package/dist/commands/view.js +4 -0
- package/dist/index.js +18 -1
- package/dist/lib/activity.d.ts +77 -12
- package/dist/lib/activity.js +424 -74
- 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/devices/registry.d.ts +14 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/events-ingest.d.ts +46 -0
- package/dist/lib/events-ingest.js +182 -0
- package/dist/lib/events.d.ts +15 -3
- package/dist/lib/events.js +55 -3
- package/dist/lib/feed-post.js +8 -2
- package/dist/lib/hosts/remote-cmd.js +4 -0
- package/dist/lib/linear-project-counts.d.ts +62 -0
- package/dist/lib/linear-project-counts.js +122 -0
- package/dist/lib/linear-projects.d.ts +50 -0
- package/dist/lib/linear-projects.js +114 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +14 -4
- package/dist/lib/menubar/install-menubar.js +20 -6
- package/dist/lib/menubar/notify-desktop.d.ts +17 -2
- package/dist/lib/menubar/notify-desktop.js +8 -2
- package/dist/lib/project-key.d.ts +44 -0
- package/dist/lib/project-key.js +79 -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-root.js +16 -0
- package/dist/lib/project-status.d.ts +100 -0
- package/dist/lib/project-status.js +182 -0
- package/dist/lib/projects.d.ts +144 -0
- package/dist/lib/projects.js +313 -0
- package/dist/lib/remote-agents-json.d.ts +9 -0
- package/dist/lib/remote-agents-json.js +11 -5
- package/dist/lib/routine-notify.d.ts +11 -0
- package/dist/lib/routine-notify.js +22 -0
- package/dist/lib/run-notify.js +3 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/audit.d.ts +1 -1
- package/dist/lib/secrets/audit.js +53 -10
- 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/bash-command.d.ts +53 -0
- package/dist/lib/session/bash-command.js +364 -0
- package/dist/lib/session/digest.d.ts +6 -0
- package/dist/lib/session/digest.js +19 -0
- package/dist/lib/session/relative-time.d.ts +23 -0
- package/dist/lib/session/relative-time.js +60 -8
- 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.js +5 -2
- package/dist/lib/session/render.d.ts +2 -9
- package/dist/lib/session/render.js +25 -56
- package/dist/lib/sqlite.js +28 -1
- package/dist/lib/ssh-exec.d.ts +6 -0
- package/dist/lib/ssh-exec.js +10 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/state.d.ts +14 -0
- package/dist/lib/state.js +19 -0
- package/dist/lib/terminal/backends/index.d.ts +10 -2
- package/dist/lib/terminal/backends/index.js +14 -2
- package/dist/lib/terminal/backends/terminal-app.d.ts +13 -0
- package/dist/lib/terminal/backends/terminal-app.js +73 -0
- package/dist/lib/terminal/index.d.ts +2 -1
- package/dist/lib/terminal/index.js +2 -1
- package/dist/lib/terminal/preferred.d.ts +89 -0
- package/dist/lib/terminal/preferred.js +87 -0
- package/dist/lib/terminal/run-surface.d.ts +82 -0
- package/dist/lib/terminal/run-surface.js +146 -0
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/types.d.ts +6 -5
- package/dist/lib/versions.d.ts +6 -0
- package/dist/lib/versions.js +6 -4
- package/package.json +2 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delivery envelope for `agents send` / `agents notify`.
|
|
3
|
+
*
|
|
4
|
+
* One primitive: resolve a destination (channel + target), compose text + urls +
|
|
5
|
+
* attachments, hand off to a channel provider. `notify` is the same path with
|
|
6
|
+
* destination defaulted to `notify.owner` in agents.yaml — owner is an address
|
|
7
|
+
* alias (`--to owner`), not a separate stack.
|
|
8
|
+
*
|
|
9
|
+
* Agent control (`agents message`, `sessions inject`) stays outside this module.
|
|
10
|
+
*/
|
|
11
|
+
import type { Meta } from '../types.js';
|
|
12
|
+
import type { SendResult } from './registry.js';
|
|
13
|
+
/** Normalized delivery request after CLI/config resolution. */
|
|
14
|
+
export interface SendEnvelope {
|
|
15
|
+
text: string;
|
|
16
|
+
channel: string;
|
|
17
|
+
to: string;
|
|
18
|
+
thread?: string;
|
|
19
|
+
attachments?: string[];
|
|
20
|
+
from?: string;
|
|
21
|
+
dryRun?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface ResolveSendInput {
|
|
24
|
+
/**
|
|
25
|
+
* Body text. Prefer `--text`; positional `[text]` is accepted for compat and
|
|
26
|
+
* folded in when `--text` is omitted.
|
|
27
|
+
*/
|
|
28
|
+
text?: string;
|
|
29
|
+
/** Positional `[text]` from commander (legacy). */
|
|
30
|
+
positionalText?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Recipient. Channel-specific id, or the alias `owner` which expands to
|
|
33
|
+
* `notify.owner.{channel,to}`.
|
|
34
|
+
*/
|
|
35
|
+
to?: string;
|
|
36
|
+
/** Provider/channel name. Required unless `to` is `owner` (or ownerMode). */
|
|
37
|
+
channel?: string;
|
|
38
|
+
thread?: string;
|
|
39
|
+
/** Local file paths. */
|
|
40
|
+
attachments?: string[];
|
|
41
|
+
/** Links / remote media refs — appended to the body so every provider sees them. */
|
|
42
|
+
urls?: string[];
|
|
43
|
+
from?: string;
|
|
44
|
+
dryRun?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* When true (`agents notify`), missing channel/to default to `notify.owner`.
|
|
47
|
+
* Explicit flags still win.
|
|
48
|
+
*/
|
|
49
|
+
ownerMode?: boolean;
|
|
50
|
+
}
|
|
51
|
+
export type ResolveSendResult = {
|
|
52
|
+
ok: true;
|
|
53
|
+
envelope: SendEnvelope;
|
|
54
|
+
} | {
|
|
55
|
+
ok: false;
|
|
56
|
+
error: string;
|
|
57
|
+
};
|
|
58
|
+
/** True when the destination token means “the configured owner”. */
|
|
59
|
+
export declare function isOwnerAlias(to: string | undefined): boolean;
|
|
60
|
+
/** Compose body + optional URL lines (skip urls already present in the body). */
|
|
61
|
+
export declare function composeSendText(text: string, urls?: string[]): string;
|
|
62
|
+
/** Read notify.owner; null when either field is missing. */
|
|
63
|
+
export declare function readOwnerDest(meta: Meta): {
|
|
64
|
+
channel: string;
|
|
65
|
+
to: string;
|
|
66
|
+
} | null;
|
|
67
|
+
/**
|
|
68
|
+
* Resolve CLI/config into a send envelope. Pure except for reading `meta` —
|
|
69
|
+
* no I/O, no provider registration — so unit tests do not need a real PATH.
|
|
70
|
+
*/
|
|
71
|
+
export declare function resolveSendEnvelope(input: ResolveSendInput, meta: Meta): ResolveSendResult;
|
|
72
|
+
/**
|
|
73
|
+
* Register providers, resolve transport, deliver. Used by the CLI and by any
|
|
74
|
+
* internal caller that already has a resolved envelope.
|
|
75
|
+
*/
|
|
76
|
+
export declare function deliverEnvelope(envelope: SendEnvelope, meta: Meta): Promise<SendResult>;
|
|
77
|
+
/** Resolve + deliver in one step (CLI happy path). */
|
|
78
|
+
export declare function sendMessage(input: ResolveSendInput, meta: Meta): Promise<{
|
|
79
|
+
result: SendResult;
|
|
80
|
+
envelope: SendEnvelope;
|
|
81
|
+
} | {
|
|
82
|
+
error: string;
|
|
83
|
+
}>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { registerBuiltinProviders } from './providers/index.js';
|
|
2
|
+
import { resolveTransport } from './resolve.js';
|
|
3
|
+
const OWNER_ALIAS = 'owner';
|
|
4
|
+
/** True when the destination token means “the configured owner”. */
|
|
5
|
+
export function isOwnerAlias(to) {
|
|
6
|
+
return (to ?? '').trim().toLowerCase() === OWNER_ALIAS;
|
|
7
|
+
}
|
|
8
|
+
/** Compose body + optional URL lines (skip urls already present in the body). */
|
|
9
|
+
export function composeSendText(text, urls) {
|
|
10
|
+
const body = text.trim();
|
|
11
|
+
const extra = (urls ?? [])
|
|
12
|
+
.map((u) => u.trim())
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
.filter((u) => !body.includes(u));
|
|
15
|
+
if (extra.length === 0)
|
|
16
|
+
return body;
|
|
17
|
+
return body ? `${body}\n${extra.join('\n')}` : extra.join('\n');
|
|
18
|
+
}
|
|
19
|
+
/** Read notify.owner; null when either field is missing. */
|
|
20
|
+
export function readOwnerDest(meta) {
|
|
21
|
+
const owner = meta.notify?.owner;
|
|
22
|
+
const channel = owner?.channel?.trim();
|
|
23
|
+
const to = owner?.to?.trim();
|
|
24
|
+
if (!channel || !to)
|
|
25
|
+
return null;
|
|
26
|
+
return { channel, to };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolve CLI/config into a send envelope. Pure except for reading `meta` —
|
|
30
|
+
* no I/O, no provider registration — so unit tests do not need a real PATH.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveSendEnvelope(input, meta) {
|
|
33
|
+
const positional = (input.positionalText ?? '').trim();
|
|
34
|
+
const flagged = (input.text ?? '').trim();
|
|
35
|
+
if (positional && flagged && positional !== flagged) {
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
error: 'Pass the message once: use --text, or a positional argument, not both with different values.',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const rawText = flagged || positional;
|
|
42
|
+
const urls = (input.urls ?? []).map((u) => u.trim()).filter(Boolean);
|
|
43
|
+
const text = composeSendText(rawText, urls);
|
|
44
|
+
if (!text) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
error: 'Message is empty. Pass --text "…", a positional message, and/or --url.',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// Owner defaults fill only missing fields (and expand the bare "owner" alias).
|
|
51
|
+
// Explicit --channel/--to always win; a complete notify.owner is NOT required
|
|
52
|
+
// when both flags are already set (same merge-then-require shape as main).
|
|
53
|
+
const ownerCfg = meta.notify?.owner;
|
|
54
|
+
const ownerChannel = ownerCfg?.channel?.trim() || '';
|
|
55
|
+
const ownerTo = ownerCfg?.to?.trim() || '';
|
|
56
|
+
let channel = (input.channel ?? '').trim();
|
|
57
|
+
let to = (input.to ?? '').trim();
|
|
58
|
+
const usedOwnerAlias = isOwnerAlias(to);
|
|
59
|
+
if (input.ownerMode || usedOwnerAlias) {
|
|
60
|
+
if (!channel)
|
|
61
|
+
channel = ownerChannel;
|
|
62
|
+
if (!to || usedOwnerAlias)
|
|
63
|
+
to = ownerTo;
|
|
64
|
+
}
|
|
65
|
+
if (!channel || !to) {
|
|
66
|
+
const hint = input.ownerMode || usedOwnerAlias
|
|
67
|
+
? 'Set notify.owner.{channel,to} in agents.yaml, or pass --channel and --to explicitly.'
|
|
68
|
+
: 'Need --channel and --to (or --to owner with notify.owner configured). ' +
|
|
69
|
+
'Example: agents send --channel desktop --to local --text "hi"';
|
|
70
|
+
return { ok: false, error: hint };
|
|
71
|
+
}
|
|
72
|
+
const attachments = [
|
|
73
|
+
...(input.attachments ?? []),
|
|
74
|
+
]
|
|
75
|
+
.map((p) => p.trim())
|
|
76
|
+
.filter(Boolean);
|
|
77
|
+
return {
|
|
78
|
+
ok: true,
|
|
79
|
+
envelope: {
|
|
80
|
+
text,
|
|
81
|
+
channel,
|
|
82
|
+
to,
|
|
83
|
+
thread: input.thread?.trim() || undefined,
|
|
84
|
+
attachments: attachments.length ? attachments : undefined,
|
|
85
|
+
from: input.from?.trim() || undefined,
|
|
86
|
+
dryRun: input.dryRun,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Register providers, resolve transport, deliver. Used by the CLI and by any
|
|
92
|
+
* internal caller that already has a resolved envelope.
|
|
93
|
+
*/
|
|
94
|
+
export async function deliverEnvelope(envelope, meta) {
|
|
95
|
+
registerBuiltinProviders();
|
|
96
|
+
const provider = resolveTransport(envelope.channel, meta);
|
|
97
|
+
return provider.send(envelope.text, {
|
|
98
|
+
target: envelope.to,
|
|
99
|
+
thread: envelope.thread,
|
|
100
|
+
attachments: envelope.attachments,
|
|
101
|
+
from: envelope.from,
|
|
102
|
+
dryRun: envelope.dryRun,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/** Resolve + deliver in one step (CLI happy path). */
|
|
106
|
+
export async function sendMessage(input, meta) {
|
|
107
|
+
const resolved = resolveSendEnvelope(input, meta);
|
|
108
|
+
if (!resolved.ok)
|
|
109
|
+
return { error: resolved.error };
|
|
110
|
+
const result = await deliverEnvelope(resolved.envelope, meta);
|
|
111
|
+
return { result, envelope: resolved.envelope };
|
|
112
|
+
}
|
|
@@ -80,6 +80,20 @@ export interface DeviceProfile {
|
|
|
80
80
|
export declare function deviceRole(d: DeviceProfile): DeviceRole;
|
|
81
81
|
/** True for a control-only device (a cockpit) that must never be dialed/scheduled. */
|
|
82
82
|
export declare function isControlDevice(d: DeviceProfile): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Whether a fan-out should dial this device, honouring the preference stated on
|
|
85
|
+
* {@link DeviceProfile.reachability}: the live SSH probe wins over the cached
|
|
86
|
+
* {@link DeviceTailscale.online} snapshot.
|
|
87
|
+
*
|
|
88
|
+
* Reading only `tailscale.online` is wrong in both directions, and both were
|
|
89
|
+
* live on a real fleet. A `via:"manual"` device never gets a tailscale peer
|
|
90
|
+
* entry at all, so its `online` is permanently `undefined` and a strict
|
|
91
|
+
* `=== true` test skipped it forever — every session on that box was invisible
|
|
92
|
+
* to the cross-fleet sweep. Conversely a box that has since gone to sleep keeps
|
|
93
|
+
* a stale `online:true` and gets dialed, burning a full ConnectTimeout and
|
|
94
|
+
* reporting a false "unreachable" that callers treat as doubt.
|
|
95
|
+
*/
|
|
96
|
+
export declare function isDialableDevice(d: DeviceProfile): boolean;
|
|
83
97
|
/** Map of device name to profile. */
|
|
84
98
|
export type DeviceRegistry = Record<string, DeviceProfile>;
|
|
85
99
|
/** Throw if `name` is not usable as an ssh alias (no spaces, quotes, etc.). */
|
|
@@ -27,6 +27,43 @@ export function deviceRole(d) {
|
|
|
27
27
|
export function isControlDevice(d) {
|
|
28
28
|
return deviceRole(d) === 'control';
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Whether a fan-out should dial this device, honouring the preference stated on
|
|
32
|
+
* {@link DeviceProfile.reachability}: the live SSH probe wins over the cached
|
|
33
|
+
* {@link DeviceTailscale.online} snapshot.
|
|
34
|
+
*
|
|
35
|
+
* Reading only `tailscale.online` is wrong in both directions, and both were
|
|
36
|
+
* live on a real fleet. A `via:"manual"` device never gets a tailscale peer
|
|
37
|
+
* entry at all, so its `online` is permanently `undefined` and a strict
|
|
38
|
+
* `=== true` test skipped it forever — every session on that box was invisible
|
|
39
|
+
* to the cross-fleet sweep. Conversely a box that has since gone to sleep keeps
|
|
40
|
+
* a stale `online:true` and gets dialed, burning a full ConnectTimeout and
|
|
41
|
+
* reporting a false "unreachable" that callers treat as doubt.
|
|
42
|
+
*/
|
|
43
|
+
export function isDialableDevice(d) {
|
|
44
|
+
// Union, deliberately: either signal saying "go" is enough. A probe may only
|
|
45
|
+
// ADD a peer to the sweep, never remove one.
|
|
46
|
+
//
|
|
47
|
+
// The probe is not trustworthy enough to exclude on. It runs with a short SSH
|
|
48
|
+
// budget, so on a congested tailnet it returns false negatives — observed
|
|
49
|
+
// marking the LOCAL machine unreachable, and flipping a live worker box from
|
|
50
|
+
// reachable to unreachable nine minutes apart. Letting that shrink the sweep
|
|
51
|
+
// would hide sessions on healthy boxes, a worse failure than the one below.
|
|
52
|
+
//
|
|
53
|
+
// The snapshot alone is not enough either: a device registered with
|
|
54
|
+
// `address.via: "manual"` never gets a tailscale peer entry, so `online`
|
|
55
|
+
// stays undefined and a strict `=== true` test skipped it forever, making
|
|
56
|
+
// every session on that box unresolvable from any other machine.
|
|
57
|
+
//
|
|
58
|
+
// So: no tailscale block at all is unknown-not-offline (the rule `ssh.ts`
|
|
59
|
+
// renderDeviceTable and Factory's `isDeviceOnline` already use, so the picker
|
|
60
|
+
// and the sweep agree on who exists), and a positive probe rescues a device
|
|
61
|
+
// whose snapshot says offline. The cost of dialing a box that is actually
|
|
62
|
+
// asleep is one ConnectTimeout — the pre-existing behaviour, not a regression.
|
|
63
|
+
if (d.reachability?.reachable)
|
|
64
|
+
return true;
|
|
65
|
+
return !d.tailscale || d.tailscale.online === true;
|
|
66
|
+
}
|
|
30
67
|
function registryPath() {
|
|
31
68
|
return getDevicesRegistryPath();
|
|
32
69
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface IngestReject {
|
|
2
|
+
/** 1-based index of the offending line within the batch. */
|
|
3
|
+
line: number;
|
|
4
|
+
reason: string;
|
|
5
|
+
}
|
|
6
|
+
export interface IngestResult {
|
|
7
|
+
written: number;
|
|
8
|
+
rejected: IngestReject[];
|
|
9
|
+
/** Per-store counts, so a caller/test can assert routing without reading files. */
|
|
10
|
+
routed: {
|
|
11
|
+
operational: number;
|
|
12
|
+
activity: number;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface IngestOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Producer name, stamped as `module` on operational records so
|
|
18
|
+
* `agents events --module <source>` filters to this producer. Required: an
|
|
19
|
+
* unattributed external event is not auditable.
|
|
20
|
+
*/
|
|
21
|
+
source: string;
|
|
22
|
+
/** Validate and report without writing either store. */
|
|
23
|
+
dryRun?: boolean;
|
|
24
|
+
/** Override the activity root (tests). */
|
|
25
|
+
activityRoot?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Route one already-validated line.
|
|
29
|
+
*
|
|
30
|
+
* The rule is forced by the stores, not chosen:
|
|
31
|
+
* - The activity store is keyed by session id ON DISK (one file per session),
|
|
32
|
+
* so an event with no session has nowhere to live there.
|
|
33
|
+
* - Activity records are read back with `module: 'activity'` hardcoded, so
|
|
34
|
+
* anything that must stay filterable by its producer has to be operational.
|
|
35
|
+
* Therefore: milestone + usable sessionId -> activity; everything else -> ops.
|
|
36
|
+
* `readUnifiedEvents` merges the two at read time, so `agents events` sees both.
|
|
37
|
+
*/
|
|
38
|
+
export declare function routeFor(event: string, sessionId: unknown): 'activity' | 'operational';
|
|
39
|
+
/**
|
|
40
|
+
* Ingest a JSONL batch.
|
|
41
|
+
*
|
|
42
|
+
* Rejection is PER LINE and lossless: a single bad line never discards the rest
|
|
43
|
+
* of the batch. One typo in a 100-event flush must not silently drop 99 real
|
|
44
|
+
* events, and the caller still learns exactly which line failed and why.
|
|
45
|
+
*/
|
|
46
|
+
export declare function ingestBatch(input: string, opts: IngestOptions): IngestResult;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ingest events produced OUTSIDE this process — the writer behind
|
|
3
|
+
* `agents events emit`.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: `emit()` (lib/events.ts) and `appendActivityEvent()`
|
|
6
|
+
* (lib/activity.ts) are in-process APIs, but the producers that most need to
|
|
7
|
+
* record events are not agents-cli processes at all — the Factory VS Code
|
|
8
|
+
* extension host, a shell guard, any external tool. They shell out instead, and
|
|
9
|
+
* this module is the one place that turns their JSONL into real records.
|
|
10
|
+
*
|
|
11
|
+
* It deliberately does NOT reuse `feed post`: that surface hardcodes the
|
|
12
|
+
* `status.posted` kind, infers identity by walking the pid-registry ancestor
|
|
13
|
+
* chain (wrong for a process that is not a descendant of an agent), throws when
|
|
14
|
+
* that inference fails, and fires the configured broadcast sinks. A telemetry
|
|
15
|
+
* path must do none of those things.
|
|
16
|
+
*
|
|
17
|
+
* Pure except for the two writers it calls, so the routing and validation rules
|
|
18
|
+
* below are unit-testable against a temp events path + activity root.
|
|
19
|
+
*/
|
|
20
|
+
import { emit, isEventType, } from './events.js';
|
|
21
|
+
import { appendActivityEvent, tierForEvent, } from './activity.js';
|
|
22
|
+
/** Envelope keys an incoming line may set directly; everything else is payload. */
|
|
23
|
+
const ENVELOPE_KEYS = [
|
|
24
|
+
'event', 'ts', 'sessionId', 'mailboxId', 'terminalId', 'launchId', 'tmuxPane',
|
|
25
|
+
'host', 'runtime', 'agent', 'tool', 'detail', 'url', 'project', 'cwd',
|
|
26
|
+
];
|
|
27
|
+
const ENVELOPE_KEY_SET = new Set(ENVELOPE_KEYS);
|
|
28
|
+
/** ISO-8601 with at least seconds. Rejects "now", epoch ints, and garbage. */
|
|
29
|
+
function isIsoTimestamp(value) {
|
|
30
|
+
if (typeof value !== 'string')
|
|
31
|
+
return false;
|
|
32
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value))
|
|
33
|
+
return false;
|
|
34
|
+
return !Number.isNaN(Date.parse(value));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A session id must be non-empty AND survive the activity writer's filename
|
|
38
|
+
* sanitizer — `activityPath` throws on an id that reduces to nothing, and that
|
|
39
|
+
* throw would abort a whole batch mid-write. Check it here so the line is
|
|
40
|
+
* rejected cleanly and its siblings still land.
|
|
41
|
+
*/
|
|
42
|
+
function isUsableSessionId(value) {
|
|
43
|
+
return typeof value === 'string' && value.replace(/[^A-Za-z0-9._-]/g, '-').replace(/-+/g, '') !== '';
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Route one already-validated line.
|
|
47
|
+
*
|
|
48
|
+
* The rule is forced by the stores, not chosen:
|
|
49
|
+
* - The activity store is keyed by session id ON DISK (one file per session),
|
|
50
|
+
* so an event with no session has nowhere to live there.
|
|
51
|
+
* - Activity records are read back with `module: 'activity'` hardcoded, so
|
|
52
|
+
* anything that must stay filterable by its producer has to be operational.
|
|
53
|
+
* Therefore: milestone + usable sessionId -> activity; everything else -> ops.
|
|
54
|
+
* `readUnifiedEvents` merges the two at read time, so `agents events` sees both.
|
|
55
|
+
*/
|
|
56
|
+
export function routeFor(event, sessionId) {
|
|
57
|
+
return tierForEvent(event) === 'milestone' && isUsableSessionId(sessionId) ? 'activity' : 'operational';
|
|
58
|
+
}
|
|
59
|
+
/** Validate one raw JSONL line. Returns either a parsed line or a rejection. */
|
|
60
|
+
function parseLine(raw, lineNo) {
|
|
61
|
+
let obj;
|
|
62
|
+
try {
|
|
63
|
+
obj = JSON.parse(raw);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return { line: lineNo, reason: 'not valid JSON' };
|
|
67
|
+
}
|
|
68
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
69
|
+
return { line: lineNo, reason: 'not a JSON object' };
|
|
70
|
+
}
|
|
71
|
+
const rec = obj;
|
|
72
|
+
const event = rec.event;
|
|
73
|
+
if (typeof event !== 'string' || event === '') {
|
|
74
|
+
return { line: lineNo, reason: 'missing "event"' };
|
|
75
|
+
}
|
|
76
|
+
if (!isEventType(event)) {
|
|
77
|
+
return { line: lineNo, reason: `unknown event kind: ${event}` };
|
|
78
|
+
}
|
|
79
|
+
if (rec.ts !== undefined && !isIsoTimestamp(rec.ts)) {
|
|
80
|
+
return { line: lineNo, reason: `invalid "ts" (want ISO-8601): ${String(rec.ts)}` };
|
|
81
|
+
}
|
|
82
|
+
// A milestone with no usable session id is REJECTED, not quietly demoted to
|
|
83
|
+
// the operational store. Silently writing it somewhere else would look like
|
|
84
|
+
// success while the event never appears in the activity lane the producer
|
|
85
|
+
// asked for -- exactly the "wrong path that looks like success" this codebase
|
|
86
|
+
// forbids at a boundary.
|
|
87
|
+
if (tierForEvent(event) === 'milestone' && !isUsableSessionId(rec.sessionId)) {
|
|
88
|
+
return {
|
|
89
|
+
line: lineNo,
|
|
90
|
+
reason: `"${event}" is a milestone and needs a non-empty "sessionId" (the activity log is keyed by it)`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const envelope = {};
|
|
94
|
+
const payload = {};
|
|
95
|
+
for (const [key, value] of Object.entries(rec)) {
|
|
96
|
+
if (key === 'event' || key === 'ts')
|
|
97
|
+
continue;
|
|
98
|
+
if (ENVELOPE_KEY_SET.has(key))
|
|
99
|
+
envelope[key] = value;
|
|
100
|
+
else
|
|
101
|
+
payload[key] = value;
|
|
102
|
+
}
|
|
103
|
+
return { line: lineNo, event, ts: rec.ts, envelope, payload };
|
|
104
|
+
}
|
|
105
|
+
function str(value) {
|
|
106
|
+
return typeof value === 'string' && value !== '' ? value : undefined;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Ingest a JSONL batch.
|
|
110
|
+
*
|
|
111
|
+
* Rejection is PER LINE and lossless: a single bad line never discards the rest
|
|
112
|
+
* of the batch. One typo in a 100-event flush must not silently drop 99 real
|
|
113
|
+
* events, and the caller still learns exactly which line failed and why.
|
|
114
|
+
*/
|
|
115
|
+
export function ingestBatch(input, opts) {
|
|
116
|
+
const source = opts.source.trim();
|
|
117
|
+
if (!source)
|
|
118
|
+
throw new Error('events emit: --source is required (it names the producer)');
|
|
119
|
+
const result = { written: 0, rejected: [], routed: { operational: 0, activity: 0 } };
|
|
120
|
+
const rawLines = input.split('\n');
|
|
121
|
+
let lineNo = 0;
|
|
122
|
+
for (const raw of rawLines) {
|
|
123
|
+
lineNo += 1;
|
|
124
|
+
if (raw.trim() === '')
|
|
125
|
+
continue;
|
|
126
|
+
const parsed = parseLine(raw, lineNo);
|
|
127
|
+
if ('reason' in parsed) {
|
|
128
|
+
result.rejected.push(parsed);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const route = routeFor(parsed.event, parsed.envelope.sessionId);
|
|
132
|
+
result.routed[route] += 1;
|
|
133
|
+
if (opts.dryRun) {
|
|
134
|
+
result.written += 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (route === 'activity') {
|
|
138
|
+
const sessionId = parsed.envelope.sessionId;
|
|
139
|
+
const ev = {
|
|
140
|
+
ts: parsed.ts ?? new Date().toISOString(),
|
|
141
|
+
event: parsed.event,
|
|
142
|
+
sessionId,
|
|
143
|
+
mailboxId: str(parsed.envelope.mailboxId) ?? sessionId,
|
|
144
|
+
host: str(parsed.envelope.host) ?? '',
|
|
145
|
+
runtime: str(parsed.envelope.runtime) ?? source,
|
|
146
|
+
...(str(parsed.envelope.cwd) ? { cwd: parsed.envelope.cwd } : {}),
|
|
147
|
+
...(str(parsed.envelope.project) ? { project: parsed.envelope.project } : {}),
|
|
148
|
+
...(str(parsed.envelope.agent) ? { agent: parsed.envelope.agent } : {}),
|
|
149
|
+
...(str(parsed.envelope.tool) ? { tool: parsed.envelope.tool } : {}),
|
|
150
|
+
...(str(parsed.envelope.detail) ? { detail: parsed.envelope.detail } : {}),
|
|
151
|
+
...(str(parsed.envelope.url) ? { url: parsed.envelope.url } : {}),
|
|
152
|
+
...(str(parsed.envelope.launchId) ? { launchId: parsed.envelope.launchId } : {}),
|
|
153
|
+
...(str(parsed.envelope.terminalId) ? { terminalId: parsed.envelope.terminalId } : {}),
|
|
154
|
+
...(str(parsed.envelope.tmuxPane) ? { tmuxPane: parsed.envelope.tmuxPane } : {}),
|
|
155
|
+
};
|
|
156
|
+
appendActivityEvent(ev, opts.activityRoot);
|
|
157
|
+
result.written += 1;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
// Operational. `module: source` is what makes `--module factory` work;
|
|
161
|
+
// the envelope fields that have no EventPayload home ride along as payload
|
|
162
|
+
// keys, where sanitizePayload still applies its redaction rules.
|
|
163
|
+
const payload = {
|
|
164
|
+
module: source,
|
|
165
|
+
...parsed.payload,
|
|
166
|
+
...(str(parsed.envelope.sessionId) ? { sessionId: parsed.envelope.sessionId } : {}),
|
|
167
|
+
...(str(parsed.envelope.agent) ? { agent: parsed.envelope.agent } : {}),
|
|
168
|
+
...(str(parsed.envelope.cwd) ? { cwd: parsed.envelope.cwd } : {}),
|
|
169
|
+
...(str(parsed.envelope.project) ? { project: parsed.envelope.project } : {}),
|
|
170
|
+
...(str(parsed.envelope.detail) ? { detail: parsed.envelope.detail } : {}),
|
|
171
|
+
...(str(parsed.envelope.url) ? { url: parsed.envelope.url } : {}),
|
|
172
|
+
...(str(parsed.envelope.terminalId) ? { terminalId: parsed.envelope.terminalId } : {}),
|
|
173
|
+
...(str(parsed.envelope.launchId) ? { launchId: parsed.envelope.launchId } : {}),
|
|
174
|
+
...(str(parsed.envelope.host) ? { sourceHost: parsed.envelope.host } : {}),
|
|
175
|
+
...(str(parsed.envelope.runtime) ? { runtime: parsed.envelope.runtime } : {}),
|
|
176
|
+
...(str(parsed.envelope.tool) ? { tool: parsed.envelope.tool } : {}),
|
|
177
|
+
};
|
|
178
|
+
emit(parsed.event, payload, parsed.ts ? { ts: parsed.ts } : {});
|
|
179
|
+
result.written += 1;
|
|
180
|
+
}
|
|
181
|
+
return result;
|
|
182
|
+
}
|
package/dist/lib/events.d.ts
CHANGED
|
@@ -13,7 +13,11 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { type ActorKind } from './actor.js';
|
|
15
15
|
export type EventLevel = 'audit' | 'warn' | 'info' | 'debug';
|
|
16
|
-
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.unlocked' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'webhook.received' | 'webhook.authorized' | 'webhook.rejected' | 'webhook.matched' | 'webhook.fired' | 'webhook.handler.start' | 'webhook.handler.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'friction' | 'error' | 'warn' | 'info' | 'debug';
|
|
16
|
+
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.unlocked' | 'secrets.create' | 'secrets.import' | 'secrets.export' | 'secrets.view' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'webhook.received' | 'webhook.authorized' | 'webhook.rejected' | 'webhook.matched' | 'webhook.fired' | 'webhook.handler.start' | 'webhook.handler.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'factory.command' | 'factory.action' | 'factory.uri' | 'factory.launch' | 'friction' | 'error' | 'warn' | 'info' | 'debug';
|
|
17
|
+
/** Every known event kind. Derived from {@link EVENT_TYPE_TABLE}, never hand-listed. */
|
|
18
|
+
export declare const EVENT_TYPES: readonly EventType[];
|
|
19
|
+
/** Runtime guard for an event kind arriving from outside this process. */
|
|
20
|
+
export declare function isEventType(value: string): value is EventType;
|
|
17
21
|
export declare function levelFor(event: EventType): EventLevel;
|
|
18
22
|
export interface EventMeta {
|
|
19
23
|
ts: string;
|
|
@@ -100,8 +104,16 @@ export declare function detectCaller(env?: NodeJS.ProcessEnv, stdoutIsTTY?: bool
|
|
|
100
104
|
*
|
|
101
105
|
* @param event - The event type
|
|
102
106
|
* @param payload - Event-specific data (agent, version, cwd, etc.)
|
|
103
|
-
|
|
104
|
-
|
|
107
|
+
* @param overrides - Envelope fields the CALLER owns rather than the writer.
|
|
108
|
+
* Only `ts` today: a batched out-of-process producer (`agents events emit`)
|
|
109
|
+
* records when each event HAPPENED, but flushes them together later, so
|
|
110
|
+
* stamping write-time would collapse a whole batch onto the flush instant and
|
|
111
|
+
* corrupt every `--since` boundary. `ts` stays in RESERVED_META_KEYS so a
|
|
112
|
+
* *payload* still cannot inject it — this explicit channel is the only way in.
|
|
113
|
+
*/
|
|
114
|
+
export declare function emit(event: EventType, payload?: EventPayload, overrides?: {
|
|
115
|
+
ts?: string;
|
|
116
|
+
}): void;
|
|
105
117
|
/**
|
|
106
118
|
* Convenience wrapper for timed operations.
|
|
107
119
|
* Returns a function to call when the operation completes.
|
package/dist/lib/events.js
CHANGED
|
@@ -72,9 +72,50 @@ function isDisabled() {
|
|
|
72
72
|
const DIR_MODE = 0o700;
|
|
73
73
|
/** File permissions (owner read/write only). */
|
|
74
74
|
const FILE_MODE = 0o600;
|
|
75
|
+
/**
|
|
76
|
+
* Every {@link EventType}, as a runtime-checkable table.
|
|
77
|
+
*
|
|
78
|
+
* Typed `Record<EventType, true>` on purpose: the object literal is
|
|
79
|
+
* exhaustiveness-checked at COMPILE time, so adding a member to the union
|
|
80
|
+
* without adding it here fails `tsc`. That is what keeps the runtime validator
|
|
81
|
+
* (`isEventType`, used by `agents events emit` to reject an unknown kind from an
|
|
82
|
+
* out-of-process producer) from silently drifting behind the union.
|
|
83
|
+
*/
|
|
84
|
+
const EVENT_TYPE_TABLE = {
|
|
85
|
+
'agent.run.start': true, 'agent.run.end': true, 'agent.spawn.start': true, 'agent.spawn.end': true,
|
|
86
|
+
'version.install': true, 'version.switch': true, 'version.remove': true,
|
|
87
|
+
'skill.install': true, 'skill.remove': true,
|
|
88
|
+
'browser.launch': true, 'browser.close': true, 'browser.navigate': true, 'browser.screenshot': true,
|
|
89
|
+
'secrets.get': true, 'secrets.unlocked': true, 'secrets.create': true, 'secrets.import': true, 'secrets.export': true, 'secrets.view': true, 'secrets.set': true, 'secrets.delete': true, 'secrets.rename': true,
|
|
90
|
+
'cloud.dispatch': true, 'cloud.complete': true, 'cloud.cancel': true, 'cloud.message': true,
|
|
91
|
+
'teams.create': true, 'teams.add': true, 'teams.start': true, 'teams.complete': true, 'teams.disband': true,
|
|
92
|
+
'hook.fire': true, 'hook.complete': true, 'hook.error': true,
|
|
93
|
+
'mcp.add': true, 'mcp.remove': true, 'mcp.register': true,
|
|
94
|
+
'resource.sync': true,
|
|
95
|
+
'rotation.resolved': true,
|
|
96
|
+
'command.start': true, 'command.end': true,
|
|
97
|
+
'perf.timing': true,
|
|
98
|
+
'session.start': true, 'session.end': true,
|
|
99
|
+
'webhook.received': true, 'webhook.authorized': true, 'webhook.rejected': true, 'webhook.matched': true,
|
|
100
|
+
'webhook.fired': true, 'webhook.handler.start': true, 'webhook.handler.end': true,
|
|
101
|
+
'plan.created': true, 'pr.opened': true, 'pr.merged': true, 'worktree.created': true,
|
|
102
|
+
'worktree.removed': true, 'commit.created': true, 'pushed': true, 'subagent.spawned': true,
|
|
103
|
+
'artifact.created': true, 'task.completed': true, 'checklist.created': true, 'status.posted': true,
|
|
104
|
+
'file.edited': true,
|
|
105
|
+
'factory.command': true, 'factory.action': true, 'factory.uri': true, 'factory.launch': true,
|
|
106
|
+
'friction': true, 'error': true, 'warn': true, 'info': true, 'debug': true,
|
|
107
|
+
};
|
|
108
|
+
/** Every known event kind. Derived from {@link EVENT_TYPE_TABLE}, never hand-listed. */
|
|
109
|
+
export const EVENT_TYPES = Object.keys(EVENT_TYPE_TABLE);
|
|
110
|
+
const EVENT_TYPE_SET = new Set(EVENT_TYPES);
|
|
111
|
+
/** Runtime guard for an event kind arriving from outside this process. */
|
|
112
|
+
export function isEventType(value) {
|
|
113
|
+
return EVENT_TYPE_SET.has(value);
|
|
114
|
+
}
|
|
75
115
|
const AUDIT_EVENTS = new Set([
|
|
76
116
|
'command.start', 'command.end',
|
|
77
|
-
'secrets.get', 'secrets.unlocked', 'secrets.
|
|
117
|
+
'secrets.get', 'secrets.unlocked', 'secrets.create', 'secrets.import', 'secrets.export', 'secrets.view',
|
|
118
|
+
'secrets.set', 'secrets.delete', 'secrets.rename',
|
|
78
119
|
'teams.create', 'teams.add', 'teams.start', 'teams.complete', 'teams.disband',
|
|
79
120
|
'cloud.dispatch', 'cloud.complete', 'cloud.cancel', 'cloud.message',
|
|
80
121
|
'version.install', 'version.switch', 'version.remove',
|
|
@@ -82,6 +123,11 @@ const AUDIT_EVENTS = new Set([
|
|
|
82
123
|
'mcp.add', 'mcp.remove', 'mcp.register',
|
|
83
124
|
'rotation.resolved',
|
|
84
125
|
'session.start', 'session.end',
|
|
126
|
+
// An external process reaching into the user's editor (the CLI's
|
|
127
|
+
// vscodium-agent backend driving `/spawn` / `/inject` / `/focus`) is a
|
|
128
|
+
// "who reached in from outside" fact, which is what the audit lane answers.
|
|
129
|
+
// The other factory.* kinds are ordinary info — a palette press is not audit.
|
|
130
|
+
'factory.uri',
|
|
85
131
|
]);
|
|
86
132
|
export function levelFor(event) {
|
|
87
133
|
if (event === 'warn')
|
|
@@ -334,8 +380,14 @@ function resolveProvenance(env = process.env) {
|
|
|
334
380
|
*
|
|
335
381
|
* @param event - The event type
|
|
336
382
|
* @param payload - Event-specific data (agent, version, cwd, etc.)
|
|
383
|
+
* @param overrides - Envelope fields the CALLER owns rather than the writer.
|
|
384
|
+
* Only `ts` today: a batched out-of-process producer (`agents events emit`)
|
|
385
|
+
* records when each event HAPPENED, but flushes them together later, so
|
|
386
|
+
* stamping write-time would collapse a whole batch onto the flush instant and
|
|
387
|
+
* corrupt every `--since` boundary. `ts` stays in RESERVED_META_KEYS so a
|
|
388
|
+
* *payload* still cannot inject it — this explicit channel is the only way in.
|
|
337
389
|
*/
|
|
338
|
-
export function emit(event, payload = {}) {
|
|
390
|
+
export function emit(event, payload = {}, overrides = {}) {
|
|
339
391
|
if (isDisabled())
|
|
340
392
|
return;
|
|
341
393
|
try {
|
|
@@ -346,7 +398,7 @@ export function emit(event, payload = {}) {
|
|
|
346
398
|
// Provenance floor first: env-sourced defaults an explicit payload overrides.
|
|
347
399
|
...resolveProvenance(),
|
|
348
400
|
...safePayload,
|
|
349
|
-
ts: new Date().toISOString(),
|
|
401
|
+
ts: overrides.ts ?? new Date().toISOString(),
|
|
350
402
|
tz: getTimezoneOffset(),
|
|
351
403
|
tzName: getTimezoneName(),
|
|
352
404
|
hostname: os.hostname(),
|
package/dist/lib/feed-post.js
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
import * as fs from 'fs';
|
|
16
16
|
import * as path from 'path';
|
|
17
17
|
import { spawnSync } from 'child_process';
|
|
18
|
-
import { appendActivityEvent,
|
|
18
|
+
import { appendActivityEvent, } from './activity.js';
|
|
19
|
+
import { resolveProjectNameForCwd, listProjectDefs } from './projects.js';
|
|
19
20
|
import { getHistoryDir } from './state.js';
|
|
20
21
|
import { machineId } from './machine-id.js';
|
|
21
22
|
import { isValidMailboxId } from './mailbox.js';
|
|
@@ -263,7 +264,12 @@ export function postFeedStatus(input) {
|
|
|
263
264
|
+ '(AGENT_SESSION_ID / AGENTS_MAILBOX_DIR / pid registry), or pass --session <id>.');
|
|
264
265
|
}
|
|
265
266
|
const ts = input.ts ?? new Date().toISOString();
|
|
266
|
-
|
|
267
|
+
// The post is written where the agent runs, so the cwd is a local path and
|
|
268
|
+
// gets full canonical resolution — a defined project's name wins (a post from
|
|
269
|
+
// any repo of a multi-repo project files under that project), else the repo
|
|
270
|
+
// key, matching how the timeline groups everything else. listProjectDefs is
|
|
271
|
+
// fail-open, so this costs one small readdir + YAML parse per post.
|
|
272
|
+
const project = resolveProjectNameForCwd(identity.cwd, listProjectDefs());
|
|
267
273
|
const attachments = buildAttachments(input.attach, {
|
|
268
274
|
copyRoot: input.attachmentsRoot ?? path.join(getHistoryDir(), 'attachments'),
|
|
269
275
|
sessionId: identity.sessionId,
|
|
@@ -88,6 +88,7 @@ export const RUN_OPTION_FORWARDING = {
|
|
|
88
88
|
autoSecrets: 'forward', // workflow frontmatter secrets resolve on the REMOTE keychain
|
|
89
89
|
emitSessionId: 'forward', // remote prints its session id as a stdout sentinel the launcher captures (session-marker.ts)
|
|
90
90
|
// rejected — cannot cross the SSH boundary; fail loud, never degrade
|
|
91
|
+
terminal: 'reject', // opens a tab on THIS machine's desktop; a remote tab is a different request
|
|
91
92
|
secrets: 'reject',
|
|
92
93
|
secretsKeys: 'reject',
|
|
93
94
|
allowExpired: 'reject',
|
|
@@ -126,6 +127,9 @@ export const RUN_OPTION_FORWARDING = {
|
|
|
126
127
|
};
|
|
127
128
|
/** Actionable messages for value-aware rejections, keyed by attribute name. */
|
|
128
129
|
export const RUN_OPTION_REJECT_MESSAGES = {
|
|
130
|
+
terminal: '--terminal opens a tab on THIS machine; it cannot be combined with --host. ' +
|
|
131
|
+
'Drop --terminal to dispatch to the host, or drop --host to open the tab here. ' +
|
|
132
|
+
'To watch a remote run in a terminal, dispatch it and follow with `agents sessions focus <id>`.',
|
|
129
133
|
secrets: '--secrets cannot cross the SSH boundary — Keychain values are never sent to a host implicitly. ' +
|
|
130
134
|
'Provision the bundle on the host first (agents secrets export --host <name>), then run without --secrets; ' +
|
|
131
135
|
'workflow frontmatter secrets resolve from the HOST\'s own keychain.',
|