replicas-mcp 0.2.9 → 0.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -61,7 +61,7 @@ The CLI is the canonical surface. Chat transcripts that show \`DISPLAY=:99 xdoto
61
61
  \`\`\`bash
62
62
  # DO
63
63
  replicas computer key Return
64
- replicas computer screenshot /tmp/state.png
64
+ replicas computer observe /tmp/state.png
65
65
 
66
66
  # DON'T
67
67
  DISPLAY=:99 xdotool key Return
@@ -79,9 +79,9 @@ replicas computer info
79
79
  # 2) Launch a browser on the workspace display.
80
80
  replicas computer launch chrome https://news.ycombinator.com
81
81
 
82
- # 3) Take a screenshot so you can see what's there.
83
- replicas computer screenshot /tmp/state.png
84
- # (Read the PNG yourself before deciding where to click.)
82
+ # 3) Observe the settled screen and browser tab state before clicking.
83
+ replicas computer observe /tmp/state.png
84
+ replicas computer browser --snapshot
85
85
 
86
86
  # 4) Drive the UI.
87
87
  replicas computer click 521 700 # click coordinates from the screenshot
@@ -102,16 +102,39 @@ Prints the live noVNC viewer URL (\`https://6080-<hash>.tryreplicas.com/\`) for
102
102
  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
103
 
104
104
  ### \`replicas computer status\`
105
- Prints which desktop services are running and the active preview URL (if any). Useful for debugging when a tool call seems to be doing nothing.
105
+ Checks and repairs the desktop bridge, then prints which desktop services are running and the active preview URL (if any). Useful for debugging when a tool call seems to be doing nothing.
106
106
 
107
- ### \`replicas computer screenshot <path>\`
108
- Captures the current desktop to a PNG at the given path. Read the file (e.g. with your Read tool) to see what's on screen - coordinates from the screenshot drive subsequent \`click\` / \`move\` / \`drag\` calls.
107
+ ### \`replicas computer screenshot <path> [--raw] [--grid [px]]\`
108
+ Captures the current desktop to a PNG at the given path.
109
+
110
+ Use \`--raw\` for a 1:1 desktop capture with no branding, padding, or rounded corners. Use \`--grid\` for the same 1:1 capture plus a coordinate grid; the optional value sets grid spacing in pixels and defaults to 100. Use the default branded screenshot only when you plan to share the image with \`replicas media upload\`.
111
+
112
+ ### \`replicas computer observe <path> [--raw] [--grid [px]] [--timeout MS] [--stable-ms MS] [--poll-ms MS]\`
113
+ Waits briefly for the screen to stop changing, saves a 1:1 screenshot, and prints JSON with screen dimensions, whether the screen stabilized, frame/change counts, mouse location, active window title, and visible window titles.
114
+
115
+ 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
+
117
+ ### \`replicas computer browser\`
118
+ Prints JSON for Chrome tabs launched through Replicas, including page titles and URLs. Pass \`--snapshot\` to include visible page text and interactive controls with DOM viewport bounding boxes.
119
+
120
+ Use this alongside \`observe\` when testing web apps so you do not infer navigation, page state, or click targets from pixels alone. Snapshot coordinates are DOM viewport coordinates, not desktop click coordinates.
121
+
122
+ ### \`replicas computer browser-click <text> [--exact] [--index N]\`
123
+ Clicks the first visible Chrome control whose text, label, placeholder, or href matches \`<text>\`. Use this for web buttons and links found via \`browser --snapshot\`; it is faster and less error-prone than converting DOM coordinates to desktop pixels.
124
+
125
+ ### \`replicas computer browser-fill <field> <value> [--exact] [--index N]\`
126
+ Fills the first visible Chrome field whose label, placeholder, name, or text matches \`<field>\`, then dispatches input/change events. Use this for web forms instead of clicking a field and typing through the desktop.
127
+
128
+ ### \`replicas computer browser-wait <text> [--mode any|text|title|url|control] [--exact] [--timeout MS]\`
129
+ 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
+
131
+ For \`browser-click\`, \`browser-fill\`, and \`browser-wait\`, pass \`--id <id>\`, \`--title <text>\`, \`--url <text>\`, or \`--page <n>\` when multiple Chrome tabs are open. Run \`replicas computer browser\` first to list tabs. Prefer \`--id\` when a click may change the page title or URL.
109
132
 
110
133
  ### \`replicas computer click <x> <y> [--button N] [--double] [--modifiers ctrl+shift]\`
111
- Move to (x, y) and click. 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).
134
+ 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).
112
135
 
113
136
  ### \`replicas computer move <x> <y>\`
114
- Move the mouse without clicking. Useful for hovering tooltips.
137
+ Move the mouse without clicking. Coordinates can be absolute pixels or percentages. Useful for hovering tooltips.
115
138
 
116
139
  ### \`replicas computer type <text> [--delay MS]\`
117
140
  Type a literal string into the focused field. Default per-character delay is 12ms (~80 wpm) - feels human and avoids breaking apps that debounce input. Bump \`--delay 30\` for stricter apps.
@@ -122,10 +145,10 @@ For key combos (not literal text), use \`key\`. \`type "ctrl+l"\` will literally
122
145
  Press a single key or combo. Examples: \`Return\`, \`Escape\`, \`Tab\`, \`ctrl+l\`, \`ctrl+shift+t\`, \`alt+Left\`, \`Page_Down\`, \`Home\`. Syntax matches \`xdotool key\`.
123
146
 
124
147
  ### \`replicas computer scroll <up|down|left|right> [--amount N] [--x X --y Y]\`
125
- Scroll the wheel. Pass \`--x\` / \`--y\` to hover before scrolling (otherwise scrolls wherever the cursor currently is). Default amount is 3 wheel ticks.
148
+ Scroll the wheel. Pass \`--x\` / \`--y\` to hover before scrolling (otherwise scrolls wherever the cursor currently is). Hover coordinates can be absolute pixels or percentages. Default amount is 3 wheel ticks.
126
149
 
127
150
  ### \`replicas computer drag <fromX> <fromY> <toX> <toY>\`
128
- Press left mouse at (fromX, fromY), drag to (toX, toY), release. For things like dragging a file onto an upload zone.
151
+ Press left mouse at (fromX, fromY), drag to (toX, toY), release. Coordinates can be absolute pixels or percentages. For things like dragging a file onto an upload zone.
129
152
 
130
153
  ### \`replicas computer launch <app> [args...]\`
131
154
  Spawns an app on the workspace display. Built-in aliases:
@@ -148,31 +171,37 @@ SIGINTs ffmpeg, waits for it to finalize the MP4, prints the output path. Upload
148
171
 
149
172
  ## Patterns
150
173
 
151
- ### Action / screenshot loop
152
- You are blind between tool calls. After any action that changes the screen, take a screenshot before deciding the next coordinate:
174
+ ### Action / observe loop
175
+ You are blind between tool calls. After any action that changes the screen, observe before deciding the next coordinate:
153
176
 
154
177
  \`\`\`bash
155
178
  replicas computer click 521 700
156
- sleep 2 # let the page settle
157
- replicas computer screenshot /tmp/after-click.png
158
- # read /tmp/after-click.png, decide next click
179
+ replicas computer observe /tmp/after-click.png
180
+ # read /tmp/after-click.png and the JSON output, decide next click
159
181
  \`\`\`
160
182
 
161
- \`sleep\` is a regular shell sleep - there's no \`replicas computer wait\` command, but you can mix shell sleeps freely.
183
+ The JSON \`stable\`, \`frames\`, and \`changes\` fields tell you whether something changed while you were waiting. If \`stable\` is false, observe again or increase \`--timeout\` before acting on coordinates.
162
184
 
163
185
  ### Typing into an address bar
164
186
  \`\`\`bash
165
187
  replicas computer launch chrome
166
- sleep 2
167
- replicas computer key ctrl+l # focus address bar
188
+ replicas computer observe /tmp/browser-open.png
189
+ replicas computer key ctrl+l
168
190
  replicas computer type "https://example.com"
169
191
  replicas computer key Return
170
- sleep 3 # wait for page load
171
- replicas computer screenshot /tmp/loaded.png
192
+ replicas computer observe /tmp/loaded.png
193
+ replicas computer browser --snapshot
194
+ replicas computer browser-fill "Search" "replicas"
195
+ replicas computer browser-click "More information" --title "Example"
196
+ replicas computer browser-wait "Example Domain" --mode title --title "Example"
172
197
  \`\`\`
173
198
 
174
199
  ### Coordinates from screenshots
175
- The display is 1920\xD71080 by default. Screenshot pixels map 1:1 to click coordinates - if your Read tool shows a button at pixel (520, 700), click \`replicas computer click 520 700\`. **No translation needed.** Modern image-reading models often imagine the screenshot is at a different resolution; trust the \`xdpyinfo\` value (\`replicas computer status\` shows the real size).
200
+ The display is 1920\xD71080 by default. For click planning, use \`observe\`, \`screenshot --raw\`, or \`screenshot --grid\`; those pixels map 1:1 to click coordinates. If the grid/raw screenshot shows a button at pixel (520, 700), click \`replicas computer click 520 700\`. The default screenshot is branded for sharing and has padding around the desktop, so do not use it for coordinates.
201
+
202
+ Use percentages for broad, layout-relative targets when exact pixels are unnecessary: \`replicas computer click 50% 50%\` clicks the center of the screen, and \`replicas computer move 95% 5%\` moves near the top-right.
203
+
204
+ Modern image-reading models often imagine the screenshot is at a different resolution. Trust the dimensions printed by \`observe\`, \`replicas computer screenshot ... --raw\` / \`--grid\`, and the \`xdpyinfo\` value shown by \`replicas computer status\`.
176
205
 
177
206
  ### Letting the user watch
178
207
  The Desktop tab is already live in the dashboard - the user can open it any time. If you're communicating with the user somewhere else (Slack, PR comment, etc.), grab the URL with \`replicas computer info\` and share it inline so they can watch you work.
@@ -190,8 +219,8 @@ Then embed the printed \`![\u2026](\u2026)\` line in your chat reply. See \`MEDI
190
219
  ## Failure modes
191
220
 
192
221
  - **"Desktop services script missing"**: workspace image is older than this skill. Tell the user - nothing you can do from the CLI side.
193
- - **\`xdotool ... failed: Can't open display\`**: Xvfb didn't come up. \`replicas computer status\` will show which service is dead. Re-running any CLI command auto-attempts to start it.
194
- - **Browser doesn't appear after \`launch chrome\`**: give it 1-2s, then screenshot. Chrome cold-start on the virtual display takes ~500ms but bigger pages take longer.
222
+ - **\`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
+ - **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.
195
224
  - **Live preview shows static / black screen**: the browser may have crashed. \`replicas computer status\` should show no Chrome process - re-launch.
196
225
  - **\`replicas computer info\` errors with "not registered"**: engine couldn't register the preview at startup (transient monolith error, or warming mode). Re-running the engine usually fixes it. Until it's registered, the Desktop tab will show a placeholder.
197
226
 
@@ -1354,7 +1383,8 @@ replicas automation create "Review my MRs" \\
1354
1383
  replicas automation create ... --disabled
1355
1384
 
1356
1385
  # Workspace lifecycle
1357
- replicas automation create ... --lifecycle delete_when_done
1386
+ replicas automation create ... --lifecycle archive_when_done
1387
+ replicas automation create ... --lifecycle sleep_when_done
1358
1388
  replicas automation create ... --lifecycle delete_after_inactivity --auto-stop-minutes 30
1359
1389
  \`\`\`
1360
1390
 
@@ -2377,6 +2407,11 @@ function parseClaudeEvents(events, parentToolUseId) {
2377
2407
  const toolCallMap = /* @__PURE__ */ new Map();
2378
2408
  const taskMessageMap = /* @__PURE__ */ new Map();
2379
2409
  const partialIndexes = /* @__PURE__ */ new Map();
2410
+ const completedStreamIds = /* @__PURE__ */ new Set();
2411
+ const supersededStreamIds = /* @__PURE__ */ new Set();
2412
+ let liveStreamId = null;
2413
+ const assistantThinking = /* @__PURE__ */ new Map();
2414
+ const assistantTextCounts = /* @__PURE__ */ new Map();
2380
2415
  const taskAccumulator = new TaskAccumulator();
2381
2416
  const taskSnapshot = () => taskAccumulator.getTasks().map((task) => ({
2382
2417
  text: task.subject,
@@ -2387,6 +2422,10 @@ function parseClaudeEvents(events, parentToolUseId) {
2387
2422
  if (event.type === CLAUDE_PARTIAL_MESSAGE_EVENT_TYPE) {
2388
2423
  const payload = coerceClaudePartialMessagePayload(event.payload);
2389
2424
  if (!payload) return;
2425
+ if (liveStreamId !== null && liveStreamId !== payload.streamId) {
2426
+ supersededStreamIds.add(liveStreamId);
2427
+ }
2428
+ liveStreamId = payload.streamId;
2390
2429
  const existing = partialIndexes.get(payload.streamId) ?? {};
2391
2430
  if (payload.thinking) {
2392
2431
  const reasoningMessage = {
@@ -2449,23 +2488,51 @@ function parseClaudeEvents(events, parentToolUseId) {
2449
2488
  if (event.type === "claude-assistant") {
2450
2489
  const messageId = event.payload.message?.id;
2451
2490
  const contentBlocks = normalizeContentBlocks(event.payload.message?.content);
2452
- contentBlocks.forEach((block, blockIndex) => {
2491
+ const messageKey = messageId || event.timestamp;
2492
+ const streamRefs = messageId ? partialIndexes.get(messageId) : void 0;
2493
+ if (messageId) {
2494
+ completedStreamIds.add(messageId);
2495
+ if (liveStreamId !== null && liveStreamId !== messageId) {
2496
+ supersededStreamIds.add(liveStreamId);
2497
+ }
2498
+ liveStreamId = null;
2499
+ }
2500
+ const thinkingBlocks = contentBlocks.flatMap(
2501
+ (block) => block.type === "thinking" && typeof block.thinking === "string" ? [block.thinking] : []
2502
+ );
2503
+ if (thinkingBlocks.length > 0) {
2504
+ const allThinking = [...assistantThinking.get(messageKey) ?? [], ...thinkingBlocks];
2505
+ assistantThinking.set(messageKey, allThinking);
2506
+ const reasoningMessage = {
2507
+ id: `reasoning-${messageKey}-thinking`,
2508
+ type: "reasoning",
2509
+ content: allThinking.join("\n\n"),
2510
+ status: "completed",
2511
+ timestamp: event.timestamp
2512
+ };
2513
+ if (streamRefs?.reasoning !== void 0) {
2514
+ messages[streamRefs.reasoning] = reasoningMessage;
2515
+ streamRefs.reasoning = void 0;
2516
+ } else {
2517
+ upsertDisplayMessage(messages, reasoningMessage);
2518
+ }
2519
+ }
2520
+ contentBlocks.forEach((block) => {
2453
2521
  if (block.type === "text" && block.text) {
2454
- upsertDisplayMessage(messages, {
2455
- id: `agent-${messageId || event.timestamp}-${blockIndex}`,
2522
+ const textIndex = assistantTextCounts.get(messageKey) ?? 0;
2523
+ assistantTextCounts.set(messageKey, textIndex + 1);
2524
+ const agentMessage = {
2525
+ id: `agent-${messageKey}-${textIndex}`,
2456
2526
  type: "agent",
2457
2527
  content: block.text,
2458
2528
  timestamp: event.timestamp
2459
- });
2460
- }
2461
- if (block.type === "thinking" && block.thinking) {
2462
- upsertDisplayMessage(messages, {
2463
- id: `reasoning-${messageId || event.timestamp}-thinking`,
2464
- type: "reasoning",
2465
- content: block.thinking,
2466
- status: "completed",
2467
- timestamp: event.timestamp
2468
- });
2529
+ };
2530
+ if (streamRefs?.agent !== void 0) {
2531
+ messages[streamRefs.agent] = agentMessage;
2532
+ streamRefs.agent = void 0;
2533
+ } else {
2534
+ upsertDisplayMessage(messages, agentMessage);
2535
+ }
2469
2536
  }
2470
2537
  if (block.type === "tool_use" && block.id) {
2471
2538
  const toolName = block.name || "unknown";
@@ -2737,7 +2804,14 @@ function parseClaudeEvents(events, parentToolUseId) {
2737
2804
  }
2738
2805
  }
2739
2806
  });
2740
- return messages;
2807
+ const staleIndexes = /* @__PURE__ */ new Set();
2808
+ for (const streamId of [...completedStreamIds, ...supersededStreamIds]) {
2809
+ const refs = partialIndexes.get(streamId);
2810
+ if (refs?.reasoning !== void 0) staleIndexes.add(refs.reasoning);
2811
+ if (refs?.agent !== void 0) staleIndexes.add(refs.agent);
2812
+ }
2813
+ if (staleIndexes.size === 0) return messages;
2814
+ return messages.filter((_, index) => !staleIndexes.has(index));
2741
2815
  }
2742
2816
 
2743
2817
  // ../shared/src/display-message/parsers/codex-asp-parser.ts
@@ -2757,6 +2831,34 @@ function parseAgentEvents(events, agentType) {
2757
2831
  return parseClaudeEvents(events);
2758
2832
  }
2759
2833
 
2834
+ // ../shared/src/routes/workspaces.ts
2835
+ var WORKSPACE_STATUSES = ["active", "sleeping", "archived", "preparing", "error"];
2836
+ 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
+ var WORKSPACE_STATUS_FILTERS = WORKSPACE_STATUSES.map((status) => status === "error" ? "failed" : status);
2840
+ var WORKSPACE_SORT_CHOICES = [
2841
+ ["activity", "Last activity"],
2842
+ ["created", "Created date"],
2843
+ ["status", "Status"]
2844
+ ];
2845
+ var WORKSPACE_SORT_OPTIONS = WORKSPACE_SORT_CHOICES.map(([option]) => option);
2846
+ var WORKSPACE_SORT_LABELS = Object.fromEntries(WORKSPACE_SORT_CHOICES);
2847
+ var WORKSPACE_SIDEBAR_ORGANIZATION_CHOICES = [
2848
+ ["chronological", "Chronological"],
2849
+ ["environments", "Environments"],
2850
+ ["recent", "Recent environments"]
2851
+ ];
2852
+ var WORKSPACE_SIDEBAR_ORGANIZATIONS = WORKSPACE_SIDEBAR_ORGANIZATION_CHOICES.map(([organization]) => organization);
2853
+ var WORKSPACE_SIDEBAR_ORGANIZATION_LABELS = Object.fromEntries(WORKSPACE_SIDEBAR_ORGANIZATION_CHOICES);
2854
+ var DEFAULT_STATUS_FILTERS = [...WORKSPACE_STATUS_FILTERS];
2855
+ var DEFAULT_WORKSPACE_FILTERS = {
2856
+ statuses: [...DEFAULT_STATUS_FILTERS],
2857
+ ownerIds: [],
2858
+ activity: "any"
2859
+ };
2860
+ var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
2861
+
2760
2862
  // src/index.ts
2761
2863
  var API_URL = process.env.REPLICAS_API_URL || "https://api.tryreplicas.com";
2762
2864
  var API_KEY = process.env.REPLICAS_API_KEY;
@@ -2790,12 +2892,13 @@ function result(data, isError = false) {
2790
2892
  }
2791
2893
  var server = new McpServer({
2792
2894
  name: "replicas-mcp",
2793
- version: "0.2.9"
2895
+ version: "0.2.11"
2794
2896
  });
2795
2897
  var registerTool = server.registerTool.bind(server);
2796
2898
  var codingAgentSchema = z.string().refine(isValidCodingAgentProvider, {
2797
2899
  message: `Coding agent must be ${getCodingAgentDisplayNames()}`
2798
2900
  });
2901
+ var lifecyclePolicySchema = z.enum(VALID_LIFECYCLE_POLICIES);
2799
2902
  registerTool(
2800
2903
  "create_replica",
2801
2904
  {
@@ -2807,7 +2910,7 @@ registerTool(
2807
2910
  repository_set_id: z.string().optional().describe("Repository set ID to use. Mutually exclusive with repository_ids."),
2808
2911
  coding_agent: codingAgentSchema.optional().describe("Coding agent to use (default: claude)"),
2809
2912
  model: z.string().optional().describe("Model to use for the coding agent"),
2810
- lifecycle_policy: z.enum(["default", "delete_when_done", "delete_after_inactivity"]).optional().describe("Lifecycle policy for the workspace")
2913
+ lifecycle_policy: lifecyclePolicySchema.optional().describe("Lifecycle policy for the workspace")
2811
2914
  }
2812
2915
  },
2813
2916
  async (args) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-mcp",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/index.ts CHANGED
@@ -14,6 +14,9 @@ import {
14
14
  import {
15
15
  type ReadReplicaHistoryResponse,
16
16
  } from '../../shared/src/routes/replica';
17
+ import {
18
+ VALID_LIFECYCLE_POLICIES,
19
+ } from '../../shared/src/routes/workspaces';
17
20
 
18
21
  const API_URL = process.env.REPLICAS_API_URL || 'https://api.tryreplicas.com';
19
22
  const API_KEY = process.env.REPLICAS_API_KEY;
@@ -68,13 +71,14 @@ type RegisterToolFn = (
68
71
 
69
72
  const server = new McpServer({
70
73
  name: 'replicas-mcp',
71
- version: '0.2.9',
74
+ version: '0.2.11',
72
75
  });
73
76
 
74
77
  const registerTool = server.registerTool.bind(server) as RegisterToolFn;
75
78
  const codingAgentSchema = z.string().refine(isValidCodingAgentProvider, {
76
79
  message: `Coding agent must be ${getCodingAgentDisplayNames()}`,
77
80
  });
81
+ const lifecyclePolicySchema = z.enum(VALID_LIFECYCLE_POLICIES);
78
82
 
79
83
  // ---------------------------------------------------------------------------
80
84
  // Replica CRUD
@@ -91,7 +95,7 @@ registerTool(
91
95
  repository_set_id: z.string().optional().describe('Repository set ID to use. Mutually exclusive with repository_ids.'),
92
96
  coding_agent: codingAgentSchema.optional().describe('Coding agent to use (default: claude)'),
93
97
  model: z.string().optional().describe('Model to use for the coding agent'),
94
- lifecycle_policy: z.enum(['default', 'delete_when_done', 'delete_after_inactivity']).optional().describe('Lifecycle policy for the workspace'),
98
+ lifecycle_policy: lifecyclePolicySchema.optional().describe('Lifecycle policy for the workspace'),
95
99
  },
96
100
  },
97
101
  async (args) => {