opengstack 0.13.10 → 0.14.2

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 (189) hide show
  1. package/AGENTS.md +4 -4
  2. package/CLAUDE.md +127 -110
  3. package/README.md +10 -5
  4. package/SKILL.md +500 -70
  5. package/bin/opengstack.js +69 -69
  6. package/{skills/land-and-deploy/SKILL.md → commands/autoplan.md} +7 -25
  7. package/{skills/benchmark/SKILL.md → commands/benchmark.md} +84 -108
  8. package/{skills/browse/SKILL.md → commands/browse.md} +60 -81
  9. package/{skills/ship/SKILL.md → commands/canary.md} +7 -27
  10. package/{skills/careful/SKILL.md → commands/careful.md} +2 -22
  11. package/{skills/canary/SKILL.md → commands/codex.md} +7 -26
  12. package/{skills/connect-chrome/SKILL.md → commands/connect-chrome.md} +7 -24
  13. package/commands/cso.md +70 -0
  14. package/commands/design-consultation.md +70 -0
  15. package/commands/design-review.md +70 -0
  16. package/commands/design-shotgun.md +70 -0
  17. package/commands/document-release.md +70 -0
  18. package/{skills/freeze/SKILL.md → commands/freeze.md} +3 -29
  19. package/{skills/guard/SKILL.md → commands/guard.md} +4 -35
  20. package/commands/investigate.md +70 -0
  21. package/commands/land-and-deploy.md +70 -0
  22. package/commands/office-hours.md +70 -0
  23. package/{skills/gstack-upgrade/SKILL.md → commands/opengstack-upgrade.md} +64 -79
  24. package/commands/plan-ceo-review.md +70 -0
  25. package/commands/plan-design-review.md +70 -0
  26. package/commands/plan-eng-review.md +70 -0
  27. package/commands/qa-only.md +70 -0
  28. package/commands/qa.md +70 -0
  29. package/commands/retro.md +70 -0
  30. package/commands/review.md +70 -0
  31. package/{skills/setup-browser-cookies/SKILL.md → commands/setup-browser-cookies.md} +22 -40
  32. package/commands/setup-deploy.md +70 -0
  33. package/commands/ship.md +70 -0
  34. package/commands/unfreeze.md +25 -0
  35. package/docs/designs/CHROME_VS_CHROMIUM_EXPLORATION.md +9 -9
  36. package/docs/designs/CONDUCTOR_CHROME_SIDEBAR_INTEGRATION.md +2 -2
  37. package/docs/designs/CONDUCTOR_SESSION_API.md +16 -16
  38. package/docs/designs/DESIGN_SHOTGUN.md +74 -74
  39. package/docs/designs/DESIGN_TOOLS_V1.md +111 -111
  40. package/docs/skills.md +483 -202
  41. package/package.json +42 -43
  42. package/scripts/analytics.ts +188 -0
  43. package/scripts/dev-skill.ts +83 -0
  44. package/scripts/discover-skills.ts +39 -0
  45. package/scripts/eval-compare.ts +97 -0
  46. package/scripts/eval-list.ts +117 -0
  47. package/scripts/eval-select.ts +86 -0
  48. package/scripts/eval-summary.ts +188 -0
  49. package/scripts/eval-watch.ts +172 -0
  50. package/scripts/gen-skill-docs.ts +473 -0
  51. package/scripts/resolvers/browse.ts +129 -0
  52. package/scripts/resolvers/codex-helpers.ts +133 -0
  53. package/scripts/resolvers/composition.ts +48 -0
  54. package/scripts/resolvers/confidence.ts +37 -0
  55. package/scripts/resolvers/constants.ts +50 -0
  56. package/scripts/resolvers/design.ts +950 -0
  57. package/scripts/resolvers/index.ts +59 -0
  58. package/scripts/resolvers/learnings.ts +96 -0
  59. package/scripts/resolvers/preamble.ts +505 -0
  60. package/scripts/resolvers/review.ts +884 -0
  61. package/scripts/resolvers/testing.ts +573 -0
  62. package/scripts/resolvers/types.ts +45 -0
  63. package/scripts/resolvers/utility.ts +421 -0
  64. package/scripts/skill-check.ts +190 -0
  65. package/scripts/cleanup.py +0 -100
  66. package/scripts/filter-skills.sh +0 -114
  67. package/scripts/filter_skills.py +0 -164
  68. package/scripts/install-skills.js +0 -60
  69. package/skills/autoplan/SKILL.md +0 -96
  70. package/skills/autoplan/SKILL.md.tmpl +0 -694
  71. package/skills/benchmark/SKILL.md.tmpl +0 -222
  72. package/skills/browse/SKILL.md.tmpl +0 -131
  73. package/skills/browse/bin/find-browse +0 -21
  74. package/skills/browse/bin/remote-slug +0 -14
  75. package/skills/browse/scripts/build-node-server.sh +0 -48
  76. package/skills/browse/src/activity.ts +0 -208
  77. package/skills/browse/src/browser-manager.ts +0 -959
  78. package/skills/browse/src/buffers.ts +0 -137
  79. package/skills/browse/src/bun-polyfill.cjs +0 -109
  80. package/skills/browse/src/cli.ts +0 -678
  81. package/skills/browse/src/commands.ts +0 -128
  82. package/skills/browse/src/config.ts +0 -150
  83. package/skills/browse/src/cookie-import-browser.ts +0 -625
  84. package/skills/browse/src/cookie-picker-routes.ts +0 -230
  85. package/skills/browse/src/cookie-picker-ui.ts +0 -688
  86. package/skills/browse/src/find-browse.ts +0 -61
  87. package/skills/browse/src/meta-commands.ts +0 -550
  88. package/skills/browse/src/platform.ts +0 -17
  89. package/skills/browse/src/read-commands.ts +0 -358
  90. package/skills/browse/src/server.ts +0 -1192
  91. package/skills/browse/src/sidebar-agent.ts +0 -280
  92. package/skills/browse/src/sidebar-utils.ts +0 -21
  93. package/skills/browse/src/snapshot.ts +0 -407
  94. package/skills/browse/src/url-validation.ts +0 -95
  95. package/skills/browse/src/write-commands.ts +0 -364
  96. package/skills/browse/test/activity.test.ts +0 -120
  97. package/skills/browse/test/adversarial-security.test.ts +0 -32
  98. package/skills/browse/test/browser-manager-unit.test.ts +0 -17
  99. package/skills/browse/test/bun-polyfill.test.ts +0 -72
  100. package/skills/browse/test/commands.test.ts +0 -2075
  101. package/skills/browse/test/compare-board.test.ts +0 -342
  102. package/skills/browse/test/config.test.ts +0 -316
  103. package/skills/browse/test/cookie-import-browser.test.ts +0 -519
  104. package/skills/browse/test/cookie-picker-routes.test.ts +0 -260
  105. package/skills/browse/test/file-drop.test.ts +0 -271
  106. package/skills/browse/test/find-browse.test.ts +0 -50
  107. package/skills/browse/test/findport.test.ts +0 -191
  108. package/skills/browse/test/fixtures/basic.html +0 -33
  109. package/skills/browse/test/fixtures/cursor-interactive.html +0 -22
  110. package/skills/browse/test/fixtures/dialog.html +0 -15
  111. package/skills/browse/test/fixtures/empty.html +0 -2
  112. package/skills/browse/test/fixtures/forms.html +0 -55
  113. package/skills/browse/test/fixtures/iframe.html +0 -30
  114. package/skills/browse/test/fixtures/network-idle.html +0 -30
  115. package/skills/browse/test/fixtures/qa-eval-checkout.html +0 -108
  116. package/skills/browse/test/fixtures/qa-eval-spa.html +0 -98
  117. package/skills/browse/test/fixtures/qa-eval.html +0 -51
  118. package/skills/browse/test/fixtures/responsive.html +0 -49
  119. package/skills/browse/test/fixtures/snapshot.html +0 -55
  120. package/skills/browse/test/fixtures/spa.html +0 -24
  121. package/skills/browse/test/fixtures/states.html +0 -17
  122. package/skills/browse/test/fixtures/upload.html +0 -25
  123. package/skills/browse/test/gstack-config.test.ts +0 -138
  124. package/skills/browse/test/gstack-update-check.test.ts +0 -514
  125. package/skills/browse/test/handoff.test.ts +0 -235
  126. package/skills/browse/test/path-validation.test.ts +0 -91
  127. package/skills/browse/test/platform.test.ts +0 -37
  128. package/skills/browse/test/server-auth.test.ts +0 -65
  129. package/skills/browse/test/sidebar-agent-roundtrip.test.ts +0 -226
  130. package/skills/browse/test/sidebar-agent.test.ts +0 -199
  131. package/skills/browse/test/sidebar-integration.test.ts +0 -320
  132. package/skills/browse/test/sidebar-unit.test.ts +0 -96
  133. package/skills/browse/test/snapshot.test.ts +0 -467
  134. package/skills/browse/test/state-ttl.test.ts +0 -35
  135. package/skills/browse/test/test-server.ts +0 -57
  136. package/skills/browse/test/url-validation.test.ts +0 -72
  137. package/skills/browse/test/watch.test.ts +0 -129
  138. package/skills/canary/SKILL.md.tmpl +0 -212
  139. package/skills/careful/SKILL.md.tmpl +0 -56
  140. package/skills/careful/bin/check-careful.sh +0 -112
  141. package/skills/codex/SKILL.md +0 -90
  142. package/skills/codex/SKILL.md.tmpl +0 -417
  143. package/skills/connect-chrome/SKILL.md.tmpl +0 -195
  144. package/skills/cso/ACKNOWLEDGEMENTS.md +0 -14
  145. package/skills/cso/SKILL.md +0 -93
  146. package/skills/cso/SKILL.md.tmpl +0 -606
  147. package/skills/design-consultation/SKILL.md +0 -94
  148. package/skills/design-consultation/SKILL.md.tmpl +0 -415
  149. package/skills/design-review/SKILL.md +0 -94
  150. package/skills/design-review/SKILL.md.tmpl +0 -290
  151. package/skills/design-shotgun/SKILL.md +0 -91
  152. package/skills/design-shotgun/SKILL.md.tmpl +0 -285
  153. package/skills/document-release/SKILL.md +0 -91
  154. package/skills/document-release/SKILL.md.tmpl +0 -359
  155. package/skills/freeze/SKILL.md.tmpl +0 -77
  156. package/skills/freeze/bin/check-freeze.sh +0 -79
  157. package/skills/gstack-upgrade/SKILL.md.tmpl +0 -222
  158. package/skills/guard/SKILL.md.tmpl +0 -77
  159. package/skills/investigate/SKILL.md +0 -105
  160. package/skills/investigate/SKILL.md.tmpl +0 -194
  161. package/skills/land-and-deploy/SKILL.md.tmpl +0 -881
  162. package/skills/office-hours/SKILL.md +0 -96
  163. package/skills/office-hours/SKILL.md.tmpl +0 -645
  164. package/skills/plan-ceo-review/SKILL.md +0 -94
  165. package/skills/plan-ceo-review/SKILL.md.tmpl +0 -811
  166. package/skills/plan-design-review/SKILL.md +0 -92
  167. package/skills/plan-design-review/SKILL.md.tmpl +0 -446
  168. package/skills/plan-eng-review/SKILL.md +0 -93
  169. package/skills/plan-eng-review/SKILL.md.tmpl +0 -303
  170. package/skills/qa/SKILL.md +0 -95
  171. package/skills/qa/SKILL.md.tmpl +0 -316
  172. package/skills/qa/references/issue-taxonomy.md +0 -85
  173. package/skills/qa/templates/qa-report-template.md +0 -126
  174. package/skills/qa-only/SKILL.md +0 -89
  175. package/skills/qa-only/SKILL.md.tmpl +0 -101
  176. package/skills/retro/SKILL.md +0 -89
  177. package/skills/retro/SKILL.md.tmpl +0 -820
  178. package/skills/review/SKILL.md +0 -92
  179. package/skills/review/SKILL.md.tmpl +0 -281
  180. package/skills/review/TODOS-format.md +0 -62
  181. package/skills/review/checklist.md +0 -220
  182. package/skills/review/design-checklist.md +0 -132
  183. package/skills/review/greptile-triage.md +0 -220
  184. package/skills/setup-browser-cookies/SKILL.md.tmpl +0 -81
  185. package/skills/setup-deploy/SKILL.md +0 -92
  186. package/skills/setup-deploy/SKILL.md.tmpl +0 -215
  187. package/skills/ship/SKILL.md.tmpl +0 -636
  188. package/skills/unfreeze/SKILL.md +0 -37
  189. package/skills/unfreeze/SKILL.md.tmpl +0 -36
@@ -1,61 +0,0 @@
1
- /**
2
- * find-browse — locate the gstack browse binary.
3
- *
4
- * Compiled to browse/dist/find-browse (standalone binary, no bun runtime needed).
5
- * Outputs the absolute path to the browse binary on stdout, or exits 1 if not found.
6
- */
7
-
8
- import { existsSync } from 'fs';
9
- import { join } from 'path';
10
- import { homedir } from 'os';
11
-
12
- // ─── Binary Discovery ───────────────────────────────────────────
13
-
14
- function getGitRoot(): string | null {
15
- try {
16
- const proc = Bun.spawnSync(['git', 'rev-parse', '--show-toplevel'], {
17
- stdout: 'pipe',
18
- stderr: 'pipe',
19
- });
20
- if (proc.exitCode !== 0) return null;
21
- return proc.stdout.toString().trim();
22
- } catch {
23
- return null;
24
- }
25
- }
26
-
27
- export function locateBinary(): string | null {
28
- const root = getGitRoot();
29
- const home = homedir();
30
- const markers = ['.codex', '.agents', '.claude'];
31
-
32
- // Workspace-local takes priority (for development)
33
- if (root) {
34
- for (const m of markers) {
35
- const local = join(root, m, 'skills', 'gstack', 'browse', 'dist', 'browse');
36
- if (existsSync(local)) return local;
37
- }
38
- }
39
-
40
- // Global fallback
41
- for (const m of markers) {
42
- const global = join(home, m, 'skills', 'gstack', 'browse', 'dist', 'browse');
43
- if (existsSync(global)) return global;
44
- }
45
-
46
- return null;
47
- }
48
-
49
- // ─── Main ───────────────────────────────────────────────────────
50
-
51
- function main() {
52
- const bin = locateBinary();
53
- if (!bin) {
54
- process.stderr.write('ERROR: browse binary not found. Run: cd <skill-dir> && ./setup\n');
55
- process.exit(1);
56
- }
57
-
58
- console.log(bin);
59
- }
60
-
61
- main();
@@ -1,550 +0,0 @@
1
- /**
2
- * Meta commands — tabs, server control, screenshots, chain, diff, snapshot
3
- */
4
-
5
- import type { BrowserManager } from './browser-manager';
6
- import { handleSnapshot } from './snapshot';
7
- import { getCleanText } from './read-commands';
8
- import { READ_COMMANDS, WRITE_COMMANDS, META_COMMANDS } from './commands';
9
- import { validateNavigationUrl } from './url-validation';
10
- import * as Diff from 'diff';
11
- import * as fs from 'fs';
12
- import * as path from 'path';
13
- import { TEMP_DIR, isPathWithin } from './platform';
14
- import { resolveConfig } from './config';
15
- import type { Frame } from 'playwright';
16
-
17
- // Security: Path validation to prevent path traversal attacks
18
- const SAFE_DIRECTORIES = [TEMP_DIR, process.cwd()];
19
-
20
- export function validateOutputPath(filePath: string): void {
21
- const resolved = path.resolve(filePath);
22
- const isSafe = SAFE_DIRECTORIES.some(dir => isPathWithin(resolved, dir));
23
- if (!isSafe) {
24
- throw new Error(`Path must be within: ${SAFE_DIRECTORIES.join(', ')}`);
25
- }
26
- }
27
-
28
- /** Tokenize a pipe segment respecting double-quoted strings. */
29
- function tokenizePipeSegment(segment: string): string[] {
30
- const tokens: string[] = [];
31
- let current = '';
32
- let inQuote = false;
33
- for (let i = 0; i < segment.length; i++) {
34
- const ch = segment[i];
35
- if (ch === '"') {
36
- inQuote = !inQuote;
37
- } else if (ch === ' ' && !inQuote) {
38
- if (current) { tokens.push(current); current = ''; }
39
- } else {
40
- current += ch;
41
- }
42
- }
43
- if (current) tokens.push(current);
44
- return tokens;
45
- }
46
-
47
- export async function handleMetaCommand(
48
- command: string,
49
- args: string[],
50
- bm: BrowserManager,
51
- shutdown: () => Promise<void> | void
52
- ): Promise<string> {
53
- switch (command) {
54
- // ─── Tabs ──────────────────────────────────────────
55
- case 'tabs': {
56
- const tabs = await bm.getTabListWithTitles();
57
- return tabs.map(t =>
58
- `${t.active ? '→ ' : ' '}[${t.id}] ${t.title || '(untitled)'} — ${t.url}`
59
- ).join('\n');
60
- }
61
-
62
- case 'tab': {
63
- const id = parseInt(args[0], 10);
64
- if (isNaN(id)) throw new Error('Usage: browse tab <id>');
65
- bm.switchTab(id);
66
- return `Switched to tab ${id}`;
67
- }
68
-
69
- case 'newtab': {
70
- const url = args[0];
71
- const id = await bm.newTab(url);
72
- return `Opened tab ${id}${url ? ` → ${url}` : ''}`;
73
- }
74
-
75
- case 'closetab': {
76
- const id = args[0] ? parseInt(args[0], 10) : undefined;
77
- await bm.closeTab(id);
78
- return `Closed tab${id ? ` ${id}` : ''}`;
79
- }
80
-
81
- // ─── Server Control ────────────────────────────────
82
- case 'status': {
83
- const page = bm.getPage();
84
- const tabs = bm.getTabCount();
85
- const mode = bm.getConnectionMode();
86
- return [
87
- `Status: healthy`,
88
- `Mode: ${mode}`,
89
- `URL: ${page.url()}`,
90
- `Tabs: ${tabs}`,
91
- `PID: ${process.pid}`,
92
- ].join('\n');
93
- }
94
-
95
- case 'url': {
96
- return bm.getCurrentUrl();
97
- }
98
-
99
- case 'stop': {
100
- await shutdown();
101
- return 'Server stopped';
102
- }
103
-
104
- case 'restart': {
105
- // Signal that we want a restart — the CLI will detect exit and restart
106
- console.log('[browse] Restart requested. Exiting for CLI to restart.');
107
- await shutdown();
108
- return 'Restarting...';
109
- }
110
-
111
- // ─── Visual ────────────────────────────────────────
112
- case 'screenshot': {
113
- // Parse priority: flags (--viewport, --clip) → selector (@ref, CSS) → output path
114
- const page = bm.getPage();
115
- let outputPath = `${TEMP_DIR}/browse-screenshot.png`;
116
- let clipRect: { x: number; y: number; width: number; height: number } | undefined;
117
- let targetSelector: string | undefined;
118
- let viewportOnly = false;
119
-
120
- const remaining: string[] = [];
121
- for (let i = 0; i < args.length; i++) {
122
- if (args[i] === '--viewport') {
123
- viewportOnly = true;
124
- } else if (args[i] === '--clip') {
125
- const coords = args[++i];
126
- if (!coords) throw new Error('Usage: screenshot --clip x,y,w,h [path]');
127
- const parts = coords.split(',').map(Number);
128
- if (parts.length !== 4 || parts.some(isNaN))
129
- throw new Error('Usage: screenshot --clip x,y,width,height — all must be numbers');
130
- clipRect = { x: parts[0], y: parts[1], width: parts[2], height: parts[3] };
131
- } else if (args[i].startsWith('--')) {
132
- throw new Error(`Unknown screenshot flag: ${args[i]}`);
133
- } else {
134
- remaining.push(args[i]);
135
- }
136
- }
137
-
138
- // Separate target (selector/@ref) from output path
139
- for (const arg of remaining) {
140
- // File paths containing / and ending with an image/pdf extension are never CSS selectors
141
- const isFilePath = arg.includes('/') && /\.(png|jpe?g|webp|pdf)$/i.test(arg);
142
- if (isFilePath) {
143
- outputPath = arg;
144
- } else if (arg.startsWith('@e') || arg.startsWith('@c') || arg.startsWith('.') || arg.startsWith('#') || arg.includes('[')) {
145
- targetSelector = arg;
146
- } else {
147
- outputPath = arg;
148
- }
149
- }
150
-
151
- validateOutputPath(outputPath);
152
-
153
- if (clipRect && targetSelector) {
154
- throw new Error('Cannot use --clip with a selector/ref — choose one');
155
- }
156
- if (viewportOnly && clipRect) {
157
- throw new Error('Cannot use --viewport with --clip — choose one');
158
- }
159
-
160
- if (targetSelector) {
161
- const resolved = await bm.resolveRef(targetSelector);
162
- const locator = 'locator' in resolved ? resolved.locator : page.locator(resolved.selector);
163
- await locator.screenshot({ path: outputPath, timeout: 5000 });
164
- return `Screenshot saved (element): ${outputPath}`;
165
- }
166
-
167
- if (clipRect) {
168
- await page.screenshot({ path: outputPath, clip: clipRect });
169
- return `Screenshot saved (clip ${clipRect.x},${clipRect.y},${clipRect.width},${clipRect.height}): ${outputPath}`;
170
- }
171
-
172
- await page.screenshot({ path: outputPath, fullPage: !viewportOnly });
173
- return `Screenshot saved${viewportOnly ? ' (viewport)' : ''}: ${outputPath}`;
174
- }
175
-
176
- case 'pdf': {
177
- const page = bm.getPage();
178
- const pdfPath = args[0] || `${TEMP_DIR}/browse-page.pdf`;
179
- validateOutputPath(pdfPath);
180
- await page.pdf({ path: pdfPath, format: 'A4' });
181
- return `PDF saved: ${pdfPath}`;
182
- }
183
-
184
- case 'responsive': {
185
- const page = bm.getPage();
186
- const prefix = args[0] || `${TEMP_DIR}/browse-responsive`;
187
- validateOutputPath(prefix);
188
- const viewports = [
189
- { name: 'mobile', width: 375, height: 812 },
190
- { name: 'tablet', width: 768, height: 1024 },
191
- { name: 'desktop', width: 1280, height: 720 },
192
- ];
193
- const originalViewport = page.viewportSize();
194
- const results: string[] = [];
195
-
196
- for (const vp of viewports) {
197
- await page.setViewportSize({ width: vp.width, height: vp.height });
198
- const path = `${prefix}-${vp.name}.png`;
199
- await page.screenshot({ path, fullPage: true });
200
- results.push(`${vp.name} (${vp.width}x${vp.height}): ${path}`);
201
- }
202
-
203
- // Restore original viewport
204
- if (originalViewport) {
205
- await page.setViewportSize(originalViewport);
206
- }
207
-
208
- return results.join('\n');
209
- }
210
-
211
- // ─── Chain ─────────────────────────────────────────
212
- case 'chain': {
213
- // Read JSON array from args[0] (if provided) or expect it was passed as body
214
- const jsonStr = args[0];
215
- if (!jsonStr) throw new Error(
216
- 'Usage: echo \'[["goto","url"],["text"]]\' | browse chain\n' +
217
- ' or: browse chain \'goto url | click @e5 | snapshot -ic\''
218
- );
219
-
220
- let commands: string[][];
221
- try {
222
- commands = JSON.parse(jsonStr);
223
- if (!Array.isArray(commands)) throw new Error('not array');
224
- } catch {
225
- // Fallback: pipe-delimited format "goto url | click @e5 | snapshot -ic"
226
- commands = jsonStr.split(' | ')
227
- .filter(seg => seg.trim().length > 0)
228
- .map(seg => tokenizePipeSegment(seg.trim()));
229
- }
230
-
231
- const results: string[] = [];
232
- const { handleReadCommand } = await import('./read-commands');
233
- const { handleWriteCommand } = await import('./write-commands');
234
-
235
- let lastWasWrite = false;
236
- for (const cmd of commands) {
237
- const [name, ...cmdArgs] = cmd;
238
- try {
239
- let result: string;
240
- if (WRITE_COMMANDS.has(name)) {
241
- result = await handleWriteCommand(name, cmdArgs, bm);
242
- lastWasWrite = true;
243
- } else if (READ_COMMANDS.has(name)) {
244
- result = await handleReadCommand(name, cmdArgs, bm);
245
- lastWasWrite = false;
246
- } else if (META_COMMANDS.has(name)) {
247
- result = await handleMetaCommand(name, cmdArgs, bm, shutdown);
248
- lastWasWrite = false;
249
- } else {
250
- throw new Error(`Unknown command: ${name}`);
251
- }
252
- results.push(`[${name}] ${result}`);
253
- } catch (err: any) {
254
- results.push(`[${name}] ERROR: ${err.message}`);
255
- }
256
- }
257
-
258
- // Wait for network to settle after write commands before returning
259
- if (lastWasWrite) {
260
- await bm.getPage().waitForLoadState('networkidle', { timeout: 2000 }).catch(() => {});
261
- }
262
-
263
- return results.join('\n\n');
264
- }
265
-
266
- // ─── Diff ──────────────────────────────────────────
267
- case 'diff': {
268
- const [url1, url2] = args;
269
- if (!url1 || !url2) throw new Error('Usage: browse diff <url1> <url2>');
270
-
271
- const page = bm.getPage();
272
- await validateNavigationUrl(url1);
273
- await page.goto(url1, { waitUntil: 'domcontentloaded', timeout: 15000 });
274
- const text1 = await getCleanText(page);
275
-
276
- await validateNavigationUrl(url2);
277
- await page.goto(url2, { waitUntil: 'domcontentloaded', timeout: 15000 });
278
- const text2 = await getCleanText(page);
279
-
280
- const changes = Diff.diffLines(text1, text2);
281
- const output: string[] = [`--- ${url1}`, `+++ ${url2}`, ''];
282
-
283
- for (const part of changes) {
284
- const prefix = part.added ? '+' : part.removed ? '-' : ' ';
285
- const lines = part.value.split('\n').filter(l => l.length > 0);
286
- for (const line of lines) {
287
- output.push(`${prefix} ${line}`);
288
- }
289
- }
290
-
291
- return output.join('\n');
292
- }
293
-
294
- // ─── Snapshot ─────────────────────────────────────
295
- case 'snapshot': {
296
- return await handleSnapshot(args, bm);
297
- }
298
-
299
- // ─── Handoff ────────────────────────────────────
300
- case 'handoff': {
301
- const message = args.join(' ') || 'User takeover requested';
302
- return await bm.handoff(message);
303
- }
304
-
305
- case 'resume': {
306
- bm.resume();
307
- // Re-snapshot to capture current page state after human interaction
308
- const snapshot = await handleSnapshot(['-i'], bm);
309
- return `RESUMED\n${snapshot}`;
310
- }
311
-
312
- // ─── Headed Mode ──────────────────────────────────────
313
- case 'connect': {
314
- // connect is handled as a pre-server command in cli.ts
315
- // If we get here, server is already running — tell the user
316
- if (bm.getConnectionMode() === 'headed') {
317
- return 'Already in headed mode with extension.';
318
- }
319
- return 'The connect command must be run from the CLI (not sent to a running server). Run: $B connect';
320
- }
321
-
322
- case 'disconnect': {
323
- if (bm.getConnectionMode() !== 'headed') {
324
- return 'Not in headed mode — nothing to disconnect.';
325
- }
326
- // Signal that we want a restart in headless mode
327
- console.log('[browse] Disconnecting headed browser. Restarting in headless mode.');
328
- await shutdown();
329
- return 'Disconnected. Server will restart in headless mode on next command.';
330
- }
331
-
332
- case 'focus': {
333
- if (bm.getConnectionMode() !== 'headed') {
334
- return 'focus requires headed mode. Run `$B connect` first.';
335
- }
336
- try {
337
- const { execSync } = await import('child_process');
338
- // Try common Chromium-based browser app names to bring to foreground
339
- const appNames = ['Comet', 'Google Chrome', 'Arc', 'Brave Browser', 'Microsoft Edge'];
340
- let activated = false;
341
- for (const appName of appNames) {
342
- try {
343
- execSync(`osascript -e 'tell application "${appName}" to activate'`, { stdio: 'pipe', timeout: 3000 });
344
- activated = true;
345
- break;
346
- } catch {
347
- // Try next browser
348
- }
349
- }
350
-
351
- if (!activated) {
352
- return 'Could not bring browser to foreground. macOS only.';
353
- }
354
-
355
- // If a ref was passed, scroll it into view
356
- if (args.length > 0 && args[0].startsWith('@')) {
357
- try {
358
- const resolved = await bm.resolveRef(args[0]);
359
- if ('locator' in resolved) {
360
- await resolved.locator.scrollIntoViewIfNeeded({ timeout: 5000 });
361
- return `Browser activated. Scrolled ${args[0]} into view.`;
362
- }
363
- } catch {
364
- // Ref not found — still activated the browser
365
- }
366
- }
367
-
368
- return 'Browser window activated.';
369
- } catch (err: any) {
370
- return `focus failed: ${err.message}. macOS only.`;
371
- }
372
- }
373
-
374
- // ─── Watch ──────────────────────────────────────────
375
- case 'watch': {
376
- if (args[0] === 'stop') {
377
- if (!bm.isWatching()) return 'Not currently watching.';
378
- const result = bm.stopWatch();
379
- const durationSec = Math.round(result.duration / 1000);
380
- return [
381
- `WATCH STOPPED (${durationSec}s, ${result.snapshots.length} snapshots)`,
382
- '',
383
- 'Last snapshot:',
384
- result.snapshots.length > 0 ? result.snapshots[result.snapshots.length - 1] : '(none)',
385
- ].join('\n');
386
- }
387
-
388
- if (bm.isWatching()) return 'Already watching. Run `$B watch stop` to stop.';
389
- if (bm.getConnectionMode() !== 'headed') {
390
- return 'watch requires headed mode. Run `$B connect` first.';
391
- }
392
-
393
- bm.startWatch();
394
- return 'WATCHING — observing user browsing. Periodic snapshots every 5s.\nRun `$B watch stop` to stop and get summary.';
395
- }
396
-
397
- // ─── Inbox ──────────────────────────────────────────
398
- case 'inbox': {
399
- const { execSync } = await import('child_process');
400
- let gitRoot: string;
401
- try {
402
- gitRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
403
- } catch {
404
- return 'Not in a git repository — cannot locate inbox.';
405
- }
406
-
407
- const inboxDir = path.join(gitRoot, '.context', 'sidebar-inbox');
408
- if (!fs.existsSync(inboxDir)) return 'Inbox empty.';
409
-
410
- const files = fs.readdirSync(inboxDir)
411
- .filter(f => f.endsWith('.json') && !f.startsWith('.'))
412
- .sort()
413
- .reverse(); // newest first
414
-
415
- if (files.length === 0) return 'Inbox empty.';
416
-
417
- const messages: { timestamp: string; url: string; userMessage: string }[] = [];
418
- for (const file of files) {
419
- try {
420
- const data = JSON.parse(fs.readFileSync(path.join(inboxDir, file), 'utf-8'));
421
- messages.push({
422
- timestamp: data.timestamp || '',
423
- url: data.page?.url || 'unknown',
424
- userMessage: data.userMessage || '',
425
- });
426
- } catch {
427
- // Skip malformed files
428
- }
429
- }
430
-
431
- if (messages.length === 0) return 'Inbox empty.';
432
-
433
- const lines: string[] = [];
434
- lines.push(`SIDEBAR INBOX (${messages.length} message${messages.length === 1 ? '' : 's'})`);
435
- lines.push('────────────────────────────────');
436
-
437
- for (const msg of messages) {
438
- const ts = msg.timestamp ? `[${msg.timestamp}]` : '[unknown]';
439
- lines.push(`${ts} ${msg.url}`);
440
- lines.push(` "${msg.userMessage}"`);
441
- lines.push('');
442
- }
443
-
444
- lines.push('────────────────────────────────');
445
-
446
- // Handle --clear flag
447
- if (args.includes('--clear')) {
448
- for (const file of files) {
449
- try { fs.unlinkSync(path.join(inboxDir, file)); } catch {}
450
- }
451
- lines.push(`Cleared ${files.length} message${files.length === 1 ? '' : 's'}.`);
452
- }
453
-
454
- return lines.join('\n');
455
- }
456
-
457
- // ─── State ────────────────────────────────────────
458
- case 'state': {
459
- const [action, name] = args;
460
- if (!action || !name) throw new Error('Usage: state save|load <name>');
461
-
462
- // Sanitize name: alphanumeric + hyphens + underscores only
463
- if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
464
- throw new Error('State name must be alphanumeric (a-z, 0-9, _, -)');
465
- }
466
-
467
- const config = resolveConfig();
468
- const stateDir = path.join(config.stateDir, 'browse-states');
469
- fs.mkdirSync(stateDir, { recursive: true });
470
- const statePath = path.join(stateDir, `${name}.json`);
471
-
472
- if (action === 'save') {
473
- const state = await bm.saveState();
474
- // V1: cookies + URLs only (not localStorage — breaks on load-before-navigate)
475
- const saveData = {
476
- version: 1,
477
- savedAt: new Date().toISOString(),
478
- cookies: state.cookies,
479
- pages: state.pages.map(p => ({ url: p.url, isActive: p.isActive })),
480
- };
481
- fs.writeFileSync(statePath, JSON.stringify(saveData, null, 2), { mode: 0o600 });
482
- return `State saved: ${statePath} (${state.cookies.length} cookies, ${state.pages.length} pages)\n⚠️ Cookies stored in plaintext. Delete when no longer needed.`;
483
- }
484
-
485
- if (action === 'load') {
486
- if (!fs.existsSync(statePath)) throw new Error(`State not found: ${statePath}`);
487
- const data = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
488
- if (!Array.isArray(data.cookies) || !Array.isArray(data.pages)) {
489
- throw new Error('Invalid state file: expected cookies and pages arrays');
490
- }
491
- // Warn on state files older than 7 days
492
- if (data.savedAt) {
493
- const ageMs = Date.now() - new Date(data.savedAt).getTime();
494
- const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000;
495
- if (ageMs > SEVEN_DAYS) {
496
- console.warn(`[browse] Warning: State file is ${Math.round(ageMs / 86400000)} days old. Consider re-saving.`);
497
- }
498
- }
499
- // Close existing pages, then restore (replace, not merge)
500
- bm.setFrame(null);
501
- await bm.closeAllPages();
502
- await bm.restoreState({
503
- cookies: data.cookies,
504
- pages: data.pages.map((p: any) => ({ ...p, storage: null })),
505
- });
506
- return `State loaded: ${data.cookies.length} cookies, ${data.pages.length} pages`;
507
- }
508
-
509
- throw new Error('Usage: state save|load <name>');
510
- }
511
-
512
- // ─── Frame ───────────────────────────────────────
513
- case 'frame': {
514
- const target = args[0];
515
- if (!target) throw new Error('Usage: frame <selector|@ref|--name name|--url pattern|main>');
516
-
517
- if (target === 'main') {
518
- bm.setFrame(null);
519
- bm.clearRefs();
520
- return 'Switched to main frame';
521
- }
522
-
523
- const page = bm.getPage();
524
- let frame: Frame | null = null;
525
-
526
- if (target === '--name') {
527
- if (!args[1]) throw new Error('Usage: frame --name <name>');
528
- frame = page.frame({ name: args[1] });
529
- } else if (target === '--url') {
530
- if (!args[1]) throw new Error('Usage: frame --url <pattern>');
531
- frame = page.frame({ url: new RegExp(args[1]) });
532
- } else {
533
- // CSS selector or @ref for the iframe element
534
- const resolved = await bm.resolveRef(target);
535
- const locator = 'locator' in resolved ? resolved.locator : page.locator(resolved.selector);
536
- const elementHandle = await locator.elementHandle({ timeout: 5000 });
537
- frame = await elementHandle?.contentFrame() ?? null;
538
- await elementHandle?.dispose();
539
- }
540
-
541
- if (!frame) throw new Error(`Frame not found: ${target}`);
542
- bm.setFrame(frame);
543
- bm.clearRefs();
544
- return `Switched to frame: ${frame.url()}`;
545
- }
546
-
547
- default:
548
- throw new Error(`Unknown meta command: ${command}`);
549
- }
550
- }
@@ -1,17 +0,0 @@
1
- /**
2
- * Cross-platform constants for gstack browse.
3
- *
4
- * On macOS/Linux: TEMP_DIR = '/tmp', path.sep = '/' — identical to hardcoded values.
5
- * On Windows: TEMP_DIR = os.tmpdir(), path.sep = '\\' — correct Windows behavior.
6
- */
7
-
8
- import * as os from 'os';
9
- import * as path from 'path';
10
-
11
- export const IS_WINDOWS = process.platform === 'win32';
12
- export const TEMP_DIR = IS_WINDOWS ? os.tmpdir() : '/tmp';
13
-
14
- /** Check if resolvedPath is within dir, using platform-aware separators. */
15
- export function isPathWithin(resolvedPath: string, dir: string): boolean {
16
- return resolvedPath === dir || resolvedPath.startsWith(dir + path.sep);
17
- }