codeam-cli 2.21.2 → 2.22.1

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 +12 -0
  2. package/dist/index.js +812 -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.2",
444
+ version: "2.22.1",
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.2" : "0.0.0-dev",
5743
+ cliVersion: true ? "2.22.1" : "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,16 @@ 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. Also reused by `get_context` /
13821
+ // `list_models` payloads which carry the currently-selected
13822
+ // agent for that session. We accept any string here (rather than
13823
+ // a narrow enum) because the web sends internal ids ('claude')
13824
+ // while the auto-link backend sends public ids ('claude_code'),
13825
+ // and the consuming handlers normalize themselves.
13826
+ agentId: import_zod2.z.string().max(64).optional()
13817
13827
  });
13818
13828
  function parsePayload2(schema, raw) {
13819
13829
  const result = schema.safeParse(raw);
@@ -14653,240 +14663,582 @@ function findGitRoot2(startDir) {
14653
14663
  return null;
14654
14664
  }
14655
14665
 
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();
14666
+ // src/commands/link.ts
14667
+ var import_node_crypto4 = require("crypto");
14668
+ var fs26 = __toESM(require("fs"));
14669
+ var path32 = __toESM(require("path"));
14670
+ var import_chokidar = __toESM(require("chokidar"));
14671
+ var import_picocolors2 = __toESM(require("picocolors"));
14672
+
14673
+ // src/ui/prompts.ts
14674
+ async function confirmAction(message) {
14675
+ const result = await ot2({ message });
14676
+ if (q(result)) return false;
14677
+ return result;
14666
14678
  }
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;
14679
+ async function selectSession(sessions3, activeId) {
14680
+ const result = await _t({
14681
+ message: "Select active session:",
14682
+ options: sessions3.map((s) => ({
14683
+ value: s.id,
14684
+ label: `${s.userName} ${s.plan}`,
14685
+ hint: s.id === activeId ? "active" : `paired ${new Date(s.pairedAt).toLocaleDateString()}`
14686
+ })),
14687
+ initialValue: activeId ?? void 0
14674
14688
  });
14689
+ if (q(result)) return null;
14690
+ return result;
14675
14691
  }
14676
- function dispatchPrompt(ctx, prompt) {
14677
- ctx.outputSvc.newTurn();
14678
- ctx.agent.sendCommand(prompt);
14692
+
14693
+ // src/commands/link.ts
14694
+ function buildLinkContext(agentId) {
14695
+ const runtime = createRuntimeStrategy(agentId);
14696
+ return {
14697
+ runtime,
14698
+ locator: runtime.credentialLocator(),
14699
+ launcher: runtime.loginLauncher(),
14700
+ displayName: runtime.meta.displayName,
14701
+ binary: runtime.meta.binaryName
14702
+ };
14679
14703
  }
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);
14704
+ function enabledLinkableAgents() {
14705
+ return Object.values(AGENT_REGISTRY).filter((m) => m.enabled).map((m) => m.id);
14706
+ }
14707
+ function parseLinkArgs(args2) {
14708
+ const positional = args2.find((a) => !a.startsWith("--"));
14709
+ const valid = enabledLinkableAgents();
14710
+ if (!positional) {
14711
+ throw new Error(
14712
+ `Usage: codeam link <agent>
14713
+ agent: ${valid.join(" | ")}`
14714
+ );
14699
14715
  }
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;
14716
+ const normalised = positional === "claude_code" ? "claude" : positional;
14717
+ if (!isKnownAgentId(normalised) || !AGENT_REGISTRY[normalised].enabled) {
14718
+ throw new Error(
14719
+ `Unknown or unsupported agent "${positional}". Valid: ${valid.join(", ")}`
14720
+ );
14743
14721
  }
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", {});
14722
+ const reuseExisting = args2.includes("--reuse-existing");
14723
+ const dryRun = args2.includes("--dry-run");
14724
+ const apiKeyFileArg = args2.find((a) => a.startsWith("--api-key-file="));
14725
+ let apiKey = null;
14726
+ if (apiKeyFileArg) {
14727
+ const filePath = apiKeyFileArg.slice("--api-key-file=".length);
14728
+ try {
14729
+ apiKey = fs26.readFileSync(path32.resolve(filePath), "utf8").trim();
14730
+ } catch (err) {
14731
+ throw new Error(`Could not read --api-key-file ${filePath}: ${err.message}`);
14732
+ }
14733
+ if (!apiKey) {
14734
+ throw new Error(`--api-key-file ${filePath} is empty.`);
14735
+ }
14736
+ } else {
14737
+ const apiKeyArg = args2.find((a) => a.startsWith("--api-key="));
14738
+ apiKey = apiKeyArg ? apiKeyArg.slice("--api-key=".length) : null;
14749
14739
  }
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" });
14740
+ const tokenFileArg = args2.find((a) => a.startsWith("--token-file="));
14741
+ const tokenFile = tokenFileArg ? tokenFileArg.slice("--token-file=".length) : null;
14742
+ return { agent: normalised, reuseExisting, apiKey, tokenFile, dryRun };
14743
+ }
14744
+ async function link(args2 = []) {
14745
+ const parsed = parseLinkArgs(args2);
14746
+ const ctx = buildLinkContext(parsed.agent);
14747
+ showIntro();
14748
+ console.log(
14749
+ import_picocolors2.default.bold(` Link ${ctx.displayName}`) + import_picocolors2.default.dim(` \xB7 ${ctx.locator.vendor}`)
14750
+ );
14751
+ console.log("");
14752
+ if (parsed.dryRun) {
14753
+ await linkDryRunPreflight(ctx);
14759
14754
  return;
14760
14755
  }
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;
14756
+ const pluginId = (0, import_node_crypto4.randomUUID)();
14757
+ const spin = dist_exports.spinner();
14758
+ spin.start("Requesting pairing code...");
14759
+ const pairing = await requestCode(pluginId);
14760
+ if (!pairing) {
14761
+ spin.stop("Failed");
14762
+ showError("Could not reach the server. Check your connection and try again.");
14763
+ process.exit(1);
14771
14764
  }
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"
14765
+ spin.stop("Got pairing code");
14766
+ showPairingCode(pairing.code);
14767
+ console.log(import_picocolors2.default.dim(" Scan the QR or enter the code in CodeAgent Mobile."));
14768
+ console.log("");
14769
+ const waitSpin = dist_exports.spinner();
14770
+ const waitMsg = () => `Waiting for mobile pair... \xB7 expires in ${formatRemaining(pairing.expiresAt)}`;
14771
+ waitSpin.start(waitMsg());
14772
+ const countdown = setInterval(() => waitSpin.message(waitMsg()), 1e3);
14773
+ countdown.unref?.();
14774
+ const paired = await new Promise((resolve5, reject) => {
14775
+ let stopPoll = null;
14776
+ const sigint = () => {
14777
+ clearInterval(countdown);
14778
+ stopPoll?.();
14779
+ reject(new Error("cancelled"));
14780
+ };
14781
+ stopPoll = pollStatus(
14782
+ pluginId,
14783
+ (info) => {
14784
+ process.removeListener("SIGINT", sigint);
14785
+ clearInterval(countdown);
14786
+ waitSpin.stop("Paired");
14787
+ resolve5(info);
14788
+ },
14789
+ () => {
14790
+ clearInterval(countdown);
14791
+ waitSpin.stop("Timed out");
14792
+ reject(new Error("Pairing timed out after 5 minutes. Run codeam link again."));
14792
14793
  }
14793
14794
  );
14794
- } catch {
14795
+ process.once("SIGINT", sigint);
14796
+ });
14797
+ if (!paired.pluginAuthToken) {
14798
+ showError(
14799
+ "Backend did not return a pluginAuthToken \u2014 upgrade api-v2 (deploy includes the link endpoint)."
14800
+ );
14801
+ process.exit(1);
14795
14802
  }
14796
- };
14797
- var sessionTerminated = (ctx) => {
14798
- showInfo("Session was deleted from the app \u2014 exiting.");
14799
- try {
14800
- ctx.agent.kill();
14801
- } catch {
14803
+ addSession({
14804
+ id: paired.sessionId,
14805
+ pluginId,
14806
+ userName: paired.userName,
14807
+ userEmail: paired.userEmail,
14808
+ plan: paired.plan,
14809
+ pairedAt: Date.now(),
14810
+ pluginAuthToken: paired.pluginAuthToken,
14811
+ agent: ctx.runtime.id
14812
+ });
14813
+ saveCliConfig({ ...loadCliConfig(), preferredAgent: ctx.runtime.id });
14814
+ if (parsed.apiKey) {
14815
+ await uploadAndSucceed(ctx, paired, pluginId, {
14816
+ method: "api_key",
14817
+ credential: parsed.apiKey.trim(),
14818
+ source: "manual"
14819
+ });
14820
+ return;
14802
14821
  }
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"
14822
+ if (parsed.tokenFile) {
14823
+ const credential = fs26.readFileSync(path32.resolve(parsed.tokenFile), "utf8").trim();
14824
+ if (!credential) {
14825
+ showError(`--token-file ${parsed.tokenFile} is empty.`);
14826
+ process.exit(1);
14827
+ }
14828
+ await uploadAndSucceed(ctx, paired, pluginId, {
14829
+ method: "oauth",
14830
+ credential,
14831
+ source: "manual"
14807
14832
  });
14808
- proc.unref();
14809
- } catch {
14833
+ return;
14810
14834
  }
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 {
14835
+ const installSpin = dist_exports.spinner();
14836
+ installSpin.start(`Checking that ${ctx.binary} is installed...`);
14837
+ const installed = await ctx.launcher.ensureInstalled();
14838
+ if (!installed) {
14839
+ installSpin.stop("Failed");
14840
+ showError(`Could not install ${ctx.displayName}. Install it manually then re-run.`);
14841
+ process.exit(1);
14819
14842
  }
14820
- try {
14821
- ctx.agent.kill();
14822
- } catch {
14843
+ installSpin.stop(`${ctx.displayName} is installed`);
14844
+ const existing = await ctx.locator.extract();
14845
+ if (existing) {
14846
+ showInfo(`Found existing ${ctx.displayName} credentials at ${import_picocolors2.default.bold(existing.source)}.`);
14847
+ await uploadAndSucceed(ctx, paired, pluginId, existing);
14848
+ return;
14823
14849
  }
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
- }
14850
+ if (parsed.reuseExisting) {
14851
+ showError(
14852
+ `--reuse-existing set, but no local ${ctx.displayName} credentials were found at ${ctx.locator.hint}.`
14853
+ );
14854
+ process.exit(1);
14834
14855
  }
14856
+ showInfo(
14857
+ `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.`
14858
+ );
14859
+ console.log("");
14860
+ const captured = await captureFreshCredentials(ctx);
14861
+ console.log("");
14862
+ await uploadAndSucceed(ctx, paired, pluginId, captured);
14863
+ }
14864
+ async function captureFreshCredentials(ctx) {
14865
+ const isWin = ctx.runtime.os.id === "win32";
14866
+ const watcher = import_chokidar.default.watch(ctx.locator.watchPaths(), {
14867
+ persistent: true,
14868
+ ignoreInitial: false,
14869
+ awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 },
14870
+ // Windows-only: when a credential file's ancestor is missing,
14871
+ // chokidar walks up to the closest existing parent and starts
14872
+ // traversing it. On a default Windows shell that ancestor is
14873
+ // `C:\Users\<u>`, which contains legacy junctions whose ACL makes
14874
+ // `fs.watch` throw EPERM (#43). These flags are no-ops on macOS,
14875
+ // where the user has read access to their entire home.
14876
+ ...isWin ? { followSymlinks: false, ignorePermissionErrors: true } : {}
14877
+ });
14878
+ watcher.on("error", () => {
14879
+ });
14880
+ let child = null;
14881
+ let keychainPoll = null;
14882
+ const cleanup = () => {
14883
+ void watcher.close();
14884
+ if (keychainPoll) clearInterval(keychainPoll);
14885
+ if (child && !child.killed) {
14886
+ try {
14887
+ child.kill("SIGTERM");
14888
+ } catch {
14889
+ }
14890
+ }
14891
+ };
14835
14892
  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"
14893
+ const token = await new Promise((resolve5, reject) => {
14894
+ let settled = false;
14895
+ const tryExtract = async () => {
14896
+ if (settled) return;
14897
+ const t2 = await ctx.locator.extract();
14898
+ if (t2 && !settled) {
14899
+ settled = true;
14900
+ resolve5(t2);
14901
+ }
14902
+ };
14903
+ watcher.on("add", () => void tryExtract());
14904
+ watcher.on("change", () => void tryExtract());
14905
+ keychainPoll = setInterval(() => void tryExtract(), 2e3);
14906
+ keychainPoll.unref?.();
14907
+ const sigint = () => {
14908
+ if (settled) return;
14909
+ settled = true;
14910
+ reject(new Error("cancelled"));
14911
+ };
14912
+ process.once("SIGINT", sigint);
14913
+ setTimeout(() => {
14914
+ if (settled) return;
14915
+ settled = true;
14916
+ reject(new Error(`Timed out waiting for ${ctx.displayName} sign-in (5 minutes).`));
14917
+ }, 5 * 6e4);
14918
+ child = ctx.launcher.launch();
14919
+ child.on("exit", () => {
14920
+ void tryExtract().then(() => {
14921
+ if (!settled) {
14922
+ settled = true;
14923
+ reject(
14924
+ new Error(
14925
+ `${ctx.binary} exited but no credentials were written at ${ctx.locator.hint}.`
14926
+ )
14927
+ );
14928
+ }
14929
+ });
14930
+ });
14839
14931
  });
14840
- proc.unref();
14841
- } catch {
14932
+ cleanup();
14933
+ return token;
14934
+ } catch (err) {
14935
+ cleanup();
14936
+ throw err;
14842
14937
  }
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;
14938
+ }
14939
+ async function uploadAndSucceed(ctx, paired, pluginId, token) {
14940
+ if (!paired.pluginAuthToken) {
14941
+ showError("Missing pluginAuthToken; re-run codeam link.");
14942
+ process.exit(1);
14851
14943
  }
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;
14944
+ const uploadSpin = dist_exports.spinner();
14945
+ uploadSpin.start("Sealing credential in your vault...");
14946
+ const result = await postLinkCredential({
14947
+ agentId: ctx.locator.publicId,
14948
+ sessionId: paired.sessionId,
14949
+ pluginId,
14950
+ pluginAuthToken: paired.pluginAuthToken,
14951
+ method: token.method,
14952
+ credential: token.credential
14953
+ });
14954
+ if (!result.ok) {
14955
+ uploadSpin.stop("Failed");
14956
+ if (result.status === 401) {
14957
+ showError("Pair token rejected by the backend (401). Re-run `codeam link`.");
14958
+ } else if (result.status === 404) {
14959
+ showError(
14960
+ `${ctx.displayName} link endpoint not available on this backend (404). The api-v2 deployment may not yet include the route.`
14961
+ );
14962
+ } else {
14963
+ showError(`Upload failed: ${result.message}`);
14964
+ }
14965
+ process.exit(1);
14859
14966
  }
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
14967
+ uploadSpin.stop("Linked");
14968
+ console.log("");
14969
+ showSuccess(`${ctx.displayName} is now linked to ${paired.userEmail || paired.userName}.`);
14970
+ showInfo(
14971
+ `Your codespaces and @codeagent mentions can now use ${ctx.displayName} without you signing in again.`
14972
+ );
14973
+ console.log("");
14974
+ }
14975
+ async function linkDryRunPreflight(ctx) {
14976
+ const publicId = ctx.locator.publicId;
14977
+ const spin = dist_exports.spinner();
14978
+ spin.start(`Probing ${publicId} link endpoint...`);
14979
+ const result = await postLinkCredential({
14980
+ agentId: publicId,
14981
+ sessionId: "dryrun-session",
14982
+ pluginId: "dryrun-plugin",
14983
+ pluginAuthToken: "dryrun-token",
14984
+ method: "oauth",
14985
+ credential: "dryrun-credential"
14872
14986
  });
14873
- if ("error" in r) {
14874
- await ctx.relay.sendResult(cmd.id, "failed", { error: r.error });
14875
- return;
14987
+ if (result.ok) {
14988
+ spin.stop("Unexpected 2xx");
14989
+ showError(
14990
+ "Link dry-run: backend accepted a stub credential (2xx). PluginAuthGuard appears to be disabled \u2014 investigate api-v2."
14991
+ );
14992
+ process.exit(1);
14876
14993
  }
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;
14994
+ if (result.status === 401) {
14995
+ spin.stop("Endpoint OK");
14996
+ showSuccess(
14997
+ `Link dry-run OK \u2014 /api/plugin/agents/${publicId}/link reachable and auth-gated (401 as expected).`
14998
+ );
14999
+ process.exit(0);
14883
15000
  }
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" });
15001
+ spin.stop("Failed");
15002
+ showError(
15003
+ `Link dry-run: unexpected response from /api/plugin/agents/${publicId}/link (status=${result.status}, message=${result.message}). Expected 401.`
15004
+ );
15005
+ process.exit(1);
15006
+ }
15007
+
15008
+ // src/commands/start/handlers.ts
15009
+ var pendingAttachmentFiles = /* @__PURE__ */ new Set();
15010
+ function cleanupAttachmentTempFiles() {
15011
+ for (const p2 of pendingAttachmentFiles) {
15012
+ try {
15013
+ fs27.unlinkSync(p2);
15014
+ } catch {
15015
+ }
15016
+ }
15017
+ pendingAttachmentFiles.clear();
15018
+ }
15019
+ function saveFilesTemp(files) {
15020
+ return files.filter(({ base64 }) => base64 && base64.length > 0).map(({ filename, base64 }) => {
15021
+ const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
15022
+ const tmpPath = path33.join(os23.tmpdir(), `codeam-${(0, import_crypto5.randomUUID)()}-${safeName}`);
15023
+ fs27.writeFileSync(tmpPath, Buffer.from(base64, "base64"));
15024
+ pendingAttachmentFiles.add(tmpPath);
15025
+ return tmpPath;
15026
+ });
15027
+ }
15028
+ function dispatchPrompt(ctx, prompt) {
15029
+ ctx.outputSvc.newTurn();
15030
+ ctx.agent.sendCommand(prompt);
15031
+ }
15032
+ var startTask = (ctx, _cmd, parsed) => {
15033
+ const { prompt, files } = parsed;
15034
+ const effectivePrompt = prompt ?? "";
15035
+ if (files && files.length > 0) {
15036
+ const paths = saveFilesTemp(files);
15037
+ const atRefs = paths.map((p2) => `@${p2}`).join(" ");
15038
+ ctx.outputSvc.newTurn();
15039
+ ctx.agent.sendCommand(`${atRefs} ${effectivePrompt}`.trim());
15040
+ setTimeout(() => {
15041
+ for (const p2 of paths) {
15042
+ try {
15043
+ fs27.unlinkSync(p2);
15044
+ } catch {
15045
+ }
15046
+ pendingAttachmentFiles.delete(p2);
15047
+ }
15048
+ }, 12e4);
15049
+ } else if (effectivePrompt) {
15050
+ dispatchPrompt(ctx, effectivePrompt);
15051
+ }
15052
+ };
15053
+ var provideInput = (ctx, _cmd, parsed) => {
15054
+ if (parsed.input) dispatchPrompt(ctx, parsed.input);
15055
+ };
15056
+ var selectOption = (ctx, _cmd, parsed) => {
15057
+ const index = parsed.index ?? 0;
15058
+ const from = parsed.from ?? 0;
15059
+ ctx.outputSvc.newTurn();
15060
+ ctx.agent.selectOption(index, from);
15061
+ };
15062
+ var escapeKey = (ctx) => {
15063
+ ctx.outputSvc.newTurn();
15064
+ ctx.agent.sendEscape();
15065
+ };
15066
+ var stopTask = (ctx) => {
15067
+ ctx.agent.interrupt();
15068
+ };
15069
+ var resumeSession = async (ctx, _cmd, parsed) => {
15070
+ const { id, auto } = parsed;
15071
+ if (!id) return;
15072
+ ctx.historySvc.setCurrentConversationId(id);
15073
+ await ctx.historySvc.loadConversation(id);
15074
+ await ctx.outputSvc.newTurnResume(id);
15075
+ ctx.agent.restart(id, auto ?? false);
15076
+ };
15077
+ var getContext = async (ctx, cmd) => {
15078
+ const usage = ctx.historySvc.getCurrentUsage();
15079
+ const monthlyCost = ctx.historySvc.getMonthlyEstimatedCost();
15080
+ const rateLimitReset = ctx.historySvc.getRateLimitReset();
15081
+ const quotaPercent = ctx.historySvc.getQuotaPercent();
15082
+ const base = usage ? { ...usage, monthlyCost } : { used: 0, total: 2e5, percent: 0, model: null, outputTokens: 0, cacheReadTokens: 0, monthlyCost, error: "No usage data found" };
15083
+ const result = {
15084
+ ...base,
15085
+ ...rateLimitReset ? { rateLimitReset } : {},
15086
+ ...quotaPercent !== null ? { quotaPercent } : {}
15087
+ };
15088
+ await ctx.relay.sendResult(cmd.id, "completed", result);
15089
+ };
15090
+ var getConversation = async (ctx, cmd) => {
15091
+ const currentId = ctx.historySvc.getCurrentConversationId();
15092
+ if (!currentId) {
15093
+ await ctx.relay.sendResult(cmd.id, "completed", { conversationId: null });
15094
+ return;
15095
+ }
15096
+ try {
15097
+ await ctx.historySvc.loadConversation(currentId);
15098
+ await ctx.relay.sendResult(cmd.id, "completed", { conversationId: currentId });
15099
+ } catch {
15100
+ await ctx.relay.sendResult(cmd.id, "failed", {});
15101
+ }
15102
+ };
15103
+ var listModels = async (ctx, cmd) => {
15104
+ const models = await ctx.runtime.listModels();
15105
+ await ctx.relay.sendResult(cmd.id, "completed", { models });
15106
+ };
15107
+ var changeModel = async (ctx, cmd) => {
15108
+ const params = cmd.payload;
15109
+ if (typeof params.modelId !== "string" || !params.modelId) {
15110
+ await ctx.relay.sendResult(cmd.id, "failed", { error: "modelId required" });
15111
+ return;
15112
+ }
15113
+ const instr = ctx.runtime.changeModelInstruction(params.modelId);
15114
+ if (instr.type === "pty") {
15115
+ if (!instr.ptyInput) {
15116
+ await ctx.relay.sendResult(cmd.id, "failed", { error: "no pty input for this agent" });
15117
+ return;
15118
+ }
15119
+ ctx.agent.sendRawPtyInput(instr.ptyInput);
15120
+ } else if (instr.type === "restart") {
15121
+ await ctx.relay.sendResult(cmd.id, "failed", { error: "restart-mode change_model not supported in Phase 1" });
15122
+ return;
15123
+ }
15124
+ await ctx.relay.sendResult(cmd.id, "completed", {});
15125
+ };
15126
+ var summarize = async (ctx, cmd) => {
15127
+ const params = cmd.payload;
15128
+ const mode = params.mode === "auto" ? "auto" : "normal";
15129
+ const instr = ctx.runtime.summarizeInstruction(mode);
15130
+ ctx.agent.sendRawPtyInput(instr.ptyInput);
15131
+ await ctx.relay.sendResult(cmd.id, "completed", {});
15132
+ };
15133
+ var setKeepAlive = async (ctx, cmd) => {
15134
+ const enabled = !!cmd.payload.enabled;
15135
+ ctx.setKeepAlive(enabled);
15136
+ try {
15137
+ await ctx.relay.sendResult(
15138
+ cmd.id,
15139
+ "success",
15140
+ {
15141
+ enabled,
15142
+ applied: enabled && ctx.keepAliveCtx.inCodespace,
15143
+ runtime: ctx.keepAliveCtx.inCodespace ? "github-codespaces" : "local"
15144
+ }
15145
+ );
15146
+ } catch {
15147
+ }
15148
+ };
15149
+ var sessionTerminated = (ctx) => {
15150
+ showInfo("Session was deleted from the app \u2014 exiting.");
15151
+ try {
15152
+ ctx.agent.kill();
15153
+ } catch {
15154
+ }
15155
+ try {
15156
+ const proc = (0, import_child_process13.spawn)("bash", ["-lc", "pm2 delete codeam-pair >/dev/null 2>&1 || true"], {
15157
+ detached: true,
15158
+ stdio: "ignore"
15159
+ });
15160
+ proc.unref();
15161
+ } catch {
15162
+ }
15163
+ ctx.outputSvc.dispose();
15164
+ ctx.relay.stop();
15165
+ process.exit(0);
15166
+ };
15167
+ var shutdownSession = async (ctx, cmd) => {
15168
+ try {
15169
+ await ctx.relay.sendResult(cmd.id, "success", { ok: true });
15170
+ } catch {
15171
+ }
15172
+ try {
15173
+ ctx.agent.kill();
15174
+ } catch {
15175
+ }
15176
+ if (ctx.keepAliveCtx.inCodespace && ctx.keepAliveCtx.codespaceName) {
15177
+ try {
15178
+ const stopProc = (0, import_child_process13.spawn)(
15179
+ "bash",
15180
+ ["-lc", `sleep 1; gh codespace stop -c ${JSON.stringify(ctx.keepAliveCtx.codespaceName)} >/dev/null 2>&1 || true`],
15181
+ { detached: true, stdio: "ignore" }
15182
+ );
15183
+ stopProc.unref();
15184
+ } catch {
15185
+ }
15186
+ }
15187
+ try {
15188
+ const proc = (0, import_child_process13.spawn)("bash", ["-lc", "pm2 delete codeam-pair >/dev/null 2>&1 || true"], {
15189
+ detached: true,
15190
+ stdio: "ignore"
15191
+ });
15192
+ proc.unref();
15193
+ } catch {
15194
+ }
15195
+ ctx.outputSvc.dispose();
15196
+ ctx.relay.stop();
15197
+ process.exit(0);
15198
+ };
15199
+ var readFile4 = async (ctx, cmd, parsed) => {
15200
+ if (!parsed.path) {
15201
+ await ctx.relay.sendResult(cmd.id, "failed", { error: "Missing path" });
15202
+ return;
15203
+ }
15204
+ const result = await readProjectFile(parsed.path);
15205
+ await ctx.relay.sendResult(cmd.id, "completed", result);
15206
+ };
15207
+ var writeFile3 = async (ctx, cmd, parsed) => {
15208
+ if (!parsed.path || typeof parsed.content !== "string") {
15209
+ await ctx.relay.sendResult(cmd.id, "failed", { error: "Missing path or content" });
15210
+ return;
15211
+ }
15212
+ const result = await writeProjectFile(parsed.path, parsed.content);
15213
+ await ctx.relay.sendResult(cmd.id, "completed", result);
15214
+ };
15215
+ var listFiles = async (ctx, cmd, parsed) => {
15216
+ const result = await listProjectFiles({ query: parsed.query });
15217
+ await ctx.relay.sendResult(cmd.id, "completed", result);
15218
+ };
15219
+ var terminalOpenH = async (ctx, cmd, parsed) => {
15220
+ const r = openTerminal({
15221
+ cols: typeof parsed.cols === "number" ? parsed.cols : void 0,
15222
+ rows: typeof parsed.rows === "number" ? parsed.rows : void 0,
15223
+ cwd: typeof parsed.cwd === "string" ? parsed.cwd : void 0
15224
+ });
15225
+ if ("error" in r) {
15226
+ await ctx.relay.sendResult(cmd.id, "failed", { error: r.error });
15227
+ return;
15228
+ }
15229
+ await ctx.relay.sendResult(cmd.id, "completed", r);
15230
+ };
15231
+ var terminalWriteH = async (ctx, cmd, parsed) => {
15232
+ if (typeof parsed.sessionId !== "string" || typeof parsed.data !== "string") {
15233
+ await ctx.relay.sendResult(cmd.id, "failed", { error: "Missing sessionId or data" });
15234
+ return;
15235
+ }
15236
+ const r = writeTerminal(parsed.sessionId, parsed.data);
15237
+ await ctx.relay.sendResult(cmd.id, r.ok ? "completed" : "failed", r);
15238
+ };
15239
+ var terminalResizeH = async (ctx, cmd, parsed) => {
15240
+ if (typeof parsed.sessionId !== "string" || typeof parsed.cols !== "number" || typeof parsed.rows !== "number") {
15241
+ await ctx.relay.sendResult(cmd.id, "failed", { error: "Missing sessionId / cols / rows" });
14890
15242
  return;
14891
15243
  }
14892
15244
  const r = resizeTerminal(parsed.sessionId, parsed.cols, parsed.rows);
@@ -14974,25 +15326,66 @@ var applyFileReviewH = async (ctx, cmd, parsed) => {
14974
15326
  result
14975
15327
  );
14976
15328
  };
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,
15329
+ var requestLinkCredentialsH = async (ctx, _cmd, parsed) => {
15330
+ const publicId = parsed.agentId;
15331
+ if (!publicId) return;
15332
+ if (!ctx.pluginAuthToken) {
15333
+ log.trace("auto-link", "skipped \u2014 no pluginAuthToken on this paired session");
15334
+ return;
15335
+ }
15336
+ const internalId = publicId === "claude_code" ? "claude" : publicId;
15337
+ if (!isKnownAgentId(internalId) || !AGENT_REGISTRY[internalId].enabled) {
15338
+ log.trace("auto-link", `unknown / disabled agent: ${internalId}`);
15339
+ return;
15340
+ }
15341
+ let linkCtx;
15342
+ try {
15343
+ linkCtx = buildLinkContext(internalId);
15344
+ } catch (err) {
15345
+ log.trace("auto-link", "buildLinkContext threw", err);
15346
+ return;
15347
+ }
15348
+ const token = await linkCtx.locator.extract().catch((err) => {
15349
+ log.trace("auto-link", `locator.extract failed for ${publicId}`, err);
15350
+ return null;
15351
+ });
15352
+ if (!token) {
15353
+ log.trace("auto-link", `no local ${linkCtx.displayName} credentials \u2014 skipping`);
15354
+ return;
15355
+ }
15356
+ const result = await postLinkCredential({
15357
+ agentId: publicId,
15358
+ sessionId: ctx.sessionId,
15359
+ pluginId: ctx.pluginId,
15360
+ pluginAuthToken: ctx.pluginAuthToken,
15361
+ method: token.method,
15362
+ credential: token.credential
15363
+ });
15364
+ if (result.ok) {
15365
+ log.trace("auto-link", `vaulted ${publicId} from ${token.source}`);
15366
+ } else {
15367
+ log.trace("auto-link", `upload failed (${result.status}): ${result.message}`);
15368
+ }
15369
+ };
15370
+ var handlers = {
15371
+ start_task: startTask,
15372
+ provide_input: provideInput,
15373
+ select_option: selectOption,
15374
+ escape_key: escapeKey,
15375
+ stop_task: stopTask,
15376
+ resume_session: resumeSession,
15377
+ get_context: getContext,
15378
+ get_conversation: getConversation,
15379
+ list_models: listModels,
15380
+ change_model: changeModel,
15381
+ summarize,
15382
+ set_keep_alive: setKeepAlive,
15383
+ session_terminated: sessionTerminated,
15384
+ shutdown_session: shutdownSession,
15385
+ read_file: readFile4,
15386
+ write_file: writeFile3,
15387
+ list_files: listFiles,
15388
+ search_files: searchFilesH,
14996
15389
  terminal_open: terminalOpenH,
14997
15390
  terminal_write: terminalWriteH,
14998
15391
  terminal_resize: terminalResizeH,
@@ -15005,7 +15398,8 @@ var handlers = {
15005
15398
  git_push: gitPushH,
15006
15399
  git_pull: gitPullH,
15007
15400
  git_resolve: gitResolveH,
15008
- apply_file_review: applyFileReviewH
15401
+ apply_file_review: applyFileReviewH,
15402
+ request_link_credentials: requestLinkCredentialsH
15009
15403
  };
15010
15404
  async function dispatchCommand(ctx, cmd) {
15011
15405
  const parsed = parsePayload2(startCommandSchema, cmd.payload);
@@ -15025,14 +15419,14 @@ async function start(requestedAgent) {
15025
15419
  if (!session) {
15026
15420
  if (requestedAgent) {
15027
15421
  const displayName = AGENT_REGISTRY[requestedAgent]?.displayName ?? requestedAgent;
15028
- console.log(` ${import_picocolors2.default.dim(`No paired ${displayName} session found.`)}`);
15422
+ console.log(` ${import_picocolors3.default.dim(`No paired ${displayName} session found.`)}`);
15029
15423
  console.log(
15030
- ` ${import_picocolors2.default.dim(`Run ${import_picocolors2.default.white("codeam pair")} from a ${displayName} setup to connect your mobile app.`)}
15424
+ ` ${import_picocolors3.default.dim(`Run ${import_picocolors3.default.white("codeam pair")} from a ${displayName} setup to connect your mobile app.`)}
15031
15425
  `
15032
15426
  );
15033
15427
  } 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.`)}
15428
+ console.log(` ${import_picocolors3.default.dim("No paired session found.")}`);
15429
+ console.log(` ${import_picocolors3.default.dim(`Run ${import_picocolors3.default.white("codeam pair")} to connect your mobile app.`)}
15036
15430
  `);
15037
15431
  }
15038
15432
  process.exit(0);
@@ -15041,7 +15435,7 @@ async function start(requestedAgent) {
15041
15435
  throw new Error("Active session has no agent \u2014 re-pair with `codeam pair`.");
15042
15436
  }
15043
15437
  const pluginId = session.pluginId ?? ensurePluginId();
15044
- showInfo(`${session.userName} \xB7 ${import_picocolors2.default.cyan(session.plan)}`);
15438
+ showInfo(`${session.userName} \xB7 ${import_picocolors3.default.cyan(session.plan)}`);
15045
15439
  showInfo(`Launching ${AGENT_REGISTRY[session.agent].displayName}...
15046
15440
  `);
15047
15441
  identifyUser({
@@ -15142,7 +15536,10 @@ async function start(requestedAgent) {
15142
15536
  runtime,
15143
15537
  relay: void 0,
15144
15538
  setKeepAlive: setKeepAlive2,
15145
- keepAliveCtx
15539
+ keepAliveCtx,
15540
+ pluginId,
15541
+ sessionId: session.id,
15542
+ pluginAuthToken: session.pluginAuthToken ?? void 0
15146
15543
  };
15147
15544
  const relay = new CommandRelayService(pluginId, async (cmd) => {
15148
15545
  await dispatchCommand(ctx, cmd);
@@ -15187,27 +15584,7 @@ async function start(requestedAgent) {
15187
15584
 
15188
15585
  // src/commands/pair.ts
15189
15586
  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
- }
15587
+ var import_picocolors4 = __toESM(require("picocolors"));
15211
15588
 
15212
15589
  // src/utils/agent-prompt.ts
15213
15590
  function parseAgentFlag(args2) {
@@ -15276,7 +15653,7 @@ async function pair(args2 = []) {
15276
15653
  process.exit(0);
15277
15654
  }
15278
15655
  showPairingCode(result.code);
15279
- console.log(import_picocolors3.default.dim(" Scan the QR code or enter the code in CodeAgent Mobile."));
15656
+ console.log(import_picocolors4.default.dim(" Scan the QR code or enter the code in CodeAgent Mobile."));
15280
15657
  console.log("");
15281
15658
  const waitSpin = dist_exports.spinner();
15282
15659
  const waitMessage = () => `Waiting for mobile app... \xB7 expires in ${formatRemaining(result.expiresAt)}`;
@@ -15341,7 +15718,7 @@ async function pair(args2 = []) {
15341
15718
  }
15342
15719
 
15343
15720
  // src/commands/pair-auto.ts
15344
- var fs27 = __toESM(require("fs"));
15721
+ var fs28 = __toESM(require("fs"));
15345
15722
  var os24 = __toESM(require("os"));
15346
15723
  var import_crypto7 = require("crypto");
15347
15724
  var API_BASE8 = resolveApiBaseUrl();
@@ -15361,10 +15738,10 @@ function readTokenFromArgs(args2) {
15361
15738
  if (fileFlag) {
15362
15739
  const path40 = fileFlag.slice("--token-file=".length);
15363
15740
  try {
15364
- const content = fs27.readFileSync(path40, "utf8").trim();
15741
+ const content = fs28.readFileSync(path40, "utf8").trim();
15365
15742
  if (content.length === 0) fail(`--token-file ${path40} is empty`);
15366
15743
  try {
15367
- fs27.unlinkSync(path40);
15744
+ fs28.unlinkSync(path40);
15368
15745
  } catch {
15369
15746
  }
15370
15747
  return content;
@@ -15487,7 +15864,7 @@ async function pairAuto(args2) {
15487
15864
  }
15488
15865
 
15489
15866
  // src/commands/sessions.ts
15490
- var import_picocolors4 = __toESM(require("picocolors"));
15867
+ var import_picocolors5 = __toESM(require("picocolors"));
15491
15868
  async function sessions2(args2) {
15492
15869
  const [sub, id] = args2;
15493
15870
  if (sub === "switch") return switchSession();
@@ -15504,18 +15881,18 @@ function listSessions() {
15504
15881
  showIntro();
15505
15882
  const config = getConfig();
15506
15883
  if (config.sessions.length === 0) {
15507
- console.log(import_picocolors4.default.dim(" No paired sessions. Run codeam pair to connect.\n"));
15884
+ console.log(import_picocolors5.default.dim(" No paired sessions. Run codeam pair to connect.\n"));
15508
15885
  return;
15509
15886
  }
15510
- console.log(import_picocolors4.default.bold(" Paired sessions:\n"));
15887
+ console.log(import_picocolors5.default.bold(" Paired sessions:\n"));
15511
15888
  for (const s of config.sessions) {
15512
15889
  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());
15890
+ const bullet = isActive ? import_picocolors5.default.green(" \u25CF") : import_picocolors5.default.dim(" \u25CB");
15891
+ const name = isActive ? import_picocolors5.default.bold(s.userName) : s.userName;
15892
+ const plan = import_picocolors5.default.cyan(s.plan);
15893
+ const date = import_picocolors5.default.dim(new Date(s.pairedAt).toLocaleDateString());
15517
15894
  console.log(`${bullet} ${name} ${plan} ${date}`);
15518
- console.log(import_picocolors4.default.dim(` ${s.id}`));
15895
+ console.log(import_picocolors5.default.dim(` ${s.id}`));
15519
15896
  }
15520
15897
  console.log("");
15521
15898
  }
@@ -15533,7 +15910,7 @@ async function switchSession() {
15533
15910
  }
15534
15911
  setActiveSession(chosen);
15535
15912
  const s = config.sessions.find((x) => x.id === chosen);
15536
- console.log(import_picocolors4.default.green(`
15913
+ console.log(import_picocolors5.default.green(`
15537
15914
  \u2713 Switched to ${s?.userName ?? chosen}
15538
15915
  `));
15539
15916
  }
@@ -15551,29 +15928,29 @@ async function deleteSession(id) {
15551
15928
  return;
15552
15929
  }
15553
15930
  removeSession(id);
15554
- console.log(import_picocolors4.default.green("\n \u2713 Session deleted\n"));
15931
+ console.log(import_picocolors5.default.green("\n \u2713 Session deleted\n"));
15555
15932
  }
15556
15933
 
15557
15934
  // src/commands/status.ts
15558
- var import_picocolors5 = __toESM(require("picocolors"));
15935
+ var import_picocolors6 = __toESM(require("picocolors"));
15559
15936
  function status() {
15560
15937
  showIntro();
15561
15938
  const config = getConfig();
15562
15939
  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")}`);
15940
+ console.log(import_picocolors6.default.bold(" Status\n"));
15941
+ console.log(` Plugin ID ${import_picocolors6.default.dim(config.pluginId || "not generated yet")}`);
15565
15942
  console.log(` Sessions ${config.sessions.length} paired`);
15566
15943
  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)}`);
15944
+ console.log(` Active ${import_picocolors6.default.bold(active.userName)} ${import_picocolors6.default.cyan(active.plan)}`);
15945
+ console.log(` Session ID ${import_picocolors6.default.dim(active.id)}`);
15569
15946
  } else {
15570
- console.log(` Active ${import_picocolors5.default.yellow("none")} ${import_picocolors5.default.dim("run codeam pair to connect")}`);
15947
+ console.log(` Active ${import_picocolors6.default.yellow("none")} ${import_picocolors6.default.dim("run codeam pair to connect")}`);
15571
15948
  }
15572
15949
  console.log("");
15573
15950
  }
15574
15951
 
15575
15952
  // src/commands/logout.ts
15576
- var import_picocolors6 = __toESM(require("picocolors"));
15953
+ var import_picocolors7 = __toESM(require("picocolors"));
15577
15954
  var API_BASE9 = resolveApiBaseUrl();
15578
15955
  async function notifyBackendOffline() {
15579
15956
  const cfg = loadCliConfig();
@@ -15607,17 +15984,17 @@ async function logout() {
15607
15984
  }
15608
15985
  await notifyBackendOffline();
15609
15986
  clearAll();
15610
- console.log(import_picocolors6.default.green("\n \u2713 Done. All sessions removed.\n"));
15987
+ console.log(import_picocolors7.default.green("\n \u2713 Done. All sessions removed.\n"));
15611
15988
  }
15612
15989
 
15613
15990
  // src/commands/deploy.ts
15614
- var import_picocolors9 = __toESM(require("picocolors"));
15991
+ var import_picocolors10 = __toESM(require("picocolors"));
15615
15992
 
15616
15993
  // src/services/providers/github-codespaces.ts
15617
15994
  var import_child_process14 = require("child_process");
15618
15995
  var import_util3 = require("util");
15619
- var import_picocolors7 = __toESM(require("picocolors"));
15620
- var path33 = __toESM(require("path"));
15996
+ var import_picocolors8 = __toESM(require("picocolors"));
15997
+ var path34 = __toESM(require("path"));
15621
15998
  var execFileP4 = (0, import_util3.promisify)(import_child_process14.execFile);
15622
15999
  var MAX_BUFFER = 8 * 1024 * 1024;
15623
16000
  function resetStdinForChild() {
@@ -15684,7 +16061,7 @@ var GitHubCodespacesProvider = class {
15684
16061
  if (expectedUser) {
15685
16062
  noteLines.push("");
15686
16063
  noteLines.push(
15687
- `${import_picocolors7.default.yellow("\u26A0")} Sign in as ${import_picocolors7.default.cyan(expectedUser)} in the browser.`
16064
+ `${import_picocolors8.default.yellow("\u26A0")} Sign in as ${import_picocolors8.default.cyan(expectedUser)} in the browser.`
15688
16065
  );
15689
16066
  noteLines.push(
15690
16067
  " If a different GitHub account is already signed in, sign out"
@@ -15707,7 +16084,7 @@ var GitHubCodespacesProvider = class {
15707
16084
  if (refreshCode !== 0) {
15708
16085
  const lines = [
15709
16086
  "The browser approval came back for a different GitHub account",
15710
- `than the one gh is configured for${expectedUser ? ` (${import_picocolors7.default.cyan(expectedUser)})` : ""}.`,
16087
+ `than the one gh is configured for${expectedUser ? ` (${import_picocolors8.default.cyan(expectedUser)})` : ""}.`,
15711
16088
  "",
15712
16089
  "To recover:",
15713
16090
  " 1. Open https://github.com and sign out of any non-target",
@@ -15716,7 +16093,7 @@ var GitHubCodespacesProvider = class {
15716
16093
  "",
15717
16094
  "You can also grant the scope manually first and skip this step",
15718
16095
  "on the next run:",
15719
- ` ${import_picocolors7.default.cyan("gh auth refresh -h github.com -s codespace")}`
16096
+ ` ${import_picocolors8.default.cyan("gh auth refresh -h github.com -s codespace")}`
15720
16097
  ];
15721
16098
  throw new Error(lines.join("\n"));
15722
16099
  }
@@ -15783,7 +16160,7 @@ var GitHubCodespacesProvider = class {
15783
16160
  async tryInstallGh() {
15784
16161
  const platform2 = process.platform;
15785
16162
  wt(
15786
- `GitHub CLI (${import_picocolors7.default.cyan("gh")}) is required for Codespaces deploys but isn't on your PATH.`,
16163
+ `GitHub CLI (${import_picocolors8.default.cyan("gh")}) is required for Codespaces deploys but isn't on your PATH.`,
15787
16164
  "Heads up"
15788
16165
  );
15789
16166
  if (platform2 === "linux") {
@@ -15839,7 +16216,7 @@ var GitHubCodespacesProvider = class {
15839
16216
  return;
15840
16217
  }
15841
16218
  const proceed = await ot2({
15842
- message: `Run ${import_picocolors7.default.cyan(installCmd.describe)} now?`,
16219
+ message: `Run ${import_picocolors8.default.cyan(installCmd.describe)} now?`,
15843
16220
  initialValue: true
15844
16221
  });
15845
16222
  if (q(proceed) || !proceed) return;
@@ -16106,7 +16483,7 @@ var GitHubCodespacesProvider = class {
16106
16483
  });
16107
16484
  }
16108
16485
  async uploadFile(workspaceId, remotePath, contents, options = {}) {
16109
- const remoteDir = path33.posix.dirname(remotePath);
16486
+ const remoteDir = path34.posix.dirname(remotePath);
16110
16487
  const parts = [
16111
16488
  `mkdir -p ${shellQuote(remoteDir)}`,
16112
16489
  `cat > ${shellQuote(remotePath)}`
@@ -16176,8 +16553,8 @@ function shellQuote(s) {
16176
16553
  // src/services/providers/gitpod.ts
16177
16554
  var import_child_process15 = require("child_process");
16178
16555
  var import_util4 = require("util");
16179
- var path34 = __toESM(require("path"));
16180
- var import_picocolors8 = __toESM(require("picocolors"));
16556
+ var path35 = __toESM(require("path"));
16557
+ var import_picocolors9 = __toESM(require("picocolors"));
16181
16558
  var execFileP5 = (0, import_util4.promisify)(import_child_process15.execFile);
16182
16559
  var MAX_BUFFER2 = 8 * 1024 * 1024;
16183
16560
  function resetStdinForChild2() {
@@ -16416,7 +16793,7 @@ var GitpodProvider = class {
16416
16793
  });
16417
16794
  }
16418
16795
  async uploadFile(workspaceId, remotePath, contents, options = {}) {
16419
- const remoteDir = path34.posix.dirname(remotePath);
16796
+ const remoteDir = path35.posix.dirname(remotePath);
16420
16797
  const parts = [
16421
16798
  `mkdir -p ${shellQuote2(remoteDir)}`,
16422
16799
  `cat > ${shellQuote2(remotePath)}`
@@ -16452,7 +16829,7 @@ function shellQuote2(s) {
16452
16829
  // src/services/providers/gitlab-workspaces.ts
16453
16830
  var import_child_process16 = require("child_process");
16454
16831
  var import_util5 = require("util");
16455
- var path35 = __toESM(require("path"));
16832
+ var path36 = __toESM(require("path"));
16456
16833
  var execFileP6 = (0, import_util5.promisify)(import_child_process16.execFile);
16457
16834
  var MAX_BUFFER3 = 8 * 1024 * 1024;
16458
16835
  var GITLAB_API_BASE = process.env.CODEAM_GITLAB_API_URL ?? "https://gitlab.com/api/v4";
@@ -16712,7 +17089,7 @@ Docs: https://docs.gitlab.com/ee/user/workspace/configuration.html`
16712
17089
  }
16713
17090
  async uploadFile(workspaceId, remotePath, contents, options = {}) {
16714
17091
  const sshHost = process.env.CODEAM_GITLAB_SSH_HOST ?? "workspaces.gitlab.com";
16715
- const remoteDir = path35.posix.dirname(remotePath);
17092
+ const remoteDir = path36.posix.dirname(remotePath);
16716
17093
  const parts = [`mkdir -p ${shellQuote3(remoteDir)}`, `cat > ${shellQuote3(remotePath)}`];
16717
17094
  if (options.mode != null) {
16718
17095
  parts.push(`chmod ${options.mode.toString(8)} ${shellQuote3(remotePath)}`);
@@ -16780,7 +17157,7 @@ function shellQuote3(s) {
16780
17157
  // src/services/providers/railway.ts
16781
17158
  var import_child_process17 = require("child_process");
16782
17159
  var import_util6 = require("util");
16783
- var path36 = __toESM(require("path"));
17160
+ var path37 = __toESM(require("path"));
16784
17161
  var execFileP7 = (0, import_util6.promisify)(import_child_process17.execFile);
16785
17162
  var MAX_BUFFER4 = 8 * 1024 * 1024;
16786
17163
  function resetStdinForChild4() {
@@ -17016,7 +17393,7 @@ var RailwayProvider = class {
17016
17393
  if (!projectId || !serviceId) {
17017
17394
  throw new Error("Invalid Railway workspace id (expected projectId/serviceId).");
17018
17395
  }
17019
- const remoteDir = path36.posix.dirname(remotePath);
17396
+ const remoteDir = path37.posix.dirname(remotePath);
17020
17397
  const parts = [`mkdir -p ${shellQuote4(remoteDir)}`, `cat > ${shellQuote4(remotePath)}`];
17021
17398
  if (options.mode != null) {
17022
17399
  parts.push(`chmod ${options.mode.toString(8)} ${shellQuote4(remotePath)}`);
@@ -17057,7 +17434,7 @@ var PROVIDERS = [
17057
17434
  // src/commands/deploy.ts
17058
17435
  async function deploy(args2 = []) {
17059
17436
  console.log();
17060
- mt(import_picocolors9.default.bgMagenta(import_picocolors9.default.white(" codeam deploy ")));
17437
+ mt(import_picocolors10.default.bgMagenta(import_picocolors10.default.white(" codeam deploy ")));
17061
17438
  const provider = await pickProvider();
17062
17439
  if (!provider) {
17063
17440
  pt("No provider selected.");
@@ -17094,7 +17471,7 @@ async function deploy(args2 = []) {
17094
17471
  if (provider.expandListScopes) {
17095
17472
  options.push({
17096
17473
  value: EXPAND_SCOPES,
17097
- label: import_picocolors9.default.cyan("+ Don't see your project? Expand scopes\u2026"),
17474
+ label: import_picocolors10.default.cyan("+ Don't see your project? Expand scopes\u2026"),
17098
17475
  hint: "Re-authorize with broader scopes (org / team repos)"
17099
17476
  });
17100
17477
  }
@@ -17142,7 +17519,7 @@ async function deploy(args2 = []) {
17142
17519
  label: w3.displayName ?? w3.id,
17143
17520
  hint: [w3.state, formatLastUsed(w3.lastUsedAt)].filter(Boolean).join(" \xB7 ")
17144
17521
  })),
17145
- { value: "__new__", label: import_picocolors9.default.green("+ Create a new workspace"), hint: "fresh codespace" }
17522
+ { value: "__new__", label: import_picocolors10.default.green("+ Create a new workspace"), hint: "fresh codespace" }
17146
17523
  ]
17147
17524
  });
17148
17525
  if (q(choice)) {
@@ -17243,12 +17620,12 @@ async function deploy(args2 = []) {
17243
17620
  cliStep.stop("\u2713 codeam-cli installed");
17244
17621
  wt(
17245
17622
  [
17246
- `Workspace: ${import_picocolors9.default.cyan(workspace.displayName ?? workspace.id)}`,
17247
- workspace.webUrl ? `Web: ${import_picocolors9.default.cyan(workspace.webUrl)}` : "",
17623
+ `Workspace: ${import_picocolors10.default.cyan(workspace.displayName ?? workspace.id)}`,
17624
+ workspace.webUrl ? `Web: ${import_picocolors10.default.cyan(workspace.webUrl)}` : "",
17248
17625
  "",
17249
17626
  `Starting \`codeam pair\` on the workspace (agent: ${AGENT_REGISTRY[agentId].displayName}).`,
17250
17627
  "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.)")
17628
+ import_picocolors10.default.dim("(Once paired, this terminal disconnects automatically; the session stays alive on the codespace.)")
17252
17629
  ].filter(Boolean).join("\n"),
17253
17630
  "Almost there"
17254
17631
  );
@@ -17363,11 +17740,11 @@ async function deploy(args2 = []) {
17363
17740
  ].join("\n");
17364
17741
  const code = (await provider.streamCommand(workspace.id, `bash -lc ${shellQuoteSingle(wrapper)}`)).code;
17365
17742
  if (code === 0) {
17366
- gt(import_picocolors9.default.green("\u2713 Workspace deployed and paired. Drive from your phone, anywhere."));
17743
+ gt(import_picocolors10.default.green("\u2713 Workspace deployed and paired. Drive from your phone, anywhere."));
17367
17744
  } else if (code === 130) {
17368
- gt(import_picocolors9.default.yellow("Disconnected from local terminal. Mobile session keeps running on the codespace."));
17745
+ gt(import_picocolors10.default.yellow("Disconnected from local terminal. Mobile session keeps running on the codespace."));
17369
17746
  } else {
17370
- gt(import_picocolors9.default.yellow('Pairing did not complete. Run "codeam pair" inside the codespace if needed.'));
17747
+ gt(import_picocolors10.default.yellow('Pairing did not complete. Run "codeam pair" inside the codespace if needed.'));
17371
17748
  }
17372
17749
  }
17373
17750
  function shellQuoteSingle(s) {
@@ -17401,7 +17778,7 @@ async function pickProvider() {
17401
17778
  message: "Where do you want to deploy?",
17402
17779
  options: PROVIDERS.map((prov) => ({
17403
17780
  value: prov.id,
17404
- label: prov.available ? prov.displayName : `${prov.displayName} ${import_picocolors9.default.dim("(coming soon)")}`,
17781
+ label: prov.available ? prov.displayName : `${prov.displayName} ${import_picocolors10.default.dim("(coming soon)")}`,
17405
17782
  hint: prov.tagline
17406
17783
  }))
17407
17784
  });
@@ -17415,464 +17792,144 @@ async function pickProvider() {
17415
17792
  return null;
17416
17793
  }
17417
17794
  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);
17795
+ }
17796
+
17797
+ // src/commands/deploy-manage.ts
17798
+ var import_picocolors11 = __toESM(require("picocolors"));
17799
+ async function deployList() {
17800
+ console.log();
17801
+ mt(import_picocolors11.default.bgMagenta(import_picocolors11.default.white(" codeam deploy ls ")));
17802
+ const workspaces = await collectWorkspacesWithStatus();
17803
+ if (workspaces.length === 0) {
17804
+ gt(import_picocolors11.default.dim("No deployed workspaces found."));
17718
17805
  return;
17719
17806
  }
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);
17807
+ for (const w3 of workspaces) {
17808
+ 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"}`);
17809
+ console.log(` ${tag} ${import_picocolors11.default.cyan(w3.displayName ?? w3.id)} ${import_picocolors11.default.dim("(" + w3.providerName + ")")}`);
17725
17810
  }
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);
17811
+ gt(import_picocolors11.default.dim("Use `codeam deploy stop` to terminate a session."));
17733
17812
  }
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", () => {
17813
+ async function deployStop() {
17814
+ console.log();
17815
+ mt(import_picocolors11.default.bgMagenta(import_picocolors11.default.white(" codeam deploy stop ")));
17816
+ const workspaces = await collectWorkspacesWithStatus();
17817
+ if (workspaces.length === 0) {
17818
+ gt(import_picocolors11.default.dim("No deployed workspaces found."));
17819
+ return;
17820
+ }
17821
+ const choice = await _t({
17822
+ message: "Pick a workspace to stop:",
17823
+ options: workspaces.map((w3) => ({
17824
+ value: w3.id,
17825
+ label: w3.displayName ?? w3.id,
17826
+ hint: [
17827
+ w3.providerName,
17828
+ w3.codeamRunning ? import_picocolors11.default.green("\u25CF codeam-pair running") : import_picocolors11.default.dim("\u25CB no codeam-pair"),
17829
+ w3.state ?? ""
17830
+ ].filter(Boolean).join(" \xB7 ")
17831
+ }))
17749
17832
  });
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;
17833
+ if (q(choice)) {
17834
+ pt("Cancelled.");
17835
+ process.exit(0);
17807
17836
  }
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);
17837
+ const target = workspaces.find((w3) => w3.id === choice);
17838
+ if (target.codeamRunning) {
17839
+ const stopStep = fe();
17840
+ stopStep.start("Stopping codeam-pair on the workspace\u2026");
17841
+ try {
17842
+ const result = await target.provider.exec(
17843
+ target.id,
17844
+ "pm2 delete codeam-pair >/dev/null 2>&1; pm2 list 2>/dev/null | grep -c codeam-pair || true"
17845
+ );
17846
+ void result;
17847
+ stopStep.stop("\u2713 codeam-pair stopped \u2014 your phone is now disconnected from this workspace");
17848
+ } catch (err) {
17849
+ stopStep.stop("\u26A0 Could not reach the workspace to stop codeam-pair");
17850
+ void err;
17851
+ }
17852
+ } else {
17853
+ O2.info("No codeam-pair process to stop on this workspace.");
17813
17854
  }
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
17855
+ const alsoStop = await ot2({
17856
+ message: `Also stop the workspace ${import_picocolors11.default.cyan(target.displayName ?? target.id)} to save compute hours?`,
17857
+ initialValue: true
17823
17858
  });
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.`
17859
+ if (!q(alsoStop) && alsoStop) {
17860
+ const cs = fe();
17861
+ cs.start("Stopping workspace\u2026");
17862
+ try {
17863
+ const result = await target.provider.exec(
17864
+ target.id,
17865
+ // We'd ideally use a provider method; for now do it inline
17866
+ // — works for the github-codespaces provider, no-op-ish for
17867
+ // others (the command will fail and we fall back gracefully).
17868
+ "echo stopping"
17831
17869
  );
17832
- } else {
17833
- showError(`Upload failed: ${result.message}`);
17870
+ void result;
17871
+ await stopWorkspaceFromLocal(target);
17872
+ cs.stop(`\u2713 Workspace ${target.displayName ?? target.id} is stopping`);
17873
+ } catch (err) {
17874
+ cs.stop("\u26A0 Could not stop the workspace");
17875
+ O2.warn(err instanceof Error ? err.message : String(err));
17834
17876
  }
17835
- process.exit(1);
17836
17877
  }
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("");
17878
+ gt(import_picocolors11.default.green("\u2713 Done."));
17844
17879
  }
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);
17880
+ async function collectWorkspacesWithStatus() {
17881
+ const out2 = [];
17882
+ const ready = PROVIDERS.filter((prov) => prov.available);
17883
+ for (const provider of ready) {
17884
+ if (!provider.listExistingWorkspaces) continue;
17885
+ const probeStep = fe();
17886
+ probeStep.start(`Listing ${provider.displayName} workspaces\u2026`);
17887
+ let workspaces = [];
17888
+ try {
17889
+ await provider.authorize();
17890
+ workspaces = await provider.listExistingWorkspaces();
17891
+ probeStep.stop(`\u2713 ${workspaces.length} workspace${workspaces.length === 1 ? "" : "s"} on ${provider.displayName}`);
17892
+ } catch (err) {
17893
+ probeStep.stop(`\u2717 Could not list ${provider.displayName} workspaces`);
17894
+ O2.warn(err instanceof Error ? err.message : String(err));
17895
+ continue;
17896
+ }
17897
+ for (const w3 of workspaces) {
17898
+ const codeamRunning = await probeCodeamPair(provider, w3);
17899
+ out2.push({
17900
+ ...w3,
17901
+ provider,
17902
+ providerName: provider.displayName,
17903
+ codeamRunning
17904
+ });
17905
+ }
17863
17906
  }
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).`
17907
+ return out2;
17908
+ }
17909
+ async function probeCodeamPair(provider, workspace) {
17910
+ if (workspace.state && workspace.state !== "Available") return false;
17911
+ try {
17912
+ const result = await provider.exec(
17913
+ workspace.id,
17914
+ // `online` is the only state we care about — `errored`, `stopped`,
17915
+ // `stopping` all mean it's not actively serving the user's phone.
17916
+ `pm2 jlist 2>/dev/null | grep -c '"name":"codeam-pair"[^}]*"status":"online"' || echo 0`
17868
17917
  );
17869
- process.exit(0);
17918
+ if (result.code !== 0) return false;
17919
+ const n = parseInt(result.stdout.trim(), 10);
17920
+ return Number.isFinite(n) && n > 0;
17921
+ } catch {
17922
+ return false;
17923
+ }
17924
+ }
17925
+ async function stopWorkspaceFromLocal(target) {
17926
+ if (target.provider.id === "github-codespaces") {
17927
+ const { execFile: execFile8 } = await import("child_process");
17928
+ const { promisify: promisify9 } = await import("util");
17929
+ const execFileP8 = promisify9(execFile8);
17930
+ await execFileP8("gh", ["codespace", "stop", "-c", target.id], { maxBuffer: 8 * 1024 * 1024 });
17931
+ return;
17870
17932
  }
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
17933
  }
17877
17934
 
17878
17935
  // src/commands/doctor.ts
@@ -18048,7 +18105,7 @@ function checkChokidar() {
18048
18105
  }
18049
18106
  async function doctor(args2 = []) {
18050
18107
  const json = args2.includes("--json");
18051
- const cliVersion = true ? "2.21.2" : "0.0.0-dev";
18108
+ const cliVersion = true ? "2.22.1" : "0.0.0-dev";
18052
18109
  const apiBase = resolveApiBaseUrl();
18053
18110
  const diagnosticId = (0, import_node_crypto5.randomUUID)();
18054
18111
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -18247,7 +18304,7 @@ async function completion(args2) {
18247
18304
  // src/commands/version.ts
18248
18305
  var import_picocolors13 = __toESM(require("picocolors"));
18249
18306
  function version2() {
18250
- const v = true ? "2.21.2" : "unknown";
18307
+ const v = true ? "2.22.1" : "unknown";
18251
18308
  console.log(`${import_picocolors13.default.bold("codeam-cli")} ${import_picocolors13.default.cyan(v)}`);
18252
18309
  }
18253
18310
 
@@ -18475,7 +18532,7 @@ function checkForUpdates() {
18475
18532
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
18476
18533
  if (process.env.CI) return;
18477
18534
  if (!process.stdout.isTTY) return;
18478
- const current = true ? "2.21.2" : null;
18535
+ const current = true ? "2.22.1" : null;
18479
18536
  if (!current) return;
18480
18537
  const cache = readCache();
18481
18538
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;