@animalabs/agent-framework 0.1.0
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 +233 -0
- package/dist/src/agent.d.ts +130 -0
- package/dist/src/agent.d.ts.map +1 -0
- package/dist/src/agent.js +406 -0
- package/dist/src/agent.js.map +1 -0
- package/dist/src/api/mcp-server.d.ts +37 -0
- package/dist/src/api/mcp-server.d.ts.map +1 -0
- package/dist/src/api/mcp-server.js +598 -0
- package/dist/src/api/mcp-server.js.map +1 -0
- package/dist/src/api/server.d.ts +83 -0
- package/dist/src/api/server.d.ts.map +1 -0
- package/dist/src/api/server.js +785 -0
- package/dist/src/api/server.js.map +1 -0
- package/dist/src/api/types.d.ts +395 -0
- package/dist/src/api/types.d.ts.map +1 -0
- package/dist/src/api/types.js +5 -0
- package/dist/src/api/types.js.map +1 -0
- package/dist/src/framework.d.ts +309 -0
- package/dist/src/framework.d.ts.map +1 -0
- package/dist/src/framework.js +2309 -0
- package/dist/src/framework.js.map +1 -0
- package/dist/src/gate/event-gate.d.ts +75 -0
- package/dist/src/gate/event-gate.d.ts.map +1 -0
- package/dist/src/gate/event-gate.js +503 -0
- package/dist/src/gate/event-gate.js.map +1 -0
- package/dist/src/gate/index.d.ts +3 -0
- package/dist/src/gate/index.d.ts.map +1 -0
- package/dist/src/gate/index.js +2 -0
- package/dist/src/gate/index.js.map +1 -0
- package/dist/src/gate/types.d.ts +82 -0
- package/dist/src/gate/types.d.ts.map +1 -0
- package/dist/src/gate/types.js +9 -0
- package/dist/src/gate/types.js.map +1 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +18 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/mcpl/channel-registry.d.ts +141 -0
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -0
- package/dist/src/mcpl/channel-registry.js +647 -0
- package/dist/src/mcpl/channel-registry.js.map +1 -0
- package/dist/src/mcpl/checkpoint-manager.d.ts +79 -0
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -0
- package/dist/src/mcpl/checkpoint-manager.js +430 -0
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -0
- package/dist/src/mcpl/errors.d.ts +52 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -0
- package/dist/src/mcpl/errors.js +93 -0
- package/dist/src/mcpl/errors.js.map +1 -0
- package/dist/src/mcpl/feature-set-manager.d.ts +91 -0
- package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -0
- package/dist/src/mcpl/feature-set-manager.js +250 -0
- package/dist/src/mcpl/feature-set-manager.js.map +1 -0
- package/dist/src/mcpl/hook-orchestrator.d.ts +51 -0
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -0
- package/dist/src/mcpl/hook-orchestrator.js +217 -0
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -0
- package/dist/src/mcpl/index.d.ts +14 -0
- package/dist/src/mcpl/index.d.ts.map +1 -0
- package/dist/src/mcpl/index.js +21 -0
- package/dist/src/mcpl/index.js.map +1 -0
- package/dist/src/mcpl/inference-router.d.ts +67 -0
- package/dist/src/mcpl/inference-router.d.ts.map +1 -0
- package/dist/src/mcpl/inference-router.js +249 -0
- package/dist/src/mcpl/inference-router.js.map +1 -0
- package/dist/src/mcpl/push-handler.d.ts +60 -0
- package/dist/src/mcpl/push-handler.d.ts.map +1 -0
- package/dist/src/mcpl/push-handler.js +162 -0
- package/dist/src/mcpl/push-handler.js.map +1 -0
- package/dist/src/mcpl/scope-manager.d.ts +76 -0
- package/dist/src/mcpl/scope-manager.d.ts.map +1 -0
- package/dist/src/mcpl/scope-manager.js +213 -0
- package/dist/src/mcpl/scope-manager.js.map +1 -0
- package/dist/src/mcpl/server-connection.d.ts +174 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -0
- package/dist/src/mcpl/server-connection.js +559 -0
- package/dist/src/mcpl/server-connection.js.map +1 -0
- package/dist/src/mcpl/server-registry.d.ts +63 -0
- package/dist/src/mcpl/server-registry.d.ts.map +1 -0
- package/dist/src/mcpl/server-registry.js +119 -0
- package/dist/src/mcpl/server-registry.js.map +1 -0
- package/dist/src/mcpl/types.d.ts +755 -0
- package/dist/src/mcpl/types.d.ts.map +1 -0
- package/dist/src/mcpl/types.js +48 -0
- package/dist/src/mcpl/types.js.map +1 -0
- package/dist/src/module-registry.d.ts +93 -0
- package/dist/src/module-registry.d.ts.map +1 -0
- package/dist/src/module-registry.js +386 -0
- package/dist/src/module-registry.js.map +1 -0
- package/dist/src/modules/api/index.d.ts +22 -0
- package/dist/src/modules/api/index.d.ts.map +1 -0
- package/dist/src/modules/api/index.js +63 -0
- package/dist/src/modules/api/index.js.map +1 -0
- package/dist/src/modules/discord/discord-js-client.d.ts +58 -0
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -0
- package/dist/src/modules/discord/discord-js-client.js +273 -0
- package/dist/src/modules/discord/discord-js-client.js.map +1 -0
- package/dist/src/modules/discord/index.d.ts +82 -0
- package/dist/src/modules/discord/index.d.ts.map +1 -0
- package/dist/src/modules/discord/index.js +959 -0
- package/dist/src/modules/discord/index.js.map +1 -0
- package/dist/src/modules/discord/types.d.ts +327 -0
- package/dist/src/modules/discord/types.d.ts.map +1 -0
- package/dist/src/modules/discord/types.js +5 -0
- package/dist/src/modules/discord/types.js.map +1 -0
- package/dist/src/modules/index.d.ts +11 -0
- package/dist/src/modules/index.d.ts.map +1 -0
- package/dist/src/modules/index.js +7 -0
- package/dist/src/modules/index.js.map +1 -0
- package/dist/src/modules/workspace/index.d.ts +59 -0
- package/dist/src/modules/workspace/index.d.ts.map +1 -0
- package/dist/src/modules/workspace/index.js +831 -0
- package/dist/src/modules/workspace/index.js.map +1 -0
- package/dist/src/modules/workspace/sync.d.ts +48 -0
- package/dist/src/modules/workspace/sync.d.ts.map +1 -0
- package/dist/src/modules/workspace/sync.js +253 -0
- package/dist/src/modules/workspace/sync.js.map +1 -0
- package/dist/src/modules/workspace/types.d.ts +166 -0
- package/dist/src/modules/workspace/types.d.ts.map +1 -0
- package/dist/src/modules/workspace/types.js +5 -0
- package/dist/src/modules/workspace/types.js.map +1 -0
- package/dist/src/modules/workspace/watcher.d.ts +43 -0
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -0
- package/dist/src/modules/workspace/watcher.js +120 -0
- package/dist/src/modules/workspace/watcher.js.map +1 -0
- package/dist/src/queue.d.ts +50 -0
- package/dist/src/queue.d.ts.map +1 -0
- package/dist/src/queue.js +95 -0
- package/dist/src/queue.js.map +1 -0
- package/dist/src/safe-slice.d.ts +8 -0
- package/dist/src/safe-slice.d.ts.map +1 -0
- package/dist/src/safe-slice.js +18 -0
- package/dist/src/safe-slice.js.map +1 -0
- package/dist/src/types/agent.d.ts +118 -0
- package/dist/src/types/agent.d.ts.map +1 -0
- package/dist/src/types/agent.js +2 -0
- package/dist/src/types/agent.js.map +1 -0
- package/dist/src/types/events.d.ts +220 -0
- package/dist/src/types/events.d.ts.map +1 -0
- package/dist/src/types/events.js +2 -0
- package/dist/src/types/events.js.map +1 -0
- package/dist/src/types/framework.d.ts +184 -0
- package/dist/src/types/framework.d.ts.map +1 -0
- package/dist/src/types/framework.js +2 -0
- package/dist/src/types/framework.js.map +1 -0
- package/dist/src/types/index.d.ts +7 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +7 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/inference-log.d.ts +64 -0
- package/dist/src/types/inference-log.d.ts.map +1 -0
- package/dist/src/types/inference-log.js +5 -0
- package/dist/src/types/inference-log.js.map +1 -0
- package/dist/src/types/module.d.ts +302 -0
- package/dist/src/types/module.d.ts.map +1 -0
- package/dist/src/types/module.js +2 -0
- package/dist/src/types/module.js.map +1 -0
- package/dist/src/types/trace.d.ts +173 -0
- package/dist/src/types/trace.d.ts.map +1 -0
- package/dist/src/types/trace.js +2 -0
- package/dist/src/types/trace.js.map +1 -0
- package/dist/test/event-gate.test.d.ts +2 -0
- package/dist/test/event-gate.test.d.ts.map +1 -0
- package/dist/test/event-gate.test.js +535 -0
- package/dist/test/event-gate.test.js.map +1 -0
- package/dist/test/framework.test.d.ts +5 -0
- package/dist/test/framework.test.d.ts.map +1 -0
- package/dist/test/framework.test.js +738 -0
- package/dist/test/framework.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +58 -0
- package/src/agent.ts +512 -0
- package/src/api/mcp-server.ts +687 -0
- package/src/api/server.ts +1023 -0
- package/src/api/types.ts +542 -0
- package/src/framework.ts +2784 -0
- package/src/gate/event-gate.ts +623 -0
- package/src/gate/index.ts +12 -0
- package/src/gate/types.ts +112 -0
- package/src/index.ts +63 -0
- package/src/mcpl/channel-registry.ts +824 -0
- package/src/mcpl/checkpoint-manager.ts +545 -0
- package/src/mcpl/errors.ts +114 -0
- package/src/mcpl/feature-set-manager.ts +333 -0
- package/src/mcpl/hook-orchestrator.ts +281 -0
- package/src/mcpl/index.ts +134 -0
- package/src/mcpl/inference-router.ts +335 -0
- package/src/mcpl/push-handler.ts +232 -0
- package/src/mcpl/scope-manager.ts +262 -0
- package/src/mcpl/server-connection.ts +694 -0
- package/src/mcpl/server-registry.ts +144 -0
- package/src/mcpl/types.ts +991 -0
- package/src/module-registry.ts +547 -0
- package/src/modules/api/index.ts +94 -0
- package/src/modules/discord/discord-js-client.ts +353 -0
- package/src/modules/discord/index.ts +1146 -0
- package/src/modules/discord/types.ts +465 -0
- package/src/modules/index.ts +43 -0
- package/src/modules/workspace/index.ts +969 -0
- package/src/modules/workspace/sync.ts +314 -0
- package/src/modules/workspace/types.ts +202 -0
- package/src/modules/workspace/watcher.ts +142 -0
- package/src/queue.ts +106 -0
- package/src/safe-slice.ts +18 -0
- package/src/types/agent.ts +119 -0
- package/src/types/events.ts +252 -0
- package/src/types/framework.ts +211 -0
- package/src/types/index.ts +6 -0
- package/src/types/inference-log.ts +64 -0
- package/src/types/module.ts +367 -0
- package/src/types/trace.ts +196 -0
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChannelRegistry — manages MCPL channel lifecycle, incoming messages, and
|
|
3
|
+
* synthesized channel tools.
|
|
4
|
+
*
|
|
5
|
+
* Adapted from battle-tested patterns in Anarchid/agent-framework@mcpl-module-proto.
|
|
6
|
+
*
|
|
7
|
+
* Responsibilities:
|
|
8
|
+
* - Register/unregister channel descriptors from MCPL servers
|
|
9
|
+
* - Auto-open channels on registration
|
|
10
|
+
* - Route incoming messages to the processing queue
|
|
11
|
+
* - Manage typing indicator timers (7s interval for Discord compatibility)
|
|
12
|
+
* - Expose synthesized tools: channel_list, channel_open, channel_close, channel_publish
|
|
13
|
+
* - Build channel context for beforeInference params
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ContentBlock } from '@animalabs/membrane';
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
ChannelDescriptor,
|
|
20
|
+
ChannelContext,
|
|
21
|
+
ChannelsRegisterParams,
|
|
22
|
+
ChannelsRegisterResult,
|
|
23
|
+
ChannelsChangedParams,
|
|
24
|
+
ChannelsIncomingParams,
|
|
25
|
+
ChannelsIncomingResult,
|
|
26
|
+
ChannelIncomingMessageResult,
|
|
27
|
+
ChannelsPublishParams,
|
|
28
|
+
McplContentBlock,
|
|
29
|
+
} from './types.js';
|
|
30
|
+
|
|
31
|
+
import type { McplServerRegistry } from './server-registry.js';
|
|
32
|
+
import type { FeatureSetManager } from './feature-set-manager.js';
|
|
33
|
+
import type { ToolDefinition, ToolResult, ProcessEvent } from '../types/index.js';
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Typing indicator interval (Discord typing lasts ~10s, so 7s keeps it alive)
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
const TYPING_INTERVAL_MS = 7_000;
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Internal Types
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
/** A registered channel entry, keyed by `{serverId}:{channelId}`. */
|
|
46
|
+
interface ChannelEntry {
|
|
47
|
+
serverId: string;
|
|
48
|
+
descriptor: ChannelDescriptor;
|
|
49
|
+
open: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Minimal responder interface for sending JSON-RPC results back. */
|
|
53
|
+
interface Responder {
|
|
54
|
+
respond(result: unknown): void;
|
|
55
|
+
respondError?(code: number, message: string, data?: unknown): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Event pushed to the processing queue when an incoming channel message arrives.
|
|
60
|
+
* Uses the CustomEvent pattern (`${string}:${string}`) from ProcessEvent.
|
|
61
|
+
*/
|
|
62
|
+
interface McplChannelIncomingEvent {
|
|
63
|
+
type: 'mcpl:channel-incoming';
|
|
64
|
+
serverId: string;
|
|
65
|
+
channelId: string;
|
|
66
|
+
messageId: string;
|
|
67
|
+
threadId?: string;
|
|
68
|
+
author: { id: string; name: string };
|
|
69
|
+
content: ContentBlock[];
|
|
70
|
+
timestamp: string;
|
|
71
|
+
metadata?: Record<string, unknown>;
|
|
72
|
+
triggerInference?: boolean;
|
|
73
|
+
targetAgents?: string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// Content Conversion: McplContentBlock → membrane ContentBlock
|
|
78
|
+
// ============================================================================
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Convert a single MCPL wire-format content block to a membrane ContentBlock.
|
|
82
|
+
*/
|
|
83
|
+
function convertBlock(block: McplContentBlock): ContentBlock {
|
|
84
|
+
switch (block.type) {
|
|
85
|
+
case 'text':
|
|
86
|
+
return { type: 'text', text: block.text };
|
|
87
|
+
|
|
88
|
+
case 'image':
|
|
89
|
+
if (block.data && block.mimeType) {
|
|
90
|
+
return {
|
|
91
|
+
type: 'image',
|
|
92
|
+
source: { type: 'base64', data: block.data, mediaType: block.mimeType },
|
|
93
|
+
} as ContentBlock;
|
|
94
|
+
}
|
|
95
|
+
if (block.uri) {
|
|
96
|
+
return {
|
|
97
|
+
type: 'image',
|
|
98
|
+
source: { type: 'url', url: block.uri },
|
|
99
|
+
} as ContentBlock;
|
|
100
|
+
}
|
|
101
|
+
return { type: 'text', text: '[Image: no data]' };
|
|
102
|
+
|
|
103
|
+
case 'audio':
|
|
104
|
+
if (block.data && block.mimeType) {
|
|
105
|
+
return {
|
|
106
|
+
type: 'audio',
|
|
107
|
+
source: { type: 'base64', data: block.data, mediaType: block.mimeType },
|
|
108
|
+
} as ContentBlock;
|
|
109
|
+
}
|
|
110
|
+
return { type: 'text', text: '[Audio: no data]' };
|
|
111
|
+
|
|
112
|
+
case 'resource':
|
|
113
|
+
return { type: 'text', text: `[Resource: ${block.uri}]` };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// Channel Tool Definitions
|
|
119
|
+
// ============================================================================
|
|
120
|
+
|
|
121
|
+
const CHANNEL_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
122
|
+
{
|
|
123
|
+
name: 'channel_list',
|
|
124
|
+
description: 'List all available channels',
|
|
125
|
+
inputSchema: { type: 'object' as const, properties: {} },
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'channel_open',
|
|
129
|
+
description: 'Open a channel to start receiving messages',
|
|
130
|
+
inputSchema: {
|
|
131
|
+
type: 'object' as const,
|
|
132
|
+
properties: {
|
|
133
|
+
channelId: { type: 'string', description: 'ID of the channel to open' },
|
|
134
|
+
},
|
|
135
|
+
required: ['channelId'],
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'channel_close',
|
|
140
|
+
description: 'Close a channel to stop receiving messages',
|
|
141
|
+
inputSchema: {
|
|
142
|
+
type: 'object' as const,
|
|
143
|
+
properties: {
|
|
144
|
+
channelId: { type: 'string', description: 'ID of the channel to close' },
|
|
145
|
+
},
|
|
146
|
+
required: ['channelId'],
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: 'channel_publish',
|
|
151
|
+
description: 'Publish a message to a channel. If channelId is omitted, publishes to the most recent incoming channel.',
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: 'object' as const,
|
|
154
|
+
properties: {
|
|
155
|
+
channelId: { type: 'string', description: 'ID of the channel to publish to (defaults to the most recent incoming channel)' },
|
|
156
|
+
content: { type: 'string', description: 'Text content to publish' },
|
|
157
|
+
text: { type: 'string', description: 'Alias for content' },
|
|
158
|
+
},
|
|
159
|
+
required: [],
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
// ============================================================================
|
|
165
|
+
// Constructor Options
|
|
166
|
+
// ============================================================================
|
|
167
|
+
|
|
168
|
+
interface ChannelRegistryOptions {
|
|
169
|
+
/** Callback to determine whether an incoming message should trigger inference. */
|
|
170
|
+
shouldTriggerInference?: (content: string, metadata: Record<string, unknown>) => boolean;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ============================================================================
|
|
174
|
+
// ChannelRegistry
|
|
175
|
+
// ============================================================================
|
|
176
|
+
|
|
177
|
+
export class ChannelRegistry {
|
|
178
|
+
private serverRegistry: McplServerRegistry;
|
|
179
|
+
private featureSetManager: FeatureSetManager;
|
|
180
|
+
private pushEventFn: (event: ProcessEvent) => void;
|
|
181
|
+
private emitTraceFn: (event: { type: string; [key: string]: unknown }) => void;
|
|
182
|
+
private sendTypingFn?: (serverId: string, channelId: string) => void;
|
|
183
|
+
private shouldTriggerInference?: (content: string, metadata: Record<string, unknown>) => boolean;
|
|
184
|
+
|
|
185
|
+
/** Registered channels, keyed by `{serverId}:{channelId}`. */
|
|
186
|
+
private channels = new Map<string, ChannelEntry>();
|
|
187
|
+
|
|
188
|
+
/** Most recent incoming channel ID — used for speech routing / default publish. */
|
|
189
|
+
private defaultPublishChannel: string | null = null;
|
|
190
|
+
|
|
191
|
+
/** Most recent incoming message metadata, used for buildChannelContext. */
|
|
192
|
+
private defaultPublishMessageId: string | null = null;
|
|
193
|
+
private defaultPublishThreadId: string | undefined = undefined;
|
|
194
|
+
|
|
195
|
+
/** Per-channel typing indicator timers. */
|
|
196
|
+
private typingIntervals = new Map<string, ReturnType<typeof setInterval>>();
|
|
197
|
+
|
|
198
|
+
constructor(
|
|
199
|
+
serverRegistry: McplServerRegistry,
|
|
200
|
+
featureSetManager: FeatureSetManager,
|
|
201
|
+
pushEventFn: (event: ProcessEvent) => void,
|
|
202
|
+
emitTraceFn: (event: { type: string; [key: string]: unknown }) => void,
|
|
203
|
+
options?: ChannelRegistryOptions & {
|
|
204
|
+
sendTypingFn?: (serverId: string, channelId: string) => void;
|
|
205
|
+
},
|
|
206
|
+
) {
|
|
207
|
+
this.serverRegistry = serverRegistry;
|
|
208
|
+
this.featureSetManager = featureSetManager;
|
|
209
|
+
this.pushEventFn = pushEventFn;
|
|
210
|
+
this.emitTraceFn = emitTraceFn;
|
|
211
|
+
this.sendTypingFn = options?.sendTypingFn;
|
|
212
|
+
this.shouldTriggerInference = options?.shouldTriggerInference;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ==========================================================================
|
|
216
|
+
// Handler Methods (called from framework.ts wireMcplEvents)
|
|
217
|
+
// ==========================================================================
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Handle `channels/register` from a server.
|
|
221
|
+
*
|
|
222
|
+
* Registers each channel descriptor in the map and auto-opens them.
|
|
223
|
+
*/
|
|
224
|
+
async handleRegister(
|
|
225
|
+
serverId: string,
|
|
226
|
+
params: ChannelsRegisterParams,
|
|
227
|
+
responder?: Responder,
|
|
228
|
+
): Promise<void> {
|
|
229
|
+
const registeredIds: string[] = [];
|
|
230
|
+
|
|
231
|
+
for (const channel of params.channels) {
|
|
232
|
+
const key = `${serverId}:${channel.id}`;
|
|
233
|
+
this.channels.set(key, {
|
|
234
|
+
serverId,
|
|
235
|
+
descriptor: channel,
|
|
236
|
+
open: false,
|
|
237
|
+
});
|
|
238
|
+
registeredIds.push(channel.id);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Respond before auto-opening — the server blocks on this response and
|
|
242
|
+
// can't process channels/open until it arrives.
|
|
243
|
+
const result: ChannelsRegisterResult = { registered: registeredIds };
|
|
244
|
+
responder?.respond(result);
|
|
245
|
+
|
|
246
|
+
// Auto-open all registered channels
|
|
247
|
+
await this.autoOpenChannels(serverId, params.channels);
|
|
248
|
+
|
|
249
|
+
this.emitTraceFn({
|
|
250
|
+
type: 'mcpl:channels-register',
|
|
251
|
+
serverId,
|
|
252
|
+
channelIds: registeredIds,
|
|
253
|
+
count: registeredIds.length,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Handle `channels/changed` notification from a server.
|
|
259
|
+
*
|
|
260
|
+
* Processes added (register + auto-open), removed (delete + stop typing),
|
|
261
|
+
* and updated (replace descriptor) channels.
|
|
262
|
+
*/
|
|
263
|
+
async handleChanged(
|
|
264
|
+
serverId: string,
|
|
265
|
+
params: ChannelsChangedParams,
|
|
266
|
+
): Promise<void> {
|
|
267
|
+
// Process removed channels
|
|
268
|
+
if (params.removed) {
|
|
269
|
+
for (const channelId of params.removed) {
|
|
270
|
+
const key = `${serverId}:${channelId}`;
|
|
271
|
+
this.channels.delete(key);
|
|
272
|
+
this.stopTyping(channelId);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Process updated channels (replace descriptor, preserve open state)
|
|
277
|
+
if (params.updated) {
|
|
278
|
+
for (const channel of params.updated) {
|
|
279
|
+
const key = `${serverId}:${channel.id}`;
|
|
280
|
+
const existing = this.channels.get(key);
|
|
281
|
+
if (existing) {
|
|
282
|
+
existing.descriptor = channel;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Process added channels (register + auto-open)
|
|
288
|
+
if (params.added) {
|
|
289
|
+
for (const channel of params.added) {
|
|
290
|
+
const key = `${serverId}:${channel.id}`;
|
|
291
|
+
this.channels.set(key, {
|
|
292
|
+
serverId,
|
|
293
|
+
descriptor: channel,
|
|
294
|
+
open: false,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
await this.autoOpenChannels(serverId, params.added);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this.emitTraceFn({
|
|
301
|
+
type: 'mcpl:channels-changed',
|
|
302
|
+
serverId,
|
|
303
|
+
added: params.added?.map((c) => c.id) ?? [],
|
|
304
|
+
removed: params.removed ?? [],
|
|
305
|
+
updated: params.updated?.map((c) => c.id) ?? [],
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Handle `channels/incoming` from a server.
|
|
311
|
+
*
|
|
312
|
+
* Converts each message's content, pushes McplChannelIncomingEvent to the
|
|
313
|
+
* queue, and responds with per-message results.
|
|
314
|
+
*/
|
|
315
|
+
handleIncoming(
|
|
316
|
+
serverId: string,
|
|
317
|
+
params: ChannelsIncomingParams,
|
|
318
|
+
responder?: Responder,
|
|
319
|
+
): void {
|
|
320
|
+
const results: ChannelIncomingMessageResult[] = [];
|
|
321
|
+
|
|
322
|
+
for (const message of params.messages) {
|
|
323
|
+
// Convert MCPL content blocks to membrane ContentBlocks
|
|
324
|
+
const convertedContent: ContentBlock[] = message.content.map(convertBlock);
|
|
325
|
+
|
|
326
|
+
// Track default publish channel (most recent incoming)
|
|
327
|
+
this.defaultPublishChannel = message.channelId;
|
|
328
|
+
this.defaultPublishMessageId = message.messageId;
|
|
329
|
+
this.defaultPublishThreadId = message.threadId;
|
|
330
|
+
|
|
331
|
+
// Determine whether to trigger inference
|
|
332
|
+
let triggerInference = true;
|
|
333
|
+
if (this.shouldTriggerInference) {
|
|
334
|
+
const textContent = message.content
|
|
335
|
+
.filter((b): b is { type: 'text'; text: string } => b.type === 'text')
|
|
336
|
+
.map((b) => b.text)
|
|
337
|
+
.join('\n');
|
|
338
|
+
triggerInference = this.shouldTriggerInference(
|
|
339
|
+
textContent,
|
|
340
|
+
{
|
|
341
|
+
...message.metadata,
|
|
342
|
+
eventType: 'channel:incoming',
|
|
343
|
+
serverId,
|
|
344
|
+
channelId: message.channelId,
|
|
345
|
+
messageId: message.messageId,
|
|
346
|
+
threadId: message.threadId,
|
|
347
|
+
author: message.author,
|
|
348
|
+
},
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Build the incoming event
|
|
353
|
+
const event: McplChannelIncomingEvent = {
|
|
354
|
+
type: 'mcpl:channel-incoming',
|
|
355
|
+
serverId,
|
|
356
|
+
channelId: message.channelId,
|
|
357
|
+
messageId: message.messageId,
|
|
358
|
+
threadId: message.threadId,
|
|
359
|
+
author: message.author,
|
|
360
|
+
content: convertedContent,
|
|
361
|
+
timestamp: message.timestamp,
|
|
362
|
+
metadata: message.metadata,
|
|
363
|
+
triggerInference,
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// Push to the processing queue
|
|
367
|
+
// Cast through unknown because McplChannelIncomingEvent matches the
|
|
368
|
+
// CustomEvent `${string}:${string}` type pattern but lacks an index signature.
|
|
369
|
+
this.pushEventFn(event as unknown as ProcessEvent);
|
|
370
|
+
|
|
371
|
+
// Collect per-message result
|
|
372
|
+
results.push({
|
|
373
|
+
messageId: message.messageId,
|
|
374
|
+
accepted: true,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const result: ChannelsIncomingResult = { results };
|
|
379
|
+
responder?.respond(result);
|
|
380
|
+
|
|
381
|
+
this.emitTraceFn({
|
|
382
|
+
type: 'mcpl:channels-incoming',
|
|
383
|
+
serverId,
|
|
384
|
+
messageCount: params.messages.length,
|
|
385
|
+
channelIds: [...new Set(params.messages.map((m) => m.channelId))],
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ==========================================================================
|
|
390
|
+
// Typing Indicator Management
|
|
391
|
+
// ==========================================================================
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Start sending typing indicators for a channel.
|
|
395
|
+
*
|
|
396
|
+
* Sends a typing notification immediately and every 7 seconds thereafter.
|
|
397
|
+
* Discord typing indicators last ~10s, so 7s keeps them alive.
|
|
398
|
+
*
|
|
399
|
+
* No-op if already typing on this channel.
|
|
400
|
+
*/
|
|
401
|
+
startTyping(channelId: string): void {
|
|
402
|
+
if (this.typingIntervals.has(channelId)) {
|
|
403
|
+
return; // Already typing
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Find the channel entry and its server
|
|
407
|
+
const entry = this.findChannelEntry(channelId);
|
|
408
|
+
if (!entry) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Send typing immediately
|
|
413
|
+
this.sendTypingNotification(entry.serverId, channelId);
|
|
414
|
+
|
|
415
|
+
// Set up interval
|
|
416
|
+
const interval = setInterval(() => {
|
|
417
|
+
this.sendTypingNotification(entry.serverId, channelId);
|
|
418
|
+
}, TYPING_INTERVAL_MS);
|
|
419
|
+
|
|
420
|
+
this.typingIntervals.set(channelId, interval);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Stop sending typing indicators.
|
|
425
|
+
*
|
|
426
|
+
* If channelId is specified, stops typing on that channel only.
|
|
427
|
+
* If no channelId, stops all typing indicators.
|
|
428
|
+
*/
|
|
429
|
+
stopTyping(channelId?: string): void {
|
|
430
|
+
if (channelId !== undefined) {
|
|
431
|
+
const interval = this.typingIntervals.get(channelId);
|
|
432
|
+
if (interval) {
|
|
433
|
+
clearInterval(interval);
|
|
434
|
+
this.typingIntervals.delete(channelId);
|
|
435
|
+
}
|
|
436
|
+
} else {
|
|
437
|
+
// Clear all typing intervals
|
|
438
|
+
for (const interval of this.typingIntervals.values()) {
|
|
439
|
+
clearInterval(interval);
|
|
440
|
+
}
|
|
441
|
+
this.typingIntervals.clear();
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ==========================================================================
|
|
446
|
+
// Accessors
|
|
447
|
+
// ==========================================================================
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Get the default publish channel ID (most recent incoming channel).
|
|
451
|
+
*/
|
|
452
|
+
getDefaultPublishChannel(): string | null {
|
|
453
|
+
return this.defaultPublishChannel;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Get all open channels.
|
|
458
|
+
*/
|
|
459
|
+
getOpenChannels(): ChannelEntry[] {
|
|
460
|
+
const result: ChannelEntry[] = [];
|
|
461
|
+
for (const entry of this.channels.values()) {
|
|
462
|
+
if (entry.open) {
|
|
463
|
+
result.push(entry);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return result;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ==========================================================================
|
|
470
|
+
// Synthesized Channel Tools
|
|
471
|
+
// ==========================================================================
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Get synthesized tool definitions for channel operations.
|
|
475
|
+
*/
|
|
476
|
+
getChannelTools(): ToolDefinition[] {
|
|
477
|
+
return CHANNEL_TOOL_DEFINITIONS;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Handle a call to one of the synthesized channel tools.
|
|
482
|
+
*/
|
|
483
|
+
async handleChannelToolCall(toolName: string, input: unknown): Promise<ToolResult> {
|
|
484
|
+
switch (toolName) {
|
|
485
|
+
case 'channel_list':
|
|
486
|
+
return this.handleToolList();
|
|
487
|
+
|
|
488
|
+
case 'channel_open':
|
|
489
|
+
return this.handleToolOpen(input as { channelId: string });
|
|
490
|
+
|
|
491
|
+
case 'channel_close':
|
|
492
|
+
return this.handleToolClose(input as { channelId: string });
|
|
493
|
+
|
|
494
|
+
case 'channel_publish':
|
|
495
|
+
return this.handleToolPublish(input as { channelId: string; content: string });
|
|
496
|
+
|
|
497
|
+
default:
|
|
498
|
+
return { success: false, error: `Unknown channel tool: ${toolName}`, isError: true };
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ==========================================================================
|
|
503
|
+
// Channel Context for beforeInference
|
|
504
|
+
// ==========================================================================
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Build channel context for inclusion in beforeInference params.
|
|
508
|
+
*
|
|
509
|
+
* Returns undefined if no channels are active.
|
|
510
|
+
*/
|
|
511
|
+
buildChannelContext(): ChannelContext | undefined {
|
|
512
|
+
const openChannels = this.getOpenChannels();
|
|
513
|
+
if (openChannels.length === 0 && !this.defaultPublishChannel) {
|
|
514
|
+
return undefined;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const context: ChannelContext = {};
|
|
518
|
+
|
|
519
|
+
// Incoming: from defaultPublishChannel if set
|
|
520
|
+
if (this.defaultPublishChannel && this.defaultPublishMessageId) {
|
|
521
|
+
context.incoming = {
|
|
522
|
+
channelId: this.defaultPublishChannel,
|
|
523
|
+
messageId: this.defaultPublishMessageId,
|
|
524
|
+
threadId: this.defaultPublishThreadId,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Default outgoing: same as incoming (reply to last channel)
|
|
529
|
+
if (this.defaultPublishChannel) {
|
|
530
|
+
context.defaultOutgoing = {
|
|
531
|
+
channelId: this.defaultPublishChannel,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Candidates: all open channel IDs
|
|
536
|
+
if (openChannels.length > 0) {
|
|
537
|
+
context.candidates = openChannels.map((e) => e.descriptor.id);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return context;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ==========================================================================
|
|
544
|
+
// Lifecycle
|
|
545
|
+
// ==========================================================================
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Stop all typing intervals and clear all channel registrations.
|
|
549
|
+
*/
|
|
550
|
+
stopAll(): void {
|
|
551
|
+
// Clear all typing intervals
|
|
552
|
+
for (const interval of this.typingIntervals.values()) {
|
|
553
|
+
clearInterval(interval);
|
|
554
|
+
}
|
|
555
|
+
this.typingIntervals.clear();
|
|
556
|
+
|
|
557
|
+
// Clear channels map
|
|
558
|
+
this.channels.clear();
|
|
559
|
+
|
|
560
|
+
// Reset default publish tracking
|
|
561
|
+
this.defaultPublishChannel = null;
|
|
562
|
+
this.defaultPublishMessageId = null;
|
|
563
|
+
this.defaultPublishThreadId = undefined;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// ==========================================================================
|
|
567
|
+
// Private: Auto-open channels
|
|
568
|
+
// ==========================================================================
|
|
569
|
+
|
|
570
|
+
private async autoOpenChannels(
|
|
571
|
+
serverId: string,
|
|
572
|
+
channels: ChannelDescriptor[],
|
|
573
|
+
): Promise<void> {
|
|
574
|
+
const server = this.serverRegistry.getServer(serverId);
|
|
575
|
+
if (!server) return;
|
|
576
|
+
|
|
577
|
+
for (const channel of channels) {
|
|
578
|
+
const key = `${serverId}:${channel.id}`;
|
|
579
|
+
try {
|
|
580
|
+
await server.sendChannelsOpen({
|
|
581
|
+
type: channel.type,
|
|
582
|
+
address: channel.address,
|
|
583
|
+
});
|
|
584
|
+
const entry = this.channels.get(key);
|
|
585
|
+
if (entry) {
|
|
586
|
+
entry.open = true;
|
|
587
|
+
}
|
|
588
|
+
} catch (err) {
|
|
589
|
+
this.emitTraceFn({
|
|
590
|
+
type: 'mcpl:channel-open-failed',
|
|
591
|
+
serverId,
|
|
592
|
+
channelId: channel.id,
|
|
593
|
+
error: (err as Error).message,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// ==========================================================================
|
|
600
|
+
// Private: Typing notification
|
|
601
|
+
// ==========================================================================
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Send a typing notification for a channel.
|
|
605
|
+
*
|
|
606
|
+
* Uses the sendTypingFn callback if provided. If not, this is a no-op
|
|
607
|
+
* (typing timer lifecycle is still managed for when the callback is wired).
|
|
608
|
+
*/
|
|
609
|
+
private sendTypingNotification(serverId: string, channelId: string): void {
|
|
610
|
+
if (this.sendTypingFn) {
|
|
611
|
+
this.sendTypingFn(serverId, channelId);
|
|
612
|
+
}
|
|
613
|
+
// TODO: When server-connection exposes a public sendNotification or
|
|
614
|
+
// sendTyping method, wire it here directly instead of using a callback.
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ==========================================================================
|
|
618
|
+
// Private: Channel Lookup
|
|
619
|
+
// ==========================================================================
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Find a channel entry by channelId (searches across all servers).
|
|
623
|
+
* Returns the first match.
|
|
624
|
+
*/
|
|
625
|
+
private findChannelEntry(channelId: string): ChannelEntry | undefined {
|
|
626
|
+
for (const [key, entry] of this.channels) {
|
|
627
|
+
if (entry.descriptor.id === channelId) {
|
|
628
|
+
return entry;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return undefined;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Find the composite key for a channel by its channelId.
|
|
636
|
+
*/
|
|
637
|
+
private findChannelKey(channelId: string): string | undefined {
|
|
638
|
+
for (const [key, entry] of this.channels) {
|
|
639
|
+
if (entry.descriptor.id === channelId) {
|
|
640
|
+
return key;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return undefined;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// ==========================================================================
|
|
647
|
+
// Private: Tool Handlers
|
|
648
|
+
// ==========================================================================
|
|
649
|
+
|
|
650
|
+
private handleToolList(): ToolResult {
|
|
651
|
+
const allChannels: Array<{
|
|
652
|
+
id: string;
|
|
653
|
+
type: string;
|
|
654
|
+
label: string;
|
|
655
|
+
direction: string;
|
|
656
|
+
open: boolean;
|
|
657
|
+
serverId: string;
|
|
658
|
+
}> = [];
|
|
659
|
+
|
|
660
|
+
for (const entry of this.channels.values()) {
|
|
661
|
+
allChannels.push({
|
|
662
|
+
id: entry.descriptor.id,
|
|
663
|
+
type: entry.descriptor.type,
|
|
664
|
+
label: entry.descriptor.label,
|
|
665
|
+
direction: entry.descriptor.direction,
|
|
666
|
+
open: entry.open,
|
|
667
|
+
serverId: entry.serverId,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
success: true,
|
|
673
|
+
data: allChannels,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
private async handleToolOpen(input: { channelId: string }): Promise<ToolResult> {
|
|
678
|
+
const entry = this.findChannelEntry(input.channelId);
|
|
679
|
+
if (!entry) {
|
|
680
|
+
return {
|
|
681
|
+
success: false,
|
|
682
|
+
error: `Channel not found: ${input.channelId}`,
|
|
683
|
+
isError: true,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (entry.open) {
|
|
688
|
+
return {
|
|
689
|
+
success: true,
|
|
690
|
+
data: { channelId: input.channelId, status: 'already open' },
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const server = this.serverRegistry.getServer(entry.serverId);
|
|
695
|
+
if (!server) {
|
|
696
|
+
return {
|
|
697
|
+
success: false,
|
|
698
|
+
error: `Server not found: ${entry.serverId}`,
|
|
699
|
+
isError: true,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
try {
|
|
704
|
+
await server.sendChannelsOpen({
|
|
705
|
+
type: entry.descriptor.type,
|
|
706
|
+
address: entry.descriptor.address,
|
|
707
|
+
});
|
|
708
|
+
entry.open = true;
|
|
709
|
+
return {
|
|
710
|
+
success: true,
|
|
711
|
+
data: { channelId: input.channelId, status: 'opened' },
|
|
712
|
+
};
|
|
713
|
+
} catch (err) {
|
|
714
|
+
return {
|
|
715
|
+
success: false,
|
|
716
|
+
error: `Failed to open channel: ${(err as Error).message}`,
|
|
717
|
+
isError: true,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
private async handleToolClose(input: { channelId: string }): Promise<ToolResult> {
|
|
723
|
+
const entry = this.findChannelEntry(input.channelId);
|
|
724
|
+
if (!entry) {
|
|
725
|
+
return {
|
|
726
|
+
success: false,
|
|
727
|
+
error: `Channel not found: ${input.channelId}`,
|
|
728
|
+
isError: true,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (!entry.open) {
|
|
733
|
+
return {
|
|
734
|
+
success: true,
|
|
735
|
+
data: { channelId: input.channelId, status: 'already closed' },
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const server = this.serverRegistry.getServer(entry.serverId);
|
|
740
|
+
if (!server) {
|
|
741
|
+
return {
|
|
742
|
+
success: false,
|
|
743
|
+
error: `Server not found: ${entry.serverId}`,
|
|
744
|
+
isError: true,
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
try {
|
|
749
|
+
await server.sendChannelsClose({ channelId: input.channelId });
|
|
750
|
+
entry.open = false;
|
|
751
|
+
this.stopTyping(input.channelId);
|
|
752
|
+
return {
|
|
753
|
+
success: true,
|
|
754
|
+
data: { channelId: input.channelId, status: 'closed' },
|
|
755
|
+
};
|
|
756
|
+
} catch (err) {
|
|
757
|
+
return {
|
|
758
|
+
success: false,
|
|
759
|
+
error: `Failed to close channel: ${(err as Error).message}`,
|
|
760
|
+
isError: true,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
private async handleToolPublish(input: { channelId?: string; content?: string; text?: string }): Promise<ToolResult> {
|
|
766
|
+
// Resolve content: accept both `content` and `text` (backward compat)
|
|
767
|
+
const messageText = input.content ?? input.text;
|
|
768
|
+
if (!messageText) {
|
|
769
|
+
return {
|
|
770
|
+
success: false,
|
|
771
|
+
error: 'Either content or text parameter is required',
|
|
772
|
+
isError: true,
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Resolve channelId: default to most recent incoming channel
|
|
777
|
+
const channelId = input.channelId ?? this.defaultPublishChannel;
|
|
778
|
+
if (!channelId) {
|
|
779
|
+
return {
|
|
780
|
+
success: false,
|
|
781
|
+
error: 'No channelId specified and no default channel available',
|
|
782
|
+
isError: true,
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const entry = this.findChannelEntry(channelId);
|
|
787
|
+
if (!entry) {
|
|
788
|
+
return {
|
|
789
|
+
success: false,
|
|
790
|
+
error: `Channel not found: ${channelId}`,
|
|
791
|
+
isError: true,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const server = this.serverRegistry.getServer(entry.serverId);
|
|
796
|
+
if (!server) {
|
|
797
|
+
return {
|
|
798
|
+
success: false,
|
|
799
|
+
error: `Server not found: ${entry.serverId}`,
|
|
800
|
+
isError: true,
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
try {
|
|
805
|
+
const publishParams: ChannelsPublishParams = {
|
|
806
|
+
conversationId: '', // Framework will fill this when wired
|
|
807
|
+
channelId,
|
|
808
|
+
content: [{ type: 'text', text: messageText }],
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
const result = await server.sendChannelsPublish(publishParams);
|
|
812
|
+
return {
|
|
813
|
+
success: true,
|
|
814
|
+
data: result ?? { delivered: true },
|
|
815
|
+
};
|
|
816
|
+
} catch (err) {
|
|
817
|
+
return {
|
|
818
|
+
success: false,
|
|
819
|
+
error: `Failed to publish to channel: ${(err as Error).message}`,
|
|
820
|
+
isError: true,
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|