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/opsCmd.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Extracted verbatim from cli.ts in the per-command-module split;
|
|
4
4
|
* gatherGuideFacts is exported for the CLI/dashboard guidance contract tests.
|
|
5
5
|
*/
|
|
6
|
+
import { capBasisSpend } from '../budget/guard.js';
|
|
6
7
|
import { Store } from '../store/db.js';
|
|
7
8
|
import { loadConfig, mutateConfig, dbPath, configPath, isDemo } from '../config.js';
|
|
8
9
|
import { attributeCommits, isGitRepo } from '../git/correlate.js';
|
|
@@ -11,6 +12,7 @@ import { buildGuide } from '../guide.js';
|
|
|
11
12
|
import { computeAlertCoverage, computeAlerts } from '../alerts/detect.js';
|
|
12
13
|
import { notifyWebhook } from '../alerts/notify.js';
|
|
13
14
|
import { pricingStatus } from '../cost/pricing.js';
|
|
15
|
+
import { summarizeBasis } from '../cost/basis.js';
|
|
14
16
|
import { baselineManifestStatus } from '../value/liftBaseline.js';
|
|
15
17
|
import { probeProxyState } from '../egress/proxyHealth.js';
|
|
16
18
|
import { C, color, usd, num, printNotAGitRepo, printJson } from './ui.js';
|
|
@@ -108,7 +110,30 @@ export async function cmdAlerts(flags) {
|
|
|
108
110
|
process.exitCode = 1;
|
|
109
111
|
store.close();
|
|
110
112
|
}
|
|
111
|
-
|
|
113
|
+
/**
|
|
114
|
+
* How the last 30 days were priced, in one line. A price you set is not a list
|
|
115
|
+
* price and was never checked against one, so it is named on its own rather
|
|
116
|
+
* than folded into "each model's own list rate" (PR #146 review).
|
|
117
|
+
*/
|
|
118
|
+
export function doctorPricingLine(basis) {
|
|
119
|
+
if (basis.cohorts.length === 0)
|
|
120
|
+
return 'no priced requests in 30d';
|
|
121
|
+
const weak = basis.cohorts.filter((c) => (c.id === 'fallback' || c.id === 'tool_reported' || c.id === 'unrecorded') && c.costUsd >= 0.005);
|
|
122
|
+
const declared = basis.cohorts.find((c) => c.id === 'user_declared');
|
|
123
|
+
const parts = [];
|
|
124
|
+
if (weak.length > 0) {
|
|
125
|
+
parts.push(`${usd(weak.reduce((s, c) => s + c.costUsd, 0))} of 30d spend priced without the model's own rate (${weak.map((c) => c.label).join(', ')}) — see "segreant month"`);
|
|
126
|
+
}
|
|
127
|
+
if (declared)
|
|
128
|
+
parts.push(`${usd(declared.costUsd)} at prices you set (not checked against a price list)`);
|
|
129
|
+
const free = basis.cohorts.find((c) => c.id === 'user_free');
|
|
130
|
+
if (free && !basis.cohorts.some((c) => c.id === 'list_exact' || c.id === 'list_family')) {
|
|
131
|
+
// Only free models: no rate card priced any of it (PR #146 review).
|
|
132
|
+
parts.push(`${num(free.requests)} requests on models you marked free (no charge)`);
|
|
133
|
+
}
|
|
134
|
+
return parts.length > 0 ? parts.join('; ') : "30d spend priced at each model's own list rate (an estimate, not a bill)";
|
|
135
|
+
}
|
|
136
|
+
export async function cmdDoctor(flags = { _: [] }) {
|
|
112
137
|
const tty = process.stdout.isTTY ?? false;
|
|
113
138
|
const cfg = loadConfig();
|
|
114
139
|
const store = new Store(dbPath());
|
|
@@ -121,6 +146,25 @@ export async function cmdDoctor() {
|
|
|
121
146
|
const criticals = alerts.filter((a) => a.severity === 'critical').length;
|
|
122
147
|
const proxyStatus = await probeProxyState(cfg);
|
|
123
148
|
const proxyUp = proxyStatus.kind === 'up';
|
|
149
|
+
const basis = summarizeBasis(store.pricingEvidenceByModel(now - 30 * day, now + 1000));
|
|
150
|
+
if (flags.json) {
|
|
151
|
+
// The same checks as the text below, as data. `doctor --json` used to print
|
|
152
|
+
// the text (H013).
|
|
153
|
+
const price = pricingStatus(cfg.pricing.maxAgeDays);
|
|
154
|
+
const base = baselineManifestStatus();
|
|
155
|
+
const coverage = computeAlertCoverage(store, cfg);
|
|
156
|
+
printJson({
|
|
157
|
+
config: { path: configPath() },
|
|
158
|
+
database: { path: dbPath(), last30Days: { requests: sum30.requests, costUsd: sum30.costUsd } },
|
|
159
|
+
proxy: { port: cfg.port, state: proxyStatus.kind },
|
|
160
|
+
budget: { dailyUsd: cfg.budget.dailyUsd },
|
|
161
|
+
pricing: { basis, rateCard: price },
|
|
162
|
+
baseline: base,
|
|
163
|
+
alerts: { active: alerts.length, critical: criticals, coverage },
|
|
164
|
+
});
|
|
165
|
+
store.close();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
124
168
|
const mark = (good) => (good ? color(tty, C.green, '✓') : color(tty, C.yellow, '!'));
|
|
125
169
|
console.log('');
|
|
126
170
|
console.log(color(tty, C.bold, ' Segreant — doctor'));
|
|
@@ -134,7 +178,7 @@ export async function cmdDoctor() {
|
|
|
134
178
|
: color(tty, C.yellow, `not reachable on :${cfg.port} — start with "segreant start"`);
|
|
135
179
|
console.log(` ${mark(proxyUp)} Proxy ${proxyMessage}`);
|
|
136
180
|
console.log(` ${mark(cfg.budget.dailyUsd !== null)} Daily cap ${cfg.budget.dailyUsd !== null ? usd(cfg.budget.dailyUsd) : color(tty, C.yellow, 'none — metering only (set with "segreant budget --daily N")')}`);
|
|
137
|
-
console.log(` ${mark(estShare <= 0.2)} Pricing ${
|
|
181
|
+
console.log(` ${mark(estShare <= 0.2)} Pricing ${doctorPricingLine(basis)}`);
|
|
138
182
|
const price = pricingStatus(cfg.pricing.maxAgeDays);
|
|
139
183
|
const priceAge = price.ageDays === null ? '' : ` · ${price.ageDays}d old`;
|
|
140
184
|
const priceEvidence = price.source === 'cache'
|
|
@@ -201,6 +245,7 @@ export async function gatherGuideFacts() {
|
|
|
201
245
|
const all = store.summary(0, now + 1000);
|
|
202
246
|
const sum30 = store.summary(now - 30 * day, now + 1000);
|
|
203
247
|
const outcomeSignals = store.countSignals();
|
|
248
|
+
const capBasis = capBasisSpend(store, cfg.budget, now - 30 * day, now + 1000);
|
|
204
249
|
const realizationUnits = store.countRealizationUnits();
|
|
205
250
|
// Read before the store closes: without it `requestsAllTime` is a count of
|
|
206
251
|
// survivors presented as a count of everything (D-170).
|
|
@@ -216,6 +261,8 @@ export async function gatherGuideFacts() {
|
|
|
216
261
|
requestsAllTime: all.requests,
|
|
217
262
|
requestsRetention,
|
|
218
263
|
spend30dUsd: sum30.costUsd,
|
|
264
|
+
capBasisSpend30dUsd: capBasis.usd,
|
|
265
|
+
capBasis: capBasis.basis,
|
|
219
266
|
dailyCapUsd: cfg.budget.dailyUsd,
|
|
220
267
|
outcomeSignals,
|
|
221
268
|
realizationUnits,
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `segreant plan`: which plan each tool runs on, what the person pays for it,
|
|
3
|
+
* and how much list-price work the plan did. Two bases, always named: the plan
|
|
4
|
+
* price is what the person entered; the work is list cost from the rate card.
|
|
5
|
+
*/
|
|
6
|
+
import { computeCost, withoutDeclaredPrices } from '../cost/pricing.js';
|
|
7
|
+
import { Store } from '../store/db.js';
|
|
8
|
+
import { dbPath, loadConfig, mutateConfig, PLAN_SOURCES } from '../config.js';
|
|
9
|
+
import { detectPlans, planName, suggestedPrice, PUBLIC_PLAN_PRICES_CHECKED, } from '../plans/detect.js';
|
|
10
|
+
import { C, color, usd, printJson, num } from './ui.js';
|
|
11
|
+
import { rangeFor, UserInputError } from './flags.js';
|
|
12
|
+
const TOOL_LABEL = { 'claude-code': 'Claude Code', codex: 'Codex' };
|
|
13
|
+
/** List cost of a tool's imported (subscription) use in a window. */
|
|
14
|
+
export function importedListCost(store, source, startMs, endMs) {
|
|
15
|
+
return importedListWork(store, source, startMs, endMs).listCostUsd;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The list-price work a tool's imported use did, and how many requests on a
|
|
19
|
+
* price you set were left out because the card has no exact price for their
|
|
20
|
+
* model. "List-price work" means list prices: a declared row is priced from
|
|
21
|
+
* the card here, so a declaration never inflates, shrinks or hides the plan
|
|
22
|
+
* line, and a private model the card only estimates (family or fallback) is
|
|
23
|
+
* withheld rather than shown as a published price (PR #146 review). Other
|
|
24
|
+
* rows keep the price they were recorded at.
|
|
25
|
+
*/
|
|
26
|
+
export function importedListWork(store, source, startMs, endMs) {
|
|
27
|
+
let listCostUsd = 0;
|
|
28
|
+
let withheldRequests = 0;
|
|
29
|
+
for (const r of store.requestsInRange(startMs, endMs)) {
|
|
30
|
+
if (r.source !== source || r.via !== 'import')
|
|
31
|
+
continue;
|
|
32
|
+
if (r.pricing?.costBasis !== 'user_declared_price') {
|
|
33
|
+
listCostUsd += r.costUsd;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const card = withoutDeclaredPrices(() => computeCost(r.provider, r.model, {
|
|
37
|
+
inputTokens: r.inputTokens, outputTokens: r.outputTokens,
|
|
38
|
+
cacheWriteTokens: r.cacheWriteTokens, cacheReadTokens: r.cacheReadTokens,
|
|
39
|
+
...(r.cacheWriteTtl === undefined ? {} : { cacheWriteTtl: r.cacheWriteTtl }),
|
|
40
|
+
}));
|
|
41
|
+
if (card.estimated) {
|
|
42
|
+
withheldRequests++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
listCostUsd += card.costUsd;
|
|
46
|
+
}
|
|
47
|
+
return { listCostUsd, withheldRequests };
|
|
48
|
+
}
|
|
49
|
+
export function planComparisons(store, startMs, endMs) {
|
|
50
|
+
const cfg = loadConfig();
|
|
51
|
+
return detectPlans().map((detected) => ({
|
|
52
|
+
source: detected.source,
|
|
53
|
+
detected,
|
|
54
|
+
price: cfg.plans[detected.source] ?? null,
|
|
55
|
+
...importedListWork(store, detected.source, startMs, endMs),
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
function withheldNote(r) {
|
|
59
|
+
return r.withheldRequests > 0
|
|
60
|
+
? ` · ${num(r.withheldRequests)} request${r.withheldRequests === 1 ? '' : 's'} on models with no published price left out`
|
|
61
|
+
: '';
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The month's plan lines, printed under the list cost by `month` and `scan`.
|
|
65
|
+
* A price the person set: "Your $20 Claude Pro plan did $1,830 of list-price
|
|
66
|
+
* work". A plan with no price: the plan, and the command that sets it.
|
|
67
|
+
*/
|
|
68
|
+
export function printPlanLines(tty, rows) {
|
|
69
|
+
for (const r of rows) {
|
|
70
|
+
if (r.listCostUsd < 0.005)
|
|
71
|
+
continue;
|
|
72
|
+
const name = planName(r.detected) ?? `${TOOL_LABEL[r.source]} (plan not detected)`;
|
|
73
|
+
if (r.price !== null) {
|
|
74
|
+
const times = r.price.monthlyUsd > 0 ? `, ${(r.listCostUsd / r.price.monthlyUsd).toFixed(1)}× its price` : '';
|
|
75
|
+
console.log(` ${color(tty, C.bold, `Your ${usd(r.price.monthlyUsd)}/month ${name} plan`)} did ${color(tty, C.green, usd(r.listCostUsd))} of list-price work in the last 30 days${color(tty, C.gray, `${times}`)}`);
|
|
76
|
+
console.log(color(tty, C.gray, ` plan price: what you set${r.price.plan !== null && r.price.plan !== r.detected.plan ? ` (for ${r.price.plan}; the tool now reports ${r.detected.plan ?? 'no plan'})` : ''} · work: priced from the rate card${withheldNote(r)}`));
|
|
77
|
+
}
|
|
78
|
+
else if (r.detected.plan !== null) {
|
|
79
|
+
const hint = suggestedPrice(r.detected);
|
|
80
|
+
console.log(color(tty, C.gray, ` ${name} plan: ${usd(r.listCostUsd)} of list-price work${withheldNote(r)}. Compare it with what you pay: segreant plan set ${r.source} <dollars per month>${hint !== null ? ` (public price ${usd(hint)})` : ''}`));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Ask once, at the end of `scan --setup`, what the person pays for each plan a
|
|
86
|
+
* tool reports and no price is set for. Interactive terminals only: a script,
|
|
87
|
+
* a pipe or --json never waits. Enter skips; an answer is saved through the
|
|
88
|
+
* same validated path as `segreant plan set`. Returns how many were saved.
|
|
89
|
+
*/
|
|
90
|
+
export async function askPlanPrices(tty, io = {
|
|
91
|
+
input: process.stdin, output: process.stdout, interactive: process.stdin.isTTY === true,
|
|
92
|
+
}) {
|
|
93
|
+
if (!io.interactive)
|
|
94
|
+
return 0;
|
|
95
|
+
const cfg = loadConfig();
|
|
96
|
+
const pending = detectPlans().filter((p) => p.plan !== null && cfg.plans[p.source] === undefined);
|
|
97
|
+
if (pending.length === 0)
|
|
98
|
+
return 0;
|
|
99
|
+
const { createInterface } = await import('node:readline/promises');
|
|
100
|
+
const rl = createInterface({ input: io.input, output: io.output, terminal: false });
|
|
101
|
+
let saved = 0;
|
|
102
|
+
try {
|
|
103
|
+
io.output.write(color(tty, C.bold, ' What do you pay for your plans?') + color(tty, C.gray, ' so Segreant can set the plan price beside the list-price work (Enter skips)') + '\n');
|
|
104
|
+
for (const p of pending) {
|
|
105
|
+
const hint = suggestedPrice(p);
|
|
106
|
+
const answer = (await rl.question(` ${planName(p)}, dollars per month${hint !== null ? ` (public price ${hint})` : ''}: `)).trim().replace(/^\$/, '');
|
|
107
|
+
if (answer === '')
|
|
108
|
+
continue;
|
|
109
|
+
const monthlyUsd = Number(answer);
|
|
110
|
+
if (!Number.isFinite(monthlyUsd) || monthlyUsd < 0 || monthlyUsd > 100_000) {
|
|
111
|
+
io.output.write(color(tty, C.yellow, ` Not a dollar amount; skipped. Set it later: segreant plan set ${p.source} <dollars>`) + '\n');
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
mutateConfig((c) => ({ ...c, plans: { ...c.plans, [p.source]: { monthlyUsd, plan: p.plan, setAt: new Date().toISOString() } } }));
|
|
115
|
+
saved += 1;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
rl.close();
|
|
120
|
+
}
|
|
121
|
+
return saved;
|
|
122
|
+
}
|
|
123
|
+
export function cmdPlan(flags) {
|
|
124
|
+
const [action, tool, amount] = flags._.map(String);
|
|
125
|
+
if (action === 'set' || action === 'clear') {
|
|
126
|
+
if (tool === undefined || !PLAN_SOURCES.includes(tool)) {
|
|
127
|
+
throw new UserInputError(`Name the tool: segreant plan ${action} ${PLAN_SOURCES.join('|')}${action === 'set' ? ' <dollars per month>' : ''}`);
|
|
128
|
+
}
|
|
129
|
+
const source = tool;
|
|
130
|
+
const detected = detectPlans().find((p) => p.source === source);
|
|
131
|
+
if (action === 'set') {
|
|
132
|
+
if (amount === undefined)
|
|
133
|
+
throw new UserInputError(`Give the monthly price: segreant plan set ${source} 20`);
|
|
134
|
+
const monthlyUsd = Number(amount.replace(/^\$/, ''));
|
|
135
|
+
if (!Number.isFinite(monthlyUsd) || monthlyUsd < 0 || monthlyUsd > 100_000) {
|
|
136
|
+
throw new UserInputError('The price must be a dollar amount per month, for example 20.');
|
|
137
|
+
}
|
|
138
|
+
mutateConfig((cfg) => ({
|
|
139
|
+
...cfg,
|
|
140
|
+
plans: { ...cfg.plans, [source]: { monthlyUsd, plan: detected.plan, setAt: new Date().toISOString() } },
|
|
141
|
+
}));
|
|
142
|
+
console.log(` Saved: ${planName(detected) ?? TOOL_LABEL[source]} costs you ${usd(monthlyUsd)} a month.`);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
mutateConfig((cfg) => {
|
|
146
|
+
const plans = { ...cfg.plans };
|
|
147
|
+
delete plans[source];
|
|
148
|
+
return { ...cfg, plans };
|
|
149
|
+
});
|
|
150
|
+
console.log(` Cleared the plan price for ${TOOL_LABEL[source]}.`);
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (action !== undefined)
|
|
155
|
+
throw new UserInputError('Usage: segreant plan [set <tool> <dollars> | clear <tool>]');
|
|
156
|
+
const store = new Store(dbPath());
|
|
157
|
+
const { startMs, endMs } = rangeFor('month');
|
|
158
|
+
const rows = planComparisons(store, startMs, endMs);
|
|
159
|
+
store.close();
|
|
160
|
+
if (flags.json) {
|
|
161
|
+
printJson({ windowDays: 30, plans: rows, publicPricesChecked: PUBLIC_PLAN_PRICES_CHECKED });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const tty = process.stdout.isTTY ?? false;
|
|
165
|
+
console.log('');
|
|
166
|
+
console.log(color(tty, C.bold, ' Plans') + color(tty, C.gray, ' read from each tool\'s own files · the price is only what you set'));
|
|
167
|
+
for (const r of rows) {
|
|
168
|
+
const name = planName(r.detected);
|
|
169
|
+
const price = r.price === null ? color(tty, C.gray, 'price not set') : `${usd(r.price.monthlyUsd)}/month (you set this ${r.price.setAt.slice(0, 10)})`;
|
|
170
|
+
console.log(` ${TOOL_LABEL[r.source].padEnd(12)} ${(name ?? 'not detected').padEnd(18)} ${price}`);
|
|
171
|
+
console.log(color(tty, C.gray, ` ${r.detected.evidence}${r.detected.billing ? ` · billed via ${r.detected.billing.replace(/_/g, ' ')}` : ''}`));
|
|
172
|
+
}
|
|
173
|
+
console.log('');
|
|
174
|
+
printPlanLines(tty, rows);
|
|
175
|
+
const unset = rows.filter((r) => r.price === null && r.detected.plan !== null);
|
|
176
|
+
if (unset.length > 0) {
|
|
177
|
+
console.log('');
|
|
178
|
+
console.log(color(tty, C.gray, ` Public prices (checked ${PUBLIC_PLAN_PRICES_CHECKED}) are only a hint: App Store billing, tax and annual plans change what you pay.`));
|
|
179
|
+
}
|
|
180
|
+
console.log('');
|
|
181
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/** User-declared model prices; historical request amounts remain unchanged. */
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { dbPath, isDemo } from '../config.js';
|
|
4
|
+
import { modelNameError, PriceOverridesError, removePriceOverride, } from '../cost/overrides.js';
|
|
5
|
+
import { declarePrice as declare, unknownModels } from '../cost/declarations.js';
|
|
6
|
+
import { Store } from '../store/db.js';
|
|
7
|
+
import { printJson, usd } from './ui.js';
|
|
8
|
+
const NEXT_STEP = 'segreant reprice previews re-pricing recorded requests; nothing changes without --apply.';
|
|
9
|
+
const SET_WAYS = 'Set a price: segreant pricing set <model> --input <usd> --output <usd> (USD per 1M tokens), --like <known-model>, or --free.';
|
|
10
|
+
const PRICE_OPTIONS = ['input', 'output', 'cache-read', 'cache-write', 'cache-write-1h', 'like', 'free'];
|
|
11
|
+
function priceInput(flags) {
|
|
12
|
+
// Read every form before choosing one, including cache-only mixtures.
|
|
13
|
+
const input = flags.input;
|
|
14
|
+
const output = flags.output;
|
|
15
|
+
const cacheRead = flags['cache-read'];
|
|
16
|
+
const cacheWrite = flags['cache-write'];
|
|
17
|
+
const cacheWrite1h = flags['cache-write-1h'];
|
|
18
|
+
const like = flags.like;
|
|
19
|
+
const free = flags.free;
|
|
20
|
+
const rate = [input, output, cacheRead, cacheWrite, cacheWrite1h].some((v) => v !== undefined);
|
|
21
|
+
if (Number(rate) + Number(like !== undefined) + Number(free !== undefined) !== 1) {
|
|
22
|
+
throw new PriceOverridesError('Choose exactly one price form: --input with --output, --like, or --free; do not mix them.');
|
|
23
|
+
}
|
|
24
|
+
if (free !== undefined) {
|
|
25
|
+
if (free !== true)
|
|
26
|
+
throw new PriceOverridesError('--free is a switch; give it no value.');
|
|
27
|
+
return { kind: 'free' };
|
|
28
|
+
}
|
|
29
|
+
if (like !== undefined) {
|
|
30
|
+
if (typeof like !== 'string')
|
|
31
|
+
throw new PriceOverridesError('--like needs an exact model name from the rate card.');
|
|
32
|
+
return { kind: 'like', like };
|
|
33
|
+
}
|
|
34
|
+
if (typeof input !== 'string' || typeof output !== 'string') {
|
|
35
|
+
throw new PriceOverridesError('A rate needs both --input <usd> and --output <usd> (USD per 1M tokens).');
|
|
36
|
+
}
|
|
37
|
+
if (cacheRead !== undefined && typeof cacheRead !== 'string')
|
|
38
|
+
throw new PriceOverridesError('--cache-read needs a decimal USD price per 1M tokens.');
|
|
39
|
+
if (cacheWrite !== undefined && typeof cacheWrite !== 'string')
|
|
40
|
+
throw new PriceOverridesError('--cache-write needs a decimal USD price per 1M tokens.');
|
|
41
|
+
if (cacheWrite1h !== undefined && typeof cacheWrite1h !== 'string')
|
|
42
|
+
throw new PriceOverridesError('--cache-write-1h needs a decimal USD price per 1M tokens.');
|
|
43
|
+
return {
|
|
44
|
+
kind: 'rate', input, output,
|
|
45
|
+
...(cacheRead === undefined ? {} : { cacheRead }),
|
|
46
|
+
...(cacheWrite === undefined ? {} : { cacheWrite }),
|
|
47
|
+
...(cacheWrite1h === undefined ? {} : { cacheWrite1h }),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function storedLine(model, price) {
|
|
51
|
+
if (price.kind === 'free')
|
|
52
|
+
return `Stored user-declared price for ${model}: free ($0 per request).`;
|
|
53
|
+
if (price.kind === 'like')
|
|
54
|
+
return `Stored user-declared price for ${model}: like ${price.like} (exact rate-card model).`;
|
|
55
|
+
const cache = [
|
|
56
|
+
...(price.cacheRead === undefined ? [] : [`cache-read $${price.cacheRead}`]),
|
|
57
|
+
...(price.cacheWrite === undefined ? [] : [`cache-write $${price.cacheWrite}`]),
|
|
58
|
+
...(price.cacheWrite1h === undefined ? [] : [`cache-write-1h $${price.cacheWrite1h}`]),
|
|
59
|
+
];
|
|
60
|
+
// A cache price left out is not yours: it is the usual multiple of your
|
|
61
|
+
// input price. Say so where the price is stored (independent review,
|
|
62
|
+
// 2026-10-18), naming only the parts that were left out.
|
|
63
|
+
// --cache-write also prices 1-hour writes unless --cache-write-1h is given
|
|
64
|
+
// (declaredRate), so 2× is derived only when neither was (PR #150 review).
|
|
65
|
+
const derived = [
|
|
66
|
+
...(price.cacheWrite === undefined ? ['5-minute cache writes at 1.25×'] : []),
|
|
67
|
+
...(price.cacheWrite1h === undefined && price.cacheWrite === undefined ? ['1-hour cache writes at 2×'] : []),
|
|
68
|
+
...(price.cacheRead === undefined ? ['cache reads at 0.1×'] : []),
|
|
69
|
+
];
|
|
70
|
+
const note = derived.length
|
|
71
|
+
? ` Not set, so priced at the usual multiple of your input price: ${derived.join(', ')} (set --cache-write, --cache-write-1h or --cache-read to give your own).`
|
|
72
|
+
: '';
|
|
73
|
+
return `Stored user-declared price for ${model}: input $${price.input}, output $${price.output}${cache.length ? `, ${cache.join(', ')}` : ''} (USD per 1M tokens).${note}`;
|
|
74
|
+
}
|
|
75
|
+
/** No ledger is created just to inspect historical usage. */
|
|
76
|
+
function readLedger(empty, read) {
|
|
77
|
+
const path = dbPath();
|
|
78
|
+
if (!existsSync(path))
|
|
79
|
+
return empty;
|
|
80
|
+
// An existing ledger opens as every command opens it; a missing one is not created.
|
|
81
|
+
const store = new Store(path);
|
|
82
|
+
try {
|
|
83
|
+
return read(store);
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
store.close();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** `flags._` includes the action. Input refusals are one line and exit 2. */
|
|
90
|
+
export function cmdPricingDeclare(action, flags) {
|
|
91
|
+
try {
|
|
92
|
+
const allowed = new Set(['_', 'debug', 'help', 'demo', ...(action === 'set' ? PRICE_OPTIONS : action === 'unknown' ? ['json'] : [])]);
|
|
93
|
+
const unsupported = Object.keys(flags).find((k) => !allowed.has(k));
|
|
94
|
+
if (unsupported)
|
|
95
|
+
throw new PriceOverridesError(`--${unsupported} does nothing for segreant pricing ${action}.`);
|
|
96
|
+
if (action === 'unknown') {
|
|
97
|
+
if (flags._.length !== 1)
|
|
98
|
+
throw new PriceOverridesError('Usage: segreant pricing unknown [--json]');
|
|
99
|
+
const models = readLedger([], unknownModels);
|
|
100
|
+
if (flags.json) {
|
|
101
|
+
printJson({ models, costBasis: 'recorded cost (estimated)', nextStep: SET_WAYS, demo: isDemo() });
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
if (isDemo())
|
|
105
|
+
console.log(' DEMO DATA — synthetic requests.');
|
|
106
|
+
if (models.length === 0)
|
|
107
|
+
console.log(' No unresolved models with recorded guessed prices.');
|
|
108
|
+
for (const r of models) {
|
|
109
|
+
console.log(` ${r.provider} ${r.model} ${r.requests} recorded requests recorded cost (estimated): ${usd(r.costUsd)} current match: ${r.rateMatchKind}`);
|
|
110
|
+
}
|
|
111
|
+
console.log(` ${SET_WAYS}`);
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const model = flags._[1];
|
|
116
|
+
if (flags._.length !== 2 || model === undefined)
|
|
117
|
+
throw new PriceOverridesError(`Usage: segreant pricing ${action} <model>${action === 'set' ? ' --input <usd> --output <usd> | --like <known-model> | --free' : ''}`);
|
|
118
|
+
const error = modelNameError(model);
|
|
119
|
+
if (error)
|
|
120
|
+
throw new PriceOverridesError(error);
|
|
121
|
+
if (isDemo())
|
|
122
|
+
throw new PriceOverridesError('Demo mode only shows sample data; run without --demo to change a declared price.');
|
|
123
|
+
if (action === 'unset') {
|
|
124
|
+
console.log(removePriceOverride(model) ? ` Removed declared price for ${model}.` : ` No declared price for ${model}.`);
|
|
125
|
+
console.log(` ${NEXT_STEP}`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const input = priceInput(flags);
|
|
129
|
+
const recorded = readLedger({ requests: 0, costUsd: 0 }, (store) => {
|
|
130
|
+
return store.byModel(Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER)
|
|
131
|
+
.filter((r) => r.label === model)
|
|
132
|
+
.reduce((total, r) => ({ requests: total.requests + r.requests, costUsd: total.costUsd + r.costUsd }), { requests: 0, costUsd: 0 });
|
|
133
|
+
});
|
|
134
|
+
const price = declare(model, input);
|
|
135
|
+
console.log(` ${storedLine(model, price)}`);
|
|
136
|
+
console.log(` ${recorded.requests} recorded requests use ${model}; recorded cost (estimated): ${usd(recorded.costUsd)}.`);
|
|
137
|
+
console.log(` ${NEXT_STEP}`);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (!(error instanceof PriceOverridesError))
|
|
141
|
+
throw error;
|
|
142
|
+
console.error(' ' + error.message.replace(/[\r\n]+/g, ' '));
|
|
143
|
+
process.exitCode = 2;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** Ask once per model name, with at most one retry; callers own terminal IO. */
|
|
147
|
+
export async function promptForUnknownModels(models, io) {
|
|
148
|
+
const made = [];
|
|
149
|
+
for (const model of new Set(models.map((m) => m.model))) {
|
|
150
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
151
|
+
const answer = (await io.ask(`price for ${model}? [Enter] skip l <model> like another p <in> <out> price per 1M f free`)).trim();
|
|
152
|
+
if (answer === '' || answer === 's')
|
|
153
|
+
break;
|
|
154
|
+
const words = answer.split(/\s+/);
|
|
155
|
+
let input;
|
|
156
|
+
if (words[0] === 'f' && words.length === 1)
|
|
157
|
+
input = { kind: 'free' };
|
|
158
|
+
if (words[0] === 'l' && words.length === 2)
|
|
159
|
+
input = { kind: 'like', like: words[1] };
|
|
160
|
+
if (words[0] === 'p' && words.length === 3)
|
|
161
|
+
input = { kind: 'rate', input: words[1], output: words[2] };
|
|
162
|
+
try {
|
|
163
|
+
if (!input)
|
|
164
|
+
throw new PriceOverridesError('Use Enter or s to skip, l <model>, p <in> <out>, or f.');
|
|
165
|
+
const price = declare(model, input);
|
|
166
|
+
made.push({ model, ...price });
|
|
167
|
+
io.print(` ${storedLine(model, price)}`);
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (!(error instanceof PriceOverridesError))
|
|
172
|
+
throw error;
|
|
173
|
+
io.print(` ${error.message.replace(/[\r\n]+/g, ' ')} ${attempt === 0 ? 'Try once more (Enter skips).' : 'Skipped.'}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return made;
|
|
178
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `segreant quota`: each vendor's own usage meter, read from its tool's logs,
|
|
3
|
+
* the pace to the next reset, and whether the meter moves in step with what
|
|
4
|
+
* Segreant counted. It reads what the last import or status-line reading
|
|
5
|
+
* stored; `segreant import all` refreshes it.
|
|
6
|
+
*/
|
|
7
|
+
import { Store } from '../store/db.js';
|
|
8
|
+
import { dbPath } from '../config.js';
|
|
9
|
+
import { quotaView, median } from '../quota/view.js';
|
|
10
|
+
import { statuslineLine } from '../quota/statusline.js';
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
import { detectPlans, planName } from '../plans/detect.js';
|
|
14
|
+
import { C, color, usd, printJson } from './ui.js';
|
|
15
|
+
const DAY = 86_400_000;
|
|
16
|
+
function when(ms, nowMs) {
|
|
17
|
+
const d = new Date(ms);
|
|
18
|
+
const sameDay = new Date(nowMs).toDateString() === d.toDateString();
|
|
19
|
+
const time = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
20
|
+
return sameDay ? `today ${time}` : `${d.toLocaleDateString([], { weekday: 'short' })} ${time}`;
|
|
21
|
+
}
|
|
22
|
+
function inFor(ms, nowMs) {
|
|
23
|
+
const mins = Math.max(0, Math.round((ms - nowMs) / 60_000));
|
|
24
|
+
if (mins < 60)
|
|
25
|
+
return `${mins}m`;
|
|
26
|
+
const h = Math.floor(mins / 60);
|
|
27
|
+
return h < 48 ? `${h}h ${mins % 60}m` : `${Math.round(h / 24)}d`;
|
|
28
|
+
}
|
|
29
|
+
function windowLabel(w) {
|
|
30
|
+
if (w.windowMinutes === 300)
|
|
31
|
+
return '5-hour';
|
|
32
|
+
if (w.windowMinutes === 10_080)
|
|
33
|
+
return 'weekly';
|
|
34
|
+
return w.windowMinutes === null ? 'window' : `${Math.round(w.windowMinutes / 60)}-hour`;
|
|
35
|
+
}
|
|
36
|
+
const STATUSLINE_ABOUT = ` Claude Code then hands Segreant its live usage meter (5-hour and weekly percentages)
|
|
37
|
+
each time the status line refreshes. Segreant records a reading when it changes and
|
|
38
|
+
prints one short line. Nothing leaves your machine; segreant quota shows the history.`;
|
|
39
|
+
/**
|
|
40
|
+
* The setup text for THIS install. It used to name the bare
|
|
41
|
+
* segreant-statusline command, which exists only after a global install;
|
|
42
|
+
* after a local install or npx, Claude Code ran a command that was not there
|
|
43
|
+
* (H012). It now names the installed file by its absolute path, run with node,
|
|
44
|
+
* so nothing has to be on PATH. Under npx the package lives in a cache that
|
|
45
|
+
* can be cleared, so it asks for a lasting install first.
|
|
46
|
+
*
|
|
47
|
+
* `launcher` is bin/segreant.mjs as started: process.argv[1] in the runtime
|
|
48
|
+
* child, which the launcher spawns with its own real path, never the snapshot.
|
|
49
|
+
*/
|
|
50
|
+
export function statuslineSetupText(launcher) {
|
|
51
|
+
const bin = launcher ? join(dirname(launcher), 'segreant-statusline.mjs').replace(/\\/g, '/') : null;
|
|
52
|
+
if (bin === null || /[\\/]_npx[\\/]/.test(launcher)) {
|
|
53
|
+
return ` Segreant is running from npx's temporary cache, which can be cleared at any time.
|
|
54
|
+
Install it so the status line has a file that stays put:
|
|
55
|
+
|
|
56
|
+
npm install -g segreant
|
|
57
|
+
|
|
58
|
+
then run segreant statusline --setup again for the exact line to add.`;
|
|
59
|
+
}
|
|
60
|
+
const command = `node "${bin}"`;
|
|
61
|
+
return ` Add this to ~/.claude/settings.json (or merge it into an existing statusLine):
|
|
62
|
+
|
|
63
|
+
"statusLine": ${JSON.stringify({ type: 'command', command })}
|
|
64
|
+
|
|
65
|
+
It runs the installed file directly, so nothing has to be on PATH.
|
|
66
|
+
${STATUSLINE_ABOUT}`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* `segreant statusline`: the same lean path as the `segreant-statusline` bin
|
|
70
|
+
* (src/quota/statusline.ts), reachable through the full CLI for convenience.
|
|
71
|
+
* The bin is what the setup snippet names, because it starts far faster.
|
|
72
|
+
*/
|
|
73
|
+
export function cmdStatusline(flags) {
|
|
74
|
+
if (flags.setup) {
|
|
75
|
+
console.log(statuslineSetupText(process.argv[1]));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
let raw = '';
|
|
79
|
+
try {
|
|
80
|
+
raw = readFileSync(0, 'utf8').slice(0, 1_000_000);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
raw = '';
|
|
84
|
+
}
|
|
85
|
+
process.stdout.write(statuslineLine(dbPath(), raw, Date.now()) + '\n');
|
|
86
|
+
}
|
|
87
|
+
export function cmdQuota(flags) {
|
|
88
|
+
const store = new Store(dbPath());
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
const events = store.quotaEvents(now - 35 * DAY);
|
|
91
|
+
const view = quotaView(events, (source, a, b) => store.sourceCostBetween(source, a, b), now);
|
|
92
|
+
store.close();
|
|
93
|
+
if (flags.json) {
|
|
94
|
+
printJson({ generatedAtMs: now, ...view });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const tty = process.stdout.isTTY ?? false;
|
|
98
|
+
const plans = new Map(detectPlans().map((p) => [p.source, planName(p)]));
|
|
99
|
+
console.log('');
|
|
100
|
+
console.log(color(tty, C.bold, ' Quota') + color(tty, C.gray, " the vendors' own meters, read from their tools' logs · refresh with segreant import all"));
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log(color(tty, C.bold, ` Codex${plans.get('codex') ? ` (${plans.get('codex')})` : ''}`));
|
|
103
|
+
if (view.codex === null) {
|
|
104
|
+
console.log(color(tty, C.gray, ' No rate-limit readings in the Codex logs yet.'));
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
for (const w of view.codex.windows) {
|
|
108
|
+
const label = windowLabel(w).padEnd(7);
|
|
109
|
+
if (w.reset) {
|
|
110
|
+
console.log(` ${label} ${color(tty, C.gray, `reset at ${when(w.resetsAtMs, now)}; no use logged since, so no current reading`)}`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const resets = w.resetsAtMs === null ? '' : ` · resets ${when(w.resetsAtMs, now)} (in ${inFor(w.resetsAtMs, now)})`;
|
|
114
|
+
const tone = w.usedPercent >= 90 ? C.red : w.usedPercent >= 70 ? C.yellow : C.green;
|
|
115
|
+
console.log(` ${label} ${color(tty, tone, `${Math.round(w.usedPercent)}% used`)}${resets}${color(tty, C.gray, ` · read ${when(w.asOfMs, now)}`)}`);
|
|
116
|
+
if (w.limitAtMs !== null && w.limitAtMs <= now) {
|
|
117
|
+
console.log(color(tty, C.yellow, ` at the pace of that reading the limit was due ${when(w.limitAtMs, now)}; nothing has been logged since, so whether it was reached is not known yet`));
|
|
118
|
+
}
|
|
119
|
+
else if (w.limitAtMs !== null) {
|
|
120
|
+
console.log(color(tty, C.yellow, ` at this pace you reach the limit ${when(w.limitAtMs, now)} (estimate: the pace since the window opened)`));
|
|
121
|
+
}
|
|
122
|
+
else if (w.projectedAtResetPercent !== null) {
|
|
123
|
+
console.log(color(tty, C.gray, ` at this pace: about ${Math.round(w.projectedAtResetPercent)}% by the reset (estimate)`));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const m = view.codex.weekly;
|
|
127
|
+
if (m.currentUsdPerPercent !== null) {
|
|
128
|
+
const earlier = median(m.earlierUsdPerPercent);
|
|
129
|
+
const line = ` Segreant counted ${usd(m.currentUsdPerPercent)} of list-price work per 1% of the weekly meter this week`
|
|
130
|
+
+ (earlier === null ? '' : `; ${usd(earlier)} per 1% in earlier weeks (median of ${m.earlierUsdPerPercent.length})`);
|
|
131
|
+
console.log(color(tty, C.gray, line));
|
|
132
|
+
if (m.agrees === false) {
|
|
133
|
+
console.log(color(tty, C.yellow, ' The meter and the count disagree beyond 1.5x this week. Both figures are above; the vendor may weight models or caching differently from list price.'));
|
|
134
|
+
}
|
|
135
|
+
else if (m.agrees === true) {
|
|
136
|
+
console.log(color(tty, C.gray, ' The meter moves in step with what Segreant counted.'));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
console.log('');
|
|
141
|
+
console.log(color(tty, C.bold, ` Claude Code${plans.get('claude-code') ? ` (${plans.get('claude-code')})` : ''}`));
|
|
142
|
+
const c = view.claude;
|
|
143
|
+
if (c === null) {
|
|
144
|
+
console.log(color(tty, C.gray, ' No Claude Code use or limit messages in the last 35 days.'));
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
if (view.claudeWindows.length > 0) {
|
|
148
|
+
for (const w of view.claudeWindows) {
|
|
149
|
+
const label = (w.kind === 'claude_spend_window' ? 'spend' : windowLabel(w)).padEnd(7);
|
|
150
|
+
if (w.reset) {
|
|
151
|
+
console.log(` ${label} ${color(tty, C.gray, `reset at ${when(w.resetsAtMs, now)}; no reading since`)}`);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const resets = w.resetsAtMs === null ? '' : ` · resets ${when(w.resetsAtMs, now)} (in ${inFor(w.resetsAtMs, now)})`;
|
|
155
|
+
const tone = w.usedPercent >= 90 ? C.red : w.usedPercent >= 70 ? C.yellow : C.green;
|
|
156
|
+
console.log(` ${label} ${color(tty, tone, `${Math.round(w.usedPercent)}% used`)}${resets}${color(tty, C.gray, ` · read ${when(w.asOfMs, now)} from the status line`)}`);
|
|
157
|
+
if (w.limitAtMs !== null && w.limitAtMs > now) {
|
|
158
|
+
console.log(color(tty, C.yellow, ` at this pace you reach the limit ${when(w.limitAtMs, now)} (estimate: the pace since the window opened)`));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
console.log(color(tty, C.gray, ' Claude Code logs no percentage; it logs a message when a limit is reached. For the live percentage, set segreant as'));
|
|
164
|
+
console.log(color(tty, C.gray, ' your Claude Code status line (segreant statusline --setup shows how), or see claude.ai/settings/usage.'));
|
|
165
|
+
}
|
|
166
|
+
const parts = [
|
|
167
|
+
`the session limit ${c.sessionHits} time${c.sessionHits === 1 ? '' : 's'}`,
|
|
168
|
+
`the weekly limit ${c.weeklyHits} time${c.weeklyHits === 1 ? '' : 's'}`,
|
|
169
|
+
...(c.spendHits > 0 ? [`the monthly spend limit ${c.spendHits} time${c.spendHits === 1 ? '' : 's'}`] : []),
|
|
170
|
+
];
|
|
171
|
+
console.log(` Last 35 days: you reached ${parts.join(', ')}.`);
|
|
172
|
+
if (c.lastHit !== null) {
|
|
173
|
+
const reset = c.lastHit.resetsAtMs === null ? '' : `, reset ${when(c.lastHit.resetsAtMs, now)}`;
|
|
174
|
+
console.log(color(tty, C.gray, ` Last: ${when(c.lastHit.tsEpochMs, now)}${reset} ("${c.lastHit.detail ?? ''}")`));
|
|
175
|
+
}
|
|
176
|
+
const before = c.usdBeforeSessionHit.filter((x) => x > 0);
|
|
177
|
+
const med = median(before);
|
|
178
|
+
if (med !== null) {
|
|
179
|
+
const lo = Math.min(...before);
|
|
180
|
+
const hi = Math.max(...before);
|
|
181
|
+
console.log(` Before a session-limit message, the 5 hours held ${usd(lo)}–${usd(hi)} of list-price work (median ${usd(med)}).`);
|
|
182
|
+
const share = Math.round((c.lastFiveHoursUsd / med) * 100);
|
|
183
|
+
console.log(color(tty, C.gray, ` The last 5 hours: ${usd(c.lastFiveHoursUsd)}, about ${share}% of that median (estimate: the window's real start is not logged).`));
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
console.log(color(tty, C.gray, ` The last 5 hours: ${usd(c.lastFiveHoursUsd)} of list-price work.`));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
console.log('');
|
|
190
|
+
}
|