codeam-cli 1.4.0 → 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.
- package/dist/index.js +221 -18
- 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
|
|
28
|
-
var
|
|
29
|
-
var
|
|
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.
|
|
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
|
-
|
|
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
|
|
@@ -982,9 +994,10 @@ function detectSelector(lines) {
|
|
|
982
994
|
if (/^[─━—═\-]{3,}$/.test(t)) continue;
|
|
983
995
|
if (/^\[.*\]$/.test(t)) continue;
|
|
984
996
|
if (/^[>❯]\s/.test(t)) continue;
|
|
997
|
+
if (!t.includes(" ") && t.length > 15) continue;
|
|
985
998
|
questionParts.push(t);
|
|
986
999
|
}
|
|
987
|
-
const question = questionParts.join("
|
|
1000
|
+
const question = questionParts.filter((line, i, arr) => !arr.some((other, j) => j !== i && other.includes(line))).join("\n").trim();
|
|
988
1001
|
const optionLabels = /* @__PURE__ */ new Map();
|
|
989
1002
|
const optionDescs = /* @__PURE__ */ new Map();
|
|
990
1003
|
let currentNum = -1;
|
|
@@ -1039,9 +1052,10 @@ function detectListSelector(lines) {
|
|
|
1039
1052
|
}
|
|
1040
1053
|
if (/^[>❯]\s/.test(t)) continue;
|
|
1041
1054
|
if (/[↑↓].*navigate/i.test(t)) continue;
|
|
1055
|
+
if (!t.includes(" ") && t.length > 15) continue;
|
|
1042
1056
|
questionParts.push(t);
|
|
1043
1057
|
}
|
|
1044
|
-
const question = questionParts.join("
|
|
1058
|
+
const question = questionParts.filter((line, i, arr) => !arr.some((other, j) => j !== i && other.includes(line))).join("\n").trim();
|
|
1045
1059
|
const options = [];
|
|
1046
1060
|
let currentIndex = 0;
|
|
1047
1061
|
for (const line of lines.slice(optionStartIdx)) {
|
|
@@ -1124,6 +1138,22 @@ var OutputService = class _OutputService {
|
|
|
1124
1138
|
});
|
|
1125
1139
|
this.pollTimer = setInterval(() => this.tick(), _OutputService.POLL_MS);
|
|
1126
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
|
+
}
|
|
1127
1157
|
push(raw) {
|
|
1128
1158
|
if (!this.active) return;
|
|
1129
1159
|
this.rawBuffer += raw;
|
|
@@ -1240,12 +1270,162 @@ var OutputService = class _OutputService {
|
|
|
1240
1270
|
}
|
|
1241
1271
|
};
|
|
1242
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
|
+
|
|
1243
1423
|
// src/commands/start.ts
|
|
1244
1424
|
function saveFilesTemp(files) {
|
|
1245
1425
|
return files.filter(({ base64 }) => base64 && base64.length > 0).map(({ filename, base64 }) => {
|
|
1246
1426
|
const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
|
|
1247
|
-
const tmpPath =
|
|
1248
|
-
|
|
1427
|
+
const tmpPath = path5.join(os5.tmpdir(), `codeam-${(0, import_crypto.randomUUID)()}-${safeName}`);
|
|
1428
|
+
fs5.writeFileSync(tmpPath, Buffer.from(base64, "base64"));
|
|
1249
1429
|
return tmpPath;
|
|
1250
1430
|
});
|
|
1251
1431
|
}
|
|
@@ -1267,7 +1447,7 @@ async function start() {
|
|
|
1267
1447
|
outputSvc.newTurn();
|
|
1268
1448
|
claude.sendCommand(prompt);
|
|
1269
1449
|
}
|
|
1270
|
-
const relay = new CommandRelayService(pluginId, (cmd) => {
|
|
1450
|
+
const relay = new CommandRelayService(pluginId, async (cmd) => {
|
|
1271
1451
|
switch (cmd.type) {
|
|
1272
1452
|
case "start_task": {
|
|
1273
1453
|
const prompt = cmd.payload.prompt;
|
|
@@ -1281,7 +1461,7 @@ async function start() {
|
|
|
1281
1461
|
setTimeout(() => {
|
|
1282
1462
|
for (const p2 of paths) {
|
|
1283
1463
|
try {
|
|
1284
|
-
|
|
1464
|
+
fs5.unlinkSync(p2);
|
|
1285
1465
|
} catch {
|
|
1286
1466
|
}
|
|
1287
1467
|
}
|
|
@@ -1310,6 +1490,15 @@ async function start() {
|
|
|
1310
1490
|
case "stop_task":
|
|
1311
1491
|
claude.interrupt();
|
|
1312
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
|
+
}
|
|
1313
1502
|
}
|
|
1314
1503
|
});
|
|
1315
1504
|
ws.addHandler({
|
|
@@ -1333,7 +1522,7 @@ async function start() {
|
|
|
1333
1522
|
setTimeout(() => {
|
|
1334
1523
|
for (const p2 of paths) {
|
|
1335
1524
|
try {
|
|
1336
|
-
|
|
1525
|
+
fs5.unlinkSync(p2);
|
|
1337
1526
|
} catch {
|
|
1338
1527
|
}
|
|
1339
1528
|
}
|
|
@@ -1354,6 +1543,15 @@ async function start() {
|
|
|
1354
1543
|
claude.sendEscape();
|
|
1355
1544
|
} else if (cmdType === "stop_task") {
|
|
1356
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
|
+
}
|
|
1357
1555
|
}
|
|
1358
1556
|
}
|
|
1359
1557
|
});
|
|
@@ -1381,6 +1579,11 @@ async function start() {
|
|
|
1381
1579
|
}
|
|
1382
1580
|
process.once("SIGINT", sigintHandler);
|
|
1383
1581
|
claude.spawn();
|
|
1582
|
+
const historySvc = new HistoryService(pluginId, process.cwd());
|
|
1583
|
+
setTimeout(() => {
|
|
1584
|
+
historySvc.load().catch(() => {
|
|
1585
|
+
});
|
|
1586
|
+
}, 2e3);
|
|
1384
1587
|
}
|
|
1385
1588
|
|
|
1386
1589
|
// src/commands/pair.ts
|