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.
- package/README.md +50 -17
- package/dist/catalog.d.ts +13 -1
- package/dist/catalog.js +78 -9
- package/dist/client.d.ts +4 -0
- package/dist/client.js +16 -0
- package/dist/commands/catalog/backup.js +64 -18
- package/dist/commands/catalog/doctor.js +72 -16
- package/dist/commands/catalog/restore.js +64 -11
- package/dist/commands/face/create.d.ts +1 -0
- package/dist/commands/face/create.js +27 -5
- package/dist/commands/face/get.d.ts +1 -0
- package/dist/commands/face/get.js +13 -9
- package/dist/commands/face/list.d.ts +3 -0
- package/dist/commands/face/list.js +21 -6
- package/dist/commands/face/tag/add.d.ts +14 -0
- package/dist/commands/face/tag/add.js +40 -0
- package/dist/commands/face/tag/all.d.ts +10 -0
- package/dist/commands/face/tag/all.js +31 -0
- package/dist/commands/face/tag/list.d.ts +13 -0
- package/dist/commands/face/tag/list.js +35 -0
- package/dist/commands/face/tag/remove.d.ts +14 -0
- package/dist/commands/face/tag/remove.js +40 -0
- package/dist/commands/face/tag/set.d.ts +14 -0
- package/dist/commands/face/tag/set.js +40 -0
- package/dist/commands/face/teams.d.ts +14 -0
- package/dist/commands/face/teams.js +29 -0
- package/dist/commands/face/update.d.ts +1 -0
- package/dist/commands/face/update.js +34 -11
- package/dist/commands/team/add.d.ts +14 -0
- package/dist/commands/team/add.js +68 -0
- package/dist/commands/team/create.d.ts +15 -0
- package/dist/commands/team/create.js +69 -0
- package/dist/commands/team/delete.d.ts +15 -0
- package/dist/commands/team/delete.js +44 -0
- package/dist/commands/team/get.d.ts +13 -0
- package/dist/commands/team/get.js +28 -0
- package/dist/commands/team/list.d.ts +10 -0
- package/dist/commands/team/list.js +40 -0
- package/dist/commands/team/members.d.ts +13 -0
- package/dist/commands/team/members.js +28 -0
- package/dist/commands/team/remove.d.ts +14 -0
- package/dist/commands/team/remove.js +56 -0
- package/dist/commands/team/tag/add.d.ts +14 -0
- package/dist/commands/team/tag/add.js +29 -0
- package/dist/commands/team/tag/list.d.ts +13 -0
- package/dist/commands/team/tag/list.js +35 -0
- package/dist/commands/team/tag/remove.d.ts +14 -0
- package/dist/commands/team/tag/remove.js +29 -0
- package/dist/commands/team/tag/set.d.ts +14 -0
- package/dist/commands/team/tag/set.js +29 -0
- package/dist/commands/team/update.d.ts +17 -0
- package/dist/commands/team/update.js +78 -0
- package/dist/team-catalog.d.ts +17 -0
- package/dist/team-catalog.js +104 -0
- package/dist/utils.d.ts +9 -2
- package/dist/utils.js +17 -2
- package/oclif.manifest.json +1537 -309
- 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
|
|
11
|
+
Requires Node.js >= 18.
|
|
12
12
|
|
|
13
13
|
## Quick start
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
|
|
17
|
-
faces
|
|
18
|
-
faces
|
|
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
|
|
25
|
-
faces
|
|
26
|
-
|
|
27
|
-
faces
|
|
28
|
-
faces
|
|
29
|
-
|
|
30
|
-
faces
|
|
31
|
-
|
|
32
|
-
faces
|
|
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
|
|
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=
|
|
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(
|
|
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;
|
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 {
|
|
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
|
-
|
|
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:
|
|
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
|
}
|