faces-cli 1.5.14 → 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.
- 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/compile/all.d.ts +11 -0
- package/dist/commands/compile/all.js +116 -0
- package/oclif.manifest.json +884 -706
- package/package.json +1 -1
|
@@ -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
|
+
}
|