@parall/parall 1.35.0 → 1.36.1

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.
@@ -26662,6 +26662,25 @@ 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 === "external_trigger") {
26666
+ lines.push(`[Event: external.trigger]`);
26667
+ lines.push(`[Trigger: prll://${event.targetId}]`);
26668
+ lines.push(`[Run: prll://${event.messageId}]`);
26669
+ if (event.externalConnectionId) {
26670
+ const label = event.externalConnectionDisplayName ? `${sanitizeMeta(event.externalConnectionDisplayName)} (prll://${event.externalConnectionId})` : `prll://${event.externalConnectionId}`;
26671
+ lines.push(`[Connection: ${label}]`);
26672
+ }
26673
+ if (event.externalIngressEventId)
26674
+ lines.push(`[Ingress: prll://${event.externalIngressEventId}]`);
26675
+ if (event.attachedUri)
26676
+ lines.push(`[Attached: ${sanitizeMeta(event.attachedUri)}]`);
26677
+ if (event.externalConnectionSourceType) {
26678
+ lines.push(`[Source: ${sanitizeMeta(event.externalConnectionSourceType)}]`);
26679
+ }
26680
+ if (event.externalIngressEventType) {
26681
+ lines.push(`[External event: ${sanitizeMeta(event.externalIngressEventType)}]`);
26682
+ }
26683
+ lines.push("", event.body);
26665
26684
  } else {
26666
26685
  lines.push(`[Event: task.assigned]`);
26667
26686
  const taskLabel = event.targetName ? `${event.targetName} (prll://${event.targetId})` : `prll://${event.targetId}`;
@@ -26693,6 +26712,10 @@ function buildSendMessageHint(event) {
26693
26712
  if (event.targetId.startsWith("sch_")) {
26694
26713
  return `
26695
26714
  <system-reminder>To communicate, use the CLI: \`parall messages send\` / \`parall dm\`. Your plain text output is not delivered.</system-reminder>`;
26715
+ }
26716
+ if (event.type === "external_trigger" || event.targetId.startsWith("xtr_")) {
26717
+ return `
26718
+ <system-reminder>This external trigger is incoming-only. Your plain text output is not sent back to the external provider. To communicate in Parall, use \`parall messages send\` / \`parall dm\`; provider-specific outbound actions require a separate capability.</system-reminder>`;
26696
26719
  }
26697
26720
  return "";
26698
26721
  }
@@ -27148,6 +27171,17 @@ var ENDPOINTS = {
27148
27171
  SCHEDULE_CANCEL: (orgId, id) => `${API_BASE}/orgs/${orgId}/schedules/${id}/cancel`,
27149
27172
  SCHEDULE_RUNS: (orgId, id) => `${API_BASE}/orgs/${orgId}/schedules/${id}/runs`,
27150
27173
  SCHEDULE_RUN: (orgId, runId) => `${API_BASE}/orgs/${orgId}/schedule_runs/${runId}`,
27174
+ // External triggers (org-scoped incoming integration primitive)
27175
+ EXTERNAL_CONNECTIONS: (orgId) => `${API_BASE}/orgs/${orgId}/external-connections`,
27176
+ EXTERNAL_CONNECTION: (orgId, connectionId) => `${API_BASE}/orgs/${orgId}/external-connections/${connectionId}`,
27177
+ EXTERNAL_CONNECTION_INGRESS_TOKEN_REGENERATE: (orgId, connectionId) => `${API_BASE}/orgs/${orgId}/external-connections/${connectionId}/ingress-token/regenerate`,
27178
+ EXTERNAL_TRIGGER_SCHEMA: (orgId, connectionId) => `${API_BASE}/orgs/${orgId}/external-connections/${connectionId}/trigger-schema`,
27179
+ EXTERNAL_INGRESS_EVENTS: (orgId) => `${API_BASE}/orgs/${orgId}/external-ingress-events`,
27180
+ EXTERNAL_INGRESS_EVENT: (orgId, eventId) => `${API_BASE}/orgs/${orgId}/external-ingress-events/${eventId}`,
27181
+ EXTERNAL_TRIGGERS: (orgId) => `${API_BASE}/orgs/${orgId}/external-triggers`,
27182
+ EXTERNAL_TRIGGER: (orgId, triggerId) => `${API_BASE}/orgs/${orgId}/external-triggers/${triggerId}`,
27183
+ EXTERNAL_TRIGGER_RUNS: (orgId) => `${API_BASE}/orgs/${orgId}/external-trigger-runs`,
27184
+ EXTERNAL_TRIGGER_RUN: (orgId, runId) => `${API_BASE}/orgs/${orgId}/external-trigger-runs/${runId}`,
27151
27185
  // Invitations (org-scoped, admin)
27152
27186
  ORG_INVITATIONS: (orgId) => `${API_BASE}/orgs/${orgId}/invitations`,
27153
27187
  ORG_INVITATION: (orgId, invId) => `${API_BASE}/orgs/${orgId}/invitations/${invId}`,
@@ -27229,6 +27263,7 @@ var ENDPOINTS = {
27229
27263
  // References (org-scoped)
27230
27264
  REFS_RESOLVE: (orgId) => `${API_BASE}/orgs/${orgId}/refs/resolve`,
27231
27265
  REFS_BACKLINKS: (orgId) => `${API_BASE}/orgs/${orgId}/refs/backlinks`,
27266
+ REFS_GRAPH: (orgId) => `${API_BASE}/orgs/${orgId}/refs/graph`,
27232
27267
  REFS_CHECK: (orgId) => `${API_BASE}/orgs/${orgId}/refs/check`,
27233
27268
  // Platform config (agent-scoped, not org-scoped)
27234
27269
  PLATFORM_CONFIG: `${API_BASE}/agents/platform-config`,
@@ -27357,6 +27392,7 @@ var WS_EVENTS = {
27357
27392
  // ../sdk/dist/client.js
27358
27393
  var ParallClient = class _ParallClient {
27359
27394
  baseUrl;
27395
+ wikiBaseUrl;
27360
27396
  token;
27361
27397
  onTokenExpired;
27362
27398
  getRefreshToken;
@@ -27408,12 +27444,22 @@ var ParallClient = class _ParallClient {
27408
27444
  }
27409
27445
  constructor(options = {}) {
27410
27446
  this.baseUrl = options.baseUrl ?? "";
27447
+ this.wikiBaseUrl = options.wikiBaseUrl ?? this.baseUrl;
27411
27448
  this.token = options.token ?? null;
27412
27449
  this.onTokenExpired = options.onTokenExpired;
27413
27450
  this.getRefreshToken = options.getRefreshToken;
27414
27451
  this.setTokens = options.setTokens;
27415
27452
  this.swimlaneName = options.swimlaneName;
27416
27453
  }
27454
+ /**
27455
+ * Pick the origin for a request path: wiki-service base for `/wiki/v1`
27456
+ * endpoints, api base for everything else. The path itself (from ENDPOINTS)
27457
+ * is authoritative, so wiki vs api routing can't drift from how a caller
27458
+ * happens to invoke the client.
27459
+ */
27460
+ baseUrlFor(path7) {
27461
+ return path7.startsWith(WIKI_BASE) ? this.wikiBaseUrl : this.baseUrl;
27462
+ }
27417
27463
  setToken(token) {
27418
27464
  this.token = token;
27419
27465
  }
@@ -27478,7 +27524,7 @@ var ParallClient = class _ParallClient {
27478
27524
  if (!retried) {
27479
27525
  await this.ensureFreshToken(path7);
27480
27526
  }
27481
- let url = `${this.baseUrl}${path7}`;
27527
+ let url = `${this.baseUrlFor(path7)}${path7}`;
27482
27528
  if (query) {
27483
27529
  const params = new URLSearchParams();
27484
27530
  for (const [key, value] of Object.entries(query)) {
@@ -27550,7 +27596,7 @@ var ParallClient = class _ParallClient {
27550
27596
  void _drop;
27551
27597
  let res;
27552
27598
  try {
27553
- res = await fetch(`${this.baseUrl}${path7}`, {
27599
+ res = await fetch(`${this.baseUrlFor(path7)}${path7}`, {
27554
27600
  method,
27555
27601
  headers,
27556
27602
  body,
@@ -27710,6 +27756,21 @@ var ParallClient = class _ParallClient {
27710
27756
  q.limit = String(params.limit);
27711
27757
  return this.request("GET", ENDPOINTS.ORG_MEMBER_TASKS(orgId, memberId), void 0, q);
27712
27758
  }
27759
+ // Auto-paginated variant of getMemberTasks: fetches ALL pending tasks
27760
+ // (todo + in_progress) assigned to a member, including subtasks (the
27761
+ // endpoint does not filter parent_id). Powers the CLI `tasks assigned`
27762
+ // command so an agent answering "what's on X's plate" sees the full
27763
+ // backlog, not just the first page.
27764
+ async getMemberTasksAll(orgId, memberId) {
27765
+ const all = [];
27766
+ let cursor;
27767
+ do {
27768
+ const res = await this.getMemberTasks(orgId, memberId, { cursor, limit: 100 });
27769
+ all.push(...res.data);
27770
+ cursor = res.has_more ? res.next_cursor : void 0;
27771
+ } while (cursor);
27772
+ return all;
27773
+ }
27713
27774
  // ---- Invitations ----
27714
27775
  async createInvitation(orgId, email, role) {
27715
27776
  return this.request("POST", ENDPOINTS.ORG_INVITATIONS(orgId), { email, role });
@@ -28139,7 +28200,8 @@ var ParallClient = class _ParallClient {
28139
28200
  const res = await this.request("GET", ENDPOINTS.MACHINES_ME_CLIPS);
28140
28201
  return res.data;
28141
28202
  }
28142
- /** `GET /machines/me/browser-profiles` — browser profiles hosted by this machine. */
28203
+ /** `GET /machines/me/browser-profiles` — browser profiles hosted by this machine.
28204
+ * Returns the daemon DTO (carries proxy_password for bb-browser replay). */
28143
28205
  async listMachineBrowserProfiles() {
28144
28206
  const res = await this.request("GET", ENDPOINTS.MACHINES_ME_BROWSER_PROFILES);
28145
28207
  return res.data;
@@ -28299,7 +28361,7 @@ var ParallClient = class _ParallClient {
28299
28361
  * Returns null when the server responds with 304 (config unchanged).
28300
28362
  */
28301
28363
  async getPlatformConfig(currentVersion) {
28302
- const url = `${this.baseUrl}${ENDPOINTS.PLATFORM_CONFIG}`;
28364
+ const url = `${this.baseUrlFor(ENDPOINTS.PLATFORM_CONFIG)}${ENDPOINTS.PLATFORM_CONFIG}`;
28303
28365
  const extra = {};
28304
28366
  if (currentVersion !== void 0) {
28305
28367
  extra["If-None-Match"] = currentVersion;
@@ -28465,6 +28527,55 @@ var ParallClient = class _ParallClient {
28465
28527
  async getScheduleRun(orgId, runId) {
28466
28528
  return this.request("GET", ENDPOINTS.SCHEDULE_RUN(orgId, runId));
28467
28529
  }
28530
+ // ---- External triggers (org-scoped) ----
28531
+ async createExternalConnection(orgId, input) {
28532
+ return this.request("POST", ENDPOINTS.EXTERNAL_CONNECTIONS(orgId), input);
28533
+ }
28534
+ async listExternalConnections(orgId, filters) {
28535
+ return this.request("GET", ENDPOINTS.EXTERNAL_CONNECTIONS(orgId), void 0, filters);
28536
+ }
28537
+ async getExternalConnection(orgId, connectionId) {
28538
+ return this.request("GET", ENDPOINTS.EXTERNAL_CONNECTION(orgId, connectionId));
28539
+ }
28540
+ async updateExternalConnection(orgId, connectionId, patch) {
28541
+ return this.request("PATCH", ENDPOINTS.EXTERNAL_CONNECTION(orgId, connectionId), patch);
28542
+ }
28543
+ async regenerateExternalConnectionIngressToken(orgId, connectionId) {
28544
+ return this.request("POST", ENDPOINTS.EXTERNAL_CONNECTION_INGRESS_TOKEN_REGENERATE(orgId, connectionId));
28545
+ }
28546
+ async deleteExternalConnection(orgId, connectionId) {
28547
+ return this.request("DELETE", ENDPOINTS.EXTERNAL_CONNECTION(orgId, connectionId));
28548
+ }
28549
+ async getExternalTriggerSchema(orgId, connectionId) {
28550
+ return this.request("GET", ENDPOINTS.EXTERNAL_TRIGGER_SCHEMA(orgId, connectionId));
28551
+ }
28552
+ async listExternalIngressEvents(orgId, filters) {
28553
+ return this.request("GET", ENDPOINTS.EXTERNAL_INGRESS_EVENTS(orgId), void 0, filters);
28554
+ }
28555
+ async getExternalIngressEvent(orgId, eventId) {
28556
+ return this.request("GET", ENDPOINTS.EXTERNAL_INGRESS_EVENT(orgId, eventId));
28557
+ }
28558
+ async createExternalTrigger(orgId, input) {
28559
+ return this.request("POST", ENDPOINTS.EXTERNAL_TRIGGERS(orgId), input);
28560
+ }
28561
+ async listExternalTriggers(orgId, filters) {
28562
+ return this.request("GET", ENDPOINTS.EXTERNAL_TRIGGERS(orgId), void 0, filters);
28563
+ }
28564
+ async getExternalTrigger(orgId, triggerId) {
28565
+ return this.request("GET", ENDPOINTS.EXTERNAL_TRIGGER(orgId, triggerId));
28566
+ }
28567
+ async updateExternalTrigger(orgId, triggerId, patch) {
28568
+ return this.request("PATCH", ENDPOINTS.EXTERNAL_TRIGGER(orgId, triggerId), patch);
28569
+ }
28570
+ async deleteExternalTrigger(orgId, triggerId) {
28571
+ return this.request("DELETE", ENDPOINTS.EXTERNAL_TRIGGER(orgId, triggerId));
28572
+ }
28573
+ async listExternalTriggerRuns(orgId, filters) {
28574
+ return this.request("GET", ENDPOINTS.EXTERNAL_TRIGGER_RUNS(orgId), void 0, filters);
28575
+ }
28576
+ async getExternalTriggerRun(orgId, runId) {
28577
+ return this.request("GET", ENDPOINTS.EXTERNAL_TRIGGER_RUN(orgId, runId));
28578
+ }
28468
28579
  // ---- Wikis (org-scoped) ----
28469
28580
  async createWiki(orgId, data) {
28470
28581
  return this.request("POST", ENDPOINTS.WIKIS(orgId), data);
@@ -28479,8 +28590,35 @@ var ParallClient = class _ParallClient {
28479
28590
  async getWikiTree(orgId, wikiId, params) {
28480
28591
  return this.request("GET", ENDPOINTS.WIKI_TREE(orgId, wikiId), void 0, params);
28481
28592
  }
28593
+ /**
28594
+ * Resolve a server-returned, host-relative media URL (a wiki `signed_url`
28595
+ * like `/wiki/v1/signed/files?token=...`) against this client's base origin,
28596
+ * so it can be dropped straight into a browser `<img>`/`<video>`/`<iframe>`
28597
+ * `src`.
28598
+ *
28599
+ * wiki-service returns these relative on purpose — it doesn't know its own
28600
+ * public origin. A relative `src` resolves against the *page* origin, which
28601
+ * only works when the page and wiki-service share an origin (local dev:
28602
+ * same-origin + Next.js `/wiki/*` proxy). In deployed envs the app
28603
+ * (app.parall.com) and wiki-service (api.parall.com) are different origins,
28604
+ * so `app.parall.com/wiki/v1/signed/files` hits the SPA's own `/wiki/[...]`
28605
+ * catch-all route — an `<iframe>` then recursively renders the whole app
28606
+ * instead of the file. Prefixing with the wiki base (the exact origin every
28607
+ * wiki API request already uses — `baseUrlFor` resolves `/wiki/v1` paths to
28608
+ * `wikiBaseUrl`) makes the URL absolute against the origin that actually
28609
+ * serves the bytes. An empty base (local dev, same-origin proxy) leaves it
28610
+ * relative, preserving the proxy path.
28611
+ */
28612
+ absoluteMediaUrl(url) {
28613
+ if (/^https?:\/\//i.test(url))
28614
+ return url;
28615
+ return `${this.baseUrlFor(url)}${url}`;
28616
+ }
28482
28617
  async getWikiBlob(orgId, wikiId, params) {
28483
- return this.request("GET", ENDPOINTS.WIKI_BLOB(orgId, wikiId), void 0, params);
28618
+ const blob = await this.request("GET", ENDPOINTS.WIKI_BLOB(orgId, wikiId), void 0, params);
28619
+ if (blob.signed_url)
28620
+ blob.signed_url = this.absoluteMediaUrl(blob.signed_url);
28621
+ return blob;
28484
28622
  }
28485
28623
  async getWikiNodeSections(orgId, wikiId, params) {
28486
28624
  return this.request("GET", ENDPOINTS.WIKI_NODE_SECTIONS(orgId, wikiId), void 0, params);
@@ -28570,7 +28708,10 @@ var ParallClient = class _ParallClient {
28570
28708
  * token — don't leak it.
28571
28709
  */
28572
28710
  async getWikiFilePreviewUrl(orgId, wikiId, params) {
28573
- return this.request("POST", ENDPOINTS.WIKI_FILE_PREVIEW_URL(orgId, wikiId), params);
28711
+ const res = await this.request("POST", ENDPOINTS.WIKI_FILE_PREVIEW_URL(orgId, wikiId), params);
28712
+ if (res.url)
28713
+ res.url = this.absoluteMediaUrl(res.url);
28714
+ return res;
28574
28715
  }
28575
28716
  // ---- Wiki Path Scopes (AFCS ACL) ----
28576
28717
  async getWikiPathScopes(orgId, wikiId) {
@@ -28647,6 +28788,16 @@ var ParallClient = class _ParallClient {
28647
28788
  async getBacklinks(orgId, params) {
28648
28789
  return this.request("GET", ENDPOINTS.REFS_BACKLINKS(orgId), void 0, params);
28649
28790
  }
28791
+ /**
28792
+ * Bounded multi-hop walk of the prll:// reference graph around `uri`. `uri`
28793
+ * must be an entity-level prll:// URI — a refined URI (path/query/fragment) is
28794
+ * rejected with 400 UNSUPPORTED_REFINED_URI. `depth` is clamped server-side to
28795
+ * [1, 4]; breadth (node/edge counts) is capped server-side and surfaced via
28796
+ * `truncated`. ACL is applied per hop (chat membership + wiki path scope).
28797
+ */
28798
+ async getRefsGraph(orgId, params) {
28799
+ return this.request("GET", ENDPOINTS.REFS_GRAPH(orgId), void 0, params);
28800
+ }
28650
28801
  async checkBrokenRefs(orgId) {
28651
28802
  return this.request("GET", ENDPOINTS.REFS_CHECK(orgId));
28652
28803
  }
@@ -28786,6 +28937,9 @@ var ParallClient = class _ParallClient {
28786
28937
  const resp = await this.request("GET", ENDPOINTS.CLIP_ONLINE(orgId));
28787
28938
  return resp.data;
28788
28939
  }
28940
+ /** Org-wide browser-profile discovery list. Returns the sanitized
28941
+ * {@link BrowserProfileListItem} shape (not the full domain model), each row
28942
+ * carrying a per-viewer `can_open` control hint. */
28789
28943
  async listBrowserProfiles(orgId) {
28790
28944
  const resp = await this.request("GET", ENDPOINTS.BROWSER_PROFILES(orgId));
28791
28945
  return resp.data;
@@ -29439,6 +29593,9 @@ function resolveStepTarget(event) {
29439
29593
  if (event.type === "schedule" || event.targetId.startsWith("sch_")) {
29440
29594
  return { target_type: "schedule", target_id: event.targetId };
29441
29595
  }
29596
+ if (event.type === "external_trigger" || event.targetId.startsWith("xtr_")) {
29597
+ return { target_type: "external_trigger", target_id: event.targetId };
29598
+ }
29442
29599
  if (event.type === "wiki_comment") {
29443
29600
  return { target_type: "wiki", target_id: event.targetId || void 0 };
29444
29601
  }
@@ -29643,6 +29800,18 @@ var ParallAgentGateway = class {
29643
29800
  } catch (err) {
29644
29801
  this.opts.log?.error(`schedule fire dispatch failed for ${data.source_id}: ${String(err)}`);
29645
29802
  }
29803
+ } else if (data.event_type === "external_trigger") {
29804
+ if (!data.source_id)
29805
+ return;
29806
+ try {
29807
+ const dispatched = await this.fetchAndHandleExternalTriggerRun(data.source_id);
29808
+ if (dispatched) {
29809
+ this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29810
+ });
29811
+ }
29812
+ } catch (err) {
29813
+ this.opts.log?.error(`external trigger dispatch failed for ${data.source_id}: ${String(err)}`);
29814
+ }
29646
29815
  } else if (data.event_type === "approval_decided") {
29647
29816
  if (!data.source_id)
29648
29817
  return;
@@ -29721,8 +29890,13 @@ var ParallAgentGateway = class {
29721
29890
  target_type: target.target_type,
29722
29891
  target_id: target.target_id,
29723
29892
  content: {
29724
- 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 === "approval" ? "approval_decided" : "mention",
29725
- 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 === "approval" ? { approval_id: event.messageId } : { message_id: event.messageId },
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",
29894
+ 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
+ trigger_id: event.targetId,
29896
+ run_id: event.messageId,
29897
+ connection_id: event.externalConnectionId,
29898
+ ingress_event_id: event.externalIngressEventId
29899
+ } : event.type === "approval" ? { approval_id: event.messageId } : { message_id: event.messageId },
29726
29900
  sender_id: event.senderId,
29727
29901
  sender_name: event.senderName,
29728
29902
  summary: event.body.substring(0, 200),
@@ -30758,6 +30932,62 @@ var ParallAgentGateway = class {
30758
30932
  }
30759
30933
  return dispatched;
30760
30934
  }
30935
+ async fetchAndHandleExternalTriggerRun(runId) {
30936
+ let run = null;
30937
+ try {
30938
+ run = await this.opts.client.getExternalTriggerRun(this.opts.config.org_id, runId);
30939
+ } catch (err) {
30940
+ const status = err?.status;
30941
+ if (status === 404) {
30942
+ this.opts.log?.warn(`external trigger run ${runId} not accessible (404), acking stale dispatch`);
30943
+ return true;
30944
+ }
30945
+ this.opts.log?.warn(`external trigger run fetch failed for ${runId}, leaving pending: ${String(err)}`);
30946
+ return false;
30947
+ }
30948
+ if (!run)
30949
+ return true;
30950
+ return this.handleExternalTriggerRun(run);
30951
+ }
30952
+ async handleExternalTriggerRun(run) {
30953
+ if (this.shuttingDown)
30954
+ return false;
30955
+ const dedupeKey = `external_trigger_run:${run.id}`;
30956
+ if (this.dispatchedTasks.has(dedupeKey))
30957
+ return false;
30958
+ this.dispatchedTasks.add(dedupeKey);
30959
+ this.opts.log?.info(`external trigger fired: ${run.id} (trigger ${run.trigger_id})`);
30960
+ const attachedUri = typeof run.trigger_snapshot?.attached_to_uri === "string" ? run.trigger_snapshot.attached_to_uri : void 0;
30961
+ const event = {
30962
+ type: "external_trigger",
30963
+ targetId: run.trigger_id,
30964
+ targetName: run.trigger_name || void 0,
30965
+ targetType: "external_trigger",
30966
+ senderId: "system",
30967
+ senderName: "external",
30968
+ messageId: run.id,
30969
+ body: run.agent_input_body ?? "",
30970
+ externalConnectionId: run.connection_id,
30971
+ externalConnectionSourceType: run.connection_source_type || void 0,
30972
+ externalConnectionDisplayName: run.connection_display_name || void 0,
30973
+ externalIngressEventId: run.ingress_event_id,
30974
+ externalIngressEventType: run.ingress_event_type || void 0,
30975
+ attachedUri,
30976
+ ackSourceType: "external_trigger_run",
30977
+ ackSourceId: run.id
30978
+ };
30979
+ let dispatched;
30980
+ try {
30981
+ dispatched = await this.handleInboundEvent(event);
30982
+ } catch (err) {
30983
+ this.dispatchedTasks.delete(dedupeKey);
30984
+ throw err;
30985
+ }
30986
+ if (!dispatched) {
30987
+ this.dispatchedTasks.delete(dedupeKey);
30988
+ }
30989
+ return dispatched;
30990
+ }
30761
30991
  async fetchAndHandleApprovalDecided(approvalId, actorId, chatId) {
30762
30992
  let approval = null;
30763
30993
  try {
@@ -30868,6 +31098,8 @@ var ParallAgentGateway = class {
30868
31098
  dispatched = await this.handleWikiComment(item.source_id, item.actor_id, item.delivery_reason);
30869
31099
  } else if (item.event_type === "schedule.fire" && item.source_id) {
30870
31100
  dispatched = await this.fetchAndHandleScheduleFire(item.source_id, item.actor_id);
31101
+ } else if (item.event_type === "external_trigger" && item.source_id) {
31102
+ dispatched = await this.fetchAndHandleExternalTriggerRun(item.source_id);
30871
31103
  } else if (item.event_type === "approval_decided" && item.source_id) {
30872
31104
  dispatched = await this.fetchAndHandleApprovalDecided(item.source_id, item.actor_id, item.chat_id ?? null);
30873
31105
  } else if (item.event_type === "message" && item.source_id && item.chat_id) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/parall",
3
- "version": "1.35.0",
3
+ "version": "1.36.1",
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.35.0",
20
- "@parall/sdk": "1.35.0"
19
+ "@parall/agent-core": "1.36.1",
20
+ "@parall/sdk": "1.36.1"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/node": "^22.0.0",
@@ -0,0 +1,92 @@
1
+ ---
2
+ name: parall-external-triggers
3
+ description: "Parall External Trigger operations: create incoming connections, configure CEL/Liquid triggers, inspect incoming events and runs, and respond to `[Event: external.trigger]` dispatches. Use when: user asks to connect an external system to an agent, set up incoming callbacks/notifications, or when the agent receives an external trigger event."
4
+ ---
5
+
6
+ # Parall External Triggers
7
+
8
+ An **External Trigger** is an incoming platform trigger. External systems send events to an External Trigger Connection, Parall matches active triggers with CEL, renders a Liquid template into an agent input body, and dispatches that input to the configured target agents.
9
+
10
+ Use External Triggers for incoming events such as GitHub callbacks, Slack/Feishu notifications, generic webhooks, or emails once the platform has a connection for them. The runtime behavior is incoming-only: receiving a trigger does not imply that you can call the external system back unless another explicit Parall capability is available.
11
+
12
+ ## Prerequisite
13
+
14
+ External Trigger CLI commands are gated by the org-level `external-triggers` feature flag. If a command reports that the feature is unavailable, ask a human org admin to enable the flag before trying again. Public ingress delivery may still be live even when the management CLI is hidden behind the rollout flag.
15
+
16
+ ## Creating a trigger
17
+
18
+ ```bash
19
+ # 1. Create a connection. The ingress token is shown once; prefer writing it
20
+ # to a local file so it does not land in shell history or logs.
21
+ parall external-triggers create-connection --name "GitHub CI" --token-file ./github-ci-webhook-token.txt
22
+
23
+ # 2. Create a trigger that targets one or more agents.
24
+ parall external-triggers create \
25
+ --connection prll://xcn_xxx \
26
+ --name "Failed checks" \
27
+ --target-ids prll://usr_agent_xxx \
28
+ --filter "body.json.check_run.conclusion == 'failure'" \
29
+ --template-file ./github-check-failed.md \
30
+ --attached-to-uri prll://tsk_xxx
31
+ ```
32
+
33
+ `--filter` is CEL. Omit it to match all incoming events (`true`). Keep filters small and deterministic; do not model provider-specific branching in agent code when the CEL expression can select the relevant events first.
34
+
35
+ `--template` / `--template-file` is Liquid with the safe Parall profile. It can read event data, body data, safe request metadata, trigger fields, run fields, and connection fields. It cannot call HTTP, read databases, evaluate arbitrary code, access platform secrets, or read arbitrary request headers/query parameters.
36
+
37
+ Example template:
38
+
39
+ ```liquid
40
+ GitHub check failed.
41
+
42
+ Event: {{ request.headers.x_github_event | default: "unknown" }}
43
+ Repo: {{ body.json.repository.full_name }}
44
+ PR: {{ body.json.pull_request.number }} {{ body.json.pull_request.title }}
45
+ Check: {{ body.json.check_run.name }}
46
+ Conclusion: {{ body.json.check_run.conclusion }}
47
+
48
+ Run: {{ body.json.check_run.html_url }}
49
+ ```
50
+
51
+ Do not render access tokens, signing secrets, cookies, or private credentials into agent input. Request snapshots and bodies may contain third-party data; treat them as user-provided input.
52
+
53
+ ## Inspecting and lifecycle
54
+
55
+ ```bash
56
+ parall external-triggers connections
57
+ parall external-triggers connection prll://xcn_xxx
58
+ parall external-triggers schema prll://xcn_xxx
59
+
60
+ parall external-triggers list
61
+ parall external-triggers list --connection prll://xcn_xxx
62
+ parall external-triggers get prll://xtr_xxx
63
+ parall external-triggers update prll://xtr_xxx --filter "event.type == 'check_run'"
64
+ parall external-triggers pause prll://xtr_xxx
65
+ parall external-triggers resume prll://xtr_xxx
66
+ parall external-triggers delete prll://xtr_xxx
67
+
68
+ parall external-triggers runs prll://xtr_xxx
69
+ parall external-triggers run prll://xrn_xxx
70
+ parall external-triggers events --connection prll://xcn_xxx
71
+ parall external-triggers event prll://xin_xxx
72
+ ```
73
+
74
+ ## Responding to external trigger dispatches
75
+
76
+ When you receive `[Event: external.trigger]`, Parall has already matched a trigger and rendered its template. The prompt includes headers such as:
77
+
78
+ - `[Trigger: prll://xtr_xxx]`
79
+ - `[Run: prll://xrn_xxx]`
80
+ - `[Connection: ... (prll://xcn_xxx)]`
81
+ - `[Ingress: prll://xin_xxx]`
82
+ - Optional `[Attached: prll://...]`
83
+
84
+ The rendered agent input body follows those headers. You usually do not need to fetch the run before acting. Fetch the run only for audit/debugging:
85
+
86
+ ```bash
87
+ parall external-triggers run prll://xrn_xxx
88
+ ```
89
+
90
+ Act on the rendered input the same way you would act on a user message: send a message, create or update tasks, edit wiki pages, or use available clips. If no visible response is needed, use `parall no-reply --reason "handled external trigger"` before sending any message.
91
+
92
+ CLI command results are JSON on stdout; mutation commands may emit auxiliary hints on stderr, for example `Created: prll://xtr_xxx`.
@@ -69,8 +69,36 @@ 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.
@@ -161,9 +189,37 @@ Every entity is addressable with a `prll://` URI. Common prefixes you'll see in
161
189
  | `prll://prj_` | Project | parall-tasks |
162
190
  | `prll://sch_` | Schedule (time trigger) | parall-schedules |
163
191
  | `prll://srn_` | Schedule run (single fire audit record; carries fire-time snapshot) | parall-schedules |
192
+ | `prll://xcn_` | External Trigger Connection (incoming endpoint) | parall-external-triggers |
193
+ | `prll://xin_` | External Trigger Event (single incoming event audit record) | parall-external-triggers |
194
+ | `prll://xtr_` | External Trigger (incoming trigger configuration) | parall-external-triggers |
195
+ | `prll://xrn_` | External Trigger run (single matched dispatch audit record) | parall-external-triggers |
164
196
  | `prll://wik_` | Wiki | parall-wiki |
165
197
  | `prll://att_` | Attachment | parall-platform (files) |
166
198
 
167
199
  When a message or event references `prll://sch_xxx` or `prll://srn_xxx`, or when you receive `[Event: schedule.fired]`, switch to the **parall-schedules** skill for the CLI commands (create / list / pause / resume / cancel / runs).
168
200
 
201
+ 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).
202
+
203
+ ## References (relationship graph)
204
+
205
+ `prll://` references between entities form a graph — a message cites a task, a
206
+ task cites a wiki page, and so on. Walk it to answer "what is this decision /
207
+ entity connected to". All results are permission-filtered to what you can see.
208
+
209
+ ```bash
210
+ # Resolve URIs to entity metadata (titles, status, previews)
211
+ parall refs resolve prll://tsk_xxx prll://wik_xxx
212
+
213
+ # Single hop — who references X
214
+ parall refs backlinks prll://tsk_xxx
215
+
216
+ # Multi-hop — the connected sub-graph around X (entity-level URI only — no
217
+ # path/anchor; depth 1–4, default 2)
218
+ parall refs graph prll://tsk_xxx --depth 2
219
+ ```
220
+
221
+ `refs graph` traverses both directions (inbound + outbound) and returns `nodes`
222
+ and `edges` with each node's hop `depth`. `truncated: true` means a size cap clipped
223
+ the result — narrow it with a smaller `--depth`.
224
+
169
225
  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.
@@ -7,16 +7,39 @@ description: "Parall task operations: create, update, comment on, and query task
7
7
 
8
8
  Manage tasks and projects via the Parall CLI. Auth and runtime context are pre-configured.
9
9
 
10
+ ## Finding What's on Someone's Plate (incl. subtasks)
11
+
12
+ To answer "what do I still have to do", "what's <person> working on", or any
13
+ "open work assigned to X" question, use `tasks assigned`:
14
+
15
+ ```bash
16
+ # Pending tasks (todo + in_progress) assigned to a member — INCLUDES subtasks.
17
+ parall tasks assigned prll://usr_xxx # a specific person (e.g. the human who asked)
18
+ parall tasks assigned # yourself (defaults to the authenticated user)
19
+ ```
20
+
21
+ This is the authoritative "open work for a person" query. It returns every
22
+ pending task assigned to that member **including subtasks** — even when the
23
+ subtask's parent task belongs to someone else. Decomposed work usually lives in
24
+ subtasks, so do NOT answer this kind of question from `tasks list` alone:
25
+ that is org-wide, page-capped, and not scoped to a person, so a person's
26
+ subtasks are easily missed.
27
+
28
+ Resolve a person's `prll://usr_` id from the message context, the members
29
+ list, or ref search; your own id comes from `parall whoami`.
30
+
10
31
  ## Task Commands
11
32
 
12
33
  ```bash
13
- # List tasks (filterable by status)
34
+ # List tasks (org-wide; filter by status, assignee, or parent)
14
35
  parall tasks list
15
36
  parall tasks list --status todo
16
37
  parall tasks list --status in_progress
38
+ parall tasks list --assignee-id prll://usr_xxx # first page only (default 20) — for a person's FULL backlog use 'tasks assigned' above
39
+ parall tasks subtasks prll://tsk_xxx # children of a single parent task
17
40
 
18
- # Create a task
19
- parall tasks create --title "Task title" [--assignee-id prll://usr_xxx] [--project-id prll://prj_xxx]
41
+ # Create a task (add --parent-id to make it a SUBTASK of another task)
42
+ parall tasks create --title "Task title" [--assignee-id prll://usr_xxx] [--parent-id prll://tsk_xxx] [--project-id prll://prj_xxx]
20
43
 
21
44
  # Update task status
22
45
  parall tasks update prll://tsk_xxx --status in_progress
@@ -26,6 +49,12 @@ parall tasks update prll://tsk_xxx --status done
26
49
  parall tasks comments add prll://tsk_xxx --body "Progress update..."
27
50
  ```
28
51
 
52
+ Subtasks are just tasks with a parent: create one with `tasks create --parent-id`,
53
+ re-parent with `tasks update --parent-id`, list a parent's children with
54
+ `tasks subtasks`. `tasks list` without `--parent-id` already returns both
55
+ top-level tasks and subtasks; per-person open work is best fetched with
56
+ `tasks assigned` (above).
57
+
29
58
  ## Project Commands
30
59
 
31
60
  ```bash
@@ -21,8 +21,11 @@ Key facts the commands won't tell you:
21
21
  the absolute workspace path (`synced → /path/to/<slug>` / `Mount: ...`).
22
22
  Always address wiki files by that absolute path — your shell cwd is usually
23
23
  NOT inside the workspace.
24
- - **Text files only.** Changesets reject binary content. Don't put images or
25
- archives in the workspace; binary uploads go through the web UI.
24
+ - **Text and binary are two paths.** The workspace + changeset flow is for
25
+ text (markdown, code, config). Binary assets (images, PDFs, archives) are
26
+ diff-less — they don't go in the workspace; use `parall wiki file` (see
27
+ **Binary files** below). Dropping a binary into the workspace just gets it
28
+ rejected on propose.
26
29
  - **`cat`, `search`, `query`, `outline`, and `section` read your local
27
30
  workspace copy when it exists** — including your own unproposed edits. Add
28
31
  `--remote` to `cat` to read the server version instead.
@@ -103,6 +106,50 @@ Re-propose REPLACES the changeset's previous contents with your current
103
106
  workspace diff — to withdraw a file from the proposal, revert it locally
104
107
  (restore the synced content) and re-propose; it drops out of the changeset.
105
108
 
109
+ ## Binary files
110
+
111
+ Images, PDFs, archives — anything that can't be diffed — bypass the workspace
112
+ and changeset-text flow entirely. They never belong in the synced workspace
113
+ (propose rejects them); use `parall wiki file` instead. `cat` is text-only —
114
+ to read a binary's real bytes use `file get` (a plain `sync` only leaves a
115
+ few-line Git-LFS pointer on disk, since the runtime has no git-lfs).
116
+
117
+ ```bash
118
+ # Maintainer: direct-commit a binary to the default branch (no review)
119
+ parall wiki file upload ./diagram.png docs/assets/diagram.png
120
+
121
+ # Read a binary's real bytes (LFS pointers resolved server-side) to a file.
122
+ # Always use --output for binaries — without it the bytes stream to stdout and
123
+ # would flood your context.
124
+ parall wiki file get docs/assets/diagram.png --output ./diagram.png
125
+ parall wiki file get docs/assets/diagram.png --ref <commit-or-branch> --output ./diagram.png # a specific revision
126
+
127
+ # Remove a binary from the default branch (git history still has it)
128
+ parall wiki file delete docs/assets/diagram.png
129
+ ```
130
+
131
+ `upload` needs **maintain**; it routes by size automatically (≤1 MiB inline,
132
+ larger → LFS). A text file sent to `upload` is rejected — that's the changeset
133
+ flow's job.
134
+
135
+ ### Reader: propose markdown that embeds an image
136
+
137
+ Without maintain you can still propose a doc with images — upload the binary
138
+ into your **changeset's** branch (read + author), not the default branch:
139
+
140
+ ```bash
141
+ parall wiki sync
142
+ # edit a .md in the workspace to add ![alt](assets/foo.png)
143
+ parall wiki changeset create <wiki> --title "Add foo diagram" # creates the changeset (note its id)
144
+ parall wiki file upload ./foo.png assets/foo.png <wiki> --changeset <changesetId>
145
+ # leave it for a maintainer to merge — both the markdown and the image squash in together
146
+ ```
147
+
148
+ Do the markdown `changeset create` first so the changeset exists, then attach
149
+ the image to it. Don't re-propose (`--update`) after attaching a binary —
150
+ re-propose replays only the text workspace and the server rejects dropping the
151
+ attached binary (422 `REPLACE_HAS_BINARY`).
152
+
106
153
  ## Discovery & history
107
154
 
108
155
  ```bash