codeam-cli 1.0.7 → 1.0.9
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 +265 -9
- 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.9",
|
|
114
114
|
description: "Remote control Claude Code from your mobile device",
|
|
115
115
|
main: "dist/index.js",
|
|
116
116
|
bin: {
|
|
@@ -510,13 +510,70 @@ var CommandRelayService = class {
|
|
|
510
510
|
// src/services/claude.service.ts
|
|
511
511
|
var import_child_process = require("child_process");
|
|
512
512
|
var fs2 = __toESM(require("fs"));
|
|
513
|
+
var os3 = __toESM(require("os"));
|
|
513
514
|
var path2 = __toESM(require("path"));
|
|
515
|
+
var PYTHON_PTY_HELPER = `import os,pty,sys,select,signal,struct,fcntl,termios,errno
|
|
516
|
+
m,s=pty.openpty()
|
|
517
|
+
try:
|
|
518
|
+
cols=int(os.environ.get('COLUMNS','220'))
|
|
519
|
+
rows=int(os.environ.get('LINES','50'))
|
|
520
|
+
fcntl.ioctl(s,termios.TIOCSWINSZ,struct.pack('HHHH',rows,cols,0,0))
|
|
521
|
+
except Exception:pass
|
|
522
|
+
pid=os.fork()
|
|
523
|
+
if pid==0:
|
|
524
|
+
os.close(m)
|
|
525
|
+
os.setsid()
|
|
526
|
+
try:fcntl.ioctl(s,termios.TIOCSCTTY,0)
|
|
527
|
+
except Exception:pass
|
|
528
|
+
for fd in[0,1,2]:os.dup2(s,fd)
|
|
529
|
+
if s>2:os.close(s)
|
|
530
|
+
os.execvp(sys.argv[1],sys.argv[1:])
|
|
531
|
+
sys.exit(127)
|
|
532
|
+
os.close(s)
|
|
533
|
+
done=[False]
|
|
534
|
+
def onchld(n,f):
|
|
535
|
+
try:os.waitpid(pid,os.WNOHANG)
|
|
536
|
+
except Exception:pass
|
|
537
|
+
done[0]=True
|
|
538
|
+
def onwinch(n,f):
|
|
539
|
+
try:
|
|
540
|
+
sz=os.get_terminal_size(2)
|
|
541
|
+
fcntl.ioctl(m,termios.TIOCSWINSZ,struct.pack('HHHH',sz.lines,sz.columns,0,0))
|
|
542
|
+
except Exception:pass
|
|
543
|
+
signal.signal(signal.SIGCHLD,onchld)
|
|
544
|
+
signal.signal(signal.SIGWINCH,onwinch)
|
|
545
|
+
i=sys.stdin.fileno()
|
|
546
|
+
o=sys.stdout.fileno()
|
|
547
|
+
while not done[0]:
|
|
548
|
+
try:r,_,_=select.select([i,m],[],[],0.1)
|
|
549
|
+
except OSError as e:
|
|
550
|
+
if e.errno==errno.EINTR:continue
|
|
551
|
+
break
|
|
552
|
+
if i in r:
|
|
553
|
+
try:
|
|
554
|
+
d=os.read(i,4096)
|
|
555
|
+
if d:os.write(m,d)
|
|
556
|
+
else:break
|
|
557
|
+
except OSError:break
|
|
558
|
+
if m in r:
|
|
559
|
+
try:
|
|
560
|
+
d=os.read(m,4096)
|
|
561
|
+
if d:os.write(o,d)
|
|
562
|
+
except OSError:done[0]=True
|
|
563
|
+
try:os.kill(pid,signal.SIGTERM)
|
|
564
|
+
except Exception:pass
|
|
565
|
+
try:
|
|
566
|
+
_,st=os.waitpid(pid,0)
|
|
567
|
+
sys.exit((st>>8)&0xFF)
|
|
568
|
+
except Exception:sys.exit(0)
|
|
569
|
+
`;
|
|
514
570
|
var ClaudeService = class {
|
|
515
571
|
constructor(opts) {
|
|
516
572
|
this.opts = opts;
|
|
517
573
|
}
|
|
518
574
|
opts;
|
|
519
575
|
proc = null;
|
|
576
|
+
helperPath = null;
|
|
520
577
|
spawn() {
|
|
521
578
|
if (this.proc) {
|
|
522
579
|
this.cleanup();
|
|
@@ -530,11 +587,73 @@ var ClaudeService = class {
|
|
|
530
587
|
process.exit(1);
|
|
531
588
|
}
|
|
532
589
|
const claudeCmd = findInPath("claude") ? "claude" : "claude-code";
|
|
590
|
+
if (process.platform === "win32") {
|
|
591
|
+
this.spawnDirect(claudeCmd);
|
|
592
|
+
} else {
|
|
593
|
+
this.spawnWithPty(claudeCmd);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* macOS / Linux: use a Python PTY helper to give Claude a real TTY.
|
|
598
|
+
* Falls back to direct spawn if python3 is not available.
|
|
599
|
+
*/
|
|
600
|
+
spawnWithPty(claudeCmd) {
|
|
601
|
+
const python = findInPath("python3") ?? findInPath("python");
|
|
602
|
+
if (!python) {
|
|
603
|
+
console.error(
|
|
604
|
+
" \xB7 python3 not found; mobile command injection may be limited.\n"
|
|
605
|
+
);
|
|
606
|
+
this.spawnDirect(claudeCmd);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
533
609
|
const shell = process.env.SHELL || "/bin/sh";
|
|
534
|
-
|
|
610
|
+
const cols = process.stdout.columns || 220;
|
|
611
|
+
const rows = process.stdout.rows || 50;
|
|
612
|
+
this.helperPath = path2.join(os3.tmpdir(), "codeam-pty-helper.py");
|
|
613
|
+
fs2.writeFileSync(this.helperPath, PYTHON_PTY_HELPER, { mode: 420 });
|
|
614
|
+
this.proc = (0, import_child_process.spawn)(python, [this.helperPath, shell, "-c", `exec ${claudeCmd}`], {
|
|
615
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
616
|
+
cwd: this.opts.cwd,
|
|
617
|
+
env: {
|
|
618
|
+
...process.env,
|
|
619
|
+
TERM: "xterm-256color",
|
|
620
|
+
COLUMNS: String(cols),
|
|
621
|
+
LINES: String(rows)
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
this.proc.on("error", (err) => {
|
|
625
|
+
console.error(
|
|
626
|
+
`
|
|
627
|
+
\u2717 Failed to launch Claude Code: ${err.message}
|
|
628
|
+
Make sure claude is correctly installed: npm install -g @anthropic-ai/claude-code
|
|
629
|
+
`
|
|
630
|
+
);
|
|
631
|
+
process.exit(1);
|
|
632
|
+
});
|
|
633
|
+
this.proc.stdout?.on("data", (chunk) => {
|
|
634
|
+
process.stdout.write(chunk);
|
|
635
|
+
this.opts.onData?.(chunk.toString("utf8"));
|
|
636
|
+
});
|
|
637
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
638
|
+
process.stdin.resume();
|
|
639
|
+
process.stdin.on("data", this.stdinHandler);
|
|
640
|
+
process.on("SIGWINCH", this.handleResize);
|
|
641
|
+
this.proc.on("exit", (code) => {
|
|
642
|
+
this.removeTempFile();
|
|
643
|
+
this.cleanup();
|
|
644
|
+
this.opts.onExit(code ?? 0);
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Windows (or Python-unavailable) fallback: direct spawn without PTY.
|
|
649
|
+
* Mobile command injection is limited on Windows.
|
|
650
|
+
*/
|
|
651
|
+
spawnDirect(claudeCmd) {
|
|
652
|
+
this.proc = (0, import_child_process.spawn)(claudeCmd, [], {
|
|
535
653
|
stdio: ["pipe", "inherit", "inherit"],
|
|
536
654
|
cwd: this.opts.cwd,
|
|
537
|
-
env: process.env
|
|
655
|
+
env: process.env,
|
|
656
|
+
shell: true
|
|
538
657
|
});
|
|
539
658
|
this.proc.on("error", (err) => {
|
|
540
659
|
console.error(
|
|
@@ -545,6 +664,7 @@ var ClaudeService = class {
|
|
|
545
664
|
);
|
|
546
665
|
process.exit(1);
|
|
547
666
|
});
|
|
667
|
+
this.proc.stdin?.write("");
|
|
548
668
|
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
549
669
|
process.stdin.resume();
|
|
550
670
|
process.stdin.on("data", this.stdinHandler);
|
|
@@ -554,16 +674,17 @@ var ClaudeService = class {
|
|
|
554
674
|
this.opts.onExit(code ?? 0);
|
|
555
675
|
});
|
|
556
676
|
}
|
|
557
|
-
/** Send a command to Claude's stdin (remote control from mobile) */
|
|
677
|
+
/** Send a command to Claude's stdin (remote control from mobile). */
|
|
558
678
|
sendCommand(text) {
|
|
559
679
|
this.proc?.stdin?.write(text + "\r");
|
|
560
680
|
}
|
|
561
|
-
/** Send Ctrl+C to Claude */
|
|
681
|
+
/** Send Ctrl+C to Claude. */
|
|
562
682
|
interrupt() {
|
|
563
683
|
this.proc?.stdin?.write("");
|
|
564
684
|
}
|
|
565
685
|
kill() {
|
|
566
686
|
this.proc?.kill();
|
|
687
|
+
this.removeTempFile();
|
|
567
688
|
this.cleanup();
|
|
568
689
|
}
|
|
569
690
|
stdinHandler = (chunk) => {
|
|
@@ -587,6 +708,15 @@ var ClaudeService = class {
|
|
|
587
708
|
}
|
|
588
709
|
}
|
|
589
710
|
}
|
|
711
|
+
removeTempFile() {
|
|
712
|
+
if (this.helperPath) {
|
|
713
|
+
try {
|
|
714
|
+
fs2.unlinkSync(this.helperPath);
|
|
715
|
+
} catch {
|
|
716
|
+
}
|
|
717
|
+
this.helperPath = null;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
590
720
|
};
|
|
591
721
|
function findInPath(name) {
|
|
592
722
|
const dirs = (process.env.PATH ?? "").split(path2.delimiter);
|
|
@@ -601,6 +731,121 @@ function findInPath(name) {
|
|
|
601
731
|
return null;
|
|
602
732
|
}
|
|
603
733
|
|
|
734
|
+
// src/services/output.service.ts
|
|
735
|
+
var https2 = __toESM(require("https"));
|
|
736
|
+
var http2 = __toESM(require("http"));
|
|
737
|
+
var API_BASE4 = process.env.CODEAM_API_URL ?? "https://codeagent-mobile-api.vercel.app";
|
|
738
|
+
function stripAnsi(raw) {
|
|
739
|
+
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");
|
|
740
|
+
}
|
|
741
|
+
var OutputService = class _OutputService {
|
|
742
|
+
constructor(sessionId, pluginId) {
|
|
743
|
+
this.sessionId = sessionId;
|
|
744
|
+
this.pluginId = pluginId;
|
|
745
|
+
}
|
|
746
|
+
sessionId;
|
|
747
|
+
pluginId;
|
|
748
|
+
buffer = "";
|
|
749
|
+
flushTimer = null;
|
|
750
|
+
streamTimer = null;
|
|
751
|
+
active = false;
|
|
752
|
+
static STREAM_INTERVAL_MS = 1e3;
|
|
753
|
+
static DONE_DEBOUNCE_MS = 2500;
|
|
754
|
+
/**
|
|
755
|
+
* Call before sending a command from mobile.
|
|
756
|
+
* Clears previous output and pushes a new_turn event so the mobile app
|
|
757
|
+
* shows the typing indicator.
|
|
758
|
+
*/
|
|
759
|
+
async newTurn() {
|
|
760
|
+
this.stopTimers();
|
|
761
|
+
this.buffer = "";
|
|
762
|
+
this.active = true;
|
|
763
|
+
await this.postChunk({ clear: true });
|
|
764
|
+
await this.postChunk({ type: "new_turn", content: "", done: false });
|
|
765
|
+
this.startStreamTimer();
|
|
766
|
+
}
|
|
767
|
+
/** Feed raw terminal output from Claude (called on every stdout chunk). */
|
|
768
|
+
push(raw) {
|
|
769
|
+
if (!this.active) return;
|
|
770
|
+
const text = stripAnsi(raw).replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n");
|
|
771
|
+
if (!text.trim()) return;
|
|
772
|
+
this.buffer += text;
|
|
773
|
+
this.resetDoneDebounce();
|
|
774
|
+
}
|
|
775
|
+
dispose() {
|
|
776
|
+
this.stopTimers();
|
|
777
|
+
this.active = false;
|
|
778
|
+
}
|
|
779
|
+
startStreamTimer() {
|
|
780
|
+
this.streamTimer = setInterval(() => {
|
|
781
|
+
const text = this.buffer;
|
|
782
|
+
if (text.trim()) {
|
|
783
|
+
this.postChunk({ type: "text", content: text, done: false }).catch(() => {
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
}, _OutputService.STREAM_INTERVAL_MS);
|
|
787
|
+
}
|
|
788
|
+
resetDoneDebounce() {
|
|
789
|
+
if (this.flushTimer) clearTimeout(this.flushTimer);
|
|
790
|
+
this.flushTimer = setTimeout(() => {
|
|
791
|
+
this.flushTimer = null;
|
|
792
|
+
this.stopTimers();
|
|
793
|
+
const text = this.buffer;
|
|
794
|
+
this.buffer = "";
|
|
795
|
+
this.active = false;
|
|
796
|
+
if (text.trim()) {
|
|
797
|
+
this.postChunk({ type: "text", content: text, done: true }).catch(() => {
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
}, _OutputService.DONE_DEBOUNCE_MS);
|
|
801
|
+
}
|
|
802
|
+
stopTimers() {
|
|
803
|
+
if (this.flushTimer) {
|
|
804
|
+
clearTimeout(this.flushTimer);
|
|
805
|
+
this.flushTimer = null;
|
|
806
|
+
}
|
|
807
|
+
if (this.streamTimer) {
|
|
808
|
+
clearInterval(this.streamTimer);
|
|
809
|
+
this.streamTimer = null;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
postChunk(body) {
|
|
813
|
+
return new Promise((resolve) => {
|
|
814
|
+
const payload = JSON.stringify({
|
|
815
|
+
sessionId: this.sessionId,
|
|
816
|
+
pluginId: this.pluginId,
|
|
817
|
+
...body
|
|
818
|
+
});
|
|
819
|
+
const u = new URL(`${API_BASE4}/api/commands/output`);
|
|
820
|
+
const transport = u.protocol === "https:" ? https2 : http2;
|
|
821
|
+
const req = transport.request(
|
|
822
|
+
{
|
|
823
|
+
hostname: u.hostname,
|
|
824
|
+
port: u.port || (u.protocol === "https:" ? 443 : 80),
|
|
825
|
+
path: u.pathname,
|
|
826
|
+
method: "POST",
|
|
827
|
+
headers: {
|
|
828
|
+
"Content-Type": "application/json",
|
|
829
|
+
"Content-Length": Buffer.byteLength(payload)
|
|
830
|
+
},
|
|
831
|
+
timeout: 8e3
|
|
832
|
+
},
|
|
833
|
+
(res) => {
|
|
834
|
+
res.resume();
|
|
835
|
+
resolve();
|
|
836
|
+
}
|
|
837
|
+
);
|
|
838
|
+
req.on("error", () => resolve());
|
|
839
|
+
req.on("timeout", () => {
|
|
840
|
+
req.destroy();
|
|
841
|
+
resolve();
|
|
842
|
+
});
|
|
843
|
+
req.write(payload);
|
|
844
|
+
req.end();
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
|
|
604
849
|
// src/commands/start.ts
|
|
605
850
|
async function start() {
|
|
606
851
|
showIntro();
|
|
@@ -615,16 +860,22 @@ async function start() {
|
|
|
615
860
|
showInfo(`${session.userName} \xB7 ${import_picocolors2.default.cyan(session.plan)}`);
|
|
616
861
|
showInfo("Launching Claude Code...\n");
|
|
617
862
|
const ws = new WebSocketService(session.id, pluginId);
|
|
863
|
+
const outputSvc = new OutputService(session.id, pluginId);
|
|
864
|
+
function sendPrompt(prompt) {
|
|
865
|
+
outputSvc.newTurn().catch(() => {
|
|
866
|
+
});
|
|
867
|
+
claude.sendCommand(prompt);
|
|
868
|
+
}
|
|
618
869
|
const relay = new CommandRelayService(pluginId, (cmd) => {
|
|
619
870
|
switch (cmd.type) {
|
|
620
871
|
case "start_task": {
|
|
621
872
|
const prompt = cmd.payload.prompt;
|
|
622
|
-
if (prompt)
|
|
873
|
+
if (prompt) sendPrompt(prompt);
|
|
623
874
|
break;
|
|
624
875
|
}
|
|
625
876
|
case "provide_input": {
|
|
626
877
|
const input = cmd.payload.input;
|
|
627
|
-
if (input)
|
|
878
|
+
if (input) sendPrompt(input);
|
|
628
879
|
break;
|
|
629
880
|
}
|
|
630
881
|
case "stop_task":
|
|
@@ -643,10 +894,10 @@ async function start() {
|
|
|
643
894
|
const inner = payload.payload ?? {};
|
|
644
895
|
if (cmdType === "start_task") {
|
|
645
896
|
const prompt = inner.prompt;
|
|
646
|
-
if (prompt)
|
|
897
|
+
if (prompt) sendPrompt(prompt);
|
|
647
898
|
} else if (cmdType === "provide_input") {
|
|
648
899
|
const input = inner.input;
|
|
649
|
-
if (input)
|
|
900
|
+
if (input) sendPrompt(input);
|
|
650
901
|
} else if (cmdType === "stop_task") {
|
|
651
902
|
claude.interrupt();
|
|
652
903
|
}
|
|
@@ -656,8 +907,12 @@ async function start() {
|
|
|
656
907
|
relay.start();
|
|
657
908
|
const claude = new ClaudeService({
|
|
658
909
|
cwd: process.cwd(),
|
|
910
|
+
onData(raw) {
|
|
911
|
+
outputSvc.push(raw);
|
|
912
|
+
},
|
|
659
913
|
onExit(code) {
|
|
660
914
|
process.removeListener("SIGINT", sigintHandler);
|
|
915
|
+
outputSvc.dispose();
|
|
661
916
|
relay.stop();
|
|
662
917
|
ws.disconnect();
|
|
663
918
|
process.exit(code);
|
|
@@ -665,6 +920,7 @@ async function start() {
|
|
|
665
920
|
});
|
|
666
921
|
function sigintHandler() {
|
|
667
922
|
claude.kill();
|
|
923
|
+
outputSvc.dispose();
|
|
668
924
|
relay.stop();
|
|
669
925
|
ws.disconnect();
|
|
670
926
|
process.exit(0);
|