@ouro.bot/cli 0.1.0-alpha.237 → 0.1.0-alpha.239

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
@@ -2,7 +2,14 @@
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
4
  {
5
- "version": "0.1.0-alpha.237",
5
+ "version": "0.1.0-alpha.239",
6
+ "changes": [
7
+ "fix(friends): stable local CLI identity \u2014 dropped hostname from external ID (was `username@hostname`, now just `username`). macOS hostname instability (`Mac` vs `Aris-MacBook-Pro.local`) was creating duplicate friend records with separate sessions and trust levels.",
8
+ "feat(friends): migration fallback \u2014 FriendResolver now searches for old `username@*` format IDs when exact match fails, linking new stable ID to existing friend record."
9
+ ]
10
+ },
11
+ {
12
+ "version": "0.1.0-alpha.238",
6
13
  "changes": [
7
14
  "feat: pretty `ouro status` \u2014 ANSI colored output with box-drawing header, status dots, grouped senses/workers by agent. Added git sync info to overview.",
8
15
  "fix: socket half-close \u2014 sendDaemonCommand now calls client.end() after writing, preventing intermittent connection hangs.",
@@ -91,7 +91,7 @@ function writeFriendImprint(bundleRoot, humanName, now) {
91
91
  fs.mkdirSync(friendsDir, { recursive: true });
92
92
  const nowIso = now.toISOString();
93
93
  const id = `friend-${(0, config_1.slugify)(humanName) || "friend"}`;
94
- const localExternalId = `${os.userInfo().username}@${os.hostname()}`;
94
+ const localExternalId = os.userInfo().username;
95
95
  const record = {
96
96
  id,
97
97
  name: humanName,
@@ -31,6 +31,42 @@ class FriendResolver {
31
31
  }
32
32
  if (existing)
33
33
  return existing;
34
+ // Migration: local provider previously used "${username}@${hostname}" format.
35
+ // If no exact match, try finding a friend with old-format external ID.
36
+ /* v8 ignore start -- one-time migration: hostname→username format, tested manually @preserve */
37
+ if (this.params.provider === "local" && !this.params.externalId.includes("@")) {
38
+ try {
39
+ const all = typeof this.store.listAll === "function" ? await this.store.listAll() : [];
40
+ const migrationMatch = all.find((f) => f.externalIds.some((eid) => eid.provider === "local" && eid.externalId.startsWith(this.params.externalId + "@")));
41
+ if (migrationMatch) {
42
+ // Link the new external ID format to the existing friend
43
+ const now = new Date().toISOString();
44
+ migrationMatch.externalIds.push({
45
+ provider: this.params.provider,
46
+ externalId: this.params.externalId,
47
+ linkedAt: now,
48
+ });
49
+ migrationMatch.updatedAt = now;
50
+ try {
51
+ await this.store.put(migrationMatch.id, migrationMatch);
52
+ }
53
+ catch {
54
+ // best-effort persist
55
+ }
56
+ (0, runtime_1.emitNervesEvent)({
57
+ component: "friends",
58
+ event: "friends.local_id_migrated",
59
+ message: `migrated local friend identity from hostname format to username-only`,
60
+ meta: { friendId: migrationMatch.id, newExternalId: this.params.externalId },
61
+ });
62
+ return migrationMatch;
63
+ }
64
+ }
65
+ catch {
66
+ // fall through to create new
67
+ }
68
+ }
69
+ /* v8 ignore stop */
34
70
  // First encounter -- create new FriendRecord
35
71
  const now = new Date().toISOString();
36
72
  const externalId = {
@@ -67,6 +67,8 @@ const DISPATCH_EXEMPT_PATTERNS = [
67
67
  "repertoire/api-client",
68
68
  "repertoire/github-client",
69
69
  "mind/embedding-provider",
70
+ // Commerce utility module: error classes and pure helpers (no independent side effects).
71
+ "repertoire/commerce-errors",
70
72
  ];
71
73
  function isDispatchExempt(filePath) {
72
74
  return DISPATCH_EXEMPT_PATTERNS.some((pattern) => filePath.includes(pattern));
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.BitwardenCredentialStore = void 0;
13
13
  const node_child_process_1 = require("node:child_process");
14
14
  const runtime_1 = require("../nerves/runtime");
15
+ const bw_installer_1 = require("./bw-installer");
15
16
  // ---------------------------------------------------------------------------
16
17
  // bw CLI wrapper
17
18
  // ---------------------------------------------------------------------------
@@ -76,6 +77,8 @@ class BitwardenCredentialStore {
76
77
  * Retries transient failures (network/timeout) up to MAX_RETRIES with exponential backoff.
77
78
  */
78
79
  async login() {
80
+ // Ensure bw CLI is installed before any bw commands
81
+ await (0, bw_installer_1.ensureBwCli)();
79
82
  let lastError;
80
83
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
81
84
  try {
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ /**
3
+ * Lazy bw CLI installer — auto-installs the Bitwarden CLI when not present.
4
+ *
5
+ * Mirrors the whisper-cpp pattern in senses/bluebubbles/media.ts:
6
+ * check PATH first, install via npm if missing, emit nerves event.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.ensureBwCli = ensureBwCli;
10
+ const node_child_process_1 = require("node:child_process");
11
+ const runtime_1 = require("../nerves/runtime");
12
+ const INSTALL_TIMEOUT_MS = 120_000;
13
+ const WHICH_TIMEOUT_MS = 5_000;
14
+ function execFileAsync(cmd, args, timeout) {
15
+ return new Promise((resolve, reject) => {
16
+ (0, node_child_process_1.execFile)(cmd, args, { timeout }, (err, stdout) => {
17
+ if (err) {
18
+ reject(err);
19
+ return;
20
+ }
21
+ resolve(stdout);
22
+ });
23
+ });
24
+ }
25
+ /**
26
+ * Ensure the `bw` CLI is available, installing it via npm if needed.
27
+ * Returns the path to the `bw` binary.
28
+ */
29
+ async function ensureBwCli() {
30
+ // 1. Check if bw is already in PATH
31
+ try {
32
+ const existing = (await execFileAsync("which", ["bw"], WHICH_TIMEOUT_MS)).trim();
33
+ if (existing) {
34
+ return existing;
35
+ }
36
+ }
37
+ catch {
38
+ // Not found — fall through to install
39
+ }
40
+ // 2. Install via npm
41
+ (0, runtime_1.emitNervesEvent)({
42
+ event: "repertoire.bw_cli_install_start",
43
+ component: "repertoire",
44
+ message: "bw CLI not found, installing via npm",
45
+ meta: {},
46
+ });
47
+ try {
48
+ await execFileAsync("npm", ["install", "-g", "@bitwarden/cli"], INSTALL_TIMEOUT_MS);
49
+ }
50
+ catch (err) {
51
+ /* v8 ignore next -- execFileCb always throws Error instances @preserve */
52
+ const reason = err instanceof Error ? err.message : String(err);
53
+ (0, runtime_1.emitNervesEvent)({
54
+ level: "error",
55
+ event: "repertoire.bw_cli_install_fail",
56
+ component: "repertoire",
57
+ message: "failed to install bw CLI via npm",
58
+ meta: { reason },
59
+ });
60
+ throw new Error(`failed to install bw CLI via npm: ${reason}`);
61
+ }
62
+ // 3. Verify installation and return path
63
+ try {
64
+ const installed = (await execFileAsync("which", ["bw"], WHICH_TIMEOUT_MS)).trim();
65
+ if (installed) {
66
+ (0, runtime_1.emitNervesEvent)({
67
+ event: "repertoire.bw_cli_install_end",
68
+ component: "repertoire",
69
+ message: "bw CLI installed successfully",
70
+ meta: { path: installed },
71
+ });
72
+ return installed;
73
+ }
74
+ }
75
+ catch {
76
+ // Fall through to error
77
+ }
78
+ throw new Error("bw CLI installed via npm but binary not found in PATH");
79
+ }
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ /**
3
+ * Commerce-specific error types and helpers.
4
+ *
5
+ * These errors carry structured `code` and `meta` fields for nerves event
6
+ * compatibility and provide patterns for common commerce failure modes:
7
+ * retry, price change detection, and partial failure reporting.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.ProfileError = exports.BookingError = exports.PaymentError = exports.CommerceError = void 0;
11
+ exports.retryOnce = retryOnce;
12
+ exports.priceChangeGuard = priceChangeGuard;
13
+ exports.partialFailureReport = partialFailureReport;
14
+ // ---------------------------------------------------------------------------
15
+ // Error types
16
+ // ---------------------------------------------------------------------------
17
+ class CommerceError extends Error {
18
+ code;
19
+ meta;
20
+ constructor(message, code, meta = {}) {
21
+ super(message);
22
+ this.name = "CommerceError";
23
+ this.code = code;
24
+ this.meta = meta;
25
+ }
26
+ }
27
+ exports.CommerceError = CommerceError;
28
+ class PaymentError extends CommerceError {
29
+ constructor(message, code, meta = {}) {
30
+ super(message, code, meta);
31
+ this.name = "PaymentError";
32
+ }
33
+ }
34
+ exports.PaymentError = PaymentError;
35
+ class BookingError extends CommerceError {
36
+ constructor(message, code, meta = {}) {
37
+ super(message, code, meta);
38
+ this.name = "BookingError";
39
+ }
40
+ }
41
+ exports.BookingError = BookingError;
42
+ class ProfileError extends CommerceError {
43
+ constructor(message, code, meta = {}) {
44
+ super(message, code, meta);
45
+ this.name = "ProfileError";
46
+ }
47
+ }
48
+ exports.ProfileError = ProfileError;
49
+ // ---------------------------------------------------------------------------
50
+ // Transient error detection
51
+ // ---------------------------------------------------------------------------
52
+ const TRANSIENT_CODES = new Set(["COMMERCE_TRANSIENT", "COMMERCE_TIMEOUT", "COMMERCE_NETWORK"]);
53
+ function isTransient(err) {
54
+ return err instanceof CommerceError && TRANSIENT_CODES.has(err.code);
55
+ }
56
+ // ---------------------------------------------------------------------------
57
+ // Helpers
58
+ // ---------------------------------------------------------------------------
59
+ /**
60
+ * Retry a function once on transient failure, then throw.
61
+ * Non-transient errors are thrown immediately without retry.
62
+ */
63
+ async function retryOnce(fn) {
64
+ try {
65
+ return await fn();
66
+ }
67
+ catch (err) {
68
+ if (!isTransient(err))
69
+ throw err;
70
+ // One retry
71
+ return fn();
72
+ }
73
+ }
74
+ /**
75
+ * Throw PaymentError if the actual price differs from the approved price
76
+ * by more than 5%. Used to protect against price changes between search
77
+ * and booking.
78
+ */
79
+ function priceChangeGuard(approved, actual) {
80
+ if (approved === 0 && actual === 0)
81
+ return;
82
+ if (approved === 0) {
83
+ throw new PaymentError(`price changed from $0 to $${actual} — cannot verify percentage change`, "PAYMENT_PRICE_CHANGED", { approved, actual });
84
+ }
85
+ const delta = Math.abs(actual - approved) / approved;
86
+ if (delta > 0.05) {
87
+ throw new PaymentError(`price changed by ${(delta * 100).toFixed(1)}% (approved: $${approved}, actual: $${actual})`, "PAYMENT_PRICE_CHANGED", { approved, actual, deltaPercent: delta * 100 });
88
+ }
89
+ }
90
+ /**
91
+ * Format a human-readable status report for multi-service booking attempts.
92
+ * Each entry has a service name, status, and optional error message.
93
+ */
94
+ function partialFailureReport(results) {
95
+ if (results.length === 0) {
96
+ return "no services attempted.";
97
+ }
98
+ const lines = results.map((r) => {
99
+ const status = r.status === "success" ? "success" : "failed";
100
+ const detail = r.error ? ` — ${r.error}` : "";
101
+ return ` ${r.service}: ${status}${detail}`;
102
+ });
103
+ const succeeded = results.filter((r) => r.status === "success").length;
104
+ const failed = results.length - succeeded;
105
+ return [
106
+ `booking status: ${succeeded} succeeded, ${failed} failed`,
107
+ ...lines,
108
+ ].join("\n");
109
+ }
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ /**
3
+ * Commerce self-test — per-service health checks for the agent's
4
+ * commerce infrastructure (Stripe, Duffel, LiteAPI).
5
+ *
6
+ * Used by the setup wizard to verify configuration and provide
7
+ * actionable error messages when services are misconfigured.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.commerceSelfTest = commerceSelfTest;
11
+ const stripe_client_1 = require("./stripe-client");
12
+ const duffel_client_1 = require("./duffel-client");
13
+ const credential_access_1 = require("./credential-access");
14
+ const runtime_1 = require("../nerves/runtime");
15
+ // ---------------------------------------------------------------------------
16
+ // Per-service tests
17
+ // ---------------------------------------------------------------------------
18
+ async function testStripe() {
19
+ try {
20
+ const client = await (0, stripe_client_1.createStripeClient)();
21
+ // Create a test card and immediately deactivate it
22
+ const card = await client.createVirtualCard({
23
+ type: "single_use",
24
+ spendLimit: 1,
25
+ currency: "usd",
26
+ });
27
+ await client.deactivateCard(card.cardId);
28
+ return { status: "ok", message: "Stripe Issuing working. Test card created and deactivated." };
29
+ }
30
+ catch (err) {
31
+ /* v8 ignore next -- reason @preserve */
32
+ const reason = err instanceof Error ? err.message : String(err);
33
+ if (reason.includes("no credential found") || reason.includes("restrictedKey")) {
34
+ return {
35
+ status: "error",
36
+ message: `Stripe key missing. Add your restricted key at https://dashboard.stripe.com/apikeys and store it in the vault as stripe.com/restrictedKey.`,
37
+ };
38
+ }
39
+ if (reason.includes("401") || reason.includes("Invalid API Key")) {
40
+ return {
41
+ status: "error",
42
+ message: `Stripe key returned 401. Verify it at https://dashboard.stripe.com/apikeys.`,
43
+ };
44
+ }
45
+ return {
46
+ status: "error",
47
+ message: `Stripe error: ${reason}`,
48
+ };
49
+ }
50
+ }
51
+ async function testDuffel() {
52
+ try {
53
+ const client = await (0, duffel_client_1.createDuffelClient)();
54
+ await client.searchFlights({
55
+ origin: "SFO",
56
+ destination: "JFK",
57
+ departureDate: new Date(Date.now() + 30 * 86400000).toISOString().split("T")[0],
58
+ passengers: [{ type: "adult" }],
59
+ });
60
+ return { status: "ok", message: "Duffel Flights working. Test search completed." };
61
+ }
62
+ catch (err) {
63
+ /* v8 ignore next -- reason @preserve */
64
+ const reason = err instanceof Error ? err.message : String(err);
65
+ if (reason.includes("no credential found") || reason.includes("apiKey")) {
66
+ return {
67
+ status: "error",
68
+ message: `Duffel key missing. Add your API key from https://app.duffel.com/tokens and store it in the vault as duffel.com/apiKey.`,
69
+ };
70
+ }
71
+ if (reason.includes("401") || reason.includes("Unauthorized")) {
72
+ return {
73
+ status: "error",
74
+ message: `Your Duffel key returned 401. Verify it at https://app.duffel.com/tokens.`,
75
+ };
76
+ }
77
+ return {
78
+ status: "error",
79
+ message: `Duffel error: ${reason}`,
80
+ };
81
+ }
82
+ }
83
+ async function testLiteApi() {
84
+ try {
85
+ const store = (0, credential_access_1.getCredentialStore)();
86
+ await store.getRawSecret("liteapi.travel", "apiKey");
87
+ // If we can retrieve the key, the config is present.
88
+ // LiteAPI is accessed via MCP, so we can't do a direct API call here.
89
+ // The actual health check happens when the MCP server starts.
90
+ return { status: "ok", message: "LiteAPI key found in vault. MCP server will use vault:liteapi.travel/apiKey." };
91
+ }
92
+ catch (err) {
93
+ /* v8 ignore next -- reason @preserve */
94
+ const reason = err instanceof Error ? err.message : String(err);
95
+ if (reason.includes("no credential found")) {
96
+ return {
97
+ status: "error",
98
+ message: `LiteAPI key missing. Get your API key from https://dashboard.liteapi.travel and store it in the vault as liteapi.travel/apiKey.`,
99
+ };
100
+ }
101
+ return {
102
+ status: "error",
103
+ message: `LiteAPI error: ${reason}`,
104
+ };
105
+ }
106
+ }
107
+ // ---------------------------------------------------------------------------
108
+ // Main self-test
109
+ // ---------------------------------------------------------------------------
110
+ async function commerceSelfTest() {
111
+ (0, runtime_1.emitNervesEvent)({
112
+ component: "repertoire",
113
+ event: "repertoire.commerce_self_test_start",
114
+ message: "starting commerce self-test",
115
+ meta: {},
116
+ });
117
+ const [stripe, duffel, liteapi] = await Promise.all([
118
+ testStripe(),
119
+ testDuffel(),
120
+ testLiteApi(),
121
+ ]);
122
+ const services = { stripe, duffel, liteapi };
123
+ const okCount = Object.values(services).filter((s) => s.status === "ok").length;
124
+ const total = Object.keys(services).length;
125
+ let overall;
126
+ if (okCount === total) {
127
+ overall = "healthy";
128
+ }
129
+ else if (okCount > 0) {
130
+ overall = "partial";
131
+ }
132
+ else {
133
+ overall = "unhealthy";
134
+ }
135
+ const lines = [];
136
+ if (stripe.status === "ok")
137
+ lines.push("Stripe: working");
138
+ else
139
+ lines.push(`Stripe: ${stripe.message}`);
140
+ if (duffel.status === "ok")
141
+ lines.push("Duffel: working");
142
+ else
143
+ lines.push(`Duffel: ${duffel.message}`);
144
+ if (liteapi.status === "ok")
145
+ lines.push("LiteAPI: working");
146
+ else
147
+ lines.push(`LiteAPI: ${liteapi.message}`);
148
+ const summary = `Commerce health: ${okCount}/${total} services ok.\n${lines.join("\n")}`;
149
+ (0, runtime_1.emitNervesEvent)({
150
+ component: "repertoire",
151
+ event: "repertoire.commerce_self_test_end",
152
+ message: "commerce self-test complete",
153
+ meta: { overall, okCount, total },
154
+ });
155
+ return { overall, services, summary };
156
+ }
@@ -45,6 +45,7 @@ var __importStar = (this && this.__importStar) || (function () {
45
45
  Object.defineProperty(exports, "__esModule", { value: true });
46
46
  exports.AacCredentialStore = exports.BuiltInCredentialStore = void 0;
47
47
  exports.domainToSlug = domainToSlug;
48
+ exports.ensureVaultDefaults = ensureVaultDefaults;
48
49
  exports.getCredentialStore = getCredentialStore;
49
50
  exports.resetCredentialStore = resetCredentialStore;
50
51
  const crypto = __importStar(require("node:crypto"));
@@ -415,6 +416,37 @@ exports.AacCredentialStore = AacCredentialStore;
415
416
  // Singleton + auto-detection
416
417
  // ---------------------------------------------------------------------------
417
418
  let _store = null;
419
+ /**
420
+ * Auto-add vault defaults to agent.json when no vault section exists.
421
+ * Returns the vault config (either existing or freshly written defaults).
422
+ */
423
+ function ensureVaultDefaults(agentName, agentRoot, existingVault) {
424
+ if (existingVault) {
425
+ return existingVault;
426
+ }
427
+ const configPath = path.join(agentRoot, "agent.json");
428
+ let raw;
429
+ try {
430
+ raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
431
+ }
432
+ catch {
433
+ // agent.json unreadable — cannot auto-configure
434
+ return undefined;
435
+ }
436
+ const defaults = {
437
+ email: `${agentName}@ouro.bot`,
438
+ serverUrl: "https://vault.ouro.bot",
439
+ };
440
+ raw.vault = defaults;
441
+ fs.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
442
+ (0, runtime_1.emitNervesEvent)({
443
+ event: "repertoire.vault_defaults_auto_configured",
444
+ component: "repertoire",
445
+ message: "auto-configured vault defaults in agent.json",
446
+ meta: { agentName, email: defaults.email, serverUrl: defaults.serverUrl },
447
+ });
448
+ return defaults;
449
+ }
418
450
  /**
419
451
  * Get the credential store singleton.
420
452
  *
@@ -433,16 +465,19 @@ function getCredentialStore() {
433
465
  // Dynamic import to avoid circular dependency at module level
434
466
  const identity = require("../heart/identity");
435
467
  agentName = identity.getAgentName();
468
+ const agentRoot = identity.getAgentRoot(agentName);
436
469
  // Try to load vault config from agent.json + secrets.json
437
470
  const config = identity.loadAgentConfig?.();
438
- const vaultConfig = identity.resolveVaultConfig?.(agentName, config?.vault);
471
+ // Auto-configure vault defaults if missing
472
+ const vaultSection = ensureVaultDefaults(agentName, agentRoot, config?.vault);
473
+ const vaultConfig = identity.resolveVaultConfig?.(agentName, vaultSection);
439
474
  const secretsPath = identity.getAgentSecretsPath?.(agentName);
440
475
  let vaultSecrets;
441
476
  /* v8 ignore start -- requires real agent secrets.json + vault config + bw CLI @preserve */
442
477
  if (secretsPath) {
443
478
  try {
444
- const fs = require("fs");
445
- const secrets = JSON.parse(fs.readFileSync(secretsPath, "utf-8"));
479
+ const fsSync = require("fs");
480
+ const secrets = JSON.parse(fsSync.readFileSync(secretsPath, "utf-8"));
446
481
  vaultSecrets = secrets.vault;
447
482
  }
448
483
  catch {