@interactive-inc/claude-funnel 0.10.0 → 0.10.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.
- package/dist/bin.js +448 -448
- package/dist/connectors/slack.d.ts +1 -29
- package/dist/gateway/daemon.js +166 -166
- package/dist/index.d.ts +4 -11
- package/dist/index.js +133 -120
- package/dist/slack-event-processor-CS-bAit9.d.ts +43 -0
- package/package.json +1 -6
- package/dist/slack-connector-schema-D7zAHN8k.d.ts +0 -15
- package/lib/bin.ts +0 -3
- package/lib/cli/factory.ts +0 -10
- package/lib/cli/index.ts +0 -85
- package/lib/cli/router/query-to-cli-args.ts +0 -20
- package/lib/cli/router/to-request.ts +0 -113
- package/lib/cli/router/validator.ts +0 -27
- package/lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts +0 -27
- package/lib/cli/routes/channels.$channel.connectors.$connector.request.ts +0 -40
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts +0 -41
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts +0 -22
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.ts +0 -23
- package/lib/cli/routes/channels.$channel.connectors.$connector.ts +0 -26
- package/lib/cli/routes/channels.$channel.connectors.add.$connector.ts +0 -92
- package/lib/cli/routes/channels.$channel.connectors.remove.$connector.ts +0 -22
- package/lib/cli/routes/channels.$channel.connectors.set.$connector.ts +0 -63
- package/lib/cli/routes/channels.$channel.connectors.ts +0 -26
- package/lib/cli/routes/channels.$channel.publish.ts +0 -52
- package/lib/cli/routes/channels.$channel.rename.$newName.ts +0 -22
- package/lib/cli/routes/channels.$channel.set.delivery.$mode.ts +0 -34
- package/lib/cli/routes/channels.$channel.ts +0 -34
- package/lib/cli/routes/channels.add.$channel.ts +0 -33
- package/lib/cli/routes/channels.remove.$channel.ts +0 -20
- package/lib/cli/routes/channels.ts +0 -39
- package/lib/cli/routes/claude.ts +0 -70
- package/lib/cli/routes/gateway.listeners.ts +0 -41
- package/lib/cli/routes/gateway.logs.ts +0 -123
- package/lib/cli/routes/gateway.restart.ts +0 -50
- package/lib/cli/routes/gateway.run.ts +0 -41
- package/lib/cli/routes/gateway.start.ts +0 -50
- package/lib/cli/routes/gateway.status.ts +0 -19
- package/lib/cli/routes/gateway.stop.ts +0 -32
- package/lib/cli/routes/gateway.ts +0 -55
- package/lib/cli/routes/index.ts +0 -219
- package/lib/cli/routes/profiles.$profile.as-default.ts +0 -22
- package/lib/cli/routes/profiles.$profile.rename.$newName.ts +0 -22
- package/lib/cli/routes/profiles.$profile.run.ts +0 -36
- package/lib/cli/routes/profiles.add.$profile.ts +0 -49
- package/lib/cli/routes/profiles.remove.$profile.ts +0 -20
- package/lib/cli/routes/profiles.set.$profile.ts +0 -45
- package/lib/cli/routes/profiles.ts +0 -40
- package/lib/cli/routes/status.ts +0 -93
- package/lib/cli/routes/update.ts +0 -27
- package/lib/connectors/connector-adapter.ts +0 -9
- package/lib/connectors/connector-config-schema.ts +0 -16
- package/lib/connectors/connector-factory.ts +0 -94
- package/lib/connectors/connector-listener.ts +0 -20
- package/lib/connectors/discord-adapter.ts +0 -51
- package/lib/connectors/discord-connector-schema.ts +0 -12
- package/lib/connectors/discord-event-processor.ts +0 -48
- package/lib/connectors/discord-listener.ts +0 -111
- package/lib/connectors/discord.ts +0 -4
- package/lib/connectors/gh-adapter.ts +0 -48
- package/lib/connectors/gh-connector-schema.ts +0 -12
- package/lib/connectors/gh-listener.ts +0 -137
- package/lib/connectors/gh.ts +0 -3
- package/lib/connectors/match-cron.ts +0 -78
- package/lib/connectors/schedule-connector-schema.ts +0 -33
- package/lib/connectors/schedule-listener.ts +0 -207
- package/lib/connectors/schedule-state-store.ts +0 -54
- package/lib/connectors/schedule.ts +0 -4
- package/lib/connectors/slack-adapter.ts +0 -36
- package/lib/connectors/slack-connector-schema.ts +0 -13
- package/lib/connectors/slack-event-processor.ts +0 -97
- package/lib/connectors/slack-listener.ts +0 -97
- package/lib/connectors/slack.ts +0 -4
- package/lib/engine/channels/channels.ts +0 -520
- package/lib/engine/claude/claude.ts +0 -205
- package/lib/engine/claude/gateway-controller.ts +0 -4
- package/lib/engine/fs/file-system.ts +0 -23
- package/lib/engine/fs/memory-file-system.ts +0 -102
- package/lib/engine/fs/node-file-system.ts +0 -68
- package/lib/engine/http/http-client.ts +0 -17
- package/lib/engine/http/memory-http-client.ts +0 -36
- package/lib/engine/http/node-http-client.ts +0 -23
- package/lib/engine/id/id-generator.ts +0 -7
- package/lib/engine/id/memory-id-generator.ts +0 -20
- package/lib/engine/id/node-id-generator.ts +0 -7
- package/lib/engine/logger/logger.ts +0 -11
- package/lib/engine/logger/memory-logger.ts +0 -28
- package/lib/engine/logger/node-logger.ts +0 -49
- package/lib/engine/logger/noop-logger.ts +0 -9
- package/lib/engine/mcp/channel-server.ts +0 -123
- package/lib/engine/mcp/channel-subscriber.ts +0 -82
- package/lib/engine/mcp/mcp.ts +0 -126
- package/lib/engine/mcp/read-channel-connectors.ts +0 -34
- package/lib/engine/mcp/read-gateway-token.ts +0 -16
- package/lib/engine/mcp/usage-hint-for-type.ts +0 -15
- package/lib/engine/process/memory-process-runner.ts +0 -88
- package/lib/engine/process/node-process-runner.ts +0 -91
- package/lib/engine/process/process-runner.ts +0 -33
- package/lib/engine/profiles/profile-channel-checker.ts +0 -7
- package/lib/engine/profiles/profiles.ts +0 -126
- package/lib/engine/settings/mock-settings-reader.ts +0 -27
- package/lib/engine/settings/settings-reader.ts +0 -6
- package/lib/engine/settings/settings-schema.ts +0 -48
- package/lib/engine/settings/settings-store.ts +0 -110
- package/lib/engine/time/clock.ts +0 -15
- package/lib/engine/time/memory-clock.ts +0 -26
- package/lib/engine/time/node-clock.ts +0 -7
- package/lib/funnel.ts +0 -294
- package/lib/gateway/auth-middleware.ts +0 -44
- package/lib/gateway/broadcaster.ts +0 -319
- package/lib/gateway/channel-publisher.ts +0 -67
- package/lib/gateway/daemon.ts +0 -47
- package/lib/gateway/factory.ts +0 -10
- package/lib/gateway/funnel-event-store.ts +0 -155
- package/lib/gateway/gateway-server.ts +0 -426
- package/lib/gateway/gateway-token.ts +0 -79
- package/lib/gateway/gateway.ts +0 -209
- package/lib/gateway/kill-competing-slack-gateways.ts +0 -56
- package/lib/gateway/listener-supervisor.ts +0 -339
- package/lib/gateway/listeners-client.ts +0 -128
- package/lib/gateway/publish-schema.ts +0 -27
- package/lib/gateway/resolve-daemon-script.ts +0 -26
- package/lib/gateway/routes/channels.connectors.call.ts +0 -39
- package/lib/gateway/routes/channels.publish.ts +0 -44
- package/lib/gateway/routes/health.ts +0 -13
- package/lib/gateway/routes/index.ts +0 -26
- package/lib/gateway/routes/listeners.list.ts +0 -6
- package/lib/gateway/routes/listeners.restart.ts +0 -15
- package/lib/gateway/routes/listeners.start.ts +0 -15
- package/lib/gateway/routes/listeners.stop.ts +0 -15
- package/lib/gateway/routes/route-deps.ts +0 -19
- package/lib/gateway/routes/status.ts +0 -15
- package/lib/gateway/routes/validator.ts +0 -17
- package/lib/index.ts +0 -67
- package/lib/logger/leuco-human-file-writer.ts +0 -65
- package/lib/logger/leuco-human-logger.ts +0 -98
- package/lib/logger/leuco-human-record.ts +0 -16
- package/lib/logger/leuco-human-stdout-writer.ts +0 -26
- package/lib/logger/leuco-human-writer.ts +0 -14
- package/lib/logger/leuco-logger-memory-sink.ts +0 -67
- package/lib/logger/leuco-logger-record.ts +0 -13
- package/lib/logger/leuco-logger-sink.ts +0 -33
- package/lib/logger/leuco-logger-sqlite-sink.ts +0 -355
- package/lib/logger/leuco-logger.ts +0 -135
- package/lib/tui/app.tsx +0 -357
- package/lib/tui/components/add-row.tsx +0 -18
- package/lib/tui/components/brand.tsx +0 -27
- package/lib/tui/components/card.tsx +0 -44
- package/lib/tui/components/detail-bar.tsx +0 -46
- package/lib/tui/components/editable-field.tsx +0 -33
- package/lib/tui/components/empty-state.tsx +0 -11
- package/lib/tui/components/gateway-status.tsx +0 -66
- package/lib/tui/components/keymap.tsx +0 -29
- package/lib/tui/components/menu-item.tsx +0 -73
- package/lib/tui/components/menu.tsx +0 -26
- package/lib/tui/components/panel-header.tsx +0 -22
- package/lib/tui/components/readonly-field.tsx +0 -18
- package/lib/tui/components/section-header.tsx +0 -25
- package/lib/tui/components/selection-accent.tsx +0 -32
- package/lib/tui/components/session-item.tsx +0 -33
- package/lib/tui/components/session-list.tsx +0 -33
- package/lib/tui/components/ui/hascii/accordion-item.tsx +0 -88
- package/lib/tui/components/ui/hascii/accordion.tsx +0 -96
- package/lib/tui/components/ui/hascii/alert-dialog.tsx +0 -43
- package/lib/tui/components/ui/hascii/badge.tsx +0 -51
- package/lib/tui/components/ui/hascii/breadcrumb.tsx +0 -58
- package/lib/tui/components/ui/hascii/button.tsx +0 -194
- package/lib/tui/components/ui/hascii/card-content.tsx +0 -14
- package/lib/tui/components/ui/hascii/card-description.tsx +0 -13
- package/lib/tui/components/ui/hascii/card-footer.tsx +0 -14
- package/lib/tui/components/ui/hascii/card-header.tsx +0 -14
- package/lib/tui/components/ui/hascii/card-title.tsx +0 -13
- package/lib/tui/components/ui/hascii/card.tsx +0 -27
- package/lib/tui/components/ui/hascii/checkbox.tsx +0 -65
- package/lib/tui/components/ui/hascii/command.tsx +0 -159
- package/lib/tui/components/ui/hascii/dialog-content.tsx +0 -14
- package/lib/tui/components/ui/hascii/dialog-description.tsx +0 -13
- package/lib/tui/components/ui/hascii/dialog-footer.tsx +0 -14
- package/lib/tui/components/ui/hascii/dialog-header.tsx +0 -14
- package/lib/tui/components/ui/hascii/dialog-title.tsx +0 -13
- package/lib/tui/components/ui/hascii/dialog.tsx +0 -27
- package/lib/tui/components/ui/hascii/file-tree.tsx +0 -142
- package/lib/tui/components/ui/hascii/focus-group.tsx +0 -62
- package/lib/tui/components/ui/hascii/form-item.tsx +0 -43
- package/lib/tui/components/ui/hascii/input-otp.tsx +0 -86
- package/lib/tui/components/ui/hascii/input.tsx +0 -130
- package/lib/tui/components/ui/hascii/pagination.tsx +0 -105
- package/lib/tui/components/ui/hascii/progress.tsx +0 -28
- package/lib/tui/components/ui/hascii/select.tsx +0 -131
- package/lib/tui/components/ui/hascii/separator.tsx +0 -35
- package/lib/tui/components/ui/hascii/sidebar-content.tsx +0 -23
- package/lib/tui/components/ui/hascii/sidebar-header.tsx +0 -14
- package/lib/tui/components/ui/hascii/sidebar-menu-item.tsx +0 -67
- package/lib/tui/components/ui/hascii/sidebar.tsx +0 -24
- package/lib/tui/components/ui/hascii/skeleton.tsx +0 -60
- package/lib/tui/components/ui/hascii/slider.tsx +0 -91
- package/lib/tui/components/ui/hascii/snackbar.tsx +0 -75
- package/lib/tui/components/ui/hascii/sparkline.tsx +0 -53
- package/lib/tui/components/ui/hascii/spinner.tsx +0 -47
- package/lib/tui/components/ui/hascii/stepper.tsx +0 -54
- package/lib/tui/components/ui/hascii/switch.tsx +0 -66
- package/lib/tui/components/ui/hascii/table.tsx +0 -95
- package/lib/tui/components/ui/hascii/tabs.tsx +0 -59
- package/lib/tui/components/ui/hascii/toggle-group-item.tsx +0 -45
- package/lib/tui/components/ui/hascii/toggle-group.tsx +0 -99
- package/lib/tui/components/ui/hascii/tree.tsx +0 -104
- package/lib/tui/components/view-shell.tsx +0 -44
- package/lib/tui/filter-input.tsx +0 -33
- package/lib/tui/hooks/hascii/use-pressable.ts +0 -54
- package/lib/tui/parse-comma-list.ts +0 -14
- package/lib/tui/profile-launcher.tsx +0 -61
- package/lib/tui/scrollbar-options.ts +0 -19
- package/lib/tui/sidebar.tsx +0 -50
- package/lib/tui/theme.ts +0 -40
- package/lib/tui/tui.tsx +0 -20
- package/lib/tui/types.ts +0 -38
- package/lib/tui/unique-name.ts +0 -18
- package/lib/tui/use-event-stream.ts +0 -133
- package/lib/tui/use-snapshot.ts +0 -99
- package/lib/tui/utils/hascii/form-item-context.tsx +0 -23
- package/lib/tui/utils/hascii/input-focus-context.tsx +0 -31
- package/lib/tui/utils/hascii/theme-context.tsx +0 -26
- package/lib/tui/utils/hascii/theme.ts +0 -176
- package/lib/tui/views/channels-view.tsx +0 -108
- package/lib/tui/views/connectors-view.tsx +0 -164
- package/lib/tui/views/events-view.tsx +0 -160
- package/lib/tui/views/listeners-view.tsx +0 -80
- package/lib/tui/views/profiles-view.tsx +0 -152
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { FunnelLogger } from "@/engine/logger/logger"
|
|
2
|
-
import { NodeFunnelLogger } from "@/engine/logger/node-logger"
|
|
3
|
-
import { FunnelProcessRunner } from "@/engine/process/process-runner"
|
|
4
|
-
import { NodeFunnelProcessRunner } from "@/engine/process/node-process-runner"
|
|
5
|
-
|
|
6
|
-
type Props = {
|
|
7
|
-
selfPid: number
|
|
8
|
-
process?: FunnelProcessRunner
|
|
9
|
-
logger?: FunnelLogger
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const defaultProcess = new NodeFunnelProcessRunner()
|
|
13
|
-
const defaultLogger = new NodeFunnelLogger()
|
|
14
|
-
|
|
15
|
-
const isBun = (args: string): boolean => {
|
|
16
|
-
return args.includes("bun ") || /\/bun(\s|$)/.test(args)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const looksLikeSlackGateway = (args: string): boolean => {
|
|
20
|
-
return /(gateway|bolt|slack)/i.test(args)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export const killCompetingSlackGateways = async (props: Props): Promise<number[]> => {
|
|
24
|
-
const runner = props.process ?? defaultProcess
|
|
25
|
-
const logger = props.logger ?? defaultLogger
|
|
26
|
-
const result = await runner.run(["ps", "-e", "-o", "pid=,args="])
|
|
27
|
-
|
|
28
|
-
if (result.exitCode !== 0) return []
|
|
29
|
-
|
|
30
|
-
const killed: number[] = []
|
|
31
|
-
|
|
32
|
-
for (const raw of result.stdout.split("\n")) {
|
|
33
|
-
const line = raw.trim()
|
|
34
|
-
|
|
35
|
-
if (!line) continue
|
|
36
|
-
|
|
37
|
-
const match = /^(\d+)\s+(.+)$/.exec(line)
|
|
38
|
-
|
|
39
|
-
if (!match) continue
|
|
40
|
-
|
|
41
|
-
const pid = Number(match[1])
|
|
42
|
-
const args = match[2]!
|
|
43
|
-
|
|
44
|
-
if (!Number.isInteger(pid) || pid <= 0) continue
|
|
45
|
-
if (pid === props.selfPid) continue
|
|
46
|
-
if (!isBun(args)) continue
|
|
47
|
-
if (!looksLikeSlackGateway(args)) continue
|
|
48
|
-
|
|
49
|
-
runner.kill(pid, "SIGTERM")
|
|
50
|
-
killed.push(pid)
|
|
51
|
-
|
|
52
|
-
logger.info("killed competing Slack gateway process", { pid, args: args.slice(0, 160) })
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return killed
|
|
56
|
-
}
|
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
import type { ConnectorConfig } from "@/connectors/connector-config-schema"
|
|
2
|
-
import type { FunnelConnectorListener } from "@/connectors/connector-listener"
|
|
3
|
-
import type { ChannelConnectorView } from "@/engine/channels/channels"
|
|
4
|
-
import { FunnelLogger } from "@/engine/logger/logger"
|
|
5
|
-
import { NodeFunnelLogger } from "@/engine/logger/node-logger"
|
|
6
|
-
|
|
7
|
-
type ConnectorRegistry = {
|
|
8
|
-
listAllConnectors(): ChannelConnectorView[]
|
|
9
|
-
createListener(
|
|
10
|
-
channelName: string,
|
|
11
|
-
connectorName: string,
|
|
12
|
-
): { config: ConnectorConfig; channelId: string; listener: FunnelConnectorListener } | null
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
type SupervisorNotify = (
|
|
16
|
-
channelName: string,
|
|
17
|
-
connectorName: string,
|
|
18
|
-
content: string,
|
|
19
|
-
meta?: Record<string, string>,
|
|
20
|
-
) => Promise<void>
|
|
21
|
-
|
|
22
|
-
type RunningEntry = {
|
|
23
|
-
config: ConnectorConfig
|
|
24
|
-
channelName: string
|
|
25
|
-
channelId: string
|
|
26
|
-
listener: FunnelConnectorListener
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
type ListenerStats = {
|
|
30
|
-
events: number
|
|
31
|
-
errors: number
|
|
32
|
-
failureCount: number
|
|
33
|
-
lastEventAt: string | null
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
type Deps = {
|
|
37
|
-
channels: ConnectorRegistry
|
|
38
|
-
notify: SupervisorNotify
|
|
39
|
-
logger?: FunnelLogger
|
|
40
|
-
healthCheckIntervalMs?: number
|
|
41
|
-
maxBackoffMs?: number
|
|
42
|
-
sleep?: (ms: number) => Promise<void>
|
|
43
|
-
now?: () => number
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const defaultLogger = new NodeFunnelLogger()
|
|
47
|
-
const DEFAULT_HEALTH_INTERVAL_MS = 30_000
|
|
48
|
-
const DEFAULT_MAX_BACKOFF_MS = 60_000
|
|
49
|
-
|
|
50
|
-
const defaultSleep = (ms: number): Promise<void> =>
|
|
51
|
-
new Promise((r) => {
|
|
52
|
-
setTimeout(r, ms)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
type ListenerEntryStatus = {
|
|
56
|
-
channelName: string
|
|
57
|
-
channelId: string
|
|
58
|
-
name: string
|
|
59
|
-
type: ConnectorConfig["type"]
|
|
60
|
-
alive: boolean
|
|
61
|
-
events: number
|
|
62
|
-
errors: number
|
|
63
|
-
failureCount: number
|
|
64
|
-
lastEventAt: string | null
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Owns the running listener instances and their lifecycle.
|
|
69
|
-
*
|
|
70
|
-
* Lives in the gateway process and is the only place that calls
|
|
71
|
-
* `listener.start()` / `listener.stop()`. Each entry is keyed by
|
|
72
|
-
* `${channelName}/${connectorName}` so the same connector name can exist in
|
|
73
|
-
* multiple channels without colliding.
|
|
74
|
-
*
|
|
75
|
-
* Periodically polls each running listener's `isAlive()` and auto-restarts
|
|
76
|
-
* dead listeners with exponential backoff (1s, 2s, 4s, ... capped). Resets
|
|
77
|
-
* the backoff counter on successful restart.
|
|
78
|
-
*/
|
|
79
|
-
export class FunnelListenerSupervisor {
|
|
80
|
-
private readonly channels: ConnectorRegistry
|
|
81
|
-
private readonly notify: SupervisorNotify
|
|
82
|
-
private readonly logger: FunnelLogger
|
|
83
|
-
private readonly running = new Map<string, RunningEntry>()
|
|
84
|
-
private readonly failureCounts = new Map<string, number>()
|
|
85
|
-
private readonly stats = new Map<string, ListenerStats>()
|
|
86
|
-
private readonly healthCheckIntervalMs: number
|
|
87
|
-
private readonly maxBackoffMs: number
|
|
88
|
-
private readonly sleep: (ms: number) => Promise<void>
|
|
89
|
-
private readonly now: () => number
|
|
90
|
-
private healthCheckTimer: ReturnType<typeof setInterval> | null = null
|
|
91
|
-
private healthCheckInFlight = false
|
|
92
|
-
|
|
93
|
-
constructor(deps: Deps) {
|
|
94
|
-
this.channels = deps.channels
|
|
95
|
-
this.notify = deps.notify
|
|
96
|
-
this.logger = deps.logger ?? defaultLogger
|
|
97
|
-
this.healthCheckIntervalMs = deps.healthCheckIntervalMs ?? DEFAULT_HEALTH_INTERVAL_MS
|
|
98
|
-
this.maxBackoffMs = deps.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS
|
|
99
|
-
this.sleep = deps.sleep ?? defaultSleep
|
|
100
|
-
this.now = deps.now ?? (() => Date.now())
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
static keyOf(channelName: string, connectorName: string): string {
|
|
104
|
-
return `${channelName}/${connectorName}`
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
isRunning(channelName: string, connectorName: string): boolean {
|
|
108
|
-
return this.running.has(FunnelListenerSupervisor.keyOf(channelName, connectorName))
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
list(): ListenerEntryStatus[] {
|
|
112
|
-
return [...this.running.entries()].map(([key, entry]) => {
|
|
113
|
-
const stats = this.stats.get(key)
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
channelName: entry.channelName,
|
|
117
|
-
channelId: entry.channelId,
|
|
118
|
-
name: entry.config.name,
|
|
119
|
-
type: entry.config.type,
|
|
120
|
-
alive: entry.listener.isAlive(),
|
|
121
|
-
events: stats?.events ?? 0,
|
|
122
|
-
errors: stats?.errors ?? 0,
|
|
123
|
-
failureCount: this.failureCounts.get(key) ?? 0,
|
|
124
|
-
lastEventAt: stats?.lastEventAt ?? null,
|
|
125
|
-
}
|
|
126
|
-
})
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
async start(
|
|
130
|
-
channelName: string,
|
|
131
|
-
connectorName: string,
|
|
132
|
-
): Promise<{ ok: boolean; reason?: string }> {
|
|
133
|
-
const key = FunnelListenerSupervisor.keyOf(channelName, connectorName)
|
|
134
|
-
|
|
135
|
-
if (this.running.has(key)) {
|
|
136
|
-
return { ok: true, reason: "already running" }
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const created = this.channels.createListener(channelName, connectorName)
|
|
140
|
-
|
|
141
|
-
if (!created) {
|
|
142
|
-
return {
|
|
143
|
-
ok: false,
|
|
144
|
-
reason: `connector "${connectorName}" not found in channel "${channelName}"`,
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const bind = async (content: string, meta?: Record<string, string>) => {
|
|
149
|
-
try {
|
|
150
|
-
await this.notify(channelName, connectorName, content, meta)
|
|
151
|
-
this.recordEvent(key)
|
|
152
|
-
} catch (error) {
|
|
153
|
-
this.recordError(key)
|
|
154
|
-
throw error
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
try {
|
|
159
|
-
await created.listener.start(bind)
|
|
160
|
-
this.running.set(key, {
|
|
161
|
-
config: created.config,
|
|
162
|
-
channelName,
|
|
163
|
-
channelId: created.channelId,
|
|
164
|
-
listener: created.listener,
|
|
165
|
-
})
|
|
166
|
-
this.ensureStats(key)
|
|
167
|
-
this.logger.info(`${created.config.type} listener started`, {
|
|
168
|
-
channel: channelName,
|
|
169
|
-
connector: connectorName,
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
return { ok: true }
|
|
173
|
-
} catch (error) {
|
|
174
|
-
this.logger.error(`${created.config.type} listener failed to start`, {
|
|
175
|
-
channel: channelName,
|
|
176
|
-
connector: connectorName,
|
|
177
|
-
error: error instanceof Error ? error.message : String(error),
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
return {
|
|
181
|
-
ok: false,
|
|
182
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
async stop(
|
|
188
|
-
channelName: string,
|
|
189
|
-
connectorName: string,
|
|
190
|
-
): Promise<{ ok: boolean; reason?: string }> {
|
|
191
|
-
const key = FunnelListenerSupervisor.keyOf(channelName, connectorName)
|
|
192
|
-
const entry = this.running.get(key)
|
|
193
|
-
|
|
194
|
-
if (!entry) return { ok: true, reason: "not running" }
|
|
195
|
-
|
|
196
|
-
try {
|
|
197
|
-
await entry.listener.stop()
|
|
198
|
-
this.running.delete(key)
|
|
199
|
-
this.failureCounts.delete(key)
|
|
200
|
-
this.logger.info(`${entry.config.type} listener stopped`, {
|
|
201
|
-
channel: channelName,
|
|
202
|
-
connector: connectorName,
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
return { ok: true }
|
|
206
|
-
} catch (error) {
|
|
207
|
-
this.logger.error(`${entry.config.type} listener failed to stop`, {
|
|
208
|
-
channel: channelName,
|
|
209
|
-
connector: connectorName,
|
|
210
|
-
error: error instanceof Error ? error.message : String(error),
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
ok: false,
|
|
215
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
async restart(
|
|
221
|
-
channelName: string,
|
|
222
|
-
connectorName: string,
|
|
223
|
-
): Promise<{ ok: boolean; reason?: string }> {
|
|
224
|
-
const stopped = await this.stop(channelName, connectorName)
|
|
225
|
-
|
|
226
|
-
if (!stopped.ok) return stopped
|
|
227
|
-
|
|
228
|
-
return await this.start(channelName, connectorName)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async startAll(): Promise<void> {
|
|
232
|
-
const all = this.channels.listAllConnectors()
|
|
233
|
-
|
|
234
|
-
for (const view of all) {
|
|
235
|
-
await this.start(view.channelName, view.name)
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
this.startHealthCheck()
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async stopAll(): Promise<void> {
|
|
242
|
-
this.stopHealthCheck()
|
|
243
|
-
|
|
244
|
-
for (const [, entry] of [...this.running.entries()]) {
|
|
245
|
-
await this.stop(entry.channelName, entry.config.name)
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
private ensureStats(key: string): ListenerStats {
|
|
250
|
-
const existing = this.stats.get(key)
|
|
251
|
-
|
|
252
|
-
if (existing) return existing
|
|
253
|
-
|
|
254
|
-
const fresh: ListenerStats = { events: 0, errors: 0, failureCount: 0, lastEventAt: null }
|
|
255
|
-
|
|
256
|
-
this.stats.set(key, fresh)
|
|
257
|
-
|
|
258
|
-
return fresh
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
private recordEvent(key: string): void {
|
|
262
|
-
const stats = this.ensureStats(key)
|
|
263
|
-
|
|
264
|
-
stats.events += 1
|
|
265
|
-
stats.lastEventAt = new Date(this.now()).toISOString()
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
private recordError(key: string): void {
|
|
269
|
-
this.ensureStats(key).errors += 1
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
private startHealthCheck(): void {
|
|
273
|
-
if (this.healthCheckTimer) return
|
|
274
|
-
|
|
275
|
-
this.healthCheckTimer = setInterval(() => {
|
|
276
|
-
void this.runHealthCheck()
|
|
277
|
-
}, this.healthCheckIntervalMs)
|
|
278
|
-
|
|
279
|
-
this.healthCheckTimer.unref()
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
private stopHealthCheck(): void {
|
|
283
|
-
if (!this.healthCheckTimer) return
|
|
284
|
-
|
|
285
|
-
clearInterval(this.healthCheckTimer)
|
|
286
|
-
this.healthCheckTimer = null
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
private async runHealthCheck(): Promise<void> {
|
|
290
|
-
if (this.healthCheckInFlight) return
|
|
291
|
-
|
|
292
|
-
this.healthCheckInFlight = true
|
|
293
|
-
|
|
294
|
-
try {
|
|
295
|
-
for (const [key, entry] of [...this.running.entries()]) {
|
|
296
|
-
if (entry.listener.isAlive()) {
|
|
297
|
-
this.failureCounts.delete(key)
|
|
298
|
-
continue
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
await this.recoverDead(entry.channelName, entry.config.name, entry.config.type)
|
|
302
|
-
}
|
|
303
|
-
} finally {
|
|
304
|
-
this.healthCheckInFlight = false
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
private async recoverDead(
|
|
309
|
-
channelName: string,
|
|
310
|
-
connectorName: string,
|
|
311
|
-
type: ConnectorConfig["type"],
|
|
312
|
-
): Promise<void> {
|
|
313
|
-
const key = FunnelListenerSupervisor.keyOf(channelName, connectorName)
|
|
314
|
-
const failureCount = this.failureCounts.get(key) ?? 0
|
|
315
|
-
const backoffMs = Math.min(1000 * 2 ** failureCount, this.maxBackoffMs)
|
|
316
|
-
|
|
317
|
-
this.logger.warn(`${type} listener unhealthy, restarting`, {
|
|
318
|
-
channel: channelName,
|
|
319
|
-
connector: connectorName,
|
|
320
|
-
attempt: failureCount + 1,
|
|
321
|
-
backoffMs,
|
|
322
|
-
})
|
|
323
|
-
|
|
324
|
-
await this.stop(channelName, connectorName)
|
|
325
|
-
await this.sleep(backoffMs)
|
|
326
|
-
|
|
327
|
-
const result = await this.start(channelName, connectorName)
|
|
328
|
-
|
|
329
|
-
if (result.ok) {
|
|
330
|
-
this.failureCounts.delete(key)
|
|
331
|
-
this.logger.info(`${type} listener recovered`, {
|
|
332
|
-
channel: channelName,
|
|
333
|
-
connector: connectorName,
|
|
334
|
-
})
|
|
335
|
-
} else {
|
|
336
|
-
this.failureCounts.set(key, failureCount + 1)
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { z } from "zod"
|
|
2
|
-
|
|
3
|
-
type Deps = {
|
|
4
|
-
port: number
|
|
5
|
-
isDaemonRunning: () => boolean
|
|
6
|
-
/** Returns the daemon's gateway token, or null if unavailable. Sent as `Authorization: Bearer`. */
|
|
7
|
-
getToken?: () => string | null
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const listenerEntrySchema = z.object({
|
|
11
|
-
channelName: z.string(),
|
|
12
|
-
channelId: z.string(),
|
|
13
|
-
name: z.string(),
|
|
14
|
-
type: z.string(),
|
|
15
|
-
alive: z.boolean(),
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
const listenersResponseSchema = z.object({
|
|
19
|
-
listeners: z.array(listenerEntrySchema),
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
const opErrorBodySchema = z.object({
|
|
23
|
-
reason: z.string().optional(),
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
export type ListenerEntry = z.infer<typeof listenerEntrySchema>
|
|
27
|
-
|
|
28
|
-
export type ListenerOpResult =
|
|
29
|
-
| { state: "ok" }
|
|
30
|
-
| { state: "offline" }
|
|
31
|
-
| { state: "error"; reason: string }
|
|
32
|
-
|
|
33
|
-
export type ListListenersResult =
|
|
34
|
-
| { state: "ok"; listeners: ListenerEntry[] }
|
|
35
|
-
| { state: "offline" }
|
|
36
|
-
| { state: "error"; reason: string }
|
|
37
|
-
|
|
38
|
-
const OFFLINE: ListenerOpResult = { state: "offline" }
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* HTTP client for listener operations on a running gateway daemon.
|
|
42
|
-
*
|
|
43
|
-
* Returns `{ state: "offline" }` when the daemon isn't running so callers
|
|
44
|
-
* (CLI hot-reload paths) can treat that as a no-op without parsing strings.
|
|
45
|
-
* Pair this with `FunnelGateway` (process control) for the full picture.
|
|
46
|
-
*/
|
|
47
|
-
export class FunnelListenersClient {
|
|
48
|
-
private readonly port: number
|
|
49
|
-
private readonly isDaemonRunning: () => boolean
|
|
50
|
-
private readonly getToken: () => string | null
|
|
51
|
-
|
|
52
|
-
constructor(deps: Deps) {
|
|
53
|
-
this.port = deps.port
|
|
54
|
-
this.isDaemonRunning = deps.isDaemonRunning
|
|
55
|
-
this.getToken = deps.getToken ?? (() => null)
|
|
56
|
-
Object.freeze(this)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async list(): Promise<ListListenersResult> {
|
|
60
|
-
if (!this.isDaemonRunning()) return { state: "offline" }
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
const res = await fetch(`http://localhost:${this.port}/listeners`, {
|
|
64
|
-
headers: this.authHeaders(),
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
if (!res.ok) return { state: "error", reason: `HTTP ${res.status}` }
|
|
68
|
-
|
|
69
|
-
const parsed = listenersResponseSchema.safeParse(await res.json())
|
|
70
|
-
|
|
71
|
-
if (!parsed.success) {
|
|
72
|
-
return { state: "error", reason: "malformed daemon response" }
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return { state: "ok", listeners: parsed.data.listeners }
|
|
76
|
-
} catch (error) {
|
|
77
|
-
return { state: "error", reason: error instanceof Error ? error.message : String(error) }
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async start(channelName: string, connectorName: string): Promise<ListenerOpResult> {
|
|
82
|
-
if (!this.isDaemonRunning()) return OFFLINE
|
|
83
|
-
|
|
84
|
-
return await this.call("POST", `/listeners/${this.path(channelName, connectorName)}/start`)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async stop(channelName: string, connectorName: string): Promise<ListenerOpResult> {
|
|
88
|
-
if (!this.isDaemonRunning()) return OFFLINE
|
|
89
|
-
|
|
90
|
-
return await this.call("DELETE", `/listeners/${this.path(channelName, connectorName)}`)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async restart(channelName: string, connectorName: string): Promise<ListenerOpResult> {
|
|
94
|
-
if (!this.isDaemonRunning()) return OFFLINE
|
|
95
|
-
|
|
96
|
-
return await this.call("POST", `/listeners/${this.path(channelName, connectorName)}/restart`)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
private path(channelName: string, connectorName: string): string {
|
|
100
|
-
return `${encodeURIComponent(channelName)}/${encodeURIComponent(connectorName)}`
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
private authHeaders(): Record<string, string> {
|
|
104
|
-
const token = this.getToken()
|
|
105
|
-
|
|
106
|
-
return token ? { authorization: `Bearer ${token}` } : {}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
private async call(method: "POST" | "DELETE", path: string): Promise<ListenerOpResult> {
|
|
110
|
-
try {
|
|
111
|
-
const res = await fetch(`http://localhost:${this.port}${path}`, {
|
|
112
|
-
method,
|
|
113
|
-
headers: this.authHeaders(),
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
if (!res.ok) {
|
|
117
|
-
const parsed = opErrorBodySchema.safeParse(await res.json().catch(() => null))
|
|
118
|
-
const reason = parsed.success ? parsed.data.reason : undefined
|
|
119
|
-
|
|
120
|
-
return { state: "error", reason: reason ?? `HTTP ${res.status}` }
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return { state: "ok" }
|
|
124
|
-
} catch (error) {
|
|
125
|
-
return { state: "error", reason: error instanceof Error ? error.message : String(error) }
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { z } from "zod"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Shared schema for `POST /channels/:channel/publish` — used by both the
|
|
5
|
-
* gateway route handler (input validation) and the CLI / programmable client
|
|
6
|
-
* (request shape). The route resolves `channel` from the path; this body
|
|
7
|
-
* covers everything else.
|
|
8
|
-
*/
|
|
9
|
-
export const publishRequestSchema = z.object({
|
|
10
|
-
content: z.string().min(1),
|
|
11
|
-
meta: z.record(z.string(), z.string()).optional(),
|
|
12
|
-
connector: z.string().min(1).optional(),
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
export type PublishRequest = z.infer<typeof publishRequestSchema>
|
|
16
|
-
|
|
17
|
-
export const publishResponseSchema = z.object({
|
|
18
|
-
ok: z.literal(true),
|
|
19
|
-
offset: z.number().int().nonnegative(),
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
export type PublishResponse = z.infer<typeof publishResponseSchema>
|
|
23
|
-
|
|
24
|
-
export type PublishResult =
|
|
25
|
-
| { state: "ok"; offset: number }
|
|
26
|
-
| { state: "offline" }
|
|
27
|
-
| { state: "error"; reason: string }
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs"
|
|
2
|
-
import { resolve } from "node:path"
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Locate the daemon entry script. Works in both dev (running from source)
|
|
6
|
-
* and built mode (bundled into dist/bin.js with daemon at dist/gateway/daemon.js).
|
|
7
|
-
*
|
|
8
|
-
* The candidates cover:
|
|
9
|
-
* 1. dev: this helper lives at lib/gateway/, so daemon.ts is its sibling
|
|
10
|
-
* 2. built sibling: dist/gateway/daemon.js if the helper itself ends up at dist/gateway/
|
|
11
|
-
* 3. bundled: when this helper is inlined into dist/bin.js, import.meta.dir is dist/,
|
|
12
|
-
* and daemon.js lives at dist/gateway/daemon.js
|
|
13
|
-
*/
|
|
14
|
-
export const resolveDaemonScript = (): string => {
|
|
15
|
-
const candidates = [
|
|
16
|
-
resolve(import.meta.dir, "./daemon.ts"),
|
|
17
|
-
resolve(import.meta.dir, "./daemon.js"),
|
|
18
|
-
resolve(import.meta.dir, "./gateway/daemon.js"),
|
|
19
|
-
]
|
|
20
|
-
|
|
21
|
-
for (const candidate of candidates) {
|
|
22
|
-
if (existsSync(candidate)) return candidate
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
throw new Error(`daemon script not found (looked in ${candidates.join(", ")})`)
|
|
26
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { HTTPException } from "hono/http-exception"
|
|
2
|
-
import { z } from "zod"
|
|
3
|
-
import { factory } from "@/gateway/factory"
|
|
4
|
-
import { zParam } from "@/gateway/routes/validator"
|
|
5
|
-
|
|
6
|
-
const bodySchema = z.object({
|
|
7
|
-
method: z.string().min(1),
|
|
8
|
-
path: z.string().min(1),
|
|
9
|
-
body: z.unknown().optional(),
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* POST /channels/:channel/connectors/:connector/call
|
|
14
|
-
*
|
|
15
|
-
* Generic adapter call. Used by the funnel MCP server (running in the Claude
|
|
16
|
-
* Code process) to send replies/reactions/etc. without spawning a CLI
|
|
17
|
-
* subprocess. Mirrors the CLI's `funnel channels <c> connectors <conn> request
|
|
18
|
-
* --method=...` but with a structured JSON body and no shell.
|
|
19
|
-
*/
|
|
20
|
-
export const channelsConnectorsCallHandler = factory.createHandlers(
|
|
21
|
-
zParam(z.object({ channel: z.string().min(1), connector: z.string().min(1) })),
|
|
22
|
-
async (c) => {
|
|
23
|
-
const param = c.req.valid("param")
|
|
24
|
-
const raw = await c.req.json().catch(() => null)
|
|
25
|
-
const parsed = bodySchema.safeParse(raw)
|
|
26
|
-
|
|
27
|
-
if (!parsed.success) {
|
|
28
|
-
throw new HTTPException(400, { message: parsed.error.issues[0]?.message ?? "invalid body" })
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const result = await c.var.deps.channels.call(param.channel, param.connector, {
|
|
32
|
-
method: parsed.data.method,
|
|
33
|
-
path: parsed.data.path,
|
|
34
|
-
body: parsed.data.body ?? {},
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
return c.json({ ok: true, result })
|
|
38
|
-
},
|
|
39
|
-
)
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { zValidator } from "@hono/zod-validator"
|
|
2
|
-
import { z } from "zod"
|
|
3
|
-
import { factory } from "@/gateway/factory"
|
|
4
|
-
import { publishRequestSchema } from "@/gateway/publish-schema"
|
|
5
|
-
import type { PublishResponse } from "@/gateway/publish-schema"
|
|
6
|
-
import { zParam } from "@/gateway/routes/validator"
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* POST /channels/:channel/publish
|
|
10
|
-
*
|
|
11
|
-
* Inject arbitrary content into a channel. Mirrors the connector-driven `notify`
|
|
12
|
-
* path: events go through `broadcaster.broadcast` + `eventStore.record`, so
|
|
13
|
-
* subscribers see them exactly as if a listener had produced them.
|
|
14
|
-
*
|
|
15
|
-
* Body validation is Zod-shared with the client (`publishRequestSchema`); the
|
|
16
|
-
* response (`publishResponseSchema`) carries the assigned offset so callers can
|
|
17
|
-
* correlate with the persistent event store.
|
|
18
|
-
*/
|
|
19
|
-
export const channelsPublishHandler = factory.createHandlers(
|
|
20
|
-
zParam(z.object({ channel: z.string().min(1) })),
|
|
21
|
-
zValidator("json", publishRequestSchema, (result, c) => {
|
|
22
|
-
if (result.success) return
|
|
23
|
-
|
|
24
|
-
const issue = result.error.issues[0]
|
|
25
|
-
const reason = issue ? `${issue.path.join(".")}: ${issue.message}` : "invalid body"
|
|
26
|
-
|
|
27
|
-
return c.json({ ok: false, reason }, 400)
|
|
28
|
-
}),
|
|
29
|
-
(c) => {
|
|
30
|
-
const param = c.req.valid("param")
|
|
31
|
-
const body = c.req.valid("json")
|
|
32
|
-
|
|
33
|
-
const event = c.var.deps.emit({
|
|
34
|
-
channel: param.channel,
|
|
35
|
-
connector: body.connector,
|
|
36
|
-
content: body.content,
|
|
37
|
-
meta: body.meta,
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
const response: PublishResponse = { ok: true, offset: event.offset }
|
|
41
|
-
|
|
42
|
-
return c.json(response)
|
|
43
|
-
},
|
|
44
|
-
)
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { factory } from "@/gateway/factory"
|
|
2
|
-
|
|
3
|
-
/** GET /health — liveness + listener registry snapshot. */
|
|
4
|
-
export const healthHandler = factory.createHandlers((c) => {
|
|
5
|
-
const deps = c.var.deps
|
|
6
|
-
|
|
7
|
-
return c.json({
|
|
8
|
-
ok: true,
|
|
9
|
-
pid: deps.selfPid,
|
|
10
|
-
clients: deps.broadcaster.getClientCount(),
|
|
11
|
-
listeners: deps.supervisor.list(),
|
|
12
|
-
})
|
|
13
|
-
})
|