@neta-art/cohub 1.2.1 → 1.3.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.
@@ -0,0 +1,9 @@
1
+ import type { ExploreSpaceItem } from "../types.js";
2
+ import type { HttpTransport } from "../transport.js";
3
+ export declare class ExploreApi {
4
+ private readonly transport;
5
+ constructor(transport: HttpTransport);
6
+ spaces(): Promise<{
7
+ spaces: ExploreSpaceItem[];
8
+ }>;
9
+ }
@@ -0,0 +1,9 @@
1
+ export class ExploreApi {
2
+ transport;
3
+ constructor(transport) {
4
+ this.transport = transport;
5
+ }
6
+ spaces() {
7
+ return this.transport.request("/api/explore/spaces");
8
+ }
9
+ }
@@ -1,24 +1,27 @@
1
+ import type { SpacePublicEndpoints } from "@neta-art/cohub-protocol/ports";
1
2
  import type { WebsocketClient, WebsocketEventPayload } from "../websocket.js";
2
3
  import type { HttpTransport, Fetch } from "../transport.js";
3
4
  import { type SessionPatchApplyResult } from "../session-patch-reducer.js";
4
- 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";
5
+ import type { CheckpointRecord, ContentBlock, SessionMessageResponse, SessionMessagesPaginatedResponse, SessionMessagesResponse, SessionTurnResponse, SessionTurnIndexResponse, SessionTurnWindowResponse, SessionTurnsPaginatedResponse, SessionTurnSignedUrlsResponse, SessionRecord, SpaceAccessPolicy, SpaceBootstrapSource, SpaceChannelBindingInput, SpaceCheckpointDetailResponse, SpaceCreateResponse, SpaceEnvInput, SpaceFsFileResponse, SpaceFsMoveInput, SpaceFsTreeResponse, SpaceFsUploadResponse, SpaceUsageResponse, SpaceFsWriteFileInput, SpaceMarkKind, SpaceMarkListItem, SpaceMarkResourceType, SpaceMember, SpaceRecord, SpaceRole, SpaceSessionsResponse } from "../types.js";
5
6
  import { SpaceInvitationsApi } from "./invitations.js";
6
7
  export type SessionSubscriptionHandlers = {
7
8
  patch?: (event: WebsocketEventPayload) => void;
8
9
  patchState?: (result: SessionPatchApplyResult) => void;
10
+ snapshot?: (event: WebsocketEventPayload) => void;
9
11
  progress?: (event: WebsocketEventPayload) => void;
10
12
  final?: (event: WebsocketEventPayload) => void;
13
+ turnUpdated?: (event: WebsocketEventPayload) => void;
14
+ turnFinalized?: (event: WebsocketEventPayload) => void;
11
15
  error?: (event: WebsocketEventPayload) => void;
12
16
  persisted?: (event: WebsocketEventPayload) => void;
13
17
  event?: (event: WebsocketEventPayload) => void;
14
18
  };
15
- export type SessionEventName = "turn.patch" | "turn.progress" | "turn.final" | "turn.error" | "message.persisted";
16
- export type SpaceEventName = SessionEventName | "event";
19
+ export type SessionEventName = "turn.patch" | "turn.snapshot" | "turn.progress" | "turn.final" | "turn.updated" | "turn.error" | "message.persisted";
20
+ export type SpaceEventName = SessionEventName | "ports.changed" | "event";
17
21
  type SessionSendMessageInput = {
18
22
  content: ContentBlock[];
19
23
  model?: string;
20
24
  provider?: string;
21
- clientMessageId?: string;
22
25
  };
23
26
  export declare class SpacesApi {
24
27
  private readonly transport;
@@ -40,7 +43,16 @@ export declare class SpaceFilesApi {
40
43
  constructor(transport: HttpTransport, spaceId: string);
41
44
  list(path?: string, customFetch?: Fetch): Promise<SpaceFsTreeResponse>;
42
45
  read(path: string, customFetch?: Fetch): Promise<SpaceFsFileResponse>;
46
+ /**
47
+ * Build a direct download URL. For private files, prefer `download()` so the
48
+ * SDK can attach authorization headers.
49
+ */
43
50
  getDownloadUrl(path: string): string;
51
+ download(path: string, customFetch?: Fetch): Promise<{
52
+ blob: Blob;
53
+ filename: string;
54
+ mimeType: string;
55
+ }>;
44
56
  write(input: SpaceFsWriteFileInput): Promise<{
45
57
  ok: true;
46
58
  path: string;
@@ -83,8 +95,31 @@ declare class SessionMessagesClient {
83
95
  send(input: SessionSendMessageInput): Promise<{
84
96
  ok: true;
85
97
  userMessageId: string;
98
+ turnId: string;
86
99
  }>;
87
100
  }
101
+ declare class SessionTurnsClient {
102
+ private readonly transport;
103
+ private readonly sessionId;
104
+ constructor(transport: HttpTransport, sessionId: string);
105
+ listPaginated(options?: {
106
+ cursor?: number;
107
+ limit?: number;
108
+ direction?: "older" | "newer";
109
+ }, customFetch?: Fetch): Promise<SessionTurnsPaginatedResponse>;
110
+ index(options?: {
111
+ cursor?: number;
112
+ limit?: number;
113
+ }, customFetch?: Fetch): Promise<SessionTurnIndexResponse>;
114
+ window(options: {
115
+ sequence?: number;
116
+ turnId?: string;
117
+ before?: number;
118
+ after?: number;
119
+ }, customFetch?: Fetch): Promise<SessionTurnWindowResponse>;
120
+ get(turnId: string, customFetch?: Fetch): Promise<SessionTurnResponse>;
121
+ signedUrls(turnId: string, objectKeys: string[]): Promise<SessionTurnSignedUrlsResponse>;
122
+ }
88
123
  declare class SessionRealtimeClient {
89
124
  private readonly websocketClient;
90
125
  private readonly spaceId;
@@ -99,6 +134,7 @@ export declare class SessionClient {
99
134
  readonly id: string;
100
135
  private readonly transport;
101
136
  readonly messages: SessionMessagesClient;
137
+ readonly turns: SessionTurnsClient;
102
138
  readonly realtime: SessionRealtimeClient;
103
139
  constructor(spaceId: string, id: string, transport: HttpTransport, websocketClient: WebsocketClient | null);
104
140
  get(customFetch?: Fetch): Promise<{
@@ -215,6 +251,52 @@ export declare class SpaceEnvApi {
215
251
  env: SpaceEnvInput[];
216
252
  }>;
217
253
  }
254
+ export type SpaceSandboxRecord = {
255
+ status: string | null;
256
+ podName?: string | null;
257
+ desiredImage?: string | null;
258
+ reportedImageVersion?: string | null;
259
+ lastHeartbeatAt?: string | null;
260
+ reportedAt?: string | null;
261
+ meta?: Record<string, unknown> | null;
262
+ };
263
+ export declare class SpaceSandboxApi {
264
+ private readonly transport;
265
+ private readonly spaceId;
266
+ constructor(transport: HttpTransport, spaceId: string);
267
+ get(): Promise<{
268
+ sandbox: SpaceSandboxRecord | null;
269
+ }>;
270
+ ports(): Promise<{
271
+ endpoints: SpacePublicEndpoints;
272
+ }>;
273
+ recreate(): Promise<{
274
+ ok: boolean;
275
+ status?: string;
276
+ verified?: boolean;
277
+ checks?: Record<string, boolean> | null;
278
+ message?: string;
279
+ }>;
280
+ }
281
+ export declare class SpaceMarksApi {
282
+ private readonly transport;
283
+ private readonly spaceId;
284
+ constructor(transport: HttpTransport, spaceId: string);
285
+ list(kind?: SpaceMarkKind): Promise<{
286
+ marks: SpaceMarkListItem[];
287
+ }>;
288
+ create(input: {
289
+ kind?: SpaceMarkKind;
290
+ resourceType: SpaceMarkResourceType;
291
+ resourceRef: string;
292
+ label?: string | null;
293
+ }): Promise<{
294
+ mark: SpaceMarkListItem;
295
+ }>;
296
+ delete(markId: string): Promise<{
297
+ ok: true;
298
+ }>;
299
+ }
218
300
  export declare class SpaceCheckpointsApi {
219
301
  private readonly transport;
220
302
  private readonly spaceId;
@@ -240,12 +322,20 @@ export declare class SpaceClient {
240
322
  readonly usage: SpaceUsageApi;
241
323
  readonly channels: SpaceChannelsApi;
242
324
  readonly env: SpaceEnvApi;
325
+ readonly sandbox: SpaceSandboxApi;
243
326
  readonly invitations: SpaceInvitationsApi;
327
+ readonly marks: SpaceMarksApi;
244
328
  constructor(id: string, transport: HttpTransport, websocketClient: WebsocketClient | null);
245
329
  get(customFetch?: Fetch): Promise<SpaceRecord>;
246
330
  rename(name: string): Promise<{
247
331
  space: SpaceRecord;
248
332
  }>;
333
+ profile(body: {
334
+ description?: string | null;
335
+ pictureUrl?: string | null;
336
+ }): Promise<{
337
+ space: SpaceRecord;
338
+ }>;
249
339
  session(sessionId: string): SessionClient;
250
340
  subscribe(handler: (event: WebsocketEventPayload) => void): () => void;
251
341
  on(type: SpaceEventName, handler: (event: WebsocketEventPayload) => void): () => void;
@@ -2,14 +2,35 @@ import { ensureRealtimeConnected } from "../realtime.js";
2
2
  import { SessionPatchReducer, } from "../session-patch-reducer.js";
3
3
  import { SpaceInvitationsApi } from "./invitations.js";
4
4
  const DEFAULT_DEDUP_WINDOW_MS = 2000;
5
+ const getFilenameFromContentDisposition = (value) => {
6
+ if (!value)
7
+ return null;
8
+ const encodedMatch = value.match(/filename\*=UTF-8''([^;]+)/i);
9
+ if (encodedMatch?.[1]) {
10
+ try {
11
+ return decodeURIComponent(encodedMatch[1]);
12
+ }
13
+ catch {
14
+ return encodedMatch[1];
15
+ }
16
+ }
17
+ const plainMatch = value.match(/filename="?([^";]+)"?/i);
18
+ return plainMatch?.[1] ?? null;
19
+ };
5
20
  const toSessionEventName = (type) => {
6
21
  switch (type) {
7
22
  case "session.turn.patch":
8
23
  return "turn.patch";
24
+ case "session.turn.snapshot":
25
+ return "turn.snapshot";
9
26
  case "session.turn.progress":
10
27
  return "turn.progress";
11
28
  case "session.turn.error":
12
29
  return "turn.error";
30
+ case "session.turn.updated":
31
+ return "turn.updated";
32
+ case "session.turn.finalized":
33
+ return "turn.final";
13
34
  case "session.message.persisted":
14
35
  return "message.persisted";
15
36
  default:
@@ -23,7 +44,16 @@ const isAssistantFinalPersistedEvent = (event) => {
23
44
  if (!message || typeof message !== "object")
24
45
  return false;
25
46
  const record = message;
26
- return record.role === "assistant" && record.meta?.messageKind === "assistant_final";
47
+ return record.role === "assistant" && (record.meta?.messageKind === "assistant_final" || record.meta?.messageKind === "assistant_error");
48
+ };
49
+ const isAssistantIntermediatePersistedEvent = (event) => {
50
+ if (event.type !== "session.message.persisted")
51
+ return false;
52
+ const message = event.payload.message;
53
+ if (!message || typeof message !== "object")
54
+ return false;
55
+ const record = message;
56
+ return record.role === "assistant" && record.meta?.messageKind === "assistant_intermediate";
27
57
  };
28
58
  const getPersistedMessageTurnId = (event) => {
29
59
  if (event.type !== "session.message.persisted")
@@ -79,10 +109,26 @@ export class SpaceFilesApi {
79
109
  const params = new URLSearchParams({ path });
80
110
  return this.transport.request(`/api/spaces/${this.spaceId}/fs/file?${params.toString()}`, { fetch: customFetch });
81
111
  }
112
+ /**
113
+ * Build a direct download URL. For private files, prefer `download()` so the
114
+ * SDK can attach authorization headers.
115
+ */
82
116
  getDownloadUrl(path) {
83
117
  const params = new URLSearchParams({ path });
84
118
  return `/api/spaces/${this.spaceId}/fs/download?${params.toString()}`;
85
119
  }
120
+ async download(path, customFetch) {
121
+ const params = new URLSearchParams({ path });
122
+ const raw = await this.transport.raw(`/api/spaces/${this.spaceId}/fs/download?${params.toString()}`, { fetch: customFetch });
123
+ const blob = await raw.blob();
124
+ const filename = getFilenameFromContentDisposition(raw.response.headers.get("content-disposition")) ??
125
+ path.split("/").pop() ??
126
+ "download";
127
+ const mimeType = raw.response.headers.get("content-type") ??
128
+ blob.type ??
129
+ "application/octet-stream";
130
+ return { blob, filename, mimeType };
131
+ }
86
132
  write(input) {
87
133
  return this.transport.request(`/api/spaces/${this.spaceId}/fs/file`, {
88
134
  method: "PUT",
@@ -183,11 +229,61 @@ class SessionMessagesClient {
183
229
  content: input.content,
184
230
  model: input.model,
185
231
  provider: input.provider,
186
- clientMessageId: input.clientMessageId,
187
232
  }),
188
233
  });
189
234
  }
190
235
  }
236
+ class SessionTurnsClient {
237
+ transport;
238
+ sessionId;
239
+ constructor(transport, sessionId) {
240
+ this.transport = transport;
241
+ this.sessionId = sessionId;
242
+ }
243
+ listPaginated(options, customFetch) {
244
+ const params = new URLSearchParams();
245
+ if (options?.cursor !== undefined)
246
+ params.set("cursor", String(options.cursor));
247
+ if (options?.limit !== undefined)
248
+ params.set("limit", String(options.limit));
249
+ if (options?.direction)
250
+ params.set("direction", options.direction);
251
+ const query = params.toString();
252
+ return this.transport.request(`/api/sessions/${this.sessionId}/turns${query ? `?${query}` : ""}`, { fetch: customFetch });
253
+ }
254
+ index(options, customFetch) {
255
+ const params = new URLSearchParams();
256
+ if (options?.cursor !== undefined)
257
+ params.set("cursor", String(options.cursor));
258
+ if (options?.limit !== undefined)
259
+ params.set("limit", String(options.limit));
260
+ const query = params.toString();
261
+ return this.transport.request(`/api/sessions/${this.sessionId}/turns/index${query ? `?${query}` : ""}`, { fetch: customFetch });
262
+ }
263
+ window(options, customFetch) {
264
+ const params = new URLSearchParams();
265
+ if (options.sequence !== undefined)
266
+ params.set("sequence", String(options.sequence));
267
+ if (options.turnId)
268
+ params.set("turnId", options.turnId);
269
+ if (options.before !== undefined)
270
+ params.set("before", String(options.before));
271
+ if (options.after !== undefined)
272
+ params.set("after", String(options.after));
273
+ const query = params.toString();
274
+ return this.transport.request(`/api/sessions/${this.sessionId}/turns/window${query ? `?${query}` : ""}`, { fetch: customFetch });
275
+ }
276
+ get(turnId, customFetch) {
277
+ return this.transport.request(`/api/sessions/${this.sessionId}/turns/${turnId}`, { fetch: customFetch });
278
+ }
279
+ signedUrls(turnId, objectKeys) {
280
+ return this.transport.request(`/api/sessions/${this.sessionId}/turns/${turnId}/signed-urls`, {
281
+ method: "POST",
282
+ headers: { "Content-Type": "application/json" },
283
+ body: JSON.stringify({ objectKeys }),
284
+ });
285
+ }
286
+ }
191
287
  class SessionRealtimeClient {
192
288
  websocketClient;
193
289
  spaceId;
@@ -229,6 +325,8 @@ class SessionRealtimeClient {
229
325
  }
230
326
  }
231
327
  }
328
+ if (eventName === "turn.snapshot")
329
+ handlers.snapshot?.(event);
232
330
  if (eventName === "turn.progress")
233
331
  handlers.progress?.(event);
234
332
  if (eventName === "turn.error") {
@@ -238,8 +336,26 @@ class SessionRealtimeClient {
238
336
  });
239
337
  handlers.error?.(event);
240
338
  }
241
- if (eventName === "message.persisted")
339
+ if (eventName === "message.persisted") {
242
340
  handlers.persisted?.(event);
341
+ if (isAssistantIntermediatePersistedEvent(event)) {
342
+ this.patchReducer.reset({
343
+ spaceId: this.spaceId,
344
+ sessionId: this.sessionId,
345
+ });
346
+ }
347
+ }
348
+ if (eventName === "turn.updated")
349
+ handlers.turnUpdated?.(event);
350
+ if (eventName === "turn.final") {
351
+ this.patchReducer.complete({
352
+ spaceId: this.spaceId,
353
+ sessionId: this.sessionId,
354
+ turnId: typeof event.payload.turn === "object" && event.payload.turn && "id" in event.payload.turn ? String(event.payload.turn.id) : null,
355
+ });
356
+ handlers.turnFinalized?.(event);
357
+ handlers.final?.(event);
358
+ }
243
359
  if (isAssistantFinalPersistedEvent(event)) {
244
360
  this.patchReducer.complete({
245
361
  spaceId: this.spaceId,
@@ -269,12 +385,14 @@ export class SessionClient {
269
385
  id;
270
386
  transport;
271
387
  messages;
388
+ turns;
272
389
  realtime;
273
390
  constructor(spaceId, id, transport, websocketClient) {
274
391
  this.spaceId = spaceId;
275
392
  this.id = id;
276
393
  this.transport = transport;
277
394
  this.messages = new SessionMessagesClient(transport, id);
395
+ this.turns = new SessionTurnsClient(transport, id);
278
396
  this.realtime = new SessionRealtimeClient(websocketClient, spaceId, id);
279
397
  }
280
398
  get(customFetch) {
@@ -355,6 +473,10 @@ export class SpaceEventsApi {
355
473
  handler(event);
356
474
  return;
357
475
  }
476
+ if (type === "ports.changed" && event.type === "space.ports.changed") {
477
+ handler(event);
478
+ return;
479
+ }
358
480
  if (type === "turn.final" && isAssistantFinalPersistedEvent(event)) {
359
481
  handler(event);
360
482
  return;
@@ -468,6 +590,47 @@ export class SpaceEnvApi {
468
590
  return this.transport.request(`/api/spaces/${this.spaceId}/env/${encodeURIComponent(name)}`, { method: "DELETE" });
469
591
  }
470
592
  }
593
+ export class SpaceSandboxApi {
594
+ transport;
595
+ spaceId;
596
+ constructor(transport, spaceId) {
597
+ this.transport = transport;
598
+ this.spaceId = spaceId;
599
+ }
600
+ get() {
601
+ return this.transport.request(`/api/spaces/${this.spaceId}/sandbox`);
602
+ }
603
+ ports() {
604
+ return this.transport.request(`/api/spaces/${this.spaceId}/sandbox/ports`);
605
+ }
606
+ recreate() {
607
+ return this.transport.request(`/api/spaces/${this.spaceId}/sandbox/recreate`, {
608
+ method: "POST",
609
+ });
610
+ }
611
+ }
612
+ export class SpaceMarksApi {
613
+ transport;
614
+ spaceId;
615
+ constructor(transport, spaceId) {
616
+ this.transport = transport;
617
+ this.spaceId = spaceId;
618
+ }
619
+ list(kind = "pin") {
620
+ const params = new URLSearchParams({ kind });
621
+ return this.transport.request(`/api/spaces/${this.spaceId}/marks?${params.toString()}`);
622
+ }
623
+ create(input) {
624
+ return this.transport.request(`/api/spaces/${this.spaceId}/marks`, {
625
+ method: "POST",
626
+ headers: { "Content-Type": "application/json" },
627
+ body: JSON.stringify({ kind: "pin", ...input }),
628
+ });
629
+ }
630
+ delete(markId) {
631
+ return this.transport.request(`/api/spaces/${this.spaceId}/marks/${markId}`, { method: "DELETE" });
632
+ }
633
+ }
471
634
  export class SpaceCheckpointsApi {
472
635
  transport;
473
636
  spaceId;
@@ -501,7 +664,9 @@ export class SpaceClient {
501
664
  usage;
502
665
  channels;
503
666
  env;
667
+ sandbox;
504
668
  invitations;
669
+ marks;
505
670
  constructor(id, transport, websocketClient) {
506
671
  this.id = id;
507
672
  this.transport = transport;
@@ -514,7 +679,9 @@ export class SpaceClient {
514
679
  this.usage = new SpaceUsageApi(transport, id);
515
680
  this.channels = new SpaceChannelsApi(transport, id);
516
681
  this.env = new SpaceEnvApi(transport, id);
682
+ this.sandbox = new SpaceSandboxApi(transport, id);
517
683
  this.invitations = new SpaceInvitationsApi(transport, id);
684
+ this.marks = new SpaceMarksApi(transport, id);
518
685
  }
519
686
  get(customFetch) {
520
687
  return this.transport.request(`/api/spaces/${this.id}`, {
@@ -530,6 +697,13 @@ export class SpaceClient {
530
697
  body: JSON.stringify({ name }),
531
698
  });
532
699
  }
700
+ profile(body) {
701
+ return this.transport.request(`/api/spaces/${this.id}/profile`, {
702
+ method: "PATCH",
703
+ headers: { "Content-Type": "application/json" },
704
+ body: JSON.stringify(body),
705
+ });
706
+ }
533
707
  session(sessionId) {
534
708
  return new SessionClient(this.id, sessionId, this.transport, this.websocketClient);
535
709
  }
@@ -1,5 +1,5 @@
1
1
  import { type HttpTransport, type Fetch } from "../transport.js";
2
- import type { UserSshKey } from "../types.js";
2
+ import type { UserRulesResponse, UserSshKey } from "../types.js";
3
3
  export declare class UserApi {
4
4
  private readonly transport;
5
5
  private readonly transportBaseUrl;
@@ -7,6 +7,7 @@ export declare class UserApi {
7
7
  private readonly clearStoredAuthToken?;
8
8
  constructor(transport: HttpTransport, transportBaseUrl: string, setStoredAuthToken?: ((token: string) => void) | undefined, clearStoredAuthToken?: (() => void) | undefined);
9
9
  getMe(customFetch?: Fetch): Promise<unknown>;
10
+ getRules(customFetch?: Fetch): Promise<UserRulesResponse>;
10
11
  setAuthToken(token: string): Promise<any>;
11
12
  clearAuthToken(): Promise<null>;
12
13
  getSshKeys(customFetch?: Fetch): Promise<UserSshKey[]>;
package/dist/apis/user.js CHANGED
@@ -13,6 +13,12 @@ export class UserApi {
13
13
  getMe(customFetch) {
14
14
  return this.transport.request("/api/me", { fetch: customFetch });
15
15
  }
16
+ getRules(customFetch) {
17
+ return this.transport.request("/api/me/rules", {
18
+ method: "GET",
19
+ fetch: customFetch,
20
+ });
21
+ }
16
22
  async setAuthToken(token) {
17
23
  const trimmedToken = token.trim();
18
24
  const response = await fetch(this.transportBaseUrl ? `${this.transportBaseUrl}/api/me` : "/api/me", {
package/dist/client.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ChannelsApi } from "./apis/channels.js";
2
2
  import { CronJobsApi } from "./apis/cron-jobs.js";
3
+ import { ExploreApi } from "./apis/explore.js";
3
4
  import { ModelsApi } from "./apis/models.js";
4
5
  import { PromptsApi } from "./apis/prompts.js";
5
6
  import { SessionAccessApi } from "./apis/session-access.js";
@@ -17,6 +18,7 @@ export declare class CohubClient {
17
18
  readonly sessionAccess: SessionAccessApi;
18
19
  readonly tasks: TasksApi;
19
20
  readonly cronJobs: CronJobsApi;
21
+ readonly explore: ExploreApi;
20
22
  readonly invite: PublicInviteApi;
21
23
  private readonly transport;
22
24
  private readonly websocketClient;
package/dist/client.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ChannelsApi } from "./apis/channels.js";
2
2
  import { CronJobsApi } from "./apis/cron-jobs.js";
3
+ import { ExploreApi } from "./apis/explore.js";
3
4
  import { ModelsApi } from "./apis/models.js";
4
5
  import { PromptsApi } from "./apis/prompts.js";
5
6
  import { SessionAccessApi } from "./apis/session-access.js";
@@ -19,6 +20,7 @@ export class CohubClient {
19
20
  sessionAccess;
20
21
  tasks;
21
22
  cronJobs;
23
+ explore;
22
24
  invite;
23
25
  transport;
24
26
  websocketClient;
@@ -41,6 +43,7 @@ export class CohubClient {
41
43
  this.sessionAccess = new SessionAccessApi(this.transport);
42
44
  this.tasks = new TasksApi(this.transport);
43
45
  this.cronJobs = new CronJobsApi(this.transport);
46
+ this.explore = new ExploreApi(this.transport);
44
47
  this.invite = new PublicInviteApi(this.transport);
45
48
  }
46
49
  space(spaceId) {
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export { CohubClient, createCohubClient } from "./client.js";
3
3
  export { WebsocketClient, createWebsocketClient } from "./websocket.js";
4
4
  export { SessionPatchReducer, createSessionPatchReducer } from "./session-patch-reducer.js";
5
5
  export { HttpError } from "./transport.js";
6
+ export type { RawHttpResponse } from "./transport.js";
6
7
  export { COHUB_ENVIRONMENTS, normalizeBaseUrl, normalizeWebsocketUrl, resolveApiBaseUrl, resolveCohubEnvironment, resolveWebsocketUrl, } from "./environment.js";
7
8
  export type { CohubClientOptions, Fetch } from "./transport.js";
8
9
  export type { CohubEnvironment } from "./environment.js";
@@ -38,6 +38,9 @@ export declare class SessionPatchReducer {
38
38
  private key;
39
39
  get(input: SessionPatchKeyInput): SessionPatchState;
40
40
  start(input: SessionPatchKeyInput): SessionPatchState;
41
+ replaceTurnId(input: SessionPatchKeyInput & {
42
+ nextTurnId: string | null;
43
+ }): SessionPatchState;
41
44
  complete(input: SessionPatchKeyInput): SessionPatchState;
42
45
  fail(input: SessionPatchKeyInput): SessionPatchState;
43
46
  reset(input: SessionPatchKeyInput): void;
@@ -177,12 +177,21 @@ export class SessionPatchReducer {
177
177
  contentBlocks: [],
178
178
  anchorUserMessageId: null,
179
179
  patchSeq: 0,
180
- turnId: null,
180
+ turnId: input.turnId ?? null,
181
181
  appendPath: null,
182
182
  };
183
183
  this.states.set(this.key(input), state);
184
184
  return state;
185
185
  }
186
+ replaceTurnId(input) {
187
+ const current = this.get(input);
188
+ const state = {
189
+ ...current,
190
+ turnId: input.nextTurnId,
191
+ };
192
+ this.states.set(this.key(input), state);
193
+ return state;
194
+ }
186
195
  complete(input) {
187
196
  const current = this.get(input);
188
197
  const state = {
@@ -231,6 +240,11 @@ export class SessionPatchReducer {
231
240
  const isDifferentKnownTurn = Boolean(currentTurnId && inputTurnId && currentTurnId !== inputTurnId);
232
241
  const isFreshKnownTurn = isDifferentKnownTurn && input.baseSeq === 0;
233
242
  const currentSeq = isFreshKnownTurn ? 0 : current.patchSeq;
243
+ const isSameTurnKeyframe = Boolean(currentTurnId &&
244
+ inputTurnId &&
245
+ currentTurnId === inputTurnId &&
246
+ input.baseSeq === 0 &&
247
+ input.seq >= currentSeq);
234
248
  const isTerminalSameTurn = (current.status === "completed" || current.status === "failed") &&
235
249
  Boolean(currentTurnId) &&
236
250
  currentTurnId === inputTurnId;
@@ -240,13 +254,13 @@ export class SessionPatchReducer {
240
254
  if (isDifferentKnownTurn && !isFreshKnownTurn) {
241
255
  return { applied: false, reason: "version_mismatch", state: current };
242
256
  }
243
- if (input.seq <= currentSeq) {
257
+ if (!isSameTurnKeyframe && input.seq <= currentSeq) {
244
258
  return { applied: false, reason: "duplicate", state: current };
245
259
  }
246
- if (input.baseSeq !== currentSeq) {
260
+ if (!isSameTurnKeyframe && input.baseSeq !== currentSeq) {
247
261
  return { applied: false, reason: "version_mismatch", state: current };
248
262
  }
249
- const startingFresh = input.baseSeq === 0 || isFreshKnownTurn;
263
+ const startingFresh = input.baseSeq === 0 || isFreshKnownTurn || isSameTurnKeyframe;
250
264
  const baseBlocks = startingFresh ? [] : current.contentBlocks;
251
265
  const patched = applyPatchOpsToBlocks(baseBlocks, input.ops, startingFresh ? null : current.appendPath);
252
266
  if (patched.failed) {
@@ -1,6 +1,15 @@
1
1
  import type { CohubEnvironment } from "./environment.js";
2
2
  import type { WebsocketClientOptions } from "./websocket.js";
3
3
  export type Fetch = typeof globalThis.fetch;
4
+ type RequestInitWithFetch = RequestInit & {
5
+ fetch?: Fetch;
6
+ };
7
+ export type RawHttpResponse = {
8
+ response: Response;
9
+ blob(): Promise<Blob>;
10
+ arrayBuffer(): Promise<ArrayBuffer>;
11
+ text(): Promise<string>;
12
+ };
4
13
  export type CohubClientOptions = {
5
14
  env?: CohubEnvironment;
6
15
  baseUrl?: string;
@@ -23,7 +32,9 @@ export declare class HttpTransport {
23
32
  private readonly onUnauthorized?;
24
33
  constructor(options?: CohubClientOptions);
25
34
  private withAuthorization;
26
- request<T>(path: string, init?: RequestInit & {
27
- fetch?: Fetch;
28
- }): Promise<T>;
35
+ private send;
36
+ request<T>(path: string, init?: RequestInitWithFetch): Promise<T>;
37
+ raw(path: string, init?: RequestInitWithFetch): Promise<RawHttpResponse>;
38
+ blob(path: string, init?: RequestInitWithFetch): Promise<Blob>;
29
39
  }
40
+ export {};
package/dist/transport.js CHANGED
@@ -1,4 +1,11 @@
1
1
  import { resolveApiBaseUrl } from "./environment.js";
2
+ const responseBodyForError = async (response) => {
3
+ const contentType = response.headers.get("content-type") ?? "";
4
+ return contentType.includes("application/json")
5
+ ? await response.json().catch(() => null)
6
+ : await response.text().catch(() => response.statusText);
7
+ };
8
+ const messageFromErrorBody = (body, fallback) => typeof body === "string" ? body : JSON.stringify(body ?? null) || fallback;
2
9
  export class HttpError extends Error {
3
10
  status;
4
11
  body;
@@ -34,7 +41,7 @@ export class HttpTransport {
34
41
  headers,
35
42
  };
36
43
  }
37
- async request(path, init) {
44
+ async send(path, init) {
38
45
  const fetcher = init?.fetch ?? this.fetcher;
39
46
  const url = this.baseUrl ? `${this.baseUrl}${path}` : path;
40
47
  const response = await fetcher(url, await this.withAuthorization(init));
@@ -43,16 +50,29 @@ export class HttpTransport {
43
50
  throw new HttpError("unauthorized", 401, null);
44
51
  }
45
52
  if (!response.ok) {
46
- const contentType = response.headers.get("content-type") ?? "";
47
- const body = contentType.includes("application/json")
48
- ? await response.json().catch(() => null)
49
- : await response.text().catch(() => response.statusText);
50
- const message = typeof body === "string" ? body : JSON.stringify(body ?? null);
51
- throw new HttpError(message || response.statusText, response.status, body);
53
+ const body = await responseBodyForError(response);
54
+ throw new HttpError(messageFromErrorBody(body, response.statusText), response.status, body);
52
55
  }
56
+ return response;
57
+ }
58
+ async request(path, init) {
59
+ const response = await this.send(path, init);
53
60
  if (response.status === 204) {
54
61
  return null;
55
62
  }
56
63
  return response.json();
57
64
  }
65
+ async raw(path, init) {
66
+ const response = await this.send(path, init);
67
+ return {
68
+ response,
69
+ blob: () => response.blob(),
70
+ arrayBuffer: () => response.arrayBuffer(),
71
+ text: () => response.text(),
72
+ };
73
+ }
74
+ async blob(path, init) {
75
+ const raw = await this.raw(path, init);
76
+ return raw.blob();
77
+ }
58
78
  }
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { SessionBindingRecord as ProtocolSessionBindingRecord, SessionRecord as ProtocolSessionRecord } from "@neta-art/cohub-protocol/model";
1
+ import type { SessionBindingRecord as ProtocolSessionBindingRecord, SessionRecord as ProtocolSessionRecord, SessionTurnIndexItem, SessionTurnRecord } from "@neta-art/cohub-protocol/model";
2
2
  import type { ChannelConfig } from "@neta-art/cohub-protocol/gateway";
3
3
  import type { ContentBlock } from "@neta-art/cohub-protocol/core";
4
4
  import type { MessageRecord } from "@neta-art/cohub-protocol/model";
@@ -6,7 +6,13 @@ export type { ChannelConfig, DiscordChannelConfig, } from "@neta-art/cohub-proto
6
6
  export type ApiError = {
7
7
  message: string;
8
8
  };
9
- export type { ContentBlock, MessageRecord };
9
+ export type UserRulesResponse = {
10
+ content: string;
11
+ updatedAt: string | null;
12
+ source: "config-space";
13
+ path: string;
14
+ };
15
+ export type { ContentBlock, MessageRecord, SessionTurnRecord, SessionTurnIndexItem };
10
16
  export type SpaceFsEntry = {
11
17
  name: string;
12
18
  path: string;
@@ -120,6 +126,34 @@ export type SessionMessagesPaginatedResponse = {
120
126
  hasMore: boolean;
121
127
  nextCursor: number | undefined;
122
128
  };
129
+ export type SessionTurnsPaginatedResponse = {
130
+ session: SessionRecord;
131
+ turns: SessionTurnRecord[];
132
+ hasMore: boolean;
133
+ nextCursor: number | undefined;
134
+ };
135
+ export type SessionTurnIndexResponse = {
136
+ session: SessionRecord;
137
+ turns: SessionTurnIndexItem[];
138
+ hasMore: boolean;
139
+ nextCursor: number | undefined;
140
+ };
141
+ export type SessionTurnWindowResponse = {
142
+ session: SessionRecord;
143
+ turns: SessionTurnRecord[];
144
+ hasMoreOlder: boolean;
145
+ hasMoreNewer: boolean;
146
+ oldestCursor: number | undefined;
147
+ newestCursor: number | undefined;
148
+ anchorSequence: number | undefined;
149
+ };
150
+ export type SessionTurnResponse = {
151
+ session: SessionRecord;
152
+ turn: SessionTurnRecord;
153
+ };
154
+ export type SessionTurnSignedUrlsResponse = {
155
+ urls: Record<string, string>;
156
+ };
123
157
  export type ModelCatalogEntry = {
124
158
  provider: string;
125
159
  id: string;
@@ -241,6 +275,44 @@ export type SpaceMember = {
241
275
  createdAt: string;
242
276
  updatedAt: string;
243
277
  };
278
+ export type SpaceMarkKind = "pin";
279
+ export type SpaceMarkResourceType = "session" | "checkpoint" | "file";
280
+ export type SpaceMarkRecord = {
281
+ id: string;
282
+ spaceId: string;
283
+ kind: SpaceMarkKind;
284
+ resourceType: SpaceMarkResourceType;
285
+ resourceRef: string;
286
+ label: string | null;
287
+ rank: number;
288
+ createdBy: string;
289
+ createdAt: string;
290
+ updatedAt: string;
291
+ };
292
+ export type SpaceMarkListItem = SpaceMarkRecord & {
293
+ href: string;
294
+ resource: {
295
+ title: string;
296
+ subtitle: string | null;
297
+ status: string | null;
298
+ } | null;
299
+ };
300
+ export type ExploreSpaceItem = {
301
+ space: SpaceRecord;
302
+ accessAudience: "anonymous" | "signed_in";
303
+ explore: {
304
+ rank: number;
305
+ category: string | null;
306
+ label: string | null;
307
+ };
308
+ latestCheckpoints: CheckpointRecord[];
309
+ stats: {
310
+ pinnedCount: number;
311
+ checkpointCount: number;
312
+ forkCount: number;
313
+ };
314
+ sandboxStatus: string | null;
315
+ };
244
316
  export type SpaceAccessPolicy = {
245
317
  signed_in_user: SpaceRole | null;
246
318
  anonymous_user: SpaceRole | null;
@@ -97,6 +97,7 @@ export declare class WebsocketClient {
97
97
  private lastPingRequestId;
98
98
  private pongDeadlineAt;
99
99
  private readonly compactStreamContexts;
100
+ private readonly patchStreamBuffers;
100
101
  state: WebsocketClientState;
101
102
  connectionId: string | null;
102
103
  private readonly listeners;
@@ -126,6 +127,10 @@ export declare class WebsocketClient {
126
127
  private rejectAuthWaiter;
127
128
  private handleMessage;
128
129
  private rememberCompactStreamContext;
130
+ private getPatchStreamBufferKey;
131
+ private handlePatchEnvelope;
132
+ private enforcePatchStreamBufferLimit;
133
+ private flushPatchStreamBuffer;
129
134
  private handleCompactFrame;
130
135
  private startPingLoop;
131
136
  private stopPingLoop;
package/dist/websocket.js CHANGED
@@ -32,6 +32,7 @@ const isRetryableCloseCode = (code) => {
32
32
  return true;
33
33
  };
34
34
  const AUTH_CLOSE_REASON = "authentication failed";
35
+ const PATCH_STREAM_BUFFER_MAX_PENDING = 128;
35
36
  const isRecord = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
36
37
  const compactFrameToPatchOperation = (frame) => {
37
38
  if (frame.t === "d")
@@ -82,6 +83,7 @@ export class WebsocketClient {
82
83
  lastPingRequestId = null;
83
84
  pongDeadlineAt = 0;
84
85
  compactStreamContexts = new Map();
86
+ patchStreamBuffers = new Map();
85
87
  state = "idle";
86
88
  connectionId = null;
87
89
  listeners = createEventMap();
@@ -169,6 +171,8 @@ export class WebsocketClient {
169
171
  const wasConnecting = this.state === "connecting" || this.state === "reconnecting";
170
172
  this.state = "closed";
171
173
  this.ws = null;
174
+ this.compactStreamContexts.clear();
175
+ this.patchStreamBuffers.clear();
172
176
  const closeError = new Error(formatCloseMessage(event.code, event.reason));
173
177
  this.rejectAuthWaiter(closeError);
174
178
  const willReconnect = !this.manuallyClosed && this.autoReconnect && isRetryableCloseCode(event.code);
@@ -197,6 +201,8 @@ export class WebsocketClient {
197
201
  this.ws?.close(code, reason);
198
202
  this.ws = null;
199
203
  this.connectPromise = null;
204
+ this.compactStreamContexts.clear();
205
+ this.patchStreamBuffers.clear();
200
206
  }
201
207
  async sendMessage(input) {
202
208
  await this.ensureOpen();
@@ -294,6 +300,10 @@ export class WebsocketClient {
294
300
  }
295
301
  const envelope = result.data;
296
302
  this.rememberCompactStreamContext(envelope);
303
+ if (envelope.type === "session.turn.patch") {
304
+ this.handlePatchEnvelope(envelope);
305
+ return;
306
+ }
297
307
  switch (envelope.type) {
298
308
  case "system.ready": {
299
309
  const connectionId = typeof envelope.payload.connectionId === "string"
@@ -401,6 +411,9 @@ export class WebsocketClient {
401
411
  sessionId: envelope.sessionId ?? null,
402
412
  turnId,
403
413
  messageId,
414
+ messageOrdinal: typeof payload.messageOrdinal === "number"
415
+ ? payload.messageOrdinal
416
+ : null,
404
417
  anchorUserMessageId: typeof payload.anchorUserMessageId === "string"
405
418
  ? payload.anchorUserMessageId
406
419
  : null,
@@ -417,8 +430,89 @@ export class WebsocketClient {
417
430
  if (!turnId)
418
431
  return;
419
432
  for (const [sid, context] of this.compactStreamContexts.entries()) {
420
- if (context.turnId === turnId)
433
+ if (context.turnId === turnId) {
421
434
  this.compactStreamContexts.delete(sid);
435
+ this.patchStreamBuffers.delete(sid);
436
+ }
437
+ }
438
+ }
439
+ getPatchStreamBufferKey(envelope) {
440
+ if (envelope.type !== "session.turn.patch")
441
+ return null;
442
+ const payload = envelope.payload;
443
+ const realtimeMeta = payload._rt && typeof payload._rt === "object"
444
+ ? payload._rt
445
+ : null;
446
+ if (typeof realtimeMeta?.sid === "string" && realtimeMeta.sid.trim()) {
447
+ return realtimeMeta.sid;
448
+ }
449
+ if (typeof payload.turnId === "string" && payload.turnId.trim())
450
+ return payload.turnId;
451
+ if (typeof payload.messageId === "string" && payload.messageId.trim())
452
+ return payload.messageId;
453
+ return typeof envelope.sessionId === "string" && envelope.sessionId.trim()
454
+ ? envelope.sessionId
455
+ : null;
456
+ }
457
+ handlePatchEnvelope(envelope) {
458
+ const payload = envelope.payload;
459
+ if (typeof payload.seq !== "number" ||
460
+ typeof payload.baseSeq !== "number" ||
461
+ !Number.isInteger(payload.seq) ||
462
+ !Number.isInteger(payload.baseSeq) ||
463
+ payload.seq < 0 ||
464
+ payload.baseSeq < 0) {
465
+ this.emit("event", envelope);
466
+ return;
467
+ }
468
+ const key = this.getPatchStreamBufferKey(envelope);
469
+ if (!key) {
470
+ this.emit("event", envelope);
471
+ return;
472
+ }
473
+ if (payload.baseSeq === 0) {
474
+ const buffer = { nextSeq: payload.seq + 1, pending: new Map() };
475
+ this.patchStreamBuffers.set(key, buffer);
476
+ this.emit("event", envelope);
477
+ this.flushPatchStreamBuffer(buffer);
478
+ return;
479
+ }
480
+ const buffer = this.patchStreamBuffers.get(key);
481
+ if (!buffer) {
482
+ this.patchStreamBuffers.set(key, {
483
+ nextSeq: payload.baseSeq,
484
+ pending: new Map([[payload.seq, envelope]]),
485
+ });
486
+ return;
487
+ }
488
+ if (payload.seq < buffer.nextSeq)
489
+ return;
490
+ buffer.pending.set(payload.seq, envelope);
491
+ if (!this.enforcePatchStreamBufferLimit(key, buffer))
492
+ return;
493
+ this.flushPatchStreamBuffer(buffer);
494
+ }
495
+ enforcePatchStreamBufferLimit(key, buffer) {
496
+ if (buffer.pending.size <= PATCH_STREAM_BUFFER_MAX_PENDING)
497
+ return true;
498
+ this.patchStreamBuffers.delete(key);
499
+ this.emit("error", {
500
+ error: new Error(`patch stream buffer overflow: ${key}`),
501
+ recoverable: true,
502
+ });
503
+ return false;
504
+ }
505
+ flushPatchStreamBuffer(buffer) {
506
+ while (true) {
507
+ const envelope = buffer.pending.get(buffer.nextSeq);
508
+ if (!envelope)
509
+ return;
510
+ const seq = envelope.payload.seq;
511
+ if (typeof seq !== "number" || !Number.isInteger(seq))
512
+ return;
513
+ buffer.pending.delete(buffer.nextSeq);
514
+ buffer.nextSeq = seq + 1;
515
+ this.emit("event", envelope);
422
516
  }
423
517
  }
424
518
  handleCompactFrame(frame) {
@@ -448,13 +542,15 @@ export class WebsocketClient {
448
542
  payload: {
449
543
  turnId: context.turnId,
450
544
  messageId: context.messageId,
545
+ messageOrdinal: context.messageOrdinal,
451
546
  anchorUserMessageId: context.anchorUserMessageId,
452
547
  seq: frame.s,
453
548
  baseSeq: frame.b,
454
549
  ops: [op],
550
+ _rt: { sid: frame.sid },
455
551
  },
456
552
  };
457
- this.emit("event", envelope);
553
+ this.handlePatchEnvelope(envelope);
458
554
  }
459
555
  startPingLoop() {
460
556
  this.stopPingLoop();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Cohub SDK for spaces, sessions, checkpoints, and realtime agent collaboration.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -44,7 +44,7 @@
44
44
  "README.md"
45
45
  ],
46
46
  "dependencies": {
47
- "@neta-art/cohub-protocol": "1.2.1"
47
+ "@neta-art/cohub-protocol": "1.2.2"
48
48
  },
49
49
  "devDependencies": {
50
50
  "typescript": "^6.0.3"