faces-cli 1.5.13 → 1.5.15

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.
@@ -11,7 +11,7 @@ export default class AuthRefresh extends BaseCommand {
11
11
  const client = this.makeClient(flags);
12
12
  let data;
13
13
  try {
14
- data = (await client.post('/v1/auth/refresh', { requireJwt: true }));
14
+ data = (await client.post('/auth/refresh', { requireJwt: true }));
15
15
  }
16
16
  catch (err) {
17
17
  if (err instanceof FacesAPIError)
@@ -15,9 +15,15 @@ export default class BillingUsage extends BaseCommand {
15
15
  async run() {
16
16
  const { flags } = await this.parse(BillingUsage);
17
17
  const client = this.makeClient(flags);
18
+ const groupByMap = {
19
+ api_key: 'api_key_id',
20
+ model: 'llm_model',
21
+ llm: 'llm_model',
22
+ date: 'date',
23
+ };
18
24
  const params = {};
19
25
  if (flags['group-by'])
20
- params.group_by = flags['group-by'];
26
+ params.group_by = groupByMap[flags['group-by']] ?? flags['group-by'];
21
27
  if (flags.from)
22
28
  params.from = flags.from;
23
29
  if (flags.to)
@@ -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
+ }
@@ -0,0 +1,11 @@
1
+ import { BaseCommand } from '../../base.js';
2
+ export default class CompileAll extends BaseCommand {
3
+ static description: string;
4
+ static flags: {
5
+ timeout: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
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
+ run(): Promise<unknown>;
11
+ }
@@ -0,0 +1,116 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../base.js';
3
+ import { FacesAPIError } from '../../client.js';
4
+ import { pollCompileProgress } from '../../poll.js';
5
+ export default class CompileAll extends BaseCommand {
6
+ static description = 'Compile all uncompiled documents and threads across all faces';
7
+ static flags = {
8
+ ...BaseCommand.baseFlags,
9
+ timeout: Flags.integer({ description: 'Per-item compile timeout in seconds (default: 600)', default: 600 }),
10
+ };
11
+ async run() {
12
+ const { flags } = await this.parse(CompileAll);
13
+ const client = this.makeClient(flags);
14
+ const json = this.jsonEnabled();
15
+ // Fetch all faces
16
+ let faces;
17
+ try {
18
+ const resp = await client.get('/v1/faces');
19
+ faces = (resp.data ?? resp);
20
+ }
21
+ catch (err) {
22
+ if (err instanceof FacesAPIError)
23
+ this.error(`Error (${err.statusCode}): ${err.message}`);
24
+ throw err;
25
+ }
26
+ // Collect uncompiled items
27
+ const items = [];
28
+ for (const face of faces) {
29
+ const alias = face.alias;
30
+ if (face.formula)
31
+ continue; // composite faces have no source material
32
+ // Check documents
33
+ try {
34
+ const docs = await client.get('/v1/compile/documents', { params: { alias } });
35
+ for (const doc of docs) {
36
+ const status = doc.prepare_status;
37
+ if (status !== 'synced' && status !== 'ready') {
38
+ items.push({
39
+ type: 'document',
40
+ id: doc.document_id,
41
+ alias,
42
+ label: doc.label ?? null,
43
+ });
44
+ }
45
+ }
46
+ }
47
+ catch { /* no docs */ }
48
+ // Check threads
49
+ try {
50
+ const threads = await client.get('/v1/compile/threads', { params: { alias } });
51
+ for (const thread of threads) {
52
+ const status = thread.prepare_status;
53
+ if (status !== 'synced' && status !== 'ready') {
54
+ items.push({
55
+ type: 'thread',
56
+ id: thread.thread_id,
57
+ alias,
58
+ label: thread.label ?? null,
59
+ });
60
+ }
61
+ }
62
+ }
63
+ catch { /* no threads */ }
64
+ }
65
+ if (items.length === 0) {
66
+ if (!json)
67
+ this.log('Nothing to compile — all documents and threads are up to date.');
68
+ return { compiled: 0, failed: 0, items: [] };
69
+ }
70
+ if (!json)
71
+ this.log(`Found ${items.length} item(s) to compile\n`);
72
+ let compiled = 0;
73
+ let failed = 0;
74
+ const results = [];
75
+ for (let i = 0; i < items.length; i++) {
76
+ const item = items[i];
77
+ const tag = `[${i + 1}/${items.length}]`;
78
+ const name = item.label ?? item.id.slice(0, 8);
79
+ if (!json)
80
+ process.stderr.write(`${tag} ${item.type} "${name}" (${item.alias})... `);
81
+ const makeEndpoint = item.type === 'document'
82
+ ? `/v1/compile/documents/${item.id}/make`
83
+ : `/v1/compile/threads/${item.id}/make`;
84
+ const pollEndpoint = item.type === 'document'
85
+ ? `/v1/compile/documents/${item.id}`
86
+ : `/v1/compile/threads/${item.id}`;
87
+ try {
88
+ await client.post(makeEndpoint);
89
+ await pollCompileProgress(client, item.id, {
90
+ timeoutMs: flags.timeout * 1000,
91
+ endpoint: pollEndpoint,
92
+ onProgress: (p) => {
93
+ if (!json) {
94
+ process.stderr.write(`[${p.chunks_completed ?? '?'}/${p.chunks_total ?? '?'}] `);
95
+ }
96
+ },
97
+ });
98
+ compiled++;
99
+ results.push({ ...item, status: 'compiled' });
100
+ if (!json)
101
+ process.stderr.write('done\n');
102
+ }
103
+ catch (err) {
104
+ failed++;
105
+ const msg = err instanceof FacesAPIError ? `${err.statusCode}: ${err.message}` : err instanceof Error ? err.message : String(err);
106
+ results.push({ ...item, status: `failed: ${msg}` });
107
+ if (!json)
108
+ process.stderr.write(`failed: ${msg}\n`);
109
+ }
110
+ }
111
+ if (!json) {
112
+ this.log(`\nCompile complete: ${compiled} succeeded, ${failed} failed`);
113
+ }
114
+ return { compiled, failed, items: results };
115
+ }
116
+ }
@@ -25,6 +25,8 @@ export default class FaceCreate extends BaseCommand {
25
25
  const payload = { name: flags.name, alias: flags.alias };
26
26
  if (flags.formula) {
27
27
  payload.formula = flags.formula;
28
+ if (flags['default-model'])
29
+ payload.default_model = flags['default-model'];
28
30
  }
29
31
  else {
30
32
  const parsedAttrs = {};
@@ -25,7 +25,7 @@ export default class KeysCreate extends BaseCommand {
25
25
  payload.allowed_models = flags.model;
26
26
  let data;
27
27
  try {
28
- data = await client.post('/v1/api-keys', { requireJwt: true, body: payload });
28
+ data = await client.post('/v1/auth/api-keys', { requireJwt: true, body: payload });
29
29
  }
30
30
  catch (err) {
31
31
  if (err instanceof FacesAPIError)
@@ -10,7 +10,7 @@ export default class KeysList extends BaseCommand {
10
10
  const client = this.makeClient(flags);
11
11
  let data;
12
12
  try {
13
- data = await client.get('/v1/api-keys', { requireJwt: true });
13
+ data = await client.get('/v1/auth/api-keys', { requireJwt: true });
14
14
  }
15
15
  catch (err) {
16
16
  if (err instanceof FacesAPIError)
@@ -21,7 +21,7 @@ export default class KeysRevoke extends BaseCommand {
21
21
  }
22
22
  let data;
23
23
  try {
24
- data = await client.delete(`/v1/api-keys/${args.key_id}`, { requireJwt: true });
24
+ data = await client.delete(`/v1/auth/api-keys/${args.key_id}`, { requireJwt: true });
25
25
  }
26
26
  catch (err) {
27
27
  if (err instanceof FacesAPIError)
@@ -26,7 +26,7 @@ export default class KeysUpdate extends BaseCommand {
26
26
  this.error('Provide at least one field to update.');
27
27
  let data;
28
28
  try {
29
- data = await client.patch(`/v1/api-keys/${args.key_id}`, { requireJwt: true, body: payload });
29
+ data = await client.patch(`/v1/auth/api-keys/${args.key_id}`, { requireJwt: true, body: payload });
30
30
  }
31
31
  catch (err) {
32
32
  if (err instanceof FacesAPIError)