@nextclaw/extension-sdk 0.3.24 → 0.3.26
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 +3 -2
- package/dist/index.js +2 -1
- package/dist/services/extension-channel-controller.service.d.ts +3 -1
- package/dist/services/extension-channel-controller.service.d.ts.map +1 -1
- package/dist/services/extension-channel-controller.service.js +22 -7
- package/dist/services/extension-channel-controller.service.js.map +1 -1
- package/dist/services/extension-client.service.d.ts +2 -1
- package/dist/services/extension-client.service.d.ts.map +1 -1
- package/dist/services/extension-client.service.js +33 -1
- package/dist/services/extension-client.service.js.map +1 -1
- package/dist/services/extension-transport.service.js +8 -3
- package/dist/services/extension-transport.service.js.map +1 -1
- package/dist/types/extension-sdk.types.d.ts +7 -2
- package/dist/types/extension-sdk.types.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { ChannelCommandExecuteRequest, ChannelCommandExecuteResponse, ChannelCommandListRequest, ChannelCommandListResponse, ChannelCommandOption, ChannelCommandOptionType, ChannelCommandSpec, ChannelConfigGetRequest, ChannelConfigGetResponse, ChannelFileContent, ChannelImageContent, ChannelMessageContent, ChannelSubmittedAttachment, ChannelSubmittedMessage, ChannelTextContent, ExtensionCapabilities, ExtensionCapabilityHandler, ExtensionCapabilityPayload, ExtensionChannel, ExtensionChannelCommands, ExtensionChannelConfig, ExtensionChannels, ExtensionRequest, ExtensionRequestHandler, ExtensionRequestResponse, ExtensionTransportEnvelope, NextClawExtensionOptions, NextClawExtensionWebSocketLike } from "./types/extension-sdk.types.js";
|
|
1
|
+
import { ChannelCommandExecuteRequest, ChannelCommandExecuteResponse, ChannelCommandListRequest, ChannelCommandListResponse, ChannelCommandOption, ChannelCommandOptionType, ChannelCommandSpec, ChannelConfigGetRequest, ChannelConfigGetResponse, ChannelFileContent, ChannelImageContent, ChannelMessageContent, ChannelSubmittedAttachment, ChannelSubmittedMessage, ChannelTextContent, ExtensionCapabilities, ExtensionCapabilityHandler, ExtensionCapabilityPayload, ExtensionChannel, ExtensionChannelCommands, ExtensionChannelConfig, ExtensionChannels, ExtensionDiagnostics, ExtensionRequest, ExtensionRequestHandler, ExtensionRequestResponse, ExtensionTransportEnvelope, NextClawExtensionOptions, NextClawExtensionWebSocketLike } from "./types/extension-sdk.types.js";
|
|
2
2
|
import { NextClawExtension } from "./services/extension-client.service.js";
|
|
3
3
|
import { ChannelTypingController, ChannelTypingControllerOptions } from "./services/channel-typing-controller.service.js";
|
|
4
4
|
import { BusChannelCreateContext, BusChannelExtensionDefinition, BusChannelInboundMessage, BusChannelMessageBus, BusChannelRuntime, ChannelExtensionContext, ChannelExtensionDefinition, ChannelSubmittedMessageInput, ExtensionChannelAdapter, ExtensionChannelController, startBusChannelExtension, startChannelExtension, warnNcpEventError } from "./services/extension-channel-controller.service.js";
|
|
5
|
-
|
|
5
|
+
import { DIAGNOSTIC_CORRELATION_METADATA_KEY, DiagnosticErrorClassification, classifyDiagnosticError } from "@nextclaw/shared";
|
|
6
|
+
export { type BusChannelCreateContext, type BusChannelExtensionDefinition, type BusChannelInboundMessage, type BusChannelMessageBus, type BusChannelRuntime, type ChannelCommandExecuteRequest, type ChannelCommandExecuteResponse, type ChannelCommandListRequest, type ChannelCommandListResponse, type ChannelCommandOption, type ChannelCommandOptionType, type ChannelCommandSpec, type ChannelConfigGetRequest, type ChannelConfigGetResponse, type ChannelExtensionContext, type ChannelExtensionDefinition, type ChannelFileContent, type ChannelImageContent, type ChannelMessageContent, type ChannelSubmittedAttachment, type ChannelSubmittedMessage, type ChannelSubmittedMessageInput, type ChannelTextContent, ChannelTypingController, type ChannelTypingControllerOptions, DIAGNOSTIC_CORRELATION_METADATA_KEY, type DiagnosticErrorClassification, type ExtensionCapabilities, type ExtensionCapabilityHandler, type ExtensionCapabilityPayload, type ExtensionChannel, type ExtensionChannelAdapter, type ExtensionChannelCommands, type ExtensionChannelConfig, ExtensionChannelController, type ExtensionChannels, type ExtensionDiagnostics, type ExtensionRequest, type ExtensionRequestHandler, type ExtensionRequestResponse, type ExtensionTransportEnvelope, NextClawExtension, type NextClawExtensionOptions, type NextClawExtensionWebSocketLike, classifyDiagnosticError, startBusChannelExtension, startChannelExtension, warnNcpEventError };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NextClawExtension } from "./services/extension-client.service.js";
|
|
2
2
|
import { ChannelTypingController } from "./services/channel-typing-controller.service.js";
|
|
3
3
|
import { ExtensionChannelController, startBusChannelExtension, startChannelExtension, warnNcpEventError } from "./services/extension-channel-controller.service.js";
|
|
4
|
-
|
|
4
|
+
import { DIAGNOSTIC_CORRELATION_METADATA_KEY, classifyDiagnosticError } from "@nextclaw/shared";
|
|
5
|
+
export { ChannelTypingController, DIAGNOSTIC_CORRELATION_METADATA_KEY, ExtensionChannelController, NextClawExtension, classifyDiagnosticError, startBusChannelExtension, startChannelExtension, warnNcpEventError };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ChannelSubmittedAttachment, ChannelSubmittedMessage, ExtensionChannel, NextClawExtensionOptions } from "../types/extension-sdk.types.js";
|
|
1
|
+
import { ChannelSubmittedAttachment, ChannelSubmittedMessage, ExtensionChannel, ExtensionDiagnostics, NextClawExtensionOptions } from "../types/extension-sdk.types.js";
|
|
2
2
|
import { NcpEndpointEvent } from "@nextclaw/ncp";
|
|
3
3
|
|
|
4
4
|
//#region src/services/extension-channel-controller.service.d.ts
|
|
@@ -53,6 +53,7 @@ type BusChannelCreateContext<TConfig, TBus = BusChannelMessageBus> = {
|
|
|
53
53
|
config: TConfig;
|
|
54
54
|
bus: TBus;
|
|
55
55
|
channel: ExtensionChannel;
|
|
56
|
+
diagnostics: ExtensionDiagnostics;
|
|
56
57
|
};
|
|
57
58
|
type BusChannelExtensionDefinition<TConfig, TBus = BusChannelMessageBus> = {
|
|
58
59
|
channelId: string;
|
|
@@ -69,6 +70,7 @@ type ExtensionChannelControllerOptions<TConfig, TInbound> = {
|
|
|
69
70
|
};
|
|
70
71
|
type ChannelExtensionContext = {
|
|
71
72
|
channel: ExtensionChannel;
|
|
73
|
+
diagnostics: ExtensionDiagnostics;
|
|
72
74
|
};
|
|
73
75
|
type ChannelExtensionDefinition<TConfig, TInbound> = {
|
|
74
76
|
channelId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extension-channel-controller.service.d.ts","names":[],"sources":["../../src/services/extension-channel-controller.service.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"extension-channel-controller.service.d.ts","names":[],"sources":["../../src/services/extension-channel-controller.service.ts"],"mappings":"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"}
|
|
@@ -128,9 +128,13 @@ var ExtensionChannelController = class {
|
|
|
128
128
|
var BusChannelAdapter = class {
|
|
129
129
|
channel = null;
|
|
130
130
|
messageHandler = null;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
definition;
|
|
132
|
+
diagnostics;
|
|
133
|
+
extensionChannel;
|
|
134
|
+
constructor(params) {
|
|
135
|
+
this.definition = params.definition;
|
|
136
|
+
this.diagnostics = params.diagnostics;
|
|
137
|
+
this.extensionChannel = params.extensionChannel;
|
|
134
138
|
}
|
|
135
139
|
configure = async (config) => {
|
|
136
140
|
await this.stop();
|
|
@@ -139,7 +143,8 @@ var BusChannelAdapter = class {
|
|
|
139
143
|
bus: { publishInbound: async (message) => {
|
|
140
144
|
await this.messageHandler?.(message);
|
|
141
145
|
} },
|
|
142
|
-
channel: this.extensionChannel
|
|
146
|
+
channel: this.extensionChannel,
|
|
147
|
+
diagnostics: this.diagnostics
|
|
143
148
|
});
|
|
144
149
|
};
|
|
145
150
|
start = async () => {
|
|
@@ -203,11 +208,17 @@ async function startChannelExtension(definition, options = {}) {
|
|
|
203
208
|
const channel = extension.channels.use(definition.channelId);
|
|
204
209
|
const controller = new ExtensionChannelController({
|
|
205
210
|
channel,
|
|
206
|
-
adapter: new LazyExtensionChannelAdapter(() => definition.createAdapter({
|
|
211
|
+
adapter: new LazyExtensionChannelAdapter(() => definition.createAdapter({
|
|
212
|
+
channel,
|
|
213
|
+
diagnostics: extension.diagnostics
|
|
214
|
+
})),
|
|
207
215
|
mapInbound: definition.mapInbound,
|
|
208
216
|
onNcpEventError: definition.onNcpEventError
|
|
209
217
|
});
|
|
210
|
-
if (definition.createAuthCapability) extension.capabilities.provide("channel.auth", definition.createAuthCapability({
|
|
218
|
+
if (definition.createAuthCapability) extension.capabilities.provide("channel.auth", definition.createAuthCapability({
|
|
219
|
+
channel,
|
|
220
|
+
diagnostics: extension.diagnostics
|
|
221
|
+
}));
|
|
211
222
|
extension.capabilities.provideHandler("channel.outbound.sendText", async (payload) => await controller.sendOutboundText({
|
|
212
223
|
to: readRequiredPayloadString(payload.to, "to"),
|
|
213
224
|
text: readRequiredPayloadString(payload.text, "text"),
|
|
@@ -222,7 +233,11 @@ async function startChannelExtension(definition, options = {}) {
|
|
|
222
233
|
async function startBusChannelExtension(definition, options = {}) {
|
|
223
234
|
await startChannelExtension({
|
|
224
235
|
channelId: definition.channelId,
|
|
225
|
-
createAdapter: ({ channel }) => new BusChannelAdapter(
|
|
236
|
+
createAdapter: ({ channel, diagnostics }) => new BusChannelAdapter({
|
|
237
|
+
definition,
|
|
238
|
+
diagnostics,
|
|
239
|
+
extensionChannel: channel
|
|
240
|
+
}),
|
|
226
241
|
mapInbound: definition.mapInbound ?? toSubmittedTextMessage
|
|
227
242
|
}, options);
|
|
228
243
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extension-channel-controller.service.js","names":[],"sources":["../../src/services/extension-channel-controller.service.ts"],"sourcesContent":["import { NcpEventType, type NcpEndpointEvent, type NcpMessagePart } from \"@nextclaw/ncp\";\nimport type {\n ChannelSubmittedMessage,\n ChannelSubmittedAttachment,\n ExtensionChannel,\n NextClawExtensionOptions,\n} from \"../types/extension-sdk.types.js\";\nimport { NextClawExtension } from \"./extension-client.service.js\";\n\nfunction readRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? value as Record<string, unknown>\n : {};\n}\n\nconst AGENT_ROUTE_KINDS = new Set([\"direct\", \"group\", \"channel\", \"thread\"]);\n\nfunction parseSessionDeliveryRoute(sessionId: string): {\n channel?: string;\n chatId?: string;\n accountId?: string;\n} {\n const parts = sessionId.trim().split(\":\");\n if (parts[0] !== \"agent\" || parts.length < 5) {\n return {};\n }\n const hasAccount = !AGENT_ROUTE_KINDS.has(parts[3]);\n const kind = parts[hasAccount ? 4 : 3];\n if (!AGENT_ROUTE_KINDS.has(kind ?? \"\")) {\n return {};\n }\n return {\n channel: parts[2],\n ...(hasAccount ? { accountId: parts[3] } : {}),\n chatId: parts.slice(hasAccount ? 5 : 4).join(\":\"),\n };\n}\n\nfunction readString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() ? value.trim() : undefined;\n}\n\nfunction renderMessageText(parts: readonly NcpMessagePart[]): string {\n return (parts as Array<{ type: string; text?: string }>).map((part) =>\n (part.type === \"text\" || part.type === \"rich-text\") && part.text ? part.text : \"\",\n ).filter(Boolean).join(\"\\n\\n\");\n}\n\nexport type ExtensionChannelAdapter<TConfig, TInbound> = {\n configure: (config: TConfig) => Promise<void>;\n start: () => Promise<void>;\n stop: () => Promise<void>;\n onMessage: (handler: (message: TInbound) => void | Promise<void>) => () => void;\n sendNcpEvent: (event: NcpEndpointEvent) => Promise<void>;\n sendOutboundText: (params: {\n to: string;\n text: string;\n accountId?: string | null;\n replyTo?: string | null;\n media?: string[];\n metadata?: Record<string, unknown>;\n }) => Promise<void>;\n};\n\nexport type ChannelSubmittedMessageInput = Omit<ChannelSubmittedMessage, \"channelId\">;\n\nexport type BusChannelInboundMessage = {\n channel: string;\n senderId: string;\n chatId: string;\n content: string;\n attachments?: ChannelSubmittedAttachment[];\n metadata?: Record<string, unknown>;\n};\n\nexport type BusChannelMessageBus = {\n publishInbound: (message: BusChannelInboundMessage) => Promise<void>;\n};\n\nexport type BusChannelRuntime = {\n start: () => Promise<void>;\n stop: () => Promise<void>;\n handleControlMessage?: (message: {\n channel: string;\n chatId: string;\n content: string;\n replyTo?: string | null;\n media: string[];\n metadata: Record<string, unknown>;\n }) => Promise<boolean>;\n send: (message: {\n channel: string;\n chatId: string;\n content: string;\n replyTo?: string | null;\n media: string[];\n metadata: Record<string, unknown>;\n }) => Promise<void>;\n};\n\nexport type BusChannelCreateContext<TConfig, TBus = BusChannelMessageBus> = {\n config: TConfig;\n bus: TBus;\n channel: ExtensionChannel;\n};\n\nexport type BusChannelExtensionDefinition<TConfig, TBus = BusChannelMessageBus> = {\n channelId: string;\n createChannel: (\n context: BusChannelCreateContext<TConfig, TBus>,\n ) => BusChannelRuntime | Promise<BusChannelRuntime>;\n mapInbound?: ExtensionChannelInboundMapper<BusChannelInboundMessage>;\n onChannelStartError?: (error: unknown) => void | Promise<void>;\n};\n\nexport type ExtensionChannelInboundMapper<TInbound> =\n (message: TInbound) => ChannelSubmittedMessageInput | Promise<ChannelSubmittedMessageInput>;\n\ntype ExtensionChannelControllerOptions<TConfig, TInbound> = {\n channel: ExtensionChannel;\n adapter: ExtensionChannelAdapter<TConfig, TInbound>;\n mapInbound: ExtensionChannelInboundMapper<TInbound>;\n onNcpEventError?: (error: unknown, event: NcpEndpointEvent) => void | Promise<void>;\n};\n\nexport type ChannelExtensionContext = {\n channel: ExtensionChannel;\n};\n\nexport type ChannelExtensionDefinition<TConfig, TInbound> = {\n channelId: string;\n createAdapter: (\n context: ChannelExtensionContext,\n ) => ExtensionChannelAdapter<TConfig, TInbound> | Promise<ExtensionChannelAdapter<TConfig, TInbound>>;\n mapInbound: ExtensionChannelInboundMapper<TInbound>;\n createAuthCapability?: (context: ChannelExtensionContext) => object;\n onNcpEventError?: (error: unknown, event: NcpEndpointEvent) => void | Promise<void>;\n};\n\nfunction isChannelEnabled(config: unknown): boolean {\n return Boolean(\n config &&\n typeof config === \"object\" &&\n !Array.isArray(config) &&\n (config as { enabled?: unknown }).enabled === true\n );\n}\n\nclass LazyExtensionChannelAdapter<TConfig, TInbound> implements ExtensionChannelAdapter<TConfig, TInbound> {\n private adapterCleanup: (() => void) | null = null;\n private adapterPromise: Promise<ExtensionChannelAdapter<TConfig, TInbound>> | null = null;\n private messageHandler: ((message: TInbound) => void | Promise<void>) | null = null;\n\n constructor(\n private readonly createAdapter: () =>\n ExtensionChannelAdapter<TConfig, TInbound> | Promise<ExtensionChannelAdapter<TConfig, TInbound>>,\n ) {}\n\n configure = async (config: TConfig): Promise<void> => {\n await (await this.getAdapter()).configure(config);\n };\n\n start = async (): Promise<void> => {\n await (await this.getAdapter()).start();\n };\n\n stop = async (): Promise<void> => {\n if (this.adapterPromise) {\n await (await this.adapterPromise).stop();\n }\n };\n\n onMessage = (handler: (message: TInbound) => void | Promise<void>): (() => void) => {\n this.messageHandler = handler;\n if (this.adapterPromise) {\n void this.adapterPromise.then((adapter) => this.attachMessageHandler(adapter)).catch(() => undefined);\n }\n return () => {\n if (this.messageHandler === handler) {\n this.messageHandler = null;\n this.adapterCleanup?.();\n this.adapterCleanup = null;\n }\n };\n };\n\n sendNcpEvent = async (event: NcpEndpointEvent): Promise<void> => {\n if (this.adapterPromise) {\n await (await this.adapterPromise).sendNcpEvent(event);\n }\n };\n\n sendOutboundText = async (params: {\n to: string;\n text: string;\n accountId?: string | null;\n replyTo?: string | null;\n media?: string[];\n metadata?: Record<string, unknown>;\n }): Promise<void> => {\n await (await this.getAdapter()).sendOutboundText(params);\n };\n\n private getAdapter = async (): Promise<ExtensionChannelAdapter<TConfig, TInbound>> => {\n this.adapterPromise ??= Promise.resolve(this.createAdapter()).then((adapter) => {\n this.attachMessageHandler(adapter);\n return adapter;\n });\n return await this.adapterPromise;\n };\n\n private attachMessageHandler = (adapter: ExtensionChannelAdapter<TConfig, TInbound>): void => {\n this.adapterCleanup?.();\n this.adapterCleanup = this.messageHandler ? adapter.onMessage(this.messageHandler) : null;\n };\n}\n\nexport class ExtensionChannelController<TConfig, TInbound> {\n private readonly cleanups: Array<() => void> = [];\n private started = false;\n\n constructor(\n private readonly options: ExtensionChannelControllerOptions<TConfig, TInbound>,\n ) {}\n\n start = async (): Promise<void> => {\n if (this.started) {\n return;\n }\n this.started = true;\n this.cleanups.push(this.options.adapter.onMessage(this.submitMessage));\n this.cleanups.push(this.options.channel.onNcpEvent(this.sendNcpEvent));\n this.cleanups.push(this.options.channel.config.onChange(async () => {\n await this.applyConfig();\n }));\n await this.applyConfig();\n };\n\n stop = async (): Promise<void> => {\n if (!this.started) {\n return;\n }\n this.started = false;\n this.drainCleanups();\n await this.options.adapter.stop();\n };\n\n sendOutboundText = async (params: {\n to: string;\n text: string;\n accountId?: string | null;\n replyTo?: string | null;\n media?: string[];\n metadata?: Record<string, unknown>;\n }): Promise<{ accepted: true }> => {\n await this.options.adapter.sendOutboundText(params);\n return { accepted: true };\n };\n\n private applyConfig = async (): Promise<void> => {\n const config = await this.options.channel.config.get<TConfig>();\n if (!isChannelEnabled(config)) {\n await this.options.adapter.stop();\n return;\n }\n await this.options.adapter.configure(config);\n await this.options.adapter.start();\n };\n\n private submitMessage = async (message: TInbound): Promise<void> => {\n await this.options.channel.submitMessage(await this.options.mapInbound(message));\n };\n\n private sendNcpEvent = async (event: NcpEndpointEvent): Promise<void> => {\n try {\n await this.options.adapter.sendNcpEvent(event);\n } catch (error) {\n await this.options.onNcpEventError?.(error, event);\n }\n };\n\n private drainCleanups = (): void => {\n for (const cleanup of this.cleanups.splice(0).reverse()) {\n cleanup();\n }\n };\n}\n\nclass BusChannelAdapter<TConfig, TBus> implements ExtensionChannelAdapter<TConfig, BusChannelInboundMessage> {\n private channel: BusChannelRuntime | null = null;\n private messageHandler: ((message: BusChannelInboundMessage) => void | Promise<void>) | null = null;\n\n constructor(\n private readonly definition: BusChannelExtensionDefinition<TConfig, TBus>,\n private readonly extensionChannel: ExtensionChannel,\n ) {}\n\n configure = async (config: TConfig): Promise<void> => {\n await this.stop();\n this.channel = await this.definition.createChannel({\n config,\n bus: {\n publishInbound: async (message: BusChannelInboundMessage) => {\n await this.messageHandler?.(message);\n },\n } as TBus,\n channel: this.extensionChannel,\n });\n };\n\n start = async (): Promise<void> => {\n const channel = this.channel;\n if (!channel) {\n return;\n }\n void channel.start().catch(async (error: unknown) => {\n await this.definition.onChannelStartError?.(error);\n });\n };\n\n stop = async (): Promise<void> => {\n const channel = this.channel;\n this.channel = null;\n await channel?.stop();\n };\n\n onMessage = (\n handler: (message: BusChannelInboundMessage) => void | Promise<void>,\n ): (() => void) => {\n this.messageHandler = handler;\n return () => {\n if (this.messageHandler === handler) {\n this.messageHandler = null;\n }\n };\n };\n\n sendNcpEvent = async (event: NcpEndpointEvent): Promise<void> => {\n if (event.type !== NcpEventType.MessageCompleted) {\n return;\n }\n const metadata = readRecord(event.payload.message.metadata);\n const sessionRoute = parseSessionDeliveryRoute(event.payload.message.sessionId);\n const eventChannel = readString(metadata.channel) ?? readString(metadata.channelId) ?? sessionRoute.channel;\n if (eventChannel !== this.definition.channelId) {\n return;\n }\n const chatId = readString(metadata.chatId) ?? readString(metadata.chat_id) ?? sessionRoute.chatId;\n const content = renderMessageText(event.payload.message.parts).trim();\n if (!chatId || !content) {\n return;\n }\n const replyTo = readString(metadata.message_id);\n await this.channel?.send({\n channel: this.definition.channelId,\n chatId,\n content,\n ...(replyTo ? { replyTo } : {}),\n media: [],\n metadata: {\n ...metadata,\n ...(sessionRoute.accountId && !metadata.accountId ? { accountId: sessionRoute.accountId } : {}),\n },\n });\n };\n\n sendOutboundText = async (params: {\n to: string;\n text: string;\n accountId?: string | null;\n replyTo?: string | null;\n media?: string[];\n metadata?: Record<string, unknown>;\n }): Promise<void> => {\n const { accountId, media, metadata, replyTo, text, to } = params;\n const outbound = {\n channel: this.definition.channelId,\n chatId: to,\n content: text,\n ...(replyTo !== undefined ? { replyTo } : {}),\n media: media ?? [],\n metadata: {\n ...(metadata ?? {}),\n ...(accountId ? { accountId } : {}),\n },\n };\n if (await this.channel?.handleControlMessage?.(outbound)) {\n return;\n }\n await this.channel?.send(outbound);\n };\n}\n\nexport async function startChannelExtension<TConfig, TInbound>(\n definition: ChannelExtensionDefinition<TConfig, TInbound>,\n options: NextClawExtensionOptions = {},\n): Promise<void> {\n const extension = new NextClawExtension(options);\n const channel = extension.channels.use(definition.channelId);\n const adapter = new LazyExtensionChannelAdapter(() => definition.createAdapter({ channel }));\n const controller = new ExtensionChannelController({\n channel,\n adapter,\n mapInbound: definition.mapInbound,\n onNcpEventError: definition.onNcpEventError,\n });\n if (definition.createAuthCapability) {\n extension.capabilities.provide(\"channel.auth\", definition.createAuthCapability({ channel }));\n }\n extension.capabilities.provideHandler(\"channel.outbound.sendText\", async (payload) =>\n await controller.sendOutboundText({\n to: readRequiredPayloadString(payload.to, \"to\"),\n text: readRequiredPayloadString(payload.text, \"text\"),\n accountId: readOptionalPayloadString(payload.accountId),\n replyTo: readOptionalPayloadString(payload.replyTo),\n media: readStringArray(payload.media),\n metadata: readOptionalRecord(payload.metadata),\n }),\n );\n await controller.start();\n await extension.ready();\n}\n\nexport async function startBusChannelExtension<TConfig, TBus = BusChannelMessageBus>(\n definition: BusChannelExtensionDefinition<TConfig, TBus>,\n options: NextClawExtensionOptions = {},\n): Promise<void> {\n await startChannelExtension(\n {\n channelId: definition.channelId,\n createAdapter: ({ channel }) => new BusChannelAdapter(definition, channel),\n mapInbound: definition.mapInbound ?? toSubmittedTextMessage,\n },\n options,\n );\n}\n\nexport function warnNcpEventError(channelId: string): (error: unknown) => void {\n return (error) => {\n const message = error instanceof Error ? error.message : String(error);\n console.warn(`[${channelId}] failed to send NCP event: ${message}`);\n };\n}\n\nfunction readRequiredPayloadString(value: unknown, name: string): string {\n if (typeof value !== \"string\" || !value.trim()) {\n throw new Error(`${name} is required`);\n }\n return value.trim();\n}\n\nfunction readOptionalPayloadString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() ? value.trim() : undefined;\n}\n\nfunction readStringArray(value: unknown): string[] | undefined {\n if (!Array.isArray(value)) {\n return undefined;\n }\n return value.filter((item): item is string => typeof item === \"string\");\n}\n\nfunction readOptionalRecord(value: unknown): Record<string, unknown> | undefined {\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? value as Record<string, unknown>\n : undefined;\n}\n\nfunction toSubmittedTextMessage(message: BusChannelInboundMessage): ChannelSubmittedMessageInput {\n return {\n conversationId: message.chatId,\n senderId: message.senderId,\n content: {\n type: \"text\",\n text: message.content,\n },\n ...(message.attachments ? { attachments: message.attachments } : {}),\n ...(message.metadata ? { metadata: message.metadata } : {}),\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"extension-channel-controller.service.js","names":[],"sources":["../../src/services/extension-channel-controller.service.ts"],"sourcesContent":["import { NcpEventType, type NcpEndpointEvent, type NcpMessagePart } from \"@nextclaw/ncp\";\nimport type {\n ChannelSubmittedMessage,\n ChannelSubmittedAttachment,\n ExtensionChannel,\n ExtensionDiagnostics,\n NextClawExtensionOptions,\n} from \"../types/extension-sdk.types.js\";\nimport { NextClawExtension } from \"./extension-client.service.js\";\n\nfunction readRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? value as Record<string, unknown>\n : {};\n}\n\nconst AGENT_ROUTE_KINDS = new Set([\"direct\", \"group\", \"channel\", \"thread\"]);\n\nfunction parseSessionDeliveryRoute(sessionId: string): {\n channel?: string;\n chatId?: string;\n accountId?: string;\n} {\n const parts = sessionId.trim().split(\":\");\n if (parts[0] !== \"agent\" || parts.length < 5) {\n return {};\n }\n const hasAccount = !AGENT_ROUTE_KINDS.has(parts[3]);\n const kind = parts[hasAccount ? 4 : 3];\n if (!AGENT_ROUTE_KINDS.has(kind ?? \"\")) {\n return {};\n }\n return {\n channel: parts[2],\n ...(hasAccount ? { accountId: parts[3] } : {}),\n chatId: parts.slice(hasAccount ? 5 : 4).join(\":\"),\n };\n}\n\nfunction readString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() ? value.trim() : undefined;\n}\n\nfunction renderMessageText(parts: readonly NcpMessagePart[]): string {\n return (parts as Array<{ type: string; text?: string }>).map((part) =>\n (part.type === \"text\" || part.type === \"rich-text\") && part.text ? part.text : \"\",\n ).filter(Boolean).join(\"\\n\\n\");\n}\n\nexport type ExtensionChannelAdapter<TConfig, TInbound> = {\n configure: (config: TConfig) => Promise<void>;\n start: () => Promise<void>;\n stop: () => Promise<void>;\n onMessage: (handler: (message: TInbound) => void | Promise<void>) => () => void;\n sendNcpEvent: (event: NcpEndpointEvent) => Promise<void>;\n sendOutboundText: (params: {\n to: string;\n text: string;\n accountId?: string | null;\n replyTo?: string | null;\n media?: string[];\n metadata?: Record<string, unknown>;\n }) => Promise<void>;\n};\n\nexport type ChannelSubmittedMessageInput = Omit<ChannelSubmittedMessage, \"channelId\">;\n\nexport type BusChannelInboundMessage = {\n channel: string;\n senderId: string;\n chatId: string;\n content: string;\n attachments?: ChannelSubmittedAttachment[];\n metadata?: Record<string, unknown>;\n};\n\nexport type BusChannelMessageBus = {\n publishInbound: (message: BusChannelInboundMessage) => Promise<void>;\n};\n\nexport type BusChannelRuntime = {\n start: () => Promise<void>;\n stop: () => Promise<void>;\n handleControlMessage?: (message: {\n channel: string;\n chatId: string;\n content: string;\n replyTo?: string | null;\n media: string[];\n metadata: Record<string, unknown>;\n }) => Promise<boolean>;\n send: (message: {\n channel: string;\n chatId: string;\n content: string;\n replyTo?: string | null;\n media: string[];\n metadata: Record<string, unknown>;\n }) => Promise<void>;\n};\n\nexport type BusChannelCreateContext<TConfig, TBus = BusChannelMessageBus> = {\n config: TConfig;\n bus: TBus;\n channel: ExtensionChannel;\n diagnostics: ExtensionDiagnostics;\n};\n\nexport type BusChannelExtensionDefinition<TConfig, TBus = BusChannelMessageBus> = {\n channelId: string;\n createChannel: (\n context: BusChannelCreateContext<TConfig, TBus>,\n ) => BusChannelRuntime | Promise<BusChannelRuntime>;\n mapInbound?: ExtensionChannelInboundMapper<BusChannelInboundMessage>;\n onChannelStartError?: (error: unknown) => void | Promise<void>;\n};\n\nexport type ExtensionChannelInboundMapper<TInbound> =\n (message: TInbound) => ChannelSubmittedMessageInput | Promise<ChannelSubmittedMessageInput>;\n\ntype ExtensionChannelControllerOptions<TConfig, TInbound> = {\n channel: ExtensionChannel;\n adapter: ExtensionChannelAdapter<TConfig, TInbound>;\n mapInbound: ExtensionChannelInboundMapper<TInbound>;\n onNcpEventError?: (error: unknown, event: NcpEndpointEvent) => void | Promise<void>;\n};\n\nexport type ChannelExtensionContext = {\n channel: ExtensionChannel;\n diagnostics: ExtensionDiagnostics;\n};\n\nexport type ChannelExtensionDefinition<TConfig, TInbound> = {\n channelId: string;\n createAdapter: (\n context: ChannelExtensionContext,\n ) => ExtensionChannelAdapter<TConfig, TInbound> | Promise<ExtensionChannelAdapter<TConfig, TInbound>>;\n mapInbound: ExtensionChannelInboundMapper<TInbound>;\n createAuthCapability?: (context: ChannelExtensionContext) => object;\n onNcpEventError?: (error: unknown, event: NcpEndpointEvent) => void | Promise<void>;\n};\n\nfunction isChannelEnabled(config: unknown): boolean {\n return Boolean(\n config &&\n typeof config === \"object\" &&\n !Array.isArray(config) &&\n (config as { enabled?: unknown }).enabled === true\n );\n}\n\nclass LazyExtensionChannelAdapter<TConfig, TInbound> implements ExtensionChannelAdapter<TConfig, TInbound> {\n private adapterCleanup: (() => void) | null = null;\n private adapterPromise: Promise<ExtensionChannelAdapter<TConfig, TInbound>> | null = null;\n private messageHandler: ((message: TInbound) => void | Promise<void>) | null = null;\n\n constructor(\n private readonly createAdapter: () =>\n ExtensionChannelAdapter<TConfig, TInbound> | Promise<ExtensionChannelAdapter<TConfig, TInbound>>,\n ) {}\n\n configure = async (config: TConfig): Promise<void> => {\n await (await this.getAdapter()).configure(config);\n };\n\n start = async (): Promise<void> => {\n await (await this.getAdapter()).start();\n };\n\n stop = async (): Promise<void> => {\n if (this.adapterPromise) {\n await (await this.adapterPromise).stop();\n }\n };\n\n onMessage = (handler: (message: TInbound) => void | Promise<void>): (() => void) => {\n this.messageHandler = handler;\n if (this.adapterPromise) {\n void this.adapterPromise.then((adapter) => this.attachMessageHandler(adapter)).catch(() => undefined);\n }\n return () => {\n if (this.messageHandler === handler) {\n this.messageHandler = null;\n this.adapterCleanup?.();\n this.adapterCleanup = null;\n }\n };\n };\n\n sendNcpEvent = async (event: NcpEndpointEvent): Promise<void> => {\n if (this.adapterPromise) {\n await (await this.adapterPromise).sendNcpEvent(event);\n }\n };\n\n sendOutboundText = async (params: {\n to: string;\n text: string;\n accountId?: string | null;\n replyTo?: string | null;\n media?: string[];\n metadata?: Record<string, unknown>;\n }): Promise<void> => {\n await (await this.getAdapter()).sendOutboundText(params);\n };\n\n private getAdapter = async (): Promise<ExtensionChannelAdapter<TConfig, TInbound>> => {\n this.adapterPromise ??= Promise.resolve(this.createAdapter()).then((adapter) => {\n this.attachMessageHandler(adapter);\n return adapter;\n });\n return await this.adapterPromise;\n };\n\n private attachMessageHandler = (adapter: ExtensionChannelAdapter<TConfig, TInbound>): void => {\n this.adapterCleanup?.();\n this.adapterCleanup = this.messageHandler ? adapter.onMessage(this.messageHandler) : null;\n };\n}\n\nexport class ExtensionChannelController<TConfig, TInbound> {\n private readonly cleanups: Array<() => void> = [];\n private started = false;\n\n constructor(\n private readonly options: ExtensionChannelControllerOptions<TConfig, TInbound>,\n ) {}\n\n start = async (): Promise<void> => {\n if (this.started) {\n return;\n }\n this.started = true;\n this.cleanups.push(this.options.adapter.onMessage(this.submitMessage));\n this.cleanups.push(this.options.channel.onNcpEvent(this.sendNcpEvent));\n this.cleanups.push(this.options.channel.config.onChange(async () => {\n await this.applyConfig();\n }));\n await this.applyConfig();\n };\n\n stop = async (): Promise<void> => {\n if (!this.started) {\n return;\n }\n this.started = false;\n this.drainCleanups();\n await this.options.adapter.stop();\n };\n\n sendOutboundText = async (params: {\n to: string;\n text: string;\n accountId?: string | null;\n replyTo?: string | null;\n media?: string[];\n metadata?: Record<string, unknown>;\n }): Promise<{ accepted: true }> => {\n await this.options.adapter.sendOutboundText(params);\n return { accepted: true };\n };\n\n private applyConfig = async (): Promise<void> => {\n const config = await this.options.channel.config.get<TConfig>();\n if (!isChannelEnabled(config)) {\n await this.options.adapter.stop();\n return;\n }\n await this.options.adapter.configure(config);\n await this.options.adapter.start();\n };\n\n private submitMessage = async (message: TInbound): Promise<void> => {\n await this.options.channel.submitMessage(await this.options.mapInbound(message));\n };\n\n private sendNcpEvent = async (event: NcpEndpointEvent): Promise<void> => {\n try {\n await this.options.adapter.sendNcpEvent(event);\n } catch (error) {\n await this.options.onNcpEventError?.(error, event);\n }\n };\n\n private drainCleanups = (): void => {\n for (const cleanup of this.cleanups.splice(0).reverse()) {\n cleanup();\n }\n };\n}\n\nclass BusChannelAdapter<TConfig, TBus> implements ExtensionChannelAdapter<TConfig, BusChannelInboundMessage> {\n private channel: BusChannelRuntime | null = null;\n private messageHandler: ((message: BusChannelInboundMessage) => void | Promise<void>) | null = null;\n private readonly definition: BusChannelExtensionDefinition<TConfig, TBus>;\n private readonly diagnostics: ExtensionDiagnostics;\n private readonly extensionChannel: ExtensionChannel;\n\n constructor(params: {\n definition: BusChannelExtensionDefinition<TConfig, TBus>;\n diagnostics: ExtensionDiagnostics;\n extensionChannel: ExtensionChannel;\n }) {\n this.definition = params.definition;\n this.diagnostics = params.diagnostics;\n this.extensionChannel = params.extensionChannel;\n }\n\n configure = async (config: TConfig): Promise<void> => {\n await this.stop();\n this.channel = await this.definition.createChannel({\n config,\n bus: {\n publishInbound: async (message: BusChannelInboundMessage) => {\n await this.messageHandler?.(message);\n },\n } as TBus,\n channel: this.extensionChannel,\n diagnostics: this.diagnostics,\n });\n };\n\n start = async (): Promise<void> => {\n const channel = this.channel;\n if (!channel) {\n return;\n }\n void channel.start().catch(async (error: unknown) => {\n await this.definition.onChannelStartError?.(error);\n });\n };\n\n stop = async (): Promise<void> => {\n const channel = this.channel;\n this.channel = null;\n await channel?.stop();\n };\n\n onMessage = (\n handler: (message: BusChannelInboundMessage) => void | Promise<void>,\n ): (() => void) => {\n this.messageHandler = handler;\n return () => {\n if (this.messageHandler === handler) {\n this.messageHandler = null;\n }\n };\n };\n\n sendNcpEvent = async (event: NcpEndpointEvent): Promise<void> => {\n if (event.type !== NcpEventType.MessageCompleted) {\n return;\n }\n const metadata = readRecord(event.payload.message.metadata);\n const sessionRoute = parseSessionDeliveryRoute(event.payload.message.sessionId);\n const eventChannel = readString(metadata.channel) ?? readString(metadata.channelId) ?? sessionRoute.channel;\n if (eventChannel !== this.definition.channelId) {\n return;\n }\n const chatId = readString(metadata.chatId) ?? readString(metadata.chat_id) ?? sessionRoute.chatId;\n const content = renderMessageText(event.payload.message.parts).trim();\n if (!chatId || !content) {\n return;\n }\n const replyTo = readString(metadata.message_id);\n await this.channel?.send({\n channel: this.definition.channelId,\n chatId,\n content,\n ...(replyTo ? { replyTo } : {}),\n media: [],\n metadata: {\n ...metadata,\n ...(sessionRoute.accountId && !metadata.accountId ? { accountId: sessionRoute.accountId } : {}),\n },\n });\n };\n\n sendOutboundText = async (params: {\n to: string;\n text: string;\n accountId?: string | null;\n replyTo?: string | null;\n media?: string[];\n metadata?: Record<string, unknown>;\n }): Promise<void> => {\n const { accountId, media, metadata, replyTo, text, to } = params;\n const outbound = {\n channel: this.definition.channelId,\n chatId: to,\n content: text,\n ...(replyTo !== undefined ? { replyTo } : {}),\n media: media ?? [],\n metadata: {\n ...(metadata ?? {}),\n ...(accountId ? { accountId } : {}),\n },\n };\n if (await this.channel?.handleControlMessage?.(outbound)) {\n return;\n }\n await this.channel?.send(outbound);\n };\n}\n\nexport async function startChannelExtension<TConfig, TInbound>(\n definition: ChannelExtensionDefinition<TConfig, TInbound>,\n options: NextClawExtensionOptions = {},\n): Promise<void> {\n const extension = new NextClawExtension(options);\n const channel = extension.channels.use(definition.channelId);\n const adapter = new LazyExtensionChannelAdapter(() => definition.createAdapter({\n channel,\n diagnostics: extension.diagnostics,\n }));\n const controller = new ExtensionChannelController({\n channel,\n adapter,\n mapInbound: definition.mapInbound,\n onNcpEventError: definition.onNcpEventError,\n });\n if (definition.createAuthCapability) {\n extension.capabilities.provide(\"channel.auth\", definition.createAuthCapability({\n channel,\n diagnostics: extension.diagnostics,\n }));\n }\n extension.capabilities.provideHandler(\"channel.outbound.sendText\", async (payload) =>\n await controller.sendOutboundText({\n to: readRequiredPayloadString(payload.to, \"to\"),\n text: readRequiredPayloadString(payload.text, \"text\"),\n accountId: readOptionalPayloadString(payload.accountId),\n replyTo: readOptionalPayloadString(payload.replyTo),\n media: readStringArray(payload.media),\n metadata: readOptionalRecord(payload.metadata),\n }),\n );\n await controller.start();\n await extension.ready();\n}\n\nexport async function startBusChannelExtension<TConfig, TBus = BusChannelMessageBus>(\n definition: BusChannelExtensionDefinition<TConfig, TBus>,\n options: NextClawExtensionOptions = {},\n): Promise<void> {\n await startChannelExtension(\n {\n channelId: definition.channelId,\n createAdapter: ({ channel, diagnostics }) => new BusChannelAdapter({\n definition,\n diagnostics,\n extensionChannel: channel,\n }),\n mapInbound: definition.mapInbound ?? toSubmittedTextMessage,\n },\n options,\n );\n}\n\nexport function warnNcpEventError(channelId: string): (error: unknown) => void {\n return (error) => {\n const message = error instanceof Error ? error.message : String(error);\n console.warn(`[${channelId}] failed to send NCP event: ${message}`);\n };\n}\n\nfunction readRequiredPayloadString(value: unknown, name: string): string {\n if (typeof value !== \"string\" || !value.trim()) {\n throw new Error(`${name} is required`);\n }\n return value.trim();\n}\n\nfunction readOptionalPayloadString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() ? value.trim() : undefined;\n}\n\nfunction readStringArray(value: unknown): string[] | undefined {\n if (!Array.isArray(value)) {\n return undefined;\n }\n return value.filter((item): item is string => typeof item === \"string\");\n}\n\nfunction readOptionalRecord(value: unknown): Record<string, unknown> | undefined {\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? value as Record<string, unknown>\n : undefined;\n}\n\nfunction toSubmittedTextMessage(message: BusChannelInboundMessage): ChannelSubmittedMessageInput {\n return {\n conversationId: message.chatId,\n senderId: message.senderId,\n content: {\n type: \"text\",\n text: message.content,\n },\n ...(message.attachments ? { attachments: message.attachments } : {}),\n ...(message.metadata ? { metadata: message.metadata } : {}),\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ExtensionCapabilities, ExtensionChannels, ExtensionRequestHandler, NextClawExtensionOptions } from "../types/extension-sdk.types.js";
|
|
1
|
+
import { ExtensionCapabilities, ExtensionChannels, ExtensionDiagnostics, ExtensionRequestHandler, NextClawExtensionOptions } from "../types/extension-sdk.types.js";
|
|
2
2
|
import { EventBus } from "@nextclaw/shared";
|
|
3
3
|
|
|
4
4
|
//#region src/services/extension-client.service.d.ts
|
|
@@ -6,6 +6,7 @@ declare class NextClawExtension {
|
|
|
6
6
|
readonly eventBus: EventBus;
|
|
7
7
|
readonly channels: ExtensionChannels;
|
|
8
8
|
readonly capabilities: ExtensionCapabilities;
|
|
9
|
+
readonly diagnostics: ExtensionDiagnostics;
|
|
9
10
|
readonly extensionId: string;
|
|
10
11
|
readonly generation: string;
|
|
11
12
|
private readonly transport;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extension-client.service.d.ts","names":[],"sources":["../../src/services/extension-client.service.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"extension-client.service.d.ts","names":[],"sources":["../../src/services/extension-client.service.ts"],"mappings":";;;;cAwNa,iBAAA;EAAA,SACF,QAAA,EAAU,QAAA;EAAA,SACV,QAAA,EAAU,iBAAA;EAAA,SACV,YAAA,EAAc,qBAAA;EAAA,SACd,WAAA,EAAa,oBAAA;EAAA,SACb,WAAA;EAAA,SACA,UAAA;EAAA,iBACQ,SAAA;EAAA,QACT,uBAAA;EAAA,QACA,oBAAA;cAEI,OAAA,GAAS,wBAAA;EAAA,SA+BZ,KAAA;EAAA,SASA,KAAA,QAAkB,OAAA;EAAA,SASlB,SAAA,GAAa,OAAA,EAAS,uBAAA;EAAA,iBAYd,kBAAA;EAAA,iBAMA,yBAAA;AAAA"}
|
|
@@ -1,8 +1,38 @@
|
|
|
1
1
|
import { ExtensionChannelService } from "./extension-channel.service.js";
|
|
2
2
|
import { ExtensionTransportService } from "./extension-transport.service.js";
|
|
3
|
-
import { EventBus } from "@nextclaw/shared";
|
|
3
|
+
import { EventBus, getKeyId, ingressKeys } from "@nextclaw/shared";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
5
|
//#region src/services/extension-client.service.ts
|
|
5
6
|
const EXTENSION_PARENT_WATCH_INTERVAL_MS = 1e3;
|
|
7
|
+
var ExtensionDiagnosticClient = class {
|
|
8
|
+
constructor(transport, timeoutMs) {
|
|
9
|
+
this.transport = transport;
|
|
10
|
+
this.timeoutMs = timeoutMs;
|
|
11
|
+
}
|
|
12
|
+
createTraceId = (stableId) => {
|
|
13
|
+
const normalizedStableId = stableId?.trim();
|
|
14
|
+
if (!normalizedStableId) return randomUUID();
|
|
15
|
+
return createHash("sha256").update(normalizedStableId).digest("hex").slice(0, 24);
|
|
16
|
+
};
|
|
17
|
+
emit = async (input) => {
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
let timeout;
|
|
20
|
+
try {
|
|
21
|
+
await Promise.race([this.transport.postIngress(getKeyId(ingressKeys.extension.diagnosticEmit), input, { signal: controller.signal }), new Promise((_resolve, reject) => {
|
|
22
|
+
timeout = setTimeout(() => {
|
|
23
|
+
controller.abort();
|
|
24
|
+
reject(/* @__PURE__ */ new Error("Extension diagnostic emission timed out."));
|
|
25
|
+
}, this.timeoutMs);
|
|
26
|
+
timeout.unref?.();
|
|
27
|
+
})]);
|
|
28
|
+
return true;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
} finally {
|
|
32
|
+
if (timeout) clearTimeout(timeout);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
};
|
|
6
36
|
var ExtensionChannelRegistry = class {
|
|
7
37
|
channels = /* @__PURE__ */ new Map();
|
|
8
38
|
constructor(params) {
|
|
@@ -96,6 +126,7 @@ var NextClawExtension = class {
|
|
|
96
126
|
eventBus;
|
|
97
127
|
channels;
|
|
98
128
|
capabilities;
|
|
129
|
+
diagnostics;
|
|
99
130
|
extensionId;
|
|
100
131
|
generation;
|
|
101
132
|
transport;
|
|
@@ -125,6 +156,7 @@ var NextClawExtension = class {
|
|
|
125
156
|
extensionId: this.extensionId,
|
|
126
157
|
transport: this.transport
|
|
127
158
|
});
|
|
159
|
+
this.diagnostics = new ExtensionDiagnosticClient(this.transport, Math.max(50, options.diagnosticTimeoutMs ?? 2e3));
|
|
128
160
|
this.parentProcessWatcher = this.startParentProcessWatcher();
|
|
129
161
|
}
|
|
130
162
|
close = () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extension-client.service.js","names":[],"sources":["../../src/services/extension-client.service.ts"],"sourcesContent":["import { EventBus } from \"@nextclaw/shared\";\nimport type {\n ExtensionCapabilities,\n ExtensionCapabilityHandler,\n ExtensionChannels,\n ExtensionRequest,\n ExtensionRequestHandler,\n ExtensionTransportEnvelope,\n NextClawExtensionOptions,\n} from \"../types/extension-sdk.types.js\";\nimport { ExtensionChannelService } from \"./extension-channel.service.js\";\nimport { ExtensionTransportService } from \"./extension-transport.service.js\";\n\nconst EXTENSION_PARENT_WATCH_INTERVAL_MS = 1000;\n\nclass ExtensionChannelRegistry implements ExtensionChannels {\n private readonly channels = new Map<string, ExtensionChannelService>();\n\n constructor(\n private readonly params: {\n eventBus: EventBus;\n transport: ExtensionTransportService;\n },\n ) {}\n\n readonly use = (channelId: string): ExtensionChannelService => {\n const normalizedChannelId = channelId.trim();\n if (!normalizedChannelId) {\n throw new Error(\"channelId is required.\");\n }\n const existing = this.channels.get(normalizedChannelId);\n if (existing) {\n return existing;\n }\n const channel = new ExtensionChannelService({\n channelId: normalizedChannelId,\n eventBus: this.params.eventBus,\n transport: this.params.transport,\n });\n this.channels.set(normalizedChannelId, channel);\n return channel;\n };\n}\n\nfunction readRequest(payload: unknown): ExtensionRequest | null {\n if (!payload || typeof payload !== \"object\" || Array.isArray(payload)) {\n return null;\n }\n const record = payload as Record<string, unknown>;\n if (\n typeof record.requestId !== \"string\" ||\n typeof record.extensionId !== \"string\" ||\n typeof record.generation !== \"string\" ||\n typeof record.kind !== \"string\"\n ) {\n return null;\n }\n return {\n requestId: record.requestId,\n extensionId: record.extensionId,\n generation: record.generation,\n kind: record.kind,\n payload: record.payload && typeof record.payload === \"object\" && !Array.isArray(record.payload)\n ? record.payload as Record<string, unknown>\n : {},\n };\n}\n\nfunction normalizeName(value: string, name: string): string {\n const normalized = value.trim();\n if (!normalized) {\n throw new Error(`${name} is required.`);\n }\n return normalized;\n}\n\nasync function handleRequest(\n transport: ExtensionTransportService,\n request: ExtensionRequest,\n handler: ExtensionRequestHandler,\n): Promise<void> {\n try {\n const data = await handler(request);\n await transport.respondToRequest({\n requestId: request.requestId,\n ok: true,\n data,\n });\n } catch (error) {\n await transport.respondToRequest({\n requestId: request.requestId,\n ok: false,\n error: {\n message: error instanceof Error ? error.message : String(error),\n },\n });\n }\n}\n\nfunction readParentProcessId(): number | null {\n const raw = process.env.NEXTCLAW_EXTENSION_PARENT_PID?.trim();\n if (!raw) {\n return null;\n }\n const pid = Number(raw);\n return Number.isInteger(pid) && pid > 0 ? pid : null;\n}\n\nfunction isProcessRunning(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\nclass ExtensionCapabilityRegistry implements ExtensionCapabilities {\n constructor(\n private readonly params: {\n eventBus: EventBus;\n extensionId: string;\n transport: ExtensionTransportService;\n },\n ) {}\n\n readonly provide = (namespace: string, capability: object): (() => void) => {\n const normalizedNamespace = normalizeName(namespace, \"namespace\");\n const unsubscribeHandlers = this.readHandlers(capability).map(([name, handler]) =>\n this.provideHandler(`${normalizedNamespace}.${name}`, handler),\n );\n if (unsubscribeHandlers.length === 0) {\n throw new Error(`capability '${normalizedNamespace}' has no callable methods.`);\n }\n return () => {\n for (const unsubscribe of unsubscribeHandlers) {\n unsubscribe();\n }\n };\n };\n\n readonly provideHandler = (kind: string, handler: ExtensionCapabilityHandler): (() => void) => {\n const normalizedKind = normalizeName(kind, \"kind\");\n return this.params.eventBus.subscribeAll((event) => {\n if (event.type !== \"extension.request\") {\n return;\n }\n const request = readRequest(event.payload);\n if (\n !request ||\n request.extensionId !== this.params.extensionId ||\n request.generation !== this.params.transport.generation ||\n request.kind !== normalizedKind\n ) {\n return;\n }\n void handleRequest(this.params.transport, request, async (matchedRequest) =>\n await handler(matchedRequest.payload ?? {}, matchedRequest),\n );\n });\n };\n\n private readonly readHandlers = (capability: object): Array<[string, ExtensionCapabilityHandler]> =>\n Object.entries(capability).filter(\n (entry): entry is [string, ExtensionCapabilityHandler] => typeof entry[1] === \"function\",\n );\n}\n\nexport class NextClawExtension {\n readonly eventBus: EventBus;\n readonly channels: ExtensionChannels;\n readonly capabilities: ExtensionCapabilities;\n readonly extensionId: string;\n readonly generation: string;\n private readonly transport: ExtensionTransportService;\n private eventStreamSubscription: { close: () => void; ready: Promise<void> } | null = null;\n private parentProcessWatcher: ReturnType<typeof setInterval> | null = null;\n\n constructor(options: NextClawExtensionOptions = {}) {\n this.transport = new ExtensionTransportService(options);\n this.extensionId = this.transport.extensionId;\n this.generation = this.transport.generation;\n this.eventBus = new EventBus({\n onFirstSubscriber: () => {\n this.eventStreamSubscription ??= this.transport.subscribe((event) => {\n this.eventBus.emitEnvelope(this.toEventBusEnvelope(event));\n });\n },\n onNoSubscribers: () => {\n this.eventStreamSubscription?.close();\n this.eventStreamSubscription = null;\n },\n });\n this.channels = new ExtensionChannelRegistry({\n eventBus: this.eventBus,\n transport: this.transport,\n });\n this.capabilities = new ExtensionCapabilityRegistry({\n eventBus: this.eventBus,\n extensionId: this.extensionId,\n transport: this.transport,\n });\n this.parentProcessWatcher = this.startParentProcessWatcher();\n }\n\n readonly close = (): void => {\n if (this.parentProcessWatcher) {\n clearInterval(this.parentProcessWatcher);\n this.parentProcessWatcher = null;\n }\n this.eventStreamSubscription?.close();\n this.eventStreamSubscription = null;\n };\n\n readonly ready = async (): Promise<void> => {\n const subscription = this.eventStreamSubscription;\n if (!subscription) {\n throw new Error(`Extension ${this.extensionId} has no event-stream subscription.`);\n }\n await subscription.ready;\n await this.transport.reportReady(process.pid);\n };\n\n readonly onRequest = (handler: ExtensionRequestHandler): (() => void) =>\n this.eventBus.subscribeAll((event) => {\n if (event.type !== \"extension.request\") {\n return;\n }\n const request = readRequest(event.payload);\n if (!request || request.extensionId !== this.extensionId || request.generation !== this.generation) {\n return;\n }\n void handleRequest(this.transport, request, handler);\n });\n\n private readonly toEventBusEnvelope = (event: ExtensionTransportEnvelope): ExtensionTransportEnvelope => ({\n ...event,\n emittedAt: event.emittedAt ?? new Date().toISOString(),\n source: event.source ?? \"event-stream\",\n });\n\n private readonly startParentProcessWatcher = (): ReturnType<typeof setInterval> | null => {\n const parentPid = readParentProcessId();\n if (!parentPid) {\n return null;\n }\n const timer = setInterval(() => {\n if (isProcessRunning(parentPid)) {\n return;\n }\n this.close();\n process.exit(0);\n }, EXTENSION_PARENT_WATCH_INTERVAL_MS);\n (timer as NodeJS.Timeout).unref?.();\n return timer;\n };\n\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"extension-client.service.js","names":[],"sources":["../../src/services/extension-client.service.ts"],"sourcesContent":["import { EventBus, getKeyId, ingressKeys } from \"@nextclaw/shared\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport type {\n ExtensionCapabilities,\n ExtensionCapabilityHandler,\n ExtensionChannels,\n ExtensionDiagnostics,\n ExtensionRequest,\n ExtensionRequestHandler,\n ExtensionTransportEnvelope,\n NextClawExtensionOptions,\n} from \"../types/extension-sdk.types.js\";\nimport { ExtensionChannelService } from \"./extension-channel.service.js\";\nimport { ExtensionTransportService } from \"./extension-transport.service.js\";\n\nconst EXTENSION_PARENT_WATCH_INTERVAL_MS = 1000;\n\nclass ExtensionDiagnosticClient implements ExtensionDiagnostics {\n constructor(\n private readonly transport: ExtensionTransportService,\n private readonly timeoutMs: number,\n ) {}\n\n readonly createTraceId = (stableId?: string): string => {\n const normalizedStableId = stableId?.trim();\n if (!normalizedStableId) {\n return randomUUID();\n }\n return createHash(\"sha256\")\n .update(normalizedStableId)\n .digest(\"hex\")\n .slice(0, 24);\n };\n\n readonly emit: ExtensionDiagnostics[\"emit\"] = async (input) => {\n const controller = new AbortController();\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n await Promise.race([\n this.transport.postIngress(\n getKeyId(ingressKeys.extension.diagnosticEmit),\n input,\n { signal: controller.signal },\n ),\n new Promise<never>((_resolve, reject) => {\n timeout = setTimeout(() => {\n controller.abort();\n reject(new Error(\"Extension diagnostic emission timed out.\"));\n }, this.timeoutMs);\n timeout.unref?.();\n }),\n ]);\n return true;\n } catch {\n return false;\n } finally {\n if (timeout) {\n clearTimeout(timeout);\n }\n }\n };\n}\n\nclass ExtensionChannelRegistry implements ExtensionChannels {\n private readonly channels = new Map<string, ExtensionChannelService>();\n\n constructor(\n private readonly params: {\n eventBus: EventBus;\n transport: ExtensionTransportService;\n },\n ) {}\n\n readonly use = (channelId: string): ExtensionChannelService => {\n const normalizedChannelId = channelId.trim();\n if (!normalizedChannelId) {\n throw new Error(\"channelId is required.\");\n }\n const existing = this.channels.get(normalizedChannelId);\n if (existing) {\n return existing;\n }\n const channel = new ExtensionChannelService({\n channelId: normalizedChannelId,\n eventBus: this.params.eventBus,\n transport: this.params.transport,\n });\n this.channels.set(normalizedChannelId, channel);\n return channel;\n };\n}\n\nfunction readRequest(payload: unknown): ExtensionRequest | null {\n if (!payload || typeof payload !== \"object\" || Array.isArray(payload)) {\n return null;\n }\n const record = payload as Record<string, unknown>;\n if (\n typeof record.requestId !== \"string\" ||\n typeof record.extensionId !== \"string\" ||\n typeof record.generation !== \"string\" ||\n typeof record.kind !== \"string\"\n ) {\n return null;\n }\n return {\n requestId: record.requestId,\n extensionId: record.extensionId,\n generation: record.generation,\n kind: record.kind,\n payload: record.payload && typeof record.payload === \"object\" && !Array.isArray(record.payload)\n ? record.payload as Record<string, unknown>\n : {},\n };\n}\n\nfunction normalizeName(value: string, name: string): string {\n const normalized = value.trim();\n if (!normalized) {\n throw new Error(`${name} is required.`);\n }\n return normalized;\n}\n\nasync function handleRequest(\n transport: ExtensionTransportService,\n request: ExtensionRequest,\n handler: ExtensionRequestHandler,\n): Promise<void> {\n try {\n const data = await handler(request);\n await transport.respondToRequest({\n requestId: request.requestId,\n ok: true,\n data,\n });\n } catch (error) {\n await transport.respondToRequest({\n requestId: request.requestId,\n ok: false,\n error: {\n message: error instanceof Error ? error.message : String(error),\n },\n });\n }\n}\n\nfunction readParentProcessId(): number | null {\n const raw = process.env.NEXTCLAW_EXTENSION_PARENT_PID?.trim();\n if (!raw) {\n return null;\n }\n const pid = Number(raw);\n return Number.isInteger(pid) && pid > 0 ? pid : null;\n}\n\nfunction isProcessRunning(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\nclass ExtensionCapabilityRegistry implements ExtensionCapabilities {\n constructor(\n private readonly params: {\n eventBus: EventBus;\n extensionId: string;\n transport: ExtensionTransportService;\n },\n ) {}\n\n readonly provide = (namespace: string, capability: object): (() => void) => {\n const normalizedNamespace = normalizeName(namespace, \"namespace\");\n const unsubscribeHandlers = this.readHandlers(capability).map(([name, handler]) =>\n this.provideHandler(`${normalizedNamespace}.${name}`, handler),\n );\n if (unsubscribeHandlers.length === 0) {\n throw new Error(`capability '${normalizedNamespace}' has no callable methods.`);\n }\n return () => {\n for (const unsubscribe of unsubscribeHandlers) {\n unsubscribe();\n }\n };\n };\n\n readonly provideHandler = (kind: string, handler: ExtensionCapabilityHandler): (() => void) => {\n const normalizedKind = normalizeName(kind, \"kind\");\n return this.params.eventBus.subscribeAll((event) => {\n if (event.type !== \"extension.request\") {\n return;\n }\n const request = readRequest(event.payload);\n if (\n !request ||\n request.extensionId !== this.params.extensionId ||\n request.generation !== this.params.transport.generation ||\n request.kind !== normalizedKind\n ) {\n return;\n }\n void handleRequest(this.params.transport, request, async (matchedRequest) =>\n await handler(matchedRequest.payload ?? {}, matchedRequest),\n );\n });\n };\n\n private readonly readHandlers = (capability: object): Array<[string, ExtensionCapabilityHandler]> =>\n Object.entries(capability).filter(\n (entry): entry is [string, ExtensionCapabilityHandler] => typeof entry[1] === \"function\",\n );\n}\n\nexport class NextClawExtension {\n readonly eventBus: EventBus;\n readonly channels: ExtensionChannels;\n readonly capabilities: ExtensionCapabilities;\n readonly diagnostics: ExtensionDiagnostics;\n readonly extensionId: string;\n readonly generation: string;\n private readonly transport: ExtensionTransportService;\n private eventStreamSubscription: { close: () => void; ready: Promise<void> } | null = null;\n private parentProcessWatcher: ReturnType<typeof setInterval> | null = null;\n\n constructor(options: NextClawExtensionOptions = {}) {\n this.transport = new ExtensionTransportService(options);\n this.extensionId = this.transport.extensionId;\n this.generation = this.transport.generation;\n this.eventBus = new EventBus({\n onFirstSubscriber: () => {\n this.eventStreamSubscription ??= this.transport.subscribe((event) => {\n this.eventBus.emitEnvelope(this.toEventBusEnvelope(event));\n });\n },\n onNoSubscribers: () => {\n this.eventStreamSubscription?.close();\n this.eventStreamSubscription = null;\n },\n });\n this.channels = new ExtensionChannelRegistry({\n eventBus: this.eventBus,\n transport: this.transport,\n });\n this.capabilities = new ExtensionCapabilityRegistry({\n eventBus: this.eventBus,\n extensionId: this.extensionId,\n transport: this.transport,\n });\n this.diagnostics = new ExtensionDiagnosticClient(\n this.transport,\n Math.max(50, options.diagnosticTimeoutMs ?? 2_000),\n );\n this.parentProcessWatcher = this.startParentProcessWatcher();\n }\n\n readonly close = (): void => {\n if (this.parentProcessWatcher) {\n clearInterval(this.parentProcessWatcher);\n this.parentProcessWatcher = null;\n }\n this.eventStreamSubscription?.close();\n this.eventStreamSubscription = null;\n };\n\n readonly ready = async (): Promise<void> => {\n const subscription = this.eventStreamSubscription;\n if (!subscription) {\n throw new Error(`Extension ${this.extensionId} has no event-stream subscription.`);\n }\n await subscription.ready;\n await this.transport.reportReady(process.pid);\n };\n\n readonly onRequest = (handler: ExtensionRequestHandler): (() => void) =>\n this.eventBus.subscribeAll((event) => {\n if (event.type !== \"extension.request\") {\n return;\n }\n const request = readRequest(event.payload);\n if (!request || request.extensionId !== this.extensionId || request.generation !== this.generation) {\n return;\n }\n void handleRequest(this.transport, request, handler);\n });\n\n private readonly toEventBusEnvelope = (event: ExtensionTransportEnvelope): ExtensionTransportEnvelope => ({\n ...event,\n emittedAt: event.emittedAt ?? new Date().toISOString(),\n source: event.source ?? \"event-stream\",\n });\n\n private readonly startParentProcessWatcher = (): ReturnType<typeof setInterval> | null => {\n const parentPid = readParentProcessId();\n if (!parentPid) {\n return null;\n }\n const timer = setInterval(() => {\n if (isProcessRunning(parentPid)) {\n return;\n }\n this.close();\n process.exit(0);\n }, EXTENSION_PARENT_WATCH_INTERVAL_MS);\n (timer as NodeJS.Timeout).unref?.();\n return timer;\n };\n\n}\n"],"mappings":"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"}
|
|
@@ -27,7 +27,7 @@ var ExtensionTransportService = class {
|
|
|
27
27
|
this.webSocketFactory = options.webSocketFactory;
|
|
28
28
|
if (typeof this.fetchImpl !== "function") throw new Error("fetch is unavailable. Provide fetch when creating the extension.");
|
|
29
29
|
}
|
|
30
|
-
postIngress = async (type, payload) => {
|
|
30
|
+
postIngress = async (type, payload, options = {}) => {
|
|
31
31
|
const envelope = {
|
|
32
32
|
type,
|
|
33
33
|
extensionId: this.extensionId,
|
|
@@ -42,10 +42,15 @@ var ExtensionTransportService = class {
|
|
|
42
42
|
"content-type": "application/json",
|
|
43
43
|
authorization: `Bearer ${this.token}`
|
|
44
44
|
},
|
|
45
|
-
body: JSON.stringify(envelope)
|
|
45
|
+
body: JSON.stringify(envelope),
|
|
46
|
+
signal: options.signal
|
|
46
47
|
});
|
|
47
48
|
const body = await response.json().catch(() => null);
|
|
48
|
-
if (!response.ok)
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
const error = new Error(this.readErrorMessage(body, `NextClaw ingress failed with ${response.status}`));
|
|
51
|
+
Object.assign(error, { status: response.status });
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
49
54
|
return this.readResponseData(body);
|
|
50
55
|
};
|
|
51
56
|
respondToRequest = async (response) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extension-transport.service.js","names":[],"sources":["../../src/services/extension-transport.service.ts"],"sourcesContent":["import type {\n ExtensionRequestResponse,\n ExtensionTransportEnvelope,\n NextClawExtensionOptions,\n NextClawExtensionWebSocketFactoryOptions,\n NextClawExtensionWebSocketLike,\n} from \"../types/extension-sdk.types.js\";\nimport { getKeyId, ingressKeys } from \"@nextclaw/shared\";\nimport { normalizeEndpoint, resolveWebSocketUrl } from \"../utils/extension-url.utils.js\";\nimport WebSocket from \"ws\";\n\ntype EventStreamHandler = (event: ExtensionTransportEnvelope) => void;\n\ntype RuntimeEnv = {\n NEXTCLAW_EXTENSION_ENDPOINT?: string;\n NEXTCLAW_EXTENSION_GENERATION?: string;\n NEXTCLAW_EXTENSION_TOKEN?: string;\n NEXTCLAW_EXTENSION_ID?: string;\n};\n\ndeclare const process: { env: RuntimeEnv } | undefined;\n\nfunction readRuntimeEnv(): RuntimeEnv {\n return typeof process === \"undefined\" ? {} : process.env;\n}\n\nfunction requireRuntimeValue(value: string | undefined, name: string): string {\n const trimmed = value?.trim();\n if (!trimmed) {\n throw new Error(`${name} is required.`);\n }\n return trimmed;\n}\n\nexport class ExtensionTransportService {\n readonly generation: string;\n readonly token: string;\n readonly extensionId: string;\n private readonly endpoint: string;\n private readonly fetchImpl: typeof fetch;\n private readonly webSocketFactory?: (\n url: string,\n options?: NextClawExtensionWebSocketFactoryOptions,\n ) => NextClawExtensionWebSocketLike;\n\n constructor(options: NextClawExtensionOptions = {}) {\n const env = readRuntimeEnv();\n this.endpoint = normalizeEndpoint(\n options.endpoint ?? requireRuntimeValue(env.NEXTCLAW_EXTENSION_ENDPOINT, \"NEXTCLAW_EXTENSION_ENDPOINT\"),\n );\n this.token = options.token ?? requireRuntimeValue(env.NEXTCLAW_EXTENSION_TOKEN, \"NEXTCLAW_EXTENSION_TOKEN\");\n this.extensionId =\n options.extensionId ?? requireRuntimeValue(env.NEXTCLAW_EXTENSION_ID, \"NEXTCLAW_EXTENSION_ID\");\n this.generation =\n options.generation ?? requireRuntimeValue(env.NEXTCLAW_EXTENSION_GENERATION, \"NEXTCLAW_EXTENSION_GENERATION\");\n this.fetchImpl = options.fetch ?? globalThis.fetch;\n this.webSocketFactory = options.webSocketFactory;\n if (typeof this.fetchImpl !== \"function\") {\n throw new Error(\"fetch is unavailable. Provide fetch when creating the extension.\");\n }\n }\n\n readonly postIngress = async <TResponse = unknown>(\n type: string,\n payload: unknown,\n ): Promise<TResponse> => {\n const envelope: ExtensionTransportEnvelope = {\n type,\n extensionId: this.extensionId,\n generation: this.generation,\n payload,\n emittedAt: new Date().toISOString(),\n source: \"extension-sdk\",\n };\n const response = await this.fetchImpl(`${this.endpoint}/webhook`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${this.token}`,\n },\n body: JSON.stringify(envelope),\n });\n const body = (await response.json().catch(() => null)) as unknown;\n if (!response.ok) {\n
|
|
1
|
+
{"version":3,"file":"extension-transport.service.js","names":[],"sources":["../../src/services/extension-transport.service.ts"],"sourcesContent":["import type {\n ExtensionRequestResponse,\n ExtensionTransportEnvelope,\n NextClawExtensionOptions,\n NextClawExtensionWebSocketFactoryOptions,\n NextClawExtensionWebSocketLike,\n} from \"../types/extension-sdk.types.js\";\nimport { getKeyId, ingressKeys } from \"@nextclaw/shared\";\nimport { normalizeEndpoint, resolveWebSocketUrl } from \"../utils/extension-url.utils.js\";\nimport WebSocket from \"ws\";\n\ntype EventStreamHandler = (event: ExtensionTransportEnvelope) => void;\n\ntype RuntimeEnv = {\n NEXTCLAW_EXTENSION_ENDPOINT?: string;\n NEXTCLAW_EXTENSION_GENERATION?: string;\n NEXTCLAW_EXTENSION_TOKEN?: string;\n NEXTCLAW_EXTENSION_ID?: string;\n};\n\ndeclare const process: { env: RuntimeEnv } | undefined;\n\nfunction readRuntimeEnv(): RuntimeEnv {\n return typeof process === \"undefined\" ? {} : process.env;\n}\n\nfunction requireRuntimeValue(value: string | undefined, name: string): string {\n const trimmed = value?.trim();\n if (!trimmed) {\n throw new Error(`${name} is required.`);\n }\n return trimmed;\n}\n\nexport class ExtensionTransportService {\n readonly generation: string;\n readonly token: string;\n readonly extensionId: string;\n private readonly endpoint: string;\n private readonly fetchImpl: typeof fetch;\n private readonly webSocketFactory?: (\n url: string,\n options?: NextClawExtensionWebSocketFactoryOptions,\n ) => NextClawExtensionWebSocketLike;\n\n constructor(options: NextClawExtensionOptions = {}) {\n const env = readRuntimeEnv();\n this.endpoint = normalizeEndpoint(\n options.endpoint ?? requireRuntimeValue(env.NEXTCLAW_EXTENSION_ENDPOINT, \"NEXTCLAW_EXTENSION_ENDPOINT\"),\n );\n this.token = options.token ?? requireRuntimeValue(env.NEXTCLAW_EXTENSION_TOKEN, \"NEXTCLAW_EXTENSION_TOKEN\");\n this.extensionId =\n options.extensionId ?? requireRuntimeValue(env.NEXTCLAW_EXTENSION_ID, \"NEXTCLAW_EXTENSION_ID\");\n this.generation =\n options.generation ?? requireRuntimeValue(env.NEXTCLAW_EXTENSION_GENERATION, \"NEXTCLAW_EXTENSION_GENERATION\");\n this.fetchImpl = options.fetch ?? globalThis.fetch;\n this.webSocketFactory = options.webSocketFactory;\n if (typeof this.fetchImpl !== \"function\") {\n throw new Error(\"fetch is unavailable. Provide fetch when creating the extension.\");\n }\n }\n\n readonly postIngress = async <TResponse = unknown>(\n type: string,\n payload: unknown,\n options: { signal?: AbortSignal } = {},\n ): Promise<TResponse> => {\n const envelope: ExtensionTransportEnvelope = {\n type,\n extensionId: this.extensionId,\n generation: this.generation,\n payload,\n emittedAt: new Date().toISOString(),\n source: \"extension-sdk\",\n };\n const response = await this.fetchImpl(`${this.endpoint}/webhook`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${this.token}`,\n },\n body: JSON.stringify(envelope),\n signal: options.signal,\n });\n const body = (await response.json().catch(() => null)) as unknown;\n if (!response.ok) {\n const error = new Error(this.readErrorMessage(body, `NextClaw ingress failed with ${response.status}`));\n Object.assign(error, { status: response.status });\n throw error;\n }\n return this.readResponseData<TResponse>(body);\n };\n\n readonly respondToRequest = async (response: ExtensionRequestResponse): Promise<void> => {\n await this.postIngress(getKeyId(ingressKeys.extension.response), response);\n };\n\n readonly reportReady = async (pid: number): Promise<void> => {\n await this.postIngress(getKeyId(ingressKeys.extension.runtimeReady), {\n generation: this.generation,\n pid,\n });\n };\n\n readonly subscribe = (handler: EventStreamHandler): { close: () => void; ready: Promise<void> } => {\n const socket = this.createSocket(resolveWebSocketUrl(this.endpoint, \"/ws\"), {\n headers: {\n authorization: `Bearer ${this.token}`,\n \"x-nextclaw-extension-id\": this.extensionId,\n \"x-nextclaw-extension-generation\": this.generation,\n },\n });\n let opened = false;\n let resolveReady!: () => void;\n let rejectReady!: (error: Error) => void;\n const ready = new Promise<void>((resolve, reject) => {\n resolveReady = resolve;\n rejectReady = reject;\n });\n void ready.catch(() => undefined);\n socket.onopen = () => {\n opened = true;\n resolveReady();\n };\n socket.onerror = () => {\n if (!opened) {\n rejectReady(new Error(`Extension ${this.extensionId} event stream failed before opening.`));\n }\n };\n socket.onclose = () => {\n if (!opened) {\n rejectReady(new Error(`Extension ${this.extensionId} event stream closed before opening.`));\n }\n };\n socket.onmessage = (event) => {\n const envelope = this.parseEnvelope(event.data);\n if (envelope) {\n handler(envelope);\n }\n };\n return {\n close: () => socket.close(),\n ready,\n };\n };\n\n private readonly createSocket = (\n url: string,\n options: NextClawExtensionWebSocketFactoryOptions,\n ): NextClawExtensionWebSocketLike => {\n if (this.webSocketFactory) {\n return this.webSocketFactory(url, options);\n }\n const socket = new WebSocket(url, { headers: options.headers });\n socket.on(\"error\", () => undefined);\n return socket as unknown as NextClawExtensionWebSocketLike;\n };\n\n private readonly parseEnvelope = (value: unknown): ExtensionTransportEnvelope | null => {\n if (typeof value !== \"string\") {\n return null;\n }\n try {\n const parsed = JSON.parse(value) as ExtensionTransportEnvelope;\n return parsed && typeof parsed === \"object\" && typeof parsed.type === \"string\" ? parsed : null;\n } catch {\n return null;\n }\n };\n\n private readonly readErrorMessage = (body: unknown, fallback: string): string => {\n if (!body || typeof body !== \"object\") {\n return fallback;\n }\n const error = (body as { error?: { message?: unknown } }).error;\n return typeof error?.message === \"string\" && error.message.trim() ? error.message : fallback;\n };\n\n private readonly readResponseData = <TResponse>(body: unknown): TResponse => {\n if (body && typeof body === \"object\" && !Array.isArray(body)) {\n const record = body as { ok?: unknown; data?: unknown };\n if (record.ok === true && \"data\" in record) {\n return record.data as TResponse;\n }\n }\n return body as TResponse;\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ExtensionChannelCommandExecuteIngressPayload as ChannelCommandExecuteRequest, ExtensionChannelCommandExecuteResponse, ExtensionChannelCommandExecuteResponse as ChannelCommandExecuteResponse, ExtensionChannelCommandListIngressPayload as ChannelCommandListRequest, ExtensionChannelCommandListResponse as ChannelCommandListResponse, ExtensionChannelCommandOption as ChannelCommandOption, ExtensionChannelCommandOptionType as ChannelCommandOptionType, ExtensionChannelCommandSpec, ExtensionChannelCommandSpec as ChannelCommandSpec, ExtensionChannelConfigGetIngressPayload as ChannelConfigGetRequest, ExtensionChannelFileContent as ChannelFileContent, ExtensionChannelImageContent as ChannelImageContent, ExtensionChannelMessageContent as ChannelMessageContent, ExtensionChannelMessageSubmitIngressPayload, ExtensionChannelMessageSubmitIngressPayload as ChannelSubmittedMessage, ExtensionChannelSubmittedAttachment as ChannelSubmittedAttachment, ExtensionChannelTextContent as ChannelTextContent, ExtensionResponseIngressPayload as ExtensionRequestResponse, Unsubscribe } from "@nextclaw/shared";
|
|
1
|
+
import { ExtensionChannelCommandExecuteIngressPayload as ChannelCommandExecuteRequest, ExtensionChannelCommandExecuteResponse, ExtensionChannelCommandExecuteResponse as ChannelCommandExecuteResponse, ExtensionChannelCommandListIngressPayload as ChannelCommandListRequest, ExtensionChannelCommandListResponse as ChannelCommandListResponse, ExtensionChannelCommandOption as ChannelCommandOption, ExtensionChannelCommandOptionType as ChannelCommandOptionType, ExtensionChannelCommandSpec, ExtensionChannelCommandSpec as ChannelCommandSpec, ExtensionChannelConfigGetIngressPayload as ChannelConfigGetRequest, ExtensionChannelFileContent as ChannelFileContent, ExtensionChannelImageContent as ChannelImageContent, ExtensionChannelMessageContent as ChannelMessageContent, ExtensionChannelMessageSubmitIngressPayload, ExtensionChannelMessageSubmitIngressPayload as ChannelSubmittedMessage, ExtensionChannelSubmittedAttachment as ChannelSubmittedAttachment, ExtensionChannelTextContent as ChannelTextContent, ExtensionDiagnosticIngressPayload, ExtensionResponseIngressPayload as ExtensionRequestResponse, Unsubscribe } from "@nextclaw/shared";
|
|
2
2
|
import { NcpEndpointEvent } from "@nextclaw/ncp";
|
|
3
3
|
|
|
4
4
|
//#region src/types/extension-sdk.types.d.ts
|
|
@@ -7,6 +7,7 @@ type NextClawExtensionOptions = {
|
|
|
7
7
|
token?: string;
|
|
8
8
|
extensionId?: string;
|
|
9
9
|
generation?: string;
|
|
10
|
+
diagnosticTimeoutMs?: number;
|
|
10
11
|
fetch?: typeof fetch;
|
|
11
12
|
webSocketFactory?: (url: string, options?: NextClawExtensionWebSocketFactoryOptions) => NextClawExtensionWebSocketLike;
|
|
12
13
|
};
|
|
@@ -60,6 +61,10 @@ type ExtensionChannelCommands = {
|
|
|
60
61
|
metadata?: Record<string, unknown>;
|
|
61
62
|
}) => Promise<ExtensionChannelCommandExecuteResponse | null>;
|
|
62
63
|
};
|
|
64
|
+
type ExtensionDiagnostics = {
|
|
65
|
+
createTraceId: (providerMessageId?: string) => string;
|
|
66
|
+
emit: (input: ExtensionDiagnosticIngressPayload) => Promise<boolean>;
|
|
67
|
+
};
|
|
63
68
|
type ExtensionChannel = {
|
|
64
69
|
id: string;
|
|
65
70
|
submitMessage: (input: Omit<ExtensionChannelMessageSubmitIngressPayload, "channelId">) => Promise<void>;
|
|
@@ -78,5 +83,5 @@ type ExtensionCapabilities = {
|
|
|
78
83
|
provideHandler: (kind: string, handler: ExtensionCapabilityHandler) => Unsubscribe;
|
|
79
84
|
};
|
|
80
85
|
//#endregion
|
|
81
|
-
export { type ChannelCommandExecuteRequest, type ChannelCommandExecuteResponse, type ChannelCommandListRequest, type ChannelCommandListResponse, type ChannelCommandOption, type ChannelCommandOptionType, type ChannelCommandSpec, type ChannelConfigGetRequest, ChannelConfigGetResponse, type ChannelFileContent, type ChannelImageContent, type ChannelMessageContent, type ChannelSubmittedAttachment, type ChannelSubmittedMessage, type ChannelTextContent, ExtensionCapabilities, ExtensionCapabilityHandler, ExtensionCapabilityPayload, ExtensionChannel, ExtensionChannelCommands, ExtensionChannelConfig, ExtensionChannels, ExtensionRequest, ExtensionRequestHandler, type ExtensionRequestResponse, ExtensionTransportEnvelope, NextClawExtensionOptions, NextClawExtensionWebSocketLike };
|
|
86
|
+
export { type ChannelCommandExecuteRequest, type ChannelCommandExecuteResponse, type ChannelCommandListRequest, type ChannelCommandListResponse, type ChannelCommandOption, type ChannelCommandOptionType, type ChannelCommandSpec, type ChannelConfigGetRequest, ChannelConfigGetResponse, type ChannelFileContent, type ChannelImageContent, type ChannelMessageContent, type ChannelSubmittedAttachment, type ChannelSubmittedMessage, type ChannelTextContent, ExtensionCapabilities, ExtensionCapabilityHandler, ExtensionCapabilityPayload, ExtensionChannel, ExtensionChannelCommands, ExtensionChannelConfig, ExtensionChannels, ExtensionDiagnostics, ExtensionRequest, ExtensionRequestHandler, type ExtensionRequestResponse, ExtensionTransportEnvelope, NextClawExtensionOptions, NextClawExtensionWebSocketLike };
|
|
82
87
|
//# sourceMappingURL=extension-sdk.types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extension-sdk.types.d.ts","names":[],"sources":["../../src/types/extension-sdk.types.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"extension-sdk.types.d.ts","names":[],"sources":["../../src/types/extension-sdk.types.ts"],"mappings":";;;;KA2BY,wBAAA;EACV,QAAA;EACA,KAAA;EACA,WAAA;EACA,UAAA;EACA,mBAAA;EACA,KAAA,UAAe,KAAA;EACf,gBAAA,IACE,GAAA,UACA,OAAA,GAAU,wCAAA,KACP,8BAAA;AAAA;AAAA,KAGK,wCAAA;EACV,OAAA,GAAU,MAAA;AAAA;AAAA,KAGA,8BAAA;EACV,MAAA;EACA,SAAA,IAAa,KAAA;IAAS,IAAA;EAAA;EACtB,OAAA,IAAW,KAAA;EACX,OAAA;EACA,KAAA;AAAA;AAAA,KAGU,0BAAA;EACV,IAAA;EACA,WAAA;EACA,UAAA;EACA,OAAA,EAAS,QAAA;EACT,SAAA;EACA,MAAA;AAAA;AAAA,KAGU,gBAAA;EACV,SAAA;EACA,WAAA;EACA,UAAA;EACA,IAAA;EACA,OAAA,GAAU,MAAA;AAAA;AAAA,KAGA,wBAAA;EACV,MAAA,EAAQ,OAAA;AAAA;AAAA,KAGE,sBAAA;EACV,GAAA,2BAA8B,OAAA,CAAQ,OAAA;EACtC,QAAA,sBACE,OAAA,GAAU,MAAA,EAAQ,OAAA,YAAmB,OAAA,WAClC,WAAA;AAAA;AAAA,KAGK,wBAAA;EACV,IAAA,QAAY,OAAA,CAAQ,2BAAA;EACpB,OAAA,GAAU,KAAA;IACR,WAAA;IACA,IAAA,GAAO,MAAA;IACP,cAAA;IACA,QAAA;IACA,QAAA,GAAW,MAAA;EAAA,MACP,OAAA,CAAQ,sCAAA;EACd,WAAA,GAAc,KAAA;IACZ,OAAA;IACA,cAAA;IACA,QAAA;IACA,QAAA,GAAW,MAAA;EAAA,MACP,OAAA,CAAQ,sCAAA;AAAA;AAAA,KAGJ,oBAAA;EACV,aAAA,GAAgB,iBAAA;EAChB,IAAA,GAAO,KAAA,EAAO,iCAAA,KAAsC,OAAA;AAAA;AAAA,KAG1C,gBAAA;EACV,EAAA;EACA,aAAA,GAAgB,KAAA,EAAO,IAAA,CAAK,2CAAA,mBAA8D,OAAA;EAC1F,UAAA,GAAa,OAAA,GAAU,KAAA,EAAO,gBAAA,YAA4B,OAAA,WAAkB,WAAA;EAC5E,MAAA,EAAQ,sBAAA;EACR,QAAA,EAAU,wBAAA;AAAA;AAAA,KAKA,iBAAA;EACV,GAAA,GAAM,SAAA,aAAsB,gBAAA;AAAA;AAAA,KAGlB,uBAAA,IAA2B,OAAA,EAAS,gBAAA,eAA+B,OAAA;AAAA,KAEnE,0BAAA,GAA6B,MAAA;AAAA,KAE7B,0BAAA,kBAA4C,0BAAA,GAA6B,0BAAA,KACnF,OAAA,EAAS,QAAA,EACT,OAAA,EAAS,gBAAA,eACI,OAAA;AAAA,KAEH,qBAAA;EACV,OAAA,GAAU,SAAA,UAAmB,UAAA,aAAuB,WAAA;EACpD,cAAA,GAAiB,IAAA,UAAc,OAAA,EAAS,0BAAA,KAA+B,WAAA;AAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/extension-sdk",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.26",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Backend SDK for NextClaw extension server processes.",
|
|
6
6
|
"type": "module",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"ws": "^8.18.0",
|
|
39
|
-
"@nextclaw/
|
|
40
|
-
"@nextclaw/
|
|
39
|
+
"@nextclaw/shared": "0.4.26",
|
|
40
|
+
"@nextclaw/ncp": "0.8.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^20.17.6",
|