faces-cli 1.6.2 → 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/dist/catalog.d.ts CHANGED
@@ -5,21 +5,30 @@ export interface FaceFrontmatter {
5
5
  description?: string;
6
6
  alias: string;
7
7
  attributes?: Record<string, string>;
8
+ tags?: string[];
9
+ default_model?: string;
10
+ formula?: string | null;
8
11
  component_counts?: {
9
12
  alpha: number;
10
13
  beta: number;
11
14
  delta: number;
12
15
  epsilon: number;
13
16
  };
17
+ compiled_tokens?: number;
14
18
  profile_token_count?: number;
19
+ [key: string]: unknown;
15
20
  }
16
21
  export interface FaceDataInput {
17
22
  name: string;
18
23
  alias: string;
19
24
  uid?: string;
25
+ description?: string | null;
20
26
  basic_facts?: Record<string, string | {
21
27
  value: string;
22
28
  }> | null;
29
+ tags?: string[] | null;
30
+ default_model?: string | null;
31
+ formula?: string | null;
23
32
  component_counts?: {
24
33
  alpha: number;
25
34
  beta: number;
@@ -27,10 +36,13 @@ export interface FaceDataInput {
27
36
  epsilon: number;
28
37
  } | null;
29
38
  profile_token_count?: number | null;
39
+ total_tokens_saved?: number | null;
30
40
  }
31
41
  export declare class CatalogService {
32
42
  isEnabled(): boolean;
33
43
  writeFace(faceData: FaceDataInput, description?: string): void;
34
- deleteFace(username: string): void;
44
+ deleteFace(alias: string): void;
45
+ /** Update just tags on an existing FACE.md without touching other fields */
46
+ updateTags(alias: string, tags: string[]): void;
35
47
  rebuildIndex(): void;
36
48
  }
package/dist/catalog.js CHANGED
@@ -5,7 +5,7 @@ import { loadConfig } from './config.js';
5
5
  export const CATALOG_DIR = path.join(os.homedir(), '.faces', 'catalog');
6
6
  export const CATALOG_INDEX = path.join(os.homedir(), '.faces', 'catalog.json');
7
7
  function quoteYaml(value) {
8
- if (/[:#\[\]{}&*!|>'"%@`]/.test(value) || value !== value.trim() || value === '') {
8
+ if (/[:#\[\]{}&*!|>'"%@`\n]/.test(value) || value !== value.trim() || value === '') {
9
9
  return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
10
10
  }
11
11
  return value;
@@ -16,16 +16,26 @@ function serializeFrontmatter(fm) {
16
16
  if (fm.description)
17
17
  lines.push(`description: ${quoteYaml(fm.description)}`);
18
18
  lines.push(`alias: ${quoteYaml(fm.alias)}`);
19
+ if (fm.default_model)
20
+ lines.push(`default_model: ${quoteYaml(fm.default_model)}`);
21
+ if (fm.formula)
22
+ lines.push(`formula: ${quoteYaml(fm.formula)}`);
19
23
  if (fm.attributes && Object.keys(fm.attributes).length > 0) {
20
24
  lines.push('attributes:');
21
25
  for (const [k, v] of Object.entries(fm.attributes)) {
22
26
  lines.push(` ${k}: ${quoteYaml(String(v))}`);
23
27
  }
24
28
  }
29
+ if (fm.tags && fm.tags.length > 0) {
30
+ lines.push(`tags: [${fm.tags.join(', ')}]`);
31
+ }
25
32
  if (fm.component_counts) {
26
33
  const cc = fm.component_counts;
27
34
  lines.push(`component_counts: ${cc.alpha + cc.beta + cc.delta + cc.epsilon}`);
28
35
  }
36
+ if (fm.compiled_tokens) {
37
+ lines.push(`compiled_tokens: ${fm.compiled_tokens}`);
38
+ }
29
39
  if (fm.profile_token_count) {
30
40
  lines.push(`profile_token_count: ${fm.profile_token_count}`);
31
41
  }
@@ -35,12 +45,14 @@ function serializeFrontmatter(fm) {
35
45
  function parseFrontmatter(content) {
36
46
  const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
37
47
  if (!match)
38
- return { frontmatter: null, body: content };
48
+ return { frontmatter: null, body: content, extraKeys: {} };
39
49
  const yamlBlock = match[1];
40
50
  const body = match[2];
41
51
  const fm = {};
52
+ const extraKeys = {};
42
53
  let inAttributes = false;
43
54
  const attributes = {};
55
+ const knownKeys = new Set(['name', 'description', 'alias', 'attributes', 'tags', 'default_model', 'formula', 'component_counts', 'compiled_tokens', 'profile_token_count']);
44
56
  for (const line of yamlBlock.split('\n')) {
45
57
  if (line.startsWith(' ') && inAttributes) {
46
58
  const colonIdx = line.indexOf(':');
@@ -63,14 +75,30 @@ function parseFrontmatter(content) {
63
75
  fm.description = val;
64
76
  else if (key === 'alias')
65
77
  fm.alias = val;
78
+ else if (key === 'default_model')
79
+ fm.default_model = val;
80
+ else if (key === 'formula')
81
+ fm.formula = val;
66
82
  else if (key === 'attributes')
67
83
  inAttributes = true;
84
+ else if (key === 'tags') {
85
+ const inner = val.replace(/^\[/, '').replace(/\]$/, '');
86
+ fm.tags = inner ? inner.split(',').map(s => s.trim()).filter(Boolean) : [];
87
+ }
88
+ else if (key === 'compiled_tokens')
89
+ fm.compiled_tokens = Number(val) || undefined;
90
+ else if (key === 'profile_token_count')
91
+ fm.profile_token_count = Number(val) || undefined;
92
+ else if (!knownKeys.has(key)) {
93
+ // Preserve agent/user-managed keys
94
+ extraKeys[key] = val;
95
+ }
68
96
  }
69
97
  if (Object.keys(attributes).length > 0)
70
98
  fm.attributes = attributes;
71
99
  if (!fm.name || !fm.alias)
72
- return { frontmatter: null, body: content };
73
- return { frontmatter: fm, body };
100
+ return { frontmatter: null, body: content, extraKeys: {} };
101
+ return { frontmatter: fm, body, extraKeys };
74
102
  }
75
103
  const DEFAULT_BODY = '\n## Notes\n\n';
76
104
  export class CatalogService {
@@ -82,12 +110,13 @@ export class CatalogService {
82
110
  if (!this.isEnabled())
83
111
  return;
84
112
  try {
85
- const username = faceData.alias;
86
- const dir = path.join(CATALOG_DIR, username);
113
+ const alias = faceData.alias;
114
+ const dir = path.join(CATALOG_DIR, alias);
87
115
  fs.mkdirSync(dir, { recursive: true });
88
116
  const filePath = path.join(dir, 'FACE.md');
89
117
  let existingBody = DEFAULT_BODY;
90
118
  let existingDescription;
119
+ let existingExtraKeys = {};
91
120
  if (fs.existsSync(filePath)) {
92
121
  const existing = fs.readFileSync(filePath, 'utf8');
93
122
  const parsed = parseFrontmatter(existing);
@@ -95,17 +124,26 @@ export class CatalogService {
95
124
  existingBody = parsed.body;
96
125
  if (parsed.frontmatter?.description)
97
126
  existingDescription = parsed.frontmatter.description;
127
+ existingExtraKeys = parsed.extraKeys;
98
128
  }
99
129
  const fm = {
100
130
  name: faceData.name,
101
- alias: username,
131
+ alias,
102
132
  };
133
+ // Description: explicit arg > existing > API response
103
134
  if (description) {
104
135
  fm.description = description;
105
136
  }
137
+ else if (faceData.description) {
138
+ fm.description = faceData.description;
139
+ }
106
140
  else if (existingDescription) {
107
141
  fm.description = existingDescription;
108
142
  }
143
+ if (faceData.default_model)
144
+ fm.default_model = faceData.default_model;
145
+ if (faceData.formula)
146
+ fm.formula = faceData.formula;
109
147
  if (faceData.basic_facts && Object.keys(faceData.basic_facts).length > 0) {
110
148
  const attrs = {};
111
149
  for (const [k, v] of Object.entries(faceData.basic_facts)) {
@@ -113,10 +151,19 @@ export class CatalogService {
113
151
  }
114
152
  fm.attributes = attrs;
115
153
  }
154
+ if (faceData.tags && faceData.tags.length > 0)
155
+ fm.tags = faceData.tags;
116
156
  if (faceData.component_counts)
117
157
  fm.component_counts = faceData.component_counts;
118
158
  if (faceData.profile_token_count)
119
159
  fm.profile_token_count = faceData.profile_token_count;
160
+ if (faceData.total_tokens_saved)
161
+ fm.compiled_tokens = faceData.total_tokens_saved;
162
+ // Preserve agent/user-managed extra keys
163
+ for (const [k, v] of Object.entries(existingExtraKeys)) {
164
+ if (!(k in fm))
165
+ fm[k] = v;
166
+ }
120
167
  const content = serializeFrontmatter(fm) + '\n' + existingBody;
121
168
  fs.writeFileSync(filePath, content);
122
169
  this.rebuildIndex();
@@ -126,11 +173,11 @@ export class CatalogService {
126
173
  process.stderr.write(`warn: catalog write failed: ${msg}\n`);
127
174
  }
128
175
  }
129
- deleteFace(username) {
176
+ deleteFace(alias) {
130
177
  if (!this.isEnabled())
131
178
  return;
132
179
  try {
133
- const dir = path.join(CATALOG_DIR, username);
180
+ const dir = path.join(CATALOG_DIR, alias);
134
181
  fs.rmSync(dir, { recursive: true, force: true });
135
182
  this.rebuildIndex();
136
183
  }
@@ -139,6 +186,28 @@ export class CatalogService {
139
186
  process.stderr.write(`warn: catalog delete failed: ${msg}\n`);
140
187
  }
141
188
  }
189
+ /** Update just tags on an existing FACE.md without touching other fields */
190
+ updateTags(alias, tags) {
191
+ if (!this.isEnabled())
192
+ return;
193
+ try {
194
+ const filePath = path.join(CATALOG_DIR, alias, 'FACE.md');
195
+ if (!fs.existsSync(filePath))
196
+ return;
197
+ const content = fs.readFileSync(filePath, 'utf8');
198
+ const parsed = parseFrontmatter(content);
199
+ if (!parsed.frontmatter)
200
+ return;
201
+ parsed.frontmatter.tags = tags;
202
+ const newContent = serializeFrontmatter(parsed.frontmatter) + '\n' + parsed.body;
203
+ fs.writeFileSync(filePath, newContent);
204
+ this.rebuildIndex();
205
+ }
206
+ catch (err) {
207
+ const msg = err instanceof Error ? err.message : String(err);
208
+ process.stderr.write(`warn: catalog tag update failed: ${msg}\n`);
209
+ }
210
+ }
142
211
  rebuildIndex() {
143
212
  if (!this.isEnabled())
144
213
  return;
@@ -3,7 +3,7 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { BaseCommand } from '../../base.js';
5
5
  import { FacesAPIError } from '../../client.js';
6
- import { flattenBasicFacts } from '../../utils.js';
6
+ import { flattenAttributes } from '../../utils.js';
7
7
  export default class CatalogBackup extends BaseCommand {
8
8
  static description = 'Snapshot all faces, teams, and source material for migration';
9
9
  static flags = {
@@ -13,10 +13,10 @@ export default class CatalogBackup extends BaseCommand {
13
13
  const { flags } = await this.parse(CatalogBackup);
14
14
  const client = this.makeClient(flags);
15
15
  const json = this.jsonEnabled();
16
- // Fetch all faces
16
+ // Fetch all faces with tags included
17
17
  let faces;
18
18
  try {
19
- const resp = await client.get('/v1/faces');
19
+ const resp = await client.get('/v1/faces?include=tags');
20
20
  faces = (resp.data ?? resp);
21
21
  }
22
22
  catch (err) {
@@ -31,27 +31,16 @@ export default class CatalogBackup extends BaseCommand {
31
31
  const alias = face.alias;
32
32
  if (!json)
33
33
  process.stderr.write(` ${alias}: `);
34
- // Fetch full face details (list endpoint may omit description)
35
- let fullFace = face;
36
- try {
37
- fullFace = await client.get(`/v1/faces/${alias}`);
38
- }
39
- catch { /* use list data */ }
40
- // Fetch tags
41
- let tags = [];
42
- try {
43
- const tagResp = await client.get(`/v1/iam/${alias}/tags`);
44
- tags = tagResp.tags ?? [];
45
- }
46
- catch { /* no tags */ }
34
+ // Tags and description come from the enriched list response
35
+ const tags = face.tags ?? [];
47
36
  const entry = {
48
37
  alias,
49
- name: fullFace.name,
50
- description: fullFace.description ?? null,
51
- basic_facts: fullFace.basic_facts ? flattenBasicFacts(fullFace.basic_facts) : null,
52
- default_model: fullFace.default_model ?? null,
53
- default_tools: fullFace.default_tools ?? [],
54
- formula: fullFace.formula ?? null,
38
+ name: face.name,
39
+ description: face.description ?? null,
40
+ basic_facts: face.basic_facts ? flattenAttributes(face.basic_facts) : null,
41
+ default_model: face.default_model ?? null,
42
+ default_tools: face.default_tools ?? [],
43
+ formula: face.formula ?? null,
55
44
  tags,
56
45
  documents: [],
57
46
  threads: [],
@@ -91,18 +80,10 @@ export default class CatalogBackup extends BaseCommand {
91
80
  }
92
81
  // Fetch teams
93
82
  const backupTeams = [];
94
- let username;
95
- try {
96
- const resp = await client.get('/auth/me');
97
- const whoami = (resp.data ?? resp);
98
- username = whoami.username ?? undefined;
99
- }
100
- catch { /* proceed */ }
101
83
  try {
102
84
  const resp = await client.get('/v1/me/teams');
103
85
  let teams = resp.data ?? resp;
104
- if (username)
105
- teams = teams.filter(t => t.name !== username);
86
+ teams = teams.filter(t => t.kind !== 'personal');
106
87
  for (const team of teams) {
107
88
  const teamId = team.id;
108
89
  if (!json)
@@ -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
- catalog.writeFace(face, face.description ?? undefined);
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 and description
140
- catalog.writeFace(remote, remote.description ?? undefined);
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();
@@ -176,8 +227,7 @@ export default class CatalogDoctor extends BaseCommand {
176
227
  }
177
228
  if (text) {
178
229
  const desc = text.trim();
179
- catalog.writeFace(remote, desc);
180
- // Also sync description to server
230
+ catalog.writeFace({ ...remote, description: desc });
181
231
  try {
182
232
  await client.patch(`/v1/faces/${alias}`, { body: { description: desc } });
183
233
  }
@@ -197,6 +247,6 @@ export default class CatalogDoctor extends BaseCommand {
197
247
  this.log(`Generated ${generated} description(s).`);
198
248
  }
199
249
  catalog.rebuildIndex();
200
- return { fixed, removed, generated: flags.generate ? noDescription.length : 0 };
250
+ return { fixed, removed, teamsSynced, generated: flags.generate ? noDescription.length : 0 };
201
251
  }
202
252
  }
@@ -2,7 +2,7 @@ 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 = {
@@ -60,22 +60,31 @@ export default class FaceCreate extends BaseCommand {
60
60
  for (const w of face.warnings)
61
61
  this.warn(w);
62
62
  }
63
- if (face.basic_facts && typeof face.basic_facts === 'object') {
64
- face.basic_facts = flattenBasicFacts(face.basic_facts);
65
- }
66
63
  // Post tags if provided
67
- if (flags.tag && flags.tag.length > 0) {
64
+ const tags = flags.tag ?? [];
65
+ if (tags.length > 0) {
68
66
  try {
69
- await client.post(`/v1/iam/${flags.alias}/tags`, { body: { tags: flags.tag } });
67
+ await client.post(`/v1/iam/${flags.alias}/tags`, { body: { tags } });
68
+ face.tags = tags;
70
69
  }
71
70
  catch (err) {
72
71
  if (err instanceof FacesAPIError)
73
72
  this.warn(`Tags failed (${err.statusCode}): ${err.message}`);
74
73
  }
75
74
  }
75
+ // Rename basic_facts → attributes in response
76
+ renameFaceFields(face);
77
+ // Write to local catalog with tags
76
78
  try {
77
79
  const catalog = new CatalogService();
78
- 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
+ });
79
88
  }
80
89
  catch { /* catalog errors are non-fatal */ }
81
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
  }
@@ -3,6 +3,8 @@ export default class FaceList extends BaseCommand {
3
3
  static description: string;
4
4
  static flags: {
5
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>;
6
8
  'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
9
  token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
10
  'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -1,26 +1,33 @@
1
1
  import { 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 FaceList extends BaseCommand {
6
6
  static description = 'List all owned faces';
7
7
  static flags = {
8
8
  ...BaseCommand.baseFlags,
9
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)' }),
10
12
  };
11
13
  async run() {
12
14
  const { flags } = await this.parse(FaceList);
13
15
  const client = this.makeClient(flags);
14
- const params = {};
15
- // Tags need to be passed as repeated query params: ?tag=a&tag=b
16
- // Build the query string manually since URLSearchParams dedupes keys
17
- let tagQuery = '';
16
+ // Build query string manually for repeated params
17
+ const parts = [];
18
18
  if (flags.tag && flags.tag.length > 0) {
19
- tagQuery = flags.tag.map(t => `tag=${encodeURIComponent(t)}`).join('&');
19
+ for (const t of flags.tag)
20
+ parts.push(`tag=${encodeURIComponent(t)}`);
20
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)}`);
21
28
  let data;
22
29
  try {
23
- const path = tagQuery ? `/v1/faces?${tagQuery}` : '/v1/faces';
30
+ const path = parts.length > 0 ? `/v1/faces?${parts.join('&')}` : '/v1/faces';
24
31
  data = await client.get(path);
25
32
  }
26
33
  catch (err) {
@@ -28,12 +35,10 @@ export default class FaceList extends BaseCommand {
28
35
  this.error(`Error (${err.statusCode}): ${err.message}`);
29
36
  throw err;
30
37
  }
31
- // Flatten basic_facts in response
38
+ // Rename basic_facts → attributes in response
32
39
  const raw = data.data ?? data;
33
40
  for (const f of raw) {
34
- if (f.basic_facts && typeof f.basic_facts === 'object') {
35
- f.basic_facts = flattenBasicFacts(f.basic_facts);
36
- }
41
+ renameFaceFields(f);
37
42
  }
38
43
  if (!this.jsonEnabled()) {
39
44
  const faces = raw;