@phnx-labs/agents-cli 1.22.11 → 1.22.13

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +5 -1
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/cli.js +12 -12
  5. package/dist/commands/events.d.ts +20 -1
  6. package/dist/commands/events.js +71 -55
  7. package/dist/commands/logs.js +14 -138
  8. package/dist/commands/repo.d.ts +1 -1
  9. package/dist/commands/repo.js +3 -3
  10. package/dist/commands/sessions.d.ts +15 -0
  11. package/dist/commands/sessions.js +79 -10
  12. package/dist/commands/view.d.ts +1 -1
  13. package/dist/commands/view.js +7 -7
  14. package/dist/commands/workflows.js +1 -0
  15. package/dist/lib/cli-resources.js +2 -2
  16. package/dist/lib/event-stream.d.ts +1 -1
  17. package/dist/lib/event-stream.js +1 -1
  18. package/dist/lib/events.d.ts +21 -12
  19. package/dist/lib/events.js +312 -65
  20. package/dist/lib/exec.js +1 -2
  21. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  22. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  23. package/dist/lib/migrate.d.ts +8 -0
  24. package/dist/lib/migrate.js +26 -62
  25. package/dist/lib/resources/workflows.js +41 -21
  26. package/dist/lib/resources.d.ts +1 -1
  27. package/dist/lib/runner.js +1 -2
  28. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  29. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  30. package/dist/lib/secrets/audit.js +1 -1
  31. package/dist/lib/startup/command-registry.js +1 -1
  32. package/dist/lib/state.d.ts +1 -1
  33. package/dist/lib/state.js +60 -2
  34. package/dist/lib/watchdog/runner.d.ts +13 -0
  35. package/dist/lib/watchdog/runner.js +75 -6
  36. package/dist/lib/workflows.d.ts +22 -4
  37. package/dist/lib/workflows.js +79 -29
  38. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.22.13
4
+
5
+ - **`agents sessions` accepts direct live-state flags and remains fleet-wide by default.** `--working`, `--idle`, `--waiting`, `--orphan`/`--orphaned`, `--crashed`, `--closed`, `--abandoned`, `--queued`, and `--unknown` each imply the live scan; multiple flags form a union. `--working` is narrower than `--active`: it excludes idle, waiting, and lifecycle-failure rows. Cross-device collection was already the default and stays that way; `--local` opts out, while `--all` continues to widen historical directory and time scope. Source: `apps/cli/src/commands/sessions.ts`, `apps/cli/src/commands/sessions.test.ts`.
6
+
7
+ - **Workflows: `name@source` disambiguation (Phase 5 packaging).** When two plugins (or a plugin and an extra repo) ship the same workflow name, pin the source: `agents run deploy@ship-tools` or `agents run workflow:deploy@social`. Bare names keep layered precedence (project > user > plugin > extra > system); a missing source returns no match instead of silently falling back. Source: `apps/cli/src/lib/workflows.ts`, `apps/cli/src/lib/resources/workflows.ts`.
8
+
9
+ ## 1.22.12
10
+
11
+ - Store operational events in daily history directories, retain 7 days and at most 50 MiB automatically, and make `agents logs audit` use the `agents events --audit` reader.
12
+
13
+ - **`agents cli` renamed to `agents clis`; resource directory `cli/` renamed to `clis/`.** The CLI resource kind and its subdirectory are now plural throughout: `ResourceKind` changes from `'cli'` to `'clis'`, manifests live at `clis/<name>.yaml`, `agents clis` is the only command surface (no `agents cli` alias), and `agents view --clis` replaces `--cli`. A startup migration renames any existing `cli/` directory to `clis/` in the user, system, and project `.agents/` layers; if both `cli/` and `clis/` are present the migration fails with a clear error rather than silently merging. Source: `apps/cli/src/lib/resources.ts`, `apps/cli/src/lib/cli-resources.ts`, `apps/cli/src/commands/cli.ts`, `apps/cli/src/lib/startup/command-registry.ts`, `apps/cli/src/commands/repo.ts`, `apps/cli/src/commands/view.ts`, `apps/cli/src/lib/migrate.ts`.
14
+
15
+ - **`agents.yaml` no longer silently loses top-level keys across a version-skewed
16
+ fleet.** `serializeCentral` (`lib/state.ts`) rewrote the synced `agents.yaml`
17
+ with a delete-any-key-not-in-the-in-memory-object pass. An **older CLI version
18
+ whose `Meta` type predated a key** (`beta:`, `notify.owner`, `feed:`, imported
19
+ `projects`) would parse the file, never surface that key, delete it on the next
20
+ write, and sync the deletion to every machine — the recurring "my config
21
+ vanished" data-loss (see the restore in commit `04295e3`). The delete pass now
22
+ consults a `Record<keyof Meta, 'central' | 'device'>` scope map (compile-time
23
+ exhaustive — a new `Meta` field that isn't classified fails the build) and
24
+ deletes **only keys this version knows** (a cleared central key, or a device
25
+ key that is legacy cruft in the synced file); a key it doesn't know is
26
+ preserved verbatim. Once a machine runs a CLI carrying this fix, it can never
27
+ drop a newer version's key again. Source: `apps/cli/src/lib/state.ts`,
28
+ `apps/cli/src/lib/__tests__/state.test.ts`.
29
+
30
+ - **Watchdog files a feed block only when a session genuinely needs the human.**
31
+ When the smart brain concludes a stalled session must be left for the human
32
+ (`needsHuman`), the watchdog now surfaces that on the owner's feed instead of
33
+ dropping it in a menubar-only flag. Two cases: if the session is addressable it
34
+ injects a self-file reminder into the agent ("You appear stuck. File it: `agents
35
+ feed post … --blocked`") so the agent declares its own block; if it is
36
+ un-addressable — the case where the watchdog can't even reach the terminal to
37
+ remind it — the watchdog files a declared block on the agent's behalf so the owner
38
+ is still paged. Paging fires **only** on this confirmed-needs-human path: a plain
39
+ nudge-worthy drive-forward poke (un-addressable or under a hands-off policy) is
40
+ flagged for the tray but never texts the owner. Both paths are gated by the
41
+ existing cooldown ledger (at most once per `WATCHDOG_COOLDOWN_MS` window) and are
42
+ no-ops when a block for the session already exists, so no double-paging. Source:
43
+ `apps/cli/src/lib/watchdog/runner.ts` (`NudgeDecision.needsHuman`,
44
+ `WatchdogTickOptions.publishBlockFn`, the needs-human skip branch),
45
+ `apps/cli/src/lib/watchdog/runner.test.ts`.
46
+
3
47
  ## 1.22.11
4
48
 
5
49
  - **`--blocked` iMessage notifications are now phone-actionable.** The forwarded message dropped the block's `--option`s, `--default`, and timeout and instead showed `agents focus <id>` — a CLI command that is useless on a phone. It now shows the choices (`Options: publish / wait`) and the safe-default fallback (`Default in 15 min: wait`) and omits the `agents focus` line, so a `--blocked` post that carries a `--default` self-resolves when the owner can't reply. Source: `apps/cli/src/lib/feed-broadcast.ts`.
package/README.md CHANGED
@@ -292,6 +292,10 @@ Search is the past tense. `--active` is the present -- it infers what each runni
292
292
 
293
293
  ```bash
294
294
  agents sessions --active # every live run across the fleet, with state
295
+ agents sessions --working # actively producing work (fleet-wide)
296
+ agents sessions --idle # stopped between turns (fleet-wide)
297
+ agents sessions --orphan # agent outlived its terminal client
298
+ agents sessions --crashed # terminal and agent disappeared uncleanly
295
299
  agents sessions focus a1b2c3d4 # jump back into one — attach in place, or resume
296
300
  ```
297
301
 
@@ -322,7 +326,7 @@ Filters **stack** (they AND together), the active set shows in the header, and t
322
326
  | --- | --- |
323
327
  | ![sessions browser, preview hidden](assets/demos/sessions-preview-before.png) | ![sessions browser, preview open with a links line](assets/demos/sessions-preview-after.png) |
324
328
 
325
- Each live session resolves to `working`, `waiting_input` (with why -- a question, a plan review, or a permission prompt), or `idle`, alongside badges for the PR it opened, the worktree it sits in, and the ticket it's working. `agents sessions focus [id]` attaches the live pane in place -- the tmux split locally or over SSH, or its Ghostty tab -- and falls back to a fresh tab + resume when the terminal is gone.
329
+ Each live session resolves to `working`, `waiting_input` (with why -- a question, a plan review, or a permission prompt), `idle`, or a lifecycle state such as `orphaned`, `crashed`, `closed`, `abandoned`, `queued`, or `unknown`. Pass the matching flag (`--working`, `--idle`, `--waiting`, `--orphan`, `--crashed`, `--closed`, `--abandoned`, `--queued`, `--unknown`) directly; each implies `--active`, and several flags form a union. The fleet fan-out is already the default; `--local` opts out. `--all` instead widens historical directory and time scope. Rows also carry badges for the PR, worktree, and ticket. `agents sessions focus [id]` attaches the live pane in place -- the tmux split locally or over SSH, or its Ghostty tab -- and falls back to a fresh tab + resume when the terminal is gone.
326
330
 
327
331
  Landing on a session cold? `agents sessions <id>` prints a catch-up digest: an inferred title, files changed grouped by directory (created / modified / deleted), a histogram of which tools did the work (including parsed Bash commands -- `git`, `npm`, `ffmpeg`, `ssh`, and so on), and the last test verdict -- the signals to reload a task in seconds.
328
332
 
package/dist/bin/agents CHANGED
Binary file
@@ -6,13 +6,13 @@ import { listCliManifests, listCliStatus, resolveCliManifest, installCli, descri
6
6
  import { getUserAgentsDir } from '../lib/state.js';
7
7
  import { isPromptCancelled } from './utils.js';
8
8
  function userCliDir() {
9
- return path.join(getUserAgentsDir(), 'cli');
9
+ return path.join(getUserAgentsDir(), 'clis');
10
10
  }
11
11
  /** Render the status table — one row per declared CLI. */
12
12
  function printStatus(rows) {
13
13
  if (rows.length === 0) {
14
14
  console.log(chalk.gray('No CLIs declared.'));
15
- console.log(chalk.gray(`Create one with: agents cli add <name>`));
15
+ console.log(chalk.gray(`Create one with: agents clis add <name>`));
16
16
  return;
17
17
  }
18
18
  const nameWidth = Math.max(4, ...rows.map((r) => r.manifest.name.length));
@@ -28,32 +28,32 @@ function printStatus(rows) {
28
28
  }
29
29
  export function registerCliCommands(program) {
30
30
  const cliCmd = program
31
- .command('cli')
31
+ .command('clis')
32
32
  .description('Declare and install host CLI binaries (gh, higgsfield, glab, ...)')
33
33
  .addHelpText('after', `
34
- CLI manifests live in <repo>/cli/<name>.yaml and declare how to install a
35
- binary on the host. On a fresh machine, 'agents cli install' runs the first
34
+ CLI manifests live in <repo>/clis/<name>.yaml and declare how to install a
35
+ binary on the host. On a fresh machine, 'agents clis install' runs the first
36
36
  compatible method (npm > brew > script > binary) for every declared entry.
37
37
 
38
38
  Examples:
39
39
  # See which declared CLIs are installed on this host
40
- agents cli list
40
+ agents clis list
41
41
 
42
42
  # Install everything that's missing
43
- agents cli install
43
+ agents clis install
44
44
 
45
45
  # Install one
46
- agents cli install higgsfield
46
+ agents clis install higgsfield
47
47
 
48
48
  # Show the manifest detail
49
- agents cli view higgsfield
49
+ agents clis view higgsfield
50
50
 
51
51
  # Exit 0 if all declared CLIs are installed (use in CI / setup scripts)
52
- agents cli check
52
+ agents clis check
53
53
 
54
54
  When to use:
55
55
  - After 'agents sync' on a new machine, to materialize host binaries
56
- - In a team setup: commit cli/ entries so teammates get the same toolchain
56
+ - In a team setup: commit clis/ entries so teammates get the same toolchain
57
57
  `);
58
58
  cliCmd
59
59
  .command('list')
@@ -206,7 +206,7 @@ When to use:
206
206
  });
207
207
  cliCmd
208
208
  .command('add <name>')
209
- .description('Scaffold a new manifest at ~/.agents/cli/<name>.yaml')
209
+ .description('Scaffold a new manifest at ~/.agents/clis/<name>.yaml')
210
210
  .option('--npm <pkg>', 'declare an npm install method')
211
211
  .option('--brew <formula>', 'declare a brew install method')
212
212
  .option('--script <url>', 'declare a curl|sh install method')
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `agents events` — read the unified event stream.
3
3
  *
4
- * One stream over BOTH operational events (`~/.agents/.history/events/events.jsonl`: every
4
+ * One stream over BOTH operational events (`~/.agents/.history/events/YYYY-MM-DD/`: every
5
5
  * `agents <module> <cmd>` invocation plus typed events like secrets access,
6
6
  * version installs) AND agent-semantic events (the per-session activity logs:
7
7
  * plans, PRs, worktrees, sub-agents, artifacts). Each is stamped with who ran
@@ -30,4 +30,23 @@ export declare function capRecords<T>(fetched: T[], limit: number | undefined):
30
30
  records: T[];
31
31
  truncated: boolean;
32
32
  };
33
+ export interface EventsOptions {
34
+ module?: string;
35
+ command?: string;
36
+ event?: string[];
37
+ agent?: string;
38
+ caller?: string;
39
+ level?: string;
40
+ session?: string;
41
+ bundle?: string;
42
+ since?: string;
43
+ limit?: string;
44
+ json?: boolean;
45
+ follow?: boolean;
46
+ audit?: boolean;
47
+ }
48
+ /** Add the one canonical event-reader option surface to a command or alias. */
49
+ export declare function addEventsReadOptions(command: Command, includeAuditFlag?: boolean): Command;
50
+ /** Canonical reader used by both `agents events` and `agents logs audit`. */
51
+ export declare function runEventsCommand(options: EventsOptions, forceAudit?: boolean): Promise<void>;
33
52
  export declare function registerEventsCommand(program: Command): void;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `agents events` — read the unified event stream.
3
3
  *
4
- * One stream over BOTH operational events (`~/.agents/.history/events/events.jsonl`: every
4
+ * One stream over BOTH operational events (`~/.agents/.history/events/YYYY-MM-DD/`: every
5
5
  * `agents <module> <cmd>` invocation plus typed events like secrets access,
6
6
  * version installs) AND agent-semantic events (the per-session activity logs:
7
7
  * plans, PRs, worktrees, sub-agents, artifacts). Each is stamped with who ran
@@ -184,21 +184,77 @@ function registerEmitSubcommand(events) {
184
184
  `,
185
185
  });
186
186
  }
187
- export function registerEventsCommand(program) {
188
- const events = program
189
- .command('events')
190
- .description('Read the unified event stream (operational + agent activity)')
187
+ /** Add the one canonical event-reader option surface to a command or alias. */
188
+ export function addEventsReadOptions(command, includeAuditFlag = true) {
189
+ command
191
190
  .option('--module <name>', 'Only events from this group (e.g. teams, secrets, activity)')
192
191
  .option('--command <path>', 'Only this command path — prefix match (e.g. "teams create")')
193
192
  .option('--event <type>', 'Only this typed event (repeatable, e.g. secrets.get, secrets.unlocked, pr.opened)', collect, [])
194
193
  .option('--agent <name>', 'Only events tagged with this agent')
194
+ .option('--caller <kind>', 'Only this caller kind (claude-code, codex, gemini, cursor, terminal, script)')
195
+ .option('--level <level>', 'Only this level: audit, warn, info, debug')
195
196
  .option('--session <id>', 'Only events from this session (the provenance sessionId) — e.g. trace which session read a secret')
196
197
  .option('--bundle <name>', 'Only events carrying this bundle in their payload — e.g. `--module secrets --bundle share` for every read of the share bundle')
197
198
  .option('--since <time>', 'Only events newer than this (e.g. 2h, 7d, or ISO date)')
198
- .option('--audit', 'Operational events only (skip agent activity)')
199
199
  .option('--limit <n>', 'Max records to show; 0 for no cap (default 50)', '50')
200
200
  .option('--json', 'Output raw records as JSON')
201
- .option('-f, --follow', "Tail today's operational log live")
201
+ .option('-f, --follow', "Tail today's operational log live");
202
+ if (includeAuditFlag)
203
+ command.option('--audit', 'Operational events only (skip agent activity)');
204
+ return command;
205
+ }
206
+ /** Canonical reader used by both `agents events` and `agents logs audit`. */
207
+ export async function runEventsCommand(options, forceAudit = false) {
208
+ if (options.follow) {
209
+ await followLog();
210
+ return;
211
+ }
212
+ let limit;
213
+ let startDate;
214
+ try {
215
+ limit = resolveEventsLimit(options.limit);
216
+ startDate = options.since ? parseSince(options.since) : undefined;
217
+ }
218
+ catch (err) {
219
+ console.error(chalk.red(err.message));
220
+ process.exitCode = 2;
221
+ return;
222
+ }
223
+ const fetched = readUnifiedEvents({
224
+ startDate,
225
+ eventTypes: options.event && options.event.length ? options.event : undefined,
226
+ level: options.level,
227
+ agent: options.agent,
228
+ sessionId: options.session,
229
+ bundle: options.bundle,
230
+ caller: options.caller,
231
+ command: options.command,
232
+ module: options.module,
233
+ limit: limit === undefined ? undefined : limit + 1,
234
+ includeActivity: !(forceAudit || options.audit),
235
+ });
236
+ const { records, truncated } = capRecords(fetched, limit);
237
+ const capNote = `Showing the newest ${limit} — more events matched. Pass --limit 0 for all.`;
238
+ if (options.json) {
239
+ if (truncated)
240
+ console.error(chalk.yellow(capNote));
241
+ console.log(JSON.stringify(records, null, 2));
242
+ return;
243
+ }
244
+ if (records.length === 0) {
245
+ console.log(chalk.gray('No matching events.'));
246
+ return;
247
+ }
248
+ for (const r of records.slice().reverse())
249
+ console.log(renderRow(r));
250
+ console.log(chalk.gray(`\n${records.length} event(s). Log: ${getLogsPath()}`));
251
+ if (truncated)
252
+ console.log(chalk.yellow(capNote));
253
+ }
254
+ export function registerEventsCommand(program) {
255
+ const events = addEventsReadOptions(program
256
+ .command('events')
257
+ .description('Read the unified event stream (operational + agent activity)'))
202
258
  .addHelpText('after', `
203
259
  Examples:
204
260
  agents events Everything — ops + agent activity
@@ -214,53 +270,7 @@ Examples:
214
270
  agents events --event pr.opened --since 30d --limit 0 --json
215
271
  Every match — use --limit 0 whenever you
216
272
  aggregate, or you rank only the newest 50`)
217
- .action(async (options) => {
218
- if (options.follow) {
219
- await followLog();
220
- return;
221
- }
222
- let limit;
223
- let startDate;
224
- try {
225
- limit = resolveEventsLimit(options.limit);
226
- startDate = options.since ? parseSince(options.since) : undefined;
227
- }
228
- catch (err) {
229
- console.error(chalk.red(err.message));
230
- process.exit(2);
231
- }
232
- // Read one past the cap so we can tell a full result from a clipped one.
233
- const fetched = readUnifiedEvents({
234
- startDate,
235
- eventTypes: options.event && options.event.length ? options.event : undefined,
236
- agent: options.agent,
237
- sessionId: options.session,
238
- bundle: options.bundle,
239
- command: options.command,
240
- module: options.module,
241
- limit: limit === undefined ? undefined : limit + 1,
242
- includeActivity: !options.audit,
243
- });
244
- const { records, truncated } = capRecords(fetched, limit);
245
- const capNote = `Showing the newest ${limit} — more events matched. Pass --limit 0 for all.`;
246
- if (options.json) {
247
- // Notice goes to stderr so `--json | jq` still receives clean JSON.
248
- if (truncated)
249
- console.error(chalk.yellow(capNote));
250
- console.log(JSON.stringify(records, null, 2));
251
- return;
252
- }
253
- if (records.length === 0) {
254
- console.log(chalk.gray('No matching events.'));
255
- return;
256
- }
257
- // query() returns newest-first; print oldest-first so a tail reads naturally.
258
- for (const r of records.slice().reverse())
259
- console.log(renderRow(r));
260
- console.log(chalk.gray(`\n${records.length} event(s). Log: ${getLogsPath()}`));
261
- if (truncated)
262
- console.log(chalk.yellow(capNote));
263
- });
273
+ .action((_options, command) => runEventsCommand(command.optsWithGlobals()));
264
274
  // `events` both reads (its own action, above) and writes (this subcommand) —
265
275
  // the same shape as `feed` / `feed post`.
266
276
  registerEmitSubcommand(events);
@@ -271,7 +281,7 @@ function collect(value, previous) {
271
281
  }
272
282
  /** Tail today's event file, printing new lines as they land. */
273
283
  async function followLog() {
274
- const file = getLogsPath();
284
+ let file = getLogsPath();
275
285
  let offset = 0;
276
286
  try {
277
287
  offset = fs.statSync(file).size;
@@ -281,6 +291,12 @@ async function followLog() {
281
291
  }
282
292
  console.log(chalk.gray(`Tailing ${file} — Ctrl-C to stop`));
283
293
  const drain = () => {
294
+ const nextFile = getLogsPath();
295
+ if (nextFile !== file) {
296
+ file = nextFile;
297
+ offset = 0;
298
+ console.log(chalk.gray(`Tailing ${file}`));
299
+ }
284
300
  let size = 0;
285
301
  try {
286
302
  size = fs.statSync(file).size;
@@ -21,14 +21,14 @@
21
21
  * the same underlying helpers (showHostTaskLog / streamSessionTail).
22
22
  */
23
23
  import chalk from 'chalk';
24
- import * as fs from 'fs';
25
24
  import { discoverSessions, resolveSessionById } from '../lib/session/discover.js';
26
25
  import { parseAgentFilter, renderSessionLog, renderSessionLogJson } from './sessions.js';
27
26
  import { streamSessionTail, isTailable } from './sessions-tail.js';
28
27
  import { showHostTaskLog, hostTaskLogJson } from '../lib/hosts/logs.js';
29
28
  import { listTasks } from '../lib/hosts/tasks.js';
30
29
  import { itemPicker } from '../lib/picker.js';
31
- import { query, stats, getLogsPath, rotate, levelFor, } from '../lib/events.js';
30
+ import { stats, getLogsPath, rotate, } from '../lib/events.js';
31
+ import { addEventsReadOptions, runEventsCommand } from './events.js';
32
32
  /** Compact one-line label used by both the picker and the non-TTY list. */
33
33
  function candidateLabel(c) {
34
34
  if (c.kind === 'task') {
@@ -178,32 +178,6 @@ function parseSince(s) {
178
178
  throw new Error(`Invalid --since value: ${s} (use e.g. 2h, 7d, or an ISO date)`);
179
179
  return new Date(ms);
180
180
  }
181
- function originLabel(r) {
182
- if (r.transport === 'ssh') {
183
- return chalk.yellow(`ssh${r.sshClientIp ? ' ' + r.sshClientIp : ''}`);
184
- }
185
- return chalk.gray('local');
186
- }
187
- function auditDetailFor(r) {
188
- if (r.command)
189
- return r.command;
190
- const bits = [];
191
- if (typeof r.team === 'string')
192
- bits.push(`team=${r.team}`);
193
- if (typeof r.bundle === 'string')
194
- bits.push(`bundle=${r.bundle}`);
195
- if (typeof r.skill === 'string')
196
- bits.push(`skill=${r.skill}`);
197
- if (typeof r.version === 'string')
198
- bits.push(`v=${r.version}`);
199
- if (typeof r.profile === 'string')
200
- bits.push(`profile=${r.profile}`);
201
- if (typeof r.server === 'string')
202
- bits.push(`server=${r.server}`);
203
- if (typeof r.error === 'string')
204
- bits.push(chalk.red(r.error));
205
- return bits.join(' ');
206
- }
207
181
  function levelColor(level) {
208
182
  if (level === 'audit')
209
183
  return chalk.magenta(level);
@@ -213,99 +187,6 @@ function levelColor(level) {
213
187
  return chalk.gray(level);
214
188
  return chalk.blue(level);
215
189
  }
216
- function renderAuditRow(r) {
217
- const time = chalk.gray(r.ts.slice(0, 19).replace('T', ' '));
218
- const user = `${r.osUser ?? '?'}@${r.hostname}`;
219
- const ev = r.event.startsWith('error') ? chalk.red(r.event) : chalk.cyan(r.event);
220
- const lvl = levelColor(r.level ?? levelFor(r.event));
221
- const agent = r.agent ? chalk.gray(` ${r.agent}`) : '';
222
- const caller = chalk.gray(`via ${r.caller ?? 'unknown'}${r.session ? ` ${r.session}` : ''}`);
223
- return `${time} ${lvl.padEnd(14)} ${originLabel(r).padEnd(24)} ${user.padEnd(22)} ${caller.padEnd(28)} ${ev.padEnd(26)}${agent} ${auditDetailFor(r)}`;
224
- }
225
- function collect(value, previous) {
226
- return previous.concat([value]);
227
- }
228
- async function runAudit(opts) {
229
- if (opts.follow) {
230
- await followAuditLog();
231
- return;
232
- }
233
- const limit = Math.max(1, parseInt(opts.limit ?? '50', 10) || 50);
234
- let startDate;
235
- try {
236
- startDate = opts.since ? parseSince(opts.since) : undefined;
237
- }
238
- catch (err) {
239
- console.error(chalk.red(err.message));
240
- process.exit(2);
241
- }
242
- const records = query({
243
- startDate,
244
- eventTypes: opts.event?.length ? opts.event : undefined,
245
- level: opts.level,
246
- agent: opts.agent,
247
- caller: opts.caller,
248
- command: opts.command,
249
- module: opts.module,
250
- limit,
251
- });
252
- if (opts.json) {
253
- console.log(JSON.stringify(records, null, 2));
254
- return;
255
- }
256
- if (records.length === 0) {
257
- console.log(chalk.gray('No matching events.'));
258
- return;
259
- }
260
- for (const r of records.slice().reverse())
261
- console.log(renderAuditRow(r));
262
- console.log(chalk.gray(`\n${records.length} event(s). Log: ${getLogsPath()}`));
263
- }
264
- async function followAuditLog() {
265
- const file = getLogsPath();
266
- let offset = 0;
267
- try {
268
- offset = fs.statSync(file).size;
269
- }
270
- catch {
271
- // File may not exist yet — start at 0.
272
- }
273
- console.log(chalk.gray(`Tailing ${file} — Ctrl-C to stop`));
274
- const drain = () => {
275
- let size = 0;
276
- try {
277
- size = fs.statSync(file).size;
278
- }
279
- catch {
280
- return;
281
- }
282
- if (size <= offset) {
283
- if (size < offset)
284
- offset = 0;
285
- return;
286
- }
287
- const fd = fs.openSync(file, 'r');
288
- try {
289
- const buf = Buffer.alloc(size - offset);
290
- fs.readSync(fd, buf, 0, buf.length, offset);
291
- offset = size;
292
- for (const line of buf.toString('utf-8').split('\n').filter(Boolean)) {
293
- try {
294
- console.log(renderAuditRow(JSON.parse(line)));
295
- }
296
- catch {
297
- // Skip malformed lines.
298
- }
299
- }
300
- }
301
- finally {
302
- fs.closeSync(fd);
303
- }
304
- };
305
- await new Promise(() => {
306
- setInterval(drain, 500);
307
- });
308
- }
309
190
  function humanBytes(bytes) {
310
191
  if (bytes < 1024)
311
192
  return `${bytes} B`;
@@ -379,19 +260,9 @@ export function registerLogsCommand(program) {
379
260
  .option('-m, --full', 'Show the full raw transcript / stdout instead of the concise summary')
380
261
  .option('--json', 'Machine-readable JSON: a host task as { kind, task, log }, a session as the redacted { session, events } (same shape as `sessions <id> --json`)')
381
262
  .action((id, opts) => runLogs(id, opts));
382
- logsCmd
263
+ addEventsReadOptions(logsCmd
383
264
  .command('audit')
384
- .description('Read the structured audit/event log (who ran what, from where)')
385
- .option('--module <name>', 'Only events from this command group (e.g. teams, secrets)')
386
- .option('--command <path>', 'Only this command path — prefix match (e.g. "teams create")')
387
- .option('--event <type>', 'Only this typed event (repeatable)', collect, [])
388
- .option('--agent <name>', 'Only events tagged with this agent')
389
- .option('--caller <kind>', 'Only this caller kind (claude-code, codex, gemini, cursor, terminal, script)')
390
- .option('--level <level>', 'Only this level: audit, warn, info, debug')
391
- .option('--since <time>', 'Only events newer than this (e.g. 2h, 7d, or ISO date)')
392
- .option('--limit <n>', 'Max records to show (default 50)', '50')
393
- .option('--json', 'Output raw records as JSON')
394
- .option('-f, --follow', "Tail today's log live")
265
+ .description('Alias for `agents events --audit`'), false)
395
266
  .addHelpText('after', `
396
267
  Examples:
397
268
  agents logs audit Recent activity across everything
@@ -401,7 +272,7 @@ Examples:
401
272
  agents logs audit --command "teams create" Just team creations
402
273
  agents logs audit --event secrets.get --since 7d --json
403
274
  agents logs audit -f Live tail`)
404
- .action(async (options) => runAudit(options));
275
+ .action((_options, command) => runEventsCommand(command.optsWithGlobals(), true));
405
276
  logsCmd
406
277
  .command('stats')
407
278
  .description('Show aggregate audit statistics')
@@ -410,16 +281,21 @@ Examples:
410
281
  .action(async (opts) => runStats(opts));
411
282
  logsCmd
412
283
  .command('rotate')
413
- .description('Force log rotation — remove files older than the retention period')
284
+ .description('Apply event retention and the storage ceiling immediately')
414
285
  .option('--days <n>', 'Retention period in days (default 7)', '7')
286
+ .option('--max-mb <n>', 'Total event storage ceiling in MiB (default 50)', '50')
415
287
  .action((opts) => {
416
288
  const days = Math.max(1, parseInt(opts.days ?? '7', 10) || 7);
417
- const removed = rotate(days);
289
+ const maxMb = Math.max(1, parseInt(opts.maxMb ?? '50', 10) || 50);
290
+ const result = rotate(days, maxMb * 1024 * 1024);
291
+ const removed = result.removedByAge + result.removedBySize;
418
292
  if (removed > 0) {
419
- console.log(`Removed ${removed} log file${removed === 1 ? '' : 's'} older than ${days} day${days === 1 ? '' : 's'}.`);
293
+ console.log(`Removed ${removed} event file${removed === 1 ? '' : 's'} ` +
294
+ `(${result.removedByAge} by age, ${result.removedBySize} by size); ` +
295
+ `reclaimed ${humanBytes(result.bytesReclaimed)}.`);
420
296
  }
421
297
  else {
422
- console.log(chalk.gray('No log files to remove.'));
298
+ console.log(chalk.gray(`No event files removed (retention ${days} days, ceiling ${maxMb} MiB).`));
423
299
  }
424
300
  });
425
301
  }
@@ -17,7 +17,7 @@ import type { Command } from 'commander';
17
17
  * unit a user reasons about: one skill, one command, one plugin — even if it
18
18
  * spans several files on disk.
19
19
  */
20
- type RepoResourceKind = 'skill' | 'command' | 'plugin' | 'hook' | 'mcp' | 'subagent' | 'rule' | 'workflow' | 'routine' | 'profile' | 'permission' | 'cli' | 'config' | 'other';
20
+ type RepoResourceKind = 'skill' | 'command' | 'plugin' | 'hook' | 'mcp' | 'subagent' | 'rule' | 'workflow' | 'routine' | 'profile' | 'permission' | 'clis' | 'config' | 'other';
21
21
  export type ChangeAction = 'new' | 'changed' | 'removed';
22
22
  /**
23
23
  * Map a repo-relative path to the resource unit it belongs to. Directory-based
@@ -78,7 +78,7 @@ const RESOURCE_DIRS = {
78
78
  skills: 'skill', commands: 'command', prompts: 'command', plugins: 'plugin',
79
79
  hooks: 'hook', mcp: 'mcp', subagents: 'subagent', rules: 'rule',
80
80
  workflows: 'workflow', routines: 'routine', profiles: 'profile',
81
- permissions: 'permission', cli: 'cli',
81
+ permissions: 'permission', clis: 'clis',
82
82
  };
83
83
  /** [singular, plural] display labels per kind. */
84
84
  const RESOURCE_LABELS = {
@@ -87,13 +87,13 @@ const RESOURCE_LABELS = {
87
87
  subagent: ['subagent', 'subagents'], rule: ['rule', 'rules'],
88
88
  workflow: ['workflow', 'workflows'], routine: ['routine', 'routines'],
89
89
  profile: ['profile', 'profiles'], permission: ['permission', 'permissions'],
90
- cli: ['CLI', 'CLIs'], config: ['config file', 'config files'],
90
+ clis: ['CLI', 'CLIs'], config: ['config file', 'config files'],
91
91
  other: ['other file', 'other files'],
92
92
  };
93
93
  /** Display order — the resources a user cares about most come first. */
94
94
  const RESOURCE_ORDER = [
95
95
  'skill', 'command', 'plugin', 'hook', 'mcp', 'subagent', 'rule',
96
- 'workflow', 'routine', 'profile', 'permission', 'cli', 'config', 'other',
96
+ 'workflow', 'routine', 'profile', 'permission', 'clis', 'config', 'other',
97
97
  ];
98
98
  /**
99
99
  * Map a repo-relative path to the resource unit it belongs to. Directory-based
@@ -46,6 +46,16 @@ interface SessionsOptions extends SessionFilterOptions {
46
46
  flat?: boolean;
47
47
  /** With --active: show only sessions waiting on user input; exit 1 if any. */
48
48
  waiting?: boolean;
49
+ /** Live-state shorthand filters. Any one implies --active; several compose as OR. */
50
+ working?: boolean;
51
+ idle?: boolean;
52
+ orphan?: boolean;
53
+ orphaned?: boolean;
54
+ crashed?: boolean;
55
+ closed?: boolean;
56
+ abandoned?: boolean;
57
+ queued?: boolean;
58
+ unknown?: boolean;
49
59
  /** Show only favorited (starred) sessions — the `f` key's flag twin. */
50
60
  favorites?: boolean;
51
61
  /** Enrich the listing with live glyphs/preview for running rows. Default on;
@@ -311,6 +321,11 @@ export declare function gatherActiveSessions(opts?: {
311
321
  sessions: ActiveSession[];
312
322
  remoteDeviceCount: number;
313
323
  }>;
324
+ export type LiveStatusFilter = 'working' | 'idle' | 'waiting' | 'orphaned' | 'crashed' | 'closed' | 'abandoned' | 'queued' | 'unknown';
325
+ /** Match the status words users see, preserving activity's richer working signal. */
326
+ export declare function matchesLiveStatus(session: ActiveSession, status: LiveStatusFilter): boolean;
327
+ /** Resolve convenience flags once. Multiple flags intentionally form a union. */
328
+ export declare function requestedLiveStatuses(options: SessionsOptions): LiveStatusFilter[];
314
329
  /**
315
330
  * A bare interactive fleet listing — no query, no render/filter flag — that the
316
331
  * `runSessionBrowser` picker can represent. The single predicate shared by the