@neta-art/cohub 1.1.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
  }
@@ -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, SpaceUsageResponse, 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;
@@ -114,13 +119,19 @@ export declare class SpaceSessionsApi {
114
119
  ok: true;
115
120
  session: SessionRecord;
116
121
  }>;
117
- list(customFetch?: Fetch): Promise<SpaceSessionsResponse>;
122
+ list(optionsOrFetch?: {
123
+ limit?: number;
124
+ cursor?: string | null;
125
+ } | Fetch, customFetch?: Fetch): Promise<SpaceSessionsResponse>;
118
126
  byId(sessionId: string): SessionClient;
119
127
  }
120
128
  export type WebSocketConnectionState = {
121
- state: "open" | "closed" | "error";
129
+ state: "connecting" | "reconnecting" | "open" | "closed" | "error";
122
130
  willReconnect: boolean;
123
131
  connectionId?: string | null;
132
+ attempt?: number;
133
+ delayMs?: number;
134
+ recoverable?: boolean;
124
135
  };
125
136
  export declare class SpaceEventsApi {
126
137
  private readonly websocketClient;
@@ -157,6 +168,49 @@ export declare class SpaceUsageApi {
157
168
  constructor(transport: HttpTransport, spaceId: string);
158
169
  get(days?: number, customFetch?: Fetch): Promise<SpaceUsageResponse>;
159
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
+ }
160
214
  export declare class SpaceCheckpointsApi {
161
215
  private readonly transport;
162
216
  private readonly spaceId;
@@ -180,6 +234,9 @@ export declare class SpaceClient {
180
234
  readonly access: SpaceAccessApi;
181
235
  readonly checkpoints: SpaceCheckpointsApi;
182
236
  readonly usage: SpaceUsageApi;
237
+ readonly channels: SpaceChannelsApi;
238
+ readonly env: SpaceEnvApi;
239
+ readonly invitations: SpaceInvitationsApi;
183
240
  constructor(id: string, transport: HttpTransport, websocketClient: WebsocketClient | null);
184
241
  get(customFetch?: Fetch): Promise<SpaceRecord>;
185
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)
@@ -233,9 +258,17 @@ export class SpaceSessionsApi {
233
258
  body: JSON.stringify(input ?? {}),
234
259
  });
235
260
  }
236
- list(customFetch) {
237
- return this.transport.request(`/api/spaces/${this.spaceId}/sessions`, {
238
- 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,
239
272
  });
240
273
  }
241
274
  byId(sessionId) {
@@ -326,6 +359,55 @@ export class SpaceUsageApi {
326
359
  return this.transport.request(`/api/spaces/${this.spaceId}/usage?${params.toString()}`, { fetch: customFetch });
327
360
  }
328
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
+ }
329
411
  export class SpaceCheckpointsApi {
330
412
  transport;
331
413
  spaceId;
@@ -357,6 +439,9 @@ export class SpaceClient {
357
439
  access;
358
440
  checkpoints;
359
441
  usage;
442
+ channels;
443
+ env;
444
+ invitations;
360
445
  constructor(id, transport, websocketClient) {
361
446
  this.id = id;
362
447
  this.transport = transport;
@@ -367,6 +452,9 @@ export class SpaceClient {
367
452
  this.access = new SpaceAccessApi(transport, id);
368
453
  this.checkpoints = new SpaceCheckpointsApi(transport, id);
369
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);
370
458
  }
371
459
  get(customFetch) {
372
460
  return this.transport.request(`/api/spaces/${this.id}`, {
package/dist/client.d.ts CHANGED
@@ -6,6 +6,7 @@ import { SessionAccessApi } from "./apis/session-access.js";
6
6
  import { SpaceClient, SpacesApi, type WebSocketConnectionState } from "./apis/spaces.js";
7
7
  import { TasksApi } from "./apis/tasks.js";
8
8
  import { UserApi } from "./apis/user.js";
9
+ import { PublicInviteApi } from "./apis/invitations.js";
9
10
  import { type CohubClientOptions } from "./transport.js";
10
11
  export declare class CohubClient {
11
12
  readonly spaces: SpacesApi;
@@ -16,6 +17,7 @@ export declare class CohubClient {
16
17
  readonly sessionAccess: SessionAccessApi;
17
18
  readonly tasks: TasksApi;
18
19
  readonly cronJobs: CronJobsApi;
20
+ readonly invite: PublicInviteApi;
19
21
  private readonly transport;
20
22
  private readonly websocketClient;
21
23
  constructor(options?: CohubClientOptions);
package/dist/client.js CHANGED
@@ -6,8 +6,10 @@ import { SessionAccessApi } from "./apis/session-access.js";
6
6
  import { SpaceClient, SpacesApi } from "./apis/spaces.js";
7
7
  import { TasksApi } from "./apis/tasks.js";
8
8
  import { UserApi } from "./apis/user.js";
9
+ import { PublicInviteApi } from "./apis/invitations.js";
9
10
  import { HttpTransport } from "./transport.js";
10
11
  import { createWebsocketClient } from "./websocket.js";
12
+ import { resolveApiBaseUrl, resolveWebsocketUrl } from "./environment.js";
11
13
  export class CohubClient {
12
14
  spaces;
13
15
  channels;
@@ -17,27 +19,49 @@ export class CohubClient {
17
19
  sessionAccess;
18
20
  tasks;
19
21
  cronJobs;
22
+ invite;
20
23
  transport;
21
24
  websocketClient;
22
25
  constructor(options = {}) {
26
+ const apiBaseUrl = resolveApiBaseUrl(options);
23
27
  this.transport = new HttpTransport(options);
24
28
  this.websocketClient = createWebsocketClient({
29
+ url: resolveWebsocketUrl({
30
+ env: options.websocket?.env ?? options.env,
31
+ url: options.websocket?.url,
32
+ }),
25
33
  ...options.websocket,
26
34
  getAccessToken: options.getAccessToken,
27
35
  });
28
36
  this.spaces = new SpacesApi(this.transport);
29
37
  this.channels = new ChannelsApi(this.transport);
30
- this.user = new UserApi(this.transport, options.baseUrl ?? "", options.setStoredAuthToken, options.clearStoredAuthToken);
31
- this.models = new ModelsApi(options.fetch ?? fetch, options.baseUrl ?? "");
32
- this.prompts = new PromptsApi(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);
33
41
  this.sessionAccess = new SessionAccessApi(this.transport);
34
42
  this.tasks = new TasksApi(this.transport);
35
43
  this.cronJobs = new CronJobsApi(this.transport);
44
+ this.invite = new PublicInviteApi(this.transport);
36
45
  }
37
46
  space(spaceId) {
38
47
  return new SpaceClient(spaceId, this.transport, this.websocketClient);
39
48
  }
40
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
+ });
41
65
  const openCleanup = this.websocketClient.on("open", (payload) => {
42
66
  handler({
43
67
  state: "open",
@@ -51,10 +75,16 @@ export class CohubClient {
51
75
  willReconnect: payload.willReconnect,
52
76
  });
53
77
  });
54
- const errorCleanup = this.websocketClient.on("error", () => {
55
- handler({ state: "error", willReconnect: true });
78
+ const errorCleanup = this.websocketClient.on("error", (payload) => {
79
+ handler({
80
+ state: "error",
81
+ willReconnect: payload.recoverable,
82
+ recoverable: payload.recoverable,
83
+ });
56
84
  });
57
85
  return () => {
86
+ connectingCleanup();
87
+ reconnectingCleanup();
58
88
  openCleanup();
59
89
  closeCleanup();
60
90
  errorCleanup();
@@ -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
@@ -6,6 +6,7 @@ import { SessionAccessApi } from "./apis/session-access.js";
6
6
  import { SpaceClient, SpacesApi } from "./apis/spaces.js";
7
7
  import { TasksApi } from "./apis/tasks.js";
8
8
  import { UserApi } from "./apis/user.js";
9
+ import { PublicInviteApi } from "./apis/invitations.js";
9
10
  import { HttpTransport, HttpError, type CohubClientOptions, type Fetch } from "./transport.js";
10
11
  export declare class CohubHttpClient {
11
12
  readonly spaces: SpacesApi;
@@ -16,6 +17,7 @@ export declare class CohubHttpClient {
16
17
  readonly sessionAccess: SessionAccessApi;
17
18
  readonly tasks: TasksApi;
18
19
  readonly cronJobs: CronJobsApi;
20
+ readonly invite: PublicInviteApi;
19
21
  private readonly transport;
20
22
  constructor(options?: CohubClientOptions);
21
23
  space(spaceId: string): SpaceClient;
package/dist/http.js CHANGED
@@ -6,7 +6,9 @@ import { SessionAccessApi } from "./apis/session-access.js";
6
6
  import { SpaceClient, SpacesApi } from "./apis/spaces.js";
7
7
  import { TasksApi } from "./apis/tasks.js";
8
8
  import { UserApi } from "./apis/user.js";
9
+ import { PublicInviteApi } from "./apis/invitations.js";
9
10
  import { HttpTransport, HttpError } from "./transport.js";
11
+ import { resolveApiBaseUrl } from "./environment.js";
10
12
  export class CohubHttpClient {
11
13
  spaces;
12
14
  channels;
@@ -16,17 +18,20 @@ export class CohubHttpClient {
16
18
  sessionAccess;
17
19
  tasks;
18
20
  cronJobs;
21
+ invite;
19
22
  transport;
20
23
  constructor(options = {}) {
24
+ const apiBaseUrl = resolveApiBaseUrl(options);
21
25
  this.transport = new HttpTransport(options);
22
26
  this.spaces = new SpacesApi(this.transport);
23
27
  this.channels = new ChannelsApi(this.transport);
24
- this.user = new UserApi(this.transport, options.baseUrl ?? "", options.setStoredAuthToken, options.clearStoredAuthToken);
25
- this.models = new ModelsApi(options.fetch ?? fetch, options.baseUrl ?? "");
26
- this.prompts = new PromptsApi(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);
27
31
  this.sessionAccess = new SessionAccessApi(this.transport);
28
32
  this.tasks = new TasksApi(this.transport);
29
33
  this.cronJobs = new CronJobsApi(this.transport);
34
+ this.invite = new PublicInviteApi(this.transport);
30
35
  }
31
36
  space(spaceId) {
32
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, WebSocketConnectionState } 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";
@@ -89,6 +110,10 @@ export type SessionMessagesResponse = {
89
110
  session: SessionRecord;
90
111
  messages: MessageRecord[];
91
112
  };
113
+ export type SessionMessageResponse = {
114
+ session: SessionRecord;
115
+ message: MessageRecord;
116
+ };
92
117
  export type SessionMessagesPaginatedResponse = {
93
118
  session: SessionRecord;
94
119
  messages: MessageRecord[];
@@ -134,6 +159,10 @@ export type SpaceChannelBindingInput = {
134
159
  };
135
160
  export type SpaceSessionsResponse = {
136
161
  sessions: SessionRecord[];
162
+ pageInfo?: {
163
+ hasMore: boolean;
164
+ nextCursor: string | null;
165
+ };
137
166
  };
138
167
  export type UserSshKey = {
139
168
  id: string;
@@ -245,3 +274,36 @@ export type SpaceUsageResponse = {
245
274
  summary: SpaceUsageSummary;
246
275
  days: number;
247
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.1.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,
@@ -43,14 +43,14 @@
43
43
  "dist",
44
44
  "README.md"
45
45
  ],
46
- "scripts": {
47
- "build": "tsc -p tsconfig.build.json",
48
- "typecheck": "tsc -p tsconfig.json --noEmit"
49
- },
50
46
  "dependencies": {
51
- "@neta-art/cohub-protocol": "^1.0.0"
47
+ "@neta-art/cohub-protocol": "1.2.0"
52
48
  },
53
49
  "devDependencies": {
54
50
  "typescript": "^6.0.3"
51
+ },
52
+ "scripts": {
53
+ "build": "tsc -p tsconfig.build.json",
54
+ "typecheck": "tsc -p tsconfig.json --noEmit"
55
55
  }
56
- }
56
+ }