faces-cli 1.6.17 → 1.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/dist/client.d.ts CHANGED
@@ -2,9 +2,11 @@ export declare class FacesAPIError extends Error {
2
2
  statusCode: number;
3
3
  errorCode?: string;
4
4
  fallbackAvailable?: boolean;
5
+ details?: Record<string, unknown>;
5
6
  constructor(statusCode: number, message: string, opts?: {
6
7
  errorCode?: string;
7
8
  fallbackAvailable?: boolean;
9
+ details?: Record<string, unknown>;
8
10
  });
9
11
  }
10
12
  export interface ResponseWithHeaders<T = unknown> {
package/dist/client.js CHANGED
@@ -2,12 +2,14 @@ export class FacesAPIError extends Error {
2
2
  statusCode;
3
3
  errorCode;
4
4
  fallbackAvailable;
5
+ details;
5
6
  constructor(statusCode, message, opts) {
6
7
  super(message);
7
8
  this.statusCode = statusCode;
8
9
  this.name = 'FacesAPIError';
9
10
  this.errorCode = opts?.errorCode;
10
11
  this.fallbackAvailable = opts?.fallbackAvailable;
12
+ this.details = opts?.details;
11
13
  }
12
14
  }
13
15
  export class FacesClient {
@@ -46,6 +48,7 @@ export class FacesClient {
46
48
  let msg = resp.statusText;
47
49
  let errorCode;
48
50
  let fallbackAvailable;
51
+ let details;
49
52
  try {
50
53
  const body = await resp.json();
51
54
  // Structured OAuth rejection (422)
@@ -56,11 +59,16 @@ export class FacesClient {
56
59
  }
57
60
  else {
58
61
  const raw = body.detail ?? body.error ?? body.message ?? msg;
59
- if (typeof raw === 'object' && raw !== null && 'message' in raw) {
60
- msg = String(raw.message);
62
+ if (typeof raw === 'object' && raw !== null) {
63
+ // Structured error detail, e.g. {error, message, settings_url, settings_steps}
64
+ const obj = raw;
65
+ details = obj;
66
+ if (typeof obj.error === 'string')
67
+ errorCode = obj.error;
68
+ msg = 'message' in obj ? String(obj.message) : JSON.stringify(obj);
61
69
  }
62
70
  else {
63
- msg = typeof raw === 'object' ? JSON.stringify(raw) : String(raw);
71
+ msg = String(raw);
64
72
  }
65
73
  }
66
74
  }
@@ -71,7 +79,7 @@ export class FacesClient {
71
79
  msg = `Payment required: ${msg}`;
72
80
  if (resp.status === 403)
73
81
  msg = `Forbidden: ${msg}`;
74
- return new FacesAPIError(resp.status, msg, { errorCode, fallbackAvailable });
82
+ return new FacesAPIError(resp.status, msg, { errorCode, fallbackAvailable, details });
75
83
  }
76
84
  async get(path, opts = {}) {
77
85
  let url = this.url(path);
@@ -5,11 +5,12 @@ export default class AuthConnect extends BaseCommand {
5
5
  provider: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
6
6
  };
7
7
  static flags: {
8
- manual: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
8
  'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
9
  token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
10
  'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
11
  };
13
12
  static examples: string[];
14
13
  run(): Promise<unknown>;
14
+ private startDevice;
15
+ private handleStartError;
15
16
  }
@@ -1,71 +1,27 @@
1
- import { Args, Flags } from '@oclif/core';
2
- import * as http from 'node:http';
3
- import * as readline from 'node:readline';
4
- import { spawn } from 'node:child_process';
1
+ import { Args } from '@oclif/core';
5
2
  import { BaseCommand } from '../../base.js';
6
3
  import { FacesAPIError } from '../../client.js';
7
- const CALLBACK_PORT = 1455;
8
- const HTML_SUCCESS = `<!DOCTYPE html><html>
9
- <head><title>Connected</title><style>body{font-family:sans-serif;padding:48px;max-width:480px;color:#333}</style></head>
10
- <body><h2 style="color:#2e7d32">✅ Connected</h2>
11
- <p>Your account is linked. You can close this tab and return to your terminal.</p>
12
- </body></html>`;
13
- const HTML_ERROR = `<!DOCTYPE html><html>
14
- <head><title>Error</title><style>body{font-family:sans-serif;padding:48px;max-width:480px;color:#333}</style></head>
15
- <body><h2 style="color:#c62828">Connection failed</h2>
16
- <p>Something went wrong. Check your terminal for details.</p>
17
- </body></html>`;
18
- function openBrowser(url) {
19
- const p = process.platform;
20
- const [cmd, args] = p === 'darwin' ? ['open', [url]] :
21
- p === 'win32' ? ['cmd', ['/c', 'start', url]] :
22
- ['xdg-open', [url]];
23
- spawn(cmd, args, { detached: true, stdio: 'ignore' }).unref();
4
+ const VERIFICATION_FALLBACK = 'https://auth.openai.com/codex/device';
5
+ const SETTINGS_URL = 'https://chatgpt.com/#settings/Security';
6
+ const HARD_STOP_MS = 15 * 60 * 1000; // device codes expire after 15 minutes
7
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
8
+ const emit = (msg = '') => process.stderr.write(msg + '\n');
9
+ // Unwrap a possible StandardResponse envelope ({data: {...}}) without
10
+ // clobbering a flat payload that already has the expected key.
11
+ function unwrap(raw, key) {
12
+ const r = raw;
13
+ if (r && typeof r === 'object' && !(key in r) && r.data && typeof r.data === 'object') {
14
+ return r.data;
15
+ }
16
+ return raw;
24
17
  }
25
- function waitForCallback(timeoutMs = 300_000) {
26
- return new Promise((resolve, reject) => {
27
- const server = http.createServer((req, res) => {
28
- const url = new URL(req.url ?? '/', `http://127.0.0.1:${CALLBACK_PORT}`);
29
- if (url.pathname !== '/auth/callback') {
30
- res.writeHead(404);
31
- res.end();
32
- return;
33
- }
34
- const code = url.searchParams.get('code');
35
- const state = url.searchParams.get('state');
36
- if (!code || !state) {
37
- res.writeHead(400, { 'Content-Type': 'text/html' });
38
- res.end(HTML_ERROR);
39
- server.close();
40
- reject(new Error('OAuth callback missing code or state'));
41
- return;
42
- }
43
- res.writeHead(200, { 'Content-Type': 'text/html' });
44
- res.end(HTML_SUCCESS);
45
- server.close();
46
- resolve({ code, state });
47
- });
48
- server.on('error', (err) => {
49
- const code = err.code;
50
- if (code === 'EADDRINUSE') {
51
- reject(new Error(`Port ${CALLBACK_PORT} is already in use. ` +
52
- 'Make sure no other process is listening on that port and retry.'));
53
- }
54
- else {
55
- reject(err);
56
- }
57
- });
58
- server.listen(CALLBACK_PORT, '127.0.0.1');
59
- const timer = setTimeout(() => {
60
- server.close();
61
- reject(new Error('Timed out waiting for OAuth callback (5 minutes). Please try again.'));
62
- }, timeoutMs);
63
- // Don't let the timer keep the process alive
64
- timer.unref();
65
- });
18
+ function planLabel(plan) {
19
+ if (!plan)
20
+ return '';
21
+ return plan.charAt(0).toUpperCase() + plan.slice(1);
66
22
  }
67
23
  export default class AuthConnect extends BaseCommand {
68
- static description = 'Connect an OAuth provider account to Faces (e.g. ChatGPT Plus/Pro)';
24
+ static description = 'Connect your ChatGPT account to Faces via device-code authorization (Subscription Connect plan)';
69
25
  static args = {
70
26
  provider: Args.string({
71
27
  description: 'OAuth provider to connect',
@@ -75,165 +31,177 @@ export default class AuthConnect extends BaseCommand {
75
31
  };
76
32
  static flags = {
77
33
  ...BaseCommand.baseFlags,
78
- manual: Flags.boolean({
79
- description: 'Manual mode for headless environments: prints the authorize URL and ' +
80
- 'prompts you to paste the callback URL after approving in any browser.',
81
- default: false,
82
- }),
83
34
  };
84
35
  static examples = [
85
36
  '<%= config.bin %> auth connect openai',
86
- '<%= config.bin %> auth connect openai --manual',
37
+ '<%= config.bin %> auth connect openai --json',
87
38
  ];
88
39
  async run() {
89
- const { args, flags } = await this.parse(AuthConnect);
40
+ const { flags } = await this.parse(AuthConnect);
90
41
  const client = this.makeClient(flags, true);
91
- // 0. Short-circuit if already connected
42
+ // 0. Short-circuit if already connected.
92
43
  try {
93
44
  const existing = (await client.get('/v1/oauth', { requireJwt: true }));
94
- if (existing.some((r) => r.provider === args.provider)) {
95
- const output = { provider: args.provider, connected: true, already_connected: true };
96
- if (!this.jsonEnabled())
97
- this.log(`Already connected to ${args.provider}. Run 'faces auth:disconnect ${args.provider}' to reconnect.`);
45
+ const row = existing.find((r) => r.provider === 'openai');
46
+ if (row) {
47
+ const email = row.account_email ?? null;
48
+ const plan = row.chatgpt_plan_type ?? null;
49
+ const output = { provider: 'openai', connected: true, already_connected: true, account_email: email, chatgpt_plan_type: plan };
50
+ if (!this.jsonEnabled()) {
51
+ const who = email ? ` as ${email}` : '';
52
+ const tier = plan ? ` (${planLabel(plan)})` : '';
53
+ this.log(`Already connected to ChatGPT${who}${tier}. Run 'faces auth:disconnect openai' to reconnect.`);
54
+ }
98
55
  return output;
99
56
  }
100
57
  }
101
- catch { /* ignore — proceed with connect flow */ }
102
- // 1. Get the authorize URL (backend generates PKCE, stores state)
103
- this.log(`Connecting ${args.provider}...`);
104
- let authorizeData;
58
+ catch {
59
+ /* ignore proceed with connect flow */
60
+ }
61
+ // 1. Start the device authorization (backend brokers OpenAI).
62
+ let start;
105
63
  try {
106
- authorizeData = (await client.get(`/v1/oauth/${args.provider}/authorize`, {
107
- requireJwt: true,
108
- }));
64
+ start = await this.startDevice(client);
109
65
  }
110
66
  catch (err) {
111
- if (err instanceof FacesAPIError) {
112
- if (err.statusCode === 403)
113
- this.error('OAuth provider routing requires the connect plan.\n' +
114
- 'Upgrade with: faces billing checkout');
115
- if (err.statusCode === 501)
116
- this.error(`Provider '${args.provider}' is not yet available.`);
117
- this.error(`Failed to get authorize URL (${err.statusCode}): ${err.message}`);
118
- }
119
- throw err;
67
+ this.handleStartError(err);
120
68
  }
121
- // 2. Get code + state — either via local server or manual paste
122
- let code, state;
123
- if (flags.manual) {
124
- this.log('\nOpen this URL in any browser (phone, laptop, etc.):\n' +
125
- ` ${authorizeData.authorize_url}\n\n` +
126
- 'After approving on the OpenAI page:\n' +
127
- ' • If you have the Faces browser extension installed, it will handle the rest automatically.\n' +
128
- ' If not, your browser will try to load localhost:1455 and fail — copy the full URL\n' +
129
- ' from your address bar and paste it below.\n');
130
- // Race: poll for connection (extension path) vs paste (no-extension path)
131
- let rl = null;
132
- const pastePromise = new Promise((resolve) => {
133
- rl = readline.createInterface({ input: process.stdin, output: process.stderr });
134
- rl.question('Callback URL (skip if using extension): ', (answer) => {
135
- rl?.close();
136
- rl = null;
137
- resolve(answer);
69
+ const verificationUri = start.verification_uri || VERIFICATION_FALLBACK;
70
+ // 2. Cancel the device session if the user aborts (Ctrl-C).
71
+ let done = false;
72
+ const onSigint = () => {
73
+ if (done)
74
+ return;
75
+ done = true;
76
+ emit('\nAborted. Cancelling device authorization…');
77
+ Promise.race([
78
+ client.post('/v1/oauth/openai/device/cancel', { requireJwt: true, body: { device_auth_id: start.device_auth_id } }).catch(() => { }),
79
+ sleep(2000),
80
+ ]).finally(() => process.exit(130));
81
+ };
82
+ process.once('SIGINT', onSigint);
83
+ // 3. Show the code + URL and the one-time enable-setting guidance.
84
+ emit('');
85
+ emit('To connect ChatGPT:');
86
+ emit(` 1. Open this URL in any browser (phone, laptop — anywhere):`);
87
+ emit(` ${verificationUri}`);
88
+ emit(` 2. Enter this code: ${start.user_code}`);
89
+ emit(` 3. Sign in to ChatGPT and click Continue.`);
90
+ emit('');
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)`);
93
+ emit(' 2. Turn on "Enable device code authorization for Codex"');
94
+ emit(' 3. Then enter the code above');
95
+ emit('');
96
+ emit('Waiting for approval (the code expires in 15 minutes; press Ctrl-C to cancel)…');
97
+ // 4. Poll until completed / expired / timed out.
98
+ const startedAt = Date.now();
99
+ const expiresAt = Date.parse(start.expires_at); // NaN if unparseable
100
+ let intervalMs = Math.max(1, start.interval || 5) * 1000;
101
+ for (;;) {
102
+ const now = Date.now();
103
+ if (now - startedAt > HARD_STOP_MS || (!Number.isNaN(expiresAt) && now >= expiresAt)) {
104
+ process.removeListener('SIGINT', onSigint);
105
+ this.error('The code expired before it was approved. Run `faces auth:connect openai` again to get a new code.');
106
+ }
107
+ await sleep(intervalMs);
108
+ let poll;
109
+ try {
110
+ const raw = await client.post('/v1/oauth/openai/device/poll', {
111
+ requireJwt: true,
112
+ body: { device_auth_id: start.device_auth_id },
138
113
  });
139
- });
140
- const result = await Promise.race([
141
- pollForConnection(client, args.provider, 300_000),
142
- pastePromise,
143
- ]);
144
- // Clean up readline + stdin if polling won (keeps Node from hanging)
145
- if (rl) {
146
- rl.close();
147
- rl = null;
114
+ poll = unwrap(raw, 'status');
148
115
  }
149
- process.stdin.destroy();
150
- if (result === 'connected') {
151
- const output = { provider: args.provider, connected: true };
152
- if (!this.jsonEnabled())
153
- this.log(`\n✅ ${args.provider} connected. Your connect plan requests will route through your subscription.`);
154
- return output;
116
+ catch (err) {
117
+ // Transient upstream errors: keep polling. Everything else is fatal.
118
+ if (err instanceof FacesAPIError && (err.statusCode === 502 || err.statusCode === 503)) {
119
+ process.stderr.write('.');
120
+ continue;
121
+ }
122
+ process.removeListener('SIGINT', onSigint);
123
+ if (err instanceof FacesAPIError)
124
+ this.error(`Polling failed (${err.statusCode}): ${err.message}`);
125
+ throw err;
155
126
  }
156
- // User pasted a URL — exchange it ourselves
157
- const raw = result;
158
- if (!raw.trim())
159
- this.error('No callback URL provided and connection was not detected.');
160
- let parsed;
161
- try {
162
- const normalized = raw.trim().startsWith('http') ? raw.trim() : `http://localhost:1455${raw.trim()}`;
163
- parsed = new URL(normalized);
127
+ if (poll.status === 'completed') {
128
+ done = true;
129
+ process.removeListener('SIGINT', onSigint);
130
+ const email = poll.account_email ?? null;
131
+ const plan = poll.chatgpt_plan_type ?? null;
132
+ const output = { provider: 'openai', connected: true, account_email: email, chatgpt_plan_type: plan };
133
+ if (!this.jsonEnabled()) {
134
+ const who = email ? ` as ${email}` : '';
135
+ const tier = plan ? ` (${planLabel(plan)})` : '';
136
+ this.log(`\n✅ ChatGPT connected${who}${tier}.`);
137
+ if (!plan || plan === 'free') {
138
+ this.log('⚠️ Your ChatGPT plan may not include API access. Connecting requires a paid plan (Plus/Pro/Team, etc.).');
139
+ }
140
+ else {
141
+ this.log('Requests to supported OpenAI models will now route through your ChatGPT subscription.');
142
+ }
143
+ }
144
+ return output;
164
145
  }
165
- catch {
166
- this.error('Could not parse the URL you pasted. Make sure to copy the full address bar URL.');
146
+ if (poll.status === 'expired') {
147
+ process.removeListener('SIGINT', onSigint);
148
+ this.error('The code expired or was not recognized. Run `faces auth:connect openai` again to get a new code.');
167
149
  }
168
- code = parsed.searchParams.get('code') ?? '';
169
- state = parsed.searchParams.get('state') ?? '';
170
- if (!code || !state)
171
- this.error('The pasted URL is missing code or state parameters. Did you copy the full URL?');
150
+ // pending honor server-supplied retry_after, else fall back to interval.
151
+ intervalMs = (poll.retry_after && poll.retry_after > 0 ? poll.retry_after : (start.interval || 5)) * 1000;
152
+ process.stderr.write('.');
172
153
  }
173
- else {
174
- // Automatic mode: local callback server + open browser
175
- const callbackPromise = waitForCallback();
176
- openBrowser(authorizeData.authorize_url);
177
- this.log(`\nBrowser opened. Approve access on the ${args.provider} page.\n` +
178
- `If the browser didn't open, paste this URL:\n ${authorizeData.authorize_url}\n`);
154
+ }
155
+ // Start the device flow, retrying transient upstream (502/503) errors.
156
+ async startDevice(client) {
157
+ let attempts502 = 0;
158
+ let attempts503 = 0;
159
+ for (;;) {
179
160
  try {
180
- ;
181
- ({ code, state } = await callbackPromise);
161
+ const raw = await client.post('/v1/oauth/openai/device/start', { requireJwt: true });
162
+ return unwrap(raw, 'device_auth_id');
182
163
  }
183
164
  catch (err) {
184
- this.error(err instanceof Error ? err.message : String(err));
185
- }
186
- }
187
- // 3. Exchange code for tokens (stored encrypted in backend DB)
188
- this.log('Completing authorization...');
189
- try {
190
- const raw = (await client.post('/v1/oauth/openai/exchange', {
191
- requireJwt: true,
192
- body: { code, state },
193
- }));
194
- // Unwrap StandardResponse envelope if present
195
- const inner = (raw.data ?? raw);
196
- if (!inner.connected)
197
- this.error('Exchange succeeded but the server did not confirm the connection.');
198
- }
199
- catch (err) {
200
- if (err instanceof FacesAPIError) {
201
- // If state was already consumed (browser extension beat us to it),
202
- // check whether the connection was stored anyway and succeed if so.
203
- if (err.statusCode === 400) {
204
- try {
205
- const rows = (await client.get('/v1/oauth', { requireJwt: true }));
206
- if (rows.some((r) => r.provider === args.provider)) {
207
- const output = { provider: args.provider, connected: true };
208
- if (!this.jsonEnabled())
209
- this.log(`\n✅ ${args.provider} connected. Your connect plan requests will route through your subscription.`);
210
- return output;
211
- }
212
- }
213
- catch { /* ignore */ }
165
+ if (err instanceof FacesAPIError && err.statusCode === 503 && attempts503 < 4) {
166
+ attempts503++;
167
+ emit('OpenAI is temporarily unavailable, retrying…');
168
+ await sleep(2000);
169
+ continue;
170
+ }
171
+ if (err instanceof FacesAPIError && err.statusCode === 502 && attempts502 < 2) {
172
+ attempts502++;
173
+ emit('OpenAI returned an error, retrying…');
174
+ await sleep(2000);
175
+ continue;
214
176
  }
215
- this.error(`Token exchange failed (${err.statusCode}): ${err.message}`);
177
+ throw err;
216
178
  }
217
- throw err;
218
179
  }
219
- const output = { provider: args.provider, connected: true };
220
- if (!this.jsonEnabled())
221
- this.log(`\n✅ ${args.provider} connected. Your connect plan requests will route through your subscription.`);
222
- return output;
223
180
  }
224
- }
225
- async function pollForConnection(client, provider, timeoutMs) {
226
- const deadline = Date.now() + timeoutMs;
227
- while (Date.now() < deadline) {
228
- await new Promise((r) => setTimeout(r, 2000));
229
- try {
230
- const rows = (await client.get('/v1/oauth', { requireJwt: true }));
231
- if (rows.some((r) => r.provider === provider))
232
- return 'connected';
233
- }
234
- catch {
235
- // ignore transient errors, keep polling
181
+ handleStartError(err) {
182
+ if (err instanceof FacesAPIError) {
183
+ if (err.statusCode === 401) {
184
+ this.error('Your Faces session is missing or expired. Run `faces auth:login` and try again.');
185
+ }
186
+ if (err.statusCode === 403 || err.errorCode === 'connect_plan_required') {
187
+ this.error('Connecting ChatGPT requires the Subscription Connect plan.\n' +
188
+ 'Upgrade with: faces billing checkout');
189
+ }
190
+ if (err.statusCode === 422 || err.errorCode === 'device_auth_not_enabled') {
191
+ const d = err.details ?? {};
192
+ const url = d.settings_url || SETTINGS_URL;
193
+ const steps = Array.isArray(d.settings_steps) && d.settings_steps.length > 0
194
+ ? d.settings_steps
195
+ : [
196
+ `Open ${url} (the settings panel can take a few seconds to appear — give it a moment)`,
197
+ 'Turn on "Enable device code authorization for Codex"',
198
+ 'Then run `faces auth:connect openai` again',
199
+ ];
200
+ this.error('Device-code authorization is not enabled on your ChatGPT account yet:\n' +
201
+ steps.map((s, i) => ` ${i + 1}. ${s}`).join('\n'));
202
+ }
203
+ this.error(`Failed to start device authorization (${err.statusCode}): ${err.message}`);
236
204
  }
205
+ throw err;
237
206
  }
238
- throw new Error('Timed out waiting for connection (5 minutes).');
239
207
  }
@@ -19,7 +19,19 @@ export default class AuthConnections extends BaseCommand {
19
19
  }
20
20
  else {
21
21
  for (const row of rows) {
22
- this.log(`${row.provider} connected_at=${row.connected_at}${row.scope ? ` scope=${row.scope}` : ''}`);
22
+ if (row.provider === 'openai') {
23
+ const plan = row.chatgpt_plan_type;
24
+ const label = plan ? plan.charAt(0).toUpperCase() + plan.slice(1) : null;
25
+ const who = row.account_email ? ` as ${row.account_email}` : '';
26
+ const tier = label ? ` (${label})` : '';
27
+ this.log(`openai connected${who}${tier} connected_at=${row.connected_at}`);
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.).');
30
+ }
31
+ }
32
+ else {
33
+ this.log(`${row.provider} connected_at=${row.connected_at}${row.scope ? ` scope=${row.scope}` : ''}`);
34
+ }
23
35
  }
24
36
  }
25
37
  }
@@ -6,6 +6,7 @@ import { FacesAPIError } from '../../client.js';
6
6
  import { loadConfig } from '../../config.js';
7
7
  import { CatalogService, CATALOG_DIR } from '../../catalog.js';
8
8
  import { TeamCatalogService } from '../../team-catalog.js';
9
+ import { resolveEndpoint, MESSAGES_ENDPOINT, RESPONSES_ENDPOINT } from '../../routing.js';
9
10
  export default class CatalogDoctor extends BaseCommand {
10
11
  static description = 'Diagnose and repair the local face and team catalog';
11
12
  static flags = {
@@ -203,6 +204,8 @@ export default class CatalogDoctor extends BaseCommand {
203
204
  const cfg = loadConfig();
204
205
  const catalogModel = cfg.catalog_model ?? 'gpt-5-nano';
205
206
  this.log(`Generating descriptions for ${noDescription.length} face(s) via ${catalogModel}...`);
207
+ // Route the description LLM the same way chat does — off the catalog.
208
+ const { endpoint: descEndpoint } = await resolveEndpoint(client, `_@${catalogModel}`);
206
209
  let generated = 0;
207
210
  for (const alias of noDescription) {
208
211
  const remote = remoteByAlias.get(alias);
@@ -211,14 +214,26 @@ export default class CatalogDoctor extends BaseCommand {
211
214
  try {
212
215
  const prompt = 'Describe yourself in one paragraph. Who are you, what do you care about, and what kind of questions are you best suited to answer?';
213
216
  const faceModel = `${alias}@${catalogModel}`;
214
- const isAnthropic = catalogModel.startsWith('claude');
215
217
  let text;
216
- if (isAnthropic) {
218
+ if (descEndpoint === MESSAGES_ENDPOINT) {
217
219
  const resp = await client.post('/v1/messages', {
218
220
  body: { model: faceModel, messages: [{ role: 'user', content: prompt }], max_tokens: 256 },
219
221
  });
220
222
  text = resp.content?.find((b) => b.type === 'text')?.text;
221
223
  }
224
+ else if (descEndpoint === RESPONSES_ENDPOINT) {
225
+ const resp = await client.post('/v1/responses', {
226
+ body: { model: faceModel, input: [{ role: 'user', content: prompt }], max_output_tokens: 256 },
227
+ });
228
+ let out = '';
229
+ for (const item of resp.output ?? []) {
230
+ for (const block of item.content ?? []) {
231
+ if (block.type === 'output_text')
232
+ out += block.text ?? '';
233
+ }
234
+ }
235
+ text = out || undefined;
236
+ }
222
237
  else {
223
238
  const resp = await client.post('/v1/chat/completions', {
224
239
  body: { model: faceModel, messages: [{ role: 'user', content: prompt }], max_tokens: 256 },
@@ -19,8 +19,10 @@ export default class ChatChat extends BaseCommand {
19
19
  face_username: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
20
20
  };
21
21
  run(): Promise<unknown>;
22
+ private oauthHint;
23
+ private metaFor;
24
+ private streamDeltas;
22
25
  private runChatCompletions;
23
26
  private runMessages;
24
27
  private runResponses;
25
- private extractText;
26
28
  }