@ouro.bot/cli 0.1.0-alpha.241 → 0.1.0-alpha.242

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
@@ -11,7 +11,10 @@
11
11
  "version": "0.1.0-alpha.242",
12
12
  "changes": [
13
13
  "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.",
14
- "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."
14
+ "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.",
15
+ "fix(heart): retry-everything-except-blocklist policy \u2014 the SDK 'Request timed out.' error from MiniMax (and other providers) was reaching slugger as a terminal failure because neither the generic isTransientError detector nor the per-provider classifier recognized it. Replaced the two-layer transient detection with a small blocklist (HTTP 400/401/403/404/422 + classifications auth-failure/usage-limit). Default policy now retries every other error.",
16
+ "fix(heart/providers): drop the 30s OpenAI/Anthropic SDK timeout from all five providers (anthropic, azure, github-copilot, minimax, openai-codex). The SDK timeout caps the entire stream lifetime, so 30s killed any reasoning model mid-generation. SDK defaults (\u224810min) are sane.",
17
+ "refactor(heart/providers): consolidate duplicated isNetworkError + classifyXxxError scaffolding into a shared `error-classification.ts` module. Each provider now delegates via `classifyHttpError(err, overrides)` and only carries its own provider-specific quirks (Anthropic 529, Codex usage-limit message detection)."
15
18
  ]
16
19
  },
17
20
  {
@@ -10,8 +10,7 @@ exports.isExternalStateQuery = isExternalStateQuery;
10
10
  exports.getSettleRetryError = getSettleRetryError;
11
11
  exports.stripLastToolCalls = stripLastToolCalls;
12
12
  exports.repairOrphanedToolCalls = repairOrphanedToolCalls;
13
- exports.isTransientError = isTransientError;
14
- exports.classifyTransientError = classifyTransientError;
13
+ exports.isRetryBlocked = isRetryBlocked;
15
14
  exports.runAgent = runAgent;
16
15
  const config_1 = require("./config");
17
16
  const identity_1 = require("./identity");
@@ -353,48 +352,39 @@ function isContextOverflow(err) {
353
352
  return true;
354
353
  return false;
355
354
  }
356
- // Detect transient network errors worth retrying
357
- function isTransientError(err) {
358
- if (!(err instanceof Error))
359
- return false;
360
- const msg = err.message || "";
361
- const code = err.code || "";
362
- // Node.js network error codes
363
- if (["ECONNRESET", "ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT", "EPIPE",
364
- "EAI_AGAIN", "EHOSTUNREACH", "ENETUNREACH", "ECONNABORTED"].includes(code))
365
- return true;
366
- // OpenAI SDK / fetch errors
367
- if (msg.includes("fetch failed"))
368
- return true;
369
- if (msg.includes("network") && !msg.includes("context"))
355
+ // HTTP statuses that will never become retryable on their own — the request is
356
+ // semantically wrong (malformed, unauthorized, missing route, etc.) and the
357
+ // caller has to do something different before it can succeed.
358
+ const NON_RETRYABLE_HTTP_STATUSES = new Set([
359
+ 400, // Bad Request — malformed payload
360
+ 401, // Unauthorized — credentials invalid/expired
361
+ 403, // Forbidden — credentials lack permission
362
+ 404, // Not Found — model/route doesn't exist
363
+ 422, // Unprocessable Entity — semantic validation failure
364
+ ]);
365
+ // Provider-classified error categories that we never retry. usage-limit is
366
+ // distinct from rate-limit: rate limits clear in seconds (retryable), usage
367
+ // limits are billing quotas that take hours/days to reset.
368
+ const NON_RETRYABLE_CLASSIFICATIONS = new Set([
369
+ "auth-failure",
370
+ "usage-limit",
371
+ ]);
372
+ // Default policy: retry every error from the provider, EXCEPT the small set
373
+ // above. The user explicitly requested this — past behavior was to retry only
374
+ // on a known-transient list, which silently dropped real harness/SDK timeouts
375
+ // (e.g. OpenAI SDK's "Request timed out." has no err.code and no status, so
376
+ // the substring matchers missed it).
377
+ function isRetryBlocked(error, classification) {
378
+ const status = error.status;
379
+ if (status !== undefined && NON_RETRYABLE_HTTP_STATUSES.has(status))
370
380
  return true;
371
- if (msg.includes("ECONNRESET") || msg.includes("ETIMEDOUT"))
372
- return true;
373
- if (msg.includes("socket hang up"))
374
- return true;
375
- if (msg.includes("getaddrinfo"))
376
- return true;
377
- // HTTP 429 / 500 / 502 / 503 / 504
378
- const status = err.status;
379
- if (status === 429 || status === 500 || status === 502 || status === 503 || status === 504)
381
+ if (NON_RETRYABLE_CLASSIFICATIONS.has(classification))
380
382
  return true;
381
383
  return false;
382
384
  }
383
- function classifyTransientError(err) {
384
- if (!(err instanceof Error))
385
- return "unknown error";
386
- const status = err.status;
387
- if (status === 429)
388
- return "rate limited";
389
- if (status === 401 || status === 403)
390
- return "auth error";
391
- if (status && status >= 500)
392
- return "server error";
393
- return "network error";
394
- }
395
385
  const MAX_RETRIES = 3;
396
386
  const RETRY_BASE_MS = 2000;
397
- const TRANSIENT_RETRY_LABELS = {
387
+ const RETRY_LABELS = {
398
388
  "auth-failure": "auth error",
399
389
  "usage-limit": "usage limit",
400
390
  "rate-limit": "rate limited",
@@ -942,10 +932,10 @@ async function runAgent(messages, callbacks, channel, signal, options) {
942
932
  callbacks.onError(new Error("context trimmed, retrying..."), "transient");
943
933
  continue;
944
934
  }
945
- // Transient errors: retry with exponential backoff.
946
- // Two-layer detection: generic isTransientError (HTTP codes, network patterns)
947
- // PLUS provider-specific classifyError (catches SDK-wrapped errors that
948
- // generic detection might miss).
935
+ // Retry policy: retry every error EXCEPT those on the blocklist
936
+ // (NON_RETRYABLE_HTTP_STATUSES / NON_RETRYABLE_CLASSIFICATIONS).
937
+ // The classification still drives the user-facing label and the
938
+ // auth-failure guidance message below — it just no longer gates retries.
949
939
  const errorForClassification = e instanceof Error ? e : /* v8 ignore next -- defensive @preserve */ new Error(String(e));
950
940
  let providerClassification;
951
941
  try {
@@ -955,25 +945,23 @@ async function runAgent(messages, callbacks, channel, signal, options) {
955
945
  /* v8 ignore next -- defensive: classifyError should not throw @preserve */
956
946
  providerClassification = "unknown";
957
947
  }
958
- const retryableByClassification = providerClassification === "rate-limit"
959
- || providerClassification === "server-error"
960
- || providerClassification === "network-error";
961
- const retryableByGeneric = isTransientError(e);
962
- const shouldRetry = (retryableByGeneric || retryableByClassification) && retryCount < MAX_RETRIES;
948
+ const blocked = isRetryBlocked(errorForClassification, providerClassification);
949
+ const shouldRetry = !blocked && retryCount < MAX_RETRIES;
963
950
  (0, runtime_1.emitNervesEvent)({
964
951
  level: shouldRetry ? "info" : "warn",
965
952
  event: shouldRetry ? "engine.provider_retry" : "engine.provider_retry_skip",
966
953
  component: "engine",
967
954
  message: shouldRetry
968
955
  ? `provider error is retryable (attempt ${retryCount + 1}/${MAX_RETRIES})`
969
- : `provider error is not retryable or retries exhausted`,
956
+ : blocked
957
+ ? `provider error is on retry blocklist`
958
+ : `provider error retries exhausted`,
970
959
  meta: {
971
960
  provider: providerRuntime.id,
972
961
  model: providerRuntime.model,
973
962
  retryCount,
974
963
  maxRetries: MAX_RETRIES,
975
- retryableByGeneric,
976
- retryableByClassification,
964
+ blocked,
977
965
  providerClassification,
978
966
  errorMessage: errorForClassification.message.slice(0, 200),
979
967
  httpStatus: e.status ?? null,
@@ -982,8 +970,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
982
970
  if (shouldRetry) {
983
971
  retryCount++;
984
972
  const delay = RETRY_BASE_MS * Math.pow(2, retryCount - 1);
985
- /* v8 ignore next -- defensive: all classifications have labels @preserve */
986
- const cause = TRANSIENT_RETRY_LABELS[providerClassification] ?? providerClassification;
973
+ const cause = RETRY_LABELS[providerClassification];
987
974
  callbacks.onError(new Error(`${cause}, retrying in ${delay / 1000}s (${retryCount}/${MAX_RETRIES})...`), "transient");
988
975
  // Wait with abort support
989
976
  const aborted = await new Promise((resolve) => {
@@ -45,6 +45,7 @@ const identity_1 = require("../identity");
45
45
  const runtime_1 = require("../../nerves/runtime");
46
46
  const streaming_1 = require("../streaming");
47
47
  const model_capabilities_1 = require("../model-capabilities");
48
+ const error_classification_1 = require("./error-classification");
48
49
  const ANTHROPIC_SETUP_TOKEN_PREFIX = "sk-ant-oat01-";
49
50
  const ANTHROPIC_SETUP_TOKEN_MIN_LENGTH = 80;
50
51
  const ANTHROPIC_OAUTH_BETA_HEADER = "claude-code-20250219,oauth-2025-04-20,fine-grained-tool-streaming-2025-05-14,interleaved-thinking-2025-05-14";
@@ -221,42 +222,19 @@ function mergeAnthropicToolArguments(current, partial) {
221
222
  }
222
223
  return current + partial;
223
224
  }
224
- /* v8 ignore start -- shared network error utility, tested via classification tests @preserve */
225
- function isNetworkError(error) {
226
- const code = error.code || "";
227
- if (["ECONNRESET", "ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT", "EPIPE",
228
- "EAI_AGAIN", "EHOSTUNREACH", "ENETUNREACH", "ECONNABORTED"].includes(code))
229
- return true;
230
- const msg = error.message || "";
231
- return msg.includes("fetch failed") || msg.includes("socket hang up") || msg.includes("getaddrinfo");
232
- }
233
- /* v8 ignore stop */
234
225
  function classifyAnthropicError(error) {
235
- const status = error.status;
236
- if (status === 401 || status === 403 || isAnthropicAuthFailure(error))
237
- return "auth-failure";
238
- if (status === 429)
239
- return "rate-limit";
240
- if (status === 529 || (status && status >= 500))
241
- return "server-error";
242
- if (isNetworkError(error))
243
- return "network-error";
244
- return "unknown";
226
+ return (0, error_classification_1.classifyHttpError)(error, {
227
+ isAuthFailure: isAnthropicAuthFailure,
228
+ isServerError: (e) => e.status === 529,
229
+ });
245
230
  }
246
- /* v8 ignore start -- auth detection: only called from classifyAnthropicError which always passes Error @preserve */
247
231
  function isAnthropicAuthFailure(error) {
248
- if (!(error instanceof Error))
249
- return false;
250
- const status = error.status;
251
- if (status === 401 || status === 403)
252
- return true;
253
232
  const lower = error.message.toLowerCase();
254
233
  return (lower.includes("oauth authentication") ||
255
234
  lower.includes("authentication failed") ||
256
235
  lower.includes("unauthorized") ||
257
236
  lower.includes("invalid api key"));
258
237
  }
259
- /* v8 ignore stop */
260
238
  async function streamAnthropicMessages(client, model, request) {
261
239
  const { system, messages } = toAnthropicMessages(request.messages);
262
240
  const anthropicTools = toAnthropicTools(request.activeTools);
@@ -438,7 +416,6 @@ function createAnthropicProviderRuntime(model) {
438
416
  function createClient(token) {
439
417
  return new sdk_1.default({
440
418
  authToken: token,
441
- timeout: 30000,
442
419
  maxRetries: 0,
443
420
  defaultHeaders: {
444
421
  "anthropic-beta": ANTHROPIC_OAUTH_BETA_HEADER,
@@ -41,28 +41,10 @@ const config_1 = require("../config");
41
41
  const runtime_1 = require("../../nerves/runtime");
42
42
  const streaming_1 = require("../streaming");
43
43
  const model_capabilities_1 = require("../model-capabilities");
44
+ const error_classification_1 = require("./error-classification");
44
45
  const COGNITIVE_SERVICES_SCOPE = "https://cognitiveservices.azure.com/.default";
45
- /* v8 ignore start -- shared network error utility, tested via classification tests @preserve */
46
- function isNetworkError(error) {
47
- const code = error.code || "";
48
- if (["ECONNRESET", "ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT", "EPIPE",
49
- "EAI_AGAIN", "EHOSTUNREACH", "ENETUNREACH", "ECONNABORTED"].includes(code))
50
- return true;
51
- const msg = error.message || "";
52
- return msg.includes("fetch failed") || msg.includes("socket hang up") || msg.includes("getaddrinfo");
53
- }
54
- /* v8 ignore stop */
55
46
  function classifyAzureError(error) {
56
- const status = error.status;
57
- if (status === 401 || status === 403)
58
- return "auth-failure";
59
- if (status === 429)
60
- return "rate-limit";
61
- if (status && status >= 500)
62
- return "server-error";
63
- if (isNetworkError(error))
64
- return "network-error";
65
- return "unknown";
47
+ return (0, error_classification_1.classifyHttpError)(error);
66
48
  }
67
49
  // @azure/identity is imported dynamically (below) rather than at the top level
68
50
  // because it's a heavy package (~30+ transitive deps) and we only need it when
@@ -113,7 +95,6 @@ function createAzureProviderRuntime(model) {
113
95
  endpoint: azureConfig.endpoint.replace(/\/openai.*$/, ""),
114
96
  deployment: azureConfig.deployment,
115
97
  apiVersion: azureConfig.apiVersion,
116
- timeout: 30000,
117
98
  maxRetries: 0,
118
99
  };
119
100
  if (useApiKey) {
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isNetworkError = isNetworkError;
4
+ exports.classifyHttpError = classifyHttpError;
5
+ const runtime_1 = require("../../nerves/runtime");
6
+ // Node socket / DNS error codes that indicate a transient network failure.
7
+ const NETWORK_ERROR_CODES = new Set([
8
+ "ECONNRESET",
9
+ "ECONNREFUSED",
10
+ "ENOTFOUND",
11
+ "ETIMEDOUT",
12
+ "EPIPE",
13
+ "EAI_AGAIN",
14
+ "EHOSTUNREACH",
15
+ "ENETUNREACH",
16
+ "ECONNABORTED",
17
+ ]);
18
+ // Substrings the OpenAI/Anthropic SDKs use when wrapping fetch/socket failures
19
+ // into Error.message instead of an err.code.
20
+ const NETWORK_ERROR_MESSAGE_PATTERNS = [
21
+ "fetch failed",
22
+ "socket hang up",
23
+ "getaddrinfo",
24
+ "request timed out", // OpenAI SDK timeout — see SDK source
25
+ "request timeout",
26
+ "connection error",
27
+ ];
28
+ // True if the error looks like a transient network issue (no HTTP status, just
29
+ // a socket/DNS/timeout failure from the underlying transport).
30
+ function isNetworkError(error) {
31
+ const code = error.code || "";
32
+ if (NETWORK_ERROR_CODES.has(code))
33
+ return true;
34
+ const msg = (error.message || "").toLowerCase();
35
+ return NETWORK_ERROR_MESSAGE_PATTERNS.some((pat) => msg.includes(pat));
36
+ }
37
+ // Standard HTTP error → ProviderErrorClassification mapping. Providers wrap
38
+ // this with their own overrides.
39
+ function classifyHttpError(error, overrides) {
40
+ const status = error.status;
41
+ if (overrides?.isAuthFailure?.(error) || status === 401 || status === 403) {
42
+ return "auth-failure";
43
+ }
44
+ if (status === 429) {
45
+ if (overrides?.isUsageLimit?.(error))
46
+ return "usage-limit";
47
+ return "rate-limit";
48
+ }
49
+ if (overrides?.isServerError?.(error) || (status !== undefined && status >= 500)) {
50
+ return "server-error";
51
+ }
52
+ if (isNetworkError(error))
53
+ return "network-error";
54
+ return "unknown";
55
+ }
56
+ /* v8 ignore start — module-level observability event */
57
+ (0, runtime_1.emitNervesEvent)({
58
+ component: "engine",
59
+ event: "engine.error_classification_loaded",
60
+ message: "shared provider error classification loaded",
61
+ meta: {},
62
+ });
63
+ /* v8 ignore stop */
@@ -10,30 +10,10 @@ const config_1 = require("../config");
10
10
  const runtime_1 = require("../../nerves/runtime");
11
11
  const streaming_1 = require("../streaming");
12
12
  const model_capabilities_1 = require("../model-capabilities");
13
- /* v8 ignore start -- duplicated from shared provider utils, tested there @preserve */
14
- function isNetworkError(error) {
15
- const code = error.code || "";
16
- if (["ECONNRESET", "ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT", "EPIPE",
17
- "EAI_AGAIN", "EHOSTUNREACH", "ENETUNREACH", "ECONNABORTED"].includes(code))
18
- return true;
19
- const msg = error.message || "";
20
- return msg.includes("fetch failed") || msg.includes("socket hang up") || msg.includes("getaddrinfo");
21
- }
22
- /* v8 ignore stop */
23
- /* v8 ignore start -- duplicated classification pattern, tested via provider unit tests @preserve */
13
+ const error_classification_1 = require("./error-classification");
24
14
  function classifyGithubCopilotError(error) {
25
- const status = error.status;
26
- if (status === 401 || status === 403)
27
- return "auth-failure";
28
- if (status === 429)
29
- return "rate-limit";
30
- if (status && status >= 500)
31
- return "server-error";
32
- if (isNetworkError(error))
33
- return "network-error";
34
- return "unknown";
15
+ return (0, error_classification_1.classifyHttpError)(error);
35
16
  }
36
- /* v8 ignore stop */
37
17
  function createGithubCopilotProviderRuntime(model) {
38
18
  (0, runtime_1.emitNervesEvent)({
39
19
  component: "engine",
@@ -57,7 +37,6 @@ function createGithubCopilotProviderRuntime(model) {
57
37
  const client = new openai_1.default({
58
38
  apiKey: config.githubToken,
59
39
  baseURL: config.baseUrl,
60
- timeout: 30000,
61
40
  maxRetries: 0,
62
41
  });
63
42
  if (isCompletionsModel) {
@@ -8,27 +8,9 @@ exports.createMinimaxProviderRuntime = createMinimaxProviderRuntime;
8
8
  const openai_1 = __importDefault(require("openai"));
9
9
  const config_1 = require("../config");
10
10
  const runtime_1 = require("../../nerves/runtime");
11
- /* v8 ignore start -- shared network error utility, tested via classification tests @preserve */
12
- function isNetworkError(error) {
13
- const code = error.code || "";
14
- if (["ECONNRESET", "ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT", "EPIPE",
15
- "EAI_AGAIN", "EHOSTUNREACH", "ENETUNREACH", "ECONNABORTED"].includes(code))
16
- return true;
17
- const msg = error.message || "";
18
- return msg.includes("fetch failed") || msg.includes("socket hang up") || msg.includes("getaddrinfo");
19
- }
20
- /* v8 ignore stop */
11
+ const error_classification_1 = require("./error-classification");
21
12
  function classifyMinimaxError(error) {
22
- const status = error.status;
23
- if (status === 401 || status === 403)
24
- return "auth-failure";
25
- if (status === 429)
26
- return "rate-limit";
27
- if (status && status >= 500)
28
- return "server-error";
29
- if (isNetworkError(error))
30
- return "network-error";
31
- return "unknown";
13
+ return (0, error_classification_1.classifyHttpError)(error);
32
14
  }
33
15
  const streaming_1 = require("../streaming");
34
16
  const model_capabilities_1 = require("../model-capabilities");
@@ -48,7 +30,6 @@ function createMinimaxProviderRuntime(model) {
48
30
  const client = new openai_1.default({
49
31
  apiKey: minimaxConfig.apiKey,
50
32
  baseURL: "https://api.minimaxi.chat/v1",
51
- timeout: 30000,
52
33
  maxRetries: 0,
53
34
  });
54
35
  return {
@@ -11,6 +11,7 @@ const identity_1 = require("../identity");
11
11
  const runtime_1 = require("../../nerves/runtime");
12
12
  const streaming_1 = require("../streaming");
13
13
  const model_capabilities_1 = require("../model-capabilities");
14
+ const error_classification_1 = require("./error-classification");
14
15
  const OPENAI_CODEX_AUTH_FAILURE_MARKERS = [
15
16
  "authentication failed",
16
17
  "unauthorized",
@@ -43,43 +44,19 @@ function getOpenAICodexReauthGuidance(reason) {
43
44
  getOpenAICodexOAuthInstructions(),
44
45
  ].join("\n");
45
46
  }
46
- /* v8 ignore start -- shared network error utility, tested via classification tests @preserve */
47
- function isNetworkError(error) {
48
- const code = error.code || "";
49
- if (["ECONNRESET", "ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT", "EPIPE",
50
- "EAI_AGAIN", "EHOSTUNREACH", "ENETUNREACH", "ECONNABORTED"].includes(code))
51
- return true;
52
- const msg = error.message || "";
53
- return msg.includes("fetch failed") || msg.includes("socket hang up") || msg.includes("getaddrinfo");
54
- }
55
- /* v8 ignore stop */
56
47
  function classifyOpenAICodexError(error) {
57
- const status = error.status;
58
- if (status === 401 || status === 403 || isOpenAICodexAuthFailure(error))
59
- return "auth-failure";
60
- if (status === 429) {
61
- const lower = error.message.toLowerCase();
62
- if (lower.includes("usage") || lower.includes("quota") || lower.includes("exceeded your"))
63
- return "usage-limit";
64
- return "rate-limit";
65
- }
66
- if (status && status >= 500)
67
- return "server-error";
68
- if (isNetworkError(error))
69
- return "network-error";
70
- return "unknown";
48
+ return (0, error_classification_1.classifyHttpError)(error, {
49
+ isAuthFailure: isOpenAICodexAuthFailure,
50
+ isUsageLimit: (e) => {
51
+ const lower = e.message.toLowerCase();
52
+ return lower.includes("usage") || lower.includes("quota") || lower.includes("exceeded your");
53
+ },
54
+ });
71
55
  }
72
- /* v8 ignore start -- auth detection: only called from classifyOpenAICodexError which always passes Error @preserve */
73
56
  function isOpenAICodexAuthFailure(error) {
74
- if (!(error instanceof Error))
75
- return false;
76
- const status = error.status;
77
- if (status === 401 || status === 403)
78
- return true;
79
57
  const lower = error.message.toLowerCase();
80
58
  return OPENAI_CODEX_AUTH_FAILURE_MARKERS.some((marker) => lower.includes(marker));
81
59
  }
82
- /* v8 ignore stop */
83
60
  function decodeJwtPayload(token) {
84
61
  const parts = token.split(".");
85
62
  if (parts.length < 2)
@@ -143,7 +120,6 @@ function createOpenAICodexProviderRuntime(model) {
143
120
  "OpenAI-Beta": "responses=experimental",
144
121
  originator: "ouroboros",
145
122
  },
146
- timeout: 30000,
147
123
  maxRetries: 0,
148
124
  });
149
125
  let nativeInput = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.241",
3
+ "version": "0.1.0-alpha.242",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",