gsd-pi 2.22.0 → 2.23.0

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 (128) hide show
  1. package/README.md +25 -1
  2. package/dist/cli.js +62 -4
  3. package/dist/headless.d.ts +21 -0
  4. package/dist/headless.js +346 -0
  5. package/dist/help-text.js +32 -0
  6. package/dist/mcp-server.d.ts +20 -3
  7. package/dist/mcp-server.js +21 -1
  8. package/dist/models-resolver.d.ts +32 -0
  9. package/dist/models-resolver.js +50 -0
  10. package/dist/resources/extensions/bg-shell/output-formatter.ts +36 -16
  11. package/dist/resources/extensions/bg-shell/process-manager.ts +6 -4
  12. package/dist/resources/extensions/bg-shell/types.ts +33 -1
  13. package/dist/resources/extensions/browser-tools/capture.ts +18 -16
  14. package/dist/resources/extensions/browser-tools/index.ts +20 -0
  15. package/dist/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
  16. package/dist/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
  17. package/dist/resources/extensions/browser-tools/tools/codegen.ts +274 -0
  18. package/dist/resources/extensions/browser-tools/tools/device.ts +183 -0
  19. package/dist/resources/extensions/browser-tools/tools/extract.ts +229 -0
  20. package/dist/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
  21. package/dist/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
  22. package/dist/resources/extensions/browser-tools/tools/pdf.ts +92 -0
  23. package/dist/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
  24. package/dist/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
  25. package/dist/resources/extensions/browser-tools/tools/zoom.ts +104 -0
  26. package/dist/resources/extensions/gsd/auto-dashboard.ts +2 -0
  27. package/dist/resources/extensions/gsd/auto-recovery.ts +10 -0
  28. package/dist/resources/extensions/gsd/auto.ts +437 -11
  29. package/dist/resources/extensions/gsd/captures.ts +49 -0
  30. package/dist/resources/extensions/gsd/commands.ts +20 -3
  31. package/dist/resources/extensions/gsd/dashboard-overlay.ts +16 -2
  32. package/dist/resources/extensions/gsd/diff-context.ts +73 -80
  33. package/dist/resources/extensions/gsd/doctor.ts +20 -1
  34. package/dist/resources/extensions/gsd/forensics.ts +95 -52
  35. package/dist/resources/extensions/gsd/guided-flow.ts +10 -5
  36. package/dist/resources/extensions/gsd/mcp-server.ts +33 -12
  37. package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  38. package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -0
  39. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
  40. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
  41. package/dist/resources/extensions/gsd/prompts/system.md +2 -1
  42. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +91 -0
  43. package/dist/resources/extensions/gsd/roadmap-slices.ts +41 -1
  44. package/dist/resources/extensions/gsd/session-forensics.ts +36 -2
  45. package/dist/resources/extensions/gsd/templates/milestone-validation.md +62 -0
  46. package/dist/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
  47. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +64 -0
  48. package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
  49. package/dist/resources/extensions/gsd/tests/doctor.test.ts +58 -0
  50. package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
  51. package/dist/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
  52. package/dist/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
  53. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
  54. package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
  55. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
  56. package/dist/resources/extensions/gsd/triage-resolution.ts +83 -0
  57. package/dist/resources/extensions/gsd/visualizer-overlay.ts +8 -1
  58. package/dist/resources/extensions/gsd/workspace-index.ts +34 -6
  59. package/package.json +1 -1
  60. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts +10 -0
  61. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts.map +1 -0
  62. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js +79 -0
  63. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js.map +1 -0
  64. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +18 -0
  65. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  66. package/packages/pi-coding-agent/dist/core/tools/bash.js +77 -1
  67. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  68. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -1
  69. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  70. package/packages/pi-coding-agent/dist/core/tools/index.js +1 -1
  71. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  72. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  73. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  74. package/packages/pi-coding-agent/dist/index.js +1 -1
  75. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  76. package/packages/pi-coding-agent/src/core/tools/bash-background.test.ts +91 -0
  77. package/packages/pi-coding-agent/src/core/tools/bash.ts +83 -1
  78. package/packages/pi-coding-agent/src/core/tools/index.ts +1 -0
  79. package/packages/pi-coding-agent/src/index.ts +1 -0
  80. package/src/resources/extensions/bg-shell/output-formatter.ts +36 -16
  81. package/src/resources/extensions/bg-shell/process-manager.ts +6 -4
  82. package/src/resources/extensions/bg-shell/types.ts +33 -1
  83. package/src/resources/extensions/browser-tools/capture.ts +18 -16
  84. package/src/resources/extensions/browser-tools/index.ts +20 -0
  85. package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
  86. package/src/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
  87. package/src/resources/extensions/browser-tools/tools/codegen.ts +274 -0
  88. package/src/resources/extensions/browser-tools/tools/device.ts +183 -0
  89. package/src/resources/extensions/browser-tools/tools/extract.ts +229 -0
  90. package/src/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
  91. package/src/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
  92. package/src/resources/extensions/browser-tools/tools/pdf.ts +92 -0
  93. package/src/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
  94. package/src/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
  95. package/src/resources/extensions/browser-tools/tools/zoom.ts +104 -0
  96. package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
  97. package/src/resources/extensions/gsd/auto-recovery.ts +10 -0
  98. package/src/resources/extensions/gsd/auto.ts +437 -11
  99. package/src/resources/extensions/gsd/captures.ts +49 -0
  100. package/src/resources/extensions/gsd/commands.ts +20 -3
  101. package/src/resources/extensions/gsd/dashboard-overlay.ts +16 -2
  102. package/src/resources/extensions/gsd/diff-context.ts +73 -80
  103. package/src/resources/extensions/gsd/doctor.ts +20 -1
  104. package/src/resources/extensions/gsd/forensics.ts +95 -52
  105. package/src/resources/extensions/gsd/guided-flow.ts +10 -5
  106. package/src/resources/extensions/gsd/mcp-server.ts +33 -12
  107. package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  108. package/src/resources/extensions/gsd/prompts/execute-task.md +5 -0
  109. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
  110. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
  111. package/src/resources/extensions/gsd/prompts/system.md +2 -1
  112. package/src/resources/extensions/gsd/prompts/validate-milestone.md +91 -0
  113. package/src/resources/extensions/gsd/roadmap-slices.ts +41 -1
  114. package/src/resources/extensions/gsd/session-forensics.ts +36 -2
  115. package/src/resources/extensions/gsd/templates/milestone-validation.md +62 -0
  116. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
  117. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +64 -0
  118. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
  119. package/src/resources/extensions/gsd/tests/doctor.test.ts +58 -0
  120. package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
  121. package/src/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
  122. package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
  123. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
  124. package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
  125. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
  126. package/src/resources/extensions/gsd/triage-resolution.ts +83 -0
  127. package/src/resources/extensions/gsd/visualizer-overlay.ts +8 -1
  128. package/src/resources/extensions/gsd/workspace-index.ts +34 -6
package/README.md CHANGED
@@ -221,6 +221,26 @@ gsd
221
221
 
222
222
  Both terminals read and write the same `.gsd/` files on disk. Your decisions in terminal 2 are picked up automatically at the next phase boundary — no need to stop auto mode.
223
223
 
224
+ ### Headless mode — CI and scripts
225
+
226
+ `gsd headless` runs any `/gsd` command without a TUI. Designed for CI pipelines, cron jobs, and scripted automation.
227
+
228
+ ```bash
229
+ # Run auto mode in CI
230
+ gsd headless --timeout 600000
231
+
232
+ # One unit at a time (cron-friendly)
233
+ gsd headless next
234
+
235
+ # Machine-readable status
236
+ gsd headless --json status
237
+
238
+ # Force a specific pipeline phase
239
+ gsd headless dispatch plan
240
+ ```
241
+
242
+ Headless auto-responds to interactive prompts, detects completion, and exits with structured codes: `0` complete, `1` error/timeout, `2` blocked. Pair with [remote questions](./docs/remote-questions.md) to route decisions to Slack or Discord when human input is needed.
243
+
224
244
  ### First launch
225
245
 
226
246
  On first run, GSD launches a branded setup wizard that walks you through LLM provider selection (OAuth or API key), then optional tool API keys (Brave Search, Context7, Jina, Slack, Discord). Every step is skippable — press Enter to skip any. If you have an existing Pi installation, your provider credentials (LLM and tool keys) are imported automatically. Run `gsd config` anytime to re-run the wizard.
@@ -254,7 +274,10 @@ On first run, GSD launches a branded setup wizard that walks you through LLM pro
254
274
  | `Ctrl+Alt+V` | Toggle voice transcription |
255
275
  | `Ctrl+Alt+B` | Show background shell processes |
256
276
  | `gsd config` | Re-run the setup wizard (LLM provider + tool keys) |
277
+ | `gsd update` | Update GSD to the latest version |
278
+ | `gsd headless [cmd]` | Run `/gsd` commands without TUI (CI, cron, scripts) |
257
279
  | `gsd --continue` (`-c`) | Resume the most recent session for the current directory |
280
+ | `gsd sessions` | Interactive session picker — browse and resume any saved session |
258
281
 
259
282
  ---
260
283
 
@@ -396,7 +419,7 @@ GSD ships with 14 extensions, all loaded automatically:
396
419
  | Extension | What it provides |
397
420
  | ---------------------- | ---------------------------------------------------------------------------------------------------------------------- |
398
421
  | **GSD** | Core workflow engine, auto mode, commands, dashboard |
399
- | **Browser Tools** | Playwright-based browser with form intelligence, intent-ranked element finding, and semantic actions |
422
+ | **Browser Tools** | Playwright-based browser with form intelligence, intent-ranked element finding, semantic actions, PDF export, session state persistence, network mocking, device emulation, structured extraction, visual diffing, region zoom, test code generation, and prompt injection detection |
400
423
  | **Search the Web** | Brave Search, Tavily, or Jina page extraction |
401
424
  | **Google Search** | Gemini-powered web search with AI-synthesized answers |
402
425
  | **Context7** | Up-to-date library/framework documentation |
@@ -482,6 +505,7 @@ GSD is a TypeScript application that embeds the Pi coding agent SDK.
482
505
  gsd (CLI binary)
483
506
  └─ loader.ts Sets PI_PACKAGE_DIR, GSD env vars, dynamic-imports cli.ts
484
507
  └─ cli.ts Wires SDK managers, loads extensions, starts InteractiveMode
508
+ ├─ headless.ts Headless orchestrator (spawns RPC child, auto-responds, detects completion)
485
509
  ├─ onboarding.ts First-run setup wizard (LLM provider + tool keys)
486
510
  ├─ wizard.ts Env hydration from stored auth.json credentials
487
511
  ├─ app-paths.ts ~/.gsd/agent/, ~/.gsd/sessions/, auth.json
package/dist/cli.js CHANGED
@@ -91,6 +91,59 @@ if (cliFlags.messages[0] === 'update') {
91
91
  await runUpdate();
92
92
  process.exit(0);
93
93
  }
94
+ // `gsd sessions` — list past sessions and pick one to resume
95
+ if (cliFlags.messages[0] === 'sessions') {
96
+ const cwd = process.cwd();
97
+ const safePath = `--${cwd.replace(/^[/\\]/, '').replace(/[/\\:]/g, '-')}--`;
98
+ const projectSessionsDir = join(sessionsDir, safePath);
99
+ process.stderr.write(chalk.dim(`Loading sessions for ${cwd}...\n`));
100
+ const sessions = await SessionManager.list(cwd, projectSessionsDir);
101
+ if (sessions.length === 0) {
102
+ process.stderr.write(chalk.yellow('No sessions found for this directory.\n'));
103
+ process.exit(0);
104
+ }
105
+ process.stderr.write(chalk.bold(`\n Sessions (${sessions.length}):\n\n`));
106
+ const maxShow = 20;
107
+ const toShow = sessions.slice(0, maxShow);
108
+ for (let i = 0; i < toShow.length; i++) {
109
+ const s = toShow[i];
110
+ const date = s.modified.toLocaleString();
111
+ const msgs = s.messageCount;
112
+ const name = s.name ? ` ${chalk.cyan(s.name)}` : '';
113
+ const preview = s.firstMessage
114
+ ? s.firstMessage.replace(/\n/g, ' ').substring(0, 80)
115
+ : chalk.dim('(empty)');
116
+ const num = String(i + 1).padStart(3);
117
+ process.stderr.write(` ${chalk.bold(num)}. ${chalk.green(date)} ${chalk.dim(`(${msgs} msgs)`)}${name}\n`);
118
+ process.stderr.write(` ${chalk.dim(preview)}\n\n`);
119
+ }
120
+ if (sessions.length > maxShow) {
121
+ process.stderr.write(chalk.dim(` ... and ${sessions.length - maxShow} more\n\n`));
122
+ }
123
+ // Interactive selection
124
+ const readline = await import('node:readline');
125
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
126
+ const answer = await new Promise((resolve) => {
127
+ rl.question(chalk.bold(' Enter session number to resume (or q to quit): '), resolve);
128
+ });
129
+ rl.close();
130
+ const choice = parseInt(answer, 10);
131
+ if (isNaN(choice) || choice < 1 || choice > toShow.length) {
132
+ process.stderr.write(chalk.dim('Cancelled.\n'));
133
+ process.exit(0);
134
+ }
135
+ const selected = toShow[choice - 1];
136
+ process.stderr.write(chalk.green(`\nResuming session from ${selected.modified.toLocaleString()}...\n\n`));
137
+ // Mark for the interactive session below to open this specific session
138
+ cliFlags.continue = true;
139
+ cliFlags._selectedSessionPath = selected.path;
140
+ }
141
+ // `gsd headless` — run auto-mode without TUI
142
+ if (cliFlags.messages[0] === 'headless') {
143
+ const { runHeadless, parseHeadlessArgs } = await import('./headless.js');
144
+ await runHeadless(parseHeadlessArgs(process.argv));
145
+ process.exit(0);
146
+ }
94
147
  // Pi's tool bootstrap can mis-detect already-installed fd/rg on some systems
95
148
  // because spawnSync(..., ["--version"]) returns EPERM despite a zero exit code.
96
149
  // Provision local managed binaries first so Pi sees them without probing PATH.
@@ -98,7 +151,10 @@ ensureManagedTools(join(agentDir, 'bin'));
98
151
  const authStorage = AuthStorage.create(authFilePath);
99
152
  loadStoredEnvKeys(authStorage);
100
153
  migratePiCredentials(authStorage);
101
- const modelRegistry = new ModelRegistry(authStorage);
154
+ // Resolve models.json path with fallback to ~/.pi/agent/models.json
155
+ const { resolveModelsJsonPath } = await import('./models-resolver.js');
156
+ const modelsJsonPath = resolveModelsJsonPath();
157
+ const modelRegistry = new ModelRegistry(authStorage, modelsJsonPath);
102
158
  const settingsManager = SettingsManager.create(agentDir);
103
159
  // Run onboarding wizard on first launch (no LLM provider configured)
104
160
  if (!isPrintMode && shouldRunOnboarding(authStorage, settingsManager.getDefaultProvider())) {
@@ -298,9 +354,11 @@ if (existsSync(sessionsDir)) {
298
354
  // Non-fatal — don't block startup if migration fails
299
355
  }
300
356
  }
301
- const sessionManager = cliFlags.continue
302
- ? SessionManager.continueRecent(cwd, projectSessionsDir)
303
- : SessionManager.create(cwd, projectSessionsDir);
357
+ const sessionManager = cliFlags._selectedSessionPath
358
+ ? SessionManager.open(cliFlags._selectedSessionPath, projectSessionsDir)
359
+ : cliFlags.continue
360
+ ? SessionManager.continueRecent(cwd, projectSessionsDir)
361
+ : SessionManager.create(cwd, projectSessionsDir);
304
362
  exitIfManagedResourcesAreNewer(agentDir);
305
363
  initResources(agentDir);
306
364
  const resourceLoader = buildResourceLoader(agentDir);
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Headless Orchestrator — `gsd headless`
3
+ *
4
+ * Runs any /gsd subcommand without a TUI by spawning a child process in
5
+ * RPC mode, auto-responding to extension UI requests, and streaming
6
+ * progress to stderr.
7
+ *
8
+ * Exit codes:
9
+ * 0 — complete (command finished successfully)
10
+ * 1 — error or timeout
11
+ * 2 — blocked (command reported a blocker)
12
+ */
13
+ export interface HeadlessOptions {
14
+ timeout: number;
15
+ json: boolean;
16
+ model?: string;
17
+ command: string;
18
+ commandArgs: string[];
19
+ }
20
+ export declare function parseHeadlessArgs(argv: string[]): HeadlessOptions;
21
+ export declare function runHeadless(options: HeadlessOptions): Promise<void>;
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Headless Orchestrator — `gsd headless`
3
+ *
4
+ * Runs any /gsd subcommand without a TUI by spawning a child process in
5
+ * RPC mode, auto-responding to extension UI requests, and streaming
6
+ * progress to stderr.
7
+ *
8
+ * Exit codes:
9
+ * 0 — complete (command finished successfully)
10
+ * 1 — error or timeout
11
+ * 2 — blocked (command reported a blocker)
12
+ */
13
+ import { existsSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ // RpcClient is not in @gsd/pi-coding-agent's public exports — import from dist directly.
16
+ // This relative path resolves correctly from both src/ (via tsx) and dist/ (compiled).
17
+ import { RpcClient } from '../packages/pi-coding-agent/dist/modes/rpc/rpc-client.js';
18
+ // ---------------------------------------------------------------------------
19
+ // CLI Argument Parser
20
+ // ---------------------------------------------------------------------------
21
+ export function parseHeadlessArgs(argv) {
22
+ const options = {
23
+ timeout: 300_000,
24
+ json: false,
25
+ command: 'auto',
26
+ commandArgs: [],
27
+ };
28
+ const args = argv.slice(2);
29
+ let positionalStarted = false;
30
+ for (let i = 0; i < args.length; i++) {
31
+ const arg = args[i];
32
+ if (arg === 'headless')
33
+ continue;
34
+ if (!positionalStarted && arg.startsWith('--')) {
35
+ if (arg === '--timeout' && i + 1 < args.length) {
36
+ options.timeout = parseInt(args[++i], 10);
37
+ if (Number.isNaN(options.timeout) || options.timeout <= 0) {
38
+ process.stderr.write('[headless] Error: --timeout must be a positive integer (milliseconds)\n');
39
+ process.exit(1);
40
+ }
41
+ }
42
+ else if (arg === '--json') {
43
+ options.json = true;
44
+ }
45
+ else if (arg === '--model' && i + 1 < args.length) {
46
+ // --model can also be passed from the main CLI; headless-specific takes precedence
47
+ options.model = args[++i];
48
+ }
49
+ }
50
+ else if (!positionalStarted) {
51
+ positionalStarted = true;
52
+ options.command = arg;
53
+ }
54
+ else {
55
+ options.commandArgs.push(arg);
56
+ }
57
+ }
58
+ return options;
59
+ }
60
+ // ---------------------------------------------------------------------------
61
+ // JSONL Helper
62
+ // ---------------------------------------------------------------------------
63
+ function serializeJsonLine(obj) {
64
+ return JSON.stringify(obj) + '\n';
65
+ }
66
+ // ---------------------------------------------------------------------------
67
+ // Extension UI Auto-Responder
68
+ // ---------------------------------------------------------------------------
69
+ function handleExtensionUIRequest(event, writeToStdin) {
70
+ const { id, method } = event;
71
+ let response;
72
+ switch (method) {
73
+ case 'select':
74
+ response = { type: 'extension_ui_response', id, value: event.options?.[0] ?? '' };
75
+ break;
76
+ case 'confirm':
77
+ response = { type: 'extension_ui_response', id, confirmed: true };
78
+ break;
79
+ case 'input':
80
+ response = { type: 'extension_ui_response', id, value: '' };
81
+ break;
82
+ case 'editor':
83
+ response = { type: 'extension_ui_response', id, value: event.prefill ?? '' };
84
+ break;
85
+ case 'notify':
86
+ case 'setStatus':
87
+ case 'setWidget':
88
+ case 'setTitle':
89
+ case 'set_editor_text':
90
+ response = { type: 'extension_ui_response', id, value: '' };
91
+ break;
92
+ default:
93
+ process.stderr.write(`[headless] Warning: unknown extension_ui_request method "${method}", cancelling\n`);
94
+ response = { type: 'extension_ui_response', id, cancelled: true };
95
+ break;
96
+ }
97
+ writeToStdin(serializeJsonLine(response));
98
+ }
99
+ // ---------------------------------------------------------------------------
100
+ // Progress Formatter
101
+ // ---------------------------------------------------------------------------
102
+ function formatProgress(event) {
103
+ const type = String(event.type ?? '');
104
+ switch (type) {
105
+ case 'tool_execution_start':
106
+ return `[tool] ${event.toolName ?? 'unknown'}`;
107
+ case 'agent_start':
108
+ return '[agent] Session started';
109
+ case 'agent_end':
110
+ return '[agent] Session ended';
111
+ case 'extension_ui_request':
112
+ if (event.method === 'notify') {
113
+ return `[gsd] ${event.message ?? ''}`;
114
+ }
115
+ return null;
116
+ default:
117
+ return null;
118
+ }
119
+ }
120
+ // ---------------------------------------------------------------------------
121
+ // Completion Detection
122
+ // ---------------------------------------------------------------------------
123
+ const TERMINAL_KEYWORDS = ['complete', 'stopped', 'blocked'];
124
+ const IDLE_TIMEOUT_MS = 15_000;
125
+ function isTerminalNotification(event) {
126
+ if (event.type !== 'extension_ui_request' || event.method !== 'notify')
127
+ return false;
128
+ const message = String(event.message ?? '').toLowerCase();
129
+ return TERMINAL_KEYWORDS.some((kw) => message.includes(kw));
130
+ }
131
+ function isBlockedNotification(event) {
132
+ if (event.type !== 'extension_ui_request' || event.method !== 'notify')
133
+ return false;
134
+ return String(event.message ?? '').toLowerCase().includes('blocked');
135
+ }
136
+ // ---------------------------------------------------------------------------
137
+ // Quick Command Detection
138
+ // ---------------------------------------------------------------------------
139
+ const QUICK_COMMANDS = new Set([
140
+ 'status', 'queue', 'history', 'hooks', 'export', 'stop', 'pause',
141
+ 'capture', 'skip', 'undo', 'knowledge', 'config', 'prefs',
142
+ 'cleanup', 'migrate', 'doctor', 'remote', 'help', 'steer',
143
+ 'triage', 'visualize',
144
+ ]);
145
+ function isQuickCommand(command) {
146
+ return QUICK_COMMANDS.has(command);
147
+ }
148
+ // ---------------------------------------------------------------------------
149
+ // Main Orchestrator
150
+ // ---------------------------------------------------------------------------
151
+ export async function runHeadless(options) {
152
+ const startTime = Date.now();
153
+ // Validate .gsd/ directory
154
+ const gsdDir = join(process.cwd(), '.gsd');
155
+ if (!existsSync(gsdDir)) {
156
+ process.stderr.write('[headless] Error: No .gsd/ directory found in current directory.\n');
157
+ process.stderr.write("[headless] Run 'gsd' interactively first to initialize a project.\n");
158
+ process.exit(1);
159
+ }
160
+ // Resolve CLI path for the child process
161
+ const cliPath = process.env.GSD_BIN_PATH || process.argv[1];
162
+ if (!cliPath) {
163
+ process.stderr.write('[headless] Error: Cannot determine CLI path. Set GSD_BIN_PATH or run via gsd.\n');
164
+ process.exit(1);
165
+ }
166
+ // Create RPC client
167
+ const clientOptions = {
168
+ cliPath,
169
+ cwd: process.cwd(),
170
+ };
171
+ if (options.model) {
172
+ clientOptions.model = options.model;
173
+ }
174
+ const client = new RpcClient(clientOptions);
175
+ // Event tracking
176
+ let totalEvents = 0;
177
+ let toolCallCount = 0;
178
+ let blocked = false;
179
+ let completed = false;
180
+ let exitCode = 0;
181
+ const recentEvents = [];
182
+ function trackEvent(event) {
183
+ totalEvents++;
184
+ const type = String(event.type ?? 'unknown');
185
+ if (type === 'tool_execution_start') {
186
+ toolCallCount++;
187
+ }
188
+ // Keep last 20 events for diagnostics
189
+ const detail = type === 'tool_execution_start'
190
+ ? String(event.toolName ?? '')
191
+ : type === 'extension_ui_request'
192
+ ? `${event.method}: ${event.title ?? event.message ?? ''}`
193
+ : undefined;
194
+ recentEvents.push({ type, timestamp: Date.now(), detail });
195
+ if (recentEvents.length > 20)
196
+ recentEvents.shift();
197
+ }
198
+ // Stdin writer for sending extension_ui_response to child
199
+ let stdinWriter = null;
200
+ // Completion promise
201
+ let resolveCompletion;
202
+ const completionPromise = new Promise((resolve) => {
203
+ resolveCompletion = resolve;
204
+ });
205
+ // Idle timeout — fallback completion detection
206
+ let idleTimer = null;
207
+ function resetIdleTimer() {
208
+ if (idleTimer)
209
+ clearTimeout(idleTimer);
210
+ if (toolCallCount > 0) {
211
+ idleTimer = setTimeout(() => {
212
+ completed = true;
213
+ resolveCompletion();
214
+ }, IDLE_TIMEOUT_MS);
215
+ }
216
+ }
217
+ // Overall timeout
218
+ const timeoutTimer = setTimeout(() => {
219
+ process.stderr.write(`[headless] Timeout after ${options.timeout / 1000}s\n`);
220
+ exitCode = 1;
221
+ resolveCompletion();
222
+ }, options.timeout);
223
+ // Event handler
224
+ client.onEvent((event) => {
225
+ const eventObj = event;
226
+ trackEvent(eventObj);
227
+ resetIdleTimer();
228
+ // --json mode: forward all events as JSONL to stdout
229
+ if (options.json) {
230
+ process.stdout.write(JSON.stringify(eventObj) + '\n');
231
+ }
232
+ else {
233
+ // Progress output to stderr
234
+ const line = formatProgress(eventObj);
235
+ if (line)
236
+ process.stderr.write(line + '\n');
237
+ }
238
+ // Handle extension_ui_request
239
+ if (eventObj.type === 'extension_ui_request' && stdinWriter) {
240
+ // Check for terminal notification before auto-responding
241
+ if (isBlockedNotification(eventObj)) {
242
+ blocked = true;
243
+ }
244
+ if (isTerminalNotification(eventObj)) {
245
+ completed = true;
246
+ }
247
+ handleExtensionUIRequest(eventObj, stdinWriter);
248
+ // If we detected a terminal notification, resolve after responding
249
+ if (completed) {
250
+ exitCode = blocked ? 2 : 0;
251
+ resolveCompletion();
252
+ return;
253
+ }
254
+ }
255
+ // Quick commands: resolve on first agent_end
256
+ if (eventObj.type === 'agent_end' && isQuickCommand(options.command) && !completed) {
257
+ completed = true;
258
+ resolveCompletion();
259
+ return;
260
+ }
261
+ // Long-running commands: agent_end after tool execution — possible completion
262
+ // The idle timer + terminal notification handle this case.
263
+ });
264
+ // Signal handling
265
+ const signalHandler = () => {
266
+ process.stderr.write('\n[headless] Interrupted, stopping child process...\n');
267
+ exitCode = 1;
268
+ client.stop().finally(() => {
269
+ clearTimeout(timeoutTimer);
270
+ if (idleTimer)
271
+ clearTimeout(idleTimer);
272
+ process.exit(exitCode);
273
+ });
274
+ };
275
+ process.on('SIGINT', signalHandler);
276
+ process.on('SIGTERM', signalHandler);
277
+ // Start the RPC session
278
+ try {
279
+ await client.start();
280
+ }
281
+ catch (err) {
282
+ process.stderr.write(`[headless] Error: Failed to start RPC session: ${err instanceof Error ? err.message : String(err)}\n`);
283
+ clearTimeout(timeoutTimer);
284
+ process.exit(1);
285
+ }
286
+ // Access stdin writer from the internal process
287
+ const internalProcess = client.process;
288
+ if (!internalProcess?.stdin) {
289
+ process.stderr.write('[headless] Error: Cannot access child process stdin\n');
290
+ await client.stop();
291
+ clearTimeout(timeoutTimer);
292
+ process.exit(1);
293
+ }
294
+ stdinWriter = (data) => {
295
+ internalProcess.stdin.write(data);
296
+ };
297
+ // Detect child process crash
298
+ internalProcess.on('exit', (code) => {
299
+ if (!completed) {
300
+ const msg = `[headless] Child process exited unexpectedly with code ${code ?? 'null'}\n`;
301
+ process.stderr.write(msg);
302
+ exitCode = 1;
303
+ resolveCompletion();
304
+ }
305
+ });
306
+ if (!options.json) {
307
+ process.stderr.write(`[headless] Running /gsd ${options.command}${options.commandArgs.length > 0 ? ' ' + options.commandArgs.join(' ') : ''}...\n`);
308
+ }
309
+ // Send the command
310
+ const command = `/gsd ${options.command}${options.commandArgs.length > 0 ? ' ' + options.commandArgs.join(' ') : ''}`;
311
+ try {
312
+ await client.prompt(command);
313
+ }
314
+ catch (err) {
315
+ process.stderr.write(`[headless] Error: Failed to send prompt: ${err instanceof Error ? err.message : String(err)}\n`);
316
+ exitCode = 1;
317
+ }
318
+ // Wait for completion
319
+ if (exitCode === 0 || exitCode === 2) {
320
+ await completionPromise;
321
+ }
322
+ // Cleanup
323
+ clearTimeout(timeoutTimer);
324
+ if (idleTimer)
325
+ clearTimeout(idleTimer);
326
+ process.removeListener('SIGINT', signalHandler);
327
+ process.removeListener('SIGTERM', signalHandler);
328
+ await client.stop();
329
+ // Summary
330
+ const duration = ((Date.now() - startTime) / 1000).toFixed(1);
331
+ const status = blocked ? 'blocked' : exitCode === 1 ? (totalEvents === 0 ? 'error' : 'timeout') : 'complete';
332
+ process.stderr.write(`[headless] Status: ${status}\n`);
333
+ process.stderr.write(`[headless] Duration: ${duration}s\n`);
334
+ process.stderr.write(`[headless] Events: ${totalEvents} total, ${toolCallCount} tool calls\n`);
335
+ // On failure, print last 5 events for diagnostics
336
+ if (exitCode !== 0) {
337
+ const lastFive = recentEvents.slice(-5);
338
+ if (lastFive.length > 0) {
339
+ process.stderr.write('[headless] Last events:\n');
340
+ for (const e of lastFive) {
341
+ process.stderr.write(` ${e.type}${e.detail ? `: ${e.detail}` : ''}\n`);
342
+ }
343
+ }
344
+ }
345
+ process.exit(exitCode);
346
+ }
package/dist/help-text.js CHANGED
@@ -17,6 +17,36 @@ const SUBCOMMAND_HELP = {
17
17
  '',
18
18
  'Equivalent to: npm install -g gsd-pi@latest',
19
19
  ].join('\n'),
20
+ sessions: [
21
+ 'Usage: gsd sessions',
22
+ '',
23
+ 'List all saved sessions for the current directory and interactively',
24
+ 'pick one to resume. Shows date, message count, and a preview of the',
25
+ 'first message for each session.',
26
+ '',
27
+ 'Sessions are stored per-directory, so you only see sessions that were',
28
+ 'started from the current working directory.',
29
+ '',
30
+ 'Compare with --continue (-c) which always resumes the most recent session.',
31
+ ].join('\n'),
32
+ headless: [
33
+ 'Usage: gsd headless [flags] [command] [args...]',
34
+ '',
35
+ 'Run /gsd commands without the TUI. Default command: auto',
36
+ '',
37
+ 'Flags:',
38
+ ' --timeout N Overall timeout in ms (default: 300000)',
39
+ ' --json JSONL event stream to stdout',
40
+ ' --model ID Override model',
41
+ '',
42
+ 'Examples:',
43
+ ' gsd headless Run /gsd auto',
44
+ ' gsd headless next Run one unit',
45
+ ' gsd headless --json status Machine-readable status',
46
+ ' gsd headless --timeout 60000 With 1-minute timeout',
47
+ '',
48
+ 'Exit codes: 0 = complete, 1 = error/timeout, 2 = blocked',
49
+ ].join('\n'),
20
50
  };
21
51
  export function printHelp(version) {
22
52
  process.stdout.write(`GSD v${version} — Get Shit Done\n\n`);
@@ -35,6 +65,8 @@ export function printHelp(version) {
35
65
  process.stdout.write('\nSubcommands:\n');
36
66
  process.stdout.write(' config Re-run the setup wizard\n');
37
67
  process.stdout.write(' update Update GSD to the latest version\n');
68
+ process.stdout.write(' sessions List and resume a past session\n');
69
+ process.stdout.write(' headless [cmd] [args] Run /gsd commands without TUI (default: auto)\n');
38
70
  process.stdout.write('\nRun gsd <subcommand> --help for subcommand-specific help.\n');
39
71
  }
40
72
  export function printSubcommandHelp(subcommand, version) {
@@ -1,4 +1,8 @@
1
- interface McpTool {
1
+ /**
2
+ * Minimal tool interface matching GSD's AgentTool shape.
3
+ * Avoids a direct dependency on @gsd/pi-agent-core from this compiled module.
4
+ */
5
+ export interface McpToolDef {
2
6
  name: string;
3
7
  description: string;
4
8
  parameters: Record<string, unknown>;
@@ -11,8 +15,21 @@ interface McpTool {
11
15
  }>;
12
16
  }>;
13
17
  }
18
+ /**
19
+ * Starts a native MCP (Model Context Protocol) server over stdin/stdout.
20
+ *
21
+ * This enables GSD's tools (read, write, edit, bash, grep, glob, ls, etc.)
22
+ * to be used by external AI clients such as Claude Desktop, VS Code Copilot,
23
+ * and any MCP-compatible host.
24
+ *
25
+ * The server registers all tools from the agent session's tool registry and
26
+ * maps MCP tools/list and tools/call requests to GSD tool definitions and
27
+ * execution, respectively.
28
+ *
29
+ * All MCP SDK imports are dynamic to avoid subpath export resolution issues
30
+ * with TypeScript's NodeNext module resolution.
31
+ */
14
32
  export declare function startMcpServer(options: {
15
- tools: McpTool[];
33
+ tools: McpToolDef[];
16
34
  version?: string;
17
35
  }): Promise<void>;
18
- export {};
@@ -2,6 +2,20 @@
2
2
  // at runtime but TypeScript cannot statically type-check. We construct the
3
3
  // specifiers dynamically so tsc treats them as `any`.
4
4
  const MCP_PKG = '@modelcontextprotocol/sdk';
5
+ /**
6
+ * Starts a native MCP (Model Context Protocol) server over stdin/stdout.
7
+ *
8
+ * This enables GSD's tools (read, write, edit, bash, grep, glob, ls, etc.)
9
+ * to be used by external AI clients such as Claude Desktop, VS Code Copilot,
10
+ * and any MCP-compatible host.
11
+ *
12
+ * The server registers all tools from the agent session's tool registry and
13
+ * maps MCP tools/list and tools/call requests to GSD tool definitions and
14
+ * execution, respectively.
15
+ *
16
+ * All MCP SDK imports are dynamic to avoid subpath export resolution issues
17
+ * with TypeScript's NodeNext module resolution.
18
+ */
5
19
  export async function startMcpServer(options) {
6
20
  const { tools, version = '0.0.0' } = options;
7
21
  const serverMod = await import(`${MCP_PKG}/server`);
@@ -10,11 +24,13 @@ export async function startMcpServer(options) {
10
24
  const Server = serverMod.Server;
11
25
  const StdioServerTransport = stdioMod.StdioServerTransport;
12
26
  const { ListToolsRequestSchema, CallToolRequestSchema } = typesMod;
27
+ // Build a lookup map for fast tool resolution on calls
13
28
  const toolMap = new Map();
14
29
  for (const tool of tools) {
15
30
  toolMap.set(tool.name, tool);
16
31
  }
17
32
  const server = new Server({ name: 'gsd', version }, { capabilities: { tools: {} } });
33
+ // tools/list — return every registered GSD tool with its JSON Schema parameters
18
34
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
19
35
  tools: tools.map((t) => ({
20
36
  name: t.name,
@@ -22,6 +38,7 @@ export async function startMcpServer(options) {
22
38
  inputSchema: t.parameters,
23
39
  })),
24
40
  }));
41
+ // tools/call — execute the requested tool and return content blocks
25
42
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
26
43
  const { name, arguments: args } = request.params;
27
44
  const tool = toolMap.get(name);
@@ -32,7 +49,9 @@ export async function startMcpServer(options) {
32
49
  };
33
50
  }
34
51
  try {
35
- const result = await tool.execute(`mcp-${Date.now()}`, args ?? {}, undefined, undefined);
52
+ const result = await tool.execute(`mcp-${Date.now()}`, args ?? {}, undefined, // no AbortSignal
53
+ undefined);
54
+ // Convert AgentToolResult content blocks to MCP content format
36
55
  const content = result.content.map((block) => {
37
56
  if (block.type === 'text')
38
57
  return { type: 'text', text: block.text ?? '' };
@@ -47,6 +66,7 @@ export async function startMcpServer(options) {
47
66
  return { isError: true, content: [{ type: 'text', text: message }] };
48
67
  }
49
68
  });
69
+ // Connect to stdin/stdout transport
50
70
  const transport = new StdioServerTransport();
51
71
  await server.connect(transport);
52
72
  process.stderr.write(`[gsd] MCP server started (v${version})\n`);