gsd-pi 0.3.0 → 0.3.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/README.md +3 -1
- package/dist/cli.js +112 -5
- package/dist/tool-bootstrap.d.ts +4 -0
- package/dist/tool-bootstrap.js +74 -0
- package/dist/wizard.js +15 -5
- package/package.json +4 -2
- package/patches/@mariozechner+pi-coding-agent+0.57.1.patch +48 -0
- package/patches/@mariozechner+pi-tui+0.57.1.patch +47 -0
- package/scripts/postinstall.js +8 -0
- package/src/resources/extensions/bg-shell/index.ts +57 -8
- package/src/resources/extensions/browser-tools/index.ts +4 -1
- package/src/resources/extensions/github/gh-api.ts +46 -30
- package/src/resources/extensions/gsd/commands.ts +1 -1
- package/src/resources/extensions/gsd/guided-flow.ts +1 -1
- package/src/resources/extensions/gsd/index.ts +24 -2
- package/src/resources/extensions/gsd/prompts/discuss.md +3 -1
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/tests/discuss-prompt.test.ts +38 -0
- package/src/resources/extensions/slash-commands/gsd-run.ts +1 -1
package/README.md
CHANGED
|
@@ -412,7 +412,9 @@ Use expensive models where quality matters (planning, complex execution) and che
|
|
|
412
412
|
|
|
413
413
|
## Star History
|
|
414
414
|
|
|
415
|
-
|
|
415
|
+
<a href="https://star-history.com/#gsd-build/gsd-2&Date">
|
|
416
|
+
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=gsd-build/gsd-2&type=Date" />
|
|
417
|
+
</a>
|
|
416
418
|
|
|
417
419
|
---
|
|
418
420
|
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,56 @@
|
|
|
1
|
-
import { AuthStorage, ModelRegistry, SettingsManager, SessionManager, createAgentSession, InteractiveMode, } from '@mariozechner/pi-coding-agent';
|
|
1
|
+
import { AuthStorage, DefaultResourceLoader, ModelRegistry, SettingsManager, SessionManager, createAgentSession, InteractiveMode, runPrintMode, } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
2
4
|
import { agentDir, sessionsDir, authFilePath } from './app-paths.js';
|
|
3
|
-
import {
|
|
5
|
+
import { initResources } from './resource-loader.js';
|
|
6
|
+
import { ensureManagedTools } from './tool-bootstrap.js';
|
|
4
7
|
import { loadStoredEnvKeys, runWizardIfNeeded } from './wizard.js';
|
|
8
|
+
function parseCliArgs(argv) {
|
|
9
|
+
const flags = { extensions: [], messages: [] };
|
|
10
|
+
const args = argv.slice(2); // skip node + script
|
|
11
|
+
for (let i = 0; i < args.length; i++) {
|
|
12
|
+
const arg = args[i];
|
|
13
|
+
if (arg === '--mode' && i + 1 < args.length) {
|
|
14
|
+
const m = args[++i];
|
|
15
|
+
if (m === 'text' || m === 'json' || m === 'rpc')
|
|
16
|
+
flags.mode = m;
|
|
17
|
+
}
|
|
18
|
+
else if (arg === '--print' || arg === '-p') {
|
|
19
|
+
flags.print = true;
|
|
20
|
+
}
|
|
21
|
+
else if (arg === '--no-session') {
|
|
22
|
+
flags.noSession = true;
|
|
23
|
+
}
|
|
24
|
+
else if (arg === '--model' && i + 1 < args.length) {
|
|
25
|
+
flags.model = args[++i];
|
|
26
|
+
}
|
|
27
|
+
else if (arg === '--extension' && i + 1 < args.length) {
|
|
28
|
+
flags.extensions.push(args[++i]);
|
|
29
|
+
}
|
|
30
|
+
else if (arg === '--append-system-prompt' && i + 1 < args.length) {
|
|
31
|
+
flags.appendSystemPrompt = args[++i];
|
|
32
|
+
}
|
|
33
|
+
else if (arg === '--tools' && i + 1 < args.length) {
|
|
34
|
+
flags.tools = args[++i].split(',');
|
|
35
|
+
}
|
|
36
|
+
else if (!arg.startsWith('--') && !arg.startsWith('-')) {
|
|
37
|
+
flags.messages.push(arg);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return flags;
|
|
41
|
+
}
|
|
42
|
+
const cliFlags = parseCliArgs(process.argv);
|
|
43
|
+
const isPrintMode = cliFlags.print || cliFlags.mode !== undefined;
|
|
44
|
+
// Pi's tool bootstrap can mis-detect already-installed fd/rg on some systems
|
|
45
|
+
// because spawnSync(..., ["--version"]) returns EPERM despite a zero exit code.
|
|
46
|
+
// Provision local managed binaries first so Pi sees them without probing PATH.
|
|
47
|
+
ensureManagedTools(join(agentDir, 'bin'));
|
|
5
48
|
const authStorage = AuthStorage.create(authFilePath);
|
|
6
49
|
loadStoredEnvKeys(authStorage);
|
|
7
|
-
|
|
50
|
+
// Skip the setup wizard in print mode — it requires TTY interaction
|
|
51
|
+
if (!isPrintMode) {
|
|
52
|
+
await runWizardIfNeeded(authStorage);
|
|
53
|
+
}
|
|
8
54
|
const modelRegistry = new ModelRegistry(authStorage);
|
|
9
55
|
const settingsManager = SettingsManager.create(agentDir);
|
|
10
56
|
// Validate configured model on startup — catches stale settings from prior installs
|
|
@@ -37,9 +83,70 @@ if (!settingsManager.getQuietStartup()) {
|
|
|
37
83
|
if (!settingsManager.getCollapseChangelog()) {
|
|
38
84
|
settingsManager.setCollapseChangelog(true);
|
|
39
85
|
}
|
|
40
|
-
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Print / subagent mode — single-shot execution, no TTY required
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
if (isPrintMode) {
|
|
90
|
+
const sessionManager = cliFlags.noSession
|
|
91
|
+
? SessionManager.inMemory()
|
|
92
|
+
: SessionManager.create(process.cwd());
|
|
93
|
+
// Read --append-system-prompt file content (subagent writes agent system prompts to temp files)
|
|
94
|
+
let appendSystemPrompt;
|
|
95
|
+
if (cliFlags.appendSystemPrompt) {
|
|
96
|
+
try {
|
|
97
|
+
appendSystemPrompt = readFileSync(cliFlags.appendSystemPrompt, 'utf-8');
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// If it's not a file path, treat it as literal text
|
|
101
|
+
appendSystemPrompt = cliFlags.appendSystemPrompt;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
initResources(agentDir);
|
|
105
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
106
|
+
agentDir,
|
|
107
|
+
additionalExtensionPaths: cliFlags.extensions.length > 0 ? cliFlags.extensions : undefined,
|
|
108
|
+
appendSystemPrompt,
|
|
109
|
+
});
|
|
110
|
+
await resourceLoader.reload();
|
|
111
|
+
const { session, extensionsResult } = await createAgentSession({
|
|
112
|
+
authStorage,
|
|
113
|
+
modelRegistry,
|
|
114
|
+
settingsManager,
|
|
115
|
+
sessionManager,
|
|
116
|
+
resourceLoader,
|
|
117
|
+
});
|
|
118
|
+
if (extensionsResult.errors.length > 0) {
|
|
119
|
+
for (const err of extensionsResult.errors) {
|
|
120
|
+
process.stderr.write(`[gsd] Extension load error: ${err.error}\n`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Apply --model override if specified
|
|
124
|
+
if (cliFlags.model) {
|
|
125
|
+
const available = modelRegistry.getAvailable();
|
|
126
|
+
const match = available.find((m) => m.id === cliFlags.model) ||
|
|
127
|
+
available.find((m) => `${m.provider}/${m.id}` === cliFlags.model);
|
|
128
|
+
if (match) {
|
|
129
|
+
session.setModel(match);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const mode = cliFlags.mode || 'text';
|
|
133
|
+
await runPrintMode(session, {
|
|
134
|
+
mode: mode === 'rpc' ? 'json' : mode,
|
|
135
|
+
messages: cliFlags.messages,
|
|
136
|
+
});
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Interactive mode — normal TTY session
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Per-directory session storage — same encoding as the upstream SDK so that
|
|
143
|
+
// /resume only shows sessions from the current working directory.
|
|
144
|
+
const cwd = process.cwd();
|
|
145
|
+
const safePath = `--${cwd.replace(/^[/\\]/, '').replace(/[/\\:]/g, '-')}--`;
|
|
146
|
+
const projectSessionsDir = join(sessionsDir, safePath);
|
|
147
|
+
const sessionManager = SessionManager.create(cwd, projectSessionsDir);
|
|
41
148
|
initResources(agentDir);
|
|
42
|
-
const resourceLoader =
|
|
149
|
+
const resourceLoader = new DefaultResourceLoader({ agentDir });
|
|
43
150
|
await resourceLoader.reload();
|
|
44
151
|
const { session, extensionsResult } = await createAgentSession({
|
|
45
152
|
authStorage,
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, rmSync, symlinkSync } from "node:fs";
|
|
2
|
+
import { delimiter, join } from "node:path";
|
|
3
|
+
const TOOL_SPECS = {
|
|
4
|
+
fd: {
|
|
5
|
+
targetName: process.platform === "win32" ? "fd.exe" : "fd",
|
|
6
|
+
candidates: process.platform === "win32" ? ["fd.exe", "fd", "fdfind.exe", "fdfind"] : ["fd", "fdfind"],
|
|
7
|
+
},
|
|
8
|
+
rg: {
|
|
9
|
+
targetName: process.platform === "win32" ? "rg.exe" : "rg",
|
|
10
|
+
candidates: process.platform === "win32" ? ["rg.exe", "rg"] : ["rg"],
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
function splitPath(pathValue) {
|
|
14
|
+
if (!pathValue)
|
|
15
|
+
return [];
|
|
16
|
+
return pathValue.split(delimiter).map((segment) => segment.trim()).filter(Boolean);
|
|
17
|
+
}
|
|
18
|
+
function getCandidateNames(name) {
|
|
19
|
+
if (process.platform !== "win32")
|
|
20
|
+
return [name];
|
|
21
|
+
const lower = name.toLowerCase();
|
|
22
|
+
if (lower.endsWith(".exe") || lower.endsWith(".cmd") || lower.endsWith(".bat"))
|
|
23
|
+
return [name];
|
|
24
|
+
return [name, `${name}.exe`, `${name}.cmd`, `${name}.bat`];
|
|
25
|
+
}
|
|
26
|
+
function isRegularFile(path) {
|
|
27
|
+
try {
|
|
28
|
+
return lstatSync(path).isFile() || lstatSync(path).isSymbolicLink();
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function resolveToolFromPath(tool, pathValue = process.env.PATH) {
|
|
35
|
+
const spec = TOOL_SPECS[tool];
|
|
36
|
+
for (const dir of splitPath(pathValue)) {
|
|
37
|
+
for (const candidate of spec.candidates) {
|
|
38
|
+
for (const name of getCandidateNames(candidate)) {
|
|
39
|
+
const fullPath = join(dir, name);
|
|
40
|
+
if (existsSync(fullPath) && isRegularFile(fullPath)) {
|
|
41
|
+
return fullPath;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function provisionTool(targetDir, tool, sourcePath) {
|
|
49
|
+
const targetPath = join(targetDir, TOOL_SPECS[tool].targetName);
|
|
50
|
+
if (existsSync(targetPath))
|
|
51
|
+
return targetPath;
|
|
52
|
+
mkdirSync(targetDir, { recursive: true });
|
|
53
|
+
try {
|
|
54
|
+
symlinkSync(sourcePath, targetPath);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
rmSync(targetPath, { force: true });
|
|
58
|
+
copyFileSync(sourcePath, targetPath);
|
|
59
|
+
chmodSync(targetPath, 0o755);
|
|
60
|
+
}
|
|
61
|
+
return targetPath;
|
|
62
|
+
}
|
|
63
|
+
export function ensureManagedTools(targetDir, pathValue = process.env.PATH) {
|
|
64
|
+
const provisioned = [];
|
|
65
|
+
for (const tool of Object.keys(TOOL_SPECS)) {
|
|
66
|
+
if (existsSync(join(targetDir, TOOL_SPECS[tool].targetName)))
|
|
67
|
+
continue;
|
|
68
|
+
const sourcePath = resolveToolFromPath(tool, pathValue);
|
|
69
|
+
if (!sourcePath)
|
|
70
|
+
continue;
|
|
71
|
+
provisioned.push(provisionTool(targetDir, tool, sourcePath));
|
|
72
|
+
}
|
|
73
|
+
return provisioned;
|
|
74
|
+
}
|
package/dist/wizard.js
CHANGED
|
@@ -21,6 +21,18 @@ async function promptMasked(label, hint) {
|
|
|
21
21
|
process.stdin.resume();
|
|
22
22
|
process.stdin.setEncoding('utf8');
|
|
23
23
|
let value = '';
|
|
24
|
+
const redraw = () => {
|
|
25
|
+
process.stdout.clearLine(0);
|
|
26
|
+
process.stdout.cursorTo(0);
|
|
27
|
+
if (value.length === 0) {
|
|
28
|
+
process.stdout.write(' ');
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const dots = '●'.repeat(Math.min(value.length, 24));
|
|
32
|
+
const counter = value.length > 24 ? ` ${dim}(${value.length})${reset}` : ` ${dim}${value.length}${reset}`;
|
|
33
|
+
process.stdout.write(` ${dots}${counter}`);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
24
36
|
const handler = (ch) => {
|
|
25
37
|
if (ch === '\r' || ch === '\n') {
|
|
26
38
|
process.stdin.setRawMode(false);
|
|
@@ -34,17 +46,15 @@ async function promptMasked(label, hint) {
|
|
|
34
46
|
process.stdout.write('\n');
|
|
35
47
|
process.exit(0);
|
|
36
48
|
}
|
|
37
|
-
else if (ch === '\u007f') {
|
|
49
|
+
else if (ch === '\u007f' || ch === '\b') {
|
|
38
50
|
if (value.length > 0) {
|
|
39
51
|
value = value.slice(0, -1);
|
|
40
52
|
}
|
|
41
|
-
|
|
42
|
-
process.stdout.cursorTo(0);
|
|
43
|
-
process.stdout.write(' ' + '*'.repeat(value.length));
|
|
53
|
+
redraw();
|
|
44
54
|
}
|
|
45
55
|
else {
|
|
46
56
|
value += ch;
|
|
47
|
-
|
|
57
|
+
redraw();
|
|
48
58
|
}
|
|
49
59
|
};
|
|
50
60
|
process.stdin.on('data', handler);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gsd-pi",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "GSD — Get
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "GSD — Get Shit Done coding agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
20
|
"dist",
|
|
21
|
+
"patches",
|
|
21
22
|
"pkg",
|
|
22
23
|
"src/resources",
|
|
23
24
|
"scripts/postinstall.js",
|
|
@@ -46,6 +47,7 @@
|
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@types/node": "^22.0.0",
|
|
50
|
+
"patch-package": "^8.0.1",
|
|
49
51
|
"typescript": "^5.4.0"
|
|
50
52
|
},
|
|
51
53
|
"overrides": {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
diff --git a/node_modules/@mariozechner/pi-coding-agent/dist/core/tools/bash.js b/node_modules/@mariozechner/pi-coding-agent/dist/core/tools/bash.js
|
|
2
|
+
index 27fe820..68f277f 100644
|
|
3
|
+
--- a/node_modules/@mariozechner/pi-coding-agent/dist/core/tools/bash.js
|
|
4
|
+
+++ b/node_modules/@mariozechner/pi-coding-agent/dist/core/tools/bash.js
|
|
5
|
+
@@ -1,11 +1,35 @@
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
7
|
+
import { createWriteStream, existsSync } from "node:fs";
|
|
8
|
+
+import { createRequire } from "node:module";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { Type } from "@sinclair/typebox";
|
|
12
|
+
import { spawn } from "child_process";
|
|
13
|
+
import { getShellConfig, getShellEnv, killProcessTree } from "../../utils/shell.js";
|
|
14
|
+
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateTail } from "./truncate.js";
|
|
15
|
+
+// Cached Win32 FFI handles for restoring VT input after child processes
|
|
16
|
+
+let _vtHandles = null;
|
|
17
|
+
+function restoreWindowsVTInput() {
|
|
18
|
+
+ if (process.platform !== "win32") return;
|
|
19
|
+
+ try {
|
|
20
|
+
+ if (!_vtHandles) {
|
|
21
|
+
+ const cjsRequire = createRequire(import.meta.url);
|
|
22
|
+
+ const koffi = cjsRequire("koffi");
|
|
23
|
+
+ const k32 = koffi.load("kernel32.dll");
|
|
24
|
+
+ const GetStdHandle = k32.func("void* __stdcall GetStdHandle(int)");
|
|
25
|
+
+ const GetConsoleMode = k32.func("bool __stdcall GetConsoleMode(void*, _Out_ uint32_t*)");
|
|
26
|
+
+ const SetConsoleMode = k32.func("bool __stdcall SetConsoleMode(void*, uint32_t)");
|
|
27
|
+
+ const handle = GetStdHandle(-10);
|
|
28
|
+
+ _vtHandles = { GetConsoleMode, SetConsoleMode, handle };
|
|
29
|
+
+ }
|
|
30
|
+
+ const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200;
|
|
31
|
+
+ const mode = new Uint32Array(1);
|
|
32
|
+
+ _vtHandles.GetConsoleMode(_vtHandles.handle, mode);
|
|
33
|
+
+ if (!(mode[0] & ENABLE_VIRTUAL_TERMINAL_INPUT)) {
|
|
34
|
+
+ _vtHandles.SetConsoleMode(_vtHandles.handle, mode[0] | ENABLE_VIRTUAL_TERMINAL_INPUT);
|
|
35
|
+
+ }
|
|
36
|
+
+ } catch { }
|
|
37
|
+
+}
|
|
38
|
+
/**
|
|
39
|
+
* Generate a unique temp file path for bash output
|
|
40
|
+
*/
|
|
41
|
+
@@ -76,6 +100,7 @@ const defaultBashOperations = {
|
|
42
|
+
}
|
|
43
|
+
// Handle process exit
|
|
44
|
+
child.on("close", (code) => {
|
|
45
|
+
+ restoreWindowsVTInput();
|
|
46
|
+
if (timeoutHandle)
|
|
47
|
+
clearTimeout(timeoutHandle);
|
|
48
|
+
if (signal)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
diff --git a/node_modules/@mariozechner/pi-tui/dist/terminal.js b/node_modules/@mariozechner/pi-tui/dist/terminal.js
|
|
2
|
+
index cd20330..e836fcd 100644
|
|
3
|
+
--- a/node_modules/@mariozechner/pi-tui/dist/terminal.js
|
|
4
|
+
+++ b/node_modules/@mariozechner/pi-tui/dist/terminal.js
|
|
5
|
+
@@ -7,6 +7,7 @@ const cjsRequire = createRequire(import.meta.url);
|
|
6
|
+
* Real terminal using process.stdin/stdout
|
|
7
|
+
*/
|
|
8
|
+
export class ProcessTerminal {
|
|
9
|
+
+ static _vtHandles = null;
|
|
10
|
+
wasRaw = false;
|
|
11
|
+
inputHandler;
|
|
12
|
+
resizeHandler;
|
|
13
|
+
@@ -126,20 +127,23 @@ export class ProcessTerminal {
|
|
14
|
+
if (process.platform !== "win32")
|
|
15
|
+
return;
|
|
16
|
+
try {
|
|
17
|
+
- // Dynamic require to avoid bundling koffi's 74MB of cross-platform
|
|
18
|
+
- // native binaries into every compiled binary. Koffi is only needed
|
|
19
|
+
- // on Windows for VT input support.
|
|
20
|
+
- const koffi = cjsRequire("koffi");
|
|
21
|
+
- const k32 = koffi.load("kernel32.dll");
|
|
22
|
+
- const GetStdHandle = k32.func("void* __stdcall GetStdHandle(int)");
|
|
23
|
+
- const GetConsoleMode = k32.func("bool __stdcall GetConsoleMode(void*, _Out_ uint32_t*)");
|
|
24
|
+
- const SetConsoleMode = k32.func("bool __stdcall SetConsoleMode(void*, uint32_t)");
|
|
25
|
+
- const STD_INPUT_HANDLE = -10;
|
|
26
|
+
+ if (!ProcessTerminal._vtHandles) {
|
|
27
|
+
+ const koffi = cjsRequire("koffi");
|
|
28
|
+
+ const k32 = koffi.load("kernel32.dll");
|
|
29
|
+
+ const GetStdHandle = k32.func("void* __stdcall GetStdHandle(int)");
|
|
30
|
+
+ const GetConsoleMode = k32.func("bool __stdcall GetConsoleMode(void*, _Out_ uint32_t*)");
|
|
31
|
+
+ const SetConsoleMode = k32.func("bool __stdcall SetConsoleMode(void*, uint32_t)");
|
|
32
|
+
+ const STD_INPUT_HANDLE = -10;
|
|
33
|
+
+ const handle = GetStdHandle(STD_INPUT_HANDLE);
|
|
34
|
+
+ ProcessTerminal._vtHandles = { GetConsoleMode, SetConsoleMode, handle };
|
|
35
|
+
+ }
|
|
36
|
+
const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200;
|
|
37
|
+
- const handle = GetStdHandle(STD_INPUT_HANDLE);
|
|
38
|
+
+ const { GetConsoleMode, SetConsoleMode, handle } = ProcessTerminal._vtHandles;
|
|
39
|
+
const mode = new Uint32Array(1);
|
|
40
|
+
GetConsoleMode(handle, mode);
|
|
41
|
+
- SetConsoleMode(handle, mode[0] | ENABLE_VIRTUAL_TERMINAL_INPUT);
|
|
42
|
+
+ if (!(mode[0] & ENABLE_VIRTUAL_TERMINAL_INPUT)) {
|
|
43
|
+
+ SetConsoleMode(handle, mode[0] | ENABLE_VIRTUAL_TERMINAL_INPUT);
|
|
44
|
+
+ }
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// koffi not available — Shift+Tab won't be distinguishable from Tab
|
package/scripts/postinstall.js
CHANGED
|
@@ -35,6 +35,14 @@ const banner =
|
|
|
35
35
|
|
|
36
36
|
process.stderr.write(banner)
|
|
37
37
|
|
|
38
|
+
// Apply patches to upstream dependencies (non-fatal)
|
|
39
|
+
try {
|
|
40
|
+
execSync('npx patch-package', { stdio: 'inherit', cwd: resolve(__dirname, '..') })
|
|
41
|
+
process.stderr.write(`\n ${green}✓${reset} Patches applied\n`)
|
|
42
|
+
} catch {
|
|
43
|
+
process.stderr.write(`\n ${yellow}⚠${reset} Failed to apply patches — run ${cyan}npx patch-package${reset} manually\n`)
|
|
44
|
+
}
|
|
45
|
+
|
|
38
46
|
// Install Playwright chromium for browser tools (non-fatal)
|
|
39
47
|
const args = os.platform() === 'linux' ? '--with-deps' : ''
|
|
40
48
|
try {
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
truncateHead,
|
|
34
34
|
DEFAULT_MAX_BYTES,
|
|
35
35
|
DEFAULT_MAX_LINES,
|
|
36
|
+
getShellConfig,
|
|
36
37
|
} from "@mariozechner/pi-coding-agent";
|
|
37
38
|
import {
|
|
38
39
|
Text,
|
|
@@ -42,11 +43,39 @@ import {
|
|
|
42
43
|
Key,
|
|
43
44
|
} from "@mariozechner/pi-tui";
|
|
44
45
|
import { Type } from "@sinclair/typebox";
|
|
45
|
-
import { spawn, type ChildProcess } from "node:child_process";
|
|
46
|
+
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
|
|
46
47
|
import { createConnection } from "node:net";
|
|
47
48
|
import { randomUUID } from "node:crypto";
|
|
48
49
|
import { writeFileSync, readFileSync, existsSync, mkdirSync } from "node:fs";
|
|
49
50
|
import { join } from "node:path";
|
|
51
|
+
import { createRequire } from "node:module";
|
|
52
|
+
|
|
53
|
+
// ── Windows VT Input Restoration ────────────────────────────────────────────
|
|
54
|
+
// Child processes (esp. Git Bash / MSYS2) can strip the ENABLE_VIRTUAL_TERMINAL_INPUT
|
|
55
|
+
// flag from the shared stdin console handle. Re-enable it after each child exits.
|
|
56
|
+
|
|
57
|
+
let _vtHandles: { GetConsoleMode: Function; SetConsoleMode: Function; handle: unknown } | null = null;
|
|
58
|
+
function restoreWindowsVTInput(): void {
|
|
59
|
+
if (process.platform !== "win32") return;
|
|
60
|
+
try {
|
|
61
|
+
if (!_vtHandles) {
|
|
62
|
+
const cjsRequire = createRequire(import.meta.url);
|
|
63
|
+
const koffi = cjsRequire("koffi");
|
|
64
|
+
const k32 = koffi.load("kernel32.dll");
|
|
65
|
+
const GetStdHandle = k32.func("void* __stdcall GetStdHandle(int)");
|
|
66
|
+
const GetConsoleMode = k32.func("bool __stdcall GetConsoleMode(void*, _Out_ uint32_t*)");
|
|
67
|
+
const SetConsoleMode = k32.func("bool __stdcall SetConsoleMode(void*, uint32_t)");
|
|
68
|
+
const handle = GetStdHandle(-10);
|
|
69
|
+
_vtHandles = { GetConsoleMode, SetConsoleMode, handle };
|
|
70
|
+
}
|
|
71
|
+
const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200;
|
|
72
|
+
const mode = new Uint32Array(1);
|
|
73
|
+
_vtHandles.GetConsoleMode(_vtHandles.handle, mode);
|
|
74
|
+
if (!(mode[0] & ENABLE_VIRTUAL_TERMINAL_INPUT)) {
|
|
75
|
+
_vtHandles.SetConsoleMode(_vtHandles.handle, mode[0] | ENABLE_VIRTUAL_TERMINAL_INPUT);
|
|
76
|
+
}
|
|
77
|
+
} catch { /* koffi not available on non-Windows */ }
|
|
78
|
+
}
|
|
50
79
|
|
|
51
80
|
// ── Types ──────────────────────────────────────────────────────────────────
|
|
52
81
|
|
|
@@ -551,11 +580,12 @@ function startProcess(opts: StartOptions): BgProcess {
|
|
|
551
580
|
|
|
552
581
|
const env = { ...process.env, ...(opts.env || {}) };
|
|
553
582
|
|
|
554
|
-
const
|
|
583
|
+
const { shell, args: shellArgs } = getShellConfig();
|
|
584
|
+
const proc = spawn(shell, [...shellArgs, opts.command], {
|
|
555
585
|
cwd: opts.cwd,
|
|
556
586
|
stdio: ["pipe", "pipe", "pipe"],
|
|
557
587
|
env,
|
|
558
|
-
detached:
|
|
588
|
+
detached: process.platform !== "win32",
|
|
559
589
|
});
|
|
560
590
|
|
|
561
591
|
const bg: BgProcess = {
|
|
@@ -621,6 +651,7 @@ function startProcess(opts: StartOptions): BgProcess {
|
|
|
621
651
|
});
|
|
622
652
|
|
|
623
653
|
proc.on("exit", (code, sig) => {
|
|
654
|
+
restoreWindowsVTInput();
|
|
624
655
|
bg.alive = false;
|
|
625
656
|
bg.exitCode = code;
|
|
626
657
|
bg.signal = sig ?? null;
|
|
@@ -686,14 +717,32 @@ function killProcess(id: string, sig: NodeJS.Signals = "SIGTERM"): boolean {
|
|
|
686
717
|
if (!bg) return false;
|
|
687
718
|
if (!bg.alive) return true;
|
|
688
719
|
try {
|
|
689
|
-
if (
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
720
|
+
if (process.platform === "win32") {
|
|
721
|
+
// Windows: use taskkill /F /T to force-kill the entire process tree.
|
|
722
|
+
// process.kill(-pid) (Unix process groups) does not work on Windows.
|
|
723
|
+
if (bg.proc.pid) {
|
|
724
|
+
const result = spawnSync("taskkill", ["/F", "/T", "/PID", String(bg.proc.pid)], {
|
|
725
|
+
timeout: 5000,
|
|
726
|
+
encoding: "utf-8",
|
|
727
|
+
});
|
|
728
|
+
if (result.status !== 0 && result.status !== 128) {
|
|
729
|
+
// taskkill failed — try the direct kill as fallback
|
|
730
|
+
bg.proc.kill(sig);
|
|
731
|
+
}
|
|
732
|
+
} else {
|
|
693
733
|
bg.proc.kill(sig);
|
|
694
734
|
}
|
|
695
735
|
} else {
|
|
696
|
-
|
|
736
|
+
// Unix/macOS: kill the process group via negative PID
|
|
737
|
+
if (bg.proc.pid) {
|
|
738
|
+
try {
|
|
739
|
+
process.kill(-bg.proc.pid, sig);
|
|
740
|
+
} catch {
|
|
741
|
+
bg.proc.kill(sig);
|
|
742
|
+
}
|
|
743
|
+
} else {
|
|
744
|
+
bg.proc.kill(sig);
|
|
745
|
+
}
|
|
697
746
|
}
|
|
698
747
|
return true;
|
|
699
748
|
} catch {
|
|
@@ -343,7 +343,10 @@ async function ensureBrowser(): Promise<{ browser: Browser; context: BrowserCont
|
|
|
343
343
|
// Lazy import so playwright is only loaded when actually needed
|
|
344
344
|
const { chromium } = await import("playwright");
|
|
345
345
|
|
|
346
|
-
|
|
346
|
+
const launchOptions: Record<string, unknown> = { headless: false };
|
|
347
|
+
const customPath = process.env.BROWSER_PATH;
|
|
348
|
+
if (customPath) launchOptions.executablePath = customPath;
|
|
349
|
+
browser = await chromium.launch(launchOptions);
|
|
347
350
|
context = await browser.newContext({
|
|
348
351
|
deviceScaleFactor: 2,
|
|
349
352
|
viewport: { width: 1280, height: 800 },
|
|
@@ -6,20 +6,44 @@
|
|
|
6
6
|
* Falls back to raw REST API with GITHUB_TOKEN env var.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { execSync } from "node:child_process";
|
|
9
|
+
import { execSync, spawnSync, type SpawnSyncReturns } from "node:child_process";
|
|
10
10
|
|
|
11
11
|
// ─── Auth detection ───────────────────────────────────────────────────────────
|
|
12
12
|
|
|
13
13
|
let _useGhCli: boolean | null = null;
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
let ghSpawnImpl = (args: string[], input?: string, cwd?: string): SpawnSyncReturns<string> =>
|
|
16
|
+
spawnSync("gh", args, {
|
|
17
|
+
cwd,
|
|
18
|
+
encoding: "utf8",
|
|
19
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
20
|
+
input,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function ghSpawn(args: string[], input?: string, cwd?: string): SpawnSyncReturns<string> {
|
|
24
|
+
return ghSpawnImpl(args, input, cwd);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resetGhCliDetectionForTests(): void {
|
|
28
|
+
_useGhCli = null;
|
|
29
|
+
ghSpawnImpl = (args: string[], input?: string, cwd?: string): SpawnSyncReturns<string> =>
|
|
30
|
+
spawnSync("gh", args, {
|
|
31
|
+
cwd,
|
|
32
|
+
encoding: "utf8",
|
|
33
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
34
|
+
input,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function setGhSpawnForTests(fn: (args: string[], input?: string, cwd?: string) => SpawnSyncReturns<string>): void {
|
|
39
|
+
ghSpawnImpl = fn;
|
|
40
|
+
_useGhCli = null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function hasGhCli(): boolean {
|
|
16
44
|
if (_useGhCli !== null) return _useGhCli;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
_useGhCli = true;
|
|
20
|
-
} catch {
|
|
21
|
-
_useGhCli = false;
|
|
22
|
-
}
|
|
45
|
+
const result = ghSpawn(["auth", "token"]);
|
|
46
|
+
_useGhCli = result.status === 0 && !result.error && !!result.stdout?.trim();
|
|
23
47
|
return _useGhCli;
|
|
24
48
|
}
|
|
25
49
|
|
|
@@ -120,11 +144,6 @@ export async function ghApi<T = unknown>(
|
|
|
120
144
|
return fetchApi<T>(endpoint, method, options.params, options.body, token);
|
|
121
145
|
}
|
|
122
146
|
|
|
123
|
-
function shellEscape(s: string): string {
|
|
124
|
-
// Single-quote wrapping, escaping any existing single quotes
|
|
125
|
-
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
126
|
-
}
|
|
127
|
-
|
|
128
147
|
function ghCliApi<T>(
|
|
129
148
|
endpoint: string,
|
|
130
149
|
method: string,
|
|
@@ -132,39 +151,36 @@ function ghCliApi<T>(
|
|
|
132
151
|
body?: Record<string, unknown>,
|
|
133
152
|
cwd?: string,
|
|
134
153
|
): T {
|
|
135
|
-
const
|
|
154
|
+
const args = ["api", endpoint, "--method", method];
|
|
136
155
|
|
|
137
156
|
if (params) {
|
|
138
157
|
for (const [key, val] of Object.entries(params)) {
|
|
139
158
|
if (val === undefined) continue;
|
|
140
159
|
if (Array.isArray(val)) {
|
|
141
160
|
for (const v of val) {
|
|
142
|
-
|
|
161
|
+
args.push("-f", `${key}[]=${v}`);
|
|
143
162
|
}
|
|
144
163
|
} else {
|
|
145
|
-
|
|
164
|
+
args.push("-f", `${key}=${String(val)}`);
|
|
146
165
|
}
|
|
147
166
|
}
|
|
148
167
|
}
|
|
149
168
|
|
|
150
169
|
if (body) {
|
|
151
|
-
|
|
170
|
+
args.push("--input", "-");
|
|
152
171
|
}
|
|
153
172
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
});
|
|
161
|
-
if (!result.trim()) return {} as T;
|
|
162
|
-
return JSON.parse(result) as T;
|
|
163
|
-
} catch (e: unknown) {
|
|
164
|
-
const err = e as { stderr?: string; stdout?: string; message?: string };
|
|
165
|
-
const msg = err.stderr?.trim() || err.stdout?.trim() || err.message || String(e);
|
|
166
|
-
throw new Error(`gh api error: ${msg}`);
|
|
173
|
+
const result = ghSpawn(args, body ? JSON.stringify(body) : undefined, cwd ?? process.cwd());
|
|
174
|
+
|
|
175
|
+
const stdout = result.stdout?.trim() ?? "";
|
|
176
|
+
const stderr = result.stderr?.trim() ?? "";
|
|
177
|
+
|
|
178
|
+
if (result.status !== 0) {
|
|
179
|
+
throw new Error(`gh api error: ${stderr || stdout || result.error?.message || `exit code ${result.status}`}`);
|
|
167
180
|
}
|
|
181
|
+
|
|
182
|
+
if (!stdout) return {} as T;
|
|
183
|
+
return JSON.parse(stdout) as T;
|
|
168
184
|
}
|
|
169
185
|
|
|
170
186
|
async function fetchApi<T>(
|
|
@@ -52,7 +52,7 @@ function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportT
|
|
|
52
52
|
|
|
53
53
|
export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
54
54
|
pi.registerCommand("gsd", {
|
|
55
|
-
description: "GSD — Get
|
|
55
|
+
description: "GSD — Get Shit Done: /gsd auto|stop|status|queue|prefs|doctor|migrate",
|
|
56
56
|
|
|
57
57
|
getArgumentCompletions: (prefix: string) => {
|
|
58
58
|
const subcommands = ["auto", "stop", "status", "queue", "discuss", "prefs", "doctor", "migrate"];
|
|
@@ -63,13 +63,35 @@ export default function (pi: ExtensionAPI) {
|
|
|
63
63
|
registerGSDCommand(pi);
|
|
64
64
|
registerWorktreeCommand(pi);
|
|
65
65
|
|
|
66
|
-
// ── Dynamic-cwd bash tool
|
|
66
|
+
// ── Dynamic-cwd bash tool with default timeout ────────────────────────
|
|
67
67
|
// The built-in bash tool captures cwd at startup. This replacement uses
|
|
68
68
|
// a spawnHook to read process.cwd() dynamically so that process.chdir()
|
|
69
69
|
// (used by /worktree switch) propagates to shell commands.
|
|
70
|
-
|
|
70
|
+
//
|
|
71
|
+
// The upstream SDK's bash tool has no default timeout — if the LLM omits
|
|
72
|
+
// the timeout parameter, commands run indefinitely, causing hangs on
|
|
73
|
+
// Windows where process killing is unreliable (see #40). We wrap execute
|
|
74
|
+
// to inject a 120-second default when no timeout is provided.
|
|
75
|
+
const DEFAULT_BASH_TIMEOUT_SECS = 120;
|
|
76
|
+
const baseBash = createBashTool(process.cwd(), {
|
|
71
77
|
spawnHook: (ctx) => ({ ...ctx, cwd: process.cwd() }),
|
|
72
78
|
});
|
|
79
|
+
const dynamicBash = {
|
|
80
|
+
...baseBash,
|
|
81
|
+
execute: async (
|
|
82
|
+
toolCallId: string,
|
|
83
|
+
params: { command: string; timeout?: number },
|
|
84
|
+
signal?: AbortSignal,
|
|
85
|
+
onUpdate?: any,
|
|
86
|
+
ctx?: any,
|
|
87
|
+
) => {
|
|
88
|
+
const paramsWithTimeout = {
|
|
89
|
+
...params,
|
|
90
|
+
timeout: params.timeout ?? DEFAULT_BASH_TIMEOUT_SECS,
|
|
91
|
+
};
|
|
92
|
+
return baseBash.execute(toolCallId, paramsWithTimeout, signal, onUpdate, ctx);
|
|
93
|
+
},
|
|
94
|
+
};
|
|
73
95
|
pi.registerTool(dynamicBash as any);
|
|
74
96
|
|
|
75
97
|
// ── session_start: render branded GSD header ───────────────────────────
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{{preamble}}
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Ask: "What's the vision?" once, and then use whatever the user replies with as the vision input to continue.
|
|
4
|
+
|
|
5
|
+
Special handling: if the user message is not a project description (for example, they ask about status, branch state, or other clarifications), treat it as the vision input and proceed with discussion logic instead of repeating "What's the vision?".
|
|
4
6
|
|
|
5
7
|
## Discussion Phase
|
|
6
8
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
let passed = 0;
|
|
5
|
+
let failed = 0;
|
|
6
|
+
|
|
7
|
+
function assert(condition: boolean, message: string): void {
|
|
8
|
+
if (condition) passed++;
|
|
9
|
+
else {
|
|
10
|
+
failed++;
|
|
11
|
+
console.error(` FAIL: ${message}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const promptPath = join(process.cwd(), 'src/resources/extensions/gsd/prompts/discuss.md');
|
|
16
|
+
const discussPrompt = readFileSync(promptPath, 'utf-8');
|
|
17
|
+
|
|
18
|
+
console.log('\n=== discuss prompt: resilient vision framing ===');
|
|
19
|
+
{
|
|
20
|
+
const hardenedPattern = /Say exactly:\s*"What's the vision\?"/;
|
|
21
|
+
assert(!hardenedPattern.test(discussPrompt), 'prompt no longer uses exact-verbosity lock');
|
|
22
|
+
assert(
|
|
23
|
+
discussPrompt.includes('Ask: "What\'s the vision?" once'),
|
|
24
|
+
'prompt asks for vision exactly once',
|
|
25
|
+
);
|
|
26
|
+
assert(
|
|
27
|
+
discussPrompt.includes('Special handling'),
|
|
28
|
+
'prompt documents special handling for non-vision user messages',
|
|
29
|
+
);
|
|
30
|
+
assert(
|
|
31
|
+
discussPrompt.includes('instead of repeating "What\'s the vision?"'),
|
|
32
|
+
'prompt forbids repeating the vision question',
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(`\nResults: ${passed} passed, ${failed} failed`);
|
|
37
|
+
if (failed > 0) process.exit(1);
|
|
38
|
+
console.log('All tests passed ✓');
|
|
@@ -6,7 +6,7 @@ export default function gsdRun(pi: ExtensionAPI) {
|
|
|
6
6
|
pi.registerCommand("gsd-run", {
|
|
7
7
|
description: "Read GSD-WORKFLOW.md and execute — lightweight protocol-driven GSD",
|
|
8
8
|
async handler(args: string, ctx: ExtensionCommandContext) {
|
|
9
|
-
const workflowPath = join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
|
|
9
|
+
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
|
|
10
10
|
|
|
11
11
|
let workflow: string;
|
|
12
12
|
try {
|