@hoox-sh/hoox-tui 0.2.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 (104) hide show
  1. package/README.md +247 -0
  2. package/bin/hoox-tui.js +35 -0
  3. package/dist/main.js +17594 -0
  4. package/package.json +72 -0
  5. package/src/app.tsx +696 -0
  6. package/src/cli-bridge-test-double.ts +212 -0
  7. package/src/components/layout/layout.test.tsx +205 -0
  8. package/src/components/layout/sidebar.tsx +69 -0
  9. package/src/components/layout/statusbar.test.tsx +423 -0
  10. package/src/components/layout/statusbar.tsx +348 -0
  11. package/src/components/shared/animated-border.tsx +58 -0
  12. package/src/components/shared/chrome.test.tsx +29 -0
  13. package/src/components/shared/command-palette.tsx +361 -0
  14. package/src/components/shared/cool-brackets.tsx +75 -0
  15. package/src/components/shared/crash-screen.tsx +167 -0
  16. package/src/components/shared/error-boundary.tsx +99 -0
  17. package/src/components/shared/keybinding-hint.tsx +37 -0
  18. package/src/components/shared/panel.tsx +61 -0
  19. package/src/components/shared/quit-modal.tsx +65 -0
  20. package/src/components/shared/shared.test.tsx +538 -0
  21. package/src/components/shared/spinner.tsx +85 -0
  22. package/src/components/shared/status-dot.tsx +54 -0
  23. package/src/components/shared/view-header.tsx +53 -0
  24. package/src/components/ui/connection-toasts.test.ts +50 -0
  25. package/src/components/ui/connection-toasts.ts +215 -0
  26. package/src/components/ui/dialog.tsx +281 -0
  27. package/src/components/ui/select.tsx +197 -0
  28. package/src/components/ui/toast.tsx +185 -0
  29. package/src/components/views/ai-chat.test.tsx +81 -0
  30. package/src/components/views/ai-chat.tsx +576 -0
  31. package/src/components/views/config-editor/action-bar.tsx +102 -0
  32. package/src/components/views/config-editor/code-editor.tsx +597 -0
  33. package/src/components/views/config-editor/file-tree.tsx +113 -0
  34. package/src/components/views/config-editor/types.ts +37 -0
  35. package/src/components/views/config-editor.test.tsx +465 -0
  36. package/src/components/views/config-editor.tsx +709 -0
  37. package/src/components/views/dashboard/alerts-panel.tsx +154 -0
  38. package/src/components/views/dashboard/auto-repair-panel.tsx +229 -0
  39. package/src/components/views/dashboard/model-health-section.tsx +276 -0
  40. package/src/components/views/dashboard/pyne-health-section.tsx +112 -0
  41. package/src/components/views/dashboard.test.tsx +347 -0
  42. package/src/components/views/dashboard.tsx +647 -0
  43. package/src/components/views/db-query.test.tsx +115 -0
  44. package/src/components/views/db-query.tsx +849 -0
  45. package/src/components/views/edge-topology.test.tsx +198 -0
  46. package/src/components/views/edge-topology.tsx +572 -0
  47. package/src/components/views/kv-viewer.test.tsx +241 -0
  48. package/src/components/views/kv-viewer.tsx +717 -0
  49. package/src/components/views/logs-viewer.test.tsx +273 -0
  50. package/src/components/views/logs-viewer.tsx +616 -0
  51. package/src/components/views/queue-depth.test.tsx +221 -0
  52. package/src/components/views/queue-depth.tsx +376 -0
  53. package/src/components/views/secrets-viewer.test.tsx +65 -0
  54. package/src/components/views/secrets-viewer.tsx +548 -0
  55. package/src/components/views/service-manager.test.tsx +358 -0
  56. package/src/components/views/service-manager.tsx +946 -0
  57. package/src/components/views/settings.test.tsx +648 -0
  58. package/src/components/views/settings.tsx +1497 -0
  59. package/src/components/views/setup-wizard.test.tsx +297 -0
  60. package/src/components/views/setup-wizard.tsx +1321 -0
  61. package/src/components/views/trade-monitor.test.tsx +592 -0
  62. package/src/components/views/trade-monitor.tsx +745 -0
  63. package/src/components/views/worker-detail.test.tsx +289 -0
  64. package/src/components/views/worker-detail.tsx +676 -0
  65. package/src/components/views/worker-settings.test.tsx +43 -0
  66. package/src/components/views/worker-settings.tsx +563 -0
  67. package/src/components/views/workers-overview.test.tsx +260 -0
  68. package/src/components/views/workers-overview.tsx +631 -0
  69. package/src/hooks/hooks.test.ts +72 -0
  70. package/src/hooks/index.ts +11 -0
  71. package/src/hooks/renderer-ref.ts +22 -0
  72. package/src/hooks/use-keyboard.ts +130 -0
  73. package/src/hooks/use-polling.ts +73 -0
  74. package/src/hooks/use-service-data.ts +18 -0
  75. package/src/main.tsx +117 -0
  76. package/src/network-test-double.ts +133 -0
  77. package/src/opentui-augment.d.ts +45 -0
  78. package/src/opentui-jsx.d.ts +70 -0
  79. package/src/services/cli-bridge/index.ts +1662 -0
  80. package/src/services/cli-bridge/standalone.ts +315 -0
  81. package/src/services/cli-bridge/types.ts +307 -0
  82. package/src/services/cli-bridge.test.ts +406 -0
  83. package/src/services/clipboard.test.ts +57 -0
  84. package/src/services/clipboard.ts +180 -0
  85. package/src/services/dashboard-settings-loader.ts +31 -0
  86. package/src/services/dev-log.test.ts +168 -0
  87. package/src/services/dev-log.ts +194 -0
  88. package/src/services/hoox-path-service.test.ts +183 -0
  89. package/src/services/hoox-path-service.ts +185 -0
  90. package/src/services/tui-connection.test.ts +212 -0
  91. package/src/services/tui-connection.ts +236 -0
  92. package/src/services/tui-storage.test.ts +92 -0
  93. package/src/services/tui-storage.ts +86 -0
  94. package/src/stores/config-store.test.ts +360 -0
  95. package/src/stores/service-store.test.ts +1026 -0
  96. package/src/stores/session-restore.test.ts +134 -0
  97. package/src/stores/ui-store.test.ts +276 -0
  98. package/src/test-setup.ts +265 -0
  99. package/src/test-utils.ts +179 -0
  100. package/src/types.ts +153 -0
  101. package/src/utils/colors.test.ts +326 -0
  102. package/src/utils/formatters.test.ts +236 -0
  103. package/src/view-registry.test.ts +132 -0
  104. package/src/view-registry.tsx +368 -0
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * CliBridge — Standalone exported items extracted from cli-bridge.ts
8
+ *
9
+ * Contains:
10
+ * - validateReadOnlySql + helper constants/functions
11
+ * - AI_MODEL_OPTIONS
12
+ * - agentChatStream + helper constants
13
+ */
14
+ import type {
15
+ SqlValidationResult,
16
+ AiModelOption,
17
+ ChatMessage,
18
+ AgentChatStreamResult,
19
+ } from "./types";
20
+
21
+ // ─── SQL Validation ───────────────────────────────────────────────────────────
22
+
23
+ /**
24
+ * SQL keywords that the TUI's `db-query` view is forbidden from sending.
25
+ *
26
+ * Defends against accidental or malicious DML/DDL via the TUI's read-only
27
+ * SQL input. Detection is case-insensitive and word-boundary aware.
28
+ */
29
+ const FORBIDDEN_SQL_KEYWORDS = [
30
+ "INSERT",
31
+ "UPDATE",
32
+ "DELETE",
33
+ "REPLACE",
34
+ "DROP",
35
+ "CREATE",
36
+ "ALTER",
37
+ "TRUNCATE",
38
+ "PRAGMA",
39
+ "ATTACH",
40
+ "DETACH",
41
+ "VACUUM",
42
+ "REINDEX",
43
+ ] as const;
44
+
45
+ /**
46
+ * SQL keywords permitted as the first non-whitespace, non-comment token.
47
+ *
48
+ * `SELECT` is the canonical read-only entry point. `WITH` is allowed because
49
+ * CTEs (Common Table Expressions) are read-only when followed by a SELECT.
50
+ * `EXPLAIN` is allowed because it is read-only and is useful for query
51
+ * diagnostics.
52
+ */
53
+ const ALLOWED_ENTRY_KEYWORDS = ["SELECT", "WITH", "EXPLAIN"] as const;
54
+
55
+ /** Strip SQL line comments (`-- ...\n`) and block comments (`/* ... *\/`). */
56
+ function stripSqlComments(input: string): string {
57
+ // Block comments: /* ... */ (non-greedy, multi-line)
58
+ const noBlock = input.replace(/\/\*[\s\S]*?\*\//g, " ");
59
+ // Line comments: -- to end-of-line
60
+ const noLine = noBlock.replace(/--[^\n]*/g, " ");
61
+ return noLine;
62
+ }
63
+
64
+ /** Strip single-quoted and double-quoted string literals (rough heuristic). */
65
+ function stripSqlStrings(input: string): string {
66
+ // Single-quoted with doubled '' escapes
67
+ const noSingle = input.replace(/'(?:''|[^'])*'/g, "''");
68
+ // Double-quoted identifiers
69
+ const noDouble = noSingle.replace(/"[^"]*"/g, '""');
70
+ return noDouble;
71
+ }
72
+
73
+ /**
74
+ * Validate that a SQL string is a read-only D1 query safe to send through
75
+ * the TUI's `db-query` view.
76
+ *
77
+ * Rules enforced:
78
+ * 1. Must be non-empty after trimming.
79
+ * 2. First non-whitespace, non-comment, non-string token must be one of
80
+ * {@link ALLOWED_ENTRY_KEYWORDS} (case-insensitive).
81
+ * 3. The statement must not contain any of {@link FORBIDDEN_SQL_KEYWORDS}
82
+ * as a word (case-insensitive, word-boundary aware).
83
+ * 4. At most one `;` is allowed, and only as the trailing terminator
84
+ * (i.e. multi-statement payloads like `SELECT 1; DROP TABLE x` are
85
+ * rejected).
86
+ *
87
+ * The function never throws. Returns a structured result the view can
88
+ * render directly.
89
+ */
90
+ export function validateReadOnlySql(sql: string): SqlValidationResult {
91
+ if (typeof sql !== "string" || sql.trim().length === 0) {
92
+ return { readonly: false, reason: "SQL is empty" };
93
+ }
94
+
95
+ const stripped = stripSqlStrings(stripSqlComments(sql)).trim();
96
+ if (stripped.length === 0) {
97
+ return { readonly: false, reason: "SQL is empty (only comments?)" };
98
+ }
99
+
100
+ // First significant token: take the first whitespace-delimited word.
101
+ const firstTokenMatch = stripped.match(/^[\s(]*([A-Za-z_]+)/);
102
+ const firstToken = firstTokenMatch?.[1]?.toUpperCase() ?? "";
103
+ if (
104
+ !ALLOWED_ENTRY_KEYWORDS.includes(
105
+ firstToken as (typeof ALLOWED_ENTRY_KEYWORDS)[number]
106
+ )
107
+ ) {
108
+ return {
109
+ readonly: false,
110
+ reason: `Only SELECT, WITH, or EXPLAIN queries are allowed (got "${firstToken || "(nothing)"}")`,
111
+ };
112
+ }
113
+
114
+ // Reject any forbidden keyword as a whole word (case-insensitive).
115
+ for (const keyword of FORBIDDEN_SQL_KEYWORDS) {
116
+ const pattern = new RegExp(`\\b${keyword}\\b`, "i");
117
+ if (pattern.test(stripped)) {
118
+ return {
119
+ readonly: false,
120
+ reason: `Forbidden keyword: ${keyword}. Read-only queries only.`,
121
+ };
122
+ }
123
+ }
124
+
125
+ // Reject multiple statements. Allow at most one trailing `;`.
126
+ const semicolons = (stripped.match(/;/g) ?? []).length;
127
+ if (semicolons > 1) {
128
+ return {
129
+ readonly: false,
130
+ reason: "Multiple statements are not allowed. Use a single query.",
131
+ };
132
+ }
133
+ if (semicolons === 1) {
134
+ const lastSemi = stripped.lastIndexOf(";");
135
+ if (lastSemi !== stripped.length - 1) {
136
+ // Allow 1 trailing whitespace after the semicolon, but nothing else.
137
+ const trailing = stripped.slice(lastSemi + 1).trim();
138
+ if (trailing.length > 0) {
139
+ return {
140
+ readonly: false,
141
+ reason: "Semicolons are only allowed at the end of the query.",
142
+ };
143
+ }
144
+ }
145
+ }
146
+
147
+ return { readonly: true };
148
+ }
149
+
150
+ // ─── AI Model Options ─────────────────────────────────────────────────────────
151
+
152
+ /** Default AI model options offered in the chat UI. */
153
+ export const AI_MODEL_OPTIONS: AiModelOption[] = [
154
+ { id: "workers-ai", label: "Workers AI (Llama 3.1)", provider: "cloudflare" },
155
+ {
156
+ id: "openai:gpt-4o-mini",
157
+ label: "OpenAI (GPT-4o-mini)",
158
+ provider: "openai",
159
+ },
160
+ {
161
+ id: "anthropic:claude-3-5-haiku",
162
+ label: "Anthropic (Claude 3.5 Haiku)",
163
+ provider: "anthropic",
164
+ },
165
+ ];
166
+
167
+ // ─── AI Chat SSE Streaming ────────────────────────────────────────────────────
168
+
169
+ /** Reconnection backoff for SSE streaming. */
170
+ const SSE_RECONNECT_BASE_MS = 1_000;
171
+ const SSE_RECONNECT_MAX_MS = 16_000;
172
+ const SSE_MAX_RECONNECT_ATTEMPTS = 5;
173
+
174
+ /**
175
+ * Start an SSE stream to the AI chat endpoint.
176
+ *
177
+ * @param params Chat parameters (messages, model, temperature, etc.)
178
+ * @param apiBase Base URL for the API (default: http://localhost:8787)
179
+ * @param apiToken Bearer token for auth (default: HOOX_API_TOKEN env var)
180
+ * @param onToken Called with each streamed token string fragment
181
+ * @param onStatus Optional callback for connection status updates
182
+ * @returns An AgentChatStreamResult with abort + finished
183
+ */
184
+ export function agentChatStream(
185
+ params: {
186
+ messages: ChatMessage[];
187
+ model?: string;
188
+ temperature?: number;
189
+ maxTokens?: number;
190
+ },
191
+ apiBase: string = "http://localhost:8787",
192
+ apiToken: string = process.env.HOOX_API_TOKEN ?? "",
193
+ onToken?: (token: string) => void,
194
+ onStatus?: (status: "connected" | "reconnecting" | "disconnected") => void
195
+ ): AgentChatStreamResult {
196
+ const abortController = new AbortController();
197
+ let completed = false;
198
+
199
+ const finished = (async () => {
200
+ let attempt = 0;
201
+
202
+ while (
203
+ !abortController.signal.aborted &&
204
+ attempt < SSE_MAX_RECONNECT_ATTEMPTS
205
+ ) {
206
+ try {
207
+ const url = `${apiBase.replace(/\/$/, "")}/api/agent/chat`;
208
+ const body = JSON.stringify({
209
+ messages: params.messages.map((m) => ({
210
+ role: m.role,
211
+ content: m.content,
212
+ })),
213
+ model:
214
+ params.model && params.model !== "workers-ai"
215
+ ? params.model
216
+ : undefined,
217
+ temperature: params.temperature ?? 0.7,
218
+ maxTokens: params.maxTokens ?? 500,
219
+ stream: true,
220
+ });
221
+
222
+ const response = await fetch(url, {
223
+ method: "POST",
224
+ headers: {
225
+ "Content-Type": "application/json",
226
+ Accept: "text/event-stream",
227
+ ...(apiToken ? { Authorization: `Bearer ${apiToken}` } : {}),
228
+ },
229
+ body,
230
+ signal: abortController.signal,
231
+ });
232
+
233
+ if (!response.ok || !response.body) {
234
+ throw new Error(`Chat stream failed: HTTP ${response.status}`);
235
+ }
236
+
237
+ attempt = 0;
238
+ onStatus?.("connected");
239
+
240
+ const reader = response.body.getReader();
241
+ const decoder = new TextDecoder();
242
+ let buffer = "";
243
+
244
+ while (!abortController.signal.aborted) {
245
+ const { done, value } = await reader.read();
246
+ if (done) break;
247
+
248
+ buffer += decoder.decode(value, { stream: true });
249
+ const lines = buffer.split("\n");
250
+ buffer = lines.pop() ?? "";
251
+
252
+ for (const line of lines) {
253
+ const trimmed = line.trim();
254
+ if (trimmed.startsWith("data: ")) {
255
+ const data = trimmed.slice(6);
256
+ if (data === "[DONE]") {
257
+ completed = true;
258
+ break;
259
+ }
260
+ try {
261
+ const parsed = JSON.parse(data) as { content?: string };
262
+ if (parsed.content) {
263
+ onToken?.(parsed.content);
264
+ }
265
+ } catch {
266
+ // Skip malformed events during streaming
267
+ }
268
+ }
269
+ }
270
+ }
271
+
272
+ if (completed || abortController.signal.aborted) {
273
+ break;
274
+ }
275
+ } catch {
276
+ if (abortController.signal.aborted || completed) break;
277
+
278
+ onStatus?.("reconnecting");
279
+ attempt++;
280
+
281
+ if (attempt < SSE_MAX_RECONNECT_ATTEMPTS) {
282
+ const delay = Math.min(
283
+ SSE_RECONNECT_BASE_MS * Math.pow(2, attempt - 1),
284
+ SSE_RECONNECT_MAX_MS
285
+ );
286
+ // Abortable backoff — unmount/cancel must not wait out full delay
287
+ await new Promise<void>((resolve) => {
288
+ if (abortController.signal.aborted) {
289
+ resolve();
290
+ return;
291
+ }
292
+ const timer = setTimeout(resolve, delay);
293
+ abortController.signal.addEventListener(
294
+ "abort",
295
+ () => {
296
+ clearTimeout(timer);
297
+ resolve();
298
+ },
299
+ { once: true }
300
+ );
301
+ });
302
+ }
303
+ }
304
+ }
305
+
306
+ if (!abortController.signal.aborted && !completed) {
307
+ onStatus?.("disconnected");
308
+ }
309
+ })();
310
+
311
+ return {
312
+ abort: abortController,
313
+ finished,
314
+ };
315
+ }
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * CliBridge — Spawns the hoox CLI binary and returns structured results.
8
+ *
9
+ * Provides convenience methods for all CLI operations used by TUI views,
10
+ * with configurable timeout, abort support, and stderr streaming.
11
+ *
12
+ * Every `CliResult` now carries `command` (the exact command string) and
13
+ * `errorType` (a `CliErrorType` classification) so the status bar can
14
+ * surface actionable diagnostics instead of a generic OFFLINE message.
15
+ *
16
+ * Global error sink: callers (e.g. the app root) can register an
17
+ * `onError` listener once via `cliBridge.onError(fn)` and receive a
18
+ * structured `CliErrorDetails` payload for **every** failed command —
19
+ * not just the ones the caller explicitly invoked. This is the wiring
20
+ * point that lets the status bar surface the real diagnostic context
21
+ * for any CLI failure, regardless of which view triggered it.
22
+ */
23
+
24
+ export interface ExecOptions {
25
+ json?: boolean;
26
+ yes?: boolean;
27
+ /** Wall-clock timeout in ms (default 30_000). On expiry the subprocess is killed. */
28
+ timeout?: number;
29
+ onProgress?: (chunk: string) => void;
30
+ tag?: string;
31
+ /**
32
+ * Optional external abort signal (view unmount, user cancel).
33
+ * Combined with the bridge's internal timeout controller — either aborts the spawn.
34
+ */
35
+ signal?: AbortSignal;
36
+ }
37
+
38
+ /**
39
+ * Semantic action name for the kill-switch bridge. The UI speaks engage/release
40
+ * (user-facing verbs), the CLI speaks on/off. We accept the semantic name
41
+ * here and translate internally.
42
+ */
43
+ export type KillSwitchAction = "show" | "engage" | "release";
44
+
45
+ /**
46
+ * Normalized status of the global trade kill switch.
47
+ *
48
+ * Returned by {@link CliBridgeImpl.monitorKillSwitch} so views don't need to
49
+ * parse the CLI's colored output themselves.
50
+ */
51
+ export interface KillSwitchStatus {
52
+ /** True when trading is halted (KV value === "true") */
53
+ engaged: boolean;
54
+ /** Raw KV value as returned by the CLI ("true" | "false" | null when unset) */
55
+ rawValue: string | null;
56
+ /** ISO-8601 timestamp captured client-side when the status was read */
57
+ timestamp: string;
58
+ /** The action that produced this status (helpful for UI feedback) */
59
+ action: KillSwitchAction;
60
+ }
61
+
62
+ /**
63
+ * Backlog pressure for a single Cloudflare Queue.
64
+ *
65
+ * Returned by {@link CliBridgeImpl.monitorQueueDepth} — the shape is fully
66
+ * normalized on the bridge side so views can render directly without
67
+ * needing to parse wrangler's JSON or the CLI's stdout.
68
+ *
69
+ * The `depth` field is a *heuristic estimate* derived from
70
+ * `producers_total_count` (each producer is assumed to enqueue ~100 msgs
71
+ * on average) because Cloudflare Queues does not expose real-time backlog
72
+ * counts via `wrangler queues list`. The view's color coding
73
+ * (green/yellow/red) operates on this estimate.
74
+ *
75
+ * Threshold conventions (used by the view's color coding):
76
+ * - depth < 100 → "healthy"
77
+ * - 100 <= depth <= 500 → "backlogged"
78
+ * - depth > 500 → "critical"
79
+ * - delivery_paused = true → "paused" (overrides depth thresholds)
80
+ * - no data → "unknown"
81
+ */
82
+ export type QueueDepthStatus =
83
+ | "healthy"
84
+ | "backlogged"
85
+ | "critical"
86
+ | "paused"
87
+ | "unknown";
88
+
89
+ /** Per-queue depth info. See {@link QueueDepthStatus} for status semantics. */
90
+ export interface QueueDepth {
91
+ queueName: string;
92
+ depth: number;
93
+ max: number;
94
+ status: QueueDepthStatus;
95
+ /** Cloudflare producer count (informational, view can render) */
96
+ producers: number;
97
+ /** Cloudflare consumer count (informational, view can render) */
98
+ consumers: number;
99
+ /** True when delivery is paused on this queue */
100
+ paused: boolean;
101
+ /** ISO-8601 timestamp when the depth was sampled */
102
+ timestamp: string;
103
+ }
104
+
105
+ /**
106
+ * Normalized result of a read-only D1 SQL query executed through
107
+ * {@link CliBridgeImpl.dbQuery}.
108
+ *
109
+ * Mirrors the wrangler `d1 execute --json` envelope shape but collapses it
110
+ * to what the TUI view actually needs:
111
+ *
112
+ * wrangler:
113
+ * [ { results: [{...}, {...}], success: true,
114
+ * meta: { duration: 0.123, rows_read: 42, ... } } ]
115
+ *
116
+ * → DbQueryResult:
117
+ * { columns: [...], rows: [...], rowCount, executionTimeMs, meta }
118
+ */
119
+ export interface DbQueryResult {
120
+ /** Column names in display order (derived from the first row). */
121
+ columns: string[];
122
+ /** Row data, one record per result. */
123
+ rows: Record<string, unknown>[];
124
+ /** Convenience: `rows.length`. */
125
+ rowCount: number;
126
+ /** Wall-clock SQL execution time in milliseconds (from wrangler's `meta.duration`),
127
+ * or `null` if wrangler did not report it. */
128
+ executionTimeMs: number | null;
129
+ /** Raw wrangler `meta` block for callers that want additional fields
130
+ * (rows_read, changed_db, etc.). Null when the CLI did not return a meta block. */
131
+ meta: Record<string, unknown> | null;
132
+ }
133
+
134
+ /**
135
+ * Outcome of a client-side SQL safety check.
136
+ *
137
+ * Returned by {@link validateReadOnlySql} so the view can surface a
138
+ * specific, actionable error message without ever round-tripping the
139
+ * command to the local `hoox` binary.
140
+ */
141
+ export type SqlValidationResult =
142
+ | { readonly: true }
143
+ | { readonly: false; reason: string };
144
+
145
+ /**
146
+ * A single Cloudflare Workers secret metadata (read-only, no values exposed).
147
+ *
148
+ * Returned by {@link CliBridgeImpl.configSecretsList}. Values are intentionally
149
+ * omitted — the TUI viewer is strictly read-only and the CLI equivalent
150
+ * (`hoox config secrets list`) only surfaces names/types anyway.
151
+ */
152
+ export interface SecretMetadata {
153
+ /** Secret name / key, e.g. "BINANCE_KEY_BINDING" or "OPENAI_API_KEY". */
154
+ name: string;
155
+ /**
156
+ * Inferred type based on naming conventions. Cloudflare secrets store
157
+ * does not expose per-secret type metadata, so we derive it from the
158
+ * key name suffix.
159
+ */
160
+ type: "api_key" | "secret" | "token" | "password" | "unknown";
161
+ /**
162
+ * Source of the secret declaration: either "Cloudflare" (via wrangler secret)
163
+ * or "config" (declared in wrangler.jsonc but not yet synced).
164
+ */
165
+ source: "Cloudflare" | "config";
166
+ }
167
+
168
+ /**
169
+ * A read-only snapshot of all secrets across workers. Returned by
170
+ * {@link CliBridgeImpl.configSecretsList}.
171
+ */
172
+ export interface SecretsSnapshot {
173
+ secrets: SecretMetadata[];
174
+ /**
175
+ * ISO-8601 timestamp captured client-side when the snapshot was read.
176
+ * Surfaces a "Last sampled" footer in the view.
177
+ */
178
+ timestamp: string;
179
+ }
180
+
181
+ /**
182
+ * A single Cloudflare KV key as exposed by `hoox config kv list --json`.
183
+ *
184
+ * The view treats `valueSize` and `lastModified` as best-effort metadata:
185
+ * the canonical CLI output only includes `name` and the human-readable
186
+ * flag. Future CLI versions may surface richer metadata; the view renders
187
+ * a dash in those columns when the data is missing so the table stays
188
+ * aligned.
189
+ *
190
+ * Security note: `isSecret` is *advisory* — it is derived by cross-
191
+ * referencing the key name against the known KV manifest. Users should
192
+ * still treat any KV value as potentially sensitive.
193
+ */
194
+ export interface KvKey {
195
+ /** Full key name, e.g. `"trade:kill_switch"` or `"agent:openai_key"`. */
196
+ name: string;
197
+ /** Byte size of the stored value. `null` if unknown. */
198
+ valueSize: number | null;
199
+ /**
200
+ * ISO-8601 timestamp of the last write. `null` if the CLI did not
201
+ * surface it (current `hoox config kv list` does not include it).
202
+ */
203
+ lastModified: string | null;
204
+ /**
205
+ * True if the key is known to be secret per the KV manifest. Manifest
206
+ * keys with `secret: true` (API keys, watermarks, etc.) are flagged
207
+ * here so the view can render a warning before showing the value.
208
+ */
209
+ isSecret: boolean;
210
+ /**
211
+ * Manifest-declared type for this key, when known. One of
212
+ * `"boolean" | "number" | "string" | null`. Useful for rendering a
213
+ * type tag in the table.
214
+ */
215
+ manifestType: "boolean" | "number" | "string" | null;
216
+ }
217
+
218
+ /**
219
+ * A read-only snapshot of all KV keys plus the manifest cross-reference
220
+ * used to flag secret keys. Returned by {@link CliBridgeImpl.configKvList}.
221
+ */
222
+ export interface KvKeySnapshot {
223
+ keys: KvKey[];
224
+ /**
225
+ * ISO-8601 timestamp captured client-side when the snapshot was read.
226
+ * Surfaces a "Last sampled" footer in the view.
227
+ */
228
+ timestamp: string;
229
+ /**
230
+ * The KV namespace ID that was used to fetch the keys. `null` when the
231
+ * CLI auto-detection failed (e.g. wrangler not installed).
232
+ */
233
+ namespaceId: string | null;
234
+ }
235
+
236
+ /**
237
+ * Health status for a single AI model provider.
238
+ */
239
+ export interface ModelHealth {
240
+ /** Provider name, e.g. "Workers AI", "OpenAI", "Anthropic", "Google", "Azure" */
241
+ name: string;
242
+ /** Provider-specific model identifier, e.g. "@cf/meta/llama-3.1-8b-instruct" */
243
+ model: string;
244
+ /** Operational state */
245
+ status: "online" | "degraded" | "offline";
246
+ /** Latency in milliseconds (null when unknown or offline) */
247
+ latencyMs: number | null;
248
+ /** Daily request count (null when unavailable) */
249
+ dailyRequests: number | null;
250
+ /** Error message when status is not "online" */
251
+ error?: string;
252
+ }
253
+
254
+ /**
255
+ * Result of an AI model health check covering all configured providers.
256
+ */
257
+ export interface AgentHealthResult {
258
+ providers: ModelHealth[];
259
+ /** ISO-8601 timestamp when the check was performed */
260
+ timestamp: string;
261
+ }
262
+
263
+ /**
264
+ * Result of `hoox pyne health --json` — PYNE edge evaluate worker probe.
265
+ */
266
+ export interface PyneHealthResult {
267
+ worker: string;
268
+ url: string;
269
+ status: "healthy" | "degraded" | "down";
270
+ httpStatus?: number;
271
+ latencyMs?: number;
272
+ body?: unknown;
273
+ error?: string;
274
+ timestamp: string;
275
+ }
276
+
277
+ /** Local AI chat message shape. */
278
+ export interface ChatMessage {
279
+ role: "user" | "assistant";
280
+ content: string;
281
+ timestamp: string;
282
+ }
283
+
284
+ /** Available AI providers/models for the chat interface. */
285
+ export interface AiModelOption {
286
+ id: string;
287
+ label: string;
288
+ provider: string;
289
+ }
290
+
291
+ /**
292
+ * SSE streaming subscription for AI chat responses.
293
+ *
294
+ * Connects to the hoox API's /api/agent/chat endpoint and invokes
295
+ * the onToken callback for each streamed token fragment. Supports
296
+ * reconnection with exponential backoff on connection loss.
297
+ *
298
+ * Returns an AbortController whose `.signal.aborted` flag can be set
299
+ * to terminate the stream; callers should check this flag to avoid
300
+ * processing tokens after abort.
301
+ */
302
+ export interface AgentChatStreamResult {
303
+ /** AbortController for cancelling the stream. */
304
+ abort: AbortController;
305
+ /** Promise that resolves when the stream finishes or fails. */
306
+ finished: Promise<void>;
307
+ }