@parall/parall 1.34.0 → 1.36.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.
@@ -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
  }
@@ -27010,6 +27033,7 @@ var ENDPOINTS = {
27010
27033
  // Org-scoped
27011
27034
  ORG: (orgId) => `${API_BASE}/orgs/${orgId}`,
27012
27035
  ORG_MEMBERS: (orgId) => `${API_BASE}/orgs/${orgId}/members`,
27036
+ TEAMS: (orgId) => `${API_BASE}/orgs/${orgId}/teams`,
27013
27037
  ORG_MEMBERS_ONLINE: (orgId) => `${API_BASE}/orgs/${orgId}/members/online`,
27014
27038
  ORG_MEMBER: (orgId, userId) => `${API_BASE}/orgs/${orgId}/members/${userId}`,
27015
27039
  ORG_MEMBER_CHATS: (orgId, memberId) => `${API_BASE}/orgs/${orgId}/members/${memberId}/chats`,
@@ -27054,6 +27078,7 @@ var ENDPOINTS = {
27054
27078
  AGENT: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}`,
27055
27079
  AGENT_API_KEYS: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/api-keys`,
27056
27080
  AGENT_API_KEY: (orgId, agentId, key) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/api-keys/${key}`,
27081
+ AGENT_API_KEY_REGENERATE: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/api-keys/regenerate`,
27057
27082
  AGENT_AVATAR: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/avatar`,
27058
27083
  AGENT_ACTIVITY: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/activity`,
27059
27084
  AGENT_MONITOR: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/monitor`,
@@ -27093,6 +27118,7 @@ var ENDPOINTS = {
27093
27118
  MACHINE_AGENT_WORKSPACE_SETUP: (orgId, machineId, agentId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/agents/${agentId}/workspace/setup`,
27094
27119
  MACHINE_LLM_SOURCE: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/llm-source`,
27095
27120
  MACHINE_PROVIDER_ENABLED: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/provider-enabled`,
27121
+ MACHINE_CAPABILITIES: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/capabilities`,
27096
27122
  MACHINE_RUNTIME_AUTH: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/runtime-auth`,
27097
27123
  MACHINE_KEYS: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/keys`,
27098
27124
  MACHINE_KEY: (orgId, machineId, keyId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/keys/${keyId}`,
@@ -27113,6 +27139,12 @@ var ENDPOINTS = {
27113
27139
  MACHINES_ME_AGENT_WORKSPACE_STATE: (agentId) => `${API_BASE}/machines/me/agents/${agentId}/workspace-state`,
27114
27140
  MACHINES_ME_WS_TICKET: `${API_BASE}/machines/me/ws/ticket`,
27115
27141
  MACHINES_ME_BROWSE_RESPONSE: (requestId) => `${API_BASE}/machines/me/browse-response/${requestId}`,
27142
+ // Hosted browser live-viewer control plane (api-server, NOT clip-service):
27143
+ // the web client drives WebRTC signaling + tab nav through VIEWER_COMMAND;
27144
+ // api-server brokers each command to the host daemon via the machine:{id}
27145
+ // request/reply bridge (mirrors filesystem browse), and the daemon replies on
27146
+ // VIEWER_RESPONSE. See docs/engineering-design/hosted-browser-provider-design.md.
27147
+ MACHINES_ME_BROWSER_PROFILE_VIEWER_RESPONSE: (requestId) => `${API_BASE}/machines/me/browser-profiles/viewer-response/${requestId}`,
27116
27148
  // Tasks (org-scoped)
27117
27149
  TASKS: (orgId) => `${API_BASE}/orgs/${orgId}/tasks`,
27118
27150
  TASK: (orgId, taskId) => `${API_BASE}/orgs/${orgId}/tasks/${taskId}`,
@@ -27139,6 +27171,17 @@ var ENDPOINTS = {
27139
27171
  SCHEDULE_CANCEL: (orgId, id) => `${API_BASE}/orgs/${orgId}/schedules/${id}/cancel`,
27140
27172
  SCHEDULE_RUNS: (orgId, id) => `${API_BASE}/orgs/${orgId}/schedules/${id}/runs`,
27141
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}`,
27142
27185
  // Invitations (org-scoped, admin)
27143
27186
  ORG_INVITATIONS: (orgId) => `${API_BASE}/orgs/${orgId}/invitations`,
27144
27187
  ORG_INVITATION: (orgId, invId) => `${API_BASE}/orgs/${orgId}/invitations/${invId}`,
@@ -27178,6 +27221,9 @@ var ENDPOINTS = {
27178
27221
  // Wiki Path Scopes (AFCS ACL)
27179
27222
  WIKI_PATH_SCOPES: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/path-scopes`,
27180
27223
  WIKI_PATH_SCOPE: (orgId, wikiId, scopeId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/path-scopes/${scopeId}`,
27224
+ // Wiki Path Restrictions (AFCS narrowing ACL — private subtrees)
27225
+ WIKI_RESTRICTIONS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/restrictions`,
27226
+ WIKI_RESTRICTION: (orgId, wikiId, restrictionId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/restrictions/${restrictionId}`,
27181
27227
  WIKI_ACCESS_STATUS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/access-status`,
27182
27228
  WIKI_ACCESS_REQUESTS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/access-requests`,
27183
27229
  // Wiki History (commits, file commits, blame)
@@ -27243,6 +27289,7 @@ var ENDPOINTS = {
27243
27289
  // Clips (org-scoped, served by clip-service)
27244
27290
  CLIPS: (orgId) => `${CLIP_BASE}/orgs/${orgId}/clips`,
27245
27291
  CLIP: (orgId, clipId) => `${CLIP_BASE}/orgs/${orgId}/clips/${clipId}`,
27292
+ CLIPS_BULK_METADATA: (orgId) => `${CLIP_BASE}/orgs/${orgId}/clips/bulk-metadata`,
27246
27293
  CLIP_AGENTS: (orgId, clipId) => `${CLIP_BASE}/orgs/${orgId}/clips/${clipId}/agents`,
27247
27294
  AGENT_CLIPS: (orgId, agentId) => `${CLIP_BASE}/orgs/${orgId}/agents/${agentId}/clips`,
27248
27295
  AGENT_CLIP: (orgId, agentId, clipId) => `${CLIP_BASE}/orgs/${orgId}/agents/${agentId}/clips/${clipId}`,
@@ -27255,6 +27302,9 @@ var ENDPOINTS = {
27255
27302
  BROWSER_PROFILE_RESET: (orgId, profileId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles/${profileId}/reset`,
27256
27303
  BROWSER_PROFILE_CONSENTS: (orgId, profileId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles/${profileId}/consents`,
27257
27304
  BROWSER_PROFILE_CONSENT: (orgId, profileId, clipId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles/${profileId}/consents/${clipId}`,
27305
+ // Live viewer command — on api-server (API_BASE), not clip-service: it rides
27306
+ // the machine control-plane request/reply bridge that lives in api-server.
27307
+ BROWSER_PROFILE_VIEWER_COMMAND: (orgId, profileId) => `${API_BASE}/orgs/${orgId}/browser-profiles/${profileId}/viewer/command`,
27258
27308
  // Clip registry (global, served by clip-service → Pinix Hub proxy)
27259
27309
  CLIP_REGISTRY: () => `${CLIP_BASE}/registry/clips`
27260
27310
  };
@@ -27331,6 +27381,7 @@ var WS_EVENTS = {
27331
27381
  MACHINE_CONFIG_UPDATED: "machine.config.updated",
27332
27382
  MACHINE_CLIP_SYNC: "machine.clip.sync",
27333
27383
  MACHINE_BROWSER_PROFILE_LIFECYCLE: "machine.browser_profile.lifecycle",
27384
+ MACHINE_BROWSER_PROFILE_VIEWER: "machine.browser_profile.viewer",
27334
27385
  AGENT_NEW_SESSION: "agent.new_session",
27335
27386
  CLIP_CREATED: "clip.created",
27336
27387
  CLIP_REMOVED: "clip.removed",
@@ -27340,6 +27391,7 @@ var WS_EVENTS = {
27340
27391
  // ../sdk/dist/client.js
27341
27392
  var ParallClient = class _ParallClient {
27342
27393
  baseUrl;
27394
+ wikiBaseUrl;
27343
27395
  token;
27344
27396
  onTokenExpired;
27345
27397
  getRefreshToken;
@@ -27391,12 +27443,22 @@ var ParallClient = class _ParallClient {
27391
27443
  }
27392
27444
  constructor(options = {}) {
27393
27445
  this.baseUrl = options.baseUrl ?? "";
27446
+ this.wikiBaseUrl = options.wikiBaseUrl ?? this.baseUrl;
27394
27447
  this.token = options.token ?? null;
27395
27448
  this.onTokenExpired = options.onTokenExpired;
27396
27449
  this.getRefreshToken = options.getRefreshToken;
27397
27450
  this.setTokens = options.setTokens;
27398
27451
  this.swimlaneName = options.swimlaneName;
27399
27452
  }
27453
+ /**
27454
+ * Pick the origin for a request path: wiki-service base for `/wiki/v1`
27455
+ * endpoints, api base for everything else. The path itself (from ENDPOINTS)
27456
+ * is authoritative, so wiki vs api routing can't drift from how a caller
27457
+ * happens to invoke the client.
27458
+ */
27459
+ baseUrlFor(path7) {
27460
+ return path7.startsWith(WIKI_BASE) ? this.wikiBaseUrl : this.baseUrl;
27461
+ }
27400
27462
  setToken(token) {
27401
27463
  this.token = token;
27402
27464
  }
@@ -27461,7 +27523,7 @@ var ParallClient = class _ParallClient {
27461
27523
  if (!retried) {
27462
27524
  await this.ensureFreshToken(path7);
27463
27525
  }
27464
- let url = `${this.baseUrl}${path7}`;
27526
+ let url = `${this.baseUrlFor(path7)}${path7}`;
27465
27527
  if (query) {
27466
27528
  const params = new URLSearchParams();
27467
27529
  for (const [key, value] of Object.entries(query)) {
@@ -27482,7 +27544,10 @@ var ParallClient = class _ParallClient {
27482
27544
  method,
27483
27545
  headers,
27484
27546
  body: body ? JSON.stringify(body) : void 0,
27485
- signal
27547
+ signal,
27548
+ // keepalive lets a request fired during page unload (e.g. the browser
27549
+ // viewer's stream.close on pagehide) outlive the document.
27550
+ keepalive: opts?.keepalive
27486
27551
  });
27487
27552
  } catch (err) {
27488
27553
  throw _ParallClient.normalizeFetchError(err);
@@ -27530,7 +27595,7 @@ var ParallClient = class _ParallClient {
27530
27595
  void _drop;
27531
27596
  let res;
27532
27597
  try {
27533
- res = await fetch(`${this.baseUrl}${path7}`, {
27598
+ res = await fetch(`${this.baseUrlFor(path7)}${path7}`, {
27534
27599
  method,
27535
27600
  headers,
27536
27601
  body,
@@ -27653,6 +27718,10 @@ var ParallClient = class _ParallClient {
27653
27718
  const res = await this.request("GET", ENDPOINTS.ORG_MEMBERS(orgId));
27654
27719
  return res.data;
27655
27720
  }
27721
+ async getTeams(orgId) {
27722
+ const res = await this.request("GET", ENDPOINTS.TEAMS(orgId));
27723
+ return res.data;
27724
+ }
27656
27725
  async getOnlineMembers(orgId) {
27657
27726
  const res = await this.request("GET", ENDPOINTS.ORG_MEMBERS_ONLINE(orgId));
27658
27727
  return res.user_ids ?? [];
@@ -27686,6 +27755,21 @@ var ParallClient = class _ParallClient {
27686
27755
  q.limit = String(params.limit);
27687
27756
  return this.request("GET", ENDPOINTS.ORG_MEMBER_TASKS(orgId, memberId), void 0, q);
27688
27757
  }
27758
+ // Auto-paginated variant of getMemberTasks: fetches ALL pending tasks
27759
+ // (todo + in_progress) assigned to a member, including subtasks (the
27760
+ // endpoint does not filter parent_id). Powers the CLI `tasks assigned`
27761
+ // command so an agent answering "what's on X's plate" sees the full
27762
+ // backlog, not just the first page.
27763
+ async getMemberTasksAll(orgId, memberId) {
27764
+ const all = [];
27765
+ let cursor;
27766
+ do {
27767
+ const res = await this.getMemberTasks(orgId, memberId, { cursor, limit: 100 });
27768
+ all.push(...res.data);
27769
+ cursor = res.has_more ? res.next_cursor : void 0;
27770
+ } while (cursor);
27771
+ return all;
27772
+ }
27689
27773
  // ---- Invitations ----
27690
27774
  async createInvitation(orgId, email, role) {
27691
27775
  return this.request("POST", ENDPOINTS.ORG_INVITATIONS(orgId), { email, role });
@@ -27891,6 +27975,10 @@ var ParallClient = class _ParallClient {
27891
27975
  async createAgentApiKey(orgId, agentId) {
27892
27976
  return this.request("POST", ENDPOINTS.AGENT_API_KEYS(orgId, agentId));
27893
27977
  }
27978
+ /** Revokes all of the agent's active API keys and mints a replacement. */
27979
+ async regenerateAgentApiKey(orgId, agentId) {
27980
+ return this.request("POST", ENDPOINTS.AGENT_API_KEY_REGENERATE(orgId, agentId));
27981
+ }
27894
27982
  async revokeAgentApiKey(orgId, agentId, key) {
27895
27983
  return this.request("DELETE", ENDPOINTS.AGENT_API_KEY(orgId, agentId, key));
27896
27984
  }
@@ -28062,6 +28150,17 @@ var ParallClient = class _ParallClient {
28062
28150
  provider_enabled: providerEnabled
28063
28151
  });
28064
28152
  }
28153
+ /**
28154
+ * Replace the machine's capability set (admin). Primary use: healing a
28155
+ * machine that registered without `browser_provider` during the capability
28156
+ * migration's rolling-deploy window. Removing `agent_host` is rejected by the
28157
+ * server while agents are attached (409 AGENTS_STILL_ATTACHED).
28158
+ */
28159
+ async patchMachineCapabilities(orgId, machineId, capabilities) {
28160
+ return this.request("PATCH", ENDPOINTS.MACHINE_CAPABILITIES(orgId, machineId), {
28161
+ capabilities
28162
+ });
28163
+ }
28065
28164
  /** Get machine-level runtime auth state. */
28066
28165
  async getMachineRuntimeAuth(orgId, machineId) {
28067
28166
  return this.request("GET", ENDPOINTS.MACHINE_RUNTIME_AUTH(orgId, machineId));
@@ -28152,6 +28251,22 @@ var ParallClient = class _ParallClient {
28152
28251
  async postBrowseResponse(requestId, response) {
28153
28252
  return this.request("POST", ENDPOINTS.MACHINES_ME_BROWSE_RESPONSE(requestId), response);
28154
28253
  }
28254
+ /**
28255
+ * `POST /machines/me/browser-profiles/viewer-response/{requestId}` — daemon
28256
+ * reply to a `machine.browser_profile.viewer` control command. Wakes the
28257
+ * api-server request/reply bridge (mirrors {@link postBrowseResponse}).
28258
+ */
28259
+ async postBrowserProfileViewerResponse(requestId, response) {
28260
+ return this.request("POST", ENDPOINTS.MACHINES_ME_BROWSER_PROFILE_VIEWER_RESPONSE(requestId), response);
28261
+ }
28262
+ /**
28263
+ * `POST /orgs/{orgId}/browser-profiles/{profileId}/viewer/command` — drive the
28264
+ * hosted browser live viewer (WebRTC signaling + tab nav). Authz: profile
28265
+ * owner or org admin. api-server brokers the command to the host daemon.
28266
+ */
28267
+ async browserViewerCommand(orgId, profileId, req, opts) {
28268
+ return this.request("POST", ENDPOINTS.BROWSER_PROFILE_VIEWER_COMMAND(orgId, profileId), req, void 0, false, opts);
28269
+ }
28155
28270
  async resizeMachine(orgId, machineId, spec) {
28156
28271
  return this.request("PATCH", ENDPOINTS.MACHINE_SPEC(orgId, machineId), spec);
28157
28272
  }
@@ -28244,7 +28359,7 @@ var ParallClient = class _ParallClient {
28244
28359
  * Returns null when the server responds with 304 (config unchanged).
28245
28360
  */
28246
28361
  async getPlatformConfig(currentVersion) {
28247
- const url = `${this.baseUrl}${ENDPOINTS.PLATFORM_CONFIG}`;
28362
+ const url = `${this.baseUrlFor(ENDPOINTS.PLATFORM_CONFIG)}${ENDPOINTS.PLATFORM_CONFIG}`;
28248
28363
  const extra = {};
28249
28364
  if (currentVersion !== void 0) {
28250
28365
  extra["If-None-Match"] = currentVersion;
@@ -28410,6 +28525,55 @@ var ParallClient = class _ParallClient {
28410
28525
  async getScheduleRun(orgId, runId) {
28411
28526
  return this.request("GET", ENDPOINTS.SCHEDULE_RUN(orgId, runId));
28412
28527
  }
28528
+ // ---- External triggers (org-scoped) ----
28529
+ async createExternalConnection(orgId, input) {
28530
+ return this.request("POST", ENDPOINTS.EXTERNAL_CONNECTIONS(orgId), input);
28531
+ }
28532
+ async listExternalConnections(orgId, filters) {
28533
+ return this.request("GET", ENDPOINTS.EXTERNAL_CONNECTIONS(orgId), void 0, filters);
28534
+ }
28535
+ async getExternalConnection(orgId, connectionId) {
28536
+ return this.request("GET", ENDPOINTS.EXTERNAL_CONNECTION(orgId, connectionId));
28537
+ }
28538
+ async updateExternalConnection(orgId, connectionId, patch) {
28539
+ return this.request("PATCH", ENDPOINTS.EXTERNAL_CONNECTION(orgId, connectionId), patch);
28540
+ }
28541
+ async regenerateExternalConnectionIngressToken(orgId, connectionId) {
28542
+ return this.request("POST", ENDPOINTS.EXTERNAL_CONNECTION_INGRESS_TOKEN_REGENERATE(orgId, connectionId));
28543
+ }
28544
+ async deleteExternalConnection(orgId, connectionId) {
28545
+ return this.request("DELETE", ENDPOINTS.EXTERNAL_CONNECTION(orgId, connectionId));
28546
+ }
28547
+ async getExternalTriggerSchema(orgId, connectionId) {
28548
+ return this.request("GET", ENDPOINTS.EXTERNAL_TRIGGER_SCHEMA(orgId, connectionId));
28549
+ }
28550
+ async listExternalIngressEvents(orgId, filters) {
28551
+ return this.request("GET", ENDPOINTS.EXTERNAL_INGRESS_EVENTS(orgId), void 0, filters);
28552
+ }
28553
+ async getExternalIngressEvent(orgId, eventId) {
28554
+ return this.request("GET", ENDPOINTS.EXTERNAL_INGRESS_EVENT(orgId, eventId));
28555
+ }
28556
+ async createExternalTrigger(orgId, input) {
28557
+ return this.request("POST", ENDPOINTS.EXTERNAL_TRIGGERS(orgId), input);
28558
+ }
28559
+ async listExternalTriggers(orgId, filters) {
28560
+ return this.request("GET", ENDPOINTS.EXTERNAL_TRIGGERS(orgId), void 0, filters);
28561
+ }
28562
+ async getExternalTrigger(orgId, triggerId) {
28563
+ return this.request("GET", ENDPOINTS.EXTERNAL_TRIGGER(orgId, triggerId));
28564
+ }
28565
+ async updateExternalTrigger(orgId, triggerId, patch) {
28566
+ return this.request("PATCH", ENDPOINTS.EXTERNAL_TRIGGER(orgId, triggerId), patch);
28567
+ }
28568
+ async deleteExternalTrigger(orgId, triggerId) {
28569
+ return this.request("DELETE", ENDPOINTS.EXTERNAL_TRIGGER(orgId, triggerId));
28570
+ }
28571
+ async listExternalTriggerRuns(orgId, filters) {
28572
+ return this.request("GET", ENDPOINTS.EXTERNAL_TRIGGER_RUNS(orgId), void 0, filters);
28573
+ }
28574
+ async getExternalTriggerRun(orgId, runId) {
28575
+ return this.request("GET", ENDPOINTS.EXTERNAL_TRIGGER_RUN(orgId, runId));
28576
+ }
28413
28577
  // ---- Wikis (org-scoped) ----
28414
28578
  async createWiki(orgId, data) {
28415
28579
  return this.request("POST", ENDPOINTS.WIKIS(orgId), data);
@@ -28424,8 +28588,35 @@ var ParallClient = class _ParallClient {
28424
28588
  async getWikiTree(orgId, wikiId, params) {
28425
28589
  return this.request("GET", ENDPOINTS.WIKI_TREE(orgId, wikiId), void 0, params);
28426
28590
  }
28591
+ /**
28592
+ * Resolve a server-returned, host-relative media URL (a wiki `signed_url`
28593
+ * like `/wiki/v1/signed/files?token=...`) against this client's base origin,
28594
+ * so it can be dropped straight into a browser `<img>`/`<video>`/`<iframe>`
28595
+ * `src`.
28596
+ *
28597
+ * wiki-service returns these relative on purpose — it doesn't know its own
28598
+ * public origin. A relative `src` resolves against the *page* origin, which
28599
+ * only works when the page and wiki-service share an origin (local dev:
28600
+ * same-origin + Next.js `/wiki/*` proxy). In deployed envs the app
28601
+ * (app.parall.com) and wiki-service (api.parall.com) are different origins,
28602
+ * so `app.parall.com/wiki/v1/signed/files` hits the SPA's own `/wiki/[...]`
28603
+ * catch-all route — an `<iframe>` then recursively renders the whole app
28604
+ * instead of the file. Prefixing with the wiki base (the exact origin every
28605
+ * wiki API request already uses — `baseUrlFor` resolves `/wiki/v1` paths to
28606
+ * `wikiBaseUrl`) makes the URL absolute against the origin that actually
28607
+ * serves the bytes. An empty base (local dev, same-origin proxy) leaves it
28608
+ * relative, preserving the proxy path.
28609
+ */
28610
+ absoluteMediaUrl(url) {
28611
+ if (/^https?:\/\//i.test(url))
28612
+ return url;
28613
+ return `${this.baseUrlFor(url)}${url}`;
28614
+ }
28427
28615
  async getWikiBlob(orgId, wikiId, params) {
28428
- return this.request("GET", ENDPOINTS.WIKI_BLOB(orgId, wikiId), void 0, params);
28616
+ const blob = await this.request("GET", ENDPOINTS.WIKI_BLOB(orgId, wikiId), void 0, params);
28617
+ if (blob.signed_url)
28618
+ blob.signed_url = this.absoluteMediaUrl(blob.signed_url);
28619
+ return blob;
28429
28620
  }
28430
28621
  async getWikiNodeSections(orgId, wikiId, params) {
28431
28622
  return this.request("GET", ENDPOINTS.WIKI_NODE_SECTIONS(orgId, wikiId), void 0, params);
@@ -28515,7 +28706,10 @@ var ParallClient = class _ParallClient {
28515
28706
  * token — don't leak it.
28516
28707
  */
28517
28708
  async getWikiFilePreviewUrl(orgId, wikiId, params) {
28518
- return this.request("POST", ENDPOINTS.WIKI_FILE_PREVIEW_URL(orgId, wikiId), params);
28709
+ const res = await this.request("POST", ENDPOINTS.WIKI_FILE_PREVIEW_URL(orgId, wikiId), params);
28710
+ if (res.url)
28711
+ res.url = this.absoluteMediaUrl(res.url);
28712
+ return res;
28519
28713
  }
28520
28714
  // ---- Wiki Path Scopes (AFCS ACL) ----
28521
28715
  async getWikiPathScopes(orgId, wikiId) {
@@ -28528,6 +28722,17 @@ var ParallClient = class _ParallClient {
28528
28722
  async deleteWikiPathScope(orgId, wikiId, scopeId) {
28529
28723
  await this.request("DELETE", ENDPOINTS.WIKI_PATH_SCOPE(orgId, wikiId, scopeId));
28530
28724
  }
28725
+ // ---- Wiki Path Restrictions (narrowing ACL — private subtrees) ----
28726
+ async getWikiRestrictions(orgId, wikiId) {
28727
+ const res = await this.request("GET", ENDPOINTS.WIKI_RESTRICTIONS(orgId, wikiId));
28728
+ return res.data;
28729
+ }
28730
+ async createWikiRestriction(orgId, wikiId, data) {
28731
+ return this.request("POST", ENDPOINTS.WIKI_RESTRICTIONS(orgId, wikiId), data);
28732
+ }
28733
+ async deleteWikiRestriction(orgId, wikiId, restrictionId) {
28734
+ await this.request("DELETE", ENDPOINTS.WIKI_RESTRICTION(orgId, wikiId, restrictionId));
28735
+ }
28531
28736
  async getWikiAccessStatus(orgId, wikiId, path7) {
28532
28737
  return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path7 ? { path: path7 } : void 0);
28533
28738
  }
@@ -28690,6 +28895,10 @@ var ParallClient = class _ParallClient {
28690
28895
  async updateClip(orgId, clipId, req) {
28691
28896
  return this.request("PATCH", ENDPOINTS.CLIP(orgId, clipId), req);
28692
28897
  }
28898
+ /** Atomically set display_name and/or description on multiple clip instances (application metadata). */
28899
+ async bulkUpdateClipMetadata(orgId, req) {
28900
+ return this.request("POST", ENDPOINTS.CLIPS_BULK_METADATA(orgId), req);
28901
+ }
28693
28902
  async deleteClip(orgId, clipId) {
28694
28903
  await this.request("DELETE", ENDPOINTS.CLIP(orgId, clipId));
28695
28904
  }
@@ -28716,6 +28925,9 @@ var ParallClient = class _ParallClient {
28716
28925
  const resp = await this.request("GET", ENDPOINTS.CLIP_ONLINE(orgId));
28717
28926
  return resp.data;
28718
28927
  }
28928
+ /** Org-wide browser-profile discovery list. Returns the sanitized
28929
+ * {@link BrowserProfileListItem} shape (not the full domain model), each row
28930
+ * carrying a per-viewer `can_open` control hint. */
28719
28931
  async listBrowserProfiles(orgId) {
28720
28932
  const resp = await this.request("GET", ENDPOINTS.BROWSER_PROFILES(orgId));
28721
28933
  return resp.data;
@@ -29369,6 +29581,9 @@ function resolveStepTarget(event) {
29369
29581
  if (event.type === "schedule" || event.targetId.startsWith("sch_")) {
29370
29582
  return { target_type: "schedule", target_id: event.targetId };
29371
29583
  }
29584
+ if (event.type === "external_trigger" || event.targetId.startsWith("xtr_")) {
29585
+ return { target_type: "external_trigger", target_id: event.targetId };
29586
+ }
29372
29587
  if (event.type === "wiki_comment") {
29373
29588
  return { target_type: "wiki", target_id: event.targetId || void 0 };
29374
29589
  }
@@ -29573,6 +29788,18 @@ var ParallAgentGateway = class {
29573
29788
  } catch (err) {
29574
29789
  this.opts.log?.error(`schedule fire dispatch failed for ${data.source_id}: ${String(err)}`);
29575
29790
  }
29791
+ } else if (data.event_type === "external_trigger") {
29792
+ if (!data.source_id)
29793
+ return;
29794
+ try {
29795
+ const dispatched = await this.fetchAndHandleExternalTriggerRun(data.source_id);
29796
+ if (dispatched) {
29797
+ this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {
29798
+ });
29799
+ }
29800
+ } catch (err) {
29801
+ this.opts.log?.error(`external trigger dispatch failed for ${data.source_id}: ${String(err)}`);
29802
+ }
29576
29803
  } else if (data.event_type === "approval_decided") {
29577
29804
  if (!data.source_id)
29578
29805
  return;
@@ -29651,8 +29878,13 @@ var ParallAgentGateway = class {
29651
29878
  target_type: target.target_type,
29652
29879
  target_id: target.target_id,
29653
29880
  content: {
29654
- 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",
29655
- 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 },
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",
29882
+ 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
+ trigger_id: event.targetId,
29884
+ run_id: event.messageId,
29885
+ connection_id: event.externalConnectionId,
29886
+ ingress_event_id: event.externalIngressEventId
29887
+ } : event.type === "approval" ? { approval_id: event.messageId } : { message_id: event.messageId },
29656
29888
  sender_id: event.senderId,
29657
29889
  sender_name: event.senderName,
29658
29890
  summary: event.body.substring(0, 200),
@@ -30221,7 +30453,7 @@ var ParallAgentGateway = class {
30221
30453
  this.dispatchState.mainDispatching = true;
30222
30454
  void this.drainMainBuffer();
30223
30455
  }
30224
- }, 5e3);
30456
+ }, 5e3).unref();
30225
30457
  }
30226
30458
  }
30227
30459
  }
@@ -30688,6 +30920,62 @@ var ParallAgentGateway = class {
30688
30920
  }
30689
30921
  return dispatched;
30690
30922
  }
30923
+ async fetchAndHandleExternalTriggerRun(runId) {
30924
+ let run = null;
30925
+ try {
30926
+ run = await this.opts.client.getExternalTriggerRun(this.opts.config.org_id, runId);
30927
+ } catch (err) {
30928
+ const status = err?.status;
30929
+ if (status === 404) {
30930
+ this.opts.log?.warn(`external trigger run ${runId} not accessible (404), acking stale dispatch`);
30931
+ return true;
30932
+ }
30933
+ this.opts.log?.warn(`external trigger run fetch failed for ${runId}, leaving pending: ${String(err)}`);
30934
+ return false;
30935
+ }
30936
+ if (!run)
30937
+ return true;
30938
+ return this.handleExternalTriggerRun(run);
30939
+ }
30940
+ async handleExternalTriggerRun(run) {
30941
+ if (this.shuttingDown)
30942
+ return false;
30943
+ const dedupeKey = `external_trigger_run:${run.id}`;
30944
+ if (this.dispatchedTasks.has(dedupeKey))
30945
+ return false;
30946
+ this.dispatchedTasks.add(dedupeKey);
30947
+ this.opts.log?.info(`external trigger fired: ${run.id} (trigger ${run.trigger_id})`);
30948
+ const attachedUri = typeof run.trigger_snapshot?.attached_to_uri === "string" ? run.trigger_snapshot.attached_to_uri : void 0;
30949
+ const event = {
30950
+ type: "external_trigger",
30951
+ targetId: run.trigger_id,
30952
+ targetName: run.trigger_name || void 0,
30953
+ targetType: "external_trigger",
30954
+ senderId: "system",
30955
+ senderName: "external",
30956
+ messageId: run.id,
30957
+ body: run.agent_input_body ?? "",
30958
+ externalConnectionId: run.connection_id,
30959
+ externalConnectionSourceType: run.connection_source_type || void 0,
30960
+ externalConnectionDisplayName: run.connection_display_name || void 0,
30961
+ externalIngressEventId: run.ingress_event_id,
30962
+ externalIngressEventType: run.ingress_event_type || void 0,
30963
+ attachedUri,
30964
+ ackSourceType: "external_trigger_run",
30965
+ ackSourceId: run.id
30966
+ };
30967
+ let dispatched;
30968
+ try {
30969
+ dispatched = await this.handleInboundEvent(event);
30970
+ } catch (err) {
30971
+ this.dispatchedTasks.delete(dedupeKey);
30972
+ throw err;
30973
+ }
30974
+ if (!dispatched) {
30975
+ this.dispatchedTasks.delete(dedupeKey);
30976
+ }
30977
+ return dispatched;
30978
+ }
30691
30979
  async fetchAndHandleApprovalDecided(approvalId, actorId, chatId) {
30692
30980
  let approval = null;
30693
30981
  try {
@@ -30798,6 +31086,8 @@ var ParallAgentGateway = class {
30798
31086
  dispatched = await this.handleWikiComment(item.source_id, item.actor_id, item.delivery_reason);
30799
31087
  } else if (item.event_type === "schedule.fire" && item.source_id) {
30800
31088
  dispatched = await this.fetchAndHandleScheduleFire(item.source_id, item.actor_id);
31089
+ } else if (item.event_type === "external_trigger" && item.source_id) {
31090
+ dispatched = await this.fetchAndHandleExternalTriggerRun(item.source_id);
30801
31091
  } else if (item.event_type === "approval_decided" && item.source_id) {
30802
31092
  dispatched = await this.fetchAndHandleApprovalDecided(item.source_id, item.actor_id, item.chat_id ?? null);
30803
31093
  } 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.34.0",
3
+ "version": "1.36.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.34.0",
20
- "@parall/sdk": "1.34.0"
19
+ "@parall/sdk": "1.36.0",
20
+ "@parall/agent-core": "1.36.0"
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`.
@@ -161,9 +161,15 @@ Every entity is addressable with a `prll://` URI. Common prefixes you'll see in
161
161
  | `prll://prj_` | Project | parall-tasks |
162
162
  | `prll://sch_` | Schedule (time trigger) | parall-schedules |
163
163
  | `prll://srn_` | Schedule run (single fire audit record; carries fire-time snapshot) | parall-schedules |
164
+ | `prll://xcn_` | External Trigger Connection (incoming endpoint) | parall-external-triggers |
165
+ | `prll://xin_` | External Trigger Event (single incoming event audit record) | parall-external-triggers |
166
+ | `prll://xtr_` | External Trigger (incoming trigger configuration) | parall-external-triggers |
167
+ | `prll://xrn_` | External Trigger run (single matched dispatch audit record) | parall-external-triggers |
164
168
  | `prll://wik_` | Wiki | parall-wiki |
165
169
  | `prll://att_` | Attachment | parall-platform (files) |
166
170
 
167
171
  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
172
 
173
+ 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
+
169
175
  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
@@ -7,129 +7,175 @@ description: "Parall wiki operations: read, search, edit, and propose changes to
7
7
 
8
8
  Manage organization wikis via the Parall CLI. Auth is pre-configured.
9
9
 
10
- ## Browsing (no local state needed)
10
+ ## Mental model
11
+
12
+ Wiki editing works on a **local workspace**: `parall wiki sync` downloads the
13
+ wiki into a directory on disk, you edit those files with your normal file
14
+ tools, then `parall wiki changeset create` uploads the result as a proposal.
15
+ There is no git in the workspace — your edits are detected by diffing against
16
+ the synced baseline.
17
+
18
+ Key facts the commands won't tell you:
19
+
20
+ - **Workspace location is fixed.** Sync output and `parall wiki status` print
21
+ the absolute workspace path (`synced → /path/to/<slug>` / `Mount: ...`).
22
+ Always address wiki files by that absolute path — your shell cwd is usually
23
+ NOT inside the workspace.
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.
29
+ - **`cat`, `search`, `query`, `outline`, and `section` read your local
30
+ workspace copy when it exists** — including your own unproposed edits. Add
31
+ `--remote` to `cat` to read the server version instead.
32
+ - **`search`/`query` are keyword (lexical) matching**, not semantic search.
33
+ `query` additionally ranks whole documents — better for multi-word
34
+ questions; `search` for a single identifier.
35
+ - The `<wiki>` argument is the slug or ID from `parall wiki list`; with a
36
+ single wiki in the org it can be omitted.
37
+
38
+ ## Core workflow
11
39
 
12
40
  ```bash
13
- parall wiki list # List all wikis
14
- parall wiki tree <slug> # List files and directories
15
- parall wiki tree <slug> --path docs/ # List a subdirectory
41
+ parall wiki sync # 1. get/update files (prints workspace path)
42
+ # 2. read + edit files under the workspace path with standard file tools
43
+ parall wiki diff <wiki> # 3. review exactly what you'll propose
44
+ parall wiki changeset create <wiki> --title "..." # 4. submit
16
45
  ```
17
46
 
18
- ## Editing
47
+ Always sync before starting and always check `diff` before proposing — the
48
+ changeset uploads the full content of every changed file.
19
49
 
20
- Wiki editing works on a **local workspace** — a directory on disk where wiki
21
- files are synced. You sync from the server, edit files locally, then propose
22
- a changeset.
50
+ Unprotected paths auto-merge immediately (`auto_merged: true`); protected
51
+ paths stay open for human review. Follow the returned `next_action` either way.
23
52
 
24
- ### Step 1: Sync
53
+ ## Stale base (server moved since your sync)
25
54
 
26
- ```bash
27
- parall wiki sync <slug>
28
- ```
29
-
30
- This downloads wiki files to a local directory. The output includes the
31
- **absolute mount path** for each wiki (e.g. `synced → /path/to/workspace/kb`).
32
-
33
- ### Step 2: Find the mount path
34
-
35
- The sync output JSON contains `synced[].path` — the absolute path where files
36
- live. You can also check it anytime with:
55
+ If files changed on the server after your last sync, `changeset create` is
56
+ rejected both by a CLI precheck and by the server (409 `STALE_BASE`) — so
57
+ you can't silently overwrite someone's concurrent edit. Recovery:
37
58
 
38
59
  ```bash
39
- parall wiki status <slug>
60
+ parall wiki sync # pull latest; your local edits are preserved
61
+ # if a file conflicts, resolve it (see next section)
62
+ parall wiki changeset create <wiki> --title "..."
40
63
  ```
41
64
 
42
- The output includes `Mount: /absolute/path/to/<slug>`.
43
-
44
- Use this path with `read`, `write`, and `edit` tools. For example, if the mount
45
- is `/data/.openclaw/workspace/kb`, then `README.md` is at
46
- `/data/.openclaw/workspace/kb/README.md`.
65
+ ## Sync conflicts
47
66
 
48
- ### Step 3: Edit files
67
+ `sync` three-way merges. When both you and the server changed the same file,
68
+ your file is left intact and the upstream copy lands under
69
+ `<workspace>/.parall-wiki/conflicts/`:
49
70
 
50
- Use standard file tools (`read`, `write`, `edit`) on files under the mount path.
71
+ | Marker | Meaning |
72
+ |--------|---------|
73
+ | `conflicts/<path>.remote` | Server has different content for `<path>` |
74
+ | `conflicts/<path>.remote-deleted` | Server deleted `<path>`; you still have edits |
51
75
 
52
- ### Step 4: Review changes
76
+ All paths below are relative to the workspace root. Pick one:
53
77
 
54
78
  ```bash
55
- parall wiki diff <slug> # Shows unified diff of all local changes
56
- parall wiki status <slug> # Shows which files changed with +/- line counts
79
+ # Accept upstream (drop your edit):
80
+ cp <workspace>/.parall-wiki/conflicts/<path>.remote <workspace>/<path>
81
+
82
+ # Keep yours / hand-merge: edit <workspace>/<path> to final content, then
83
+ parall wiki changeset create <wiki> --title "Reconcile <path>"
84
+
85
+ # Accept server delete (.remote-deleted only):
86
+ rm <workspace>/<path>
57
87
  ```
58
88
 
59
- Always review before proposing.
89
+ Then re-run `parall wiki sync` and delete the used marker file. Conflicts
90
+ exit 0 (they need your decision); `failed[]` entries (download error,
91
+ shape-conflict) exit 1 and retry on the next sync.
60
92
 
61
- ### Step 5: Propose changeset
93
+ ## Changesets
62
94
 
63
95
  ```bash
64
- parall wiki changeset create <slug> --title "Description of changes"
96
+ parall wiki changeset list <wiki>
97
+ parall wiki changeset show <changesetId> <wiki> # status + feedback
98
+ parall wiki changeset diff <changesetId> <wiki>
99
+ parall wiki changeset create <wiki> --update <id> # re-propose after rejection (title inherited)
65
100
  ```
66
101
 
67
- This uploads your local changes. Unprotected paths auto-merge immediately; the
68
- CLI prints `auto_merged: true` and refreshes the local manifest. Protected
69
- paths remain as a changeset for review; follow the returned `next_action`.
102
+ Rejected: read the feedback (`show` / `status`), fix the files, re-propose
103
+ with `--update <id>`. Conflict status: `sync`, resolve, then `--update <id>`.
70
104
 
71
- ## Changeset Management
105
+ Re-propose REPLACES the changeset's previous contents with your current
106
+ workspace diff — to withdraw a file from the proposal, revert it locally
107
+ (restore the synced content) and re-propose; it drops out of the changeset.
72
108
 
73
- ```bash
74
- parall wiki changeset list <slug> # List all changesets
75
- parall wiki changeset show <changesetId> <slug> # Show detail + feedback
76
- parall wiki changeset diff <changesetId> <slug> # Show changeset diff
77
- ```
109
+ ## Binary files
78
110
 
79
- If a changeset is rejected, fix the files locally and re-propose:
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).
80
116
 
81
117
  ```bash
82
- parall wiki changeset create <slug> --update <changesetId>
83
- ```
118
+ # Maintainer: direct-commit a binary to the default branch (no review)
119
+ parall wiki file upload ./diagram.png docs/assets/diagram.png
84
120
 
85
- The title is inherited from the original changeset no need to repeat it.
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
86
126
 
87
- ## Handling sync conflicts
127
+ # Remove a binary from the default branch (git history still has it)
128
+ parall wiki file delete docs/assets/diagram.png
129
+ ```
88
130
 
89
- `wiki sync` runs a three-way merge. When both you and the server changed the
90
- same file, sync **does not overwrite your work**. It leaves your file intact
91
- and drops the upstream version under `.parall-wiki/conflicts/`:
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.
92
134
 
93
- | Marker | Meaning |
94
- |--------|---------|
95
- | `.parall-wiki/conflicts/<path>.remote` | Server has different content (concurrent edit, new file collision, or server changed a file you deleted) |
96
- | `.parall-wiki/conflicts/<path>.remote-deleted` | Server deleted the file; you still have edits |
135
+ ### Reader: propose markdown that embeds an image
97
136
 
98
- stderr prints one line per conflict. Recovery:
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:
99
139
 
100
- **Accept upstream** (drop your edit):
101
140
  ```bash
102
- cp .parall-wiki/conflicts/<path>.remote <path>
103
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
104
146
  ```
105
147
 
106
- **Keep yours** (or hand-merge, then propose):
107
- ```bash
108
- # edit <path> to final content
109
- parall wiki diff <slug>
110
- parall wiki changeset create <slug> --title "Reconcile <path>"
111
- parall wiki sync # fast-forwards after server merges
112
- ```
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
+
153
+ ## Discovery & history
113
154
 
114
- **Accept server delete** (`.remote-deleted` only):
115
155
  ```bash
116
- rm <path>
117
- parall wiki sync
156
+ parall wiki query "how is auth configured" <wiki> # multi-word lookup (query FIRST, wiki second)
157
+ parall wiki search "JWT" <wiki> # single keyword (query FIRST, wiki second)
158
+ parall wiki outline <wiki> --path docs/ # heading structure
159
+ parall wiki cat docs/auth.md <wiki> # print a file (--remote for server version)
160
+ parall wiki tree <wiki> # list files
161
+ parall wiki log <wiki> # recent operations
162
+ parall wiki log <wiki> docs/auth.md # per-file commit history
118
163
  ```
119
164
 
120
- Sync failures (`download`, `shape-conflict`) set exit code 1 and retry on
121
- next sync. Conflicts exit 0 — they need your decision, not a retry.
122
-
123
- ## Discarding local changes
165
+ ## Permissions
124
166
 
125
- ```bash
126
- parall wiki reset <slug> # Restore all files to last synced state
127
- ```
167
+ Reads and writes are ACL-checked server-side per path. `parall wiki access
168
+ <path>` shows your level for a path. On a `PERMISSION_DENIED`, errors include
169
+ a `Request approval:` hint — use `parall wiki request-access <path> --reason
170
+ "..."` to file an approval card for a maintainer, then re-sync after approval.
128
171
 
129
- ## History
172
+ ## Recovery
130
173
 
131
174
  ```bash
132
- parall wiki log <slug> # Recent wiki operations
175
+ parall wiki reset <wiki> # discard ALL local edits, restore last-synced state
176
+ parall wiki status <wiki> # local changes + your changesets, anytime
133
177
  ```
134
178
 
135
- CLI success output is JSON; errors may add a plain-text `Request approval:` line on a `PERMISSION_DENIED` — read both. Run `parall wiki --help` for full options.
179
+ CLI success output is JSON on stdout (human summary on stderr); errors state
180
+ the reason and the next step — follow them. `parall wiki --help` for the
181
+ full command list.