replicas-cli 0.2.202 → 0.2.204

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.mjs +1314 -1115
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -7491,642 +7491,153 @@ async function getCurrentUser() {
7491
7491
  };
7492
7492
  }
7493
7493
 
7494
- // src/lib/api.ts
7495
- var MONOLITH_URL = process.env.REPLICAS_MONOLITH_URL || "https://api.tryreplicas.com";
7496
- async function authenticatedFetch(url, options) {
7497
- const token = await getValidToken();
7498
- if (!token) {
7499
- throw new Error("No access token available");
7500
- }
7501
- return apiFetch(url, {
7502
- "Authorization": `Bearer ${token}`,
7503
- ...options?.headers || {}
7504
- }, options);
7494
+ // ../shared/src/agent.ts
7495
+ var VALID_AGENT_PROVIDERS = ["claude", "codex", "relay"];
7496
+ function isValidAgentProvider(value) {
7497
+ return VALID_AGENT_PROVIDERS.some((p) => p === value);
7505
7498
  }
7506
- var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
7507
- var MAX_REDIRECTS = 5;
7508
- async function fetchWithRedirects(url, init, attempt = 0) {
7509
- const response = await fetch(url, { ...init, redirect: "manual" });
7510
- if (REDIRECT_STATUSES.has(response.status)) {
7511
- if (attempt >= MAX_REDIRECTS) {
7512
- throw new Error("Too many redirects while contacting the Replicas API");
7513
- }
7514
- const location = response.headers.get("location");
7515
- if (!location) {
7516
- throw new Error(`Redirect status ${response.status} missing Location header`);
7517
- }
7518
- const nextUrl = new URL(location, url).toString();
7519
- const shouldForceGet = response.status === 303 && init.method !== void 0 && init.method.toUpperCase() !== "GET";
7520
- const nextInit = {
7521
- ...init,
7522
- method: shouldForceGet ? "GET" : init.method,
7523
- body: shouldForceGet ? void 0 : init.body
7524
- };
7525
- return fetchWithRedirects(nextUrl, nextInit, attempt + 1);
7526
- }
7527
- return response;
7499
+ var VALID_THINKING_LEVELS = ["low", "medium", "high", "max"];
7500
+ function isValidThinkingLevel(value) {
7501
+ return VALID_THINKING_LEVELS.some((l) => l === value);
7528
7502
  }
7529
- async function orgAuthenticatedFetch(url, options) {
7530
- let bearer;
7531
- const extraHeaders = {};
7532
- if (isAgentMode()) {
7533
- const agent = readAgentConfig();
7534
- if (!agent) {
7535
- throw new Error("Agent mode config is incomplete");
7536
- }
7537
- bearer = agent.engine_secret;
7538
- extraHeaders["X-Workspace-Id"] = agent.workspace_id;
7539
- } else {
7540
- bearer = await getValidToken();
7541
- const organizationId = getOrganizationId();
7542
- if (!organizationId) {
7543
- throw new Error(
7544
- 'No organization selected. Please run "replicas org switch" to select an organization.'
7545
- );
7546
- }
7547
- extraHeaders["Replicas-Org-Id"] = organizationId;
7503
+ function getProviderDisplayName(provider) {
7504
+ switch (provider) {
7505
+ case "codex":
7506
+ return "Codex";
7507
+ case "relay":
7508
+ return "Relay";
7509
+ case "claude":
7510
+ return "Claude";
7548
7511
  }
7549
- return apiFetch(url, {
7550
- "Authorization": `Bearer ${bearer}`,
7551
- ...extraHeaders,
7552
- ...options?.headers || {}
7553
- }, options);
7554
7512
  }
7555
- async function apiFetch(url, headers, options) {
7556
- const requestHeaders = {
7557
- "Content-Type": "application/json",
7558
- ...headers
7559
- };
7560
- const absoluteUrl = `${MONOLITH_URL}${url}`;
7561
- const requestInit = {
7562
- ...options,
7563
- headers: requestHeaders,
7564
- body: options?.body !== void 0 ? JSON.stringify(options.body) : void 0
7513
+
7514
+ // ../shared/src/event.ts
7515
+ var USER_MESSAGE_ID_PAYLOAD_KEY = "replicasMessageId";
7516
+ var CODEX_ASP_ITEM_ID_PAYLOAD_KEY = "codexAspItemId";
7517
+ var CODEX_QUOTA_STATUS_EVENT_TYPE = "codex-quota-status";
7518
+
7519
+ // ../shared/src/pricing.ts
7520
+ var PLANS = {
7521
+ hobby: {
7522
+ id: "hobby",
7523
+ name: "Hobby",
7524
+ monthlyPrice: 0,
7525
+ seatPriceCents: 0,
7526
+ creditsIncluded: 1200,
7527
+ features: [
7528
+ "1,200 minutes of human-initiated workspace usage (one-time)",
7529
+ "1,200 minutes of API + automation usage (one-time)",
7530
+ "Automations (limited to 2)",
7531
+ "Warm pools and warm hooks",
7532
+ "Up to 3 repositories",
7533
+ "Up to 5 environments"
7534
+ ]
7535
+ },
7536
+ developer: {
7537
+ id: "developer",
7538
+ name: "Developer",
7539
+ monthlyPrice: 120,
7540
+ seatPriceCents: 12e3,
7541
+ creditsIncluded: 0,
7542
+ features: [
7543
+ "Unlimited human-initiated workspaces",
7544
+ "5,000 included automation/API minutes per month",
7545
+ "Up to 10 repositories",
7546
+ "Up to 15 environments",
7547
+ "Up to 5 automations",
7548
+ "Warm pools and warm hooks",
7549
+ "API access"
7550
+ ]
7551
+ },
7552
+ team: {
7553
+ id: "team",
7554
+ name: "Team",
7555
+ monthlyPrice: 300,
7556
+ seatPriceCents: 3e4,
7557
+ creditsIncluded: 0,
7558
+ features: [
7559
+ "Unlimited human-initiated workspaces",
7560
+ "15,000 included automation/API minutes per month",
7561
+ "Unlimited repositories",
7562
+ "Unlimited automations",
7563
+ "Higher API rate limits",
7564
+ "Warm pools and warm hooks",
7565
+ "Auto-upgraded sandbox resources (4 vCPU, 32 GB disk, 16 GB memory)",
7566
+ "Shared Slack support channel"
7567
+ ]
7568
+ },
7569
+ enterprise: {
7570
+ id: "enterprise",
7571
+ name: "Enterprise",
7572
+ monthlyPrice: 0,
7573
+ seatPriceCents: 0,
7574
+ creditsIncluded: 0,
7575
+ features: [
7576
+ "Unlimited usage",
7577
+ "Unlimited automations",
7578
+ "Custom API rates",
7579
+ "Custom rate limits",
7580
+ "Custom warm hooks and pools",
7581
+ "Shared Slack support channel",
7582
+ "SOC 2"
7583
+ ]
7584
+ }
7585
+ };
7586
+ var TEAM_PLAN = PLANS.team;
7587
+ var ENTERPRISE_PLAN = PLANS.enterprise;
7588
+
7589
+ // ../shared/src/sandbox.ts
7590
+ var SANDBOX_LIFECYCLE = {
7591
+ AUTO_STOP_MINUTES: 60,
7592
+ AUTO_ARCHIVE_MINUTES: 60 * 24 * 7,
7593
+ AUTO_DELETE_MINUTES: -1,
7594
+ SSH_TOKEN_EXPIRATION_MINUTES: 3 * 60
7595
+ };
7596
+ function buildPaths(homeDir) {
7597
+ return {
7598
+ HOME_DIR: homeDir,
7599
+ WORKSPACES_DIR: `${homeDir}/workspaces`,
7600
+ REPLICAS_DIR: `${homeDir}/.replicas`,
7601
+ REPLICAS_FILES_DIR: `${homeDir}/.replicas/files`,
7602
+ REPLICAS_RUNTIME_ENV_FILE: `${homeDir}/.replicas/runtime-env.sh`
7565
7603
  };
7566
- const response = await fetchWithRedirects(absoluteUrl, requestInit);
7567
- if (!response.ok) {
7568
- const error = await response.json().catch(() => ({ error: "Request failed" }));
7569
- throw new Error(error.error || `Request failed with status ${response.status}`);
7604
+ }
7605
+ var DAYTONA_PATHS = buildPaths("/home/ubuntu");
7606
+ var E2B_PATHS = buildPaths("/home/user");
7607
+ function getSandboxPaths(providerId) {
7608
+ switch (providerId) {
7609
+ case "daytona":
7610
+ return DAYTONA_PATHS;
7611
+ case "e2b":
7612
+ return E2B_PATHS;
7570
7613
  }
7571
- return response.json();
7572
7614
  }
7615
+ var WORKSPACE_SIZES = ["small", "large"];
7616
+ var INVALID_WORKSPACE_SIZE_ERROR = `Invalid size: must be one of ${WORKSPACE_SIZES.join(", ")}`;
7573
7617
 
7574
- // src/lib/organization.ts
7575
- async function fetchOrganizations() {
7576
- const response = await authenticatedFetch(
7577
- "/v1/user/organizations"
7578
- );
7579
- return response.organizations;
7618
+ // ../shared/src/urls.ts
7619
+ function getWorkspaceDashboardUrl(workspaceId, options = {}) {
7620
+ const { encodeWorkspaceId = true, mode, mediaId } = options;
7621
+ const workspaceIdValue = encodeWorkspaceId ? encodeURIComponent(workspaceId) : workspaceId;
7622
+ const effectiveMode = mediaId ? mode ?? "media" : mode;
7623
+ const params = [];
7624
+ if (effectiveMode) params.push(`mode=${encodeURIComponent(effectiveMode)}`);
7625
+ if (mediaId) params.push(`media=${encodeURIComponent(mediaId)}`);
7626
+ const query = params.length > 0 ? `?${params.join("&")}` : "";
7627
+ return `https://tryreplicas.com/workspaces/${workspaceIdValue}${query}`;
7580
7628
  }
7581
- async function setActiveOrganization(organizationId) {
7582
- const organizations = await fetchOrganizations();
7583
- const organization = organizations.find((org2) => org2.id === organizationId);
7584
- if (!organization) {
7585
- throw new Error(`Organization with ID ${organizationId} not found or you don't have access to it.`);
7586
- }
7587
- setOrganizationId(organizationId);
7629
+ var PR_URL_REGEX = /github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/;
7630
+ function parsePrUrl(url) {
7631
+ const match = url.match(PR_URL_REGEX);
7632
+ if (!match) return null;
7633
+ const [, owner, repo, numberStr] = match;
7634
+ const number = Number.parseInt(numberStr, 10);
7635
+ if (!Number.isFinite(number)) return null;
7636
+ return { owner, repo, number };
7588
7637
  }
7589
- async function ensureOrganization() {
7590
- const organizations = await fetchOrganizations();
7591
- if (organizations.length === 0) {
7592
- throw new Error("You are not a member of any organization. Please contact support.");
7593
- }
7594
- const defaultOrg = organizations[0];
7595
- setOrganizationId(defaultOrg.id);
7596
- return defaultOrg.id;
7597
- }
7598
-
7599
- // src/commands/login.ts
7600
- var WEB_APP_URL = process.env.REPLICAS_WEB_URL || "https://tryreplicas.com";
7601
- function generateState() {
7602
- return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
7603
- }
7604
- async function loginCommand() {
7605
- const state = generateState();
7606
- return new Promise((resolve2, reject) => {
7607
- let authTimeout;
7608
- let hasHandledCallback = false;
7609
- let lastRedirectUrl = null;
7610
- let lastMessage = null;
7611
- const pendingResponses = /* @__PURE__ */ new Set();
7612
- function respondWithRedirect(res) {
7613
- if (lastRedirectUrl) {
7614
- res.writeHead(302, {
7615
- "Location": lastRedirectUrl,
7616
- "Connection": "close"
7617
- });
7618
- res.end();
7619
- } else {
7620
- res.writeHead(200, {
7621
- "Content-Type": "text/html; charset=utf-8",
7622
- "Connection": "close"
7623
- });
7624
- res.end(`
7625
- <!DOCTYPE html>
7626
- <html lang="en">
7627
- <head>
7628
- <meta charset="utf-8" />
7629
- <title>Replicas CLI Login</title>
7630
- <style>
7631
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background:#09090b; color:#f4f4f5; display:flex; align-items:center; justify-content:center; height:100vh; margin:0; }
7632
- .card { border:1px solid #27272a; padding:32px; border-radius:12px; background:#0f0f12; max-width:420px; text-align:center; }
7633
- .title { font-size:20px; margin-bottom:12px; letter-spacing:0.08em; text-transform:uppercase; color:#a855f7; }
7634
- .message { font-family: 'Menlo', 'Source Code Pro', monospace; font-size:14px; line-height:1.6; color:#d4d4d8; white-space:pre-line; }
7635
- </style>
7636
- </head>
7637
- <body>
7638
- <div class="card">
7639
- <div class="title">Replicas CLI</div>
7640
- <div class="message">${lastMessage || "Authentication already processed. You can close this tab."}</div>
7641
- </div>
7642
- </body>
7643
- </html>
7644
- `);
7645
- }
7646
- }
7647
- function flushPendingResponses() {
7648
- for (const response of pendingResponses) {
7649
- respondWithRedirect(response);
7650
- }
7651
- pendingResponses.clear();
7652
- }
7653
- const server = http.createServer(async (req, res) => {
7654
- const url = new URL2(req.url || "", `http://${req.headers.host}`);
7655
- if (url.pathname === "/callback") {
7656
- if (hasHandledCallback) {
7657
- if (!lastRedirectUrl) {
7658
- pendingResponses.add(res);
7659
- res.on("close", () => pendingResponses.delete(res));
7660
- return;
7661
- }
7662
- respondWithRedirect(res);
7663
- return;
7664
- }
7665
- const returnedState = url.searchParams.get("state");
7666
- const accessToken = url.searchParams.get("access_token");
7667
- const refreshToken2 = url.searchParams.get("refresh_token");
7668
- const expiresAt = url.searchParams.get("expires_at");
7669
- const error = url.searchParams.get("error");
7670
- if (error) {
7671
- const errorUrl = `${WEB_APP_URL}/cli-login/error?message=${encodeURIComponent(error)}`;
7672
- lastRedirectUrl = errorUrl;
7673
- lastMessage = `Authentication failed: ${error}`;
7674
- respondWithRedirect(res);
7675
- flushPendingResponses();
7676
- clearTimeout(authTimeout);
7677
- setImmediate(() => {
7678
- server.closeAllConnections?.();
7679
- server.close();
7680
- reject(new Error(`Authentication failed: ${error}`));
7681
- });
7682
- return;
7683
- }
7684
- if (returnedState !== state) {
7685
- const errorUrl = `${WEB_APP_URL}/cli-login/error?message=${encodeURIComponent("Invalid state parameter. This might be a CSRF attack.")}`;
7686
- lastRedirectUrl = errorUrl;
7687
- lastMessage = "Invalid state parameter. This might be a CSRF attack.";
7688
- respondWithRedirect(res);
7689
- flushPendingResponses();
7690
- clearTimeout(authTimeout);
7691
- setImmediate(() => {
7692
- server.closeAllConnections?.();
7693
- server.close();
7694
- reject(new Error("Invalid state parameter"));
7695
- });
7696
- return;
7697
- }
7698
- if (!accessToken || !refreshToken2 || !expiresAt) {
7699
- const errorUrl = `${WEB_APP_URL}/cli-login/error?message=${encodeURIComponent("Missing required authentication tokens.")}`;
7700
- lastRedirectUrl = errorUrl;
7701
- lastMessage = "Missing required authentication tokens.";
7702
- respondWithRedirect(res);
7703
- flushPendingResponses();
7704
- clearTimeout(authTimeout);
7705
- setImmediate(() => {
7706
- server.closeAllConnections?.();
7707
- server.close();
7708
- reject(new Error("Missing authentication tokens"));
7709
- });
7710
- return;
7711
- }
7712
- hasHandledCallback = true;
7713
- const existingConfig = readConfig();
7714
- const config2 = {
7715
- access_token: accessToken,
7716
- refresh_token: refreshToken2,
7717
- expires_at: parseInt(expiresAt, 10),
7718
- organization_id: existingConfig?.organization_id,
7719
- ide_command: existingConfig?.ide_command
7720
- };
7721
- try {
7722
- writeConfig(config2);
7723
- const user = await getCurrentUser();
7724
- try {
7725
- const orgId = await ensureOrganization();
7726
- console.log(chalk.gray(` Organization: ${orgId}`));
7727
- } catch (orgError) {
7728
- console.log(chalk.yellow(" Warning: Could not fetch organizations"));
7729
- console.log(chalk.gray(" You can set your organization later with: replicas org switch"));
7730
- }
7731
- const successUrl = `${WEB_APP_URL}/cli-login/success?email=${encodeURIComponent(user.email)}`;
7732
- lastRedirectUrl = successUrl;
7733
- lastMessage = `Successfully logged in as ${user.email}. You can close this tab.`;
7734
- respondWithRedirect(res);
7735
- flushPendingResponses();
7736
- console.log(chalk.green("\n\u2713 Successfully logged in!"));
7737
- console.log(chalk.gray(` Email: ${user.email}
7738
- `));
7739
- clearTimeout(authTimeout);
7740
- setImmediate(() => {
7741
- server.closeAllConnections?.();
7742
- server.close();
7743
- resolve2();
7744
- });
7745
- } catch (error2) {
7746
- const errorUrl = `${WEB_APP_URL}/cli-login/error?message=${encodeURIComponent("Failed to verify authentication.")}`;
7747
- lastRedirectUrl = errorUrl;
7748
- lastMessage = "Failed to verify authentication.";
7749
- respondWithRedirect(res);
7750
- flushPendingResponses();
7751
- clearTimeout(authTimeout);
7752
- setImmediate(() => {
7753
- server.closeAllConnections?.();
7754
- server.close();
7755
- reject(error2);
7756
- });
7757
- }
7758
- } else {
7759
- res.writeHead(404);
7760
- res.end("Not found");
7761
- }
7762
- });
7763
- server.listen(0, "localhost", () => {
7764
- const address = server.address();
7765
- if (!address || typeof address === "string") {
7766
- reject(new Error("Failed to start server"));
7767
- return;
7768
- }
7769
- const port = address.port;
7770
- const loginUrl = `${WEB_APP_URL}/cli-login?port=${port}&state=${state}`;
7771
- console.log(chalk.blue("\nOpening browser for authentication..."));
7772
- console.log(chalk.gray(`If the browser doesn't open automatically, visit:
7773
- ${loginUrl}
7774
- `));
7775
- open(loginUrl).catch((error) => {
7776
- console.log(chalk.yellow("Failed to open browser automatically."));
7777
- console.log(chalk.gray(`Please open this URL manually:
7778
- ${loginUrl}
7779
- `));
7780
- });
7781
- });
7782
- authTimeout = setTimeout(() => {
7783
- server.closeAllConnections?.();
7784
- server.close();
7785
- reject(new Error("Authentication timeout. Please try again."));
7786
- }, 5 * 60 * 1e3);
7787
- });
7788
- }
7789
-
7790
- // src/commands/logout.ts
7791
- import chalk2 from "chalk";
7792
- function logoutCommand() {
7793
- if (!isAuthenticated()) {
7794
- console.log(chalk2.yellow("You are not logged in."));
7795
- return;
7796
- }
7797
- deleteConfig();
7798
- console.log(chalk2.green("\u2713 Successfully logged out!"));
7799
- }
7800
-
7801
- // src/commands/whoami.ts
7802
- import chalk3 from "chalk";
7803
- async function whoamiCommand() {
7804
- if (isAgentMode()) {
7805
- const agent = readAgentConfig();
7806
- if (!agent) {
7807
- console.log(chalk3.red("Agent mode config is incomplete."));
7808
- process.exit(1);
7809
- }
7810
- console.log(chalk3.blue("\nWorkspace identity:"));
7811
- console.log(chalk3.gray(` Workspace ID: ${agent.workspace_id}`));
7812
- if (agent.organization_id) {
7813
- console.log(chalk3.gray(` Organization ID: ${agent.organization_id}`));
7814
- }
7815
- console.log();
7816
- return;
7817
- }
7818
- if (!isAuthenticated()) {
7819
- console.log(chalk3.yellow("You are not logged in."));
7820
- console.log(chalk3.gray('Run "replicas login" to authenticate.'));
7821
- return;
7822
- }
7823
- try {
7824
- const user = await getCurrentUser();
7825
- console.log(chalk3.blue("\nCurrent User:"));
7826
- console.log(chalk3.gray(` ID: ${user.id}`));
7827
- console.log(chalk3.gray(` Email: ${user.email}
7828
- `));
7829
- } catch (error) {
7830
- console.error(chalk3.red("Failed to get user information."));
7831
- if (error instanceof Error) {
7832
- console.error(chalk3.gray(error.message));
7833
- }
7834
- console.log(chalk3.gray('\nTry running "replicas login" again.'));
7835
- process.exit(1);
7836
- }
7837
- }
7838
-
7839
- // src/commands/connect.ts
7840
- import chalk5 from "chalk";
7841
-
7842
- // src/lib/ssh.ts
7843
- import { spawn } from "child_process";
7844
- var SSH_OPTIONS = ["-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"];
7845
- async function connectSSH(token, host, proxyCommand) {
7846
- return new Promise((resolve2, reject) => {
7847
- const sshArgs = proxyCommand ? [...SSH_OPTIONS, "-o", `ProxyCommand=${proxyCommand}`, `${token}@${host}`] : [...SSH_OPTIONS, `${token}@${host}`];
7848
- const ssh = spawn("ssh", sshArgs, {
7849
- stdio: "inherit"
7850
- });
7851
- ssh.on("close", () => {
7852
- resolve2();
7853
- });
7854
- ssh.on("error", reject);
7855
- });
7856
- }
7857
-
7858
- // src/lib/workspace-connection.ts
7859
- import chalk4 from "chalk";
7860
- import prompts from "prompts";
7861
-
7862
- // src/lib/git.ts
7863
- import { execSync } from "child_process";
7864
- import path2 from "path";
7865
- function getGitRoot() {
7866
- try {
7867
- const root = execSync("git rev-parse --show-toplevel", {
7868
- encoding: "utf-8",
7869
- stdio: ["pipe", "pipe", "pipe"]
7870
- }).trim();
7871
- return root;
7872
- } catch (error) {
7873
- throw new Error("Not inside a git repository");
7874
- }
7875
- }
7876
- function getGitRepoName() {
7877
- const root = getGitRoot();
7878
- return path2.basename(root);
7879
- }
7880
- function isInsideGitRepo() {
7881
- try {
7882
- execSync("git rev-parse --is-inside-work-tree", {
7883
- encoding: "utf-8",
7884
- stdio: ["pipe", "pipe", "pipe"]
7885
- });
7886
- return true;
7887
- } catch {
7888
- return false;
7889
- }
7890
- }
7891
-
7892
- // src/lib/workspace-connection.ts
7893
- async function prepareWorkspaceConnection(workspaceName) {
7894
- const orgId = getOrganizationId();
7895
- if (!orgId) {
7896
- throw new Error('No organization selected. Please run "replicas org switch" first.');
7897
- }
7898
- console.log(chalk4.blue(`
7899
- Searching for workspace: ${workspaceName}...`));
7900
- const response = await orgAuthenticatedFetch(
7901
- `/v1/workspaces?name=${encodeURIComponent(workspaceName)}`
7902
- );
7903
- if (response.workspaces.length === 0) {
7904
- throw new Error(`No workspaces found with name matching "${workspaceName}".`);
7905
- }
7906
- let selectedWorkspace;
7907
- if (response.workspaces.length === 1) {
7908
- selectedWorkspace = response.workspaces[0];
7909
- } else {
7910
- console.log(chalk4.yellow(`
7911
- Found ${response.workspaces.length} workspaces matching "${workspaceName}":`));
7912
- const selectResponse = await prompts({
7913
- type: "select",
7914
- name: "workspaceId",
7915
- message: "Select a workspace:",
7916
- choices: response.workspaces.map((ws) => ({
7917
- title: `${ws.name} (${ws.status || "unknown"})`,
7918
- value: ws.id,
7919
- description: `Status: ${ws.status || "unknown"}`
7920
- }))
7921
- });
7922
- if (!selectResponse.workspaceId) {
7923
- throw new Error("Workspace selection cancelled.");
7924
- }
7925
- selectedWorkspace = response.workspaces.find((ws) => ws.id === selectResponse.workspaceId);
7926
- }
7927
- console.log(chalk4.green(`
7928
- \u2713 Selected workspace: ${selectedWorkspace.name}`));
7929
- console.log(chalk4.gray(` Status: ${selectedWorkspace.status || "unknown"}`));
7930
- if (selectedWorkspace.status === "sleeping") {
7931
- throw new Error(
7932
- "Workspace is currently sleeping. Wake it using `replicas app` (press w on a sleeping workspace) or visit https://tryreplicas.com"
7933
- );
7934
- }
7935
- console.log(chalk4.blue("\nRequesting SSH access token..."));
7936
- const tokenResponse = await orgAuthenticatedFetch(
7937
- `/v1/workspaces/${selectedWorkspace.id}/ssh-token`,
7938
- { method: "POST" }
7939
- );
7940
- console.log(chalk4.green("\u2713 SSH token received"));
7941
- let repoName = null;
7942
- if (isInsideGitRepo()) {
7943
- try {
7944
- repoName = getGitRepoName();
7945
- } catch {
7946
- }
7947
- }
7948
- return {
7949
- workspace: selectedWorkspace,
7950
- sshToken: tokenResponse.token,
7951
- sshHost: tokenResponse.host,
7952
- sshProxyCommand: tokenResponse.proxyCommand,
7953
- repoName
7954
- };
7955
- }
7956
-
7957
- // src/commands/connect.ts
7958
- async function connectCommand(workspaceName) {
7959
- if (!isAuthenticated()) {
7960
- console.log(chalk5.red('Not logged in. Please run "replicas login" first.'));
7961
- process.exit(1);
7962
- }
7963
- try {
7964
- const { workspace, sshToken, sshHost, sshProxyCommand } = await prepareWorkspaceConnection(workspaceName);
7965
- console.log(chalk5.blue(`
7966
- Connecting to ${workspace.name}...`));
7967
- const sshCommand = `ssh ${sshToken}@${sshHost}`;
7968
- console.log(chalk5.gray(`SSH command: ${sshCommand}`));
7969
- console.log(chalk5.gray("\nPress Ctrl+D to disconnect.\n"));
7970
- await connectSSH(sshToken, sshHost, sshProxyCommand);
7971
- console.log(chalk5.green("\n\u2713 Disconnected from workspace.\n"));
7972
- } catch (error) {
7973
- console.error(chalk5.red(`
7974
- Error: ${error instanceof Error ? error.message : "Unknown error"}`));
7975
- process.exit(1);
7976
- }
7977
- }
7978
-
7979
- // src/commands/code.ts
7980
- import chalk6 from "chalk";
7981
- import { spawn as spawn2 } from "child_process";
7982
-
7983
- // ../shared/src/agent.ts
7984
- var VALID_AGENT_PROVIDERS = ["claude", "codex", "relay"];
7985
- function isValidAgentProvider(value) {
7986
- return VALID_AGENT_PROVIDERS.some((p) => p === value);
7987
- }
7988
- var VALID_THINKING_LEVELS = ["low", "medium", "high", "max"];
7989
- function isValidThinkingLevel(value) {
7990
- return VALID_THINKING_LEVELS.some((l) => l === value);
7991
- }
7992
- function getProviderDisplayName(provider) {
7993
- switch (provider) {
7994
- case "codex":
7995
- return "Codex";
7996
- case "relay":
7997
- return "Relay";
7998
- case "claude":
7999
- return "Claude";
8000
- }
8001
- }
8002
-
8003
- // ../shared/src/event.ts
8004
- var USER_MESSAGE_ID_PAYLOAD_KEY = "replicasMessageId";
8005
- var CODEX_ASP_ITEM_ID_PAYLOAD_KEY = "codexAspItemId";
8006
- var CODEX_QUOTA_STATUS_EVENT_TYPE = "codex-quota-status";
8007
-
8008
- // ../shared/src/pricing.ts
8009
- var PLANS = {
8010
- hobby: {
8011
- id: "hobby",
8012
- name: "Hobby",
8013
- monthlyPrice: 0,
8014
- seatPriceCents: 0,
8015
- creditsIncluded: 1200,
8016
- features: [
8017
- "1,200 minutes of human-initiated workspace usage (one-time)",
8018
- "1,200 minutes of API + automation usage (one-time)",
8019
- "Automations (limited to 2)",
8020
- "Warm pools and warm hooks",
8021
- "Up to 3 repositories",
8022
- "Up to 5 environments"
8023
- ]
8024
- },
8025
- developer: {
8026
- id: "developer",
8027
- name: "Developer",
8028
- monthlyPrice: 120,
8029
- seatPriceCents: 12e3,
8030
- creditsIncluded: 0,
8031
- features: [
8032
- "Unlimited human-initiated workspaces",
8033
- "5,000 included automation/API minutes per month",
8034
- "Up to 10 repositories",
8035
- "Up to 15 environments",
8036
- "Up to 5 automations",
8037
- "Warm pools and warm hooks",
8038
- "API access"
8039
- ]
8040
- },
8041
- team: {
8042
- id: "team",
8043
- name: "Team",
8044
- monthlyPrice: 300,
8045
- seatPriceCents: 3e4,
8046
- creditsIncluded: 0,
8047
- features: [
8048
- "Unlimited human-initiated workspaces",
8049
- "15,000 included automation/API minutes per month",
8050
- "Unlimited repositories",
8051
- "Unlimited automations",
8052
- "Higher API rate limits",
8053
- "Warm pools and warm hooks",
8054
- "Auto-upgraded sandbox resources (4 vCPU, 32 GB disk, 16 GB memory)",
8055
- "Shared Slack support channel"
8056
- ]
8057
- },
8058
- enterprise: {
8059
- id: "enterprise",
8060
- name: "Enterprise",
8061
- monthlyPrice: 0,
8062
- seatPriceCents: 0,
8063
- creditsIncluded: 0,
8064
- features: [
8065
- "Unlimited usage",
8066
- "Unlimited automations",
8067
- "Custom API rates",
8068
- "Custom rate limits",
8069
- "Custom warm hooks and pools",
8070
- "Shared Slack support channel",
8071
- "SOC 2"
8072
- ]
8073
- }
8074
- };
8075
- var TEAM_PLAN = PLANS.team;
8076
- var ENTERPRISE_PLAN = PLANS.enterprise;
8077
-
8078
- // ../shared/src/sandbox.ts
8079
- var SANDBOX_LIFECYCLE = {
8080
- AUTO_STOP_MINUTES: 60,
8081
- AUTO_ARCHIVE_MINUTES: 60 * 24 * 7,
8082
- AUTO_DELETE_MINUTES: -1,
8083
- SSH_TOKEN_EXPIRATION_MINUTES: 3 * 60
8084
- };
8085
- function buildPaths(homeDir) {
8086
- return {
8087
- HOME_DIR: homeDir,
8088
- WORKSPACES_DIR: `${homeDir}/workspaces`,
8089
- REPLICAS_DIR: `${homeDir}/.replicas`,
8090
- REPLICAS_FILES_DIR: `${homeDir}/.replicas/files`,
8091
- REPLICAS_RUNTIME_ENV_FILE: `${homeDir}/.replicas/runtime-env.sh`
8092
- };
8093
- }
8094
- var DAYTONA_PATHS = buildPaths("/home/ubuntu");
8095
- var E2B_PATHS = buildPaths("/home/user");
8096
- function getSandboxPaths(providerId) {
8097
- switch (providerId) {
8098
- case "daytona":
8099
- return DAYTONA_PATHS;
8100
- case "e2b":
8101
- return E2B_PATHS;
8102
- }
8103
- }
8104
- var WORKSPACE_SIZES = ["small", "large"];
8105
- var INVALID_WORKSPACE_SIZE_ERROR = `Invalid size: must be one of ${WORKSPACE_SIZES.join(", ")}`;
8106
-
8107
- // ../shared/src/urls.ts
8108
- function getWorkspaceDashboardUrl(workspaceId, options = {}) {
8109
- const { encodeWorkspaceId = true, mode, mediaId } = options;
8110
- const workspaceIdValue = encodeWorkspaceId ? encodeURIComponent(workspaceId) : workspaceId;
8111
- const effectiveMode = mediaId ? mode ?? "media" : mode;
8112
- const params = [];
8113
- if (effectiveMode) params.push(`mode=${encodeURIComponent(effectiveMode)}`);
8114
- if (mediaId) params.push(`media=${encodeURIComponent(mediaId)}`);
8115
- const query = params.length > 0 ? `?${params.join("&")}` : "";
8116
- return `https://tryreplicas.com/workspaces/${workspaceIdValue}${query}`;
8117
- }
8118
- var PR_URL_REGEX = /github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/;
8119
- function parsePrUrl(url) {
8120
- const match = url.match(PR_URL_REGEX);
8121
- if (!match) return null;
8122
- const [, owner, repo, numberStr] = match;
8123
- const number = Number.parseInt(numberStr, 10);
8124
- if (!Number.isFinite(number)) return null;
8125
- return { owner, repo, number };
8126
- }
8127
- function extractPrNumber(url) {
8128
- const parsed = parsePrUrl(url);
8129
- return parsed ? String(parsed.number) : null;
7638
+ function extractPrNumber(url) {
7639
+ const parsed = parsePrUrl(url);
7640
+ return parsed ? String(parsed.number) : null;
8130
7641
  }
8131
7642
 
8132
7643
  // ../shared/src/default-skills/replicas-agent/abilities/computer.ts
@@ -9822,6 +9333,19 @@ function parseCodexEvents(events) {
9822
9333
  return messages;
9823
9334
  }
9824
9335
 
9336
+ // ../shared/src/display-message/parsers/mcp.ts
9337
+ function parseMcpToolName(name) {
9338
+ const prefix = "mcp__";
9339
+ if (!name.startsWith(prefix)) return null;
9340
+ const rest = name.slice(prefix.length);
9341
+ const sep = rest.indexOf("__");
9342
+ if (sep <= 0) return null;
9343
+ const server = rest.slice(0, sep);
9344
+ const tool = rest.slice(sep + 2);
9345
+ if (!server || !tool) return null;
9346
+ return { server, tool };
9347
+ }
9348
+
9825
9349
  // ../shared/src/display-message/parsers/claude-parser.ts
9826
9350
  function safeJsonParse2(str, fallback) {
9827
9351
  try {
@@ -10023,11 +9547,12 @@ function parseClaudeEvents(events, parentToolUseId) {
10023
9547
  });
10024
9548
  } else if (toolName === "mcp__relay-subagent-tools__delete_agent") {
10025
9549
  const inputObj = typeof toolInput === "string" ? safeJsonParse2(toolInput, {}) : toolInput;
9550
+ const mcp = parseMcpToolName(toolName);
10026
9551
  messages.push({
10027
9552
  id: `toolcall-${event.timestamp}-${messages.length}`,
10028
9553
  type: "tool_call",
10029
- server: "relay-subagent-tools",
10030
- tool: "delete_agent",
9554
+ server: mcp?.server ?? "relay-subagent-tools",
9555
+ tool: mcp?.tool ?? "delete_agent",
10031
9556
  input: inputObj,
10032
9557
  status: "in_progress",
10033
9558
  timestamp: event.timestamp
@@ -10146,530 +9671,1060 @@ function parseAgentEvents(events, agentType) {
10146
9671
  if (agentType === "codex") {
10147
9672
  return parseCodexEvents(events);
10148
9673
  }
10149
- return parseClaudeEvents(events);
9674
+ return parseClaudeEvents(events);
9675
+ }
9676
+ function isCodexInitializationPrompt(message) {
9677
+ return message.type === "user" && removeReplicasInstructions(message.content).trim() === "Hello";
9678
+ }
9679
+ function createUserMessage(content) {
9680
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
9681
+ return {
9682
+ message: {
9683
+ id: `user-${Date.now()}`,
9684
+ type: "user",
9685
+ content,
9686
+ timestamp
9687
+ },
9688
+ timestamp
9689
+ };
9690
+ }
9691
+ function isAgentBackendEvent(value) {
9692
+ if (!value || typeof value !== "object") return false;
9693
+ const candidate = value;
9694
+ return typeof candidate.timestamp === "string" && typeof candidate.type === "string" && typeof candidate.payload === "object" && candidate.payload !== null;
9695
+ }
9696
+ function filterDisplayMessages(messages, provider) {
9697
+ let result = messages;
9698
+ if (provider === "codex") {
9699
+ const userMessages = result.map((msg, index) => ({ msg, index })).filter(({ msg }) => msg.type === "user");
9700
+ if (userMessages.length >= 2 && isCodexInitializationPrompt(userMessages[0].msg)) {
9701
+ result = result.slice(userMessages[1].index);
9702
+ }
9703
+ }
9704
+ result = result.map((msg) => {
9705
+ if (msg.type !== "user") return msg;
9706
+ const cleaned = removeReplicasInstructions(msg.content).trim();
9707
+ if (INTERRUPTED_MESSAGE_REGEX.test(cleaned)) {
9708
+ return { ...msg, content: "Request interrupted" };
9709
+ }
9710
+ return cleaned !== msg.content ? { ...msg, content: cleaned } : msg;
9711
+ });
9712
+ return result;
9713
+ }
9714
+
9715
+ // ../shared/src/user-message-parser/plan-quote.ts
9716
+ var MARKER_PREFIX = "> _Quoting from plan_";
9717
+ var MARKER_LINE_REGEX = /^> _Quoting from plan_ `([^`\n]+)`\s*$/;
9718
+ function hasPlanQuoteMarker(content) {
9719
+ return content.includes(MARKER_PREFIX);
9720
+ }
9721
+ function parsePlanQuote(content) {
9722
+ if (!hasPlanQuoteMarker(content)) return null;
9723
+ const lines = content.split("\n");
9724
+ const blocks = [];
9725
+ const leadingBuffer = [];
9726
+ let currentFilename = null;
9727
+ let quoteBuffer = [];
9728
+ let replyBuffer = [];
9729
+ let inQuoteBody = false;
9730
+ const flushCurrentBlock = () => {
9731
+ if (currentFilename !== null) {
9732
+ blocks.push({
9733
+ planFilename: currentFilename,
9734
+ quotedText: trimBlankEdges(quoteBuffer).join("\n"),
9735
+ replyText: trimBlankEdges(replyBuffer).join("\n")
9736
+ });
9737
+ }
9738
+ currentFilename = null;
9739
+ quoteBuffer = [];
9740
+ replyBuffer = [];
9741
+ inQuoteBody = false;
9742
+ };
9743
+ for (const line of lines) {
9744
+ const markerMatch = line.match(MARKER_LINE_REGEX);
9745
+ if (markerMatch) {
9746
+ flushCurrentBlock();
9747
+ currentFilename = markerMatch[1].trim();
9748
+ inQuoteBody = true;
9749
+ continue;
9750
+ }
9751
+ if (currentFilename !== null) {
9752
+ if (inQuoteBody && isBlockquoteLine(line)) {
9753
+ quoteBuffer.push(stripBlockquotePrefix(line));
9754
+ continue;
9755
+ }
9756
+ inQuoteBody = false;
9757
+ replyBuffer.push(line);
9758
+ continue;
9759
+ }
9760
+ leadingBuffer.push(line);
9761
+ }
9762
+ flushCurrentBlock();
9763
+ if (blocks.length === 0) return null;
9764
+ return {
9765
+ source: "plan_quote",
9766
+ blocks,
9767
+ leadingText: trimBlankEdges(leadingBuffer).join("\n")
9768
+ };
9769
+ }
9770
+ function isBlockquoteLine(line) {
9771
+ return line.startsWith(">");
9772
+ }
9773
+ function stripBlockquotePrefix(line) {
9774
+ return line.replace(/^>\s?/, "");
9775
+ }
9776
+ function trimBlankEdges(lines) {
9777
+ let start = 0;
9778
+ let end = lines.length;
9779
+ while (start < end && lines[start].trim() === "") start++;
9780
+ while (end > start && lines[end - 1].trim() === "") end--;
9781
+ return lines.slice(start, end);
9782
+ }
9783
+
9784
+ // ../shared/src/user-message-parser/inline-diff-comments.ts
9785
+ var INLINE_DIFF_COMMENTS_HEADER = "Inline diff comments for the current changes:";
9786
+ var INLINE_DIFF_COMMENT_BLOCK_HEADER_REGEX = /^Inline diff comment \d+\s*$/;
9787
+ var INLINE_DIFF_COMMENT_BLOCK_START_REGEX = /(?:^|\n\n)(Inline diff comment \d+\s*\n[\s\S]*?)(?=\n\nInline diff comment \d+\s*\n|$)/g;
9788
+ function parseInlineDiffComments(content) {
9789
+ const headerIndex = content.indexOf(INLINE_DIFF_COMMENTS_HEADER);
9790
+ if (headerIndex === -1) return null;
9791
+ const leadingText = content.slice(0, headerIndex).trim();
9792
+ const body = content.slice(headerIndex + INLINE_DIFF_COMMENTS_HEADER.length).trim();
9793
+ const comments = [];
9794
+ for (const match of body.matchAll(INLINE_DIFF_COMMENT_BLOCK_START_REGEX)) {
9795
+ const comment = parseInlineDiffCommentBlock(match[1].trim());
9796
+ if (comment) comments.push(comment);
9797
+ }
9798
+ if (comments.length === 0) return null;
9799
+ return {
9800
+ source: "inline_diff_comments",
9801
+ comments,
9802
+ leadingText,
9803
+ leadingPlanQuote: parsePlanQuote(leadingText) ?? void 0
9804
+ };
9805
+ }
9806
+ function parseInlineDiffCommentBlock(block) {
9807
+ const lines = block.split("\n");
9808
+ if (!INLINE_DIFF_COMMENT_BLOCK_HEADER_REGEX.test(lines[0] ?? "")) return null;
9809
+ const location = readInlineDiffCommentField(block, "File");
9810
+ const side = readInlineDiffCommentField(block, "Side");
9811
+ const commentMarker = "\nComment:\n";
9812
+ const commentIndex = block.indexOf(commentMarker);
9813
+ if (!location || !side || commentIndex === -1) return null;
9814
+ const parsedLocation = parseInlineDiffCommentLocation(location);
9815
+ const codeMatch = block.match(/\nCode on that line:\n```(?:[^\n]*)?\n([\s\S]*?)\n```/);
9816
+ return {
9817
+ path: parsedLocation.path,
9818
+ lineNumber: parsedLocation.lineNumber,
9819
+ location,
9820
+ side,
9821
+ lineText: codeMatch?.[1]?.trimEnd() ?? "",
9822
+ body: block.slice(commentIndex + commentMarker.length).trim()
9823
+ };
9824
+ }
9825
+ function readInlineDiffCommentField(block, label) {
9826
+ const match = block.match(new RegExp(`^${label}:\\s*(.+)$`, "m"));
9827
+ return match?.[1]?.trim() ?? "";
9828
+ }
9829
+ function parseInlineDiffCommentLocation(location) {
9830
+ const match = location.match(/^(.*):(\d+)$/);
9831
+ if (!match) return { path: location, lineNumber: 0 };
9832
+ return {
9833
+ path: match[1],
9834
+ lineNumber: parseInt(match[2], 10)
9835
+ };
9836
+ }
9837
+
9838
+ // ../shared/src/user-message-parser/parsers.ts
9839
+ function parseCIFailure(content) {
9840
+ if (!content.startsWith("# CI/CD Workflow Failed")) return null;
9841
+ const prMatch = content.match(/Pull Request #(\d+)/);
9842
+ const workflowNameMatch = content.match(/\*\*Workflow Name:\*\*\s*(.+)/);
9843
+ const workflowFileMatch = content.match(/\*\*Workflow File:\*\*\s*(.+)/);
9844
+ const conclusionMatch = content.match(/\*\*Conclusion:\*\*\s*(.+)/);
9845
+ const runUrlMatch = content.match(/\*\*Run URL:\*\*\s*(.+)/);
9846
+ const conclusionTextMatch = content.match(
9847
+ /has (failed|timed out|requires action|was cancelled|failed to start)/
9848
+ );
9849
+ const prUrlMatch = content.match(/\*\*PR URL:\*\*\s*(.+)/);
9850
+ const fileUrlMatch = content.match(/\*\*File URL:\*\*\s*(.+)/);
9851
+ if (!prMatch) return null;
9852
+ return {
9853
+ source: "ci_failure",
9854
+ prNumber: parseInt(prMatch[1], 10),
9855
+ workflowName: workflowNameMatch?.[1]?.trim() ?? "Unknown",
9856
+ workflowFile: workflowFileMatch?.[1]?.trim() ?? "",
9857
+ conclusion: conclusionMatch?.[1]?.trim() ?? "failure",
9858
+ runUrl: runUrlMatch?.[1]?.trim() ?? "",
9859
+ conclusionText: conclusionTextMatch?.[1] ?? "failed",
9860
+ prUrl: prUrlMatch?.[1]?.trim(),
9861
+ fileUrl: fileUrlMatch?.[1]?.trim()
9862
+ };
9863
+ }
9864
+ function parseGitHubIssueNew(content) {
9865
+ const headerMatch = content.match(/^# Task: GitHub Issue #(\d+) - (.+)$/m);
9866
+ if (!headerMatch) return null;
9867
+ const issueNumber = parseInt(headerMatch[1], 10);
9868
+ const issueTitle = headerMatch[2].trim();
9869
+ const descMatch = content.match(/## Issue Description\n([\s\S]*?)(?=\n## |$)/);
9870
+ const triggerMatch = content.match(
9871
+ /## Triggering Comment from @(\S+)\n([\s\S]*?)(?=\n\*\*(?:PR|Issue|User|File) URL:\*\*|\n## |$)/
9872
+ );
9873
+ const issueUrlMatch = content.match(/\*\*Issue URL:\*\*\s*(.+)/);
9874
+ const userUrlMatch = content.match(/\*\*User URL:\*\*\s*(.+)/);
9875
+ return {
9876
+ source: "github_issue_new",
9877
+ issueNumber,
9878
+ issueTitle,
9879
+ issueDescription: descMatch?.[1]?.trim() ?? "",
9880
+ triggeringUser: triggerMatch?.[1] ?? "",
9881
+ triggeringComment: triggerMatch?.[2]?.trim() ?? "",
9882
+ issueUrl: issueUrlMatch?.[1]?.trim(),
9883
+ userUrl: userUrlMatch?.[1]?.trim()
9884
+ };
9885
+ }
9886
+ function parseGitHubIssueExisting(content) {
9887
+ const headerMatch = content.match(
9888
+ /^You received a new comment on GitHub Issue #(\d+)\./
9889
+ );
9890
+ if (!headerMatch) return null;
9891
+ const issueNumber = parseInt(headerMatch[1], 10);
9892
+ const commentMatch = content.match(
9893
+ /Comment from @(\S+):\n([\s\S]*?)(?=\n\*\*(?:PR|Issue|User|File) URL:\*\*|\nIMPORTANT INSTRUCTIONS:)/
9894
+ );
9895
+ const issueUrlMatch = content.match(/\*\*Issue URL:\*\*\s*(.+)/);
9896
+ const userUrlMatch = content.match(/\*\*User URL:\*\*\s*(.+)/);
9897
+ return {
9898
+ source: "github_issue_existing",
9899
+ issueNumber,
9900
+ commentUser: commentMatch?.[1] ?? "",
9901
+ commentBody: commentMatch?.[2]?.trim() ?? "",
9902
+ issueUrl: issueUrlMatch?.[1]?.trim(),
9903
+ userUrl: userUrlMatch?.[1]?.trim()
9904
+ };
9905
+ }
9906
+ function parseGitHubPRNew(content) {
9907
+ const headerMatch = content.match(
9908
+ /^# Task: Review and work on Pull Request #(\d+)/m
9909
+ );
9910
+ if (!headerMatch) return null;
9911
+ const prNumber = parseInt(headerMatch[1], 10);
9912
+ const contextMatch = content.match(
9913
+ /@(\S+) mentioned @(?:replicas-connector|tryreplicas|replicas).*?\n\n>([\s\S]*?)(?=\n\*\*(?:PR|Issue|User|File) URL:\*\*|\n## Your Assignment)/
9914
+ );
9915
+ let contextMessage = "";
9916
+ if (contextMatch) {
9917
+ contextMessage = contextMatch[2].split("\n").map((line) => line.replace(/^>\s?/, "")).join("\n").trim();
9918
+ }
9919
+ const prUrlMatch = content.match(/\*\*PR URL:\*\*\s*(.+)/);
9920
+ const userUrlMatch = content.match(/\*\*User URL:\*\*\s*(.+)/);
9921
+ return {
9922
+ source: "github_pr_new",
9923
+ prNumber,
9924
+ mentioningUser: contextMatch?.[1] ?? "",
9925
+ contextMessage,
9926
+ prUrl: prUrlMatch?.[1]?.trim(),
9927
+ userUrl: userUrlMatch?.[1]?.trim()
9928
+ };
9929
+ }
9930
+ function parseGitHubPRExistingPRReview(content) {
9931
+ const headerMatch = content.match(
9932
+ /^You received a pull request review on PR #(\d+)\./m
9933
+ );
9934
+ if (!headerMatch) return null;
9935
+ const prNumber = parseInt(headerMatch[1], 10);
9936
+ const actionMatch = content.match(
9937
+ /@(\S+)\s+(APPROVED the pull request|REQUESTED CHANGES on the pull request|left a review comment on the pull request|had their review dismissed)/
9938
+ );
9939
+ const commentMatch = content.match(
9940
+ /Comment from @\S+:\n([\s\S]*?)(?=\n\*\*(?:PR|Issue|User|File) URL:\*\*|\nIMPORTANT INSTRUCTIONS:)/
9941
+ );
9942
+ let commentBody = commentMatch?.[1]?.trim() ?? "";
9943
+ if (!commentBody) {
9944
+ const bodyMatch = content.match(
9945
+ /reviewed the pull request\.\n([\s\S]*?)(?=\n\*\*(?:PR|Issue|User|File) URL:\*\*|\nIMPORTANT INSTRUCTIONS:)/
9946
+ );
9947
+ commentBody = bodyMatch?.[1]?.trim() ?? "";
9948
+ }
9949
+ const prUrlMatch = content.match(/\*\*PR URL:\*\*\s*(.+)/);
9950
+ const userUrlMatch = content.match(/\*\*User URL:\*\*\s*(.+)/);
9951
+ return {
9952
+ source: "github_pr_existing_pr_review",
9953
+ prNumber,
9954
+ reviewUser: actionMatch?.[1] ?? "",
9955
+ reviewAction: actionMatch?.[2] ?? "reviewed the pull request",
9956
+ commentBody,
9957
+ prUrl: prUrlMatch?.[1]?.trim(),
9958
+ userUrl: userUrlMatch?.[1]?.trim()
9959
+ };
10150
9960
  }
10151
- function isCodexInitializationPrompt(message) {
10152
- return message.type === "user" && removeReplicasInstructions(message.content).trim() === "Hello";
9961
+ function parseGitHubPRExistingReview(content) {
9962
+ const headerMatch = content.match(
9963
+ /^You received a comment on Pull Request #(\d+)\./m
9964
+ );
9965
+ if (!headerMatch) return null;
9966
+ const fileMatch = content.match(/\nFile: (.+)/);
9967
+ const diffMatch = content.match(/\nDiff context:\n```diff\n([\s\S]*?)```/);
9968
+ if (!fileMatch || !diffMatch) return null;
9969
+ const prNumber = parseInt(headerMatch[1], 10);
9970
+ const commentMatch = content.match(
9971
+ /Comment from @(\S+):\n([\s\S]*?)(?=\n\*\*(?:PR|Issue|User|File) URL:\*\*|\nIMPORTANT INSTRUCTIONS:)/
9972
+ );
9973
+ const prUrlMatch = content.match(/\*\*PR URL:\*\*\s*(.+)/);
9974
+ const userUrlMatch = content.match(/\*\*User URL:\*\*\s*(.+)/);
9975
+ const fileUrlMatch = content.match(/\*\*File URL:\*\*\s*(.+)/);
9976
+ return {
9977
+ source: "github_pr_existing_review",
9978
+ prNumber,
9979
+ commentUser: commentMatch?.[1] ?? "",
9980
+ commentBody: commentMatch?.[2]?.trim() ?? "",
9981
+ filePath: fileMatch[1].trim(),
9982
+ diffHunk: diffMatch[1].trim(),
9983
+ prUrl: prUrlMatch?.[1]?.trim(),
9984
+ userUrl: userUrlMatch?.[1]?.trim(),
9985
+ fileUrl: fileUrlMatch?.[1]?.trim()
9986
+ };
10153
9987
  }
10154
- function createUserMessage(content) {
10155
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
9988
+ function parseGitHubPRExistingGeneral(content) {
9989
+ const headerMatch = content.match(
9990
+ /^You received a comment on Pull Request #(\d+)\./m
9991
+ );
9992
+ if (!headerMatch) return null;
9993
+ const prNumber = parseInt(headerMatch[1], 10);
9994
+ const commentMatch = content.match(
9995
+ /Comment from @(\S+):\n([\s\S]*?)(?=\n\*\*(?:PR|Issue|User|File) URL:\*\*|\nIMPORTANT INSTRUCTIONS:)/
9996
+ );
9997
+ const prUrlMatch = content.match(/\*\*PR URL:\*\*\s*(.+)/);
9998
+ const userUrlMatch = content.match(/\*\*User URL:\*\*\s*(.+)/);
10156
9999
  return {
10157
- message: {
10158
- id: `user-${Date.now()}`,
10159
- type: "user",
10160
- content,
10161
- timestamp
10162
- },
10163
- timestamp
10000
+ source: "github_pr_existing_general",
10001
+ prNumber,
10002
+ commentUser: commentMatch?.[1] ?? "",
10003
+ commentBody: commentMatch?.[2]?.trim() ?? "",
10004
+ prUrl: prUrlMatch?.[1]?.trim(),
10005
+ userUrl: userUrlMatch?.[1]?.trim()
10164
10006
  };
10165
10007
  }
10166
- function isAgentBackendEvent(value) {
10167
- if (!value || typeof value !== "object") return false;
10168
- const candidate = value;
10169
- return typeof candidate.timestamp === "string" && typeof candidate.type === "string" && typeof candidate.payload === "object" && candidate.payload !== null;
10008
+ function parseSlackTask(content) {
10009
+ if (!content.startsWith("# Task from Slack")) return null;
10010
+ const targetMatch = content.match(/## Workspace Target\nWorking in: (.+?)(?=\n\*\*(?:Thread|Target) URL:\*\*|\n\n)/);
10011
+ const threadMatch = content.match(
10012
+ /## (?:Thread Context|New Messages in Thread)\n([\s\S]*?)(?=\n## Latest Message)/
10013
+ );
10014
+ const latestMatch = content.match(
10015
+ /## Latest Message \(mentions you\)\n@(.+?): ([\s\S]*?)(?=\n## Response Instructions)/
10016
+ );
10017
+ const threadUrlMatch = content.match(/\*\*Thread URL:\*\*\s*(.+)/);
10018
+ const targetUrlMatch = content.match(/\*\*Target URL:\*\*\s*(.+)/);
10019
+ return {
10020
+ source: "slack_task",
10021
+ workspaceTarget: targetMatch?.[1]?.trim() ?? "",
10022
+ threadContext: threadMatch?.[1]?.trim() ?? "",
10023
+ latestMessageUser: latestMatch?.[1]?.trim() ?? "",
10024
+ latestMessageText: latestMatch?.[2]?.trim() ?? "",
10025
+ threadUrl: threadUrlMatch?.[1]?.trim(),
10026
+ targetUrl: targetUrlMatch?.[1]?.trim()
10027
+ };
10170
10028
  }
10171
- function filterDisplayMessages(messages, provider) {
10172
- let result = messages;
10173
- if (provider === "codex") {
10174
- const userMessages = result.map((msg, index) => ({ msg, index })).filter(({ msg }) => msg.type === "user");
10175
- if (userMessages.length >= 2 && isCodexInitializationPrompt(userMessages[0].msg)) {
10176
- result = result.slice(userMessages[1].index);
10177
- }
10029
+ function parseLinearIssue(content) {
10030
+ const headerMatch = content.match(/^# Task: ([A-Z]+-\d+) - (.+)$/m);
10031
+ if (!headerMatch) return null;
10032
+ if (content.includes("GitHub Issue #")) return null;
10033
+ if (content.includes("Pull Request #")) return null;
10034
+ const identifier = headerMatch[1];
10035
+ const title = headerMatch[2].trim();
10036
+ let parentIssue;
10037
+ const parentMatch = content.match(
10038
+ /## Parent Issue\n\*\*([A-Z]+-\d+)\*\*: (.+)\n\n([\s\S]*?)(?=\n## |$)/
10039
+ );
10040
+ if (parentMatch) {
10041
+ parentIssue = {
10042
+ identifier: parentMatch[1],
10043
+ title: parentMatch[2].trim(),
10044
+ description: parentMatch[3].trim()
10045
+ };
10178
10046
  }
10179
- result = result.map((msg) => {
10180
- if (msg.type !== "user") return msg;
10181
- const cleaned = removeReplicasInstructions(msg.content).trim();
10182
- if (INTERRUPTED_MESSAGE_REGEX.test(cleaned)) {
10183
- return { ...msg, content: "Request interrupted" };
10184
- }
10185
- return cleaned !== msg.content ? { ...msg, content: cleaned } : msg;
10186
- });
10187
- return result;
10047
+ const descMatch = content.match(/## Description\n([\s\S]*?)(?=\n## |$)/);
10048
+ const description = descMatch?.[1]?.trim() ?? "";
10049
+ const contextMatch = content.match(
10050
+ /## Additional Context\n([\s\S]*?)(?=\n## |$)/
10051
+ );
10052
+ const additionalContext = contextMatch?.[1]?.trim() ?? "";
10053
+ const issueUrlMatch = content.match(/\*\*Issue URL:\*\*\s*(.+)/);
10054
+ return {
10055
+ source: "linear_issue",
10056
+ identifier,
10057
+ title,
10058
+ parentIssue,
10059
+ description,
10060
+ additionalContext,
10061
+ issueUrl: issueUrlMatch?.[1]?.trim()
10062
+ };
10188
10063
  }
10189
-
10190
- // ../shared/src/user-message-parser/plan-quote.ts
10191
- var MARKER_PREFIX = "> _Quoting from plan_";
10192
- var MARKER_LINE_REGEX = /^> _Quoting from plan_ `([^`\n]+)`\s*$/;
10193
- function hasPlanQuoteMarker(content) {
10194
- return content.includes(MARKER_PREFIX);
10064
+ function safeHttpsUrl(raw) {
10065
+ if (!raw) return void 0;
10066
+ const trimmed = raw.trim();
10067
+ return trimmed.startsWith("https://") ? trimmed : void 0;
10195
10068
  }
10196
- function parsePlanQuote(content) {
10197
- if (!hasPlanQuoteMarker(content)) return null;
10198
- const lines = content.split("\n");
10199
- const blocks = [];
10200
- const leadingBuffer = [];
10201
- let currentFilename = null;
10202
- let quoteBuffer = [];
10203
- let replyBuffer = [];
10204
- let inQuoteBody = false;
10205
- const flushCurrentBlock = () => {
10206
- if (currentFilename !== null) {
10207
- blocks.push({
10208
- planFilename: currentFilename,
10209
- quotedText: trimBlankEdges(quoteBuffer).join("\n"),
10210
- replyText: trimBlankEdges(replyBuffer).join("\n")
10069
+ function parseAutomationTriggered(content) {
10070
+ if (!content.startsWith("# Automation Triggered")) return null;
10071
+ const nameMatch = content.match(/\*\*Automation Name:\*\*\s*(.+)/);
10072
+ if (!nameMatch) return null;
10073
+ const triggerTypeMatch = content.match(/\*\*Trigger Type:\*\*\s*(.+)/);
10074
+ const triggeredAtMatch = content.match(/\*\*Triggered At:\*\*\s*(.+)/);
10075
+ const eventMatch = content.match(/\*\*Event:\*\*\s*(.+)/);
10076
+ const repoMatch = content.match(/\*\*Repository:\*\*\s*(.+)/);
10077
+ const repoUrlMatch = content.match(/\*\*Repository URL:\*\*\s*(.+)/);
10078
+ const prNumberMatch = content.match(/\*\*PR Number:\*\*\s*(.+)/);
10079
+ const prUrlMatch = content.match(/\*\*PR URL:\*\*\s*(.+)/);
10080
+ const triggeredByMatch = content.match(/\*\*Triggered By:\*\*\s*@?(.+)/);
10081
+ const triggeredByUrlMatch = content.match(/\*\*Triggered By URL:\*\*\s*(.+)/);
10082
+ const promptMatch = content.match(/## Automation Prompt\n([\s\S]*)$/);
10083
+ const parsedPrNumber = prNumberMatch ? parseInt(prNumberMatch[1].trim(), 10) : NaN;
10084
+ return {
10085
+ source: "automation_triggered",
10086
+ automationName: nameMatch[1].trim(),
10087
+ triggerType: triggerTypeMatch?.[1]?.trim() ?? "unknown",
10088
+ triggeredAt: triggeredAtMatch?.[1]?.trim() ?? "",
10089
+ event: eventMatch?.[1]?.trim(),
10090
+ repositoryName: repoMatch?.[1]?.trim(),
10091
+ repositoryUrl: safeHttpsUrl(repoUrlMatch?.[1]),
10092
+ prNumber: isNaN(parsedPrNumber) ? void 0 : parsedPrNumber,
10093
+ prUrl: safeHttpsUrl(prUrlMatch?.[1]),
10094
+ triggeredBy: triggeredByMatch?.[1]?.trim(),
10095
+ triggeredByUrl: safeHttpsUrl(triggeredByUrlMatch?.[1]),
10096
+ userPrompt: promptMatch?.[1]?.trim() ?? ""
10097
+ };
10098
+ }
10099
+ function parseSingleMessage(content) {
10100
+ return parseCIFailure(content) ?? parseGitHubIssueNew(content) ?? parseGitHubIssueExisting(content) ?? parseGitHubPRNew(content) ?? parseGitHubPRExistingPRReview(content) ?? parseGitHubPRExistingReview(content) ?? parseGitHubPRExistingGeneral(content) ?? parseSlackTask(content) ?? parseLinearIssue(content) ?? parseAutomationTriggered(content) ?? parseInlineDiffComments(content) ?? parsePlanQuote(content) ?? { source: "raw", content };
10101
+ }
10102
+ function parseMerged(content) {
10103
+ if (!content.includes(MERGED_MESSAGE_SEPARATOR)) return null;
10104
+ const chunks = content.split(MERGED_MESSAGE_SEPARATOR).map((chunk) => chunk.trim()).filter((chunk) => chunk.length > 0);
10105
+ if (chunks.length < 2) return null;
10106
+ return {
10107
+ source: "merged",
10108
+ messages: chunks.map((chunk) => parseSingleMessage(chunk))
10109
+ };
10110
+ }
10111
+ function parseUserMessage(rawContent) {
10112
+ const content = removeReplicasInstructions(rawContent).trim();
10113
+ return parseMerged(content) ?? parseSingleMessage(content);
10114
+ }
10115
+
10116
+ // ../shared/src/user-message-parser/source-config.ts
10117
+ var SOURCE_CONFIG = {
10118
+ ci_failure: { label: "CI Failure", color: "#ff4444" },
10119
+ linear_issue: { label: "Linear", color: "#5E6AD2" },
10120
+ github_issue_new: { label: "GitHub Issue", color: "#8b949e" },
10121
+ github_issue_existing: { label: "GitHub Issue", color: "#8b949e" },
10122
+ github_pr_new: { label: "GitHub PR", color: "#8b949e" },
10123
+ github_pr_existing_review: { label: "Code Review", color: "#8b949e" },
10124
+ github_pr_existing_pr_review: { label: "PR Review", color: "#8b949e" },
10125
+ github_pr_existing_general: { label: "GitHub PR", color: "#8b949e" },
10126
+ slack_task: { label: "Slack", color: "#BF6CC2" },
10127
+ automation_triggered: { label: "Automation", color: "#f59e0b" },
10128
+ plan_quote: { label: "Plan", color: "#66bb6a" },
10129
+ inline_diff_comments: { label: "Diff Comments", color: "#66bb6a" },
10130
+ merged: { label: "Merged", color: "#a78bfa" }
10131
+ };
10132
+
10133
+ // ../shared/src/workspace-groups.ts
10134
+ function buildGroups(workspaces, _workspacesData, environments, _repositorySets) {
10135
+ const groupMap = /* @__PURE__ */ new Map();
10136
+ for (const env of environments) {
10137
+ groupMap.set(env.id, {
10138
+ type: "env",
10139
+ id: env.id,
10140
+ name: env.name,
10141
+ environmentId: env.id,
10142
+ isGlobal: env.is_global,
10143
+ repoIdForCreate: env.repository_id,
10144
+ repoSetIdForCreate: env.repository_set_id,
10145
+ workspaces: []
10146
+ });
10147
+ }
10148
+ const placeUngrouped = (workspace) => {
10149
+ const key = "__ungrouped__";
10150
+ if (!groupMap.has(key)) {
10151
+ groupMap.set(key, {
10152
+ type: "env",
10153
+ id: key,
10154
+ name: "Other",
10155
+ environmentId: key,
10156
+ isGlobal: false,
10157
+ repoIdForCreate: null,
10158
+ repoSetIdForCreate: null,
10159
+ workspaces: []
10211
10160
  });
10212
10161
  }
10213
- currentFilename = null;
10214
- quoteBuffer = [];
10215
- replyBuffer = [];
10216
- inQuoteBody = false;
10162
+ groupMap.get(key).workspaces.push(workspace);
10217
10163
  };
10218
- for (const line of lines) {
10219
- const markerMatch = line.match(MARKER_LINE_REGEX);
10220
- if (markerMatch) {
10221
- flushCurrentBlock();
10222
- currentFilename = markerMatch[1].trim();
10223
- inQuoteBody = true;
10224
- continue;
10225
- }
10226
- if (currentFilename !== null) {
10227
- if (inQuoteBody && isBlockquoteLine(line)) {
10228
- quoteBuffer.push(stripBlockquotePrefix(line));
10229
- continue;
10230
- }
10231
- inQuoteBody = false;
10232
- replyBuffer.push(line);
10164
+ for (const workspace of workspaces) {
10165
+ if (workspace.environment_id && groupMap.has(workspace.environment_id)) {
10166
+ groupMap.get(workspace.environment_id).workspaces.push(workspace);
10233
10167
  continue;
10234
10168
  }
10235
- leadingBuffer.push(line);
10169
+ placeUngrouped(workspace);
10236
10170
  }
10237
- flushCurrentBlock();
10238
- if (blocks.length === 0) return null;
10239
- return {
10240
- source: "plan_quote",
10241
- blocks,
10242
- leadingText: trimBlankEdges(leadingBuffer).join("\n")
10243
- };
10244
- }
10245
- function isBlockquoteLine(line) {
10246
- return line.startsWith(">");
10247
- }
10248
- function stripBlockquotePrefix(line) {
10249
- return line.replace(/^>\s?/, "");
10250
- }
10251
- function trimBlankEdges(lines) {
10252
- let start = 0;
10253
- let end = lines.length;
10254
- while (start < end && lines[start].trim() === "") start++;
10255
- while (end > start && lines[end - 1].trim() === "") end--;
10256
- return lines.slice(start, end);
10171
+ return Array.from(groupMap.values()).sort((a, b) => a.name.localeCompare(b.name));
10257
10172
  }
10258
10173
 
10259
- // ../shared/src/user-message-parser/inline-diff-comments.ts
10260
- var INLINE_DIFF_COMMENTS_HEADER = "Inline diff comments for the current changes:";
10261
- var INLINE_DIFF_COMMENT_BLOCK_HEADER_REGEX = /^Inline diff comment \d+\s*$/;
10262
- var INLINE_DIFF_COMMENT_BLOCK_START_REGEX = /(?:^|\n\n)(Inline diff comment \d+\s*\n[\s\S]*?)(?=\n\nInline diff comment \d+\s*\n|$)/g;
10263
- function parseInlineDiffComments(content) {
10264
- const headerIndex = content.indexOf(INLINE_DIFF_COMMENTS_HEADER);
10265
- if (headerIndex === -1) return null;
10266
- const leadingText = content.slice(0, headerIndex).trim();
10267
- const body = content.slice(headerIndex + INLINE_DIFF_COMMENTS_HEADER.length).trim();
10268
- const comments = [];
10269
- for (const match of body.matchAll(INLINE_DIFF_COMMENT_BLOCK_START_REGEX)) {
10270
- const comment = parseInlineDiffCommentBlock(match[1].trim());
10271
- if (comment) comments.push(comment);
10174
+ // ../shared/src/object-store/types.ts
10175
+ var MEDIA_KIND = {
10176
+ IMAGE: "image",
10177
+ VIDEO: "video",
10178
+ AUDIO: "audio"
10179
+ };
10180
+ var MEDIA_KINDS = [MEDIA_KIND.IMAGE, MEDIA_KIND.VIDEO, MEDIA_KIND.AUDIO];
10181
+
10182
+ // ../shared/src/sse.ts
10183
+ function parseSseChunk(chunk) {
10184
+ let data = "";
10185
+ for (const line of chunk.split("\n")) {
10186
+ if (line.startsWith("data: ")) {
10187
+ data += line.slice(6);
10188
+ }
10189
+ }
10190
+ if (!data) return null;
10191
+ try {
10192
+ return JSON.parse(data);
10193
+ } catch {
10194
+ return null;
10272
10195
  }
10273
- if (comments.length === 0) return null;
10274
- return {
10275
- source: "inline_diff_comments",
10276
- comments,
10277
- leadingText,
10278
- leadingPlanQuote: parsePlanQuote(leadingText) ?? void 0
10279
- };
10280
- }
10281
- function parseInlineDiffCommentBlock(block) {
10282
- const lines = block.split("\n");
10283
- if (!INLINE_DIFF_COMMENT_BLOCK_HEADER_REGEX.test(lines[0] ?? "")) return null;
10284
- const location = readInlineDiffCommentField(block, "File");
10285
- const side = readInlineDiffCommentField(block, "Side");
10286
- const commentMarker = "\nComment:\n";
10287
- const commentIndex = block.indexOf(commentMarker);
10288
- if (!location || !side || commentIndex === -1) return null;
10289
- const parsedLocation = parseInlineDiffCommentLocation(location);
10290
- const codeMatch = block.match(/\nCode on that line:\n```(?:[^\n]*)?\n([\s\S]*?)\n```/);
10291
- return {
10292
- path: parsedLocation.path,
10293
- lineNumber: parsedLocation.lineNumber,
10294
- location,
10295
- side,
10296
- lineText: codeMatch?.[1]?.trimEnd() ?? "",
10297
- body: block.slice(commentIndex + commentMarker.length).trim()
10298
- };
10299
- }
10300
- function readInlineDiffCommentField(block, label) {
10301
- const match = block.match(new RegExp(`^${label}:\\s*(.+)$`, "m"));
10302
- return match?.[1]?.trim() ?? "";
10303
10196
  }
10304
- function parseInlineDiffCommentLocation(location) {
10305
- const match = location.match(/^(.*):(\d+)$/);
10306
- if (!match) return { path: location, lineNumber: 0 };
10307
- return {
10308
- path: match[1],
10309
- lineNumber: parseInt(match[2], 10)
10310
- };
10197
+ async function readSseStream(body, onEvent, shouldContinue = () => true) {
10198
+ const reader = body.getReader();
10199
+ const decoder = new TextDecoder();
10200
+ let buffer = "";
10201
+ while (shouldContinue()) {
10202
+ const { done, value } = await reader.read();
10203
+ if (done) break;
10204
+ buffer += decoder.decode(value, { stream: true });
10205
+ const chunks = buffer.split("\n\n");
10206
+ buffer = chunks.pop() ?? "";
10207
+ for (const chunk of chunks) {
10208
+ if (!shouldContinue()) break;
10209
+ const event = parseSseChunk(chunk);
10210
+ if (!event) continue;
10211
+ await onEvent(event);
10212
+ }
10213
+ }
10311
10214
  }
10312
10215
 
10313
- // ../shared/src/user-message-parser/parsers.ts
10314
- function parseCIFailure(content) {
10315
- if (!content.startsWith("# CI/CD Workflow Failed")) return null;
10316
- const prMatch = content.match(/Pull Request #(\d+)/);
10317
- const workflowNameMatch = content.match(/\*\*Workflow Name:\*\*\s*(.+)/);
10318
- const workflowFileMatch = content.match(/\*\*Workflow File:\*\*\s*(.+)/);
10319
- const conclusionMatch = content.match(/\*\*Conclusion:\*\*\s*(.+)/);
10320
- const runUrlMatch = content.match(/\*\*Run URL:\*\*\s*(.+)/);
10321
- const conclusionTextMatch = content.match(
10322
- /has (failed|timed out|requires action|was cancelled|failed to start)/
10323
- );
10324
- const prUrlMatch = content.match(/\*\*PR URL:\*\*\s*(.+)/);
10325
- const fileUrlMatch = content.match(/\*\*File URL:\*\*\s*(.+)/);
10326
- if (!prMatch) return null;
10327
- return {
10328
- source: "ci_failure",
10329
- prNumber: parseInt(prMatch[1], 10),
10330
- workflowName: workflowNameMatch?.[1]?.trim() ?? "Unknown",
10331
- workflowFile: workflowFileMatch?.[1]?.trim() ?? "",
10332
- conclusion: conclusionMatch?.[1]?.trim() ?? "failure",
10333
- runUrl: runUrlMatch?.[1]?.trim() ?? "",
10334
- conclusionText: conclusionTextMatch?.[1] ?? "failed",
10335
- prUrl: prUrlMatch?.[1]?.trim(),
10336
- fileUrl: fileUrlMatch?.[1]?.trim()
10337
- };
10216
+ // src/lib/api.ts
10217
+ var MONOLITH_URL = process.env.REPLICAS_MONOLITH_URL || "https://api.tryreplicas.com";
10218
+ async function buildOrgAuthHeaders() {
10219
+ const extraHeaders = {};
10220
+ if (isAgentMode()) {
10221
+ const agent = readAgentConfig();
10222
+ if (!agent) {
10223
+ throw new Error("Agent mode config is incomplete");
10224
+ }
10225
+ extraHeaders["X-Workspace-Id"] = agent.workspace_id;
10226
+ return { bearer: agent.engine_secret, extraHeaders };
10227
+ }
10228
+ const bearer = await getValidToken();
10229
+ const organizationId = getOrganizationId();
10230
+ if (!organizationId) {
10231
+ throw new Error(
10232
+ 'No organization selected. Please run "replicas org switch" to select an organization.'
10233
+ );
10234
+ }
10235
+ extraHeaders["Replicas-Org-Id"] = organizationId;
10236
+ return { bearer, extraHeaders };
10338
10237
  }
10339
- function parseGitHubIssueNew(content) {
10340
- const headerMatch = content.match(/^# Task: GitHub Issue #(\d+) - (.+)$/m);
10341
- if (!headerMatch) return null;
10342
- const issueNumber = parseInt(headerMatch[1], 10);
10343
- const issueTitle = headerMatch[2].trim();
10344
- const descMatch = content.match(/## Issue Description\n([\s\S]*?)(?=\n## |$)/);
10345
- const triggerMatch = content.match(
10346
- /## Triggering Comment from @(\S+)\n([\s\S]*?)(?=\n\*\*(?:PR|Issue|User|File) URL:\*\*|\n## |$)/
10347
- );
10348
- const issueUrlMatch = content.match(/\*\*Issue URL:\*\*\s*(.+)/);
10349
- const userUrlMatch = content.match(/\*\*User URL:\*\*\s*(.+)/);
10350
- return {
10351
- source: "github_issue_new",
10352
- issueNumber,
10353
- issueTitle,
10354
- issueDescription: descMatch?.[1]?.trim() ?? "",
10355
- triggeringUser: triggerMatch?.[1] ?? "",
10356
- triggeringComment: triggerMatch?.[2]?.trim() ?? "",
10357
- issueUrl: issueUrlMatch?.[1]?.trim(),
10358
- userUrl: userUrlMatch?.[1]?.trim()
10359
- };
10238
+ async function authenticatedFetch(url, options) {
10239
+ const token = await getValidToken();
10240
+ if (!token) {
10241
+ throw new Error("No access token available");
10242
+ }
10243
+ return apiFetch(url, {
10244
+ "Authorization": `Bearer ${token}`,
10245
+ ...options?.headers || {}
10246
+ }, options);
10360
10247
  }
10361
- function parseGitHubIssueExisting(content) {
10362
- const headerMatch = content.match(
10363
- /^You received a new comment on GitHub Issue #(\d+)\./
10364
- );
10365
- if (!headerMatch) return null;
10366
- const issueNumber = parseInt(headerMatch[1], 10);
10367
- const commentMatch = content.match(
10368
- /Comment from @(\S+):\n([\s\S]*?)(?=\n\*\*(?:PR|Issue|User|File) URL:\*\*|\nIMPORTANT INSTRUCTIONS:)/
10369
- );
10370
- const issueUrlMatch = content.match(/\*\*Issue URL:\*\*\s*(.+)/);
10371
- const userUrlMatch = content.match(/\*\*User URL:\*\*\s*(.+)/);
10372
- return {
10373
- source: "github_issue_existing",
10374
- issueNumber,
10375
- commentUser: commentMatch?.[1] ?? "",
10376
- commentBody: commentMatch?.[2]?.trim() ?? "",
10377
- issueUrl: issueUrlMatch?.[1]?.trim(),
10378
- userUrl: userUrlMatch?.[1]?.trim()
10248
+ var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
10249
+ var MAX_REDIRECTS = 5;
10250
+ async function fetchWithRedirects(url, init, attempt = 0) {
10251
+ const response = await fetch(url, { ...init, redirect: "manual" });
10252
+ if (REDIRECT_STATUSES.has(response.status)) {
10253
+ if (attempt >= MAX_REDIRECTS) {
10254
+ throw new Error("Too many redirects while contacting the Replicas API");
10255
+ }
10256
+ const location = response.headers.get("location");
10257
+ if (!location) {
10258
+ throw new Error(`Redirect status ${response.status} missing Location header`);
10259
+ }
10260
+ const nextUrl = new URL(location, url).toString();
10261
+ const shouldForceGet = response.status === 303 && init.method !== void 0 && init.method.toUpperCase() !== "GET";
10262
+ const nextInit = {
10263
+ ...init,
10264
+ method: shouldForceGet ? "GET" : init.method,
10265
+ body: shouldForceGet ? void 0 : init.body
10266
+ };
10267
+ return fetchWithRedirects(nextUrl, nextInit, attempt + 1);
10268
+ }
10269
+ return response;
10270
+ }
10271
+ async function orgAuthenticatedFetch(url, options) {
10272
+ const { bearer, extraHeaders } = await buildOrgAuthHeaders();
10273
+ return apiFetch(url, {
10274
+ "Authorization": `Bearer ${bearer}`,
10275
+ ...extraHeaders,
10276
+ ...options?.headers || {}
10277
+ }, options);
10278
+ }
10279
+ async function throwResponseError(response) {
10280
+ const error = await response.json().catch(() => ({ error: "Request failed" }));
10281
+ throw new Error(error.error || `Request failed with status ${response.status}`);
10282
+ }
10283
+ async function apiFetch(url, headers, options) {
10284
+ const requestHeaders = {
10285
+ "Content-Type": "application/json",
10286
+ ...headers
10287
+ };
10288
+ const absoluteUrl = `${MONOLITH_URL}${url}`;
10289
+ const requestInit = {
10290
+ ...options,
10291
+ headers: requestHeaders,
10292
+ body: options?.body !== void 0 ? JSON.stringify(options.body) : void 0
10379
10293
  };
10294
+ const response = await fetchWithRedirects(absoluteUrl, requestInit);
10295
+ if (!response.ok) {
10296
+ await throwResponseError(response);
10297
+ }
10298
+ return response.json();
10380
10299
  }
10381
- function parseGitHubPRNew(content) {
10382
- const headerMatch = content.match(
10383
- /^# Task: Review and work on Pull Request #(\d+)/m
10384
- );
10385
- if (!headerMatch) return null;
10386
- const prNumber = parseInt(headerMatch[1], 10);
10387
- const contextMatch = content.match(
10388
- /@(\S+) mentioned @(?:replicas-connector|tryreplicas|replicas).*?\n\n>([\s\S]*?)(?=\n\*\*(?:PR|Issue|User|File) URL:\*\*|\n## Your Assignment)/
10389
- );
10390
- let contextMessage = "";
10391
- if (contextMatch) {
10392
- contextMessage = contextMatch[2].split("\n").map((line) => line.replace(/^>\s?/, "")).join("\n").trim();
10300
+ async function orgAuthenticatedSseStream(url, options) {
10301
+ const { bearer, extraHeaders } = await buildOrgAuthHeaders();
10302
+ const response = await fetch(`${MONOLITH_URL}${url}`, {
10303
+ method: "POST",
10304
+ headers: {
10305
+ "Authorization": `Bearer ${bearer}`,
10306
+ "Content-Type": "application/json",
10307
+ "Accept": "text/event-stream",
10308
+ ...extraHeaders
10309
+ },
10310
+ body: JSON.stringify(options.body)
10311
+ });
10312
+ if (!response.ok || !response.body) {
10313
+ await throwResponseError(response);
10314
+ return;
10393
10315
  }
10394
- const prUrlMatch = content.match(/\*\*PR URL:\*\*\s*(.+)/);
10395
- const userUrlMatch = content.match(/\*\*User URL:\*\*\s*(.+)/);
10396
- return {
10397
- source: "github_pr_new",
10398
- prNumber,
10399
- mentioningUser: contextMatch?.[1] ?? "",
10400
- contextMessage,
10401
- prUrl: prUrlMatch?.[1]?.trim(),
10402
- userUrl: userUrlMatch?.[1]?.trim()
10403
- };
10316
+ await readSseStream(response.body, options.onEvent);
10404
10317
  }
10405
- function parseGitHubPRExistingPRReview(content) {
10406
- const headerMatch = content.match(
10407
- /^You received a pull request review on PR #(\d+)\./m
10408
- );
10409
- if (!headerMatch) return null;
10410
- const prNumber = parseInt(headerMatch[1], 10);
10411
- const actionMatch = content.match(
10412
- /@(\S+)\s+(APPROVED the pull request|REQUESTED CHANGES on the pull request|left a review comment on the pull request|had their review dismissed)/
10413
- );
10414
- const commentMatch = content.match(
10415
- /Comment from @\S+:\n([\s\S]*?)(?=\n\*\*(?:PR|Issue|User|File) URL:\*\*|\nIMPORTANT INSTRUCTIONS:)/
10318
+
10319
+ // src/lib/organization.ts
10320
+ async function fetchOrganizations() {
10321
+ const response = await authenticatedFetch(
10322
+ "/v1/user/organizations"
10416
10323
  );
10417
- let commentBody = commentMatch?.[1]?.trim() ?? "";
10418
- if (!commentBody) {
10419
- const bodyMatch = content.match(
10420
- /reviewed the pull request\.\n([\s\S]*?)(?=\n\*\*(?:PR|Issue|User|File) URL:\*\*|\nIMPORTANT INSTRUCTIONS:)/
10421
- );
10422
- commentBody = bodyMatch?.[1]?.trim() ?? "";
10324
+ return response.organizations;
10325
+ }
10326
+ async function setActiveOrganization(organizationId) {
10327
+ const organizations = await fetchOrganizations();
10328
+ const organization = organizations.find((org2) => org2.id === organizationId);
10329
+ if (!organization) {
10330
+ throw new Error(`Organization with ID ${organizationId} not found or you don't have access to it.`);
10423
10331
  }
10424
- const prUrlMatch = content.match(/\*\*PR URL:\*\*\s*(.+)/);
10425
- const userUrlMatch = content.match(/\*\*User URL:\*\*\s*(.+)/);
10426
- return {
10427
- source: "github_pr_existing_pr_review",
10428
- prNumber,
10429
- reviewUser: actionMatch?.[1] ?? "",
10430
- reviewAction: actionMatch?.[2] ?? "reviewed the pull request",
10431
- commentBody,
10432
- prUrl: prUrlMatch?.[1]?.trim(),
10433
- userUrl: userUrlMatch?.[1]?.trim()
10434
- };
10332
+ setOrganizationId(organizationId);
10435
10333
  }
10436
- function parseGitHubPRExistingReview(content) {
10437
- const headerMatch = content.match(
10438
- /^You received a comment on Pull Request #(\d+)\./m
10439
- );
10440
- if (!headerMatch) return null;
10441
- const fileMatch = content.match(/\nFile: (.+)/);
10442
- const diffMatch = content.match(/\nDiff context:\n```diff\n([\s\S]*?)```/);
10443
- if (!fileMatch || !diffMatch) return null;
10444
- const prNumber = parseInt(headerMatch[1], 10);
10445
- const commentMatch = content.match(
10446
- /Comment from @(\S+):\n([\s\S]*?)(?=\n\*\*(?:PR|Issue|User|File) URL:\*\*|\nIMPORTANT INSTRUCTIONS:)/
10447
- );
10448
- const prUrlMatch = content.match(/\*\*PR URL:\*\*\s*(.+)/);
10449
- const userUrlMatch = content.match(/\*\*User URL:\*\*\s*(.+)/);
10450
- const fileUrlMatch = content.match(/\*\*File URL:\*\*\s*(.+)/);
10451
- return {
10452
- source: "github_pr_existing_review",
10453
- prNumber,
10454
- commentUser: commentMatch?.[1] ?? "",
10455
- commentBody: commentMatch?.[2]?.trim() ?? "",
10456
- filePath: fileMatch[1].trim(),
10457
- diffHunk: diffMatch[1].trim(),
10458
- prUrl: prUrlMatch?.[1]?.trim(),
10459
- userUrl: userUrlMatch?.[1]?.trim(),
10460
- fileUrl: fileUrlMatch?.[1]?.trim()
10461
- };
10334
+ async function ensureOrganization() {
10335
+ const organizations = await fetchOrganizations();
10336
+ if (organizations.length === 0) {
10337
+ throw new Error("You are not a member of any organization. Please contact support.");
10338
+ }
10339
+ const defaultOrg = organizations[0];
10340
+ setOrganizationId(defaultOrg.id);
10341
+ return defaultOrg.id;
10462
10342
  }
10463
- function parseGitHubPRExistingGeneral(content) {
10464
- const headerMatch = content.match(
10465
- /^You received a comment on Pull Request #(\d+)\./m
10466
- );
10467
- if (!headerMatch) return null;
10468
- const prNumber = parseInt(headerMatch[1], 10);
10469
- const commentMatch = content.match(
10470
- /Comment from @(\S+):\n([\s\S]*?)(?=\n\*\*(?:PR|Issue|User|File) URL:\*\*|\nIMPORTANT INSTRUCTIONS:)/
10471
- );
10472
- const prUrlMatch = content.match(/\*\*PR URL:\*\*\s*(.+)/);
10473
- const userUrlMatch = content.match(/\*\*User URL:\*\*\s*(.+)/);
10474
- return {
10475
- source: "github_pr_existing_general",
10476
- prNumber,
10477
- commentUser: commentMatch?.[1] ?? "",
10478
- commentBody: commentMatch?.[2]?.trim() ?? "",
10479
- prUrl: prUrlMatch?.[1]?.trim(),
10480
- userUrl: userUrlMatch?.[1]?.trim()
10481
- };
10343
+
10344
+ // src/commands/login.ts
10345
+ var WEB_APP_URL = process.env.REPLICAS_WEB_URL || "https://tryreplicas.com";
10346
+ function generateState() {
10347
+ return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
10482
10348
  }
10483
- function parseSlackTask(content) {
10484
- if (!content.startsWith("# Task from Slack")) return null;
10485
- const targetMatch = content.match(/## Workspace Target\nWorking in: (.+?)(?=\n\*\*(?:Thread|Target) URL:\*\*|\n\n)/);
10486
- const threadMatch = content.match(
10487
- /## (?:Thread Context|New Messages in Thread)\n([\s\S]*?)(?=\n## Latest Message)/
10488
- );
10489
- const latestMatch = content.match(
10490
- /## Latest Message \(mentions you\)\n@(.+?): ([\s\S]*?)(?=\n## Response Instructions)/
10491
- );
10492
- const threadUrlMatch = content.match(/\*\*Thread URL:\*\*\s*(.+)/);
10493
- const targetUrlMatch = content.match(/\*\*Target URL:\*\*\s*(.+)/);
10494
- return {
10495
- source: "slack_task",
10496
- workspaceTarget: targetMatch?.[1]?.trim() ?? "",
10497
- threadContext: threadMatch?.[1]?.trim() ?? "",
10498
- latestMessageUser: latestMatch?.[1]?.trim() ?? "",
10499
- latestMessageText: latestMatch?.[2]?.trim() ?? "",
10500
- threadUrl: threadUrlMatch?.[1]?.trim(),
10501
- targetUrl: targetUrlMatch?.[1]?.trim()
10502
- };
10349
+ async function loginCommand() {
10350
+ const state = generateState();
10351
+ return new Promise((resolve2, reject) => {
10352
+ let authTimeout;
10353
+ let hasHandledCallback = false;
10354
+ let lastRedirectUrl = null;
10355
+ let lastMessage = null;
10356
+ const pendingResponses = /* @__PURE__ */ new Set();
10357
+ function respondWithRedirect(res) {
10358
+ if (lastRedirectUrl) {
10359
+ res.writeHead(302, {
10360
+ "Location": lastRedirectUrl,
10361
+ "Connection": "close"
10362
+ });
10363
+ res.end();
10364
+ } else {
10365
+ res.writeHead(200, {
10366
+ "Content-Type": "text/html; charset=utf-8",
10367
+ "Connection": "close"
10368
+ });
10369
+ res.end(`
10370
+ <!DOCTYPE html>
10371
+ <html lang="en">
10372
+ <head>
10373
+ <meta charset="utf-8" />
10374
+ <title>Replicas CLI Login</title>
10375
+ <style>
10376
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background:#09090b; color:#f4f4f5; display:flex; align-items:center; justify-content:center; height:100vh; margin:0; }
10377
+ .card { border:1px solid #27272a; padding:32px; border-radius:12px; background:#0f0f12; max-width:420px; text-align:center; }
10378
+ .title { font-size:20px; margin-bottom:12px; letter-spacing:0.08em; text-transform:uppercase; color:#a855f7; }
10379
+ .message { font-family: 'Menlo', 'Source Code Pro', monospace; font-size:14px; line-height:1.6; color:#d4d4d8; white-space:pre-line; }
10380
+ </style>
10381
+ </head>
10382
+ <body>
10383
+ <div class="card">
10384
+ <div class="title">Replicas CLI</div>
10385
+ <div class="message">${lastMessage || "Authentication already processed. You can close this tab."}</div>
10386
+ </div>
10387
+ </body>
10388
+ </html>
10389
+ `);
10390
+ }
10391
+ }
10392
+ function flushPendingResponses() {
10393
+ for (const response of pendingResponses) {
10394
+ respondWithRedirect(response);
10395
+ }
10396
+ pendingResponses.clear();
10397
+ }
10398
+ const server = http.createServer(async (req, res) => {
10399
+ const url = new URL2(req.url || "", `http://${req.headers.host}`);
10400
+ if (url.pathname === "/callback") {
10401
+ if (hasHandledCallback) {
10402
+ if (!lastRedirectUrl) {
10403
+ pendingResponses.add(res);
10404
+ res.on("close", () => pendingResponses.delete(res));
10405
+ return;
10406
+ }
10407
+ respondWithRedirect(res);
10408
+ return;
10409
+ }
10410
+ const returnedState = url.searchParams.get("state");
10411
+ const accessToken = url.searchParams.get("access_token");
10412
+ const refreshToken2 = url.searchParams.get("refresh_token");
10413
+ const expiresAt = url.searchParams.get("expires_at");
10414
+ const error = url.searchParams.get("error");
10415
+ if (error) {
10416
+ const errorUrl = `${WEB_APP_URL}/cli-login/error?message=${encodeURIComponent(error)}`;
10417
+ lastRedirectUrl = errorUrl;
10418
+ lastMessage = `Authentication failed: ${error}`;
10419
+ respondWithRedirect(res);
10420
+ flushPendingResponses();
10421
+ clearTimeout(authTimeout);
10422
+ setImmediate(() => {
10423
+ server.closeAllConnections?.();
10424
+ server.close();
10425
+ reject(new Error(`Authentication failed: ${error}`));
10426
+ });
10427
+ return;
10428
+ }
10429
+ if (returnedState !== state) {
10430
+ const errorUrl = `${WEB_APP_URL}/cli-login/error?message=${encodeURIComponent("Invalid state parameter. This might be a CSRF attack.")}`;
10431
+ lastRedirectUrl = errorUrl;
10432
+ lastMessage = "Invalid state parameter. This might be a CSRF attack.";
10433
+ respondWithRedirect(res);
10434
+ flushPendingResponses();
10435
+ clearTimeout(authTimeout);
10436
+ setImmediate(() => {
10437
+ server.closeAllConnections?.();
10438
+ server.close();
10439
+ reject(new Error("Invalid state parameter"));
10440
+ });
10441
+ return;
10442
+ }
10443
+ if (!accessToken || !refreshToken2 || !expiresAt) {
10444
+ const errorUrl = `${WEB_APP_URL}/cli-login/error?message=${encodeURIComponent("Missing required authentication tokens.")}`;
10445
+ lastRedirectUrl = errorUrl;
10446
+ lastMessage = "Missing required authentication tokens.";
10447
+ respondWithRedirect(res);
10448
+ flushPendingResponses();
10449
+ clearTimeout(authTimeout);
10450
+ setImmediate(() => {
10451
+ server.closeAllConnections?.();
10452
+ server.close();
10453
+ reject(new Error("Missing authentication tokens"));
10454
+ });
10455
+ return;
10456
+ }
10457
+ hasHandledCallback = true;
10458
+ const existingConfig = readConfig();
10459
+ const config2 = {
10460
+ access_token: accessToken,
10461
+ refresh_token: refreshToken2,
10462
+ expires_at: parseInt(expiresAt, 10),
10463
+ organization_id: existingConfig?.organization_id,
10464
+ ide_command: existingConfig?.ide_command
10465
+ };
10466
+ try {
10467
+ writeConfig(config2);
10468
+ const user = await getCurrentUser();
10469
+ try {
10470
+ const orgId = await ensureOrganization();
10471
+ console.log(chalk.gray(` Organization: ${orgId}`));
10472
+ } catch (orgError) {
10473
+ console.log(chalk.yellow(" Warning: Could not fetch organizations"));
10474
+ console.log(chalk.gray(" You can set your organization later with: replicas org switch"));
10475
+ }
10476
+ const successUrl = `${WEB_APP_URL}/cli-login/success?email=${encodeURIComponent(user.email)}`;
10477
+ lastRedirectUrl = successUrl;
10478
+ lastMessage = `Successfully logged in as ${user.email}. You can close this tab.`;
10479
+ respondWithRedirect(res);
10480
+ flushPendingResponses();
10481
+ console.log(chalk.green("\n\u2713 Successfully logged in!"));
10482
+ console.log(chalk.gray(` Email: ${user.email}
10483
+ `));
10484
+ clearTimeout(authTimeout);
10485
+ setImmediate(() => {
10486
+ server.closeAllConnections?.();
10487
+ server.close();
10488
+ resolve2();
10489
+ });
10490
+ } catch (error2) {
10491
+ const errorUrl = `${WEB_APP_URL}/cli-login/error?message=${encodeURIComponent("Failed to verify authentication.")}`;
10492
+ lastRedirectUrl = errorUrl;
10493
+ lastMessage = "Failed to verify authentication.";
10494
+ respondWithRedirect(res);
10495
+ flushPendingResponses();
10496
+ clearTimeout(authTimeout);
10497
+ setImmediate(() => {
10498
+ server.closeAllConnections?.();
10499
+ server.close();
10500
+ reject(error2);
10501
+ });
10502
+ }
10503
+ } else {
10504
+ res.writeHead(404);
10505
+ res.end("Not found");
10506
+ }
10507
+ });
10508
+ server.listen(0, "localhost", () => {
10509
+ const address = server.address();
10510
+ if (!address || typeof address === "string") {
10511
+ reject(new Error("Failed to start server"));
10512
+ return;
10513
+ }
10514
+ const port = address.port;
10515
+ const loginUrl = `${WEB_APP_URL}/cli-login?port=${port}&state=${state}`;
10516
+ console.log(chalk.blue("\nOpening browser for authentication..."));
10517
+ console.log(chalk.gray(`If the browser doesn't open automatically, visit:
10518
+ ${loginUrl}
10519
+ `));
10520
+ open(loginUrl).catch((error) => {
10521
+ console.log(chalk.yellow("Failed to open browser automatically."));
10522
+ console.log(chalk.gray(`Please open this URL manually:
10523
+ ${loginUrl}
10524
+ `));
10525
+ });
10526
+ });
10527
+ authTimeout = setTimeout(() => {
10528
+ server.closeAllConnections?.();
10529
+ server.close();
10530
+ reject(new Error("Authentication timeout. Please try again."));
10531
+ }, 5 * 60 * 1e3);
10532
+ });
10503
10533
  }
10504
- function parseLinearIssue(content) {
10505
- const headerMatch = content.match(/^# Task: ([A-Z]+-\d+) - (.+)$/m);
10506
- if (!headerMatch) return null;
10507
- if (content.includes("GitHub Issue #")) return null;
10508
- if (content.includes("Pull Request #")) return null;
10509
- const identifier = headerMatch[1];
10510
- const title = headerMatch[2].trim();
10511
- let parentIssue;
10512
- const parentMatch = content.match(
10513
- /## Parent Issue\n\*\*([A-Z]+-\d+)\*\*: (.+)\n\n([\s\S]*?)(?=\n## |$)/
10514
- );
10515
- if (parentMatch) {
10516
- parentIssue = {
10517
- identifier: parentMatch[1],
10518
- title: parentMatch[2].trim(),
10519
- description: parentMatch[3].trim()
10520
- };
10534
+
10535
+ // src/commands/logout.ts
10536
+ import chalk2 from "chalk";
10537
+ function logoutCommand() {
10538
+ if (!isAuthenticated()) {
10539
+ console.log(chalk2.yellow("You are not logged in."));
10540
+ return;
10521
10541
  }
10522
- const descMatch = content.match(/## Description\n([\s\S]*?)(?=\n## |$)/);
10523
- const description = descMatch?.[1]?.trim() ?? "";
10524
- const contextMatch = content.match(
10525
- /## Additional Context\n([\s\S]*?)(?=\n## |$)/
10526
- );
10527
- const additionalContext = contextMatch?.[1]?.trim() ?? "";
10528
- const issueUrlMatch = content.match(/\*\*Issue URL:\*\*\s*(.+)/);
10529
- return {
10530
- source: "linear_issue",
10531
- identifier,
10532
- title,
10533
- parentIssue,
10534
- description,
10535
- additionalContext,
10536
- issueUrl: issueUrlMatch?.[1]?.trim()
10537
- };
10542
+ deleteConfig();
10543
+ console.log(chalk2.green("\u2713 Successfully logged out!"));
10538
10544
  }
10539
- function safeHttpsUrl(raw) {
10540
- if (!raw) return void 0;
10541
- const trimmed = raw.trim();
10542
- return trimmed.startsWith("https://") ? trimmed : void 0;
10545
+
10546
+ // src/commands/whoami.ts
10547
+ import chalk3 from "chalk";
10548
+ async function whoamiCommand() {
10549
+ if (isAgentMode()) {
10550
+ const agent = readAgentConfig();
10551
+ if (!agent) {
10552
+ console.log(chalk3.red("Agent mode config is incomplete."));
10553
+ process.exit(1);
10554
+ }
10555
+ console.log(chalk3.blue("\nWorkspace identity:"));
10556
+ console.log(chalk3.gray(` Workspace ID: ${agent.workspace_id}`));
10557
+ if (agent.organization_id) {
10558
+ console.log(chalk3.gray(` Organization ID: ${agent.organization_id}`));
10559
+ }
10560
+ console.log();
10561
+ return;
10562
+ }
10563
+ if (!isAuthenticated()) {
10564
+ console.log(chalk3.yellow("You are not logged in."));
10565
+ console.log(chalk3.gray('Run "replicas login" to authenticate.'));
10566
+ return;
10567
+ }
10568
+ try {
10569
+ const user = await getCurrentUser();
10570
+ console.log(chalk3.blue("\nCurrent User:"));
10571
+ console.log(chalk3.gray(` ID: ${user.id}`));
10572
+ console.log(chalk3.gray(` Email: ${user.email}
10573
+ `));
10574
+ } catch (error) {
10575
+ console.error(chalk3.red("Failed to get user information."));
10576
+ if (error instanceof Error) {
10577
+ console.error(chalk3.gray(error.message));
10578
+ }
10579
+ console.log(chalk3.gray('\nTry running "replicas login" again.'));
10580
+ process.exit(1);
10581
+ }
10543
10582
  }
10544
- function parseAutomationTriggered(content) {
10545
- if (!content.startsWith("# Automation Triggered")) return null;
10546
- const nameMatch = content.match(/\*\*Automation Name:\*\*\s*(.+)/);
10547
- if (!nameMatch) return null;
10548
- const triggerTypeMatch = content.match(/\*\*Trigger Type:\*\*\s*(.+)/);
10549
- const triggeredAtMatch = content.match(/\*\*Triggered At:\*\*\s*(.+)/);
10550
- const eventMatch = content.match(/\*\*Event:\*\*\s*(.+)/);
10551
- const repoMatch = content.match(/\*\*Repository:\*\*\s*(.+)/);
10552
- const repoUrlMatch = content.match(/\*\*Repository URL:\*\*\s*(.+)/);
10553
- const prNumberMatch = content.match(/\*\*PR Number:\*\*\s*(.+)/);
10554
- const prUrlMatch = content.match(/\*\*PR URL:\*\*\s*(.+)/);
10555
- const triggeredByMatch = content.match(/\*\*Triggered By:\*\*\s*@?(.+)/);
10556
- const triggeredByUrlMatch = content.match(/\*\*Triggered By URL:\*\*\s*(.+)/);
10557
- const promptMatch = content.match(/## Automation Prompt\n([\s\S]*)$/);
10558
- const parsedPrNumber = prNumberMatch ? parseInt(prNumberMatch[1].trim(), 10) : NaN;
10559
- return {
10560
- source: "automation_triggered",
10561
- automationName: nameMatch[1].trim(),
10562
- triggerType: triggerTypeMatch?.[1]?.trim() ?? "unknown",
10563
- triggeredAt: triggeredAtMatch?.[1]?.trim() ?? "",
10564
- event: eventMatch?.[1]?.trim(),
10565
- repositoryName: repoMatch?.[1]?.trim(),
10566
- repositoryUrl: safeHttpsUrl(repoUrlMatch?.[1]),
10567
- prNumber: isNaN(parsedPrNumber) ? void 0 : parsedPrNumber,
10568
- prUrl: safeHttpsUrl(prUrlMatch?.[1]),
10569
- triggeredBy: triggeredByMatch?.[1]?.trim(),
10570
- triggeredByUrl: safeHttpsUrl(triggeredByUrlMatch?.[1]),
10571
- userPrompt: promptMatch?.[1]?.trim() ?? ""
10572
- };
10583
+
10584
+ // src/commands/connect.ts
10585
+ import chalk5 from "chalk";
10586
+
10587
+ // src/lib/ssh.ts
10588
+ import { spawn } from "child_process";
10589
+ var SSH_OPTIONS = ["-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"];
10590
+ async function connectSSH(token, host, proxyCommand) {
10591
+ return new Promise((resolve2, reject) => {
10592
+ const sshArgs = proxyCommand ? [...SSH_OPTIONS, "-o", `ProxyCommand=${proxyCommand}`, `${token}@${host}`] : [...SSH_OPTIONS, `${token}@${host}`];
10593
+ const ssh = spawn("ssh", sshArgs, {
10594
+ stdio: "inherit"
10595
+ });
10596
+ ssh.on("close", () => {
10597
+ resolve2();
10598
+ });
10599
+ ssh.on("error", reject);
10600
+ });
10573
10601
  }
10574
- function parseSingleMessage(content) {
10575
- return parseCIFailure(content) ?? parseGitHubIssueNew(content) ?? parseGitHubIssueExisting(content) ?? parseGitHubPRNew(content) ?? parseGitHubPRExistingPRReview(content) ?? parseGitHubPRExistingReview(content) ?? parseGitHubPRExistingGeneral(content) ?? parseSlackTask(content) ?? parseLinearIssue(content) ?? parseAutomationTriggered(content) ?? parseInlineDiffComments(content) ?? parsePlanQuote(content) ?? { source: "raw", content };
10602
+
10603
+ // src/lib/workspace-connection.ts
10604
+ import chalk4 from "chalk";
10605
+ import prompts from "prompts";
10606
+
10607
+ // src/lib/git.ts
10608
+ import { execSync } from "child_process";
10609
+ import path2 from "path";
10610
+ function getGitRoot() {
10611
+ try {
10612
+ const root = execSync("git rev-parse --show-toplevel", {
10613
+ encoding: "utf-8",
10614
+ stdio: ["pipe", "pipe", "pipe"]
10615
+ }).trim();
10616
+ return root;
10617
+ } catch (error) {
10618
+ throw new Error("Not inside a git repository");
10619
+ }
10576
10620
  }
10577
- function parseMerged(content) {
10578
- if (!content.includes(MERGED_MESSAGE_SEPARATOR)) return null;
10579
- const chunks = content.split(MERGED_MESSAGE_SEPARATOR).map((chunk) => chunk.trim()).filter((chunk) => chunk.length > 0);
10580
- if (chunks.length < 2) return null;
10581
- return {
10582
- source: "merged",
10583
- messages: chunks.map((chunk) => parseSingleMessage(chunk))
10584
- };
10621
+ function getGitRepoName() {
10622
+ const root = getGitRoot();
10623
+ return path2.basename(root);
10585
10624
  }
10586
- function parseUserMessage(rawContent) {
10587
- const content = removeReplicasInstructions(rawContent).trim();
10588
- return parseMerged(content) ?? parseSingleMessage(content);
10625
+ function isInsideGitRepo() {
10626
+ try {
10627
+ execSync("git rev-parse --is-inside-work-tree", {
10628
+ encoding: "utf-8",
10629
+ stdio: ["pipe", "pipe", "pipe"]
10630
+ });
10631
+ return true;
10632
+ } catch {
10633
+ return false;
10634
+ }
10589
10635
  }
10590
10636
 
10591
- // ../shared/src/user-message-parser/source-config.ts
10592
- var SOURCE_CONFIG = {
10593
- ci_failure: { label: "CI Failure", color: "#ff4444" },
10594
- linear_issue: { label: "Linear", color: "#5E6AD2" },
10595
- github_issue_new: { label: "GitHub Issue", color: "#8b949e" },
10596
- github_issue_existing: { label: "GitHub Issue", color: "#8b949e" },
10597
- github_pr_new: { label: "GitHub PR", color: "#8b949e" },
10598
- github_pr_existing_review: { label: "Code Review", color: "#8b949e" },
10599
- github_pr_existing_pr_review: { label: "PR Review", color: "#8b949e" },
10600
- github_pr_existing_general: { label: "GitHub PR", color: "#8b949e" },
10601
- slack_task: { label: "Slack", color: "#BF6CC2" },
10602
- automation_triggered: { label: "Automation", color: "#f59e0b" },
10603
- plan_quote: { label: "Plan", color: "#66bb6a" },
10604
- inline_diff_comments: { label: "Diff Comments", color: "#66bb6a" },
10605
- merged: { label: "Merged", color: "#a78bfa" }
10606
- };
10607
-
10608
- // ../shared/src/workspace-groups.ts
10609
- function buildGroups(workspaces, _workspacesData, environments, _repositorySets) {
10610
- const groupMap = /* @__PURE__ */ new Map();
10611
- for (const env of environments) {
10612
- groupMap.set(env.id, {
10613
- type: "env",
10614
- id: env.id,
10615
- name: env.name,
10616
- environmentId: env.id,
10617
- isGlobal: env.is_global,
10618
- repoIdForCreate: env.repository_id,
10619
- repoSetIdForCreate: env.repository_set_id,
10620
- workspaces: []
10621
- });
10637
+ // src/lib/workspace-connection.ts
10638
+ async function prepareWorkspaceConnection(workspaceName) {
10639
+ const orgId = getOrganizationId();
10640
+ if (!orgId) {
10641
+ throw new Error('No organization selected. Please run "replicas org switch" first.');
10622
10642
  }
10623
- const placeUngrouped = (workspace) => {
10624
- const key = "__ungrouped__";
10625
- if (!groupMap.has(key)) {
10626
- groupMap.set(key, {
10627
- type: "env",
10628
- id: key,
10629
- name: "Other",
10630
- environmentId: key,
10631
- isGlobal: false,
10632
- repoIdForCreate: null,
10633
- repoSetIdForCreate: null,
10634
- workspaces: []
10635
- });
10643
+ console.log(chalk4.blue(`
10644
+ Searching for workspace: ${workspaceName}...`));
10645
+ const response = await orgAuthenticatedFetch(
10646
+ `/v1/workspaces?name=${encodeURIComponent(workspaceName)}`
10647
+ );
10648
+ if (response.workspaces.length === 0) {
10649
+ throw new Error(`No workspaces found with name matching "${workspaceName}".`);
10650
+ }
10651
+ let selectedWorkspace;
10652
+ if (response.workspaces.length === 1) {
10653
+ selectedWorkspace = response.workspaces[0];
10654
+ } else {
10655
+ console.log(chalk4.yellow(`
10656
+ Found ${response.workspaces.length} workspaces matching "${workspaceName}":`));
10657
+ const selectResponse = await prompts({
10658
+ type: "select",
10659
+ name: "workspaceId",
10660
+ message: "Select a workspace:",
10661
+ choices: response.workspaces.map((ws) => ({
10662
+ title: `${ws.name} (${ws.status || "unknown"})`,
10663
+ value: ws.id,
10664
+ description: `Status: ${ws.status || "unknown"}`
10665
+ }))
10666
+ });
10667
+ if (!selectResponse.workspaceId) {
10668
+ throw new Error("Workspace selection cancelled.");
10636
10669
  }
10637
- groupMap.get(key).workspaces.push(workspace);
10638
- };
10639
- for (const workspace of workspaces) {
10640
- if (workspace.environment_id && groupMap.has(workspace.environment_id)) {
10641
- groupMap.get(workspace.environment_id).workspaces.push(workspace);
10642
- continue;
10670
+ selectedWorkspace = response.workspaces.find((ws) => ws.id === selectResponse.workspaceId);
10671
+ }
10672
+ console.log(chalk4.green(`
10673
+ \u2713 Selected workspace: ${selectedWorkspace.name}`));
10674
+ console.log(chalk4.gray(` Status: ${selectedWorkspace.status || "unknown"}`));
10675
+ if (selectedWorkspace.status === "sleeping") {
10676
+ throw new Error(
10677
+ "Workspace is currently sleeping. Wake it using `replicas app` (press w on a sleeping workspace) or visit https://tryreplicas.com"
10678
+ );
10679
+ }
10680
+ console.log(chalk4.blue("\nRequesting SSH access token..."));
10681
+ const tokenResponse = await orgAuthenticatedFetch(
10682
+ `/v1/workspaces/${selectedWorkspace.id}/ssh-token`,
10683
+ { method: "POST" }
10684
+ );
10685
+ console.log(chalk4.green("\u2713 SSH token received"));
10686
+ let repoName = null;
10687
+ if (isInsideGitRepo()) {
10688
+ try {
10689
+ repoName = getGitRepoName();
10690
+ } catch {
10643
10691
  }
10644
- placeUngrouped(workspace);
10645
10692
  }
10646
- return Array.from(groupMap.values()).sort((a, b) => a.name.localeCompare(b.name));
10693
+ return {
10694
+ workspace: selectedWorkspace,
10695
+ sshToken: tokenResponse.token,
10696
+ sshHost: tokenResponse.host,
10697
+ sshProxyCommand: tokenResponse.proxyCommand,
10698
+ repoName
10699
+ };
10647
10700
  }
10648
10701
 
10649
- // ../shared/src/object-store/types.ts
10650
- var MEDIA_KIND = {
10651
- IMAGE: "image",
10652
- VIDEO: "video",
10653
- AUDIO: "audio"
10654
- };
10655
- var MEDIA_KINDS = [MEDIA_KIND.IMAGE, MEDIA_KIND.VIDEO, MEDIA_KIND.AUDIO];
10656
-
10657
- // ../shared/src/sse.ts
10658
- function parseSseChunk(chunk) {
10659
- let data = "";
10660
- for (const line of chunk.split("\n")) {
10661
- if (line.startsWith("data: ")) {
10662
- data += line.slice(6);
10663
- }
10702
+ // src/commands/connect.ts
10703
+ async function connectCommand(workspaceName) {
10704
+ if (!isAuthenticated()) {
10705
+ console.log(chalk5.red('Not logged in. Please run "replicas login" first.'));
10706
+ process.exit(1);
10664
10707
  }
10665
- if (!data) return null;
10666
10708
  try {
10667
- return JSON.parse(data);
10668
- } catch {
10669
- return null;
10709
+ const { workspace, sshToken, sshHost, sshProxyCommand } = await prepareWorkspaceConnection(workspaceName);
10710
+ console.log(chalk5.blue(`
10711
+ Connecting to ${workspace.name}...`));
10712
+ const sshCommand = `ssh ${sshToken}@${sshHost}`;
10713
+ console.log(chalk5.gray(`SSH command: ${sshCommand}`));
10714
+ console.log(chalk5.gray("\nPress Ctrl+D to disconnect.\n"));
10715
+ await connectSSH(sshToken, sshHost, sshProxyCommand);
10716
+ console.log(chalk5.green("\n\u2713 Disconnected from workspace.\n"));
10717
+ } catch (error) {
10718
+ console.error(chalk5.red(`
10719
+ Error: ${error instanceof Error ? error.message : "Unknown error"}`));
10720
+ process.exit(1);
10670
10721
  }
10671
10722
  }
10672
10723
 
10724
+ // src/commands/code.ts
10725
+ import chalk6 from "chalk";
10726
+ import { spawn as spawn2 } from "child_process";
10727
+
10673
10728
  // src/lib/ssh-config.ts
10674
10729
  import fs2 from "fs";
10675
10730
  import path3 from "path";
@@ -13516,20 +13571,7 @@ function useWorkspaceEvents(workspaceId, enabled = true) {
13516
13571
  return false;
13517
13572
  }
13518
13573
  setConnected(true);
13519
- const reader = response.body.getReader();
13520
- const decoder = new TextDecoder();
13521
- let buffer = "";
13522
- while (!cancelled) {
13523
- const { done, value } = await reader.read();
13524
- if (done) return false;
13525
- buffer += decoder.decode(value, { stream: true });
13526
- const chunks = buffer.split("\n\n");
13527
- buffer = chunks.pop() ?? "";
13528
- for (const chunk of chunks) {
13529
- const event = parseSseChunk(chunk);
13530
- if (event) handleEvent(event);
13531
- }
13532
- }
13574
+ await readSseStream(response.body, handleEvent, () => !cancelled);
13533
13575
  return false;
13534
13576
  } catch {
13535
13577
  setConnected(false);
@@ -16579,9 +16621,117 @@ async function envFilesDeleteCommand(envIdOrName, pathOrId, options) {
16579
16621
  Deleted file ${pathOrId}.
16580
16622
  `));
16581
16623
  }
16624
+ async function envStartHookGetCommand(envIdOrName) {
16625
+ ensureOrgApiAuthenticated();
16626
+ const id = await resolveEnvironmentId(envIdOrName);
16627
+ const response = await orgAuthenticatedFetch(
16628
+ `/v1/environments/${id}/start-hooks`
16629
+ );
16630
+ if (!response.start_hook) {
16631
+ console.log(chalk21.yellow("\nNo start hook configured.\n"));
16632
+ return;
16633
+ }
16634
+ const hook = response.start_hook;
16635
+ console.log(chalk21.green(`
16636
+ Start hook (v${hook.version}, ${hook.is_active ? "active" : "inactive"}):
16637
+ `));
16638
+ console.log(chalk21.gray(` ID: ${hook.id}`));
16639
+ console.log(chalk21.gray(` Created: ${formatDate2(hook.created_at)}
16640
+ `));
16641
+ console.log(hook.content);
16642
+ console.log();
16643
+ }
16644
+ async function envStartHookSaveCommand(envIdOrName, options) {
16645
+ ensureOrgApiAuthenticated();
16646
+ const id = await resolveEnvironmentId(envIdOrName);
16647
+ const content = readFileContent(options);
16648
+ const body = { content };
16649
+ const response = await orgAuthenticatedFetch(
16650
+ `/v1/environments/${id}/start-hooks/save`,
16651
+ { method: "POST", body }
16652
+ );
16653
+ console.log(chalk21.green(`
16654
+ Saved start hook v${response.start_hook.version}.
16655
+ `));
16656
+ }
16657
+ async function envStartHookTestCommand(envIdOrName, options) {
16658
+ ensureOrgApiAuthenticated();
16659
+ const id = await resolveEnvironmentId(envIdOrName);
16660
+ const content = readFileContent(options);
16661
+ let exitCode = null;
16662
+ let timedOut = false;
16663
+ let errorMessage = null;
16664
+ await orgAuthenticatedSseStream(
16665
+ `/v1/environments/${id}/start-hooks/test/stream`,
16666
+ {
16667
+ body: { content },
16668
+ onEvent: (event) => {
16669
+ if (event.type === "progress" && event.message) {
16670
+ console.log(chalk21.gray(event.message));
16671
+ } else if (event.type === "output" && event.output) {
16672
+ process.stdout.write(event.output);
16673
+ } else if (event.type === "complete") {
16674
+ exitCode = event.exit_code ?? null;
16675
+ timedOut = event.timed_out ?? false;
16676
+ } else if (event.type === "error") {
16677
+ errorMessage = event.message ?? "Unknown error";
16678
+ }
16679
+ }
16680
+ }
16681
+ );
16682
+ if (errorMessage) {
16683
+ console.log(chalk21.red(`
16684
+ ${errorMessage}
16685
+ `));
16686
+ process.exit(1);
16687
+ }
16688
+ if (timedOut) {
16689
+ console.log(chalk21.yellow("\nStart hook timed out.\n"));
16690
+ process.exit(1);
16691
+ }
16692
+ if (exitCode === 0) {
16693
+ console.log(chalk21.green(`
16694
+ Start hook passed (exit code ${exitCode}).
16695
+ `));
16696
+ } else {
16697
+ console.log(chalk21.red(`
16698
+ Start hook failed (exit code ${exitCode ?? "unknown"}).
16699
+ `));
16700
+ process.exit(1);
16701
+ }
16702
+ }
16703
+ async function envStartHookRepositoryHooksCommand(envIdOrName) {
16704
+ ensureOrgApiAuthenticated();
16705
+ const id = await resolveEnvironmentId(envIdOrName);
16706
+ const response = await orgAuthenticatedFetch(
16707
+ `/v1/environments/${id}/start-hooks/repository-hooks`
16708
+ );
16709
+ if (response.repositories.length === 0) {
16710
+ console.log(chalk21.yellow("\nNo repositories bound to this environment.\n"));
16711
+ return;
16712
+ }
16713
+ console.log(chalk21.green(`
16714
+ Repository start hooks (${response.repositories.length}):
16715
+ `));
16716
+ for (const repo of response.repositories) {
16717
+ console.log(chalk21.white(` ${repo.repository_name} @${repo.default_branch}`));
16718
+ if (repo.error) {
16719
+ console.log(chalk21.red(` Error: ${repo.error}`));
16720
+ } else if (repo.start_hook) {
16721
+ console.log(chalk21.gray(` Source: ${repo.filename ?? "(unknown)"}`));
16722
+ console.log(chalk21.gray(` Commands (${repo.start_hook.commands.length}):`));
16723
+ for (const cmd of repo.start_hook.commands) {
16724
+ console.log(chalk21.gray(` ${cmd}`));
16725
+ }
16726
+ } else {
16727
+ console.log(chalk21.gray(` No startHook defined.`));
16728
+ }
16729
+ console.log();
16730
+ }
16731
+ }
16582
16732
 
16583
16733
  // src/index.ts
16584
- var CLI_VERSION = "0.2.202";
16734
+ var CLI_VERSION = "0.2.204";
16585
16735
  function parseBooleanOption(value) {
16586
16736
  if (value === "true") return true;
16587
16737
  if (value === "false") return false;
@@ -17067,6 +17217,55 @@ envFiles.command("delete <env> <path-or-id>").description("Delete a file by dest
17067
17217
  process.exit(1);
17068
17218
  }
17069
17219
  });
17220
+ var envStartHooks = environment.command("start-hooks").description("Manage env-level start hooks (run on every workspace boot, before repo-level start hooks)");
17221
+ envStartHooks.command("get <env>").description("Show the active start hook for an environment").action(async (env) => {
17222
+ try {
17223
+ await envStartHookGetCommand(env);
17224
+ } catch (error) {
17225
+ if (error instanceof Error) {
17226
+ console.error(chalk22.red(`
17227
+ \u2717 ${error.message}
17228
+ `));
17229
+ }
17230
+ process.exit(1);
17231
+ }
17232
+ });
17233
+ envStartHooks.command("save <env>").description("Save and activate a start hook (provide --content or --file)").option("-c, --content <content>", "Inline hook script").option("-f, --file <local-path>", "Read hook script from a local file").action(async (env, options) => {
17234
+ try {
17235
+ await envStartHookSaveCommand(env, options);
17236
+ } catch (error) {
17237
+ if (error instanceof Error) {
17238
+ console.error(chalk22.red(`
17239
+ \u2717 ${error.message}
17240
+ `));
17241
+ }
17242
+ process.exit(1);
17243
+ }
17244
+ });
17245
+ envStartHooks.command("test <env>").description("Run a start hook in an isolated sandbox without saving it").option("-c, --content <content>", "Inline hook script").option("-f, --file <local-path>", "Read hook script from a local file").action(async (env, options) => {
17246
+ try {
17247
+ await envStartHookTestCommand(env, options);
17248
+ } catch (error) {
17249
+ if (error instanceof Error) {
17250
+ console.error(chalk22.red(`
17251
+ \u2717 ${error.message}
17252
+ `));
17253
+ }
17254
+ process.exit(1);
17255
+ }
17256
+ });
17257
+ envStartHooks.command("repository-hooks <env>").description("List per-repo start hooks defined in replicas.json / replicas.yaml").action(async (env) => {
17258
+ try {
17259
+ await envStartHookRepositoryHooksCommand(env);
17260
+ } catch (error) {
17261
+ if (error instanceof Error) {
17262
+ console.error(chalk22.red(`
17263
+ \u2717 ${error.message}
17264
+ `));
17265
+ }
17266
+ process.exit(1);
17267
+ }
17268
+ });
17070
17269
  environment.action(async () => {
17071
17270
  try {
17072
17271
  await environmentListCommand();