codeam-cli 2.4.16 → 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 +6 -0
- package/README.md +20 -3
- package/dist/index.js +155 -15
- package/package.json +1 -1
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),
|
|
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. **
|
|
66
|
-
4. **
|
|
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.
|
|
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
|
|
5555
|
-
|
|
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,
|
|
@@ -5925,7 +5920,12 @@ async function deploy() {
|
|
|
5925
5920
|
// pairing code" lines per second in the file — pure noise. Drop
|
|
5926
5921
|
// them so the user sees just the QR + the pairing code + the
|
|
5927
5922
|
// "Paired with" / "for shortcuts" markers.
|
|
5928
|
-
|
|
5923
|
+
// `tail -n +1` shows everything in the file from the start —
|
|
5924
|
+
// critical because pm2 has already written the QR + pairing
|
|
5925
|
+
// code by the time we get here (during the `sleep 2` above).
|
|
5926
|
+
// `-n 0` would miss all of that and only show the post-spawn
|
|
5927
|
+
// spinner spam, leaving the user staring at a blank screen.
|
|
5928
|
+
'tail -n +1 -F "$LOG" 2>/dev/null | grep --line-buffered -vE "Waiting for mobile app|Requesting pairing code" &',
|
|
5929
5929
|
"TAIL=$!",
|
|
5930
5930
|
"trap 'kill $TAIL 2>/dev/null; exit 130' INT TERM",
|
|
5931
5931
|
// Phase 1 — wait for "Paired with", or for codeam to print a
|
|
@@ -6129,6 +6129,144 @@ async function pickProvider() {
|
|
|
6129
6129
|
return found;
|
|
6130
6130
|
}
|
|
6131
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
|
+
|
|
6132
6270
|
// src/index.ts
|
|
6133
6271
|
var [, , command, ...args] = process.argv;
|
|
6134
6272
|
async function main() {
|
|
@@ -6142,6 +6280,8 @@ async function main() {
|
|
|
6142
6280
|
case "logout":
|
|
6143
6281
|
return logout();
|
|
6144
6282
|
case "deploy":
|
|
6283
|
+
if (args[0] === "ls" || args[0] === "list") return deployList();
|
|
6284
|
+
if (args[0] === "stop" || args[0] === "remove") return deployStop();
|
|
6145
6285
|
return deploy();
|
|
6146
6286
|
default:
|
|
6147
6287
|
return start();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeam-cli",
|
|
3
|
-
"version": "2.4.
|
|
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": {
|