@parall/parall 1.36.1 → 1.37.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/dist/hooks.js CHANGED
@@ -72,9 +72,9 @@ To respond, you **must** use the Parall CLI via the exec (Bash) tool. If \`paral
72
72
 
73
73
  ### Event types and where to reply
74
74
 
75
- - **\`[Event: message.new]\`** — includes \`[Chat: ... (prll://cht_xxx)]\`. Reply into that chat:
75
+ - **\`[Event: message.new]\`** — includes \`[Chat: ... (prll://cht_xxx)]\`. Reply into that chat. Write the reply with your file tool first (shell-safe), then send it — don't wrap message content in \`--text "..."\`, since the shell expands \`$1,000\`→\`,000\` and runs \`$(...)\` inside double quotes:
76
76
 
77
- parall messages send prll://cht_xxx --text "Your reply here"
77
+ parall messages send prll://cht_xxx --text-file /tmp/reply.md
78
78
 
79
79
  - **\`[Event: task.assigned]\` / \`[Event: task.comment.created]\`** — includes \`[Task: ... (prll://tsk_xxx)]\`. Act on the task; use task CLI subcommands (\`tasks update\`, \`tasks comment\`). See the \`parall-tasks\` skill.
80
80
 
@@ -26662,6 +26662,17 @@ function buildEventBody(event) {
26662
26662
  if (event.attachedUri)
26663
26663
  lines.push(`[Attached: ${sanitizeMeta(event.attachedUri)}]`);
26664
26664
  lines.push("", event.body);
26665
+ } else if (event.type === "channel_message") {
26666
+ lines.push(`[Event: channel.message]`);
26667
+ const providerLabel = sanitizeMeta(event.channelProvider ?? "external IM");
26668
+ const convLabel = event.channelExternalConversationId ? `${sanitizeMeta(event.channelExternalConversationId)} (${sanitizeMeta(event.channelConversationType ?? "conversation")})` : sanitizeMeta(event.channelConversationType ?? "conversation");
26669
+ lines.push(`[Channel: ${providerLabel} | conversation: ${convLabel}]`);
26670
+ lines.push(`[From: ${sanitizeMeta(event.senderName)} (external user, not a Parall member)]`);
26671
+ if (event.channelExternalMessageId) {
26672
+ lines.push(`[External message ID: ${sanitizeMeta(event.channelExternalMessageId)}]`);
26673
+ }
26674
+ lines.push(`[Audience: this conversation lives on ${providerLabel}, OUTSIDE Parall. Readers cannot open prll:// links, Parall cards, or internal attachments \u2014 never include them in replies. Write plain conversational text.]`);
26675
+ lines.push("", event.body);
26665
26676
  } else if (event.type === "external_trigger") {
26666
26677
  lines.push(`[Event: external.trigger]`);
26667
26678
  lines.push(`[Trigger: prll://${event.targetId}]`);
@@ -26703,7 +26714,7 @@ function buildSendMessageHint(event) {
26703
26714
  }
26704
26715
  if (event.targetId.startsWith("cht_")) {
26705
26716
  return `
26706
- <system-reminder>To reply, run: \`parall messages send prll://${event.targetId} --text "..."\` \u2014 your plain text output is not delivered to the chat.</system-reminder>`;
26717
+ <system-reminder>To reply, run \`parall messages send prll://${event.targetId} --text-file - <<'EOF'\` \u2026 \`EOF\` \u2014 the quoted heredoc keeps \`$\`, backticks and apostrophes literal (plain \`--text "$1,000"\` sends \`,000\`). Your plain text output is not delivered to the chat.</system-reminder>`;
26707
26718
  }
26708
26719
  if (event.targetId.startsWith("tsk_")) {
26709
26720
  return `
@@ -26712,6 +26723,14 @@ function buildSendMessageHint(event) {
26712
26723
  if (event.targetId.startsWith("sch_")) {
26713
26724
  return `
26714
26725
  <system-reminder>To communicate, use the CLI: \`parall messages send\` / \`parall dm\`. Your plain text output is not delivered.</system-reminder>`;
26726
+ }
26727
+ if (event.type === "channel_message") {
26728
+ const clipLabel = event.channelProvider ? `the \`${event.channelProvider}\` clip's` : "your channel provider clip's";
26729
+ const apiLabel = event.channelProvider ?? "external platform";
26730
+ const target = event.channelExternalConversationId ? `{"chat_id": "${event.channelExternalConversationId}", "text": "..."}` : `{"chat_id": "<conversation id>", "text": "..."}`;
26731
+ const threadAlt = event.channelExternalMessageId ? ` To reply in-thread to this specific message, use {"message_id": "${event.channelExternalMessageId}", "text": "..."} instead.` : "";
26732
+ return `
26733
+ <system-reminder>To reply, invoke ${clipLabel} \`send_message\` command with ${target} \u2014 your plain text output is NOT delivered to the external conversation.${threadAlt} The same clip's \`call\` command reaches the wider ${apiLabel} API when needed.</system-reminder>`;
26715
26734
  }
26716
26735
  if (event.type === "external_trigger" || event.targetId.startsWith("xtr_")) {
26717
26736
  return `
@@ -26919,6 +26938,8 @@ Or upload first and reuse across chats:
26919
26938
  parall messages send prll://cht_aaa --attachment att_yyy --text "Report"
26920
26939
  parall messages send prll://cht_bbb --attachment att_yyy --text "FYI"
26921
26940
 
26941
+ The \`--text\` captions above are safe short literals. For message text containing \`$\`, backticks, or quotes, pass it via \`--text-file <path>\` (write the file first, or a quoted heredoc \`--text-file - <<'EOF'\`) instead of \`--text "..."\` \u2014 inside double quotes the shell turns \`$1,000\` into \`,000\` and executes \`$(...)\`.
26942
+
26922
26943
  ### When to reference
26923
26944
 
26924
26945
  - **Origin** \u2014 always link the message or task that triggered your work
@@ -27033,6 +27054,7 @@ var ENDPOINTS = {
27033
27054
  // Org-scoped
27034
27055
  ORG: (orgId) => `${API_BASE}/orgs/${orgId}`,
27035
27056
  ORG_MEMBERS: (orgId) => `${API_BASE}/orgs/${orgId}/members`,
27057
+ ORG_MEMBERS_FORMER: (orgId) => `${API_BASE}/orgs/${orgId}/members/former`,
27036
27058
  TEAMS: (orgId) => `${API_BASE}/orgs/${orgId}/teams`,
27037
27059
  ORG_MEMBERS_ONLINE: (orgId) => `${API_BASE}/orgs/${orgId}/members/online`,
27038
27060
  ORG_MEMBER: (orgId, userId) => `${API_BASE}/orgs/${orgId}/members/${userId}`,
@@ -27182,6 +27204,19 @@ var ENDPOINTS = {
27182
27204
  EXTERNAL_TRIGGER: (orgId, triggerId) => `${API_BASE}/orgs/${orgId}/external-triggers/${triggerId}`,
27183
27205
  EXTERNAL_TRIGGER_RUNS: (orgId) => `${API_BASE}/orgs/${orgId}/external-trigger-runs`,
27184
27206
  EXTERNAL_TRIGGER_RUN: (orgId, runId) => `${API_BASE}/orgs/${orgId}/external-trigger-runs/${runId}`,
27207
+ // External IM channel (org-scoped, platform-mediated Feishu/Slack)
27208
+ CHANNEL_CONNECTIONS: (orgId) => `${API_BASE}/orgs/${orgId}/channel-connections`,
27209
+ CHANNEL_CONNECTION: (orgId, connectionId) => `${API_BASE}/orgs/${orgId}/channel-connections/${connectionId}`,
27210
+ CHANNEL_CONNECTION_CREDENTIALS: (orgId, connectionId) => `${API_BASE}/orgs/${orgId}/channel-connections/${connectionId}/credentials`,
27211
+ CHANNEL_CONNECTION_INGRESS_TOKEN_REGENERATE: (orgId, connectionId) => `${API_BASE}/orgs/${orgId}/channel-connections/${connectionId}/ingress-token/regenerate`,
27212
+ CHANNEL_CONNECTION_CONVERSATIONS: (orgId, connectionId) => `${API_BASE}/orgs/${orgId}/channel-connections/${connectionId}/conversations`,
27213
+ CHANNEL_CONVERSATION: (orgId, conversationId) => `${API_BASE}/orgs/${orgId}/channel-conversations/${conversationId}`,
27214
+ CHANNEL_CONVERSATION_MESSAGES: (orgId, conversationId) => `${API_BASE}/orgs/${orgId}/channel-conversations/${conversationId}/messages`,
27215
+ CHANNEL_CONVERSATION_SESSION: (orgId, conversationId) => `${API_BASE}/orgs/${orgId}/channel-conversations/${conversationId}/session`,
27216
+ CHANNEL_MESSAGE: (orgId, messageId) => `${API_BASE}/orgs/${orgId}/channel-messages/${messageId}`,
27217
+ CHANNEL_PROVISIONING: (orgId) => `${API_BASE}/orgs/${orgId}/channel-provisioning`,
27218
+ CHANNEL_PROVISIONING_SESSION: (orgId, sessionId) => `${API_BASE}/orgs/${orgId}/channel-provisioning/${sessionId}`,
27219
+ CHANNEL_PROVISIONING_CANCEL: (orgId, sessionId) => `${API_BASE}/orgs/${orgId}/channel-provisioning/${sessionId}/cancel`,
27185
27220
  // Invitations (org-scoped, admin)
27186
27221
  ORG_INVITATIONS: (orgId) => `${API_BASE}/orgs/${orgId}/invitations`,
27187
27222
  ORG_INVITATION: (orgId, invId) => `${API_BASE}/orgs/${orgId}/invitations/${invId}`,
@@ -27199,9 +27234,16 @@ var ENDPOINTS = {
27199
27234
  INVITE_LINK_JOIN: `${API_BASE}/invite-link/join`,
27200
27235
  // Wikis (org-scoped, served by wiki-service)
27201
27236
  WIKIS: (orgId) => `${WIKI_BASE}/orgs/${orgId}/wikis`,
27237
+ // Recycle bin — soft-deleted wikis (owner only). Must precede WIKI in the
27238
+ // backend router so `deleted` isn't captured as a {wikiId}; the SDK builder
27239
+ // is just a string.
27240
+ WIKIS_DELETED: (orgId) => `${WIKI_BASE}/orgs/${orgId}/wikis/deleted`,
27241
+ // Detail URL — also reused for DELETE (soft-delete) and `${WIKI}/restore`.
27202
27242
  WIKI: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}`,
27243
+ WIKI_RESTORE: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/restore`,
27203
27244
  WIKI_TREE: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/tree`,
27204
27245
  WIKI_BLOB: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/blob`,
27246
+ WIKI_COPY: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/copy`,
27205
27247
  WIKI_NODE_SECTIONS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/node-sections`,
27206
27248
  WIKI_SEARCH: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/search`,
27207
27249
  WIKI_PAGE_INDEX: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/page-index`,
@@ -27284,6 +27326,14 @@ var ENDPOINTS = {
27284
27326
  BILLING_AUTO_RELOAD: (orgId) => `${API_BASE}/orgs/${orgId}/billing/auto-reload`,
27285
27327
  BILLING_SETUP_INTENT: (orgId) => `${API_BASE}/orgs/${orgId}/billing/setup-intent`,
27286
27328
  COMPUTE_PRICING: () => `${API_BASE}/billing/compute-pricing`,
27329
+ // Runtime capability table (public, no auth) — SSOT for the create/settings
27330
+ // interlock: per-runtime compute modes, native model family, cross-family
27331
+ // availability, and recommended model.
27332
+ RUNTIMES: () => `${API_BASE}/runtimes`,
27333
+ // Model catalog (public, no auth) — served from the server's DB-backed
27334
+ // snapshot. Optional include_model keeps an agent's pinned hidden legacy
27335
+ // model representable in settings pickers.
27336
+ MODELS: (includeModel) => includeModel ? `${API_BASE}/models?include_model=${encodeURIComponent(includeModel)}` : `${API_BASE}/models`,
27287
27337
  // ADMIN_GRANTS intentionally NOT exported here — it sits under the
27288
27338
  // unauthenticated `/internal/admin/*` surface and must not bleed into the
27289
27339
  // public SDK. Admin dashboard hits the URL directly from its own client.
@@ -27296,6 +27346,7 @@ var ENDPOINTS = {
27296
27346
  AGENT_CLIP: (orgId, agentId, clipId) => `${CLIP_BASE}/orgs/${orgId}/agents/${agentId}/clips/${clipId}`,
27297
27347
  CLIP_INVOKE: (orgId) => `${CLIP_BASE}/orgs/${orgId}/invoke`,
27298
27348
  CLIP_ONLINE: (orgId) => `${CLIP_BASE}/orgs/${orgId}/online`,
27349
+ BROWSER_RUNTIME_STATUS: (orgId) => `${CLIP_BASE}/orgs/${orgId}/browser-runtime/status`,
27299
27350
  BROWSER_PROFILES: (orgId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles`,
27300
27351
  BROWSER_PROFILE: (orgId, profileId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles/${profileId}`,
27301
27352
  BROWSER_PROFILE_OPEN: (orgId, profileId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles/${profileId}/open`,
@@ -27349,10 +27400,13 @@ var WS_EVENTS = {
27349
27400
  INVITATION_REVOKED: "invitation.revoked",
27350
27401
  ORG_JOIN_REQUEST_NEW: "org.join_request.new",
27351
27402
  ORG_INVITE_LINK_JOINED: "org.invite_link.joined",
27403
+ ORG_MEMBER_REMOVED: "org.member.removed",
27352
27404
  AGENT_CONFIG_UPDATE: "agent_config.update",
27353
27405
  PRESENCE_UPDATE: "presence.update",
27354
27406
  WIKI_CHANGESET_CREATED: "wiki.changeset.created",
27355
27407
  WIKI_CHANGESET_UPDATED: "wiki.changeset.updated",
27408
+ WIKI_DELETED: "wiki.deleted",
27409
+ WIKI_RESTORED: "wiki.restored",
27356
27410
  COMMENT_CREATED: "comment.created",
27357
27411
  COMMENT_UPDATED: "comment.updated",
27358
27412
  COMMENT_DELETED: "comment.deleted",
@@ -27380,6 +27434,7 @@ var WS_EVENTS = {
27380
27434
  MACHINE_FILESYSTEM_BROWSE: "machine.filesystem.browse",
27381
27435
  MACHINE_UPDATE: "machine.update",
27382
27436
  MACHINE_CONFIG_UPDATED: "machine.config.updated",
27437
+ MACHINE_AGENT_CONFIG_UPDATED: "machine.agent_config.updated",
27383
27438
  MACHINE_CLIP_SYNC: "machine.clip.sync",
27384
27439
  MACHINE_BROWSER_PROFILE_LIFECYCLE: "machine.browser_profile.lifecycle",
27385
27440
  MACHINE_BROWSER_PROFILE_VIEWER: "machine.browser_profile.viewer",
@@ -27719,6 +27774,13 @@ var ParallClient = class _ParallClient {
27719
27774
  const res = await this.request("GET", ENDPOINTS.ORG_MEMBERS(orgId));
27720
27775
  return res.data;
27721
27776
  }
27777
+ /** User IDs of soft-removed (former) org members — for marking their avatar /
27778
+ * name as "left" where dormant relations still surface them (DMs, task
27779
+ * assignees, message history). */
27780
+ async getFormerMemberIds(orgId) {
27781
+ const res = await this.request("GET", ENDPOINTS.ORG_MEMBERS_FORMER(orgId));
27782
+ return res.user_ids ?? [];
27783
+ }
27722
27784
  async getTeams(orgId) {
27723
27785
  const res = await this.request("GET", ENDPOINTS.TEAMS(orgId));
27724
27786
  return res.data;
@@ -28207,20 +28269,25 @@ var ParallClient = class _ParallClient {
28207
28269
  return res.data;
28208
28270
  }
28209
28271
  /** `POST /machines/me/browser-profiles/{profileId}/status` — report runtime status. */
28210
- async reportBrowserProfileStatus(profileId, status, errorMsg) {
28272
+ async reportBrowserProfileStatus(profileId, status, errorMsg, generation) {
28211
28273
  await this.request("POST", ENDPOINTS.MACHINES_ME_BROWSER_PROFILE_STATUS(profileId), {
28212
28274
  status,
28213
- ...errorMsg ? { error_msg: errorMsg } : {}
28275
+ ...errorMsg ? { error_msg: errorMsg } : {},
28276
+ ...generation !== void 0 ? { generation } : {}
28214
28277
  });
28215
28278
  }
28216
28279
  /**
28217
28280
  * `POST /machines/me/health` — bump the Machine's `updated_at` to now and
28218
- * report daemon state. The daemon should call this on a fixed cadence (e.g.
28219
- * every 30s) so an external observer can detect a wedged supervisor. Both
28281
+ * report daemon state. The daemon calls this on startup and again whenever
28282
+ * periodic runtime re-detection produces a CHANGED result (there is no
28283
+ * fixed-cadence keepalive loop today — steady state posts nothing). All
28220
28284
  * fields are optional and only persisted when changed:
28221
28285
  * - `daemonVersion` — the running bundle/launcher version.
28222
28286
  * - `selfUpdateCapable` — whether the daemon can act on a machine.update
28223
28287
  * signal (true under a service manager, false for bare `npx` foreground).
28288
+ * - `detectedRuntimes` — runtime CLIs found on the host. Omitted = no
28289
+ * report this beat (server keeps the stored value); [] = detection ran
28290
+ * and found nothing (server clears to empty).
28224
28291
  */
28225
28292
  async postMachineHeartbeat(opts) {
28226
28293
  const body = {};
@@ -28228,6 +28295,8 @@ var ParallClient = class _ParallClient {
28228
28295
  body.daemon_version = opts.daemonVersion;
28229
28296
  if (opts?.selfUpdateCapable !== void 0)
28230
28297
  body.self_update_capable = opts.selfUpdateCapable;
28298
+ if (opts?.detectedRuntimes !== void 0)
28299
+ body.detected_runtimes = opts.detectedRuntimes;
28231
28300
  return this.request("POST", ENDPOINTS.MACHINES_ME_HEALTH, Object.keys(body).length > 0 ? body : void 0);
28232
28301
  }
28233
28302
  async reportAgentWorkspaceState(agentId, state) {
@@ -28546,6 +28615,59 @@ var ParallClient = class _ParallClient {
28546
28615
  async deleteExternalConnection(orgId, connectionId) {
28547
28616
  return this.request("DELETE", ENDPOINTS.EXTERNAL_CONNECTION(orgId, connectionId));
28548
28617
  }
28618
+ // ---- External IM channel (org-scoped) ----
28619
+ async createChannelConnection(orgId, input) {
28620
+ return this.request("POST", ENDPOINTS.CHANNEL_CONNECTIONS(orgId), input);
28621
+ }
28622
+ async listChannelConnections(orgId) {
28623
+ return this.request("GET", ENDPOINTS.CHANNEL_CONNECTIONS(orgId));
28624
+ }
28625
+ async getChannelConnection(orgId, connectionId) {
28626
+ return this.request("GET", ENDPOINTS.CHANNEL_CONNECTION(orgId, connectionId));
28627
+ }
28628
+ async updateChannelConnection(orgId, connectionId, patch) {
28629
+ return this.request("PATCH", ENDPOINTS.CHANNEL_CONNECTION(orgId, connectionId), patch);
28630
+ }
28631
+ async archiveChannelConnection(orgId, connectionId) {
28632
+ return this.request("DELETE", ENDPOINTS.CHANNEL_CONNECTION(orgId, connectionId));
28633
+ }
28634
+ async deliverChannelCredentials(orgId, connectionId, credentials) {
28635
+ return this.request("POST", ENDPOINTS.CHANNEL_CONNECTION_CREDENTIALS(orgId, connectionId), credentials);
28636
+ }
28637
+ async regenerateChannelIngressToken(orgId, connectionId) {
28638
+ return this.request("POST", ENDPOINTS.CHANNEL_CONNECTION_INGRESS_TOKEN_REGENERATE(orgId, connectionId));
28639
+ }
28640
+ /** Start a Feishu one-click provisioning session (device flow QR). */
28641
+ async initiateChannelProvisioning(orgId, input) {
28642
+ return this.request("POST", ENDPOINTS.CHANNEL_PROVISIONING(orgId), input);
28643
+ }
28644
+ /**
28645
+ * Lazy status poll — server-side this may forward one provider poll, so
28646
+ * call it at the session's `poll_interval_seconds` cadence, not faster.
28647
+ */
28648
+ async getChannelProvisioningSession(orgId, sessionId) {
28649
+ return this.request("GET", ENDPOINTS.CHANNEL_PROVISIONING_SESSION(orgId, sessionId));
28650
+ }
28651
+ async cancelChannelProvisioning(orgId, sessionId) {
28652
+ return this.request("POST", ENDPOINTS.CHANNEL_PROVISIONING_CANCEL(orgId, sessionId));
28653
+ }
28654
+ async listChannelConversations(orgId, connectionId) {
28655
+ return this.request("GET", ENDPOINTS.CHANNEL_CONNECTION_CONVERSATIONS(orgId, connectionId));
28656
+ }
28657
+ async getChannelConversation(orgId, conversationId) {
28658
+ return this.request("GET", ENDPOINTS.CHANNEL_CONVERSATION(orgId, conversationId));
28659
+ }
28660
+ async listChannelConversationMessages(orgId, conversationId, limit) {
28661
+ return this.request("GET", ENDPOINTS.CHANNEL_CONVERSATION_MESSAGES(orgId, conversationId), void 0, { limit });
28662
+ }
28663
+ async setChannelConversationSession(orgId, conversationId, agentSessionId) {
28664
+ return this.request("PATCH", ENDPOINTS.CHANNEL_CONVERSATION_SESSION(orgId, conversationId), {
28665
+ agent_session_id: agentSessionId
28666
+ });
28667
+ }
28668
+ async getChannelMessage(orgId, messageId) {
28669
+ return this.request("GET", ENDPOINTS.CHANNEL_MESSAGE(orgId, messageId));
28670
+ }
28549
28671
  async getExternalTriggerSchema(orgId, connectionId) {
28550
28672
  return this.request("GET", ENDPOINTS.EXTERNAL_TRIGGER_SCHEMA(orgId, connectionId));
28551
28673
  }
@@ -28587,6 +28709,23 @@ var ParallClient = class _ParallClient {
28587
28709
  async getWiki(orgId, wikiId) {
28588
28710
  return this.request("GET", ENDPOINTS.WIKI(orgId, wikiId));
28589
28711
  }
28712
+ /** Soft-delete an entire wiki (org owner only; the default wiki is
28713
+ * protected → 403 WIKI_PROTECTED). Returns the deleted wiki. Recoverable
28714
+ * via restoreWiki within the retention window. */
28715
+ async deleteWiki(orgId, wikiId) {
28716
+ return this.request("DELETE", ENDPOINTS.WIKI(orgId, wikiId));
28717
+ }
28718
+ /** Restore a soft-deleted wiki (org owner only). 409 WIKI_PURGE_STARTED once
28719
+ * purge has been claimed (no longer restorable), or 404 once the row is gone.
28720
+ * Returns the restored wiki. */
28721
+ async restoreWiki(orgId, wikiId) {
28722
+ return this.request("POST", ENDPOINTS.WIKI_RESTORE(orgId, wikiId));
28723
+ }
28724
+ /** Recycle bin — list soft-deleted wikis for the org (owner only). */
28725
+ async getDeletedWikis(orgId) {
28726
+ const res = await this.request("GET", ENDPOINTS.WIKIS_DELETED(orgId));
28727
+ return res.data;
28728
+ }
28590
28729
  async getWikiTree(orgId, wikiId, params) {
28591
28730
  return this.request("GET", ENDPOINTS.WIKI_TREE(orgId, wikiId), void 0, params);
28592
28731
  }
@@ -28620,6 +28759,15 @@ var ParallClient = class _ParallClient {
28620
28759
  blob.signed_url = this.absoluteMediaUrl(blob.signed_url);
28621
28760
  return blob;
28622
28761
  }
28762
+ /**
28763
+ * Copy one wiki file to another path as an independent snapshot (no sync).
28764
+ * Used to "share a private file out": copy a file from the caller's personal
28765
+ * namespace (`users/{userId}/…`) into the public wiki, leaving the original
28766
+ * untouched. 409 `FILE_EXISTS` if `dest_path` already exists.
28767
+ */
28768
+ async copyWikiFile(orgId, wikiId, req) {
28769
+ return this.request("POST", ENDPOINTS.WIKI_COPY(orgId, wikiId), req);
28770
+ }
28623
28771
  async getWikiNodeSections(orgId, wikiId, params) {
28624
28772
  return this.request("GET", ENDPOINTS.WIKI_NODE_SECTIONS(orgId, wikiId), void 0, params);
28625
28773
  }
@@ -28893,6 +29041,19 @@ var ParallClient = class _ParallClient {
28893
29041
  const resp = await this.request("GET", ENDPOINTS.COMPUTE_PRICING());
28894
29042
  return resp.data;
28895
29043
  }
29044
+ /** Runtime capability table (public) — SSOT for the create/settings interlock. */
29045
+ async getRuntimes() {
29046
+ const resp = await this.request("GET", ENDPOINTS.RUNTIMES());
29047
+ return resp.data;
29048
+ }
29049
+ /**
29050
+ * Platform model catalog (public) — DB-backed, replaces the compile-time
29051
+ * PLATFORM_MODELS constant. Pass includeModel to keep an agent's pinned
29052
+ * hidden legacy model representable (settings picker).
29053
+ */
29054
+ async getModels(includeModel) {
29055
+ return this.request("GET", ENDPOINTS.MODELS(includeModel));
29056
+ }
28896
29057
  // ---- Clips ----
28897
29058
  async listClips(orgId) {
28898
29059
  const resp = await this.request("GET", ENDPOINTS.CLIPS(orgId));
@@ -28937,6 +29098,11 @@ var ParallClient = class _ParallClient {
28937
29098
  const resp = await this.request("GET", ENDPOINTS.CLIP_ONLINE(orgId));
28938
29099
  return resp.data;
28939
29100
  }
29101
+ /** Deployment-wide hosted browser runtime availability — drives whether the
29102
+ * create UI offers the platform-hosted placement. Fail-closed server-side. */
29103
+ async getBrowserRuntimeStatus(orgId) {
29104
+ return this.request("GET", ENDPOINTS.BROWSER_RUNTIME_STATUS(orgId));
29105
+ }
28940
29106
  /** Org-wide browser-profile discovery list. Returns the sanitized
28941
29107
  * {@link BrowserProfileListItem} shape (not the full domain model), each row
28942
29108
  * carrying a per-viewer `can_open` control hint. */
@@ -29596,6 +29762,9 @@ function resolveStepTarget(event) {
29596
29762
  if (event.type === "external_trigger" || event.targetId.startsWith("xtr_")) {
29597
29763
  return { target_type: "external_trigger", target_id: event.targetId };
29598
29764
  }
29765
+ if (event.type === "channel_message" || event.targetId.startsWith("chv_")) {
29766
+ return { target_type: "channel_conversation", target_id: event.targetId };
29767
+ }
29599
29768
  if (event.type === "wiki_comment") {
29600
29769
  return { target_type: "wiki", target_id: event.targetId || void 0 };
29601
29770
  }
@@ -29639,6 +29808,9 @@ var ParallAgentGateway = class {
29639
29808
  opts;
29640
29809
  chatInfoMap = /* @__PURE__ */ new Map();
29641
29810
  dispatchedTasks = /* @__PURE__ */ new Set();
29811
+ // connection id → provider alias, for channel_message prompt labeling
29812
+ // (stable mapping; avoids one connection fetch per inbound message).
29813
+ channelConnectionProviders = /* @__PURE__ */ new Map();
29642
29814
  dispatchedMessages = /* @__PURE__ */ new Set();
29643
29815
  forkStates = /* @__PURE__ */ new Map();
29644
29816
  dispatchState = {
@@ -29812,6 +29984,18 @@ var ParallAgentGateway = class {
29812
29984
  } catch (err) {
29813
29985
  this.opts.log?.error(`external trigger dispatch failed for ${data.source_id}: ${String(err)}`);
29814
29986
  }
29987
+ } else if (data.event_type === "channel_message") {
29988
+ if (!data.source_id)
29989
+ return;
29990
+ try {
29991
+ const dispatched = await this.fetchAndHandleChannelMessage(data.source_id);
29992
+ if (dispatched) {
29993
+ this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29994
+ });
29995
+ }
29996
+ } catch (err) {
29997
+ this.opts.log?.error(`channel message dispatch failed for ${data.source_id}: ${String(err)}`);
29998
+ }
29815
29999
  } else if (data.event_type === "approval_decided") {
29816
30000
  if (!data.source_id)
29817
30001
  return;
@@ -29890,12 +30074,17 @@ var ParallAgentGateway = class {
29890
30074
  target_type: target.target_type,
29891
30075
  target_id: target.target_id,
29892
30076
  content: {
29893
- trigger_type: event.type === "task" ? "task_assign" : event.type === "task_comment" ? "task_comment" : event.type === "wiki_comment" ? "wiki_comment" : event.type === "schedule" ? "schedule_fire" : event.type === "external_trigger" ? "external_trigger" : event.type === "approval" ? "approval_decided" : "mention",
30077
+ trigger_type: event.type === "task" ? "task_assign" : event.type === "task_comment" ? "task_comment" : event.type === "wiki_comment" ? "wiki_comment" : event.type === "schedule" ? "schedule_fire" : event.type === "external_trigger" ? "external_trigger" : event.type === "channel_message" ? "channel_message" : event.type === "approval" ? "approval_decided" : "mention",
29894
30078
  trigger_ref: event.type === "task" ? { task_id: event.targetId } : event.type === "task_comment" ? { comment_id: event.messageId, task_id: event.targetId } : event.type === "wiki_comment" ? { comment_id: event.messageId, target_uri: event.replyTargetUri } : event.type === "schedule" ? { schedule_id: event.targetId, run_id: event.messageId } : event.type === "external_trigger" ? {
29895
30079
  trigger_id: event.targetId,
29896
30080
  run_id: event.messageId,
29897
30081
  connection_id: event.externalConnectionId,
29898
30082
  ingress_event_id: event.externalIngressEventId
30083
+ } : event.type === "channel_message" ? {
30084
+ conversation_id: event.targetId,
30085
+ channel_message_id: event.messageId,
30086
+ provider: event.channelProvider,
30087
+ external_conversation_id: event.channelExternalConversationId
29899
30088
  } : event.type === "approval" ? { approval_id: event.messageId } : { message_id: event.messageId },
29900
30089
  sender_id: event.senderId,
29901
30090
  sender_name: event.senderName,
@@ -30146,7 +30335,15 @@ var ParallAgentGateway = class {
30146
30335
  context: dispatchContext
30147
30336
  })) {
30148
30337
  if (runtimeEvent.type === "runtime_session") {
30338
+ const priorAgentSessionId = binding?.agentSessionId;
30149
30339
  binding = await this.bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath);
30340
+ if (event.targetType === "channel_conversation" && binding.agentSessionId !== priorAgentSessionId) {
30341
+ try {
30342
+ await this.opts.client.setChannelConversationSession(this.opts.config.org_id, event.targetId, binding.agentSessionId);
30343
+ } catch (err) {
30344
+ this.opts.log?.warn(`failed to record session mapping for channel conversation ${event.targetId}: ${String(err)}`);
30345
+ }
30346
+ }
30150
30347
  if (!inputStepsCreated) {
30151
30348
  if (earlierEvents.length > 0) {
30152
30349
  await this.createInputStepsForEarlierEvents(binding.agentSessionId, earlierEvents);
@@ -30949,6 +31146,75 @@ var ParallAgentGateway = class {
30949
31146
  return true;
30950
31147
  return this.handleExternalTriggerRun(run);
30951
31148
  }
31149
+ // fetchAndHandleChannelMessage resolves a channel_message dispatch to its
31150
+ // durable ChannelMessage + conversation and hands it to the inbound
31151
+ // pipeline. targetId = the ChannelConversation id, so per-conversation
31152
+ // multi-turn continuity rides the same per-target session mechanics as
31153
+ // chats. Design: docs/engineering-design/external-im-channel-design.md.
31154
+ async fetchAndHandleChannelMessage(messageId) {
31155
+ if (this.shuttingDown)
31156
+ return false;
31157
+ const claimKey = `channel_message:${messageId}`;
31158
+ if (!this.tryClaimMessage(claimKey))
31159
+ return false;
31160
+ let msg = null;
31161
+ let conv = null;
31162
+ try {
31163
+ msg = await this.opts.client.getChannelMessage(this.opts.config.org_id, messageId);
31164
+ conv = await this.opts.client.getChannelConversation(this.opts.config.org_id, msg.conversation_id);
31165
+ } catch (err) {
31166
+ const status = err?.status;
31167
+ if (status === 404) {
31168
+ this.opts.log?.warn(`channel message ${messageId} not accessible (404), acking stale dispatch`);
31169
+ return true;
31170
+ }
31171
+ this.dispatchedMessages.delete(claimKey);
31172
+ this.opts.log?.warn(`channel message fetch failed for ${messageId}, leaving pending: ${String(err)}`);
31173
+ return false;
31174
+ }
31175
+ if (!msg || !conv) {
31176
+ return true;
31177
+ }
31178
+ this.opts.log?.info(`channel message: ${msg.id} (conversation ${conv.id})`);
31179
+ let provider = this.channelConnectionProviders.get(conv.connection_id);
31180
+ if (!provider) {
31181
+ try {
31182
+ const connection = await this.opts.client.getChannelConnection(this.opts.config.org_id, conv.connection_id);
31183
+ provider = connection.provider;
31184
+ this.channelConnectionProviders.set(conv.connection_id, provider);
31185
+ } catch {
31186
+ provider = void 0;
31187
+ }
31188
+ }
31189
+ const event = {
31190
+ type: "channel_message",
31191
+ targetId: conv.id,
31192
+ targetName: conv.external_user_name || conv.external_conversation_id,
31193
+ targetType: "channel_conversation",
31194
+ senderId: msg.external_user_id || "external",
31195
+ senderName: msg.external_user_name || msg.external_user_id || "external user",
31196
+ messageId: msg.id,
31197
+ body: msg.text,
31198
+ sentAt: msg.received_at,
31199
+ channelProvider: provider,
31200
+ channelConversationType: conv.conversation_type || void 0,
31201
+ channelExternalConversationId: conv.external_conversation_id,
31202
+ channelExternalMessageId: msg.external_message_id,
31203
+ ackSourceType: "channel_message",
31204
+ ackSourceId: msg.id
31205
+ };
31206
+ let dispatched;
31207
+ try {
31208
+ dispatched = await this.handleInboundEvent(event);
31209
+ } catch (err) {
31210
+ this.dispatchedMessages.delete(claimKey);
31211
+ throw err;
31212
+ }
31213
+ if (!dispatched) {
31214
+ this.dispatchedMessages.delete(claimKey);
31215
+ }
31216
+ return dispatched;
31217
+ }
30952
31218
  async handleExternalTriggerRun(run) {
30953
31219
  if (this.shuttingDown)
30954
31220
  return false;
@@ -31100,6 +31366,8 @@ var ParallAgentGateway = class {
31100
31366
  dispatched = await this.fetchAndHandleScheduleFire(item.source_id, item.actor_id);
31101
31367
  } else if (item.event_type === "external_trigger" && item.source_id) {
31102
31368
  dispatched = await this.fetchAndHandleExternalTriggerRun(item.source_id);
31369
+ } else if (item.event_type === "channel_message" && item.source_id) {
31370
+ dispatched = await this.fetchAndHandleChannelMessage(item.source_id);
31103
31371
  } else if (item.event_type === "approval_decided" && item.source_id) {
31104
31372
  dispatched = await this.fetchAndHandleApprovalDecided(item.source_id, item.actor_id, item.chat_id ?? null);
31105
31373
  } else if (item.event_type === "message" && item.source_id && item.chat_id) {
@@ -33117,9 +33385,9 @@ To respond, you **must** use the Parall CLI via the exec (Bash) tool. If \`paral
33117
33385
 
33118
33386
  ### Event types and where to reply
33119
33387
 
33120
- - **\`[Event: message.new]\`** \u2014 includes \`[Chat: ... (prll://cht_xxx)]\`. Reply into that chat:
33388
+ - **\`[Event: message.new]\`** \u2014 includes \`[Chat: ... (prll://cht_xxx)]\`. Reply into that chat. Write the reply with your file tool first (shell-safe), then send it \u2014 don't wrap message content in \`--text "..."\`, since the shell expands \`$1,000\`\u2192\`,000\` and runs \`$(...)\` inside double quotes:
33121
33389
 
33122
- parall messages send prll://cht_xxx --text "Your reply here"
33390
+ parall messages send prll://cht_xxx --text-file /tmp/reply.md
33123
33391
 
33124
33392
  - **\`[Event: task.assigned]\` / \`[Event: task.comment.created]\`** \u2014 includes \`[Task: ... (prll://tsk_xxx)]\`. Act on the task; use task CLI subcommands (\`tasks update\`, \`tasks comment\`). See the \`parall-tasks\` skill.
33125
33393
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/parall",
3
- "version": "1.36.1",
3
+ "version": "1.37.0",
4
4
  "description": "OpenClaw channel plugin for Parall IM",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -16,8 +16,8 @@
16
16
  "openclaw.plugin.json"
17
17
  ],
18
18
  "dependencies": {
19
- "@parall/agent-core": "1.36.1",
20
- "@parall/sdk": "1.36.1"
19
+ "@parall/sdk": "1.37.0",
20
+ "@parall/agent-core": "1.37.0"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/node": "^22.0.0",
@@ -103,16 +103,35 @@ original discussion and its approval/rejection IS the precedent.
103
103
 
104
104
  Each `[Event: message.new]` includes `[Chat: ... (prll://cht_xxx)]` — use that chat URI to reply.
105
105
 
106
+ > **How you pass the message body matters — your command runs through a shell.**
107
+ > Inside double quotes the shell expands `$`, backticks, and `$(...)` *before*
108
+ > the CLI sees them: `--text "That costs $1,000"` sends `That costs ,000`, and
109
+ > `--text "$(cmd)"` runs `cmd`. Single quotes instead break on apostrophes
110
+ > (`I'm`, `don't`). So do **not** wrap real message content in quotes — pass it
111
+ > through `--text-file` (a written file, or a quoted heredoc `<<'EOF'` that
112
+ > disables all expansion). Reserve `--text "..."` for short literals with no
113
+ > `$`, backtick, or apostrophe.
114
+
106
115
  ```bash
107
- # Reply to a chat (use the chat URI from the event)
108
- parall messages send prll://cht_xxx --text "Your reply"
116
+ # One-off reply quoted heredoc into stdin. The quoted delimiter <<'EOF'
117
+ # disables ALL shell expansion, so $, backticks and apostrophes pass verbatim.
118
+ parall messages send prll://cht_xxx --text-file - <<'PARALL_EOF'
119
+ Sure — that's $1,000, and $(whoami) stays literal. I'm on it.
120
+ PARALL_EOF
121
+
122
+ # Longer / multi-line reply → write it with your file tool (no shell touches
123
+ # the body), then point --text-file at the file.
124
+ parall messages send prll://cht_xxx --text-file /tmp/reply.md
125
+
126
+ # Short literal with no $, backtick, or apostrophe → --text is fine.
127
+ parall messages send prll://cht_xxx --text "On it"
109
128
 
110
- # Direct message by user URI or display name
111
- parall dm prll://usr_xxx --text "Hello"
129
+ # Direct message by user URI or display name (same --text-file / heredoc rules)
130
+ parall dm prll://usr_xxx --text-file /tmp/reply.md
112
131
  parall dm "Alice" --text "Hello"
113
132
 
114
133
  # Thread reply
115
- parall messages send prll://cht_xxx --text "Reply" --thread-root-id 01JWC...
134
+ parall messages send prll://cht_xxx --text-file /tmp/reply.md --thread-root-id 01JWC...
116
135
 
117
136
  # FYI message (no response expected — the recipient sees `[Hint: no_reply]`)
118
137
  parall messages send prll://cht_xxx --text "FYI: done" --no-reply
@@ -144,7 +163,9 @@ parall messages send prll://cht_xxx --attachment att_xxx --text "See attached"
144
163
  parall dm "Alice" --file /tmp/report.pdf --text "Report attached"
145
164
  ```
146
165
 
147
- `--file` and `--attachment` are mutually exclusive. `--text` can be combined with either.
166
+ `--file` and `--attachment` are mutually exclusive. A caption (`--text` for
167
+ short literals, or `--text-file` for anything with `$`, backticks, or quotes)
168
+ can be combined with either.
148
169
 
149
170
  ## Approvals
150
171
 
package/src/hooks.ts CHANGED
@@ -90,9 +90,9 @@ To respond, you **must** use the Parall CLI via the exec (Bash) tool. If \`paral
90
90
 
91
91
  ### Event types and where to reply
92
92
 
93
- - **\`[Event: message.new]\`** — includes \`[Chat: ... (prll://cht_xxx)]\`. Reply into that chat:
93
+ - **\`[Event: message.new]\`** — includes \`[Chat: ... (prll://cht_xxx)]\`. Reply into that chat. Write the reply with your file tool first (shell-safe), then send it — don't wrap message content in \`--text "..."\`, since the shell expands \`$1,000\`→\`,000\` and runs \`$(...)\` inside double quotes:
94
94
 
95
- parall messages send prll://cht_xxx --text "Your reply here"
95
+ parall messages send prll://cht_xxx --text-file /tmp/reply.md
96
96
 
97
97
  - **\`[Event: task.assigned]\` / \`[Event: task.comment.created]\`** — includes \`[Task: ... (prll://tsk_xxx)]\`. Act on the task; use task CLI subcommands (\`tasks update\`, \`tasks comment\`). See the \`parall-tasks\` skill.
98
98