faces-cli 1.7.0 → 1.7.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.
@@ -1,9 +1,11 @@
1
1
  import { BaseCommand } from '../../base.js';
2
2
  export default class ChatChat extends BaseCommand {
3
3
  static description: string;
4
+ static examples: string[];
4
5
  static flags: {
5
6
  message: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
6
7
  llm: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ formula: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
9
  system: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
10
  stream: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
11
  'max-tokens': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -16,7 +18,7 @@ export default class ChatChat extends BaseCommand {
16
18
  'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
19
  };
18
20
  static args: {
19
- face_username: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
21
+ face_username: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
20
22
  };
21
23
  run(): Promise<unknown>;
22
24
  private oauthHint;
@@ -3,13 +3,19 @@ import fs from 'node:fs';
3
3
  import { BaseCommand } from '../../base.js';
4
4
  import { FacesAPIError } from '../../client.js';
5
5
  import { extractStreamDelta } from '../../utils.js';
6
- import { resolveEndpoint, MESSAGES_ENDPOINT, RESPONSES_ENDPOINT } from '../../routing.js';
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.';
9
+ static examples = [
10
+ '<%= config.bin %> <%= command.id %> socrates -m "What is virtue?"',
11
+ '<%= config.bin %> <%= command.id %> "(socrates | nietzsche)@claude-sonnet-4-6" -m "What is virtue?"',
12
+ '<%= config.bin %> <%= command.id %> --formula "socrates | nietzsche" --llm claude-sonnet-4-6 -m "What is virtue?"',
13
+ ];
9
14
  static flags = {
10
15
  ...BaseCommand.baseFlags,
11
16
  message: Flags.string({ char: 'm', description: 'User message (repeatable)', multiple: true, required: false }),
12
17
  llm: Flags.string({ description: 'LLM override (e.g. gpt-4o-mini, claude-sonnet-4-6)' }),
18
+ formula: Flags.string({ description: 'Compose a run-time composite from your own faces, e.g. "socrates | nietzsche" (operators: | & ^ -). Requires --llm.' }),
13
19
  system: Flags.string({ description: 'System prompt / instructions' }),
14
20
  stream: Flags.boolean({ description: 'Stream the response', default: false }),
15
21
  'max-tokens': Flags.integer({ description: 'Max tokens' }),
@@ -19,20 +25,39 @@ export default class ChatChat extends BaseCommand {
19
25
  'oauth-only': Flags.boolean({ description: 'Prevent fallback to paid system key (use OAuth only)', default: false }),
20
26
  };
21
27
  static args = {
22
- face_username: Args.string({ description: 'Face alias (or alias@model)', required: true }),
28
+ face_username: Args.string({
29
+ description: 'Face alias (alias@model, owner:alias@model for a published face, or a composite formula like "(a | b)@model"). Omit when using --formula.',
30
+ required: false,
31
+ }),
23
32
  };
24
33
  async run() {
25
34
  const { args, flags } = await this.parse(ChatChat);
26
35
  const client = this.makeClient(flags);
27
36
  // Build model string
28
37
  let model;
29
- if (flags.llm) {
30
- const base = args.face_username.split('@')[0];
38
+ if (flags.formula) {
39
+ if (args.face_username)
40
+ this.error('Pass either a face argument or --formula, not both.');
41
+ if (!flags.llm)
42
+ this.error('--formula needs a model. Add --llm <model>, e.g. --llm claude-sonnet-4-6.');
43
+ model = `(${flags.formula.trim()})@${flags.llm}`;
44
+ }
45
+ else if (!args.face_username) {
46
+ this.error('Provide a face (alias, owner:alias, or a composite formula), or use --formula.');
47
+ }
48
+ else if (flags.llm) {
49
+ const base = faceSpecOf(args.face_username);
31
50
  model = `${base}@${flags.llm}`;
32
51
  }
33
52
  else {
34
53
  model = args.face_username;
35
54
  }
55
+ // A run-time composite has no stored default_model — require an explicit @model (rule #1).
56
+ if (isCompositeFormula(model) && !llmFromArg(model)) {
57
+ const spec = faceSpecOf(model);
58
+ this.error(`Composite face '${spec}' has no default model. Specify one explicitly, ` +
59
+ `e.g. '${spec}@claude-sonnet-4-6', or pass --llm <model>.`);
60
+ }
36
61
  // Collect user messages
37
62
  const userMessages = [];
38
63
  if (flags.file) {
@@ -12,7 +12,7 @@ export default class ChatMessages extends BaseCommand {
12
12
  'oauth-only': Flags.boolean({ description: 'Prevent fallback to paid system key (use OAuth only)', default: false }),
13
13
  };
14
14
  static args = {
15
- face_model: Args.string({ description: 'Face model (e.g. myface or myface@claude-sonnet-4-6)', required: true }),
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 }),
16
16
  };
17
17
  async run() {
18
18
  const { args, flags } = await this.parse(ChatMessages);
@@ -11,7 +11,7 @@ export default class ChatResponses extends BaseCommand {
11
11
  'oauth-only': Flags.boolean({ description: 'Prevent fallback to paid system key (use OAuth only)', default: false }),
12
12
  };
13
13
  static args = {
14
- face_model: Args.string({ description: 'Face model (e.g. myface or myface@gpt-4o)', required: true }),
14
+ face_model: Args.string({ description: 'Face model (e.g. myface, myface@gpt-4o, or head:logician@gpt-5.4)', required: true }),
15
15
  };
16
16
  async run() {
17
17
  const { args, flags } = await this.parse(ChatResponses);
@@ -98,7 +98,7 @@ export default class ChatThread extends BaseCommand {
98
98
  };
99
99
  static args = {
100
100
  face_username: Args.string({
101
- description: 'Face alias (or alias@model) — required when starting a new thread',
101
+ description: 'Face alias (alias@model, or owner:alias@model for a published face) — required when starting a new thread',
102
102
  required: false,
103
103
  }),
104
104
  };
@@ -5,6 +5,10 @@ export default class FaceList extends BaseCommand {
5
5
  tag: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
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
+ public: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ system: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ 'from-users': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ 'not-from-users': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
12
  'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
13
  token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
14
  'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -2,17 +2,34 @@ import { Flags } from '@oclif/core';
2
2
  import { BaseCommand } from '../../base.js';
3
3
  import { FacesAPIError } from '../../client.js';
4
4
  import { renameFaceFields } from '../../utils.js';
5
+ /** Curated system faces are served under this owner account. */
6
+ const SYSTEM_OWNER = 'head';
5
7
  export default class FaceList extends BaseCommand {
6
- static description = 'List all owned faces';
8
+ static description = 'List faces. By default lists owned faces; use --public to include published faces from other accounts.';
7
9
  static flags = {
8
10
  ...BaseCommand.baseFlags,
9
11
  tag: Flags.string({ description: 'Filter by tag (repeatable, AND logic)', multiple: true }),
10
12
  team: Flags.string({ description: 'Filter by team ID (repeatable, OR logic)', multiple: true }),
11
13
  include: Flags.string({ description: 'Include extra fields: tags, teams (comma-separated)' }),
14
+ public: Flags.boolean({ description: 'Include published faces from other accounts' }),
15
+ system: Flags.boolean({ description: `Show only the curated system faces (published faces owned by '${SYSTEM_OWNER}')` }),
16
+ 'from-users': Flags.string({ description: 'Only published faces from these owners (repeatable/comma-separated)', multiple: true }),
17
+ 'not-from-users': Flags.string({ description: 'Exclude published faces from these owners (repeatable/comma-separated)', multiple: true }),
12
18
  };
13
19
  async run() {
14
20
  const { flags } = await this.parse(FaceList);
15
21
  const client = this.makeClient(flags);
22
+ // Split comma-separated owner lists into individual values.
23
+ const split = (vals) => (vals ?? []).flatMap(v => v.split(',')).map(v => v.trim()).filter(Boolean);
24
+ const fromUsers = split(flags['from-users']);
25
+ if (flags.system)
26
+ fromUsers.push(SYSTEM_OWNER);
27
+ const notFromUsers = split(flags['not-from-users']);
28
+ if (fromUsers.length > 0 && notFromUsers.length > 0) {
29
+ this.error('--from-users and --not-from-users are mutually exclusive');
30
+ }
31
+ // Any cross-account owner filter implies we want published faces.
32
+ const includePublished = flags.public || flags.system || fromUsers.length > 0 || notFromUsers.length > 0;
16
33
  // Build query string manually for repeated params
17
34
  const parts = [];
18
35
  if (flags.tag && flags.tag.length > 0) {
@@ -25,6 +42,12 @@ export default class FaceList extends BaseCommand {
25
42
  }
26
43
  if (flags.include)
27
44
  parts.push(`include=${encodeURIComponent(flags.include)}`);
45
+ if (includePublished)
46
+ parts.push('include_published=true');
47
+ for (const u of fromUsers)
48
+ parts.push(`from_users=${encodeURIComponent(u)}`);
49
+ for (const u of notFromUsers)
50
+ parts.push(`not_from_users=${encodeURIComponent(u)}`);
28
51
  let data;
29
52
  try {
30
53
  const path = parts.length > 0 ? `/v1/faces?${parts.join('&')}` : '/v1/faces';
@@ -40,13 +63,31 @@ export default class FaceList extends BaseCommand {
40
63
  for (const f of raw) {
41
64
  renameFaceFields(f);
42
65
  }
66
+ // The API's from_users/not_from_users only constrain the *appended published*
67
+ // set — the caller's own faces always come back. Filter client-side so
68
+ // --system / --from-users / --not-from-users actually mean "from these owners".
69
+ let faces = raw;
70
+ if (fromUsers.length > 0) {
71
+ const allow = new Set(fromUsers);
72
+ faces = faces.filter(f => f.owned_by != null && allow.has(f.owned_by));
73
+ }
74
+ else if (notFromUsers.length > 0) {
75
+ const block = new Set(notFromUsers);
76
+ faces = faces.filter(f => f.owned_by == null || !block.has(f.owned_by));
77
+ }
78
+ // Keep --json output consistent with what we display.
79
+ if (data.data !== undefined)
80
+ data.data = faces;
81
+ else
82
+ data = faces;
43
83
  if (!this.jsonEnabled()) {
44
- const faces = raw;
84
+ // 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;
45
86
  if (faces.length === 0) {
46
87
  this.log('(no faces)');
47
88
  }
48
89
  else {
49
- const aliasWidth = Math.max(...faces.map(f => f.alias.length));
90
+ const handleWidth = Math.max(...faces.map(f => handle(f).length));
50
91
  const nameWidth = Math.max(...faces.map(f => f.name.length));
51
92
  for (const f of faces) {
52
93
  let suffix = '';
@@ -59,7 +100,9 @@ export default class FaceList extends BaseCommand {
59
100
  const profile = f.profile_token_count ?? 0;
60
101
  suffix = ` [profile: ${profile} tok, components: ${total}]`;
61
102
  }
62
- this.log(`${f.alias.padEnd(aliasWidth)} ${f.name.padEnd(nameWidth)}${suffix}`);
103
+ if (f.published)
104
+ suffix += ' [public · requires @model]';
105
+ this.log(`${handle(f).padEnd(handleWidth)} ${f.name.padEnd(nameWidth)}${suffix}`);
63
106
  }
64
107
  }
65
108
  }
package/dist/routing.d.ts CHANGED
@@ -11,6 +11,15 @@ export interface RouteInfo {
11
11
  }
12
12
  /** Split `alias@llm` → llm; bare `alias` → undefined. */
13
13
  export declare function llmFromArg(modelArg: string): string | undefined;
14
+ /** The face-spec (everything left of the trailing `@model`), e.g. `(a | b)@x` → `(a | b)`. */
15
+ export declare function faceSpecOf(modelArg: string): string;
16
+ /**
17
+ * True when a model arg's face-spec is a run-time composite formula, i.e. it
18
+ * contains a set operator (`| & ^`), grouping parens, or a space-delimited `-`
19
+ * difference. None of these characters are legal in an alias (`^[a-z0-9-]+$`) or
20
+ * in `owner:alias`, so detection is unambiguous. Mirrors the backend's check.
21
+ */
22
+ export declare function isCompositeFormula(modelArg: string): boolean;
14
23
  /**
15
24
  * Decide which API endpoint a chat request should be posted to, driven entirely
16
25
  * by the model catalog (GET /v1/models). For a bare alias we resolve the face's
package/dist/routing.js CHANGED
@@ -88,6 +88,21 @@ export function llmFromArg(modelArg) {
88
88
  const i = modelArg.lastIndexOf('@');
89
89
  return i >= 0 ? modelArg.slice(i + 1) : undefined;
90
90
  }
91
+ /** The face-spec (everything left of the trailing `@model`), e.g. `(a | b)@x` → `(a | b)`. */
92
+ export function faceSpecOf(modelArg) {
93
+ const i = modelArg.lastIndexOf('@');
94
+ return i >= 0 ? modelArg.slice(0, i) : modelArg;
95
+ }
96
+ /**
97
+ * True when a model arg's face-spec is a run-time composite formula, i.e. it
98
+ * contains a set operator (`| & ^`), grouping parens, or a space-delimited `-`
99
+ * difference. None of these characters are legal in an alias (`^[a-z0-9-]+$`) or
100
+ * in `owner:alias`, so detection is unambiguous. Mirrors the backend's check.
101
+ */
102
+ export function isCompositeFormula(modelArg) {
103
+ const spec = faceSpecOf(modelArg).trim();
104
+ return /[|&^()]/.test(spec) || /\S\s+\S/.test(spec);
105
+ }
91
106
  /**
92
107
  * Decide which API endpoint a chat request should be posted to, driven entirely
93
108
  * by the model catalog (GET /v1/models). For a bare alias we resolve the face's
@@ -95,7 +110,9 @@ export function llmFromArg(modelArg) {
95
110
  */
96
111
  export async function resolveEndpoint(client, modelArg) {
97
112
  let llm = llmFromArg(modelArg);
98
- if (!llm) {
113
+ // A composite formula has no stored default_model and isn't a catalog face, so
114
+ // never attempt an alias lookup — route purely off the explicit `@model`.
115
+ if (!llm && !isCompositeFormula(modelArg)) {
99
116
  const alias = modelArg.split('@')[0];
100
117
  llm = defaultModelFromCatalog(alias);
101
118
  if (!llm) {