@ouro.bot/cli 0.1.0-alpha.72 → 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,20 @@
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
+ },
10
+ {
11
+ "version": "0.1.0-alpha.73",
12
+ "changes": [
13
+ "New `ouro config models --agent <name>` command: list available models for the current provider. For github-copilot, queries the models API; other providers show a static message.",
14
+ "Fix: `ouro config model` now validates model availability for github-copilot before writing, showing available models if the requested one isn't found.",
15
+ "Fix: system prompt now tells the agent that model/provider changes take effect on the next turn automatically (no restart needed)."
16
+ ]
17
+ },
4
18
  {
5
19
  "version": "0.1.0-alpha.72",
6
20
  "changes": [
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.ensureDaemonRunning = ensureDaemonRunning;
37
+ exports.listGithubCopilotModels = listGithubCopilotModels;
38
+ exports.pingGithubCopilotModel = pingGithubCopilotModel;
37
39
  exports.parseOuroCommand = parseOuroCommand;
38
40
  exports.discoverExistingCredentials = discoverExistingCredentials;
39
41
  exports.createDefaultOuroCliDeps = createDefaultOuroCliDeps;
@@ -270,6 +272,7 @@ function usage() {
270
272
  " ouro stop|down|status|logs|hatch",
271
273
  " ouro -v|--version",
272
274
  " ouro config model --agent <name> <model-name>",
275
+ " ouro config models --agent <name>",
273
276
  " ouro auth --agent <name> [--provider <provider>]",
274
277
  " ouro auth verify --agent <name> [--provider <provider>]",
275
278
  " ouro auth switch --agent <name> --provider <provider>",
@@ -488,7 +491,73 @@ async function verifyProviderCredentials(provider, providers, fetchImpl = fetch)
488
491
  return "failed (no api key)";
489
492
  return "ok";
490
493
  }
491
- /* v8 ignore stop */
494
+ async function listGithubCopilotModels(baseUrl, token, fetchImpl = fetch) {
495
+ const url = `${baseUrl.replace(/\/+$/, "")}/models`;
496
+ const response = await fetchImpl(url, {
497
+ headers: { Authorization: `Bearer ${token}` },
498
+ });
499
+ if (!response.ok) {
500
+ throw new Error(`model listing failed (HTTP ${response.status})`);
501
+ }
502
+ const body = await response.json();
503
+ /* v8 ignore start -- response shape handling: tested via config-models.test.ts @preserve */
504
+ const items = Array.isArray(body) ? body : (body?.data ?? []);
505
+ return items.map((item) => {
506
+ const rec = item;
507
+ const capabilities = Array.isArray(rec.capabilities)
508
+ ? rec.capabilities.filter((c) => typeof c === "string")
509
+ : undefined;
510
+ return {
511
+ id: String(rec.id ?? rec.name ?? ""),
512
+ name: String(rec.name ?? rec.id ?? ""),
513
+ ...(capabilities ? { capabilities } : {}),
514
+ };
515
+ });
516
+ /* v8 ignore stop */
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
+ }
492
561
  function parseHatchCommand(args) {
493
562
  let agentName;
494
563
  let humanName;
@@ -805,6 +874,11 @@ function parseConfigCommand(args) {
805
874
  throw new Error(`Usage: ouro config model --agent <name> <model-name>`);
806
875
  return { kind: "config.model", agent, modelName };
807
876
  }
877
+ if (sub === "models") {
878
+ if (!agent)
879
+ throw new Error("--agent is required for config models");
880
+ return { kind: "config.models", agent };
881
+ }
808
882
  throw new Error(`Usage\n${usage()}`);
809
883
  }
810
884
  function parseMcpCommand(args) {
@@ -1796,9 +1870,69 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
1796
1870
  return message;
1797
1871
  }
1798
1872
  /* v8 ignore stop */
1873
+ // ── config models (local, no daemon socket needed) ──
1874
+ /* v8 ignore start -- config models: tested via daemon-cli.test.ts @preserve */
1875
+ if (command.kind === "config.models") {
1876
+ const { config } = (0, auth_flow_1.readAgentConfigForAgent)(command.agent);
1877
+ const provider = config.provider;
1878
+ if (provider !== "github-copilot") {
1879
+ const message = `model listing not available for ${provider} — check provider documentation.`;
1880
+ deps.writeStdout(message);
1881
+ return message;
1882
+ }
1883
+ const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
1884
+ const ghConfig = secrets.providers["github-copilot"];
1885
+ if (!ghConfig.githubToken || !ghConfig.baseUrl) {
1886
+ throw new Error(`github-copilot credentials not configured. Run \`ouro auth --agent ${command.agent} --provider github-copilot\` first.`);
1887
+ }
1888
+ const fetchFn = deps.fetchImpl ?? fetch;
1889
+ const models = await listGithubCopilotModels(ghConfig.baseUrl, ghConfig.githubToken, fetchFn);
1890
+ if (models.length === 0) {
1891
+ const message = "no models found";
1892
+ deps.writeStdout(message);
1893
+ return message;
1894
+ }
1895
+ const lines = ["available models:"];
1896
+ for (const m of models) {
1897
+ const caps = m.capabilities?.length ? ` (${m.capabilities.join(", ")})` : "";
1898
+ lines.push(` ${m.id}${caps}`);
1899
+ }
1900
+ const message = lines.join("\n");
1901
+ deps.writeStdout(message);
1902
+ return message;
1903
+ }
1904
+ /* v8 ignore stop */
1799
1905
  // ── config model (local, no daemon socket needed) ──
1800
1906
  /* v8 ignore start -- config model: tested via daemon-cli.test.ts @preserve */
1801
1907
  if (command.kind === "config.model") {
1908
+ // Validate model availability for github-copilot before writing
1909
+ const { config } = (0, auth_flow_1.readAgentConfigForAgent)(command.agent);
1910
+ if (config.provider === "github-copilot") {
1911
+ const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
1912
+ const ghConfig = secrets.providers["github-copilot"];
1913
+ if (ghConfig.githubToken && ghConfig.baseUrl) {
1914
+ const fetchFn = deps.fetchImpl ?? fetch;
1915
+ try {
1916
+ const models = await listGithubCopilotModels(ghConfig.baseUrl, ghConfig.githubToken, fetchFn);
1917
+ const available = models.map((m) => m.id);
1918
+ if (available.length > 0 && !available.includes(command.modelName)) {
1919
+ const message = `model '${command.modelName}' not found. available models:\n${available.map((id) => ` ${id}`).join("\n")}`;
1920
+ deps.writeStdout(message);
1921
+ return message;
1922
+ }
1923
+ }
1924
+ catch {
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;
1933
+ }
1934
+ }
1935
+ }
1802
1936
  const { provider, previousModel } = (0, auth_flow_1.writeAgentModel)(command.agent, command.modelName);
1803
1937
  const message = previousModel
1804
1938
  ? `updated ${command.agent} model on ${provider}: ${previousModel} → ${command.modelName}`
@@ -182,12 +182,15 @@ my bones give me the \`ouro\` cli. always pass \`--agent ${agentName}\`:
182
182
  ouro session list --agent ${agentName}
183
183
  ouro reminder create --agent ${agentName} <title> --body <body>
184
184
  ouro config model --agent ${agentName} <model-name>
185
+ ouro config models --agent ${agentName}
185
186
  ouro auth --agent ${agentName} --provider <provider>
186
187
  ouro auth verify --agent ${agentName} [--provider <provider>]
187
188
  ouro auth switch --agent ${agentName} --provider <provider>
188
189
  ouro mcp list --agent ${agentName}
189
190
  ouro mcp call --agent ${agentName} <server> <tool> --args '{...}'
190
- ouro --help`;
191
+ ouro --help
192
+
193
+ provider/model changes via \`ouro config model\` or \`ouro auth switch\` take effect on the next turn automatically — no restart needed.`;
191
194
  }
192
195
  function mcpToolsSection(mcpManager) {
193
196
  if (!mcpManager)
@@ -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)) {
@@ -168,6 +168,7 @@ exports.OURO_CLI_TRUST_MANIFEST = {
168
168
  "friend update": "family",
169
169
  "reminder create": "friend",
170
170
  "config model": "friend",
171
+ "config models": "friend",
171
172
  "mcp list": "acquaintance",
172
173
  "mcp call": "friend",
173
174
  auth: "family",
@@ -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.72",
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",