groundcrew-cli 0.15.17 → 0.16.1
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 +150 -4
- package/package.json +1 -1
- package/src/index.ts +122 -6
package/dist/index.js
CHANGED
|
@@ -5,9 +5,42 @@ import fs from "fs/promises";
|
|
|
5
5
|
import { existsSync } from "fs";
|
|
6
6
|
import path from "path";
|
|
7
7
|
import readline from "readline";
|
|
8
|
-
import { execFile } from "child_process";
|
|
8
|
+
import { execFile, execFileSync } from "child_process";
|
|
9
9
|
import { promisify } from "util";
|
|
10
10
|
var execFileAsync = promisify(execFile);
|
|
11
|
+
function getGitContext() {
|
|
12
|
+
try {
|
|
13
|
+
const branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
14
|
+
encoding: "utf8",
|
|
15
|
+
timeout: 500,
|
|
16
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
17
|
+
}).trim();
|
|
18
|
+
let dirty = "";
|
|
19
|
+
try {
|
|
20
|
+
const status2 = execFileSync("git", ["status", "--porcelain", "-uno"], {
|
|
21
|
+
encoding: "utf8",
|
|
22
|
+
timeout: 500,
|
|
23
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
24
|
+
}).trim();
|
|
25
|
+
if (status2) {
|
|
26
|
+
const hasStaged = status2.split("\n").some((l) => l[0] !== " " && l[0] !== "?");
|
|
27
|
+
const hasUnstaged = status2.split("\n").some((l) => l[1] === "M" || l[1] === "D");
|
|
28
|
+
if (hasStaged) dirty += "+";
|
|
29
|
+
if (hasUnstaged) dirty += "*";
|
|
30
|
+
}
|
|
31
|
+
const untracked = execFileSync("git", ["ls-files", "--others", "--exclude-standard"], {
|
|
32
|
+
encoding: "utf8",
|
|
33
|
+
timeout: 500,
|
|
34
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
35
|
+
}).trim();
|
|
36
|
+
if (untracked) dirty += "%";
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
return { branch, dirty };
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
11
44
|
var GROUNDCREW_DIR = ".groundcrew";
|
|
12
45
|
var SESSIONS_DIR = path.join(GROUNDCREW_DIR, "sessions");
|
|
13
46
|
var ACTIVE_SESSIONS_FILE = path.join(GROUNDCREW_DIR, "active-sessions.json");
|
|
@@ -429,7 +462,7 @@ var CHAT_COMMANDS = [
|
|
|
429
462
|
{ cmd: "/clear", desc: "Clear pending tasks" },
|
|
430
463
|
{ cmd: "/exit", desc: "Exit chat" }
|
|
431
464
|
];
|
|
432
|
-
function readMultilineInput(sessionId) {
|
|
465
|
+
function readMultilineInput(sessionId, projectName, gitCtx) {
|
|
433
466
|
return new Promise((resolve) => {
|
|
434
467
|
const lines = [""];
|
|
435
468
|
let crow = 0;
|
|
@@ -444,7 +477,13 @@ function readMultilineInput(sessionId) {
|
|
|
444
477
|
if (lastTermRow > 0) buf.push(`\x1B[${lastTermRow}A`);
|
|
445
478
|
buf.push("\r\x1B[J");
|
|
446
479
|
const termW = process.stdout.columns || 80;
|
|
447
|
-
|
|
480
|
+
let info = ` ${sessionId} `;
|
|
481
|
+
const ctxParts = [];
|
|
482
|
+
if (projectName) ctxParts.push(projectName);
|
|
483
|
+
if (gitCtx) {
|
|
484
|
+
ctxParts.push(`git:(${gitCtx.branch}${gitCtx.dirty})`);
|
|
485
|
+
}
|
|
486
|
+
if (ctxParts.length) info += ` ${ctxParts.join(" ")} `;
|
|
448
487
|
const dashRight = "\u2500".repeat(Math.max(0, termW - 4 - info.length));
|
|
449
488
|
buf.push(dim("\u2500\u2500\u2500" + info + dashRight));
|
|
450
489
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -599,6 +638,105 @@ function readMultilineInput(sessionId) {
|
|
|
599
638
|
continue;
|
|
600
639
|
}
|
|
601
640
|
if (str[i] === "\x1B" && i + 1 < str.length && str[i + 1] === "[") {
|
|
641
|
+
const csiMatch = str.slice(i).match(/^\x1b\[(\d+)(?:;(\d+))?u/);
|
|
642
|
+
if (csiMatch) {
|
|
643
|
+
const codepoint = parseInt(csiMatch[1], 10);
|
|
644
|
+
const modifier = csiMatch[2] ? parseInt(csiMatch[2], 10) : 1;
|
|
645
|
+
const isCtrl = modifier - 1 & 4;
|
|
646
|
+
const seqLen = csiMatch[0].length;
|
|
647
|
+
if (!isCtrl && modifier <= 1) {
|
|
648
|
+
switch (codepoint) {
|
|
649
|
+
case 9:
|
|
650
|
+
str = str.slice(0, i) + " " + str.slice(i + seqLen);
|
|
651
|
+
continue;
|
|
652
|
+
case 13:
|
|
653
|
+
str = str.slice(0, i) + "\r" + str.slice(i + seqLen);
|
|
654
|
+
continue;
|
|
655
|
+
case 27:
|
|
656
|
+
i += seqLen;
|
|
657
|
+
continue;
|
|
658
|
+
case 127:
|
|
659
|
+
str = str.slice(0, i) + "\x7F" + str.slice(i + seqLen);
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (isCtrl) {
|
|
664
|
+
switch (codepoint) {
|
|
665
|
+
case 99:
|
|
666
|
+
if (fullText() || lines.length > 1 || lines[0].length > 0) {
|
|
667
|
+
const lastRow = lines.length - 1;
|
|
668
|
+
const rowsDown = lastRow - crow;
|
|
669
|
+
if (rowsDown > 0) process.stdout.write(`\x1B[${rowsDown}B`);
|
|
670
|
+
process.stdout.write("\r\n");
|
|
671
|
+
lines.length = 0;
|
|
672
|
+
lines.push("");
|
|
673
|
+
crow = 0;
|
|
674
|
+
ccol = 0;
|
|
675
|
+
lastTermRow = 0;
|
|
676
|
+
render();
|
|
677
|
+
} else {
|
|
678
|
+
process.stdout.write("\r\n");
|
|
679
|
+
finish(null);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
i += seqLen;
|
|
683
|
+
continue;
|
|
684
|
+
case 100:
|
|
685
|
+
if (fullText()) {
|
|
686
|
+
doDelete();
|
|
687
|
+
} else {
|
|
688
|
+
process.stdout.write("\n");
|
|
689
|
+
finish(null);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
i += seqLen;
|
|
693
|
+
continue;
|
|
694
|
+
case 97:
|
|
695
|
+
ccol = 0;
|
|
696
|
+
render();
|
|
697
|
+
i += seqLen;
|
|
698
|
+
continue;
|
|
699
|
+
case 101:
|
|
700
|
+
ccol = lines[crow].length;
|
|
701
|
+
render();
|
|
702
|
+
i += seqLen;
|
|
703
|
+
continue;
|
|
704
|
+
case 117:
|
|
705
|
+
lines[crow] = lines[crow].slice(ccol);
|
|
706
|
+
ccol = 0;
|
|
707
|
+
render();
|
|
708
|
+
i += seqLen;
|
|
709
|
+
continue;
|
|
710
|
+
case 107:
|
|
711
|
+
lines[crow] = lines[crow].slice(0, ccol);
|
|
712
|
+
render();
|
|
713
|
+
i += seqLen;
|
|
714
|
+
continue;
|
|
715
|
+
case 119:
|
|
716
|
+
{
|
|
717
|
+
const before = lines[crow].slice(0, ccol);
|
|
718
|
+
const stripped = before.replace(/\s+$/, "");
|
|
719
|
+
const sp = stripped.lastIndexOf(" ");
|
|
720
|
+
const newBefore = sp >= 0 ? stripped.slice(0, sp + 1) : "";
|
|
721
|
+
lines[crow] = newBefore + lines[crow].slice(ccol);
|
|
722
|
+
ccol = newBefore.length;
|
|
723
|
+
render();
|
|
724
|
+
}
|
|
725
|
+
i += seqLen;
|
|
726
|
+
continue;
|
|
727
|
+
case 108:
|
|
728
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
729
|
+
lastTermRow = 0;
|
|
730
|
+
render();
|
|
731
|
+
i += seqLen;
|
|
732
|
+
continue;
|
|
733
|
+
default:
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
i += seqLen;
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
602
740
|
let j = i + 2;
|
|
603
741
|
while (j < str.length && str.charCodeAt(j) >= 48 && str.charCodeAt(j) <= 63) j++;
|
|
604
742
|
if (j < str.length) j++;
|
|
@@ -677,6 +815,13 @@ function readMultilineInput(sessionId) {
|
|
|
677
815
|
i++;
|
|
678
816
|
continue;
|
|
679
817
|
}
|
|
818
|
+
if (str[i] === "\f") {
|
|
819
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
820
|
+
lastTermRow = 0;
|
|
821
|
+
render();
|
|
822
|
+
i++;
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
680
825
|
if (str[i] === "\n") {
|
|
681
826
|
insertNewline();
|
|
682
827
|
i++;
|
|
@@ -819,7 +964,8 @@ async function chat(explicitSession) {
|
|
|
819
964
|
process.exit(0);
|
|
820
965
|
};
|
|
821
966
|
while (true) {
|
|
822
|
-
const
|
|
967
|
+
const gitCtx = getGitContext();
|
|
968
|
+
const text = await readMultilineInput(current.id, projectName, gitCtx);
|
|
823
969
|
if (text === null) exitChat();
|
|
824
970
|
if (!text) continue;
|
|
825
971
|
const trimmed = text.trim();
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -2,11 +2,40 @@ import fs from "fs/promises";
|
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import readline from "readline";
|
|
5
|
-
import { execFile } from "child_process";
|
|
5
|
+
import { execFile, execFileSync } from "child_process";
|
|
6
6
|
import { promisify } from "util";
|
|
7
7
|
|
|
8
8
|
const execFileAsync = promisify(execFile);
|
|
9
9
|
|
|
10
|
+
// Git context for separator line (cached, refreshed periodically)
|
|
11
|
+
function getGitContext(): { branch: string; dirty: string } | null {
|
|
12
|
+
try {
|
|
13
|
+
const branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
14
|
+
encoding: "utf8", timeout: 500, stdio: ["pipe", "pipe", "pipe"],
|
|
15
|
+
}).trim();
|
|
16
|
+
// Status indicators matching Copilot CLI: * unstaged, + staged, % untracked
|
|
17
|
+
let dirty = "";
|
|
18
|
+
try {
|
|
19
|
+
const status = execFileSync("git", ["status", "--porcelain", "-uno"], {
|
|
20
|
+
encoding: "utf8", timeout: 500, stdio: ["pipe", "pipe", "pipe"],
|
|
21
|
+
}).trim();
|
|
22
|
+
if (status) {
|
|
23
|
+
const hasStaged = status.split("\n").some(l => l[0] !== " " && l[0] !== "?");
|
|
24
|
+
const hasUnstaged = status.split("\n").some(l => l[1] === "M" || l[1] === "D");
|
|
25
|
+
if (hasStaged) dirty += "+";
|
|
26
|
+
if (hasUnstaged) dirty += "*";
|
|
27
|
+
}
|
|
28
|
+
const untracked = execFileSync("git", ["ls-files", "--others", "--exclude-standard"], {
|
|
29
|
+
encoding: "utf8", timeout: 500, stdio: ["pipe", "pipe", "pipe"],
|
|
30
|
+
}).trim();
|
|
31
|
+
if (untracked) dirty += "%";
|
|
32
|
+
} catch { /* ignore status errors */ }
|
|
33
|
+
return { branch, dirty };
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
10
39
|
// Resolved at startup by resolveRoot() — git-aware project root discovery
|
|
11
40
|
let GROUNDCREW_DIR = ".groundcrew";
|
|
12
41
|
let SESSIONS_DIR = path.join(GROUNDCREW_DIR, "sessions");
|
|
@@ -578,7 +607,7 @@ const CHAT_COMMANDS: Array<{ cmd: string; desc: string }> = [
|
|
|
578
607
|
* Tab: slash command completion
|
|
579
608
|
* Paste: bracketed paste with multiline support
|
|
580
609
|
*/
|
|
581
|
-
function readMultilineInput(sessionId: string): Promise<string | null> {
|
|
610
|
+
function readMultilineInput(sessionId: string, projectName: string, gitCtx: { branch: string; dirty: string } | null): Promise<string | null> {
|
|
582
611
|
return new Promise((resolve) => {
|
|
583
612
|
const lines: string[] = [""];
|
|
584
613
|
let crow = 0; // cursor row in lines[]
|
|
@@ -601,9 +630,15 @@ function readMultilineInput(sessionId: string): Promise<string | null> {
|
|
|
601
630
|
if (lastTermRow > 0) buf.push(`\x1b[${lastTermRow}A`);
|
|
602
631
|
buf.push("\r\x1b[J"); // col 0 + clear to end of screen
|
|
603
632
|
|
|
604
|
-
// Separator line: ─── sessionId
|
|
633
|
+
// Separator line: ─── sessionId projectName git:(branch*) ───
|
|
605
634
|
const termW = process.stdout.columns || 80;
|
|
606
|
-
|
|
635
|
+
let info = ` ${sessionId} `;
|
|
636
|
+
const ctxParts: string[] = [];
|
|
637
|
+
if (projectName) ctxParts.push(projectName);
|
|
638
|
+
if (gitCtx) {
|
|
639
|
+
ctxParts.push(`git:(${gitCtx.branch}${gitCtx.dirty})`);
|
|
640
|
+
}
|
|
641
|
+
if (ctxParts.length) info += ` ${ctxParts.join(" ")} `;
|
|
607
642
|
const dashRight = "─".repeat(Math.max(0, termW - 4 - info.length));
|
|
608
643
|
buf.push(dim("───" + info + dashRight));
|
|
609
644
|
|
|
@@ -748,8 +783,82 @@ function readMultilineInput(sessionId: string): Promise<string | null> {
|
|
|
748
783
|
// End (\x1b[F)
|
|
749
784
|
if (str.startsWith("\x1b[F", i)) { ccol = lines[crow].length; render(); i += 3; continue; }
|
|
750
785
|
|
|
751
|
-
//
|
|
786
|
+
// CSI u (Kitty keyboard protocol) — decode \x1b[{codepoint};{modifier}u
|
|
787
|
+
// Also handles single-param \x1b[{codepoint}u (unmodified key)
|
|
788
|
+
// modifier bit 3 (value 4) = Ctrl, sent as bits+1 so modifier 5 = Ctrl
|
|
752
789
|
if (str[i] === "\x1b" && i + 1 < str.length && str[i + 1] === "[") {
|
|
790
|
+
const csiMatch = str.slice(i).match(/^\x1b\[(\d+)(?:;(\d+))?u/);
|
|
791
|
+
if (csiMatch) {
|
|
792
|
+
const codepoint = parseInt(csiMatch[1], 10);
|
|
793
|
+
const modifier = csiMatch[2] ? parseInt(csiMatch[2], 10) : 1;
|
|
794
|
+
const isCtrl = (modifier - 1) & 4;
|
|
795
|
+
const seqLen = csiMatch[0].length;
|
|
796
|
+
|
|
797
|
+
// Handle unmodified functional keys encoded as CSI u
|
|
798
|
+
if (!isCtrl && modifier <= 1) {
|
|
799
|
+
switch (codepoint) {
|
|
800
|
+
case 9: // Tab (unmodified)
|
|
801
|
+
// Delegate to Tab handler by injecting \t
|
|
802
|
+
str = str.slice(0, i) + "\t" + str.slice(i + seqLen);
|
|
803
|
+
continue;
|
|
804
|
+
case 13: // Enter (unmodified)
|
|
805
|
+
str = str.slice(0, i) + "\r" + str.slice(i + seqLen);
|
|
806
|
+
continue;
|
|
807
|
+
case 27: // Escape (unmodified)
|
|
808
|
+
i += seqLen; continue;
|
|
809
|
+
case 127: // Backspace (unmodified)
|
|
810
|
+
str = str.slice(0, i) + "\x7f" + str.slice(i + seqLen);
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (isCtrl) {
|
|
816
|
+
switch (codepoint) {
|
|
817
|
+
case 99: // Ctrl+C
|
|
818
|
+
if (fullText() || lines.length > 1 || lines[0].length > 0) {
|
|
819
|
+
const lastRow = lines.length - 1;
|
|
820
|
+
const rowsDown = lastRow - crow;
|
|
821
|
+
if (rowsDown > 0) process.stdout.write(`\x1b[${rowsDown}B`);
|
|
822
|
+
process.stdout.write("\r\n");
|
|
823
|
+
lines.length = 0; lines.push("");
|
|
824
|
+
crow = 0; ccol = 0; lastTermRow = 0;
|
|
825
|
+
render();
|
|
826
|
+
} else {
|
|
827
|
+
process.stdout.write("\r\n");
|
|
828
|
+
finish(null); return;
|
|
829
|
+
}
|
|
830
|
+
i += seqLen; continue;
|
|
831
|
+
case 100: // Ctrl+D
|
|
832
|
+
if (fullText()) { doDelete(); } else { process.stdout.write("\n"); finish(null); return; }
|
|
833
|
+
i += seqLen; continue;
|
|
834
|
+
case 97: // Ctrl+A — home
|
|
835
|
+
ccol = 0; render(); i += seqLen; continue;
|
|
836
|
+
case 101: // Ctrl+E — end
|
|
837
|
+
ccol = lines[crow].length; render(); i += seqLen; continue;
|
|
838
|
+
case 117: // Ctrl+U — clear before cursor
|
|
839
|
+
lines[crow] = lines[crow].slice(ccol); ccol = 0; render(); i += seqLen; continue;
|
|
840
|
+
case 107: // Ctrl+K — clear after cursor
|
|
841
|
+
lines[crow] = lines[crow].slice(0, ccol); render(); i += seqLen; continue;
|
|
842
|
+
case 119: // Ctrl+W — delete word before cursor
|
|
843
|
+
{ const before = lines[crow].slice(0, ccol);
|
|
844
|
+
const stripped = before.replace(/\s+$/, "");
|
|
845
|
+
const sp = stripped.lastIndexOf(" ");
|
|
846
|
+
const newBefore = sp >= 0 ? stripped.slice(0, sp + 1) : "";
|
|
847
|
+
lines[crow] = newBefore + lines[crow].slice(ccol);
|
|
848
|
+
ccol = newBefore.length; render(); }
|
|
849
|
+
i += seqLen; continue;
|
|
850
|
+
case 108: // Ctrl+L — clear screen
|
|
851
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
852
|
+
lastTermRow = 0; render();
|
|
853
|
+
i += seqLen; continue;
|
|
854
|
+
default: break;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
// Unhandled CSI u — skip it
|
|
858
|
+
i += seqLen; continue;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Skip unknown CSI sequences (non-u final byte)
|
|
753
862
|
let j = i + 2;
|
|
754
863
|
while (j < str.length && str.charCodeAt(j) >= 0x30 && str.charCodeAt(j) <= 0x3f) j++;
|
|
755
864
|
if (j < str.length) j++; // skip final byte
|
|
@@ -804,6 +913,11 @@ function readMultilineInput(sessionId: string): Promise<string | null> {
|
|
|
804
913
|
lines[crow] = newBefore + lines[crow].slice(ccol);
|
|
805
914
|
ccol = newBefore.length; render(); i++; continue;
|
|
806
915
|
}
|
|
916
|
+
// Ctrl+L — clear screen (legacy byte)
|
|
917
|
+
if (str[i] === "\x0c") {
|
|
918
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
919
|
+
lastTermRow = 0; render(); i++; continue;
|
|
920
|
+
}
|
|
807
921
|
|
|
808
922
|
// Ctrl+J (LF, 0x0A) — newline (cross-terminal)
|
|
809
923
|
if (str[i] === "\n") { insertNewline(); i++; continue; }
|
|
@@ -963,7 +1077,9 @@ async function chat(explicitSession?: string): Promise<void> {
|
|
|
963
1077
|
|
|
964
1078
|
// ── Main chat loop ─────────────────────────────────────────────────────────────────
|
|
965
1079
|
while (true) {
|
|
966
|
-
|
|
1080
|
+
// Refresh git context each turn (branch may change between prompts)
|
|
1081
|
+
const gitCtx = getGitContext();
|
|
1082
|
+
const text = await readMultilineInput(current.id, projectName, gitCtx);
|
|
967
1083
|
|
|
968
1084
|
if (text === null) exitChat();
|
|
969
1085
|
if (!text) continue;
|