codeam-cli 2.4.17 → 2.4.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to `codeam-cli` are documented here.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [2.4.16] — 2026-05-03
8
+
9
+ ### Fixed
10
+
11
+ - **cli:** Clean codeam deploy log output under PM2 (v2.4.16)
12
+
7
13
  ## [2.4.15] — 2026-05-03
8
14
 
9
15
  ### Fixed
package/README.md CHANGED
@@ -47,12 +47,14 @@ That's it. Open the [CodeAgent Mobile app](https://codeagent-mobile.com), enter
47
47
  | `codeam status` | Show connection status |
48
48
  | `codeam logout` | Remove all paired sessions |
49
49
  | `codeam deploy` | Provision a cloud workspace (GitHub Codespaces) and pair it to your phone |
50
+ | `codeam deploy ls` | List the cloud workspaces you've deployed (and which still have a session running) |
51
+ | `codeam deploy stop` | Pick a deployed workspace and stop its codeam session (and optionally the workspace itself) |
50
52
 
51
53
  ---
52
54
 
53
55
  ## `codeam deploy` — drive a cloud workspace from your phone
54
56
 
55
- Don't want to keep your laptop running while you control Claude from the train? `codeam deploy` spins up a fresh **GitHub Codespace** for any of your repos, installs Claude Code + `codeam-cli` inside it, copies your local Claude credentials so you skip the re-auth (or runs `claude login` interactively if you don't have a local config yet), and finishes by streaming `codeam pair` from inside the codespace so you get a pairing code on this terminal already wired to the remote workspace.
57
+ Don't want to keep your laptop running while you control Claude from the train? `codeam deploy` spins up a fresh **GitHub Codespace** for any of your repos, installs Claude Code + `codeam-cli` inside it, copies your local Claude credentials so you skip the re-auth (or runs `claude login` interactively if you don't have a local config yet), supervises the agent with **PM2** so the session survives even after you close your laptop, and gives you a QR/code to pair your phone straight from your local terminal.
56
58
 
57
59
  ```bash
58
60
  codeam deploy
@@ -62,11 +64,26 @@ That's it. You'll be guided through:
62
64
 
63
65
  1. **Pick a provider** (GitHub Codespaces today; more coming).
64
66
  2. **Pick a repo** from your account.
65
- 3. **Wait ~1 minute** while the codespace boots and tools install.
66
- 4. **Scan the QR / enter the code** on the CodeAgent Mobile app same flow as `codeam pair`, only the agent is now running in the cloud.
67
+ 3. **Reuse an existing codespace or create a new one** re-runs of `codeam deploy` against the same project don't pile up codespaces.
68
+ 4. **Wait ~1 minute** while the codespace boots and tools install.
69
+ 5. **Scan the QR / enter the code** on the CodeAgent Mobile app.
70
+ 6. Your local terminal **automatically disconnects** once Claude is ready — close the laptop, the agent keeps running on the codespace, and your phone stays connected.
67
71
 
68
72
  Requirements: the [GitHub CLI (`gh`)](https://cli.github.com/) installed and authenticated (`gh auth login`). The deploy flow re-uses `gh`'s OAuth — we don't ask for a separate token.
69
73
 
74
+ ### Managing your deployed workspaces
75
+
76
+ ```bash
77
+ # Show every workspace you've deployed and whether codeam is still running on it.
78
+ codeam deploy ls
79
+
80
+ # Pick one and stop the codeam session — also offers to stop the workspace
81
+ # itself so you don't burn compute hours.
82
+ codeam deploy stop
83
+ ```
84
+
85
+ Stopping a workspace via `codeam deploy stop` is non-destructive: the GitHub Codespace stays around (preserving your branch, files, and dotfiles); only the running compute is paused. Re-running `codeam deploy` will offer to resume that same codespace.
86
+
70
87
  Adding more cloud backends (Gitpod, Coder, your own SSH host, …) is a single new file in `apps/cli/src/services/providers/` — the `CloudProvider` interface keeps it pluggable.
71
88
 
72
89
  ---
package/dist/index.js CHANGED
@@ -179,7 +179,7 @@ var import_qrcode_terminal = __toESM(require("qrcode-terminal"));
179
179
  // package.json
180
180
  var package_default = {
181
181
  name: "codeam-cli",
182
- version: "2.4.17",
182
+ version: "2.4.18",
183
183
  description: "Remote control Claude Code (and other AI coding agents) from your mobile phone. Pair your device, send prompts, stream responses in real-time, and approve commands \u2014 from anywhere.",
184
184
  main: "dist/index.js",
185
185
  bin: {
@@ -406,11 +406,15 @@ var os2 = __toESM(require("os"));
406
406
  var API_BASE2 = process.env.CODEAM_API_URL ?? "https://codeagent-mobile-api.vercel.app";
407
407
  async function requestCode(pluginId) {
408
408
  try {
409
+ const runtime = process.env.CODESPACES === "true" ? "github-codespaces" : "local";
410
+ const codespaceName = process.env.CODESPACE_NAME;
409
411
  const result = await _transport.postJson(`${API_BASE2}/api/pairing/code`, {
410
412
  pluginId,
411
413
  ideName: "Terminal (codeam-cli)",
412
414
  ideVersion: package_default.version,
413
- hostname: os2.hostname()
415
+ hostname: os2.hostname(),
416
+ runtime,
417
+ ...codespaceName ? { codespaceName } : {}
414
418
  });
415
419
  const data = result?.data;
416
420
  if (!data?.code) return null;
@@ -5551,18 +5555,9 @@ var GitHubCodespacesProvider = class {
5551
5555
  }
5552
5556
  async listExistingWorkspaces(projectId) {
5553
5557
  try {
5554
- const { stdout } = await execFileP2(
5555
- "gh",
5556
- [
5557
- "codespace",
5558
- "list",
5559
- "--repo",
5560
- projectId,
5561
- "--json",
5562
- "name,displayName,state,lastUsedAt"
5563
- ],
5564
- { maxBuffer: MAX_BUFFER }
5565
- );
5558
+ const args2 = ["codespace", "list", "--json", "name,displayName,state,lastUsedAt"];
5559
+ if (projectId) args2.push("--repo", projectId);
5560
+ const { stdout } = await execFileP2("gh", args2, { maxBuffer: MAX_BUFFER });
5566
5561
  const list = JSON.parse(stdout);
5567
5562
  return list.map((c2) => ({
5568
5563
  id: c2.name,
@@ -6134,6 +6129,144 @@ async function pickProvider() {
6134
6129
  return found;
6135
6130
  }
6136
6131
 
6132
+ // src/commands/deploy-manage.ts
6133
+ var import_picocolors9 = __toESM(require("picocolors"));
6134
+ async function deployList() {
6135
+ console.log();
6136
+ mt(import_picocolors9.default.bgMagenta(import_picocolors9.default.white(" codeam deploy ls ")));
6137
+ const workspaces = await collectWorkspacesWithStatus();
6138
+ if (workspaces.length === 0) {
6139
+ gt(import_picocolors9.default.dim("No deployed workspaces found."));
6140
+ return;
6141
+ }
6142
+ for (const w3 of workspaces) {
6143
+ const tag = w3.codeamRunning ? import_picocolors9.default.green("\u25CF running") : w3.state === "Available" ? import_picocolors9.default.dim("\u25CB idle") : import_picocolors9.default.dim(`\u25CB ${w3.state ?? "stopped"}`);
6144
+ console.log(` ${tag} ${import_picocolors9.default.cyan(w3.displayName ?? w3.id)} ${import_picocolors9.default.dim("(" + w3.providerName + ")")}`);
6145
+ }
6146
+ gt(import_picocolors9.default.dim("Use `codeam deploy stop` to terminate a session."));
6147
+ }
6148
+ async function deployStop() {
6149
+ console.log();
6150
+ mt(import_picocolors9.default.bgMagenta(import_picocolors9.default.white(" codeam deploy stop ")));
6151
+ const workspaces = await collectWorkspacesWithStatus();
6152
+ if (workspaces.length === 0) {
6153
+ gt(import_picocolors9.default.dim("No deployed workspaces found."));
6154
+ return;
6155
+ }
6156
+ const choice = await _t({
6157
+ message: "Pick a workspace to stop:",
6158
+ options: workspaces.map((w3) => ({
6159
+ value: w3.id,
6160
+ label: w3.displayName ?? w3.id,
6161
+ hint: [
6162
+ w3.providerName,
6163
+ w3.codeamRunning ? import_picocolors9.default.green("\u25CF codeam-pair running") : import_picocolors9.default.dim("\u25CB no codeam-pair"),
6164
+ w3.state ?? ""
6165
+ ].filter(Boolean).join(" \xB7 ")
6166
+ }))
6167
+ });
6168
+ if (q(choice)) {
6169
+ pt("Cancelled.");
6170
+ process.exit(0);
6171
+ }
6172
+ const target = workspaces.find((w3) => w3.id === choice);
6173
+ if (target.codeamRunning) {
6174
+ const stopStep = fe();
6175
+ stopStep.start("Stopping codeam-pair on the workspace\u2026");
6176
+ try {
6177
+ const result = await target.provider.exec(
6178
+ target.id,
6179
+ "pm2 delete codeam-pair >/dev/null 2>&1; pm2 list 2>/dev/null | grep -c codeam-pair || true"
6180
+ );
6181
+ void result;
6182
+ stopStep.stop("\u2713 codeam-pair stopped \u2014 your phone is now disconnected from this workspace");
6183
+ } catch (err) {
6184
+ stopStep.stop("\u26A0 Could not reach the workspace to stop codeam-pair");
6185
+ void err;
6186
+ }
6187
+ } else {
6188
+ O2.info("No codeam-pair process to stop on this workspace.");
6189
+ }
6190
+ const alsoStop = await ot2({
6191
+ message: `Also stop the workspace ${import_picocolors9.default.cyan(target.displayName ?? target.id)} to save compute hours?`,
6192
+ initialValue: true
6193
+ });
6194
+ if (!q(alsoStop) && alsoStop) {
6195
+ const cs = fe();
6196
+ cs.start("Stopping workspace\u2026");
6197
+ try {
6198
+ const result = await target.provider.exec(
6199
+ target.id,
6200
+ // We'd ideally use a provider method; for now do it inline
6201
+ // — works for the github-codespaces provider, no-op-ish for
6202
+ // others (the command will fail and we fall back gracefully).
6203
+ "echo stopping"
6204
+ );
6205
+ void result;
6206
+ await stopWorkspaceFromLocal(target);
6207
+ cs.stop(`\u2713 Workspace ${target.displayName ?? target.id} is stopping`);
6208
+ } catch (err) {
6209
+ cs.stop("\u26A0 Could not stop the workspace");
6210
+ O2.warn(err instanceof Error ? err.message : String(err));
6211
+ }
6212
+ }
6213
+ gt(import_picocolors9.default.green("\u2713 Done."));
6214
+ }
6215
+ async function collectWorkspacesWithStatus() {
6216
+ const out = [];
6217
+ const ready = PROVIDERS.filter((prov) => prov.available);
6218
+ for (const provider of ready) {
6219
+ if (!provider.listExistingWorkspaces) continue;
6220
+ const probeStep = fe();
6221
+ probeStep.start(`Listing ${provider.displayName} workspaces\u2026`);
6222
+ let workspaces = [];
6223
+ try {
6224
+ await provider.authorize();
6225
+ workspaces = await provider.listExistingWorkspaces();
6226
+ probeStep.stop(`\u2713 ${workspaces.length} workspace${workspaces.length === 1 ? "" : "s"} on ${provider.displayName}`);
6227
+ } catch (err) {
6228
+ probeStep.stop(`\u2717 Could not list ${provider.displayName} workspaces`);
6229
+ O2.warn(err instanceof Error ? err.message : String(err));
6230
+ continue;
6231
+ }
6232
+ for (const w3 of workspaces) {
6233
+ const codeamRunning = await probeCodeamPair(provider, w3);
6234
+ out.push({
6235
+ ...w3,
6236
+ provider,
6237
+ providerName: provider.displayName,
6238
+ codeamRunning
6239
+ });
6240
+ }
6241
+ }
6242
+ return out;
6243
+ }
6244
+ async function probeCodeamPair(provider, workspace) {
6245
+ if (workspace.state && workspace.state !== "Available") return false;
6246
+ try {
6247
+ const result = await provider.exec(
6248
+ workspace.id,
6249
+ // `online` is the only state we care about — `errored`, `stopped`,
6250
+ // `stopping` all mean it's not actively serving the user's phone.
6251
+ `pm2 jlist 2>/dev/null | grep -c '"name":"codeam-pair"[^}]*"status":"online"' || echo 0`
6252
+ );
6253
+ if (result.code !== 0) return false;
6254
+ const n = parseInt(result.stdout.trim(), 10);
6255
+ return Number.isFinite(n) && n > 0;
6256
+ } catch {
6257
+ return false;
6258
+ }
6259
+ }
6260
+ async function stopWorkspaceFromLocal(target) {
6261
+ if (target.provider.id === "github-codespaces") {
6262
+ const { execFile: execFile4 } = await import("child_process");
6263
+ const { promisify: promisify4 } = await import("util");
6264
+ const execFileP4 = promisify4(execFile4);
6265
+ await execFileP4("gh", ["codespace", "stop", "-c", target.id], { maxBuffer: 8 * 1024 * 1024 });
6266
+ return;
6267
+ }
6268
+ }
6269
+
6137
6270
  // src/index.ts
6138
6271
  var [, , command, ...args] = process.argv;
6139
6272
  async function main() {
@@ -6147,6 +6280,8 @@ async function main() {
6147
6280
  case "logout":
6148
6281
  return logout();
6149
6282
  case "deploy":
6283
+ if (args[0] === "ls" || args[0] === "list") return deployList();
6284
+ if (args[0] === "stop" || args[0] === "remove") return deployStop();
6150
6285
  return deploy();
6151
6286
  default:
6152
6287
  return start();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.4.17",
3
+ "version": "2.4.18",
4
4
  "description": "Remote control Claude Code (and other AI coding agents) from your mobile phone. Pair your device, send prompts, stream responses in real-time, and approve commands — from anywhere.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {