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.
Files changed (58) hide show
  1. package/README.md +50 -17
  2. package/dist/catalog.d.ts +13 -1
  3. package/dist/catalog.js +78 -9
  4. package/dist/client.d.ts +4 -0
  5. package/dist/client.js +16 -0
  6. package/dist/commands/catalog/backup.js +64 -18
  7. package/dist/commands/catalog/doctor.js +72 -16
  8. package/dist/commands/catalog/restore.js +64 -11
  9. package/dist/commands/face/create.d.ts +1 -0
  10. package/dist/commands/face/create.js +27 -5
  11. package/dist/commands/face/get.d.ts +1 -0
  12. package/dist/commands/face/get.js +13 -9
  13. package/dist/commands/face/list.d.ts +3 -0
  14. package/dist/commands/face/list.js +21 -6
  15. package/dist/commands/face/tag/add.d.ts +14 -0
  16. package/dist/commands/face/tag/add.js +40 -0
  17. package/dist/commands/face/tag/all.d.ts +10 -0
  18. package/dist/commands/face/tag/all.js +31 -0
  19. package/dist/commands/face/tag/list.d.ts +13 -0
  20. package/dist/commands/face/tag/list.js +35 -0
  21. package/dist/commands/face/tag/remove.d.ts +14 -0
  22. package/dist/commands/face/tag/remove.js +40 -0
  23. package/dist/commands/face/tag/set.d.ts +14 -0
  24. package/dist/commands/face/tag/set.js +40 -0
  25. package/dist/commands/face/teams.d.ts +14 -0
  26. package/dist/commands/face/teams.js +29 -0
  27. package/dist/commands/face/update.d.ts +1 -0
  28. package/dist/commands/face/update.js +34 -11
  29. package/dist/commands/team/add.d.ts +14 -0
  30. package/dist/commands/team/add.js +68 -0
  31. package/dist/commands/team/create.d.ts +15 -0
  32. package/dist/commands/team/create.js +69 -0
  33. package/dist/commands/team/delete.d.ts +15 -0
  34. package/dist/commands/team/delete.js +44 -0
  35. package/dist/commands/team/get.d.ts +13 -0
  36. package/dist/commands/team/get.js +28 -0
  37. package/dist/commands/team/list.d.ts +10 -0
  38. package/dist/commands/team/list.js +40 -0
  39. package/dist/commands/team/members.d.ts +13 -0
  40. package/dist/commands/team/members.js +28 -0
  41. package/dist/commands/team/remove.d.ts +14 -0
  42. package/dist/commands/team/remove.js +56 -0
  43. package/dist/commands/team/tag/add.d.ts +14 -0
  44. package/dist/commands/team/tag/add.js +29 -0
  45. package/dist/commands/team/tag/list.d.ts +13 -0
  46. package/dist/commands/team/tag/list.js +35 -0
  47. package/dist/commands/team/tag/remove.d.ts +14 -0
  48. package/dist/commands/team/tag/remove.js +29 -0
  49. package/dist/commands/team/tag/set.d.ts +14 -0
  50. package/dist/commands/team/tag/set.js +29 -0
  51. package/dist/commands/team/update.d.ts +17 -0
  52. package/dist/commands/team/update.js +78 -0
  53. package/dist/team-catalog.d.ts +17 -0
  54. package/dist/team-catalog.js +104 -0
  55. package/dist/utils.d.ts +9 -2
  56. package/dist/utils.js +17 -2
  57. package/oclif.manifest.json +1537 -309
  58. 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
- // Fetch all faces from API
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
- // List endpoint returns minimal fields; fetch full details for each
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
- return await client.get(`/v1/faces/${f.alias}`);
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 missing and stale entries
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
- // Also refresh faces that exist but might have stale attributes
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 orphans
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
- this.log(`Fixed ${fixed} catalog entries, removed ${removed} orphans.`);
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
- catalog.writeFace(remote, text.trim());
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
- // Create face
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 — create thread then PATCH messages in bulk
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, _json) {
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 { flattenBasicFacts } from '../../utils.js';
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: 'Description for local catalog' }),
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
- if (face.basic_facts && typeof face.basic_facts === 'object') {
61
- face.basic_facts = flattenBasicFacts(face.basic_facts);
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(face, flags.description);
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 { flattenBasicFacts } from '../../utils.js';
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(`/v1/faces/${args.face_id}`);
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 f = data;
26
- if (f.basic_facts)
27
- f.basic_facts = flattenBasicFacts(f.basic_facts);
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.basic_facts && Object.keys(f.basic_facts).length > 0) {
41
- this.log('basic_facts:');
42
- for (const [k, v] of Object.entries(f.basic_facts)) {
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 { flattenBasicFacts } from '../../utils.js';
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
- data = await client.get('/v1/faces');
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
- // Flatten basic_facts in response
38
+ // Rename basic_facts → attributes in response
22
39
  const raw = data.data ?? data;
23
40
  for (const f of raw) {
24
- if (f.basic_facts && typeof f.basic_facts === 'object') {
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
+ }