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