@ouro.bot/cli 0.1.0-alpha.409 → 0.1.0-alpha.410

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/README.md CHANGED
@@ -11,10 +11,11 @@ Ouroboros is a TypeScript harness for daemon-managed agents that live in externa
11
11
  - `ouro up` starts the daemon from the installed production version, syncs the launcher, installs workflow helpers, and reconciles stale runtime state.
12
12
  - `ouro dev` starts the daemon from a local repo build. It auto-builds from source, disables launchd auto-restart (so the installed daemon doesn't respawn underneath you), persists the repo path in `~/.ouro-cli/dev-config.json` for next time, and force-restarts the daemon. If you run `ouro dev` from inside the repo, it detects the CWD automatically. Run `ouro up` to return to production mode (this also cleans up `dev-config.json`).
13
13
  - Agent bundles live outside the repo at `~/AgentBundles/<agent>.ouro/`.
14
- - Credentials live in the owning agent's Bitwarden/Vaultwarden vault. Provider credentials use `providers/<provider>`, runtime/sense/integration credentials use `runtime/config`, and travel/tool credentials use ordinary vault credential items.
14
+ - Credentials live in the owning agent's Bitwarden/Vaultwarden vault: the agent's password manager. Provider credentials use `providers/<provider>`, portable runtime/integration credentials use `runtime/config`, local attachments use `runtime/machines/<machine-id>/config`, and travel/tool credentials use ordinary vault credential items.
15
15
  - Vault coordinates and local runtime state live in the agent bundle; raw credentials do not.
16
16
  - The only Ouro-owned durable credential locations are the bundle and the agent vault. Local unlock material is a machine-local cache, not a credential source of truth.
17
- - Machine-scoped test and runtime spillover lives under `~/.agentstate/...`.
17
+ - Creating or replacing a vault asks for the unlock secret twice without echoing it, and requires at least 8 characters with uppercase and lowercase letters, one number, and one special character.
18
+ - Machine-scoped harness state lives under `~/.ouro-cli/...`; agent-owned runtime/session/log/PII state lives under the bundle.
18
19
 
19
20
  Current first-class senses:
20
21
 
@@ -96,7 +97,9 @@ Task docs do not live in this repo anymore. Planning and doing docs live in the
96
97
  - `state/providers.json` is the local source of truth for provider+model selection on this machine. It has two lanes: `outward` for CLI, Teams, and BlueBubbles turns, and `inner` for inner dialogue.
97
98
  - Each agent has one credential vault for provider, runtime, sense, integration, travel, and tool credentials. There is no machine-wide credential pool.
98
99
  - Vault unlock material is local machine state. Prefer macOS Keychain, Windows DPAPI, or Linux Secret Service; plaintext fallback is allowed only by explicit human choice.
100
+ - New vault unlock secrets are confirmed before use and rejected if they do not meet the minimum strength requirements.
99
101
  - Provider and runtime credentials are loaded into process memory at startup/auth/unlock/refresh and reused. The remote vault is not queried for every model or sense request.
102
+ - CLI commands that mutate bundle config, such as vault setup or `ouro connect bluebubbles`, run bundle sync after the change when `sync.enabled` is true and report a compact `bundle sync:` line.
100
103
  - The daemon discovers bundles dynamically from `~/AgentBundles`.
101
104
  - `ouro status` reports version, last-updated time, discovered agents, senses, and workers.
102
105
  - `bundle-meta.json` tracks the runtime version that last touched a bundle.
@@ -104,6 +107,7 @@ Task docs do not live in this repo anymore. Planning and doing docs live in the
104
107
  - Sense availability is explicit:
105
108
  - `interactive`
106
109
  - `disabled`
110
+ - `not_attached`
107
111
  - `needs_config`
108
112
  - `ready`
109
113
  - `running`
@@ -171,8 +175,11 @@ ouro logs
171
175
  ouro stop
172
176
  ouro vault unlock --agent <name>
173
177
  ouro vault status --agent <name>
174
- ouro vault config set --agent <name> --key bluebubbles.password
175
- ouro vault config status --agent <name>
178
+ ouro vault config set --agent <name> --key teams.clientSecret
179
+ ouro vault config status --agent <name> --scope all
180
+ ouro connect --agent <name>
181
+ ouro connect perplexity --agent <name>
182
+ ouro connect bluebubbles --agent <name>
176
183
  ouro auth --agent <name>
177
184
  ouro auth --agent <name> --provider <provider>
178
185
  ouro auth verify --agent <name> [--provider <provider>]
package/changelog.json CHANGED
@@ -1,6 +1,18 @@
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.410",
6
+ "changes": [
7
+ "Added guided `ouro connect` onboarding for Perplexity search and local BlueBubbles attachment setup, with hidden secret prompts and compact success output.",
8
+ "Runtime credentials now distinguish portable `runtime/config` from machine-scoped `runtime/machines/<machine-id>/config`, so BlueBubbles local Mac bridge settings do not travel as universal agent config.",
9
+ "BlueBubbles can now be enabled but `not_attached` on a machine without degrading the agent, while incomplete or broken local attachment config still reports repairable guidance.",
10
+ "Sync-enabled bundle config mutations now run the existing post-change bundle sync path after vault setup/recovery/replacement, BlueBubbles attachment, and clone success, surfacing a compact `bundle sync:` result.",
11
+ "New vault unlock secret flows now require hidden confirmation and minimum strength checks before creating, replacing, recovering, or hatching an agent vault.",
12
+ "Updated auth/provider, cross-machine, README, AGENTS, and testing docs to lock the agent-vault-as-password-manager model and the portable-vs-local credential split.",
13
+ "`@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the runtime credential onboarding release."
14
+ ]
15
+ },
4
16
  {
5
17
  "version": "0.1.0-alpha.409",
6
18
  "changes": [
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.loadConfig = loadConfig;
37
37
  exports.resetConfigCache = resetConfigCache;
38
38
  exports.cacheRuntimeConfigForTests = cacheRuntimeConfigForTests;
39
+ exports.cacheMachineRuntimeConfigForTests = cacheMachineRuntimeConfigForTests;
39
40
  exports.patchRuntimeConfig = patchRuntimeConfig;
40
41
  exports.getAzureConfig = getAzureConfig;
41
42
  exports.getMinimaxConfig = getMinimaxConfig;
@@ -142,12 +143,16 @@ function localRuntimeFields(config) {
142
143
  return localFields;
143
144
  }
144
145
  function loadConfig() {
145
- const runtimeResult = (0, runtime_credentials_1.readRuntimeCredentialConfig)((0, identity_1.getAgentName)());
146
+ const agentName = (0, identity_1.getAgentName)();
147
+ const runtimeResult = (0, runtime_credentials_1.readRuntimeCredentialConfig)(agentName);
148
+ const machineRuntimeResult = (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agentName);
146
149
  const vaultData = runtimeResult.ok ? localRuntimeFields(runtimeResult.config) : {};
150
+ const machineVaultData = machineRuntimeResult.ok ? localRuntimeFields(machineRuntimeResult.config) : {};
147
151
  const mergedConfig = deepMerge(defaultRuntimeConfig(), vaultData);
152
+ const mergedWithMachineConfig = deepMerge(mergedConfig, machineVaultData);
148
153
  const config = _runtimeConfigOverride
149
- ? deepMerge(mergedConfig, _runtimeConfigOverride)
150
- : mergedConfig;
154
+ ? deepMerge(mergedWithMachineConfig, _runtimeConfigOverride)
155
+ : mergedWithMachineConfig;
151
156
  (0, runtime_1.emitNervesEvent)({
152
157
  event: "config.load",
153
158
  component: "config/identity",
@@ -155,6 +160,7 @@ function loadConfig() {
155
160
  meta: {
156
161
  source: runtimeResult.ok ? "vault-cache" : "defaults",
157
162
  used_defaults_only: !runtimeResult.ok,
163
+ machine_runtime_credentials: machineRuntimeResult.ok ? "available" : machineRuntimeResult.reason,
158
164
  override_applied: _runtimeConfigOverride !== null,
159
165
  runtime_credentials: runtimeResult.ok ? "available" : runtimeResult.reason,
160
166
  },
@@ -171,6 +177,9 @@ function resetConfigCache() {
171
177
  function cacheRuntimeConfigForTests(agentName, config) {
172
178
  (0, runtime_credentials_1.cacheRuntimeCredentialConfig)(agentName, config, new Date(0));
173
179
  }
180
+ function cacheMachineRuntimeConfigForTests(agentName, config) {
181
+ (0, runtime_credentials_1.cacheMachineRuntimeCredentialConfig)(agentName, config, new Date(0), "machine_test");
182
+ }
174
183
  function seedProviderCredentialCache(providers) {
175
184
  if (!providers)
176
185
  return;
@@ -277,10 +286,10 @@ function getBlueBubblesConfig() {
277
286
  const config = loadConfig();
278
287
  const { serverUrl, password, accountId } = config.bluebubbles;
279
288
  if (!serverUrl.trim()) {
280
- throw new Error("bluebubbles.serverUrl is required in the agent vault runtime/config item to run the BlueBubbles sense.");
289
+ throw new Error("bluebubbles.serverUrl is required in this machine's agent-vault runtime config. Run `ouro connect bluebubbles --agent <agent>`.");
281
290
  }
282
291
  if (!password.trim()) {
283
- throw new Error("bluebubbles.password is required in the agent vault runtime/config item to run the BlueBubbles sense.");
292
+ throw new Error("bluebubbles.password is required in this machine's agent-vault runtime config. Run `ouro connect bluebubbles --agent <agent>`.");
284
293
  }
285
294
  return {
286
295
  serverUrl: serverUrl.trim(),
@@ -46,7 +46,7 @@ function formatBlueBubblesHealthcheckFailure(serverUrlInput, error) {
46
46
  case "network-error":
47
47
  return `Cannot reach BlueBubbles at ${serverUrl}. Check \`bluebubbles.serverUrl\`, confirm the BlueBubbles app/API is running, and verify this machine can reach it. Raw error: ${rawReason}`;
48
48
  case "auth-failure":
49
- return `BlueBubbles auth failed at ${serverUrl} (HTTP ${status}). Check \`bluebubbles.password\` in the agent vault runtime/config item and confirm the server accepts it. Raw error: ${rawReason}`;
49
+ return `BlueBubbles auth failed at ${serverUrl} (HTTP ${status}). Check this machine's BlueBubbles attachment with \`ouro connect bluebubbles --agent <agent>\` and confirm the server accepts the password. Raw error: ${rawReason}`;
50
50
  case "server-error":
51
51
  return `BlueBubbles upstream returned HTTP ${status} at ${serverUrl}. Check the BlueBubbles app/server logs and confirm the upstream API is healthy. Raw error: ${rawReason}`;
52
52
  default:
@@ -75,6 +75,7 @@ const provider_state_1 = require("../provider-state");
75
75
  const machine_identity_1 = require("../machine-identity");
76
76
  const provider_models_1 = require("../provider-models");
77
77
  const ouro_version_manager_1 = require("../versioning/ouro-version-manager");
78
+ const sync_1 = require("../sync");
78
79
  const cli_parse_1 = require("./cli-parse");
79
80
  const cli_parse_2 = require("./cli-parse");
80
81
  const cli_help_1 = require("./cli-help");
@@ -611,6 +612,35 @@ function providerCliAgentRoot(command, deps) {
611
612
  function providerCliNow(deps) {
612
613
  return new Date((deps.now ?? Date.now)());
613
614
  }
615
+ function readAgentSyncConfigForCliMutation(agent, deps) {
616
+ try {
617
+ const configPath = path.join(providerCliAgentRoot({ agent }, deps), "agent.json");
618
+ const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
619
+ return {
620
+ enabled: parsed.sync?.enabled === true,
621
+ remote: typeof parsed.sync?.remote === "string" && parsed.sync.remote.trim() ? parsed.sync.remote : "origin",
622
+ };
623
+ }
624
+ catch {
625
+ /* v8 ignore next -- defensive: post-mutation sync should not break an already-successful CLI repair if agent.json becomes unreadable @preserve */
626
+ return { enabled: false, remote: "origin" };
627
+ }
628
+ }
629
+ function pushAgentBundleAfterCliMutation(agent, deps) {
630
+ const sync = readAgentSyncConfigForCliMutation(agent, deps);
631
+ if (!sync.enabled)
632
+ return null;
633
+ const agentRoot = providerCliAgentRoot({ agent }, deps);
634
+ const result = (0, sync_1.postTurnPush)(agentRoot, { enabled: true, remote: sync.remote });
635
+ if (result.ok) {
636
+ return `bundle sync: ran post-change sync (remote: ${sync.remote})`;
637
+ }
638
+ return `bundle sync: could not push bundle changes (${result.error})`;
639
+ }
640
+ function appendBundleSyncSummary(message, agent, deps) {
641
+ const syncSummary = pushAgentBundleAfterCliMutation(agent, deps);
642
+ return syncSummary ? `${message}\n${syncSummary}` : message;
643
+ }
614
644
  function writeAgentVaultConfig(agentName, configPath, config, vault) {
615
645
  const nextConfig = {
616
646
  ...config,
@@ -674,6 +704,7 @@ function recoverProviderImports(raw) {
674
704
  return imports;
675
705
  }
676
706
  const RECOVER_RUNTIME_EXCLUDED_TOP_LEVEL = new Set(["providers", "vault", "context", "schemaVersion", "updatedAt"]);
707
+ const MACHINE_RUNTIME_CONFIG_TOP_LEVEL = new Set(["bluebubbles", "bluebubblesChannel"]);
677
708
  function recoverRuntimeConfig(raw) {
678
709
  const config = {};
679
710
  for (const [key, value] of Object.entries(raw)) {
@@ -695,6 +726,15 @@ function mergeRuntimeConfig(a, b) {
695
726
  }
696
727
  return merged;
697
728
  }
729
+ function splitRuntimeConfigByScope(config) {
730
+ const agentConfig = {};
731
+ const machineConfig = {};
732
+ for (const [key, value] of Object.entries(config)) {
733
+ const target = MACHINE_RUNTIME_CONFIG_TOP_LEVEL.has(key) ? machineConfig : agentConfig;
734
+ target[key] = isJsonRecord(value) ? cloneJsonRecord(value) : value;
735
+ }
736
+ return { agentConfig, machineConfig };
737
+ }
698
738
  function readVaultRecoverSource(sourcePath) {
699
739
  const resolved = path.resolve(sourcePath);
700
740
  let parsed;
@@ -791,10 +831,12 @@ async function executeVaultCreate(command, deps) {
791
831
  const email = command.email ?? config.vault?.email ?? configuredVault.email;
792
832
  const promptSecret = ensureVaultSecretPrompt(deps.promptSecret, "create");
793
833
  const serverUrl = command.serverUrl ?? config.vault?.serverUrl ?? configuredVault.serverUrl;
794
- const unlockSecret = (await promptSecret(`Choose Ouro vault unlock secret for ${email}: `)).trim();
795
- if (!unlockSecret) {
796
- throw new Error("vault create requires an unlock secret. Re-run in an interactive terminal and enter a human-chosen unlock secret.");
797
- }
834
+ const unlockSecret = await (0, vault_unlock_1.promptConfirmedVaultUnlockSecret)({
835
+ promptSecret,
836
+ question: `Choose Ouro vault unlock secret for ${email}: `,
837
+ confirmQuestion: `Confirm Ouro vault unlock secret for ${email}: `,
838
+ emptyError: "vault create requires an unlock secret. Re-run in an interactive terminal and enter a human-chosen unlock secret.",
839
+ });
798
840
  const result = await (0, vault_setup_1.createVaultAccount)("Ouro credential vault", serverUrl, email, unlockSecret);
799
841
  if (!result.success) {
800
842
  const message = [
@@ -814,13 +856,13 @@ async function executeVaultCreate(command, deps) {
814
856
  }, unlockSecret, { homeDir: deps.homeDir, store: command.store });
815
857
  (0, credential_access_1.resetCredentialStore)();
816
858
  await (0, credential_access_1.getCredentialStore)(command.agent).get("__ouro_vault_probe__");
817
- const message = [
859
+ const message = appendBundleSyncSummary([
818
860
  `vault created for ${command.agent}`,
819
861
  `vault: ${email} at ${serverUrl}`,
820
862
  `local unlock store: ${store.kind}${store.secure ? "" : " (explicit plaintext fallback)"}`,
821
863
  "All raw credentials for this agent will be stored in this Ouro credential vault.",
822
864
  "Keep the vault unlock secret saved outside Ouro. Another machine will need it once.",
823
- ].join("\n");
865
+ ].join("\n"), command.agent, deps);
824
866
  deps.writeStdout(message);
825
867
  return message;
826
868
  }
@@ -835,10 +877,12 @@ async function executeVaultReplace(command, deps) {
835
877
  const configuredVault = (0, identity_1.resolveVaultConfig)(command.agent, config.vault);
836
878
  const email = command.email ?? defaultRepairVaultEmail(command.agent, config);
837
879
  const serverUrl = command.serverUrl ?? config.vault?.serverUrl ?? configuredVault.serverUrl;
838
- const unlockSecret = (await promptSecret(`Choose new Ouro vault unlock secret for ${email}: `)).trim();
839
- if (!unlockSecret) {
840
- throw new Error("vault replace requires an unlock secret. Re-run in an interactive terminal and enter a human-chosen unlock secret.");
841
- }
880
+ const unlockSecret = await (0, vault_unlock_1.promptConfirmedVaultUnlockSecret)({
881
+ promptSecret,
882
+ question: `Choose new Ouro vault unlock secret for ${email}: `,
883
+ confirmQuestion: `Confirm new Ouro vault unlock secret for ${email}: `,
884
+ emptyError: "vault replace requires an unlock secret. Re-run in an interactive terminal and enter a human-chosen unlock secret.",
885
+ });
842
886
  const repair = await createRepairVaultForAgent({
843
887
  action: "replace",
844
888
  agentName: command.agent,
@@ -852,14 +896,14 @@ async function executeVaultReplace(command, deps) {
852
896
  });
853
897
  if (!repair.ok)
854
898
  return repair.message;
855
- const message = [
899
+ const message = appendBundleSyncSummary([
856
900
  `vault replaced for ${command.agent}`,
857
901
  `vault: ${email} at ${serverUrl}`,
858
902
  `local unlock store: ${repair.store.kind}${repair.store.secure ? "" : " (explicit plaintext fallback)"}`,
859
903
  "imported: none",
860
904
  `next: ouro repair --agent ${command.agent}`,
861
905
  "Keep the vault unlock secret saved outside Ouro. Another machine will need it once.",
862
- ].join("\n");
906
+ ].join("\n"), command.agent, deps);
863
907
  deps.writeStdout(message);
864
908
  return message;
865
909
  }
@@ -876,10 +920,12 @@ async function executeVaultRecover(command, deps) {
876
920
  const configuredVault = (0, identity_1.resolveVaultConfig)(command.agent, config.vault);
877
921
  const email = command.email ?? defaultRepairVaultEmail(command.agent, config);
878
922
  const serverUrl = command.serverUrl ?? config.vault?.serverUrl ?? configuredVault.serverUrl;
879
- const unlockSecret = (await promptSecret(`Choose new Ouro vault unlock secret for ${email}: `)).trim();
880
- if (!unlockSecret) {
881
- throw new Error("vault recover requires an unlock secret. Re-run in an interactive terminal and enter a human-chosen unlock secret.");
882
- }
923
+ const unlockSecret = await (0, vault_unlock_1.promptConfirmedVaultUnlockSecret)({
924
+ promptSecret,
925
+ question: `Choose new Ouro vault unlock secret for ${email}: `,
926
+ confirmQuestion: `Confirm new Ouro vault unlock secret for ${email}: `,
927
+ emptyError: "vault recover requires an unlock secret. Re-run in an interactive terminal and enter a human-chosen unlock secret.",
928
+ });
883
929
  const repair = await createRepairVaultForAgent({
884
930
  action: "recover",
885
931
  agentName: command.agent,
@@ -894,7 +940,7 @@ async function executeVaultRecover(command, deps) {
894
940
  if (!repair.ok)
895
941
  return repair.message;
896
942
  const importedProviders = new Set();
897
- let mergedRuntimeConfig = {};
943
+ let mergedRecoveredRuntimeConfig = {};
898
944
  for (const source of sourceImports) {
899
945
  for (const provider of source.providers) {
900
946
  await (0, provider_credentials_1.upsertProviderCredential)({
@@ -907,23 +953,29 @@ async function executeVaultRecover(command, deps) {
907
953
  });
908
954
  importedProviders.add(provider.provider);
909
955
  }
910
- mergedRuntimeConfig = mergeRuntimeConfig(mergedRuntimeConfig, source.runtimeConfig);
956
+ mergedRecoveredRuntimeConfig = mergeRuntimeConfig(mergedRecoveredRuntimeConfig, source.runtimeConfig);
911
957
  }
958
+ const { agentConfig: mergedRuntimeConfig, machineConfig: mergedMachineRuntimeConfig } = splitRuntimeConfigByScope(mergedRecoveredRuntimeConfig);
912
959
  const runtimeFields = summarizeRuntimeConfigFields(mergedRuntimeConfig);
960
+ const machineRuntimeFields = summarizeRuntimeConfigFields(mergedMachineRuntimeConfig);
913
961
  if (runtimeFields.length > 0) {
914
962
  await (0, runtime_credentials_1.upsertRuntimeCredentialConfig)(command.agent, mergedRuntimeConfig, now);
915
963
  }
964
+ if (machineRuntimeFields.length > 0) {
965
+ await (0, runtime_credentials_1.upsertMachineRuntimeCredentialConfig)(command.agent, currentMachineId(deps), mergedMachineRuntimeConfig, now);
966
+ }
916
967
  const providerList = [...importedProviders].sort();
917
- const message = [
968
+ const message = appendBundleSyncSummary([
918
969
  `vault recovered for ${command.agent}`,
919
970
  `vault: ${email} at ${serverUrl}`,
920
971
  `local unlock store: ${repair.store.kind}${repair.store.secure ? "" : " (explicit plaintext fallback)"}`,
921
972
  `sources imported: ${sourceImports.length}`,
922
973
  `provider credentials imported: ${providerList.length === 0 ? "none" : providerList.join(", ")}`,
923
974
  `runtime credentials imported: ${runtimeFields.length === 0 ? "none" : runtimeFields.join(", ")}`,
975
+ `machine runtime credentials imported: ${machineRuntimeFields.length === 0 ? "none" : machineRuntimeFields.join(", ")}`,
924
976
  "credential values were not printed",
925
977
  "Keep the vault unlock secret saved outside Ouro. Another machine will need it once.",
926
- ].join("\n");
978
+ ].join("\n"), command.agent, deps);
927
979
  deps.writeStdout(message);
928
980
  return message;
929
981
  }
@@ -1033,24 +1085,65 @@ function summarizeRuntimeConfigFields(config) {
1033
1085
  visit("", config);
1034
1086
  return fields.filter(Boolean).sort();
1035
1087
  }
1088
+ function isSensitiveRuntimeConfigKey(key) {
1089
+ const lastSegment = key.split(".").pop();
1090
+ return /(password|secret|token|api[-_]?key|key)$/i.test(lastSegment);
1091
+ }
1092
+ function currentMachineId(deps) {
1093
+ return (0, machine_identity_1.loadOrCreateMachineIdentity)({
1094
+ homeDir: providerCliHomeDir(deps),
1095
+ now: () => providerCliNow(deps),
1096
+ }).machineId;
1097
+ }
1098
+ async function promptRuntimeConfigValue(command, deps) {
1099
+ if (command.value !== undefined)
1100
+ return command.value;
1101
+ if (isSensitiveRuntimeConfigKey(command.key)) {
1102
+ if (!deps.promptSecret) {
1103
+ throw new Error("secret entry requires an interactive terminal so the value can be hidden. Re-run without --value in a terminal, or pass --value only from a trusted non-logged script.");
1104
+ }
1105
+ return deps.promptSecret(`Value for ${command.key}: `);
1106
+ }
1107
+ const prompt = deps.promptInput;
1108
+ return prompt ? prompt(`Value for ${command.key}: `) : "";
1109
+ }
1110
+ function runtimeScopeLabel(scope) {
1111
+ return scope === "machine" ? "this machine's vault runtime config item" : "the agent vault runtime/config item";
1112
+ }
1113
+ async function storeRuntimeConfigKey(input) {
1114
+ const machineId = input.scope === "machine" ? currentMachineId(input.deps) : undefined;
1115
+ const current = input.scope === "machine"
1116
+ ? await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(input.agent, machineId, { preserveCachedOnFailure: true })
1117
+ : await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(input.agent, { preserveCachedOnFailure: true });
1118
+ if (!current.ok && current.reason !== "missing") {
1119
+ throw new Error(`cannot read existing runtime credentials from ${current.itemPath}: ${current.error}`);
1120
+ }
1121
+ const nextConfig = setRuntimeConfigValue(current.ok ? current.config : {}, input.key, input.value);
1122
+ const stored = input.scope === "machine"
1123
+ ? await (0, runtime_credentials_1.upsertMachineRuntimeCredentialConfig)(input.agent, machineId, nextConfig, providerCliNow(input.deps))
1124
+ : await (0, runtime_credentials_1.upsertRuntimeCredentialConfig)(input.agent, nextConfig, providerCliNow(input.deps));
1125
+ return { revision: stored.revision, itemPath: stored.itemPath, ...(machineId ? { machineId } : {}) };
1126
+ }
1036
1127
  async function executeVaultConfigSet(command, deps) {
1037
1128
  if (command.agent === "SerpentGuide") {
1038
1129
  throw new Error("SerpentGuide does not have persistent runtime credentials. Store credentials in the hatchling agent vault.");
1039
1130
  }
1040
- const prompt = deps.promptInput;
1041
- const value = command.value ?? (prompt ? await prompt(`Value for ${command.key}: `) : "");
1131
+ const scope = command.scope ?? "agent";
1132
+ const value = await promptRuntimeConfigValue(command, deps);
1042
1133
  if (!value) {
1043
1134
  throw new Error("vault config set requires --value <value> or an interactive prompt");
1044
1135
  }
1045
- const current = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(command.agent, { preserveCachedOnFailure: true });
1046
- if (!current.ok && current.reason !== "missing") {
1047
- throw new Error(`cannot read existing runtime credentials from ${current.itemPath}: ${current.error}`);
1048
- }
1049
- const nextConfig = setRuntimeConfigValue(current.ok ? current.config : {}, command.key, value);
1050
- const stored = await (0, runtime_credentials_1.upsertRuntimeCredentialConfig)(command.agent, nextConfig, providerCliNow(deps));
1136
+ const stored = await storeRuntimeConfigKey({
1137
+ agent: command.agent,
1138
+ key: command.key,
1139
+ value,
1140
+ scope,
1141
+ deps,
1142
+ });
1051
1143
  const message = [
1052
- `stored ${command.key} for ${command.agent} in the agent vault runtime/config item`,
1144
+ `stored ${command.key} for ${command.agent} in ${runtimeScopeLabel(scope)}`,
1053
1145
  `runtime credentials: ${stored.revision}`,
1146
+ `item: ${stored.itemPath}`,
1054
1147
  "value was not printed",
1055
1148
  ].join("\n");
1056
1149
  deps.writeStdout(message);
@@ -1062,24 +1155,217 @@ async function executeVaultConfigStatus(command, deps) {
1062
1155
  deps.writeStdout(message);
1063
1156
  return message;
1064
1157
  }
1065
- const runtime = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(command.agent, { preserveCachedOnFailure: true });
1066
- const lines = [`agent: ${command.agent}`, `runtime config item: ${runtime.itemPath}`];
1067
- if (runtime.ok) {
1068
- lines.push(`status: available (${runtime.revision})`);
1069
- const fields = summarizeRuntimeConfigFields(runtime.config);
1070
- lines.push(`fields: ${fields.length === 0 ? "none stored" : fields.join(", ")}`);
1071
- }
1072
- else {
1073
- lines.push(`status: ${runtime.reason}`);
1074
- lines.push(`error: ${runtime.error}`);
1075
- lines.push(runtime.reason === "missing"
1076
- ? `fix: Run 'ouro vault config set --agent ${command.agent} --key <field>' to store runtime credentials.`
1077
- : `fix: ${(0, vault_unlock_1.vaultUnlockReplaceRecoverFix)(command.agent, "Then retry 'ouro vault config status'.")}`);
1158
+ const scopes = command.scope === "all" ? ["agent", "machine"] : [command.scope ?? "agent"];
1159
+ const lines = [`agent: ${command.agent}`];
1160
+ for (const scope of scopes) {
1161
+ const machineId = scope === "machine" ? currentMachineId(deps) : undefined;
1162
+ const runtime = scope === "machine"
1163
+ ? await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(command.agent, machineId, { preserveCachedOnFailure: true })
1164
+ : await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(command.agent, { preserveCachedOnFailure: true });
1165
+ if (scopes.length > 1)
1166
+ lines.push("");
1167
+ lines.push(`${scope} runtime config item: ${runtime.itemPath}`);
1168
+ if (runtime.ok) {
1169
+ lines.push(`status: available (${runtime.revision})`);
1170
+ const fields = summarizeRuntimeConfigFields(runtime.config);
1171
+ lines.push(`fields: ${fields.length === 0 ? "none stored" : fields.join(", ")}`);
1172
+ }
1173
+ else {
1174
+ lines.push(`status: ${runtime.reason}`);
1175
+ lines.push(`error: ${runtime.error}`);
1176
+ lines.push(runtime.reason === "missing"
1177
+ ? `fix: Run 'ouro vault config set --agent ${command.agent} --key <field>${scope === "machine" ? " --scope machine" : ""}' to store runtime credentials.`
1178
+ : `fix: ${(0, vault_unlock_1.vaultUnlockReplaceRecoverFix)(command.agent, "Then retry 'ouro vault config status'.")}`);
1179
+ }
1078
1180
  }
1079
1181
  const message = lines.join("\n");
1080
1182
  deps.writeStdout(message);
1081
1183
  return message;
1082
1184
  }
1185
+ function requirePromptSecret(deps, purpose) {
1186
+ if (deps.promptSecret)
1187
+ return deps.promptSecret;
1188
+ throw new Error(`${purpose} requires an interactive terminal so the secret can be hidden.`);
1189
+ }
1190
+ function requirePromptInput(deps, purpose) {
1191
+ if (deps.promptInput)
1192
+ return deps.promptInput;
1193
+ throw new Error(`${purpose} requires an interactive terminal.`);
1194
+ }
1195
+ function parseOptionalPort(value, fallback, label) {
1196
+ const trimmed = value.trim();
1197
+ if (!trimmed)
1198
+ return fallback;
1199
+ const parsed = Number(trimmed);
1200
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
1201
+ throw new Error(`${label} must be an integer between 1 and 65535`);
1202
+ }
1203
+ return parsed;
1204
+ }
1205
+ function parseOptionalPositiveInteger(value, fallback, label) {
1206
+ const trimmed = value.trim();
1207
+ if (!trimmed)
1208
+ return fallback;
1209
+ const parsed = Number(trimmed);
1210
+ if (!Number.isInteger(parsed) || parsed < 1) {
1211
+ throw new Error(`${label} must be a positive integer`);
1212
+ }
1213
+ return parsed;
1214
+ }
1215
+ function normalizeWebhookPath(value, fallback) {
1216
+ const trimmed = value.trim();
1217
+ if (!trimmed)
1218
+ return fallback;
1219
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
1220
+ }
1221
+ function enableAgentSense(agent, sense, deps) {
1222
+ const { configPath } = (0, auth_flow_1.readAgentConfigForAgent)(agent, deps.bundlesRoot);
1223
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
1224
+ const senses = raw.senses && typeof raw.senses === "object" && !Array.isArray(raw.senses)
1225
+ ? raw.senses
1226
+ : {};
1227
+ const existing = senses[sense] && typeof senses[sense] === "object" && !Array.isArray(senses[sense])
1228
+ ? senses[sense]
1229
+ : {};
1230
+ raw.senses = {
1231
+ ...senses,
1232
+ cli: senses.cli ?? { enabled: true },
1233
+ teams: senses.teams ?? { enabled: false },
1234
+ [sense]: { ...existing, enabled: true },
1235
+ };
1236
+ fs.writeFileSync(configPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
1237
+ }
1238
+ function connectMenu(agent) {
1239
+ return [
1240
+ `Connect ${agent}`,
1241
+ "",
1242
+ "Portable agent capabilities",
1243
+ " 1. Perplexity search",
1244
+ " stores: agent vault runtime/config -> integrations.perplexityApiKey",
1245
+ "",
1246
+ "This machine only",
1247
+ " 2. BlueBubbles iMessage",
1248
+ " stores: agent vault runtime/machines/<this-machine>/config",
1249
+ "",
1250
+ "Model providers",
1251
+ " 3. Provider auth",
1252
+ ` runs separately: ouro auth --agent ${agent} --provider <provider>`,
1253
+ "",
1254
+ " 4. Cancel",
1255
+ "",
1256
+ "Choose [1-4]: ",
1257
+ ].join("\n");
1258
+ }
1259
+ async function executeConnectPerplexity(agent, deps) {
1260
+ if (agent === "SerpentGuide") {
1261
+ throw new Error("SerpentGuide has no persistent runtime credentials. Connect Perplexity on the hatchling agent instead.");
1262
+ }
1263
+ const promptSecret = requirePromptSecret(deps, "Perplexity API key entry");
1264
+ const key = (await promptSecret("Perplexity API key: ")).trim();
1265
+ if (!key)
1266
+ throw new Error("Perplexity API key cannot be blank");
1267
+ const stored = await storeRuntimeConfigKey({
1268
+ agent,
1269
+ key: "integrations.perplexityApiKey",
1270
+ value: key,
1271
+ scope: "agent",
1272
+ deps,
1273
+ });
1274
+ const message = [
1275
+ `Perplexity connected for ${agent}`,
1276
+ "capability: Perplexity search",
1277
+ `stored: ${stored.itemPath}`,
1278
+ "secret was not printed",
1279
+ "",
1280
+ "Next: ask the agent to search, or run `ouro up` again if the daemon was already running.",
1281
+ ].join("\n");
1282
+ deps.writeStdout(message);
1283
+ return message;
1284
+ }
1285
+ async function executeConnectBlueBubbles(agent, deps) {
1286
+ if (agent === "SerpentGuide") {
1287
+ throw new Error("SerpentGuide has no persistent runtime credentials. Attach BlueBubbles on the hatchling agent instead.");
1288
+ }
1289
+ const promptInput = requirePromptInput(deps, "BlueBubbles setup");
1290
+ const promptSecret = requirePromptSecret(deps, "BlueBubbles password entry");
1291
+ const serverUrl = (await promptInput("BlueBubbles server URL for this machine: ")).trim();
1292
+ if (!serverUrl)
1293
+ throw new Error("BlueBubbles server URL cannot be blank");
1294
+ const password = (await promptSecret("BlueBubbles app password: ")).trim();
1295
+ if (!password)
1296
+ throw new Error("BlueBubbles app password cannot be blank");
1297
+ const port = parseOptionalPort(await promptInput("Local webhook port [18790]: "), 18790, "BlueBubbles webhook port");
1298
+ const webhookPath = normalizeWebhookPath(await promptInput("Local webhook path [/bluebubbles-webhook]: "), "/bluebubbles-webhook");
1299
+ const requestTimeoutMs = parseOptionalPositiveInteger(await promptInput("Request timeout ms [30000]: "), 30000, "BlueBubbles request timeout");
1300
+ const machineId = currentMachineId(deps);
1301
+ const current = await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agent, machineId, { preserveCachedOnFailure: true });
1302
+ if (!current.ok && current.reason !== "missing") {
1303
+ throw new Error(`cannot read existing machine runtime credentials from ${current.itemPath}: ${current.error}`);
1304
+ }
1305
+ const nextConfig = {
1306
+ ...(current.ok ? current.config : {}),
1307
+ bluebubbles: {
1308
+ serverUrl,
1309
+ password,
1310
+ accountId: "default",
1311
+ },
1312
+ bluebubblesChannel: {
1313
+ port,
1314
+ webhookPath,
1315
+ requestTimeoutMs,
1316
+ },
1317
+ };
1318
+ const stored = await (0, runtime_credentials_1.upsertMachineRuntimeCredentialConfig)(agent, machineId, nextConfig, providerCliNow(deps));
1319
+ enableAgentSense(agent, "bluebubbles", deps);
1320
+ const message = appendBundleSyncSummary([
1321
+ `BlueBubbles attached for ${agent} on this machine`,
1322
+ `machine: ${machineId}`,
1323
+ `stored: ${stored.itemPath}`,
1324
+ "agent.json: senses.bluebubbles.enabled = true",
1325
+ "secret was not printed",
1326
+ "",
1327
+ "Next: point BlueBubbles at this machine's webhook, then run `ouro up`.",
1328
+ ].join("\n"), agent, deps);
1329
+ deps.writeStdout(message);
1330
+ return message;
1331
+ }
1332
+ async function executeConnect(command, deps) {
1333
+ if (command.target === "perplexity")
1334
+ return executeConnectPerplexity(command.agent, deps);
1335
+ if (command.target === "bluebubbles")
1336
+ return executeConnectBlueBubbles(command.agent, deps);
1337
+ const promptInput = deps.promptInput;
1338
+ if (!promptInput) {
1339
+ const message = [
1340
+ connectMenu(command.agent).replace(/\nChoose \[1-4\]: $/, ""),
1341
+ "",
1342
+ `Run: ouro connect perplexity --agent ${command.agent}`,
1343
+ `Or: ouro connect bluebubbles --agent ${command.agent}`,
1344
+ ].join("\n");
1345
+ deps.writeStdout(message);
1346
+ return message;
1347
+ }
1348
+ const answer = (await promptInput(connectMenu(command.agent))).trim().toLowerCase();
1349
+ if (answer === "1" || answer === "perplexity" || answer === "perplexity-search") {
1350
+ return executeConnectPerplexity(command.agent, deps);
1351
+ }
1352
+ if (answer === "2" || answer === "bluebubbles" || answer === "imessage" || answer === "messages") {
1353
+ return executeConnectBlueBubbles(command.agent, deps);
1354
+ }
1355
+ if (answer === "3" || answer === "provider" || answer === "providers" || answer === "auth") {
1356
+ const message = [
1357
+ "Provider auth is its own flow:",
1358
+ ` ouro auth --agent ${command.agent} --provider <provider>`,
1359
+ "",
1360
+ "Use `ouro connect` for integrations and local senses after provider auth is ready.",
1361
+ ].join("\n");
1362
+ deps.writeStdout(message);
1363
+ return message;
1364
+ }
1365
+ const message = "connect cancelled.";
1366
+ deps.writeStdout(message);
1367
+ return message;
1368
+ }
1083
1369
  function readOrBootstrapProviderState(agentName, deps) {
1084
1370
  const agentRoot = providerCliAgentRoot({ agent: agentName }, deps);
1085
1371
  const readResult = (0, provider_state_1.readProviderState)(agentRoot);
@@ -2130,6 +2416,9 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
2130
2416
  deps.writeStdout(text);
2131
2417
  return text;
2132
2418
  }
2419
+ if (command.kind === "connect") {
2420
+ return executeConnect(command, deps);
2421
+ }
2133
2422
  if (command.kind === "daemon.up") {
2134
2423
  // ── dev mode cleanup: delete dev-config.json so the wrapper stops dispatching to dev repo ──
2135
2424
  /* v8 ignore start -- dev-config cleanup: requires real filesystem state @preserve */
@@ -3577,8 +3866,10 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
3577
3866
  }
3578
3867
  // 8. Output success message
3579
3868
  (0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.clone_complete", message: "clone complete", meta: { agentName, targetPath } });
3869
+ const syncSummary = syncEnabled ? pushAgentBundleAfterCliMutation(agentName, deps) : null;
3580
3870
  const syncMsg = syncEnabled ? "\nsync enabled (remote: origin)" : "\nwarning: no agent.json found — this may not be a valid agent bundle";
3581
- deps.writeStdout(`cloned ${agentName} to ${targetPath}${syncMsg}`);
3871
+ const syncPushMsg = syncSummary ? `\n${syncSummary}` : "";
3872
+ deps.writeStdout(`cloned ${agentName} to ${targetPath}${syncMsg}${syncPushMsg}`);
3582
3873
  // 9. Guided post-clone flow (when interactive)
3583
3874
  if (deps.promptInput) {
3584
3875
  // Auth