@nextclaw/extension-sdk 0.1.1
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/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/services/extension-channel.service.js +38 -0
- package/dist/services/extension-client.service.d.ts +18 -0
- package/dist/services/extension-client.service.js +127 -0
- package/dist/services/extension-transport.service.js +85 -0
- package/dist/types/extension-sdk.types.d.ts +111 -0
- package/dist/utils/extension-url.utils.js +13 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NextClaw contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { ChannelConfigGetRequest, ChannelConfigGetResponse, ChannelFileContent, ChannelImageContent, ChannelMessageContent, ChannelSubmittedAttachment, ChannelSubmittedMessage, ChannelTextContent, ExtensionCapabilities, ExtensionCapabilityHandler, ExtensionCapabilityPayload, ExtensionChannel, ExtensionChannelConfig, ExtensionChannels, ExtensionRequest, ExtensionRequestHandler, ExtensionRequestResponse, ExtensionTransportEnvelope, NextClawExtensionOptions, NextClawExtensionWebSocketLike } from "./types/extension-sdk.types.js";
|
|
2
|
+
import { NextClawExtension } from "./services/extension-client.service.js";
|
|
3
|
+
export { type ChannelConfigGetRequest, type ChannelConfigGetResponse, type ChannelFileContent, type ChannelImageContent, type ChannelMessageContent, type ChannelSubmittedAttachment, type ChannelSubmittedMessage, type ChannelTextContent, type ExtensionCapabilities, type ExtensionCapabilityHandler, type ExtensionCapabilityPayload, type ExtensionChannel, type ExtensionChannelConfig, type ExtensionChannels, type ExtensionRequest, type ExtensionRequestHandler, type ExtensionRequestResponse, type ExtensionTransportEnvelope, NextClawExtension, type NextClawExtensionOptions, type NextClawExtensionWebSocketLike };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { eventKeys, getKeyId } from "@nextclaw/shared";
|
|
2
|
+
//#region src/services/extension-channel.service.ts
|
|
3
|
+
const CONFIG_GET_EVENT_TYPE = "extension.channel.config.get";
|
|
4
|
+
const MESSAGE_SUBMIT_EVENT_TYPE = "extension.channel.message.submit";
|
|
5
|
+
var ChannelConfig = class {
|
|
6
|
+
constructor(params) {
|
|
7
|
+
this.params = params;
|
|
8
|
+
}
|
|
9
|
+
get = async () => {
|
|
10
|
+
return (await this.params.transport.postIngress(CONFIG_GET_EVENT_TYPE, { channelId: this.params.channelId })).config;
|
|
11
|
+
};
|
|
12
|
+
onChange = (handler) => this.params.eventBus.subscribeAll((event) => {
|
|
13
|
+
if (event.type !== getKeyId(eventKeys.configUpdated)) return;
|
|
14
|
+
const payload = event.payload;
|
|
15
|
+
if (payload.path !== "channels" && payload.path !== `channels.${this.params.channelId}`) return;
|
|
16
|
+
this.get().then((config) => handler(config));
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
var ExtensionChannelService = class {
|
|
20
|
+
id;
|
|
21
|
+
config;
|
|
22
|
+
constructor(params) {
|
|
23
|
+
this.params = params;
|
|
24
|
+
this.id = params.channelId;
|
|
25
|
+
this.config = new ChannelConfig(params);
|
|
26
|
+
}
|
|
27
|
+
submitMessage = async (input) => {
|
|
28
|
+
await this.params.transport.postIngress(MESSAGE_SUBMIT_EVENT_TYPE, {
|
|
29
|
+
...input,
|
|
30
|
+
channelId: this.id
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
onNcpEvent = (handler) => this.params.eventBus.on(eventKeys.ncpEvent, (event) => {
|
|
34
|
+
handler(event);
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
//#endregion
|
|
38
|
+
export { ExtensionChannelService };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ExtensionCapabilities, ExtensionChannels, ExtensionRequestHandler, NextClawExtensionOptions } from "../types/extension-sdk.types.js";
|
|
2
|
+
import { EventBus } from "@nextclaw/shared";
|
|
3
|
+
|
|
4
|
+
//#region src/services/extension-client.service.d.ts
|
|
5
|
+
declare class NextClawExtension {
|
|
6
|
+
readonly eventBus: EventBus;
|
|
7
|
+
readonly channels: ExtensionChannels;
|
|
8
|
+
readonly capabilities: ExtensionCapabilities;
|
|
9
|
+
readonly extensionId: string;
|
|
10
|
+
private readonly transport;
|
|
11
|
+
private realtimeSubscription;
|
|
12
|
+
constructor(options?: NextClawExtensionOptions);
|
|
13
|
+
readonly close: () => void;
|
|
14
|
+
readonly onRequest: (handler: ExtensionRequestHandler) => (() => void);
|
|
15
|
+
private readonly toEventBusEnvelope;
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { NextClawExtension };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { ExtensionChannelService } from "./extension-channel.service.js";
|
|
2
|
+
import { ExtensionTransportService } from "./extension-transport.service.js";
|
|
3
|
+
import { EventBus } from "@nextclaw/shared";
|
|
4
|
+
//#region src/services/extension-client.service.ts
|
|
5
|
+
var ExtensionChannelRegistry = class {
|
|
6
|
+
channels = /* @__PURE__ */ new Map();
|
|
7
|
+
constructor(params) {
|
|
8
|
+
this.params = params;
|
|
9
|
+
}
|
|
10
|
+
use = (channelId) => {
|
|
11
|
+
const normalizedChannelId = channelId.trim();
|
|
12
|
+
if (!normalizedChannelId) throw new Error("channelId is required.");
|
|
13
|
+
const existing = this.channels.get(normalizedChannelId);
|
|
14
|
+
if (existing) return existing;
|
|
15
|
+
const channel = new ExtensionChannelService({
|
|
16
|
+
channelId: normalizedChannelId,
|
|
17
|
+
eventBus: this.params.eventBus,
|
|
18
|
+
transport: this.params.transport
|
|
19
|
+
});
|
|
20
|
+
this.channels.set(normalizedChannelId, channel);
|
|
21
|
+
return channel;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
function readRequest(payload) {
|
|
25
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null;
|
|
26
|
+
const record = payload;
|
|
27
|
+
if (typeof record.requestId !== "string" || typeof record.extensionId !== "string" || typeof record.kind !== "string") return null;
|
|
28
|
+
return {
|
|
29
|
+
requestId: record.requestId,
|
|
30
|
+
extensionId: record.extensionId,
|
|
31
|
+
kind: record.kind,
|
|
32
|
+
payload: record.payload && typeof record.payload === "object" && !Array.isArray(record.payload) ? record.payload : {}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function normalizeName(value, name) {
|
|
36
|
+
const normalized = value.trim();
|
|
37
|
+
if (!normalized) throw new Error(`${name} is required.`);
|
|
38
|
+
return normalized;
|
|
39
|
+
}
|
|
40
|
+
async function handleRequest(transport, request, handler) {
|
|
41
|
+
try {
|
|
42
|
+
const data = await handler(request);
|
|
43
|
+
await transport.respondToRequest({
|
|
44
|
+
requestId: request.requestId,
|
|
45
|
+
ok: true,
|
|
46
|
+
data
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
await transport.respondToRequest({
|
|
50
|
+
requestId: request.requestId,
|
|
51
|
+
ok: false,
|
|
52
|
+
error: { message: error instanceof Error ? error.message : String(error) }
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
var ExtensionCapabilityRegistry = class {
|
|
57
|
+
constructor(params) {
|
|
58
|
+
this.params = params;
|
|
59
|
+
}
|
|
60
|
+
provide = (namespace, capability) => {
|
|
61
|
+
const normalizedNamespace = normalizeName(namespace, "namespace");
|
|
62
|
+
const unsubscribeHandlers = this.readHandlers(capability).map(([name, handler]) => this.provideHandler(`${normalizedNamespace}.${name}`, handler));
|
|
63
|
+
if (unsubscribeHandlers.length === 0) throw new Error(`capability '${normalizedNamespace}' has no callable methods.`);
|
|
64
|
+
return () => {
|
|
65
|
+
for (const unsubscribe of unsubscribeHandlers) unsubscribe();
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
provideHandler = (kind, handler) => {
|
|
69
|
+
const normalizedKind = normalizeName(kind, "kind");
|
|
70
|
+
return this.params.eventBus.subscribeAll((event) => {
|
|
71
|
+
if (event.type !== "extension.request") return;
|
|
72
|
+
const request = readRequest(event.payload);
|
|
73
|
+
if (!request || request.extensionId !== this.params.extensionId || request.kind !== normalizedKind) return;
|
|
74
|
+
handleRequest(this.params.transport, request, async (matchedRequest) => await handler(matchedRequest.payload ?? {}, matchedRequest));
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
readHandlers = (capability) => Object.entries(capability).filter((entry) => typeof entry[1] === "function");
|
|
78
|
+
};
|
|
79
|
+
var NextClawExtension = class {
|
|
80
|
+
eventBus;
|
|
81
|
+
channels;
|
|
82
|
+
capabilities;
|
|
83
|
+
extensionId;
|
|
84
|
+
transport;
|
|
85
|
+
realtimeSubscription = null;
|
|
86
|
+
constructor(options = {}) {
|
|
87
|
+
this.transport = new ExtensionTransportService(options);
|
|
88
|
+
this.extensionId = this.transport.extensionId;
|
|
89
|
+
this.eventBus = new EventBus({
|
|
90
|
+
onFirstSubscriber: () => {
|
|
91
|
+
this.realtimeSubscription ??= this.transport.subscribe((event) => {
|
|
92
|
+
this.eventBus.emitEnvelope(this.toEventBusEnvelope(event));
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
onNoSubscribers: () => {
|
|
96
|
+
this.realtimeSubscription?.close();
|
|
97
|
+
this.realtimeSubscription = null;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
this.channels = new ExtensionChannelRegistry({
|
|
101
|
+
eventBus: this.eventBus,
|
|
102
|
+
transport: this.transport
|
|
103
|
+
});
|
|
104
|
+
this.capabilities = new ExtensionCapabilityRegistry({
|
|
105
|
+
eventBus: this.eventBus,
|
|
106
|
+
extensionId: this.extensionId,
|
|
107
|
+
transport: this.transport
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
close = () => {
|
|
111
|
+
this.realtimeSubscription?.close();
|
|
112
|
+
this.realtimeSubscription = null;
|
|
113
|
+
};
|
|
114
|
+
onRequest = (handler) => this.eventBus.subscribeAll((event) => {
|
|
115
|
+
if (event.type !== "extension.request") return;
|
|
116
|
+
const request = readRequest(event.payload);
|
|
117
|
+
if (!request || request.extensionId !== this.extensionId) return;
|
|
118
|
+
handleRequest(this.transport, request, handler);
|
|
119
|
+
});
|
|
120
|
+
toEventBusEnvelope = (event) => ({
|
|
121
|
+
...event,
|
|
122
|
+
emittedAt: event.emittedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
123
|
+
source: event.source ?? "realtime"
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
//#endregion
|
|
127
|
+
export { NextClawExtension };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { normalizeEndpoint, resolveWebSocketUrl } from "../utils/extension-url.utils.js";
|
|
2
|
+
//#region src/services/extension-transport.service.ts
|
|
3
|
+
function readRuntimeEnv() {
|
|
4
|
+
return typeof process === "undefined" ? {} : process.env;
|
|
5
|
+
}
|
|
6
|
+
function requireRuntimeValue(value, name) {
|
|
7
|
+
const trimmed = value?.trim();
|
|
8
|
+
if (!trimmed) throw new Error(`${name} is required.`);
|
|
9
|
+
return trimmed;
|
|
10
|
+
}
|
|
11
|
+
var ExtensionTransportService = class {
|
|
12
|
+
token;
|
|
13
|
+
extensionId;
|
|
14
|
+
endpoint;
|
|
15
|
+
fetchImpl;
|
|
16
|
+
webSocketFactory;
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
const env = readRuntimeEnv();
|
|
19
|
+
this.endpoint = normalizeEndpoint(options.endpoint ?? requireRuntimeValue(env.NEXTCLAW_EXTENSION_ENDPOINT, "NEXTCLAW_EXTENSION_ENDPOINT"));
|
|
20
|
+
this.token = options.token ?? requireRuntimeValue(env.NEXTCLAW_EXTENSION_TOKEN, "NEXTCLAW_EXTENSION_TOKEN");
|
|
21
|
+
this.extensionId = options.extensionId ?? requireRuntimeValue(env.NEXTCLAW_EXTENSION_ID, "NEXTCLAW_EXTENSION_ID");
|
|
22
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
23
|
+
this.webSocketFactory = options.webSocketFactory;
|
|
24
|
+
if (typeof this.fetchImpl !== "function") throw new Error("fetch is unavailable. Provide fetch when creating the extension.");
|
|
25
|
+
}
|
|
26
|
+
postIngress = async (type, payload) => {
|
|
27
|
+
const envelope = {
|
|
28
|
+
type,
|
|
29
|
+
extensionId: this.extensionId,
|
|
30
|
+
payload,
|
|
31
|
+
emittedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
32
|
+
source: "extension-sdk"
|
|
33
|
+
};
|
|
34
|
+
const response = await this.fetchImpl(`${this.endpoint}/webhook`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: {
|
|
37
|
+
"content-type": "application/json",
|
|
38
|
+
authorization: `Bearer ${this.token}`
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify(envelope)
|
|
41
|
+
});
|
|
42
|
+
const body = await response.json().catch(() => null);
|
|
43
|
+
if (!response.ok) throw new Error(this.readErrorMessage(body, `NextClaw ingress failed with ${response.status}`));
|
|
44
|
+
return this.readResponseData(body);
|
|
45
|
+
};
|
|
46
|
+
respondToRequest = async (response) => {
|
|
47
|
+
await this.postIngress("extension.response", response);
|
|
48
|
+
};
|
|
49
|
+
subscribe = (handler) => {
|
|
50
|
+
const socket = this.createSocket(resolveWebSocketUrl(this.endpoint, "/ws"));
|
|
51
|
+
socket.onmessage = (event) => {
|
|
52
|
+
const envelope = this.parseEnvelope(event.data);
|
|
53
|
+
if (envelope) handler(envelope);
|
|
54
|
+
};
|
|
55
|
+
return { close: () => socket.close() };
|
|
56
|
+
};
|
|
57
|
+
createSocket = (url) => {
|
|
58
|
+
if (this.webSocketFactory) return this.webSocketFactory(url);
|
|
59
|
+
if (typeof globalThis.WebSocket !== "function") throw new Error("WebSocket is unavailable. Provide webSocketFactory when creating the extension.");
|
|
60
|
+
return new globalThis.WebSocket(url);
|
|
61
|
+
};
|
|
62
|
+
parseEnvelope = (value) => {
|
|
63
|
+
if (typeof value !== "string") return null;
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(value);
|
|
66
|
+
return parsed && typeof parsed === "object" && typeof parsed.type === "string" ? parsed : null;
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
readErrorMessage = (body, fallback) => {
|
|
72
|
+
if (!body || typeof body !== "object") return fallback;
|
|
73
|
+
const error = body.error;
|
|
74
|
+
return typeof error?.message === "string" && error.message.trim() ? error.message : fallback;
|
|
75
|
+
};
|
|
76
|
+
readResponseData = (body) => {
|
|
77
|
+
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
78
|
+
const record = body;
|
|
79
|
+
if (record.ok === true && "data" in record) return record.data;
|
|
80
|
+
}
|
|
81
|
+
return body;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
//#endregion
|
|
85
|
+
export { ExtensionTransportService };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Unsubscribe } from "@nextclaw/shared";
|
|
2
|
+
import { NcpEndpointEvent } from "@nextclaw/ncp";
|
|
3
|
+
|
|
4
|
+
//#region src/types/extension-sdk.types.d.ts
|
|
5
|
+
type NextClawExtensionOptions = {
|
|
6
|
+
endpoint?: string;
|
|
7
|
+
token?: string;
|
|
8
|
+
extensionId?: string;
|
|
9
|
+
fetch?: typeof fetch;
|
|
10
|
+
webSocketFactory?: (url: string) => NextClawExtensionWebSocketLike;
|
|
11
|
+
};
|
|
12
|
+
type NextClawExtensionWebSocketLike = {
|
|
13
|
+
onopen: (() => void) | null;
|
|
14
|
+
onmessage: ((event: {
|
|
15
|
+
data: unknown;
|
|
16
|
+
}) => void) | null;
|
|
17
|
+
onerror: ((event: unknown) => void) | null;
|
|
18
|
+
onclose: (() => void) | null;
|
|
19
|
+
close: () => void;
|
|
20
|
+
};
|
|
21
|
+
type ExtensionTransportEnvelope<TPayload = unknown> = {
|
|
22
|
+
type: string;
|
|
23
|
+
extensionId: string;
|
|
24
|
+
payload: TPayload;
|
|
25
|
+
emittedAt?: string;
|
|
26
|
+
source?: string;
|
|
27
|
+
};
|
|
28
|
+
type ExtensionRequest = {
|
|
29
|
+
requestId: string;
|
|
30
|
+
extensionId: string;
|
|
31
|
+
kind: string;
|
|
32
|
+
payload?: Record<string, unknown>;
|
|
33
|
+
};
|
|
34
|
+
type ExtensionRequestResponse = {
|
|
35
|
+
requestId: string;
|
|
36
|
+
ok: true;
|
|
37
|
+
data?: unknown;
|
|
38
|
+
} | {
|
|
39
|
+
requestId: string;
|
|
40
|
+
ok: false;
|
|
41
|
+
error: {
|
|
42
|
+
message: string;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
type ChannelTextContent = {
|
|
46
|
+
type: "text";
|
|
47
|
+
text: string;
|
|
48
|
+
};
|
|
49
|
+
type ChannelImageContent = {
|
|
50
|
+
type: "image";
|
|
51
|
+
url?: string;
|
|
52
|
+
assetUri?: string;
|
|
53
|
+
mimeType?: string;
|
|
54
|
+
name?: string;
|
|
55
|
+
};
|
|
56
|
+
type ChannelFileContent = {
|
|
57
|
+
type: "file";
|
|
58
|
+
url?: string;
|
|
59
|
+
assetUri?: string;
|
|
60
|
+
mimeType?: string;
|
|
61
|
+
name?: string;
|
|
62
|
+
};
|
|
63
|
+
type ChannelMessageContent = ChannelTextContent | ChannelImageContent | ChannelFileContent;
|
|
64
|
+
type ChannelSubmittedAttachment = {
|
|
65
|
+
id?: string;
|
|
66
|
+
name?: string;
|
|
67
|
+
path?: string;
|
|
68
|
+
url?: string;
|
|
69
|
+
assetUri?: string;
|
|
70
|
+
mimeType?: string;
|
|
71
|
+
size?: number;
|
|
72
|
+
source?: string;
|
|
73
|
+
status?: "ready" | "remote-only";
|
|
74
|
+
errorCode?: "too_large" | "download_failed" | "http_error" | "invalid_payload";
|
|
75
|
+
};
|
|
76
|
+
type ChannelSubmittedMessage = {
|
|
77
|
+
channelId: string;
|
|
78
|
+
conversationId: string;
|
|
79
|
+
senderId: string;
|
|
80
|
+
content: ChannelMessageContent;
|
|
81
|
+
attachments?: ChannelSubmittedAttachment[];
|
|
82
|
+
metadata?: Record<string, unknown>;
|
|
83
|
+
};
|
|
84
|
+
type ChannelConfigGetRequest = {
|
|
85
|
+
channelId: string;
|
|
86
|
+
};
|
|
87
|
+
type ChannelConfigGetResponse<TConfig = unknown> = {
|
|
88
|
+
config: TConfig;
|
|
89
|
+
};
|
|
90
|
+
type ExtensionChannelConfig = {
|
|
91
|
+
get: <TConfig = unknown>() => Promise<TConfig>;
|
|
92
|
+
onChange: <TConfig = unknown>(handler: (config: TConfig) => void | Promise<void>) => Unsubscribe;
|
|
93
|
+
};
|
|
94
|
+
type ExtensionChannel = {
|
|
95
|
+
id: string;
|
|
96
|
+
submitMessage: (input: Omit<ChannelSubmittedMessage, "channelId">) => Promise<void>;
|
|
97
|
+
onNcpEvent: (handler: (event: NcpEndpointEvent) => void | Promise<void>) => Unsubscribe;
|
|
98
|
+
config: ExtensionChannelConfig;
|
|
99
|
+
};
|
|
100
|
+
type ExtensionChannels = {
|
|
101
|
+
use: (channelId: string) => ExtensionChannel;
|
|
102
|
+
};
|
|
103
|
+
type ExtensionRequestHandler = (request: ExtensionRequest) => unknown | Promise<unknown>;
|
|
104
|
+
type ExtensionCapabilityPayload = Record<string, unknown>;
|
|
105
|
+
type ExtensionCapabilityHandler<TPayload extends ExtensionCapabilityPayload = ExtensionCapabilityPayload> = (payload: TPayload, request: ExtensionRequest) => unknown | Promise<unknown>;
|
|
106
|
+
type ExtensionCapabilities = {
|
|
107
|
+
provide: (namespace: string, capability: object) => Unsubscribe;
|
|
108
|
+
provideHandler: (kind: string, handler: ExtensionCapabilityHandler) => Unsubscribe;
|
|
109
|
+
};
|
|
110
|
+
//#endregion
|
|
111
|
+
export { ChannelConfigGetRequest, ChannelConfigGetResponse, ChannelFileContent, ChannelImageContent, ChannelMessageContent, ChannelSubmittedAttachment, ChannelSubmittedMessage, ChannelTextContent, ExtensionCapabilities, ExtensionCapabilityHandler, ExtensionCapabilityPayload, ExtensionChannel, ExtensionChannelConfig, ExtensionChannels, ExtensionRequest, ExtensionRequestHandler, ExtensionRequestResponse, ExtensionTransportEnvelope, NextClawExtensionOptions, NextClawExtensionWebSocketLike };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/utils/extension-url.utils.ts
|
|
2
|
+
function normalizeEndpoint(endpoint) {
|
|
3
|
+
const trimmed = endpoint.trim();
|
|
4
|
+
if (!trimmed) throw new Error("NextClaw extension endpoint is required.");
|
|
5
|
+
return trimmed.replace(/\/+$/, "");
|
|
6
|
+
}
|
|
7
|
+
function resolveWebSocketUrl(endpoint, path) {
|
|
8
|
+
const url = new URL(path, `${normalizeEndpoint(endpoint)}/`);
|
|
9
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
10
|
+
return url.toString();
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { normalizeEndpoint, resolveWebSocketUrl };
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nextclaw/extension-sdk",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Backend SDK for NextClaw extension server processes.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Peiiii/nextclaw.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/Peiiii/nextclaw/tree/master/packages/nextclaw-extension-sdk",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Peiiii/nextclaw/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"nextclaw",
|
|
18
|
+
"extension-sdk",
|
|
19
|
+
"channel",
|
|
20
|
+
"realtime"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"development": "./src/index.ts",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js",
|
|
30
|
+
"default": "./dist/index.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md"
|
|
36
|
+
],
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@nextclaw/ncp": "0.5.7",
|
|
39
|
+
"@nextclaw/shared": "0.1.1"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^20.17.6",
|
|
43
|
+
"typescript": "^5.6.3",
|
|
44
|
+
"vitest": "^4.1.2"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsdown src/index.ts --dts --clean --target es2022 --no-fixedExtension --unbundle",
|
|
48
|
+
"lint": "eslint src --max-warnings=0",
|
|
49
|
+
"tsc": "tsc -p tsconfig.json",
|
|
50
|
+
"test": "vitest run"
|
|
51
|
+
}
|
|
52
|
+
}
|