gsd-pi 2.37.1 → 2.38.0-dev.4d4d14a

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 (222) hide show
  1. package/README.md +1 -1
  2. package/dist/app-paths.js +1 -1
  3. package/dist/cli.js +9 -0
  4. package/dist/extension-discovery.d.ts +5 -3
  5. package/dist/extension-discovery.js +14 -9
  6. package/dist/extension-registry.js +2 -2
  7. package/dist/onboarding.js +1 -0
  8. package/dist/remote-questions-config.js +2 -2
  9. package/dist/resources/extensions/browser-tools/package.json +3 -1
  10. package/dist/resources/extensions/cmux/index.js +55 -1
  11. package/dist/resources/extensions/context7/package.json +1 -1
  12. package/dist/resources/extensions/env-utils.js +29 -0
  13. package/dist/resources/extensions/get-secrets-from-user.js +5 -24
  14. package/dist/resources/extensions/github-sync/cli.js +284 -0
  15. package/dist/resources/extensions/github-sync/index.js +73 -0
  16. package/dist/resources/extensions/github-sync/mapping.js +67 -0
  17. package/dist/resources/extensions/github-sync/sync.js +424 -0
  18. package/dist/resources/extensions/github-sync/templates.js +118 -0
  19. package/dist/resources/extensions/github-sync/types.js +7 -0
  20. package/dist/resources/extensions/google-search/package.json +3 -1
  21. package/dist/resources/extensions/gsd/auto/session.js +6 -23
  22. package/dist/resources/extensions/gsd/auto-dispatch.js +74 -9
  23. package/dist/resources/extensions/gsd/auto-loop.js +149 -170
  24. package/dist/resources/extensions/gsd/auto-post-unit.js +105 -68
  25. package/dist/resources/extensions/gsd/auto-prompts.js +98 -33
  26. package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
  27. package/dist/resources/extensions/gsd/auto-start.js +13 -2
  28. package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
  29. package/dist/resources/extensions/gsd/auto.js +143 -96
  30. package/dist/resources/extensions/gsd/captures.js +9 -1
  31. package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
  32. package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
  33. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  34. package/dist/resources/extensions/gsd/commands.js +22 -2
  35. package/dist/resources/extensions/gsd/context-budget.js +2 -10
  36. package/dist/resources/extensions/gsd/detection.js +1 -2
  37. package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  38. package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
  39. package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
  40. package/dist/resources/extensions/gsd/doctor-format.js +15 -0
  41. package/dist/resources/extensions/gsd/doctor-providers.js +62 -12
  42. package/dist/resources/extensions/gsd/doctor.js +184 -11
  43. package/dist/resources/extensions/gsd/export.js +1 -1
  44. package/dist/resources/extensions/gsd/files.js +43 -2
  45. package/dist/resources/extensions/gsd/forensics.js +1 -1
  46. package/dist/resources/extensions/gsd/git-service.js +8 -1
  47. package/dist/resources/extensions/gsd/index.js +24 -20
  48. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  49. package/dist/resources/extensions/gsd/observability-validator.js +24 -0
  50. package/dist/resources/extensions/gsd/package.json +1 -1
  51. package/dist/resources/extensions/gsd/preferences-models.js +0 -12
  52. package/dist/resources/extensions/gsd/preferences-types.js +3 -2
  53. package/dist/resources/extensions/gsd/preferences-validation.js +101 -11
  54. package/dist/resources/extensions/gsd/preferences.js +8 -5
  55. package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
  56. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
  57. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  58. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  59. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  60. package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  61. package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
  62. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
  63. package/dist/resources/extensions/gsd/prompts/run-uat.md +25 -10
  64. package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  65. package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
  66. package/dist/resources/extensions/gsd/repo-identity.js +21 -4
  67. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  68. package/dist/resources/extensions/gsd/state.js +1 -1
  69. package/dist/resources/extensions/gsd/templates/task-plan.md +11 -3
  70. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  71. package/dist/resources/extensions/gsd/worktree.js +35 -16
  72. package/dist/resources/extensions/remote-questions/status.js +2 -1
  73. package/dist/resources/extensions/remote-questions/store.js +2 -1
  74. package/dist/resources/extensions/search-the-web/provider.js +2 -1
  75. package/dist/resources/extensions/subagent/index.js +12 -3
  76. package/dist/resources/extensions/subagent/isolation.js +2 -1
  77. package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
  78. package/dist/resources/extensions/universal-config/package.json +1 -1
  79. package/dist/welcome-screen.d.ts +12 -0
  80. package/dist/welcome-screen.js +53 -0
  81. package/package.json +2 -1
  82. package/packages/pi-ai/dist/env-api-keys.js +13 -0
  83. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  84. package/packages/pi-ai/dist/models.generated.d.ts +172 -0
  85. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  86. package/packages/pi-ai/dist/models.generated.js +172 -0
  87. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  88. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
  89. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
  90. package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
  91. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
  92. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
  93. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
  94. package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
  95. package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
  96. package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
  97. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  98. package/packages/pi-ai/dist/providers/anthropic.js +47 -764
  99. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  100. package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
  101. package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
  102. package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
  103. package/packages/pi-ai/dist/types.d.ts +2 -2
  104. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  105. package/packages/pi-ai/dist/types.js.map +1 -1
  106. package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
  107. package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
  108. package/packages/pi-ai/package.json +1 -0
  109. package/packages/pi-ai/src/env-api-keys.ts +14 -0
  110. package/packages/pi-ai/src/models.generated.ts +172 -0
  111. package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
  112. package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
  113. package/packages/pi-ai/src/providers/anthropic.ts +76 -868
  114. package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
  115. package/packages/pi-ai/src/types.ts +2 -0
  116. package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
  117. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  118. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  119. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  121. package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
  122. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  123. package/packages/pi-coding-agent/package.json +1 -1
  124. package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
  125. package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
  126. package/pkg/package.json +1 -1
  127. package/src/resources/extensions/cmux/index.ts +57 -1
  128. package/src/resources/extensions/env-utils.ts +31 -0
  129. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  130. package/src/resources/extensions/github-sync/cli.ts +364 -0
  131. package/src/resources/extensions/github-sync/index.ts +93 -0
  132. package/src/resources/extensions/github-sync/mapping.ts +81 -0
  133. package/src/resources/extensions/github-sync/sync.ts +556 -0
  134. package/src/resources/extensions/github-sync/templates.ts +183 -0
  135. package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
  136. package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
  137. package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
  138. package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
  139. package/src/resources/extensions/github-sync/types.ts +47 -0
  140. package/src/resources/extensions/gsd/auto/session.ts +7 -25
  141. package/src/resources/extensions/gsd/auto-dispatch.ts +99 -8
  142. package/src/resources/extensions/gsd/auto-loop.ts +207 -252
  143. package/src/resources/extensions/gsd/auto-post-unit.ts +82 -39
  144. package/src/resources/extensions/gsd/auto-prompts.ts +132 -36
  145. package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
  146. package/src/resources/extensions/gsd/auto-start.ts +18 -2
  147. package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
  148. package/src/resources/extensions/gsd/auto.ts +139 -101
  149. package/src/resources/extensions/gsd/captures.ts +10 -1
  150. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  151. package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
  152. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  153. package/src/resources/extensions/gsd/commands.ts +24 -2
  154. package/src/resources/extensions/gsd/context-budget.ts +2 -12
  155. package/src/resources/extensions/gsd/detection.ts +2 -2
  156. package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  157. package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
  158. package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
  159. package/src/resources/extensions/gsd/doctor-format.ts +20 -0
  160. package/src/resources/extensions/gsd/doctor-providers.ts +64 -10
  161. package/src/resources/extensions/gsd/doctor-types.ts +16 -1
  162. package/src/resources/extensions/gsd/doctor.ts +177 -13
  163. package/src/resources/extensions/gsd/export.ts +1 -1
  164. package/src/resources/extensions/gsd/files.ts +47 -2
  165. package/src/resources/extensions/gsd/forensics.ts +1 -1
  166. package/src/resources/extensions/gsd/git-service.ts +13 -1
  167. package/src/resources/extensions/gsd/index.ts +24 -17
  168. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  169. package/src/resources/extensions/gsd/observability-validator.ts +27 -0
  170. package/src/resources/extensions/gsd/preferences-models.ts +0 -12
  171. package/src/resources/extensions/gsd/preferences-types.ts +9 -5
  172. package/src/resources/extensions/gsd/preferences-validation.ts +92 -11
  173. package/src/resources/extensions/gsd/preferences.ts +8 -5
  174. package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
  175. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
  176. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  177. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  178. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  179. package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  180. package/src/resources/extensions/gsd/prompts/queue.md +4 -8
  181. package/src/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
  182. package/src/resources/extensions/gsd/prompts/run-uat.md +25 -10
  183. package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  184. package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
  185. package/src/resources/extensions/gsd/repo-identity.ts +23 -4
  186. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  187. package/src/resources/extensions/gsd/state.ts +1 -1
  188. package/src/resources/extensions/gsd/templates/task-plan.md +11 -3
  189. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
  190. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +16 -37
  191. package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
  192. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
  193. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +191 -3
  194. package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
  195. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
  196. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
  197. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +511 -0
  198. package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
  199. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
  200. package/src/resources/extensions/gsd/tests/run-uat.test.ts +11 -3
  201. package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
  202. package/src/resources/extensions/gsd/types.ts +43 -1
  203. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  204. package/src/resources/extensions/gsd/worktree.ts +35 -15
  205. package/src/resources/extensions/remote-questions/status.ts +3 -1
  206. package/src/resources/extensions/remote-questions/store.ts +3 -1
  207. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  208. package/src/resources/extensions/subagent/index.ts +12 -3
  209. package/src/resources/extensions/subagent/isolation.ts +3 -1
  210. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
  211. package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
  212. package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
  213. package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
  214. package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
  215. package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
  216. package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
  217. package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
  218. package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
  219. package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
  220. package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
  221. package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
  222. package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
package/README.md CHANGED
@@ -629,7 +629,7 @@ GSD isn't locked to one provider. It runs on the [Pi SDK](https://github.com/bad
629
629
 
630
630
  ### Built-in Providers
631
631
 
632
- Anthropic, OpenAI, Google (Gemini), OpenRouter, GitHub Copilot, Amazon Bedrock, Azure OpenAI, Google Vertex, Groq, Cerebras, Mistral, xAI, HuggingFace, Vercel AI Gateway, and more.
632
+ Anthropic, Anthropic (Vertex AI), OpenAI, Google (Gemini), OpenRouter, GitHub Copilot, Amazon Bedrock, Azure OpenAI, Google Vertex, Groq, Cerebras, Mistral, xAI, HuggingFace, Vercel AI Gateway, and more.
633
633
 
634
634
  ### OAuth / Max Plans
635
635
 
package/dist/app-paths.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { homedir } from 'os';
2
2
  import { join } from 'path';
3
- export const appRoot = join(homedir(), '.gsd');
3
+ export const appRoot = process.env.GSD_HOME || join(homedir(), '.gsd');
4
4
  export const agentDir = join(appRoot, 'agent');
5
5
  export const sessionsDir = join(appRoot, 'sessions');
6
6
  export const authFilePath = join(agentDir, 'auth.json');
package/dist/cli.js CHANGED
@@ -505,6 +505,15 @@ if (enabledModelPatterns && enabledModelPatterns.length > 0) {
505
505
  session.setScopedModels(scopedModels);
506
506
  }
507
507
  }
508
+ // Welcome screen — shown on every fresh interactive session before TUI takes over
509
+ {
510
+ const { printWelcomeScreen } = await import('./welcome-screen.js');
511
+ printWelcomeScreen({
512
+ version: process.env.GSD_VERSION || '0.0.0',
513
+ modelName: settingsManager.getDefaultModel() || undefined,
514
+ provider: settingsManager.getDefaultProvider() || undefined,
515
+ });
516
+ }
508
517
  const interactiveMode = new InteractiveMode(session);
509
518
  markStartup('InteractiveMode');
510
519
  printStartupTimings();
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Resolves the entry-point file(s) for a single extension directory.
3
3
  *
4
- * 1. If the directory contains a package.json with a `pi.extensions` array,
5
- * each entry is resolved relative to the directory and returned (if it exists).
6
- * 2. Otherwise falls back to `index.ts` → `index.js`.
4
+ * 1. If the directory contains a package.json with a `pi` manifest object,
5
+ * the manifest is authoritative:
6
+ * - `pi.extensions` array resolve each entry relative to the directory.
7
+ * - `pi: {}` (no extensions) → return empty (library opt-out, e.g. cmux).
8
+ * 2. Only when no `pi` manifest exists does it fall back to `index.ts` → `index.js`.
7
9
  */
8
10
  export declare function resolveExtensionEntries(dir: string): string[];
9
11
  /**
@@ -6,24 +6,29 @@ function isExtensionFile(name) {
6
6
  /**
7
7
  * Resolves the entry-point file(s) for a single extension directory.
8
8
  *
9
- * 1. If the directory contains a package.json with a `pi.extensions` array,
10
- * each entry is resolved relative to the directory and returned (if it exists).
11
- * 2. Otherwise falls back to `index.ts` → `index.js`.
9
+ * 1. If the directory contains a package.json with a `pi` manifest object,
10
+ * the manifest is authoritative:
11
+ * - `pi.extensions` array resolve each entry relative to the directory.
12
+ * - `pi: {}` (no extensions) → return empty (library opt-out, e.g. cmux).
13
+ * 2. Only when no `pi` manifest exists does it fall back to `index.ts` → `index.js`.
12
14
  */
13
15
  export function resolveExtensionEntries(dir) {
14
16
  const packageJsonPath = join(dir, 'package.json');
15
17
  if (existsSync(packageJsonPath)) {
16
18
  try {
17
19
  const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
18
- const declared = pkg?.pi?.extensions;
19
- if (Array.isArray(declared)) {
20
- const resolved = declared
20
+ if (pkg?.pi && typeof pkg.pi === 'object') {
21
+ // When a pi manifest exists, it is authoritative — don't fall through
22
+ // to index.ts/index.js auto-detection. This allows library directories
23
+ // (like cmux) to opt out by declaring "pi": {} with no extensions.
24
+ const declared = pkg.pi.extensions;
25
+ if (!Array.isArray(declared) || declared.length === 0) {
26
+ return [];
27
+ }
28
+ return declared
21
29
  .filter((entry) => typeof entry === 'string')
22
30
  .map((entry) => resolve(dir, entry))
23
31
  .filter((entry) => existsSync(entry));
24
- if (resolved.length > 0) {
25
- return resolved;
26
- }
27
32
  }
28
33
  }
29
34
  catch {
@@ -6,7 +6,7 @@
6
6
  * The only way an extension stops loading is an explicit `gsd extensions disable <id>`.
7
7
  */
8
8
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
9
- import { homedir } from "node:os";
9
+ import { appRoot } from "./app-paths.js";
10
10
  import { dirname, join } from "node:path";
11
11
  // ─── Validation ─────────────────────────────────────────────────────────────
12
12
  function isRegistry(data) {
@@ -26,7 +26,7 @@ function isManifest(data) {
26
26
  }
27
27
  // ─── Registry Path ──────────────────────────────────────────────────────────
28
28
  export function getRegistryPath() {
29
- return join(homedir(), ".gsd", "extensions", "registry.json");
29
+ return join(appRoot, "extensions", "registry.json");
30
30
  }
31
31
  // ─── Registry I/O ───────────────────────────────────────────────────────────
32
32
  function defaultRegistry() {
@@ -38,6 +38,7 @@ const TOOL_KEYS = [
38
38
  /** Known LLM provider IDs that, if authed, mean the user doesn't need onboarding */
39
39
  const LLM_PROVIDER_IDS = [
40
40
  'anthropic',
41
+ 'anthropic-vertex',
41
42
  'openai',
42
43
  'github-copilot',
43
44
  'openai-codex',
@@ -9,12 +9,12 @@
9
9
  */
10
10
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
11
11
  import { dirname, join } from "node:path";
12
- import { homedir } from "node:os";
12
+ import { appRoot } from "./app-paths.js";
13
13
  // Inlined from preferences.ts to avoid crossing the compiled/uncompiled
14
14
  // boundary — this file is compiled by tsc, but preferences.ts is loaded
15
15
  // via jiti at runtime. Importing it as .js fails because no .js exists
16
16
  // in dist/. See #592, #1110.
17
- const GLOBAL_PREFERENCES_PATH = join(homedir(), ".gsd", "preferences.md");
17
+ const GLOBAL_PREFERENCES_PATH = join(appRoot, "preferences.md");
18
18
  export function saveRemoteQuestionsConfig(channel, channelId) {
19
19
  const prefsPath = GLOBAL_PREFERENCES_PATH;
20
20
  const block = [
@@ -7,7 +7,9 @@
7
7
  "test": "node --test tests/*.test.mjs"
8
8
  },
9
9
  "pi": {
10
- "extensions": ["./index.ts"]
10
+ "extensions": [
11
+ "./index.js"
12
+ ]
11
13
  },
12
14
  "peerDependencies": {
13
15
  "playwright": ">=1.40.0",
@@ -237,11 +237,14 @@ export class CmuxClient {
237
237
  return extractSurfaceIds(parsed);
238
238
  }
239
239
  async createSplit(direction) {
240
+ return this.createSplitFrom(this.config.surfaceId, direction);
241
+ }
242
+ async createSplitFrom(sourceSurfaceId, direction) {
240
243
  if (!this.config.splits)
241
244
  return null;
242
245
  const before = new Set(await this.listSurfaceIds());
243
246
  const args = ["new-split", direction];
244
- const scopedArgs = this.appendSurface(this.appendWorkspace(args), this.config.surfaceId);
247
+ const scopedArgs = this.appendSurface(this.appendWorkspace(args), sourceSurfaceId);
245
248
  await this.runAsync(scopedArgs);
246
249
  const after = await this.listSurfaceIds();
247
250
  for (const id of after) {
@@ -250,6 +253,57 @@ export class CmuxClient {
250
253
  }
251
254
  return null;
252
255
  }
256
+ /**
257
+ * Create a grid of surfaces for parallel agent execution.
258
+ *
259
+ * Layout strategy (gsd stays in the original surface):
260
+ * 1 agent: [gsd | A]
261
+ * 2 agents: [gsd | A]
262
+ * [ | B]
263
+ * 3 agents: [gsd | A]
264
+ * [ C | B]
265
+ * 4 agents: [gsd | A]
266
+ * [ C | B] (D splits from B downward)
267
+ * [ | D]
268
+ *
269
+ * Returns surface IDs in order, or empty array on failure.
270
+ */
271
+ async createGridLayout(count) {
272
+ if (!this.config.splits || count <= 0)
273
+ return [];
274
+ const surfaces = [];
275
+ // First split: create right column from the gsd surface
276
+ const rightCol = await this.createSplitFrom(this.config.surfaceId, "right");
277
+ if (!rightCol)
278
+ return [];
279
+ surfaces.push(rightCol);
280
+ if (count === 1)
281
+ return surfaces;
282
+ // Second split: split right column down → bottom-right
283
+ const bottomRight = await this.createSplitFrom(rightCol, "down");
284
+ if (!bottomRight)
285
+ return surfaces;
286
+ surfaces.push(bottomRight);
287
+ if (count === 2)
288
+ return surfaces;
289
+ // Third split: split gsd surface down → bottom-left
290
+ const bottomLeft = await this.createSplitFrom(this.config.surfaceId, "down");
291
+ if (!bottomLeft)
292
+ return surfaces;
293
+ surfaces.push(bottomLeft);
294
+ if (count === 3)
295
+ return surfaces;
296
+ // Fourth+: split subsequent surfaces down from the last created
297
+ let lastSurface = bottomRight;
298
+ for (let i = 3; i < count; i++) {
299
+ const next = await this.createSplitFrom(lastSurface, "down");
300
+ if (!next)
301
+ break;
302
+ surfaces.push(next);
303
+ lastSurface = next;
304
+ }
305
+ return surfaces;
306
+ }
253
307
  async sendSurface(surfaceId, text) {
254
308
  const payload = text.endsWith("\n") ? text : `${text}\n`;
255
309
  const stdout = await this.runAsync(["send-surface", "--surface", surfaceId, payload]);
@@ -5,7 +5,7 @@
5
5
  "type": "module",
6
6
  "pi": {
7
7
  "extensions": [
8
- "./index.ts"
8
+ "./index.js"
9
9
  ]
10
10
  }
11
11
  }
@@ -0,0 +1,29 @@
1
+ // GSD Extension — Environment variable utilities
2
+ // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
3
+ //
4
+ // Pure utility for checking existing env keys in .env files and process.env.
5
+ // Extracted from get-secrets-from-user.ts to avoid pulling in @gsd/pi-tui
6
+ // when only env-checking is needed (e.g. from files.ts during report generation).
7
+ import { readFile } from "node:fs/promises";
8
+ /**
9
+ * Check which keys already exist in a .env file or process.env.
10
+ * Returns the subset of `keys` that are already set.
11
+ */
12
+ export async function checkExistingEnvKeys(keys, envFilePath) {
13
+ let fileContent = "";
14
+ try {
15
+ fileContent = await readFile(envFilePath, "utf8");
16
+ }
17
+ catch {
18
+ // ENOENT or other read error — proceed with empty content
19
+ }
20
+ const existing = [];
21
+ for (const key of keys) {
22
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23
+ const regex = new RegExp(`^${escaped}\\s*=`, "m");
24
+ if (regex.test(fileContent) || key in process.env) {
25
+ existing.push(key);
26
+ }
27
+ }
28
+ return existing;
29
+ }
@@ -46,30 +46,11 @@ async function writeEnvKey(filePath, key, value) {
46
46
  await writeFile(filePath, content, "utf8");
47
47
  }
48
48
  // ─── Exported utilities ───────────────────────────────────────────────────────
49
- /**
50
- * Check which keys already exist in the .env file or process.env.
51
- * Returns the subset of `keys` that are already set.
52
- * Handles ENOENT gracefully (still checks process.env).
53
- * Empty-string values count as existing.
54
- */
55
- export async function checkExistingEnvKeys(keys, envFilePath) {
56
- let fileContent = "";
57
- try {
58
- fileContent = await readFile(envFilePath, "utf8");
59
- }
60
- catch {
61
- // ENOENT or other read error — proceed with empty content
62
- }
63
- const existing = [];
64
- for (const key of keys) {
65
- const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
66
- const regex = new RegExp(`^${escaped}\\s*=`, "m");
67
- if (regex.test(fileContent) || key in process.env) {
68
- existing.push(key);
69
- }
70
- }
71
- return existing;
72
- }
49
+ // Re-export from env-utils.ts so existing consumers still work.
50
+ // The implementation lives in env-utils.ts to avoid pulling @gsd/pi-tui
51
+ // into modules that only need env-checking (e.g. files.ts during reports).
52
+ import { checkExistingEnvKeys } from "./env-utils.js";
53
+ export { checkExistingEnvKeys };
73
54
  /**
74
55
  * Detect the write destination based on project files in basePath.
75
56
  * Priority: vercel.json → convex/ dir → fallback "dotenv".
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Thin wrapper around the `gh` CLI.
3
+ *
4
+ * Every public function returns `GhResult<T>` — never throws.
5
+ * Uses `execFileSync` (not `execSync`) for safety.
6
+ */
7
+ import { execFileSync } from "node:child_process";
8
+ function ok(data) {
9
+ return { ok: true, data };
10
+ }
11
+ function fail(error) {
12
+ return { ok: false, error };
13
+ }
14
+ // ─── gh Availability ────────────────────────────────────────────────────────
15
+ let _ghAvailable = null;
16
+ export function ghIsAvailable() {
17
+ if (_ghAvailable !== null)
18
+ return _ghAvailable;
19
+ try {
20
+ execFileSync("gh", ["--version"], {
21
+ encoding: "utf-8",
22
+ stdio: ["ignore", "pipe", "ignore"],
23
+ timeout: 5_000,
24
+ });
25
+ _ghAvailable = true;
26
+ }
27
+ catch {
28
+ _ghAvailable = false;
29
+ }
30
+ return _ghAvailable;
31
+ }
32
+ /** Reset cached availability (for testing). */
33
+ export function _resetGhCache() {
34
+ _ghAvailable = null;
35
+ }
36
+ // ─── Rate Limit Check ───────────────────────────────────────────────────────
37
+ let _rateLimitCheckedAt = 0;
38
+ let _rateLimitOk = true;
39
+ const RATE_LIMIT_CHECK_INTERVAL_MS = 300_000; // 5 minutes
40
+ export function ghHasRateLimit(cwd) {
41
+ const now = Date.now();
42
+ if (now - _rateLimitCheckedAt < RATE_LIMIT_CHECK_INTERVAL_MS)
43
+ return _rateLimitOk;
44
+ _rateLimitCheckedAt = now;
45
+ try {
46
+ const raw = execFileSync("gh", ["api", "rate_limit", "--jq", ".rate.remaining"], {
47
+ cwd,
48
+ encoding: "utf-8",
49
+ stdio: ["ignore", "pipe", "ignore"],
50
+ timeout: 10_000,
51
+ }).trim();
52
+ const remaining = parseInt(raw, 10);
53
+ _rateLimitOk = Number.isFinite(remaining) && remaining >= 100;
54
+ }
55
+ catch {
56
+ // Can't check — assume OK so we don't silently disable sync
57
+ _rateLimitOk = true;
58
+ }
59
+ return _rateLimitOk;
60
+ }
61
+ // ─── Helpers ────────────────────────────────────────────────────────────────
62
+ const GH_TIMEOUT = 15_000;
63
+ const MAX_BODY_LENGTH = 65_000;
64
+ function truncateBody(body) {
65
+ if (body.length <= MAX_BODY_LENGTH)
66
+ return body;
67
+ return body.slice(0, MAX_BODY_LENGTH) + "\n\n---\n*Body truncated (exceeded 65K characters)*";
68
+ }
69
+ function runGh(args, cwd) {
70
+ try {
71
+ const stdout = execFileSync("gh", args, {
72
+ cwd,
73
+ encoding: "utf-8",
74
+ stdio: ["ignore", "pipe", "pipe"],
75
+ timeout: GH_TIMEOUT,
76
+ }).trim();
77
+ return ok(stdout);
78
+ }
79
+ catch (err) {
80
+ const msg = err instanceof Error ? err.message : String(err);
81
+ return fail(msg);
82
+ }
83
+ }
84
+ function runGhJson(args, cwd) {
85
+ const result = runGh(args, cwd);
86
+ if (!result.ok)
87
+ return fail(result.error);
88
+ try {
89
+ return ok(JSON.parse(result.data));
90
+ }
91
+ catch {
92
+ return fail(`Failed to parse JSON: ${result.data}`);
93
+ }
94
+ }
95
+ // ─── Repo Detection ─────────────────────────────────────────────────────────
96
+ export function ghDetectRepo(cwd) {
97
+ const result = runGh(["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], cwd);
98
+ if (!result.ok)
99
+ return fail(result.error);
100
+ const repo = result.data.trim();
101
+ if (!repo || !repo.includes("/"))
102
+ return fail("Could not detect repo");
103
+ return ok(repo);
104
+ }
105
+ export function ghCreateIssue(cwd, opts) {
106
+ const args = [
107
+ "issue", "create",
108
+ "--repo", opts.repo,
109
+ "--title", opts.title,
110
+ "--body", truncateBody(opts.body),
111
+ ];
112
+ if (opts.labels?.length) {
113
+ args.push("--label", opts.labels.join(","));
114
+ }
115
+ if (opts.milestone) {
116
+ args.push("--milestone", String(opts.milestone));
117
+ }
118
+ const result = runGh(args, cwd);
119
+ if (!result.ok)
120
+ return fail(result.error);
121
+ // gh issue create returns the URL; extract issue number
122
+ const match = result.data.match(/\/issues\/(\d+)/);
123
+ if (!match)
124
+ return fail(`Could not parse issue number from: ${result.data}`);
125
+ const issueNumber = parseInt(match[1], 10);
126
+ // If parent specified, add as sub-issue via GraphQL
127
+ if (opts.parentIssue) {
128
+ ghAddSubIssue(cwd, opts.repo, opts.parentIssue, issueNumber);
129
+ }
130
+ return ok(issueNumber);
131
+ }
132
+ export function ghCloseIssue(cwd, repo, issueNumber, comment) {
133
+ if (comment) {
134
+ ghAddComment(cwd, repo, issueNumber, comment);
135
+ }
136
+ const result = runGh(["issue", "close", String(issueNumber), "--repo", repo], cwd);
137
+ if (!result.ok)
138
+ return fail(result.error);
139
+ return ok(undefined);
140
+ }
141
+ export function ghAddComment(cwd, repo, issueNumber, body) {
142
+ const result = runGh(["issue", "comment", String(issueNumber), "--repo", repo, "--body", truncateBody(body)], cwd);
143
+ if (!result.ok)
144
+ return fail(result.error);
145
+ return ok(undefined);
146
+ }
147
+ // ─── Sub-Issues (GraphQL) ───────────────────────────────────────────────────
148
+ function ghAddSubIssue(cwd, repo, parentNumber, childNumber) {
149
+ // Get node IDs for both issues
150
+ const parentResult = runGhJson(["api", `repos/${repo}/issues/${parentNumber}`, "--jq", "{id: .node_id}"], cwd);
151
+ const childResult = runGhJson(["api", `repos/${repo}/issues/${childNumber}`, "--jq", "{id: .node_id}"], cwd);
152
+ if (!parentResult.ok || !childResult.ok) {
153
+ return fail("Could not resolve issue node IDs for sub-issue linking");
154
+ }
155
+ const mutation = `mutation { addSubIssue(input: { issueId: "${parentResult.data.id}", subIssueId: "${childResult.data.id}" }) { issue { id } } }`;
156
+ return runGh(["api", "graphql", "-f", `query=${mutation}`], cwd);
157
+ }
158
+ // ─── Milestones ─────────────────────────────────────────────────────────────
159
+ export function ghCreateMilestone(cwd, repo, title, description) {
160
+ const result = runGhJson([
161
+ "api", `repos/${repo}/milestones`,
162
+ "-X", "POST",
163
+ "-f", `title=${title}`,
164
+ "-f", `description=${truncateBody(description)}`,
165
+ "-f", "state=open",
166
+ "--jq", "{number: .number}",
167
+ ], cwd);
168
+ if (!result.ok)
169
+ return fail(result.error);
170
+ return ok(result.data.number);
171
+ }
172
+ export function ghCloseMilestone(cwd, repo, milestoneNumber) {
173
+ const result = runGh([
174
+ "api", `repos/${repo}/milestones/${milestoneNumber}`,
175
+ "-X", "PATCH",
176
+ "-f", "state=closed",
177
+ ], cwd);
178
+ if (!result.ok)
179
+ return fail(result.error);
180
+ return ok(undefined);
181
+ }
182
+ export function ghCreatePR(cwd, opts) {
183
+ const args = [
184
+ "pr", "create",
185
+ "--repo", opts.repo,
186
+ "--base", opts.base,
187
+ "--head", opts.head,
188
+ "--title", opts.title,
189
+ "--body", truncateBody(opts.body),
190
+ ];
191
+ if (opts.draft)
192
+ args.push("--draft");
193
+ const result = runGh(args, cwd);
194
+ if (!result.ok)
195
+ return fail(result.error);
196
+ const match = result.data.match(/\/pull\/(\d+)/);
197
+ if (!match)
198
+ return fail(`Could not parse PR number from: ${result.data}`);
199
+ return ok(parseInt(match[1], 10));
200
+ }
201
+ export function ghMarkPRReady(cwd, repo, prNumber) {
202
+ const result = runGh(["pr", "ready", String(prNumber), "--repo", repo], cwd);
203
+ if (!result.ok)
204
+ return fail(result.error);
205
+ return ok(undefined);
206
+ }
207
+ export function ghMergePR(cwd, repo, prNumber, strategy = "squash") {
208
+ const args = [
209
+ "pr", "merge", String(prNumber),
210
+ "--repo", repo,
211
+ strategy === "squash" ? "--squash" : "--merge",
212
+ "--delete-branch",
213
+ ];
214
+ const result = runGh(args, cwd);
215
+ if (!result.ok)
216
+ return fail(result.error);
217
+ return ok(undefined);
218
+ }
219
+ // ─── Projects v2 ────────────────────────────────────────────────────────────
220
+ export function ghAddToProject(cwd, repo, projectNumber, issueNumber) {
221
+ // Get the issue's node ID first
222
+ const issueResult = runGhJson(["api", `repos/${repo}/issues/${issueNumber}`, "--jq", "{id: .node_id}"], cwd);
223
+ if (!issueResult.ok)
224
+ return fail(issueResult.error);
225
+ // Get the project's node ID
226
+ const [owner] = repo.split("/");
227
+ const projectResult = runGhJson([
228
+ "api", "graphql",
229
+ "-f", `query=query { user(login: "${owner}") { projectV2(number: ${projectNumber}) { id } } }`,
230
+ "--jq", ".data.user.projectV2.id",
231
+ ], cwd);
232
+ // Try org if user fails
233
+ let projectId;
234
+ if (projectResult.ok && projectResult.data?.id) {
235
+ projectId = projectResult.data.id;
236
+ }
237
+ else {
238
+ const orgResult = runGhJson([
239
+ "api", "graphql",
240
+ "-f", `query=query { organization(login: "${owner}") { projectV2(number: ${projectNumber}) { id } } }`,
241
+ "--jq", ".data.organization.projectV2.id",
242
+ ], cwd);
243
+ if (orgResult.ok)
244
+ projectId = orgResult.data?.id;
245
+ }
246
+ if (!projectId)
247
+ return fail("Could not find project");
248
+ const mutation = `mutation { addProjectV2ItemById(input: { projectId: "${projectId}", contentId: "${issueResult.data.id}" }) { item { id } } }`;
249
+ return runGh(["api", "graphql", "-f", `query=${mutation}`], cwd);
250
+ }
251
+ // ─── Branch Operations ──────────────────────────────────────────────────────
252
+ export function ghPushBranch(cwd, branch, setUpstream = true) {
253
+ const args = ["git", "push"];
254
+ if (setUpstream)
255
+ args.push("-u", "origin", branch);
256
+ else
257
+ args.push("origin", branch);
258
+ try {
259
+ execFileSync(args[0], args.slice(1), {
260
+ cwd,
261
+ encoding: "utf-8",
262
+ stdio: ["ignore", "pipe", "pipe"],
263
+ timeout: 30_000,
264
+ });
265
+ return ok(undefined);
266
+ }
267
+ catch (err) {
268
+ return fail(err instanceof Error ? err.message : String(err));
269
+ }
270
+ }
271
+ export function ghCreateBranch(cwd, branch, from) {
272
+ try {
273
+ execFileSync("git", ["branch", branch, from], {
274
+ cwd,
275
+ encoding: "utf-8",
276
+ stdio: ["ignore", "pipe", "pipe"],
277
+ timeout: 10_000,
278
+ });
279
+ return ok(undefined);
280
+ }
281
+ catch (err) {
282
+ return fail(err instanceof Error ? err.message : String(err));
283
+ }
284
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * GitHub Sync extension for GSD.
3
+ *
4
+ * Opt-in extension that syncs GSD lifecycle events to GitHub:
5
+ * milestones → GH Milestones + tracking issues, slices → draft PRs,
6
+ * tasks → sub-issues with auto-close on commit.
7
+ *
8
+ * Integration happens via a single dynamic import in auto-post-unit.ts.
9
+ * This index registers a `/github-sync` command for manual bootstrap
10
+ * and status display.
11
+ */
12
+ import { bootstrapSync } from "./sync.js";
13
+ import { loadSyncMapping } from "./mapping.js";
14
+ import { ghIsAvailable } from "./cli.js";
15
+ export default function (pi) {
16
+ pi.registerCommand("github-sync", {
17
+ description: "Bootstrap GitHub sync or show sync status",
18
+ handler: async (args, ctx) => {
19
+ const subcommand = args.trim().toLowerCase();
20
+ if (subcommand === "status") {
21
+ await showStatus(ctx);
22
+ return;
23
+ }
24
+ if (subcommand === "bootstrap" || subcommand === "") {
25
+ await runBootstrap(ctx);
26
+ return;
27
+ }
28
+ ctx.ui.notify("Usage: /github-sync [bootstrap|status]", "info");
29
+ },
30
+ });
31
+ }
32
+ async function showStatus(ctx) {
33
+ if (!ghIsAvailable()) {
34
+ ctx.ui.notify("GitHub sync: `gh` CLI not installed or not authenticated.", "warning");
35
+ return;
36
+ }
37
+ const mapping = loadSyncMapping(ctx.cwd);
38
+ if (!mapping) {
39
+ ctx.ui.notify("GitHub sync: No sync mapping found. Run `/github-sync bootstrap` to initialize.", "info");
40
+ return;
41
+ }
42
+ const milestoneCount = Object.keys(mapping.milestones).length;
43
+ const sliceCount = Object.keys(mapping.slices).length;
44
+ const taskCount = Object.keys(mapping.tasks).length;
45
+ const openMilestones = Object.values(mapping.milestones).filter(m => m.state === "open").length;
46
+ const openSlices = Object.values(mapping.slices).filter(s => s.state === "open").length;
47
+ const openTasks = Object.values(mapping.tasks).filter(t => t.state === "open").length;
48
+ ctx.ui.notify([
49
+ `GitHub sync: repo=${mapping.repo}`,
50
+ ` Milestones: ${milestoneCount} (${openMilestones} open)`,
51
+ ` Slices: ${sliceCount} (${openSlices} open)`,
52
+ ` Tasks: ${taskCount} (${openTasks} open)`,
53
+ ].join("\n"), "info");
54
+ }
55
+ async function runBootstrap(ctx) {
56
+ if (!ghIsAvailable()) {
57
+ ctx.ui.notify("GitHub sync: `gh` CLI not installed or not authenticated.", "warning");
58
+ return;
59
+ }
60
+ ctx.ui.notify("GitHub sync: bootstrapping...", "info");
61
+ try {
62
+ const counts = await bootstrapSync(ctx.cwd);
63
+ if (counts.milestones === 0 && counts.slices === 0 && counts.tasks === 0) {
64
+ ctx.ui.notify("GitHub sync: everything already synced (or no milestones found).", "info");
65
+ }
66
+ else {
67
+ ctx.ui.notify(`GitHub sync: created ${counts.milestones} milestone(s), ${counts.slices} slice(s), ${counts.tasks} task(s).`, "info");
68
+ }
69
+ }
70
+ catch (err) {
71
+ ctx.ui.notify(`GitHub sync bootstrap failed: ${err}`, "error");
72
+ }
73
+ }