@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 +20 -0
- package/README.md +64 -2
- package/SECURITY.md +4 -4
- package/bin/vendor.js +24 -0
- package/package.json +7 -3
- package/skills/bitbucket-browser-fetch/SKILL.md +66 -0
- package/skills/bitbucket-browser-fetch/references/distribution.md +25 -0
- package/skills/bitbucket-browser-fetch/references/usage.md +84 -0
- package/skills/bitbucket-browser-fetch/scripts/atlassian-browser.js +261 -0
- package/skills/bitbucket-browser-fetch/scripts/bitbucket-browser-fetch.js +163 -0
- package/skills/bitbucket-browser-fetch/scripts/lib.js +113 -0
- package/skills/confluence-browser-fetch/scripts/atlassian-browser.js +261 -0
- package/skills/confluence-browser-fetch/scripts/confluence-browser-fetch.js +15 -210
- package/skills/confluence-update/scripts/atlassian-browser.js +261 -0
- package/skills/confluence-update/scripts/confluence-update.js +31 -224
- package/skills/jira-browser-fetch/scripts/atlassian-browser.js +261 -0
- package/skills/jira-browser-fetch/scripts/jira-browser-fetch.js +27 -230
- package/skills/jira-update/SKILL.md +121 -0
- package/skills/jira-update/references/distribution.md +5 -0
- package/skills/jira-update/references/usage.md +75 -0
- package/skills/jira-update/scripts/atlassian-browser.js +261 -0
- package/skills/jira-update/scripts/jira-update.js +404 -0
- package/skills/jira-update/scripts/lib.js +283 -0
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
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
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
|
+
};
|