faces-cli 1.7.14 → 1.8.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.
@@ -0,0 +1,65 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../base.js';
3
+ import { FacesAPIError } from '../../client.js';
4
+ import { VERSIONS_PATH } from '../../style.js';
5
+ export default class StyleRevert extends BaseCommand {
6
+ static description = 'Go back to the style a face had before the last style:make. It steps backwards through the kept ' +
7
+ 'versions and there is no step forward, so the way to undo a revert is to capture the style again. ' +
8
+ 'It changes how the face writes immediately, so it asks first.';
9
+ static examples = ['<%= config.bin %> <%= command.id %> alice --yes'];
10
+ static flags = {
11
+ ...BaseCommand.baseFlags,
12
+ yes: Flags.boolean({ description: 'Skip confirmation', default: false }),
13
+ };
14
+ static args = {
15
+ alias: Args.string({ description: 'Face alias', required: true }),
16
+ };
17
+ async run() {
18
+ const { args, flags } = await this.parse(StyleRevert);
19
+ const client = this.makeClient(flags);
20
+ // This writes to a live face the moment it is called. Requiring --yes is
21
+ // not ceremony: the endpoint takes a single field and gives no dry run, so
22
+ // an exploratory call is indistinguishable from a real one.
23
+ if (!flags.yes) {
24
+ this.error(`This changes how '${args.alias}' writes, straight away. Re-run with --yes to confirm.\n` +
25
+ `See what it would go back to: faces style:versions ${args.alias}`);
26
+ }
27
+ let data;
28
+ try {
29
+ data = (await client.post(`${VERSIONS_PATH}/revert`, { body: { self_face: args.alias } }));
30
+ }
31
+ catch (err) {
32
+ if (err instanceof FacesAPIError) {
33
+ if (err.statusCode === 404)
34
+ this.error(`No face named '${args.alias}'. It does not exist, or is not yours.`);
35
+ if (err.statusCode === 409)
36
+ this.error(`Error (409): ${err.message}\nThe face may be locked. Unlock it first.`);
37
+ // Reverting past the oldest kept version fails this way rather than
38
+ // with a clean refusal (faces-backend-shared#590).
39
+ if (err.statusCode === 500) {
40
+ this.error(`Error (500): ${err.message}\n` +
41
+ `'${args.alias}' may already be on the oldest style that is still kept, in which case there ` +
42
+ 'is nothing further back to go to. Check with: faces style:versions ' +
43
+ args.alias);
44
+ }
45
+ this.error(`Error (${err.statusCode}): ${err.message}`);
46
+ }
47
+ throw err;
48
+ }
49
+ if (this.jsonEnabled())
50
+ return data;
51
+ const from = data.from_version;
52
+ const to = data.to_version;
53
+ if (from !== undefined && to !== undefined) {
54
+ this.log(`'${args.alias}' moved from version ${from} back to version ${to}.`);
55
+ }
56
+ else {
57
+ this.log(`'${args.alias}' reverted.`);
58
+ }
59
+ // Not "run it again to undo": another revert steps back again, it does not
60
+ // return. Verified against production.
61
+ this.log(`There is no step forward. To undo this, capture the style again: faces style:make ${args.alias} --all`);
62
+ this.log(`List versions: faces style:versions ${args.alias}`);
63
+ return data;
64
+ }
65
+ }
@@ -0,0 +1,19 @@
1
+ import { BaseCommand } from '../../base.js';
2
+ export default class StyleStatus extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ face: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
8
+ wait: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ timeout: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ 'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ };
14
+ static args: {
15
+ job_id: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
16
+ };
17
+ run(): Promise<unknown>;
18
+ private list;
19
+ }
@@ -0,0 +1,102 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../base.js';
3
+ import { FacesAPIError } from '../../client.js';
4
+ import { BUILDS_PATH, EXIT_NO_SUCH_FACE, formatBuildReport, jobFailureMessage, faceExists, pollStyleJob, publicJob } from '../../style.js';
5
+ export default class StyleStatus extends BaseCommand {
6
+ static description = 'Read a style build. Give a job id for one build, or --face to list a face\'s recent builds. ' +
7
+ 'Listing is how you find a job id you no longer have.';
8
+ static examples = [
9
+ '<%= config.bin %> <%= command.id %> 7a8b9c0d',
10
+ '<%= config.bin %> <%= command.id %> --face alice',
11
+ '<%= config.bin %> <%= command.id %> 7a8b9c0d --wait',
12
+ ];
13
+ static flags = {
14
+ ...BaseCommand.baseFlags,
15
+ face: Flags.string({ description: 'List recent builds for this face instead of reading one job' }),
16
+ limit: Flags.integer({ description: 'How many builds to list with --face (default: 10)', default: 10 }),
17
+ wait: Flags.boolean({ description: 'Block until the job finishes', default: false }),
18
+ timeout: Flags.integer({ description: 'How long to wait with --wait, in seconds (default: 3600)', default: 3600 }),
19
+ };
20
+ static args = {
21
+ job_id: Args.string({ description: 'Build job id', required: false }),
22
+ };
23
+ async run() {
24
+ const { args, flags } = await this.parse(StyleStatus);
25
+ const client = this.makeClient(flags);
26
+ const json = this.jsonEnabled();
27
+ if (!args.job_id && !flags.face) {
28
+ this.error('Give a job id, or --face <alias> to list a face\'s builds.');
29
+ }
30
+ if (args.job_id && flags.face) {
31
+ this.error('Pass either a job id or --face, not both.');
32
+ }
33
+ if (flags.face)
34
+ return this.list(client, flags.face, flags.limit, json);
35
+ let data;
36
+ try {
37
+ data = flags.wait
38
+ ? await pollStyleJob(client, BUILDS_PATH, args.job_id, { timeoutMs: flags.timeout * 1000 })
39
+ : (await client.get(`${BUILDS_PATH}/${encodeURIComponent(args.job_id)}`));
40
+ }
41
+ catch (err) {
42
+ if (err instanceof FacesAPIError) {
43
+ if (err.statusCode === 404)
44
+ this.error(`No build job '${args.job_id}'. List a face's builds with --face <alias>.`);
45
+ this.error(`Error (${err.statusCode}): ${err.message}`);
46
+ }
47
+ throw err;
48
+ }
49
+ if (json)
50
+ return publicJob(data);
51
+ for (const line of formatBuildReport(data))
52
+ this.log(line);
53
+ if (data.status === 'failed')
54
+ this.log(`\nerror: ${jobFailureMessage(data.error)}`);
55
+ return data;
56
+ }
57
+ async list(client, face, limit, json) {
58
+ let data;
59
+ try {
60
+ data = (await client.get(`${BUILDS_PATH}?self_face=${encodeURIComponent(face)}&limit=${limit}`));
61
+ }
62
+ catch (err) {
63
+ if (err instanceof FacesAPIError) {
64
+ if (err.statusCode === 404)
65
+ this.error(`No face named '${face}'. It does not exist, or is not yours.`);
66
+ this.error(`Error (${err.statusCode}): ${err.message}`);
67
+ }
68
+ throw err;
69
+ }
70
+ const jobs = data.jobs ?? [];
71
+ // An empty list comes back for an unknown face too. "No builds yet" and
72
+ // "no such face" are opposite answers, so they must not share an output.
73
+ if (jobs.length === 0 && !(await faceExists(client, face))) {
74
+ this.error(`No face named '${face}'. It does not exist, or is not yours.`, { exit: EXIT_NO_SUCH_FACE });
75
+ }
76
+ if (json)
77
+ return { ...data, jobs: jobs.map((j) => publicJob(j)) };
78
+ if (jobs.length === 0) {
79
+ this.log(`'${face}' has no style builds yet. Capture one with: faces style:make ${face} --all`);
80
+ return data;
81
+ }
82
+ const rows = jobs.map((j) => ({
83
+ job: String(j.job_id ?? '-'),
84
+ status: String(j.status ?? '-'),
85
+ when: String(j.created_at ?? '').slice(0, 19).replace('T', ' ') || '-',
86
+ model: String(j.report?.model ?? j.model ?? '-'),
87
+ }));
88
+ const cols = [
89
+ ['JOB', (r) => r.job],
90
+ ['STATUS', (r) => r.status],
91
+ ['STARTED', (r) => r.when],
92
+ ];
93
+ const w = cols.map(([h, g]) => Math.max(h.length, ...rows.map((r) => g(r).length)));
94
+ const line = (c) => c.map((x, i) => x.padEnd(w[i])).join(' ').trimEnd();
95
+ this.log(line(cols.map(([h]) => h)));
96
+ for (const r of rows)
97
+ this.log(line(cols.map(([, g]) => g(r))));
98
+ this.log('');
99
+ this.log(`${rows.length} build(s) shown, newest first. Raise the cap with --limit.`);
100
+ return data;
101
+ }
102
+ }
@@ -0,0 +1,18 @@
1
+ import { BaseCommand } from '../../base.js';
2
+ export default class StyleUpload extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ type: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ 'messages-per-room': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
8
+ strict: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ 'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ };
13
+ static args: {
14
+ alias: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
15
+ file: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
16
+ };
17
+ run(): Promise<unknown>;
18
+ }
@@ -0,0 +1,81 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { BaseCommand } from '../../base.js';
5
+ import { FacesAPIError } from '../../client.js';
6
+ export default class StyleUpload extends BaseCommand {
7
+ static description = 'Load a corpus of a person\'s own writing (a mail export, a message archive) as source material for ' +
8
+ 'style capture. This only stores the material: nothing is compiled and nothing is billed. Capture ' +
9
+ 'the style afterwards with style:make.';
10
+ static examples = [
11
+ '<%= config.bin %> <%= command.id %> alice ./mail.jsonl',
12
+ '<%= config.bin %> <%= command.id %> alice ./thread.json --type thread_json',
13
+ '<%= config.bin %> <%= command.id %> alice ./mail.jsonl --messages-per-room 100 --strict',
14
+ ];
15
+ static flags = {
16
+ ...BaseCommand.baseFlags,
17
+ type: Flags.string({
18
+ description: 'email_jsonl: one message per line, split across several rooms. thread_json: a JSON array that ' +
19
+ 'becomes a single room.',
20
+ options: ['email_jsonl', 'thread_json'],
21
+ default: 'email_jsonl',
22
+ }),
23
+ 'messages-per-room': Flags.integer({
24
+ description: 'How many messages per room when the type is email_jsonl (1-1000, default: 50)',
25
+ default: 50,
26
+ }),
27
+ strict: Flags.boolean({
28
+ description: 'Reject the whole upload if any message is invalid. By default invalid messages are skipped and ' +
29
+ 'reported, because a corpus is statistical and a few bad lines are not worth losing the rest.',
30
+ default: false,
31
+ }),
32
+ };
33
+ static args = {
34
+ alias: Args.string({ description: 'Face alias', required: true }),
35
+ file: Args.string({ description: 'Corpus file', required: true }),
36
+ };
37
+ async run() {
38
+ const { args, flags } = await this.parse(StyleUpload);
39
+ const client = this.makeClient(flags);
40
+ const json = this.jsonEnabled();
41
+ if (!fs.existsSync(args.file))
42
+ this.error(`File not found: ${args.file}`);
43
+ const form = new FormData();
44
+ form.append('file', new Blob([fs.readFileSync(args.file)], { type: 'application/json' }), path.basename(args.file));
45
+ const params = new URLSearchParams({ type: flags.type });
46
+ if (flags.type === 'email_jsonl')
47
+ params.set('emails_per_thread', String(flags['messages-per-room']));
48
+ if (flags.strict)
49
+ params.set('strict', 'true');
50
+ let data;
51
+ try {
52
+ data = (await client.postForm(`/v1/faces/${encodeURIComponent(args.alias)}/upload?${params}`, form));
53
+ }
54
+ catch (err) {
55
+ if (err instanceof FacesAPIError) {
56
+ if (err.statusCode === 404)
57
+ this.error(`No face named '${args.alias}'. It does not exist, or is not yours.`);
58
+ if (err.statusCode === 422 && flags.strict) {
59
+ this.error(`Error (422): ${err.message}\nDrop --strict to skip invalid messages instead of refusing the upload.`);
60
+ }
61
+ this.error(`Error (${err.statusCode}): ${err.message}`);
62
+ }
63
+ throw err;
64
+ }
65
+ if (json)
66
+ return data;
67
+ const roomIds = data.room_ids ?? [];
68
+ const skipped = data.skipped;
69
+ this.log(`face: ${data.alias ?? args.alias}`);
70
+ this.log(`messages: ${data.message_count ?? '?'}`);
71
+ this.log(`rooms: ${data.room_count ?? roomIds.length}`);
72
+ // Never let a partial upload read as a complete one.
73
+ if (skipped?.length) {
74
+ this.log(`skipped: ${skipped.length} message(s) failed validation and were not stored.`);
75
+ this.log(' Re-run with --strict to refuse the whole upload instead.');
76
+ }
77
+ this.log('');
78
+ this.log(`Capture the style: faces style:make ${args.alias} --all`);
79
+ return data;
80
+ }
81
+ }
@@ -0,0 +1,14 @@
1
+ import { BaseCommand } from '../../base.js';
2
+ export default class StyleVersions extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ 'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ };
10
+ static args: {
11
+ alias: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
12
+ };
13
+ run(): Promise<unknown>;
14
+ }
@@ -0,0 +1,77 @@
1
+ import { Args } from '@oclif/core';
2
+ import { BaseCommand } from '../../base.js';
3
+ import { FacesAPIError } from '../../client.js';
4
+ import { EXIT_NO_SUCH_FACE, VERSIONS_PATH, faceExists } from '../../style.js';
5
+ export default class StyleVersions extends BaseCommand {
6
+ static description = 'List the styles captured for a face, and which one it is writing in now. Each style:make keeps ' +
7
+ 'the previous one, so this is what style:revert has to go back to.';
8
+ static examples = ['<%= config.bin %> <%= command.id %> alice', '<%= config.bin %> <%= command.id %> alice --json'];
9
+ static flags = { ...BaseCommand.baseFlags };
10
+ static args = {
11
+ alias: Args.string({ description: 'Face alias', required: true }),
12
+ };
13
+ async run() {
14
+ const { args, flags } = await this.parse(StyleVersions);
15
+ const client = this.makeClient(flags);
16
+ let data;
17
+ try {
18
+ data = (await client.get(`${VERSIONS_PATH}/${encodeURIComponent(args.alias)}`));
19
+ }
20
+ catch (err) {
21
+ if (err instanceof FacesAPIError && err.statusCode === 404) {
22
+ // The 404 covers both "no style" and "no face". Only one is an error.
23
+ if (await faceExists(client, args.alias)) {
24
+ if (this.jsonEnabled())
25
+ return { self_face: args.alias, versions: [] };
26
+ this.log(`'${args.alias}' exists and has no captured style yet.`);
27
+ this.log(`Capture one with: faces style:make ${args.alias} --all`);
28
+ return { self_face: args.alias, versions: [] };
29
+ }
30
+ this.error(`No face named '${args.alias}'. It does not exist, or is not yours.`, { exit: EXIT_NO_SUCH_FACE });
31
+ }
32
+ if (err instanceof FacesAPIError)
33
+ this.error(`Error (${err.statusCode}): ${err.message}`);
34
+ throw err;
35
+ }
36
+ const versions = data.versions ?? [];
37
+ if (this.jsonEnabled())
38
+ return data;
39
+ if (versions.length === 0) {
40
+ this.log(`'${args.alias}' has no captured style. Capture one with: faces style:make ${args.alias} --all`);
41
+ return data;
42
+ }
43
+ const rows = versions.map((v) => ({
44
+ version: String(v.version ?? '-'),
45
+ active: v.active ? 'yes' : '-',
46
+ model: v.model?.trim() || '-',
47
+ created: String(v.created_at ?? '').slice(0, 19).replace('T', ' ') || '-',
48
+ }));
49
+ const cols = [
50
+ ['VERSION', (r) => r.version],
51
+ ['ACTIVE', (r) => r.active],
52
+ ['MODEL', (r) => r.model],
53
+ ['CAPTURED', (r) => r.created],
54
+ ];
55
+ const w = cols.map(([h, g]) => Math.max(h.length, ...rows.map((r) => g(r).length)));
56
+ const line = (c) => c.map((x, i) => x.padEnd(w[i])).join(' ').trimEnd();
57
+ this.log(line(cols.map(([h]) => h)));
58
+ for (const r of rows)
59
+ this.log(line(cols.map(([, g]) => g(r))));
60
+ // More than one row can come back marked active (faces-backend-shared#589).
61
+ // Saying "you are writing in version N" would be a guess, so when the data
62
+ // is ambiguous the command says so instead of picking one.
63
+ const active = versions.filter((v) => v.active);
64
+ this.log('');
65
+ if (active.length === 1) {
66
+ this.log(`Writing in version ${active[0].version} now. ${versions.length} version(s) kept.`);
67
+ }
68
+ else if (active.length === 0) {
69
+ this.log(`${versions.length} version(s) kept. None is marked active.`);
70
+ }
71
+ else {
72
+ this.log(`${versions.length} version(s) kept, and ${active.length} are marked active, so which one is in ` +
73
+ 'use cannot be read from this list. Reported upstream.');
74
+ }
75
+ return data;
76
+ }
77
+ }
package/dist/routing.d.ts CHANGED
@@ -26,3 +26,12 @@ export declare function isCompositeFormula(modelArg: string): boolean;
26
26
  * default model from the local catalog, falling back to GET /v1/faces/{alias}.
27
27
  */
28
28
  export declare function resolveEndpoint(client: FacesClient, modelArg: string): Promise<RouteInfo>;
29
+ /**
30
+ * Whether a model runs on the user's own linked ChatGPT subscription, and so
31
+ * costs them nothing, or always bills.
32
+ *
33
+ * Returns undefined when the catalog cannot say — an unknown model, or a cache
34
+ * written before this field was read. Undefined must not be treated as "bills":
35
+ * refusing to run because we are out of date is worse than running.
36
+ */
37
+ export declare function codexEligible(client: FacesClient, llm: string): Promise<boolean | undefined>;
package/dist/routing.js CHANGED
@@ -131,3 +131,23 @@ export async function resolveEndpoint(client, modelArg) {
131
131
  return { endpoint: hit.endpoint, llm, freeWhenConnected: hit.freeWhenConnected };
132
132
  return { endpoint: fallbackEndpoint(llm), llm, freeWhenConnected: false };
133
133
  }
134
+ /**
135
+ * Whether a model runs on the user's own linked ChatGPT subscription, and so
136
+ * costs them nothing, or always bills.
137
+ *
138
+ * Returns undefined when the catalog cannot say — an unknown model, or a cache
139
+ * written before this field was read. Undefined must not be treated as "bills":
140
+ * refusing to run because we are out of date is worse than running.
141
+ */
142
+ export async function codexEligible(client, llm) {
143
+ const hits = (await getModels(client)).filter((m) => m.id === llm);
144
+ if (hits.length === 0)
145
+ return undefined;
146
+ // The catalog lists paid and OAuth twins under one id; eligible on either
147
+ // means the free route exists.
148
+ if (hits.some((m) => m.codex_eligible === true))
149
+ return true;
150
+ if (hits.every((m) => m.codex_eligible === false))
151
+ return false;
152
+ return undefined;
153
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Shared helpers for the `style` command family.
3
+ *
4
+ * "Style" is what the CLI calls the API's voiceprint: the writing manner
5
+ * captured from a person's own material and installed on their face. It is a
6
+ * separate act from `compile`, which captures what a face knows. One family
7
+ * answers "what does it know", the other "how does it write".
8
+ *
9
+ * The API names a source by a triple — id, medium, source_type. Users should
10
+ * only ever have to give an id, so everything here exists to turn one into the
11
+ * other from what the list endpoints already report.
12
+ */
13
+ import { FacesClient } from './client.js';
14
+ export declare const BUILDS_PATH = "/v1/voiceprint/builds";
15
+ export declare const VERSIONS_PATH = "/v1/voiceprint/versions";
16
+ export declare const STYLE_FACES_PATH = "/v1/voiceprint/faces";
17
+ /**
18
+ * Analyst model used when the caller does not choose one.
19
+ *
20
+ * Not the server default, deliberately. The server still defaults to an older
21
+ * model (faces-backend-shared#574) and the build is the most expensive call in
22
+ * the product, so the CLI names the current recommendation rather than
23
+ * inheriting a stale one.
24
+ */
25
+ export declare const DEFAULT_STYLE_MODEL = "gpt-5.6-terra";
26
+ /**
27
+ * Medium assumed for a room that does not declare one.
28
+ *
29
+ * Corpus rooms carry `corpus_kind`. Ordinary threads do not, because they are
30
+ * live conversations rather than an imported corpus — so a thread with no
31
+ * declared medium is a conversation, which is the one case where the medium
32
+ * follows from the source type rather than from a guess about content.
33
+ */
34
+ export declare const ROOM_DEFAULT_MEDIUM = "conversation";
35
+ /** Exit code for "no such face", so a caller can tell it from a face with no style. */
36
+ export declare const EXIT_NO_SUCH_FACE = 4;
37
+ /**
38
+ * Whether a face exists at all.
39
+ *
40
+ * The voiceprint routes cannot answer this. `GET /voiceprint/versions/{alias}`
41
+ * returns the same 404 for a face that has no style as for a face that does not
42
+ * exist, and `GET /voiceprint/builds?self_face=` returns an empty list for
43
+ * both. Those are opposite answers, so the face route is asked directly rather
44
+ * than reporting one as the other.
45
+ */
46
+ export declare function faceExists(client: FacesClient, alias: string): Promise<boolean>;
47
+ /** A source the CLI can name in a build request. */
48
+ export interface Selectable {
49
+ id: string;
50
+ sourceType: 'room' | 'document';
51
+ /** Resolved from the listing. Undefined means the caller has to declare it. */
52
+ medium?: string;
53
+ label: string;
54
+ synced: boolean;
55
+ /**
56
+ * True for imported corpus material. Distinguishes it from a live thread,
57
+ * which is the one source type a build may not compile for free.
58
+ */
59
+ isCorpus: boolean;
60
+ /** Present once this source has been printed. */
61
+ printedAt?: string;
62
+ printedMedium?: string;
63
+ }
64
+ /** What goes on the wire. `medium`, not `kind` — renamed 2026-08-27, no alias. */
65
+ export interface SourceRef {
66
+ id: string;
67
+ medium: string;
68
+ source_type: 'room' | 'document';
69
+ }
70
+ /**
71
+ * Every source on a face that a build could name.
72
+ *
73
+ * `include_uploads` matters: imported corpus rooms are excluded by default, and
74
+ * they are the main thing a style build is made from.
75
+ */
76
+ export declare function listSelectable(client: FacesClient, alias: string): Promise<Selectable[]>;
77
+ /**
78
+ * Parse one `--source` value: `<id>` or `<id>:<medium>`.
79
+ *
80
+ * Ids are UUIDs, so the first colon separates cleanly and a medium containing
81
+ * spaces ("academic paper") survives intact.
82
+ */
83
+ export declare function parseSourceArg(raw: string): {
84
+ id: string;
85
+ medium?: string;
86
+ };
87
+ /**
88
+ * Poll a voiceprint job until it stops.
89
+ *
90
+ * Returns the terminal payload including a failed one — whether a failure is an
91
+ * error is the caller's decision, not this function's.
92
+ */
93
+ export declare function pollStyleJob(client: FacesClient, basePath: string, jobId: string, opts?: {
94
+ intervalMs?: number;
95
+ timeoutMs?: number;
96
+ onPoll?: (d: Record<string, unknown>) => void;
97
+ }): Promise<Record<string, unknown>>;
98
+ /** Failure text, with the one hint that is actionable rather than descriptive. */
99
+ export declare function jobFailureMessage(error: unknown): string;
100
+ /**
101
+ * A job payload safe to hand onward, with its report reduced to public fields.
102
+ *
103
+ * Applied to --json too. A machine consumer reading our output is as much a way
104
+ * for an internal key to escape as a terminal is.
105
+ */
106
+ export declare function publicJob(data: Record<string, unknown>): Record<string, unknown>;
107
+ /** Human-readable build summary. Never dumps the report. */
108
+ export declare function formatBuildReport(data: Record<string, unknown>): string[];