@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.
- package/CHANGELOG.md +19 -0
- package/README.md +1 -1
- package/dist/commands/budget.d.ts +14 -0
- package/dist/commands/budget.js +137 -0
- package/dist/commands/cost.d.ts +12 -0
- package/dist/commands/cost.js +139 -0
- package/dist/commands/exec.d.ts +20 -0
- package/dist/commands/exec.js +382 -5
- package/dist/commands/secrets.d.ts +15 -0
- package/dist/commands/secrets.js +250 -4
- package/dist/commands/sessions.js +4 -0
- package/dist/commands/sync.d.ts +10 -3
- package/dist/commands/sync.js +72 -9
- package/dist/index.js +4 -0
- package/dist/lib/budget/config.d.ts +9 -0
- package/dist/lib/budget/config.js +115 -0
- package/dist/lib/budget/enforce.d.ts +94 -0
- package/dist/lib/budget/enforce.js +151 -0
- package/dist/lib/budget/ledger.d.ts +61 -0
- package/dist/lib/budget/ledger.js +107 -0
- package/dist/lib/budget/preflight.d.ts +110 -0
- package/dist/lib/budget/preflight.js +200 -0
- package/dist/lib/checkpoint.d.ts +54 -0
- package/dist/lib/checkpoint.js +56 -0
- package/dist/lib/cloud/rush.js +18 -0
- package/dist/lib/exec.d.ts +36 -0
- package/dist/lib/exec.js +192 -4
- package/dist/lib/git.d.ts +18 -0
- package/dist/lib/git.js +67 -4
- package/dist/lib/hooks.js +12 -0
- package/dist/lib/loop.d.ts +145 -0
- package/dist/lib/loop.js +330 -0
- package/dist/lib/mcp.d.ts +7 -0
- package/dist/lib/mcp.js +24 -0
- package/dist/lib/models.d.ts +11 -0
- package/dist/lib/models.js +21 -0
- package/dist/lib/plugin-marketplace.js +16 -6
- package/dist/lib/plugins.js +5 -2
- package/dist/lib/pricing/cost.d.ts +46 -0
- package/dist/lib/pricing/cost.js +71 -0
- package/dist/lib/pricing/index.d.ts +8 -0
- package/dist/lib/pricing/index.js +8 -0
- package/dist/lib/pricing/prices.json +138 -0
- package/dist/lib/pricing/table.d.ts +17 -0
- package/dist/lib/pricing/table.js +73 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +134 -0
- package/dist/lib/secrets/agent.js +501 -0
- package/dist/lib/secrets/bundles.d.ts +21 -0
- package/dist/lib/secrets/bundles.js +43 -0
- package/dist/lib/secrets/drivers/rush.d.ts +14 -0
- package/dist/lib/secrets/drivers/rush.js +84 -0
- package/dist/lib/secrets/linux.js +88 -10
- package/dist/lib/secrets/sync-backend.d.ts +48 -0
- package/dist/lib/secrets/sync-backend.js +13 -0
- package/dist/lib/secrets/sync.d.ts +15 -23
- package/dist/lib/secrets/sync.js +31 -66
- package/dist/lib/session/db.d.ts +40 -0
- package/dist/lib/session/db.js +84 -2
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +126 -2
- package/dist/lib/session/render.d.ts +2 -0
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/types.d.ts +4 -0
- package/dist/lib/sync-umbrella.d.ts +76 -0
- package/dist/lib/sync-umbrella.js +125 -0
- package/dist/lib/teams/agents.d.ts +32 -0
- package/dist/lib/teams/agents.js +66 -3
- package/dist/lib/teams/api.js +20 -0
- package/dist/lib/teams/parsers.js +16 -4
- package/dist/lib/types.d.ts +48 -0
- package/dist/lib/workflows.d.ts +56 -0
- package/dist/lib/workflows.js +72 -5
- package/package.json +2 -1
package/dist/lib/models.js
CHANGED
|
@@ -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(`
|
|
216
|
-
`
|
|
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(`
|
|
221
|
-
`(e.g. "./${field}/${entry}"). Claude Code rejects the
|
|
222
|
-
`
|
|
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
|
-
|
|
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,
|
package/dist/lib/plugins.js
CHANGED
|
@@ -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
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -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>;
|