segreant 0.1.0 → 0.2.0
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/LICENSING.md +5 -3
- package/README.md +57 -19
- package/bin/segreant-statusline.mjs +20 -0
- package/bin/segreant.mjs +19 -8
- package/dist/alerts/detect.js +13 -0
- package/dist/billing/epistemic.js +4 -0
- package/dist/billing/importer.js +29 -10
- package/dist/billing/reconcile.js +122 -32
- package/dist/billing/scope.js +7 -0
- package/dist/budget/guard.js +33 -4
- package/dist/cli/allocCmd.js +73 -4
- package/dist/cli/askModelPrices.js +59 -0
- package/dist/cli/backupCmd.js +2 -1
- package/dist/cli/basisLines.js +45 -0
- package/dist/cli/billingCmd.js +68 -9
- package/dist/cli/capitalCmd.js +2 -1
- package/dist/cli/connectCmd.js +50 -9
- package/dist/cli/egressCmd.js +15 -3
- package/dist/cli/evidenceCmd.js +6 -3
- package/dist/cli/flags.js +68 -0
- package/dist/cli/homeCmd.js +117 -0
- package/dist/cli/importCmd.js +210 -54
- package/dist/cli/launchCmd.js +2 -1
- package/dist/cli/opsCmd.js +49 -2
- package/dist/cli/planCmd.js +181 -0
- package/dist/cli/pricingDeclareCmd.js +178 -0
- package/dist/cli/quotaCmd.js +190 -0
- package/dist/cli/runCmd.js +203 -51
- package/dist/cli/showCmd.js +37 -11
- package/dist/cli/teamCmd.js +5 -5
- package/dist/cli/ui.js +24 -8
- package/dist/cli/valueCmd.js +189 -14
- package/dist/cli.js +102 -21
- package/dist/config.js +95 -6
- package/dist/connect/antigravity.js +310 -0
- package/dist/connect/claudeCode.js +43 -3
- package/dist/connect/codex.js +38 -1
- package/dist/connect/importShared.js +58 -5
- package/dist/connect/opencode.js +37 -9
- package/dist/connect/protobuf.js +126 -0
- package/dist/connect/registry.js +46 -0
- package/dist/connect/repoRoot.js +112 -0
- package/dist/cost/basis.js +168 -0
- package/dist/cost/declarations.js +37 -0
- package/dist/cost/exactPricing.js +4 -1
- package/dist/cost/firstSight.js +98 -0
- package/dist/cost/overrides.js +230 -0
- package/dist/cost/pricing.js +228 -15
- package/dist/cost/reprice.js +130 -0
- package/dist/dashboard/claim-support.js +11 -8
- package/dist/dashboard/contracts.js +23 -3
- package/dist/dashboard/routes.js +980 -112
- package/dist/dashboard/sessions.js +345 -0
- package/dist/dashboard/settings.js +2 -1
- package/dist/dashboard/value-cache.js +105 -0
- package/dist/dashboard/web/app/components/claimInspector.js +6 -5
- package/dist/dashboard/web/app/components/drawer.js +105 -35
- package/dist/dashboard/web/app/components/ruleFields.js +144 -0
- package/dist/dashboard/web/app/components/spine.js +54 -24
- package/dist/dashboard/web/app/components/workflow.js +82 -0
- package/dist/dashboard/web/app/core/actions.js +734 -23
- package/dist/dashboard/web/app/core/api.js +37 -1
- package/dist/dashboard/web/app/core/chain.js +11 -4
- package/dist/dashboard/web/app/core/claimLayers.js +179 -51
- package/dist/dashboard/web/app/core/claimTypes.js +30 -0
- package/dist/dashboard/web/app/core/fmt.js +19 -2
- package/dist/dashboard/web/app/core/generated-contract.js +23 -3
- package/dist/dashboard/web/app/core/generated-payload-contract.js +1944 -465
- package/dist/dashboard/web/app/core/generated-types.js +1 -1
- package/dist/dashboard/web/app/core/griffin.js +10 -0
- package/dist/dashboard/web/app/core/jobs.js +12 -0
- package/dist/dashboard/web/app/core/period.js +35 -0
- package/dist/dashboard/web/app/core/periodWords.js +10 -0
- package/dist/dashboard/web/app/core/periods.js +32 -0
- package/dist/dashboard/web/app/core/pricingActions.js +264 -0
- package/dist/dashboard/web/app/core/privacyText.js +78 -0
- package/dist/dashboard/web/app/core/reconcileWords.js +63 -0
- package/dist/dashboard/web/app/core/registry.js +79 -46
- package/dist/dashboard/web/app/core/slug.js +9 -0
- package/dist/dashboard/web/app/core/theme.js +48 -0
- package/dist/dashboard/web/app/core/view.js +69 -0
- package/dist/dashboard/web/app/main.js +197 -15
- package/dist/dashboard/web/app/views/allocation.js +249 -61
- package/dist/dashboard/web/app/views/control.js +57 -16
- package/dist/dashboard/web/app/views/data.js +4 -4
- package/dist/dashboard/web/app/views/evidence.js +336 -87
- package/dist/dashboard/web/app/views/privacy.js +13 -0
- package/dist/dashboard/web/app/views/spend.js +30 -15
- package/dist/dashboard/web/app/views/statement.js +189 -0
- package/dist/dashboard/web/app/views/system.js +44 -4
- package/dist/dashboard/web/app/views/value.js +105 -59
- package/dist/dashboard/web/app/views/workbench.js +312 -0
- package/dist/dashboard/web/classic.html +16 -4
- package/dist/dashboard/web/index.html +1 -1
- package/dist/dashboard/web/styles/app.css +536 -23
- package/dist/demo/seed.js +153 -14
- package/dist/economics/ledger.js +1 -1
- package/dist/egress/policy.js +129 -5
- package/dist/export/allocationCsv.js +70 -0
- package/dist/export/billingCsv.js +6 -2
- package/dist/export/csv.js +10 -2
- package/dist/export/economic.js +6 -2
- package/dist/export/statementCsv.js +104 -0
- package/dist/git/correlate.js +86 -9
- package/dist/git/quality.js +107 -24
- package/dist/git/repoScope.js +207 -0
- package/dist/git/survivalStore.js +186 -0
- package/dist/guide.js +9 -4
- package/dist/plans/detect.js +147 -0
- package/dist/proxy/server.js +134 -14
- package/dist/proxy/usage.js +9 -4
- package/dist/quota/limits.js +138 -0
- package/dist/quota/statusline.js +68 -0
- package/dist/quota/view.js +122 -0
- package/dist/scan/knownApps.js +6 -2
- package/dist/scan/scan.js +9 -0
- package/dist/store/allocation.js +14 -0
- package/dist/store/db.js +259 -34
- package/dist/store/realization.js +78 -14
- package/dist/store/schema.js +59 -1
- package/dist/store/scopeEvidence.js +67 -0
- package/dist/util/pool.js +19 -0
- package/dist/util/time.js +7 -0
- package/dist/value/realization.js +321 -30
- package/dist/value/report.js +28 -8
- package/dist/value/selfReported.js +10 -9
- package/docs/ARCHITECTURE.md +8 -4
- package/docs/CAPABILITY-EVIDENCE-CONTRACT.md +33 -11
- package/docs/CLI-JSON-CONTRACT.md +38 -0
- package/docs/DATA-BOUNDARIES.md +66 -6
- package/docs/FAQ.md +43 -9
- package/docs/GETTING-STARTED.md +19 -12
- package/docs/GUIDE.md +81 -18
- package/docs/INTEGRATIONS.md +9 -9
- package/docs/METHODOLOGY.md +3 -2
- package/docs/NEUTRALITY.md +1 -1
- package/docs/PRIVACY.md +51 -0
- package/docs/README.md +1 -0
- package/docs/RELEASE-GATE.md +52 -7
- package/docs/THREAT-MODEL.md +2 -1
- package/package.json +3 -2
- package/pricing/models.json +1366 -60
package/dist/cli/runCmd.js
CHANGED
|
@@ -11,12 +11,58 @@ import { createDashboardServer } from '../dashboard/server.js';
|
|
|
11
11
|
import { loadConfig, mutateConfig, dbPath, demoDbPath, isDemo, unlinkDemoDb, segreantHome } from '../config.js';
|
|
12
12
|
import { packageVersion } from '../version.js';
|
|
13
13
|
import { seedDemo } from '../demo/seed.js';
|
|
14
|
-
import { startOfLocalDay } from '../budget/guard.js';
|
|
14
|
+
import { capBasisSpend, startOfLocalDay } from '../budget/guard.js';
|
|
15
|
+
import { previewReprice, applyReprice } from '../cost/reprice.js';
|
|
15
16
|
import { pricingCoverage } from '../cost/coverage.js';
|
|
16
|
-
import { refreshPricing, pricingStatus,
|
|
17
|
+
import { refreshPricing, pricingStatus, DEFAULT_MANIFEST_URL } from '../cost/pricing.js';
|
|
17
18
|
import { refreshBaselineManifest, baselineManifestStatus } from '../value/liftBaseline.js';
|
|
19
|
+
import { createFirstSightWatcher } from '../cost/firstSight.js';
|
|
18
20
|
import { C, color, usd, num, printJson } from './ui.js';
|
|
19
21
|
import {} from './flags.js';
|
|
22
|
+
import { capConfigured } from './launchCmd.js';
|
|
23
|
+
/**
|
|
24
|
+
* Fill an empty sample ledger before the dashboard serves it. `start --demo`
|
|
25
|
+
* used to serve whatever demo.db held, so on a fresh install every page said
|
|
26
|
+
* "sample data" over $0 and "no recorded requests". demo.db only ever holds
|
|
27
|
+
* synthetic rows, so seeding it never touches real metering; a ledger that
|
|
28
|
+
* already has sample rows is left exactly as it is.
|
|
29
|
+
*/
|
|
30
|
+
export function ensureDemoSeeded(store) {
|
|
31
|
+
if (store.earliestRequestMs() !== null)
|
|
32
|
+
return null;
|
|
33
|
+
return seedDemo(store);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Listen, or say why not. A busy port used to surface as Node's unhandled
|
|
37
|
+
* 'error' event and a stack trace; the usual cause is a Segreant that is
|
|
38
|
+
* already running, and the fix is one flag.
|
|
39
|
+
*/
|
|
40
|
+
export function listenOrExplain(server, port, what, flag) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
const onError = (error) => {
|
|
43
|
+
server.off('listening', onListening);
|
|
44
|
+
if (error.code === 'EADDRINUSE') {
|
|
45
|
+
console.error(` Port ${port} (${what}) is already in use, so Segreant did not start.`);
|
|
46
|
+
console.error(` Another Segreant may be running: open http://localhost:${port} to check.`);
|
|
47
|
+
console.error(` Otherwise pick a free port: segreant start ${flag} ${port + 10}`);
|
|
48
|
+
}
|
|
49
|
+
else if (error.code === 'EACCES') {
|
|
50
|
+
console.error(` Port ${port} (${what}) needs permission this account does not have. Pick another: segreant start ${flag} ${port + 10}`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
console.error(` The ${what} could not listen on 127.0.0.1:${port}: ${error.message}`);
|
|
54
|
+
}
|
|
55
|
+
resolve(false);
|
|
56
|
+
};
|
|
57
|
+
const onListening = () => {
|
|
58
|
+
server.off('error', onError);
|
|
59
|
+
resolve(true);
|
|
60
|
+
};
|
|
61
|
+
server.once('error', onError);
|
|
62
|
+
server.once('listening', onListening);
|
|
63
|
+
server.listen(port, '127.0.0.1');
|
|
64
|
+
});
|
|
65
|
+
}
|
|
20
66
|
export async function cmdStart(flags) {
|
|
21
67
|
const cfg = loadConfig();
|
|
22
68
|
if (flags.port)
|
|
@@ -25,6 +71,11 @@ export async function cmdStart(flags) {
|
|
|
25
71
|
cfg.dashboardPort = Number(flags['dashboard-port']);
|
|
26
72
|
const store = new Store(dbPath());
|
|
27
73
|
const tty = process.stdout.isTTY ?? false;
|
|
74
|
+
if (isDemo()) {
|
|
75
|
+
const seeded = ensureDemoSeeded(store);
|
|
76
|
+
if (seeded)
|
|
77
|
+
console.log(color(tty, C.gray, ` Sample data generated: ${num(seeded.requests)} requests over ${seeded.days} days (same as segreant demo).`));
|
|
78
|
+
}
|
|
28
79
|
// The proposal store may contain literal suggested code. Honor its short
|
|
29
80
|
// retention boundary whenever the local service starts; this does not touch
|
|
30
81
|
// the longer-lived cost ledger, and an operator can still clear proposals
|
|
@@ -44,29 +95,94 @@ export async function cmdStart(flags) {
|
|
|
44
95
|
? color(tty, C.gray, ` ↻ pricing refreshed — ${res.modelCount} models (${cfg.pricing.manifestUrl ?? DEFAULT_MANIFEST_URL})`)
|
|
45
96
|
: color(tty, C.yellow, ` ↻ pricing auto-refresh skipped: ${res.error ?? 'unreachable'} — keeping current table`));
|
|
46
97
|
}
|
|
47
|
-
|
|
98
|
+
// With --demo the open store is demo.db. A proxy started here recorded
|
|
99
|
+
// real provider traffic into the sample store while the banner said "Real
|
|
100
|
+
// metering is untouched", so demo mode serves the dashboard only. A tool
|
|
101
|
+
// started with `segreant launch` then finds no proxy, warns that it is
|
|
102
|
+
// unmetered and goes direct, as it does whenever Segreant is not running.
|
|
103
|
+
const demo = isDemo();
|
|
104
|
+
// A model priced by a guess sends one lookup to the public price list (never
|
|
105
|
+
// in the request's path). Rows are not rewritten: the user is told, and
|
|
106
|
+
// `segreant reprice` previews the correction.
|
|
107
|
+
const firstSight = createFirstSightWatcher({
|
|
108
|
+
// Read from the file at each sighting: `segreant pricing --auto off` in
|
|
109
|
+
// another terminal must stop the next lookup without a restart (PR #146
|
|
110
|
+
// review). A config that no longer reads is treated as off.
|
|
111
|
+
enabled: () => {
|
|
112
|
+
try {
|
|
113
|
+
// Only against the built-in public list, which is what the banner and
|
|
114
|
+
// the privacy statement disclose: an operator who points
|
|
115
|
+
// pricing.manifestUrl at their own card refreshes it themselves
|
|
116
|
+
// (PR #146 review).
|
|
117
|
+
const pricing = loadConfig().pricing;
|
|
118
|
+
return pricing.lookupUnknownModels && pricingSourceLabel(pricing.manifestUrl) === 'the public price list';
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
// The source the live config selects, not the one at start: enabled() has
|
|
125
|
+
// just confirmed it is the built-in list, and a source changed since then
|
|
126
|
+
// is never fetched behind the operator's back (PR #146 review).
|
|
127
|
+
refresh: async () => {
|
|
128
|
+
let url;
|
|
129
|
+
try {
|
|
130
|
+
url = loadConfig().pricing.manifestUrl;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return { ok: false, error: 'the configuration could not be read' };
|
|
134
|
+
}
|
|
135
|
+
if (pricingSourceLabel(url) !== 'the public price list')
|
|
136
|
+
return { ok: false, error: 'the price source is no longer the public price list' };
|
|
137
|
+
return refreshPricing(DEFAULT_MANIFEST_URL);
|
|
138
|
+
},
|
|
139
|
+
onLearned: (models) => {
|
|
140
|
+
const names = models.map((m) => m.model).join(', ');
|
|
141
|
+
// refresh() above fetches only the built-in list, so that is the source
|
|
142
|
+
// that learned them, whatever the config said at start (PR #146 review).
|
|
143
|
+
console.log(color(tty, C.green, ` ↻ the public price list now prices ${names}: new requests use that price.`));
|
|
144
|
+
console.log(color(tty, C.gray, ' Requests already recorded keep their estimate until you run: segreant reprice (preview), then segreant reprice --apply'));
|
|
145
|
+
},
|
|
146
|
+
onFailed: (error) => {
|
|
147
|
+
console.log(color(tty, C.yellow, ` ↻ price lookup skipped: ${error}. Estimated prices stand; segreant pricing set <model> gives one yourself.`));
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
const proxy = demo ? null : createProxyServer({
|
|
48
151
|
store,
|
|
49
152
|
config: cfg,
|
|
50
153
|
onLog: (row) => {
|
|
154
|
+
if (row.pricing)
|
|
155
|
+
firstSight.note(row.provider, row.model, row.pricing.rateMatchKind);
|
|
51
156
|
if (row.statusCode === 429) {
|
|
52
157
|
console.log(color(tty, C.red, ` ⛔ blocked ${row.provider}/${row.model} (budget)`));
|
|
53
158
|
return;
|
|
54
159
|
}
|
|
55
|
-
const today = store.spendBetween(startOfLocalDay(), Date.now() + 1000);
|
|
56
160
|
const time = new Date(row.tsEpochMs).toLocaleTimeString('en-US', { hour12: false });
|
|
57
|
-
|
|
161
|
+
// A price you set is not a guess, and not a list price either (PR #146 sweep).
|
|
162
|
+
const est = row.estimated ? color(tty, C.yellow, ' ~est')
|
|
163
|
+
: row.pricing?.costBasis === 'user_declared_price' ? color(tty, C.gray, ' your price') : '';
|
|
58
164
|
console.log(` ${color(tty, C.gray, time)} ${row.provider}/${row.model} ` +
|
|
59
165
|
`in ${num(row.inputTokens)} out ${num(row.outputTokens)} ` +
|
|
60
|
-
`${color(tty, C.green, usd(row.costUsd))}${est} ${color(tty, C.gray,
|
|
166
|
+
`${color(tty, C.green, usd(row.costUsd))}${est} ${color(tty, C.gray, liveTodayLine(store, cfg.budget))}`);
|
|
61
167
|
},
|
|
62
168
|
});
|
|
63
169
|
const dashboard = createDashboardServer({ store, config: cfg, version: packageVersion() });
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
170
|
+
const listening = (!proxy || await listenOrExplain(proxy, cfg.port, 'proxy', '--port'))
|
|
171
|
+
&& await listenOrExplain(dashboard, cfg.dashboardPort, 'dashboard', '--dashboard-port');
|
|
172
|
+
if (!listening) {
|
|
173
|
+
proxy?.close();
|
|
174
|
+
dashboard.close();
|
|
175
|
+
store.close();
|
|
176
|
+
process.exitCode = 1;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (demo)
|
|
180
|
+
printDemoBanner(cfg, tty);
|
|
181
|
+
else
|
|
182
|
+
printBanner(cfg, tty);
|
|
67
183
|
const shutdown = () => {
|
|
68
184
|
console.log('\n Shutting down…');
|
|
69
|
-
proxy
|
|
185
|
+
proxy?.close();
|
|
70
186
|
dashboard.close();
|
|
71
187
|
store.close();
|
|
72
188
|
process.exit(0);
|
|
@@ -74,6 +190,20 @@ export async function cmdStart(flags) {
|
|
|
74
190
|
process.on('SIGINT', shutdown);
|
|
75
191
|
process.on('SIGTERM', shutdown);
|
|
76
192
|
}
|
|
193
|
+
function printDemoBanner(cfg, tty) {
|
|
194
|
+
const line = (s) => console.log(' ' + s);
|
|
195
|
+
console.log('');
|
|
196
|
+
line(color(tty, C.bold, 'Segreant · sample-data dashboard'));
|
|
197
|
+
console.log('');
|
|
198
|
+
line(`Dashboard ${color(tty, C.cyan, `http://localhost:${cfg.dashboardPort}`)}`);
|
|
199
|
+
console.log('');
|
|
200
|
+
line(color(tty, C.yellow, '● DEMO DATA — synthetic, isolated in demo.db. No proxy runs in demo mode,'));
|
|
201
|
+
line(color(tty, C.yellow, ' so nothing you send to a provider is recorded here. Real metering is untouched.'));
|
|
202
|
+
line(color(tty, C.gray, ' To meter your own tools, run "segreant start" without --demo.'));
|
|
203
|
+
console.log('');
|
|
204
|
+
line(color(tty, C.gray, 'Press Ctrl+C to stop.'));
|
|
205
|
+
console.log('');
|
|
206
|
+
}
|
|
77
207
|
function printBanner(cfg, tty) {
|
|
78
208
|
const line = (s) => console.log(' ' + s);
|
|
79
209
|
console.log('');
|
|
@@ -90,7 +220,9 @@ function printBanner(cfg, tty) {
|
|
|
90
220
|
}
|
|
91
221
|
line(color(tty, C.bold, 'Start your tools through Segreant:'));
|
|
92
222
|
line(color(tty, C.gray, ' segreant launch -- claude (or codex, aider, opencode, …)'));
|
|
93
|
-
line(color(tty, C.gray,
|
|
223
|
+
line(color(tty, C.gray, capConfigured(cfg)
|
|
224
|
+
? ' Metered while this proxy runs. With a cap set, launch refuses while it is stopped (--allow-unmetered overrides).'
|
|
225
|
+
: ' Metered while this proxy runs; starts unmetered with a warning when it does not.'));
|
|
94
226
|
line(color(tty, C.bold, 'Or point a tool here yourself (PowerShell):'));
|
|
95
227
|
line(color(tty, C.gray, ` $env:ANTHROPIC_BASE_URL="http://localhost:${cfg.port}"`));
|
|
96
228
|
line(color(tty, C.gray, ` $env:OPENAI_BASE_URL="http://localhost:${cfg.port}/v1"`));
|
|
@@ -101,11 +233,46 @@ function printBanner(cfg, tty) {
|
|
|
101
233
|
else {
|
|
102
234
|
line(color(tty, C.gray, 'No budget cap set. Add one: segreant budget --daily 25 --soft 18'));
|
|
103
235
|
}
|
|
236
|
+
// The one request Segreant makes on its own: said where the user starts it.
|
|
237
|
+
if (cfg.pricing.lookupUnknownModels && pricingSourceLabel(cfg.pricing.manifestUrl) === 'the public price list') {
|
|
238
|
+
line(color(tty, C.gray, 'Prices a model missing from the rate card is looked up in the public price list'));
|
|
239
|
+
line(color(tty, C.gray, ' (a plain download; sends nothing about you). Off: segreant pricing --auto off'));
|
|
240
|
+
}
|
|
241
|
+
else if (cfg.pricing.lookupUnknownModels) {
|
|
242
|
+
line(color(tty, C.gray, `Prices ${pricingSourceLabel(cfg.pricing.manifestUrl)} is not looked up automatically;`));
|
|
243
|
+
line(color(tty, C.gray, ' refresh it with: segreant pricing --refresh'));
|
|
244
|
+
}
|
|
104
245
|
console.log('');
|
|
105
246
|
line(color(tty, C.gray, 'Press Ctrl+C to stop. A tool pointed at this address by hand cannot connect while'));
|
|
106
247
|
line(color(tty, C.gray, 'Segreant is stopped; unset the two variables, or use segreant launch, to go direct.'));
|
|
107
248
|
console.log('');
|
|
108
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* The source a price refresh reads, in words. The built-in list is spelled two
|
|
252
|
+
* ways, no URL configured or its own URL; both are the public price list (PR #146 review).
|
|
253
|
+
*/
|
|
254
|
+
/**
|
|
255
|
+
* The running total on each live log line. With a daily cap it is the spend the
|
|
256
|
+
* cap counts, beside the cap: the all-sources total read "today $51.29" next to
|
|
257
|
+
* a $25 cap while requests went through, because imported spend does not count
|
|
258
|
+
* toward the cap by default (launch pass, 2026-10-18). Any other spend today is
|
|
259
|
+
* named separately, never added in.
|
|
260
|
+
*/
|
|
261
|
+
export function liveTodayLine(store, budget, nowMs = Date.now()) {
|
|
262
|
+
const start = startOfLocalDay(nowMs);
|
|
263
|
+
const all = store.spendBetween(start, nowMs + 1000);
|
|
264
|
+
if (budget.dailyUsd === null)
|
|
265
|
+
return `today ${usd(all)}`;
|
|
266
|
+
const counted = capBasisSpend(store, budget, start, nowMs + 1000);
|
|
267
|
+
const other = all - counted.usd;
|
|
268
|
+
return `today ${usd(counted.usd)} of the ${usd(budget.dailyUsd)} cap`
|
|
269
|
+
+ (other >= 0.005 ? ` (+${usd(other)} imported, not counted)` : '');
|
|
270
|
+
}
|
|
271
|
+
export function pricingSourceLabel(manifestUrl) {
|
|
272
|
+
return manifestUrl === null || manifestUrl === undefined || manifestUrl === DEFAULT_MANIFEST_URL
|
|
273
|
+
? 'the public price list'
|
|
274
|
+
: `your price source (${manifestUrl})`;
|
|
275
|
+
}
|
|
109
276
|
export async function cmdDemo(flags) {
|
|
110
277
|
const tty = process.stdout.isTTY ?? false;
|
|
111
278
|
if (flags.clear) {
|
|
@@ -141,6 +308,10 @@ export async function cmdDemo(flags) {
|
|
|
141
308
|
}
|
|
142
309
|
}
|
|
143
310
|
export async function cmdPricing(flags) {
|
|
311
|
+
const action = flags._[0];
|
|
312
|
+
if (action === 'set' || action === 'unset' || action === 'unknown') {
|
|
313
|
+
return (await import('./pricingDeclareCmd.js')).cmdPricingDeclare(action, flags);
|
|
314
|
+
}
|
|
144
315
|
const cfg = loadConfig();
|
|
145
316
|
const on = process.stdout.isTTY ?? false;
|
|
146
317
|
// Ledger evidence is deliberately a separate read-only surface from the
|
|
@@ -200,15 +371,18 @@ export async function cmdPricing(flags) {
|
|
|
200
371
|
// file — nothing about the user). `--auto off` turns it back off.
|
|
201
372
|
if (flags['auto'] !== undefined) {
|
|
202
373
|
const enable = flags['auto'] !== 'off' && flags['auto'] !== 'false';
|
|
203
|
-
|
|
374
|
+
// One switch for both: the age-based refresh and the first-sight lookup.
|
|
375
|
+
const updated = mutateConfig((latest) => ({ ...latest, pricing: { ...latest.pricing, autoRefresh: enable, lookupUnknownModels: enable } }));
|
|
204
376
|
console.log('');
|
|
205
377
|
if (enable) {
|
|
206
|
-
console.log(` ${color(on, C.green, '✓')} Auto-refresh ON — "segreant start" updates the rate card when it is older than ${cfg.pricing.maxAgeDays}d
|
|
378
|
+
console.log(` ${color(on, C.green, '✓')} Auto-refresh ON — "segreant start" updates the rate card when it is older than ${cfg.pricing.maxAgeDays}d,`);
|
|
379
|
+
console.log(' and a running Segreant looks up a model the card does not know the first time it sees it.');
|
|
207
380
|
console.log(` ${color(on, C.dim, `Source: ${updated.pricing.manifestUrl ?? DEFAULT_MANIFEST_URL}`)}`);
|
|
208
381
|
console.log(` ${color(on, C.dim, 'The fetch is a GET of a public pricing file — it sends nothing about you. Turn off: segreant pricing --auto off')}`);
|
|
209
382
|
}
|
|
210
383
|
else {
|
|
211
|
-
console.log(` ${color(on, C.green, '✓')} Auto-refresh OFF — the rate card only changes when you run "segreant pricing --refresh"
|
|
384
|
+
console.log(` ${color(on, C.green, '✓')} Auto-refresh OFF — the rate card only changes when you run "segreant pricing --refresh",`);
|
|
385
|
+
console.log(' and a model the card does not know stays at an estimate until you refresh or run segreant pricing set.');
|
|
212
386
|
}
|
|
213
387
|
console.log('');
|
|
214
388
|
return;
|
|
@@ -343,46 +517,19 @@ export function cmdReprice(flags) {
|
|
|
343
517
|
const store = new Store(dbPath());
|
|
344
518
|
const tty = process.stdout.isTTY ?? false;
|
|
345
519
|
try {
|
|
346
|
-
const
|
|
347
|
-
const updates =
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
for (const r of rows) {
|
|
351
|
-
const c = computeCost(r.provider, r.model, {
|
|
352
|
-
inputTokens: r.inputTokens,
|
|
353
|
-
outputTokens: r.outputTokens,
|
|
354
|
-
cacheWriteTokens: r.cacheWriteTokens,
|
|
355
|
-
cacheReadTokens: r.cacheReadTokens,
|
|
356
|
-
});
|
|
357
|
-
if (c.estimated) {
|
|
358
|
-
stillEstimated++;
|
|
359
|
-
continue; // still a guess — keep the original guess rather than churn numbers
|
|
360
|
-
}
|
|
361
|
-
updates.push({ requestId: r.requestId, costUsd: c.costUsd, pricing: c.pricing, ...(c.exact === undefined ? {} : { economicAmount: c.exact.total }) });
|
|
362
|
-
// Tuple key, not a delimiter join (D-217/D-241): a model name may carry '/'.
|
|
363
|
-
const key = JSON.stringify([r.provider, r.model]);
|
|
364
|
-
const agg = byModel.get(key) ?? { n: 0, before: 0, after: 0 };
|
|
365
|
-
agg.n++;
|
|
366
|
-
agg.before += r.costUsd;
|
|
367
|
-
agg.after += c.costUsd;
|
|
368
|
-
byModel.set(key, agg);
|
|
369
|
-
}
|
|
370
|
-
const before = [...byModel.values()].reduce((a, v) => a + v.before, 0);
|
|
371
|
-
const after = [...byModel.values()].reduce((a, v) => a + v.after, 0);
|
|
520
|
+
const report = flags.apply ? applyReprice(store) : previewReprice(store);
|
|
521
|
+
const { updates, stillEstimated, totalBeforeUsd: before, totalAfterUsd: after } = report;
|
|
522
|
+
const rows = { length: report.estimatedRows };
|
|
523
|
+
const byModel = new Map(report.byModel.map((v) => [JSON.stringify([v.provider, v.model]), { n: v.rows, before: v.beforeUsd, after: v.afterUsd }]));
|
|
372
524
|
if (flags.json) {
|
|
373
525
|
// Write BEFORE serializing, so the payload can report what the reprice did
|
|
374
526
|
// to the stored realized-value snapshots rather than describing the request
|
|
375
527
|
// rows alone and leaving the snapshots' state to be discovered elsewhere.
|
|
376
|
-
const sync =
|
|
528
|
+
const sync = report.realizationSync;
|
|
377
529
|
printJson({
|
|
378
530
|
applied: !!flags.apply, estimatedRows: rows.length, repriceable: updates.length,
|
|
379
|
-
stillEstimated, totalBeforeUsd: before, totalAfterUsd: after,
|
|
380
|
-
priceEvidence:
|
|
381
|
-
localRateCardOnly: true,
|
|
382
|
-
cardSha256: updates[0].pricing.rateCardSha256,
|
|
383
|
-
sourceKind: updates[0].pricing.rateCardSourceKind,
|
|
384
|
-
note: 'Local list-price estimate only; never provider invoice, discount, tax, credit, or reconciliation.',
|
|
385
|
-
} : null,
|
|
531
|
+
stillEstimated, cacheDurationUnknown: report.cacheDurationUnknown, alreadyRepriced: report.alreadyRepriced, totalBeforeUsd: before, totalAfterUsd: after,
|
|
532
|
+
priceEvidence: report.priceEvidence,
|
|
386
533
|
byModel: [...byModel.entries()].map(([key, v]) => {
|
|
387
534
|
const [provider, model] = JSON.parse(key);
|
|
388
535
|
return { provider, model, rows: v.n, beforeUsd: v.before, afterUsd: v.after };
|
|
@@ -399,20 +546,25 @@ export function cmdReprice(flags) {
|
|
|
399
546
|
return;
|
|
400
547
|
}
|
|
401
548
|
console.log(` Estimated rows ${num(rows.length)}`);
|
|
402
|
-
console.log(` Now exactly priced ${num(updates.length)} ${color(tty, C.gray, '(current card
|
|
549
|
+
console.log(` Now exactly priced ${num(updates.length)} ${color(tty, C.gray, '(the current card, or a price you set, gives these models an exact rate)')}`);
|
|
403
550
|
if (stillEstimated)
|
|
404
551
|
console.log(` Still estimates ${num(stillEstimated)} ${color(tty, C.gray, 'left untouched — a new guess is not more honest than the old one')}`);
|
|
552
|
+
if (report.alreadyRepriced)
|
|
553
|
+
console.log(` Already re-priced ${num(report.alreadyRepriced)} ${color(tty, C.gray, 'another reprice wrote these first; left as it wrote them and not counted here')}`);
|
|
554
|
+
if (report.cacheDurationUnknown)
|
|
555
|
+
console.log(` Cache duration ${num(report.cacheDurationUnknown)} ${color(tty, C.gray, 'rows recorded before Segreant kept the cache-write duration; their writes are priced at the 5-minute rate')}`);
|
|
405
556
|
if (updates.length) {
|
|
406
557
|
console.log('');
|
|
407
558
|
for (const [model, v] of [...byModel.entries()].sort((a, b) => b[1].after - a[1].after).slice(0, 10)) {
|
|
408
559
|
const delta = v.after - v.before;
|
|
409
560
|
const dc = delta > 0 ? C.red : C.green;
|
|
410
|
-
|
|
561
|
+
const [provider, name] = JSON.parse(model);
|
|
562
|
+
console.log(` ${`${provider}/${name}`.padEnd(40)} ${num(v.n).padStart(6)} rows ${usd(v.before).padStart(10)} → ${usd(v.after).padStart(10)} ${color(tty, dc, (delta >= 0 ? '+' : '') + usd(delta))}`);
|
|
411
563
|
}
|
|
412
564
|
console.log('');
|
|
413
565
|
console.log(` Total ${usd(before)} → ${usd(after)} ${color(tty, after - before >= 0 ? C.red : C.green, (after - before >= 0 ? '+' : '') + usd(after - before))}`);
|
|
414
566
|
if (flags.apply) {
|
|
415
|
-
const sync =
|
|
567
|
+
const sync = report.realizationSync;
|
|
416
568
|
console.log(color(tty, C.green, ` ✓ Applied — ${num(updates.length)} rows re-costed, estimated flag cleared.`));
|
|
417
569
|
if (sync.resynced) {
|
|
418
570
|
const delta = sync.costUsdAfter - sync.costUsdBefore;
|
package/dist/cli/showCmd.js
CHANGED
|
@@ -12,10 +12,13 @@ import { economicRequestsToCsv, economicRequestsToJson } from '../export/economi
|
|
|
12
12
|
import { computeAlerts, computeAlertCoverage } from '../alerts/detect.js';
|
|
13
13
|
import { describeSourceDepth } from '../value/sourceDepth.js';
|
|
14
14
|
import { isDeclaredAttribution } from '../value/characterization.js';
|
|
15
|
-
import { C, color, usd, num, pct, printJson } from './ui.js';
|
|
15
|
+
import { C, color, usd, num, pct, printJson, cliJsonResponse } from './ui.js';
|
|
16
16
|
import { stringifyJson } from '../util/json.js';
|
|
17
17
|
import { rangeFor, UserInputError, usdFlag } from './flags.js';
|
|
18
18
|
import { retentionNotice } from './retention.js';
|
|
19
|
+
import { planComparisons, printPlanLines } from './planCmd.js';
|
|
20
|
+
import { summarizeBasis } from '../cost/basis.js';
|
|
21
|
+
import { basisLines } from './basisLines.js';
|
|
19
22
|
import { instant } from '../epistemic/time.js';
|
|
20
23
|
export function cmdShow(window, flags) {
|
|
21
24
|
const cfg = loadConfig();
|
|
@@ -44,7 +47,7 @@ export function cmdShow(window, flags) {
|
|
|
44
47
|
}
|
|
45
48
|
if (flags.json) {
|
|
46
49
|
printJson({
|
|
47
|
-
window, label, demo: isDemo(), retention, summary, byModel, byProject, byUser, bySource,
|
|
50
|
+
window, label, demo: isDemo(), retention, summary, basis: summarizeBasis(store.pricingEvidenceByModel(startMs, endMs)), byModel, byProject, byUser, bySource,
|
|
48
51
|
...(alerts === null || coverage === null ? {} : { alerts, alertCoverage: coverage }),
|
|
49
52
|
});
|
|
50
53
|
store.close();
|
|
@@ -60,19 +63,30 @@ export function cmdShow(window, flags) {
|
|
|
60
63
|
const truncation = retentionNotice(retention);
|
|
61
64
|
if (truncation !== null)
|
|
62
65
|
console.log(color(tty, C.yellow, ` ● ${truncation}`));
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
66
|
+
// The total is never "spend". Its word follows the weakest part of it: list
|
|
67
|
+
// cost only when every dollar was priced at a model's own card rate. A model
|
|
68
|
+
// missing from the card, an amount a tool reported, or an unpriced provider
|
|
69
|
+
// each gets its own line, so no part is presented as another (H003/H007).
|
|
70
|
+
const basis = summarizeBasis(store.pricingEvidenceByModel(startMs, endMs));
|
|
67
71
|
const liveCostUsd = store.spendBetween(startMs, endMs, true);
|
|
68
72
|
const importedCostUsd = Math.max(0, summary.costUsd - liveCostUsd);
|
|
69
|
-
|
|
73
|
+
const single = basis.cohorts.length === 0 || (basis.cohorts.length === 1 && basis.cohorts[0].id === 'list_exact');
|
|
74
|
+
console.log(` ${basis.headlineLabel.padEnd(10)} ${color(tty, C.green, usd(summary.costUsd))} ${color(tty, C.gray, single
|
|
75
|
+
? basis.cohorts.length === 0 ? `(${num(summary.requests)} requests)` : `(${num(summary.requests)} requests · priced at each model's API list rate · not your bill)`
|
|
76
|
+
: `(${num(summary.requests)} requests · priced in parts:)`)}`);
|
|
77
|
+
if (!single) {
|
|
78
|
+
for (const line of basisLines(basis))
|
|
79
|
+
console.log(color(tty, C.gray, line));
|
|
80
|
+
}
|
|
70
81
|
if (importedCostUsd >= 0.005) {
|
|
71
|
-
console.log(color(tty, C.gray, ` ${usd(importedCostUsd)} read from tool logs:
|
|
82
|
+
console.log(color(tty, C.gray, ` ${usd(importedCostUsd)} read from tool logs: use on your plans and keys, priced as above, not your invoice`));
|
|
72
83
|
}
|
|
73
84
|
if (liveCostUsd >= 0.005) {
|
|
74
85
|
console.log(color(tty, C.gray, ` ${usd(liveCostUsd)} metered through the proxy: list price; your provider's bill may differ`));
|
|
75
86
|
}
|
|
87
|
+
// A plan price is monthly, so only the 30-day window sets the two side by side.
|
|
88
|
+
if (window === 'month' && !isDemo())
|
|
89
|
+
printPlanLines(tty, planComparisons(store, startMs, endMs));
|
|
76
90
|
console.log(` Input ${num(summary.inputTokens)} tokens`);
|
|
77
91
|
console.log(` Output ${num(summary.outputTokens)} tokens`);
|
|
78
92
|
if (alerts !== null && coverage !== null) {
|
|
@@ -261,12 +275,20 @@ export function cmdExport(flags) {
|
|
|
261
275
|
}
|
|
262
276
|
const asJson = flags.json === true || flags.format === 'json';
|
|
263
277
|
const out = economic
|
|
264
|
-
? (asJson ? economicRequestsToJson(economicRows) : economicRequestsToCsv(economicRows))
|
|
265
|
-
: (asJson ? `${stringifyJson(rows)}\n` : requestsToCsv(rows));
|
|
278
|
+
? (asJson ? `${stringifyJson(cliJsonResponse(JSON.parse(economicRequestsToJson(economicRows))))}\n` : economicRequestsToCsv(economicRows))
|
|
279
|
+
: (asJson ? `${stringifyJson(cliJsonResponse(rows))}\n` : requestsToCsv(rows));
|
|
280
|
+
// The economic export leaves out requests refused before they were sent
|
|
281
|
+
// (they cost nothing), so its count is its own rows, and the gap is named
|
|
282
|
+
// rather than left for the reader to discover (PR #145 review).
|
|
283
|
+
const exportedCount = economicRows ? economicRows.length : rows.length;
|
|
284
|
+
const notSent = economicRows ? rows.length - economicRows.length : 0;
|
|
285
|
+
if (notSent > 0) {
|
|
286
|
+
console.error(color(process.stdout.isTTY ?? false, C.gray, ` ${num(notSent)} request${notSent === 1 ? '' : 's'} refused before sending left out: no cost, no economic record.`));
|
|
287
|
+
}
|
|
266
288
|
if (typeof flags.out === 'string') {
|
|
267
289
|
writeFileSync(flags.out, out);
|
|
268
290
|
const tty = process.stdout.isTTY ?? false;
|
|
269
|
-
console.error(color(tty, C.green, ` Exported ${num(
|
|
291
|
+
console.error(color(tty, C.green, ` Exported ${num(exportedCount)} requests (${economic ? 'economic-' : ''}${asJson ? 'json' : 'csv'}) → ${flags.out}`));
|
|
270
292
|
}
|
|
271
293
|
else {
|
|
272
294
|
process.stdout.write(out);
|
|
@@ -313,6 +335,10 @@ export function cmdBudget(flags) {
|
|
|
313
335
|
}
|
|
314
336
|
return updated;
|
|
315
337
|
});
|
|
338
|
+
if (flags.json) {
|
|
339
|
+
printJson({ updated: changing, budget: next.budget });
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
316
342
|
console.log('');
|
|
317
343
|
console.log(changing ? ' Budget updated:' : ' Current caps (change one with, for example, segreant budget --daily 20):');
|
|
318
344
|
console.log(` Daily hard cap: ${next.budget.dailyUsd === null ? 'off' : usd(next.budget.dailyUsd)}`);
|
package/dist/cli/teamCmd.js
CHANGED
|
@@ -109,7 +109,7 @@ export async function cmdReceipt(flags) {
|
|
|
109
109
|
if (flags.pubkey) {
|
|
110
110
|
const keys = loadOrCreateKeyPair(join(segreantHome(), 'receipt-key.json'));
|
|
111
111
|
if (flags.json) {
|
|
112
|
-
|
|
112
|
+
printJson({ keyId: keys.keyId, publicKey: keys.publicPem });
|
|
113
113
|
return;
|
|
114
114
|
}
|
|
115
115
|
console.log('');
|
|
@@ -255,7 +255,7 @@ export async function cmdReceipt(flags) {
|
|
|
255
255
|
store.saveReceipt({ unit: r.body.unit, project, tsEpochMs: Date.now(), realized: r.body.realized, receiptJson: JSON.stringify(r) });
|
|
256
256
|
}
|
|
257
257
|
if (flags.json) {
|
|
258
|
-
|
|
258
|
+
printJson(receipts);
|
|
259
259
|
store.close();
|
|
260
260
|
return;
|
|
261
261
|
}
|
|
@@ -323,7 +323,7 @@ export async function cmdJudge(flags) {
|
|
|
323
323
|
if (!picked) {
|
|
324
324
|
store.close();
|
|
325
325
|
if (flags.json) {
|
|
326
|
-
|
|
326
|
+
printJson({ error: 'no-sessions-in-window', project, windowDays, sessions: 0 });
|
|
327
327
|
return;
|
|
328
328
|
}
|
|
329
329
|
console.log('');
|
|
@@ -540,7 +540,7 @@ export async function cmdTeamPush(flags) {
|
|
|
540
540
|
if (flags.pubkey) {
|
|
541
541
|
const keys = loadOrCreateKeyPair(keyPath);
|
|
542
542
|
if (flags.json) {
|
|
543
|
-
|
|
543
|
+
printJson({ keyId: keys.keyId, publicKey: keys.publicPem });
|
|
544
544
|
return;
|
|
545
545
|
}
|
|
546
546
|
console.log('');
|
|
@@ -571,7 +571,7 @@ export async function cmdTeamPush(flags) {
|
|
|
571
571
|
console.log(JSON.stringify({ ok: false, error: transportError }, null, 2));
|
|
572
572
|
}
|
|
573
573
|
else {
|
|
574
|
-
console.error(` ${color(tty, C.red, '
|
|
574
|
+
console.error(` ${color(tty, C.red, '✗')} ${transportError}`);
|
|
575
575
|
}
|
|
576
576
|
process.exitCode = 1;
|
|
577
577
|
return;
|
package/dist/cli/ui.js
CHANGED
|
@@ -23,13 +23,17 @@ export function color(on, code, s) {
|
|
|
23
23
|
return on ? `${code}${s}${C.reset}` : s;
|
|
24
24
|
}
|
|
25
25
|
export function usd(n) {
|
|
26
|
-
|
|
26
|
+
// Sign outside the symbol, and the same precision either side of zero: a
|
|
27
|
+
// credit of 5 dollars used to fall through every branch to "$-5.000000".
|
|
28
|
+
const sign = n < 0 ? '-' : '';
|
|
29
|
+
const a = Math.abs(n);
|
|
30
|
+
if (a === 0)
|
|
27
31
|
return '$0.00';
|
|
28
|
-
if (
|
|
29
|
-
return
|
|
30
|
-
if (
|
|
31
|
-
return
|
|
32
|
-
return
|
|
32
|
+
if (a >= 1)
|
|
33
|
+
return `${sign}$${a.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
34
|
+
if (a >= 0.01)
|
|
35
|
+
return `${sign}$${a.toFixed(4)}`;
|
|
36
|
+
return `${sign}$${a.toFixed(6)}`;
|
|
33
37
|
}
|
|
34
38
|
export function num(n) {
|
|
35
39
|
return n.toLocaleString('en-US');
|
|
@@ -37,9 +41,21 @@ export function num(n) {
|
|
|
37
41
|
export function pct(n) {
|
|
38
42
|
return `${(n * 100).toFixed(0)}%`;
|
|
39
43
|
}
|
|
40
|
-
/**
|
|
44
|
+
/** CLI response version, separate from database and exported artifact schemas. */
|
|
45
|
+
export const CLI_JSON_SCHEMA_VERSION = 1;
|
|
46
|
+
export function cliJsonResponse(value) {
|
|
47
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
48
|
+
// A document that already names its own schema version keeps it: that
|
|
49
|
+
// number describes the document, and the CLI must not relabel it.
|
|
50
|
+
if (Object.hasOwn(value, 'schemaVersion'))
|
|
51
|
+
return value;
|
|
52
|
+
return { ...value, schemaVersion: CLI_JSON_SCHEMA_VERSION };
|
|
53
|
+
}
|
|
54
|
+
return { schemaVersion: CLI_JSON_SCHEMA_VERSION, data: value };
|
|
55
|
+
}
|
|
56
|
+
/** Emit one versioned response with stable six-decimal USD fields. */
|
|
41
57
|
export function printJson(value, space = 2) {
|
|
42
|
-
process.stdout.write(stringifyJson(value, space) + '\n');
|
|
58
|
+
process.stdout.write(stringifyJson(cliJsonResponse(value), space) + '\n');
|
|
43
59
|
}
|
|
44
60
|
/** Actionable not-a-git-repo message: tell the user what to do, not just what's wrong. */
|
|
45
61
|
export function printNotAGitRepo(repo) {
|