gsd-pi 2.36.0-dev.f887f4e → 2.37.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 (48) hide show
  1. package/dist/resources/extensions/cmux/index.js +321 -0
  2. package/dist/resources/extensions/gsd/auto-dashboard.js +334 -104
  3. package/dist/resources/extensions/gsd/auto-loop.js +11 -0
  4. package/dist/resources/extensions/gsd/auto.js +16 -0
  5. package/dist/resources/extensions/gsd/commands-cmux.js +120 -0
  6. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  7. package/dist/resources/extensions/gsd/commands.js +51 -1
  8. package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -0
  9. package/dist/resources/extensions/gsd/index.js +5 -0
  10. package/dist/resources/extensions/gsd/notifications.js +10 -1
  11. package/dist/resources/extensions/gsd/preferences-types.js +2 -0
  12. package/dist/resources/extensions/gsd/preferences-validation.js +29 -0
  13. package/dist/resources/extensions/gsd/preferences.js +3 -0
  14. package/dist/resources/extensions/gsd/prompts/research-milestone.md +4 -3
  15. package/dist/resources/extensions/gsd/prompts/research-slice.md +3 -2
  16. package/dist/resources/extensions/gsd/templates/preferences.md +6 -0
  17. package/dist/resources/extensions/search-the-web/native-search.js +45 -4
  18. package/dist/resources/extensions/shared/terminal.js +5 -0
  19. package/dist/resources/extensions/subagent/index.js +180 -60
  20. package/package.json +1 -1
  21. package/packages/pi-coding-agent/package.json +1 -1
  22. package/packages/pi-tui/dist/terminal-image.d.ts.map +1 -1
  23. package/packages/pi-tui/dist/terminal-image.js +4 -0
  24. package/packages/pi-tui/dist/terminal-image.js.map +1 -1
  25. package/packages/pi-tui/src/terminal-image.ts +5 -0
  26. package/pkg/package.json +1 -1
  27. package/src/resources/extensions/cmux/index.ts +384 -0
  28. package/src/resources/extensions/gsd/auto-dashboard.ts +363 -116
  29. package/src/resources/extensions/gsd/auto-loop.ts +42 -0
  30. package/src/resources/extensions/gsd/auto.ts +21 -0
  31. package/src/resources/extensions/gsd/commands-cmux.ts +143 -0
  32. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  33. package/src/resources/extensions/gsd/commands.ts +54 -1
  34. package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -0
  35. package/src/resources/extensions/gsd/index.ts +8 -0
  36. package/src/resources/extensions/gsd/notifications.ts +10 -1
  37. package/src/resources/extensions/gsd/preferences-types.ts +13 -0
  38. package/src/resources/extensions/gsd/preferences-validation.ts +26 -0
  39. package/src/resources/extensions/gsd/preferences.ts +4 -0
  40. package/src/resources/extensions/gsd/prompts/research-milestone.md +4 -3
  41. package/src/resources/extensions/gsd/prompts/research-slice.md +3 -2
  42. package/src/resources/extensions/gsd/templates/preferences.md +6 -0
  43. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -0
  44. package/src/resources/extensions/gsd/tests/cmux.test.ts +98 -0
  45. package/src/resources/extensions/gsd/tests/preferences.test.ts +23 -0
  46. package/src/resources/extensions/search-the-web/native-search.ts +50 -4
  47. package/src/resources/extensions/shared/terminal.ts +5 -0
  48. package/src/resources/extensions/subagent/index.ts +236 -79
@@ -0,0 +1,321 @@
1
+ import { execFile, execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { promisify } from "node:util";
4
+ const execFileAsync = promisify(execFile);
5
+ const DEFAULT_SOCKET_PATH = "/tmp/cmux.sock";
6
+ const STATUS_KEY = "gsd";
7
+ const lastSidebarSnapshots = new Map();
8
+ let cmuxPromptedThisSession = false;
9
+ let cachedCliAvailability = null;
10
+ export function detectCmuxEnvironment(env = process.env, socketExists = existsSync, cliAvailable = isCmuxCliAvailable) {
11
+ const socketPath = env.CMUX_SOCKET_PATH ?? DEFAULT_SOCKET_PATH;
12
+ const workspaceId = env.CMUX_WORKSPACE_ID?.trim() || undefined;
13
+ const surfaceId = env.CMUX_SURFACE_ID?.trim() || undefined;
14
+ const available = Boolean(workspaceId && surfaceId && socketExists(socketPath));
15
+ return {
16
+ available,
17
+ cliAvailable: cliAvailable(),
18
+ socketPath,
19
+ workspaceId,
20
+ surfaceId,
21
+ };
22
+ }
23
+ export function resolveCmuxConfig(preferences, env = process.env, socketExists = existsSync, cliAvailable = isCmuxCliAvailable) {
24
+ const detected = detectCmuxEnvironment(env, socketExists, cliAvailable);
25
+ const cmux = preferences?.cmux ?? {};
26
+ const enabled = detected.available && cmux.enabled === true;
27
+ return {
28
+ ...detected,
29
+ enabled,
30
+ notifications: enabled && cmux.notifications !== false,
31
+ sidebar: enabled && cmux.sidebar !== false,
32
+ splits: enabled && cmux.splits === true,
33
+ browser: enabled && cmux.browser === true,
34
+ };
35
+ }
36
+ export function shouldPromptToEnableCmux(preferences, env = process.env, socketExists = existsSync, cliAvailable = isCmuxCliAvailable) {
37
+ if (cmuxPromptedThisSession)
38
+ return false;
39
+ const detected = detectCmuxEnvironment(env, socketExists, cliAvailable);
40
+ if (!detected.available)
41
+ return false;
42
+ return preferences?.cmux?.enabled === undefined;
43
+ }
44
+ export function markCmuxPromptShown() {
45
+ cmuxPromptedThisSession = true;
46
+ }
47
+ export function resetCmuxPromptState() {
48
+ cmuxPromptedThisSession = false;
49
+ }
50
+ export function isCmuxCliAvailable() {
51
+ if (cachedCliAvailability !== null)
52
+ return cachedCliAvailability;
53
+ try {
54
+ execFileSync("cmux", ["--help"], { stdio: "ignore", timeout: 1000 });
55
+ cachedCliAvailability = true;
56
+ }
57
+ catch {
58
+ cachedCliAvailability = false;
59
+ }
60
+ return cachedCliAvailability;
61
+ }
62
+ export function supportsOsc777Notifications(env = process.env) {
63
+ const termProgram = env.TERM_PROGRAM?.toLowerCase() ?? "";
64
+ return termProgram === "ghostty" || termProgram === "wezterm" || termProgram === "iterm.app";
65
+ }
66
+ export function emitOsc777Notification(title, body) {
67
+ if (!supportsOsc777Notifications())
68
+ return;
69
+ const safeTitle = normalizeNotificationText(title).replace(/;/g, ",");
70
+ const safeBody = normalizeNotificationText(body).replace(/;/g, ",");
71
+ process.stdout.write(`\x1b]777;notify;${safeTitle};${safeBody}\x07`);
72
+ }
73
+ export function buildCmuxStatusLabel(state) {
74
+ const parts = [];
75
+ if (state.activeMilestone)
76
+ parts.push(state.activeMilestone.id);
77
+ if (state.activeSlice)
78
+ parts.push(state.activeSlice.id);
79
+ if (state.activeTask) {
80
+ const prev = parts.pop();
81
+ parts.push(prev ? `${prev}/${state.activeTask.id}` : state.activeTask.id);
82
+ }
83
+ if (parts.length === 0)
84
+ return state.phase;
85
+ return `${parts.join(" ")} · ${state.phase}`;
86
+ }
87
+ export function buildCmuxProgress(state) {
88
+ const progress = state.progress;
89
+ if (!progress)
90
+ return null;
91
+ const choose = (done, total, label) => {
92
+ if (total <= 0)
93
+ return null;
94
+ return { value: Math.max(0, Math.min(1, done / total)), label: `${done}/${total} ${label}` };
95
+ };
96
+ return choose(progress.tasks?.done ?? 0, progress.tasks?.total ?? 0, "tasks")
97
+ ?? choose(progress.slices?.done ?? 0, progress.slices?.total ?? 0, "slices")
98
+ ?? choose(progress.milestones.done, progress.milestones.total, "milestones");
99
+ }
100
+ function phaseVisuals(phase) {
101
+ switch (phase) {
102
+ case "blocked":
103
+ return { icon: "triangle-alert", color: "#ef4444" };
104
+ case "paused":
105
+ return { icon: "pause", color: "#f59e0b" };
106
+ case "complete":
107
+ case "completing-milestone":
108
+ return { icon: "check", color: "#22c55e" };
109
+ case "planning":
110
+ case "researching":
111
+ case "replanning-slice":
112
+ return { icon: "compass", color: "#3b82f6" };
113
+ case "validating-milestone":
114
+ case "verifying":
115
+ return { icon: "shield-check", color: "#06b6d4" };
116
+ default:
117
+ return { icon: "rocket", color: "#4ade80" };
118
+ }
119
+ }
120
+ function sidebarSnapshotKey(config) {
121
+ return config.workspaceId ?? "default";
122
+ }
123
+ export class CmuxClient {
124
+ config;
125
+ constructor(config) {
126
+ this.config = config;
127
+ }
128
+ static fromPreferences(preferences) {
129
+ return new CmuxClient(resolveCmuxConfig(preferences));
130
+ }
131
+ getConfig() {
132
+ return this.config;
133
+ }
134
+ canRun() {
135
+ return this.config.available && this.config.cliAvailable;
136
+ }
137
+ appendWorkspace(args) {
138
+ return this.config.workspaceId ? [...args, "--workspace", this.config.workspaceId] : args;
139
+ }
140
+ appendSurface(args, surfaceId) {
141
+ return surfaceId ? [...args, "--surface", surfaceId] : args;
142
+ }
143
+ runSync(args) {
144
+ if (!this.canRun())
145
+ return null;
146
+ try {
147
+ return execFileSync("cmux", args, {
148
+ encoding: "utf-8",
149
+ timeout: 3000,
150
+ env: process.env,
151
+ });
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ }
157
+ async runAsync(args) {
158
+ if (!this.canRun())
159
+ return null;
160
+ try {
161
+ const result = await execFileAsync("cmux", args, {
162
+ encoding: "utf-8",
163
+ timeout: 5000,
164
+ env: process.env,
165
+ });
166
+ return result.stdout;
167
+ }
168
+ catch {
169
+ return null;
170
+ }
171
+ }
172
+ getCapabilities() {
173
+ const stdout = this.runSync(["capabilities", "--json"]);
174
+ return stdout ? parseJson(stdout) : null;
175
+ }
176
+ identify() {
177
+ const stdout = this.runSync(["identify", "--json"]);
178
+ return stdout ? parseJson(stdout) : null;
179
+ }
180
+ setStatus(label, phase) {
181
+ if (!this.config.sidebar)
182
+ return;
183
+ const visuals = phaseVisuals(phase);
184
+ this.runSync(this.appendWorkspace([
185
+ "set-status",
186
+ STATUS_KEY,
187
+ label,
188
+ "--icon",
189
+ visuals.icon,
190
+ "--color",
191
+ visuals.color,
192
+ ]));
193
+ }
194
+ clearStatus() {
195
+ if (!this.config.sidebar)
196
+ return;
197
+ this.runSync(this.appendWorkspace(["clear-status", STATUS_KEY]));
198
+ }
199
+ setProgress(progress) {
200
+ if (!this.config.sidebar)
201
+ return;
202
+ if (!progress) {
203
+ this.runSync(this.appendWorkspace(["clear-progress"]));
204
+ return;
205
+ }
206
+ this.runSync(this.appendWorkspace([
207
+ "set-progress",
208
+ progress.value.toFixed(3),
209
+ "--label",
210
+ progress.label,
211
+ ]));
212
+ }
213
+ log(message, level = "info", source = "gsd") {
214
+ if (!this.config.sidebar)
215
+ return;
216
+ this.runSync(this.appendWorkspace([
217
+ "log",
218
+ "--level",
219
+ level,
220
+ "--source",
221
+ source,
222
+ "--",
223
+ message,
224
+ ]));
225
+ }
226
+ notify(title, body, subtitle) {
227
+ if (!this.config.notifications)
228
+ return false;
229
+ const args = ["notify", "--title", title, "--body", body];
230
+ if (subtitle)
231
+ args.push("--subtitle", subtitle);
232
+ return this.runSync(args) !== null;
233
+ }
234
+ async listSurfaceIds() {
235
+ const stdout = await this.runAsync(this.appendWorkspace(["list-surfaces", "--json", "--id-format", "both"]));
236
+ const parsed = stdout ? parseJson(stdout) : null;
237
+ return extractSurfaceIds(parsed);
238
+ }
239
+ async createSplit(direction) {
240
+ if (!this.config.splits)
241
+ return null;
242
+ const before = new Set(await this.listSurfaceIds());
243
+ const args = ["new-split", direction];
244
+ const scopedArgs = this.appendSurface(this.appendWorkspace(args), this.config.surfaceId);
245
+ await this.runAsync(scopedArgs);
246
+ const after = await this.listSurfaceIds();
247
+ for (const id of after) {
248
+ if (!before.has(id))
249
+ return id;
250
+ }
251
+ return null;
252
+ }
253
+ async sendSurface(surfaceId, text) {
254
+ const payload = text.endsWith("\n") ? text : `${text}\n`;
255
+ const stdout = await this.runAsync(["send-surface", "--surface", surfaceId, payload]);
256
+ return stdout !== null;
257
+ }
258
+ }
259
+ export function syncCmuxSidebar(preferences, state) {
260
+ const client = CmuxClient.fromPreferences(preferences);
261
+ const config = client.getConfig();
262
+ if (!config.sidebar)
263
+ return;
264
+ const label = buildCmuxStatusLabel(state);
265
+ const progress = buildCmuxProgress(state);
266
+ const snapshot = JSON.stringify({ label, progress, phase: state.phase });
267
+ const key = sidebarSnapshotKey(config);
268
+ if (lastSidebarSnapshots.get(key) === snapshot)
269
+ return;
270
+ client.setStatus(label, state.phase);
271
+ client.setProgress(progress);
272
+ lastSidebarSnapshots.set(key, snapshot);
273
+ }
274
+ export function clearCmuxSidebar(preferences) {
275
+ const config = resolveCmuxConfig(preferences);
276
+ if (!config.available || !config.cliAvailable)
277
+ return;
278
+ const client = new CmuxClient({ ...config, enabled: true, sidebar: true });
279
+ const key = sidebarSnapshotKey(config);
280
+ client.clearStatus();
281
+ client.setProgress(null);
282
+ lastSidebarSnapshots.delete(key);
283
+ }
284
+ export function logCmuxEvent(preferences, message, level = "info") {
285
+ CmuxClient.fromPreferences(preferences).log(message, level);
286
+ }
287
+ export function shellEscape(value) {
288
+ return `'${value.replace(/'/g, `'\\''`)}'`;
289
+ }
290
+ function normalizeNotificationText(value) {
291
+ return value.replace(/\r?\n/g, " ").trim();
292
+ }
293
+ function parseJson(text) {
294
+ try {
295
+ return JSON.parse(text);
296
+ }
297
+ catch {
298
+ return null;
299
+ }
300
+ }
301
+ function extractSurfaceIds(value) {
302
+ const found = new Set();
303
+ const visit = (node) => {
304
+ if (Array.isArray(node)) {
305
+ for (const item of node)
306
+ visit(item);
307
+ return;
308
+ }
309
+ if (!node || typeof node !== "object")
310
+ return;
311
+ for (const [key, child] of Object.entries(node)) {
312
+ if (typeof child === "string"
313
+ && (key === "surface_id" || key === "surface" || (key === "id" && child.includes("surface")))) {
314
+ found.add(child);
315
+ }
316
+ visit(child);
317
+ }
318
+ };
319
+ visit(value);
320
+ return Array.from(found);
321
+ }