@interactive-inc/claude-funnel 0.10.0 → 0.15.1

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 (236) hide show
  1. package/README.md +106 -56
  2. package/dist/bin.js +557 -530
  3. package/dist/connectors/schedule.d.ts +2 -49
  4. package/dist/connectors/schedule.js +1 -1
  5. package/dist/connectors/slack.d.ts +4 -48
  6. package/dist/connectors/slack.js +1 -1
  7. package/dist/gateway/daemon.js +213 -211
  8. package/dist/index.d.ts +465 -173
  9. package/dist/index.js +692 -154
  10. package/dist/{schedule-connector-schema-CkuIQ0JQ.js → schedule-connector-schema-FxP7LPlx.js} +11 -0
  11. package/dist/{file-system-Co60LrmR.d.ts → schedule-listener-BPodvbld.d.ts} +56 -1
  12. package/dist/{slack-connector-schema-Cd22WiHB.js → slack-connector-schema-B4hsf3AY.js} +10 -1
  13. package/dist/slack-listener-CHj6uMY-.d.ts +74 -0
  14. package/package.json +2 -6
  15. package/schemas/funnel.schema.json +144 -0
  16. package/dist/slack-connector-schema-D7zAHN8k.d.ts +0 -15
  17. package/lib/bin.ts +0 -3
  18. package/lib/cli/factory.ts +0 -10
  19. package/lib/cli/index.ts +0 -85
  20. package/lib/cli/router/query-to-cli-args.ts +0 -20
  21. package/lib/cli/router/to-request.ts +0 -113
  22. package/lib/cli/router/validator.ts +0 -27
  23. package/lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts +0 -27
  24. package/lib/cli/routes/channels.$channel.connectors.$connector.request.ts +0 -40
  25. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts +0 -41
  26. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts +0 -22
  27. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.ts +0 -23
  28. package/lib/cli/routes/channels.$channel.connectors.$connector.ts +0 -26
  29. package/lib/cli/routes/channels.$channel.connectors.add.$connector.ts +0 -92
  30. package/lib/cli/routes/channels.$channel.connectors.remove.$connector.ts +0 -22
  31. package/lib/cli/routes/channels.$channel.connectors.set.$connector.ts +0 -63
  32. package/lib/cli/routes/channels.$channel.connectors.ts +0 -26
  33. package/lib/cli/routes/channels.$channel.publish.ts +0 -52
  34. package/lib/cli/routes/channels.$channel.rename.$newName.ts +0 -22
  35. package/lib/cli/routes/channels.$channel.set.delivery.$mode.ts +0 -34
  36. package/lib/cli/routes/channels.$channel.ts +0 -34
  37. package/lib/cli/routes/channels.add.$channel.ts +0 -33
  38. package/lib/cli/routes/channels.remove.$channel.ts +0 -20
  39. package/lib/cli/routes/channels.ts +0 -39
  40. package/lib/cli/routes/claude.ts +0 -70
  41. package/lib/cli/routes/gateway.listeners.ts +0 -41
  42. package/lib/cli/routes/gateway.logs.ts +0 -123
  43. package/lib/cli/routes/gateway.restart.ts +0 -50
  44. package/lib/cli/routes/gateway.run.ts +0 -41
  45. package/lib/cli/routes/gateway.start.ts +0 -50
  46. package/lib/cli/routes/gateway.status.ts +0 -19
  47. package/lib/cli/routes/gateway.stop.ts +0 -32
  48. package/lib/cli/routes/gateway.ts +0 -55
  49. package/lib/cli/routes/index.ts +0 -219
  50. package/lib/cli/routes/profiles.$profile.as-default.ts +0 -22
  51. package/lib/cli/routes/profiles.$profile.rename.$newName.ts +0 -22
  52. package/lib/cli/routes/profiles.$profile.run.ts +0 -36
  53. package/lib/cli/routes/profiles.add.$profile.ts +0 -49
  54. package/lib/cli/routes/profiles.remove.$profile.ts +0 -20
  55. package/lib/cli/routes/profiles.set.$profile.ts +0 -45
  56. package/lib/cli/routes/profiles.ts +0 -40
  57. package/lib/cli/routes/status.ts +0 -93
  58. package/lib/cli/routes/update.ts +0 -27
  59. package/lib/connectors/connector-adapter.ts +0 -9
  60. package/lib/connectors/connector-config-schema.ts +0 -16
  61. package/lib/connectors/connector-factory.ts +0 -94
  62. package/lib/connectors/connector-listener.ts +0 -20
  63. package/lib/connectors/discord-adapter.ts +0 -51
  64. package/lib/connectors/discord-connector-schema.ts +0 -12
  65. package/lib/connectors/discord-event-processor.ts +0 -48
  66. package/lib/connectors/discord-listener.ts +0 -111
  67. package/lib/connectors/discord.ts +0 -4
  68. package/lib/connectors/gh-adapter.ts +0 -48
  69. package/lib/connectors/gh-connector-schema.ts +0 -12
  70. package/lib/connectors/gh-listener.ts +0 -137
  71. package/lib/connectors/gh.ts +0 -3
  72. package/lib/connectors/match-cron.ts +0 -78
  73. package/lib/connectors/schedule-connector-schema.ts +0 -33
  74. package/lib/connectors/schedule-listener.ts +0 -207
  75. package/lib/connectors/schedule-state-store.ts +0 -54
  76. package/lib/connectors/schedule.ts +0 -4
  77. package/lib/connectors/slack-adapter.ts +0 -36
  78. package/lib/connectors/slack-connector-schema.ts +0 -13
  79. package/lib/connectors/slack-event-processor.ts +0 -97
  80. package/lib/connectors/slack-listener.ts +0 -97
  81. package/lib/connectors/slack.ts +0 -4
  82. package/lib/engine/channels/channels.ts +0 -520
  83. package/lib/engine/claude/claude.ts +0 -205
  84. package/lib/engine/claude/gateway-controller.ts +0 -4
  85. package/lib/engine/fs/file-system.ts +0 -23
  86. package/lib/engine/fs/memory-file-system.ts +0 -102
  87. package/lib/engine/fs/node-file-system.ts +0 -68
  88. package/lib/engine/http/http-client.ts +0 -17
  89. package/lib/engine/http/memory-http-client.ts +0 -36
  90. package/lib/engine/http/node-http-client.ts +0 -23
  91. package/lib/engine/id/id-generator.ts +0 -7
  92. package/lib/engine/id/memory-id-generator.ts +0 -20
  93. package/lib/engine/id/node-id-generator.ts +0 -7
  94. package/lib/engine/logger/logger.ts +0 -11
  95. package/lib/engine/logger/memory-logger.ts +0 -28
  96. package/lib/engine/logger/node-logger.ts +0 -49
  97. package/lib/engine/logger/noop-logger.ts +0 -9
  98. package/lib/engine/mcp/channel-server.ts +0 -123
  99. package/lib/engine/mcp/channel-subscriber.ts +0 -82
  100. package/lib/engine/mcp/mcp.ts +0 -126
  101. package/lib/engine/mcp/read-channel-connectors.ts +0 -34
  102. package/lib/engine/mcp/read-gateway-token.ts +0 -16
  103. package/lib/engine/mcp/usage-hint-for-type.ts +0 -15
  104. package/lib/engine/process/memory-process-runner.ts +0 -88
  105. package/lib/engine/process/node-process-runner.ts +0 -91
  106. package/lib/engine/process/process-runner.ts +0 -33
  107. package/lib/engine/profiles/profile-channel-checker.ts +0 -7
  108. package/lib/engine/profiles/profiles.ts +0 -126
  109. package/lib/engine/settings/mock-settings-reader.ts +0 -27
  110. package/lib/engine/settings/settings-reader.ts +0 -6
  111. package/lib/engine/settings/settings-schema.ts +0 -48
  112. package/lib/engine/settings/settings-store.ts +0 -110
  113. package/lib/engine/time/clock.ts +0 -15
  114. package/lib/engine/time/memory-clock.ts +0 -26
  115. package/lib/engine/time/node-clock.ts +0 -7
  116. package/lib/funnel.ts +0 -294
  117. package/lib/gateway/auth-middleware.ts +0 -44
  118. package/lib/gateway/broadcaster.ts +0 -319
  119. package/lib/gateway/channel-publisher.ts +0 -67
  120. package/lib/gateway/daemon.ts +0 -47
  121. package/lib/gateway/factory.ts +0 -10
  122. package/lib/gateway/funnel-event-store.ts +0 -155
  123. package/lib/gateway/gateway-server.ts +0 -426
  124. package/lib/gateway/gateway-token.ts +0 -79
  125. package/lib/gateway/gateway.ts +0 -209
  126. package/lib/gateway/kill-competing-slack-gateways.ts +0 -56
  127. package/lib/gateway/listener-supervisor.ts +0 -339
  128. package/lib/gateway/listeners-client.ts +0 -128
  129. package/lib/gateway/publish-schema.ts +0 -27
  130. package/lib/gateway/resolve-daemon-script.ts +0 -26
  131. package/lib/gateway/routes/channels.connectors.call.ts +0 -39
  132. package/lib/gateway/routes/channels.publish.ts +0 -44
  133. package/lib/gateway/routes/health.ts +0 -13
  134. package/lib/gateway/routes/index.ts +0 -26
  135. package/lib/gateway/routes/listeners.list.ts +0 -6
  136. package/lib/gateway/routes/listeners.restart.ts +0 -15
  137. package/lib/gateway/routes/listeners.start.ts +0 -15
  138. package/lib/gateway/routes/listeners.stop.ts +0 -15
  139. package/lib/gateway/routes/route-deps.ts +0 -19
  140. package/lib/gateway/routes/status.ts +0 -15
  141. package/lib/gateway/routes/validator.ts +0 -17
  142. package/lib/index.ts +0 -67
  143. package/lib/logger/leuco-human-file-writer.ts +0 -65
  144. package/lib/logger/leuco-human-logger.ts +0 -98
  145. package/lib/logger/leuco-human-record.ts +0 -16
  146. package/lib/logger/leuco-human-stdout-writer.ts +0 -26
  147. package/lib/logger/leuco-human-writer.ts +0 -14
  148. package/lib/logger/leuco-logger-memory-sink.ts +0 -67
  149. package/lib/logger/leuco-logger-record.ts +0 -13
  150. package/lib/logger/leuco-logger-sink.ts +0 -33
  151. package/lib/logger/leuco-logger-sqlite-sink.ts +0 -355
  152. package/lib/logger/leuco-logger.ts +0 -135
  153. package/lib/tui/app.tsx +0 -357
  154. package/lib/tui/components/add-row.tsx +0 -18
  155. package/lib/tui/components/brand.tsx +0 -27
  156. package/lib/tui/components/card.tsx +0 -44
  157. package/lib/tui/components/detail-bar.tsx +0 -46
  158. package/lib/tui/components/editable-field.tsx +0 -33
  159. package/lib/tui/components/empty-state.tsx +0 -11
  160. package/lib/tui/components/gateway-status.tsx +0 -66
  161. package/lib/tui/components/keymap.tsx +0 -29
  162. package/lib/tui/components/menu-item.tsx +0 -73
  163. package/lib/tui/components/menu.tsx +0 -26
  164. package/lib/tui/components/panel-header.tsx +0 -22
  165. package/lib/tui/components/readonly-field.tsx +0 -18
  166. package/lib/tui/components/section-header.tsx +0 -25
  167. package/lib/tui/components/selection-accent.tsx +0 -32
  168. package/lib/tui/components/session-item.tsx +0 -33
  169. package/lib/tui/components/session-list.tsx +0 -33
  170. package/lib/tui/components/ui/hascii/accordion-item.tsx +0 -88
  171. package/lib/tui/components/ui/hascii/accordion.tsx +0 -96
  172. package/lib/tui/components/ui/hascii/alert-dialog.tsx +0 -43
  173. package/lib/tui/components/ui/hascii/badge.tsx +0 -51
  174. package/lib/tui/components/ui/hascii/breadcrumb.tsx +0 -58
  175. package/lib/tui/components/ui/hascii/button.tsx +0 -194
  176. package/lib/tui/components/ui/hascii/card-content.tsx +0 -14
  177. package/lib/tui/components/ui/hascii/card-description.tsx +0 -13
  178. package/lib/tui/components/ui/hascii/card-footer.tsx +0 -14
  179. package/lib/tui/components/ui/hascii/card-header.tsx +0 -14
  180. package/lib/tui/components/ui/hascii/card-title.tsx +0 -13
  181. package/lib/tui/components/ui/hascii/card.tsx +0 -27
  182. package/lib/tui/components/ui/hascii/checkbox.tsx +0 -65
  183. package/lib/tui/components/ui/hascii/command.tsx +0 -159
  184. package/lib/tui/components/ui/hascii/dialog-content.tsx +0 -14
  185. package/lib/tui/components/ui/hascii/dialog-description.tsx +0 -13
  186. package/lib/tui/components/ui/hascii/dialog-footer.tsx +0 -14
  187. package/lib/tui/components/ui/hascii/dialog-header.tsx +0 -14
  188. package/lib/tui/components/ui/hascii/dialog-title.tsx +0 -13
  189. package/lib/tui/components/ui/hascii/dialog.tsx +0 -27
  190. package/lib/tui/components/ui/hascii/file-tree.tsx +0 -142
  191. package/lib/tui/components/ui/hascii/focus-group.tsx +0 -62
  192. package/lib/tui/components/ui/hascii/form-item.tsx +0 -43
  193. package/lib/tui/components/ui/hascii/input-otp.tsx +0 -86
  194. package/lib/tui/components/ui/hascii/input.tsx +0 -130
  195. package/lib/tui/components/ui/hascii/pagination.tsx +0 -105
  196. package/lib/tui/components/ui/hascii/progress.tsx +0 -28
  197. package/lib/tui/components/ui/hascii/select.tsx +0 -131
  198. package/lib/tui/components/ui/hascii/separator.tsx +0 -35
  199. package/lib/tui/components/ui/hascii/sidebar-content.tsx +0 -23
  200. package/lib/tui/components/ui/hascii/sidebar-header.tsx +0 -14
  201. package/lib/tui/components/ui/hascii/sidebar-menu-item.tsx +0 -67
  202. package/lib/tui/components/ui/hascii/sidebar.tsx +0 -24
  203. package/lib/tui/components/ui/hascii/skeleton.tsx +0 -60
  204. package/lib/tui/components/ui/hascii/slider.tsx +0 -91
  205. package/lib/tui/components/ui/hascii/snackbar.tsx +0 -75
  206. package/lib/tui/components/ui/hascii/sparkline.tsx +0 -53
  207. package/lib/tui/components/ui/hascii/spinner.tsx +0 -47
  208. package/lib/tui/components/ui/hascii/stepper.tsx +0 -54
  209. package/lib/tui/components/ui/hascii/switch.tsx +0 -66
  210. package/lib/tui/components/ui/hascii/table.tsx +0 -95
  211. package/lib/tui/components/ui/hascii/tabs.tsx +0 -59
  212. package/lib/tui/components/ui/hascii/toggle-group-item.tsx +0 -45
  213. package/lib/tui/components/ui/hascii/toggle-group.tsx +0 -99
  214. package/lib/tui/components/ui/hascii/tree.tsx +0 -104
  215. package/lib/tui/components/view-shell.tsx +0 -44
  216. package/lib/tui/filter-input.tsx +0 -33
  217. package/lib/tui/hooks/hascii/use-pressable.ts +0 -54
  218. package/lib/tui/parse-comma-list.ts +0 -14
  219. package/lib/tui/profile-launcher.tsx +0 -61
  220. package/lib/tui/scrollbar-options.ts +0 -19
  221. package/lib/tui/sidebar.tsx +0 -50
  222. package/lib/tui/theme.ts +0 -40
  223. package/lib/tui/tui.tsx +0 -20
  224. package/lib/tui/types.ts +0 -38
  225. package/lib/tui/unique-name.ts +0 -18
  226. package/lib/tui/use-event-stream.ts +0 -133
  227. package/lib/tui/use-snapshot.ts +0 -99
  228. package/lib/tui/utils/hascii/form-item-context.tsx +0 -23
  229. package/lib/tui/utils/hascii/input-focus-context.tsx +0 -31
  230. package/lib/tui/utils/hascii/theme-context.tsx +0 -26
  231. package/lib/tui/utils/hascii/theme.ts +0 -176
  232. package/lib/tui/views/channels-view.tsx +0 -108
  233. package/lib/tui/views/connectors-view.tsx +0 -164
  234. package/lib/tui/views/events-view.tsx +0 -160
  235. package/lib/tui/views/listeners-view.tsx +0 -80
  236. package/lib/tui/views/profiles-view.tsx +0 -152
@@ -1,19 +0,0 @@
1
- import { funnel } from "@/tui/theme"
2
- import type { HasciiTheme } from "@/tui/utils/hascii/theme"
3
-
4
- /**
5
- * Shared OpenTUI scrollbar styling. Used by `ViewShell` and `DetailBar`
6
- * so both scroll containers paint the track with the surrounding
7
- * `surface` tone and the thumb in `mutedForeground` instead of OpenTUI's
8
- * default electric blue.
9
- *
10
- * Takes the active hascii theme since scrollbar coloring depends on
11
- * the same palette as everything else.
12
- */
13
- export const verticalScrollbarOptions = (theme: HasciiTheme) =>
14
- ({
15
- trackOptions: {
16
- backgroundColor: funnel.surface,
17
- foregroundColor: theme.color.mutedForeground,
18
- },
19
- }) as const
@@ -1,50 +0,0 @@
1
- /** @jsxImportSource @opentui/react */
2
- import { Brand } from "@/tui/components/brand"
3
- import { GatewayStatus } from "@/tui/components/gateway-status"
4
- import { Menu } from "@/tui/components/menu"
5
- import { SectionHeader } from "@/tui/components/section-header"
6
- import { SessionList } from "@/tui/components/session-list"
7
- import { HasciiSidebar } from "@/tui/components/ui/hascii/sidebar"
8
- import { HasciiSidebarHeader } from "@/tui/components/ui/hascii/sidebar-header"
9
- import { funnel } from "@/tui/theme"
10
- import type { MenuItem, Snapshot, View } from "@/tui/types"
11
-
12
- type Props = {
13
- snapshot: Snapshot
14
- menuItems: MenuItem[]
15
- view: View
16
- onSelect: (view: View) => void
17
- busy: boolean
18
- onToggleGateway: () => void
19
- }
20
-
21
- /**
22
- * Left rail built on hascii's HasciiSidebar shell.
23
- *
24
- * Sections (top → bottom): brand (header slot), gateway card, sessions,
25
- * navigation menu. The funnel `Menu` items keep the left-edge `▏`
26
- * primary accent and right-aligned counts that hascii's stock sidebar
27
- * menu does not provide.
28
- */
29
- export function Sidebar(props: Props) {
30
- return (
31
- <HasciiSidebar width={funnel.sidebarWidth}>
32
- <HasciiSidebarHeader>
33
- <Brand />
34
- </HasciiSidebarHeader>
35
-
36
- <GatewayStatus
37
- gateway={props.snapshot.gateway}
38
- busy={props.busy}
39
- onToggle={props.onToggleGateway}
40
- />
41
-
42
- <box style={{ flexDirection: "column" }}>
43
- <SectionHeader label="sessions" />
44
- <SessionList sessions={props.snapshot.sessions} />
45
- </box>
46
-
47
- <Menu items={props.menuItems} active={props.view} onSelect={props.onSelect} />
48
- </HasciiSidebar>
49
- )
50
- }
package/lib/tui/theme.ts DELETED
@@ -1,40 +0,0 @@
1
- import { hasciiTw } from "@/tui/utils/hascii/theme"
2
-
3
- /**
4
- * Funnel-specific TUI tokens that hascii does not cover.
5
- *
6
- * Generic colors (background / foreground / primary / muted / etc.)
7
- * come from hascii via `useHasciiTheme()` — components that need them
8
- * read the theme there. Only the funnel-only concerns live here:
9
- *
10
- * - status accents (`alive` / `dead` / `warn`) and the selection
11
- * accent (`primary` blue) — semantic colors not in hascii's palette
12
- * - the in-between background tier `surface` (zinc[900]) sitting
13
- * between hascii's `background` and `muted`
14
- * - the deeper text tier `faint` (zinc[600]) below `mutedForeground`
15
- * - layout constants (paddingX / paddingY / gap / sidebarWidth / ...)
16
- */
17
- export const funnel = {
18
- // ─ funnel-specific colors ────────────────────────────────
19
- alive: "#86efac",
20
- dead: "#fca5a5",
21
- warn: "#fcd34d",
22
- primary: "#3b82f6",
23
-
24
- surface: hasciiTw.colors.zinc[900],
25
- faint: hasciiTw.colors.zinc[600],
26
-
27
- // ─ layout ────────────────────────────────────────────────
28
- paddingX: 2,
29
- paddingY: 1,
30
- gap: 1,
31
-
32
- sidebarWidth: 24,
33
-
34
- modalTop: 4,
35
- modalInset: "20%",
36
-
37
- barHeight: 1,
38
-
39
- detailPanelHeight: 14,
40
- } as const
package/lib/tui/tui.tsx DELETED
@@ -1,20 +0,0 @@
1
- /** @jsxImportSource @opentui/react */
2
- import { createCliRenderer } from "@opentui/core"
3
- import { createRoot } from "@opentui/react"
4
- import { App } from "@/tui/app"
5
- import { HasciiThemeProvider } from "@/tui/utils/hascii/theme-context"
6
- import type { Funnel } from "@/funnel"
7
-
8
- export async function launchTui(funnel: Funnel): Promise<void> {
9
- const renderer = await createCliRenderer()
10
-
11
- createRoot(renderer).render(
12
- <HasciiThemeProvider>
13
- <App funnel={funnel} />
14
- </HasciiThemeProvider>,
15
- )
16
-
17
- await new Promise<void>((resolve) => {
18
- renderer.once("destroy", () => resolve())
19
- })
20
- }
package/lib/tui/types.ts DELETED
@@ -1,38 +0,0 @@
1
- import type { ChannelConnectorView } from "@/engine/channels/channels"
2
- import type { ListenerEntry } from "@/gateway/listeners-client"
3
- import type { ChannelConfig, ProfileConfig } from "@/engine/settings/settings-schema"
4
-
5
- export type Session = {
6
- channel: string
7
- connectors: string[]
8
- }
9
-
10
- export type Snapshot = {
11
- connectors: ChannelConnectorView[]
12
- channels: ChannelConfig[]
13
- profiles: ProfileConfig[]
14
- gateway: { running: boolean; pid: number | null; port: number }
15
- listeners: ListenerEntry[]
16
- sessions: Session[]
17
- daemonReachable: boolean
18
- refreshedAt: number
19
- }
20
-
21
- export type StreamEvent = {
22
- id: number
23
- receivedAt: number
24
- content: string
25
- meta: Record<string, string>
26
- }
27
-
28
- export type StreamStatus = "connecting" | "open" | "closed" | "disabled"
29
-
30
- export type Mode = "browse" | "filter" | "profile-launcher"
31
-
32
- export type View = "events" | "connectors" | "channels" | "profiles" | "listeners"
33
-
34
- export type MenuItem = {
35
- view: View
36
- label: string
37
- count?: number
38
- }
@@ -1,18 +0,0 @@
1
- /**
2
- * Pick the first `${prefix}-N` (N = 1, 2, …) that doesn't appear in the
3
- * `existing` list.
4
- *
5
- * Used by the connectors / channels / profiles views to mint a unique
6
- * default name when the user clicks "+ add". Stops at 10 000 to avoid a
7
- * runaway loop if `existing` is full of placeholder names — falls back
8
- * to `${prefix}-${Date.now()}` so the caller still gets a legal name.
9
- */
10
- export function uniqueName(existing: string[], prefix: string): string {
11
- for (let i = 1; i < 10000; i += 1) {
12
- const candidate = `${prefix}-${i}`
13
-
14
- if (!existing.includes(candidate)) return candidate
15
- }
16
-
17
- return `${prefix}-${Date.now()}`
18
- }
@@ -1,133 +0,0 @@
1
- import { useEffect, useState } from "react"
2
- import { z } from "zod"
3
- import type { StreamEvent, StreamStatus } from "@/tui/types"
4
-
5
- const MAX_BUFFER = 200
6
- const RECONNECT_BASE_MS = 500
7
- const RECONNECT_MAX_MS = 8000
8
-
9
- const eventPayloadSchema = z.object({
10
- content: z.string(),
11
- meta: z.record(z.string(), z.string()).optional(),
12
- offset: z.number().int().nonnegative().optional(),
13
- })
14
-
15
- type Result = {
16
- events: StreamEvent[]
17
- status: StreamStatus
18
- reset: () => void
19
- }
20
-
21
- /**
22
- * Opens a `tap=all` WebSocket against the gateway daemon and accumulates
23
- * received events in a ring buffer. Reconnects with capped exponential
24
- * backoff. Returns `disabled` status until the daemon comes online.
25
- *
26
- * `token` is appended as `?token=` so the gateway accepts the upgrade.
27
- */
28
- export const useEventStream = (
29
- port: number,
30
- daemonReachable: boolean,
31
- token: string | null,
32
- ): Result => {
33
- const [events, setEvents] = useState<StreamEvent[]>([])
34
- const [status, setStatus] = useState<StreamStatus>("disabled")
35
- const [resetTick, setResetTick] = useState(0)
36
-
37
- useEffect(() => {
38
- if (!daemonReachable) {
39
- setStatus("disabled")
40
- return
41
- }
42
-
43
- let cancelled = false
44
- let socket: WebSocket | null = null
45
- let reconnectTimer: ReturnType<typeof setTimeout> | null = null
46
- let attempt = 0
47
- let nextId = events.length > 0 ? Math.max(...events.map((e) => e.id)) + 1 : 1
48
- let lastOffset = 0
49
-
50
- const connect = () => {
51
- if (cancelled) return
52
-
53
- setStatus("connecting")
54
- const sinceQuery = lastOffset > 0 ? `&since=${lastOffset}` : ""
55
- const protocols = token ? [`funnel.token.${token}`] : undefined
56
- socket = new WebSocket(`ws://localhost:${port}/ws?tap=all${sinceQuery}`, protocols)
57
-
58
- socket.addEventListener("open", () => {
59
- if (cancelled) return
60
-
61
- attempt = 0
62
- setStatus("open")
63
- })
64
-
65
- socket.addEventListener("message", (event) => {
66
- if (cancelled) return
67
-
68
- const raw: unknown = (() => {
69
- try {
70
- return JSON.parse(String(event.data))
71
- } catch {
72
- return null
73
- }
74
- })()
75
- const parsed = eventPayloadSchema.safeParse(raw)
76
-
77
- if (!parsed.success) return
78
-
79
- if (typeof parsed.data.offset === "number") {
80
- lastOffset = parsed.data.offset
81
- }
82
-
83
- const next: StreamEvent = {
84
- id: nextId,
85
- receivedAt: Date.now(),
86
- content: parsed.data.content,
87
- meta: parsed.data.meta ?? {},
88
- }
89
- nextId += 1
90
-
91
- setEvents((prev) => {
92
- const merged = [next, ...prev]
93
- return merged.length > MAX_BUFFER ? merged.slice(0, MAX_BUFFER) : merged
94
- })
95
- })
96
-
97
- socket.addEventListener("close", () => {
98
- if (cancelled) return
99
-
100
- setStatus("closed")
101
- attempt += 1
102
-
103
- const delay = Math.min(RECONNECT_BASE_MS * 2 ** (attempt - 1), RECONNECT_MAX_MS)
104
-
105
- reconnectTimer = setTimeout(connect, delay)
106
- })
107
-
108
- socket.addEventListener("error", () => {
109
- // close fires after error; reconnect happens there.
110
- })
111
- }
112
-
113
- connect()
114
-
115
- return () => {
116
- cancelled = true
117
-
118
- if (reconnectTimer) clearTimeout(reconnectTimer)
119
- if (socket) socket.close()
120
- }
121
- // events / nextId intentionally captured at hook entry
122
- // eslint-disable-next-line react-hooks/exhaustive-deps
123
- }, [port, daemonReachable, resetTick, token])
124
-
125
- return {
126
- events,
127
- status,
128
- reset: () => {
129
- setEvents([])
130
- setResetTick((value) => value + 1)
131
- },
132
- }
133
- }
@@ -1,99 +0,0 @@
1
- import { useEffect, useState } from "react"
2
- import { z } from "zod"
3
- import type { Session, Snapshot } from "@/tui/types"
4
- import type { Funnel } from "@/funnel"
5
-
6
- const POLL_INTERVAL_MS = 3000
7
-
8
- const sessionSchema = z.object({
9
- channel: z.string(),
10
- connectors: z.array(z.string()),
11
- })
12
-
13
- const statusResponseSchema = z.object({
14
- clients: z.array(sessionSchema),
15
- })
16
-
17
- const emptySnapshot: Snapshot = {
18
- connectors: [],
19
- channels: [],
20
- profiles: [],
21
- gateway: { running: false, pid: null, port: 9742 },
22
- listeners: [],
23
- sessions: [],
24
- daemonReachable: false,
25
- refreshedAt: 0,
26
- }
27
-
28
- const fetchSessions = async (
29
- port: number,
30
- daemonRunning: boolean,
31
- token: string | null,
32
- ): Promise<Session[]> => {
33
- if (!daemonRunning) return []
34
-
35
- try {
36
- const headers: Record<string, string> = token ? { authorization: `Bearer ${token}` } : {}
37
- const response = await fetch(`http://localhost:${port}/status`, { headers })
38
-
39
- if (!response.ok) return []
40
-
41
- const parsed = statusResponseSchema.safeParse(await response.json())
42
-
43
- if (!parsed.success) return []
44
-
45
- return parsed.data.clients.filter((client) => client.channel !== "*tap*")
46
- } catch {
47
- return []
48
- }
49
- }
50
-
51
- /**
52
- * Polls Funnel state every few seconds. The returned `refresh` callback forces
53
- * an immediate refetch — used by the manual `r` key and by listener-action
54
- * keystrokes that change daemon state.
55
- */
56
- export const useSnapshot = (funnel: Funnel): { snapshot: Snapshot; refresh: () => void } => {
57
- const [snapshot, setSnapshot] = useState<Snapshot>(emptySnapshot)
58
- const [tick, setTick] = useState(0)
59
-
60
- useEffect(() => {
61
- let cancelled = false
62
-
63
- const load = async () => {
64
- const gateway = funnel.gateway.getStatus()
65
- const token = funnel.gatewayToken.read()
66
- const [listenersResult, sessions] = await Promise.all([
67
- funnel.listeners.list(),
68
- fetchSessions(gateway.port, gateway.running, token),
69
- ])
70
-
71
- if (cancelled) return
72
-
73
- setSnapshot({
74
- connectors: funnel.channels.listAllConnectors(),
75
- channels: funnel.channels.list(),
76
- profiles: funnel.profiles.list(),
77
- gateway,
78
- listeners: listenersResult.state === "ok" ? listenersResult.listeners : [],
79
- sessions,
80
- daemonReachable: listenersResult.state === "ok",
81
- refreshedAt: Date.now(),
82
- })
83
- }
84
-
85
- void load()
86
-
87
- const timer = setInterval(() => void load(), POLL_INTERVAL_MS)
88
-
89
- return () => {
90
- cancelled = true
91
- clearInterval(timer)
92
- }
93
- }, [tick, funnel])
94
-
95
- return {
96
- snapshot,
97
- refresh: () => setTick((value) => value + 1),
98
- }
99
- }
@@ -1,23 +0,0 @@
1
- import { createContext, useContext } from "react"
2
- import type { ReactNode } from "react"
3
-
4
- export type FormItemContextValue = {
5
- focusId: string
6
- }
7
-
8
- const FormItemContext = createContext<FormItemContextValue | null>(null)
9
-
10
- /** Read the surrounding HasciiFormItem context. Returns null when called outside one. */
11
- export function useHasciiFormItem(): FormItemContextValue | null {
12
- return useContext(FormItemContext)
13
- }
14
-
15
- export type Props = {
16
- value: FormItemContextValue
17
- children: ReactNode
18
- }
19
-
20
- /** Provider used by HasciiFormItem to share the row's focus id with the wrapped HasciiInput. */
21
- export function HasciiFormItemProvider(props: Props) {
22
- return <FormItemContext.Provider value={props.value}>{props.children}</FormItemContext.Provider>
23
- }
@@ -1,31 +0,0 @@
1
- /** @jsxImportSource @opentui/react */
2
- import { createContext, useContext, useState } from "react"
3
- import type { ReactNode } from "react"
4
-
5
- type ContextValue = {
6
- focusedId: string | null
7
- setFocusedId: (id: string | null) => void
8
- }
9
-
10
- const InputFocusContext = createContext<ContextValue | null>(null)
11
-
12
- /** Returns the surrounding HasciiInputFocusProvider's API. Null when no provider is mounted. */
13
- export function useHasciiInputFocus(): ContextValue | null {
14
- return useContext(InputFocusContext)
15
- }
16
-
17
- export type Props = {
18
- children: ReactNode
19
- }
20
-
21
- /** Holds the id of the currently focused HasciiInput so siblings and outer click handlers can blur it. */
22
- export function HasciiInputFocusProvider(props: Props) {
23
- const focusedState = useState<string | null>(null)
24
-
25
- const value: ContextValue = {
26
- focusedId: focusedState[0],
27
- setFocusedId: focusedState[1],
28
- }
29
-
30
- return <InputFocusContext.Provider value={value}>{props.children}</InputFocusContext.Provider>
31
- }
@@ -1,26 +0,0 @@
1
- /** @jsxImportSource @opentui/react */
2
- import { createContext, useContext } from "react"
3
- import type { ReactNode } from "react"
4
- import type { HasciiTheme } from "@/tui/utils/hascii/theme"
5
- import { hasciiTheme } from "@/tui/utils/hascii/theme"
6
-
7
- const HasciiThemeContext = createContext<HasciiTheme>(hasciiTheme)
8
-
9
- /** Read the active theme from context. Falls back to the default theme outside a provider. */
10
- export function useHasciiTheme(): HasciiTheme {
11
- return useContext(HasciiThemeContext)
12
- }
13
-
14
- export type Props = {
15
- theme?: HasciiTheme
16
- children: ReactNode
17
- }
18
-
19
- /** Wraps a subtree with a HasciiTheme so descendants can read tokens via useHasciiTheme. */
20
- export function HasciiThemeProvider(props: Props) {
21
- return (
22
- <HasciiThemeContext.Provider value={props.theme ?? hasciiTheme}>
23
- {props.children}
24
- </HasciiThemeContext.Provider>
25
- )
26
- }
@@ -1,176 +0,0 @@
1
- import { z } from "zod"
2
- import tokens from "@/tui/utils/hascii/tokens.json"
3
-
4
- // TailwindCSS v4 default color palette (subset). Use as raw primitives — wire into theme tokens, not directly into components.
5
- export const hasciiTw = {
6
- colors: {
7
- neutral: {
8
- 50: "#fafafa",
9
- 100: "#f5f5f5",
10
- 200: "#e5e5e5",
11
- 300: "#d4d4d4",
12
- 400: "#a3a3a3",
13
- 500: "#737373",
14
- 600: "#525252",
15
- 700: "#404040",
16
- 800: "#262626",
17
- 900: "#171717",
18
- 950: "#0a0a0a",
19
- },
20
- zinc: {
21
- 50: "#fafafa",
22
- 100: "#f4f4f5",
23
- 200: "#e4e4e7",
24
- 300: "#d4d4d8",
25
- 400: "#a1a1aa",
26
- 500: "#71717a",
27
- 600: "#52525b",
28
- 700: "#3f3f46",
29
- 800: "#27272a",
30
- 900: "#18181b",
31
- 950: "#09090b",
32
- },
33
- red: {
34
- 50: "#fef2f2",
35
- 100: "#fee2e2",
36
- 200: "#fecaca",
37
- 300: "#fca5a5",
38
- 400: "#f87171",
39
- 500: "#ef4444",
40
- 600: "#dc2626",
41
- 700: "#b91c1c",
42
- 800: "#991b1b",
43
- 900: "#7f1d1d",
44
- 950: "#450a0a",
45
- },
46
- },
47
- } as const
48
-
49
- export type HasciiTw = typeof hasciiTw
50
-
51
- const HEX = /^#[0-9a-fA-F]{6}$/
52
-
53
- const TokensSchema = z.object({
54
- theme: z.object({
55
- extend: z.object({
56
- colors: z.object({
57
- background: z.string().regex(HEX),
58
- foreground: z.string().regex(HEX),
59
- primary: z.string().regex(HEX),
60
- "primary-foreground": z.string().regex(HEX),
61
- "primary-hover": z.string().regex(HEX),
62
- "primary-active": z.string().regex(HEX),
63
- secondary: z.string().regex(HEX),
64
- "secondary-foreground": z.string().regex(HEX),
65
- "secondary-hover": z.string().regex(HEX),
66
- "secondary-active": z.string().regex(HEX),
67
- card: z.string().regex(HEX),
68
- "card-foreground": z.string().regex(HEX),
69
- popover: z.string().regex(HEX),
70
- "popover-foreground": z.string().regex(HEX),
71
- muted: z.string().regex(HEX),
72
- "muted-foreground": z.string().regex(HEX),
73
- accent: z.string().regex(HEX),
74
- "accent-foreground": z.string().regex(HEX),
75
- "accent-hover": z.string().regex(HEX),
76
- "accent-active": z.string().regex(HEX),
77
- destructive: z.string().regex(HEX),
78
- "destructive-foreground": z.string().regex(HEX),
79
- "destructive-hover": z.string().regex(HEX),
80
- "destructive-active": z.string().regex(HEX),
81
- border: z.string().regex(HEX),
82
- input: z.string().regex(HEX),
83
- ring: z.string().regex(HEX),
84
- "hover-active": z.string().regex(HEX),
85
- }),
86
- }),
87
- }),
88
- })
89
-
90
- const parsed = TokensSchema.parse(tokens)
91
- const c = parsed.theme.extend.colors
92
-
93
- export type HasciiTheme = {
94
- color: {
95
- background: string
96
- foreground: string
97
-
98
- primary: string
99
- primaryForeground: string
100
- primaryHover: string
101
- primaryActive: string
102
-
103
- secondary: string
104
- secondaryForeground: string
105
- secondaryHover: string
106
- secondaryActive: string
107
-
108
- card: string
109
- cardForeground: string
110
-
111
- popover: string
112
- popoverForeground: string
113
-
114
- muted: string
115
- mutedForeground: string
116
-
117
- accent: string
118
- accentForeground: string
119
- accentHover: string
120
- accentActive: string
121
-
122
- destructive: string
123
- destructiveForeground: string
124
- destructiveHover: string
125
- destructiveActive: string
126
-
127
- border: string
128
- input: string
129
- ring: string
130
-
131
- hoverActive: string
132
- }
133
- }
134
-
135
- /** Default dark theme. Tokens are loaded from tokens.json (generated from DESIGN.md by `make tokens`) and validated with zod. */
136
- export const hasciiTheme: HasciiTheme = {
137
- color: {
138
- background: c.background,
139
- foreground: c.foreground,
140
-
141
- primary: c.primary,
142
- primaryForeground: c["primary-foreground"],
143
- primaryHover: c["primary-hover"],
144
- primaryActive: c["primary-active"],
145
-
146
- secondary: c.secondary,
147
- secondaryForeground: c["secondary-foreground"],
148
- secondaryHover: c["secondary-hover"],
149
- secondaryActive: c["secondary-active"],
150
-
151
- card: c.card,
152
- cardForeground: c["card-foreground"],
153
-
154
- popover: c.popover,
155
- popoverForeground: c["popover-foreground"],
156
-
157
- muted: c.muted,
158
- mutedForeground: c["muted-foreground"],
159
-
160
- accent: c.accent,
161
- accentForeground: c["accent-foreground"],
162
- accentHover: c["accent-hover"],
163
- accentActive: c["accent-active"],
164
-
165
- destructive: c.destructive,
166
- destructiveForeground: c["destructive-foreground"],
167
- destructiveHover: c["destructive-hover"],
168
- destructiveActive: c["destructive-active"],
169
-
170
- border: c.border,
171
- input: c.input,
172
- ring: c.ring,
173
-
174
- hoverActive: c["hover-active"],
175
- },
176
- }