replicas-mcp 0.2.10 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,16 +3,38 @@
3
3
  // src/index.ts
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
- import { z } from "zod";
6
+ import { z as z2 } from "zod";
7
7
 
8
8
  // ../shared/src/agent.ts
9
- var VALID_AGENT_PROVIDERS = ["claude", "codex", "cursor", "opencode", "relay"];
9
+ var AGENT = {
10
+ CLAUDE: "claude",
11
+ CODEX: "codex",
12
+ CURSOR: "cursor",
13
+ OPENCODE: "opencode",
14
+ PI: "pi",
15
+ RELAY: "relay"
16
+ };
17
+ var VALID_AGENT_PROVIDERS = Object.values(AGENT);
18
+ var VALID_RELAY_SUBAGENT_PROVIDERS = VALID_AGENT_PROVIDERS;
10
19
  var VALID_CODING_AGENT_PROVIDERS = VALID_AGENT_PROVIDERS.filter(
11
- (provider) => provider !== "relay"
20
+ (provider) => provider !== AGENT.RELAY
12
21
  );
13
22
  function isValidCodingAgentProvider(value) {
14
23
  return VALID_CODING_AGENT_PROVIDERS.some((provider) => provider === value);
15
24
  }
25
+ var VALID_THINKING_LEVELS = ["low", "medium", "high", "xhigh", "max", "ultra", "ultracode"];
26
+ var STANDARD_THINKING_LEVELS = ["low", "medium", "high", "xhigh", "max"];
27
+ var THINKING_LEVELS_BY_AGENT = {
28
+ claude: [...STANDARD_THINKING_LEVELS, "ultracode"],
29
+ codex: [...STANDARD_THINKING_LEVELS, "ultra"],
30
+ cursor: STANDARD_THINKING_LEVELS,
31
+ opencode: STANDARD_THINKING_LEVELS,
32
+ pi: STANDARD_THINKING_LEVELS,
33
+ relay: [...STANDARD_THINKING_LEVELS, "ultracode"]
34
+ };
35
+ function isValidThinkingLevel(value) {
36
+ return VALID_THINKING_LEVELS.some((l) => l === value);
37
+ }
16
38
  function getProviderDisplayName(provider, variant = "short") {
17
39
  switch (provider) {
18
40
  case "codex":
@@ -21,6 +43,8 @@ function getProviderDisplayName(provider, variant = "short") {
21
43
  return "Cursor";
22
44
  case "opencode":
23
45
  return "Opencode";
46
+ case "pi":
47
+ return "Pi";
24
48
  case "relay":
25
49
  return "Relay";
26
50
  case "claude":
@@ -31,9 +55,92 @@ function getCodingAgentDisplayNames() {
31
55
  return VALID_CODING_AGENT_PROVIDERS.map((provider) => getProviderDisplayName(provider)).join(", ");
32
56
  }
33
57
 
58
+ // ../shared/src/credentials/types.ts
59
+ var CREDENTIAL_SCOPE = {
60
+ USER: "user",
61
+ ORG: "org"
62
+ };
63
+ var CREDENTIAL_SCOPES = Object.values(CREDENTIAL_SCOPE);
64
+ var CREDENTIAL_SCOPE_PRIORITY = [CREDENTIAL_SCOPE.USER, CREDENTIAL_SCOPE.ORG];
65
+
66
+ // ../shared/src/type-guards.ts
67
+ function isRecord(value) {
68
+ return typeof value === "object" && value !== null && !Array.isArray(value);
69
+ }
70
+
71
+ // ../shared/src/credentials/agents.ts
72
+ var CREDENTIAL_METHOD = {
73
+ OAUTH: "oauth",
74
+ API_KEY: "api_key",
75
+ FOUNDRY: "foundry",
76
+ BEDROCK: "bedrock",
77
+ OPENCODE_GO: "opencode-go",
78
+ ASTER: "aster",
79
+ OPENROUTER: "openrouter"
80
+ };
81
+ var CREDENTIAL_TYPE = {
82
+ CLAUDE_OAUTH: "claude",
83
+ ANTHROPIC_API_KEY: "claude_anthropic_api_key",
84
+ CLAUDE_FOUNDRY: "claude_foundry",
85
+ CLAUDE_BEDROCK: "claude_bedrock",
86
+ CODEX_OAUTH: "codex",
87
+ OPENAI_API_KEY: "openai_api_key",
88
+ CODEX_FOUNDRY: "codex_foundry",
89
+ CURSOR_API_KEY: "cursor_api_key",
90
+ OPENCODE_GO_API_KEY: "opencode_go_api_key",
91
+ ASTER_API_KEY: "aster_api_key",
92
+ OPENROUTER_API_KEY: "openrouter_api_key"
93
+ };
94
+ var AGENT_CREDENTIAL_METHODS_IN_ORDER = {
95
+ [AGENT.CLAUDE]: [
96
+ { method: CREDENTIAL_METHOD.OAUTH, credentialType: CREDENTIAL_TYPE.CLAUDE_OAUTH },
97
+ { method: CREDENTIAL_METHOD.API_KEY, credentialType: CREDENTIAL_TYPE.ANTHROPIC_API_KEY },
98
+ { method: CREDENTIAL_METHOD.FOUNDRY, credentialType: CREDENTIAL_TYPE.CLAUDE_FOUNDRY },
99
+ { method: CREDENTIAL_METHOD.BEDROCK, credentialType: CREDENTIAL_TYPE.CLAUDE_BEDROCK }
100
+ ],
101
+ [AGENT.CODEX]: [
102
+ { method: CREDENTIAL_METHOD.OAUTH, credentialType: CREDENTIAL_TYPE.CODEX_OAUTH },
103
+ { method: CREDENTIAL_METHOD.API_KEY, credentialType: CREDENTIAL_TYPE.OPENAI_API_KEY },
104
+ { method: CREDENTIAL_METHOD.FOUNDRY, credentialType: CREDENTIAL_TYPE.CODEX_FOUNDRY }
105
+ ],
106
+ [AGENT.CURSOR]: [
107
+ { method: CREDENTIAL_METHOD.API_KEY, credentialType: CREDENTIAL_TYPE.CURSOR_API_KEY }
108
+ ],
109
+ [AGENT.OPENCODE]: [
110
+ { method: CREDENTIAL_METHOD.OPENCODE_GO, credentialType: CREDENTIAL_TYPE.OPENCODE_GO_API_KEY },
111
+ { method: CREDENTIAL_METHOD.ASTER, credentialType: CREDENTIAL_TYPE.ASTER_API_KEY },
112
+ { method: CREDENTIAL_METHOD.OPENROUTER, credentialType: CREDENTIAL_TYPE.OPENROUTER_API_KEY }
113
+ ],
114
+ [AGENT.PI]: [
115
+ { method: CREDENTIAL_METHOD.ASTER, credentialType: CREDENTIAL_TYPE.ASTER_API_KEY },
116
+ { method: CREDENTIAL_METHOD.OPENROUTER, credentialType: CREDENTIAL_TYPE.OPENROUTER_API_KEY }
117
+ ]
118
+ };
119
+
120
+ // ../shared/src/event.ts
121
+ var CLAUDE_PARTIAL_MESSAGE_EVENT_TYPE = "claude-partial-message";
122
+ function coerceClaudePartialMessagePayload(payload) {
123
+ const streamId = payload.streamId;
124
+ if (typeof streamId !== "string" || !streamId) return null;
125
+ const parentToolUseId = payload.parent_tool_use_id;
126
+ const text = typeof payload.text === "string" && payload.text ? payload.text : void 0;
127
+ const thinking = typeof payload.thinking === "string" && payload.thinking ? payload.thinking : void 0;
128
+ if (!text && !thinking) return null;
129
+ return {
130
+ streamId,
131
+ parent_tool_use_id: typeof parentToolUseId === "string" ? parentToolUseId : null,
132
+ ...text ? { text } : {},
133
+ ...thinking ? { thinking } : {},
134
+ status: payload.status === "completed" ? "completed" : "in_progress"
135
+ };
136
+ }
137
+ var USER_MESSAGE_ID_PAYLOAD_KEY = "replicasMessageId";
138
+ var CODEX_ASP_ITEM_ID_PAYLOAD_KEY = "codexAspItemId";
139
+ var CODEX_QUOTA_STATUS_EVENT_TYPE = "codex-quota-status";
140
+
34
141
  // ../shared/src/default-skills/replicas-agent/abilities/computer.ts
35
142
  var SECTION = `### Computer use (Linux desktop control)
36
- Drive the workspace's Linux desktop - open a browser, click, type, scroll, screenshot, record - via the \`replicas computer\` CLI. Every Replicas workspace boots with Xvfb / openbox / x11vnc / noVNC pre-installed and the live noVNC viewer is automatically published as an authenticated preview, so a \`Desktop\` tab is always available in the dashboard. Use \`replicas computer info\` to get the viewer URL when you want to share it (e.g. point a user on Slack at the live stream).
143
+ Drive the workspace's Linux desktop - open a browser, click, type, scroll, screenshot, record - via the \`replicas computer\` CLI. Every Replicas workspace boots with a live H.264 and Opus stream published as an authenticated preview, so a \`Desktop\` tab is always available in the dashboard. Use \`replicas computer info\` to get the viewer URL when you want to share it (e.g. point a user on Slack at the live stream).
37
144
 
38
145
  **Reference:** \`references/COMPUTER-USE.md\`
39
146
 
@@ -44,7 +151,7 @@ Use this when:
44
151
  - You want to record a screen capture of a task as proof to share back`;
45
152
  var REFERENCE = `# Computer use (Linux desktop control)
46
153
 
47
- Every Replicas workspace boots with a full Linux desktop stack - Xvfb (1920\xD71080), openbox, tint2, x11vnc, noVNC, ffmpeg, and Google Chrome. The engine also auto-registers the noVNC viewer (port 6080) as an authenticated preview at startup, so the dashboard \`Desktop\` tab is live the moment the workspace is up. You drive the desktop through the \`replicas computer\` CLI.
154
+ Every Replicas workspace boots with a full Linux desktop stack - Xvfb (1920\xD71080), openbox, tint2, PulseAudio, ffmpeg, and Google Chrome. The engine auto-registers the H.264 and Opus viewer (port 6080) as an authenticated preview at startup, so the dashboard \`Desktop\` tab is live the moment the workspace is up. You drive the desktop through the \`replicas computer\` CLI.
48
155
 
49
156
  Use this for anything the user can't reasonably do via an API - clicking around web apps, filling forms, testing UI changes, dragging files between desktop apps, recording a walkthrough.
50
157
 
@@ -79,25 +186,24 @@ replicas computer info
79
186
  # 2) Launch a browser on the workspace display.
80
187
  replicas computer launch chrome https://news.ycombinator.com
81
188
 
82
- # 3) Observe the settled screen and browser tab state before clicking.
83
- replicas computer observe /tmp/state.png
84
- replicas computer browser --snapshot
189
+ # 3) Capture the settled viewport and native accessibility tree together.
190
+ replicas computer browser-state /tmp/state.png --full
85
191
 
86
- # 4) Drive the UI.
87
- replicas computer click 521 700 # click coordinates from the screenshot
88
- replicas computer scroll down --amount 5
192
+ # 4) Batch actions that use refs from the same state, then capture the semantic diff.
193
+ replicas computer browser-batch '[{"action":"fill","ref":17,"value":"hello"},{"action":"click","ref":42}]'
194
+ replicas computer browser-state /tmp/after-click.png
89
195
 
90
196
  # 5) (Optional) Record a screencap to share back.
91
197
  replicas computer record start /tmp/demo.mp4 --fps 60
92
198
  # ... do stuff ...
93
199
  replicas computer record stop
94
- replicas media upload /tmp/demo.mp4
200
+ replicas media upload /tmp/demo.mp4 --name "Demo recording"
95
201
  \`\`\`
96
202
 
97
203
  ## Command reference
98
204
 
99
205
  ### \`replicas computer info\`
100
- Prints the live noVNC viewer URL (\`https://6080-<hash>.tryreplicas.com/\`) for the workspace desktop. The engine auto-registers this authenticated preview on startup; \`info\` just looks it up. Use it when you want to share the live stream URL with the user out-of-band (e.g. in a Slack reply) - the dashboard \`Desktop\` tab already points at the same URL automatically.
206
+ Prints the live desktop viewer URL (\`https://6080-<hash>.tryreplicas.com/\`). The engine auto-registers this authenticated preview on startup; \`info\` just looks it up. Use it when you want to share the live stream URL with the user out-of-band (e.g. in a Slack reply) - the dashboard \`Desktop\` tab already points at the same URL automatically.
101
207
 
102
208
  If invoked very early in the workspace lifecycle, \`info\` will poll briefly while the engine finishes registering the preview, then error if it's still not available.
103
209
 
@@ -114,21 +220,33 @@ Waits briefly for the screen to stop changing, saves a 1:1 screenshot, and print
114
220
 
115
221
  Use this instead of hand-written \`sleep && screenshot\` loops after clicks, navigation, typing, or page loads. By default it saves a 100px coordinate-grid screenshot and waits up to 3s for 600ms of visual stability. Pass \`--raw\` if you need an unannotated 1:1 screenshot.
116
222
 
117
- ### \`replicas computer browser\`
118
- Prints JSON for Chrome tabs launched through Replicas, including page titles and URLs. Pass \`--snapshot\` to include visible page text and interactive controls with DOM viewport bounding boxes.
223
+ ### \`replicas computer browser-state <path> [--full] [--timeout MS] [--stable-ms MS]\`
224
+ Waits for the selected Chrome page to settle, saves its viewport screenshot, and prints a compact native Chrome accessibility tree in the same coordinate space. Actionable nodes have DOM-backed refs such as \`[ref=42] button "Save"\`; the tree includes accessible roles, names, values, states, visible bounds, and iframe content.
225
+
226
+ The first call returns a full tree. Later calls for the same target and document return semantic changes; navigation or reload forces a new full tree. Pass \`--full\` to reset the baseline. Refs are valid for the current document.
227
+
228
+ ### \`replicas computer browser [--snapshot]\`
229
+ Lists Chrome tabs. \`--snapshot\` returns the compatibility accessibility snapshot. Use \`browser-state\` for the primary action/observe loop.
230
+
231
+ ### \`replicas computer browser-batch '<actions-json>'\`
232
+ Runs up to 100 ordered browser actions through one Chrome session and stops at the first failure. Supported actions are \`click\`, \`fill\`, \`key\`, \`type\`, \`scroll\`, and \`wait\`. Prefer this when several actions use refs from the same \`browser-state\`; it avoids repeated process, tab-selection, and connection setup. Do not carry refs across navigation\u2014capture fresh state and start a new batch.
119
233
 
120
- Use this alongside \`observe\` when testing web apps so you do not infer navigation, page state, or click targets from pixels alone. Snapshot coordinates are DOM viewport coordinates, not desktop click coordinates.
234
+ ### \`replicas computer browser-click --ref ID [--button left|middle|right] [--double]\`
235
+ Scrolls the referenced node into view and sends trusted Chrome mouse input at its current visible bounds. Prefer refs from \`browser-state\`. \`browser-click <text> [--exact] [--index N]\` remains a fallback when no fresh ref is available.
121
236
 
122
- ### \`replicas computer browser-click <text> [--exact] [--index N]\`
123
- Clicks the first visible Chrome control whose text, label, placeholder, or href matches \`<text>\`. Use this for web buttons and links found via \`browser --snapshot\`; it is faster and less error-prone than converting DOM coordinates to desktop pixels.
237
+ ### \`replicas computer browser-fill --ref ID <value>\`
238
+ Focuses the referenced field, fills it with trusted Chrome input, and verifies the resulting value. Select controls use their semantic option value. \`browser-fill <field> <value> [--exact] [--index N]\` remains a text-matching fallback.
124
239
 
125
- ### \`replicas computer browser-fill <field> <value> [--exact] [--index N]\`
126
- Fills the first visible Chrome field whose label, placeholder, name, or text matches \`<field>\`, then dispatches input/change events. Use this for web forms instead of clicking a field and typing through the desktop.
240
+ ### \`replicas computer browser-scroll <up|down|left|right> [--ref ID] [--amount PX]\`
241
+ Sends trusted wheel input at a referenced element or the center of the viewport.
242
+
243
+ ### \`replicas computer browser-key <combo>\` / \`browser-type <text>\`
244
+ Sends trusted keyboard input to the focused element in the selected Chrome target. Prefer \`browser-fill\` when setting a known field value.
127
245
 
128
246
  ### \`replicas computer browser-wait <text> [--mode any|text|title|url|control] [--exact] [--timeout MS]\`
129
247
  Waits until the active Chrome page matches text in the title, URL, body text, or visible controls. Use this after \`browser-click\` / \`browser-fill\` when you need web app state to settle without screenshot polling.
130
248
 
131
- For \`browser-click\`, \`browser-fill\`, and \`browser-wait\`, pass \`--id <id>\`, \`--title <text>\`, \`--url <text>\`, or \`--page <n>\` when multiple Chrome tabs are open. Run \`replicas computer browser\` first to list tabs. Prefer \`--id\` when a click may change the page title or URL.
249
+ For browser state and actions, pass \`--target-id <id>\`, \`--title <text>\`, \`--url <text>\`, or \`--page <n>\` when multiple Chrome tabs are open. Prefer \`--target-id\` because titles and URLs can change after an action. The matched tab is brought to the foreground before each action, so driving multiple tabs or recording the screen switches which tab is visible.
132
250
 
133
251
  ### \`replicas computer click <x> <y> [--button N] [--double] [--modifiers ctrl+shift]\`
134
252
  Move to (x, y) and click. Coordinates can be absolute pixels or percentages such as \`50%\` \`50%\`. Default is left-click (button 1); pass \`--button 3\` for right-click. \`--modifiers\` holds keys during the click (e.g. ctrl-click a link to open in a new tab).
@@ -161,26 +279,29 @@ Spawns an app on the workspace display. Built-in aliases:
161
279
  Anything else gets \`exec\`'d verbatim, so \`replicas computer launch xeyes\` works if xeyes is installed.
162
280
  When opening a known page, prefer passing the URL directly: \`replicas computer launch chrome http://localhost:3000/\`.
163
281
 
282
+ Chrome launches do not return until the requested page is controllable. The output includes its page ID; carry that ID with \`--target-id\` into \`browser-state\` and browser actions so unrelated tabs cannot steal task context.
283
+
164
284
  ### \`replicas computer record start <path> [--fps N]\`
165
- Starts an ffmpeg screen recorder. Output is a fragmented MP4 (still playable if the workspace dies mid-record). Default 60fps; drop to 30 if the workspace is CPU-constrained.
285
+ Starts an ffmpeg screen recorder and returns only after ffmpeg has written the first bytes. Output is post-processed when stopped: action windows stay at normal speed, idle gaps accelerate, click moments get eased camera zoom, and a synthetic cursor animates between logged mouse positions. The raw capture is fragmented MP4 while active (still playable if the workspace dies mid-record). Default 60fps; drop to 30 if the workspace is CPU-constrained.
166
286
 
167
287
  Only one recording at a time. Re-running \`start\` while one is active fails - call \`stop\` first.
168
288
 
169
289
  ### \`replicas computer record stop\`
170
- SIGINTs ffmpeg, waits for it to finalize the MP4, prints the output path. Upload it with \`replicas media upload <path>\` to share it.
290
+ SIGINTs ffmpeg, verifies that the process has finalized the MP4, then prints the output path. A finalization timeout preserves the recording state so \`stop\` can be retried safely. Upload it with \`replicas media upload <path> --name "<name>"\` to share it.
171
291
 
172
292
  ## Patterns
173
293
 
174
- ### Action / observe loop
175
- You are blind between tool calls. After any action that changes the screen, observe before deciding the next coordinate:
294
+ ### Browser action / state loop
295
+ For Chrome, use the native accessibility state before interpreting pixels or guessing coordinates:
176
296
 
177
297
  \`\`\`bash
178
- replicas computer click 521 700
179
- replicas computer observe /tmp/after-click.png
180
- # read /tmp/after-click.png and the JSON output, decide next click
298
+ replicas computer launch chrome https://example.com
299
+ replicas computer browser-state /tmp/before.png --full --target-id <page-id>
300
+ replicas computer browser-batch '[{"action":"fill","ref":17,"value":"hello"},{"action":"click","ref":23}]' --target-id <page-id>
301
+ replicas computer browser-state /tmp/after.png --target-id <page-id>
181
302
  \`\`\`
182
303
 
183
- The JSON \`stable\`, \`frames\`, and \`changes\` fields tell you whether something changed while you were waiting. If \`stable\` is false, observe again or increase \`--timeout\` before acting on coordinates.
304
+ Read the screenshot and tree as one state. Batch ordered actions whose refs all come from that state, then inspect the next semantic diff. A batch is fail-fast and validates each ref against the cached document. Split at navigation or whenever a later action depends on newly rendered refs. Fall back to \`observe\` plus desktop coordinates for canvas content, browser chrome, non-Chrome apps, or controls missing from the accessibility tree.
184
305
 
185
306
  ### Typing into an address bar
186
307
  \`\`\`bash
@@ -189,10 +310,9 @@ replicas computer observe /tmp/browser-open.png
189
310
  replicas computer key ctrl+l
190
311
  replicas computer type "https://example.com"
191
312
  replicas computer key Return
192
- replicas computer observe /tmp/loaded.png
193
- replicas computer browser --snapshot
194
- replicas computer browser-fill "Search" "replicas"
195
- replicas computer browser-click "More information" --title "Example"
313
+ replicas computer browser-state /tmp/loaded.png --full
314
+ replicas computer browser-fill --ref 17 "replicas"
315
+ replicas computer browser-click --ref 23 --title "Example"
196
316
  replicas computer browser-wait "Example Domain" --mode title --title "Example"
197
317
  \`\`\`
198
318
 
@@ -212,12 +332,13 @@ For tasks the user wants proof of:
212
332
  replicas computer record start /tmp/walkthrough.mp4
213
333
  # ... your work ...
214
334
  replicas computer record stop
215
- replicas media upload /tmp/walkthrough.mp4
335
+ replicas media upload /tmp/walkthrough.mp4 --name "Walkthrough recording"
216
336
  \`\`\`
217
337
  Then embed the printed \`![\u2026](\u2026)\` line in your chat reply. See \`MEDIA.md\`.
218
338
 
219
339
  ## Failure modes
220
340
 
341
+ - **"Computer-use daemon is unavailable"**: the package-managed systemd service did not start. Check \`systemctl status replicas-computer.service\` and its journal.
221
342
  - **"Desktop services script missing"**: workspace image is older than this skill. Tell the user - nothing you can do from the CLI side.
222
343
  - **\`xdotool ... failed: Can't open display\`**: Xvfb didn't come up. \`replicas computer status\` will show which service is dead and auto-repair the desktop bridge.
223
344
  - **Browser doesn't appear after \`launch chrome\`**: run \`replicas computer observe /tmp/state.png\`. Chrome cold-start on the virtual display takes ~500ms but bigger pages take longer.
@@ -228,17 +349,18 @@ Then embed the printed \`![\u2026](\u2026)\` line in your chat reply. See \`MEDI
228
349
 
229
350
  | Component | Where |
230
351
  |---|---|
231
- | \`xvfb\`, \`openbox\`, \`tint2\`, \`x11vnc\`, \`websockify\`, \`xdotool\`, \`scrot\`, \`ffmpeg\`, \`google-chrome\` | Baked into the workspace image |
232
- | Xvfb / openbox / tint2 / x11vnc / websockify processes | Started at workspace boot (\`replicas-start-desktop-services\`) |
233
- | noVNC preview URL (port 6080) | Registered by the engine on startup (authenticated) |
352
+ | \`xvfb\`, \`openbox\`, \`tint2\`, \`pulseaudio\`, \`xautomation\`, \`xdotool\`, \`scrot\`, \`ffmpeg\`, \`google-chrome\` | Baked into the workspace image |
353
+ | \`replicas-computer\`, desktop assets, and service unit | Installed by \`replicas-computer setup\` during the image build |
354
+ | Xvfb / openbox / tint2 / PulseAudio / stream processes | Managed by \`replicas-computer.service\` |
355
+ | Desktop stream URL (port 6080) | Registered by the engine on startup (authenticated) |
234
356
  | Dashboard \`Desktop\` tab | Always available once the engine has registered the preview |
235
357
 
236
358
  You can call any of the input tools (\`click\`, \`type\`, \`screenshot\`, etc.) immediately - the daemons are already running.
237
359
  `;
238
360
  var COMPUTER_ABILITY = {
239
361
  label: "Computer use",
240
- description: "Drive the workspace's Linux desktop and stream a live noVNC viewer.",
241
- bullet: "- Driving the workspace Linux desktop (open a browser, click, type, scroll, screenshot, record) and exposing a live noVNC viewer in the dashboard",
362
+ description: "Drive the workspace's Linux desktop and stream a live H.264 and Opus viewer.",
363
+ bullet: "- Driving the workspace Linux desktop (open a browser, click, type, scroll, screenshot, record) and exposing a live audio and video stream in the dashboard",
242
364
  section: SECTION,
243
365
  referenceFile: { name: "COMPUTER-USE.md", content: REFERENCE }
244
366
  };
@@ -285,7 +407,7 @@ var DOCKER_ABILITY = {
285
407
 
286
408
  // ../shared/src/default-skills/replicas-agent/abilities/github.ts
287
409
  var SECTION3 = `### GitHub
288
- Use the pre-authenticated \`gh\` CLI for pull requests, issues, actions, and API calls.
410
+ Use the pre-authenticated \`gh\` CLI for repository workflows and the workspace-scoped TypeScript SDK for GitHub API calls.
289
411
 
290
412
  **Reference:** \`references/GITHUB.md\`
291
413
 
@@ -296,7 +418,7 @@ Use this when:
296
418
  - You need to include images in PR descriptions`;
297
419
  var REFERENCE3 = `# GitHub Integration
298
420
 
299
- This guide covers how to interact with GitHub from within your Replicas workspace.
421
+ This guide covers how to interact with GitHub from within your Replicas workspace. Prefer \`@replicas/sdk\` for API calls so batching and data processing stay in one TypeScript program. The \`gh\` CLI remains useful for ergonomic pull request and git-adjacent workflows.
300
422
 
301
423
  ## Prerequisites
302
424
 
@@ -381,17 +503,21 @@ gh run rerun RUN_ID --failed
381
503
 
382
504
  ### GitHub API (Advanced)
383
505
 
384
- For operations not covered by \`gh\` subcommands, use the API directly:
506
+ For operations not covered by \`gh\` subcommands, use the scoped SDK. Pass only a relative GitHub API path:
385
507
 
386
- \`\`\`bash
387
- # GET request
388
- gh api repos/owner/repo/pulls/123/comments
508
+ \`\`\`ts
509
+ import { replicas } from '@replicas/sdk';
389
510
 
390
- # POST request
391
- gh api repos/owner/repo/issues/123/comments -f body="Comment text"
511
+ const comments = await replicas.github.request('/repos/owner/repo/pulls/123/comments');
512
+ await replicas.github.request('/repos/owner/repo/issues/123/comments', {
513
+ method: 'POST',
514
+ body: { body: 'Comment text' },
515
+ });
392
516
 
393
- # GraphQL query
394
- gh api graphql -f query='{ repository(owner: "owner", name: "repo") { issues(first: 10) { nodes { title number } } } }'
517
+ const result = await replicas.github.request('/graphql', {
518
+ method: 'POST',
519
+ body: { query: '{ viewer { login } }' },
520
+ });
395
521
  \`\`\`
396
522
 
397
523
  ### Working with PR Reviews
@@ -413,12 +539,15 @@ gh pr review 123 --request-changes --body "Changes needed"
413
539
  GitHub does NOT have a public API for uploading images to PRs/issues. When you need to include images:
414
540
  - Do NOT use placeholder image URLs
415
541
  - Do NOT commit screenshots as files to the repository
416
- - Use the \`replicas-agent\` skill to share the image
542
+ - Run \`replicas media upload <image> --name "<display name>" --share\` and put the printed \`Forge embed\` Markdown in the PR body or comment
543
+ - For a self-contained HTML report, run \`replicas media upload <page.html> --name "<display name>" --access organization\` and put the printed \`Forge link\` Markdown in the PR body or comment. Organization members who are already signed in open it directly; others sign in and return to the page
544
+ - For video, embed a shared poster image linked to the recording's **View in Replicas** URL; GitHub does not reliably render externally hosted video inline
545
+ - Treat the public URL as an opt-in bearer capability; revoke it with \`replicas media revoke <media-id>\` only when the user asks or the media should stop rendering \u2014 revoking breaks embeds already posted on the PR
417
546
  - If you were triggered from Slack, also upload the image to the Slack thread so the user can see it directly
418
547
  `;
419
548
  var GITHUB_ABILITY = {
420
549
  label: "GitHub",
421
- description: "Pre-authenticated `gh` CLI for PRs, issues, releases, and GraphQL.",
550
+ description: "GitHub workflows through gh and API calls through @replicas/sdk.",
422
551
  bullet: "- Interacting with GitHub (creating PRs, managing issues, using the API, etc.)",
423
552
  section: SECTION3,
424
553
  referenceFile: { name: "GITHUB.md", content: REFERENCE3 }
@@ -426,435 +555,152 @@ var GITHUB_ABILITY = {
426
555
 
427
556
  // ../shared/src/default-skills/replicas-agent/abilities/gitlab.ts
428
557
  var SECTION4 = `### GitLab
429
- Workspace repos hosted on GitLab use pre-configured git credentials \u2014 push, pull, and open merge requests with plain \`git\`.
558
+ Use plain git for repository traffic and the workspace-scoped TypeScript SDK for GitLab API operations.
430
559
 
431
560
  **Reference:** \`references/GITLAB.md\`
432
561
 
433
562
  Use this when:
434
- - A repo's remote is on gitlab.com or a self-managed GitLab instance
435
- - You need to open or update a merge request`;
563
+ - A repository remote is on gitlab.com or a self-managed GitLab instance
564
+ - You need to open or update a merge request
565
+ - You need GitLab issues, pipelines, comments, or other API data`;
436
566
  var REFERENCE4 = `# GitLab Integration
437
567
 
438
- This guide covers how to work with GitLab-hosted repositories from within your Replicas workspace.
439
-
440
- ## Prerequisites
441
-
442
- Git credentials for the workspace's GitLab hosts are pre-configured in \`~/.git-credentials\` and refreshed automatically. Plain \`git fetch\` / \`git pull\` / \`git push\` over HTTPS work with no additional setup.
568
+ Git credentials are preconfigured for fetch, pull, and push. Use \`@replicas/sdk\` for API traffic; never inspect \`~/.git-credentials\`, request a token, or send a credential with curl.
443
569
 
444
- - Never ask the user for a GitLab token or PAT \u2014 credentials are already wired. If a push fails with an authentication error, report it to the user instead of working around it.
445
- - There is no \`glab\` CLI in the workspace, and \`gh\` only works for GitHub remotes. Check a repo's host with \`git remote get-url origin\` before choosing the GitHub or GitLab workflow.
570
+ ## Merge requests
446
571
 
447
- ## Merge Requests
448
-
449
- Create a merge request directly from a push using push options:
572
+ Create a merge request directly from a push:
450
573
 
451
574
  \`\`\`bash
452
- git push -o merge_request.create -o merge_request.target=<default-branch> origin HEAD
575
+ git push -o merge_request.create -o merge_request.target=<default-branch> -o merge_request.title="Title" origin HEAD
453
576
  \`\`\`
454
577
 
455
- Useful options:
578
+ Useful options are \`-o merge_request.draft\` and \`-o merge_request.remove_source_branch\`. GitLab prints the merge request URL.
456
579
 
457
- \`\`\`bash
458
- -o merge_request.title="Title"
459
- -o merge_request.description="Description"
460
- -o merge_request.draft # open as draft
461
- -o merge_request.remove_source_branch # delete branch on merge
462
- \`\`\`
580
+ ## API requests
463
581
 
464
- GitLab prints the MR URL in the push output \u2014 include it in your reply to the user.
582
+ Pass a relative path below \`/api/v4\`. Replicas selects the credential only from GitLab hosts attached to the workspace repositories.
465
583
 
466
- ## GitLab API (advanced)
584
+ \`\`\`ts
585
+ import { replicas } from '@replicas/sdk';
467
586
 
468
- For operations with no git equivalent (commenting on MRs, reading pipelines), call the REST API with the workspace credential:
587
+ const project = encodeURIComponent('group/project');
588
+ const mergeRequests = await replicas.gitlab.request(
589
+ \`/projects/\${project}/merge_requests?state=opened\`,
590
+ );
469
591
 
470
- \`\`\`bash
471
- TOKEN=$(grep -m1 '://oauth2:' ~/.git-credentials | sed -E 's#https://oauth2:([^@]+)@.*#\\1#')
472
- curl -s -H "Authorization: Bearer $TOKEN" "https://gitlab.com/api/v4/projects/<url-encoded-path>/merge_requests"
592
+ await replicas.gitlab.request(\`/projects/\${project}/issues/42/notes\`, {
593
+ method: 'POST',
594
+ body: { body: 'Update from Replicas' },
595
+ });
596
+ \`\`\`
597
+
598
+ When a workspace contains repositories from more than one GitLab host, pass the exact repository host:
599
+
600
+ \`\`\`ts
601
+ await replicas.gitlab.request('/projects/group%2Frepo/pipelines', {
602
+ host: 'gitlab.example.com',
603
+ });
473
604
  \`\`\`
474
605
 
475
- Use the repo's own host in the API base URL for self-managed instances.
606
+ ## Descriptions and media
607
+
608
+ API updates replace the entire merge request description, so always preserve the required Replicas footer and co-author trailers. For media, first run \`replicas media upload\` as documented in \`MEDIA.md\`; use GitLab's project uploads endpoint only when a native repository-protected embed is required.
476
609
  `;
477
610
  var GITLAB_ABILITY = {
478
611
  label: "GitLab",
479
- description: "Pre-configured git credentials for GitLab repos; merge requests via push options.",
612
+ description: "Git for repository traffic and @replicas/sdk for the GitLab API.",
480
613
  bullet: "- Interacting with GitLab (pushing to GitLab repos, opening merge requests)",
481
614
  section: SECTION4,
482
615
  referenceFile: { name: "GITLAB.md", content: REFERENCE4 }
483
616
  };
484
617
 
485
- // ../shared/src/default-skills/replicas-agent/abilities/google.ts
486
- var SECTION5 = `### Google Workspace (Docs, Sheets, Forms, Drive)
487
- Create and edit Google Docs, Sheets, and Forms via the Replicas gateway. Files are owned by Replicas \u2014 the integration cannot access pre-existing Google content created outside of it.
488
-
489
- **Reference:** \`references/GOOGLE.md\`
490
-
491
- Use this when:
492
- - You need to create or edit a Google Doc, Sheet, or Form
493
- - You need to share, rename, move, or delete a Replicas-created Google file
494
- - You need to read responses from a Replicas-created Google Form`;
495
- var REFERENCE5 = `# Google Workspace (Docs, Sheets, Forms, Drive)
496
-
497
- This guide covers how to create and edit Google Docs, Sheets, and Forms \u2014 plus do basic Drive file operations \u2014 from inside a Replicas workspace, using the monolith as a gateway to Google's APIs.
498
-
499
- ## Prerequisites
500
-
501
- The integration is configured at the org or user level by the Replicas admin. From inside a workspace you don't have a Google access token directly; instead you call the monolith's \`/v1/gdrive/*\` endpoints, authenticated with your workspace's engine secret. The monolith refreshes the org's (or user's) Google access token and proxies the call.
502
-
503
- Quick check that the integration is connected:
504
-
505
- \`\`\`bash
506
- curl -s -X GET "$MONOLITH_URL/v1/gdrive/credentials" \\
507
- -H "Authorization: Bearer $REPLICAS_ENGINE_SECRET" \\
508
- -H "X-Workspace-Id: $WORKSPACE_ID"
509
- \`\`\`
510
-
511
- - If \`hasCredentials\` is \`true\`: you're good to go.
512
- - If \`hasCredentials\` is \`false\`: Google has not been connected for this org. Ask the user to go to **Settings \u2192 Integrations \u2192 Google** in the [Replicas dashboard](https://replicas.dev) and connect a Google account. Do not attempt Google operations until it's connected.
513
-
514
- Standard auth headers used by every call below:
515
-
516
- \`\`\`
517
- Authorization: Bearer $REPLICAS_ENGINE_SECRET
518
- X-Workspace-Id: $WORKSPACE_ID
519
- \`\`\`
520
-
521
- For brevity the examples below use a shell variable:
522
-
523
- \`\`\`bash
524
- GDRIVE_AUTH=(-H "Authorization: Bearer $REPLICAS_ENGINE_SECRET" -H "X-Workspace-Id: $WORKSPACE_ID")
525
- \`\`\`
526
-
527
- ## Important constraint: drive.file scope
528
-
529
- The integration uses the **sensitive-tier \`drive.file\` scope**. That means Replicas can only read and edit Google files **it created itself**. It **cannot**:
530
-
531
- - Read or edit a user's pre-existing Google Docs, Sheets, or Forms \u2014 even ones that were shared with the connected Google account.
532
- - List or search the user's broader Drive.
533
- - Touch any file that was not created via these gateway endpoints.
534
-
535
- If the user asks you to edit an existing doc that Replicas didn't create, tell them this constraint and offer to create a new doc that mirrors what they want.
536
-
537
- ## Google Docs
538
-
539
- ### Create a new doc
540
-
541
- \`\`\`bash
542
- curl -s -X POST "$MONOLITH_URL/v1/gdrive/docs" "\${GDRIVE_AUTH[@]}" \\
543
- -H "Content-Type: application/json" \\
544
- -d '{"title":"Meeting notes 2026-05-14"}'
545
- \`\`\`
546
-
547
- Returns the full Doc object; grab \`.documentId\` for follow-up calls.
548
-
549
- ### Read a doc
550
-
551
- \`\`\`bash
552
- curl -s "$MONOLITH_URL/v1/gdrive/docs/$DOC_ID" "\${GDRIVE_AUTH[@]}"
553
- \`\`\`
554
-
555
- Returns the full document structure \u2014 \`body.content\` is an ordered list of structural elements (paragraphs, tables, etc.) with character indexes you can target for edits.
556
-
557
- ### Edit a doc (batchUpdate)
558
-
559
- The Docs API edits use a list of [structural requests](https://developers.google.com/workspace/docs/api/reference/rest/v1/documents/request). Insert text, then style it; or insert tables, images, page breaks, etc.
560
-
561
- \`\`\`bash
562
- curl -s -X POST "$MONOLITH_URL/v1/gdrive/docs/$DOC_ID/batchUpdate" "\${GDRIVE_AUTH[@]}" \\
563
- -H "Content-Type: application/json" \\
564
- -d '{
565
- "requests": [
566
- { "insertText": { "location": { "index": 1 }, "text": "Hello, world!\\n" } }
567
- ]
568
- }'
569
- \`\`\`
570
-
571
- Common request types:
572
-
573
- - \`insertText\` \u2014 insert plain text at a given index
574
- - \`deleteContentRange\` \u2014 delete a range
575
- - \`replaceAllText\` \u2014 find-and-replace
576
- - \`updateTextStyle\` \u2014 bold/italic/colors/fonts/sizes for a range
577
- - \`updateParagraphStyle\` \u2014 headings (\`HEADING_1\`..\`HEADING_6\`), alignment, spacing
578
- - \`createParagraphBullets\` \u2014 turn paragraphs into bulleted/numbered lists
579
- - \`insertTable\` \u2014 insert a table
580
- - \`insertInlineImage\` \u2014 insert an image from a URL
581
-
582
- Edits are verbose but powerful. Prefer batching many requests into a single \`batchUpdate\` call rather than making many round trips \u2014 it's faster and keeps the doc state consistent.
583
-
584
- ## Google Sheets
585
-
586
- ### Create a new spreadsheet
587
-
588
- \`\`\`bash
589
- curl -s -X POST "$MONOLITH_URL/v1/gdrive/sheets" "\${GDRIVE_AUTH[@]}" \\
590
- -H "Content-Type: application/json" \\
591
- -d '{"title":"Q2 metrics"}'
592
- \`\`\`
593
-
594
- Returns the full Spreadsheet object; grab \`.spreadsheetId\`.
595
-
596
- ### Read a range of cells
597
-
598
- \`\`\`bash
599
- # Range is in A1 notation, e.g. "Sheet1!A1:C10"
600
- curl -s "$MONOLITH_URL/v1/gdrive/sheets/$SHEET_ID/values/$(printf %s 'Sheet1!A1:C10' | jq -sRr @uri)" \\
601
- "\${GDRIVE_AUTH[@]}"
602
- \`\`\`
603
-
604
- ### Write a range of cells
605
-
606
- \`\`\`bash
607
- curl -s -X PUT \\
608
- "$MONOLITH_URL/v1/gdrive/sheets/$SHEET_ID/values/$(printf %s 'Sheet1!A1:B2' | jq -sRr @uri)?valueInputOption=USER_ENTERED" \\
609
- "\${GDRIVE_AUTH[@]}" \\
610
- -H "Content-Type: application/json" \\
611
- -d '{
612
- "range": "Sheet1!A1:B2",
613
- "majorDimension": "ROWS",
614
- "values": [
615
- ["Name", "Revenue"],
616
- ["Q1", "=SUM(B3:B100)"]
617
- ]
618
- }'
619
- \`\`\`
620
-
621
- \`valueInputOption=USER_ENTERED\` makes formulas evaluate as if a human typed them. Use \`RAW\` to write literal strings.
622
-
623
- ### Bulk operations (formatting, charts, new tabs, etc.)
624
-
625
- \`\`\`bash
626
- curl -s -X POST "$MONOLITH_URL/v1/gdrive/sheets/$SHEET_ID/batchUpdate" "\${GDRIVE_AUTH[@]}" \\
627
- -H "Content-Type: application/json" \\
628
- -d '{ "requests": [ { "addSheet": { "properties": { "title": "Raw data" } } } ] }'
629
- \`\`\`
630
-
631
- \`batchUpdate\` accepts an array of [Sheets API requests](https://developers.google.com/workspace/sheets/api/reference/rest/v4/spreadsheets/request) \u2014 addSheet, updateSheetProperties, repeatCell, addChart, autoResizeDimensions, etc.
632
-
633
- ## Google Forms
634
-
635
- ### Create a new form
618
+ // ../shared/src/default-skills/replicas-agent/abilities/learnings.ts
619
+ var SECTION5 = `### Learnings
620
+ Propose additions, updates, or deletions to curated organization knowledge.
636
621
 
637
- \`\`\`bash
638
- curl -s -X POST "$MONOLITH_URL/v1/gdrive/forms" "\${GDRIVE_AUTH[@]}" \\
639
- -H "Content-Type: application/json" \\
640
- -d '{"title":"Customer feedback"}'
641
- \`\`\`
642
-
643
- Returns the form. Grab \`.formId\`.
644
-
645
- ### Add questions / edit form structure
646
-
647
- The Forms API uses its own \`batchUpdate\`:
648
-
649
- \`\`\`bash
650
- curl -s -X POST "$MONOLITH_URL/v1/gdrive/forms/$FORM_ID/batchUpdate" "\${GDRIVE_AUTH[@]}" \\
651
- -H "Content-Type: application/json" \\
652
- -d '{
653
- "requests": [
654
- {
655
- "createItem": {
656
- "item": {
657
- "title": "How likely are you to recommend us?",
658
- "questionItem": {
659
- "question": {
660
- "required": true,
661
- "scaleQuestion": { "low": 0, "high": 10, "lowLabel": "Not at all", "highLabel": "Very likely" }
662
- }
663
- }
664
- },
665
- "location": { "index": 0 }
666
- }
667
- }
668
- ]
669
- }'
670
- \`\`\`
671
-
672
- See the [Forms API Request reference](https://developers.google.com/workspace/forms/api/reference/rest/v1/forms/request) for all supported request types \u2014 text questions, multiple choice, checkboxes, scale, grid, date, time, file upload, section breaks, branching logic, quiz settings, etc.
673
-
674
- ### Read responses
675
-
676
- \`\`\`bash
677
- curl -s "$MONOLITH_URL/v1/gdrive/forms/$FORM_ID/responses" "\${GDRIVE_AUTH[@]}"
678
- \`\`\`
679
-
680
- Pagination: pass \`?pageToken=...&pageSize=...\` to walk through responses.
681
-
682
- ## Drive operations (only on Replicas-created files)
683
-
684
- ### Share a file with a person
685
-
686
- \`\`\`bash
687
- curl -s -X POST "$MONOLITH_URL/v1/gdrive/files/$FILE_ID/permissions" "\${GDRIVE_AUTH[@]}" \\
688
- -H "Content-Type: application/json" \\
689
- -d '{
690
- "type": "user",
691
- "role": "writer",
692
- "emailAddress": "alice@example.com",
693
- "sendNotificationEmail": true
694
- }'
695
- \`\`\`
696
-
697
- Roles: \`reader\`, \`commenter\`, \`writer\`. Types: \`user\`, \`group\`, \`domain\`, \`anyone\`.
698
-
699
- ### Make a file viewable by anyone with the link
700
-
701
- \`\`\`bash
702
- curl -s -X POST "$MONOLITH_URL/v1/gdrive/files/$FILE_ID/permissions" "\${GDRIVE_AUTH[@]}" \\
703
- -H "Content-Type: application/json" \\
704
- -d '{ "type": "anyone", "role": "reader" }'
705
- \`\`\`
706
-
707
- ### List existing permissions
708
-
709
- \`\`\`bash
710
- curl -s "$MONOLITH_URL/v1/gdrive/files/$FILE_ID/permissions" "\${GDRIVE_AUTH[@]}"
711
- \`\`\`
712
-
713
- ### Rename / move a file
622
+ **Reference:** \`references/LEARNINGS.md\``;
623
+ var REFERENCE5 = `# Learnings
714
624
 
715
- \`\`\`bash
716
- curl -s -X PATCH "$MONOLITH_URL/v1/gdrive/files/$FILE_ID" "\${GDRIVE_AUTH[@]}" \\
717
- -H "Content-Type: application/json" \\
718
- -d '{"name":"Final report.docx"}'
719
- \`\`\`
720
-
721
- To move into a folder, also pass \`addParents\` and \`removeParents\` as query parameters per [Drive API docs](https://developers.google.com/workspace/drive/api/reference/rest/v3/files/update).
722
-
723
- ### Get file metadata
724
-
725
- \`\`\`bash
726
- curl -s "$MONOLITH_URL/v1/gdrive/files/$FILE_ID?fields=id,name,mimeType,webViewLink,parents,modifiedTime" \\
727
- "\${GDRIVE_AUTH[@]}"
728
- \`\`\`
729
-
730
- \`webViewLink\` is the human-shareable URL that opens in Google Docs/Sheets/Forms.
625
+ Learnings are curated, human-approved knowledge entries your organization maintains: conventions, gotchas, environment facts, and process rules.
731
626
 
732
- ### Delete a file
627
+ All writes are proposals: a human reviews them in the dashboard, and nothing takes effect until approved.
733
628
 
734
- \`\`\`bash
735
- curl -s -X DELETE "$MONOLITH_URL/v1/gdrive/files/$FILE_ID" "\${GDRIVE_AUTH[@]}"
736
- \`\`\`
737
-
738
- ### List Replicas-created files
739
-
740
- \`\`\`bash
741
- curl -s "$MONOLITH_URL/v1/gdrive/files?pageSize=50" "\${GDRIVE_AUTH[@]}"
742
- \`\`\`
629
+ Use \`replicas learnings --help\` and subcommand help such as \`replicas learnings add --help\`, \`replicas learnings update --help\`, or \`replicas learnings delete --help\` for current command syntax.
743
630
 
744
- Pass \`?q=...\` to filter using [Drive API search syntax](https://developers.google.com/workspace/drive/api/guides/search-files). Only files the app created or was granted access to will appear.
631
+ A human approves every proposal before it takes effect, so don't agonize over borderline calls \u2014 but keep proposals genuinely useful rather than reflexive.
745
632
 
746
- ## Tips
747
-
748
- - **Always return the \`webViewLink\`** to the user when you create or edit a file so they can open it. Get it from \`/v1/gdrive/files/$FILE_ID?fields=webViewLink\` or from \`documents.documentId\` \u2192 \`https://docs.google.com/document/d/$ID/edit\`.
749
- - **Batch your edits.** A single \`batchUpdate\` with 20 requests beats 20 round-trips.
750
- - **Watch error responses.** A 412 from the gateway means the org has no Google credentials connected \u2014 tell the user to connect Google in the dashboard. Other 4xx responses come straight from Google and usually explain the issue clearly.
751
- `;
752
- var GOOGLE_ABILITY = {
753
- label: "Google Workspace",
754
- description: "Create / edit Docs, Sheets, Forms, and Drive files via the Replicas gateway.",
755
- bullet: "- Interacting with Google Workspace (creating and editing Docs, Sheets, and Forms, sharing files, reading form responses, etc.)",
633
+ - Propose \`add\` for anything repeatable and org-useful: user corrections and preferences, hard-won environment or process knowledge, conventions and decisions worth following, and gotchas that would waste time again. If you learned it the hard way this task, capture it.
634
+ - Never store task-specific details that won't recur, facts trivially derivable by reading the repo, or secrets/credentials.
635
+ - Propose \`update\` or \`delete\` when a learning is contradicted by the current code or docs, especially if the outdated guidance would affect future repeated work.
636
+ - Write narrow triggers describing when the entry should surface (e.g. "UI/UX work on the admin panel"), not vague topics.`;
637
+ var LEARNINGS_ABILITY = {
638
+ label: "Learnings",
639
+ description: "Propose changes to curated org knowledge for human review.",
640
+ bullet: "- Proposing changes to organization Learnings (curated, human-approved knowledge)",
756
641
  section: SECTION5,
757
- referenceFile: { name: "GOOGLE.md", content: REFERENCE5 }
642
+ referenceFile: { name: "LEARNINGS.md", content: REFERENCE5 }
758
643
  };
759
644
 
760
645
  // ../shared/src/default-skills/replicas-agent/abilities/linear.ts
761
646
  var SECTION6 = `### Linear
762
- Fetch issues, update state, add comments, and search via the Linear GraphQL API.
647
+ Fetch issues, update state, add comments, and search through the workspace-scoped TypeScript SDK.
763
648
 
764
649
  **Reference:** \`references/LINEAR.md\`
765
650
 
766
651
  Use this when:
767
652
  - You encounter a Linear issue link and need to understand the task
768
- - You need to update an issue's state (e.g. mark as done)
769
- - You need to comment on or search for Linear issues`;
653
+ - You need to update an issue's state or add a comment
654
+ - You need to search Linear issues`;
770
655
  var REFERENCE6 = `# Linear Integration
771
656
 
772
- This guide covers how to interact with Linear from within your Replicas workspace.
773
-
774
- ## Prerequisites
775
-
776
- Check that the workspace has the Replicas engine credentials needed to proxy Linear requests:
777
-
778
- \`\`\`bash
779
- test -n "$MONOLITH_URL" && test -n "$REPLICAS_ENGINE_SECRET" && test -n "$WORKSPACE_ID" && echo set
780
- \`\`\`
781
-
782
- - If **set**: Use the Replicas Linear proxy below. It refreshes Linear OAuth tokens for you.
783
- - If **not set**: The workspace cannot authenticate to Replicas. Let the user know and do not attempt Linear operations.
784
-
785
- Then verify Linear is connected:
786
-
787
- \`\`\`bash
788
- curl -fsS -X POST "$MONOLITH_URL/v1/engine/linear/refresh-token" \\
789
- -H "Authorization: Bearer $REPLICAS_ENGINE_SECRET" \\
790
- -H "X-Workspace-Id: $WORKSPACE_ID" \\
791
- -H "Content-Type: application/json" \\
792
- -d '{}' >/dev/null
793
- \`\`\`
794
-
795
- If this fails because no Linear credential is connected, the user needs to connect Linear in the [Replicas dashboard](https://replicas.dev) under their organization's integration settings.
796
-
797
- ## Using the Linear API
798
-
799
- Linear uses a GraphQL API. Always call it through Replicas so the access token stays fresh:
800
-
801
- \`\`\`bash
802
- linear_graphql() {
803
- curl -sS -X POST "$MONOLITH_URL/v1/engine/linear/graphql" \\
804
- -H "Authorization: Bearer $REPLICAS_ENGINE_SECRET" \\
805
- -H "X-Workspace-Id: $WORKSPACE_ID" \\
806
- -H "Content-Type: application/json" \\
807
- --data-binary @-
808
- }
809
- \`\`\`
810
-
811
- ### Fetching an Issue
812
-
813
- If you encounter a Linear issue link (e.g. \`https://linear.app/team/issue/ENG-123\`), the identifier is the last path segment (\`ENG-123\`).
657
+ Use \`@replicas/sdk\`; never request or handle a Linear OAuth token. Replicas refreshes the organization credential behind the gateway.
814
658
 
815
- \`\`\`bash
816
- jq -n --arg identifier "ENG-123" '{
817
- query: "query($identifier: String!) { issue(id: $identifier) { id identifier title description state { name } assignee { name } parent { identifier title description } } }",
818
- variables: { identifier: $identifier }
819
- }' | linear_graphql
820
- \`\`\`
659
+ ## Check access
821
660
 
822
- ### Updating Issue State
661
+ \`\`\`ts
662
+ import { replicas } from '@replicas/sdk';
823
663
 
824
- \`\`\`bash
825
- jq -n --arg issueId "ISSUE_UUID" --arg stateId "STATE_UUID" '{
826
- query: "mutation($issueId: String!, $stateId: String!) { issueUpdate(id: $issueId, input: { stateId: $stateId }) { success issue { identifier state { name } } } }",
827
- variables: { issueId: $issueId, stateId: $stateId }
828
- }' | linear_graphql
664
+ const { native } = await replicas.integrations.list();
665
+ if (!native.linear) throw new Error('Connect Linear in Replicas organization integrations first.');
829
666
  \`\`\`
830
667
 
831
- To find available states, query: \`query { workflowStates { nodes { id name } } }\`
668
+ ## Fetch an issue
832
669
 
833
- ### Adding a Comment
834
-
835
- \`\`\`bash
836
- jq -n --arg issueId "ISSUE_UUID" --arg body "Your comment here" '{
837
- query: "mutation($issueId: String!, $body: String!) { commentCreate(input: { issueId: $issueId, body: $body }) { success comment { id } } }",
838
- variables: { issueId: $issueId, body: $body }
839
- }' | linear_graphql
670
+ \`\`\`ts
671
+ const issue = await replicas.linear.graphql({
672
+ query: \`query($identifier: String!) {
673
+ issue(id: $identifier) {
674
+ id identifier title description state { id name } assignee { name }
675
+ parent { identifier title description }
676
+ }
677
+ }\`,
678
+ variables: { identifier: 'ENG-123' },
679
+ });
840
680
  \`\`\`
841
681
 
842
- ### Searching Issues
682
+ ## Update and comment in one script
843
683
 
844
- \`\`\`bash
845
- jq -n --arg query "search terms" '{
846
- query: "query($query: String!) { issues(filter: { or: [{ title: { containsIgnoreCase: $query } }, { description: { containsIgnoreCase: $query } }] }, first: 10) { nodes { identifier title state { name } } } }",
847
- variables: { query: $query }
848
- }' | linear_graphql
684
+ \`\`\`ts
685
+ await replicas.linear.graphql({
686
+ query: \`mutation($issueId: String!, $stateId: String!) {
687
+ issueUpdate(id: $issueId, input: { stateId: $stateId }) { success }
688
+ }\`,
689
+ variables: { issueId, stateId },
690
+ });
691
+ await replicas.linear.graphql({
692
+ query: \`mutation($issueId: String!, $body: String!) {
693
+ commentCreate(input: { issueId: $issueId, body: $body }) { success comment { id } }
694
+ }\`,
695
+ variables: { issueId, body: 'Implemented in PR #123.' },
696
+ });
849
697
  \`\`\`
850
698
 
851
- ### Other Operations
852
-
853
- The Linear GraphQL API supports creating issues, managing projects, labels, cycles, and more. For the full schema and documentation, see: https://developers.linear.app/docs/graphql/working-with-the-graphql-api
699
+ Search with \`containsIgnoreCase: $query\` filters, for example on both \`title\` and \`description\`. Batch independent queries with \`Promise.all\` to save round trips.
854
700
  `;
855
701
  var LINEAR_ABILITY = {
856
702
  label: "Linear",
857
- description: "Fetch issues, post comments, update states via the Linear GraphQL API.",
703
+ description: "Fetch issues, comment, and update state through @replicas/sdk.",
858
704
  bullet: "- Interacting with Linear (fetching issues, updating state, commenting, etc.)",
859
705
  section: SECTION6,
860
706
  referenceFile: { name: "LINEAR.md", content: REFERENCE6 }
@@ -862,17 +708,18 @@ var LINEAR_ABILITY = {
862
708
 
863
709
  // ../shared/src/default-skills/replicas-agent/abilities/media.ts
864
710
  var SECTION7 = `### Media
865
- Share screenshots, screen recordings, generated diagrams, and audio clips inline in the Replicas chat and natively embedded in external messages.
711
+ Share screenshots, screen recordings, generated diagrams, audio clips, and self-contained HTML pages through Replicas and external messages.
866
712
 
867
713
  **Reference:** \`references/MEDIA.md\`
868
714
 
869
715
  Use this when:
870
716
  - You produce a screenshot, recording, generated image, or audio clip the user should see
717
+ - You produce a self-contained HTML report that should be linked from a pull request
871
718
  - You record video output (browser automation, screen capture) \u2014 including the recommended aspect ratio and FPS
872
719
  - You need to embed media in a Slack/Linear/GitHub message AND keep a referenceable copy in the Replicas dashboard`;
873
- var REFERENCE7 = `# Media (Screenshots, Recordings, Audio)
720
+ var REFERENCE7 = `# Media (Screenshots, Recordings, Audio, HTML)
874
721
 
875
- This guide covers how to share screenshots, screen recordings, generated diagrams, and audio clips \u2014 both inline in the Replicas chat and natively embedded in external surfaces (Slack, Linear, GitHub).
722
+ This guide covers how to share screenshots, screen recordings, generated diagrams, audio clips, and self-contained HTML pages through Replicas and external surfaces (Slack, Linear, GitHub).
876
723
 
877
724
  ## Prerequisites
878
725
 
@@ -883,7 +730,7 @@ The \`replicas\` CLI is pre-installed and authenticated in your workspace. No ad
883
730
  Upload to Replicas in these cases \u2014 and **only** these cases:
884
731
 
885
732
  1. **Media you produce.** Any screenshot, screen recording, generated diagram, or audio clip you create that the user might want to see. Upload before doing anything else with the file (analyzing, deleting, sending elsewhere). This applies even when you're also sending the file to Slack, Linear, GitHub, etc.
886
- 2. **Files the user explicitly asks you to upload.** If the user sends or points at a file (image, video, audio) and asks you to upload it, run \`replicas media upload\`. Otherwise leave it alone \u2014 files in the workspace the user did not ask about should not be auto-uploaded as media.
733
+ 2. **Files the user explicitly asks you to upload.** If the user sends or points at a file (image, video, audio, HTML) and asks you to upload it, run \`replicas media upload\`. Otherwise leave it alone \u2014 files in the workspace the user did not ask about should not be auto-uploaded as media.
887
734
  3. **Anything you plan to share externally (Slack, Linear, GitHub, etc.).** Upload to Replicas *in addition to* the platform's native upload. Never as a replacement.
888
735
 
889
736
  If none of these apply, don't upload.
@@ -891,24 +738,60 @@ If none of these apply, don't upload.
891
738
  ## Uploading to Replicas
892
739
 
893
740
  \`\`\`bash
894
- replicas media upload <path-to-file> [<path-to-file> ...]
741
+ replicas media upload <path-to-file> --name "<display name>" [<path-to-file> --name "<display name>" ...]
742
+ \`\`\`
743
+
744
+ Pass one or more file paths, each with a required \`--name\` (see below). Uploading several files in a single invocation is preferred over running the command repeatedly: it's faster and keeps the output grouped.
745
+
746
+ ### Name each upload
747
+
748
+ \`--name\` is **required** and sets the short, human-readable display name shown in the Replicas dashboard (the backend still tracks the asset by UUID). Make it descriptive of the content, e.g. \`login-page\`, \`checkout-error\`, or \`db-latency-chart\`. Never use a generic value like \`screenshot\` or \`image\`.
749
+
750
+ \`\`\`bash
751
+ replicas media upload /tmp/shot.png --name "Login page after fix"
752
+ \`\`\`
753
+
754
+ Pass one \`--name\` per file, in the same order as the file paths:
755
+
756
+ \`\`\`bash
757
+ replicas media upload /tmp/before.png /tmp/after.png --name "Before fix" --name "After fix"
895
758
  \`\`\`
896
759
 
897
- Pass one or more file paths. Uploading several files in a single invocation is preferred over running the command repeatedly \u2014 it's faster and keeps the output grouped.
760
+ Names are normalized to hyphen-case (spaces become hyphens, e.g. \`Login page\` is stored as \`login-page\`). Give every asset a **distinct** name \u2014 duplicates are not rejected, but Replicas keeps the first as-is and adds a \`-1\`, \`-2\` suffix to later collisions, which produces confusing lists, so pick unique names yourself.
898
761
 
899
- For each file, the CLI prints two lines:
762
+ For each media file, the CLI prints two lines:
900
763
 
901
764
  1. \`![filename](<api-url>)\` \u2014 the chat embed (renders inline in Replicas chat **only** \u2014 see below).
902
- 2. \`View in Replicas: <deep-link>\` \u2014 a per-file dashboard URL of the form \`https://tryreplicas.com/workspaces/<workspace-id>?mode=media&media=<media-id>\` that opens directly to that specific file.
765
+ 2. \`View in Replicas: <deep-link>\` \u2014 a per-file dashboard URL of the form \`https://tryreplicas.com/workspaces/<workspace-id>?media=<media-id>\` that opens directly to that specific file.
903
766
 
904
767
  Match each "View in Replicas" line to the embed line directly above it \u2014 that's the deep link for that file.
905
768
 
906
- ## The two URLs the CLI gives you \u2014 and where each one is allowed
769
+ HTML uploads print an \`HTML page\` Markdown link instead of an image embed.
770
+
771
+ For an HTML page that should stay inside the Replicas organization, create an authenticated link:
772
+
773
+ \`\`\`bash
774
+ replicas media upload recap.html --name "Visual recap" --access organization
775
+ \`\`\`
776
+
777
+ The printed \`Forge link (organization only)\` opens a dedicated page. Signed-in organization members go straight to it; signed-out members sign in and return to the page.
778
+
779
+ Use public access only when the media must render without Replicas authentication:
780
+
781
+ \`\`\`bash
782
+ replicas media upload screenshot.png --name "Login page" --access public
783
+ \`\`\`
784
+
785
+ \`--share\` remains an alias for \`--access public\`. Public access prints an additional \`Forge embed\` for image, video, or audio media, backed by an opt-in bearer URL. Anyone with that unguessable URL can view the asset until it is rotated or revoked. HTML pages only support organization access. Use \`replicas media share <media-id>\` to rotate a public media URL and \`replicas media revoke <media-id>\` to revoke it.
786
+
787
+ ## The URLs the CLI gives you \u2014 and where each one is allowed
907
788
 
908
789
  | URL | What it is | Where it's allowed |
909
790
  |---|---|---|
910
791
  | \`https://api.tryreplicas.com/v1/media/<id>\` (inside \`![\u2026](\u2026)\`) | Authenticated API redirect to a presigned blob \u2014 requires the Replicas chat session | **Only** in the Replicas chat reply. Nowhere else. |
911
- | \`https://tryreplicas.com/workspaces/<workspace-id>?mode=media&media=<media-id>\` | Public dashboard deep link to the media tab, scrolled to that specific file | Everywhere external \u2014 Slack, GitHub, Linear, PR bodies, comments, emails. |
792
+ | \`https://tryreplicas.com/organizations/<organization-id>/media/<media-id>\` | Organization-only page printed by \`--access organization\` | Pull requests and other places where every viewer belongs to the Replicas organization. |
793
+ | \`https://api.tryreplicas.com/v1/media/public/<id>/<token>\` | Opt-in public bearer URL printed by \`--access public\` or \`--share\` | GitHub embeds for image, video, or audio media. |
794
+ | \`https://tryreplicas.com/workspaces/<workspace-id>?media=<media-id>\` | Authenticated dashboard deep link that opens the media file in the workspace | Everywhere external \u2014 Slack, GitHub, Linear, PR bodies, comments, emails. |
912
795
 
913
796
  ### NEVER link the API redirect URL outside the Replicas chat reply
914
797
 
@@ -921,38 +804,36 @@ This rule has no exceptions:
921
804
  - \u274C Never link to it from any external doc, email, or external chat.
922
805
  - \u274C Never include it as a "backup link" in case the dashboard link breaks \u2014 it will always break externally.
923
806
 
924
- ### ALWAYS link the dashboard URL \u2014 never the API URL \u2014 when referencing media externally
807
+ ### ALWAYS link a Replicas web URL \u2014 never the authenticated API URL \u2014 when referencing media externally
925
808
 
926
- When you reference an uploaded file from anywhere outside the Replicas chat reply, the link must be the dashboard form:
809
+ For an organization-only HTML page, use the \`Forge link\` printed by \`--access organization\`. For other authenticated media references, use the dashboard form:
927
810
 
928
811
  \`\`\`
929
- https://tryreplicas.com/workspaces/<workspace-id>?mode=media&media=<media-id>
812
+ https://tryreplicas.com/workspaces/<workspace-id>?media=<media-id>
930
813
  \`\`\`
931
814
 
932
- Both query params are required:
933
- - \`mode=media\` opens the media tab in the dashboard.
934
- - \`media=<media-id>\` scrolls to and opens that specific file.
815
+ The \`media=<media-id>\` query param opens that specific file in the workspace.
935
816
 
936
- Never link \`tryreplicas.com/workspaces/<id>?mode=media\` without the \`media=<id>\` param when you have a specific file in mind \u2014 that lands on the gallery, not the file. Never link the bare workspace URL.
817
+ When you have a specific file in mind, never link the bare workspace URL without the \`media=<id>\` param.
937
818
 
938
819
  Always render as a markdown / mrkdwn hyperlink labeled **View in Replicas** \u2014 never paste a raw URL.
939
820
 
940
821
  ## In your Replicas chat reply
941
822
 
942
- Include each markdown embed line **verbatim** where you want that file to render inline. The chat substitutes each one with an embedded image, video, or audio player. Multiple embeds can appear in a single reply.
823
+ Include each markdown embed line **verbatim** where you want that file to render inline. The chat substitutes each one with an embedded image, video, or audio player. For HTML, use the \`HTML page\` or \`View in Replicas\` link instead. Multiple embeds can appear in a single reply.
943
824
 
944
- After (or alongside) the embeds, include a \`[View in Replicas](<deep-link>)\` hyperlink for each file using the per-file dashboard URL from the CLI. This lets the user jump to the specific item in the media tab.
825
+ After (or alongside) the embeds, include a \`[View in Replicas](<deep-link>)\` hyperlink for each file using the per-file dashboard URL from the CLI. This lets the user open the specific item in the workspace.
945
826
 
946
827
  ## On external platforms \u2014 upload natively AND link the dashboard
947
828
 
948
829
  Sharing media externally is a **two-step requirement** for every file:
949
830
 
950
831
  1. **Upload the raw bytes natively to that platform** so the recipient sees the media embedded inline in the message itself.
951
- 2. **Link the Replicas dashboard deep link** (\`?mode=media&media=<id>\` form) so the user can jump straight to the workspace.
832
+ 2. **Link the Replicas dashboard deep link** (\`?media=<id>\` form) so the user can jump straight to the file in the workspace.
952
833
 
953
834
  A link alone is not enough. A native upload alone is not enough. Do both.
954
835
 
955
- The one exception is GitHub, which has no public image upload API \u2014 there, the dashboard link is the only option (see below).
836
+ GitHub has no native upload API, so use an explicit Replicas public share for screenshots and recording previews. GitLab has a native upload API and should receive the original bytes.
956
837
 
957
838
  ### Slack \u2014 embed natively via the 3-step external file upload flow
958
839
 
@@ -967,7 +848,7 @@ LENGTH=$(stat -c%s "$FILE" 2>/dev/null || stat -f%z "$FILE")
967
848
 
968
849
  # 1. Reserve an upload URL + file ID.
969
850
  RESERVE=$(curl -s -G "https://slack.com/api/files.getUploadURLExternal" \\
970
- -H "Authorization: Bearer $SLACK_BOT_TOKEN" \\
851
+ -H "Authorization: Bearer \${REPLICAS_SLACK_BOT_TOKEN:-$SLACK_BOT_TOKEN}" \\
971
852
  --data-urlencode "filename=$FILENAME" \\
972
853
  --data-urlencode "length=$LENGTH")
973
854
  UPLOAD_URL=$(echo "$RESERVE" | jq -r '.upload_url')
@@ -982,13 +863,13 @@ curl -s -X POST "$UPLOAD_URL" \\
982
863
 
983
864
  # 3. Complete the upload, sharing it into the channel/thread with the dashboard link.
984
865
  curl -s -X POST "https://slack.com/api/files.completeUploadExternal" \\
985
- -H "Authorization: Bearer $SLACK_BOT_TOKEN" \\
866
+ -H "Authorization: Bearer \${REPLICAS_SLACK_BOT_TOKEN:-$SLACK_BOT_TOKEN}" \\
986
867
  -H "Content-Type: application/json; charset=utf-8" \\
987
868
  -d "{
988
869
  \\"files\\": [{\\"id\\": \\"$FILE_ID\\", \\"title\\": \\"Screenshot\\"}],
989
870
  \\"channel_id\\": \\"CHANNEL_ID\\",
990
871
  \\"thread_ts\\": \\"THREAD_TS\\",
991
- \\"initial_comment\\": \\"<https://tryreplicas.com/workspaces/<workspace-id>?mode=media&media=<media-id>|View in Replicas>\\"
872
+ \\"initial_comment\\": \\"<https://tryreplicas.com/workspaces/<workspace-id>?media=<media-id>|View in Replicas>\\"
992
873
  }"
993
874
  \`\`\`
994
875
 
@@ -1016,9 +897,9 @@ ISSUE_UUID=... # the Linear issue's UUID
1016
897
  MEDIA_ID=... # the media ID printed by \`replicas media upload\`
1017
898
 
1018
899
  linear_graphql() {
1019
- curl -sS -X POST "$MONOLITH_URL/v1/engine/linear/graphql" \\
900
+ curl -sS -X POST "$REPLICAS_MONOLITH_URL/v1/engine/linear/graphql" \\
1020
901
  -H "Authorization: Bearer $REPLICAS_ENGINE_SECRET" \\
1021
- -H "X-Workspace-Id: $WORKSPACE_ID" \\
902
+ -H "X-Workspace-Id: $REPLICAS_WORKSPACE_ID" \\
1022
903
  -H "Content-Type: application/json" \\
1023
904
  --data-binary @-
1024
905
  }
@@ -1048,8 +929,8 @@ curl -s -X PUT "$UPLOAD_URL" \\
1048
929
  --data-binary @"$FILE"
1049
930
 
1050
931
  # 3. Reference assetUrl inline in the comment body, alongside the dashboard link.
1051
- BODY=$(printf '![screenshot](%s)\\n\\n[View in Replicas](https://tryreplicas.com/workspaces/%s?mode=media&media=%s)' \\
1052
- "$ASSET_URL" "$WORKSPACE_ID" "$MEDIA_ID")
932
+ BODY=$(printf '![screenshot](%s)\\n\\n[View in Replicas](https://tryreplicas.com/workspaces/%s?media=%s)' \\
933
+ "$ASSET_URL" "$REPLICAS_WORKSPACE_ID" "$MEDIA_ID")
1053
934
 
1054
935
  jq -n \\
1055
936
  --arg issueId "$ISSUE_UUID" \\
@@ -1064,20 +945,36 @@ The same flow works for issue bodies (use \`issueCreate\` / \`issueUpdate\` with
1064
945
 
1065
946
  Never paste the \`api.tryreplicas.com/v1/media/<id>\` URL into a Linear body or comment \u2014 it will render as a broken image to anyone outside your Replicas chat.
1066
947
 
1067
- ### GitHub \u2014 link the dashboard deep link (no public upload API exists)
948
+ ### GitHub: inline screenshots and recording previews
949
+
950
+ GitHub has **no public API** for uploading attachments to PRs or issues. Its web UI uses an undocumented internal endpoint that API clients must not emulate. For screenshots, explicitly create a forge share and put the printed \`Forge embed\` Markdown directly in the PR body or comment, followed by the dashboard link:
951
+
952
+ \`\`\`bash
953
+ replicas media upload /tmp/screenshot.png --name "PR screenshot" --share
954
+ \`\`\`
955
+
956
+ GitHub does not reliably render arbitrary external videos as inline players. For a recording:
1068
957
 
1069
- GitHub has **no public API** for uploading images to PRs or issues. Their web UI uses an undocumented internal endpoint that's not available to API clients. So for GitHub specifically, the only correct way to share media is the dashboard deep link, rendered as a markdown hyperlink:
958
+ 1. Upload the MP4 normally so it remains available in Replicas.
959
+ 2. Generate a poster image from the recording: \`ffmpeg -i /tmp/demo.mp4 -frames:v 1 /tmp/demo-poster.png\`.
960
+ 3. Upload the poster with \`--share\`.
961
+ 4. Wrap the poster embed in a link to the recording's **View in Replicas** URL.
1070
962
 
1071
963
  \`\`\`markdown
1072
- [View in Replicas](https://tryreplicas.com/workspaces/<workspace-id>?mode=media&media=<media-id>)
964
+ [![Demo recording](<public-poster-url>)](<recording-dashboard-url>)
1073
965
  \`\`\`
1074
966
 
1075
- Do **not**:
1076
- - Commit screenshots as files to the repo as a workaround.
1077
- - Use placeholder image URLs.
1078
- - Embed the \`api.tryreplicas.com/v1/media/<id>\` URL in markdown \u2014 it renders as a broken image for every viewer.
967
+ Public forge shares are bearer capabilities. For private repositories, create one only when the user asked for inline PR media. Revoke with \`replicas media revoke <media-id>\` **only** when the user asks or the media should intentionally stop rendering \u2014 never as routine post-task cleanup, since revoking breaks every embed already posted on the PR.
1079
968
 
1080
- This applies to PR descriptions, PR review comments, issue bodies, issue comments, and commit messages.
969
+ For a self-contained HTML report, upload it with \`--access organization\` and put the printed \`Forge link\` Markdown in the PR body or comment. GitHub cannot render the page inline, so use the normal link rather than image syntax. HTML pages cannot be shared publicly.
970
+
971
+ Do **not** commit screenshots to the repo, use placeholder URLs, expose the authenticated \`/v1/media/<id>\` URL, or automate GitHub's undocumented browser upload flow.
972
+
973
+ ### GitLab: native inline screenshots and videos
974
+
975
+ GitLab supports native project Markdown uploads. Always upload the media to Replicas first, then POST the same original file to the repository's \`/projects/:id/uploads\` API using the workspace GitLab credential. Insert the returned \`markdown\` value into the merge request body or comment and include the Replicas dashboard link. Image uploads render inline; MP4, MOV, and WebM uploads render as inline video players.
976
+
977
+ Do not use a Replicas public forge share for GitLab when the native project upload succeeds; native uploads preserve project access controls.
1081
978
 
1082
979
  ## Recording defaults
1083
980
 
@@ -1105,23 +1002,27 @@ Auto-detected from the filename extension:
1105
1002
  | \`png\`, \`jpg\`, \`jpeg\`, \`webp\` | image |
1106
1003
  | \`mp4\`, \`webm\` | video |
1107
1004
  | \`mp3\`, \`wav\` | audio |
1005
+ | \`html\`, \`htm\` | html |
1108
1006
 
1109
- For other extensions, pass \`--kind image|video|audio\` explicitly. The kind applies to every file in that invocation, so group files of the same kind together:
1007
+ For other extensions, pass \`--kind image|video|audio|html\` explicitly. The kind applies to every file in that invocation, so group files of the same kind together:
1110
1008
 
1111
1009
  \`\`\`bash
1112
- replicas media upload diagram.svg --kind image
1113
- replicas media upload chart-a.svg chart-b.svg --kind image
1010
+ replicas media upload diagram.svg --name "Architecture diagram" --kind image
1011
+ replicas media upload chart-a.svg chart-b.svg --name "Latency chart" --name "Error rate chart" --kind image
1114
1012
  \`\`\`
1115
1013
 
1116
1014
  ## Options
1117
1015
 
1118
- - \`--kind <image|video|audio>\` \u2014 override auto-detection
1016
+ - \`--kind <image|video|audio|html>\` \u2014 override auto-detection
1017
+ - \`--name <name>\`: required display name shown in the dashboard, one per file in order
1119
1018
  - \`--session-id <id>\` \u2014 associate the upload with a specific session
1019
+ - \`--access organization|public\`: create an organization-only link, or a public bearer URL for non-HTML media
1020
+ - \`--share\`: alias for \`--access public\`; not supported for HTML
1120
1021
  `;
1121
1022
  var MEDIA_ABILITY = {
1122
1023
  label: "Media",
1123
- description: "Share screenshots, recordings, generated images, and audio clips.",
1124
- bullet: "- Producing or showing the user any media \u2014 screenshots, screen recordings, generated images or diagrams, audio clips \u2014 including in your Replicas chat reply, PR descriptions/comments, and other external platforms",
1024
+ description: "Share screenshots, recordings, generated images, audio clips, and HTML pages.",
1025
+ bullet: "- Producing or showing the user any media \u2014 screenshots, screen recordings, generated images or diagrams, audio clips, or HTML recap pages \u2014 including in your Replicas chat reply, PR descriptions/comments, and other external platforms",
1125
1026
  section: SECTION7,
1126
1027
  referenceFile: { name: "MEDIA.md", content: REFERENCE7 }
1127
1028
  };
@@ -1142,24 +1043,18 @@ When you run services on ports \u2014 such as a web app, API server, or database
1142
1043
 
1143
1044
  ## Running Services for Preview
1144
1045
 
1145
- Services must run as detached background processes so they survive after your command session ends. Do not leave them attached to a foreground terminal.
1046
+ Always start services with \`replicas service start\` (see \`REPLICAS.md\`, "Long-running services"). It daemonizes the process so it survives your turn ending and workspace sleep/wake \u2014 a service left attached to your shell or backgrounded with \`&\`/\`nohup\` dies when the workspace sleeps, leaving the preview broken for the user.
1146
1047
 
1147
- Some potential methods:
1148
1048
  \`\`\`bash
1149
- # Start a detached service with logging
1150
- setsid -f bash -lc 'cd /path/to/app && exec yarn dev >> /tmp/app.log 2>&1'
1151
-
1152
- # For daemons like Docker
1153
- nohup dockerd > /tmp/dockerd.log 2>&1 &
1049
+ replicas service start web "yarn dev" --cwd /path/to/app
1154
1050
  \`\`\`
1155
1051
 
1156
1052
  After starting a service:
1157
- 1. Verify the process is running: \`pgrep -af 'yarn dev'\`
1158
- 2. Check logs for readiness: \`tail -f /tmp/app.log\`
1159
- 3. Confirm it's actually serving: \`curl -s http://localhost:3000\` (or appropriate health check)
1160
- 4. Only create the preview after the service is healthy
1053
+ 1. Check logs for readiness: \`replicas service logs web\`
1054
+ 2. Confirm it's actually serving: \`curl -s http://localhost:3000\` (or appropriate health check)
1055
+ 3. Only create the preview after the service is healthy
1161
1056
 
1162
- If a prior detached process exists on the same port, stop it before restarting.
1057
+ \`replicas service start\` on the same name restarts the service, so you never need to hunt down stale processes on the port.
1163
1058
 
1164
1059
  ## Creating Previews
1165
1060
 
@@ -1222,26 +1117,100 @@ var PREVIEWS_ABILITY = {
1222
1117
  referenceFile: { name: "PREVIEWS.md", content: REFERENCE8 }
1223
1118
  };
1224
1119
 
1225
- // ../shared/src/default-skills/replicas-agent/abilities/replicas.ts
1226
- var SECTION9 = `### Replicas (in-workspace CLI)
1227
- Take action *with* Replicas itself \u2014 manage automations, environments (variables, files), repos, and \`replicas.json\` config \u2014 using the pre-installed, pre-authenticated \`replicas\` CLI.
1120
+ // ../shared/src/default-skills/replicas-agent/abilities/plugins.ts
1121
+ var SECTION9 = `### Plugins
1122
+ Discover and execute installed marketplace tools through the workspace-scoped TypeScript SDK.
1228
1123
 
1229
- **Reference:** \`references/REPLICAS.md\`
1124
+ **Reference:** \`references/PLUGINS.md\`
1230
1125
 
1231
1126
  Use this when:
1232
- - The user asks you to create, edit, run, or delete an automation
1233
- - The user asks you to manage environments, environment variables, or environment files
1234
- - The user asks "what envs / repos / automations do I have?"
1235
- - The user asks you to scaffold a \`replicas.json\` / \`replicas.yaml\` in a repo`;
1236
- var REFERENCE9 = `# Replicas (in-workspace CLI)
1127
+ - The user asks for CRM data from Attio
1128
+ - The user asks to manage Calendly events, invitees, or scheduling links
1129
+ - The user asks to read or change Google Ads, Docs, Drive, Search Console, or Sheets data
1130
+ - The user asks for ClickHouse analytics or PostHog product data
1131
+ - The user asks for Stripe customers, payments, invoices, subscriptions, or balances`;
1132
+ var REFERENCE9 = `# Plugins
1237
1133
 
1238
- This guide covers how to take action *with* Replicas itself from inside a Replicas workspace \u2014 managing automations, environments (and their variables/files), repos, previews, and the user's \`replicas.json\` config \u2014 using the pre-installed \`replicas\` CLI.
1134
+ Plugins are TypeScript libraries, not MCP servers. Import \`@replicas/sdk\`; Replicas authenticates the workspace and selects only the owner\u2019s personal connection or the organization fallback. Composio credentials, sessions, and project keys are never exposed.
1239
1135
 
1240
- When the user asks _about_ Replicas (concepts, pricing, how a feature works), check https://docs.replicas.dev first. When the user asks you to _do_ something in their Replicas org, this is the file to use.
1136
+ Plugin tools may read or write provider data according to the connected account's permissions. Only execute write or destructive tools when they match the user's request.
1241
1137
 
1242
- ## Prerequisites
1138
+ ## Discover installed plugins
1243
1139
 
1244
- The CLI is pre-installed in every workspace and pre-authenticated using the workspace's engine secret. You do **not** need to log in or set an API key. Verify:
1140
+ \`\`\`ts
1141
+ import { replicas } from '@replicas/sdk';
1142
+
1143
+ const plugins = await replicas.plugins.list();
1144
+ const installed = plugins.filter((plugin) => plugin.connected);
1145
+ \`\`\`
1146
+
1147
+ If a plugin is not connected, ask the user to install it under personal or organization Integrations in the Replicas dashboard.
1148
+
1149
+ ## Search, inspect, execute
1150
+
1151
+ \`\`\`ts
1152
+ const search = await replicas.plugins.search({
1153
+ plugin: 'stripe',
1154
+ query: 'list recent successful payments with customer details',
1155
+ });
1156
+
1157
+ const [tool] = search.tools;
1158
+ if (!tool) throw new Error('No matching tool found');
1159
+
1160
+ const { data } = await replicas.plugins.execute({
1161
+ plugin: 'stripe',
1162
+ tool: tool.slug,
1163
+ arguments: { limit: 25 },
1164
+ });
1165
+ \`\`\`
1166
+
1167
+ Use \`replicas.plugins.describe({ plugin, tools: [slug] })\` when search returned a partial schema or before constructing unfamiliar arguments.
1168
+
1169
+ ## Batch work
1170
+
1171
+ Keep dependent operations in one TypeScript file and batch independent calls:
1172
+
1173
+ \`\`\`ts
1174
+ const results = await Promise.all(customerIds.map((customer) =>
1175
+ replicas.plugins.execute({
1176
+ plugin: 'stripe',
1177
+ tool: 'STRIPE_RETRIEVE_CUSTOMER',
1178
+ arguments: { customer },
1179
+ }),
1180
+ ));
1181
+ \`\`\`
1182
+
1183
+ Never attempt to import Composio, create a session, manage a connection, or call a provider with raw credentials. Those operations are intentionally unavailable inside the workspace.
1184
+ `;
1185
+ var PLUGINS_ABILITY = {
1186
+ label: "Plugins",
1187
+ description: "Use installed marketplace tools through @replicas/sdk.",
1188
+ bullet: "- Interacting with installed marketplace plugins through TypeScript",
1189
+ section: SECTION9,
1190
+ referenceFile: { name: "PLUGINS.md", content: REFERENCE9 }
1191
+ };
1192
+
1193
+ // ../shared/src/default-skills/replicas-agent/abilities/replicas.ts
1194
+ var SECTION10 = `### Replicas (in-workspace CLI)
1195
+ Take action *with* Replicas itself \u2014 manage automations, environments (variables, files), repos, and \`replicas.json\` config \u2014 using the pre-installed, pre-authenticated \`replicas\` CLI.
1196
+
1197
+ **Reference:** \`references/REPLICAS.md\`
1198
+
1199
+ Use this when:
1200
+ - The user asks you to create, edit, run, or delete an automation
1201
+ - The user asks you to manage environments, environment variables, or environment files
1202
+ - The user asks "what envs / repos / automations do I have?"
1203
+ - The user asks you to scaffold a \`replicas.json\` / \`replicas.yaml\` in a repo
1204
+ - You need to run a long-lived service (dev server, daemon) \u2014 always use \`replicas service start\` so it survives workspace sleep/wake`;
1205
+ var REFERENCE10 = `# Replicas (in-workspace CLI)
1206
+
1207
+ This guide covers how to take action *with* Replicas itself from inside a Replicas workspace \u2014 managing automations, environments (and their variables/files), repos, previews, and the user's \`replicas.json\` config \u2014 using the pre-installed \`replicas\` CLI.
1208
+
1209
+ When the user asks _about_ Replicas (concepts, pricing, how a feature works), check https://docs.replicas.dev first. When the user asks you to _do_ something in their Replicas org, this is the file to use.
1210
+
1211
+ ## Prerequisites
1212
+
1213
+ The CLI is pre-installed in every workspace and pre-authenticated using the workspace's engine secret. You do **not** need to log in or set an API key. Verify:
1245
1214
 
1246
1215
  \`\`\`bash
1247
1216
  replicas whoami
@@ -1268,7 +1237,9 @@ In agent mode the CLI hides commands that don't make sense for in-workspace agen
1268
1237
  | \`replicas environment ...\` | Manage environments, env vars, env files |
1269
1238
  | \`replicas automation ...\` | Manage automations (cron + GitHub/GitLab event triggers) |
1270
1239
  | \`replicas preview ...\` | Register / list preview URLs (covered in \`PREVIEWS.md\`) |
1240
+ | \`replicas service ...\` | Run long-lived services detached so they survive workspace sleep/wake (see below) |
1271
1241
  | \`replicas media ...\` | Upload screenshots, videos, audio (covered in \`MEDIA.md\`) |
1242
+ | \`replicas slack thread ...\` | Attach or switch Slack thread routing (covered in \`SLACK.md\`) |
1272
1243
 
1273
1244
  \`replicas <command> --help\` is always the source of truth for flags.
1274
1245
 
@@ -1285,6 +1256,7 @@ In agent mode the CLI hides commands that don't make sense for in-workspace agen
1285
1256
  | "Make an automation that runs every weekday at 9am" | \`replicas automation create\` (interactive) or \`--trigger-cron "0 9 * * 1-5"\` |
1286
1257
  | "Make an automation that runs when a PR opens on \`acme/web\`" | \`replicas automation create ... --trigger-github pull_request.opened --trigger-github-repos acme/web\` |
1287
1258
  | "Make an automation that runs when an MR opens on \`acme/web\`" | \`replicas automation create ... --trigger-gitlab merge_request.opened --trigger-gitlab-repos acme/web\` |
1259
+ | "Ignore Dependabot for this PR automation" | \`replicas automation edit <id> --trigger-github-exclude-users 'dependabot[bot]'\` |
1288
1260
  | "What repos are connected?" | \`replicas repos\` |
1289
1261
  | "Set up a \`replicas.json\` in this repo" | \`replicas init\` (\`-y\` for YAML) |
1290
1262
 
@@ -1371,21 +1343,23 @@ replicas automation create "Review my PRs" \\
1371
1343
  --prompt "Leave a code review on this PR" \\
1372
1344
  --environment <env-name-or-id> \\
1373
1345
  --trigger-github pull_request.opened \\
1374
- --trigger-github-repos acme/web,acme/api
1346
+ --trigger-github-repos acme/web,acme/api \\
1347
+ --trigger-github-exclude-users 'dependabot[bot],renovate[bot]'
1375
1348
 
1376
1349
  replicas automation create "Review my MRs" \\
1377
1350
  --prompt "Leave a code review on this MR" \\
1378
1351
  --environment <env-name-or-id> \\
1379
1352
  --trigger-gitlab merge_request.opened \\
1380
- --trigger-gitlab-repos acme/web,acme/api
1353
+ --trigger-gitlab-repos acme/web,acme/api \\
1354
+ --trigger-gitlab-exclude-users 'bot-user'
1381
1355
 
1382
1356
  # Disable on creation
1383
1357
  replicas automation create ... --disabled
1384
1358
 
1385
1359
  # Workspace lifecycle
1386
- replicas automation create ... --lifecycle delete_when_done
1360
+ replicas automation create ... --lifecycle archive_when_done
1387
1361
  replicas automation create ... --lifecycle sleep_when_done
1388
- replicas automation create ... --lifecycle delete_after_inactivity --auto-stop-minutes 30
1362
+ replicas automation create ... --lifecycle default --auto-stop-minutes 30
1389
1363
  \`\`\`
1390
1364
 
1391
1365
  When the user says "make me an automation that...", default to **picking the environment for them by listing the available envs** and asking only when the user hasn't already pinned one. Same for repos in GitHub triggers. Don't pepper the user with questions you can answer by reading the existing config.
@@ -1401,8 +1375,28 @@ replicas automation edit <id> \\
1401
1375
  --trigger-cron "0 4 * * *" # replaces existing triggers
1402
1376
  \`\`\`
1403
1377
 
1378
+ \`replicas automation edit <id> --trigger-github-exclude-users "dependabot[bot],renovate[bot]"\` updates existing GitHub triggers without replacing the rest of the trigger list. The GitLab equivalent is \`--trigger-gitlab-exclude-users\`. Passing an empty string (for example, \`--trigger-github-exclude-users ""\`) clears existing exclusions for that code host. Passing \`--trigger-github\` or \`--trigger-gitlab\` still replaces triggers, and the matching exclude flag is applied to the replacement trigger.
1379
+
1404
1380
  \`replicas automation edit <id>\` with no flags drops into interactive mode.
1405
1381
 
1382
+ ## Long-running services
1383
+
1384
+ **Always use \`replicas service start\` to run anything that should keep running after your current command or turn ends** \u2014 dev servers, backend APIs, databases, daemons, watchers. Never leave them attached to your shell, and never rely on plain \`&\`, \`nohup\`, or your own backgrounding: processes started inside your session are torn down when your turn ends and the workspace goes to sleep, so the user finds them dead after waking the workspace. \`replicas service start\` daemonizes the process into its own session, which survives workspace sleep/wake.
1385
+
1386
+ \`\`\`bash
1387
+ replicas service start <name> "<command>" # start (or restart) a named service
1388
+ replicas service start web "bun run dev" --cwd ~/workspaces/app
1389
+ replicas service list # names, pids, running/stopped
1390
+ replicas service logs <name> [-n 100] # tail the service log
1391
+ replicas service stop <name> # stop the whole process group
1392
+ \`\`\`
1393
+
1394
+ Notes:
1395
+ - Quote the command if it contains shell operators: \`replicas service start web "cd app && bun dev"\`.
1396
+ - \`start\` on an existing name restarts it (the old process group is stopped first). It fails fast and prints the log tail if the service dies within the first second.
1397
+ - Logs stream to \`~/.replicas/services/<name>.log\`.
1398
+ - After starting, verify the service is actually healthy (check \`logs\`, then \`curl\` its port) before telling the user it's up or creating a preview for it.
1399
+
1406
1400
  ## Repositories
1407
1401
 
1408
1402
  Read-only listing of repos connected to the org. Use when the user asks "what repos can I use?", or to validate a \`--repository\` value before passing it to \`environment create\` / \`automation create\`:
@@ -1435,125 +1429,128 @@ var REPLICAS_ABILITY = {
1435
1429
  description: "Teach the agent about Replicas itself \u2014 automations, environments, the in-workspace CLI.",
1436
1430
  // No bullet — help_instructions covers the `replicas` CLI surface in detail.
1437
1431
  bullet: "",
1438
- section: SECTION9,
1439
- referenceFile: { name: "REPLICAS.md", content: REFERENCE9 },
1432
+ section: SECTION10,
1433
+ referenceFile: { name: "REPLICAS.md", content: REFERENCE10 },
1440
1434
  locked: true
1441
1435
  };
1442
1436
 
1443
- // ../shared/src/default-skills/replicas-agent/abilities/slack.ts
1444
- var SECTION10 = `### Slack
1445
- Send messages, read threads, search conversations, and upload files via the Slack Web API.
1437
+ // ../shared/src/default-skills/replicas-agent/abilities/sentry.ts
1438
+ var SECTION11 = `### Sentry
1439
+ Read issues and project data through the workspace-scoped TypeScript SDK.
1446
1440
 
1447
- **Reference:** \`references/SLACK.md\`
1441
+ **Reference:** \`references/SENTRY.md\`
1448
1442
 
1449
1443
  Use this when:
1450
- - You need to send a message to a Slack channel or thread
1451
- - You need to read or fetch a Slack conversation
1452
- - You encounter a Slack message link and need to retrieve its content
1453
- - The task asks you to notify, update, or communicate via Slack`;
1454
- var REFERENCE10 = `# Slack Integration
1444
+ - You need to inspect Sentry projects or issues
1445
+ - A user asks for error-monitoring context outside a webhook-triggered task`;
1446
+ var REFERENCE11 = `# Sentry Integration
1455
1447
 
1456
- This guide covers how to interact with Slack from within your Replicas workspace.
1448
+ Use \`@replicas/sdk\`; never request or read a Sentry token. Sentry access is organization-scoped and read-only.
1457
1449
 
1458
- ## Prerequisites
1450
+ \`\`\`ts
1451
+ import { replicas } from '@replicas/sdk';
1459
1452
 
1460
- Check if the \`SLACK_BOT_TOKEN\` environment variable is set:
1453
+ const { native } = await replicas.integrations.list();
1454
+ if (!native.sentry) throw new Error('Connect Sentry in Replicas organization integrations first.');
1461
1455
 
1462
- \`\`\`bash
1463
- echo "\${SLACK_BOT_TOKEN:+set}"
1456
+ const issues = await replicas.sentry.request('/organizations/acme/issues/?query=is:unresolved');
1464
1457
  \`\`\`
1465
1458
 
1466
- - If **set**: Your workspace has Slack access. You can use the Slack Web API as described below.
1467
- - If **not set**: Slack has not been configured for this workspace. The user needs to connect Slack in the [Replicas dashboard](https://replicas.dev) under their organization's integration settings. Let the user know and do not attempt Slack operations.
1468
-
1469
- ## Using the Slack API
1459
+ Pass only a relative Sentry API path. The Replicas gateway selects the connected organization credential and rejects writes, absolute URLs, oversized responses, and long-running requests.
1460
+ `;
1461
+ var SENTRY_ABILITY = {
1462
+ label: "Sentry",
1463
+ description: "Read Sentry projects and issues through @replicas/sdk.",
1464
+ bullet: "- Interacting with Sentry (reading projects, issues, and error context)",
1465
+ section: SECTION11,
1466
+ referenceFile: { name: "SENTRY.md", content: REFERENCE11 }
1467
+ };
1470
1468
 
1471
- All requests use the \`$SLACK_BOT_TOKEN\` for authentication via the Slack Web API.
1469
+ // ../shared/src/default-skills/replicas-agent/abilities/slack.ts
1470
+ var SECTION12 = `### Slack
1471
+ Send messages, read threads, search conversations, and upload files through the workspace-scoped TypeScript SDK.
1472
1472
 
1473
- ### Fetching a Thread from a Slack Link
1473
+ **Reference:** \`references/SLACK.md\`
1474
1474
 
1475
- If you encounter a Slack message link (e.g. \`https://team.slack.com/archives/C0123ABC/p1234567890123456\`), extract the channel ID and thread timestamp:
1475
+ Use this when:
1476
+ - You need to send a message to a Slack channel or thread
1477
+ - You need to read or search Slack conversations
1478
+ - You encounter a Slack message link and need its full thread`;
1479
+ var REFERENCE12 = `# Slack Integration
1476
1480
 
1477
- - **Channel ID**: The segment after \`/archives/\` (e.g. \`C0123ABC\`)
1478
- - **Thread TS**: The \`p\` value with a dot inserted before the last 6 digits (e.g. \`p1234567890123456\` -> \`1234567890.123456\`)
1481
+ Use \`@replicas/sdk\`; never read or send a Slack token directly. The SDK calls Replicas, which selects only this workspace's organization connection.
1479
1482
 
1480
- \`\`\`bash
1481
- curl -s "https://slack.com/api/conversations.replies?channel=CHANNEL_ID&ts=THREAD_TS" \\
1482
- -H "Authorization: Bearer $SLACK_BOT_TOKEN"
1483
- \`\`\`
1483
+ ## Check access
1484
1484
 
1485
- ### Sending a Message
1485
+ \`\`\`ts
1486
+ import { replicas } from '@replicas/sdk';
1486
1487
 
1487
- \`\`\`bash
1488
- curl -s -X POST "https://slack.com/api/chat.postMessage" \\
1489
- -H "Authorization: Bearer $SLACK_BOT_TOKEN" \\
1490
- -H "Content-Type: application/json" \\
1491
- -d '{
1492
- "channel": "CHANNEL_ID",
1493
- "text": "Your message here",
1494
- "thread_ts": "OPTIONAL_THREAD_TS"
1495
- }'
1488
+ const { native } = await replicas.integrations.list();
1489
+ if (!native.slack) throw new Error('Connect Slack in Replicas organization integrations first.');
1496
1490
  \`\`\`
1497
1491
 
1498
- Omit \`thread_ts\` to post a new message to the channel. Include it to reply in a thread.
1492
+ ## Read a thread
1499
1493
 
1500
- ### Searching Messages
1494
+ For a link such as \`https://team.slack.com/archives/C0123ABC/p1234567890123456\`, the channel is \`C0123ABC\` and the timestamp is \`1234567890.123456\`.
1501
1495
 
1502
- \`\`\`bash
1503
- curl -s "https://slack.com/api/search.messages?query=YOUR_SEARCH_QUERY" \\
1504
- -H "Authorization: Bearer $SLACK_BOT_TOKEN"
1496
+ \`\`\`ts
1497
+ const thread = await replicas.slack.call('conversations.replies', {
1498
+ channel: 'C0123ABC',
1499
+ ts: '1234567890.123456',
1500
+ limit: 200,
1501
+ });
1505
1502
  \`\`\`
1506
1503
 
1507
- ### Uploading Files
1504
+ ## Send and attach a message
1508
1505
 
1509
- Slack deprecated \`files.upload\`; the raw HTTP replacement is a 3-step flow. (\`files.uploadV2\` is **only** an SDK convenience wrapper and is NOT a real Web API HTTP endpoint \u2014 \`POST https://slack.com/api/files.uploadV2\` returns \`unknown_method\`.)
1510
-
1511
- \`\`\`bash
1512
- FILE=/path/to/file
1513
- FILENAME=$(basename "$FILE")
1514
- LENGTH=$(stat -c%s "$FILE" 2>/dev/null || stat -f%z "$FILE")
1506
+ \`\`\`ts
1507
+ const message = await replicas.slack.call<{ channel: string; ts: string }>('chat.postMessage', {
1508
+ channel: 'C0123ABC',
1509
+ text: 'Update from Replicas',
1510
+ });
1511
+ await replicas.slack.attachThread({ channel: message.channel, threadTs: message.ts });
1512
+ \`\`\`
1515
1513
 
1516
- # 1. Reserve an upload URL + file ID.
1517
- RESERVE=$(curl -s -G "https://slack.com/api/files.getUploadURLExternal" \\
1518
- -H "Authorization: Bearer $SLACK_BOT_TOKEN" \\
1519
- --data-urlencode "filename=$FILENAME" \\
1520
- --data-urlencode "length=$LENGTH")
1521
- UPLOAD_URL=$(echo "$RESERVE" | jq -r '.upload_url')
1522
- FILE_ID=$(echo "$RESERVE" | jq -r '.file_id')
1514
+ A link in the message text stays plain text, so when the message carries a pull request or merge request link, pass \`blocks\` alongside \`text\` to render a button for it. Keep \`text\` set to the same message \u2014 Slack uses it for notifications and fallback.
1523
1515
 
1524
- # 2. POST the raw bytes to the reserved URL.
1525
- # Do not use -F / multipart here \u2014 the presigned URL expects the bytes
1526
- # directly in the request body.
1527
- curl -s -X POST "$UPLOAD_URL" \\
1528
- -H "Content-Type: application/octet-stream" \\
1529
- --data-binary @"$FILE"
1530
-
1531
- # 3. Complete the upload and share into a channel (and optionally a thread).
1532
- curl -s -X POST "https://slack.com/api/files.completeUploadExternal" \\
1533
- -H "Authorization: Bearer $SLACK_BOT_TOKEN" \\
1534
- -H "Content-Type: application/json; charset=utf-8" \\
1535
- -d "{
1536
- \\"files\\": [{\\"id\\": \\"$FILE_ID\\", \\"title\\": \\"File title\\"}],
1537
- \\"channel_id\\": \\"CHANNEL_ID\\",
1538
- \\"thread_ts\\": \\"OPTIONAL_THREAD_TS\\",
1539
- \\"initial_comment\\": \\"Optional message body\\"
1540
- }"
1516
+ \`\`\`ts
1517
+ await replicas.slack.call('chat.postMessage', {
1518
+ channel: 'C0123ABC',
1519
+ thread_ts: '1234567890.123456',
1520
+ text: summary,
1521
+ blocks: [
1522
+ { type: 'section', text: { type: 'mrkdwn', text: summary } },
1523
+ {
1524
+ type: 'actions',
1525
+ elements: [
1526
+ { type: 'button', text: { type: 'plain_text', text: 'View PR' }, url: prUrl, action_id: 'view_pr' },
1527
+ ],
1528
+ },
1529
+ ],
1530
+ });
1541
1531
  \`\`\`
1542
1532
 
1543
- Omit \`thread_ts\` and/or \`initial_comment\` if you don't need them.
1533
+ Use "View MR" for GitLab. With several links, give each button a unique \`action_id\` and name its repository in the label ("View PR \xB7 web").
1534
+
1535
+ Attach every new top-level conversation so future replies route to this workspace. For a reply in another thread, attach its root \`thread_ts\`, not the reply timestamp. Do not attach the originating thread again.
1544
1536
 
1545
- ### Other Operations
1537
+ ## Search and batch
1546
1538
 
1547
- You can list channels, read channel history, add reactions, and perform any other operation supported by the Slack Web API using the same authentication pattern.
1539
+ \`\`\`ts
1540
+ const [messages, users] = await Promise.all([
1541
+ replicas.slack.call('search.messages', { query: 'launch status' }),
1542
+ replicas.slack.call('users.list', { limit: 200 }),
1543
+ ]);
1544
+ \`\`\`
1548
1545
 
1549
- For full API documentation, see: https://docs.slack.dev/apis/web-api/
1546
+ Supported methods cover conversations, messages, search, users, reactions, and Slack's external file-upload flow. An unsupported method is rejected by Replicas before Slack is called.
1550
1547
  `;
1551
1548
  var SLACK_ABILITY = {
1552
1549
  label: "Slack",
1553
- description: "Send messages, read threads, search conversations, upload files.",
1550
+ description: "Send messages, read threads, and search through @replicas/sdk.",
1554
1551
  bullet: "- Interacting with Slack (sending messages, reading threads, etc.)",
1555
- section: SECTION10,
1556
- referenceFile: { name: "SLACK.md", content: REFERENCE10 }
1552
+ section: SECTION12,
1553
+ referenceFile: { name: "SLACK.md", content: REFERENCE12 }
1557
1554
  };
1558
1555
 
1559
1556
  // ../shared/src/default-skills/replicas-agent/registry.ts
@@ -1563,10 +1560,12 @@ var REPLICAS_AGENT_ABILITY_REGISTRY = {
1563
1560
  docker: DOCKER_ABILITY,
1564
1561
  github: GITHUB_ABILITY,
1565
1562
  gitlab: GITLAB_ABILITY,
1566
- google: GOOGLE_ABILITY,
1563
+ learnings: LEARNINGS_ABILITY,
1567
1564
  linear: LINEAR_ABILITY,
1568
1565
  media: MEDIA_ABILITY,
1566
+ plugins: PLUGINS_ABILITY,
1569
1567
  previews: PREVIEWS_ABILITY,
1568
+ sentry: SENTRY_ABILITY,
1570
1569
  slack: SLACK_ABILITY
1571
1570
  };
1572
1571
  var REPLICAS_AGENT_ABILITIES = Object.keys(
@@ -1574,36 +1573,306 @@ var REPLICAS_AGENT_ABILITIES = Object.keys(
1574
1573
  );
1575
1574
 
1576
1575
  // ../shared/src/routes/preferences.ts
1577
- var DEFAULT_REPLICAS_AGENT_ABILITIES = Object.fromEntries(
1578
- REPLICAS_AGENT_ABILITIES.map((key) => [key, true])
1579
- );
1576
+ var EMPTY_AGENT_DEFAULT_SETTINGS = {
1577
+ agent_provider: null,
1578
+ model: null,
1579
+ thinking_level: null,
1580
+ goal_mode: null,
1581
+ fast_mode: null
1582
+ };
1583
+ var DEFAULT_RELAY_SUBAGENT_PROVIDERS = [...VALID_RELAY_SUBAGENT_PROVIDERS];
1584
+ var DEFAULT_USER_PREFERENCES = {
1585
+ default_agent: "codex",
1586
+ default_model: null,
1587
+ claude_models: null,
1588
+ codex_models: null,
1589
+ cursor_models: null,
1590
+ openrouter_models: null,
1591
+ relay_models: null,
1592
+ close_workspace_on_pr_close: true,
1593
+ close_workspace_on_pr_merge: true,
1594
+ default_environment_id: null,
1595
+ respond_to_ci_workflows: true,
1596
+ respond_to_ci_checks: true,
1597
+ respond_to_human_pr_reviews: false,
1598
+ pr_user_attribution: false,
1599
+ auto_draft_prs: false,
1600
+ open_prs_in_graphite: false,
1601
+ open_links_in_desktop_app: false,
1602
+ default_fast_mode: false,
1603
+ agent_defaults: { ...EMPTY_AGENT_DEFAULT_SETTINGS },
1604
+ credential_priority: null
1605
+ };
1606
+ function defaultReplicasAgentAbilities() {
1607
+ const abilities = {};
1608
+ for (const key of REPLICAS_AGENT_ABILITIES) {
1609
+ abilities[key] = true;
1610
+ }
1611
+ if (hasAllReplicasAgentAbilities(abilities)) return abilities;
1612
+ throw new Error("Default Replicas agent ability flags are incomplete");
1613
+ }
1614
+ function hasAllReplicasAgentAbilities(abilities) {
1615
+ return REPLICAS_AGENT_ABILITIES.every((key) => typeof abilities[key] === "boolean");
1616
+ }
1580
1617
  var DEFAULT_DEFAULT_SKILLS = {
1581
1618
  replicas_agent: {
1582
1619
  enabled: true,
1583
- abilities: { ...DEFAULT_REPLICAS_AGENT_ABILITIES }
1620
+ abilities: defaultReplicasAgentAbilities()
1584
1621
  }
1585
1622
  };
1586
1623
 
1587
- // ../shared/src/event.ts
1588
- var CLAUDE_PARTIAL_MESSAGE_EVENT_TYPE = "claude-partial-message";
1589
- function coerceClaudePartialMessagePayload(payload) {
1590
- const streamId = payload.streamId;
1591
- if (typeof streamId !== "string" || !streamId) return null;
1592
- const parentToolUseId = payload.parent_tool_use_id;
1593
- const text = typeof payload.text === "string" && payload.text ? payload.text : void 0;
1594
- const thinking = typeof payload.thinking === "string" && payload.thinking ? payload.thinking : void 0;
1595
- if (!text && !thinking) return null;
1624
+ // ../shared/src/aster.ts
1625
+ var ASTER_MODELS = [
1626
+ "kimi-k3",
1627
+ "glm-5.2",
1628
+ "gpt-oss-120b-fast",
1629
+ "gpt-oss-120b"
1630
+ ];
1631
+ var DEFAULT_ASTER_MODEL = ASTER_MODELS[0];
1632
+ var ASTER_MODEL_LABELS = {
1633
+ "kimi-k3": "Kimi K3",
1634
+ "glm-5.2": "GLM 5.2",
1635
+ "gpt-oss-120b-fast": "GPT-OSS 120B Fast",
1636
+ "gpt-oss-120b": "GPT-OSS 120B"
1637
+ };
1638
+
1639
+ // ../shared/src/engine/types.ts
1640
+ var CLAUDE_OPUS_1M_MODEL = "opus[1m]";
1641
+ var LEGACY_CLAUDE_OPUS_1M_MODEL = "opus-1m";
1642
+ var CLAUDE_FABLE_5_MODEL = "claude-fable-5";
1643
+ var CLAUDE_OPUS_5_MODEL = "claude-opus-5";
1644
+ var CLAUDE_OPUS_4_8_MODEL = "claude-opus-4-8";
1645
+ var CLAUDE_SONNET_5_MODEL = "claude-sonnet-5";
1646
+ var CLAUDE_HAIKU_4_5_MODEL = "claude-haiku-4-5";
1647
+ var CLAUDE_FABLE_5_BEDROCK_MODEL = "anthropic.claude-fable-5";
1648
+ var CLAUDE_OPUS_5_BEDROCK_MODEL = "anthropic.claude-opus-5";
1649
+ var CLAUDE_OPUS_4_8_BEDROCK_MODEL = "us.anthropic.claude-opus-4-8";
1650
+ var CLAUDE_SONNET_5_BEDROCK_MODEL = "anthropic.claude-sonnet-5";
1651
+ var CLAUDE_HAIKU_4_5_BEDROCK_MODEL = "us.anthropic.claude-haiku-4-5-20251001-v1:0";
1652
+ var GPT_5_6_SOL_MODEL = "gpt-5.6-sol";
1653
+ var GPT_5_6_TERRA_MODEL = "gpt-5.6-terra";
1654
+ var GPT_5_6_LUNA_MODEL = "gpt-5.6-luna";
1655
+ var DEFAULT_CODEX_MODEL = GPT_5_6_SOL_MODEL;
1656
+ var DEFAULT_OPENCODE_MODEL = "z-ai/glm-5.2";
1657
+ var OPENROUTER_MODELS = [
1658
+ DEFAULT_OPENCODE_MODEL,
1659
+ "minimax/minimax-m3",
1660
+ "xiaomi/mimo-v2.5-pro",
1661
+ "moonshotai/kimi-k2.6"
1662
+ ];
1663
+ var CLAUDE_MODEL_CONTEXT_WINDOWS = {
1664
+ [CLAUDE_FABLE_5_MODEL]: 1e6,
1665
+ [CLAUDE_OPUS_5_MODEL]: 1e6,
1666
+ [CLAUDE_OPUS_4_8_MODEL]: 1e6,
1667
+ [CLAUDE_SONNET_5_MODEL]: 1e6,
1668
+ [CLAUDE_HAIKU_4_5_MODEL]: 2e5,
1669
+ [CLAUDE_FABLE_5_BEDROCK_MODEL]: 1e6,
1670
+ [CLAUDE_OPUS_5_BEDROCK_MODEL]: 1e6,
1671
+ [CLAUDE_OPUS_4_8_BEDROCK_MODEL]: 1e6,
1672
+ [CLAUDE_SONNET_5_BEDROCK_MODEL]: 1e6,
1673
+ [CLAUDE_HAIKU_4_5_BEDROCK_MODEL]: 2e5,
1674
+ "anthropic.claude-opus-4-8": 1e6,
1675
+ "us.anthropic.claude-sonnet-5": 1e6,
1676
+ "anthropic.claude-haiku-4-5": 2e5,
1677
+ "claude-haiku-4-5-20251001": 2e5,
1678
+ "claude-haiku-4-5@20251001": 2e5
1679
+ };
1680
+ var AGENT_MODELS = {
1681
+ claude: [
1682
+ CLAUDE_FABLE_5_MODEL,
1683
+ CLAUDE_OPUS_5_MODEL,
1684
+ CLAUDE_OPUS_4_8_MODEL,
1685
+ CLAUDE_SONNET_5_MODEL,
1686
+ CLAUDE_HAIKU_4_5_MODEL
1687
+ ],
1688
+ codex: [DEFAULT_CODEX_MODEL, GPT_5_6_TERRA_MODEL, GPT_5_6_LUNA_MODEL, "gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.2"],
1689
+ cursor: [
1690
+ CLAUDE_FABLE_5_MODEL,
1691
+ CLAUDE_OPUS_5_MODEL,
1692
+ CLAUDE_OPUS_4_8_MODEL,
1693
+ GPT_5_6_SOL_MODEL,
1694
+ GPT_5_6_TERRA_MODEL,
1695
+ GPT_5_6_LUNA_MODEL,
1696
+ "gpt-5.5",
1697
+ "composer-2.5",
1698
+ CLAUDE_SONNET_5_MODEL,
1699
+ "claude-opus-4-7",
1700
+ "gpt-5.4",
1701
+ "gpt-5.4-mini",
1702
+ "gpt-5.4-nano",
1703
+ "gpt-5.3-codex",
1704
+ "gpt-5.2",
1705
+ "gpt-5.2-codex",
1706
+ "gpt-5.1",
1707
+ "gpt-5.1-codex-max",
1708
+ "gpt-5.1-codex-mini",
1709
+ "gpt-5-mini",
1710
+ "gemini-3.1-pro",
1711
+ "gemini-3-flash",
1712
+ "grok-4.5",
1713
+ "grok-4.3",
1714
+ CLAUDE_HAIKU_4_5_MODEL,
1715
+ "kimi-k2.5"
1716
+ ],
1717
+ opencode: [...OPENROUTER_MODELS, ...ASTER_MODELS],
1718
+ pi: [...OPENROUTER_MODELS, ...ASTER_MODELS],
1719
+ relay: [CLAUDE_FABLE_5_MODEL, CLAUDE_OPUS_5_MODEL, CLAUDE_OPUS_4_8_MODEL, CLAUDE_SONNET_5_MODEL]
1720
+ };
1721
+ var MODEL_LABELS = {
1722
+ fable: "Fable 5",
1723
+ [CLAUDE_FABLE_5_MODEL]: "Fable 5",
1724
+ sonnet: "Sonnet 5",
1725
+ "claude-sonnet-5": "Sonnet 5",
1726
+ opus: "Opus 5",
1727
+ [CLAUDE_OPUS_5_MODEL]: "Opus 5",
1728
+ [CLAUDE_OPUS_1M_MODEL]: "Opus (1M)",
1729
+ [LEGACY_CLAUDE_OPUS_1M_MODEL]: "Opus (1M)",
1730
+ haiku: "Haiku 4.5",
1731
+ [GPT_5_6_SOL_MODEL]: "GPT-5.6 Sol",
1732
+ [GPT_5_6_TERRA_MODEL]: "GPT-5.6 Terra",
1733
+ [GPT_5_6_LUNA_MODEL]: "GPT-5.6 Luna",
1734
+ "gpt-5.5": "GPT-5.5",
1735
+ [DEFAULT_OPENCODE_MODEL]: "GLM 5.2 via OpenRouter",
1736
+ "minimax/minimax-m3": "MiniMax M3 via OpenRouter",
1737
+ "xiaomi/mimo-v2.5-pro": "MiMo V2.5 Pro via OpenRouter",
1738
+ "moonshotai/kimi-k2.6": "Kimi K2.6 via OpenRouter",
1739
+ ...ASTER_MODEL_LABELS,
1740
+ "gpt-5.4": "GPT-5.4",
1741
+ "gpt-5.4-mini": "GPT-5.4 Mini",
1742
+ "gpt-5.4-nano": "GPT-5.4 Nano",
1743
+ "gpt-5.3-codex": "GPT-5.3 Codex",
1744
+ "gpt-5.2": "GPT-5.2",
1745
+ "gpt-5.2-codex": "GPT-5.2 Codex",
1746
+ "gpt-5.1": "GPT-5.1",
1747
+ "gpt-5.1-codex-max": "GPT-5.1 Codex Max",
1748
+ "gpt-5.1-codex-mini": "GPT-5.1 Codex Mini",
1749
+ "gpt-5-mini": "GPT-5 Mini",
1750
+ "composer-2.5": "Composer 2.5",
1751
+ [CLAUDE_OPUS_4_8_MODEL]: "Opus 4.8",
1752
+ "claude-opus-4-7": "Opus 4.7",
1753
+ [CLAUDE_HAIKU_4_5_MODEL]: "Haiku 4.5",
1754
+ "gemini-3.1-pro": "Gemini 3.1 Pro",
1755
+ "gemini-3-flash": "Gemini 3 Flash",
1756
+ "grok-4.5": "Grok 4.5",
1757
+ "grok-4.3": "Grok 4.3",
1758
+ "kimi-k2.5": "Kimi K2.5"
1759
+ };
1760
+ var IMAGE_MEDIA_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
1761
+ var MAX_CANVAS_FILE_BYTES = 25 * 1024 * 1024;
1762
+ var MAX_CANVAS_PREVIEW_BYTES = 5 * 1024 * 1024;
1763
+
1764
+ // ../shared/src/engine/v1.ts
1765
+ import { z } from "zod";
1766
+ var createChatRequestSchema = z.object({
1767
+ id: z.string().uuid().optional(),
1768
+ createdAt: z.string().datetime().optional(),
1769
+ provider: z.enum(VALID_AGENT_PROVIDERS),
1770
+ title: z.string().min(1).optional(),
1771
+ parentChatId: z.string().uuid().optional(),
1772
+ clientRequestId: z.string().min(1).max(128).optional()
1773
+ });
1774
+ var sendChatMessageRequestSchema = z.object({
1775
+ messageId: z.string().optional(),
1776
+ submittedAt: z.string().optional(),
1777
+ message: z.string(),
1778
+ model: z.string().optional(),
1779
+ customInstructions: z.string().optional(),
1780
+ planMode: z.boolean().optional(),
1781
+ images: z.array(z.object({
1782
+ type: z.literal("image"),
1783
+ source: z.discriminatedUnion("type", [
1784
+ z.object({ type: z.literal("base64"), media_type: z.enum(IMAGE_MEDIA_TYPES), data: z.string() }),
1785
+ z.object({ type: z.literal("url"), url: z.string() })
1786
+ ])
1787
+ })).optional(),
1788
+ thinkingLevel: z.enum(VALID_THINKING_LEVELS).optional(),
1789
+ goalMode: z.boolean().optional(),
1790
+ fastMode: z.boolean().optional(),
1791
+ enableInteractiveTools: z.boolean().optional(),
1792
+ type: z.string().optional(),
1793
+ merge: z.boolean().optional(),
1794
+ idempotencyKey: z.string().optional(),
1795
+ senderUserId: z.string().optional(),
1796
+ senderEmail: z.string().optional(),
1797
+ senderDisplayName: z.string().optional(),
1798
+ senderAvatarUrl: z.string().optional(),
1799
+ errorNotificationTarget: z.discriminatedUnion("type", [
1800
+ z.object({ type: z.literal("slack") }),
1801
+ z.object({ type: z.literal("linear"), sessionId: z.string() }),
1802
+ z.object({
1803
+ type: z.literal("code_host"),
1804
+ provider: z.enum(["github", "gitlab"]),
1805
+ resource: z.enum(["issue", "pull_request"]),
1806
+ repositoryId: z.string(),
1807
+ resourceNumber: z.number()
1808
+ }),
1809
+ z.object({ type: z.literal("automation"), executionId: z.string() })
1810
+ ]).optional()
1811
+ }).passthrough();
1812
+
1813
+ // ../shared/src/display-message/types.ts
1814
+ var BACKGROUND_TASK_SUBTYPES = /* @__PURE__ */ new Set([
1815
+ "task_started",
1816
+ "task_progress",
1817
+ "task_updated",
1818
+ "task_notification"
1819
+ ]);
1820
+ function isBackgroundTaskSubtype(subtype) {
1821
+ return typeof subtype === "string" && BACKGROUND_TASK_SUBTYPES.has(subtype);
1822
+ }
1823
+ function optionalString(value) {
1824
+ return typeof value === "string" ? value : void 0;
1825
+ }
1826
+ function coerceBackgroundTaskPayload(payload) {
1827
+ if (!isRecord(payload)) return null;
1828
+ const subtype = payload.subtype;
1829
+ if (!isBackgroundTaskSubtype(subtype)) return null;
1830
+ const taskId = payload.task_id;
1831
+ if (typeof taskId !== "string" || !taskId) return null;
1832
+ const patch = isRecord(payload.patch) ? {
1833
+ status: optionalString(payload.patch.status),
1834
+ description: optionalString(payload.patch.description),
1835
+ error: optionalString(payload.patch.error),
1836
+ isBackgrounded: optionalBoolean(payload.patch.is_backgrounded)
1837
+ } : void 0;
1838
+ const usage = isRecord(payload.usage) ? {
1839
+ totalTokens: optionalNumber(payload.usage.total_tokens),
1840
+ toolUses: optionalNumber(payload.usage.tool_uses),
1841
+ durationMs: optionalNumber(payload.usage.duration_ms)
1842
+ } : void 0;
1596
1843
  return {
1597
- streamId,
1598
- parent_tool_use_id: typeof parentToolUseId === "string" ? parentToolUseId : null,
1599
- ...text ? { text } : {},
1600
- ...thinking ? { thinking } : {},
1601
- status: payload.status === "completed" ? "completed" : "in_progress"
1844
+ subtype,
1845
+ taskId,
1846
+ description: optionalString(payload.description),
1847
+ summary: optionalString(payload.summary),
1848
+ outputFile: optionalString(payload.output_file),
1849
+ status: optionalString(payload.status),
1850
+ taskType: optionalString(payload.task_type),
1851
+ workflowName: optionalString(payload.workflow_name),
1852
+ prompt: optionalString(payload.prompt),
1853
+ lastToolName: optionalString(payload.last_tool_name),
1854
+ usage,
1855
+ patch
1602
1856
  };
1603
1857
  }
1604
- var USER_MESSAGE_ID_PAYLOAD_KEY = "replicasMessageId";
1605
- var CODEX_ASP_ITEM_ID_PAYLOAD_KEY = "codexAspItemId";
1606
- var CODEX_QUOTA_STATUS_EVENT_TYPE = "codex-quota-status";
1858
+ function optionalNumber(value) {
1859
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1860
+ }
1861
+ function optionalBoolean(value) {
1862
+ return typeof value === "boolean" ? value : void 0;
1863
+ }
1864
+ function normalizeBackgroundTaskStatus(status) {
1865
+ if (status === "completed" || status === "failed" || status === "stopped") {
1866
+ return status;
1867
+ }
1868
+ if (status === "killed") {
1869
+ return "stopped";
1870
+ }
1871
+ return "in_progress";
1872
+ }
1873
+
1874
+ // ../shared/src/display-message/constants.ts
1875
+ var USER_MESSAGE_MATCH_GRACE_PERIOD_MS = 3e4;
1607
1876
 
1608
1877
  // ../shared/src/json.ts
1609
1878
  function safeJsonParse(str, fallback) {
@@ -1614,9 +1883,277 @@ function safeJsonParse(str, fallback) {
1614
1883
  }
1615
1884
  }
1616
1885
 
1617
- // ../shared/src/type-guards.ts
1618
- function isRecord(value) {
1619
- return typeof value === "object" && value !== null && !Array.isArray(value);
1886
+ // ../shared/src/display-message/parsers/utils.ts
1887
+ function userMessageImages(value) {
1888
+ if (!Array.isArray(value)) return void 0;
1889
+ const images = value.filter((item) => isRecord(item) && item.type === "image" && typeof item.mediaType === "string" && typeof item.data === "string");
1890
+ return images.length > 0 ? images : void 0;
1891
+ }
1892
+ function stringifyDisplayValue(value) {
1893
+ if (value === void 0 || value === null) return void 0;
1894
+ if (typeof value === "string") return value;
1895
+ try {
1896
+ return JSON.stringify(value, null, 2);
1897
+ } catch {
1898
+ return String(value);
1899
+ }
1900
+ }
1901
+ function upsertDisplayMessage(messages, message) {
1902
+ const index = messages.findIndex((candidate) => candidate.id === message.id);
1903
+ if (index === -1) messages.push(message);
1904
+ else messages[index] = message;
1905
+ }
1906
+ function parseSkillName(tool, input) {
1907
+ if (typeof tool !== "string") return null;
1908
+ const normalizedTool = tool.toLowerCase();
1909
+ if (normalizedTool.startsWith("skill.")) return tool.slice("skill.".length) || null;
1910
+ if (normalizedTool !== "skill") return null;
1911
+ const parsedInput = typeof input === "string" ? safeJsonParse(input, null) : input;
1912
+ if (!isRecord(parsedInput)) return null;
1913
+ const skillName = parsedInput.skill ?? parsedInput.name;
1914
+ return typeof skillName === "string" && skillName ? skillName : null;
1915
+ }
1916
+ function skillNameFromFilePath(path) {
1917
+ const skillName = skillNameFromSkillRootPath(path);
1918
+ if (skillName) return skillName;
1919
+ const parts = path.split(/[\\/]+/).filter(Boolean);
1920
+ if (parts.length < 2 || parts[parts.length - 1].toLowerCase() !== "skill.md") return null;
1921
+ const fallbackSkillName = parts[parts.length - 2];
1922
+ return fallbackSkillName && fallbackSkillName.toLowerCase() !== "skills" ? fallbackSkillName : null;
1923
+ }
1924
+ function skillNameFromSkillRootPath(path) {
1925
+ const parts = path.split(/[\\/]+/).filter(Boolean);
1926
+ const skillsIndex = parts.findLastIndex((part) => part.toLowerCase() === "skills");
1927
+ if (skillsIndex === -1) return null;
1928
+ const firstSkillPart = parts[skillsIndex + 1];
1929
+ const skillName = firstSkillPart?.toLowerCase() === ".system" ? parts[skillsIndex + 2] : firstSkillPart;
1930
+ return skillName && skillName.toLowerCase() !== "skill.md" ? skillName : null;
1931
+ }
1932
+ function parseSkillFilePath(input) {
1933
+ const parsedInput = typeof input === "string" ? safeJsonParse(input, null) : input;
1934
+ if (!isRecord(parsedInput)) return null;
1935
+ const path = [parsedInput.path, parsedInput.filePath, parsedInput.file_path].find((value) => typeof value === "string" && value.length > 0);
1936
+ if (!path) return null;
1937
+ return skillNameFromFilePath(path);
1938
+ }
1939
+ function skillNameFromToolCall(tool, input) {
1940
+ const directSkillName = parseSkillName(tool, input);
1941
+ if (directSkillName) return directSkillName;
1942
+ if (typeof tool !== "string" || !["read", "read_file", "readfile"].includes(tool.toLowerCase())) return null;
1943
+ return parseSkillFilePath(input);
1944
+ }
1945
+ function createCallDisplayMessage(message) {
1946
+ const skillName = message.server.toLowerCase() === "skills" ? message.tool : skillNameFromToolCall(message.tool, message.input);
1947
+ if (skillName === null) return { ...message, type: "tool_call" };
1948
+ return {
1949
+ id: message.id,
1950
+ type: "skill",
1951
+ skillName,
1952
+ status: message.status,
1953
+ timestamp: message.timestamp
1954
+ };
1955
+ }
1956
+
1957
+ // ../shared/src/display-message/format.ts
1958
+ function unquoteShellArg(value) {
1959
+ const trimmed = value.trim();
1960
+ if (trimmed.length < 2) return trimmed;
1961
+ if (trimmed[0] === "'" && trimmed[trimmed.length - 1] === "'") {
1962
+ return trimmed.slice(1, -1).replace(/'\\''/g, "'");
1963
+ }
1964
+ if (trimmed[0] === '"' && trimmed[trimmed.length - 1] === '"') {
1965
+ return trimmed.slice(1, -1).replace(/\\(["\\$`])/g, "$1");
1966
+ }
1967
+ return trimmed;
1968
+ }
1969
+ function shellPayload(command) {
1970
+ const match = command.trim().match(/^(?:\S*\/)?(?:ba|z|c|fi)?sh\s+-(?:[a-zA-Z]*c[a-zA-Z]*|c)\s+([\s\S]+)$/);
1971
+ return match ? unquoteShellArg(match[1]) : command.trim();
1972
+ }
1973
+ function splitShellWords(command) {
1974
+ const words = [];
1975
+ let word = "";
1976
+ let quote = null;
1977
+ let escaped = false;
1978
+ for (const char of command) {
1979
+ if (escaped) {
1980
+ word += char;
1981
+ escaped = false;
1982
+ continue;
1983
+ }
1984
+ if (char === "\\" && quote !== "'") {
1985
+ escaped = true;
1986
+ continue;
1987
+ }
1988
+ if (quote) {
1989
+ if (char === quote) quote = null;
1990
+ else word += char;
1991
+ continue;
1992
+ }
1993
+ if (char === '"' || char === "'") {
1994
+ quote = char;
1995
+ continue;
1996
+ }
1997
+ if (/\s/.test(char)) {
1998
+ if (word) {
1999
+ words.push(word);
2000
+ word = "";
2001
+ }
2002
+ continue;
2003
+ }
2004
+ word += char;
2005
+ }
2006
+ if (word) words.push(word);
2007
+ return words;
2008
+ }
2009
+ function commandWords(command) {
2010
+ const payload = shellPayload(command);
2011
+ if (!payload || /[;|<>]|&&/.test(payload)) return null;
2012
+ return splitShellWords(payload);
2013
+ }
2014
+ function skillNamesFromCommand(command) {
2015
+ const words = commandWords(command);
2016
+ if (!words) return [];
2017
+ return Array.from(new Set(words.flatMap((word) => {
2018
+ const skillName = skillNameFromSkillRootPath(word);
2019
+ return skillName ? [skillName] : [];
2020
+ })));
2021
+ }
2022
+
2023
+ // ../shared/src/display-message/task-accumulator.ts
2024
+ function mapTaskStatus(status) {
2025
+ if (status === "in_progress" || status === "completed") return status;
2026
+ return "pending";
2027
+ }
2028
+ function toAccumulatorInput(input) {
2029
+ if (typeof input === "string") {
2030
+ try {
2031
+ const parsed = JSON.parse(input);
2032
+ return isRecord(parsed) ? parsed : {};
2033
+ } catch {
2034
+ return {};
2035
+ }
2036
+ }
2037
+ return isRecord(input) ? input : {};
2038
+ }
2039
+ function extractToolResultText(content, separator = "\n") {
2040
+ if (typeof content === "string") return content;
2041
+ if (!Array.isArray(content)) return "";
2042
+ const texts = [];
2043
+ for (const part of content) {
2044
+ if (isRecord(part) && part.type === "text" && typeof part.text === "string") {
2045
+ texts.push(part.text);
2046
+ }
2047
+ }
2048
+ return texts.join(separator);
2049
+ }
2050
+ function normalizeContentBlocks(content) {
2051
+ if (typeof content === "string") {
2052
+ return content ? [{ type: "text", text: content }] : [];
2053
+ }
2054
+ return Array.isArray(content) ? content.filter((block) => isRecord(block) && typeof block.type === "string") : [];
2055
+ }
2056
+ function parseStringField(value) {
2057
+ if (typeof value !== "string") return null;
2058
+ const trimmed = value.trim();
2059
+ return trimmed ? trimmed : null;
2060
+ }
2061
+ function parseCreateResultTaskId(resultText) {
2062
+ try {
2063
+ const parsed = JSON.parse(resultText);
2064
+ if (isRecord(parsed) && isRecord(parsed.task)) {
2065
+ return parseStringField(parsed.task.id);
2066
+ }
2067
+ } catch {
2068
+ }
2069
+ return resultText.match(/\bTask\s+#?([A-Za-z0-9_-]+)\s+created\b/i)?.[1] ?? null;
2070
+ }
2071
+ var TaskAccumulator = class {
2072
+ tasksByTaskId = /* @__PURE__ */ new Map();
2073
+ tasksByCreateToolUseId = /* @__PURE__ */ new Map();
2074
+ orderedKeys = [];
2075
+ processToolUse(toolName, toolUseId, input) {
2076
+ if (toolName === "TaskCreate") {
2077
+ const subject = parseStringField(input.subject);
2078
+ if (!subject) return false;
2079
+ this.tasksByCreateToolUseId.set(toolUseId, { subject, status: "pending" });
2080
+ this.orderedKeys.push(toolUseId);
2081
+ return true;
2082
+ }
2083
+ if (toolName === "TaskUpdate") {
2084
+ const taskId = parseStringField(input.taskId);
2085
+ if (!taskId) return false;
2086
+ if (input.status === "deleted") {
2087
+ if (!this.tasksByTaskId.delete(taskId)) return false;
2088
+ const idx = this.orderedKeys.indexOf(taskId);
2089
+ if (idx !== -1) this.orderedKeys.splice(idx, 1);
2090
+ return true;
2091
+ }
2092
+ const existing = this.tasksByTaskId.get(taskId);
2093
+ const subject = parseStringField(input.subject) ?? existing?.subject;
2094
+ if (!subject) return false;
2095
+ const status = input.status !== void 0 ? mapTaskStatus(input.status) : existing?.status ?? "pending";
2096
+ this.tasksByTaskId.set(taskId, { subject, status });
2097
+ if (!existing) this.orderedKeys.push(taskId);
2098
+ return true;
2099
+ }
2100
+ if (toolName === "TaskList") {
2101
+ if (!Array.isArray(input.tasks)) return false;
2102
+ let changed = false;
2103
+ for (const raw of input.tasks) {
2104
+ if (!isRecord(raw)) continue;
2105
+ const id = parseStringField(raw.id);
2106
+ const subject = parseStringField(raw.subject);
2107
+ if (!id || !subject) continue;
2108
+ const existing = this.tasksByTaskId.get(id);
2109
+ const next = { subject, status: mapTaskStatus(raw.status) };
2110
+ if (existing && existing.subject === next.subject && existing.status === next.status) continue;
2111
+ this.tasksByTaskId.set(id, next);
2112
+ if (!existing) this.orderedKeys.push(id);
2113
+ changed = true;
2114
+ }
2115
+ return changed;
2116
+ }
2117
+ return false;
2118
+ }
2119
+ processCreateResult(toolUseId, resultText) {
2120
+ const pending = this.tasksByCreateToolUseId.get(toolUseId);
2121
+ if (!pending) return false;
2122
+ const taskId = parseCreateResultTaskId(resultText);
2123
+ if (!taskId) return false;
2124
+ this.tasksByTaskId.set(taskId, pending);
2125
+ this.tasksByCreateToolUseId.delete(toolUseId);
2126
+ const idx = this.orderedKeys.indexOf(toolUseId);
2127
+ if (idx !== -1) this.orderedKeys.splice(idx, 1, taskId);
2128
+ return true;
2129
+ }
2130
+ getTasks() {
2131
+ const out = [];
2132
+ for (const key of this.orderedKeys) {
2133
+ const task = this.tasksByTaskId.get(key) ?? this.tasksByCreateToolUseId.get(key);
2134
+ if (task && task.subject) out.push(task);
2135
+ }
2136
+ return out;
2137
+ }
2138
+ processContentBlock(block) {
2139
+ if (!isRecord(block)) return false;
2140
+ if (block.type === "tool_use" && typeof block.name === "string" && typeof block.id === "string") {
2141
+ return this.processToolUse(block.name, block.id, toAccumulatorInput(block.input));
2142
+ }
2143
+ if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
2144
+ return this.processCreateResult(block.tool_use_id, extractToolResultText(block.content, ""));
2145
+ }
2146
+ return false;
2147
+ }
2148
+ };
2149
+
2150
+ // ../shared/src/user-message-matching.ts
2151
+ function parseTimestampMs(timestamp) {
2152
+ const value = Date.parse(timestamp);
2153
+ return Number.isFinite(value) ? value : 0;
2154
+ }
2155
+ function areUserMessagesWithinMatchWindow(a, b) {
2156
+ return a.content === b.content && Math.abs(parseTimestampMs(a.timestamp) - parseTimestampMs(b.timestamp)) <= USER_MESSAGE_MATCH_GRACE_PERIOD_MS;
1620
2157
  }
1621
2158
 
1622
2159
  // ../shared/src/display-message/parsers/codex-parser.ts
@@ -1628,16 +2165,8 @@ function getPayloadString(event, key) {
1628
2165
  return typeof value === "string" ? value : null;
1629
2166
  }
1630
2167
  function displayId(event, eventIndex, prefix) {
1631
- const stableId = getPayloadString(event, USER_MESSAGE_ID_PAYLOAD_KEY) ?? getPayloadString(event, CODEX_ASP_ITEM_ID_PAYLOAD_KEY);
1632
- return stableId ? `${prefix}-${stableId}` : `${prefix}-${event.timestamp}-${eventIndex}`;
1633
- }
1634
- function userMessageImages(value) {
1635
- if (!Array.isArray(value)) return void 0;
1636
- const images = value.filter((item) => {
1637
- if (!isRecord(item)) return false;
1638
- return item.type === "image" && typeof item.mediaType === "string" && typeof item.data === "string";
1639
- });
1640
- return images.length > 0 ? images : void 0;
2168
+ const stableId = getPayloadString(event, USER_MESSAGE_ID_PAYLOAD_KEY) ?? getPayloadString(event, CODEX_ASP_ITEM_ID_PAYLOAD_KEY);
2169
+ return stableId ? `${prefix}-${stableId}` : `${prefix}-${event.timestamp}-${eventIndex}`;
1641
2170
  }
1642
2171
  function parseShellOutput(raw) {
1643
2172
  const exitCodeMatch = raw.match(/^Exit code: (\d+)/m) || raw.match(/Process exited with code (\d+)/);
@@ -1758,10 +2287,12 @@ function parseCodexEvents(events) {
1758
2287
  const callId = event.payload.call_id;
1759
2288
  const args = safeJsonParse(event.payload.arguments || "{}", {});
1760
2289
  const command = args.cmd || (Array.isArray(args.command) ? args.command.join(" ") : args.command) || "";
2290
+ const skillNames = skillNamesFromCommand(command);
1761
2291
  const msg = {
1762
2292
  id: `command-${callId || "no-call-id"}-${eventIndex}`,
1763
2293
  type: "command",
1764
2294
  command,
2295
+ ...skillNames.length > 0 ? { skillNames } : {},
1765
2296
  output: "",
1766
2297
  status: "in_progress",
1767
2298
  timestamp: event.timestamp
@@ -1793,17 +2324,17 @@ function parseCodexEvents(events) {
1793
2324
  const operations = parsePatch(input);
1794
2325
  pendingPatches.set(callId, { input, status, timestamp: event.timestamp, operations });
1795
2326
  } else {
1796
- const msg = {
1797
- id: `toolcall-${callId || getPayloadString(event, CODEX_ASP_ITEM_ID_PAYLOAD_KEY) || `${event.timestamp}-${eventIndex}`}`,
1798
- type: "tool_call",
2327
+ const id = `toolcall-${callId || getPayloadString(event, CODEX_ASP_ITEM_ID_PAYLOAD_KEY) || `${event.timestamp}-${eventIndex}`}`;
2328
+ const msg = createCallDisplayMessage({
2329
+ id,
1799
2330
  server: server2,
1800
2331
  tool: name,
1801
2332
  input,
1802
2333
  status,
1803
2334
  timestamp: event.timestamp
1804
- };
2335
+ });
1805
2336
  messages.push(msg);
1806
- if (callId) {
2337
+ if (callId && msg.type === "tool_call") {
1807
2338
  pendingToolCalls.set(callId, msg);
1808
2339
  }
1809
2340
  }
@@ -1862,17 +2393,6 @@ function parseCodexEvents(events) {
1862
2393
  return messages;
1863
2394
  }
1864
2395
 
1865
- // ../shared/src/display-message/parsers/utils.ts
1866
- function stringifyDisplayValue(value) {
1867
- if (value === void 0 || value === null) return void 0;
1868
- if (typeof value === "string") return value;
1869
- try {
1870
- return JSON.stringify(value, null, 2);
1871
- } catch {
1872
- return String(value);
1873
- }
1874
- }
1875
-
1876
2396
  // ../shared/src/display-message/parsers/cursor-parser.ts
1877
2397
  function getTextContent(value) {
1878
2398
  if (!Array.isArray(value)) return "";
@@ -1898,10 +2418,10 @@ function getCursorThinkingText(event) {
1898
2418
  function isTerminalStatus(status) {
1899
2419
  return status === "FINISHED" || status === "ERROR" || status === "CANCELLED" || status === "EXPIRED";
1900
2420
  }
1901
- function finalizeOpenTools(messages, toolIndexes, status) {
1902
- for (const index of toolIndexes.values()) {
2421
+ function finalizeOpenCalls(messages, callIndexes, status) {
2422
+ for (const index of callIndexes.values()) {
1903
2423
  const message = messages[index];
1904
- if (message?.type === "tool_call" && message.status === "in_progress") {
2424
+ if ((message?.type === "tool_call" || message?.type === "skill") && message.status === "in_progress") {
1905
2425
  messages[index] = {
1906
2426
  ...message,
1907
2427
  status
@@ -1911,7 +2431,7 @@ function finalizeOpenTools(messages, toolIndexes, status) {
1911
2431
  }
1912
2432
  function parseCursorEvents(events) {
1913
2433
  const messages = [];
1914
- const toolIndexes = /* @__PURE__ */ new Map();
2434
+ const callIndexes = /* @__PURE__ */ new Map();
1915
2435
  const runAssistantSegments = /* @__PURE__ */ new Map();
1916
2436
  const runThinkingSegments = /* @__PURE__ */ new Map();
1917
2437
  let activeAssistant = null;
@@ -2003,21 +2523,20 @@ function parseCursorEvents(events) {
2003
2523
  const tool = typeof event.payload.name === "string" ? event.payload.name : "tool";
2004
2524
  const status = cursorStatusToDisplayStatus(event.payload.status);
2005
2525
  const input = isRecord(event.payload.args) ? event.payload.args : typeof event.payload.args === "string" ? event.payload.args : void 0;
2006
- const existing = toolIndexes.get(callId);
2007
- const next = {
2526
+ const existing = callIndexes.get(callId);
2527
+ const next = createCallDisplayMessage({
2008
2528
  id: callId,
2009
- type: "tool_call",
2010
2529
  server: "cursor",
2011
2530
  tool,
2012
2531
  ...input !== void 0 ? { input } : {},
2013
2532
  output: stringifyDisplayValue(event.payload.result),
2014
2533
  status,
2015
2534
  timestamp: event.timestamp
2016
- };
2535
+ });
2017
2536
  if (existing === void 0) {
2018
- toolIndexes.set(callId, messages.push(next) - 1);
2537
+ callIndexes.set(callId, messages.push(next) - 1);
2019
2538
  } else {
2020
- messages[existing] = { ...messages[existing], ...next };
2539
+ messages[existing] = next;
2021
2540
  }
2022
2541
  continue;
2023
2542
  }
@@ -2039,7 +2558,7 @@ function parseCursorEvents(events) {
2039
2558
  if (isTerminalStatus(status)) {
2040
2559
  const displayStatus = cursorStatusToDisplayStatus(status);
2041
2560
  finalizeActiveThinking(displayStatus);
2042
- finalizeOpenTools(messages, toolIndexes, displayStatus);
2561
+ finalizeOpenCalls(messages, callIndexes, displayStatus);
2043
2562
  activeThinking = null;
2044
2563
  activeAssistant = null;
2045
2564
  }
@@ -2047,7 +2566,7 @@ function parseCursorEvents(events) {
2047
2566
  }
2048
2567
  if (event.type === "cursor-error") {
2049
2568
  finalizeActiveThinking("failed");
2050
- finalizeOpenTools(messages, toolIndexes, "failed");
2569
+ finalizeOpenCalls(messages, callIndexes, "failed");
2051
2570
  activeAssistant = null;
2052
2571
  activeThinking = null;
2053
2572
  const message = typeof event.payload.message === "string" ? event.payload.message : "Cursor run failed";
@@ -2072,14 +2591,6 @@ function partFromEvent(event) {
2072
2591
  function toolStatus(status) {
2073
2592
  return status === "completed" ? "completed" : status === "error" ? "failed" : "in_progress";
2074
2593
  }
2075
- function setById(messages, id, next) {
2076
- const index = messages.findIndex((message) => message.id === id);
2077
- if (index === -1) {
2078
- messages.push(next);
2079
- } else {
2080
- messages[index] = next;
2081
- }
2082
- }
2083
2594
  function assistantError(info) {
2084
2595
  if (!isRecord(info) || !isRecord(info.error)) return null;
2085
2596
  const data = isRecord(info.error.data) ? info.error.data : null;
@@ -2093,8 +2604,9 @@ function parseOpencodeEvents(events) {
2093
2604
  if (event.type === "event_msg" && event.payload.type === "user_message") {
2094
2605
  const message = event.payload.message;
2095
2606
  if (typeof message === "string" && message.trim()) {
2607
+ const messageId = event.payload[USER_MESSAGE_ID_PAYLOAD_KEY];
2096
2608
  messages.push({
2097
- id: `opencode-user-${event.timestamp}-${messages.length}`,
2609
+ id: typeof messageId === "string" ? messageId : `opencode-user-${event.timestamp}`,
2098
2610
  type: "user",
2099
2611
  content: message,
2100
2612
  timestamp: event.timestamp
@@ -2106,7 +2618,7 @@ function parseOpencodeEvents(events) {
2106
2618
  const error = assistantError(event.payload.info);
2107
2619
  if (error) {
2108
2620
  messages.push({
2109
- id: `opencode-error-${event.timestamp}-${messages.length}`,
2621
+ id: `opencode-${event.type}-${event.timestamp}`,
2110
2622
  type: "error",
2111
2623
  message: error,
2112
2624
  timestamp: event.timestamp
@@ -2117,22 +2629,39 @@ function parseOpencodeEvents(events) {
2117
2629
  if (event.type === "opencode-error") {
2118
2630
  const message = typeof event.payload.message === "string" ? event.payload.message : "Opencode run failed";
2119
2631
  messages.push({
2120
- id: `opencode-error-${event.timestamp}-${messages.length}`,
2632
+ id: `opencode-${event.type}-${event.timestamp}`,
2121
2633
  type: "error",
2122
2634
  message,
2123
2635
  timestamp: event.timestamp
2124
2636
  });
2125
2637
  continue;
2126
2638
  }
2639
+ if (event.type === "opencode-session.next.tool.called") {
2640
+ const tool = typeof event.payload.tool === "string" ? event.payload.tool : "tool";
2641
+ const input = event.payload.input ?? event.payload.arguments;
2642
+ const callId = typeof event.payload.callID === "string" ? event.payload.callID : null;
2643
+ const id2 = callId ? `opencode-${callId}` : `opencode-${event.type}-${event.timestamp}`;
2644
+ upsertDisplayMessage(messages, createCallDisplayMessage({
2645
+ id: id2,
2646
+ server: "opencode",
2647
+ tool,
2648
+ input: isRecord(input) ? input : stringifyDisplayValue(input),
2649
+ status: "in_progress",
2650
+ timestamp: event.timestamp
2651
+ }));
2652
+ continue;
2653
+ }
2127
2654
  const part = partFromEvent(event);
2128
2655
  if (!part) continue;
2129
- const id = typeof part.id === "string" ? `opencode-${part.id}` : `opencode-${event.timestamp}-${messages.length}`;
2656
+ let id = `opencode-${event.type}-${event.timestamp}`;
2657
+ if (typeof part.callID === "string") id = `opencode-${part.callID}`;
2658
+ else if (typeof part.id === "string") id = `opencode-${part.id}`;
2130
2659
  const time = isRecord(part.time) ? part.time : null;
2131
2660
  const timestamp = timestampFromMs(time?.start, event.timestamp);
2132
2661
  if (part.type === "text") {
2133
2662
  const text = typeof part.text === "string" ? part.text : "";
2134
2663
  if (text.trim()) {
2135
- setById(messages, id, {
2664
+ upsertDisplayMessage(messages, {
2136
2665
  id,
2137
2666
  type: "agent",
2138
2667
  content: text,
@@ -2144,7 +2673,7 @@ function parseOpencodeEvents(events) {
2144
2673
  if (part.type === "reasoning") {
2145
2674
  const text = typeof part.text === "string" ? part.text : "";
2146
2675
  if (text.trim()) {
2147
- setById(messages, id, {
2676
+ upsertDisplayMessage(messages, {
2148
2677
  id,
2149
2678
  type: "reasoning",
2150
2679
  content: text,
@@ -2157,23 +2686,24 @@ function parseOpencodeEvents(events) {
2157
2686
  if (part.type === "tool" && isRecord(part.state)) {
2158
2687
  const state = part.state;
2159
2688
  const status = toolStatus(state.status);
2689
+ const tool = typeof part.tool === "string" ? part.tool : "tool";
2690
+ const input = state.input;
2160
2691
  const output = state.status === "completed" ? stringifyDisplayValue(state.output) : state.status === "error" ? stringifyDisplayValue(state.error) : void 0;
2161
- setById(messages, id, {
2692
+ upsertDisplayMessage(messages, createCallDisplayMessage({
2162
2693
  id,
2163
- type: "tool_call",
2164
2694
  server: "opencode",
2165
- tool: typeof part.tool === "string" ? part.tool : "tool",
2166
- input: isRecord(state.input) ? state.input : stringifyDisplayValue(state.input),
2695
+ tool,
2696
+ input: isRecord(input) ? input : stringifyDisplayValue(input),
2167
2697
  output,
2168
2698
  status,
2169
2699
  timestamp
2170
- });
2700
+ }));
2171
2701
  continue;
2172
2702
  }
2173
2703
  if (part.type === "patch") {
2174
2704
  const files = Array.isArray(part.files) ? part.files.filter((file) => typeof file === "string") : [];
2175
2705
  if (files.length > 0) {
2176
- setById(messages, id, {
2706
+ upsertDisplayMessage(messages, {
2177
2707
  id,
2178
2708
  type: "file_change",
2179
2709
  changes: files.map((path) => ({ path, kind: "update" })),
@@ -2186,193 +2716,83 @@ function parseOpencodeEvents(events) {
2186
2716
  return messages;
2187
2717
  }
2188
2718
 
2189
- // ../shared/src/display-message/types.ts
2190
- var BACKGROUND_TASK_SUBTYPES = /* @__PURE__ */ new Set([
2191
- "task_started",
2192
- "task_progress",
2193
- "task_updated",
2194
- "task_notification"
2195
- ]);
2196
- function isBackgroundTaskSubtype(subtype) {
2197
- return typeof subtype === "string" && BACKGROUND_TASK_SUBTYPES.has(subtype);
2198
- }
2199
- function optionalString(value) {
2200
- return typeof value === "string" ? value : void 0;
2201
- }
2202
- function coerceBackgroundTaskPayload(payload) {
2203
- if (!isRecord(payload)) return null;
2204
- const subtype = payload.subtype;
2205
- if (!isBackgroundTaskSubtype(subtype)) return null;
2206
- const taskId = payload.task_id;
2207
- if (typeof taskId !== "string" || !taskId) return null;
2208
- const patch = isRecord(payload.patch) ? {
2209
- status: optionalString(payload.patch.status),
2210
- description: optionalString(payload.patch.description),
2211
- error: optionalString(payload.patch.error),
2212
- isBackgrounded: optionalBoolean(payload.patch.is_backgrounded)
2213
- } : void 0;
2214
- const usage = isRecord(payload.usage) ? {
2215
- totalTokens: optionalNumber(payload.usage.total_tokens),
2216
- toolUses: optionalNumber(payload.usage.tool_uses),
2217
- durationMs: optionalNumber(payload.usage.duration_ms)
2218
- } : void 0;
2219
- return {
2220
- subtype,
2221
- taskId,
2222
- description: optionalString(payload.description),
2223
- summary: optionalString(payload.summary),
2224
- outputFile: optionalString(payload.output_file),
2225
- status: optionalString(payload.status),
2226
- taskType: optionalString(payload.task_type),
2227
- workflowName: optionalString(payload.workflow_name),
2228
- prompt: optionalString(payload.prompt),
2229
- lastToolName: optionalString(payload.last_tool_name),
2230
- usage,
2231
- patch
2232
- };
2233
- }
2234
- function optionalNumber(value) {
2235
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2236
- }
2237
- function optionalBoolean(value) {
2238
- return typeof value === "boolean" ? value : void 0;
2239
- }
2240
- function normalizeBackgroundTaskStatus(status) {
2241
- if (status === "completed" || status === "failed" || status === "stopped") {
2242
- return status;
2243
- }
2244
- if (status === "killed") {
2245
- return "stopped";
2246
- }
2247
- return "in_progress";
2248
- }
2249
-
2250
- // ../shared/src/display-message/task-accumulator.ts
2251
- function mapTaskStatus(status) {
2252
- if (status === "in_progress" || status === "completed") return status;
2253
- return "pending";
2254
- }
2255
- function toAccumulatorInput(input) {
2256
- if (typeof input === "string") {
2257
- try {
2258
- const parsed = JSON.parse(input);
2259
- return isRecord(parsed) ? parsed : {};
2260
- } catch {
2261
- return {};
2262
- }
2263
- }
2264
- return isRecord(input) ? input : {};
2265
- }
2266
- function extractToolResultText(content, separator = "\n") {
2267
- if (typeof content === "string") return content;
2268
- if (!Array.isArray(content)) return "";
2269
- const texts = [];
2270
- for (const part of content) {
2271
- if (isRecord(part) && part.type === "text" && typeof part.text === "string") {
2272
- texts.push(part.text);
2273
- }
2274
- }
2275
- return texts.join(separator);
2276
- }
2277
- function normalizeContentBlocks(content) {
2278
- if (typeof content === "string") {
2279
- return content ? [{ type: "text", text: content }] : [];
2280
- }
2281
- return content ?? [];
2719
+ // ../shared/src/display-message/parsers/pi-parser.ts
2720
+ function nestedPayload(event) {
2721
+ return isRecord(event.payload.assistantMessageEvent) ? event.payload.assistantMessageEvent : event.payload;
2282
2722
  }
2283
- function parseStringField(value) {
2284
- if (typeof value !== "string") return null;
2285
- const trimmed = value.trim();
2286
- return trimmed ? trimmed : null;
2723
+ function toolStatus2(value) {
2724
+ return value === "completed" || value === "success" ? "completed" : value === "error" ? "failed" : "in_progress";
2287
2725
  }
2288
- function parseCreateResultTaskId(resultText) {
2289
- try {
2290
- const parsed = JSON.parse(resultText);
2291
- if (isRecord(parsed) && isRecord(parsed.task)) {
2292
- return parseStringField(parsed.task.id);
2293
- }
2294
- } catch {
2295
- }
2296
- return resultText.match(/\bTask\s+#?([A-Za-z0-9_-]+)\s+created\b/i)?.[1] ?? null;
2726
+ function assistantErrorMessage(value) {
2727
+ if (!isRecord(value) || value.role !== "assistant" || value.stopReason !== "error") return null;
2728
+ return typeof value.errorMessage === "string" && value.errorMessage ? value.errorMessage : "Pi run failed";
2297
2729
  }
2298
- var TaskAccumulator = class {
2299
- tasksByTaskId = /* @__PURE__ */ new Map();
2300
- tasksByCreateToolUseId = /* @__PURE__ */ new Map();
2301
- orderedKeys = [];
2302
- processToolUse(toolName, toolUseId, input) {
2303
- if (toolName === "TaskCreate") {
2304
- const subject = parseStringField(input.subject);
2305
- if (!subject) return false;
2306
- this.tasksByCreateToolUseId.set(toolUseId, { subject, status: "pending" });
2307
- this.orderedKeys.push(toolUseId);
2308
- return true;
2309
- }
2310
- if (toolName === "TaskUpdate") {
2311
- const taskId = parseStringField(input.taskId);
2312
- if (!taskId) return false;
2313
- if (input.status === "deleted") {
2314
- if (!this.tasksByTaskId.delete(taskId)) return false;
2315
- const idx = this.orderedKeys.indexOf(taskId);
2316
- if (idx !== -1) this.orderedKeys.splice(idx, 1);
2317
- return true;
2730
+ function parsePiEvents(events) {
2731
+ const messages = [];
2732
+ const text = /* @__PURE__ */ new Map();
2733
+ const thinking = /* @__PURE__ */ new Map();
2734
+ let activeMessageId = "assistant";
2735
+ for (const event of events) {
2736
+ if (event.type === "event_msg" && event.payload.type === "user_message" && typeof event.payload.message === "string") {
2737
+ messages.push({ id: `pi-user-${event.timestamp}-${messages.length}`, type: "user", content: event.payload.message, timestamp: event.timestamp });
2738
+ if (typeof event.payload.skillName === "string" && event.payload.skillName) {
2739
+ messages.push({
2740
+ id: `pi-skill-${event.timestamp}-${messages.length}`,
2741
+ type: "skill",
2742
+ skillName: event.payload.skillName,
2743
+ status: "completed",
2744
+ timestamp: event.timestamp
2745
+ });
2318
2746
  }
2319
- const existing = this.tasksByTaskId.get(taskId);
2320
- const subject = parseStringField(input.subject) ?? existing?.subject;
2321
- if (!subject) return false;
2322
- const status = input.status !== void 0 ? mapTaskStatus(input.status) : existing?.status ?? "pending";
2323
- this.tasksByTaskId.set(taskId, { subject, status });
2324
- if (!existing) this.orderedKeys.push(taskId);
2325
- return true;
2747
+ continue;
2326
2748
  }
2327
- if (toolName === "TaskList") {
2328
- if (!Array.isArray(input.tasks)) return false;
2329
- let changed = false;
2330
- for (const raw of input.tasks) {
2331
- if (!isRecord(raw)) continue;
2332
- const id = parseStringField(raw.id);
2333
- const subject = parseStringField(raw.subject);
2334
- if (!id || !subject) continue;
2335
- const existing = this.tasksByTaskId.get(id);
2336
- const next = { subject, status: mapTaskStatus(raw.status) };
2337
- if (existing && existing.subject === next.subject && existing.status === next.status) continue;
2338
- this.tasksByTaskId.set(id, next);
2339
- if (!existing) this.orderedKeys.push(id);
2340
- changed = true;
2749
+ if (event.type === "pi-error") {
2750
+ messages.push({ id: `pi-error-${event.timestamp}-${messages.length}`, type: "error", message: String(event.payload.message ?? "Pi run failed"), timestamp: event.timestamp });
2751
+ continue;
2752
+ }
2753
+ if (event.type === "pi-message_end") {
2754
+ const error = assistantErrorMessage(event.payload.message);
2755
+ if (error) {
2756
+ messages.push({ id: `pi-error-${event.timestamp}-${messages.length}`, type: "error", message: error, timestamp: event.timestamp });
2341
2757
  }
2342
- return changed;
2758
+ continue;
2343
2759
  }
2344
- return false;
2345
- }
2346
- processCreateResult(toolUseId, resultText) {
2347
- const pending = this.tasksByCreateToolUseId.get(toolUseId);
2348
- if (!pending) return false;
2349
- const taskId = parseCreateResultTaskId(resultText);
2350
- if (!taskId) return false;
2351
- this.tasksByTaskId.set(taskId, pending);
2352
- this.tasksByCreateToolUseId.delete(toolUseId);
2353
- const idx = this.orderedKeys.indexOf(toolUseId);
2354
- if (idx !== -1) this.orderedKeys.splice(idx, 1, taskId);
2355
- return true;
2356
- }
2357
- getTasks() {
2358
- const out = [];
2359
- for (const key of this.orderedKeys) {
2360
- const task = this.tasksByTaskId.get(key) ?? this.tasksByCreateToolUseId.get(key);
2361
- if (task && task.subject) out.push(task);
2760
+ if (event.type === "pi-message_update") {
2761
+ const payload = nestedPayload(event);
2762
+ const id = typeof payload.id === "string" ? payload.id : activeMessageId;
2763
+ if (payload.type === "text_delta" && typeof payload.delta === "string") text.set(id, `${text.get(id) ?? ""}${payload.delta}`);
2764
+ if (payload.type === "thinking_delta" && typeof payload.delta === "string") thinking.set(id, `${thinking.get(id) ?? ""}${payload.delta}`);
2765
+ const textContent = text.get(id);
2766
+ const thinkingContent = thinking.get(id);
2767
+ if (textContent !== void 0) upsertDisplayMessage(messages, { id: `pi-${id}`, type: "agent", content: textContent, timestamp: event.timestamp });
2768
+ if (thinkingContent !== void 0) upsertDisplayMessage(messages, { id: `pi-thinking-${id}`, type: "reasoning", content: thinkingContent, status: "in_progress", timestamp: event.timestamp });
2769
+ continue;
2362
2770
  }
2363
- return out;
2364
- }
2365
- processContentBlock(block) {
2366
- if (!isRecord(block)) return false;
2367
- if (block.type === "tool_use" && typeof block.name === "string" && typeof block.id === "string") {
2368
- return this.processToolUse(block.name, block.id, toAccumulatorInput(block.input));
2771
+ if (event.type === "pi-message_start") {
2772
+ const message = isRecord(event.payload.message) ? event.payload.message : null;
2773
+ activeMessageId = typeof message?.id === "string" ? message.id : `assistant-${event.timestamp}`;
2774
+ continue;
2369
2775
  }
2370
- if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
2371
- return this.processCreateResult(block.tool_use_id, extractToolResultText(block.content, ""));
2776
+ if (event.type === "pi-tool_execution_start" || event.type === "pi-tool_execution_update" || event.type === "pi-tool_execution_end") {
2777
+ const payload = event.payload;
2778
+ const input = payload.args ?? payload.input;
2779
+ const id = typeof payload.toolCallId === "string" ? payload.toolCallId : typeof payload.id === "string" ? payload.id : `tool-${event.timestamp}`;
2780
+ const tool = typeof payload.toolName === "string" ? payload.toolName : "tool";
2781
+ const messageId = `pi-tool-${id}`;
2782
+ const status = event.type === "pi-tool_execution_end" ? toolStatus2(payload.isError ? "error" : "completed") : "in_progress";
2783
+ upsertDisplayMessage(messages, createCallDisplayMessage({
2784
+ id: messageId,
2785
+ server: "pi",
2786
+ tool,
2787
+ input: isRecord(input) ? input : stringifyDisplayValue(input),
2788
+ output: event.type === "pi-tool_execution_end" ? stringifyDisplayValue(payload.result ?? payload.error) : void 0,
2789
+ status,
2790
+ timestamp: event.timestamp
2791
+ }));
2372
2792
  }
2373
- return false;
2374
2793
  }
2375
- };
2794
+ return messages;
2795
+ }
2376
2796
 
2377
2797
  // ../shared/src/display-message/parsers/mcp.ts
2378
2798
  function parseMcpToolName(name) {
@@ -2388,13 +2808,22 @@ function parseMcpToolName(name) {
2388
2808
  }
2389
2809
 
2390
2810
  // ../shared/src/display-message/parsers/claude-parser.ts
2391
- function upsertDisplayMessage(messages, message) {
2392
- const index = messages.findIndex((existing) => existing.id === message.id);
2393
- if (index === -1) {
2394
- messages.push(message);
2395
- } else {
2396
- messages[index] = message;
2397
- }
2811
+ var ABORTED_TERMINAL_REASONS = /* @__PURE__ */ new Set(["aborted_streaming", "aborted_tools"]);
2812
+ function coerceClaudeResultPayload(payload) {
2813
+ return {
2814
+ is_error: typeof payload.is_error === "boolean" ? payload.is_error : void 0,
2815
+ subtype: typeof payload.subtype === "string" ? payload.subtype : void 0,
2816
+ errors: Array.isArray(payload.errors) ? payload.errors.filter((e) => typeof e === "string") : void 0,
2817
+ terminal_reason: typeof payload.terminal_reason === "string" ? payload.terminal_reason : void 0
2818
+ };
2819
+ }
2820
+ function stripAgentDiagnosticErrors(errors) {
2821
+ return errors.filter((error) => !error.startsWith("[ede_diagnostic]"));
2822
+ }
2823
+ function isClaudeResultError(payload) {
2824
+ if (payload.terminal_reason && ABORTED_TERMINAL_REASONS.has(payload.terminal_reason)) return false;
2825
+ if (payload.errors?.length && stripAgentDiagnosticErrors(payload.errors).length === 0) return false;
2826
+ return Boolean(payload.is_error) || payload.subtype !== "success";
2398
2827
  }
2399
2828
  var LOCAL_COMMAND_ECHO_REGEX = /^<(?:command-name|command-message|local-command-stdout|local-command-stderr)>/;
2400
2829
  function parseClaudeEvents(events, parentToolUseId) {
@@ -2412,6 +2841,14 @@ function parseClaudeEvents(events, parentToolUseId) {
2412
2841
  let liveStreamId = null;
2413
2842
  const assistantThinking = /* @__PURE__ */ new Map();
2414
2843
  const assistantTextCounts = /* @__PURE__ */ new Map();
2844
+ const acceptedUserMessageIndexes = /* @__PURE__ */ new Set();
2845
+ const canonicalUserMessageIndexes = /* @__PURE__ */ new Set();
2846
+ const findMatchingUserMessageIndex = (indexes, content, timestamp) => {
2847
+ return [...indexes].find((index) => {
2848
+ const message = messages[index];
2849
+ return message?.type === "user" && areUserMessagesWithinMatchWindow(message, { content, timestamp });
2850
+ });
2851
+ };
2415
2852
  const taskAccumulator = new TaskAccumulator();
2416
2853
  const taskSnapshot = () => taskAccumulator.getTasks().map((task) => ({
2417
2854
  text: task.subject,
@@ -2419,6 +2856,29 @@ function parseClaudeEvents(events, parentToolUseId) {
2419
2856
  itemStatus: task.status
2420
2857
  }));
2421
2858
  filteredEvents.forEach((event) => {
2859
+ if (event.type === "event_msg" && event.payload.type === "user_message") {
2860
+ const content = typeof event.payload.message === "string" ? event.payload.message : "";
2861
+ const images = userMessageImages(event.payload.images);
2862
+ if (content || images) {
2863
+ const messageId = event.payload[USER_MESSAGE_ID_PAYLOAD_KEY];
2864
+ const canonicalIndex = findMatchingUserMessageIndex(canonicalUserMessageIndexes, content, event.timestamp);
2865
+ const canonical = canonicalIndex === void 0 ? void 0 : messages[canonicalIndex];
2866
+ const message = {
2867
+ id: typeof messageId === "string" ? messageId : `user-${event.timestamp}`,
2868
+ type: "user",
2869
+ content,
2870
+ images: images?.length ? images : canonical?.type === "user" ? canonical.images : void 0,
2871
+ timestamp: event.timestamp
2872
+ };
2873
+ if (canonicalIndex === void 0) {
2874
+ acceptedUserMessageIndexes.add(messages.push(message) - 1);
2875
+ } else {
2876
+ messages[canonicalIndex] = message;
2877
+ canonicalUserMessageIndexes.delete(canonicalIndex);
2878
+ }
2879
+ }
2880
+ return;
2881
+ }
2422
2882
  if (event.type === CLAUDE_PARTIAL_MESSAGE_EVENT_TYPE) {
2423
2883
  const payload = coerceClaudePartialMessagePayload(event.payload);
2424
2884
  if (!payload) return;
@@ -2476,13 +2936,25 @@ function parseClaudeEvents(events, parentToolUseId) {
2476
2936
  };
2477
2937
  }).filter((img) => img.data);
2478
2938
  if (textContent || images.length > 0) {
2479
- messages.push({
2480
- id: `user-${event.timestamp}`,
2939
+ const acceptedIndex = findMatchingUserMessageIndex(
2940
+ acceptedUserMessageIndexes,
2941
+ textContent,
2942
+ event.timestamp
2943
+ );
2944
+ const accepted = acceptedIndex === void 0 ? void 0 : messages[acceptedIndex];
2945
+ const message = {
2946
+ id: accepted?.id ?? `user-${event.timestamp}`,
2481
2947
  type: "user",
2482
2948
  content: textContent || (images.length > 0 ? `[${images.length} image${images.length > 1 ? "s" : ""} attached]` : ""),
2483
- images: images.length > 0 ? images : void 0,
2949
+ images: images.length > 0 ? images : accepted?.type === "user" ? accepted.images : void 0,
2484
2950
  timestamp: event.timestamp
2485
- });
2951
+ };
2952
+ if (acceptedIndex === void 0) {
2953
+ canonicalUserMessageIndexes.add(messages.push(message) - 1);
2954
+ } else {
2955
+ messages[acceptedIndex] = message;
2956
+ acceptedUserMessageIndexes.delete(acceptedIndex);
2957
+ }
2486
2958
  }
2487
2959
  }
2488
2960
  if (event.type === "claude-assistant") {
@@ -2565,10 +3037,12 @@ function parseClaudeEvents(events, parentToolUseId) {
2565
3037
  } else if (toolName === "Bash" || toolName === "bash" || toolName === "shell") {
2566
3038
  const inputObj = typeof toolInput === "string" ? safeJsonParse(toolInput, {}) : toolInput;
2567
3039
  const command = inputObj.command || "";
3040
+ const skillNames = skillNamesFromCommand(command);
2568
3041
  messages.push({
2569
3042
  id: `command-${event.timestamp}-${messages.length}`,
2570
3043
  type: "command",
2571
3044
  command,
3045
+ ...skillNames.length > 0 ? { skillNames } : {},
2572
3046
  output: "",
2573
3047
  status: "in_progress",
2574
3048
  timestamp: event.timestamp
@@ -2635,7 +3109,7 @@ function parseClaudeEvents(events, parentToolUseId) {
2635
3109
  } else if (toolName === "Skill") {
2636
3110
  const inputObj = typeof toolInput === "string" ? safeJsonParse(toolInput, {}) : toolInput;
2637
3111
  messages.push({
2638
- id: `skill-${event.timestamp}-${messages.length}`,
3112
+ id: `skill-${toolUseId}`,
2639
3113
  type: "skill",
2640
3114
  skillName: inputObj.skill || "unknown",
2641
3115
  args: inputObj.args,
@@ -2644,19 +3118,37 @@ function parseClaudeEvents(events, parentToolUseId) {
2644
3118
  });
2645
3119
  } else if (toolName === "mcp__relay-subagent-tools__spawn_agent") {
2646
3120
  const inputObj = typeof toolInput === "string" ? safeJsonParse(toolInput, {}) : toolInput;
3121
+ const provider = typeof inputObj.provider === "string" ? inputObj.provider : "unknown";
3122
+ const prompt = typeof inputObj.prompt === "string" ? inputObj.prompt : "";
3123
+ const model = typeof inputObj.model === "string" ? inputObj.model : void 0;
3124
+ const title = typeof inputObj.title === "string" ? inputObj.title : void 0;
2647
3125
  messages.push({
2648
3126
  id: `subagent-${event.timestamp}-${messages.length}`,
2649
3127
  type: "subagent",
2650
3128
  toolUseId,
2651
- description: inputObj.title || `Relay subagent (${inputObj.provider || "unknown"})`,
2652
- prompt: inputObj.prompt || "",
2653
- subagentType: inputObj.provider || "unknown",
2654
- model: inputObj.model,
3129
+ description: title || `Relay subagent (${provider})`,
3130
+ prompt,
3131
+ subagentType: provider,
3132
+ model,
3133
+ thinkingLevel: isValidThinkingLevel(inputObj.thinking_level) ? inputObj.thinking_level : void 0,
2655
3134
  status: "in_progress",
2656
3135
  nestedEvents: [],
2657
3136
  isChatBased: true,
2658
3137
  timestamp: event.timestamp
2659
3138
  });
3139
+ } else if (toolName === "mcp__relay-subagent-tools__message_agent") {
3140
+ const inputObj = typeof toolInput === "string" ? safeJsonParse(toolInput, {}) : toolInput;
3141
+ messages.push({
3142
+ id: `subagent-followup-${event.timestamp}-${messages.length}`,
3143
+ type: "subagent_followup",
3144
+ toolUseId,
3145
+ chatId: typeof inputObj.chatId === "string" ? inputObj.chatId : "",
3146
+ message: typeof inputObj.message === "string" ? inputObj.message : "",
3147
+ model: typeof inputObj.model === "string" ? inputObj.model : void 0,
3148
+ thinkingLevel: isValidThinkingLevel(inputObj.thinking_level) ? inputObj.thinking_level : void 0,
3149
+ status: "in_progress",
3150
+ timestamp: event.timestamp
3151
+ });
2660
3152
  } else if (toolName === "mcp__relay-subagent-tools__delete_agent") {
2661
3153
  const inputObj = typeof toolInput === "string" ? safeJsonParse(toolInput, {}) : toolInput;
2662
3154
  const mcp = parseMcpToolName(toolName);
@@ -2684,14 +3176,13 @@ function parseClaudeEvents(events, parentToolUseId) {
2684
3176
  });
2685
3177
  }
2686
3178
  if (event.type === "claude-result") {
2687
- const payload = event.payload;
2688
- if (payload.is_error || payload.subtype !== "success") {
2689
- const errorList = payload.errors || [];
2690
- const errorMessage = errorList.length > 0 ? errorList.join("\n") : "Claude session encountered an unexpected error.";
3179
+ const payload = coerceClaudeResultPayload(event.payload);
3180
+ if (isClaudeResultError(payload)) {
3181
+ const genuineErrors = stripAgentDiagnosticErrors(payload.errors ?? []);
2691
3182
  messages.push({
2692
3183
  id: `error-${event.timestamp}`,
2693
3184
  type: "error",
2694
- message: errorMessage,
3185
+ message: genuineErrors.length > 0 ? genuineErrors.join("\n") : "Claude session encountered an unexpected error.",
2695
3186
  timestamp: event.timestamp
2696
3187
  });
2697
3188
  }
@@ -2800,10 +3291,21 @@ function parseClaudeEvents(events, parentToolUseId) {
2800
3291
  message.chatId = parsed.chatId;
2801
3292
  }
2802
3293
  }
3294
+ } else if (message.type === "subagent_followup") {
3295
+ message.output = resultContent;
3296
+ message.status = status;
2803
3297
  }
2804
3298
  }
2805
3299
  }
2806
3300
  });
3301
+ for (const message of messages) {
3302
+ if (message.type !== "subagent_followup") continue;
3303
+ const target = messages.find((candidate) => candidate.type === "subagent" && candidate.chatId === message.chatId);
3304
+ if (target?.type === "subagent") {
3305
+ message.targetTitle = target.description;
3306
+ message.targetProvider = target.subagentType;
3307
+ }
3308
+ }
2807
3309
  const staleIndexes = /* @__PURE__ */ new Set();
2808
3310
  for (const streamId of [...completedStreamIds, ...supersededStreamIds]) {
2809
3311
  const refs = partialIndexes.get(streamId);
@@ -2828,16 +3330,18 @@ function parseAgentEvents(events, agentType) {
2828
3330
  if (agentType === "opencode") {
2829
3331
  return parseOpencodeEvents(events);
2830
3332
  }
3333
+ if (agentType === "pi") {
3334
+ return parsePiEvents(events);
3335
+ }
2831
3336
  return parseClaudeEvents(events);
2832
3337
  }
2833
3338
 
2834
3339
  // ../shared/src/routes/workspaces.ts
2835
3340
  var WORKSPACE_STATUSES = ["active", "sleeping", "archived", "preparing", "error"];
2836
- var VALID_LIFECYCLE_POLICIES = ["default", "delete_when_done", "sleep_when_done", "delete_after_inactivity"];
2837
- var WORKSPACE_FILE_UPLOAD_MAX_SIZE_BYTES = 20 * 1024 * 1024;
2838
- var WORKSPACE_FILE_CONTENT_MAX_SIZE_BYTES = 1 * 1024 * 1024;
3341
+ var VALID_LIFECYCLE_POLICIES = ["default", "archive_when_done", "sleep_when_done", "delete_after_inactivity"];
2839
3342
  var WORKSPACE_STATUS_FILTERS = WORKSPACE_STATUSES.map((status) => status === "error" ? "failed" : status);
2840
3343
  var WORKSPACE_SORT_CHOICES = [
3344
+ ["manual", "Custom"],
2841
3345
  ["activity", "Last activity"],
2842
3346
  ["created", "Created date"],
2843
3347
  ["status", "Status"]
@@ -2892,24 +3396,24 @@ function result(data, isError = false) {
2892
3396
  }
2893
3397
  var server = new McpServer({
2894
3398
  name: "replicas-mcp",
2895
- version: "0.2.10"
3399
+ version: "0.2.12"
2896
3400
  });
2897
3401
  var registerTool = server.registerTool.bind(server);
2898
- var codingAgentSchema = z.string().refine(isValidCodingAgentProvider, {
3402
+ var codingAgentSchema = z2.string().refine(isValidCodingAgentProvider, {
2899
3403
  message: `Coding agent must be ${getCodingAgentDisplayNames()}`
2900
3404
  });
2901
- var lifecyclePolicySchema = z.enum(VALID_LIFECYCLE_POLICIES);
3405
+ var lifecyclePolicySchema = z2.enum(VALID_LIFECYCLE_POLICIES);
2902
3406
  registerTool(
2903
3407
  "create_replica",
2904
3408
  {
2905
3409
  description: "Create a new replica workspace and send initial message",
2906
3410
  inputSchema: {
2907
- name: z.string().regex(/^\S+$/, "Name cannot contain spaces"),
2908
- message: z.string(),
2909
- repository_ids: z.array(z.string()).min(1).optional().describe("Repository IDs to include. Mutually exclusive with repository_set_id."),
2910
- repository_set_id: z.string().optional().describe("Repository set ID to use. Mutually exclusive with repository_ids."),
3411
+ name: z2.string().regex(/^\S+$/, "Name cannot contain spaces"),
3412
+ message: z2.string(),
3413
+ repository_ids: z2.array(z2.string()).min(1).optional().describe("Repository IDs to include. Mutually exclusive with repository_set_id."),
3414
+ repository_set_id: z2.string().optional().describe("Repository set ID to use. Mutually exclusive with repository_ids."),
2911
3415
  coding_agent: codingAgentSchema.optional().describe("Coding agent to use (default: claude)"),
2912
- model: z.string().optional().describe("Model to use for the coding agent"),
3416
+ model: z2.string().optional().describe("Model to use for the coding agent"),
2913
3417
  lifecycle_policy: lifecyclePolicySchema.optional().describe("Lifecycle policy for the workspace")
2914
3418
  }
2915
3419
  },
@@ -2935,11 +3439,11 @@ registerTool(
2935
3439
  {
2936
3440
  description: "List all replicas with pagination and optional filters",
2937
3441
  inputSchema: {
2938
- page: z.number().optional(),
2939
- limit: z.number().optional(),
2940
- user_id: z.string().optional().describe("Filter by creator user ID"),
2941
- source: z.enum(["api", "dashboard", "slack", "linear", "github", "gitlab", "automation"]).optional().describe("Filter by creation source"),
2942
- status: z.enum(["active", "sleeping", "preparing"]).optional().describe("Filter by workspace status")
3442
+ page: z2.number().optional(),
3443
+ limit: z2.number().optional(),
3444
+ user_id: z2.string().optional().describe("Filter by creator user ID"),
3445
+ source: z2.enum(["api", "dashboard", "slack", "linear", "github", "gitlab", "automation"]).optional().describe("Filter by creation source"),
3446
+ status: z2.enum(["active", "sleeping", "preparing"]).optional().describe("Filter by workspace status")
2943
3447
  }
2944
3448
  },
2945
3449
  async (args) => {
@@ -2962,8 +3466,8 @@ registerTool(
2962
3466
  {
2963
3467
  description: "Get replica details by ID, including current status, chats, and repository statuses",
2964
3468
  inputSchema: {
2965
- id: z.string(),
2966
- include: z.array(z.enum(["environment", "diffs"])).optional().describe("Extra data to include in response")
3469
+ id: z2.string(),
3470
+ include: z2.array(z2.enum(["environment", "diffs"])).optional().describe("Extra data to include in response")
2967
3471
  }
2968
3472
  },
2969
3473
  async (args) => {
@@ -2982,12 +3486,12 @@ registerTool(
2982
3486
  {
2983
3487
  description: "Send a message to an existing replica. Automatically handles waking sleeping replicas.",
2984
3488
  inputSchema: {
2985
- id: z.string(),
2986
- message: z.string(),
3489
+ id: z2.string(),
3490
+ message: z2.string(),
2987
3491
  coding_agent: codingAgentSchema.optional().describe("Coding agent to use"),
2988
- chat_id: z.string().optional().describe("Chat ID to send the message to (uses default chat if omitted)"),
2989
- model: z.string().optional().describe("Model to use"),
2990
- plan_mode: z.boolean().optional().describe("Whether to use plan mode")
3492
+ chat_id: z2.string().optional().describe("Chat ID to send the message to (uses default chat if omitted)"),
3493
+ model: z2.string().optional().describe("Model to use"),
3494
+ plan_mode: z2.boolean().optional().describe("Whether to use plan mode")
2991
3495
  }
2992
3496
  },
2993
3497
  async (args) => {
@@ -3008,7 +3512,7 @@ registerTool(
3008
3512
  {
3009
3513
  description: "Delete a replica. This terminates the workspace and cannot be undone.",
3010
3514
  inputSchema: {
3011
- id: z.string()
3515
+ id: z2.string()
3012
3516
  }
3013
3517
  },
3014
3518
  async (args) => {
@@ -3050,7 +3554,7 @@ registerTool(
3050
3554
  {
3051
3555
  description: "List chats in a replica workspace",
3052
3556
  inputSchema: {
3053
- id: z.string().describe("Replica ID")
3557
+ id: z2.string().describe("Replica ID")
3054
3558
  }
3055
3559
  },
3056
3560
  async (args) => {
@@ -3066,9 +3570,9 @@ registerTool(
3066
3570
  {
3067
3571
  description: "Create a new chat in a replica workspace",
3068
3572
  inputSchema: {
3069
- id: z.string().describe("Replica ID"),
3573
+ id: z2.string().describe("Replica ID"),
3070
3574
  provider: codingAgentSchema.describe("Coding agent provider for the chat"),
3071
- title: z.string().optional().describe("Optional title for the chat")
3575
+ title: z2.string().optional().describe("Optional title for the chat")
3072
3576
  }
3073
3577
  },
3074
3578
  async (args) => {
@@ -3086,8 +3590,8 @@ registerTool(
3086
3590
  {
3087
3591
  description: "Delete a chat from a replica workspace",
3088
3592
  inputSchema: {
3089
- id: z.string().describe("Replica ID"),
3090
- chat_id: z.string().describe("Chat ID to delete")
3593
+ id: z2.string().describe("Replica ID"),
3594
+ chat_id: z2.string().describe("Chat ID to delete")
3091
3595
  }
3092
3596
  },
3093
3597
  async (args) => {
@@ -3103,8 +3607,8 @@ registerTool(
3103
3607
  {
3104
3608
  description: "Read conversation history of a replica. Returns parsed DisplayMessage objects (user messages, agent responses, commands, file changes, tool calls, etc). If the workspace is sleeping, it will be woken and you should retry in 30-90 seconds.",
3105
3609
  inputSchema: {
3106
- id: z.string(),
3107
- chat_id: z.string().optional().describe("Read history for a specific chat (uses default chat if omitted)")
3610
+ id: z2.string(),
3611
+ chat_id: z2.string().optional().describe("Read history for a specific chat (uses default chat if omitted)")
3108
3612
  }
3109
3613
  },
3110
3614
  async (args) => {
@@ -3138,7 +3642,7 @@ registerTool(
3138
3642
  {
3139
3643
  description: "Get warm and start hook logs for a replica workspace",
3140
3644
  inputSchema: {
3141
- id: z.string().describe("Replica ID")
3645
+ id: z2.string().describe("Replica ID")
3142
3646
  }
3143
3647
  },
3144
3648
  async (args) => {