@shadowob/sdk 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ // Re-export shared event constants for convenience
2
+
3
+ export type { ClientEvent, ServerEvent } from '@shadowob/shared'
4
+ export { CLIENT_EVENTS, SERVER_EVENTS } from '@shadowob/shared'
5
+
6
+ // ─── Room helpers ───────────────────────────────────────────────────────────
7
+
8
+ /** Build a Socket.IO room name for a channel */
9
+ export const channelRoom = (channelId: string) => `channel:${channelId}` as const
10
+
11
+ /** Build a Socket.IO room name for a thread */
12
+ export const threadRoom = (threadId: string) => `thread:${threadId}` as const
13
+
14
+ /** Build a Socket.IO room name for user-level notifications */
15
+ export const userRoom = (userId: string) => `user:${userId}` as const
package/src/index.ts ADDED
@@ -0,0 +1,47 @@
1
+ // Client
2
+ export { ShadowClient } from './client'
3
+ export type { ClientEvent, ServerEvent } from './constants'
4
+ // Constants & room helpers
5
+ export {
6
+ CLIENT_EVENTS,
7
+ channelRoom,
8
+ SERVER_EVENTS,
9
+ threadRoom,
10
+ userRoom,
11
+ } from './constants'
12
+ export type { ShadowSocketOptions } from './socket'
13
+ // Socket
14
+ export { ShadowSocket } from './socket'
15
+
16
+ // Types
17
+ export type {
18
+ ChannelCreatedPayload,
19
+ ChannelMemberAddedPayload,
20
+ ChannelMemberRemovedPayload,
21
+ ClientEventMap,
22
+ DmMessage,
23
+ MemberJoinPayload,
24
+ MemberLeavePayload,
25
+ MessageDeletedPayload,
26
+ PolicyChangedPayload,
27
+ PresenceActivityPayload,
28
+ PresenceChangePayload,
29
+ ReactionPayload,
30
+ ServerEventMap,
31
+ ServerJoinedPayload,
32
+ ShadowAttachment,
33
+ ShadowChannel,
34
+ ShadowChannelPolicy,
35
+ ShadowDmChannel,
36
+ ShadowInviteCode,
37
+ ShadowMember,
38
+ ShadowMessage,
39
+ ShadowNotification,
40
+ ShadowRemoteChannel,
41
+ ShadowRemoteConfig,
42
+ ShadowRemoteServer,
43
+ ShadowServer,
44
+ ShadowThread,
45
+ ShadowUser,
46
+ TypingPayload,
47
+ } from './types'
package/src/socket.ts ADDED
@@ -0,0 +1,180 @@
1
+ import { io, type Socket } from 'socket.io-client'
2
+ import type { ClientEventMap, ServerEventMap } from './types'
3
+
4
+ export interface ShadowSocketOptions {
5
+ /** Shadow server base URL (e.g. "https://shadow.example.com") */
6
+ serverUrl: string
7
+ /** JWT token for authentication */
8
+ token: string
9
+ /** Socket.IO transports (default: ['websocket']) */
10
+ transports?: string[]
11
+ /** Auto-reconnect on disconnect (default: true) */
12
+ autoReconnect?: boolean
13
+ /** Reconnection delay in ms (default: 1000) */
14
+ reconnectionDelay?: number
15
+ }
16
+
17
+ type ServerEventName = keyof ServerEventMap
18
+ type ServerEventHandler<E extends ServerEventName> = ServerEventMap[E]
19
+
20
+ /**
21
+ * Shadow real-time event listener.
22
+ *
23
+ * Wraps Socket.IO with strongly-typed events that match the Shadow server
24
+ * gateway broadcasts. Provides channel/thread room management and
25
+ * convenience methods for sending messages and typing indicators.
26
+ */
27
+ export class ShadowSocket {
28
+ private socket: Socket
29
+ private _connected = false
30
+
31
+ constructor(options: ShadowSocketOptions) {
32
+ this.socket = io(options.serverUrl, {
33
+ auth: { token: options.token },
34
+ transports: options.transports ?? ['websocket'],
35
+ autoConnect: false,
36
+ reconnection: options.autoReconnect ?? true,
37
+ reconnectionDelay: options.reconnectionDelay ?? 1000,
38
+ })
39
+
40
+ this.socket.on('connect', () => {
41
+ this._connected = true
42
+ })
43
+ this.socket.on('disconnect', () => {
44
+ this._connected = false
45
+ })
46
+ }
47
+
48
+ /** Whether the socket is currently connected */
49
+ get connected(): boolean {
50
+ return this._connected
51
+ }
52
+
53
+ /** The underlying Socket.IO socket instance */
54
+ get raw(): Socket {
55
+ return this.socket
56
+ }
57
+
58
+ // ── Connection lifecycle ──────────────────────────────────────────────
59
+
60
+ /** Connect to the Shadow server */
61
+ connect(): void {
62
+ if (!this.socket.connected) {
63
+ this.socket.connect()
64
+ }
65
+ }
66
+
67
+ /** Disconnect from the Shadow server */
68
+ disconnect(): void {
69
+ this.socket.disconnect()
70
+ }
71
+
72
+ /** Wait until the socket is connected (resolves immediately if already connected) */
73
+ waitForConnect(timeoutMs = 5000): Promise<void> {
74
+ if (this.socket.connected) return Promise.resolve()
75
+ return new Promise<void>((resolve, reject) => {
76
+ const timer = setTimeout(() => {
77
+ reject(new Error(`Socket connect timeout after ${timeoutMs}ms`))
78
+ }, timeoutMs)
79
+ this.socket.once('connect', () => {
80
+ clearTimeout(timer)
81
+ resolve()
82
+ })
83
+ })
84
+ }
85
+
86
+ // ── Typed event listeners ─────────────────────────────────────────────
87
+
88
+ /** Listen for a server event */
89
+ on<E extends ServerEventName>(event: E, handler: ServerEventHandler<E>): this {
90
+ this.socket.on(event as string, handler as (...args: unknown[]) => void)
91
+ return this
92
+ }
93
+
94
+ /** Listen for a server event (one-time) */
95
+ once<E extends ServerEventName>(event: E, handler: ServerEventHandler<E>): this {
96
+ this.socket.once(event as string, handler as (...args: unknown[]) => void)
97
+ return this
98
+ }
99
+
100
+ /** Remove a specific event listener */
101
+ off<E extends ServerEventName>(event: E, handler: ServerEventHandler<E>): this {
102
+ this.socket.off(event as string, handler as (...args: unknown[]) => void)
103
+ return this
104
+ }
105
+
106
+ /** Remove all listeners for an event or all events */
107
+ removeAllListeners(event?: ServerEventName): this {
108
+ if (event) {
109
+ this.socket.removeAllListeners(event)
110
+ } else {
111
+ this.socket.removeAllListeners()
112
+ }
113
+ return this
114
+ }
115
+
116
+ // ── Connection event listeners ────────────────────────────────────────
117
+
118
+ /** Listen for raw connection events (connect, disconnect, connect_error) */
119
+ onConnect(handler: () => void): this {
120
+ this.socket.on('connect', handler)
121
+ return this
122
+ }
123
+
124
+ onDisconnect(handler: (reason: string) => void): this {
125
+ this.socket.on('disconnect', handler)
126
+ return this
127
+ }
128
+
129
+ onConnectError(handler: (error: Error) => void): this {
130
+ this.socket.on('connect_error', handler)
131
+ return this
132
+ }
133
+
134
+ // ── Room management ───────────────────────────────────────────────────
135
+
136
+ /** Join a channel room to receive its messages and events */
137
+ joinChannel(channelId: string): Promise<{ ok: boolean }> {
138
+ return new Promise((resolve) => {
139
+ this.socket.emit(
140
+ 'channel:join' satisfies keyof ClientEventMap,
141
+ { channelId },
142
+ (res: { ok: boolean }) => {
143
+ resolve(res ?? { ok: true })
144
+ },
145
+ )
146
+ })
147
+ }
148
+
149
+ /** Leave a channel room */
150
+ leaveChannel(channelId: string): void {
151
+ this.socket.emit('channel:leave' satisfies keyof ClientEventMap, { channelId })
152
+ }
153
+
154
+ // ── Client actions ────────────────────────────────────────────────────
155
+
156
+ /** Send a message via WebSocket (text-only; for file attachments use REST) */
157
+ sendMessage(data: {
158
+ channelId: string
159
+ content: string
160
+ threadId?: string
161
+ replyToId?: string
162
+ }): void {
163
+ this.socket.emit('message:send' satisfies keyof ClientEventMap, data)
164
+ }
165
+
166
+ /** Send a typing indicator */
167
+ sendTyping(channelId: string): void {
168
+ this.socket.emit('message:typing' satisfies keyof ClientEventMap, { channelId })
169
+ }
170
+
171
+ /** Update user presence status */
172
+ updatePresence(status: 'online' | 'idle' | 'dnd' | 'offline'): void {
173
+ this.socket.emit('presence:update' satisfies keyof ClientEventMap, { status })
174
+ }
175
+
176
+ /** Update activity status in a channel (e.g. 'thinking', 'working', null) */
177
+ updateActivity(channelId: string, activity: string | null): void {
178
+ this.socket.emit('presence:activity' satisfies keyof ClientEventMap, { channelId, activity })
179
+ }
180
+ }
package/src/types.ts ADDED
@@ -0,0 +1,252 @@
1
+ // ─── Shadow SDK Types ───────────────────────────────────────────────────────
2
+
3
+ /** Message returned by the Shadow REST API and Socket.IO broadcasts */
4
+ export interface ShadowMessage {
5
+ id: string
6
+ content: string
7
+ channelId: string
8
+ authorId: string
9
+ threadId?: string | null
10
+ replyToId?: string | null
11
+ isPinned?: boolean
12
+ createdAt: string
13
+ updatedAt: string
14
+ author?: {
15
+ id: string
16
+ username: string
17
+ displayName?: string | null
18
+ avatarUrl?: string | null
19
+ isBot?: boolean
20
+ }
21
+ attachments?: ShadowAttachment[]
22
+ }
23
+
24
+ export interface ShadowAttachment {
25
+ id: string
26
+ filename: string
27
+ url: string
28
+ contentType: string
29
+ size: number
30
+ width?: number | null
31
+ height?: number | null
32
+ }
33
+
34
+ export interface ShadowChannel {
35
+ id: string
36
+ name: string
37
+ type: string
38
+ serverId: string
39
+ description?: string | null
40
+ position?: number
41
+ }
42
+
43
+ export interface ShadowDmChannel {
44
+ id: string
45
+ user1Id: string
46
+ user2Id: string
47
+ createdAt: string
48
+ }
49
+
50
+ export interface ShadowThread {
51
+ id: string
52
+ name: string
53
+ channelId: string
54
+ parentMessageId: string
55
+ createdAt: string
56
+ }
57
+
58
+ export interface ShadowMember {
59
+ userId: string
60
+ serverId: string
61
+ role: string
62
+ user?: ShadowUser
63
+ }
64
+
65
+ export interface ShadowInviteCode {
66
+ id: string
67
+ code: string
68
+ createdBy: string
69
+ usedBy?: string | null
70
+ usedAt?: string | null
71
+ isActive: boolean
72
+ note?: string | null
73
+ createdAt: string
74
+ }
75
+
76
+ export interface ShadowServer {
77
+ id: string
78
+ name: string
79
+ slug: string
80
+ description: string | null
81
+ iconUrl: string | null
82
+ bannerUrl: string | null
83
+ homepageHtml: string | null
84
+ isPublic: boolean
85
+ }
86
+
87
+ export interface ShadowUser {
88
+ id: string
89
+ username: string
90
+ displayName?: string
91
+ avatarUrl?: string
92
+ isBot?: boolean
93
+ agentId?: string
94
+ }
95
+
96
+ export interface ShadowNotification {
97
+ id: string
98
+ userId: string
99
+ type: string
100
+ title: string
101
+ body: string
102
+ referenceId?: string
103
+ referenceType?: string
104
+ isRead: boolean
105
+ createdAt: string
106
+ }
107
+
108
+ // ─── Channel Policy Types ───────────────────────────────────────────────────
109
+
110
+ export interface ShadowChannelPolicy {
111
+ listen: boolean
112
+ reply: boolean
113
+ mentionOnly: boolean
114
+ config: Record<string, unknown>
115
+ }
116
+
117
+ export interface ShadowRemoteChannel {
118
+ id: string
119
+ name: string
120
+ type: string
121
+ policy: ShadowChannelPolicy
122
+ }
123
+
124
+ export interface ShadowRemoteServer {
125
+ id: string
126
+ name: string
127
+ slug?: string
128
+ iconUrl?: string | null
129
+ defaultPolicy: ShadowChannelPolicy
130
+ channels: ShadowRemoteChannel[]
131
+ }
132
+
133
+ export interface ShadowRemoteConfig {
134
+ agentId: string
135
+ botUserId: string
136
+ servers: ShadowRemoteServer[]
137
+ }
138
+
139
+ // ─── Socket Event Payloads ──────────────────────────────────────────────────
140
+
141
+ export interface TypingPayload {
142
+ channelId: string
143
+ userId: string
144
+ username: string
145
+ }
146
+
147
+ export interface PresenceChangePayload {
148
+ userId: string
149
+ status: 'online' | 'idle' | 'dnd' | 'offline'
150
+ }
151
+
152
+ export interface PresenceActivityPayload {
153
+ userId: string
154
+ activity: string | null
155
+ channelId: string
156
+ }
157
+
158
+ export interface MemberJoinPayload {
159
+ channelId: string
160
+ userId: string
161
+ }
162
+
163
+ export interface MemberLeavePayload {
164
+ channelId: string
165
+ userId: string
166
+ }
167
+
168
+ export interface ReactionPayload {
169
+ messageId: string
170
+ userId: string
171
+ emoji: string
172
+ }
173
+
174
+ export interface MessageDeletedPayload {
175
+ id: string
176
+ channelId: string
177
+ }
178
+
179
+ export interface ChannelCreatedPayload {
180
+ id: string
181
+ name: string
182
+ type: string
183
+ serverId: string
184
+ }
185
+
186
+ export interface ChannelMemberAddedPayload {
187
+ channelId: string
188
+ userId: string
189
+ }
190
+
191
+ export interface ChannelMemberRemovedPayload {
192
+ channelId: string
193
+ userId: string
194
+ }
195
+
196
+ export interface ServerJoinedPayload {
197
+ serverId: string
198
+ serverName: string
199
+ }
200
+
201
+ export interface PolicyChangedPayload {
202
+ agentId: string
203
+ serverId: string
204
+ channelId?: string | null
205
+ }
206
+
207
+ export interface DmMessage {
208
+ id: string
209
+ content: string
210
+ senderId: string
211
+ receiverId: string
212
+ createdAt: string
213
+ }
214
+
215
+ // ─── Socket Event Map ───────────────────────────────────────────────────────
216
+
217
+ /** Events the server pushes to the client */
218
+ export interface ServerEventMap {
219
+ 'message:new': (message: ShadowMessage) => void
220
+ 'message:updated': (message: ShadowMessage) => void
221
+ 'message:deleted': (payload: MessageDeletedPayload) => void
222
+ 'member:typing': (payload: TypingPayload) => void
223
+ 'member:join': (payload: MemberJoinPayload) => void
224
+ 'member:leave': (payload: MemberLeavePayload) => void
225
+ 'presence:change': (payload: PresenceChangePayload) => void
226
+ 'presence:activity': (payload: PresenceActivityPayload) => void
227
+ 'reaction:add': (payload: ReactionPayload) => void
228
+ 'reaction:remove': (payload: ReactionPayload) => void
229
+ 'notification:new': (notification: ShadowNotification) => void
230
+ 'dm:message:new': (message: DmMessage) => void
231
+ 'channel:created': (payload: ChannelCreatedPayload) => void
232
+ 'channel:member-added': (payload: ChannelMemberAddedPayload) => void
233
+ 'channel:member-removed': (payload: ChannelMemberRemovedPayload) => void
234
+ 'server:joined': (payload: ServerJoinedPayload) => void
235
+ 'agent:policy-changed': (payload: PolicyChangedPayload) => void
236
+ error: (payload: { message: string }) => void
237
+ }
238
+
239
+ /** Events the client sends to the server */
240
+ export interface ClientEventMap {
241
+ 'channel:join': (data: { channelId: string }, ack?: (res: { ok: boolean }) => void) => void
242
+ 'channel:leave': (data: { channelId: string }) => void
243
+ 'message:send': (data: {
244
+ channelId: string
245
+ content: string
246
+ threadId?: string
247
+ replyToId?: string
248
+ }) => void
249
+ 'message:typing': (data: { channelId: string }) => void
250
+ 'presence:update': (data: { status: 'online' | 'idle' | 'dnd' | 'offline' }) => void
251
+ 'presence:activity': (data: { channelId: string; activity: string | null }) => void
252
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "outDir": "./dist"
6
+ },
7
+ "include": ["src"]
8
+ }