@aholbreich/agent-skills 0.4.0 → 0.6.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 +12 -1
- package/COMPATIBILITY.md +8 -0
- package/README.md +26 -1
- package/SECURITY.md +4 -3
- package/bin/agent-skills.js +67 -2
- package/package.json +1 -1
- package/skills/confluence-browser-fetch/SKILL.md +5 -4
- package/skills/confluence-browser-fetch/references/usage.md +26 -10
- package/skills/confluence-browser-fetch/scripts/confluence-browser-fetch.js +105 -10
- package/skills/jira-browser-fetch/SKILL.md +4 -3
- package/skills/jira-browser-fetch/references/usage.md +27 -11
- package/skills/jira-browser-fetch/scripts/jira-browser-fetch.js +121 -48
package/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.6.0 - 2026-05-07
|
|
4
4
|
|
|
5
5
|
Added:
|
|
6
6
|
|
|
7
|
+
- Shared `ATLASSIAN_CHROME_PROFILE` and `ATLASSIAN_CHROME_DEBUG_PORT` support so Jira and Confluence fetchers can reuse one dedicated Atlassian SSO browser session.
|
|
8
|
+
- Browser fetchers now open the requested target URL in a new tab when reusing an existing DevTools browser.
|
|
9
|
+
|
|
10
|
+
## 0.5.0 - 2026-05-07
|
|
11
|
+
|
|
12
|
+
Added:
|
|
13
|
+
|
|
14
|
+
- `confluence-browser-fetch` now verifies an authenticated Confluence REST session before fetching pages, avoiding false positives from Atlassian login-page cookies.
|
|
15
|
+
- `jira-browser-fetch` now verifies an authenticated Jira REST session before issue, JQL, or backlog fetches, avoiding false positives from Atlassian login-page cookies.
|
|
7
16
|
- `jira-browser-fetch --backlog URL|BOARD_ID` to fetch all issues from a Jira Software board backlog through the authenticated browser session.
|
|
8
17
|
- Backlog manifests at `raw/jira-board-<board-id>-backlog.json` and a `backlogs` section in `raw/jira-browser-fetch-run.json`.
|
|
9
18
|
- Documentation examples for natural-language user requests that should invoke the skills.
|
|
10
19
|
- Recommended `npx skills add aholbreich/agent-skills -g` cross-agent install path, plus collision/update guidance for Pi and project-local overrides.
|
|
11
20
|
- CI/package dry-run scripts that use `npm pack --dry-run` for compatibility with older local pnpm launchers.
|
|
21
|
+
- `agent-skills install --skill NAME` and `--pick` to install only selected bundled skills from the fallback npx installer.
|
|
22
|
+
- Browser fetchers now auto-detect common Chromium-compatible browsers (Chrome, Chromium, Brave, Edge, Vivaldi) instead of only trying `/usr/bin/google-chrome` unless `CHROME` is set.
|
|
12
23
|
|
|
13
24
|
## 0.1.0 - 2026-05-06
|
|
14
25
|
|
package/COMPATIBILITY.md
CHANGED
|
@@ -79,6 +79,14 @@ npx @aholbreich/agent-skills --target project-agents
|
|
|
79
79
|
npx @aholbreich/agent-skills install --dir /path/to/skills
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
+
Select one or more skills with `--skill`, or use the interactive picker:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npx @aholbreich/agent-skills install --skill jira-browser-fetch
|
|
86
|
+
npx @aholbreich/agent-skills install --skill jira-browser-fetch --skill confluence-browser-fetch
|
|
87
|
+
npx @aholbreich/agent-skills install --pick
|
|
88
|
+
```
|
|
89
|
+
|
|
82
90
|
This fallback copies files. For symlinked, multi-agent installs, prefer `npx skills add aholbreich/agent-skills`.
|
|
83
91
|
|
|
84
92
|
## Collision behavior
|
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ See [`COMPATIBILITY.md`](COMPATIBILITY.md) for details, including collision beha
|
|
|
29
29
|
## Requirements
|
|
30
30
|
|
|
31
31
|
- Node.js `>=22`.
|
|
32
|
-
-
|
|
32
|
+
- A Chromium-compatible browser: Chrome, Chromium, Brave, Edge, or Vivaldi.
|
|
33
33
|
- Access to the Jira/Confluence site in the browser account you use.
|
|
34
34
|
- Pi, or any Agent Skills-compatible harness, if you want skill discovery.
|
|
35
35
|
|
|
@@ -105,6 +105,20 @@ npx @aholbreich/agent-skills install --target codex
|
|
|
105
105
|
npx @aholbreich/agent-skills install --target project
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
+
Install only selected skills:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npx @aholbreich/agent-skills install --skill jira-browser-fetch
|
|
112
|
+
npx @aholbreich/agent-skills install --skill confluence-browser-fetch
|
|
113
|
+
npx @aholbreich/agent-skills install --skill jira-browser-fetch --target project
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Or use the dependency-free interactive picker:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npx @aholbreich/agent-skills install --pick
|
|
120
|
+
```
|
|
121
|
+
|
|
108
122
|
Overwrite existing installed skill directories:
|
|
109
123
|
|
|
110
124
|
```bash
|
|
@@ -163,6 +177,17 @@ jira-browser-fetch
|
|
|
163
177
|
confluence-browser-fetch
|
|
164
178
|
```
|
|
165
179
|
|
|
180
|
+
## Reuse one Atlassian browser login
|
|
181
|
+
|
|
182
|
+
To avoid separate Jira and Confluence SSO prompts, use one shared automation profile and DevTools port for both fetchers:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
export ATLASSIAN_CHROME_PROFILE="$HOME/.local/share/atlassian-browser-fetch-chrome"
|
|
186
|
+
export ATLASSIAN_CHROME_DEBUG_PORT=9223
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Skill-specific variables such as `JIRA_CHROME_PROFILE` or `CONFLUENCE_CHROME_PROFILE` still override the shared profile when needed.
|
|
190
|
+
|
|
166
191
|
## Jira examples
|
|
167
192
|
|
|
168
193
|
Fetch one issue:
|
package/SECURITY.md
CHANGED
|
@@ -8,10 +8,11 @@ These skills are local automation tools. They can fetch potentially sensitive Ji
|
|
|
8
8
|
|
|
9
9
|
The Jira and Confluence fetchers:
|
|
10
10
|
|
|
11
|
-
1. launch or reuse
|
|
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.
|
|
14
|
+
4. verify those cookies represent an authenticated Jira/Confluence REST session,
|
|
15
|
+
5. call Atlassian REST endpoints with those cookies.
|
|
15
16
|
|
|
16
17
|
They do **not** require you to paste API tokens or cookies into chat.
|
|
17
18
|
|
|
@@ -22,7 +23,7 @@ They do **not** require you to paste API tokens or cookies into chat.
|
|
|
22
23
|
- Do not commit fetched Jira/Confluence exports or attachments to a public repository.
|
|
23
24
|
- Review generated `attachments.json` manifests before sharing; they may contain private URLs and filenames.
|
|
24
25
|
- Chrome remote debugging is configured for `127.0.0.1`; do not expose it to a network interface.
|
|
25
|
-
- Use dedicated browser profiles for fetch automation.
|
|
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.
|
|
26
27
|
- The default attachment download cap is `5mb`; skipped large attachments are still referenced in `attachments.json`.
|
|
27
28
|
|
|
28
29
|
## Attachment size limits
|
package/bin/agent-skills.js
CHANGED
|
@@ -5,6 +5,7 @@ const fs = require('fs');
|
|
|
5
5
|
const fsp = require('fs/promises');
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const path = require('path');
|
|
8
|
+
const readline = require('readline/promises');
|
|
8
9
|
|
|
9
10
|
const packageRoot = path.resolve(__dirname, '..');
|
|
10
11
|
const sourceSkillsDir = path.join(packageRoot, 'skills');
|
|
@@ -41,12 +42,16 @@ Commands:
|
|
|
41
42
|
Options for install:
|
|
42
43
|
--target NAME pi | agents | claude | codex | openclaw | project | project-agents | project-claude | project-codex (default: agents)
|
|
43
44
|
--dir PATH Custom skills directory, overrides --target
|
|
45
|
+
--skill NAME Install only selected skill(s); repeatable, comma-separated, or '*' for all
|
|
46
|
+
--pick Interactively choose which bundled skills to install
|
|
44
47
|
--force Overwrite existing skill directories
|
|
45
48
|
--dry-run Show what would be copied without writing
|
|
46
49
|
|
|
47
50
|
Examples:
|
|
48
51
|
npx skills add aholbreich/agent-skills -g
|
|
49
52
|
npx @aholbreich/agent-skills
|
|
53
|
+
npx @aholbreich/agent-skills install --skill jira-browser-fetch
|
|
54
|
+
npx @aholbreich/agent-skills install --pick
|
|
50
55
|
npx @aholbreich/agent-skills install --target agents --force
|
|
51
56
|
npx @aholbreich/agent-skills install --target pi --force
|
|
52
57
|
npx @aholbreich/agent-skills install --target project
|
|
@@ -80,16 +85,72 @@ async function copyDir(src, dest) {
|
|
|
80
85
|
await fsp.cp(src, dest, { recursive: true, force: true, errorOnExist: false });
|
|
81
86
|
}
|
|
82
87
|
|
|
88
|
+
function addSkillFilters(filters, value) {
|
|
89
|
+
if (!value) throw new Error('--skill requires a skill name');
|
|
90
|
+
for (const item of String(value).split(',')) {
|
|
91
|
+
const skill = item.trim();
|
|
92
|
+
if (skill) filters.push(skill);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function selectSkills(allSkills, filters) {
|
|
97
|
+
if (!filters.length || filters.includes('*')) return allSkills;
|
|
98
|
+
const known = new Set(allSkills);
|
|
99
|
+
const selected = [...new Set(filters)];
|
|
100
|
+
const unknown = selected.filter(skill => !known.has(skill));
|
|
101
|
+
if (unknown.length) {
|
|
102
|
+
throw new Error(`Unknown skill(s): ${unknown.join(', ')}. Available: ${allSkills.join(', ')}`);
|
|
103
|
+
}
|
|
104
|
+
return selected.sort();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parsePickedSkills(answer, allSkills) {
|
|
108
|
+
const value = String(answer || '').trim();
|
|
109
|
+
if (!value || value === '*') return allSkills;
|
|
110
|
+
const selected = [];
|
|
111
|
+
for (const raw of value.split(',')) {
|
|
112
|
+
const token = raw.trim();
|
|
113
|
+
if (!token) continue;
|
|
114
|
+
if (/^\d+$/.test(token)) {
|
|
115
|
+
const index = Number(token) - 1;
|
|
116
|
+
if (index < 0 || index >= allSkills.length) throw new Error(`Invalid skill number: ${token}`);
|
|
117
|
+
selected.push(allSkills[index]);
|
|
118
|
+
} else {
|
|
119
|
+
selected.push(token);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return selectSkills(allSkills, selected);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function pickSkills(allSkills) {
|
|
126
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
127
|
+
throw new Error('--pick requires an interactive terminal; use --skill NAME for non-interactive installs');
|
|
128
|
+
}
|
|
129
|
+
console.log('Bundled skills:');
|
|
130
|
+
allSkills.forEach((skill, index) => console.log(` ${index + 1}) ${skill}`));
|
|
131
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
132
|
+
try {
|
|
133
|
+
const answer = await rl.question('Install which skills? Enter numbers/names separated by commas, or blank for all: ');
|
|
134
|
+
return parsePickedSkills(answer, allSkills);
|
|
135
|
+
} finally {
|
|
136
|
+
rl.close();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
83
140
|
async function install(args) {
|
|
84
141
|
let target = 'agents';
|
|
85
142
|
let customDir = '';
|
|
86
143
|
let force = false;
|
|
87
144
|
let dryRun = false;
|
|
145
|
+
let pick = false;
|
|
146
|
+
const skillFilters = [];
|
|
88
147
|
|
|
89
148
|
for (let i = 0; i < args.length; i++) {
|
|
90
149
|
const a = args[i];
|
|
91
150
|
if (a === '--target') target = args[++i];
|
|
92
151
|
else if (a === '--dir') customDir = args[++i];
|
|
152
|
+
else if (a === '--skill' || a === '-s') addSkillFilters(skillFilters, args[++i]);
|
|
153
|
+
else if (a === '--pick') pick = true;
|
|
93
154
|
else if (a === '--force') force = true;
|
|
94
155
|
else if (a === '--dry-run') dryRun = true;
|
|
95
156
|
else if (a === '-h' || a === '--help') { usage(); return; }
|
|
@@ -99,11 +160,15 @@ async function install(args) {
|
|
|
99
160
|
if (!customDir && !TARGETS[target]) {
|
|
100
161
|
throw new Error(`Unknown target '${target}'. Valid targets: ${Object.keys(TARGETS).join(', ')}`);
|
|
101
162
|
}
|
|
163
|
+
if (pick && skillFilters.length) {
|
|
164
|
+
throw new Error('Use either --pick or --skill, not both');
|
|
165
|
+
}
|
|
102
166
|
|
|
103
167
|
const destRoot = path.resolve(expandHome(customDir || TARGETS[target]));
|
|
104
|
-
const
|
|
168
|
+
const allSkills = await listSkills();
|
|
169
|
+
const skills = pick ? await pickSkills(allSkills) : selectSkills(allSkills, skillFilters);
|
|
105
170
|
|
|
106
|
-
console.log(`Installing ${skills.length} skill(s) to ${destRoot}`);
|
|
171
|
+
console.log(`Installing ${skills.length} of ${allSkills.length} skill(s) to ${destRoot}`);
|
|
107
172
|
if (dryRun) console.log('Dry run: no files will be written.');
|
|
108
173
|
|
|
109
174
|
if (!dryRun) await fsp.mkdir(destRoot, { recursive: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aholbreich/agent-skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
name: confluence-browser-fetch
|
|
3
3
|
description: Fetch Confluence Cloud pages through an authenticated Chrome browser session when API tokens do not work, especially with Microsoft/SSO. Use to archive Confluence page JSON, storage/view HTML, browser HTML, attachments, CQL search results, or page descendants into a raw wiki folder.
|
|
4
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
|
|
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
6
|
---
|
|
7
7
|
|
|
8
8
|
# Confluence Browser Fetch
|
|
9
9
|
|
|
10
10
|
Use this skill when a user wants Confluence pages ingested into an LLM wiki `raw/` folder and normal Atlassian API-token auth is unavailable or inconvenient due to SSO.
|
|
11
11
|
|
|
12
|
-
The script opens/reuses Chrome with a dedicated profile, lets the user complete SSO once, extracts Atlassian cookies via Chrome DevTools, and fetches Confluence REST data plus rendered page HTML and attachments.
|
|
12
|
+
The script opens/reuses Chrome with a dedicated profile, lets the user complete SSO once, extracts Atlassian cookies via Chrome DevTools, verifies they represent an authenticated Confluence REST session, and fetches Confluence REST data plus rendered page HTML and attachments.
|
|
13
13
|
|
|
14
14
|
## Safety
|
|
15
15
|
|
|
@@ -49,8 +49,9 @@ Important options:
|
|
|
49
49
|
2. If the user gives a title, ask for the space key or use `--cql`.
|
|
50
50
|
3. Show the command before running it.
|
|
51
51
|
4. If Chrome opens, ask the user to complete SSO in that browser window.
|
|
52
|
-
5.
|
|
53
|
-
6.
|
|
52
|
+
5. To share one Atlassian SSO login with `jira-browser-fetch`, use `ATLASSIAN_CHROME_PROFILE` plus `ATLASSIAN_CHROME_DEBUG_PORT` (or matching `--profile-dir` and `--port`) for both tools.
|
|
53
|
+
6. Verify saved files.
|
|
54
|
+
7. If this is an LLM wiki ingest, process the saved `raw/confluence/...` material into `wiki/` per the project `AGENTS.md`.
|
|
54
55
|
|
|
55
56
|
Example:
|
|
56
57
|
|
|
@@ -7,24 +7,25 @@ Confluence Cloud pages are often behind Microsoft/SSO. API-token Basic auth may
|
|
|
7
7
|
1. Launching Chrome with a dedicated user profile.
|
|
8
8
|
2. Letting the user complete normal SSO.
|
|
9
9
|
3. Reading Atlassian cookies through local Chrome DevTools.
|
|
10
|
-
4.
|
|
10
|
+
4. Verifying those cookies represent an authenticated Confluence REST session.
|
|
11
|
+
5. Calling Confluence REST endpoints with those cookies.
|
|
11
12
|
|
|
12
13
|
No cookie or API token needs to be pasted into chat.
|
|
13
14
|
|
|
14
15
|
## Requirements
|
|
15
16
|
|
|
16
17
|
- Node.js 22+.
|
|
17
|
-
-
|
|
18
|
+
- A Chromium-compatible browser: Chrome, Chromium, Brave, Edge, or Vivaldi.
|
|
18
19
|
- Access to the Confluence page with the logged-in account.
|
|
19
20
|
|
|
20
21
|
Check:
|
|
21
22
|
|
|
22
23
|
```bash
|
|
23
24
|
node --version
|
|
24
|
-
which google-chrome || which chromium || which chromium-browser
|
|
25
|
+
which google-chrome || which chromium || which chromium-browser || which brave-browser || which microsoft-edge
|
|
25
26
|
```
|
|
26
27
|
|
|
27
|
-
If
|
|
28
|
+
The script auto-detects common Chromium-compatible browsers. If yours has a different path:
|
|
28
29
|
|
|
29
30
|
```bash
|
|
30
31
|
CHROME=/path/to/chrome scripts/confluence-browser-fetch.js 123456
|
|
@@ -105,15 +106,26 @@ By default, pages with matching local `metadata.json` Confluence `version.number
|
|
|
105
106
|
|---|---|
|
|
106
107
|
| `CONFLUENCE_SITE` | Default Atlassian site, e.g. `https://example.atlassian.net` |
|
|
107
108
|
| `CONFLUENCE_RAW_DIR` | Default output raw directory |
|
|
108
|
-
| `CONFLUENCE_CHROME_DEBUG_PORT` | Chrome DevTools port, default `9224` |
|
|
109
|
+
| `CONFLUENCE_CHROME_DEBUG_PORT` | Chrome DevTools port, default `9224`; overrides `ATLASSIAN_CHROME_DEBUG_PORT` |
|
|
110
|
+
| `ATLASSIAN_CHROME_DEBUG_PORT` | Shared Chrome DevTools port for Jira and Confluence browser fetchers. If only `ATLASSIAN_CHROME_PROFILE` is set, Confluence defaults to shared port `9223`. |
|
|
109
111
|
| `CONFLUENCE_FETCH_WAIT_SEC` | Wait timeout, default `900` |
|
|
110
112
|
| `CONFLUENCE_MAX_SEARCH_RESULTS` | Max CQL pages, default `200` |
|
|
111
113
|
| `CONFLUENCE_MAX_ATTACHMENT_SIZE` / `CONFLUENCE_MAX_ATTACHMENT_BYTES` | Max attachment download size, default `5mb`; skipped files are listed in `attachments.json` |
|
|
112
114
|
| `CONFLUENCE_RETRIES` | Retry count for transient HTTP errors, default `3` |
|
|
113
115
|
| `CONFLUENCE_REQUEST_TIMEOUT_SEC` | Per-request timeout, default `60` |
|
|
114
116
|
| `CONFLUENCE_SKIP_UNCHANGED` | Set to `0` to disable default skip-unchanged behavior |
|
|
115
|
-
| `CONFLUENCE_CHROME_PROFILE` | Dedicated Chrome profile dir |
|
|
116
|
-
| `
|
|
117
|
+
| `CONFLUENCE_CHROME_PROFILE` | Dedicated Chrome profile dir; overrides `ATLASSIAN_CHROME_PROFILE` |
|
|
118
|
+
| `ATLASSIAN_CHROME_PROFILE` | Shared browser profile dir for Jira and Confluence browser fetchers |
|
|
119
|
+
| `CHROME` / `CHROMIUM` | Browser executable path override |
|
|
120
|
+
|
|
121
|
+
To reuse one Atlassian SSO login across Jira and Confluence fetches, set a shared profile and port for both tools:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
export ATLASSIAN_CHROME_PROFILE="$HOME/.local/share/atlassian-browser-fetch-chrome"
|
|
125
|
+
export ATLASSIAN_CHROME_DEBUG_PORT=9223
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
When reusing an existing DevTools browser on the configured port, the script opens the requested Confluence URL in a new tab before verifying the REST session.
|
|
117
129
|
|
|
118
130
|
## Output Files
|
|
119
131
|
|
|
@@ -129,13 +141,17 @@ For each page:
|
|
|
129
141
|
|
|
130
142
|
## Troubleshooting
|
|
131
143
|
|
|
132
|
-
### `no Atlassian cookies yet`
|
|
144
|
+
### `no Atlassian cookies yet` / `not authenticated yet`
|
|
145
|
+
|
|
146
|
+
Complete SSO in the Chrome window opened by the script. Login-page cookies are not enough; the script waits until a Confluence REST session probe succeeds.
|
|
147
|
+
|
|
148
|
+
### `Could not verify authenticated Confluence session`
|
|
133
149
|
|
|
134
|
-
Complete SSO
|
|
150
|
+
The browser did not reach an authenticated Confluence REST session before `--wait` expired. Complete SSO, confirm you can open the target Confluence site in that browser profile, then rerun or increase `--wait`.
|
|
135
151
|
|
|
136
152
|
### `Page failed HTTP 404`
|
|
137
153
|
|
|
138
|
-
|
|
154
|
+
After authentication is verified, this usually means the authenticated user cannot see the page, or the page ID/site is wrong.
|
|
139
155
|
|
|
140
156
|
### URL cannot be resolved
|
|
141
157
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const fs = require('fs');
|
|
4
5
|
const fsp = require('fs/promises');
|
|
5
6
|
const os = require('os');
|
|
6
7
|
const path = require('path');
|
|
@@ -29,8 +30,8 @@ Options:
|
|
|
29
30
|
--retries N HTTP retry count for transient failures (default: 3)
|
|
30
31
|
--request-timeout SEC Per-request timeout (default: 60)
|
|
31
32
|
--wait SEC Wait time for SSO/session (default: 900)
|
|
32
|
-
--port PORT Chrome DevTools port (default: 9224)
|
|
33
|
-
--profile-dir DIR Chrome profile dir (default: ~/.local/share/confluence-browser-fetch-chrome)
|
|
33
|
+
--port PORT Chrome DevTools port (default: CONFLUENCE_CHROME_DEBUG_PORT, ATLASSIAN_CHROME_DEBUG_PORT, or 9224)
|
|
34
|
+
--profile-dir DIR Chrome profile dir (default: CONFLUENCE_CHROME_PROFILE, ATLASSIAN_CHROME_PROFILE, or ~/.local/share/confluence-browser-fetch-chrome)
|
|
34
35
|
--help Show this help
|
|
35
36
|
|
|
36
37
|
Examples:
|
|
@@ -45,9 +46,9 @@ Examples:
|
|
|
45
46
|
const opts = {
|
|
46
47
|
site: process.env.CONFLUENCE_SITE || '',
|
|
47
48
|
rawDir: process.env.CONFLUENCE_RAW_DIR || path.resolve(process.cwd(), 'raw'),
|
|
48
|
-
port: Number(process.env.CONFLUENCE_CHROME_DEBUG_PORT || 9224),
|
|
49
|
+
port: Number(process.env.CONFLUENCE_CHROME_DEBUG_PORT || process.env.ATLASSIAN_CHROME_DEBUG_PORT || (process.env.ATLASSIAN_CHROME_PROFILE ? 9223 : 9224)),
|
|
49
50
|
waitSec: Number(process.env.CONFLUENCE_FETCH_WAIT_SEC || 900),
|
|
50
|
-
profileDir: process.env.CONFLUENCE_CHROME_PROFILE || path.join(os.homedir(), '.local/share/confluence-browser-fetch-chrome'),
|
|
51
|
+
profileDir: process.env.CONFLUENCE_CHROME_PROFILE || process.env.ATLASSIAN_CHROME_PROFILE || path.join(os.homedir(), '.local/share/confluence-browser-fetch-chrome'),
|
|
51
52
|
maxSearchResults: Number(process.env.CONFLUENCE_MAX_SEARCH_RESULTS || 200),
|
|
52
53
|
retries: Number(process.env.CONFLUENCE_RETRIES || 3),
|
|
53
54
|
requestTimeoutSec: Number(process.env.CONFLUENCE_REQUEST_TIMEOUT_SEC || 60),
|
|
@@ -115,8 +116,65 @@ async function waitDevtools() {
|
|
|
115
116
|
throw new Error('Chrome DevTools endpoint did not start');
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
async function openDevtoolsTab(url) {
|
|
120
|
+
if (!url) return false;
|
|
121
|
+
const endpointUrl = `http://127.0.0.1:${opts.port}/json/new?${encodeURIComponent(url)}`;
|
|
122
|
+
for (const init of [{ method: 'PUT' }, {}]) {
|
|
123
|
+
try {
|
|
124
|
+
const res = await fetch(endpointUrl, init);
|
|
125
|
+
if (res.ok) {
|
|
126
|
+
await sleep(500);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
} catch {}
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isExecutable(file) {
|
|
135
|
+
try { fs.accessSync(file, fs.constants.X_OK); return true; } catch { return false; }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function resolveBrowserCandidate(candidate) {
|
|
139
|
+
if (!candidate) return null;
|
|
140
|
+
if (candidate.includes(path.sep)) return isExecutable(candidate) ? candidate : null;
|
|
141
|
+
for (const dir of String(process.env.PATH || '').split(path.delimiter)) {
|
|
142
|
+
if (!dir) continue;
|
|
143
|
+
const full = path.join(dir, candidate);
|
|
144
|
+
if (isExecutable(full)) return full;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function findBrowserExecutable() {
|
|
150
|
+
const candidates = [
|
|
151
|
+
process.env.CHROME,
|
|
152
|
+
process.env.CHROMIUM,
|
|
153
|
+
'google-chrome',
|
|
154
|
+
'google-chrome-stable',
|
|
155
|
+
'chromium',
|
|
156
|
+
'chromium-browser',
|
|
157
|
+
'brave-browser',
|
|
158
|
+
'brave',
|
|
159
|
+
'microsoft-edge',
|
|
160
|
+
'microsoft-edge-stable',
|
|
161
|
+
'vivaldi',
|
|
162
|
+
'vivaldi-stable',
|
|
163
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
164
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
165
|
+
'/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
|
|
166
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
167
|
+
'/Applications/Vivaldi.app/Contents/MacOS/Vivaldi',
|
|
168
|
+
];
|
|
169
|
+
for (const candidate of candidates) {
|
|
170
|
+
const resolved = resolveBrowserCandidate(candidate);
|
|
171
|
+
if (resolved) return resolved;
|
|
172
|
+
}
|
|
173
|
+
throw new Error('Could not find a Chromium-compatible browser. Install Chrome/Chromium/Brave/Edge/Vivaldi or set CHROME=/path/to/browser.');
|
|
174
|
+
}
|
|
175
|
+
|
|
118
176
|
function launchChrome(url) {
|
|
119
|
-
const
|
|
177
|
+
const browser = findBrowserExecutable();
|
|
120
178
|
const args = [
|
|
121
179
|
`--remote-debugging-port=${opts.port}`,
|
|
122
180
|
'--remote-debugging-address=127.0.0.1',
|
|
@@ -126,16 +184,24 @@ function launchChrome(url) {
|
|
|
126
184
|
'--no-default-browser-check',
|
|
127
185
|
url,
|
|
128
186
|
];
|
|
129
|
-
|
|
187
|
+
console.log(`Launching browser: ${browser}`);
|
|
188
|
+
const child = spawn(browser, args, { detached: true, stdio: 'ignore' });
|
|
189
|
+
child.on('error', err => console.error(`Failed to launch browser ${browser}: ${err.message}`));
|
|
130
190
|
child.unref();
|
|
131
191
|
}
|
|
132
192
|
|
|
133
193
|
async function ensureBrowser(openUrl) {
|
|
134
194
|
if (!(await devtoolsReady())) {
|
|
135
|
-
console.log(`Opening
|
|
195
|
+
console.log(`Opening Chromium-compatible browser with reusable profile: ${opts.profileDir}`);
|
|
136
196
|
launchChrome(openUrl || wikiBase);
|
|
137
197
|
} else {
|
|
138
198
|
console.log(`Reusing Chrome DevTools on port ${opts.port}`);
|
|
199
|
+
const targetUrl = openUrl || wikiBase;
|
|
200
|
+
if (targetUrl) {
|
|
201
|
+
const opened = await openDevtoolsTab(targetUrl);
|
|
202
|
+
if (opened) console.log(`Opened target URL in reused browser: ${targetUrl}`);
|
|
203
|
+
else console.warn(`Could not open target URL through DevTools; continuing with existing tabs.`);
|
|
204
|
+
}
|
|
139
205
|
}
|
|
140
206
|
await waitDevtools();
|
|
141
207
|
}
|
|
@@ -246,6 +312,30 @@ async function fetchJson(url, cookie) {
|
|
|
246
312
|
return { ...result, json };
|
|
247
313
|
}
|
|
248
314
|
|
|
315
|
+
async function verifyConfluenceSession(cookie) {
|
|
316
|
+
if (!cookie) return { ok: false, message: 'no Atlassian cookies yet' };
|
|
317
|
+
|
|
318
|
+
const probes = [
|
|
319
|
+
`${wikiBase}/rest/api/user/current`,
|
|
320
|
+
`${wikiBase}/rest/api/space?limit=1`,
|
|
321
|
+
];
|
|
322
|
+
|
|
323
|
+
for (const url of probes) {
|
|
324
|
+
const result = await fetchJson(url, cookie);
|
|
325
|
+
if (result.status === 200 && result.json) return { ok: true, url };
|
|
326
|
+
if (result.status === 401 || result.status === 403) {
|
|
327
|
+
return { ok: false, message: `not authenticated yet (${result.status} from ${url})` };
|
|
328
|
+
}
|
|
329
|
+
if (result.status === 302 || result.status === 303) {
|
|
330
|
+
return { ok: false, message: `still redirected to login (${result.status} from ${url})` };
|
|
331
|
+
}
|
|
332
|
+
if (result.status === 404) continue;
|
|
333
|
+
return { ok: false, message: `session probe HTTP ${result.status} from ${url}` };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return { ok: false, message: 'could not verify Confluence session' };
|
|
337
|
+
}
|
|
338
|
+
|
|
249
339
|
async function getCookieWithWait(openUrl) {
|
|
250
340
|
await ensureBrowser(openUrl || wikiBase);
|
|
251
341
|
console.log(`If prompted in Chrome, complete SSO for: ${openUrl || wikiBase}`);
|
|
@@ -254,14 +344,19 @@ async function getCookieWithWait(openUrl) {
|
|
|
254
344
|
while (Date.now() < deadline) {
|
|
255
345
|
try {
|
|
256
346
|
const cookie = await getCookieHeader();
|
|
257
|
-
|
|
258
|
-
|
|
347
|
+
const session = await verifyConfluenceSession(cookie);
|
|
348
|
+
if (session.ok) {
|
|
349
|
+
process.stdout.write('\n');
|
|
350
|
+
console.log(`Authenticated Confluence session verified via ${session.url}`);
|
|
351
|
+
return cookie;
|
|
352
|
+
}
|
|
353
|
+
last = session.message;
|
|
259
354
|
} catch (e) { last = e.message; }
|
|
260
355
|
process.stdout.write(`\r${new Date().toLocaleTimeString()} ${last.padEnd(120).slice(0, 120)}`);
|
|
261
356
|
await sleep(3000);
|
|
262
357
|
}
|
|
263
358
|
process.stdout.write('\n');
|
|
264
|
-
throw new Error(`Could not
|
|
359
|
+
throw new Error(`Could not verify authenticated Confluence session. Last result: ${last}`);
|
|
265
360
|
}
|
|
266
361
|
|
|
267
362
|
function cqlQuote(s) {
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
name: jira-browser-fetch
|
|
3
3
|
description: Fetch Jira issue raw data through an authenticated Chrome browser session when jira-cli/API tokens do not work, especially with Microsoft/SSO. Use to archive Jira issues, Jira Software board backlogs, JQL result sets, linked tickets, rendered HTML/XML, remote links, and attachments into a raw wiki folder.
|
|
4
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
|
|
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
6
|
---
|
|
7
7
|
|
|
8
8
|
# Jira Browser Fetch
|
|
9
9
|
|
|
10
10
|
Use this skill when Jira API-token authentication fails or the organization uses Microsoft/SSO and the user wants Jira issues, Jira Software board backlogs, or JQL result sets archived into a local raw/wiki folder.
|
|
11
11
|
|
|
12
|
-
The bundled script opens/reuses Chrome with a dedicated profile, lets the user complete SSO once, extracts Jira cookies via Chrome DevTools, and fetches Jira REST/HTML/XML/attachments into a raw directory.
|
|
12
|
+
The bundled script opens/reuses Chrome with a dedicated profile, lets the user complete SSO once, extracts Jira cookies via Chrome DevTools, verifies they represent an authenticated Jira REST session, and fetches Jira REST/HTML/XML/attachments into a raw directory.
|
|
13
13
|
|
|
14
14
|
## Safety
|
|
15
15
|
|
|
@@ -55,7 +55,8 @@ Use this skill for user requests like:
|
|
|
55
55
|
1. Identify raw directory.
|
|
56
56
|
2. Run the script and show the command first.
|
|
57
57
|
3. If Chrome opens, ask the user to complete SSO in that browser window.
|
|
58
|
-
4.
|
|
58
|
+
4. To share one Atlassian SSO login with `confluence-browser-fetch`, use `ATLASSIAN_CHROME_PROFILE` plus `ATLASSIAN_CHROME_DEBUG_PORT` (or matching `--profile-dir` and `--port`) for both tools.
|
|
59
|
+
5. Verify saved files.
|
|
59
60
|
|
|
60
61
|
Example:
|
|
61
62
|
|
|
@@ -7,13 +7,14 @@ Some Jira Cloud organizations use Microsoft/SSO and block or break API-token Bas
|
|
|
7
7
|
1. Launching Chrome with a dedicated user profile.
|
|
8
8
|
2. Letting the user complete normal SSO in Chrome.
|
|
9
9
|
3. Reading Jira cookies through the local Chrome DevTools protocol.
|
|
10
|
-
4.
|
|
10
|
+
4. Verifying those cookies represent an authenticated Jira REST session.
|
|
11
|
+
5. Calling Jira REST endpoints with those cookies.
|
|
11
12
|
|
|
12
13
|
No token or cookie needs to be pasted into chat.
|
|
13
14
|
|
|
14
15
|
## Requirements
|
|
15
16
|
|
|
16
|
-
- Linux/macOS with Chrome or
|
|
17
|
+
- Linux/macOS with a Chromium-compatible browser: Chrome, Chromium, Brave, Edge, or Vivaldi.
|
|
17
18
|
- Node.js 22+.
|
|
18
19
|
- Network access to the Jira site.
|
|
19
20
|
|
|
@@ -21,10 +22,10 @@ Check:
|
|
|
21
22
|
|
|
22
23
|
```bash
|
|
23
24
|
node --version
|
|
24
|
-
which google-chrome || which chromium || which chromium-browser
|
|
25
|
+
which google-chrome || which chromium || which chromium-browser || which brave-browser || which microsoft-edge
|
|
25
26
|
```
|
|
26
27
|
|
|
27
|
-
If
|
|
28
|
+
The script auto-detects common Chromium-compatible browsers. If yours has a different path:
|
|
28
29
|
|
|
29
30
|
```bash
|
|
30
31
|
CHROME=/path/to/chrome scripts/jira-browser-fetch.js PROJ-123
|
|
@@ -120,12 +121,23 @@ Default max attachment download size is `5mb`. Use `--max-attachment-size unlimi
|
|
|
120
121
|
|---|---|
|
|
121
122
|
| `JIRA_SERVER` | Default Jira base URL |
|
|
122
123
|
| `JIRA_RAW_DIR` | Default output raw directory |
|
|
123
|
-
| `JIRA_CHROME_DEBUG_PORT` | Chrome DevTools port, default `9223` |
|
|
124
|
+
| `JIRA_CHROME_DEBUG_PORT` | Chrome DevTools port, default `9223`; overrides `ATLASSIAN_CHROME_DEBUG_PORT` |
|
|
125
|
+
| `ATLASSIAN_CHROME_DEBUG_PORT` | Shared Chrome DevTools port for Jira and Confluence browser fetchers |
|
|
124
126
|
| `JIRA_FETCH_WAIT_SEC` | Wait timeout per issue, default `900` |
|
|
125
127
|
| `JIRA_MAX_SEARCH_RESULTS` | Max issues added per JQL or backlog search, default `1000` |
|
|
126
128
|
| `JIRA_MAX_ATTACHMENT_SIZE` / `JIRA_MAX_ATTACHMENT_BYTES` | Max attachment download size, default `5mb`; skipped files are listed in `attachments.json` |
|
|
127
|
-
| `JIRA_CHROME_PROFILE` | Dedicated Chrome profile dir |
|
|
128
|
-
| `
|
|
129
|
+
| `JIRA_CHROME_PROFILE` | Dedicated Chrome profile dir; overrides `ATLASSIAN_CHROME_PROFILE` |
|
|
130
|
+
| `ATLASSIAN_CHROME_PROFILE` | Shared browser profile dir for Jira and Confluence browser fetchers |
|
|
131
|
+
| `CHROME` / `CHROMIUM` | Browser executable path override |
|
|
132
|
+
|
|
133
|
+
To reuse one Atlassian SSO login across Jira and Confluence fetches, set a shared profile and port for both tools:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
export ATLASSIAN_CHROME_PROFILE="$HOME/.local/share/atlassian-browser-fetch-chrome"
|
|
137
|
+
export ATLASSIAN_CHROME_DEBUG_PORT=9223
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
When reusing an existing DevTools browser on the configured port, the script opens the requested Jira URL in a new tab before verifying the REST session.
|
|
129
141
|
|
|
130
142
|
## Example user requests
|
|
131
143
|
|
|
@@ -139,17 +151,21 @@ Agents should invoke this skill for requests such as:
|
|
|
139
151
|
|
|
140
152
|
## Troubleshooting
|
|
141
153
|
|
|
142
|
-
### `no
|
|
154
|
+
### `no Atlassian cookies yet` / `not authenticated yet`
|
|
155
|
+
|
|
156
|
+
Complete SSO in the Chrome window opened by the script. Login-page cookies are not enough; the script waits until a Jira REST session probe succeeds.
|
|
157
|
+
|
|
158
|
+
### `Could not verify authenticated Jira session`
|
|
143
159
|
|
|
144
|
-
Complete SSO
|
|
160
|
+
The browser did not reach an authenticated Jira REST session before `--wait` expired. Complete SSO, confirm you can open the target Jira site in that browser profile, then rerun or increase `--wait`.
|
|
145
161
|
|
|
146
162
|
### `HTTP 404 Issue does not exist or you do not have permission`
|
|
147
163
|
|
|
148
164
|
The session works, but the account cannot see the issue or the key is not a Jira issue.
|
|
149
165
|
|
|
150
|
-
###
|
|
166
|
+
### Browser does not open
|
|
151
167
|
|
|
152
|
-
|
|
168
|
+
The script tries `CHROME`, `CHROMIUM`, then common Chrome/Chromium/Brave/Edge/Vivaldi executable names and macOS app paths. If auto-detection fails, set the executable path:
|
|
153
169
|
|
|
154
170
|
```bash
|
|
155
171
|
CHROME=/usr/bin/chromium scripts/jira-browser-fetch.js PROJ-123
|
|
@@ -35,8 +35,8 @@ Options:
|
|
|
35
35
|
--max-attachment-size S Skip attachment downloads larger than S (default: 5mb; use unlimited to disable)
|
|
36
36
|
--prefix A,B,C Only fetch referenced keys with these project prefixes
|
|
37
37
|
--wait SEC Wait time for SSO/session per issue (default: 900)
|
|
38
|
-
--port PORT Chrome DevTools port (default: 9223)
|
|
39
|
-
--profile-dir DIR Chrome profile dir (default: ~/.local/share/jira-browser-fetch-chrome)
|
|
38
|
+
--port PORT Chrome DevTools port (default: JIRA_CHROME_DEBUG_PORT, ATLASSIAN_CHROME_DEBUG_PORT, or 9223)
|
|
39
|
+
--profile-dir DIR Chrome profile dir (default: JIRA_CHROME_PROFILE, ATLASSIAN_CHROME_PROFILE, or ~/.local/share/jira-browser-fetch-chrome)
|
|
40
40
|
--no-attachments Do not download Jira attachments
|
|
41
41
|
--no-html Do not save browser HTML
|
|
42
42
|
--no-xml Do not save Jira XML issue view
|
|
@@ -54,9 +54,9 @@ Examples:
|
|
|
54
54
|
const opts = {
|
|
55
55
|
server: process.env.JIRA_SERVER || '',
|
|
56
56
|
rawDir: process.env.JIRA_RAW_DIR || path.resolve(process.cwd(), 'raw'),
|
|
57
|
-
port: Number(process.env.JIRA_CHROME_DEBUG_PORT || 9223),
|
|
57
|
+
port: Number(process.env.JIRA_CHROME_DEBUG_PORT || process.env.ATLASSIAN_CHROME_DEBUG_PORT || 9223),
|
|
58
58
|
waitSec: Number(process.env.JIRA_FETCH_WAIT_SEC || 900),
|
|
59
|
-
profileDir: process.env.JIRA_CHROME_PROFILE || path.join(os.homedir(), '.local/share/jira-browser-fetch-chrome'),
|
|
59
|
+
profileDir: process.env.JIRA_CHROME_PROFILE || process.env.ATLASSIAN_CHROME_PROFILE || path.join(os.homedir(), '.local/share/jira-browser-fetch-chrome'),
|
|
60
60
|
connected: false,
|
|
61
61
|
depth: undefined,
|
|
62
62
|
scanText: false,
|
|
@@ -128,8 +128,65 @@ async function waitDevtools() {
|
|
|
128
128
|
throw new Error('Chrome DevTools endpoint did not start');
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
async function openDevtoolsTab(url) {
|
|
132
|
+
if (!url) return false;
|
|
133
|
+
const endpointUrl = `http://127.0.0.1:${opts.port}/json/new?${encodeURIComponent(url)}`;
|
|
134
|
+
for (const init of [{ method: 'PUT' }, {}]) {
|
|
135
|
+
try {
|
|
136
|
+
const res = await fetch(endpointUrl, init);
|
|
137
|
+
if (res.ok) {
|
|
138
|
+
await sleep(500);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
} catch {}
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isExecutable(file) {
|
|
147
|
+
try { fs.accessSync(file, fs.constants.X_OK); return true; } catch { return false; }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function resolveBrowserCandidate(candidate) {
|
|
151
|
+
if (!candidate) return null;
|
|
152
|
+
if (candidate.includes(path.sep)) return isExecutable(candidate) ? candidate : null;
|
|
153
|
+
for (const dir of String(process.env.PATH || '').split(path.delimiter)) {
|
|
154
|
+
if (!dir) continue;
|
|
155
|
+
const full = path.join(dir, candidate);
|
|
156
|
+
if (isExecutable(full)) return full;
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function findBrowserExecutable() {
|
|
162
|
+
const candidates = [
|
|
163
|
+
process.env.CHROME,
|
|
164
|
+
process.env.CHROMIUM,
|
|
165
|
+
'google-chrome',
|
|
166
|
+
'google-chrome-stable',
|
|
167
|
+
'chromium',
|
|
168
|
+
'chromium-browser',
|
|
169
|
+
'brave-browser',
|
|
170
|
+
'brave',
|
|
171
|
+
'microsoft-edge',
|
|
172
|
+
'microsoft-edge-stable',
|
|
173
|
+
'vivaldi',
|
|
174
|
+
'vivaldi-stable',
|
|
175
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
176
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
177
|
+
'/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
|
|
178
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
179
|
+
'/Applications/Vivaldi.app/Contents/MacOS/Vivaldi',
|
|
180
|
+
];
|
|
181
|
+
for (const candidate of candidates) {
|
|
182
|
+
const resolved = resolveBrowserCandidate(candidate);
|
|
183
|
+
if (resolved) return resolved;
|
|
184
|
+
}
|
|
185
|
+
throw new Error('Could not find a Chromium-compatible browser. Install Chrome/Chromium/Brave/Edge/Vivaldi or set CHROME=/path/to/browser.');
|
|
186
|
+
}
|
|
187
|
+
|
|
131
188
|
function launchChrome(url) {
|
|
132
|
-
const
|
|
189
|
+
const browser = findBrowserExecutable();
|
|
133
190
|
const args = [
|
|
134
191
|
`--remote-debugging-port=${opts.port}`,
|
|
135
192
|
'--remote-debugging-address=127.0.0.1',
|
|
@@ -139,7 +196,9 @@ function launchChrome(url) {
|
|
|
139
196
|
'--no-default-browser-check',
|
|
140
197
|
url,
|
|
141
198
|
];
|
|
142
|
-
|
|
199
|
+
console.log(`Launching browser: ${browser}`);
|
|
200
|
+
const child = spawn(browser, args, { detached: true, stdio: 'ignore' });
|
|
201
|
+
child.on('error', err => console.error(`Failed to launch browser ${browser}: ${err.message}`));
|
|
143
202
|
child.unref();
|
|
144
203
|
}
|
|
145
204
|
|
|
@@ -223,21 +282,57 @@ async function fetchJson(url, cookie, accept) {
|
|
|
223
282
|
return { ...result, json };
|
|
224
283
|
}
|
|
225
284
|
|
|
285
|
+
async function verifyJiraSession(cookie) {
|
|
286
|
+
if (!cookie) return { ok: false, message: 'no Atlassian cookies yet' };
|
|
287
|
+
|
|
288
|
+
const probes = [
|
|
289
|
+
`${opts.server}/rest/api/3/myself`,
|
|
290
|
+
`${opts.server}/rest/api/2/myself`,
|
|
291
|
+
];
|
|
292
|
+
|
|
293
|
+
for (const url of probes) {
|
|
294
|
+
const result = await fetchJson(url, cookie, 'application/json');
|
|
295
|
+
if (result.status === 200 && result.json && (result.json.accountId || result.json.name || result.json.key || result.json.displayName)) {
|
|
296
|
+
return { ok: true, url };
|
|
297
|
+
}
|
|
298
|
+
if (result.status === 200) {
|
|
299
|
+
const kind = result.json ? 'unexpected JSON response' : (/html/i.test(result.contentType) ? 'login page' : 'non-JSON response');
|
|
300
|
+
return { ok: false, message: `not authenticated yet (${kind} from ${url})` };
|
|
301
|
+
}
|
|
302
|
+
if (result.status === 401 || result.status === 403) {
|
|
303
|
+
return { ok: false, message: `not authenticated yet (${result.status} from ${url})` };
|
|
304
|
+
}
|
|
305
|
+
if (result.status === 302 || result.status === 303) {
|
|
306
|
+
return { ok: false, message: `still redirected to login (${result.status} from ${url})` };
|
|
307
|
+
}
|
|
308
|
+
if (result.status === 404) continue;
|
|
309
|
+
return { ok: false, message: `session probe HTTP ${result.status} from ${url}` };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return { ok: false, message: 'could not verify Jira session' };
|
|
313
|
+
}
|
|
314
|
+
|
|
226
315
|
async function getCookieWithWait(openUrl) {
|
|
227
316
|
await ensureBrowser(openUrl || `${opts.server}/`);
|
|
317
|
+
console.log(`If prompted in Chrome, complete SSO for: ${openUrl || opts.server}`);
|
|
228
318
|
const deadline = Date.now() + opts.waitSec * 1000;
|
|
229
319
|
let last = '';
|
|
230
320
|
while (Date.now() < deadline) {
|
|
231
321
|
try {
|
|
232
322
|
const cookie = await getCookieHeader();
|
|
233
|
-
|
|
234
|
-
|
|
323
|
+
const session = await verifyJiraSession(cookie);
|
|
324
|
+
if (session.ok) {
|
|
325
|
+
process.stdout.write('\n');
|
|
326
|
+
console.log(`Authenticated Jira session verified via ${session.url}`);
|
|
327
|
+
return cookie;
|
|
328
|
+
}
|
|
329
|
+
last = session.message;
|
|
235
330
|
} catch (e) { last = e.message; }
|
|
236
331
|
process.stdout.write(`\r${new Date().toLocaleTimeString()} ${last.padEnd(120).slice(0, 120)}`);
|
|
237
332
|
await sleep(3000);
|
|
238
333
|
}
|
|
239
334
|
process.stdout.write('\n');
|
|
240
|
-
throw new Error(`Could not
|
|
335
|
+
throw new Error(`Could not verify authenticated Jira session. Last result: ${last}`);
|
|
241
336
|
}
|
|
242
337
|
|
|
243
338
|
async function searchJql(jql) {
|
|
@@ -271,21 +366,16 @@ async function searchJql(jql) {
|
|
|
271
366
|
return [...new Set(found)];
|
|
272
367
|
}
|
|
273
368
|
|
|
274
|
-
async function fetchBacklogPageWithWait(url) {
|
|
369
|
+
async function fetchBacklogPageWithWait(url, cookie) {
|
|
275
370
|
const deadline = Date.now() + opts.waitSec * 1000;
|
|
276
371
|
let last = '';
|
|
277
372
|
while (Date.now() < deadline) {
|
|
278
373
|
try {
|
|
279
|
-
const
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
} else {
|
|
283
|
-
const result = await fetchJson(url, cookie, 'application/json');
|
|
284
|
-
if (result.status === 200 && result.json && Array.isArray(result.json.issues)) return result.json;
|
|
285
|
-
last = `HTTP ${result.status} ${(result.text || '').slice(0, 180).replace(/\s+/g, ' ')}`;
|
|
286
|
-
}
|
|
374
|
+
const result = await fetchJson(url, cookie, 'application/json');
|
|
375
|
+
if (result.status === 200 && result.json && Array.isArray(result.json.issues)) return result.json;
|
|
376
|
+
last = `HTTP ${result.status} ${(result.text || '').slice(0, 180).replace(/\s+/g, ' ')}`;
|
|
287
377
|
} catch (e) { last = e.message; }
|
|
288
|
-
process.stdout.write(`\r${new Date().toLocaleTimeString()} waiting for
|
|
378
|
+
process.stdout.write(`\r${new Date().toLocaleTimeString()} waiting for Jira backlog access: ${last.padEnd(120).slice(0, 120)}`);
|
|
289
379
|
await sleep(3000);
|
|
290
380
|
}
|
|
291
381
|
process.stdout.write('\n');
|
|
@@ -294,8 +384,7 @@ async function fetchBacklogPageWithWait(url) {
|
|
|
294
384
|
|
|
295
385
|
async function searchBacklog(input) {
|
|
296
386
|
const backlog = parseBacklogInput(input, opts.server);
|
|
297
|
-
await
|
|
298
|
-
console.log(`If prompted in Chrome, complete SSO for: ${backlog.browseUrl}`);
|
|
387
|
+
const cookie = await getCookieWithWait(backlog.browseUrl);
|
|
299
388
|
console.log(`Waiting up to ${opts.waitSec}s for Jira backlog access...`);
|
|
300
389
|
|
|
301
390
|
const found = [];
|
|
@@ -305,7 +394,7 @@ async function searchBacklog(input) {
|
|
|
305
394
|
while (found.length < opts.maxSearchResults) {
|
|
306
395
|
const limit = Math.min(pageSize, opts.maxSearchResults - found.length);
|
|
307
396
|
const url = backlogApiUrl(opts.server, backlog.boardId, startAt, limit);
|
|
308
|
-
const page = await fetchBacklogPageWithWait(url);
|
|
397
|
+
const page = await fetchBacklogPageWithWait(url, cookie);
|
|
309
398
|
const keys = issueKeysFromAgilePage(page);
|
|
310
399
|
for (const key of keys) found.push(key);
|
|
311
400
|
console.log(`Fetched backlog page board=${backlog.boardId} startAt=${startAt}, issues=${keys.length}${typeof page.total === 'number' ? `, total=${page.total}` : ''}`);
|
|
@@ -426,10 +515,15 @@ async function downloadAttachments(issueJson, cookie, outDir) {
|
|
|
426
515
|
|
|
427
516
|
async function ensureBrowser(browseUrl) {
|
|
428
517
|
if (!(await devtoolsReady())) {
|
|
429
|
-
console.log(`Opening
|
|
518
|
+
console.log(`Opening Chromium-compatible browser with reusable profile: ${opts.profileDir}`);
|
|
430
519
|
launchChrome(browseUrl);
|
|
431
520
|
} else {
|
|
432
521
|
console.log(`Reusing Chrome DevTools on port ${opts.port}`);
|
|
522
|
+
if (browseUrl) {
|
|
523
|
+
const opened = await openDevtoolsTab(browseUrl);
|
|
524
|
+
if (opened) console.log(`Opened target URL in reused browser: ${browseUrl}`);
|
|
525
|
+
else console.warn(`Could not open target URL through DevTools; continuing with existing tabs.`);
|
|
526
|
+
}
|
|
433
527
|
}
|
|
434
528
|
await waitDevtools();
|
|
435
529
|
}
|
|
@@ -443,32 +537,11 @@ async function fetchIssue(issue) {
|
|
|
443
537
|
const remoteLinksUrl = `${opts.server}/rest/api/3/issue/${issue}/remotelink`;
|
|
444
538
|
const xmlUrl = `${opts.server}/si/jira.issueviews:issue-xml/${issue}/${issue}.xml`;
|
|
445
539
|
|
|
446
|
-
await
|
|
447
|
-
console.log(`If prompted in Chrome, complete SSO for: ${browseUrl}`);
|
|
448
|
-
console.log(`Waiting up to ${opts.waitSec}s for Jira session...`);
|
|
449
|
-
|
|
450
|
-
const deadline = Date.now() + opts.waitSec * 1000;
|
|
451
|
-
let last = '';
|
|
452
|
-
let cookie = '';
|
|
453
|
-
let rest = null;
|
|
454
|
-
|
|
455
|
-
while (Date.now() < deadline) {
|
|
456
|
-
try {
|
|
457
|
-
cookie = await getCookieHeader();
|
|
458
|
-
if (cookie) {
|
|
459
|
-
rest = await fetchText(restUrl, cookie, 'application/json');
|
|
460
|
-
const body = rest.text || '';
|
|
461
|
-
if (rest.status === 200 && body.includes(`"key":"${issue}"`)) break;
|
|
462
|
-
last = `HTTP ${rest.status} ${body.slice(0, 110).replace(/\s+/g, ' ')}`;
|
|
463
|
-
} else last = 'no Jira cookies yet';
|
|
464
|
-
} catch (e) { last = e.message; }
|
|
465
|
-
process.stdout.write(`\r${new Date().toLocaleTimeString()} ${last.padEnd(120).slice(0, 120)}`);
|
|
466
|
-
await sleep(3000);
|
|
467
|
-
}
|
|
468
|
-
process.stdout.write('\n');
|
|
540
|
+
const cookie = await getCookieWithWait(browseUrl);
|
|
469
541
|
|
|
470
|
-
|
|
471
|
-
|
|
542
|
+
const rest = await fetchJson(restUrl, cookie, 'application/json');
|
|
543
|
+
if (rest.status !== 200 || !rest.json || rest.json.key !== issue) {
|
|
544
|
+
throw new Error(`Could not fetch ${issue}. HTTP ${rest.status}: ${(rest.text || '').slice(0, 300).replace(/\s+/g, ' ')}`);
|
|
472
545
|
}
|
|
473
546
|
|
|
474
547
|
await fsp.writeFile(path.join(outDir, 'issue.json'), rest.text);
|