@pikku/core 0.12.1 → 0.12.3

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 (165) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/crypto-utils.d.ts +10 -0
  3. package/dist/crypto-utils.js +62 -0
  4. package/dist/function/function-runner.js +4 -4
  5. package/dist/function/functions.types.d.ts +26 -1
  6. package/dist/function/functions.types.js +16 -0
  7. package/dist/handle-error.d.ts +1 -1
  8. package/dist/handle-error.js +7 -3
  9. package/dist/index.d.ts +14 -2
  10. package/dist/index.js +1 -1
  11. package/dist/middleware-runner.js +1 -0
  12. package/dist/permissions.js +1 -0
  13. package/dist/pikku-state.js +7 -1
  14. package/dist/schema.js +1 -1
  15. package/dist/services/gateway-service.d.ts +19 -0
  16. package/dist/services/gateway-service.js +1 -0
  17. package/dist/services/in-memory-workflow-service.d.ts +8 -2
  18. package/dist/services/in-memory-workflow-service.js +28 -1
  19. package/dist/services/index.d.ts +2 -0
  20. package/dist/services/index.js +1 -0
  21. package/dist/services/local-gateway-service.d.ts +22 -0
  22. package/dist/services/local-gateway-service.js +51 -0
  23. package/dist/services/workflow-service.d.ts +8 -2
  24. package/dist/testing/index.d.ts +2 -0
  25. package/dist/testing/index.js +1 -0
  26. package/dist/testing/service-tests.d.ts +31 -0
  27. package/dist/testing/service-tests.js +598 -0
  28. package/dist/types/core.types.d.ts +12 -2
  29. package/dist/types/state.types.d.ts +9 -2
  30. package/dist/utils/hash.d.ts +2 -0
  31. package/dist/utils/hash.js +23 -0
  32. package/dist/utils.js +1 -1
  33. package/dist/wirings/ai-agent/agent-dynamic-workflow.d.ts +6 -0
  34. package/dist/wirings/ai-agent/agent-dynamic-workflow.js +348 -0
  35. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +1 -1
  36. package/dist/wirings/ai-agent/ai-agent-memory.js +19 -7
  37. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +10 -7
  38. package/dist/wirings/ai-agent/ai-agent-prepare.js +141 -48
  39. package/dist/wirings/ai-agent/ai-agent-runner.d.ts +4 -0
  40. package/dist/wirings/ai-agent/ai-agent-runner.js +381 -27
  41. package/dist/wirings/ai-agent/ai-agent-stream.d.ts +7 -2
  42. package/dist/wirings/ai-agent/ai-agent-stream.js +126 -48
  43. package/dist/wirings/ai-agent/ai-agent.types.d.ts +57 -3
  44. package/dist/wirings/ai-agent/index.d.ts +2 -3
  45. package/dist/wirings/ai-agent/index.js +1 -2
  46. package/dist/wirings/channel/channel-handler.d.ts +5 -2
  47. package/dist/wirings/channel/channel-handler.js +16 -2
  48. package/dist/wirings/channel/channel-middleware-runner.js +1 -0
  49. package/dist/wirings/channel/channel.types.d.ts +7 -7
  50. package/dist/wirings/channel/index.d.ts +1 -1
  51. package/dist/wirings/channel/local/local-channel-handler.d.ts +8 -1
  52. package/dist/wirings/channel/local/local-channel-handler.js +22 -3
  53. package/dist/wirings/channel/local/local-channel-runner.js +28 -5
  54. package/dist/wirings/channel/pikku-abstract-channel-handler.d.ts +2 -1
  55. package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
  56. package/dist/wirings/channel/serverless/serverless-channel-runner.js +5 -2
  57. package/dist/wirings/cli/cli-runner.js +13 -2
  58. package/dist/wirings/cli/cli.types.d.ts +1 -0
  59. package/dist/wirings/gateway/gateway-runner.d.ts +24 -0
  60. package/dist/wirings/gateway/gateway-runner.js +325 -0
  61. package/dist/wirings/gateway/gateway.types.d.ts +127 -0
  62. package/dist/wirings/gateway/gateway.types.js +1 -0
  63. package/dist/wirings/gateway/index.d.ts +2 -0
  64. package/dist/wirings/gateway/index.js +1 -0
  65. package/dist/wirings/http/http-routes.js +0 -1
  66. package/dist/wirings/http/http-runner.d.ts +1 -1
  67. package/dist/wirings/http/http-runner.js +28 -3
  68. package/dist/wirings/http/http.types.d.ts +3 -11
  69. package/dist/wirings/http/index.d.ts +2 -1
  70. package/dist/wirings/http/index.js +1 -0
  71. package/dist/wirings/http/pikku-fetch-http-request.d.ts +1 -0
  72. package/dist/wirings/http/pikku-fetch-http-request.js +3 -0
  73. package/dist/wirings/http/pikku-fetch-http-response.d.ts +1 -0
  74. package/dist/wirings/http/pikku-fetch-http-response.js +4 -4
  75. package/dist/wirings/http/web-request.d.ts +12 -0
  76. package/dist/wirings/http/web-request.js +85 -0
  77. package/dist/wirings/mcp/mcp-endpoint-registry.js +1 -1
  78. package/dist/wirings/mcp/mcp-runner.js +13 -1
  79. package/dist/wirings/rpc/rpc-runner.d.ts +30 -6
  80. package/dist/wirings/rpc/rpc-runner.js +36 -14
  81. package/dist/wirings/rpc/rpc-types.d.ts +6 -1
  82. package/dist/wirings/rpc/wire-addon.d.ts +1 -0
  83. package/dist/wirings/rpc/wire-addon.js +1 -1
  84. package/dist/wirings/scheduler/scheduler-runner.js +1 -0
  85. package/dist/wirings/workflow/graph/graph-validation.d.ts +3 -0
  86. package/dist/wirings/workflow/graph/graph-validation.js +175 -0
  87. package/dist/wirings/workflow/graph/index.d.ts +1 -0
  88. package/dist/wirings/workflow/graph/index.js +1 -0
  89. package/dist/wirings/workflow/index.d.ts +1 -1
  90. package/dist/wirings/workflow/pikku-workflow-service.d.ts +8 -2
  91. package/dist/wirings/workflow/pikku-workflow-service.js +1 -2
  92. package/dist/wirings/workflow/workflow.types.d.ts +10 -1
  93. package/package.json +6 -3
  94. package/src/crypto-utils.test.ts +99 -1
  95. package/src/crypto-utils.ts +114 -0
  96. package/src/function/function-runner.ts +5 -5
  97. package/src/function/functions.types.ts +38 -1
  98. package/src/handle-error.test.ts +65 -2
  99. package/src/handle-error.ts +8 -4
  100. package/src/index.ts +14 -0
  101. package/src/middleware-runner.ts +1 -0
  102. package/src/permissions.ts +1 -0
  103. package/src/pikku-state.ts +8 -1
  104. package/src/schema.ts +1 -1
  105. package/src/services/gateway-service.ts +20 -0
  106. package/src/services/in-memory-workflow-service.ts +45 -2
  107. package/src/services/index.ts +2 -0
  108. package/src/services/local-gateway-service.ts +62 -0
  109. package/src/services/workflow-service.ts +12 -1
  110. package/src/testing/index.ts +2 -0
  111. package/src/testing/service-tests.ts +873 -0
  112. package/src/types/core.types.ts +12 -1
  113. package/src/types/state.types.ts +12 -2
  114. package/src/utils/hash.test.ts +68 -0
  115. package/src/utils/hash.ts +26 -0
  116. package/src/utils.ts +1 -1
  117. package/src/wirings/ai-agent/agent-dynamic-workflow.ts +469 -0
  118. package/src/wirings/ai-agent/ai-agent-memory.ts +17 -7
  119. package/src/wirings/ai-agent/ai-agent-prepare.ts +194 -62
  120. package/src/wirings/ai-agent/ai-agent-runner.test.ts +3 -1
  121. package/src/wirings/ai-agent/ai-agent-runner.ts +553 -31
  122. package/src/wirings/ai-agent/ai-agent-stream.test.ts +209 -2
  123. package/src/wirings/ai-agent/ai-agent-stream.ts +195 -91
  124. package/src/wirings/ai-agent/ai-agent.types.ts +57 -3
  125. package/src/wirings/ai-agent/index.ts +3 -5
  126. package/src/wirings/channel/channel-handler.ts +41 -4
  127. package/src/wirings/channel/channel-middleware-runner.ts +1 -0
  128. package/src/wirings/channel/channel.types.ts +12 -7
  129. package/src/wirings/channel/index.ts +1 -0
  130. package/src/wirings/channel/local/local-channel-handler.ts +31 -3
  131. package/src/wirings/channel/local/local-channel-runner.ts +32 -8
  132. package/src/wirings/channel/pikku-abstract-channel-handler.test.ts +2 -0
  133. package/src/wirings/channel/pikku-abstract-channel-handler.ts +8 -1
  134. package/src/wirings/channel/serverless/serverless-channel-runner.ts +5 -2
  135. package/src/wirings/cli/cli-runner.ts +15 -2
  136. package/src/wirings/cli/cli.types.ts +1 -0
  137. package/src/wirings/gateway/gateway-runner.test.ts +474 -0
  138. package/src/wirings/gateway/gateway-runner.ts +411 -0
  139. package/src/wirings/gateway/gateway.types.ts +149 -0
  140. package/src/wirings/gateway/index.ts +13 -0
  141. package/src/wirings/http/http-routes.ts +0 -1
  142. package/src/wirings/http/http-runner.ts +38 -10
  143. package/src/wirings/http/http.types.ts +3 -12
  144. package/src/wirings/http/index.ts +3 -0
  145. package/src/wirings/http/pikku-fetch-http-request.ts +4 -0
  146. package/src/wirings/http/pikku-fetch-http-response.ts +4 -4
  147. package/src/wirings/http/web-request.test.ts +236 -0
  148. package/src/wirings/http/web-request.ts +104 -0
  149. package/src/wirings/mcp/mcp-endpoint-registry.ts +1 -1
  150. package/src/wirings/mcp/mcp-runner.ts +30 -12
  151. package/src/wirings/rpc/rpc-runner.ts +45 -24
  152. package/src/wirings/rpc/rpc-types.ts +6 -1
  153. package/src/wirings/rpc/wire-addon.ts +2 -1
  154. package/src/wirings/scheduler/scheduler-runner.ts +1 -0
  155. package/src/wirings/workflow/graph/graph-validation.test.ts +170 -0
  156. package/src/wirings/workflow/graph/graph-validation.ts +237 -0
  157. package/src/wirings/workflow/graph/index.ts +5 -0
  158. package/src/wirings/workflow/index.ts +1 -0
  159. package/src/wirings/workflow/pikku-workflow-service.ts +14 -3
  160. package/src/wirings/workflow/workflow.types.ts +9 -1
  161. package/tsconfig.tsbuildinfo +1 -1
  162. package/dist/wirings/ai-agent/ai-agent-assistant-ui.d.ts +0 -5
  163. package/dist/wirings/ai-agent/ai-agent-assistant-ui.js +0 -146
  164. package/src/wirings/ai-agent/ai-agent-assistant-ui.test.ts +0 -363
  165. package/src/wirings/ai-agent/ai-agent-assistant-ui.ts +0 -214
@@ -1,6 +1,5 @@
1
1
  import type { SerializeOptions } from 'cookie'
2
2
  import type { StandardSchemaV1 } from '@standard-schema/spec'
3
- import type { PikkuError } from '../../errors/error-handler.js'
4
3
  import type {
5
4
  CorePikkuMiddleware,
6
5
  CommonWireMeta,
@@ -36,6 +35,7 @@ export type RunHTTPWiringOptions = Partial<{
36
35
  logWarningsForStatusCodes: number[]
37
36
  coerceDataFromSchema: boolean
38
37
  bubbleErrors: boolean
38
+ exposeErrors: boolean
39
39
  generateRequestId: () => string
40
40
  }>
41
41
 
@@ -59,16 +59,6 @@ export type HTTPHeadersSchema = StandardSchemaV1<
59
59
  Record<string, string | string[] | undefined>
60
60
  >
61
61
 
62
- /**
63
- * Documentation config for OpenAPI generation
64
- */
65
- export type HTTPRouteDocsConfig = {
66
- description?: string
67
- response?: string
68
- errors?: Array<typeof PikkuError>
69
- tags?: string[]
70
- }
71
-
72
62
  /**
73
63
  * Common HTTP route configuration shared between wireHTTP and wireHTTPRoutes
74
64
  */
@@ -77,7 +67,6 @@ export type HTTPRouteBaseConfig = {
77
67
  timeout?: number
78
68
  tags?: string[]
79
69
  headers?: HTTPHeadersSchema
80
- docs?: HTTPRouteDocsConfig
81
70
  }
82
71
 
83
72
  /**
@@ -271,6 +260,7 @@ export interface PikkuHTTPRequest<In = unknown> {
271
260
  data(): Promise<In>
272
261
  json(): Promise<unknown>
273
262
  arrayBuffer(): Promise<ArrayBuffer>
263
+ headers(): Record<string, string>
274
264
  header(headerName: string): string | null
275
265
  cookie(name?: string): string | null
276
266
  params(): Partial<Record<string, string | string[]>>
@@ -293,6 +283,7 @@ export interface PikkuHTTPResponse<Out = unknown> {
293
283
  | ReadableStream
294
284
  ): this
295
285
  json(data: Out): this
286
+ send?(data: string | ArrayBuffer | Buffer): this
296
287
  redirect(location: string, status?: number): this
297
288
  close?: () => void
298
289
  setMode?: (mode: 'stream') => void
@@ -11,12 +11,15 @@ export {
11
11
  } from './http-runner.js'
12
12
 
13
13
  export { wireHTTPRoutes, defineHTTPRoutes } from './http-routes.js'
14
+ export { toWebRequest, applyWebResponse } from './web-request.js'
14
15
 
15
16
  export type {
16
17
  AssertHTTPWiringParams,
17
18
  CoreHTTPFunctionWiring,
18
19
  HTTPMethod,
19
20
  HTTPRouteBaseConfig,
21
+ HTTPRouteContract,
22
+ HTTPRouteMap,
20
23
  HTTPWiringsMeta,
21
24
  PikkuHTTPRequest,
22
25
  PikkuHTTPResponse,
@@ -43,6 +43,10 @@ export class PikkuFetchHTTPRequest<In = unknown>
43
43
  return this.request.arrayBuffer()
44
44
  }
45
45
 
46
+ public headers(): Record<string, string> {
47
+ return Object.fromEntries(this.request.headers.entries())
48
+ }
49
+
46
50
  /**
47
51
  * Retrieves the value of a specific header.
48
52
  * @param headerName - The name of the header to retrieve.
@@ -83,10 +83,10 @@ export class PikkuFetchHTTPResponse implements PikkuHTTPResponse {
83
83
  return this
84
84
  }
85
85
 
86
- // public body(body: BodyInit): this {
87
- // this.#body = body
88
- // return this
89
- // }
86
+ public send(data: any): this {
87
+ this.#body = data
88
+ return this
89
+ }
90
90
 
91
91
  public redirect(location: string, status: number = 302): this {
92
92
  if (
@@ -0,0 +1,236 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { toWebRequest, applyWebResponse } from './web-request.js'
4
+ import type {
5
+ PikkuHTTPRequest,
6
+ PikkuHTTPResponse,
7
+ PikkuQuery,
8
+ } from './http.types.js'
9
+
10
+ const createMockRequest = (
11
+ overrides: Partial<{
12
+ method: string
13
+ path: string
14
+ headers: Record<string, string>
15
+ query: Record<string, string>
16
+ body: ArrayBuffer
17
+ }> = {}
18
+ ): PikkuHTTPRequest => ({
19
+ method: () => (overrides.method ?? 'get') as any,
20
+ path: () => overrides.path ?? '/test',
21
+ headers: () => overrides.headers ?? {},
22
+ header: (name: string) =>
23
+ (overrides.headers ?? {})[name.toLowerCase()] ?? null,
24
+ cookie: () => null,
25
+ params: () => ({}),
26
+ setParams: () => {},
27
+ query: () => (overrides.query ?? {}) as PikkuQuery,
28
+ json: async () => ({}),
29
+ arrayBuffer: async () => overrides.body ?? new ArrayBuffer(0),
30
+ data: async () => ({}) as any,
31
+ })
32
+
33
+ const createMockResponse = () => {
34
+ const state = {
35
+ statusCode: 0,
36
+ headers: {} as Record<string, string | string[]>,
37
+ body: null as string | null,
38
+ redirectUrl: null as string | null,
39
+ redirectStatus: null as number | null,
40
+ }
41
+
42
+ const res: PikkuHTTPResponse = {
43
+ status(code: number) {
44
+ state.statusCode = code
45
+ return res
46
+ },
47
+ header(name: string, value: string | string[]) {
48
+ state.headers[name.toLowerCase()] = value
49
+ return res
50
+ },
51
+ cookie() {
52
+ return res
53
+ },
54
+ json() {
55
+ return res
56
+ },
57
+ arrayBuffer(data: any) {
58
+ state.body = typeof data === 'string' ? data : null
59
+ return res
60
+ },
61
+ redirect(location: string, status?: number) {
62
+ state.redirectUrl = location
63
+ state.redirectStatus = status ?? 302
64
+ return res
65
+ },
66
+ }
67
+
68
+ return { res, state }
69
+ }
70
+
71
+ describe('toWebRequest', () => {
72
+ test('converts a GET request with correct URL and method', async () => {
73
+ const req = createMockRequest({
74
+ method: 'get',
75
+ path: '/api/test',
76
+ headers: { host: 'example.com' },
77
+ })
78
+
79
+ const webReq = toWebRequest(req)
80
+
81
+ assert.equal(webReq.method, 'GET')
82
+ assert.equal(new URL(webReq.url).pathname, '/api/test')
83
+ assert.equal(new URL(webReq.url).host, 'example.com')
84
+ assert.equal(webReq.body, null)
85
+ })
86
+
87
+ test('copies query parameters', async () => {
88
+ const req = createMockRequest({
89
+ query: { search: 'hello', page: '2' },
90
+ })
91
+
92
+ const webReq = toWebRequest(req)
93
+ const url = new URL(webReq.url)
94
+
95
+ assert.equal(url.searchParams.get('search'), 'hello')
96
+ assert.equal(url.searchParams.get('page'), '2')
97
+ })
98
+
99
+ test('copies all headers', async () => {
100
+ const req = createMockRequest({
101
+ headers: {
102
+ 'content-type': 'application/json',
103
+ authorization: 'Bearer abc123',
104
+ host: 'localhost',
105
+ },
106
+ })
107
+
108
+ const webReq = toWebRequest(req)
109
+
110
+ assert.equal(webReq.headers.get('content-type'), 'application/json')
111
+ assert.equal(webReq.headers.get('authorization'), 'Bearer abc123')
112
+ })
113
+
114
+ test('uses custom baseUrl when provided', async () => {
115
+ const req = createMockRequest({ path: '/callback' })
116
+
117
+ const webReq = toWebRequest(req, 'https://myapp.com')
118
+ const url = new URL(webReq.url)
119
+
120
+ assert.equal(url.origin, 'https://myapp.com')
121
+ assert.equal(url.pathname, '/callback')
122
+ })
123
+
124
+ test('falls back to localhost when no host header', async () => {
125
+ const req = createMockRequest({ path: '/test' })
126
+
127
+ const webReq = toWebRequest(req)
128
+ const url = new URL(webReq.url)
129
+
130
+ assert.equal(url.host, 'localhost')
131
+ })
132
+
133
+ test('POST request includes streaming body', async () => {
134
+ const bodyContent = JSON.stringify({
135
+ username: 'admin',
136
+ password: 'secret',
137
+ })
138
+ const encoder = new TextEncoder()
139
+ const bodyBuffer = encoder.encode(bodyContent).buffer as ArrayBuffer
140
+
141
+ const req = createMockRequest({
142
+ method: 'post',
143
+ path: '/signin',
144
+ headers: { 'content-type': 'application/json', host: 'localhost' },
145
+ body: bodyBuffer,
146
+ })
147
+
148
+ const webReq = toWebRequest(req)
149
+
150
+ assert.equal(webReq.method, 'POST')
151
+ assert.notEqual(webReq.body, null)
152
+
153
+ const text = await webReq.text()
154
+ assert.equal(text, bodyContent)
155
+ })
156
+
157
+ test('GET request has no body', async () => {
158
+ const req = createMockRequest({ method: 'get' })
159
+ const webReq = toWebRequest(req)
160
+ assert.equal(webReq.body, null)
161
+ })
162
+
163
+ test('HEAD request has no body', async () => {
164
+ const req = createMockRequest({ method: 'head' })
165
+ const webReq = toWebRequest(req)
166
+ assert.equal(webReq.body, null)
167
+ })
168
+
169
+ test('OPTIONS request has no body', async () => {
170
+ const req = createMockRequest({ method: 'options' })
171
+ const webReq = toWebRequest(req)
172
+ assert.equal(webReq.body, null)
173
+ })
174
+ })
175
+
176
+ describe('applyWebResponse', () => {
177
+ test('copies status code', async () => {
178
+ const { res, state } = createMockResponse()
179
+ const webRes = new Response('OK', { status: 201 })
180
+
181
+ await applyWebResponse(res, webRes)
182
+
183
+ assert.equal(state.statusCode, 201)
184
+ })
185
+
186
+ test('copies response headers', async () => {
187
+ const { res, state } = createMockResponse()
188
+ const webRes = new Response('', {
189
+ status: 200,
190
+ headers: {
191
+ 'content-type': 'text/html',
192
+ 'x-custom': 'value',
193
+ },
194
+ })
195
+
196
+ await applyWebResponse(res, webRes)
197
+
198
+ assert.equal(state.headers['content-type'], 'text/html')
199
+ assert.equal(state.headers['x-custom'], 'value')
200
+ })
201
+
202
+ test('handles redirect via location header', async () => {
203
+ const { res, state } = createMockResponse()
204
+ const webRes = new Response(null, {
205
+ status: 302,
206
+ headers: { location: 'https://github.com/login/oauth/authorize' },
207
+ })
208
+
209
+ await applyWebResponse(res, webRes)
210
+
211
+ assert.equal(state.redirectUrl, 'https://github.com/login/oauth/authorize')
212
+ assert.equal(state.statusCode, 302)
213
+ })
214
+
215
+ test('writes text body', async () => {
216
+ const { res, state } = createMockResponse()
217
+ const webRes = new Response('{"session": true}', {
218
+ status: 200,
219
+ headers: { 'content-type': 'application/json' },
220
+ })
221
+
222
+ await applyWebResponse(res, webRes)
223
+
224
+ assert.equal(state.body, '{"session": true}')
225
+ })
226
+
227
+ test('does not write empty body', async () => {
228
+ const { res, state } = createMockResponse()
229
+ const webRes = new Response(null, { status: 204 })
230
+
231
+ await applyWebResponse(res, webRes)
232
+
233
+ assert.equal(state.body, null)
234
+ assert.equal(state.statusCode, 204)
235
+ })
236
+ })
@@ -0,0 +1,104 @@
1
+ import type { PikkuHTTPRequest } from './http.types.js'
2
+ import type { PikkuHTTPResponse } from './http.types.js'
3
+
4
+ /**
5
+ * Converts a PikkuHTTPRequest into a Web API Request.
6
+ * Useful for bridging Pikku routes to libraries that expect standard Web Request objects.
7
+ */
8
+ export function toWebRequest(req: PikkuHTTPRequest, baseUrl?: string): Request {
9
+ const url = new URL(
10
+ req.path(),
11
+ baseUrl ?? `http://${req.header('host') ?? 'localhost'}`
12
+ )
13
+
14
+ const query = req.query()
15
+ for (const [key, value] of Object.entries(query)) {
16
+ if (value != null) {
17
+ url.searchParams.set(key, String(value))
18
+ }
19
+ }
20
+
21
+ const headers = new Headers(req.headers())
22
+
23
+ const method = req.method().toUpperCase()
24
+ const hasBody = !['GET', 'HEAD', 'OPTIONS'].includes(method)
25
+
26
+ if (!hasBody) {
27
+ return new Request(url, { method, headers })
28
+ }
29
+
30
+ return new Request(url, {
31
+ method,
32
+ headers,
33
+ body: new ReadableStream({
34
+ async start(controller) {
35
+ try {
36
+ const buffer = await req.arrayBuffer()
37
+ if (buffer.byteLength > 0) {
38
+ controller.enqueue(new Uint8Array(buffer))
39
+ } else {
40
+ // arrayBuffer may be empty if body was pre-parsed (e.g., Express middleware)
41
+ const contentType = (
42
+ headers.get('content-type') || ''
43
+ ).toLowerCase()
44
+ const parsed = await req.json()
45
+ if (
46
+ parsed &&
47
+ typeof parsed === 'object' &&
48
+ Object.keys(parsed as any).length > 0
49
+ ) {
50
+ let reconstructed: string
51
+ if (contentType.includes('application/x-www-form-urlencoded')) {
52
+ reconstructed = new URLSearchParams(
53
+ parsed as Record<string, string>
54
+ ).toString()
55
+ } else {
56
+ reconstructed = JSON.stringify(parsed)
57
+ }
58
+ controller.enqueue(new TextEncoder().encode(reconstructed))
59
+ }
60
+ }
61
+ } catch {
62
+ // Empty body
63
+ }
64
+ controller.close()
65
+ },
66
+ }),
67
+ // @ts-ignore - duplex is needed for streaming body in Node.js
68
+ duplex: 'half',
69
+ })
70
+ }
71
+
72
+ const SKIP_RESPONSE_HEADERS = new Set(['content-length', 'transfer-encoding'])
73
+
74
+ /**
75
+ * Applies a Web API Response to a PikkuHTTPResponse.
76
+ * Copies status, headers (including Set-Cookie), redirects, and body.
77
+ */
78
+ export async function applyWebResponse(
79
+ res: PikkuHTTPResponse,
80
+ webResponse: Response
81
+ ): Promise<void> {
82
+ res.status(webResponse.status)
83
+
84
+ webResponse.headers.forEach((value, name) => {
85
+ const lower = name.toLowerCase()
86
+ if (SKIP_RESPONSE_HEADERS.has(lower)) {
87
+ return
88
+ }
89
+ if (lower === 'location') {
90
+ res.redirect(value, webResponse.status)
91
+ } else {
92
+ res.header(name, value)
93
+ }
94
+ })
95
+
96
+ const body = await webResponse.text()
97
+ if (body) {
98
+ if (res.send) {
99
+ res.send(body)
100
+ } else {
101
+ res.arrayBuffer(body)
102
+ }
103
+ }
104
+ }
@@ -44,7 +44,7 @@ export class MCPEndpointRegistry {
44
44
  name: tool.name,
45
45
  title: tool.title,
46
46
  description: tool.description,
47
- inputSchema: tool.parameters,
47
+ inputSchema: tool.parameters ?? { type: 'object' },
48
48
  outputSchema: tool.returns,
49
49
  enabled: tool.enabled !== undefined ? tool.enabled : true,
50
50
  })
@@ -19,6 +19,7 @@ import {
19
19
  getCreateWireServices,
20
20
  } from '../../pikku-state.js'
21
21
  import { addFunction, runPikkuFunc } from '../../function/function-runner.js'
22
+ import { resolveNamespace } from '../rpc/rpc-runner.js'
22
23
  import {
23
24
  BadRequestError,
24
25
  NotFoundError,
@@ -231,18 +232,35 @@ async function runMCPPikkuFunc(
231
232
  meta = pikkuState(null, 'mcp', 'promptsMeta')[name]
232
233
  }
233
234
 
234
- let result = await runPikkuFunc('mcp', `${type}:${name}`, pikkuFuncId, {
235
- singletonServices,
236
- createWireServices,
237
- data: () => request.params,
238
- inheritedMiddleware: meta?.middleware,
239
- wireMiddleware: mcp?.middleware,
240
- inheritedPermissions: meta?.permissions,
241
- wirePermissions: mcp?.permissions,
242
- tags: mcp?.tags,
243
- wire,
244
- sessionService: mcpSessionService,
245
- })
235
+ // Resolve namespaced function IDs (e.g., 'swaggerPetstore:findPetsByStatus')
236
+ let resolvedFuncName = pikkuFuncId
237
+ let resolvedPackageName: string | null = meta?.packageName ?? null
238
+ if (pikkuFuncId.includes(':')) {
239
+ const resolved = resolveNamespace(pikkuFuncId)
240
+ if (resolved) {
241
+ resolvedFuncName = resolved.function
242
+ resolvedPackageName = resolved.package
243
+ }
244
+ }
245
+
246
+ let result = await runPikkuFunc(
247
+ 'mcp',
248
+ `${type}:${name}`,
249
+ resolvedFuncName,
250
+ {
251
+ singletonServices,
252
+ createWireServices,
253
+ data: () => request.params,
254
+ inheritedMiddleware: meta?.middleware,
255
+ wireMiddleware: mcp?.middleware,
256
+ inheritedPermissions: meta?.permissions,
257
+ wirePermissions: mcp?.permissions,
258
+ tags: mcp?.tags,
259
+ wire,
260
+ sessionService: mcpSessionService,
261
+ packageName: resolvedPackageName,
262
+ }
263
+ )
246
264
 
247
265
  if (type === 'tool' && meta?.outputSchema !== 'MCPToolResponse') {
248
266
  result = [{ type: 'text', text: JSON.stringify(result) }]
@@ -4,7 +4,7 @@ import type { CoreUserSession } from '../../types/core.types.js'
4
4
  import { runPikkuFunc } from '../../function/function-runner.js'
5
5
  import { pikkuState } from '../../pikku-state.js'
6
6
  import { ForbiddenError } from '../../errors/errors.js'
7
- import { PikkuError } from '../../errors/error-handler.js'
7
+ import { PikkuError, addError } from '../../errors/error-handler.js'
8
8
  import type { PikkuRPC, ResolvedFunction } from './rpc-types.js'
9
9
  import { parseVersionedId } from '../../version.js'
10
10
  import { encryptJSON } from '../../crypto-utils.js'
@@ -16,18 +16,22 @@ export class RPCNotFoundError extends PikkuError {
16
16
  this.rpcName = rpcName
17
17
  }
18
18
  }
19
+ addError(RPCNotFoundError, {
20
+ status: 404,
21
+ mcpCode: -32601,
22
+ message: 'RPC function not found.',
23
+ })
19
24
  import type { AIAgentInput } from '../ai-agent/ai-agent.types.js'
20
25
  import type { AIStreamChannel } from '../ai-agent/ai-agent.types.js'
21
26
  import type { StreamAIAgentOptions } from '../ai-agent/ai-agent-prepare.js'
22
- import { runAIAgent } from '../ai-agent/ai-agent-runner.js'
27
+ import { runAIAgent, resumeAIAgentSync } from '../ai-agent/ai-agent-runner.js'
23
28
  import { streamAIAgent, resumeAIAgent } from '../ai-agent/ai-agent-stream.js'
24
- import { approveAIAgent } from '../ai-agent/ai-agent-registry.js'
25
29
 
26
30
  /**
27
31
  * Resolve a namespaced function reference to package and function names
28
32
  * Uses pikkuState to look up the namespace -> package mapping
29
33
  */
30
- const resolveNamespace = (
34
+ export const resolveNamespace = (
31
35
  namespacedFunction: string
32
36
  ): ResolvedFunction | null => {
33
37
  const colonIndex = namespacedFunction.indexOf(':')
@@ -38,16 +42,16 @@ const resolveNamespace = (
38
42
  const namespace = namespacedFunction.substring(0, colonIndex)
39
43
  const functionName = namespacedFunction.substring(colonIndex + 1)
40
44
 
41
- const addons = pikkuState(null, 'rpc', 'addons')
45
+ const addons = pikkuState(null, 'addons', 'packages')
42
46
  const pkgConfig = addons.get(namespace)
43
47
  if (!pkgConfig) {
44
48
  return null
45
49
  }
46
50
 
47
51
  return {
48
- namespace,
49
52
  package: pkgConfig.package,
50
53
  function: functionName,
54
+ addonConfig: pkgConfig,
51
55
  }
52
56
  }
53
57
 
@@ -90,7 +94,7 @@ export class ContextAwareRPCService {
90
94
  functionMeta = pikkuState(null, 'function', 'meta')[funcName]
91
95
  }
92
96
  if (!functionMeta) {
93
- throw new Error(`Function not found: ${funcName}`)
97
+ throw new RPCNotFoundError(funcName)
94
98
  }
95
99
  if (!functionMeta.expose) {
96
100
  throw new ForbiddenError()
@@ -165,11 +169,11 @@ export class ContextAwareRPCService {
165
169
  }
166
170
  const funcName = funcMeta.pikkuFuncId || resolved.function
167
171
 
168
- const addonConfig = pikkuState(null, 'rpc', 'addons').get(
169
- resolved.namespace
170
- )
171
- const auth = addonConfig?.auth ?? this.options.requiresAuth
172
- const tags = [...(addonConfig?.tags ?? []), ...(funcMeta.tags ?? [])]
172
+ const auth = resolved.addonConfig?.auth ?? this.options.requiresAuth
173
+ const tags = [
174
+ ...(resolved.addonConfig?.tags ?? []),
175
+ ...(funcMeta.tags ?? []),
176
+ ]
173
177
 
174
178
  // Execute the function using runPikkuFunc with the addon package's state
175
179
  // We use the parent services (this.services) since addon packages share services
@@ -201,6 +205,11 @@ export class ContextAwareRPCService {
201
205
  }
202
206
  : undefined,
203
207
  }
208
+
209
+ if (rpcName.includes(':')) {
210
+ return this.invokeAddonFunction<In, Out>(rpcName, data, mergedWire)
211
+ }
212
+
204
213
  return runPikkuFunc<In, Out>(
205
214
  'rpc',
206
215
  rpcName,
@@ -250,13 +259,21 @@ export class ContextAwareRPCService {
250
259
  runId: result.runId,
251
260
  result: result.object ?? result.text,
252
261
  usage: result.usage,
262
+ ...(result.status === 'suspended' && {
263
+ status: 'suspended' as const,
264
+ pendingApprovals: result.pendingApprovals,
265
+ }),
253
266
  }
254
267
  },
255
268
  stream: async (
256
269
  agentName: string,
257
- input:
258
- | { message: string; threadId: string; resourceId: string }
259
- | Record<string, unknown>,
270
+ input: {
271
+ message: string
272
+ threadId: string
273
+ resourceId: string
274
+ model?: string
275
+ temperature?: number
276
+ },
260
277
  options?: StreamAIAgentOptions
261
278
  ) => {
262
279
  const channel = this.wire.channel as unknown as AIStreamChannel
@@ -289,14 +306,18 @@ export class ContextAwareRPCService {
289
306
  approvals: { toolCallId: string; approved: boolean }[],
290
307
  expectedAgentName?: string
291
308
  ) => {
292
- if (!this.services.aiRunState)
293
- throw new Error('AIRunStateService not available')
294
- return approveAIAgent(
295
- this.services.aiRunState,
296
- runId,
297
- approvals,
298
- expectedAgentName
299
- )
309
+ const result = await resumeAIAgentSync(runId, approvals, {
310
+ sessionService: this.options.sessionService,
311
+ }, expectedAgentName)
312
+ return {
313
+ runId: result.runId,
314
+ result: result.object ?? result.text,
315
+ usage: result.usage,
316
+ ...(result.status === 'suspended' && {
317
+ status: 'suspended' as const,
318
+ pendingApprovals: result.pendingApprovals,
319
+ }),
320
+ }
300
321
  },
301
322
  }
302
323
  }
@@ -313,7 +334,7 @@ export class ContextAwareRPCService {
313
334
  const colonIndex = funcName.indexOf(':')
314
335
  if (colonIndex !== -1) {
315
336
  const namespace = funcName.substring(0, colonIndex)
316
- const addons = pikkuState(null, 'rpc', 'addons')
337
+ const addons = pikkuState(null, 'addons', 'packages')
317
338
  const pkgConfig = addons.get(namespace)
318
339
  endpoint = pkgConfig?.rpcEndpoint
319
340
  }
@@ -45,7 +45,12 @@ export interface RPCHandler<Input, Output> {
45
45
  * Resolved function reference from namespace
46
46
  */
47
47
  export interface ResolvedFunction {
48
- namespace: string
49
48
  package: string
50
49
  function: string
50
+ addonConfig: {
51
+ package: string
52
+ auth?: boolean
53
+ tags?: string[]
54
+ rpcEndpoint?: string
55
+ }
51
56
  }
@@ -5,13 +5,14 @@ export type WireAddonConfig = {
5
5
  package: string
6
6
  rpcEndpoint?: string
7
7
  auth?: boolean
8
+ mcp?: boolean
8
9
  tags?: string[]
9
10
  secretOverrides?: Record<string, string>
10
11
  variableOverrides?: Record<string, string>
11
12
  }
12
13
 
13
14
  export const wireAddon = (config: WireAddonConfig): void => {
14
- pikkuState(null, 'rpc', 'addons').set(config.name, {
15
+ pikkuState(null, 'addons', 'packages').set(config.name, {
15
16
  package: config.package,
16
17
  rpcEndpoint: config.rpcEndpoint,
17
18
  auth: config.auth,