replicas-cli 0.3.102 → 0.3.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.cjs +424 -94
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -7363,9 +7363,9 @@ var require_dist = __commonJS({
7363
7363
 
7364
7364
  // src/index.ts
7365
7365
  var import_config20 = require("dotenv/config");
7366
- var import_node_child_process3 = require("child_process");
7366
+ var import_node_child_process4 = require("child_process");
7367
7367
  var import_commander = require("commander");
7368
- var import_chalk31 = __toESM(require("chalk"));
7368
+ var import_chalk32 = __toESM(require("chalk"));
7369
7369
 
7370
7370
  // ../shared/src/urls.ts
7371
7371
  var DEFAULT_WEB_APP_URL = "https://app.replicas.dev";
@@ -22611,9 +22611,9 @@ var EXT_TO_LANGUAGE = {
22611
22611
  function detectLanguageByPath(filePath) {
22612
22612
  const dot = filePath.lastIndexOf(".");
22613
22613
  if (dot === -1) {
22614
- const basename = filePath.split("/").pop() ?? "";
22615
- if (basename === "Dockerfile") return "dockerfile";
22616
- if (basename === "Makefile") return "makefile";
22614
+ const basename2 = filePath.split("/").pop() ?? "";
22615
+ if (basename2 === "Dockerfile") return "dockerfile";
22616
+ if (basename2 === "Makefile") return "makefile";
22617
22617
  return null;
22618
22618
  }
22619
22619
  const ext = filePath.slice(dot).toLowerCase();
@@ -24964,8 +24964,103 @@ var GITLAB_ABILITY = {
24964
24964
  referenceFile: { name: "GITLAB.md", content: REFERENCE5 }
24965
24965
  };
24966
24966
 
24967
+ // ../shared/src/routes/compute.ts
24968
+ var COMPUTE_SANDBOX_KINDS = ["gpu"];
24969
+ var GPU_TYPES = ["T4", "L4", "A10G", "L40S", "A100", "A100-80GB", "H100", "H200", "B200"];
24970
+ var DEFAULT_GPU_TYPE = "L4";
24971
+ var DEFAULT_GPU_IMAGE = "pytorch/pytorch:2.5.1-cuda12.4-cudnn9-runtime";
24972
+ var COMPUTE_SANDBOX_WORKDIR = "/workspace";
24973
+ var COMPUTE_SANDBOX_LIMITS = {
24974
+ maxActivePerWorkspace: 3,
24975
+ maxActivePerOrganization: 10,
24976
+ maxGpuCount: 8,
24977
+ maxPorts: 5,
24978
+ defaultTimeoutMinutes: 60,
24979
+ maxTimeoutMinutes: 24 * 60,
24980
+ defaultIdleTimeoutMinutes: 10,
24981
+ maxIdleTimeoutMinutes: 60,
24982
+ maxTransferBytes: 1024 ** 3
24983
+ };
24984
+ var computeEnvSchema = external_exports.record(external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).max(128), external_exports.string().max(32768)).refine((env) => Object.keys(env).length <= 64, "At most 64 environment variables are allowed");
24985
+ var createComputeSandboxRequestSchema = external_exports.object({
24986
+ kind: external_exports.enum(COMPUTE_SANDBOX_KINDS),
24987
+ gpu: external_exports.string().trim().optional(),
24988
+ image: external_exports.string().trim().min(1).max(256).regex(/^\S+$/, "Image must not contain whitespace").optional(),
24989
+ timeoutMinutes: external_exports.number().int().min(1).max(COMPUTE_SANDBOX_LIMITS.maxTimeoutMinutes).optional(),
24990
+ idleTimeoutMinutes: external_exports.number().int().min(1).max(COMPUTE_SANDBOX_LIMITS.maxIdleTimeoutMinutes).optional(),
24991
+ ports: external_exports.array(external_exports.number().int().min(1).max(65535)).max(COMPUTE_SANDBOX_LIMITS.maxPorts).optional(),
24992
+ env: computeEnvSchema.optional()
24993
+ });
24994
+ var computeExecRequestSchema = external_exports.object({
24995
+ command: external_exports.string().min(1).max(1e5),
24996
+ workdir: external_exports.string().min(1).max(4096).optional(),
24997
+ env: computeEnvSchema.optional(),
24998
+ detach: external_exports.boolean().optional()
24999
+ });
25000
+
25001
+ // ../shared/src/default-skills/replicas-agent/abilities/gpu.ts
25002
+ var SECTION6 = `### GPU sandboxes
25003
+ Run GPU workloads (training, inference, CUDA builds, rendering) on cloud GPU sandboxes with \`replicas gpu\`. The workspace itself has no GPU.
25004
+
25005
+ **Reference:** \`references/GPU.md\`
25006
+
25007
+ Use this when:
25008
+ - A task needs a GPU: training or fine-tuning, model inference, CUDA code, benchmarks, or GPU rendering
25009
+ - You need to verify GPU code actually runs instead of only writing it`;
25010
+ var REFERENCE6 = `# GPU sandboxes
25011
+
25012
+ \`replicas gpu\` starts cloud GPU sandboxes and runs commands in them. It is authenticated automatically, and usage is billed at cost to the organization, so stop sandboxes when you are done.
25013
+
25014
+ ## One-shot runs
25015
+
25016
+ \`\`\`bash
25017
+ replicas gpu run --gpu H100 --pull outputs -- python train.py
25018
+ \`\`\`
25019
+
25020
+ This starts a sandbox, copies the current directory to \`${COMPUTE_SANDBOX_WORKDIR}\` (git repositories honor \`.gitignore\`), streams the command's output, copies \`${COMPUTE_SANDBOX_WORKDIR}/outputs\` back to \`./outputs\`, stops the sandbox, and exits with the command's exit code. Add \`--keep\` to leave it running.
25021
+
25022
+ ## Interactive sessions
25023
+
25024
+ \`\`\`bash
25025
+ replicas gpu create --gpu A100-80GB --timeout 3h # waits until running
25026
+ replicas gpu push # . -> ${COMPUTE_SANDBOX_WORKDIR}
25027
+ replicas gpu exec -- nvidia-smi
25028
+ replicas gpu exec -- pip install -r requirements.txt
25029
+ replicas gpu exec --detach -- python train.py # prints a job ID
25030
+ replicas gpu logs <jobId> -f # streams until the job exits
25031
+ replicas gpu pull ${COMPUTE_SANDBOX_WORKDIR}/checkpoints ./
25032
+ replicas gpu delete
25033
+ \`\`\`
25034
+
25035
+ - Commands run with \`sh -c\` in \`${COMPUTE_SANDBOX_WORKDIR}\`; quote shell operators: \`replicas gpu exec -- "cd src && make"\`.
25036
+ - \`--sandbox <id>\` picks a sandbox when more than one is running; \`replicas gpu list\` shows them.
25037
+ - Use \`--detach\` and \`logs -f\` for anything longer than a few minutes. A foreground \`exec\` dies with its connection.
25038
+ - \`--port 8000\` on create exposes a container port as a public HTTPS URL (shown by \`list\`).
25039
+ - \`--env KEY=value\` passes secrets such as \`HF_TOKEN\`; never bake them into files you push.
25040
+
25041
+ ## Choosing hardware and images
25042
+
25043
+ - GPUs: ${GPU_TYPES.join(", ")} (default ${DEFAULT_GPU_TYPE}). Add a count for multi-GPU, e.g. \`H100:4\` (max ${COMPUTE_SANDBOX_LIMITS.maxGpuCount}). Pick the smallest GPU that fits: T4/L4 for small models and smoke tests, A100/H100 for real training.
25044
+ - Default image: \`${DEFAULT_GPU_IMAGE}\`. Pass any public registry image with \`--image\`; the first start of a new image takes a few minutes to pull.
25045
+ - The sandbox has internet access. Download datasets and weights inside it rather than pushing them; pushes and pulls are capped at ${COMPUTE_SANDBOX_LIMITS.maxTransferBytes / 1024 ** 3} GiB.
25046
+
25047
+ ## Limits
25048
+
25049
+ - ${COMPUTE_SANDBOX_LIMITS.maxActivePerWorkspace} running sandboxes per workspace and ${COMPUTE_SANDBOX_LIMITS.maxActivePerOrganization} per organization
25050
+ - Lifetime 1 hour by default, up to 24 hours with \`--timeout\`
25051
+ - Sandboxes stop after 10 minutes with no running command (\`--idle-timeout\`, up to 1 hour)
25052
+ - Sandboxes stop when this workspace sleeps, is archived, or is deleted, and their files are lost; pull results before finishing
25053
+ `;
25054
+ var GPU_ABILITY = {
25055
+ label: "GPU sandboxes",
25056
+ description: "Run training, inference, and other GPU workloads on cloud GPUs with `replicas gpu`.",
25057
+ bullet: "- Running GPU workloads (training, inference, CUDA, rendering) on cloud GPU sandboxes with `replicas gpu`",
25058
+ section: SECTION6,
25059
+ referenceFile: { name: "GPU.md", content: REFERENCE6 }
25060
+ };
25061
+
24967
25062
  // ../shared/src/default-skills/replicas-agent/abilities/linear.ts
24968
- var SECTION6 = `### Linear
25063
+ var SECTION7 = `### Linear
24969
25064
  Create tickets, fetch issues, update state, add comments, and search through \`replicas.linear.graphql\` in \`@replicas/sdk\`. This works without a Linear MCP server and refreshes authentication automatically.
24970
25065
 
24971
25066
  **Reference:** \`references/LINEAR.md\`
@@ -24976,7 +25071,7 @@ Use this when:
24976
25071
  - You need to update an issue's state or add a comment
24977
25072
  - You need to search Linear issues
24978
25073
  - A Linear request failed with an authentication error`;
24979
- var REFERENCE6 = `# Linear Integration
25074
+ var REFERENCE7 = `# Linear Integration
24980
25075
 
24981
25076
  Use \`@replicas/sdk\`; never request or handle a Linear OAuth token. Replicas refreshes the organization credential behind the gateway.
24982
25077
 
@@ -25069,12 +25164,12 @@ var LINEAR_ABILITY = {
25069
25164
  label: "Linear",
25070
25165
  description: "Create tickets, fetch issues, comment, and update state through @replicas/sdk.",
25071
25166
  bullet: "- Interacting with Linear (creating tickets, fetching issues including any linear.app link the user shares, updating state, commenting, or troubleshooting authentication): use replicas.linear.graphql from @replicas/sdk, which refreshes tokens automatically and needs no Linear MCP server. Read references/LINEAR.md before treating a rejected workspace token as a disconnected integration.",
25072
- section: SECTION6,
25073
- referenceFile: { name: "LINEAR.md", content: REFERENCE6 }
25167
+ section: SECTION7,
25168
+ referenceFile: { name: "LINEAR.md", content: REFERENCE7 }
25074
25169
  };
25075
25170
 
25076
25171
  // ../shared/src/default-skills/replicas-agent/abilities/media.ts
25077
- var SECTION7 = `### Media
25172
+ var SECTION8 = `### Media
25078
25173
  Deliver screenshots, GIFs, screen recordings, generated diagrams, audio clips, and self-contained HTML pages to the Replicas dashboard and, when applicable, inline in the destination workflow.
25079
25174
 
25080
25175
  **Reference:** \`references/MEDIA.md\`
@@ -25086,7 +25181,7 @@ Use this when:
25086
25181
  - You produce a self-contained HTML report that should be linked from a pull request
25087
25182
  - You record video output (browser automation, screen capture) \u2014 including the recommended aspect ratio and FPS
25088
25183
  - You need to embed media in a Slack/Linear/GitHub/GitLab message AND keep a referenceable copy in the Replicas dashboard`;
25089
- var REFERENCE7 = `# Media (Screenshots, GIFs, Recordings, Audio, HTML)
25184
+ var REFERENCE8 = `# Media (Screenshots, GIFs, Recordings, Audio, HTML)
25090
25185
 
25091
25186
  This is the single source of truth for delivering screenshots, GIFs, screen recordings, generated diagrams, audio clips, and self-contained HTML pages through Replicas and external surfaces (Slack, Linear, GitHub, GitLab).
25092
25187
 
@@ -25466,12 +25561,12 @@ var MEDIA_ABILITY = {
25466
25561
  label: "Media",
25467
25562
  description: "Deliver media to Replicas and destination workflows with inline uploads.",
25468
25563
  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. Paste the printed `![\u2026](\u2026)` embeds in your chat reply, never only dashboard links, and upload the file itself to Slack, Linear, or GitLab. To show motion, turn the recording into a short GIF with `replicas media gif` instead of a grid of frames",
25469
- section: SECTION7,
25470
- referenceFile: { name: "MEDIA.md", content: REFERENCE7 }
25564
+ section: SECTION8,
25565
+ referenceFile: { name: "MEDIA.md", content: REFERENCE8 }
25471
25566
  };
25472
25567
 
25473
25568
  // ../shared/src/default-skills/replicas-agent/abilities/previews.ts
25474
- var SECTION8 = `### Previews
25569
+ var SECTION9 = `### Previews
25475
25570
  Expose locally running services (web apps, APIs, databases) as public preview URLs so humans can interact with them directly.
25476
25571
 
25477
25572
  **Reference:** \`references/PREVIEWS.md\`
@@ -25481,7 +25576,7 @@ Use this when:
25481
25576
  - The task involves UI work that benefits from human review
25482
25577
  - You are verifying frontend/backend integrations visually
25483
25578
  - You need a URL for OAuth redirects, GitHub App callbacks, or webhooks (never the sandbox's \`*.e2b.app\` address)`;
25484
- var REFERENCE8 = `# Preview URLs
25579
+ var REFERENCE9 = `# Preview URLs
25485
25580
 
25486
25581
  When you run services on ports \u2014 such as a web app, API server, or database \u2014 humans may want to interact with them directly. You can expose your locally running services as public preview URLs.
25487
25582
 
@@ -25570,12 +25665,12 @@ var PREVIEWS_ABILITY = {
25570
25665
  label: "Previews",
25571
25666
  description: "Expose locally running services on public preview URLs for humans.",
25572
25667
  bullet: "- Creating preview URLs for locally running services, including OAuth redirect, app callback, and webhook URLs (never a raw `*.e2b.app` address)",
25573
- section: SECTION8,
25574
- referenceFile: { name: "PREVIEWS.md", content: REFERENCE8 }
25668
+ section: SECTION9,
25669
+ referenceFile: { name: "PREVIEWS.md", content: REFERENCE9 }
25575
25670
  };
25576
25671
 
25577
25672
  // ../shared/src/default-skills/replicas-agent/abilities/plugins.ts
25578
- var SECTION9 = `### Plugins
25673
+ var SECTION10 = `### Plugins
25579
25674
  Discover and execute installed marketplace tools through the workspace-scoped TypeScript SDK.
25580
25675
 
25581
25676
  **Reference:** \`references/PLUGINS.md\`
@@ -25594,7 +25689,7 @@ Use this when:
25594
25689
  - The user asks to read, triage, label, or draft replies to LinkedIn messages through Kondo
25595
25690
  - The user asks to query Sazabi logs, investigate Sazabi issues, or ask the Sazabi agent about production
25596
25691
  - The user asks to analyze text with Pangram`;
25597
- var REFERENCE9 = `# Plugins
25692
+ var REFERENCE10 = `# Plugins
25598
25693
 
25599
25694
  Plugins are TypeScript libraries, not MCP servers. Import \`@replicas/sdk\`; Replicas authenticates the workspace and selects the most specific environment the workspace inherits from, then Global. Provider credentials, sessions, and project keys are never exposed.
25600
25695
 
@@ -25800,12 +25895,12 @@ var PLUGINS_ABILITY = {
25800
25895
  label: "Plugins",
25801
25896
  description: "Use installed marketplace tools through @replicas/sdk.",
25802
25897
  bullet: "- Interacting with installed marketplace plugins through TypeScript",
25803
- section: SECTION9,
25804
- referenceFile: { name: "PLUGINS.md", content: REFERENCE9 }
25898
+ section: SECTION10,
25899
+ referenceFile: { name: "PLUGINS.md", content: REFERENCE10 }
25805
25900
  };
25806
25901
 
25807
25902
  // ../shared/src/default-skills/replicas-agent/abilities/replicas.ts
25808
- var SECTION10 = `### Replicas (in-workspace CLI)
25903
+ var SECTION11 = `### Replicas (in-workspace CLI)
25809
25904
  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.
25810
25905
 
25811
25906
  **Reference:** \`references/REPLICAS.md\`
@@ -25817,7 +25912,7 @@ Use this when:
25817
25912
  - The user asks "what envs / repos / automations do I have?"
25818
25913
  - The user asks you to scaffold a \`replicas.json\` / \`replicas.yaml\` in a repo
25819
25914
  - You need to run a long-lived service (dev server, daemon) \u2014 always use \`replicas service start\` so it survives workspace sleep/wake`;
25820
- var REFERENCE10 = `# Replicas (in-workspace CLI)
25915
+ var REFERENCE11 = `# Replicas (in-workspace CLI)
25821
25916
 
25822
25917
  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.
25823
25918
 
@@ -26078,13 +26173,13 @@ var REPLICAS_ABILITY = {
26078
26173
  description: "Teach the agent about Replicas itself \u2014 automations, environments, the in-workspace CLI.",
26079
26174
  // No bullet — help_instructions covers the `replicas` CLI surface in detail.
26080
26175
  bullet: "",
26081
- section: SECTION10,
26082
- referenceFile: { name: "REPLICAS.md", content: REFERENCE10 },
26176
+ section: SECTION11,
26177
+ referenceFile: { name: "REPLICAS.md", content: REFERENCE11 },
26083
26178
  locked: true
26084
26179
  };
26085
26180
 
26086
26181
  // ../shared/src/default-skills/replicas-agent/abilities/sentry.ts
26087
- var SECTION11 = `### Sentry
26182
+ var SECTION12 = `### Sentry
26088
26183
  Read issues and project data through the workspace-scoped TypeScript SDK.
26089
26184
 
26090
26185
  **Reference:** \`references/SENTRY.md\`
@@ -26093,7 +26188,7 @@ Use this when:
26093
26188
  - A user shares a sentry.io link
26094
26189
  - You need to inspect Sentry projects or issues
26095
26190
  - A user asks for error-monitoring context outside a webhook-triggered task`;
26096
- var REFERENCE11 = `# Sentry Integration
26191
+ var REFERENCE12 = `# Sentry Integration
26097
26192
 
26098
26193
  Use \`@replicas/sdk\`; never request or read a Sentry token. Sentry access is organization-scoped and read-only.
26099
26194
 
@@ -26122,8 +26217,8 @@ var SENTRY_ABILITY = {
26122
26217
  label: "Sentry",
26123
26218
  description: "Read Sentry projects and issues through @replicas/sdk.",
26124
26219
  bullet: "- Interacting with Sentry (reading projects, issues, and error context, including any sentry.io link the user shares): use replicas.sentry.request from @replicas/sdk. Read references/SENTRY.md to turn an issue link into API paths.",
26125
- section: SECTION11,
26126
- referenceFile: { name: "SENTRY.md", content: REFERENCE11 }
26220
+ section: SECTION12,
26221
+ referenceFile: { name: "SENTRY.md", content: REFERENCE12 }
26127
26222
  };
26128
26223
 
26129
26224
  // ../shared/src/memory.ts
@@ -26131,11 +26226,11 @@ var HISTORY_ROOT = `${SANDBOX_PATHS.REPLICAS_DIR}/history`;
26131
26226
  var HISTORY_INDEX_FILENAME = "INDEX.md";
26132
26227
 
26133
26228
  // ../shared/src/default-skills/replicas-agent/abilities/session-history.ts
26134
- var SECTION12 = `### Session history
26229
+ var SECTION13 = `### Session history
26135
26230
  Search past agent sessions in this organization when the user refers to prior work, a date, a teammate's session, or "last time".
26136
26231
 
26137
26232
  **Reference:** \`references/SESSION-HISTORY.md\``;
26138
- var REFERENCE12 = `# Session history
26233
+ var REFERENCE13 = `# Session history
26139
26234
 
26140
26235
  Past sessions are installed under \`${HISTORY_ROOT}\`:
26141
26236
 
@@ -26164,8 +26259,8 @@ var SESSION_HISTORY_ABILITY = {
26164
26259
  label: "Session history",
26165
26260
  description: "Search past agent sessions in this organization.",
26166
26261
  bullet: "",
26167
- section: SECTION12,
26168
- referenceFile: { name: "SESSION-HISTORY.md", content: REFERENCE12 },
26262
+ section: SECTION13,
26263
+ referenceFile: { name: "SESSION-HISTORY.md", content: REFERENCE13 },
26169
26264
  locked: true
26170
26265
  };
26171
26266
 
@@ -26176,7 +26271,7 @@ var SLACK_BLOCK_KIT_GUIDANCE = "Use Block Kit whenever structure or a link would
26176
26271
  var SLACK_PR_BUTTON_GUIDANCE = 'The first time you share a given pull request or merge request link in a thread \u2014 the message announcing you opened it \u2014 attach a button for it: send `blocks` alongside `text` \u2014 a `section` block holding your message as `mrkdwn`, then an `actions` block with one element per newly shared link, `{"type": "button", "style": "primary", "text": {"type": "plain_text", "text": "View PR"}, "url": "<pr url>", "action_id": "view_pr"}`. Later messages about a pull request you already linked in this thread keep the link in the sentence and get no button, and never re-post a button for a link someone already has. Label it "View MR" for GitLab, and if there are several give each button a unique `action_id` and name its repository in the label ("View PR \xB7 web"). Always keep `text` populated with the same message \u2014 Slack uses it for notifications and fallback \u2014 and keep the link in the sentence too.';
26177
26272
 
26178
26273
  // ../shared/src/default-skills/replicas-agent/abilities/slack.ts
26179
- var SECTION13 = `### Slack
26274
+ var SECTION14 = `### Slack
26180
26275
  Send messages, read threads, search conversations, and transfer files through the workspace-scoped TypeScript SDK.
26181
26276
 
26182
26277
  **Reference:** \`references/SLACK.md\`
@@ -26186,7 +26281,7 @@ Use this when:
26186
26281
  - You need to read or search Slack conversations
26187
26282
  - You need to download a file attached to a Slack message
26188
26283
  - You encounter a Slack message link and need its full thread`;
26189
- var REFERENCE13 = `# Slack Integration
26284
+ var REFERENCE14 = `# Slack Integration
26190
26285
 
26191
26286
  Use \`@replicas/sdk\`; never read or send a Slack token directly. The SDK calls Replicas, which selects only this workspace's organization connection.
26192
26287
 
@@ -26264,8 +26359,8 @@ var SLACK_ABILITY = {
26264
26359
  label: "Slack",
26265
26360
  description: "Send messages, read threads, search, and transfer files through @replicas/sdk.",
26266
26361
  bullet: "- Interacting with Slack (listing channels, sending messages, reading threads, or searching): use replicas.slack.call from @replicas/sdk. Read references/SLACK.md; no Slack MCP server or provider token in the VM is required.",
26267
- section: SECTION13,
26268
- referenceFile: { name: "SLACK.md", content: REFERENCE13 }
26362
+ section: SECTION14,
26363
+ referenceFile: { name: "SLACK.md", content: REFERENCE14 }
26269
26364
  };
26270
26365
 
26271
26366
  // ../shared/src/default-skills/replicas-agent/registry.ts
@@ -26277,6 +26372,7 @@ var REPLICAS_AGENT_ABILITY_REGISTRY = {
26277
26372
  environment_setup: ENVIRONMENT_SETUP_ABILITY,
26278
26373
  github: GITHUB_ABILITY,
26279
26374
  gitlab: GITLAB_ABILITY,
26375
+ gpu: GPU_ABILITY,
26280
26376
  linear: LINEAR_ABILITY,
26281
26377
  media: MEDIA_ABILITY,
26282
26378
  plugins: PLUGINS_ABILITY,
@@ -28669,7 +28765,7 @@ var headlessAgentRequestSchema = external_exports.object({
28669
28765
  });
28670
28766
 
28671
28767
  // ../shared/src/cli-version.ts
28672
- var CLI_VERSION = "0.3.102";
28768
+ var CLI_VERSION = "0.3.104";
28673
28769
 
28674
28770
  // ../shared/src/version.ts
28675
28771
  function compareVersions(v1, v2) {
@@ -29931,7 +30027,7 @@ async function apiKeyLoginCommand(key) {
29931
30027
  }
29932
30028
  async function loginCommand() {
29933
30029
  const state = generateState();
29934
- return new Promise((resolve2, reject) => {
30030
+ return new Promise((resolve3, reject) => {
29935
30031
  let authTimeout;
29936
30032
  let hasHandledCallback = false;
29937
30033
  let lastRedirectUrl = null;
@@ -30081,7 +30177,7 @@ async function loginCommand() {
30081
30177
  setImmediate(() => {
30082
30178
  server.closeAllConnections?.();
30083
30179
  server.close();
30084
- resolve2();
30180
+ resolve3();
30085
30181
  });
30086
30182
  } catch (error52) {
30087
30183
  const errorUrl = `${WEB_APP_URL}/cli-login/error?message=${encodeURIComponent("Failed to verify authentication.")}`;
@@ -30321,7 +30417,7 @@ function buildSSHArgs(token, host, proxyCommand, localForward) {
30321
30417
  ];
30322
30418
  }
30323
30419
  async function connectSSH(token, host, proxyCommand, localForward) {
30324
- return new Promise((resolve2, reject) => {
30420
+ return new Promise((resolve3, reject) => {
30325
30421
  const ssh = (0, import_child_process.spawn)("ssh", buildSSHArgs(token, host, proxyCommand, localForward), {
30326
30422
  stdio: "inherit"
30327
30423
  });
@@ -30329,7 +30425,7 @@ async function connectSSH(token, host, proxyCommand, localForward) {
30329
30425
  if (localForward && code) {
30330
30426
  reject(new Error(`SSH tunnel exited with code ${code}`));
30331
30427
  } else {
30332
- resolve2();
30428
+ resolve3();
30333
30429
  }
30334
30430
  });
30335
30431
  ssh.on("error", reject);
@@ -30827,7 +30923,7 @@ async function exchangeCodeForTokens(code, codeVerifier) {
30827
30923
  };
30828
30924
  }
30829
30925
  function startCallbackServer(expectedState, codeVerifier, save) {
30830
- return new Promise((resolve2, reject) => {
30926
+ return new Promise((resolve3, reject) => {
30831
30927
  const server = import_http2.default.createServer(async (req, res) => {
30832
30928
  try {
30833
30929
  if (!req.url) {
@@ -30871,7 +30967,7 @@ function startCallbackServer(expectedState, codeVerifier, save) {
30871
30967
  res.writeHead(302, { "Location": completionUrl });
30872
30968
  res.end();
30873
30969
  server.close();
30874
- resolve2();
30970
+ resolve3();
30875
30971
  } catch (tokenError) {
30876
30972
  const errorMessage = encodeURIComponent(tokenError instanceof Error ? tokenError.message : "Unknown error");
30877
30973
  res.writeHead(302, { "Location": `${WEB_APP_URL2}/codex/oauth/error?message=${errorMessage}` });
@@ -30905,7 +31001,7 @@ async function runCodexOAuthFlow(totalSteps, save) {
30905
31001
  const state = generateState2();
30906
31002
  const authUrl = buildAuthorizationUrl(pkce.codeChallenge, state);
30907
31003
  const tokensPromise = startCallbackServer(state, pkce.codeVerifier, save);
30908
- await new Promise((resolve2) => setTimeout(resolve2, 500));
31004
+ await new Promise((resolve3) => setTimeout(resolve3, 500));
30909
31005
  renderAuthFlowStep(1, totalSteps, "Approve access in your browser");
30910
31006
  await openForApproval("auth.openai.com", authUrl);
30911
31007
  renderAuthFlowDetail("waiting for sign-in\u2026");
@@ -31053,10 +31149,10 @@ async function promptForAuthorizationCode() {
31053
31149
  process.stdin.setRawMode(false);
31054
31150
  process.stdin.resume();
31055
31151
  const rl = import_readline.default.createInterface({ input: process.stdin, output: process.stdout });
31056
- return new Promise((resolve2) => {
31057
- rl.on("close", () => resolve2(""));
31152
+ return new Promise((resolve3) => {
31153
+ rl.on("close", () => resolve3(""));
31058
31154
  rl.question(import_chalk15.default.gray(" Paste code here: "), (answer) => {
31059
- resolve2(answer.trim());
31155
+ resolve3(answer.trim());
31060
31156
  rl.close();
31061
31157
  });
31062
31158
  });
@@ -31203,7 +31299,7 @@ async function runMuseOAuthFlow(totalSteps) {
31203
31299
  const deadline = Date.now() + (typeof device.expires_in === "number" ? device.expires_in : 900) * 1e3;
31204
31300
  let intervalMs = Math.max(1, typeof device.interval === "number" ? device.interval : 5) * 1e3;
31205
31301
  while (Date.now() < deadline) {
31206
- await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
31302
+ await new Promise((resolve3) => setTimeout(resolve3, intervalMs));
31207
31303
  const response = await post("/oidc/device/token/", new URLSearchParams({
31208
31304
  grant_type: "urn:ietf:params:oauth:grant-type:device_code",
31209
31305
  device_code: device.device_code,
@@ -32861,7 +32957,7 @@ async function environmentSetupWaitVariablesCommand(options) {
32861
32957
  Waiting up to ${formatDuration(timeoutMs)} for: ${blocking(before).map((request) => request.key).join(", ")}`));
32862
32958
  const deadline = Date.now() + timeoutMs;
32863
32959
  while (blocking(run2.variable_requests).length > 0 && Date.now() < deadline) {
32864
- await new Promise((resolve2) => setTimeout(resolve2, ENVIRONMENT_SETUP_VARIABLE_WAIT.pollMs));
32960
+ await new Promise((resolve3) => setTimeout(resolve3, ENVIRONMENT_SETUP_VARIABLE_WAIT.pollMs));
32865
32961
  run2 = (await fetchCurrentRun()).run;
32866
32962
  }
32867
32963
  }
@@ -33755,19 +33851,22 @@ async function agentRequest(path8, options) {
33755
33851
  if (!config3) {
33756
33852
  throw new Error("Agent mode config not found");
33757
33853
  }
33758
- const isFormData = options?.body instanceof FormData;
33854
+ const rawBody = options?.body;
33855
+ const isFormData = rawBody instanceof FormData;
33856
+ const isStream = rawBody instanceof ReadableStream;
33759
33857
  const headers = {
33760
33858
  "Authorization": `Bearer ${config3.engine_secret}`,
33761
33859
  "X-Workspace-Id": config3.workspace_id,
33762
33860
  // FormData sets its own multipart Content-Type with boundary.
33763
- ...isFormData ? {} : { "Content-Type": "application/json" },
33861
+ ...isFormData ? {} : { "Content-Type": isStream ? "application/octet-stream" : "application/json" },
33764
33862
  ...options?.headers || {}
33765
33863
  };
33766
- const body = options?.body === void 0 ? void 0 : isFormData ? options.body : JSON.stringify(options.body);
33864
+ const body = rawBody === void 0 ? void 0 : rawBody instanceof FormData || rawBody instanceof ReadableStream ? rawBody : JSON.stringify(rawBody);
33767
33865
  const response = await fetch(`${MONOLITH_URL}${path8}`, {
33768
33866
  ...options,
33769
33867
  headers,
33770
- body
33868
+ body,
33869
+ ...isStream ? { duplex: "half" } : {}
33771
33870
  });
33772
33871
  if (!response.ok) {
33773
33872
  const error51 = await response.json().catch(() => ({ error: "Request failed" }));
@@ -34139,27 +34238,247 @@ async function mediaGifCommand(input, options) {
34139
34238
  );
34140
34239
  }
34141
34240
 
34142
- // src/commands/history.ts
34241
+ // src/commands/gpu.ts
34242
+ var import_chalk28 = __toESM(require("chalk"));
34243
+ var import_node_child_process2 = require("child_process");
34143
34244
  var import_node_fs2 = require("fs");
34144
34245
  var import_node_path2 = require("path");
34246
+ var import_node_stream = require("stream");
34247
+ var import_promises = require("stream/promises");
34248
+ var BASE = "/v1/engine/compute/sandboxes";
34249
+ var CREATE_POLL_INTERVAL_MS = 3e3;
34250
+ var CREATE_WAIT_TIMEOUT_MS = 25 * 6e4;
34251
+ function parseMinutes(value, flag) {
34252
+ if (value === void 0) return void 0;
34253
+ const match = value.trim().match(/^(\d+)(m|h)?$/);
34254
+ if (!match) throw new Error(`${flag} must be minutes or a duration like 30m or 2h`);
34255
+ return Number(match[1]) * (match[2] === "h" ? 60 : 1);
34256
+ }
34257
+ function parseEnv(values) {
34258
+ return values?.length ? parseKeyValuePairs(values) : void 0;
34259
+ }
34260
+ function describe3(sandbox) {
34261
+ console.log(`${import_chalk28.default.bold(sandbox.id)} ${sandbox.status} ${sandbox.gpu ?? "no GPU"} ${sandbox.image}`);
34262
+ console.log(import_chalk28.default.gray(` expires ${new Date(sandbox.expiresAt).toLocaleString()}`));
34263
+ for (const tunnel of sandbox.tunnels) console.log(` port ${tunnel.port}: ${tunnel.url}`);
34264
+ if (sandbox.error) console.log(import_chalk28.default.red(` ${sandbox.error}`));
34265
+ }
34266
+ async function resolveSandboxId(options) {
34267
+ if (options.sandbox) return options.sandbox;
34268
+ const { sandboxes } = await agentFetch(BASE);
34269
+ if (sandboxes.length === 1) return sandboxes[0].id;
34270
+ if (sandboxes.length === 0) throw new Error('No GPU sandbox is running. Start one with "replicas gpu create".');
34271
+ throw new Error(`Several GPU sandboxes are running; pick one with --sandbox: ${sandboxes.map((sandbox) => sandbox.id).join(", ")}`);
34272
+ }
34273
+ async function createSandbox(options) {
34274
+ const request = {
34275
+ kind: "gpu",
34276
+ gpu: options.gpu,
34277
+ image: options.image,
34278
+ timeoutMinutes: parseMinutes(options.timeout, "--timeout"),
34279
+ idleTimeoutMinutes: parseMinutes(options.idleTimeout, "--idle-timeout"),
34280
+ ports: options.port?.map(parsePort),
34281
+ env: parseEnv(options.env)
34282
+ };
34283
+ let { sandbox } = await agentFetch(BASE, { method: "POST", body: request });
34284
+ if (options.wait === false) return sandbox;
34285
+ process.stderr.write(import_chalk28.default.gray(`Starting ${sandbox.gpu} sandbox ${sandbox.id} (a new image can take a few minutes to pull)...
34286
+ `));
34287
+ const deadline = Date.now() + CREATE_WAIT_TIMEOUT_MS;
34288
+ while (sandbox.status === "creating") {
34289
+ if (Date.now() > deadline) throw new Error(`GPU sandbox ${sandbox.id} is still starting; check it with "replicas gpu list"`);
34290
+ await new Promise((done) => setTimeout(done, CREATE_POLL_INTERVAL_MS));
34291
+ ({ sandbox } = await agentFetch(`${BASE}/${sandbox.id}`));
34292
+ }
34293
+ if (sandbox.status !== "running") throw new Error(`GPU sandbox failed to start: ${sandbox.error ?? "it stopped while starting"}`);
34294
+ return sandbox;
34295
+ }
34296
+ async function gpuCreateCommand(options) {
34297
+ output(await createSandbox(options), describe3);
34298
+ }
34299
+ async function gpuListCommand() {
34300
+ const { sandboxes } = await agentFetch(BASE);
34301
+ output(sandboxes, (list2) => {
34302
+ if (list2.length === 0) console.log("No GPU sandboxes are running.");
34303
+ list2.forEach(describe3);
34304
+ });
34305
+ }
34306
+ async function gpuDeleteCommand(id, options) {
34307
+ const sandboxId = id ?? await resolveSandboxId(options);
34308
+ await agentRequest(`${BASE}/${sandboxId}`, { method: "DELETE" });
34309
+ outputSuccess(`Stopped GPU sandbox ${sandboxId}`);
34310
+ }
34311
+ async function streamOutput(response) {
34312
+ if (!response.body) throw new Error("Empty response from the GPU sandbox");
34313
+ const decoder2 = new TextDecoder();
34314
+ let buffer = "";
34315
+ let exit;
34316
+ const handle = (line) => {
34317
+ if (!line.trim()) return;
34318
+ const event = JSON.parse(line);
34319
+ if (event.type === "stdout") (isJsonMode() ? process.stderr : process.stdout).write(event.data);
34320
+ else if (event.type === "stderr") process.stderr.write(event.data);
34321
+ else if (event.type === "error") throw new Error(event.message);
34322
+ else if (event.type === "exit") exit = { exitCode: event.exitCode };
34323
+ };
34324
+ for await (const chunk of import_node_stream.Readable.fromWeb(response.body)) {
34325
+ buffer += decoder2.decode(chunk, { stream: true });
34326
+ const lines = buffer.split("\n");
34327
+ buffer = lines.pop() ?? "";
34328
+ lines.forEach(handle);
34329
+ }
34330
+ handle(buffer + decoder2.decode());
34331
+ if (!exit) {
34332
+ throw new Error('Lost the connection to the GPU sandbox; the command may still be running. Use --detach and "replicas gpu logs -f" for long jobs.');
34333
+ }
34334
+ return exit.exitCode;
34335
+ }
34336
+ async function exec(sandboxId, command, options) {
34337
+ return agentRequest(`${BASE}/${sandboxId}/exec`, {
34338
+ method: "POST",
34339
+ body: { command, workdir: options.workdir, env: parseEnv(options.env), detach: options.detach }
34340
+ });
34341
+ }
34342
+ async function gpuExecCommand(command, options) {
34343
+ if (command.length === 0) throw new Error("Pass a command after --, e.g. replicas gpu exec -- nvidia-smi");
34344
+ const sandboxId = await resolveSandboxId(options);
34345
+ const response = await exec(sandboxId, command.join(" "), options);
34346
+ if (options.detach) {
34347
+ const { jobId } = await response.json();
34348
+ output({ jobId, sandboxId }, () => {
34349
+ console.log(`Started job ${import_chalk28.default.bold(jobId)} in ${sandboxId}`);
34350
+ console.log(import_chalk28.default.gray(`Follow it with: replicas gpu logs ${jobId} -f`));
34351
+ });
34352
+ return;
34353
+ }
34354
+ const exitCode = await streamOutput(response);
34355
+ process.exitCode = exitCode ?? 1;
34356
+ if (isJsonMode()) output({ sandboxId, exitCode }, () => {
34357
+ });
34358
+ }
34359
+ async function gpuLogsCommand(jobId, options) {
34360
+ const sandboxId = await resolveSandboxId(options);
34361
+ const response = await agentRequest(`${BASE}/${sandboxId}/jobs/${jobId}/logs${options.follow ? "?follow=true" : ""}`);
34362
+ const exitCode = await streamOutput(response);
34363
+ if (isJsonMode()) {
34364
+ if (options.follow && exitCode !== null) process.exitCode = exitCode;
34365
+ output({ jobId, sandboxId, running: exitCode === null, exitCode }, () => {
34366
+ });
34367
+ return;
34368
+ }
34369
+ if (exitCode === null) {
34370
+ process.stderr.write(import_chalk28.default.gray(`
34371
+ Job ${jobId} is still running.
34372
+ `));
34373
+ return;
34374
+ }
34375
+ if (options.follow) process.exitCode = exitCode;
34376
+ process.stderr.write(import_chalk28.default.gray(`
34377
+ Job ${jobId} exited with code ${exitCode}.
34378
+ `));
34379
+ }
34380
+ function assertTransferSize(bytes, localPath) {
34381
+ if (bytes > COMPUTE_SANDBOX_LIMITS.maxTransferBytes) {
34382
+ throw new Error(`${localPath} is larger than ${COMPUTE_SANDBOX_LIMITS.maxTransferBytes / 1024 ** 3} GiB. Download large datasets and weights inside the sandbox instead (it has internet access).`);
34383
+ }
34384
+ }
34385
+ function archive(localPath) {
34386
+ const path8 = (0, import_node_path2.resolve)(localPath);
34387
+ if (!(0, import_node_fs2.existsSync)(path8)) throw new Error(`${path8} does not exist`);
34388
+ if (!(0, import_node_fs2.statSync)(path8).isDirectory()) {
34389
+ assertTransferSize((0, import_node_fs2.statSync)(path8).size, localPath);
34390
+ return (0, import_node_child_process2.spawn)("tar", ["-czf", "-", "-C", (0, import_node_path2.dirname)(path8), (0, import_node_path2.basename)(path8)], { stdio: ["ignore", "pipe", "pipe"] });
34391
+ }
34392
+ const listed = (0, import_node_child_process2.spawnSync)("git", ["-C", path8, "ls-files", "-co", "--exclude-standard", "-z"], { maxBuffer: 256 * 1024 * 1024 });
34393
+ if (listed.status !== 0) {
34394
+ const du = (0, import_node_child_process2.spawnSync)("du", ["-sk", "--exclude=.git", "--exclude=node_modules", path8]);
34395
+ assertTransferSize(Number(du.stdout.toString().split(" ")[0]) * 1024, localPath);
34396
+ return (0, import_node_child_process2.spawn)("tar", ["-czf", "-", "-C", path8, "--exclude=.git", "--exclude=node_modules", "."], { stdio: ["ignore", "pipe", "pipe"] });
34397
+ }
34398
+ const files = listed.stdout.toString().split("\0").filter((file2) => file2 && (0, import_node_fs2.existsSync)((0, import_node_path2.join)(path8, file2)));
34399
+ assertTransferSize(files.reduce((total, file2) => total + (0, import_node_fs2.statSync)((0, import_node_path2.join)(path8, file2)).size, 0), localPath);
34400
+ const child = (0, import_node_child_process2.spawn)("tar", ["-czf", "-", "-C", path8, "--null", "-T", "-"], { stdio: ["pipe", "pipe", "pipe"] });
34401
+ child.stdin.end(files.map((file2) => `${file2}\0`).join(""));
34402
+ return child;
34403
+ }
34404
+ async function push(sandboxId, localPath, remotePath) {
34405
+ const tar = archive(localPath);
34406
+ let stderr = "";
34407
+ tar.stderr?.on("data", (chunk) => {
34408
+ stderr += chunk.toString();
34409
+ });
34410
+ const exited = new Promise((done) => tar.on("close", done));
34411
+ await agentRequest(`${BASE}/${sandboxId}/files?path=${encodeURIComponent(remotePath)}`, {
34412
+ method: "PUT",
34413
+ body: import_node_stream.Readable.toWeb(tar.stdout)
34414
+ });
34415
+ if (await exited !== 0) throw new Error(`Archiving ${localPath} failed: ${stderr.trim()}`);
34416
+ }
34417
+ async function gpuPushCommand(localPath = ".", remotePath = COMPUTE_SANDBOX_WORKDIR, options = {}) {
34418
+ const sandboxId = await resolveSandboxId(options);
34419
+ await push(sandboxId, localPath, remotePath);
34420
+ outputSuccess(`Copied ${localPath} to ${remotePath} in ${sandboxId}`);
34421
+ }
34422
+ async function pull(sandboxId, remotePath, localPath) {
34423
+ const response = await agentRequest(`${BASE}/${sandboxId}/files?path=${encodeURIComponent(remotePath)}`);
34424
+ (0, import_node_fs2.mkdirSync)(localPath, { recursive: true });
34425
+ const tar = (0, import_node_child_process2.spawn)("tar", ["-xzf", "-", "-C", localPath], { stdio: ["pipe", "ignore", "pipe"] });
34426
+ let stderr = "";
34427
+ tar.stderr.on("data", (chunk) => {
34428
+ stderr += chunk.toString();
34429
+ });
34430
+ const exited = new Promise((done) => tar.on("close", done));
34431
+ await (0, import_promises.pipeline)(import_node_stream.Readable.fromWeb(response.body), tar.stdin);
34432
+ if (await exited !== 0) throw new Error(`Extracting ${remotePath} failed: ${stderr.trim()}`);
34433
+ }
34434
+ async function gpuPullCommand(remotePath, localPath = ".", options = {}) {
34435
+ const sandboxId = await resolveSandboxId(options);
34436
+ await pull(sandboxId, remotePath, localPath);
34437
+ outputSuccess(`Copied ${remotePath} from ${sandboxId} into ${localPath}`);
34438
+ }
34439
+ async function gpuRunCommand(command, options) {
34440
+ if (command.length === 0) throw new Error("Pass a command after --, e.g. replicas gpu run -- python train.py");
34441
+ const { env, ...createOptions } = options;
34442
+ const sandbox = await createSandbox({ ...createOptions, wait: true });
34443
+ try {
34444
+ process.stderr.write(import_chalk28.default.gray(`Copying ${process.cwd()} to ${COMPUTE_SANDBOX_WORKDIR}...
34445
+ `));
34446
+ await push(sandbox.id, ".", COMPUTE_SANDBOX_WORKDIR);
34447
+ const exitCode = await streamOutput(await exec(sandbox.id, command.join(" "), { env }));
34448
+ for (const remotePath of options.pull ?? []) {
34449
+ await pull(sandbox.id, remotePath.startsWith("/") ? remotePath : `${COMPUTE_SANDBOX_WORKDIR}/${remotePath}`, ".");
34450
+ }
34451
+ process.exitCode = exitCode ?? 1;
34452
+ if (isJsonMode()) output({ sandboxId: sandbox.id, exitCode }, () => {
34453
+ });
34454
+ } finally {
34455
+ if (options.keep) process.stderr.write(import_chalk28.default.gray(`Kept GPU sandbox ${sandbox.id} running.
34456
+ `));
34457
+ else await agentRequest(`${BASE}/${sandbox.id}`, { method: "DELETE" });
34458
+ }
34459
+ }
34460
+
34461
+ // src/commands/history.ts
34462
+ var import_node_fs3 = require("fs");
34463
+ var import_node_path3 = require("path");
34145
34464
  async function historyTranscriptCommand(name) {
34146
34465
  const session = name.trim().replace(/\.md$/, "");
34147
34466
  if (!/^[a-zA-Z0-9-]+$/.test(session)) {
34148
34467
  throw new Error(`Invalid session name '${name}'. Use the name from INDEX.md, e.g. 2026-01-31-<id>`);
34149
34468
  }
34150
34469
  const response = await agentRequest(`/v1/engine/history/transcripts/${session}`);
34151
- const dir = (0, import_node_path2.join)(HISTORY_ROOT, "transcripts");
34152
- (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
34153
- const file2 = (0, import_node_path2.join)(dir, `${session}.jsonl`);
34154
- (0, import_node_fs2.writeFileSync)(file2, Buffer.from(await response.arrayBuffer()));
34470
+ const dir = (0, import_node_path3.join)(HISTORY_ROOT, "transcripts");
34471
+ (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
34472
+ const file2 = (0, import_node_path3.join)(dir, `${session}.jsonl`);
34473
+ (0, import_node_fs3.writeFileSync)(file2, Buffer.from(await response.arrayBuffer()));
34155
34474
  output({ path: file2 }, () => console.log(file2));
34156
34475
  }
34157
34476
 
34158
34477
  // src/commands/skills.ts
34159
- var import_node_fs3 = require("fs");
34160
- var import_chalk28 = __toESM(require("chalk"));
34478
+ var import_node_fs4 = require("fs");
34479
+ var import_chalk29 = __toESM(require("chalk"));
34161
34480
  async function skillsSuggestCommand(file2, options) {
34162
- const body = { content: (0, import_node_fs3.readFileSync)(file2, "utf-8"), title: options.title };
34481
+ const body = { content: (0, import_node_fs4.readFileSync)(file2, "utf-8"), title: options.title };
34163
34482
  const { id, name, update } = await agentFetch("/v1/learned-skills/suggestions", {
34164
34483
  method: "POST",
34165
34484
  body
@@ -34174,12 +34493,12 @@ async function skillsSuggestCommand(file2, options) {
34174
34493
  async function skillsListCommand() {
34175
34494
  output(await orgAuthenticatedFetch("/v1/learned-skills"), ({ skills: skills2, suggestions }) => {
34176
34495
  for (const skill of skills2) console.log(`${skill.id} ${skill.name} ${skill.title}`);
34177
- if (suggestions.length > 0) console.log(import_chalk28.default.gray(`${suggestions.length} pending suggestion(s) to review in the dashboard.`));
34496
+ if (suggestions.length > 0) console.log(import_chalk29.default.gray(`${suggestions.length} pending suggestion(s) to review in the dashboard.`));
34178
34497
  });
34179
34498
  }
34180
34499
  async function skillsCreateCommand(file2, options) {
34181
- const body = { content: (0, import_node_fs3.readFileSync)(file2, "utf-8"), title: options.title };
34182
- output(await orgAuthenticatedFetch("/v1/learned-skills", { method: "POST", body }), ({ id, name }) => console.log(import_chalk28.default.green(`Saved learning '${name}' (${id})`)));
34500
+ const body = { content: (0, import_node_fs4.readFileSync)(file2, "utf-8"), title: options.title };
34501
+ output(await orgAuthenticatedFetch("/v1/learned-skills", { method: "POST", body }), ({ id, name }) => console.log(import_chalk29.default.green(`Saved learning '${name}' (${id})`)));
34183
34502
  }
34184
34503
  async function skillsUpdateCommand(id, options) {
34185
34504
  const { skills: skills2 } = await orgAuthenticatedFetch("/v1/learned-skills");
@@ -34187,9 +34506,9 @@ async function skillsUpdateCommand(id, options) {
34187
34506
  if (!existing) throw new Error(`Learning not found: ${id}`);
34188
34507
  const body = {
34189
34508
  title: options.title ?? existing.title,
34190
- content: options.file ? (0, import_node_fs3.readFileSync)(options.file, "utf-8") : existing.content
34509
+ content: options.file ? (0, import_node_fs4.readFileSync)(options.file, "utf-8") : existing.content
34191
34510
  };
34192
- output(await orgAuthenticatedFetch(`/v1/learned-skills/${existing.id}`, { method: "PUT", body }), ({ name }) => console.log(import_chalk28.default.green(`Updated learning '${name}'`)));
34511
+ output(await orgAuthenticatedFetch(`/v1/learned-skills/${existing.id}`, { method: "PUT", body }), ({ name }) => console.log(import_chalk29.default.green(`Updated learning '${name}'`)));
34193
34512
  }
34194
34513
  async function skillsDeleteCommand(id) {
34195
34514
  await orgAuthenticatedFetch(`/v1/learned-skills/${id}`, { method: "DELETE" });
@@ -34197,7 +34516,7 @@ async function skillsDeleteCommand(id) {
34197
34516
  }
34198
34517
 
34199
34518
  // src/commands/slack.ts
34200
- var import_chalk29 = __toESM(require("chalk"));
34519
+ var import_chalk30 = __toESM(require("chalk"));
34201
34520
  function getThreadOptions(options) {
34202
34521
  const channelId = options.channel || process.env.REPLICAS_SLACK_CHANNEL_ID || process.env.SLACK_CHANNEL_ID;
34203
34522
  const threadTs = options.threadTs || process.env.REPLICAS_SLACK_THREAD_TS || process.env.SLACK_THREAD_TS;
@@ -34215,10 +34534,10 @@ async function attachThread(request) {
34215
34534
  body: request
34216
34535
  });
34217
34536
  output(response, ({ thread, workspace }) => {
34218
- console.log(import_chalk29.default.green("Slack thread attached."));
34219
- console.log(import_chalk29.default.gray(` Channel: ${thread.channel_id}`));
34220
- console.log(import_chalk29.default.gray(` Thread: ${thread.thread_ts}`));
34221
- console.log(import_chalk29.default.gray(` Workspace: ${workspace.name} (${workspace.id})`));
34537
+ console.log(import_chalk30.default.green("Slack thread attached."));
34538
+ console.log(import_chalk30.default.gray(` Channel: ${thread.channel_id}`));
34539
+ console.log(import_chalk30.default.gray(` Thread: ${thread.thread_ts}`));
34540
+ console.log(import_chalk30.default.gray(` Workspace: ${workspace.name} (${workspace.id})`));
34222
34541
  });
34223
34542
  }
34224
34543
  async function slackThreadAttachCommand(options) {
@@ -34248,12 +34567,12 @@ async function slackThreadSwitchCommand(workspace, options) {
34248
34567
  }
34249
34568
 
34250
34569
  // src/commands/service.ts
34251
- var import_chalk30 = __toESM(require("chalk"));
34252
- var import_node_child_process2 = require("child_process");
34253
- var import_node_fs4 = require("fs");
34570
+ var import_chalk31 = __toESM(require("chalk"));
34571
+ var import_node_child_process3 = require("child_process");
34572
+ var import_node_fs5 = require("fs");
34254
34573
  var import_node_os2 = require("os");
34255
- var import_node_path3 = require("path");
34256
- var SERVICES_DIR = (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".replicas", "services");
34574
+ var import_node_path4 = require("path");
34575
+ var SERVICES_DIR = (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".replicas", "services");
34257
34576
  var NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
34258
34577
  function isValidServiceName(name) {
34259
34578
  return NAME_PATTERN.test(name);
@@ -34264,7 +34583,7 @@ function tailLines(content, lines) {
34264
34583
  return all.slice(-lines).join("\n").trim();
34265
34584
  }
34266
34585
  function statePath(name) {
34267
- return (0, import_node_path3.join)(SERVICES_DIR, `${name}.json`);
34586
+ return (0, import_node_path4.join)(SERVICES_DIR, `${name}.json`);
34268
34587
  }
34269
34588
  function isServiceState(value) {
34270
34589
  if (typeof value !== "object" || value === null) return false;
@@ -34274,7 +34593,7 @@ function isServiceState(value) {
34274
34593
  function readState(name) {
34275
34594
  let parsed;
34276
34595
  try {
34277
- parsed = JSON.parse((0, import_node_fs4.readFileSync)(statePath(name), "utf-8"));
34596
+ parsed = JSON.parse((0, import_node_fs5.readFileSync)(statePath(name), "utf-8"));
34278
34597
  } catch {
34279
34598
  return null;
34280
34599
  }
@@ -34308,7 +34627,7 @@ async function killServiceGroup(pid) {
34308
34627
  }
34309
34628
  function tailLog(logFile, lines) {
34310
34629
  try {
34311
- return tailLines((0, import_node_fs4.readFileSync)(logFile, "utf-8"), lines);
34630
+ return tailLines((0, import_node_fs5.readFileSync)(logFile, "utf-8"), lines);
34312
34631
  } catch {
34313
34632
  return "";
34314
34633
  }
@@ -34326,20 +34645,20 @@ async function serviceStartCommand(name, commandParts, options) {
34326
34645
  console.log(`Stopping existing "${name}" (pid ${existing.pid}) before restart`);
34327
34646
  await killServiceGroup(existing.pid);
34328
34647
  }
34329
- (0, import_node_fs4.mkdirSync)(SERVICES_DIR, { recursive: true });
34330
- const cwd = (0, import_node_path3.resolve)(options.cwd ?? process.cwd());
34331
- const logFile = (0, import_node_path3.join)(SERVICES_DIR, `${name}.log`);
34332
- const logFd = (0, import_node_fs4.openSync)(logFile, "a");
34333
- (0, import_node_fs4.writeFileSync)(logFd, `
34648
+ (0, import_node_fs5.mkdirSync)(SERVICES_DIR, { recursive: true });
34649
+ const cwd = (0, import_node_path4.resolve)(options.cwd ?? process.cwd());
34650
+ const logFile = (0, import_node_path4.join)(SERVICES_DIR, `${name}.log`);
34651
+ const logFd = (0, import_node_fs5.openSync)(logFile, "a");
34652
+ (0, import_node_fs5.writeFileSync)(logFd, `
34334
34653
  === [replicas service] "${name}" started ${(/* @__PURE__ */ new Date()).toISOString()} in ${cwd}: ${command} ===
34335
34654
  `);
34336
- const child = (0, import_node_child_process2.spawn)("bash", ["-lc", command], {
34655
+ const child = (0, import_node_child_process3.spawn)("bash", ["-lc", command], {
34337
34656
  cwd,
34338
34657
  detached: true,
34339
34658
  stdio: ["ignore", logFd, logFd],
34340
34659
  env: process.env
34341
34660
  });
34342
- (0, import_node_fs4.closeSync)(logFd);
34661
+ (0, import_node_fs5.closeSync)(logFd);
34343
34662
  if (child.pid === void 0) {
34344
34663
  throw new Error("Failed to spawn service process");
34345
34664
  }
@@ -34352,11 +34671,11 @@ async function serviceStartCommand(name, commandParts, options) {
34352
34671
  logFile,
34353
34672
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
34354
34673
  };
34355
- (0, import_node_fs4.writeFileSync)(statePath(name), JSON.stringify(state, null, 2));
34674
+ (0, import_node_fs5.writeFileSync)(statePath(name), JSON.stringify(state, null, 2));
34356
34675
  await sleep(1e3);
34357
34676
  if (!isRunning(child.pid)) {
34358
34677
  const logs = tailLog(logFile, 20);
34359
- (0, import_node_fs4.rmSync)(statePath(name), { force: true });
34678
+ (0, import_node_fs5.rmSync)(statePath(name), { force: true });
34360
34679
  throw new Error(`Service "${name}" exited immediately.${logs ? `
34361
34680
 
34362
34681
  Last log output:
@@ -34374,7 +34693,7 @@ async function serviceStopCommand(name) {
34374
34693
  }
34375
34694
  const wasRunning = isRunning(state.pid);
34376
34695
  if (wasRunning) await killServiceGroup(state.pid);
34377
- (0, import_node_fs4.rmSync)(statePath(name), { force: true });
34696
+ (0, import_node_fs5.rmSync)(statePath(name), { force: true });
34378
34697
  output({ name, pid: state.pid, was_running: wasRunning }, () => {
34379
34698
  console.log(wasRunning ? `Stopped "${name}" (pid ${state.pid})` : `Service "${name}" was not running`);
34380
34699
  });
@@ -34382,7 +34701,7 @@ async function serviceStopCommand(name) {
34382
34701
  async function serviceListCommand() {
34383
34702
  let entries;
34384
34703
  try {
34385
- entries = (0, import_node_fs4.readdirSync)(SERVICES_DIR).filter((f) => f.endsWith(".json"));
34704
+ entries = (0, import_node_fs5.readdirSync)(SERVICES_DIR).filter((f) => f.endsWith(".json"));
34386
34705
  } catch {
34387
34706
  entries = [];
34388
34707
  }
@@ -34393,7 +34712,7 @@ async function serviceListCommand() {
34393
34712
  return;
34394
34713
  }
34395
34714
  for (const service of services) {
34396
- const status = service.running ? import_chalk30.default.green("running") : import_chalk30.default.red("stopped");
34715
+ const status = service.running ? import_chalk31.default.green("running") : import_chalk31.default.red("stopped");
34397
34716
  console.log(`${service.name} ${status} pid ${service.pid} started ${service.startedAt}`);
34398
34717
  console.log(` command: ${service.command} (cwd: ${service.cwd})`);
34399
34718
  console.log(` logs: ${service.logFile}`);
@@ -34672,6 +34991,16 @@ if (isAgentMode()) {
34672
34991
  media.command("share <mediaId>").description("Rotate an image's public forge URL, or create one for video or audio media").action(run(mediaShareCommand));
34673
34992
  media.command("revoke <mediaId>").description("Revoke the public forge URL for uploaded media").action(run(mediaRevokeCommand));
34674
34993
  media.command("delete <mediaId>").description("Permanently delete media uploaded by this workspace").action(run(mediaDeleteCommand));
34994
+ const gpu = program.command("gpu").description("Run commands on cloud GPU sandboxes (billed at cost to your organization)");
34995
+ const addCreateOptions = (command) => command.option("-g, --gpu <type>", `GPU type with an optional count: ${GPU_TYPES.join(", ")}, e.g. H100:2 (default ${DEFAULT_GPU_TYPE})`).option("-i, --image <image>", `Public registry image (default ${DEFAULT_GPU_IMAGE})`).option("-t, --timeout <duration>", "Maximum lifetime, e.g. 90m or 4h (default 1h, max 24h)").option("--idle-timeout <duration>", "Stop after this long with no running command (default 10m, max 1h)").option("-p, --port <port>", "Expose a container port as a public HTTPS URL (repeatable)", collect, []).option("-e, --env <KEY=value>", "Environment variable for the sandbox (repeatable)", collect, []);
34996
+ addCreateOptions(gpu.command("create")).description("Start a GPU sandbox and wait until it is running").option("--no-wait", "Return while the sandbox is still starting").action(run(gpuCreateCommand));
34997
+ gpu.command("list").description("List the running GPU sandboxes of this workspace").action(run(gpuListCommand));
34998
+ gpu.command("exec [command...]").description("Run a shell command in a GPU sandbox and stream its output: replicas gpu exec -- python train.py").option("-s, --sandbox <id>", "Sandbox ID (defaults to the only running sandbox)").option("-w, --workdir <dir>", `Working directory (default ${COMPUTE_SANDBOX_WORKDIR})`).option("-e, --env <KEY=value>", "Environment variable for the command (repeatable)", collect, []).option("-d, --detach", 'Run in the background and print a job ID for "replicas gpu logs"').action(run(gpuExecCommand));
34999
+ gpu.command("logs <jobId>").description("Print the output of a detached job").option("-s, --sandbox <id>", "Sandbox ID (defaults to the only running sandbox)").option("-f, --follow", "Stream until the job exits and exit with its code").action(run(gpuLogsCommand));
35000
+ gpu.command("push [local] [remote]").description(`Copy a local file or directory into a GPU sandbox (default . to ${COMPUTE_SANDBOX_WORKDIR}); git repositories honor .gitignore`).option("-s, --sandbox <id>", "Sandbox ID (defaults to the only running sandbox)").action(run(gpuPushCommand));
35001
+ gpu.command("pull <remote> [local]").description("Copy a file or directory out of a GPU sandbox into a local directory (default .)").option("-s, --sandbox <id>", "Sandbox ID (defaults to the only running sandbox)").action(run(gpuPullCommand));
35002
+ addCreateOptions(gpu.command("run [command...]")).description("Start a GPU sandbox, copy the current directory in, run a command, then stop it: replicas gpu run --gpu H100 -- python train.py").option("--pull <path>", `Copy a path (relative to ${COMPUTE_SANDBOX_WORKDIR}) back into the current directory afterwards (repeatable)`, collect, []).option("--keep", "Keep the sandbox running afterwards").action(run(gpuRunCommand));
35003
+ gpu.command("delete [id]").description("Stop a GPU sandbox").option("-s, --sandbox <id>", "Sandbox ID (defaults to the only running sandbox)").action(run(gpuDeleteCommand));
34675
35004
  program.command("history").description("Read past session history").command("transcript <name>").description("Download the full transcript of a session in INDEX.md and print its path").action(run(historyTranscriptCommand));
34676
35005
  skills.command("suggest <file>").description("Suggest saving a SKILL.md as an organization skill; the user reviews it before it is saved").requiredOption("--title <text>", 'Short title shown to the user, e.g. "Always use Conventional Commits"').action(run(skillsSuggestCommand));
34677
35006
  program.command("computer").description("Drive the workspace Linux desktop through replicas-computer");
@@ -34684,6 +35013,7 @@ if (isAgentMode()) {
34684
35013
  "preview",
34685
35014
  "service",
34686
35015
  "media",
35016
+ "gpu",
34687
35017
  "history",
34688
35018
  "skills",
34689
35019
  "slack",
@@ -34721,9 +35051,9 @@ if (isAgentMode()) {
34721
35051
  }
34722
35052
  async function main() {
34723
35053
  if (process.argv[2] === "computer" && isAgentMode()) {
34724
- const result = (0, import_node_child_process3.spawnSync)("replicas-computer", process.argv.slice(3), { stdio: "inherit" });
35054
+ const result = (0, import_node_child_process4.spawnSync)("replicas-computer", process.argv.slice(3), { stdio: "inherit" });
34725
35055
  if (result.error) {
34726
- console.error(import_chalk31.default.red(`
35056
+ console.error(import_chalk32.default.red(`
34727
35057
  \u2717 replicas-computer is unavailable in this workspace image
34728
35058
  `));
34729
35059
  process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-cli",
3
- "version": "0.3.102",
3
+ "version": "0.3.104",
4
4
  "description": "CLI for managing Replicas workspaces - SSH into cloud dev environments with automatic port forwarding",
5
5
  "main": "dist/index.cjs",
6
6
  "bin": {