laohuang 0.3.2 → 0.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.
Files changed (86) hide show
  1. package/README.md +142 -16
  2. package/dist/agent.d.ts +166 -0
  3. package/dist/agent.js +858 -0
  4. package/dist/agent.js.map +1 -0
  5. package/dist/bash-runner.d.ts +79 -0
  6. package/dist/bash-runner.js +464 -0
  7. package/dist/bash-runner.js.map +1 -0
  8. package/dist/cancellation.d.ts +36 -0
  9. package/dist/cancellation.js +123 -0
  10. package/dist/cancellation.js.map +1 -0
  11. package/dist/cli.d.ts +117 -0
  12. package/dist/cli.js +1307 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/client.d.ts +21 -0
  15. package/dist/client.js +17 -0
  16. package/dist/client.js.map +1 -0
  17. package/dist/commands.d.ts +123 -0
  18. package/dist/commands.js +660 -0
  19. package/dist/commands.js.map +1 -0
  20. package/dist/config.d.ts +51 -0
  21. package/dist/config.js +183 -0
  22. package/dist/config.js.map +1 -0
  23. package/dist/credentials.d.ts +12 -0
  24. package/dist/credentials.js +102 -0
  25. package/dist/credentials.js.map +1 -0
  26. package/dist/events.d.ts +288 -0
  27. package/dist/events.js +838 -0
  28. package/dist/events.js.map +1 -0
  29. package/dist/model-adapter.d.ts +138 -0
  30. package/dist/model-adapter.js +244 -0
  31. package/dist/model-adapter.js.map +1 -0
  32. package/dist/model-selection.d.ts +67 -0
  33. package/dist/model-selection.js +148 -0
  34. package/dist/model-selection.js.map +1 -0
  35. package/dist/model-stream.d.ts +128 -0
  36. package/dist/model-stream.js +582 -0
  37. package/dist/model-stream.js.map +1 -0
  38. package/dist/project-instructions.d.ts +99 -0
  39. package/dist/project-instructions.js +348 -0
  40. package/dist/project-instructions.js.map +1 -0
  41. package/dist/providers.d.ts +9 -0
  42. package/dist/providers.js +26 -0
  43. package/dist/providers.js.map +1 -0
  44. package/dist/routing.d.ts +170 -0
  45. package/dist/routing.js +669 -0
  46. package/dist/routing.js.map +1 -0
  47. package/dist/semantic-classifier.d.ts +66 -0
  48. package/dist/semantic-classifier.js +86 -0
  49. package/dist/semantic-classifier.js.map +1 -0
  50. package/dist/session.d.ts +162 -0
  51. package/dist/session.js +871 -0
  52. package/dist/session.js.map +1 -0
  53. package/dist/system-prompt.d.ts +16 -0
  54. package/dist/system-prompt.js +42 -0
  55. package/dist/system-prompt.js.map +1 -0
  56. package/dist/terminal/editor.d.ts +161 -0
  57. package/dist/terminal/editor.js +1060 -0
  58. package/dist/terminal/editor.js.map +1 -0
  59. package/dist/terminal/input.d.ts +61 -0
  60. package/dist/terminal/input.js +276 -0
  61. package/dist/terminal/input.js.map +1 -0
  62. package/dist/terminal/markdown.d.ts +20 -0
  63. package/dist/terminal/markdown.js +621 -0
  64. package/dist/terminal/markdown.js.map +1 -0
  65. package/dist/terminal/screen.d.ts +66 -0
  66. package/dist/terminal/screen.js +624 -0
  67. package/dist/terminal/screen.js.map +1 -0
  68. package/dist/terminal/theme.d.ts +23 -0
  69. package/dist/terminal/theme.js +101 -0
  70. package/dist/terminal/theme.js.map +1 -0
  71. package/dist/terminal/ui.d.ts +285 -0
  72. package/dist/terminal/ui.js +1815 -0
  73. package/dist/terminal/ui.js.map +1 -0
  74. package/dist/tools.d.ts +95 -0
  75. package/dist/tools.js +444 -0
  76. package/dist/tools.js.map +1 -0
  77. package/dist/ui-state.d.ts +51 -0
  78. package/dist/ui-state.js +194 -0
  79. package/dist/ui-state.js.map +1 -0
  80. package/dist/web.d.ts +56 -0
  81. package/dist/web.js +247 -0
  82. package/dist/web.js.map +1 -0
  83. package/package.json +24 -21
  84. package/bin/laohuang.js +0 -10
  85. package/lib/launcher.js +0 -133
  86. package/vendor/laohuangcode-0.3.2-py3-none-any.whl +0 -0
@@ -0,0 +1,660 @@
1
+ /** Slash commands available inside an interactive agent session. */
2
+ import { EventKind, EventSource } from "./events.js";
3
+ import { getProvider, providerNames } from "./providers.js";
4
+ /**
5
+ * Split a command line the way POSIX `shlex.split` does: whitespace separated,
6
+ * single/double quotes, backslash escapes. Throws on unterminated quotes.
7
+ */
8
+ export function shlexSplit(text) {
9
+ const tokens = [];
10
+ let current = "";
11
+ let started = false;
12
+ let quote = null;
13
+ for (let index = 0; index < text.length; index += 1) {
14
+ const char = text[index];
15
+ if (quote === "'") {
16
+ if (char === "'") {
17
+ quote = null;
18
+ }
19
+ else {
20
+ current += char;
21
+ }
22
+ continue;
23
+ }
24
+ if (quote === '"') {
25
+ if (char === '"') {
26
+ quote = null;
27
+ }
28
+ else if (char === "\\" &&
29
+ index + 1 < text.length &&
30
+ ['"', "\\", "$", "`"].includes(text[index + 1])) {
31
+ index += 1;
32
+ current += text[index];
33
+ }
34
+ else {
35
+ current += char;
36
+ }
37
+ continue;
38
+ }
39
+ if (char === "\\") {
40
+ if (index + 1 < text.length) {
41
+ index += 1;
42
+ current += text[index];
43
+ }
44
+ else {
45
+ current += char;
46
+ }
47
+ }
48
+ else if (char === "'" || char === '"') {
49
+ quote = char;
50
+ started = true;
51
+ }
52
+ else if (/\s/.test(char)) {
53
+ if (started || current.length > 0) {
54
+ tokens.push(current);
55
+ current = "";
56
+ started = false;
57
+ }
58
+ }
59
+ else {
60
+ current += char;
61
+ }
62
+ }
63
+ if (quote !== null) {
64
+ throw new Error("No closing quotation");
65
+ }
66
+ if (started || current.length > 0) {
67
+ tokens.push(current);
68
+ }
69
+ return tokens;
70
+ }
71
+ // --- difflib.get_close_matches equivalent -----------------------------------
72
+ // difflib's SequenceMatcher ratio for short inputs (the autojunk heuristic
73
+ // only kicks in above 200 characters): 2 * M / T where M counts the recursive
74
+ // longest contiguous matching blocks.
75
+ function longestMatchSize(a, aStart, aEnd, b, bStart, bEnd) {
76
+ let best = { size: 0, aIndex: aStart, bIndex: bStart };
77
+ for (let i = aStart; i < aEnd; i += 1) {
78
+ for (let j = bStart; j < bEnd; j += 1) {
79
+ let size = 0;
80
+ while (i + size < aEnd &&
81
+ j + size < bEnd &&
82
+ a[i + size] === b[j + size]) {
83
+ size += 1;
84
+ }
85
+ if (size > best.size) {
86
+ best = { size, aIndex: i, bIndex: j };
87
+ }
88
+ }
89
+ }
90
+ return best;
91
+ }
92
+ function matchingBlockCount(a, aStart, aEnd, b, bStart, bEnd) {
93
+ const match = longestMatchSize(a, aStart, aEnd, b, bStart, bEnd);
94
+ if (match.size === 0) {
95
+ return 0;
96
+ }
97
+ return (match.size +
98
+ matchingBlockCount(a, aStart, match.aIndex, b, bStart, match.bIndex) +
99
+ matchingBlockCount(a, match.aIndex + match.size, aEnd, b, match.bIndex + match.size, bEnd));
100
+ }
101
+ function sequenceRatio(a, b) {
102
+ const total = a.length + b.length;
103
+ if (total === 0) {
104
+ return 1;
105
+ }
106
+ return (2 * matchingBlockCount(a, 0, a.length, b, 0, b.length)) / total;
107
+ }
108
+ /** Store slash commands without coupling prompt rendering to handlers. */
109
+ export class CommandRegistry {
110
+ #specs = new Map();
111
+ constructor(specs = []) {
112
+ for (const spec of specs) {
113
+ this.register(spec);
114
+ }
115
+ }
116
+ register(spec) {
117
+ if (!spec.name.startsWith("/")) {
118
+ throw new Error("Command names must start with '/'");
119
+ }
120
+ this.#specs.set(spec.name, spec);
121
+ }
122
+ get(name) {
123
+ return this.#specs.get(name);
124
+ }
125
+ all() {
126
+ return [...this.#specs.keys()].sort().map((name) => this.#specs.get(name));
127
+ }
128
+ suggest(name) {
129
+ let best = null;
130
+ let bestScore = 0;
131
+ for (const candidate of this.#specs.keys()) {
132
+ const score = sequenceRatio(name, candidate);
133
+ // Strict ">" keeps the earliest registration on ties, mirroring the
134
+ // stable ordering of difflib.get_close_matches(n=1).
135
+ if (score >= 0.55 && score > bestScore) {
136
+ best = candidate;
137
+ bestScore = score;
138
+ }
139
+ }
140
+ return best;
141
+ }
142
+ /** Return slash-command and argument candidates for the given state. */
143
+ complete(text, options) {
144
+ const { state } = options;
145
+ if (!text.startsWith("/") || text.includes("\n")) {
146
+ return [];
147
+ }
148
+ if (!text.includes(" ")) {
149
+ return this.all()
150
+ .filter((spec) => spec.name.startsWith(text) &&
151
+ CommandRegistry.available(spec, state))
152
+ .map((spec) => ({
153
+ value: spec.name,
154
+ description: spec.description,
155
+ start: -text.length,
156
+ }));
157
+ }
158
+ const splitAt = text.indexOf(" ");
159
+ const commandName = text.slice(0, splitAt);
160
+ const rawArguments = text.slice(splitAt + 1);
161
+ const spec = this.get(commandName);
162
+ if (spec === undefined || spec.argumentCompleter === undefined) {
163
+ return [];
164
+ }
165
+ const completed = rawArguments.split(/\s+/).filter((part) => part !== "");
166
+ const endsWithSpace = rawArguments.endsWith(" ");
167
+ const fragment = endsWithSpace
168
+ ? ""
169
+ : (completed[completed.length - 1] ?? "");
170
+ const fixed = endsWithSpace ? completed : completed.slice(0, -1);
171
+ const items = [];
172
+ for (const [value, description] of spec.argumentCompleter(fixed)) {
173
+ if (value.startsWith(fragment) &&
174
+ CommandRegistry.argumentAvailable(spec, value, state)) {
175
+ items.push({ value, description, start: -fragment.length });
176
+ }
177
+ }
178
+ return items;
179
+ }
180
+ static available(spec, state) {
181
+ return !(spec.allowedStates !== undefined &&
182
+ spec.allowedStates.size > 0 &&
183
+ !spec.allowedStates.has(state) &&
184
+ spec.name !== "/model");
185
+ }
186
+ static argumentAvailable(spec, value, state) {
187
+ return !(spec.allowedStates !== undefined &&
188
+ spec.allowedStates.size > 0 &&
189
+ !spec.allowedStates.has(state) &&
190
+ !(spec.name === "/model" && value === "current"));
191
+ }
192
+ async dispatch(command, options = {}) {
193
+ let parts;
194
+ try {
195
+ parts = shlexSplit(command);
196
+ }
197
+ catch {
198
+ return false;
199
+ }
200
+ const first = parts[0];
201
+ if (first === undefined) {
202
+ return false;
203
+ }
204
+ const spec = this.get(first);
205
+ if (spec === undefined || spec.handler === undefined) {
206
+ return false;
207
+ }
208
+ if (spec.allowedStates !== undefined &&
209
+ spec.allowedStates.size > 0 &&
210
+ (options.state === undefined || !spec.allowedStates.has(options.state))) {
211
+ return false;
212
+ }
213
+ return spec.handler(parts.slice(1));
214
+ }
215
+ }
216
+ /**
217
+ * Widget adapter mirroring the old prompt_toolkit CommandCompleter: turns the
218
+ * registry's widget-independent completion into a `text => items` function the
219
+ * terminal input layer can consume.
220
+ */
221
+ export function createCommandCompleter(registry, stateFn = () => "IDLE") {
222
+ return (text) => registry.complete(text, { state: stateFn() });
223
+ }
224
+ function* modelCompletions(args) {
225
+ if (args.length === 0) {
226
+ yield ["current", "显示当前模型"];
227
+ for (const name of providerNames()) {
228
+ yield [name, "模型供应商"];
229
+ }
230
+ return;
231
+ }
232
+ if (args.length === 1) {
233
+ let provider;
234
+ try {
235
+ provider = getProvider(args[0]);
236
+ }
237
+ catch {
238
+ return;
239
+ }
240
+ for (const model of provider.suggestedModels) {
241
+ yield [model, `${provider.name} 模型`];
242
+ }
243
+ }
244
+ }
245
+ function* queueCompletions(args) {
246
+ if (args.length === 0) {
247
+ yield ["resume", "恢复保留的消息"];
248
+ yield ["clear", "清空待处理和保留消息"];
249
+ }
250
+ }
251
+ function* providerCompletions(args) {
252
+ if (args.length === 0) {
253
+ for (const name of providerNames()) {
254
+ yield [name, "模型供应商"];
255
+ }
256
+ }
257
+ }
258
+ const ALL_STATES = new Set([
259
+ "IDLE",
260
+ "RUNNING_MODEL",
261
+ "RUNNING_TOOLS",
262
+ "CANCELLING",
263
+ "FAILED",
264
+ ]);
265
+ const IDLE_ONLY = new Set(["IDLE", "FAILED"]);
266
+ function errorMessage(error) {
267
+ return error instanceof Error ? error.message : String(error);
268
+ }
269
+ /** Handle session-local model and credential commands. */
270
+ export class SessionCommands {
271
+ registry;
272
+ #agent;
273
+ #selector;
274
+ #credentials;
275
+ #input;
276
+ #secretInput;
277
+ #output;
278
+ #session;
279
+ #currentConfig;
280
+ constructor(options) {
281
+ this.#agent = options.agent;
282
+ this.#selector = options.selector;
283
+ this.#credentials = options.credentials;
284
+ this.#currentConfig = options.currentConfig;
285
+ this.#input = options.input;
286
+ this.#secretInput = options.secretInput;
287
+ this.#output = options.output ?? ((message) => console.log(message));
288
+ this.#session = options.session ?? null;
289
+ this.registry = new CommandRegistry([
290
+ {
291
+ name: "/model",
292
+ description: "选择供应商和模型",
293
+ usage: "/model [provider] [model]",
294
+ handler: (args) => this.handleModel(args),
295
+ allowedStates: IDLE_ONLY,
296
+ argumentCompleter: modelCompletions,
297
+ },
298
+ {
299
+ name: "/login",
300
+ description: "输入或更新API Key",
301
+ usage: "/login [provider]",
302
+ handler: (args) => this.handleLogin(args),
303
+ allowedStates: IDLE_ONLY,
304
+ argumentCompleter: providerCompletions,
305
+ },
306
+ {
307
+ name: "/logout",
308
+ description: "删除保存的API Key",
309
+ usage: "/logout [provider]",
310
+ handler: (args) => this.handleLogout(args),
311
+ allowedStates: IDLE_ONLY,
312
+ argumentCompleter: providerCompletions,
313
+ },
314
+ {
315
+ name: "/apikey",
316
+ description: "管理API Key(兼容命令)",
317
+ usage: "/apikey [set|remove] [provider]",
318
+ handler: (args) => this.handleApiKey(args),
319
+ allowedStates: IDLE_ONLY,
320
+ },
321
+ {
322
+ name: "/cancel",
323
+ description: "取消当前任务",
324
+ usage: "/cancel",
325
+ handler: (args) => this.handleCancel(args),
326
+ allowedStates: ALL_STATES,
327
+ },
328
+ {
329
+ name: "/queue",
330
+ description: "查看或管理待处理消息",
331
+ usage: "/queue [resume|clear]",
332
+ handler: (args) => this.handleQueue(args),
333
+ allowedStates: ALL_STATES,
334
+ argumentCompleter: queueCompletions,
335
+ },
336
+ {
337
+ name: "/clear",
338
+ description: "清空当前对话上下文",
339
+ usage: "/clear",
340
+ handler: (args) => this.handleClear(args),
341
+ allowedStates: IDLE_ONLY,
342
+ },
343
+ {
344
+ name: "/help",
345
+ description: "查看命令帮助",
346
+ usage: "/help",
347
+ handler: (args) => this.handleHelp(args),
348
+ allowedStates: ALL_STATES,
349
+ },
350
+ {
351
+ name: "/exit",
352
+ description: "退出程序",
353
+ usage: "/exit",
354
+ allowedStates: ALL_STATES,
355
+ },
356
+ ]);
357
+ }
358
+ get currentConfig() {
359
+ return this.#currentConfig;
360
+ }
361
+ async handle(command) {
362
+ let parts;
363
+ try {
364
+ parts = shlexSplit(command);
365
+ }
366
+ catch (error) {
367
+ this.#output(`Invalid command: ${errorMessage(error)}`);
368
+ return true;
369
+ }
370
+ const first = parts[0];
371
+ if (first === undefined) {
372
+ return false;
373
+ }
374
+ const spec = this.registry.get(first);
375
+ if (spec === undefined || spec.handler === undefined) {
376
+ return false;
377
+ }
378
+ const state = this.runtimeState();
379
+ const readOnlyModelQuery = parts.length === 2 && parts[0] === "/model" && parts[1] === "current";
380
+ if (spec.allowedStates !== undefined &&
381
+ spec.allowedStates.size > 0 &&
382
+ !spec.allowedStates.has(state) &&
383
+ !readOnlyModelQuery) {
384
+ this.#output(`${spec.name} is unavailable while the task is ${state.toLowerCase()}.`);
385
+ return true;
386
+ }
387
+ return spec.handler(parts.slice(1));
388
+ }
389
+ runtimeState() {
390
+ const session = this.#session;
391
+ if (session === null) {
392
+ return "IDLE";
393
+ }
394
+ const active = session.activeTask ?? null;
395
+ if (active === null) {
396
+ return "IDLE";
397
+ }
398
+ const rawState = active.state ?? "IDLE";
399
+ const named = typeof rawState === "object" &&
400
+ rawState !== null &&
401
+ "name" in rawState
402
+ ? rawState.name
403
+ : rawState;
404
+ return String(named).toUpperCase();
405
+ }
406
+ handleHelp(args) {
407
+ if (args.length > 0) {
408
+ this.#output("Usage: /help");
409
+ return true;
410
+ }
411
+ this.#output("Commands:");
412
+ for (const spec of this.registry.all()) {
413
+ this.#output(` ${spec.usage.padEnd(32)} ${spec.description}`);
414
+ }
415
+ return true;
416
+ }
417
+ handleCancel(args) {
418
+ if (args.length > 0) {
419
+ this.#output("Usage: /cancel");
420
+ return true;
421
+ }
422
+ if (this.#session === null) {
423
+ this.#output("No active task to cancel.");
424
+ return true;
425
+ }
426
+ const cancelled = this.#session.cancelActiveTask();
427
+ this.#output(cancelled ? "Cancelling current task…" : "No active task to cancel.");
428
+ return true;
429
+ }
430
+ handleQueue(args) {
431
+ if (args.length > 1 ||
432
+ (args.length > 0 && args[0] !== "resume" && args[0] !== "clear")) {
433
+ this.#output("Usage: /queue [resume|clear]");
434
+ return true;
435
+ }
436
+ if (this.#session === null) {
437
+ this.#output("Pending: 0 · Held: 0 · Dead letters: 0");
438
+ return true;
439
+ }
440
+ if (args[0] === "clear") {
441
+ const cleared = this.#session.clearQueues();
442
+ this.#output(`Cleared ${cleared} queued message(s).`);
443
+ return true;
444
+ }
445
+ if (args[0] === "resume") {
446
+ const resumed = this.#session.resumeHeld();
447
+ this.#output(`Resumed ${resumed} held message(s).`);
448
+ return true;
449
+ }
450
+ const status = this.#session.queueStatus();
451
+ this.#output(`Pending: ${status.pending ?? 0}` +
452
+ ` (${status.pendingTokens ?? 0} est. tokens)` +
453
+ ` · Held: ${status.held ?? 0}` +
454
+ ` (${status.heldTokens ?? 0} est. tokens)` +
455
+ ` · Dead letters: ${status.deadLetters ?? 0}`);
456
+ return true;
457
+ }
458
+ handleClear(args) {
459
+ if (args.length > 0) {
460
+ this.#output("Usage: /clear");
461
+ return true;
462
+ }
463
+ const clear = this.#agent.clearHistory;
464
+ if (typeof clear === "function") {
465
+ clear.call(this.#agent);
466
+ }
467
+ else if (this.#agent.messages !== undefined && this.#agent.messages.length > 0) {
468
+ this.#agent.messages.splice(1);
469
+ }
470
+ this.#output("Conversation cleared.");
471
+ return true;
472
+ }
473
+ async handleModel(args) {
474
+ if (args.length === 1 && args[0] === "current") {
475
+ this.#output(`Current model: ${this.#currentConfig.provider} / ` +
476
+ `${this.#currentConfig.model}`);
477
+ return true;
478
+ }
479
+ if (args.length > 2) {
480
+ this.#output("Usage: /model [provider] [model]");
481
+ return true;
482
+ }
483
+ const provider = args[0];
484
+ const model = args.length === 2 ? args[1] : undefined;
485
+ let selection;
486
+ try {
487
+ selection = await this.#selector.select({
488
+ providerName: provider,
489
+ modelName: model,
490
+ promptForMissingKey: false,
491
+ });
492
+ }
493
+ catch (error) {
494
+ this.#output(`Could not switch model: ${errorMessage(error)}`);
495
+ return true;
496
+ }
497
+ if (selection === null) {
498
+ return true;
499
+ }
500
+ const previousProvider = this.#currentConfig.provider;
501
+ const previousModel = this.#currentConfig.model;
502
+ this.#agent.switchModel({
503
+ client: selection.client,
504
+ model: selection.config.model,
505
+ provider: selection.config.provider,
506
+ });
507
+ this.#currentConfig = selection.config;
508
+ this.publishModelSwitched(previousProvider, previousModel);
509
+ this.#output(`Switched to ${selection.config.provider} / ${selection.config.model}`);
510
+ return true;
511
+ }
512
+ async handleLogin(args) {
513
+ if (args.length > 1) {
514
+ this.#output("Usage: /login [provider]");
515
+ return true;
516
+ }
517
+ const provider = args[0] ?? (await this.chooseProvider());
518
+ if (provider === null || provider === undefined) {
519
+ return true;
520
+ }
521
+ try {
522
+ getProvider(provider);
523
+ }
524
+ catch (error) {
525
+ this.#output(errorMessage(error));
526
+ return true;
527
+ }
528
+ let apiKey;
529
+ try {
530
+ apiKey = (await this.#secretInput(`Enter ${provider} API key: `)).trim();
531
+ }
532
+ catch {
533
+ this.#output("Login cancelled; credentials were not changed.");
534
+ return true;
535
+ }
536
+ if (!apiKey) {
537
+ this.#output("Login cancelled; credentials were not changed.");
538
+ return true;
539
+ }
540
+ this.#credentials.set(provider, apiKey);
541
+ if (this.#currentConfig.provider === provider) {
542
+ let selection;
543
+ try {
544
+ selection = await this.#selector.select({
545
+ providerName: provider,
546
+ modelName: this.#currentConfig.model,
547
+ promptForMissingKey: false,
548
+ });
549
+ }
550
+ catch (error) {
551
+ this.#output(`Credentials saved but could not be applied: ${errorMessage(error)}`);
552
+ return true;
553
+ }
554
+ if (selection !== null) {
555
+ const previousProvider = this.#currentConfig.provider;
556
+ const previousModel = this.#currentConfig.model;
557
+ this.#agent.switchModel({
558
+ client: selection.client,
559
+ model: selection.config.model,
560
+ provider: selection.config.provider,
561
+ });
562
+ this.#currentConfig = selection.config;
563
+ this.publishModelSwitched(previousProvider, previousModel);
564
+ this.#output(`Logged in to ${provider}; credentials applied.`);
565
+ return true;
566
+ }
567
+ }
568
+ this.#output(`Logged in to ${provider}; use /model to select it.`);
569
+ return true;
570
+ }
571
+ async handleLogout(args) {
572
+ if (args.length > 1) {
573
+ this.#output("Usage: /logout [provider]");
574
+ return true;
575
+ }
576
+ const provider = args[0] ?? (await this.chooseProvider());
577
+ if (provider === null || provider === undefined) {
578
+ return true;
579
+ }
580
+ try {
581
+ getProvider(provider);
582
+ }
583
+ catch (error) {
584
+ this.#output(errorMessage(error));
585
+ return true;
586
+ }
587
+ if (this.#credentials.remove(provider)) {
588
+ const suffix = this.#currentConfig.provider === provider
589
+ ? " The current client remains active until you switch models or exit."
590
+ : "";
591
+ this.#output(`Logged out of ${provider}.${suffix}`);
592
+ }
593
+ else {
594
+ this.#output(`No credentials stored for ${provider}.`);
595
+ }
596
+ return true;
597
+ }
598
+ /** Compatibility alias for the pre-/login credential commands. */
599
+ async handleApiKey(args) {
600
+ const first = args[0];
601
+ if (first === undefined) {
602
+ const configured = new Set(this.#credentials.providers());
603
+ for (const provider of providerNames()) {
604
+ const status = configured.has(provider)
605
+ ? "configured"
606
+ : "not configured";
607
+ this.#output(`${provider}: ${status}`);
608
+ }
609
+ this.#output("Use /login or /logout to manage credentials.");
610
+ return true;
611
+ }
612
+ if (first === "set") {
613
+ return this.handleLogin(args.slice(1));
614
+ }
615
+ if (first === "remove") {
616
+ return this.handleLogout(args.slice(1));
617
+ }
618
+ this.#output("Usage: /apikey [set|remove] [provider]");
619
+ return true;
620
+ }
621
+ publishModelSwitched(previousProvider, previousModel) {
622
+ const eventBus = this.#session?.eventBus ?? null;
623
+ const sessionId = this.#session?.sessionId ?? null;
624
+ if (eventBus === null || sessionId === null) {
625
+ return;
626
+ }
627
+ eventBus.publish(EventKind.ModelSwitched, {
628
+ source: EventSource.Session,
629
+ session_id: sessionId,
630
+ payload: {
631
+ provider: this.#currentConfig.provider,
632
+ model: this.#currentConfig.model,
633
+ previous_provider: previousProvider,
634
+ previous_model: previousModel,
635
+ },
636
+ });
637
+ }
638
+ async chooseProvider() {
639
+ const providers = providerNames();
640
+ providers.forEach((provider, index) => {
641
+ this.#output(` ${index + 1}. ${provider}`);
642
+ });
643
+ let answer;
644
+ try {
645
+ answer = (await this.#input("Select provider: ")).trim();
646
+ }
647
+ catch {
648
+ this.#output("Provider selection cancelled.");
649
+ return null;
650
+ }
651
+ const choice = Number(answer);
652
+ const selected = Number.isInteger(choice) ? providers[choice - 1] : undefined;
653
+ if (selected === undefined) {
654
+ this.#output("Invalid provider selection.");
655
+ return null;
656
+ }
657
+ return selected;
658
+ }
659
+ }
660
+ //# sourceMappingURL=commands.js.map