@nextclaw/ncp-http-agent-client 0.3.11 → 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.
Files changed (3) hide show
  1. package/dist/index.d.ts +33 -31
  2. package/dist/index.js +351 -415
  3. package/package.json +3 -4
package/dist/index.d.ts CHANGED
@@ -1,37 +1,39 @@
1
- import { NcpAgentClientEndpoint, NcpEndpointManifest, NcpEndpointEvent, NcpEndpointSubscriber, NcpRequestEnvelope, NcpStreamRequestPayload, NcpMessageAbortPayload } from '@nextclaw/ncp';
1
+ import { NcpAgentClientEndpoint, NcpEndpointEvent, NcpEndpointManifest, NcpEndpointSubscriber, NcpMessageAbortPayload, NcpRequestEnvelope, NcpStreamRequestPayload } from "@nextclaw/ncp";
2
2
 
3
+ //#region src/utils.d.ts
3
4
  type FetchLike = (input: URL | string | Request, init?: RequestInit) => Promise<Response>;
4
-
5
+ //#endregion
6
+ //#region src/client.d.ts
5
7
  type NcpHttpAgentClientOptions = {
6
- baseUrl: string;
7
- basePath?: string;
8
- endpointId?: string;
9
- headers?: Record<string, string>;
10
- fetchImpl?: FetchLike;
8
+ baseUrl: string;
9
+ basePath?: string;
10
+ endpointId?: string;
11
+ headers?: Record<string, string>;
12
+ fetchImpl?: FetchLike;
11
13
  };
12
14
  declare class NcpHttpAgentClientEndpoint implements NcpAgentClientEndpoint {
13
- readonly manifest: NcpEndpointManifest;
14
- private readonly baseUrl;
15
- private readonly basePath;
16
- private readonly fetchImpl;
17
- private readonly defaultHeaders;
18
- private readonly subscribers;
19
- private readonly activeControllers;
20
- private started;
21
- constructor(options: NcpHttpAgentClientOptions);
22
- start(): Promise<void>;
23
- stop(): Promise<void>;
24
- emit(event: NcpEndpointEvent): Promise<void>;
25
- subscribe(listener: NcpEndpointSubscriber): () => void;
26
- send(envelope: NcpRequestEnvelope): Promise<void>;
27
- stream(payload: NcpStreamRequestPayload): Promise<void>;
28
- abort(payload: NcpMessageAbortPayload): Promise<void>;
29
- private ensureStarted;
30
- private publish;
31
- private resolveUrl;
32
- private jsonRequest;
33
- private streamRequest;
34
- private handleSseFrame;
15
+ readonly manifest: NcpEndpointManifest;
16
+ private readonly baseUrl;
17
+ private readonly basePath;
18
+ private readonly fetchImpl;
19
+ private readonly defaultHeaders;
20
+ private readonly subscribers;
21
+ private readonly activeControllers;
22
+ private started;
23
+ constructor(options: NcpHttpAgentClientOptions);
24
+ start(): Promise<void>;
25
+ stop(): Promise<void>;
26
+ emit(event: NcpEndpointEvent): Promise<void>;
27
+ subscribe(listener: NcpEndpointSubscriber): () => void;
28
+ send(envelope: NcpRequestEnvelope): Promise<void>;
29
+ stream(payload: NcpStreamRequestPayload): Promise<void>;
30
+ abort(payload: NcpMessageAbortPayload): Promise<void>;
31
+ private ensureStarted;
32
+ private publish;
33
+ private resolveUrl;
34
+ private jsonRequest;
35
+ private streamRequest;
36
+ private handleSseFrame;
35
37
  }
36
-
37
- export { NcpHttpAgentClientEndpoint, type NcpHttpAgentClientOptions };
38
+ //#endregion
39
+ export { NcpHttpAgentClientEndpoint, type NcpHttpAgentClientOptions };
package/dist/index.js CHANGED
@@ -1,453 +1,389 @@
1
- // src/client.ts
2
- import {
3
- NcpEventType
4
- } from "@nextclaw/ncp";
5
-
6
- // src/sse.ts
1
+ import { NcpEventType } from "@nextclaw/ncp";
2
+ //#region src/sse.ts
7
3
  function parseSseFrame(frameText) {
8
- const lines = frameText.split(/\r?\n/);
9
- let eventName = "message";
10
- const dataLines = [];
11
- for (const rawLine of lines) {
12
- const line = rawLine.trimEnd();
13
- if (!line || line.startsWith(":")) {
14
- continue;
15
- }
16
- if (line.startsWith("event:")) {
17
- eventName = line.slice(6).trim() || "message";
18
- continue;
19
- }
20
- if (line.startsWith("data:")) {
21
- dataLines.push(line.slice(5).trimStart());
22
- }
23
- }
24
- if (dataLines.length === 0) {
25
- return null;
26
- }
27
- return {
28
- event: eventName,
29
- data: dataLines.join("\n")
30
- };
4
+ const lines = frameText.split(/\r?\n/);
5
+ let eventName = "message";
6
+ const dataLines = [];
7
+ for (const rawLine of lines) {
8
+ const line = rawLine.trimEnd();
9
+ if (!line || line.startsWith(":")) continue;
10
+ if (line.startsWith("event:")) {
11
+ eventName = line.slice(6).trim() || "message";
12
+ continue;
13
+ }
14
+ if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart());
15
+ }
16
+ if (dataLines.length === 0) return null;
17
+ return {
18
+ event: eventName,
19
+ data: dataLines.join("\n")
20
+ };
31
21
  }
32
22
  async function* consumeSseStream(stream) {
33
- const reader = stream.getReader();
34
- const decoder = new TextDecoder();
35
- let buffer = "";
36
- try {
37
- while (true) {
38
- const { value, done } = await reader.read();
39
- if (done) {
40
- break;
41
- }
42
- buffer += decoder.decode(value, { stream: true });
43
- const { frames: frames2, rest } = drainFrames(buffer);
44
- buffer = rest;
45
- for (const frame of frames2) {
46
- yield frame;
47
- }
48
- }
49
- buffer += decoder.decode();
50
- const { frames } = drainFrames(buffer, true);
51
- for (const frame of frames) {
52
- yield frame;
53
- }
54
- } finally {
55
- reader.releaseLock();
56
- }
23
+ const reader = stream.getReader();
24
+ const decoder = new TextDecoder();
25
+ let buffer = "";
26
+ try {
27
+ while (true) {
28
+ const { value, done } = await reader.read();
29
+ if (done) break;
30
+ buffer += decoder.decode(value, { stream: true });
31
+ const { frames, rest } = drainFrames(buffer);
32
+ buffer = rest;
33
+ for (const frame of frames) yield frame;
34
+ }
35
+ buffer += decoder.decode();
36
+ const { frames } = drainFrames(buffer, true);
37
+ for (const frame of frames) yield frame;
38
+ } finally {
39
+ reader.releaseLock();
40
+ }
57
41
  }
58
42
  function drainFrames(rawBuffer, flush = false) {
59
- const parts = rawBuffer.split(/\r?\n\r?\n/);
60
- const rest = parts.pop() ?? "";
61
- const frames = [];
62
- for (const part of parts) {
63
- const frame = parseSseFrame(part);
64
- if (frame) {
65
- frames.push(frame);
66
- }
67
- }
68
- if (flush && rest.trim()) {
69
- const frame = parseSseFrame(rest);
70
- if (frame) {
71
- frames.push(frame);
72
- }
73
- return { frames, rest: "" };
74
- }
75
- return { frames, rest };
43
+ const parts = rawBuffer.split(/\r?\n\r?\n/);
44
+ const rest = parts.pop() ?? "";
45
+ const frames = [];
46
+ for (const part of parts) {
47
+ const frame = parseSseFrame(part);
48
+ if (frame) frames.push(frame);
49
+ }
50
+ if (flush && rest.trim()) {
51
+ const frame = parseSseFrame(rest);
52
+ if (frame) frames.push(frame);
53
+ return {
54
+ frames,
55
+ rest: ""
56
+ };
57
+ }
58
+ return {
59
+ frames,
60
+ rest
61
+ };
76
62
  }
77
-
78
- // src/utils.ts
79
- var DEFAULT_BASE_PATH = "/ncp/agent";
80
- var DEFAULT_ENDPOINT_ID = "ncp-http-agent-client";
63
+ //#endregion
64
+ //#region src/utils.ts
65
+ const DEFAULT_BASE_PATH = "/ncp/agent";
81
66
  var NcpHttpAgentClientError = class extends Error {
82
- ncpError;
83
- alreadyPublished;
84
- constructor(ncpError, alreadyPublished = false) {
85
- super(ncpError.message);
86
- this.name = `NcpHttpAgentClientError(${ncpError.code})`;
87
- this.ncpError = ncpError;
88
- this.alreadyPublished = alreadyPublished;
89
- }
67
+ ncpError;
68
+ alreadyPublished;
69
+ constructor(ncpError, alreadyPublished = false) {
70
+ super(ncpError.message);
71
+ this.name = `NcpHttpAgentClientError(${ncpError.code})`;
72
+ this.ncpError = ncpError;
73
+ this.alreadyPublished = alreadyPublished;
74
+ }
90
75
  };
91
76
  function toBaseUrl(baseUrl) {
92
- const trimmed = baseUrl.trim();
93
- if (!trimmed) {
94
- throw new Error("NcpHttpAgentClient requires a non-empty baseUrl.");
95
- }
96
- return new URL(trimmed);
77
+ const trimmed = baseUrl.trim();
78
+ if (!trimmed) throw new Error("NcpHttpAgentClient requires a non-empty baseUrl.");
79
+ return new URL(trimmed);
97
80
  }
98
81
  function resolveFetchImpl(fetchImpl) {
99
- if (fetchImpl) {
100
- return fetchImpl;
101
- }
102
- if (typeof globalThis.fetch === "function") {
103
- return globalThis.fetch.bind(globalThis);
104
- }
105
- throw new Error("No fetch implementation found. Pass options.fetchImpl explicitly.");
82
+ if (fetchImpl) return fetchImpl;
83
+ if (typeof globalThis.fetch === "function") return globalThis.fetch.bind(globalThis);
84
+ throw new Error("No fetch implementation found. Pass options.fetchImpl explicitly.");
106
85
  }
107
86
  function normalizeBasePath(basePath) {
108
- const raw = (basePath ?? DEFAULT_BASE_PATH).trim();
109
- if (!raw) {
110
- return DEFAULT_BASE_PATH;
111
- }
112
- const withSlash = raw.startsWith("/") ? raw : `/${raw}`;
113
- return withSlash.endsWith("/") ? withSlash.slice(0, -1) : withSlash;
87
+ const raw = (basePath ?? "/ncp/agent").trim();
88
+ if (!raw) return DEFAULT_BASE_PATH;
89
+ const withSlash = raw.startsWith("/") ? raw : `/${raw}`;
90
+ return withSlash.endsWith("/") ? withSlash.slice(0, -1) : withSlash;
114
91
  }
115
92
  async function safeReadText(response) {
116
- try {
117
- const text = await response.text();
118
- return text.trim();
119
- } catch {
120
- return "";
121
- }
93
+ try {
94
+ return (await response.text()).trim();
95
+ } catch {
96
+ return "";
97
+ }
122
98
  }
123
99
  function toNcpError(error) {
124
- if (isNcpHttpAgentClientError(error)) {
125
- return error.ncpError;
126
- }
127
- if (isNcpError(error)) {
128
- return error;
129
- }
130
- const message = error instanceof Error ? error.message : String(error ?? "Unknown error");
131
- return {
132
- code: normalizeErrorCode(void 0),
133
- message,
134
- ...error instanceof Error && error.stack ? { details: { stack: error.stack } } : {}
135
- };
100
+ if (isNcpHttpAgentClientError(error)) return error.ncpError;
101
+ if (isNcpError(error)) return error;
102
+ const message = error instanceof Error ? error.message : String(error ?? "Unknown error");
103
+ return {
104
+ code: normalizeErrorCode(void 0),
105
+ message,
106
+ ...error instanceof Error && error.stack ? { details: { stack: error.stack } } : {}
107
+ };
136
108
  }
137
109
  function ncpErrorToError(error, options = {}) {
138
- return new NcpHttpAgentClientError(error, options.alreadyPublished ?? false);
110
+ return new NcpHttpAgentClientError(error, options.alreadyPublished ?? false);
139
111
  }
140
112
  function isNcpError(value) {
141
- return isRecord(value) && typeof value.code === "string" && typeof value.message === "string";
113
+ return isRecord(value) && typeof value.code === "string" && typeof value.message === "string";
142
114
  }
143
115
  function isRecord(value) {
144
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
116
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
145
117
  }
146
118
  function isNcpHttpAgentClientError(error) {
147
- return error instanceof NcpHttpAgentClientError;
119
+ return error instanceof NcpHttpAgentClientError;
148
120
  }
149
- var ERROR_CODE_MAP = {
150
- "config-error": "config-error",
151
- "auth-error": "auth-error",
152
- "runtime-error": "runtime-error",
153
- "timeout-error": "timeout-error",
154
- "abort-error": "abort-error"
121
+ const ERROR_CODE_MAP = {
122
+ "config-error": "config-error",
123
+ "auth-error": "auth-error",
124
+ "runtime-error": "runtime-error",
125
+ "timeout-error": "timeout-error",
126
+ "abort-error": "abort-error"
155
127
  };
156
128
  function normalizeErrorCode(code) {
157
- if (!code) {
158
- return "runtime-error";
159
- }
160
- const mapped = ERROR_CODE_MAP[code];
161
- if (mapped) {
162
- return mapped;
163
- }
164
- const lowered = code.toLowerCase();
165
- if (lowered.includes("timeout")) return "timeout-error";
166
- if (lowered.includes("abort") || lowered.includes("cancel")) return "abort-error";
167
- if (lowered.includes("auth")) return "auth-error";
168
- if (lowered.includes("config")) return "config-error";
169
- return "runtime-error";
129
+ if (!code) return "runtime-error";
130
+ const mapped = ERROR_CODE_MAP[code];
131
+ if (mapped) return mapped;
132
+ const lowered = code.toLowerCase();
133
+ if (lowered.includes("timeout")) return "timeout-error";
134
+ if (lowered.includes("abort") || lowered.includes("cancel")) return "abort-error";
135
+ if (lowered.includes("auth")) return "auth-error";
136
+ if (lowered.includes("config")) return "config-error";
137
+ return "runtime-error";
170
138
  }
171
-
172
- // src/parsers.ts
139
+ //#endregion
140
+ //#region src/parsers.ts
173
141
  function parseNcpEvent(rawData) {
174
- const parsed = parseJsonRecord(rawData);
175
- if (!parsed) {
176
- return null;
177
- }
178
- if (typeof parsed.type !== "string" || !parsed.type.trim()) {
179
- return null;
180
- }
181
- return parsed;
142
+ const parsed = parseJsonRecord(rawData);
143
+ if (!parsed) return null;
144
+ if (typeof parsed.type !== "string" || !parsed.type.trim()) return null;
145
+ return parsed;
182
146
  }
183
147
  function parseNcpError(rawData) {
184
- const parsed = parseJsonRecord(rawData);
185
- if (!parsed) {
186
- return {
187
- code: "runtime-error",
188
- message: rawData || "Unknown stream error."
189
- };
190
- }
191
- const inputCode = typeof parsed.code === "string" ? parsed.code : void 0;
192
- const normalizedCode = normalizeErrorCode(inputCode);
193
- const details = isRecord(parsed.details) ? parsed.details : {};
194
- if (inputCode && inputCode !== normalizedCode) {
195
- details.originalCode = inputCode;
196
- }
197
- return {
198
- code: normalizedCode,
199
- message: typeof parsed.message === "string" && parsed.message ? parsed.message : "Unknown stream error.",
200
- ...Object.keys(details).length > 0 ? { details } : {}
201
- };
148
+ const parsed = parseJsonRecord(rawData);
149
+ if (!parsed) return {
150
+ code: "runtime-error",
151
+ message: rawData || "Unknown stream error."
152
+ };
153
+ const inputCode = typeof parsed.code === "string" ? parsed.code : void 0;
154
+ const normalizedCode = normalizeErrorCode(inputCode);
155
+ const details = isRecord(parsed.details) ? parsed.details : {};
156
+ if (inputCode && inputCode !== normalizedCode) details.originalCode = inputCode;
157
+ return {
158
+ code: normalizedCode,
159
+ message: typeof parsed.message === "string" && parsed.message ? parsed.message : "Unknown stream error.",
160
+ ...Object.keys(details).length > 0 ? { details } : {}
161
+ };
202
162
  }
203
163
  function parseJsonRecord(value) {
204
- try {
205
- const parsed = JSON.parse(value);
206
- return isRecord(parsed) ? parsed : null;
207
- } catch {
208
- return null;
209
- }
164
+ try {
165
+ const parsed = JSON.parse(value);
166
+ return isRecord(parsed) ? parsed : null;
167
+ } catch {
168
+ return null;
169
+ }
210
170
  }
211
-
212
- // src/client.ts
213
- var SUPPORTED_PART_TYPES = [
214
- "text",
215
- "file",
216
- "source",
217
- "step-start",
218
- "reasoning",
219
- "tool-invocation",
220
- "card",
221
- "rich-text",
222
- "action",
223
- "extension"
171
+ //#endregion
172
+ //#region src/client.ts
173
+ const SUPPORTED_PART_TYPES = [
174
+ "text",
175
+ "file",
176
+ "source",
177
+ "step-start",
178
+ "reasoning",
179
+ "tool-invocation",
180
+ "card",
181
+ "rich-text",
182
+ "action",
183
+ "extension"
224
184
  ];
225
185
  var NcpHttpAgentClientEndpoint = class {
226
- manifest;
227
- baseUrl;
228
- basePath;
229
- fetchImpl;
230
- defaultHeaders;
231
- subscribers = /* @__PURE__ */ new Set();
232
- activeControllers = /* @__PURE__ */ new Set();
233
- started = false;
234
- constructor(options) {
235
- this.baseUrl = toBaseUrl(options.baseUrl);
236
- this.basePath = normalizeBasePath(options.basePath);
237
- this.fetchImpl = resolveFetchImpl(options.fetchImpl);
238
- this.defaultHeaders = options.headers ?? {};
239
- this.manifest = {
240
- endpointKind: "custom",
241
- endpointId: options.endpointId?.trim() || DEFAULT_ENDPOINT_ID,
242
- version: "0.1.0",
243
- supportsStreaming: true,
244
- supportsAbort: true,
245
- supportsProactiveMessages: false,
246
- supportsLiveSessionStream: true,
247
- supportedPartTypes: SUPPORTED_PART_TYPES,
248
- expectedLatency: "seconds",
249
- metadata: { transport: "http+sse", scope: "agent" }
250
- };
251
- }
252
- async start() {
253
- if (this.started) {
254
- return;
255
- }
256
- this.started = true;
257
- this.publish({ type: NcpEventType.EndpointReady });
258
- }
259
- async stop() {
260
- if (!this.started) {
261
- return;
262
- }
263
- this.started = false;
264
- for (const controller of this.activeControllers) {
265
- controller.abort();
266
- }
267
- this.activeControllers.clear();
268
- }
269
- async emit(event) {
270
- switch (event.type) {
271
- case "message.request":
272
- await this.send(event.payload);
273
- return;
274
- case "message.stream-request":
275
- await this.stream(event.payload);
276
- return;
277
- case "message.abort":
278
- await this.abort(event.payload);
279
- return;
280
- default:
281
- this.publish(event);
282
- return;
283
- }
284
- }
285
- subscribe(listener) {
286
- this.subscribers.add(listener);
287
- return () => {
288
- this.subscribers.delete(listener);
289
- };
290
- }
291
- async send(envelope) {
292
- await this.ensureStarted();
293
- await this.jsonRequest({
294
- path: "/send",
295
- method: "POST",
296
- body: envelope
297
- });
298
- }
299
- async stream(payload) {
300
- await this.ensureStarted();
301
- const query = new URLSearchParams({
302
- sessionId: payload.sessionId
303
- });
304
- await this.streamRequest({
305
- path: `/stream?${query.toString()}`,
306
- method: "GET"
307
- });
308
- }
309
- async abort(payload) {
310
- await this.ensureStarted();
311
- const controller = new AbortController();
312
- this.activeControllers.add(controller);
313
- try {
314
- const response = await this.fetchImpl(this.resolveUrl("/abort"), {
315
- method: "POST",
316
- headers: {
317
- ...this.defaultHeaders,
318
- "content-type": "application/json",
319
- accept: "application/json"
320
- },
321
- body: JSON.stringify(payload),
322
- signal: controller.signal
323
- });
324
- if (!response.ok) {
325
- throw new Error(
326
- `Abort request failed with HTTP ${response.status}: ${await safeReadText(response)}`
327
- );
328
- }
329
- } catch (error) {
330
- if (controller.signal.aborted) {
331
- return;
332
- }
333
- const ncpError = toNcpError(error);
334
- this.publish({ type: NcpEventType.EndpointError, payload: ncpError });
335
- throw ncpErrorToError(ncpError);
336
- } finally {
337
- this.activeControllers.delete(controller);
338
- }
339
- }
340
- async ensureStarted() {
341
- if (!this.started) {
342
- await this.start();
343
- }
344
- }
345
- publish(event) {
346
- for (const subscriber of this.subscribers) {
347
- subscriber(event);
348
- }
349
- }
350
- resolveUrl(path) {
351
- return new URL(`${this.basePath}${path}`, this.baseUrl);
352
- }
353
- async jsonRequest(options) {
354
- const controller = new AbortController();
355
- this.activeControllers.add(controller);
356
- try {
357
- const response = await this.fetchImpl(this.resolveUrl(options.path), {
358
- method: options.method,
359
- headers: {
360
- ...this.defaultHeaders,
361
- "content-type": "application/json",
362
- accept: "application/json"
363
- },
364
- body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
365
- signal: controller.signal
366
- });
367
- if (!response.ok) {
368
- throw new Error(
369
- `NCP request failed with HTTP ${response.status}: ${await safeReadText(response)}`
370
- );
371
- }
372
- } catch (error) {
373
- if (controller.signal.aborted) {
374
- return;
375
- }
376
- if (isNcpHttpAgentClientError(error)) {
377
- throw error;
378
- }
379
- const ncpError = toNcpError(error);
380
- this.publish({ type: NcpEventType.EndpointError, payload: ncpError });
381
- throw ncpErrorToError(ncpError);
382
- } finally {
383
- this.activeControllers.delete(controller);
384
- }
385
- }
386
- async streamRequest(options) {
387
- const controller = new AbortController();
388
- this.activeControllers.add(controller);
389
- try {
390
- const response = await this.fetchImpl(this.resolveUrl(options.path), {
391
- method: options.method,
392
- headers: {
393
- ...this.defaultHeaders,
394
- accept: "text/event-stream",
395
- ...options.body !== void 0 ? { "content-type": "application/json" } : {}
396
- },
397
- body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
398
- signal: controller.signal
399
- });
400
- if (!response.ok) {
401
- throw new Error(
402
- `NCP stream request failed with HTTP ${response.status}: ${await safeReadText(response)}`
403
- );
404
- }
405
- if (!response.body) {
406
- throw new Error("NCP stream response has no body.");
407
- }
408
- for await (const frame of consumeSseStream(response.body)) {
409
- if (controller.signal.aborted) {
410
- return;
411
- }
412
- this.handleSseFrame(frame);
413
- }
414
- } catch (error) {
415
- if (controller.signal.aborted) {
416
- return;
417
- }
418
- if (isNcpHttpAgentClientError(error)) {
419
- throw error;
420
- }
421
- const ncpError = toNcpError(error);
422
- this.publish({ type: NcpEventType.EndpointError, payload: ncpError });
423
- throw ncpErrorToError(ncpError);
424
- } finally {
425
- this.activeControllers.delete(controller);
426
- }
427
- }
428
- handleSseFrame(frame) {
429
- if (frame.event === "ncp-event") {
430
- const event = parseNcpEvent(frame.data);
431
- if (!event) {
432
- this.publish({
433
- type: NcpEventType.EndpointError,
434
- payload: {
435
- code: "runtime-error",
436
- message: "Received malformed ncp-event frame."
437
- }
438
- });
439
- return;
440
- }
441
- this.publish(event);
442
- return;
443
- }
444
- if (frame.event === "error") {
445
- const ncpError = parseNcpError(frame.data);
446
- this.publish({ type: NcpEventType.EndpointError, payload: ncpError });
447
- throw ncpErrorToError(ncpError, { alreadyPublished: true });
448
- }
449
- }
450
- };
451
- export {
452
- NcpHttpAgentClientEndpoint
186
+ manifest;
187
+ baseUrl;
188
+ basePath;
189
+ fetchImpl;
190
+ defaultHeaders;
191
+ subscribers = /* @__PURE__ */ new Set();
192
+ activeControllers = /* @__PURE__ */ new Set();
193
+ started = false;
194
+ constructor(options) {
195
+ this.baseUrl = toBaseUrl(options.baseUrl);
196
+ this.basePath = normalizeBasePath(options.basePath);
197
+ this.fetchImpl = resolveFetchImpl(options.fetchImpl);
198
+ this.defaultHeaders = options.headers ?? {};
199
+ this.manifest = {
200
+ endpointKind: "custom",
201
+ endpointId: options.endpointId?.trim() || "ncp-http-agent-client",
202
+ version: "0.1.0",
203
+ supportsStreaming: true,
204
+ supportsAbort: true,
205
+ supportsProactiveMessages: false,
206
+ supportsLiveSessionStream: true,
207
+ supportedPartTypes: SUPPORTED_PART_TYPES,
208
+ expectedLatency: "seconds",
209
+ metadata: {
210
+ transport: "http+sse",
211
+ scope: "agent"
212
+ }
213
+ };
214
+ }
215
+ async start() {
216
+ if (this.started) return;
217
+ this.started = true;
218
+ this.publish({ type: NcpEventType.EndpointReady });
219
+ }
220
+ async stop() {
221
+ if (!this.started) return;
222
+ this.started = false;
223
+ for (const controller of this.activeControllers) controller.abort();
224
+ this.activeControllers.clear();
225
+ }
226
+ async emit(event) {
227
+ switch (event.type) {
228
+ case "message.request":
229
+ await this.send(event.payload);
230
+ return;
231
+ case "message.stream-request":
232
+ await this.stream(event.payload);
233
+ return;
234
+ case "message.abort":
235
+ await this.abort(event.payload);
236
+ return;
237
+ default:
238
+ this.publish(event);
239
+ return;
240
+ }
241
+ }
242
+ subscribe(listener) {
243
+ this.subscribers.add(listener);
244
+ return () => {
245
+ this.subscribers.delete(listener);
246
+ };
247
+ }
248
+ async send(envelope) {
249
+ await this.ensureStarted();
250
+ await this.jsonRequest({
251
+ path: "/send",
252
+ method: "POST",
253
+ body: envelope
254
+ });
255
+ }
256
+ async stream(payload) {
257
+ await this.ensureStarted();
258
+ const query = new URLSearchParams({ sessionId: payload.sessionId });
259
+ await this.streamRequest({
260
+ path: `/stream?${query.toString()}`,
261
+ method: "GET"
262
+ });
263
+ }
264
+ async abort(payload) {
265
+ await this.ensureStarted();
266
+ const controller = new AbortController();
267
+ this.activeControllers.add(controller);
268
+ try {
269
+ const response = await this.fetchImpl(this.resolveUrl("/abort"), {
270
+ method: "POST",
271
+ headers: {
272
+ ...this.defaultHeaders,
273
+ "content-type": "application/json",
274
+ accept: "application/json"
275
+ },
276
+ body: JSON.stringify(payload),
277
+ signal: controller.signal
278
+ });
279
+ if (!response.ok) throw new Error(`Abort request failed with HTTP ${response.status}: ${await safeReadText(response)}`);
280
+ } catch (error) {
281
+ if (controller.signal.aborted) return;
282
+ const ncpError = toNcpError(error);
283
+ this.publish({
284
+ type: NcpEventType.EndpointError,
285
+ payload: ncpError
286
+ });
287
+ throw ncpErrorToError(ncpError);
288
+ } finally {
289
+ this.activeControllers.delete(controller);
290
+ }
291
+ }
292
+ async ensureStarted() {
293
+ if (!this.started) await this.start();
294
+ }
295
+ publish(event) {
296
+ for (const subscriber of this.subscribers) subscriber(event);
297
+ }
298
+ resolveUrl(path) {
299
+ return new URL(`${this.basePath}${path}`, this.baseUrl);
300
+ }
301
+ async jsonRequest(options) {
302
+ const controller = new AbortController();
303
+ this.activeControllers.add(controller);
304
+ try {
305
+ const response = await this.fetchImpl(this.resolveUrl(options.path), {
306
+ method: options.method,
307
+ headers: {
308
+ ...this.defaultHeaders,
309
+ "content-type": "application/json",
310
+ accept: "application/json"
311
+ },
312
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
313
+ signal: controller.signal
314
+ });
315
+ if (!response.ok) throw new Error(`NCP request failed with HTTP ${response.status}: ${await safeReadText(response)}`);
316
+ } catch (error) {
317
+ if (controller.signal.aborted) return;
318
+ if (isNcpHttpAgentClientError(error)) throw error;
319
+ const ncpError = toNcpError(error);
320
+ this.publish({
321
+ type: NcpEventType.EndpointError,
322
+ payload: ncpError
323
+ });
324
+ throw ncpErrorToError(ncpError);
325
+ } finally {
326
+ this.activeControllers.delete(controller);
327
+ }
328
+ }
329
+ async streamRequest(options) {
330
+ const controller = new AbortController();
331
+ this.activeControllers.add(controller);
332
+ try {
333
+ const response = await this.fetchImpl(this.resolveUrl(options.path), {
334
+ method: options.method,
335
+ headers: {
336
+ ...this.defaultHeaders,
337
+ accept: "text/event-stream",
338
+ ...options.body !== void 0 ? { "content-type": "application/json" } : {}
339
+ },
340
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
341
+ signal: controller.signal
342
+ });
343
+ if (!response.ok) throw new Error(`NCP stream request failed with HTTP ${response.status}: ${await safeReadText(response)}`);
344
+ if (!response.body) throw new Error("NCP stream response has no body.");
345
+ for await (const frame of consumeSseStream(response.body)) {
346
+ if (controller.signal.aborted) return;
347
+ this.handleSseFrame(frame);
348
+ }
349
+ } catch (error) {
350
+ if (controller.signal.aborted) return;
351
+ if (isNcpHttpAgentClientError(error)) throw error;
352
+ const ncpError = toNcpError(error);
353
+ this.publish({
354
+ type: NcpEventType.EndpointError,
355
+ payload: ncpError
356
+ });
357
+ throw ncpErrorToError(ncpError);
358
+ } finally {
359
+ this.activeControllers.delete(controller);
360
+ }
361
+ }
362
+ handleSseFrame(frame) {
363
+ if (frame.event === "ncp-event") {
364
+ const event = parseNcpEvent(frame.data);
365
+ if (!event) {
366
+ this.publish({
367
+ type: NcpEventType.EndpointError,
368
+ payload: {
369
+ code: "runtime-error",
370
+ message: "Received malformed ncp-event frame."
371
+ }
372
+ });
373
+ return;
374
+ }
375
+ this.publish(event);
376
+ return;
377
+ }
378
+ if (frame.event === "error") {
379
+ const ncpError = parseNcpError(frame.data);
380
+ this.publish({
381
+ type: NcpEventType.EndpointError,
382
+ payload: ncpError
383
+ });
384
+ throw ncpErrorToError(ncpError, { alreadyPublished: true });
385
+ }
386
+ }
453
387
  };
388
+ //#endregion
389
+ export { NcpHttpAgentClientEndpoint };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ncp-http-agent-client",
3
- "version": "0.3.11",
3
+ "version": "0.3.13",
4
4
  "private": false,
5
5
  "description": "HTTP/SSE client transport adapter for NCP agent endpoints.",
6
6
  "type": "module",
@@ -15,17 +15,16 @@
15
15
  "dist"
16
16
  ],
17
17
  "dependencies": {
18
- "@nextclaw/ncp": "0.4.6"
18
+ "@nextclaw/ncp": "0.5.1"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/node": "^20.17.6",
22
22
  "prettier": "^3.3.3",
23
- "tsup": "^8.3.5",
24
23
  "typescript": "^5.6.3",
25
24
  "vitest": "^4.1.2"
26
25
  },
27
26
  "scripts": {
28
- "build": "tsup src/index.ts --format esm --dts --out-dir dist",
27
+ "build": "tsdown src/index.ts --dts --clean --target es2022 --no-fixedExtension",
29
28
  "lint": "eslint .",
30
29
  "tsc": "tsc -p tsconfig.json",
31
30
  "test": "vitest run"