@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/dist/index.js ADDED
@@ -0,0 +1,429 @@
1
+ // src/prototypes/Playlist.ts
2
+ class Playlist {
3
+ machines;
4
+ finalizer;
5
+ constructor(machines = [], finalizer) {
6
+ this.machines = machines;
7
+ this.finalizer = finalizer;
8
+ }
9
+ addTask(task, builder) {
10
+ const machine = { task, builder };
11
+ const newMachines = [...this.machines, machine];
12
+ return new Playlist(newMachines, this.finalizer);
13
+ }
14
+ finally(finalizer) {
15
+ this.finalizer = finalizer;
16
+ return this;
17
+ }
18
+ async run(source) {
19
+ const outputs = {};
20
+ for (const machine of this.machines) {
21
+ const input = machine.builder(source, outputs);
22
+ const isValid = await machine.task.validateInput(input);
23
+ if (!isValid) {
24
+ throw new Error(`Input validation failed for task '${machine.task.ident}'`);
25
+ }
26
+ const result = await machine.task.run(input);
27
+ outputs[machine.task.ident] = result;
28
+ }
29
+ if (this.finalizer) {
30
+ await this.finalizer(source, outputs);
31
+ }
32
+ return outputs;
33
+ }
34
+ }
35
+
36
+ // src/prototypes/Workflow.ts
37
+ class Workflow {
38
+ playlist;
39
+ triggers;
40
+ constructor(triggers, playlist) {
41
+ this.triggers = triggers;
42
+ this.playlist = playlist;
43
+ }
44
+ addTrigger(trigger) {
45
+ const newTriggers = [...this.triggers, trigger];
46
+ const newPlaylist = this.playlist;
47
+ return new Workflow(newTriggers, newPlaylist);
48
+ }
49
+ setPlaylist(builder) {
50
+ const initialPlaylist = new Playlist;
51
+ const finalPlaylist = builder(initialPlaylist);
52
+ return new Workflow(this.triggers, finalPlaylist);
53
+ }
54
+ async start({ interval = 5000, callback } = {}) {
55
+ if (!this.playlist) {
56
+ throw new Error("Cannot start a workflow without a playlist.");
57
+ }
58
+ for (const trigger of this.triggers) {
59
+ await trigger.start();
60
+ }
61
+ const runTick = async () => {
62
+ for (const trigger of this.triggers) {
63
+ const event = trigger.poll();
64
+ if (event) {
65
+ try {
66
+ const outputs = await this.playlist.run(event);
67
+ if (callback) {
68
+ callback(event, outputs);
69
+ }
70
+ } catch (error) {
71
+ console.error(`[Workflow] Error during playlist execution for trigger '${event.triggerIdent}':`, error);
72
+ }
73
+ }
74
+ }
75
+ setTimeout(runTick, interval);
76
+ };
77
+ runTick();
78
+ }
79
+ static create() {
80
+ return new Workflow([], null);
81
+ }
82
+ }
83
+ // src/prototypes/Task.ts
84
+ class Task {
85
+ ident;
86
+ constructor(ident) {
87
+ this.ident = ident;
88
+ }
89
+ }
90
+ // src/prototypes/Trigger.ts
91
+ class EventQueue {
92
+ size;
93
+ queue = [];
94
+ constructor(size) {
95
+ this.size = size;
96
+ }
97
+ push(item) {
98
+ if (this.queue.length + 1 > this.size) {
99
+ this.queue.shift();
100
+ }
101
+ this.queue.push(item);
102
+ }
103
+ shift() {
104
+ return this.queue.shift();
105
+ }
106
+ get length() {
107
+ return this.queue.length;
108
+ }
109
+ }
110
+
111
+ class Trigger {
112
+ ident;
113
+ queue;
114
+ constructor(ident, queueSize = 50) {
115
+ this.ident = ident;
116
+ this.queue = new EventQueue(queueSize);
117
+ }
118
+ pushEvent(data) {
119
+ this.queue.push({
120
+ triggerIdent: this.ident,
121
+ data
122
+ });
123
+ }
124
+ poll() {
125
+ const event = this.queue.shift();
126
+ return event ?? null;
127
+ }
128
+ }
129
+ // src/prototypes/Machine.ts
130
+ import pino from "pino";
131
+ import { randomUUID } from "crypto";
132
+ var glogger = null;
133
+
134
+ class StateNode {
135
+ transitions;
136
+ playlist;
137
+ timeToNextTick;
138
+ ident;
139
+ tempTransitions;
140
+ retry;
141
+ maxRetries;
142
+ constructor(transitions, playlist) {
143
+ this.transitions = transitions;
144
+ this.playlist = playlist;
145
+ this.timeToNextTick = 1000;
146
+ this.ident = "";
147
+ this.retry = 1000;
148
+ this.maxRetries = false;
149
+ }
150
+ static create() {
151
+ return new StateNode([], new Playlist);
152
+ }
153
+ addTransition({ to, condition, weight }) {
154
+ if (!this.tempTransitions) {
155
+ this.tempTransitions = [];
156
+ }
157
+ this.tempTransitions?.push({ to, condition, weight });
158
+ return this;
159
+ }
160
+ setPlaylist(arg) {
161
+ if (typeof arg === "function") {
162
+ const initial = new Playlist;
163
+ const finalPlaylist = arg(initial);
164
+ this.playlist = finalPlaylist;
165
+ return this;
166
+ }
167
+ this.playlist = arg;
168
+ return this;
169
+ }
170
+ preventRetry() {
171
+ this.retry = false;
172
+ return this;
173
+ }
174
+ retryDelayMs(delayMs) {
175
+ this.retry = delayMs;
176
+ return this;
177
+ }
178
+ retryLimit(maxRetries) {
179
+ this.maxRetries = maxRetries;
180
+ return this;
181
+ }
182
+ setIdent(ident) {
183
+ this.ident = ident;
184
+ return this;
185
+ }
186
+ getByIdent(ident, visited = []) {
187
+ if (this.ident === ident) {
188
+ return this;
189
+ }
190
+ if (visited.includes(this.ident)) {
191
+ return null;
192
+ }
193
+ visited.push(this.ident);
194
+ if (this.transitions) {
195
+ for (const transition of this.transitions) {
196
+ const found = transition.to?.getByIdent(ident, visited);
197
+ if (found)
198
+ return found;
199
+ }
200
+ }
201
+ return null;
202
+ }
203
+ async next(data) {
204
+ const logger = glogger?.child({ path: "StateNode.next", state: this.ident });
205
+ logger?.info({ phase: "start" }, "Evaluating next state");
206
+ const sorted = [...this.transitions || []].map((t, i) => ({ t, i })).sort((a, b) => (b.t.weight ?? 0) - (a.t.weight ?? 0) || a.i - b.i);
207
+ for (const { t } of sorted) {
208
+ try {
209
+ if (await t.condition(data)) {
210
+ logger?.info({ phase: "end", nextState: t.to?.ident }, "Condition met, transitioning");
211
+ return t.to;
212
+ }
213
+ } catch (err) {
214
+ logger?.error({ phase: "error", error: err }, "Transition condition failed");
215
+ }
216
+ }
217
+ logger?.info({ phase: "end", nextState: null }, "No condition met, no transition");
218
+ return null;
219
+ }
220
+ }
221
+
222
+ class Machine {
223
+ initialState = null;
224
+ statesToCreate = [];
225
+ currentState = null;
226
+ finalized = false;
227
+ logger;
228
+ ident;
229
+ getAllStates() {
230
+ const logger = glogger?.child({ path: "machine.getAllStates" });
231
+ logger?.info({ phase: "start" }, "Gathering all states...");
232
+ if (!this.initialState)
233
+ return [];
234
+ const visited = new Set;
235
+ const result = [];
236
+ const stack = [this.initialState];
237
+ while (stack.length > 0) {
238
+ const node = stack.pop();
239
+ if (!node.ident || visited.has(node.ident))
240
+ continue;
241
+ visited.add(node.ident);
242
+ result.push(node);
243
+ for (const tr of node.transitions || []) {
244
+ if (tr.to && tr.to.ident && !visited.has(tr.to.ident)) {
245
+ stack.push(tr.to);
246
+ }
247
+ }
248
+ }
249
+ logger?.info({ phase: "end" }, "States gathered");
250
+ return result;
251
+ }
252
+ sleep(ms) {
253
+ return new Promise((resolve) => setTimeout(resolve, ms));
254
+ }
255
+ static create() {
256
+ return new Machine;
257
+ }
258
+ finalize({
259
+ verbose,
260
+ ident
261
+ } = {}) {
262
+ const logger = glogger?.child({ path: "machine.finalize", instance: this.ident });
263
+ if (!this.initialState || this.statesToCreate.length === 0) {
264
+ logger?.error({ phase: "error" }, "Finalization failed: no initial state or states to create");
265
+ throw new Error("Cannot finalize a machine without an initial state or states to create.");
266
+ }
267
+ if (ident) {
268
+ this.ident = ident;
269
+ } else {
270
+ this.ident = randomUUID();
271
+ }
272
+ if (verbose)
273
+ glogger = pino();
274
+ logger?.info("Logging enabled.");
275
+ logger?.info({ phase: "start" }, `Finalizing machine ${this.ident}...`);
276
+ const registry = new Map;
277
+ logger?.info({ phase: "progress" }, `Building state registry...`);
278
+ for (const s of this.statesToCreate) {
279
+ if (!s.ident) {
280
+ logger?.error({ phase: "error" }, "Finalization failed: state missing ident");
281
+ throw new Error("State missing ident.");
282
+ }
283
+ if (registry.has(s.ident)) {
284
+ logger?.error({ phase: "error", state: s.ident }, "Finalization failed: duplicate state ident");
285
+ throw new Error(`Duplicate state ident '${s.ident}'.`);
286
+ }
287
+ registry.set(s.ident, s);
288
+ }
289
+ logger?.info({ phase: "progress", count: registry.size }, `State registry built.`);
290
+ logger?.info({ phase: "progress" }, `Resolving transitions...`);
291
+ for (const state of this.statesToCreate) {
292
+ state.transitions = [];
293
+ for (const tr of state.tempTransitions || []) {
294
+ const toNode = registry.get(tr.to);
295
+ if (!toNode) {
296
+ logger?.error({ phase: "error", from: state.ident, to: tr.to }, "Finalization failed: target state not found");
297
+ throw new Error(`State '${tr.to}' not found.`);
298
+ }
299
+ state.transitions.push({ to: toNode, condition: tr.condition, weight: tr.weight });
300
+ }
301
+ state.tempTransitions = undefined;
302
+ }
303
+ logger?.info({ phase: "progress" }, `Transitions resolved.`);
304
+ this.statesToCreate = [];
305
+ this.finalized = true;
306
+ logger?.info({ phase: "end" }, `Machine ${this.ident} finalized.`);
307
+ return this;
308
+ }
309
+ addState(state, options = {}) {
310
+ const logger = glogger?.child({ path: "machine.addState", instance: this.ident });
311
+ logger?.info({ phase: "start", state: state.ident, isInitial: !!options.initial }, "Adding state");
312
+ this.statesToCreate.push(state);
313
+ if (options.initial) {
314
+ this.initialState = state;
315
+ }
316
+ logger?.info({ phase: "end", state: state.ident }, "State added");
317
+ return this;
318
+ }
319
+ async start(stateData, options) {
320
+ const logger = glogger?.child({ path: "machine.start", instance: this.ident });
321
+ logger?.info({ phase: "start" }, "Starting machine...");
322
+ if (!this.finalized) {
323
+ logger?.error({ phase: "error" }, "Machine not finalized");
324
+ throw new Error("Cannot start a machine that is not finalized.");
325
+ }
326
+ if (!this.initialState) {
327
+ logger?.error({ phase: "error" }, "No initial state");
328
+ throw new Error("Cannot start a machine without an initial state.");
329
+ }
330
+ const interval = options?.interval ?? 1000;
331
+ logger?.info({ phase: "progress", interval }, "Machine interval set");
332
+ if (!this.currentState) {
333
+ this.currentState = this.initialState;
334
+ logger?.info({ phase: "progress", state: this.currentState.ident }, "Set initial state. Running playlist.");
335
+ await this.currentState.playlist.run(stateData);
336
+ logger?.info({ phase: "progress", state: this.currentState.ident }, "Initial playlist run complete.");
337
+ }
338
+ const tick = async () => {
339
+ const tickLogger = logger?.child({ path: "machine.tick" });
340
+ tickLogger?.info({ phase: "start", state: this.currentState.ident }, "Tick.");
341
+ const next = await this.currentState.next(stateData);
342
+ if (next) {
343
+ tickLogger?.info({ phase: "progress", from: this.currentState.ident, to: next.ident }, "Transitioning state.");
344
+ this.currentState = next;
345
+ await this.currentState.playlist.run(stateData);
346
+ tickLogger?.info({ phase: "progress", state: this.currentState.ident }, "Playlist run complete.");
347
+ } else {
348
+ tickLogger?.info({ phase: "progress", state: this.currentState.ident }, "No next state.");
349
+ }
350
+ setTimeout(tick, interval);
351
+ };
352
+ tick();
353
+ logger?.info({ phase: "end" }, "Machine started, tick loop initiated.");
354
+ }
355
+ async run(stateData) {
356
+ const logger = glogger?.child({ path: "machine.run", instance: this.ident });
357
+ logger?.info({ phase: "start" }, "Running machine...");
358
+ if (!this.finalized) {
359
+ logger?.error({ phase: "error" }, "Machine not finalized");
360
+ throw new Error("Cannot run a machine that is not finalized.");
361
+ }
362
+ if (!this.initialState) {
363
+ logger?.error({ phase: "error" }, "No initial state");
364
+ throw new Error("Cannot run a machine without an initial state.");
365
+ }
366
+ const allStates = this.getAllStates();
367
+ const visitedIdents = new Set;
368
+ let current = this.initialState;
369
+ logger?.info({ phase: "progress", state: current.ident }, "Set initial state. Running playlist.");
370
+ await current.playlist.run(stateData);
371
+ visitedIdents.add(current.ident);
372
+ while (true) {
373
+ if (!current.transitions || current.transitions.length === 0) {
374
+ logger?.info({ phase: "end", state: current.ident }, "Reached leaf node, run complete.");
375
+ return stateData;
376
+ }
377
+ let next = await current.next(stateData);
378
+ if (!next) {
379
+ const retryDelay = current.retry;
380
+ if (!retryDelay) {
381
+ logger?.info({ phase: "end", state: current.ident }, "No next state and retries disabled, run complete.");
382
+ return stateData;
383
+ }
384
+ let retries = 0;
385
+ logger?.info({ phase: "progress", state: current.ident, retryDelay }, "No next state, beginning retry logic.");
386
+ while (!next) {
387
+ if (current.maxRetries && retries >= current.maxRetries) {
388
+ logger?.warn({ phase: "end", state: current.ident, retries }, "Retry limit exhausted, run complete.");
389
+ return stateData;
390
+ }
391
+ await this.sleep(retryDelay);
392
+ logger?.info({ phase: "progress", state: current.ident, attempt: retries + 1 }, "Retrying to find next state.");
393
+ next = await current.next(stateData);
394
+ if (next) {
395
+ logger?.info({ phase: "progress", state: current.ident, nextState: next.ident }, "Retry successful.");
396
+ break;
397
+ }
398
+ retries++;
399
+ }
400
+ if (!next) {
401
+ logger?.info({ phase: "end" }, "No next state after retries, run complete.");
402
+ return stateData;
403
+ }
404
+ }
405
+ if (next === this.initialState) {
406
+ logger?.info({ phase: "end" }, "Next state is initial state, run complete.");
407
+ return stateData;
408
+ }
409
+ logger?.info({ phase: "progress", from: current.ident, to: next.ident }, "Transitioning state.");
410
+ current = next;
411
+ await current.playlist.run(stateData);
412
+ if (current.ident)
413
+ visitedIdents.add(current.ident);
414
+ if (visitedIdents.size >= allStates.length) {
415
+ logger?.info({ phase: "end" }, "All reachable states visited, run complete.");
416
+ return stateData;
417
+ }
418
+ }
419
+ return stateData;
420
+ }
421
+ }
422
+ export {
423
+ Workflow,
424
+ Trigger,
425
+ Task,
426
+ StateNode,
427
+ Playlist,
428
+ Machine
429
+ };
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@fkws/klonk",
3
+ "version": "0.0.4",
4
+ "description": "A lightweight, extensible workflow automation engine for Node.js and Bun",
5
+ "bin": {
6
+ "klonk": "./dist/cli.js"
7
+ },
8
+ "module": "dist/index.js",
9
+ "main": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "type": "module",
12
+ "exports": {
13
+ ".": {
14
+ "import": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "require": {
19
+ "types": "./dist/index.d.cts",
20
+ "default": "./dist/index.cjs"
21
+ }
22
+ }
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "scripts": {
28
+ "start": "bun index.ts",
29
+ "build": "bunx rimraf dist && bunup",
30
+ "prepublishOnly": "bun run build",
31
+ "release": "bun scripts/release.js",
32
+ "release:patch": "bun scripts/release.js patch",
33
+ "release:minor": "bun scripts/release.js minor",
34
+ "release:major": "bun scripts/release.js major"
35
+ },
36
+ "keywords": [
37
+ "workflow",
38
+ "automation",
39
+ "zapier",
40
+ "notion",
41
+ "openai"
42
+ ],
43
+ "author": "FKWS (https://klonk.dev)",
44
+ "devDependencies": {
45
+ "@types/bun": "latest",
46
+ "@types/express": "^5.0.1",
47
+ "@types/node": "latest",
48
+ "bunup": "^0.6.2",
49
+ "tavily": "^1.0.2"
50
+ },
51
+ "peerDependencies": {
52
+ "typescript": "^5"
53
+ },
54
+ "dependencies": {
55
+ "cli-highlight": "^2.1.11",
56
+ "commander": "^13.1.0",
57
+ "express": "^5.1.0",
58
+ "json-to-zod": "^1.1.2",
59
+ "pino": "^10.1.0"
60
+ }
61
+ }