@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,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device/user config keys — typed read/write over the two-tier agents.yaml store.
|
|
3
|
+
*
|
|
4
|
+
* One registry (`CONFIG_KEYS`) maps each CLI dotted name to where it lives:
|
|
5
|
+
* - user scope → central `~/.agents/agents.yaml` under `config:` (syncs
|
|
6
|
+
* fleet-wide via `agents repo push/pull`)
|
|
7
|
+
* - device scope → `~/.agents/devices/<host>/agents.yaml` under `config:`
|
|
8
|
+
* (per-machine; mirrors how `defaultBrowserProfile` is routed)
|
|
9
|
+
*
|
|
10
|
+
* This machine's keys go through the readMeta/updateMeta funnel (state.ts) so the
|
|
11
|
+
* partition/overlay logic stays the single writer. Another device's doc is
|
|
12
|
+
* read/written in place — the devices/ tree syncs via the DotAgents repo, so
|
|
13
|
+
* editing `devices/mac-mini/agents.yaml` locally is how `configure`/`note`
|
|
14
|
+
* target a peer (`--device`-style).
|
|
15
|
+
*
|
|
16
|
+
* `browser.profile` is NOT a `config:` key — it is the existing
|
|
17
|
+
* `Meta.defaultBrowserProfile` field; the registry entry documents the mapping
|
|
18
|
+
* and set/get route to it so there is one source of truth (no duplicate key).
|
|
19
|
+
*
|
|
20
|
+
* Unset always means today's behavior.
|
|
21
|
+
*/
|
|
22
|
+
import * as fs from 'fs';
|
|
23
|
+
import * as path from 'path';
|
|
24
|
+
import * as yaml from 'yaml';
|
|
25
|
+
import { META_HEADER, getUserAgentsDir, readMeta, updateMeta } from './state.js';
|
|
26
|
+
import { atomicWriteFileSync } from './fs-atomic.js';
|
|
27
|
+
import { machineId } from './machine-id.js';
|
|
28
|
+
import { assertValidDeviceName } from './devices/registry.js';
|
|
29
|
+
export const CONFIG_KEYS = [
|
|
30
|
+
{
|
|
31
|
+
name: 'interactive.host',
|
|
32
|
+
yamlKey: 'interactiveHost',
|
|
33
|
+
scope: 'user',
|
|
34
|
+
type: 'string',
|
|
35
|
+
description: 'Device that shows the user artifacts (browser opens, dashboards) — the "online macOS box" skills should use instead of guessing.',
|
|
36
|
+
validate: (v) => {
|
|
37
|
+
try {
|
|
38
|
+
assertValidDeviceName(v);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
return err?.message ?? String(err);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'browser.profile',
|
|
48
|
+
yamlKey: 'defaultBrowserProfile',
|
|
49
|
+
scope: 'device',
|
|
50
|
+
type: 'string',
|
|
51
|
+
field: 'defaultBrowserProfile',
|
|
52
|
+
description: 'Browser profile `agents browser start` resolves to without --profile (set via `agents browser profiles set-default`).',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'agents.max-concurrent',
|
|
56
|
+
yamlKey: 'maxAgents',
|
|
57
|
+
scope: 'device',
|
|
58
|
+
type: 'int',
|
|
59
|
+
description: 'Cap on concurrent agents on this device. What counts toward it depends on the consumer: ' +
|
|
60
|
+
'Factory auto-launch counts device-wide running agents; teams placement counts the team’s own roster on the device.',
|
|
61
|
+
validate: (v) => (v >= 1 ? null : 'agents.max-concurrent must be >= 1.'),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'scheduler.enabled',
|
|
65
|
+
yamlKey: 'schedulerEnabled',
|
|
66
|
+
scope: 'device',
|
|
67
|
+
type: 'bool',
|
|
68
|
+
description: 'Whether the routines scheduler (daemon) may fire on this device.',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'notes',
|
|
72
|
+
yamlKey: 'notes',
|
|
73
|
+
scope: 'device',
|
|
74
|
+
type: 'string-list',
|
|
75
|
+
description: 'Free-form operator notes about this device (one entry per `agents devices note`).',
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
/** Look up a key spec by CLI dotted name, or throw listing the known keys. */
|
|
79
|
+
export function configKeySpec(name) {
|
|
80
|
+
const spec = CONFIG_KEYS.find((k) => k.name === name);
|
|
81
|
+
if (!spec) {
|
|
82
|
+
throw new Error(`Unknown config key '${name}'. Known keys: ${CONFIG_KEYS.map((k) => k.name).join(', ')}.`);
|
|
83
|
+
}
|
|
84
|
+
return spec;
|
|
85
|
+
}
|
|
86
|
+
/** Throw when `value` does not match the key's declared type or validation. */
|
|
87
|
+
function assertValidValue(spec, value) {
|
|
88
|
+
switch (spec.type) {
|
|
89
|
+
case 'string':
|
|
90
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
91
|
+
throw new Error(`Config key '${spec.name}' expects a non-empty string, got ${JSON.stringify(value)}.`);
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
case 'int':
|
|
95
|
+
if (!Number.isInteger(value)) {
|
|
96
|
+
throw new Error(`Config key '${spec.name}' expects an integer, got ${JSON.stringify(value)}.`);
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
case 'bool':
|
|
100
|
+
if (typeof value !== 'boolean') {
|
|
101
|
+
throw new Error(`Config key '${spec.name}' expects a boolean, got ${JSON.stringify(value)}.`);
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
case 'string-list':
|
|
105
|
+
if (!Array.isArray(value) || value.some((v) => typeof v !== 'string')) {
|
|
106
|
+
throw new Error(`Config key '${spec.name}' expects a list of strings, got ${JSON.stringify(value)}.`);
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
const err = spec.validate?.(value);
|
|
111
|
+
if (err)
|
|
112
|
+
throw new Error(`Invalid value for '${spec.name}': ${err}`);
|
|
113
|
+
}
|
|
114
|
+
// ─── Sibling-device doc access ────────────────────────────────────────────────
|
|
115
|
+
/** Path to any device's doc (self or a peer) under the synced devices/ tree. */
|
|
116
|
+
function deviceDocPath(device) {
|
|
117
|
+
return path.join(getUserAgentsDir(), 'devices', device, 'agents.yaml');
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Read a device doc directly. Returns null when the file does not exist. A
|
|
121
|
+
* malformed file is a hard error — silently returning null would let the next
|
|
122
|
+
* write wipe the device's pins/default (same contract as the device registry).
|
|
123
|
+
* A valid-but-non-map document (a bare string, a list) is the same kind of
|
|
124
|
+
* corruption: reject it here instead of failing later with a TypeError.
|
|
125
|
+
*/
|
|
126
|
+
function readDeviceDoc(device) {
|
|
127
|
+
const p = deviceDocPath(device);
|
|
128
|
+
let raw;
|
|
129
|
+
try {
|
|
130
|
+
raw = fs.readFileSync(p, 'utf-8');
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
if (err && err.code === 'ENOENT')
|
|
134
|
+
return null;
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
const corrupted = (detail) => new Error(`Device config corrupted at ${p}: ${detail}. Inspect and restore from backup.`);
|
|
138
|
+
let parsed;
|
|
139
|
+
try {
|
|
140
|
+
parsed = yaml.parse(raw);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
throw corrupted(err?.message ?? String(err));
|
|
144
|
+
}
|
|
145
|
+
if (parsed === null || parsed === undefined)
|
|
146
|
+
return {};
|
|
147
|
+
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
148
|
+
throw corrupted(`expected a YAML map, got ${Array.isArray(parsed) ? 'a list' : JSON.stringify(parsed)}`);
|
|
149
|
+
}
|
|
150
|
+
return parsed;
|
|
151
|
+
}
|
|
152
|
+
/** Write a device doc in place (atomic). Only ever holds device-local fields. */
|
|
153
|
+
function writeDeviceDoc(device, doc) {
|
|
154
|
+
const p = deviceDocPath(device);
|
|
155
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
156
|
+
const body = Object.keys(doc).length > 0 ? doc : { agents: {} };
|
|
157
|
+
atomicWriteFileSync(p, META_HEADER + yaml.stringify(body));
|
|
158
|
+
}
|
|
159
|
+
// ─── Reads ────────────────────────────────────────────────────────────────────
|
|
160
|
+
function entryFromMeta(spec, meta) {
|
|
161
|
+
if (spec.field === 'defaultBrowserProfile') {
|
|
162
|
+
const value = meta.defaultBrowserProfile;
|
|
163
|
+
return { spec, value, layer: value !== undefined ? 'device' : undefined };
|
|
164
|
+
}
|
|
165
|
+
if (spec.scope === 'user') {
|
|
166
|
+
const value = meta.config?.[spec.yamlKey];
|
|
167
|
+
return { spec, value, layer: value !== undefined ? 'user' : undefined };
|
|
168
|
+
}
|
|
169
|
+
const value = meta.deviceConfig?.[spec.yamlKey];
|
|
170
|
+
return { spec, value, layer: value !== undefined ? 'device' : undefined };
|
|
171
|
+
}
|
|
172
|
+
/** True when `device` names this machine (case-insensitive, mirroring
|
|
173
|
+
* `isLocalDevice` in teams/scheduler.ts) — `configure ZION` on host zion must
|
|
174
|
+
* take the self path (readMeta/updateMeta funnel), not the peer-doc path. */
|
|
175
|
+
function isSelfDevice(device) {
|
|
176
|
+
return device.toLowerCase() === machineId();
|
|
177
|
+
}
|
|
178
|
+
/** Get one config key's value and the layer that set it. */
|
|
179
|
+
export function getConfigValue(name, opts) {
|
|
180
|
+
const spec = configKeySpec(name);
|
|
181
|
+
if (spec.scope === 'device' && opts?.device && !isSelfDevice(opts.device)) {
|
|
182
|
+
const doc = readDeviceDoc(opts.device) ?? {};
|
|
183
|
+
return entryFromMeta(spec, { deviceConfig: doc.config, defaultBrowserProfile: doc.defaultBrowserProfile });
|
|
184
|
+
}
|
|
185
|
+
return entryFromMeta(spec, readMeta());
|
|
186
|
+
}
|
|
187
|
+
/** List every known key with its value and the layer that set it. */
|
|
188
|
+
export function listConfig(opts) {
|
|
189
|
+
return CONFIG_KEYS.map((spec) => getConfigValue(spec.name, opts));
|
|
190
|
+
}
|
|
191
|
+
// ─── Writes ───────────────────────────────────────────────────────────────────
|
|
192
|
+
function setInMeta(spec, value) {
|
|
193
|
+
updateMeta((m) => {
|
|
194
|
+
if (spec.field === 'defaultBrowserProfile') {
|
|
195
|
+
return { ...m, defaultBrowserProfile: value };
|
|
196
|
+
}
|
|
197
|
+
if (spec.scope === 'user') {
|
|
198
|
+
return { ...m, config: { ...m.config, [spec.yamlKey]: value } };
|
|
199
|
+
}
|
|
200
|
+
return { ...m, deviceConfig: { ...m.deviceConfig, [spec.yamlKey]: value } };
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
function unsetInMeta(spec) {
|
|
204
|
+
updateMeta((m) => {
|
|
205
|
+
if (spec.field === 'defaultBrowserProfile') {
|
|
206
|
+
const { defaultBrowserProfile, ...rest } = m;
|
|
207
|
+
void defaultBrowserProfile;
|
|
208
|
+
return rest;
|
|
209
|
+
}
|
|
210
|
+
const block = spec.scope === 'user' ? m.config : m.deviceConfig;
|
|
211
|
+
if (!block || !(spec.yamlKey in block))
|
|
212
|
+
return m;
|
|
213
|
+
const next = { ...block };
|
|
214
|
+
delete next[spec.yamlKey];
|
|
215
|
+
const cleaned = Object.keys(next).length > 0 ? next : undefined;
|
|
216
|
+
return spec.scope === 'user' ? { ...m, config: cleaned } : { ...m, deviceConfig: cleaned };
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
function setInDeviceDoc(device, spec, value) {
|
|
220
|
+
const doc = readDeviceDoc(device) ?? {};
|
|
221
|
+
if (spec.field === 'defaultBrowserProfile') {
|
|
222
|
+
doc.defaultBrowserProfile = value;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
doc.config = { ...doc.config, [spec.yamlKey]: value };
|
|
226
|
+
}
|
|
227
|
+
writeDeviceDoc(device, doc);
|
|
228
|
+
}
|
|
229
|
+
function unsetInDeviceDoc(device, spec) {
|
|
230
|
+
const doc = readDeviceDoc(device);
|
|
231
|
+
if (!doc)
|
|
232
|
+
return; // nothing stored — unset is a no-op
|
|
233
|
+
if (spec.field === 'defaultBrowserProfile') {
|
|
234
|
+
delete doc.defaultBrowserProfile;
|
|
235
|
+
}
|
|
236
|
+
else if (doc.config && spec.yamlKey in doc.config) {
|
|
237
|
+
delete doc.config[spec.yamlKey];
|
|
238
|
+
if (Object.keys(doc.config).length === 0)
|
|
239
|
+
delete doc.config;
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
return; // key not present — no write needed
|
|
243
|
+
}
|
|
244
|
+
writeDeviceDoc(device, doc);
|
|
245
|
+
}
|
|
246
|
+
/** Set a config key (validated). Device-scope keys target this machine unless `opts.device` names a peer. */
|
|
247
|
+
export function setConfigValue(name, value, opts) {
|
|
248
|
+
const spec = configKeySpec(name);
|
|
249
|
+
assertValidValue(spec, value);
|
|
250
|
+
if (spec.scope === 'device' && opts?.device && !isSelfDevice(opts.device)) {
|
|
251
|
+
setInDeviceDoc(opts.device, spec, value);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
setInMeta(spec, value);
|
|
255
|
+
}
|
|
256
|
+
/** Unset a config key — restores default behavior. No-op when already unset. */
|
|
257
|
+
export function unsetConfigValue(name, opts) {
|
|
258
|
+
const spec = configKeySpec(name);
|
|
259
|
+
if (spec.scope === 'device' && opts?.device && !isSelfDevice(opts.device)) {
|
|
260
|
+
unsetInDeviceDoc(opts.device, spec);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
unsetInMeta(spec);
|
|
264
|
+
}
|
|
265
|
+
// ─── Consumers' helpers ───────────────────────────────────────────────────────
|
|
266
|
+
/** True unless this machine's device doc disables the routines scheduler. */
|
|
267
|
+
export function isSchedulerEnabled() {
|
|
268
|
+
return getConfigValue('scheduler.enabled').value !== false;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Throw when the routines scheduler is disabled on this machine, naming the
|
|
272
|
+
* setting and the fix. The single message every scheduler-start surface
|
|
273
|
+
* (auto-start on `routines add`, manual `routines start`, the daemon's own
|
|
274
|
+
* scheduler init) refuses with.
|
|
275
|
+
*/
|
|
276
|
+
export function assertSchedulerEnabled() {
|
|
277
|
+
if (isSchedulerEnabled())
|
|
278
|
+
return;
|
|
279
|
+
throw new Error(`The routines scheduler is disabled on this device (scheduler.enabled=false in ~/.agents/devices/${machineId()}/agents.yaml). ` +
|
|
280
|
+
`Re-enable with: agents devices configure ${machineId()} --scheduler on`);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Read the `agents.max-concurrent` cap for each named device from its synced
|
|
284
|
+
* device doc (no SSH). Devices without a cap are omitted — uncapped is the
|
|
285
|
+
* default. Used as an input to host ranking (teams placement, Factory
|
|
286
|
+
* auto-launch), never as a remote probe.
|
|
287
|
+
*/
|
|
288
|
+
export function readMaxConcurrentCaps(devices) {
|
|
289
|
+
const caps = {};
|
|
290
|
+
for (const device of devices) {
|
|
291
|
+
const value = getConfigValue('agents.max-concurrent', { device }).value;
|
|
292
|
+
if (typeof value === 'number')
|
|
293
|
+
caps[device] = value;
|
|
294
|
+
}
|
|
295
|
+
return caps;
|
|
296
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ActorKind } from './actor.js';
|
|
2
|
+
export interface EventProvenance {
|
|
3
|
+
osUser: string;
|
|
4
|
+
transport: 'local' | 'ssh';
|
|
5
|
+
sshClientIp?: string;
|
|
6
|
+
actor: string;
|
|
7
|
+
kind: ActorKind;
|
|
8
|
+
machineId: string;
|
|
9
|
+
sessionId?: string;
|
|
10
|
+
agent?: string;
|
|
11
|
+
launchId?: string;
|
|
12
|
+
parentSessionId?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Stamp the shared identity floor used by both operational and activity events.
|
|
16
|
+
* Explicit event payload fields may override these defaults at the call site.
|
|
17
|
+
*/
|
|
18
|
+
export declare function stampProvenance(env?: NodeJS.ProcessEnv): EventProvenance;
|
|
19
|
+
export declare function resetEventProvenanceForTest(): void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as os from 'os';
|
|
2
|
+
import { resolveActor } from './actor.js';
|
|
3
|
+
import { machineId } from './machine-id.js';
|
|
4
|
+
import { parseSshConnection } from './session/provenance.js';
|
|
5
|
+
let cachedOrigin;
|
|
6
|
+
let cachedDeviceId;
|
|
7
|
+
/**
|
|
8
|
+
* Stamp the shared identity floor used by both operational and activity events.
|
|
9
|
+
* Explicit event payload fields may override these defaults at the call site.
|
|
10
|
+
*/
|
|
11
|
+
export function stampProvenance(env = process.env) {
|
|
12
|
+
if (!cachedOrigin) {
|
|
13
|
+
let osUser = 'unknown';
|
|
14
|
+
try {
|
|
15
|
+
osUser = os.userInfo().username;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// A uid without a passwd entry has no attributable OS user.
|
|
19
|
+
}
|
|
20
|
+
const ssh = env.SSH_CONNECTION ? parseSshConnection(env.SSH_CONNECTION) : undefined;
|
|
21
|
+
const actor = resolveActor();
|
|
22
|
+
cachedOrigin = {
|
|
23
|
+
osUser,
|
|
24
|
+
transport: ssh ? 'ssh' : 'local',
|
|
25
|
+
...(ssh ? { sshClientIp: ssh.clientIp } : {}),
|
|
26
|
+
actor: actor.id,
|
|
27
|
+
kind: actor.kind,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const provenance = {
|
|
31
|
+
...cachedOrigin,
|
|
32
|
+
machineId: (cachedDeviceId ??= machineId()),
|
|
33
|
+
};
|
|
34
|
+
const sessionId = env.AGENT_SESSION_ID || env.AGENTS_SESSION_ID;
|
|
35
|
+
if (sessionId)
|
|
36
|
+
provenance.sessionId = sessionId;
|
|
37
|
+
if (env.AGENTS_AGENT_NAME)
|
|
38
|
+
provenance.agent = env.AGENTS_AGENT_NAME;
|
|
39
|
+
if (env.AGENT_LAUNCH_ID)
|
|
40
|
+
provenance.launchId = env.AGENT_LAUNCH_ID;
|
|
41
|
+
if (env.AGENTS_PARENT_SESSION_ID)
|
|
42
|
+
provenance.parentSessionId = env.AGENTS_PARENT_SESSION_ID;
|
|
43
|
+
return provenance;
|
|
44
|
+
}
|
|
45
|
+
export function resetEventProvenanceForTest() {
|
|
46
|
+
cachedOrigin = undefined;
|
|
47
|
+
cachedDeviceId = undefined;
|
|
48
|
+
}
|
|
@@ -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
|
@@ -11,9 +11,13 @@
|
|
|
11
11
|
* - Permissions: logs dir is 0700, files are 0600 (owner-only)
|
|
12
12
|
* - Performance tracking: withTiming() wrapper for any async function
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
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;
|
|
@@ -42,7 +46,7 @@ export interface EventMeta {
|
|
|
42
46
|
/** Resolved actor id — which human/agent is behind this event (RUSH-2020). */
|
|
43
47
|
actor?: string;
|
|
44
48
|
/** Actor kind (`human`/`agent`). */
|
|
45
|
-
kind?: ActorKind;
|
|
49
|
+
kind?: ActorKind | 'unknown';
|
|
46
50
|
}
|
|
47
51
|
export interface EventPayload {
|
|
48
52
|
agent?: 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.
|