@phnx-labs/agents-cli 1.20.16 → 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 (75) hide show
  1. package/CHANGELOG.md +19 -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/commands/sync.d.ts +10 -3
  13. package/dist/commands/sync.js +72 -9
  14. package/dist/index.js +4 -0
  15. package/dist/lib/budget/config.d.ts +9 -0
  16. package/dist/lib/budget/config.js +115 -0
  17. package/dist/lib/budget/enforce.d.ts +94 -0
  18. package/dist/lib/budget/enforce.js +151 -0
  19. package/dist/lib/budget/ledger.d.ts +61 -0
  20. package/dist/lib/budget/ledger.js +107 -0
  21. package/dist/lib/budget/preflight.d.ts +110 -0
  22. package/dist/lib/budget/preflight.js +200 -0
  23. package/dist/lib/checkpoint.d.ts +54 -0
  24. package/dist/lib/checkpoint.js +56 -0
  25. package/dist/lib/cloud/rush.js +18 -0
  26. package/dist/lib/exec.d.ts +36 -0
  27. package/dist/lib/exec.js +192 -4
  28. package/dist/lib/git.d.ts +18 -0
  29. package/dist/lib/git.js +67 -4
  30. package/dist/lib/hooks.js +12 -0
  31. package/dist/lib/loop.d.ts +145 -0
  32. package/dist/lib/loop.js +330 -0
  33. package/dist/lib/mcp.d.ts +7 -0
  34. package/dist/lib/mcp.js +24 -0
  35. package/dist/lib/models.d.ts +11 -0
  36. package/dist/lib/models.js +21 -0
  37. package/dist/lib/plugin-marketplace.js +16 -6
  38. package/dist/lib/plugins.js +5 -2
  39. package/dist/lib/pricing/cost.d.ts +46 -0
  40. package/dist/lib/pricing/cost.js +71 -0
  41. package/dist/lib/pricing/index.d.ts +8 -0
  42. package/dist/lib/pricing/index.js +8 -0
  43. package/dist/lib/pricing/prices.json +138 -0
  44. package/dist/lib/pricing/table.d.ts +17 -0
  45. package/dist/lib/pricing/table.js +73 -0
  46. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  47. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  48. package/dist/lib/secrets/agent.d.ts +134 -0
  49. package/dist/lib/secrets/agent.js +501 -0
  50. package/dist/lib/secrets/bundles.d.ts +21 -0
  51. package/dist/lib/secrets/bundles.js +43 -0
  52. package/dist/lib/secrets/drivers/rush.d.ts +14 -0
  53. package/dist/lib/secrets/drivers/rush.js +84 -0
  54. package/dist/lib/secrets/linux.js +88 -10
  55. package/dist/lib/secrets/sync-backend.d.ts +48 -0
  56. package/dist/lib/secrets/sync-backend.js +13 -0
  57. package/dist/lib/secrets/sync.d.ts +15 -23
  58. package/dist/lib/secrets/sync.js +31 -66
  59. package/dist/lib/session/db.d.ts +40 -0
  60. package/dist/lib/session/db.js +84 -2
  61. package/dist/lib/session/discover.d.ts +2 -0
  62. package/dist/lib/session/discover.js +126 -2
  63. package/dist/lib/session/render.d.ts +2 -0
  64. package/dist/lib/session/render.js +1 -1
  65. package/dist/lib/session/types.d.ts +4 -0
  66. package/dist/lib/sync-umbrella.d.ts +76 -0
  67. package/dist/lib/sync-umbrella.js +125 -0
  68. package/dist/lib/teams/agents.d.ts +32 -0
  69. package/dist/lib/teams/agents.js +66 -3
  70. package/dist/lib/teams/api.js +20 -0
  71. package/dist/lib/teams/parsers.js +16 -4
  72. package/dist/lib/types.d.ts +48 -0
  73. package/dist/lib/workflows.d.ts +56 -0
  74. package/dist/lib/workflows.js +72 -5
  75. package/package.json +2 -1
@@ -710,6 +710,27 @@ export function resolveModel(agent, version, requested) {
710
710
  warning: `model "${requested}" not in known catalog for ${agent}@${version}; forwarding as-is${hint}`,
711
711
  };
712
712
  }
713
+ /**
714
+ * Resolve the model id an `agents run` will ACTUALLY use, for cost estimation
715
+ * (issue #346). The run path resolves the model in this precedence:
716
+ * 1. explicit `--model` (or profile/workflow/runDefaults value) — `requested`
717
+ * 2. otherwise the agent CLI's own built-in default, which we read from the
718
+ * extracted catalog's `isDefault` model.
719
+ * Returns null only when we have neither — the caller must then treat the
720
+ * estimate as unpriced rather than silently using an unpriced placeholder id
721
+ * like `${agent}-default`.
722
+ */
723
+ export function resolveEffectiveModel(agent, version, requested) {
724
+ if (requested && requested.trim() !== '') {
725
+ const resolved = resolveModel(agent, version, requested);
726
+ return resolved.canonical ?? resolved.forwarded;
727
+ }
728
+ const catalog = getModelCatalog(agent, version);
729
+ if (!catalog)
730
+ return null;
731
+ const def = catalog.models.find((m) => m.isDefault);
732
+ return def?.id ?? null;
733
+ }
713
734
  /** Find the closest matching model ids/aliases using edit distance. */
714
735
  function pickSuggestions(requested, catalog) {
715
736
  const all = [...catalog.models.map((m) => m.id), ...Object.keys(catalog.aliases)];
@@ -209,17 +209,24 @@ export function validateClaudePluginManifest(manifest) {
209
209
  const value = m[field];
210
210
  if (value === undefined || value === null)
211
211
  continue;
212
+ // How-to-fix written so a human OR a coding agent reading stderr can act
213
+ // without further investigation. Deleting the field is the recommended fix:
214
+ // Claude auto-discovers skills/commands/agents from their directories, which
215
+ // is why every well-formed plugin omits these fields entirely.
216
+ const fix = `Fix: delete the "${field}" field from plugin.json (recommended — Claude ` +
217
+ `auto-discovers from the ${field}/ directory), or rewrite every entry as a ` +
218
+ `"./"-relative path (e.g. "./${field}/<name>").`;
212
219
  const entries = Array.isArray(value) ? value : [value];
213
220
  for (const entry of entries) {
214
221
  if (typeof entry !== 'string') {
215
- warnings.push(`plugin.json field "${field}" must contain relative paths starting with "./" ` +
216
- `(e.g. "./${field}/<name>"); found a non-string value. Claude Code will reject the whole plugin.`);
222
+ warnings.push(`field "${field}" must be a "./"-relative path string or an array of them; ` +
223
+ `found a non-string entry. Claude Code silently rejects the ENTIRE plugin. ${fix}`);
217
224
  break;
218
225
  }
219
226
  if (!entry.startsWith('./')) {
220
- warnings.push(`plugin.json field "${field}" entry "${entry}" must be a relative path starting with "./" ` +
221
- `(e.g. "./${field}/${entry}"). Claude Code rejects the entire plugin otherwise — ` +
222
- `remove the field to auto-discover from ${field}/, or use relative paths.`);
227
+ warnings.push(`field "${field}" entry "${entry}" must be a relative path starting with "./" ` +
228
+ `(e.g. "./${field}/${entry}"), not a bare name. Claude Code silently rejects the ` +
229
+ `ENTIRE plugin no commands or skills load. ${fix}`);
223
230
  break;
224
231
  }
225
232
  }
@@ -268,7 +275,10 @@ export function syncMarketplaceManifest(spec, agent, versionHome) {
268
275
  continue;
269
276
  }
270
277
  for (const warning of validateClaudePluginManifest(manifest)) {
271
- process.stderr.write(`agents-cli: plugin '${manifest.name ?? entry.name}': ${warning}\n`);
278
+ // Reference the plugin by name, not the marketplace-copy path: that copy is
279
+ // regenerated from source on every sync, so editing it gets stomped. The fix
280
+ // belongs in the plugin's SOURCE .claude-plugin/plugin.json.
281
+ process.stderr.write(`agents-cli: plugin '${manifest.name ?? entry.name}' has a Claude-invalid manifest — ${warning}\n`);
272
282
  }
273
283
  entries.push({
274
284
  name: manifest.name,
@@ -13,6 +13,7 @@ import * as path from 'path';
13
13
  import { execFileSync } from 'child_process';
14
14
  import { getPluginsDir, getTrashPluginsDir, getExtraPluginsDir, getProjectPluginsDir } from './state.js';
15
15
  import { IS_WINDOWS, isWindowsAbsolutePath, homeDir } from './platform/index.js';
16
+ import { assertSafeGitTransport } from './git.js';
16
17
  import { listInstalledVersions, getVersionHomePath } from './versions.js';
17
18
  import { AGENTS, agentConfigDirName } from './agents.js';
18
19
  import { capableAgents, isCapable } from './capabilities.js';
@@ -1078,11 +1079,13 @@ export async function installPlugin(spec) {
1078
1079
  fs.cpSync(resolvedSource, targetRoot, { recursive: true });
1079
1080
  }
1080
1081
  else {
1081
- // Git clone
1082
+ // Git clone. Validate the transport (blocks ext::/file:///http:///leading-"-")
1083
+ // and pass "--" so the source can never be parsed as a git option.
1084
+ assertSafeGitTransport(resolvedSource);
1082
1085
  if (fs.existsSync(targetRoot)) {
1083
1086
  fs.rmSync(targetRoot, { recursive: true, force: true });
1084
1087
  }
1085
- execFileSync('git', ['clone', '--depth', '1', resolvedSource, targetRoot], {
1088
+ execFileSync('git', ['clone', '--depth', '1', '--', resolvedSource, targetRoot], {
1086
1089
  stdio: 'pipe',
1087
1090
  });
1088
1091
  }
@@ -0,0 +1,46 @@
1
+ /** A single usage record: one model and the tokens it consumed in each direction. */
2
+ export interface TokenUsage {
3
+ model?: string;
4
+ inputTokens?: number;
5
+ outputTokens?: number;
6
+ cacheReadTokens?: number;
7
+ cacheCreationTokens?: number;
8
+ }
9
+ /**
10
+ * USD cost of one usage record. Returns 0 when the model is missing or unpriced
11
+ * (cost is additive — an unknown model contributes nothing, not NaN). Cache
12
+ * read/write tokens are priced at their dedicated rates when the table exposes
13
+ * them, otherwise they fall back to the input rate (the standard LiteLLM
14
+ * convention for models that don't publish a separate cache price).
15
+ */
16
+ export declare function costOfUsage(u: TokenUsage): number;
17
+ /** Sum the USD cost of every usage record in a session. */
18
+ export declare function costOfSession(usages: TokenUsage[]): number;
19
+ /**
20
+ * Format a USD amount for human display. Cents-precise, with a "<$0.01" floor
21
+ * so tiny-but-nonzero sessions don't render as "$0.00" and read as free.
22
+ */
23
+ export declare function formatUsd(usd: number): string;
24
+ /** Token bundle accepted by the #346 estimator/actual-cost helpers. */
25
+ interface EstimatorTokens {
26
+ inputTokens: number;
27
+ outputTokens: number;
28
+ cacheReadTokens?: number;
29
+ cacheCreationTokens?: number;
30
+ }
31
+ /**
32
+ * Pre-flight cost estimate for a model + token bundle (issue #346's budget
33
+ * gate). Returns the resolved canonical model id (`modelMatched`) so callers
34
+ * can warn when an estimate fell back to $0 because the model is unpriced.
35
+ */
36
+ export declare function estimateCost(model: string, tokens: EstimatorTokens): {
37
+ usd: number;
38
+ modelMatched: string | null;
39
+ };
40
+ /** Actual (post-hoc) cost of a model + observed usage. Thin alias over costOfUsage. */
41
+ export declare function actualCost(model: string, usage: EstimatorTokens): {
42
+ usd: number;
43
+ };
44
+ /** True when the model resolves to a priced entry in the table. */
45
+ export declare function isModelPriced(model: string): boolean;
46
+ export {};
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Token-usage → USD cost math, built on the offline pricing table.
3
+ *
4
+ * `costOfUsage` is the single multiply-by-price primitive every other helper
5
+ * (and issue #346's budget pre-flight estimator) routes through. It returns 0
6
+ * for unknown/unpriced models rather than throwing — cost is additive, and a
7
+ * single unknown model in a session shouldn't blow up the whole rollup.
8
+ */
9
+ import { getModelPricing } from './table.js';
10
+ /**
11
+ * USD cost of one usage record. Returns 0 when the model is missing or unpriced
12
+ * (cost is additive — an unknown model contributes nothing, not NaN). Cache
13
+ * read/write tokens are priced at their dedicated rates when the table exposes
14
+ * them, otherwise they fall back to the input rate (the standard LiteLLM
15
+ * convention for models that don't publish a separate cache price).
16
+ */
17
+ export function costOfUsage(u) {
18
+ if (!u.model)
19
+ return 0;
20
+ const pricing = getModelPricing(u.model);
21
+ if (!pricing)
22
+ return 0;
23
+ const input = u.inputTokens ?? 0;
24
+ const output = u.outputTokens ?? 0;
25
+ const cacheRead = u.cacheReadTokens ?? 0;
26
+ const cacheWrite = u.cacheCreationTokens ?? 0;
27
+ const cacheReadRate = pricing.cacheReadPerToken ?? pricing.inputPerToken;
28
+ const cacheWriteRate = pricing.cacheWritePerToken ?? pricing.inputPerToken;
29
+ return (input * pricing.inputPerToken +
30
+ output * pricing.outputPerToken +
31
+ cacheRead * cacheReadRate +
32
+ cacheWrite * cacheWriteRate);
33
+ }
34
+ /** Sum the USD cost of every usage record in a session. */
35
+ export function costOfSession(usages) {
36
+ let total = 0;
37
+ for (const u of usages)
38
+ total += costOfUsage(u);
39
+ return total;
40
+ }
41
+ /**
42
+ * Format a USD amount for human display. Cents-precise, with a "<$0.01" floor
43
+ * so tiny-but-nonzero sessions don't render as "$0.00" and read as free.
44
+ */
45
+ export function formatUsd(usd) {
46
+ if (!Number.isFinite(usd) || usd <= 0)
47
+ return '$0.00';
48
+ if (usd < 0.01)
49
+ return '<$0.01';
50
+ return `$${usd.toFixed(2)}`;
51
+ }
52
+ /**
53
+ * Pre-flight cost estimate for a model + token bundle (issue #346's budget
54
+ * gate). Returns the resolved canonical model id (`modelMatched`) so callers
55
+ * can warn when an estimate fell back to $0 because the model is unpriced.
56
+ */
57
+ export function estimateCost(model, tokens) {
58
+ const pricing = getModelPricing(model);
59
+ const usd = costOfUsage({ model, ...tokens });
60
+ // modelMatched is the input model when priced, null when unknown — callers
61
+ // only need the priced/unpriced signal, not the internal canonical key.
62
+ return { usd, modelMatched: pricing ? model : null };
63
+ }
64
+ /** Actual (post-hoc) cost of a model + observed usage. Thin alias over costOfUsage. */
65
+ export function actualCost(model, usage) {
66
+ return { usd: costOfUsage({ model, ...usage }) };
67
+ }
68
+ /** True when the model resolves to a priced entry in the table. */
69
+ export function isModelPriced(model) {
70
+ return getModelPricing(model) !== null;
71
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Canonical, reusable pricing module.
3
+ *
4
+ * Public surface re-exported here is the contract issue #346 (budget
5
+ * enforcement) imports against — keep it stable.
6
+ */
7
+ export { type ModelPricing, PRICING_VERSION, getModelPricing, listPricedModels, } from './table.js';
8
+ export { type TokenUsage, costOfUsage, costOfSession, formatUsd, estimateCost, actualCost, isModelPriced, } from './cost.js';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Canonical, reusable pricing module.
3
+ *
4
+ * Public surface re-exported here is the contract issue #346 (budget
5
+ * enforcement) imports against — keep it stable.
6
+ */
7
+ export { PRICING_VERSION, getModelPricing, listPricedModels, } from './table.js';
8
+ export { costOfUsage, costOfSession, formatUsd, estimateCost, actualCost, isModelPriced, } from './cost.js';
@@ -0,0 +1,138 @@
1
+ {
2
+ "version": "2026-06-24",
3
+ "models": {
4
+ "claude-opus-4": {
5
+ "inputPerToken": 0.000005,
6
+ "outputPerToken": 0.000025,
7
+ "cacheReadPerToken": 0.0000005,
8
+ "cacheWritePerToken": 0.00000625
9
+ },
10
+ "claude-sonnet-4": {
11
+ "inputPerToken": 0.000003,
12
+ "outputPerToken": 0.000015,
13
+ "cacheReadPerToken": 0.0000003,
14
+ "cacheWritePerToken": 0.00000375
15
+ },
16
+ "claude-haiku-4": {
17
+ "inputPerToken": 0.000001,
18
+ "outputPerToken": 0.000005,
19
+ "cacheReadPerToken": 0.0000001,
20
+ "cacheWritePerToken": 0.00000125
21
+ },
22
+ "claude-fable-5": {
23
+ "inputPerToken": 0.00001,
24
+ "outputPerToken": 0.00005,
25
+ "cacheReadPerToken": 0.000001,
26
+ "cacheWritePerToken": 0.0000125
27
+ },
28
+ "claude-mythos-5": {
29
+ "inputPerToken": 0.00001,
30
+ "outputPerToken": 0.00005,
31
+ "cacheReadPerToken": 0.000001,
32
+ "cacheWritePerToken": 0.0000125
33
+ },
34
+ "claude-3-5-sonnet": {
35
+ "inputPerToken": 0.000003,
36
+ "outputPerToken": 0.000015,
37
+ "cacheReadPerToken": 0.0000003,
38
+ "cacheWritePerToken": 0.00000375
39
+ },
40
+ "claude-3-5-haiku": {
41
+ "inputPerToken": 0.0000008,
42
+ "outputPerToken": 0.000004,
43
+ "cacheReadPerToken": 0.00000008,
44
+ "cacheWritePerToken": 0.000001
45
+ },
46
+ "claude-3-opus": {
47
+ "inputPerToken": 0.000015,
48
+ "outputPerToken": 0.000075,
49
+ "cacheReadPerToken": 0.0000015,
50
+ "cacheWritePerToken": 0.00001875
51
+ },
52
+ "gpt-5.5": {
53
+ "inputPerToken": 0.000005,
54
+ "outputPerToken": 0.00003,
55
+ "cacheReadPerToken": 0.0000005
56
+ },
57
+ "gpt-5.4": {
58
+ "inputPerToken": 0.0000025,
59
+ "outputPerToken": 0.000015,
60
+ "cacheReadPerToken": 0.00000025
61
+ },
62
+ "gpt-5.4-mini": {
63
+ "inputPerToken": 0.00000075,
64
+ "outputPerToken": 0.0000045,
65
+ "cacheReadPerToken": 0.000000075
66
+ },
67
+ "gpt-5.4-nano": {
68
+ "inputPerToken": 0.0000002,
69
+ "outputPerToken": 0.00000125,
70
+ "cacheReadPerToken": 0.00000002
71
+ },
72
+ "gpt-5": {
73
+ "inputPerToken": 0.00000125,
74
+ "outputPerToken": 0.00001,
75
+ "cacheReadPerToken": 0.000000125
76
+ },
77
+ "gpt-4.1": {
78
+ "inputPerToken": 0.000002,
79
+ "outputPerToken": 0.000008,
80
+ "cacheReadPerToken": 0.0000005
81
+ },
82
+ "gpt-4.1-mini": {
83
+ "inputPerToken": 0.0000004,
84
+ "outputPerToken": 0.0000016,
85
+ "cacheReadPerToken": 0.0000001
86
+ },
87
+ "gpt-4.1-nano": {
88
+ "inputPerToken": 0.0000001,
89
+ "outputPerToken": 0.0000004,
90
+ "cacheReadPerToken": 0.000000025
91
+ },
92
+ "gpt-4o": {
93
+ "inputPerToken": 0.0000025,
94
+ "outputPerToken": 0.00001,
95
+ "cacheReadPerToken": 0.00000125
96
+ },
97
+ "gpt-4o-mini": {
98
+ "inputPerToken": 0.00000015,
99
+ "outputPerToken": 0.0000006,
100
+ "cacheReadPerToken": 0.000000075
101
+ },
102
+ "o3": {
103
+ "inputPerToken": 0.000002,
104
+ "outputPerToken": 0.000008,
105
+ "cacheReadPerToken": 0.0000005
106
+ },
107
+ "o4-mini": {
108
+ "inputPerToken": 0.0000011,
109
+ "outputPerToken": 0.0000044,
110
+ "cacheReadPerToken": 0.000000275
111
+ },
112
+ "gemini-2.5-pro": {
113
+ "inputPerToken": 0.00000125,
114
+ "outputPerToken": 0.00001,
115
+ "cacheReadPerToken": 0.0000003125
116
+ },
117
+ "gemini-2.5-flash": {
118
+ "inputPerToken": 0.0000003,
119
+ "outputPerToken": 0.0000025,
120
+ "cacheReadPerToken": 0.000000075
121
+ },
122
+ "gemini-2.5-flash-lite": {
123
+ "inputPerToken": 0.0000001,
124
+ "outputPerToken": 0.0000004,
125
+ "cacheReadPerToken": 0.000000025
126
+ },
127
+ "gemini-1.5-pro": {
128
+ "inputPerToken": 0.00000125,
129
+ "outputPerToken": 0.000005,
130
+ "cacheReadPerToken": 0.0000003125
131
+ },
132
+ "gemini-1.5-flash": {
133
+ "inputPerToken": 0.000000075,
134
+ "outputPerToken": 0.0000003,
135
+ "cacheReadPerToken": 0.00000001875
136
+ }
137
+ }
138
+ }
@@ -0,0 +1,17 @@
1
+ /** Per-token USD prices for a single model. Cache fields optional (not all vendors expose them). */
2
+ export interface ModelPricing {
3
+ inputPerToken: number;
4
+ outputPerToken: number;
5
+ cacheReadPerToken?: number;
6
+ cacheWritePerToken?: number;
7
+ }
8
+ /** Date-stamped version of the pricing table (e.g. "2026-06-24"). */
9
+ export declare const PRICING_VERSION: string;
10
+ /**
11
+ * Resolve per-token pricing for a model id. Tolerant of vendor prefixes,
12
+ * version dashes, and date suffixes. Returns null when no canonical key is a
13
+ * substring of the normalized id (i.e. genuinely unknown model).
14
+ */
15
+ export declare function getModelPricing(modelId: string): ModelPricing | null;
16
+ /** List every canonical model id that carries a price. */
17
+ export declare function listPricedModels(): string[];
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Offline, versioned per-model pricing table.
3
+ *
4
+ * The canonical data lives in `prices.json` (LiteLLM-style per-token USD map)
5
+ * and is imported with a `type: json` attribute so it survives `tsc` emit AND
6
+ * Node ESM's import-attribute requirement at runtime (the package is ESM).
7
+ *
8
+ * `getModelPricing` is prefix/suffix-tolerant: real model identifiers carry
9
+ * vendor prefixes (`us.anthropic.`), version dashes (`claude-opus-4-8`), and
10
+ * date suffixes (`-20250514`), none of which appear in the canonical keys. We
11
+ * normalize the input, then match against the LONGEST canonical key the
12
+ * normalized id contains so `claude-opus-4` wins over a hypothetical
13
+ * `claude-opus` when both are present.
14
+ */
15
+ import pricesData from './prices.json' with { type: 'json' };
16
+ const PRICES = pricesData;
17
+ /** Date-stamped version of the pricing table (e.g. "2026-06-24"). */
18
+ export const PRICING_VERSION = PRICES.version;
19
+ const MODELS = PRICES.models;
20
+ // Canonical keys sorted longest-first so containment matching prefers the most
21
+ // specific key (e.g. "gemini-2.5-flash-lite" before "gemini-2.5-flash").
22
+ const KEYS_BY_LENGTH = Object.keys(MODELS).sort((a, b) => b.length - a.length);
23
+ /**
24
+ * Normalize a raw model id into the dash-delimited token space the canonical
25
+ * keys live in. Strips vendor prefixes (`anthropic/`, `us.anthropic.`,
26
+ * `models/`, `openai/`), lowercases, and collapses any non [a-z0-9.] run to a
27
+ * single dash so `claude-opus-4-8`, `Claude Opus 4`, and `claude.opus.4` all
28
+ * normalize to a comparable form.
29
+ */
30
+ function normalizeModelId(modelId) {
31
+ let id = modelId.trim().toLowerCase();
32
+ // Drop a leading vendor segment: "anthropic/claude-..", "us.anthropic.claude-..",
33
+ // "google/gemini-..", "models/gemini-..", "openai/gpt-..".
34
+ id = id.replace(/^[a-z]+\//, ''); // "anthropic/x" -> "x"
35
+ id = id.replace(/^[a-z]+\.[a-z]+\./, ''); // "us.anthropic.x" -> "x"
36
+ id = id.replace(/^models\//, ''); // already handled, defensive
37
+ // Collapse separators to single dashes, keep dots (gpt-5.4) intact.
38
+ id = id.replace(/[\s_]+/g, '-').replace(/-+/g, '-');
39
+ return id;
40
+ }
41
+ /**
42
+ * Resolve per-token pricing for a model id. Tolerant of vendor prefixes,
43
+ * version dashes, and date suffixes. Returns null when no canonical key is a
44
+ * substring of the normalized id (i.e. genuinely unknown model).
45
+ */
46
+ export function getModelPricing(modelId) {
47
+ if (!modelId)
48
+ return null;
49
+ const norm = normalizeModelId(modelId);
50
+ // Exact key first (fast path + unambiguous).
51
+ if (MODELS[norm])
52
+ return MODELS[norm];
53
+ // Containment match, longest canonical key wins. The canonical key must
54
+ // appear as a dash-bounded prefix of the normalized id so "claude-opus-4"
55
+ // matches "claude-opus-4-8" and "claude-opus-4-20250514" but a stray
56
+ // "gpt-4" inside "gpt-40-turbo-experimental" still requires the boundary.
57
+ for (const key of KEYS_BY_LENGTH) {
58
+ if (norm === key || norm.startsWith(key + '-') || norm.startsWith(key + '.')) {
59
+ return MODELS[key];
60
+ }
61
+ }
62
+ // Last resort: canonical key contained anywhere (handles "anthropic-claude-opus-4"
63
+ // style ids the prefix strip missed). Still longest-first.
64
+ for (const key of KEYS_BY_LENGTH) {
65
+ if (norm.includes(key))
66
+ return MODELS[key];
67
+ }
68
+ return null;
69
+ }
70
+ /** List every canonical model id that carries a price. */
71
+ export function listPricedModels() {
72
+ return Object.keys(MODELS);
73
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * The secrets-agent: a local broker that holds resolved bundle env in memory
3
+ * after a single Touch ID unlock, so concurrent agent processes don't each pop
4
+ * their own prompt.
5
+ *
6
+ * Why this exists: every secret item carries a biometry access control, and
7
+ * macOS refuses to cache that across processes — N concurrent `agents run`
8
+ * spawns = N Touch ID prompts (see src/lib/secrets/bundles.ts). The Swift
9
+ * helper's LAContext only deduplicates reads *within one process*. This broker
10
+ * is the ssh-agent answer: `agents secrets unlock <bundle>` decrypts the bundle
11
+ * once (one prompt), ships the resolved env here, and every later read returns
12
+ * from memory over a user-only Unix socket — no prompt.
13
+ *
14
+ * Security model (deliberate): while a bundle is unlocked, any same-user
15
+ * process that can reach the socket reads it silently. That's strictly the same
16
+ * trust boundary the keychain already concedes (docs/secrets.md: the ACL is
17
+ * user-presence, not code-identity — any same-user process can pop the prompt
18
+ * and read), minus the visible prompt. We bound it with: explicit per-bundle
19
+ * opt-in (nothing is held unless you `unlock` it), an absolute TTL, auto-lock
20
+ * on screen-lock / sleep, and `agents secrets lock`. Nothing ever touches disk.
21
+ *
22
+ * macOS only: Linux libsecret has no biometry prompt, so there's nothing to
23
+ * deduplicate — every entry point here no-ops off darwin.
24
+ */
25
+ import type { SecretsBundle } from './bundles.js';
26
+ /** Default lifetime of an unlocked bundle when `--ttl` is not given. */
27
+ export declare const DEFAULT_TTL_MS: number;
28
+ export interface StoredBundle {
29
+ bundle: SecretsBundle;
30
+ env: Record<string, string>;
31
+ /** epoch ms; the entry is gone once Date.now() passes this. */
32
+ expiresAt: number;
33
+ }
34
+ /** One unlocked bundle as reported by `status`. */
35
+ export interface AgentStatusEntry {
36
+ name: string;
37
+ expiresAt: number;
38
+ keyCount: number;
39
+ }
40
+ export type Request = {
41
+ cmd: 'ping';
42
+ } | {
43
+ cmd: 'get';
44
+ name: string;
45
+ } | {
46
+ cmd: 'load';
47
+ name: string;
48
+ bundle: SecretsBundle;
49
+ env: Record<string, string>;
50
+ ttlMs: number;
51
+ } | {
52
+ cmd: 'lock';
53
+ name?: string;
54
+ } | {
55
+ cmd: 'status';
56
+ };
57
+ export type Response = {
58
+ ok: true;
59
+ cmd: 'ping';
60
+ version: number;
61
+ } | {
62
+ ok: true;
63
+ cmd: 'get';
64
+ hit: false;
65
+ } | {
66
+ ok: true;
67
+ cmd: 'get';
68
+ hit: true;
69
+ bundle: SecretsBundle;
70
+ env: Record<string, string>;
71
+ } | {
72
+ ok: true;
73
+ cmd: 'load';
74
+ } | {
75
+ ok: true;
76
+ cmd: 'lock';
77
+ wiped: number;
78
+ } | {
79
+ ok: true;
80
+ cmd: 'status';
81
+ entries: AgentStatusEntry[];
82
+ } | {
83
+ ok: false;
84
+ error: string;
85
+ };
86
+ /**
87
+ * Pure request handler over the in-memory store. Extracted so the store
88
+ * semantics (lazy expiry on get/status, lock-one vs lock-all, load TTL) are
89
+ * unit-testable with a controlled `now`, without a socket or a spawned process.
90
+ * Mutates `store` in place; returns the wire response.
91
+ */
92
+ export declare function handleAgentRequest(store: Map<string, StoredBundle>, req: Request, now?: number): Response;
93
+ /**
94
+ * Run the broker in the foreground. Spawned detached by ensureAgentRunning via
95
+ * `agents secrets _agent-run`. Holds the store in memory, serves the socket,
96
+ * sweeps expired entries, wipes on screen-lock/sleep, and self-exits when idle.
97
+ */
98
+ export declare function runSecretsAgent(): Promise<void>;
99
+ /** True if a broker socket exists at all. Cheap; gates the sync read so the
100
+ * never-unlocked path stays a single stat. */
101
+ export declare function agentSocketExists(): boolean;
102
+ /**
103
+ * Synchronous read for the hot path. Returns the cached resolved bundle, or
104
+ * null if the agent isn't running / doesn't hold this bundle / anything fails
105
+ * (soft — caller falls through to the real keychain). macOS only.
106
+ */
107
+ export declare function agentGetSync(name: string): {
108
+ bundle: SecretsBundle;
109
+ env: Record<string, string>;
110
+ } | null;
111
+ /** True when `secrets.agent.auto` is enabled in agents.yaml. Best-effort; a
112
+ * missing/unreadable meta reads as off. */
113
+ export declare function secretsAgentAutoEnabled(): boolean;
114
+ /**
115
+ * Fire-and-forget: populate the broker with a freshly-resolved bundle so the
116
+ * NEXT process reads it without a prompt. Used by the auto-cache path after a
117
+ * real keychain read of a `session`-tier bundle. Adds no latency to the caller
118
+ * — it spawns the agent (if needed) and a detached loader, both unref'd, then
119
+ * returns immediately. Entirely best-effort; never throws. macOS only.
120
+ */
121
+ export declare function agentAutoLoadSync(name: string, bundle: SecretsBundle, env: Record<string, string>, ttlMs: number): void;
122
+ /** Store a resolved bundle in the broker. Returns false on transport failure. */
123
+ export declare function agentLoad(name: string, bundle: SecretsBundle, env: Record<string, string>, ttlMs: number): Promise<boolean>;
124
+ /** Wipe one bundle (or all if name omitted) from the broker. Returns the count
125
+ * wiped, or 0 when no broker is running. */
126
+ export declare function agentLock(name?: string): Promise<number>;
127
+ /** List currently-unlocked bundles, or [] when no broker is running. */
128
+ export declare function agentStatus(): Promise<AgentStatusEntry[]>;
129
+ /**
130
+ * Ensure a broker is running and reachable, spawning one detached if not.
131
+ * Returns true once the socket answers a ping. On protocol-version skew, kills
132
+ * the stale broker and respawns. macOS only.
133
+ */
134
+ export declare function ensureAgentRunning(timeoutMs?: number): Promise<boolean>;