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.
Files changed (2) hide show
  1. package/dist/index.js +265 -9
  2. 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.7",
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
- this.proc = (0, import_child_process.spawn)(shell, ["-c", `exec ${claudeCmd}`], {
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) claude.sendCommand(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) claude.sendCommand(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) claude.sendCommand(prompt);
897
+ if (prompt) sendPrompt(prompt);
647
898
  } else if (cmdType === "provide_input") {
648
899
  const input = inner.input;
649
- if (input) claude.sendCommand(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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Remote control Claude Code from your mobile device",
5
5
  "main": "dist/index.js",
6
6
  "bin": {