@ebowwa/channel-types 0.1.1 → 0.2.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.
Files changed (39) hide show
  1. package/dist/config.d.ts +13 -1
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +15 -0
  4. package/dist/config.js.map +1 -1
  5. package/dist/index.d.ts +3 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/plugins/index.d.ts +4 -0
  10. package/dist/plugins/index.d.ts.map +1 -0
  11. package/dist/plugins/index.js +5 -0
  12. package/dist/plugins/index.js.map +1 -0
  13. package/dist/plugins/registry.d.ts +100 -0
  14. package/dist/plugins/registry.d.ts.map +1 -0
  15. package/dist/plugins/registry.js +150 -0
  16. package/dist/plugins/registry.js.map +1 -0
  17. package/dist/plugins/types.adapters.d.ts +126 -0
  18. package/dist/plugins/types.adapters.d.ts.map +1 -0
  19. package/dist/plugins/types.adapters.js +3 -0
  20. package/dist/plugins/types.adapters.js.map +1 -0
  21. package/dist/plugins/types.core.d.ts +82 -0
  22. package/dist/plugins/types.core.d.ts.map +1 -0
  23. package/dist/plugins/types.core.js +51 -0
  24. package/dist/plugins/types.core.js.map +1 -0
  25. package/dist/plugins/types.plugin.d.ts +141 -0
  26. package/dist/plugins/types.plugin.d.ts.map +1 -0
  27. package/dist/plugins/types.plugin.js +3 -0
  28. package/dist/plugins/types.plugin.js.map +1 -0
  29. package/package.json +5 -1
  30. package/src/config.js +270 -0
  31. package/src/config.ts +32 -1
  32. package/src/example.js +431 -0
  33. package/src/index.js +76 -0
  34. package/src/index.ts +5 -0
  35. package/src/plugins/index.ts +5 -0
  36. package/src/plugins/registry.ts +232 -0
  37. package/src/plugins/types.adapters.ts +183 -0
  38. package/src/plugins/types.core.ts +161 -0
  39. package/src/plugins/types.plugin.ts +227 -0
@@ -0,0 +1,232 @@
1
+ // src/plugins/registry.ts
2
+
3
+ import type { ChannelId } from "../index.js";
4
+ import type { ChannelPlugin } from "./types.plugin.js";
5
+
6
+ // ============================================================
7
+ // REGISTRATION TYPES
8
+ // ============================================================
9
+
10
+ export interface PluginChannelRegistration {
11
+ /** Plugin ID (usually matches channel ID) */
12
+ pluginId: string;
13
+
14
+ /** The channel plugin */
15
+ plugin: ChannelPlugin;
16
+
17
+ /** Optional dock metadata (lightweight channel info) */
18
+ dock?: ChannelDock;
19
+
20
+ /** Source (e.g., "core", "extension", "custom") */
21
+ source: string;
22
+ }
23
+
24
+ export interface ChannelDock {
25
+ /** Channel ID */
26
+ id: ChannelId;
27
+
28
+ /** Human-readable label */
29
+ label: string;
30
+
31
+ /** Capabilities summary */
32
+ capabilities: {
33
+ chatTypes: string[];
34
+ hasMedia: boolean;
35
+ hasThreads: boolean;
36
+ hasReactions: boolean;
37
+ };
38
+
39
+ /** Outbound limits */
40
+ outboundLimits: {
41
+ textChunkLimit: number;
42
+ maxFileSize?: number;
43
+ };
44
+ }
45
+
46
+ // ============================================================
47
+ // PLUGIN REGISTRY
48
+ // ============================================================
49
+
50
+ class PluginRegistryImpl {
51
+ private channels: Map<string, PluginChannelRegistration> = new Map();
52
+ private channelOrder: string[] = [];
53
+
54
+ /**
55
+ * Register a channel plugin
56
+ */
57
+ registerChannel(registration: PluginChannelRegistration): void {
58
+ const id = registration.plugin.id;
59
+
60
+ if (typeof id !== "string") {
61
+ throw new Error("Channel plugin id must be a string");
62
+ }
63
+
64
+ const normalizedId = id.trim().toLowerCase();
65
+
66
+ if (this.channels.has(normalizedId)) {
67
+ console.warn(`[PluginRegistry] Channel "${normalizedId}" already registered, replacing`);
68
+ }
69
+
70
+ this.channels.set(normalizedId, registration);
71
+
72
+ // Add to order if not present
73
+ if (!this.channelOrder.includes(normalizedId)) {
74
+ this.channelOrder.push(normalizedId);
75
+ }
76
+
77
+ console.log(`[PluginRegistry] Registered channel: ${normalizedId} (source: ${registration.source})`);
78
+ }
79
+
80
+ /**
81
+ * Unregister a channel plugin
82
+ */
83
+ unregisterChannel(channelId: string): boolean {
84
+ const normalizedId = channelId.trim().toLowerCase();
85
+ const existed = this.channels.delete(normalizedId);
86
+
87
+ if (existed) {
88
+ this.channelOrder = this.channelOrder.filter(id => id !== normalizedId);
89
+ }
90
+
91
+ return existed;
92
+ }
93
+
94
+ /**
95
+ * Get a channel plugin by ID
96
+ */
97
+ getChannel(channelId: string): ChannelPlugin | undefined {
98
+ const normalizedId = this.normalizeChannelId(channelId);
99
+ const registration = this.channels.get(normalizedId);
100
+ return registration?.plugin;
101
+ }
102
+
103
+ /**
104
+ * Get channel registration (includes dock and source)
105
+ */
106
+ getChannelRegistration(channelId: string): PluginChannelRegistration | undefined {
107
+ const normalizedId = this.normalizeChannelId(channelId);
108
+ return this.channels.get(normalizedId);
109
+ }
110
+
111
+ /**
112
+ * Get all registered channels
113
+ */
114
+ getChannels(): ChannelPlugin[] {
115
+ return this.channelOrder
116
+ .map(id => this.channels.get(id)?.plugin)
117
+ .filter((p): p is ChannelPlugin => p !== undefined);
118
+ }
119
+
120
+ /**
121
+ * Get all channel registrations
122
+ */
123
+ getChannelRegistrations(): PluginChannelRegistration[] {
124
+ return this.channelOrder
125
+ .map(id => this.channels.get(id))
126
+ .filter((r): r is PluginChannelRegistration => r !== undefined);
127
+ }
128
+
129
+ /**
130
+ * Get channels ordered by registration
131
+ */
132
+ getChannelOrder(): string[] {
133
+ return [...this.channelOrder];
134
+ }
135
+
136
+ /**
137
+ * Set channel order
138
+ */
139
+ setChannelOrder(order: string[]): void {
140
+ this.channelOrder = order.map(id => id.toLowerCase().trim());
141
+ }
142
+
143
+ /**
144
+ * Get channels that support a specific capability
145
+ */
146
+ getChannelsByCapability(capability: keyof ChannelPlugin["capabilities"]["supports"]): ChannelPlugin[] {
147
+ return this.getChannels().filter(plugin =>
148
+ plugin.capabilities.supports[capability] === true
149
+ );
150
+ }
151
+
152
+ /**
153
+ * Get channels by chat type
154
+ */
155
+ getChannelsByChatType(chatType: string): ChannelPlugin[] {
156
+ return this.getChannels().filter(plugin =>
157
+ plugin.capabilities.chatTypes?.includes(chatType as any)
158
+ );
159
+ }
160
+
161
+ /**
162
+ * Check if a channel is registered
163
+ */
164
+ hasChannel(channelId: string): boolean {
165
+ return this.channels.has(this.normalizeChannelId(channelId));
166
+ }
167
+
168
+ /**
169
+ * Normalize channel ID (handle aliases)
170
+ */
171
+ normalizeChannelId(channelId: string): string {
172
+ const id = channelId.trim().toLowerCase();
173
+
174
+ // Common aliases
175
+ const aliases: Record<string, string> = {
176
+ "tg": "telegram",
177
+ "imsg": "imessage",
178
+ "wa": "whatsapp",
179
+ "dc": "discord",
180
+ "sl": "slack",
181
+ };
182
+
183
+ return aliases[id] || id;
184
+ }
185
+
186
+ /**
187
+ * Clear all registrations (for testing)
188
+ */
189
+ clear(): void {
190
+ this.channels.clear();
191
+ this.channelOrder = [];
192
+ }
193
+
194
+ /**
195
+ * Get registry stats
196
+ */
197
+ getStats(): {
198
+ channelCount: number;
199
+ channels: string[];
200
+ } {
201
+ return {
202
+ channelCount: this.channels.size,
203
+ channels: this.getChannelOrder(),
204
+ };
205
+ }
206
+ }
207
+
208
+ // ============================================================
209
+ // SINGLETON INSTANCE
210
+ // ============================================================
211
+
212
+ export const pluginRegistry = new PluginRegistryImpl();
213
+
214
+ // ============================================================
215
+ // CONVENIENCE FUNCTIONS
216
+ // ============================================================
217
+
218
+ export function registerChannel(registration: PluginChannelRegistration): void {
219
+ pluginRegistry.registerChannel(registration);
220
+ }
221
+
222
+ export function getChannel(channelId: string): ChannelPlugin | undefined {
223
+ return pluginRegistry.getChannel(channelId);
224
+ }
225
+
226
+ export function getChannels(): ChannelPlugin[] {
227
+ return pluginRegistry.getChannels();
228
+ }
229
+
230
+ export function hasChannel(channelId: string): boolean {
231
+ return pluginRegistry.hasChannel(channelId);
232
+ }
@@ -0,0 +1,183 @@
1
+ // src/plugins/types.adapters.ts
2
+
3
+ import type { ChannelId, ChannelMessage } from "../index.js";
4
+
5
+ // ============================================================
6
+ // CONFIG ADAPTER
7
+ // ============================================================
8
+
9
+ export interface ChannelConfigAdapter<ResolvedAccount = unknown> {
10
+ /** List available account IDs for this channel */
11
+ listAccountIds: (config: Record<string, unknown>) => Promise<string[]>;
12
+
13
+ /** Resolve account config to a normalized account object */
14
+ resolveAccount: (config: Record<string, unknown>, accountId: string) => Promise<ResolvedAccount>;
15
+
16
+ /** Validate account credentials */
17
+ validateAccount?: (account: ResolvedAccount) => Promise<boolean>;
18
+ }
19
+
20
+ // ============================================================
21
+ // OUTBOUND ADAPTER (Message Delivery)
22
+ // ============================================================
23
+
24
+ export type DeliveryMode = "direct" | "queue" | "webhook";
25
+
26
+ export interface ChunkResult {
27
+ chunks: string[];
28
+ totalChunks: number;
29
+ }
30
+
31
+ export interface ChannelOutboundAdapter<ResolvedAccount = unknown> {
32
+ /** How messages are delivered */
33
+ deliveryMode: DeliveryMode;
34
+
35
+ /** Max characters per text message */
36
+ textChunkLimit?: number;
37
+
38
+ /** Chunk text for this channel (markdown-aware) */
39
+ chunker?: (text: string, limit: number) => ChunkResult;
40
+
41
+ /** Send text message */
42
+ sendText: (params: {
43
+ to: string;
44
+ text: string;
45
+ accountId: string;
46
+ account: ResolvedAccount;
47
+ replyToId?: string;
48
+ threadId?: string;
49
+ }) => Promise<OutboundResult>;
50
+
51
+ /** Send media message */
52
+ sendMedia?: (params: {
53
+ to: string;
54
+ text?: string;
55
+ mediaUrl: string;
56
+ mediaType: "image" | "video" | "audio" | "file";
57
+ accountId: string;
58
+ account: ResolvedAccount;
59
+ }) => Promise<OutboundResult>;
60
+
61
+ /** Send poll (if supported) */
62
+ sendPoll?: (params: {
63
+ to: string;
64
+ question: string;
65
+ options: string[];
66
+ accountId: string;
67
+ account: ResolvedAccount;
68
+ }) => Promise<OutboundResult>;
69
+ }
70
+
71
+ export interface OutboundResult {
72
+ success: boolean;
73
+ messageId?: string;
74
+ error?: string;
75
+ timestamp: Date;
76
+ }
77
+
78
+ // ============================================================
79
+ // GATEWAY ADAPTER (Account Lifecycle)
80
+ // ============================================================
81
+
82
+ export interface ChannelGatewayAdapter<ResolvedAccount = unknown> {
83
+ /** Start monitoring an account for incoming messages */
84
+ startAccount: (params: {
85
+ accountId: string;
86
+ account: ResolvedAccount;
87
+ onMessage: (message: ChannelMessage) => void;
88
+ }) => Promise<void>;
89
+
90
+ /** Stop monitoring an account */
91
+ stopAccount: (params: {
92
+ accountId: string;
93
+ account: ResolvedAccount;
94
+ }) => Promise<void>;
95
+
96
+ /** Check if account is active */
97
+ isAccountActive?: (accountId: string) => boolean;
98
+ }
99
+
100
+ // ============================================================
101
+ // STATUS ADAPTER (Health & Audits)
102
+ // ============================================================
103
+
104
+ export interface ChannelStatusAdapter<ResolvedAccount = unknown> {
105
+ /** Health probe for account */
106
+ healthProbe: (params: {
107
+ accountId: string;
108
+ account: ResolvedAccount;
109
+ }) => Promise<HealthStatus>;
110
+
111
+ /** Audit account for issues */
112
+ audit?: (params: {
113
+ accountId: string;
114
+ account: ResolvedAccount;
115
+ }) => Promise<ChannelAudit[]>;
116
+ }
117
+
118
+ export interface HealthStatus {
119
+ healthy: boolean;
120
+ latency?: number;
121
+ error?: string;
122
+ lastCheck: Date;
123
+ }
124
+
125
+ export interface ChannelAudit {
126
+ level: "info" | "warning" | "error";
127
+ message: string;
128
+ timestamp: Date;
129
+ }
130
+
131
+ // ============================================================
132
+ // THREADING ADAPTER
133
+ // ============================================================
134
+
135
+ export interface ChannelThreadingAdapter {
136
+ /** Supported reply modes */
137
+ replyModes: ("quote" | "thread" | "reference")[];
138
+
139
+ /** Default reply mode */
140
+ defaultReplyMode: "quote" | "thread" | "reference";
141
+
142
+ /** Get thread context for a message */
143
+ getThreadContext?: (messageId: string) => Promise<ChannelMessage[]>;
144
+ }
145
+
146
+ // ============================================================
147
+ // MESSAGING ADAPTER (Target Normalization)
148
+ // ============================================================
149
+
150
+ export interface ChannelMessagingAdapter {
151
+ /** Normalize a target string (e.g., "@user", "tg:user", URL) to canonical format */
152
+ normalizeTarget: (target: string) => Promise<NormalizedTarget>;
153
+
154
+ /** Parse target from URL */
155
+ parseUrl?: (url: string) => Promise<NormalizedTarget | null>;
156
+ }
157
+
158
+ export interface NormalizedTarget {
159
+ channelId: ChannelId;
160
+ targetId: string;
161
+ targetType: "user" | "group" | "channel";
162
+ original: string;
163
+ }
164
+
165
+ // ============================================================
166
+ // ONBOARDING ADAPTER (Setup Wizard)
167
+ // ============================================================
168
+
169
+ export interface ChannelOnboardingAdapter {
170
+ /** CLI setup wizard steps */
171
+ wizardSteps: OnboardingStep[];
172
+
173
+ /** Validate setup input */
174
+ validateInput?: (step: string, input: string) => Promise<boolean>;
175
+ }
176
+
177
+ export interface OnboardingStep {
178
+ id: string;
179
+ prompt: string;
180
+ type: "text" | "password" | "select" | "confirm";
181
+ options?: string[];
182
+ required: boolean;
183
+ }
@@ -0,0 +1,161 @@
1
+ // src/plugins/types.core.ts
2
+
3
+ /**
4
+ * Core types that support the plugin system.
5
+ * These are the foundational types used across the channel plugin architecture.
6
+ */
7
+
8
+ import type { ChannelId } from "../index.js";
9
+
10
+ // ============================================================
11
+ // ACCOUNT RESOLUTION
12
+ // ============================================================
13
+
14
+ /** Resolved account interface - minimal guaranteed shape */
15
+ export interface ResolvedAccount {
16
+ /** Unique account identifier */
17
+ id: string;
18
+
19
+ /** Human-readable label */
20
+ label?: string;
21
+
22
+ /** Platform-specific data */
23
+ [key: string]: unknown;
24
+ }
25
+
26
+ // ============================================================
27
+ // CHANNEL IDENTITY
28
+ // ============================================================
29
+
30
+ /** String-based channel ID (for serialization) */
31
+ export type ChannelIdString = string;
32
+
33
+ /** Convert ChannelId to string representation */
34
+ export function channelIdToString(id: ChannelId): ChannelIdString {
35
+ const base = `${id.platform}:${id.accountId}`;
36
+ return id.instanceId ? `${base}:${id.instanceId}` : base;
37
+ }
38
+
39
+ /** Parse string back to ChannelId */
40
+ export function parseChannelIdString(str: ChannelIdString): ChannelId {
41
+ const parts = str.split(":");
42
+ if (parts.length < 2) {
43
+ throw new Error(`Invalid channel ID string: ${str}`);
44
+ }
45
+ return {
46
+ platform: parts[0] as ChannelId["platform"],
47
+ accountId: parts[1],
48
+ instanceId: parts[2],
49
+ };
50
+ }
51
+
52
+ // ============================================================
53
+ // PLUGIN LIFECYCLE
54
+ // ============================================================
55
+
56
+ export interface PluginLifecycle {
57
+ /** Called when plugin is registered */
58
+ onRegister?: () => Promise<void> | void;
59
+
60
+ /** Called when plugin is unregistered */
61
+ onUnregister?: () => Promise<void> | void;
62
+
63
+ /** Called before channel starts */
64
+ beforeStart?: () => Promise<void> | void;
65
+
66
+ /** Called after channel stops */
67
+ afterStop?: () => Promise<void> | void;
68
+ }
69
+
70
+ // ============================================================
71
+ // PLUGIN CONTEXT
72
+ // ============================================================
73
+
74
+ export interface PluginContext {
75
+ /** Plugin ID */
76
+ pluginId: string;
77
+
78
+ /** Working directory for plugin */
79
+ workDir: string;
80
+
81
+ /** Data directory for persistent storage */
82
+ dataDir: string;
83
+
84
+ /** Temporary directory */
85
+ tempDir: string;
86
+
87
+ /** Logger instance */
88
+ logger: PluginLogger;
89
+
90
+ /** Get a value from plugin config */
91
+ getConfig: (key: string, defaultValue?: unknown) => unknown;
92
+
93
+ /** Set a value in plugin config */
94
+ setConfig: (key: string, value: unknown) => Promise<void>;
95
+ }
96
+
97
+ export interface PluginLogger {
98
+ debug: (message: string, ...args: unknown[]) => void;
99
+ info: (message: string, ...args: unknown[]) => void;
100
+ warn: (message: string, ...args: unknown[]) => void;
101
+ error: (message: string, ...args: unknown[]) => void;
102
+ }
103
+
104
+ // ============================================================
105
+ // PLUGIN ERROR
106
+ // ============================================================
107
+
108
+ export class PluginError extends Error {
109
+ constructor(
110
+ public readonly pluginId: string,
111
+ public readonly code: string,
112
+ message: string,
113
+ public readonly cause?: Error
114
+ ) {
115
+ super(`[${pluginId}] ${code}: ${message}`);
116
+ this.name = "PluginError";
117
+ }
118
+ }
119
+
120
+ export enum PluginErrorCode {
121
+ /** Invalid configuration */
122
+ INVALID_CONFIG = "INVALID_CONFIG",
123
+
124
+ /** Authentication failed */
125
+ AUTH_FAILED = "AUTH_FAILED",
126
+
127
+ /** Rate limit exceeded */
128
+ RATE_LIMITED = "RATE_LIMITED",
129
+
130
+ /** Network error */
131
+ NETWORK_ERROR = "NETWORK_ERROR",
132
+
133
+ /** Account not found */
134
+ ACCOUNT_NOT_FOUND = "ACCOUNT_NOT_FOUND",
135
+
136
+ /** Feature not supported */
137
+ NOT_SUPPORTED = "NOT_SUPPORTED",
138
+
139
+ /** Internal plugin error */
140
+ INTERNAL_ERROR = "INTERNAL_ERROR",
141
+ }
142
+
143
+ // ============================================================
144
+ // CHANNEL EVENTS
145
+ // ============================================================
146
+
147
+ export type ChannelEventType =
148
+ | "message"
149
+ | "message_edit"
150
+ | "message_delete"
151
+ | "reaction"
152
+ | "typing"
153
+ | "presence"
154
+ | "error";
155
+
156
+ export interface ChannelEvent {
157
+ type: ChannelEventType;
158
+ channelId: ChannelId;
159
+ timestamp: Date;
160
+ data: unknown;
161
+ }