@neta-art/cohub 1.0.0 → 1.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.
package/README.md CHANGED
@@ -13,6 +13,39 @@ npm install @neta-art/cohub @neta-art/cohub-protocol
13
13
  ```ts
14
14
  import { createCohubClient } from "@neta-art/cohub";
15
15
 
16
+ const client = createCohubClient({
17
+ getAccessToken: async () => localStorage.getItem("token"),
18
+ });
19
+ ```
20
+
21
+ The SDK connects to production by default:
22
+
23
+ - API: `https://api.cohub.run`
24
+ - WebSocket: `wss://gateway.cohub.run/ws`
25
+
26
+ Use development with `ENV=dev` in Node.js:
27
+
28
+ ```bash
29
+ ENV=dev node app.js
30
+ ```
31
+
32
+ Or select it explicitly in code:
33
+
34
+ ```ts
35
+ const client = createCohubClient({
36
+ env: "dev",
37
+ getAccessToken: async () => localStorage.getItem("token"),
38
+ });
39
+ ```
40
+
41
+ Development uses:
42
+
43
+ - API: `https://api-dev.cohub.run`
44
+ - WebSocket: `wss://gateway-dev.cohub.run/ws`
45
+
46
+ Custom endpoints are still supported when needed:
47
+
48
+ ```ts
16
49
  const client = createCohubClient({
17
50
  baseUrl: "https://api.example.com",
18
51
  getAccessToken: async () => localStorage.getItem("token"),
@@ -87,7 +120,6 @@ If you only want HTTP transport, use the dedicated entry:
87
120
  import { createHttpClient } from "@neta-art/cohub/http";
88
121
 
89
122
  const http = createHttpClient({
90
- baseUrl: "https://api.example.com",
91
123
  getAccessToken: async () => localStorage.getItem("token"),
92
124
  });
93
125
 
@@ -104,7 +136,6 @@ If you need direct realtime transport access, use the websocket entry:
104
136
  import { createWebsocketClient } from "@neta-art/cohub/websocket";
105
137
 
106
138
  const ws = createWebsocketClient({
107
- url: "https://gateway.example.com",
108
139
  getAccessToken: async () => localStorage.getItem("token"),
109
140
  });
110
141
 
@@ -118,15 +149,4 @@ This SDK is intentionally built around Cohub's co-creation model:
118
149
  - work with `space(...)` and `session(...)` as the primary creative surface
119
150
  - send messages through `session.messages.send(...)`
120
151
  - subscribe through `space.subscribe(...)` and `session.subscribe(...)`
121
- - keep protocol details behind the SDK surface
122
-
123
- ## Publish checklist
124
-
125
- Before publishing:
126
-
127
- 1. build the protocol package: `pnpm --filter @neta-art/cohub-protocol build`
128
- 2. build this package: `pnpm --filter @neta-art/cohub build`
129
- 3. typecheck the protocol package: `pnpm --filter @neta-art/cohub-protocol typecheck`
130
- 4. typecheck this package: `pnpm --filter @neta-art/cohub typecheck`
131
- 5. verify consuming apps still typecheck
132
- 6. verify `dist/` contains `index`, `http`, and `websocket` outputs
152
+ - keep HTTP and realtime transports separate but coordinated
@@ -0,0 +1,20 @@
1
+ import type { HttpTransport } from "../transport.js";
2
+ import type { SpaceInvitation, CreateInvitationInput, CreateInvitationResponse, InvitationDetail, AcceptInvitationResponse } from "../types.js";
3
+ export declare class SpaceInvitationsApi {
4
+ private readonly transport;
5
+ private readonly spaceId;
6
+ constructor(transport: HttpTransport, spaceId: string);
7
+ list(): Promise<{
8
+ items: SpaceInvitation[];
9
+ }>;
10
+ create(input?: CreateInvitationInput): Promise<CreateInvitationResponse>;
11
+ revoke(token: string): Promise<{
12
+ ok: true;
13
+ }>;
14
+ }
15
+ export declare class PublicInviteApi {
16
+ private readonly transport;
17
+ constructor(transport: HttpTransport);
18
+ get(token: string): Promise<InvitationDetail>;
19
+ accept(token: string): Promise<AcceptInvitationResponse>;
20
+ }
@@ -0,0 +1,36 @@
1
+ export class SpaceInvitationsApi {
2
+ transport;
3
+ spaceId;
4
+ constructor(transport, spaceId) {
5
+ this.transport = transport;
6
+ this.spaceId = spaceId;
7
+ }
8
+ list() {
9
+ return this.transport.request(`/api/spaces/${this.spaceId}/invitations`);
10
+ }
11
+ create(input) {
12
+ return this.transport.request(`/api/spaces/${this.spaceId}/invitations`, {
13
+ method: "POST",
14
+ headers: { "Content-Type": "application/json" },
15
+ body: JSON.stringify(input ?? {}),
16
+ });
17
+ }
18
+ revoke(token) {
19
+ return this.transport.request(`/api/spaces/${this.spaceId}/invitations/${token}`, { method: "DELETE" });
20
+ }
21
+ }
22
+ // Public invite API (no auth required for viewing)
23
+ export class PublicInviteApi {
24
+ transport;
25
+ constructor(transport) {
26
+ this.transport = transport;
27
+ }
28
+ get(token) {
29
+ return this.transport.request(`/api/invite/${token}`);
30
+ }
31
+ accept(token) {
32
+ return this.transport.request(`/api/invite/${token}/accept`, {
33
+ method: "POST",
34
+ });
35
+ }
36
+ }
@@ -1,8 +1,7 @@
1
- import type { Fetch } from "../transport.js";
1
+ import type { HttpTransport } from "../transport.js";
2
2
  import type { ModelCatalogEntry } from "../types.js";
3
3
  export declare class ModelsApi {
4
- private readonly fetcher;
5
- private readonly baseUrl;
6
- constructor(fetcher: Fetch, baseUrl: string);
7
- list(customFetch?: Fetch): Promise<Record<string, ModelCatalogEntry[]>>;
4
+ private readonly transport;
5
+ constructor(transport: HttpTransport);
6
+ list(): Promise<Record<string, ModelCatalogEntry[]>>;
8
7
  }
@@ -1,17 +1,9 @@
1
1
  export class ModelsApi {
2
- fetcher;
3
- baseUrl;
4
- constructor(fetcher, baseUrl) {
5
- this.fetcher = fetcher;
6
- this.baseUrl = baseUrl;
2
+ transport;
3
+ constructor(transport) {
4
+ this.transport = transport;
7
5
  }
8
- async list(customFetch) {
9
- const fetchImpl = customFetch ?? this.fetcher;
10
- const url = this.baseUrl ? `${this.baseUrl}/api/models` : "/api/models";
11
- const response = await fetchImpl(url);
12
- if (!response.ok) {
13
- throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`);
14
- }
15
- return response.json();
6
+ async list() {
7
+ return this.transport.request("/api/models");
16
8
  }
17
9
  }
@@ -0,0 +1,10 @@
1
+ import type { Fetch } from "../transport.js";
2
+ import type { PromptTemplateCatalogResponse } from "../types.js";
3
+ export declare class PromptsApi {
4
+ private readonly fetcher;
5
+ private readonly baseUrl;
6
+ constructor(fetcher: Fetch, baseUrl: string);
7
+ list(options?: {
8
+ spaceId?: string;
9
+ }, customFetch?: Fetch): Promise<PromptTemplateCatalogResponse>;
10
+ }
@@ -0,0 +1,22 @@
1
+ export class PromptsApi {
2
+ fetcher;
3
+ baseUrl;
4
+ constructor(fetcher, baseUrl) {
5
+ this.fetcher = fetcher;
6
+ this.baseUrl = baseUrl;
7
+ }
8
+ async list(options, customFetch) {
9
+ const fetchImpl = customFetch ?? this.fetcher;
10
+ const params = new URLSearchParams();
11
+ if (options?.spaceId)
12
+ params.set("spaceId", options.spaceId);
13
+ const query = params.toString();
14
+ const base = this.baseUrl ? `${this.baseUrl}/api/prompts` : "/api/prompts";
15
+ const url = query ? `${base}?${query}` : base;
16
+ const response = await fetchImpl(url);
17
+ if (!response.ok) {
18
+ throw new Error(`Failed to fetch prompt templates: ${response.status} ${response.statusText}`);
19
+ }
20
+ return response.json();
21
+ }
22
+ }
@@ -8,7 +8,7 @@ export class SessionAccessApi {
8
8
  }
9
9
  set(sessionId, body) {
10
10
  return this.transport.request(`/api/sessions/${sessionId}/access`, {
11
- method: "PUT",
11
+ method: "PATCH",
12
12
  headers: { "Content-Type": "application/json" },
13
13
  body: JSON.stringify(body),
14
14
  });
@@ -1,6 +1,7 @@
1
1
  import type { WebsocketClient, WebsocketEventPayload } from "../websocket.js";
2
2
  import type { HttpTransport, Fetch } from "../transport.js";
3
- import type { CheckpointRecord, ContentBlock, SessionMessagesPaginatedResponse, SessionMessagesResponse, SessionRecord, SpaceAccessPolicy, SpaceBootstrapSource, SpaceChannelBindingInput, SpaceCheckpointDetailResponse, SpaceCreateResponse, SpaceEnvInput, SpaceFsFileResponse, SpaceFsMoveInput, SpaceFsTreeResponse, SpaceFsWriteFileInput, SpaceMember, SpaceRecord, SpaceRole, SpaceSessionsResponse } from "../types.js";
3
+ import type { CheckpointRecord, ContentBlock, SessionMessageResponse, SessionMessagesPaginatedResponse, SessionMessagesResponse, SessionRecord, SpaceAccessPolicy, SpaceBootstrapSource, SpaceChannelBindingInput, SpaceCheckpointDetailResponse, SpaceCreateResponse, SpaceEnvInput, SpaceFsFileResponse, SpaceFsMoveInput, SpaceFsTreeResponse, SpaceFsUploadResponse, SpaceUsageResponse, SpaceFsWriteFileInput, SpaceMember, SpaceRecord, SpaceRole, SpaceSessionsResponse } from "../types.js";
4
+ import { SpaceInvitationsApi } from "./invitations.js";
4
5
  export type SessionSubscriptionHandlers = {
5
6
  progress?: (event: WebsocketEventPayload) => void;
6
7
  final?: (event: WebsocketEventPayload) => void;
@@ -58,6 +59,7 @@ export declare class SpaceFilesApi {
58
59
  fromPath: string;
59
60
  toPath: string;
60
61
  }>;
62
+ upload(files: File[], dir?: string): Promise<SpaceFsUploadResponse>;
61
63
  }
62
64
  declare class SessionMessagesClient {
63
65
  private readonly transport;
@@ -67,6 +69,9 @@ declare class SessionMessagesClient {
67
69
  private lastSentAt;
68
70
  constructor(transport: HttpTransport, sessionId: string);
69
71
  list(customFetch?: Fetch): Promise<SessionMessagesResponse>;
72
+ get(messageId: string, optionsOrFetch?: {
73
+ detail?: "summary" | "full";
74
+ } | Fetch, customFetch?: Fetch): Promise<SessionMessageResponse>;
70
75
  listPaginated(options?: {
71
76
  cursor?: number;
72
77
  limit?: number;
@@ -96,6 +101,9 @@ export declare class SessionClient {
96
101
  space: SpaceRecord;
97
102
  session: SessionRecord;
98
103
  }>;
104
+ rename(title: string | null, customFetch?: Fetch): Promise<{
105
+ session: SessionRecord;
106
+ }>;
99
107
  subscribe(handlers: SessionSubscriptionHandlers): () => void;
100
108
  on(type: SessionEventName, handler: (event: WebsocketEventPayload) => void): () => void;
101
109
  }
@@ -111,9 +119,20 @@ export declare class SpaceSessionsApi {
111
119
  ok: true;
112
120
  session: SessionRecord;
113
121
  }>;
114
- list(customFetch?: Fetch): Promise<SpaceSessionsResponse>;
122
+ list(optionsOrFetch?: {
123
+ limit?: number;
124
+ cursor?: string | null;
125
+ } | Fetch, customFetch?: Fetch): Promise<SpaceSessionsResponse>;
115
126
  byId(sessionId: string): SessionClient;
116
127
  }
128
+ export type WebSocketConnectionState = {
129
+ state: "connecting" | "reconnecting" | "open" | "closed" | "error";
130
+ willReconnect: boolean;
131
+ connectionId?: string | null;
132
+ attempt?: number;
133
+ delayMs?: number;
134
+ recoverable?: boolean;
135
+ };
117
136
  export declare class SpaceEventsApi {
118
137
  private readonly websocketClient;
119
138
  private readonly spaceId;
@@ -143,6 +162,55 @@ export declare class SpaceAccessApi {
143
162
  anonymous_user?: SpaceRole | null;
144
163
  }): Promise<SpaceAccessPolicy>;
145
164
  }
165
+ export declare class SpaceUsageApi {
166
+ private readonly transport;
167
+ private readonly spaceId;
168
+ constructor(transport: HttpTransport, spaceId: string);
169
+ get(days?: number, customFetch?: Fetch): Promise<SpaceUsageResponse>;
170
+ }
171
+ export type SpaceChannelBindingRecord = {
172
+ id: string;
173
+ spaceId: string;
174
+ channelId: string;
175
+ config: Record<string, unknown> | null;
176
+ createdAt: string;
177
+ channel: {
178
+ id: string;
179
+ userUuid: string;
180
+ provider: string;
181
+ name: string;
182
+ status: string;
183
+ createdAt: string;
184
+ updatedAt: string;
185
+ } | null;
186
+ };
187
+ export declare class SpaceChannelsApi {
188
+ private readonly transport;
189
+ private readonly spaceId;
190
+ constructor(transport: HttpTransport, spaceId: string);
191
+ list(): Promise<SpaceChannelBindingRecord[]>;
192
+ bind(channelId: string, config?: Record<string, unknown> | null): Promise<SpaceChannelBindingRecord>;
193
+ unbind(channelId: string): Promise<{
194
+ ok: true;
195
+ }>;
196
+ }
197
+ export declare class SpaceEnvApi {
198
+ private readonly transport;
199
+ private readonly spaceId;
200
+ constructor(transport: HttpTransport, spaceId: string);
201
+ list(): Promise<{
202
+ env: SpaceEnvInput[];
203
+ }>;
204
+ create(input: SpaceEnvInput): Promise<{
205
+ env: SpaceEnvInput[];
206
+ }>;
207
+ update(name: string, value: string): Promise<{
208
+ env: SpaceEnvInput[];
209
+ }>;
210
+ remove(name: string): Promise<{
211
+ env: SpaceEnvInput[];
212
+ }>;
213
+ }
146
214
  export declare class SpaceCheckpointsApi {
147
215
  private readonly transport;
148
216
  private readonly spaceId;
@@ -165,6 +233,10 @@ export declare class SpaceClient {
165
233
  readonly members: SpaceMembersApi;
166
234
  readonly access: SpaceAccessApi;
167
235
  readonly checkpoints: SpaceCheckpointsApi;
236
+ readonly usage: SpaceUsageApi;
237
+ readonly channels: SpaceChannelsApi;
238
+ readonly env: SpaceEnvApi;
239
+ readonly invitations: SpaceInvitationsApi;
168
240
  constructor(id: string, transport: HttpTransport, websocketClient: WebsocketClient | null);
169
241
  get(customFetch?: Fetch): Promise<SpaceRecord>;
170
242
  rename(name: string): Promise<{
@@ -1,4 +1,5 @@
1
1
  import { ensureRealtimeConnected } from "../realtime.js";
2
+ import { SpaceInvitationsApi } from "./invitations.js";
2
3
  const DEFAULT_DEDUP_WINDOW_MS = 2000;
3
4
  const toSessionEventName = (type) => {
4
5
  switch (type) {
@@ -90,6 +91,19 @@ export class SpaceFilesApi {
90
91
  body: JSON.stringify(input),
91
92
  });
92
93
  }
94
+ upload(files, dir = "") {
95
+ const params = new URLSearchParams();
96
+ if (dir)
97
+ params.set("dir", dir);
98
+ const query = params.toString();
99
+ const formData = new FormData();
100
+ for (const file of files)
101
+ formData.append("files", file);
102
+ return this.transport.request(`/api/spaces/${this.spaceId}/fs/upload${query ? `?${query}` : ""}`, {
103
+ method: "POST",
104
+ body: formData,
105
+ });
106
+ }
93
107
  }
94
108
  class SessionMessagesClient {
95
109
  transport;
@@ -106,6 +120,17 @@ class SessionMessagesClient {
106
120
  fetch: customFetch,
107
121
  });
108
122
  }
123
+ get(messageId, optionsOrFetch, customFetch) {
124
+ const options = typeof optionsOrFetch === "function" ? undefined : optionsOrFetch;
125
+ const fetch = typeof optionsOrFetch === "function" ? optionsOrFetch : customFetch;
126
+ const params = new URLSearchParams();
127
+ if (options?.detail)
128
+ params.set("detail", options.detail);
129
+ const query = params.toString();
130
+ return this.transport.request(`/api/sessions/${this.sessionId}/messages/${messageId}${query ? `?${query}` : ""}`, {
131
+ fetch,
132
+ });
133
+ }
109
134
  listPaginated(options, customFetch) {
110
135
  const params = new URLSearchParams();
111
136
  if (options?.cursor !== undefined)
@@ -201,6 +226,13 @@ export class SessionClient {
201
226
  fetch: customFetch,
202
227
  });
203
228
  }
229
+ rename(title, customFetch) {
230
+ return this.transport.request(`/api/sessions/${this.id}`, {
231
+ method: "PATCH",
232
+ body: JSON.stringify({ title }),
233
+ fetch: customFetch,
234
+ });
235
+ }
204
236
  subscribe(handlers) {
205
237
  return this.realtime.subscribe(handlers);
206
238
  }
@@ -226,9 +258,17 @@ export class SpaceSessionsApi {
226
258
  body: JSON.stringify(input ?? {}),
227
259
  });
228
260
  }
229
- list(customFetch) {
230
- return this.transport.request(`/api/spaces/${this.spaceId}/sessions`, {
231
- fetch: customFetch,
261
+ list(optionsOrFetch, customFetch) {
262
+ const options = typeof optionsOrFetch === "function" ? undefined : optionsOrFetch;
263
+ const fetch = typeof optionsOrFetch === "function" ? optionsOrFetch : customFetch;
264
+ const params = new URLSearchParams();
265
+ if (options?.limit !== undefined)
266
+ params.set("limit", String(options.limit));
267
+ if (options?.cursor)
268
+ params.set("cursor", options.cursor);
269
+ const query = params.toString();
270
+ return this.transport.request(`/api/spaces/${this.spaceId}/sessions${query ? `?${query}` : ""}`, {
271
+ fetch,
232
272
  });
233
273
  }
234
274
  byId(sessionId) {
@@ -301,12 +341,73 @@ export class SpaceAccessApi {
301
341
  }
302
342
  set(body) {
303
343
  return this.transport.request(`/api/spaces/${this.spaceId}/access`, {
304
- method: "PUT",
344
+ method: "PATCH",
305
345
  headers: { "Content-Type": "application/json" },
306
346
  body: JSON.stringify(body),
307
347
  });
308
348
  }
309
349
  }
350
+ export class SpaceUsageApi {
351
+ transport;
352
+ spaceId;
353
+ constructor(transport, spaceId) {
354
+ this.transport = transport;
355
+ this.spaceId = spaceId;
356
+ }
357
+ get(days = 30, customFetch) {
358
+ const params = new URLSearchParams({ days: String(days) });
359
+ return this.transport.request(`/api/spaces/${this.spaceId}/usage?${params.toString()}`, { fetch: customFetch });
360
+ }
361
+ }
362
+ export class SpaceChannelsApi {
363
+ transport;
364
+ spaceId;
365
+ constructor(transport, spaceId) {
366
+ this.transport = transport;
367
+ this.spaceId = spaceId;
368
+ }
369
+ list() {
370
+ return this.transport.request(`/api/spaces/${this.spaceId}/channels`);
371
+ }
372
+ bind(channelId, config) {
373
+ return this.transport.request(`/api/spaces/${this.spaceId}/channels/${channelId}`, {
374
+ method: "POST",
375
+ headers: { "Content-Type": "application/json" },
376
+ body: JSON.stringify({ config: config ?? null }),
377
+ });
378
+ }
379
+ unbind(channelId) {
380
+ return this.transport.request(`/api/spaces/${this.spaceId}/channels/${channelId}`, { method: "DELETE" });
381
+ }
382
+ }
383
+ export class SpaceEnvApi {
384
+ transport;
385
+ spaceId;
386
+ constructor(transport, spaceId) {
387
+ this.transport = transport;
388
+ this.spaceId = spaceId;
389
+ }
390
+ list() {
391
+ return this.transport.request(`/api/spaces/${this.spaceId}/env`);
392
+ }
393
+ create(input) {
394
+ return this.transport.request(`/api/spaces/${this.spaceId}/env`, {
395
+ method: "POST",
396
+ headers: { "Content-Type": "application/json" },
397
+ body: JSON.stringify(input),
398
+ });
399
+ }
400
+ update(name, value) {
401
+ return this.transport.request(`/api/spaces/${this.spaceId}/env/${encodeURIComponent(name)}`, {
402
+ method: "PUT",
403
+ headers: { "Content-Type": "application/json" },
404
+ body: JSON.stringify({ value }),
405
+ });
406
+ }
407
+ remove(name) {
408
+ return this.transport.request(`/api/spaces/${this.spaceId}/env/${encodeURIComponent(name)}`, { method: "DELETE" });
409
+ }
410
+ }
310
411
  export class SpaceCheckpointsApi {
311
412
  transport;
312
413
  spaceId;
@@ -337,6 +438,10 @@ export class SpaceClient {
337
438
  members;
338
439
  access;
339
440
  checkpoints;
441
+ usage;
442
+ channels;
443
+ env;
444
+ invitations;
340
445
  constructor(id, transport, websocketClient) {
341
446
  this.id = id;
342
447
  this.transport = transport;
@@ -346,6 +451,10 @@ export class SpaceClient {
346
451
  this.members = new SpaceMembersApi(transport, id);
347
452
  this.access = new SpaceAccessApi(transport, id);
348
453
  this.checkpoints = new SpaceCheckpointsApi(transport, id);
454
+ this.usage = new SpaceUsageApi(transport, id);
455
+ this.channels = new SpaceChannelsApi(transport, id);
456
+ this.env = new SpaceEnvApi(transport, id);
457
+ this.invitations = new SpaceInvitationsApi(transport, id);
349
458
  }
350
459
  get(customFetch) {
351
460
  return this.transport.request(`/api/spaces/${this.id}`, {
package/dist/client.d.ts CHANGED
@@ -1,22 +1,27 @@
1
1
  import { ChannelsApi } from "./apis/channels.js";
2
2
  import { CronJobsApi } from "./apis/cron-jobs.js";
3
3
  import { ModelsApi } from "./apis/models.js";
4
+ import { PromptsApi } from "./apis/prompts.js";
4
5
  import { SessionAccessApi } from "./apis/session-access.js";
5
- import { SpaceClient, SpacesApi } from "./apis/spaces.js";
6
+ import { SpaceClient, SpacesApi, type WebSocketConnectionState } from "./apis/spaces.js";
6
7
  import { TasksApi } from "./apis/tasks.js";
7
8
  import { UserApi } from "./apis/user.js";
9
+ import { PublicInviteApi } from "./apis/invitations.js";
8
10
  import { type CohubClientOptions } from "./transport.js";
9
11
  export declare class CohubClient {
10
12
  readonly spaces: SpacesApi;
11
13
  readonly channels: ChannelsApi;
12
14
  readonly user: UserApi;
13
15
  readonly models: ModelsApi;
16
+ readonly prompts: PromptsApi;
14
17
  readonly sessionAccess: SessionAccessApi;
15
18
  readonly tasks: TasksApi;
16
19
  readonly cronJobs: CronJobsApi;
20
+ readonly invite: PublicInviteApi;
17
21
  private readonly transport;
18
22
  private readonly websocketClient;
19
23
  constructor(options?: CohubClientOptions);
20
24
  space(spaceId: string): SpaceClient;
25
+ onConnection(handler: (state: WebSocketConnectionState) => void): () => void;
21
26
  }
22
27
  export declare const createCohubClient: (options?: CohubClientOptions) => CohubClient;
package/dist/client.js CHANGED
@@ -1,38 +1,94 @@
1
1
  import { ChannelsApi } from "./apis/channels.js";
2
2
  import { CronJobsApi } from "./apis/cron-jobs.js";
3
3
  import { ModelsApi } from "./apis/models.js";
4
+ import { PromptsApi } from "./apis/prompts.js";
4
5
  import { SessionAccessApi } from "./apis/session-access.js";
5
6
  import { SpaceClient, SpacesApi } from "./apis/spaces.js";
6
7
  import { TasksApi } from "./apis/tasks.js";
7
8
  import { UserApi } from "./apis/user.js";
9
+ import { PublicInviteApi } from "./apis/invitations.js";
8
10
  import { HttpTransport } from "./transport.js";
9
11
  import { createWebsocketClient } from "./websocket.js";
12
+ import { resolveApiBaseUrl, resolveWebsocketUrl } from "./environment.js";
10
13
  export class CohubClient {
11
14
  spaces;
12
15
  channels;
13
16
  user;
14
17
  models;
18
+ prompts;
15
19
  sessionAccess;
16
20
  tasks;
17
21
  cronJobs;
22
+ invite;
18
23
  transport;
19
24
  websocketClient;
20
25
  constructor(options = {}) {
26
+ const apiBaseUrl = resolveApiBaseUrl(options);
21
27
  this.transport = new HttpTransport(options);
22
28
  this.websocketClient = createWebsocketClient({
29
+ url: resolveWebsocketUrl({
30
+ env: options.websocket?.env ?? options.env,
31
+ url: options.websocket?.url,
32
+ }),
23
33
  ...options.websocket,
24
34
  getAccessToken: options.getAccessToken,
25
35
  });
26
36
  this.spaces = new SpacesApi(this.transport);
27
37
  this.channels = new ChannelsApi(this.transport);
28
- this.user = new UserApi(this.transport, options.baseUrl ?? "", options.setStoredAuthToken, options.clearStoredAuthToken);
29
- this.models = new ModelsApi(options.fetch ?? fetch, options.baseUrl ?? "");
38
+ this.user = new UserApi(this.transport, apiBaseUrl, options.setStoredAuthToken, options.clearStoredAuthToken);
39
+ this.models = new ModelsApi(this.transport);
40
+ this.prompts = new PromptsApi(options.fetch ?? fetch, apiBaseUrl);
30
41
  this.sessionAccess = new SessionAccessApi(this.transport);
31
42
  this.tasks = new TasksApi(this.transport);
32
43
  this.cronJobs = new CronJobsApi(this.transport);
44
+ this.invite = new PublicInviteApi(this.transport);
33
45
  }
34
46
  space(spaceId) {
35
47
  return new SpaceClient(spaceId, this.transport, this.websocketClient);
36
48
  }
49
+ onConnection(handler) {
50
+ const connectingCleanup = this.websocketClient.on("connecting", (payload) => {
51
+ handler({
52
+ state: payload.isReconnect ? "reconnecting" : "connecting",
53
+ willReconnect: payload.isReconnect,
54
+ attempt: payload.attempt,
55
+ });
56
+ });
57
+ const reconnectingCleanup = this.websocketClient.on("reconnecting", (payload) => {
58
+ handler({
59
+ state: "reconnecting",
60
+ willReconnect: true,
61
+ attempt: payload.attempt,
62
+ delayMs: payload.delayMs,
63
+ });
64
+ });
65
+ const openCleanup = this.websocketClient.on("open", (payload) => {
66
+ handler({
67
+ state: "open",
68
+ willReconnect: false,
69
+ connectionId: payload.connectionId,
70
+ });
71
+ });
72
+ const closeCleanup = this.websocketClient.on("close", (payload) => {
73
+ handler({
74
+ state: "closed",
75
+ willReconnect: payload.willReconnect,
76
+ });
77
+ });
78
+ const errorCleanup = this.websocketClient.on("error", (payload) => {
79
+ handler({
80
+ state: "error",
81
+ willReconnect: payload.recoverable,
82
+ recoverable: payload.recoverable,
83
+ });
84
+ });
85
+ return () => {
86
+ connectingCleanup();
87
+ reconnectingCleanup();
88
+ openCleanup();
89
+ closeCleanup();
90
+ errorCleanup();
91
+ };
92
+ }
37
93
  }
38
94
  export const createCohubClient = (options) => new CohubClient(options);
@@ -0,0 +1,22 @@
1
+ export type CohubEnvironment = "prod" | "dev";
2
+ export declare const COHUB_ENVIRONMENTS: {
3
+ readonly prod: {
4
+ readonly apiBaseUrl: "https://api.cohub.run";
5
+ readonly websocketUrl: "wss://gateway.cohub.run/ws";
6
+ };
7
+ readonly dev: {
8
+ readonly apiBaseUrl: "https://api-dev.cohub.run";
9
+ readonly websocketUrl: "wss://gateway-dev.cohub.run/ws";
10
+ };
11
+ };
12
+ export declare const resolveCohubEnvironment: (env?: CohubEnvironment) => CohubEnvironment;
13
+ export declare const normalizeBaseUrl: (url: string) => string;
14
+ export declare const normalizeWebsocketUrl: (input: string) => string;
15
+ export declare const resolveApiBaseUrl: (options?: {
16
+ baseUrl?: string;
17
+ env?: CohubEnvironment;
18
+ }) => string;
19
+ export declare const resolveWebsocketUrl: (options?: {
20
+ url?: string;
21
+ env?: CohubEnvironment;
22
+ }) => string;
@@ -0,0 +1,37 @@
1
+ export const COHUB_ENVIRONMENTS = {
2
+ prod: {
3
+ apiBaseUrl: "https://api.cohub.run",
4
+ websocketUrl: "wss://gateway.cohub.run/ws",
5
+ },
6
+ dev: {
7
+ apiBaseUrl: "https://api-dev.cohub.run",
8
+ websocketUrl: "wss://gateway-dev.cohub.run/ws",
9
+ },
10
+ };
11
+ const readRuntimeEnv = () => {
12
+ const runtime = globalThis;
13
+ return runtime.process?.env?.ENV;
14
+ };
15
+ export const resolveCohubEnvironment = (env) => {
16
+ if (env)
17
+ return env;
18
+ return readRuntimeEnv() === "dev" ? "dev" : "prod";
19
+ };
20
+ export const normalizeBaseUrl = (url) => url.trim().replace(/\/+$/, "");
21
+ export const normalizeWebsocketUrl = (input) => {
22
+ const trimmed = normalizeBaseUrl(input);
23
+ const withProtocol = trimmed
24
+ .replace(/^http:/, "ws:")
25
+ .replace(/^https:/, "wss:");
26
+ return withProtocol.endsWith("/ws") ? withProtocol : `${withProtocol}/ws`;
27
+ };
28
+ export const resolveApiBaseUrl = (options = {}) => {
29
+ if (options.baseUrl)
30
+ return normalizeBaseUrl(options.baseUrl);
31
+ return COHUB_ENVIRONMENTS[resolveCohubEnvironment(options.env)].apiBaseUrl;
32
+ };
33
+ export const resolveWebsocketUrl = (options = {}) => {
34
+ if (options.url)
35
+ return normalizeWebsocketUrl(options.url);
36
+ return COHUB_ENVIRONMENTS[resolveCohubEnvironment(options.env)].websocketUrl;
37
+ };
package/dist/http.d.ts CHANGED
@@ -1,19 +1,23 @@
1
1
  import { ChannelsApi } from "./apis/channels.js";
2
2
  import { CronJobsApi } from "./apis/cron-jobs.js";
3
3
  import { ModelsApi } from "./apis/models.js";
4
+ import { PromptsApi } from "./apis/prompts.js";
4
5
  import { SessionAccessApi } from "./apis/session-access.js";
5
6
  import { SpaceClient, SpacesApi } from "./apis/spaces.js";
6
7
  import { TasksApi } from "./apis/tasks.js";
7
8
  import { UserApi } from "./apis/user.js";
9
+ import { PublicInviteApi } from "./apis/invitations.js";
8
10
  import { HttpTransport, HttpError, type CohubClientOptions, type Fetch } from "./transport.js";
9
11
  export declare class CohubHttpClient {
10
12
  readonly spaces: SpacesApi;
11
13
  readonly channels: ChannelsApi;
12
14
  readonly user: UserApi;
13
15
  readonly models: ModelsApi;
16
+ readonly prompts: PromptsApi;
14
17
  readonly sessionAccess: SessionAccessApi;
15
18
  readonly tasks: TasksApi;
16
19
  readonly cronJobs: CronJobsApi;
20
+ readonly invite: PublicInviteApi;
17
21
  private readonly transport;
18
22
  constructor(options?: CohubClientOptions);
19
23
  space(spaceId: string): SpaceClient;
package/dist/http.js CHANGED
@@ -1,29 +1,37 @@
1
1
  import { ChannelsApi } from "./apis/channels.js";
2
2
  import { CronJobsApi } from "./apis/cron-jobs.js";
3
3
  import { ModelsApi } from "./apis/models.js";
4
+ import { PromptsApi } from "./apis/prompts.js";
4
5
  import { SessionAccessApi } from "./apis/session-access.js";
5
6
  import { SpaceClient, SpacesApi } from "./apis/spaces.js";
6
7
  import { TasksApi } from "./apis/tasks.js";
7
8
  import { UserApi } from "./apis/user.js";
9
+ import { PublicInviteApi } from "./apis/invitations.js";
8
10
  import { HttpTransport, HttpError } from "./transport.js";
11
+ import { resolveApiBaseUrl } from "./environment.js";
9
12
  export class CohubHttpClient {
10
13
  spaces;
11
14
  channels;
12
15
  user;
13
16
  models;
17
+ prompts;
14
18
  sessionAccess;
15
19
  tasks;
16
20
  cronJobs;
21
+ invite;
17
22
  transport;
18
23
  constructor(options = {}) {
24
+ const apiBaseUrl = resolveApiBaseUrl(options);
19
25
  this.transport = new HttpTransport(options);
20
26
  this.spaces = new SpacesApi(this.transport);
21
27
  this.channels = new ChannelsApi(this.transport);
22
- this.user = new UserApi(this.transport, options.baseUrl ?? "", options.setStoredAuthToken, options.clearStoredAuthToken);
23
- this.models = new ModelsApi(options.fetch ?? fetch, options.baseUrl ?? "");
28
+ this.user = new UserApi(this.transport, apiBaseUrl, options.setStoredAuthToken, options.clearStoredAuthToken);
29
+ this.models = new ModelsApi(this.transport);
30
+ this.prompts = new PromptsApi(options.fetch ?? fetch, apiBaseUrl);
24
31
  this.sessionAccess = new SessionAccessApi(this.transport);
25
32
  this.tasks = new TasksApi(this.transport);
26
33
  this.cronJobs = new CronJobsApi(this.transport);
34
+ this.invite = new PublicInviteApi(this.transport);
27
35
  }
28
36
  space(spaceId) {
29
37
  return new SpaceClient(spaceId, this.transport, null);
package/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ export { CohubHttpClient, createHttpClient } from "./http.js";
2
2
  export { CohubClient, createCohubClient } from "./client.js";
3
3
  export { WebsocketClient, createWebsocketClient } from "./websocket.js";
4
4
  export { HttpError } from "./transport.js";
5
+ export { COHUB_ENVIRONMENTS, normalizeBaseUrl, normalizeWebsocketUrl, resolveApiBaseUrl, resolveCohubEnvironment, resolveWebsocketUrl, } from "./environment.js";
5
6
  export type { CohubClientOptions, Fetch } from "./transport.js";
7
+ export type { CohubEnvironment } from "./environment.js";
6
8
  export * from "./types.js";
7
- export type { SessionEventName, SessionSubscriptionHandlers, SpaceEventName } from "./apis/spaces.js";
9
+ export type { SessionEventName, SessionSubscriptionHandlers, SpaceChannelBindingRecord, SpaceEventName, WebSocketConnectionState } from "./apis/spaces.js";
package/dist/index.js CHANGED
@@ -2,4 +2,5 @@ export { CohubHttpClient, createHttpClient } from "./http.js";
2
2
  export { CohubClient, createCohubClient } from "./client.js";
3
3
  export { WebsocketClient, createWebsocketClient } from "./websocket.js";
4
4
  export { HttpError } from "./transport.js";
5
+ export { COHUB_ENVIRONMENTS, normalizeBaseUrl, normalizeWebsocketUrl, resolveApiBaseUrl, resolveCohubEnvironment, resolveWebsocketUrl, } from "./environment.js";
5
6
  export * from "./types.js";
package/dist/realtime.js CHANGED
@@ -1,4 +1,7 @@
1
1
  export function ensureRealtimeConnected(websocketClient) {
2
+ if (websocketClient.state === "open" || websocketClient.state === "connecting" || websocketClient.state === "reconnecting") {
3
+ return;
4
+ }
2
5
  void websocketClient.connect().catch((error) => {
3
6
  console.error("[CohubClient] Failed to connect realtime websocket:", error);
4
7
  });
@@ -1,6 +1,8 @@
1
+ import type { CohubEnvironment } from "./environment.js";
1
2
  import type { WebsocketClientOptions } from "./websocket.js";
2
3
  export type Fetch = typeof globalThis.fetch;
3
4
  export type CohubClientOptions = {
5
+ env?: CohubEnvironment;
4
6
  baseUrl?: string;
5
7
  getAccessToken?: () => Promise<string | null> | string | null;
6
8
  onUnauthorized?: () => Promise<void> | void;
package/dist/transport.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { resolveApiBaseUrl } from "./environment.js";
1
2
  export class HttpError extends Error {
2
3
  status;
3
4
  body;
@@ -14,7 +15,7 @@ export class HttpTransport {
14
15
  getAccessToken;
15
16
  onUnauthorized;
16
17
  constructor(options = {}) {
17
- this.baseUrl = options.baseUrl ?? "";
18
+ this.baseUrl = resolveApiBaseUrl(options);
18
19
  this.fetcher = options.fetch ?? fetch;
19
20
  this.getAccessToken = options.getAccessToken;
20
21
  this.onUnauthorized = options.onUnauthorized;
package/dist/types.d.ts CHANGED
@@ -40,6 +40,22 @@ export type SpaceFsMoveInput = {
40
40
  fromPath: string;
41
41
  toPath: string;
42
42
  };
43
+ export type SpaceFsUploadEntry = {
44
+ path: string;
45
+ name: string;
46
+ size: number;
47
+ mimeType: string | null;
48
+ mtimeMs: number;
49
+ };
50
+ export type SpaceFsUploadError = {
51
+ name: string;
52
+ code: "file_too_large" | "name_invalid" | "write_failed";
53
+ message: string;
54
+ };
55
+ export type SpaceFsUploadResponse = {
56
+ uploaded: SpaceFsUploadEntry[];
57
+ errors: SpaceFsUploadError[];
58
+ };
43
59
  export type SessionBindingRecord = ProtocolSessionBindingRecord;
44
60
  export type SessionRecord = ProtocolSessionRecord & {
45
61
  bindings?: SessionBindingRecord[];
@@ -49,6 +65,10 @@ export type SessionRecord = ProtocolSessionRecord & {
49
65
  totalOutputTokens?: number;
50
66
  totalCost?: string | number | null;
51
67
  };
68
+ export type SpaceGitInfo = {
69
+ giteaHost: string;
70
+ giteaUsername: string;
71
+ };
52
72
  export type SpaceRecord = {
53
73
  id: string;
54
74
  userUuid: string;
@@ -69,6 +89,7 @@ export type SpaceRecord = {
69
89
  status: string;
70
90
  }[];
71
91
  accessLevel?: "minimal";
92
+ gitInfo?: SpaceGitInfo | null;
72
93
  };
73
94
  export type SpaceBootstrapSource = {
74
95
  type: "blank";
@@ -86,12 +107,14 @@ export type SpaceCreateResponse = {
86
107
  };
87
108
  export type SpaceListItem = SpaceRecord;
88
109
  export type SessionMessagesResponse = {
89
- space: SpaceRecord;
90
110
  session: SessionRecord;
91
111
  messages: MessageRecord[];
92
112
  };
113
+ export type SessionMessageResponse = {
114
+ session: SessionRecord;
115
+ message: MessageRecord;
116
+ };
93
117
  export type SessionMessagesPaginatedResponse = {
94
- space: SpaceRecord;
95
118
  session: SessionRecord;
96
119
  messages: MessageRecord[];
97
120
  hasMore: boolean;
@@ -102,6 +125,16 @@ export type ModelCatalogEntry = {
102
125
  id: string;
103
126
  model: Record<string, unknown>;
104
127
  };
128
+ export type PromptTemplateCatalogEntry = {
129
+ name: string;
130
+ description: string;
131
+ argumentHint?: string;
132
+ category?: string;
133
+ scope: "platform";
134
+ };
135
+ export type PromptTemplateCatalogResponse = {
136
+ prompts: PromptTemplateCatalogEntry[];
137
+ };
105
138
  export type Channel = {
106
139
  id: string;
107
140
  userUuid: string;
@@ -125,8 +158,11 @@ export type SpaceChannelBindingInput = {
125
158
  config?: ChannelConfig | null;
126
159
  };
127
160
  export type SpaceSessionsResponse = {
128
- space: SpaceRecord;
129
161
  sessions: SessionRecord[];
162
+ pageInfo?: {
163
+ hasMore: boolean;
164
+ nextCursor: string | null;
165
+ };
130
166
  };
131
167
  export type UserSshKey = {
132
168
  id: string;
@@ -198,7 +234,7 @@ export type CreateScheduledTaskInput = {
198
234
  spaceId?: string;
199
235
  sessionId?: string;
200
236
  };
201
- export type SpaceRole = "host" | "maker" | "guest";
237
+ export type SpaceRole = "host" | "builder" | "guest";
202
238
  export type SpaceMember = {
203
239
  userId: string;
204
240
  role: SpaceRole;
@@ -209,3 +245,65 @@ export type SpaceAccessPolicy = {
209
245
  signed_in_user: SpaceRole | null;
210
246
  anonymous_user: SpaceRole | null;
211
247
  };
248
+ export type SpaceUsageHourlyStat = {
249
+ bucketStartAt: string;
250
+ totalTokens: number;
251
+ inputTokens: number;
252
+ outputTokens: number;
253
+ cacheReadTokens: number;
254
+ cacheWriteTokens: number;
255
+ costTotal: number;
256
+ requestCount: number;
257
+ successCount: number;
258
+ errorCount: number;
259
+ models: string[];
260
+ };
261
+ export type SpaceUsageSummary = {
262
+ totalTokens: number;
263
+ inputTokens: number;
264
+ outputTokens: number;
265
+ cacheReadTokens: number;
266
+ cacheWriteTokens: number;
267
+ costTotal: number;
268
+ requestCount: number;
269
+ successCount: number;
270
+ errorCount: number;
271
+ };
272
+ export type SpaceUsageResponse = {
273
+ hourly: SpaceUsageHourlyStat[];
274
+ summary: SpaceUsageSummary;
275
+ days: number;
276
+ };
277
+ export type SpaceInvitation = {
278
+ token: string;
279
+ role: SpaceRole;
280
+ status: "active" | "revoked" | "exhausted";
281
+ useCount: number;
282
+ maxUses: number | null;
283
+ createdAt: string | null;
284
+ expiresInSeconds: number | null;
285
+ };
286
+ export type CreateInvitationInput = {
287
+ role?: SpaceRole;
288
+ ttlSeconds?: number;
289
+ maxUses?: number;
290
+ };
291
+ export type CreateInvitationResponse = {
292
+ token: string;
293
+ role: SpaceRole;
294
+ expiresAt: string;
295
+ maxUses: number | null;
296
+ };
297
+ export type InvitationDetail = {
298
+ token: string;
299
+ spaceId: string;
300
+ spaceName: string;
301
+ role: SpaceRole;
302
+ expiresInSeconds: number | null;
303
+ };
304
+ export type AcceptInvitationResponse = {
305
+ ok: true;
306
+ spaceId: string;
307
+ spaceName: string;
308
+ role: SpaceRole;
309
+ };
@@ -1,5 +1,6 @@
1
1
  import { type ChannelEnvelope } from "@neta-art/cohub-protocol/realtime";
2
2
  import type { ContentBlock } from "@neta-art/cohub-protocol/core";
3
+ import type { CohubEnvironment } from "./environment.js";
3
4
  export type WebsocketEventPayload = ChannelEnvelope;
4
5
  export type WebSocketLike = {
5
6
  readonly readyState: number;
@@ -12,6 +13,7 @@ export type WebSocketLike = {
12
13
  };
13
14
  export type WebSocketConstructor = new (url: string) => WebSocketLike;
14
15
  export type WebsocketClientOptions = {
16
+ env?: CohubEnvironment;
15
17
  url?: string;
16
18
  autoReconnect?: boolean;
17
19
  reconnectBaseDelayMs?: number;
@@ -22,8 +24,18 @@ export type WebsocketClientOptions = {
22
24
  getAccessToken?: () => Promise<string | null> | string | null;
23
25
  WebSocketImpl?: WebSocketConstructor;
24
26
  };
25
- export type WebsocketClientState = "idle" | "connecting" | "open" | "closed";
27
+ export type WebsocketClientState = "idle" | "connecting" | "reconnecting" | "open" | "closed";
26
28
  export type WebsocketClientEvents = {
29
+ connecting: {
30
+ isReconnect: boolean;
31
+ attempt: number;
32
+ };
33
+ reconnecting: {
34
+ attempt: number;
35
+ delayMs: number;
36
+ reason?: string;
37
+ code?: number;
38
+ };
27
39
  open: {
28
40
  connectionId?: string | null;
29
41
  };
@@ -34,6 +46,7 @@ export type WebsocketClientEvents = {
34
46
  };
35
47
  error: {
36
48
  error: unknown;
49
+ recoverable: boolean;
37
50
  };
38
51
  event: WebsocketEventPayload;
39
52
  ready: {
@@ -75,6 +88,7 @@ export declare class WebsocketClient {
75
88
  private ws;
76
89
  private pingTimer;
77
90
  private reconnectTimer;
91
+ private reconnectTimerResolver;
78
92
  private reconnectAttempt;
79
93
  private manuallyClosed;
80
94
  private connectPromise;
package/dist/websocket.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { realtimeEnvelopeSchema, } from "@neta-art/cohub-protocol/realtime";
2
+ import { resolveWebsocketUrl } from "./environment.js";
2
3
  const createEventMap = () => ({
4
+ connecting: new Set(),
5
+ reconnecting: new Set(),
3
6
  open: new Set(),
4
7
  close: new Set(),
5
8
  error: new Set(),
@@ -10,24 +13,9 @@ const createEventMap = () => ({
10
13
  serverError: new Set(),
11
14
  pong: new Set(),
12
15
  });
13
- const toWebSocketUrl = (input) => {
14
- const base = (input?.trim() || "").replace(/\/$/, "");
15
- if (base) {
16
- if (base.startsWith("ws://") || base.startsWith("wss://"))
17
- return `${base}/ws`;
18
- if (base.startsWith("http://"))
19
- return `${base.replace(/^http:/, "ws:")}/ws`;
20
- if (base.startsWith("https://"))
21
- return `${base.replace(/^https:/, "wss:")}/ws`;
22
- }
23
- if (typeof window !== "undefined") {
24
- const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
25
- return `${protocol}//${window.location.host}/ws`;
26
- }
27
- return "ws://localhost:8788/ws";
28
- };
16
+ const toWebSocketUrl = (input, env) => resolveWebsocketUrl({ url: input, env });
29
17
  const normalizeOptions = (options = {}) => ({
30
- url: toWebSocketUrl(options.url),
18
+ url: toWebSocketUrl(options.url, options.env),
31
19
  autoReconnect: options.autoReconnect !== false,
32
20
  reconnectBaseDelayMs: options.reconnectBaseDelayMs ?? 1000,
33
21
  reconnectMaxDelayMs: options.reconnectMaxDelayMs ?? 15000,
@@ -35,6 +23,15 @@ const normalizeOptions = (options = {}) => ({
35
23
  pongTimeoutMs: options.pongTimeoutMs ?? 15000,
36
24
  debug: options.debug === true,
37
25
  });
26
+ const formatCloseMessage = (code, reason) => `WebSocket closed: ${code ?? 0} ${reason || ""}`.trim();
27
+ const isRetryableCloseCode = (code) => {
28
+ if (code === 1000)
29
+ return false;
30
+ if (code === 4003)
31
+ return false;
32
+ return true;
33
+ };
34
+ const AUTH_CLOSE_REASON = "authentication failed";
38
35
  class WebsocketAuthError extends Error {
39
36
  constructor(message) {
40
37
  super(message);
@@ -54,6 +51,7 @@ export class WebsocketClient {
54
51
  ws = null;
55
52
  pingTimer = null;
56
53
  reconnectTimer = null;
54
+ reconnectTimerResolver = null;
57
55
  reconnectAttempt = 0;
58
56
  manuallyClosed = false;
59
57
  connectPromise = null;
@@ -97,8 +95,11 @@ export class WebsocketClient {
97
95
  return this.connectPromise;
98
96
  if (this.state === "open" && this.ws?.readyState === WebSocket.OPEN)
99
97
  return;
98
+ const isReconnect = this.reconnectAttempt > 0 || this.state === "reconnecting";
100
99
  this.manuallyClosed = false;
101
- this.state = "connecting";
100
+ this.clearReconnectTimer();
101
+ this.state = isReconnect ? "reconnecting" : "connecting";
102
+ this.emit("connecting", { isReconnect, attempt: this.reconnectAttempt });
102
103
  this.connectPromise = new Promise((resolve, reject) => {
103
104
  const ws = new this.WebSocketImpl(this.url);
104
105
  this.ws = ws;
@@ -119,7 +120,7 @@ export class WebsocketClient {
119
120
  };
120
121
  ws.onopen = async () => {
121
122
  try {
122
- this.log("connected", this.url);
123
+ this.log("connected", { url: this.url, isReconnect, attempt: this.reconnectAttempt });
123
124
  this.startPingLoop();
124
125
  await this.authenticate();
125
126
  this.state = "open";
@@ -129,37 +130,36 @@ export class WebsocketClient {
129
130
  }
130
131
  catch (error) {
131
132
  const authError = error instanceof Error ? error : new Error("authentication failed");
133
+ this.emit("error", { error: authError, recoverable: false });
132
134
  rejectOnce(authError);
133
- ws.close(4003, authError.message);
135
+ ws.close(4003, AUTH_CLOSE_REASON);
134
136
  }
135
137
  };
136
138
  ws.onmessage = (event) => {
137
139
  this.handleMessage(event.data);
138
140
  };
139
141
  ws.onerror = (error) => {
140
- this.emit("error", { error });
142
+ this.emit("error", { error, recoverable: !this.manuallyClosed });
141
143
  };
142
144
  ws.onclose = (event) => {
143
145
  this.stopPingLoop();
144
- const wasConnecting = this.state === "connecting";
146
+ const wasConnecting = this.state === "connecting" || this.state === "reconnecting";
145
147
  this.state = "closed";
146
148
  this.ws = null;
147
- this.rejectAuthWaiter(new Error(`WebSocket closed: ${event.code} ${event.reason || ""}`.trim()));
148
- const willReconnect = !this.manuallyClosed && this.autoReconnect;
149
+ const closeError = new Error(formatCloseMessage(event.code, event.reason));
150
+ this.rejectAuthWaiter(closeError);
151
+ const willReconnect = !this.manuallyClosed && this.autoReconnect && isRetryableCloseCode(event.code);
152
+ this.log("closed", { code: event.code, reason: event.reason, willReconnect, wasConnecting });
149
153
  this.emit("close", {
150
154
  code: event.code,
151
155
  reason: event.reason,
152
156
  willReconnect,
153
157
  });
154
158
  if (wasConnecting) {
155
- rejectOnce(new Error(`WebSocket closed: ${event.code} ${event.reason || ""}`.trim()));
156
- if (event.code === 4001 && willReconnect) {
157
- void this.scheduleReconnect();
158
- }
159
- return;
159
+ rejectOnce(closeError);
160
160
  }
161
161
  if (willReconnect) {
162
- void this.scheduleReconnect();
162
+ void this.scheduleReconnect(event.code, event.reason);
163
163
  }
164
164
  };
165
165
  });
@@ -253,12 +253,12 @@ export class WebsocketClient {
253
253
  parsed = typeof raw === "string" ? JSON.parse(raw) : JSON.parse(String(raw));
254
254
  }
255
255
  catch {
256
- this.emit("error", { error: new Error("invalid websocket payload") });
256
+ this.emit("error", { error: new Error("invalid websocket payload"), recoverable: true });
257
257
  return;
258
258
  }
259
259
  const result = realtimeEnvelopeSchema.safeParse(parsed);
260
260
  if (!result.success) {
261
- this.emit("error", { error: new Error("invalid realtime envelope") });
261
+ this.emit("error", { error: new Error("invalid realtime envelope"), recoverable: true });
262
262
  return;
263
263
  }
264
264
  const envelope = result.data;
@@ -359,7 +359,7 @@ export class WebsocketClient {
359
359
  if (this.awaitingPong &&
360
360
  this.pongDeadlineAt > 0 &&
361
361
  Date.now() > this.pongDeadlineAt) {
362
- this.emit("error", { error: new Error("websocket pong timeout") });
362
+ this.emit("error", { error: new Error("websocket pong timeout"), recoverable: true });
363
363
  this.ws.close(4002, "pong timeout");
364
364
  return;
365
365
  }
@@ -376,22 +376,41 @@ export class WebsocketClient {
376
376
  this.pongDeadlineAt = 0;
377
377
  }
378
378
  clearReconnectTimer() {
379
- if (!this.reconnectTimer)
380
- return;
381
- clearTimeout(this.reconnectTimer);
382
- this.reconnectTimer = null;
379
+ if (this.reconnectTimer) {
380
+ clearTimeout(this.reconnectTimer);
381
+ this.reconnectTimer = null;
382
+ }
383
+ if (this.reconnectTimerResolver) {
384
+ const resolve = this.reconnectTimerResolver;
385
+ this.reconnectTimerResolver = null;
386
+ resolve();
387
+ }
383
388
  }
384
- async scheduleReconnect() {
389
+ async scheduleReconnect(code, reason) {
385
390
  this.clearReconnectTimer();
391
+ const attempt = this.reconnectAttempt + 1;
386
392
  const delay = Math.min(this.reconnectBaseDelayMs * 2 ** this.reconnectAttempt, this.reconnectMaxDelayMs);
387
- this.reconnectAttempt += 1;
393
+ this.reconnectAttempt = attempt;
394
+ this.state = "reconnecting";
395
+ this.log("schedule reconnect", { attempt, delay, code, reason });
396
+ this.emit("reconnecting", {
397
+ attempt,
398
+ delayMs: delay,
399
+ code,
400
+ reason,
401
+ });
388
402
  await new Promise((resolve) => {
389
- this.reconnectTimer = setTimeout(() => resolve(), delay);
403
+ this.reconnectTimerResolver = resolve;
404
+ this.reconnectTimer = setTimeout(() => {
405
+ this.reconnectTimer = null;
406
+ this.reconnectTimerResolver = null;
407
+ resolve();
408
+ }, delay);
390
409
  });
391
410
  if (this.manuallyClosed)
392
411
  return;
393
412
  await this.connect().catch((error) => {
394
- this.emit("error", { error });
413
+ this.emit("error", { error, recoverable: true });
395
414
  });
396
415
  }
397
416
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Cohub SDK for spaces, sessions, checkpoints, and realtime agent collaboration.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -8,10 +8,10 @@
8
8
  "sideEffects": false,
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "https://github.com/neta-art/cohub.git",
11
+ "url": "https://github.com/talesofai/cohub.git",
12
12
  "directory": "packages/sdk"
13
13
  },
14
- "homepage": "https://github.com/neta-art/cohub",
14
+ "homepage": "https://github.com/talesofai/cohub",
15
15
  "keywords": [
16
16
  "cohub",
17
17
  "sdk",
@@ -44,7 +44,7 @@
44
44
  "README.md"
45
45
  ],
46
46
  "dependencies": {
47
- "@neta-art/cohub-protocol": "^1.0.0"
47
+ "@neta-art/cohub-protocol": "1.2.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "typescript": "^6.0.3"