groundcrew-cli 0.16.3 → 0.16.5
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 +66 -6
- package/package.json +1 -1
- package/src/index.ts +70 -9
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;
|
|
@@ -507,12 +539,23 @@ function readMultilineInput(sessionId, projectName, gitCtx) {
|
|
|
507
539
|
}
|
|
508
540
|
}
|
|
509
541
|
const lastRow = lines.length - 1;
|
|
510
|
-
const
|
|
511
|
-
|
|
542
|
+
const termRowsForLine = (i) => {
|
|
543
|
+
const lineLen = (i === 0 ? padWidth : padWidth) + lines[i].length;
|
|
544
|
+
return lineLen === 0 ? 1 : Math.max(1, Math.ceil(lineLen / termW));
|
|
545
|
+
};
|
|
546
|
+
let rowsBelowCursor = suggestionRows;
|
|
547
|
+
for (let i = lastRow; i > crow; i--) rowsBelowCursor += termRowsForLine(i);
|
|
548
|
+
const cursorLineTermRows = termRowsForLine(crow);
|
|
549
|
+
const cursorRowWithinLine = Math.floor((padWidth + ccol) / termW);
|
|
550
|
+
rowsBelowCursor += cursorLineTermRows - 1 - cursorRowWithinLine;
|
|
551
|
+
if (rowsBelowCursor > 0) buf.push(`\x1B[${rowsBelowCursor}A`);
|
|
512
552
|
buf.push("\r");
|
|
513
|
-
const col = padWidth + ccol;
|
|
553
|
+
const col = (padWidth + ccol) % termW;
|
|
514
554
|
if (col > 0) buf.push(`\x1B[${col}C`);
|
|
515
|
-
|
|
555
|
+
let rowsAbove = 1;
|
|
556
|
+
for (let i = 0; i < crow; i++) rowsAbove += termRowsForLine(i);
|
|
557
|
+
rowsAbove += cursorRowWithinLine;
|
|
558
|
+
lastTermRow = rowsAbove;
|
|
516
559
|
process.stdout.write(buf.join(""));
|
|
517
560
|
};
|
|
518
561
|
const finish = (result) => {
|
|
@@ -741,6 +784,15 @@ function readMultilineInput(sessionId, projectName, gitCtx) {
|
|
|
741
784
|
render();
|
|
742
785
|
i += seqLen;
|
|
743
786
|
continue;
|
|
787
|
+
case 118:
|
|
788
|
+
{
|
|
789
|
+
const clip = pasteFromClipboard(sessionDir);
|
|
790
|
+
if (clip?.type === "text") insertText(clip.text);
|
|
791
|
+
else if (clip?.type === "image") insertText(`[\u{1F4F7} ${clip.path}]`);
|
|
792
|
+
render();
|
|
793
|
+
}
|
|
794
|
+
i += seqLen;
|
|
795
|
+
continue;
|
|
744
796
|
default:
|
|
745
797
|
break;
|
|
746
798
|
}
|
|
@@ -827,6 +879,14 @@ function readMultilineInput(sessionId, projectName, gitCtx) {
|
|
|
827
879
|
i++;
|
|
828
880
|
continue;
|
|
829
881
|
}
|
|
882
|
+
if (str[i] === "") {
|
|
883
|
+
const clip = pasteFromClipboard(sessionDir);
|
|
884
|
+
if (clip?.type === "text") insertText(clip.text);
|
|
885
|
+
else if (clip?.type === "image") insertText(`[\u{1F4F7} ${clip.path}]`);
|
|
886
|
+
render();
|
|
887
|
+
i++;
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
830
890
|
if (str[i] === "\n") {
|
|
831
891
|
insertNewline();
|
|
832
892
|
i++;
|
|
@@ -970,7 +1030,7 @@ async function chat(explicitSession) {
|
|
|
970
1030
|
};
|
|
971
1031
|
while (true) {
|
|
972
1032
|
const gitCtx = getGitContext();
|
|
973
|
-
const text = await readMultilineInput(current.id, projectName, gitCtx);
|
|
1033
|
+
const text = await readMultilineInput(current.id, projectName, gitCtx, current.dir);
|
|
974
1034
|
if (text === null) exitChat();
|
|
975
1035
|
if (!text) continue;
|
|
976
1036
|
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[]
|
|
@@ -667,17 +699,33 @@ function readMultilineInput(sessionId: string, projectName: string, gitCtx: { br
|
|
|
667
699
|
|
|
668
700
|
// Position cursor at (crow, ccol)
|
|
669
701
|
const lastRow = lines.length - 1;
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
702
|
+
|
|
703
|
+
// Calculate actual terminal rows each line occupies (for wrapped lines)
|
|
704
|
+
const termRowsForLine = (i: number): number => {
|
|
705
|
+
const lineLen = (i === 0 ? padWidth : padWidth) + lines[i].length;
|
|
706
|
+
return lineLen === 0 ? 1 : Math.max(1, Math.ceil(lineLen / termW));
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
// Move up from the end of the last drawn line to the cursor position
|
|
710
|
+
// Count terminal rows below cursor line (remaining input lines + suggestions)
|
|
711
|
+
let rowsBelowCursor = suggestionRows;
|
|
712
|
+
for (let i = lastRow; i > crow; i--) rowsBelowCursor += termRowsForLine(i);
|
|
713
|
+
// Add any extra wrapped rows on the cursor line itself (below the cursor's row within wraps)
|
|
714
|
+
const cursorLineTermRows = termRowsForLine(crow);
|
|
715
|
+
const cursorRowWithinLine = Math.floor((padWidth + ccol) / termW);
|
|
716
|
+
rowsBelowCursor += (cursorLineTermRows - 1 - cursorRowWithinLine);
|
|
717
|
+
|
|
718
|
+
if (rowsBelowCursor > 0) buf.push(`\x1b[${rowsBelowCursor}A`);
|
|
673
719
|
|
|
674
720
|
buf.push("\r");
|
|
675
|
-
const col = padWidth + ccol;
|
|
721
|
+
const col = (padWidth + ccol) % termW;
|
|
676
722
|
if (col > 0) buf.push(`\x1b[${col}C`);
|
|
677
723
|
|
|
678
|
-
// lastTermRow = rows above cursor (separator + input lines above crow)
|
|
679
|
-
|
|
680
|
-
|
|
724
|
+
// lastTermRow = terminal rows above cursor (separator + wrapped input lines above crow + cursor's wrapped rows above)
|
|
725
|
+
let rowsAbove = 1; // separator line
|
|
726
|
+
for (let i = 0; i < crow; i++) rowsAbove += termRowsForLine(i);
|
|
727
|
+
rowsAbove += cursorRowWithinLine;
|
|
728
|
+
lastTermRow = rowsAbove;
|
|
681
729
|
process.stdout.write(buf.join(""));
|
|
682
730
|
};
|
|
683
731
|
|
|
@@ -868,6 +916,12 @@ function readMultilineInput(sessionId: string, projectName: string, gitCtx: { br
|
|
|
868
916
|
process.stdout.write("\x1b[2J\x1b[H");
|
|
869
917
|
lastTermRow = 0; render();
|
|
870
918
|
i += seqLen; continue;
|
|
919
|
+
case 118: // Ctrl+V — paste from clipboard
|
|
920
|
+
{ const clip = pasteFromClipboard(sessionDir);
|
|
921
|
+
if (clip?.type === "text") insertText(clip.text);
|
|
922
|
+
else if (clip?.type === "image") insertText(`[📷 ${clip.path}]`);
|
|
923
|
+
render(); }
|
|
924
|
+
i += seqLen; continue;
|
|
871
925
|
default: break;
|
|
872
926
|
}
|
|
873
927
|
}
|
|
@@ -930,6 +984,13 @@ function readMultilineInput(sessionId: string, projectName: string, gitCtx: { br
|
|
|
930
984
|
process.stdout.write("\x1b[2J\x1b[H");
|
|
931
985
|
lastTermRow = 0; render(); i++; continue;
|
|
932
986
|
}
|
|
987
|
+
// Ctrl+V — paste from clipboard (legacy byte)
|
|
988
|
+
if (str[i] === "\x16") {
|
|
989
|
+
const clip = pasteFromClipboard(sessionDir);
|
|
990
|
+
if (clip?.type === "text") insertText(clip.text);
|
|
991
|
+
else if (clip?.type === "image") insertText(`[📷 ${clip.path}]`);
|
|
992
|
+
render(); i++; continue;
|
|
993
|
+
}
|
|
933
994
|
|
|
934
995
|
// Ctrl+J (LF, 0x0A) — newline (cross-terminal)
|
|
935
996
|
if (str[i] === "\n") { insertNewline(); i++; continue; }
|
|
@@ -1091,7 +1152,7 @@ async function chat(explicitSession?: string): Promise<void> {
|
|
|
1091
1152
|
while (true) {
|
|
1092
1153
|
// Refresh git context each turn (branch may change between prompts)
|
|
1093
1154
|
const gitCtx = getGitContext();
|
|
1094
|
-
const text = await readMultilineInput(current.id, projectName, gitCtx);
|
|
1155
|
+
const text = await readMultilineInput(current.id, projectName, gitCtx, current.dir);
|
|
1095
1156
|
|
|
1096
1157
|
if (text === null) exitChat();
|
|
1097
1158
|
if (!text) continue;
|