codeam-cli 1.0.6 → 1.0.8
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 +203 -10
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -110,7 +110,7 @@ var import_picocolors = __toESM(require("picocolors"));
|
|
|
110
110
|
// package.json
|
|
111
111
|
var package_default = {
|
|
112
112
|
name: "codeam-cli",
|
|
113
|
-
version: "1.0.
|
|
113
|
+
version: "1.0.8",
|
|
114
114
|
description: "Remote control Claude Code from your mobile device",
|
|
115
115
|
main: "dist/index.js",
|
|
116
116
|
bin: {
|
|
@@ -290,7 +290,7 @@ async function requestCode(pluginId) {
|
|
|
290
290
|
const result = await _transport.postJson(`${API_BASE2}/api/pairing/code`, {
|
|
291
291
|
pluginId,
|
|
292
292
|
ideName: "Terminal (codeam-cli)",
|
|
293
|
-
ideVersion:
|
|
293
|
+
ideVersion: package_default.version,
|
|
294
294
|
hostname: os2.hostname()
|
|
295
295
|
});
|
|
296
296
|
const data = result?.data;
|
|
@@ -530,11 +530,69 @@ var ClaudeService = class {
|
|
|
530
530
|
process.exit(1);
|
|
531
531
|
}
|
|
532
532
|
const claudeCmd = findInPath("claude") ? "claude" : "claude-code";
|
|
533
|
+
if (process.platform === "win32") {
|
|
534
|
+
this.spawnDirect(claudeCmd);
|
|
535
|
+
} else {
|
|
536
|
+
this.spawnWithPty(claudeCmd);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* macOS / Linux: wrap Claude in `script` to give it a real PTY.
|
|
541
|
+
*
|
|
542
|
+
* Why: Claude Code (React Ink) checks stdin.isTTY. When stdin is a pipe it
|
|
543
|
+
* shows a "no stdin data in 3s" warning and then stops reading stdin entirely.
|
|
544
|
+
* `script` creates a PTY for the child so Claude sees stdin as a terminal,
|
|
545
|
+
* while the parent (`codeam`) still controls script's stdin/stdout as pipes —
|
|
546
|
+
* enabling mobile command injection and output capture.
|
|
547
|
+
*/
|
|
548
|
+
spawnWithPty(claudeCmd) {
|
|
533
549
|
const shell = process.env.SHELL || "/bin/sh";
|
|
534
|
-
|
|
550
|
+
const cols = process.stdout.columns || 220;
|
|
551
|
+
const rows = process.stdout.rows || 50;
|
|
552
|
+
const scriptArgs = buildScriptArgs(shell, claudeCmd);
|
|
553
|
+
this.proc = (0, import_child_process.spawn)("script", scriptArgs, {
|
|
554
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
555
|
+
cwd: this.opts.cwd,
|
|
556
|
+
env: {
|
|
557
|
+
...process.env,
|
|
558
|
+
TERM: "xterm-256color",
|
|
559
|
+
COLUMNS: String(cols),
|
|
560
|
+
LINES: String(rows)
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
this.proc.on("error", (err) => {
|
|
564
|
+
console.error(
|
|
565
|
+
`
|
|
566
|
+
\u2717 Failed to launch Claude Code: ${err.message}
|
|
567
|
+
Make sure claude is correctly installed: npm install -g @anthropic-ai/claude-code
|
|
568
|
+
`
|
|
569
|
+
);
|
|
570
|
+
process.exit(1);
|
|
571
|
+
});
|
|
572
|
+
this.proc.stdout?.on("data", (chunk) => {
|
|
573
|
+
process.stdout.write(chunk);
|
|
574
|
+
this.opts.onData?.(chunk.toString("utf8"));
|
|
575
|
+
});
|
|
576
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
577
|
+
process.stdin.resume();
|
|
578
|
+
process.stdin.on("data", this.stdinHandler);
|
|
579
|
+
process.on("SIGWINCH", this.handleResize);
|
|
580
|
+
this.proc.on("exit", (code) => {
|
|
581
|
+
this.cleanup();
|
|
582
|
+
this.opts.onExit(code ?? 0);
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Windows fallback: direct spawn without PTY.
|
|
587
|
+
* Mobile command injection is limited (Claude may ignore pipe stdin after 3 s),
|
|
588
|
+
* but the terminal session still works for manual use.
|
|
589
|
+
*/
|
|
590
|
+
spawnDirect(claudeCmd) {
|
|
591
|
+
this.proc = (0, import_child_process.spawn)(claudeCmd, [], {
|
|
535
592
|
stdio: ["pipe", "inherit", "inherit"],
|
|
536
593
|
cwd: this.opts.cwd,
|
|
537
|
-
env: process.env
|
|
594
|
+
env: process.env,
|
|
595
|
+
shell: true
|
|
538
596
|
});
|
|
539
597
|
this.proc.on("error", (err) => {
|
|
540
598
|
console.error(
|
|
@@ -545,6 +603,7 @@ var ClaudeService = class {
|
|
|
545
603
|
);
|
|
546
604
|
process.exit(1);
|
|
547
605
|
});
|
|
606
|
+
this.proc.stdin?.write("");
|
|
548
607
|
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
549
608
|
process.stdin.resume();
|
|
550
609
|
process.stdin.on("data", this.stdinHandler);
|
|
@@ -554,11 +613,11 @@ var ClaudeService = class {
|
|
|
554
613
|
this.opts.onExit(code ?? 0);
|
|
555
614
|
});
|
|
556
615
|
}
|
|
557
|
-
/** Send a command to Claude's stdin (remote control from mobile) */
|
|
616
|
+
/** Send a command to Claude's stdin (remote control from mobile). */
|
|
558
617
|
sendCommand(text) {
|
|
559
618
|
this.proc?.stdin?.write(text + "\r");
|
|
560
619
|
}
|
|
561
|
-
/** Send Ctrl+C to Claude */
|
|
620
|
+
/** Send Ctrl+C to Claude. */
|
|
562
621
|
interrupt() {
|
|
563
622
|
this.proc?.stdin?.write("");
|
|
564
623
|
}
|
|
@@ -588,6 +647,14 @@ var ClaudeService = class {
|
|
|
588
647
|
}
|
|
589
648
|
}
|
|
590
649
|
};
|
|
650
|
+
function buildScriptArgs(shell, claudeCmd) {
|
|
651
|
+
const innerCmd = `exec ${claudeCmd}`;
|
|
652
|
+
if (process.platform === "darwin") {
|
|
653
|
+
return ["-q", "/dev/null", shell, "-c", innerCmd];
|
|
654
|
+
}
|
|
655
|
+
const shellCmd = `${shell} -c ${JSON.stringify(innerCmd)}`;
|
|
656
|
+
return ["-q", "-e", "-c", shellCmd, "/dev/null"];
|
|
657
|
+
}
|
|
591
658
|
function findInPath(name) {
|
|
592
659
|
const dirs = (process.env.PATH ?? "").split(path2.delimiter);
|
|
593
660
|
for (const dir of dirs) {
|
|
@@ -601,6 +668,121 @@ function findInPath(name) {
|
|
|
601
668
|
return null;
|
|
602
669
|
}
|
|
603
670
|
|
|
671
|
+
// src/services/output.service.ts
|
|
672
|
+
var https2 = __toESM(require("https"));
|
|
673
|
+
var http2 = __toESM(require("http"));
|
|
674
|
+
var API_BASE4 = process.env.CODEAM_API_URL ?? "https://codeagent-mobile-api.vercel.app";
|
|
675
|
+
function stripAnsi(raw) {
|
|
676
|
+
return raw.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "").replace(/\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)/g, "").replace(/\x1B[@-Z\\-_]/g, "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
677
|
+
}
|
|
678
|
+
var OutputService = class _OutputService {
|
|
679
|
+
constructor(sessionId, pluginId) {
|
|
680
|
+
this.sessionId = sessionId;
|
|
681
|
+
this.pluginId = pluginId;
|
|
682
|
+
}
|
|
683
|
+
sessionId;
|
|
684
|
+
pluginId;
|
|
685
|
+
buffer = "";
|
|
686
|
+
flushTimer = null;
|
|
687
|
+
streamTimer = null;
|
|
688
|
+
active = false;
|
|
689
|
+
static STREAM_INTERVAL_MS = 1e3;
|
|
690
|
+
static DONE_DEBOUNCE_MS = 2500;
|
|
691
|
+
/**
|
|
692
|
+
* Call before sending a command from mobile.
|
|
693
|
+
* Clears previous output and pushes a new_turn event so the mobile app
|
|
694
|
+
* shows the typing indicator.
|
|
695
|
+
*/
|
|
696
|
+
async newTurn() {
|
|
697
|
+
this.stopTimers();
|
|
698
|
+
this.buffer = "";
|
|
699
|
+
this.active = true;
|
|
700
|
+
await this.postChunk({ clear: true });
|
|
701
|
+
await this.postChunk({ type: "new_turn", content: "", done: false });
|
|
702
|
+
this.startStreamTimer();
|
|
703
|
+
}
|
|
704
|
+
/** Feed raw terminal output from Claude (called on every stdout chunk). */
|
|
705
|
+
push(raw) {
|
|
706
|
+
if (!this.active) return;
|
|
707
|
+
const text = stripAnsi(raw).replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n");
|
|
708
|
+
if (!text.trim()) return;
|
|
709
|
+
this.buffer += text;
|
|
710
|
+
this.resetDoneDebounce();
|
|
711
|
+
}
|
|
712
|
+
dispose() {
|
|
713
|
+
this.stopTimers();
|
|
714
|
+
this.active = false;
|
|
715
|
+
}
|
|
716
|
+
startStreamTimer() {
|
|
717
|
+
this.streamTimer = setInterval(() => {
|
|
718
|
+
const text = this.buffer;
|
|
719
|
+
if (text.trim()) {
|
|
720
|
+
this.postChunk({ type: "text", content: text, done: false }).catch(() => {
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
}, _OutputService.STREAM_INTERVAL_MS);
|
|
724
|
+
}
|
|
725
|
+
resetDoneDebounce() {
|
|
726
|
+
if (this.flushTimer) clearTimeout(this.flushTimer);
|
|
727
|
+
this.flushTimer = setTimeout(() => {
|
|
728
|
+
this.flushTimer = null;
|
|
729
|
+
this.stopTimers();
|
|
730
|
+
const text = this.buffer;
|
|
731
|
+
this.buffer = "";
|
|
732
|
+
this.active = false;
|
|
733
|
+
if (text.trim()) {
|
|
734
|
+
this.postChunk({ type: "text", content: text, done: true }).catch(() => {
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
}, _OutputService.DONE_DEBOUNCE_MS);
|
|
738
|
+
}
|
|
739
|
+
stopTimers() {
|
|
740
|
+
if (this.flushTimer) {
|
|
741
|
+
clearTimeout(this.flushTimer);
|
|
742
|
+
this.flushTimer = null;
|
|
743
|
+
}
|
|
744
|
+
if (this.streamTimer) {
|
|
745
|
+
clearInterval(this.streamTimer);
|
|
746
|
+
this.streamTimer = null;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
postChunk(body) {
|
|
750
|
+
return new Promise((resolve) => {
|
|
751
|
+
const payload = JSON.stringify({
|
|
752
|
+
sessionId: this.sessionId,
|
|
753
|
+
pluginId: this.pluginId,
|
|
754
|
+
...body
|
|
755
|
+
});
|
|
756
|
+
const u = new URL(`${API_BASE4}/api/commands/output`);
|
|
757
|
+
const transport = u.protocol === "https:" ? https2 : http2;
|
|
758
|
+
const req = transport.request(
|
|
759
|
+
{
|
|
760
|
+
hostname: u.hostname,
|
|
761
|
+
port: u.port || (u.protocol === "https:" ? 443 : 80),
|
|
762
|
+
path: u.pathname,
|
|
763
|
+
method: "POST",
|
|
764
|
+
headers: {
|
|
765
|
+
"Content-Type": "application/json",
|
|
766
|
+
"Content-Length": Buffer.byteLength(payload)
|
|
767
|
+
},
|
|
768
|
+
timeout: 8e3
|
|
769
|
+
},
|
|
770
|
+
(res) => {
|
|
771
|
+
res.resume();
|
|
772
|
+
resolve();
|
|
773
|
+
}
|
|
774
|
+
);
|
|
775
|
+
req.on("error", () => resolve());
|
|
776
|
+
req.on("timeout", () => {
|
|
777
|
+
req.destroy();
|
|
778
|
+
resolve();
|
|
779
|
+
});
|
|
780
|
+
req.write(payload);
|
|
781
|
+
req.end();
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
|
|
604
786
|
// src/commands/start.ts
|
|
605
787
|
async function start() {
|
|
606
788
|
showIntro();
|
|
@@ -615,16 +797,22 @@ async function start() {
|
|
|
615
797
|
showInfo(`${session.userName} \xB7 ${import_picocolors2.default.cyan(session.plan)}`);
|
|
616
798
|
showInfo("Launching Claude Code...\n");
|
|
617
799
|
const ws = new WebSocketService(session.id, pluginId);
|
|
800
|
+
const outputSvc = new OutputService(session.id, pluginId);
|
|
801
|
+
function sendPrompt(prompt) {
|
|
802
|
+
outputSvc.newTurn().catch(() => {
|
|
803
|
+
});
|
|
804
|
+
claude.sendCommand(prompt);
|
|
805
|
+
}
|
|
618
806
|
const relay = new CommandRelayService(pluginId, (cmd) => {
|
|
619
807
|
switch (cmd.type) {
|
|
620
808
|
case "start_task": {
|
|
621
809
|
const prompt = cmd.payload.prompt;
|
|
622
|
-
if (prompt)
|
|
810
|
+
if (prompt) sendPrompt(prompt);
|
|
623
811
|
break;
|
|
624
812
|
}
|
|
625
813
|
case "provide_input": {
|
|
626
814
|
const input = cmd.payload.input;
|
|
627
|
-
if (input)
|
|
815
|
+
if (input) sendPrompt(input);
|
|
628
816
|
break;
|
|
629
817
|
}
|
|
630
818
|
case "stop_task":
|
|
@@ -643,10 +831,10 @@ async function start() {
|
|
|
643
831
|
const inner = payload.payload ?? {};
|
|
644
832
|
if (cmdType === "start_task") {
|
|
645
833
|
const prompt = inner.prompt;
|
|
646
|
-
if (prompt)
|
|
834
|
+
if (prompt) sendPrompt(prompt);
|
|
647
835
|
} else if (cmdType === "provide_input") {
|
|
648
836
|
const input = inner.input;
|
|
649
|
-
if (input)
|
|
837
|
+
if (input) sendPrompt(input);
|
|
650
838
|
} else if (cmdType === "stop_task") {
|
|
651
839
|
claude.interrupt();
|
|
652
840
|
}
|
|
@@ -656,8 +844,12 @@ async function start() {
|
|
|
656
844
|
relay.start();
|
|
657
845
|
const claude = new ClaudeService({
|
|
658
846
|
cwd: process.cwd(),
|
|
847
|
+
onData(raw) {
|
|
848
|
+
outputSvc.push(raw);
|
|
849
|
+
},
|
|
659
850
|
onExit(code) {
|
|
660
851
|
process.removeListener("SIGINT", sigintHandler);
|
|
852
|
+
outputSvc.dispose();
|
|
661
853
|
relay.stop();
|
|
662
854
|
ws.disconnect();
|
|
663
855
|
process.exit(code);
|
|
@@ -665,6 +857,7 @@ async function start() {
|
|
|
665
857
|
});
|
|
666
858
|
function sigintHandler() {
|
|
667
859
|
claude.kill();
|
|
860
|
+
outputSvc.dispose();
|
|
668
861
|
relay.stop();
|
|
669
862
|
ws.disconnect();
|
|
670
863
|
process.exit(0);
|