@parall/sdk 1.36.1 → 1.38.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/src/client.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import { ENDPOINTS, WIKI_BASE } from './constants.js';
2
2
  import type {
3
+ ClaimDispatchRequest,
4
+ ClaimDispatchResponse,
5
+ SteerDispatchRequest,
6
+ SteerDispatchResponse,
7
+ CompleteDispatchRequest,
8
+ CompleteDispatchResult,
3
9
  AuthTokens,
4
10
  AvatarUploadResponse,
5
11
  RegisterRequest,
@@ -41,6 +47,7 @@ import type {
41
47
  MachineRuntimeAuthState,
42
48
  AgentWorkspaceState,
43
49
  AgentWorkspaceReportStatus,
50
+ DetectedRuntime,
44
51
  BrowseMachineFilesystemResponse,
45
52
  FilesystemEntry,
46
53
  DaemonAgentConfig,
@@ -74,6 +81,14 @@ import type {
74
81
  CreateExternalConnectionInput,
75
82
  UpdateExternalConnectionInput,
76
83
  ExternalConnectionFilters,
84
+ ChannelConnection,
85
+ ChannelConversation,
86
+ ChannelMessage,
87
+ ChannelCredentialsInput,
88
+ ChannelProvisioningSession,
89
+ CreateChannelConnectionInput,
90
+ InitiateChannelProvisioningInput,
91
+ UpdateChannelConnectionInput,
77
92
  ExternalIngressEvent,
78
93
  ExternalIngressEventFilters,
79
94
  ExternalTrigger,
@@ -97,6 +112,8 @@ import type {
97
112
  PlatformConfigResponse,
98
113
  Wiki,
99
114
  WikiBlob,
115
+ CopyWikiFileRequest,
116
+ CopyWikiFileResponse,
100
117
  WikiChangeset,
101
118
  WikiFileUploadResponse,
102
119
  WikiFilePreviewUrlResponse,
@@ -163,6 +180,8 @@ import type {
163
180
  UpdateAutoReloadRequest,
164
181
  ComputePricing,
165
182
  ComputePricingResponse,
183
+ RuntimeCapability,
184
+ PlatformModelsResponse,
166
185
  Clip,
167
186
  AgentClip,
168
187
  CreateClipRequest,
@@ -178,6 +197,7 @@ import type {
178
197
  MachineBrowserProfile,
179
198
  BrowserProfileListItem,
180
199
  BrowserProfileStatus,
200
+ BrowserRuntimeStatus,
181
201
  BrowserProfileConsent,
182
202
  CreateBrowserProfileRequest,
183
203
  UpdateBrowserProfileRequest,
@@ -360,7 +380,13 @@ export class ParallClient {
360
380
  body?: unknown,
361
381
  query?: Record<string, string | number | boolean | undefined>,
362
382
  retried = false,
363
- opts?: { timeoutMs?: number; signal?: AbortSignal; keepalive?: boolean },
383
+ opts?: {
384
+ timeoutMs?: number;
385
+ signal?: AbortSignal;
386
+ keepalive?: boolean;
387
+ /** Observes the final HTTP status of a successful request (e.g. 200-idempotent-replay vs 201-created). */
388
+ onStatus?: (status: number) => void;
389
+ },
364
390
  ): Promise<T> {
365
391
  // Proactive refresh: block until token is fresh (no-op if still valid)
366
392
  if (!retried) {
@@ -427,6 +453,7 @@ export class ParallClient {
427
453
  throw buildApiError(res, rawErrorBody);
428
454
  }
429
455
 
456
+ opts?.onStatus?.(res.status);
430
457
  if (res.status === 204) return undefined as T;
431
458
  // 202 Accepted may have an empty body (e.g., async restart) or a JSON body.
432
459
  if (res.status === 202) {
@@ -631,6 +658,17 @@ export class ParallClient {
631
658
  return res.data;
632
659
  }
633
660
 
661
+ /** User IDs of soft-removed (former) org members — for marking their avatar /
662
+ * name as "left" where dormant relations still surface them (DMs, task
663
+ * assignees, message history). */
664
+ async getFormerMemberIds(orgId: string): Promise<string[]> {
665
+ const res = await this.request<{ user_ids: string[] }>(
666
+ 'GET',
667
+ ENDPOINTS.ORG_MEMBERS_FORMER(orgId),
668
+ );
669
+ return res.user_ids ?? [];
670
+ }
671
+
634
672
  async getTeams(orgId: string): Promise<Team[]> {
635
673
  const res = await this.request<{ data: Team[] }>('GET', ENDPOINTS.TEAMS(orgId));
636
674
  return res.data;
@@ -944,6 +982,33 @@ export class ParallClient {
944
982
  return this.request('POST', ENDPOINTS.CHAT_MESSAGES(orgId, chatId), req);
945
983
  }
946
984
 
985
+ /**
986
+ * sendMessage variant that also reports whether the server answered with an
987
+ * idempotent replay (HTTP 200 — the message already existed for this
988
+ * idempotency/effect key) instead of a fresh create (201). Used by the CLI
989
+ * to surface "already sent by a previous run (deduplicated)".
990
+ */
991
+ async sendMessageDetailed(
992
+ orgId: string,
993
+ chatId: string,
994
+ req: SendMessageRequest,
995
+ ): Promise<{ message: Message; deduplicated: boolean }> {
996
+ let status = 0;
997
+ const message = await this.request<Message>(
998
+ 'POST',
999
+ ENDPOINTS.CHAT_MESSAGES(orgId, chatId),
1000
+ req,
1001
+ undefined,
1002
+ false,
1003
+ {
1004
+ onStatus: (s) => {
1005
+ status = s;
1006
+ },
1007
+ },
1008
+ );
1009
+ return { message, deduplicated: status === 200 };
1010
+ }
1011
+
947
1012
  async getMessages(
948
1013
  orgId: string,
949
1014
  chatId: string,
@@ -1515,29 +1580,40 @@ export class ParallClient {
1515
1580
  profileId: string,
1516
1581
  status: BrowserProfileStatus,
1517
1582
  errorMsg?: string,
1583
+ // The lifecycle generation this report was issued under (captured at op start,
1584
+ // PR #1650). Omitted by pre-generation callers; the server then falls back to
1585
+ // safe-state compatibility. The server no-ops a report older than the row's.
1586
+ generation?: number,
1518
1587
  ): Promise<void> {
1519
1588
  await this.request('POST', ENDPOINTS.MACHINES_ME_BROWSER_PROFILE_STATUS(profileId), {
1520
1589
  status,
1521
1590
  ...(errorMsg ? { error_msg: errorMsg } : {}),
1591
+ ...(generation !== undefined ? { generation } : {}),
1522
1592
  });
1523
1593
  }
1524
1594
 
1525
1595
  /**
1526
1596
  * `POST /machines/me/health` — bump the Machine's `updated_at` to now and
1527
- * report daemon state. The daemon should call this on a fixed cadence (e.g.
1528
- * every 30s) so an external observer can detect a wedged supervisor. Both
1597
+ * report daemon state. The daemon calls this on startup and again whenever
1598
+ * periodic runtime re-detection produces a CHANGED result (there is no
1599
+ * fixed-cadence keepalive loop today — steady state posts nothing). All
1529
1600
  * fields are optional and only persisted when changed:
1530
1601
  * - `daemonVersion` — the running bundle/launcher version.
1531
1602
  * - `selfUpdateCapable` — whether the daemon can act on a machine.update
1532
1603
  * signal (true under a service manager, false for bare `npx` foreground).
1604
+ * - `detectedRuntimes` — runtime CLIs found on the host. Omitted = no
1605
+ * report this beat (server keeps the stored value); [] = detection ran
1606
+ * and found nothing (server clears to empty).
1533
1607
  */
1534
1608
  async postMachineHeartbeat(opts?: {
1535
1609
  daemonVersion?: string;
1536
1610
  selfUpdateCapable?: boolean;
1611
+ detectedRuntimes?: DetectedRuntime[];
1537
1612
  }): Promise<void> {
1538
1613
  const body: Record<string, unknown> = {};
1539
1614
  if (opts?.daemonVersion) body.daemon_version = opts.daemonVersion;
1540
1615
  if (opts?.selfUpdateCapable !== undefined) body.self_update_capable = opts.selfUpdateCapable;
1616
+ if (opts?.detectedRuntimes !== undefined) body.detected_runtimes = opts.detectedRuntimes;
1541
1617
  return this.request(
1542
1618
  'POST',
1543
1619
  ENDPOINTS.MACHINES_ME_HEALTH,
@@ -1785,6 +1861,41 @@ export class ParallClient {
1785
1861
  return this.request('POST', ENDPOINTS.DISPATCH_EXPIRE(orgId), undefined, params);
1786
1862
  }
1787
1863
 
1864
+ /**
1865
+ * Claim a dispatch lane (explicit consume endpoint). Occupies the
1866
+ * (agent, target, thread) lane and folds claimable WorkItems into it;
1867
+ * `claimed: false` means a healthy incumbent holds the lane.
1868
+ */
1869
+ async claimDispatch(orgId: string, req: ClaimDispatchRequest): Promise<ClaimDispatchResponse> {
1870
+ return this.request('POST', ENDPOINTS.DISPATCH_CLAIM(orgId), req);
1871
+ }
1872
+
1873
+ /** Fold a pending same-target WorkItem into a live lane (409 STALE_LANE when dethroned). */
1874
+ async steerDispatch(orgId: string, req: SteerDispatchRequest): Promise<SteerDispatchResponse> {
1875
+ return this.request('POST', ENDPOINTS.DISPATCH_STEER(orgId), req);
1876
+ }
1877
+
1878
+ /** End a turn: no_action sweep of the lane's members + lane release + re-drive check. */
1879
+ async completeDispatch(
1880
+ orgId: string,
1881
+ req: CompleteDispatchRequest,
1882
+ ): Promise<CompleteDispatchResult> {
1883
+ return this.request('POST', ENDPOINTS.DISPATCH_COMPLETE(orgId), req);
1884
+ }
1885
+
1886
+ /** Release a lane on graceful shutdown — members return to pending immediately. */
1887
+ async releaseDispatchLane(orgId: string, lane: string): Promise<void> {
1888
+ return this.request('POST', ENDPOINTS.DISPATCH_RELEASE(orgId), { lane });
1889
+ }
1890
+
1891
+ /** Renew a live lane's lease (long-turn keepalive). 409 STALE_LANE when dethroned. */
1892
+ async heartbeatDispatchLane(
1893
+ orgId: string,
1894
+ req: { lane: string; target_uri: string; thread_root_id?: string },
1895
+ ): Promise<{ lease_until?: string }> {
1896
+ return this.request('POST', ENDPOINTS.DISPATCH_HEARTBEAT(orgId), req);
1897
+ }
1898
+
1788
1899
  async getDispatchByMessages(
1789
1900
  orgId: string,
1790
1901
  chatId: string,
@@ -2165,6 +2276,124 @@ export class ParallClient {
2165
2276
  return this.request('DELETE', ENDPOINTS.EXTERNAL_CONNECTION(orgId, connectionId));
2166
2277
  }
2167
2278
 
2279
+ // ---- External IM channel (org-scoped) ----
2280
+
2281
+ async createChannelConnection(
2282
+ orgId: string,
2283
+ input: CreateChannelConnectionInput,
2284
+ ): Promise<ChannelConnection> {
2285
+ return this.request('POST', ENDPOINTS.CHANNEL_CONNECTIONS(orgId), input);
2286
+ }
2287
+
2288
+ async listChannelConnections(orgId: string): Promise<{ connections: ChannelConnection[] }> {
2289
+ return this.request('GET', ENDPOINTS.CHANNEL_CONNECTIONS(orgId));
2290
+ }
2291
+
2292
+ async getChannelConnection(orgId: string, connectionId: string): Promise<ChannelConnection> {
2293
+ return this.request('GET', ENDPOINTS.CHANNEL_CONNECTION(orgId, connectionId));
2294
+ }
2295
+
2296
+ async updateChannelConnection(
2297
+ orgId: string,
2298
+ connectionId: string,
2299
+ patch: UpdateChannelConnectionInput,
2300
+ ): Promise<ChannelConnection> {
2301
+ return this.request('PATCH', ENDPOINTS.CHANNEL_CONNECTION(orgId, connectionId), patch);
2302
+ }
2303
+
2304
+ async archiveChannelConnection(orgId: string, connectionId: string): Promise<ChannelConnection> {
2305
+ return this.request('DELETE', ENDPOINTS.CHANNEL_CONNECTION(orgId, connectionId));
2306
+ }
2307
+
2308
+ async deliverChannelCredentials(
2309
+ orgId: string,
2310
+ connectionId: string,
2311
+ credentials: ChannelCredentialsInput,
2312
+ ): Promise<ChannelConnection> {
2313
+ return this.request(
2314
+ 'POST',
2315
+ ENDPOINTS.CHANNEL_CONNECTION_CREDENTIALS(orgId, connectionId),
2316
+ credentials,
2317
+ );
2318
+ }
2319
+
2320
+ async regenerateChannelIngressToken(
2321
+ orgId: string,
2322
+ connectionId: string,
2323
+ ): Promise<ChannelConnection> {
2324
+ return this.request(
2325
+ 'POST',
2326
+ ENDPOINTS.CHANNEL_CONNECTION_INGRESS_TOKEN_REGENERATE(orgId, connectionId),
2327
+ );
2328
+ }
2329
+
2330
+ /** Start a Feishu one-click provisioning session (device flow QR). */
2331
+ async initiateChannelProvisioning(
2332
+ orgId: string,
2333
+ input: InitiateChannelProvisioningInput,
2334
+ ): Promise<ChannelProvisioningSession> {
2335
+ return this.request('POST', ENDPOINTS.CHANNEL_PROVISIONING(orgId), input);
2336
+ }
2337
+
2338
+ /**
2339
+ * Lazy status poll — server-side this may forward one provider poll, so
2340
+ * call it at the session's `poll_interval_seconds` cadence, not faster.
2341
+ */
2342
+ async getChannelProvisioningSession(
2343
+ orgId: string,
2344
+ sessionId: string,
2345
+ ): Promise<ChannelProvisioningSession> {
2346
+ return this.request('GET', ENDPOINTS.CHANNEL_PROVISIONING_SESSION(orgId, sessionId));
2347
+ }
2348
+
2349
+ async cancelChannelProvisioning(
2350
+ orgId: string,
2351
+ sessionId: string,
2352
+ ): Promise<ChannelProvisioningSession> {
2353
+ return this.request('POST', ENDPOINTS.CHANNEL_PROVISIONING_CANCEL(orgId, sessionId));
2354
+ }
2355
+
2356
+ async listChannelConversations(
2357
+ orgId: string,
2358
+ connectionId: string,
2359
+ ): Promise<{ conversations: ChannelConversation[] }> {
2360
+ return this.request('GET', ENDPOINTS.CHANNEL_CONNECTION_CONVERSATIONS(orgId, connectionId));
2361
+ }
2362
+
2363
+ async getChannelConversation(
2364
+ orgId: string,
2365
+ conversationId: string,
2366
+ ): Promise<ChannelConversation> {
2367
+ return this.request('GET', ENDPOINTS.CHANNEL_CONVERSATION(orgId, conversationId));
2368
+ }
2369
+
2370
+ async listChannelConversationMessages(
2371
+ orgId: string,
2372
+ conversationId: string,
2373
+ limit?: number,
2374
+ ): Promise<{ messages: ChannelMessage[] }> {
2375
+ return this.request(
2376
+ 'GET',
2377
+ ENDPOINTS.CHANNEL_CONVERSATION_MESSAGES(orgId, conversationId),
2378
+ undefined,
2379
+ { limit },
2380
+ );
2381
+ }
2382
+
2383
+ async setChannelConversationSession(
2384
+ orgId: string,
2385
+ conversationId: string,
2386
+ agentSessionId: string,
2387
+ ): Promise<ChannelConversation> {
2388
+ return this.request('PATCH', ENDPOINTS.CHANNEL_CONVERSATION_SESSION(orgId, conversationId), {
2389
+ agent_session_id: agentSessionId,
2390
+ });
2391
+ }
2392
+
2393
+ async getChannelMessage(orgId: string, messageId: string): Promise<ChannelMessage> {
2394
+ return this.request('GET', ENDPOINTS.CHANNEL_MESSAGE(orgId, messageId));
2395
+ }
2396
+
2168
2397
  async getExternalTriggerSchema(
2169
2398
  orgId: string,
2170
2399
  connectionId: string,
@@ -2254,6 +2483,26 @@ export class ParallClient {
2254
2483
  return this.request('GET', ENDPOINTS.WIKI(orgId, wikiId));
2255
2484
  }
2256
2485
 
2486
+ /** Soft-delete an entire wiki (org owner only; the default wiki is
2487
+ * protected → 403 WIKI_PROTECTED). Returns the deleted wiki. Recoverable
2488
+ * via restoreWiki within the retention window. */
2489
+ async deleteWiki(orgId: string, wikiId: string): Promise<Wiki> {
2490
+ return this.request('DELETE', ENDPOINTS.WIKI(orgId, wikiId));
2491
+ }
2492
+
2493
+ /** Restore a soft-deleted wiki (org owner only). 409 WIKI_PURGE_STARTED once
2494
+ * purge has been claimed (no longer restorable), or 404 once the row is gone.
2495
+ * Returns the restored wiki. */
2496
+ async restoreWiki(orgId: string, wikiId: string): Promise<Wiki> {
2497
+ return this.request('POST', ENDPOINTS.WIKI_RESTORE(orgId, wikiId));
2498
+ }
2499
+
2500
+ /** Recycle bin — list soft-deleted wikis for the org (owner only). */
2501
+ async getDeletedWikis(orgId: string): Promise<Wiki[]> {
2502
+ const res = await this.request<{ data: Wiki[] }>('GET', ENDPOINTS.WIKIS_DELETED(orgId));
2503
+ return res.data;
2504
+ }
2505
+
2257
2506
  async getWikiTree(
2258
2507
  orgId: string,
2259
2508
  wikiId: string,
@@ -2301,6 +2550,20 @@ export class ParallClient {
2301
2550
  return blob;
2302
2551
  }
2303
2552
 
2553
+ /**
2554
+ * Copy one wiki file to another path as an independent snapshot (no sync).
2555
+ * Used to "share a private file out": copy a file from the caller's personal
2556
+ * namespace (`users/{userId}/…`) into the public wiki, leaving the original
2557
+ * untouched. 409 `FILE_EXISTS` if `dest_path` already exists.
2558
+ */
2559
+ async copyWikiFile(
2560
+ orgId: string,
2561
+ wikiId: string,
2562
+ req: CopyWikiFileRequest,
2563
+ ): Promise<CopyWikiFileResponse> {
2564
+ return this.request('POST', ENDPOINTS.WIKI_COPY(orgId, wikiId), req);
2565
+ }
2566
+
2304
2567
  async getWikiNodeSections(
2305
2568
  orgId: string,
2306
2569
  wikiId: string,
@@ -2828,6 +3091,21 @@ export class ParallClient {
2828
3091
  return resp.data;
2829
3092
  }
2830
3093
 
3094
+ /** Runtime capability table (public) — SSOT for the create/settings interlock. */
3095
+ async getRuntimes(): Promise<RuntimeCapability[]> {
3096
+ const resp = await this.request<{ data: RuntimeCapability[] }>('GET', ENDPOINTS.RUNTIMES());
3097
+ return resp.data;
3098
+ }
3099
+
3100
+ /**
3101
+ * Platform model catalog (public) — DB-backed, replaces the compile-time
3102
+ * PLATFORM_MODELS constant. Pass includeModel to keep an agent's pinned
3103
+ * hidden legacy model representable (settings picker).
3104
+ */
3105
+ async getModels(includeModel?: string): Promise<PlatformModelsResponse> {
3106
+ return this.request<PlatformModelsResponse>('GET', ENDPOINTS.MODELS(includeModel));
3107
+ }
3108
+
2831
3109
  // ---- Clips ----
2832
3110
 
2833
3111
  async listClips(orgId: string): Promise<Clip[]> {
@@ -2895,6 +3173,12 @@ export class ParallClient {
2895
3173
  return resp.data;
2896
3174
  }
2897
3175
 
3176
+ /** Deployment-wide hosted browser runtime availability — drives whether the
3177
+ * create UI offers the platform-hosted placement. Fail-closed server-side. */
3178
+ async getBrowserRuntimeStatus(orgId: string): Promise<BrowserRuntimeStatus> {
3179
+ return this.request('GET', ENDPOINTS.BROWSER_RUNTIME_STATUS(orgId));
3180
+ }
3181
+
2898
3182
  /** Org-wide browser-profile discovery list. Returns the sanitized
2899
3183
  * {@link BrowserProfileListItem} shape (not the full domain model), each row
2900
3184
  * carrying a per-viewer `can_open` control hint. */
package/src/constants.ts CHANGED
@@ -7,11 +7,17 @@ export const WIKI_BASE = '/wiki/v1';
7
7
  // Clip-service base path (served by clip-service, routed via LB path rules)
8
8
  export const CLIP_BASE = '/clip/v1';
9
9
 
10
- // LLM model catalog — mirrors server/pkg/llmproxy/models.go DefaultModels.
11
- // SSOT is the Go side; keep this list in sync when models change. `provider`
12
- // is used by clients to filter runtime-specific pickers. `runtime_names` is
13
- // included for catalog completeness and agent runtime delivery; product UI
14
- // should continue to display canonical model IDs/names.
10
+ // LLM model catalog — frozen compile-time mirror of the catalog at the time
11
+ // the DB-backed source shipped (docs/engineering-design/model-catalog-design.md).
12
+ // Kept ONLY as the offline/degraded fallback for pickers; it is NOT updated
13
+ // when models are added through the admin catalog.
14
+ /**
15
+ * @deprecated The catalog is DB-backed — fetch it via `client.getModels()`
16
+ * (GET /api/v1/models). This constant is a frozen degraded-mode fallback and
17
+ * will be removed after one release cycle. Still structurally consumed by
18
+ * FALLBACK_CATALOG (`@parall/app` agent-interlock) — do not delete before
19
+ * that fallback strategy changes (PR-3).
20
+ */
15
21
  export const PLATFORM_MODELS = [
16
22
  // Fable 5 rejects an explicit thinking "disabled" param (400) — bridges must
17
23
  // omit the thinking config entirely instead of disabling it.
@@ -211,6 +217,12 @@ export const PLATFORM_MODELS = [
211
217
 
212
218
  // Hidden from new-agent pickers, but available to editors so persisted Beta
213
219
  // agent defaults remain representable.
220
+ /**
221
+ * @deprecated Fetch via `client.getModels(currentModelId)` — the include_model
222
+ * parameter serves the legacy-pin case. Frozen degraded-mode fallback only.
223
+ * Still structurally consumed by FALLBACK_CATALOG (`@parall/app`
224
+ * agent-interlock) — do not delete before that fallback changes (PR-3).
225
+ */
214
226
  export const PLATFORM_LEGACY_MODELS = [
215
227
  {
216
228
  id: 'openai/gpt-5.4-mini',
@@ -284,9 +296,13 @@ export const PLATFORM_LEGACY_MODELS = [
284
296
  },
285
297
  ] as const;
286
298
 
287
- // Mirrors server/pkg/llmproxy/models.go DefaultModelIDForRuntime. Go remains
288
- // the SSOT; keep this client-side fallback in sync until the catalog is codegen
289
- // or fetched before render.
299
+ // Frozen mirror of the per-runtime defaults at ship time of the DB-backed
300
+ // catalog (llm_runtime_defaults). Live values arrive as recommended_model on
301
+ // GET /runtimes.
302
+ /**
303
+ * @deprecated Read `recommended_model` from `client.getRuntimes()` instead.
304
+ * Frozen degraded-mode fallback only.
305
+ */
290
306
  export const PLATFORM_DEFAULT_MODEL_BY_RUNTIME = {
291
307
  openclaw: 'anthropic/claude-sonnet-4.6',
292
308
  'claude-code': 'anthropic/claude-sonnet-4.6',
@@ -312,6 +328,11 @@ export function platformDefaultModelForRuntime(runtime: string | null | undefine
312
328
  case '':
313
329
  case 'openclaw':
314
330
  case 'claude-code':
331
+ // hermes / parel route through the Parall proxy and accept any model; the
332
+ // server (DefaultModelIDForRuntime) recommends the platform default, so
333
+ // mirror it.
334
+ case 'hermes':
335
+ case 'parel':
315
336
  return PLATFORM_DEFAULT_MODEL_BY_RUNTIME.openclaw;
316
337
  case 'codex':
317
338
  return PLATFORM_DEFAULT_MODEL_BY_RUNTIME.codex;
@@ -347,6 +368,7 @@ export const ENDPOINTS = {
347
368
  // Org-scoped
348
369
  ORG: (orgId: string) => `${API_BASE}/orgs/${orgId}`,
349
370
  ORG_MEMBERS: (orgId: string) => `${API_BASE}/orgs/${orgId}/members`,
371
+ ORG_MEMBERS_FORMER: (orgId: string) => `${API_BASE}/orgs/${orgId}/members/former`,
350
372
  TEAMS: (orgId: string) => `${API_BASE}/orgs/${orgId}/teams`,
351
373
  ORG_MEMBERS_ONLINE: (orgId: string) => `${API_BASE}/orgs/${orgId}/members/online`,
352
374
  ORG_MEMBER: (orgId: string, userId: string) => `${API_BASE}/orgs/${orgId}/members/${userId}`,
@@ -584,6 +606,30 @@ export const ENDPOINTS = {
584
606
  EXTERNAL_TRIGGER_RUN: (orgId: string, runId: string) =>
585
607
  `${API_BASE}/orgs/${orgId}/external-trigger-runs/${runId}`,
586
608
 
609
+ // External IM channel (org-scoped, platform-mediated Feishu/Slack)
610
+ CHANNEL_CONNECTIONS: (orgId: string) => `${API_BASE}/orgs/${orgId}/channel-connections`,
611
+ CHANNEL_CONNECTION: (orgId: string, connectionId: string) =>
612
+ `${API_BASE}/orgs/${orgId}/channel-connections/${connectionId}`,
613
+ CHANNEL_CONNECTION_CREDENTIALS: (orgId: string, connectionId: string) =>
614
+ `${API_BASE}/orgs/${orgId}/channel-connections/${connectionId}/credentials`,
615
+ CHANNEL_CONNECTION_INGRESS_TOKEN_REGENERATE: (orgId: string, connectionId: string) =>
616
+ `${API_BASE}/orgs/${orgId}/channel-connections/${connectionId}/ingress-token/regenerate`,
617
+ CHANNEL_CONNECTION_CONVERSATIONS: (orgId: string, connectionId: string) =>
618
+ `${API_BASE}/orgs/${orgId}/channel-connections/${connectionId}/conversations`,
619
+ CHANNEL_CONVERSATION: (orgId: string, conversationId: string) =>
620
+ `${API_BASE}/orgs/${orgId}/channel-conversations/${conversationId}`,
621
+ CHANNEL_CONVERSATION_MESSAGES: (orgId: string, conversationId: string) =>
622
+ `${API_BASE}/orgs/${orgId}/channel-conversations/${conversationId}/messages`,
623
+ CHANNEL_CONVERSATION_SESSION: (orgId: string, conversationId: string) =>
624
+ `${API_BASE}/orgs/${orgId}/channel-conversations/${conversationId}/session`,
625
+ CHANNEL_MESSAGE: (orgId: string, messageId: string) =>
626
+ `${API_BASE}/orgs/${orgId}/channel-messages/${messageId}`,
627
+ CHANNEL_PROVISIONING: (orgId: string) => `${API_BASE}/orgs/${orgId}/channel-provisioning`,
628
+ CHANNEL_PROVISIONING_SESSION: (orgId: string, sessionId: string) =>
629
+ `${API_BASE}/orgs/${orgId}/channel-provisioning/${sessionId}`,
630
+ CHANNEL_PROVISIONING_CANCEL: (orgId: string, sessionId: string) =>
631
+ `${API_BASE}/orgs/${orgId}/channel-provisioning/${sessionId}/cancel`,
632
+
587
633
  // Invitations (org-scoped, admin)
588
634
  ORG_INVITATIONS: (orgId: string) => `${API_BASE}/orgs/${orgId}/invitations`,
589
635
  ORG_INVITATION: (orgId: string, invId: string) =>
@@ -608,9 +654,17 @@ export const ENDPOINTS = {
608
654
 
609
655
  // Wikis (org-scoped, served by wiki-service)
610
656
  WIKIS: (orgId: string) => `${WIKI_BASE}/orgs/${orgId}/wikis`,
657
+ // Recycle bin — soft-deleted wikis (owner only). Must precede WIKI in the
658
+ // backend router so `deleted` isn't captured as a {wikiId}; the SDK builder
659
+ // is just a string.
660
+ WIKIS_DELETED: (orgId: string) => `${WIKI_BASE}/orgs/${orgId}/wikis/deleted`,
661
+ // Detail URL — also reused for DELETE (soft-delete) and `${WIKI}/restore`.
611
662
  WIKI: (orgId: string, wikiId: string) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}`,
663
+ WIKI_RESTORE: (orgId: string, wikiId: string) =>
664
+ `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/restore`,
612
665
  WIKI_TREE: (orgId: string, wikiId: string) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/tree`,
613
666
  WIKI_BLOB: (orgId: string, wikiId: string) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/blob`,
667
+ WIKI_COPY: (orgId: string, wikiId: string) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/copy`,
614
668
  WIKI_NODE_SECTIONS: (orgId: string, wikiId: string) =>
615
669
  `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/node-sections`,
616
670
  WIKI_SEARCH: (orgId: string, wikiId: string) =>
@@ -695,6 +749,11 @@ export const ENDPOINTS = {
695
749
  DISPATCH_ACK_BY_ID: (orgId: string, id: string) => `${API_BASE}/orgs/${orgId}/dispatch/${id}/ack`,
696
750
  DISPATCH_EXPIRE: (orgId: string) => `${API_BASE}/orgs/${orgId}/dispatch/expire`,
697
751
  DISPATCH_BY_MESSAGES: (orgId: string) => `${API_BASE}/orgs/${orgId}/dispatch/by-messages`,
752
+ DISPATCH_CLAIM: (orgId: string) => `${API_BASE}/orgs/${orgId}/dispatch/claim`,
753
+ DISPATCH_STEER: (orgId: string) => `${API_BASE}/orgs/${orgId}/dispatch/steer`,
754
+ DISPATCH_COMPLETE: (orgId: string) => `${API_BASE}/orgs/${orgId}/dispatch/complete`,
755
+ DISPATCH_RELEASE: (orgId: string) => `${API_BASE}/orgs/${orgId}/dispatch/release`,
756
+ DISPATCH_HEARTBEAT: (orgId: string) => `${API_BASE}/orgs/${orgId}/dispatch/heartbeat`,
698
757
 
699
758
  // Unread
700
759
  UNREAD: `${API_BASE}/me/unread`,
@@ -734,6 +793,19 @@ export const ENDPOINTS = {
734
793
  BILLING_SETUP_INTENT: (orgId: string) => `${API_BASE}/orgs/${orgId}/billing/setup-intent`,
735
794
  COMPUTE_PRICING: () => `${API_BASE}/billing/compute-pricing`,
736
795
 
796
+ // Runtime capability table (public, no auth) — SSOT for the create/settings
797
+ // interlock: per-runtime compute modes, native model family, cross-family
798
+ // availability, and recommended model.
799
+ RUNTIMES: () => `${API_BASE}/runtimes`,
800
+
801
+ // Model catalog (public, no auth) — served from the server's DB-backed
802
+ // snapshot. Optional include_model keeps an agent's pinned hidden legacy
803
+ // model representable in settings pickers.
804
+ MODELS: (includeModel?: string) =>
805
+ includeModel
806
+ ? `${API_BASE}/models?include_model=${encodeURIComponent(includeModel)}`
807
+ : `${API_BASE}/models`,
808
+
737
809
  // ADMIN_GRANTS intentionally NOT exported here — it sits under the
738
810
  // unauthenticated `/internal/admin/*` surface and must not bleed into the
739
811
  // public SDK. Admin dashboard hits the URL directly from its own client.
@@ -750,6 +822,7 @@ export const ENDPOINTS = {
750
822
  `${CLIP_BASE}/orgs/${orgId}/agents/${agentId}/clips/${clipId}`,
751
823
  CLIP_INVOKE: (orgId: string) => `${CLIP_BASE}/orgs/${orgId}/invoke`,
752
824
  CLIP_ONLINE: (orgId: string) => `${CLIP_BASE}/orgs/${orgId}/online`,
825
+ BROWSER_RUNTIME_STATUS: (orgId: string) => `${CLIP_BASE}/orgs/${orgId}/browser-runtime/status`,
753
826
  BROWSER_PROFILES: (orgId: string) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles`,
754
827
  BROWSER_PROFILE: (orgId: string, profileId: string) =>
755
828
  `${CLIP_BASE}/orgs/${orgId}/browser-profiles/${profileId}`,
@@ -772,6 +845,42 @@ export const ENDPOINTS = {
772
845
  CLIP_REGISTRY: () => `${CLIP_BASE}/registry/clips`,
773
846
  } as const;
774
847
 
848
+ /**
849
+ * Wiki personal-namespace prefix — the path subtree a member owns for
850
+ * self-service private spaces: `users/{userId}/`. SSOT mirror of the
851
+ * wiki-service `personalNamespacePrefix` (server/internal/wiki/handler/acl.go):
852
+ * a member may create/manage path restrictions anywhere under this prefix
853
+ * without org/path admin. The trailing slash is load-bearing — it keeps a user
854
+ * ID that is a string prefix of another from matching across namespaces.
855
+ */
856
+ export function wikiPersonalNamespacePrefix(userId: string): string {
857
+ return `users/${userId}/`;
858
+ }
859
+
860
+ /**
861
+ * Whether `path` falls inside `userId`'s personal namespace (the namespace root
862
+ * itself or any descendant). Mirrors the server's `ownsPersonalNamespacePath`;
863
+ * use it for UI affordance gating only — the wiki-service stays the security
864
+ * SSOT and re-checks on every mutation.
865
+ */
866
+ export function isInWikiPersonalNamespace(userId: string, path: string): boolean {
867
+ if (!userId) return false;
868
+ const prefix = wikiPersonalNamespacePrefix(userId);
869
+ return path === prefix || path.startsWith(prefix);
870
+ }
871
+
872
+ /**
873
+ * Whether `path` falls inside ANY user's personal namespace (`users/{id}/…`),
874
+ * regardless of whose. UI affordance helper — e.g. badging member personal-space
875
+ * restriction rows in the admin Access tab, or keeping "copy to shared wiki"
876
+ * destinations out of personal namespaces. Matches by shape (`users/<segment>/`
877
+ * or the bare `users/<segment>`), deliberately broader than
878
+ * `isInWikiPersonalNamespace`.
879
+ */
880
+ export function isWikiPersonalNamespacePath(path: string): boolean {
881
+ return /^users\/[^/]+(\/|$)/.test(path);
882
+ }
883
+
775
884
  // WebSocket event types
776
885
  export const WS_EVENTS = {
777
886
  // Client -> Server
@@ -813,10 +922,13 @@ export const WS_EVENTS = {
813
922
  INVITATION_REVOKED: 'invitation.revoked',
814
923
  ORG_JOIN_REQUEST_NEW: 'org.join_request.new',
815
924
  ORG_INVITE_LINK_JOINED: 'org.invite_link.joined',
925
+ ORG_MEMBER_REMOVED: 'org.member.removed',
816
926
  AGENT_CONFIG_UPDATE: 'agent_config.update',
817
927
  PRESENCE_UPDATE: 'presence.update',
818
928
  WIKI_CHANGESET_CREATED: 'wiki.changeset.created',
819
929
  WIKI_CHANGESET_UPDATED: 'wiki.changeset.updated',
930
+ WIKI_DELETED: 'wiki.deleted',
931
+ WIKI_RESTORED: 'wiki.restored',
820
932
  COMMENT_CREATED: 'comment.created',
821
933
  COMMENT_UPDATED: 'comment.updated',
822
934
  COMMENT_DELETED: 'comment.deleted',
@@ -827,6 +939,7 @@ export const WS_EVENTS = {
827
939
  READ_POSITION_UPDATED: 'read_position.updated',
828
940
  DISPATCH_NEW: 'dispatch.new',
829
941
  DISPATCH_RECEIVED: 'dispatch.received',
942
+ DISPATCH_RESOLVED: 'dispatch.resolved',
830
943
  SCHEDULE_CREATED: 'schedule.created',
831
944
  SCHEDULE_UPDATED: 'schedule.updated',
832
945
  SCHEDULE_DELETED: 'schedule.deleted',
@@ -844,6 +957,7 @@ export const WS_EVENTS = {
844
957
  MACHINE_FILESYSTEM_BROWSE: 'machine.filesystem.browse',
845
958
  MACHINE_UPDATE: 'machine.update',
846
959
  MACHINE_CONFIG_UPDATED: 'machine.config.updated',
960
+ MACHINE_AGENT_CONFIG_UPDATED: 'machine.agent_config.updated',
847
961
  MACHINE_CLIP_SYNC: 'machine.clip.sync',
848
962
  MACHINE_BROWSER_PROFILE_LIFECYCLE: 'machine.browser_profile.lifecycle',
849
963
  MACHINE_BROWSER_PROFILE_VIEWER: 'machine.browser_profile.viewer',