codeam-cli 1.4.1 → 1.4.2

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 (2) hide show
  1. package/dist/index.js +217 -16
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -24,9 +24,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/commands/start.ts
27
- var fs4 = __toESM(require("fs"));
28
- var os4 = __toESM(require("os"));
29
- var path4 = __toESM(require("path"));
27
+ var fs5 = __toESM(require("fs"));
28
+ var os5 = __toESM(require("os"));
29
+ var path5 = __toESM(require("path"));
30
30
  var import_crypto = require("crypto");
31
31
  var import_picocolors2 = __toESM(require("picocolors"));
32
32
 
@@ -114,7 +114,7 @@ var import_picocolors = __toESM(require("picocolors"));
114
114
  // package.json
115
115
  var package_default = {
116
116
  name: "codeam-cli",
117
- version: "1.4.1",
117
+ version: "1.4.2",
118
118
  description: "Remote control Claude Code from your mobile device",
119
119
  main: "dist/index.js",
120
120
  bin: {
@@ -594,13 +594,13 @@ var UnixPtyStrategy = class {
594
594
  opts;
595
595
  proc = null;
596
596
  helperPath = null;
597
- spawn(cmd, cwd) {
597
+ spawn(cmd, cwd, args2 = []) {
598
598
  const python = findInPath("python3") ?? findInPath("python");
599
599
  if (!python) {
600
600
  console.error(
601
601
  " \xB7 python3 not found; mobile command injection may be limited.\n"
602
602
  );
603
- this.spawnDirect(cmd, cwd);
603
+ this.spawnDirect(cmd, cwd, args2);
604
604
  return;
605
605
  }
606
606
  const shell = process.env.SHELL || "/bin/sh";
@@ -608,7 +608,8 @@ var UnixPtyStrategy = class {
608
608
  const rows = process.stdout.rows || 50;
609
609
  this.helperPath = path3.join(os3.tmpdir(), "codeam-pty-helper.py");
610
610
  fs3.writeFileSync(this.helperPath, PYTHON_PTY_HELPER, { mode: 420 });
611
- this.proc = (0, import_child_process.spawn)(python, [this.helperPath, shell, "-c", `exec ${cmd}`], {
611
+ const fullCmd = args2.length > 0 ? `${cmd} ${args2.join(" ")}` : cmd;
612
+ this.proc = (0, import_child_process.spawn)(python, [this.helperPath, shell, "-c", `exec ${fullCmd}`], {
612
613
  stdio: ["pipe", "pipe", "inherit"],
613
614
  cwd,
614
615
  env: {
@@ -645,8 +646,8 @@ var UnixPtyStrategy = class {
645
646
  * Python-unavailable fallback: direct spawn without PTY.
646
647
  * Mobile command injection is limited (no real TTY for Claude).
647
648
  */
648
- spawnDirect(cmd, cwd) {
649
- this.proc = (0, import_child_process.spawn)(cmd, [], {
649
+ spawnDirect(cmd, cwd, args2 = []) {
650
+ this.proc = (0, import_child_process.spawn)(cmd, args2, {
650
651
  stdio: ["pipe", "inherit", "inherit"],
651
652
  cwd,
652
653
  env: process.env,
@@ -719,8 +720,8 @@ var WindowsPtyStrategy = class {
719
720
  }
720
721
  opts;
721
722
  proc = null;
722
- spawn(cmd, cwd) {
723
- this.proc = (0, import_child_process2.spawn)(cmd, [], {
723
+ spawn(cmd, cwd, args2 = []) {
724
+ this.proc = (0, import_child_process2.spawn)(cmd, args2, {
724
725
  stdio: ["pipe", "pipe", "inherit"],
725
726
  cwd,
726
727
  env: {
@@ -849,6 +850,17 @@ var ClaudeService = class {
849
850
  kill() {
850
851
  this.strategy.kill();
851
852
  }
853
+ /**
854
+ * Kill the current Claude process and relaunch it resuming the given session.
855
+ * Pass auto=true to add --dangerously-skip-permissions (no confirmation prompts).
856
+ */
857
+ restart(sessionId, auto = false) {
858
+ const claudeCmd = findInPath("claude") ? "claude" : "claude-code";
859
+ this.strategy.kill();
860
+ const args2 = ["--resume", sessionId];
861
+ if (auto) args2.push("--dangerously-skip-permissions");
862
+ this.strategy.spawn(claudeCmd, this.opts.cwd, args2);
863
+ }
852
864
  };
853
865
 
854
866
  // src/services/output.service.ts
@@ -1126,6 +1138,22 @@ var OutputService = class _OutputService {
1126
1138
  });
1127
1139
  this.pollTimer = setInterval(() => this.tick(), _OutputService.POLL_MS);
1128
1140
  }
1141
+ /**
1142
+ * Like newTurn() but signals clients that a session is being resumed.
1143
+ * The resumedSessionId tells clients to fetch the conversation from the API.
1144
+ * Awaits the POST so callers can guarantee the signal is sent before restarting Claude.
1145
+ */
1146
+ async newTurnResume(resumedSessionId) {
1147
+ this.stopPoll();
1148
+ this.rawBuffer = "";
1149
+ this.lastSentContent = "";
1150
+ this.lastPushTime = 0;
1151
+ this.active = true;
1152
+ this.startTime = Date.now();
1153
+ await this.postChunk({ clear: true });
1154
+ await this.postChunk({ type: "new_turn", resumedSessionId, content: "", done: false });
1155
+ this.pollTimer = setInterval(() => this.tick(), _OutputService.POLL_MS);
1156
+ }
1129
1157
  push(raw) {
1130
1158
  if (!this.active) return;
1131
1159
  this.rawBuffer += raw;
@@ -1242,12 +1270,162 @@ var OutputService = class _OutputService {
1242
1270
  }
1243
1271
  };
1244
1272
 
1273
+ // src/services/history.service.ts
1274
+ var fs4 = __toESM(require("fs"));
1275
+ var path4 = __toESM(require("path"));
1276
+ var os4 = __toESM(require("os"));
1277
+ var https3 = __toESM(require("https"));
1278
+ var http3 = __toESM(require("http"));
1279
+ var API_BASE5 = process.env.CODEAM_API_URL ?? "https://codeagent-mobile-api.vercel.app";
1280
+ function encodeCwd(cwd) {
1281
+ return cwd.replace(/\//g, "-");
1282
+ }
1283
+ function extractText(content) {
1284
+ if (typeof content === "string") return content;
1285
+ if (Array.isArray(content)) {
1286
+ return content.filter((b) => b["type"] === "text").map((b) => b["text"]).join("\n");
1287
+ }
1288
+ return "";
1289
+ }
1290
+ function parseJsonl(filePath) {
1291
+ const messages = [];
1292
+ let raw;
1293
+ try {
1294
+ raw = fs4.readFileSync(filePath, "utf8");
1295
+ } catch {
1296
+ return messages;
1297
+ }
1298
+ const lines = raw.split("\n").filter(Boolean);
1299
+ for (const line of lines) {
1300
+ try {
1301
+ const record = JSON.parse(line);
1302
+ const type = record["type"];
1303
+ const msg = record["message"];
1304
+ const ts = record["timestamp"];
1305
+ const timestamp = typeof ts === "string" ? new Date(ts).getTime() : typeof ts === "number" ? ts : Date.now();
1306
+ const uuid = record["uuid"] ?? `${Date.now()}-${Math.random()}`;
1307
+ if (type === "user" && msg) {
1308
+ const text = extractText(msg["content"]).trim();
1309
+ if (text) messages.push({ id: uuid, role: "user", text, timestamp });
1310
+ } else if (type === "assistant" && msg) {
1311
+ const text = extractText(msg["content"]).trim();
1312
+ if (text) messages.push({ id: uuid, role: "agent", text, timestamp });
1313
+ }
1314
+ } catch {
1315
+ }
1316
+ }
1317
+ return messages;
1318
+ }
1319
+ function post(endpoint, body) {
1320
+ return new Promise((resolve) => {
1321
+ const payload = JSON.stringify(body);
1322
+ const u = new URL(`${API_BASE5}${endpoint}`);
1323
+ const transport = u.protocol === "https:" ? https3 : http3;
1324
+ const req = transport.request(
1325
+ {
1326
+ hostname: u.hostname,
1327
+ port: u.port || (u.protocol === "https:" ? 443 : 80),
1328
+ path: u.pathname,
1329
+ method: "POST",
1330
+ headers: {
1331
+ "Content-Type": "application/json",
1332
+ "Content-Length": Buffer.byteLength(payload)
1333
+ },
1334
+ timeout: 8e3
1335
+ },
1336
+ (res) => {
1337
+ res.resume();
1338
+ resolve();
1339
+ }
1340
+ );
1341
+ req.on("error", () => resolve());
1342
+ req.on("timeout", () => {
1343
+ req.destroy();
1344
+ resolve();
1345
+ });
1346
+ req.write(payload);
1347
+ req.end();
1348
+ });
1349
+ }
1350
+ var HistoryService = class {
1351
+ constructor(pluginId, cwd) {
1352
+ this.pluginId = pluginId;
1353
+ this.cwd = cwd;
1354
+ }
1355
+ pluginId;
1356
+ cwd;
1357
+ get projectDir() {
1358
+ return path4.join(os4.homedir(), ".claude", "projects", encodeCwd(this.cwd));
1359
+ }
1360
+ /**
1361
+ * Read session list from disk and POST it to the API.
1362
+ * Called once ~2 s after Claude spawns (non-blocking).
1363
+ */
1364
+ async load() {
1365
+ const dir = this.projectDir;
1366
+ let entries;
1367
+ try {
1368
+ entries = fs4.readdirSync(dir, { withFileTypes: true });
1369
+ } catch {
1370
+ return;
1371
+ }
1372
+ const sessions2 = [];
1373
+ for (const entry of entries) {
1374
+ if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
1375
+ const id = path4.basename(entry.name, ".jsonl");
1376
+ const filePath = path4.join(dir, entry.name);
1377
+ let mtime = Date.now();
1378
+ try {
1379
+ mtime = fs4.statSync(filePath).mtimeMs;
1380
+ } catch {
1381
+ }
1382
+ let summary = "";
1383
+ try {
1384
+ const raw = fs4.readFileSync(filePath, "utf8");
1385
+ for (const line of raw.split("\n")) {
1386
+ if (!line.trim()) continue;
1387
+ try {
1388
+ const record = JSON.parse(line);
1389
+ if (record["type"] === "user") {
1390
+ const msg = record["message"];
1391
+ const text = extractText(msg?.["content"]).trim();
1392
+ if (text) {
1393
+ summary = text.slice(0, 120);
1394
+ break;
1395
+ }
1396
+ }
1397
+ } catch {
1398
+ }
1399
+ }
1400
+ } catch {
1401
+ }
1402
+ if (summary) sessions2.push({ id, summary, timestamp: mtime });
1403
+ }
1404
+ if (sessions2.length === 0) return;
1405
+ sessions2.sort((a, b) => b.timestamp - a.timestamp);
1406
+ await post("/api/sessions/claude-sessions", { pluginId: this.pluginId, sessions: sessions2 });
1407
+ }
1408
+ /**
1409
+ * Read a specific session's full conversation and POST it to the API.
1410
+ * Called before sending the resume signal so clients can fetch it immediately.
1411
+ */
1412
+ async loadConversation(sessionId) {
1413
+ const filePath = path4.join(this.projectDir, `${sessionId}.jsonl`);
1414
+ const messages = parseJsonl(filePath);
1415
+ await post("/api/sessions/claude-conversation", {
1416
+ pluginId: this.pluginId,
1417
+ sessionId,
1418
+ messages
1419
+ });
1420
+ }
1421
+ };
1422
+
1245
1423
  // src/commands/start.ts
1246
1424
  function saveFilesTemp(files) {
1247
1425
  return files.filter(({ base64 }) => base64 && base64.length > 0).map(({ filename, base64 }) => {
1248
1426
  const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
1249
- const tmpPath = path4.join(os4.tmpdir(), `codeam-${(0, import_crypto.randomUUID)()}-${safeName}`);
1250
- fs4.writeFileSync(tmpPath, Buffer.from(base64, "base64"));
1427
+ const tmpPath = path5.join(os5.tmpdir(), `codeam-${(0, import_crypto.randomUUID)()}-${safeName}`);
1428
+ fs5.writeFileSync(tmpPath, Buffer.from(base64, "base64"));
1251
1429
  return tmpPath;
1252
1430
  });
1253
1431
  }
@@ -1269,7 +1447,7 @@ async function start() {
1269
1447
  outputSvc.newTurn();
1270
1448
  claude.sendCommand(prompt);
1271
1449
  }
1272
- const relay = new CommandRelayService(pluginId, (cmd) => {
1450
+ const relay = new CommandRelayService(pluginId, async (cmd) => {
1273
1451
  switch (cmd.type) {
1274
1452
  case "start_task": {
1275
1453
  const prompt = cmd.payload.prompt;
@@ -1283,7 +1461,7 @@ async function start() {
1283
1461
  setTimeout(() => {
1284
1462
  for (const p2 of paths) {
1285
1463
  try {
1286
- fs4.unlinkSync(p2);
1464
+ fs5.unlinkSync(p2);
1287
1465
  } catch {
1288
1466
  }
1289
1467
  }
@@ -1312,6 +1490,15 @@ async function start() {
1312
1490
  case "stop_task":
1313
1491
  claude.interrupt();
1314
1492
  break;
1493
+ case "resume_session": {
1494
+ const id = cmd.payload.id;
1495
+ const auto = cmd.payload.auto ?? false;
1496
+ if (!id) break;
1497
+ await historySvc.loadConversation(id);
1498
+ await outputSvc.newTurnResume(id);
1499
+ claude.restart(id, auto);
1500
+ break;
1501
+ }
1315
1502
  }
1316
1503
  });
1317
1504
  ws.addHandler({
@@ -1335,7 +1522,7 @@ async function start() {
1335
1522
  setTimeout(() => {
1336
1523
  for (const p2 of paths) {
1337
1524
  try {
1338
- fs4.unlinkSync(p2);
1525
+ fs5.unlinkSync(p2);
1339
1526
  } catch {
1340
1527
  }
1341
1528
  }
@@ -1356,6 +1543,15 @@ async function start() {
1356
1543
  claude.sendEscape();
1357
1544
  } else if (cmdType === "stop_task") {
1358
1545
  claude.interrupt();
1546
+ } else if (cmdType === "resume_session") {
1547
+ const id = inner.id;
1548
+ const auto = inner.auto ?? false;
1549
+ if (id) {
1550
+ historySvc.loadConversation(id).then(() => outputSvc.newTurnResume(id)).then(() => {
1551
+ claude.restart(id, auto);
1552
+ }).catch(() => {
1553
+ });
1554
+ }
1359
1555
  }
1360
1556
  }
1361
1557
  });
@@ -1383,6 +1579,11 @@ async function start() {
1383
1579
  }
1384
1580
  process.once("SIGINT", sigintHandler);
1385
1581
  claude.spawn();
1582
+ const historySvc = new HistoryService(pluginId, process.cwd());
1583
+ setTimeout(() => {
1584
+ historySvc.load().catch(() => {
1585
+ });
1586
+ }, 2e3);
1386
1587
  }
1387
1588
 
1388
1589
  // src/commands/pair.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
4
  "description": "Remote control Claude Code from your mobile device",
5
5
  "main": "dist/index.js",
6
6
  "bin": {