faces-cli 1.6.15 → 1.6.16

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.
@@ -2,6 +2,7 @@ import { Args, Flags } from '@oclif/core';
2
2
  import fs from 'node:fs';
3
3
  import { BaseCommand } from '../../base.js';
4
4
  import { FacesAPIError } from '../../client.js';
5
+ import { extractStreamDelta } from '../../utils.js';
5
6
  function isAnthropicModel(model) {
6
7
  // Extract model name from face@model format
7
8
  const parts = model.split('@');
@@ -91,18 +92,18 @@ export default class ChatChat extends BaseCommand {
91
92
  if (flags['oauth-only'])
92
93
  payload.oauth_only = true;
93
94
  if (flags.stream) {
95
+ // The backend may re-route /v1/chat/completions to /v1/messages or
96
+ // /v1/responses, so handle all three SSE event shapes.
94
97
  for await (const line of client.stream('/v1/chat/completions', payload)) {
95
98
  if (!line.startsWith('data: '))
96
99
  continue;
97
100
  const chunk = line.slice(6);
98
- if (chunk.trim() === '[DONE]')
99
- break;
101
+ if (chunk.trim() === '[DONE]' || chunk.trim() === '')
102
+ continue;
100
103
  try {
101
- const parsed = JSON.parse(chunk);
102
- const choices = parsed?.choices;
103
- const delta = choices?.[0]?.delta?.content ?? '';
104
+ const delta = extractStreamDelta(JSON.parse(chunk));
104
105
  if (delta)
105
- process.stdout.write(String(delta));
106
+ process.stdout.write(delta);
106
107
  }
107
108
  catch { /* ignore parse errors */ }
108
109
  }
@@ -149,12 +150,9 @@ export default class ChatChat extends BaseCommand {
149
150
  if (chunk.trim() === '[DONE]' || chunk.trim() === '')
150
151
  continue;
151
152
  try {
152
- const obj = JSON.parse(chunk);
153
- if (obj.type === 'content_block_delta') {
154
- const delta = obj.delta?.text ?? '';
155
- if (delta)
156
- process.stdout.write(String(delta));
157
- }
153
+ const delta = extractStreamDelta(JSON.parse(chunk));
154
+ if (delta)
155
+ process.stdout.write(delta);
158
156
  }
159
157
  catch { /* ignore */ }
160
158
  }
@@ -195,12 +193,9 @@ export default class ChatChat extends BaseCommand {
195
193
  if (chunk.trim() === '[DONE]' || chunk.trim() === '')
196
194
  continue;
197
195
  try {
198
- const obj = JSON.parse(chunk);
199
- if (obj.type === 'response.output_text.delta') {
200
- const delta = obj.delta ?? '';
201
- if (delta)
202
- process.stdout.write(String(delta));
203
- }
196
+ const delta = extractStreamDelta(JSON.parse(chunk));
197
+ if (delta)
198
+ process.stdout.write(delta);
204
199
  }
205
200
  catch { /* ignore */ }
206
201
  }
@@ -0,0 +1,38 @@
1
+ import { BaseCommand } from '../../base.js';
2
+ export default class ChatThread extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ id: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ message: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ llm: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ system: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ 'max-tokens': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ temperature: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ stream: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ 'oauth-only': import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ list: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ show: import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
+ delete: import("@oclif/core/interfaces").BooleanFlag<boolean>;
18
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
+ 'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
+ };
22
+ static args: {
23
+ face_username: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
24
+ };
25
+ run(): Promise<unknown>;
26
+ private handleList;
27
+ private handleDelete;
28
+ private handleShow;
29
+ private handleMessage;
30
+ private sendOpenAI;
31
+ private sendAnthropic;
32
+ /**
33
+ * Stream an SSE response, printing deltas to stdout as they arrive while
34
+ * accumulating the full text to persist on the thread. Handles any of the
35
+ * three event shapes, so a re-routed stream still renders correctly.
36
+ */
37
+ private streamAndAccumulate;
38
+ }
@@ -0,0 +1,325 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { BaseCommand } from '../../base.js';
7
+ import { FacesAPIError } from '../../client.js';
8
+ import { extractStreamDelta } from '../../utils.js';
9
+ const THREADS_DIR = path.join(os.homedir(), '.faces', 'threads');
10
+ function isAnthropicModel(model) {
11
+ const parts = model.split('@');
12
+ const llm = parts.length > 1 ? parts[parts.length - 1] : '';
13
+ return llm.startsWith('claude');
14
+ }
15
+ /**
16
+ * Extract assistant text from any response body shape. The backend may serve a
17
+ * request at the endpoint we hit, or re-route it (signalled by the
18
+ * x-faces-routed-endpoint header) — so handle all three families regardless of
19
+ * which endpoint we posted to: Anthropic Messages, OpenAI Responses, and Chat
20
+ * Completions.
21
+ */
22
+ function extractAssistantText(body, routedEndpoint) {
23
+ // Anthropic Messages shape: { type: 'message', content: [{type:'text', text}] }
24
+ if (routedEndpoint === '/v1/messages' || (Array.isArray(body.content) && body.type === 'message')) {
25
+ let text = '';
26
+ for (const block of body.content ?? []) {
27
+ if (block.type === 'text')
28
+ text += String(block.text ?? '');
29
+ }
30
+ if (text)
31
+ return text;
32
+ }
33
+ // OpenAI Responses shape: { output: [{content: [{type:'output_text', text}]}] }
34
+ if (routedEndpoint === '/v1/responses' || Array.isArray(body.output)) {
35
+ let text = '';
36
+ for (const item of body.output ?? []) {
37
+ for (const block of item.content ?? []) {
38
+ if (block.type === 'output_text')
39
+ text += String(block.text ?? '');
40
+ }
41
+ }
42
+ if (text)
43
+ return text;
44
+ }
45
+ // Standard Chat Completions shape: { choices: [{message: {content}}] }
46
+ const choices = body.choices;
47
+ const message = choices?.[0]?.message;
48
+ return String(message?.content ?? '');
49
+ }
50
+ function newThreadId() {
51
+ return 't_' + crypto.randomBytes(6).toString('hex');
52
+ }
53
+ function threadPath(id) {
54
+ return path.join(THREADS_DIR, `${id}.json`);
55
+ }
56
+ function ensureThreadsDir() {
57
+ if (!fs.existsSync(THREADS_DIR))
58
+ fs.mkdirSync(THREADS_DIR, { recursive: true, mode: 0o700 });
59
+ }
60
+ function loadThread(id) {
61
+ const p = threadPath(id);
62
+ if (!fs.existsSync(p))
63
+ return null;
64
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
65
+ }
66
+ function saveThread(t) {
67
+ ensureThreadsDir();
68
+ t.updated_at = new Date().toISOString();
69
+ fs.writeFileSync(threadPath(t.id), JSON.stringify(t, null, 2), { mode: 0o600 });
70
+ }
71
+ function listThreads() {
72
+ if (!fs.existsSync(THREADS_DIR))
73
+ return [];
74
+ const out = [];
75
+ for (const f of fs.readdirSync(THREADS_DIR)) {
76
+ if (!f.endsWith('.json'))
77
+ continue;
78
+ try {
79
+ out.push(JSON.parse(fs.readFileSync(path.join(THREADS_DIR, f), 'utf8')));
80
+ }
81
+ catch { /* skip corrupt */ }
82
+ }
83
+ out.sort((a, b) => (b.updated_at ?? '').localeCompare(a.updated_at ?? ''));
84
+ return out;
85
+ }
86
+ export default class ChatThread extends BaseCommand {
87
+ static description = 'Multi-turn chat thread. Conversation history is stored under ~/.faces/threads/<id>.json — resume any thread later with --id.';
88
+ static examples = [
89
+ '<%= config.bin %> <%= command.id %> socrates -m "What is justice?"',
90
+ '<%= config.bin %> <%= command.id %> --id t_abc123 -m "Say more about that"',
91
+ '<%= config.bin %> <%= command.id %> --list',
92
+ '<%= config.bin %> <%= command.id %> --id t_abc123 --show',
93
+ '<%= config.bin %> <%= command.id %> --id t_abc123 --delete',
94
+ ];
95
+ static flags = {
96
+ ...BaseCommand.baseFlags,
97
+ id: Flags.string({ description: 'Resume an existing thread by id' }),
98
+ message: Flags.string({ char: 'm', description: 'User message' }),
99
+ file: Flags.string({ description: 'Read message from file' }),
100
+ llm: Flags.string({ description: 'LLM override (only honored when starting a new thread)' }),
101
+ system: Flags.string({ description: 'System prompt (only honored when starting a new thread)' }),
102
+ 'max-tokens': Flags.integer({ description: 'Max tokens' }),
103
+ temperature: Flags.string({ description: 'Temperature (0.0-2.0, OpenAI/Chat Completions only)' }),
104
+ stream: Flags.boolean({ description: 'Stream the response', default: false }),
105
+ 'oauth-only': Flags.boolean({ description: 'Prevent fallback to paid system key', default: false }),
106
+ list: Flags.boolean({ description: 'List saved threads', default: false }),
107
+ show: Flags.boolean({ description: 'Show the thread transcript (requires --id)', default: false }),
108
+ delete: Flags.boolean({ description: 'Delete the thread (requires --id)', default: false }),
109
+ };
110
+ static args = {
111
+ face_username: Args.string({
112
+ description: 'Face alias (or alias@model) — required when starting a new thread',
113
+ required: false,
114
+ }),
115
+ };
116
+ async run() {
117
+ const { args, flags } = await this.parse(ChatThread);
118
+ if (flags.list)
119
+ return this.handleList();
120
+ if (flags.delete)
121
+ return this.handleDelete(flags.id);
122
+ if (flags.show)
123
+ return this.handleShow(flags.id);
124
+ return this.handleMessage(args.face_username, flags);
125
+ }
126
+ handleList() {
127
+ const threads = listThreads();
128
+ if (this.jsonEnabled()) {
129
+ return threads.map((t) => ({
130
+ id: t.id,
131
+ face: t.face_username,
132
+ model: t.model,
133
+ turns: t.messages.length,
134
+ created_at: t.created_at,
135
+ updated_at: t.updated_at,
136
+ }));
137
+ }
138
+ if (threads.length === 0) {
139
+ this.log('(no threads)');
140
+ return {};
141
+ }
142
+ for (const t of threads) {
143
+ const last = t.messages[t.messages.length - 1];
144
+ const preview = last
145
+ ? last.content.slice(0, 60).replace(/\s+/g, ' ') + (last.content.length > 60 ? '…' : '')
146
+ : '';
147
+ this.log(`${t.id} ${t.model} (${t.messages.length} turns, updated ${t.updated_at})`);
148
+ if (preview)
149
+ this.log(` ${last.role}: ${preview}`);
150
+ }
151
+ return {};
152
+ }
153
+ handleDelete(id) {
154
+ if (!id)
155
+ this.error('--delete requires --id <thread-id>');
156
+ const p = threadPath(id);
157
+ if (!fs.existsSync(p))
158
+ this.error(`Thread not found: ${id}`);
159
+ fs.unlinkSync(p);
160
+ if (this.jsonEnabled())
161
+ return { deleted: id };
162
+ this.log(`Deleted thread ${id}`);
163
+ return { deleted: id };
164
+ }
165
+ handleShow(id) {
166
+ if (!id)
167
+ this.error('--show requires --id <thread-id>');
168
+ const t = loadThread(id);
169
+ if (!t)
170
+ this.error(`Thread not found: ${id}`);
171
+ if (this.jsonEnabled())
172
+ return t;
173
+ this.log(`Thread ${t.id} (${t.model}, provider=${t.provider})`);
174
+ this.log(`Created: ${t.created_at}`);
175
+ this.log(`Updated: ${t.updated_at}`);
176
+ if (t.system) {
177
+ this.log('---');
178
+ this.log(`system: ${t.system}`);
179
+ }
180
+ for (const m of t.messages) {
181
+ this.log('---');
182
+ this.log(`${m.role} [${m.ts}]:`);
183
+ this.log(m.content);
184
+ }
185
+ return t;
186
+ }
187
+ async handleMessage(face_username, flags) {
188
+ let userMessage;
189
+ if (flags.file) {
190
+ if (!fs.existsSync(flags.file))
191
+ this.error(`File not found: ${flags.file}`);
192
+ userMessage = fs.readFileSync(flags.file, 'utf8');
193
+ }
194
+ else if (flags.message) {
195
+ userMessage = flags.message;
196
+ }
197
+ else {
198
+ this.error('Provide --message/-m or --file');
199
+ }
200
+ const client = this.makeClient(flags);
201
+ let thread;
202
+ if (flags.id) {
203
+ const existing = loadThread(flags.id);
204
+ if (!existing)
205
+ this.error(`Thread not found: ${flags.id}`);
206
+ thread = existing;
207
+ if (flags.llm || flags.system) {
208
+ this.warn('--llm and --system are ignored when resuming a thread; using values stored on the thread');
209
+ }
210
+ }
211
+ else {
212
+ if (!face_username) {
213
+ this.error('Provide a face (e.g. `faces chat:thread socrates -m "…"`) or --id <thread-id> to resume');
214
+ }
215
+ const baseAlias = face_username.split('@')[0];
216
+ const model = flags.llm ? `${baseAlias}@${flags.llm}` : face_username;
217
+ const now = new Date().toISOString();
218
+ thread = {
219
+ id: newThreadId(),
220
+ face_username: baseAlias,
221
+ model,
222
+ provider: isAnthropicModel(model) ? 'anthropic' : 'openai',
223
+ system: flags.system,
224
+ created_at: now,
225
+ updated_at: now,
226
+ messages: [],
227
+ };
228
+ }
229
+ thread.messages.push({ role: 'user', content: userMessage, ts: new Date().toISOString() });
230
+ try {
231
+ const assistantText = thread.provider === 'anthropic'
232
+ ? await this.sendAnthropic(client, thread, flags)
233
+ : await this.sendOpenAI(client, thread, flags);
234
+ thread.messages.push({ role: 'assistant', content: assistantText, ts: new Date().toISOString() });
235
+ saveThread(thread);
236
+ if (this.jsonEnabled()) {
237
+ return {
238
+ thread_id: thread.id,
239
+ model: thread.model,
240
+ provider: thread.provider,
241
+ assistant: assistantText,
242
+ turns: thread.messages.length,
243
+ };
244
+ }
245
+ if (!flags.stream)
246
+ this.log(assistantText);
247
+ this.log(`\n[thread ${thread.id} · ${thread.messages.length} turns]`);
248
+ return { thread_id: thread.id, assistant: assistantText };
249
+ }
250
+ catch (err) {
251
+ if (err instanceof FacesAPIError) {
252
+ if (err.errorCode === 'oauth_rejected') {
253
+ const hint = err.fallbackAvailable
254
+ ? 'Run `faces account:preferences api_fallback true` to allow paid fallback.'
255
+ : 'Run `faces billing:topup` to add credits, then enable fallback with `faces account:preferences api_fallback true`.';
256
+ this.error(`OAuth failed: ${err.message}\n${hint}`);
257
+ }
258
+ this.error(`Error (${err.statusCode}): ${err.message}`);
259
+ }
260
+ throw err;
261
+ }
262
+ }
263
+ async sendOpenAI(client, thread, flags) {
264
+ const messages = [];
265
+ if (thread.system)
266
+ messages.push({ role: 'system', content: thread.system });
267
+ for (const m of thread.messages)
268
+ messages.push({ role: m.role, content: m.content });
269
+ const payload = { model: thread.model, messages, stream: flags.stream };
270
+ if (flags['max-tokens'])
271
+ payload.max_tokens = flags['max-tokens'];
272
+ if (flags.temperature !== undefined)
273
+ payload.temperature = Number.parseFloat(flags.temperature);
274
+ if (flags['oauth-only'])
275
+ payload.oauth_only = true;
276
+ if (flags.stream) {
277
+ return this.streamAndAccumulate(client, '/v1/chat/completions', payload);
278
+ }
279
+ const { data, headers } = await client.postWithHeaders('/v1/chat/completions', { body: payload });
280
+ return extractAssistantText(data, headers['x-faces-routed-endpoint']);
281
+ }
282
+ async sendAnthropic(client, thread, flags) {
283
+ const msgs = thread.messages.map((m) => ({ role: m.role, content: m.content }));
284
+ const payload = {
285
+ model: thread.model,
286
+ messages: msgs,
287
+ max_tokens: flags['max-tokens'] ?? 1024,
288
+ stream: flags.stream,
289
+ };
290
+ if (thread.system)
291
+ payload.system = thread.system;
292
+ if (flags['oauth-only'])
293
+ payload.oauth_only = true;
294
+ if (flags.stream) {
295
+ return this.streamAndAccumulate(client, '/v1/messages', payload);
296
+ }
297
+ const { data, headers } = await client.postWithHeaders('/v1/messages', { body: payload });
298
+ return extractAssistantText(data, headers['x-faces-routed-endpoint']);
299
+ }
300
+ /**
301
+ * Stream an SSE response, printing deltas to stdout as they arrive while
302
+ * accumulating the full text to persist on the thread. Handles any of the
303
+ * three event shapes, so a re-routed stream still renders correctly.
304
+ */
305
+ async streamAndAccumulate(client, endpoint, payload) {
306
+ let accum = '';
307
+ for await (const line of client.stream(endpoint, payload)) {
308
+ if (!line.startsWith('data: '))
309
+ continue;
310
+ const chunk = line.slice(6);
311
+ if (chunk.trim() === '[DONE]' || chunk.trim() === '')
312
+ continue;
313
+ try {
314
+ const delta = extractStreamDelta(JSON.parse(chunk));
315
+ if (delta) {
316
+ accum += delta;
317
+ process.stdout.write(delta);
318
+ }
319
+ }
320
+ catch { /* ignore parse errors */ }
321
+ }
322
+ process.stdout.write('\n');
323
+ return accum;
324
+ }
325
+ }
package/dist/utils.d.ts CHANGED
@@ -10,3 +10,13 @@ export declare const flattenBasicFacts: typeof flattenAttributes;
10
10
  * Mutates the object in place and returns it.
11
11
  */
12
12
  export declare function renameFaceFields(face: Record<string, unknown>): Record<string, unknown>;
13
+ /**
14
+ * Extract a streamed text delta from a parsed SSE event, regardless of which
15
+ * API family produced it. A request posted to /v1/chat/completions may be
16
+ * re-routed by the backend to /v1/messages or /v1/responses, so a stream
17
+ * consumer must handle all three event shapes:
18
+ * - Anthropic Messages: { type: 'content_block_delta', delta: {text} }
19
+ * - OpenAI Responses: { type: 'response.output_text.delta', delta }
20
+ * - OpenAI Chat Completions: { choices: [{delta: {content}}] }
21
+ */
22
+ export declare function extractStreamDelta(parsed: Record<string, unknown>): string;
package/dist/utils.js CHANGED
@@ -29,3 +29,22 @@ export function renameFaceFields(face) {
29
29
  }
30
30
  return face;
31
31
  }
32
+ /**
33
+ * Extract a streamed text delta from a parsed SSE event, regardless of which
34
+ * API family produced it. A request posted to /v1/chat/completions may be
35
+ * re-routed by the backend to /v1/messages or /v1/responses, so a stream
36
+ * consumer must handle all three event shapes:
37
+ * - Anthropic Messages: { type: 'content_block_delta', delta: {text} }
38
+ * - OpenAI Responses: { type: 'response.output_text.delta', delta }
39
+ * - OpenAI Chat Completions: { choices: [{delta: {content}}] }
40
+ */
41
+ export function extractStreamDelta(parsed) {
42
+ if (parsed.type === 'content_block_delta') {
43
+ return String(parsed.delta?.text ?? '');
44
+ }
45
+ if (parsed.type === 'response.output_text.delta') {
46
+ return String(parsed.delta ?? '');
47
+ }
48
+ const choices = parsed.choices;
49
+ return String(choices?.[0]?.delta?.content ?? '');
50
+ }
@@ -1676,6 +1676,152 @@
1676
1676
  "responses.js"
1677
1677
  ]
1678
1678
  },
1679
+ "chat:thread": {
1680
+ "aliases": [],
1681
+ "args": {
1682
+ "face_username": {
1683
+ "description": "Face alias (or alias@model) — required when starting a new thread",
1684
+ "name": "face_username",
1685
+ "required": false
1686
+ }
1687
+ },
1688
+ "description": "Multi-turn chat thread. Conversation history is stored under ~/.faces/threads/<id>.json — resume any thread later with --id.",
1689
+ "examples": [
1690
+ "<%= config.bin %> <%= command.id %> socrates -m \"What is justice?\"",
1691
+ "<%= config.bin %> <%= command.id %> --id t_abc123 -m \"Say more about that\"",
1692
+ "<%= config.bin %> <%= command.id %> --list",
1693
+ "<%= config.bin %> <%= command.id %> --id t_abc123 --show",
1694
+ "<%= config.bin %> <%= command.id %> --id t_abc123 --delete"
1695
+ ],
1696
+ "flags": {
1697
+ "json": {
1698
+ "description": "Format output as json.",
1699
+ "helpGroup": "GLOBAL",
1700
+ "name": "json",
1701
+ "allowNo": false,
1702
+ "type": "boolean"
1703
+ },
1704
+ "base-url": {
1705
+ "description": "API base URL",
1706
+ "env": "FACES_BASE_URL",
1707
+ "name": "base-url",
1708
+ "hasDynamicHelp": false,
1709
+ "multiple": false,
1710
+ "type": "option"
1711
+ },
1712
+ "token": {
1713
+ "description": "JWT bearer token",
1714
+ "env": "FACES_TOKEN",
1715
+ "name": "token",
1716
+ "hasDynamicHelp": false,
1717
+ "multiple": false,
1718
+ "type": "option"
1719
+ },
1720
+ "api-key": {
1721
+ "description": "API key",
1722
+ "env": "FACES_API_KEY",
1723
+ "name": "api-key",
1724
+ "hasDynamicHelp": false,
1725
+ "multiple": false,
1726
+ "type": "option"
1727
+ },
1728
+ "id": {
1729
+ "description": "Resume an existing thread by id",
1730
+ "name": "id",
1731
+ "hasDynamicHelp": false,
1732
+ "multiple": false,
1733
+ "type": "option"
1734
+ },
1735
+ "message": {
1736
+ "char": "m",
1737
+ "description": "User message",
1738
+ "name": "message",
1739
+ "hasDynamicHelp": false,
1740
+ "multiple": false,
1741
+ "type": "option"
1742
+ },
1743
+ "file": {
1744
+ "description": "Read message from file",
1745
+ "name": "file",
1746
+ "hasDynamicHelp": false,
1747
+ "multiple": false,
1748
+ "type": "option"
1749
+ },
1750
+ "llm": {
1751
+ "description": "LLM override (only honored when starting a new thread)",
1752
+ "name": "llm",
1753
+ "hasDynamicHelp": false,
1754
+ "multiple": false,
1755
+ "type": "option"
1756
+ },
1757
+ "system": {
1758
+ "description": "System prompt (only honored when starting a new thread)",
1759
+ "name": "system",
1760
+ "hasDynamicHelp": false,
1761
+ "multiple": false,
1762
+ "type": "option"
1763
+ },
1764
+ "max-tokens": {
1765
+ "description": "Max tokens",
1766
+ "name": "max-tokens",
1767
+ "hasDynamicHelp": false,
1768
+ "multiple": false,
1769
+ "type": "option"
1770
+ },
1771
+ "temperature": {
1772
+ "description": "Temperature (0.0-2.0, OpenAI/Chat Completions only)",
1773
+ "name": "temperature",
1774
+ "hasDynamicHelp": false,
1775
+ "multiple": false,
1776
+ "type": "option"
1777
+ },
1778
+ "stream": {
1779
+ "description": "Stream the response",
1780
+ "name": "stream",
1781
+ "allowNo": false,
1782
+ "type": "boolean"
1783
+ },
1784
+ "oauth-only": {
1785
+ "description": "Prevent fallback to paid system key",
1786
+ "name": "oauth-only",
1787
+ "allowNo": false,
1788
+ "type": "boolean"
1789
+ },
1790
+ "list": {
1791
+ "description": "List saved threads",
1792
+ "name": "list",
1793
+ "allowNo": false,
1794
+ "type": "boolean"
1795
+ },
1796
+ "show": {
1797
+ "description": "Show the thread transcript (requires --id)",
1798
+ "name": "show",
1799
+ "allowNo": false,
1800
+ "type": "boolean"
1801
+ },
1802
+ "delete": {
1803
+ "description": "Delete the thread (requires --id)",
1804
+ "name": "delete",
1805
+ "allowNo": false,
1806
+ "type": "boolean"
1807
+ }
1808
+ },
1809
+ "hasDynamicHelp": false,
1810
+ "hiddenAliases": [],
1811
+ "id": "chat:thread",
1812
+ "pluginAlias": "faces-cli",
1813
+ "pluginName": "faces-cli",
1814
+ "pluginType": "core",
1815
+ "strict": true,
1816
+ "enableJsonFlag": true,
1817
+ "isESM": true,
1818
+ "relativePath": [
1819
+ "dist",
1820
+ "commands",
1821
+ "chat",
1822
+ "thread.js"
1823
+ ]
1824
+ },
1679
1825
  "compile:all": {
1680
1826
  "aliases": [],
1681
1827
  "args": {},
@@ -5894,5 +6040,5 @@
5894
6040
  ]
5895
6041
  }
5896
6042
  },
5897
- "version": "1.6.15"
6043
+ "version": "1.6.16"
5898
6044
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "faces-cli",
3
- "version": "1.6.15",
3
+ "version": "1.6.16",
4
4
  "description": "CLI for the Faces AI platform",
5
5
  "type": "module",
6
6
  "author": "sybileak <sybileak@proton.me>",