codeam-cli 2.21.1 → 2.22.0

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 (3) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/index.js +810 -755
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -87,7 +87,7 @@ var require_src = __commonJS({
87
87
  });
88
88
 
89
89
  // src/commands/start.ts
90
- var import_picocolors2 = __toESM(require("picocolors"));
90
+ var import_picocolors3 = __toESM(require("picocolors"));
91
91
 
92
92
  // ../../packages/shared/src/protocol/constants.ts
93
93
  var PROTOCOL_VERSION = "2.0.0";
@@ -441,7 +441,7 @@ var import_qrcode_terminal = __toESM(require("qrcode-terminal"));
441
441
  // package.json
442
442
  var package_default = {
443
443
  name: "codeam-cli",
444
- version: "2.21.1",
444
+ version: "2.22.0",
445
445
  description: "Workflow-continuity bridge for AI coding agents. Wrap Claude Code or Codex in a PTY and supervise, approve, and redirect the session from any device \u2014 async. The terminal companion for CodeAgent Mobile.",
446
446
  type: "commonjs",
447
447
  main: "dist/index.js",
@@ -5740,7 +5740,7 @@ function readAnonId() {
5740
5740
  }
5741
5741
  function superProperties() {
5742
5742
  return {
5743
- cliVersion: true ? "2.21.1" : "0.0.0-dev",
5743
+ cliVersion: true ? "2.22.0" : "0.0.0-dev",
5744
5744
  nodeVersion: process.version,
5745
5745
  platform: process.platform,
5746
5746
  arch: process.arch,
@@ -6035,7 +6035,8 @@ var CommandRelayService = class {
6035
6035
  async sendHeartbeat(online) {
6036
6036
  await _postJson(`${API_BASE2}/api/plugin/heartbeat`, {
6037
6037
  pluginId: this.pluginId,
6038
- online
6038
+ online,
6039
+ agentId: this.agentMeta.id
6039
6040
  }).then(() => log.trace("relay", `heartbeat ok online=${online}`)).catch((err) => log.trace("relay", `heartbeat failed online=${online}`, err));
6040
6041
  }
6041
6042
  reportAgents() {
@@ -13753,9 +13754,9 @@ function buildKeepAlive(ctx) {
13753
13754
  }
13754
13755
 
13755
13756
  // src/commands/start/handlers.ts
13756
- var fs26 = __toESM(require("fs"));
13757
+ var fs27 = __toESM(require("fs"));
13757
13758
  var os23 = __toESM(require("os"));
13758
- var path32 = __toESM(require("path"));
13759
+ var path33 = __toESM(require("path"));
13759
13760
  var import_crypto5 = require("crypto");
13760
13761
  var import_child_process13 = require("child_process");
13761
13762
 
@@ -13813,7 +13814,14 @@ var startCommandSchema = import_zod2.z.object({
13813
13814
  // `git restore` from there. `action='approved'` stages the edit,
13814
13815
  // `action='rejected'` discards every worktree change on the file.
13815
13816
  filePath: import_zod2.z.string().min(1).max(4096).optional(),
13816
- action: import_zod2.z.enum(["approved", "rejected"]).optional()
13817
+ action: import_zod2.z.enum(["approved", "rejected"]).optional(),
13818
+ // `request_link_credentials` — backend fires this from the
13819
+ // heartbeat handler when it notices the user is running an agent
13820
+ // they haven't vaulted yet. We reuse the `codeam link` token-
13821
+ // capture path to push the credential up; if extraction fails
13822
+ // (no local auth, missing file), the handler no-ops silently —
13823
+ // no browser-login surprises during a normal `codeam pair`.
13824
+ agentId: import_zod2.z.enum(["claude_code", "codex", "cursor", "aider", "coderabbit"]).optional()
13817
13825
  });
13818
13826
  function parsePayload2(schema, raw) {
13819
13827
  const result = schema.safeParse(raw);
@@ -14653,240 +14661,582 @@ function findGitRoot2(startDir) {
14653
14661
  return null;
14654
14662
  }
14655
14663
 
14656
- // src/commands/start/handlers.ts
14657
- var pendingAttachmentFiles = /* @__PURE__ */ new Set();
14658
- function cleanupAttachmentTempFiles() {
14659
- for (const p2 of pendingAttachmentFiles) {
14660
- try {
14661
- fs26.unlinkSync(p2);
14662
- } catch {
14663
- }
14664
- }
14665
- pendingAttachmentFiles.clear();
14664
+ // src/commands/link.ts
14665
+ var import_node_crypto4 = require("crypto");
14666
+ var fs26 = __toESM(require("fs"));
14667
+ var path32 = __toESM(require("path"));
14668
+ var import_chokidar = __toESM(require("chokidar"));
14669
+ var import_picocolors2 = __toESM(require("picocolors"));
14670
+
14671
+ // src/ui/prompts.ts
14672
+ async function confirmAction(message) {
14673
+ const result = await ot2({ message });
14674
+ if (q(result)) return false;
14675
+ return result;
14666
14676
  }
14667
- function saveFilesTemp(files) {
14668
- return files.filter(({ base64 }) => base64 && base64.length > 0).map(({ filename, base64 }) => {
14669
- const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
14670
- const tmpPath = path32.join(os23.tmpdir(), `codeam-${(0, import_crypto5.randomUUID)()}-${safeName}`);
14671
- fs26.writeFileSync(tmpPath, Buffer.from(base64, "base64"));
14672
- pendingAttachmentFiles.add(tmpPath);
14673
- return tmpPath;
14677
+ async function selectSession(sessions3, activeId) {
14678
+ const result = await _t({
14679
+ message: "Select active session:",
14680
+ options: sessions3.map((s) => ({
14681
+ value: s.id,
14682
+ label: `${s.userName} ${s.plan}`,
14683
+ hint: s.id === activeId ? "active" : `paired ${new Date(s.pairedAt).toLocaleDateString()}`
14684
+ })),
14685
+ initialValue: activeId ?? void 0
14674
14686
  });
14687
+ if (q(result)) return null;
14688
+ return result;
14675
14689
  }
14676
- function dispatchPrompt(ctx, prompt) {
14677
- ctx.outputSvc.newTurn();
14678
- ctx.agent.sendCommand(prompt);
14690
+
14691
+ // src/commands/link.ts
14692
+ function buildLinkContext(agentId) {
14693
+ const runtime = createRuntimeStrategy(agentId);
14694
+ return {
14695
+ runtime,
14696
+ locator: runtime.credentialLocator(),
14697
+ launcher: runtime.loginLauncher(),
14698
+ displayName: runtime.meta.displayName,
14699
+ binary: runtime.meta.binaryName
14700
+ };
14679
14701
  }
14680
- var startTask = (ctx, _cmd, parsed) => {
14681
- const { prompt, files } = parsed;
14682
- const effectivePrompt = prompt ?? "";
14683
- if (files && files.length > 0) {
14684
- const paths = saveFilesTemp(files);
14685
- const atRefs = paths.map((p2) => `@${p2}`).join(" ");
14686
- ctx.outputSvc.newTurn();
14687
- ctx.agent.sendCommand(`${atRefs} ${effectivePrompt}`.trim());
14688
- setTimeout(() => {
14689
- for (const p2 of paths) {
14690
- try {
14691
- fs26.unlinkSync(p2);
14692
- } catch {
14693
- }
14694
- pendingAttachmentFiles.delete(p2);
14695
- }
14696
- }, 12e4);
14697
- } else if (effectivePrompt) {
14698
- dispatchPrompt(ctx, effectivePrompt);
14702
+ function enabledLinkableAgents() {
14703
+ return Object.values(AGENT_REGISTRY).filter((m) => m.enabled).map((m) => m.id);
14704
+ }
14705
+ function parseLinkArgs(args2) {
14706
+ const positional = args2.find((a) => !a.startsWith("--"));
14707
+ const valid = enabledLinkableAgents();
14708
+ if (!positional) {
14709
+ throw new Error(
14710
+ `Usage: codeam link <agent>
14711
+ agent: ${valid.join(" | ")}`
14712
+ );
14699
14713
  }
14700
- };
14701
- var provideInput = (ctx, _cmd, parsed) => {
14702
- if (parsed.input) dispatchPrompt(ctx, parsed.input);
14703
- };
14704
- var selectOption = (ctx, _cmd, parsed) => {
14705
- const index = parsed.index ?? 0;
14706
- const from = parsed.from ?? 0;
14707
- ctx.outputSvc.newTurn();
14708
- ctx.agent.selectOption(index, from);
14709
- };
14710
- var escapeKey = (ctx) => {
14711
- ctx.outputSvc.newTurn();
14712
- ctx.agent.sendEscape();
14713
- };
14714
- var stopTask = (ctx) => {
14715
- ctx.agent.interrupt();
14716
- };
14717
- var resumeSession = async (ctx, _cmd, parsed) => {
14718
- const { id, auto } = parsed;
14719
- if (!id) return;
14720
- ctx.historySvc.setCurrentConversationId(id);
14721
- await ctx.historySvc.loadConversation(id);
14722
- await ctx.outputSvc.newTurnResume(id);
14723
- ctx.agent.restart(id, auto ?? false);
14724
- };
14725
- var getContext = async (ctx, cmd) => {
14726
- const usage = ctx.historySvc.getCurrentUsage();
14727
- const monthlyCost = ctx.historySvc.getMonthlyEstimatedCost();
14728
- const rateLimitReset = ctx.historySvc.getRateLimitReset();
14729
- const quotaPercent = ctx.historySvc.getQuotaPercent();
14730
- const base = usage ? { ...usage, monthlyCost } : { used: 0, total: 2e5, percent: 0, model: null, outputTokens: 0, cacheReadTokens: 0, monthlyCost, error: "No usage data found" };
14731
- const result = {
14732
- ...base,
14733
- ...rateLimitReset ? { rateLimitReset } : {},
14734
- ...quotaPercent !== null ? { quotaPercent } : {}
14735
- };
14736
- await ctx.relay.sendResult(cmd.id, "completed", result);
14737
- };
14738
- var getConversation = async (ctx, cmd) => {
14739
- const currentId = ctx.historySvc.getCurrentConversationId();
14740
- if (!currentId) {
14741
- await ctx.relay.sendResult(cmd.id, "completed", { conversationId: null });
14742
- return;
14714
+ const normalised = positional === "claude_code" ? "claude" : positional;
14715
+ if (!isKnownAgentId(normalised) || !AGENT_REGISTRY[normalised].enabled) {
14716
+ throw new Error(
14717
+ `Unknown or unsupported agent "${positional}". Valid: ${valid.join(", ")}`
14718
+ );
14743
14719
  }
14744
- try {
14745
- await ctx.historySvc.loadConversation(currentId);
14746
- await ctx.relay.sendResult(cmd.id, "completed", { conversationId: currentId });
14747
- } catch {
14748
- await ctx.relay.sendResult(cmd.id, "failed", {});
14720
+ const reuseExisting = args2.includes("--reuse-existing");
14721
+ const dryRun = args2.includes("--dry-run");
14722
+ const apiKeyFileArg = args2.find((a) => a.startsWith("--api-key-file="));
14723
+ let apiKey = null;
14724
+ if (apiKeyFileArg) {
14725
+ const filePath = apiKeyFileArg.slice("--api-key-file=".length);
14726
+ try {
14727
+ apiKey = fs26.readFileSync(path32.resolve(filePath), "utf8").trim();
14728
+ } catch (err) {
14729
+ throw new Error(`Could not read --api-key-file ${filePath}: ${err.message}`);
14730
+ }
14731
+ if (!apiKey) {
14732
+ throw new Error(`--api-key-file ${filePath} is empty.`);
14733
+ }
14734
+ } else {
14735
+ const apiKeyArg = args2.find((a) => a.startsWith("--api-key="));
14736
+ apiKey = apiKeyArg ? apiKeyArg.slice("--api-key=".length) : null;
14749
14737
  }
14750
- };
14751
- var listModels = async (ctx, cmd) => {
14752
- const models = await ctx.runtime.listModels();
14753
- await ctx.relay.sendResult(cmd.id, "completed", { models });
14754
- };
14755
- var changeModel = async (ctx, cmd) => {
14756
- const params = cmd.payload;
14757
- if (typeof params.modelId !== "string" || !params.modelId) {
14758
- await ctx.relay.sendResult(cmd.id, "failed", { error: "modelId required" });
14738
+ const tokenFileArg = args2.find((a) => a.startsWith("--token-file="));
14739
+ const tokenFile = tokenFileArg ? tokenFileArg.slice("--token-file=".length) : null;
14740
+ return { agent: normalised, reuseExisting, apiKey, tokenFile, dryRun };
14741
+ }
14742
+ async function link(args2 = []) {
14743
+ const parsed = parseLinkArgs(args2);
14744
+ const ctx = buildLinkContext(parsed.agent);
14745
+ showIntro();
14746
+ console.log(
14747
+ import_picocolors2.default.bold(` Link ${ctx.displayName}`) + import_picocolors2.default.dim(` \xB7 ${ctx.locator.vendor}`)
14748
+ );
14749
+ console.log("");
14750
+ if (parsed.dryRun) {
14751
+ await linkDryRunPreflight(ctx);
14759
14752
  return;
14760
14753
  }
14761
- const instr = ctx.runtime.changeModelInstruction(params.modelId);
14762
- if (instr.type === "pty") {
14763
- if (!instr.ptyInput) {
14764
- await ctx.relay.sendResult(cmd.id, "failed", { error: "no pty input for this agent" });
14765
- return;
14766
- }
14767
- ctx.agent.sendRawPtyInput(instr.ptyInput);
14768
- } else if (instr.type === "restart") {
14769
- await ctx.relay.sendResult(cmd.id, "failed", { error: "restart-mode change_model not supported in Phase 1" });
14770
- return;
14754
+ const pluginId = (0, import_node_crypto4.randomUUID)();
14755
+ const spin = dist_exports.spinner();
14756
+ spin.start("Requesting pairing code...");
14757
+ const pairing = await requestCode(pluginId);
14758
+ if (!pairing) {
14759
+ spin.stop("Failed");
14760
+ showError("Could not reach the server. Check your connection and try again.");
14761
+ process.exit(1);
14771
14762
  }
14772
- await ctx.relay.sendResult(cmd.id, "completed", {});
14773
- };
14774
- var summarize = async (ctx, cmd) => {
14775
- const params = cmd.payload;
14776
- const mode = params.mode === "auto" ? "auto" : "normal";
14777
- const instr = ctx.runtime.summarizeInstruction(mode);
14778
- ctx.agent.sendRawPtyInput(instr.ptyInput);
14779
- await ctx.relay.sendResult(cmd.id, "completed", {});
14780
- };
14781
- var setKeepAlive = async (ctx, cmd) => {
14782
- const enabled = !!cmd.payload.enabled;
14783
- ctx.setKeepAlive(enabled);
14784
- try {
14785
- await ctx.relay.sendResult(
14786
- cmd.id,
14787
- "success",
14788
- {
14789
- enabled,
14790
- applied: enabled && ctx.keepAliveCtx.inCodespace,
14791
- runtime: ctx.keepAliveCtx.inCodespace ? "github-codespaces" : "local"
14763
+ spin.stop("Got pairing code");
14764
+ showPairingCode(pairing.code);
14765
+ console.log(import_picocolors2.default.dim(" Scan the QR or enter the code in CodeAgent Mobile."));
14766
+ console.log("");
14767
+ const waitSpin = dist_exports.spinner();
14768
+ const waitMsg = () => `Waiting for mobile pair... \xB7 expires in ${formatRemaining(pairing.expiresAt)}`;
14769
+ waitSpin.start(waitMsg());
14770
+ const countdown = setInterval(() => waitSpin.message(waitMsg()), 1e3);
14771
+ countdown.unref?.();
14772
+ const paired = await new Promise((resolve5, reject) => {
14773
+ let stopPoll = null;
14774
+ const sigint = () => {
14775
+ clearInterval(countdown);
14776
+ stopPoll?.();
14777
+ reject(new Error("cancelled"));
14778
+ };
14779
+ stopPoll = pollStatus(
14780
+ pluginId,
14781
+ (info) => {
14782
+ process.removeListener("SIGINT", sigint);
14783
+ clearInterval(countdown);
14784
+ waitSpin.stop("Paired");
14785
+ resolve5(info);
14786
+ },
14787
+ () => {
14788
+ clearInterval(countdown);
14789
+ waitSpin.stop("Timed out");
14790
+ reject(new Error("Pairing timed out after 5 minutes. Run codeam link again."));
14792
14791
  }
14793
14792
  );
14794
- } catch {
14793
+ process.once("SIGINT", sigint);
14794
+ });
14795
+ if (!paired.pluginAuthToken) {
14796
+ showError(
14797
+ "Backend did not return a pluginAuthToken \u2014 upgrade api-v2 (deploy includes the link endpoint)."
14798
+ );
14799
+ process.exit(1);
14795
14800
  }
14796
- };
14797
- var sessionTerminated = (ctx) => {
14798
- showInfo("Session was deleted from the app \u2014 exiting.");
14799
- try {
14800
- ctx.agent.kill();
14801
- } catch {
14801
+ addSession({
14802
+ id: paired.sessionId,
14803
+ pluginId,
14804
+ userName: paired.userName,
14805
+ userEmail: paired.userEmail,
14806
+ plan: paired.plan,
14807
+ pairedAt: Date.now(),
14808
+ pluginAuthToken: paired.pluginAuthToken,
14809
+ agent: ctx.runtime.id
14810
+ });
14811
+ saveCliConfig({ ...loadCliConfig(), preferredAgent: ctx.runtime.id });
14812
+ if (parsed.apiKey) {
14813
+ await uploadAndSucceed(ctx, paired, pluginId, {
14814
+ method: "api_key",
14815
+ credential: parsed.apiKey.trim(),
14816
+ source: "manual"
14817
+ });
14818
+ return;
14802
14819
  }
14803
- try {
14804
- const proc = (0, import_child_process13.spawn)("bash", ["-lc", "pm2 delete codeam-pair >/dev/null 2>&1 || true"], {
14805
- detached: true,
14806
- stdio: "ignore"
14820
+ if (parsed.tokenFile) {
14821
+ const credential = fs26.readFileSync(path32.resolve(parsed.tokenFile), "utf8").trim();
14822
+ if (!credential) {
14823
+ showError(`--token-file ${parsed.tokenFile} is empty.`);
14824
+ process.exit(1);
14825
+ }
14826
+ await uploadAndSucceed(ctx, paired, pluginId, {
14827
+ method: "oauth",
14828
+ credential,
14829
+ source: "manual"
14807
14830
  });
14808
- proc.unref();
14809
- } catch {
14831
+ return;
14810
14832
  }
14811
- ctx.outputSvc.dispose();
14812
- ctx.relay.stop();
14813
- process.exit(0);
14814
- };
14815
- var shutdownSession = async (ctx, cmd) => {
14816
- try {
14817
- await ctx.relay.sendResult(cmd.id, "success", { ok: true });
14818
- } catch {
14833
+ const installSpin = dist_exports.spinner();
14834
+ installSpin.start(`Checking that ${ctx.binary} is installed...`);
14835
+ const installed = await ctx.launcher.ensureInstalled();
14836
+ if (!installed) {
14837
+ installSpin.stop("Failed");
14838
+ showError(`Could not install ${ctx.displayName}. Install it manually then re-run.`);
14839
+ process.exit(1);
14819
14840
  }
14820
- try {
14821
- ctx.agent.kill();
14822
- } catch {
14841
+ installSpin.stop(`${ctx.displayName} is installed`);
14842
+ const existing = await ctx.locator.extract();
14843
+ if (existing) {
14844
+ showInfo(`Found existing ${ctx.displayName} credentials at ${import_picocolors2.default.bold(existing.source)}.`);
14845
+ await uploadAndSucceed(ctx, paired, pluginId, existing);
14846
+ return;
14823
14847
  }
14824
- if (ctx.keepAliveCtx.inCodespace && ctx.keepAliveCtx.codespaceName) {
14825
- try {
14826
- const stopProc = (0, import_child_process13.spawn)(
14827
- "bash",
14828
- ["-lc", `sleep 1; gh codespace stop -c ${JSON.stringify(ctx.keepAliveCtx.codespaceName)} >/dev/null 2>&1 || true`],
14829
- { detached: true, stdio: "ignore" }
14830
- );
14831
- stopProc.unref();
14832
- } catch {
14833
- }
14848
+ if (parsed.reuseExisting) {
14849
+ showError(
14850
+ `--reuse-existing set, but no local ${ctx.displayName} credentials were found at ${ctx.locator.hint}.`
14851
+ );
14852
+ process.exit(1);
14834
14853
  }
14854
+ showInfo(
14855
+ `No local ${ctx.displayName} credentials found. Launching the sign-in \u2014 complete it in your browser, the CLI will detect the new token and finish automatically.`
14856
+ );
14857
+ console.log("");
14858
+ const captured = await captureFreshCredentials(ctx);
14859
+ console.log("");
14860
+ await uploadAndSucceed(ctx, paired, pluginId, captured);
14861
+ }
14862
+ async function captureFreshCredentials(ctx) {
14863
+ const isWin = ctx.runtime.os.id === "win32";
14864
+ const watcher = import_chokidar.default.watch(ctx.locator.watchPaths(), {
14865
+ persistent: true,
14866
+ ignoreInitial: false,
14867
+ awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 },
14868
+ // Windows-only: when a credential file's ancestor is missing,
14869
+ // chokidar walks up to the closest existing parent and starts
14870
+ // traversing it. On a default Windows shell that ancestor is
14871
+ // `C:\Users\<u>`, which contains legacy junctions whose ACL makes
14872
+ // `fs.watch` throw EPERM (#43). These flags are no-ops on macOS,
14873
+ // where the user has read access to their entire home.
14874
+ ...isWin ? { followSymlinks: false, ignorePermissionErrors: true } : {}
14875
+ });
14876
+ watcher.on("error", () => {
14877
+ });
14878
+ let child = null;
14879
+ let keychainPoll = null;
14880
+ const cleanup = () => {
14881
+ void watcher.close();
14882
+ if (keychainPoll) clearInterval(keychainPoll);
14883
+ if (child && !child.killed) {
14884
+ try {
14885
+ child.kill("SIGTERM");
14886
+ } catch {
14887
+ }
14888
+ }
14889
+ };
14835
14890
  try {
14836
- const proc = (0, import_child_process13.spawn)("bash", ["-lc", "pm2 delete codeam-pair >/dev/null 2>&1 || true"], {
14837
- detached: true,
14838
- stdio: "ignore"
14891
+ const token = await new Promise((resolve5, reject) => {
14892
+ let settled = false;
14893
+ const tryExtract = async () => {
14894
+ if (settled) return;
14895
+ const t2 = await ctx.locator.extract();
14896
+ if (t2 && !settled) {
14897
+ settled = true;
14898
+ resolve5(t2);
14899
+ }
14900
+ };
14901
+ watcher.on("add", () => void tryExtract());
14902
+ watcher.on("change", () => void tryExtract());
14903
+ keychainPoll = setInterval(() => void tryExtract(), 2e3);
14904
+ keychainPoll.unref?.();
14905
+ const sigint = () => {
14906
+ if (settled) return;
14907
+ settled = true;
14908
+ reject(new Error("cancelled"));
14909
+ };
14910
+ process.once("SIGINT", sigint);
14911
+ setTimeout(() => {
14912
+ if (settled) return;
14913
+ settled = true;
14914
+ reject(new Error(`Timed out waiting for ${ctx.displayName} sign-in (5 minutes).`));
14915
+ }, 5 * 6e4);
14916
+ child = ctx.launcher.launch();
14917
+ child.on("exit", () => {
14918
+ void tryExtract().then(() => {
14919
+ if (!settled) {
14920
+ settled = true;
14921
+ reject(
14922
+ new Error(
14923
+ `${ctx.binary} exited but no credentials were written at ${ctx.locator.hint}.`
14924
+ )
14925
+ );
14926
+ }
14927
+ });
14928
+ });
14839
14929
  });
14840
- proc.unref();
14841
- } catch {
14930
+ cleanup();
14931
+ return token;
14932
+ } catch (err) {
14933
+ cleanup();
14934
+ throw err;
14842
14935
  }
14843
- ctx.outputSvc.dispose();
14844
- ctx.relay.stop();
14845
- process.exit(0);
14846
- };
14847
- var readFile4 = async (ctx, cmd, parsed) => {
14848
- if (!parsed.path) {
14849
- await ctx.relay.sendResult(cmd.id, "failed", { error: "Missing path" });
14850
- return;
14936
+ }
14937
+ async function uploadAndSucceed(ctx, paired, pluginId, token) {
14938
+ if (!paired.pluginAuthToken) {
14939
+ showError("Missing pluginAuthToken; re-run codeam link.");
14940
+ process.exit(1);
14851
14941
  }
14852
- const result = await readProjectFile(parsed.path);
14853
- await ctx.relay.sendResult(cmd.id, "completed", result);
14854
- };
14855
- var writeFile3 = async (ctx, cmd, parsed) => {
14856
- if (!parsed.path || typeof parsed.content !== "string") {
14857
- await ctx.relay.sendResult(cmd.id, "failed", { error: "Missing path or content" });
14858
- return;
14942
+ const uploadSpin = dist_exports.spinner();
14943
+ uploadSpin.start("Sealing credential in your vault...");
14944
+ const result = await postLinkCredential({
14945
+ agentId: ctx.locator.publicId,
14946
+ sessionId: paired.sessionId,
14947
+ pluginId,
14948
+ pluginAuthToken: paired.pluginAuthToken,
14949
+ method: token.method,
14950
+ credential: token.credential
14951
+ });
14952
+ if (!result.ok) {
14953
+ uploadSpin.stop("Failed");
14954
+ if (result.status === 401) {
14955
+ showError("Pair token rejected by the backend (401). Re-run `codeam link`.");
14956
+ } else if (result.status === 404) {
14957
+ showError(
14958
+ `${ctx.displayName} link endpoint not available on this backend (404). The api-v2 deployment may not yet include the route.`
14959
+ );
14960
+ } else {
14961
+ showError(`Upload failed: ${result.message}`);
14962
+ }
14963
+ process.exit(1);
14859
14964
  }
14860
- const result = await writeProjectFile(parsed.path, parsed.content);
14861
- await ctx.relay.sendResult(cmd.id, "completed", result);
14862
- };
14863
- var listFiles = async (ctx, cmd, parsed) => {
14864
- const result = await listProjectFiles({ query: parsed.query });
14865
- await ctx.relay.sendResult(cmd.id, "completed", result);
14866
- };
14867
- var terminalOpenH = async (ctx, cmd, parsed) => {
14868
- const r = openTerminal({
14869
- cols: typeof parsed.cols === "number" ? parsed.cols : void 0,
14870
- rows: typeof parsed.rows === "number" ? parsed.rows : void 0,
14871
- cwd: typeof parsed.cwd === "string" ? parsed.cwd : void 0
14965
+ uploadSpin.stop("Linked");
14966
+ console.log("");
14967
+ showSuccess(`${ctx.displayName} is now linked to ${paired.userEmail || paired.userName}.`);
14968
+ showInfo(
14969
+ `Your codespaces and @codeagent mentions can now use ${ctx.displayName} without you signing in again.`
14970
+ );
14971
+ console.log("");
14972
+ }
14973
+ async function linkDryRunPreflight(ctx) {
14974
+ const publicId = ctx.locator.publicId;
14975
+ const spin = dist_exports.spinner();
14976
+ spin.start(`Probing ${publicId} link endpoint...`);
14977
+ const result = await postLinkCredential({
14978
+ agentId: publicId,
14979
+ sessionId: "dryrun-session",
14980
+ pluginId: "dryrun-plugin",
14981
+ pluginAuthToken: "dryrun-token",
14982
+ method: "oauth",
14983
+ credential: "dryrun-credential"
14872
14984
  });
14873
- if ("error" in r) {
14874
- await ctx.relay.sendResult(cmd.id, "failed", { error: r.error });
14875
- return;
14985
+ if (result.ok) {
14986
+ spin.stop("Unexpected 2xx");
14987
+ showError(
14988
+ "Link dry-run: backend accepted a stub credential (2xx). PluginAuthGuard appears to be disabled \u2014 investigate api-v2."
14989
+ );
14990
+ process.exit(1);
14876
14991
  }
14877
- await ctx.relay.sendResult(cmd.id, "completed", r);
14878
- };
14879
- var terminalWriteH = async (ctx, cmd, parsed) => {
14880
- if (typeof parsed.sessionId !== "string" || typeof parsed.data !== "string") {
14881
- await ctx.relay.sendResult(cmd.id, "failed", { error: "Missing sessionId or data" });
14882
- return;
14992
+ if (result.status === 401) {
14993
+ spin.stop("Endpoint OK");
14994
+ showSuccess(
14995
+ `Link dry-run OK \u2014 /api/plugin/agents/${publicId}/link reachable and auth-gated (401 as expected).`
14996
+ );
14997
+ process.exit(0);
14883
14998
  }
14884
- const r = writeTerminal(parsed.sessionId, parsed.data);
14885
- await ctx.relay.sendResult(cmd.id, r.ok ? "completed" : "failed", r);
14886
- };
14887
- var terminalResizeH = async (ctx, cmd, parsed) => {
14888
- if (typeof parsed.sessionId !== "string" || typeof parsed.cols !== "number" || typeof parsed.rows !== "number") {
14889
- await ctx.relay.sendResult(cmd.id, "failed", { error: "Missing sessionId / cols / rows" });
14999
+ spin.stop("Failed");
15000
+ showError(
15001
+ `Link dry-run: unexpected response from /api/plugin/agents/${publicId}/link (status=${result.status}, message=${result.message}). Expected 401.`
15002
+ );
15003
+ process.exit(1);
15004
+ }
15005
+
15006
+ // src/commands/start/handlers.ts
15007
+ var pendingAttachmentFiles = /* @__PURE__ */ new Set();
15008
+ function cleanupAttachmentTempFiles() {
15009
+ for (const p2 of pendingAttachmentFiles) {
15010
+ try {
15011
+ fs27.unlinkSync(p2);
15012
+ } catch {
15013
+ }
15014
+ }
15015
+ pendingAttachmentFiles.clear();
15016
+ }
15017
+ function saveFilesTemp(files) {
15018
+ return files.filter(({ base64 }) => base64 && base64.length > 0).map(({ filename, base64 }) => {
15019
+ const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
15020
+ const tmpPath = path33.join(os23.tmpdir(), `codeam-${(0, import_crypto5.randomUUID)()}-${safeName}`);
15021
+ fs27.writeFileSync(tmpPath, Buffer.from(base64, "base64"));
15022
+ pendingAttachmentFiles.add(tmpPath);
15023
+ return tmpPath;
15024
+ });
15025
+ }
15026
+ function dispatchPrompt(ctx, prompt) {
15027
+ ctx.outputSvc.newTurn();
15028
+ ctx.agent.sendCommand(prompt);
15029
+ }
15030
+ var startTask = (ctx, _cmd, parsed) => {
15031
+ const { prompt, files } = parsed;
15032
+ const effectivePrompt = prompt ?? "";
15033
+ if (files && files.length > 0) {
15034
+ const paths = saveFilesTemp(files);
15035
+ const atRefs = paths.map((p2) => `@${p2}`).join(" ");
15036
+ ctx.outputSvc.newTurn();
15037
+ ctx.agent.sendCommand(`${atRefs} ${effectivePrompt}`.trim());
15038
+ setTimeout(() => {
15039
+ for (const p2 of paths) {
15040
+ try {
15041
+ fs27.unlinkSync(p2);
15042
+ } catch {
15043
+ }
15044
+ pendingAttachmentFiles.delete(p2);
15045
+ }
15046
+ }, 12e4);
15047
+ } else if (effectivePrompt) {
15048
+ dispatchPrompt(ctx, effectivePrompt);
15049
+ }
15050
+ };
15051
+ var provideInput = (ctx, _cmd, parsed) => {
15052
+ if (parsed.input) dispatchPrompt(ctx, parsed.input);
15053
+ };
15054
+ var selectOption = (ctx, _cmd, parsed) => {
15055
+ const index = parsed.index ?? 0;
15056
+ const from = parsed.from ?? 0;
15057
+ ctx.outputSvc.newTurn();
15058
+ ctx.agent.selectOption(index, from);
15059
+ };
15060
+ var escapeKey = (ctx) => {
15061
+ ctx.outputSvc.newTurn();
15062
+ ctx.agent.sendEscape();
15063
+ };
15064
+ var stopTask = (ctx) => {
15065
+ ctx.agent.interrupt();
15066
+ };
15067
+ var resumeSession = async (ctx, _cmd, parsed) => {
15068
+ const { id, auto } = parsed;
15069
+ if (!id) return;
15070
+ ctx.historySvc.setCurrentConversationId(id);
15071
+ await ctx.historySvc.loadConversation(id);
15072
+ await ctx.outputSvc.newTurnResume(id);
15073
+ ctx.agent.restart(id, auto ?? false);
15074
+ };
15075
+ var getContext = async (ctx, cmd) => {
15076
+ const usage = ctx.historySvc.getCurrentUsage();
15077
+ const monthlyCost = ctx.historySvc.getMonthlyEstimatedCost();
15078
+ const rateLimitReset = ctx.historySvc.getRateLimitReset();
15079
+ const quotaPercent = ctx.historySvc.getQuotaPercent();
15080
+ const base = usage ? { ...usage, monthlyCost } : { used: 0, total: 2e5, percent: 0, model: null, outputTokens: 0, cacheReadTokens: 0, monthlyCost, error: "No usage data found" };
15081
+ const result = {
15082
+ ...base,
15083
+ ...rateLimitReset ? { rateLimitReset } : {},
15084
+ ...quotaPercent !== null ? { quotaPercent } : {}
15085
+ };
15086
+ await ctx.relay.sendResult(cmd.id, "completed", result);
15087
+ };
15088
+ var getConversation = async (ctx, cmd) => {
15089
+ const currentId = ctx.historySvc.getCurrentConversationId();
15090
+ if (!currentId) {
15091
+ await ctx.relay.sendResult(cmd.id, "completed", { conversationId: null });
15092
+ return;
15093
+ }
15094
+ try {
15095
+ await ctx.historySvc.loadConversation(currentId);
15096
+ await ctx.relay.sendResult(cmd.id, "completed", { conversationId: currentId });
15097
+ } catch {
15098
+ await ctx.relay.sendResult(cmd.id, "failed", {});
15099
+ }
15100
+ };
15101
+ var listModels = async (ctx, cmd) => {
15102
+ const models = await ctx.runtime.listModels();
15103
+ await ctx.relay.sendResult(cmd.id, "completed", { models });
15104
+ };
15105
+ var changeModel = async (ctx, cmd) => {
15106
+ const params = cmd.payload;
15107
+ if (typeof params.modelId !== "string" || !params.modelId) {
15108
+ await ctx.relay.sendResult(cmd.id, "failed", { error: "modelId required" });
15109
+ return;
15110
+ }
15111
+ const instr = ctx.runtime.changeModelInstruction(params.modelId);
15112
+ if (instr.type === "pty") {
15113
+ if (!instr.ptyInput) {
15114
+ await ctx.relay.sendResult(cmd.id, "failed", { error: "no pty input for this agent" });
15115
+ return;
15116
+ }
15117
+ ctx.agent.sendRawPtyInput(instr.ptyInput);
15118
+ } else if (instr.type === "restart") {
15119
+ await ctx.relay.sendResult(cmd.id, "failed", { error: "restart-mode change_model not supported in Phase 1" });
15120
+ return;
15121
+ }
15122
+ await ctx.relay.sendResult(cmd.id, "completed", {});
15123
+ };
15124
+ var summarize = async (ctx, cmd) => {
15125
+ const params = cmd.payload;
15126
+ const mode = params.mode === "auto" ? "auto" : "normal";
15127
+ const instr = ctx.runtime.summarizeInstruction(mode);
15128
+ ctx.agent.sendRawPtyInput(instr.ptyInput);
15129
+ await ctx.relay.sendResult(cmd.id, "completed", {});
15130
+ };
15131
+ var setKeepAlive = async (ctx, cmd) => {
15132
+ const enabled = !!cmd.payload.enabled;
15133
+ ctx.setKeepAlive(enabled);
15134
+ try {
15135
+ await ctx.relay.sendResult(
15136
+ cmd.id,
15137
+ "success",
15138
+ {
15139
+ enabled,
15140
+ applied: enabled && ctx.keepAliveCtx.inCodespace,
15141
+ runtime: ctx.keepAliveCtx.inCodespace ? "github-codespaces" : "local"
15142
+ }
15143
+ );
15144
+ } catch {
15145
+ }
15146
+ };
15147
+ var sessionTerminated = (ctx) => {
15148
+ showInfo("Session was deleted from the app \u2014 exiting.");
15149
+ try {
15150
+ ctx.agent.kill();
15151
+ } catch {
15152
+ }
15153
+ try {
15154
+ const proc = (0, import_child_process13.spawn)("bash", ["-lc", "pm2 delete codeam-pair >/dev/null 2>&1 || true"], {
15155
+ detached: true,
15156
+ stdio: "ignore"
15157
+ });
15158
+ proc.unref();
15159
+ } catch {
15160
+ }
15161
+ ctx.outputSvc.dispose();
15162
+ ctx.relay.stop();
15163
+ process.exit(0);
15164
+ };
15165
+ var shutdownSession = async (ctx, cmd) => {
15166
+ try {
15167
+ await ctx.relay.sendResult(cmd.id, "success", { ok: true });
15168
+ } catch {
15169
+ }
15170
+ try {
15171
+ ctx.agent.kill();
15172
+ } catch {
15173
+ }
15174
+ if (ctx.keepAliveCtx.inCodespace && ctx.keepAliveCtx.codespaceName) {
15175
+ try {
15176
+ const stopProc = (0, import_child_process13.spawn)(
15177
+ "bash",
15178
+ ["-lc", `sleep 1; gh codespace stop -c ${JSON.stringify(ctx.keepAliveCtx.codespaceName)} >/dev/null 2>&1 || true`],
15179
+ { detached: true, stdio: "ignore" }
15180
+ );
15181
+ stopProc.unref();
15182
+ } catch {
15183
+ }
15184
+ }
15185
+ try {
15186
+ const proc = (0, import_child_process13.spawn)("bash", ["-lc", "pm2 delete codeam-pair >/dev/null 2>&1 || true"], {
15187
+ detached: true,
15188
+ stdio: "ignore"
15189
+ });
15190
+ proc.unref();
15191
+ } catch {
15192
+ }
15193
+ ctx.outputSvc.dispose();
15194
+ ctx.relay.stop();
15195
+ process.exit(0);
15196
+ };
15197
+ var readFile4 = async (ctx, cmd, parsed) => {
15198
+ if (!parsed.path) {
15199
+ await ctx.relay.sendResult(cmd.id, "failed", { error: "Missing path" });
15200
+ return;
15201
+ }
15202
+ const result = await readProjectFile(parsed.path);
15203
+ await ctx.relay.sendResult(cmd.id, "completed", result);
15204
+ };
15205
+ var writeFile3 = async (ctx, cmd, parsed) => {
15206
+ if (!parsed.path || typeof parsed.content !== "string") {
15207
+ await ctx.relay.sendResult(cmd.id, "failed", { error: "Missing path or content" });
15208
+ return;
15209
+ }
15210
+ const result = await writeProjectFile(parsed.path, parsed.content);
15211
+ await ctx.relay.sendResult(cmd.id, "completed", result);
15212
+ };
15213
+ var listFiles = async (ctx, cmd, parsed) => {
15214
+ const result = await listProjectFiles({ query: parsed.query });
15215
+ await ctx.relay.sendResult(cmd.id, "completed", result);
15216
+ };
15217
+ var terminalOpenH = async (ctx, cmd, parsed) => {
15218
+ const r = openTerminal({
15219
+ cols: typeof parsed.cols === "number" ? parsed.cols : void 0,
15220
+ rows: typeof parsed.rows === "number" ? parsed.rows : void 0,
15221
+ cwd: typeof parsed.cwd === "string" ? parsed.cwd : void 0
15222
+ });
15223
+ if ("error" in r) {
15224
+ await ctx.relay.sendResult(cmd.id, "failed", { error: r.error });
15225
+ return;
15226
+ }
15227
+ await ctx.relay.sendResult(cmd.id, "completed", r);
15228
+ };
15229
+ var terminalWriteH = async (ctx, cmd, parsed) => {
15230
+ if (typeof parsed.sessionId !== "string" || typeof parsed.data !== "string") {
15231
+ await ctx.relay.sendResult(cmd.id, "failed", { error: "Missing sessionId or data" });
15232
+ return;
15233
+ }
15234
+ const r = writeTerminal(parsed.sessionId, parsed.data);
15235
+ await ctx.relay.sendResult(cmd.id, r.ok ? "completed" : "failed", r);
15236
+ };
15237
+ var terminalResizeH = async (ctx, cmd, parsed) => {
15238
+ if (typeof parsed.sessionId !== "string" || typeof parsed.cols !== "number" || typeof parsed.rows !== "number") {
15239
+ await ctx.relay.sendResult(cmd.id, "failed", { error: "Missing sessionId / cols / rows" });
14890
15240
  return;
14891
15241
  }
14892
15242
  const r = resizeTerminal(parsed.sessionId, parsed.cols, parsed.rows);
@@ -14974,25 +15324,66 @@ var applyFileReviewH = async (ctx, cmd, parsed) => {
14974
15324
  result
14975
15325
  );
14976
15326
  };
14977
- var handlers = {
14978
- start_task: startTask,
14979
- provide_input: provideInput,
14980
- select_option: selectOption,
14981
- escape_key: escapeKey,
14982
- stop_task: stopTask,
14983
- resume_session: resumeSession,
14984
- get_context: getContext,
14985
- get_conversation: getConversation,
14986
- list_models: listModels,
14987
- change_model: changeModel,
14988
- summarize,
14989
- set_keep_alive: setKeepAlive,
14990
- session_terminated: sessionTerminated,
14991
- shutdown_session: shutdownSession,
14992
- read_file: readFile4,
14993
- write_file: writeFile3,
14994
- list_files: listFiles,
14995
- search_files: searchFilesH,
15327
+ var requestLinkCredentialsH = async (ctx, _cmd, parsed) => {
15328
+ const publicId = parsed.agentId;
15329
+ if (!publicId) return;
15330
+ if (!ctx.pluginAuthToken) {
15331
+ log.trace("auto-link", "skipped \u2014 no pluginAuthToken on this paired session");
15332
+ return;
15333
+ }
15334
+ const internalId = publicId === "claude_code" ? "claude" : publicId;
15335
+ if (!isKnownAgentId(internalId) || !AGENT_REGISTRY[internalId].enabled) {
15336
+ log.trace("auto-link", `unknown / disabled agent: ${internalId}`);
15337
+ return;
15338
+ }
15339
+ let linkCtx;
15340
+ try {
15341
+ linkCtx = buildLinkContext(internalId);
15342
+ } catch (err) {
15343
+ log.trace("auto-link", "buildLinkContext threw", err);
15344
+ return;
15345
+ }
15346
+ const token = await linkCtx.locator.extract().catch((err) => {
15347
+ log.trace("auto-link", `locator.extract failed for ${publicId}`, err);
15348
+ return null;
15349
+ });
15350
+ if (!token) {
15351
+ log.trace("auto-link", `no local ${linkCtx.displayName} credentials \u2014 skipping`);
15352
+ return;
15353
+ }
15354
+ const result = await postLinkCredential({
15355
+ agentId: publicId,
15356
+ sessionId: ctx.sessionId,
15357
+ pluginId: ctx.pluginId,
15358
+ pluginAuthToken: ctx.pluginAuthToken,
15359
+ method: token.method,
15360
+ credential: token.credential
15361
+ });
15362
+ if (result.ok) {
15363
+ log.trace("auto-link", `vaulted ${publicId} from ${token.source}`);
15364
+ } else {
15365
+ log.trace("auto-link", `upload failed (${result.status}): ${result.message}`);
15366
+ }
15367
+ };
15368
+ var handlers = {
15369
+ start_task: startTask,
15370
+ provide_input: provideInput,
15371
+ select_option: selectOption,
15372
+ escape_key: escapeKey,
15373
+ stop_task: stopTask,
15374
+ resume_session: resumeSession,
15375
+ get_context: getContext,
15376
+ get_conversation: getConversation,
15377
+ list_models: listModels,
15378
+ change_model: changeModel,
15379
+ summarize,
15380
+ set_keep_alive: setKeepAlive,
15381
+ session_terminated: sessionTerminated,
15382
+ shutdown_session: shutdownSession,
15383
+ read_file: readFile4,
15384
+ write_file: writeFile3,
15385
+ list_files: listFiles,
15386
+ search_files: searchFilesH,
14996
15387
  terminal_open: terminalOpenH,
14997
15388
  terminal_write: terminalWriteH,
14998
15389
  terminal_resize: terminalResizeH,
@@ -15005,7 +15396,8 @@ var handlers = {
15005
15396
  git_push: gitPushH,
15006
15397
  git_pull: gitPullH,
15007
15398
  git_resolve: gitResolveH,
15008
- apply_file_review: applyFileReviewH
15399
+ apply_file_review: applyFileReviewH,
15400
+ request_link_credentials: requestLinkCredentialsH
15009
15401
  };
15010
15402
  async function dispatchCommand(ctx, cmd) {
15011
15403
  const parsed = parsePayload2(startCommandSchema, cmd.payload);
@@ -15025,14 +15417,14 @@ async function start(requestedAgent) {
15025
15417
  if (!session) {
15026
15418
  if (requestedAgent) {
15027
15419
  const displayName = AGENT_REGISTRY[requestedAgent]?.displayName ?? requestedAgent;
15028
- console.log(` ${import_picocolors2.default.dim(`No paired ${displayName} session found.`)}`);
15420
+ console.log(` ${import_picocolors3.default.dim(`No paired ${displayName} session found.`)}`);
15029
15421
  console.log(
15030
- ` ${import_picocolors2.default.dim(`Run ${import_picocolors2.default.white("codeam pair")} from a ${displayName} setup to connect your mobile app.`)}
15422
+ ` ${import_picocolors3.default.dim(`Run ${import_picocolors3.default.white("codeam pair")} from a ${displayName} setup to connect your mobile app.`)}
15031
15423
  `
15032
15424
  );
15033
15425
  } else {
15034
- console.log(` ${import_picocolors2.default.dim("No paired session found.")}`);
15035
- console.log(` ${import_picocolors2.default.dim(`Run ${import_picocolors2.default.white("codeam pair")} to connect your mobile app.`)}
15426
+ console.log(` ${import_picocolors3.default.dim("No paired session found.")}`);
15427
+ console.log(` ${import_picocolors3.default.dim(`Run ${import_picocolors3.default.white("codeam pair")} to connect your mobile app.`)}
15036
15428
  `);
15037
15429
  }
15038
15430
  process.exit(0);
@@ -15041,7 +15433,7 @@ async function start(requestedAgent) {
15041
15433
  throw new Error("Active session has no agent \u2014 re-pair with `codeam pair`.");
15042
15434
  }
15043
15435
  const pluginId = session.pluginId ?? ensurePluginId();
15044
- showInfo(`${session.userName} \xB7 ${import_picocolors2.default.cyan(session.plan)}`);
15436
+ showInfo(`${session.userName} \xB7 ${import_picocolors3.default.cyan(session.plan)}`);
15045
15437
  showInfo(`Launching ${AGENT_REGISTRY[session.agent].displayName}...
15046
15438
  `);
15047
15439
  identifyUser({
@@ -15142,7 +15534,10 @@ async function start(requestedAgent) {
15142
15534
  runtime,
15143
15535
  relay: void 0,
15144
15536
  setKeepAlive: setKeepAlive2,
15145
- keepAliveCtx
15537
+ keepAliveCtx,
15538
+ pluginId,
15539
+ sessionId: session.id,
15540
+ pluginAuthToken: session.pluginAuthToken ?? void 0
15146
15541
  };
15147
15542
  const relay = new CommandRelayService(pluginId, async (cmd) => {
15148
15543
  await dispatchCommand(ctx, cmd);
@@ -15187,27 +15582,7 @@ async function start(requestedAgent) {
15187
15582
 
15188
15583
  // src/commands/pair.ts
15189
15584
  var import_crypto6 = require("crypto");
15190
- var import_picocolors3 = __toESM(require("picocolors"));
15191
-
15192
- // src/ui/prompts.ts
15193
- async function confirmAction(message) {
15194
- const result = await ot2({ message });
15195
- if (q(result)) return false;
15196
- return result;
15197
- }
15198
- async function selectSession(sessions3, activeId) {
15199
- const result = await _t({
15200
- message: "Select active session:",
15201
- options: sessions3.map((s) => ({
15202
- value: s.id,
15203
- label: `${s.userName} ${s.plan}`,
15204
- hint: s.id === activeId ? "active" : `paired ${new Date(s.pairedAt).toLocaleDateString()}`
15205
- })),
15206
- initialValue: activeId ?? void 0
15207
- });
15208
- if (q(result)) return null;
15209
- return result;
15210
- }
15585
+ var import_picocolors4 = __toESM(require("picocolors"));
15211
15586
 
15212
15587
  // src/utils/agent-prompt.ts
15213
15588
  function parseAgentFlag(args2) {
@@ -15276,7 +15651,7 @@ async function pair(args2 = []) {
15276
15651
  process.exit(0);
15277
15652
  }
15278
15653
  showPairingCode(result.code);
15279
- console.log(import_picocolors3.default.dim(" Scan the QR code or enter the code in CodeAgent Mobile."));
15654
+ console.log(import_picocolors4.default.dim(" Scan the QR code or enter the code in CodeAgent Mobile."));
15280
15655
  console.log("");
15281
15656
  const waitSpin = dist_exports.spinner();
15282
15657
  const waitMessage = () => `Waiting for mobile app... \xB7 expires in ${formatRemaining(result.expiresAt)}`;
@@ -15341,7 +15716,7 @@ async function pair(args2 = []) {
15341
15716
  }
15342
15717
 
15343
15718
  // src/commands/pair-auto.ts
15344
- var fs27 = __toESM(require("fs"));
15719
+ var fs28 = __toESM(require("fs"));
15345
15720
  var os24 = __toESM(require("os"));
15346
15721
  var import_crypto7 = require("crypto");
15347
15722
  var API_BASE8 = resolveApiBaseUrl();
@@ -15361,10 +15736,10 @@ function readTokenFromArgs(args2) {
15361
15736
  if (fileFlag) {
15362
15737
  const path40 = fileFlag.slice("--token-file=".length);
15363
15738
  try {
15364
- const content = fs27.readFileSync(path40, "utf8").trim();
15739
+ const content = fs28.readFileSync(path40, "utf8").trim();
15365
15740
  if (content.length === 0) fail(`--token-file ${path40} is empty`);
15366
15741
  try {
15367
- fs27.unlinkSync(path40);
15742
+ fs28.unlinkSync(path40);
15368
15743
  } catch {
15369
15744
  }
15370
15745
  return content;
@@ -15487,7 +15862,7 @@ async function pairAuto(args2) {
15487
15862
  }
15488
15863
 
15489
15864
  // src/commands/sessions.ts
15490
- var import_picocolors4 = __toESM(require("picocolors"));
15865
+ var import_picocolors5 = __toESM(require("picocolors"));
15491
15866
  async function sessions2(args2) {
15492
15867
  const [sub, id] = args2;
15493
15868
  if (sub === "switch") return switchSession();
@@ -15504,18 +15879,18 @@ function listSessions() {
15504
15879
  showIntro();
15505
15880
  const config = getConfig();
15506
15881
  if (config.sessions.length === 0) {
15507
- console.log(import_picocolors4.default.dim(" No paired sessions. Run codeam pair to connect.\n"));
15882
+ console.log(import_picocolors5.default.dim(" No paired sessions. Run codeam pair to connect.\n"));
15508
15883
  return;
15509
15884
  }
15510
- console.log(import_picocolors4.default.bold(" Paired sessions:\n"));
15885
+ console.log(import_picocolors5.default.bold(" Paired sessions:\n"));
15511
15886
  for (const s of config.sessions) {
15512
15887
  const isActive = s.id === config.activeSessionId;
15513
- const bullet = isActive ? import_picocolors4.default.green(" \u25CF") : import_picocolors4.default.dim(" \u25CB");
15514
- const name = isActive ? import_picocolors4.default.bold(s.userName) : s.userName;
15515
- const plan = import_picocolors4.default.cyan(s.plan);
15516
- const date = import_picocolors4.default.dim(new Date(s.pairedAt).toLocaleDateString());
15888
+ const bullet = isActive ? import_picocolors5.default.green(" \u25CF") : import_picocolors5.default.dim(" \u25CB");
15889
+ const name = isActive ? import_picocolors5.default.bold(s.userName) : s.userName;
15890
+ const plan = import_picocolors5.default.cyan(s.plan);
15891
+ const date = import_picocolors5.default.dim(new Date(s.pairedAt).toLocaleDateString());
15517
15892
  console.log(`${bullet} ${name} ${plan} ${date}`);
15518
- console.log(import_picocolors4.default.dim(` ${s.id}`));
15893
+ console.log(import_picocolors5.default.dim(` ${s.id}`));
15519
15894
  }
15520
15895
  console.log("");
15521
15896
  }
@@ -15533,7 +15908,7 @@ async function switchSession() {
15533
15908
  }
15534
15909
  setActiveSession(chosen);
15535
15910
  const s = config.sessions.find((x) => x.id === chosen);
15536
- console.log(import_picocolors4.default.green(`
15911
+ console.log(import_picocolors5.default.green(`
15537
15912
  \u2713 Switched to ${s?.userName ?? chosen}
15538
15913
  `));
15539
15914
  }
@@ -15551,29 +15926,29 @@ async function deleteSession(id) {
15551
15926
  return;
15552
15927
  }
15553
15928
  removeSession(id);
15554
- console.log(import_picocolors4.default.green("\n \u2713 Session deleted\n"));
15929
+ console.log(import_picocolors5.default.green("\n \u2713 Session deleted\n"));
15555
15930
  }
15556
15931
 
15557
15932
  // src/commands/status.ts
15558
- var import_picocolors5 = __toESM(require("picocolors"));
15933
+ var import_picocolors6 = __toESM(require("picocolors"));
15559
15934
  function status() {
15560
15935
  showIntro();
15561
15936
  const config = getConfig();
15562
15937
  const active = config.sessions.find((s) => s.id === config.activeSessionId) ?? null;
15563
- console.log(import_picocolors5.default.bold(" Status\n"));
15564
- console.log(` Plugin ID ${import_picocolors5.default.dim(config.pluginId || "not generated yet")}`);
15938
+ console.log(import_picocolors6.default.bold(" Status\n"));
15939
+ console.log(` Plugin ID ${import_picocolors6.default.dim(config.pluginId || "not generated yet")}`);
15565
15940
  console.log(` Sessions ${config.sessions.length} paired`);
15566
15941
  if (active) {
15567
- console.log(` Active ${import_picocolors5.default.bold(active.userName)} ${import_picocolors5.default.cyan(active.plan)}`);
15568
- console.log(` Session ID ${import_picocolors5.default.dim(active.id)}`);
15942
+ console.log(` Active ${import_picocolors6.default.bold(active.userName)} ${import_picocolors6.default.cyan(active.plan)}`);
15943
+ console.log(` Session ID ${import_picocolors6.default.dim(active.id)}`);
15569
15944
  } else {
15570
- console.log(` Active ${import_picocolors5.default.yellow("none")} ${import_picocolors5.default.dim("run codeam pair to connect")}`);
15945
+ console.log(` Active ${import_picocolors6.default.yellow("none")} ${import_picocolors6.default.dim("run codeam pair to connect")}`);
15571
15946
  }
15572
15947
  console.log("");
15573
15948
  }
15574
15949
 
15575
15950
  // src/commands/logout.ts
15576
- var import_picocolors6 = __toESM(require("picocolors"));
15951
+ var import_picocolors7 = __toESM(require("picocolors"));
15577
15952
  var API_BASE9 = resolveApiBaseUrl();
15578
15953
  async function notifyBackendOffline() {
15579
15954
  const cfg = loadCliConfig();
@@ -15607,17 +15982,17 @@ async function logout() {
15607
15982
  }
15608
15983
  await notifyBackendOffline();
15609
15984
  clearAll();
15610
- console.log(import_picocolors6.default.green("\n \u2713 Done. All sessions removed.\n"));
15985
+ console.log(import_picocolors7.default.green("\n \u2713 Done. All sessions removed.\n"));
15611
15986
  }
15612
15987
 
15613
15988
  // src/commands/deploy.ts
15614
- var import_picocolors9 = __toESM(require("picocolors"));
15989
+ var import_picocolors10 = __toESM(require("picocolors"));
15615
15990
 
15616
15991
  // src/services/providers/github-codespaces.ts
15617
15992
  var import_child_process14 = require("child_process");
15618
15993
  var import_util3 = require("util");
15619
- var import_picocolors7 = __toESM(require("picocolors"));
15620
- var path33 = __toESM(require("path"));
15994
+ var import_picocolors8 = __toESM(require("picocolors"));
15995
+ var path34 = __toESM(require("path"));
15621
15996
  var execFileP4 = (0, import_util3.promisify)(import_child_process14.execFile);
15622
15997
  var MAX_BUFFER = 8 * 1024 * 1024;
15623
15998
  function resetStdinForChild() {
@@ -15684,7 +16059,7 @@ var GitHubCodespacesProvider = class {
15684
16059
  if (expectedUser) {
15685
16060
  noteLines.push("");
15686
16061
  noteLines.push(
15687
- `${import_picocolors7.default.yellow("\u26A0")} Sign in as ${import_picocolors7.default.cyan(expectedUser)} in the browser.`
16062
+ `${import_picocolors8.default.yellow("\u26A0")} Sign in as ${import_picocolors8.default.cyan(expectedUser)} in the browser.`
15688
16063
  );
15689
16064
  noteLines.push(
15690
16065
  " If a different GitHub account is already signed in, sign out"
@@ -15707,7 +16082,7 @@ var GitHubCodespacesProvider = class {
15707
16082
  if (refreshCode !== 0) {
15708
16083
  const lines = [
15709
16084
  "The browser approval came back for a different GitHub account",
15710
- `than the one gh is configured for${expectedUser ? ` (${import_picocolors7.default.cyan(expectedUser)})` : ""}.`,
16085
+ `than the one gh is configured for${expectedUser ? ` (${import_picocolors8.default.cyan(expectedUser)})` : ""}.`,
15711
16086
  "",
15712
16087
  "To recover:",
15713
16088
  " 1. Open https://github.com and sign out of any non-target",
@@ -15716,7 +16091,7 @@ var GitHubCodespacesProvider = class {
15716
16091
  "",
15717
16092
  "You can also grant the scope manually first and skip this step",
15718
16093
  "on the next run:",
15719
- ` ${import_picocolors7.default.cyan("gh auth refresh -h github.com -s codespace")}`
16094
+ ` ${import_picocolors8.default.cyan("gh auth refresh -h github.com -s codespace")}`
15720
16095
  ];
15721
16096
  throw new Error(lines.join("\n"));
15722
16097
  }
@@ -15783,7 +16158,7 @@ var GitHubCodespacesProvider = class {
15783
16158
  async tryInstallGh() {
15784
16159
  const platform2 = process.platform;
15785
16160
  wt(
15786
- `GitHub CLI (${import_picocolors7.default.cyan("gh")}) is required for Codespaces deploys but isn't on your PATH.`,
16161
+ `GitHub CLI (${import_picocolors8.default.cyan("gh")}) is required for Codespaces deploys but isn't on your PATH.`,
15787
16162
  "Heads up"
15788
16163
  );
15789
16164
  if (platform2 === "linux") {
@@ -15839,7 +16214,7 @@ var GitHubCodespacesProvider = class {
15839
16214
  return;
15840
16215
  }
15841
16216
  const proceed = await ot2({
15842
- message: `Run ${import_picocolors7.default.cyan(installCmd.describe)} now?`,
16217
+ message: `Run ${import_picocolors8.default.cyan(installCmd.describe)} now?`,
15843
16218
  initialValue: true
15844
16219
  });
15845
16220
  if (q(proceed) || !proceed) return;
@@ -16106,7 +16481,7 @@ var GitHubCodespacesProvider = class {
16106
16481
  });
16107
16482
  }
16108
16483
  async uploadFile(workspaceId, remotePath, contents, options = {}) {
16109
- const remoteDir = path33.posix.dirname(remotePath);
16484
+ const remoteDir = path34.posix.dirname(remotePath);
16110
16485
  const parts = [
16111
16486
  `mkdir -p ${shellQuote(remoteDir)}`,
16112
16487
  `cat > ${shellQuote(remotePath)}`
@@ -16176,8 +16551,8 @@ function shellQuote(s) {
16176
16551
  // src/services/providers/gitpod.ts
16177
16552
  var import_child_process15 = require("child_process");
16178
16553
  var import_util4 = require("util");
16179
- var path34 = __toESM(require("path"));
16180
- var import_picocolors8 = __toESM(require("picocolors"));
16554
+ var path35 = __toESM(require("path"));
16555
+ var import_picocolors9 = __toESM(require("picocolors"));
16181
16556
  var execFileP5 = (0, import_util4.promisify)(import_child_process15.execFile);
16182
16557
  var MAX_BUFFER2 = 8 * 1024 * 1024;
16183
16558
  function resetStdinForChild2() {
@@ -16416,7 +16791,7 @@ var GitpodProvider = class {
16416
16791
  });
16417
16792
  }
16418
16793
  async uploadFile(workspaceId, remotePath, contents, options = {}) {
16419
- const remoteDir = path34.posix.dirname(remotePath);
16794
+ const remoteDir = path35.posix.dirname(remotePath);
16420
16795
  const parts = [
16421
16796
  `mkdir -p ${shellQuote2(remoteDir)}`,
16422
16797
  `cat > ${shellQuote2(remotePath)}`
@@ -16452,7 +16827,7 @@ function shellQuote2(s) {
16452
16827
  // src/services/providers/gitlab-workspaces.ts
16453
16828
  var import_child_process16 = require("child_process");
16454
16829
  var import_util5 = require("util");
16455
- var path35 = __toESM(require("path"));
16830
+ var path36 = __toESM(require("path"));
16456
16831
  var execFileP6 = (0, import_util5.promisify)(import_child_process16.execFile);
16457
16832
  var MAX_BUFFER3 = 8 * 1024 * 1024;
16458
16833
  var GITLAB_API_BASE = process.env.CODEAM_GITLAB_API_URL ?? "https://gitlab.com/api/v4";
@@ -16712,7 +17087,7 @@ Docs: https://docs.gitlab.com/ee/user/workspace/configuration.html`
16712
17087
  }
16713
17088
  async uploadFile(workspaceId, remotePath, contents, options = {}) {
16714
17089
  const sshHost = process.env.CODEAM_GITLAB_SSH_HOST ?? "workspaces.gitlab.com";
16715
- const remoteDir = path35.posix.dirname(remotePath);
17090
+ const remoteDir = path36.posix.dirname(remotePath);
16716
17091
  const parts = [`mkdir -p ${shellQuote3(remoteDir)}`, `cat > ${shellQuote3(remotePath)}`];
16717
17092
  if (options.mode != null) {
16718
17093
  parts.push(`chmod ${options.mode.toString(8)} ${shellQuote3(remotePath)}`);
@@ -16780,7 +17155,7 @@ function shellQuote3(s) {
16780
17155
  // src/services/providers/railway.ts
16781
17156
  var import_child_process17 = require("child_process");
16782
17157
  var import_util6 = require("util");
16783
- var path36 = __toESM(require("path"));
17158
+ var path37 = __toESM(require("path"));
16784
17159
  var execFileP7 = (0, import_util6.promisify)(import_child_process17.execFile);
16785
17160
  var MAX_BUFFER4 = 8 * 1024 * 1024;
16786
17161
  function resetStdinForChild4() {
@@ -17016,7 +17391,7 @@ var RailwayProvider = class {
17016
17391
  if (!projectId || !serviceId) {
17017
17392
  throw new Error("Invalid Railway workspace id (expected projectId/serviceId).");
17018
17393
  }
17019
- const remoteDir = path36.posix.dirname(remotePath);
17394
+ const remoteDir = path37.posix.dirname(remotePath);
17020
17395
  const parts = [`mkdir -p ${shellQuote4(remoteDir)}`, `cat > ${shellQuote4(remotePath)}`];
17021
17396
  if (options.mode != null) {
17022
17397
  parts.push(`chmod ${options.mode.toString(8)} ${shellQuote4(remotePath)}`);
@@ -17057,7 +17432,7 @@ var PROVIDERS = [
17057
17432
  // src/commands/deploy.ts
17058
17433
  async function deploy(args2 = []) {
17059
17434
  console.log();
17060
- mt(import_picocolors9.default.bgMagenta(import_picocolors9.default.white(" codeam deploy ")));
17435
+ mt(import_picocolors10.default.bgMagenta(import_picocolors10.default.white(" codeam deploy ")));
17061
17436
  const provider = await pickProvider();
17062
17437
  if (!provider) {
17063
17438
  pt("No provider selected.");
@@ -17094,7 +17469,7 @@ async function deploy(args2 = []) {
17094
17469
  if (provider.expandListScopes) {
17095
17470
  options.push({
17096
17471
  value: EXPAND_SCOPES,
17097
- label: import_picocolors9.default.cyan("+ Don't see your project? Expand scopes\u2026"),
17472
+ label: import_picocolors10.default.cyan("+ Don't see your project? Expand scopes\u2026"),
17098
17473
  hint: "Re-authorize with broader scopes (org / team repos)"
17099
17474
  });
17100
17475
  }
@@ -17142,7 +17517,7 @@ async function deploy(args2 = []) {
17142
17517
  label: w3.displayName ?? w3.id,
17143
17518
  hint: [w3.state, formatLastUsed(w3.lastUsedAt)].filter(Boolean).join(" \xB7 ")
17144
17519
  })),
17145
- { value: "__new__", label: import_picocolors9.default.green("+ Create a new workspace"), hint: "fresh codespace" }
17520
+ { value: "__new__", label: import_picocolors10.default.green("+ Create a new workspace"), hint: "fresh codespace" }
17146
17521
  ]
17147
17522
  });
17148
17523
  if (q(choice)) {
@@ -17243,12 +17618,12 @@ async function deploy(args2 = []) {
17243
17618
  cliStep.stop("\u2713 codeam-cli installed");
17244
17619
  wt(
17245
17620
  [
17246
- `Workspace: ${import_picocolors9.default.cyan(workspace.displayName ?? workspace.id)}`,
17247
- workspace.webUrl ? `Web: ${import_picocolors9.default.cyan(workspace.webUrl)}` : "",
17621
+ `Workspace: ${import_picocolors10.default.cyan(workspace.displayName ?? workspace.id)}`,
17622
+ workspace.webUrl ? `Web: ${import_picocolors10.default.cyan(workspace.webUrl)}` : "",
17248
17623
  "",
17249
17624
  `Starting \`codeam pair\` on the workspace (agent: ${AGENT_REGISTRY[agentId].displayName}).`,
17250
17625
  "Scan the QR code from your phone to pair.",
17251
- import_picocolors9.default.dim("(Once paired, this terminal disconnects automatically; the session stays alive on the codespace.)")
17626
+ import_picocolors10.default.dim("(Once paired, this terminal disconnects automatically; the session stays alive on the codespace.)")
17252
17627
  ].filter(Boolean).join("\n"),
17253
17628
  "Almost there"
17254
17629
  );
@@ -17363,11 +17738,11 @@ async function deploy(args2 = []) {
17363
17738
  ].join("\n");
17364
17739
  const code = (await provider.streamCommand(workspace.id, `bash -lc ${shellQuoteSingle(wrapper)}`)).code;
17365
17740
  if (code === 0) {
17366
- gt(import_picocolors9.default.green("\u2713 Workspace deployed and paired. Drive from your phone, anywhere."));
17741
+ gt(import_picocolors10.default.green("\u2713 Workspace deployed and paired. Drive from your phone, anywhere."));
17367
17742
  } else if (code === 130) {
17368
- gt(import_picocolors9.default.yellow("Disconnected from local terminal. Mobile session keeps running on the codespace."));
17743
+ gt(import_picocolors10.default.yellow("Disconnected from local terminal. Mobile session keeps running on the codespace."));
17369
17744
  } else {
17370
- gt(import_picocolors9.default.yellow('Pairing did not complete. Run "codeam pair" inside the codespace if needed.'));
17745
+ gt(import_picocolors10.default.yellow('Pairing did not complete. Run "codeam pair" inside the codespace if needed.'));
17371
17746
  }
17372
17747
  }
17373
17748
  function shellQuoteSingle(s) {
@@ -17401,7 +17776,7 @@ async function pickProvider() {
17401
17776
  message: "Where do you want to deploy?",
17402
17777
  options: PROVIDERS.map((prov) => ({
17403
17778
  value: prov.id,
17404
- label: prov.available ? prov.displayName : `${prov.displayName} ${import_picocolors9.default.dim("(coming soon)")}`,
17779
+ label: prov.available ? prov.displayName : `${prov.displayName} ${import_picocolors10.default.dim("(coming soon)")}`,
17405
17780
  hint: prov.tagline
17406
17781
  }))
17407
17782
  });
@@ -17415,464 +17790,144 @@ async function pickProvider() {
17415
17790
  return null;
17416
17791
  }
17417
17792
  return found;
17418
- }
17419
-
17420
- // src/commands/deploy-manage.ts
17421
- var import_picocolors10 = __toESM(require("picocolors"));
17422
- async function deployList() {
17423
- console.log();
17424
- mt(import_picocolors10.default.bgMagenta(import_picocolors10.default.white(" codeam deploy ls ")));
17425
- const workspaces = await collectWorkspacesWithStatus();
17426
- if (workspaces.length === 0) {
17427
- gt(import_picocolors10.default.dim("No deployed workspaces found."));
17428
- return;
17429
- }
17430
- for (const w3 of workspaces) {
17431
- const tag = w3.codeamRunning ? import_picocolors10.default.green("\u25CF running") : w3.state === "Available" ? import_picocolors10.default.dim("\u25CB idle") : import_picocolors10.default.dim(`\u25CB ${w3.state ?? "stopped"}`);
17432
- console.log(` ${tag} ${import_picocolors10.default.cyan(w3.displayName ?? w3.id)} ${import_picocolors10.default.dim("(" + w3.providerName + ")")}`);
17433
- }
17434
- gt(import_picocolors10.default.dim("Use `codeam deploy stop` to terminate a session."));
17435
- }
17436
- async function deployStop() {
17437
- console.log();
17438
- mt(import_picocolors10.default.bgMagenta(import_picocolors10.default.white(" codeam deploy stop ")));
17439
- const workspaces = await collectWorkspacesWithStatus();
17440
- if (workspaces.length === 0) {
17441
- gt(import_picocolors10.default.dim("No deployed workspaces found."));
17442
- return;
17443
- }
17444
- const choice = await _t({
17445
- message: "Pick a workspace to stop:",
17446
- options: workspaces.map((w3) => ({
17447
- value: w3.id,
17448
- label: w3.displayName ?? w3.id,
17449
- hint: [
17450
- w3.providerName,
17451
- w3.codeamRunning ? import_picocolors10.default.green("\u25CF codeam-pair running") : import_picocolors10.default.dim("\u25CB no codeam-pair"),
17452
- w3.state ?? ""
17453
- ].filter(Boolean).join(" \xB7 ")
17454
- }))
17455
- });
17456
- if (q(choice)) {
17457
- pt("Cancelled.");
17458
- process.exit(0);
17459
- }
17460
- const target = workspaces.find((w3) => w3.id === choice);
17461
- if (target.codeamRunning) {
17462
- const stopStep = fe();
17463
- stopStep.start("Stopping codeam-pair on the workspace\u2026");
17464
- try {
17465
- const result = await target.provider.exec(
17466
- target.id,
17467
- "pm2 delete codeam-pair >/dev/null 2>&1; pm2 list 2>/dev/null | grep -c codeam-pair || true"
17468
- );
17469
- void result;
17470
- stopStep.stop("\u2713 codeam-pair stopped \u2014 your phone is now disconnected from this workspace");
17471
- } catch (err) {
17472
- stopStep.stop("\u26A0 Could not reach the workspace to stop codeam-pair");
17473
- void err;
17474
- }
17475
- } else {
17476
- O2.info("No codeam-pair process to stop on this workspace.");
17477
- }
17478
- const alsoStop = await ot2({
17479
- message: `Also stop the workspace ${import_picocolors10.default.cyan(target.displayName ?? target.id)} to save compute hours?`,
17480
- initialValue: true
17481
- });
17482
- if (!q(alsoStop) && alsoStop) {
17483
- const cs = fe();
17484
- cs.start("Stopping workspace\u2026");
17485
- try {
17486
- const result = await target.provider.exec(
17487
- target.id,
17488
- // We'd ideally use a provider method; for now do it inline
17489
- // — works for the github-codespaces provider, no-op-ish for
17490
- // others (the command will fail and we fall back gracefully).
17491
- "echo stopping"
17492
- );
17493
- void result;
17494
- await stopWorkspaceFromLocal(target);
17495
- cs.stop(`\u2713 Workspace ${target.displayName ?? target.id} is stopping`);
17496
- } catch (err) {
17497
- cs.stop("\u26A0 Could not stop the workspace");
17498
- O2.warn(err instanceof Error ? err.message : String(err));
17499
- }
17500
- }
17501
- gt(import_picocolors10.default.green("\u2713 Done."));
17502
- }
17503
- async function collectWorkspacesWithStatus() {
17504
- const out2 = [];
17505
- const ready = PROVIDERS.filter((prov) => prov.available);
17506
- for (const provider of ready) {
17507
- if (!provider.listExistingWorkspaces) continue;
17508
- const probeStep = fe();
17509
- probeStep.start(`Listing ${provider.displayName} workspaces\u2026`);
17510
- let workspaces = [];
17511
- try {
17512
- await provider.authorize();
17513
- workspaces = await provider.listExistingWorkspaces();
17514
- probeStep.stop(`\u2713 ${workspaces.length} workspace${workspaces.length === 1 ? "" : "s"} on ${provider.displayName}`);
17515
- } catch (err) {
17516
- probeStep.stop(`\u2717 Could not list ${provider.displayName} workspaces`);
17517
- O2.warn(err instanceof Error ? err.message : String(err));
17518
- continue;
17519
- }
17520
- for (const w3 of workspaces) {
17521
- const codeamRunning = await probeCodeamPair(provider, w3);
17522
- out2.push({
17523
- ...w3,
17524
- provider,
17525
- providerName: provider.displayName,
17526
- codeamRunning
17527
- });
17528
- }
17529
- }
17530
- return out2;
17531
- }
17532
- async function probeCodeamPair(provider, workspace) {
17533
- if (workspace.state && workspace.state !== "Available") return false;
17534
- try {
17535
- const result = await provider.exec(
17536
- workspace.id,
17537
- // `online` is the only state we care about — `errored`, `stopped`,
17538
- // `stopping` all mean it's not actively serving the user's phone.
17539
- `pm2 jlist 2>/dev/null | grep -c '"name":"codeam-pair"[^}]*"status":"online"' || echo 0`
17540
- );
17541
- if (result.code !== 0) return false;
17542
- const n = parseInt(result.stdout.trim(), 10);
17543
- return Number.isFinite(n) && n > 0;
17544
- } catch {
17545
- return false;
17546
- }
17547
- }
17548
- async function stopWorkspaceFromLocal(target) {
17549
- if (target.provider.id === "github-codespaces") {
17550
- const { execFile: execFile8 } = await import("child_process");
17551
- const { promisify: promisify9 } = await import("util");
17552
- const execFileP8 = promisify9(execFile8);
17553
- await execFileP8("gh", ["codespace", "stop", "-c", target.id], { maxBuffer: 8 * 1024 * 1024 });
17554
- return;
17555
- }
17556
- }
17557
-
17558
- // src/commands/link.ts
17559
- var import_node_crypto4 = require("crypto");
17560
- var fs28 = __toESM(require("fs"));
17561
- var path37 = __toESM(require("path"));
17562
- var import_chokidar = __toESM(require("chokidar"));
17563
- var import_picocolors11 = __toESM(require("picocolors"));
17564
- function buildLinkContext(agentId) {
17565
- const runtime = createRuntimeStrategy(agentId);
17566
- return {
17567
- runtime,
17568
- locator: runtime.credentialLocator(),
17569
- launcher: runtime.loginLauncher(),
17570
- displayName: runtime.meta.displayName,
17571
- binary: runtime.meta.binaryName
17572
- };
17573
- }
17574
- function enabledLinkableAgents() {
17575
- return Object.values(AGENT_REGISTRY).filter((m) => m.enabled).map((m) => m.id);
17576
- }
17577
- function parseLinkArgs(args2) {
17578
- const positional = args2.find((a) => !a.startsWith("--"));
17579
- const valid = enabledLinkableAgents();
17580
- if (!positional) {
17581
- throw new Error(
17582
- `Usage: codeam link <agent>
17583
- agent: ${valid.join(" | ")}`
17584
- );
17585
- }
17586
- const normalised = positional === "claude_code" ? "claude" : positional;
17587
- if (!isKnownAgentId(normalised) || !AGENT_REGISTRY[normalised].enabled) {
17588
- throw new Error(
17589
- `Unknown or unsupported agent "${positional}". Valid: ${valid.join(", ")}`
17590
- );
17591
- }
17592
- const reuseExisting = args2.includes("--reuse-existing");
17593
- const dryRun = args2.includes("--dry-run");
17594
- const apiKeyFileArg = args2.find((a) => a.startsWith("--api-key-file="));
17595
- let apiKey = null;
17596
- if (apiKeyFileArg) {
17597
- const filePath = apiKeyFileArg.slice("--api-key-file=".length);
17598
- try {
17599
- apiKey = fs28.readFileSync(path37.resolve(filePath), "utf8").trim();
17600
- } catch (err) {
17601
- throw new Error(`Could not read --api-key-file ${filePath}: ${err.message}`);
17602
- }
17603
- if (!apiKey) {
17604
- throw new Error(`--api-key-file ${filePath} is empty.`);
17605
- }
17606
- } else {
17607
- const apiKeyArg = args2.find((a) => a.startsWith("--api-key="));
17608
- apiKey = apiKeyArg ? apiKeyArg.slice("--api-key=".length) : null;
17609
- }
17610
- const tokenFileArg = args2.find((a) => a.startsWith("--token-file="));
17611
- const tokenFile = tokenFileArg ? tokenFileArg.slice("--token-file=".length) : null;
17612
- return { agent: normalised, reuseExisting, apiKey, tokenFile, dryRun };
17613
- }
17614
- async function link(args2 = []) {
17615
- const parsed = parseLinkArgs(args2);
17616
- const ctx = buildLinkContext(parsed.agent);
17617
- showIntro();
17618
- console.log(
17619
- import_picocolors11.default.bold(` Link ${ctx.displayName}`) + import_picocolors11.default.dim(` \xB7 ${ctx.locator.vendor}`)
17620
- );
17621
- console.log("");
17622
- if (parsed.dryRun) {
17623
- await linkDryRunPreflight(ctx);
17624
- return;
17625
- }
17626
- const pluginId = (0, import_node_crypto4.randomUUID)();
17627
- const spin = dist_exports.spinner();
17628
- spin.start("Requesting pairing code...");
17629
- const pairing = await requestCode(pluginId);
17630
- if (!pairing) {
17631
- spin.stop("Failed");
17632
- showError("Could not reach the server. Check your connection and try again.");
17633
- process.exit(1);
17634
- }
17635
- spin.stop("Got pairing code");
17636
- showPairingCode(pairing.code);
17637
- console.log(import_picocolors11.default.dim(" Scan the QR or enter the code in CodeAgent Mobile."));
17638
- console.log("");
17639
- const waitSpin = dist_exports.spinner();
17640
- const waitMsg = () => `Waiting for mobile pair... \xB7 expires in ${formatRemaining(pairing.expiresAt)}`;
17641
- waitSpin.start(waitMsg());
17642
- const countdown = setInterval(() => waitSpin.message(waitMsg()), 1e3);
17643
- countdown.unref?.();
17644
- const paired = await new Promise((resolve5, reject) => {
17645
- let stopPoll = null;
17646
- const sigint = () => {
17647
- clearInterval(countdown);
17648
- stopPoll?.();
17649
- reject(new Error("cancelled"));
17650
- };
17651
- stopPoll = pollStatus(
17652
- pluginId,
17653
- (info) => {
17654
- process.removeListener("SIGINT", sigint);
17655
- clearInterval(countdown);
17656
- waitSpin.stop("Paired");
17657
- resolve5(info);
17658
- },
17659
- () => {
17660
- clearInterval(countdown);
17661
- waitSpin.stop("Timed out");
17662
- reject(new Error("Pairing timed out after 5 minutes. Run codeam link again."));
17663
- }
17664
- );
17665
- process.once("SIGINT", sigint);
17666
- });
17667
- if (!paired.pluginAuthToken) {
17668
- showError(
17669
- "Backend did not return a pluginAuthToken \u2014 upgrade api-v2 (deploy includes the link endpoint)."
17670
- );
17671
- process.exit(1);
17672
- }
17673
- addSession({
17674
- id: paired.sessionId,
17675
- pluginId,
17676
- userName: paired.userName,
17677
- userEmail: paired.userEmail,
17678
- plan: paired.plan,
17679
- pairedAt: Date.now(),
17680
- pluginAuthToken: paired.pluginAuthToken,
17681
- agent: ctx.runtime.id
17682
- });
17683
- saveCliConfig({ ...loadCliConfig(), preferredAgent: ctx.runtime.id });
17684
- if (parsed.apiKey) {
17685
- await uploadAndSucceed(ctx, paired, pluginId, {
17686
- method: "api_key",
17687
- credential: parsed.apiKey.trim(),
17688
- source: "manual"
17689
- });
17690
- return;
17691
- }
17692
- if (parsed.tokenFile) {
17693
- const credential = fs28.readFileSync(path37.resolve(parsed.tokenFile), "utf8").trim();
17694
- if (!credential) {
17695
- showError(`--token-file ${parsed.tokenFile} is empty.`);
17696
- process.exit(1);
17697
- }
17698
- await uploadAndSucceed(ctx, paired, pluginId, {
17699
- method: "oauth",
17700
- credential,
17701
- source: "manual"
17702
- });
17703
- return;
17704
- }
17705
- const installSpin = dist_exports.spinner();
17706
- installSpin.start(`Checking that ${ctx.binary} is installed...`);
17707
- const installed = await ctx.launcher.ensureInstalled();
17708
- if (!installed) {
17709
- installSpin.stop("Failed");
17710
- showError(`Could not install ${ctx.displayName}. Install it manually then re-run.`);
17711
- process.exit(1);
17712
- }
17713
- installSpin.stop(`${ctx.displayName} is installed`);
17714
- const existing = await ctx.locator.extract();
17715
- if (existing) {
17716
- showInfo(`Found existing ${ctx.displayName} credentials at ${import_picocolors11.default.bold(existing.source)}.`);
17717
- await uploadAndSucceed(ctx, paired, pluginId, existing);
17793
+ }
17794
+
17795
+ // src/commands/deploy-manage.ts
17796
+ var import_picocolors11 = __toESM(require("picocolors"));
17797
+ async function deployList() {
17798
+ console.log();
17799
+ mt(import_picocolors11.default.bgMagenta(import_picocolors11.default.white(" codeam deploy ls ")));
17800
+ const workspaces = await collectWorkspacesWithStatus();
17801
+ if (workspaces.length === 0) {
17802
+ gt(import_picocolors11.default.dim("No deployed workspaces found."));
17718
17803
  return;
17719
17804
  }
17720
- if (parsed.reuseExisting) {
17721
- showError(
17722
- `--reuse-existing set, but no local ${ctx.displayName} credentials were found at ${ctx.locator.hint}.`
17723
- );
17724
- process.exit(1);
17805
+ for (const w3 of workspaces) {
17806
+ const tag = w3.codeamRunning ? import_picocolors11.default.green("\u25CF running") : w3.state === "Available" ? import_picocolors11.default.dim("\u25CB idle") : import_picocolors11.default.dim(`\u25CB ${w3.state ?? "stopped"}`);
17807
+ console.log(` ${tag} ${import_picocolors11.default.cyan(w3.displayName ?? w3.id)} ${import_picocolors11.default.dim("(" + w3.providerName + ")")}`);
17725
17808
  }
17726
- showInfo(
17727
- `No local ${ctx.displayName} credentials found. Launching the sign-in \u2014 complete it in your browser, the CLI will detect the new token and finish automatically.`
17728
- );
17729
- console.log("");
17730
- const captured = await captureFreshCredentials(ctx);
17731
- console.log("");
17732
- await uploadAndSucceed(ctx, paired, pluginId, captured);
17809
+ gt(import_picocolors11.default.dim("Use `codeam deploy stop` to terminate a session."));
17733
17810
  }
17734
- async function captureFreshCredentials(ctx) {
17735
- const isWin = ctx.runtime.os.id === "win32";
17736
- const watcher = import_chokidar.default.watch(ctx.locator.watchPaths(), {
17737
- persistent: true,
17738
- ignoreInitial: false,
17739
- awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 },
17740
- // Windows-only: when a credential file's ancestor is missing,
17741
- // chokidar walks up to the closest existing parent and starts
17742
- // traversing it. On a default Windows shell that ancestor is
17743
- // `C:\Users\<u>`, which contains legacy junctions whose ACL makes
17744
- // `fs.watch` throw EPERM (#43). These flags are no-ops on macOS,
17745
- // where the user has read access to their entire home.
17746
- ...isWin ? { followSymlinks: false, ignorePermissionErrors: true } : {}
17747
- });
17748
- watcher.on("error", () => {
17811
+ async function deployStop() {
17812
+ console.log();
17813
+ mt(import_picocolors11.default.bgMagenta(import_picocolors11.default.white(" codeam deploy stop ")));
17814
+ const workspaces = await collectWorkspacesWithStatus();
17815
+ if (workspaces.length === 0) {
17816
+ gt(import_picocolors11.default.dim("No deployed workspaces found."));
17817
+ return;
17818
+ }
17819
+ const choice = await _t({
17820
+ message: "Pick a workspace to stop:",
17821
+ options: workspaces.map((w3) => ({
17822
+ value: w3.id,
17823
+ label: w3.displayName ?? w3.id,
17824
+ hint: [
17825
+ w3.providerName,
17826
+ w3.codeamRunning ? import_picocolors11.default.green("\u25CF codeam-pair running") : import_picocolors11.default.dim("\u25CB no codeam-pair"),
17827
+ w3.state ?? ""
17828
+ ].filter(Boolean).join(" \xB7 ")
17829
+ }))
17749
17830
  });
17750
- let child = null;
17751
- let keychainPoll = null;
17752
- const cleanup = () => {
17753
- void watcher.close();
17754
- if (keychainPoll) clearInterval(keychainPoll);
17755
- if (child && !child.killed) {
17756
- try {
17757
- child.kill("SIGTERM");
17758
- } catch {
17759
- }
17760
- }
17761
- };
17762
- try {
17763
- const token = await new Promise((resolve5, reject) => {
17764
- let settled = false;
17765
- const tryExtract = async () => {
17766
- if (settled) return;
17767
- const t2 = await ctx.locator.extract();
17768
- if (t2 && !settled) {
17769
- settled = true;
17770
- resolve5(t2);
17771
- }
17772
- };
17773
- watcher.on("add", () => void tryExtract());
17774
- watcher.on("change", () => void tryExtract());
17775
- keychainPoll = setInterval(() => void tryExtract(), 2e3);
17776
- keychainPoll.unref?.();
17777
- const sigint = () => {
17778
- if (settled) return;
17779
- settled = true;
17780
- reject(new Error("cancelled"));
17781
- };
17782
- process.once("SIGINT", sigint);
17783
- setTimeout(() => {
17784
- if (settled) return;
17785
- settled = true;
17786
- reject(new Error(`Timed out waiting for ${ctx.displayName} sign-in (5 minutes).`));
17787
- }, 5 * 6e4);
17788
- child = ctx.launcher.launch();
17789
- child.on("exit", () => {
17790
- void tryExtract().then(() => {
17791
- if (!settled) {
17792
- settled = true;
17793
- reject(
17794
- new Error(
17795
- `${ctx.binary} exited but no credentials were written at ${ctx.locator.hint}.`
17796
- )
17797
- );
17798
- }
17799
- });
17800
- });
17801
- });
17802
- cleanup();
17803
- return token;
17804
- } catch (err) {
17805
- cleanup();
17806
- throw err;
17831
+ if (q(choice)) {
17832
+ pt("Cancelled.");
17833
+ process.exit(0);
17807
17834
  }
17808
- }
17809
- async function uploadAndSucceed(ctx, paired, pluginId, token) {
17810
- if (!paired.pluginAuthToken) {
17811
- showError("Missing pluginAuthToken; re-run codeam link.");
17812
- process.exit(1);
17835
+ const target = workspaces.find((w3) => w3.id === choice);
17836
+ if (target.codeamRunning) {
17837
+ const stopStep = fe();
17838
+ stopStep.start("Stopping codeam-pair on the workspace\u2026");
17839
+ try {
17840
+ const result = await target.provider.exec(
17841
+ target.id,
17842
+ "pm2 delete codeam-pair >/dev/null 2>&1; pm2 list 2>/dev/null | grep -c codeam-pair || true"
17843
+ );
17844
+ void result;
17845
+ stopStep.stop("\u2713 codeam-pair stopped \u2014 your phone is now disconnected from this workspace");
17846
+ } catch (err) {
17847
+ stopStep.stop("\u26A0 Could not reach the workspace to stop codeam-pair");
17848
+ void err;
17849
+ }
17850
+ } else {
17851
+ O2.info("No codeam-pair process to stop on this workspace.");
17813
17852
  }
17814
- const uploadSpin = dist_exports.spinner();
17815
- uploadSpin.start("Sealing credential in your vault...");
17816
- const result = await postLinkCredential({
17817
- agentId: ctx.locator.publicId,
17818
- sessionId: paired.sessionId,
17819
- pluginId,
17820
- pluginAuthToken: paired.pluginAuthToken,
17821
- method: token.method,
17822
- credential: token.credential
17853
+ const alsoStop = await ot2({
17854
+ message: `Also stop the workspace ${import_picocolors11.default.cyan(target.displayName ?? target.id)} to save compute hours?`,
17855
+ initialValue: true
17823
17856
  });
17824
- if (!result.ok) {
17825
- uploadSpin.stop("Failed");
17826
- if (result.status === 401) {
17827
- showError("Pair token rejected by the backend (401). Re-run `codeam link`.");
17828
- } else if (result.status === 404) {
17829
- showError(
17830
- `${ctx.displayName} link endpoint not available on this backend (404). The api-v2 deployment may not yet include the route.`
17857
+ if (!q(alsoStop) && alsoStop) {
17858
+ const cs = fe();
17859
+ cs.start("Stopping workspace\u2026");
17860
+ try {
17861
+ const result = await target.provider.exec(
17862
+ target.id,
17863
+ // We'd ideally use a provider method; for now do it inline
17864
+ // — works for the github-codespaces provider, no-op-ish for
17865
+ // others (the command will fail and we fall back gracefully).
17866
+ "echo stopping"
17831
17867
  );
17832
- } else {
17833
- showError(`Upload failed: ${result.message}`);
17868
+ void result;
17869
+ await stopWorkspaceFromLocal(target);
17870
+ cs.stop(`\u2713 Workspace ${target.displayName ?? target.id} is stopping`);
17871
+ } catch (err) {
17872
+ cs.stop("\u26A0 Could not stop the workspace");
17873
+ O2.warn(err instanceof Error ? err.message : String(err));
17834
17874
  }
17835
- process.exit(1);
17836
17875
  }
17837
- uploadSpin.stop("Linked");
17838
- console.log("");
17839
- showSuccess(`${ctx.displayName} is now linked to ${paired.userEmail || paired.userName}.`);
17840
- showInfo(
17841
- `Your codespaces and @codeagent mentions can now use ${ctx.displayName} without you signing in again.`
17842
- );
17843
- console.log("");
17876
+ gt(import_picocolors11.default.green("\u2713 Done."));
17844
17877
  }
17845
- async function linkDryRunPreflight(ctx) {
17846
- const publicId = ctx.locator.publicId;
17847
- const spin = dist_exports.spinner();
17848
- spin.start(`Probing ${publicId} link endpoint...`);
17849
- const result = await postLinkCredential({
17850
- agentId: publicId,
17851
- sessionId: "dryrun-session",
17852
- pluginId: "dryrun-plugin",
17853
- pluginAuthToken: "dryrun-token",
17854
- method: "oauth",
17855
- credential: "dryrun-credential"
17856
- });
17857
- if (result.ok) {
17858
- spin.stop("Unexpected 2xx");
17859
- showError(
17860
- "Link dry-run: backend accepted a stub credential (2xx). PluginAuthGuard appears to be disabled \u2014 investigate api-v2."
17861
- );
17862
- process.exit(1);
17878
+ async function collectWorkspacesWithStatus() {
17879
+ const out2 = [];
17880
+ const ready = PROVIDERS.filter((prov) => prov.available);
17881
+ for (const provider of ready) {
17882
+ if (!provider.listExistingWorkspaces) continue;
17883
+ const probeStep = fe();
17884
+ probeStep.start(`Listing ${provider.displayName} workspaces\u2026`);
17885
+ let workspaces = [];
17886
+ try {
17887
+ await provider.authorize();
17888
+ workspaces = await provider.listExistingWorkspaces();
17889
+ probeStep.stop(`\u2713 ${workspaces.length} workspace${workspaces.length === 1 ? "" : "s"} on ${provider.displayName}`);
17890
+ } catch (err) {
17891
+ probeStep.stop(`\u2717 Could not list ${provider.displayName} workspaces`);
17892
+ O2.warn(err instanceof Error ? err.message : String(err));
17893
+ continue;
17894
+ }
17895
+ for (const w3 of workspaces) {
17896
+ const codeamRunning = await probeCodeamPair(provider, w3);
17897
+ out2.push({
17898
+ ...w3,
17899
+ provider,
17900
+ providerName: provider.displayName,
17901
+ codeamRunning
17902
+ });
17903
+ }
17863
17904
  }
17864
- if (result.status === 401) {
17865
- spin.stop("Endpoint OK");
17866
- showSuccess(
17867
- `Link dry-run OK \u2014 /api/plugin/agents/${publicId}/link reachable and auth-gated (401 as expected).`
17905
+ return out2;
17906
+ }
17907
+ async function probeCodeamPair(provider, workspace) {
17908
+ if (workspace.state && workspace.state !== "Available") return false;
17909
+ try {
17910
+ const result = await provider.exec(
17911
+ workspace.id,
17912
+ // `online` is the only state we care about — `errored`, `stopped`,
17913
+ // `stopping` all mean it's not actively serving the user's phone.
17914
+ `pm2 jlist 2>/dev/null | grep -c '"name":"codeam-pair"[^}]*"status":"online"' || echo 0`
17868
17915
  );
17869
- process.exit(0);
17916
+ if (result.code !== 0) return false;
17917
+ const n = parseInt(result.stdout.trim(), 10);
17918
+ return Number.isFinite(n) && n > 0;
17919
+ } catch {
17920
+ return false;
17921
+ }
17922
+ }
17923
+ async function stopWorkspaceFromLocal(target) {
17924
+ if (target.provider.id === "github-codespaces") {
17925
+ const { execFile: execFile8 } = await import("child_process");
17926
+ const { promisify: promisify9 } = await import("util");
17927
+ const execFileP8 = promisify9(execFile8);
17928
+ await execFileP8("gh", ["codespace", "stop", "-c", target.id], { maxBuffer: 8 * 1024 * 1024 });
17929
+ return;
17870
17930
  }
17871
- spin.stop("Failed");
17872
- showError(
17873
- `Link dry-run: unexpected response from /api/plugin/agents/${publicId}/link (status=${result.status}, message=${result.message}). Expected 401.`
17874
- );
17875
- process.exit(1);
17876
17931
  }
17877
17932
 
17878
17933
  // src/commands/doctor.ts
@@ -18048,7 +18103,7 @@ function checkChokidar() {
18048
18103
  }
18049
18104
  async function doctor(args2 = []) {
18050
18105
  const json = args2.includes("--json");
18051
- const cliVersion = true ? "2.21.1" : "0.0.0-dev";
18106
+ const cliVersion = true ? "2.22.0" : "0.0.0-dev";
18052
18107
  const apiBase = resolveApiBaseUrl();
18053
18108
  const diagnosticId = (0, import_node_crypto5.randomUUID)();
18054
18109
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -18247,7 +18302,7 @@ async function completion(args2) {
18247
18302
  // src/commands/version.ts
18248
18303
  var import_picocolors13 = __toESM(require("picocolors"));
18249
18304
  function version2() {
18250
- const v = true ? "2.21.1" : "unknown";
18305
+ const v = true ? "2.22.0" : "unknown";
18251
18306
  console.log(`${import_picocolors13.default.bold("codeam-cli")} ${import_picocolors13.default.cyan(v)}`);
18252
18307
  }
18253
18308
 
@@ -18475,7 +18530,7 @@ function checkForUpdates() {
18475
18530
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
18476
18531
  if (process.env.CI) return;
18477
18532
  if (!process.stdout.isTTY) return;
18478
- const current = true ? "2.21.1" : null;
18533
+ const current = true ? "2.22.0" : null;
18479
18534
  if (!current) return;
18480
18535
  const cache = readCache();
18481
18536
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;