@aholbreich/agent-skills 0.8.0 → 1.0.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ (empty)
6
+
7
+ ## 0.10.0 - 2026-05-08
8
+
9
+ Added:
10
+
11
+ - New `jira-update` skill for dry-run-first Jira Cloud writes through an authenticated browser session: `create`, `comment`, `transition`, `update-fields`, and `link` commands. Markdown-to-ADF conversion by default; ADF passthrough as escape hatch.
12
+
13
+ Changed:
14
+
15
+ - Extracted browser/CDP/cookie helpers from all four existing skills into a single source-of-truth `lib/atlassian-browser.js`. Vendored at pack time into each `skills/*/scripts/atlassian-browser.js` so each skill folder remains self-contained on disk. Eliminates ~870 lines of duplicated code across the bundle.
16
+
17
+ ## 0.9.0 - 2026-05-07
18
+
19
+ Added:
20
+
21
+ - New `bitbucket-browser-fetch` skill for browser-authenticated Bitbucket Cloud project repository inventory, SSH/HTTPS clone URL lists, Markdown summaries, and safe clone helper scripts.
22
+
3
23
  ## 0.8.0 - 2026-05-07
4
24
 
5
25
  Added:
package/README.md CHANGED
@@ -2,15 +2,17 @@
2
2
 
3
3
  Handcrafted [Agent Skills](https://agentskills.io/) for developer and LLM-wiki workflows. The package is intentionally a pure skills package with broad compatibility across Pi, Claude Code, Codex, OpenClaw/generic `.agents` setups, and other Agent Skills-compatible harnesses.
4
4
 
5
- This repository is a pure skills package. It currently contains browser-authenticated Atlassian fetch/update tools that work well when Jira/Confluence API-token authentication is unavailable because an organization uses Microsoft/SSO.
5
+ This repository is a pure skills package. It currently contains browser-authenticated Atlassian fetch and update tools (Jira read+write, Confluence read+write, Bitbucket read) that work well when Jira/Confluence/Bitbucket API-token authentication is unavailable because an organization uses Microsoft/SSO.
6
6
 
7
7
  ## Skills
8
8
 
9
9
  | Skill | Purpose |
10
10
  |---|---|
11
11
  | [`jira-browser-fetch`](skills/jira-browser-fetch/) | Fetch Jira issue JSON, rendered HTML/XML, linked/referenced issues, Jira Software board backlogs, JQL result sets, and attachments through an authenticated Chrome session. |
12
+ | [`jira-update`](skills/jira-update/) | Dry-run-first Jira Cloud writes through an authenticated browser session: create issues, add comments, transition workflows, update fields, and link issues. Markdown-to-ADF conversion by default; ADF passthrough as escape hatch. |
12
13
  | [`confluence-browser-fetch`](skills/confluence-browser-fetch/) | Fetch Confluence page JSON, storage/view HTML, browser HTML, descendants, CQL result sets, and attachments through an authenticated Chrome session. |
13
14
  | [`confluence-update`](skills/confluence-update/) | Dry-run-first Confluence page updates, agent-owned block replacement, Markdown-to-storage conversion, and page creation through an authenticated browser session. |
15
+ | [`bitbucket-browser-fetch`](skills/bitbucket-browser-fetch/) | Fetch Bitbucket Cloud project repository inventories and clone URL lists through an authenticated browser session. |
14
16
 
15
17
  ## Compatibility
16
18
 
@@ -175,8 +177,10 @@ If installed globally via npm, the package exposes:
175
177
  ```bash
176
178
  agent-skills
177
179
  jira-browser-fetch
180
+ jira-update
178
181
  confluence-browser-fetch
179
182
  confluence-update
183
+ bitbucket-browser-fetch
180
184
  ```
181
185
 
182
186
  ## Reuse one Atlassian browser login
@@ -188,7 +192,17 @@ export ATLASSIAN_CHROME_PROFILE="$HOME/.local/share/atlassian-browser-fetch-chro
188
192
  export ATLASSIAN_CHROME_DEBUG_PORT=9223
189
193
  ```
190
194
 
191
- Skill-specific variables such as `JIRA_CHROME_PROFILE` or `CONFLUENCE_CHROME_PROFILE` still override the shared profile when needed.
195
+ Skill-specific variables such as `JIRA_CHROME_PROFILE`, `CONFLUENCE_CHROME_PROFILE`, or `BITBUCKET_CHROME_PROFILE` still override the shared profile when needed.
196
+
197
+ ## Bitbucket examples
198
+
199
+ Fetch all repositories in a Bitbucket project and write SSH clone URL lists:
200
+
201
+ ```bash
202
+ bitbucket-browser-fetch \
203
+ 'https://bitbucket.org/myneva/workspace/projects/SWI' \
204
+ --raw-dir ./raw
205
+ ```
192
206
 
193
207
  ## Confluence update examples
194
208
 
@@ -269,6 +283,54 @@ Example user requests that should invoke this skill:
269
283
  - "Pull my assigned Jira issues without asking me for an API token."
270
284
  - "Use this JQL and store the raw Jira evidence under the wiki raw folder."
271
285
 
286
+ ## Jira update examples
287
+
288
+ Dry-run an issue creation from a manifest:
289
+
290
+ ```bash
291
+ jira-update create \
292
+ --server https://example.atlassian.net \
293
+ --file ./new-bug.json
294
+ ```
295
+
296
+ Apply after review:
297
+
298
+ ```bash
299
+ jira-update create \
300
+ --server https://example.atlassian.net \
301
+ --file ./new-bug.json \
302
+ --apply
303
+ ```
304
+
305
+ Add a comment from Markdown:
306
+
307
+ ```bash
308
+ jira-update comment PROJ-123 \
309
+ --server https://example.atlassian.net \
310
+ --file ./reply.md \
311
+ --apply
312
+ ```
313
+
314
+ Transition with a comment:
315
+
316
+ ```bash
317
+ jira-update transition PROJ-123 \
318
+ --server https://example.atlassian.net \
319
+ --to "In Progress" \
320
+ --comment-file ./status.md \
321
+ --apply
322
+ ```
323
+
324
+ Link two issues:
325
+
326
+ ```bash
327
+ jira-update link PROJ-123 \
328
+ --server https://example.atlassian.net \
329
+ --to PROJ-456 \
330
+ --type blocks \
331
+ --apply
332
+ ```
333
+
272
334
  ## Confluence examples
273
335
 
274
336
  Fetch one page by URL:
package/SECURITY.md CHANGED
@@ -2,16 +2,16 @@
2
2
 
3
3
  ## Read this before installing or running
4
4
 
5
- These skills are local automation tools. They can fetch potentially sensitive Jira and Confluence data into your filesystem, and `confluence-update` can write to Confluence when explicitly run with `--apply`.
5
+ These skills are local automation tools. They can fetch potentially sensitive Jira, Confluence, and Bitbucket data into your filesystem, and `confluence-update` can write to Confluence when explicitly run with `--apply`.
6
6
 
7
7
  ## Browser authentication model
8
8
 
9
- The Jira and Confluence browser tools:
9
+ The Jira, Confluence, and Bitbucket browser tools:
10
10
 
11
11
  1. launch or reuse a Chromium-compatible browser with a dedicated local profile,
12
12
  2. let you complete normal Atlassian SSO in the browser,
13
13
  3. read Atlassian cookies through the local Chrome DevTools protocol,
14
- 4. verify those cookies represent an authenticated Jira/Confluence REST session,
14
+ 4. verify those cookies represent an authenticated Jira/Confluence/Bitbucket session,
15
15
  5. call Atlassian REST endpoints with those cookies.
16
16
 
17
17
  They do **not** require you to paste API tokens or cookies into chat.
@@ -20,7 +20,7 @@ They do **not** require you to paste API tokens or cookies into chat.
20
20
 
21
21
  - Do not paste Atlassian cookies, API tokens, passwords, or session headers into prompts, issues, logs, or commits.
22
22
  - Treat everything under `raw/` as confidential unless you know it is public.
23
- - Do not commit fetched Jira/Confluence exports, update audit files, or attachments to a public repository.
23
+ - Do not commit fetched Jira/Confluence/Bitbucket exports, update audit files, clone URL lists, or attachments to a public repository.
24
24
  - Review generated `attachments.json` manifests before sharing; they may contain private URLs and filenames.
25
25
  - Chrome remote debugging is configured for `127.0.0.1`; do not expose it to a network interface.
26
26
  - Use dedicated browser profiles for fetch automation. If reusing SSO between Jira and Confluence, share only a dedicated automation profile via `ATLASSIAN_CHROME_PROFILE`, not your everyday browser profile.
package/bin/vendor.js ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+
7
+ const repoRoot = path.resolve(__dirname, '..');
8
+ const source = path.join(repoRoot, 'lib/atlassian-browser.js');
9
+ const skillsDir = path.join(repoRoot, 'skills');
10
+
11
+ if (!fs.existsSync(source)) {
12
+ console.error(`vendor: source not found at ${source}`);
13
+ process.exit(1);
14
+ }
15
+
16
+ const content = fs.readFileSync(source);
17
+ const skills = fs.readdirSync(skillsDir, { withFileTypes: true }).filter(d => d.isDirectory());
18
+ for (const skill of skills) {
19
+ const scriptsDir = path.join(skillsDir, skill.name, 'scripts');
20
+ if (!fs.existsSync(scriptsDir)) continue;
21
+ const dest = path.join(scriptsDir, 'atlassian-browser.js');
22
+ fs.writeFileSync(dest, content);
23
+ console.log(`vendored -> ${path.relative(repoRoot, dest)}`);
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aholbreich/agent-skills",
3
- "version": "0.8.0",
3
+ "version": "1.0.0",
4
4
  "description": "Handcrafted Agent Skills for browser-authenticated Jira and Confluence ingestion, LLM wiki workflows, and developer automation.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -38,10 +38,14 @@
38
38
  "agent-skills": "bin/agent-skills.js",
39
39
  "jira-browser-fetch": "skills/jira-browser-fetch/scripts/jira-browser-fetch.js",
40
40
  "confluence-browser-fetch": "skills/confluence-browser-fetch/scripts/confluence-browser-fetch.js",
41
- "confluence-update": "skills/confluence-update/scripts/confluence-update.js"
41
+ "confluence-update": "skills/confluence-update/scripts/confluence-update.js",
42
+ "bitbucket-browser-fetch": "skills/bitbucket-browser-fetch/scripts/bitbucket-browser-fetch.js",
43
+ "jira-update": "skills/jira-update/scripts/jira-update.js"
42
44
  },
43
45
  "scripts": {
44
- "check": "node --check bin/agent-skills.js && node --check skills/jira-browser-fetch/scripts/jira-browser-fetch.js && node --check skills/jira-browser-fetch/scripts/lib.js && node --check skills/confluence-browser-fetch/scripts/confluence-browser-fetch.js && node --check skills/confluence-browser-fetch/scripts/lib.js && node --check skills/confluence-update/scripts/confluence-update.js && node --check skills/confluence-update/scripts/lib.js",
46
+ "vendor": "node bin/vendor.js",
47
+ "check": "node --check bin/agent-skills.js && node --check bin/vendor.js && node --check lib/atlassian-browser.js && npm run vendor && node --check skills/jira-browser-fetch/scripts/jira-browser-fetch.js && node --check skills/jira-browser-fetch/scripts/lib.js && node --check skills/jira-browser-fetch/scripts/atlassian-browser.js && node --check skills/confluence-browser-fetch/scripts/confluence-browser-fetch.js && node --check skills/confluence-browser-fetch/scripts/lib.js && node --check skills/confluence-browser-fetch/scripts/atlassian-browser.js && node --check skills/confluence-update/scripts/confluence-update.js && node --check skills/confluence-update/scripts/lib.js && node --check skills/confluence-update/scripts/atlassian-browser.js && node --check skills/bitbucket-browser-fetch/scripts/bitbucket-browser-fetch.js && node --check skills/bitbucket-browser-fetch/scripts/lib.js && node --check skills/bitbucket-browser-fetch/scripts/atlassian-browser.js && node --check skills/jira-update/scripts/jira-update.js && node --check skills/jira-update/scripts/lib.js && node --check skills/jira-update/scripts/atlassian-browser.js",
48
+ "pretest": "npm run vendor",
45
49
  "test": "node --test",
46
50
  "ci": "npm run check && npm test && npm pack --dry-run",
47
51
  "pack:dry": "npm pack --dry-run",
@@ -0,0 +1,66 @@
1
+ ---
2
+ name: bitbucket-browser-fetch
3
+ description: Fetch Bitbucket Cloud project repository inventory through an authenticated browser session when API tokens/app passwords are unavailable, especially with Atlassian SSO. Use to list repositories in a Bitbucket workspace project and produce JSON, Markdown, SSH clone URL lists, HTTPS clone URL lists, and a safe clone helper script.
4
+ license: MIT
5
+ compatibility: Agent Skills standard. Tested with Pi; installable into Claude Code, Codex, OpenClaw/generic .agents skills directories. Requires Node.js 22+ with built-in fetch/WebSocket and a Chromium-compatible browser with remote debugging (Chrome, Chromium, Brave, Edge, or Vivaldi). No npm dependencies.
6
+ ---
7
+
8
+ # Bitbucket Browser Fetch
9
+
10
+ Use this skill when a user wants all repositories in a Bitbucket Cloud project inventoried through an authenticated browser session. This is useful when Bitbucket app passwords/API tokens are unavailable or inconvenient because the organization uses Atlassian SSO.
11
+
12
+ The script opens/reuses Chrome with a dedicated profile, lets the user complete Bitbucket login once, extracts Bitbucket cookies via Chrome DevTools, verifies project access, and fetches the repository list using Bitbucket's browser/internal API.
13
+
14
+ ## Safety
15
+
16
+ - Never ask the user to paste Bitbucket cookies, app passwords, or API tokens into chat.
17
+ - The skill is read-only and does not clone repositories itself.
18
+ - It writes clone URL lists and a helper script; review before executing any clone script.
19
+ - Treat repository names/URLs as potentially confidential.
20
+
21
+ ## Script
22
+
23
+ ```bash
24
+ scripts/bitbucket-browser-fetch.js <PROJECT_URL> [options]
25
+ ```
26
+
27
+ Important options:
28
+
29
+ ```bash
30
+ --workspace NAME override workspace parsed from URL
31
+ --project KEY override project key parsed from URL
32
+ --raw-dir DIR output raw directory
33
+ --pagelen N internal API page size, default 100
34
+ --wait SEC SSO/session wait timeout
35
+ --port PORT Chrome DevTools port
36
+ --profile-dir DIR Chrome profile dir
37
+ ```
38
+
39
+ ## Example
40
+
41
+ ```bash
42
+ scripts/bitbucket-browser-fetch.js \
43
+ 'https://bitbucket.org/myneva/workspace/projects/SWI' \
44
+ --raw-dir ./raw
45
+ ```
46
+
47
+ ## Output
48
+
49
+ ```text
50
+ raw/bitbucket/<workspace>/projects/<project-key>/
51
+ ├── repositories.json
52
+ ├── repositories.md
53
+ ├── clone-ssh.txt
54
+ ├── clone-https.txt
55
+ ├── clone-ssh.sh
56
+ ├── bitbucket-browser-fetch-run.json
57
+ └── pages/
58
+ └── repositories-page-1.json
59
+ ```
60
+
61
+ Agents should normally use `repositories.json` for metadata and `clone-ssh.txt` for selective checkout with normal Git SSH credentials.
62
+
63
+ ## References
64
+
65
+ - [Usage reference](references/usage.md)
66
+ - [Distribution guide](references/distribution.md)
@@ -0,0 +1,25 @@
1
+ # Bitbucket Browser Fetch Distribution
2
+
3
+ This skill is distributed as part of `@aholbreich/agent-skills`.
4
+
5
+ Directory layout:
6
+
7
+ ```text
8
+ bitbucket-browser-fetch/
9
+ ├── SKILL.md
10
+ ├── references/
11
+ │ ├── distribution.md
12
+ │ └── usage.md
13
+ └── scripts/
14
+ ├── bitbucket-browser-fetch.js
15
+ └── lib.js
16
+ ```
17
+
18
+ Use directly by path or install a convenience symlink:
19
+
20
+ ```bash
21
+ mkdir -p ~/.local/bin
22
+ ln -sf ~/.pi/agent/skills/bitbucket-browser-fetch/scripts/bitbucket-browser-fetch.js ~/.local/bin/bitbucket-browser-fetch
23
+ ```
24
+
25
+ The package exposes a `bitbucket-browser-fetch` npm bin when installed globally.
@@ -0,0 +1,84 @@
1
+ # Bitbucket Browser Fetch Usage
2
+
3
+ ## Why Browser Fetch?
4
+
5
+ Bitbucket Cloud organizations often rely on Atlassian SSO. Browser fetch avoids pasted secrets by:
6
+
7
+ 1. Launching/reusing a dedicated Chromium-compatible browser profile.
8
+ 2. Letting the user complete normal Bitbucket/Atlassian login in the browser.
9
+ 3. Reading Bitbucket cookies through local Chrome DevTools.
10
+ 4. Verifying access to the requested Bitbucket project.
11
+ 5. Calling Bitbucket's browser/internal API to list project repositories.
12
+
13
+ The official `api.bitbucket.org/2.0` API does not reliably accept browser cookies, so this skill uses the same internal API the Bitbucket UI uses for project repository lists.
14
+
15
+ ## Common Command
16
+
17
+ ```bash
18
+ scripts/bitbucket-browser-fetch.js \
19
+ 'https://bitbucket.org/myneva/workspace/projects/SWI' \
20
+ --raw-dir ./raw
21
+ ```
22
+
23
+ ## Output Files
24
+
25
+ For `https://bitbucket.org/myneva/workspace/projects/SWI`:
26
+
27
+ ```text
28
+ raw/bitbucket/myneva/projects/SWI/
29
+ ├── repositories.json # normalized machine-readable inventory
30
+ ├── repositories.md # human/LLM-friendly table
31
+ ├── clone-ssh.txt # one SSH git clone URL per line
32
+ ├── clone-https.txt # one HTTPS git clone URL per line
33
+ ├── clone-ssh.sh # safe helper script; not executed automatically
34
+ ├── bitbucket-browser-fetch-run.json
35
+ └── pages/
36
+ ├── repositories-page-1.json # raw Bitbucket internal API responses
37
+ └── repositories-page-2.json
38
+ ```
39
+
40
+ ## Agent Checkout Workflow
41
+
42
+ The skill does not clone automatically. After reviewing the output, agents can selectively clone with normal Git SSH credentials:
43
+
44
+ ```bash
45
+ mkdir -p repos
46
+ while read -r url; do
47
+ name="$(basename "$url" .git)"
48
+ [ -d "repos/$name/.git" ] && echo "SKIP $name" && continue
49
+ git clone "$url" "repos/$name"
50
+ done < raw/bitbucket/myneva/projects/SWI/clone-ssh.txt
51
+ ```
52
+
53
+ Or run the generated helper script after review:
54
+
55
+ ```bash
56
+ raw/bitbucket/myneva/projects/SWI/clone-ssh.sh repos
57
+ ```
58
+
59
+ ## Environment Variables
60
+
61
+ | Variable | Meaning |
62
+ |---|---|
63
+ | `BITBUCKET_RAW_DIR` | Default output raw directory |
64
+ | `BITBUCKET_CHROME_DEBUG_PORT` | Chrome DevTools port; overrides `ATLASSIAN_CHROME_DEBUG_PORT` |
65
+ | `ATLASSIAN_CHROME_DEBUG_PORT` | Shared DevTools port for Atlassian browser tools |
66
+ | `BITBUCKET_CHROME_PROFILE` | Dedicated Chrome profile dir; overrides `ATLASSIAN_CHROME_PROFILE` |
67
+ | `ATLASSIAN_CHROME_PROFILE` | Shared browser profile dir for Atlassian tools |
68
+ | `BITBUCKET_FETCH_WAIT_SEC` | Wait timeout, default `900` |
69
+ | `BITBUCKET_PAGELEN` | Internal API page size, default `100` |
70
+ | `CHROME` / `CHROMIUM` | Browser executable path override |
71
+
72
+ ## Troubleshooting
73
+
74
+ ### Project not found / no access
75
+
76
+ Complete Bitbucket/Atlassian login in the opened browser and confirm you can view the project URL manually.
77
+
78
+ ### Official API returns empty but UI shows repositories
79
+
80
+ Expected in SSO/browser-cookie mode. This skill intentionally uses Bitbucket's browser/internal API.
81
+
82
+ ### Git clone fails
83
+
84
+ Browser authentication and Git authentication are separate. Configure SSH keys or Git credentials for Bitbucket before cloning.
@@ -0,0 +1,261 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { spawn } = require('node:child_process');
6
+
7
+ const sleep = ms => new Promise(r => setTimeout(r, ms));
8
+
9
+ function isExecutable(file) {
10
+ try { fs.accessSync(file, fs.constants.X_OK); return true; } catch { return false; }
11
+ }
12
+
13
+ function resolveBrowserCandidate(candidate) {
14
+ if (!candidate) return null;
15
+ if (candidate.includes(path.sep)) return isExecutable(candidate) ? candidate : null;
16
+ for (const dir of String(process.env.PATH || '').split(path.delimiter)) {
17
+ if (!dir) continue;
18
+ const full = path.join(dir, candidate);
19
+ if (isExecutable(full)) return full;
20
+ }
21
+ return null;
22
+ }
23
+
24
+ function findBrowserExecutable() {
25
+ const candidates = [
26
+ process.env.CHROME,
27
+ process.env.CHROMIUM,
28
+ 'google-chrome',
29
+ 'google-chrome-stable',
30
+ 'chromium',
31
+ 'chromium-browser',
32
+ 'brave-browser',
33
+ 'brave',
34
+ 'microsoft-edge',
35
+ 'microsoft-edge-stable',
36
+ 'vivaldi',
37
+ 'vivaldi-stable',
38
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
39
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
40
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
41
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
42
+ '/Applications/Vivaldi.app/Contents/MacOS/Vivaldi',
43
+ ];
44
+ for (const candidate of candidates) {
45
+ const resolved = resolveBrowserCandidate(candidate);
46
+ if (resolved) return resolved;
47
+ }
48
+ throw new Error('Could not find a Chromium-compatible browser. Install Chrome/Chromium/Brave/Edge/Vivaldi or set CHROME=/path/to/browser.');
49
+ }
50
+
51
+ function connectCdp(wsUrl) {
52
+ return new Promise((resolve, reject) => {
53
+ const ws = new WebSocket(wsUrl);
54
+ let id = 0;
55
+ const pending = new Map();
56
+ const failTimer = setTimeout(() => reject(new Error('CDP websocket timeout')), 10000);
57
+
58
+ ws.addEventListener('open', () => {
59
+ clearTimeout(failTimer);
60
+ resolve({
61
+ send(method, params = {}) {
62
+ return new Promise((res, rej) => {
63
+ const msgId = ++id;
64
+ pending.set(msgId, { res, rej });
65
+ ws.send(JSON.stringify({ id: msgId, method, params }));
66
+ });
67
+ },
68
+ close() { try { ws.close(); } catch {} },
69
+ });
70
+ });
71
+
72
+ ws.addEventListener('message', ev => {
73
+ let data = ev.data;
74
+ if (typeof data !== 'string') data = Buffer.from(data).toString('utf8');
75
+ const msg = JSON.parse(data);
76
+ if (!msg.id || !pending.has(msg.id)) return;
77
+ const { res, rej } = pending.get(msg.id);
78
+ pending.delete(msg.id);
79
+ if (msg.error) rej(new Error(`${msg.error.message || 'CDP error'} ${JSON.stringify(msg.error)}`));
80
+ else res(msg.result);
81
+ });
82
+
83
+ ws.addEventListener('error', err => reject(err));
84
+ });
85
+ }
86
+
87
+ function createBrowserSession({ port, profileDir, waitSec, serverHost, verifySession, cookieUrls, userAgent }) {
88
+ if (!serverHost) throw new Error('createBrowserSession requires serverHost');
89
+ if (typeof verifySession !== 'function') throw new Error('createBrowserSession requires verifySession callback');
90
+ const ua = userAgent || 'agent-skills/1.0';
91
+
92
+ async function endpoint(pathname) {
93
+ const res = await fetch(`http://127.0.0.1:${port}${pathname}`);
94
+ if (!res.ok) throw new Error(`DevTools HTTP ${res.status} for ${pathname}`);
95
+ return res.json();
96
+ }
97
+
98
+ async function devtoolsReady() {
99
+ try { await endpoint('/json/version'); return true; } catch { return false; }
100
+ }
101
+
102
+ async function waitDevtools() {
103
+ for (let i = 0; i < 80; i++) {
104
+ if (await devtoolsReady()) return;
105
+ await sleep(250);
106
+ }
107
+ throw new Error('Chrome DevTools endpoint did not start');
108
+ }
109
+
110
+ async function openDevtoolsTab(url) {
111
+ if (!url) return false;
112
+ const endpointUrl = `http://127.0.0.1:${port}/json/new?${encodeURIComponent(url)}`;
113
+ for (const init of [{ method: 'PUT' }, {}]) {
114
+ try {
115
+ const res = await fetch(endpointUrl, init);
116
+ if (res.ok) { await sleep(500); return true; }
117
+ } catch {}
118
+ }
119
+ return false;
120
+ }
121
+
122
+ async function hasDevtoolsTabForHost(url, pathPrefix) {
123
+ if (!url) return false;
124
+ const host = new URL(url).host;
125
+ const list = await endpoint('/json/list');
126
+ return list.some(t => t.type === 'page' && t.url && (() => {
127
+ try {
128
+ const tabUrl = new URL(t.url);
129
+ if (tabUrl.host !== host) return false;
130
+ if (pathPrefix && !tabUrl.pathname.startsWith(pathPrefix)) return false;
131
+ return true;
132
+ } catch { return false; }
133
+ })());
134
+ }
135
+
136
+ function launchChrome(url) {
137
+ const browser = findBrowserExecutable();
138
+ const args = [
139
+ `--remote-debugging-port=${port}`,
140
+ '--remote-debugging-address=127.0.0.1',
141
+ '--remote-allow-origins=*',
142
+ `--user-data-dir=${profileDir}`,
143
+ '--no-first-run',
144
+ '--no-default-browser-check',
145
+ url,
146
+ ];
147
+ console.log(`Launching browser: ${browser}`);
148
+ const child = spawn(browser, args, { detached: true, stdio: 'ignore' });
149
+ child.on('error', err => console.error(`Failed to launch browser ${browser}: ${err.message}`));
150
+ child.unref();
151
+ }
152
+
153
+ async function ensureBrowser(openUrl, { tabPathPrefix } = {}) {
154
+ if (!(await devtoolsReady())) {
155
+ console.log(`Opening Chromium-compatible browser with reusable profile: ${profileDir}`);
156
+ launchChrome(openUrl);
157
+ } else {
158
+ console.log(`Reusing Chrome DevTools on port ${port}`);
159
+ if (openUrl) {
160
+ const hasTab = await hasDevtoolsTabForHost(openUrl, tabPathPrefix);
161
+ if (hasTab) {
162
+ console.log(`Found existing tab for ${new URL(openUrl).host}; not opening another tab.`);
163
+ } else {
164
+ const opened = await openDevtoolsTab(openUrl);
165
+ if (opened) console.log(`Opened target URL in reused browser: ${openUrl}`);
166
+ else console.warn('Could not open target URL through DevTools; continuing with existing tabs.');
167
+ }
168
+ }
169
+ }
170
+ await waitDevtools();
171
+ }
172
+
173
+ async function getPageWsUrl() {
174
+ const list = await endpoint('/json/list');
175
+ const pages = list.filter(t => t.type === 'page' && t.webSocketDebuggerUrl);
176
+ const preferred = pages.find(t => (t.url || '').includes(serverHost)) || pages[0];
177
+ return preferred && preferred.webSocketDebuggerUrl;
178
+ }
179
+
180
+ async function getCookieHeader() {
181
+ const wsUrl = await getPageWsUrl();
182
+ if (!wsUrl) return '';
183
+ const cdp = await connectCdp(wsUrl);
184
+ try {
185
+ await cdp.send('Network.enable');
186
+ const urls = (cookieUrls && cookieUrls.length) ? cookieUrls : [`https://${serverHost}/`];
187
+ const result = await cdp.send('Network.getCookies', { urls });
188
+ const cookies = (result.cookies || [])
189
+ .filter(c => c.domain && (c.domain === serverHost || c.domain.endsWith(`.${serverHost}`)))
190
+ .map(c => `${c.name}=${c.value}`);
191
+ return cookies.join('; ');
192
+ } finally {
193
+ cdp.close();
194
+ }
195
+ }
196
+
197
+ async function fetchText(url, cookie, options = {}) {
198
+ const method = options.method || 'GET';
199
+ const headers = {
200
+ Cookie: cookie,
201
+ Accept: options.accept || '*/*',
202
+ 'User-Agent': ua,
203
+ };
204
+ if (options.body !== undefined && options.body !== null) headers['Content-Type'] = options.contentType || 'application/json';
205
+ const res = await fetch(url, { method, redirect: 'follow', headers, body: options.body ?? null });
206
+ return { status: res.status, contentType: res.headers.get('content-type') || '', text: await res.text() };
207
+ }
208
+
209
+ async function fetchJson(url, cookie, options = {}) {
210
+ const result = await fetchText(url, cookie, { ...options, accept: options.accept || 'application/json' });
211
+ let json = null;
212
+ try { json = JSON.parse(result.text); } catch {}
213
+ return { ...result, json };
214
+ }
215
+
216
+ async function getCookieWithWait(openUrl, { tabPathPrefix } = {}) {
217
+ await ensureBrowser(openUrl, { tabPathPrefix });
218
+ console.log(`If prompted in Chrome, complete SSO for: ${openUrl}`);
219
+ const deadline = Date.now() + waitSec * 1000;
220
+ let last = '';
221
+ while (Date.now() < deadline) {
222
+ try {
223
+ const cookie = await getCookieHeader();
224
+ const result = await verifySession(cookie);
225
+ if (result && result.ok) {
226
+ if (process.stdout.isTTY) process.stdout.write('\n');
227
+ console.log(`Authenticated session verified${result.url ? ` via ${result.url}` : ''}`);
228
+ return cookie;
229
+ }
230
+ last = (result && result.message) || 'session not yet verified';
231
+ } catch (e) { last = e.message; }
232
+ if (process.stdout.isTTY) {
233
+ process.stdout.write(`\r${new Date().toLocaleTimeString()} ${last.padEnd(120).slice(0, 120)}`);
234
+ }
235
+ await sleep(3000);
236
+ }
237
+ if (process.stdout.isTTY) process.stdout.write('\n');
238
+ throw new Error(`Could not verify authenticated session. Last result: ${last}`);
239
+ }
240
+
241
+ return {
242
+ devtoolsReady,
243
+ waitDevtools,
244
+ openDevtoolsTab,
245
+ hasDevtoolsTabForHost,
246
+ launchChrome,
247
+ ensureBrowser,
248
+ getPageWsUrl,
249
+ getCookieHeader,
250
+ getCookieWithWait,
251
+ fetchText,
252
+ fetchJson,
253
+ };
254
+ }
255
+
256
+ module.exports = {
257
+ createBrowserSession,
258
+ findBrowserExecutable,
259
+ resolveBrowserCandidate,
260
+ connectCdp,
261
+ };