groundcrew-cli 0.16.2 → 0.16.4

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 (3) hide show
  1. package/dist/index.js +62 -20
  2. package/package.json +1 -1
  3. package/src/index.ts +65 -22
package/dist/index.js CHANGED
@@ -41,6 +41,38 @@ function getGitContext() {
41
41
  return null;
42
42
  }
43
43
  }
44
+ function pasteFromClipboard(sessionDir) {
45
+ try {
46
+ const text = execFileSync("pbpaste", [], { encoding: "utf8", timeout: 1e3, stdio: ["pipe", "pipe", "pipe"] }).replace(/\r\n/g, "\n");
47
+ if (text) return { type: "text", text };
48
+ } catch {
49
+ }
50
+ if (process.platform !== "darwin") return null;
51
+ try {
52
+ const check = execFileSync("osascript", [
53
+ "-e",
54
+ 'try\nthe clipboard as \xABclass PNGf\xBB\nreturn "image"\non error\nreturn "none"\nend try'
55
+ ], { encoding: "utf8", timeout: 2e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
56
+ if (check !== "image") return null;
57
+ const attachDir = path.join(sessionDir, "attachments");
58
+ try {
59
+ execFileSync("mkdir", ["-p", attachDir]);
60
+ } catch {
61
+ }
62
+ const fname = `clipboard-${Date.now()}.png`;
63
+ const fpath = path.join(attachDir, fname);
64
+ execFileSync("osascript", ["-e", `
65
+ set theFile to POSIX file "${fpath}"
66
+ set imageData to the clipboard as \xABclass PNGf\xBB
67
+ set fp to open for access theFile with write permission
68
+ set eof fp to 0
69
+ write imageData to fp
70
+ close access fp`], { timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] });
71
+ return { type: "image", path: fpath };
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
44
76
  var GROUNDCREW_DIR = ".groundcrew";
45
77
  var SESSIONS_DIR = path.join(GROUNDCREW_DIR, "sessions");
46
78
  var ACTIVE_SESSIONS_FILE = path.join(GROUNDCREW_DIR, "active-sessions.json");
@@ -462,7 +494,7 @@ var CHAT_COMMANDS = [
462
494
  { cmd: "/clear", desc: "Clear pending tasks" },
463
495
  { cmd: "/exit", desc: "Exit chat" }
464
496
  ];
465
- function readMultilineInput(sessionId, projectName, gitCtx) {
497
+ function readMultilineInput(sessionId, projectName, gitCtx, sessionDir) {
466
498
  return new Promise((resolve) => {
467
499
  const lines = [""];
468
500
  let crow = 0;
@@ -521,13 +553,17 @@ function readMultilineInput(sessionId, projectName, gitCtx) {
521
553
  };
522
554
  const submit = () => {
523
555
  const text = fullText();
524
- const lastRow = lines.length - 1;
525
- const rowsDown = lastRow - crow;
526
556
  const buf = [];
527
- if (rowsDown > 0) buf.push(`\x1B[${rowsDown}B`);
528
- buf.push("\r");
529
- const endCol = padWidth + lines[lastRow].length;
530
- if (endCol > 0) buf.push(`\x1B[${endCol}C`);
557
+ if (lastTermRow > 0) buf.push(`\x1B[${lastTermRow}A`);
558
+ buf.push("\r\x1B[J");
559
+ for (let i = 0; i < lines.length; i++) {
560
+ if (i > 0) buf.push("\n");
561
+ if (i === 0) {
562
+ buf.push(dim(`[${sessionId}]`) + " " + bold(">") + " " + lines[i]);
563
+ } else {
564
+ buf.push(" ".repeat(padWidth) + lines[i]);
565
+ }
566
+ }
531
567
  buf.push("\n");
532
568
  process.stdout.write(buf.join(""));
533
569
  lastTermRow = 0;
@@ -676,15 +712,10 @@ function readMultilineInput(sessionId, projectName, gitCtx) {
676
712
  switch (codepoint) {
677
713
  case 99:
678
714
  if (fullText() || lines.length > 1 || lines[0].length > 0) {
679
- const lastRow = lines.length - 1;
680
- const rowsDown = lastRow - crow;
681
- if (rowsDown > 0) process.stdout.write(`\x1B[${rowsDown}B`);
682
- process.stdout.write("\r\n");
683
715
  lines.length = 0;
684
716
  lines.push("");
685
717
  crow = 0;
686
718
  ccol = 0;
687
- lastTermRow = 0;
688
719
  render();
689
720
  } else {
690
721
  process.stdout.write("\r\n");
@@ -742,6 +773,15 @@ function readMultilineInput(sessionId, projectName, gitCtx) {
742
773
  render();
743
774
  i += seqLen;
744
775
  continue;
776
+ case 118:
777
+ {
778
+ const clip = pasteFromClipboard(sessionDir);
779
+ if (clip?.type === "text") insertText(clip.text);
780
+ else if (clip?.type === "image") insertText(`[\u{1F4F7} ${clip.path}]`);
781
+ render();
782
+ }
783
+ i += seqLen;
784
+ continue;
745
785
  default:
746
786
  break;
747
787
  }
@@ -760,17 +800,11 @@ function readMultilineInput(sessionId, projectName, gitCtx) {
760
800
  continue;
761
801
  }
762
802
  if (str[i] === "") {
763
- const hasText = fullText();
764
- if (hasText || lines.length > 1 || lines[0].length > 0) {
765
- const lastRow = lines.length - 1;
766
- const rowsDown = lastRow - crow;
767
- if (rowsDown > 0) process.stdout.write(`\x1B[${rowsDown}B`);
768
- process.stdout.write("\r\n");
803
+ if (fullText() || lines.length > 1 || lines[0].length > 0) {
769
804
  lines.length = 0;
770
805
  lines.push("");
771
806
  crow = 0;
772
807
  ccol = 0;
773
- lastTermRow = 0;
774
808
  render();
775
809
  } else {
776
810
  process.stdout.write("\r\n");
@@ -834,6 +868,14 @@ function readMultilineInput(sessionId, projectName, gitCtx) {
834
868
  i++;
835
869
  continue;
836
870
  }
871
+ if (str[i] === "") {
872
+ const clip = pasteFromClipboard(sessionDir);
873
+ if (clip?.type === "text") insertText(clip.text);
874
+ else if (clip?.type === "image") insertText(`[\u{1F4F7} ${clip.path}]`);
875
+ render();
876
+ i++;
877
+ continue;
878
+ }
837
879
  if (str[i] === "\n") {
838
880
  insertNewline();
839
881
  i++;
@@ -977,7 +1019,7 @@ async function chat(explicitSession) {
977
1019
  };
978
1020
  while (true) {
979
1021
  const gitCtx = getGitContext();
980
- const text = await readMultilineInput(current.id, projectName, gitCtx);
1022
+ const text = await readMultilineInput(current.id, projectName, gitCtx, current.dir);
981
1023
  if (text === null) exitChat();
982
1024
  if (!text) continue;
983
1025
  const trimmed = text.trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groundcrew-cli",
3
- "version": "0.16.2",
3
+ "version": "0.16.4",
4
4
  "description": "CLI companion for Groundcrew — queue tasks, send feedback, monitor your Copilot agent from another terminal.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -36,6 +36,38 @@ function getGitContext(): { branch: string; dirty: string } | null {
36
36
  }
37
37
  }
38
38
 
39
+ // Clipboard paste with image support (macOS)
40
+ function pasteFromClipboard(sessionDir: string): { type: "text"; text: string } | { type: "image"; path: string } | null {
41
+ // Try text first (fast)
42
+ try {
43
+ const text = execFileSync("pbpaste", [], { encoding: "utf8", timeout: 1000, stdio: ["pipe", "pipe", "pipe"] }).replace(/\r\n/g, "\n");
44
+ if (text) return { type: "text", text };
45
+ } catch { /* no text */ }
46
+
47
+ // Check for image in clipboard
48
+ if (process.platform !== "darwin") return null;
49
+ try {
50
+ const check = execFileSync("osascript", ["-e",
51
+ 'try\nthe clipboard as «class PNGf»\nreturn "image"\non error\nreturn "none"\nend try',
52
+ ], { encoding: "utf8", timeout: 2000, stdio: ["pipe", "pipe", "pipe"] }).trim();
53
+ if (check !== "image") return null;
54
+
55
+ // Save image to session attachments dir
56
+ const attachDir = path.join(sessionDir, "attachments");
57
+ try { execFileSync("mkdir", ["-p", attachDir]); } catch { /* exists */ }
58
+ const fname = `clipboard-${Date.now()}.png`;
59
+ const fpath = path.join(attachDir, fname);
60
+ execFileSync("osascript", ["-e", `
61
+ set theFile to POSIX file "${fpath}"
62
+ set imageData to the clipboard as «class PNGf»
63
+ set fp to open for access theFile with write permission
64
+ set eof fp to 0
65
+ write imageData to fp
66
+ close access fp`], { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
67
+ return { type: "image", path: fpath };
68
+ } catch { return null; }
69
+ }
70
+
39
71
  // Resolved at startup by resolveRoot() — git-aware project root discovery
40
72
  let GROUNDCREW_DIR = ".groundcrew";
41
73
  let SESSIONS_DIR = path.join(GROUNDCREW_DIR, "sessions");
@@ -607,7 +639,7 @@ const CHAT_COMMANDS: Array<{ cmd: string; desc: string }> = [
607
639
  * Tab: slash command completion
608
640
  * Paste: bracketed paste with multiline support
609
641
  */
610
- function readMultilineInput(sessionId: string, projectName: string, gitCtx: { branch: string; dirty: string } | null): Promise<string | null> {
642
+ function readMultilineInput(sessionId: string, projectName: string, gitCtx: { branch: string; dirty: string } | null, sessionDir: string): Promise<string | null> {
611
643
  return new Promise((resolve) => {
612
644
  const lines: string[] = [""];
613
645
  let crow = 0; // cursor row in lines[]
@@ -688,14 +720,20 @@ function readMultilineInput(sessionId: string, projectName: string, gitCtx: { br
688
720
 
689
721
  const submit = () => {
690
722
  const text = fullText();
691
- // Move cursor to end of input for clean output
692
- const lastRow = lines.length - 1;
693
- const rowsDown = lastRow - crow;
723
+ // Erase the separator + input area, then re-draw only the prompt (no separator in history)
694
724
  const buf: string[] = [];
695
- if (rowsDown > 0) buf.push(`\x1b[${rowsDown}B`);
696
- buf.push("\r");
697
- const endCol = padWidth + lines[lastRow].length;
698
- if (endCol > 0) buf.push(`\x1b[${endCol}C`);
725
+ if (lastTermRow > 0) buf.push(`\x1b[${lastTermRow}A`);
726
+ buf.push("\r\x1b[J"); // clear from separator line down
727
+
728
+ // Re-draw only the input lines (no separator)
729
+ for (let i = 0; i < lines.length; i++) {
730
+ if (i > 0) buf.push("\n");
731
+ if (i === 0) {
732
+ buf.push(dim(`[${sessionId}]`) + " " + bold(">") + " " + lines[i]);
733
+ } else {
734
+ buf.push(" ".repeat(padWidth) + lines[i]);
735
+ }
736
+ }
699
737
  buf.push("\n");
700
738
  process.stdout.write(buf.join(""));
701
739
  lastTermRow = 0;
@@ -830,12 +868,9 @@ function readMultilineInput(sessionId: string, projectName: string, gitCtx: { br
830
868
  switch (codepoint) {
831
869
  case 99: // Ctrl+C
832
870
  if (fullText() || lines.length > 1 || lines[0].length > 0) {
833
- const lastRow = lines.length - 1;
834
- const rowsDown = lastRow - crow;
835
- if (rowsDown > 0) process.stdout.write(`\x1b[${rowsDown}B`);
836
- process.stdout.write("\r\n");
871
+ // Clear input in-place (no scrollback residue)
837
872
  lines.length = 0; lines.push("");
838
- crow = 0; ccol = 0; lastTermRow = 0;
873
+ crow = 0; ccol = 0;
839
874
  render();
840
875
  } else {
841
876
  process.stdout.write("\r\n");
@@ -865,6 +900,12 @@ function readMultilineInput(sessionId: string, projectName: string, gitCtx: { br
865
900
  process.stdout.write("\x1b[2J\x1b[H");
866
901
  lastTermRow = 0; render();
867
902
  i += seqLen; continue;
903
+ case 118: // Ctrl+V — paste from clipboard
904
+ { const clip = pasteFromClipboard(sessionDir);
905
+ if (clip?.type === "text") insertText(clip.text);
906
+ else if (clip?.type === "image") insertText(`[📷 ${clip.path}]`);
907
+ render(); }
908
+ i += seqLen; continue;
868
909
  default: break;
869
910
  }
870
911
  }
@@ -883,15 +924,10 @@ function readMultilineInput(sessionId: string, projectName: string, gitCtx: { br
883
924
 
884
925
  // Ctrl+C — clear input or exit
885
926
  if (str[i] === "\x03") {
886
- const hasText = fullText();
887
- if (hasText || lines.length > 1 || lines[0].length > 0) {
888
- // Move past current rendering to below last line, start fresh
889
- const lastRow = lines.length - 1;
890
- const rowsDown = lastRow - crow;
891
- if (rowsDown > 0) process.stdout.write(`\x1b[${rowsDown}B`);
892
- process.stdout.write("\r\n");
927
+ if (fullText() || lines.length > 1 || lines[0].length > 0) {
928
+ // Clear input in-place (no scrollback residue)
893
929
  lines.length = 0; lines.push("");
894
- crow = 0; ccol = 0; lastTermRow = 0;
930
+ crow = 0; ccol = 0;
895
931
  render();
896
932
  } else {
897
933
  process.stdout.write("\r\n");
@@ -932,6 +968,13 @@ function readMultilineInput(sessionId: string, projectName: string, gitCtx: { br
932
968
  process.stdout.write("\x1b[2J\x1b[H");
933
969
  lastTermRow = 0; render(); i++; continue;
934
970
  }
971
+ // Ctrl+V — paste from clipboard (legacy byte)
972
+ if (str[i] === "\x16") {
973
+ const clip = pasteFromClipboard(sessionDir);
974
+ if (clip?.type === "text") insertText(clip.text);
975
+ else if (clip?.type === "image") insertText(`[📷 ${clip.path}]`);
976
+ render(); i++; continue;
977
+ }
935
978
 
936
979
  // Ctrl+J (LF, 0x0A) — newline (cross-terminal)
937
980
  if (str[i] === "\n") { insertNewline(); i++; continue; }
@@ -1093,7 +1136,7 @@ async function chat(explicitSession?: string): Promise<void> {
1093
1136
  while (true) {
1094
1137
  // Refresh git context each turn (branch may change between prompts)
1095
1138
  const gitCtx = getGitContext();
1096
- const text = await readMultilineInput(current.id, projectName, gitCtx);
1139
+ const text = await readMultilineInput(current.id, projectName, gitCtx, current.dir);
1097
1140
 
1098
1141
  if (text === null) exitChat();
1099
1142
  if (!text) continue;