@ouro.bot/cli 0.1.0-alpha.233 → 0.1.0-alpha.235

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,16 @@
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.235",
6
+ "changes": [
7
+ "fix: MCP tool double-prefix \u2014 tools already prefixed by server name no longer get a redundant second prefix in the unified registry.",
8
+ "feat: Open-Meteo zero-auth weather \u2014 replaced OpenWeatherMap with Open-Meteo forecast + geocoding API. Weather now works without any API key or credential provisioning.",
9
+ "feat: expanded ISO/FIPS divergence table \u2014 21 new entries for correct travel advisory resolution across all major divergent country codes.",
10
+ "fix: BitwardenCredentialStore retry logic \u2014 exponential backoff with configurable retries for transient bw CLI failures, plus bw-not-installed error.",
11
+ "chore: travel MCP packages (Duffel, Expedia) status confirmed GitHub-only, not published to npm."
12
+ ]
13
+ },
4
14
  {
5
15
  "version": "0.1.0-alpha.233",
6
16
  "changes": [
@@ -33,6 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports._senseTurnRetryDelays = exports.SENSE_TURN_RETRY_DELAYS_MS = exports.SENSE_TURN_MAX_RETRIES = void 0;
37
+ exports._setSenseTurnRetryDelays = _setSenseTurnRetryDelays;
36
38
  exports.createMcpServer = createMcpServer;
37
39
  exports.getToolSchemas = getToolSchemas;
38
40
  const socket_client_1 = require("../daemon/socket-client");
@@ -40,6 +42,47 @@ const agentService = __importStar(require("../daemon/agent-service"));
40
42
  const runtime_1 = require("../../nerves/runtime");
41
43
  const session_id_resolver_1 = require("../daemon/session-id-resolver");
42
44
  const pending_1 = require("../../mind/pending");
45
+ exports.SENSE_TURN_MAX_RETRIES = 3;
46
+ exports.SENSE_TURN_RETRY_DELAYS_MS = [1000, 2000, 4000];
47
+ // Allow test override
48
+ exports._senseTurnRetryDelays = exports.SENSE_TURN_RETRY_DELAYS_MS;
49
+ function _setSenseTurnRetryDelays(delays) { exports._senseTurnRetryDelays = delays; }
50
+ /**
51
+ * Send a senseTurn command to the daemon with retry logic.
52
+ * Retries on transient failures: empty response (daemon mid-restart),
53
+ * ECONNREFUSED (daemon not yet listening), ENOENT (socket not yet created).
54
+ */
55
+ async function sendSenseTurnWithRetry(socketPath, command) {
56
+ let lastError = null;
57
+ /* v8 ignore start -- retry loop: functionally tested via mcp-send-message retry tests @preserve */
58
+ for (let attempt = 0; attempt <= exports.SENSE_TURN_MAX_RETRIES; attempt++) {
59
+ try {
60
+ const response = await (0, socket_client_1.sendDaemonCommand)(socketPath, command);
61
+ return response;
62
+ }
63
+ catch (error) {
64
+ lastError = error instanceof Error ? error : new Error(String(error));
65
+ const msg = lastError.message;
66
+ const isTransient = msg.includes("ECONNREFUSED")
67
+ || msg.includes("ENOENT")
68
+ || msg.includes("empty response")
69
+ || msg.includes("Empty response");
70
+ if (!isTransient || attempt >= exports.SENSE_TURN_MAX_RETRIES) {
71
+ throw lastError;
72
+ }
73
+ const delay = exports._senseTurnRetryDelays[attempt] ?? 4000;
74
+ (0, runtime_1.emitNervesEvent)({
75
+ component: "daemon",
76
+ event: "daemon.mcp_sense_turn_retry",
77
+ message: `senseTurn attempt ${attempt + 1} failed, retrying in ${delay}ms`,
78
+ meta: { attempt: attempt + 1, error: msg, delay },
79
+ });
80
+ await new Promise((resolve) => setTimeout(resolve, delay));
81
+ }
82
+ }
83
+ throw lastError ?? new Error("senseTurn failed after retries");
84
+ /* v8 ignore stop */
85
+ }
43
86
  /**
44
87
  * Maps MCP tool names to daemon command kinds.
45
88
  */
@@ -248,7 +291,7 @@ function createMcpServer(options) {
248
291
  const message = toolArgs.message ?? "";
249
292
  /* v8 ignore stop */
250
293
  try {
251
- const response = await (0, socket_client_1.sendDaemonCommand)(socketPath, {
294
+ const response = await sendSenseTurnWithRetry(socketPath, {
252
295
  kind: "agent.senseTurn",
253
296
  agent,
254
297
  friendId,
@@ -322,7 +365,7 @@ function createMcpServer(options) {
322
365
  const context = toolArgs.context;
323
366
  const delegateMessage = context ? `[delegate] ${task}\n\ncontext: ${context}` : `[delegate] ${task}`;
324
367
  try {
325
- const response = await (0, socket_client_1.sendDaemonCommand)(socketPath, {
368
+ const response = await sendSenseTurnWithRetry(socketPath, {
326
369
  kind: "agent.senseTurn",
327
370
  agent,
328
371
  friendId,
@@ -22,6 +22,10 @@ function execBw(args, sessionToken) {
22
22
  return new Promise((resolve, reject) => {
23
23
  (0, node_child_process_1.execFile)("bw", args, { timeout: 30_000, env }, (err, stdout) => {
24
24
  if (err) {
25
+ if (isBwNotInstalled(err)) {
26
+ reject(new Error("bw CLI not found. Install from https://bitwarden.com/help/cli/"));
27
+ return;
28
+ }
25
29
  reject(new Error(`bw CLI error: ${err.message}`));
26
30
  return;
27
31
  }
@@ -29,6 +33,27 @@ function execBw(args, sessionToken) {
29
33
  });
30
34
  });
31
35
  }
36
+ /** Check if the error indicates the bw CLI binary is not installed. */
37
+ function isBwNotInstalled(err) {
38
+ const msg = err.message.toLowerCase();
39
+ const code = err.code;
40
+ return code === "ENOENT" || msg.includes("enoent") || msg.includes("not found") || msg.includes("command not found");
41
+ }
42
+ /** Check if the error is transient (network/timeout) and worth retrying. */
43
+ function isTransientError(err) {
44
+ const msg = err.message.toLowerCase();
45
+ return (msg.includes("econnrefused") ||
46
+ msg.includes("etimedout") ||
47
+ msg.includes("enotfound") ||
48
+ msg.includes("socket hang up") ||
49
+ msg.includes("503") ||
50
+ msg.includes("server unavailable"));
51
+ }
52
+ const MAX_RETRIES = 3;
53
+ const BASE_BACKOFF_MS = 1000;
54
+ function delay(ms) {
55
+ return new Promise((resolve) => setTimeout(resolve, ms));
56
+ }
32
57
  // ---------------------------------------------------------------------------
33
58
  // BitwardenCredentialStore
34
59
  // ---------------------------------------------------------------------------
@@ -48,15 +73,50 @@ class BitwardenCredentialStore {
48
73
  /**
49
74
  * Ensure the bw CLI is authenticated and unlocked.
50
75
  * Handles three states: logged out → login, locked → unlock, already unlocked → no-op.
76
+ * Retries transient failures (network/timeout) up to MAX_RETRIES with exponential backoff.
51
77
  */
52
78
  async login() {
79
+ let lastError;
80
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
81
+ try {
82
+ await this.loginAttempt();
83
+ return;
84
+ }
85
+ catch (err) {
86
+ /* v8 ignore next -- defensive: loginAttempt always throws Error instances @preserve */
87
+ lastError = err instanceof Error ? err : new Error(String(err));
88
+ // Don't retry non-transient errors (auth failures, bw not installed)
89
+ if (!isTransientError(lastError)) {
90
+ throw lastError;
91
+ }
92
+ // Don't retry after final attempt
93
+ if (attempt === MAX_RETRIES - 1)
94
+ break;
95
+ const backoffMs = BASE_BACKOFF_MS * Math.pow(2, attempt);
96
+ (0, runtime_1.emitNervesEvent)({
97
+ event: "repertoire.bw_login_retry",
98
+ component: "repertoire",
99
+ message: `bw login attempt ${attempt + 1} failed, retrying in ${backoffMs}ms`,
100
+ meta: { attempt: attempt + 1, backoffMs, reason: lastError.message },
101
+ });
102
+ await delay(backoffMs);
103
+ }
104
+ }
105
+ throw lastError;
106
+ }
107
+ /** Single login attempt — called by login() retry loop. */
108
+ async loginAttempt() {
53
109
  // Check current status
54
110
  let status = {};
55
111
  try {
56
112
  const raw = await execBw(["status"]);
57
113
  status = JSON.parse(raw);
58
114
  }
59
- catch {
115
+ catch (err) {
116
+ // If bw CLI is not installed or a transient error, propagate it for retry
117
+ if (err instanceof Error && (isBwNotInstalled(err) || isTransientError(err))) {
118
+ throw err;
119
+ }
60
120
  // CLI not configured or broken — proceed with full setup
61
121
  }
62
122
  // Configure server URL if needed (only works when logged out)
@@ -18,7 +18,9 @@ function mcpToolsAsDefinitions(mcpManager) {
18
18
  tool: {
19
19
  type: "function",
20
20
  function: {
21
- name: `${entry.server}_${tool.name}`,
21
+ name: tool.name.startsWith(`${entry.server}_`) || tool.name === entry.server
22
+ ? tool.name
23
+ : `${entry.server}_${tool.name}`,
22
24
  description: tool.description || `MCP tool: ${tool.name} (server: ${entry.server})`,
23
25
  parameters: tool.inputSchema ?? { type: "object", properties: {} },
24
26
  },
@@ -3,12 +3,13 @@
3
3
  * Travel API client module.
4
4
  *
5
5
  * Provides HTTP clients for:
6
- * - OpenWeatherMap (query-param auth via vault)
6
+ * - Open-Meteo weather forecast (no auth, WMO weather codes)
7
7
  * - US State Dept Travel Advisories (no auth)
8
8
  * - Nominatim Geocoding (no auth, requires User-Agent)
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.TRAVEL_ADVISORY_RSS_URL = void 0;
11
+ exports.TRAVEL_ADVISORY_RSS_URL = exports.WMO_WEATHER_CODES = void 0;
12
+ exports.wmoWeatherDescription = wmoWeatherDescription;
12
13
  exports.getWeather = getWeather;
13
14
  exports.getWeatherByCity = getWeatherByCity;
14
15
  exports.isoToCountryName = isoToCountryName;
@@ -20,47 +21,105 @@ exports.geocode = geocode;
20
21
  exports.reverseGeocode = reverseGeocode;
21
22
  exports.searchPOI = searchPOI;
22
23
  const runtime_1 = require("../nerves/runtime");
23
- const credential_access_1 = require("./credential-access");
24
- // --- OpenWeatherMap ---
25
- /** Domain used for weather API key credential retrieval. */
26
- const WEATHER_CREDENTIAL_DOMAIN = "api.openweathermap.org";
27
- async function getWeatherApiKey() {
28
- const store = (0, credential_access_1.getCredentialStore)();
29
- return store.getRawSecret(WEATHER_CREDENTIAL_DOMAIN, "password");
24
+ // --- Open-Meteo Weather (zero auth) ---
25
+ const OPEN_METEO_FORECAST = "https://api.open-meteo.com/v1/forecast";
26
+ const OPEN_METEO_GEOCODING = "https://geocoding-api.open-meteo.com/v1/search";
27
+ const OPEN_METEO_HEADERS = {
28
+ "User-Agent": "Ouroboros/1.0 (https://github.com/ouroborosbot/ouroboros)",
29
+ Accept: "application/json",
30
+ };
31
+ /**
32
+ * WMO weather interpretation codes (WW) to human-readable descriptions.
33
+ * Spec: https://www.nodc.noaa.gov/archive/arc0021/0002199/1.1/data/0-data/HTML/WMO-CODE/WMO4677.HTM
34
+ */
35
+ exports.WMO_WEATHER_CODES = {
36
+ 0: "clear sky",
37
+ 1: "mainly clear",
38
+ 2: "partly cloudy",
39
+ 3: "overcast",
40
+ 45: "fog",
41
+ 48: "depositing rime fog",
42
+ 51: "light drizzle",
43
+ 53: "moderate drizzle",
44
+ 55: "dense drizzle",
45
+ 56: "light freezing drizzle",
46
+ 57: "dense freezing drizzle",
47
+ 61: "slight rain",
48
+ 63: "moderate rain",
49
+ 65: "heavy rain",
50
+ 66: "light freezing rain",
51
+ 67: "heavy freezing rain",
52
+ 71: "slight snowfall",
53
+ 73: "moderate snowfall",
54
+ 75: "heavy snowfall",
55
+ 77: "snow grains",
56
+ 80: "slight rain showers",
57
+ 81: "moderate rain showers",
58
+ 82: "violent rain showers",
59
+ 85: "slight snow showers",
60
+ 86: "heavy snow showers",
61
+ 95: "thunderstorm",
62
+ 96: "thunderstorm with slight hail",
63
+ 99: "thunderstorm with heavy hail",
64
+ };
65
+ /** Convert a WMO weather code to a human-readable description. */
66
+ function wmoWeatherDescription(code) {
67
+ return exports.WMO_WEATHER_CODES[code] ?? "unknown";
30
68
  }
31
69
  async function getWeather(lat, lon) {
32
70
  return withNervesEvents("getWeather", { lat, lon }, async () => {
33
- const apiKey = await getWeatherApiKey();
34
- const url = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${apiKey}&units=metric`;
35
- const res = await fetch(url);
71
+ const url = `${OPEN_METEO_FORECAST}?latitude=${lat}&longitude=${lon}` +
72
+ `&current=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,weather_code` +
73
+ `&daily=temperature_2m_max,temperature_2m_min,weather_code` +
74
+ `&timezone=auto`;
75
+ const res = await fetch(url, { headers: OPEN_METEO_HEADERS });
36
76
  if (!res.ok) {
37
- throw new Error(`OpenWeatherMap API error: ${res.status} ${res.statusText}`);
77
+ throw new Error(`Open-Meteo API error: ${res.status} ${res.statusText}`);
38
78
  }
39
79
  const data = await res.json();
40
- return parseWeatherResponse(data);
80
+ return parseOpenMeteoResponse(data, lat, lon);
41
81
  });
42
82
  }
43
83
  async function getWeatherByCity(city) {
44
84
  return withNervesEvents("getWeatherByCity", { city }, async () => {
45
- const apiKey = await getWeatherApiKey();
46
- const url = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&appid=${apiKey}&units=metric`;
47
- const res = await fetch(url);
85
+ // Step 1: geocode the city name via Open-Meteo geocoding
86
+ const geoUrl = `${OPEN_METEO_GEOCODING}?name=${encodeURIComponent(city)}&count=1&language=en`;
87
+ const geoRes = await fetch(geoUrl, { headers: OPEN_METEO_HEADERS });
88
+ if (!geoRes.ok) {
89
+ throw new Error(`Open-Meteo geocoding error: ${geoRes.status} ${geoRes.statusText}`);
90
+ }
91
+ const geoData = await geoRes.json();
92
+ const results = geoData.results;
93
+ if (!results || results.length === 0) {
94
+ throw new Error(`No location found for city "${city}"`);
95
+ }
96
+ const loc = results[0];
97
+ // Step 2: fetch forecast for the resolved coordinates
98
+ const url = `${OPEN_METEO_FORECAST}?latitude=${loc.latitude}&longitude=${loc.longitude}` +
99
+ `&current=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,weather_code` +
100
+ `&daily=temperature_2m_max,temperature_2m_min,weather_code` +
101
+ `&timezone=auto`;
102
+ const res = await fetch(url, { headers: OPEN_METEO_HEADERS });
48
103
  if (!res.ok) {
49
- throw new Error(`OpenWeatherMap API error: ${res.status} ${res.statusText}`);
104
+ throw new Error(`Open-Meteo API error: ${res.status} ${res.statusText}`);
50
105
  }
51
106
  const data = await res.json();
52
- return parseWeatherResponse(data);
107
+ return parseOpenMeteoResponse(data, loc.latitude, loc.longitude, loc.name, loc.country_code);
53
108
  });
54
109
  }
55
- function parseWeatherResponse(data) {
110
+ function parseOpenMeteoResponse(data, lat, lon, cityName, countryCode) {
111
+ const current = data.current ?? {};
112
+ const daily = data.daily ?? {};
56
113
  return {
57
- temperature: data.main.temp,
58
- feelsLike: data.main.feels_like,
59
- description: data.weather?.[0]?.description ?? "unknown",
60
- humidity: data.main.humidity,
61
- windSpeed: data.wind.speed,
62
- city: data.name,
63
- country: data.sys.country,
114
+ temperature: current.temperature_2m ?? 0,
115
+ feelsLike: current.apparent_temperature ?? 0,
116
+ description: wmoWeatherDescription(current.weather_code ?? -1),
117
+ humidity: current.relative_humidity_2m ?? 0,
118
+ windSpeed: current.wind_speed_10m ?? 0,
119
+ city: cityName ?? `${lat},${lon}`,
120
+ country: countryCode ?? "",
121
+ dailyHigh: daily.temperature_2m_max?.[0],
122
+ dailyLow: daily.temperature_2m_min?.[0],
64
123
  };
65
124
  }
66
125
  // --- US State Dept Travel Advisories ---
@@ -79,31 +138,52 @@ exports.TRAVEL_ADVISORY_RSS_URL = "https://travel.state.gov/_res/rss/TAsTWs.xml"
79
138
  * This table handles the ~30 codes where they diverge.
80
139
  */
81
140
  const ISO_FIPS_DIVERGENCE = {
141
+ AG: "Antigua and Barbuda", // FIPS: AC
82
142
  AT: "Austria", // FIPS: AU
83
143
  AU: "Australia", // FIPS: AS
84
144
  BD: "Bangladesh", // FIPS: BG
145
+ BH: "Bahrain", // FIPS: BA
85
146
  BN: "Brunei", // FIPS: BX
86
147
  BO: "Bolivia", // FIPS: BL
148
+ BS: "Bahamas", // FIPS: BF
149
+ BW: "Botswana", // FIPS: BC
150
+ CD: "Congo (Kinshasa)", // FIPS: CG
87
151
  CH: "Switzerland", // FIPS: SZ
152
+ CI: "Cote d'Ivoire", // FIPS: IV
153
+ CL: "Chile", // FIPS: CI
154
+ CN: "China", // FIPS: CH
88
155
  CZ: "Czechia", // FIPS: EZ
89
156
  DE: "Germany", // FIPS: GM
157
+ DK: "Denmark", // FIPS: DA
158
+ DO: "Dominican Republic", // FIPS: DR
159
+ DZ: "Algeria", // FIPS: AG
90
160
  ES: "Spain", // FIPS: SP
91
161
  FI: "Finland", // FIPS: FI → same, but title might say "Finland"
92
162
  GQ: "Equatorial Guinea", // FIPS: EK
93
163
  HR: "Croatia", // FIPS: HR → same
94
164
  IE: "Ireland", // FIPS: EI
165
+ IL: "Israel", // FIPS: IS
166
+ JP: "Japan", // FIPS: JA
95
167
  KP: "North Korea", // FIPS: KN
96
168
  KR: "South Korea", // FIPS: KS
97
169
  LT: "Lithuania", // FIPS: LH
170
+ MA: "Morocco", // FIPS: MO
98
171
  MM: "Burma", // FIPS: BM (State Dept uses "Burma (Myanmar)")
172
+ NG: "Nigeria", // FIPS: NI
99
173
  NO: "Norway", // FIPS: NO → same
100
174
  PH: "Philippines", // FIPS: RP
175
+ PT: "Portugal", // FIPS: PO
101
176
  RO: "Romania", // FIPS: RO → same
102
177
  SE: "Sweden", // FIPS: SW
178
+ SG: "Singapore", // FIPS: SN
103
179
  TL: "Timor-Leste", // FIPS: TT
180
+ TR: "Turkey", // FIPS: TU
104
181
  TW: "Taiwan", // FIPS: TW → same
182
+ UA: "Ukraine", // FIPS: UP
105
183
  VA: "Holy See", // FIPS: VT
184
+ VN: "Vietnam", // FIPS: VM
106
185
  YE: "Yemen", // FIPS: YM
186
+ ZA: "South Africa", // FIPS: SF
107
187
  };
108
188
  /**
109
189
  * Look up the expected country name for an ISO alpha-2 code,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.233",
3
+ "version": "0.1.0-alpha.235",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",
@@ -117,7 +117,7 @@ Track and reference these travel preferences:
117
117
  ## Tool Reference
118
118
 
119
119
  ### Native Tools (always available)
120
- - `weather_lookup` - Current weather by city or coordinates
120
+ - `weather_lookup` - Current weather and daily forecast by city or coordinates (Open-Meteo, zero config)
121
121
  - `travel_advisory` - US State Dept advisory by country code
122
122
  - `geocode_search` - Location/POI search with coordinates
123
123
  - `credential_get` - Check credential metadata for a domain (never returns passwords)