@phnx-labs/agents-cli 1.20.58 → 1.20.59
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 +7 -2
- package/dist/bin/agents +0 -0
- package/dist/commands/exec.js +1 -1
- package/dist/commands/output.d.ts +19 -0
- package/dist/commands/output.js +333 -0
- package/dist/commands/secrets.js +6 -6
- package/dist/index.js +2 -1
- package/dist/lib/agents.js +14 -10
- package/dist/lib/hosts/passthrough.js +1 -0
- package/dist/lib/mcp.js +1 -1
- package/dist/lib/output/git-output.d.ts +74 -0
- package/dist/lib/output/git-output.js +213 -0
- package/dist/lib/permissions.d.ts +12 -0
- package/dist/lib/permissions.js +73 -9
- package/dist/lib/project-root.js +2 -1
- package/dist/lib/resources/mcp.js +1 -1
- package/dist/lib/resources/permissions.js +3 -1
- package/dist/lib/resources/skills.js +6 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/secrets/remote.d.ts +7 -2
- package/dist/lib/secrets/remote.js +11 -10
- package/dist/lib/session/db.d.ts +3 -0
- package/dist/lib/session/db.js +20 -4
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +40 -4
- package/dist/lib/session/types.d.ts +2 -0
- package/dist/lib/staleness/detectors/permissions.js +22 -1
- package/dist/lib/staleness/detectors/subagents.js +11 -11
- package/dist/lib/staleness/writers/commands.js +3 -3
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/subagents.d.ts +1 -0
- package/dist/lib/subagents.js +15 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 1.20.59
|
|
6
|
+
|
|
7
|
+
- **Fix: remote secrets now choose the Windows PowerShell wrapper from the original `--host` name, not the resolved `user@ip` SSH target.** Inline enrolled Windows hosts resolve to address-based SSH targets, but the OS registry is keyed by the host name; `agents secrets view/list/exec --host <windows>`, `agents run --secrets bundle@<windows>`, and remote secrets unlock/export paths now pass that original name into command construction so Windows hosts no longer fall back to `bash -lc`. Source: `apps/cli/src/lib/secrets/remote.ts`, `apps/cli/src/commands/secrets.ts`, `apps/cli/src/commands/exec.ts`. (RUSH-1431)
|
|
8
|
+
- **Wire Droid skills support (RUSH-1397).** Droid loads skills from `.factory/skills/` (since 0.26.0). Flip `skills: { since: '0.26.0' }`, register generic skills writer/detector. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/__tests__/capabilities.test.ts`.
|
|
9
|
+
- **Wire skills support for Goose CLI (RUSH-1394).** Goose reads skills directly from `~/.agents/skills/` via the Summon extension (block-goose-cli >= 1.25.0). Flip `skills: { since: '1.25.0' }`, register generic skills writer/detector. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/staleness/writers/commands.ts`.
|
|
10
|
+
- **Wire Droid allowlist support (RUSH-1396).** Droid stores allow/deny in `.factory/settings.json` (`commandAllowlist`/`commandDenylist`). Flip `allowlist: true` since 0.57.5, add `convertToDroidFormat`, wire writer/detector. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/permissions.ts`.
|
|
11
|
+
- **Wire Codex permissions/allowlist support (RUSH-1566).** Codex stores allow/deny in `.codex/config.toml` (`approval_policy`, `sandbox_mode`). Flip `allowlist: true` since 0.128.0, add `convertToCodexFormat`, wire writer/detector. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/permissions.ts`.
|
|
12
|
+
- **OpenCode permissions write to the loaded config path (RUSH-1623).** Global config is `~/.config/opencode/opencode.jsonc` (not `~/.opencode/`); project config is `opencode.jsonc` at the project root. Source: `apps/cli/src/lib/permissions.ts`, `apps/cli/src/lib/agents.ts`.
|
|
13
|
+
- **Catch-up publishes now require the exact CI-tested release tree and its full green matrix.** A package version already present on `main` no longer counts as release validation by itself: `release.sh` resolves the merged `release/v<version>` PR, requires its merge commit to be current `main`, fetches the PR head that ran CI, requires that head tree to equal current `main`, and rechecks every expected CI context before publishing. This closes the path that let 1.20.58 reach npm before its tag-triggered Windows matrix exposed failures. Source: `apps/cli/scripts/release.sh`.
|
|
14
|
+
- **Windows release validation now matches portable path behavior.** Home-relative project paths normalize native separators to `/` before they are stored as `~/…`, and the systemd-manifest and remote-shell assertions now compare the escaped/quoted forms that the runtime deliberately emits. This restores the Windows Node 22/24 release matrix without weakening command escaping. Source: `apps/cli/src/lib/project-root.ts`, `apps/cli/src/lib/{project-root,daemon}.test.ts`, `apps/cli/src/lib/hosts/dispatch.test.ts`.
|
|
5
15
|
## 1.20.58
|
|
6
16
|
|
|
7
17
|
- **Self-updating agent CLIs are represented as one live installation.** `agents view` no longer invents version-home rows for single-binary installers such as Droid, Grok, Cursor, Kiro, Goose, and Hermes; it reports the version returned by the installed binary and folds away stale per-version directories. `agents add <agent>@<version>` now installs or keeps that agent's current release instead of rejecting an unsupported pinned install. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/versions.ts`, `apps/cli/src/commands/{versions,view}.ts`. (RUSH-1321)
|
package/README.md
CHANGED
|
@@ -855,9 +855,10 @@ Which DotAgents resources each agent CLI can load. Source of truth: [src/lib/age
|
|
|
855
855
|
| OpenCode | yes | no | yes | no | yes | yes | no | no | `AGENTS.md` | no |
|
|
856
856
|
| Copilot | yes | no | yes | no | yes | yes | no | no | `AGENTS.md` | no |
|
|
857
857
|
| Amp | yes | no | yes | no | yes | yes | no | no | `AGENTS.md` | no |
|
|
858
|
-
| Kiro | yes | no | yes | >= 2.8.0 | yes | yes | no |
|
|
858
|
+
| Kiro | yes | no | yes | >= 2.8.0 | yes | yes | no | >= 1.23.0 | `AGENTS.md` | no |
|
|
859
859
|
| Goose | yes | no | yes | no | no | no | no | no | `AGENTS.md` | no |
|
|
860
860
|
| Roo Code | yes | no | yes | no | yes | yes | no | no | `AGENTS.md` | no |
|
|
861
|
+
| Droid | yes | yes | yes | >= 0.57.5 | >= 0.26.0 | yes | yes | yes | `AGENTS.md` | no |
|
|
861
862
|
|
|
862
863
|
**Legend:** `yes` / `no` = synced or skipped at install time. `skills ($name)` = no file-based slash-command dir; behavior ships as a generated skill invoked with `$command`. `gateway` = OpenClaw resolves slash commands at runtime, not from synced files. Version suffixes are enforced at sync time — out-of-range versions are skipped with a clear message.
|
|
863
864
|
|
|
@@ -875,13 +876,17 @@ Which DotAgents resources each agent CLI can load. Source of truth: [src/lib/age
|
|
|
875
876
|
| Grok Build | -- | yes | yes |
|
|
876
877
|
| Antigravity | -- | yes | -- |
|
|
877
878
|
| Copilot | -- | -- | yes |
|
|
878
|
-
| OpenClaw, Amp,
|
|
879
|
+
| OpenClaw, Amp, Goose, Roo | -- | -- | -- |
|
|
880
|
+
| Kiro | -- | -- | -- |
|
|
879
881
|
|
|
880
882
|
### Version-gated sync
|
|
881
883
|
|
|
882
884
|
| Capability | Agent | Gate |
|
|
883
885
|
|------------|-------|------|
|
|
884
886
|
| Hooks | Codex | >= 0.116.0 |
|
|
887
|
+
| Subagents | Kiro | >= 1.23.0 |
|
|
888
|
+
| Skills | Droid | >= 0.26.0 |
|
|
889
|
+
| Permissions | Droid | >= 0.57.5 |
|
|
885
890
|
| Hooks | Gemini | >= 0.26.0 |
|
|
886
891
|
| Permissions | Kiro | >= 2.8.0 |
|
|
887
892
|
| File-based commands | Codex | < 0.117.0 (0.117+ uses command-as-skill) |
|
package/dist/bin/agents
CHANGED
|
Binary file
|
package/dist/commands/exec.js
CHANGED
|
@@ -1227,7 +1227,7 @@ export function registerRunCommand(program) {
|
|
|
1227
1227
|
// Remote bundle (`bundle@host`): resolve over SSH and inject
|
|
1228
1228
|
// ephemerally — values never touch this machine's keychain or disk.
|
|
1229
1229
|
const target = await resolveSshTarget(host);
|
|
1230
|
-
const bundleEnv = await remoteResolveEnv(target, bundleName);
|
|
1230
|
+
const bundleEnv = await remoteResolveEnv(target, bundleName, { osLookupName: host });
|
|
1231
1231
|
console.log(chalk.gray(`[secrets] Resolved ${bundleName}@${host}: ${Object.keys(bundleEnv).length} keys (remote, ephemeral)`));
|
|
1232
1232
|
secretsEnv = { ...secretsEnv, ...bundleEnv };
|
|
1233
1233
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output command — productivity: token *burn* vs shipped *output*.
|
|
3
|
+
*
|
|
4
|
+
* `agents cost` answers "what did we burn?" (dollars + duration). This joins that
|
|
5
|
+
* burn to what actually shipped — real generated (output) tokens plus PRs and
|
|
6
|
+
* commits across every git identity — so you can see burn-vs-output and ratios
|
|
7
|
+
* like $/PR and output-tokens/$. Pure SQLite + local git/gh, no server, no
|
|
8
|
+
* telemetry — the same offline spirit as `cost`.
|
|
9
|
+
*
|
|
10
|
+
* Why not just show `token_count`? Because that number sums cache-read/-write
|
|
11
|
+
* context re-counted every turn and is dominated by cheap re-reads (often ~100x
|
|
12
|
+
* the real generation). `output_tokens` (scanned per-agent into the session DB)
|
|
13
|
+
* is the honest "work produced" signal, and it is what this command leads with.
|
|
14
|
+
*
|
|
15
|
+
* `--all-hosts` fans the same rollup across every online device (`ag devices`)
|
|
16
|
+
* over SSH and merges — one fleet-wide burn-vs-output view.
|
|
17
|
+
*/
|
|
18
|
+
import type { Command } from 'commander';
|
|
19
|
+
export declare function registerOutputCommand(program: Command): void;
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { execFile } from 'child_process';
|
|
5
|
+
import { promisify } from 'util';
|
|
6
|
+
import { addHostOption } from '../lib/hosts/option.js';
|
|
7
|
+
import { discoverSessions, parseTimeFilter } from '../lib/session/discover.js';
|
|
8
|
+
import { queryUsageRollup } from '../lib/session/db.js';
|
|
9
|
+
import { formatUsd, PRICING_VERSION } from '../lib/pricing/index.js';
|
|
10
|
+
import { formatDuration } from '../lib/session/render.js';
|
|
11
|
+
import { terminalWidth, truncateToWidth, padToWidth } from '../lib/session/width.js';
|
|
12
|
+
import { collectGitOutput } from '../lib/output/git-output.js';
|
|
13
|
+
import { loadDevices } from '../lib/devices/registry.js';
|
|
14
|
+
import { machineId } from '../lib/session/sync/config.js';
|
|
15
|
+
const execFileAsync = promisify(execFile);
|
|
16
|
+
export function registerOutputCommand(program) {
|
|
17
|
+
addHostOption(program.command('output'))
|
|
18
|
+
.description('Productivity rollup — token burn vs shipped output (PRs, commits) across agents')
|
|
19
|
+
.option('--json', 'Output the rollup as JSON')
|
|
20
|
+
.option('--since <time>', 'Only sessions/commits newer than this: 1h, 24h, 7d, 4w, 1mo, 1y, or ISO date (default 7d)')
|
|
21
|
+
.option('--by <dimension>', 'Group the burn/output breakdown by: agent (default), project, or day')
|
|
22
|
+
.option('--repos-dir <dir>', 'Root scanned for git repos (default ~/src)')
|
|
23
|
+
.option('--author <email...>', 'Count commits by these author emails (default: your git identities)')
|
|
24
|
+
.option('--login <login...>', 'Count PRs for these GitHub logins (default: current gh user)')
|
|
25
|
+
.option('--no-prs', 'Skip the GitHub PR lookup (commits only)')
|
|
26
|
+
.option('--all-hosts', 'Aggregate across every online device (ag devices) over SSH')
|
|
27
|
+
.addHelpText('after', `
|
|
28
|
+
Examples:
|
|
29
|
+
agents output Last 7 days: burn, output tokens, PRs, commits, ratios
|
|
30
|
+
agents output --since 24h Last 24 hours
|
|
31
|
+
agents output --since 1mo Last month (units: 1h 24h 7d 4w 1mo 1y, or ISO date)
|
|
32
|
+
agents output --all-hosts Fleet-wide, folding in every online machine
|
|
33
|
+
agents output --by day --json Machine-readable daily burn/output rollup
|
|
34
|
+
|
|
35
|
+
Burn (cost) is computed offline from a versioned per-model price table (${PRICING_VERSION}).
|
|
36
|
+
Output tokens are the real generated tokens — NOT the cache-inflated total token count.
|
|
37
|
+
`)
|
|
38
|
+
.action(async (options) => {
|
|
39
|
+
await outputAction(options);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function resolveGroup(by) {
|
|
43
|
+
if (by === undefined)
|
|
44
|
+
return 'agent';
|
|
45
|
+
if (by === 'agent' || by === 'project' || by === 'day')
|
|
46
|
+
return by;
|
|
47
|
+
console.error(chalk.red('error: --by must be one of: agent, project, day'));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
/** Compact token formatter: 38.6M, 4.1K, 10.6B. */
|
|
51
|
+
function formatCompact(n) {
|
|
52
|
+
const abs = Math.abs(n);
|
|
53
|
+
if (abs >= 1e9)
|
|
54
|
+
return `${(n / 1e9).toFixed(1)}B`;
|
|
55
|
+
if (abs >= 1e6)
|
|
56
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
57
|
+
if (abs >= 1e3)
|
|
58
|
+
return `${(n / 1e3).toFixed(1)}K`;
|
|
59
|
+
return String(n);
|
|
60
|
+
}
|
|
61
|
+
function emptyBurn() {
|
|
62
|
+
return { costUsd: 0, outputTokens: 0, tokenCount: 0, sessionCount: 0, durationMs: 0 };
|
|
63
|
+
}
|
|
64
|
+
function sumBurn(rows) {
|
|
65
|
+
return rows.reduce((acc, r) => {
|
|
66
|
+
acc.costUsd += r.costUsd;
|
|
67
|
+
acc.outputTokens += r.outputTokens;
|
|
68
|
+
acc.tokenCount += r.tokenCount;
|
|
69
|
+
acc.sessionCount += r.sessionCount;
|
|
70
|
+
acc.durationMs += r.durationMs;
|
|
71
|
+
return acc;
|
|
72
|
+
}, emptyBurn());
|
|
73
|
+
}
|
|
74
|
+
/** Compute this machine's payload from the local session DB + git. */
|
|
75
|
+
async function computeLocalPayload(options, includePrs) {
|
|
76
|
+
const since = options.since ?? '7d';
|
|
77
|
+
const sinceMs = parseTimeFilter(since);
|
|
78
|
+
// Ensure the index is fresh (and migrated to v12 so output_tokens is populated).
|
|
79
|
+
await discoverSessions({ all: true, since, limit: 1 });
|
|
80
|
+
const filter = { sinceMs };
|
|
81
|
+
const groupBy = resolveGroup(options.by);
|
|
82
|
+
const breakdown = queryUsageRollup({ ...filter, groupBy });
|
|
83
|
+
const burn = sumBurn(breakdown);
|
|
84
|
+
const reposDir = options.reposDir ?? path.join(os.homedir(), 'src');
|
|
85
|
+
const git = await collectGitOutput({
|
|
86
|
+
reposDir,
|
|
87
|
+
sinceMs,
|
|
88
|
+
authors: options.author,
|
|
89
|
+
logins: options.login,
|
|
90
|
+
includePrs,
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
machine: machineId(),
|
|
94
|
+
pricingVersion: PRICING_VERSION,
|
|
95
|
+
since,
|
|
96
|
+
burn,
|
|
97
|
+
output: {
|
|
98
|
+
commits: git.commits,
|
|
99
|
+
commitShas: git.commitShas,
|
|
100
|
+
prsOpened: git.prsOpened,
|
|
101
|
+
prsMerged: git.prsMerged,
|
|
102
|
+
reposScanned: git.reposScanned,
|
|
103
|
+
ghAvailable: git.ghAvailable,
|
|
104
|
+
authors: git.authors,
|
|
105
|
+
logins: git.logins,
|
|
106
|
+
},
|
|
107
|
+
breakdown: { by: groupBy, rows: breakdown },
|
|
108
|
+
uncostedAgents: groupBy === 'agent' ? breakdown.filter(r => r.costUsd === 0).map(r => r.key) : [],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** Fetch one remote device's payload by re-invoking `agents output --json --host <name>`. */
|
|
112
|
+
async function fetchRemotePayload(device, options) {
|
|
113
|
+
const args = ['output', '--json', '--no-prs', '--host', device, '--since', options.since ?? '7d'];
|
|
114
|
+
if (options.by)
|
|
115
|
+
args.push('--by', options.by);
|
|
116
|
+
if (options.reposDir)
|
|
117
|
+
args.push('--repos-dir', options.reposDir);
|
|
118
|
+
for (const a of options.author ?? [])
|
|
119
|
+
args.push('--author', a);
|
|
120
|
+
try {
|
|
121
|
+
const { stdout } = await execFileAsync('agents', args, {
|
|
122
|
+
timeout: 120_000,
|
|
123
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
124
|
+
});
|
|
125
|
+
const parsed = JSON.parse(stdout);
|
|
126
|
+
parsed.machine = parsed.machine || device;
|
|
127
|
+
return parsed;
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
return {
|
|
131
|
+
machine: device,
|
|
132
|
+
pricingVersion: PRICING_VERSION,
|
|
133
|
+
since: options.since ?? '7d',
|
|
134
|
+
burn: emptyBurn(),
|
|
135
|
+
output: { commits: 0, commitShas: [], prsOpened: 0, prsMerged: 0, reposScanned: 0, ghAvailable: false, authors: [], logins: [] },
|
|
136
|
+
breakdown: { by: resolveGroup(options.by), rows: [] },
|
|
137
|
+
uncostedAgents: [],
|
|
138
|
+
error: (err?.stderr || err?.message || 'unreachable').toString().split('\n')[0].slice(0, 120),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function outputAction(options) {
|
|
143
|
+
const includePrs = options.prs !== false;
|
|
144
|
+
if (!options.allHosts) {
|
|
145
|
+
const payload = await computeLocalPayload(options, includePrs);
|
|
146
|
+
if (options.json) {
|
|
147
|
+
process.stdout.write(JSON.stringify(withRatios(payload, [payload]), null, 2) + '\n');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
renderSingle(payload);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Fleet: local payload + every online device, folded in over SSH.
|
|
154
|
+
const self = machineId();
|
|
155
|
+
const registry = await loadDevices();
|
|
156
|
+
const remotes = Object.values(registry)
|
|
157
|
+
.filter(d => d.tailscale?.online && d.name !== self)
|
|
158
|
+
.map(d => d.name);
|
|
159
|
+
if (!options.json)
|
|
160
|
+
console.error(chalk.gray(`Folding in ${remotes.length} online device${remotes.length !== 1 ? 's' : ''}…`));
|
|
161
|
+
// PRs are global (gh search by author, not machine-bound) — compute once, locally.
|
|
162
|
+
const local = await computeLocalPayload(options, includePrs);
|
|
163
|
+
const remotePayloads = await Promise.all(remotes.map(d => fetchRemotePayload(d, options)));
|
|
164
|
+
const machines = [local, ...remotePayloads];
|
|
165
|
+
if (options.json) {
|
|
166
|
+
process.stdout.write(JSON.stringify(withRatios(mergeMachines(machines, options), machines), null, 2) + '\n');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
renderFleet(machines, options);
|
|
170
|
+
}
|
|
171
|
+
/** Merge per-machine payloads into a combined one (burn + commits summed; PRs local-only). */
|
|
172
|
+
function mergeMachines(machines, options) {
|
|
173
|
+
const burn = emptyBurn();
|
|
174
|
+
const byKey = new Map();
|
|
175
|
+
// Union commit SHAs across machines: shared repos (cloned on several boxes)
|
|
176
|
+
// expose the same commits to `git log` on each, so summing counts would
|
|
177
|
+
// multi-count. A commit's SHA is its global identity — union, don't add.
|
|
178
|
+
const allShas = new Set();
|
|
179
|
+
const uncosted = new Set();
|
|
180
|
+
for (const m of machines) {
|
|
181
|
+
burn.costUsd += m.burn.costUsd;
|
|
182
|
+
burn.outputTokens += m.burn.outputTokens;
|
|
183
|
+
burn.tokenCount += m.burn.tokenCount;
|
|
184
|
+
burn.sessionCount += m.burn.sessionCount;
|
|
185
|
+
burn.durationMs += m.burn.durationMs;
|
|
186
|
+
for (const s of m.output.commitShas)
|
|
187
|
+
allShas.add(s);
|
|
188
|
+
for (const a of m.uncostedAgents)
|
|
189
|
+
uncosted.add(a);
|
|
190
|
+
for (const r of m.breakdown.rows) {
|
|
191
|
+
const cur = byKey.get(r.key) ?? { key: r.key, costUsd: 0, durationMs: 0, sessionCount: 0, tokenCount: 0, outputTokens: 0 };
|
|
192
|
+
cur.costUsd += r.costUsd;
|
|
193
|
+
cur.durationMs += r.durationMs;
|
|
194
|
+
cur.sessionCount += r.sessionCount;
|
|
195
|
+
cur.tokenCount += r.tokenCount;
|
|
196
|
+
cur.outputTokens += r.outputTokens;
|
|
197
|
+
byKey.set(r.key, cur);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const rows = [...byKey.values()].sort((a, b) => b.costUsd - a.costUsd);
|
|
201
|
+
// PRs from the local machine only (machines[0]) — gh search is not machine-scoped.
|
|
202
|
+
const localGit = machines[0].output;
|
|
203
|
+
return {
|
|
204
|
+
machine: 'fleet',
|
|
205
|
+
pricingVersion: PRICING_VERSION,
|
|
206
|
+
since: options.since ?? '7d',
|
|
207
|
+
burn,
|
|
208
|
+
output: { ...localGit, commits: allShas.size, commitShas: [...allShas] },
|
|
209
|
+
breakdown: { by: resolveGroup(options.by), rows },
|
|
210
|
+
uncostedAgents: [...uncosted],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/** Attach burn-vs-output ratios to a payload for JSON output. */
|
|
214
|
+
function withRatios(payload, machines) {
|
|
215
|
+
const prsTotal = payload.output.prsOpened + payload.output.prsMerged;
|
|
216
|
+
return {
|
|
217
|
+
...payload,
|
|
218
|
+
ratios: {
|
|
219
|
+
costPerPr: prsTotal > 0 ? payload.burn.costUsd / prsTotal : null,
|
|
220
|
+
costPerCommit: payload.output.commits > 0 ? payload.burn.costUsd / payload.output.commits : null,
|
|
221
|
+
outputTokensPerUsd: payload.burn.costUsd > 0 ? payload.burn.outputTokens / payload.burn.costUsd : null,
|
|
222
|
+
},
|
|
223
|
+
machines: machines.length > 1 ? machines.map(m => ({ machine: m.machine, burn: m.burn, commits: m.output.commits, error: m.error })) : undefined,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/** Shared header line: burned · output tokens · PRs · commits, plus ratios. */
|
|
227
|
+
function headerLines(payload) {
|
|
228
|
+
const prsTotal = payload.output.prsOpened + payload.output.prsMerged;
|
|
229
|
+
const out = [];
|
|
230
|
+
out.push(' ' +
|
|
231
|
+
`${chalk.green(formatUsd(payload.burn.costUsd))} burned` +
|
|
232
|
+
chalk.gray(' · ') +
|
|
233
|
+
`${chalk.cyan(formatCompact(payload.burn.outputTokens))} output tokens` +
|
|
234
|
+
chalk.gray(' · ') +
|
|
235
|
+
`${chalk.yellow(String(prsTotal))} PRs ${chalk.gray(`(${payload.output.prsMerged} merged)`)}` +
|
|
236
|
+
chalk.gray(' · ') +
|
|
237
|
+
`${chalk.yellow(String(payload.output.commits))} commits`);
|
|
238
|
+
const ratios = [];
|
|
239
|
+
if (prsTotal > 0)
|
|
240
|
+
ratios.push(`${formatUsd(payload.burn.costUsd / prsTotal)}/PR`);
|
|
241
|
+
if (payload.output.commits > 0)
|
|
242
|
+
ratios.push(`${formatUsd(payload.burn.costUsd / payload.output.commits)}/commit`);
|
|
243
|
+
if (payload.burn.costUsd > 0)
|
|
244
|
+
ratios.push(`${formatCompact(Math.round(payload.burn.outputTokens / payload.burn.costUsd))} out-tok/$`);
|
|
245
|
+
if (ratios.length > 0)
|
|
246
|
+
out.push(chalk.gray(' ' + ratios.join(' · ')));
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
/** Render the per-group burn/output table. */
|
|
250
|
+
function renderBreakdown(rows, groupBy) {
|
|
251
|
+
const out = [chalk.bold(`By ${groupBy}`)];
|
|
252
|
+
if (rows.length === 0)
|
|
253
|
+
return out;
|
|
254
|
+
const cols = terminalWidth();
|
|
255
|
+
const burnW = Math.max(...rows.map(r => formatUsd(r.costUsd).length), 4);
|
|
256
|
+
const outW = Math.max(...rows.map(r => formatCompact(r.outputTokens).length), 6);
|
|
257
|
+
const sessW = Math.max(...rows.map(r => String(r.sessionCount).length), 3);
|
|
258
|
+
const fixedW = 2 + 2 + burnW + 2 + outW + 2 + sessW + 8;
|
|
259
|
+
const keyW = Math.max(8, Math.min(Math.max(...rows.map(r => r.key.length), groupBy.length), cols - fixedW));
|
|
260
|
+
out.push(' ' + chalk.gray(padToWidth('', keyW)) + ' ' + chalk.gray(padToWidth('burn', burnW)) + ' ' + chalk.gray(padToWidth('output', outW)) + ' ' + chalk.gray('sessions'));
|
|
261
|
+
for (const r of rows) {
|
|
262
|
+
out.push(' ' +
|
|
263
|
+
padToWidth(truncateToWidth(r.key, keyW), keyW) +
|
|
264
|
+
' ' +
|
|
265
|
+
chalk.green(padToWidth(formatUsd(r.costUsd), burnW)) +
|
|
266
|
+
' ' +
|
|
267
|
+
chalk.cyan(padToWidth(formatCompact(r.outputTokens), outW)) +
|
|
268
|
+
' ' +
|
|
269
|
+
chalk.gray(padToWidth(String(r.sessionCount), sessW)));
|
|
270
|
+
}
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
function renderSingle(payload) {
|
|
274
|
+
const out = [];
|
|
275
|
+
out.push(chalk.bold('Output') + chalk.gray(` · pricing ${payload.pricingVersion} · since ${payload.since}`));
|
|
276
|
+
out.push(...headerLines(payload));
|
|
277
|
+
out.push('');
|
|
278
|
+
if (payload.burn.sessionCount === 0) {
|
|
279
|
+
out.push(chalk.gray('No sessions with cost data found. Run `agents sessions --all` to index, then retry.'));
|
|
280
|
+
console.log(out.join('\n'));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
out.push(...renderBreakdown(payload.breakdown.rows, payload.breakdown.by));
|
|
284
|
+
out.push('');
|
|
285
|
+
out.push(chalk.bold('Shipped'));
|
|
286
|
+
out.push(` ${chalk.yellow(String(payload.output.commits))} commits across ${payload.output.reposScanned} repos` + chalk.gray(` (authors: ${payload.output.authors.length > 0 ? payload.output.authors.join(', ') : 'none detected'})`));
|
|
287
|
+
if (payload.output.ghAvailable) {
|
|
288
|
+
out.push(` ${chalk.yellow(String(payload.output.prsOpened))} PRs opened, ${chalk.yellow(String(payload.output.prsMerged))} merged` + chalk.gray(` (logins: ${payload.output.logins.join(', ')})`));
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
out.push(chalk.gray(' PRs: gh unavailable or unauthed — not counted'));
|
|
292
|
+
}
|
|
293
|
+
out.push('');
|
|
294
|
+
out.push(chalk.gray(notCountedLine(payload.uncostedAgents)));
|
|
295
|
+
if (payload.burn.durationMs > 0)
|
|
296
|
+
out.push(chalk.gray(`agent wall-clock: ${formatDuration(payload.burn.durationMs)}`));
|
|
297
|
+
console.log(out.join('\n'));
|
|
298
|
+
}
|
|
299
|
+
function renderFleet(machines, options) {
|
|
300
|
+
const merged = mergeMachines(machines, options);
|
|
301
|
+
const out = [];
|
|
302
|
+
out.push(chalk.bold('Output') + chalk.gray(` · fleet (${machines.length} machines) · pricing ${merged.pricingVersion} · since ${merged.since}`));
|
|
303
|
+
out.push(...headerLines(merged));
|
|
304
|
+
out.push('');
|
|
305
|
+
// By machine.
|
|
306
|
+
out.push(chalk.bold('By machine'));
|
|
307
|
+
const nameW = Math.max(...machines.map(m => m.machine.length), 7);
|
|
308
|
+
const burnW = Math.max(...machines.map(m => formatUsd(m.burn.costUsd).length), 4);
|
|
309
|
+
for (const m of machines) {
|
|
310
|
+
const note = m.error ? chalk.red(` (${m.error})`) : '';
|
|
311
|
+
out.push(' ' +
|
|
312
|
+
padToWidth(m.machine, nameW) +
|
|
313
|
+
' ' +
|
|
314
|
+
chalk.green(padToWidth(formatUsd(m.burn.costUsd), burnW)) +
|
|
315
|
+
' ' +
|
|
316
|
+
chalk.cyan(padToWidth(formatCompact(m.burn.outputTokens), 7)) +
|
|
317
|
+
' ' +
|
|
318
|
+
chalk.gray(`${m.burn.sessionCount} sessions, ${m.output.commits} commits`) +
|
|
319
|
+
note);
|
|
320
|
+
}
|
|
321
|
+
out.push('');
|
|
322
|
+
out.push(...renderBreakdown(merged.breakdown.rows, merged.breakdown.by));
|
|
323
|
+
out.push('');
|
|
324
|
+
out.push(chalk.gray(notCountedLine(merged.uncostedAgents)));
|
|
325
|
+
console.log(out.join('\n'));
|
|
326
|
+
}
|
|
327
|
+
function notCountedLine(uncosted) {
|
|
328
|
+
const notes = [];
|
|
329
|
+
if (uncosted.length > 0)
|
|
330
|
+
notes.push(`no price table for: ${[...new Set(uncosted)].join(', ')} (burn undercounts)`);
|
|
331
|
+
notes.push('cloud runs (Rush/Codex/Factory) not counted');
|
|
332
|
+
return 'not counted: ' + notes.join(' · ');
|
|
333
|
+
}
|
package/dist/commands/secrets.js
CHANGED
|
@@ -328,12 +328,12 @@ async function browseRemote(targets, args, tty) {
|
|
|
328
328
|
if (tty) {
|
|
329
329
|
for (const t of targets) {
|
|
330
330
|
const target = await resolveSshTarget(t);
|
|
331
|
-
render(t, remoteSecretsRaw(target, args, { tty: true }));
|
|
331
|
+
render(t, remoteSecretsRaw(target, args, { tty: true, osLookupName: t }));
|
|
332
332
|
}
|
|
333
333
|
}
|
|
334
334
|
else {
|
|
335
|
-
const resolved = await Promise.all(targets.map((t) => resolveSshTarget(t)));
|
|
336
|
-
const results = resolved.map((target) => remoteSecretsRaw(target, args));
|
|
335
|
+
const resolved = await Promise.all(targets.map(async (t) => ({ name: t, target: await resolveSshTarget(t) })));
|
|
336
|
+
const results = resolved.map(({ name, target }) => remoteSecretsRaw(target, args, { osLookupName: name }));
|
|
337
337
|
targets.forEach((t, i) => render(t, results[i]));
|
|
338
338
|
}
|
|
339
339
|
if (failures > 0)
|
|
@@ -1476,7 +1476,7 @@ Examples:
|
|
|
1476
1476
|
// engine (BatchMode, ConnectTimeout, keepalive, control-socket
|
|
1477
1477
|
// reuse) via the same path the READ inverse (`remoteResolveEnv`)
|
|
1478
1478
|
// uses. `--from -` reads the .env off ssh stdin.
|
|
1479
|
-
res = remoteSecretsRaw(host, ['import', resolvedBundleName, '--from', '-', ...(opts.force ? ['--force'] : [])], { input: dotenv });
|
|
1479
|
+
res = remoteSecretsRaw(host, ['import', resolvedBundleName, '--from', '-', ...(opts.force ? ['--force'] : [])], { input: dotenv, osLookupName: host });
|
|
1480
1480
|
}
|
|
1481
1481
|
if (res.code === null) {
|
|
1482
1482
|
failures++;
|
|
@@ -1584,7 +1584,7 @@ Examples:
|
|
|
1584
1584
|
// locally instead.
|
|
1585
1585
|
const { assertRemoteBundleFlagsUnsupported } = await import('../lib/secrets/bundles.js');
|
|
1586
1586
|
assertRemoteBundleFlagsUnsupported(bundleName, execOpts.host, { keys: keysSubset, allowExpired: execOpts.allowExpired }, { keysFlag: '--keys', allowExpiredFlag: '--allow-expired' });
|
|
1587
|
-
secretEnv = await remoteResolveEnv(await resolveSshTarget(execOpts.host), bundleName);
|
|
1587
|
+
secretEnv = await remoteResolveEnv(await resolveSshTarget(execOpts.host), bundleName, { osLookupName: execOpts.host });
|
|
1588
1588
|
}
|
|
1589
1589
|
else {
|
|
1590
1590
|
const { readAndResolveBundleEnv } = await import('../lib/secrets/bundles.js');
|
|
@@ -1738,7 +1738,7 @@ Examples:
|
|
|
1738
1738
|
// sees a real TTY, which requires our local terminal to pass straight
|
|
1739
1739
|
// through. The remote's prompt + output stream to this terminal; we get
|
|
1740
1740
|
// back only the exit code.
|
|
1741
|
-
const code = remoteSecretsStream(target, unlockArgs);
|
|
1741
|
+
const code = remoteSecretsStream(target, unlockArgs, { osLookupName: h });
|
|
1742
1742
|
if (code === 0) {
|
|
1743
1743
|
console.log(chalk.green(`${h}: unlocked`));
|
|
1744
1744
|
}
|
package/dist/index.js
CHANGED
|
@@ -50,7 +50,7 @@ if (IS_DEV_BUILD) {
|
|
|
50
50
|
// module on each invocation (which loaded the whole ~50-module tree before the
|
|
51
51
|
// first byte of output), the registry maps a command name to a thunk that
|
|
52
52
|
// imports only what that command needs. See src/lib/startup/command-registry.ts.
|
|
53
|
-
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadFeed, } from './lib/startup/command-registry.js';
|
|
53
|
+
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadOutput, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadFeed, } from './lib/startup/command-registry.js';
|
|
54
54
|
import { applyGlobalHelpConventions } from './lib/help.js';
|
|
55
55
|
import { renderWhatsNew } from './lib/whats-new.js';
|
|
56
56
|
import { emit, redactArgs } from './lib/events.js';
|
|
@@ -690,6 +690,7 @@ async function registerAllEagerCommands() {
|
|
|
690
690
|
await reg(loadFactory);
|
|
691
691
|
await reg(loadUsage);
|
|
692
692
|
await reg(loadCost);
|
|
693
|
+
await reg(loadOutput);
|
|
693
694
|
await reg(loadBudget);
|
|
694
695
|
await reg(loadAlias);
|
|
695
696
|
await reg(loadPty);
|
package/dist/lib/agents.js
CHANGED
|
@@ -236,7 +236,7 @@ export const AGENTS = {
|
|
|
236
236
|
cloudProvider: 'codex',
|
|
237
237
|
// Subagents: multi-agent plumbing since 0.117.0; custom agents as
|
|
238
238
|
// ~/.codex/agents/*.toml (name, description, developer_instructions).
|
|
239
|
-
capabilities: { hooks: { since: '0.116.0' }, mcp: true, mcpHttp: true, mcpHeaders: false, allowlist:
|
|
239
|
+
capabilities: { hooks: { since: '0.116.0' }, mcp: true, mcpHttp: true, mcpHeaders: false, allowlist: { since: '0.138.0' }, skills: true, commands: { until: '0.117.0' }, plugins: { since: '0.128.0' }, subagents: { since: '0.117.0' }, rules: { file: 'AGENTS.md' }, workflows: false, memory: true, modes: ['plan', 'edit', 'skip'] },
|
|
240
240
|
},
|
|
241
241
|
gemini: {
|
|
242
242
|
id: 'gemini',
|
|
@@ -405,7 +405,10 @@ export const AGENTS = {
|
|
|
405
405
|
configDir: path.join(HOME, '.config', 'goose'),
|
|
406
406
|
commandsDir: path.join(HOME, '.config', 'goose', 'commands'),
|
|
407
407
|
commandsSubdir: 'commands',
|
|
408
|
-
|
|
408
|
+
// Goose reads skills directly from central ~/.agents/skills/ via the Summon
|
|
409
|
+
// extension (block-goose-cli ≥ 1.25.0). No per-version copy is written.
|
|
410
|
+
skillsDir: path.join(HOME, '.agents', 'skills'),
|
|
411
|
+
nativeAgentsSkillsDir: true,
|
|
409
412
|
// Hooks: Open Plugins format — auto-discovered from
|
|
410
413
|
// ~/.agents/plugins/<name>/hooks/hooks.json (shipped block-goose-cli
|
|
411
414
|
// ≥ 1.34.0). See registerHooksForGoose.
|
|
@@ -416,7 +419,7 @@ export const AGENTS = {
|
|
|
416
419
|
supportsHooks: true,
|
|
417
420
|
// Plugins: Open Plugins under ~/.agents/plugins/<name>/ (same layout as
|
|
418
421
|
// agents-cli source). Version isolation copies into versionHome/.agents/plugins/.
|
|
419
|
-
capabilities: { hooks: { since: '1.34.0' }, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: false, skills:
|
|
422
|
+
capabilities: { hooks: { since: '1.34.0' }, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: false, skills: { since: '1.25.0' }, commands: false, plugins: true, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, memory: false, modes: ['edit'] },
|
|
420
423
|
},
|
|
421
424
|
// Google Antigravity CLI (`agy`) — official replacement for Gemini CLI as of IO 2026.
|
|
422
425
|
// configDir nests inside `~/.gemini/` since agy shares the parent dir with the Gemini
|
|
@@ -560,7 +563,7 @@ export const AGENTS = {
|
|
|
560
563
|
authFiles: ['auth.v2.file', 'auth.v2.key'],
|
|
561
564
|
commandsDir: path.join(HOME, '.factory', 'commands'),
|
|
562
565
|
commandsSubdir: 'commands',
|
|
563
|
-
skillsDir: '',
|
|
566
|
+
skillsDir: path.join(HOME, '.factory', 'skills'),
|
|
564
567
|
hooksDir: 'hooks',
|
|
565
568
|
pluginManifestDir: '.factory-plugin',
|
|
566
569
|
instructionsFile: 'AGENTS.md',
|
|
@@ -575,8 +578,8 @@ export const AGENTS = {
|
|
|
575
578
|
mcp: true,
|
|
576
579
|
mcpHttp: false,
|
|
577
580
|
mcpHeaders: false,
|
|
578
|
-
allowlist:
|
|
579
|
-
skills:
|
|
581
|
+
allowlist: { since: '0.57.5' },
|
|
582
|
+
skills: { since: '0.26.0' },
|
|
580
583
|
commands: true,
|
|
581
584
|
plugins: true,
|
|
582
585
|
subagents: true,
|
|
@@ -1826,8 +1829,8 @@ export function getUserMcpConfigPath(agentId) {
|
|
|
1826
1829
|
// Codex uses TOML config
|
|
1827
1830
|
return path.join(agent.configDir, 'config.toml');
|
|
1828
1831
|
case 'opencode':
|
|
1829
|
-
// OpenCode
|
|
1830
|
-
return path.join(
|
|
1832
|
+
// OpenCode loads ~/.config/opencode/opencode.jsonc (not ~/.opencode/)
|
|
1833
|
+
return path.join(HOME, '.config', 'opencode', 'opencode.jsonc');
|
|
1831
1834
|
case 'cursor':
|
|
1832
1835
|
// Cursor uses mcp.json
|
|
1833
1836
|
return path.join(agent.configDir, 'mcp.json');
|
|
@@ -1865,7 +1868,7 @@ export function getMcpConfigPathForHome(agentId, home) {
|
|
|
1865
1868
|
case 'codex':
|
|
1866
1869
|
return path.join(home, '.codex', 'config.toml');
|
|
1867
1870
|
case 'opencode':
|
|
1868
|
-
return path.join(home, '.opencode', 'opencode.jsonc');
|
|
1871
|
+
return path.join(home, '.config', 'opencode', 'opencode.jsonc');
|
|
1869
1872
|
case 'cursor':
|
|
1870
1873
|
return path.join(home, '.cursor', 'mcp.json');
|
|
1871
1874
|
case 'openclaw':
|
|
@@ -1903,7 +1906,8 @@ function getProjectMcpConfigPath(agentId, cwd = process.cwd()) {
|
|
|
1903
1906
|
case 'codex':
|
|
1904
1907
|
return path.join(cwd, `.${agentId}`, 'config.toml');
|
|
1905
1908
|
case 'opencode':
|
|
1906
|
-
|
|
1909
|
+
// Project config is opencode.jsonc at project root (not .opencode/)
|
|
1910
|
+
return path.join(cwd, 'opencode.jsonc');
|
|
1907
1911
|
case 'cursor':
|
|
1908
1912
|
return path.join(cwd, `.${agentId}`, 'mcp.json');
|
|
1909
1913
|
case 'openclaw':
|
package/dist/lib/mcp.js
CHANGED
|
@@ -485,7 +485,7 @@ function installMcpToForgeConfig(server, versionHome) {
|
|
|
485
485
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
486
486
|
}
|
|
487
487
|
function installMcpToOpenCodeConfig(server, versionHome) {
|
|
488
|
-
const configPath = path.join(versionHome, '.opencode', 'opencode.jsonc');
|
|
488
|
+
const configPath = path.join(versionHome, '.config', 'opencode', 'opencode.jsonc');
|
|
489
489
|
let config = {};
|
|
490
490
|
if (fs.existsSync(configPath)) {
|
|
491
491
|
const content = fs.readFileSync(configPath, 'utf-8');
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** Commit tally for one author email. */
|
|
2
|
+
export interface AuthorCommits {
|
|
3
|
+
author: string;
|
|
4
|
+
commits: number;
|
|
5
|
+
}
|
|
6
|
+
/** The shipped-work rollup for a window. */
|
|
7
|
+
export interface GitOutputSummary {
|
|
8
|
+
reposScanned: number;
|
|
9
|
+
commits: number;
|
|
10
|
+
byAuthor: AuthorCommits[];
|
|
11
|
+
prsOpened: number;
|
|
12
|
+
prsMerged: number;
|
|
13
|
+
/**
|
|
14
|
+
* Deduped commit SHAs authored by us in the window. Carried so `--all-hosts`
|
|
15
|
+
* can UNION across machines — a repo cloned on several boxes exposes the same
|
|
16
|
+
* commits to `git log` on each, so summing counts would multi-count.
|
|
17
|
+
*/
|
|
18
|
+
commitShas: string[];
|
|
19
|
+
/** false when `gh` is missing/unauthed — PR counts are then 0 and not trustworthy. */
|
|
20
|
+
ghAvailable: boolean;
|
|
21
|
+
/** Author emails counted as "ours". */
|
|
22
|
+
authors: string[];
|
|
23
|
+
/** gh logins queried for PRs. */
|
|
24
|
+
logins: string[];
|
|
25
|
+
sinceIso: string;
|
|
26
|
+
}
|
|
27
|
+
export interface GitOutputOptions {
|
|
28
|
+
/** Root scanned for git repos (e.g. ~/src). */
|
|
29
|
+
reposDir: string;
|
|
30
|
+
/** Window start, epoch ms. */
|
|
31
|
+
sinceMs: number;
|
|
32
|
+
/** Restrict commit authorship to these emails; default: identities discovered from git config. */
|
|
33
|
+
authors?: string[];
|
|
34
|
+
/** gh logins to search PRs for; default: the current `gh api user` login. */
|
|
35
|
+
logins?: string[];
|
|
36
|
+
/** Repo discovery depth below reposDir (default 4 — covers ~/src/host/org/repo). */
|
|
37
|
+
maxDepth?: number;
|
|
38
|
+
/** Query PRs via gh (default true). */
|
|
39
|
+
includePrs?: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Find git repositories under `root` up to `maxDepth` levels deep. A directory
|
|
43
|
+
* with a `.git` entry is a repo and is NOT descended into (so nested worktrees
|
|
44
|
+
* / submodules don't double-count).
|
|
45
|
+
*/
|
|
46
|
+
export declare function findGitRepos(root: string, maxDepth?: number): string[];
|
|
47
|
+
/**
|
|
48
|
+
* Count commits by our authors across all repos, deduped by SHA (so the same
|
|
49
|
+
* commit reachable via multiple repo clones/worktrees on this machine — or, at
|
|
50
|
+
* the fleet layer, across machines — is counted once), tallied per email.
|
|
51
|
+
*/
|
|
52
|
+
export declare function collectCommits(repos: string[], sinceIso: string, authors: string[]): Promise<{
|
|
53
|
+
total: number;
|
|
54
|
+
byAuthor: AuthorCommits[];
|
|
55
|
+
shas: string[];
|
|
56
|
+
}>;
|
|
57
|
+
/**
|
|
58
|
+
* Count PRs opened and merged in the window across the given logins. `gh search
|
|
59
|
+
* prs` searches all of GitHub, so one authed gh can cover multiple accounts'
|
|
60
|
+
* logins. Returns ghAvailable=false if gh can't be reached at all.
|
|
61
|
+
*/
|
|
62
|
+
export declare function collectPrs(logins: string[], sinceDate: string): Promise<{
|
|
63
|
+
opened: number;
|
|
64
|
+
merged: number;
|
|
65
|
+
logins: string[];
|
|
66
|
+
ghAvailable: boolean;
|
|
67
|
+
}>;
|
|
68
|
+
/** Format an epoch-ms as a YYYY-MM-DD date (UTC), for gh's date-range search. */
|
|
69
|
+
export declare function toSearchDate(ms: number): string;
|
|
70
|
+
/**
|
|
71
|
+
* Collect the full shipped-work summary for a window: commits (across accounts)
|
|
72
|
+
* plus PRs opened/merged.
|
|
73
|
+
*/
|
|
74
|
+
export declare function collectGitOutput(options: GitOutputOptions): Promise<GitOutputSummary>;
|