@phnx-labs/agents-cli 1.20.17 → 1.20.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +1 -1
  3. package/dist/commands/budget.d.ts +14 -0
  4. package/dist/commands/budget.js +137 -0
  5. package/dist/commands/cost.d.ts +12 -0
  6. package/dist/commands/cost.js +139 -0
  7. package/dist/commands/exec.d.ts +20 -0
  8. package/dist/commands/exec.js +382 -5
  9. package/dist/commands/secrets.d.ts +15 -0
  10. package/dist/commands/secrets.js +250 -4
  11. package/dist/commands/sessions.js +4 -0
  12. package/dist/index.js +4 -0
  13. package/dist/lib/budget/config.d.ts +9 -0
  14. package/dist/lib/budget/config.js +115 -0
  15. package/dist/lib/budget/enforce.d.ts +94 -0
  16. package/dist/lib/budget/enforce.js +151 -0
  17. package/dist/lib/budget/ledger.d.ts +61 -0
  18. package/dist/lib/budget/ledger.js +107 -0
  19. package/dist/lib/budget/preflight.d.ts +110 -0
  20. package/dist/lib/budget/preflight.js +200 -0
  21. package/dist/lib/checkpoint.d.ts +54 -0
  22. package/dist/lib/checkpoint.js +56 -0
  23. package/dist/lib/cloud/rush.js +18 -0
  24. package/dist/lib/exec.d.ts +36 -0
  25. package/dist/lib/exec.js +192 -4
  26. package/dist/lib/git.d.ts +18 -0
  27. package/dist/lib/git.js +67 -4
  28. package/dist/lib/loop.d.ts +145 -0
  29. package/dist/lib/loop.js +330 -0
  30. package/dist/lib/mcp.d.ts +7 -0
  31. package/dist/lib/mcp.js +24 -0
  32. package/dist/lib/models.d.ts +11 -0
  33. package/dist/lib/models.js +21 -0
  34. package/dist/lib/plugins.js +5 -2
  35. package/dist/lib/pricing/cost.d.ts +46 -0
  36. package/dist/lib/pricing/cost.js +71 -0
  37. package/dist/lib/pricing/index.d.ts +8 -0
  38. package/dist/lib/pricing/index.js +8 -0
  39. package/dist/lib/pricing/prices.json +138 -0
  40. package/dist/lib/pricing/table.d.ts +17 -0
  41. package/dist/lib/pricing/table.js +73 -0
  42. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  43. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  44. package/dist/lib/secrets/agent.d.ts +134 -0
  45. package/dist/lib/secrets/agent.js +501 -0
  46. package/dist/lib/secrets/bundles.d.ts +21 -0
  47. package/dist/lib/secrets/bundles.js +43 -0
  48. package/dist/lib/session/db.d.ts +40 -0
  49. package/dist/lib/session/db.js +84 -2
  50. package/dist/lib/session/discover.d.ts +2 -0
  51. package/dist/lib/session/discover.js +126 -2
  52. package/dist/lib/session/render.d.ts +2 -0
  53. package/dist/lib/session/render.js +1 -1
  54. package/dist/lib/session/types.d.ts +4 -0
  55. package/dist/lib/teams/agents.d.ts +32 -0
  56. package/dist/lib/teams/agents.js +66 -3
  57. package/dist/lib/teams/api.js +20 -0
  58. package/dist/lib/teams/parsers.js +16 -4
  59. package/dist/lib/types.d.ts +48 -0
  60. package/dist/lib/workflows.d.ts +56 -0
  61. package/dist/lib/workflows.js +72 -5
  62. package/package.json +2 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ **`agents secrets unlock`: a secrets-agent that ends Touch ID prompt spam (macOS)**
6
+
7
+ - macOS pops a Touch ID prompt **per bundle, per process** — the biometry assertion is process-local and macOS refuses to cache `kSecAccessControl`+biometry items, so running several agents at once (`agents teams`, parallel `agents run --secrets`) re-prompts once per process. New `agents secrets unlock <bundle>` reads the bundle once (one prompt) and holds the resolved env in a local broker; every later resolution — `agents run`, teammates, browser profiles, the routines daemon — is served from memory over a user-only Unix socket (`~/.agents/.cache/helpers/secrets-agent/`, `0700`) with no prompt. `agents secrets lock` wipes it; `agents secrets status` shows what's held and when it locks. The hold also ends on TTL expiry (default 24h, `--ttl`) and on screen-lock / sleep.
8
+ - **Opt-in by construction:** if you never `unlock`, resolution is byte-for-byte the existing keychain path — guarded behind a single `agentSocketExists()` stat. The single integration point is `readAndResolveBundleEnv`, so every consumer benefits without per-call-site changes. Broker-served reads are tagged `"source":"agent"` in the audit log.
9
+ - **Security trade-off (documented in `docs/secrets.md`):** while unlocked, a same-user process that can reach the socket reads the bundle silently — the same trust boundary the keychain already concedes (the ACL is user-presence, not code-identity), minus the visible prompt. Bounded by explicit per-bundle opt-in, TTL, screen-lock/sleep auto-lock, and `lock`.
10
+ - Snapshot semantics: `unlock` freezes a bundle's dynamic `exec:`/`env:`/`file:` refs at unlock time; keychain and literal values are unaffected.
11
+ - **Release note:** auto-lock on screen-lock/sleep adds a `watch-lock` subcommand to `keychain-helper.swift`. The signed helper must be rebuilt + re-notarized and its sha re-pinned (`scripts/build-keychain-helper.sh`, `scripts/Agents CLI.app.sha256`) for that path to ship; until then the agent degrades gracefully to TTL-only locking. Source: `src/lib/secrets/agent.ts`.
12
+
13
+ **Per-bundle tiers + opt-in auto-cache for the secrets-agent**
14
+
15
+ - Bundles now carry a tier (`agents secrets tier <bundle> [biometry|session]`, or `--tier` on `create`). `biometry` (default) is today's behavior — only an explicit `unlock` puts it in the agent. `session` makes a bundle agent-eligible.
16
+ - New `secrets.agent.auto: true` in `agents.yaml` (default off): the first real keychain read of a **`session`**-tier bundle auto-loads it into the broker in the background (no added latency, secret passed over stdin not argv), so the next concurrent run reads it silently — no manual `unlock`. A `biometry`-tier bundle is never auto-held.
17
+ - A `none` tier (items without the biometry ACL, fully silent, no agent) is intentionally **not** offered yet — it needs a separate signed-helper change and is the global downgrade the agent exists to avoid.
18
+ - Default secrets-agent TTL is 24h.
19
+
5
20
  **Headless Linux: `agents secrets` works out of the box when the keyring is locked**
6
21
 
7
22
  - On a headless server the libsecret/GNOME-keyring collection is locked, so the encrypted-file fallback is the only option — but it previously hard-failed unless `AGENTS_SECRETS_PASSPHRASE` was set, leaving `agents secrets` silently unusable. Now, on a headless run with no passphrase set, a random machine-local passphrase is auto-provisioned once at `~/.agents/.cache/secrets/.passphrase` (mode 0600) so the encrypted-file store just works. `AGENTS_SECRETS_PASSPHRASE` still takes precedence (off-disk key), an existing `.passphrase` is reused for stable interactive/headless behavior, and interactive TTY sessions are still prompted. Security model + resolution order documented in `docs/secrets.md`. (#371)
package/README.md CHANGED
@@ -553,7 +553,7 @@ Two repos with the same shape, different roles:
553
553
 
554
554
  See [docs/00-concepts.md](docs/00-concepts.md) for the full mental model: DotAgents repos, resource kinds, and how resolution works end-to-end.
555
555
 
556
- Other useful commands: `agents doctor` checks CLI availability and resource sync drift, `agents usage` shows available quota/rate-limit data for installed agents, `agents import` adopts an existing unmanaged install, `agents trash` lists and restores soft-deleted version directories, and `agents subagents` installs reusable subagent definitions for parent-agent workflows.
556
+ Other useful commands: `agents doctor` checks CLI availability and resource sync drift, `agents usage` shows available quota/rate-limit data for installed agents, `agents budget` shows cross-vendor spend caps and current spend-to-cap (and enforces pre-flight estimates + a hard-cap kill-switch on every run — see [docs/06-observability.md](docs/06-observability.md#budget-guardrails-agents-budget)), `agents import` adopts an existing unmanaged install, `agents trash` lists and restores soft-deleted version directories, and `agents subagents` installs reusable subagent definitions for parent-agent workflows.
557
557
 
558
558
  ---
559
559
 
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `agents budget` — view and set spend caps (issue #346).
3
+ *
4
+ * agents budget show effective caps + spend-to-cap (today + project)
5
+ * agents budget --json machine-readable snapshot
6
+ * agents budget set <cap> <n> write a cap to the user agents.yaml budget: block
7
+ *
8
+ * Caps resolve project > user (see lib/budget/config.ts); `agents budget`
9
+ * reports the EFFECTIVE merged config for the current directory, and `set`
10
+ * writes the user-global layer (the project layer is hand-edited in the repo's
11
+ * agents.yaml, like every other project override).
12
+ */
13
+ import type { Command } from 'commander';
14
+ export declare function registerBudgetCommand(program: Command): void;
@@ -0,0 +1,137 @@
1
+ import chalk from 'chalk';
2
+ import { ALL_AGENT_IDS } from '../lib/agents.js';
3
+ import { readMeta, updateMeta } from '../lib/state.js';
4
+ import { resolveBudgetConfig, hasAnyCap } from '../lib/budget/config.js';
5
+ import { loadLedger, spendForDay, spendForProject, spendForAgentDay, localDay } from '../lib/budget/ledger.js';
6
+ import { formatUsd } from '../lib/pricing/index.js';
7
+ const TOP_CAPS = ['per_run', 'per_day', 'per_project'];
8
+ export function registerBudgetCommand(program) {
9
+ const budgetCmd = program
10
+ .command('budget')
11
+ .description('Show spend caps and current spend-to-cap (issue #346)')
12
+ .option('--json', 'Emit the budget + spend snapshot as JSON')
13
+ .action((options) => {
14
+ const cwd = process.cwd();
15
+ const cfg = resolveBudgetConfig(cwd);
16
+ const ledger = loadLedger();
17
+ const today = localDay();
18
+ const daySpend = spendForDay(today, ledger);
19
+ const projectSpend = spendForProject(cwd, ledger);
20
+ const perAgentSpend = {};
21
+ if (cfg.per_agent) {
22
+ for (const agent of Object.keys(cfg.per_agent)) {
23
+ perAgentSpend[agent] = spendForAgentDay(agent, today, ledger);
24
+ }
25
+ }
26
+ if (options.json) {
27
+ console.log(JSON.stringify({
28
+ currency: cfg.currency ?? 'USD',
29
+ on_exceed: cfg.on_exceed ?? 'block',
30
+ require_confirm_over: cfg.require_confirm_over ?? null,
31
+ caps: {
32
+ per_run: cfg.per_run ?? null,
33
+ per_day: cfg.per_day ?? null,
34
+ per_project: cfg.per_project ?? null,
35
+ per_agent: cfg.per_agent ?? {},
36
+ },
37
+ spend: {
38
+ day: daySpend,
39
+ project: projectSpend,
40
+ per_agent_day: perAgentSpend,
41
+ },
42
+ configured: hasAnyCap(cfg),
43
+ project: cwd,
44
+ day: today,
45
+ }, null, 2));
46
+ return;
47
+ }
48
+ console.log(renderBudget(cfg, { daySpend, projectSpend, perAgentSpend, project: cwd, day: today }));
49
+ });
50
+ budgetCmd
51
+ .command('set <cap> <amount>')
52
+ .description(`Set a user-global cap. <cap> = ${TOP_CAPS.join(' | ')} | per_agent.<agent> | on_exceed | require_confirm_over`)
53
+ .addHelpText('after', `
54
+ Examples:
55
+ agents budget set per_run 5 Cap any single run at $5
56
+ agents budget set per_day 50 Cap total spend per day at $50
57
+ agents budget set per_agent.claude 30 Cap Claude's daily spend at $30
58
+ agents budget set on_exceed warn Switch to warn-only (do not block)
59
+ `)
60
+ .action((cap, amount) => {
61
+ const meta = readMeta();
62
+ const current = { ...(meta.budget ?? {}) };
63
+ if (cap === 'on_exceed') {
64
+ if (amount !== 'block' && amount !== 'warn') {
65
+ console.error(chalk.red(`on_exceed must be 'block' or 'warn'.`));
66
+ process.exit(1);
67
+ }
68
+ current.on_exceed = amount;
69
+ }
70
+ else if (cap.startsWith('per_agent.')) {
71
+ const agent = cap.slice('per_agent.'.length);
72
+ if (!ALL_AGENT_IDS.includes(agent)) {
73
+ console.error(chalk.red(`Unknown agent '${agent}'. Known: ${ALL_AGENT_IDS.join(', ')}`));
74
+ process.exit(1);
75
+ }
76
+ const value = parseAmount(amount);
77
+ current.per_agent = { ...(current.per_agent ?? {}), [agent]: value };
78
+ }
79
+ else if (TOP_CAPS.includes(cap) || cap === 'require_confirm_over') {
80
+ const value = parseAmount(amount);
81
+ current[cap] = value;
82
+ }
83
+ else {
84
+ console.error(chalk.red(`Unknown cap '${cap}'. Use: ${TOP_CAPS.join(', ')}, per_agent.<agent>, on_exceed, require_confirm_over.`));
85
+ process.exit(1);
86
+ }
87
+ updateMeta((m) => ({ ...m, budget: current }));
88
+ console.log(chalk.green(`Set budget.${cap} = ${amount}`));
89
+ });
90
+ }
91
+ function parseAmount(amount) {
92
+ const value = Number(amount.replace(/^\$/, ''));
93
+ if (!Number.isFinite(value) || value < 0) {
94
+ console.error(chalk.red(`Invalid amount '${amount}'. Use a non-negative number (e.g. 5 or 5.00).`));
95
+ process.exit(1);
96
+ }
97
+ return value;
98
+ }
99
+ /** Render one cap line: " per_run $0.42 / $5.00 ▮▮▯▯▯▯▯▯▯▯". Unset caps render as "(unset)". */
100
+ function capLine(label, spend, cap) {
101
+ if (cap === undefined) {
102
+ return ` ${label.padEnd(14)} ${chalk.dim('(unset)')}`;
103
+ }
104
+ const spent = spend ?? 0;
105
+ const ratio = cap > 0 ? Math.min(spent / cap, 1) : 0;
106
+ const bars = Math.round(ratio * 10);
107
+ const bar = '▮'.repeat(bars) + '▯'.repeat(10 - bars);
108
+ const color = ratio >= 1 ? chalk.red : ratio >= 0.8 ? chalk.yellow : chalk.green;
109
+ const figure = spend === null ? formatUsd(cap) : `${formatUsd(spent)} / ${formatUsd(cap)}`;
110
+ return ` ${label.padEnd(14)} ${color(figure.padEnd(18))} ${color(bar)}`;
111
+ }
112
+ function renderBudget(cfg, snap) {
113
+ const lines = [];
114
+ lines.push(chalk.bold('Budget') + chalk.dim(` (on_exceed: ${cfg.on_exceed ?? 'block'}, currency: ${cfg.currency ?? 'USD'})`));
115
+ lines.push(chalk.dim(` project: ${snap.project}`));
116
+ lines.push(chalk.dim(` day: ${snap.day}`));
117
+ lines.push('');
118
+ if (!hasAnyCap(cfg)) {
119
+ lines.push(chalk.dim(' No caps configured. Set one with: agents budget set per_run 5'));
120
+ return lines.join('\n');
121
+ }
122
+ lines.push(capLine('per_run', null, cfg.per_run));
123
+ lines.push(capLine('per_day', snap.daySpend, cfg.per_day));
124
+ lines.push(capLine('per_project', snap.projectSpend, cfg.per_project));
125
+ if (cfg.per_agent && Object.keys(cfg.per_agent).length > 0) {
126
+ lines.push('');
127
+ lines.push(chalk.bold('Per-agent (today)'));
128
+ for (const [agent, cap] of Object.entries(cfg.per_agent)) {
129
+ lines.push(capLine(agent, snap.perAgentSpend[agent] ?? 0, cap));
130
+ }
131
+ }
132
+ if (cfg.require_confirm_over !== undefined) {
133
+ lines.push('');
134
+ lines.push(chalk.dim(` confirm prompt over: ${formatUsd(cfg.require_confirm_over)}`));
135
+ }
136
+ return lines.join('\n');
137
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Cost command — roll up $ spend and wall-clock duration across the local,
3
+ * cross-agent session index.
4
+ *
5
+ * This is the read side of issue #323. Cost and duration are computed and
6
+ * persisted at scan time (see src/lib/session/discover.ts + db.ts); this
7
+ * command only queries and renders them — a pure SQLite/CLI win, no server,
8
+ * no telemetry. Distinct from `agents usage`, which reports live rate-limit /
9
+ * quota status per agent and is left untouched.
10
+ */
11
+ import type { Command } from 'commander';
12
+ export declare function registerCostCommand(program: Command): void;
@@ -0,0 +1,139 @@
1
+ import chalk from 'chalk';
2
+ import { discoverSessions, parseTimeFilter } from '../lib/session/discover.js';
3
+ import { queryUsageRollup, topSessionsByCost, } from '../lib/session/db.js';
4
+ import { formatUsd, PRICING_VERSION } from '../lib/pricing/index.js';
5
+ import { formatDuration } from '../lib/session/render.js';
6
+ export function registerCostCommand(program) {
7
+ program
8
+ .command('cost')
9
+ .description('Roll up $ cost and duration across local agent sessions')
10
+ .option('--json', 'Output the rollup as JSON')
11
+ .option('--since <time>', 'Only sessions newer than this (e.g., 7d, 4w, or ISO date)')
12
+ .option('--by <dimension>', 'Group the breakdown by: agent (default), project, or day')
13
+ .addHelpText('after', `
14
+ Examples:
15
+ agents cost Daily histogram + top sessions + per-agent breakdown
16
+ agents cost --since 30d Last 30 days only
17
+ agents cost --by project Break down by project instead of agent
18
+ agents cost --by day --json Machine-readable daily rollup
19
+
20
+ Cost is computed offline from a versioned per-model price table (${PRICING_VERSION}).
21
+ `)
22
+ .action(async (options) => {
23
+ await costAction(options);
24
+ });
25
+ }
26
+ /** Map the --by flag to a rollup group, rejecting unknown values. */
27
+ function resolveGroup(by) {
28
+ if (by === undefined)
29
+ return 'agent';
30
+ if (by === 'agent' || by === 'project' || by === 'day')
31
+ return by;
32
+ console.error(chalk.red('error: --by must be one of: agent, project, day'));
33
+ process.exit(1);
34
+ }
35
+ async function costAction(options) {
36
+ const sinceMs = options.since ? parseTimeFilter(options.since) : undefined;
37
+ // Ensure the index is fresh (and migrated to v6) before we read costs.
38
+ await discoverSessions({ all: true, since: options.since, limit: 1 });
39
+ const filter = {};
40
+ if (typeof sinceMs === 'number')
41
+ filter.sinceMs = sinceMs;
42
+ const groupBy = resolveGroup(options.by);
43
+ const daily = queryUsageRollup({ ...filter, groupBy: 'day' });
44
+ const breakdown = queryUsageRollup({ ...filter, groupBy });
45
+ const top = topSessionsByCost(10, filter);
46
+ const totalCost = breakdown.reduce((s, r) => s + r.costUsd, 0);
47
+ const totalSessions = breakdown.reduce((s, r) => s + r.sessionCount, 0);
48
+ const totalDuration = breakdown.reduce((s, r) => s + r.durationMs, 0);
49
+ if (options.json) {
50
+ process.stdout.write(JSON.stringify({
51
+ pricingVersion: PRICING_VERSION,
52
+ since: options.since ?? null,
53
+ totals: { costUsd: totalCost, sessionCount: totalSessions, durationMs: totalDuration },
54
+ daily,
55
+ breakdown: { by: groupBy, rows: breakdown },
56
+ topSessions: top.map(t => ({
57
+ id: t.meta.id,
58
+ shortId: t.meta.shortId,
59
+ agent: t.meta.agent,
60
+ project: t.meta.project ?? null,
61
+ topic: t.meta.topic ?? t.meta.label ?? null,
62
+ costUsd: t.costUsd,
63
+ durationMs: t.durationMs,
64
+ timestamp: t.meta.timestamp,
65
+ })),
66
+ }, null, 2) + '\n');
67
+ return;
68
+ }
69
+ if (totalSessions === 0) {
70
+ console.log(chalk.gray('No sessions with cost data found. Run `agents sessions --all` to index, then retry.'));
71
+ return;
72
+ }
73
+ const out = [];
74
+ out.push(chalk.bold('Cost') + chalk.gray(` · pricing ${PRICING_VERSION}${options.since ? ` · since ${options.since}` : ''}`));
75
+ out.push(` ${chalk.green(formatUsd(totalCost))} across ${totalSessions} session${totalSessions !== 1 ? 's' : ''}` +
76
+ (totalDuration > 0 ? chalk.gray(` · ${formatDuration(totalDuration)} total`) : ''));
77
+ out.push('');
78
+ // Daily histogram (unicode block sparkline, zero deps).
79
+ if (daily.length > 0) {
80
+ out.push(chalk.bold('Daily'));
81
+ out.push(renderDailyHistogram(daily));
82
+ out.push('');
83
+ }
84
+ // Top sessions by cost.
85
+ if (top.length > 0) {
86
+ out.push(chalk.bold('Top sessions by cost'));
87
+ const costW = Math.max(...top.map(t => formatUsd(t.costUsd).length), 4);
88
+ for (const t of top) {
89
+ const cost = formatUsd(t.costUsd).padStart(costW);
90
+ 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
+ out.push(` ${chalk.green(cost)} ${chalk.gray(t.meta.shortId)} ${chalk.cyan(t.meta.agent.padEnd(7))} ${truncate(label, 48)}` +
94
+ proj +
95
+ chalk.gray(` ${dur}`));
96
+ }
97
+ out.push('');
98
+ }
99
+ // Per-agent / per-project / per-day breakdown.
100
+ const groupLabel = groupBy === 'agent' ? 'agent' : groupBy === 'project' ? 'project' : 'day';
101
+ out.push(chalk.bold(`By ${groupLabel}`));
102
+ const keyW = Math.max(...breakdown.map(r => r.key.length), groupLabel.length);
103
+ const costW2 = Math.max(...breakdown.map(r => formatUsd(r.costUsd).length), 4);
104
+ for (const r of breakdown) {
105
+ const cost = formatUsd(r.costUsd).padStart(costW2);
106
+ const dur = r.durationMs > 0 ? formatDuration(r.durationMs) : '—';
107
+ out.push(` ${r.key.padEnd(keyW)} ${chalk.green(cost)} ${chalk.gray(`${r.sessionCount} session${r.sessionCount !== 1 ? 's' : ''}`)} ${chalk.gray(dur)}`);
108
+ }
109
+ console.log(out.join('\n'));
110
+ }
111
+ /** Eight levels of vertical block characters for sparkline rendering. */
112
+ const BLOCKS = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
113
+ /** Render a per-day cost histogram as a unicode sparkline plus a labeled list. */
114
+ function renderDailyHistogram(daily) {
115
+ // Daily comes back cost-desc; show it chronologically for the sparkline.
116
+ const sorted = [...daily].sort((a, b) => a.key.localeCompare(b.key));
117
+ const max = Math.max(...sorted.map(d => d.costUsd), 0);
118
+ const spark = sorted
119
+ .map(d => {
120
+ if (max <= 0 || d.costUsd <= 0)
121
+ return BLOCKS[0];
122
+ const idx = Math.min(BLOCKS.length - 1, Math.round((d.costUsd / max) * (BLOCKS.length - 1)));
123
+ return BLOCKS[idx];
124
+ })
125
+ .join('');
126
+ const lines = [` ${chalk.green(spark)}`];
127
+ // Show the most expensive days as a short list under the sparkline.
128
+ const topDays = [...daily].slice(0, 7);
129
+ const costW = Math.max(...topDays.map(d => formatUsd(d.costUsd).length), 4);
130
+ for (const d of topDays) {
131
+ lines.push(` ${chalk.gray(d.key)} ${chalk.green(formatUsd(d.costUsd).padStart(costW))}`);
132
+ }
133
+ return lines.join('\n');
134
+ }
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
+ }
@@ -6,5 +6,25 @@
6
6
  * injection, and multi-agent fallback chains for rate-limit resilience.
7
7
  */
8
8
  import type { Command } from 'commander';
9
+ /**
10
+ * Build the LoopConfig the driver consumes from CLI flags and/or a workflow's
11
+ * `loop:` frontmatter block (issue #332). Returns undefined when neither source
12
+ * activates a loop (the common single-shot run). CLI flags take precedence over
13
+ * the workflow's declared values field-by-field, so `--max-iterations 5`
14
+ * overrides a workflow's `max_iterations: 3`.
15
+ *
16
+ * `--loop` with no sub-options is a valid bare loop (driver applies its own
17
+ * maxIterations safety cap). A workflow `loop:` block activates a loop even
18
+ * without `--loop` so `agents run <workflow>` honors a declared loop.
19
+ */
20
+ export declare function buildLoopConfig(flags: {
21
+ loop?: boolean;
22
+ maxIterations?: string;
23
+ budget?: string;
24
+ until?: string;
25
+ interval?: string;
26
+ }, workflowLoop?: import('../lib/workflows.js').LoopConfigRaw): import('../lib/loop.js').LoopConfig | undefined;
27
+ /** Map a loop stop reason to a process exit code. condition-met/max are clean exits. */
28
+ export declare function loopExitCode(stoppedBy: import('../lib/loop.js').LoopStoppedBy): number;
9
29
  /** Register the `agents run <agent> [prompt]` command. */
10
30
  export declare function registerRunCommand(program: Command): void;