@parall/agent-core 1.31.0 → 1.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/bridge-workspace.d.ts +1 -1
  2. package/dist/bridge-workspace.d.ts.map +1 -1
  3. package/dist/bridge-workspace.js +13 -13
  4. package/dist/dispatch-adapter.d.ts +15 -8
  5. package/dist/dispatch-adapter.d.ts.map +1 -1
  6. package/dist/event-format.d.ts +1 -1
  7. package/dist/event-format.d.ts.map +1 -1
  8. package/dist/event-format.js +68 -25
  9. package/dist/gateway-base.d.ts +14 -13
  10. package/dist/gateway-base.d.ts.map +1 -1
  11. package/dist/gateway-base.js +650 -313
  12. package/dist/index.d.ts +15 -13
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +13 -12
  15. package/dist/internal/attachment-input.d.ts +3 -3
  16. package/dist/internal/attachment-input.d.ts.map +1 -1
  17. package/dist/internal/attachment-input.js +61 -58
  18. package/dist/logger.d.ts +1 -1
  19. package/dist/platform-config.d.ts +28 -2
  20. package/dist/platform-config.d.ts.map +1 -1
  21. package/dist/platform-config.js +42 -11
  22. package/dist/prompt-fragments.d.ts +2 -2
  23. package/dist/prompt-fragments.d.ts.map +1 -1
  24. package/dist/prompt-fragments.js +37 -14
  25. package/dist/provider-config.d.ts +9 -0
  26. package/dist/provider-config.d.ts.map +1 -1
  27. package/dist/provider-config.js +13 -2
  28. package/dist/routing.d.ts +5 -5
  29. package/dist/routing.js +6 -6
  30. package/dist/session-state.d.ts +16 -0
  31. package/dist/session-state.d.ts.map +1 -1
  32. package/dist/session-state.js +45 -0
  33. package/dist/skills/index.d.ts +5 -4
  34. package/dist/skills/index.d.ts.map +1 -1
  35. package/dist/skills/index.js +28 -21
  36. package/dist/skills/parall-clips.d.ts +2 -0
  37. package/dist/skills/parall-clips.d.ts.map +1 -0
  38. package/dist/skills/parall-clips.js +44 -0
  39. package/dist/skills/parall-platform.d.ts +1 -1
  40. package/dist/skills/parall-platform.d.ts.map +1 -1
  41. package/dist/skills/parall-platform.js +6 -2
  42. package/dist/skills/parall-tasks.d.ts +1 -1
  43. package/dist/skills/parall-tasks.d.ts.map +1 -1
  44. package/dist/skills/parall-tasks.js +1 -1
  45. package/dist/skills/parall-wiki.d.ts +1 -1
  46. package/dist/skills/parall-wiki.d.ts.map +1 -1
  47. package/dist/skills/parall-wiki.js +1 -1
  48. package/dist/telemetry.d.ts +27 -0
  49. package/dist/telemetry.d.ts.map +1 -0
  50. package/dist/telemetry.js +205 -0
  51. package/dist/types.d.ts +18 -2
  52. package/dist/types.d.ts.map +1 -1
  53. package/package.json +11 -2
  54. package/src/bridge-workspace.ts +13 -13
  55. package/src/dispatch-adapter.ts +31 -8
  56. package/src/event-format.ts +80 -30
  57. package/src/gateway-base.ts +988 -445
  58. package/src/index.ts +23 -13
  59. package/src/internal/attachment-input.ts +127 -100
  60. package/src/logger.ts +1 -1
  61. package/src/platform-config.ts +61 -16
  62. package/src/prompt-fragments.ts +37 -14
  63. package/src/provider-config.ts +14 -2
  64. package/src/routing.ts +11 -11
  65. package/src/session-state.ts +62 -0
  66. package/src/skills/index.ts +34 -23
  67. package/src/skills/parall-clips.ts +44 -0
  68. package/src/skills/parall-platform.ts +6 -2
  69. package/src/skills/parall-tasks.ts +1 -1
  70. package/src/skills/parall-wiki.ts +1 -1
  71. package/src/telemetry.ts +252 -0
  72. package/src/types.ts +18 -2
@@ -27,29 +27,32 @@ be direct, and care about the outcome of the work — not just the request in fr
27
27
  of you.`;
28
28
 
29
29
  function sanitizeProfileField(value: string): string {
30
- return value.replace(/[\r\n]+/g, " ").replace(/`/g, "'").trim();
30
+ return value
31
+ .replace(/[\r\n]+/g, ' ')
32
+ .replace(/`/g, "'")
33
+ .trim();
31
34
  }
32
35
 
33
36
  function sanitizeProfileBlock(value: string): string {
34
- return value.replace(/\r\n?/g, "\n").trim();
37
+ return value.replace(/\r\n?/g, '\n').trim();
35
38
  }
36
39
 
37
40
  export function buildIdentity(agent?: AgentIdentity): string {
38
41
  if (!agent) return PRLL_IDENTITY_BASE;
39
42
  const name = sanitizeProfileField(agent.displayName);
40
- const lines = [PRLL_IDENTITY_BASE, "", "### Your Parall Identity", ""];
43
+ const lines = [PRLL_IDENTITY_BASE, '', '### Your Parall Identity', ''];
41
44
  lines.push(`You are **${name}** (\`prll://${agent.userId}\`).`);
42
45
  if (agent.description) {
43
46
  const description = sanitizeProfileBlock(agent.description);
44
47
  if (description) {
45
- lines.push("", "### Your Agent Profile", "", description);
48
+ lines.push('', '### Your Agent Profile', '', description);
46
49
  }
47
50
  }
48
51
  lines.push(
49
- "",
52
+ '',
50
53
  `When you see \`${agent.userId}\` or \`prll://${agent.userId}\` in messages, mentions, or events — that's you.`,
51
54
  );
52
- return lines.join("\n");
55
+ return lines.join('\n');
53
56
  }
54
57
 
55
58
  /** @deprecated Use buildIdentity() instead. Kept for backward compat during migration. */
@@ -90,10 +93,15 @@ Other agents share this workspace. Before starting work, check whether someone
90
93
  ### Permissions and approvals
91
94
  You have real permissions based on your roles (chat member/admin, org member).
92
95
  If you lack permission for an action, the API returns PERMISSION_DENIED with the
93
- \`action\` and \`resource_uri\` that were denied. When that happens, use
94
- \`approvals request\` to ask someone with permission to approve it don't retry
95
- or work around the denial. Only request approval after an actual denial, never
96
- preemptively.
96
+ \`action\` and \`resource_uri\` that were denied. The server decides whether that
97
+ action is approvable: if it is, the CLI prints an \`approvals request\` command —
98
+ fill in the placeholders it shows (\`--chat\`, \`--title\`, \`--reason\`) and run
99
+ it to ask someone with permission. If it is NOT approvable, the output says so;
100
+ ask a human with permission instead of requesting approval. A
101
+ \`INVALID_TARGET\` error instead means you addressed the wrong kind of thing
102
+ (e.g. a \`usr_\` id where a chat is expected) — follow the message (e.g. use
103
+ \`dm\` for a user). Don't retry or work around a denial; only request approval
104
+ after an actual denial, never preemptively.
97
105
 
98
106
  ### When in doubt
99
107
  Prefer asking over guessing. Prefer "I don't know" over fabricating. Your
@@ -143,6 +151,18 @@ resolves and renders the entity title automatically.
143
151
 
144
152
  prll://tsk_xxx/description#Implementation heading within task description
145
153
 
154
+ ### Unread context
155
+
156
+ When dispatched to a chat, you may see \`[Unread: N messages | since: prll://msg_xxx]\`.
157
+ This shows messages since your last interaction — your read cursor advances after each
158
+ dispatch, so context you skip now won't appear as unread next time. Use
159
+ \`parall messages list <chat> --limit 20\` to fetch recent context. For large unread
160
+ counts (50+), fetch only recent messages rather than everything.
161
+
162
+ Thread dispatches may show \`[Thread: prll://msg_root | N replies | M unread | since: prll://msg_r]\`.
163
+ Same semantics — use \`parall messages list <chat> --thread-root-id <thread_root> --limit 20\` to
164
+ catch up on the thread.
165
+
146
166
  ### Reading context on demand
147
167
 
148
168
  An event only carries the single triggering message. If you're mentioned in a
@@ -206,7 +226,7 @@ export type LocalAttachmentResult = {
206
226
  };
207
227
 
208
228
  export function renderLocalAttachmentSection(section: LocalAttachmentResult): string {
209
- if (section.images.length === 0 && section.notes.length === 0) return "";
229
+ if (section.images.length === 0 && section.notes.length === 0) return '';
210
230
 
211
231
  // Each image renders as a metadata header line plus the absolute path on
212
232
  // its own line. The path stands alone (no quoting / escaping) so file-
@@ -215,7 +235,7 @@ export function renderLocalAttachmentSection(section: LocalAttachmentResult): st
215
235
  // the bridge operator. A pathological workspace dir with embedded newlines
216
236
  // would split across lines; that's exotic enough not to warrant scrubbing
217
237
  // every consumer's path through escape rules.
218
- const lines = ["[Local attachment files]"];
238
+ const lines = ['[Local attachment files]'];
219
239
  for (const image of section.images) {
220
240
  lines.push(
221
241
  `- prll://${image.attachmentId} (${sanitizePromptMeta(image.mimeType)}, ${formatBytes(image.fileSize)}, ${sanitizePromptMeta(image.fileName)})`,
@@ -223,11 +243,14 @@ export function renderLocalAttachmentSection(section: LocalAttachmentResult): st
223
243
  );
224
244
  }
225
245
  lines.push(...section.notes);
226
- return lines.join("\n");
246
+ return lines.join('\n');
227
247
  }
228
248
 
229
249
  function sanitizePromptMeta(value: string): string {
230
- return value.replace(/[\r\n]+/g, " ").replace(/[()]/g, " ").trim();
250
+ return value
251
+ .replace(/[\r\n]+/g, ' ')
252
+ .replace(/[()]/g, ' ')
253
+ .trim();
231
254
  }
232
255
 
233
256
  export function formatBytes(bytes: number): string {
@@ -7,6 +7,18 @@ export interface ProviderConfig {
7
7
  }
8
8
 
9
9
  export function llmSource(pc?: ProviderConfig): string {
10
+ return effectiveLLMSourceExplicit(pc) || 'parall';
11
+ }
12
+
13
+ /**
14
+ * Returns the explicitly-configured LLM source — the llm_source value when
15
+ * set, or "custom" when BYO provider credentials are present — or "" when the
16
+ * provider_config carries no source signal at all (e.g. the empty `{}` default
17
+ * that self-hosted agents always have). The daemon supervisor uses the empty
18
+ * case to defer to the machine-level llm_source instead of letting `{}` shadow
19
+ * it as "parall". Mirrors the Go `ProviderConfig.EffectiveLLMSourceExplicit`.
20
+ */
21
+ export function effectiveLLMSourceExplicit(pc?: ProviderConfig): string {
10
22
  if (pc?.llm_source) return pc.llm_source;
11
23
  if (
12
24
  pc?.openai_api_key ||
@@ -14,9 +26,9 @@ export function llmSource(pc?: ProviderConfig): string {
14
26
  pc?.anthropic_auth_token ||
15
27
  pc?.anthropic_base_url
16
28
  ) {
17
- return "custom";
29
+ return 'custom';
18
30
  }
19
- return "parall";
31
+ return '';
20
32
  }
21
33
 
22
34
  export function clearAllProviderCreds(env: NodeJS.ProcessEnv): void {
package/src/routing.ts CHANGED
@@ -1,11 +1,11 @@
1
- import type { DispatchState, ParallEvent } from "./types.js";
1
+ import type { DispatchState, ParallEvent } from './types.js';
2
2
 
3
3
  /** Where an inbound event should be routed. */
4
4
  export type TriggerDisposition =
5
- | { action: "main" }
6
- | { action: "buffer-main" }
7
- | { action: "buffer-fork"; forkKey: string }
8
- | { action: "new-fork" };
5
+ | { action: 'main' }
6
+ | { action: 'buffer-main' }
7
+ | { action: 'buffer-fork'; forkKey: string }
8
+ | { action: 'new-fork' };
9
9
 
10
10
  /** Pluggable strategy for routing triggers when main session is busy. */
11
11
  export type RoutingStrategy = (event: ParallEvent, state: DispatchState) => TriggerDisposition;
@@ -21,17 +21,17 @@ const MAX_CONCURRENT_FORKS = 20;
21
21
  */
22
22
  export const defaultRoutingStrategy: RoutingStrategy = (event, state) => {
23
23
  if (state.mainCurrentTargetId === event.targetId) {
24
- return { action: "buffer-main" };
24
+ return { action: 'buffer-main' };
25
25
  }
26
26
 
27
27
  const existingForkKey = state.activeForks.get(event.targetId);
28
- if (existingForkKey) return { action: "buffer-fork", forkKey: existingForkKey };
28
+ if (existingForkKey) return { action: 'buffer-fork', forkKey: existingForkKey };
29
29
 
30
30
  if (state.activeForks.size >= MAX_CONCURRENT_FORKS) {
31
- return { action: "buffer-main" };
31
+ return { action: 'buffer-main' };
32
32
  }
33
33
 
34
- return { action: "new-fork" };
34
+ return { action: 'new-fork' };
35
35
  };
36
36
 
37
37
  /** Route an inbound event based on current dispatch state. */
@@ -41,7 +41,7 @@ export function routeTrigger(
41
41
  strategy: RoutingStrategy = defaultRoutingStrategy,
42
42
  ): TriggerDisposition {
43
43
  const existingForkKey = state.activeForks.get(event.targetId);
44
- if (existingForkKey) return { action: "buffer-fork", forkKey: existingForkKey };
45
- if (!state.mainDispatching) return { action: "main" };
44
+ if (existingForkKey) return { action: 'buffer-fork', forkKey: existingForkKey };
45
+ if (!state.mainDispatching) return { action: 'main' };
46
46
  return strategy(event, state);
47
47
  }
@@ -67,3 +67,65 @@ export function getDispatchNoReply(sessionKey: string): boolean {
67
67
  export function clearDispatchNoReply(sessionKey: string) {
68
68
  dispatchNoReplyMap.delete(normalizeSessionKey(sessionKey));
69
69
  }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Per-dispatch metrics — populated by runtime adapters, reported by gateway
73
+ // ---------------------------------------------------------------------------
74
+
75
+ export type DispatchMetrics = {
76
+ deliver_text_chunks: number;
77
+ deliver_text_chars: number;
78
+ message_send_attempts: number;
79
+ message_send_successes: number;
80
+ no_reply_called: boolean;
81
+ tool_call_count: number;
82
+ started_at: number;
83
+ };
84
+
85
+ const dispatchMetricsMap = new Map<string, DispatchMetrics>();
86
+
87
+ export function resetDispatchMetrics(sessionKey: string): void {
88
+ dispatchMetricsMap.set(normalizeSessionKey(sessionKey), {
89
+ deliver_text_chunks: 0,
90
+ deliver_text_chars: 0,
91
+ message_send_attempts: 0,
92
+ message_send_successes: 0,
93
+ no_reply_called: false,
94
+ tool_call_count: 0,
95
+ started_at: Date.now(),
96
+ });
97
+ }
98
+
99
+ export function getDispatchMetrics(sessionKey: string): DispatchMetrics | undefined {
100
+ return dispatchMetricsMap.get(normalizeSessionKey(sessionKey));
101
+ }
102
+
103
+ export function clearDispatchMetrics(sessionKey: string): void {
104
+ dispatchMetricsMap.delete(normalizeSessionKey(sessionKey));
105
+ }
106
+
107
+ export function recordDeliverText(sessionKey: string, charCount: number): void {
108
+ const m = dispatchMetricsMap.get(normalizeSessionKey(sessionKey));
109
+ if (!m) return;
110
+ m.deliver_text_chunks++;
111
+ m.deliver_text_chars += charCount;
112
+ }
113
+
114
+ export function recordMessageSend(sessionKey: string, success: boolean): void {
115
+ const m = dispatchMetricsMap.get(normalizeSessionKey(sessionKey));
116
+ if (!m) return;
117
+ m.message_send_attempts++;
118
+ if (success) m.message_send_successes++;
119
+ }
120
+
121
+ export function recordNoReply(sessionKey: string): void {
122
+ const m = dispatchMetricsMap.get(normalizeSessionKey(sessionKey));
123
+ if (!m) return;
124
+ m.no_reply_called = true;
125
+ }
126
+
127
+ export function recordToolCall(sessionKey: string): void {
128
+ const m = dispatchMetricsMap.get(normalizeSessionKey(sessionKey));
129
+ if (!m) return;
130
+ m.tool_call_count++;
131
+ }
@@ -1,52 +1,63 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
3
 
4
- export { PARALL_PLATFORM_SKILL } from "./parall-platform.js";
5
- export { PARALL_TASKS_SKILL } from "./parall-tasks.js";
6
- export { PARALL_WIKI_SKILL } from "./parall-wiki.js";
7
- export { PARALL_SCHEDULES_SKILL } from "./parall-schedules.js";
4
+ export { PARALL_PLATFORM_SKILL } from './parall-platform.js';
5
+ export { PARALL_TASKS_SKILL } from './parall-tasks.js';
6
+ export { PARALL_WIKI_SKILL } from './parall-wiki.js';
7
+ export { PARALL_SCHEDULES_SKILL } from './parall-schedules.js';
8
+ export { PARALL_CLIPS_SKILL } from './parall-clips.js';
8
9
 
9
- import { PARALL_PLATFORM_SKILL } from "./parall-platform.js";
10
- import { PARALL_TASKS_SKILL } from "./parall-tasks.js";
11
- import { PARALL_WIKI_SKILL } from "./parall-wiki.js";
12
- import { PARALL_SCHEDULES_SKILL } from "./parall-schedules.js";
10
+ import { PARALL_PLATFORM_SKILL } from './parall-platform.js';
11
+ import { PARALL_TASKS_SKILL } from './parall-tasks.js';
12
+ import { PARALL_WIKI_SKILL } from './parall-wiki.js';
13
+ import { PARALL_SCHEDULES_SKILL } from './parall-schedules.js';
14
+ import { PARALL_CLIPS_SKILL } from './parall-clips.js';
13
15
 
14
16
  export type SkillMeta = { name: string; description: string; content: string };
15
17
 
16
18
  export const SKILLS: SkillMeta[] = [
17
19
  {
18
- name: "parall-platform",
19
- 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.",
20
+ name: 'parall-platform',
21
+ description:
22
+ "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.",
20
23
  content: PARALL_PLATFORM_SKILL,
21
24
  },
22
25
  {
23
- name: "parall-tasks",
24
- description: "Parall task operations: create, update, comment on, and query tasks and projects. Use when: user asks to create a task, update task status, add comments, list tasks, or manage projects.",
26
+ name: 'parall-tasks',
27
+ description:
28
+ 'Parall task operations: create, update, comment on, and query tasks and projects. Use when: user asks to create a task, update task status, add comments, list tasks, or manage projects.',
25
29
  content: PARALL_TASKS_SKILL,
26
30
  },
27
31
  {
28
- name: "parall-wiki",
29
- description: "Parall wiki operations: read, search, edit, and propose changes to organization knowledge bases. Use when: user asks to read/write docs, edit wiki pages, search knowledge base, propose changes, or review changesets.",
32
+ name: 'parall-wiki',
33
+ description:
34
+ 'Parall wiki operations: read, search, edit, and propose changes to organization knowledge bases. Use when: user asks to read/write docs, edit wiki pages, search knowledge base, propose changes, or review changesets.',
30
35
  content: PARALL_WIKI_SKILL,
31
36
  },
32
37
  {
33
- name: "parall-schedules",
34
- description: "Parall schedule operations: create / pause / resume / cancel recurring or one-shot time triggers; respond to schedule fire events. Use when: user asks to set up a recurring reminder, schedule a delayed prompt, run cron-like work, or when the agent receives an `[Event: schedule.fired]` dispatch.",
38
+ name: 'parall-schedules',
39
+ description:
40
+ 'Parall schedule operations: create / pause / resume / cancel recurring or one-shot time triggers; respond to schedule fire events. Use when: user asks to set up a recurring reminder, schedule a delayed prompt, run cron-like work, or when the agent receives an `[Event: schedule.fired]` dispatch.',
35
41
  content: PARALL_SCHEDULES_SKILL,
36
42
  },
43
+ {
44
+ name: 'parall-clips',
45
+ description:
46
+ 'Parall clip operations: list installed clips, invoke clip commands, inspect clip details. Use when: the task requires external capabilities (GitHub, web search, etc.), user asks about available tools/clips, or you need to call a clip command.',
47
+ content: PARALL_CLIPS_SKILL,
48
+ },
37
49
  ];
38
50
 
39
51
  /** Write plain skill markdown files to a target directory (CC/Codex). */
40
52
  export function writeSkillFiles(targetDir: string): void {
41
53
  fs.mkdirSync(targetDir, { recursive: true });
42
54
  for (const skill of SKILLS) {
43
- fs.writeFileSync(path.join(targetDir, `${skill.name}.md`), skill.content, "utf8");
55
+ fs.writeFileSync(path.join(targetDir, `${skill.name}.md`), skill.content, 'utf8');
44
56
  }
45
57
  }
46
58
 
47
-
48
59
  export function buildSkillReferences(workspaceDir: string): string {
49
- const dir = path.join(workspaceDir, ".parall", "skills");
50
- const lines = SKILLS.map((s) => `- ${s.description.split(":")[0]}: \`${dir}/${s.name}.md\``);
51
- return `## Platform Skills (read on demand)\n\n${lines.join("\n")}\n`;
60
+ const dir = path.join(workspaceDir, '.parall', 'skills');
61
+ const lines = SKILLS.map((s) => `- ${s.description.split(':')[0]}: \`${dir}/${s.name}.md\``);
62
+ return `## Platform Skills (read on demand)\n\n${lines.join('\n')}\n`;
52
63
  }
@@ -0,0 +1,44 @@
1
+ export const PARALL_CLIPS_SKILL = `# Parall Clips
2
+
3
+ Clips are capability extensions — packaged toolkits that give you extra commands (e.g. GitHub operations, web search, code analysis). Clips installed in the org are available for any agent to invoke via the CLI.
4
+
5
+ ## Discovering available clips
6
+
7
+ \`\`\`bash
8
+ # List all clips installed in the org
9
+ parall clip list
10
+
11
+ # Show detailed info about a clip (manifest, commands, version)
12
+ parall clip info <alias>
13
+ \`\`\`
14
+
15
+ ## Invoking a clip command
16
+
17
+ \`\`\`bash
18
+ # Invoke a command on a clip by alias
19
+ parall clip invoke <alias> <command> [input]
20
+
21
+ # input is optional — when provided, it can be a JSON string or plain text
22
+ parall clip invoke github-tools list-repos '{"org": "acme"}'
23
+ parall clip invoke web-search search "latest Node.js LTS version"
24
+
25
+ # Custom timeout (default 30s)
26
+ parall clip invoke github-tools create-issue '{"title": "Bug report"}' --timeout 60000
27
+ \`\`\`
28
+
29
+ ## How clips work
30
+
31
+ 1. An org admin installs a clip from the Pinix registry or creates a custom one
32
+ 2. \`parall clip list\` shows every clip installed in the org
33
+ 3. You can only **invoke** clips that an admin has **bound to you** — invoking an unbound clip returns a "not bound" error. Ask an admin to bind the clip if you need it.
34
+ 4. Each clip exposes one or more named commands with typed input/output
35
+
36
+ ## When to use clips
37
+
38
+ - Check \`parall clip list\` when a task requires capabilities beyond your built-in tools (e.g. GitHub API, external services, specialized analysis)
39
+ - Use \`parall clip info <alias>\` to discover available commands and their expected input format
40
+ - If \`parall clip invoke\` reports the clip isn't bound to you, that clip exists in the org but hasn't been granted to you — ask an admin to bind it
41
+ - Clip invocations return JSON output on success or an error message on failure
42
+
43
+ CLI command results are JSON on stdout.
44
+ `;
@@ -115,7 +115,11 @@ parall dm "Alice" --file /tmp/report.pdf --text "Report attached"
115
115
 
116
116
  ## Approvals
117
117
 
118
- When a CLI command returns a \`PERMISSION_DENIED\` error, the output includes a \`Tip:\` line with an approval-request template copy it and fill in the remaining placeholders (\`--chat\`, \`--title\`, \`--reason\`). The \`Tip:\` only appears for commonly approvable actions (archive, delete, restore); for other actions, use \`parall approvals actions\` to check if it's approvable.
118
+ When 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):
119
+ - If it IS approvable, a \`Request approval:\` line with a \`parall approvals request\` command follows — fill in the placeholders it shows (\`--chat\`, \`--title\`, \`--reason\`) and run it.
120
+ - If it is NOT approvable, the output says so — ask a human with permission instead of requesting approval.
121
+
122
+ A 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) — do not request approval for it.
119
123
 
120
124
  \`\`\`bash
121
125
  # Request approval (use action and resource_uri from the error)
@@ -157,5 +161,5 @@ Every entity is addressable with a \`prll://\` URI. Common prefixes you'll see i
157
161
 
158
162
  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).
159
163
 
160
- All CLI output is JSON.
164
+ 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.
161
165
  `;
@@ -61,5 +61,5 @@ When you receive \`[Event: task.comment.created]\`, someone commented on a task
61
61
  2. Reply via comment: \`tasks comments add prll://tsk_xxx --body "Response..."\`
62
62
  3. If the comment requests status changes, update accordingly
63
63
 
64
- All CLI output is JSON.
64
+ CLI success output is JSON; errors print a JSON line plus, on a \`PERMISSION_DENIED\`, an optional plain-text \`Request approval:\` line — read both.
65
65
  `;
@@ -127,5 +127,5 @@ parall wiki reset <slug> # Restore all files to last synced state
127
127
  parall wiki log <slug> # Recent wiki operations
128
128
  \`\`\`
129
129
 
130
- All CLI output is JSON. Run \`parall wiki --help\` for full options.
130
+ 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.
131
131
  `;