@ouro.bot/cli 0.1.0-alpha.73 → 0.1.0-alpha.74

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/changelog.json CHANGED
@@ -1,6 +1,12 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.74",
6
+ "changes": [
7
+ "Fix: `ouro config model` now pings the model with a tiny API call before switching, rejecting the change if the model returns an error (403, not supported, etc.)."
8
+ ]
9
+ },
4
10
  {
5
11
  "version": "0.1.0-alpha.73",
6
12
  "changes": [
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.ensureDaemonRunning = ensureDaemonRunning;
37
37
  exports.listGithubCopilotModels = listGithubCopilotModels;
38
+ exports.pingGithubCopilotModel = pingGithubCopilotModel;
38
39
  exports.parseOuroCommand = parseOuroCommand;
39
40
  exports.discoverExistingCredentials = discoverExistingCredentials;
40
41
  exports.createDefaultOuroCliDeps = createDefaultOuroCliDeps;
@@ -514,6 +515,49 @@ async function listGithubCopilotModels(baseUrl, token, fetchImpl = fetch) {
514
515
  });
515
516
  /* v8 ignore stop */
516
517
  }
518
+ async function pingGithubCopilotModel(baseUrl, token, model, fetchImpl = fetch) {
519
+ const base = baseUrl.replace(/\/+$/, "");
520
+ const isClaude = model.startsWith("claude");
521
+ const url = isClaude ? `${base}/chat/completions` : `${base}/responses`;
522
+ const body = isClaude
523
+ ? JSON.stringify({ model, messages: [{ role: "user", content: "ping" }], max_tokens: 1 })
524
+ : JSON.stringify({ model, input: "ping", max_output_tokens: 16 });
525
+ try {
526
+ const response = await fetchImpl(url, {
527
+ method: "POST",
528
+ headers: {
529
+ Authorization: `Bearer ${token}`,
530
+ "Content-Type": "application/json",
531
+ },
532
+ body,
533
+ });
534
+ if (response.ok)
535
+ return { ok: true };
536
+ let detail = `HTTP ${response.status}`;
537
+ try {
538
+ const json = await response.json();
539
+ /* v8 ignore start -- error format parsing: all branches tested via config-models.test.ts @preserve */
540
+ if (typeof json.error === "string")
541
+ detail = json.error;
542
+ else if (typeof json.error === "object" && json.error !== null) {
543
+ const errObj = json.error;
544
+ if (typeof errObj.message === "string")
545
+ detail = errObj.message;
546
+ }
547
+ else if (typeof json.message === "string")
548
+ detail = json.message;
549
+ /* v8 ignore stop */
550
+ }
551
+ catch {
552
+ // response body not JSON — keep HTTP status
553
+ }
554
+ return { ok: false, error: detail };
555
+ }
556
+ catch (err) {
557
+ /* v8 ignore next -- defensive: fetch errors are always Error instances @preserve */
558
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
559
+ }
560
+ }
517
561
  function parseHatchCommand(args) {
518
562
  let agentName;
519
563
  let humanName;
@@ -1878,7 +1922,14 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
1878
1922
  }
1879
1923
  }
1880
1924
  catch {
1881
- // Validation failed — fall through and write anyway
1925
+ // Catalog validation failed — fall through to ping test
1926
+ }
1927
+ // Ping test: verify the model actually works before switching
1928
+ const pingResult = await pingGithubCopilotModel(ghConfig.baseUrl, ghConfig.githubToken, command.modelName, fetchFn);
1929
+ if (!pingResult.ok) {
1930
+ const message = `model '${command.modelName}' ping failed: ${pingResult.error}\nrun \`ouro config models --agent ${command.agent}\` to see available models.`;
1931
+ deps.writeStdout(message);
1932
+ return message;
1882
1933
  }
1883
1934
  }
1884
1935
  }
@@ -4,6 +4,7 @@ exports.createTraceId = createTraceId;
4
4
  exports.ensureTraceId = ensureTraceId;
5
5
  exports.createFanoutSink = createFanoutSink;
6
6
  exports.formatTerminalEntry = formatTerminalEntry;
7
+ exports.registerSpinnerHooks = registerSpinnerHooks;
7
8
  exports.createTerminalSink = createTerminalSink;
8
9
  exports.createStderrSink = createStderrSink;
9
10
  exports.createNdjsonFileSink = createNdjsonFileSink;
@@ -73,15 +74,26 @@ function formatTerminalEntry(entry) {
73
74
  const level = entry.level.toUpperCase();
74
75
  return `${formatTerminalTime(entry.ts)} ${level} [${entry.component}] ${entry.message}${formatTerminalMeta(entry.meta)}`;
75
76
  }
77
+ // Spinner coordination: the CLI sense registers these so log output
78
+ // doesn't interleave with the active spinner animation.
79
+ let _pauseSpinner = null;
80
+ let _resumeSpinner = null;
81
+ function registerSpinnerHooks(pause, resume) {
82
+ _pauseSpinner = pause;
83
+ _resumeSpinner = resume;
84
+ }
76
85
  function createTerminalSink(write = (chunk) => process.stderr.write(chunk), colorize = true) {
77
86
  return (entry) => {
87
+ _pauseSpinner?.();
78
88
  const line = formatTerminalEntry(entry);
79
89
  if (!colorize) {
80
90
  write(`${line}\n`);
91
+ _resumeSpinner?.();
81
92
  return;
82
93
  }
83
94
  const prefix = LEVEL_COLORS[entry.level];
84
95
  write(`${prefix}${line}\x1b[0m\n`);
96
+ _resumeSpinner?.();
85
97
  };
86
98
  }
87
99
  function createStderrSink(write = (chunk) => process.stderr.write(chunk)) {
@@ -36,6 +36,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.MarkdownStreamer = exports.InputController = exports.Spinner = exports.wrapCliText = exports.formatEchoedInputSummary = void 0;
37
37
  exports.formatPendingPrefix = formatPendingPrefix;
38
38
  exports.getCliContinuityIngressTexts = getCliContinuityIngressTexts;
39
+ exports.pauseActiveSpinner = pauseActiveSpinner;
40
+ exports.resumeActiveSpinner = resumeActiveSpinner;
41
+ exports.setActiveSpinner = setActiveSpinner;
39
42
  exports.handleSigint = handleSigint;
40
43
  exports.addHistory = addHistory;
41
44
  exports.renderMarkdown = renderMarkdown;
@@ -90,6 +93,14 @@ function getCliContinuityIngressTexts(input) {
90
93
  const trimmed = input.trim();
91
94
  return trimmed ? [trimmed] : [];
92
95
  }
96
+ // Module-level active spinner for log coordination.
97
+ // The terminal log sink calls these to avoid interleaving with spinner output.
98
+ let _activeSpinner = null;
99
+ /* v8 ignore start -- spinner coordination: exercised at runtime, not unit-testable without real terminal @preserve */
100
+ function pauseActiveSpinner() { _activeSpinner?.pause(); }
101
+ function resumeActiveSpinner() { _activeSpinner?.resume(); }
102
+ /* v8 ignore stop */
103
+ function setActiveSpinner(s) { _activeSpinner = s; }
93
104
  // spinner that only touches stderr, cleans up after itself
94
105
  // exported for direct testability (stop-without-start branch)
95
106
  class Spinner {
@@ -131,6 +142,20 @@ class Spinner {
131
142
  this.lastPhrase = next;
132
143
  this.msg = next;
133
144
  }
145
+ /* v8 ignore start -- pause/resume: exercised at runtime via log sink coordination @preserve */
146
+ /** Clear the spinner line temporarily so other output can print cleanly. */
147
+ pause() {
148
+ if (this.stopped)
149
+ return;
150
+ process.stderr.write("\r\x1b[K");
151
+ }
152
+ /** Restore the spinner line after a pause. */
153
+ resume() {
154
+ if (this.stopped)
155
+ return;
156
+ this.spin();
157
+ }
158
+ /* v8 ignore stop */
134
159
  stop(ok) {
135
160
  this.stopped = true;
136
161
  if (this.iv) {
@@ -319,6 +344,7 @@ function createCliCallbacks() {
319
344
  meta: {},
320
345
  });
321
346
  let currentSpinner = null;
347
+ function setSpinner(s) { currentSpinner = s; setActiveSpinner(s); }
322
348
  let hadReasoning = false;
323
349
  let hadToolRun = false;
324
350
  let textDirty = false; // true when text/reasoning was written without a trailing newline
@@ -326,14 +352,14 @@ function createCliCallbacks() {
326
352
  return {
327
353
  onModelStart: () => {
328
354
  currentSpinner?.stop();
329
- currentSpinner = null;
355
+ setSpinner(null);
330
356
  hadReasoning = false;
331
357
  textDirty = false;
332
358
  streamer.reset();
333
359
  const phrases = (0, phrases_1.getPhrases)();
334
360
  const pool = hadToolRun ? phrases.followup : phrases.thinking;
335
361
  const first = (0, phrases_1.pickPhrase)(pool);
336
- currentSpinner = new Spinner(first, pool);
362
+ setSpinner(new Spinner(first, pool));
337
363
  currentSpinner.start();
338
364
  },
339
365
  onModelStreamStart: () => {
@@ -350,7 +376,7 @@ function createCliCallbacks() {
350
376
  // otherwise keep running (and its \r writes overwrite response text).
351
377
  if (currentSpinner) {
352
378
  currentSpinner.stop();
353
- currentSpinner = null;
379
+ setSpinner(null);
354
380
  }
355
381
  if (hadReasoning) {
356
382
  // Single newline to separate reasoning from reply — reasoning
@@ -366,7 +392,7 @@ function createCliCallbacks() {
366
392
  onReasoningChunk: (text) => {
367
393
  if (currentSpinner) {
368
394
  currentSpinner.stop();
369
- currentSpinner = null;
395
+ setSpinner(null);
370
396
  }
371
397
  hadReasoning = true;
372
398
  process.stdout.write(`\x1b[2m${text}\x1b[0m`);
@@ -385,13 +411,13 @@ function createCliCallbacks() {
385
411
  }
386
412
  const toolPhrases = (0, phrases_1.getPhrases)().tool;
387
413
  const first = (0, phrases_1.pickPhrase)(toolPhrases);
388
- currentSpinner = new Spinner(first, toolPhrases);
414
+ setSpinner(new Spinner(first, toolPhrases));
389
415
  currentSpinner.start();
390
416
  hadToolRun = true;
391
417
  },
392
418
  onToolEnd: (name, argSummary, success) => {
393
419
  currentSpinner?.stop();
394
- currentSpinner = null;
420
+ setSpinner(null);
395
421
  const msg = (0, format_1.formatToolResult)(name, argSummary, success);
396
422
  const color = success ? "\x1b[32m" : "\x1b[31m";
397
423
  process.stderr.write(`${color}${msg}\x1b[0m\n`);
@@ -399,17 +425,17 @@ function createCliCallbacks() {
399
425
  onError: (error, severity) => {
400
426
  if (severity === "transient") {
401
427
  currentSpinner?.fail(error.message);
402
- currentSpinner = null;
428
+ setSpinner(null);
403
429
  }
404
430
  else {
405
431
  currentSpinner?.stop();
406
- currentSpinner = null;
432
+ setSpinner(null);
407
433
  process.stderr.write(`\x1b[31m${(0, format_1.formatError)(error)}\x1b[0m\n`);
408
434
  }
409
435
  },
410
436
  onKick: () => {
411
437
  currentSpinner?.stop();
412
- currentSpinner = null;
438
+ setSpinner(null);
413
439
  if (textDirty) {
414
440
  process.stdout.write("\n");
415
441
  textDirty = false;
@@ -418,7 +444,7 @@ function createCliCallbacks() {
418
444
  },
419
445
  flushMarkdown: () => {
420
446
  currentSpinner?.stop();
421
- currentSpinner = null;
447
+ setSpinner(null);
422
448
  const remaining = streamer.flush();
423
449
  if (remaining)
424
450
  process.stdout.write(remaining);
@@ -694,6 +720,8 @@ async function main(agentName, options) {
694
720
  if (agentName)
695
721
  (0, identity_1.setAgentName)(agentName);
696
722
  const pasteDebounceMs = options?.pasteDebounceMs ?? 50;
723
+ // Register spinner hooks so log output clears the spinner before printing
724
+ (0, nerves_1.registerSpinnerHooks)(pauseActiveSpinner, resumeActiveSpinner);
697
725
  // Fallback: apply pending updates for daemon-less direct CLI usage
698
726
  (0, update_hooks_1.registerUpdateHook)(bundle_meta_1.bundleMetaHook);
699
727
  await (0, update_hooks_1.applyPendingUpdates)((0, identity_1.getAgentBundlesRoot)(), (0, bundle_manifest_1.getPackageVersion)());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.73",
3
+ "version": "0.1.0-alpha.74",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",