@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 CHANGED
@@ -1,14 +1,25 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
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
- - Google Chrome or Chromium.
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 Chrome/Chromium with a dedicated local profile,
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. call Atlassian REST endpoints with those cookies.
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
@@ -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 skills = await listSkills();
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.4.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 Google Chrome/Chromium with remote debugging. No npm dependencies.
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. Verify saved files.
53
- 6. If this is an LLM wiki ingest, process the saved `raw/confluence/...` material into `wiki/` per the project `AGENTS.md`.
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. Calling Confluence REST endpoints with those cookies.
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
- - Google Chrome or Chromium.
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 Chrome has a different path:
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
- | `CHROME` | Chrome executable path |
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 in the Chrome window opened by the script.
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
- The authenticated user cannot see the page, or the page ID/site is wrong.
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 chrome = process.env.CHROME || '/usr/bin/google-chrome';
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
- const child = spawn(chrome, args, { detached: true, stdio: 'ignore' });
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 Chrome with reusable profile: ${opts.profileDir}`);
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
- if (cookie) return cookie;
258
- last = 'no Atlassian cookies yet';
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 get browser cookies. Last result: ${last}`);
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 Google Chrome/Chromium with remote debugging. No npm dependencies.
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. Verify saved files.
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. Calling Jira REST endpoints with those cookies.
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 Chromium.
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 Chrome has a different path:
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
- | `CHROME` | Chrome executable path |
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 Jira cookies yet`
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 in the Chrome window opened by the script.
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
- ### Chrome does not open
166
+ ### Browser does not open
151
167
 
152
- Set the executable path:
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 chrome = process.env.CHROME || '/usr/bin/google-chrome';
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
- const child = spawn(chrome, args, { detached: true, stdio: 'ignore' });
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
- if (cookie) return cookie;
234
- last = 'no Jira cookies yet';
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 get Jira browser cookies. Last result: ${last}`);
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 cookie = await getCookieHeader();
280
- if (!cookie) {
281
- last = 'no Jira cookies yet';
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 authenticated Jira backlog session: ${last.padEnd(120).slice(0, 120)}`);
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 ensureBrowser(backlog.browseUrl);
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 Chrome with reusable profile: ${opts.profileDir}`);
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 ensureBrowser(browseUrl);
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
- if (!rest || rest.status !== 200 || !rest.text.includes(`"key":"${issue}"`)) {
471
- throw new Error(`Could not fetch ${issue}. Last result: ${last}`);
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);