faces-cli 1.7.10 → 1.7.14

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.
Files changed (37) hide show
  1. package/dist/catalog.d.ts +2 -12
  2. package/dist/catalog.js +2 -3
  3. package/dist/commands/chat/chat.d.ts +1 -0
  4. package/dist/commands/chat/chat.js +36 -3
  5. package/dist/commands/chat/messages.d.ts +1 -0
  6. package/dist/commands/chat/messages.js +5 -1
  7. package/dist/commands/chat/responses.d.ts +1 -0
  8. package/dist/commands/chat/responses.js +5 -1
  9. package/dist/commands/chat/thread.d.ts +1 -0
  10. package/dist/commands/chat/thread.js +31 -3
  11. package/dist/commands/compile/doc/create.d.ts +1 -0
  12. package/dist/commands/compile/doc/create.js +4 -0
  13. package/dist/commands/compile/doc/edit.d.ts +1 -0
  14. package/dist/commands/compile/doc/edit.js +4 -0
  15. package/dist/commands/compile/doc/index.d.ts +1 -0
  16. package/dist/commands/compile/doc/index.js +5 -1
  17. package/dist/commands/compile/doc/make.js +1 -1
  18. package/dist/commands/compile/thread/make.js +1 -1
  19. package/dist/commands/compile/upload.d.ts +1 -0
  20. package/dist/commands/compile/upload.js +9 -0
  21. package/dist/commands/face/diff.js +8 -6
  22. package/dist/commands/face/list.d.ts +1 -0
  23. package/dist/commands/face/list.js +16 -3
  24. package/dist/commands/face/neighbors.js +20 -4
  25. package/dist/commands/face/publish.d.ts +15 -0
  26. package/dist/commands/face/publish.js +41 -0
  27. package/dist/commands/face/share.d.ts +21 -0
  28. package/dist/commands/face/share.js +106 -0
  29. package/dist/commands/face/unpublish.d.ts +13 -0
  30. package/dist/commands/face/unpublish.js +30 -0
  31. package/dist/commands/face/unshare.d.ts +17 -0
  32. package/dist/commands/face/unshare.js +67 -0
  33. package/dist/poll.d.ts +1 -6
  34. package/dist/utils.d.ts +34 -0
  35. package/dist/utils.js +57 -0
  36. package/oclif.manifest.json +1039 -659
  37. package/package.json +1 -1
package/dist/catalog.d.ts CHANGED
@@ -8,12 +8,7 @@ export interface FaceFrontmatter {
8
8
  tags?: string[];
9
9
  default_model?: string;
10
10
  formula?: string | null;
11
- component_counts?: {
12
- alpha: number;
13
- beta: number;
14
- delta: number;
15
- epsilon: number;
16
- };
11
+ component_counts?: number[];
17
12
  compiled_tokens?: number;
18
13
  profile_token_count?: number;
19
14
  [key: string]: unknown;
@@ -29,12 +24,7 @@ export interface FaceDataInput {
29
24
  tags?: string[] | null;
30
25
  default_model?: string | null;
31
26
  formula?: string | null;
32
- component_counts?: {
33
- alpha: number;
34
- beta: number;
35
- delta: number;
36
- epsilon: number;
37
- } | null;
27
+ component_counts?: number[] | null;
38
28
  profile_token_count?: number | null;
39
29
  total_tokens_saved?: number | null;
40
30
  }
package/dist/catalog.js CHANGED
@@ -29,9 +29,8 @@ function serializeFrontmatter(fm) {
29
29
  if (fm.tags && fm.tags.length > 0) {
30
30
  lines.push(`tags: [${fm.tags.join(', ')}]`);
31
31
  }
32
- if (fm.component_counts) {
33
- const cc = fm.component_counts;
34
- lines.push(`component_counts: ${cc.alpha + cc.beta + cc.delta + cc.epsilon}`);
32
+ if (fm.component_counts?.length) {
33
+ lines.push(`component_counts: ${fm.component_counts.reduce((a, b) => a + b, 0)}`);
35
34
  }
36
35
  if (fm.compiled_tokens) {
37
36
  lines.push(`compiled_tokens: ${fm.compiled_tokens}`);
@@ -12,6 +12,7 @@ export default class ChatChat extends BaseCommand {
12
12
  temperature: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
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
+ medium: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
16
  'oauth-only': import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
17
  'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
18
  token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -2,7 +2,7 @@ import { Args, Flags } from '@oclif/core';
2
2
  import fs from 'node:fs';
3
3
  import { BaseCommand } from '../../base.js';
4
4
  import { FacesAPIError } from '../../client.js';
5
- import { extractStreamDelta } from '../../utils.js';
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
8
  static description = 'Chat via a face. Auto-routes to the correct API endpoint based on the model catalog.';
@@ -22,6 +22,7 @@ export default class ChatChat extends BaseCommand {
22
22
  temperature: Flags.string({ description: 'Temperature (0.0-2.0)' }),
23
23
  file: Flags.string({ description: 'Read message from file' }),
24
24
  responses: Flags.boolean({ description: 'Force the OpenAI Responses API endpoint (escape hatch; routing is automatic otherwise)', default: false }),
25
+ medium: Flags.string({ description: MEDIUM_FLAG_DESCRIPTION }),
25
26
  'oauth-only': Flags.boolean({ description: 'Prevent fallback to paid system key (use OAuth only)', default: false }),
26
27
  };
27
28
  static args = {
@@ -140,13 +141,32 @@ export default class ChatChat extends BaseCommand {
140
141
  payload.max_tokens = flags['max-tokens'];
141
142
  if (flags.temperature !== undefined)
142
143
  payload.temperature = Number.parseFloat(flags.temperature);
144
+ if (flags.medium)
145
+ payload.medium = flags.medium;
143
146
  if (flags['oauth-only'])
144
147
  payload.oauth_only = true;
145
148
  if (flags.stream) {
146
149
  await this.streamDeltas(client, '/v1/chat/completions', payload);
147
150
  return {};
148
151
  }
149
- const { data, headers } = await client.postWithHeaders('/v1/chat/completions', { body: payload });
152
+ let data;
153
+ let headers;
154
+ try {
155
+ ;
156
+ ({ data, headers } = await client.postWithHeaders('/v1/chat/completions', { body: payload }));
157
+ }
158
+ catch (err) {
159
+ if (err instanceof FacesAPIError && isMaxTokensRenameError(err.message) && payload.max_tokens !== undefined) {
160
+ payload.max_completion_tokens = payload.max_tokens;
161
+ delete payload.max_tokens;
162
+ ({ data, headers } = await client.postWithHeaders('/v1/chat/completions', { body: payload }));
163
+ }
164
+ else {
165
+ throw err;
166
+ }
167
+ }
168
+ if (!this.jsonEnabled())
169
+ warnDroppedParams(headers, CHAT_PARAM_FLAGS);
150
170
  const body = data;
151
171
  const choices = body.choices;
152
172
  const text = String(choices?.[0]?.message?.content ?? '');
@@ -167,6 +187,8 @@ export default class ChatChat extends BaseCommand {
167
187
  };
168
188
  if (flags.system)
169
189
  payload.system = flags.system;
190
+ if (flags.medium)
191
+ payload.medium = flags.medium;
170
192
  if (flags['oauth-only'])
171
193
  payload.oauth_only = true;
172
194
  if (flags.stream) {
@@ -174,6 +196,8 @@ export default class ChatChat extends BaseCommand {
174
196
  return {};
175
197
  }
176
198
  const { data, headers } = await client.postWithHeaders('/v1/messages', { body: payload });
199
+ if (!this.jsonEnabled())
200
+ warnDroppedParams(headers, CHAT_PARAM_FLAGS);
177
201
  const body = data;
178
202
  if (this.jsonEnabled()) {
179
203
  body._meta = this.metaFor(headers, '/v1/messages');
@@ -199,10 +223,17 @@ export default class ChatChat extends BaseCommand {
199
223
  // `temperature` is accepted per-model (gpt-5.1/5.2/5.4 yes; gpt-5.5/5.6-*/o3
200
224
  // no) with no capability flag to query, so we always send what the user asked
201
225
  // for — the backend strips the param and retries when a model 400s on it.
202
- if (flags['max-tokens'])
226
+ if (flags['max-tokens']) {
227
+ if (flags['max-tokens'] < MIN_MAX_OUTPUT_TOKENS) {
228
+ this.error(`--max-tokens must be at least ${MIN_MAX_OUTPUT_TOKENS} for this model (got ${flags['max-tokens']}). ` +
229
+ 'The Responses API rejects a smaller output cap.');
230
+ }
203
231
  payload.max_output_tokens = flags['max-tokens'];
232
+ }
204
233
  if (flags.temperature !== undefined)
205
234
  payload.temperature = Number.parseFloat(flags.temperature);
235
+ if (flags.medium)
236
+ payload.medium = flags.medium;
206
237
  if (flags['oauth-only'])
207
238
  payload.oauth_only = true;
208
239
  if (flags.stream) {
@@ -210,6 +241,8 @@ export default class ChatChat extends BaseCommand {
210
241
  return {};
211
242
  }
212
243
  const { data, headers } = await client.postWithHeaders('/v1/responses', { body: payload });
244
+ if (!this.jsonEnabled())
245
+ warnDroppedParams(headers, CHAT_PARAM_FLAGS);
213
246
  const body = data;
214
247
  if (this.jsonEnabled()) {
215
248
  body._meta = this.metaFor(headers, '/v1/responses');
@@ -6,6 +6,7 @@ export default class ChatMessages extends BaseCommand {
6
6
  system: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
7
  stream: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
8
  'max-tokens': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
9
+ medium: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
10
  'oauth-only': import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
11
  'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
12
  token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -1,5 +1,6 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import { BaseCommand } from '../../base.js';
3
+ import { MEDIUM_FLAG_DESCRIPTION } from '../../utils.js';
3
4
  import { FacesAPIError } from '../../client.js';
4
5
  export default class ChatMessages extends BaseCommand {
5
6
  static description = 'Anthropic Messages API proxy via a face (alias: chat:chat auto-routes Anthropic models here)';
@@ -9,10 +10,11 @@ export default class ChatMessages extends BaseCommand {
9
10
  system: Flags.string({ description: 'System prompt' }),
10
11
  stream: Flags.boolean({ description: 'Stream the response', default: false }),
11
12
  'max-tokens': Flags.integer({ description: 'Max tokens', default: 1024 }),
13
+ medium: Flags.string({ description: MEDIUM_FLAG_DESCRIPTION }),
12
14
  'oauth-only': Flags.boolean({ description: 'Prevent fallback to paid system key (use OAuth only)', default: false }),
13
15
  };
14
16
  static args = {
15
- face_model: Args.string({ description: 'Face model (e.g. myface, myface@claude-sonnet-4-6, or head:judge@claude-sonnet-4-6)', required: true }),
17
+ face_model: Args.string({ description: 'Face model (e.g. myface, myface@claude-sonnet-4-6, or head:socrates@claude-sonnet-4-6)', required: true }),
16
18
  };
17
19
  async run() {
18
20
  const { args, flags } = await this.parse(ChatMessages);
@@ -26,6 +28,8 @@ export default class ChatMessages extends BaseCommand {
26
28
  };
27
29
  if (flags.system)
28
30
  payload.system = flags.system;
31
+ if (flags.medium)
32
+ payload.medium = flags.medium;
29
33
  if (flags['oauth-only'])
30
34
  payload.oauth_only = true;
31
35
  try {
@@ -5,6 +5,7 @@ export default class ChatResponses extends BaseCommand {
5
5
  message: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
6
6
  instructions: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
7
  stream: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ medium: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
9
  'oauth-only': import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
10
  'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
11
  token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -1,5 +1,6 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import { BaseCommand } from '../../base.js';
3
+ import { MEDIUM_FLAG_DESCRIPTION } from '../../utils.js';
3
4
  import { FacesAPIError } from '../../client.js';
4
5
  export default class ChatResponses extends BaseCommand {
5
6
  static description = 'OpenAI Responses API proxy via a face (alias: chat:chat --responses)';
@@ -8,10 +9,11 @@ export default class ChatResponses extends BaseCommand {
8
9
  message: Flags.string({ char: 'm', description: 'User input message', required: true }),
9
10
  instructions: Flags.string({ description: 'System instructions' }),
10
11
  stream: Flags.boolean({ description: 'Stream the response', default: false }),
12
+ medium: Flags.string({ description: MEDIUM_FLAG_DESCRIPTION }),
11
13
  'oauth-only': Flags.boolean({ description: 'Prevent fallback to paid system key (use OAuth only)', default: false }),
12
14
  };
13
15
  static args = {
14
- face_model: Args.string({ description: 'Face model (e.g. myface, myface@gpt-4o, or head:logician@gpt-5.4)', required: true }),
16
+ face_model: Args.string({ description: 'Face model (e.g. myface, myface@gpt-4o, or head:socrates@gpt-5.4)', required: true }),
15
17
  };
16
18
  async run() {
17
19
  const { args, flags } = await this.parse(ChatResponses);
@@ -23,6 +25,8 @@ export default class ChatResponses extends BaseCommand {
23
25
  };
24
26
  if (flags.instructions)
25
27
  payload.instructions = flags.instructions;
28
+ if (flags.medium)
29
+ payload.medium = flags.medium;
26
30
  if (flags['oauth-only'])
27
31
  payload.oauth_only = true;
28
32
  try {
@@ -11,6 +11,7 @@ export default class ChatThread extends BaseCommand {
11
11
  'max-tokens': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
12
  temperature: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
13
  stream: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ medium: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
15
  'oauth-only': import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
16
  list: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
17
  show: import("@oclif/core/interfaces").BooleanFlag<boolean>;
@@ -5,7 +5,7 @@ import os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import { BaseCommand } from '../../base.js';
7
7
  import { FacesAPIError } from '../../client.js';
8
- import { extractStreamDelta } from '../../utils.js';
8
+ import { extractStreamDelta, isMaxTokensRenameError, MIN_MAX_OUTPUT_TOKENS, MEDIUM_FLAG_DESCRIPTION } from '../../utils.js';
9
9
  import { resolveEndpoint, MESSAGES_ENDPOINT, RESPONSES_ENDPOINT, CHAT_COMPLETIONS_ENDPOINT } from '../../routing.js';
10
10
  const THREADS_DIR = path.join(os.homedir(), '.faces', 'threads');
11
11
  /**
@@ -91,6 +91,7 @@ export default class ChatThread extends BaseCommand {
91
91
  'max-tokens': Flags.integer({ description: 'Max tokens' }),
92
92
  temperature: Flags.string({ description: 'Temperature (0.0-2.0, OpenAI/Chat Completions only)' }),
93
93
  stream: Flags.boolean({ description: 'Stream the response', default: false }),
94
+ medium: Flags.string({ description: MEDIUM_FLAG_DESCRIPTION }),
94
95
  'oauth-only': Flags.boolean({ description: 'Prevent fallback to paid system key', default: false }),
95
96
  list: Flags.boolean({ description: 'List saved threads', default: false }),
96
97
  show: Flags.boolean({ description: 'Show the thread transcript (requires --id)', default: false }),
@@ -279,12 +280,30 @@ export default class ChatThread extends BaseCommand {
279
280
  payload.max_tokens = flags['max-tokens'];
280
281
  if (flags.temperature !== undefined)
281
282
  payload.temperature = Number.parseFloat(flags.temperature);
283
+ if (flags.medium)
284
+ payload.medium = flags.medium;
282
285
  if (flags['oauth-only'])
283
286
  payload.oauth_only = true;
284
287
  if (flags.stream) {
285
288
  return this.streamAndAccumulate(client, CHAT_COMPLETIONS_ENDPOINT, payload);
286
289
  }
287
- const { data } = await client.postWithHeaders(CHAT_COMPLETIONS_ENDPOINT, { body: payload });
290
+ let data;
291
+ try {
292
+ ;
293
+ ({ data } = await client.postWithHeaders(CHAT_COMPLETIONS_ENDPOINT, { body: payload }));
294
+ }
295
+ catch (err) {
296
+ // Newer OpenAI models require max_completion_tokens; retry on the provider's
297
+ // own rejection rather than keeping a model list that goes stale.
298
+ if (err instanceof FacesAPIError && isMaxTokensRenameError(err.message) && payload.max_tokens !== undefined) {
299
+ payload.max_completion_tokens = payload.max_tokens;
300
+ delete payload.max_tokens;
301
+ ({ data } = await client.postWithHeaders(CHAT_COMPLETIONS_ENDPOINT, { body: payload }));
302
+ }
303
+ else {
304
+ throw err;
305
+ }
306
+ }
288
307
  return extractAssistantText(data, CHAT_COMPLETIONS_ENDPOINT);
289
308
  }
290
309
  async sendResponses(client, thread, flags) {
@@ -294,8 +313,15 @@ export default class ChatThread extends BaseCommand {
294
313
  const payload = { model: thread.model, input, stream: flags.stream };
295
314
  if (thread.system)
296
315
  payload.instructions = thread.system;
297
- if (flags['max-tokens'])
316
+ if (flags['max-tokens']) {
317
+ if (flags['max-tokens'] < MIN_MAX_OUTPUT_TOKENS) {
318
+ throw new Error(`--max-tokens must be at least ${MIN_MAX_OUTPUT_TOKENS} for this model (got ${flags['max-tokens']}). ` +
319
+ 'The Responses API rejects a smaller output cap.');
320
+ }
298
321
  payload.max_output_tokens = flags['max-tokens'];
322
+ }
323
+ if (flags.medium)
324
+ payload.medium = flags.medium;
299
325
  if (flags['oauth-only'])
300
326
  payload.oauth_only = true;
301
327
  if (flags.stream) {
@@ -314,6 +340,8 @@ export default class ChatThread extends BaseCommand {
314
340
  };
315
341
  if (thread.system)
316
342
  payload.system = thread.system;
343
+ if (flags.medium)
344
+ payload.medium = flags.medium;
317
345
  if (flags['oauth-only'])
318
346
  payload.oauth_only = true;
319
347
  if (flags.stream) {
@@ -5,6 +5,7 @@ export default class CompileDocCreate extends BaseCommand {
5
5
  label: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
6
6
  content: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
7
  file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ medium: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
9
  perspective: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
10
  'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
11
  token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -1,6 +1,7 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import fs from 'node:fs';
3
3
  import { BaseCommand } from '../../../base.js';
4
+ import { MEDIUM_FLAG_DESCRIPTION } from '../../../utils.js';
4
5
  import { FacesAPIError } from '../../../client.js';
5
6
  export default class CompileDocCreate extends BaseCommand {
6
7
  static description = 'Submit a document to a face';
@@ -9,6 +10,7 @@ export default class CompileDocCreate extends BaseCommand {
9
10
  label: Flags.string({ description: 'Document label/title' }),
10
11
  content: Flags.string({ description: 'Inline text content' }),
11
12
  file: Flags.string({ description: 'Read content from file' }),
13
+ medium: Flags.string({ description: MEDIUM_FLAG_DESCRIPTION }),
12
14
  perspective: Flags.string({ description: 'Perspective (first-person or third-person)', options: ['first-person', 'third-person'], default: 'first-person' }),
13
15
  };
14
16
  static args = {
@@ -28,6 +30,8 @@ export default class CompileDocCreate extends BaseCommand {
28
30
  const payload = { alias: args.face_id, content };
29
31
  if (flags.label)
30
32
  payload.label = flags.label;
33
+ if (flags.medium)
34
+ payload.medium = flags.medium;
31
35
  if (flags.perspective)
32
36
  payload.perspective = flags.perspective;
33
37
  let data;
@@ -5,6 +5,7 @@ export default class CompileDocEdit extends BaseCommand {
5
5
  label: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
6
6
  content: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
7
  file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ medium: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
9
  perspective: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
10
  'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
11
  token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -1,6 +1,7 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import fs from 'node:fs';
3
3
  import { BaseCommand } from '../../../base.js';
4
+ import { MEDIUM_FLAG_DESCRIPTION } from '../../../utils.js';
4
5
  import { FacesAPIError } from '../../../client.js';
5
6
  export default class CompileDocEdit extends BaseCommand {
6
7
  static description = 'Edit a document (label, content, or perspective)';
@@ -9,6 +10,7 @@ export default class CompileDocEdit extends BaseCommand {
9
10
  label: Flags.string({ description: 'New label/title' }),
10
11
  content: Flags.string({ description: 'New inline text content' }),
11
12
  file: Flags.string({ description: 'Read new content from file' }),
13
+ medium: Flags.string({ description: MEDIUM_FLAG_DESCRIPTION }),
12
14
  perspective: Flags.string({ description: 'Perspective (first-person or third-person)', options: ['first-person', 'third-person'] }),
13
15
  };
14
16
  static args = {
@@ -20,6 +22,8 @@ export default class CompileDocEdit extends BaseCommand {
20
22
  const payload = {};
21
23
  if (flags.label)
22
24
  payload.label = flags.label;
25
+ if (flags.medium)
26
+ payload.medium = flags.medium;
23
27
  if (flags.perspective)
24
28
  payload.perspective = flags.perspective;
25
29
  if (flags.file) {
@@ -6,6 +6,7 @@ export default class CompileDoc extends BaseCommand {
6
6
  label: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
7
  content: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
8
  file: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ medium: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
10
  perspective: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
11
  timeout: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
11
12
  'no-wait': import("@oclif/core/interfaces").BooleanFlag<boolean>;
@@ -2,6 +2,7 @@ import { Args, Flags } from '@oclif/core';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { BaseCommand } from '../../../base.js';
5
+ import { MEDIUM_FLAG_DESCRIPTION } from '../../../utils.js';
5
6
  import { FacesAPIError } from '../../../client.js';
6
7
  import { pollCompileProgress } from '../../../poll.js';
7
8
  export default class CompileDoc extends BaseCommand {
@@ -17,6 +18,7 @@ export default class CompileDoc extends BaseCommand {
17
18
  label: Flags.string({ description: 'Document label/title (single document only; with several files each is labelled by its filename)' }),
18
19
  content: Flags.string({ description: 'Inline text content', exclusive: ['file'] }),
19
20
  file: Flags.string({ description: 'Read content from file (repeatable — each file becomes its own document)', multiple: true }),
21
+ medium: Flags.string({ description: MEDIUM_FLAG_DESCRIPTION }),
20
22
  perspective: Flags.string({
21
23
  description: 'Perspective',
22
24
  options: ['first-person', 'third-person'],
@@ -71,6 +73,8 @@ export default class CompileDoc extends BaseCommand {
71
73
  payload.label = flags.label;
72
74
  else if (multi)
73
75
  payload.label = path.basename(source.name);
76
+ if (flags.medium)
77
+ payload.medium = flags.medium;
74
78
  if (flags.perspective)
75
79
  payload.perspective = flags.perspective;
76
80
  if (!json)
@@ -123,7 +127,7 @@ export default class CompileDoc extends BaseCommand {
123
127
  if (json)
124
128
  return;
125
129
  const c = p.current_counts;
126
- const counts = c ? `ε=${c.epsilon} β=${c.beta} δ=${c.delta} α=${c.alpha}` : '';
130
+ const counts = c?.length ? `${c.reduce((a, b) => a + b, 0)} components` : '';
127
131
  const phase = p.prepare_status === 'syncing' ? ' (syncing)' : '';
128
132
  process.stderr.write(` [${p.chunks_completed ?? '?'}/${p.chunks_total ?? '?'}] ${counts}${phase}\n`);
129
133
  },
@@ -45,7 +45,7 @@ export default class CompileDocMake extends BaseCommand {
45
45
  onProgress: (p) => {
46
46
  if (!json) {
47
47
  const c = p.current_counts;
48
- const counts = c ? `ε=${c.epsilon} β=${c.beta} δ=${c.delta} α=${c.alpha}` : '';
48
+ const counts = c?.length ? `${c.reduce((a, b) => a + b, 0)} components` : '';
49
49
  const phase = p.prepare_status === 'syncing' ? ' (syncing)' : '';
50
50
  process.stderr.write(` [${p.chunks_completed ?? '?'}/${p.chunks_total ?? '?'}] ${counts}${phase}\n`);
51
51
  }
@@ -46,7 +46,7 @@ export default class CompileThreadMake extends BaseCommand {
46
46
  onProgress: (p) => {
47
47
  if (!json) {
48
48
  const c = p.current_counts;
49
- const counts = c ? `ε=${c.epsilon} β=${c.beta} δ=${c.delta} α=${c.alpha}` : '';
49
+ const counts = c?.length ? `${c.reduce((a, b) => a + b, 0)} components` : '';
50
50
  const phase = p.prepare_status === 'syncing' ? ' (syncing)' : '';
51
51
  process.stderr.write(` [${p.chunks_completed ?? '?'}/${p.chunks_total ?? '?'}] ${counts}${phase}\n`);
52
52
  }
@@ -5,6 +5,7 @@ export default class CompileUpload extends BaseCommand {
5
5
  file: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
6
6
  kind: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
7
  perspective: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
+ medium: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
9
  'face-speaker': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
10
  'no-wait': import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
11
  'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -2,6 +2,7 @@ import { Args, Flags } from '@oclif/core';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { BaseCommand } from '../../base.js';
5
+ import { MEDIUM_FLAG_DESCRIPTION } from '../../utils.js';
5
6
  import { FacesAPIError } from '../../client.js';
6
7
  function sleep(ms) {
7
8
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -21,6 +22,7 @@ export default class CompileUpload extends BaseCommand {
21
22
  options: ['first-person', 'third-person'],
22
23
  default: 'third-person',
23
24
  }),
25
+ medium: Flags.string({ description: MEDIUM_FLAG_DESCRIPTION + ' Only valid with --kind document.' }),
24
26
  'face-speaker': Flags.string({
25
27
  description: 'Speaker name to map to the face (thread only). If omitted, auto-detected from face name.',
26
28
  }),
@@ -42,8 +44,15 @@ export default class CompileUpload extends BaseCommand {
42
44
  const fileBlob = new Blob([fs.readFileSync(flags.file)], { type: 'application/octet-stream' });
43
45
  const form = new FormData();
44
46
  form.append('file', fileBlob, filename);
47
+ // The API accepts medium for document uploads only — a thread carries a kind
48
+ // per message, derived from the file. Refuse here rather than on a 422.
49
+ if (flags.medium && flags.kind !== 'document') {
50
+ this.error(`--medium applies to --kind document only (got --kind ${flags.kind}); a thread carries a kind per message.`);
51
+ }
45
52
  const params = new URLSearchParams();
46
53
  params.set('type', flags.kind);
54
+ if (flags.medium)
55
+ params.set('medium', flags.medium);
47
56
  params.set('perspective', flags.perspective);
48
57
  if (flags['face-speaker'])
49
58
  params.set('face_speaker', flags['face-speaker']);
@@ -30,16 +30,18 @@ export default class FaceDiff extends BaseCommand {
30
30
  if (!this.jsonEnabled()) {
31
31
  const res = data;
32
32
  const faces = res.faces;
33
- const components = ['face', 'beta', 'delta', 'epsilon'];
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];
37
37
  const b = faces[j];
38
- const scores = res.similarity[a][b];
39
- const parts = components.map(c => {
40
- const v = scores[c];
41
- return `${c}: ${v === null ? '—' : v.toFixed(2)}`;
42
- });
38
+ const { face: overall, components } = res.similarity[a][b];
39
+ const parts = [
40
+ `overall: ${fmt(overall)}`,
41
+ // Label by true index, never by ordinal-among-non-null: the position
42
+ // is the identity, and it is the same vocabulary the API uses.
43
+ ...(components ?? []).map((v, n) => `component ${n}: ${fmt(v)}`),
44
+ ];
43
45
  this.log(`${a} ↔ ${b} ${parts.join(' ')}`);
44
46
  }
45
47
  }
@@ -6,6 +6,7 @@ export default class FaceList extends BaseCommand {
6
6
  team: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
7
  include: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
8
  public: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ shared: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
10
  system: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
11
  'from-users': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
12
  'not-from-users': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -11,7 +11,8 @@ export default class FaceList extends BaseCommand {
11
11
  tag: Flags.string({ description: 'Filter by tag (repeatable, AND logic)', multiple: true }),
12
12
  team: Flags.string({ description: 'Filter by team ID (repeatable, OR logic)', multiple: true }),
13
13
  include: Flags.string({ description: 'Include extra fields: tags, teams (comma-separated)' }),
14
- public: Flags.boolean({ description: 'Include published faces from other accounts' }),
14
+ public: Flags.boolean({ description: 'Include published faces from other accounts (open to everybody)' }),
15
+ shared: Flags.boolean({ description: 'Include faces other accounts have shared with YOU in particular' }),
15
16
  system: Flags.boolean({ description: `Show only the curated system faces (published faces owned by '${SYSTEM_OWNER}')` }),
16
17
  'from-users': Flags.string({ description: 'Only published faces from these owners (repeatable/comma-separated)', multiple: true }),
17
18
  'not-from-users': Flags.string({ description: 'Exclude published faces from these owners (repeatable/comma-separated)', multiple: true }),
@@ -44,6 +45,8 @@ export default class FaceList extends BaseCommand {
44
45
  parts.push(`include=${encodeURIComponent(flags.include)}`);
45
46
  if (includePublished)
46
47
  parts.push('include_published=true');
48
+ if (flags.shared)
49
+ parts.push('include_shared=true');
47
50
  for (const u of fromUsers)
48
51
  parts.push(`from_users=${encodeURIComponent(u)}`);
49
52
  for (const u of notFromUsers)
@@ -82,7 +85,11 @@ export default class FaceList extends BaseCommand {
82
85
  data = faces;
83
86
  if (!this.jsonEnabled()) {
84
87
  // Published faces are chatted as `owner:alias`; own faces stay bare.
85
- const handle = (f) => f.published && f.owned_by ? `${f.owned_by}:${f.alias}` : f.alias;
88
+ // A face you do not own needs the owner prefix to resolve at all. Both
89
+ // published and shared faces are someone else's, and only those two carry
90
+ // `published` / `via`, so either marks the alias as needing qualifying.
91
+ const isForeign = (f) => Boolean(f.published || f.via?.length);
92
+ const handle = (f) => isForeign(f) && f.owned_by ? `${f.owned_by}:${f.alias}` : f.alias;
86
93
  if (faces.length === 0) {
87
94
  this.log('(no faces)');
88
95
  }
@@ -96,10 +103,16 @@ export default class FaceList extends BaseCommand {
96
103
  }
97
104
  else if (f.component_counts || f.profile_token_count) {
98
105
  const cc = f.component_counts;
99
- const total = cc ? cc.alpha + cc.beta + cc.delta + cc.epsilon : 0;
106
+ const total = cc?.reduce((a, b) => a + b, 0) ?? 0;
100
107
  const profile = f.profile_token_count ?? 0;
101
108
  suffix = ` [profile: ${profile} tok, components: ${total}]`;
102
109
  }
110
+ if (f.via?.length) {
111
+ const how = f.via.includes('shared') && f.via.includes('workspace')
112
+ ? 'shared with you, and via a workspace'
113
+ : f.via.includes('workspace') ? 'via a workspace' : 'shared with you';
114
+ suffix += ` [${how} · requires @model]`;
115
+ }
103
116
  if (f.published)
104
117
  suffix += ' [public · requires @model]';
105
118
  if (f.read_only)
@@ -12,8 +12,8 @@ export default class FaceNeighbors extends BaseCommand {
12
12
  max: 20,
13
13
  }),
14
14
  component: Flags.string({
15
- description: 'Centroid component to rank on (beta, delta, epsilon are the principal components of a face; face is their composite)',
16
- options: ['face', 'beta', 'delta', 'epsilon'],
15
+ description: 'What to rank on: `face` for overall similarity, or a component position such as 1. ' +
16
+ 'Which positions are comparable varies; the API names them if you pick a wrong one.',
17
17
  default: 'face',
18
18
  }),
19
19
  direction: Flags.string({
@@ -33,19 +33,35 @@ export default class FaceNeighbors extends BaseCommand {
33
33
  component: flags.component,
34
34
  direction: flags.direction,
35
35
  };
36
+ // Positions are open-ended (they follow the component-counts order, which can
37
+ // grow), so validate the shape only and let the API say which are comparable
38
+ // — it knows, and the answer differs per face.
39
+ if (flags.component !== 'face' && !/^\d+$/.test(flags.component)) {
40
+ this.error(`--component must be 'face' or a component position such as 1 (got '${flags.component}').`);
41
+ }
36
42
  let data;
37
43
  try {
38
44
  data = await client.get(`/v1/faces/${args.face_id}/neighbors`, { params });
39
45
  }
40
46
  catch (err) {
41
- if (err instanceof FacesAPIError)
47
+ if (err instanceof FacesAPIError) {
48
+ // A face with nothing at that position is an empty answer, not a failure:
49
+ // it mirrors the null at the same index in face:diff. Say so plainly and
50
+ // exit clean, so a caller can tell "nothing here" from "something broke".
51
+ if (err.statusCode === 422 && /has no (data for component position|centroid)/.test(err.message)) {
52
+ if (!this.jsonEnabled())
53
+ this.log(err.message);
54
+ return { face: args.face_id, component: flags.component, direction: flags.direction, neighbors: [], reason: err.message };
55
+ }
42
56
  this.error(`Error (${err.statusCode}): ${err.message}`);
57
+ }
43
58
  throw err;
44
59
  }
45
60
  if (!this.jsonEnabled()) {
46
61
  const res = data;
47
62
  const label = res.direction === 'nearest' ? 'Nearest' : 'Most dissimilar';
48
- this.log(`${label} neighbors to ${res.face} (by ${res.component}):`);
63
+ const by = res.component === 'face' ? 'overall similarity' : `component ${res.component}`;
64
+ this.log(`${label} neighbors to ${res.face} (by ${by}):`);
49
65
  this.log('');
50
66
  if (res.neighbors.length === 0) {
51
67
  this.log(' (none)');