@parall/parall 1.36.0 → 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`,
@@ -27263,6 +27305,7 @@ var ENDPOINTS = {
27263
27305
  // References (org-scoped)
27264
27306
  REFS_RESOLVE: (orgId) => `${API_BASE}/orgs/${orgId}/refs/resolve`,
27265
27307
  REFS_BACKLINKS: (orgId) => `${API_BASE}/orgs/${orgId}/refs/backlinks`,
27308
+ REFS_GRAPH: (orgId) => `${API_BASE}/orgs/${orgId}/refs/graph`,
27266
27309
  REFS_CHECK: (orgId) => `${API_BASE}/orgs/${orgId}/refs/check`,
27267
27310
  // Platform config (agent-scoped, not org-scoped)
27268
27311
  PLATFORM_CONFIG: `${API_BASE}/agents/platform-config`,
@@ -27283,6 +27326,14 @@ var ENDPOINTS = {
27283
27326
  BILLING_AUTO_RELOAD: (orgId) => `${API_BASE}/orgs/${orgId}/billing/auto-reload`,
27284
27327
  BILLING_SETUP_INTENT: (orgId) => `${API_BASE}/orgs/${orgId}/billing/setup-intent`,
27285
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`,
27286
27337
  // ADMIN_GRANTS intentionally NOT exported here — it sits under the
27287
27338
  // unauthenticated `/internal/admin/*` surface and must not bleed into the
27288
27339
  // public SDK. Admin dashboard hits the URL directly from its own client.
@@ -27295,6 +27346,7 @@ var ENDPOINTS = {
27295
27346
  AGENT_CLIP: (orgId, agentId, clipId) => `${CLIP_BASE}/orgs/${orgId}/agents/${agentId}/clips/${clipId}`,
27296
27347
  CLIP_INVOKE: (orgId) => `${CLIP_BASE}/orgs/${orgId}/invoke`,
27297
27348
  CLIP_ONLINE: (orgId) => `${CLIP_BASE}/orgs/${orgId}/online`,
27349
+ BROWSER_RUNTIME_STATUS: (orgId) => `${CLIP_BASE}/orgs/${orgId}/browser-runtime/status`,
27298
27350
  BROWSER_PROFILES: (orgId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles`,
27299
27351
  BROWSER_PROFILE: (orgId, profileId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles/${profileId}`,
27300
27352
  BROWSER_PROFILE_OPEN: (orgId, profileId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles/${profileId}/open`,
@@ -27348,10 +27400,13 @@ var WS_EVENTS = {
27348
27400
  INVITATION_REVOKED: "invitation.revoked",
27349
27401
  ORG_JOIN_REQUEST_NEW: "org.join_request.new",
27350
27402
  ORG_INVITE_LINK_JOINED: "org.invite_link.joined",
27403
+ ORG_MEMBER_REMOVED: "org.member.removed",
27351
27404
  AGENT_CONFIG_UPDATE: "agent_config.update",
27352
27405
  PRESENCE_UPDATE: "presence.update",
27353
27406
  WIKI_CHANGESET_CREATED: "wiki.changeset.created",
27354
27407
  WIKI_CHANGESET_UPDATED: "wiki.changeset.updated",
27408
+ WIKI_DELETED: "wiki.deleted",
27409
+ WIKI_RESTORED: "wiki.restored",
27355
27410
  COMMENT_CREATED: "comment.created",
27356
27411
  COMMENT_UPDATED: "comment.updated",
27357
27412
  COMMENT_DELETED: "comment.deleted",
@@ -27379,6 +27434,7 @@ var WS_EVENTS = {
27379
27434
  MACHINE_FILESYSTEM_BROWSE: "machine.filesystem.browse",
27380
27435
  MACHINE_UPDATE: "machine.update",
27381
27436
  MACHINE_CONFIG_UPDATED: "machine.config.updated",
27437
+ MACHINE_AGENT_CONFIG_UPDATED: "machine.agent_config.updated",
27382
27438
  MACHINE_CLIP_SYNC: "machine.clip.sync",
27383
27439
  MACHINE_BROWSER_PROFILE_LIFECYCLE: "machine.browser_profile.lifecycle",
27384
27440
  MACHINE_BROWSER_PROFILE_VIEWER: "machine.browser_profile.viewer",
@@ -27718,6 +27774,13 @@ var ParallClient = class _ParallClient {
27718
27774
  const res = await this.request("GET", ENDPOINTS.ORG_MEMBERS(orgId));
27719
27775
  return res.data;
27720
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
+ }
27721
27784
  async getTeams(orgId) {
27722
27785
  const res = await this.request("GET", ENDPOINTS.TEAMS(orgId));
27723
27786
  return res.data;
@@ -28199,26 +28262,32 @@ var ParallClient = class _ParallClient {
28199
28262
  const res = await this.request("GET", ENDPOINTS.MACHINES_ME_CLIPS);
28200
28263
  return res.data;
28201
28264
  }
28202
- /** `GET /machines/me/browser-profiles` — browser profiles hosted by this machine. */
28265
+ /** `GET /machines/me/browser-profiles` — browser profiles hosted by this machine.
28266
+ * Returns the daemon DTO (carries proxy_password for bb-browser replay). */
28203
28267
  async listMachineBrowserProfiles() {
28204
28268
  const res = await this.request("GET", ENDPOINTS.MACHINES_ME_BROWSER_PROFILES);
28205
28269
  return res.data;
28206
28270
  }
28207
28271
  /** `POST /machines/me/browser-profiles/{profileId}/status` — report runtime status. */
28208
- async reportBrowserProfileStatus(profileId, status, errorMsg) {
28272
+ async reportBrowserProfileStatus(profileId, status, errorMsg, generation) {
28209
28273
  await this.request("POST", ENDPOINTS.MACHINES_ME_BROWSER_PROFILE_STATUS(profileId), {
28210
28274
  status,
28211
- ...errorMsg ? { error_msg: errorMsg } : {}
28275
+ ...errorMsg ? { error_msg: errorMsg } : {},
28276
+ ...generation !== void 0 ? { generation } : {}
28212
28277
  });
28213
28278
  }
28214
28279
  /**
28215
28280
  * `POST /machines/me/health` — bump the Machine's `updated_at` to now and
28216
- * report daemon state. The daemon should call this on a fixed cadence (e.g.
28217
- * 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
28218
28284
  * fields are optional and only persisted when changed:
28219
28285
  * - `daemonVersion` — the running bundle/launcher version.
28220
28286
  * - `selfUpdateCapable` — whether the daemon can act on a machine.update
28221
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).
28222
28291
  */
28223
28292
  async postMachineHeartbeat(opts) {
28224
28293
  const body = {};
@@ -28226,6 +28295,8 @@ var ParallClient = class _ParallClient {
28226
28295
  body.daemon_version = opts.daemonVersion;
28227
28296
  if (opts?.selfUpdateCapable !== void 0)
28228
28297
  body.self_update_capable = opts.selfUpdateCapable;
28298
+ if (opts?.detectedRuntimes !== void 0)
28299
+ body.detected_runtimes = opts.detectedRuntimes;
28229
28300
  return this.request("POST", ENDPOINTS.MACHINES_ME_HEALTH, Object.keys(body).length > 0 ? body : void 0);
28230
28301
  }
28231
28302
  async reportAgentWorkspaceState(agentId, state) {
@@ -28544,6 +28615,59 @@ var ParallClient = class _ParallClient {
28544
28615
  async deleteExternalConnection(orgId, connectionId) {
28545
28616
  return this.request("DELETE", ENDPOINTS.EXTERNAL_CONNECTION(orgId, connectionId));
28546
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
+ }
28547
28671
  async getExternalTriggerSchema(orgId, connectionId) {
28548
28672
  return this.request("GET", ENDPOINTS.EXTERNAL_TRIGGER_SCHEMA(orgId, connectionId));
28549
28673
  }
@@ -28585,6 +28709,23 @@ var ParallClient = class _ParallClient {
28585
28709
  async getWiki(orgId, wikiId) {
28586
28710
  return this.request("GET", ENDPOINTS.WIKI(orgId, wikiId));
28587
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
+ }
28588
28729
  async getWikiTree(orgId, wikiId, params) {
28589
28730
  return this.request("GET", ENDPOINTS.WIKI_TREE(orgId, wikiId), void 0, params);
28590
28731
  }
@@ -28618,6 +28759,15 @@ var ParallClient = class _ParallClient {
28618
28759
  blob.signed_url = this.absoluteMediaUrl(blob.signed_url);
28619
28760
  return blob;
28620
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
+ }
28621
28771
  async getWikiNodeSections(orgId, wikiId, params) {
28622
28772
  return this.request("GET", ENDPOINTS.WIKI_NODE_SECTIONS(orgId, wikiId), void 0, params);
28623
28773
  }
@@ -28786,6 +28936,16 @@ var ParallClient = class _ParallClient {
28786
28936
  async getBacklinks(orgId, params) {
28787
28937
  return this.request("GET", ENDPOINTS.REFS_BACKLINKS(orgId), void 0, params);
28788
28938
  }
28939
+ /**
28940
+ * Bounded multi-hop walk of the prll:// reference graph around `uri`. `uri`
28941
+ * must be an entity-level prll:// URI — a refined URI (path/query/fragment) is
28942
+ * rejected with 400 UNSUPPORTED_REFINED_URI. `depth` is clamped server-side to
28943
+ * [1, 4]; breadth (node/edge counts) is capped server-side and surfaced via
28944
+ * `truncated`. ACL is applied per hop (chat membership + wiki path scope).
28945
+ */
28946
+ async getRefsGraph(orgId, params) {
28947
+ return this.request("GET", ENDPOINTS.REFS_GRAPH(orgId), void 0, params);
28948
+ }
28789
28949
  async checkBrokenRefs(orgId) {
28790
28950
  return this.request("GET", ENDPOINTS.REFS_CHECK(orgId));
28791
28951
  }
@@ -28881,6 +29041,19 @@ var ParallClient = class _ParallClient {
28881
29041
  const resp = await this.request("GET", ENDPOINTS.COMPUTE_PRICING());
28882
29042
  return resp.data;
28883
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
+ }
28884
29057
  // ---- Clips ----
28885
29058
  async listClips(orgId) {
28886
29059
  const resp = await this.request("GET", ENDPOINTS.CLIPS(orgId));
@@ -28925,6 +29098,11 @@ var ParallClient = class _ParallClient {
28925
29098
  const resp = await this.request("GET", ENDPOINTS.CLIP_ONLINE(orgId));
28926
29099
  return resp.data;
28927
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
+ }
28928
29106
  /** Org-wide browser-profile discovery list. Returns the sanitized
28929
29107
  * {@link BrowserProfileListItem} shape (not the full domain model), each row
28930
29108
  * carrying a per-viewer `can_open` control hint. */
@@ -29584,6 +29762,9 @@ function resolveStepTarget(event) {
29584
29762
  if (event.type === "external_trigger" || event.targetId.startsWith("xtr_")) {
29585
29763
  return { target_type: "external_trigger", target_id: event.targetId };
29586
29764
  }
29765
+ if (event.type === "channel_message" || event.targetId.startsWith("chv_")) {
29766
+ return { target_type: "channel_conversation", target_id: event.targetId };
29767
+ }
29587
29768
  if (event.type === "wiki_comment") {
29588
29769
  return { target_type: "wiki", target_id: event.targetId || void 0 };
29589
29770
  }
@@ -29627,6 +29808,9 @@ var ParallAgentGateway = class {
29627
29808
  opts;
29628
29809
  chatInfoMap = /* @__PURE__ */ new Map();
29629
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();
29630
29814
  dispatchedMessages = /* @__PURE__ */ new Set();
29631
29815
  forkStates = /* @__PURE__ */ new Map();
29632
29816
  dispatchState = {
@@ -29800,6 +29984,18 @@ var ParallAgentGateway = class {
29800
29984
  } catch (err) {
29801
29985
  this.opts.log?.error(`external trigger dispatch failed for ${data.source_id}: ${String(err)}`);
29802
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
+ }
29803
29999
  } else if (data.event_type === "approval_decided") {
29804
30000
  if (!data.source_id)
29805
30001
  return;
@@ -29878,12 +30074,17 @@ var ParallAgentGateway = class {
29878
30074
  target_type: target.target_type,
29879
30075
  target_id: target.target_id,
29880
30076
  content: {
29881
- 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",
29882
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" ? {
29883
30079
  trigger_id: event.targetId,
29884
30080
  run_id: event.messageId,
29885
30081
  connection_id: event.externalConnectionId,
29886
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
29887
30088
  } : event.type === "approval" ? { approval_id: event.messageId } : { message_id: event.messageId },
29888
30089
  sender_id: event.senderId,
29889
30090
  sender_name: event.senderName,
@@ -30134,7 +30335,15 @@ var ParallAgentGateway = class {
30134
30335
  context: dispatchContext
30135
30336
  })) {
30136
30337
  if (runtimeEvent.type === "runtime_session") {
30338
+ const priorAgentSessionId = binding?.agentSessionId;
30137
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
+ }
30138
30347
  if (!inputStepsCreated) {
30139
30348
  if (earlierEvents.length > 0) {
30140
30349
  await this.createInputStepsForEarlierEvents(binding.agentSessionId, earlierEvents);
@@ -30937,6 +31146,75 @@ var ParallAgentGateway = class {
30937
31146
  return true;
30938
31147
  return this.handleExternalTriggerRun(run);
30939
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
+ }
30940
31218
  async handleExternalTriggerRun(run) {
30941
31219
  if (this.shuttingDown)
30942
31220
  return false;
@@ -31088,6 +31366,8 @@ var ParallAgentGateway = class {
31088
31366
  dispatched = await this.fetchAndHandleScheduleFire(item.source_id, item.actor_id);
31089
31367
  } else if (item.event_type === "external_trigger" && item.source_id) {
31090
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);
31091
31371
  } else if (item.event_type === "approval_decided" && item.source_id) {
31092
31372
  dispatched = await this.fetchAndHandleApprovalDecided(item.source_id, item.actor_id, item.chat_id ?? null);
31093
31373
  } else if (item.event_type === "message" && item.source_id && item.chat_id) {
@@ -33105,9 +33385,9 @@ To respond, you **must** use the Parall CLI via the exec (Bash) tool. If \`paral
33105
33385
 
33106
33386
  ### Event types and where to reply
33107
33387
 
33108
- - **\`[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:
33109
33389
 
33110
- parall messages send prll://cht_xxx --text "Your reply here"
33390
+ parall messages send prll://cht_xxx --text-file /tmp/reply.md
33111
33391
 
33112
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.
33113
33393
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/parall",
3
- "version": "1.36.0",
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/sdk": "1.36.0",
20
- "@parall/agent-core": "1.36.0"
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",
@@ -69,22 +69,69 @@ parall machines logs prll://mch_xxx --lines 100
69
69
  ```bash
70
70
  parall chats list # List all chats
71
71
  parall messages list prll://cht_xxx # Read chat message history
72
+ parall messages list prll://cht_xxx --since 2026-01-01 # Only messages at/after a date (RFC3339 or YYYY-MM-DD)
72
73
  ```
73
74
 
75
+ ## Org-Context Search
76
+
77
+ Before deciding or starting non-trivial work, search the org's real history —
78
+ past discussions, decisions, tasks, and wiki notes — so you don't re-litigate
79
+ settled questions or repeat known mistakes. This searches live org data
80
+ (semantic + keyword), not a local copy, and is permission-filtered to what you
81
+ can see.
82
+
83
+ ```bash
84
+ # Semantic + keyword search across messages, tasks, and wiki
85
+ parall search "auth v5 upgrade"
86
+
87
+ # Restrict entity types (m=message, t=task, w=wiki). --channel narrows the
88
+ # MESSAGE hits to one chat (tasks/wiki are unaffected by it).
89
+ parall search "auth v5 upgrade" --types m,w --channel prll://cht_eng
90
+
91
+ # Time-box to recent activity (RFC3339 or YYYY-MM-DD). Narrows messages + tasks;
92
+ # wiki is always matched by relevance (the index has no authored timestamp).
93
+ parall search "auth v5 upgrade" --since 2026-01-01
94
+
95
+ # Narrow wiki hits to a frontmatter document type
96
+ parall search "deploy steps" --types w --wiki-type Runbook
97
+ ```
98
+
99
+ Even with zero curated notes, the raw message + task history is searchable — the
100
+ original discussion and its approval/rejection IS the precedent.
101
+
74
102
  ## Sending Messages
75
103
 
76
104
  Each `[Event: message.new]` includes `[Chat: ... (prll://cht_xxx)]` — use that chat URI to reply.
77
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
+
78
115
  ```bash
79
- # Reply to a chat (use the chat URI from the event)
80
- 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
81
121
 
82
- # Direct message by user URI or display name
83
- parall dm prll://usr_xxx --text "Hello"
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"
128
+
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
84
131
  parall dm "Alice" --text "Hello"
85
132
 
86
133
  # Thread reply
87
- 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...
88
135
 
89
136
  # FYI message (no response expected — the recipient sees `[Hint: no_reply]`)
90
137
  parall messages send prll://cht_xxx --text "FYI: done" --no-reply
@@ -116,7 +163,9 @@ parall messages send prll://cht_xxx --attachment att_xxx --text "See attached"
116
163
  parall dm "Alice" --file /tmp/report.pdf --text "Report attached"
117
164
  ```
118
165
 
119
- `--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.
120
169
 
121
170
  ## Approvals
122
171
 
@@ -172,4 +221,26 @@ When a message or event references `prll://sch_xxx` or `prll://srn_xxx`, or when
172
221
 
173
222
  When a message or event references `prll://xcn_xxx`, `prll://xin_xxx`, `prll://xtr_xxx`, or `prll://xrn_xxx`, or when you receive `[Event: external.trigger]`, switch to the **parall-external-triggers** skill for the CLI commands (connections / triggers / events / runs).
174
223
 
224
+ ## References (relationship graph)
225
+
226
+ `prll://` references between entities form a graph — a message cites a task, a
227
+ task cites a wiki page, and so on. Walk it to answer "what is this decision /
228
+ entity connected to". All results are permission-filtered to what you can see.
229
+
230
+ ```bash
231
+ # Resolve URIs to entity metadata (titles, status, previews)
232
+ parall refs resolve prll://tsk_xxx prll://wik_xxx
233
+
234
+ # Single hop — who references X
235
+ parall refs backlinks prll://tsk_xxx
236
+
237
+ # Multi-hop — the connected sub-graph around X (entity-level URI only — no
238
+ # path/anchor; depth 1–4, default 2)
239
+ parall refs graph prll://tsk_xxx --depth 2
240
+ ```
241
+
242
+ `refs graph` traverses both directions (inbound + outbound) and returns `nodes`
243
+ and `edges` with each node's hop `depth`. `truncated: true` means a size cap clipped
244
+ the result — narrow it with a smaller `--depth`.
245
+
175
246
  CLI success output is JSON. Errors print a JSON line (`{"error","status","code",...}`) and, on a `PERMISSION_DENIED`, may add a plain-text `Request approval:` line — read both.
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