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 +13 -1
- package/dist/catalog.js +78 -9
- package/dist/commands/catalog/backup.js +12 -31
- package/dist/commands/catalog/doctor.js +69 -19
- package/dist/commands/face/create.js +16 -7
- package/dist/commands/face/get.d.ts +1 -0
- package/dist/commands/face/get.js +13 -9
- package/dist/commands/face/list.d.ts +2 -0
- package/dist/commands/face/list.js +16 -11
- package/dist/commands/face/tag/add.js +12 -1
- package/dist/commands/face/tag/all.d.ts +10 -0
- package/dist/commands/face/tag/all.js +31 -0
- package/dist/commands/face/tag/remove.js +12 -1
- package/dist/commands/face/tag/set.js +12 -1
- package/dist/commands/face/teams.d.ts +14 -0
- package/dist/commands/face/teams.js +29 -0
- package/dist/commands/face/update.js +18 -6
- package/dist/commands/team/add.js +17 -0
- package/dist/commands/team/create.js +18 -2
- package/dist/commands/team/list.js +5 -14
- package/dist/commands/team/remove.js +14 -0
- package/dist/commands/team/update.js +30 -0
- package/dist/team-catalog.d.ts +1 -0
- package/dist/team-catalog.js +6 -1
- package/dist/utils.d.ts +9 -2
- package/dist/utils.js +17 -2
- package/oclif.manifest.json +746 -604
- package/package.json +1 -1
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(
|
|
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 (/[:#\[\]{}&*!|>'"
|
|
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
|
|
86
|
-
const dir = path.join(CATALOG_DIR,
|
|
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
|
|
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(
|
|
176
|
+
deleteFace(alias) {
|
|
130
177
|
if (!this.isEnabled())
|
|
131
178
|
return;
|
|
132
179
|
try {
|
|
133
|
-
const dir = path.join(CATALOG_DIR,
|
|
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 {
|
|
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
|
-
//
|
|
35
|
-
|
|
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:
|
|
50
|
-
description:
|
|
51
|
-
basic_facts:
|
|
52
|
-
default_model:
|
|
53
|
-
default_tools:
|
|
54
|
-
formula:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
catalog.writeFace(face
|
|
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
|
-
|
|
140
|
-
catalog.writeFace(remote, remote.description ?? undefined);
|
|
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();
|
|
@@ -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 {
|
|
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
|
-
|
|
64
|
+
const tags = flags.tag ?? [];
|
|
65
|
+
if (tags.length > 0) {
|
|
68
66
|
try {
|
|
69
|
-
await client.post(`/v1/iam/${flags.alias}/tags`, { body: { tags
|
|
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(
|
|
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 {
|
|
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
|
}
|
|
@@ -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 {
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
//
|
|
38
|
+
// Rename basic_facts → attributes in response
|
|
32
39
|
const raw = data.data ?? data;
|
|
33
40
|
for (const f of raw) {
|
|
34
|
-
|
|
35
|
-
f.basic_facts = flattenBasicFacts(f.basic_facts);
|
|
36
|
-
}
|
|
41
|
+
renameFaceFields(f);
|
|
37
42
|
}
|
|
38
43
|
if (!this.jsonEnabled()) {
|
|
39
44
|
const faces = raw;
|