@hoox-sh/hoox-cli 0.11.1 → 0.11.2

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +143 -38
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -455,7 +455,7 @@ The CI pipeline runs in this order:
455
455
 
456
456
  1. `bun run lint` — ESLint check
457
457
  2. `bun run typecheck` — TypeScript check (`tsc --noEmit`)
458
- 3. `bun test packages/cli --coverage` — Run tests with coverage (80% threshold)
458
+ 3. `bun test packages/cli --coverage` — Run tests with coverage (~80%+ overall; `src/` ~95%)
459
459
  4. `bun run build` — TypeScript build check
460
460
 
461
461
  ## Project Structure
package/dist/index.js CHANGED
@@ -14719,6 +14719,7 @@ var init_types = __esm(() => {
14719
14719
  price: exports_external.number().positive().finite().optional(),
14720
14720
  leverage: exports_external.number().int().positive().max(125).optional(),
14721
14721
  test: exports_external.boolean().optional(),
14722
+ idempotencyKey: exports_external.string().min(1).max(512).optional(),
14722
14723
  queuedAt: exports_external.string().min(1).max(64)
14723
14724
  }).strict();
14724
14725
  TradeSignalSchema = exports_external.object({
@@ -14753,7 +14754,7 @@ var init_types = __esm(() => {
14753
14754
  });
14754
14755
 
14755
14756
  // ../shared/src/kvKeys.ts
14756
- var KV_TRADE_DEFAULT_LEVERAGE = "trade:default_leverage", KV_TRADE_MAX_POSITION_SIZE = "trade:max_position_size", KV_TRADE_MAX_DAILY_DRAWDOWN_PERCENT = "trade:max_daily_drawdown_percent", KV_TRADE_TRAILING_STOP_PERCENT = "trade:trailing_stop_percent", KV_TRADE_KILL_SWITCH = "trade:kill_switch", KV_TRADE_ROUTING = "trade:routing", KV_BOT_ENABLED = "bot:enabled", KV_BOT_DEFAULT_CHAT_ID = "bot:default_chat_id", KV_BOT_NOTIFY_ON_EXECUTION = "bot:notify_on_execution", KV_BOT_NOTIFY_ON_ERROR = "bot:notify_on_error", KV_WEBHOOK_IP_CHECK_ENABLED = "webhook:tradingview:ip_check_enabled", KV_WEBHOOK_ALLOWED_IPS = "webhook:tradingview:allowed_ips", KV_WEBHOOK_QUEUE_MODE = "webhook:queue_mode", KV_EMAIL_SCAN_SUBJECT = "email:scan_subject", KV_EMAIL_USE_IMAP = "email:use_imap", KV_EMAIL_COIN_PATTERN = "email:coin_pattern", KV_EMAIL_ACTION_PATTERN = "email:action_pattern", KV_EMAIL_QUANTITY_MULTIPLIER = "email:quantity_multiplier", KV_AGENT_CONFIG = "agent:config", KV_AGENT_OPENAI_KEY = "agent:openai_key", KV_AGENT_ANTHROPIC_KEY = "agent:anthropic_key", KV_AGENT_GOOGLE_KEY = "agent:google_key", KV_DASHBOARD_AI_HEALTH_SUMMARY = "dashboard:ai_health_summary", KV_HEALTH_CHECK = "health_check", KV_HOUSEKEEPING_LAST_CHECK = "housekeeping:last_check", QUEUE_MODE_FAILOVER = "queue_failover", QUEUE_MODE_EVERYWHERE = "queue_everywhere", QUEUE_MODE_DISABLED = "queue_disabled", KVKeys;
14757
+ var KV_TRADE_DEFAULT_LEVERAGE = "trade:default_leverage", KV_TRADE_MAX_POSITION_SIZE = "trade:max_position_size", KV_TRADE_MAX_DAILY_DRAWDOWN_PERCENT = "trade:max_daily_drawdown_percent", KV_TRADE_TRAILING_STOP_PERCENT = "trade:trailing_stop_percent", KV_TRADE_KILL_SWITCH = "trade:kill_switch", KV_TRADE_ROUTING = "trade:routing", KV_BOT_ENABLED = "bot:enabled", KV_BOT_DEFAULT_CHAT_ID = "bot:default_chat_id", KV_BOT_NOTIFY_ON_EXECUTION = "bot:notify_on_execution", KV_BOT_NOTIFY_ON_ERROR = "bot:notify_on_error", KV_WEBHOOK_IP_CHECK_ENABLED = "webhook:tradingview:ip_check_enabled", KV_WEBHOOK_ALLOWED_IPS = "webhook:tradingview:allowed_ips", KV_WEBHOOK_QUEUE_MODE = "webhook:queue_mode", KV_EMAIL_SCAN_SUBJECT = "email:scan_subject", KV_EMAIL_USE_IMAP = "email:use_imap", KV_EMAIL_COIN_PATTERN = "email:coin_pattern", KV_EMAIL_ACTION_PATTERN = "email:action_pattern", KV_EMAIL_QUANTITY_MULTIPLIER = "email:quantity_multiplier", KV_AGENT_CONFIG = "agent:config", KV_AGENT_OPENAI_KEY = "agent:openai_key", KV_AGENT_ANTHROPIC_KEY = "agent:anthropic_key", KV_AGENT_GOOGLE_KEY = "agent:google_key", KV_DASHBOARD_AI_HEALTH_SUMMARY = "dashboard:ai_health_summary", KV_GLOBAL_KILL_SWITCH = "global:kill_switch", KV_HEALTH_CHECK = "health_check", KV_HOUSEKEEPING_LAST_CHECK = "housekeeping:last_check", QUEUE_MODE_FAILOVER = "queue_failover", QUEUE_MODE_EVERYWHERE = "queue_everywhere", QUEUE_MODE_DISABLED = "queue_disabled", KVKeys;
14757
14758
  var init_kvKeys = __esm(() => {
14758
14759
  KVKeys = {
14759
14760
  KV_TRADE_DEFAULT_LEVERAGE,
@@ -14779,6 +14780,7 @@ var init_kvKeys = __esm(() => {
14779
14780
  KV_AGENT_ANTHROPIC_KEY,
14780
14781
  KV_AGENT_GOOGLE_KEY,
14781
14782
  KV_DASHBOARD_AI_HEALTH_SUMMARY,
14783
+ KV_GLOBAL_KILL_SWITCH,
14782
14784
  KV_HEALTH_CHECK,
14783
14785
  KV_HOUSEKEEPING_LAST_CHECK,
14784
14786
  QUEUE_MODE_FAILOVER,
@@ -14787,6 +14789,54 @@ var init_kvKeys = __esm(() => {
14787
14789
  };
14788
14790
  });
14789
14791
 
14792
+ // ../shared/src/kill-switch.ts
14793
+ function isTruthyKillSwitchFlag(value) {
14794
+ if (value == null)
14795
+ return false;
14796
+ const v = value.trim().toLowerCase();
14797
+ return v === "true" || v === "1" || v === "yes" || v === "on";
14798
+ }
14799
+ async function checkKillSwitch(kv, options) {
14800
+ const onMissingKv = options?.onMissingKv ?? "open";
14801
+ const onReadError = options?.onReadError ?? "closed";
14802
+ if (!kv) {
14803
+ if (onMissingKv === "closed") {
14804
+ return { enabled: true, source: "missing_kv" };
14805
+ }
14806
+ return { enabled: false };
14807
+ }
14808
+ try {
14809
+ const values = await Promise.all(KILL_SWITCH_KEYS.map(async (key) => ({
14810
+ key,
14811
+ value: await kv.get(key)
14812
+ })));
14813
+ for (const { key, value } of values) {
14814
+ if (isTruthyKillSwitchFlag(value)) {
14815
+ return { enabled: true, source: key };
14816
+ }
14817
+ }
14818
+ return { enabled: false };
14819
+ } catch (error51) {
14820
+ const errorStr = String(error51);
14821
+ if (onReadError === "closed") {
14822
+ return { enabled: true, source: "read_error", error: errorStr };
14823
+ }
14824
+ return { enabled: false, error: errorStr };
14825
+ }
14826
+ }
14827
+ async function isTradingPaused(kv, options) {
14828
+ const result = await checkKillSwitch(kv, options);
14829
+ return result.enabled;
14830
+ }
14831
+ var KILL_SWITCH_ACTIVE_PREFIX = "KILL_SWITCH_ACTIVE", KILL_SWITCH_KEYS;
14832
+ var init_kill_switch = __esm(() => {
14833
+ init_kvKeys();
14834
+ KILL_SWITCH_KEYS = [
14835
+ KV_TRADE_KILL_SWITCH,
14836
+ KV_GLOBAL_KILL_SWITCH
14837
+ ];
14838
+ });
14839
+
14790
14840
  // ../shared/src/errors.ts
14791
14841
  function toError(err, fallback = "Unknown error") {
14792
14842
  if (err instanceof Error)
@@ -15908,7 +15958,11 @@ var init_config = __esm(() => {
15908
15958
 
15909
15959
  // ../shared/src/operator-transport.ts
15910
15960
  function stripTrailingSlashes(url2) {
15911
- return url2.replace(/\/+$/, "");
15961
+ let end = url2.length;
15962
+ while (end > 0 && url2.charCodeAt(end - 1) === 47) {
15963
+ end -= 1;
15964
+ }
15965
+ return end === url2.length ? url2 : url2.slice(0, end);
15912
15966
  }
15913
15967
  function parseTransport(raw) {
15914
15968
  if (!raw)
@@ -15955,7 +16009,7 @@ function hasOperatorClientCredentials(profile) {
15955
16009
  return Boolean(profile.accessClientId && profile.accessClientSecret);
15956
16010
  }
15957
16011
  function operatorUrl(profile, path2) {
15958
- const base = profile.apiBase.replace(/\/+$/, "");
16012
+ const base = stripTrailingSlashes(profile.apiBase);
15959
16013
  const p = path2.startsWith("/") ? path2 : `/${path2}`;
15960
16014
  return `${base}${p}`;
15961
16015
  }
@@ -22006,6 +22060,8 @@ __export(exports_src, {
22006
22060
  kvManifestFromDashboardManifests: () => kvManifestFromDashboardManifests,
22007
22061
  isWithinHooxHome: () => isWithinHooxHome,
22008
22062
  isUnixModeGroupOrWorldAccessible: () => isUnixModeGroupOrWorldAccessible,
22063
+ isTruthyKillSwitchFlag: () => isTruthyKillSwitchFlag,
22064
+ isTradingPaused: () => isTradingPaused,
22009
22065
  isHooxSetupRoot: () => isHooxSetupRoot,
22010
22066
  isDashboardSectionFlatKv: () => isDashboardSectionFlatKv,
22011
22067
  isDashboardSectionEditable: () => isDashboardSectionEditable,
@@ -22064,6 +22120,7 @@ __export(exports_src, {
22064
22120
  createJsonResponse: () => createJsonResponse,
22065
22121
  createErrorResponse: () => createErrorResponse,
22066
22122
  createCronHandler: () => createCronHandler,
22123
+ checkKillSwitch: () => checkKillSwitch,
22067
22124
  buildOperatorAuthHeaders: () => buildOperatorAuthHeaders,
22068
22125
  buildDashboardKvKey: () => buildDashboardKvKey,
22069
22126
  authenticatedServiceFetch: () => authenticatedServiceFetch,
@@ -22090,6 +22147,8 @@ __export(exports_src, {
22090
22147
  PRESETS: () => PRESETS,
22091
22148
  LogLevelColor: () => LogLevelColor,
22092
22149
  KVKeys: () => KVKeys,
22150
+ KILL_SWITCH_KEYS: () => KILL_SWITCH_KEYS,
22151
+ KILL_SWITCH_ACTIVE_PREFIX: () => KILL_SWITCH_ACTIVE_PREFIX,
22093
22152
  InternalAuthKeyFields: () => InternalAuthKeyFields,
22094
22153
  INTEGRATIONS: () => INTEGRATIONS,
22095
22154
  HOOX_DIR_MODE: () => HOOX_DIR_MODE,
@@ -22127,6 +22186,7 @@ var init_src = __esm(() => {
22127
22186
  init_legal();
22128
22187
  init_types();
22129
22188
  init_kvKeys();
22189
+ init_kill_switch();
22130
22190
  init_errors3();
22131
22191
  init_exchanges();
22132
22192
  init_kvUtils();
@@ -29255,18 +29315,20 @@ async function runInitCommand(options, globalOpts, isNonInteractive) {
29255
29315
  engine = new WizardEngine;
29256
29316
  }
29257
29317
  const skipRiskWarning = options.acceptRisk;
29258
- if (!skipRiskWarning && engine.getCurrentStep().id === "PREREQUISITES") {
29259
- const accepted = await confirm({
29260
- message: "Hoox connects to live trading exchanges and can execute real trades with real money. By continuing, you acknowledge that you are solely responsible for all trading activity and accept the risk of financial loss. Do you accept these terms?",
29261
- initialValue: false
29262
- });
29263
- if (isCancel(accepted)) {
29264
- cancel("Setup cancelled.");
29265
- process.exitCode = 0;
29266
- }
29267
- if (!accepted) {
29268
- outro("Setup cancelled. See DISCLAIMER.md for full terms.");
29269
- process.exitCode = 0;
29318
+ if (engine.getCurrentStep().id === "PREREQUISITES") {
29319
+ if (!skipRiskWarning) {
29320
+ const accepted = await confirm({
29321
+ message: "Hoox connects to live trading exchanges and can execute real trades with real money. By continuing, you acknowledge that you are solely responsible for all trading activity and accept the risk of financial loss. Do you accept these terms?",
29322
+ initialValue: false
29323
+ });
29324
+ if (isCancel(accepted)) {
29325
+ cancel("Setup cancelled.");
29326
+ process.exitCode = 0;
29327
+ }
29328
+ if (!accepted) {
29329
+ outro("Setup cancelled. See DISCLAIMER.md for full terms.");
29330
+ process.exitCode = 0;
29331
+ }
29270
29332
  }
29271
29333
  engine.execute({ checksPassed: true });
29272
29334
  await saveState(engine);
@@ -38529,7 +38591,31 @@ function sanitizeErrorMessage(message, secrets) {
38529
38591
  out = out.replace(/([?&]key=)[^&\s]+/gi, "$1[redacted]");
38530
38592
  return out;
38531
38593
  }
38594
+ var PROBE_ALLOWED_HOSTS = new Set([
38595
+ "api.cloudflare.com",
38596
+ "api.openai.com",
38597
+ "api.anthropic.com",
38598
+ "generativelanguage.googleapis.com",
38599
+ "api.cognitive.microsoft.com"
38600
+ ]);
38601
+ function assertProbeUrl(url2) {
38602
+ let parsed;
38603
+ try {
38604
+ parsed = new URL(url2);
38605
+ } catch {
38606
+ throw new Error(`Invalid probe URL: ${url2}`);
38607
+ }
38608
+ if (parsed.protocol !== "https:") {
38609
+ throw new Error(`Probe URL must be https: ${url2}`);
38610
+ }
38611
+ const host = parsed.hostname.toLowerCase();
38612
+ const allowed = PROBE_ALLOWED_HOSTS.has(host) || host.endsWith(".openai.azure.com") || host.endsWith(".cognitiveservices.azure.com");
38613
+ if (!allowed) {
38614
+ throw new Error(`Probe URL host not allowlisted: ${host}`);
38615
+ }
38616
+ }
38532
38617
  async function timedFetch(url2, init, timeoutMs) {
38618
+ assertProbeUrl(url2);
38533
38619
  const start = performance.now();
38534
38620
  const response = await fetch(url2, {
38535
38621
  ...init,
@@ -38638,9 +38724,8 @@ async function probeAnthropic(apiKey, timeoutMs) {
38638
38724
  return { status: "online", latencyMs };
38639
38725
  }
38640
38726
  if (response.status === 404) {
38641
- const start = performance.now();
38642
38727
  try {
38643
- const msgRes = await fetch("https://api.anthropic.com/v1/messages", {
38728
+ const { response: msgRes, latencyMs: lat } = await timedFetch("https://api.anthropic.com/v1/messages", {
38644
38729
  method: "POST",
38645
38730
  headers: {
38646
38731
  "x-api-key": apiKey,
@@ -38651,10 +38736,8 @@ async function probeAnthropic(apiKey, timeoutMs) {
38651
38736
  model: "claude-sonnet-4-20250514",
38652
38737
  max_tokens: 1,
38653
38738
  messages: [{ role: "user", content: "ping" }]
38654
- }),
38655
- signal: AbortSignal.timeout(timeoutMs)
38656
- });
38657
- const lat = Math.round(performance.now() - start);
38739
+ })
38740
+ }, timeoutMs);
38658
38741
  if (msgRes.ok)
38659
38742
  return { status: "online", latencyMs: lat };
38660
38743
  if (msgRes.status === 401 || msgRes.status === 403) {
@@ -38730,7 +38813,9 @@ async function probeAzure(apiKey, timeoutMs) {
38730
38813
  error: "AZURE_ENDPOINT missing"
38731
38814
  };
38732
38815
  }
38733
- const base = endpoint.replace(/\/+$/, "");
38816
+ let base = endpoint;
38817
+ while (base.endsWith("/"))
38818
+ base = base.slice(0, -1);
38734
38819
  const url2 = `${base}/openai/models?api-version=2024-02-01`;
38735
38820
  try {
38736
38821
  const { response, latencyMs } = await timedFetch(url2, {
@@ -38988,15 +39073,18 @@ init_config2();
38988
39073
  import { existsSync as existsSync13, readFileSync as readFileSync8 } from "fs";
38989
39074
  import { resolve as resolve7 } from "path";
38990
39075
  init_errors4();
38991
- var PYNE_WORKER_DIR = resolve7(process.cwd(), "workers/pyne-worker");
38992
39076
  var DEFAULT_TIMEOUT_MS = 30000;
39077
+ function getPyneWorkerDir() {
39078
+ return resolve7(process.cwd(), "workers/pyne-worker");
39079
+ }
38993
39080
  function requirePyneDir() {
38994
- if (!existsSync13(PYNE_WORKER_DIR)) {
38995
- throw new CLIError(`pyne-worker directory not found: ${PYNE_WORKER_DIR}
39081
+ const dir = getPyneWorkerDir();
39082
+ if (!existsSync13(dir)) {
39083
+ throw new CLIError(`pyne-worker directory not found: ${dir}
38996
39084
  ` + ` Clone it with: hoox clone pyne-worker
38997
39085
  ` + ` Or: git submodule update --init workers/pyne-worker`, 1 /* ERROR */);
38998
39086
  }
38999
- return PYNE_WORKER_DIR;
39087
+ return dir;
39000
39088
  }
39001
39089
  function loadDevVars(workerDir) {
39002
39090
  const out = {};
@@ -39064,11 +39152,33 @@ function resolveApiKey(explicit, workerDir) {
39064
39152
  }
39065
39153
  return null;
39066
39154
  }
39155
+ function normalizeBaseUrl(url2) {
39156
+ let end = url2.length;
39157
+ while (end > 0 && url2.charCodeAt(end - 1) === 47) {
39158
+ end -= 1;
39159
+ }
39160
+ return end === url2.length ? url2 : url2.slice(0, end);
39161
+ }
39162
+ function buildPyneRequestUrl(baseUrl, path6) {
39163
+ const base = normalizeBaseUrl(baseUrl);
39164
+ let parsed;
39165
+ try {
39166
+ parsed = new URL(base);
39167
+ } catch {
39168
+ throw new CLIError(`Invalid pyne-worker URL: ${baseUrl}`, 1 /* ERROR */);
39169
+ }
39170
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
39171
+ throw new CLIError(`pyne-worker URL must be http(s): ${baseUrl}`, 1 /* ERROR */);
39172
+ }
39173
+ const suffix = path6.startsWith("/") ? path6 : `/${path6}`;
39174
+ return `${parsed.origin}${parsed.pathname === "/" ? "" : parsed.pathname}${suffix}`;
39175
+ }
39067
39176
  async function pyneFetch(baseUrl, path6, options = {}) {
39068
39177
  const method = options.method ?? "GET";
39069
39178
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
39070
39179
  const controller = new AbortController;
39071
39180
  const timer = setTimeout(() => controller.abort(), timeoutMs);
39181
+ const requestUrl = buildPyneRequestUrl(baseUrl, path6);
39072
39182
  const headers = {
39073
39183
  Accept: "application/json"
39074
39184
  };
@@ -39081,7 +39191,7 @@ async function pyneFetch(baseUrl, path6, options = {}) {
39081
39191
  body = JSON.stringify(options.body);
39082
39192
  }
39083
39193
  try {
39084
- const res = await fetch(`${baseUrl.replace(/\/+$/, "")}${path6}`, {
39194
+ const res = await fetch(requestUrl, {
39085
39195
  method,
39086
39196
  headers,
39087
39197
  body,
@@ -39101,8 +39211,7 @@ async function pyneFetch(baseUrl, path6, options = {}) {
39101
39211
  }
39102
39212
  function printResult(data, fmt) {
39103
39213
  if (fmt.json) {
39104
- process.stdout.write(formatJson(data) + `
39105
- `);
39214
+ formatJson(data, fmt);
39106
39215
  return;
39107
39216
  }
39108
39217
  if (typeof data === "string") {
@@ -39111,8 +39220,7 @@ function printResult(data, fmt) {
39111
39220
  `));
39112
39221
  return;
39113
39222
  }
39114
- process.stdout.write(formatJson(data) + `
39115
- `);
39223
+ formatJson(data, fmt);
39116
39224
  }
39117
39225
  async function spawnInPyneDir(cmd, cwd) {
39118
39226
  const proc = Bun.spawn(cmd, {
@@ -39164,27 +39272,24 @@ EXAMPLES:
39164
39272
  if (!res.ok)
39165
39273
  process.exitCode = 1 /* ERROR */;
39166
39274
  if (fmt.json) {
39167
- process.stdout.write(formatJson(payload) + `
39168
- `);
39275
+ formatJson(payload, fmt);
39169
39276
  } else {
39170
39277
  const icon = res.ok ? "ok" : "fail";
39171
39278
  formatSuccess(`pyne-worker ${icon} \u2014 HTTP ${res.status} in ${latencyMs}ms (${baseUrl})`, fmt);
39172
39279
  if (res.json) {
39173
- process.stdout.write(formatJson(res.json) + `
39174
- `);
39280
+ formatJson(res.json, fmt);
39175
39281
  }
39176
39282
  }
39177
39283
  } catch (err) {
39178
39284
  process.exitCode = 1 /* ERROR */;
39179
39285
  const message = err instanceof Error ? err.message : String(err);
39180
39286
  if (fmt.json) {
39181
- process.stdout.write(formatJson({
39287
+ formatJson({
39182
39288
  worker: "pyne-worker",
39183
39289
  url: `${baseUrl}/health`,
39184
39290
  status: "down",
39185
39291
  error: message
39186
- }) + `
39187
- `);
39292
+ }, fmt);
39188
39293
  } else {
39189
39294
  throw new CLIError(`pyne-worker health failed: ${message}`, 1 /* ERROR */);
39190
39295
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hoox-sh/hoox-cli",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
4
4
  "description": "Hoox CLI — manage Cloudflare Workers, infrastructure, secrets, and deployments (@hoox-sh/hoox-cli)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,7 +26,7 @@
26
26
  "@hoox-sh/hoox-tui": "workspace:*"
27
27
  },
28
28
  "devDependencies": {
29
- "@hoox-sh/hoox-shared": "^1.2.0",
29
+ "@hoox-sh/hoox-shared": "^1.2.1",
30
30
  "@types/node": "^26.1.1",
31
31
  "bun-types": "^1.3.14",
32
32
  "typescript": "^6.0.3"