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/flags.js
CHANGED
|
@@ -49,6 +49,74 @@ export function parseFlags(argv) {
|
|
|
49
49
|
}
|
|
50
50
|
return flags;
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Every option name some command reads, used ONLY to suggest a correction for
|
|
54
|
+
* a mistyped one. It is not an allowlist: commands read options through
|
|
55
|
+
* helpers and arrays a source scan cannot follow, so rejecting by this list
|
|
56
|
+
* would refuse valid options.
|
|
57
|
+
*/
|
|
58
|
+
const KNOWN_OPTION_NAMES = [
|
|
59
|
+
'account-ref', 'all', 'allow-unmetered', 'apply', 'archive', 'as-of', 'at', 'attempts', 'auto', 'category', 'centre',
|
|
60
|
+
'cache-read', 'cache-write', 'cache-write-1h', 'clear', 'clear-webhook', 'close-status', 'commit', 'control', 'coverage', 'cwd', 'daily', 'dashboard-port', 'data-class',
|
|
61
|
+
'days', 'debug', 'decision', 'deep', 'demo', 'detail', 'disabled', 'dry-run', 'economic', 'effective-at', 'every',
|
|
62
|
+
'exact-money', 'file', 'finalize', 'format', 'free', 'from', 'grain', 'help', 'id', 'import-id', 'include-imported', 'input', 'into',
|
|
63
|
+
'json', 'key', 'key-id', 'kind', 'labor-rate', 'like', 'limit', 'list', 'manifest', 'materiality', 'me', 'method', 'mode',
|
|
64
|
+
'name', 'note', 'notify', 'notify-url', 'origin', 'out', 'output', 'owner', 'path-prefix', 'policy', 'port', 'priority',
|
|
65
|
+
'project', 'project-ref', 'pubkey', 'purpose', 'rating', 'reason', 'recommend', 'reconcile', 'record-id', 'refresh',
|
|
66
|
+
'reopen', 'repo', 'request', 'rescan', 'risk', 'root', 'runaway', 'scope', 'sensitivity', 'serve', 'session',
|
|
67
|
+
'set-webhook', 'setup', 'sign', 'soft', 'start', 'target-currency', 'to', 'tool', 'trust', 'ts', 'tsf', 'unit',
|
|
68
|
+
'until', 'url', 'use', 'verdict', 'verify', 'watch', 'window', 'wrap', 'write',
|
|
69
|
+
];
|
|
70
|
+
function editDistance(a, b) {
|
|
71
|
+
const row = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
72
|
+
for (let i = 1; i <= a.length; i++) {
|
|
73
|
+
let prev = row[0];
|
|
74
|
+
row[0] = i;
|
|
75
|
+
for (let j = 1; j <= b.length; j++) {
|
|
76
|
+
const keep = row[j];
|
|
77
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
78
|
+
prev = keep;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return row[b.length];
|
|
82
|
+
}
|
|
83
|
+
/** The closest known option name, when one is close enough to be the intended one. */
|
|
84
|
+
export function suggestOption(name) {
|
|
85
|
+
let best = null;
|
|
86
|
+
let bestD = Infinity;
|
|
87
|
+
for (const known of KNOWN_OPTION_NAMES) {
|
|
88
|
+
const d = editDistance(name, known);
|
|
89
|
+
if (d < bestD) {
|
|
90
|
+
best = known;
|
|
91
|
+
bestD = d;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return best !== null && bestD <= Math.max(1, Math.floor(name.length / 3)) ? best : null;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Wrap parsed flags so the options a command actually read are recorded.
|
|
98
|
+
* `segreant budget --dayly 5` used to exit 0 having set nothing (H014), and a
|
|
99
|
+
* command that ignores `--json` printed prose without saying so (H013). After
|
|
100
|
+
* a command finishes, every option it never read is reported and the exit code
|
|
101
|
+
* is 2. A mistyped `--apply` is therefore caught after the default preview,
|
|
102
|
+
* which writes nothing.
|
|
103
|
+
*/
|
|
104
|
+
export function trackFlags(flags) {
|
|
105
|
+
const read = new Set(['_']);
|
|
106
|
+
const proxy = new Proxy(flags, {
|
|
107
|
+
get(target, key, receiver) {
|
|
108
|
+
if (typeof key === 'string')
|
|
109
|
+
read.add(key);
|
|
110
|
+
return Reflect.get(target, key, receiver);
|
|
111
|
+
},
|
|
112
|
+
has(target, key) {
|
|
113
|
+
if (typeof key === 'string')
|
|
114
|
+
read.add(key);
|
|
115
|
+
return Reflect.has(target, key);
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
return { flags: proxy, unread: () => Object.keys(flags).filter((k) => !read.has(k)) };
|
|
119
|
+
}
|
|
52
120
|
export function rangeFor(window) {
|
|
53
121
|
const now = Date.now();
|
|
54
122
|
const day = 24 * 60 * 60 * 1000;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bare `segreant`: the first thing anyone sees.
|
|
3
|
+
*
|
|
4
|
+
* Read-only, like every command without --apply: it never imports on its own.
|
|
5
|
+
* With nothing recorded yet it says what Segreant does, which tools it can read
|
|
6
|
+
* on this machine, and the one command that reads them. With a ledger it leads
|
|
7
|
+
* with the month, the plans, and how much of the AI work stayed in the code
|
|
8
|
+
* (from the last measurement), then the next step. `segreant guide` keeps the
|
|
9
|
+
* full checklist.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { Store } from '../store/db.js';
|
|
13
|
+
import { dbPath, isDemo } from '../config.js';
|
|
14
|
+
import { defaultClaudeCodeRoot } from '../connect/claudeCode.js';
|
|
15
|
+
import { defaultCodexRoot } from '../connect/codex.js';
|
|
16
|
+
import { defaultOpencodeDbPath } from '../connect/opencode.js';
|
|
17
|
+
import { defaultAntigravityRoot } from '../connect/antigravity.js';
|
|
18
|
+
import { detectPlans, planName } from '../plans/detect.js';
|
|
19
|
+
import { keptSummary, realizationFromStore } from '../value/realization.js';
|
|
20
|
+
import { planComparisons, printPlanLines } from './planCmd.js';
|
|
21
|
+
import { summarizeBasis } from '../cost/basis.js';
|
|
22
|
+
import { basisLines } from './basisLines.js';
|
|
23
|
+
import { C, color, usd, num, printJson } from './ui.js';
|
|
24
|
+
import { rangeFor } from './flags.js';
|
|
25
|
+
const DAY = 86_400_000;
|
|
26
|
+
function toolsOnThisMachine() {
|
|
27
|
+
const plans = new Map(detectPlans().map((p) => [p.source, planName(p)]));
|
|
28
|
+
const out = [];
|
|
29
|
+
if (existsSync(defaultClaudeCodeRoot()))
|
|
30
|
+
out.push({ id: 'claude-code', label: 'Claude Code', plan: plans.get('claude-code') ?? null });
|
|
31
|
+
const codex = defaultCodexRoot();
|
|
32
|
+
if (codex !== null && existsSync(codex))
|
|
33
|
+
out.push({ id: 'codex', label: 'Codex', plan: plans.get('codex') ?? null });
|
|
34
|
+
const opencode = defaultOpencodeDbPath();
|
|
35
|
+
if (opencode !== null && existsSync(opencode))
|
|
36
|
+
out.push({ id: 'opencode', label: 'opencode', plan: null });
|
|
37
|
+
if (defaultAntigravityRoot() !== null)
|
|
38
|
+
out.push({ id: 'antigravity', label: 'Antigravity', plan: null });
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
export function cmdHome(flags) {
|
|
42
|
+
const store = new Store(dbPath());
|
|
43
|
+
const month = rangeFor('month');
|
|
44
|
+
const summary = store.summary(month.startMs, month.endMs);
|
|
45
|
+
const everything = store.summary(0, Date.now() + 1000);
|
|
46
|
+
const tools = toolsOnThisMachine();
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
// The last measurement of what stayed in the code, over the last 90 days of commits.
|
|
49
|
+
const stored = realizationFromStore(store);
|
|
50
|
+
const recent = stored.units.filter((u) => u.tsEpochMs >= now - 90 * DAY);
|
|
51
|
+
const kept = recent.length > 0 ? keptSummary({ units: recent, windowDays: stored.windowDays }) : null;
|
|
52
|
+
const plans = everything.requests > 0 && !isDemo() ? planComparisons(store, month.startMs, month.endMs) : [];
|
|
53
|
+
const basis = summarizeBasis(store.pricingEvidenceByModel(month.startMs, month.endMs));
|
|
54
|
+
store.close();
|
|
55
|
+
if (flags.json) {
|
|
56
|
+
printJson({
|
|
57
|
+
empty: everything.requests === 0,
|
|
58
|
+
tools,
|
|
59
|
+
last30Days: { listCostUsd: summary.costUsd, requests: summary.requests, basis },
|
|
60
|
+
kept: kept === null ? null : { ...kept, asOf: new Date(stored.generatedAt).toISOString() },
|
|
61
|
+
plans,
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const tty = process.stdout.isTTY ?? false;
|
|
66
|
+
console.log('');
|
|
67
|
+
console.log(color(tty, C.bold, ' Segreant') + color(tty, C.gray, ' — what your AI coding spend produced, measured on your machine') + (isDemo() ? color(tty, C.yellow, ' ● DEMO DATA') : ''));
|
|
68
|
+
console.log('');
|
|
69
|
+
if (everything.requests === 0) {
|
|
70
|
+
if (tools.length > 0) {
|
|
71
|
+
const found = tools.map((t) => `${t.label}${t.plan ? ` (${t.plan})` : ''}`).join(' · ');
|
|
72
|
+
console.log(` Found on this machine: ${color(tty, C.bold, found)}`);
|
|
73
|
+
console.log('');
|
|
74
|
+
console.log(' Read their logs and measure what the work produced:');
|
|
75
|
+
console.log(color(tty, C.cyan, ' segreant scan --setup'));
|
|
76
|
+
console.log(color(tty, C.gray, ' Nothing about you leaves this machine. The first run takes a few minutes; the month appears after about one.'));
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
console.log(' No AI coding tool logs found here (Claude Code, Codex, Antigravity and opencode are read today).');
|
|
80
|
+
console.log(color(tty, C.cyan, ' segreant scan') + color(tty, C.gray, ' looks further and lists what it can and cannot read'));
|
|
81
|
+
}
|
|
82
|
+
console.log('');
|
|
83
|
+
console.log(color(tty, C.gray, ' See every screen with sample data first: segreant demo --serve'));
|
|
84
|
+
console.log('');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const plainList = basis.cohorts.length === 1 && basis.cohorts[0].id === 'list_exact';
|
|
88
|
+
console.log(` ${color(tty, C.bold, 'Last 30 days')} ${color(tty, C.green, usd(summary.costUsd))} ${basis.headlineLabel.toLowerCase()} ${color(tty, C.gray, plainList
|
|
89
|
+
? `(${num(summary.requests)} requests · each model's API list rate · not your bill)`
|
|
90
|
+
: `(${num(summary.requests)} requests, priced in parts:)`)}`);
|
|
91
|
+
if (!plainList)
|
|
92
|
+
for (const line of basisLines(basis, ' '))
|
|
93
|
+
console.log(color(tty, C.gray, line));
|
|
94
|
+
printPlanLines(tty, plans);
|
|
95
|
+
console.log('');
|
|
96
|
+
if (kept !== null && kept.kept.units + kept.notKept.units + kept.contested.units + kept.unknown.units + kept.maturing.units > 0) {
|
|
97
|
+
const asOf = new Date(stored.generatedAt).toISOString().slice(0, 10);
|
|
98
|
+
console.log(color(tty, C.bold, ' Did the AI work stay in the code?') + color(tty, C.gray, ` commits of the last 90 days, all projects · measured ${asOf}`));
|
|
99
|
+
const row = (label, b, tone, what) => {
|
|
100
|
+
if (b.units === 0)
|
|
101
|
+
return;
|
|
102
|
+
console.log(` ${label.padEnd(10)} ${color(tty, tone, usd(b.costUsd).padStart(10))} ${String(b.units).padStart(4)} commits ${color(tty, C.gray, what)}`);
|
|
103
|
+
};
|
|
104
|
+
row('Kept', kept.kept, C.green, 'still in the code after 14 days, no tracked check failed');
|
|
105
|
+
row('Not kept', kept.notKept, C.yellow, 'rewritten, removed or reverted');
|
|
106
|
+
row('Disputed', kept.contested, C.gray, 'the checks disagreed; not settled either way');
|
|
107
|
+
row('Unknown', kept.unknown, C.gray, 'not measured');
|
|
108
|
+
row('Maturing', kept.maturing, C.gray, 'younger than 14 days when measured');
|
|
109
|
+
console.log(color(tty, C.gray, ' Refresh: segreant scan --setup · one project in full: segreant realize --repo <path>'));
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
console.log(color(tty, C.gray, ' Not measured yet: what the work produced. Run segreant scan --setup, or segreant realize --repo <path> for one project.'));
|
|
113
|
+
}
|
|
114
|
+
console.log('');
|
|
115
|
+
console.log(` ${color(tty, C.bold, 'Next')} ${color(tty, C.cyan, 'segreant start')}${color(tty, C.gray, ' opens the dashboard')} · ${color(tty, C.cyan, 'segreant quota')}${color(tty, C.gray, ' your plan limits')} · ${color(tty, C.cyan, 'segreant guide')}${color(tty, C.gray, ' the full checklist')}`);
|
|
116
|
+
console.log('');
|
|
117
|
+
}
|
package/dist/cli/importCmd.js
CHANGED
|
@@ -6,15 +6,20 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { Store } from '../store/db.js';
|
|
9
|
-
import { dbPath, loadConfig } from '../config.js';
|
|
10
|
-
import { pricingStatus } from '../cost/pricing.js';
|
|
11
|
-
import {
|
|
9
|
+
import { dbPath, loadConfig, isDemo, DemoWriteBlockedError } from '../config.js';
|
|
10
|
+
import { pricingStatus, refreshPricing, DEFAULT_MANIFEST_URL } from '../cost/pricing.js';
|
|
11
|
+
import { priceCheckDue } from '../cost/firstSight.js';
|
|
12
|
+
import { realizeDiscoveredProjects, projectValueBreakdown, CLI_PERIOD_DAYS, CLI_GIT_BUDGET_MS, SCAN_VALUE_BUDGET_MS, } from '../value/realization.js';
|
|
12
13
|
import { scanWithDiff, saveScan } from '../scan/scan.js';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { importCodex, defaultCodexRoot } from '../connect/codex.js';
|
|
14
|
+
import { defaultClaudeCodeRoot } from '../connect/claudeCode.js';
|
|
15
|
+
import { IMPORT_REGISTRY, importerFor } from '../connect/registry.js';
|
|
16
16
|
import {} from '../connect/importShared.js';
|
|
17
17
|
import { C, color, usd, num, printJson } from './ui.js';
|
|
18
|
+
import { rangeFor } from './flags.js';
|
|
19
|
+
import { askPlanPrices } from './planCmd.js';
|
|
20
|
+
import { askModelPrices } from './askModelPrices.js';
|
|
21
|
+
import { summarizeBasis } from '../cost/basis.js';
|
|
22
|
+
import { basisLines } from './basisLines.js';
|
|
18
23
|
/**
|
|
19
24
|
* Live import: poll the source(s) on an interval and fold in new traffic as it
|
|
20
25
|
* appears. Reads are read-only snapshots (SQLite WAL / streamed JSONL), so the
|
|
@@ -78,33 +83,107 @@ async function cmdImportWatch(targets, opts) {
|
|
|
78
83
|
process.on('SIGTERM', stop);
|
|
79
84
|
});
|
|
80
85
|
}
|
|
81
|
-
const IMPORT_RUNNERS = {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
location: (r) => r ?? defaultClaudeCodeRoot(),
|
|
85
|
-
run:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
label: 'opencode',
|
|
89
|
-
location: (r) => r ?? defaultOpencodeDbPath() ?? '(opencode not found on this machine)',
|
|
90
|
-
run: (store, opts) => importOpencode(store, opts),
|
|
91
|
-
},
|
|
92
|
-
codex: {
|
|
93
|
-
label: 'Codex CLI',
|
|
94
|
-
location: (r) => r ?? defaultCodexRoot() ?? '(Codex not found on this machine)',
|
|
95
|
-
run: (store, opts) => importCodex(store, opts),
|
|
96
|
-
},
|
|
97
|
-
};
|
|
86
|
+
const IMPORT_RUNNERS = Object.fromEntries(IMPORT_REGISTRY.map((e) => [e.id, {
|
|
87
|
+
label: e.label,
|
|
88
|
+
// Claude Code's root is reported even when absent (it says where to look).
|
|
89
|
+
location: (r) => r ?? e.locate() ?? (e.id === 'claude-code' ? defaultClaudeCodeRoot() : `(${e.label} not found on this machine)`),
|
|
90
|
+
run: e.run,
|
|
91
|
+
recentFirst: e.recentFirst,
|
|
92
|
+
}]));
|
|
98
93
|
/** Normalize the aliases users actually type. */
|
|
99
94
|
function resolveImporterId(what) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
95
|
+
return importerFor(what)?.id ?? null;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* One source's two passes as one summary. The backfill counted every file
|
|
99
|
+
* again (the recent ones as unchanged), so file and truncation totals come
|
|
100
|
+
* from it; rows and money add, because each pass inserted different rows.
|
|
101
|
+
*/
|
|
102
|
+
function mergeImportSummaries(recent, rest) {
|
|
103
|
+
if (recent === undefined)
|
|
104
|
+
return rest;
|
|
105
|
+
const byModel = {};
|
|
106
|
+
for (const part of [recent.byModel, rest.byModel]) {
|
|
107
|
+
for (const [model, v] of Object.entries(part)) {
|
|
108
|
+
const into = byModel[model] ?? { requests: 0, costUsd: 0 };
|
|
109
|
+
into.requests += v.requests;
|
|
110
|
+
into.costUsd += v.costUsd;
|
|
111
|
+
byModel[model] = into;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const lo = [recent.earliestMs, rest.earliestMs].filter((v) => v !== null);
|
|
115
|
+
const hi = [recent.latestMs, rest.latestMs].filter((v) => v !== null);
|
|
116
|
+
const conflicts = (recent.conflictingObservations ?? 0) + (rest.conflictingObservations ?? 0);
|
|
117
|
+
return {
|
|
118
|
+
...rest,
|
|
119
|
+
inserted: recent.inserted + rest.inserted,
|
|
120
|
+
eventsSeen: recent.eventsSeen + rest.eventsSeen,
|
|
121
|
+
costUsd: recent.costUsd + rest.costUsd,
|
|
122
|
+
estimatedCostUsd: recent.estimatedCostUsd + rest.estimatedCostUsd,
|
|
123
|
+
byModel,
|
|
124
|
+
earliestMs: lo.length > 0 ? Math.min(...lo) : null,
|
|
125
|
+
latestMs: hi.length > 0 ? Math.max(...hi) : null,
|
|
126
|
+
relabelled: [...recent.relabelled, ...rest.relabelled],
|
|
127
|
+
...(conflicts > 0 ? { conflictingObservations: conflicts } : {}),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** "kept $1,643 · not kept $408": a project's answer from git, or why there is none yet. */
|
|
131
|
+
function keptLine(tty, d) {
|
|
132
|
+
if (!d.measured)
|
|
133
|
+
return color(tty, C.gray, d.skipped === 'no-spend' ? 'no priced spend' : 'not measured in this run');
|
|
134
|
+
const k = d.kept;
|
|
135
|
+
if (k === undefined || k.kept.units + k.notKept.units + k.contested.units + k.unknown.units === 0) {
|
|
136
|
+
return color(tty, C.gray, k !== undefined && k.maturing.units > 0 ? 'maturing (commits under 14 days)' : 'no commits to judge');
|
|
137
|
+
}
|
|
138
|
+
const parts = [];
|
|
139
|
+
if (k.kept.costUsd >= 0.005)
|
|
140
|
+
parts.push(color(tty, C.green, `kept ${usd(k.kept.costUsd)}`));
|
|
141
|
+
if (k.notKept.costUsd >= 0.005)
|
|
142
|
+
parts.push(color(tty, C.yellow, `not kept ${usd(k.notKept.costUsd)}`));
|
|
143
|
+
if (k.contested.costUsd >= 0.005)
|
|
144
|
+
parts.push(color(tty, C.gray, `disputed ${usd(k.contested.costUsd)}`));
|
|
145
|
+
if (k.unknown.costUsd >= 0.005)
|
|
146
|
+
parts.push(color(tty, C.gray, `not measured yet ${usd(k.unknown.costUsd)}`));
|
|
147
|
+
if (k.maturing.costUsd >= 0.005)
|
|
148
|
+
parts.push(color(tty, C.gray, `maturing ${usd(k.maturing.costUsd)}`));
|
|
149
|
+
if (parts.length === 0)
|
|
150
|
+
return color(tty, C.gray, 'no spend on its commits');
|
|
151
|
+
return parts.join(color(tty, C.gray, ' · '));
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The part of a project's period spend that is on no measured commit, with its
|
|
155
|
+
* reasons. Printed under the project line so the buckets above plus this line
|
|
156
|
+
* add up to the project's total; before, $222 of one project's $567 had no line.
|
|
157
|
+
*/
|
|
158
|
+
export function offCommitLine(d) {
|
|
159
|
+
const c = d.coverage;
|
|
160
|
+
if (c === undefined)
|
|
161
|
+
return null;
|
|
162
|
+
const parts = [];
|
|
163
|
+
if (c.noCommitFollowedUsd >= 0.005)
|
|
164
|
+
parts.push(`${usd(c.noCommitFollowedUsd)} no commit followed within 8 hours`);
|
|
165
|
+
if (c.beforeOldestUsd >= 0.005)
|
|
166
|
+
parts.push(`${usd(c.beforeOldestUsd)} before the oldest commit measured`);
|
|
167
|
+
if (c.notCommittedYetUsd >= 0.005)
|
|
168
|
+
parts.push(`${usd(c.notCommittedYetUsd)} not committed yet`);
|
|
169
|
+
return parts.length === 0 ? null : `not on a commit: ${parts.join(' · ')}`;
|
|
170
|
+
}
|
|
171
|
+
/** Repositories the shared deadline did not reach: named, with the command that measures them. */
|
|
172
|
+
function printUnmeasured(tty, discovered) {
|
|
173
|
+
const left = discovered.filter((d) => d.skipped === 'deadline');
|
|
174
|
+
if (left.length > 0) {
|
|
175
|
+
const spend = left.reduce((s, d) => s + (d.periodCostUsd ?? d.costUsd), 0);
|
|
176
|
+
console.log(color(tty, C.gray, ` ${left.length} smaller project(s) (${usd(spend)} of spend) were not measured in the time this scan allows.`));
|
|
177
|
+
console.log(color(tty, C.gray, ` Measure one with: segreant realize --repo "${left[0].repoPath}"`));
|
|
178
|
+
}
|
|
179
|
+
// The scan keeps to its time budget, so a large project can come back partly
|
|
180
|
+
// measured. Name the biggest such gap and the command that finishes it.
|
|
181
|
+
const partial = discovered
|
|
182
|
+
.filter((d) => d.measured && d.kept !== undefined && d.kept.unknown.costUsd >= 1)
|
|
183
|
+
.sort((a, b) => b.kept.unknown.costUsd - a.kept.unknown.costUsd)[0];
|
|
184
|
+
if (partial !== undefined) {
|
|
185
|
+
console.log(color(tty, C.gray, ` ${partial.project}: ${usd(partial.kept.unknown.costUsd)} not measured yet in the time this scan allows. Finish it with: segreant realize --repo "${partial.repoPath}"`));
|
|
186
|
+
}
|
|
108
187
|
}
|
|
109
188
|
/** "3 files, 507 lines" — the parts of an import that hit a resource bound. */
|
|
110
189
|
function truncationDetail(sum) {
|
|
@@ -186,6 +265,12 @@ export async function cmdImport(flags) {
|
|
|
186
265
|
process.exitCode = 1;
|
|
187
266
|
return;
|
|
188
267
|
}
|
|
268
|
+
// Demo mode opens demo.db. Real usage must not land there, under sample-data labels.
|
|
269
|
+
if (isDemo()) {
|
|
270
|
+
console.error(` ${new DemoWriteBlockedError('the import').message}`);
|
|
271
|
+
process.exitCode = 1;
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
189
274
|
const days = typeof flags.days === 'string' ? Number(flags.days) : null;
|
|
190
275
|
const sinceMs = days && days > 0 ? Date.now() - days * 24 * 60 * 60 * 1000 : 0;
|
|
191
276
|
const root = typeof flags.root === 'string' ? flags.root : undefined;
|
|
@@ -253,7 +338,10 @@ export async function cmdDiscover(flags) {
|
|
|
253
338
|
const store = new Store(dbPath());
|
|
254
339
|
const paths = store.projectPaths();
|
|
255
340
|
const progress = repoProgress(flags);
|
|
256
|
-
const discovered = await realizeDiscoveredProjects(store, {
|
|
341
|
+
const discovered = await realizeDiscoveredProjects(store, {
|
|
342
|
+
windowDays, onProgress: progress.onProgress, sinceDays: CLI_PERIOD_DAYS, gitScanBudgetMs: CLI_GIT_BUDGET_MS,
|
|
343
|
+
stageBudgetMs: SCAN_VALUE_BUDGET_MS,
|
|
344
|
+
});
|
|
257
345
|
progress.finish();
|
|
258
346
|
const projects = projectValueBreakdown(store, { windowDays });
|
|
259
347
|
store.close();
|
|
@@ -286,10 +374,11 @@ export async function cmdDiscover(flags) {
|
|
|
286
374
|
? color(tty, C.gray, 'RoI —')
|
|
287
375
|
: color(tty, roi > 60 ? C.green : roi > 30 ? C.yellow : C.red, `RoI ${Math.round(roi)}`);
|
|
288
376
|
const tools = d.sources.length ? d.sources.join(', ') : 'unknown';
|
|
289
|
-
console.log(` ${color(tty, C.bold, d.project.padEnd(22))} ${usd(d.costUsd).padStart(10)} ${d
|
|
377
|
+
console.log(` ${color(tty, C.bold, d.project.padEnd(22))} ${usd(d.costUsd).padStart(10)} ${keptLine(tty, d)} ${roiStr}`);
|
|
290
378
|
console.log(color(tty, C.gray, ` ${d.repoPath}`));
|
|
291
379
|
console.log(color(tty, C.gray, ` coded with: ${tools}`));
|
|
292
380
|
}
|
|
381
|
+
printUnmeasured(tty, discovered);
|
|
293
382
|
console.log('');
|
|
294
383
|
console.log(color(tty, C.gray, ' RoI here scores every stored unit (all time); "segreant roi --repo" scopes to a window — the two can differ.'));
|
|
295
384
|
console.log(color(tty, C.gray, ' Now live in: segreant roi · segreant today · the dashboard (By project).'));
|
|
@@ -358,7 +447,7 @@ export async function cmdScan(flags) {
|
|
|
358
447
|
console.log('');
|
|
359
448
|
console.log(color(tty, C.bold, ' Scan — find your AI tools and repos, then set it all up'));
|
|
360
449
|
console.log(color(tty, C.gray, ' ' + '─'.repeat(64)));
|
|
361
|
-
console.log(color(tty, C.gray, '
|
|
450
|
+
console.log(color(tty, C.gray, ' Reads what exists on disk and remembers this scan to report changes. Nothing is imported or sent.'));
|
|
362
451
|
console.log('');
|
|
363
452
|
// Detected tools.
|
|
364
453
|
console.log(color(tty, C.bold, ' AI coding tools on this machine'));
|
|
@@ -375,7 +464,7 @@ export async function cmdScan(flags) {
|
|
|
375
464
|
if (otherPresent.length > 0) {
|
|
376
465
|
console.log(color(tty, C.bold, ' Other AI tools detected (not yet natively supported)'));
|
|
377
466
|
for (const a of otherPresent) {
|
|
378
|
-
console.log(` ${color(tty, C.gray, '·')} ${a.label.padEnd(16)} ${color(tty, C.gray, a.evidence ?? '')}`);
|
|
467
|
+
console.log(` ${color(tty, C.gray, '·')} ${a.label.padEnd(16)} ${color(tty, C.gray, a.notReadBecause ? `not read: ${a.notReadBecause}` : a.evidence ?? '')}`);
|
|
379
468
|
}
|
|
380
469
|
console.log(color(tty, C.gray, ' We see these exist, but don\'t read their usage data yet — spend from them isn\'t counted above.'));
|
|
381
470
|
console.log('');
|
|
@@ -412,7 +501,25 @@ export async function cmdScan(flags) {
|
|
|
412
501
|
// Imported rows keep the price they were recorded at, so a stale card turns
|
|
413
502
|
// every model newer than it into a fallback estimate for good (until a
|
|
414
503
|
// reprice). Say so BEFORE the import, where it can still be avoided.
|
|
415
|
-
const
|
|
504
|
+
const pricingCfg = loadConfig().pricing;
|
|
505
|
+
let prices = pricingStatus(pricingCfg.maxAgeDays);
|
|
506
|
+
// Imported rows keep the price they are recorded at, so the best moment to
|
|
507
|
+
// learn new models is before the import, not after it. Only when someone is
|
|
508
|
+
// at the terminal: a scripted import does not gain a network call it did not
|
|
509
|
+
// ask for (the running proxy still looks models up on first sight). Only the
|
|
510
|
+
// built-in public list, which is what this line and the privacy statement
|
|
511
|
+
// disclose: a custom pricing.manifestUrl is refreshed when its operator asks
|
|
512
|
+
// (segreant pricing --refresh, or autoRefresh), never by scan (PR #146 review).
|
|
513
|
+
const builtInList = pricingCfg.manifestUrl === null || pricingCfg.manifestUrl === undefined || pricingCfg.manifestUrl === DEFAULT_MANIFEST_URL;
|
|
514
|
+
if (flags.setup && !flags.json && tty && !isDemo() && pricingCfg.lookupUnknownModels && builtInList && priceCheckDue(prices)) {
|
|
515
|
+
console.log(color(tty, C.gray, ' Checking the public price list so imported requests get current prices (sends nothing about you)…'));
|
|
516
|
+
const checked = await refreshPricing(DEFAULT_MANIFEST_URL);
|
|
517
|
+
console.log(checked.ok
|
|
518
|
+
? color(tty, C.gray, ` Prices current: ${num(checked.modelCount ?? 0)} models on the card.`)
|
|
519
|
+
: color(tty, C.yellow, ` Price check skipped: ${checked.error ?? 'the price list could not be read'}.`));
|
|
520
|
+
prices = pricingStatus(pricingCfg.maxAgeDays);
|
|
521
|
+
console.log('');
|
|
522
|
+
}
|
|
416
523
|
if (prices.stale && !flags.json) {
|
|
417
524
|
const age = prices.ageDays === null ? 'of unknown age' : `${num(prices.ageDays)} days old`;
|
|
418
525
|
console.log(color(tty, C.yellow, ` Prices are ${age}: models released since then will be priced by a fallback estimate.`));
|
|
@@ -448,6 +555,34 @@ export async function cmdScan(flags) {
|
|
|
448
555
|
if (!flags.json)
|
|
449
556
|
console.log(color(tty, C.bold, ' Setting up…'));
|
|
450
557
|
let totalNew = 0;
|
|
558
|
+
// Newest first: the logs of the last 30 days, then the month's number, then
|
|
559
|
+
// the older history. The month is exact after the first pass (see
|
|
560
|
+
// ImportOptions.modifiedSinceMs); the backfill only adds older rows.
|
|
561
|
+
const month = rangeFor('month');
|
|
562
|
+
const reading = !flags.json && process.stderr.isTTY;
|
|
563
|
+
const recent = new Map();
|
|
564
|
+
for (const t of present) {
|
|
565
|
+
const runner = IMPORT_RUNNERS[t.id];
|
|
566
|
+
if (!runner)
|
|
567
|
+
continue;
|
|
568
|
+
if (reading)
|
|
569
|
+
process.stderr.write(` Reading the last 30 days of ${t.label} logs…`);
|
|
570
|
+
recent.set(t.id, await runner.run(store, runner.recentFirst ? { modifiedSinceMs: month.startMs } : {}));
|
|
571
|
+
if (reading)
|
|
572
|
+
process.stderr.write('\r\x1b[2K');
|
|
573
|
+
}
|
|
574
|
+
if (!flags.json && recent.size > 0) {
|
|
575
|
+
const m = store.summary(month.startMs, month.endMs);
|
|
576
|
+
const basis = summarizeBasis(store.pricingEvidenceByModel(month.startMs, month.endMs));
|
|
577
|
+
const plainList = basis.cohorts.length === 1 && basis.cohorts[0].id === 'list_exact';
|
|
578
|
+
console.log(` ${color(tty, C.bold, 'Last 30 days')} ${color(tty, C.green, usd(m.costUsd))} ${basis.headlineLabel.toLowerCase()} ${color(tty, C.gray, plainList
|
|
579
|
+
? `(${num(m.requests)} requests · each model's API list rate · not your bill)`
|
|
580
|
+
: `(${num(m.requests)} requests, priced in parts:)`)}`);
|
|
581
|
+
if (!plainList)
|
|
582
|
+
for (const line of basisLines(basis, ' '))
|
|
583
|
+
console.log(color(tty, C.gray, line));
|
|
584
|
+
console.log(color(tty, C.gray, ' Now reading older history, then measuring what the work produced…'));
|
|
585
|
+
}
|
|
451
586
|
for (const t of present) {
|
|
452
587
|
const runner = IMPORT_RUNNERS[t.id];
|
|
453
588
|
if (!runner) {
|
|
@@ -456,12 +591,18 @@ export async function cmdScan(flags) {
|
|
|
456
591
|
}
|
|
457
592
|
continue;
|
|
458
593
|
}
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
594
|
+
const first = recent.get(t.id);
|
|
595
|
+
let sum;
|
|
596
|
+
if (runner.recentFirst) {
|
|
597
|
+
if (reading)
|
|
598
|
+
process.stderr.write(` Reading older ${t.label} logs…`);
|
|
599
|
+
sum = mergeImportSummaries(first, await runner.run(store, {}));
|
|
600
|
+
if (reading)
|
|
601
|
+
process.stderr.write('\r\x1b[2K');
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
sum = first;
|
|
605
|
+
}
|
|
465
606
|
totalNew += sum.inserted;
|
|
466
607
|
if (!flags.json) {
|
|
467
608
|
console.log(` ${color(tty, C.green, '✓')} ${t.label.padEnd(16)} ${color(tty, C.green, `${num(sum.inserted)} new`)}` +
|
|
@@ -474,27 +615,42 @@ export async function cmdScan(flags) {
|
|
|
474
615
|
if (present.length === 0 && !flags.json)
|
|
475
616
|
console.log(color(tty, C.gray, ' No detected tools to import.'));
|
|
476
617
|
const progress = repoProgress(flags);
|
|
477
|
-
const discovered = await realizeDiscoveredProjects(store, {
|
|
618
|
+
const discovered = await realizeDiscoveredProjects(store, {
|
|
619
|
+
onProgress: progress.onProgress, sinceDays: CLI_PERIOD_DAYS, gitScanBudgetMs: CLI_GIT_BUDGET_MS,
|
|
620
|
+
stageBudgetMs: SCAN_VALUE_BUDGET_MS,
|
|
621
|
+
});
|
|
478
622
|
progress.finish();
|
|
479
|
-
const projects = projectValueBreakdown(store, {});
|
|
480
|
-
const roiByProject = new Map(projects.map((p) => [p.project, p.roiIndex]));
|
|
481
623
|
store.close();
|
|
482
624
|
if (!flags.json) {
|
|
483
625
|
console.log('');
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
const roiStr = roi == null
|
|
488
|
-
? color(tty, C.gray, 'RoI —')
|
|
489
|
-
: color(tty, roi > 60 ? C.green : roi > 30 ? C.yellow : C.red, `RoI ${Math.round(roi)}`);
|
|
626
|
+
const shown = discovered.filter((d) => d.skipped !== 'no-spend');
|
|
627
|
+
console.log(color(tty, C.bold, ` Did the AI work stay in the code?`) + color(tty, C.gray, ` last ${CLI_PERIOD_DAYS} days by project · list cost (estimate), not your bill`));
|
|
628
|
+
for (const d of shown.slice(0, 12)) {
|
|
490
629
|
const tools = d.sources.length ? d.sources.join(', ') : 'unknown';
|
|
491
|
-
console.log(` ${color(tty, C.bold, d.project.padEnd(
|
|
630
|
+
console.log(` ${color(tty, C.bold, d.project.padEnd(24))} ${usd(d.periodCostUsd ?? d.costUsd).padStart(10)} ${keptLine(tty, d)} ${color(tty, C.gray, `coded with: ${tools}`)}`);
|
|
631
|
+
const off = offCommitLine(d);
|
|
632
|
+
if (off !== null)
|
|
633
|
+
console.log(color(tty, C.gray, ` ${''.padEnd(24)} ${''.padStart(10)} ${off}`));
|
|
492
634
|
}
|
|
635
|
+
if (shown.length > 12)
|
|
636
|
+
console.log(color(tty, C.gray, ` …and ${shown.length - 12} more (segreant discover lists them all)`));
|
|
637
|
+
const idle = discovered.length - shown.length;
|
|
638
|
+
if (idle > 0)
|
|
639
|
+
console.log(color(tty, C.gray, ` ${idle} more repositor${idle === 1 ? 'y has' : 'ies have'} no priced AI spend in the last ${CLI_PERIOD_DAYS} days.`));
|
|
640
|
+
printUnmeasured(tty, discovered);
|
|
493
641
|
console.log('');
|
|
494
|
-
console.log(color(tty, C.gray, '
|
|
495
|
-
console.log(color(tty, C.gray,
|
|
642
|
+
console.log(color(tty, C.gray, ' Kept: most of a commit\'s lines are still in the code after 14 days and no check you track failed. Not kept: a tracked check failed, or the lines were rewritten, removed or reverted.'));
|
|
643
|
+
console.log(color(tty, C.gray, ' The full picture for one project: segreant realize --repo <path> · value against your time: segreant roi'));
|
|
644
|
+
console.log(color(tty, C.gray, ` Imported ${num(totalNew)} new request(s). Now live in: segreant today · month · the dashboard.`));
|
|
496
645
|
console.log(color(tty, C.gray, ' Safe to re-run any time to fold in new tools, repos, and traffic.'));
|
|
497
646
|
console.log('');
|
|
647
|
+
if (await askPlanPrices(tty) > 0)
|
|
648
|
+
console.log(color(tty, C.gray, ' Saved. segreant month now shows each plan beside the work it did.'));
|
|
649
|
+
await askModelPrices(tty);
|
|
650
|
+
if (present.some((t) => t.id === 'claude-code')) {
|
|
651
|
+
console.log(color(tty, C.gray, ' Claude\'s live usage meter: segreant statusline --setup shows the one line to add to Claude Code.'));
|
|
652
|
+
}
|
|
653
|
+
console.log('');
|
|
498
654
|
}
|
|
499
655
|
if (flags.json)
|
|
500
656
|
printJson({ setup: true, totalNew, discovered });
|
package/dist/cli/launchCmd.js
CHANGED
|
@@ -14,7 +14,8 @@ import { spawn } from 'node:child_process';
|
|
|
14
14
|
import { loadConfig } from '../config.js';
|
|
15
15
|
import { probeProxyState } from '../egress/proxyHealth.js';
|
|
16
16
|
import {} from './flags.js';
|
|
17
|
-
|
|
17
|
+
/** Whether any hard cap is set. The start banner uses the same test, so its advice matches launch. */
|
|
18
|
+
export function capConfigured(cfg) {
|
|
18
19
|
const b = cfg.budget;
|
|
19
20
|
return b.dailyUsd !== null || b.sessionUsd !== null || b.runawayMaxUsd !== null;
|
|
20
21
|
}
|