@phnx-labs/agents-cli 1.20.43 → 1.20.44
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 +10 -0
- package/README.md +4 -3
- package/dist/commands/exec.js +30 -2
- package/dist/commands/hosts.js +5 -4
- package/dist/commands/logs.d.ts +4 -0
- package/dist/commands/logs.js +19 -13
- package/dist/commands/routines.d.ts +6 -0
- package/dist/commands/routines.js +70 -12
- package/dist/commands/sessions.d.ts +6 -5
- package/dist/commands/sessions.js +50 -22
- package/dist/commands/teams.js +43 -5
- package/dist/lib/daemon.js +34 -9
- package/dist/lib/exec.d.ts +8 -0
- package/dist/lib/exec.js +70 -6
- package/dist/lib/hosts/logs.d.ts +14 -5
- package/dist/lib/hosts/logs.js +39 -13
- package/dist/lib/redact.js +1 -0
- package/dist/lib/session/active.d.ts +6 -0
- package/dist/lib/session/active.js +10 -1
- package/dist/lib/shims.d.ts +1 -1
- package/dist/lib/shims.js +17 -3
- package/dist/lib/teams/agents.js +16 -7
- package/dist/lib/tmux/session.d.ts +40 -0
- package/dist/lib/tmux/session.js +92 -0
- package/dist/lib/versions.d.ts +54 -1
- package/dist/lib/versions.js +138 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 1.20.44
|
|
6
|
+
|
|
7
|
+
- **Every `logs` command is concise by default; the token-heavy raw dump is now opt-in behind `--full`.** Agents that spin up agents on other machines or add teammates were pulling whole transcripts just to glance at status — `agents logs <session>` printed the full markdown transcript, and `agents hosts logs` / `agents teams logs` / `agents routines logs` each `cat`'d their entire captured stdout, because each subsystem had hand-rolled its own "cat the log" verb over its own storage. All four now default to a bounded, concise view, with `-m/--full` for the raw log: `agents logs <session>` renders the same summary digest as `agents sessions <id>` (a real session shrank 92% — 29.9 KB → 2.6 KB); `agents routines logs <name>` shows a status header + the extracted report (a real run shrank 99.5% — 386 KB → 1.8 KB), falling back to a bounded stdout tail when no report was extracted; `agents teams logs <teammate>` renders the teammate's session summary (its agentId **is** the session id), with `-n <lines>` / `--full` for raw stdout; `agents hosts logs <id>` shows a bounded tail of the captured stdout (`tailLines`, with a "… N earlier lines hidden — pass --full" note) instead of the whole log. `renderSessionLog` now takes a mode and defaults to `'summary'`; `agents sessions <id>` was already summary-by-default and is unchanged. Regression-tested: `tailLines` truncation/elision math (`hosts/logs.test.ts`) and `formatRunDuration` human-time formatting (`routines-logs.test.ts`). Source: `apps/cli/src/commands/{logs,sessions,hosts,teams,routines}.ts`, `apps/cli/src/lib/hosts/logs.ts`. Scoped follow-up (not in this PR): host-task and sandboxed-routine runs write their real transcript on the remote / in an overlay HOME, so `logs` can't yet resolve them to the full `renderSummary` — making those runs discoverable is a separate change; until then the bounded tail / extracted report is the safe concise default.
|
|
8
|
+
- **The daemon now self-heals the `pane-died` hook on already-running `agents run` sessions.** The v1.20.42 fix that stops exiting a split from kicking you out of tmux is installed once, at session creation — so sessions already alive under the long-lived shared tmux server keep the old, unconditional `detach-client` hook until they exit or the server is recycled. On a machine that's never "between sessions," that meant hand-repairing live sessions. The daemon now runs `reconcileSessionHooks()` ~20s after startup and every ~5 min: it walks the managed `ag-` sessions on the shared socket and retrofits the `#{hook_pane}`-guarded hook onto any whose hook predates the current schema. It is strictly **non-destructive — `set-hook` only, never a `kill-pane` or `detach-client`** — so it is safe to run against sessions you're attached to; a per-session `@ag_hook_schema` marker makes steady-state a no-op. The hook string is now built in one place (`agentPaneDiedHook`) shared by the spawn-wrap and the reconcile so they can't drift. Source: `apps/cli/src/lib/tmux/session.ts`, `apps/cli/src/lib/daemon.ts`, `apps/cli/src/lib/exec.ts`.
|
|
9
|
+
- **NEW: `agents run` self-heals a gutted install instead of crashing with `ENOENT`.** The recurring failure: an npm agent whose native binary ships as an optional per-arch dependency (codex → `@openai/codex-<platform>`) can have that tarball extract **partially** — the platform package's `package.json` lands, its `vendor/<triple>/…/codex` binary does not (an interrupted or concurrently-raced `agents add` into the same version dir). The CLI's wrapper `require.resolve`s the platform package, finds the `package.json`, and sails straight past its own "missing optional dependency" guard into a `spawn(binaryPath)` that dies with a raw `ENOENT`. `agents run` now probes the version it's about to launch and, if the binary can't run, **repairs it in place** (a *clean* reinstall — the partial `node_modules` is wiped first, because npm treats the present-but-gutted platform package as already installed and would otherwise skip re-fetching it), then falls back to another installed version that launches (re-pinning it as the default so the shim path heals too), then to installing `latest` — only erroring if nothing can be made runnable. `installVersion` gained a `{ clean }` option for the wipe-then-reinstall. Source: `apps/cli/src/lib/versions.ts` (`ensureAgentRunnable`), `apps/cli/src/commands/exec.ts`.
|
|
10
|
+
- **Fix: a broken agent install no longer launches into a silent `[detached]` — the real crash is surfaced.** When an interactive `ag run <agent>` wrapped the agent in tmux and the agent died the instant it spawned (e.g. a gutted install crashing with `spawn … ENOENT`, a bad flag, a startup crash), the `pane-died` hook detached the client before you could read anything — you got a bare `[detached (from session …)]` with zero indication of why. `runInTmux` now recaps the dead pane's last output (read from scrollback via `capture-pane -S -200`, since the pane's visible screen is just the "Pane is dead" banner) plus the exit code to stderr, and points you at `--no-tmux`. Fast failures (dead before attach) always recap; a post-attach nonzero exit recaps too (a clean exit or a manual detach stays quiet). Source: `apps/cli/src/lib/exec.ts`, `apps/cli/src/lib/tmux/session.test.ts`.
|
|
11
|
+
- **NEW: `--no-tmux` / `--disable-tmux` on `agents run`.** The interactive tmux wrapper (which gives `%pane` addressing + re-attach) already had an opt-out, but it was hidden behind the opaquely-named `--raw`. `--no-tmux` (and its alias `--disable-tmux`) spawn the agent directly with full stdio inherited — the fastest way to see an agent's real startup output when a launch is failing. Same effect as `--raw` and `AGENTS_NO_TMUX=1`. Source: `apps/cli/src/commands/exec.ts`.
|
|
12
|
+
- **Fix: `agents add <agent>@<version>` no longer records a gutted install as healthy (root cause of the ENOENT crash + a broken default pin).** npm packages that ship their native binary via an optional per-arch dependency (e.g. codex → `@openai/codex-<platform>`) can land the JS wrapper at `node_modules/.bin/<cli>` while the real platform binary is missing (interrupted install, omitted optional dep, `--ignore-scripts`). `getBinaryPath()` only checked the wrapper, so the broken version read as installed, got pinned as the default, and got picked to run — then died with ENOENT. `installVersion` now probes `<binary> --version` (under the version's isolated HOME) after install and **fails the install** if the binary can't launch, so a broken version is never silently pinned. The check is deliberately narrow — only the missing-binary signature (`ENOENT`/"no such file"/"command not found") fails it; a plain nonzero exit or a timeout is treated as healthy, so a well-behaved agent that dislikes `--version` is never false-failed. Source: `apps/cli/src/lib/versions.ts`, `apps/cli/src/lib/versions-integrity.test.ts`.
|
|
13
|
+
- **Security fix: the routines daemon log no longer leaks GitHub / AWS / npm tokens.** `daemon.ts` carried its own private `redactSecrets` (used by every `log()` write to `logs.jsonl`) that predated and diverged from the canonical `redact.ts` — it caught `sk-`, `eyJ…`, `Bearer …`, and a narrow `NAME=value` list, but **not** `ghp_` (GitHub PAT), `AKIA…` (AWS access key), or `npm_` (npm token), so any of those appearing in a daemon message (a git push URL, a bundle-env dump, an error string) was written to the log in the clear. The private copy is deleted; `log()` now routes through the canonical `redactSecrets` in `redact.ts`, which covers all of those classes with a stronger quote-aware `NAME=value` pattern. The one pattern the daemon copy had and the canonical lacked — `Bearer <token>` — is added to `redact.ts`, so the shared redactor (also used by session-transcript export in `session/render.ts`) is now a strict superset. New `redact.test.ts` pins every token class as a regression guard. Source: `apps/cli/src/lib/daemon.ts`, `apps/cli/src/lib/redact.ts`, `apps/cli/src/lib/redact.test.ts`.
|
|
14
|
+
|
|
5
15
|
## 1.20.43
|
|
6
16
|
|
|
7
17
|
- **NEW: `agents run --name <slug>` — a durable, human/agent-friendly handle for any run.** An agent that dispatches another agent had no cheap status handle: the host-task id was never even printed (the `--no-follow` tip showed a literal `<id>` placeholder), and only Claude's session id is known up front (pre-minted `--session-id`) — every other agent's id is discovered later by scanning transcripts, so callers fell back to `agents logs`, which dumps the raw, token-heavy transcript. `--name` is chosen at launch, agent-agnostic, and stored on the structures that already back these views: a first-class `name` column on `sessions.db` (schema v9, additive, no rescan) parallel to `label` — `agents sessions <ref>` resolves against **both** name and label; the HostTask sidecar (forwarded to the remote run, so `agents hosts ps` gains a NAME column and `agents hosts logs <name>` resolves by name); and a run-name sidecar (`~/.agents/.cache/run-names/`) that joins a local run's name onto the index by id every scan via `syncNames` — the same idempotent pattern as `/rename` label sync. The `name` column is deliberately left out of the upsert `ON CONFLICT … SET` clause, so a discovery rescan can never null an existing name (regression-tested in `db.names.test.ts`). Omitting `--name` is a strict no-op: `name` stays unset and every id-based path is unchanged. The `--no-follow` dispatch tip now prints the real handle and steers to the compact `agents sessions` digest over the raw log. Source: `apps/cli/src/commands/exec.ts`, `apps/cli/src/lib/session/{db,run-names,discover}.ts`, `apps/cli/src/lib/hosts/{dispatch,tasks}.ts`.
|
package/README.md
CHANGED
|
@@ -243,7 +243,8 @@ agents hosts check gpu-box # reachable? which agents-cli version?
|
|
|
243
243
|
|
|
244
244
|
# Run there instead of locally
|
|
245
245
|
agents run claude --host gpu-box "profile this build" # follows live by default
|
|
246
|
-
agents logs --host gpu-box # pick a dispatched run
|
|
246
|
+
agents logs --host gpu-box # pick a dispatched run — concise summary by default
|
|
247
|
+
agents logs <id> --full # the full raw transcript / stdout (token-heavy)
|
|
247
248
|
agents logs <id> -f # re-attach to a running one and follow
|
|
248
249
|
agents view claude --host gpu-box # inspect the remote install
|
|
249
250
|
agents sync --host gpu-box # make the remote machine current
|
|
@@ -274,7 +275,7 @@ agents teams start auth-feature # Fires teammates whose deps are done
|
|
|
274
275
|
agents teams status auth-feature # Who's working, what they changed, what they said
|
|
275
276
|
```
|
|
276
277
|
|
|
277
|
-
Teammates run detached -- close your terminal, they keep working. Check in with `teams status`,
|
|
278
|
+
Teammates run detached -- close your terminal, they keep working. Check in with `teams status`, glance at a teammate's summary with `teams logs <name>` (add `--full` for the raw output), clean up with `teams disband`.
|
|
278
279
|
|
|
279
280
|
Team state is observable via `agents teams list --json` / `agents teams status --json` (compact by default; add `--verbose` for the full per-teammate shape). External tools join it with `sessions --json` (teammates get `isTeamOrigin: true`) and `cloud list --json` (for `--cloud` teammates) to build a unified fleet view. See [docs/06-observability.md](docs/06-observability.md).
|
|
280
281
|
|
|
@@ -534,7 +535,7 @@ agents routines add daily-digest \
|
|
|
534
535
|
|
|
535
536
|
agents routines list # All jobs + next run times
|
|
536
537
|
agents routines run daily-digest # Test it now, ignore the schedule
|
|
537
|
-
agents routines logs daily-digest #
|
|
538
|
+
agents routines logs daily-digest # Last execution — status + report (add --full for raw stdout)
|
|
538
539
|
```
|
|
539
540
|
|
|
540
541
|
Jobs run sandboxed -- agents only see directories and tools you explicitly allow.
|
package/dist/commands/exec.js
CHANGED
|
@@ -221,6 +221,8 @@ export function registerRunCommand(program) {
|
|
|
221
221
|
.option('--name <slug>', 'Give the run a durable name — a stable handle you can check on later with `agents sessions <name>` (and `agents hosts logs <name>` for --host runs), instead of an opaque id. Optional; omitting it keeps today\'s id-only behavior.')
|
|
222
222
|
.option('--verbose', 'Show detailed execution logs')
|
|
223
223
|
.option('--raw', 'Interactive runs on macOS/Linux launch inside a shared tmux session (for %pane addressing + re-attach). Pass --raw to spawn the agent directly instead. Also disabled by AGENTS_NO_TMUX=1.')
|
|
224
|
+
.option('--no-tmux', 'Spawn the agent directly instead of wrapping it in the shared tmux session. Same effect as --raw / AGENTS_NO_TMUX=1. Use this to see the agent\'s full startup output when a launch is failing.')
|
|
225
|
+
.option('--disable-tmux', 'Alias for --no-tmux.')
|
|
224
226
|
.option('--timeout <duration>', 'Kill the agent after this duration (e.g., 30m, 1h, 2h30m)')
|
|
225
227
|
.option('--fallback <agents>', 'Comma-separated agents to try on rate-limit failure. Each entry accepts an optional @version pin (e.g., codex@0.116.0,gemini). The primary runs first; if it exits with a rate-limit error, the next agent picks up via /continue handoff.')
|
|
226
228
|
.option('-b, --balanced', 'Shortcut for --strategy balanced. Ignored when @version is pinned.')
|
|
@@ -525,7 +527,7 @@ export function registerRunCommand(program) {
|
|
|
525
527
|
});
|
|
526
528
|
process.exit(resumeExit);
|
|
527
529
|
}
|
|
528
|
-
const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, defaultModeFor, headlessPlanStallCommand, nativeResume, resolveInteractive }, { ALL_AGENT_IDS }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported }, { splitBundleRef, resolveSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports },] = await Promise.all([
|
|
530
|
+
const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, defaultModeFor, headlessPlanStallCommand, nativeResume, resolveInteractive }, { ALL_AGENT_IDS }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported }, { splitBundleRef, resolveSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports },] = await Promise.all([
|
|
529
531
|
import('../lib/exec.js'),
|
|
530
532
|
import('../lib/agents.js'),
|
|
531
533
|
import('../lib/profiles.js'),
|
|
@@ -933,6 +935,29 @@ export function registerRunCommand(program) {
|
|
|
933
935
|
}
|
|
934
936
|
}
|
|
935
937
|
}
|
|
938
|
+
// Self-heal the launch target. A gutted install (JS wrapper present,
|
|
939
|
+
// native binary missing — a partial/raced npm extraction of the optional
|
|
940
|
+
// per-arch dependency) would otherwise spawn and die with a raw ENOENT
|
|
941
|
+
// inside the agent's own wrapper. Repair it in place (or fall back to a
|
|
942
|
+
// runnable version, re-pinning it) BEFORE we build the launch command.
|
|
943
|
+
// Skipped for headless resume-native/acp/loop paths only if it errored;
|
|
944
|
+
// here it runs for every normal dispatch. Best-effort log to stderr.
|
|
945
|
+
{
|
|
946
|
+
const launchTarget = version ?? resolveVersion(agent, cwd) ?? undefined;
|
|
947
|
+
if (launchTarget) {
|
|
948
|
+
const healed = await ensureAgentRunnable(agent, launchTarget, options.quiet ? undefined : (m) => process.stderr.write(chalk.yellow(`[agents] ${m}\n`)));
|
|
949
|
+
if (healed === null) {
|
|
950
|
+
console.error(chalk.red(`agents: ${agent}@${launchTarget} is not runnable and could not be repaired. Try: agents add ${agent}@latest`));
|
|
951
|
+
process.exit(1);
|
|
952
|
+
}
|
|
953
|
+
// Always adopt the healed version explicitly. In the version-undefined
|
|
954
|
+
// path a fallback re-pins the GLOBAL default, but `resolveVersion`
|
|
955
|
+
// prefers a PROJECT pin — so leaving `version` undefined would let the
|
|
956
|
+
// shim re-resolve the still-broken project pin and crash anyway. Pinning
|
|
957
|
+
// the runnable version here is a no-op when nothing changed.
|
|
958
|
+
version = healed;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
936
961
|
const defaultVersion = version ?? resolveVersion(agent, cwd);
|
|
937
962
|
const runDefaults = fromProfile
|
|
938
963
|
? { sources: {} }
|
|
@@ -1074,7 +1099,10 @@ export function registerRunCommand(program) {
|
|
|
1074
1099
|
name: options.name,
|
|
1075
1100
|
resume: resumeNative,
|
|
1076
1101
|
verbose: options.verbose,
|
|
1077
|
-
raw
|
|
1102
|
+
// --raw, --no-tmux (commander negation → options.tmux === false), and
|
|
1103
|
+
// --disable-tmux all bypass the interactive tmux wrapper. AGENTS_NO_TMUX=1
|
|
1104
|
+
// does the same via the env check in exec.ts.
|
|
1105
|
+
raw: options.raw || options.tmux === false || options.disableTmux === true,
|
|
1078
1106
|
timeout: options.timeout,
|
|
1079
1107
|
env,
|
|
1080
1108
|
toolsRestrict: workflowToolsRestrict,
|
package/dist/commands/hosts.js
CHANGED
|
@@ -188,11 +188,11 @@ async function doPs(json) {
|
|
|
188
188
|
console.log(t.id.padEnd(11) + nameCol + t.host.padEnd(16) + t.agent.padEnd(10) + status.padEnd(11) + promptCol);
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
|
-
async function doLogs(ref, follow) {
|
|
191
|
+
async function doLogs(ref, follow, full) {
|
|
192
192
|
// Resolve the ref as a task id first, then fall back to a `--name` handle so
|
|
193
193
|
// `agents hosts logs <name>` works, not just the opaque id.
|
|
194
194
|
const id = loadTask(ref) ? ref : (findTaskByName(ref)?.id ?? findTaskBySessionId(ref)?.id ?? ref);
|
|
195
|
-
const res = await showHostTaskLog(id, follow);
|
|
195
|
+
const res = await showHostTaskLog(id, follow, full);
|
|
196
196
|
if (!res.found) {
|
|
197
197
|
console.log(chalk.red(`Unknown task "${ref}".`));
|
|
198
198
|
process.exitCode = 1;
|
|
@@ -235,7 +235,8 @@ export function registerHostsCommand(program) {
|
|
|
235
235
|
.action((opts) => doPs(!!opts.json));
|
|
236
236
|
hosts
|
|
237
237
|
.command('logs <id>')
|
|
238
|
-
.description('Show a host task
|
|
238
|
+
.description('Show a host task’s concise summary; --full for the raw log, -f to follow a running one.')
|
|
239
239
|
.option('-f, --follow', 'Follow live output')
|
|
240
|
-
.
|
|
240
|
+
.option('-m, --full', 'Show the full raw combined-stdout log instead of the concise summary')
|
|
241
|
+
.action((id, opts) => doLogs(id, !!opts.follow, !!opts.full));
|
|
241
242
|
}
|
package/dist/commands/logs.d.ts
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* - host-dispatch tasks (`agents run --host`) → combined-stdout log, offset-tailed
|
|
6
6
|
* - sessions (the local index) → transcript, tailed via the sessions tailer
|
|
7
7
|
*
|
|
8
|
+
* Concise by default: a bare `agents logs <id>` prints the same summary digest as
|
|
9
|
+
* `agents sessions <id>` — cheap for an agent to glance at. The token-heavy full
|
|
10
|
+
* transcript / raw stdout is opt-in behind `--full` (alias `-m/--markdown`).
|
|
11
|
+
*
|
|
8
12
|
* `[id]`/`--session` load directly (host task tried first, then session). With no
|
|
9
13
|
* id, `--host`/`--agent`/`--version` filter a merged candidate list; one match is
|
|
10
14
|
* shown, several open the fuzzy picker (or, non-TTY, print the list).
|
package/dist/commands/logs.js
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* - host-dispatch tasks (`agents run --host`) → combined-stdout log, offset-tailed
|
|
6
6
|
* - sessions (the local index) → transcript, tailed via the sessions tailer
|
|
7
7
|
*
|
|
8
|
+
* Concise by default: a bare `agents logs <id>` prints the same summary digest as
|
|
9
|
+
* `agents sessions <id>` — cheap for an agent to glance at. The token-heavy full
|
|
10
|
+
* transcript / raw stdout is opt-in behind `--full` (alias `-m/--markdown`).
|
|
11
|
+
*
|
|
8
12
|
* `[id]`/`--session` load directly (host task tried first, then session). With no
|
|
9
13
|
* id, `--host`/`--agent`/`--version` filter a merged candidate list; one match is
|
|
10
14
|
* shown, several open the fuzzy picker (or, non-TTY, print the list).
|
|
@@ -33,8 +37,8 @@ function candidateLabel(c) {
|
|
|
33
37
|
const title = s.label || s.topic || '';
|
|
34
38
|
return `${chalk.gray('sess')} ${s.shortId.padEnd(9)} ${(s.agent + ver).padEnd(14)} ${chalk.gray(s.timestamp.slice(0, 16))} ${title.slice(0, 40)}`;
|
|
35
39
|
}
|
|
36
|
-
/** Show a resolved session — follow (tail) or
|
|
37
|
-
async function showSession(session, follow) {
|
|
40
|
+
/** Show a resolved session — follow (tail), concise summary, or (`full`) transcript. */
|
|
41
|
+
async function showSession(session, follow, full) {
|
|
38
42
|
if (follow) {
|
|
39
43
|
if (!isTailable(session.agent)) {
|
|
40
44
|
console.error(chalk.red(`Tailing is supported for claude and codex sessions only (got ${session.agent}).`));
|
|
@@ -43,20 +47,20 @@ async function showSession(session, follow) {
|
|
|
43
47
|
await streamSessionTail(session, {});
|
|
44
48
|
return;
|
|
45
49
|
}
|
|
46
|
-
await renderSessionLog(session);
|
|
50
|
+
await renderSessionLog(session, full ? 'markdown' : 'summary');
|
|
47
51
|
}
|
|
48
|
-
async function showCandidate(c, follow) {
|
|
52
|
+
async function showCandidate(c, follow, full) {
|
|
49
53
|
if (c.kind === 'task') {
|
|
50
|
-
const res = await showHostTaskLog(c.task.id, follow);
|
|
54
|
+
const res = await showHostTaskLog(c.task.id, follow, full);
|
|
51
55
|
if (res.exitCode !== undefined)
|
|
52
56
|
process.exitCode = res.exitCode;
|
|
53
57
|
return;
|
|
54
58
|
}
|
|
55
|
-
await showSession(c.session, follow);
|
|
59
|
+
await showSession(c.session, follow, full);
|
|
56
60
|
}
|
|
57
61
|
/** Resolve an explicit id/--session: host task first, then a session. */
|
|
58
|
-
async function showById(id, follow) {
|
|
59
|
-
const hostRes = await showHostTaskLog(id, follow);
|
|
62
|
+
async function showById(id, follow, full) {
|
|
63
|
+
const hostRes = await showHostTaskLog(id, follow, full);
|
|
60
64
|
if (hostRes.found) {
|
|
61
65
|
if (hostRes.exitCode !== undefined)
|
|
62
66
|
process.exitCode = hostRes.exitCode;
|
|
@@ -68,13 +72,14 @@ async function showById(id, follow) {
|
|
|
68
72
|
console.error(chalk.red(`No run or session found matching "${id}".`));
|
|
69
73
|
process.exit(1);
|
|
70
74
|
}
|
|
71
|
-
await showSession(matches[0], follow);
|
|
75
|
+
await showSession(matches[0], follow, full);
|
|
72
76
|
}
|
|
73
77
|
async function runLogs(id, opts) {
|
|
74
78
|
const follow = !!opts.follow;
|
|
79
|
+
const full = !!opts.full;
|
|
75
80
|
const directId = opts.session ?? id;
|
|
76
81
|
if (directId) {
|
|
77
|
-
await showById(directId, follow);
|
|
82
|
+
await showById(directId, follow, full);
|
|
78
83
|
return;
|
|
79
84
|
}
|
|
80
85
|
const { agent, version } = parseAgentFilter(opts.agent);
|
|
@@ -99,7 +104,7 @@ async function runLogs(id, opts) {
|
|
|
99
104
|
process.exit(1);
|
|
100
105
|
}
|
|
101
106
|
if (candidates.length === 1) {
|
|
102
|
-
await showCandidate(candidates[0], follow);
|
|
107
|
+
await showCandidate(candidates[0], follow, full);
|
|
103
108
|
return;
|
|
104
109
|
}
|
|
105
110
|
// Multiple sessions matched → picker if interactive, else a list to pick from.
|
|
@@ -123,17 +128,18 @@ async function runLogs(id, opts) {
|
|
|
123
128
|
});
|
|
124
129
|
if (!picked)
|
|
125
130
|
return;
|
|
126
|
-
await showCandidate(picked.item, follow);
|
|
131
|
+
await showCandidate(picked.item, follow, full);
|
|
127
132
|
}
|
|
128
133
|
/** Register the top-level `agents logs` command. */
|
|
129
134
|
export function registerLogsCommand(program) {
|
|
130
135
|
program
|
|
131
136
|
.command('logs [id]')
|
|
132
|
-
.description('Show a run’s
|
|
137
|
+
.description('Show a run’s concise summary — a host-dispatch task or a session. --full for the raw transcript, -f to follow a live one.')
|
|
133
138
|
.option('--host <name>', 'Scope to runs dispatched to a host')
|
|
134
139
|
.option('-a, --agent <agent>', 'Filter by agent (e.g. claude, codex@0.116.0)')
|
|
135
140
|
.option('--version <version>', 'Filter by agent version')
|
|
136
141
|
.option('--session <id>', 'Select a session/run by id (same as the positional id)')
|
|
137
142
|
.option('-f, --follow', 'Follow live output')
|
|
143
|
+
.option('-m, --full', 'Show the full raw transcript / stdout instead of the concise summary')
|
|
138
144
|
.action((id, opts) => runLogs(id, opts));
|
|
139
145
|
}
|
|
@@ -6,5 +6,11 @@
|
|
|
6
6
|
* Also exposes scheduler lifecycle controls (start/stop/status/logs).
|
|
7
7
|
*/
|
|
8
8
|
import type { Command } from 'commander';
|
|
9
|
+
/**
|
|
10
|
+
* Human-friendly wall-clock a run took (e.g. " · 3 min", " · 45 sec"), or ""
|
|
11
|
+
* when it hasn't completed or timestamps are unparseable. Leading separator lets
|
|
12
|
+
* callers drop it straight into a status line.
|
|
13
|
+
*/
|
|
14
|
+
export declare function formatRunDuration(startedAt: string, completedAt: string | null): string;
|
|
9
15
|
/** Register the `agents routines` command tree. */
|
|
10
16
|
export declare function registerRoutinesCommands(program: Command): void;
|
|
@@ -22,6 +22,27 @@ import { JobScheduler } from '../lib/scheduler.js';
|
|
|
22
22
|
import { detectOverdueJobs } from '../lib/overdue.js';
|
|
23
23
|
import { isInteractiveTerminal, requireInteractiveSelection } from './utils.js';
|
|
24
24
|
import { setHelpSections } from '../lib/help.js';
|
|
25
|
+
/**
|
|
26
|
+
* Human-friendly wall-clock a run took (e.g. " · 3 min", " · 45 sec"), or ""
|
|
27
|
+
* when it hasn't completed or timestamps are unparseable. Leading separator lets
|
|
28
|
+
* callers drop it straight into a status line.
|
|
29
|
+
*/
|
|
30
|
+
export function formatRunDuration(startedAt, completedAt) {
|
|
31
|
+
if (!completedAt)
|
|
32
|
+
return '';
|
|
33
|
+
const ms = Date.parse(completedAt) - Date.parse(startedAt);
|
|
34
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
35
|
+
return '';
|
|
36
|
+
const sec = Math.round(ms / 1000);
|
|
37
|
+
if (sec < 60)
|
|
38
|
+
return ` · ${sec} sec`;
|
|
39
|
+
const min = Math.round(sec / 60);
|
|
40
|
+
if (min < 60)
|
|
41
|
+
return ` · ${min} min`;
|
|
42
|
+
const hr = Math.floor(min / 60);
|
|
43
|
+
const rem = min % 60;
|
|
44
|
+
return rem ? ` · ${hr} hr ${rem} min` : ` · ${hr} hr`;
|
|
45
|
+
}
|
|
25
46
|
/**
|
|
26
47
|
* Human label for what fires a job: its cron schedule, or its event trigger
|
|
27
48
|
* for schedule-less (trigger-only) routines.
|
|
@@ -660,30 +681,67 @@ export function registerRoutinesCommands(program) {
|
|
|
660
681
|
});
|
|
661
682
|
routinesCmd
|
|
662
683
|
.command('logs [name]')
|
|
663
|
-
.description('
|
|
684
|
+
.description('Show a run’s concise summary — status + extracted report. --full for the raw stdout stream; --run for a specific past run.')
|
|
664
685
|
.option('-r, --run <runId>', 'Show logs from this run ID instead of the latest')
|
|
686
|
+
.option('-m, --full', 'Show the full raw stdout stream instead of the concise summary')
|
|
665
687
|
.action(async (name, options) => {
|
|
666
688
|
if (!name) {
|
|
667
689
|
name = await pickJob('Select job to view logs', undefined, ['agents routines logs <name>', 'agents routines logs <name> --run <run-id>']) ?? undefined;
|
|
668
690
|
if (!name)
|
|
669
691
|
return;
|
|
670
692
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
693
|
+
// Resolve the run: an explicit --run row, else the latest.
|
|
694
|
+
const run = options.run
|
|
695
|
+
? listRuns(name).find((r) => r.runId === options.run)
|
|
696
|
+
: getLatestRun(name);
|
|
697
|
+
if (!run) {
|
|
698
|
+
console.log(chalk.yellow(options.run ? `No run '${options.run}' for job '${name}'` : `No runs found for job '${name}'`));
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
const runId = run.runId;
|
|
702
|
+
const logPath = path.join(getRunDir(name, runId), 'stdout.log');
|
|
703
|
+
// --full: the raw combined stdout stream (the old default).
|
|
704
|
+
if (options.full) {
|
|
705
|
+
if (!fs.existsSync(logPath)) {
|
|
706
|
+
console.log(chalk.yellow(`Log not found: ${logPath}`));
|
|
676
707
|
return;
|
|
677
708
|
}
|
|
678
|
-
|
|
709
|
+
console.log(chalk.gray(`Run: ${runId}\n`));
|
|
710
|
+
console.log(fs.readFileSync(logPath, 'utf-8'));
|
|
711
|
+
return;
|
|
679
712
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
713
|
+
// Concise by default: a status header + the extracted report (final
|
|
714
|
+
// assistant message). Routine runs are sandboxed (transcript in an overlay
|
|
715
|
+
// HOME, not the session index), so the captured report — not renderSummary —
|
|
716
|
+
// is the concise view. Falls back to a bounded stdout tail when no report
|
|
717
|
+
// was extracted (e.g. the run failed before finishing).
|
|
718
|
+
const statusColor = run.status === 'completed' ? chalk.green
|
|
719
|
+
: run.status === 'failed' || run.status === 'timeout' ? chalk.red
|
|
720
|
+
: chalk.yellow;
|
|
721
|
+
console.log(chalk.bold(name) + chalk.gray(` run ${runId}`));
|
|
722
|
+
console.log(statusColor(run.status) +
|
|
723
|
+
chalk.gray(` ${run.startedAt}`) +
|
|
724
|
+
chalk.gray(formatRunDuration(run.startedAt, run.completedAt)) +
|
|
725
|
+
(run.exitCode !== null && run.exitCode !== undefined ? chalk.gray(` exit ${run.exitCode}`) : ''));
|
|
726
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
727
|
+
const reportPath = path.join(getRunDir(name, runId), 'report.md');
|
|
728
|
+
if (fs.existsSync(reportPath)) {
|
|
729
|
+
console.log(fs.readFileSync(reportPath, 'utf-8').trimEnd());
|
|
730
|
+
console.log(chalk.gray('\n(pass --full for the raw stdout stream)'));
|
|
683
731
|
return;
|
|
684
732
|
}
|
|
685
|
-
|
|
686
|
-
|
|
733
|
+
// No report — show a bounded tail rather than dumping the whole stream.
|
|
734
|
+
if (fs.existsSync(logPath)) {
|
|
735
|
+
const lines = fs.readFileSync(logPath, 'utf-8').split('\n');
|
|
736
|
+
const tail = lines.slice(-40).join('\n').trimEnd();
|
|
737
|
+
console.log(chalk.gray('(no report extracted — showing the last lines of stdout)'));
|
|
738
|
+
if (tail)
|
|
739
|
+
console.log(tail);
|
|
740
|
+
console.log(chalk.gray('\n(pass --full for the raw stdout stream)'));
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
console.log(chalk.gray('(no output captured for this run)'));
|
|
744
|
+
}
|
|
687
745
|
});
|
|
688
746
|
routinesCmd
|
|
689
747
|
.command('report [name]')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
-
import type { SessionAgentId, SessionMeta } from '../lib/session/types.js';
|
|
2
|
+
import type { SessionAgentId, SessionMeta, ViewMode } from '../lib/session/types.js';
|
|
3
3
|
import { type ActiveSession } from '../lib/session/active.js';
|
|
4
4
|
import { type PickedSession } from './sessions-picker.js';
|
|
5
5
|
/**
|
|
@@ -149,11 +149,12 @@ export declare function buildOverviewGroups(pool: SessionMeta[], perProjectCap:
|
|
|
149
149
|
projectCount: number;
|
|
150
150
|
};
|
|
151
151
|
/**
|
|
152
|
-
* Render a session
|
|
153
|
-
* `agents logs <sessionId>`.
|
|
154
|
-
* `agents sessions <id
|
|
152
|
+
* Render a resolved session to stdout — the non-follow view behind
|
|
153
|
+
* `agents logs <sessionId>`. Defaults to the concise `summary` digest (same as
|
|
154
|
+
* `agents sessions <id>`); pass `'markdown'` for the full transcript
|
|
155
|
+
* (`agents logs <id> --full`). Reuses the shared `renderSession` renderer.
|
|
155
156
|
*/
|
|
156
|
-
export declare function renderSessionLog(session: SessionMeta): Promise<void>;
|
|
157
|
+
export declare function renderSessionLog(session: SessionMeta, mode?: ViewMode): Promise<void>;
|
|
157
158
|
/** Column-visibility flags for the picker row, computed once over the whole pool. */
|
|
158
159
|
export interface PickerColumns {
|
|
159
160
|
/** Render the machine column (only when the pool spans more than one machine). */
|
|
@@ -425,7 +425,14 @@ export function groupActiveSessions(sessions) {
|
|
|
425
425
|
* to the same id form); else the local machine. Never keys off `ActiveSession.host`
|
|
426
426
|
* — that is the terminal *app* (code/tmux), not the computer.
|
|
427
427
|
*/
|
|
428
|
+
/** Synthetic top-level group key for provider-sandboxed cloud tasks. */
|
|
429
|
+
const CLOUD_MACHINE_KEY = 'cloud';
|
|
428
430
|
function machineKeyFor(s, localMachine) {
|
|
431
|
+
// Cloud tasks run in a provider sandbox, not on the machine they're attributed
|
|
432
|
+
// to for reply routing (s.machine = the querier). Surface them as their own
|
|
433
|
+
// top-level "cloud" group instead of nested under the local device.
|
|
434
|
+
if (s.context === 'cloud')
|
|
435
|
+
return CLOUD_MACHINE_KEY;
|
|
429
436
|
if (s.machine)
|
|
430
437
|
return s.machine;
|
|
431
438
|
if (s.provenance?.host)
|
|
@@ -449,6 +456,11 @@ export function groupSessionsByMachine(sessions, localMachine) {
|
|
|
449
456
|
return -1;
|
|
450
457
|
if (b === localMachine)
|
|
451
458
|
return 1;
|
|
459
|
+
// The synthetic "cloud" category sorts after all real machines.
|
|
460
|
+
if (a === CLOUD_MACHINE_KEY)
|
|
461
|
+
return 1;
|
|
462
|
+
if (b === CLOUD_MACHINE_KEY)
|
|
463
|
+
return -1;
|
|
452
464
|
const ac = byMachine.get(a).length, bc = byMachine.get(b).length;
|
|
453
465
|
if (ac !== bc)
|
|
454
466
|
return bc - ac;
|
|
@@ -575,38 +587,48 @@ function groupTally(sessions) {
|
|
|
575
587
|
return parts.join(' · ');
|
|
576
588
|
}
|
|
577
589
|
/** Print one machine's workspace tree, indented under its machine header. */
|
|
578
|
-
function renderWorkspaceLayout(layout, base) {
|
|
590
|
+
function renderWorkspaceLayout(layout, base, machineKey) {
|
|
579
591
|
let first = true;
|
|
580
592
|
for (const ws of layout.workspaces) {
|
|
581
593
|
if (!first)
|
|
582
594
|
console.log();
|
|
583
595
|
first = false;
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
596
|
+
// Under the top-level "cloud" machine group the __cloud__ workspace header is
|
|
597
|
+
// redundant ("▸ cloud" then "cloud") — render its rows flat under the machine
|
|
598
|
+
// header instead. Row indent collapses by one level to match.
|
|
599
|
+
const redundantCloud = ws.key === '__cloud__' && machineKey === CLOUD_MACHINE_KEY;
|
|
600
|
+
const rowBase = redundantCloud ? base : base + ' ';
|
|
601
|
+
if (!redundantCloud) {
|
|
602
|
+
const header = ws.key === '__cloud__'
|
|
603
|
+
? chalk.magenta.bold('cloud')
|
|
604
|
+
: ws.key === '__unknown__'
|
|
605
|
+
? chalk.gray.bold('unknown')
|
|
606
|
+
: chalk.cyan.bold(shortCwd(ws.key));
|
|
607
|
+
const wsSessions = [...ws.windows.flatMap(w => w.sessions), ...ws.flat];
|
|
608
|
+
const tally = groupTally(wsSessions);
|
|
609
|
+
console.log(`${base}${header} ${chalk.gray(`(${ws.total})`)}${tally ? chalk.gray(` ${tally}`) : ''}`);
|
|
610
|
+
}
|
|
592
611
|
for (const win of ws.windows) {
|
|
593
612
|
// Host is per-process, but every terminal in the same IDE window shares
|
|
594
613
|
// an ancestor — take the first non-empty host as the window's label.
|
|
595
614
|
const host = win.sessions.find((s) => s.host)?.host ?? 'terminal';
|
|
596
615
|
const winHeader = `${chalk.gray(host)} ${chalk.gray('·')} ${chalk.gray(shortWindowLabel(win.windowId))} ${chalk.gray(`(${win.sessions.length})`)}`;
|
|
597
|
-
console.log(
|
|
616
|
+
console.log(rowBase + winHeader);
|
|
598
617
|
for (const s of win.sessions)
|
|
599
|
-
printActiveRow(s,
|
|
618
|
+
printActiveRow(s, rowBase + ' ');
|
|
600
619
|
}
|
|
601
620
|
for (const s of ws.flat)
|
|
602
|
-
printActiveRow(s,
|
|
621
|
+
printActiveRow(s, rowBase);
|
|
603
622
|
}
|
|
604
623
|
}
|
|
605
624
|
/** Machine header: `▸ <name> ← this machine` for the local box (cyan), matching
|
|
606
625
|
* the `ag devices list` treatment; a plain `▸ <name>` for remotes. */
|
|
607
626
|
function printMachineHeader(mg) {
|
|
608
|
-
|
|
609
|
-
|
|
627
|
+
// The synthetic "cloud" group isn't a device — tint it magenta (matching the
|
|
628
|
+
// cloud row/label styling) so it reads as a category, not a machine.
|
|
629
|
+
const isCloud = mg.machine === CLOUD_MACHINE_KEY;
|
|
630
|
+
const marker = mg.isLocal ? chalk.cyan('▸ ') : isCloud ? chalk.magenta('▸ ') : chalk.gray('▸ ');
|
|
631
|
+
const name = mg.isLocal ? chalk.bold.cyan(mg.machine) : isCloud ? chalk.bold.magenta(mg.machine) : chalk.bold(mg.machine);
|
|
610
632
|
const here = mg.isLocal ? chalk.cyan(' ← this machine') : '';
|
|
611
633
|
console.log(`${marker}${name} ${chalk.gray(`(${mg.total})`)}${here}`);
|
|
612
634
|
}
|
|
@@ -740,11 +762,16 @@ async function renderActiveSessions(asJson, waitingOnly = false, opts = {}) {
|
|
|
740
762
|
console.log();
|
|
741
763
|
firstMachine = false;
|
|
742
764
|
printMachineHeader(mg);
|
|
743
|
-
renderWorkspaceLayout(mg.layout, ' ');
|
|
765
|
+
renderWorkspaceLayout(mg.layout, ' ', mg.machine);
|
|
744
766
|
}
|
|
745
767
|
const parts = groupTally(sessions).split(' · ').filter(Boolean);
|
|
746
|
-
|
|
747
|
-
|
|
768
|
+
// The synthetic "cloud" group is a category, not a machine — exclude it from the
|
|
769
|
+
// machine count and note it separately so the tally stays truthful.
|
|
770
|
+
const realMachines = grouped.machines.filter((m) => m.machine !== CLOUD_MACHINE_KEY).length;
|
|
771
|
+
const hasCloud = grouped.machines.some((m) => m.machine === CLOUD_MACHINE_KEY);
|
|
772
|
+
const machineWord = realMachines === 1 ? 'machine' : 'machines';
|
|
773
|
+
const cloudNote = hasCloud ? ' + cloud' : '';
|
|
774
|
+
console.log(chalk.gray(`\n${sessions.length} active (${parts.join(', ')}) across ${realMachines} ${machineWord}${cloudNote}.`));
|
|
748
775
|
// Tip only when nothing else could be included and the user didn't opt out.
|
|
749
776
|
if (!opts.local && !opts.hosts?.length && remoteDeviceCount === 0)
|
|
750
777
|
printCrossMachineTip();
|
|
@@ -1258,12 +1285,13 @@ function resolveViewMode(options, filters) {
|
|
|
1258
1285
|
return 'summary';
|
|
1259
1286
|
}
|
|
1260
1287
|
/**
|
|
1261
|
-
* Render a session
|
|
1262
|
-
* `agents logs <sessionId>`.
|
|
1263
|
-
* `agents sessions <id
|
|
1288
|
+
* Render a resolved session to stdout — the non-follow view behind
|
|
1289
|
+
* `agents logs <sessionId>`. Defaults to the concise `summary` digest (same as
|
|
1290
|
+
* `agents sessions <id>`); pass `'markdown'` for the full transcript
|
|
1291
|
+
* (`agents logs <id> --full`). Reuses the shared `renderSession` renderer.
|
|
1264
1292
|
*/
|
|
1265
|
-
export async function renderSessionLog(session) {
|
|
1266
|
-
await renderSession(session,
|
|
1293
|
+
export async function renderSessionLog(session, mode = 'summary') {
|
|
1294
|
+
await renderSession(session, mode, {});
|
|
1267
1295
|
}
|
|
1268
1296
|
async function renderSession(session, mode, filters, options = {}) {
|
|
1269
1297
|
// OpenCode stores sessions in SQLite; filePath is "db_path#session_id"
|
package/dist/commands/teams.js
CHANGED
|
@@ -11,9 +11,10 @@ import { handleSpawn, handleStatus, handleStop, handleTasks, toTaskStatusSummary
|
|
|
11
11
|
import { createTeam, ensureTeam, getTeam, loadTeams, removeTeam, teamExists, } from '../lib/teams/registry.js';
|
|
12
12
|
import { setHelpSections } from '../lib/help.js';
|
|
13
13
|
import { createWorktree, isGitRepo, hasUncommittedChanges, removeWorktree, } from '../lib/teams/worktree.js';
|
|
14
|
-
import { isVersionInstalled, resolveVersionAlias, resolveVersionAliasLoose } from '../lib/versions.js';
|
|
14
|
+
import { isVersionInstalled, resolveVersion, resolveVersionAlias, resolveVersionAliasLoose, verifyInstalledBinaryLaunches } from '../lib/versions.js';
|
|
15
15
|
import { AGENTS, warnAgentDeprecated } from '../lib/agents.js';
|
|
16
16
|
import { discoverSessions, parseTimeFilter, resolveSessionById } from '../lib/session/discover.js';
|
|
17
|
+
import { renderSessionLog } from './sessions.js';
|
|
17
18
|
import { buildPreview as buildSessionPreview } from './sessions-picker.js';
|
|
18
19
|
import { parseExecEnv } from '../lib/exec.js';
|
|
19
20
|
import { teamPicker, printTeamTable } from './teams-picker.js';
|
|
@@ -1651,8 +1652,9 @@ export function registerTeamsCommands(program) {
|
|
|
1651
1652
|
teams
|
|
1652
1653
|
.command('logs [teammate]')
|
|
1653
1654
|
.alias('log')
|
|
1654
|
-
.description("
|
|
1655
|
-
.option('-n, --tail <n>', 'Show
|
|
1655
|
+
.description("Show a teammate's concise session summary. --full (or -n <lines>) for the raw stdout. Accepts positional name, --teammate <name>, UUID, or UUID prefix.")
|
|
1656
|
+
.option('-n, --tail <n>', 'Show the last N lines of raw stdout instead of the concise summary')
|
|
1657
|
+
.option('-m, --full', 'Show the full raw stdout log instead of the concise summary')
|
|
1656
1658
|
.option('--team <team>', 'Disambiguate when the same name appears in multiple teams')
|
|
1657
1659
|
.option('--teammate <name>', 'Teammate name (alias for the positional arg; useful for scripts)')
|
|
1658
1660
|
.action(async (ref, opts) => {
|
|
@@ -1682,14 +1684,28 @@ export function registerTeamsCommands(program) {
|
|
|
1682
1684
|
}
|
|
1683
1685
|
agentId = resolved.agentId;
|
|
1684
1686
|
}
|
|
1687
|
+
// Concise by default: a teammate's agentId IS its agent session id (passed
|
|
1688
|
+
// as --session-id at launch), so render the same summary digest as
|
|
1689
|
+
// `agents sessions <id>`. --full / -n <lines> opt into the raw stdout.log.
|
|
1690
|
+
if (!opts.full && !opts.tail) {
|
|
1691
|
+
const all = await discoverSessions({ all: true, limit: 5000 });
|
|
1692
|
+
const matches = resolveSessionById(all, agentId);
|
|
1693
|
+
if (matches.length > 0) {
|
|
1694
|
+
await renderSessionLog(matches[0], 'summary');
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
// No resolvable session (e.g. a non-Claude teammate) — fall through to a
|
|
1698
|
+
// bounded tail of raw stdout rather than dumping the whole file.
|
|
1699
|
+
}
|
|
1685
1700
|
const logPath = path.join(base, agentId, 'stdout.log');
|
|
1686
1701
|
try {
|
|
1687
1702
|
const content = await fs.readFile(logPath, 'utf-8');
|
|
1688
|
-
if (
|
|
1703
|
+
if (opts.full) {
|
|
1689
1704
|
process.stdout.write(content);
|
|
1690
1705
|
return;
|
|
1691
1706
|
}
|
|
1692
|
-
|
|
1707
|
+
// Default tail size keeps an un-resolvable teammate's glance bounded too.
|
|
1708
|
+
const n = opts.tail ? Math.max(1, parseInt(opts.tail, 10) || 50) : 40;
|
|
1693
1709
|
const lines = content.split('\n');
|
|
1694
1710
|
process.stdout.write(lines.slice(-n).join('\n'));
|
|
1695
1711
|
}
|
|
@@ -1705,6 +1721,28 @@ export function registerTeamsCommands(program) {
|
|
|
1705
1721
|
.option('--json', 'Output machine-readable JSON')
|
|
1706
1722
|
.action(async (opts) => {
|
|
1707
1723
|
const info = checkAllClis();
|
|
1724
|
+
// Deep integrity probe. `checkAllClis` reports presence (shim + stub guard),
|
|
1725
|
+
// but a GUTTED native binary (JS wrapper present, platform binary missing —
|
|
1726
|
+
// the codex/kimi optional-dep partial-extract failure) still passes that. So
|
|
1727
|
+
// actually launch the resolved default version and, if it won't run, flip the
|
|
1728
|
+
// agent to not-installed with a repair hint — otherwise doctor says "ready"
|
|
1729
|
+
// and the teammate ENOENTs at spawn. Parallel; win32 is treated as healthy by
|
|
1730
|
+
// verifyInstalledBinaryLaunches.
|
|
1731
|
+
await Promise.all(Object.entries(info).map(async ([name, entry]) => {
|
|
1732
|
+
if (!entry.installed)
|
|
1733
|
+
return;
|
|
1734
|
+
const agent = name;
|
|
1735
|
+
const version = resolveVersion(agent);
|
|
1736
|
+
if (!version)
|
|
1737
|
+
return;
|
|
1738
|
+
const health = await verifyInstalledBinaryLaunches(agent, version);
|
|
1739
|
+
if (!health.ok) {
|
|
1740
|
+
entry.installed = false;
|
|
1741
|
+
entry.path = null;
|
|
1742
|
+
entry.error = `${AGENTS[agent]?.cliCommand ?? name}@${version} is installed but its binary won't launch`
|
|
1743
|
+
+ `${health.detail ? ` (${health.detail})` : ''}. Repair: agents add ${agent}@${version}`;
|
|
1744
|
+
}
|
|
1745
|
+
}));
|
|
1708
1746
|
// Advisory enrichment only. Sign-in detection is UNRELIABLE, so it never
|
|
1709
1747
|
// changes the authoritative installed/ready column — it annotates. And an
|
|
1710
1748
|
// agent that is actually running in a team is treated as signed in
|