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/allocCmd.js
CHANGED
|
@@ -7,14 +7,17 @@
|
|
|
7
7
|
* a local rate-card estimate is legitimate and allocating one while implying it
|
|
8
8
|
* is settled cost is not.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { writeFileSync } from 'node:fs';
|
|
11
|
+
import { dbPath, isDemo } from '../config.js';
|
|
12
|
+
import { allocationRunToCsv } from '../export/allocationCsv.js';
|
|
11
13
|
import { Store } from '../store/db.js';
|
|
12
14
|
import { formatUsdMicros } from '../billing/types.js';
|
|
13
15
|
import { displayUsd } from '../billing/reconcile.js';
|
|
14
16
|
import { MATCH_FIELDS } from '../alloc/rules.js';
|
|
15
17
|
import { color, C, printJson } from './ui.js';
|
|
18
|
+
import { recordedTime } from '../util/time.js';
|
|
16
19
|
function usage() {
|
|
17
|
-
console.error(' Usage: segreant alloc <centres|centre|rules|rule|revoke|run> [options]');
|
|
20
|
+
console.error(' Usage: segreant alloc <centres|centre|rules|rule|revoke|run|runs|export> [options]');
|
|
18
21
|
console.error(' segreant alloc centre <id> --name "<name>" [--owner <who>] add or update a cost centre');
|
|
19
22
|
console.error(' segreant alloc centre <id> --archive retain it, stop it receiving');
|
|
20
23
|
console.error(' segreant alloc rule <id> --method <direct|fixed_split|proportional_to_direct> \\');
|
|
@@ -24,6 +27,8 @@ function usage() {
|
|
|
24
27
|
console.error(' a proportional_to_direct rule derives its split, so its centre takes ratio 0');
|
|
25
28
|
console.error(' segreant alloc revoke <rule-id>');
|
|
26
29
|
console.error(' segreant alloc run --from <ISO> --to <ISO> [--apply] [--json]');
|
|
30
|
+
console.error(' segreant alloc runs [--json] recorded splits, newest first');
|
|
31
|
+
console.error(' segreant alloc export <run-id> [--out <file>] one recorded split as CSV');
|
|
27
32
|
console.error(' Allocation is derived: it never modifies a request row, and never feeds budgets or RoI.');
|
|
28
33
|
}
|
|
29
34
|
function parseTs(value, label) {
|
|
@@ -81,7 +86,7 @@ function printRun(result, applied, tty) {
|
|
|
81
86
|
if (result.byCostCentre.length === 0) {
|
|
82
87
|
console.log(color(tty, C.gray, lost
|
|
83
88
|
? ' No spend that SURVIVES in this period reached a cost centre — rows before '
|
|
84
|
-
+ `${
|
|
89
|
+
+ `${recordedTime(lost.prunedBeforeMs, true)} were deleted by retention, and whether they did cannot be read from here.`
|
|
85
90
|
: ' No spend reached a cost centre in this period.'));
|
|
86
91
|
}
|
|
87
92
|
else {
|
|
@@ -101,7 +106,7 @@ function printRun(result, applied, tty) {
|
|
|
101
106
|
// percentage moves for a reason that has nothing to do with the rules --
|
|
102
107
|
// measured at 76.2% becoming 100.0% on a period 95% of whose spend went.
|
|
103
108
|
console.log(color(tty, C.yellow, ' Retention deleted request rows from inside this period '
|
|
104
|
-
+ `(before ${
|
|
109
|
+
+ `(before ${recordedTime(lost.prunedBeforeMs, true)}), so the total above is what SURVIVES and the`));
|
|
105
110
|
console.log(color(tty, C.yellow, ' percentages beside it are shares of that, not of what the period cost.'));
|
|
106
111
|
}
|
|
107
112
|
if (result.unallocated.length > 0) {
|
|
@@ -139,6 +144,70 @@ export function cmdAlloc(flags) {
|
|
|
139
144
|
const action = typeof flags._[0] === 'string' ? flags._[0] : 'run';
|
|
140
145
|
const store = new Store(dbPath());
|
|
141
146
|
try {
|
|
147
|
+
if (action === 'runs') {
|
|
148
|
+
const runs = store.allocationRuns(50);
|
|
149
|
+
if (flags.json) {
|
|
150
|
+
return void printJson({
|
|
151
|
+
runs: runs.map((r) => ({
|
|
152
|
+
allocationRunId: r.allocationRunId,
|
|
153
|
+
computedAtMs: r.computedAtMs,
|
|
154
|
+
periodStartMs: r.result.periodStartMs,
|
|
155
|
+
periodEndMs: r.result.periodEndMs,
|
|
156
|
+
totalMicros: r.result.totalMicros,
|
|
157
|
+
allocatedMicros: r.result.allocatedMicros,
|
|
158
|
+
unallocatedMicros: r.result.unallocatedMicros,
|
|
159
|
+
})),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
console.log('');
|
|
163
|
+
if (runs.length === 0)
|
|
164
|
+
console.log(color(tty, C.gray, ' No recorded splits. Record one: segreant alloc run --from <ISO> --to <ISO> --apply'));
|
|
165
|
+
for (const r of runs) {
|
|
166
|
+
// A bound that is not midnight UTC keeps its time: printing the date
|
|
167
|
+
// alone made a split ending at 06:48 read as covering the whole day,
|
|
168
|
+
// and re-running those dates gave a different total.
|
|
169
|
+
const bound = (ms) => {
|
|
170
|
+
const iso = new Date(ms).toISOString();
|
|
171
|
+
// Every nonzero part of the time is kept: allocation includes rows by
|
|
172
|
+
// the exact millisecond, so a bound shown to the minute could not be
|
|
173
|
+
// copied back to reproduce the split (PR #142 review).
|
|
174
|
+
if (iso.slice(11, 23) === '00:00:00.000')
|
|
175
|
+
return iso.slice(0, 10);
|
|
176
|
+
const time = iso.slice(17, 23) === '00.000' ? iso.slice(11, 16) : iso.slice(20, 23) === '000' ? iso.slice(11, 19) : iso.slice(11, 23);
|
|
177
|
+
return `${iso.slice(0, 10)} ${time}Z`;
|
|
178
|
+
};
|
|
179
|
+
const period = `${bound(r.result.periodStartMs)} → ${bound(r.result.periodEndMs)} (end exclusive)`;
|
|
180
|
+
const share = r.result.totalMicros > 0 ? `${((r.result.allocatedMicros / r.result.totalMicros) * 100).toFixed(1)}% allocated` : 'nothing to allocate';
|
|
181
|
+
console.log(` ${r.allocationRunId} ${period} ${displayUsd(r.result.totalMicros)} ${color(tty, C.gray, share)}`);
|
|
182
|
+
}
|
|
183
|
+
if (runs.length > 0)
|
|
184
|
+
console.log(color(tty, C.gray, '\n Export one as CSV: segreant alloc export <run-id> --out split.csv'));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (action === 'export') {
|
|
188
|
+
const id = typeof flags._[1] === 'string' ? flags._[1] : '';
|
|
189
|
+
if (id === '') {
|
|
190
|
+
console.error(' alloc export needs the run id: segreant alloc runs lists them.');
|
|
191
|
+
process.exitCode = 2;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const run = store.allocationRunById(id);
|
|
195
|
+
if (run === null) {
|
|
196
|
+
console.error(` No recorded allocation run has the id "${id}". segreant alloc runs lists them.`);
|
|
197
|
+
process.exitCode = 1;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const names = new Map(store.costCentres().map((c) => [c.costCentreId, c.name]));
|
|
201
|
+
const csv = allocationRunToCsv(run, (costCentreId) => names.get(costCentreId) ?? costCentreId, isDemo());
|
|
202
|
+
if (typeof flags.out === 'string') {
|
|
203
|
+
writeFileSync(flags.out, csv);
|
|
204
|
+
console.error(color(tty, C.green, ` Exported the split for ${new Date(run.result.periodStartMs).toISOString().slice(0, 10)} → ${flags.out}`));
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
process.stdout.write(csv);
|
|
208
|
+
}
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
142
211
|
if (action === 'centres') {
|
|
143
212
|
const centres = store.costCentres();
|
|
144
213
|
if (flags.json)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ask once, at the end of an interactive `scan --setup`, for a price for the
|
|
3
|
+
* costliest models still priced by a guess: after the card and the public price
|
|
4
|
+
* list have had their turn, the person is the only one who knows what a private,
|
|
5
|
+
* fine-tuned or local model costs. Interactive terminals only, Enter skips, and
|
|
6
|
+
* at most five models, so a long tail never turns setup into a questionnaire.
|
|
7
|
+
*/
|
|
8
|
+
import { C, color, usd, num } from './ui.js';
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { dbPath } from '../config.js';
|
|
11
|
+
import { unknownModels } from '../cost/declarations.js';
|
|
12
|
+
import { Store } from '../store/db.js';
|
|
13
|
+
import { promptForUnknownModels } from './pricingDeclareCmd.js';
|
|
14
|
+
const MAX_ASKED = 5;
|
|
15
|
+
/** Below this, a guess cannot move a total enough to be worth a question. */
|
|
16
|
+
const MIN_COST_USD = 0.5;
|
|
17
|
+
export async function askModelPrices(tty, io = {
|
|
18
|
+
input: process.stdin, output: process.stdout, interactive: process.stdin.isTTY === true,
|
|
19
|
+
}) {
|
|
20
|
+
if (!io.interactive)
|
|
21
|
+
return 0;
|
|
22
|
+
if (!existsSync(dbPath()))
|
|
23
|
+
return 0;
|
|
24
|
+
const store = new Store(dbPath());
|
|
25
|
+
let all;
|
|
26
|
+
try {
|
|
27
|
+
all = unknownModels(store);
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
store.close();
|
|
31
|
+
}
|
|
32
|
+
const guessed = all.filter((m) => m.costUsd >= MIN_COST_USD);
|
|
33
|
+
if (guessed.length === 0)
|
|
34
|
+
return 0;
|
|
35
|
+
const asked = guessed.slice(0, MAX_ASKED);
|
|
36
|
+
const write = (line) => io.output.write(`${line}\n`);
|
|
37
|
+
write(color(tty, C.bold, ' Models priced by a guess') + color(tty, C.gray, ' not on the rate card (Enter skips)'));
|
|
38
|
+
for (const m of asked) {
|
|
39
|
+
const how = m.rateMatchKind === 'fallback' ? 'generic rate' : 'nearest model on the card';
|
|
40
|
+
write(color(tty, C.gray, ` ${m.model}: ${num(m.requests)} requests, ${usd(m.costUsd)} recorded (estimated, ${how})`));
|
|
41
|
+
}
|
|
42
|
+
const { createInterface } = await import('node:readline/promises');
|
|
43
|
+
const rl = createInterface({ input: io.input, output: io.output, terminal: false });
|
|
44
|
+
let made = 0;
|
|
45
|
+
try {
|
|
46
|
+
made = (await promptForUnknownModels(asked, {
|
|
47
|
+
ask: (question) => rl.question(` ${question}: `),
|
|
48
|
+
print: (line) => write(color(tty, C.gray, ` ${line}`)),
|
|
49
|
+
})).length;
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
rl.close();
|
|
53
|
+
}
|
|
54
|
+
if (guessed.length > asked.length)
|
|
55
|
+
write(color(tty, C.gray, ` ${num(guessed.length - asked.length)} more: segreant pricing unknown`));
|
|
56
|
+
if (made > 0)
|
|
57
|
+
write(color(tty, C.gray, ' Recorded requests keep their estimate until you re-price them: segreant reprice (a preview), then --apply'));
|
|
58
|
+
return made;
|
|
59
|
+
}
|
package/dist/cli/backupCmd.js
CHANGED
|
@@ -14,7 +14,8 @@ function requiredPath(flags, name) {
|
|
|
14
14
|
}
|
|
15
15
|
function emit(result, flags, action) {
|
|
16
16
|
if (flags.json) {
|
|
17
|
-
|
|
17
|
+
const { schemaVersion: ledgerSchemaVersion, ...details } = result;
|
|
18
|
+
printJson({ action, ...details, ledgerSchemaVersion });
|
|
18
19
|
if (!result.ok)
|
|
19
20
|
process.exitCode = 1;
|
|
20
21
|
return;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The lines that say how a CLI total was priced. Every command that prints a
|
|
3
|
+
* total with more than one part prints these under it, so the parts read the
|
|
4
|
+
* same everywhere (`today`, `week`, `month`, the first-run summary).
|
|
5
|
+
*/
|
|
6
|
+
import { cohortModels } from '../cost/basis.js';
|
|
7
|
+
import { num, usd } from './ui.js';
|
|
8
|
+
export function basisLines(basis, indent = ' ') {
|
|
9
|
+
const out = [];
|
|
10
|
+
for (const c of basis.cohorts) {
|
|
11
|
+
if (c.id === 'unpriced') {
|
|
12
|
+
out.push(`${indent}${num(c.requests)} requests not priced: ${cohortModels(c)} (no rate applies)`);
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (c.id === 'not_sent') {
|
|
16
|
+
out.push(`${indent}${''.padStart(10)} ${num(c.requests)} requests refused before reaching the provider (no charge)`);
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (c.id === 'tool_reported' && c.costUsd < 0.005) {
|
|
20
|
+
// A free model is a real answer ("the tool said $0"), not a missing price.
|
|
21
|
+
out.push(`${indent}${''.padStart(10)} ${num(c.requests)} requests the tool reported at no charge: ${cohortModels(c)}`);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (c.id === 'user_free') {
|
|
25
|
+
out.push(`${indent}${''.padStart(10)} ${num(c.requests)} requests on models you marked free: ${cohortModels(c)}`);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const models = c.id === 'fallback' || c.id === 'list_family' || c.id === 'user_declared' || c.id === 'unrecorded'
|
|
29
|
+
? `: ${cohortModels(c)}`
|
|
30
|
+
: '';
|
|
31
|
+
out.push(`${indent}${usd(c.costUsd).padStart(10)} ${c.label}${models}`);
|
|
32
|
+
}
|
|
33
|
+
if (basis.cohorts.some((c) => c.id === 'fallback')) {
|
|
34
|
+
out.push(`${indent} fallback = the model was not on the rate card when imported; a generic rate, a rough estimate`);
|
|
35
|
+
out.push(`${indent} segreant reprice previews new prices for models the card has learned since (nothing changes without --apply)`);
|
|
36
|
+
}
|
|
37
|
+
if (basis.cohorts.some((c) => c.id === 'user_declared')) {
|
|
38
|
+
out.push(`${indent} your price = set with segreant pricing set; not checked against a price list`);
|
|
39
|
+
}
|
|
40
|
+
if (basis.cohorts.some((c) => c.id === 'tool_reported')) {
|
|
41
|
+
out.push(`${indent} tool-reported = what the tool recorded; not checked against any rate card or bill`);
|
|
42
|
+
}
|
|
43
|
+
out.push(`${indent}None of these is your bill.`);
|
|
44
|
+
return out;
|
|
45
|
+
}
|
package/dist/cli/billingCmd.js
CHANGED
|
@@ -4,20 +4,29 @@
|
|
|
4
4
|
* a least-privilege provider connector, but it cannot establish reconciliation.
|
|
5
5
|
*/
|
|
6
6
|
import { writeFileSync } from 'node:fs';
|
|
7
|
-
import { dbPath, loadConfig } from '../config.js';
|
|
7
|
+
import { dbPath, isDemo, loadConfig } from '../config.js';
|
|
8
8
|
import { OpenAiCostsPullError, previewOpenAiCosts, pullOpenAiCosts, } from '../billing/openaiCosts.js';
|
|
9
9
|
import { newOpenAiScopeDeclaration } from '../billing/scope.js';
|
|
10
10
|
import { assessAssumptionFragility } from '../epistemic/countermodel.js';
|
|
11
11
|
import { reconciliationCountermodels } from '../billing/countermodels.js';
|
|
12
|
-
import { describeOffPathBound, displayUsd, signedUsd } from '../billing/reconcile.js';
|
|
12
|
+
import { describeLocalQuantization, describeOffPathBound, displayUsd, signedUsd } from '../billing/reconcile.js';
|
|
13
13
|
import { reconciliationReadiness } from '../billing/readiness.js';
|
|
14
14
|
import { formatUsdMicros } from '../billing/types.js';
|
|
15
15
|
import { readBillingImportFile } from '../billing/importer.js';
|
|
16
16
|
import { billingEvidenceToCsv } from '../export/billingCsv.js';
|
|
17
17
|
import { Store } from '../store/db.js';
|
|
18
|
-
import { printJson } from './ui.js';
|
|
18
|
+
import { printJson as printJsonRaw, cliJsonResponse } from './ui.js';
|
|
19
|
+
/**
|
|
20
|
+
* Billing JSON names sample data in the payload itself. The bill the demo
|
|
21
|
+
* carries is generated, and a comparison or export lifted out of the terminal
|
|
22
|
+
* must still say so; the dashboard's banner does not travel with it.
|
|
23
|
+
*/
|
|
24
|
+
function printJson(value) {
|
|
25
|
+
printJsonRaw(isDemo() && value !== null && typeof value === 'object' && !Array.isArray(value) ? { demo: true, ...value } : value);
|
|
26
|
+
}
|
|
19
27
|
import { stringifyJson } from '../util/json.js';
|
|
20
28
|
import { moneyToJson } from '../economics/money.js';
|
|
29
|
+
import { recordedTime } from '../util/time.js';
|
|
21
30
|
function billingEgressRepairAction(failureCode) {
|
|
22
31
|
return failureCode === 'egress_receipt_integrity_failed' || failureCode === 'egress_receipt_persistence_failed'
|
|
23
32
|
? 'Repair or restore the local receipt history before retrying; if the lock is stale, confirm no Segreant writer is active, then remove only that lock and rerun verify.'
|
|
@@ -66,12 +75,12 @@ function printCostsCoverage(coverage) {
|
|
|
66
75
|
console.log(` Snapshot ${coverage.observation.observationRunId}`);
|
|
67
76
|
console.log(` Period ${new Date(coverage.observation.periodStartMs).toISOString().slice(0, 10)} → ${new Date(coverage.observation.periodEndMs).toISOString().slice(0, 10)} (UTC, exclusive end)`);
|
|
68
77
|
console.log(` Provider rows ${coverage.observation.providerLineCount} (${coverage.observation.currencies.join(', ') || 'none'}); values intentionally not summed here`);
|
|
69
|
-
console.log(` Declared route ${captured.requestCount} live proxy request(s), $${formatLocalEstimate(captured.costUsd)} local
|
|
78
|
+
console.log(` Declared route ${captured.requestCount} live proxy request(s), $${formatLocalEstimate(captured.costUsd)} local estimate, not the bill (${captured.estimatedRequestCount} priced by a guess; the rest at list prices or prices you set)`);
|
|
70
79
|
// Directly under the number it falsifies, and not in `Blockers`, which lists
|
|
71
80
|
// the conditions that hold always (D-185). A zero here is what an operator
|
|
72
81
|
// uses to decide whether minting an Admin credential is worth it.
|
|
73
82
|
if (coverage.localLedgerRetention.truncated) {
|
|
74
|
-
const before =
|
|
83
|
+
const before = recordedTime(coverage.localLedgerRetention.prunedBeforeMs, true);
|
|
75
84
|
console.log(` Local ledger TRUNCATED — retention deleted rows before ${before}, inside the period above.`);
|
|
76
85
|
console.log(' Every local count on this screen is what SURVIVES, so an empty declared');
|
|
77
86
|
console.log(' route here is not evidence that the route captured nothing.');
|
|
@@ -93,7 +102,7 @@ function printReadiness(readiness) {
|
|
|
93
102
|
// null. A guard that goes quiet leaves nothing on the screen to be wrong,
|
|
94
103
|
// which is why the silence itself has to be reported (D-186).
|
|
95
104
|
if (readiness.localLedgerRetention.truncated) {
|
|
96
|
-
const before =
|
|
105
|
+
const before = recordedTime(readiness.localLedgerRetention.prunedBeforeMs, true);
|
|
97
106
|
console.log('');
|
|
98
107
|
if (c === null) {
|
|
99
108
|
console.log(` NO OPENAI SPEND SURVIVES HERE, and retention deleted rows before ${before}.`);
|
|
@@ -210,6 +219,10 @@ function printReconciliation(result, applied) {
|
|
|
210
219
|
for (const line of wrapText(describeOffPathBound(result.offPathBound), 74)) {
|
|
211
220
|
console.log(` ${line}`);
|
|
212
221
|
}
|
|
222
|
+
const rounding = describeLocalQuantization(result.localQuantization);
|
|
223
|
+
if (rounding)
|
|
224
|
+
for (const line of wrapText(rounding, 74))
|
|
225
|
+
console.log(` ${line}`);
|
|
213
226
|
console.log('');
|
|
214
227
|
console.log(` Coverage ${result.coverage.daysWithBoth} day(s) with both sides · ${result.coverage.providerOnlyDays} provider-only · ${result.coverage.localOnlyDays} local-only`);
|
|
215
228
|
console.log(` Material ${result.coverage.materialDays} day(s) differ by more than $${result.materialityUsd.toFixed(2)}`);
|
|
@@ -829,9 +842,32 @@ function wrapText(text, width) {
|
|
|
829
842
|
lines.push(current);
|
|
830
843
|
return lines;
|
|
831
844
|
}
|
|
845
|
+
/**
|
|
846
|
+
* A part's share of a whole, as a percentage label. Only the whole itself
|
|
847
|
+
* reads 100 and only nothing reads 0: rounding 99.6% up to 100% told the user
|
|
848
|
+
* the comparison covered everything when it did not (PR #142 review).
|
|
849
|
+
*/
|
|
850
|
+
export function coveragePercent(part, whole) {
|
|
851
|
+
if (part >= whole)
|
|
852
|
+
return '100';
|
|
853
|
+
if (part <= 0)
|
|
854
|
+
return '0';
|
|
855
|
+
const share = (part / whole) * 100;
|
|
856
|
+
if (share < 0.1)
|
|
857
|
+
return '<0.1';
|
|
858
|
+
if (share < 1)
|
|
859
|
+
return share.toFixed(1);
|
|
860
|
+
if (share >= 99.5)
|
|
861
|
+
return Math.min(99.9, Math.floor(share * 10) / 10).toFixed(1);
|
|
862
|
+
return String(Math.round(share));
|
|
863
|
+
}
|
|
832
864
|
/** `segreant billing` — immutable local provider-cost evidence, never an implicit reconciliation. */
|
|
833
865
|
export async function cmdBilling(flags) {
|
|
834
866
|
const action = typeof flags._[0] === 'string' ? flags._[0] : 'status';
|
|
867
|
+
// The same mark `show` and `home` print: everything below is generated.
|
|
868
|
+
if (isDemo() && !flags.json && !(action === 'export' && !flags.out)) {
|
|
869
|
+
console.log(' \u25CF DEMO DATA \u2014 synthetic, isolated in demo.db; this bill and comparison were generated, not received from a provider');
|
|
870
|
+
}
|
|
835
871
|
if (action === 'scope') {
|
|
836
872
|
cmdScope(flags);
|
|
837
873
|
return;
|
|
@@ -919,7 +955,30 @@ export async function cmdBilling(flags) {
|
|
|
919
955
|
console.log(` Imports ${summary.importCount}`);
|
|
920
956
|
console.log(` Charge lines ${summary.recordCount}`);
|
|
921
957
|
console.log(` Declared USD $${formatUsdMicros(summary.providerReportedUsdMicros)}`);
|
|
922
|
-
|
|
958
|
+
// A recorded comparison is the answer to "has this been reconciled?";
|
|
959
|
+
// the status line used to say not_reconciled whatever the ledger held.
|
|
960
|
+
const latest = store.reconciliationRuns(20);
|
|
961
|
+
if (latest.length === 0) {
|
|
962
|
+
console.log(' Reconcile not_reconciled — no comparison recorded yet. Preview one: segreant billing reconcile');
|
|
963
|
+
}
|
|
964
|
+
else {
|
|
965
|
+
const run = latest[0].result;
|
|
966
|
+
const day = (ms) => new Date(ms).toISOString().slice(0, 10);
|
|
967
|
+
console.log(` Reconcile ${latest.length === 20 ? '20+' : latest.length} recorded comparison(s); latest: ${run.providerProjectRef}, ${day(run.periodStartMs)} → ${day(run.periodEndMs)} (UTC, end exclusive)`);
|
|
968
|
+
console.log(` provider $${formatUsdMicros(run.providerReportedMicros)} · metered $${formatUsdMicros(run.localCapturedMicros)} · unexplained ${signedUsd(run.unexplainedVarianceMicros)} — scope-conditional, not a clean reconciliation`);
|
|
969
|
+
// How much of what Segreant metered in that period the comparison
|
|
970
|
+
// actually covers: one project's bill is not the month's spend.
|
|
971
|
+
// Same day quantization as the comparison itself, so the share
|
|
972
|
+
// cannot exceed the whole (PR #142 review).
|
|
973
|
+
const metered = store.meteredMicrosLikeReconciliation(run.periodStartMs, run.periodEndMs, run.declaredScopeId);
|
|
974
|
+
if (metered !== null && metered.onRouteMicros !== run.localCapturedMicros) {
|
|
975
|
+
console.log(' coverage withheld: the ledger has changed for that period since this comparison was recorded; re-run segreant billing reconcile');
|
|
976
|
+
}
|
|
977
|
+
else if (metered !== null && metered.allMicros > 0) {
|
|
978
|
+
console.log(` covers $${formatUsdMicros(run.localCapturedMicros)} of the $${formatUsdMicros(metered.allMicros)} Segreant metered across all providers in that period (${coveragePercent(run.localCapturedMicros, metered.allMicros)}%)`);
|
|
979
|
+
}
|
|
980
|
+
console.log(' Details: segreant billing reconcile');
|
|
981
|
+
}
|
|
923
982
|
if (summary.lastImportedAtMs !== null)
|
|
924
983
|
console.log(` Last import ${new Date(summary.lastImportedAtMs).toISOString()}`);
|
|
925
984
|
if (imports.length > 0)
|
|
@@ -932,10 +991,10 @@ export async function cmdBilling(flags) {
|
|
|
932
991
|
const records = store.billingEvidenceRecords();
|
|
933
992
|
const summary = store.billingSummary();
|
|
934
993
|
if (flags.json) {
|
|
935
|
-
writeOrPrint(stringifyJson({ summary, records }) + '\n', flags.out);
|
|
994
|
+
writeOrPrint(stringifyJson(cliJsonResponse({ ...(isDemo() ? { demo: true } : {}), summary, records })) + '\n', flags.out);
|
|
936
995
|
}
|
|
937
996
|
else {
|
|
938
|
-
writeOrPrint(billingEvidenceToCsv(records), flags.out);
|
|
997
|
+
writeOrPrint(billingEvidenceToCsv(records, isDemo()), flags.out);
|
|
939
998
|
}
|
|
940
999
|
return;
|
|
941
1000
|
}
|
package/dist/cli/capitalCmd.js
CHANGED
|
@@ -36,7 +36,8 @@ function usage() {
|
|
|
36
36
|
function emit(payload, flags) {
|
|
37
37
|
// Keep the JSON path canonical; --json is accepted for parity with other
|
|
38
38
|
// review commands and the human form remains machine-readable by design.
|
|
39
|
-
|
|
39
|
+
// Reading it marks it as used, so the dispatcher does not report it.
|
|
40
|
+
void flags.json;
|
|
40
41
|
printJson(payload);
|
|
41
42
|
}
|
|
42
43
|
export function cmdCapital(flags) {
|
package/dist/cli/connectCmd.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* from cli.ts in the per-command-module split; only cmdConnect is exported,
|
|
5
5
|
* the per-tool flows stay module-internal.
|
|
6
6
|
*/
|
|
7
|
+
import { isBuiltInProviderUpstream, providerRuleCommand } from '../egress/policy.js';
|
|
7
8
|
import { homedir } from 'node:os';
|
|
8
9
|
import { join, dirname } from 'node:path';
|
|
9
10
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from 'node:fs';
|
|
@@ -123,8 +124,8 @@ function wrapOpencodeFlow(cfg, flags, tty, providerName, path) {
|
|
|
123
124
|
console.log(color(tty, C.green, ` ✓ Wrapped "${providerName}". opencode now routes through Segreant.`));
|
|
124
125
|
console.log(color(tty, C.gray, ` opencode config: ${path} (backup at ${path}.bak)`));
|
|
125
126
|
console.log(color(tty, C.gray, ` Segreant upstreams.openai → ${res.originalBaseUrl}`));
|
|
126
|
-
|
|
127
|
-
console.log(color(tty, C.gray, `
|
|
127
|
+
const forwarding = upstreamForwarding(res.originalBaseUrl);
|
|
128
|
+
console.log(color(tty, forwarding.ok ? C.gray : C.yellow, ` ${forwarding.text}`));
|
|
128
129
|
console.log(color(tty, C.gray, ' JSON comments were reformatted away; your settings + keys are preserved.'));
|
|
129
130
|
console.log('');
|
|
130
131
|
console.log(color(tty, C.gray, ' Restart Segreant (segreant start), run opencode, then: segreant sources'));
|
|
@@ -258,10 +259,11 @@ function connectOpencode(cfg, flags, tty) {
|
|
|
258
259
|
finishConnectOpencode(tty);
|
|
259
260
|
}
|
|
260
261
|
/**
|
|
261
|
-
* Antigravity's BUILT-IN
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
262
|
+
* Antigravity's BUILT-IN agent routes through Google's own servers, which no
|
|
263
|
+
* cooperative proxy sits in front of; its usage is read instead from the
|
|
264
|
+
* conversation databases Antigravity keeps locally (`segreant import
|
|
265
|
+
* antigravity`, src/connect/antigravity.ts). Its CUSTOM OpenAI-compatible
|
|
266
|
+
* providers can also be routed through the proxy: provider → proxy → upstream.
|
|
265
267
|
* `--write` points the proxy's OpenAI upstream at Gemini's OpenAI-compatible
|
|
266
268
|
* endpoint, the free-tier test path; the user's key passes through untouched.
|
|
267
269
|
*/
|
|
@@ -277,8 +279,10 @@ function connectAntigravity(cfg, flags, tty) {
|
|
|
277
279
|
console.log('');
|
|
278
280
|
console.log(color(tty, C.bold, ' Connect Google Antigravity as a source'));
|
|
279
281
|
console.log(color(tty, C.gray, ' ' + '─'.repeat(52)));
|
|
280
|
-
console.log(color(tty, C.gray, ' Antigravity’s built-in
|
|
281
|
-
console.log(color(tty, C.gray, '
|
|
282
|
+
console.log(color(tty, C.gray, ' Antigravity’s built-in agent runs on Google’s servers, so no proxy sits in front'));
|
|
283
|
+
console.log(color(tty, C.gray, ' of it. Its usage is read from the logs it keeps on this machine instead:'));
|
|
284
|
+
console.log(color(tty, C.green, ' segreant import antigravity'));
|
|
285
|
+
console.log(color(tty, C.gray, ' Custom providers can also go through the proxy, live:'));
|
|
282
286
|
console.log('');
|
|
283
287
|
console.log(` 1) ${color(tty, C.bold, 'Choose the upstream')} the proxy forwards to. For the Gemini free tier:`);
|
|
284
288
|
console.log(color(tty, C.green, ' segreant connect antigravity --write'));
|
|
@@ -359,6 +363,18 @@ function connectGenericApi(cfg, flags, tty) {
|
|
|
359
363
|
console.log(color(tty, C.gray, ' The header wins if you send both. Either way it is your declaration, recorded'));
|
|
360
364
|
console.log(color(tty, C.gray, ' as such — Segreant does not verify that the label is true.'));
|
|
361
365
|
console.log('');
|
|
366
|
+
// The recipe covered the client side only; a user pointing scripts at another
|
|
367
|
+
// OpenAI-compatible provider had to find the other half in the docs.
|
|
368
|
+
console.log(color(tty, C.gray, ` Segreant forwards these requests to ${cfg.upstreams.openai}. For another`));
|
|
369
|
+
console.log(color(tty, C.gray, ' OpenAI-compatible provider (OpenRouter, Azure, a local server), set'));
|
|
370
|
+
console.log(color(tty, C.gray, ' "upstreams": { "openai": "<base URL>" } in config.json; docs/INTEGRATIONS.md has examples.'));
|
|
371
|
+
// The proxy appends the client's path to the upstream base, so a version
|
|
372
|
+
// segment in both places doubles it (/v1/v1/chat/completions) and 404s.
|
|
373
|
+
console.log(color(tty, C.gray, ' If that base URL already ends in its version (an Ollama base ending in /v1),'));
|
|
374
|
+
console.log(color(tty, C.gray, ` point the client at http://localhost:${port} without /v1. A remote upstream also`));
|
|
375
|
+
console.log(color(tty, C.gray, ' other than the official OpenAI and Anthropic APIs needs one egress rule; a refused'));
|
|
376
|
+
console.log(color(tty, C.gray, ' request prints the exact command.'));
|
|
377
|
+
console.log('');
|
|
362
378
|
console.log(color(tty, C.gray, ' Run it, then check:'));
|
|
363
379
|
console.log(color(tty, C.green, ' segreant sources'));
|
|
364
380
|
console.log(color(tty, C.gray, ' segreant project --coverage # what the spend is attributed to, and how'));
|
|
@@ -369,6 +385,31 @@ function connectGenericApi(cfg, flags, tty) {
|
|
|
369
385
|
* No tool (or --list) shows the menu; opencode writes/prints its provider block;
|
|
370
386
|
* `api` prints the generic env + header recipe (with an optional custom source name).
|
|
371
387
|
*/
|
|
388
|
+
/**
|
|
389
|
+
* What forwarding to an upstream needs. Four cases: the official provider APIs
|
|
390
|
+
* are built in; plain HTTP on this machine is always allowed; any other HTTPS
|
|
391
|
+
* address needs the one rule printed; plain HTTP elsewhere cannot be allowed at
|
|
392
|
+
* all. The last case used to be told it was the official API (independent
|
|
393
|
+
* review, 2026-10-18).
|
|
394
|
+
*/
|
|
395
|
+
export function upstreamForwarding(url) {
|
|
396
|
+
if (isBuiltInProviderUpstream(url))
|
|
397
|
+
return { ok: true, text: 'Requests to the official provider API go out with no extra step.' };
|
|
398
|
+
let target;
|
|
399
|
+
try {
|
|
400
|
+
target = new URL(url);
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
return { ok: false, text: `${url} is not a valid address.` };
|
|
404
|
+
}
|
|
405
|
+
if (target.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(target.hostname)) {
|
|
406
|
+
return { ok: true, text: 'A model on this machine is reached directly; no rule is needed.' };
|
|
407
|
+
}
|
|
408
|
+
const allow = providerRuleCommand(url);
|
|
409
|
+
return allow
|
|
410
|
+
? { ok: false, text: `${url} is not a built-in provider API. Allow Segreant to forward there with:\n ${allow}` }
|
|
411
|
+
: { ok: false, text: `${url} cannot be allowed as it is: Segreant forwards only to HTTPS, or to plain HTTP on this machine (localhost, 127.0.0.1). Use the gateway's https:// address.` };
|
|
412
|
+
}
|
|
372
413
|
export function cmdConnect(flags) {
|
|
373
414
|
const tty = process.stdout.isTTY ?? false;
|
|
374
415
|
const cfg = loadConfig();
|
|
@@ -387,7 +428,7 @@ export function cmdConnect(flags) {
|
|
|
387
428
|
console.log(color(tty, C.gray, ' segreant connect opencode --write apply it for you (backs up first)'));
|
|
388
429
|
console.log('');
|
|
389
430
|
console.log(color(tty, C.gray, ' No base URL to wire? Meter subscription tools natively — no routing, no key:'));
|
|
390
|
-
console.log(color(tty, C.green, ' segreant import claude-code | opencode | codex | all ') + color(tty, C.gray, '(--watch = live)'));
|
|
431
|
+
console.log(color(tty, C.green, ' segreant import claude-code | opencode | codex | antigravity | all ') + color(tty, C.gray, '(--watch = live)'));
|
|
391
432
|
console.log('');
|
|
392
433
|
return;
|
|
393
434
|
}
|
package/dist/cli/egressCmd.js
CHANGED
|
@@ -32,11 +32,12 @@ function ruleFromFlags(flags) {
|
|
|
32
32
|
function printUsage(tty) {
|
|
33
33
|
console.log('');
|
|
34
34
|
console.log(color(tty, C.bold, ' Segreant egress — process-scoped high-assurance transport'));
|
|
35
|
-
console.log(color(tty, C.gray,
|
|
35
|
+
console.log(color(tty, C.gray, " Built in, in every mode: loopback, Segreant's public price list, and your requests to the"));
|
|
36
|
+
console.log(color(tty, C.gray, ' official OpenAI and Anthropic APIs. Anything else needs a rule. No remote probe is made here.'));
|
|
36
37
|
console.log('');
|
|
37
38
|
console.log(color(tty, C.gray, ' segreant egress status'));
|
|
38
|
-
console.log(color(tty, C.gray, ' segreant egress plan --mode controlled_cloud --id
|
|
39
|
-
console.log(color(tty, C.gray, ' segreant egress apply --apply --mode controlled_cloud --id
|
|
39
|
+
console.log(color(tty, C.gray, ' segreant egress plan --mode controlled_cloud --id openrouter --purpose provider_inference --data-class provider_request --method POST --origin https://openrouter.ai --path-prefix /api/'));
|
|
40
|
+
console.log(color(tty, C.gray, ' segreant egress apply --apply --mode controlled_cloud --id openrouter --purpose provider_inference --data-class provider_request --method POST --origin https://openrouter.ai --path-prefix /api/'));
|
|
40
41
|
console.log(color(tty, C.gray, ' segreant egress apply --apply --mode local_locked'));
|
|
41
42
|
console.log(color(tty, C.gray, ' segreant egress receipts | segreant egress verify'));
|
|
42
43
|
console.log('');
|
|
@@ -109,6 +110,10 @@ export function cmdEgress(flags) {
|
|
|
109
110
|
console.log('');
|
|
110
111
|
console.log(color(tty, C.bold, ' Segreant egress status'));
|
|
111
112
|
console.log(' Mode: ' + payload.mode);
|
|
113
|
+
if (payload.mode === 'invalid') {
|
|
114
|
+
console.log(' The egress section of config.json is malformed, so every cloud request is refused,');
|
|
115
|
+
console.log(' your provider included. Repair: segreant egress apply --apply --mode local_locked');
|
|
116
|
+
}
|
|
112
117
|
const rules = payload.rules;
|
|
113
118
|
console.log(' Rules: ' + (rules.length ? rules.map((rule) => rule.id + ' (' + (rule.enabled ? 'enabled' : 'disabled') + ')').join(', ') : 'none'));
|
|
114
119
|
const receipts = payload.receipts;
|
|
@@ -171,6 +176,13 @@ export function cmdEgress(flags) {
|
|
|
171
176
|
const failures = validateEgressRule(candidate);
|
|
172
177
|
if (failures.length) {
|
|
173
178
|
console.error(' Rule refused: ' + failures.join('; '));
|
|
179
|
+
// Git Bash (MSYS) rewrites an argument that starts with "/" into a Windows
|
|
180
|
+
// path before Segreant sees it, so "/v1/" arrives as "C:/Program Files/Git/v1/".
|
|
181
|
+
const prefix = flags['path-prefix'];
|
|
182
|
+
if (typeof prefix === 'string' && /^[A-Za-z]:[\\/]/.test(prefix)) {
|
|
183
|
+
console.error(' --path-prefix arrived as "' + prefix + '". Git Bash turns arguments that start with / into Windows paths.');
|
|
184
|
+
console.error(' Put MSYS_NO_PATHCONV=1 in front of the command, or run it from PowerShell or cmd.');
|
|
185
|
+
}
|
|
174
186
|
process.exitCode = 1;
|
|
175
187
|
return;
|
|
176
188
|
}
|
package/dist/cli/evidenceCmd.js
CHANGED
|
@@ -4,6 +4,7 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
4
4
|
import { dirname } from 'node:path';
|
|
5
5
|
import { dbPath } from '../config.js';
|
|
6
6
|
import { Store } from '../store/db.js';
|
|
7
|
+
import { printJson } from './ui.js';
|
|
7
8
|
import { buildGithubActionsOutcome, importGithubActionsOutcome, signGithubActionsOutcome, } from '../githubActionsEvidence.js';
|
|
8
9
|
import { readBoundedUtf8File, RESOURCE_LIMITS } from '../util/resource-limits.js';
|
|
9
10
|
import { buildEpistemicUxBundle } from '../epistemic/ux.js';
|
|
@@ -35,11 +36,13 @@ function cmdUx(flags) {
|
|
|
35
36
|
throw new Error('epistemic UX options must be a JSON object');
|
|
36
37
|
}
|
|
37
38
|
const bundle = buildEpistemicUxBundle(supplied);
|
|
38
|
-
|
|
39
|
+
// Always JSON; reading --json marks it as used so the dispatcher does not report it.
|
|
40
|
+
void flags.json;
|
|
41
|
+
printJson({
|
|
39
42
|
operation: 'epistemic_ux_bundle',
|
|
40
43
|
bundle,
|
|
41
44
|
boundary: 'Review-only epistemic UX bundle; no claim issuance, strengthening, authorization, routing, or external truth inference.',
|
|
42
|
-
}
|
|
45
|
+
});
|
|
43
46
|
}
|
|
44
47
|
function readJsonArtifact(path) {
|
|
45
48
|
try {
|
|
@@ -124,7 +127,7 @@ async function cmdGithubImport(flags) {
|
|
|
124
127
|
if (!result.ok)
|
|
125
128
|
throw new Error(`evidence rejected: ${result.reason}`);
|
|
126
129
|
if (flags.json) {
|
|
127
|
-
|
|
130
|
+
printJson(result);
|
|
128
131
|
}
|
|
129
132
|
else {
|
|
130
133
|
console.log(` Imported verified signed-CI tested=${artifact.body.verdict} evidence for ${result.commit.slice(0, 12)}.`);
|