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.
- package/dist/index.js +62 -20
- package/package.json +1 -1
- 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 (
|
|
528
|
-
buf.push("\r");
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
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
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
|
-
//
|
|
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 (
|
|
696
|
-
buf.push("\r");
|
|
697
|
-
|
|
698
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
887
|
-
|
|
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;
|
|
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;
|