replicas-mcp 0.2.11 → 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 +1388 -884
- package/package.json +3 -2
- package/src/index.ts +1 -1
- package/tsconfig.json +4 -4
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
|
|
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 !==
|
|
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
|
|
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,
|
|
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)
|
|
83
|
-
replicas computer
|
|
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)
|
|
87
|
-
replicas computer
|
|
88
|
-
replicas computer
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
123
|
-
|
|
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-
|
|
126
|
-
|
|
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
|
|
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,
|
|
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
|
-
###
|
|
175
|
-
|
|
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
|
|
179
|
-
replicas computer
|
|
180
|
-
|
|
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
|
-
|
|
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
|
|
193
|
-
replicas computer browser --
|
|
194
|
-
replicas computer browser-
|
|
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 \`\` 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 \`\` line in your chat reply. See \`MEDI
|
|
|
228
349
|
|
|
229
350
|
| Component | Where |
|
|
230
351
|
|---|---|
|
|
231
|
-
| \`xvfb\`, \`openbox\`, \`tint2\`, \`
|
|
232
|
-
|
|
|
233
|
-
|
|
|
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
|
|
241
|
-
bullet: "- Driving the workspace Linux desktop (open a browser, click, type, scroll, screenshot, record) and exposing a live
|
|
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
|
|
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
|
|
506
|
+
For operations not covered by \`gh\` subcommands, use the scoped SDK. Pass only a relative GitHub API path:
|
|
385
507
|
|
|
386
|
-
\`\`\`
|
|
387
|
-
|
|
388
|
-
gh api repos/owner/repo/pulls/123/comments
|
|
508
|
+
\`\`\`ts
|
|
509
|
+
import { replicas } from '@replicas/sdk';
|
|
389
510
|
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
394
|
-
|
|
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
|
-
-
|
|
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: "
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
582
|
+
Pass a relative path below \`/api/v4\`. Replicas selects the credential only from GitLab hosts attached to the workspace repositories.
|
|
465
583
|
|
|
466
|
-
|
|
584
|
+
\`\`\`ts
|
|
585
|
+
import { replicas } from '@replicas/sdk';
|
|
467
586
|
|
|
468
|
-
|
|
587
|
+
const project = encodeURIComponent('group/project');
|
|
588
|
+
const mergeRequests = await replicas.gitlab.request(
|
|
589
|
+
\`/projects/\${project}/merge_requests?state=opened\`,
|
|
590
|
+
);
|
|
469
591
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
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: "
|
|
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/
|
|
486
|
-
var SECTION5 = `###
|
|
487
|
-
|
|
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
|
-
|
|
638
|
-
|
|
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
|
-
|
|
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
|
-
|
|
627
|
+
All writes are proposals: a human reviews them in the dashboard, and nothing takes effect until approved.
|
|
733
628
|
|
|
734
|
-
|
|
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
|
-
|
|
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
|
-
|
|
747
|
-
|
|
748
|
-
-
|
|
749
|
-
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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: "
|
|
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
|
|
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
|
|
769
|
-
- You need to
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
661
|
+
\`\`\`ts
|
|
662
|
+
import { replicas } from '@replicas/sdk';
|
|
823
663
|
|
|
824
|
-
|
|
825
|
-
|
|
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
|
-
|
|
668
|
+
## Fetch an issue
|
|
832
669
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
}
|
|
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
|
-
|
|
682
|
+
## Update and comment in one script
|
|
843
683
|
|
|
844
|
-
\`\`\`
|
|
845
|
-
|
|
846
|
-
query:
|
|
847
|
-
|
|
848
|
-
}
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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. \`\` \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>?
|
|
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
|
-
|
|
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 \`\`) | 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/
|
|
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
|
|
807
|
+
### ALWAYS link a Replicas web URL \u2014 never the authenticated API URL \u2014 when referencing media externally
|
|
925
808
|
|
|
926
|
-
|
|
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>?
|
|
812
|
+
https://tryreplicas.com/workspaces/<workspace-id>?media=<media-id>
|
|
930
813
|
\`\`\`
|
|
931
814
|
|
|
932
|
-
|
|
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
|
-
|
|
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
|
|
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** (\`?
|
|
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
|
-
|
|
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
|
|
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
|
|
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>?
|
|
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 "$
|
|
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: $
|
|
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 '\\n\\n[View in Replicas](https://tryreplicas.com/workspaces/%s?
|
|
1052
|
-
"$ASSET_URL" "$
|
|
932
|
+
BODY=$(printf '\\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
|
|
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
|
-
|
|
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
|
-
[
|
|
964
|
+
[](<recording-dashboard-url>)
|
|
1073
965
|
\`\`\`
|
|
1074
966
|
|
|
1075
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1158
|
-
2.
|
|
1159
|
-
3.
|
|
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
|
-
|
|
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/
|
|
1226
|
-
var SECTION9 = `###
|
|
1227
|
-
|
|
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/
|
|
1124
|
+
**Reference:** \`references/PLUGINS.md\`
|
|
1230
1125
|
|
|
1231
1126
|
Use this when:
|
|
1232
|
-
- The user asks
|
|
1233
|
-
- The user asks
|
|
1234
|
-
- The user asks
|
|
1235
|
-
- The user asks
|
|
1236
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
1138
|
+
## Discover installed plugins
|
|
1243
1139
|
|
|
1244
|
-
|
|
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,13 +1343,15 @@ 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
|
|
@@ -1385,7 +1359,7 @@ replicas automation create ... --disabled
|
|
|
1385
1359
|
# Workspace lifecycle
|
|
1386
1360
|
replicas automation create ... --lifecycle archive_when_done
|
|
1387
1361
|
replicas automation create ... --lifecycle sleep_when_done
|
|
1388
|
-
replicas automation create ... --lifecycle
|
|
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:
|
|
1439
|
-
referenceFile: { name: "REPLICAS.md", content:
|
|
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/
|
|
1444
|
-
var
|
|
1445
|
-
|
|
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/
|
|
1441
|
+
**Reference:** \`references/SENTRY.md\`
|
|
1448
1442
|
|
|
1449
1443
|
Use this when:
|
|
1450
|
-
- You need to
|
|
1451
|
-
-
|
|
1452
|
-
|
|
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
|
-
|
|
1448
|
+
Use \`@replicas/sdk\`; never request or read a Sentry token. Sentry access is organization-scoped and read-only.
|
|
1457
1449
|
|
|
1458
|
-
|
|
1450
|
+
\`\`\`ts
|
|
1451
|
+
import { replicas } from '@replicas/sdk';
|
|
1459
1452
|
|
|
1460
|
-
|
|
1453
|
+
const { native } = await replicas.integrations.list();
|
|
1454
|
+
if (!native.sentry) throw new Error('Connect Sentry in Replicas organization integrations first.');
|
|
1461
1455
|
|
|
1462
|
-
|
|
1463
|
-
echo "\${SLACK_BOT_TOKEN:+set}"
|
|
1456
|
+
const issues = await replicas.sentry.request('/organizations/acme/issues/?query=is:unresolved');
|
|
1464
1457
|
\`\`\`
|
|
1465
1458
|
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1473
|
+
**Reference:** \`references/SLACK.md\`
|
|
1474
1474
|
|
|
1475
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1485
|
+
\`\`\`ts
|
|
1486
|
+
import { replicas } from '@replicas/sdk';
|
|
1486
1487
|
|
|
1487
|
-
|
|
1488
|
-
|
|
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
|
-
|
|
1492
|
+
## Read a thread
|
|
1499
1493
|
|
|
1500
|
-
|
|
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
|
-
\`\`\`
|
|
1503
|
-
|
|
1504
|
-
|
|
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
|
-
|
|
1504
|
+
## Send and attach a message
|
|
1508
1505
|
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1537
|
+
## Search and batch
|
|
1546
1538
|
|
|
1547
|
-
|
|
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
|
-
|
|
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
|
|
1550
|
+
description: "Send messages, read threads, and search through @replicas/sdk.",
|
|
1554
1551
|
bullet: "- Interacting with Slack (sending messages, reading threads, etc.)",
|
|
1555
|
-
section:
|
|
1556
|
-
referenceFile: { name: "SLACK.md", content:
|
|
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
|
-
|
|
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
|
|
1578
|
-
|
|
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:
|
|
1620
|
+
abilities: defaultReplicasAgentAbilities()
|
|
1584
1621
|
}
|
|
1585
1622
|
};
|
|
1586
1623
|
|
|
1587
|
-
// ../shared/src/
|
|
1588
|
-
var
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
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
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
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
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
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/
|
|
1618
|
-
function
|
|
1619
|
-
|
|
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
|
|
1797
|
-
|
|
1798
|
-
|
|
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
|
|
1902
|
-
for (const index of
|
|
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
|
|
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 =
|
|
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
|
-
|
|
2537
|
+
callIndexes.set(callId, messages.push(next) - 1);
|
|
2019
2538
|
} else {
|
|
2020
|
-
messages[existing] =
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2692
|
+
upsertDisplayMessage(messages, createCallDisplayMessage({
|
|
2162
2693
|
id,
|
|
2163
|
-
type: "tool_call",
|
|
2164
2694
|
server: "opencode",
|
|
2165
|
-
tool
|
|
2166
|
-
input: isRecord(
|
|
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
|
-
|
|
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/
|
|
2190
|
-
|
|
2191
|
-
|
|
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
|
|
2284
|
-
|
|
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
|
|
2289
|
-
|
|
2290
|
-
|
|
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
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
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
|
-
|
|
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 (
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
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
|
-
|
|
2758
|
+
continue;
|
|
2343
2759
|
}
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
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
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
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 (
|
|
2371
|
-
|
|
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
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
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
|
-
|
|
2480
|
-
|
|
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-${
|
|
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:
|
|
2652
|
-
prompt
|
|
2653
|
-
subagentType:
|
|
2654
|
-
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
|
|
2689
|
-
const
|
|
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:
|
|
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
3341
|
var VALID_LIFECYCLE_POLICIES = ["default", "archive_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;
|
|
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.
|
|
3399
|
+
version: "0.2.12"
|
|
2896
3400
|
});
|
|
2897
3401
|
var registerTool = server.registerTool.bind(server);
|
|
2898
|
-
var codingAgentSchema =
|
|
3402
|
+
var codingAgentSchema = z2.string().refine(isValidCodingAgentProvider, {
|
|
2899
3403
|
message: `Coding agent must be ${getCodingAgentDisplayNames()}`
|
|
2900
3404
|
});
|
|
2901
|
-
var lifecyclePolicySchema =
|
|
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:
|
|
2908
|
-
message:
|
|
2909
|
-
repository_ids:
|
|
2910
|
-
repository_set_id:
|
|
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:
|
|
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:
|
|
2939
|
-
limit:
|
|
2940
|
-
user_id:
|
|
2941
|
-
source:
|
|
2942
|
-
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:
|
|
2966
|
-
include:
|
|
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:
|
|
2986
|
-
message:
|
|
3489
|
+
id: z2.string(),
|
|
3490
|
+
message: z2.string(),
|
|
2987
3491
|
coding_agent: codingAgentSchema.optional().describe("Coding agent to use"),
|
|
2988
|
-
chat_id:
|
|
2989
|
-
model:
|
|
2990
|
-
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:
|
|
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:
|
|
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:
|
|
3573
|
+
id: z2.string().describe("Replica ID"),
|
|
3070
3574
|
provider: codingAgentSchema.describe("Coding agent provider for the chat"),
|
|
3071
|
-
title:
|
|
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:
|
|
3090
|
-
chat_id:
|
|
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:
|
|
3107
|
-
chat_id:
|
|
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:
|
|
3645
|
+
id: z2.string().describe("Replica ID")
|
|
3142
3646
|
}
|
|
3143
3647
|
},
|
|
3144
3648
|
async (args) => {
|