@fkws/klonk 0.0.4

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/README.md ADDED
@@ -0,0 +1,276 @@
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset=".github/assets/logo_dark_mode.png">
3
+ <source media="(prefers-color-scheme: light)" srcset=".github/assets/logo_bright_mode.png">
4
+ <img alt="Klonk Logo" src=".github/assets/logo_bright_mode.png">
5
+ </picture>
6
+
7
+ # Klonk
8
+
9
+ Klonk is a **code-first, self-hosted automation engine** for TypeScript developers who demand true end-to-end type safety. It enables you to build complex, event-driven workflows with a fluent, declarative API.
10
+
11
+ Stop wrestling with `any` types and stringly-typed data blobs between your automation steps. With Klonk, your entire workflow is a single, statically-checked TypeScript application, from the initial trigger to the final task.
12
+
13
+ ## Core Principles
14
+
15
+ Klonk is designed to provide a developer experience focused on type safety and a code-first approach.
16
+
17
+ ### End-to-End Type Safety
18
+
19
+ Klonk's fluent API leverages TypeScript's type system to provide compile-time safety across your entire workflow. As you add tasks to a `Playlist`, the `outputs` object available to subsequent tasks is **automatically and cumulatively typed**.
20
+
21
+ ```typescript
22
+ .setPlaylist(p => p
23
+ .addTask(new A_Task("task-a", client), /* ... */)
24
+ // In the next step, `outputs["task-a"]` is fully typed! No `any`, no manual casting.
25
+ .addTask(new B_Task("task-b", client), (source, outputs) => ({
26
+ inputForB: outputs['task-a'].someProperty // Easily leverage auto-completion in your IDE
27
+ }))
28
+ );
29
+ ```
30
+
31
+ This means you get **flawless autocompletion** and can **prevent runtime errors** before your code ever runs, providing a level of safety and productivity not found in many GUI-based tools or other code-based orchestrators.
32
+
33
+ ### A Code-First Approach
34
+
35
+ Klonk is built for developers who prefer to work with code. You use the full power and expressiveness of TypeScript to:
36
+ - Implement complex logic and data transformations with ease.
37
+ - Version control your workflows with Git.
38
+ - Write unit and integration tests.
39
+ - Integrate with your existing CI/CD pipelines.
40
+
41
+ ### Lightweight & Simple to Self-Host
42
+
43
+ Klonk is incredibly lightweight, running as a simple Node.js or Bun process with no complex dependencies or external infrastructure. This makes it a simpler alternative to heavy-duty orchestrators like Airflow or Temporal. You can get a workflow up and running in minutes on your own server with no subscription fees.
44
+
45
+ ### Powerful Built-Ins
46
+ `@fkws-npm/klonk/tasks` and `@fkws-npm/klonk/triggers` implement powerful built-ins. Klonk provides a rich set of triggers and tasks that cover a wide range of common automation scenarios, from file system events to webhook listeners. These components are designed to be robust and ready for production use. When combined with the official integrations for popular services like Dropbox, Notion, and OpenRouter, you have all the tools you need to build versatile and powerful workflows right out of the box, without having to write custom components for every step.
47
+
48
+ ## Features
49
+
50
+ - **Statically-Checked Workflows**: Build complex automations with a fluent API where data passed between steps is fully type-safe.
51
+ - **Modular by Design**: Easily extend the engine with your own custom triggers and tasks.
52
+ - **Modern Integration Support**: Built-in, async-ready integrations for services like Notion, Dropbox, and OpenRouter.
53
+ - **AI-Powered**: Leverage powerful AI models for intelligent document and data processing right out of the box.
54
+ - **Code-First & Declarative**: Define workflows in pure TypeScript for maximum flexibility and clarity.
55
+ - **Self-Hosted**: Run on your own servers with no recurring fees.
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ bun add @fkws-npm/klonk
61
+ ```
62
+
63
+ Or with npm:
64
+
65
+ ```bash
66
+ npm install @fkws-npm/klonk
67
+ ```
68
+
69
+ ## Core Concepts
70
+
71
+ Klonk is built around a few key concepts that work together to create powerful automations:
72
+
73
+ - **Workflow**: The main orchestrator. You build a workflow by adding triggers and then defining a single playlist of tasks to execute.
74
+ - **Triggers**: Event sources that start a workflow's playlist. A trigger could be a file being added to a Dropbox folder, a webhook being called, or a scheduled timer. Each time a trigger fires, it produces an event that is passed to the playlist.
75
+ - **Playlist**: An ordered sequence of tasks. The playlist is run once for every event produced by a trigger. It intelligently carries the typed outputs of each task to the ones that follow.
76
+ - **Tasks**: The individual, atomic actions that make up a playlist, such as calling an API, downloading a file, or processing data. The output of one task is available by its unique ID to all subsequent tasks.
77
+
78
+ ## Getting Started: A Simple Workflow
79
+
80
+ Here's a basic example that demonstrates the core concepts. This workflow triggers when a file is added to a Dropbox folder, downloads it, and then logs its contents.
81
+
82
+ ```typescript
83
+ import { Workflow } from '@fkws-npm/klonk';
84
+ import { TRDropboxFileAdded } from '@fkws-npm/klonk/triggers';
85
+ import { TADropboxDownloadFile, TALogToConsole } from '@fkws-npm/klonk/tasks';
86
+ import { IDropbox } from '@fkws-npm/klonk/integrations';
87
+
88
+ // 1. Initialize your service clients
89
+ const dropboxClient = new IDropbox({
90
+ appKey: process.env.DROPBOX_APP_KEY!,
91
+ appSecret: process.env.DROPBOX_APP_SECRET!,
92
+ refreshToken: process.env.DROPBOX_REFRESH_TOKEN!,
93
+ });
94
+
95
+ // 2. Define the workflow using the fluent API
96
+ const simpleWorkflow = Workflow.create()
97
+ // Add a trigger. The type of `source.data` in the playlist will be inferred from this.
98
+ .addTrigger(
99
+ new TRDropboxFileAdded("new-file-in-dropbox", {
100
+ client: dropboxClient,
101
+ folderPath: "/MyKlonkFiles"
102
+ })
103
+ )
104
+ // Define the sequence of tasks to run when the trigger fires.
105
+ .setPlaylist(p =>
106
+ p.addTask(
107
+ // Each task has a unique identifier.
108
+ new TADropboxDownloadFile("download-the-file", dropboxClient),
109
+ // The builder function defines the task's input.
110
+ // `source` is the event from the trigger. `outputs` contains results from previous tasks.
111
+ (source, outputs) => ({
112
+ file_metadata: source.data // Fully typed, autocomplete-ready and will yell if wrong.
113
+ })
114
+ ) // -> Produces { file: Buffer }
115
+ .addTask(
116
+ // The second task logs the content of the downloaded file.
117
+ new TALogToConsole("log-the-content"),
118
+ (source, outputs) => ({
119
+ // The `file` property from the previous task's output is accessed in a type-safe way so you can safely call `.toString()`.
120
+ message: outputs["download-the-file"].file.toString(),
121
+ })
122
+ )
123
+ );
124
+
125
+ // 3. Start the workflow
126
+ simpleWorkflow.start();
127
+ console.log("Simple workflow started! Waiting for new files in Dropbox...");
128
+ ```
129
+
130
+ ## Advanced Example: AI-Powered Invoice Processing
131
+
132
+ This example shows a real-world workflow that:
133
+ 1. Triggers when a new invoice PDF is added to a Dropbox folder.
134
+ 2. Fetches the PDF content.
135
+ 3. Uses an AI model to extract structured data from the invoice.
136
+ 4. Creates a new record in a Notion database with the extracted data.
137
+
138
+ ```typescript
139
+ import { Workflow } from '@fkws-npm/klonk';
140
+ import { z } from 'zod';
141
+ import { TRDropboxFileAdded } from '@fkws-npm/klonk/triggers';
142
+ import {
143
+ TADropboxDownloadFile,
144
+ TAParsePdfAi,
145
+ TACreateNotionDatabaseItem
146
+ } from '@fkws-npm/klonk/tasks';
147
+ import { IDropbox, IOpenRouter, INotion } from '@fkws-npm/klonk/integrations';
148
+
149
+ // --- Client & Schema Setup ---
150
+ const dropbox = new IDropbox({ /* ... */ });
151
+ const openRouter = new IOpenRouter({ apiKeyEnvVar: "OPENROUTER_API_KEY" });
152
+ const notion = new INotion({ apiKeyEnvVar: "NOTION_API_KEY" });
153
+
154
+ const InvoiceSchema = z.object({
155
+ invoice_number: z.string(),
156
+ total_amount: z.number(),
157
+ is_paid: z.boolean(),
158
+ });
159
+
160
+ // --- Workflow Definition ---
161
+ const invoiceWorkflow = Workflow.create()
162
+ .addTrigger(new TRDropboxFileAdded("new-invoice", { client: dropbox, folderPath: "/Invoices" }))
163
+ .setPlaylist(p => p
164
+ // Step 1: Download the file from Dropbox
165
+ .addTask(new TADropboxDownloadFile("download-pdf", dropbox),
166
+ (source, outputs) => ({
167
+ file_metadata: source.data
168
+ })
169
+ )
170
+ // Step 2: Use AI to parse the downloaded PDF
171
+ .addTask(new TAParsePdfAi("parse-with-ai", openRouter),
172
+ (source, outputs) => ({
173
+ pdf: outputs['download-pdf'].file, // Type-safe access to previous output by its ID
174
+ instructions: "Extract the invoice number, total amount, and payment status from the PDF.",
175
+ zodSchema: InvoiceSchema,
176
+ })
177
+ )
178
+ // Step 3: Create a new item in a Notion database
179
+ .addTask(new TACreateNotionDatabaseItem("create-notion-record", notion),
180
+ (source, outputs) => {
181
+ const invoiceData = outputs['parse-with-ai']; // Type-safe access to the parsed data
182
+ return {
183
+ database_id: 'YOUR_INVOICES_DATABASE_ID',
184
+ properties: {
185
+ 'Invoice Number': { title: [{ text: { content: invoiceData.invoice_number } }] },
186
+ 'Total': { number: invoiceData.total_amount },
187
+ 'Status': { select: { name: invoiceData.is_paid ? "Paid" : "Unpaid" } }
188
+ }
189
+ };
190
+ }
191
+ )
192
+ );
193
+
194
+ // --- Start the workflow ---
195
+ invoiceWorkflow.start();
196
+ ```
197
+
198
+ ## Creating Custom Components
199
+
200
+ If your use case can't be served with Klonk's built-ins, you can easily extend Klonk by implementing the `Task` and `Trigger` abstract classes.
201
+
202
+ ### Custom Task Example
203
+
204
+ A task must define its Input and Output types and implement `validateInput` and `run` methods.
205
+
206
+ ```typescript
207
+ import { Task } from '@fkws-npm/klonk';
208
+ import type { IEmailService } from './my-email-service'; // Your custom service
209
+
210
+ // 1. Define the Input and Output types
211
+ type EmailSenderInput = { to: string; subject: string; body: string; };
212
+ type EmailSenderOutput = { messageId: string; };
213
+
214
+ // 2. Create the class, extending the generic Task
215
+ export class TAEmailSender<IdentType extends string> extends Task<
216
+ EmailSenderInput,
217
+ EmailSenderOutput,
218
+ IdentType
219
+ > {
220
+ // 3. The constructor receives its ident and any clients it needs
221
+ constructor(ident: IdentType, private emailClient: IEmailService) {
222
+ super(ident);
223
+ }
224
+
225
+ // 4. For runtime validation
226
+ validateInput(input: EmailSenderInput): boolean {
227
+ return !!input.to && !!input.subject && !!input.body;
228
+ }
229
+
230
+ async run(input: EmailSenderInput): Promise<EmailSenderOutput> {
231
+ const result = await this.emailClient.send(input);
232
+ return { messageId: result.id };
233
+ }
234
+ }
235
+ ```
236
+
237
+ ### Custom Trigger Example
238
+
239
+ A trigger must define the type of event data it produces and can implement a `start` method for background polling.
240
+
241
+ ```typescript
242
+ import { Trigger, TriggerEvent } from '@fkws-npm/klonk';
243
+
244
+ // 1. Define the shape of the data this trigger produces
245
+ type NewUserEventData = { userId: string; signupDate: Date; };
246
+
247
+ // 2. Create the class, extending the generic Trigger
248
+ export class TRNewUserSignedUp<IdentType extends string> extends Trigger<
249
+ IdentType,
250
+ TriggerEvent<IdentType, NewUserEventData>
251
+ > {
252
+ constructor(ident: IdentType) {
253
+ super(ident);
254
+ }
255
+
256
+ // 3. The `start` method is called by the workflow to begin polling
257
+ async start(): Promise<void> {
258
+ // Example: check for new users every 10 seconds
259
+ setInterval(async () => {
260
+ const newUser = await this.checkForNewUserInDb();
261
+ if (newUser) {
262
+ // 4. Add new events to the `this.queue` property
263
+ this.queue.push({
264
+ triggerIdent: this.ident,
265
+ data: newUser
266
+ });
267
+ }
268
+ }, 10000);
269
+ }
270
+
271
+ private async checkForNewUserInDb(): Promise<NewUserEventData | null> {
272
+ // Your database logic here...
273
+ return null;
274
+ }
275
+ }
276
+ ```
package/dist/cli.cjs ADDED
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ var import_node_module = require("node:module");
3
+ var __create = Object.create;
4
+ var __getProtoOf = Object.getPrototypeOf;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __toESM = (mod, isNodeMode, target) => {
9
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
10
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
+ for (let key of __getOwnPropNames(mod))
12
+ if (!__hasOwnProp.call(to, key))
13
+ __defProp(to, key, {
14
+ get: () => mod[key],
15
+ enumerable: true
16
+ });
17
+ return to;
18
+ };
19
+
20
+ // src/cli.ts
21
+ var import_commander = require("commander");
22
+ var import_express = __toESM(require("express"));
23
+ var import_json_to_zod = require("json-to-zod");
24
+ var import_cli_highlight = require("cli-highlight");
25
+ var readline = __toESM(require("node:readline/promises"));
26
+ var import_node_process = require("node:process");
27
+ var program = new import_commander.Command;
28
+ program.name("klonk").description("CLI for Klonk workflow automation engine").version("0.1.18");
29
+ program.command("test").description("Run a test to check if the CLI is working.").action(() => {
30
+ console.log("TEST SUCCESSFUL");
31
+ });
32
+ var setupCommand = program.command("setup").description("Setup integrations");
33
+ var setupIntegrationCommand = setupCommand.command("integration").description("Setup a specific integration");
34
+ setupIntegrationCommand.command("dropbox").description("Get a refresh token for Dropbox").action(async () => {
35
+ const rl = readline.createInterface({ input: import_node_process.stdin, output: import_node_process.stdout });
36
+ const appKey = await rl.question("Enter your Dropbox App Key: ");
37
+ const appSecret = await rl.question("Enter your Dropbox App Secret: ");
38
+ console.log(`
39
+ Please go to this URL to authorize the app:
40
+ https://www.dropbox.com/oauth2/authorize?client_id=${appKey}&response_type=code&token_access_type=offline
41
+ `);
42
+ const authCode = await rl.question("Paste the authorization code here: ");
43
+ rl.close();
44
+ try {
45
+ const response = await fetch("https://api.dropbox.com/oauth2/token", {
46
+ method: "POST",
47
+ headers: {
48
+ "Content-Type": "application/x-www-form-urlencoded",
49
+ Authorization: `Basic ${Buffer.from(`${appKey}:${appSecret}`).toString("base64")}`
50
+ },
51
+ body: new URLSearchParams({
52
+ code: authCode,
53
+ grant_type: "authorization_code"
54
+ })
55
+ });
56
+ const data = await response.json();
57
+ if (!response.ok) {
58
+ console.error(`
59
+ Error getting refresh token:`);
60
+ console.error(import_cli_highlight.highlight(JSON.stringify(data, null, 2), { language: "json" }));
61
+ process.exit(1);
62
+ }
63
+ console.log(`
64
+ Success! Here are your tokens:`);
65
+ console.log(import_cli_highlight.highlight(JSON.stringify(data, null, 2), { language: "json" }));
66
+ console.log("\nStore the `refresh_token` securely. You will use it, along with your app key and secret, to configure the Dropbox integration.");
67
+ } catch (error) {
68
+ console.error(`
69
+ An error occurred while fetching the refresh token:`, error);
70
+ }
71
+ });
72
+ var introspectCommand = program.command("introspect").description("Introspection utilities");
73
+ introspectCommand.command("webhook-payload").description("Introspect a webhook payload").option("--gt, --generate-type", "Generate a TypeScript type definition for the payload").option("-p, --port <port>", "Port to listen on", "2021").option("--base-url <url>", "Base URL for the webhook endpoint").action(async (options) => {
74
+ const app = import_express.default();
75
+ app.use(import_express.default.json());
76
+ const port = parseInt(options.port, 10);
77
+ const displayUrl = options.baseUrl ? options.baseUrl : `http://localhost:${port}`;
78
+ let server = null;
79
+ const closeServer = () => {
80
+ if (server) {
81
+ server.close(() => {
82
+ console.log(`
83
+ Server closed.`);
84
+ process.exit(0);
85
+ });
86
+ }
87
+ };
88
+ app.get("/", (req, res) => {
89
+ const challenge = req.query.challenge;
90
+ if (challenge && typeof challenge === "string") {
91
+ console.log(`
92
+ Received verification challenge. Responding with: ${challenge}`);
93
+ res.setHeader("Content-Type", "text/plain");
94
+ res.status(200).send(challenge);
95
+ console.log("Challenge response sent. Waiting for POST payload...");
96
+ return;
97
+ }
98
+ console.warn(`
99
+ Received GET request to / without a 'challenge' parameter.`);
100
+ res.status(400).send('Bad Request: Missing "challenge" query parameter.');
101
+ });
102
+ app.post("/", (req, res) => {
103
+ const payload = req.body;
104
+ console.log(`
105
+ Webhook Payload:`);
106
+ console.log(import_cli_highlight.highlight(JSON.stringify(payload, null, 2), { language: "json", ignoreIllegals: true }));
107
+ if (options.generateType) {
108
+ try {
109
+ const zodSchemaString = import_json_to_zod.jsonToZod(payload, "PayloadSchema", true);
110
+ const typeAliasString = `
111
+ type Payload = z.infer<typeof PayloadSchema>;`;
112
+ const combinedOutput = zodSchemaString + typeAliasString;
113
+ console.log(`
114
+ Generated Zod Schema & TypeScript Type:`);
115
+ console.log(import_cli_highlight.highlight(combinedOutput, { language: "typescript", ignoreIllegals: true }));
116
+ } catch (error) {
117
+ console.error(`
118
+ Error generating Zod schema:`, error);
119
+ }
120
+ }
121
+ res.status(200).send("Payload received");
122
+ closeServer();
123
+ });
124
+ server = app.listen(port, () => {
125
+ console.log(`Listening for webhook payload on ${displayUrl}`);
126
+ });
127
+ server.on("error", (error) => {
128
+ console.error("Server error:", error);
129
+ process.exit(1);
130
+ });
131
+ });
132
+ program.parse(process.argv);
package/dist/cli.d.ts ADDED
File without changes
package/dist/cli.js ADDED
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+ import express from "express";
6
+ import { jsonToZod } from "json-to-zod";
7
+ import { highlight } from "cli-highlight";
8
+ import * as readline from "node:readline/promises";
9
+ import { stdin as input, stdout as output } from "node:process";
10
+ var program = new Command;
11
+ program.name("klonk").description("CLI for Klonk workflow automation engine").version("0.1.18");
12
+ program.command("test").description("Run a test to check if the CLI is working.").action(() => {
13
+ console.log("TEST SUCCESSFUL");
14
+ });
15
+ var setupCommand = program.command("setup").description("Setup integrations");
16
+ var setupIntegrationCommand = setupCommand.command("integration").description("Setup a specific integration");
17
+ setupIntegrationCommand.command("dropbox").description("Get a refresh token for Dropbox").action(async () => {
18
+ const rl = readline.createInterface({ input, output });
19
+ const appKey = await rl.question("Enter your Dropbox App Key: ");
20
+ const appSecret = await rl.question("Enter your Dropbox App Secret: ");
21
+ console.log(`
22
+ Please go to this URL to authorize the app:
23
+ https://www.dropbox.com/oauth2/authorize?client_id=${appKey}&response_type=code&token_access_type=offline
24
+ `);
25
+ const authCode = await rl.question("Paste the authorization code here: ");
26
+ rl.close();
27
+ try {
28
+ const response = await fetch("https://api.dropbox.com/oauth2/token", {
29
+ method: "POST",
30
+ headers: {
31
+ "Content-Type": "application/x-www-form-urlencoded",
32
+ Authorization: `Basic ${Buffer.from(`${appKey}:${appSecret}`).toString("base64")}`
33
+ },
34
+ body: new URLSearchParams({
35
+ code: authCode,
36
+ grant_type: "authorization_code"
37
+ })
38
+ });
39
+ const data = await response.json();
40
+ if (!response.ok) {
41
+ console.error(`
42
+ Error getting refresh token:`);
43
+ console.error(highlight(JSON.stringify(data, null, 2), { language: "json" }));
44
+ process.exit(1);
45
+ }
46
+ console.log(`
47
+ Success! Here are your tokens:`);
48
+ console.log(highlight(JSON.stringify(data, null, 2), { language: "json" }));
49
+ console.log("\nStore the `refresh_token` securely. You will use it, along with your app key and secret, to configure the Dropbox integration.");
50
+ } catch (error) {
51
+ console.error(`
52
+ An error occurred while fetching the refresh token:`, error);
53
+ }
54
+ });
55
+ var introspectCommand = program.command("introspect").description("Introspection utilities");
56
+ introspectCommand.command("webhook-payload").description("Introspect a webhook payload").option("--gt, --generate-type", "Generate a TypeScript type definition for the payload").option("-p, --port <port>", "Port to listen on", "2021").option("--base-url <url>", "Base URL for the webhook endpoint").action(async (options) => {
57
+ const app = express();
58
+ app.use(express.json());
59
+ const port = parseInt(options.port, 10);
60
+ const displayUrl = options.baseUrl ? options.baseUrl : `http://localhost:${port}`;
61
+ let server = null;
62
+ const closeServer = () => {
63
+ if (server) {
64
+ server.close(() => {
65
+ console.log(`
66
+ Server closed.`);
67
+ process.exit(0);
68
+ });
69
+ }
70
+ };
71
+ app.get("/", (req, res) => {
72
+ const challenge = req.query.challenge;
73
+ if (challenge && typeof challenge === "string") {
74
+ console.log(`
75
+ Received verification challenge. Responding with: ${challenge}`);
76
+ res.setHeader("Content-Type", "text/plain");
77
+ res.status(200).send(challenge);
78
+ console.log("Challenge response sent. Waiting for POST payload...");
79
+ return;
80
+ }
81
+ console.warn(`
82
+ Received GET request to / without a 'challenge' parameter.`);
83
+ res.status(400).send('Bad Request: Missing "challenge" query parameter.');
84
+ });
85
+ app.post("/", (req, res) => {
86
+ const payload = req.body;
87
+ console.log(`
88
+ Webhook Payload:`);
89
+ console.log(highlight(JSON.stringify(payload, null, 2), { language: "json", ignoreIllegals: true }));
90
+ if (options.generateType) {
91
+ try {
92
+ const zodSchemaString = jsonToZod(payload, "PayloadSchema", true);
93
+ const typeAliasString = `
94
+ type Payload = z.infer<typeof PayloadSchema>;`;
95
+ const combinedOutput = zodSchemaString + typeAliasString;
96
+ console.log(`
97
+ Generated Zod Schema & TypeScript Type:`);
98
+ console.log(highlight(combinedOutput, { language: "typescript", ignoreIllegals: true }));
99
+ } catch (error) {
100
+ console.error(`
101
+ Error generating Zod schema:`, error);
102
+ }
103
+ }
104
+ res.status(200).send("Payload received");
105
+ closeServer();
106
+ });
107
+ server = app.listen(port, () => {
108
+ console.log(`Listening for webhook payload on ${displayUrl}`);
109
+ });
110
+ server.on("error", (error) => {
111
+ console.error("Server error:", error);
112
+ process.exit(1);
113
+ });
114
+ });
115
+ program.parse(process.argv);