faces-cli 1.8.6 → 1.8.8

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/dist/catalog.js CHANGED
@@ -135,7 +135,7 @@ export class CatalogService {
135
135
  // what kept it alive across writes; dropping it lets the next sync
136
136
  // replace it with the real one.
137
137
  if (existingDescription.length > MAX_FRONTMATTER_VALUE) {
138
- process.stderr.write(`warn: discarded a ${existingDescription.length}-character description on '${alias}' ` +
138
+ process.stderr.write(`warn: discarded a ${existingDescription.length}-character description on '${alias}': ` +
139
139
  'too large to be real. It will be restored from the server on the next sync.\n');
140
140
  }
141
141
  else {
@@ -83,13 +83,13 @@ export default class AuthConnect extends BaseCommand {
83
83
  // 3. Show the code + URL and the one-time enable-setting guidance.
84
84
  emit('');
85
85
  emit('To connect ChatGPT:');
86
- emit(` 1. Open this URL in any browser (phone, laptop anywhere):`);
86
+ emit(` 1. Open this URL in any browser (phone, laptop, anywhere):`);
87
87
  emit(` ${verificationUri}`);
88
88
  emit(` 2. Enter this code: ${start.user_code}`);
89
89
  emit(` 3. Sign in to ChatGPT and click Continue.`);
90
90
  emit('');
91
91
  emit('If this is your first time, you may need to enable device-code authorization in ChatGPT first:');
92
- emit(` 1. Open ${SETTINGS_URL} (the settings panel can take a few seconds to appear give it a moment)`);
92
+ emit(` 1. Open ${SETTINGS_URL} (the settings panel can take a few seconds to appear, so give it a moment)`);
93
93
  emit(' 2. Turn on "Enable device code authorization for Codex"');
94
94
  emit(' 3. Then enter the code above');
95
95
  emit('');
@@ -193,7 +193,7 @@ export default class AuthConnect extends BaseCommand {
193
193
  const steps = Array.isArray(d.settings_steps) && d.settings_steps.length > 0
194
194
  ? d.settings_steps
195
195
  : [
196
- `Open ${url} (the settings panel can take a few seconds to appear give it a moment)`,
196
+ `Open ${url} (the settings panel can take a few seconds to appear, so give it a moment)`,
197
197
  'Turn on "Enable device code authorization for Codex"',
198
198
  'Then run `faces auth:connect openai` again',
199
199
  ];
@@ -26,7 +26,7 @@ export default class AuthConnections extends BaseCommand {
26
26
  const tier = label ? ` (${label})` : '';
27
27
  this.log(`openai connected${who}${tier} connected_at=${row.connected_at}`);
28
28
  if (!plan || plan === 'free') {
29
- this.log(' ⚠️ This ChatGPT plan may not include API access connecting requires a paid plan (Plus/Pro/Team, etc.).');
29
+ this.log(' ⚠️ This ChatGPT plan may not include API access. Connecting requires a paid plan (Plus/Pro/Team, etc.).');
30
30
  }
31
31
  }
32
32
  else {
@@ -46,7 +46,7 @@ export default class BillingSubscriptionActivate extends BaseCommand {
46
46
  }
47
47
  const result = data;
48
48
  if (!json) {
49
- this.log(`Reactivated. Connect plan continues next renewal: ${result.current_period_end ?? 'unknown'}.`);
49
+ this.log(`Reactivated. Connect plan continues. Next renewal: ${result.current_period_end ?? 'unknown'}.`);
50
50
  }
51
51
  return { status: 'reactivated', ...result };
52
52
  }
@@ -63,7 +63,7 @@ export default class BillingSubscriptionActivate extends BaseCommand {
63
63
  const result = data;
64
64
  const url = result.checkout_url;
65
65
  if (!json) {
66
- this.log('Subscription Connect $17/month');
66
+ this.log('Subscription Connect: $17/month');
67
67
  this.log('');
68
68
  this.log('Complete payment at this link:');
69
69
  this.log(` ${url}`);
@@ -301,7 +301,7 @@ export default class CatalogDoctor extends BaseCommand {
301
301
  }
302
302
  catch (err) {
303
303
  const msg = err instanceof FacesAPIError ? `Error (${err.statusCode}): ${err.message}` : String(err);
304
- this.log(` ${alias}: failed ${msg}`);
304
+ this.log(` ${alias}: failed: ${msg}`);
305
305
  }
306
306
  }
307
307
  this.log(`Generated ${generated} description(s).`);
@@ -19,7 +19,7 @@ export default class CatalogList extends BaseCommand {
19
19
  if (this.jsonEnabled())
20
20
  return entries;
21
21
  if (entries.length === 0) {
22
- this.log('(no faces in catalog run faces catalog:doctor --fix)');
22
+ this.log('(no faces in catalog. Run faces catalog:doctor --fix)');
23
23
  return entries;
24
24
  }
25
25
  const nameWidth = Math.max(...entries.map((e) => String(e.alias ?? '').length));
@@ -13,6 +13,7 @@ export default class ChatChat extends BaseCommand {
13
13
  file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
14
  responses: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
15
  medium: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ 'best-of': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
17
  'oauth-only': import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
18
  'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
19
  token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -5,9 +5,12 @@ import { FacesAPIError } from '../../client.js';
5
5
  import { extractStreamDelta, isMaxTokensRenameError, MIN_MAX_OUTPUT_TOKENS, MEDIUM_FLAG_DESCRIPTION, warnDroppedParams, CHAT_PARAM_FLAGS } from '../../utils.js';
6
6
  import { resolveEndpoint, MESSAGES_ENDPOINT, RESPONSES_ENDPOINT, isCompositeFormula, faceSpecOf, llmFromArg } from '../../routing.js';
7
7
  export default class ChatChat extends BaseCommand {
8
- static description = 'Chat via a face. Auto-routes to the correct API endpoint based on the model catalog.';
8
+ static description = 'Chat via a face. Auto-routes to the correct API endpoint based on the model catalog. ' +
9
+ 'Prefix the alias with + to answer in the style the face captured, e.g. +alice; without it the face ' +
10
+ 'uses its ordinary voice.';
9
11
  static examples = [
10
12
  '<%= config.bin %> <%= command.id %> socrates -m "What is virtue?"',
13
+ '<%= config.bin %> <%= command.id %> +alice -m "Reply to the note below." --medium email',
11
14
  '<%= config.bin %> <%= command.id %> "(socrates | nietzsche)@claude-sonnet-4-6" -m "What is virtue?"',
12
15
  '<%= config.bin %> <%= command.id %> --formula "socrates | nietzsche" --llm claude-sonnet-4-6 -m "What is virtue?"',
13
16
  ];
@@ -23,11 +26,16 @@ export default class ChatChat extends BaseCommand {
23
26
  file: Flags.string({ description: 'Read message from file' }),
24
27
  responses: Flags.boolean({ description: 'Force the OpenAI Responses API endpoint (escape hatch; routing is automatic otherwise)', default: false }),
25
28
  medium: Flags.string({ description: MEDIUM_FLAG_DESCRIPTION }),
29
+ 'best-of': Flags.integer({
30
+ description: 'Write N replies and serve the one closest to the captured style (1-5, default 1). Only ' +
31
+ 'meaningful with a + alias, and it multiplies the cost of the request by N. Cannot be streamed.',
32
+ }),
26
33
  'oauth-only': Flags.boolean({ description: 'Prevent fallback to paid system key (use OAuth only)', default: false }),
27
34
  };
28
35
  static args = {
29
36
  face_username: Args.string({
30
- description: 'Face alias (alias@model, owner:alias@model for a published face, or a composite formula like "(a | b)@model"). Omit when using --formula.',
37
+ description: 'Face alias (alias@model, owner:alias@model for a published face, or a composite formula like ' +
38
+ '"(a | b)@model"). Prefix with + to use the face\'s captured style. Omit when using --formula.',
31
39
  required: false,
32
40
  }),
33
41
  };
@@ -59,6 +67,20 @@ export default class ChatChat extends BaseCommand {
59
67
  this.error(`Composite face '${spec}' has no default model. Specify one explicitly, ` +
60
68
  `e.g. '${spec}@claude-sonnet-4-6', or pass --llm <model>.`);
61
69
  }
70
+ // best_of_n picks between finished drafts, so there is nothing to stream.
71
+ // The server answers 400 deepself_streaming_unsupported; say so first.
72
+ if (flags['best-of'] !== undefined) {
73
+ if (flags['best-of'] < 1 || flags['best-of'] > 5) {
74
+ this.error(`--best-of must be between 1 and 5 (got ${flags['best-of']}).`);
75
+ }
76
+ if (flags.stream) {
77
+ this.error('--best-of cannot be streamed: choosing between drafts needs them finished. Drop --stream or --best-of.');
78
+ }
79
+ if (!model.startsWith('+')) {
80
+ this.error(`--best-of only applies to a face's captured style, so the alias needs a + prefix: '+${model}'.\n` +
81
+ 'Without it the face answers in its ordinary voice and there is nothing to choose between.');
82
+ }
83
+ }
62
84
  // Collect user messages
63
85
  const userMessages = [];
64
86
  if (flags.file) {
@@ -90,6 +112,14 @@ export default class ChatChat extends BaseCommand {
90
112
  if (err instanceof FacesAPIError) {
91
113
  if (err.errorCode === 'oauth_rejected')
92
114
  this.error(`OAuth failed: ${err.message}\n${this.oauthHint(err, { ...route, endpoint })}`);
115
+ // A + request on a face with no captured style is refused, not quietly
116
+ // downgraded. The same request without the + works.
117
+ if (err.statusCode === 409 && model.startsWith('+')) {
118
+ const bare = faceSpecOf(model).replace(/^\+/, '');
119
+ this.error(`Error (409): ${err.message}\n` +
120
+ `Capture one with: faces style:make ${bare} --all\n` +
121
+ `Or drop the + to use ${bare}'s ordinary voice.`);
122
+ }
93
123
  this.error(`Error (${err.statusCode}): ${err.message}`);
94
124
  }
95
125
  throw err;
@@ -143,6 +173,8 @@ export default class ChatChat extends BaseCommand {
143
173
  payload.temperature = Number.parseFloat(flags.temperature);
144
174
  if (flags.medium)
145
175
  payload.medium = flags.medium;
176
+ if (flags['best-of'] !== undefined)
177
+ payload.best_of_n = flags['best-of'];
146
178
  if (flags['oauth-only'])
147
179
  payload.oauth_only = true;
148
180
  if (flags.stream) {
@@ -189,6 +221,8 @@ export default class ChatChat extends BaseCommand {
189
221
  payload.system = flags.system;
190
222
  if (flags.medium)
191
223
  payload.medium = flags.medium;
224
+ if (flags['best-of'] !== undefined)
225
+ payload.best_of_n = flags['best-of'];
192
226
  if (flags['oauth-only'])
193
227
  payload.oauth_only = true;
194
228
  if (flags.stream) {
@@ -234,6 +268,8 @@ export default class ChatChat extends BaseCommand {
234
268
  payload.temperature = Number.parseFloat(flags.temperature);
235
269
  if (flags.medium)
236
270
  payload.medium = flags.medium;
271
+ if (flags['best-of'] !== undefined)
272
+ payload.best_of_n = flags['best-of'];
237
273
  if (flags['oauth-only'])
238
274
  payload.oauth_only = true;
239
275
  if (flags.stream) {
@@ -73,7 +73,7 @@ function listThreads() {
73
73
  return out;
74
74
  }
75
75
  export default class ChatThread extends BaseCommand {
76
- static description = 'Multi-turn chat thread. Conversation history is stored under ~/.faces/threads/<id>.json resume any thread later with --id.';
76
+ static description = 'Multi-turn chat thread. Conversation history is stored under ~/.faces/threads/<id>.json. Resume any thread later with --id.';
77
77
  static examples = [
78
78
  '<%= config.bin %> <%= command.id %> socrates -m "What is justice?"',
79
79
  '<%= config.bin %> <%= command.id %> --id t_abc123 -m "Say more about that"',
@@ -99,7 +99,7 @@ export default class ChatThread extends BaseCommand {
99
99
  };
100
100
  static args = {
101
101
  face_username: Args.string({
102
- description: 'Face alias (alias@model, or owner:alias@model for a published face) required when starting a new thread',
102
+ description: 'Face alias (alias@model, or owner:alias@model for a published face). Required when starting a new thread',
103
103
  required: false,
104
104
  }),
105
105
  };
@@ -64,7 +64,7 @@ export default class CompileAll extends BaseCommand {
64
64
  }
65
65
  if (items.length === 0) {
66
66
  if (!json)
67
- this.log('Nothing to compile all documents and threads are up to date.');
67
+ this.log('Nothing to compile. All documents and threads are up to date.');
68
68
  return { compiled: 0, failed: 0, items: [] };
69
69
  }
70
70
  if (!json)
@@ -17,7 +17,7 @@ export default class CompileDoc extends BaseCommand {
17
17
  ...BaseCommand.baseFlags,
18
18
  label: Flags.string({ description: 'Document label/title (single document only; with several files each is labelled by its filename)' }),
19
19
  content: Flags.string({ description: 'Inline text content', exclusive: ['file'] }),
20
- file: Flags.string({ description: 'Read content from file (repeatable each file becomes its own document)', multiple: true }),
20
+ file: Flags.string({ description: 'Read content from file (repeatable; each file becomes its own document)', multiple: true }),
21
21
  medium: Flags.string({ description: MEDIUM_FLAG_DESCRIPTION }),
22
22
  perspective: Flags.string({
23
23
  description: 'Perspective',
@@ -177,8 +177,8 @@ export default class CompileDoc extends BaseCommand {
177
177
  }
178
178
  /** Extensions the compile-documents endpoint takes as inline text. */
179
179
  const BINARY_HINTS = {
180
- '.pdf': 'PDF is supported, but not by this command — use: faces compile:upload <alias> --file <path> --kind document',
181
- '.docx': 'Word documents are supported, but not by this command — use: faces compile:upload <alias> --file <path> --kind document',
180
+ '.pdf': 'PDF is supported, but not by this command. Use: faces compile:upload <alias> --file <path> --kind document',
181
+ '.docx': 'Word documents are supported, but not by this command. Use: faces compile:upload <alias> --file <path> --kind document',
182
182
  '.doc': "Legacy '.doc' is not supported. Open it in Word or Pages and save as .docx, then upload that with: faces compile:upload <alias> --file <path> --kind document",
183
183
  '.pages': 'Pages documents are not supported. Export it as text or PDF first.',
184
184
  '.rtf': 'RTF is not supported. Convert it to plain text first (e.g. `textutil -convert txt file.rtf`).',
@@ -60,13 +60,13 @@ export default class CompileDocPause extends BaseCommand {
60
60
  }
61
61
  case 'synced':
62
62
  case 'ready': {
63
- return 'Compile finished before the pause took effect nothing to resume.';
63
+ return 'Compile finished before the pause took effect. Nothing to resume.';
64
64
  }
65
65
  case 'failed': {
66
66
  return 'Compile failed before the pause took effect.';
67
67
  }
68
68
  case null: {
69
- return 'Stopped the compile is no longer running.';
69
+ return 'Stopped. The compile is no longer running.';
70
70
  }
71
71
  default: {
72
72
  return `Still stopping after ${timeout}s. Poll with: faces compile:doc:get ${id} --json`;
@@ -56,7 +56,7 @@ export default class CompileImport extends BaseCommand {
56
56
  catch (err) {
57
57
  if (err instanceof FacesAPIError) {
58
58
  if (err.statusCode === 422 && flags.type === 'thread') {
59
- this.error(`${err.message} try again with --type document`);
59
+ this.error(`${err.message}. Try again with --type document`);
60
60
  }
61
61
  this.error(`Error (${err.statusCode}): ${err.message}`);
62
62
  }
@@ -61,13 +61,13 @@ export default class CompileThreadPause extends BaseCommand {
61
61
  }
62
62
  case 'synced':
63
63
  case 'ready': {
64
- return 'Compile finished before the pause took effect nothing to resume.';
64
+ return 'Compile finished before the pause took effect. Nothing to resume.';
65
65
  }
66
66
  case 'failed': {
67
67
  return 'Compile failed before the pause took effect.';
68
68
  }
69
69
  case null: {
70
- return 'Stopped the compile is no longer running.';
70
+ return 'Stopped. The compile is no longer running.';
71
71
  }
72
72
  default: {
73
73
  return `Still stopping after ${timeout}s. Poll with: faces compile:thread:get ${id} --json`;
@@ -30,7 +30,7 @@ export default class FaceDiff extends BaseCommand {
30
30
  if (!this.jsonEnabled()) {
31
31
  const res = data;
32
32
  const faces = res.faces;
33
- const fmt = (v) => (v === null || v === undefined ? '' : v.toFixed(2));
33
+ const fmt = (v) => (v === null || v === undefined ? '-' : v.toFixed(2));
34
34
  for (let i = 0; i < faces.length; i++) {
35
35
  for (let j = i + 1; j < faces.length; j++) {
36
36
  const a = faces[i];
@@ -2,7 +2,7 @@ import { Args } from '@oclif/core';
2
2
  import { BaseCommand } from '../../base.js';
3
3
  import { FacesAPIError } from '../../client.js';
4
4
  export default class FaceLock extends BaseCommand {
5
- static description = 'Lock a face (read-only). A locked face still reads and chats normally, but it and everything it owns (documents, threads, voiceprint) cannot be changed until unlocked.';
5
+ static description = 'Lock a face (read-only). A locked face still reads and chats normally, but it, and everything it owns (documents, threads, captured style), cannot be changed until unlocked.';
6
6
  static examples = ['<%= config.bin %> <%= command.id %> socrates'];
7
7
  static flags = {
8
8
  ...BaseCommand.baseFlags,
@@ -24,7 +24,7 @@ export default class FaceLock extends BaseCommand {
24
24
  }
25
25
  if (!this.jsonEnabled()) {
26
26
  const res = data;
27
- this.log(`Locked '${res.alias ?? args.face_id}' read only.`);
27
+ this.log(`Locked '${res.alias ?? args.face_id}' is now read only.`);
28
28
  const affected = res.affected ?? [];
29
29
  if (affected.length > 0)
30
30
  this.log(`Affected: ${affected.join(', ')}`);
@@ -6,7 +6,7 @@ export default class FaceNeighbors extends BaseCommand {
6
6
  static flags = {
7
7
  ...BaseCommand.baseFlags,
8
8
  k: Flags.integer({
9
- description: 'Number of results (120)',
9
+ description: 'Number of results (1-20)',
10
10
  default: 5,
11
11
  min: 1,
12
12
  max: 20,
@@ -15,7 +15,7 @@ export default class FaceShare extends BaseCommand {
15
15
  ...BaseCommand.baseFlags,
16
16
  list: Flags.boolean({ description: 'Show who the face is shared with, and change nothing', default: false }),
17
17
  add: Flags.string({
18
- description: 'Add an account, keeping everyone already on the list username or email (repeatable)',
18
+ description: 'Add an account, keeping everyone already on the list. Username or email (repeatable)',
19
19
  multiple: true,
20
20
  exclusive: ['with', 'none'],
21
21
  }),
@@ -2,7 +2,7 @@ import { Args } from '@oclif/core';
2
2
  import { BaseCommand } from '../../base.js';
3
3
  import { FacesAPIError } from '../../client.js';
4
4
  export default class FaceUnlock extends BaseCommand {
5
- static description = 'Unlock a face (make it writable again). Thaws the face and everything it owns (documents, threads, voiceprint).';
5
+ static description = 'Unlock a face (make it writable again). Thaws the face and everything it owns (documents, threads, captured style).';
6
6
  static examples = ['<%= config.bin %> <%= command.id %> socrates'];
7
7
  static flags = {
8
8
  ...BaseCommand.baseFlags,
@@ -24,7 +24,7 @@ export default class FaceUnlock extends BaseCommand {
24
24
  }
25
25
  if (!this.jsonEnabled()) {
26
26
  const res = data;
27
- this.log(`Unlocked '${res.alias ?? args.face_id}' writable.`);
27
+ this.log(`Unlocked '${res.alias ?? args.face_id}' is now writable.`);
28
28
  const affected = res.affected ?? [];
29
29
  if (affected.length > 0)
30
30
  this.log(`Affected: ${affected.join(', ')}`);
@@ -2,7 +2,7 @@ import { Args } from '@oclif/core';
2
2
  import { BaseCommand } from '../../base.js';
3
3
  import { FacesAPIError } from '../../client.js';
4
4
  export default class FaceUnpublish extends BaseCommand {
5
- static description = 'Stop publishing a face. Anyone it is individually shared with keeps their access publishing is an ' +
5
+ static description = 'Stop publishing a face. Anyone it is individually shared with keeps their access. Publishing is an ' +
6
6
  'override on top of sharing, not a replacement for it.';
7
7
  static flags = { ...BaseCommand.baseFlags };
8
8
  static args = {
@@ -2,7 +2,7 @@ import { Args, Flags } from '@oclif/core';
2
2
  import { BaseCommand } from '../../base.js';
3
3
  import { FacesAPIError } from '../../client.js';
4
4
  export default class FaceUnshare extends BaseCommand {
5
- static description = 'Revoke access to a shared face. Takes effect immediately there is no grace period.';
5
+ static description = 'Revoke access to a shared face. Takes effect immediately. There is no grace period.';
6
6
  static examples = [
7
7
  '<%= config.bin %> <%= command.id %> alice --from dana --yes',
8
8
  '<%= config.bin %> <%= command.id %> alice --all --yes',
@@ -55,15 +55,15 @@ export default class KeysCreate extends BaseCommand {
55
55
  }
56
56
  this.printHuman(data);
57
57
  this.log('');
58
- this.log('⚠ The full key is shown only once `faces keys:list` returns it truncated by design.');
58
+ this.log('⚠ The full key is shown only once. `faces keys:list` returns it truncated by design.');
59
59
  if (saved) {
60
- this.log('✓ Saved to ~/.faces/config.json as api_key faces commands will use it automatically.');
60
+ this.log('✓ Saved to ~/.faces/config.json as api_key. Faces commands will use it automatically.');
61
61
  }
62
62
  else if (!flags.save) {
63
63
  this.log(' Not saved (--no-save). Copy the key above now.');
64
64
  }
65
65
  else {
66
- this.warn('Could not find the key in the response nothing was saved. Copy it manually.');
66
+ this.warn('Could not find the key in the response. Nothing was saved. Copy it manually.');
67
67
  }
68
68
  return data;
69
69
  }
@@ -3,18 +3,6 @@ import { createInterface } from 'node:readline';
3
3
  import { BaseCommand } from '../../base.js';
4
4
  import { FacesAPIError } from '../../client.js';
5
5
  import { STYLE_FACES_PATH } from '../../style.js';
6
- /**
7
- * The API takes a scope: `map` clears the captured style, `all` clears it and
8
- * destroys the uploaded material with it. This CLI only ever sends `map`.
9
- *
10
- * Nothing that removes a style should be able to take source text with it. A
11
- * user reaching for "delete the style" is not asking to lose the writing it was
12
- * learned from, and the two are one keystroke apart on the same command. Source
13
- * text is deleted through the commands that own it — compile:doc:delete and
14
- * compile:thread:delete — where that is the whole point of the call rather than
15
- * a side effect of a flag value.
16
- */
17
- const SCOPE = 'map';
18
6
  export default class StyleDelete extends BaseCommand {
19
7
  static description = "Delete a face's captured style. The material it was learned from is kept, so the style can be " +
20
8
  'captured again without re-uploading. This never deletes documents or threads: remove those with ' +
@@ -43,7 +31,7 @@ export default class StyleDelete extends BaseCommand {
43
31
  }
44
32
  let data;
45
33
  try {
46
- data = await client.delete(`${STYLE_FACES_PATH}/${encodeURIComponent(args.alias)}?scope=${SCOPE}`);
34
+ data = await client.delete(`${STYLE_FACES_PATH}/${encodeURIComponent(args.alias)}`);
47
35
  }
48
36
  catch (err) {
49
37
  if (err instanceof FacesAPIError) {
@@ -104,7 +104,11 @@ export default class StyleMake extends BaseCommand {
104
104
  for (const line of formatBuildReport(final))
105
105
  this.log(line);
106
106
  this.log('');
107
- this.log(`'${args.alias}' now writes in its own style. Try it: faces chat:chat ${args.alias} -m "..."`);
107
+ // Capturing a style does not apply it. The plain alias still answers in the
108
+ // face's ordinary voice; the + prefix is how a caller asks for the style.
109
+ this.log(`'${args.alias}' has captured a style. Use it by prefixing the alias with +:`);
110
+ this.log(` faces chat:chat +${args.alias} -m "..."`);
111
+ this.log(`Without the +, the face answers in its ordinary voice. Both are valid.`);
108
112
  this.log(`Go back to the previous style: faces style:revert ${args.alias} --yes`);
109
113
  return final;
110
114
  }
@@ -173,14 +177,23 @@ export default class StyleMake extends BaseCommand {
173
177
  else {
174
178
  chosen = [];
175
179
  const missing = [];
180
+ // Naming a source twice is accepted by the server and prints it twice,
181
+ // which weights it double in the analysis for no stated reason. A later
182
+ // mention wins so `--source X --source X:essay` declares a medium rather
183
+ // than being a contradiction.
184
+ const seen = new Map();
176
185
  for (const raw of flags.source ?? []) {
177
186
  const { id, medium } = parseSourceArg(raw);
178
- const s = byId.get(id);
179
- if (s)
180
- chosen.push({ s, declared: medium });
181
- else
187
+ if (!byId.has(id)) {
182
188
  missing.push(id);
189
+ continue;
190
+ }
191
+ if (seen.has(id) && medium === undefined)
192
+ continue;
193
+ seen.set(id, medium ?? seen.get(id));
183
194
  }
195
+ for (const [id, medium] of seen)
196
+ chosen.push({ s: byId.get(id), declared: medium });
184
197
  if (missing.length > 0) {
185
198
  this.error(`Not a source on '${alias}': ${missing.join(', ')}\n` +
186
199
  `List what is there with: faces face:sources ${alias}`);
package/dist/style.d.ts CHANGED
@@ -11,9 +11,9 @@
11
11
  * other from what the list endpoints already report.
12
12
  */
13
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";
14
+ export declare const BUILDS_PATH = "/v1/style/builds";
15
+ export declare const VERSIONS_PATH = "/v1/style/versions";
16
+ export declare const STYLE_FACES_PATH = "/v1/style/faces";
17
17
  /**
18
18
  * Analyst model used when the caller does not choose one.
19
19
  *
package/dist/style.js CHANGED
@@ -11,9 +11,9 @@
11
11
  * other from what the list endpoints already report.
12
12
  */
13
13
  import { FacesAPIError } from './client.js';
14
- export const BUILDS_PATH = '/v1/voiceprint/builds';
15
- export const VERSIONS_PATH = '/v1/voiceprint/versions';
16
- export const STYLE_FACES_PATH = '/v1/voiceprint/faces';
14
+ export const BUILDS_PATH = '/v1/style/builds';
15
+ export const VERSIONS_PATH = '/v1/style/versions';
16
+ export const STYLE_FACES_PATH = '/v1/style/faces';
17
17
  /**
18
18
  * Analyst model used when the caller does not choose one.
19
19
  *
@@ -85,7 +85,7 @@ function mediumOf(r, sourceType) {
85
85
  return !declared || declared === UNIDENTIFIED_CORPUS ? undefined : declared;
86
86
  }
87
87
  function rowToSelectable(r, sourceType) {
88
- const stamp = r.voiceprint ?? undefined;
88
+ const stamp = r.style ?? undefined;
89
89
  return {
90
90
  id: String(r.document_id ?? r.thread_id ?? ''),
91
91
  sourceType,
@@ -97,10 +97,8 @@ function rowToSelectable(r, sourceType) {
97
97
  isCorpus: sourceType === 'document' || Boolean(r.corpus_medium?.trim()),
98
98
  isLiveThread: sourceType === 'room' && !r.corpus_medium?.trim(),
99
99
  authored: r.authored_message_count ?? undefined,
100
- // The stamp was renamed alongside the request field, but stored stamps kept
101
- // the old key (faces-backend-shared#589), so both are read.
102
- printedMedium: stamp?.medium ?? stamp?.kind,
103
- printedAt: stamp?.printed_at,
100
+ printedMedium: stamp?.medium,
101
+ printedAt: stamp?.captured_at,
104
102
  };
105
103
  }
106
104
  function rowsOf(data) {
@@ -181,6 +179,16 @@ export async function pollStyleJob(client, basePath, jobId, opts = {}) {
181
179
  * A build failure is shown to the user, and a stack trace or a SQL statement
182
180
  * tells them nothing they can act on while burying whatever might have. These
183
181
  * are reported as an upstream fault with the first line kept for a bug report.
182
+ *
183
+ * The server sanitises `error` itself now (faces-backend-shared#623), so this
184
+ * is inert against everything it currently returns — checked against the
185
+ * sanitised text and against every deliberate code it documents
186
+ * (CHATGPT_AUTH, INSUFFICIENT_CREDITS, NOTHING_AUTHORED, HOLDOUT_UNMEETABLE,
187
+ * OAUTH_RATE_LIMITED), none of which match. It is kept as a backstop rather
188
+ * than removed: `error` is rendered straight to a user, a raw statement reached
189
+ * one once, and a check that costs nothing until it fires is worth more than
190
+ * the line it saves. It is deliberately narrow so an actionable message can
191
+ * never be swallowed by it.
184
192
  */
185
193
  const INTERNAL_FAULT = /sqlalchemy|asyncpg|psycopg|Traceback|\[SQL:|IntegrityError|OperationalError/i;
186
194
  /** Failure text, with the one hint that is actionable rather than descriptive. */
package/dist/utils.js CHANGED
@@ -95,7 +95,7 @@ export function isMaxTokensRenameError(message) {
95
95
  * synonyms fold server-side, the set grows, and compile's own 422 names the
96
96
  * valid ones better than a stale local list could.
97
97
  */
98
- export const MEDIUM_FLAG_DESCRIPTION = 'What sort of writing this is e.g. email, text message, social post, essay, academic paper, ' +
98
+ export const MEDIUM_FLAG_DESCRIPTION = 'What sort of writing this is, e.g. email, text message, social post, essay, academic paper, ' +
99
99
  'blog post, legal document, thread reply, conversation (dialogue: transcripts, interviews, calls), ' +
100
100
  'lecture (sustained speech nobody interrupts: talks, sermons, keynotes). Common synonyms fold ' +
101
101
  'automatically. Omit it if you do not know: a wrong declaration is worse than none, because a ' +
@@ -120,7 +120,7 @@ export function warnDroppedParams(headers, flagFor) {
120
120
  .map((p) => flagFor[p] ?? `--${p.replaceAll('_', '-')}`);
121
121
  if (flags.length === 0)
122
122
  return;
123
- process.stderr.write(`Note: ${flags.join(' and ')} had no effect this request ran on your linked ChatGPT account, ` +
123
+ process.stderr.write(`Note: ${flags.join(' and ')} had no effect. This request ran on your linked ChatGPT account, ` +
124
124
  'which does not accept them. Use --no-oauth-only to run on the paid API instead.\n');
125
125
  }
126
126
  /** Maps the server's parameter names back to the flags a user actually typed. */