@aholbreich/agent-skills 0.4.0 → 0.5.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,18 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.5.0 - 2026-05-07
4
4
 
5
5
  Added:
6
6
 
7
+ - `confluence-browser-fetch` now verifies an authenticated Confluence REST session before fetching pages, avoiding false positives from Atlassian login-page cookies.
8
+ - `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
9
  - `jira-browser-fetch --backlog URL|BOARD_ID` to fetch all issues from a Jira Software board backlog through the authenticated browser session.
8
10
  - Backlog manifests at `raw/jira-board-<board-id>-backlog.json` and a `backlogs` section in `raw/jira-browser-fetch-run.json`.
9
11
  - Documentation examples for natural-language user requests that should invoke the skills.
10
12
  - Recommended `npx skills add aholbreich/agent-skills -g` cross-agent install path, plus collision/update guidance for Pi and project-local overrides.
11
13
  - CI/package dry-run scripts that use `npm pack --dry-run` for compatibility with older local pnpm launchers.
14
+ - `agent-skills install --skill NAME` and `--pick` to install only selected bundled skills from the fallback npx installer.
15
+ - 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
16
 
13
17
  ## 0.1.0 - 2026-05-06
14
18
 
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
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
 
@@ -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.5.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
 
@@ -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
@@ -113,7 +114,7 @@ By default, pages with matching local `metadata.json` Confluence `version.number
113
114
  | `CONFLUENCE_REQUEST_TIMEOUT_SEC` | Per-request timeout, default `60` |
114
115
  | `CONFLUENCE_SKIP_UNCHANGED` | Set to `0` to disable default skip-unchanged behavior |
115
116
  | `CONFLUENCE_CHROME_PROFILE` | Dedicated Chrome profile dir |
116
- | `CHROME` | Chrome executable path |
117
+ | `CHROME` / `CHROMIUM` | Browser executable path override |
117
118
 
118
119
  ## Output Files
119
120
 
@@ -129,13 +130,17 @@ For each page:
129
130
 
130
131
  ## Troubleshooting
131
132
 
132
- ### `no Atlassian cookies yet`
133
+ ### `no Atlassian cookies yet` / `not authenticated yet`
133
134
 
134
- Complete SSO in the Chrome window opened by the script.
135
+ 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.
136
+
137
+ ### `Could not verify authenticated Confluence session`
138
+
139
+ 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
140
 
136
141
  ### `Page failed HTTP 404`
137
142
 
138
- The authenticated user cannot see the page, or the page ID/site is wrong.
143
+ After authentication is verified, this usually means the authenticated user cannot see the page, or the page ID/site is wrong.
139
144
 
140
145
  ### URL cannot be resolved
141
146
 
@@ -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');
@@ -115,8 +116,50 @@ async function waitDevtools() {
115
116
  throw new Error('Chrome DevTools endpoint did not start');
116
117
  }
117
118
 
119
+ function isExecutable(file) {
120
+ try { fs.accessSync(file, fs.constants.X_OK); return true; } catch { return false; }
121
+ }
122
+
123
+ function resolveBrowserCandidate(candidate) {
124
+ if (!candidate) return null;
125
+ if (candidate.includes(path.sep)) return isExecutable(candidate) ? candidate : null;
126
+ for (const dir of String(process.env.PATH || '').split(path.delimiter)) {
127
+ if (!dir) continue;
128
+ const full = path.join(dir, candidate);
129
+ if (isExecutable(full)) return full;
130
+ }
131
+ return null;
132
+ }
133
+
134
+ function findBrowserExecutable() {
135
+ const candidates = [
136
+ process.env.CHROME,
137
+ process.env.CHROMIUM,
138
+ 'google-chrome',
139
+ 'google-chrome-stable',
140
+ 'chromium',
141
+ 'chromium-browser',
142
+ 'brave-browser',
143
+ 'brave',
144
+ 'microsoft-edge',
145
+ 'microsoft-edge-stable',
146
+ 'vivaldi',
147
+ 'vivaldi-stable',
148
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
149
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
150
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
151
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
152
+ '/Applications/Vivaldi.app/Contents/MacOS/Vivaldi',
153
+ ];
154
+ for (const candidate of candidates) {
155
+ const resolved = resolveBrowserCandidate(candidate);
156
+ if (resolved) return resolved;
157
+ }
158
+ throw new Error('Could not find a Chromium-compatible browser. Install Chrome/Chromium/Brave/Edge/Vivaldi or set CHROME=/path/to/browser.');
159
+ }
160
+
118
161
  function launchChrome(url) {
119
- const chrome = process.env.CHROME || '/usr/bin/google-chrome';
162
+ const browser = findBrowserExecutable();
120
163
  const args = [
121
164
  `--remote-debugging-port=${opts.port}`,
122
165
  '--remote-debugging-address=127.0.0.1',
@@ -126,13 +169,15 @@ function launchChrome(url) {
126
169
  '--no-default-browser-check',
127
170
  url,
128
171
  ];
129
- const child = spawn(chrome, args, { detached: true, stdio: 'ignore' });
172
+ console.log(`Launching browser: ${browser}`);
173
+ const child = spawn(browser, args, { detached: true, stdio: 'ignore' });
174
+ child.on('error', err => console.error(`Failed to launch browser ${browser}: ${err.message}`));
130
175
  child.unref();
131
176
  }
132
177
 
133
178
  async function ensureBrowser(openUrl) {
134
179
  if (!(await devtoolsReady())) {
135
- console.log(`Opening Chrome with reusable profile: ${opts.profileDir}`);
180
+ console.log(`Opening Chromium-compatible browser with reusable profile: ${opts.profileDir}`);
136
181
  launchChrome(openUrl || wikiBase);
137
182
  } else {
138
183
  console.log(`Reusing Chrome DevTools on port ${opts.port}`);
@@ -246,6 +291,30 @@ async function fetchJson(url, cookie) {
246
291
  return { ...result, json };
247
292
  }
248
293
 
294
+ async function verifyConfluenceSession(cookie) {
295
+ if (!cookie) return { ok: false, message: 'no Atlassian cookies yet' };
296
+
297
+ const probes = [
298
+ `${wikiBase}/rest/api/user/current`,
299
+ `${wikiBase}/rest/api/space?limit=1`,
300
+ ];
301
+
302
+ for (const url of probes) {
303
+ const result = await fetchJson(url, cookie);
304
+ if (result.status === 200 && result.json) return { ok: true, url };
305
+ if (result.status === 401 || result.status === 403) {
306
+ return { ok: false, message: `not authenticated yet (${result.status} from ${url})` };
307
+ }
308
+ if (result.status === 302 || result.status === 303) {
309
+ return { ok: false, message: `still redirected to login (${result.status} from ${url})` };
310
+ }
311
+ if (result.status === 404) continue;
312
+ return { ok: false, message: `session probe HTTP ${result.status} from ${url}` };
313
+ }
314
+
315
+ return { ok: false, message: 'could not verify Confluence session' };
316
+ }
317
+
249
318
  async function getCookieWithWait(openUrl) {
250
319
  await ensureBrowser(openUrl || wikiBase);
251
320
  console.log(`If prompted in Chrome, complete SSO for: ${openUrl || wikiBase}`);
@@ -254,14 +323,19 @@ async function getCookieWithWait(openUrl) {
254
323
  while (Date.now() < deadline) {
255
324
  try {
256
325
  const cookie = await getCookieHeader();
257
- if (cookie) return cookie;
258
- last = 'no Atlassian cookies yet';
326
+ const session = await verifyConfluenceSession(cookie);
327
+ if (session.ok) {
328
+ process.stdout.write('\n');
329
+ console.log(`Authenticated Confluence session verified via ${session.url}`);
330
+ return cookie;
331
+ }
332
+ last = session.message;
259
333
  } catch (e) { last = e.message; }
260
334
  process.stdout.write(`\r${new Date().toLocaleTimeString()} ${last.padEnd(120).slice(0, 120)}`);
261
335
  await sleep(3000);
262
336
  }
263
337
  process.stdout.write('\n');
264
- throw new Error(`Could not get browser cookies. Last result: ${last}`);
338
+ throw new Error(`Could not verify authenticated Confluence session. Last result: ${last}`);
265
339
  }
266
340
 
267
341
  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
 
@@ -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
@@ -125,7 +126,7 @@ Default max attachment download size is `5mb`. Use `--max-attachment-size unlimi
125
126
  | `JIRA_MAX_SEARCH_RESULTS` | Max issues added per JQL or backlog search, default `1000` |
126
127
  | `JIRA_MAX_ATTACHMENT_SIZE` / `JIRA_MAX_ATTACHMENT_BYTES` | Max attachment download size, default `5mb`; skipped files are listed in `attachments.json` |
127
128
  | `JIRA_CHROME_PROFILE` | Dedicated Chrome profile dir |
128
- | `CHROME` | Chrome executable path |
129
+ | `CHROME` / `CHROMIUM` | Browser executable path override |
129
130
 
130
131
  ## Example user requests
131
132
 
@@ -139,17 +140,21 @@ Agents should invoke this skill for requests such as:
139
140
 
140
141
  ## Troubleshooting
141
142
 
142
- ### `no Jira cookies yet`
143
+ ### `no Atlassian cookies yet` / `not authenticated yet`
143
144
 
144
- Complete SSO in the Chrome window opened by the script.
145
+ 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.
146
+
147
+ ### `Could not verify authenticated Jira session`
148
+
149
+ 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
150
 
146
151
  ### `HTTP 404 Issue does not exist or you do not have permission`
147
152
 
148
153
  The session works, but the account cannot see the issue or the key is not a Jira issue.
149
154
 
150
- ### Chrome does not open
155
+ ### Browser does not open
151
156
 
152
- Set the executable path:
157
+ 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
158
 
154
159
  ```bash
155
160
  CHROME=/usr/bin/chromium scripts/jira-browser-fetch.js PROJ-123
@@ -128,8 +128,50 @@ async function waitDevtools() {
128
128
  throw new Error('Chrome DevTools endpoint did not start');
129
129
  }
130
130
 
131
+ function isExecutable(file) {
132
+ try { fs.accessSync(file, fs.constants.X_OK); return true; } catch { return false; }
133
+ }
134
+
135
+ function resolveBrowserCandidate(candidate) {
136
+ if (!candidate) return null;
137
+ if (candidate.includes(path.sep)) return isExecutable(candidate) ? candidate : null;
138
+ for (const dir of String(process.env.PATH || '').split(path.delimiter)) {
139
+ if (!dir) continue;
140
+ const full = path.join(dir, candidate);
141
+ if (isExecutable(full)) return full;
142
+ }
143
+ return null;
144
+ }
145
+
146
+ function findBrowserExecutable() {
147
+ const candidates = [
148
+ process.env.CHROME,
149
+ process.env.CHROMIUM,
150
+ 'google-chrome',
151
+ 'google-chrome-stable',
152
+ 'chromium',
153
+ 'chromium-browser',
154
+ 'brave-browser',
155
+ 'brave',
156
+ 'microsoft-edge',
157
+ 'microsoft-edge-stable',
158
+ 'vivaldi',
159
+ 'vivaldi-stable',
160
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
161
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
162
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
163
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
164
+ '/Applications/Vivaldi.app/Contents/MacOS/Vivaldi',
165
+ ];
166
+ for (const candidate of candidates) {
167
+ const resolved = resolveBrowserCandidate(candidate);
168
+ if (resolved) return resolved;
169
+ }
170
+ throw new Error('Could not find a Chromium-compatible browser. Install Chrome/Chromium/Brave/Edge/Vivaldi or set CHROME=/path/to/browser.');
171
+ }
172
+
131
173
  function launchChrome(url) {
132
- const chrome = process.env.CHROME || '/usr/bin/google-chrome';
174
+ const browser = findBrowserExecutable();
133
175
  const args = [
134
176
  `--remote-debugging-port=${opts.port}`,
135
177
  '--remote-debugging-address=127.0.0.1',
@@ -139,7 +181,9 @@ function launchChrome(url) {
139
181
  '--no-default-browser-check',
140
182
  url,
141
183
  ];
142
- const child = spawn(chrome, args, { detached: true, stdio: 'ignore' });
184
+ console.log(`Launching browser: ${browser}`);
185
+ const child = spawn(browser, args, { detached: true, stdio: 'ignore' });
186
+ child.on('error', err => console.error(`Failed to launch browser ${browser}: ${err.message}`));
143
187
  child.unref();
144
188
  }
145
189
 
@@ -223,21 +267,57 @@ async function fetchJson(url, cookie, accept) {
223
267
  return { ...result, json };
224
268
  }
225
269
 
270
+ async function verifyJiraSession(cookie) {
271
+ if (!cookie) return { ok: false, message: 'no Atlassian cookies yet' };
272
+
273
+ const probes = [
274
+ `${opts.server}/rest/api/3/myself`,
275
+ `${opts.server}/rest/api/2/myself`,
276
+ ];
277
+
278
+ for (const url of probes) {
279
+ const result = await fetchJson(url, cookie, 'application/json');
280
+ if (result.status === 200 && result.json && (result.json.accountId || result.json.name || result.json.key || result.json.displayName)) {
281
+ return { ok: true, url };
282
+ }
283
+ if (result.status === 200) {
284
+ const kind = result.json ? 'unexpected JSON response' : (/html/i.test(result.contentType) ? 'login page' : 'non-JSON response');
285
+ return { ok: false, message: `not authenticated yet (${kind} from ${url})` };
286
+ }
287
+ if (result.status === 401 || result.status === 403) {
288
+ return { ok: false, message: `not authenticated yet (${result.status} from ${url})` };
289
+ }
290
+ if (result.status === 302 || result.status === 303) {
291
+ return { ok: false, message: `still redirected to login (${result.status} from ${url})` };
292
+ }
293
+ if (result.status === 404) continue;
294
+ return { ok: false, message: `session probe HTTP ${result.status} from ${url}` };
295
+ }
296
+
297
+ return { ok: false, message: 'could not verify Jira session' };
298
+ }
299
+
226
300
  async function getCookieWithWait(openUrl) {
227
301
  await ensureBrowser(openUrl || `${opts.server}/`);
302
+ console.log(`If prompted in Chrome, complete SSO for: ${openUrl || opts.server}`);
228
303
  const deadline = Date.now() + opts.waitSec * 1000;
229
304
  let last = '';
230
305
  while (Date.now() < deadline) {
231
306
  try {
232
307
  const cookie = await getCookieHeader();
233
- if (cookie) return cookie;
234
- last = 'no Jira cookies yet';
308
+ const session = await verifyJiraSession(cookie);
309
+ if (session.ok) {
310
+ process.stdout.write('\n');
311
+ console.log(`Authenticated Jira session verified via ${session.url}`);
312
+ return cookie;
313
+ }
314
+ last = session.message;
235
315
  } catch (e) { last = e.message; }
236
316
  process.stdout.write(`\r${new Date().toLocaleTimeString()} ${last.padEnd(120).slice(0, 120)}`);
237
317
  await sleep(3000);
238
318
  }
239
319
  process.stdout.write('\n');
240
- throw new Error(`Could not get Jira browser cookies. Last result: ${last}`);
320
+ throw new Error(`Could not verify authenticated Jira session. Last result: ${last}`);
241
321
  }
242
322
 
243
323
  async function searchJql(jql) {
@@ -271,21 +351,16 @@ async function searchJql(jql) {
271
351
  return [...new Set(found)];
272
352
  }
273
353
 
274
- async function fetchBacklogPageWithWait(url) {
354
+ async function fetchBacklogPageWithWait(url, cookie) {
275
355
  const deadline = Date.now() + opts.waitSec * 1000;
276
356
  let last = '';
277
357
  while (Date.now() < deadline) {
278
358
  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
- }
359
+ const result = await fetchJson(url, cookie, 'application/json');
360
+ if (result.status === 200 && result.json && Array.isArray(result.json.issues)) return result.json;
361
+ last = `HTTP ${result.status} ${(result.text || '').slice(0, 180).replace(/\s+/g, ' ')}`;
287
362
  } 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)}`);
363
+ process.stdout.write(`\r${new Date().toLocaleTimeString()} waiting for Jira backlog access: ${last.padEnd(120).slice(0, 120)}`);
289
364
  await sleep(3000);
290
365
  }
291
366
  process.stdout.write('\n');
@@ -294,8 +369,7 @@ async function fetchBacklogPageWithWait(url) {
294
369
 
295
370
  async function searchBacklog(input) {
296
371
  const backlog = parseBacklogInput(input, opts.server);
297
- await ensureBrowser(backlog.browseUrl);
298
- console.log(`If prompted in Chrome, complete SSO for: ${backlog.browseUrl}`);
372
+ const cookie = await getCookieWithWait(backlog.browseUrl);
299
373
  console.log(`Waiting up to ${opts.waitSec}s for Jira backlog access...`);
300
374
 
301
375
  const found = [];
@@ -305,7 +379,7 @@ async function searchBacklog(input) {
305
379
  while (found.length < opts.maxSearchResults) {
306
380
  const limit = Math.min(pageSize, opts.maxSearchResults - found.length);
307
381
  const url = backlogApiUrl(opts.server, backlog.boardId, startAt, limit);
308
- const page = await fetchBacklogPageWithWait(url);
382
+ const page = await fetchBacklogPageWithWait(url, cookie);
309
383
  const keys = issueKeysFromAgilePage(page);
310
384
  for (const key of keys) found.push(key);
311
385
  console.log(`Fetched backlog page board=${backlog.boardId} startAt=${startAt}, issues=${keys.length}${typeof page.total === 'number' ? `, total=${page.total}` : ''}`);
@@ -426,7 +500,7 @@ async function downloadAttachments(issueJson, cookie, outDir) {
426
500
 
427
501
  async function ensureBrowser(browseUrl) {
428
502
  if (!(await devtoolsReady())) {
429
- console.log(`Opening Chrome with reusable profile: ${opts.profileDir}`);
503
+ console.log(`Opening Chromium-compatible browser with reusable profile: ${opts.profileDir}`);
430
504
  launchChrome(browseUrl);
431
505
  } else {
432
506
  console.log(`Reusing Chrome DevTools on port ${opts.port}`);
@@ -443,32 +517,11 @@ async function fetchIssue(issue) {
443
517
  const remoteLinksUrl = `${opts.server}/rest/api/3/issue/${issue}/remotelink`;
444
518
  const xmlUrl = `${opts.server}/si/jira.issueviews:issue-xml/${issue}/${issue}.xml`;
445
519
 
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');
520
+ const cookie = await getCookieWithWait(browseUrl);
469
521
 
470
- if (!rest || rest.status !== 200 || !rest.text.includes(`"key":"${issue}"`)) {
471
- throw new Error(`Could not fetch ${issue}. Last result: ${last}`);
522
+ const rest = await fetchJson(restUrl, cookie, 'application/json');
523
+ if (rest.status !== 200 || !rest.json || rest.json.key !== issue) {
524
+ throw new Error(`Could not fetch ${issue}. HTTP ${rest.status}: ${(rest.text || '').slice(0, 300).replace(/\s+/g, ' ')}`);
472
525
  }
473
526
 
474
527
  await fsp.writeFile(path.join(outDir, 'issue.json'), rest.text);