@phnx-labs/agents-cli 1.20.33 → 1.20.35
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 +29 -0
- package/README.md +32 -2
- package/dist/commands/computer-actions.d.ts +15 -0
- package/dist/commands/computer-actions.js +120 -22
- package/dist/commands/computer.d.ts +23 -0
- package/dist/commands/computer.js +45 -3
- package/dist/commands/cost.js +17 -12
- package/dist/commands/doctor.d.ts +11 -0
- package/dist/commands/doctor.js +108 -14
- package/dist/commands/events.d.ts +16 -0
- package/dist/commands/events.js +173 -0
- package/dist/commands/exec.js +6 -1
- package/dist/commands/hosts.js +28 -24
- package/dist/commands/import.js +1 -1
- package/dist/commands/inspect.d.ts +2 -0
- package/dist/commands/inspect.js +54 -7
- package/dist/commands/logs.d.ts +17 -0
- package/dist/commands/logs.js +139 -0
- package/dist/commands/models.d.ts +3 -0
- package/dist/commands/models.js +27 -8
- package/dist/commands/permissions.js +9 -2
- package/dist/commands/repo.d.ts +34 -0
- package/dist/commands/repo.js +243 -65
- package/dist/commands/resource-view.d.ts +20 -0
- package/dist/commands/resource-view.js +90 -28
- package/dist/commands/rules.js +1 -1
- package/dist/commands/secrets-migrate.js +23 -11
- package/dist/commands/secrets.d.ts +20 -0
- package/dist/commands/secrets.js +93 -17
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +209 -0
- package/dist/commands/sessions-tail.d.ts +10 -0
- package/dist/commands/sessions-tail.js +11 -0
- package/dist/commands/sessions.d.ts +73 -1
- package/dist/commands/sessions.js +273 -53
- package/dist/commands/status.d.ts +12 -0
- package/dist/commands/status.js +81 -0
- package/dist/commands/teams.js +70 -6
- package/dist/commands/versions.js +2 -1
- package/dist/commands/view.d.ts +42 -0
- package/dist/commands/view.js +213 -83
- package/dist/commands/wallet.d.ts +6 -0
- package/dist/commands/wallet.js +22 -5
- package/dist/index.js +61 -32
- package/dist/lib/acp/harnesses.d.ts +1 -1
- package/dist/lib/acp/harnesses.js +2 -2
- package/dist/lib/agents.d.ts +12 -0
- package/dist/lib/agents.js +145 -41
- package/dist/lib/browser/cdp.js +5 -1
- package/dist/lib/browser/chrome.js +20 -0
- package/dist/lib/browser/drivers/ssh.d.ts +27 -0
- package/dist/lib/browser/drivers/ssh.js +94 -19
- package/dist/lib/browser/ipc.d.ts +3 -0
- package/dist/lib/browser/ipc.js +13 -9
- package/dist/lib/browser/service.js +71 -15
- package/dist/lib/daemon.d.ts +35 -0
- package/dist/lib/daemon.js +33 -5
- package/dist/lib/devices/connect.d.ts +3 -2
- package/dist/lib/devices/connect.js +5 -3
- package/dist/lib/doctor-diff.js +29 -2
- package/dist/lib/drift-sync.d.ts +43 -0
- package/dist/lib/drift-sync.js +179 -0
- package/dist/lib/events.d.ts +9 -2
- package/dist/lib/events.js +70 -11
- package/dist/lib/exec.d.ts +15 -0
- package/dist/lib/exec.js +43 -11
- package/dist/lib/hooks.js +8 -2
- package/dist/lib/hosts/dispatch.js +6 -7
- package/dist/lib/hosts/logs.d.ts +16 -0
- package/dist/lib/hosts/logs.js +45 -0
- package/dist/lib/hosts/progress.d.ts +66 -0
- package/dist/lib/hosts/progress.js +125 -17
- package/dist/lib/hosts/ready.d.ts +23 -2
- package/dist/lib/hosts/ready.js +35 -13
- package/dist/lib/hosts/reconcile.d.ts +53 -0
- package/dist/lib/hosts/reconcile.js +81 -0
- package/dist/lib/hosts/tasks.d.ts +8 -0
- package/dist/lib/hosts/tasks.js +14 -0
- package/dist/lib/permissions.d.ts +4 -0
- package/dist/lib/permissions.js +35 -0
- package/dist/lib/picker.d.ts +24 -0
- package/dist/lib/picker.js +124 -0
- package/dist/lib/platform/winpath.d.ts +31 -2
- package/dist/lib/platform/winpath.js +133 -24
- package/dist/lib/plugin-marketplace.d.ts +30 -0
- package/dist/lib/plugin-marketplace.js +215 -2
- package/dist/lib/plugins.d.ts +5 -0
- package/dist/lib/plugins.js +45 -4
- package/dist/lib/pwsh.d.ts +11 -0
- package/dist/lib/pwsh.js +13 -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/agent.d.ts +42 -1
- package/dist/lib/secrets/agent.js +89 -11
- package/dist/lib/secrets/bundles.js +40 -9
- package/dist/lib/secrets/filestore.js +31 -1
- package/dist/lib/secrets/index.d.ts +33 -1
- package/dist/lib/secrets/index.js +90 -9
- package/dist/lib/secrets/windows.d.ts +74 -0
- package/dist/lib/secrets/windows.js +440 -0
- package/dist/lib/session/active.d.ts +7 -0
- package/dist/lib/session/active.js +11 -3
- package/dist/lib/session/discover.js +94 -0
- package/dist/lib/session/parse.d.ts +6 -0
- package/dist/lib/session/parse.js +283 -2
- package/dist/lib/session/pid-registry.d.ts +21 -0
- package/dist/lib/session/pid-registry.js +80 -0
- package/dist/lib/session/remote-active.d.ts +26 -0
- package/dist/lib/session/remote-active.js +141 -0
- package/dist/lib/session/remote.js +2 -6
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/sync/config.d.ts +8 -0
- package/dist/lib/session/sync/config.js +11 -2
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +30 -2
- package/dist/lib/shims.js +107 -50
- package/dist/lib/ssh-exec.d.ts +26 -3
- package/dist/lib/ssh-exec.js +45 -3
- package/dist/lib/ssh-tunnel.d.ts +24 -5
- package/dist/lib/ssh-tunnel.js +60 -62
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +3 -0
- package/dist/lib/startup/command-registry.js +6 -0
- package/dist/lib/sync-status.d.ts +102 -0
- package/dist/lib/sync-status.js +135 -0
- package/dist/lib/teams/agents.d.ts +24 -0
- package/dist/lib/teams/agents.js +30 -1
- package/dist/lib/teams/registry.js +25 -9
- package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
- package/dist/lib/terminal/backends/ghostty.js +69 -0
- package/dist/lib/terminal/backends/index.d.ts +18 -0
- package/dist/lib/terminal/backends/index.js +29 -0
- package/dist/lib/terminal/backends/iterm.d.ts +6 -0
- package/dist/lib/terminal/backends/iterm.js +62 -0
- package/dist/lib/terminal/backends/tmux.d.ts +14 -0
- package/dist/lib/terminal/backends/tmux.js +23 -0
- package/dist/lib/terminal/engine.d.ts +39 -0
- package/dist/lib/terminal/engine.js +54 -0
- package/dist/lib/terminal/index.d.ts +14 -0
- package/dist/lib/terminal/index.js +5 -0
- package/dist/lib/terminal/policy.d.ts +11 -0
- package/dist/lib/terminal/policy.js +11 -0
- package/dist/lib/terminal/quote.d.ts +11 -0
- package/dist/lib/terminal/quote.js +13 -0
- package/dist/lib/terminal/shell.d.ts +6 -0
- package/dist/lib/terminal/shell.js +23 -0
- package/dist/lib/terminal/transport.d.ts +15 -0
- package/dist/lib/terminal/transport.js +40 -0
- package/dist/lib/terminal/types.d.ts +59 -0
- package/dist/lib/terminal/types.js +13 -0
- package/dist/lib/types.d.ts +27 -1
- package/dist/lib/usage.d.ts +30 -0
- package/dist/lib/usage.js +159 -2
- package/dist/lib/versions.d.ts +7 -3
- package/dist/lib/versions.js +10 -5
- package/dist/lib/whats-new.d.ts +9 -0
- package/dist/lib/whats-new.js +35 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 1.20.35
|
|
6
|
+
|
|
7
|
+
**CI: build node-pty's native binary on macOS/Windows so the release matrix is green cross-platform**
|
|
8
|
+
|
|
9
|
+
- The cross-platform matrix (`ci.yml`, runs only on `release/**` + `v*`) installed deps with `bun install --ignore-scripts`, so `pty.node` from `@homebridge/node-pty-prebuilt-multiarch` was never fetched/built. That package ships prebuilt binaries only for Linux; macOS/Windows obtain `pty.node` via its own install script (prebuild-install download, else a node-gyp compile). With that script skipped the native module was absent, so the daemon-liveness integration test added in #568 — which spawns the real daemon (it loads node-pty) and asserts the browser IPC socket stays up — crashed on macOS/Windows while passing on Linux, and had been red on every release since. The matrix runs only on release branches, so it never surfaced on normal PRs (bun does not run that install script even without `--ignore-scripts` in bun 1.3.x). CI now runs a dedicated step that invokes the package's own install script (`npm run install`), which prefers a prebuilt download and falls back to a node-gyp compile, so it self-heals across platforms and node ABIs. Production (`npm install`) already built the native module, so end users were unaffected. A second macOS/Windows-only failure in the same #568 daemon-liveness test was also fixed: the test rooted its fake `HOME` under `os.tmpdir()`, which on macOS is the long `/var/folders/…/T/…`, pushing the daemon's AF_UNIX socket path to ~116 bytes — past macOS's 104-byte `sun_path` limit — so `bind()` failed with `EADDRINUSE`. The test now roots `HOME` at a short base on POSIX (Windows uses length-unlimited named pipes); real users with a normal `HOME` were never affected. Source: `.github/workflows/ci.yml`, `src/lib/daemon.test.ts`.
|
|
10
|
+
|
|
11
|
+
**`agents logs`: a top-level, unified run-log viewer (#575)**
|
|
12
|
+
|
|
13
|
+
- Viewing a dispatched run's output used to be nested and undiscoverable — only `agents hosts logs <id>` and `agents daemon logs` existed, and `agents hosts` wasn't even in `--help`. `agents logs [id]` is now a discoverable top-level command that resolves a run across **two substrates** — host-dispatch task stdout (`agents run --host`) and the local session index — and shows or (`-f`) follows it. `[id]`/`--session` load directly (host task tried first, then session); with no id, `--host`/`--agent`/`--version` filter a merged candidate list (one match shows, several open a fuzzy picker, non-TTY prints the list). Additive: `agents hosts logs` and `agents sessions tail` are unchanged and share the same helpers. Source: `src/commands/logs.ts`, `src/lib/hosts/logs.ts`.
|
|
14
|
+
|
|
15
|
+
**Host-follow log tailer: no self-corruption on localhost, byte-accurate offsets (#586, #589)**
|
|
16
|
+
|
|
17
|
+
- Following a run dispatched to **localhost** tripled the on-disk log and triple-printed the output, because the local mirror file and the remote log were the same file and the tailer appended its own reads back into it; it now detects that aliasing by file identity (`dev:ino`) and echoes only. Separately, the offset tracker advanced by a re-encoded string length, so a multibyte UTF-8 char split at a poll boundary drifted the offset and corrupted the stream on non-ASCII output; the tail is now byte-exact (raw `Buffer` via `sshExecRaw`). Source: `src/lib/hosts/progress.ts`, `src/lib/ssh-exec.ts`.
|
|
18
|
+
|
|
19
|
+
**`agents upgrade`: the "What's new" changelog is now a compact heading list (#562)**
|
|
20
|
+
|
|
21
|
+
- The post-upgrade changelog dumped every heading *and* every verbose sub-bullet for each version in the range — a screenful across a multi-version jump. It now prints one bullet per feature/fix heading and links to the full CHANGELOG for the details. The parser was extracted to a pure, unit-tested `renderWhatsNew` so it can be exercised without the CLI's import-time side effects. Source: `src/lib/whats-new.ts`, `src/index.ts`.
|
|
22
|
+
|
|
23
|
+
**`agents sessions --active`: a per-pid registry de-collapses co-located agents (#546)**
|
|
24
|
+
|
|
25
|
+
- On a host with no terminal extension (bare SSH/tmux — e.g. any Linux box), `--active` could only map a discovered agent process to a session by guessing the newest `.jsonl` in its cwd, so several agents in the same repo collapsed onto one session row (observed live: a single id listed 28 times), and `/restore` couldn't tell them apart. `agents run` now records each launch to `~/.agents/.cache/terminals/by-pid/<pid>.json` (`{agent, cwd, tmuxPane, sessionId, startedAtMs}`) — the headless equivalent of the terminal extension's `live-terminals.json` — so `--active` and `/restore` attribute each co-located agent correctly. Source: `src/lib/session/pid-registry.ts`, `src/lib/session/active.ts`, `src/lib/exec.ts`.
|
|
26
|
+
|
|
27
|
+
## 1.20.34
|
|
28
|
+
|
|
29
|
+
**Test suite runs remotely on a crabbox VM (#525, #540)**
|
|
30
|
+
|
|
31
|
+
- `scripts/release.sh`'s test gate now runs `bun install && bun run build && bun run test` on a leased crabbox VM via `scripts/sandbox.sh` instead of freezing the local machine, matching CI's Build→Test order (crabbox's sync honors `.gitignore`, so the gitignored `dist/` is built on the box). A new `bun run test:remote` offloads the suite the same way for local dev. Publishing still happens locally — only the signed macOS keychain helper can be produced and notarized here, and crabbox boxes are Linux. Source: `scripts/sandbox.sh`, `scripts/release.sh`, `package.json`.
|
|
32
|
+
- `scripts/sandbox.sh` box acquisition is now robust: secrets load via `agents secrets export --plaintext` (the bare form now hard-errors), a missing `.crabbox.yaml` no longer aborts the script under `set -e`, and the agents-cli/claude install is gated to PR mode so test-mode runs match GitHub CI. Box selection gates on `crabbox status … ready=true` — skipping failed-bootstrap duds (which still report `status=running`) and warming a fresh box if none are ready — keyed on the stable `profile` label rather than an ephemeral slug. A dedicated `agents-cli` crabbox profile (`.crabbox.yaml`) isolates this repo's warm pool. Source: `scripts/sandbox.sh`, `.crabbox.yaml`.
|
|
33
|
+
|
|
5
34
|
## 1.20.31
|
|
6
35
|
|
|
7
36
|
**`agents sessions <id>`: a catch-up digest for switching between many agents (#502)**
|
package/README.md
CHANGED
|
@@ -230,6 +230,34 @@ Profile YAML has no secrets -- safe to `agents repo push` to a shared repo. `age
|
|
|
230
230
|
|
|
231
231
|
---
|
|
232
232
|
|
|
233
|
+
## Run on your own machines
|
|
234
|
+
|
|
235
|
+
Dispatch any read-only or config command -- and `agents run` itself -- to another machine over SSH. No daemon.
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
# Enroll a machine (from ~/.ssh/config, or inline with user@address)
|
|
239
|
+
agents hosts add gpu-box
|
|
240
|
+
agents hosts check gpu-box # reachable? which agents-cli version?
|
|
241
|
+
|
|
242
|
+
# Run there instead of locally
|
|
243
|
+
agents run claude --host gpu-box "profile this build" # follows live by default
|
|
244
|
+
agents logs --host gpu-box # pick a dispatched run and view its log
|
|
245
|
+
agents logs <id> -f # re-attach to a running one and follow
|
|
246
|
+
agents view claude --host gpu-box # inspect the remote install
|
|
247
|
+
agents sync --host gpu-box # make the remote machine current
|
|
248
|
+
|
|
249
|
+
# Your Tailscale fleet, auto-discovered
|
|
250
|
+
agents devices sync # ingest `tailscale status`
|
|
251
|
+
agents ssh mac-mini # hardened SSH: fails fast if offline,
|
|
252
|
+
# PowerShell on Windows, password-from-Keychain
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Hosts** (`agents hosts`) are git-synced dispatch targets in `agents.yaml`; **devices** (`agents devices`) are your Tailscale machines in a local registry. Both ride SSH. See [docs/00-concepts.md](docs/00-concepts.md#devices--hosts).
|
|
256
|
+
|
|
257
|
+
Every `--host` command rides one multiplexed SSH engine, tuned for driving a fleet from a small laptop: the first call to a machine opens a control socket and every later call reuses it (no repeat TCP+auth handshake), connections carry keepalive so a dropped link dies in ~45 s instead of zombying, and following a remote run polls in a single round-trip per cycle. Measured against a Tailscale-relayed host: repeated calls **~6–7× faster**, dispatch readiness **~2×**, and the follow loop **~21× faster with 50% fewer local ssh spawns**. Design: [docs/09-ssh-transport.md](docs/09-ssh-transport.md) · reproduce: `node scripts/bench-ssh.mjs <host>`.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
233
261
|
## Teams
|
|
234
262
|
|
|
235
263
|
```bash
|
|
@@ -646,11 +674,13 @@ By default, secrets sync via iCloud Keychain to your other Macs. With `--no-iclo
|
|
|
646
674
|
|
|
647
675
|
Which DotAgents resources each agent CLI can load. Source of truth: [src/lib/agents.ts](src/lib/agents.ts) (`capabilities`); gates use `supports(agent, cap, version)` from [src/lib/capabilities.ts](src/lib/capabilities.ts). Full matrix also in [docs/00-concepts.md](docs/00-concepts.md).
|
|
648
676
|
|
|
677
|
+
> **† Gemini CLI is deprecated.** Google retired it for free, Pro, and Ultra tiers on **June 18, 2026** (announced at Google I/O 2026); the `gemini` command no longer serves requests on those tiers. agents-cli still manages existing installs, but warns on `agents add gemini` and `agents teams add … gemini`. New setups should use **Antigravity CLI** (`antigravity`), Google's official successor — see [the transition notice](https://developers.googleblog.com/an-important-update-transitioning-gemini-cli-to-antigravity-cli/).
|
|
678
|
+
|
|
649
679
|
| Agent | Versions | Hooks | MCP | Permissions | Skills | Commands | Plugins | Subagents | Rules | Workflows |
|
|
650
680
|
|-------|----------|-------|-----|-------------|--------|----------|---------|-----------|-------|-----------|
|
|
651
681
|
| Claude Code | yes | yes | yes | yes | yes | yes | yes | yes | `CLAUDE.md` | yes |
|
|
652
682
|
| Codex CLI | yes | >= 0.116.0 | yes | no | yes | < 0.117.0 · skills ($name, >= 0.117) | >= 0.128.0 | no | `AGENTS.md` | no |
|
|
653
|
-
| Gemini CLI | yes | >= 0.26.0 | yes | no | yes | yes (.toml) | no | no | `GEMINI.md` | no |
|
|
683
|
+
| Gemini CLI † | yes | >= 0.26.0 | yes | no | yes | yes (.toml) | no | no | `GEMINI.md` | no |
|
|
654
684
|
| Antigravity | yes | yes | yes | yes | yes | yes | yes | no | `AGENTS.md` | no |
|
|
655
685
|
| Grok Build | yes | yes | yes | yes | yes | skills ($name) | yes | no | `AGENTS.md` | no |
|
|
656
686
|
| OpenClaw | yes | yes | yes | no | yes | gateway | yes | yes | `workspace/AGENTS.md` | no |
|
|
@@ -672,7 +702,7 @@ Which DotAgents resources each agent CLI can load. Source of truth: [src/lib/age
|
|
|
672
702
|
|-------|----------|-------|---------------|
|
|
673
703
|
| Claude Code | yes | yes | yes |
|
|
674
704
|
| Codex CLI | yes | yes | yes |
|
|
675
|
-
| Gemini CLI | yes | yes | yes |
|
|
705
|
+
| Gemini CLI † | yes | yes | yes |
|
|
676
706
|
| Cursor | -- | yes | -- |
|
|
677
707
|
| OpenCode | -- | yes | -- |
|
|
678
708
|
| Grok Build | -- | yes | yes |
|
|
@@ -55,4 +55,19 @@ export declare function buildWaitParams(opts: {
|
|
|
55
55
|
};
|
|
56
56
|
export declare function withClient<T>(fn: (client: ComputerClient) => Promise<T>): Promise<T>;
|
|
57
57
|
export declare function unwrap(r: RPCResponse): Record<string, unknown>;
|
|
58
|
+
export declare function resolveTargetPidDecision(client: ComputerClient, opts: {
|
|
59
|
+
pid?: number;
|
|
60
|
+
bundle?: string;
|
|
61
|
+
}, gate?: {
|
|
62
|
+
verb?: string;
|
|
63
|
+
env?: NodeJS.ProcessEnv;
|
|
64
|
+
nowMs?: number;
|
|
65
|
+
}): Promise<{
|
|
66
|
+
ok: true;
|
|
67
|
+
pid: number;
|
|
68
|
+
source: 'pid' | 'list_apps' | 'session_admission';
|
|
69
|
+
} | {
|
|
70
|
+
ok: false;
|
|
71
|
+
error: string;
|
|
72
|
+
}>;
|
|
58
73
|
export declare function registerActionCommands(program: Command): void;
|
|
@@ -6,7 +6,88 @@
|
|
|
6
6
|
// The daemon already implements every method; this file is the thin, typed
|
|
7
7
|
// CLI skin over it plus a shared target resolver so callers stay in bundle-id
|
|
8
8
|
// space and never hand-manage pids.
|
|
9
|
-
import
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import { openComputerClient, describeTransport, resolvePolicyPath, } from '../lib/computer-rpc.js';
|
|
12
|
+
import { COMPUTER_INPUT_GATED_VERBS, formatComputerPermissionGrantHint, } from '../lib/permissions.js';
|
|
13
|
+
function isComputerInputVerb(verb) {
|
|
14
|
+
return typeof verb === 'string' && COMPUTER_INPUT_GATED_VERBS.includes(verb);
|
|
15
|
+
}
|
|
16
|
+
function computerSessionId(env = process.env) {
|
|
17
|
+
return env.CODEX_THREAD_ID
|
|
18
|
+
|| env.CLAUDE_CODE_SESSION_ID
|
|
19
|
+
|| env.CLAUDE_SESSION_ID
|
|
20
|
+
|| env.AGENTS_SESSION_ID
|
|
21
|
+
|| env.AGENTS_RUN_ID
|
|
22
|
+
|| null;
|
|
23
|
+
}
|
|
24
|
+
function admissionCachePath(env = process.env) {
|
|
25
|
+
if (env.AGENTS_COMPUTER_ADMISSION_CACHE)
|
|
26
|
+
return env.AGENTS_COMPUTER_ADMISSION_CACHE;
|
|
27
|
+
return path.join(path.dirname(resolvePolicyPath()), 'computer-target-admissions.json');
|
|
28
|
+
}
|
|
29
|
+
function targetSelector(opts) {
|
|
30
|
+
return opts.bundle ? `bundle:${opts.bundle}` : 'frontmost';
|
|
31
|
+
}
|
|
32
|
+
function readAdmissionCache(filePath) {
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
35
|
+
return Array.isArray(parsed.admissions) ? parsed.admissions.filter(isAdmission) : [];
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function isAdmission(v) {
|
|
42
|
+
if (!v || typeof v !== 'object')
|
|
43
|
+
return false;
|
|
44
|
+
const r = v;
|
|
45
|
+
return r.sessionId !== undefined
|
|
46
|
+
&& typeof r.sessionId === 'string'
|
|
47
|
+
&& typeof r.selector === 'string'
|
|
48
|
+
&& r.gateClass === 'input'
|
|
49
|
+
&& typeof r.pid === 'number'
|
|
50
|
+
&& typeof r.bundle_id === 'string'
|
|
51
|
+
&& typeof r.name === 'string'
|
|
52
|
+
&& typeof r.admittedAtMs === 'number'
|
|
53
|
+
&& isComputerInputVerb(r.admittedByVerb);
|
|
54
|
+
}
|
|
55
|
+
function rememberAdmission(app, opts) {
|
|
56
|
+
const sessionId = computerSessionId(opts.env);
|
|
57
|
+
if (!sessionId)
|
|
58
|
+
return;
|
|
59
|
+
const filePath = admissionCachePath(opts.env);
|
|
60
|
+
const admissions = readAdmissionCache(filePath);
|
|
61
|
+
const next = {
|
|
62
|
+
sessionId,
|
|
63
|
+
selector: opts.selector,
|
|
64
|
+
gateClass: 'input',
|
|
65
|
+
pid: app.pid,
|
|
66
|
+
bundle_id: app.bundle_id,
|
|
67
|
+
name: app.name,
|
|
68
|
+
admittedAtMs: opts.nowMs ?? Date.now(),
|
|
69
|
+
admittedByVerb: opts.verb,
|
|
70
|
+
};
|
|
71
|
+
const filtered = admissions.filter((a) => !(a.sessionId === sessionId && a.selector === opts.selector && a.gateClass === 'input'));
|
|
72
|
+
filtered.push(next);
|
|
73
|
+
try {
|
|
74
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
75
|
+
fs.writeFileSync(filePath, JSON.stringify({ admissions: filtered }, null, 2), { mode: 0o600 });
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Cache persistence is best-effort; the daemon remains the final gate.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function findAdmission(opts) {
|
|
82
|
+
const sessionId = computerSessionId(opts.env);
|
|
83
|
+
if (!sessionId)
|
|
84
|
+
return null;
|
|
85
|
+
const admissions = readAdmissionCache(admissionCachePath(opts.env));
|
|
86
|
+
const matches = admissions
|
|
87
|
+
.filter((a) => a.sessionId === sessionId && a.selector === opts.selector && a.gateClass === 'input')
|
|
88
|
+
.sort((a, b) => b.admittedAtMs - a.admittedAtMs);
|
|
89
|
+
return matches[0] ?? null;
|
|
90
|
+
}
|
|
10
91
|
// Pure target picker — exercised by unit tests. Precedence: explicit --pid,
|
|
11
92
|
// then --bundle, then the frontmost active allow-listed app (the same default
|
|
12
93
|
// `screenshot` uses). Kept side-effect-free so the resolution rules are
|
|
@@ -24,7 +105,7 @@ export function pickTarget(list, opts) {
|
|
|
24
105
|
if (!app) {
|
|
25
106
|
return {
|
|
26
107
|
ok: false,
|
|
27
|
-
error: `bundle not in allow list (or not running): ${opts.bundle}\
|
|
108
|
+
error: `bundle not in allow list (or not running): ${opts.bundle}\n${formatComputerPermissionGrantHint(opts.bundle)}`,
|
|
28
109
|
};
|
|
29
110
|
}
|
|
30
111
|
return { ok: true, app };
|
|
@@ -33,7 +114,7 @@ export function pickTarget(list, opts) {
|
|
|
33
114
|
if (!active) {
|
|
34
115
|
return {
|
|
35
116
|
ok: false,
|
|
36
|
-
error:
|
|
117
|
+
error: `no active app found in allow list\n${formatComputerPermissionGrantHint()}`,
|
|
37
118
|
};
|
|
38
119
|
}
|
|
39
120
|
return { ok: true, app: active };
|
|
@@ -141,20 +222,37 @@ export function unwrap(r) {
|
|
|
141
222
|
}
|
|
142
223
|
return r.result ?? {};
|
|
143
224
|
}
|
|
144
|
-
|
|
145
|
-
// and exiting when no target matches.
|
|
146
|
-
async function resolveTargetPid(client, opts) {
|
|
225
|
+
export async function resolveTargetPidDecision(client, opts, gate) {
|
|
147
226
|
// A directly-supplied pid skips the list_apps roundtrip — the daemon gates.
|
|
148
227
|
if (opts.pid != null)
|
|
149
|
-
return opts.pid;
|
|
228
|
+
return { ok: true, pid: opts.pid, source: 'pid' };
|
|
150
229
|
const apps = unwrap(await client.call('list_apps'));
|
|
151
230
|
const list = apps.apps || [];
|
|
152
231
|
const picked = pickTarget(list, opts);
|
|
232
|
+
const verb = gate?.verb;
|
|
233
|
+
const selector = targetSelector(opts);
|
|
234
|
+
if (picked.ok && isComputerInputVerb(verb)) {
|
|
235
|
+
rememberAdmission(picked.app, { selector, verb, env: gate?.env, nowMs: gate?.nowMs });
|
|
236
|
+
}
|
|
153
237
|
if (!picked.ok) {
|
|
154
|
-
|
|
238
|
+
if (isComputerInputVerb(verb)) {
|
|
239
|
+
const admitted = findAdmission({ selector, env: gate?.env });
|
|
240
|
+
if (admitted)
|
|
241
|
+
return { ok: true, pid: admitted.pid, source: 'session_admission' };
|
|
242
|
+
}
|
|
243
|
+
return { ok: false, error: picked.error };
|
|
244
|
+
}
|
|
245
|
+
return { ok: true, pid: picked.app.pid, source: 'list_apps' };
|
|
246
|
+
}
|
|
247
|
+
// Resolve the target pid via list_apps + pickTarget, printing a precise error
|
|
248
|
+
// and exiting when no target matches.
|
|
249
|
+
async function resolveTargetPid(client, opts, gate) {
|
|
250
|
+
const resolved = await resolveTargetPidDecision(client, opts, gate);
|
|
251
|
+
if (!resolved.ok) {
|
|
252
|
+
console.error(resolved.error);
|
|
155
253
|
process.exit(1);
|
|
156
254
|
}
|
|
157
|
-
return
|
|
255
|
+
return resolved.pid;
|
|
158
256
|
}
|
|
159
257
|
// --raise flag: app-level focus_window before the main action so coordinate
|
|
160
258
|
// clicks and keystrokes land on a visible, key window.
|
|
@@ -210,7 +308,7 @@ export function registerActionCommands(program) {
|
|
|
210
308
|
.option('--depth <n>', 'Max tree depth', (v) => parseInt(v, 10))
|
|
211
309
|
.option('--json', 'Emit compact JSON (default: pretty)')).action(async (opts) => {
|
|
212
310
|
await withClient(async (client) => {
|
|
213
|
-
const pid = await resolveTargetPid(client, opts);
|
|
311
|
+
const pid = await resolveTargetPid(client, opts, { verb: 'describe' });
|
|
214
312
|
const params = { pid };
|
|
215
313
|
if (opts.depth != null)
|
|
216
314
|
params.max_depth = opts.depth;
|
|
@@ -228,7 +326,7 @@ export function registerActionCommands(program) {
|
|
|
228
326
|
.option('--raise', 'Bring the target app to the front first')
|
|
229
327
|
.option('--json', 'Emit JSON'))).action(async (opts) => {
|
|
230
328
|
await withClient(async (client) => {
|
|
231
|
-
const pid = await resolveTargetPid(client, opts);
|
|
329
|
+
const pid = await resolveTargetPid(client, opts, { verb: 'click' });
|
|
232
330
|
const spec = buildElementOrCoords(opts);
|
|
233
331
|
if (!spec.ok) {
|
|
234
332
|
console.error(spec.error);
|
|
@@ -250,7 +348,7 @@ export function registerActionCommands(program) {
|
|
|
250
348
|
.description('Right-click (context menu) an element or coordinate')
|
|
251
349
|
.option('--json', 'Emit JSON'))).action(async (opts) => {
|
|
252
350
|
await withClient(async (client) => {
|
|
253
|
-
const pid = await resolveTargetPid(client, opts);
|
|
351
|
+
const pid = await resolveTargetPid(client, opts, { verb: 'right-click' });
|
|
254
352
|
const spec = buildElementOrCoords(opts);
|
|
255
353
|
if (!spec.ok) {
|
|
256
354
|
console.error(spec.error);
|
|
@@ -269,7 +367,7 @@ export function registerActionCommands(program) {
|
|
|
269
367
|
.option('--allow-secure-field', 'Permit typing into a password field')
|
|
270
368
|
.option('--json', 'Emit JSON'))).action(async (opts) => {
|
|
271
369
|
await withClient(async (client) => {
|
|
272
|
-
const pid = await resolveTargetPid(client, opts);
|
|
370
|
+
const pid = await resolveTargetPid(client, opts, { verb: 'type' });
|
|
273
371
|
const spec = buildElementOrCoords(opts);
|
|
274
372
|
if (!spec.ok) {
|
|
275
373
|
console.error(spec.error);
|
|
@@ -295,7 +393,7 @@ export function registerActionCommands(program) {
|
|
|
295
393
|
.option('--char-delay <ms>', 'Inter-character delay in ms (default 4; raise for lossy keyboard relays like VM guests, e.g. 25). Clamped to [1, 250].', (v) => parseInt(v, 10))
|
|
296
394
|
.option('--json', 'Emit JSON')).action(async (opts) => {
|
|
297
395
|
await withClient(async (client) => {
|
|
298
|
-
const pid = await resolveTargetPid(client, opts);
|
|
396
|
+
const pid = await resolveTargetPid(client, opts, { verb: 'type-text' });
|
|
299
397
|
await raiseIfRequested(client, pid, opts.raise);
|
|
300
398
|
const params = { pid, text: opts.text };
|
|
301
399
|
if (opts.commit)
|
|
@@ -319,7 +417,7 @@ export function registerActionCommands(program) {
|
|
|
319
417
|
.option('--require-frontmost', 'Fail (not warn) if the target is not the frontmost app')
|
|
320
418
|
.option('--json', 'Emit JSON')).action(async (opts) => {
|
|
321
419
|
await withClient(async (client) => {
|
|
322
|
-
const pid = await resolveTargetPid(client, opts);
|
|
420
|
+
const pid = await resolveTargetPid(client, opts, { verb: 'key' });
|
|
323
421
|
await raiseIfRequested(client, pid, opts.raise);
|
|
324
422
|
const params = { pid, keys: opts.keys };
|
|
325
423
|
if (opts.requireFrontmost)
|
|
@@ -350,7 +448,7 @@ export function registerActionCommands(program) {
|
|
|
350
448
|
process.exit(1);
|
|
351
449
|
}
|
|
352
450
|
await withClient(async (client) => {
|
|
353
|
-
const pid = await resolveTargetPid(client, opts);
|
|
451
|
+
const pid = await resolveTargetPid(client, opts, { verb: 'drag' });
|
|
354
452
|
await raiseIfRequested(client, pid, opts.raise);
|
|
355
453
|
const params = {
|
|
356
454
|
pid,
|
|
@@ -373,7 +471,7 @@ export function registerActionCommands(program) {
|
|
|
373
471
|
.option('--raise', 'Bring the target app to the front first')
|
|
374
472
|
.option('--json', 'Emit JSON'))).action(async (opts) => {
|
|
375
473
|
await withClient(async (client) => {
|
|
376
|
-
const pid = await resolveTargetPid(client, opts);
|
|
474
|
+
const pid = await resolveTargetPid(client, opts, { verb: 'scroll' });
|
|
377
475
|
await raiseIfRequested(client, pid, opts.raise);
|
|
378
476
|
const params = { pid };
|
|
379
477
|
if (opts.id)
|
|
@@ -398,7 +496,7 @@ export function registerActionCommands(program) {
|
|
|
398
496
|
.requiredOption('--action <name>', 'AX action name')
|
|
399
497
|
.option('--json', 'Emit JSON')).action(async (opts) => {
|
|
400
498
|
await withClient(async (client) => {
|
|
401
|
-
const pid = await resolveTargetPid(client, opts);
|
|
499
|
+
const pid = await resolveTargetPid(client, opts, { verb: 'ax-action' });
|
|
402
500
|
const res = unwrap(await client.call('ax_action', { pid, element_id: opts.id, action: opts.action }));
|
|
403
501
|
emit(res, Boolean(opts.json), () => `performed ${opts.action}`);
|
|
404
502
|
});
|
|
@@ -410,7 +508,7 @@ export function registerActionCommands(program) {
|
|
|
410
508
|
.requiredOption('--id <@eN>', 'Element id from `describe`')
|
|
411
509
|
.option('--json', 'Emit JSON')).action(async (opts) => {
|
|
412
510
|
await withClient(async (client) => {
|
|
413
|
-
const pid = await resolveTargetPid(client, opts);
|
|
511
|
+
const pid = await resolveTargetPid(client, opts, { verb: 'focus' });
|
|
414
512
|
const res = unwrap(await client.call('set_focus', { pid, element_id: opts.id }));
|
|
415
513
|
emit(res, Boolean(opts.json), () => `focused ${opts.id}`);
|
|
416
514
|
});
|
|
@@ -426,7 +524,7 @@ export function registerActionCommands(program) {
|
|
|
426
524
|
.option('--title <s>', 'Raise the window whose title contains this string')
|
|
427
525
|
.option('--json', 'Emit JSON')).action(async (opts) => {
|
|
428
526
|
await withClient(async (client) => {
|
|
429
|
-
const pid = await resolveTargetPid(client, opts);
|
|
527
|
+
const pid = await resolveTargetPid(client, opts, { verb: 'raise' });
|
|
430
528
|
const res = unwrap(await client.call('focus_window', { pid, ...buildRaiseParams(opts) }));
|
|
431
529
|
emit(res, Boolean(opts.json), () => {
|
|
432
530
|
const scope = res.raised_window ? `window ${res.title ?? res.window_id ?? ''}`.trim() : 'app';
|
|
@@ -455,7 +553,7 @@ export function registerActionCommands(program) {
|
|
|
455
553
|
const params = { ...spec.params };
|
|
456
554
|
// duration-only waits don't need a target pid
|
|
457
555
|
if (params.duration_ms == null)
|
|
458
|
-
params.pid = await resolveTargetPid(client, opts);
|
|
556
|
+
params.pid = await resolveTargetPid(client, opts, { verb: 'wait' });
|
|
459
557
|
const res = unwrap(await client.call('wait', params));
|
|
460
558
|
emit(res, Boolean(opts.json), () => res.satisfied ? `satisfied (${res.waited_ms}ms)` : `timed out (${res.waited_ms}ms)`);
|
|
461
559
|
});
|
|
@@ -468,7 +566,7 @@ export function registerActionCommands(program) {
|
|
|
468
566
|
.option('--max-chars <n>', 'Cap the extracted text length', (v) => parseInt(v, 10))
|
|
469
567
|
.option('--json', 'Emit JSON')).action(async (opts) => {
|
|
470
568
|
await withClient(async (client) => {
|
|
471
|
-
const pid = await resolveTargetPid(client, opts);
|
|
569
|
+
const pid = await resolveTargetPid(client, opts, { verb: 'get-text' });
|
|
472
570
|
const params = { pid };
|
|
473
571
|
if (opts.id)
|
|
474
572
|
params.element_id = opts.id;
|
|
@@ -12,6 +12,29 @@ export declare function shouldBlockOffPlatform(opts: {
|
|
|
12
12
|
tcpConfigured: boolean;
|
|
13
13
|
host?: string;
|
|
14
14
|
}): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Sniff the image format from the leading magic bytes: PNG starts with the
|
|
17
|
+
* 8-byte signature `89 50 4E 47` ("\x89PNG"), JPEG with `FF D8 FF`. Returns the
|
|
18
|
+
* canonical file extension, or null for anything else.
|
|
19
|
+
*/
|
|
20
|
+
export declare function detectImageFormat(buf: Buffer): '.png' | '.jpg' | null;
|
|
21
|
+
/**
|
|
22
|
+
* Make the screenshot filename honest about its bytes. The two helper backends
|
|
23
|
+
* encode DIFFERENT formats and neither re-encodes to match the requested name:
|
|
24
|
+
* the macOS helper (ScreenCaptureKit) returns JPEG
|
|
25
|
+
* (packages/computer-helper/Sources/ComputerHelper/Screenshot.swift:207,212),
|
|
26
|
+
* the Windows helper returns PNG
|
|
27
|
+
* (packages/computer-helper-win/Screenshot.cs:33). So a fixed default extension
|
|
28
|
+
* cannot be correct for both — the only honest path is to sniff the real format
|
|
29
|
+
* and swap the extension to match. Pure so it's unit-testable.
|
|
30
|
+
*
|
|
31
|
+
* Returns the path to write to (caller's path with its extension corrected) and
|
|
32
|
+
* whether a correction was made. Unknown formats pass through unchanged.
|
|
33
|
+
*/
|
|
34
|
+
export declare function reconcileScreenshotExt(outPath: string, buf: Buffer): {
|
|
35
|
+
path: string;
|
|
36
|
+
corrected: boolean;
|
|
37
|
+
};
|
|
15
38
|
export declare function registerComputerCommand(program: Command): void;
|
|
16
39
|
export declare function registerComputerSubcommands(program: Command): void;
|
|
17
40
|
export { resolveHelperExec as resolveHelperPath };
|
|
@@ -33,6 +33,42 @@ export function shouldBlockOffPlatform(opts) {
|
|
|
33
33
|
return false; // remote path resolves its own endpoint
|
|
34
34
|
return true;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Sniff the image format from the leading magic bytes: PNG starts with the
|
|
38
|
+
* 8-byte signature `89 50 4E 47` ("\x89PNG"), JPEG with `FF D8 FF`. Returns the
|
|
39
|
+
* canonical file extension, or null for anything else.
|
|
40
|
+
*/
|
|
41
|
+
export function detectImageFormat(buf) {
|
|
42
|
+
if (buf.length >= 4 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47)
|
|
43
|
+
return '.png';
|
|
44
|
+
if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff)
|
|
45
|
+
return '.jpg';
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Make the screenshot filename honest about its bytes. The two helper backends
|
|
50
|
+
* encode DIFFERENT formats and neither re-encodes to match the requested name:
|
|
51
|
+
* the macOS helper (ScreenCaptureKit) returns JPEG
|
|
52
|
+
* (packages/computer-helper/Sources/ComputerHelper/Screenshot.swift:207,212),
|
|
53
|
+
* the Windows helper returns PNG
|
|
54
|
+
* (packages/computer-helper-win/Screenshot.cs:33). So a fixed default extension
|
|
55
|
+
* cannot be correct for both — the only honest path is to sniff the real format
|
|
56
|
+
* and swap the extension to match. Pure so it's unit-testable.
|
|
57
|
+
*
|
|
58
|
+
* Returns the path to write to (caller's path with its extension corrected) and
|
|
59
|
+
* whether a correction was made. Unknown formats pass through unchanged.
|
|
60
|
+
*/
|
|
61
|
+
export function reconcileScreenshotExt(outPath, buf) {
|
|
62
|
+
const actual = detectImageFormat(buf);
|
|
63
|
+
if (!actual)
|
|
64
|
+
return { path: outPath, corrected: false };
|
|
65
|
+
const cur = path.extname(outPath).toLowerCase();
|
|
66
|
+
const alreadyMatches = cur === actual || (actual === '.jpg' && (cur === '.jpg' || cur === '.jpeg'));
|
|
67
|
+
if (alreadyMatches)
|
|
68
|
+
return { path: outPath, corrected: false };
|
|
69
|
+
const base = outPath.slice(0, outPath.length - path.extname(outPath).length);
|
|
70
|
+
return { path: base + actual, corrected: true };
|
|
71
|
+
}
|
|
36
72
|
export function registerComputerCommand(program) {
|
|
37
73
|
const computer = program
|
|
38
74
|
.command('computer')
|
|
@@ -128,8 +164,8 @@ function registerScreenshotCommand(program) {
|
|
|
128
164
|
.option('--list', 'List the app\'s windows (id/title/layer/bounds) instead of capturing — reveals modals/popups')
|
|
129
165
|
.option('--window-id <n>', 'Capture a specific window by id (from --list)', (v) => parseInt(v, 10))
|
|
130
166
|
.option('--display', 'Capture the whole display the app is on (composites stacked modals)')
|
|
131
|
-
.option('--out <path>', 'Output JPEG
|
|
132
|
-
.option('--quality <n>', 'JPEG quality 1-100', (v) => parseInt(v, 10), 85)
|
|
167
|
+
.option('--out <path>', 'Output image path — extension auto-corrected to the encoded format (JPEG on macOS, PNG on a Windows --host)', './computer-screenshot.jpg')
|
|
168
|
+
.option('--quality <n>', 'JPEG quality 1-100 (macOS capture only; the Windows helper encodes lossless PNG and ignores this)', (v) => parseInt(v, 10), 85)
|
|
133
169
|
.option('--json', 'Emit JSON (metadata for captures; window list for --list)')
|
|
134
170
|
.action(async (opts) => {
|
|
135
171
|
const quality = Math.max(1, Math.min(100, opts.quality || 85));
|
|
@@ -176,7 +212,10 @@ function registerScreenshotCommand(program) {
|
|
|
176
212
|
process.exit(1);
|
|
177
213
|
}
|
|
178
214
|
const buf = Buffer.from(b64, 'base64');
|
|
179
|
-
|
|
215
|
+
// Sniff the real format and correct the extension so the filename never
|
|
216
|
+
// lies about its bytes (macOS -> JPEG, Windows helper -> PNG).
|
|
217
|
+
const requested = path.resolve(opts.out);
|
|
218
|
+
const { path: outPath, corrected } = reconcileScreenshotExt(requested, buf);
|
|
180
219
|
fs.writeFileSync(outPath, buf);
|
|
181
220
|
if (opts.json) {
|
|
182
221
|
// Drop the heavy base64 from the metadata echo; report where it went.
|
|
@@ -184,6 +223,9 @@ function registerScreenshotCommand(program) {
|
|
|
184
223
|
console.log(JSON.stringify(meta, null, 2));
|
|
185
224
|
}
|
|
186
225
|
else {
|
|
226
|
+
if (corrected) {
|
|
227
|
+
console.log(`note: bytes are ${path.extname(outPath).slice(1).toUpperCase()}; corrected extension from ${path.basename(requested)}`);
|
|
228
|
+
}
|
|
187
229
|
const origin = res.origin || [];
|
|
188
230
|
const originStr = origin.length === 2 ? `, origin [${origin.join(',')}], scale ${res.scale ?? '?'}` : '';
|
|
189
231
|
console.log(`saved: ${outPath} (${res.width ?? '?'}x${res.height ?? '?'}, ${buf.byteLength} bytes${originStr})`);
|
package/dist/commands/cost.js
CHANGED
|
@@ -4,6 +4,7 @@ import { discoverSessions, parseTimeFilter } from '../lib/session/discover.js';
|
|
|
4
4
|
import { queryUsageRollup, topSessionsByCost, } from '../lib/session/db.js';
|
|
5
5
|
import { formatUsd, PRICING_VERSION } from '../lib/pricing/index.js';
|
|
6
6
|
import { formatDuration } from '../lib/session/render.js';
|
|
7
|
+
import { terminalWidth, truncateToWidth, stringWidth, padToWidth } from '../lib/session/width.js';
|
|
7
8
|
export function registerCostCommand(program) {
|
|
8
9
|
addHostOption(program.command('cost'))
|
|
9
10
|
.description('Roll up $ cost and duration across local agent sessions')
|
|
@@ -84,27 +85,36 @@ async function costAction(options) {
|
|
|
84
85
|
// Top sessions by cost.
|
|
85
86
|
if (top.length > 0) {
|
|
86
87
|
out.push(chalk.bold('Top sessions by cost'));
|
|
88
|
+
const cols = terminalWidth();
|
|
89
|
+
const showProject = cols >= 100;
|
|
87
90
|
const costW = Math.max(...top.map(t => formatUsd(t.costUsd).length), 4);
|
|
88
91
|
for (const t of top) {
|
|
89
92
|
const cost = formatUsd(t.costUsd).padStart(costW);
|
|
90
93
|
const dur = t.durationMs > 0 ? formatDuration(t.durationMs) : '—';
|
|
91
|
-
const label = t.meta.label || t.meta.topic || '(untitled)';
|
|
92
|
-
const proj = t.meta.project ? chalk.gray(` ${t.meta.project}`) : '';
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
const label = (t.meta.label || t.meta.topic || '(untitled)').replace(/\s+/g, ' ').trim();
|
|
95
|
+
const proj = showProject && t.meta.project ? chalk.gray(` ${t.meta.project}`) : '';
|
|
96
|
+
const prefix = ` ${chalk.green(cost)} ${chalk.gray(t.meta.shortId)} ${chalk.cyan(t.meta.agent.padEnd(7))} `;
|
|
97
|
+
const suffix = proj + chalk.gray(` ${dur}`);
|
|
98
|
+
const topicW = Math.max(12, cols - stringWidth(prefix) - stringWidth(suffix));
|
|
99
|
+
out.push(prefix + truncateToWidth(label, topicW) + suffix);
|
|
96
100
|
}
|
|
97
101
|
out.push('');
|
|
98
102
|
}
|
|
99
103
|
// Per-agent / per-project / per-day breakdown.
|
|
100
104
|
const groupLabel = groupBy === 'agent' ? 'agent' : groupBy === 'project' ? 'project' : 'day';
|
|
101
105
|
out.push(chalk.bold(`By ${groupLabel}`));
|
|
102
|
-
const
|
|
106
|
+
const cols = terminalWidth();
|
|
103
107
|
const costW2 = Math.max(...breakdown.map(r => formatUsd(r.costUsd).length), 4);
|
|
108
|
+
const countW = Math.max(...breakdown.map(r => String(r.sessionCount).length), 1);
|
|
109
|
+
const sessionW = Math.max(...breakdown.map(r => `${String(r.sessionCount).padStart(countW)} session${r.sessionCount !== 1 ? 's' : ''}`.length));
|
|
110
|
+
const durationW = Math.max(...breakdown.map(r => r.durationMs > 0 ? stringWidth(formatDuration(r.durationMs)) : 1), 1);
|
|
111
|
+
const fixedW = 2 + 2 + costW2 + 2 + sessionW + 2 + durationW;
|
|
112
|
+
const keyW = Math.max(8, Math.min(Math.max(...breakdown.map(r => stringWidth(r.key)), groupLabel.length), cols - fixedW));
|
|
104
113
|
for (const r of breakdown) {
|
|
105
114
|
const cost = formatUsd(r.costUsd).padStart(costW2);
|
|
106
115
|
const dur = r.durationMs > 0 ? formatDuration(r.durationMs) : '—';
|
|
107
|
-
|
|
116
|
+
const sessions = `${String(r.sessionCount).padStart(countW)} session${r.sessionCount !== 1 ? 's' : ''}`;
|
|
117
|
+
out.push(` ${padToWidth(truncateToWidth(r.key, keyW), keyW)} ${chalk.green(cost)} ${chalk.gray(padToWidth(sessions, sessionW))} ${chalk.gray(padToWidth(dur, durationW))}`);
|
|
108
118
|
}
|
|
109
119
|
console.log(out.join('\n'));
|
|
110
120
|
}
|
|
@@ -132,8 +142,3 @@ function renderDailyHistogram(daily) {
|
|
|
132
142
|
}
|
|
133
143
|
return lines.join('\n');
|
|
134
144
|
}
|
|
135
|
-
/** Truncate a string to n chars with an ellipsis. */
|
|
136
|
-
function truncate(s, n) {
|
|
137
|
-
const oneLine = s.replace(/\s+/g, ' ').trim();
|
|
138
|
-
return oneLine.length > n ? oneLine.slice(0, n - 1) + '…' : oneLine;
|
|
139
|
-
}
|
|
@@ -22,4 +22,15 @@
|
|
|
22
22
|
* apply pending sync.
|
|
23
23
|
*/
|
|
24
24
|
import type { Command } from 'commander';
|
|
25
|
+
export declare function wrapLine(prefix: string, text: string, width?: number): string[];
|
|
26
|
+
/**
|
|
27
|
+
* Windows-only advisory lines. When the effective PowerShell execution policy
|
|
28
|
+
* blocks unsigned local `.ps1` scripts (`Restricted`/`AllSigned`), the generated
|
|
29
|
+
* `agents.ps1` launcher fails in PowerShell even when it is on PATH. Surface the
|
|
30
|
+
* remediation; the `.cmd` companion still works, so this is a warning, not an
|
|
31
|
+
* error, and doctor never auto-changes the policy. Pure — returns `[]` on
|
|
32
|
+
* non-Windows or a permissive policy, so it is testable without invoking
|
|
33
|
+
* PowerShell.
|
|
34
|
+
*/
|
|
35
|
+
export declare function execPolicyWarningLines(platform: NodeJS.Platform, policy: string | null): string[];
|
|
25
36
|
export declare function registerDoctorCommand(program: Command): void;
|