faces-cli 1.6.1 → 1.6.3
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/README.md +50 -17
- package/dist/catalog.d.ts +13 -1
- package/dist/catalog.js +78 -9
- package/dist/client.d.ts +4 -0
- package/dist/client.js +16 -0
- package/dist/commands/catalog/backup.js +64 -18
- package/dist/commands/catalog/doctor.js +72 -16
- package/dist/commands/catalog/restore.js +64 -11
- package/dist/commands/face/create.d.ts +1 -0
- package/dist/commands/face/create.js +27 -5
- package/dist/commands/face/get.d.ts +1 -0
- package/dist/commands/face/get.js +13 -9
- package/dist/commands/face/list.d.ts +3 -0
- package/dist/commands/face/list.js +21 -6
- package/dist/commands/face/tag/add.d.ts +14 -0
- package/dist/commands/face/tag/add.js +40 -0
- package/dist/commands/face/tag/all.d.ts +10 -0
- package/dist/commands/face/tag/all.js +31 -0
- package/dist/commands/face/tag/list.d.ts +13 -0
- package/dist/commands/face/tag/list.js +35 -0
- package/dist/commands/face/tag/remove.d.ts +14 -0
- package/dist/commands/face/tag/remove.js +40 -0
- package/dist/commands/face/tag/set.d.ts +14 -0
- package/dist/commands/face/tag/set.js +40 -0
- package/dist/commands/face/teams.d.ts +14 -0
- package/dist/commands/face/teams.js +29 -0
- package/dist/commands/face/update.d.ts +1 -0
- package/dist/commands/face/update.js +34 -11
- package/dist/commands/team/add.d.ts +14 -0
- package/dist/commands/team/add.js +68 -0
- package/dist/commands/team/create.d.ts +15 -0
- package/dist/commands/team/create.js +69 -0
- package/dist/commands/team/delete.d.ts +15 -0
- package/dist/commands/team/delete.js +44 -0
- package/dist/commands/team/get.d.ts +13 -0
- package/dist/commands/team/get.js +28 -0
- package/dist/commands/team/list.d.ts +10 -0
- package/dist/commands/team/list.js +40 -0
- package/dist/commands/team/members.d.ts +13 -0
- package/dist/commands/team/members.js +28 -0
- package/dist/commands/team/remove.d.ts +14 -0
- package/dist/commands/team/remove.js +56 -0
- package/dist/commands/team/tag/add.d.ts +14 -0
- package/dist/commands/team/tag/add.js +29 -0
- package/dist/commands/team/tag/list.d.ts +13 -0
- package/dist/commands/team/tag/list.js +35 -0
- package/dist/commands/team/tag/remove.d.ts +14 -0
- package/dist/commands/team/tag/remove.js +29 -0
- package/dist/commands/team/tag/set.d.ts +14 -0
- package/dist/commands/team/tag/set.js +29 -0
- package/dist/commands/team/update.d.ts +17 -0
- package/dist/commands/team/update.js +78 -0
- package/dist/team-catalog.d.ts +17 -0
- package/dist/team-catalog.js +104 -0
- package/dist/utils.d.ts +9 -2
- package/dist/utils.js +17 -2
- package/oclif.manifest.json +1537 -309
- package/package.json +1 -1
|
@@ -5,8 +5,9 @@ import { BaseCommand } from '../../base.js';
|
|
|
5
5
|
import { FacesAPIError } from '../../client.js';
|
|
6
6
|
import { loadConfig } from '../../config.js';
|
|
7
7
|
import { CatalogService, CATALOG_DIR } from '../../catalog.js';
|
|
8
|
+
import { TeamCatalogService } from '../../team-catalog.js';
|
|
8
9
|
export default class CatalogDoctor extends BaseCommand {
|
|
9
|
-
static description = 'Diagnose and repair the local face catalog';
|
|
10
|
+
static description = 'Diagnose and repair the local face and team catalog';
|
|
10
11
|
static flags = {
|
|
11
12
|
...BaseCommand.baseFlags,
|
|
12
13
|
fix: Flags.boolean({ description: 'Rebuild missing or stale catalog entries from API', default: false }),
|
|
@@ -16,15 +17,17 @@ export default class CatalogDoctor extends BaseCommand {
|
|
|
16
17
|
const { flags } = await this.parse(CatalogDoctor);
|
|
17
18
|
const client = this.makeClient(flags);
|
|
18
19
|
const catalog = new CatalogService();
|
|
19
|
-
|
|
20
|
+
const teamCatalog = new TeamCatalogService();
|
|
21
|
+
// Fetch all faces from API (with tags)
|
|
20
22
|
let remoteFaces;
|
|
21
23
|
try {
|
|
22
|
-
const resp = await client.get('/v1/faces');
|
|
24
|
+
const resp = await client.get('/v1/faces?include=tags');
|
|
23
25
|
const arr = resp.data ?? resp;
|
|
24
|
-
//
|
|
26
|
+
// Fetch full details for each face (tags come from list, but need description etc.)
|
|
25
27
|
remoteFaces = await Promise.all(arr.map(async (f) => {
|
|
26
28
|
try {
|
|
27
|
-
|
|
29
|
+
const full = await client.get(`/v1/faces/${f.alias}?include=tags`);
|
|
30
|
+
return full;
|
|
28
31
|
}
|
|
29
32
|
catch {
|
|
30
33
|
return { ...f, uid: f.alias };
|
|
@@ -55,7 +58,6 @@ export default class CatalogDoctor extends BaseCommand {
|
|
|
55
58
|
const content = fs.readFileSync(faceMdPath, 'utf8');
|
|
56
59
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
57
60
|
if (match) {
|
|
58
|
-
// Quick parse for name and description
|
|
59
61
|
const fm = { alias: dirent.name };
|
|
60
62
|
for (const line of match[1].split('\n')) {
|
|
61
63
|
if (line.startsWith(' '))
|
|
@@ -78,7 +80,7 @@ export default class CatalogDoctor extends BaseCommand {
|
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
catch { /* continue with what we have */ }
|
|
81
|
-
// Compute diffs
|
|
83
|
+
// Compute face diffs
|
|
82
84
|
const missing = [];
|
|
83
85
|
const stale = [];
|
|
84
86
|
const noDescription = [];
|
|
@@ -101,9 +103,21 @@ export default class CatalogDoctor extends BaseCommand {
|
|
|
101
103
|
if (!remoteByAlias.has(alias))
|
|
102
104
|
orphaned.push(alias);
|
|
103
105
|
}
|
|
106
|
+
// Check teams
|
|
107
|
+
let remoteTeams = [];
|
|
108
|
+
const localTeamNames = new Set(teamCatalog.listTeams());
|
|
109
|
+
try {
|
|
110
|
+
const resp = await client.get('/v1/me/teams');
|
|
111
|
+
remoteTeams = (resp.data ?? resp)
|
|
112
|
+
.filter(t => t.kind !== 'personal');
|
|
113
|
+
}
|
|
114
|
+
catch { /* proceed */ }
|
|
115
|
+
const missingTeams = remoteTeams.filter(t => {
|
|
116
|
+
const slug = String(t.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
117
|
+
return !localTeamNames.has(slug);
|
|
118
|
+
});
|
|
104
119
|
const doFix = flags.fix || flags.generate;
|
|
105
120
|
if (!doFix) {
|
|
106
|
-
// Diagnostic mode
|
|
107
121
|
const issues = [];
|
|
108
122
|
if (missing.length > 0)
|
|
109
123
|
issues.push(`${missing.length} face(s) missing from catalog`);
|
|
@@ -113,6 +127,8 @@ export default class CatalogDoctor extends BaseCommand {
|
|
|
113
127
|
issues.push(`${orphaned.length} orphaned catalog entries (face deleted on server)`);
|
|
114
128
|
if (noDescription.length > 0)
|
|
115
129
|
issues.push(`${noDescription.length} face(s) without a description`);
|
|
130
|
+
if (missingTeams.length > 0)
|
|
131
|
+
issues.push(`${missingTeams.length} team(s) missing from local catalog`);
|
|
116
132
|
if (issues.length === 0) {
|
|
117
133
|
this.log('Catalog is healthy.');
|
|
118
134
|
}
|
|
@@ -125,28 +141,63 @@ export default class CatalogDoctor extends BaseCommand {
|
|
|
125
141
|
this.log('Run faces catalog:doctor --generate to also create missing descriptions');
|
|
126
142
|
}
|
|
127
143
|
}
|
|
128
|
-
return { missing: missing.length, stale: stale.length, orphaned: orphaned.length, noDescription: noDescription.length };
|
|
144
|
+
return { missing: missing.length, stale: stale.length, orphaned: orphaned.length, noDescription: noDescription.length, missingTeams: missingTeams.length };
|
|
129
145
|
}
|
|
130
|
-
// Fix mode: rebuild
|
|
146
|
+
// Fix mode: rebuild face entries
|
|
131
147
|
let fixed = 0;
|
|
132
148
|
for (const face of [...missing, ...stale]) {
|
|
133
149
|
catalog.writeFace(face);
|
|
134
150
|
fixed++;
|
|
135
151
|
}
|
|
136
|
-
//
|
|
152
|
+
// Refresh all existing faces (attributes, tags, description)
|
|
137
153
|
for (const [alias, remote] of remoteByAlias) {
|
|
138
154
|
if (!missing.some((f) => f.alias === alias) && !stale.some((f) => f.alias === alias)) {
|
|
139
|
-
// Face exists locally and name matches — still refresh attributes
|
|
140
155
|
catalog.writeFace(remote);
|
|
141
156
|
}
|
|
142
157
|
}
|
|
143
|
-
// Remove
|
|
158
|
+
// Remove orphaned faces
|
|
144
159
|
let removed = 0;
|
|
145
160
|
for (const alias of orphaned) {
|
|
146
161
|
catalog.deleteFace(alias);
|
|
147
162
|
removed++;
|
|
148
163
|
}
|
|
149
|
-
|
|
164
|
+
// Sync teams
|
|
165
|
+
let teamsSynced = 0;
|
|
166
|
+
// Build uid→alias map for member resolution
|
|
167
|
+
const uidToAlias = new Map();
|
|
168
|
+
for (const f of remoteFaces) {
|
|
169
|
+
if (f.uid)
|
|
170
|
+
uidToAlias.set(f.uid, f.alias);
|
|
171
|
+
}
|
|
172
|
+
for (const team of remoteTeams) {
|
|
173
|
+
const teamId = team.id;
|
|
174
|
+
const slug = String(team.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
175
|
+
// Fetch members and tags
|
|
176
|
+
let memberAliases = [];
|
|
177
|
+
try {
|
|
178
|
+
const membersResp = await client.get(`/v1/teams/${teamId}/members`);
|
|
179
|
+
memberAliases = (membersResp.data ?? []).map(m => {
|
|
180
|
+
const iamId = m.iam_id;
|
|
181
|
+
return uidToAlias.get(iamId) ?? iamId;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
catch { /* proceed */ }
|
|
185
|
+
let tags = [];
|
|
186
|
+
try {
|
|
187
|
+
const tagResp = await client.get(`/v1/teams/${teamId}/tags`);
|
|
188
|
+
tags = tagResp.tags ?? [];
|
|
189
|
+
}
|
|
190
|
+
catch { /* proceed */ }
|
|
191
|
+
teamCatalog.writeTeam(slug, {
|
|
192
|
+
id: teamId,
|
|
193
|
+
name: team.name,
|
|
194
|
+
description: team.description ?? undefined,
|
|
195
|
+
tags,
|
|
196
|
+
members: memberAliases,
|
|
197
|
+
}, team.protocol ?? undefined);
|
|
198
|
+
teamsSynced++;
|
|
199
|
+
}
|
|
200
|
+
this.log(`Fixed ${fixed} face(s), removed ${removed} orphan(s), synced ${teamsSynced} team(s).`);
|
|
150
201
|
// Generate mode: create missing descriptions
|
|
151
202
|
if (flags.generate && noDescription.length > 0) {
|
|
152
203
|
const cfg = loadConfig();
|
|
@@ -175,7 +226,12 @@ export default class CatalogDoctor extends BaseCommand {
|
|
|
175
226
|
text = resp.choices?.[0]?.message?.content;
|
|
176
227
|
}
|
|
177
228
|
if (text) {
|
|
178
|
-
|
|
229
|
+
const desc = text.trim();
|
|
230
|
+
catalog.writeFace({ ...remote, description: desc });
|
|
231
|
+
try {
|
|
232
|
+
await client.patch(`/v1/faces/${alias}`, { body: { description: desc } });
|
|
233
|
+
}
|
|
234
|
+
catch { /* non-fatal */ }
|
|
179
235
|
generated++;
|
|
180
236
|
this.log(` ${alias}: done`);
|
|
181
237
|
}
|
|
@@ -191,6 +247,6 @@ export default class CatalogDoctor extends BaseCommand {
|
|
|
191
247
|
this.log(`Generated ${generated} description(s).`);
|
|
192
248
|
}
|
|
193
249
|
catalog.rebuildIndex();
|
|
194
|
-
return { fixed, removed, generated: flags.generate ? noDescription.length : 0 };
|
|
250
|
+
return { fixed, removed, teamsSynced, generated: flags.generate ? noDescription.length : 0 };
|
|
195
251
|
}
|
|
196
252
|
}
|
|
@@ -6,7 +6,7 @@ import { BaseCommand } from '../../base.js';
|
|
|
6
6
|
import { FacesAPIError } from '../../client.js';
|
|
7
7
|
import { CatalogService } from '../../catalog.js';
|
|
8
8
|
export default class CatalogRestore extends BaseCommand {
|
|
9
|
-
static description = 'Restore faces and source material from a backup snapshot';
|
|
9
|
+
static description = 'Restore faces, teams, and source material from a backup snapshot';
|
|
10
10
|
static flags = {
|
|
11
11
|
...BaseCommand.baseFlags,
|
|
12
12
|
compile: Flags.boolean({ description: 'Run compile:all after restoring', default: false }),
|
|
@@ -22,23 +22,23 @@ export default class CatalogRestore extends BaseCommand {
|
|
|
22
22
|
if (!fs.existsSync(filePath))
|
|
23
23
|
this.error(`Backup file not found: ${filePath}`);
|
|
24
24
|
const backup = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
25
|
-
if (backup.version !== 1)
|
|
25
|
+
if (backup.version !== 1 && backup.version !== 2)
|
|
26
26
|
this.error(`Unsupported backup version: ${backup.version}`);
|
|
27
27
|
if (!json) {
|
|
28
28
|
this.log(`Restoring from: ${filePath}`);
|
|
29
29
|
this.log(` Created: ${backup.created_at}`);
|
|
30
|
-
this.log(` Faces: ${backup.faces.length}\n`);
|
|
30
|
+
this.log(` Faces: ${backup.faces.length}, Teams: ${backup.teams?.length ?? 0}\n`);
|
|
31
31
|
}
|
|
32
32
|
const catalog = new CatalogService();
|
|
33
33
|
let facesCreated = 0;
|
|
34
34
|
let facesSkipped = 0;
|
|
35
35
|
let docsUploaded = 0;
|
|
36
36
|
let threadsUploaded = 0;
|
|
37
|
+
let teamsCreated = 0;
|
|
37
38
|
for (const face of backup.faces) {
|
|
38
39
|
if (!json)
|
|
39
40
|
process.stderr.write(`${face.alias}: `);
|
|
40
|
-
|
|
41
|
-
const created = await this.createFace(client, face, json);
|
|
41
|
+
const created = await this.createFace(client, face);
|
|
42
42
|
if (!created) {
|
|
43
43
|
facesSkipped++;
|
|
44
44
|
if (!json)
|
|
@@ -46,9 +46,16 @@ export default class CatalogRestore extends BaseCommand {
|
|
|
46
46
|
continue;
|
|
47
47
|
}
|
|
48
48
|
facesCreated++;
|
|
49
|
+
// Post tags
|
|
50
|
+
if (face.tags && face.tags.length > 0) {
|
|
51
|
+
try {
|
|
52
|
+
await client.post(`/v1/iam/${face.alias}/tags`, { body: { tags: face.tags } });
|
|
53
|
+
}
|
|
54
|
+
catch { /* non-fatal */ }
|
|
55
|
+
}
|
|
49
56
|
// Write to local catalog
|
|
50
57
|
try {
|
|
51
|
-
catalog.writeFace({ alias: face.alias, name: face.name, basic_facts: face.basic_facts });
|
|
58
|
+
catalog.writeFace({ alias: face.alias, name: face.name, basic_facts: face.basic_facts }, face.description ?? undefined);
|
|
52
59
|
}
|
|
53
60
|
catch { /* non-fatal */ }
|
|
54
61
|
// Upload documents
|
|
@@ -68,7 +75,7 @@ export default class CatalogRestore extends BaseCommand {
|
|
|
68
75
|
process.stderr.write(`\n warn: doc upload failed: ${msg}\n `);
|
|
69
76
|
}
|
|
70
77
|
}
|
|
71
|
-
// Upload threads
|
|
78
|
+
// Upload threads
|
|
72
79
|
for (const thread of face.threads) {
|
|
73
80
|
try {
|
|
74
81
|
const createPayload = { alias: face.alias };
|
|
@@ -76,7 +83,6 @@ export default class CatalogRestore extends BaseCommand {
|
|
|
76
83
|
createPayload.label = thread.label;
|
|
77
84
|
const created = await client.post('/v1/compile/threads', { body: createPayload });
|
|
78
85
|
const threadId = created.thread_id;
|
|
79
|
-
// Overwrite the auto-generated messages with the backup messages
|
|
80
86
|
if (thread.messages.length > 0) {
|
|
81
87
|
await client.patch(`/v1/compile/threads/${threadId}/messages`, {
|
|
82
88
|
body: { messages: thread.messages },
|
|
@@ -93,21 +99,69 @@ export default class CatalogRestore extends BaseCommand {
|
|
|
93
99
|
if (!json)
|
|
94
100
|
process.stderr.write(`${face.documents.length} doc(s), ${face.threads.length} thread(s)\n`);
|
|
95
101
|
}
|
|
102
|
+
// Restore teams (v2+)
|
|
103
|
+
if (backup.teams && backup.teams.length > 0) {
|
|
104
|
+
if (!json)
|
|
105
|
+
this.log(`\nRestoring ${backup.teams.length} team(s)...`);
|
|
106
|
+
for (const team of backup.teams) {
|
|
107
|
+
if (!json)
|
|
108
|
+
process.stderr.write(` ${team.name}: `);
|
|
109
|
+
try {
|
|
110
|
+
const payload = { name: team.name };
|
|
111
|
+
if (team.description)
|
|
112
|
+
payload.description = team.description;
|
|
113
|
+
if (team.protocol)
|
|
114
|
+
payload.protocol = team.protocol;
|
|
115
|
+
const created = await client.post('/v1/teams', { body: payload });
|
|
116
|
+
const teamId = created.id;
|
|
117
|
+
// Add tags
|
|
118
|
+
if (team.tags.length > 0) {
|
|
119
|
+
try {
|
|
120
|
+
await client.post(`/v1/teams/${teamId}/tags`, { body: { tags: team.tags } });
|
|
121
|
+
}
|
|
122
|
+
catch { /* non-fatal */ }
|
|
123
|
+
}
|
|
124
|
+
// Add members (resolve aliases to iam_ids)
|
|
125
|
+
for (const alias of team.members) {
|
|
126
|
+
try {
|
|
127
|
+
const face = await client.get(`/v1/faces/${alias}`);
|
|
128
|
+
const iamId = (face.uid ?? face.id ?? face.iam_id);
|
|
129
|
+
if (iamId) {
|
|
130
|
+
await client.post(`/v1/teams/${teamId}/members`, { body: { iam_id: iamId } });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch { /* member may not exist yet */ }
|
|
134
|
+
}
|
|
135
|
+
teamsCreated++;
|
|
136
|
+
if (!json)
|
|
137
|
+
process.stderr.write(`${team.members.length} member(s)\n`);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
const msg = err instanceof FacesAPIError ? `${err.statusCode}: ${err.message}` : String(err);
|
|
141
|
+
if (!json)
|
|
142
|
+
process.stderr.write(`failed: ${msg}\n`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
96
146
|
if (!json) {
|
|
97
147
|
this.log(`\nRestore complete:`);
|
|
98
148
|
this.log(` Faces created: ${facesCreated}, skipped: ${facesSkipped}`);
|
|
99
149
|
this.log(` Documents uploaded: ${docsUploaded}`);
|
|
100
150
|
this.log(` Threads uploaded: ${threadsUploaded}`);
|
|
151
|
+
if (teamsCreated > 0)
|
|
152
|
+
this.log(` Teams created: ${teamsCreated}`);
|
|
101
153
|
}
|
|
102
154
|
if (flags.compile) {
|
|
103
155
|
if (!json)
|
|
104
156
|
this.log('\nRunning compile:all...\n');
|
|
105
157
|
await this.config.runCommand('compile:all', json ? ['--json'] : []);
|
|
106
158
|
}
|
|
107
|
-
return { faces_created: facesCreated, faces_skipped: facesSkipped, documents: docsUploaded, threads: threadsUploaded };
|
|
159
|
+
return { faces_created: facesCreated, faces_skipped: facesSkipped, documents: docsUploaded, threads: threadsUploaded, teams_created: teamsCreated };
|
|
108
160
|
}
|
|
109
|
-
async createFace(client, face
|
|
161
|
+
async createFace(client, face) {
|
|
110
162
|
const payload = { name: face.name, alias: face.alias };
|
|
163
|
+
if (face.description)
|
|
164
|
+
payload.description = face.description;
|
|
111
165
|
if (face.formula) {
|
|
112
166
|
payload.formula = face.formula;
|
|
113
167
|
if (face.default_model)
|
|
@@ -127,7 +181,6 @@ export default class CatalogRestore extends BaseCommand {
|
|
|
127
181
|
}
|
|
128
182
|
catch (err) {
|
|
129
183
|
if (err instanceof FacesAPIError && err.statusCode === 400) {
|
|
130
|
-
// likely "already have a face with alias"
|
|
131
184
|
return false;
|
|
132
185
|
}
|
|
133
186
|
throw err;
|
|
@@ -5,6 +5,7 @@ export default class FaceCreate extends BaseCommand {
|
|
|
5
5
|
name: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
6
|
alias: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
7
|
description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
tag: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
9
|
'default-model': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
formula: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
11
|
attr: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -2,14 +2,15 @@ import { Flags } from '@oclif/core';
|
|
|
2
2
|
import { BaseCommand } from '../../base.js';
|
|
3
3
|
import { FacesAPIError } from '../../client.js';
|
|
4
4
|
import { CatalogService } from '../../catalog.js';
|
|
5
|
-
import {
|
|
5
|
+
import { renameFaceFields } from '../../utils.js';
|
|
6
6
|
export default class FaceCreate extends BaseCommand {
|
|
7
7
|
static description = 'Create a new face';
|
|
8
8
|
static flags = {
|
|
9
9
|
...BaseCommand.baseFlags,
|
|
10
10
|
name: Flags.string({ description: 'Display name', required: true }),
|
|
11
11
|
alias: Flags.string({ description: 'Unique alias slug', required: true }),
|
|
12
|
-
description: Flags.string({ description: '
|
|
12
|
+
description: Flags.string({ description: 'Plain-text description / bio (max 1500 chars)' }),
|
|
13
|
+
tag: Flags.string({ description: 'Tag label (repeatable, lowercase)', multiple: true }),
|
|
13
14
|
'default-model': Flags.string({ description: 'Default LLM for chat (e.g. gpt-4o-mini, claude-sonnet-4-6)' }),
|
|
14
15
|
formula: Flags.string({ description: 'Boolean formula over owned concrete face aliases (e.g. "a | b", "(a | b) - c"). Creates a composite face.' }),
|
|
15
16
|
attr: Flags.string({ description: 'Attribute KEY=VALUE (repeatable)', multiple: true }),
|
|
@@ -23,6 +24,8 @@ export default class FaceCreate extends BaseCommand {
|
|
|
23
24
|
this.error('--formula cannot be combined with --attr or --tool. Composite faces do not have compiled knowledge.');
|
|
24
25
|
}
|
|
25
26
|
const payload = { name: flags.name, alias: flags.alias };
|
|
27
|
+
if (flags.description)
|
|
28
|
+
payload.description = flags.description;
|
|
26
29
|
if (flags.formula) {
|
|
27
30
|
payload.formula = flags.formula;
|
|
28
31
|
if (flags['default-model'])
|
|
@@ -57,12 +60,31 @@ export default class FaceCreate extends BaseCommand {
|
|
|
57
60
|
for (const w of face.warnings)
|
|
58
61
|
this.warn(w);
|
|
59
62
|
}
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
// Post tags if provided
|
|
64
|
+
const tags = flags.tag ?? [];
|
|
65
|
+
if (tags.length > 0) {
|
|
66
|
+
try {
|
|
67
|
+
await client.post(`/v1/iam/${flags.alias}/tags`, { body: { tags } });
|
|
68
|
+
face.tags = tags;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
if (err instanceof FacesAPIError)
|
|
72
|
+
this.warn(`Tags failed (${err.statusCode}): ${err.message}`);
|
|
73
|
+
}
|
|
62
74
|
}
|
|
75
|
+
// Rename basic_facts → attributes in response
|
|
76
|
+
renameFaceFields(face);
|
|
77
|
+
// Write to local catalog with tags
|
|
63
78
|
try {
|
|
64
79
|
const catalog = new CatalogService();
|
|
65
|
-
catalog.writeFace(
|
|
80
|
+
catalog.writeFace({
|
|
81
|
+
...face,
|
|
82
|
+
basic_facts: face.attributes,
|
|
83
|
+
tags: face.tags ?? undefined,
|
|
84
|
+
description: face.description ?? flags.description ?? undefined,
|
|
85
|
+
default_model: face.default_model,
|
|
86
|
+
formula: face.formula,
|
|
87
|
+
});
|
|
66
88
|
}
|
|
67
89
|
catch { /* catalog errors are non-fatal */ }
|
|
68
90
|
if (!this.jsonEnabled())
|
|
@@ -2,6 +2,7 @@ import { BaseCommand } from '../../base.js';
|
|
|
2
2
|
export default class FaceGet extends BaseCommand {
|
|
3
3
|
static description: string;
|
|
4
4
|
static flags: {
|
|
5
|
+
include: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
5
6
|
'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
7
|
token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
8
|
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { Args } from '@oclif/core';
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import { BaseCommand } from '../../base.js';
|
|
3
3
|
import { FacesAPIError } from '../../client.js';
|
|
4
|
-
import {
|
|
4
|
+
import { renameFaceFields } from '../../utils.js';
|
|
5
5
|
export default class FaceGet extends BaseCommand {
|
|
6
6
|
static description = 'Get details for a face by alias';
|
|
7
7
|
static flags = {
|
|
8
8
|
...BaseCommand.baseFlags,
|
|
9
|
+
include: Flags.string({ description: 'Include extra fields: tags, teams (comma-separated)' }),
|
|
9
10
|
};
|
|
10
11
|
static args = {
|
|
11
12
|
face_id: Args.string({ description: 'Face alias', required: true }),
|
|
@@ -13,18 +14,21 @@ export default class FaceGet extends BaseCommand {
|
|
|
13
14
|
async run() {
|
|
14
15
|
const { args, flags } = await this.parse(FaceGet);
|
|
15
16
|
const client = this.makeClient(flags);
|
|
17
|
+
const path = flags.include
|
|
18
|
+
? `/v1/faces/${args.face_id}?include=${encodeURIComponent(flags.include)}`
|
|
19
|
+
: `/v1/faces/${args.face_id}`;
|
|
16
20
|
let data;
|
|
17
21
|
try {
|
|
18
|
-
data = await client.get(
|
|
22
|
+
data = await client.get(path);
|
|
19
23
|
}
|
|
20
24
|
catch (err) {
|
|
21
25
|
if (err instanceof FacesAPIError)
|
|
22
26
|
this.error(`Error (${err.statusCode}): ${err.message}`);
|
|
23
27
|
throw err;
|
|
24
28
|
}
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
const face = data;
|
|
30
|
+
renameFaceFields(face);
|
|
31
|
+
const f = face;
|
|
28
32
|
if (!this.jsonEnabled()) {
|
|
29
33
|
this.log(`alias: ${f.alias}`);
|
|
30
34
|
this.log(`uid: ${f.uid}`);
|
|
@@ -37,9 +41,9 @@ export default class FaceGet extends BaseCommand {
|
|
|
37
41
|
}
|
|
38
42
|
else {
|
|
39
43
|
this.log(`type: concrete`);
|
|
40
|
-
if (f.
|
|
41
|
-
this.log('
|
|
42
|
-
for (const [k, v] of Object.entries(f.
|
|
44
|
+
if (f.attributes && Object.keys(f.attributes).length > 0) {
|
|
45
|
+
this.log('attributes:');
|
|
46
|
+
for (const [k, v] of Object.entries(f.attributes)) {
|
|
43
47
|
this.log(` ${k}: ${v}`);
|
|
44
48
|
}
|
|
45
49
|
}
|
|
@@ -2,6 +2,9 @@ import { BaseCommand } from '../../base.js';
|
|
|
2
2
|
export default class FaceList extends BaseCommand {
|
|
3
3
|
static description: string;
|
|
4
4
|
static flags: {
|
|
5
|
+
tag: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
|
+
team: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
include: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
5
8
|
'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
9
|
token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
10
|
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -1,29 +1,44 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
1
2
|
import { BaseCommand } from '../../base.js';
|
|
2
3
|
import { FacesAPIError } from '../../client.js';
|
|
3
|
-
import {
|
|
4
|
+
import { renameFaceFields } from '../../utils.js';
|
|
4
5
|
export default class FaceList extends BaseCommand {
|
|
5
6
|
static description = 'List all owned faces';
|
|
6
7
|
static flags = {
|
|
7
8
|
...BaseCommand.baseFlags,
|
|
9
|
+
tag: Flags.string({ description: 'Filter by tag (repeatable, AND logic)', multiple: true }),
|
|
10
|
+
team: Flags.string({ description: 'Filter by team ID (repeatable, OR logic)', multiple: true }),
|
|
11
|
+
include: Flags.string({ description: 'Include extra fields: tags, teams (comma-separated)' }),
|
|
8
12
|
};
|
|
9
13
|
async run() {
|
|
10
14
|
const { flags } = await this.parse(FaceList);
|
|
11
15
|
const client = this.makeClient(flags);
|
|
16
|
+
// Build query string manually for repeated params
|
|
17
|
+
const parts = [];
|
|
18
|
+
if (flags.tag && flags.tag.length > 0) {
|
|
19
|
+
for (const t of flags.tag)
|
|
20
|
+
parts.push(`tag=${encodeURIComponent(t)}`);
|
|
21
|
+
}
|
|
22
|
+
if (flags.team && flags.team.length > 0) {
|
|
23
|
+
for (const t of flags.team)
|
|
24
|
+
parts.push(`team_id=${encodeURIComponent(t)}`);
|
|
25
|
+
}
|
|
26
|
+
if (flags.include)
|
|
27
|
+
parts.push(`include=${encodeURIComponent(flags.include)}`);
|
|
12
28
|
let data;
|
|
13
29
|
try {
|
|
14
|
-
|
|
30
|
+
const path = parts.length > 0 ? `/v1/faces?${parts.join('&')}` : '/v1/faces';
|
|
31
|
+
data = await client.get(path);
|
|
15
32
|
}
|
|
16
33
|
catch (err) {
|
|
17
34
|
if (err instanceof FacesAPIError)
|
|
18
35
|
this.error(`Error (${err.statusCode}): ${err.message}`);
|
|
19
36
|
throw err;
|
|
20
37
|
}
|
|
21
|
-
//
|
|
38
|
+
// Rename basic_facts → attributes in response
|
|
22
39
|
const raw = data.data ?? data;
|
|
23
40
|
for (const f of raw) {
|
|
24
|
-
|
|
25
|
-
f.basic_facts = flattenBasicFacts(f.basic_facts);
|
|
26
|
-
}
|
|
41
|
+
renameFaceFields(f);
|
|
27
42
|
}
|
|
28
43
|
if (!this.jsonEnabled()) {
|
|
29
44
|
const faces = raw;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BaseCommand } from '../../../base.js';
|
|
2
|
+
export default class FaceTagAdd extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static flags: {
|
|
5
|
+
tag: import("@oclif/core/interfaces").OptionFlag<string[], 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
|
+
static args: {
|
|
11
|
+
alias: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<unknown>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../../base.js';
|
|
3
|
+
import { FacesAPIError } from '../../../client.js';
|
|
4
|
+
import { CatalogService } from '../../../catalog.js';
|
|
5
|
+
export default class FaceTagAdd extends BaseCommand {
|
|
6
|
+
static description = 'Add tags to a face (appends, idempotent)';
|
|
7
|
+
static flags = {
|
|
8
|
+
...BaseCommand.baseFlags,
|
|
9
|
+
tag: Flags.string({ description: 'Tag to add (repeatable)', multiple: true, required: true }),
|
|
10
|
+
};
|
|
11
|
+
static args = {
|
|
12
|
+
alias: Args.string({ description: 'Face alias', required: true }),
|
|
13
|
+
};
|
|
14
|
+
async run() {
|
|
15
|
+
const { args, flags } = await this.parse(FaceTagAdd);
|
|
16
|
+
const client = this.makeClient(flags);
|
|
17
|
+
let data;
|
|
18
|
+
try {
|
|
19
|
+
data = await client.post(`/v1/iam/${args.alias}/tags`, { body: { tags: flags.tag } });
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
if (err instanceof FacesAPIError) {
|
|
23
|
+
if (err.statusCode === 403) {
|
|
24
|
+
this.error(`Forbidden: face '${args.alias}' is not owned by the current account. Run \`faces catalog:doctor --fix\` to sync.`);
|
|
25
|
+
}
|
|
26
|
+
this.error(`Error (${err.statusCode}): ${err.message}`);
|
|
27
|
+
}
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
// Sync tags to local catalog
|
|
31
|
+
const tags = data.tags ?? [];
|
|
32
|
+
try {
|
|
33
|
+
new CatalogService().updateTags(args.alias, tags);
|
|
34
|
+
}
|
|
35
|
+
catch { /* non-fatal */ }
|
|
36
|
+
if (!this.jsonEnabled())
|
|
37
|
+
this.printHuman(data);
|
|
38
|
+
return data;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BaseCommand } from '../../../base.js';
|
|
2
|
+
export default class FaceTagAll 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,31 @@
|
|
|
1
|
+
import { BaseCommand } from '../../../base.js';
|
|
2
|
+
import { FacesAPIError } from '../../../client.js';
|
|
3
|
+
export default class FaceTagAll extends BaseCommand {
|
|
4
|
+
static description = 'List all tags across all your faces';
|
|
5
|
+
static flags = {
|
|
6
|
+
...BaseCommand.baseFlags,
|
|
7
|
+
};
|
|
8
|
+
async run() {
|
|
9
|
+
const { flags } = await this.parse(FaceTagAll);
|
|
10
|
+
const client = this.makeClient(flags);
|
|
11
|
+
let data;
|
|
12
|
+
try {
|
|
13
|
+
data = await client.get('/v1/me/tags');
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
if (err instanceof FacesAPIError)
|
|
17
|
+
this.error(`Error (${err.statusCode}): ${err.message}`);
|
|
18
|
+
throw err;
|
|
19
|
+
}
|
|
20
|
+
if (!this.jsonEnabled()) {
|
|
21
|
+
const tags = data.tags ?? [];
|
|
22
|
+
if (tags.length === 0) {
|
|
23
|
+
this.log('(no tags)');
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
this.log(tags.join(', '));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return data;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BaseCommand } from '../../../base.js';
|
|
2
|
+
export default class FaceTagList 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
|
+
alias: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
11
|
+
};
|
|
12
|
+
run(): Promise<unknown>;
|
|
13
|
+
}
|