@neta-art/cohub 1.3.0 → 1.4.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.
@@ -2,7 +2,7 @@ import type { SpacePublicEndpoints } from "@neta-art/cohub-protocol/ports";
2
2
  import type { WebsocketClient, WebsocketEventPayload } from "../websocket.js";
3
3
  import type { HttpTransport, Fetch } from "../transport.js";
4
4
  import { type SessionPatchApplyResult } from "../session-patch-reducer.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
+ import type { CheckpointRecord, ContentBlock, SessionMessageResponse, SessionMessagesPaginatedResponse, SessionMessagesResponse, SessionTurnResponse, SessionTurnStreamSnapshotResponse, SessionTurnIndexResponse, SessionTurnWindowResponse, SessionTurnsPaginatedResponse, SessionTurnSignedUrlsResponse, SessionRecord, SpaceAccessPolicy, SpaceBootstrapSource, SpaceChannelBindingInput, SpaceCheckpointDetailResponse, SpaceCreateResponse, SpaceEnvInput, SpaceFsCompleteUploadInput, SpaceFsCompleteUploadResponse, SpaceFsCreateUploadInput, SpaceFsCreateUploadResponse, SpaceFsFileResponse, SpaceFsMoveInput, SpaceFsTreeResponse, SpaceFsUploadResponse, SpaceUsageResponse, SpaceFsWriteFileInput, SpaceMarkKind, SpaceMarkListItem, SpaceMarkResourceType, SpaceMember, SpaceRecord, SpaceRole, SpaceSessionsResponse } from "../types.js";
6
6
  import { SpaceInvitationsApi } from "./invitations.js";
7
7
  export type SessionSubscriptionHandlers = {
8
8
  patch?: (event: WebsocketEventPayload) => void;
@@ -75,6 +75,8 @@ export declare class SpaceFilesApi {
75
75
  toPath: string;
76
76
  }>;
77
77
  upload(files: File[], dir?: string): Promise<SpaceFsUploadResponse>;
78
+ createUpload(input: SpaceFsCreateUploadInput): Promise<SpaceFsCreateUploadResponse>;
79
+ completeUpload(uploadId: string, input: SpaceFsCompleteUploadInput): Promise<SpaceFsCompleteUploadResponse>;
78
80
  }
79
81
  declare class SessionMessagesClient {
80
82
  private readonly transport;
@@ -117,6 +119,7 @@ declare class SessionTurnsClient {
117
119
  before?: number;
118
120
  after?: number;
119
121
  }, customFetch?: Fetch): Promise<SessionTurnWindowResponse>;
122
+ streamSnapshot(customFetch?: Fetch): Promise<SessionTurnStreamSnapshotResponse>;
120
123
  get(turnId: string, customFetch?: Fetch): Promise<SessionTurnResponse>;
121
124
  signedUrls(turnId: string, objectKeys: string[]): Promise<SessionTurnSignedUrlsResponse>;
122
125
  }
@@ -169,6 +169,20 @@ export class SpaceFilesApi {
169
169
  body: formData,
170
170
  });
171
171
  }
172
+ createUpload(input) {
173
+ return this.transport.request(`/api/spaces/${this.spaceId}/fs/uploads`, {
174
+ method: "POST",
175
+ headers: { "Content-Type": "application/json" },
176
+ body: JSON.stringify(input),
177
+ });
178
+ }
179
+ completeUpload(uploadId, input) {
180
+ return this.transport.request(`/api/spaces/${this.spaceId}/fs/uploads/${uploadId}/complete`, {
181
+ method: "POST",
182
+ headers: { "Content-Type": "application/json" },
183
+ body: JSON.stringify(input),
184
+ });
185
+ }
172
186
  }
173
187
  class SessionMessagesClient {
174
188
  transport;
@@ -273,6 +287,9 @@ class SessionTurnsClient {
273
287
  const query = params.toString();
274
288
  return this.transport.request(`/api/sessions/${this.sessionId}/turns/window${query ? `?${query}` : ""}`, { fetch: customFetch });
275
289
  }
290
+ streamSnapshot(customFetch) {
291
+ return this.transport.request(`/api/sessions/${this.sessionId}/turns/stream-snapshot`, { fetch: customFetch });
292
+ }
276
293
  get(turnId, customFetch) {
277
294
  return this.transport.request(`/api/sessions/${this.sessionId}/turns/${turnId}`, { fetch: customFetch });
278
295
  }
@@ -1,11 +1,9 @@
1
1
  import type { HttpTransport } from "../transport.js";
2
- import type { CreateScheduledTaskInput, TaskRunRecord } from "../types.js";
2
+ import type { CreateScheduledTaskInput, TaskRunDetailResponse, TaskRunRecord } from "../types.js";
3
3
  export declare class TasksApi {
4
4
  private readonly transport;
5
5
  constructor(transport: HttpTransport);
6
- get(taskRunId: string): Promise<{
7
- run: TaskRunRecord;
8
- }>;
6
+ get(taskRunId: string): Promise<TaskRunDetailResponse>;
9
7
  list(filters?: {
10
8
  cronJobId?: string;
11
9
  spaceId?: string;
@@ -1,12 +1,18 @@
1
1
  import { type HttpTransport, type Fetch } from "../transport.js";
2
- import type { UserRulesResponse, UserSshKey } from "../types.js";
2
+ import type { MeResponse, UserProfile, UserRulesResponse, UserSshKey } from "../types.js";
3
3
  export declare class UserApi {
4
4
  private readonly transport;
5
5
  private readonly transportBaseUrl;
6
6
  private readonly setStoredAuthToken?;
7
7
  private readonly clearStoredAuthToken?;
8
8
  constructor(transport: HttpTransport, transportBaseUrl: string, setStoredAuthToken?: ((token: string) => void) | undefined, clearStoredAuthToken?: (() => void) | undefined);
9
- getMe(customFetch?: Fetch): Promise<unknown>;
9
+ getMe(customFetch?: Fetch): Promise<MeResponse>;
10
+ updateProfile(input: {
11
+ displayName?: string;
12
+ avatarUrl?: string | null;
13
+ }): Promise<{
14
+ profile: UserProfile;
15
+ }>;
10
16
  getRules(customFetch?: Fetch): Promise<UserRulesResponse>;
11
17
  setAuthToken(token: string): Promise<any>;
12
18
  clearAuthToken(): Promise<null>;
package/dist/apis/user.js CHANGED
@@ -13,6 +13,13 @@ export class UserApi {
13
13
  getMe(customFetch) {
14
14
  return this.transport.request("/api/me", { fetch: customFetch });
15
15
  }
16
+ updateProfile(input) {
17
+ return this.transport.request("/api/me/profile", {
18
+ method: "PATCH",
19
+ headers: { "Content-Type": "application/json" },
20
+ body: JSON.stringify(input),
21
+ });
22
+ }
16
23
  getRules(customFetch) {
17
24
  return this.transport.request("/api/me/rules", {
18
25
  method: "GET",
@@ -28,6 +28,13 @@ export type SessionPatchApplyResult = {
28
28
  reason: "duplicate" | "version_mismatch" | "invalid";
29
29
  state: SessionPatchState;
30
30
  };
31
+ export type SessionPatchSnapshotInput = SessionPatchKeyInput & {
32
+ turnId?: string | null;
33
+ seq: number;
34
+ contentBlocks: ContentBlock[];
35
+ anchorUserMessageId?: string | null;
36
+ appendPath?: string | null;
37
+ };
31
38
  type SessionPatchKeyInput = {
32
39
  spaceId?: string | null;
33
40
  sessionId: string;
@@ -44,6 +51,7 @@ export declare class SessionPatchReducer {
44
51
  complete(input: SessionPatchKeyInput): SessionPatchState;
45
52
  fail(input: SessionPatchKeyInput): SessionPatchState;
46
53
  reset(input: SessionPatchKeyInput): void;
54
+ applySnapshot(input: SessionPatchSnapshotInput): SessionPatchApplyResult;
47
55
  resetAll(): void;
48
56
  applyEvent(event: SessionTurnPatchEvent): SessionPatchApplyResult;
49
57
  applyPatch(input: SessionPatchApplyInput): SessionPatchApplyResult;
@@ -1,7 +1,6 @@
1
- const blockTextPathPattern = /^\/message\/content\/blocks\/(\d+)\/(text|thinking)$/;
1
+ const blockSubPathPattern = /^\/message\/content\/blocks\/(\d+)\/(.+)$/;
2
2
  const blockPathPattern = /^\/message\/content\/blocks\/(\d+)$/;
3
3
  const blockMetaPathPattern = /^\/message\/content\/blocks\/(\d+)\/_meta$/;
4
- const blockSignaturePathPattern = /^\/message\/content\/blocks\/(\d+)\/signature$/;
5
4
  const createIdleState = (input) => ({
6
5
  spaceId: input.spaceId ?? null,
7
6
  sessionId: input.sessionId,
@@ -38,6 +37,14 @@ function sortBlocksByStreamIndex(blocks) {
38
37
  function isContentBlock(value) {
39
38
  return Boolean(value && typeof value === "object" && "type" in value);
40
39
  }
40
+ function isPlainObject(value) {
41
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
42
+ }
43
+ function decodePointerSegments(encoded) {
44
+ if (!encoded)
45
+ return [];
46
+ return encoded.split("/").map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"));
47
+ }
41
48
  function ensureTextLikeBlock(blocks, streamIndex, field) {
42
49
  const existingIndex = findBlockByStreamIndex(blocks, streamIndex);
43
50
  const existing = existingIndex >= 0 ? blocks[existingIndex] : undefined;
@@ -62,21 +69,103 @@ function ensureTextLikeBlock(blocks, streamIndex, field) {
62
69
  blocks.push(block);
63
70
  return block;
64
71
  }
65
- function appendTextLikeValue(blocks, path, value) {
66
- const match = path.match(blockTextPathPattern);
67
- if (!match || typeof value !== "string")
72
+ function getOrCreateBlockForSubpath(blocks, streamIndex, firstSegment) {
73
+ const idx = findBlockByStreamIndex(blocks, streamIndex);
74
+ if (idx >= 0)
75
+ return blocks[idx] ?? null;
76
+ if (firstSegment === "text") {
77
+ return ensureTextLikeBlock(blocks, streamIndex, "text");
78
+ }
79
+ if (firstSegment === "thinking") {
80
+ return ensureTextLikeBlock(blocks, streamIndex, "thinking");
81
+ }
82
+ return null;
83
+ }
84
+ function setDeepOnContentBlock(root, segments, value) {
85
+ if (segments.length === 0)
68
86
  return false;
69
- const streamIndex = Number(match[1]);
70
- const field = match[2];
71
- const block = ensureTextLikeBlock(blocks, streamIndex, field);
72
- if (field === "text" && block.type === "text") {
73
- block.text += value;
87
+ let cur = root;
88
+ for (let i = 0; i < segments.length - 1; i++) {
89
+ const k = segments[i];
90
+ if (k === undefined)
91
+ return false;
92
+ if (!isPlainObject(cur))
93
+ return false;
94
+ const next = cur[k];
95
+ if (next === undefined)
96
+ return false;
97
+ cur = next;
74
98
  }
75
- if (field === "thinking" && block.type === "thinking") {
76
- block.thinking += value;
99
+ const last = segments[segments.length - 1];
100
+ if (last === undefined)
101
+ return false;
102
+ if (!isPlainObject(cur))
103
+ return false;
104
+ const toAssign = value !== null && typeof value === "object"
105
+ ? structuredClone(value)
106
+ : value;
107
+ cur[last] = toAssign;
108
+ return true;
109
+ }
110
+ function appendDeepOnContentBlock(root, segments, suffix) {
111
+ if (segments.length === 0)
112
+ return false;
113
+ let cur = root;
114
+ for (let i = 0; i < segments.length - 1; i++) {
115
+ const k = segments[i];
116
+ if (k === undefined)
117
+ return false;
118
+ if (!isPlainObject(cur))
119
+ return false;
120
+ const next = cur[k];
121
+ if (next === undefined)
122
+ return false;
123
+ cur = next;
77
124
  }
125
+ const last = segments[segments.length - 1];
126
+ if (last === undefined)
127
+ return false;
128
+ if (!isPlainObject(cur))
129
+ return false;
130
+ const parent = cur;
131
+ const leaf = parent[last];
132
+ if (typeof leaf !== "string")
133
+ return false;
134
+ parent[last] = leaf + suffix;
78
135
  return true;
79
136
  }
137
+ function resolveBlockForSubpath(blocks, streamIndex, firstSegment) {
138
+ const idx = findBlockByStreamIndex(blocks, streamIndex);
139
+ if (idx >= 0)
140
+ return blocks[idx] ?? null;
141
+ return getOrCreateBlockForSubpath(blocks, streamIndex, firstSegment);
142
+ }
143
+ function applyReplaceAtBlockSubpath(blocks, streamIndex, encodedTail, value) {
144
+ const segs = decodePointerSegments(encodedTail);
145
+ if (segs.length === 0)
146
+ return false;
147
+ const block = resolveBlockForSubpath(blocks, streamIndex, segs[0] ?? "");
148
+ if (!block)
149
+ return false;
150
+ return setDeepOnContentBlock(block, segs, value);
151
+ }
152
+ function applyAppendAtBlockSubpath(blocks, streamIndex, encodedTail, suffix) {
153
+ if (typeof suffix !== "string")
154
+ return false;
155
+ const segs = decodePointerSegments(encodedTail);
156
+ if (segs.length === 0)
157
+ return false;
158
+ const block = resolveBlockForSubpath(blocks, streamIndex, segs[0] ?? "");
159
+ if (!block)
160
+ return false;
161
+ return appendDeepOnContentBlock(block, segs, suffix);
162
+ }
163
+ function appendPatchStreamValue(blocks, path, value) {
164
+ const m = path.match(blockSubPathPattern);
165
+ if (!m)
166
+ return false;
167
+ return applyAppendAtBlockSubpath(blocks, Number(m[1]), m[2] ?? "", value);
168
+ }
80
169
  function applyPatchOpsToBlocks(current, ops, initialAppendPath) {
81
170
  const next = current.map(cloneBlock);
82
171
  let anchorUserMessageId;
@@ -84,7 +173,7 @@ function applyPatchOpsToBlocks(current, ops, initialAppendPath) {
84
173
  let failed = false;
85
174
  for (const op of ops) {
86
175
  if (!op.o && !op.p) {
87
- if (!appendPath || !appendTextLikeValue(next, appendPath, op.v)) {
176
+ if (!appendPath || !appendPatchStreamValue(next, appendPath, op.v)) {
88
177
  failed = true;
89
178
  break;
90
179
  }
@@ -98,7 +187,7 @@ function applyPatchOpsToBlocks(current, ops, initialAppendPath) {
98
187
  continue;
99
188
  }
100
189
  if (op.o === "append") {
101
- if (!appendTextLikeValue(next, op.p, op.v)) {
190
+ if (typeof op.p !== "string" || !appendPatchStreamValue(next, op.p, op.v)) {
102
191
  failed = true;
103
192
  break;
104
193
  }
@@ -118,15 +207,15 @@ function applyPatchOpsToBlocks(current, ops, initialAppendPath) {
118
207
  continue;
119
208
  }
120
209
  if (op.o === "replace") {
121
- const match = op.p.match(blockSignaturePathPattern);
122
- if (match) {
123
- if (typeof op.v !== "string")
210
+ const sub = op.p.match(blockSubPathPattern);
211
+ if (sub?.[2] && typeof op.p === "string") {
212
+ const streamIndex = Number(sub[1]);
213
+ const encodedTail = sub[2];
214
+ if (applyReplaceAtBlockSubpath(next, streamIndex, encodedTail, op.v)) {
124
215
  continue;
125
- const blockIndex = findBlockByStreamIndex(next, Number(match[1]));
126
- const block = blockIndex >= 0 ? next[blockIndex] : undefined;
127
- if (block?.type === "thinking")
128
- block.signature = op.v;
129
- continue;
216
+ }
217
+ failed = true;
218
+ break;
130
219
  }
131
220
  }
132
221
  if (op.o === "replace" || op.o === "add") {
@@ -219,6 +308,28 @@ export class SessionPatchReducer {
219
308
  reset(input) {
220
309
  this.states.delete(this.key(input));
221
310
  }
311
+ applySnapshot(input) {
312
+ const current = this.get(input);
313
+ const inputTurnId = input.turnId ?? null;
314
+ const currentTurnId = current.turnId;
315
+ const isDifferentKnownTurn = Boolean(currentTurnId && inputTurnId && currentTurnId !== inputTurnId);
316
+ if (!isDifferentKnownTurn && input.seq < current.patchSeq) {
317
+ return { applied: false, reason: "duplicate", state: current };
318
+ }
319
+ const state = {
320
+ ...current,
321
+ spaceId: input.spaceId ?? current.spaceId ?? null,
322
+ sessionId: input.sessionId,
323
+ status: "streaming",
324
+ contentBlocks: sortBlocksByStreamIndex(input.contentBlocks.map(cloneBlock)),
325
+ anchorUserMessageId: input.anchorUserMessageId ?? current.anchorUserMessageId ?? null,
326
+ patchSeq: input.seq,
327
+ turnId: inputTurnId ?? current.turnId ?? null,
328
+ appendPath: input.appendPath ?? null,
329
+ };
330
+ this.states.set(this.key(input), state);
331
+ return { applied: true, state };
332
+ }
222
333
  resetAll() {
223
334
  this.states.clear();
224
335
  }
package/dist/types.d.ts CHANGED
@@ -6,6 +6,17 @@ export type { ChannelConfig, DiscordChannelConfig, } from "@neta-art/cohub-proto
6
6
  export type ApiError = {
7
7
  message: string;
8
8
  };
9
+ export type UserProfile = {
10
+ userUuid: string;
11
+ logtoUserId?: string;
12
+ displayName: string;
13
+ avatarUrl: string | null;
14
+ syncedAt?: string;
15
+ };
16
+ export type MeResponse = {
17
+ uuid: string;
18
+ profile: UserProfile;
19
+ };
9
20
  export type UserRulesResponse = {
10
21
  content: string;
11
22
  updatedAt: string | null;
@@ -55,13 +66,55 @@ export type SpaceFsUploadEntry = {
55
66
  };
56
67
  export type SpaceFsUploadError = {
57
68
  name: string;
58
- code: "file_too_large" | "name_invalid" | "write_failed";
69
+ code: "file_too_large" | "name_invalid" | "path_invalid" | "write_failed" | "object_missing";
59
70
  message: string;
60
71
  };
61
72
  export type SpaceFsUploadResponse = {
62
73
  uploaded: SpaceFsUploadEntry[];
63
74
  errors: SpaceFsUploadError[];
64
75
  };
76
+ export type SpaceFsUploadPlanEntryInput = {
77
+ id: string;
78
+ name: string;
79
+ relativePath: string;
80
+ size: number;
81
+ mimeType?: string | null;
82
+ lastModified?: number;
83
+ };
84
+ export type SpaceFsCreateUploadInput = {
85
+ targetDir?: string;
86
+ entries: SpaceFsUploadPlanEntryInput[];
87
+ };
88
+ export type SpaceFsUploadPlanEntry = {
89
+ id: string;
90
+ objectKey: string;
91
+ uploadUrl: string;
92
+ headers?: Record<string, string>;
93
+ };
94
+ export type SpaceFsCreateUploadResponse = {
95
+ uploadId: string;
96
+ expiresAt: string;
97
+ entries: SpaceFsUploadPlanEntry[];
98
+ };
99
+ export type SpaceFsCompleteUploadInput = {
100
+ entries: Array<{
101
+ id: string;
102
+ etag?: string | null;
103
+ }>;
104
+ };
105
+ export type SpaceFsCompleteUploadResponse = {
106
+ ok: true;
107
+ taskRunId: string;
108
+ };
109
+ export type SpaceFsUploadProgress = {
110
+ phase: "queued" | "importing" | "done" | "failed";
111
+ totalFiles: number;
112
+ importedFiles: number;
113
+ totalBytes: number;
114
+ importedBytes: number;
115
+ currentPath?: string;
116
+ errors: SpaceFsUploadError[];
117
+ };
65
118
  export type SessionBindingRecord = ProtocolSessionBindingRecord;
66
119
  export type SessionRecord = ProtocolSessionRecord & {
67
120
  bindings?: SessionBindingRecord[];
@@ -154,6 +207,28 @@ export type SessionTurnResponse = {
154
207
  export type SessionTurnSignedUrlsResponse = {
155
208
  urls: Record<string, string>;
156
209
  };
210
+ export type SessionTurnStreamSnapshotResponse = {
211
+ snapshot: {
212
+ version: 2;
213
+ spaceId: string;
214
+ sessionId: string;
215
+ turnId: string | null;
216
+ anchorUserMessageId: string | null;
217
+ seq: number;
218
+ current: {
219
+ messageId: string | null;
220
+ messageOrdinal: number | null;
221
+ content: ContentBlock[];
222
+ appendPath: string | null;
223
+ };
224
+ intermediateMessages: Array<{
225
+ messageId: string | null;
226
+ messageOrdinal: number | null;
227
+ content: ContentBlock[];
228
+ }>;
229
+ updatedAt: number;
230
+ } | null;
231
+ };
157
232
  export type ModelCatalogEntry = {
158
233
  provider: string;
159
234
  id: string;
@@ -220,6 +295,10 @@ export type CronJobRecord = {
220
295
  createdAt: string;
221
296
  updatedAt: string;
222
297
  };
298
+ export type TaskRunDetailResponse = {
299
+ run: TaskRunRecord;
300
+ progress: unknown;
301
+ };
223
302
  export type TaskRunRecord = {
224
303
  id: string;
225
304
  jobId: string;
@@ -272,6 +351,7 @@ export type SpaceRole = "host" | "builder" | "guest";
272
351
  export type SpaceMember = {
273
352
  userId: string;
274
353
  role: SpaceRole;
354
+ profile: UserProfile;
275
355
  createdAt: string;
276
356
  updatedAt: string;
277
357
  };
package/dist/websocket.js CHANGED
@@ -610,6 +610,19 @@ export class WebsocketClient {
610
610
  });
611
611
  if (this.manuallyClosed)
612
612
  return;
613
+ if (typeof navigator !== "undefined" && navigator.onLine === false) {
614
+ await new Promise((resolve) => {
615
+ const fallbackTimer = setTimeout(resolve, this.reconnectMaxDelayMs);
616
+ const handleOnline = () => {
617
+ clearTimeout(fallbackTimer);
618
+ globalThis.removeEventListener?.("online", handleOnline);
619
+ resolve();
620
+ };
621
+ globalThis.addEventListener?.("online", handleOnline, { once: true });
622
+ });
623
+ if (this.manuallyClosed)
624
+ return;
625
+ }
613
626
  await this.connect().catch((error) => {
614
627
  this.emit("error", { error, recoverable: true });
615
628
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub",
3
- "version": "1.3.0",
3
+ "version": "1.4.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.2"
47
+ "@neta-art/cohub-protocol": "1.2.3"
48
48
  },
49
49
  "devDependencies": {
50
50
  "typescript": "^6.0.3"