@pikku/core 0.12.2 → 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.
- package/CHANGELOG.md +18 -0
- package/dist/crypto-utils.d.ts +10 -0
- package/dist/crypto-utils.js +62 -0
- package/dist/function/function-runner.js +4 -4
- package/dist/function/functions.types.d.ts +26 -1
- package/dist/function/functions.types.js +16 -0
- package/dist/handle-error.d.ts +1 -1
- package/dist/handle-error.js +7 -3
- package/dist/index.d.ts +14 -2
- package/dist/index.js +1 -1
- package/dist/pikku-state.js +3 -1
- package/dist/schema.js +1 -1
- package/dist/services/in-memory-workflow-service.d.ts +8 -2
- package/dist/services/in-memory-workflow-service.js +28 -1
- package/dist/services/workflow-service.d.ts +8 -2
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +1 -0
- package/dist/testing/service-tests.d.ts +31 -0
- package/dist/testing/service-tests.js +598 -0
- package/dist/types/core.types.d.ts +9 -1
- package/dist/types/state.types.d.ts +4 -2
- package/dist/utils/hash.d.ts +2 -0
- package/dist/utils/hash.js +23 -0
- package/dist/wirings/ai-agent/agent-dynamic-workflow.d.ts +6 -0
- package/dist/wirings/ai-agent/agent-dynamic-workflow.js +348 -0
- package/dist/wirings/ai-agent/ai-agent-memory.d.ts +1 -1
- package/dist/wirings/ai-agent/ai-agent-memory.js +6 -5
- package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +10 -7
- package/dist/wirings/ai-agent/ai-agent-prepare.js +127 -47
- package/dist/wirings/ai-agent/ai-agent-runner.d.ts +4 -0
- package/dist/wirings/ai-agent/ai-agent-runner.js +381 -27
- package/dist/wirings/ai-agent/ai-agent-stream.d.ts +7 -2
- package/dist/wirings/ai-agent/ai-agent-stream.js +109 -46
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +22 -1
- package/dist/wirings/ai-agent/index.d.ts +1 -2
- package/dist/wirings/ai-agent/index.js +1 -2
- package/dist/wirings/channel/channel-handler.js +5 -1
- package/dist/wirings/channel/channel.types.d.ts +1 -7
- package/dist/wirings/channel/local/local-channel-handler.d.ts +1 -1
- package/dist/wirings/channel/local/local-channel-handler.js +5 -3
- package/dist/wirings/channel/local/local-channel-runner.js +14 -4
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +4 -1
- package/dist/wirings/cli/cli-runner.js +10 -2
- package/dist/wirings/cli/cli.types.d.ts +1 -0
- package/dist/wirings/http/http-routes.js +0 -1
- package/dist/wirings/http/http-runner.d.ts +1 -1
- package/dist/wirings/http/http-runner.js +25 -3
- package/dist/wirings/http/http.types.d.ts +3 -11
- package/dist/wirings/http/index.d.ts +2 -1
- package/dist/wirings/http/index.js +1 -0
- package/dist/wirings/http/pikku-fetch-http-request.d.ts +1 -0
- package/dist/wirings/http/pikku-fetch-http-request.js +3 -0
- package/dist/wirings/http/pikku-fetch-http-response.d.ts +1 -0
- package/dist/wirings/http/pikku-fetch-http-response.js +4 -4
- package/dist/wirings/http/web-request.d.ts +12 -0
- package/dist/wirings/http/web-request.js +85 -0
- package/dist/wirings/mcp/mcp-endpoint-registry.js +1 -1
- package/dist/wirings/mcp/mcp-runner.js +13 -1
- package/dist/wirings/rpc/rpc-runner.d.ts +30 -6
- package/dist/wirings/rpc/rpc-runner.js +36 -14
- package/dist/wirings/rpc/rpc-types.d.ts +6 -1
- package/dist/wirings/rpc/wire-addon.d.ts +1 -0
- package/dist/wirings/rpc/wire-addon.js +1 -1
- package/dist/wirings/scheduler/scheduler-runner.js +1 -0
- package/dist/wirings/workflow/graph/graph-validation.d.ts +3 -0
- package/dist/wirings/workflow/graph/graph-validation.js +175 -0
- package/dist/wirings/workflow/graph/index.d.ts +1 -0
- package/dist/wirings/workflow/graph/index.js +1 -0
- package/dist/wirings/workflow/index.d.ts +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +8 -2
- package/dist/wirings/workflow/pikku-workflow-service.js +1 -2
- package/dist/wirings/workflow/workflow.types.d.ts +10 -1
- package/package.json +4 -2
- package/src/crypto-utils.test.ts +99 -1
- package/src/crypto-utils.ts +114 -0
- package/src/function/function-runner.ts +5 -5
- package/src/function/functions.types.ts +38 -1
- package/src/handle-error.test.ts +65 -2
- package/src/handle-error.ts +8 -4
- package/src/index.ts +14 -0
- package/src/pikku-state.ts +3 -1
- package/src/schema.ts +1 -1
- package/src/services/in-memory-workflow-service.ts +45 -2
- package/src/services/workflow-service.ts +12 -1
- package/src/testing/index.ts +2 -0
- package/src/testing/service-tests.ts +873 -0
- package/src/types/core.types.ts +9 -1
- package/src/types/state.types.ts +4 -2
- package/src/utils/hash.test.ts +68 -0
- package/src/utils/hash.ts +26 -0
- package/src/wirings/ai-agent/agent-dynamic-workflow.ts +469 -0
- package/src/wirings/ai-agent/ai-agent-memory.ts +7 -6
- package/src/wirings/ai-agent/ai-agent-prepare.ts +175 -61
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +3 -1
- package/src/wirings/ai-agent/ai-agent-runner.ts +553 -31
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +209 -2
- package/src/wirings/ai-agent/ai-agent-stream.ts +182 -89
- package/src/wirings/ai-agent/ai-agent.types.ts +23 -1
- package/src/wirings/ai-agent/index.ts +1 -5
- package/src/wirings/channel/channel-handler.ts +11 -2
- package/src/wirings/channel/channel.types.ts +1 -7
- package/src/wirings/channel/local/local-channel-handler.ts +5 -3
- package/src/wirings/channel/local/local-channel-runner.ts +17 -7
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +4 -1
- package/src/wirings/cli/cli-runner.ts +12 -2
- package/src/wirings/cli/cli.types.ts +1 -0
- package/src/wirings/http/http-routes.ts +0 -1
- package/src/wirings/http/http-runner.ts +35 -10
- package/src/wirings/http/http.types.ts +3 -12
- package/src/wirings/http/index.ts +3 -0
- package/src/wirings/http/pikku-fetch-http-request.ts +4 -0
- package/src/wirings/http/pikku-fetch-http-response.ts +4 -4
- package/src/wirings/http/web-request.test.ts +236 -0
- package/src/wirings/http/web-request.ts +104 -0
- package/src/wirings/mcp/mcp-endpoint-registry.ts +1 -1
- package/src/wirings/mcp/mcp-runner.ts +30 -12
- package/src/wirings/rpc/rpc-runner.ts +45 -24
- package/src/wirings/rpc/rpc-types.ts +6 -1
- package/src/wirings/rpc/wire-addon.ts +2 -1
- package/src/wirings/scheduler/scheduler-runner.ts +1 -0
- package/src/wirings/workflow/graph/graph-validation.test.ts +170 -0
- package/src/wirings/workflow/graph/graph-validation.ts +237 -0
- package/src/wirings/workflow/graph/index.ts +5 -0
- package/src/wirings/workflow/index.ts +1 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +14 -3
- package/src/wirings/workflow/workflow.types.ts +9 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/wirings/ai-agent/ai-agent-assistant-ui.d.ts +0 -5
- package/dist/wirings/ai-agent/ai-agent-assistant-ui.js +0 -168
- package/src/wirings/ai-agent/ai-agent-assistant-ui.test.ts +0 -363
- package/src/wirings/ai-agent/ai-agent-assistant-ui.ts +0 -238
|
@@ -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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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, '
|
|
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
|
|
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
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
259
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
runId,
|
|
297
|
-
|
|
298
|
-
|
|
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, '
|
|
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, '
|
|
15
|
+
pikkuState(null, 'addons', 'packages').set(config.name, {
|
|
15
16
|
package: config.package,
|
|
16
17
|
rpcEndpoint: config.rpcEndpoint,
|
|
17
18
|
auth: config.auth,
|