@pikku/core 0.12.4 → 0.12.6

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.
Files changed (235) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/dist/dev/hot-reload.d.ts +10 -0
  3. package/dist/dev/hot-reload.js +158 -0
  4. package/dist/middleware-runner.d.ts +1 -0
  5. package/dist/middleware-runner.js +5 -0
  6. package/dist/permissions.d.ts +1 -0
  7. package/dist/permissions.js +5 -0
  8. package/dist/services/content-service.d.ts +2 -0
  9. package/dist/services/in-memory-workflow-service.d.ts +1 -0
  10. package/dist/services/in-memory-workflow-service.js +16 -11
  11. package/dist/services/workflow-service.d.ts +1 -0
  12. package/dist/wirings/ai-agent/ai-agent-prepare.js +16 -1
  13. package/dist/wirings/channel/channel-middleware-runner.d.ts +1 -0
  14. package/dist/wirings/channel/channel-middleware-runner.js +5 -0
  15. package/dist/wirings/http/http-runner.js +1 -1
  16. package/dist/wirings/workflow/pikku-workflow-service.d.ts +6 -0
  17. package/dist/wirings/workflow/pikku-workflow-service.js +51 -16
  18. package/dist/wirings/workflow/workflow.types.d.ts +2 -0
  19. package/package.json +5 -3
  20. package/src/crypto-utils.test.ts +214 -0
  21. package/src/crypto-utils.ts +213 -0
  22. package/src/dev/hot-reload.test.ts +484 -0
  23. package/src/dev/hot-reload.ts +212 -0
  24. package/src/errors/error-handler.ts +62 -0
  25. package/src/errors/error.test.ts +195 -0
  26. package/src/errors/errors.ts +374 -0
  27. package/src/errors/index.ts +2 -0
  28. package/src/factory-functions.test.ts +109 -0
  29. package/src/function/function-runner.test.ts +536 -0
  30. package/src/function/function-runner.ts +365 -0
  31. package/src/function/functions.types.ts +304 -0
  32. package/src/function/index.ts +5 -0
  33. package/src/handle-error.test.ts +424 -0
  34. package/src/handle-error.ts +69 -0
  35. package/src/index.ts +118 -0
  36. package/src/internal.ts +6 -0
  37. package/src/middleware/auth-apikey.test.ts +363 -0
  38. package/src/middleware/auth-apikey.ts +47 -0
  39. package/src/middleware/auth-bearer.test.ts +450 -0
  40. package/src/middleware/auth-bearer.ts +72 -0
  41. package/src/middleware/auth-cookie.test.ts +528 -0
  42. package/src/middleware/auth-cookie.ts +80 -0
  43. package/src/middleware/cors.test.ts +424 -0
  44. package/src/middleware/cors.ts +104 -0
  45. package/src/middleware/index.ts +5 -0
  46. package/src/middleware/remote-auth.test.ts +488 -0
  47. package/src/middleware/remote-auth.ts +68 -0
  48. package/src/middleware/timeout.ts +15 -0
  49. package/src/middleware-runner.test.ts +418 -0
  50. package/src/middleware-runner.ts +240 -0
  51. package/src/permissions.test.ts +434 -0
  52. package/src/permissions.ts +327 -0
  53. package/src/pikku-request.ts +23 -0
  54. package/src/pikku-response.ts +5 -0
  55. package/src/pikku-state.test.ts +224 -0
  56. package/src/pikku-state.ts +216 -0
  57. package/src/run-tests-script.test.ts +49 -0
  58. package/src/schema.test.ts +249 -0
  59. package/src/schema.ts +151 -0
  60. package/src/services/ai-agent-runner-service.ts +41 -0
  61. package/src/services/ai-run-state-service.ts +20 -0
  62. package/src/services/ai-storage-service.ts +39 -0
  63. package/src/services/content-service.ts +81 -0
  64. package/src/services/deployment-service.ts +22 -0
  65. package/src/services/gateway-service.ts +20 -0
  66. package/src/services/gopass-secrets.ts +98 -0
  67. package/src/services/in-memory-ai-run-state-service.ts +73 -0
  68. package/src/services/in-memory-trigger-service.ts +64 -0
  69. package/src/services/in-memory-workflow-service.test.ts +351 -0
  70. package/src/services/in-memory-workflow-service.ts +512 -0
  71. package/src/services/index.ts +56 -0
  72. package/src/services/jwt-service.ts +30 -0
  73. package/src/services/local-content.ts +120 -0
  74. package/src/services/local-gateway-service.ts +62 -0
  75. package/src/services/local-secrets.test.ts +80 -0
  76. package/src/services/local-secrets.ts +94 -0
  77. package/src/services/local-variables.test.ts +61 -0
  78. package/src/services/local-variables.ts +39 -0
  79. package/src/services/logger-console.test.ts +118 -0
  80. package/src/services/logger-console.ts +98 -0
  81. package/src/services/logger.ts +57 -0
  82. package/src/services/scheduler-service.ts +86 -0
  83. package/src/services/schema-service.ts +33 -0
  84. package/src/services/scoped-secret-service.test.ts +74 -0
  85. package/src/services/scoped-secret-service.ts +41 -0
  86. package/src/services/secret-service.ts +38 -0
  87. package/src/services/trigger-service.ts +17 -0
  88. package/src/services/typed-secret-service.test.ts +93 -0
  89. package/src/services/typed-secret-service.ts +70 -0
  90. package/src/services/typed-variables-service.test.ts +73 -0
  91. package/src/services/typed-variables-service.ts +81 -0
  92. package/src/services/user-session-service.test.ts +113 -0
  93. package/src/services/user-session-service.ts +86 -0
  94. package/src/services/variables-service.ts +11 -0
  95. package/src/services/workflow-service.ts +96 -0
  96. package/src/testing/index.ts +2 -0
  97. package/src/testing/service-tests.ts +873 -0
  98. package/src/time-utils.test.ts +56 -0
  99. package/src/time-utils.ts +107 -0
  100. package/src/types/core.types.ts +478 -0
  101. package/src/types/state.types.ts +188 -0
  102. package/src/utils/hash.test.ts +68 -0
  103. package/src/utils/hash.ts +26 -0
  104. package/src/utils.test.ts +350 -0
  105. package/src/utils.ts +129 -0
  106. package/src/version.test.ts +80 -0
  107. package/src/version.ts +25 -0
  108. package/src/wirings/ai-agent/agent-dynamic-workflow.ts +469 -0
  109. package/src/wirings/ai-agent/ai-agent-helpers.test.ts +152 -0
  110. package/src/wirings/ai-agent/ai-agent-helpers.ts +76 -0
  111. package/src/wirings/ai-agent/ai-agent-memory.ts +310 -0
  112. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +115 -0
  113. package/src/wirings/ai-agent/ai-agent-model-config.ts +43 -0
  114. package/src/wirings/ai-agent/ai-agent-prepare.ts +659 -0
  115. package/src/wirings/ai-agent/ai-agent-registry.test.ts +37 -0
  116. package/src/wirings/ai-agent/ai-agent-registry.ts +82 -0
  117. package/src/wirings/ai-agent/ai-agent-runner.test.ts +319 -0
  118. package/src/wirings/ai-agent/ai-agent-runner.ts +773 -0
  119. package/src/wirings/ai-agent/ai-agent-stream.test.ts +859 -0
  120. package/src/wirings/ai-agent/ai-agent-stream.ts +1153 -0
  121. package/src/wirings/ai-agent/ai-agent.types.ts +382 -0
  122. package/src/wirings/ai-agent/index.ts +37 -0
  123. package/src/wirings/channel/channel-common.ts +90 -0
  124. package/src/wirings/channel/channel-handler.ts +250 -0
  125. package/src/wirings/channel/channel-middleware-runner.ts +126 -0
  126. package/src/wirings/channel/channel-runner.ts +166 -0
  127. package/src/wirings/channel/channel-store.ts +29 -0
  128. package/src/wirings/channel/channel.types.ts +172 -0
  129. package/src/wirings/channel/define-channel-routes.ts +25 -0
  130. package/src/wirings/channel/eventhub-service.ts +34 -0
  131. package/src/wirings/channel/eventhub-store.ts +13 -0
  132. package/src/wirings/channel/index.ts +24 -0
  133. package/src/wirings/channel/local/index.ts +3 -0
  134. package/src/wirings/channel/local/local-channel-handler.ts +81 -0
  135. package/src/wirings/channel/local/local-channel-runner.test.ts +215 -0
  136. package/src/wirings/channel/local/local-channel-runner.ts +210 -0
  137. package/src/wirings/channel/local/local-eventhub-service.test.ts +95 -0
  138. package/src/wirings/channel/local/local-eventhub-service.ts +101 -0
  139. package/src/wirings/channel/log-channels.ts +20 -0
  140. package/src/wirings/channel/pikku-abstract-channel-handler.test.ts +54 -0
  141. package/src/wirings/channel/pikku-abstract-channel-handler.ts +45 -0
  142. package/src/wirings/channel/serverless/index.ts +5 -0
  143. package/src/wirings/channel/serverless/serverless-channel-runner.ts +289 -0
  144. package/src/wirings/cli/channel/cli-channel-runner.ts +136 -0
  145. package/src/wirings/cli/channel/index.ts +1 -0
  146. package/src/wirings/cli/cli-runner.test.ts +403 -0
  147. package/src/wirings/cli/cli-runner.ts +529 -0
  148. package/src/wirings/cli/cli.types.ts +358 -0
  149. package/src/wirings/cli/command-parser.test.ts +445 -0
  150. package/src/wirings/cli/command-parser.ts +504 -0
  151. package/src/wirings/cli/define-cli-commands.ts +24 -0
  152. package/src/wirings/cli/index.ts +19 -0
  153. package/src/wirings/gateway/gateway-runner.test.ts +474 -0
  154. package/src/wirings/gateway/gateway-runner.ts +411 -0
  155. package/src/wirings/gateway/gateway.types.ts +149 -0
  156. package/src/wirings/gateway/index.ts +13 -0
  157. package/src/wirings/http/http-routes.test.ts +322 -0
  158. package/src/wirings/http/http-routes.ts +197 -0
  159. package/src/wirings/http/http-runner.test.ts +144 -0
  160. package/src/wirings/http/http-runner.ts +588 -0
  161. package/src/wirings/http/http.types.ts +369 -0
  162. package/src/wirings/http/index.ts +28 -0
  163. package/src/wirings/http/log-http-routes.ts +22 -0
  164. package/src/wirings/http/pikku-fetch-http-request.test.ts +237 -0
  165. package/src/wirings/http/pikku-fetch-http-request.ts +170 -0
  166. package/src/wirings/http/pikku-fetch-http-response.test.ts +82 -0
  167. package/src/wirings/http/pikku-fetch-http-response.ts +147 -0
  168. package/src/wirings/http/routers/http-router.ts +13 -0
  169. package/src/wirings/http/routers/path-to-regex.test.ts +319 -0
  170. package/src/wirings/http/routers/path-to-regex.ts +126 -0
  171. package/src/wirings/http/web-request.test.ts +236 -0
  172. package/src/wirings/http/web-request.ts +104 -0
  173. package/src/wirings/mcp/index.ts +27 -0
  174. package/src/wirings/mcp/mcp-endpoint-registry.test.ts +389 -0
  175. package/src/wirings/mcp/mcp-endpoint-registry.ts +159 -0
  176. package/src/wirings/mcp/mcp-runner.ts +323 -0
  177. package/src/wirings/mcp/mcp.types.ts +216 -0
  178. package/src/wirings/node/index.ts +2 -0
  179. package/src/wirings/node/node.types.ts +23 -0
  180. package/src/wirings/oauth2/index.ts +3 -0
  181. package/src/wirings/oauth2/oauth2-client.test.ts +929 -0
  182. package/src/wirings/oauth2/oauth2-client.ts +335 -0
  183. package/src/wirings/oauth2/oauth2.types.ts +69 -0
  184. package/src/wirings/oauth2/wire-oauth2-credential.ts +23 -0
  185. package/src/wirings/queue/index.ts +31 -0
  186. package/src/wirings/queue/queue-runner.test.ts +710 -0
  187. package/src/wirings/queue/queue-runner.ts +192 -0
  188. package/src/wirings/queue/queue.types.ts +189 -0
  189. package/src/wirings/queue/register-queue-helper.ts +60 -0
  190. package/src/wirings/queue/validate-worker-config.test.ts +108 -0
  191. package/src/wirings/queue/validate-worker-config.ts +116 -0
  192. package/src/wirings/rpc/index.ts +4 -0
  193. package/src/wirings/rpc/rpc-runner.ts +460 -0
  194. package/src/wirings/rpc/rpc-types.ts +56 -0
  195. package/src/wirings/rpc/wire-addon.ts +21 -0
  196. package/src/wirings/scheduler/index.ts +11 -0
  197. package/src/wirings/scheduler/log-schedulers.ts +20 -0
  198. package/src/wirings/scheduler/scheduler-runner.test.ts +660 -0
  199. package/src/wirings/scheduler/scheduler-runner.ts +133 -0
  200. package/src/wirings/scheduler/scheduler.types.ts +53 -0
  201. package/src/wirings/secret/index.ts +9 -0
  202. package/src/wirings/secret/secret.types.ts +32 -0
  203. package/src/wirings/secret/validate-secret-definitions.test.ts +140 -0
  204. package/src/wirings/secret/validate-secret-definitions.ts +82 -0
  205. package/src/wirings/trigger/index.ts +10 -0
  206. package/src/wirings/trigger/pikku-trigger-service.ts +112 -0
  207. package/src/wirings/trigger/trigger-runner.test.ts +79 -0
  208. package/src/wirings/trigger/trigger-runner.ts +135 -0
  209. package/src/wirings/trigger/trigger.types.ts +178 -0
  210. package/src/wirings/variable/index.ts +8 -0
  211. package/src/wirings/variable/validate-variable-definitions.test.ts +91 -0
  212. package/src/wirings/variable/validate-variable-definitions.ts +69 -0
  213. package/src/wirings/variable/variable.types.ts +22 -0
  214. package/src/wirings/workflow/dsl/index.ts +31 -0
  215. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +320 -0
  216. package/src/wirings/workflow/dsl/workflow-runner.ts +28 -0
  217. package/src/wirings/workflow/graph/graph-node.ts +222 -0
  218. package/src/wirings/workflow/graph/graph-runner.test.ts +487 -0
  219. package/src/wirings/workflow/graph/graph-runner.ts +816 -0
  220. package/src/wirings/workflow/graph/graph-validation.test.ts +170 -0
  221. package/src/wirings/workflow/graph/graph-validation.ts +237 -0
  222. package/src/wirings/workflow/graph/index.ts +19 -0
  223. package/src/wirings/workflow/graph/template.test.ts +49 -0
  224. package/src/wirings/workflow/graph/template.ts +42 -0
  225. package/src/wirings/workflow/graph/wire-workflow-graph.ts +29 -0
  226. package/src/wirings/workflow/graph/workflow-graph.types.ts +104 -0
  227. package/src/wirings/workflow/index.ts +82 -0
  228. package/src/wirings/workflow/pikku-workflow-service.test.ts +200 -0
  229. package/src/wirings/workflow/pikku-workflow-service.ts +1229 -0
  230. package/src/wirings/workflow/workflow-helpers.test.ts +129 -0
  231. package/src/wirings/workflow/workflow-helpers.ts +79 -0
  232. package/src/wirings/workflow/workflow.types.ts +276 -0
  233. package/tsconfig.json +14 -0
  234. package/tsconfig.tsbuildinfo +1 -0
  235. package/lcov.info +0 -18891
@@ -0,0 +1,170 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import {
5
+ computeEntryNodeIds,
6
+ validateWorkflowWiring,
7
+ generateMermaidDiagram,
8
+ } from './graph-validation.js'
9
+
10
+ describe('computeEntryNodeIds', () => {
11
+ it('returns nodes not referenced by any other node', () => {
12
+ const nodes = {
13
+ start: { rpcName: 'a', next: 'middle' },
14
+ middle: { rpcName: 'b', next: 'end' },
15
+ end: { rpcName: 'c' },
16
+ }
17
+ assert.deepEqual(computeEntryNodeIds(nodes), ['start'])
18
+ })
19
+
20
+ it('returns multiple entry nodes for parallel starts', () => {
21
+ const nodes = {
22
+ a: { rpcName: 'x' },
23
+ b: { rpcName: 'y' },
24
+ c: { rpcName: 'z', next: 'a' },
25
+ }
26
+ const entries = computeEntryNodeIds(nodes)
27
+ assert.ok(entries.includes('b'))
28
+ assert.ok(entries.includes('c'))
29
+ assert.ok(!entries.includes('a'))
30
+ })
31
+
32
+ it('returns empty for fully circular graph', () => {
33
+ const nodes = {
34
+ a: { rpcName: 'x', next: 'b' },
35
+ b: { rpcName: 'y', next: 'a' },
36
+ }
37
+ assert.deepEqual(computeEntryNodeIds(nodes), [])
38
+ })
39
+
40
+ it('handles onError references', () => {
41
+ const nodes = {
42
+ start: { rpcName: 'a', next: 'ok', onError: 'err' },
43
+ ok: { rpcName: 'b' },
44
+ err: { rpcName: 'c' },
45
+ }
46
+ assert.deepEqual(computeEntryNodeIds(nodes), ['start'])
47
+ })
48
+
49
+ it('handles branching next as object', () => {
50
+ const nodes = {
51
+ start: { rpcName: 'a', next: { yes: 'b', no: 'c' } },
52
+ b: { rpcName: 'x' },
53
+ c: { rpcName: 'y' },
54
+ }
55
+ assert.deepEqual(computeEntryNodeIds(nodes), ['start'])
56
+ })
57
+
58
+ it('handles parallel next as array', () => {
59
+ const nodes = {
60
+ start: { rpcName: 'a', next: ['b', 'c'] },
61
+ b: { rpcName: 'x' },
62
+ c: { rpcName: 'y' },
63
+ }
64
+ assert.deepEqual(computeEntryNodeIds(nodes), ['start'])
65
+ })
66
+ })
67
+
68
+ describe('validateWorkflowWiring', () => {
69
+ const tools = ['toolA', 'toolB', 'toolC']
70
+
71
+ it('returns no errors for a valid workflow', () => {
72
+ const nodes = {
73
+ step1: { rpcName: 'toolA', input: {}, next: 'step2' },
74
+ step2: { rpcName: 'toolB', input: {} },
75
+ }
76
+ const errors = validateWorkflowWiring(nodes, tools)
77
+ assert.deepEqual(errors, [])
78
+ })
79
+
80
+ it('errors on missing rpcName', () => {
81
+ const nodes = {
82
+ step1: { input: {} },
83
+ }
84
+ const errors = validateWorkflowWiring(nodes, tools)
85
+ assert.ok(errors.some((e) => e.includes("missing 'rpcName'")))
86
+ })
87
+
88
+ it('errors on unknown tool', () => {
89
+ const nodes = {
90
+ step1: { rpcName: 'unknownTool', input: {} },
91
+ }
92
+ const errors = validateWorkflowWiring(nodes, tools)
93
+ assert.ok(errors.some((e) => e.includes('unknown tool')))
94
+ })
95
+
96
+ it('errors on next referencing unknown node', () => {
97
+ const nodes = {
98
+ step1: { rpcName: 'toolA', next: 'nonexistent' },
99
+ }
100
+ const errors = validateWorkflowWiring(nodes, tools)
101
+ assert.ok(errors.some((e) => e.includes("unknown node 'nonexistent'")))
102
+ })
103
+
104
+ it('errors on onError referencing unknown node', () => {
105
+ const nodes = {
106
+ step1: { rpcName: 'toolA', onError: 'missing' },
107
+ }
108
+ const errors = validateWorkflowWiring(nodes, tools)
109
+ assert.ok(errors.some((e) => e.includes("unknown node 'missing'")))
110
+ })
111
+
112
+ it('errors on input referencing unknown node', () => {
113
+ const nodes = {
114
+ step1: {
115
+ rpcName: 'toolA',
116
+ input: { field: { $ref: 'missing', path: 'x' } },
117
+ },
118
+ }
119
+ const errors = validateWorkflowWiring(nodes, tools)
120
+ assert.ok(errors.some((e) => e.includes("unknown node 'missing'")))
121
+ })
122
+
123
+ it('allows trigger refs without errors', () => {
124
+ const nodes = {
125
+ step1: {
126
+ rpcName: 'toolA',
127
+ input: { field: { $ref: 'trigger', path: 'title' } },
128
+ next: 'step2',
129
+ },
130
+ step2: { rpcName: 'toolB', input: {} },
131
+ }
132
+ const errors = validateWorkflowWiring(nodes, tools)
133
+ assert.deepEqual(errors, [])
134
+ })
135
+
136
+ it('allows valid node-to-node refs', () => {
137
+ const nodes = {
138
+ step1: { rpcName: 'toolA', input: {}, next: 'step2' },
139
+ step2: {
140
+ rpcName: 'toolB',
141
+ input: { data: { $ref: 'step1', path: 'result' } },
142
+ },
143
+ }
144
+ const errors = validateWorkflowWiring(nodes, tools)
145
+ assert.deepEqual(errors, [])
146
+ })
147
+ })
148
+
149
+ describe('generateMermaidDiagram', () => {
150
+ it('generates valid mermaid output', () => {
151
+ const nodes = {
152
+ start: { rpcName: 'toolA', next: 'end' },
153
+ end: { rpcName: 'toolB' },
154
+ }
155
+ const result = generateMermaidDiagram('test-wf', nodes, ['start'])
156
+ assert.ok(result.includes('graph TD'))
157
+ assert.ok(result.includes('start([toolA])'))
158
+ assert.ok(result.includes('end[toolB]'))
159
+ assert.ok(result.includes('start --> end'))
160
+ })
161
+
162
+ it('shows error edges with dotted lines', () => {
163
+ const nodes = {
164
+ start: { rpcName: 'toolA', onError: 'handler' },
165
+ handler: { rpcName: 'toolB' },
166
+ }
167
+ const result = generateMermaidDiagram('test-wf', nodes, ['start'])
168
+ assert.ok(result.includes('start -.->|error| handler'))
169
+ })
170
+ })
@@ -0,0 +1,237 @@
1
+ import { pikkuState } from '../../../pikku-state.js'
2
+ import { resolveNamespace } from '../../rpc/rpc-runner.js'
3
+
4
+ function normalizeTargets(value: unknown): string[] {
5
+ if (!value) return []
6
+ if (typeof value === 'string') return [value]
7
+ if (Array.isArray(value)) return value.filter((v) => typeof v === 'string')
8
+ if (typeof value === 'object') {
9
+ const targets: string[] = []
10
+ for (const v of Object.values(value as Record<string, unknown>)) {
11
+ targets.push(...normalizeTargets(v))
12
+ }
13
+ return targets
14
+ }
15
+ return []
16
+ }
17
+
18
+ function collectRefs(value: unknown, refs: Set<string>): void {
19
+ if (
20
+ typeof value === 'object' &&
21
+ value !== null &&
22
+ '$ref' in value &&
23
+ typeof (value as any).$ref === 'string'
24
+ ) {
25
+ refs.add((value as any).$ref)
26
+ return
27
+ }
28
+ if (Array.isArray(value)) {
29
+ for (const item of value) collectRefs(item, refs)
30
+ return
31
+ }
32
+ if (typeof value === 'object' && value !== null) {
33
+ for (const v of Object.values(value)) collectRefs(v, refs)
34
+ }
35
+ }
36
+
37
+ function resolveToolMeta(toolName: string): {
38
+ fnMeta: any
39
+ schemas: Map<string, any>
40
+ } | null {
41
+ const resolved = toolName.includes(':') ? resolveNamespace(toolName) : null
42
+
43
+ if (resolved) {
44
+ const fnMeta = pikkuState(resolved.package, 'function', 'meta')[
45
+ resolved.function
46
+ ]
47
+ const schemas = pikkuState(resolved.package, 'misc', 'schemas')
48
+ return fnMeta ? { fnMeta, schemas } : null
49
+ }
50
+
51
+ const rpcMeta = pikkuState(null, 'rpc', 'meta')
52
+ const pikkuFuncId = rpcMeta[toolName]
53
+ if (!pikkuFuncId) return null
54
+
55
+ const fnMeta = pikkuState(null, 'function', 'meta')[pikkuFuncId]
56
+ const schemas = pikkuState(null, 'misc', 'schemas')
57
+ return fnMeta ? { fnMeta, schemas } : null
58
+ }
59
+
60
+ export function computeEntryNodeIds(nodes: Record<string, any>): string[] {
61
+ const referenced = new Set<string>()
62
+ for (const node of Object.values(nodes)) {
63
+ if (node.next) {
64
+ for (const target of normalizeTargets(node.next)) {
65
+ referenced.add(target)
66
+ }
67
+ }
68
+ if (node.onError) {
69
+ for (const target of normalizeTargets(node.onError)) {
70
+ referenced.add(target)
71
+ }
72
+ }
73
+ }
74
+ return Object.keys(nodes).filter((id) => !referenced.has(id))
75
+ }
76
+
77
+ export function validateWorkflowWiring(
78
+ nodes: Record<string, any>,
79
+ toolNames: string[]
80
+ ): string[] {
81
+ const errors: string[] = []
82
+ const nodeIds = new Set(Object.keys(nodes))
83
+ const toolSet = new Set(toolNames)
84
+
85
+ for (const [nodeId, node] of Object.entries(nodes)) {
86
+ if (!node.rpcName) {
87
+ errors.push(`Node '${nodeId}' is missing 'rpcName'`)
88
+ continue
89
+ }
90
+
91
+ if (!toolSet.has(node.rpcName)) {
92
+ errors.push(
93
+ `Node '${nodeId}' references unknown tool '${node.rpcName}'. Available tools: ${toolNames.join(', ')}`
94
+ )
95
+ continue
96
+ }
97
+
98
+ const nextTargets = normalizeTargets(node.next)
99
+ for (const target of nextTargets) {
100
+ if (!nodeIds.has(target)) {
101
+ errors.push(
102
+ `Node '${nodeId}' routes to unknown node '${target}' in 'next'`
103
+ )
104
+ }
105
+ }
106
+
107
+ const errorTargets = normalizeTargets(node.onError)
108
+ for (const target of errorTargets) {
109
+ if (!nodeIds.has(target)) {
110
+ errors.push(
111
+ `Node '${nodeId}' routes to unknown node '${target}' in 'onError'`
112
+ )
113
+ }
114
+ }
115
+
116
+ if (!node.input) continue
117
+
118
+ const refs = new Set<string>()
119
+ collectRefs(node.input, refs)
120
+
121
+ for (const ref of refs) {
122
+ if (ref === 'trigger' || ref === '$item') {
123
+ const targetMeta = resolveToolMeta(node.rpcName)
124
+ if (targetMeta?.fnMeta?.inputSchemaName) {
125
+ const targetSchema = targetMeta.schemas.get(
126
+ targetMeta.fnMeta.inputSchemaName
127
+ )
128
+ if (targetSchema?.properties) {
129
+ for (const [field, fieldValue] of Object.entries(
130
+ node.input as Record<string, any>
131
+ )) {
132
+ if (
133
+ typeof fieldValue === 'object' &&
134
+ fieldValue !== null &&
135
+ fieldValue.$ref === ref &&
136
+ !fieldValue.path
137
+ ) {
138
+ const targetType = targetSchema.properties[field]?.type
139
+ if (targetType && targetType !== 'object') {
140
+ errors.push(
141
+ `Node '${nodeId}' input field '${field}' expects type '${targetType}', but references the whole ${ref} object without a path. Use { $ref: "${ref}", path: "${field}" } to extract the field.`
142
+ )
143
+ }
144
+ }
145
+ }
146
+ }
147
+ }
148
+ continue
149
+ }
150
+
151
+ if (!nodeIds.has(ref)) {
152
+ errors.push(
153
+ `Node '${nodeId}' references unknown node '${ref}' in input`
154
+ )
155
+ continue
156
+ }
157
+
158
+ const sourceNode = nodes[ref]
159
+ if (!sourceNode?.rpcName) continue
160
+
161
+ const sourceMeta = resolveToolMeta(sourceNode.rpcName)
162
+ if (!sourceMeta?.fnMeta?.outputSchemaName) continue
163
+
164
+ const sourceSchema = sourceMeta.schemas.get(
165
+ sourceMeta.fnMeta.outputSchemaName
166
+ )
167
+ if (!sourceSchema?.properties) continue
168
+
169
+ for (const [field, fieldValue] of Object.entries(
170
+ node.input as Record<string, any>
171
+ )) {
172
+ if (
173
+ typeof fieldValue === 'object' &&
174
+ fieldValue !== null &&
175
+ fieldValue.$ref === ref &&
176
+ fieldValue.path
177
+ ) {
178
+ const pathRoot = fieldValue.path.split('.')[0]
179
+ if (
180
+ sourceSchema.properties &&
181
+ !(pathRoot in sourceSchema.properties)
182
+ ) {
183
+ errors.push(
184
+ `Node '${nodeId}' input field '${field}' references path '${fieldValue.path}' but node '${ref}' (${sourceNode.rpcName}) output has no property '${pathRoot}'`
185
+ )
186
+ continue
187
+ }
188
+
189
+ const targetMeta = resolveToolMeta(node.rpcName)
190
+ if (!targetMeta?.fnMeta?.inputSchemaName) continue
191
+ const targetSchema = targetMeta.schemas.get(
192
+ targetMeta.fnMeta.inputSchemaName
193
+ )
194
+ if (!targetSchema?.properties?.[field]) continue
195
+
196
+ const sourceType = sourceSchema.properties[pathRoot]?.type
197
+ const targetType = targetSchema.properties[field].type
198
+ if (sourceType && targetType && sourceType !== targetType) {
199
+ errors.push(
200
+ `Node '${nodeId}' input field '${field}' expects type '${targetType}', but node '${ref}' output field '${pathRoot}' is type '${sourceType}'`
201
+ )
202
+ }
203
+ }
204
+ }
205
+ }
206
+ }
207
+
208
+ return errors
209
+ }
210
+
211
+ export function generateMermaidDiagram(
212
+ workflowName: string,
213
+ nodes: Record<string, any>,
214
+ entryNodeIds: string[]
215
+ ): string {
216
+ const lines: string[] = ['graph TD']
217
+
218
+ for (const [nodeId, node] of Object.entries(nodes)) {
219
+ const label = node.rpcName || nodeId
220
+ const isEntry = entryNodeIds.includes(nodeId)
221
+ lines.push(
222
+ ` ${nodeId}${isEntry ? '([' : '['}${label}${isEntry ? '])' : ']'}`
223
+ )
224
+
225
+ const nextTargets = normalizeTargets(node.next)
226
+ for (const target of nextTargets) {
227
+ lines.push(` ${nodeId} --> ${target}`)
228
+ }
229
+
230
+ const errorTargets = normalizeTargets(node.onError)
231
+ for (const target of errorTargets) {
232
+ lines.push(` ${nodeId} -.->|error| ${target}`)
233
+ }
234
+ }
235
+
236
+ return lines.join('\n')
237
+ }
@@ -0,0 +1,19 @@
1
+ export {
2
+ continueGraph,
3
+ executeGraphStep,
4
+ onGraphNodeComplete,
5
+ runFromMeta,
6
+ runWorkflowGraph,
7
+ } from './graph-runner.js'
8
+ export { template } from './template.js'
9
+ export type { TemplateString } from './template.js'
10
+ export {
11
+ pikkuWorkflowGraph,
12
+ type PikkuWorkflowGraphConfig,
13
+ type PikkuWorkflowGraphResult,
14
+ } from './wire-workflow-graph.js'
15
+ export {
16
+ validateWorkflowWiring,
17
+ computeEntryNodeIds,
18
+ generateMermaidDiagram,
19
+ } from './graph-validation.js'
@@ -0,0 +1,49 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert'
3
+ import { template } from './template.js'
4
+
5
+ describe('template', () => {
6
+ test('should parse a simple template with one placeholder', () => {
7
+ const result = template('Hello $0!', [{ $ref: 'greeting' }])
8
+ assert.deepStrictEqual(result.$template.parts, ['Hello ', '!'])
9
+ assert.deepStrictEqual(result.$template.expressions, [{ $ref: 'greeting' }])
10
+ })
11
+
12
+ test('should parse template with multiple placeholders', () => {
13
+ const result = template('$0 and $1', [{ $ref: 'a' }, { $ref: 'b' }])
14
+ assert.deepStrictEqual(result.$template.parts, ['', ' and ', ''])
15
+ assert.strictEqual(result.$template.expressions.length, 2)
16
+ assert.strictEqual(result.$template.expressions[0].$ref, 'a')
17
+ assert.strictEqual(result.$template.expressions[1].$ref, 'b')
18
+ })
19
+
20
+ test('should preserve path in references', () => {
21
+ const result = template('Value: $0', [{ $ref: 'node1', path: 'data.name' }])
22
+ assert.deepStrictEqual(result.$template.expressions[0], {
23
+ $ref: 'node1',
24
+ path: 'data.name',
25
+ })
26
+ })
27
+
28
+ test('should handle template with no placeholders', () => {
29
+ const result = template('No placeholders here', [])
30
+ assert.deepStrictEqual(result.$template.parts, ['No placeholders here'])
31
+ assert.deepStrictEqual(result.$template.expressions, [])
32
+ })
33
+
34
+ test('should handle out-of-range ref index as unknown', () => {
35
+ const result = template('$5', [{ $ref: 'a' }])
36
+ assert.deepStrictEqual(result.$template.expressions[0], { $ref: 'unknown' })
37
+ })
38
+
39
+ test('should handle adjacent placeholders', () => {
40
+ const result = template('$0$1', [{ $ref: 'a' }, { $ref: 'b' }])
41
+ assert.deepStrictEqual(result.$template.parts, ['', '', ''])
42
+ assert.strictEqual(result.$template.expressions.length, 2)
43
+ })
44
+
45
+ test('should omit path when not provided', () => {
46
+ const result = template('$0', [{ $ref: 'node' }])
47
+ assert.strictEqual(result.$template.expressions[0].path, undefined)
48
+ })
49
+ })
@@ -0,0 +1,42 @@
1
+ export type TemplateString = {
2
+ $template: {
3
+ parts: string[]
4
+ expressions: Array<{ $ref: string; path?: string }>
5
+ }
6
+ } & { __brand: 'TemplateString' }
7
+
8
+ export function template(
9
+ templateStr: string,
10
+ refs: Array<{ $ref: string; path?: string }>
11
+ ): TemplateString {
12
+ const parts: string[] = []
13
+ const expressions: Array<{ $ref: string; path?: string }> = []
14
+
15
+ const regex = /\$(\d+)/g
16
+ let lastIndex = 0
17
+ let match
18
+
19
+ while ((match = regex.exec(templateStr)) !== null) {
20
+ parts.push(templateStr.slice(lastIndex, match.index))
21
+
22
+ const refIndex = parseInt(match[1]!, 10)
23
+ const refValue = refs[refIndex]
24
+ if (refValue) {
25
+ const expr: { $ref: string; path?: string } = { $ref: refValue.$ref }
26
+ if (refValue.path) {
27
+ expr.path = refValue.path
28
+ }
29
+ expressions.push(expr)
30
+ } else {
31
+ expressions.push({ $ref: 'unknown' })
32
+ }
33
+
34
+ lastIndex = regex.lastIndex
35
+ }
36
+
37
+ parts.push(templateStr.slice(lastIndex))
38
+
39
+ return {
40
+ $template: { parts, expressions },
41
+ } as TemplateString
42
+ }
@@ -0,0 +1,29 @@
1
+ export interface PikkuWorkflowGraphConfig<
2
+ FuncMap extends Record<string, string>,
3
+ T,
4
+ > {
5
+ disabled?: true
6
+ name?: string
7
+ description?: string
8
+ tags?: string[]
9
+ nodes: FuncMap
10
+ config?: T
11
+ }
12
+
13
+ export interface PikkuWorkflowGraphResult {
14
+ __type: 'pikkuWorkflowGraph'
15
+ name?: string
16
+ description?: string
17
+ tags?: string[]
18
+ }
19
+
20
+ export function pikkuWorkflowGraph<
21
+ const FuncMap extends Record<string, string>,
22
+ >(config: PikkuWorkflowGraphConfig<FuncMap, any>): PikkuWorkflowGraphResult {
23
+ return {
24
+ __type: 'pikkuWorkflowGraph',
25
+ name: config.name,
26
+ description: config.description,
27
+ tags: config.tags,
28
+ }
29
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Ref value - internal representation after input callback is evaluated
3
+ */
4
+ export interface RefValue {
5
+ __isRef: true
6
+ nodeId: string
7
+ path?: string
8
+ }
9
+
10
+ /**
11
+ * Helper to create a ref value
12
+ */
13
+ export const createRef = (nodeId: string, path?: string): RefValue => ({
14
+ __isRef: true,
15
+ nodeId,
16
+ path,
17
+ })
18
+
19
+ /**
20
+ * Check if a value is a ref
21
+ */
22
+ export const isRef = (value: unknown): value is RefValue =>
23
+ typeof value === 'object' &&
24
+ value !== null &&
25
+ '__isRef' in value &&
26
+ (value as RefValue).__isRef === true
27
+
28
+ /**
29
+ * Input reference for fetching step results
30
+ */
31
+ export interface InputRef {
32
+ nodeId: string
33
+ path?: string
34
+ }
35
+
36
+ /**
37
+ * Ref function type - provides type-safe references to other nodes' outputs
38
+ * The actual typing is done at the pikkuWorkflowGraph level
39
+ */
40
+ export type RefFn<NodeIds extends string = string> = (
41
+ nodeId: NodeIds,
42
+ path?: string
43
+ ) => RefValue
44
+
45
+ /**
46
+ * Next node configuration - fully serializable
47
+ * - string: single next node
48
+ * - string[]: parallel execution (all run concurrently)
49
+ * - Record<string, string | string[]>: branching (function calls graph.branch('key'))
50
+ */
51
+ export type NextConfig<NodeIds extends string = string> =
52
+ | NodeIds
53
+ | NodeIds[]
54
+ | Record<string, NodeIds | NodeIds[]>
55
+
56
+ /**
57
+ * Graph node configuration - references functions by RPC name
58
+ */
59
+ export interface GraphNodeConfig<NodeIds extends string = string> {
60
+ /** RPC function name */
61
+ func: string
62
+ /** Input mapping callback - receives ref function, returns input object */
63
+ input?: (ref: RefFn<NodeIds>) => Record<string, unknown>
64
+ /** Next nodes - string, array, or record for branching */
65
+ next?: NextConfig<NodeIds>
66
+ /** Error routing - node(s) to execute on error */
67
+ onError?: NodeIds | NodeIds[]
68
+ }
69
+
70
+ /**
71
+ * Graph wire context - available to functions running in a workflow graph
72
+ */
73
+ export interface PikkuGraphWire {
74
+ /** Workflow run ID */
75
+ runId: string
76
+ /** Graph name */
77
+ graphName: string
78
+ /** Current node ID */
79
+ nodeId: string
80
+ /**
81
+ * Select which branch to take for Record-based next config.
82
+ * Must be called if the node has a Record `next` configuration.
83
+ * @param key - The branch key to take (must match a key in the next Record)
84
+ */
85
+ branch: (key: string) => void
86
+ /**
87
+ * Set a state variable value.
88
+ * @param name - Variable name
89
+ * @param value - Value to store
90
+ */
91
+ setState: (name: string, value: unknown) => Promise<void>
92
+ /**
93
+ * Get the entire state object.
94
+ * @returns The state object with all variables
95
+ */
96
+ getState: () => Promise<Record<string, unknown>>
97
+ }
98
+
99
+ /**
100
+ * Internal mutable state for graph wire (used by runner to capture branch)
101
+ */
102
+ export interface GraphWireState {
103
+ branchKey?: string
104
+ }