@ouro.bot/cli 0.1.0-alpha.347 → 0.1.0-alpha.349

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.
@@ -63,6 +63,11 @@ const tasks_1 = require("../../repertoire/tasks");
63
63
  const thoughts_1 = require("./thoughts");
64
64
  const launchd_1 = require("./launchd");
65
65
  const auth_flow_1 = require("../auth/auth-flow");
66
+ const provider_credential_pool_1 = require("../provider-credential-pool");
67
+ const provider_binding_resolver_1 = require("../provider-binding-resolver");
68
+ const provider_state_1 = require("../provider-state");
69
+ const machine_identity_1 = require("../machine-identity");
70
+ const provider_models_1 = require("../provider-models");
66
71
  const ouro_version_manager_1 = require("../versioning/ouro-version-manager");
67
72
  const cli_parse_1 = require("./cli-parse");
68
73
  const cli_parse_2 = require("./cli-parse");
@@ -411,10 +416,6 @@ async function listGithubCopilotModels(baseUrl, token, fetchImpl = fetch) {
411
416
  /* v8 ignore stop */
412
417
  }
413
418
  // ── Provider credential verification ──
414
- /* v8 ignore next 3 -- only called from auth.switch inside integration block @preserve */
415
- function hasStoredCredentials(provider, providerSecrets) {
416
- return identity_1.PROVIDER_CREDENTIALS[provider].required.every((key) => !!providerSecrets[key]);
417
- }
418
419
  /* v8 ignore start -- verifyProviderCredentials: delegates to pingProvider @preserve */
419
420
  async function verifyProviderCredentials(provider, providers) {
420
421
  const config = providers[provider];
@@ -458,6 +459,382 @@ async function resolveHatchInput(command, deps) {
458
459
  migrationPath: command.migrationPath,
459
460
  };
460
461
  }
462
+ // ── Provider state CLI helpers ──
463
+ function providerCliHomeDir(deps) {
464
+ if (!deps.secretsRoot)
465
+ return os.homedir();
466
+ return path.basename(deps.secretsRoot) === ".agentsecrets"
467
+ ? path.dirname(deps.secretsRoot)
468
+ : deps.secretsRoot;
469
+ }
470
+ function providerCliAgentRoot(command, deps) {
471
+ return path.join(deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`);
472
+ }
473
+ function providerCliNow(deps) {
474
+ return new Date((deps.now ?? Date.now)());
475
+ }
476
+ function readOrBootstrapProviderState(agentName, deps) {
477
+ const agentRoot = providerCliAgentRoot({ agent: agentName }, deps);
478
+ const readResult = (0, provider_state_1.readProviderState)(agentRoot);
479
+ if (readResult.ok)
480
+ return { agentRoot, state: readResult.state };
481
+ if (readResult.reason === "invalid") {
482
+ throw new Error(`provider state for ${agentName} is invalid at ${readResult.statePath}: ${readResult.error}`);
483
+ }
484
+ const { config } = (0, auth_flow_1.readAgentConfigForAgent)(agentName, deps.bundlesRoot);
485
+ const homeDir = providerCliHomeDir(deps);
486
+ const machine = (0, machine_identity_1.loadOrCreateMachineIdentity)({
487
+ homeDir,
488
+ now: () => providerCliNow(deps),
489
+ });
490
+ const state = (0, provider_state_1.bootstrapProviderStateFromAgentConfig)({
491
+ machineId: machine.machineId,
492
+ now: providerCliNow(deps),
493
+ agentConfig: {
494
+ humanFacing: {
495
+ provider: config.humanFacing.provider,
496
+ model: config.humanFacing.model || (0, provider_models_1.getDefaultModelForProvider)(config.humanFacing.provider),
497
+ },
498
+ agentFacing: {
499
+ provider: config.agentFacing.provider,
500
+ model: config.agentFacing.model || (0, provider_models_1.getDefaultModelForProvider)(config.agentFacing.provider),
501
+ },
502
+ },
503
+ });
504
+ (0, provider_state_1.writeProviderState)(agentRoot, state);
505
+ (0, runtime_1.emitNervesEvent)({
506
+ component: "daemon",
507
+ event: "daemon.provider_state_bootstrapped",
508
+ message: "bootstrapped local provider state from agent config",
509
+ meta: { agent: agentName, agentRoot },
510
+ });
511
+ return { agentRoot, state };
512
+ }
513
+ function credentialPingConfig(record) {
514
+ return {
515
+ ...record.credentials,
516
+ ...record.config,
517
+ };
518
+ }
519
+ function pingAttemptCount(result) {
520
+ if (typeof result.attempts === "number")
521
+ return result.attempts;
522
+ if (Array.isArray(result.attempts))
523
+ return result.attempts.length;
524
+ return undefined;
525
+ }
526
+ function providerCliLegacyRecord(agent, provider, deps) {
527
+ try {
528
+ const legacyCandidates = (0, provider_credential_pool_1.readLegacyAgentProviderCredentials)({
529
+ homeDir: providerCliHomeDir(deps),
530
+ agentName: agent,
531
+ });
532
+ const candidate = legacyCandidates.find((entry) => entry.provider === provider);
533
+ if (candidate)
534
+ return { credentials: candidate.credentials, config: candidate.config };
535
+ }
536
+ catch {
537
+ // Fall through to the injected/secretsRoot-aware legacy reader below.
538
+ }
539
+ try {
540
+ const { secrets } = (0, auth_flow_1.loadAgentSecrets)(agent, { secretsRoot: deps.secretsRoot });
541
+ const providerSecrets = secrets.providers[provider];
542
+ const split = (0, provider_credential_pool_1.splitProviderCredentialFields)(provider, providerSecrets);
543
+ if (Object.keys(split.credentials).length === 0 && Object.keys(split.config).length === 0)
544
+ return null;
545
+ return split;
546
+ }
547
+ catch {
548
+ return null;
549
+ }
550
+ }
551
+ function readProviderCredentialRecord(agent, provider, deps) {
552
+ const homeDir = providerCliHomeDir(deps);
553
+ const poolResult = (0, provider_credential_pool_1.readProviderCredentialPool)(homeDir);
554
+ if (poolResult.ok) {
555
+ const existing = poolResult.pool.providers[provider];
556
+ if (existing)
557
+ return { ok: true, record: existing };
558
+ }
559
+ else if (poolResult.reason === "invalid") {
560
+ return { ok: false, reason: "invalid", poolPath: poolResult.poolPath, error: poolResult.error };
561
+ }
562
+ const legacy = providerCliLegacyRecord(agent, provider, deps);
563
+ if (legacy) {
564
+ const record = (0, provider_credential_pool_1.upsertProviderCredential)({
565
+ homeDir,
566
+ provider,
567
+ credentials: legacy.credentials,
568
+ config: legacy.config,
569
+ provenance: {
570
+ source: "legacy-agent-secrets",
571
+ contributedByAgent: agent,
572
+ },
573
+ now: providerCliNow(deps),
574
+ });
575
+ return { ok: true, record };
576
+ }
577
+ return {
578
+ ok: false,
579
+ reason: "missing",
580
+ poolPath: poolResult.poolPath,
581
+ error: `no credentials stored for ${provider}`,
582
+ };
583
+ }
584
+ function writeProviderBinding(input) {
585
+ const updatedAt = providerCliNow(input.deps).toISOString();
586
+ input.state.updatedAt = updatedAt;
587
+ input.state.lanes[input.lane] = {
588
+ provider: input.provider,
589
+ model: input.model,
590
+ source: "local",
591
+ updatedAt,
592
+ };
593
+ input.state.readiness[input.lane] = {
594
+ status: input.status,
595
+ provider: input.provider,
596
+ model: input.model,
597
+ checkedAt: updatedAt,
598
+ ...(input.credentialRevision ? { credentialRevision: input.credentialRevision } : {}),
599
+ ...(input.error ? { error: input.error } : {}),
600
+ ...(input.attempts !== undefined ? { attempts: input.attempts } : {}),
601
+ };
602
+ (0, provider_state_1.writeProviderState)(input.agentRoot, input.state);
603
+ }
604
+ function writeProviderReadiness(input) {
605
+ const checkedAt = providerCliNow(input.deps).toISOString();
606
+ input.state.updatedAt = checkedAt;
607
+ input.state.readiness[input.lane] = {
608
+ status: input.status,
609
+ provider: input.provider,
610
+ model: input.model,
611
+ checkedAt,
612
+ credentialRevision: input.credentialRevision,
613
+ ...(input.error ? { error: input.error } : {}),
614
+ ...(input.attempts !== undefined ? { attempts: input.attempts } : {}),
615
+ };
616
+ (0, provider_state_1.writeProviderState)(input.agentRoot, input.state);
617
+ }
618
+ async function executeProviderUse(command, deps, options = {}) {
619
+ const writeMessage = (message) => {
620
+ if (options.writeStdout !== false)
621
+ deps.writeStdout(message);
622
+ return message;
623
+ };
624
+ const { agentRoot, state } = readOrBootstrapProviderState(command.agent, deps);
625
+ const credential = readProviderCredentialRecord(command.agent, command.provider, deps);
626
+ if (!credential.ok) {
627
+ if (!command.force) {
628
+ const message = [
629
+ `no credentials stored for ${command.provider}.`,
630
+ `Run \`ouro auth --agent ${command.agent} --provider ${command.provider}\` first.`,
631
+ ].join("\n");
632
+ return writeMessage(message);
633
+ }
634
+ writeProviderBinding({
635
+ agentRoot,
636
+ state,
637
+ lane: command.lane,
638
+ provider: command.provider,
639
+ model: command.model,
640
+ deps,
641
+ status: "failed",
642
+ error: credential.error,
643
+ });
644
+ const message = `forced ${command.agent} ${command.lane} to ${command.provider} / ${command.model}: failed (${credential.error})`;
645
+ return writeMessage(message);
646
+ }
647
+ const pingResult = await (0, provider_ping_1.pingProvider)(command.provider, credentialPingConfig(credential.record), {
648
+ model: command.model,
649
+ attemptPolicy: { baseDelayMs: 0 },
650
+ sleep: deps.sleep,
651
+ });
652
+ const attempts = pingAttemptCount(pingResult);
653
+ if (!pingResult.ok && !command.force) {
654
+ const message = [
655
+ `${command.agent} ${command.lane} ${command.provider} / ${command.model}: failed (${pingResult.message})`,
656
+ `Fix credentials with \`ouro auth --agent ${command.agent} --provider ${command.provider}\` or force the local binding with \`ouro use --agent ${command.agent} --lane ${command.lane} --provider ${command.provider} --model ${command.model} --force\`.`,
657
+ ].join("\n");
658
+ return writeMessage(message);
659
+ }
660
+ writeProviderBinding({
661
+ agentRoot,
662
+ state,
663
+ lane: command.lane,
664
+ provider: command.provider,
665
+ model: command.model,
666
+ deps,
667
+ status: pingResult.ok ? "ready" : "failed",
668
+ credentialRevision: credential.record.revision,
669
+ ...(!pingResult.ok ? { error: pingResult.message } : {}),
670
+ ...(attempts !== undefined ? { attempts } : {}),
671
+ });
672
+ const status = pingResult.ok ? "ready" : `failed (${pingResult.message})`;
673
+ const message = `${command.force ? "forced " : ""}${command.agent} ${command.lane} ${command.provider} / ${command.model}: ${status}`;
674
+ (0, runtime_1.emitNervesEvent)({
675
+ component: "daemon",
676
+ event: "daemon.provider_use_completed",
677
+ message: "provider use command completed",
678
+ meta: { agent: command.agent, lane: command.lane, provider: command.provider, model: command.model, status: pingResult.ok ? "ready" : "failed" },
679
+ });
680
+ return writeMessage(message);
681
+ }
682
+ async function executeProviderCheck(command, deps) {
683
+ const { agentRoot, state } = readOrBootstrapProviderState(command.agent, deps);
684
+ const binding = state.lanes[command.lane];
685
+ const credential = readProviderCredentialRecord(command.agent, binding.provider, deps);
686
+ if (!credential.ok) {
687
+ const message = [
688
+ `${command.agent} ${command.lane} ${binding.provider} / ${binding.model}: unknown (${credential.error})`,
689
+ `Run \`ouro auth --agent ${command.agent} --provider ${binding.provider}\` first.`,
690
+ ].join("\n");
691
+ deps.writeStdout(message);
692
+ return message;
693
+ }
694
+ const pingResult = await (0, provider_ping_1.pingProvider)(binding.provider, credentialPingConfig(credential.record), {
695
+ model: binding.model,
696
+ attemptPolicy: { baseDelayMs: 0 },
697
+ sleep: deps.sleep,
698
+ });
699
+ const attempts = pingAttemptCount(pingResult);
700
+ writeProviderReadiness({
701
+ agentRoot,
702
+ state,
703
+ lane: command.lane,
704
+ provider: binding.provider,
705
+ model: binding.model,
706
+ deps,
707
+ status: pingResult.ok ? "ready" : "failed",
708
+ credentialRevision: credential.record.revision,
709
+ ...(!pingResult.ok ? { error: pingResult.message } : {}),
710
+ ...(attempts !== undefined ? { attempts } : {}),
711
+ });
712
+ const status = pingResult.ok ? "ready" : `failed (${pingResult.message})`;
713
+ const message = `${command.agent} ${command.lane} ${binding.provider} / ${binding.model}: ${status}`;
714
+ deps.writeStdout(message);
715
+ (0, runtime_1.emitNervesEvent)({
716
+ component: "daemon",
717
+ event: "daemon.provider_check_completed",
718
+ message: "provider check command completed",
719
+ meta: { agent: command.agent, lane: command.lane, provider: binding.provider, model: binding.model, status: pingResult.ok ? "ready" : "failed" },
720
+ });
721
+ return message;
722
+ }
723
+ function renderProviderCredentialLine(credential) {
724
+ if (credential.status === "present") {
725
+ const contributor = credential.contributedByAgent ? ` by ${credential.contributedByAgent}` : "";
726
+ const credentialFields = credential.credentialFields.length > 0 ? ` credentials: ${credential.credentialFields.join(", ")}` : " credentials: none";
727
+ const configFields = credential.configFields.length > 0 ? ` config: ${credential.configFields.join(", ")}` : " config: none";
728
+ return `credentials: present (${credential.source}${contributor}; ${credential.revision};${credentialFields};${configFields})`;
729
+ }
730
+ if (credential.status === "invalid-pool") {
731
+ return `credentials: invalid pool (${credential.error}); repair: ${credential.repair.command}`;
732
+ }
733
+ return `credentials: missing; repair: ${credential.repair.command}`;
734
+ }
735
+ function executeProviderStatus(command, deps) {
736
+ const agentRoot = providerCliAgentRoot(command, deps);
737
+ const homeDir = providerCliHomeDir(deps);
738
+ const lines = [`provider status: ${command.agent}`];
739
+ for (const lane of ["outward", "inner"]) {
740
+ const resolved = (0, provider_binding_resolver_1.resolveEffectiveProviderBinding)({
741
+ agentName: command.agent,
742
+ agentRoot,
743
+ homeDir,
744
+ lane,
745
+ });
746
+ if (!resolved.ok) {
747
+ lines.push(` ${lane}: unavailable`);
748
+ lines.push(` ${resolved.reason}: ${resolved.repair.command}`);
749
+ continue;
750
+ }
751
+ const binding = resolved.binding;
752
+ lines.push(` ${lane}: ${binding.provider} / ${binding.model} (${binding.source})`);
753
+ lines.push(` readiness: ${binding.readiness.status}${binding.readiness.error ? ` (${binding.readiness.error})` : ""}`);
754
+ lines.push(` ${renderProviderCredentialLine(binding.credential)}`);
755
+ for (const warning of binding.warnings) {
756
+ lines.push(` warning: ${warning.message}`);
757
+ }
758
+ }
759
+ const message = lines.join("\n");
760
+ deps.writeStdout(message);
761
+ return message;
762
+ }
763
+ async function executeLegacyAuthSwitch(command, deps) {
764
+ const { state } = readOrBootstrapProviderState(command.agent, deps);
765
+ const lanes = command.facing
766
+ ? [command.facing === "human" ? "outward" : "inner"]
767
+ : ["outward", "inner"];
768
+ const messages = [];
769
+ for (const lane of lanes) {
770
+ const model = state.lanes[lane].model;
771
+ messages.push(await executeProviderUse({
772
+ kind: "provider.use",
773
+ agent: command.agent,
774
+ lane,
775
+ provider: command.provider,
776
+ model,
777
+ legacyFacing: command.facing,
778
+ }, deps, { writeStdout: false }));
779
+ }
780
+ const message = [
781
+ `deprecated: switched this machine's local provider binding. \`ouro auth switch\` no longer edits agent.json.`,
782
+ ...messages,
783
+ `Use \`ouro use --agent ${command.agent} --lane <outward|inner> --provider ${command.provider} --model <model>\` for explicit provider/model selection.`,
784
+ ].join("\n");
785
+ deps.writeStdout(message);
786
+ return message;
787
+ }
788
+ async function executeLegacyConfigModel(command, deps) {
789
+ const lane = command.facing === "agent" ? "inner" : "outward";
790
+ const { agentRoot, state } = readOrBootstrapProviderState(command.agent, deps);
791
+ const binding = state.lanes[lane];
792
+ if (binding.provider === "github-copilot") {
793
+ const credential = readProviderCredentialRecord(command.agent, "github-copilot", deps);
794
+ if (credential.ok) {
795
+ const ghConfig = {
796
+ ...credential.record.config,
797
+ ...credential.record.credentials,
798
+ };
799
+ const githubToken = ghConfig.githubToken;
800
+ const baseUrl = ghConfig.baseUrl;
801
+ if (typeof githubToken === "string" && typeof baseUrl === "string") {
802
+ const fetchFn = deps.fetchImpl ?? fetch;
803
+ try {
804
+ const models = await listGithubCopilotModels(baseUrl, githubToken, fetchFn);
805
+ const available = models.map((m) => m.id);
806
+ if (available.length > 0 && !available.includes(command.modelName)) {
807
+ const message = `model '${command.modelName}' not found. available models:\n${available.map((id) => ` ${id}`).join("\n")}`;
808
+ deps.writeStdout(message);
809
+ return message;
810
+ }
811
+ }
812
+ catch {
813
+ // Catalog validation failed; the live ping below gives the actionable result.
814
+ }
815
+ const pingResult = await (0, provider_ping_1.pingGithubCopilotModel)(baseUrl, githubToken, command.modelName, fetchFn);
816
+ if (!pingResult.ok) {
817
+ const message = `model '${command.modelName}' ping failed: ${pingResult.error}\nrun \`ouro config models --agent ${command.agent}\` to see available models.`;
818
+ deps.writeStdout(message);
819
+ return message;
820
+ }
821
+ }
822
+ }
823
+ }
824
+ const updatedAt = providerCliNow(deps).toISOString();
825
+ state.updatedAt = updatedAt;
826
+ state.lanes[lane] = {
827
+ ...binding,
828
+ model: command.modelName,
829
+ source: "local",
830
+ updatedAt,
831
+ };
832
+ delete state.readiness[lane];
833
+ (0, provider_state_1.writeProviderState)(agentRoot, state);
834
+ const message = `deprecated: updated ${command.agent} model on ${lane}/${binding.provider}: ${binding.model} -> ${command.modelName}\nUse \`ouro use --agent ${command.agent} --lane ${lane} --provider ${binding.provider} --model ${command.modelName}\` next time.`;
835
+ deps.writeStdout(message);
836
+ return message;
837
+ }
461
838
  // ── System setup ──
462
839
  async function registerOuroBundleTypeNonBlocking(deps) {
463
840
  const registerOuroBundleType = deps.registerOuroBundleType;
@@ -1731,6 +2108,16 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
1731
2108
  deps.writeStdout(message);
1732
2109
  return message;
1733
2110
  }
2111
+ // ── provider state commands (local, no daemon socket needed) ──
2112
+ if (command.kind === "provider.use") {
2113
+ return executeProviderUse(command, deps);
2114
+ }
2115
+ if (command.kind === "provider.check") {
2116
+ return executeProviderCheck(command, deps);
2117
+ }
2118
+ if (command.kind === "provider.status") {
2119
+ return executeProviderStatus(command, deps);
2120
+ }
1734
2121
  // ── auth (local, no daemon socket needed) ──
1735
2122
  if (command.kind === "auth.run") {
1736
2123
  const provider = command.provider ?? (0, auth_flow_1.readAgentConfigForAgent)(command.agent, deps.bundlesRoot).config.humanFacing.provider;
@@ -1741,6 +2128,21 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
1741
2128
  provider,
1742
2129
  promptInput: deps.promptInput,
1743
2130
  });
2131
+ const credentials = (result.credentials ?? {});
2132
+ const split = (0, provider_credential_pool_1.splitProviderCredentialFields)(provider, credentials);
2133
+ if (Object.keys(split.credentials).length > 0 || Object.keys(split.config).length > 0) {
2134
+ (0, provider_credential_pool_1.upsertProviderCredential)({
2135
+ homeDir: providerCliHomeDir(deps),
2136
+ provider,
2137
+ credentials: split.credentials,
2138
+ config: split.config,
2139
+ provenance: {
2140
+ source: "auth-flow",
2141
+ contributedByAgent: command.agent,
2142
+ },
2143
+ now: providerCliNow(deps),
2144
+ });
2145
+ }
1744
2146
  // Behavior: ouro auth stores credentials only — does NOT switch provider.
1745
2147
  // Use `ouro auth switch` to change the active provider.
1746
2148
  deps.writeStdout(result.message);
@@ -1779,39 +2181,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
1779
2181
  }
1780
2182
  // ── auth switch (local, no daemon socket needed) ──
1781
2183
  if (command.kind === "auth.switch") {
1782
- const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent, { secretsRoot: deps.secretsRoot });
1783
- const providerSecrets = secrets.providers[command.provider];
1784
- if (!providerSecrets || !hasStoredCredentials(command.provider, providerSecrets)) {
1785
- const message = `no credentials stored for ${command.provider}. Run \`ouro auth --agent ${command.agent} --provider ${command.provider}\` first.`;
1786
- deps.writeStdout(message);
1787
- return message;
1788
- }
1789
- // Verify credentials actually work before switching
1790
- const status = await verifyProviderCredentials(command.provider, secrets.providers);
1791
- if (!status.startsWith("ok")) {
1792
- const message = `${command.provider}: ${status}. fix credentials with \`ouro auth --agent ${command.agent} --provider ${command.provider}\` before switching.`;
1793
- deps.writeStdout(message);
1794
- return message;
1795
- }
1796
- if (command.facing) {
1797
- (0, auth_flow_1.writeAgentProviderSelection)(command.agent, command.facing, command.provider, deps.bundlesRoot);
1798
- }
1799
- else {
1800
- (0, auth_flow_1.writeAgentProviderSelection)(command.agent, "human", command.provider, deps.bundlesRoot);
1801
- (0, auth_flow_1.writeAgentProviderSelection)(command.agent, "agent", command.provider, deps.bundlesRoot);
1802
- }
1803
- const { config: updatedConfig } = (0, auth_flow_1.readAgentConfigForAgent)(command.agent, deps.bundlesRoot);
1804
- const facingSummary = command.facing
1805
- ? (() => {
1806
- const facingConfig = command.facing === "human" ? updatedConfig.humanFacing : updatedConfig.agentFacing;
1807
- return `${command.facing} model: ${facingConfig.model}`;
1808
- })()
1809
- : updatedConfig.humanFacing.model === updatedConfig.agentFacing.model
1810
- ? `model: ${updatedConfig.humanFacing.model}`
1811
- : `human model: ${updatedConfig.humanFacing.model}; agent model: ${updatedConfig.agentFacing.model}`;
1812
- const message = `switched ${command.agent} to ${command.provider} (${facingSummary}; verified working)`;
1813
- deps.writeStdout(message);
1814
- return message;
2184
+ return executeLegacyAuthSwitch(command, deps);
1815
2185
  }
1816
2186
  /* v8 ignore stop */
1817
2187
  // ── config models (local, no daemon socket needed) ──
@@ -1849,42 +2219,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
1849
2219
  // ── config model (local, no daemon socket needed) ──
1850
2220
  /* v8 ignore start -- config model: tested via daemon-cli.test.ts @preserve */
1851
2221
  if (command.kind === "config.model") {
1852
- const facing = command.facing ?? "human";
1853
- // Validate model availability for github-copilot before writing
1854
- const { config } = (0, auth_flow_1.readAgentConfigForAgent)(command.agent, deps.bundlesRoot);
1855
- const facingConfig = facing === "human" ? config.humanFacing : config.agentFacing;
1856
- if (facingConfig.provider === "github-copilot") {
1857
- const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent, { secretsRoot: deps.secretsRoot });
1858
- const ghConfig = secrets.providers["github-copilot"];
1859
- if (ghConfig.githubToken && ghConfig.baseUrl) {
1860
- const fetchFn = deps.fetchImpl ?? fetch;
1861
- try {
1862
- const models = await listGithubCopilotModels(ghConfig.baseUrl, ghConfig.githubToken, fetchFn);
1863
- const available = models.map((m) => m.id);
1864
- if (available.length > 0 && !available.includes(command.modelName)) {
1865
- const message = `model '${command.modelName}' not found. available models:\n${available.map((id) => ` ${id}`).join("\n")}`;
1866
- deps.writeStdout(message);
1867
- return message;
1868
- }
1869
- }
1870
- catch {
1871
- // Catalog validation failed — fall through to ping test
1872
- }
1873
- // Ping test: verify the model actually works before switching
1874
- const pingResult = await (0, provider_ping_1.pingGithubCopilotModel)(ghConfig.baseUrl, ghConfig.githubToken, command.modelName, fetchFn);
1875
- if (!pingResult.ok) {
1876
- const message = `model '${command.modelName}' ping failed: ${pingResult.error}\nrun \`ouro config models --agent ${command.agent}\` to see available models.`;
1877
- deps.writeStdout(message);
1878
- return message;
1879
- }
1880
- }
1881
- }
1882
- const { provider, previousModel } = (0, auth_flow_1.writeAgentModel)(command.agent, facing, command.modelName, { bundlesRoot: deps.bundlesRoot });
1883
- const message = previousModel
1884
- ? `updated ${command.agent} model on ${provider}: ${previousModel} → ${command.modelName}`
1885
- : `set ${command.agent} model on ${provider}: ${command.modelName}`;
1886
- deps.writeStdout(message);
1887
- return message;
2222
+ return executeLegacyConfigModel(command, deps);
1888
2223
  }
1889
2224
  /* v8 ignore stop */
1890
2225
  // ── whoami (local, no daemon socket needed) ──
@@ -8,6 +8,7 @@
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.extractAgentFlag = extractAgentFlag;
10
10
  exports.extractFacingFlag = extractFacingFlag;
11
+ exports.facingToProviderLane = facingToProviderLane;
11
12
  exports.isAgentProvider = isAgentProvider;
12
13
  exports.usage = usage;
13
14
  exports.parseMcpServeCommand = parseMcpServeCommand;
@@ -34,6 +35,23 @@ function extractFacingFlag(args) {
34
35
  const rest = [...args.slice(0, idx), ...args.slice(idx + 2)];
35
36
  return { facing: value, rest };
36
37
  }
38
+ function facingToProviderLane(facing) {
39
+ return facing === "human" ? "outward" : "inner";
40
+ }
41
+ function isProviderLane(value) {
42
+ return value === "outward" || value === "inner";
43
+ }
44
+ function extractLaneFlag(args) {
45
+ const idx = args.indexOf("--lane");
46
+ if (idx === -1 || idx + 1 >= args.length)
47
+ return { rest: args };
48
+ const value = args[idx + 1];
49
+ if (!isProviderLane(value)) {
50
+ throw new Error("--lane must be 'outward' or 'inner'");
51
+ }
52
+ const rest = [...args.slice(0, idx), ...args.slice(idx + 2)];
53
+ return { lane: value, rest };
54
+ }
37
55
  function isAgentProvider(value) {
38
56
  return value === "azure" || value === "anthropic" || value === "minimax" || value === "openai-codex" || value === "github-copilot";
39
57
  }
@@ -43,6 +61,9 @@ function usage() {
43
61
  " ouro [up] [--no-repair]",
44
62
  " ouro dev [--repo-path <path>] [--clone [--clone-path <path>]]",
45
63
  " ouro stop|down|status|logs|hatch",
64
+ " ouro status --agent <name>",
65
+ " ouro use --agent <name> --lane outward|inner --provider <provider> --model <model> [--force]",
66
+ " ouro check --agent <name> --lane outward|inner",
46
67
  " ouro outlook [--json]",
47
68
  " ouro -v|--version",
48
69
  " ouro config model --agent <name> <model-name>",
@@ -403,6 +424,63 @@ function parseAuthCommand(args) {
403
424
  }
404
425
  return provider ? { kind: "auth.run", agent, provider } : { kind: "auth.run", agent };
405
426
  }
427
+ function parseProviderUseCommand(args) {
428
+ const { agent, rest: afterAgent } = extractAgentFlag(args);
429
+ const { facing, rest: afterFacing } = extractFacingFlag(afterAgent);
430
+ const { lane, rest } = extractLaneFlag(afterFacing);
431
+ let provider;
432
+ let model;
433
+ let force = false;
434
+ for (let i = 0; i < rest.length; i += 1) {
435
+ const token = rest[i];
436
+ if (token === "--provider") {
437
+ const value = rest[i + 1];
438
+ if (!isAgentProvider(value))
439
+ throw new Error(`Usage: ouro use --agent <name> --lane outward|inner --provider <provider> --model <model>`);
440
+ provider = value;
441
+ i += 1;
442
+ continue;
443
+ }
444
+ if (token === "--model") {
445
+ model = rest[i + 1];
446
+ i += 1;
447
+ continue;
448
+ }
449
+ if (token === "--force") {
450
+ force = true;
451
+ continue;
452
+ }
453
+ throw new Error("Usage: ouro use --agent <name> --lane outward|inner --provider <provider> --model <model> [--force]");
454
+ }
455
+ const resolvedLane = lane ?? (facing ? facingToProviderLane(facing) : undefined);
456
+ if (!agent || !resolvedLane || !provider || !model) {
457
+ throw new Error("Usage: ouro use --agent <name> --lane outward|inner --provider <provider> --model <model> [--force]");
458
+ }
459
+ return {
460
+ kind: "provider.use",
461
+ agent,
462
+ lane: resolvedLane,
463
+ provider,
464
+ model,
465
+ ...(force ? { force: true } : {}),
466
+ ...(facing ? { legacyFacing: facing } : {}),
467
+ };
468
+ }
469
+ function parseProviderCheckCommand(args) {
470
+ const { agent, rest: afterAgent } = extractAgentFlag(args);
471
+ const { facing, rest: afterFacing } = extractFacingFlag(afterAgent);
472
+ const { lane, rest } = extractLaneFlag(afterFacing);
473
+ const resolvedLane = lane ?? (facing ? facingToProviderLane(facing) : undefined);
474
+ if (!agent || !resolvedLane || rest.length > 0) {
475
+ throw new Error("Usage: ouro check --agent <name> --lane outward|inner");
476
+ }
477
+ return {
478
+ kind: "provider.check",
479
+ agent,
480
+ lane: resolvedLane,
481
+ ...(facing ? { legacyFacing: facing } : {}),
482
+ };
483
+ }
406
484
  function parseReminderCommand(args) {
407
485
  const { agent, rest: cleaned } = extractAgentFlag(args);
408
486
  const [sub, ...rest] = cleaned;
@@ -750,8 +828,19 @@ function parseOuroCommand(args) {
750
828
  return { kind: "versions" };
751
829
  if (head === "stop" || head === "down")
752
830
  return { kind: "daemon.stop" };
753
- if (head === "status")
831
+ if (head === "status") {
832
+ const { agent, rest } = extractAgentFlag(args.slice(1));
833
+ if (agent) {
834
+ if (rest.length > 0)
835
+ throw new Error("Usage: ouro status --agent <name>");
836
+ return { kind: "provider.status", agent };
837
+ }
754
838
  return { kind: "daemon.status" };
839
+ }
840
+ if (head === "use")
841
+ return parseProviderUseCommand(args.slice(1));
842
+ if (head === "check")
843
+ return parseProviderCheckCommand(args.slice(1));
755
844
  if (head === "logs") {
756
845
  if (second === "prune")
757
846
  return { kind: "daemon.logs.prune" };