faces-cli 1.2.0 → 1.3.1

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.
@@ -0,0 +1,15 @@
1
+ import { BaseCommand } from '../../base.js';
2
+ export default class AuthConnect extends BaseCommand {
3
+ static description: string;
4
+ static args: {
5
+ provider: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
6
+ };
7
+ static flags: {
8
+ manual: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ 'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ };
13
+ static examples: string[];
14
+ run(): Promise<unknown>;
15
+ }
@@ -0,0 +1,239 @@
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';
5
+ import { BaseCommand } from '../../base.js';
6
+ 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();
24
+ }
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
+ });
66
+ }
67
+ export default class AuthConnect extends BaseCommand {
68
+ static description = 'Connect an OAuth provider account to Faces (e.g. ChatGPT Plus/Pro)';
69
+ static args = {
70
+ provider: Args.string({
71
+ description: 'OAuth provider to connect',
72
+ required: true,
73
+ options: ['openai'],
74
+ }),
75
+ };
76
+ static flags = {
77
+ ...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
+ };
84
+ static examples = [
85
+ '<%= config.bin %> auth connect openai',
86
+ '<%= config.bin %> auth connect openai --manual',
87
+ ];
88
+ async run() {
89
+ const { args, flags } = await this.parse(AuthConnect);
90
+ const client = this.makeClient(flags, true);
91
+ // 0. Short-circuit if already connected
92
+ try {
93
+ 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.`);
98
+ return output;
99
+ }
100
+ }
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;
105
+ try {
106
+ authorizeData = (await client.get(`/v1/oauth/${args.provider}/authorize`, {
107
+ requireJwt: true,
108
+ }));
109
+ }
110
+ 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;
120
+ }
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);
138
+ });
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;
148
+ }
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;
155
+ }
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);
164
+ }
165
+ catch {
166
+ this.error('Could not parse the URL you pasted. Make sure to copy the full address bar URL.');
167
+ }
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?');
172
+ }
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`);
179
+ try {
180
+ ;
181
+ ({ code, state } = await callbackPromise);
182
+ }
183
+ 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 */ }
214
+ }
215
+ this.error(`Token exchange failed (${err.statusCode}): ${err.message}`);
216
+ }
217
+ throw err;
218
+ }
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
+ }
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
236
+ }
237
+ }
238
+ throw new Error('Timed out waiting for connection (5 minutes).');
239
+ }
@@ -0,0 +1,11 @@
1
+ import { BaseCommand } from '../../base.js';
2
+ export default class AuthConnections extends BaseCommand {
3
+ static description: string;
4
+ static flags: {
5
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
6
+ token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ 'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ };
9
+ static examples: string[];
10
+ run(): Promise<unknown>;
11
+ }
@@ -0,0 +1,28 @@
1
+ import { BaseCommand } from '../../base.js';
2
+ export default class AuthConnections extends BaseCommand {
3
+ static description = 'List connected OAuth provider accounts';
4
+ static flags = {
5
+ ...BaseCommand.baseFlags,
6
+ };
7
+ static examples = [
8
+ '<%= config.bin %> auth connections',
9
+ '<%= config.bin %> auth connections --json',
10
+ ];
11
+ async run() {
12
+ const { flags } = await this.parse(AuthConnections);
13
+ const client = this.makeClient(flags, true);
14
+ const data = await client.get('/v1/oauth', { requireJwt: true });
15
+ if (!this.jsonEnabled()) {
16
+ const rows = data;
17
+ if (!rows || rows.length === 0) {
18
+ this.log('No connected providers.');
19
+ }
20
+ else {
21
+ for (const row of rows) {
22
+ this.log(`${row.provider} connected_at=${row.connected_at}${row.scope ? ` scope=${row.scope}` : ''}`);
23
+ }
24
+ }
25
+ }
26
+ return data;
27
+ }
28
+ }
@@ -0,0 +1,14 @@
1
+ import { BaseCommand } from '../../base.js';
2
+ export default class AuthDisconnect extends BaseCommand {
3
+ static description: string;
4
+ static args: {
5
+ provider: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
6
+ };
7
+ static flags: {
8
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ static examples: string[];
13
+ run(): Promise<unknown>;
14
+ }
@@ -0,0 +1,38 @@
1
+ import { Args } from '@oclif/core';
2
+ import { BaseCommand } from '../../base.js';
3
+ import { FacesAPIError } from '../../client.js';
4
+ export default class AuthDisconnect extends BaseCommand {
5
+ static description = 'Disconnect a linked OAuth provider account';
6
+ static args = {
7
+ provider: Args.string({
8
+ description: 'OAuth provider to disconnect',
9
+ required: true,
10
+ options: ['openai'],
11
+ }),
12
+ };
13
+ static flags = {
14
+ ...BaseCommand.baseFlags,
15
+ };
16
+ static examples = [
17
+ '<%= config.bin %> auth disconnect openai',
18
+ ];
19
+ async run() {
20
+ const { args, flags } = await this.parse(AuthDisconnect);
21
+ const client = this.makeClient(flags, true);
22
+ try {
23
+ await client.delete(`/v1/oauth/${args.provider}`, { requireJwt: true });
24
+ }
25
+ catch (err) {
26
+ if (err instanceof FacesAPIError) {
27
+ if (err.statusCode === 404)
28
+ this.error(`No connected ${args.provider} account found.`);
29
+ this.error(`Disconnect failed (${err.statusCode}): ${err.message}`);
30
+ }
31
+ throw err;
32
+ }
33
+ const output = { provider: args.provider, connected: false };
34
+ if (!this.jsonEnabled())
35
+ this.log(`✅ ${args.provider} disconnected.`);
36
+ return output;
37
+ }
38
+ }
@@ -16,7 +16,7 @@ export default class AuthLogin extends BaseCommand {
16
16
  const client = new FacesClient(baseUrl);
17
17
  let data;
18
18
  try {
19
- data = (await client.postNoAuth('/v1/auth/login', {
19
+ data = (await client.postNoAuth('/auth/login', {
20
20
  email: flags.email,
21
21
  password: flags.password,
22
22
  }));
@@ -27,8 +27,10 @@ export default class AuthLogin extends BaseCommand {
27
27
  }
28
28
  throw err;
29
29
  }
30
- const token = (data.access_token ?? data.token);
31
- const userId = data.user_id ?? data.id ?? data.user?.id;
30
+ // Unwrap StandardResponse envelope if present
31
+ const inner = (data.data ?? data);
32
+ const token = (inner.access_token ?? inner.token);
33
+ const userId = inner.user_id ?? inner.id ?? inner.user?.id;
32
34
  if (!token)
33
35
  this.error(`Unexpected response: ${JSON.stringify(data)}`);
34
36
  saveConfig({ token, user_id: userId ? String(userId) : undefined });
@@ -4,7 +4,7 @@ export default class AuthRegister extends BaseCommand {
4
4
  static flags: {
5
5
  email: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
6
6
  password: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
- name: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
8
  username: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
9
  'invite-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
10
  'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -8,7 +8,7 @@ export default class AuthRegister extends BaseCommand {
8
8
  ...BaseCommand.baseFlags,
9
9
  email: Flags.string({ description: 'Email address', required: true }),
10
10
  password: Flags.string({ description: 'Password', required: true }),
11
- name: Flags.string({ description: 'Display name', required: true }),
11
+ name: Flags.string({ description: 'Display name (defaults to username)' }),
12
12
  username: Flags.string({ description: 'Username (lowercase, dashes, numbers)', required: true }),
13
13
  'invite-key': Flags.string({ description: 'Invite key (if required)' }),
14
14
  };
@@ -20,14 +20,14 @@ export default class AuthRegister extends BaseCommand {
20
20
  const payload = {
21
21
  email: flags.email,
22
22
  password: flags.password,
23
- name: flags.name,
23
+ name: flags.name ?? flags.username,
24
24
  username: flags.username,
25
25
  };
26
26
  if (flags['invite-key'])
27
27
  payload.invite_key = flags['invite-key'];
28
28
  let data;
29
29
  try {
30
- data = (await client.postNoAuth('/v1/auth/register', payload));
30
+ data = (await client.postNoAuth('/auth/register-email', payload));
31
31
  }
32
32
  catch (err) {
33
33
  if (err instanceof FacesAPIError) {
@@ -35,11 +35,21 @@ export default class AuthRegister extends BaseCommand {
35
35
  }
36
36
  throw err;
37
37
  }
38
- const token = (data.access_token ?? data.token);
38
+ // Unwrap StandardResponse envelope if present
39
+ const inner = (data.data ?? data);
40
+ const token = (inner.access_token ?? inner.token);
39
41
  if (token)
40
42
  saveConfig({ token });
43
+ const result = {
44
+ status: 'registered',
45
+ user_id: inner.user_id,
46
+ activation_required: inner.activation_required ?? false,
47
+ };
48
+ if (inner.activation_checkout_url) {
49
+ result.activation_checkout_url = inner.activation_checkout_url;
50
+ }
41
51
  if (!this.jsonEnabled())
42
- this.printHuman(data);
43
- return data;
52
+ this.printHuman(result);
53
+ return result;
44
54
  }
45
55
  }
@@ -0,0 +1,17 @@
1
+ import { BaseCommand } from '../../base.js';
2
+ export default class CompileImport extends BaseCommand {
3
+ static description: string;
4
+ static flags: {
5
+ url: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
6
+ type: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ perspective: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
+ 'face-speaker': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ 'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ };
13
+ static args: {
14
+ face_id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
15
+ };
16
+ run(): Promise<unknown>;
17
+ }
@@ -0,0 +1,81 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../base.js';
3
+ import { FacesAPIError } from '../../client.js';
4
+ export default class CompileImport extends BaseCommand {
5
+ static description = 'Import a YouTube video into a face via server-side download and transcription';
6
+ static flags = {
7
+ ...BaseCommand.baseFlags,
8
+ url: Flags.string({
9
+ description: 'YouTube URL (youtube.com/watch?v=... or youtu.be/...)',
10
+ required: true,
11
+ }),
12
+ type: Flags.string({
13
+ description: '"document" for solo talks, lectures, monologues; "thread" for interviews or multi-speaker conversations',
14
+ options: ['document', 'thread'],
15
+ default: 'document',
16
+ }),
17
+ perspective: Flags.string({
18
+ description: '"first-person" if the face is the speaker; "third-person" if the video is about the face',
19
+ options: ['first-person', 'third-person'],
20
+ default: 'third-person',
21
+ }),
22
+ 'face-speaker': Flags.string({
23
+ description: 'For --type thread: AssemblyAI speaker label (e.g. "A") that corresponds to the face. Defaults to first detected speaker.',
24
+ }),
25
+ };
26
+ static args = {
27
+ face_id: Args.string({ description: 'Face ID or username', required: true }),
28
+ };
29
+ async run() {
30
+ const { args, flags } = await this.parse(CompileImport);
31
+ const client = this.makeClient(flags);
32
+ if (flags['face-speaker'] && flags.type !== 'thread') {
33
+ this.error('--face-speaker is only valid with --type thread');
34
+ }
35
+ const payload = {
36
+ url: flags.url,
37
+ type: flags.type,
38
+ perspective: flags.perspective,
39
+ };
40
+ if (flags['face-speaker'])
41
+ payload.face_speaker = flags['face-speaker'];
42
+ this.log('Importing… (transcription may take 10–60s depending on video length)');
43
+ let data;
44
+ try {
45
+ data = await client.post(`/v1/faces/${args.face_id}/import`, { body: payload });
46
+ }
47
+ catch (err) {
48
+ if (err instanceof FacesAPIError) {
49
+ if (err.statusCode === 422 && flags.type === 'thread') {
50
+ this.error(`${err.message} — try again with --type document`);
51
+ }
52
+ this.error(`Error (${err.statusCode}): ${err.message}`);
53
+ }
54
+ throw err;
55
+ }
56
+ if (!this.jsonEnabled()) {
57
+ const res = data;
58
+ if (res.type === 'document') {
59
+ const doc = res;
60
+ this.log(`Imported as document`);
61
+ this.log(`id: ${doc.document_id}`);
62
+ this.log(`label: ${doc.label}`);
63
+ this.log(`tokens: ${doc.token_count}`);
64
+ this.log(``);
65
+ this.log(`Next steps:`);
66
+ this.log(` faces compile:doc:prepare ${doc.document_id}`);
67
+ this.log(` faces compile:doc:sync ${doc.document_id} --yes`);
68
+ }
69
+ else {
70
+ const thread = res;
71
+ this.log(`Imported as thread`);
72
+ this.log(`id: ${thread.thread_id}`);
73
+ this.log(`label: ${thread.label}`);
74
+ this.log(``);
75
+ this.log(`Next step:`);
76
+ this.log(` faces compile:thread:sync ${thread.thread_id}`);
77
+ }
78
+ }
79
+ return data;
80
+ }
81
+ }
@@ -7,7 +7,7 @@ export default class FaceCreate extends BaseCommand {
7
7
  ...BaseCommand.baseFlags,
8
8
  name: Flags.string({ description: 'Display name', required: true }),
9
9
  username: Flags.string({ description: 'Unique username slug', required: true }),
10
- formula: Flags.string({ description: 'Boolean formula over owned concrete face usernames (e.g. "a | b", "(a | b) - c"). Creates a synthetic face.' }),
10
+ formula: Flags.string({ description: 'Boolean formula over owned concrete face usernames (e.g. "a | b", "(a | b) - c"). Creates a composite face.' }),
11
11
  attr: Flags.string({ description: 'Attribute KEY=VALUE (repeatable)', multiple: true }),
12
12
  tool: Flags.string({ description: 'Tool name to enable (repeatable)', multiple: true }),
13
13
  };
@@ -16,7 +16,7 @@ export default class FaceCreate extends BaseCommand {
16
16
  const { flags } = await this.parse(FaceCreate);
17
17
  const client = this.makeClient(flags);
18
18
  if (flags.formula && (flags.attr?.length || flags.tool?.length)) {
19
- this.error('--formula cannot be combined with --attr or --tool. Synthetic faces do not have compiled knowledge.');
19
+ this.error('--formula cannot be combined with --attr or --tool. Composite faces do not have compiled knowledge.');
20
20
  }
21
21
  const payload = { name: flags.name, username: flags.username };
22
22
  if (flags.formula) {
@@ -30,7 +30,7 @@ export default class FaceGet extends BaseCommand {
30
30
  this.log(`created: ${new Date(f.created * 1000).toISOString().slice(0, 10)}`);
31
31
  if (f.formula) {
32
32
  this.log(`formula: ${f.formula}`);
33
- this.log(`type: synthetic`);
33
+ this.log(`type: composite`);
34
34
  }
35
35
  else {
36
36
  this.log(`type: concrete`);
@@ -26,7 +26,7 @@ export default class FaceList extends BaseCommand {
26
26
  const idWidth = Math.max(...faces.map(f => f.id.length));
27
27
  const nameWidth = Math.max(...faces.map(f => f.name.length));
28
28
  for (const f of faces) {
29
- const synthetic = f.formula ? ` [synthetic: ${f.formula}]` : '';
29
+ const synthetic = f.formula ? ` [composite: ${f.formula}]` : '';
30
30
  this.log(`${f.id.padEnd(idWidth)} ${f.name.padEnd(nameWidth)}${synthetic}`);
31
31
  }
32
32
  }