faces-cli 1.3.1 → 1.4.0
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 +36 -0
- package/dist/catalog.js +167 -0
- package/dist/client.js +2 -1
- package/dist/commands/auth/register.js +1 -0
- package/dist/commands/auth/whoami.js +1 -1
- package/dist/commands/catalog/doctor.d.ts +12 -0
- package/dist/commands/catalog/doctor.js +196 -0
- package/dist/commands/catalog/list.d.ts +10 -0
- package/dist/commands/catalog/list.js +35 -0
- package/dist/commands/chat/chat.d.ts +4 -0
- package/dist/commands/chat/chat.js +146 -39
- package/dist/commands/chat/messages.js +1 -1
- package/dist/commands/chat/responses.js +1 -1
- package/dist/commands/compile/doc/create.d.ts +1 -0
- package/dist/commands/compile/doc/create.js +5 -2
- package/dist/commands/compile/doc/edit.d.ts +17 -0
- package/dist/commands/compile/doc/edit.js +48 -0
- package/dist/commands/compile/doc/index.d.ts +18 -0
- package/dist/commands/compile/doc/index.js +95 -0
- package/dist/commands/compile/doc/list.js +1 -1
- package/dist/commands/compile/doc/make.d.ts +14 -0
- package/dist/commands/compile/doc/make.js +55 -0
- package/dist/commands/compile/import.js +2 -3
- package/dist/commands/compile/thread/create.js +2 -2
- package/dist/commands/compile/{doc/sync.d.ts → thread/delete.d.ts} +2 -2
- package/dist/commands/compile/{doc/sync.js → thread/delete.js} +7 -7
- package/dist/commands/compile/thread/edit.d.ts +14 -0
- package/dist/commands/compile/thread/edit.js +29 -0
- package/dist/commands/compile/{doc/prepare.d.ts → thread/get.d.ts} +2 -2
- package/dist/commands/compile/{doc/prepare.js → thread/get.js} +5 -5
- package/dist/commands/compile/thread/list.js +1 -1
- package/dist/commands/compile/thread/message.js +1 -1
- package/dist/commands/face/create.d.ts +2 -0
- package/dist/commands/face/create.js +17 -2
- package/dist/commands/face/delete.js +6 -0
- package/dist/commands/face/get.js +5 -3
- package/dist/commands/face/list.js +20 -3
- package/dist/commands/face/update.d.ts +2 -0
- package/dist/commands/face/update.js +39 -9
- package/dist/config.d.ts +2 -0
- package/dist/poll.d.ts +30 -0
- package/dist/poll.js +40 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +16 -0
- package/oclif.manifest.json +970 -477
- package/package.json +4 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export declare const CATALOG_DIR: string;
|
|
2
|
+
export declare const CATALOG_INDEX: string;
|
|
3
|
+
export interface FaceFrontmatter {
|
|
4
|
+
name: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
facename: string;
|
|
7
|
+
attributes?: Record<string, string>;
|
|
8
|
+
component_counts?: {
|
|
9
|
+
alpha: number;
|
|
10
|
+
beta: number;
|
|
11
|
+
delta: number;
|
|
12
|
+
epsilon: number;
|
|
13
|
+
};
|
|
14
|
+
profile_token_count?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface FaceDataInput {
|
|
17
|
+
name: string;
|
|
18
|
+
id: string;
|
|
19
|
+
uid?: string;
|
|
20
|
+
basic_facts?: Record<string, string | {
|
|
21
|
+
value: string;
|
|
22
|
+
}> | null;
|
|
23
|
+
component_counts?: {
|
|
24
|
+
alpha: number;
|
|
25
|
+
beta: number;
|
|
26
|
+
delta: number;
|
|
27
|
+
epsilon: number;
|
|
28
|
+
} | null;
|
|
29
|
+
profile_token_count?: number | null;
|
|
30
|
+
}
|
|
31
|
+
export declare class CatalogService {
|
|
32
|
+
isEnabled(): boolean;
|
|
33
|
+
writeFace(faceData: FaceDataInput, description?: string): void;
|
|
34
|
+
deleteFace(username: string): void;
|
|
35
|
+
rebuildIndex(): void;
|
|
36
|
+
}
|
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { loadConfig } from './config.js';
|
|
5
|
+
export const CATALOG_DIR = path.join(os.homedir(), '.faces', 'catalog');
|
|
6
|
+
export const CATALOG_INDEX = path.join(os.homedir(), '.faces', 'catalog.json');
|
|
7
|
+
function quoteYaml(value) {
|
|
8
|
+
if (/[:#\[\]{}&*!|>'"%@`]/.test(value) || value !== value.trim() || value === '') {
|
|
9
|
+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function serializeFrontmatter(fm) {
|
|
14
|
+
const lines = ['---'];
|
|
15
|
+
lines.push(`name: ${quoteYaml(fm.name)}`);
|
|
16
|
+
if (fm.description)
|
|
17
|
+
lines.push(`description: ${quoteYaml(fm.description)}`);
|
|
18
|
+
lines.push(`facename: ${quoteYaml(fm.facename)}`);
|
|
19
|
+
if (fm.attributes && Object.keys(fm.attributes).length > 0) {
|
|
20
|
+
lines.push('attributes:');
|
|
21
|
+
for (const [k, v] of Object.entries(fm.attributes)) {
|
|
22
|
+
lines.push(` ${k}: ${quoteYaml(String(v))}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (fm.component_counts) {
|
|
26
|
+
const cc = fm.component_counts;
|
|
27
|
+
lines.push(`component_counts: ${cc.alpha + cc.beta + cc.delta + cc.epsilon}`);
|
|
28
|
+
}
|
|
29
|
+
if (fm.profile_token_count) {
|
|
30
|
+
lines.push(`profile_token_count: ${fm.profile_token_count}`);
|
|
31
|
+
}
|
|
32
|
+
lines.push('---');
|
|
33
|
+
return lines.join('\n');
|
|
34
|
+
}
|
|
35
|
+
function parseFrontmatter(content) {
|
|
36
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
37
|
+
if (!match)
|
|
38
|
+
return { frontmatter: null, body: content };
|
|
39
|
+
const yamlBlock = match[1];
|
|
40
|
+
const body = match[2];
|
|
41
|
+
const fm = {};
|
|
42
|
+
let inAttributes = false;
|
|
43
|
+
const attributes = {};
|
|
44
|
+
for (const line of yamlBlock.split('\n')) {
|
|
45
|
+
if (line.startsWith(' ') && inAttributes) {
|
|
46
|
+
const colonIdx = line.indexOf(':');
|
|
47
|
+
if (colonIdx > 0) {
|
|
48
|
+
const k = line.slice(0, colonIdx).trim();
|
|
49
|
+
const v = line.slice(colonIdx + 1).trim().replace(/^"(.*)"$/, '$1');
|
|
50
|
+
attributes[k] = v;
|
|
51
|
+
}
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
inAttributes = false;
|
|
55
|
+
const colonIdx = line.indexOf(':');
|
|
56
|
+
if (colonIdx < 0)
|
|
57
|
+
continue;
|
|
58
|
+
const key = line.slice(0, colonIdx).trim();
|
|
59
|
+
const val = line.slice(colonIdx + 1).trim().replace(/^"(.*)"$/, '$1');
|
|
60
|
+
if (key === 'name')
|
|
61
|
+
fm.name = val;
|
|
62
|
+
else if (key === 'description')
|
|
63
|
+
fm.description = val;
|
|
64
|
+
else if (key === 'facename')
|
|
65
|
+
fm.facename = val;
|
|
66
|
+
else if (key === 'attributes')
|
|
67
|
+
inAttributes = true;
|
|
68
|
+
}
|
|
69
|
+
if (Object.keys(attributes).length > 0)
|
|
70
|
+
fm.attributes = attributes;
|
|
71
|
+
if (!fm.name || !fm.facename)
|
|
72
|
+
return { frontmatter: null, body: content };
|
|
73
|
+
return { frontmatter: fm, body };
|
|
74
|
+
}
|
|
75
|
+
const DEFAULT_BODY = '\n## Notes\n\n';
|
|
76
|
+
export class CatalogService {
|
|
77
|
+
isEnabled() {
|
|
78
|
+
const cfg = loadConfig();
|
|
79
|
+
return cfg.catalog !== 'false';
|
|
80
|
+
}
|
|
81
|
+
writeFace(faceData, description) {
|
|
82
|
+
if (!this.isEnabled())
|
|
83
|
+
return;
|
|
84
|
+
try {
|
|
85
|
+
const username = faceData.id;
|
|
86
|
+
const dir = path.join(CATALOG_DIR, username);
|
|
87
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
88
|
+
const filePath = path.join(dir, 'FACE.md');
|
|
89
|
+
let existingBody = DEFAULT_BODY;
|
|
90
|
+
let existingDescription;
|
|
91
|
+
if (fs.existsSync(filePath)) {
|
|
92
|
+
const existing = fs.readFileSync(filePath, 'utf8');
|
|
93
|
+
const parsed = parseFrontmatter(existing);
|
|
94
|
+
if (parsed.body.trim())
|
|
95
|
+
existingBody = parsed.body;
|
|
96
|
+
if (parsed.frontmatter?.description)
|
|
97
|
+
existingDescription = parsed.frontmatter.description;
|
|
98
|
+
}
|
|
99
|
+
const fm = {
|
|
100
|
+
name: faceData.name,
|
|
101
|
+
facename: username,
|
|
102
|
+
};
|
|
103
|
+
if (description) {
|
|
104
|
+
fm.description = description;
|
|
105
|
+
}
|
|
106
|
+
else if (existingDescription) {
|
|
107
|
+
fm.description = existingDescription;
|
|
108
|
+
}
|
|
109
|
+
if (faceData.basic_facts && Object.keys(faceData.basic_facts).length > 0) {
|
|
110
|
+
const attrs = {};
|
|
111
|
+
for (const [k, v] of Object.entries(faceData.basic_facts)) {
|
|
112
|
+
attrs[k] = typeof v === 'object' && v !== null && 'value' in v ? v.value : String(v);
|
|
113
|
+
}
|
|
114
|
+
fm.attributes = attrs;
|
|
115
|
+
}
|
|
116
|
+
if (faceData.component_counts)
|
|
117
|
+
fm.component_counts = faceData.component_counts;
|
|
118
|
+
if (faceData.profile_token_count)
|
|
119
|
+
fm.profile_token_count = faceData.profile_token_count;
|
|
120
|
+
const content = serializeFrontmatter(fm) + '\n' + existingBody;
|
|
121
|
+
fs.writeFileSync(filePath, content);
|
|
122
|
+
this.rebuildIndex();
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
126
|
+
process.stderr.write(`warn: catalog write failed: ${msg}\n`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
deleteFace(username) {
|
|
130
|
+
if (!this.isEnabled())
|
|
131
|
+
return;
|
|
132
|
+
try {
|
|
133
|
+
const dir = path.join(CATALOG_DIR, username);
|
|
134
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
135
|
+
this.rebuildIndex();
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
139
|
+
process.stderr.write(`warn: catalog delete failed: ${msg}\n`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
rebuildIndex() {
|
|
143
|
+
if (!this.isEnabled())
|
|
144
|
+
return;
|
|
145
|
+
try {
|
|
146
|
+
fs.mkdirSync(CATALOG_DIR, { recursive: true });
|
|
147
|
+
const entries = [];
|
|
148
|
+
const dirs = fs.readdirSync(CATALOG_DIR, { withFileTypes: true });
|
|
149
|
+
for (const dirent of dirs) {
|
|
150
|
+
if (!dirent.isDirectory())
|
|
151
|
+
continue;
|
|
152
|
+
const faceMdPath = path.join(CATALOG_DIR, dirent.name, 'FACE.md');
|
|
153
|
+
if (!fs.existsSync(faceMdPath))
|
|
154
|
+
continue;
|
|
155
|
+
const content = fs.readFileSync(faceMdPath, 'utf8');
|
|
156
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
157
|
+
if (frontmatter)
|
|
158
|
+
entries.push(frontmatter);
|
|
159
|
+
}
|
|
160
|
+
fs.writeFileSync(CATALOG_INDEX, JSON.stringify(entries, null, 2));
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
164
|
+
process.stderr.write(`warn: catalog index rebuild failed: ${msg}\n`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
package/dist/client.js
CHANGED
|
@@ -41,7 +41,8 @@ export class FacesClient {
|
|
|
41
41
|
let msg = resp.statusText;
|
|
42
42
|
try {
|
|
43
43
|
const body = await resp.json();
|
|
44
|
-
|
|
44
|
+
const raw = body.detail ?? body.error ?? body.message ?? msg;
|
|
45
|
+
msg = typeof raw === 'object' ? JSON.stringify(raw) : String(raw);
|
|
45
46
|
}
|
|
46
47
|
catch {
|
|
47
48
|
// use statusText
|
|
@@ -10,7 +10,7 @@ export default class AuthWhoami extends BaseCommand {
|
|
|
10
10
|
const client = this.makeClient(flags);
|
|
11
11
|
let data;
|
|
12
12
|
try {
|
|
13
|
-
data = await client.get('/
|
|
13
|
+
data = await client.get('/auth/me', { requireJwt: true });
|
|
14
14
|
}
|
|
15
15
|
catch (err) {
|
|
16
16
|
if (err instanceof FacesAPIError)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base.js';
|
|
2
|
+
export default class CatalogDoctor extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static flags: {
|
|
5
|
+
fix: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
6
|
+
generate: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<unknown>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { Flags } from '@oclif/core';
|
|
4
|
+
import { BaseCommand } from '../../base.js';
|
|
5
|
+
import { FacesAPIError } from '../../client.js';
|
|
6
|
+
import { loadConfig } from '../../config.js';
|
|
7
|
+
import { CatalogService, CATALOG_DIR } from '../../catalog.js';
|
|
8
|
+
export default class CatalogDoctor extends BaseCommand {
|
|
9
|
+
static description = 'Diagnose and repair the local face catalog';
|
|
10
|
+
static flags = {
|
|
11
|
+
...BaseCommand.baseFlags,
|
|
12
|
+
fix: Flags.boolean({ description: 'Rebuild missing or stale catalog entries from API', default: false }),
|
|
13
|
+
generate: Flags.boolean({ description: 'Fix + generate missing descriptions via LLM', default: false }),
|
|
14
|
+
};
|
|
15
|
+
async run() {
|
|
16
|
+
const { flags } = await this.parse(CatalogDoctor);
|
|
17
|
+
const client = this.makeClient(flags);
|
|
18
|
+
const catalog = new CatalogService();
|
|
19
|
+
// Fetch all faces from API
|
|
20
|
+
let remoteFaces;
|
|
21
|
+
try {
|
|
22
|
+
const resp = await client.get('/v1/faces');
|
|
23
|
+
const arr = resp.data ?? resp;
|
|
24
|
+
// List endpoint returns minimal fields; fetch full details for each
|
|
25
|
+
remoteFaces = await Promise.all(arr.map(async (f) => {
|
|
26
|
+
try {
|
|
27
|
+
return await client.get(`/v1/faces/${f.id}`);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return { ...f, uid: f.id };
|
|
31
|
+
}
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
if (err instanceof FacesAPIError)
|
|
36
|
+
this.error(`Error (${err.statusCode}): ${err.message}`);
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
// Build map of remote faces by username
|
|
40
|
+
const remoteByUsername = new Map();
|
|
41
|
+
for (const f of remoteFaces) {
|
|
42
|
+
remoteByUsername.set(f.id, f);
|
|
43
|
+
}
|
|
44
|
+
// Read local catalog
|
|
45
|
+
const localUsernames = new Set();
|
|
46
|
+
const localFrontmatters = new Map();
|
|
47
|
+
try {
|
|
48
|
+
if (fs.existsSync(CATALOG_DIR)) {
|
|
49
|
+
for (const dirent of fs.readdirSync(CATALOG_DIR, { withFileTypes: true })) {
|
|
50
|
+
if (!dirent.isDirectory())
|
|
51
|
+
continue;
|
|
52
|
+
localUsernames.add(dirent.name);
|
|
53
|
+
const faceMdPath = path.join(CATALOG_DIR, dirent.name, 'FACE.md');
|
|
54
|
+
if (fs.existsSync(faceMdPath)) {
|
|
55
|
+
const content = fs.readFileSync(faceMdPath, 'utf8');
|
|
56
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
57
|
+
if (match) {
|
|
58
|
+
// Quick parse for name and description
|
|
59
|
+
const fm = { facename: dirent.name };
|
|
60
|
+
for (const line of match[1].split('\n')) {
|
|
61
|
+
if (line.startsWith(' '))
|
|
62
|
+
continue;
|
|
63
|
+
const ci = line.indexOf(':');
|
|
64
|
+
if (ci < 0)
|
|
65
|
+
continue;
|
|
66
|
+
const k = line.slice(0, ci).trim();
|
|
67
|
+
const v = line.slice(ci + 1).trim().replace(/^"(.*)"$/, '$1');
|
|
68
|
+
if (k === 'name')
|
|
69
|
+
fm.name = v;
|
|
70
|
+
if (k === 'description')
|
|
71
|
+
fm.description = v;
|
|
72
|
+
}
|
|
73
|
+
if (fm.name)
|
|
74
|
+
localFrontmatters.set(dirent.name, fm);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch { /* continue with what we have */ }
|
|
81
|
+
// Compute diffs
|
|
82
|
+
const missing = [];
|
|
83
|
+
const stale = [];
|
|
84
|
+
const noDescription = [];
|
|
85
|
+
const orphaned = [];
|
|
86
|
+
for (const [username, remote] of remoteByUsername) {
|
|
87
|
+
if (!localUsernames.has(username)) {
|
|
88
|
+
missing.push(remote);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const local = localFrontmatters.get(username);
|
|
92
|
+
if (local && local.name !== remote.name) {
|
|
93
|
+
stale.push(remote);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const local = localFrontmatters.get(username);
|
|
97
|
+
if (!local?.description)
|
|
98
|
+
noDescription.push(username);
|
|
99
|
+
}
|
|
100
|
+
for (const username of localUsernames) {
|
|
101
|
+
if (!remoteByUsername.has(username))
|
|
102
|
+
orphaned.push(username);
|
|
103
|
+
}
|
|
104
|
+
const doFix = flags.fix || flags.generate;
|
|
105
|
+
if (!doFix) {
|
|
106
|
+
// Diagnostic mode
|
|
107
|
+
const issues = [];
|
|
108
|
+
if (missing.length > 0)
|
|
109
|
+
issues.push(`${missing.length} face(s) missing from catalog`);
|
|
110
|
+
if (stale.length > 0)
|
|
111
|
+
issues.push(`${stale.length} face(s) out of sync`);
|
|
112
|
+
if (orphaned.length > 0)
|
|
113
|
+
issues.push(`${orphaned.length} orphaned catalog entries (face deleted on server)`);
|
|
114
|
+
if (noDescription.length > 0)
|
|
115
|
+
issues.push(`${noDescription.length} face(s) without a description`);
|
|
116
|
+
if (issues.length === 0) {
|
|
117
|
+
this.log('Catalog is healthy.');
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
for (const issue of issues)
|
|
121
|
+
this.log(issue);
|
|
122
|
+
this.log('');
|
|
123
|
+
this.log('Run faces catalog:doctor --fix to repair catalog');
|
|
124
|
+
if (noDescription.length > 0) {
|
|
125
|
+
this.log('Run faces catalog:doctor --generate to also create missing descriptions');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { missing: missing.length, stale: stale.length, orphaned: orphaned.length, noDescription: noDescription.length };
|
|
129
|
+
}
|
|
130
|
+
// Fix mode: rebuild missing and stale entries
|
|
131
|
+
let fixed = 0;
|
|
132
|
+
for (const face of [...missing, ...stale]) {
|
|
133
|
+
catalog.writeFace(face);
|
|
134
|
+
fixed++;
|
|
135
|
+
}
|
|
136
|
+
// Also refresh faces that exist but might have stale attributes
|
|
137
|
+
for (const [username, remote] of remoteByUsername) {
|
|
138
|
+
if (!missing.some((f) => f.id === username) && !stale.some((f) => f.id === username)) {
|
|
139
|
+
// Face exists locally and name matches — still refresh attributes
|
|
140
|
+
catalog.writeFace(remote);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Remove orphans
|
|
144
|
+
let removed = 0;
|
|
145
|
+
for (const username of orphaned) {
|
|
146
|
+
catalog.deleteFace(username);
|
|
147
|
+
removed++;
|
|
148
|
+
}
|
|
149
|
+
this.log(`Fixed ${fixed} catalog entries, removed ${removed} orphans.`);
|
|
150
|
+
// Generate mode: create missing descriptions
|
|
151
|
+
if (flags.generate && noDescription.length > 0) {
|
|
152
|
+
const cfg = loadConfig();
|
|
153
|
+
const catalogModel = cfg.catalog_model ?? 'gpt-5-nano';
|
|
154
|
+
this.log(`Generating descriptions for ${noDescription.length} face(s) via ${catalogModel}...`);
|
|
155
|
+
let generated = 0;
|
|
156
|
+
for (const username of noDescription) {
|
|
157
|
+
const remote = remoteByUsername.get(username);
|
|
158
|
+
if (!remote)
|
|
159
|
+
continue;
|
|
160
|
+
try {
|
|
161
|
+
const prompt = 'Describe yourself in one paragraph. Who are you, what do you care about, and what kind of questions are you best suited to answer?';
|
|
162
|
+
const faceModel = `${username}@${catalogModel}`;
|
|
163
|
+
const isAnthropic = catalogModel.startsWith('claude');
|
|
164
|
+
let text;
|
|
165
|
+
if (isAnthropic) {
|
|
166
|
+
const resp = await client.post('/v1/messages', {
|
|
167
|
+
body: { model: faceModel, messages: [{ role: 'user', content: prompt }], max_tokens: 256 },
|
|
168
|
+
});
|
|
169
|
+
text = resp.content?.find((b) => b.type === 'text')?.text;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const resp = await client.post('/v1/chat/completions', {
|
|
173
|
+
body: { model: faceModel, messages: [{ role: 'user', content: prompt }], max_tokens: 256 },
|
|
174
|
+
});
|
|
175
|
+
text = resp.choices?.[0]?.message?.content;
|
|
176
|
+
}
|
|
177
|
+
if (text) {
|
|
178
|
+
catalog.writeFace(remote, text.trim());
|
|
179
|
+
generated++;
|
|
180
|
+
this.log(` ${username}: done`);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
this.log(` ${username}: no response`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
const msg = err instanceof FacesAPIError ? `Error (${err.statusCode}): ${err.message}` : String(err);
|
|
188
|
+
this.log(` ${username}: failed — ${msg}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
this.log(`Generated ${generated} description(s).`);
|
|
192
|
+
}
|
|
193
|
+
catalog.rebuildIndex();
|
|
194
|
+
return { fixed, removed, generated: flags.generate ? noDescription.length : 0 };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base.js';
|
|
2
|
+
export default class CatalogList 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
|
+
run(): Promise<unknown>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { BaseCommand } from '../../base.js';
|
|
3
|
+
import { CATALOG_INDEX } from '../../catalog.js';
|
|
4
|
+
export default class CatalogList extends BaseCommand {
|
|
5
|
+
static description = 'List all faces in the local catalog';
|
|
6
|
+
static flags = {
|
|
7
|
+
...BaseCommand.baseFlags,
|
|
8
|
+
};
|
|
9
|
+
async run() {
|
|
10
|
+
let entries = [];
|
|
11
|
+
try {
|
|
12
|
+
if (fs.existsSync(CATALOG_INDEX)) {
|
|
13
|
+
entries = JSON.parse(fs.readFileSync(CATALOG_INDEX, 'utf8'));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
this.error('Could not read catalog.json');
|
|
18
|
+
}
|
|
19
|
+
if (this.jsonEnabled())
|
|
20
|
+
return entries;
|
|
21
|
+
if (entries.length === 0) {
|
|
22
|
+
this.log('(no faces in catalog — run faces catalog:doctor --fix)');
|
|
23
|
+
return entries;
|
|
24
|
+
}
|
|
25
|
+
const nameWidth = Math.max(...entries.map((e) => String(e.facename ?? '').length));
|
|
26
|
+
const displayWidth = Math.max(...entries.map((e) => String(e.name ?? '').length));
|
|
27
|
+
for (const e of entries) {
|
|
28
|
+
const facename = String(e.facename ?? '').padEnd(nameWidth);
|
|
29
|
+
const name = String(e.name ?? '').padEnd(displayWidth);
|
|
30
|
+
const desc = e.description ? ` ${String(e.description).slice(0, 60)}...` : '';
|
|
31
|
+
this.log(`${facename} ${name}${desc}`);
|
|
32
|
+
}
|
|
33
|
+
return entries;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -9,6 +9,7 @@ export default class ChatChat extends BaseCommand {
|
|
|
9
9
|
'max-tokens': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
10
|
temperature: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
11
|
file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
responses: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
13
|
'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
14
|
token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
15
|
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -17,4 +18,7 @@ export default class ChatChat extends BaseCommand {
|
|
|
17
18
|
face_username: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
18
19
|
};
|
|
19
20
|
run(): Promise<unknown>;
|
|
21
|
+
private runChatCompletions;
|
|
22
|
+
private runMessages;
|
|
23
|
+
private runResponses;
|
|
20
24
|
}
|