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
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/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;
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 {
@@ -3,9 +3,9 @@ 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
- 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
  };
@@ -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,13 +31,17 @@ export default class CatalogBackup extends BaseCommand {
31
31
  const alias = face.alias;
32
32
  if (!json)
33
33
  process.stderr.write(` ${alias}: `);
34
+ // Tags and description come from the enriched list response
35
+ const tags = face.tags ?? [];
34
36
  const entry = {
35
37
  alias,
36
38
  name: face.name,
37
- basic_facts: face.basic_facts ? flattenBasicFacts(face.basic_facts) : null,
39
+ description: face.description ?? null,
40
+ basic_facts: face.basic_facts ? flattenAttributes(face.basic_facts) : null,
38
41
  default_model: face.default_model ?? null,
39
42
  default_tools: face.default_tools ?? [],
40
43
  formula: face.formula ?? null,
44
+ tags,
41
45
  documents: [],
42
46
  threads: [],
43
47
  };
@@ -52,9 +56,7 @@ export default class CatalogBackup extends BaseCommand {
52
56
  });
53
57
  }
54
58
  }
55
- catch {
56
- // face may have no documents
57
- }
59
+ catch { /* face may have no documents */ }
58
60
  // Fetch threads
59
61
  try {
60
62
  const threads = await client.get('/v1/compile/threads', { params: { alias } });
@@ -68,22 +70,66 @@ export default class CatalogBackup extends BaseCommand {
68
70
  messages,
69
71
  });
70
72
  }
71
- catch {
72
- // skip threads we can't fetch
73
- }
73
+ catch { /* skip threads we can't fetch */ }
74
74
  }
75
75
  }
76
- catch {
77
- // face may have no threads
78
- }
76
+ catch { /* face may have no threads */ }
79
77
  if (!json)
80
- process.stderr.write(`${entry.documents.length} doc(s), ${entry.threads.length} thread(s)\n`);
78
+ process.stderr.write(`${entry.documents.length} doc(s), ${entry.threads.length} thread(s), ${tags.length} tag(s)\n`);
81
79
  backupFaces.push(entry);
82
80
  }
81
+ // Fetch teams
82
+ const backupTeams = [];
83
+ try {
84
+ const resp = await client.get('/v1/me/teams');
85
+ let teams = resp.data ?? resp;
86
+ teams = teams.filter(t => t.kind !== 'personal');
87
+ for (const team of teams) {
88
+ const teamId = team.id;
89
+ if (!json)
90
+ process.stderr.write(` team ${team.name}: `);
91
+ // Fetch members
92
+ let memberAliases = [];
93
+ try {
94
+ const membersResp = await client.get(`/v1/teams/${teamId}/members`);
95
+ const members = membersResp.data ?? [];
96
+ // Build uid→alias map from original face list
97
+ const uidToAlias = new Map();
98
+ for (const f of faces) {
99
+ if (f.uid)
100
+ uidToAlias.set(f.uid, f.alias);
101
+ }
102
+ for (const m of members) {
103
+ const iamId = m.iam_id;
104
+ const alias = uidToAlias.get(iamId) ?? iamId;
105
+ memberAliases.push(alias);
106
+ }
107
+ }
108
+ catch { /* no members */ }
109
+ // Fetch tags
110
+ let teamTags = [];
111
+ try {
112
+ const tagResp = await client.get(`/v1/teams/${teamId}/tags`);
113
+ teamTags = tagResp.tags ?? [];
114
+ }
115
+ catch { /* no tags */ }
116
+ backupTeams.push({
117
+ name: team.name,
118
+ description: team.description ?? null,
119
+ protocol: team.protocol ?? null,
120
+ tags: teamTags,
121
+ members: memberAliases,
122
+ });
123
+ if (!json)
124
+ process.stderr.write(`${memberAliases.length} member(s), ${teamTags.length} tag(s)\n`);
125
+ }
126
+ }
127
+ catch { /* no teams */ }
83
128
  const backup = {
84
- version: 1,
129
+ version: 2,
85
130
  created_at: new Date().toISOString(),
86
131
  faces: backupFaces,
132
+ teams: backupTeams,
87
133
  };
88
134
  const backupDir = path.join(os.homedir(), '.faces', 'backups');
89
135
  fs.mkdirSync(backupDir, { recursive: true });
@@ -94,8 +140,8 @@ export default class CatalogBackup extends BaseCommand {
94
140
  const totalThreads = backupFaces.reduce((n, f) => n + f.threads.length, 0);
95
141
  if (!json) {
96
142
  this.log(`\nBackup saved: ${filePath}`);
97
- this.log(` ${backupFaces.length} face(s), ${totalDocs} document(s), ${totalThreads} thread(s)`);
143
+ this.log(` ${backupFaces.length} face(s), ${totalDocs} document(s), ${totalThreads} thread(s), ${backupTeams.length} team(s)`);
98
144
  }
99
- return { path: filePath, faces: backupFaces.length, documents: totalDocs, threads: totalThreads };
145
+ return { path: filePath, faces: backupFaces.length, documents: totalDocs, threads: totalThreads, teams: backupTeams.length };
100
146
  }
101
147
  }