faces-cli 1.8.1 → 1.8.2

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/utils.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import { FacesAPIError } from './client.js';
2
3
  /** Max length of a face's profile_addendum (per-face system prompt). Mirrors the backend. */
3
4
  export const PROFILE_ADDENDUM_MAX_CHARS = 100_000;
4
5
  /**
@@ -95,9 +96,10 @@ export function isMaxTokensRenameError(message) {
95
96
  * valid ones better than a stale local list could.
96
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
- 'blog post, legal document, thread reply, conversation (covers speech: transcripts, interviews, ' +
99
- 'calls). Common synonyms fold automatically. Omit it if you do not know: a wrong declaration is ' +
100
- 'worse than none, because a declaration is trusted and an absence is not.';
99
+ 'blog post, legal document, thread reply, conversation (dialogue: transcripts, interviews, calls), ' +
100
+ 'lecture (sustained speech nobody interrupts: talks, sermons, keynotes). Common synonyms fold ' +
101
+ 'automatically. Omit it if you do not know: a wrong declaration is worse than none, because a ' +
102
+ 'declaration is trusted and an absence is not.';
101
103
  /**
102
104
  * Warn when the server dropped request parameters before forwarding.
103
105
  *
@@ -128,3 +130,67 @@ export const CHAT_PARAM_FLAGS = {
128
130
  max_completion_tokens: '--max-tokens',
129
131
  temperature: '--temperature',
130
132
  };
133
+ /**
134
+ * Look up a face by `alias` or `owner:alias`.
135
+ *
136
+ * `GET /v1/faces/{alias}` only serves faces the account owns. It answers 404
137
+ * for a qualified name it does not parse and 403 for someone else's bare
138
+ * alias, so the two ways of naming the same reachable face fail differently
139
+ * and neither says what is actually true. The listing does serve published and
140
+ * shared faces, so it is used as the fallback and the caller gets one answer:
141
+ * here is the face, and you do not own it.
142
+ *
143
+ * Throws the original error when the face genuinely cannot be reached.
144
+ */
145
+ export async function resolveFace(client, spec, include = 'profile') {
146
+ const at = spec.indexOf(':');
147
+ const owner = at === -1 ? undefined : spec.slice(0, at);
148
+ const alias = at === -1 ? spec : spec.slice(at + 1);
149
+ const q = `include=${encodeURIComponent(include)}`;
150
+ // A bare name is yours until the server says otherwise.
151
+ if (!owner) {
152
+ try {
153
+ const face = (await client.get(`/v1/faces/${encodeURIComponent(alias)}?${q}`));
154
+ return { face, owned: true, handle: alias };
155
+ }
156
+ catch (err) {
157
+ // 403 means it exists and is not yours. Find who owns it so the caller
158
+ // can name it the way that works, rather than reporting a typo.
159
+ if (!(err instanceof FacesAPIError) || err.statusCode !== 403)
160
+ throw err;
161
+ const found = await findForeignFace(client, alias, undefined, q);
162
+ if (found)
163
+ return found;
164
+ throw err;
165
+ }
166
+ }
167
+ const found = await findForeignFace(client, alias, owner, q);
168
+ if (found)
169
+ return found;
170
+ throw new FacesAPIError(404, `Face ${spec} not found`);
171
+ }
172
+ /** Search the listing, which unlike the single-face route serves other people's faces. */
173
+ async function findForeignFace(client, alias, owner, q) {
174
+ const parts = [q, 'include_published=true', 'include_shared=true'];
175
+ if (owner)
176
+ parts.push(`from_users=${encodeURIComponent(owner)}`);
177
+ let rows;
178
+ try {
179
+ const data = await client.get(`/v1/faces?${parts.join('&')}`);
180
+ rows = (data.data ??
181
+ data);
182
+ }
183
+ catch {
184
+ return undefined;
185
+ }
186
+ const hit = rows.find((f) => f.alias === alias && (owner === undefined || f.owned_by === owner));
187
+ if (!hit)
188
+ return undefined;
189
+ const ownedBy = hit.owned_by;
190
+ return {
191
+ face: hit,
192
+ owned: false,
193
+ handle: ownedBy ? `${ownedBy}:${alias}` : alias,
194
+ owner: ownedBy,
195
+ };
196
+ }