faces-cli 1.5.14 → 1.6.0
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/client.d.ts +14 -1
- package/dist/client.js +40 -6
- package/dist/commands/account/preferences.d.ts +15 -0
- package/dist/commands/account/preferences.js +65 -0
- package/dist/commands/catalog/backup.d.ts +10 -0
- package/dist/commands/catalog/backup.js +101 -0
- package/dist/commands/catalog/restore.d.ts +16 -0
- package/dist/commands/catalog/restore.js +147 -0
- package/dist/commands/chat/chat.d.ts +2 -0
- package/dist/commands/chat/chat.js +88 -18
- package/dist/commands/chat/messages.d.ts +1 -0
- package/dist/commands/chat/messages.js +25 -6
- package/dist/commands/chat/responses.d.ts +1 -0
- package/dist/commands/chat/responses.js +25 -6
- package/dist/commands/compile/all.d.ts +11 -0
- package/dist/commands/compile/all.js +116 -0
- package/dist/commands/compile/thread/create.d.ts +1 -0
- package/dist/commands/compile/thread/create.js +11 -1
- package/dist/commands/compile/thread/message.d.ts +1 -0
- package/dist/commands/compile/thread/message.js +13 -2
- package/oclif.manifest.json +987 -712
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
export declare class FacesAPIError extends Error {
|
|
2
2
|
statusCode: number;
|
|
3
|
-
|
|
3
|
+
errorCode?: string;
|
|
4
|
+
fallbackAvailable?: boolean;
|
|
5
|
+
constructor(statusCode: number, message: string, opts?: {
|
|
6
|
+
errorCode?: string;
|
|
7
|
+
fallbackAvailable?: boolean;
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
export interface ResponseWithHeaders<T = unknown> {
|
|
11
|
+
data: T;
|
|
12
|
+
headers: Record<string, string>;
|
|
4
13
|
}
|
|
5
14
|
export declare class FacesClient {
|
|
6
15
|
private baseUrl;
|
|
@@ -19,6 +28,10 @@ export declare class FacesClient {
|
|
|
19
28
|
requireJwt?: boolean;
|
|
20
29
|
body?: unknown;
|
|
21
30
|
}): Promise<unknown>;
|
|
31
|
+
postWithHeaders(path: string, opts?: {
|
|
32
|
+
requireJwt?: boolean;
|
|
33
|
+
body?: unknown;
|
|
34
|
+
}): Promise<ResponseWithHeaders>;
|
|
22
35
|
postNoAuth(path: string, body: unknown): Promise<unknown>;
|
|
23
36
|
patch(path: string, opts?: {
|
|
24
37
|
requireJwt?: boolean;
|
package/dist/client.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
export class FacesAPIError extends Error {
|
|
2
2
|
statusCode;
|
|
3
|
-
|
|
3
|
+
errorCode;
|
|
4
|
+
fallbackAvailable;
|
|
5
|
+
constructor(statusCode, message, opts) {
|
|
4
6
|
super(message);
|
|
5
7
|
this.statusCode = statusCode;
|
|
6
8
|
this.name = 'FacesAPIError';
|
|
9
|
+
this.errorCode = opts?.errorCode;
|
|
10
|
+
this.fallbackAvailable = opts?.fallbackAvailable;
|
|
7
11
|
}
|
|
8
12
|
}
|
|
9
13
|
export class FacesClient {
|
|
@@ -40,14 +44,24 @@ export class FacesClient {
|
|
|
40
44
|
}
|
|
41
45
|
async parseError(resp) {
|
|
42
46
|
let msg = resp.statusText;
|
|
47
|
+
let errorCode;
|
|
48
|
+
let fallbackAvailable;
|
|
43
49
|
try {
|
|
44
50
|
const body = await resp.json();
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
51
|
+
// Structured OAuth rejection (422)
|
|
52
|
+
if (body.error === 'oauth_rejected') {
|
|
53
|
+
errorCode = 'oauth_rejected';
|
|
54
|
+
fallbackAvailable = body.fallback_available;
|
|
55
|
+
msg = String(body.detail ?? 'OAuth request rejected');
|
|
48
56
|
}
|
|
49
57
|
else {
|
|
50
|
-
|
|
58
|
+
const raw = body.detail ?? body.error ?? body.message ?? msg;
|
|
59
|
+
if (typeof raw === 'object' && raw !== null && 'message' in raw) {
|
|
60
|
+
msg = String(raw.message);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
msg = typeof raw === 'object' ? JSON.stringify(raw) : String(raw);
|
|
64
|
+
}
|
|
51
65
|
}
|
|
52
66
|
}
|
|
53
67
|
catch {
|
|
@@ -57,7 +71,7 @@ export class FacesClient {
|
|
|
57
71
|
msg = `Payment required: ${msg}`;
|
|
58
72
|
if (resp.status === 403)
|
|
59
73
|
msg = `Forbidden: ${msg}`;
|
|
60
|
-
return new FacesAPIError(resp.status, msg);
|
|
74
|
+
return new FacesAPIError(resp.status, msg, { errorCode, fallbackAvailable });
|
|
61
75
|
}
|
|
62
76
|
async get(path, opts = {}) {
|
|
63
77
|
let url = this.url(path);
|
|
@@ -91,6 +105,26 @@ export class FacesClient {
|
|
|
91
105
|
throw await this.parseError(resp);
|
|
92
106
|
return resp.json();
|
|
93
107
|
}
|
|
108
|
+
async postWithHeaders(path, opts = {}) {
|
|
109
|
+
let resp;
|
|
110
|
+
try {
|
|
111
|
+
resp = await fetch(this.url(path), {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: { ...this.authHeader(opts.requireJwt), 'Content-Type': 'application/json' },
|
|
114
|
+
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
this.wrapNetworkError(err, path);
|
|
119
|
+
}
|
|
120
|
+
if (!resp.ok)
|
|
121
|
+
throw await this.parseError(resp);
|
|
122
|
+
const headers = {};
|
|
123
|
+
for (const [k, v] of resp.headers.entries())
|
|
124
|
+
headers[k] = v;
|
|
125
|
+
const data = await resp.json();
|
|
126
|
+
return { data, headers };
|
|
127
|
+
}
|
|
94
128
|
async postNoAuth(path, body) {
|
|
95
129
|
let resp;
|
|
96
130
|
try {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base.js';
|
|
2
|
+
export default class AccountPreferences 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 args: {
|
|
10
|
+
key: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
11
|
+
value: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
12
|
+
};
|
|
13
|
+
static examples: string[];
|
|
14
|
+
run(): Promise<unknown>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../base.js';
|
|
3
|
+
import { FacesAPIError } from '../../client.js';
|
|
4
|
+
export default class AccountPreferences extends BaseCommand {
|
|
5
|
+
static description = 'View or update account preferences (api_fallback, default_model)';
|
|
6
|
+
static flags = {
|
|
7
|
+
...BaseCommand.baseFlags,
|
|
8
|
+
};
|
|
9
|
+
static args = {
|
|
10
|
+
key: Args.string({ description: 'Preference key to update (api_fallback, default_model)' }),
|
|
11
|
+
value: Args.string({ description: 'New value' }),
|
|
12
|
+
};
|
|
13
|
+
static examples = [
|
|
14
|
+
'faces account:preferences',
|
|
15
|
+
'faces account:preferences default_model gpt-5.4',
|
|
16
|
+
'faces account:preferences api_fallback true',
|
|
17
|
+
];
|
|
18
|
+
async run() {
|
|
19
|
+
const { args, flags } = await this.parse(AccountPreferences);
|
|
20
|
+
const client = this.makeClient(flags);
|
|
21
|
+
// If no key provided, show current preferences
|
|
22
|
+
if (!args.key) {
|
|
23
|
+
let data;
|
|
24
|
+
try {
|
|
25
|
+
data = await client.get('/v1/user/preferences');
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
if (err instanceof FacesAPIError)
|
|
29
|
+
this.error(`Error (${err.statusCode}): ${err.message}`);
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
if (!this.jsonEnabled())
|
|
33
|
+
this.printHuman(data);
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
// Update a preference
|
|
37
|
+
if (!args.value)
|
|
38
|
+
this.error('Provide a value to set.');
|
|
39
|
+
const validKeys = ['api_fallback', 'default_model'];
|
|
40
|
+
if (!validKeys.includes(args.key)) {
|
|
41
|
+
this.error(`Unknown preference '${args.key}'. Valid keys: ${validKeys.join(', ')}`);
|
|
42
|
+
}
|
|
43
|
+
let parsedValue = args.value;
|
|
44
|
+
if (args.key === 'api_fallback') {
|
|
45
|
+
if (args.value !== 'true' && args.value !== 'false') {
|
|
46
|
+
this.error('api_fallback must be "true" or "false"');
|
|
47
|
+
}
|
|
48
|
+
parsedValue = args.value === 'true';
|
|
49
|
+
}
|
|
50
|
+
let data;
|
|
51
|
+
try {
|
|
52
|
+
data = await client.patch('/v1/user/preferences', { body: { [args.key]: parsedValue } });
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
if (err instanceof FacesAPIError)
|
|
56
|
+
this.error(`Error (${err.statusCode}): ${err.message}`);
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
if (!this.jsonEnabled()) {
|
|
60
|
+
this.log(`Set ${args.key} = ${args.value}`);
|
|
61
|
+
this.printHuman(data);
|
|
62
|
+
}
|
|
63
|
+
return data;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base.js';
|
|
2
|
+
export default class CatalogBackup 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
|
+
run(): Promise<unknown>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { BaseCommand } from '../../base.js';
|
|
5
|
+
import { FacesAPIError } from '../../client.js';
|
|
6
|
+
import { flattenBasicFacts } from '../../utils.js';
|
|
7
|
+
export default class CatalogBackup extends BaseCommand {
|
|
8
|
+
static description = 'Snapshot all faces and source material for migration';
|
|
9
|
+
static flags = {
|
|
10
|
+
...BaseCommand.baseFlags,
|
|
11
|
+
};
|
|
12
|
+
async run() {
|
|
13
|
+
const { flags } = await this.parse(CatalogBackup);
|
|
14
|
+
const client = this.makeClient(flags);
|
|
15
|
+
const json = this.jsonEnabled();
|
|
16
|
+
// Fetch all faces
|
|
17
|
+
let faces;
|
|
18
|
+
try {
|
|
19
|
+
const resp = await client.get('/v1/faces');
|
|
20
|
+
faces = (resp.data ?? resp);
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
if (err instanceof FacesAPIError)
|
|
24
|
+
this.error(`Error (${err.statusCode}): ${err.message}`);
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
if (!json)
|
|
28
|
+
process.stderr.write(`Found ${faces.length} face(s)\n`);
|
|
29
|
+
const backupFaces = [];
|
|
30
|
+
for (const face of faces) {
|
|
31
|
+
const alias = face.alias;
|
|
32
|
+
if (!json)
|
|
33
|
+
process.stderr.write(` ${alias}: `);
|
|
34
|
+
const entry = {
|
|
35
|
+
alias,
|
|
36
|
+
name: face.name,
|
|
37
|
+
basic_facts: face.basic_facts ? flattenBasicFacts(face.basic_facts) : null,
|
|
38
|
+
default_model: face.default_model ?? null,
|
|
39
|
+
default_tools: face.default_tools ?? [],
|
|
40
|
+
formula: face.formula ?? null,
|
|
41
|
+
documents: [],
|
|
42
|
+
threads: [],
|
|
43
|
+
};
|
|
44
|
+
// Fetch documents
|
|
45
|
+
try {
|
|
46
|
+
const docs = await client.get('/v1/compile/documents', { params: { alias } });
|
|
47
|
+
for (const doc of docs) {
|
|
48
|
+
entry.documents.push({
|
|
49
|
+
label: doc.label ?? null,
|
|
50
|
+
content: doc.content,
|
|
51
|
+
perspective: doc.perspective ?? null,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// face may have no documents
|
|
57
|
+
}
|
|
58
|
+
// Fetch threads
|
|
59
|
+
try {
|
|
60
|
+
const threads = await client.get('/v1/compile/threads', { params: { alias } });
|
|
61
|
+
for (const thread of threads) {
|
|
62
|
+
const threadId = thread.thread_id;
|
|
63
|
+
try {
|
|
64
|
+
const full = await client.get(`/v1/compile/threads/${threadId}`);
|
|
65
|
+
const messages = full.messages ?? [];
|
|
66
|
+
entry.threads.push({
|
|
67
|
+
label: thread.label ?? null,
|
|
68
|
+
messages,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// skip threads we can't fetch
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// face may have no threads
|
|
78
|
+
}
|
|
79
|
+
if (!json)
|
|
80
|
+
process.stderr.write(`${entry.documents.length} doc(s), ${entry.threads.length} thread(s)\n`);
|
|
81
|
+
backupFaces.push(entry);
|
|
82
|
+
}
|
|
83
|
+
const backup = {
|
|
84
|
+
version: 1,
|
|
85
|
+
created_at: new Date().toISOString(),
|
|
86
|
+
faces: backupFaces,
|
|
87
|
+
};
|
|
88
|
+
const backupDir = path.join(os.homedir(), '.faces', 'backups');
|
|
89
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
90
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
91
|
+
const filePath = path.join(backupDir, `${timestamp}.json`);
|
|
92
|
+
fs.writeFileSync(filePath, JSON.stringify(backup, null, 2));
|
|
93
|
+
const totalDocs = backupFaces.reduce((n, f) => n + f.documents.length, 0);
|
|
94
|
+
const totalThreads = backupFaces.reduce((n, f) => n + f.threads.length, 0);
|
|
95
|
+
if (!json) {
|
|
96
|
+
this.log(`\nBackup saved: ${filePath}`);
|
|
97
|
+
this.log(` ${backupFaces.length} face(s), ${totalDocs} document(s), ${totalThreads} thread(s)`);
|
|
98
|
+
}
|
|
99
|
+
return { path: filePath, faces: backupFaces.length, documents: totalDocs, threads: totalThreads };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base.js';
|
|
2
|
+
export default class CatalogRestore extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static flags: {
|
|
5
|
+
compile: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
6
|
+
'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
};
|
|
10
|
+
static args: {
|
|
11
|
+
file: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<unknown>;
|
|
14
|
+
private createFace;
|
|
15
|
+
private findLatestBackup;
|
|
16
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { Args, Flags } from '@oclif/core';
|
|
5
|
+
import { BaseCommand } from '../../base.js';
|
|
6
|
+
import { FacesAPIError } from '../../client.js';
|
|
7
|
+
import { CatalogService } from '../../catalog.js';
|
|
8
|
+
export default class CatalogRestore extends BaseCommand {
|
|
9
|
+
static description = 'Restore faces and source material from a backup snapshot';
|
|
10
|
+
static flags = {
|
|
11
|
+
...BaseCommand.baseFlags,
|
|
12
|
+
compile: Flags.boolean({ description: 'Run compile:all after restoring', default: false }),
|
|
13
|
+
};
|
|
14
|
+
static args = {
|
|
15
|
+
file: Args.string({ description: 'Backup file path (default: most recent in ~/.faces/backups/)' }),
|
|
16
|
+
};
|
|
17
|
+
async run() {
|
|
18
|
+
const { args, flags } = await this.parse(CatalogRestore);
|
|
19
|
+
const client = this.makeClient(flags);
|
|
20
|
+
const json = this.jsonEnabled();
|
|
21
|
+
const filePath = args.file ?? this.findLatestBackup();
|
|
22
|
+
if (!fs.existsSync(filePath))
|
|
23
|
+
this.error(`Backup file not found: ${filePath}`);
|
|
24
|
+
const backup = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
25
|
+
if (backup.version !== 1)
|
|
26
|
+
this.error(`Unsupported backup version: ${backup.version}`);
|
|
27
|
+
if (!json) {
|
|
28
|
+
this.log(`Restoring from: ${filePath}`);
|
|
29
|
+
this.log(` Created: ${backup.created_at}`);
|
|
30
|
+
this.log(` Faces: ${backup.faces.length}\n`);
|
|
31
|
+
}
|
|
32
|
+
const catalog = new CatalogService();
|
|
33
|
+
let facesCreated = 0;
|
|
34
|
+
let facesSkipped = 0;
|
|
35
|
+
let docsUploaded = 0;
|
|
36
|
+
let threadsUploaded = 0;
|
|
37
|
+
for (const face of backup.faces) {
|
|
38
|
+
if (!json)
|
|
39
|
+
process.stderr.write(`${face.alias}: `);
|
|
40
|
+
// Create face
|
|
41
|
+
const created = await this.createFace(client, face, json);
|
|
42
|
+
if (!created) {
|
|
43
|
+
facesSkipped++;
|
|
44
|
+
if (!json)
|
|
45
|
+
process.stderr.write('exists, skipping\n');
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
facesCreated++;
|
|
49
|
+
// Write to local catalog
|
|
50
|
+
try {
|
|
51
|
+
catalog.writeFace({ alias: face.alias, name: face.name, basic_facts: face.basic_facts });
|
|
52
|
+
}
|
|
53
|
+
catch { /* non-fatal */ }
|
|
54
|
+
// Upload documents
|
|
55
|
+
for (const doc of face.documents) {
|
|
56
|
+
try {
|
|
57
|
+
const payload = { alias: face.alias, content: doc.content };
|
|
58
|
+
if (doc.label)
|
|
59
|
+
payload.label = doc.label;
|
|
60
|
+
if (doc.perspective)
|
|
61
|
+
payload.perspective = doc.perspective;
|
|
62
|
+
await client.post('/v1/compile/documents', { body: payload });
|
|
63
|
+
docsUploaded++;
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
const msg = err instanceof FacesAPIError ? `${err.statusCode}: ${err.message}` : String(err);
|
|
67
|
+
if (!json)
|
|
68
|
+
process.stderr.write(`\n warn: doc upload failed: ${msg}\n `);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Upload threads — create thread then PATCH messages in bulk
|
|
72
|
+
for (const thread of face.threads) {
|
|
73
|
+
try {
|
|
74
|
+
const createPayload = { alias: face.alias };
|
|
75
|
+
if (thread.label)
|
|
76
|
+
createPayload.label = thread.label;
|
|
77
|
+
const created = await client.post('/v1/compile/threads', { body: createPayload });
|
|
78
|
+
const threadId = created.thread_id;
|
|
79
|
+
// Overwrite the auto-generated messages with the backup messages
|
|
80
|
+
if (thread.messages.length > 0) {
|
|
81
|
+
await client.patch(`/v1/compile/threads/${threadId}/messages`, {
|
|
82
|
+
body: { messages: thread.messages },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
threadsUploaded++;
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
const msg = err instanceof FacesAPIError ? `${err.statusCode}: ${err.message}` : String(err);
|
|
89
|
+
if (!json)
|
|
90
|
+
process.stderr.write(`\n warn: thread upload failed: ${msg}\n `);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!json)
|
|
94
|
+
process.stderr.write(`${face.documents.length} doc(s), ${face.threads.length} thread(s)\n`);
|
|
95
|
+
}
|
|
96
|
+
if (!json) {
|
|
97
|
+
this.log(`\nRestore complete:`);
|
|
98
|
+
this.log(` Faces created: ${facesCreated}, skipped: ${facesSkipped}`);
|
|
99
|
+
this.log(` Documents uploaded: ${docsUploaded}`);
|
|
100
|
+
this.log(` Threads uploaded: ${threadsUploaded}`);
|
|
101
|
+
}
|
|
102
|
+
if (flags.compile) {
|
|
103
|
+
if (!json)
|
|
104
|
+
this.log('\nRunning compile:all...\n');
|
|
105
|
+
await this.config.runCommand('compile:all', json ? ['--json'] : []);
|
|
106
|
+
}
|
|
107
|
+
return { faces_created: facesCreated, faces_skipped: facesSkipped, documents: docsUploaded, threads: threadsUploaded };
|
|
108
|
+
}
|
|
109
|
+
async createFace(client, face, _json) {
|
|
110
|
+
const payload = { name: face.name, alias: face.alias };
|
|
111
|
+
if (face.formula) {
|
|
112
|
+
payload.formula = face.formula;
|
|
113
|
+
if (face.default_model)
|
|
114
|
+
payload.default_model = face.default_model;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
if (face.basic_facts && Object.keys(face.basic_facts).length > 0)
|
|
118
|
+
payload.basic_facts = face.basic_facts;
|
|
119
|
+
if (face.default_tools && face.default_tools.length > 0)
|
|
120
|
+
payload.default_tools = face.default_tools;
|
|
121
|
+
if (face.default_model)
|
|
122
|
+
payload.default_model = face.default_model;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
await client.post('/v1/faces', { body: payload });
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
if (err instanceof FacesAPIError && err.statusCode === 400) {
|
|
130
|
+
// likely "already have a face with alias"
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
findLatestBackup() {
|
|
137
|
+
const backupDir = path.join(os.homedir(), '.faces', 'backups');
|
|
138
|
+
if (!fs.existsSync(backupDir))
|
|
139
|
+
this.error('No backups found in ~/.faces/backups/');
|
|
140
|
+
const files = fs.readdirSync(backupDir)
|
|
141
|
+
.filter(f => f.endsWith('.json'))
|
|
142
|
+
.sort();
|
|
143
|
+
if (files.length === 0)
|
|
144
|
+
this.error('No backups found in ~/.faces/backups/');
|
|
145
|
+
return path.join(backupDir, files[files.length - 1]);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -10,6 +10,7 @@ export default class ChatChat extends BaseCommand {
|
|
|
10
10
|
temperature: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
11
|
file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
12
|
responses: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
'oauth-only': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
14
|
'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
15
|
token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
16
|
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -21,4 +22,5 @@ export default class ChatChat extends BaseCommand {
|
|
|
21
22
|
private runChatCompletions;
|
|
22
23
|
private runMessages;
|
|
23
24
|
private runResponses;
|
|
25
|
+
private extractText;
|
|
24
26
|
}
|
|
@@ -20,6 +20,7 @@ export default class ChatChat extends BaseCommand {
|
|
|
20
20
|
temperature: Flags.string({ description: 'Temperature (0.0-2.0)' }),
|
|
21
21
|
file: Flags.string({ description: 'Read message from file' }),
|
|
22
22
|
responses: Flags.boolean({ description: 'Use OpenAI Responses API instead of Chat Completions', default: false }),
|
|
23
|
+
'oauth-only': Flags.boolean({ description: 'Prevent fallback to paid system key (use OAuth only)', default: false }),
|
|
23
24
|
};
|
|
24
25
|
static args = {
|
|
25
26
|
face_username: Args.string({ description: 'Face alias (or alias@model)', required: true }),
|
|
@@ -64,8 +65,15 @@ export default class ChatChat extends BaseCommand {
|
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
catch (err) {
|
|
67
|
-
if (err instanceof FacesAPIError)
|
|
68
|
+
if (err instanceof FacesAPIError) {
|
|
69
|
+
if (err.errorCode === 'oauth_rejected') {
|
|
70
|
+
const hint = err.fallbackAvailable
|
|
71
|
+
? 'Run `faces account:preferences api_fallback true` to allow paid fallback.'
|
|
72
|
+
: 'Run `faces billing:topup` to add credits, then enable fallback with `faces account:preferences api_fallback true`.';
|
|
73
|
+
this.error(`OAuth failed: ${err.message}\n${hint}`);
|
|
74
|
+
}
|
|
68
75
|
this.error(`Error (${err.statusCode}): ${err.message}`);
|
|
76
|
+
}
|
|
69
77
|
throw err;
|
|
70
78
|
}
|
|
71
79
|
}
|
|
@@ -80,6 +88,8 @@ export default class ChatChat extends BaseCommand {
|
|
|
80
88
|
payload.max_tokens = flags['max-tokens'];
|
|
81
89
|
if (flags.temperature !== undefined)
|
|
82
90
|
payload.temperature = Number.parseFloat(flags.temperature);
|
|
91
|
+
if (flags['oauth-only'])
|
|
92
|
+
payload.oauth_only = true;
|
|
83
93
|
if (flags.stream) {
|
|
84
94
|
for await (const line of client.stream('/v1/chat/completions', payload)) {
|
|
85
95
|
if (!line.startsWith('data: '))
|
|
@@ -99,13 +109,25 @@ export default class ChatChat extends BaseCommand {
|
|
|
99
109
|
process.stdout.write('\n');
|
|
100
110
|
return {};
|
|
101
111
|
}
|
|
102
|
-
const data =
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
this.
|
|
108
|
-
|
|
112
|
+
const { data, headers } = await client.postWithHeaders('/v1/chat/completions', { body: payload });
|
|
113
|
+
const body = data;
|
|
114
|
+
const routedEndpoint = headers['x-faces-routed-endpoint'];
|
|
115
|
+
// Extract text based on which format the backend used
|
|
116
|
+
const text = this.extractText(body, routedEndpoint);
|
|
117
|
+
if (this.jsonEnabled()) {
|
|
118
|
+
const meta = {};
|
|
119
|
+
if (headers['x-faces-provider'])
|
|
120
|
+
meta.provider = headers['x-faces-provider'];
|
|
121
|
+
if (headers['x-faces-cost-usd'])
|
|
122
|
+
meta.cost_usd = headers['x-faces-cost-usd'];
|
|
123
|
+
if (routedEndpoint)
|
|
124
|
+
meta.routed_endpoint = routedEndpoint;
|
|
125
|
+
if (Object.keys(meta).length > 0)
|
|
126
|
+
body._meta = meta;
|
|
127
|
+
return body;
|
|
128
|
+
}
|
|
129
|
+
this.log(text);
|
|
130
|
+
return body;
|
|
109
131
|
}
|
|
110
132
|
async runMessages(client, model, userMessages, flags) {
|
|
111
133
|
const msgs = userMessages.map((m) => ({ role: 'user', content: m }));
|
|
@@ -117,6 +139,8 @@ export default class ChatChat extends BaseCommand {
|
|
|
117
139
|
};
|
|
118
140
|
if (flags.system)
|
|
119
141
|
payload.system = flags.system;
|
|
142
|
+
if (flags['oauth-only'])
|
|
143
|
+
payload.oauth_only = true;
|
|
120
144
|
if (flags.stream) {
|
|
121
145
|
for await (const line of client.stream('/v1/messages', payload)) {
|
|
122
146
|
if (!line.startsWith('data: '))
|
|
@@ -137,21 +161,32 @@ export default class ChatChat extends BaseCommand {
|
|
|
137
161
|
process.stdout.write('\n');
|
|
138
162
|
return {};
|
|
139
163
|
}
|
|
140
|
-
const data =
|
|
141
|
-
|
|
142
|
-
|
|
164
|
+
const { data, headers } = await client.postWithHeaders('/v1/messages', { body: payload });
|
|
165
|
+
const body = data;
|
|
166
|
+
if (this.jsonEnabled()) {
|
|
167
|
+
const meta = {};
|
|
168
|
+
if (headers['x-faces-provider'])
|
|
169
|
+
meta.provider = headers['x-faces-provider'];
|
|
170
|
+
if (headers['x-faces-cost-usd'])
|
|
171
|
+
meta.cost_usd = headers['x-faces-cost-usd'];
|
|
172
|
+
if (Object.keys(meta).length > 0)
|
|
173
|
+
body._meta = meta;
|
|
174
|
+
return body;
|
|
175
|
+
}
|
|
143
176
|
let content = '';
|
|
144
|
-
for (const block of
|
|
177
|
+
for (const block of body.content ?? []) {
|
|
145
178
|
if (block.type === 'text')
|
|
146
179
|
content += String(block.text ?? '');
|
|
147
180
|
}
|
|
148
181
|
this.log(content);
|
|
149
|
-
return
|
|
182
|
+
return body;
|
|
150
183
|
}
|
|
151
184
|
async runResponses(client, model, input, flags) {
|
|
152
185
|
const payload = { model, input, stream: flags.stream };
|
|
153
186
|
if (flags.system)
|
|
154
187
|
payload.instructions = flags.system;
|
|
188
|
+
if (flags['oauth-only'])
|
|
189
|
+
payload.oauth_only = true;
|
|
155
190
|
if (flags.stream) {
|
|
156
191
|
for await (const line of client.stream('/v1/responses', payload)) {
|
|
157
192
|
if (!line.startsWith('data: '))
|
|
@@ -172,17 +207,52 @@ export default class ChatChat extends BaseCommand {
|
|
|
172
207
|
process.stdout.write('\n');
|
|
173
208
|
return {};
|
|
174
209
|
}
|
|
175
|
-
const data =
|
|
176
|
-
|
|
177
|
-
|
|
210
|
+
const { data, headers } = await client.postWithHeaders('/v1/responses', { body: payload });
|
|
211
|
+
const body = data;
|
|
212
|
+
if (this.jsonEnabled()) {
|
|
213
|
+
const meta = {};
|
|
214
|
+
if (headers['x-faces-provider'])
|
|
215
|
+
meta.provider = headers['x-faces-provider'];
|
|
216
|
+
if (headers['x-faces-cost-usd'])
|
|
217
|
+
meta.cost_usd = headers['x-faces-cost-usd'];
|
|
218
|
+
if (Object.keys(meta).length > 0)
|
|
219
|
+
body._meta = meta;
|
|
220
|
+
return body;
|
|
221
|
+
}
|
|
178
222
|
let outputText = '';
|
|
179
|
-
for (const item of
|
|
223
|
+
for (const item of body.output ?? []) {
|
|
180
224
|
for (const block of item.content ?? []) {
|
|
181
225
|
if (block.type === 'output_text')
|
|
182
226
|
outputText += String(block.text ?? '');
|
|
183
227
|
}
|
|
184
228
|
}
|
|
185
229
|
this.log(outputText);
|
|
186
|
-
return
|
|
230
|
+
return body;
|
|
231
|
+
}
|
|
232
|
+
extractText(body, routedEndpoint) {
|
|
233
|
+
if (routedEndpoint === '/v1/responses') {
|
|
234
|
+
// OpenAI Responses format
|
|
235
|
+
let text = '';
|
|
236
|
+
for (const item of body.output ?? []) {
|
|
237
|
+
for (const block of item.content ?? []) {
|
|
238
|
+
if (block.type === 'output_text')
|
|
239
|
+
text += String(block.text ?? '');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return text;
|
|
243
|
+
}
|
|
244
|
+
if (routedEndpoint === '/v1/messages') {
|
|
245
|
+
// Anthropic Messages format
|
|
246
|
+
let text = '';
|
|
247
|
+
for (const block of body.content ?? []) {
|
|
248
|
+
if (block.type === 'text')
|
|
249
|
+
text += String(block.text ?? '');
|
|
250
|
+
}
|
|
251
|
+
return text;
|
|
252
|
+
}
|
|
253
|
+
// Standard chat/completions format (default)
|
|
254
|
+
const choices = body.choices;
|
|
255
|
+
const message = choices?.[0]?.message;
|
|
256
|
+
return String(message?.content ?? '');
|
|
187
257
|
}
|
|
188
258
|
}
|
|
@@ -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
|
+
'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>;
|
|
11
12
|
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|