@hoox-sh/hoox-shared 1.2.1 → 1.4.0

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 (67) hide show
  1. package/dist/agent-config-fields.js +2 -0
  2. package/dist/d1/index.js +4 -2
  3. package/dist/d1/repository.js +4 -2
  4. package/dist/errors.d.ts.map +1 -1
  5. package/dist/errors.js +4 -2
  6. package/dist/health.js +4 -2
  7. package/dist/index.d.ts +5 -5
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +432 -139
  10. package/dist/kill-switch.js +2 -0
  11. package/dist/kvKeys.d.ts +3 -0
  12. package/dist/kvKeys.d.ts.map +1 -1
  13. package/dist/kvKeys.js +3 -0
  14. package/dist/kvUtils.d.ts +34 -1
  15. package/dist/kvUtils.d.ts.map +1 -1
  16. package/dist/kvUtils.js +56 -3
  17. package/dist/middleware/auth.d.ts +6 -3
  18. package/dist/middleware/auth.d.ts.map +1 -1
  19. package/dist/middleware/auth.js +19 -5
  20. package/dist/middleware/index.d.ts +3 -2
  21. package/dist/middleware/index.d.ts.map +1 -1
  22. package/dist/middleware/index.js +88 -8
  23. package/dist/middleware/logger.js +4 -2
  24. package/dist/middleware/validate.d.ts +16 -0
  25. package/dist/middleware/validate.d.ts.map +1 -1
  26. package/dist/middleware/validate.js +37 -1
  27. package/dist/middleware/wait-until.d.ts +32 -0
  28. package/dist/middleware/wait-until.d.ts.map +1 -0
  29. package/dist/middleware/wait-until.js +46 -0
  30. package/dist/path-utils.d.ts +35 -4
  31. package/dist/path-utils.d.ts.map +1 -1
  32. package/dist/path-utils.js +72 -5
  33. package/dist/queue-handler.d.ts +6 -0
  34. package/dist/queue-handler.d.ts.map +1 -1
  35. package/dist/queue-handler.js +51 -20
  36. package/dist/router.d.ts.map +1 -1
  37. package/dist/router.js +12 -2
  38. package/dist/schemas/index.d.ts +2 -1
  39. package/dist/schemas/index.d.ts.map +1 -1
  40. package/dist/schemas/index.js +58 -13
  41. package/dist/schemas/registry.d.ts +13 -0
  42. package/dist/schemas/registry.d.ts.map +1 -1
  43. package/dist/schemas/registry.js +55 -12
  44. package/dist/schemas/types.d.ts +5 -0
  45. package/dist/schemas/types.d.ts.map +1 -1
  46. package/dist/schemas/validators.d.ts.map +1 -1
  47. package/dist/schemas/validators.js +3 -1
  48. package/dist/service-bindings.js +18 -5
  49. package/dist/session.js +69 -5
  50. package/dist/stores/config-store.d.ts +2 -0
  51. package/dist/stores/config-store.d.ts.map +1 -1
  52. package/dist/stores/config-store.js +267 -16
  53. package/dist/stores/service-store.d.ts +12 -0
  54. package/dist/stores/service-store.d.ts.map +1 -1
  55. package/dist/stores/service-store.js +101 -40
  56. package/dist/stores/ui-store.d.ts +14 -0
  57. package/dist/stores/ui-store.d.ts.map +1 -1
  58. package/dist/stores/ui-store.js +8 -1
  59. package/dist/types.d.ts +22 -4
  60. package/dist/types.d.ts.map +1 -1
  61. package/dist/types.js +8 -4
  62. package/dist/wizard/engine.d.ts.map +1 -1
  63. package/dist/wizard/engine.js +18 -35
  64. package/dist/wizard/index.js +18 -35
  65. package/dist/wizard/presets.d.ts.map +1 -1
  66. package/dist/wizard/presets.js +6 -32
  67. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -269,6 +269,7 @@ var KV_BOT_ENABLED = "bot:enabled";
269
269
  var KV_BOT_DEFAULT_CHAT_ID = "bot:default_chat_id";
270
270
  var KV_BOT_NOTIFY_ON_EXECUTION = "bot:notify_on_execution";
271
271
  var KV_BOT_NOTIFY_ON_ERROR = "bot:notify_on_error";
272
+ var KV_TELEGRAM_ALLOWED_CHAT_IDS = "telegram:allowed_chat_ids";
272
273
  var KV_WEBHOOK_IP_CHECK_ENABLED = "webhook:tradingview:ip_check_enabled";
273
274
  var KV_WEBHOOK_ALLOWED_IPS = "webhook:tradingview:allowed_ips";
274
275
  var KV_WEBHOOK_QUEUE_MODE = "webhook:queue_mode";
@@ -299,6 +300,7 @@ var KVKeys = {
299
300
  KV_BOT_DEFAULT_CHAT_ID,
300
301
  KV_BOT_NOTIFY_ON_EXECUTION,
301
302
  KV_BOT_NOTIFY_ON_ERROR,
303
+ KV_TELEGRAM_ALLOWED_CHAT_IDS,
302
304
  KV_WEBHOOK_IP_CHECK_ENABLED,
303
305
  KV_WEBHOOK_ALLOWED_IPS,
304
306
  KV_WEBHOOK_QUEUE_MODE,
@@ -686,8 +688,10 @@ function toError(err, fallback = "Unknown error") {
686
688
  return err.message;
687
689
  if (typeof err === "string")
688
690
  return err;
689
- if (err && typeof err === "object" && "message" in err && typeof err.message === "string") {
690
- return err.message;
691
+ if (err && typeof err === "object" && "message" in err) {
692
+ const message = err.message;
693
+ if (typeof message === "string")
694
+ return message;
691
695
  }
692
696
  if (err === null || err === undefined)
693
697
  return fallback;
@@ -1773,14 +1777,16 @@ var TradeActionSchema = z2.enum([
1773
1777
  "CLOSE_LONG",
1774
1778
  "CLOSE_SHORT"
1775
1779
  ]);
1780
+ var MAX_WEBHOOK_LEVERAGE = 125;
1781
+ var MAX_WEBHOOK_QUANTITY = 1e6;
1776
1782
  var WebhookPayloadSchema = z2.object({
1777
1783
  exchange: z2.string().min(1),
1778
1784
  action: TradeActionSchema,
1779
1785
  symbol: z2.string().min(1).max(20),
1780
- quantity: z2.number().positive().finite(),
1786
+ quantity: z2.number().positive().finite().max(MAX_WEBHOOK_QUANTITY),
1781
1787
  price: z2.number().positive().finite().optional(),
1782
1788
  orderType: z2.string().optional(),
1783
- leverage: z2.number().int().positive().optional(),
1789
+ leverage: z2.number().int().positive().max(MAX_WEBHOOK_LEVERAGE).optional(),
1784
1790
  test: z2.boolean().optional()
1785
1791
  }).strict();
1786
1792
  var TradeQueueMessageSchema = z2.object({
@@ -1788,9 +1794,9 @@ var TradeQueueMessageSchema = z2.object({
1788
1794
  exchange: z2.string().min(1).max(64),
1789
1795
  action: TradeActionSchema,
1790
1796
  symbol: z2.string().min(1).max(32),
1791
- quantity: z2.number().positive().finite(),
1797
+ quantity: z2.number().positive().finite().max(MAX_WEBHOOK_QUANTITY),
1792
1798
  price: z2.number().positive().finite().optional(),
1793
- leverage: z2.number().int().positive().max(125).optional(),
1799
+ leverage: z2.number().int().positive().max(MAX_WEBHOOK_LEVERAGE).optional(),
1794
1800
  test: z2.boolean().optional(),
1795
1801
  idempotencyKey: z2.string().min(1).max(512).optional(),
1796
1802
  queuedAt: z2.string().min(1).max(64)
@@ -1880,6 +1886,53 @@ async function logKvTimestamp(env, prefix = "timestamp") {
1880
1886
  console.error(`Failed to log timestamp to KV: ${toError(error, "Unknown error")}`);
1881
1887
  }
1882
1888
  }
1889
+ var KV_BULK_GET_MAX_KEYS = 100;
1890
+ async function tryBulkGetChunk(kv, chunk) {
1891
+ try {
1892
+ const result = await kv.get([...chunk]);
1893
+ if (!(result instanceof Map)) {
1894
+ return null;
1895
+ }
1896
+ return chunk.map((key) => result.get(key) ?? null);
1897
+ } catch {
1898
+ return null;
1899
+ }
1900
+ }
1901
+ async function kvGetMany(kv, keys) {
1902
+ if (keys.length === 0)
1903
+ return [];
1904
+ const out = new Array(keys.length);
1905
+ let offset = 0;
1906
+ while (offset < keys.length) {
1907
+ const chunk = keys.slice(offset, offset + KV_BULK_GET_MAX_KEYS);
1908
+ const bulk = await tryBulkGetChunk(kv, chunk);
1909
+ if (bulk === null) {
1910
+ const rest = await Promise.all(keys.slice(offset).map((key) => kv.get(key)));
1911
+ for (let i = 0;i < rest.length; i++) {
1912
+ out[offset + i] = rest[i] ?? null;
1913
+ }
1914
+ return out;
1915
+ }
1916
+ for (let i = 0;i < bulk.length; i++) {
1917
+ out[offset + i] = bulk[i] ?? null;
1918
+ }
1919
+ offset += chunk.length;
1920
+ }
1921
+ return out;
1922
+ }
1923
+ async function kvGetManyAsRecord(kv, keys) {
1924
+ const values = await kvGetMany(kv, keys);
1925
+ const out = {};
1926
+ for (let i = 0;i < keys.length; i++) {
1927
+ out[keys[i]] = values[i] ?? null;
1928
+ }
1929
+ return out;
1930
+ }
1931
+ async function kvPutMany(kv, entries) {
1932
+ if (entries.length === 0)
1933
+ return;
1934
+ await Promise.all(entries.map((entry) => kv.put(entry.key, entry.value, entry.options)));
1935
+ }
1883
1936
  function headersToObject(headers) {
1884
1937
  const result = {};
1885
1938
  if (!headers)
@@ -1895,29 +1948,60 @@ function headersToObject(headers) {
1895
1948
  }
1896
1949
 
1897
1950
  // src/queue-handler.ts
1951
+ async function mapPool(items, concurrency, fn) {
1952
+ if (items.length === 0)
1953
+ return;
1954
+ const limit = Math.min(Math.max(1, concurrency), items.length);
1955
+ if (limit === 1) {
1956
+ for (const item of items) {
1957
+ await fn(item);
1958
+ }
1959
+ return;
1960
+ }
1961
+ let nextIndex = 0;
1962
+ async function worker() {
1963
+ while (true) {
1964
+ const i = nextIndex++;
1965
+ if (i >= items.length)
1966
+ return;
1967
+ await fn(items[i]);
1968
+ }
1969
+ }
1970
+ await Promise.all(Array.from({ length: limit }, () => worker()));
1971
+ }
1898
1972
  function createQueueHandler(options) {
1899
- const { maxRetries, backoffDelays, onMessage, onRetry, onDLQ, logger } = options;
1900
- return async (batch) => {
1901
- for (const msg of batch.messages) {
1902
- const attemptNumber = msg.attempts || 0;
1903
- const logId = `[${msg.id}]`;
1904
- try {
1905
- logger?.info(`${logId} Processing message (attempt ${attemptNumber + 1})`);
1906
- await onMessage(msg.body, attemptNumber);
1907
- logger?.info(`${logId} Message processed successfully`);
1908
- } catch (error) {
1909
- const errorMsg = error instanceof Error ? error.message : String(error);
1910
- if (attemptNumber < maxRetries) {
1911
- const delaySeconds = backoffDelays[attemptNumber] ?? backoffDelays[backoffDelays.length - 1];
1912
- logger?.info(`${logId} Retrying in ${delaySeconds}s (attempt ${attemptNumber + 2}/${maxRetries + 1})`);
1913
- await onRetry?.(msg.body, attemptNumber, errorMsg, delaySeconds);
1914
- msg.retry({ delaySeconds });
1915
- } else {
1916
- logger?.error(`${logId} Max retries exceeded (${maxRetries + 1} attempts), moving to DLQ`);
1917
- await onDLQ?.(msg.body, attemptNumber, errorMsg);
1918
- }
1973
+ const {
1974
+ maxRetries,
1975
+ backoffDelays,
1976
+ onMessage,
1977
+ onRetry,
1978
+ onDLQ,
1979
+ logger,
1980
+ concurrency: concurrencyOpt
1981
+ } = options;
1982
+ const concurrency = typeof concurrencyOpt === "number" && Number.isFinite(concurrencyOpt) && concurrencyOpt >= 1 ? Math.floor(concurrencyOpt) : 1;
1983
+ async function processMessage(msg) {
1984
+ const attemptNumber = msg.attempts || 0;
1985
+ const logId = `[${msg.id}]`;
1986
+ try {
1987
+ logger?.info(`${logId} Processing message (attempt ${attemptNumber + 1})`);
1988
+ await onMessage(msg.body, attemptNumber);
1989
+ logger?.info(`${logId} Message processed successfully`);
1990
+ } catch (error) {
1991
+ const errorMsg = error instanceof Error ? error.message : String(error);
1992
+ if (attemptNumber < maxRetries) {
1993
+ const delaySeconds = backoffDelays[attemptNumber] ?? backoffDelays[backoffDelays.length - 1] ?? 0;
1994
+ logger?.info(`${logId} Retrying in ${delaySeconds}s (attempt ${attemptNumber + 2}/${maxRetries + 1})`);
1995
+ await onRetry?.(msg.body, attemptNumber, errorMsg, delaySeconds);
1996
+ msg.retry({ delaySeconds });
1997
+ } else {
1998
+ logger?.error(`${logId} Max retries exceeded (${maxRetries + 1} attempts), moving to DLQ`);
1999
+ await onDLQ?.(msg.body, attemptNumber, errorMsg);
1919
2000
  }
1920
2001
  }
2002
+ }
2003
+ return async (batch) => {
2004
+ await mapPool(batch.messages, concurrency, processMessage);
1921
2005
  };
1922
2006
  }
1923
2007
 
@@ -1929,12 +2013,24 @@ function timingSafeEqual(a, b) {
1929
2013
  const len = Math.max(aBuf.length, bBuf.length);
1930
2014
  let result = aBuf.length === bBuf.length ? 0 : 1;
1931
2015
  for (let i = 0;i < len; i++) {
1932
- const av = i < aBuf.length ? aBuf[i] : 0;
1933
- const bv = i < bBuf.length ? bBuf[i] : 0;
2016
+ const av = aBuf[i] ?? 0;
2017
+ const bv = bBuf[i] ?? 0;
1934
2018
  result |= av ^ bv;
1935
2019
  }
1936
2020
  return result === 0;
1937
2021
  }
2022
+ async function timingSafeEqualAsync(a, b) {
2023
+ const subtle = globalThis.crypto?.subtle;
2024
+ if (!subtle || typeof subtle.digest !== "function" || typeof subtle.timingSafeEqual !== "function") {
2025
+ return timingSafeEqual(a, b);
2026
+ }
2027
+ const encoder = new TextEncoder;
2028
+ const [providedHash, expectedHash] = await Promise.all([
2029
+ subtle.digest("SHA-256", encoder.encode(a)),
2030
+ subtle.digest("SHA-256", encoder.encode(b))
2031
+ ]);
2032
+ return subtle.timingSafeEqual(providedHash, expectedHash);
2033
+ }
1938
2034
  function resolveOperatorApiKey(env) {
1939
2035
  const preferred = env.OPERATOR_API_KEY;
1940
2036
  if (typeof preferred === "string" && preferred.length > 0) {
@@ -1959,7 +2055,7 @@ async function requireOperatorAuth(request, env) {
1959
2055
  }
1960
2056
  const authHeader = request.headers.get("Authorization");
1961
2057
  const expectedHeader = `Bearer ${apiKey}`;
1962
- if (!authHeader || !timingSafeEqual(authHeader, expectedHeader)) {
2058
+ if (!authHeader || !await timingSafeEqualAsync(authHeader, expectedHeader)) {
1963
2059
  return new Response(JSON.stringify({ error: "Unauthorized" }), {
1964
2060
  status: 401,
1965
2061
  headers: { "Content-Type": "application/json" }
@@ -1992,12 +2088,13 @@ function collectInternalAuthKeys(env, keyNames) {
1992
2088
  return keys;
1993
2089
  }
1994
2090
  function matchesAnyInternalAuthKey(providedKey, expectedKeys) {
2091
+ let matched = false;
1995
2092
  for (const expected of expectedKeys) {
1996
2093
  if (timingSafeEqual(providedKey, expected)) {
1997
- return true;
2094
+ matched = true;
1998
2095
  }
1999
2096
  }
2000
- return false;
2097
+ return matched;
2001
2098
  }
2002
2099
  function internalAuthNotConfiguredResponse(keyNames) {
2003
2100
  const label = normalizeKeyNames(keyNames).join(" | ");
@@ -2236,7 +2333,13 @@ function createMockEnv(overrides) {
2236
2333
  }
2237
2334
 
2238
2335
  // src/path-utils.ts
2239
- import { existsSync as existsSync3 } from "fs";
2336
+ import {
2337
+ existsSync as existsSync3,
2338
+ mkdirSync as mkdirSync2,
2339
+ readFileSync as readFileSync3,
2340
+ writeFileSync as writeFileSync2,
2341
+ chmodSync as chmodSync2
2342
+ } from "fs";
2240
2343
  import { homedir as homedir2 } from "os";
2241
2344
  import { dirname, join as join3, resolve } from "path";
2242
2345
  function createHooxPath(path) {
@@ -2262,7 +2365,55 @@ function isHooxSetupRoot(dir) {
2262
2365
  return false;
2263
2366
  try {
2264
2367
  const root = resolve(dir);
2265
- return existsSync3(join3(root, "wrangler.jsonc")) && existsSync3(join3(root, "packages", "cli", "package.json"));
2368
+ if (!existsSync3(join3(root, "packages", "cli", "package.json"))) {
2369
+ return false;
2370
+ }
2371
+ return existsSync3(join3(root, "wrangler.jsonc")) || existsSync3(join3(root, "wrangler.jsonc.example")) || existsSync3(join3(root, "workers")) || existsSync3(join3(root, ".gitmodules"));
2372
+ } catch {
2373
+ return false;
2374
+ }
2375
+ }
2376
+ function getRememberedMonorepoPath() {
2377
+ return resolveHooxPath("config/monorepo.json");
2378
+ }
2379
+ function getRememberedMonorepoRoot() {
2380
+ try {
2381
+ const path = getRememberedMonorepoPath();
2382
+ if (!existsSync3(path))
2383
+ return null;
2384
+ const raw = readFileSync3(path, "utf-8");
2385
+ const data = JSON.parse(raw);
2386
+ if (!data.root || typeof data.root !== "string")
2387
+ return null;
2388
+ const resolved = resolve(data.root);
2389
+ return isHooxSetupRoot(resolved) ? resolved : null;
2390
+ } catch {
2391
+ return null;
2392
+ }
2393
+ }
2394
+ function rememberMonorepoRoot(root) {
2395
+ try {
2396
+ const resolved = resolve(root);
2397
+ if (!isHooxSetupRoot(resolved))
2398
+ return false;
2399
+ const configDir = getHooxConfigDir();
2400
+ mkdirSync2(configDir, { recursive: true, mode: 448 });
2401
+ try {
2402
+ chmodSync2(configDir, 448);
2403
+ } catch {}
2404
+ const payload = {
2405
+ root: resolved,
2406
+ updatedAt: new Date().toISOString()
2407
+ };
2408
+ const path = getRememberedMonorepoPath();
2409
+ writeFileSync2(path, JSON.stringify(payload, null, 2) + `
2410
+ `, {
2411
+ mode: 384
2412
+ });
2413
+ try {
2414
+ chmodSync2(path, 384);
2415
+ } catch {}
2416
+ return true;
2266
2417
  } catch {
2267
2418
  return false;
2268
2419
  }
@@ -2282,6 +2433,7 @@ function resolveHooxRuntimeRoot(options) {
2282
2433
  const env = options?.env ?? process.env;
2283
2434
  const cwd = resolve(options?.cwd ?? process.cwd());
2284
2435
  const globalRepo = getHooxRepoPath();
2436
+ const remembered = options?.skipRemembered ? null : getRememberedMonorepoRoot();
2285
2437
  const envRepo = env.HOOX_REPO?.trim();
2286
2438
  if (envRepo) {
2287
2439
  const resolved = resolve(envRepo);
@@ -2292,6 +2444,7 @@ function resolveHooxRuntimeRoot(options) {
2292
2444
  checked: {
2293
2445
  env: resolved,
2294
2446
  cwd: findHooxSetupRoot(cwd),
2447
+ remembered,
2295
2448
  global: globalRepo
2296
2449
  }
2297
2450
  };
@@ -2302,6 +2455,7 @@ function resolveHooxRuntimeRoot(options) {
2302
2455
  checked: {
2303
2456
  env: resolved,
2304
2457
  cwd: findHooxSetupRoot(cwd),
2458
+ remembered,
2305
2459
  global: globalRepo
2306
2460
  }
2307
2461
  };
@@ -2311,20 +2465,27 @@ function resolveHooxRuntimeRoot(options) {
2311
2465
  return {
2312
2466
  root: local,
2313
2467
  source: "cwd",
2314
- checked: { cwd: local, global: globalRepo }
2468
+ checked: { cwd: local, remembered, global: globalRepo }
2469
+ };
2470
+ }
2471
+ if (remembered) {
2472
+ return {
2473
+ root: remembered,
2474
+ source: "remembered",
2475
+ checked: { cwd: null, remembered, global: globalRepo }
2315
2476
  };
2316
2477
  }
2317
2478
  if (isHooxSetupRoot(globalRepo)) {
2318
2479
  return {
2319
2480
  root: globalRepo,
2320
2481
  source: "global",
2321
- checked: { cwd: null, global: globalRepo }
2482
+ checked: { cwd: null, remembered: null, global: globalRepo }
2322
2483
  };
2323
2484
  }
2324
2485
  return {
2325
2486
  root: null,
2326
2487
  source: "none",
2327
- checked: { cwd: null, global: globalRepo }
2488
+ checked: { cwd: null, remembered: null, global: globalRepo }
2328
2489
  };
2329
2490
  }
2330
2491
  function getTuiEntryCandidates(runtimeRoot) {
@@ -2496,7 +2657,7 @@ var PRESETS = [
2496
2657
  "analytics-worker",
2497
2658
  "telegram-worker"
2498
2659
  ],
2499
- integrations: ["binance", "telegram"]
2660
+ integrations: ["exchange", "telegram"]
2500
2661
  },
2501
2662
  {
2502
2663
  name: "full",
@@ -2513,15 +2674,7 @@ var PRESETS = [
2513
2674
  "web3-wallet-worker",
2514
2675
  "pyne-worker"
2515
2676
  ],
2516
- integrations: [
2517
- "binance",
2518
- "bybit",
2519
- "mexc",
2520
- "telegram",
2521
- "openai",
2522
- "wallet",
2523
- "pyne"
2524
- ]
2677
+ integrations: ["exchange", "telegram", "openai", "wallet", "pyne"]
2525
2678
  }
2526
2679
  ];
2527
2680
  var WORKER_DEPENDENCIES = {
@@ -2553,30 +2706,12 @@ function resolveDependencies(selected) {
2553
2706
  }
2554
2707
  var INTEGRATIONS = [
2555
2708
  {
2556
- key: "binance",
2557
- label: "Binance Exchange",
2558
- workerName: "trade-worker",
2559
- secrets: {
2560
- BINANCE_KEY_BINDING: "Binance API Key",
2561
- BINANCE_SECRET_BINDING: "Binance API Secret"
2562
- }
2563
- },
2564
- {
2565
- key: "mexc",
2566
- label: "MEXC Exchange",
2709
+ key: "exchange",
2710
+ label: "Exchange API (Binance / Bybit / MEXC)",
2567
2711
  workerName: "trade-worker",
2568
2712
  secrets: {
2569
- MEXC_KEY_BINDING: "MEXC API Key",
2570
- MEXC_SECRET_BINDING: "MEXC API Secret"
2571
- }
2572
- },
2573
- {
2574
- key: "bybit",
2575
- label: "Bybit Exchange",
2576
- workerName: "trade-worker",
2577
- secrets: {
2578
- BYBIT_KEY_BINDING: "Bybit API Key",
2579
- BYBIT_SECRET_BINDING: "Bybit API Secret"
2713
+ EXCHANGE_KEY_BINDING: "Exchange API Key",
2714
+ EXCHANGE_SECRET_BINDING: "Exchange API Secret"
2580
2715
  }
2581
2716
  },
2582
2717
  {
@@ -2806,7 +2941,11 @@ class WizardEngine {
2806
2941
  return { ...this.state };
2807
2942
  }
2808
2943
  getCurrentStep() {
2809
- return STEPS.find((s) => s.id === this.state.step) ?? STEPS[0];
2944
+ const step = STEPS.find((s) => s.id === this.state.step) ?? STEPS[0];
2945
+ if (!step) {
2946
+ throw new Error("Wizard has no steps defined");
2947
+ }
2948
+ return step;
2810
2949
  }
2811
2950
  getCompletedSteps() {
2812
2951
  return [...this.state.completedSteps];
@@ -2829,8 +2968,11 @@ class WizardEngine {
2829
2968
  this.state.completedSteps.push(this.state.step);
2830
2969
  }
2831
2970
  const currentIdx = STEP_ORDER.indexOf(this.state.step);
2832
- if (currentIdx < STEP_ORDER.length - 1) {
2833
- this.state.step = STEP_ORDER[currentIdx + 1];
2971
+ if (currentIdx >= 0 && currentIdx < STEP_ORDER.length - 1) {
2972
+ const nextStep = STEP_ORDER[currentIdx + 1];
2973
+ if (nextStep !== undefined) {
2974
+ this.state.step = nextStep;
2975
+ }
2834
2976
  }
2835
2977
  this.state.updatedAt = Date.now();
2836
2978
  return [];
@@ -2841,6 +2983,8 @@ class WizardEngine {
2841
2983
  const currentIdx = STEP_ORDER.indexOf(this.state.step);
2842
2984
  if (currentIdx > 0) {
2843
2985
  const previousStep = STEP_ORDER[currentIdx - 1];
2986
+ if (previousStep === undefined)
2987
+ return;
2844
2988
  this.state.step = previousStep;
2845
2989
  this.state.completedSteps = this.state.completedSteps.filter((s) => s !== this.state.step || STEP_ORDER.indexOf(s) <= STEP_ORDER.indexOf(previousStep));
2846
2990
  }
@@ -2998,9 +3142,12 @@ function deriveCalledBy(manifests) {
2998
3142
  if (!calledBy[workerName])
2999
3143
  calledBy[workerName] = new Set;
3000
3144
  for (const svc of manifest.services) {
3001
- if (!calledBy[svc.service])
3002
- calledBy[svc.service] = new Set;
3003
- calledBy[svc.service].add(workerName);
3145
+ let callers = calledBy[svc.service];
3146
+ if (!callers) {
3147
+ callers = new Set;
3148
+ calledBy[svc.service] = callers;
3149
+ }
3150
+ callers.add(workerName);
3004
3151
  }
3005
3152
  }
3006
3153
  const result = {};
@@ -3013,6 +3160,7 @@ var manifests = {
3013
3160
  hoox: {
3014
3161
  name: "hoox",
3015
3162
  path: "workers/hoox-worker",
3163
+ defaultEnabled: true,
3016
3164
  vars: {
3017
3165
  WEBHOOK_API_KEY_BINDING: {
3018
3166
  type: "secret",
@@ -3022,7 +3170,11 @@ var manifests = {
3022
3170
  type: "secret",
3023
3171
  description: "Inter-worker auth key"
3024
3172
  },
3025
- HA_TOKEN_BINDING: { type: "secret", description: "Home Assistant token" }
3173
+ HA_TOKEN_BINDING: { type: "secret", description: "Home Assistant token" },
3174
+ TELEGRAM_ALLOWED_CHAT_IDS: {
3175
+ type: "secret",
3176
+ description: "Comma-separated chat IDs for webhook notify (fail-closed when unset); alias AUTHORIZED_CHAT_IDS"
3177
+ }
3026
3178
  },
3027
3179
  services: [
3028
3180
  {
@@ -3048,7 +3200,8 @@ var manifests = {
3048
3200
  ai: true,
3049
3201
  queues: { producer: ["trade-execution"] },
3050
3202
  durableObjects: [
3051
- { name: "IDEMPOTENCY_STORE", className: "IdempotencyStore" }
3203
+ { name: "IDEMPOTENCY_STORE", className: "IdempotencyStore" },
3204
+ { name: "RATE_LIMITER", className: "RateLimiterStore" }
3052
3205
  ]
3053
3206
  },
3054
3207
  middleware: [
@@ -3063,6 +3216,7 @@ var manifests = {
3063
3216
  "trade-worker": {
3064
3217
  name: "trade-worker",
3065
3218
  path: "workers/trade-worker",
3219
+ defaultEnabled: true,
3066
3220
  vars: {
3067
3221
  INTERNAL_KEY_BINDING: {
3068
3222
  type: "secret",
@@ -3072,15 +3226,22 @@ var manifests = {
3072
3226
  type: "secret",
3073
3227
  description: "Telegram outbound auth"
3074
3228
  },
3075
- BINANCE_KEY_BINDING: { type: "secret", description: "Binance API key" },
3076
- BINANCE_SECRET_BINDING: {
3229
+ EXCHANGE_KEY_BINDING: {
3230
+ type: "secret",
3231
+ description: "Unified exchange API key (Binance / Bybit / MEXC)"
3232
+ },
3233
+ EXCHANGE_SECRET_BINDING: {
3234
+ type: "secret",
3235
+ description: "Unified exchange API secret"
3236
+ },
3237
+ EXCHANGE_TESTNET_KEY_BINDING: {
3077
3238
  type: "secret",
3078
- description: "Binance API secret"
3239
+ description: "Optional unified testnet API key"
3079
3240
  },
3080
- MEXC_KEY_BINDING: { type: "secret", description: "MEXC API key" },
3081
- MEXC_SECRET_BINDING: { type: "secret", description: "MEXC API secret" },
3082
- BYBIT_KEY_BINDING: { type: "secret", description: "Bybit API key" },
3083
- BYBIT_SECRET_BINDING: { type: "secret", description: "Bybit API secret" }
3241
+ EXCHANGE_TESTNET_SECRET_BINDING: {
3242
+ type: "secret",
3243
+ description: "Optional unified testnet API secret"
3244
+ }
3084
3245
  },
3085
3246
  services: [
3086
3247
  {
@@ -3122,6 +3283,7 @@ var manifests = {
3122
3283
  "telegram-worker": {
3123
3284
  name: "telegram-worker",
3124
3285
  path: "workers/telegram-worker",
3286
+ defaultEnabled: true,
3125
3287
  vars: {
3126
3288
  INTERNAL_KEY_BINDING: {
3127
3289
  type: "secret",
@@ -3135,6 +3297,10 @@ var manifests = {
3135
3297
  TELEGRAM_SECRET_TOKEN: {
3136
3298
  type: "secret",
3137
3299
  description: "Webhook verification token"
3300
+ },
3301
+ AUTHORIZED_CHAT_IDS: {
3302
+ type: "secret",
3303
+ description: "Comma-separated chat IDs; inbound webhook fail-closed when unset; outbound /alert allowlist"
3138
3304
  }
3139
3305
  },
3140
3306
  services: [],
@@ -3155,6 +3321,7 @@ var manifests = {
3155
3321
  "d1-worker": {
3156
3322
  name: "d1-worker",
3157
3323
  path: "workers/d1-worker",
3324
+ defaultEnabled: true,
3158
3325
  vars: {
3159
3326
  INTERNAL_KEY_BINDING: {
3160
3327
  type: "secret",
@@ -3177,6 +3344,7 @@ var manifests = {
3177
3344
  "web3-wallet-worker": {
3178
3345
  name: "web3-wallet-worker",
3179
3346
  path: "workers/web3-wallet-worker",
3347
+ defaultEnabled: false,
3180
3348
  vars: {
3181
3349
  WALLET_PK_SECRET: { type: "secret", description: "Wallet private key" },
3182
3350
  WALLET_MNEMONIC_SECRET: {
@@ -3197,6 +3365,7 @@ var manifests = {
3197
3365
  "agent-worker": {
3198
3366
  name: "agent-worker",
3199
3367
  path: "workers/agent-worker",
3368
+ defaultEnabled: true,
3200
3369
  vars: {
3201
3370
  AGENT_INTERNAL_KEY: { type: "secret", description: "Agent worker auth" },
3202
3371
  INTERNAL_KEY_BINDING: {
@@ -3233,6 +3402,7 @@ var manifests = {
3233
3402
  "email-worker": {
3234
3403
  name: "email-worker",
3235
3404
  path: "workers/email-worker",
3405
+ defaultEnabled: false,
3236
3406
  vars: {
3237
3407
  INTERNAL_KEY_BINDING: {
3238
3408
  type: "secret",
@@ -3270,6 +3440,7 @@ var manifests = {
3270
3440
  "analytics-worker": {
3271
3441
  name: "analytics-worker",
3272
3442
  path: "workers/analytics-worker",
3443
+ defaultEnabled: true,
3273
3444
  vars: {
3274
3445
  CLOUDFLARE_API_TOKEN: {
3275
3446
  type: "secret",
@@ -3289,6 +3460,7 @@ var manifests = {
3289
3460
  "report-worker": {
3290
3461
  name: "report-worker",
3291
3462
  path: "workers/report-worker",
3463
+ defaultEnabled: false,
3292
3464
  vars: {
3293
3465
  CF_API_TOKEN_BINDING: {
3294
3466
  type: "secret",
@@ -3323,6 +3495,7 @@ var manifests = {
3323
3495
  "pyne-worker": {
3324
3496
  name: "pyne-worker",
3325
3497
  path: "workers/pyne-worker",
3498
+ defaultEnabled: true,
3326
3499
  vars: {
3327
3500
  API_KEY: {
3328
3501
  type: "secret",
@@ -3360,6 +3533,7 @@ var manifests = {
3360
3533
  dashboard: {
3361
3534
  name: "dashboard",
3362
3535
  path: "workers/dashboard",
3536
+ defaultEnabled: true,
3363
3537
  vars: {
3364
3538
  DASHBOARD_USER: {
3365
3539
  type: "secret",
@@ -3402,6 +3576,17 @@ var manifests = {
3402
3576
  var CALLED_BY = deriveCalledBy(manifests);
3403
3577
  var WORKER_MANIFESTS = manifests;
3404
3578
  var WORKER_NAMES = Object.keys(manifests).sort();
3579
+ var WORKER_CATALOG = WORKER_NAMES.map((name) => {
3580
+ const m = manifests[name];
3581
+ return {
3582
+ name: m.name,
3583
+ path: m.path,
3584
+ defaultEnabled: m.defaultEnabled
3585
+ };
3586
+ });
3587
+ function getWorkerDefaultEnabled(name) {
3588
+ return manifests[name]?.defaultEnabled;
3589
+ }
3405
3590
 
3406
3591
  // src/schemas/validators.ts
3407
3592
  import { parse } from "jsonc-parser";
@@ -3441,7 +3626,7 @@ function validateWranglerJsonc(workerName, manifest, jsoncContent) {
3441
3626
  }
3442
3627
  }
3443
3628
  for (const [name, def] of Object.entries(manifest.vars)) {
3444
- if (def.type === "secret" && !name.endsWith("_BINDING") && !name.endsWith("_SECRET") && !name.startsWith("CLOUDFLARE_") && name !== "TELEGRAM_SECRET_TOKEN" && name !== "MAILGUN_API_KEY" && name !== "EMAIL_SCAN_SUBJECT" && name !== "DASHBOARD_USER" && name !== "DASHBOARD_PASS" && name !== "SESSION_SECRET" && name !== "AGENT_INTERNAL_KEY") {
3629
+ if (def.type === "secret" && !name.endsWith("_BINDING") && !name.endsWith("_SECRET") && !name.startsWith("CLOUDFLARE_") && name !== "TELEGRAM_SECRET_TOKEN" && name !== "TELEGRAM_ALLOWED_CHAT_IDS" && name !== "AUTHORIZED_CHAT_IDS" && name !== "MAILGUN_API_KEY" && name !== "EMAIL_SCAN_SUBJECT" && name !== "DASHBOARD_USER" && name !== "DASHBOARD_PASS" && name !== "SESSION_SECRET" && name !== "AGENT_INTERNAL_KEY") {
3445
3630
  errors.push({
3446
3631
  worker: workerName,
3447
3632
  severity: "warning",
@@ -3558,6 +3743,8 @@ function generateWranglerJsonc(manifest) {
3558
3743
  parts.push(` "vars": {`);
3559
3744
  for (const [i, name] of varKeys.entries()) {
3560
3745
  const def = manifest.vars[name];
3746
+ if (!def)
3747
+ continue;
3561
3748
  const comma = i < varKeys.length - 1 ? "," : "";
3562
3749
  const val = def.type === "secret" ? "__SECRET__" : JSON.stringify(def.default ?? "");
3563
3750
  parts.push(` "${name}": ${val}${comma}`);
@@ -3677,7 +3864,8 @@ var initialState = {
3677
3864
  sidebarExpanded: true,
3678
3865
  modal: null,
3679
3866
  commandPaletteOpen: false,
3680
- previousView: null
3867
+ previousView: null,
3868
+ statusErrorExpanded: false
3681
3869
  };
3682
3870
  var useUIStore = create()(immer((set) => ({
3683
3871
  ...initialState,
@@ -3708,6 +3896,12 @@ var useUIStore = create()(immer((set) => ({
3708
3896
  state.activeView = state.previousView;
3709
3897
  state.previousView = null;
3710
3898
  }
3899
+ }),
3900
+ toggleStatusErrorExpanded: () => set((state) => {
3901
+ state.statusErrorExpanded = !state.statusErrorExpanded;
3902
+ }),
3903
+ setStatusErrorExpanded: (expanded) => set((state) => {
3904
+ state.statusErrorExpanded = expanded;
3711
3905
  })
3712
3906
  })));
3713
3907
 
@@ -3730,6 +3924,23 @@ function ringPush(arr, item, max) {
3730
3924
  var tradeStreamAbort = null;
3731
3925
  var logStreamAbort = null;
3732
3926
  var fetchWorkersGeneration = 0;
3927
+ var reconnectTimer = null;
3928
+ function clearReconnectTimer() {
3929
+ if (reconnectTimer !== null) {
3930
+ clearTimeout(reconnectTimer);
3931
+ reconnectTimer = null;
3932
+ }
3933
+ }
3934
+ function scheduleReconnect(delayMs) {
3935
+ clearReconnectTimer();
3936
+ if (delayMs < 0)
3937
+ return;
3938
+ reconnectTimer = setTimeout(() => {
3939
+ reconnectTimer = null;
3940
+ scheduledReconnectTick();
3941
+ }, delayMs);
3942
+ }
3943
+ var scheduledReconnectTick = async () => {};
3733
3944
  var MAX_TRADES = 500;
3734
3945
  var MAX_ALERTS = 100;
3735
3946
  var MAX_LOGS = 1000;
@@ -3753,7 +3964,7 @@ function getBackoffDelay(retryCount) {
3753
3964
  if (retryCount <= 0)
3754
3965
  return 0;
3755
3966
  const index = Math.min(retryCount - 1, BACKOFF_SEQUENCE.length - 1);
3756
- return BACKOFF_SEQUENCE[index];
3967
+ return BACKOFF_SEQUENCE[index] ?? 0;
3757
3968
  }
3758
3969
  var useServiceStore = create2()(immer2((set, get) => ({
3759
3970
  ...initialState2,
@@ -3764,6 +3975,7 @@ var useServiceStore = create2()(immer2((set, get) => ({
3764
3975
  const data = await hooxFetch2("/v1/workers");
3765
3976
  if (gen !== fetchWorkersGeneration)
3766
3977
  return;
3978
+ clearReconnectTimer();
3767
3979
  set((state) => {
3768
3980
  state.workers = data;
3769
3981
  state.lastUpdated = Date.now();
@@ -3789,6 +4001,7 @@ var useServiceStore = create2()(immer2((set, get) => ({
3789
4001
  timestamp: Date.now(),
3790
4002
  acknowledged: false
3791
4003
  });
4004
+ let nextDelay = 0;
3792
4005
  set((state) => {
3793
4006
  state.lastError = msg;
3794
4007
  state.disconnectedAt = state.disconnectedAt ?? Date.now();
@@ -3797,21 +4010,26 @@ var useServiceStore = create2()(immer2((set, get) => ({
3797
4010
  state.retryCount = state.retryCount + 1;
3798
4011
  if (state.retryCount >= MAX_RETRIES2) {
3799
4012
  state.connectionStatus = "offline";
3800
- state.lastError = `Connection lost after ${MAX_RETRIES2} retries`;
4013
+ state.lastError = `${msg} (gave up after ${MAX_RETRIES2} retries)`;
4014
+ state.reconnectDelay = 0;
3801
4015
  } else {
3802
4016
  state.reconnectDelay = getBackoffDelay(state.retryCount);
4017
+ nextDelay = state.reconnectDelay;
3803
4018
  }
3804
4019
  } else if (state.connectionStatus === "reconnecting") {
3805
4020
  state.retryCount = state.retryCount + 1;
3806
4021
  if (state.retryCount >= MAX_RETRIES2) {
3807
4022
  state.connectionStatus = "offline";
3808
- state.lastError = `Connection lost after ${MAX_RETRIES2} retries`;
4023
+ state.lastError = `${msg} (gave up after ${MAX_RETRIES2} retries)`;
3809
4024
  state.reconnectDelay = 0;
3810
4025
  } else {
3811
4026
  state.reconnectDelay = getBackoffDelay(state.retryCount);
4027
+ nextDelay = state.reconnectDelay;
3812
4028
  }
3813
4029
  }
3814
4030
  });
4031
+ if (nextDelay > 0)
4032
+ scheduleReconnect(nextDelay);
3815
4033
  }
3816
4034
  },
3817
4035
  streamTrades: async () => {
@@ -3873,6 +4091,17 @@ var useServiceStore = create2()(immer2((set, get) => ({
3873
4091
  const merged = [...state.alerts, ...alerts];
3874
4092
  state.alerts = merged.length > MAX_ALERTS ? merged.slice(merged.length - MAX_ALERTS) : merged;
3875
4093
  }),
4094
+ acknowledgeAlert: (id) => set((state) => {
4095
+ const hit = state.alerts.find((a) => a.id === id);
4096
+ if (hit)
4097
+ hit.acknowledged = true;
4098
+ }),
4099
+ dismissAlert: (id) => set((state) => {
4100
+ state.alerts = state.alerts.filter((a) => a.id !== id);
4101
+ }),
4102
+ clearLogs: () => set((state) => {
4103
+ state.logs = [];
4104
+ }),
3876
4105
  setConnectionStatus: (status) => set((state) => {
3877
4106
  if (status === "connected") {
3878
4107
  state.retryCount = 0;
@@ -3903,49 +4132,69 @@ var useServiceStore = create2()(immer2((set, get) => ({
3903
4132
  state.metrics = metrics;
3904
4133
  state.lastUpdated = metrics.lastUpdated;
3905
4134
  }),
3906
- handleConnectionSuccess: () => set((state) => {
3907
- state.lastSuccessfulFetch = Date.now();
3908
- state.retryCount = 0;
3909
- state.lastError = null;
3910
- state.lastErrorDetails = null;
3911
- state.reconnectDelay = 0;
3912
- state.disconnectedAt = null;
3913
- state.connectionStatus = "connected";
3914
- }),
3915
- handleConnectionFailure: (errorMessage) => set((state) => {
3916
- state.lastError = errorMessage;
3917
- state.disconnectedAt = state.disconnectedAt ?? Date.now();
3918
- if (state.connectionStatus === "connected" || state.connectionStatus === "polling") {
3919
- state.connectionStatus = "reconnecting";
3920
- state.retryCount = 1;
3921
- state.reconnectDelay = getBackoffDelay(1);
3922
- } else if (state.connectionStatus === "reconnecting") {
3923
- state.retryCount = state.retryCount + 1;
3924
- if (state.retryCount >= MAX_RETRIES2) {
3925
- state.connectionStatus = "offline";
3926
- state.lastError = `Connection lost after ${MAX_RETRIES2} retries`;
3927
- state.reconnectDelay = 0;
3928
- } else {
3929
- state.reconnectDelay = getBackoffDelay(state.retryCount);
4135
+ handleConnectionSuccess: () => {
4136
+ clearReconnectTimer();
4137
+ set((state) => {
4138
+ state.lastSuccessfulFetch = Date.now();
4139
+ state.retryCount = 0;
4140
+ state.lastError = null;
4141
+ state.lastErrorDetails = null;
4142
+ state.reconnectDelay = 0;
4143
+ state.disconnectedAt = null;
4144
+ state.connectionStatus = "connected";
4145
+ });
4146
+ },
4147
+ handleConnectionFailure: (errorMessage) => {
4148
+ let nextDelay = 0;
4149
+ set((state) => {
4150
+ state.lastError = errorMessage;
4151
+ state.disconnectedAt = state.disconnectedAt ?? Date.now();
4152
+ if (state.connectionStatus === "connected" || state.connectionStatus === "polling") {
4153
+ state.connectionStatus = "reconnecting";
4154
+ state.retryCount = 1;
4155
+ state.reconnectDelay = getBackoffDelay(1);
4156
+ nextDelay = state.reconnectDelay;
4157
+ } else if (state.connectionStatus === "reconnecting") {
4158
+ state.retryCount = state.retryCount + 1;
4159
+ if (state.retryCount >= MAX_RETRIES2) {
4160
+ state.connectionStatus = "offline";
4161
+ state.lastError = `${errorMessage} (gave up after ${MAX_RETRIES2} retries)`;
4162
+ state.reconnectDelay = 0;
4163
+ } else {
4164
+ state.reconnectDelay = getBackoffDelay(state.retryCount);
4165
+ nextDelay = state.reconnectDelay;
4166
+ }
3930
4167
  }
3931
- }
3932
- }),
3933
- resetRetries: () => set((state) => {
3934
- state.retryCount = 0;
3935
- state.lastError = null;
3936
- state.lastErrorDetails = null;
3937
- state.reconnectDelay = 0;
3938
- state.disconnectedAt = null;
3939
- }),
3940
- forceRetry: () => set((state) => {
3941
- if (state.connectionStatus === "offline") {
3942
- state.connectionStatus = "polling";
4168
+ });
4169
+ if (nextDelay > 0)
4170
+ scheduleReconnect(nextDelay);
4171
+ },
4172
+ resetRetries: () => {
4173
+ clearReconnectTimer();
4174
+ set((state) => {
3943
4175
  state.retryCount = 0;
3944
4176
  state.lastError = null;
3945
4177
  state.lastErrorDetails = null;
3946
4178
  state.reconnectDelay = 0;
4179
+ state.disconnectedAt = null;
4180
+ });
4181
+ },
4182
+ forceRetry: () => {
4183
+ clearReconnectTimer();
4184
+ const wasOffline = get().connectionStatus === "offline";
4185
+ set((state) => {
4186
+ if (state.connectionStatus === "offline") {
4187
+ state.connectionStatus = "polling";
4188
+ state.retryCount = 0;
4189
+ state.lastError = null;
4190
+ state.lastErrorDetails = null;
4191
+ state.reconnectDelay = 0;
4192
+ }
4193
+ });
4194
+ if (wasOffline) {
4195
+ scheduleReconnect(0);
3947
4196
  }
3948
- }),
4197
+ },
3949
4198
  setLastErrorDetails: (details) => set((state) => {
3950
4199
  state.lastErrorDetails = details;
3951
4200
  state.lastError = details === null ? null : summarizeCliError(details);
@@ -3968,38 +4217,73 @@ var useServiceStore = create2()(immer2((set, get) => ({
3968
4217
  }, MAX_ALERTS);
3969
4218
  })
3970
4219
  })));
4220
+ scheduledReconnectTick = async () => {
4221
+ const status = useServiceStore.getState().connectionStatus;
4222
+ if (status === "reconnecting" || status === "polling") {
4223
+ await useServiceStore.getState().fetchWorkers();
4224
+ }
4225
+ };
3971
4226
 
3972
4227
  // src/stores/config-store.ts
3973
4228
  import { create as create3 } from "zustand";
3974
4229
  import { immer as immer3 } from "zustand/middleware/immer";
3975
4230
  import { persist, createJSONStorage } from "zustand/middleware";
3976
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
3977
- import { homedir as homedir3 } from "os";
4231
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync3, existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
3978
4232
  import { join as join4 } from "path";
4233
+ function getTuiPreferencesPath() {
4234
+ return join4(getHooxHome(), ".tui-state", "preferences.json");
4235
+ }
4236
+ function getLegacyConfigJsonPath() {
4237
+ return join4(getHooxHome(), "config.json");
4238
+ }
4239
+ function extractZustandPrefsPayload(raw) {
4240
+ try {
4241
+ const parsed = JSON.parse(raw);
4242
+ if (parsed && typeof parsed === "object" && parsed.state && typeof parsed.state === "object" && !Array.isArray(parsed.state) && (("theme" in parsed.state) || ("refreshIntervalMs" in parsed.state)) && !("apiToken" in parsed)) {
4243
+ return raw;
4244
+ }
4245
+ } catch {}
4246
+ return null;
4247
+ }
3979
4248
  var bunConfigStorage = {
3980
4249
  getItem: async (_name) => {
3981
4250
  try {
3982
- const file = Bun.file(join4(homedir3(), ".hoox", "config.json"));
3983
- const exists = await file.exists();
3984
- if (!exists)
3985
- return null;
3986
- return await file.text();
4251
+ const prefsPath = getTuiPreferencesPath();
4252
+ const prefsFile = Bun.file(prefsPath);
4253
+ if (await prefsFile.exists()) {
4254
+ return await prefsFile.text();
4255
+ }
4256
+ const legacyPath = getLegacyConfigJsonPath();
4257
+ if (existsSync4(legacyPath)) {
4258
+ const legacyRaw = readFileSync4(legacyPath, "utf-8");
4259
+ const envelope = extractZustandPrefsPayload(legacyRaw);
4260
+ if (envelope) {
4261
+ await bunConfigStorage.setItem(_name, envelope);
4262
+ return envelope;
4263
+ }
4264
+ }
4265
+ return null;
3987
4266
  } catch {
3988
4267
  return null;
3989
4268
  }
3990
4269
  },
3991
4270
  setItem: async (_name, value) => {
3992
- const dir = join4(homedir3(), ".hoox");
3993
- if (!existsSync4(dir)) {
3994
- mkdirSync2(dir, { recursive: true, mode: HOOX_DIR_MODE });
4271
+ const home = getHooxHome();
4272
+ const stateDir = join4(home, ".tui-state");
4273
+ if (!existsSync4(home)) {
4274
+ mkdirSync3(home, { recursive: true, mode: HOOX_DIR_MODE });
4275
+ }
4276
+ if (!existsSync4(stateDir)) {
4277
+ mkdirSync3(stateDir, { recursive: true, mode: HOOX_DIR_MODE });
3995
4278
  }
3996
4279
  try {
3997
- chmodSync2(dir, HOOX_DIR_MODE);
4280
+ chmodSync3(home, HOOX_DIR_MODE);
4281
+ chmodSync3(stateDir, HOOX_DIR_MODE);
3998
4282
  } catch {}
3999
- const filePath = join4(dir, "config.json");
4283
+ const filePath = getTuiPreferencesPath();
4000
4284
  await Bun.write(filePath, value);
4001
4285
  try {
4002
- chmodSync2(filePath, HOOX_CONFIG_FILE_MODE);
4286
+ chmodSync3(filePath, HOOX_CONFIG_FILE_MODE);
4003
4287
  } catch {}
4004
4288
  },
4005
4289
  removeItem: async (_name) => {}
@@ -4071,7 +4355,7 @@ var useConfigStore = create3()(persist(immer3((set) => ({
4071
4355
  state.notifications[channel] = !state.notifications[channel];
4072
4356
  })
4073
4357
  })), {
4074
- name: "hoox-config",
4358
+ name: "hoox-tui-prefs",
4075
4359
  storage: createJSONStorage(() => bunConfigStorage),
4076
4360
  partialize: (state) => ({
4077
4361
  theme: state.theme,
@@ -4120,6 +4404,7 @@ export {
4120
4404
  resolveHooxRuntimeRoot,
4121
4405
  resolveHooxPath,
4122
4406
  resolveDependencies,
4407
+ rememberMonorepoRoot,
4123
4408
  readConfigSync,
4124
4409
  readConfig,
4125
4410
  parseDashboardManifest,
@@ -4128,7 +4413,10 @@ export {
4128
4413
  logKvTimestamp,
4129
4414
  loadDashboardManifestsFromRoot,
4130
4415
  loadDashboardKvManifestFromRoot,
4416
+ kvPutMany,
4131
4417
  kvManifestFromDashboardManifests,
4418
+ kvGetManyAsRecord,
4419
+ kvGetMany,
4132
4420
  isWithinHooxHome,
4133
4421
  isUnixModeGroupOrWorldAccessible,
4134
4422
  isTruthyKillSwitchFlag,
@@ -4143,7 +4431,10 @@ export {
4143
4431
  healthCheck,
4144
4432
  headersToObject,
4145
4433
  hasOperatorClientCredentials,
4434
+ getWorkerDefaultEnabled,
4146
4435
  getTuiEntryCandidates,
4436
+ getRememberedMonorepoRoot,
4437
+ getRememberedMonorepoPath,
4147
4438
  getRelativeHooxPath,
4148
4439
  getHooxWranglerPath,
4149
4440
  getHooxStatePath,
@@ -4203,6 +4494,7 @@ export {
4203
4494
  WORKER_NAMES,
4204
4495
  WORKER_MANIFESTS,
4205
4496
  WORKER_DEPENDENCIES,
4497
+ WORKER_CATALOG,
4206
4498
  WIZARD_STATE_PATH,
4207
4499
  WALLET_EXECUTE_AUTH_KEY_FIELDS,
4208
4500
  TradeSignalSchema,
@@ -4217,6 +4509,7 @@ export {
4217
4509
  PositionSchema,
4218
4510
  PRESETS,
4219
4511
  LogLevelColor,
4512
+ KV_BULK_GET_MAX_KEYS,
4220
4513
  KVKeys,
4221
4514
  KILL_SWITCH_KEYS,
4222
4515
  KILL_SWITCH_ACTIVE_PREFIX,