@parall/agent-core 1.42.0 → 1.43.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.
Files changed (45) hide show
  1. package/dist/bin/channel-exec.d.ts +4 -0
  2. package/dist/bin/channel-exec.d.ts.map +1 -0
  3. package/dist/bin/channel-exec.js +246 -0
  4. package/dist/channel-capability.d.ts +16 -0
  5. package/dist/channel-capability.d.ts.map +1 -0
  6. package/dist/channel-capability.js +155 -0
  7. package/dist/channel-token.d.ts +19 -0
  8. package/dist/channel-token.d.ts.map +1 -0
  9. package/dist/channel-token.js +73 -0
  10. package/dist/event-format.d.ts.map +1 -1
  11. package/dist/event-format.js +21 -16
  12. package/dist/gateway-base.d.ts +15 -0
  13. package/dist/gateway-base.d.ts.map +1 -1
  14. package/dist/gateway-base.js +113 -38
  15. package/dist/gateway-lane-flow.d.ts +20 -1
  16. package/dist/gateway-lane-flow.d.ts.map +1 -1
  17. package/dist/gateway-lane-flow.js +78 -6
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +3 -0
  21. package/dist/platform-config.d.ts +15 -0
  22. package/dist/platform-config.d.ts.map +1 -1
  23. package/dist/platform-config.js +28 -0
  24. package/dist/prompt-fragments.d.ts +1 -1
  25. package/dist/prompt-fragments.d.ts.map +1 -1
  26. package/dist/prompt-fragments.js +29 -7
  27. package/dist/skills/index.js +1 -1
  28. package/dist/skills/parall-platform.d.ts +1 -1
  29. package/dist/skills/parall-platform.d.ts.map +1 -1
  30. package/dist/skills/parall-platform.js +23 -4
  31. package/dist/types.d.ts +5 -1
  32. package/dist/types.d.ts.map +1 -1
  33. package/package.json +2 -2
  34. package/src/bin/channel-exec.ts +262 -0
  35. package/src/channel-capability.ts +187 -0
  36. package/src/channel-token.ts +92 -0
  37. package/src/event-format.ts +21 -16
  38. package/src/gateway-base.ts +137 -39
  39. package/src/gateway-lane-flow.ts +92 -4
  40. package/src/index.ts +3 -0
  41. package/src/platform-config.ts +44 -0
  42. package/src/prompt-fragments.ts +29 -7
  43. package/src/skills/index.ts +1 -1
  44. package/src/skills/parall-platform.ts +23 -4
  45. package/src/types.ts +5 -1
@@ -197,10 +197,10 @@ resolves and renders the entity title automatically.
197
197
 
198
198
  prll://usr_xxx user prll://prj_xxx project
199
199
  prll://tsk_xxx task prll://wik_xxx wiki
200
- prll://msg_xxx message prll://tcm_xxx task comment
201
- prll://cht_xxx chat prll://ase_xxx agent session
202
- prll://att_xxx attachment prll://sch_xxx schedule
203
- prll://srn_xxx schedule run
200
+ prll://msg_xxx message prll://cmt_xxx comment
201
+ prll://cht_xxx chat prll://tcm_xxx task comment (legacy)
202
+ prll://att_xxx attachment prll://ase_xxx agent session
203
+ prll://sch_xxx schedule prll://srn_xxx schedule run
204
204
 
205
205
  **Wiki** — path is file path, fragment is a typed anchor:
206
206
 
@@ -246,15 +246,37 @@ session already has continuity, so skip the fetch unless something is unclear.
246
246
 
247
247
  Same pattern for any other entity referenced in the event: \`tasks get\`,
248
248
  \`projects get\`, \`users get\`, \`chats get\`. Follow the reflink, don't ask.
249
+ When one entity isn't enough — you need what's *around* it — walk the
250
+ reference graph instead of guessing (see "Walk the reference graph" below).
249
251
 
250
252
  ### Find context with search first
251
253
 
252
254
  Reach for unified semantic search before paging chat history:
253
255
 
254
- parall search "pricing decision june" [--limit 10]
256
+ parall search "pricing decision june" --limit 10
255
257
 
256
- It spans messages, tasks, and wiki. Page \`messages list\` only for the verbatim
257
- recent flow of one chat, not for discovery.
258
+ It spans messages, tasks, wiki, and comments. Page \`messages list\` only for the
259
+ verbatim recent flow of one chat, not for discovery.
260
+
261
+ ### Walk the reference graph
262
+
263
+ References form a traversable graph, and you can query it — don't stop at
264
+ fetching entities one by one:
265
+
266
+ # entity metadata (title, status, preview)
267
+ parall refs resolve prll://tsk_xxx prll://wik_xxx
268
+ # who references this entity
269
+ parall refs backlinks prll://tsk_xxx
270
+ # connected sub-graph around it
271
+ parall refs graph prll://tsk_xxx --depth 2
272
+
273
+ Use \`refs backlinks\` when you need "where is this discussed / used"; use
274
+ \`refs graph\` when you need the full picture around an entity (related tasks,
275
+ docs, conversations — edges carry the author's annotation for why they linked).
276
+ Then \`refs resolve\` the interesting node URIs in one batch to get titles and
277
+ status. \`refs graph\` takes entity-level URIs only (\`prll://wik_xxx\`, not
278
+ \`prll://wik_xxx/docs/a.md\`). All results are filtered to what you can see.
279
+ Details: parall-platform skill.
258
280
 
259
281
  ### File attachments
260
282
 
@@ -15,7 +15,7 @@ import { PARALL_CLIPS_SKILL } from './parall-clips.js';
15
15
  export const SKILLS = [
16
16
  {
17
17
  name: 'parall-platform',
18
- description: "Parall platform queries and lightweight agent provisioning: list org members, agents, chats, read message history, check identity, or create another agent. Use when: user asks about org members, who's online, chat history, agent list, creating an agent, or identity/auth questions.",
18
+ description: "Parall platform queries and lightweight agent provisioning: list org members, agents, chats, read message history, check identity, create another agent, or walk the prll:// reference graph (resolve URIs, backlinks, multi-hop graph). Use when: user asks about org members, who's online, chat history, agent list, creating an agent, identity/auth questions, or you need to find what an entity is connected to / who references it.",
19
19
  content: PARALL_PLATFORM_SKILL,
20
20
  },
21
21
  {
@@ -1,2 +1,2 @@
1
- export declare const PARALL_PLATFORM_SKILL = "# Parall Platform\n\nQuery organization data via the Parall CLI. Auth is pre-configured.\n\n## Identity\n\n```bash\nparall whoami\n```\n\n## Members & Agents\n\n```bash\nparall members list # All org members (humans + agents)\nparall agents list # Agents only\nparall users get prll://usr_xxx # Get user details by ID\n```\n\nCreate a hosted agent when the user asks for a Parall-managed runtime. Hosted\nprovisioning is asynchronous: creation means the agent identity, API key, and\nmachine record were accepted, not that the runtime is online yet. Use `--wait`\nto wait until the machine reaches `running`, and use `--wait-online` when the\ntask requires the child agent to be connected before you report completion.\nFor hosted agents, use `--discard-api-key`; the server injects the one-time key\ninto the hosted runtime, so the parent agent must not print or persist it.\n\nCreate a self-hosted agent only when the runtime will be connected outside\nParall-managed compute. In that case, write the one-time `api_key` to\n`--api-key-file` so it is not captured in tool-result logs. Treat `api_key` as a\nsecret: do not print, read aloud, post it in shared chats, or echo the file\ncontents. Include the `user.id` in normal responses, and pass the key file only\nthrough an explicit secure runtime handoff when connection is required. Never\nuse `--show-api-key` from an agent runtime. Agent callers cannot set provider\noverrides until the dedicated fine-grained permission flow lands.\n\n```bash\n# Hosted runtime (Parall-managed compute)\nparall agents create \\\n --name \"Research Agent\" \\\n --runtime-type codex \\\n --machine-type cloud \\\n --machine-label standard \\\n --discard-api-key \\\n --wait \\\n --wait-online\n\n# Self-hosted runtime\nparall agents create --name \"Research Agent\" --runtime-type codex --api-key-file /tmp/research-agent.api-key\n```\n\nInspect hosted provisioning directly when a create command returns before the\nruntime is online, or when you need logs for a failed machine. If `agents create`\nexits non-zero after creating a hosted agent, read the printed `user.id` and\n`machine.id`, then use these commands to decide whether to wait, inspect logs,\nor report the failed machine for retry.\n\n```bash\nparall machines status prll://mch_xxx\nparall machines logs prll://mch_xxx --lines 100\n```\n\n## Chats & Messages\n\n```bash\nparall chats list # List all chats\nparall messages list prll://cht_xxx # Read chat message history\nparall messages list prll://cht_xxx --since 2026-01-01 # Only messages at/after a date (RFC3339 or YYYY-MM-DD)\n```\n\n## Org-Context Search\n\nBefore deciding or starting non-trivial work, search the org's real history \u2014\npast discussions, decisions, tasks, and wiki notes \u2014 so you don't re-litigate\nsettled questions or repeat known mistakes. This searches live org data\n(semantic + keyword), not a local copy, and is permission-filtered to what you\ncan see.\n\n```bash\n# Semantic + keyword search across messages, tasks, and wiki\nparall search \"auth v5 upgrade\"\n\n# Restrict entity types (m=message, t=task, w=wiki). --channel narrows the\n# MESSAGE hits to one chat (tasks/wiki are unaffected by it).\nparall search \"auth v5 upgrade\" --types m,w --channel prll://cht_eng\n\n# Time-box to recent activity (RFC3339 or YYYY-MM-DD). Narrows messages + tasks;\n# wiki is always matched by relevance (the index has no authored timestamp).\nparall search \"auth v5 upgrade\" --since 2026-01-01\n\n# Narrow wiki hits to a frontmatter document type\nparall search \"deploy steps\" --types w --wiki-type Runbook\n```\n\nEven with zero curated notes, the raw message + task history is searchable \u2014 the\noriginal discussion and its approval/rejection IS the precedent.\n\n## Sending Messages\n\nEach `[Event: message.new]` includes `[Chat: ... (prll://cht_xxx)]` \u2014 use that chat URI to reply.\n\n> **How you pass the message body matters \u2014 your command runs through a shell.**\n> Inside double quotes the shell expands `$`, backticks, and `$(...)` *before*\n> the CLI sees them: `--text \"That costs $1,000\"` sends `That costs ,000`, and\n> `--text \"$(cmd)\"` runs `cmd`. Single quotes instead break on apostrophes\n> (`I'm`, `don't`). So do **not** wrap real message content in quotes \u2014 pass it\n> through `--text-file` (a written file, or a quoted heredoc `<<'EOF'` that\n> disables all expansion). Reserve `--text \"...\"` for short literals with no\n> `$`, backtick, or apostrophe.\n\n```bash\n# One-off reply \u2192 quoted heredoc into stdin. The quoted delimiter <<'EOF'\n# disables ALL shell expansion, so $, backticks and apostrophes pass verbatim.\nparall messages send prll://cht_xxx --text-file - <<'PARALL_EOF'\nSure \u2014 that's $1,000, and $(whoami) stays literal. I'm on it.\nPARALL_EOF\n\n# Longer / multi-line reply \u2192 write it with your file tool (no shell touches\n# the body), then point --text-file at the file.\nparall messages send prll://cht_xxx --text-file /tmp/reply.md\n\n# Short literal with no $, backtick, or apostrophe \u2192 --text is fine.\nparall messages send prll://cht_xxx --text \"On it\"\n\n# Direct message by user URI or display name (same --text-file / heredoc rules)\nparall dm prll://usr_xxx --text-file /tmp/reply.md\nparall dm \"Alice\" --text \"Hello\"\n\n# Thread reply\nparall messages send prll://cht_xxx --text-file /tmp/reply.md --thread-root-id 01JWC...\n\n# FYI message (no response expected \u2014 the recipient sees `[Hint: no_reply]`)\nparall messages send prll://cht_xxx --text \"FYI: done\" --no-reply\n\n# Silence this turn entirely \u2014 no chat message produced. Use when you receive\n# `[Hint: no_reply]` or otherwise decide the turn needs no visible reply.\n# Run BEFORE any `messages send` / `dm`; those still deliver real messages.\nparall no-reply --reason \"ack only, nothing to add\"\n```\n\n## Files & Attachments\n\nAttachments appear in events as `[Attachment: prll://att_xxx | mime | size | name]`.\n\n```bash\n# Download an attachment\nparall files download att_xxx --output /tmp/file.png\n\n# Upload a file (returns attachment_id)\nparall files upload /tmp/report.pdf\n\n# Send a message with a file\nparall messages send prll://cht_xxx --file /tmp/output.png --text \"Done\"\n\n# Send an existing attachment to another chat\nparall messages send prll://cht_xxx --attachment att_xxx --text \"See attached\"\n\n# DM with a file\nparall dm \"Alice\" --file /tmp/report.pdf --text \"Report attached\"\n```\n\n`--file` and `--attachment` are mutually exclusive. A caption (`--text` for\nshort literals, or `--text-file` for anything with `$`, backticks, or quotes)\ncan be combined with either.\n\n## Approvals\n\nWhen a CLI command returns a `PERMISSION_DENIED` error, the output includes the denied `action` and `resource_uri`. Whether that action can be approved is decided by the server (there is no fixed allowlist):\n- If it IS approvable, a `Request approval:` line with a `parall approvals request` command follows \u2014 fill in the placeholders it shows (`--chat`, `--title`, `--reason`) and run it.\n- If it is NOT approvable, the output says so \u2014 ask a human with permission instead of requesting approval.\n\nA different `INVALID_TARGET` error means you addressed the wrong kind of thing (e.g. a `usr_` id where a chat is expected). Follow the message (e.g. use `parall dm` to message a user) \u2014 do not request approval for it.\n\n```bash\n# Request approval (use action and resource_uri from the error)\nparall approvals request --action chat.archive --resource prll://cht_xxx --chat prll://cht_yyy --title \"Archive old channel\" --reason \"No activity in 6 months\"\n\n# Check a specific approval's status\nparall approvals get prll://apr_xxx\n\n# Wait for a decision (blocks until approved/rejected/timeout)\nparall approvals wait prll://apr_xxx --timeout 300\n\n# List all your pending approvals\nparall approvals list\n\n# List available approvable actions\nparall approvals actions\n\n# Cancel a pending request you made\nparall approvals cancel prll://apr_xxx\n```\n\nOnly request approval after receiving an actual `PERMISSION_DENIED` error \u2014 never preemptively. The `--chat` flag specifies where the approval card appears; use the chat where the conversation is happening.\n\n## Reference URIs\n\nEvery entity is addressable with a `prll://` URI. Common prefixes you'll see in events, messages, and schedule descriptions:\n\n| Prefix | Entity | Skill |\n|--------|--------|-------|\n| `prll://usr_` | User (human or agent) | parall-platform |\n| `prll://cht_` | Chat | parall-platform |\n| `prll://msg_` | Message | parall-platform |\n| `prll://tsk_` | Task | parall-tasks |\n| `prll://prj_` | Project | parall-tasks |\n| `prll://sch_` | Schedule (time trigger) | parall-schedules |\n| `prll://srn_` | Schedule run (single fire audit record; carries fire-time snapshot) | parall-schedules |\n| `prll://xcn_` | External Trigger Connection (incoming endpoint) | parall-external-triggers |\n| `prll://xin_` | External Trigger Event (single incoming event audit record) | parall-external-triggers |\n| `prll://xtr_` | External Trigger (incoming trigger configuration) | parall-external-triggers |\n| `prll://xrn_` | External Trigger run (single matched dispatch audit record) | parall-external-triggers |\n| `prll://wik_` | Wiki | parall-wiki |\n| `prll://att_` | Attachment | parall-platform (files) |\n\nWhen 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).\n\nWhen 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).\n\n## References (relationship graph)\n\n`prll://` references between entities form a graph \u2014 a message cites a task, a\ntask cites a wiki page, and so on. Walk it to answer \"what is this decision /\nentity connected to\". All results are permission-filtered to what you can see.\n\n```bash\n# Resolve URIs to entity metadata (titles, status, previews)\nparall refs resolve prll://tsk_xxx prll://wik_xxx\n\n# Single hop \u2014 who references X\nparall refs backlinks prll://tsk_xxx\n\n# Multi-hop \u2014 the connected sub-graph around X (entity-level URI only \u2014 no\n# path/anchor; depth 1\u20134, default 2)\nparall refs graph prll://tsk_xxx --depth 2\n```\n\n`refs graph` traverses both directions (inbound + outbound) and returns `nodes`\nand `edges` with each node's hop `depth`. `truncated: true` means a size cap clipped\nthe result \u2014 narrow it with a smaller `--depth`.\n\nCLI 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 \u2014 read both.\n";
1
+ export declare const PARALL_PLATFORM_SKILL = "# Parall Platform\n\nQuery organization data via the Parall CLI. Auth is pre-configured.\n\n## Identity\n\n```bash\nparall whoami\n```\n\n## Members & Agents\n\n```bash\nparall members list # All org members (humans + agents)\nparall agents list # Agents only\nparall users get prll://usr_xxx # Get user details by ID\n```\n\nCreate a hosted agent when the user asks for a Parall-managed runtime. Hosted\nprovisioning is asynchronous: creation means the agent identity, API key, and\nmachine record were accepted, not that the runtime is online yet. Use `--wait`\nto wait until the machine reaches `running`, and use `--wait-online` when the\ntask requires the child agent to be connected before you report completion.\nFor hosted agents, use `--discard-api-key`; the server injects the one-time key\ninto the hosted runtime, so the parent agent must not print or persist it.\n\nCreate a self-hosted agent only when the runtime will be connected outside\nParall-managed compute. In that case, write the one-time `api_key` to\n`--api-key-file` so it is not captured in tool-result logs. Treat `api_key` as a\nsecret: do not print, read aloud, post it in shared chats, or echo the file\ncontents. Include the `user.id` in normal responses, and pass the key file only\nthrough an explicit secure runtime handoff when connection is required. Never\nuse `--show-api-key` from an agent runtime. Agent callers cannot set provider\noverrides until the dedicated fine-grained permission flow lands.\n\n```bash\n# Hosted runtime (Parall-managed compute)\nparall agents create \\\n --name \"Research Agent\" \\\n --runtime-type codex \\\n --machine-type cloud \\\n --machine-label standard \\\n --discard-api-key \\\n --wait \\\n --wait-online\n\n# Self-hosted runtime\nparall agents create --name \"Research Agent\" --runtime-type codex --api-key-file /tmp/research-agent.api-key\n```\n\nInspect hosted provisioning directly when a create command returns before the\nruntime is online, or when you need logs for a failed machine. If `agents create`\nexits non-zero after creating a hosted agent, read the printed `user.id` and\n`machine.id`, then use these commands to decide whether to wait, inspect logs,\nor report the failed machine for retry.\n\n```bash\nparall machines status prll://mch_xxx\nparall machines logs prll://mch_xxx --lines 100\n```\n\n## Chats & Messages\n\n```bash\nparall chats list # List all chats\nparall messages list prll://cht_xxx # Read chat message history\nparall messages list prll://cht_xxx --since 2026-01-01 # Only messages at/after a date (RFC3339 or YYYY-MM-DD)\n```\n\n## Org-Context Search\n\nBefore deciding or starting non-trivial work, search the org's real history \u2014\npast discussions, decisions, tasks, and wiki notes \u2014 so you don't re-litigate\nsettled questions or repeat known mistakes. This searches live org data\n(semantic + keyword), not a local copy, and is permission-filtered to what you\ncan see.\n\n```bash\n# Semantic + keyword search across messages, tasks, wiki, and comments\nparall search \"auth v5 upgrade\"\n\n# Restrict entity types (m=message, t=task, w=wiki, c=comment). --channel\n# narrows the MESSAGE hits to one chat (tasks/wiki/comments are unaffected).\nparall search \"auth v5 upgrade\" --types m,w --channel prll://cht_eng\n\n# Time-box to recent activity (RFC3339 or YYYY-MM-DD). Narrows messages + tasks;\n# wiki is always matched by relevance (the index has no authored timestamp).\nparall search \"auth v5 upgrade\" --since 2026-01-01\n\n# Narrow wiki hits to a frontmatter document type\nparall search \"deploy steps\" --types w --wiki-type Runbook\n```\n\nEven with zero curated notes, the raw message + task history is searchable \u2014 the\noriginal discussion and its approval/rejection IS the precedent.\n\n## Sending Messages\n\nEach `[Event: message.new]` includes `[Chat: ... (prll://cht_xxx)]` \u2014 use that chat URI to reply.\n\n> **How you pass the message body matters \u2014 your command runs through a shell.**\n> Inside double quotes the shell expands `$`, backticks, and `$(...)` *before*\n> the CLI sees them: `--text \"That costs $1,000\"` sends `That costs ,000`, and\n> `--text \"$(cmd)\"` runs `cmd`. Single quotes instead break on apostrophes\n> (`I'm`, `don't`). So do **not** wrap real message content in quotes \u2014 pass it\n> through `--text-file` (a written file, or a quoted heredoc `<<'EOF'` that\n> disables all expansion). Reserve `--text \"...\"` for short literals with no\n> `$`, backtick, or apostrophe.\n\n```bash\n# One-off reply \u2192 quoted heredoc into stdin. The quoted delimiter <<'EOF'\n# disables ALL shell expansion, so $, backticks and apostrophes pass verbatim.\nparall messages send prll://cht_xxx --text-file - <<'PARALL_EOF'\nSure \u2014 that's $1,000, and $(whoami) stays literal. I'm on it.\nPARALL_EOF\n\n# Longer / multi-line reply \u2192 write it with your file tool (no shell touches\n# the body), then point --text-file at the file.\nparall messages send prll://cht_xxx --text-file /tmp/reply.md\n\n# Short literal with no $, backtick, or apostrophe \u2192 --text is fine.\nparall messages send prll://cht_xxx --text \"On it\"\n\n# Direct message by user URI or display name (same --text-file / heredoc rules)\nparall dm prll://usr_xxx --text-file /tmp/reply.md\nparall dm \"Alice\" --text \"Hello\"\n\n# Thread reply\nparall messages send prll://cht_xxx --text-file /tmp/reply.md --thread-root-id 01JWC...\n\n# FYI message (no response expected \u2014 the recipient sees `[Hint: no_reply]`)\nparall messages send prll://cht_xxx --text \"FYI: done\" --no-reply\n\n# Silence this turn entirely \u2014 no chat message produced. Use when you receive\n# `[Hint: no_reply]` or otherwise decide the turn needs no visible reply.\n# Run BEFORE any `messages send` / `dm`; those still deliver real messages.\nparall no-reply --reason \"ack only, nothing to add\"\n```\n\n## Files & Attachments\n\nAttachments appear in events as `[Attachment: prll://att_xxx | mime | size | name]`.\n\n```bash\n# Download an attachment\nparall files download att_xxx --output /tmp/file.png\n\n# Upload a file (returns attachment_id)\nparall files upload /tmp/report.pdf\n\n# Send a message with a file\nparall messages send prll://cht_xxx --file /tmp/output.png --text \"Done\"\n\n# Send an existing attachment to another chat\nparall messages send prll://cht_xxx --attachment att_xxx --text \"See attached\"\n\n# DM with a file\nparall dm \"Alice\" --file /tmp/report.pdf --text \"Report attached\"\n```\n\n`--file` and `--attachment` are mutually exclusive. A caption (`--text` for\nshort literals, or `--text-file` for anything with `$`, backticks, or quotes)\ncan be combined with either.\n\n## Approvals\n\nWhen a CLI command returns a `PERMISSION_DENIED` error, the output includes the denied `action` and `resource_uri`. Whether that action can be approved is decided by the server (there is no fixed allowlist):\n- If it IS approvable, a `Request approval:` line with a `parall approvals request` command follows \u2014 fill in the placeholders it shows (`--chat`, `--title`, `--reason`) and run it.\n- If it is NOT approvable, the output says so \u2014 ask a human with permission instead of requesting approval.\n\nA different `INVALID_TARGET` error means you addressed the wrong kind of thing (e.g. a `usr_` id where a chat is expected). Follow the message (e.g. use `parall dm` to message a user) \u2014 do not request approval for it.\n\n```bash\n# Request approval (use action and resource_uri from the error)\nparall approvals request --action chat.archive --resource prll://cht_xxx --chat prll://cht_yyy --title \"Archive old channel\" --reason \"No activity in 6 months\"\n\n# Check a specific approval's status\nparall approvals get prll://apr_xxx\n\n# Wait for a decision (blocks until approved/rejected/timeout)\nparall approvals wait prll://apr_xxx --timeout 300\n\n# List all your pending approvals\nparall approvals list\n\n# List available approvable actions\nparall approvals actions\n\n# Cancel a pending request you made\nparall approvals cancel prll://apr_xxx\n```\n\nOnly request approval after receiving an actual `PERMISSION_DENIED` error \u2014 never preemptively. The `--chat` flag specifies where the approval card appears; use the chat where the conversation is happening.\n\n## Reference URIs\n\nEvery entity is addressable with a `prll://` URI. Common prefixes you'll see in events, messages, and schedule descriptions:\n\n| Prefix | Entity | Skill |\n|--------|--------|-------|\n| `prll://usr_` | User (human or agent) | parall-platform |\n| `prll://cht_` | Chat | parall-platform |\n| `prll://msg_` | Message | parall-platform |\n| `prll://cmt_` | Comment (on tasks, wiki pages, changesets) | by target: task comment \u2192 parall-tasks, wiki/changeset comment \u2192 parall-wiki |\n| `prll://ase_` | Agent session | parall-platform |\n| `prll://tsk_` | Task | parall-tasks |\n| `prll://prj_` | Project | parall-tasks |\n| `prll://sch_` | Schedule (time trigger) | parall-schedules |\n| `prll://srn_` | Schedule run (single fire audit record; carries fire-time snapshot) | parall-schedules |\n| `prll://xcn_` | External Trigger Connection (incoming endpoint) | parall-external-triggers |\n| `prll://xin_` | External Trigger Event (single incoming event audit record) | parall-external-triggers |\n| `prll://xtr_` | External Trigger (incoming trigger configuration) | parall-external-triggers |\n| `prll://xrn_` | External Trigger run (single matched dispatch audit record) | parall-external-triggers |\n| `prll://wik_` | Wiki | parall-wiki |\n| `prll://att_` | Attachment | parall-platform (files) |\n\nWhen 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).\n\nWhen 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).\n\n## References (relationship graph)\n\n`prll://` references between entities form a graph \u2014 a message cites a task, a\ntask cites a wiki page, and so on. Walk it to answer \"what is this decision /\nentity connected to\". All results are permission-filtered to what you can see.\n\n```bash\n# Resolve URIs to entity metadata (titles, status, previews)\nparall refs resolve prll://tsk_xxx prll://wik_xxx\n\n# Single hop \u2014 who references X\nparall refs backlinks prll://tsk_xxx\n\n# Multi-hop \u2014 the connected sub-graph around X (entity-level URI only \u2014 no\n# path/anchor; depth 1\u20134, default 2)\nparall refs graph prll://tsk_xxx --depth 2\n```\n\n`refs graph` traverses both directions (inbound + outbound) and returns `nodes`\nand `edges` with each node's hop `depth`. `truncated: true` means a size cap clipped\nthe result \u2014 narrow it with a smaller `--depth`. Edges carry `context` \u2014 the\nauthor's annotation from `[context](prll://...)` \u2014 telling you *why* two\nentities are linked, not just that they are.\n\nThe graph returns bare node URIs (no titles). The usual two-step: `refs graph`\nfor topology, then batch-`refs resolve` the node URIs you care about for\ntitles/status. If graph rejects your URI with a path/anchor error, strip it to\nthe entity root (`prll://wik_xxx/docs/a.md` \u2192 `prll://wik_xxx`) and re-query \u2014\nbut note this WIDENS the query to the whole entity, not that one file: the\ngraph seeds from the wiki id, so a specific file's outbound links may sit\ndeeper in the result (or past the size caps). For refs pointing AT one file\n(inbound), `refs backlinks` on the full file URI is precise. There is no\nprecise query for one file's OUTBOUND edges today \u2014 the widened root graph is\nbest-effort for those, or read the file itself for its `prll://` links.\nWiki-file nodes inside a graph *result* do legitimately carry paths.\n\n`refs backlinks` items include a `snippet` of the referencing content \u2014 often\nenough to judge relevance without fetching the source entity.\n\nCLI 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 \u2014 read both.\n";
2
2
  //# sourceMappingURL=parall-platform.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"parall-platform.d.ts","sourceRoot":"","sources":["../../src/skills/parall-platform.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,wzVAiPjC,CAAC"}
1
+ {"version":3,"file":"parall-platform.d.ts","sourceRoot":"","sources":["../../src/skills/parall-platform.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,wqYAoQjC,CAAC"}
@@ -76,11 +76,11 @@ settled questions or repeat known mistakes. This searches live org data
76
76
  can see.
77
77
 
78
78
  \`\`\`bash
79
- # Semantic + keyword search across messages, tasks, and wiki
79
+ # Semantic + keyword search across messages, tasks, wiki, and comments
80
80
  parall search "auth v5 upgrade"
81
81
 
82
- # Restrict entity types (m=message, t=task, w=wiki). --channel narrows the
83
- # MESSAGE hits to one chat (tasks/wiki are unaffected by it).
82
+ # Restrict entity types (m=message, t=task, w=wiki, c=comment). --channel
83
+ # narrows the MESSAGE hits to one chat (tasks/wiki/comments are unaffected).
84
84
  parall search "auth v5 upgrade" --types m,w --channel prll://cht_eng
85
85
 
86
86
  # Time-box to recent activity (RFC3339 or YYYY-MM-DD). Narrows messages + tasks;
@@ -201,6 +201,8 @@ Every entity is addressable with a \`prll://\` URI. Common prefixes you'll see i
201
201
  | \`prll://usr_\` | User (human or agent) | parall-platform |
202
202
  | \`prll://cht_\` | Chat | parall-platform |
203
203
  | \`prll://msg_\` | Message | parall-platform |
204
+ | \`prll://cmt_\` | Comment (on tasks, wiki pages, changesets) | by target: task comment → parall-tasks, wiki/changeset comment → parall-wiki |
205
+ | \`prll://ase_\` | Agent session | parall-platform |
204
206
  | \`prll://tsk_\` | Task | parall-tasks |
205
207
  | \`prll://prj_\` | Project | parall-tasks |
206
208
  | \`prll://sch_\` | Schedule (time trigger) | parall-schedules |
@@ -236,7 +238,24 @@ parall refs graph prll://tsk_xxx --depth 2
236
238
 
237
239
  \`refs graph\` traverses both directions (inbound + outbound) and returns \`nodes\`
238
240
  and \`edges\` with each node's hop \`depth\`. \`truncated: true\` means a size cap clipped
239
- the result — narrow it with a smaller \`--depth\`.
241
+ the result — narrow it with a smaller \`--depth\`. Edges carry \`context\` — the
242
+ author's annotation from \`[context](prll://...)\` — telling you *why* two
243
+ entities are linked, not just that they are.
244
+
245
+ The graph returns bare node URIs (no titles). The usual two-step: \`refs graph\`
246
+ for topology, then batch-\`refs resolve\` the node URIs you care about for
247
+ titles/status. If graph rejects your URI with a path/anchor error, strip it to
248
+ the entity root (\`prll://wik_xxx/docs/a.md\` → \`prll://wik_xxx\`) and re-query —
249
+ but note this WIDENS the query to the whole entity, not that one file: the
250
+ graph seeds from the wiki id, so a specific file's outbound links may sit
251
+ deeper in the result (or past the size caps). For refs pointing AT one file
252
+ (inbound), \`refs backlinks\` on the full file URI is precise. There is no
253
+ precise query for one file's OUTBOUND edges today — the widened root graph is
254
+ best-effort for those, or read the file itself for its \`prll://\` links.
255
+ Wiki-file nodes inside a graph *result* do legitimately carry paths.
256
+
257
+ \`refs backlinks\` items include a \`snippet\` of the referencing content — often
258
+ enough to judge relevance without fetching the source entity.
240
259
 
241
260
  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.
242
261
  `;
package/dist/types.d.ts CHANGED
@@ -63,10 +63,14 @@ export type ParallEvent = {
63
63
  /** External IM channel metadata, used for channel_message events. */
64
64
  channelProvider?: string;
65
65
  channelConversationType?: string;
66
- /** Provider-side conversation id (the send_message target). */
66
+ /** Provider-side conversation id (the reply's addressing target). */
67
67
  channelExternalConversationId?: string;
68
68
  /** Provider-side message id (in-thread reply target). */
69
69
  channelExternalMessageId?: string;
70
+ /** Live capability grant: the `<provider>-cli` capability is active, so
71
+ * the vendor CLI (broker shim) is on PATH and is THE reply path. False /
72
+ * absent → outbound is disabled for this org; the hint says so. */
73
+ channelCliCapable?: boolean;
70
74
  /** Original event timestamp (e.g., message.created_at). When present,
71
75
  * input steps use this instead of server insertion time for ordering. */
72
76
  sentAt?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/agent-core",
3
- "version": "1.42.0",
3
+ "version": "1.43.0",
4
4
  "description": "Shared agent runtime orchestration helpers for Parall",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,7 +35,7 @@
35
35
  "@opentelemetry/sdk-logs": "^0.57.0",
36
36
  "@opentelemetry/sdk-metrics": "^1.30.0",
37
37
  "@opentelemetry/sdk-trace-node": "^1.30.0",
38
- "@parall/sdk": "1.42.0"
38
+ "@parall/sdk": "1.43.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "^22.0.0",
@@ -0,0 +1,262 @@
1
+ // channel-exec — the short-lived exec form of the channel-capability
2
+ // credential engine. Invoked by the pointer shims the materializer drops on
3
+ // the capability PATH (e.g. `lark-cli` → `node <this file> --channel feishu
4
+ // --bin lark-cli --skip-dir <shimDir> -- <args…>`): mints a fresh short-lived
5
+ // token from the platform on EVERY call, injects it as the vendor CLI's own
6
+ // documented env credentials, resolves the real binary past the shim
7
+ // directory, and execs it with stdio passthrough.
8
+ //
9
+ // This is a process entry point, not a library — importing it runs main().
10
+ // It lives inside @parall/agent-core so it ships atomically with the bridge
11
+ // (image / daemon bundle), never depending on agent-writable install state.
12
+ // Design: docs/engineering-design/agent-capability-fragments-design.md §5.1.
13
+
14
+ import { spawnSync } from 'node:child_process';
15
+ import * as fs from 'node:fs';
16
+ import * as path from 'node:path';
17
+ import { pathToFileURL } from 'node:url';
18
+ import { CHANNEL_POINTER_MAGIC } from '../channel-capability.js';
19
+ import { ChannelTokenError, mintChannelToken } from '../channel-token.js';
20
+
21
+ // The vendor binary each channel targets is fixed HERE, not taken from the
22
+ // caller — channel-exec must never mint a token and hand it to a
23
+ // caller-named program. `--bin` is deliberately not an argument, so a request
24
+ // like `--channel feishu --bin evil` (with a tampered PATH) cannot redirect
25
+ // the minted token to an attacker-chosen executable.
26
+ const CHANNEL_BIN: Record<string, string> = { feishu: 'lark-cli' };
27
+
28
+ interface ExecArgs {
29
+ channel: string;
30
+ skipDir: string;
31
+ rest: string[];
32
+ }
33
+
34
+ function fail(prefix: string, msg: string, code: number): never {
35
+ // Write to fd 2 SYNCHRONOUSLY: process.exit() right after an async
36
+ // process.stderr.write() can truncate the message when stderr is a pipe
37
+ // (notably on Windows) — and this path carries the install hint / revocation
38
+ // message the agent must relay to the user. writeSync flushes before exit;
39
+ // exit still runs so fail() keeps its `never` contract.
40
+ try {
41
+ fs.writeSync(2, `${prefix}: ${msg}\n`);
42
+ } catch {
43
+ // best-effort — still exit with the intended status
44
+ }
45
+ process.exit(code);
46
+ }
47
+
48
+ function parseArgs(argv: string[]): ExecArgs {
49
+ const out: ExecArgs = { channel: '', skipDir: '', rest: [] };
50
+ for (let i = 0; i < argv.length; i++) {
51
+ const a = argv[i];
52
+ if (a === '--') {
53
+ out.rest = argv.slice(i + 1);
54
+ break;
55
+ }
56
+ if (a === '--channel') out.channel = argv[++i] ?? '';
57
+ else if (a === '--skip-dir') out.skipDir = argv[++i] ?? '';
58
+ else fail('channel-exec', `unknown argument ${a}`, 2);
59
+ }
60
+ if (!out.channel) {
61
+ fail('channel-exec', 'usage: channel-exec --channel <type> [--skip-dir <dir>] -- <args…>', 2);
62
+ }
63
+ return out;
64
+ }
65
+
66
+ // A candidate that carries the pointer magic in its head is one of OUR
67
+ // pointers (or a stray copy of one) — never the real vendor binary. This
68
+ // makes self-recursion structurally impossible even when the skip-dir hint
69
+ // is wrong or the pointer got copied elsewhere on PATH.
70
+ function isCapabilityPointer(candidate: string): boolean {
71
+ try {
72
+ const fd = fs.openSync(candidate, 'r');
73
+ try {
74
+ const buf = Buffer.alloc(256);
75
+ const n = fs.readSync(fd, buf, 0, buf.length, 0);
76
+ return buf.toString('utf8', 0, n).includes(CHANNEL_POINTER_MAGIC);
77
+ } finally {
78
+ fs.closeSync(fd);
79
+ }
80
+ } catch {
81
+ return false;
82
+ }
83
+ }
84
+
85
+ // Resolve the REAL vendor binary along PATH, skipping the shim's own
86
+ // directory (realpath comparison, so symlinked layouts don't fool it).
87
+ function resolveRealBin(bin: string, skipDir: string): string | null {
88
+ const isWin = process.platform === 'win32';
89
+ // npm on Windows installs `<bin>` (sh) + `<bin>.cmd` + `<bin>.ps1`; the sh
90
+ // file is not spawnable by CreateProcess, so prefer the .cmd/.exe forms.
91
+ const exts = isWin ? ['.cmd', '.exe', '.bat'] : [''];
92
+ let skipReal: string | null = null;
93
+ if (skipDir) {
94
+ try {
95
+ skipReal = fs.realpathSync(skipDir);
96
+ } catch {
97
+ skipReal = null;
98
+ }
99
+ }
100
+ for (const dir of (process.env.PATH || '').split(path.delimiter)) {
101
+ if (!dir) continue;
102
+ let real: string;
103
+ try {
104
+ real = fs.realpathSync(dir);
105
+ } catch {
106
+ continue;
107
+ }
108
+ if (skipReal && real === skipReal) continue;
109
+ for (const ext of exts) {
110
+ const candidate = path.join(dir, bin + ext);
111
+ try {
112
+ if (fs.statSync(candidate).isFile() && !isCapabilityPointer(candidate)) return candidate;
113
+ } catch {
114
+ // keep scanning
115
+ }
116
+ }
117
+ }
118
+ return null;
119
+ }
120
+
121
+ // The exact lark-cli env credentials the broker OWNS: the values it injects
122
+ // plus the ambient auth inputs it must strip so nothing widens the identity
123
+ // beyond the minted grant. Scrubbing is confined to THESE names — never a
124
+ // blanket LARKSUITE_CLI_* wipe — so legitimate operator controls
125
+ // (LARKSUITE_CLI_CONFIG_DIR / LOG_DIR / CONTENT_SAFETY_MODE / …) survive.
126
+ const FEISHU_OWNED_ENV = [
127
+ // injected (canonical values set below)
128
+ 'LARKSUITE_CLI_APP_ID',
129
+ 'LARKSUITE_CLI_BRAND',
130
+ 'LARKSUITE_CLI_TENANT_ACCESS_TOKEN',
131
+ 'LARKSUITE_CLI_DEFAULT_AS',
132
+ 'LARKSUITE_CLI_STRICT_MODE',
133
+ // ambient auth bypass inputs that must not reach the child
134
+ 'LARKSUITE_CLI_APP_SECRET',
135
+ 'LARKSUITE_CLI_USER_ACCESS_TOKEN',
136
+ 'LARKSUITE_CLI_AUTH_PROXY',
137
+ 'LARKSUITE_CLI_PROXY_KEY',
138
+ ];
139
+
140
+ // Per-channel env injection: the ONLY channel-specific knowledge in this
141
+ // entry.
142
+ function buildChildEnv(
143
+ channel: string,
144
+ minted: { app_id: string; brand: string; token: string },
145
+ ): NodeJS.ProcessEnv {
146
+ const env: NodeJS.ProcessEnv = { ...process.env };
147
+ if (channel === 'feishu') {
148
+ // Delete the broker-owned names case-insensitively: Windows env keys are
149
+ // case-insensitive and a plain-object spread keeps the host's casing, so
150
+ // an uppercase-only delete would miss e.g. `Larksuite_Cli_Auth_Proxy` (a
151
+ // bypass leak) or leave a stale-cased duplicate shadowing the injected
152
+ // token. Non-owned LARKSUITE_CLI_* operator settings are left untouched.
153
+ const owned = new Set(FEISHU_OWNED_ENV);
154
+ for (const key of Object.keys(env)) {
155
+ if (owned.has(key.toUpperCase())) delete env[key];
156
+ }
157
+ env.LARKSUITE_CLI_APP_ID = minted.app_id;
158
+ env.LARKSUITE_CLI_BRAND = minted.brand || 'feishu';
159
+ env.LARKSUITE_CLI_TENANT_ACCESS_TOKEN = minted.token;
160
+ // Bot lock: the credential is the app's, not a human's.
161
+ env.LARKSUITE_CLI_DEFAULT_AS = 'bot';
162
+ env.LARKSUITE_CLI_STRICT_MODE = 'bot';
163
+ return env;
164
+ }
165
+ fail('channel-exec', `unsupported channel type "${channel}"`, 2);
166
+ }
167
+
168
+ // Windows argv-preserving quoting, ported from cross-spawn (the npm-ecosystem
169
+ // standard for correctly launching .cmd shims). Node's `shell: true` does NOT
170
+ // escape arguments — it concatenates them, so JSON/message args split or
171
+ // inject on cmd.exe — and CreateProcess cannot run a .cmd directly. The proven
172
+ // path is cmd.exe /d /s /c with each token escaped for BOTH the CreateProcess
173
+ // argv layer and the cmd.exe metachar layer (double-escaped because a .cmd
174
+ // re-parses). Still Windows-only and NO Windows CI: validate on a real Windows
175
+ // host before the first Windows selfhost user (the round-trip tests lock the
176
+ // escaping rules, not the live cmd.exe behavior).
177
+ // cmd.exe metacharacters, matching cross-spawn's metaCharsRegExp (v7.0.6). ONE
178
+ // set shared by BOTH the command and the arguments — cross-spawn uses a single
179
+ // regex for both, and a split set is a real correctness hole: a metachar
180
+ // caret-escaped in the command path but left bare in an argument (or the
181
+ // reverse) survives only one of cmd.exe's two parse passes. Includes the
182
+ // separators (space, `;`, `,`) and glob chars (`*`, `?`) so a quoted argument's
183
+ // separators are still neutralized for the `.cmd` re-parse, exactly as
184
+ // cross-spawn does.
185
+ const CMD_META_CHARS = /([()\][%!^"`<>&|;, *?])/g;
186
+
187
+ function escapeCmdArgument(arg: string): string {
188
+ let out = `${arg}`;
189
+ // Double backslashes before a quote, and trailing backslashes, then wrap.
190
+ out = out.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, '$1$1');
191
+ out = `"${out}"`;
192
+ // cmd metachars, escaped twice (the .cmd forwarder re-parses).
193
+ out = out.replace(CMD_META_CHARS, '^$1').replace(CMD_META_CHARS, '^$1');
194
+ return out;
195
+ }
196
+
197
+ function escapeCmdCommand(command: string): string {
198
+ return command.replace(CMD_META_CHARS, '^$1');
199
+ }
200
+
201
+ function runReal(realBin: string, args: string[], env: NodeJS.ProcessEnv, prefix: string): never {
202
+ const isWinScript = process.platform === 'win32' && /\.(cmd|bat)$/i.test(realBin);
203
+ const result = isWinScript
204
+ ? spawnSync(
205
+ process.env.comspec || 'cmd.exe',
206
+ [
207
+ '/d',
208
+ '/s',
209
+ '/c',
210
+ `"${[escapeCmdCommand(realBin), ...args.map(escapeCmdArgument)].join(' ')}"`,
211
+ ],
212
+ { stdio: 'inherit', env, windowsVerbatimArguments: true },
213
+ )
214
+ : spawnSync(realBin, args, { stdio: 'inherit', env });
215
+ if (result.error) {
216
+ fail(prefix, `failed to run ${realBin}: ${String(result.error)}`, 1);
217
+ }
218
+ if (result.signal) {
219
+ process.kill(process.pid, result.signal);
220
+ // Unreachable in practice; satisfy the `never` contract if the signal is trapped.
221
+ process.exit(1);
222
+ }
223
+ process.exit(result.status === null ? 1 : result.status);
224
+ }
225
+
226
+ // Exported for round-trip unit tests (Windows spawn can't run in CI).
227
+ export { escapeCmdArgument, escapeCmdCommand };
228
+
229
+ async function main(): Promise<void> {
230
+ const { channel, skipDir, rest } = parseArgs(process.argv.slice(2));
231
+ const bin = CHANNEL_BIN[channel];
232
+ if (!bin) fail('channel-exec', `unsupported channel type "${channel}"`, 2);
233
+ const prefix = bin;
234
+
235
+ let minted: Awaited<ReturnType<typeof mintChannelToken>>;
236
+ try {
237
+ minted = await mintChannelToken(channel, process.env);
238
+ } catch (err) {
239
+ if (err instanceof ChannelTokenError) fail(prefix, err.message, 1);
240
+ fail(prefix, String(err), 1);
241
+ }
242
+
243
+ const real = resolveRealBin(bin, skipDir);
244
+ if (!real) {
245
+ const hint =
246
+ channel === 'feishu'
247
+ ? 'Install it once with:\n npm i -g @larksuite/cli && npx skills add larksuite/cli -y -g'
248
+ : 'Install it and retry.';
249
+ fail(prefix, `the ${bin} binary is not installed. ${hint}`, 127);
250
+ }
251
+
252
+ runReal(real, rest, buildChildEnv(channel, minted), prefix);
253
+ }
254
+
255
+ // Run main() only when invoked AS A PROGRAM (the pointer shim runs
256
+ // `node <this>`), not when a test imports this module for the escape helpers.
257
+ // Holds in both layouts: npm (argv[1] = channel-exec.js) and bundle
258
+ // (argv[1] = parall-channel-exec.js, which is also this module's own url).
259
+ const invokedPath = process.argv[1];
260
+ if (invokedPath && import.meta.url === pathToFileURL(invokedPath).href) {
261
+ void main();
262
+ }