faces-cli 1.6.1 → 1.6.2

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 (48) hide show
  1. package/README.md +50 -17
  2. package/dist/client.d.ts +4 -0
  3. package/dist/client.js +16 -0
  4. package/dist/commands/catalog/backup.js +84 -19
  5. package/dist/commands/catalog/doctor.js +10 -4
  6. package/dist/commands/catalog/restore.js +64 -11
  7. package/dist/commands/face/create.d.ts +1 -0
  8. package/dist/commands/face/create.js +14 -1
  9. package/dist/commands/face/list.d.ts +1 -0
  10. package/dist/commands/face/list.js +11 -1
  11. package/dist/commands/face/tag/add.d.ts +14 -0
  12. package/dist/commands/face/tag/add.js +29 -0
  13. package/dist/commands/face/tag/list.d.ts +13 -0
  14. package/dist/commands/face/tag/list.js +35 -0
  15. package/dist/commands/face/tag/remove.d.ts +14 -0
  16. package/dist/commands/face/tag/remove.js +29 -0
  17. package/dist/commands/face/tag/set.d.ts +14 -0
  18. package/dist/commands/face/tag/set.js +29 -0
  19. package/dist/commands/face/update.d.ts +1 -0
  20. package/dist/commands/face/update.js +17 -6
  21. package/dist/commands/team/add.d.ts +14 -0
  22. package/dist/commands/team/add.js +51 -0
  23. package/dist/commands/team/create.d.ts +15 -0
  24. package/dist/commands/team/create.js +53 -0
  25. package/dist/commands/team/delete.d.ts +15 -0
  26. package/dist/commands/team/delete.js +44 -0
  27. package/dist/commands/team/get.d.ts +13 -0
  28. package/dist/commands/team/get.js +28 -0
  29. package/dist/commands/team/list.d.ts +10 -0
  30. package/dist/commands/team/list.js +49 -0
  31. package/dist/commands/team/members.d.ts +13 -0
  32. package/dist/commands/team/members.js +28 -0
  33. package/dist/commands/team/remove.d.ts +14 -0
  34. package/dist/commands/team/remove.js +42 -0
  35. package/dist/commands/team/tag/add.d.ts +14 -0
  36. package/dist/commands/team/tag/add.js +29 -0
  37. package/dist/commands/team/tag/list.d.ts +13 -0
  38. package/dist/commands/team/tag/list.js +35 -0
  39. package/dist/commands/team/tag/remove.d.ts +14 -0
  40. package/dist/commands/team/tag/remove.js +29 -0
  41. package/dist/commands/team/tag/set.d.ts +14 -0
  42. package/dist/commands/team/tag/set.js +29 -0
  43. package/dist/commands/team/update.d.ts +17 -0
  44. package/dist/commands/team/update.js +48 -0
  45. package/dist/team-catalog.d.ts +16 -0
  46. package/dist/team-catalog.js +99 -0
  47. package/oclif.manifest.json +1522 -436
  48. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # faces-cli
2
2
 
3
- Command-line interface for the [Faces](https://faces.sh) platform.
3
+ Command-line interface for the [Faces](https://faces.sh) platform. Compile source material into AI personas with cognitive primitives, then chat with them, compare them, and compose them.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,43 +8,76 @@ Command-line interface for the [Faces](https://faces.sh) platform.
8
8
  npm install -g faces-cli
9
9
  ```
10
10
 
11
- Requires Node.js 18.
11
+ Requires Node.js >= 18.
12
12
 
13
13
  ## Quick start
14
14
 
15
15
  ```bash
16
- faces auth login
17
- faces face list
18
- faces chat chat myface -m "Hello"
16
+ # Register or login
17
+ faces auth:register --email you@example.com --password 'YourPass123!' --username you
18
+ faces auth:login --email you@example.com --password 'YourPass123!'
19
+
20
+ # Create a face
21
+ faces face:create --name "Alice" --alias alice --default-model gpt-5.4-mini \
22
+ --attr gender=female --attr age=29 --attr occupation="product designer"
23
+
24
+ # Add source material and compile
25
+ faces compile:doc alice --file essay.txt --no-wait
26
+ faces compile:upload alice --file interview.mp3 --kind thread --no-wait
27
+
28
+ # Chat
29
+ faces chat:chat alice -m "What matters most to you?"
30
+ faces chat:chat alice -m "Tell me more" --llm claude-sonnet-4-6
31
+
32
+ # Compare and compose
33
+ faces face:diff --face alice --face bob
34
+ faces face:create --name "Alice-Bob" --alias alice-bob --formula "alice | bob"
19
35
  ```
20
36
 
21
37
  ## Commands
22
38
 
23
39
  ```
24
- faces auth login / logout / register / whoami / refresh
25
- faces face create / list / get / update / delete / stats / upload
26
- faces chat chat / messages / responses
27
- faces compile doc create / list / get / prepare / sync / delete
28
- faces compile thread create / list / message / sync
29
- faces keys create / list / revoke / update
30
- faces billing balance / subscription / quota / usage / topup / checkout / card-setup / llm-costs
31
- faces account state
32
- faces config set / show / clear
40
+ faces auth:login / logout / register / whoami / refresh
41
+ faces auth:connect / disconnect / connections
42
+
43
+ faces face:create / list / get / update / delete / stats / attributes
44
+ faces face:upload / diff / neighbors
45
+
46
+ faces chat:chat / messages / responses
47
+
48
+ faces compile:doc / compile:doc:create / list / get / edit / make / pause / reset / delete
49
+ faces compile:thread:create / list / get / edit / message / make / pause / reset / sync / delete
50
+ faces compile:upload / compile:import
51
+ faces compile:all
52
+
53
+ faces catalog:list / doctor / backup / restore / manyfaced
54
+
55
+ faces keys:create / list / revoke / update
56
+
57
+ faces billing:balance / subscription / quota / usage / topup / checkout / card-setup / llm-costs
58
+
59
+ faces account:state / preferences
60
+
61
+ faces config:set / show / clear
33
62
  ```
34
63
 
35
64
  Run `faces <command> --help` for details on any command.
36
65
 
37
66
  ## Authentication
38
67
 
39
- Login stores a JWT at `~/.faces/config.json` (shared with the Python `faces-cli`).
40
- You can also authenticate via environment variables:
68
+ Login stores a JWT at `~/.faces/config.json`. You can also authenticate via environment variables:
41
69
 
42
70
  ```bash
43
- export FACES_API_KEY=your_api_key
71
+ export FACES_API_KEY=sk-faces-...
44
72
  export FACES_TOKEN=your_jwt
45
73
  export FACES_BASE_URL=https://api.faces.sh # optional override
46
74
  ```
47
75
 
76
+ ## Plans
77
+
78
+ - **Pay-per-token** — $5 activation (added as credits), 5% markup on all API calls. No monthly fee.
79
+ - **Subscription Connect** ($17/month) — same 5% markup, plus link your ChatGPT Plus/Pro account to use select OpenAI models at no extra cost for compilation and inference.
80
+
48
81
  ## License
49
82
 
50
83
  MIT — Copyright (c) 2026 Headwaters Lab Inc.
package/dist/client.d.ts CHANGED
@@ -33,6 +33,10 @@ export declare class FacesClient {
33
33
  body?: unknown;
34
34
  }): Promise<ResponseWithHeaders>;
35
35
  postNoAuth(path: string, body: unknown): Promise<unknown>;
36
+ put(path: string, opts?: {
37
+ requireJwt?: boolean;
38
+ body?: unknown;
39
+ }): Promise<unknown>;
36
40
  patch(path: string, opts?: {
37
41
  requireJwt?: boolean;
38
42
  body?: unknown;
package/dist/client.js CHANGED
@@ -141,6 +141,22 @@ export class FacesClient {
141
141
  throw await this.parseError(resp);
142
142
  return resp.json();
143
143
  }
144
+ async put(path, opts = {}) {
145
+ let resp;
146
+ try {
147
+ resp = await fetch(this.url(path), {
148
+ method: 'PUT',
149
+ headers: { ...this.authHeader(opts.requireJwt), 'Content-Type': 'application/json' },
150
+ body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
151
+ });
152
+ }
153
+ catch (err) {
154
+ this.wrapNetworkError(err, path);
155
+ }
156
+ if (!resp.ok)
157
+ throw await this.parseError(resp);
158
+ return resp.json();
159
+ }
144
160
  async patch(path, opts = {}) {
145
161
  let resp;
146
162
  try {
@@ -5,7 +5,7 @@ import { BaseCommand } from '../../base.js';
5
5
  import { FacesAPIError } from '../../client.js';
6
6
  import { flattenBasicFacts } from '../../utils.js';
7
7
  export default class CatalogBackup extends BaseCommand {
8
- static description = 'Snapshot all faces and source material for migration';
8
+ static description = 'Snapshot all faces, teams, and source material for migration';
9
9
  static flags = {
10
10
  ...BaseCommand.baseFlags,
11
11
  };
@@ -31,13 +31,28 @@ 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
47
  const entry = {
35
48
  alias,
36
- name: face.name,
37
- basic_facts: face.basic_facts ? flattenBasicFacts(face.basic_facts) : null,
38
- default_model: face.default_model ?? null,
39
- default_tools: face.default_tools ?? [],
40
- formula: face.formula ?? null,
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,
55
+ tags,
41
56
  documents: [],
42
57
  threads: [],
43
58
  };
@@ -52,9 +67,7 @@ export default class CatalogBackup extends BaseCommand {
52
67
  });
53
68
  }
54
69
  }
55
- catch {
56
- // face may have no documents
57
- }
70
+ catch { /* face may have no documents */ }
58
71
  // Fetch threads
59
72
  try {
60
73
  const threads = await client.get('/v1/compile/threads', { params: { alias } });
@@ -68,22 +81,74 @@ export default class CatalogBackup extends BaseCommand {
68
81
  messages,
69
82
  });
70
83
  }
71
- catch {
72
- // skip threads we can't fetch
73
- }
84
+ catch { /* skip threads we can't fetch */ }
74
85
  }
75
86
  }
76
- catch {
77
- // face may have no threads
78
- }
87
+ catch { /* face may have no threads */ }
79
88
  if (!json)
80
- process.stderr.write(`${entry.documents.length} doc(s), ${entry.threads.length} thread(s)\n`);
89
+ process.stderr.write(`${entry.documents.length} doc(s), ${entry.threads.length} thread(s), ${tags.length} tag(s)\n`);
81
90
  backupFaces.push(entry);
82
91
  }
92
+ // Fetch teams
93
+ 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
+ try {
102
+ const resp = await client.get('/v1/me/teams');
103
+ let teams = resp.data ?? resp;
104
+ if (username)
105
+ teams = teams.filter(t => t.name !== username);
106
+ for (const team of teams) {
107
+ const teamId = team.id;
108
+ if (!json)
109
+ process.stderr.write(` team ${team.name}: `);
110
+ // Fetch members
111
+ let memberAliases = [];
112
+ try {
113
+ const membersResp = await client.get(`/v1/teams/${teamId}/members`);
114
+ const members = membersResp.data ?? [];
115
+ // Build uid→alias map from original face list
116
+ const uidToAlias = new Map();
117
+ for (const f of faces) {
118
+ if (f.uid)
119
+ uidToAlias.set(f.uid, f.alias);
120
+ }
121
+ for (const m of members) {
122
+ const iamId = m.iam_id;
123
+ const alias = uidToAlias.get(iamId) ?? iamId;
124
+ memberAliases.push(alias);
125
+ }
126
+ }
127
+ catch { /* no members */ }
128
+ // Fetch tags
129
+ let teamTags = [];
130
+ try {
131
+ const tagResp = await client.get(`/v1/teams/${teamId}/tags`);
132
+ teamTags = tagResp.tags ?? [];
133
+ }
134
+ catch { /* no tags */ }
135
+ backupTeams.push({
136
+ name: team.name,
137
+ description: team.description ?? null,
138
+ protocol: team.protocol ?? null,
139
+ tags: teamTags,
140
+ members: memberAliases,
141
+ });
142
+ if (!json)
143
+ process.stderr.write(`${memberAliases.length} member(s), ${teamTags.length} tag(s)\n`);
144
+ }
145
+ }
146
+ catch { /* no teams */ }
83
147
  const backup = {
84
- version: 1,
148
+ version: 2,
85
149
  created_at: new Date().toISOString(),
86
150
  faces: backupFaces,
151
+ teams: backupTeams,
87
152
  };
88
153
  const backupDir = path.join(os.homedir(), '.faces', 'backups');
89
154
  fs.mkdirSync(backupDir, { recursive: true });
@@ -94,8 +159,8 @@ export default class CatalogBackup extends BaseCommand {
94
159
  const totalThreads = backupFaces.reduce((n, f) => n + f.threads.length, 0);
95
160
  if (!json) {
96
161
  this.log(`\nBackup saved: ${filePath}`);
97
- this.log(` ${backupFaces.length} face(s), ${totalDocs} document(s), ${totalThreads} thread(s)`);
162
+ this.log(` ${backupFaces.length} face(s), ${totalDocs} document(s), ${totalThreads} thread(s), ${backupTeams.length} team(s)`);
98
163
  }
99
- return { path: filePath, faces: backupFaces.length, documents: totalDocs, threads: totalThreads };
164
+ return { path: filePath, faces: backupFaces.length, documents: totalDocs, threads: totalThreads, teams: backupTeams.length };
100
165
  }
101
166
  }
@@ -130,14 +130,14 @@ export default class CatalogDoctor extends BaseCommand {
130
130
  // Fix mode: rebuild missing and stale entries
131
131
  let fixed = 0;
132
132
  for (const face of [...missing, ...stale]) {
133
- catalog.writeFace(face);
133
+ catalog.writeFace(face, face.description ?? undefined);
134
134
  fixed++;
135
135
  }
136
136
  // Also refresh faces that exist but might have stale attributes
137
137
  for (const [alias, remote] of remoteByAlias) {
138
138
  if (!missing.some((f) => f.alias === alias) && !stale.some((f) => f.alias === alias)) {
139
- // Face exists locally and name matches — still refresh attributes
140
- catalog.writeFace(remote);
139
+ // Face exists locally and name matches — still refresh attributes and description
140
+ catalog.writeFace(remote, remote.description ?? undefined);
141
141
  }
142
142
  }
143
143
  // Remove orphans
@@ -175,7 +175,13 @@ export default class CatalogDoctor extends BaseCommand {
175
175
  text = resp.choices?.[0]?.message?.content;
176
176
  }
177
177
  if (text) {
178
- catalog.writeFace(remote, text.trim());
178
+ const desc = text.trim();
179
+ catalog.writeFace(remote, desc);
180
+ // Also sync description to server
181
+ try {
182
+ await client.patch(`/v1/faces/${alias}`, { body: { description: desc } });
183
+ }
184
+ catch { /* non-fatal */ }
179
185
  generated++;
180
186
  this.log(` ${alias}: done`);
181
187
  }
@@ -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>;
@@ -9,7 +9,8 @@ export default class FaceCreate extends BaseCommand {
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'])
@@ -60,6 +63,16 @@ export default class FaceCreate extends BaseCommand {
60
63
  if (face.basic_facts && typeof face.basic_facts === 'object') {
61
64
  face.basic_facts = flattenBasicFacts(face.basic_facts);
62
65
  }
66
+ // Post tags if provided
67
+ if (flags.tag && flags.tag.length > 0) {
68
+ try {
69
+ await client.post(`/v1/iam/${flags.alias}/tags`, { body: { tags: flags.tag } });
70
+ }
71
+ catch (err) {
72
+ if (err instanceof FacesAPIError)
73
+ this.warn(`Tags failed (${err.statusCode}): ${err.message}`);
74
+ }
75
+ }
63
76
  try {
64
77
  const catalog = new CatalogService();
65
78
  catalog.writeFace(face, flags.description);
@@ -2,6 +2,7 @@ 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>;
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,3 +1,4 @@
1
+ import { Flags } from '@oclif/core';
1
2
  import { BaseCommand } from '../../base.js';
2
3
  import { FacesAPIError } from '../../client.js';
3
4
  import { flattenBasicFacts } from '../../utils.js';
@@ -5,13 +6,22 @@ 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 }),
8
10
  };
9
11
  async run() {
10
12
  const { flags } = await this.parse(FaceList);
11
13
  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 = '';
18
+ if (flags.tag && flags.tag.length > 0) {
19
+ tagQuery = flags.tag.map(t => `tag=${encodeURIComponent(t)}`).join('&');
20
+ }
12
21
  let data;
13
22
  try {
14
- data = await client.get('/v1/faces');
23
+ const path = tagQuery ? `/v1/faces?${tagQuery}` : '/v1/faces';
24
+ data = await client.get(path);
15
25
  }
16
26
  catch (err) {
17
27
  if (err instanceof FacesAPIError)
@@ -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,29 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../../base.js';
3
+ import { FacesAPIError } from '../../../client.js';
4
+ export default class FaceTagAdd extends BaseCommand {
5
+ static description = 'Add tags to a face (appends, idempotent)';
6
+ static flags = {
7
+ ...BaseCommand.baseFlags,
8
+ tag: Flags.string({ description: 'Tag to add (repeatable)', multiple: true, required: true }),
9
+ };
10
+ static args = {
11
+ alias: Args.string({ description: 'Face alias', required: true }),
12
+ };
13
+ async run() {
14
+ const { args, flags } = await this.parse(FaceTagAdd);
15
+ const client = this.makeClient(flags);
16
+ let data;
17
+ try {
18
+ data = await client.post(`/v1/iam/${args.alias}/tags`, { body: { tags: flags.tag } });
19
+ }
20
+ catch (err) {
21
+ if (err instanceof FacesAPIError)
22
+ this.error(`Error (${err.statusCode}): ${err.message}`);
23
+ throw err;
24
+ }
25
+ if (!this.jsonEnabled())
26
+ this.printHuman(data);
27
+ return data;
28
+ }
29
+ }
@@ -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
+ }
@@ -0,0 +1,35 @@
1
+ import { Args } from '@oclif/core';
2
+ import { BaseCommand } from '../../../base.js';
3
+ import { FacesAPIError } from '../../../client.js';
4
+ export default class FaceTagList extends BaseCommand {
5
+ static description = 'List tags on a face';
6
+ static flags = {
7
+ ...BaseCommand.baseFlags,
8
+ };
9
+ static args = {
10
+ alias: Args.string({ description: 'Face alias', required: true }),
11
+ };
12
+ async run() {
13
+ const { args, flags } = await this.parse(FaceTagList);
14
+ const client = this.makeClient(flags);
15
+ let data;
16
+ try {
17
+ data = await client.get(`/v1/iam/${args.alias}/tags`);
18
+ }
19
+ catch (err) {
20
+ if (err instanceof FacesAPIError)
21
+ this.error(`Error (${err.statusCode}): ${err.message}`);
22
+ throw err;
23
+ }
24
+ if (!this.jsonEnabled()) {
25
+ const tags = data.tags ?? [];
26
+ if (tags.length === 0) {
27
+ this.log('(no tags)');
28
+ }
29
+ else {
30
+ this.log(tags.join(', '));
31
+ }
32
+ }
33
+ return data;
34
+ }
35
+ }