codeam-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +25 -0
  2. package/dist/index.js +813 -0
  3. package/package.json +50 -0
package/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # codeam-cli
2
+
3
+ Remote control [Claude Code](https://claude.ai/code) from your mobile device using [CodeAgent Mobile](https://codeagentmobile.com).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g codeam-cli
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ codeam pair # Pair with your mobile device
15
+ codeam # Start Claude Code with mobile control
16
+ codeam sessions # List paired sessions
17
+ codeam status # Show connection status
18
+ codeam logout # Remove all sessions
19
+ ```
20
+
21
+ ## Requirements
22
+
23
+ - Node.js 18+
24
+ - Claude Code installed (`npm install -g @anthropic-ai/claude-code`)
25
+ - [CodeAgent Mobile](https://codeagentmobile.com) app on your phone
package/dist/index.js ADDED
@@ -0,0 +1,813 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/commands/start.ts
27
+ var import_picocolors2 = __toESM(require("picocolors"));
28
+
29
+ // src/config.ts
30
+ var fs = __toESM(require("fs"));
31
+ var os = __toESM(require("os"));
32
+ var path = __toESM(require("path"));
33
+ var crypto = __toESM(require("crypto"));
34
+ var EMPTY_CONFIG = () => ({
35
+ pluginId: crypto.randomUUID(),
36
+ activeSessionId: null,
37
+ sessions: []
38
+ });
39
+ function makeConfig(baseDir) {
40
+ const dir = path.join(baseDir ?? os.homedir(), ".codeam");
41
+ const file = path.join(dir, "config.json");
42
+ function load() {
43
+ try {
44
+ const raw = JSON.parse(fs.readFileSync(file, "utf-8"));
45
+ return {
46
+ pluginId: typeof raw.pluginId === "string" ? raw.pluginId : crypto.randomUUID(),
47
+ activeSessionId: typeof raw.activeSessionId === "string" ? raw.activeSessionId : null,
48
+ sessions: Array.isArray(raw.sessions) ? raw.sessions : []
49
+ };
50
+ } catch {
51
+ return EMPTY_CONFIG();
52
+ }
53
+ }
54
+ function save(c) {
55
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
56
+ fs.writeFileSync(file, JSON.stringify(c, null, 2), { encoding: "utf-8", mode: 384 });
57
+ }
58
+ function getConfig2() {
59
+ return load();
60
+ }
61
+ function ensurePluginId2() {
62
+ const c = load();
63
+ save(c);
64
+ return c.pluginId;
65
+ }
66
+ function addSession2(session) {
67
+ const c = load();
68
+ c.sessions = c.sessions.filter((s) => s.id !== session.id);
69
+ c.sessions.unshift(session);
70
+ if (!c.activeSessionId) c.activeSessionId = session.id;
71
+ save(c);
72
+ }
73
+ function removeSession2(sessionId) {
74
+ const c = load();
75
+ c.sessions = c.sessions.filter((s) => s.id !== sessionId);
76
+ if (c.activeSessionId === sessionId) {
77
+ c.activeSessionId = c.sessions[0]?.id ?? null;
78
+ }
79
+ save(c);
80
+ }
81
+ function setActiveSession2(sessionId) {
82
+ const c = load();
83
+ c.activeSessionId = sessionId;
84
+ save(c);
85
+ }
86
+ function getActiveSession2() {
87
+ const c = load();
88
+ if (!c.activeSessionId) return null;
89
+ const session = c.sessions.find((s) => s.id === c.activeSessionId) ?? null;
90
+ if (!session) {
91
+ c.activeSessionId = null;
92
+ save(c);
93
+ }
94
+ return session;
95
+ }
96
+ function clearAll2() {
97
+ try {
98
+ fs.unlinkSync(file);
99
+ } catch {
100
+ }
101
+ }
102
+ return { getConfig: getConfig2, ensurePluginId: ensurePluginId2, addSession: addSession2, removeSession: removeSession2, setActiveSession: setActiveSession2, getActiveSession: getActiveSession2, clearAll: clearAll2 };
103
+ }
104
+ var _default = makeConfig();
105
+ var { getConfig, ensurePluginId, addSession, removeSession, setActiveSession, getActiveSession, clearAll } = _default;
106
+
107
+ // src/ui/banner.ts
108
+ var import_picocolors = __toESM(require("picocolors"));
109
+ var VERSION = "1.0.0";
110
+ function showIntro() {
111
+ console.log("");
112
+ console.log(` ${import_picocolors.default.bold(import_picocolors.default.cyan("codeam"))} ${import_picocolors.default.dim(`v${VERSION}`)}`);
113
+ console.log("");
114
+ }
115
+ function showSuccess(msg) {
116
+ console.log(` ${import_picocolors.default.green("\u2713")} ${msg}`);
117
+ }
118
+ function showError(msg) {
119
+ console.log(` ${import_picocolors.default.red("\u2717")} ${msg}`);
120
+ }
121
+ function showInfo(msg) {
122
+ console.log(` ${import_picocolors.default.dim("\xB7")} ${msg}`);
123
+ }
124
+ function showPairingCode(code, expiresAt) {
125
+ const secs = Math.max(0, Math.floor((expiresAt - Date.now()) / 1e3));
126
+ const timer = `${Math.floor(secs / 60)}:${String(secs % 60).padStart(2, "0")}`;
127
+ console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
128
+ const codePad = " ".repeat(Math.max(0, 19 - code.length));
129
+ const timerPad = " ".repeat(Math.max(0, 15 - timer.length));
130
+ console.log(` \u2502 Code: ${import_picocolors.default.bold(import_picocolors.default.yellow(code))}${codePad}\u2502`);
131
+ console.log(` \u2502 Expires in: ${import_picocolors.default.dim(timer)}${timerPad}\u2502`);
132
+ console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
133
+ console.log("");
134
+ }
135
+
136
+ // src/services/websocket.service.ts
137
+ var import_ws = __toESM(require("ws"));
138
+ var API_BASE = process.env.CODEAM_API_URL ?? "https://codeagent-mobile-api.vercel.app";
139
+ var WS_URL = API_BASE.replace("https://", "wss://").replace("http://", "ws://") + "/api/ws";
140
+ var HEARTBEAT_MS = 3e4;
141
+ var MAX_RECONNECT = 10;
142
+ var WebSocketService = class {
143
+ constructor(sessionId, pluginId) {
144
+ this.sessionId = sessionId;
145
+ this.pluginId = pluginId;
146
+ }
147
+ sessionId;
148
+ pluginId;
149
+ client = null;
150
+ heartbeat = null;
151
+ reconnectTimer = null;
152
+ reconnectAttempts = 0;
153
+ handlers = [];
154
+ _connected = false;
155
+ get connected() {
156
+ return this._connected;
157
+ }
158
+ addHandler(h) {
159
+ this.handlers.push(h);
160
+ }
161
+ connect() {
162
+ this.disconnect();
163
+ try {
164
+ this.client = new import_ws.default(WS_URL);
165
+ this.client.on("open", () => {
166
+ this._connected = true;
167
+ this.reconnectAttempts = 0;
168
+ this.client.send(JSON.stringify({
169
+ type: "auth",
170
+ payload: { token: null, sessionId: this.sessionId, pluginId: this.pluginId },
171
+ timestamp: Date.now()
172
+ }));
173
+ this.startHeartbeat();
174
+ this.handlers.forEach((h) => h.onConnected());
175
+ });
176
+ this.client.on("message", (raw) => {
177
+ try {
178
+ const msg = JSON.parse(raw.toString());
179
+ if (msg.type === "pong" || msg.type === "auth_success" || msg.type === "auth_error") return;
180
+ this.handlers.forEach((h) => h.onMessage(msg.type, msg.payload ?? {}));
181
+ } catch {
182
+ }
183
+ });
184
+ this.client.on("close", () => {
185
+ this._connected = false;
186
+ this.stopHeartbeat();
187
+ this.handlers.forEach((h) => h.onDisconnected());
188
+ if (this.reconnectAttempts < MAX_RECONNECT) {
189
+ this.reconnectAttempts++;
190
+ const delay = Math.min(1e3 * 2 ** this.reconnectAttempts, 3e4);
191
+ this.reconnectTimer = setTimeout(() => this.connect(), delay);
192
+ }
193
+ });
194
+ this.client.on("error", () => {
195
+ });
196
+ } catch {
197
+ }
198
+ }
199
+ send(type, payload) {
200
+ if (!this._connected || !this.client) return;
201
+ this.client.send(JSON.stringify({ type, payload, timestamp: Date.now() }));
202
+ }
203
+ disconnect() {
204
+ if (this.reconnectTimer) {
205
+ clearTimeout(this.reconnectTimer);
206
+ this.reconnectTimer = null;
207
+ }
208
+ this.reconnectAttempts = 0;
209
+ this.stopHeartbeat();
210
+ this.client?.removeAllListeners();
211
+ this.client?.close();
212
+ this.client = null;
213
+ this._connected = false;
214
+ }
215
+ startHeartbeat() {
216
+ this.stopHeartbeat();
217
+ this.heartbeat = setInterval(() => {
218
+ if (this._connected) this.client?.send(JSON.stringify({ type: "ping", timestamp: Date.now() }));
219
+ }, HEARTBEAT_MS);
220
+ }
221
+ stopHeartbeat() {
222
+ if (this.heartbeat) {
223
+ clearInterval(this.heartbeat);
224
+ this.heartbeat = null;
225
+ }
226
+ }
227
+ };
228
+
229
+ // src/services/pairing.service.ts
230
+ var https = __toESM(require("https"));
231
+ var http = __toESM(require("http"));
232
+ var os2 = __toESM(require("os"));
233
+ var API_BASE2 = process.env.CODEAM_API_URL ?? "https://codeagent-mobile-api.vercel.app";
234
+ async function requestCode(pluginId) {
235
+ try {
236
+ const result = await _transport.postJson(`${API_BASE2}/api/pairing/code`, {
237
+ pluginId,
238
+ ideName: "Terminal (codeam-cli)",
239
+ ideVersion: "1.0.0",
240
+ hostname: os2.hostname()
241
+ });
242
+ const data = result?.data;
243
+ if (!data?.code) return null;
244
+ return { code: data.code, expiresAt: data.expiresAt };
245
+ } catch {
246
+ return null;
247
+ }
248
+ }
249
+ function pollStatus(pluginId, onPaired, onTimeout) {
250
+ let stopped = false;
251
+ const interval = setInterval(async () => {
252
+ if (stopped) return;
253
+ try {
254
+ const result = await _transport.getJson(
255
+ `${API_BASE2}/api/pairing/status?pluginId=${pluginId}`
256
+ );
257
+ const data = result?.data;
258
+ if (data?.paired) {
259
+ stop();
260
+ const user = data.user ?? {};
261
+ onPaired({
262
+ sessionId: data.sessionId,
263
+ userName: user.name || "",
264
+ userEmail: user.email || "",
265
+ plan: user.plan || "FREE"
266
+ });
267
+ }
268
+ } catch {
269
+ }
270
+ }, 3e3);
271
+ const timeout = setTimeout(() => {
272
+ stop();
273
+ onTimeout();
274
+ }, 3e5);
275
+ function stop() {
276
+ stopped = true;
277
+ clearInterval(interval);
278
+ clearTimeout(timeout);
279
+ }
280
+ return stop;
281
+ }
282
+ var _transport = {
283
+ postJson: _postJson,
284
+ getJson: _getJson
285
+ };
286
+ async function _postJson(url, body) {
287
+ return new Promise((resolve, reject) => {
288
+ const data = JSON.stringify(body);
289
+ const u = new URL(url);
290
+ const transport = u.protocol === "https:" ? https : http;
291
+ const req = transport.request(
292
+ {
293
+ hostname: u.hostname,
294
+ port: u.port || (u.protocol === "https:" ? 443 : 80),
295
+ path: u.pathname + u.search,
296
+ method: "POST",
297
+ headers: {
298
+ "Content-Type": "application/json",
299
+ "Content-Length": Buffer.byteLength(data)
300
+ },
301
+ timeout: 1e4
302
+ },
303
+ (res) => {
304
+ res.on("error", reject);
305
+ let body2 = "";
306
+ res.on("data", (chunk) => {
307
+ body2 += chunk.toString();
308
+ });
309
+ res.on("end", () => {
310
+ if (res.statusCode && res.statusCode >= 400) {
311
+ reject(new Error(`HTTP ${res.statusCode}`));
312
+ return;
313
+ }
314
+ try {
315
+ resolve(JSON.parse(body2));
316
+ } catch {
317
+ resolve(null);
318
+ }
319
+ });
320
+ }
321
+ );
322
+ req.on("error", reject);
323
+ req.on("timeout", () => {
324
+ req.destroy();
325
+ reject(new Error("timeout"));
326
+ });
327
+ req.write(data);
328
+ req.end();
329
+ });
330
+ }
331
+ async function _getJson(url) {
332
+ return new Promise((resolve, reject) => {
333
+ const u = new URL(url);
334
+ const transport = u.protocol === "https:" ? https : http;
335
+ const req = transport.request(
336
+ {
337
+ hostname: u.hostname,
338
+ port: u.port || (u.protocol === "https:" ? 443 : 80),
339
+ path: u.pathname + u.search,
340
+ method: "GET",
341
+ timeout: 1e4
342
+ },
343
+ (res) => {
344
+ res.on("error", reject);
345
+ let body = "";
346
+ res.on("data", (chunk) => {
347
+ body += chunk.toString();
348
+ });
349
+ res.on("end", () => {
350
+ if (res.statusCode && res.statusCode >= 400) {
351
+ reject(new Error(`HTTP ${res.statusCode}`));
352
+ return;
353
+ }
354
+ try {
355
+ resolve(JSON.parse(body));
356
+ } catch {
357
+ resolve(null);
358
+ }
359
+ });
360
+ }
361
+ );
362
+ req.on("error", reject);
363
+ req.on("timeout", () => {
364
+ req.destroy();
365
+ reject(new Error("timeout"));
366
+ });
367
+ req.end();
368
+ });
369
+ }
370
+
371
+ // src/services/command-relay.service.ts
372
+ var API_BASE3 = process.env.CODEAM_API_URL ?? "https://codeagent-mobile-api.vercel.app";
373
+ var CommandRelayService = class {
374
+ constructor(pluginId, onCommand) {
375
+ this.pluginId = pluginId;
376
+ this.onCommand = onCommand;
377
+ }
378
+ pluginId;
379
+ onCommand;
380
+ _running = false;
381
+ pollTimer = null;
382
+ heartbeatTimer = null;
383
+ start() {
384
+ if (this.pollTimer) {
385
+ clearTimeout(this.pollTimer);
386
+ this.pollTimer = null;
387
+ }
388
+ if (this.heartbeatTimer) {
389
+ clearInterval(this.heartbeatTimer);
390
+ this.heartbeatTimer = null;
391
+ }
392
+ this._running = true;
393
+ this.sendHeartbeat(true);
394
+ this.heartbeatTimer = setInterval(() => this.sendHeartbeat(true), 2e4);
395
+ void this.pollLoop();
396
+ this.reportAgents();
397
+ }
398
+ stop() {
399
+ if (!this._running) return;
400
+ this._running = false;
401
+ if (this.pollTimer) {
402
+ clearTimeout(this.pollTimer);
403
+ this.pollTimer = null;
404
+ }
405
+ if (this.heartbeatTimer) {
406
+ clearInterval(this.heartbeatTimer);
407
+ this.heartbeatTimer = null;
408
+ }
409
+ this.sendHeartbeat(false).catch(() => {
410
+ });
411
+ }
412
+ async sendResult(commandId, status2, result) {
413
+ await _postJson(`${API_BASE3}/api/commands/result`, { commandId, status: status2, result });
414
+ }
415
+ async pollLoop() {
416
+ if (!this._running) return;
417
+ await this.poll();
418
+ if (this._running) {
419
+ this.pollTimer = setTimeout(() => this.pollLoop(), 2e3);
420
+ }
421
+ }
422
+ async poll() {
423
+ try {
424
+ const data = await _getJson(
425
+ `${API_BASE3}/api/commands/pending?pluginId=${this.pluginId}`
426
+ );
427
+ const commands = data?.data;
428
+ if (!Array.isArray(commands)) return;
429
+ for (const obj of commands) {
430
+ this.onCommand({
431
+ id: obj.id,
432
+ sessionId: obj.sessionId,
433
+ type: obj.type,
434
+ payload: obj.payload ?? {}
435
+ });
436
+ }
437
+ } catch {
438
+ }
439
+ }
440
+ async sendHeartbeat(online) {
441
+ await _postJson(`${API_BASE3}/api/plugin/heartbeat`, {
442
+ pluginId: this.pluginId,
443
+ online
444
+ }).catch(() => {
445
+ });
446
+ }
447
+ reportAgents() {
448
+ _postJson(`${API_BASE3}/api/plugin/agents`, {
449
+ pluginId: this.pluginId,
450
+ agents: [{ id: "claude-code", name: "Claude Code", icon: "\u{1F916}", installed: true }]
451
+ }).catch(() => {
452
+ });
453
+ }
454
+ };
455
+
456
+ // src/services/claude.service.ts
457
+ var pty = __toESM(require("node-pty"));
458
+ var fs2 = __toESM(require("fs"));
459
+ var path2 = __toESM(require("path"));
460
+ var ClaudeService = class {
461
+ constructor(opts) {
462
+ this.opts = opts;
463
+ }
464
+ opts;
465
+ proc = null;
466
+ spawn() {
467
+ if (this.proc) {
468
+ this.cleanup();
469
+ this.proc.kill();
470
+ this.proc = null;
471
+ }
472
+ const claudePath = findInPath("claude") ?? findInPath("claude-code");
473
+ if (!claudePath) {
474
+ console.error(
475
+ "\n \u2717 claude not found in PATH.\n Install it with: npm install -g @anthropic-ai/claude-code\n"
476
+ );
477
+ process.exit(1);
478
+ }
479
+ this.proc = pty.spawn(claudePath, [], {
480
+ name: "xterm-256color",
481
+ cols: process.stdout.columns ?? 80,
482
+ rows: process.stdout.rows ?? 24,
483
+ cwd: this.opts.cwd,
484
+ env: process.env
485
+ });
486
+ this.proc.onData((data) => {
487
+ process.stdout.write(data);
488
+ this.opts.onData?.(data);
489
+ });
490
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
491
+ process.stdin.resume();
492
+ process.stdin.on("data", this.stdinHandler);
493
+ process.on("SIGWINCH", this.handleResize);
494
+ this.proc.onExit(({ exitCode }) => {
495
+ this.cleanup();
496
+ this.opts.onExit(exitCode ?? 0);
497
+ });
498
+ }
499
+ /** Send a command to Claude's stdin (remote control) */
500
+ sendCommand(text) {
501
+ this.proc?.write(text + "\r");
502
+ }
503
+ /** Send Ctrl+C to Claude */
504
+ interrupt() {
505
+ this.proc?.write("");
506
+ }
507
+ kill() {
508
+ this.proc?.kill();
509
+ this.cleanup();
510
+ }
511
+ stdinHandler = (chunk) => {
512
+ this.proc?.write(chunk.toString());
513
+ };
514
+ handleResize = () => {
515
+ this.proc?.resize(process.stdout.columns ?? 80, process.stdout.rows ?? 24);
516
+ };
517
+ cleanup() {
518
+ process.removeListener("SIGWINCH", this.handleResize);
519
+ process.stdin.removeListener("data", this.stdinHandler);
520
+ if (process.stdin.isTTY) {
521
+ try {
522
+ process.stdin.setRawMode(false);
523
+ } catch {
524
+ }
525
+ }
526
+ }
527
+ };
528
+ function findInPath(name) {
529
+ const dirs = (process.env.PATH ?? "").split(path2.delimiter);
530
+ for (const dir of dirs) {
531
+ const full = `${dir}/${name}`;
532
+ try {
533
+ fs2.accessSync(full, fs2.constants.X_OK);
534
+ return full;
535
+ } catch {
536
+ }
537
+ }
538
+ return null;
539
+ }
540
+
541
+ // src/commands/start.ts
542
+ async function start() {
543
+ showIntro();
544
+ const session = getActiveSession();
545
+ if (!session) {
546
+ console.log(` ${import_picocolors2.default.dim("No paired session found.")}`);
547
+ console.log(` ${import_picocolors2.default.dim(`Run ${import_picocolors2.default.white("codeam pair")} to connect your mobile app.`)}
548
+ `);
549
+ process.exit(0);
550
+ }
551
+ const pluginId = ensurePluginId();
552
+ showInfo(`${session.userName} \xB7 ${import_picocolors2.default.cyan(session.plan)}`);
553
+ showInfo("Launching Claude Code...\n");
554
+ const ws = new WebSocketService(session.id, pluginId);
555
+ const relay = new CommandRelayService(pluginId, (cmd) => {
556
+ switch (cmd.type) {
557
+ case "start_task": {
558
+ const prompt = cmd.payload.prompt;
559
+ if (prompt) claude.sendCommand(prompt);
560
+ break;
561
+ }
562
+ case "provide_input": {
563
+ const input = cmd.payload.input;
564
+ if (input) claude.sendCommand(input);
565
+ break;
566
+ }
567
+ case "stop_task":
568
+ claude.interrupt();
569
+ break;
570
+ }
571
+ });
572
+ ws.addHandler({
573
+ onConnected() {
574
+ },
575
+ onDisconnected() {
576
+ },
577
+ onMessage(type, payload) {
578
+ if (type !== "agent_command") return;
579
+ const cmdType = payload.type;
580
+ const inner = payload.payload ?? {};
581
+ if (cmdType === "start_task") {
582
+ const prompt = inner.prompt;
583
+ if (prompt) claude.sendCommand(prompt);
584
+ } else if (cmdType === "provide_input") {
585
+ const input = inner.input;
586
+ if (input) claude.sendCommand(input);
587
+ } else if (cmdType === "stop_task") {
588
+ claude.interrupt();
589
+ }
590
+ }
591
+ });
592
+ ws.connect();
593
+ relay.start();
594
+ const claude = new ClaudeService({
595
+ cwd: process.cwd(),
596
+ onExit(code) {
597
+ process.removeListener("SIGINT", sigintHandler);
598
+ relay.stop();
599
+ ws.disconnect();
600
+ process.exit(code);
601
+ }
602
+ });
603
+ function sigintHandler() {
604
+ claude.kill();
605
+ relay.stop();
606
+ ws.disconnect();
607
+ process.exit(0);
608
+ }
609
+ process.once("SIGINT", sigintHandler);
610
+ claude.spawn();
611
+ }
612
+
613
+ // src/commands/pair.ts
614
+ var import_picocolors3 = __toESM(require("picocolors"));
615
+
616
+ // src/ui/prompts.ts
617
+ var p = __toESM(require("@clack/prompts"));
618
+ async function confirmAction(message) {
619
+ const result = await p.confirm({ message });
620
+ if (p.isCancel(result)) return false;
621
+ return result;
622
+ }
623
+ async function selectSession(sessions2, activeId) {
624
+ const result = await p.select({
625
+ message: "Select active session:",
626
+ options: sessions2.map((s) => ({
627
+ value: s.id,
628
+ label: `${s.userName} ${s.plan}`,
629
+ hint: s.id === activeId ? "active" : `paired ${new Date(s.pairedAt).toLocaleDateString()}`
630
+ })),
631
+ initialValue: activeId ?? void 0
632
+ });
633
+ if (p.isCancel(result)) return null;
634
+ return result;
635
+ }
636
+
637
+ // src/commands/pair.ts
638
+ async function pair() {
639
+ showIntro();
640
+ const pluginId = ensurePluginId();
641
+ const spin = p.spinner();
642
+ spin.start("Requesting pairing code...");
643
+ const result = await requestCode(pluginId);
644
+ if (!result) {
645
+ spin.stop("Failed");
646
+ showError("Could not reach the server. Check your connection and try again.");
647
+ process.exit(1);
648
+ }
649
+ spin.stop("Got pairing code");
650
+ showPairingCode(result.code, result.expiresAt);
651
+ console.log(import_picocolors3.default.dim(" Open CodeAgent Mobile and enter this code."));
652
+ console.log("");
653
+ const waitSpin = p.spinner();
654
+ waitSpin.start("Waiting for mobile app...");
655
+ await new Promise((resolve) => {
656
+ let stopPolling = null;
657
+ function sigintHandler() {
658
+ stopPolling?.();
659
+ console.log("");
660
+ process.exit(0);
661
+ }
662
+ stopPolling = pollStatus(
663
+ pluginId,
664
+ (info) => {
665
+ process.removeListener("SIGINT", sigintHandler);
666
+ waitSpin.stop("Paired!");
667
+ addSession({
668
+ id: info.sessionId,
669
+ userName: info.userName,
670
+ userEmail: info.userEmail,
671
+ plan: info.plan,
672
+ pairedAt: Date.now()
673
+ });
674
+ showSuccess(`Paired with ${info.userName} (${info.plan})`);
675
+ console.log(
676
+ import_picocolors3.default.dim(`
677
+ Session saved. Run ${import_picocolors3.default.white("codeam")} to start.
678
+ `)
679
+ );
680
+ resolve();
681
+ },
682
+ () => {
683
+ waitSpin.stop("Timed out");
684
+ showError("Pairing timed out after 5 minutes. Run codeam pair to try again.");
685
+ process.exit(1);
686
+ }
687
+ );
688
+ process.once("SIGINT", sigintHandler);
689
+ });
690
+ }
691
+
692
+ // src/commands/sessions.ts
693
+ var import_picocolors4 = __toESM(require("picocolors"));
694
+ async function sessions(args2) {
695
+ const [sub, id] = args2;
696
+ if (sub === "switch") return switchSession();
697
+ if (sub === "delete") {
698
+ if (!id) {
699
+ showError("Usage: codeam sessions delete <session-id>");
700
+ process.exit(1);
701
+ }
702
+ return deleteSession(id);
703
+ }
704
+ return listSessions();
705
+ }
706
+ function listSessions() {
707
+ showIntro();
708
+ const config = getConfig();
709
+ if (config.sessions.length === 0) {
710
+ console.log(import_picocolors4.default.dim(" No paired sessions. Run codeam pair to connect.\n"));
711
+ return;
712
+ }
713
+ console.log(import_picocolors4.default.bold(" Paired sessions:\n"));
714
+ for (const s of config.sessions) {
715
+ const isActive = s.id === config.activeSessionId;
716
+ const bullet = isActive ? import_picocolors4.default.green(" \u25CF") : import_picocolors4.default.dim(" \u25CB");
717
+ const name = isActive ? import_picocolors4.default.bold(s.userName) : s.userName;
718
+ const plan = import_picocolors4.default.cyan(s.plan);
719
+ const date = import_picocolors4.default.dim(new Date(s.pairedAt).toLocaleDateString());
720
+ console.log(`${bullet} ${name} ${plan} ${date}`);
721
+ console.log(import_picocolors4.default.dim(` ${s.id}`));
722
+ }
723
+ console.log("");
724
+ }
725
+ async function switchSession() {
726
+ showIntro();
727
+ const config = getConfig();
728
+ if (config.sessions.length === 0) {
729
+ showError("No paired sessions. Run codeam pair to connect.");
730
+ process.exit(1);
731
+ }
732
+ const chosen = await selectSession(config.sessions, config.activeSessionId);
733
+ if (!chosen) {
734
+ console.log("");
735
+ return;
736
+ }
737
+ setActiveSession(chosen);
738
+ const s = config.sessions.find((x) => x.id === chosen);
739
+ console.log(import_picocolors4.default.green(`
740
+ \u2713 Switched to ${s?.userName ?? chosen}
741
+ `));
742
+ }
743
+ async function deleteSession(id) {
744
+ showIntro();
745
+ const config = getConfig();
746
+ const session = config.sessions.find((s) => s.id === id);
747
+ if (!session) {
748
+ showError(`Session not found: ${id}`);
749
+ process.exit(1);
750
+ }
751
+ const ok = await confirmAction(`Delete session for ${session.userName}?`);
752
+ if (!ok) {
753
+ console.log("");
754
+ return;
755
+ }
756
+ removeSession(id);
757
+ console.log(import_picocolors4.default.green("\n \u2713 Session deleted\n"));
758
+ }
759
+
760
+ // src/commands/status.ts
761
+ var import_picocolors5 = __toESM(require("picocolors"));
762
+ function status() {
763
+ showIntro();
764
+ const config = getConfig();
765
+ const active = config.sessions.find((s) => s.id === config.activeSessionId) ?? null;
766
+ console.log(import_picocolors5.default.bold(" Status\n"));
767
+ console.log(` Plugin ID ${import_picocolors5.default.dim(config.pluginId || "not generated yet")}`);
768
+ console.log(` Sessions ${config.sessions.length} paired`);
769
+ if (active) {
770
+ console.log(` Active ${import_picocolors5.default.bold(active.userName)} ${import_picocolors5.default.cyan(active.plan)}`);
771
+ console.log(` Session ID ${import_picocolors5.default.dim(active.id)}`);
772
+ } else {
773
+ console.log(` Active ${import_picocolors5.default.yellow("none")} ${import_picocolors5.default.dim("run codeam pair to connect")}`);
774
+ }
775
+ console.log("");
776
+ }
777
+
778
+ // src/commands/logout.ts
779
+ var import_picocolors6 = __toESM(require("picocolors"));
780
+ async function logout() {
781
+ showIntro();
782
+ const ok = await confirmAction("Remove all sessions and local config?");
783
+ if (!ok) {
784
+ console.log("");
785
+ return;
786
+ }
787
+ clearAll();
788
+ console.log(import_picocolors6.default.green("\n \u2713 Done. All sessions removed.\n"));
789
+ }
790
+
791
+ // src/index.ts
792
+ var [, , command, ...args] = process.argv;
793
+ async function main() {
794
+ switch (command) {
795
+ case "pair":
796
+ return pair();
797
+ case "sessions":
798
+ return sessions(args);
799
+ case "status":
800
+ return status();
801
+ case "logout":
802
+ return logout();
803
+ default:
804
+ return start();
805
+ }
806
+ }
807
+ main().catch((err) => {
808
+ const msg = err instanceof Error ? err.message : String(err);
809
+ console.error(`
810
+ ${msg}
811
+ `);
812
+ process.exit(1);
813
+ });
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "codeam-cli",
3
+ "version": "1.0.0",
4
+ "description": "Remote control Claude Code from your mobile device",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "codeam": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsup",
15
+ "dev": "tsup --watch",
16
+ "test": "vitest run",
17
+ "typecheck": "tsc --noEmit",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "keywords": [
21
+ "claude",
22
+ "claude-code",
23
+ "ai",
24
+ "cli",
25
+ "remote-control",
26
+ "codeagent"
27
+ ],
28
+ "author": "Edgar Durand",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/edgardurand/codeagent-mobile.git"
32
+ },
33
+ "license": "MIT",
34
+ "engines": {
35
+ "node": ">=18.0.0"
36
+ },
37
+ "dependencies": {
38
+ "@clack/prompts": "^0.10.0",
39
+ "node-pty": "^1.0.0",
40
+ "picocolors": "^1.1.0",
41
+ "ws": "^8.18.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^22.0.0",
45
+ "@types/ws": "^8.5.0",
46
+ "tsup": "^8.0.0",
47
+ "typescript": "^5.0.0",
48
+ "vitest": "^2.1.0"
49
+ }
50
+ }