@graphorin/cli 0.6.1 → 0.7.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/CHANGELOG.md +63 -0
- package/README.md +3 -3
- package/dist/bin/graphorin.js +51 -13
- package/dist/bin/graphorin.js.map +1 -1
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +2 -1
- package/dist/commands/audit.js.map +1 -1
- package/dist/commands/consolidator.d.ts +56 -1
- package/dist/commands/consolidator.d.ts.map +1 -1
- package/dist/commands/consolidator.js +88 -2
- package/dist/commands/consolidator.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/index.d.ts +4 -4
- package/dist/commands/index.js +4 -4
- package/dist/commands/init.d.ts +11 -4
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +15 -11
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/memory.d.ts +26 -1
- package/dist/commands/memory.d.ts.map +1 -1
- package/dist/commands/memory.js +56 -3
- package/dist/commands/memory.js.map +1 -1
- package/dist/commands/pricing.d.ts +6 -0
- package/dist/commands/pricing.d.ts.map +1 -1
- package/dist/commands/pricing.js +5 -2
- package/dist/commands/pricing.js.map +1 -1
- package/dist/commands/secrets.d.ts.map +1 -1
- package/dist/commands/secrets.js +2 -2
- package/dist/commands/secrets.js.map +1 -1
- package/dist/commands/skills.d.ts.map +1 -1
- package/dist/commands/skills.js +1 -1
- package/dist/commands/skills.js.map +1 -1
- package/dist/commands/storage.d.ts +41 -1
- package/dist/commands/storage.d.ts.map +1 -1
- package/dist/commands/storage.js +75 -1
- package/dist/commands/storage.js.map +1 -1
- package/dist/commands/token.d.ts.map +1 -1
- package/dist/commands/token.js +2 -2
- package/dist/commands/token.js.map +1 -1
- package/dist/commands/tools-lint.js +1 -1
- package/dist/commands/tools-lint.js.map +1 -1
- package/dist/commands/traces.d.ts +14 -2
- package/dist/commands/traces.d.ts.map +1 -1
- package/dist/commands/traces.js +39 -22
- package/dist/commands/traces.js.map +1 -1
- package/dist/commands/triggers.d.ts.map +1 -1
- package/dist/commands/triggers.js +5 -2
- package/dist/commands/triggers.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -5
- package/dist/index.js.map +1 -1
- package/dist/internal/output.js +6 -0
- package/dist/internal/output.js.map +1 -1
- package/dist/internal/store-context.js +13 -2
- package/dist/internal/store-context.js.map +1 -1
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/package.json +18 -14
- package/src/bin/graphorin.ts +1387 -0
- package/src/commands/audit.ts +256 -0
- package/src/commands/auth.ts +238 -0
- package/src/commands/consolidator.ts +382 -0
- package/src/commands/doctor.ts +253 -0
- package/src/commands/guard.ts +144 -0
- package/src/commands/index.ts +223 -0
- package/src/commands/init.ts +194 -0
- package/src/commands/memory.ts +1052 -0
- package/src/commands/migrate-config.ts +77 -0
- package/src/commands/migrate-export.ts +117 -0
- package/src/commands/migrate.ts +83 -0
- package/src/commands/pricing.ts +244 -0
- package/src/commands/secrets.ts +309 -0
- package/src/commands/skills.ts +272 -0
- package/src/commands/start.ts +180 -0
- package/src/commands/storage.ts +659 -0
- package/src/commands/telemetry.ts +91 -0
- package/src/commands/token.ts +361 -0
- package/src/commands/tools-lint.ts +430 -0
- package/src/commands/traces.ts +188 -0
- package/src/commands/triggers.ts +237 -0
- package/src/index.ts +30 -0
- package/src/internal/exit.ts +62 -0
- package/src/internal/load-config.ts +107 -0
- package/src/internal/offline.ts +81 -0
- package/src/internal/output.ts +146 -0
- package/src/internal/prompts.ts +58 -0
- package/src/internal/store-context.ts +165 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `graphorin migrate-config <input>` - config-schema migration helper.
|
|
3
|
+
*
|
|
4
|
+
* v0.1 ships exactly one config schema; the migrator is therefore a
|
|
5
|
+
* compatibility shell that round-trips the operator's config through
|
|
6
|
+
* `parseServerConfig(...)` and writes the canonical-shape JSON back to
|
|
7
|
+
* disk. Future MAJOR bumps replace this body with the rewriter that
|
|
8
|
+
* upgrades old field shapes; the CLI surface stays the same.
|
|
9
|
+
*
|
|
10
|
+
* The output path is always derived from the input - `migrate-config`
|
|
11
|
+
* writes `<input>.migrated` next to the source unless `--out` is
|
|
12
|
+
* supplied. The original is never modified.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { writeFile } from 'node:fs/promises';
|
|
18
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
19
|
+
import process from 'node:process';
|
|
20
|
+
|
|
21
|
+
import { parseServerConfig } from '@graphorin/server';
|
|
22
|
+
|
|
23
|
+
import { loadConfig } from '../internal/load-config.js';
|
|
24
|
+
import {
|
|
25
|
+
brand,
|
|
26
|
+
type CommonOutputOptions,
|
|
27
|
+
defaultPrintSink,
|
|
28
|
+
emitReport,
|
|
29
|
+
statusMarker,
|
|
30
|
+
} from '../internal/output.js';
|
|
31
|
+
|
|
32
|
+
/** @stable */
|
|
33
|
+
export interface MigrateConfigOptions extends CommonOutputOptions {
|
|
34
|
+
readonly input: string;
|
|
35
|
+
readonly out?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** @stable */
|
|
39
|
+
export interface MigrateConfigResult {
|
|
40
|
+
readonly input: string;
|
|
41
|
+
readonly output: string;
|
|
42
|
+
readonly schemaVersion: '0.1';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @stable */
|
|
46
|
+
export async function runMigrateConfig(
|
|
47
|
+
options: MigrateConfigOptions,
|
|
48
|
+
): Promise<MigrateConfigResult> {
|
|
49
|
+
const cwd = process.cwd();
|
|
50
|
+
const inputPath = isAbsolute(options.input) ? options.input : resolve(cwd, options.input);
|
|
51
|
+
const outputPath =
|
|
52
|
+
options.out !== undefined
|
|
53
|
+
? isAbsolute(options.out)
|
|
54
|
+
? options.out
|
|
55
|
+
: resolve(cwd, options.out)
|
|
56
|
+
: `${inputPath}.migrated.json`;
|
|
57
|
+
if (inputPath === outputPath) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`[graphorin/cli] migrate-config refuses to overwrite the input file '${inputPath}'.`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
const loaded = await loadConfig(inputPath);
|
|
63
|
+
const parsed = parseServerConfig(loaded.config);
|
|
64
|
+
await writeFile(outputPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
|
|
65
|
+
const out: MigrateConfigResult = Object.freeze({
|
|
66
|
+
input: inputPath,
|
|
67
|
+
output: outputPath,
|
|
68
|
+
schemaVersion: '0.1',
|
|
69
|
+
});
|
|
70
|
+
emitReport(options, out, () => {
|
|
71
|
+
const print = options.print ?? defaultPrintSink;
|
|
72
|
+
print(brand(`${statusMarker('ok')} migrated config v${out.schemaVersion}`));
|
|
73
|
+
print(` input: ${out.input}`);
|
|
74
|
+
print(` output: ${out.output}`);
|
|
75
|
+
});
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import pkg from '../../package.json' with { type: 'json' };
|
|
2
|
+
/**
|
|
3
|
+
* `graphorin migrate-export <input> --to-schema <X.Y>` - schema-version
|
|
4
|
+
* migration for `graphorin-session-export/N.N` JSONL files (DEC-155 /
|
|
5
|
+
* ADR-042).
|
|
6
|
+
*
|
|
7
|
+
* The exporter ships schema 1.0 in v0.1; the migrator's main job today
|
|
8
|
+
* is to validate that the supplied input file is a well-formed session
|
|
9
|
+
* export with a schema in the framework's N-2 backwards-compat band
|
|
10
|
+
* and to round-trip the records through the writer with the desired
|
|
11
|
+
* target schema. When the runtime reaches MAJOR 2.x / 3.x the
|
|
12
|
+
* migrator extends the per-record transforms in place.
|
|
13
|
+
*
|
|
14
|
+
* The output path is required (`--to <file>`); the helper never
|
|
15
|
+
* overwrites the input.
|
|
16
|
+
*
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
21
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
22
|
+
import process from 'node:process';
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
createBufferSink,
|
|
26
|
+
createSessionExportWriter,
|
|
27
|
+
readSessionExport,
|
|
28
|
+
SESSION_EXPORT_SCHEMA_CURRENT,
|
|
29
|
+
type SessionExportRecord,
|
|
30
|
+
} from '@graphorin/sessions';
|
|
31
|
+
|
|
32
|
+
import { EXIT_CODES } from '../internal/exit.js';
|
|
33
|
+
import {
|
|
34
|
+
brand,
|
|
35
|
+
type CommonOutputOptions,
|
|
36
|
+
defaultPrintSink,
|
|
37
|
+
emitReport,
|
|
38
|
+
statusMarker,
|
|
39
|
+
} from '../internal/output.js';
|
|
40
|
+
|
|
41
|
+
/** @stable */
|
|
42
|
+
export interface MigrateExportOptions extends CommonOutputOptions {
|
|
43
|
+
readonly input: string;
|
|
44
|
+
readonly to: string;
|
|
45
|
+
/** Defaults to the writer's current schema (e.g. `'1.0'`). */
|
|
46
|
+
readonly toSchema?: string;
|
|
47
|
+
/** Surfaced on the meta header. Defaults to `graphorin-cli@<version>` (the current package version). */
|
|
48
|
+
readonly writer?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @stable */
|
|
52
|
+
export interface MigrateExportResult {
|
|
53
|
+
readonly input: string;
|
|
54
|
+
readonly output: string;
|
|
55
|
+
readonly schemaIn: string;
|
|
56
|
+
readonly schemaOut: string;
|
|
57
|
+
readonly records: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @stable */
|
|
61
|
+
export async function runMigrateExport(
|
|
62
|
+
options: MigrateExportOptions,
|
|
63
|
+
): Promise<MigrateExportResult> {
|
|
64
|
+
const cwd = process.cwd();
|
|
65
|
+
const inputPath = isAbsolute(options.input) ? options.input : resolve(cwd, options.input);
|
|
66
|
+
const outputPath = isAbsolute(options.to) ? options.to : resolve(cwd, options.to);
|
|
67
|
+
const targetSchema = options.toSchema ?? SESSION_EXPORT_SCHEMA_CURRENT;
|
|
68
|
+
|
|
69
|
+
if (inputPath === outputPath) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`[graphorin/cli] migrate-export refuses to overwrite the input file '${inputPath}'.`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (targetSchema !== SESSION_EXPORT_SCHEMA_CURRENT) {
|
|
75
|
+
const print = options.print ?? defaultPrintSink;
|
|
76
|
+
print(
|
|
77
|
+
brand(
|
|
78
|
+
`requested target schema '${targetSchema}' is not the writer's current schema (${SESSION_EXPORT_SCHEMA_CURRENT}); v0.1 supports the current schema only.`,
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
process.exit(EXIT_CODES.UNSUPPORTED);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const raw = await readFile(inputPath, 'utf8');
|
|
85
|
+
const parsed = readSessionExport(raw);
|
|
86
|
+
|
|
87
|
+
const buffer = createBufferSink();
|
|
88
|
+
const writer = createSessionExportWriter(buffer.sink, {
|
|
89
|
+
writer: options.writer ?? `graphorin-cli@${pkg.version}`,
|
|
90
|
+
...(parsed.meta.embedderIds !== undefined ? { embedderIds: parsed.meta.embedderIds } : {}),
|
|
91
|
+
});
|
|
92
|
+
for (const record of parsed.records) {
|
|
93
|
+
if (record.kind === 'unknown') continue;
|
|
94
|
+
await writer.writeRecord(record as SessionExportRecord);
|
|
95
|
+
}
|
|
96
|
+
await writer.close();
|
|
97
|
+
await writeFile(outputPath, buffer.toString(), { mode: 0o600 });
|
|
98
|
+
|
|
99
|
+
const out: MigrateExportResult = Object.freeze({
|
|
100
|
+
input: inputPath,
|
|
101
|
+
output: outputPath,
|
|
102
|
+
schemaIn: parsed.meta.version,
|
|
103
|
+
schemaOut: targetSchema,
|
|
104
|
+
records: parsed.records.length,
|
|
105
|
+
});
|
|
106
|
+
emitReport(options, out, () => {
|
|
107
|
+
const print = options.print ?? defaultPrintSink;
|
|
108
|
+
print(
|
|
109
|
+
brand(
|
|
110
|
+
`${statusMarker('ok')} migrated ${out.records} record(s) from schema ${out.schemaIn} -> ${out.schemaOut}`,
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
print(` input: ${out.input}`);
|
|
114
|
+
print(` output: ${out.output}`);
|
|
115
|
+
});
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `graphorin migrate` - apply pending storage migrations.
|
|
3
|
+
*
|
|
4
|
+
* Idempotent: re-running the command after every migration has been
|
|
5
|
+
* applied is a no-op + a successful exit. Failures bubble up with the
|
|
6
|
+
* underlying SQLite error so operators can fix corrupted state.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import process from 'node:process';
|
|
12
|
+
|
|
13
|
+
import { parseServerConfig } from '@graphorin/server';
|
|
14
|
+
import { createSqliteStore } from '@graphorin/store-sqlite';
|
|
15
|
+
import { loadConfig } from '../internal/load-config.js';
|
|
16
|
+
import { applyHardeningEarly } from './start.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @stable
|
|
20
|
+
*/
|
|
21
|
+
export interface MigrateCommandOptions {
|
|
22
|
+
readonly config?: string;
|
|
23
|
+
readonly target?: string;
|
|
24
|
+
readonly print?: (line: string) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @stable
|
|
29
|
+
*/
|
|
30
|
+
export interface MigrateCommandResult {
|
|
31
|
+
readonly applied: ReadonlyArray<string>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @stable
|
|
36
|
+
*/
|
|
37
|
+
export async function runMigrate(
|
|
38
|
+
options: MigrateCommandOptions = {},
|
|
39
|
+
): Promise<MigrateCommandResult> {
|
|
40
|
+
applyHardeningEarly();
|
|
41
|
+
const print = options.print ?? ((line: string) => process.stderr.write(`${line}\n`));
|
|
42
|
+
let loaded: Awaited<ReturnType<typeof loadConfig>>;
|
|
43
|
+
try {
|
|
44
|
+
loaded = await loadConfig(options.config);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
print(`[graphorin/cli] ${(err as Error).message}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
print(`[graphorin/cli] resolved config: ${loaded.path}`);
|
|
50
|
+
|
|
51
|
+
let parsed: ReturnType<typeof parseServerConfig>;
|
|
52
|
+
try {
|
|
53
|
+
parsed = parseServerConfig(loaded.config);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
print(`[graphorin/cli] ${(err as Error).message}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (options.target !== undefined) {
|
|
60
|
+
print(
|
|
61
|
+
`[graphorin/cli] --target is reserved for the Phase 15 migration runner; the v0.1 store applies every pending migration in order.`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const store = await createSqliteStore({
|
|
66
|
+
path: parsed.storage.path,
|
|
67
|
+
mode: parsed.storage.mode,
|
|
68
|
+
});
|
|
69
|
+
try {
|
|
70
|
+
await store.init();
|
|
71
|
+
const ids = store.appliedMigrations.map((m) => `${m.version} (${m.name})`);
|
|
72
|
+
print(`[graphorin/cli] applied ${ids.length} migration(s):`);
|
|
73
|
+
for (const id of ids) {
|
|
74
|
+
print(` - ${id}`);
|
|
75
|
+
}
|
|
76
|
+
return Object.freeze({ applied: Object.freeze(ids) });
|
|
77
|
+
} catch (err) {
|
|
78
|
+
print(`[graphorin/cli] migration failed: ${(err as Error).message}`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
} finally {
|
|
81
|
+
await store.close();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `graphorin pricing` - operate on the bundled LLM pricing snapshot.
|
|
3
|
+
*
|
|
4
|
+
* Surface (per Phase 15 § Pricing):
|
|
5
|
+
*
|
|
6
|
+
* - `graphorin pricing status` - bundled snapshot version + entry
|
|
7
|
+
* count + canonical digest.
|
|
8
|
+
* - `graphorin pricing refresh --url <url>` - opt-in network call
|
|
9
|
+
* that fetches a fresh snapshot. Honours `GRAPHORIN_OFFLINE=1`.
|
|
10
|
+
* - `graphorin pricing diff` - row-by-row delta vs the bundled
|
|
11
|
+
* snapshot.
|
|
12
|
+
* - `graphorin pricing lookup --provider <name> --model <id>` - print
|
|
13
|
+
* the per-token price for a single (provider, model) pair.
|
|
14
|
+
* - `graphorin pricing missing` - read trace spans from a JSON file
|
|
15
|
+
* and report unknown (provider, model) pairs.
|
|
16
|
+
*
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
21
|
+
import process from 'node:process';
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
BUNDLED_SNAPSHOT,
|
|
25
|
+
computeEntriesDigest,
|
|
26
|
+
diffPricing,
|
|
27
|
+
listMissingModels,
|
|
28
|
+
lookupPrice,
|
|
29
|
+
type PricingDiffEntry,
|
|
30
|
+
type PricingSnapshot,
|
|
31
|
+
refreshPricing,
|
|
32
|
+
SNAPSHOT_DATE,
|
|
33
|
+
} from '@graphorin/pricing';
|
|
34
|
+
|
|
35
|
+
import { EXIT_CODES } from '../internal/exit.js';
|
|
36
|
+
import { checkOfflineModeBlocked } from '../internal/offline.js';
|
|
37
|
+
import {
|
|
38
|
+
brand,
|
|
39
|
+
type CommonOutputOptions,
|
|
40
|
+
defaultPrintSink,
|
|
41
|
+
emitReport,
|
|
42
|
+
statusMarker,
|
|
43
|
+
} from '../internal/output.js';
|
|
44
|
+
|
|
45
|
+
/** @stable */
|
|
46
|
+
export interface PricingCommonOptions extends CommonOutputOptions {}
|
|
47
|
+
|
|
48
|
+
/** @stable */
|
|
49
|
+
export interface PricingStatusResult {
|
|
50
|
+
readonly version: string;
|
|
51
|
+
readonly snapshotDate: string;
|
|
52
|
+
readonly entries: number;
|
|
53
|
+
readonly digest: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** @stable */
|
|
57
|
+
export function runPricingStatus(options: PricingCommonOptions = {}): PricingStatusResult {
|
|
58
|
+
const out: PricingStatusResult = Object.freeze({
|
|
59
|
+
version: BUNDLED_SNAPSHOT.version,
|
|
60
|
+
snapshotDate: SNAPSHOT_DATE,
|
|
61
|
+
entries: BUNDLED_SNAPSHOT.entries.length,
|
|
62
|
+
digest: computeEntriesDigest(BUNDLED_SNAPSHOT.entries),
|
|
63
|
+
});
|
|
64
|
+
emitReport(options, out, () => {
|
|
65
|
+
const print = options.print ?? defaultPrintSink;
|
|
66
|
+
print(brand(`pricing snapshot v${out.version} (${out.snapshotDate})`));
|
|
67
|
+
print(` entries: ${out.entries}`);
|
|
68
|
+
print(` digest: ${out.digest}`);
|
|
69
|
+
});
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** @stable */
|
|
74
|
+
export interface PricingRefreshOptions extends PricingCommonOptions {
|
|
75
|
+
readonly url: string;
|
|
76
|
+
/**
|
|
77
|
+
* Optional path to write the refreshed snapshot to. When omitted the
|
|
78
|
+
* CLI prints a status summary only; `--out` triggers a JSON write.
|
|
79
|
+
*/
|
|
80
|
+
readonly out?: string;
|
|
81
|
+
/**
|
|
82
|
+
* W-097: accepted body format - `auto` (default) tries the native
|
|
83
|
+
* shape then auto-detects the `@pydantic/genai-prices` dataset.
|
|
84
|
+
*/
|
|
85
|
+
readonly format?: 'auto' | 'graphorin' | 'genai-prices';
|
|
86
|
+
/** Test seam - inject a fetch implementation. */
|
|
87
|
+
readonly fetchImpl?: typeof fetch;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** @stable */
|
|
91
|
+
export async function runPricingRefresh(options: PricingRefreshOptions): Promise<{
|
|
92
|
+
readonly entries: number;
|
|
93
|
+
readonly version: string;
|
|
94
|
+
readonly out?: string;
|
|
95
|
+
readonly skipped?: number;
|
|
96
|
+
}> {
|
|
97
|
+
if (
|
|
98
|
+
!checkOfflineModeBlocked('pricing refresh', {
|
|
99
|
+
...(options.print !== undefined ? { print: options.print } : {}),
|
|
100
|
+
})
|
|
101
|
+
) {
|
|
102
|
+
process.exit(EXIT_CODES.RECOVERABLE_FAILURE);
|
|
103
|
+
}
|
|
104
|
+
const refreshed = await refreshPricing({
|
|
105
|
+
url: options.url,
|
|
106
|
+
...(options.format !== undefined ? { format: options.format } : {}),
|
|
107
|
+
...(options.fetchImpl !== undefined ? { fetchImpl: options.fetchImpl } : {}),
|
|
108
|
+
});
|
|
109
|
+
if (options.out !== undefined) {
|
|
110
|
+
await writeFile(options.out, JSON.stringify(refreshed, null, 2), { mode: 0o600 });
|
|
111
|
+
}
|
|
112
|
+
const result: { entries: number; version: string; out?: string; skipped?: number } = {
|
|
113
|
+
entries: refreshed.entries.length,
|
|
114
|
+
version: refreshed.version,
|
|
115
|
+
...(options.out !== undefined ? { out: options.out } : {}),
|
|
116
|
+
...(refreshed.conversion !== undefined ? { skipped: refreshed.conversion.skipped } : {}),
|
|
117
|
+
};
|
|
118
|
+
emitReport(options, result, () => {
|
|
119
|
+
const print = options.print ?? defaultPrintSink;
|
|
120
|
+
print(brand(`refreshed pricing snapshot v${result.version} (${result.entries} entries).`));
|
|
121
|
+
if (result.skipped !== undefined) {
|
|
122
|
+
print(
|
|
123
|
+
brand(
|
|
124
|
+
`converted from genai-prices; ${result.skipped} entr${result.skipped === 1 ? 'y' : 'ies'} skipped (tiered/conditional pricing).`,
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (result.out !== undefined) print(brand(`written to ${result.out} (mode 0600).`));
|
|
129
|
+
});
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** @stable */
|
|
134
|
+
export interface PricingDiffOptions extends PricingCommonOptions {
|
|
135
|
+
/**
|
|
136
|
+
* Path to a JSON file containing a `PricingSnapshot`. The CLI diffs
|
|
137
|
+
* this against the bundled snapshot.
|
|
138
|
+
*/
|
|
139
|
+
readonly snapshot: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** @stable */
|
|
143
|
+
export async function runPricingDiff(
|
|
144
|
+
options: PricingDiffOptions,
|
|
145
|
+
): Promise<ReadonlyArray<PricingDiffEntry>> {
|
|
146
|
+
const right = await readSnapshot(options.snapshot);
|
|
147
|
+
const diff = diffPricing(BUNDLED_SNAPSHOT, right);
|
|
148
|
+
emitReport(options, diff, () => {
|
|
149
|
+
const print = options.print ?? defaultPrintSink;
|
|
150
|
+
if (diff.length === 0) {
|
|
151
|
+
print(brand('no diff between bundled snapshot and supplied snapshot.'));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
print(brand(`${diff.length} pricing diff row(s):`));
|
|
155
|
+
for (const row of diff) {
|
|
156
|
+
print(` ${statusMarker('warn')} ${row.kind} ${row.provider}/${row.model}`);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
return diff;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** @stable */
|
|
163
|
+
export interface PricingLookupOptions extends PricingCommonOptions {
|
|
164
|
+
readonly provider: string;
|
|
165
|
+
readonly model: string;
|
|
166
|
+
readonly region?: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** @stable */
|
|
170
|
+
export function runPricingLookup(options: PricingLookupOptions) {
|
|
171
|
+
const result = lookupPrice({
|
|
172
|
+
provider: options.provider,
|
|
173
|
+
model: options.model,
|
|
174
|
+
...(options.region !== undefined ? { region: options.region } : {}),
|
|
175
|
+
});
|
|
176
|
+
emitReport(options, result, () => {
|
|
177
|
+
const print = options.print ?? defaultPrintSink;
|
|
178
|
+
if (result === null) {
|
|
179
|
+
print(brand(`no pricing entry for ${options.provider}/${options.model}.`));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
print(
|
|
183
|
+
brand(
|
|
184
|
+
`${options.provider}/${options.model}: input=${result.inputUsdPerToken} / output=${result.outputUsdPerToken} USD per token (source=${result.source}, snapshot=${result.snapshotDate})`,
|
|
185
|
+
),
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
// W-002: exit code independent of --json (see runAuditVerify).
|
|
189
|
+
if (result === null) process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** @stable */
|
|
194
|
+
export interface PricingMissingOptions extends PricingCommonOptions {
|
|
195
|
+
/**
|
|
196
|
+
* Path to a JSON file containing an array of trace spans (each with
|
|
197
|
+
* an `attributes` map). Output of `graphorin traces export` (Phase
|
|
198
|
+
* 15) is the canonical source.
|
|
199
|
+
*/
|
|
200
|
+
readonly spans: string;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** @stable */
|
|
204
|
+
export async function runPricingMissing(options: PricingMissingOptions) {
|
|
205
|
+
const raw = await readFile(options.spans, 'utf8');
|
|
206
|
+
let parsed: unknown;
|
|
207
|
+
try {
|
|
208
|
+
parsed = JSON.parse(raw);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`[graphorin/cli] '${options.spans}' is not valid JSON: ${(err as Error).message}`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (!Array.isArray(parsed)) {
|
|
215
|
+
throw new Error(`[graphorin/cli] '${options.spans}' must contain an array of trace spans.`);
|
|
216
|
+
}
|
|
217
|
+
// The pricing helper trusts the shape; we cast through unknown.
|
|
218
|
+
const missing = listMissingModels(
|
|
219
|
+
parsed as ReadonlyArray<{ readonly attributes: Record<string, unknown> }>,
|
|
220
|
+
);
|
|
221
|
+
emitReport(options, missing, () => {
|
|
222
|
+
const print = options.print ?? defaultPrintSink;
|
|
223
|
+
if (missing.length === 0) {
|
|
224
|
+
print(brand('every (provider, model) pair in the spans is present in the bundled snapshot.'));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
print(brand(`${missing.length} unknown (provider, model) pair(s):`));
|
|
228
|
+
for (const m of missing) {
|
|
229
|
+
print(` ${statusMarker('warn')} ${m.provider}/${m.model} (${m.count} span(s))`);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
return missing;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function readSnapshot(path: string): Promise<PricingSnapshot> {
|
|
236
|
+
const raw = await readFile(path, 'utf8');
|
|
237
|
+
try {
|
|
238
|
+
return JSON.parse(raw) as PricingSnapshot;
|
|
239
|
+
} catch (err) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`[graphorin/cli] '${path}' is not a valid pricing snapshot JSON: ${(err as Error).message}`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|