get-research-done 1.1.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 (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +560 -0
  3. package/agents/grd-architect.md +789 -0
  4. package/agents/grd-codebase-mapper.md +738 -0
  5. package/agents/grd-critic.md +1065 -0
  6. package/agents/grd-debugger.md +1203 -0
  7. package/agents/grd-evaluator.md +948 -0
  8. package/agents/grd-executor.md +784 -0
  9. package/agents/grd-explorer.md +2063 -0
  10. package/agents/grd-graduator.md +484 -0
  11. package/agents/grd-integration-checker.md +423 -0
  12. package/agents/grd-phase-researcher.md +641 -0
  13. package/agents/grd-plan-checker.md +745 -0
  14. package/agents/grd-planner.md +1386 -0
  15. package/agents/grd-project-researcher.md +865 -0
  16. package/agents/grd-research-synthesizer.md +256 -0
  17. package/agents/grd-researcher.md +2361 -0
  18. package/agents/grd-roadmapper.md +605 -0
  19. package/agents/grd-verifier.md +778 -0
  20. package/bin/install.js +1294 -0
  21. package/commands/grd/add-phase.md +207 -0
  22. package/commands/grd/add-todo.md +193 -0
  23. package/commands/grd/architect.md +283 -0
  24. package/commands/grd/audit-milestone.md +277 -0
  25. package/commands/grd/check-todos.md +228 -0
  26. package/commands/grd/complete-milestone.md +136 -0
  27. package/commands/grd/debug.md +169 -0
  28. package/commands/grd/discuss-phase.md +86 -0
  29. package/commands/grd/evaluate.md +1095 -0
  30. package/commands/grd/execute-phase.md +339 -0
  31. package/commands/grd/explore.md +258 -0
  32. package/commands/grd/graduate.md +323 -0
  33. package/commands/grd/help.md +482 -0
  34. package/commands/grd/insert-phase.md +227 -0
  35. package/commands/grd/insights.md +231 -0
  36. package/commands/grd/join-discord.md +18 -0
  37. package/commands/grd/list-phase-assumptions.md +50 -0
  38. package/commands/grd/map-codebase.md +71 -0
  39. package/commands/grd/new-milestone.md +721 -0
  40. package/commands/grd/new-project.md +1008 -0
  41. package/commands/grd/pause-work.md +134 -0
  42. package/commands/grd/plan-milestone-gaps.md +295 -0
  43. package/commands/grd/plan-phase.md +525 -0
  44. package/commands/grd/progress.md +364 -0
  45. package/commands/grd/quick-explore.md +236 -0
  46. package/commands/grd/quick.md +309 -0
  47. package/commands/grd/remove-phase.md +349 -0
  48. package/commands/grd/research-phase.md +200 -0
  49. package/commands/grd/research.md +681 -0
  50. package/commands/grd/resume-work.md +40 -0
  51. package/commands/grd/set-profile.md +106 -0
  52. package/commands/grd/settings.md +136 -0
  53. package/commands/grd/update.md +172 -0
  54. package/commands/grd/verify-work.md +219 -0
  55. package/get-research-done/config/default.json +15 -0
  56. package/get-research-done/references/checkpoints.md +1078 -0
  57. package/get-research-done/references/continuation-format.md +249 -0
  58. package/get-research-done/references/git-integration.md +254 -0
  59. package/get-research-done/references/model-profiles.md +73 -0
  60. package/get-research-done/references/planning-config.md +94 -0
  61. package/get-research-done/references/questioning.md +141 -0
  62. package/get-research-done/references/tdd.md +263 -0
  63. package/get-research-done/references/ui-brand.md +160 -0
  64. package/get-research-done/references/verification-patterns.md +612 -0
  65. package/get-research-done/templates/DEBUG.md +159 -0
  66. package/get-research-done/templates/UAT.md +247 -0
  67. package/get-research-done/templates/archive-reason.md +195 -0
  68. package/get-research-done/templates/codebase/architecture.md +255 -0
  69. package/get-research-done/templates/codebase/concerns.md +310 -0
  70. package/get-research-done/templates/codebase/conventions.md +307 -0
  71. package/get-research-done/templates/codebase/integrations.md +280 -0
  72. package/get-research-done/templates/codebase/stack.md +186 -0
  73. package/get-research-done/templates/codebase/structure.md +285 -0
  74. package/get-research-done/templates/codebase/testing.md +480 -0
  75. package/get-research-done/templates/config.json +35 -0
  76. package/get-research-done/templates/context.md +283 -0
  77. package/get-research-done/templates/continue-here.md +78 -0
  78. package/get-research-done/templates/critic-log.md +288 -0
  79. package/get-research-done/templates/data-report.md +173 -0
  80. package/get-research-done/templates/debug-subagent-prompt.md +91 -0
  81. package/get-research-done/templates/decision-log.md +58 -0
  82. package/get-research-done/templates/decision.md +138 -0
  83. package/get-research-done/templates/discovery.md +146 -0
  84. package/get-research-done/templates/experiment-readme.md +104 -0
  85. package/get-research-done/templates/graduated-script.md +180 -0
  86. package/get-research-done/templates/iteration-summary.md +234 -0
  87. package/get-research-done/templates/milestone-archive.md +123 -0
  88. package/get-research-done/templates/milestone.md +115 -0
  89. package/get-research-done/templates/objective.md +271 -0
  90. package/get-research-done/templates/phase-prompt.md +567 -0
  91. package/get-research-done/templates/planner-subagent-prompt.md +117 -0
  92. package/get-research-done/templates/project.md +184 -0
  93. package/get-research-done/templates/requirements.md +231 -0
  94. package/get-research-done/templates/research-project/ARCHITECTURE.md +204 -0
  95. package/get-research-done/templates/research-project/FEATURES.md +147 -0
  96. package/get-research-done/templates/research-project/PITFALLS.md +200 -0
  97. package/get-research-done/templates/research-project/STACK.md +120 -0
  98. package/get-research-done/templates/research-project/SUMMARY.md +170 -0
  99. package/get-research-done/templates/research.md +529 -0
  100. package/get-research-done/templates/roadmap.md +202 -0
  101. package/get-research-done/templates/scorecard.json +113 -0
  102. package/get-research-done/templates/state.md +287 -0
  103. package/get-research-done/templates/summary.md +246 -0
  104. package/get-research-done/templates/user-setup.md +311 -0
  105. package/get-research-done/templates/verification-report.md +322 -0
  106. package/get-research-done/workflows/complete-milestone.md +756 -0
  107. package/get-research-done/workflows/diagnose-issues.md +231 -0
  108. package/get-research-done/workflows/discovery-phase.md +289 -0
  109. package/get-research-done/workflows/discuss-phase.md +433 -0
  110. package/get-research-done/workflows/execute-phase.md +657 -0
  111. package/get-research-done/workflows/execute-plan.md +1844 -0
  112. package/get-research-done/workflows/list-phase-assumptions.md +178 -0
  113. package/get-research-done/workflows/map-codebase.md +322 -0
  114. package/get-research-done/workflows/resume-project.md +307 -0
  115. package/get-research-done/workflows/transition.md +556 -0
  116. package/get-research-done/workflows/verify-phase.md +628 -0
  117. package/get-research-done/workflows/verify-work.md +596 -0
  118. package/hooks/dist/grd-check-update.js +61 -0
  119. package/hooks/dist/grd-statusline.js +84 -0
  120. package/package.json +47 -0
  121. package/scripts/audit-help-commands.sh +115 -0
  122. package/scripts/build-hooks.js +42 -0
  123. package/scripts/verify-all-commands.sh +246 -0
  124. package/scripts/verify-architect-warning.sh +35 -0
  125. package/scripts/verify-insights-mode.sh +40 -0
  126. package/scripts/verify-quick-mode.sh +20 -0
  127. package/scripts/verify-revise-data-routing.sh +139 -0
package/bin/install.js ADDED
@@ -0,0 +1,1294 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const readline = require('readline');
7
+
8
+ // Colors
9
+ const cyan = '\x1b[36m';
10
+ const green = '\x1b[32m';
11
+ const yellow = '\x1b[33m';
12
+ const dim = '\x1b[2m';
13
+ const reset = '\x1b[0m';
14
+
15
+ // Get version from package.json
16
+ const pkg = require('../package.json');
17
+
18
+ // Parse args
19
+ const args = process.argv.slice(2);
20
+ const hasGlobal = args.includes('--global') || args.includes('-g');
21
+ const hasLocal = args.includes('--local') || args.includes('-l');
22
+ const hasOpencode = args.includes('--opencode');
23
+ const hasClaude = args.includes('--claude');
24
+ const hasBoth = args.includes('--both');
25
+ const hasUninstall = args.includes('--uninstall') || args.includes('-u');
26
+
27
+ // Runtime selection - can be set by flags or interactive prompt
28
+ let selectedRuntimes = [];
29
+ if (hasBoth) {
30
+ selectedRuntimes = ['claude', 'opencode'];
31
+ } else if (hasOpencode) {
32
+ selectedRuntimes = ['opencode'];
33
+ } else if (hasClaude) {
34
+ selectedRuntimes = ['claude'];
35
+ }
36
+
37
+ // Helper to get directory name for a runtime (used for local/project installs)
38
+ function getDirName(runtime) {
39
+ return runtime === 'opencode' ? '.opencode' : '.claude';
40
+ }
41
+
42
+ /**
43
+ * Get the global config directory for OpenCode
44
+ * OpenCode follows XDG Base Directory spec and uses ~/.config/opencode/
45
+ * Priority: OPENCODE_CONFIG_DIR > dirname(OPENCODE_CONFIG) > XDG_CONFIG_HOME/opencode > ~/.config/opencode
46
+ */
47
+ function getOpencodeGlobalDir() {
48
+ // 1. Explicit OPENCODE_CONFIG_DIR env var
49
+ if (process.env.OPENCODE_CONFIG_DIR) {
50
+ return expandTilde(process.env.OPENCODE_CONFIG_DIR);
51
+ }
52
+
53
+ // 2. OPENCODE_CONFIG env var (use its directory)
54
+ if (process.env.OPENCODE_CONFIG) {
55
+ return path.dirname(expandTilde(process.env.OPENCODE_CONFIG));
56
+ }
57
+
58
+ // 3. XDG_CONFIG_HOME/opencode
59
+ if (process.env.XDG_CONFIG_HOME) {
60
+ return path.join(expandTilde(process.env.XDG_CONFIG_HOME), 'opencode');
61
+ }
62
+
63
+ // 4. Default: ~/.config/opencode (XDG default)
64
+ return path.join(os.homedir(), '.config', 'opencode');
65
+ }
66
+
67
+ /**
68
+ * Get the global config directory for a runtime
69
+ * @param {string} runtime - 'claude' or 'opencode'
70
+ * @param {string|null} explicitDir - Explicit directory from --config-dir flag
71
+ */
72
+ function getGlobalDir(runtime, explicitDir = null) {
73
+ if (runtime === 'opencode') {
74
+ // For OpenCode, --config-dir overrides env vars
75
+ if (explicitDir) {
76
+ return expandTilde(explicitDir);
77
+ }
78
+ return getOpencodeGlobalDir();
79
+ }
80
+
81
+ // Claude Code: --config-dir > CLAUDE_CONFIG_DIR > ~/.claude
82
+ if (explicitDir) {
83
+ return expandTilde(explicitDir);
84
+ }
85
+ if (process.env.CLAUDE_CONFIG_DIR) {
86
+ return expandTilde(process.env.CLAUDE_CONFIG_DIR);
87
+ }
88
+ return path.join(os.homedir(), '.claude');
89
+ }
90
+
91
+ const banner = `
92
+ ${cyan} ██████╗ ██████╗ ██████╗
93
+ ██╔════╝ ██╔══██╗██╔══██╗
94
+ ██║ ███╗██████╔╝██║ ██║
95
+ ██║ ██║██╔══██╗██║ ██║
96
+ ╚██████╔╝██║ ██║██████╔╝
97
+ ╚═════╝ ╚═╝ ╚═╝╚═════╝${reset}
98
+
99
+ Get Research Done ${dim}v${pkg.version}${reset}
100
+ A recursive, agentic framework for ML research
101
+ with hypothesis-driven experimentation for Claude Code by Ulmentflam.
102
+ `;
103
+
104
+ // Parse --config-dir argument
105
+ function parseConfigDirArg() {
106
+ const configDirIndex = args.findIndex(arg => arg === '--config-dir' || arg === '-c');
107
+ if (configDirIndex !== -1) {
108
+ const nextArg = args[configDirIndex + 1];
109
+ // Error if --config-dir is provided without a value or next arg is another flag
110
+ if (!nextArg || nextArg.startsWith('-')) {
111
+ console.error(` ${yellow}--config-dir requires a path argument${reset}`);
112
+ process.exit(1);
113
+ }
114
+ return nextArg;
115
+ }
116
+ // Also handle --config-dir=value format
117
+ const configDirArg = args.find(arg => arg.startsWith('--config-dir=') || arg.startsWith('-c='));
118
+ if (configDirArg) {
119
+ const value = configDirArg.split('=')[1];
120
+ if (!value) {
121
+ console.error(` ${yellow}--config-dir requires a non-empty path${reset}`);
122
+ process.exit(1);
123
+ }
124
+ return value;
125
+ }
126
+ return null;
127
+ }
128
+ const explicitConfigDir = parseConfigDirArg();
129
+ const hasHelp = args.includes('--help') || args.includes('-h');
130
+ const forceStatusline = args.includes('--force-statusline');
131
+
132
+ console.log(banner);
133
+
134
+ // Show help if requested
135
+ if (hasHelp) {
136
+ console.log(` ${yellow}Usage:${reset} npx get-research-done [options]
137
+
138
+ ${yellow}Options:${reset}
139
+ ${cyan}-g, --global${reset} Install globally (to config directory)
140
+ ${cyan}-l, --local${reset} Install locally (to current directory)
141
+ ${cyan}--claude${reset} Install for Claude Code only
142
+ ${cyan}--opencode${reset} Install for OpenCode only
143
+ ${cyan}--both${reset} Install for both Claude Code and OpenCode
144
+ ${cyan}-u, --uninstall${reset} Uninstall GRD (remove all GRD files)
145
+ ${cyan}-c, --config-dir <path>${reset} Specify custom config directory
146
+ ${cyan}-h, --help${reset} Show this help message
147
+ ${cyan}--force-statusline${reset} Replace existing statusline config
148
+
149
+ ${yellow}Examples:${reset}
150
+ ${dim}# Interactive install (prompts for runtime and location)${reset}
151
+ npx get-research-done
152
+
153
+ ${dim}# Install for Claude Code globally${reset}
154
+ npx get-research-done --claude --global
155
+
156
+ ${dim}# Install for OpenCode globally${reset}
157
+ npx get-research-done --opencode --global
158
+
159
+ ${dim}# Install for both runtimes globally${reset}
160
+ npx get-research-done --both --global
161
+
162
+ ${dim}# Install to custom config directory${reset}
163
+ npx get-research-done --claude --global --config-dir ~/.claude-bc
164
+
165
+ ${dim}# Install to current project only${reset}
166
+ npx get-research-done --claude --local
167
+
168
+ ${dim}# Uninstall GRD from Claude Code globally${reset}
169
+ npx get-research-done --claude --global --uninstall
170
+
171
+ ${dim}# Uninstall GRD from current project${reset}
172
+ npx get-research-done --claude --local --uninstall
173
+
174
+ ${yellow}Notes:${reset}
175
+ The --config-dir option is useful when you have multiple Claude Code
176
+ configurations (e.g., for different subscriptions). It takes priority
177
+ over the CLAUDE_CONFIG_DIR environment variable.
178
+ `);
179
+ process.exit(0);
180
+ }
181
+
182
+ /**
183
+ * Expand ~ to home directory (shell doesn't expand in env vars passed to node)
184
+ */
185
+ function expandTilde(filePath) {
186
+ if (filePath && filePath.startsWith('~/')) {
187
+ return path.join(os.homedir(), filePath.slice(2));
188
+ }
189
+ return filePath;
190
+ }
191
+
192
+ /**
193
+ * Build a hook command path using forward slashes for cross-platform compatibility.
194
+ * On Windows, $HOME is not expanded by cmd.exe/PowerShell, so we use the actual path.
195
+ */
196
+ function buildHookCommand(claudeDir, hookName) {
197
+ // Use forward slashes for Node.js compatibility on all platforms
198
+ const hooksPath = claudeDir.replace(/\\/g, '/') + '/hooks/' + hookName;
199
+ return `node "${hooksPath}"`;
200
+ }
201
+
202
+ /**
203
+ * Read and parse settings.json, returning empty object if it doesn't exist
204
+ */
205
+ function readSettings(settingsPath) {
206
+ if (fs.existsSync(settingsPath)) {
207
+ try {
208
+ return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
209
+ } catch (e) {
210
+ return {};
211
+ }
212
+ }
213
+ return {};
214
+ }
215
+
216
+ /**
217
+ * Write settings.json with proper formatting
218
+ */
219
+ function writeSettings(settingsPath, settings) {
220
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
221
+ }
222
+
223
+ /**
224
+ * Convert Claude Code frontmatter to opencode format
225
+ * - Converts 'allowed-tools:' array to 'permission:' object
226
+ * @param {string} content - Markdown file content with YAML frontmatter
227
+ * @returns {string} - Content with converted frontmatter
228
+ */
229
+ // Color name to hex mapping for opencode compatibility
230
+ const colorNameToHex = {
231
+ cyan: '#00FFFF',
232
+ red: '#FF0000',
233
+ green: '#00FF00',
234
+ blue: '#0000FF',
235
+ yellow: '#FFFF00',
236
+ magenta: '#FF00FF',
237
+ orange: '#FFA500',
238
+ purple: '#800080',
239
+ pink: '#FFC0CB',
240
+ white: '#FFFFFF',
241
+ black: '#000000',
242
+ gray: '#808080',
243
+ grey: '#808080',
244
+ };
245
+
246
+ // Tool name mapping from Claude Code to OpenCode
247
+ // OpenCode uses lowercase tool names; special mappings for renamed tools
248
+ const claudeToOpencodeTools = {
249
+ AskUserQuestion: 'question',
250
+ SlashCommand: 'skill',
251
+ TodoWrite: 'todowrite',
252
+ WebFetch: 'webfetch',
253
+ WebSearch: 'websearch', // Plugin/MCP - keep for compatibility
254
+ };
255
+
256
+ /**
257
+ * Convert a Claude Code tool name to OpenCode format
258
+ * - Applies special mappings (AskUserQuestion -> question, etc.)
259
+ * - Converts to lowercase (except MCP tools which keep their format)
260
+ */
261
+ function convertToolName(claudeTool) {
262
+ // Check for special mapping first
263
+ if (claudeToOpencodeTools[claudeTool]) {
264
+ return claudeToOpencodeTools[claudeTool];
265
+ }
266
+ // MCP tools (mcp__*) keep their format
267
+ if (claudeTool.startsWith('mcp__')) {
268
+ return claudeTool;
269
+ }
270
+ // Default: convert to lowercase
271
+ return claudeTool.toLowerCase();
272
+ }
273
+
274
+ function convertClaudeToOpencodeFrontmatter(content) {
275
+ // Replace tool name references in content (applies to all files)
276
+ let convertedContent = content;
277
+ convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question');
278
+ convertedContent = convertedContent.replace(/\bSlashCommand\b/g, 'skill');
279
+ convertedContent = convertedContent.replace(/\bTodoWrite\b/g, 'todowrite');
280
+ // Replace /grd:command with /grd-command for opencode (flat command structure)
281
+ convertedContent = convertedContent.replace(/\/grd:/g, '/grd-');
282
+ // Replace ~/.claude with ~/.config/opencode (OpenCode's correct config location)
283
+ convertedContent = convertedContent.replace(/~\/\.claude\b/g, '~/.config/opencode');
284
+
285
+ // Check if content has frontmatter
286
+ if (!convertedContent.startsWith('---')) {
287
+ return convertedContent;
288
+ }
289
+
290
+ // Find the end of frontmatter
291
+ const endIndex = convertedContent.indexOf('---', 3);
292
+ if (endIndex === -1) {
293
+ return convertedContent;
294
+ }
295
+
296
+ const frontmatter = convertedContent.substring(3, endIndex).trim();
297
+ const body = convertedContent.substring(endIndex + 3);
298
+
299
+ // Parse frontmatter line by line (simple YAML parsing)
300
+ const lines = frontmatter.split('\n');
301
+ const newLines = [];
302
+ let inAllowedTools = false;
303
+ const allowedTools = [];
304
+
305
+ for (const line of lines) {
306
+ const trimmed = line.trim();
307
+
308
+ // Detect start of allowed-tools array
309
+ if (trimmed.startsWith('allowed-tools:')) {
310
+ inAllowedTools = true;
311
+ continue;
312
+ }
313
+
314
+ // Detect inline tools: field (comma-separated string)
315
+ if (trimmed.startsWith('tools:')) {
316
+ const toolsValue = trimmed.substring(6).trim();
317
+ if (toolsValue) {
318
+ // Parse comma-separated tools
319
+ const tools = toolsValue.split(',').map(t => t.trim()).filter(t => t);
320
+ allowedTools.push(...tools);
321
+ }
322
+ continue;
323
+ }
324
+
325
+ // Remove name: field - opencode uses filename for command name
326
+ if (trimmed.startsWith('name:')) {
327
+ continue;
328
+ }
329
+
330
+ // Convert color names to hex for opencode
331
+ if (trimmed.startsWith('color:')) {
332
+ const colorValue = trimmed.substring(6).trim().toLowerCase();
333
+ const hexColor = colorNameToHex[colorValue];
334
+ if (hexColor) {
335
+ newLines.push(`color: "${hexColor}"`);
336
+ } else if (colorValue.startsWith('#')) {
337
+ // Already hex, keep as is
338
+ newLines.push(line);
339
+ }
340
+ // Skip unknown color names
341
+ continue;
342
+ }
343
+
344
+ // Collect allowed-tools items
345
+ if (inAllowedTools) {
346
+ if (trimmed.startsWith('- ')) {
347
+ allowedTools.push(trimmed.substring(2).trim());
348
+ continue;
349
+ } else if (trimmed && !trimmed.startsWith('-')) {
350
+ // End of array, new field started
351
+ inAllowedTools = false;
352
+ }
353
+ }
354
+
355
+ // Keep other fields (including name: which opencode ignores)
356
+ if (!inAllowedTools) {
357
+ newLines.push(line);
358
+ }
359
+ }
360
+
361
+ // Add tools object if we had allowed-tools or tools
362
+ if (allowedTools.length > 0) {
363
+ newLines.push('tools:');
364
+ for (const tool of allowedTools) {
365
+ newLines.push(` ${convertToolName(tool)}: true`);
366
+ }
367
+ }
368
+
369
+ // Rebuild frontmatter (body already has tool names converted)
370
+ const newFrontmatter = newLines.join('\n').trim();
371
+ return `---\n${newFrontmatter}\n---${body}`;
372
+ }
373
+
374
+ /**
375
+ * Copy commands to a flat structure for OpenCode
376
+ * OpenCode expects: command/grd-help.md (invoked as /grd-help)
377
+ * Source structure: commands/grd/help.md
378
+ *
379
+ * @param {string} srcDir - Source directory (e.g., commands/grd/)
380
+ * @param {string} destDir - Destination directory (e.g., command/)
381
+ * @param {string} prefix - Prefix for filenames (e.g., 'grd')
382
+ * @param {string} pathPrefix - Path prefix for file references
383
+ * @param {string} runtime - Target runtime ('claude' or 'opencode')
384
+ */
385
+ function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) {
386
+ if (!fs.existsSync(srcDir)) {
387
+ return;
388
+ }
389
+
390
+ // Remove old grd-*.md files before copying new ones
391
+ if (fs.existsSync(destDir)) {
392
+ for (const file of fs.readdirSync(destDir)) {
393
+ if (file.startsWith(`${prefix}-`) && file.endsWith('.md')) {
394
+ fs.unlinkSync(path.join(destDir, file));
395
+ }
396
+ }
397
+ } else {
398
+ fs.mkdirSync(destDir, { recursive: true });
399
+ }
400
+
401
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
402
+
403
+ for (const entry of entries) {
404
+ const srcPath = path.join(srcDir, entry.name);
405
+
406
+ if (entry.isDirectory()) {
407
+ // Recurse into subdirectories, adding to prefix
408
+ // e.g., commands/grd/debug/start.md -> command/grd-debug-start.md
409
+ copyFlattenedCommands(srcPath, destDir, `${prefix}-${entry.name}`, pathPrefix, runtime);
410
+ } else if (entry.name.endsWith('.md')) {
411
+ // Flatten: help.md -> grd-help.md
412
+ const baseName = entry.name.replace('.md', '');
413
+ const destName = `${prefix}-${baseName}.md`;
414
+ const destPath = path.join(destDir, destName);
415
+
416
+ // Read, transform, and write
417
+ let content = fs.readFileSync(srcPath, 'utf8');
418
+ // Replace path references
419
+ const claudeDirRegex = /~\/\.claude\//g;
420
+ const opencodeDirRegex = /~\/\.opencode\//g;
421
+ content = content.replace(claudeDirRegex, pathPrefix);
422
+ content = content.replace(opencodeDirRegex, pathPrefix);
423
+ // Convert frontmatter for opencode compatibility
424
+ content = convertClaudeToOpencodeFrontmatter(content);
425
+
426
+ fs.writeFileSync(destPath, content);
427
+ }
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Recursively copy directory, replacing paths in .md files
433
+ * Deletes existing destDir first to remove orphaned files from previous versions
434
+ * @param {string} srcDir - Source directory
435
+ * @param {string} destDir - Destination directory
436
+ * @param {string} pathPrefix - Path prefix for file references
437
+ * @param {string} runtime - Target runtime ('claude' or 'opencode')
438
+ */
439
+ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime) {
440
+ const isOpencode = runtime === 'opencode';
441
+ const dirName = getDirName(runtime);
442
+
443
+ // Clean install: remove existing destination to prevent orphaned files
444
+ if (fs.existsSync(destDir)) {
445
+ fs.rmSync(destDir, { recursive: true });
446
+ }
447
+ fs.mkdirSync(destDir, { recursive: true });
448
+
449
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
450
+
451
+ for (const entry of entries) {
452
+ const srcPath = path.join(srcDir, entry.name);
453
+ const destPath = path.join(destDir, entry.name);
454
+
455
+ if (entry.isDirectory()) {
456
+ copyWithPathReplacement(srcPath, destPath, pathPrefix, runtime);
457
+ } else if (entry.name.endsWith('.md')) {
458
+ // Replace ~/.claude/ with the appropriate prefix in Markdown files
459
+ let content = fs.readFileSync(srcPath, 'utf8');
460
+ const claudeDirRegex = new RegExp(`~/${dirName.replace('.', '\\.')}/`, 'g');
461
+ content = content.replace(claudeDirRegex, pathPrefix);
462
+ // Convert frontmatter for opencode compatibility
463
+ if (isOpencode) {
464
+ content = convertClaudeToOpencodeFrontmatter(content);
465
+ }
466
+ fs.writeFileSync(destPath, content);
467
+ } else {
468
+ fs.copyFileSync(srcPath, destPath);
469
+ }
470
+ }
471
+ }
472
+
473
+ /**
474
+ * Clean up orphaned files from previous GRD versions
475
+ */
476
+ function cleanupOrphanedFiles(claudeDir) {
477
+ const orphanedFiles = [
478
+ 'hooks/gsd-notify.sh', // Removed in v1.6.x (legacy GSD)
479
+ 'hooks/statusline.js', // Renamed to grd-statusline.js
480
+ ];
481
+
482
+ for (const relPath of orphanedFiles) {
483
+ const fullPath = path.join(claudeDir, relPath);
484
+ if (fs.existsSync(fullPath)) {
485
+ fs.unlinkSync(fullPath);
486
+ console.log(` ${green}✓${reset} Removed orphaned ${relPath}`);
487
+ }
488
+ }
489
+ }
490
+
491
+ /**
492
+ * Clean up orphaned hook registrations from settings.json
493
+ */
494
+ function cleanupOrphanedHooks(settings) {
495
+ const orphanedHookPatterns = [
496
+ 'gsd-notify.sh', // Removed (legacy GSD)
497
+ 'hooks/statusline.js', // Renamed to grd-statusline.js
498
+ 'gsd-intel-index.js', // Removed (legacy GSD)
499
+ 'gsd-intel-session.js', // Removed (legacy GSD)
500
+ 'gsd-intel-prune.js', // Removed (legacy GSD)
501
+ 'gsd-statusline.js', // Legacy GSD hook
502
+ 'gsd-check-update.js', // Legacy GSD hook
503
+ ];
504
+
505
+ let cleaned = false;
506
+
507
+ // Check all hook event types (Stop, SessionStart, etc.)
508
+ if (settings.hooks) {
509
+ for (const eventType of Object.keys(settings.hooks)) {
510
+ const hookEntries = settings.hooks[eventType];
511
+ if (Array.isArray(hookEntries)) {
512
+ // Filter out entries that contain orphaned hooks
513
+ const filtered = hookEntries.filter(entry => {
514
+ if (entry.hooks && Array.isArray(entry.hooks)) {
515
+ // Check if any hook in this entry matches orphaned patterns
516
+ const hasOrphaned = entry.hooks.some(h =>
517
+ h.command && orphanedHookPatterns.some(pattern => h.command.includes(pattern))
518
+ );
519
+ if (hasOrphaned) {
520
+ cleaned = true;
521
+ return false; // Remove this entry
522
+ }
523
+ }
524
+ return true; // Keep this entry
525
+ });
526
+ settings.hooks[eventType] = filtered;
527
+ }
528
+ }
529
+ }
530
+
531
+ if (cleaned) {
532
+ console.log(` ${green}✓${reset} Removed orphaned hook registrations`);
533
+ }
534
+
535
+ return settings;
536
+ }
537
+
538
+ /**
539
+ * Uninstall GRD from the specified directory for a specific runtime
540
+ * Removes only GRD-specific files/directories, preserves user content
541
+ * @param {boolean} isGlobal - Whether to uninstall from global or local
542
+ * @param {string} runtime - Target runtime ('claude' or 'opencode')
543
+ */
544
+ function uninstall(isGlobal, runtime = 'claude') {
545
+ const isOpencode = runtime === 'opencode';
546
+ const dirName = getDirName(runtime);
547
+
548
+ // Get the target directory based on runtime and install type
549
+ const targetDir = isGlobal
550
+ ? getGlobalDir(runtime, explicitConfigDir)
551
+ : path.join(process.cwd(), dirName);
552
+
553
+ const locationLabel = isGlobal
554
+ ? targetDir.replace(os.homedir(), '~')
555
+ : targetDir.replace(process.cwd(), '.');
556
+
557
+ const runtimeLabel = isOpencode ? 'OpenCode' : 'Claude Code';
558
+ console.log(` Uninstalling GRD from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`);
559
+
560
+ // Check if target directory exists
561
+ if (!fs.existsSync(targetDir)) {
562
+ console.log(` ${yellow}⚠${reset} Directory does not exist: ${locationLabel}`);
563
+ console.log(` Nothing to uninstall.\n`);
564
+ return;
565
+ }
566
+
567
+ let removedCount = 0;
568
+
569
+ // 1. Remove GRD commands directory
570
+ if (isOpencode) {
571
+ // OpenCode: remove command/grd-*.md files
572
+ const commandDir = path.join(targetDir, 'command');
573
+ if (fs.existsSync(commandDir)) {
574
+ const files = fs.readdirSync(commandDir);
575
+ for (const file of files) {
576
+ if (file.startsWith('grd-') && file.endsWith('.md')) {
577
+ fs.unlinkSync(path.join(commandDir, file));
578
+ removedCount++;
579
+ }
580
+ }
581
+ console.log(` ${green}✓${reset} Removed GRD commands from command/`);
582
+ }
583
+ } else {
584
+ // Claude Code: remove commands/grd/ directory
585
+ const grdCommandsDir = path.join(targetDir, 'commands', 'grd');
586
+ if (fs.existsSync(grdCommandsDir)) {
587
+ fs.rmSync(grdCommandsDir, { recursive: true });
588
+ removedCount++;
589
+ console.log(` ${green}✓${reset} Removed commands/grd/`);
590
+ }
591
+ }
592
+
593
+ // 2. Remove get-research-done directory
594
+ const grdDir = path.join(targetDir, 'get-research-done');
595
+ if (fs.existsSync(grdDir)) {
596
+ fs.rmSync(grdDir, { recursive: true });
597
+ removedCount++;
598
+ console.log(` ${green}✓${reset} Removed get-research-done/`);
599
+ }
600
+
601
+ // 3. Remove GRD agents (grd-*.md files only)
602
+ const agentsDir = path.join(targetDir, 'agents');
603
+ if (fs.existsSync(agentsDir)) {
604
+ const files = fs.readdirSync(agentsDir);
605
+ let agentCount = 0;
606
+ for (const file of files) {
607
+ if (file.startsWith('grd-') && file.endsWith('.md')) {
608
+ fs.unlinkSync(path.join(agentsDir, file));
609
+ agentCount++;
610
+ }
611
+ }
612
+ if (agentCount > 0) {
613
+ removedCount++;
614
+ console.log(` ${green}✓${reset} Removed ${agentCount} GRD agents`);
615
+ }
616
+ }
617
+
618
+ // 4. Remove GRD hooks
619
+ const hooksDir = path.join(targetDir, 'hooks');
620
+ if (fs.existsSync(hooksDir)) {
621
+ const grdHooks = ['grd-statusline.js', 'grd-check-update.js', 'grd-check-update.sh'];
622
+ let hookCount = 0;
623
+ for (const hook of grdHooks) {
624
+ const hookPath = path.join(hooksDir, hook);
625
+ if (fs.existsSync(hookPath)) {
626
+ fs.unlinkSync(hookPath);
627
+ hookCount++;
628
+ }
629
+ }
630
+ if (hookCount > 0) {
631
+ removedCount++;
632
+ console.log(` ${green}✓${reset} Removed ${hookCount} GRD hooks`);
633
+ }
634
+ }
635
+
636
+ // 5. Clean up settings.json (remove GRD hooks and statusline)
637
+ const settingsPath = path.join(targetDir, 'settings.json');
638
+ if (fs.existsSync(settingsPath)) {
639
+ let settings = readSettings(settingsPath);
640
+ let settingsModified = false;
641
+
642
+ // Remove GRD statusline if it references our hook
643
+ if (settings.statusLine && settings.statusLine.command &&
644
+ settings.statusLine.command.includes('grd-statusline')) {
645
+ delete settings.statusLine;
646
+ settingsModified = true;
647
+ console.log(` ${green}✓${reset} Removed GRD statusline from settings`);
648
+ }
649
+
650
+ // Remove GRD hooks from SessionStart
651
+ if (settings.hooks && settings.hooks.SessionStart) {
652
+ const before = settings.hooks.SessionStart.length;
653
+ settings.hooks.SessionStart = settings.hooks.SessionStart.filter(entry => {
654
+ if (entry.hooks && Array.isArray(entry.hooks)) {
655
+ // Filter out GRD hooks
656
+ const hasGsdHook = entry.hooks.some(h =>
657
+ h.command && (h.command.includes('grd-check-update') || h.command.includes('grd-statusline'))
658
+ );
659
+ return !hasGsdHook;
660
+ }
661
+ return true;
662
+ });
663
+ if (settings.hooks.SessionStart.length < before) {
664
+ settingsModified = true;
665
+ console.log(` ${green}✓${reset} Removed GRD hooks from settings`);
666
+ }
667
+ // Clean up empty array
668
+ if (settings.hooks.SessionStart.length === 0) {
669
+ delete settings.hooks.SessionStart;
670
+ }
671
+ // Clean up empty hooks object
672
+ if (Object.keys(settings.hooks).length === 0) {
673
+ delete settings.hooks;
674
+ }
675
+ }
676
+
677
+ if (settingsModified) {
678
+ writeSettings(settingsPath, settings);
679
+ removedCount++;
680
+ }
681
+ }
682
+
683
+ // 6. For OpenCode, clean up permissions from opencode.json
684
+ if (isOpencode) {
685
+ const opencodeConfigDir = getOpencodeGlobalDir();
686
+ const configPath = path.join(opencodeConfigDir, 'opencode.json');
687
+ if (fs.existsSync(configPath)) {
688
+ try {
689
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
690
+ let modified = false;
691
+
692
+ // Remove GRD permission entries
693
+ if (config.permission) {
694
+ for (const permType of ['read', 'external_directory']) {
695
+ if (config.permission[permType]) {
696
+ const keys = Object.keys(config.permission[permType]);
697
+ for (const key of keys) {
698
+ if (key.includes('get-research-done')) {
699
+ delete config.permission[permType][key];
700
+ modified = true;
701
+ }
702
+ }
703
+ // Clean up empty objects
704
+ if (Object.keys(config.permission[permType]).length === 0) {
705
+ delete config.permission[permType];
706
+ }
707
+ }
708
+ }
709
+ if (Object.keys(config.permission).length === 0) {
710
+ delete config.permission;
711
+ }
712
+ }
713
+
714
+ if (modified) {
715
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
716
+ removedCount++;
717
+ console.log(` ${green}✓${reset} Removed GRD permissions from opencode.json`);
718
+ }
719
+ } catch (e) {
720
+ // Ignore JSON parse errors
721
+ }
722
+ }
723
+ }
724
+
725
+ if (removedCount === 0) {
726
+ console.log(` ${yellow}⚠${reset} No GRD files found to remove.`);
727
+ }
728
+
729
+ console.log(`
730
+ ${green}Done!${reset} GRD has been uninstalled from ${runtimeLabel}.
731
+ Your other files and settings have been preserved.
732
+ `);
733
+ }
734
+
735
+ /**
736
+ * Configure OpenCode permissions to allow reading GRD reference docs
737
+ * This prevents permission prompts when GRD accesses the get-research-done directory
738
+ */
739
+ function configureOpencodePermissions() {
740
+ // OpenCode config file is at ~/.config/opencode/opencode.json
741
+ const opencodeConfigDir = getOpencodeGlobalDir();
742
+ const configPath = path.join(opencodeConfigDir, 'opencode.json');
743
+
744
+ // Ensure config directory exists
745
+ fs.mkdirSync(opencodeConfigDir, { recursive: true });
746
+
747
+ // Read existing config or create empty object
748
+ let config = {};
749
+ if (fs.existsSync(configPath)) {
750
+ try {
751
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
752
+ } catch (e) {
753
+ // Invalid JSON - start fresh but warn user
754
+ console.log(` ${yellow}⚠${reset} opencode.json had invalid JSON, recreating`);
755
+ }
756
+ }
757
+
758
+ // Ensure permission structure exists
759
+ if (!config.permission) {
760
+ config.permission = {};
761
+ }
762
+
763
+ // Build the GRD path using the actual config directory
764
+ // Use ~ shorthand if it's in the default location, otherwise use full path
765
+ const defaultConfigDir = path.join(os.homedir(), '.config', 'opencode');
766
+ const grdPath = opencodeConfigDir === defaultConfigDir
767
+ ? '~/.config/opencode/get-research-done/*'
768
+ : `${opencodeConfigDir}/get-research-done/*`;
769
+
770
+ let modified = false;
771
+
772
+ // Configure read permission
773
+ if (!config.permission.read || typeof config.permission.read !== 'object') {
774
+ config.permission.read = {};
775
+ }
776
+ if (config.permission.read[grdPath] !== 'allow') {
777
+ config.permission.read[grdPath] = 'allow';
778
+ modified = true;
779
+ }
780
+
781
+ // Configure external_directory permission (the safety guard for paths outside project)
782
+ if (!config.permission.external_directory || typeof config.permission.external_directory !== 'object') {
783
+ config.permission.external_directory = {};
784
+ }
785
+ if (config.permission.external_directory[grdPath] !== 'allow') {
786
+ config.permission.external_directory[grdPath] = 'allow';
787
+ modified = true;
788
+ }
789
+
790
+ if (!modified) {
791
+ return; // Already configured
792
+ }
793
+
794
+ // Write config back
795
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
796
+ console.log(` ${green}✓${reset} Configured read permission for GRD docs`);
797
+ }
798
+
799
+ /**
800
+ * Verify a directory exists and contains files
801
+ */
802
+ function verifyInstalled(dirPath, description) {
803
+ if (!fs.existsSync(dirPath)) {
804
+ console.error(` ${yellow}✗${reset} Failed to install ${description}: directory not created`);
805
+ return false;
806
+ }
807
+ try {
808
+ const entries = fs.readdirSync(dirPath);
809
+ if (entries.length === 0) {
810
+ console.error(` ${yellow}✗${reset} Failed to install ${description}: directory is empty`);
811
+ return false;
812
+ }
813
+ } catch (e) {
814
+ console.error(` ${yellow}✗${reset} Failed to install ${description}: ${e.message}`);
815
+ return false;
816
+ }
817
+ return true;
818
+ }
819
+
820
+ /**
821
+ * Verify a file exists
822
+ */
823
+ function verifyFileInstalled(filePath, description) {
824
+ if (!fs.existsSync(filePath)) {
825
+ console.error(` ${yellow}✗${reset} Failed to install ${description}: file not created`);
826
+ return false;
827
+ }
828
+ return true;
829
+ }
830
+
831
+ /**
832
+ * Install to the specified directory for a specific runtime
833
+ * @param {boolean} isGlobal - Whether to install globally or locally
834
+ * @param {string} runtime - Target runtime ('claude' or 'opencode')
835
+ */
836
+ function install(isGlobal, runtime = 'claude') {
837
+ const isOpencode = runtime === 'opencode';
838
+ const dirName = getDirName(runtime); // .opencode or .claude (for local installs)
839
+ const src = path.join(__dirname, '..');
840
+
841
+ // Get the target directory based on runtime and install type
842
+ const targetDir = isGlobal
843
+ ? getGlobalDir(runtime, explicitConfigDir)
844
+ : path.join(process.cwd(), dirName);
845
+
846
+ const locationLabel = isGlobal
847
+ ? targetDir.replace(os.homedir(), '~')
848
+ : targetDir.replace(process.cwd(), '.');
849
+
850
+ // Path prefix for file references in markdown content
851
+ // For global installs: use full path (necessary when config dir is customized)
852
+ // For local installs: use relative ./.opencode/ or ./.claude/
853
+ const pathPrefix = isGlobal
854
+ ? `${targetDir}/`
855
+ : `./${dirName}/`;
856
+
857
+ const runtimeLabel = isOpencode ? 'OpenCode' : 'Claude Code';
858
+ console.log(` Installing for ${cyan}${runtimeLabel}${reset} to ${cyan}${locationLabel}${reset}\n`);
859
+
860
+ // Track installation failures
861
+ const failures = [];
862
+
863
+ // Clean up orphaned files from previous versions
864
+ cleanupOrphanedFiles(targetDir);
865
+
866
+ // OpenCode uses 'command/' (singular) with flat structure: command/grd-help.md
867
+ // Claude Code uses 'commands/' (plural) with nested structure: commands/grd/help.md
868
+ if (isOpencode) {
869
+ // OpenCode: flat structure in command/ directory
870
+ const commandDir = path.join(targetDir, 'command');
871
+ fs.mkdirSync(commandDir, { recursive: true });
872
+
873
+ // Copy commands/grd/*.md as command/grd-*.md (flatten structure)
874
+ const grdSrc = path.join(src, 'commands', 'grd');
875
+ copyFlattenedCommands(grdSrc, commandDir, 'grd', pathPrefix, runtime);
876
+ if (verifyInstalled(commandDir, 'command/grd-*')) {
877
+ const count = fs.readdirSync(commandDir).filter(f => f.startsWith('grd-')).length;
878
+ console.log(` ${green}✓${reset} Installed ${count} commands to command/`);
879
+ } else {
880
+ failures.push('command/grd-*');
881
+ }
882
+ } else {
883
+ // Claude Code: nested structure in commands/ directory
884
+ const commandsDir = path.join(targetDir, 'commands');
885
+ fs.mkdirSync(commandsDir, { recursive: true });
886
+
887
+ const grdSrc = path.join(src, 'commands', 'grd');
888
+ const grdDest = path.join(commandsDir, 'grd');
889
+ copyWithPathReplacement(grdSrc, grdDest, pathPrefix, runtime);
890
+ if (verifyInstalled(grdDest, 'commands/grd')) {
891
+ console.log(` ${green}✓${reset} Installed commands/grd`);
892
+ } else {
893
+ failures.push('commands/grd');
894
+ }
895
+ }
896
+
897
+ // Copy get-research-done skill with path replacement
898
+ const skillSrc = path.join(src, 'get-research-done');
899
+ const skillDest = path.join(targetDir, 'get-research-done');
900
+ copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime);
901
+ if (verifyInstalled(skillDest, 'get-research-done')) {
902
+ console.log(` ${green}✓${reset} Installed get-research-done`);
903
+ } else {
904
+ failures.push('get-research-done');
905
+ }
906
+
907
+ // Copy agents to agents directory (subagents must be at root level)
908
+ // Only delete grd-*.md files to preserve user's custom agents
909
+ const agentsSrc = path.join(src, 'agents');
910
+ if (fs.existsSync(agentsSrc)) {
911
+ const agentsDest = path.join(targetDir, 'agents');
912
+ fs.mkdirSync(agentsDest, { recursive: true });
913
+
914
+ // Remove old GRD agents (grd-*.md) before copying new ones
915
+ if (fs.existsSync(agentsDest)) {
916
+ for (const file of fs.readdirSync(agentsDest)) {
917
+ if (file.startsWith('grd-') && file.endsWith('.md')) {
918
+ fs.unlinkSync(path.join(agentsDest, file));
919
+ }
920
+ }
921
+ }
922
+
923
+ // Copy new agents (don't use copyWithPathReplacement which would wipe the folder)
924
+ const agentEntries = fs.readdirSync(agentsSrc, { withFileTypes: true });
925
+ for (const entry of agentEntries) {
926
+ if (entry.isFile() && entry.name.endsWith('.md')) {
927
+ let content = fs.readFileSync(path.join(agentsSrc, entry.name), 'utf8');
928
+ const dirRegex = new RegExp(`~/${dirName.replace('.', '\\.')}/`, 'g');
929
+ content = content.replace(dirRegex, pathPrefix);
930
+ // Convert frontmatter for opencode compatibility
931
+ if (isOpencode) {
932
+ content = convertClaudeToOpencodeFrontmatter(content);
933
+ }
934
+ fs.writeFileSync(path.join(agentsDest, entry.name), content);
935
+ }
936
+ }
937
+ if (verifyInstalled(agentsDest, 'agents')) {
938
+ console.log(` ${green}✓${reset} Installed agents`);
939
+ } else {
940
+ failures.push('agents');
941
+ }
942
+ }
943
+
944
+ // Copy CHANGELOG.md
945
+ const changelogSrc = path.join(src, 'CHANGELOG.md');
946
+ const changelogDest = path.join(targetDir, 'get-research-done', 'CHANGELOG.md');
947
+ if (fs.existsSync(changelogSrc)) {
948
+ fs.copyFileSync(changelogSrc, changelogDest);
949
+ if (verifyFileInstalled(changelogDest, 'CHANGELOG.md')) {
950
+ console.log(` ${green}✓${reset} Installed CHANGELOG.md`);
951
+ } else {
952
+ failures.push('CHANGELOG.md');
953
+ }
954
+ }
955
+
956
+ // Write VERSION file for whats-new command
957
+ const versionDest = path.join(targetDir, 'get-research-done', 'VERSION');
958
+ fs.writeFileSync(versionDest, pkg.version);
959
+ if (verifyFileInstalled(versionDest, 'VERSION')) {
960
+ console.log(` ${green}✓${reset} Wrote VERSION (${pkg.version})`);
961
+ } else {
962
+ failures.push('VERSION');
963
+ }
964
+
965
+ // Copy hooks from dist/ (bundled with dependencies)
966
+ const hooksSrc = path.join(src, 'hooks', 'dist');
967
+ if (fs.existsSync(hooksSrc)) {
968
+ const hooksDest = path.join(targetDir, 'hooks');
969
+ fs.mkdirSync(hooksDest, { recursive: true });
970
+ const hookEntries = fs.readdirSync(hooksSrc);
971
+ for (const entry of hookEntries) {
972
+ const srcFile = path.join(hooksSrc, entry);
973
+ // Only copy files, not directories
974
+ if (fs.statSync(srcFile).isFile()) {
975
+ const destFile = path.join(hooksDest, entry);
976
+ fs.copyFileSync(srcFile, destFile);
977
+ }
978
+ }
979
+ if (verifyInstalled(hooksDest, 'hooks')) {
980
+ console.log(` ${green}✓${reset} Installed hooks (bundled)`);
981
+ } else {
982
+ failures.push('hooks');
983
+ }
984
+ }
985
+
986
+ // If critical components failed, exit with error
987
+ if (failures.length > 0) {
988
+ console.error(`\n ${yellow}Installation incomplete!${reset} Failed: ${failures.join(', ')}`);
989
+ console.error(` Try running directly: node ~/.npm/_npx/*/node_modules/get-research-done/bin/install.js --global\n`);
990
+ process.exit(1);
991
+ }
992
+
993
+ // Configure statusline and hooks in settings.json
994
+ const settingsPath = path.join(targetDir, 'settings.json');
995
+ const settings = cleanupOrphanedHooks(readSettings(settingsPath));
996
+ const statuslineCommand = isGlobal
997
+ ? buildHookCommand(targetDir, 'grd-statusline.js')
998
+ : 'node ' + dirName + '/hooks/grd-statusline.js';
999
+ const updateCheckCommand = isGlobal
1000
+ ? buildHookCommand(targetDir, 'grd-check-update.js')
1001
+ : 'node ' + dirName + '/hooks/grd-check-update.js';
1002
+
1003
+ // Configure SessionStart hook for update checking (skip for opencode - different hook system)
1004
+ if (!isOpencode) {
1005
+ if (!settings.hooks) {
1006
+ settings.hooks = {};
1007
+ }
1008
+ if (!settings.hooks.SessionStart) {
1009
+ settings.hooks.SessionStart = [];
1010
+ }
1011
+
1012
+ // Check if GRD update hook already exists
1013
+ const hasGsdUpdateHook = settings.hooks.SessionStart.some(entry =>
1014
+ entry.hooks && entry.hooks.some(h => h.command && h.command.includes('grd-check-update'))
1015
+ );
1016
+
1017
+ if (!hasGsdUpdateHook) {
1018
+ settings.hooks.SessionStart.push({
1019
+ hooks: [
1020
+ {
1021
+ type: 'command',
1022
+ command: updateCheckCommand
1023
+ }
1024
+ ]
1025
+ });
1026
+ console.log(` ${green}✓${reset} Configured update check hook`);
1027
+ }
1028
+ }
1029
+
1030
+ return { settingsPath, settings, statuslineCommand, runtime };
1031
+ }
1032
+
1033
+ /**
1034
+ * Apply statusline config, then print completion message
1035
+ * @param {string} settingsPath - Path to settings.json
1036
+ * @param {object} settings - Settings object
1037
+ * @param {string} statuslineCommand - Statusline command
1038
+ * @param {boolean} shouldInstallStatusline - Whether to install statusline
1039
+ * @param {string} runtime - Target runtime ('claude' or 'opencode')
1040
+ */
1041
+ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = 'claude') {
1042
+ const isOpencode = runtime === 'opencode';
1043
+
1044
+ if (shouldInstallStatusline && !isOpencode) {
1045
+ settings.statusLine = {
1046
+ type: 'command',
1047
+ command: statuslineCommand
1048
+ };
1049
+ console.log(` ${green}✓${reset} Configured statusline`);
1050
+ }
1051
+
1052
+ // Always write settings (hooks were already configured in install())
1053
+ writeSettings(settingsPath, settings);
1054
+
1055
+ // Configure OpenCode permissions if needed
1056
+ if (isOpencode) {
1057
+ configureOpencodePermissions();
1058
+ }
1059
+
1060
+ const program = isOpencode ? 'OpenCode' : 'Claude Code';
1061
+ const command = isOpencode ? '/grd-help' : '/grd:help';
1062
+ console.log(`
1063
+ ${green}Done!${reset} Launch ${program} and run ${cyan}${command}${reset}.
1064
+
1065
+ ${cyan}Join the community:${reset} https://discord.gg/5JJgD5svVS
1066
+ `);
1067
+ }
1068
+
1069
+ /**
1070
+ * Handle statusline configuration with optional prompt
1071
+ */
1072
+ function handleStatusline(settings, isInteractive, callback) {
1073
+ const hasExisting = settings.statusLine != null;
1074
+
1075
+ // No existing statusline - just install it
1076
+ if (!hasExisting) {
1077
+ callback(true);
1078
+ return;
1079
+ }
1080
+
1081
+ // Has existing and --force-statusline flag
1082
+ if (forceStatusline) {
1083
+ callback(true);
1084
+ return;
1085
+ }
1086
+
1087
+ // Has existing, non-interactive mode - skip
1088
+ if (!isInteractive) {
1089
+ console.log(` ${yellow}⚠${reset} Skipping statusline (already configured)`);
1090
+ console.log(` Use ${cyan}--force-statusline${reset} to replace\n`);
1091
+ callback(false);
1092
+ return;
1093
+ }
1094
+
1095
+ // Has existing, interactive mode - prompt user
1096
+ const existingCmd = settings.statusLine.command || settings.statusLine.url || '(custom)';
1097
+
1098
+ const rl = readline.createInterface({
1099
+ input: process.stdin,
1100
+ output: process.stdout
1101
+ });
1102
+
1103
+ console.log(`
1104
+ ${yellow}⚠${reset} Existing statusline detected
1105
+
1106
+ Your current statusline:
1107
+ ${dim}command: ${existingCmd}${reset}
1108
+
1109
+ GRD includes a statusline showing:
1110
+ • Model name
1111
+ • Current task (from todo list)
1112
+ • Context window usage (color-coded)
1113
+
1114
+ ${cyan}1${reset}) Keep existing
1115
+ ${cyan}2${reset}) Replace with GRD statusline
1116
+ `);
1117
+
1118
+ rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
1119
+ rl.close();
1120
+ const choice = answer.trim() || '1';
1121
+ callback(choice === '2');
1122
+ });
1123
+ }
1124
+
1125
+ /**
1126
+ * Prompt for runtime selection (Claude Code / OpenCode / Both)
1127
+ * @param {function} callback - Called with array of selected runtimes
1128
+ */
1129
+ function promptRuntime(callback) {
1130
+ const rl = readline.createInterface({
1131
+ input: process.stdin,
1132
+ output: process.stdout
1133
+ });
1134
+
1135
+ let answered = false;
1136
+
1137
+ rl.on('close', () => {
1138
+ if (!answered) {
1139
+ answered = true;
1140
+ console.log(`\n ${yellow}Installation cancelled${reset}\n`);
1141
+ process.exit(0);
1142
+ }
1143
+ });
1144
+
1145
+ console.log(` ${yellow}Which runtime(s) would you like to install for?${reset}
1146
+
1147
+ ${cyan}1${reset}) Claude Code ${dim}(~/.claude)${reset}
1148
+ ${cyan}2${reset}) OpenCode ${dim}(~/.config/opencode)${reset} - open source, free models
1149
+ ${cyan}3${reset}) Both
1150
+ `);
1151
+
1152
+ rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
1153
+ answered = true;
1154
+ rl.close();
1155
+ const choice = answer.trim() || '1';
1156
+ if (choice === '3') {
1157
+ callback(['claude', 'opencode']);
1158
+ } else if (choice === '2') {
1159
+ callback(['opencode']);
1160
+ } else {
1161
+ callback(['claude']);
1162
+ }
1163
+ });
1164
+ }
1165
+
1166
+ /**
1167
+ * Prompt for install location
1168
+ * @param {string[]} runtimes - Array of runtimes to install for
1169
+ */
1170
+ function promptLocation(runtimes) {
1171
+ // Check if stdin is a TTY - if not, fall back to global install
1172
+ // This handles npx execution in environments like WSL2 where stdin may not be properly connected
1173
+ if (!process.stdin.isTTY) {
1174
+ console.log(` ${yellow}Non-interactive terminal detected, defaulting to global install${reset}\n`);
1175
+ installAllRuntimes(runtimes, true, false);
1176
+ return;
1177
+ }
1178
+
1179
+ const rl = readline.createInterface({
1180
+ input: process.stdin,
1181
+ output: process.stdout
1182
+ });
1183
+
1184
+ // Track whether we've processed the answer to prevent double-execution
1185
+ let answered = false;
1186
+
1187
+ // Handle readline close event (Ctrl+C, Escape, etc.) - cancel installation
1188
+ rl.on('close', () => {
1189
+ if (!answered) {
1190
+ answered = true;
1191
+ console.log(`\n ${yellow}Installation cancelled${reset}\n`);
1192
+ process.exit(0);
1193
+ }
1194
+ });
1195
+
1196
+ // Show paths for selected runtimes
1197
+ const pathExamples = runtimes.map(r => {
1198
+ // Use the proper global directory function for each runtime
1199
+ const globalPath = getGlobalDir(r, explicitConfigDir);
1200
+ return globalPath.replace(os.homedir(), '~');
1201
+ }).join(', ');
1202
+
1203
+ const localExamples = runtimes.map(r => `./${getDirName(r)}`).join(', ');
1204
+
1205
+ console.log(` ${yellow}Where would you like to install?${reset}
1206
+
1207
+ ${cyan}1${reset}) Global ${dim}(${pathExamples})${reset} - available in all projects
1208
+ ${cyan}2${reset}) Local ${dim}(${localExamples})${reset} - this project only
1209
+ `);
1210
+
1211
+ rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
1212
+ answered = true;
1213
+ rl.close();
1214
+ const choice = answer.trim() || '1';
1215
+ const isGlobal = choice !== '2';
1216
+ installAllRuntimes(runtimes, isGlobal, true);
1217
+ });
1218
+ }
1219
+
1220
+ /**
1221
+ * Install GRD for all selected runtimes
1222
+ * @param {string[]} runtimes - Array of runtimes to install for
1223
+ * @param {boolean} isGlobal - Whether to install globally
1224
+ * @param {boolean} isInteractive - Whether running interactively
1225
+ */
1226
+ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
1227
+ const results = [];
1228
+
1229
+ for (const runtime of runtimes) {
1230
+ const result = install(isGlobal, runtime);
1231
+ results.push(result);
1232
+ }
1233
+
1234
+ // Handle statusline for Claude Code only (OpenCode uses themes)
1235
+ const claudeResult = results.find(r => r.runtime === 'claude');
1236
+
1237
+ if (claudeResult) {
1238
+ handleStatusline(claudeResult.settings, isInteractive, (shouldInstallStatusline) => {
1239
+ finishInstall(claudeResult.settingsPath, claudeResult.settings, claudeResult.statuslineCommand, shouldInstallStatusline, 'claude');
1240
+
1241
+ // Finish OpenCode install if present
1242
+ const opencodeResult = results.find(r => r.runtime === 'opencode');
1243
+ if (opencodeResult) {
1244
+ finishInstall(opencodeResult.settingsPath, opencodeResult.settings, opencodeResult.statuslineCommand, false, 'opencode');
1245
+ }
1246
+ });
1247
+ } else {
1248
+ // Only OpenCode
1249
+ const opencodeResult = results[0];
1250
+ finishInstall(opencodeResult.settingsPath, opencodeResult.settings, opencodeResult.statuslineCommand, false, 'opencode');
1251
+ }
1252
+ }
1253
+
1254
+ // Main
1255
+ if (hasGlobal && hasLocal) {
1256
+ console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
1257
+ process.exit(1);
1258
+ } else if (explicitConfigDir && hasLocal) {
1259
+ console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
1260
+ process.exit(1);
1261
+ } else if (hasUninstall) {
1262
+ // Uninstall mode
1263
+ if (!hasGlobal && !hasLocal) {
1264
+ console.error(` ${yellow}--uninstall requires --global or --local${reset}`);
1265
+ console.error(` Example: npx get-research-done --claude --global --uninstall`);
1266
+ process.exit(1);
1267
+ }
1268
+ const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : ['claude'];
1269
+ for (const runtime of runtimes) {
1270
+ uninstall(hasGlobal, runtime);
1271
+ }
1272
+ } else if (selectedRuntimes.length > 0) {
1273
+ // Non-interactive: runtime specified via flags
1274
+ if (!hasGlobal && !hasLocal) {
1275
+ // Need location but runtime is specified - prompt for location only
1276
+ promptLocation(selectedRuntimes);
1277
+ } else {
1278
+ // Both runtime and location specified via flags
1279
+ installAllRuntimes(selectedRuntimes, hasGlobal, false);
1280
+ }
1281
+ } else if (hasGlobal || hasLocal) {
1282
+ // Location specified but no runtime - default to Claude Code
1283
+ installAllRuntimes(['claude'], hasGlobal, false);
1284
+ } else {
1285
+ // Fully interactive: prompt for runtime, then location
1286
+ if (!process.stdin.isTTY) {
1287
+ console.log(` ${yellow}Non-interactive terminal detected, defaulting to Claude Code global install${reset}\n`);
1288
+ installAllRuntimes(['claude'], true, false);
1289
+ } else {
1290
+ promptRuntime((runtimes) => {
1291
+ promptLocation(runtimes);
1292
+ });
1293
+ }
1294
+ }