@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,207 +0,0 @@
|
|
|
1
|
-
import { FunnelConnectorListener, type NotifyFn } from "@/connectors/connector-listener"
|
|
2
|
-
import { matchCron } from "@/connectors/match-cron"
|
|
3
|
-
import { ScheduleStateStore } from "@/connectors/schedule-state-store"
|
|
4
|
-
import { FunnelLogger } from "@/engine/logger/logger"
|
|
5
|
-
import { NodeFunnelLogger } from "@/engine/logger/node-logger"
|
|
6
|
-
import type { ScheduleConnectorConfig, ScheduleEntry } from "@/connectors/schedule-connector-schema"
|
|
7
|
-
|
|
8
|
-
type Deps = {
|
|
9
|
-
config: ScheduleConnectorConfig
|
|
10
|
-
lastFiredStore: ScheduleStateStore
|
|
11
|
-
logger?: FunnelLogger
|
|
12
|
-
now?: () => Date
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const defaultLogger = new NodeFunnelLogger()
|
|
16
|
-
|
|
17
|
-
const MAX_CATCHUP_MINUTES = 60 * 24
|
|
18
|
-
|
|
19
|
-
export class FunnelScheduleListener extends FunnelConnectorListener {
|
|
20
|
-
private readonly config: ScheduleConnectorConfig
|
|
21
|
-
private readonly lastFiredStore: ScheduleStateStore
|
|
22
|
-
private readonly logger: FunnelLogger
|
|
23
|
-
private readonly now: () => Date
|
|
24
|
-
private timer: ReturnType<typeof setTimeout> | null = null
|
|
25
|
-
private stopped = false
|
|
26
|
-
|
|
27
|
-
constructor(deps: Deps) {
|
|
28
|
-
super()
|
|
29
|
-
this.config = deps.config
|
|
30
|
-
this.lastFiredStore = deps.lastFiredStore
|
|
31
|
-
this.logger = deps.logger ?? defaultLogger
|
|
32
|
-
this.now = deps.now ?? (() => new Date())
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async start(notify: NotifyFn): Promise<void> {
|
|
36
|
-
this.stopped = false
|
|
37
|
-
|
|
38
|
-
const scheduleNext = () => {
|
|
39
|
-
if (this.stopped) return
|
|
40
|
-
|
|
41
|
-
const date = this.now()
|
|
42
|
-
const msUntilNextMinute = 60_000 - (date.getSeconds() * 1000 + date.getMilliseconds())
|
|
43
|
-
this.timer = setTimeout(async () => {
|
|
44
|
-
if (this.stopped) return
|
|
45
|
-
await this.tick(notify)
|
|
46
|
-
scheduleNext()
|
|
47
|
-
}, msUntilNextMinute)
|
|
48
|
-
|
|
49
|
-
this.timer.unref()
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
await this.tick(notify)
|
|
53
|
-
scheduleNext()
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async stop(): Promise<void> {
|
|
57
|
-
this.stopped = true
|
|
58
|
-
|
|
59
|
-
if (this.timer) {
|
|
60
|
-
clearTimeout(this.timer)
|
|
61
|
-
this.timer = null
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
override isAlive(): boolean {
|
|
66
|
-
return !this.stopped && this.timer !== null
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async tick(notify: NotifyFn): Promise<void> {
|
|
70
|
-
const now = this.truncateToMinute(this.now())
|
|
71
|
-
const state = this.lastFiredStore.load()
|
|
72
|
-
let changed = false
|
|
73
|
-
|
|
74
|
-
for (const entry of this.config.entries) {
|
|
75
|
-
if (!entry.enabled) continue
|
|
76
|
-
|
|
77
|
-
const fired = await this.fireEntry(entry, now, state, notify)
|
|
78
|
-
|
|
79
|
-
if (fired) changed = true
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (changed) this.lastFiredStore.save(state)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
private async fireEntry(
|
|
86
|
-
entry: ScheduleEntry,
|
|
87
|
-
now: Date,
|
|
88
|
-
state: Map<string, Date>,
|
|
89
|
-
notify: NotifyFn,
|
|
90
|
-
): Promise<boolean> {
|
|
91
|
-
const lastFired = state.get(entry.id)
|
|
92
|
-
const searchFrom = lastFired ? new Date(lastFired.getTime() + 60_000) : now
|
|
93
|
-
|
|
94
|
-
if (searchFrom.getTime() > now.getTime()) return false
|
|
95
|
-
|
|
96
|
-
if (entry.catchupPolicy === "skip") {
|
|
97
|
-
try {
|
|
98
|
-
if (!matchCron(entry.cron, now)) return false
|
|
99
|
-
} catch (error) {
|
|
100
|
-
this.logInvalidCron(entry, error)
|
|
101
|
-
return false
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
await this.notifyOne(entry, now, notify, false)
|
|
105
|
-
state.set(entry.id, now)
|
|
106
|
-
return true
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (entry.catchupPolicy === "all") {
|
|
110
|
-
const matches = this.findAllMatches(entry.cron, searchFrom, now, entry.id)
|
|
111
|
-
|
|
112
|
-
if (matches.length === 0) return false
|
|
113
|
-
|
|
114
|
-
for (const match of matches) {
|
|
115
|
-
await this.notifyOne(entry, match, notify, match.getTime() !== now.getTime())
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
state.set(entry.id, matches[matches.length - 1] ?? now)
|
|
119
|
-
return true
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const match = this.findMostRecentMatch(entry.cron, searchFrom, now, entry.id)
|
|
123
|
-
|
|
124
|
-
if (!match) return false
|
|
125
|
-
|
|
126
|
-
await this.notifyOne(entry, match, notify, match.getTime() !== now.getTime())
|
|
127
|
-
state.set(entry.id, match)
|
|
128
|
-
return true
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
private async notifyOne(
|
|
132
|
-
entry: ScheduleEntry,
|
|
133
|
-
firedAt: Date,
|
|
134
|
-
notify: NotifyFn,
|
|
135
|
-
catchup: boolean,
|
|
136
|
-
): Promise<void> {
|
|
137
|
-
const meta: Record<string, string> = {
|
|
138
|
-
event_type: "schedule",
|
|
139
|
-
schedule_id: entry.id,
|
|
140
|
-
cron: entry.cron,
|
|
141
|
-
fired_at: firedAt.toISOString(),
|
|
142
|
-
catchup_policy: entry.catchupPolicy,
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (catchup) meta.catchup = "true"
|
|
146
|
-
|
|
147
|
-
await notify(entry.prompt, meta)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
private findMostRecentMatch(cron: string, from: Date, until: Date, entryId: string): Date | null {
|
|
151
|
-
const maxIterations = Math.min(
|
|
152
|
-
MAX_CATCHUP_MINUTES,
|
|
153
|
-
Math.floor((until.getTime() - from.getTime()) / 60_000) + 1,
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
for (let i = 0; i < maxIterations; i++) {
|
|
157
|
-
const candidate = new Date(until.getTime() - i * 60_000)
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
if (matchCron(cron, candidate)) return candidate
|
|
161
|
-
} catch (error) {
|
|
162
|
-
this.logInvalidCron({ id: entryId, cron } as ScheduleEntry, error)
|
|
163
|
-
return null
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return null
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
private findAllMatches(cron: string, from: Date, until: Date, entryId: string): Date[] {
|
|
171
|
-
const maxIterations = Math.min(
|
|
172
|
-
MAX_CATCHUP_MINUTES,
|
|
173
|
-
Math.floor((until.getTime() - from.getTime()) / 60_000) + 1,
|
|
174
|
-
)
|
|
175
|
-
const matches: Date[] = []
|
|
176
|
-
|
|
177
|
-
for (let i = 0; i < maxIterations; i++) {
|
|
178
|
-
const candidate = new Date(from.getTime() + i * 60_000)
|
|
179
|
-
|
|
180
|
-
if (candidate.getTime() > until.getTime()) break
|
|
181
|
-
|
|
182
|
-
try {
|
|
183
|
-
if (matchCron(cron, candidate)) matches.push(candidate)
|
|
184
|
-
} catch (error) {
|
|
185
|
-
this.logInvalidCron({ id: entryId, cron } as ScheduleEntry, error)
|
|
186
|
-
return []
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return matches
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
private logInvalidCron(entry: Pick<ScheduleEntry, "id" | "cron">, error: unknown): void {
|
|
194
|
-
this.logger.error("invalid cron expression in schedule", {
|
|
195
|
-
connector: this.config.name,
|
|
196
|
-
id: entry.id,
|
|
197
|
-
cron: entry.cron,
|
|
198
|
-
error: error instanceof Error ? error.message : String(error),
|
|
199
|
-
})
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
private truncateToMinute(date: Date): Date {
|
|
203
|
-
const copy = new Date(date.getTime())
|
|
204
|
-
copy.setSeconds(0, 0)
|
|
205
|
-
return copy
|
|
206
|
-
}
|
|
207
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { dirname } from "node:path"
|
|
2
|
-
import { FunnelFileSystem } from "@/engine/fs/file-system"
|
|
3
|
-
import { NodeFunnelFileSystem } from "@/engine/fs/node-file-system"
|
|
4
|
-
|
|
5
|
-
type Deps = {
|
|
6
|
-
path: string
|
|
7
|
-
fs?: FunnelFileSystem
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const defaultFs = new NodeFunnelFileSystem()
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Per-connector lastFiredAt persistence for the schedule listener. The path is
|
|
14
|
-
* passed in by FunnelConnectorFactory so this store does not know about the
|
|
15
|
-
* funnel directory layout (`channels/<id>/connectors/<id>/state.json` lives
|
|
16
|
-
* outside this class).
|
|
17
|
-
*/
|
|
18
|
-
export class ScheduleStateStore {
|
|
19
|
-
private readonly path: string
|
|
20
|
-
private readonly fs: FunnelFileSystem
|
|
21
|
-
|
|
22
|
-
constructor(deps: Deps) {
|
|
23
|
-
this.path = deps.path
|
|
24
|
-
this.fs = deps.fs ?? defaultFs
|
|
25
|
-
Object.freeze(this)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
load(): Map<string, Date> {
|
|
29
|
-
const map = new Map<string, Date>()
|
|
30
|
-
|
|
31
|
-
if (!this.fs.existsSync(this.path)) return map
|
|
32
|
-
|
|
33
|
-
const raw: unknown = JSON.parse(this.fs.readFileSync(this.path))
|
|
34
|
-
|
|
35
|
-
if (raw === null || typeof raw !== "object") return map
|
|
36
|
-
|
|
37
|
-
for (const [id, iso] of Object.entries(raw)) {
|
|
38
|
-
if (typeof iso === "string") map.set(id, new Date(iso))
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return map
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
save(state: Map<string, Date>): void {
|
|
45
|
-
const obj: Record<string, string> = {}
|
|
46
|
-
|
|
47
|
-
for (const [id, date] of state) {
|
|
48
|
-
obj[id] = date.toISOString()
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
this.fs.mkdirSync(dirname(this.path), { recursive: true })
|
|
52
|
-
this.fs.writeFileSync(this.path, `${JSON.stringify(obj, null, 2)}\n`)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { WebClient } from "@slack/web-api"
|
|
2
|
-
import { FunnelConnectorAdapter, type CallInput } from "@/connectors/connector-adapter"
|
|
3
|
-
import type { SlackConnectorConfig } from "@/connectors/slack-connector-schema"
|
|
4
|
-
|
|
5
|
-
export type SlackWebClientLike = {
|
|
6
|
-
apiCall: (method: string, options?: Record<string, unknown>) => Promise<unknown>
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const toRecord = (value: object): Record<string, unknown> => {
|
|
10
|
-
const result: Record<string, unknown> = {}
|
|
11
|
-
|
|
12
|
-
for (const [key, val] of Object.entries(value)) result[key] = val
|
|
13
|
-
|
|
14
|
-
return result
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
type Deps = {
|
|
18
|
-
config: SlackConnectorConfig
|
|
19
|
-
client?: SlackWebClientLike
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export class FunnelSlackAdapter extends FunnelConnectorAdapter {
|
|
23
|
-
private readonly client: SlackWebClientLike
|
|
24
|
-
|
|
25
|
-
constructor(deps: Deps) {
|
|
26
|
-
super()
|
|
27
|
-
this.client = deps.client ?? new WebClient(deps.config.botToken)
|
|
28
|
-
Object.freeze(this)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async call(input: CallInput): Promise<unknown> {
|
|
32
|
-
const body = input.body !== null && typeof input.body === "object" ? toRecord(input.body) : {}
|
|
33
|
-
|
|
34
|
-
return await this.client.apiCall(input.path, body)
|
|
35
|
-
}
|
|
36
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { z } from "zod"
|
|
2
|
-
|
|
3
|
-
export const slackConnectorSchema = z.object({
|
|
4
|
-
id: z.string(),
|
|
5
|
-
name: z.string(),
|
|
6
|
-
type: z.literal("slack"),
|
|
7
|
-
botToken: z.string().startsWith("xoxb-"),
|
|
8
|
-
appToken: z.string().startsWith("xapp-"),
|
|
9
|
-
createdAt: z.string().datetime().optional(),
|
|
10
|
-
updatedAt: z.string().datetime().optional(),
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
export type SlackConnectorConfig = z.infer<typeof slackConnectorSchema>
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
export type SlackRawEvent = Record<string, unknown>
|
|
2
|
-
|
|
3
|
-
export type SlackProcessedSkip = { skip: true }
|
|
4
|
-
|
|
5
|
-
export type SlackProcessedEmit = {
|
|
6
|
-
skip: false
|
|
7
|
-
content: string
|
|
8
|
-
meta: Record<string, string>
|
|
9
|
-
shouldReact: boolean
|
|
10
|
-
channel: string
|
|
11
|
-
timestamp: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export type SlackProcessed = SlackProcessedSkip | SlackProcessedEmit
|
|
15
|
-
|
|
16
|
-
const ALLOWED_EVENTS = new Set(["message", "app_mention"])
|
|
17
|
-
const ALLOWED_SUBTYPES = new Set<string | undefined>([
|
|
18
|
-
undefined,
|
|
19
|
-
"thread_broadcast",
|
|
20
|
-
"bot_message",
|
|
21
|
-
"file_share",
|
|
22
|
-
])
|
|
23
|
-
|
|
24
|
-
const DEDUP_WINDOW = 10_000
|
|
25
|
-
|
|
26
|
-
type Props = {
|
|
27
|
-
ownBotUserId: string
|
|
28
|
-
ownBotId: string
|
|
29
|
-
now?: () => number
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const getString = (event: SlackRawEvent, key: string): string | undefined => {
|
|
33
|
-
const value = event[key]
|
|
34
|
-
|
|
35
|
-
return typeof value === "string" ? value : undefined
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export class FunnelSlackEventProcessor {
|
|
39
|
-
private readonly ownBotUserId: string
|
|
40
|
-
private readonly ownBotId: string
|
|
41
|
-
private readonly now: () => number
|
|
42
|
-
private readonly dedup = new Map<string, number>()
|
|
43
|
-
|
|
44
|
-
constructor(props: Props) {
|
|
45
|
-
this.ownBotUserId = props.ownBotUserId
|
|
46
|
-
this.ownBotId = props.ownBotId
|
|
47
|
-
this.now = props.now ?? (() => Date.now())
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
process(event: SlackRawEvent): SlackProcessed {
|
|
51
|
-
const eventType = getString(event, "type")
|
|
52
|
-
|
|
53
|
-
if (!eventType || !ALLOWED_EVENTS.has(eventType)) return { skip: true }
|
|
54
|
-
|
|
55
|
-
const subtype = getString(event, "subtype")
|
|
56
|
-
|
|
57
|
-
if (!ALLOWED_SUBTYPES.has(subtype)) return { skip: true }
|
|
58
|
-
|
|
59
|
-
const channelId = getString(event, "channel") ?? ""
|
|
60
|
-
const eventTs = getString(event, "event_ts") ?? getString(event, "ts") ?? ""
|
|
61
|
-
const dedupKey = `${channelId}:${eventTs}`
|
|
62
|
-
const now = this.now()
|
|
63
|
-
|
|
64
|
-
if (this.dedup.has(dedupKey)) return { skip: true }
|
|
65
|
-
|
|
66
|
-
this.dedup.set(dedupKey, now)
|
|
67
|
-
|
|
68
|
-
for (const key of this.dedup.keys()) {
|
|
69
|
-
if ((this.dedup.get(key) ?? 0) < now - DEDUP_WINDOW) this.dedup.delete(key)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const userId = getString(event, "user")
|
|
73
|
-
const botId = getString(event, "bot_id")
|
|
74
|
-
|
|
75
|
-
if (userId === this.ownBotUserId) return { skip: true }
|
|
76
|
-
if (botId === this.ownBotId) return { skip: true }
|
|
77
|
-
|
|
78
|
-
const text = getString(event, "text") ?? ""
|
|
79
|
-
const mentioned = text.includes(`<@${this.ownBotUserId}>`)
|
|
80
|
-
const threadTs = getString(event, "thread_ts") ?? getString(event, "ts") ?? ""
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
skip: false,
|
|
84
|
-
content: JSON.stringify(event),
|
|
85
|
-
meta: {
|
|
86
|
-
event_type: "slack",
|
|
87
|
-
channel_id: channelId,
|
|
88
|
-
user_id: userId ?? "",
|
|
89
|
-
mentioned: String(mentioned),
|
|
90
|
-
thread_ts: threadTs,
|
|
91
|
-
},
|
|
92
|
-
shouldReact: mentioned,
|
|
93
|
-
channel: channelId,
|
|
94
|
-
timestamp: getString(event, "ts") ?? "",
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { App, LogLevel } from "@slack/bolt"
|
|
2
|
-
import { z } from "zod"
|
|
3
|
-
import { FunnelConnectorListener, type NotifyFn } from "@/connectors/connector-listener"
|
|
4
|
-
import { FunnelSlackEventProcessor } from "@/connectors/slack-event-processor"
|
|
5
|
-
import { FunnelLogger } from "@/engine/logger/logger"
|
|
6
|
-
import { NodeFunnelLogger } from "@/engine/logger/node-logger"
|
|
7
|
-
import type { SlackConnectorConfig } from "@/connectors/slack-connector-schema"
|
|
8
|
-
|
|
9
|
-
const middlewareArgsSchema = z.object({
|
|
10
|
-
event: z.record(z.string(), z.unknown()).optional(),
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
type Deps = {
|
|
14
|
-
config: SlackConnectorConfig
|
|
15
|
-
logger?: FunnelLogger
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const defaultLogger = new NodeFunnelLogger()
|
|
19
|
-
|
|
20
|
-
export class FunnelSlackListener extends FunnelConnectorListener {
|
|
21
|
-
private readonly config: SlackConnectorConfig
|
|
22
|
-
private readonly logger: FunnelLogger
|
|
23
|
-
private app: App | null = null
|
|
24
|
-
|
|
25
|
-
constructor(deps: Deps) {
|
|
26
|
-
super()
|
|
27
|
-
this.config = deps.config
|
|
28
|
-
this.logger = deps.logger ?? defaultLogger
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async start(notify: NotifyFn): Promise<void> {
|
|
32
|
-
const app = new App({
|
|
33
|
-
token: this.config.botToken,
|
|
34
|
-
appToken: this.config.appToken,
|
|
35
|
-
socketMode: true,
|
|
36
|
-
logLevel: LogLevel.ERROR,
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
const authResult = await app.client.auth.test({ token: this.config.botToken })
|
|
40
|
-
const processor = new FunnelSlackEventProcessor({
|
|
41
|
-
ownBotUserId: authResult.user_id ?? "",
|
|
42
|
-
ownBotId: authResult.bot_id ?? "",
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
app.use(async (args) => {
|
|
46
|
-
const parsed = middlewareArgsSchema.safeParse(args)
|
|
47
|
-
|
|
48
|
-
if (!parsed.success || !parsed.data.event) return
|
|
49
|
-
|
|
50
|
-
const result = processor.process(parsed.data.event)
|
|
51
|
-
|
|
52
|
-
if (result.skip) return
|
|
53
|
-
|
|
54
|
-
if (result.shouldReact) {
|
|
55
|
-
try {
|
|
56
|
-
await app.client.reactions.add({
|
|
57
|
-
token: this.config.botToken,
|
|
58
|
-
channel: result.channel,
|
|
59
|
-
timestamp: result.timestamp,
|
|
60
|
-
name: "eyes",
|
|
61
|
-
})
|
|
62
|
-
} catch {
|
|
63
|
-
// ignore
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
await notify(result.content, result.meta)
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
app.error(async (error) => {
|
|
71
|
-
this.logger.error("Slack error", {
|
|
72
|
-
error: error instanceof Error ? error.message : String(error),
|
|
73
|
-
})
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
await app.start()
|
|
77
|
-
this.app = app
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async stop(): Promise<void> {
|
|
81
|
-
if (!this.app) return
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
await this.app.stop()
|
|
85
|
-
} catch (error) {
|
|
86
|
-
this.logger.error("Slack stop error", {
|
|
87
|
-
error: error instanceof Error ? error.message : String(error),
|
|
88
|
-
})
|
|
89
|
-
} finally {
|
|
90
|
-
this.app = null
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
override isAlive(): boolean {
|
|
95
|
-
return this.app !== null
|
|
96
|
-
}
|
|
97
|
-
}
|
package/lib/connectors/slack.ts
DELETED