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.
- package/dist/commands/chat/chat.d.ts +3 -1
- package/dist/commands/chat/chat.js +29 -4
- package/dist/commands/chat/messages.js +1 -1
- package/dist/commands/chat/responses.js +1 -1
- package/dist/commands/chat/thread.js +1 -1
- package/dist/commands/face/list.d.ts +4 -0
- package/dist/commands/face/list.js +47 -4
- package/dist/routing.d.ts +9 -0
- package/dist/routing.js +18 -1
- package/oclif.manifest.json +1059 -1021
- package/package.json +1 -1
|
@@ -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({
|
|
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.
|
|
30
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|