@fkws/klonk 0.0.22 → 0.0.24

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 CHANGED
@@ -20,15 +20,14 @@
20
20
 
21
21
  ## Introduction
22
22
 
23
- Klonk is a code-first, type-safe automation engine designed with developer experience as a top priority. It provides powerful, composable primitives to build complex workflows and state machines with world-class autocomplete and type inference. If you've ever wanted to build event-driven automations or a stateful agent, but in code, with all the benefits of TypeScript, Klonk is for you.
23
+ Klonk is a code-first, type-safe automation engine. It provides composable primitives to build workflows and state machines with autocomplete and type inference. If you've ever wanted to build event-driven automations or a stateful agent in code, with all the benefits of TypeScript, Klonk is for you.
24
24
 
25
- ![Code](./.github/assets/blurry.png)
26
25
  ![Skip to code examples ->](https://github.com/klar-web-services/klonk?tab=readme-ov-file#code-examples)
27
26
 
28
27
  The two main features are **Workflows** and **Machines**.
29
28
 
30
- - **Workflows**: Combine triggers with a series of tasks (a `Playlist`) to automate processes. Perfect for event-driven automation, like "when a file is added to Dropbox, parse it, and create an entry in Notion."
31
- - **Machines**: Create finite state machines where each state has its own `Playlist` of tasks and conditional transitions to other states. Ideal for building agents, multi-step processes, or any system with complex, stateful logic.
29
+ - **Workflows**: Combine triggers with a series of tasks (a `Playlist`) to automate processes. Example: "when a file is added to Dropbox, parse it, and create an entry in Notion."
30
+ - **Machines**: Finite state machines where each state has its own `Playlist` of tasks and conditional transitions to other states. Useful for agents, multi-step processes, or systems with stateful logic.
32
31
 
33
32
  ## Installation
34
33
 
@@ -38,9 +37,98 @@ bun add @fkws/klonk
38
37
  npm i @fkws/klonk
39
38
  ```
40
39
 
40
+ ### Compatibility
41
+
42
+ | Requirement | Support |
43
+ |-------------|---------|
44
+ | **Runtimes** | Node.js 18+, Bun 1.0+, Deno (via npm specifier, best-effort) |
45
+ | **Module** | ESM (native) and CJS (via bundled `/dist`) |
46
+ | **TypeScript** | 5.0+ (required for full type inference) |
47
+ | **Dependencies** | Zero runtime dependencies |
48
+
49
+ **Status:** Pre-1.0, API may change between minor versions. Aiming for stability by 1.0.
50
+
51
+ ## Quickstart
52
+
53
+ Copy-paste this to see Klonk in action. One trigger, two tasks, fully typed outputs:
54
+
55
+ ```typescript
56
+ import { Task, Trigger, Workflow, Railroad, isOk } from "@fkws/klonk";
57
+
58
+ // 1. Define two simple tasks
59
+ class FetchUser<I extends string> extends Task<{ userId: string }, { name: string; email: string }, I> {
60
+ async validateInput(input: { userId: string }) { return !!input.userId; }
61
+ async run(input: { userId: string }): Promise<Railroad<{ name: string; email: string }>> {
62
+ if (input.userId !== "123") return { success: false, error: new Error("User not found") };
63
+ return { success: true, data: { name: "Alice", email: "alice@example.com" } };
64
+ }
65
+ }
66
+
67
+ class SendEmail<I extends string> extends Task<{ to: string; subject: string }, { sent: boolean }, I> {
68
+ async validateInput(input: { to: string; subject: string }) { return !!input.to; }
69
+ async run(input: { to: string; subject: string }): Promise<Railroad<{ sent: boolean }>> {
70
+ console.log(`📧 Sending "${input.subject}" to ${input.to}`);
71
+ return { success: true, data: { sent: true } };
72
+ }
73
+ }
74
+
75
+ // 2. Create a trigger (fires once with a userId)
76
+ class ManualTrigger<I extends string> extends Trigger<I, { userId: string }> {
77
+ async start() { this.pushEvent({ userId: "123" }); }
78
+ async stop() {}
79
+ }
80
+
81
+ // 3. Wire it up: trigger → playlist with typed outputs
82
+ const workflow = Workflow.create()
83
+ .addTrigger(new ManualTrigger("manual"))
84
+ .setPlaylist(p => p
85
+ .addTask(new FetchUser("fetch-user"))
86
+ .input((source) => ({ userId: source.data.userId })) // ← source.data is typed!
87
+
88
+ .addTask(new SendEmail("send-email"))
89
+ .input((source, outputs) => {
90
+ // outputs["fetch-user"] is typed as Railroad<{ name, email }> | null
91
+ const user = outputs["fetch-user"];
92
+ if (!user || !isOk(user)) return null; // skip if failed
93
+ return { to: user.data.email, subject: `Welcome, ${user.data.name}!` };
94
+ })
95
+ );
96
+
97
+ workflow.start({ callback: (src, out) => console.log("✅ Done!", out) });
98
+ ```
99
+
100
+ **What you just saw:**
101
+ - `source.data.userId` is typed from the trigger
102
+ - `outputs["fetch-user"]` is typed by the task's ident string literal
103
+ - `user.data.email` is narrowed after the `isOk()` check
104
+
105
+ ## TypeScript Magic Moment
106
+
107
+ Klonk's type inference isn't marketing. Here's proof:
108
+
109
+ ```typescript
110
+ import { Machine } from "@fkws/klonk";
111
+
112
+ // Declare states upfront → autocomplete for ALL transitions
113
+ const machine = Machine.create<{ count: number }>()
114
+ .withStates("idle", "processing", "done") // ← These drive autocomplete
115
+ .addState("idle", node => node
116
+ .setPlaylist(p => p/* ... */)
117
+ .addTransition({
118
+ to: "processing", // ← Type "pro" and your IDE suggests "processing"
119
+ condition: async () => true,
120
+ weight: 1
121
+ })
122
+ // @ts-expect-error - "typo-state" is not a valid state
123
+ .addTransition({ to: "typo-state", condition: async () => true, weight: 1 })
124
+ , { initial: true });
125
+ ```
126
+
127
+ The `withStates<...>()` pattern means **you can't transition to a state that doesn't exist**. TypeScript catches it at compile time, not runtime.
128
+
41
129
  ## Core Concepts
42
130
 
43
- At the heart of Klonk are a few key concepts that work together.
131
+ Klonk has a few concepts that work together.
44
132
 
45
133
  ### Task
46
134
 
@@ -87,15 +175,11 @@ if (isErr(result)) {
87
175
 
88
176
  #### Why Railroad?
89
177
 
90
- The name "Railroad" comes from Railway Oriented Programming - a functional approach where success travels the "happy path" and errors get shunted to the "error track". Combined with TypeScript's type narrowing, you get:
91
-
92
- - **No uncaught exceptions** - errors are values, not thrown
93
- - **Explicit error handling** - the type system forces you to handle failures
94
- - **Familiar patterns** - if you love Rust's `Result`, you'll feel right at home
178
+ The name "Railroad" comes from Railway Oriented Programming, where success travels the "happy path" and errors get shunted to the "error track". Combined with TypeScript's type narrowing, you get explicit error handling without exceptions. If you like Rust's `Result`, you'll feel at home.
95
179
 
96
180
  ### Playlist
97
181
 
98
- A `Playlist` is a sequence of `Tasks` executed in order. The magic of a `Playlist` is that each task has access to the outputs of all previous tasks, in a fully type-safe way. You build a `Playlist` by chaining `.addTask().input()` calls:
182
+ A `Playlist` is a sequence of `Tasks` executed in order. Each task has access to the outputs of all previous tasks, in a fully type-safe way. You build a `Playlist` by chaining `.addTask().input()` calls:
99
183
 
100
184
  ```typescript
101
185
  import { isOk } from "@fkws/klonk";
@@ -161,7 +245,7 @@ Workflow.create()
161
245
  node.preventRetry() // Task failures throw immediately
162
246
  ```
163
247
 
164
- Default behavior: infinite retries at 1000ms delay. Use `.preventRetry()` to fail fast, or `.retryLimit(n)` to cap attempts.
248
+ Default behavior: infinite retries at 1000ms delay. This is designed for long-running daemons and background workers where resilience matters. **For request/response contexts** (APIs, CLIs, one-shot scripts), set `.retryLimit(n)` to cap attempts or use `.preventRetry()` to fail fast.
165
249
 
166
250
  ### Trigger
167
251
 
@@ -173,11 +257,11 @@ A `Workflow` connects one or more `Triggers` to a `Playlist`. When a trigger fir
173
257
 
174
258
  ### Machine
175
259
 
176
- A `Machine` is a finite state machine. You build it by declaring all state identifiers upfront with `.withStates<...>()`, then adding states with `.addState()`:
260
+ A `Machine` is a finite state machine. You build it by declaring all state identifiers upfront with `.withStates(...)`, then adding states with `.addState()`:
177
261
 
178
262
  ```typescript
179
263
  Machine.create<MyStateData>()
180
- .withStates<"idle" | "running" | "complete">() // Declare all states
264
+ .withStates("idle", "running", "complete") // Declare all states
181
265
  .addState("idle", node => node
182
266
  .setPlaylist(p => p.addTask(...).input(...))
183
267
  .addTransition({ to: "running", condition: ..., weight: 1 }) // Autocomplete!
@@ -206,14 +290,14 @@ Notes:
206
290
 
207
291
  ## Features
208
292
 
209
- - **Type-Safe & Autocompleted**: Klonk leverages TypeScript's inference to provide a world-class developer experience. The inputs and outputs of every step are strongly typed, so you'll know at compile time if your logic is sound.
210
- - **Code-First**: Define your automations directly in TypeScript. No YAML, no drag-and-drop UIs. Just the full power of a real programming language.
211
- - **Composable & Extensible**: The core primitives (`Task`, `Trigger`) are simple abstract classes, making it easy to create your own reusable components and integrations.
293
+ - **Type-Safe & Autocompleted**: Klonk uses TypeScript's inference so the inputs and outputs of every step are strongly typed. You'll know at compile time if your logic is sound.
294
+ - **Code-First**: Define your automations directly in TypeScript. No YAML, no drag-and-drop UIs.
295
+ - **Composable & Extensible**: The core primitives (`Task`, `Trigger`) are simple abstract classes, so you can create your own reusable components.
212
296
  - **Flexible Execution**: `Machines` run with configurable modes via `run(state, options)`: `any`, `leaf`, `roundtrip`, or `infinitely` (with optional `interval`).
213
297
 
214
298
  ## Klonkworks: Pre-built Components
215
299
 
216
- Coming soon(ish)! Klonkworks will be a large collection of pre-built Tasks, Triggers, and integrations. This will allow you to quickly assemble powerful automations that connect to a wide variety of services, often without needing to build your own components from scratch.
300
+ Coming soon(ish)! Klonkworks will be a collection of pre-built Tasks, Triggers, and integrations that connect to various services, so you don't have to build everything from scratch.
217
301
 
218
302
  ## Code Examples
219
303
 
@@ -324,7 +408,7 @@ export class IntervalTrigger<TIdent extends string> extends Trigger<TIdent, { no
324
408
  <details>
325
409
  <summary><b>Building a Workflow</b></summary>
326
410
 
327
- Workflows are perfect for event-driven automations. This example creates a workflow that triggers when a new invoice PDF is added to a Dropbox folder. It then parses the invoice and creates a new item in a Notion database.
411
+ Workflows work well for event-driven automations. This example triggers when a new invoice PDF is added to a Dropbox folder, parses the invoice, and creates a new item in a Notion database.
328
412
 
329
413
  Notice the fluent `.addTask(task).input(builder)` syntax - each task's input builder has access to `source` (trigger data) and `outputs` (all previous task results), with full type inference!
330
414
 
@@ -450,7 +534,7 @@ workflow.start({
450
534
  <details>
451
535
  <summary><b>Building a Machine</b></summary>
452
536
 
453
- `Machines` are ideal for building complex, stateful agents. This example shows an AI agent that takes a user's query, refines it, performs a web search, and generates a final response.
537
+ `Machines` work well for stateful agents. This example shows an AI agent that takes a user's query, refines it, performs a web search, and generates a response.
454
538
 
455
539
  The `Machine` manages a `StateData` object. Each `StateNode`'s `Playlist` can modify this state, and the `Transitions` between states use it to decide which state to move to next.
456
540
 
@@ -489,7 +573,7 @@ const client = new OpenRouterClient(process.env.OPENROUTER_API_KEY!)
489
573
  const webSearchAgent = Machine
490
574
  .create<StateData>()
491
575
  // Declare all states upfront for transition autocomplete
492
- .withStates<"refine_and_extract" | "search_web" | "generate_response">()
576
+ .withStates("refine_and_extract", "search_web", "generate_response")
493
577
  .addState("refine_and_extract", node => node
494
578
  .setPlaylist(p => p
495
579
  // Refine the user's input
@@ -589,7 +673,7 @@ console.log(state.finalResponse);
589
673
 
590
674
  ## Type System
591
675
 
592
- Klonk's type system is designed to be minimal yet powerful. Here's what makes it tick:
676
+ Klonk's type system is minimal. Here's how it works:
593
677
 
594
678
  ### Core Types
595
679
 
package/dist/index.cjs CHANGED
@@ -357,7 +357,7 @@ class Machine {
357
357
  static create() {
358
358
  return new Machine;
359
359
  }
360
- withStates() {
360
+ withStates(..._states) {
361
361
  return this;
362
362
  }
363
363
  finalize({
package/dist/index.d.ts CHANGED
@@ -24,16 +24,16 @@ declare function isOk<
24
24
  T,
25
25
  E
26
26
  >(r: Railroad<T, E>): r is {
27
- success: true
28
- data: T
27
+ readonly success: true
28
+ readonly data: T
29
29
  };
30
30
  /** Type guard: returns true if the Railroad is an error */
31
31
  declare function isErr<
32
32
  T,
33
33
  E
34
34
  >(r: Railroad<T, E>): r is {
35
- success: false
36
- error: E
35
+ readonly success: false
36
+ readonly error: E
37
37
  };
38
38
  /** Returns the data if success, throws the error if failure (like Rust's unwrap) */
39
39
  declare function unwrap<
@@ -463,7 +463,7 @@ declare class StateNode<
463
463
  next(data: TStateData): Promise<StateNode<TStateData> | null>;
464
464
  }
465
465
  /**
466
- * Returned by `Machine.create()` - you must call `.withStates<...>()` to declare state idents.
466
+ * Returned by `Machine.create()` - you must call `.withStates(...)` to declare state idents.
467
467
  *
468
468
  * This ensures all state identifiers are known upfront for full transition autocomplete.
469
469
  */
@@ -472,17 +472,18 @@ interface MachineNeedsStates<TStateData> {
472
472
  * Declare all state identifiers that will be used in this machine.
473
473
  * This enables full autocomplete for transition targets.
474
474
  *
475
- * @template TIdents - Union of all state idents (e.g., `"idle" | "running" | "complete"`).
475
+ * @param states - All state idents as string arguments. If omitted, any string is allowed (useful for tests).
476
476
  * @returns The machine, ready for adding states.
477
477
  *
478
478
  * @example
479
479
  * Machine.create<MyState>()
480
- * .withStates<"idle" | "running" | "complete">()
480
+ * .withStates("idle", "running", "complete")
481
481
  * .addState("idle", node => node
482
482
  * .addTransition({ to: "running", ... }) // Autocomplete works!
483
483
  * )
484
484
  */
485
- withStates<TIdents extends string>(): Machine<TStateData, TIdents>;
485
+ withStates(): Machine<TStateData, string>;
486
+ withStates<const T extends readonly string[]>(...states: T): Machine<TStateData, T[number]>;
486
487
  }
487
488
  /**
488
489
  * A finite state machine that coordinates execution of `StateNode` playlists
@@ -518,7 +519,7 @@ declare class Machine<
518
519
  */
519
520
  private sleep;
520
521
  /**
521
- * Create a new Machine. You must call `.withStates<...>()` next to declare
522
+ * Create a new Machine. You must call `.withStates(...)` next to declare
522
523
  * all state identifiers before adding states.
523
524
  *
524
525
  * @template TStateData - The shape of the mutable state carried through the machine.
@@ -526,7 +527,7 @@ declare class Machine<
526
527
  *
527
528
  * @example
528
529
  * Machine.create<MyState>()
529
- * .withStates<"idle" | "running">()
530
+ * .withStates("idle", "running")
530
531
  * .addState("idle", node => ...)
531
532
  */
532
533
  static create<TStateData>(): MachineNeedsStates<TStateData>;
@@ -536,7 +537,7 @@ declare class Machine<
536
537
  *
537
538
  * @internal
538
539
  */
539
- withStates<TIdents extends string>(): Machine<TStateData, TIdents>;
540
+ withStates<const T extends readonly string[]>(..._states: T): Machine<TStateData, T[number]>;
540
541
  /**
541
542
  * Finalize the machine by resolving state transitions and locking configuration.
542
543
  * Must be called before `start` or `run`.
@@ -572,9 +573,9 @@ declare class Machine<
572
573
  * .addTransition({ to: "idle", condition: async () => true, weight: 1 })
573
574
  * )
574
575
  */
575
- addState<const TIdent extends string>(ident: TIdent, builder: (node: StateNode<TStateData, TIdent, AllStateIdents | TIdent>) => StateNode<TStateData, TIdent, AllStateIdents | TIdent>, options?: {
576
+ addState<const TIdent extends AllStateIdents>(ident: TIdent, builder: (node: StateNode<TStateData, TIdent, AllStateIdents>) => StateNode<TStateData, TIdent, AllStateIdents>, options?: {
576
577
  initial?: boolean
577
- }): Machine<TStateData, AllStateIdents | TIdent>;
578
+ }): Machine<TStateData, AllStateIdents>;
578
579
  /**
579
580
  * Attach a logger to this machine. If the machine has an initial state set,
580
581
  * the logger will be propagated to all currently reachable states.
package/dist/index.js CHANGED
@@ -311,7 +311,7 @@ class Machine {
311
311
  static create() {
312
312
  return new Machine;
313
313
  }
314
- withStates() {
314
+ withStates(..._states) {
315
315
  return this;
316
316
  }
317
317
  finalize({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fkws/klonk",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "description": "A lightweight, extensible workflow automation engine for Node.js and Bun",
5
5
  "repository": "https://github.com/klar-web-services/klonk",
6
6
  "homepage": "https://klonk.dev",