@nextclaw/ncp-http-agent-server 0.3.12 → 0.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +43 -40
- package/dist/index.js +297 -383
- package/package.json +3 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Hono } from
|
|
1
|
+
import { NcpAgentClientEndpoint, NcpEndpointEvent, NcpStreamRequestPayload } from "@nextclaw/ncp";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
3
|
|
|
4
|
+
//#region src/types.d.ts
|
|
4
5
|
/**
|
|
5
6
|
* Streams live session events for `/stream`.
|
|
6
7
|
*
|
|
@@ -18,57 +19,59 @@ import { Hono } from 'hono';
|
|
|
18
19
|
* for that active session.
|
|
19
20
|
*/
|
|
20
21
|
type NcpHttpAgentStreamProvider = {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
stream(params: {
|
|
23
|
+
payload: NcpStreamRequestPayload;
|
|
24
|
+
signal: AbortSignal;
|
|
25
|
+
}): AsyncIterable<NcpEndpointEvent>;
|
|
25
26
|
};
|
|
26
27
|
type NcpHttpAgentServerOptions = {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
streamProvider?: NcpHttpAgentStreamProvider;
|
|
28
|
+
/** Client endpoint to forward requests to (in-process adapter or remote HTTP client). */agentClientEndpoint: NcpAgentClientEndpoint;
|
|
29
|
+
basePath?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Optional forward-stream timeout in milliseconds.
|
|
32
|
+
* When omitted or set to a non-positive value, no server-side timeout is applied.
|
|
33
|
+
*/
|
|
34
|
+
requestTimeoutMs?: number | null;
|
|
35
|
+
/**
|
|
36
|
+
* Optional. When set, `/stream` serves live session events from streamProvider instead of
|
|
37
|
+
* forwarding to the agent.
|
|
38
|
+
* When not set, forwards `message.stream-request` to the agent.
|
|
39
|
+
*/
|
|
40
|
+
streamProvider?: NcpHttpAgentStreamProvider;
|
|
41
41
|
};
|
|
42
|
-
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/handler-interface.d.ts
|
|
43
44
|
/** Framework-agnostic HTTP handler interface for NCP agent routes. */
|
|
44
45
|
interface NcpHttpAgentHandler {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
handleSend(request: Request): Promise<Response>;
|
|
47
|
+
handleStream(request: Request): Promise<Response>;
|
|
48
|
+
handleAbort(request: Request): Promise<Response>;
|
|
48
49
|
}
|
|
49
50
|
type NcpHttpAgentHandlerOptions = {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
agentClientEndpoint: NcpAgentClientEndpoint;
|
|
52
|
+
/**
|
|
53
|
+
* Optional. When set, `/stream` serves stored events instead of forwarding to the agent.
|
|
54
|
+
* See NcpHttpAgentStreamProvider for details.
|
|
55
|
+
*/
|
|
56
|
+
streamProvider?: NcpHttpAgentStreamProvider;
|
|
57
|
+
timeoutMs: number | null;
|
|
57
58
|
};
|
|
58
|
-
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/controller.d.ts
|
|
59
61
|
/**
|
|
60
62
|
* Framework-agnostic controller for NCP agent HTTP routes.
|
|
61
63
|
* Forwards /send and /stream to agentClientEndpoint; /stream uses streamProvider when set.
|
|
62
64
|
*/
|
|
63
65
|
declare class NcpHttpAgentController implements NcpHttpAgentHandler {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
private readonly options;
|
|
67
|
+
constructor(options: NcpHttpAgentHandlerOptions);
|
|
68
|
+
handleSend(request: Request): Promise<Response>;
|
|
69
|
+
handleStream(request: Request): Promise<Response>;
|
|
70
|
+
handleAbort(request: Request): Promise<Response>;
|
|
69
71
|
}
|
|
70
|
-
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/router.d.ts
|
|
71
74
|
declare function createNcpHttpAgentRouter(options: NcpHttpAgentServerOptions): Hono;
|
|
72
75
|
declare function mountNcpHttpAgentRoutes(app: Hono, options: NcpHttpAgentServerOptions): void;
|
|
73
|
-
|
|
74
|
-
export { NcpHttpAgentController, type NcpHttpAgentHandler, type NcpHttpAgentHandlerOptions, type NcpHttpAgentServerOptions, type NcpHttpAgentStreamProvider, createNcpHttpAgentRouter, mountNcpHttpAgentRoutes };
|
|
76
|
+
//#endregion
|
|
77
|
+
export { NcpHttpAgentController, type NcpHttpAgentHandler, type NcpHttpAgentHandlerOptions, type NcpHttpAgentServerOptions, type NcpHttpAgentStreamProvider, createNcpHttpAgentRouter, mountNcpHttpAgentRoutes };
|
package/dist/index.js
CHANGED
|
@@ -1,436 +1,350 @@
|
|
|
1
|
-
// src/controller.ts
|
|
2
1
|
import { NcpEventType } from "@nextclaw/ncp";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
//#region src/types.ts
|
|
4
|
+
const DEFAULT_BASE_PATH = "/ncp/agent";
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region src/parsers.ts
|
|
8
7
|
function normalizeBasePath(basePath) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const withLeadingSlash = raw.startsWith("/") ? raw : `/${raw}`;
|
|
14
|
-
return withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
|
|
8
|
+
const raw = (basePath ?? "/ncp/agent").trim();
|
|
9
|
+
if (!raw) return DEFAULT_BASE_PATH;
|
|
10
|
+
const withLeadingSlash = raw.startsWith("/") ? raw : `/${raw}`;
|
|
11
|
+
return withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
|
|
15
12
|
}
|
|
16
13
|
function sanitizeTimeout(timeoutMs) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (timeoutMs <= 0) {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
return Math.max(1e3, Math.trunc(timeoutMs));
|
|
14
|
+
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs)) return null;
|
|
15
|
+
if (timeoutMs <= 0) return null;
|
|
16
|
+
return Math.max(1e3, Math.trunc(timeoutMs));
|
|
24
17
|
}
|
|
25
18
|
async function parseRequestEnvelope(request) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
return payload;
|
|
38
|
-
} catch {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
19
|
+
try {
|
|
20
|
+
const payload = await request.json();
|
|
21
|
+
if (!isRecord$1(payload)) return null;
|
|
22
|
+
if (typeof payload.sessionId !== "string" || !payload.sessionId.trim()) return null;
|
|
23
|
+
if (!isRecord$1(payload.message)) return null;
|
|
24
|
+
return payload;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
41
28
|
}
|
|
42
29
|
function parseStreamPayloadFromUrl(url) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
return {
|
|
49
|
-
sessionId
|
|
50
|
-
};
|
|
30
|
+
const sessionId = new URL(url).searchParams.get("sessionId")?.trim();
|
|
31
|
+
if (!sessionId) return null;
|
|
32
|
+
return { sessionId };
|
|
51
33
|
}
|
|
52
34
|
async function parseAbortPayload(request) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
};
|
|
67
|
-
} catch {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
35
|
+
try {
|
|
36
|
+
const payload = await request.json();
|
|
37
|
+
if (!isRecord$1(payload)) return null;
|
|
38
|
+
const sessionId = readTrimmedString(payload.sessionId);
|
|
39
|
+
if (!sessionId) return null;
|
|
40
|
+
const messageId = readTrimmedString(payload.messageId);
|
|
41
|
+
return {
|
|
42
|
+
sessionId,
|
|
43
|
+
...messageId ? { messageId } : {}
|
|
44
|
+
};
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
70
48
|
}
|
|
71
49
|
function readTrimmedString(value) {
|
|
72
|
-
|
|
50
|
+
return typeof value === "string" ? value.trim() : "";
|
|
73
51
|
}
|
|
74
|
-
function isRecord(value) {
|
|
75
|
-
|
|
52
|
+
function isRecord$1(value) {
|
|
53
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
76
54
|
}
|
|
77
|
-
|
|
78
|
-
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/scope.ts
|
|
79
57
|
function extractScopeFromEvent(event) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
runId: readString(payload.runId)
|
|
88
|
-
};
|
|
58
|
+
const payload = readPayload(event);
|
|
59
|
+
if (!payload) return {};
|
|
60
|
+
return {
|
|
61
|
+
sessionId: readString(payload.sessionId),
|
|
62
|
+
correlationId: readString(payload.correlationId),
|
|
63
|
+
runId: readString(payload.runId)
|
|
64
|
+
};
|
|
89
65
|
}
|
|
90
66
|
function matchesScope(scope, event) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
if (scope.runId && eventScope.runId && eventScope.runId !== scope.runId) {
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
return true;
|
|
67
|
+
const eventScope = extractScopeFromEvent(event);
|
|
68
|
+
if (eventScope.sessionId && eventScope.sessionId !== scope.sessionId) return false;
|
|
69
|
+
if (scope.correlationId && eventScope.correlationId && eventScope.correlationId !== scope.correlationId) return false;
|
|
70
|
+
if (scope.runId && eventScope.runId && eventScope.runId !== scope.runId) return false;
|
|
71
|
+
return true;
|
|
102
72
|
}
|
|
103
73
|
function readPayload(event) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
return isRecord2(event.payload) ? event.payload : null;
|
|
74
|
+
if (!("payload" in event)) return null;
|
|
75
|
+
return isRecord(event.payload) ? event.payload : null;
|
|
108
76
|
}
|
|
109
77
|
function readString(value) {
|
|
110
|
-
|
|
78
|
+
return typeof value === "string" && value ? value : void 0;
|
|
111
79
|
}
|
|
112
|
-
function
|
|
113
|
-
|
|
80
|
+
function isRecord(value) {
|
|
81
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
114
82
|
}
|
|
115
|
-
|
|
116
|
-
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/sse-stream.ts
|
|
117
85
|
function createSseEventStream(events, signal) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
} finally {
|
|
141
|
-
signal?.removeEventListener("abort", onAbort);
|
|
142
|
-
close();
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
});
|
|
86
|
+
const encoder = new TextEncoder();
|
|
87
|
+
return new ReadableStream({ start: async (controller) => {
|
|
88
|
+
let closed = false;
|
|
89
|
+
const close = () => {
|
|
90
|
+
if (closed) return;
|
|
91
|
+
closed = true;
|
|
92
|
+
controller.close();
|
|
93
|
+
};
|
|
94
|
+
const onAbort = () => {
|
|
95
|
+
close();
|
|
96
|
+
};
|
|
97
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
98
|
+
try {
|
|
99
|
+
for await (const event of events) {
|
|
100
|
+
if (closed || signal?.aborted) break;
|
|
101
|
+
controller.enqueue(encoder.encode(toSseFrame(event.event, event.data)));
|
|
102
|
+
}
|
|
103
|
+
} finally {
|
|
104
|
+
signal?.removeEventListener("abort", onAbort);
|
|
105
|
+
close();
|
|
106
|
+
}
|
|
107
|
+
} });
|
|
146
108
|
}
|
|
147
109
|
function buildSseResponse(stream) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
110
|
+
return new Response(stream, {
|
|
111
|
+
status: 200,
|
|
112
|
+
headers: {
|
|
113
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
114
|
+
"Cache-Control": "no-cache, no-transform",
|
|
115
|
+
Connection: "keep-alive",
|
|
116
|
+
"X-Accel-Buffering": "no"
|
|
117
|
+
}
|
|
118
|
+
});
|
|
157
119
|
}
|
|
158
120
|
function toSseFrame(eventName, data) {
|
|
159
|
-
|
|
160
|
-
data: ${JSON.stringify(data)}
|
|
161
|
-
|
|
162
|
-
`;
|
|
121
|
+
return `event: ${eventName}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
163
122
|
}
|
|
164
123
|
function toNcpEventFrame(event) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
124
|
+
return {
|
|
125
|
+
event: "ncp-event",
|
|
126
|
+
data: event
|
|
127
|
+
};
|
|
169
128
|
}
|
|
170
129
|
function toErrorFrame(code, message) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
130
|
+
return {
|
|
131
|
+
event: "error",
|
|
132
|
+
data: {
|
|
133
|
+
code,
|
|
134
|
+
message
|
|
135
|
+
}
|
|
136
|
+
};
|
|
178
137
|
}
|
|
179
|
-
|
|
180
|
-
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/async-queue.ts
|
|
181
140
|
function createAsyncQueue() {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const resolve = pendingResolve;
|
|
228
|
-
pendingResolve = null;
|
|
229
|
-
resolve({
|
|
230
|
-
value: void 0,
|
|
231
|
-
done: true
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
},
|
|
235
|
-
iterable
|
|
236
|
-
};
|
|
141
|
+
const items = [];
|
|
142
|
+
let closed = false;
|
|
143
|
+
let pendingResolve = null;
|
|
144
|
+
return {
|
|
145
|
+
push(item) {
|
|
146
|
+
if (closed) return;
|
|
147
|
+
if (pendingResolve) {
|
|
148
|
+
const resolve = pendingResolve;
|
|
149
|
+
pendingResolve = null;
|
|
150
|
+
resolve({
|
|
151
|
+
value: item,
|
|
152
|
+
done: false
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
items.push(item);
|
|
157
|
+
},
|
|
158
|
+
close() {
|
|
159
|
+
if (closed) return;
|
|
160
|
+
closed = true;
|
|
161
|
+
if (pendingResolve) {
|
|
162
|
+
const resolve = pendingResolve;
|
|
163
|
+
pendingResolve = null;
|
|
164
|
+
resolve({
|
|
165
|
+
value: void 0,
|
|
166
|
+
done: true
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
iterable: { [Symbol.asyncIterator]() {
|
|
171
|
+
return { next: () => {
|
|
172
|
+
if (items.length > 0) return Promise.resolve({
|
|
173
|
+
value: items.shift(),
|
|
174
|
+
done: false
|
|
175
|
+
});
|
|
176
|
+
if (closed) return Promise.resolve({
|
|
177
|
+
value: void 0,
|
|
178
|
+
done: true
|
|
179
|
+
});
|
|
180
|
+
return new Promise((resolve) => {
|
|
181
|
+
pendingResolve = resolve;
|
|
182
|
+
});
|
|
183
|
+
} };
|
|
184
|
+
} }
|
|
185
|
+
};
|
|
237
186
|
}
|
|
238
|
-
|
|
239
|
-
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region src/utils.ts
|
|
240
189
|
function errorMessage(error) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
return String(error ?? "Unknown error");
|
|
190
|
+
if (error instanceof Error && error.message) return error.message;
|
|
191
|
+
return String(error ?? "Unknown error");
|
|
245
192
|
}
|
|
246
193
|
function jsonResponse(body, status = 200) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
194
|
+
return new Response(JSON.stringify(body), {
|
|
195
|
+
status,
|
|
196
|
+
headers: { "Content-Type": "application/json" }
|
|
197
|
+
});
|
|
251
198
|
}
|
|
252
|
-
|
|
253
|
-
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/stream-handlers.ts
|
|
254
201
|
function createForwardResponse(options) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
createSseEventStream(createForwardSseEvents(options), requestSignal)
|
|
258
|
-
);
|
|
202
|
+
const { requestSignal } = options;
|
|
203
|
+
return buildSseResponse(createSseEventStream(createForwardSseEvents(options), requestSignal));
|
|
259
204
|
}
|
|
260
205
|
async function* createForwardSseEvents(options) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
push(toNcpEventFrame(event));
|
|
304
|
-
});
|
|
305
|
-
void endpoint.emit(requestEvent).catch((error) => {
|
|
306
|
-
push(toErrorFrame("EMIT_FAILED", errorMessage(error)));
|
|
307
|
-
stop();
|
|
308
|
-
}).finally(() => {
|
|
309
|
-
if (!requestSignal.aborted) {
|
|
310
|
-
queueMicrotask(stop);
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
try {
|
|
314
|
-
for await (const frame of queue.iterable) {
|
|
315
|
-
yield frame;
|
|
316
|
-
}
|
|
317
|
-
} finally {
|
|
318
|
-
stop();
|
|
319
|
-
}
|
|
206
|
+
const { endpoint, requestEvent, requestSignal, timeoutMs, scope } = options;
|
|
207
|
+
const queue = createAsyncQueue();
|
|
208
|
+
let timeoutId = null;
|
|
209
|
+
let unsubscribe = null;
|
|
210
|
+
let stopped = false;
|
|
211
|
+
const push = (frame) => {
|
|
212
|
+
if (!stopped) queue.push(frame);
|
|
213
|
+
};
|
|
214
|
+
const stop = () => {
|
|
215
|
+
if (stopped) return;
|
|
216
|
+
stopped = true;
|
|
217
|
+
if (timeoutId) {
|
|
218
|
+
clearTimeout(timeoutId);
|
|
219
|
+
timeoutId = null;
|
|
220
|
+
}
|
|
221
|
+
if (unsubscribe) {
|
|
222
|
+
unsubscribe();
|
|
223
|
+
unsubscribe = null;
|
|
224
|
+
}
|
|
225
|
+
requestSignal.removeEventListener("abort", stop);
|
|
226
|
+
queue.close();
|
|
227
|
+
};
|
|
228
|
+
requestSignal.addEventListener("abort", stop, { once: true });
|
|
229
|
+
if (timeoutMs !== null) timeoutId = setTimeout(() => {
|
|
230
|
+
push(toErrorFrame("TIMEOUT", "NCP HTTP stream timed out before terminal event."));
|
|
231
|
+
stop();
|
|
232
|
+
}, timeoutMs);
|
|
233
|
+
unsubscribe = endpoint.subscribe((event) => {
|
|
234
|
+
if (!matchesScope(scope, event)) return;
|
|
235
|
+
push(toNcpEventFrame(event));
|
|
236
|
+
});
|
|
237
|
+
endpoint.emit(requestEvent).catch((error) => {
|
|
238
|
+
push(toErrorFrame("EMIT_FAILED", errorMessage(error)));
|
|
239
|
+
stop();
|
|
240
|
+
}).finally(() => {
|
|
241
|
+
if (!requestSignal.aborted) queueMicrotask(stop);
|
|
242
|
+
});
|
|
243
|
+
try {
|
|
244
|
+
for await (const frame of queue.iterable) yield frame;
|
|
245
|
+
} finally {
|
|
246
|
+
stop();
|
|
247
|
+
}
|
|
320
248
|
}
|
|
321
249
|
function createLiveStreamResponse(options) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
createSseEventStream(createLiveStreamSseEvents(options), signal)
|
|
325
|
-
);
|
|
250
|
+
const { signal } = options;
|
|
251
|
+
return buildSseResponse(createSseEventStream(createLiveStreamSseEvents(options), signal));
|
|
326
252
|
}
|
|
327
253
|
async function* createLiveStreamSseEvents(options) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
254
|
+
const { streamProvider, payload, signal } = options;
|
|
255
|
+
try {
|
|
256
|
+
for await (const event of streamProvider.stream({
|
|
257
|
+
payload,
|
|
258
|
+
signal
|
|
259
|
+
})) {
|
|
260
|
+
if (signal.aborted) break;
|
|
261
|
+
yield toNcpEventFrame(event);
|
|
262
|
+
}
|
|
263
|
+
} catch (error) {
|
|
264
|
+
yield toErrorFrame("STREAM_SOURCE_FAILED", errorMessage(error));
|
|
265
|
+
}
|
|
339
266
|
}
|
|
340
|
-
|
|
341
|
-
|
|
267
|
+
//#endregion
|
|
268
|
+
//#region src/controller.ts
|
|
269
|
+
/**
|
|
270
|
+
* Framework-agnostic controller for NCP agent HTTP routes.
|
|
271
|
+
* Forwards /send and /stream to agentClientEndpoint; /stream uses streamProvider when set.
|
|
272
|
+
*/
|
|
342
273
|
var NcpHttpAgentController = class {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const payload = await parseAbortPayload(request);
|
|
399
|
-
if (!payload) {
|
|
400
|
-
return jsonResponse(
|
|
401
|
-
{
|
|
402
|
-
ok: false,
|
|
403
|
-
error: { code: "INVALID_BODY", message: "sessionId is required." }
|
|
404
|
-
},
|
|
405
|
-
400
|
|
406
|
-
);
|
|
407
|
-
}
|
|
408
|
-
await agentClientEndpoint.abort(payload);
|
|
409
|
-
return jsonResponse({ ok: true });
|
|
410
|
-
}
|
|
274
|
+
constructor(options) {
|
|
275
|
+
this.options = options;
|
|
276
|
+
}
|
|
277
|
+
async handleSend(request) {
|
|
278
|
+
const { agentClientEndpoint } = this.options;
|
|
279
|
+
const envelope = await parseRequestEnvelope(request);
|
|
280
|
+
if (!envelope) return jsonResponse({
|
|
281
|
+
ok: false,
|
|
282
|
+
error: {
|
|
283
|
+
code: "INVALID_BODY",
|
|
284
|
+
message: "Invalid NCP request envelope."
|
|
285
|
+
}
|
|
286
|
+
}, 400);
|
|
287
|
+
await agentClientEndpoint.send(envelope);
|
|
288
|
+
return jsonResponse({ ok: true });
|
|
289
|
+
}
|
|
290
|
+
async handleStream(request) {
|
|
291
|
+
const { agentClientEndpoint, streamProvider } = this.options;
|
|
292
|
+
const streamPayload = parseStreamPayloadFromUrl(request.url);
|
|
293
|
+
if (!streamPayload) return jsonResponse({
|
|
294
|
+
ok: false,
|
|
295
|
+
error: {
|
|
296
|
+
code: "INVALID_QUERY",
|
|
297
|
+
message: "sessionId is required."
|
|
298
|
+
}
|
|
299
|
+
}, 400);
|
|
300
|
+
if (streamProvider) return createLiveStreamResponse({
|
|
301
|
+
streamProvider,
|
|
302
|
+
payload: streamPayload,
|
|
303
|
+
signal: request.signal
|
|
304
|
+
});
|
|
305
|
+
return createForwardResponse({
|
|
306
|
+
endpoint: agentClientEndpoint,
|
|
307
|
+
requestEvent: {
|
|
308
|
+
type: NcpEventType.MessageStreamRequest,
|
|
309
|
+
payload: streamPayload
|
|
310
|
+
},
|
|
311
|
+
requestSignal: request.signal,
|
|
312
|
+
timeoutMs: null,
|
|
313
|
+
scope: { sessionId: streamPayload.sessionId }
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
async handleAbort(request) {
|
|
317
|
+
const { agentClientEndpoint } = this.options;
|
|
318
|
+
const payload = await parseAbortPayload(request);
|
|
319
|
+
if (!payload) return jsonResponse({
|
|
320
|
+
ok: false,
|
|
321
|
+
error: {
|
|
322
|
+
code: "INVALID_BODY",
|
|
323
|
+
message: "sessionId is required."
|
|
324
|
+
}
|
|
325
|
+
}, 400);
|
|
326
|
+
await agentClientEndpoint.abort(payload);
|
|
327
|
+
return jsonResponse({ ok: true });
|
|
328
|
+
}
|
|
411
329
|
};
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
import { Hono } from "hono";
|
|
330
|
+
//#endregion
|
|
331
|
+
//#region src/router.ts
|
|
415
332
|
function createNcpHttpAgentRouter(options) {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
333
|
+
const app = new Hono();
|
|
334
|
+
mountNcpHttpAgentRoutes(app, options);
|
|
335
|
+
return app;
|
|
419
336
|
}
|
|
420
337
|
function mountNcpHttpAgentRoutes(app, options) {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
338
|
+
const { basePath: rawBasePath, agentClientEndpoint, streamProvider, requestTimeoutMs } = options;
|
|
339
|
+
const basePath = normalizeBasePath(rawBasePath);
|
|
340
|
+
const controller = new NcpHttpAgentController({
|
|
341
|
+
agentClientEndpoint,
|
|
342
|
+
streamProvider,
|
|
343
|
+
timeoutMs: sanitizeTimeout(requestTimeoutMs)
|
|
344
|
+
});
|
|
345
|
+
app.post(`${basePath}/send`, (c) => controller.handleSend(c.req.raw));
|
|
346
|
+
app.get(`${basePath}/stream`, (c) => controller.handleStream(c.req.raw));
|
|
347
|
+
app.post(`${basePath}/abort`, (c) => controller.handleAbort(c.req.raw));
|
|
431
348
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
createNcpHttpAgentRouter,
|
|
435
|
-
mountNcpHttpAgentRoutes
|
|
436
|
-
};
|
|
349
|
+
//#endregion
|
|
350
|
+
export { NcpHttpAgentController, createNcpHttpAgentRouter, mountNcpHttpAgentRoutes };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/ncp-http-agent-server",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "HTTP/SSE server transport adapter for NCP agent endpoints.",
|
|
6
6
|
"type": "module",
|
|
@@ -16,17 +16,16 @@
|
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"hono": "^4.9.8",
|
|
19
|
-
"@nextclaw/ncp": "0.5.
|
|
19
|
+
"@nextclaw/ncp": "0.5.1"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "^20.17.6",
|
|
23
23
|
"prettier": "^3.3.3",
|
|
24
|
-
"tsup": "^8.3.5",
|
|
25
24
|
"typescript": "^5.6.3",
|
|
26
25
|
"vitest": "^4.1.2"
|
|
27
26
|
},
|
|
28
27
|
"scripts": {
|
|
29
|
-
"build": "
|
|
28
|
+
"build": "tsdown src/index.ts --dts --clean --target es2022 --no-fixedExtension",
|
|
30
29
|
"lint": "eslint .",
|
|
31
30
|
"tsc": "tsc -p tsconfig.json",
|
|
32
31
|
"test": "vitest run"
|