@parall/sdk 1.46.0 → 1.48.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
@@ -95,6 +95,13 @@ import type {
95
95
  CreateChannelConnectionInput,
96
96
  InitiateChannelProvisioningInput,
97
97
  SendChannelMessageInput,
98
+ SlackChannelsPage,
99
+ SlackHistoryPage,
100
+ SlackManifestLinkResult,
101
+ SlackMembersPage,
102
+ SlackReadPageQuery,
103
+ SlackStatusInput,
104
+ SlackUsersPage,
98
105
  SentChannelMessage,
99
106
  UpdateChannelConnectionInput,
100
107
  ExternalIngressEvent,
@@ -202,6 +209,8 @@ import type {
202
209
  InvokeClipResponse,
203
210
  OnlineClipInfo,
204
211
  RegistryClipInfo,
212
+ ClipRegistryEntry,
213
+ InstallRegistryClipResponse,
205
214
  MachineClip,
206
215
  BrowserProfile,
207
216
  MachineBrowserProfile,
@@ -220,6 +229,8 @@ import type {
220
229
  EdgeBrowserProfile,
221
230
  ClipConnection,
222
231
  EdgeOnboardingStatus,
232
+ ExecEdgeClipRequest,
233
+ EdgeClipExecResult,
223
234
  } from './types.js';
224
235
 
225
236
  export interface ParallClientOptions {
@@ -1754,11 +1765,17 @@ export class ParallClient {
1754
1765
  return this.request('PATCH', ENDPOINTS.MACHINE_SPEC(orgId, machineId), spec);
1755
1766
  }
1756
1767
 
1757
- /** Signal a local daemon-mode Machine to check for and apply an update. */
1768
+ /** @deprecated Retired server-side (daemon-control-authorization §4.2):
1769
+ * daemons update autonomously (CDN poll + platform release signal). The
1770
+ * endpoint now answers 409 LOCAL_UPDATE_NOT_SUPPORTED unconditionally. */
1758
1771
  async requestMachineUpdate(orgId: string, machineId: string, mandatory = false): Promise<void> {
1759
1772
  await this.request('POST', ENDPOINTS.MACHINE_REQUEST_UPDATE(orgId, machineId), { mandatory });
1760
1773
  }
1761
1774
 
1775
+ /** @deprecated Retired server-side (daemon-control-authorization §4.2):
1776
+ * remote filesystem browse of a member's machine was remote device access.
1777
+ * The endpoint now answers 409 LOCAL_BROWSE_NOT_SUPPORTED unconditionally;
1778
+ * workspace paths are typed in (or picked on the machine's own Desktop). */
1762
1779
  async browseMachineFilesystem(
1763
1780
  orgId: string,
1764
1781
  machineId: string,
@@ -1814,6 +1831,20 @@ export class ParallClient {
1814
1831
  return this.request('GET', ENDPOINTS.THREAD_UNREAD(orgId, chatId, threadRootId));
1815
1832
  }
1816
1833
 
1834
+ /** Advance the per-thread read cursor (forward-only). The server auto-clears
1835
+ * thread-scoped inbox items (thread_reply + in-thread mentions) the cursor
1836
+ * now covers. */
1837
+ async markThreadRead(
1838
+ orgId: string,
1839
+ chatId: string,
1840
+ threadRootId: string,
1841
+ messageId: string,
1842
+ ): Promise<void> {
1843
+ return this.request('POST', ENDPOINTS.THREAD_READ(orgId, chatId, threadRootId), {
1844
+ message_id: messageId,
1845
+ });
1846
+ }
1847
+
1817
1848
  // ---- Inbox ----
1818
1849
 
1819
1850
  async getInbox(
@@ -1854,9 +1885,15 @@ export class ParallClient {
1854
1885
  return this.request('POST', ENDPOINTS.INBOX_ARCHIVE_ALL(orgId));
1855
1886
  }
1856
1887
 
1857
- /** Mark an inbox item as read by its source (source_type + source_id) rather than inbox item ID. */
1858
- async ackInbox(orgId: string, source: { source_type: string; source_id: string }): Promise<void> {
1859
- return this.request('POST', ENDPOINTS.INBOX_ACK(orgId), source);
1888
+ /** Mark inbox items as read by their source (source_type + source_id) or by
1889
+ * group_key, rather than by inbox item ID. The group_key form clears a whole
1890
+ * group at once (e.g. `task:{taskId}` — task_assign/task_update/task_comment
1891
+ * share it), used by the task detail view's auto-ack on open. */
1892
+ async ackInbox(
1893
+ orgId: string,
1894
+ target: { source_type: string; source_id: string } | { group_key: string },
1895
+ ): Promise<void> {
1896
+ return this.request('POST', ENDPOINTS.INBOX_ACK(orgId), target);
1860
1897
  }
1861
1898
 
1862
1899
  async deleteInboxItem(orgId: string, id: string): Promise<void> {
@@ -2420,6 +2457,19 @@ export class ParallClient {
2420
2457
  return this.request('POST', ENDPOINTS.CHANNEL_PROVISIONING(orgId), input);
2421
2458
  }
2422
2459
 
2460
+ /**
2461
+ * Mint a pending slack connection + api.slack.com manifest-prefill link
2462
+ * (guided manual path). Activate the returned connection_id with
2463
+ * deliverChannelCredentials once the user brings back the bot token +
2464
+ * signing secret.
2465
+ */
2466
+ async createSlackManifestLink(
2467
+ orgId: string,
2468
+ input: { agent_id: string },
2469
+ ): Promise<SlackManifestLinkResult> {
2470
+ return this.request('POST', ENDPOINTS.CHANNEL_SLACK_MANIFEST_LINK(orgId), input);
2471
+ }
2472
+
2423
2473
  /**
2424
2474
  * Lazy status poll — server-side this may forward one provider poll, so
2425
2475
  * call it at the session's `poll_interval_seconds` cadence, not faster.
@@ -2450,6 +2500,55 @@ export class ParallClient {
2450
2500
  return this.request('POST', ENDPOINTS.CHANNEL_SEND(orgId), input);
2451
2501
  }
2452
2502
 
2503
+ private slackReadQuery(base: string, query?: SlackReadPageQuery, extra?: Record<string, string>) {
2504
+ const params = new URLSearchParams();
2505
+ if (query?.cursor) params.set('cursor', query.cursor);
2506
+ if (query?.limit) params.set('limit', String(query.limit));
2507
+ for (const [k, v] of Object.entries(extra ?? {})) params.set(k, v);
2508
+ const qs = params.toString();
2509
+ return qs ? `${base}?${qs}` : base;
2510
+ }
2511
+
2512
+ /**
2513
+ * Tier-B read verbs (agent-only): workspace visibility as the bot sees
2514
+ * it. Same live gate as the send verb; authorization beyond it is the
2515
+ * bot's own Slack permissions.
2516
+ */
2517
+ async listSlackChannels(orgId: string, query?: SlackReadPageQuery): Promise<SlackChannelsPage> {
2518
+ return this.request('GET', this.slackReadQuery(ENDPOINTS.SLACK_CHANNELS(orgId), query));
2519
+ }
2520
+
2521
+ async listSlackUsers(orgId: string, query?: SlackReadPageQuery): Promise<SlackUsersPage> {
2522
+ return this.request('GET', this.slackReadQuery(ENDPOINTS.SLACK_USERS(orgId), query));
2523
+ }
2524
+
2525
+ async slackHistory(
2526
+ orgId: string,
2527
+ conversationId: string,
2528
+ query?: SlackReadPageQuery,
2529
+ ): Promise<SlackHistoryPage> {
2530
+ return this.request(
2531
+ 'GET',
2532
+ this.slackReadQuery(ENDPOINTS.SLACK_HISTORY(orgId), query, { conversation: conversationId }),
2533
+ );
2534
+ }
2535
+
2536
+ async slackMembers(
2537
+ orgId: string,
2538
+ conversationId: string,
2539
+ query?: SlackReadPageQuery,
2540
+ ): Promise<SlackMembersPage> {
2541
+ return this.request(
2542
+ 'GET',
2543
+ this.slackReadQuery(ENDPOINTS.SLACK_MEMBERS(orgId), query, { conversation: conversationId }),
2544
+ );
2545
+ }
2546
+
2547
+ /** Set/clear the Agents-pane "typing…" indicator (best-effort cosmetic). */
2548
+ async setSlackStatus(orgId: string, input: SlackStatusInput): Promise<void> {
2549
+ await this.request('POST', ENDPOINTS.SLACK_STATUS(orgId), input);
2550
+ }
2551
+
2453
2552
  async listChannelConversations(
2454
2553
  orgId: string,
2455
2554
  connectionId: string,
@@ -3377,6 +3476,43 @@ export class ParallClient {
3377
3476
  return resp.data;
3378
3477
  }
3379
3478
 
3479
+ // ---- Clip registry (v3, api-server org registry — `crg_` entries) ----
3480
+
3481
+ /**
3482
+ * List registry clips visible to the org: its own plus public+approved
3483
+ * cross-org entries. This is the surface `installRegistryClip` and clip
3484
+ * connections operate on — NOT the Pinix catalog proxy
3485
+ * ({@link listRegistryClips}), whose entries carry no `crg_` id.
3486
+ */
3487
+ async listOrgRegistryClips(orgId: string): Promise<ClipRegistryEntry[]> {
3488
+ // Server default page is 50 (max 100); one max-size page covers today's
3489
+ // catalogs — revisit with real pagination if registries outgrow it.
3490
+ const resp = await this.request<ClipRegistryEntry[] | null>(
3491
+ 'GET',
3492
+ `${ENDPOINTS.ORG_CLIP_REGISTRY(orgId)}?limit=100`,
3493
+ );
3494
+ return resp ?? [];
3495
+ }
3496
+
3497
+ /**
3498
+ * Install a registry clip into the org (a reference in `clip_installs`, not a
3499
+ * copy). Idempotent: installing an already-installed clip returns the same
3500
+ * `200 {ok:true}`. Fails closed with `403 CLIP_NOT_APPROVED` when the clip is
3501
+ * not eligible (cross-org requires public + approved).
3502
+ */
3503
+ async installRegistryClip(orgId: string, clipId: string): Promise<InstallRegistryClipResponse> {
3504
+ return this.request('POST', ENDPOINTS.ORG_CLIP_INSTALL(orgId), { clip_id: clipId });
3505
+ }
3506
+
3507
+ /** List the org's installed registry clips (full entries). */
3508
+ async listInstalledRegistryClips(orgId: string): Promise<ClipRegistryEntry[]> {
3509
+ const resp = await this.request<ClipRegistryEntry[] | null>(
3510
+ 'GET',
3511
+ ENDPOINTS.ORG_CLIPS_INSTALLED(orgId),
3512
+ );
3513
+ return resp ?? [];
3514
+ }
3515
+
3380
3516
  // ---- Edge devices ----
3381
3517
 
3382
3518
  async listEdgeDevices(orgId: string): Promise<{ data: EdgeDevice[] }> {
@@ -3400,8 +3536,8 @@ export class ParallClient {
3400
3536
  }
3401
3537
 
3402
3538
  /**
3403
- * Delete a hosted Cloud Profile. Hosted only — a BYOC device is removed by
3404
- * uninstalling Parall Clip on that machine (`400 EDGE_PLACEMENT_UNSUPPORTED`).
3539
+ * Delete a hosted Cloud Profile. Hosted only — use {@link unregisterEdgeDevice}
3540
+ * for an offline BYOC registration (`400 EDGE_PLACEMENT_UNSUPPORTED` here).
3405
3541
  *
3406
3542
  * Idempotent and ASYNC: returns `202` with `hosted_state: 'deleting'` on the first
3407
3543
  * call and on every repeat. The device stops being usable immediately (no exec, no
@@ -3412,6 +3548,17 @@ export class ParallClient {
3412
3548
  return this.request('DELETE', ENDPOINTS.ORG_EDGE_DEVICE(orgId, edgeId));
3413
3549
  }
3414
3550
 
3551
+ /**
3552
+ * Remove the interactive human caller's own offline BYOC registration.
3553
+ *
3554
+ * Synchronous and idempotent: a committed removal and a repeat after removal
3555
+ * both resolve with no response body. A live connection returns `EDGE_ONLINE`;
3556
+ * callers must not clear local device identity until this method resolves.
3557
+ */
3558
+ async unregisterEdgeDevice(orgId: string, edgeId: string): Promise<void> {
3559
+ await this.request('DELETE', ENDPOINTS.ORG_EDGE_DEVICE_UNREGISTER(orgId, edgeId));
3560
+ }
3561
+
3415
3562
  async getEdgeOnboarding(orgId: string): Promise<EdgeOnboardingStatus> {
3416
3563
  return this.request('GET', ENDPOINTS.ORG_EDGE_ONBOARDING(orgId));
3417
3564
  }
@@ -3420,6 +3567,68 @@ export class ParallClient {
3420
3567
  return this.request('GET', ENDPOINTS.ORG_EDGE_PROFILES(orgId, edgeId));
3421
3568
  }
3422
3569
 
3570
+ /**
3571
+ * Execute a registry clip command on an Edge device.
3572
+ *
3573
+ * A hosted (Cloud Profile) device is reachable ONLY through an explicit
3574
+ * `connection` (id `ccn_…` or alias) — there is no implicit route to an
3575
+ * org-shared browser login. BYOC keeps its legacy selectors (`edge_id`, or
3576
+ * nothing for the caller's own online device).
3577
+ *
3578
+ * Returns the result envelope on completion (`success` may be false when the
3579
+ * command RAN and failed — `error`/`error_code` describe why). Everything
3580
+ * else throws a typed {@link ApiError}; match on `err.code`:
3581
+ *
3582
+ * Safe to retry (guaranteed nothing was dispatched):
3583
+ * - `EDGE_ACTIVATING` 503 + `Retry-After` — cold cloud profile is starting.
3584
+ * Bounded backoff, same `correlation_id` across the loop.
3585
+ * - `EDGE_BUSY` 409 — the device is executing another request.
3586
+ * - `EDGE_CONCURRENCY_LIMIT` 429 — org at its concurrent-session limit.
3587
+ * - `EDGE_UNAVAILABLE` 503 — session torn down / replaced mid-dispatch.
3588
+ *
3589
+ * NOT retryable:
3590
+ * - `OUTCOME_UNKNOWN` 504 — dispatched, but no result arrived. The command
3591
+ * MAY HAVE EXECUTED (posted, ordered, deleted…). Never retry
3592
+ * automatically: verify the effect first, then decide. The message carries
3593
+ * the request id for audit.
3594
+ * - `EDGE_DEADLINE_EXCEEDED` 504 — arrived late, provably NOT executed.
3595
+ * - `EDGE_HOSTED_DISABLED_FOR_ORG` 403, `EDGE_REPAIR` 503 (operator-held),
3596
+ * `EDGE_RUNTIME_UNAVAILABLE` 503 (deployment has no hosted runtime).
3597
+ * - Routing errors: `HOSTED_CONNECTION_REQUIRED`, `CONNECTION_NOT_FOUND`,
3598
+ * `CONNECTION_CLIP_MISMATCH`, `CONNECTION_TARGET_GONE`,
3599
+ * `CONNECTION_PROFILE_MISMATCH`, `EDGE_DELETING`, `DEVICE_OFFLINE`.
3600
+ */
3601
+ async execEdgeClip(orgId: string, req: ExecEdgeClipRequest): Promise<EdgeClipExecResult> {
3602
+ // The server holds the connection open for timeout + ~5s of result wait;
3603
+ // give the HTTP layer headroom past that so a slow-but-successful exec is
3604
+ // not chopped locally into a fake transport error. The effective value
3605
+ // mirrors the SERVER's rule exactly (out-of-range → its 30s default): a
3606
+ // local clamp that disagreed would abort the HTTP call while the server
3607
+ // legitimately keeps executing — manufacturing a transport error for a
3608
+ // request that may still succeed.
3609
+ const t = req.timeout;
3610
+ const serverTimeout = t !== undefined && t > 0 && t <= 120000 ? t : 30000;
3611
+ const timeoutMs = serverTimeout + 10_000;
3612
+ try {
3613
+ return await this.request('POST', ENDPOINTS.ORG_EDGE_EXEC(orgId), req, undefined, false, {
3614
+ timeoutMs,
3615
+ });
3616
+ } catch (err) {
3617
+ // A 422 is the RESULT envelope (the command ran and failed), not the
3618
+ // standard error envelope — lift its stable `error_code` into
3619
+ // ApiError.code so callers match one field for every failure.
3620
+ if (
3621
+ err instanceof ApiError &&
3622
+ err.status === 422 &&
3623
+ !err.code &&
3624
+ typeof err.extras?.error_code === 'string'
3625
+ ) {
3626
+ err.code = err.extras.error_code;
3627
+ }
3628
+ throw err;
3629
+ }
3630
+ }
3631
+
3423
3632
  // ---- Clip connections ----
3424
3633
 
3425
3634
  async listClipConnections(orgId: string, clipId: string): Promise<{ data: ClipConnection[] }> {
package/src/constants.ts CHANGED
@@ -628,12 +628,20 @@ export const ENDPOINTS = {
628
628
  CHANNEL_MESSAGE: (orgId: string, messageId: string) =>
629
629
  `${API_BASE}/orgs/${orgId}/channel-messages/${messageId}`,
630
630
  CHANNEL_PROVISIONING: (orgId: string) => `${API_BASE}/orgs/${orgId}/channel-provisioning`,
631
+ CHANNEL_SLACK_MANIFEST_LINK: (orgId: string) =>
632
+ `${API_BASE}/orgs/${orgId}/channel-provisioning/slack/manifest-link`,
631
633
  CHANNEL_PROVISIONING_SESSION: (orgId: string, sessionId: string) =>
632
634
  `${API_BASE}/orgs/${orgId}/channel-provisioning/${sessionId}`,
633
635
  CHANNEL_PROVISIONING_CANCEL: (orgId: string, sessionId: string) =>
634
636
  `${API_BASE}/orgs/${orgId}/channel-provisioning/${sessionId}/cancel`,
635
637
  // Tier-B platform verb (agent-only): send one message as the bound bot.
636
638
  CHANNEL_SEND: (orgId: string) => `${API_BASE}/orgs/${orgId}/agents/me/channel-send`,
639
+ // Tier-B read verbs (agent-only): workspace visibility as the bot sees it.
640
+ SLACK_CHANNELS: (orgId: string) => `${API_BASE}/orgs/${orgId}/agents/me/slack/channels`,
641
+ SLACK_USERS: (orgId: string) => `${API_BASE}/orgs/${orgId}/agents/me/slack/users`,
642
+ SLACK_HISTORY: (orgId: string) => `${API_BASE}/orgs/${orgId}/agents/me/slack/history`,
643
+ SLACK_MEMBERS: (orgId: string) => `${API_BASE}/orgs/${orgId}/agents/me/slack/members`,
644
+ SLACK_STATUS: (orgId: string) => `${API_BASE}/orgs/${orgId}/agents/me/slack/status`,
637
645
 
638
646
  // Invitations (org-scoped, admin)
639
647
  ORG_INVITATIONS: (orgId: string) => `${API_BASE}/orgs/${orgId}/invitations`,
@@ -774,6 +782,8 @@ export const ENDPOINTS = {
774
782
  CHAT_READ: (orgId: string, chatId: string) => `${API_BASE}/orgs/${orgId}/chats/${chatId}/read`,
775
783
  THREAD_UNREAD: (orgId: string, chatId: string, threadRootId: string) =>
776
784
  `${API_BASE}/orgs/${orgId}/chats/${chatId}/threads/${threadRootId}/unread`,
785
+ THREAD_READ: (orgId: string, chatId: string, threadRootId: string) =>
786
+ `${API_BASE}/orgs/${orgId}/chats/${chatId}/threads/${threadRootId}/read`,
777
787
 
778
788
  // References (org-scoped)
779
789
  REFS_RESOLVE: (orgId: string) => `${API_BASE}/orgs/${orgId}/refs/resolve`,
@@ -861,13 +871,22 @@ export const ENDPOINTS = {
861
871
  ORG_EDGE: (orgId: string) => `/api/v1/orgs/${orgId}/edge`,
862
872
  ORG_EDGE_DEVICES: (orgId: string) => `/api/v1/orgs/${orgId}/edge/devices`,
863
873
  ORG_EDGE_DEVICE: (orgId: string, edgeId: string) => `/api/v1/orgs/${orgId}/edge/${edgeId}`,
874
+ ORG_EDGE_DEVICE_UNREGISTER: (orgId: string, edgeId: string) =>
875
+ `/api/v1/orgs/${orgId}/edge/${edgeId}/unregister`,
864
876
  ORG_EDGE_ONBOARDING: (orgId: string) => `/api/v1/orgs/${orgId}/edge/onboarding`,
865
877
  ORG_EDGE_PROFILES: (orgId: string, edgeId: string) =>
866
878
  `/api/v1/orgs/${orgId}/edge/${edgeId}/profiles`,
879
+ ORG_EDGE_EXEC: (orgId: string) => `/api/v1/orgs/${orgId}/edge/exec`,
867
880
  CLIP_CONNECTIONS: (orgId: string, clipId: string) =>
868
881
  `/api/v1/orgs/${orgId}/clip-registry/${clipId}/connections`,
869
882
  CLIP_CONNECTION: (orgId: string, connId: string) =>
870
883
  `/api/v1/orgs/${orgId}/clip-connections/${connId}`,
884
+
885
+ // Clip registry (v3, org-scoped, served by api-server — `crg_` entries; the
886
+ // Pinix Hub catalog proxy above is a different, id-less surface)
887
+ ORG_CLIP_REGISTRY: (orgId: string) => `/api/v1/orgs/${orgId}/clip-registry`,
888
+ ORG_CLIP_INSTALL: (orgId: string) => `/api/v1/orgs/${orgId}/clips/install`,
889
+ ORG_CLIPS_INSTALLED: (orgId: string) => `/api/v1/orgs/${orgId}/clips/installed`,
871
890
  } as const;
872
891
 
873
892
  /**
package/src/types.ts CHANGED
@@ -2497,6 +2497,14 @@ export interface InboxItem {
2497
2497
  grouping_priority: number;
2498
2498
  /** Number of events in this group. Populated by the grouped list query. */
2499
2499
  group_count: number;
2500
+ /**
2501
+ * True when ANY row in this item's group is unread — the same group-level
2502
+ * definition the unread-count badge uses. The representative row's own
2503
+ * `read_at` can be set while older rows in its group are still unread, so
2504
+ * read-state display must key off this field. Optional: absent on locally
2505
+ * cached rows written before the field shipped (fall back to `read_at`).
2506
+ */
2507
+ group_unread?: boolean;
2500
2508
  read_at: string | null;
2501
2509
  archived_at: string | null;
2502
2510
  snoozed_until: string | null;
@@ -2535,6 +2543,14 @@ export interface ChannelConnection {
2535
2543
  ingress_url?: string;
2536
2544
  /** Returned once at mint time (create / token regeneration). */
2537
2545
  ingress_token?: string;
2546
+ /**
2547
+ * The in-product scope-upgrade re-auth can act on this connection
2548
+ * (slack, active, provisioned-family — its auth_config carries the
2549
+ * OAuth client). Mirrors the upgrade endpoint's preconditions exactly.
2550
+ * Manual-family connections upgrade via the Slack console; the UI
2551
+ * hides the upgrade CTA when this is false/absent.
2552
+ */
2553
+ upgrade_eligible?: boolean;
2538
2554
  created_at: string;
2539
2555
  updated_at: string;
2540
2556
  }
@@ -2649,16 +2665,35 @@ export type InitiateChannelProvisioningInput =
2649
2665
  provider: 'slack';
2650
2666
  /**
2651
2667
  * The short-lived app-configuration token (12h, generated at
2652
- * api.slack.com/apps). Transits once into apps.manifest.create and is
2653
- * never stored.
2668
+ * api.slack.com/apps). Transits once into apps.manifest.create/update
2669
+ * and is never stored.
2654
2670
  */
2655
2671
  config_token: string;
2672
+ /**
2673
+ * Scope-upgrade re-auth: converge the agent's EXISTING active
2674
+ * connection's app on the current manifest and re-authorize it (bot
2675
+ * token replaced in place, app id preserved) instead of creating a
2676
+ * new app.
2677
+ */
2678
+ upgrade?: boolean;
2656
2679
  }
2657
2680
  | {
2658
2681
  agent_id: string;
2659
2682
  provider: Exclude<ChannelProvider, 'slack'>;
2660
2683
  };
2661
2684
 
2685
+ /**
2686
+ * Manifest-prefill link for the guided manual slack path: the platform
2687
+ * mints a pending connection and returns an api.slack.com link that opens
2688
+ * the create-app form with everything preset (events, scopes, this
2689
+ * connection's webhook URL). The user installs the app themselves and
2690
+ * activates the SAME connection by delivering bot token + signing secret.
2691
+ */
2692
+ export interface SlackManifestLinkResult {
2693
+ connection_id: string;
2694
+ manifest_url: string;
2695
+ }
2696
+
2662
2697
  /**
2663
2698
  * Tier-B platform verb request (agent-only): send as the bound bot.
2664
2699
  * Deliberately narrowed to 'slack' — the verb surface is per-vendor
@@ -2670,9 +2705,12 @@ export interface SendChannelMessageInput {
2670
2705
  /** Vendor-native conversation id from the inbound event (e.g. C…/D…). */
2671
2706
  conversation_id: string;
2672
2707
  /**
2673
- * External message id being answered (channel-domain format). REQUIRED
2674
- * for channel conversations (the reply lands in that message's thread);
2675
- * optional for DMs (always linear).
2708
+ * External message id being answered (channel-domain format:
2709
+ * `{channel}:{ts}`, thread children `{channel}:{root}#{ts}`). REQUIRED
2710
+ * for channel conversations (the reply lands in that message's thread).
2711
+ * In DMs the SESSION never forks, but reply POSITION follows the
2712
+ * question: a thread-child id lands the reply inside that thread, a
2713
+ * bare id (or omitting the field) keeps the flattened main flow.
2676
2714
  */
2677
2715
  reply_to?: string;
2678
2716
  text: string;
@@ -2681,12 +2719,94 @@ export interface SendChannelMessageInput {
2681
2719
  export interface SentChannelMessage {
2682
2720
  channel_type: 'slack';
2683
2721
  conversation_id: string;
2684
- /** Channel-domain external id ({channel}:{ts}) — usable as a reply_to. */
2722
+ /**
2723
+ * Channel-domain external id — `{channel}:{ts}` for top-level posts,
2724
+ * `{channel}:{root}#{ts}` when the reply landed inside a thread — usable
2725
+ * directly as a future reply_to.
2726
+ */
2685
2727
  message_id: string;
2686
2728
  thread_anchor?: string;
2687
2729
  sent_at: string;
2688
2730
  }
2689
2731
 
2732
+ /**
2733
+ * Tier-B read verbs (agent-only): workspace visibility as the bot sees it.
2734
+ * Authorization is the bot's own Slack permissions (invite the bot to a
2735
+ * channel to make its history readable) — reads have no external side
2736
+ * effect, so the seen-conversation restriction of the send verb does not
2737
+ * apply. Cursor pagination follows Slack's contract (empty = last page).
2738
+ */
2739
+ export interface SlackChannelInfo {
2740
+ id: string;
2741
+ name: string;
2742
+ is_private: boolean;
2743
+ is_member: boolean;
2744
+ num_members?: number;
2745
+ topic?: string;
2746
+ }
2747
+
2748
+ export interface SlackChannelsPage {
2749
+ channels: SlackChannelInfo[];
2750
+ next_cursor?: string;
2751
+ }
2752
+
2753
+ export interface SlackUserInfo {
2754
+ id: string;
2755
+ name: string;
2756
+ real_name?: string;
2757
+ is_bot: boolean;
2758
+ deleted?: boolean;
2759
+ }
2760
+
2761
+ export interface SlackUsersPage {
2762
+ users: SlackUserInfo[];
2763
+ next_cursor?: string;
2764
+ }
2765
+
2766
+ export interface SlackHistoryMessage {
2767
+ /**
2768
+ * Synthesized channel-domain id — `{conversation}:{ts}`, or
2769
+ * `{conversation}:{root}#{ts}` for thread children — directly usable as
2770
+ * the send verb's `reply_to` (no client-side root#child derivation).
2771
+ */
2772
+ message_id: string;
2773
+ ts: string;
2774
+ thread_ts?: string;
2775
+ user?: string;
2776
+ bot_id?: string;
2777
+ text: string;
2778
+ }
2779
+
2780
+ export interface SlackHistoryPage {
2781
+ messages: SlackHistoryMessage[];
2782
+ next_cursor?: string;
2783
+ }
2784
+
2785
+ export interface SlackMembersPage {
2786
+ /** Bare Slack user ids — resolve display names via the users verb. */
2787
+ members: string[];
2788
+ next_cursor?: string;
2789
+ }
2790
+
2791
+ /**
2792
+ * Slack-native pagination for the read verbs. Filters are deliberately
2793
+ * FIXED server-side (channels: public+private, archived excluded) — the
2794
+ * public contract is cursor/limit only; widening it is a deliberate scope
2795
+ * decision, not a missing passthrough.
2796
+ */
2797
+ export interface SlackReadPageQuery {
2798
+ cursor?: string;
2799
+ limit?: number;
2800
+ }
2801
+
2802
+ /** Agents-pane "typing…" indicator for one assistant thread (empty status clears). */
2803
+ export interface SlackStatusInput {
2804
+ conversation: string;
2805
+ /** The assistant thread's root ts. */
2806
+ thread_ts: string;
2807
+ status?: string;
2808
+ }
2809
+
2690
2810
  // ============================================================
2691
2811
  // Dispatch Types (agent event delivery)
2692
2812
  // ============================================================
@@ -4023,6 +4143,15 @@ export type EdgePlacement = 'byoc' | 'hosted';
4023
4143
  */
4024
4144
  export type EdgeHostedState = 'idle' | 'deleting';
4025
4145
 
4146
+ /** Stable API error codes returned by the BYOC unregister contract. */
4147
+ export type EdgeDeviceUnregisterErrorCode =
4148
+ | 'JWT_REQUIRED'
4149
+ | 'HUMAN_REQUIRED'
4150
+ | 'FORBIDDEN'
4151
+ | 'EDGE_PLACEMENT_UNSUPPORTED'
4152
+ | 'EDGE_ONLINE'
4153
+ | 'EDGE_INTEGRITY_ERROR';
4154
+
4026
4155
  export interface EdgeDevice {
4027
4156
  id: string;
4028
4157
  org_id: string;
@@ -4073,6 +4202,102 @@ export interface ClipConnection {
4073
4202
  updated_at: string;
4074
4203
  }
4075
4204
 
4205
+ /**
4206
+ * A clip in the api-server org registry (`crg_…`, table `clip_registry`) — the
4207
+ * v3 registry that install and clip connections operate on. Distinct from
4208
+ * {@link RegistryClipInfo}, which is the clip-service → Pinix Hub catalog proxy
4209
+ * and carries no registry id.
4210
+ */
4211
+ export interface ClipRegistryEntry {
4212
+ id: string;
4213
+ org_id: string;
4214
+ name: string;
4215
+ description?: string;
4216
+ visibility: 'public' | 'private';
4217
+ version?: string;
4218
+ /** clip.json manifest. `type` absent/null means browser (predates the field). */
4219
+ manifest: Record<string, unknown> | null;
4220
+ author_id: string;
4221
+ /** Reviewed snapshot served cross-org. Absent = never approved. */
4222
+ approved_version_id?: string;
4223
+ created_at: string;
4224
+ updated_at: string;
4225
+ /** Author-org-only computed fields (state of the latest submitted version). */
4226
+ review_status?: string;
4227
+ review_note?: string;
4228
+ }
4229
+
4230
+ export interface InstallRegistryClipResponse {
4231
+ ok: boolean;
4232
+ clip_id: string;
4233
+ name: string;
4234
+ }
4235
+
4236
+ /**
4237
+ * Execute a registry (Edge) clip command on an Edge device
4238
+ * (`POST /orgs/{orgId}/edge/exec`).
4239
+ *
4240
+ * Routing is EXPLICIT — exactly one of:
4241
+ * - `connection`: a clip connection id (`ccn_…`) or its org-local alias. The
4242
+ * ONLY route to a hosted (Cloud Profile) device: the connection its
4243
+ * maintainer bound IS the org-wide authorization. Also valid for BYOC.
4244
+ * - `edge_id`: a BYOC desktop device the CALLER owns. Naming a hosted device
4245
+ * here is refused (`400 HOSTED_CONNECTION_REQUIRED`).
4246
+ * - neither: legacy BYOC fallback — resolves only to the caller's OWN online
4247
+ * desktop device, never to a hosted one.
4248
+ */
4249
+ export interface ExecEdgeClipRequest {
4250
+ /**
4251
+ * Clip name in the org's clip registry (or an installed marketplace clip).
4252
+ * Exactly ONE of `clip` or `clip_id` is required — same identity contract as
4253
+ * clip invoke: bare names keep the fail-closed collision behavior (own-org
4254
+ * shadows installs; two installed namesakes → `409 CLIP_NAME_AMBIGUOUS`).
4255
+ */
4256
+ clip?: string;
4257
+ /**
4258
+ * Exact registry Clip id (`crg_…`). Selects the exact object without
4259
+ * weakening execution trust (cross-org still requires public + approved +
4260
+ * installed and runs only the approved snapshot).
4261
+ */
4262
+ clip_id?: string;
4263
+ /** Command to run — resolves to `<command>.js` in the clip's files. */
4264
+ command: string;
4265
+ /** Command arguments, forwarded verbatim to the clip. */
4266
+ args?: unknown;
4267
+ /** Connection id (`ccn_…`) or alias. Required for hosted devices. */
4268
+ connection?: string;
4269
+ /** Owned BYOC device id. Mutually exclusive with `connection`. */
4270
+ edge_id?: string;
4271
+ /**
4272
+ * Browser profile name. With `connection` it may only restate the profile
4273
+ * that connection grants (`400 CONNECTION_PROFILE_MISMATCH` otherwise).
4274
+ */
4275
+ profile?: string;
4276
+ /** Execution timeout in MILLISECONDS (default 30000, max 120000). */
4277
+ timeout?: number;
4278
+ /**
4279
+ * Optional audit tag (≤128 chars). Keep the SAME id across an
4280
+ * EDGE_ACTIVATING retry loop so the server's audit log reads the retries as
4281
+ * one logical call. It is NOT an idempotency key — it deduplicates nothing.
4282
+ */
4283
+ correlation_id?: string;
4284
+ }
4285
+
4286
+ /**
4287
+ * A completed execution's result envelope (HTTP 200). Every non-success is
4288
+ * thrown as a typed {@link ApiError} instead — see
4289
+ * {@link ParallClient.execEdgeClip} for the full code table and, critically,
4290
+ * which codes are safe to retry.
4291
+ */
4292
+ export interface EdgeClipExecResult {
4293
+ request_id: string;
4294
+ success: boolean;
4295
+ data?: unknown;
4296
+ error?: string;
4297
+ error_code?: string;
4298
+ duration_ms: number;
4299
+ }
4300
+
4076
4301
  /**
4077
4302
  * Two INDEPENDENT onboarding journeys. The original fields mean what they always
4078
4303
  * meant — "has this org set up the desktop journey?" — and hosted-backed state