@aholbreich/agent-skills 0.3.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,12 +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.
12
+ - Recommended `npx skills add aholbreich/agent-skills -g` cross-agent install path, plus collision/update guidance for Pi and project-local overrides.
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.
10
16
 
11
17
  ## 0.1.0 - 2026-05-06
12
18
 
package/COMPATIBILITY.md CHANGED
@@ -4,96 +4,119 @@ This package is designed as a pure [Agent Skills](https://agentskills.io/) packa
4
4
 
5
5
  Each bundled skill is a directory containing `SKILL.md` plus scripts and references. The frontmatter follows the Agent Skills conventions: required `name` and `description`, directory name matching the skill name, lowercase hyphenated names, and optional `license`/`compatibility` metadata.
6
6
 
7
- ## Compatibility matrix
8
-
9
- | Tool / Harness | Status | Install method |
10
- |---|---:|---|
11
- | Pi | Supported and tested | `pi install npm:@aholbreich/agent-skills` or `npx @aholbreich/agent-skills --target pi` |
12
- | Claude Code | Compatible Agent Skills layout | `npx @aholbreich/agent-skills --target claude` or copy `skills/*` into Claude's skills directory |
13
- | OpenAI Codex | Compatible Agent Skills layout | `npx @aholbreich/agent-skills --target codex` or copy `skills/*` into Codex's skills directory |
14
- | OpenClaw / generic Agent Skills harnesses | Compatible Agent Skills layout | `npx @aholbreich/agent-skills --target agents` or copy `skills/*` into `.agents/skills` / configured skills directory |
15
- | Any Agent Skills-compatible tool | Compatible layout | Copy each folder under `skills/` into the tool's configured skills directory |
7
+ ## Recommended installer
16
8
 
17
- ## Install commands
18
-
19
- ### Generic Agent Skills default
9
+ For cross-agent installation, prefer the open Skills CLI:
20
10
 
21
11
  ```bash
22
- npx @aholbreich/agent-skills
12
+ npx skills add aholbreich/agent-skills -g
23
13
  ```
24
14
 
25
- This installs to `~/.agents/skills` and is equivalent to:
15
+ The Skills CLI discovers `skills/*/SKILL.md`, supports many agent clients, and symlinks agent-specific installs to a managed source by default. Use `--copy` only when symlinks are not supported.
16
+
17
+ Useful variants:
26
18
 
27
19
  ```bash
28
- npx @aholbreich/agent-skills --target agents
20
+ npx skills add aholbreich/agent-skills --list
21
+ npx skills add aholbreich/agent-skills # project-local/team install
22
+ npx skills add aholbreich/agent-skills -g -y # non-interactive global install
23
+ npx skills update -g # update global skills installed by the Skills CLI
24
+ npx skills list -g # list global skills
29
25
  ```
30
26
 
31
- ### Pi global
27
+ ## Compatibility matrix
32
28
 
33
- ```bash
34
- pi install npm:@aholbreich/agent-skills
35
- ```
29
+ | Tool / Harness | Status | Recommended install method |
30
+ |---|---:|---|
31
+ | Pi | Supported and tested | `pi install npm:@aholbreich/agent-skills` for Pi-managed package updates, or `npx skills add aholbreich/agent-skills -g` for cross-agent installs |
32
+ | Claude Code | Compatible Agent Skills layout | `npx skills add aholbreich/agent-skills -g --agent claude-code` |
33
+ | OpenAI Codex | Compatible Agent Skills layout | `npx skills add aholbreich/agent-skills -g --agent codex` |
34
+ | OpenClaw / generic Agent Skills harnesses | Compatible Agent Skills layout | `npx skills add aholbreich/agent-skills -g` or install to `.agents/skills` |
35
+ | Any Agent Skills-compatible tool | Compatible layout | Copy or symlink each folder under `skills/` into the tool's configured skills directory |
36
+
37
+ ## Pi-native install commands
36
38
 
37
- or:
39
+ Pi can install this repository as a Pi package directly from npm:
38
40
 
39
41
  ```bash
40
- npx @aholbreich/agent-skills --target pi
42
+ pi install npm:@aholbreich/agent-skills
41
43
  ```
42
44
 
43
- ### Pi project-local
45
+ Project-local Pi package install:
44
46
 
45
47
  ```bash
46
48
  pi install -l npm:@aholbreich/agent-skills
47
49
  ```
48
50
 
49
- or:
51
+ Temporary Pi run without installing:
50
52
 
51
53
  ```bash
52
- npx @aholbreich/agent-skills --target project
54
+ pi -e npm:@aholbreich/agent-skills
53
55
  ```
54
56
 
55
- ### Claude Code
57
+ ## Package fallback installer
58
+
59
+ The npm package also ships a small dependency-free installer for environments where the Skills CLI is not available:
56
60
 
57
61
  ```bash
58
- npx @aholbreich/agent-skills --target claude
62
+ npx @aholbreich/agent-skills
59
63
  ```
60
64
 
61
- Project-local Claude-style install:
65
+ This installs to `~/.agents/skills` and is equivalent to:
62
66
 
63
67
  ```bash
64
- npx @aholbreich/agent-skills --target project-claude
68
+ npx @aholbreich/agent-skills --target agents
65
69
  ```
66
70
 
67
- ### Codex
71
+ Other targets:
68
72
 
69
73
  ```bash
74
+ npx @aholbreich/agent-skills --target pi
75
+ npx @aholbreich/agent-skills --target claude
70
76
  npx @aholbreich/agent-skills --target codex
77
+ npx @aholbreich/agent-skills --target project
78
+ npx @aholbreich/agent-skills --target project-agents
79
+ npx @aholbreich/agent-skills install --dir /path/to/skills
71
80
  ```
72
81
 
73
- Project-local Codex-style install:
82
+ Select one or more skills with `--skill`, or use the interactive picker:
74
83
 
75
84
  ```bash
76
- npx @aholbreich/agent-skills --target project-codex
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
77
88
  ```
78
89
 
79
- ### Generic `.agents/skills`
90
+ This fallback copies files. For symlinked, multi-agent installs, prefer `npx skills add aholbreich/agent-skills`.
80
91
 
81
- ```bash
82
- npx @aholbreich/agent-skills --target agents
83
- ```
92
+ ## Collision behavior
84
93
 
85
- Project-local generic install:
94
+ Agent Skills are identified by their `name` frontmatter. If the same skill name exists in more than one discovered location, agents apply their own precedence rules.
86
95
 
87
- ```bash
88
- npx @aholbreich/agent-skills --target project-agents
96
+ Common precedence pattern:
97
+
98
+ 1. Project-local skills override user/global skills.
99
+ 2. User/global skills override bundled/system skills.
100
+ 3. Duplicate names are not merged.
101
+
102
+ Pi example:
103
+
104
+ ```text
105
+ .pi/skills/jira-browser-fetch/SKILL.md
89
106
  ```
90
107
 
91
- ### Custom skills directory
108
+ shadows:
92
109
 
93
- ```bash
94
- npx @aholbreich/agent-skills install --dir /path/to/skills
110
+ ```text
111
+ ~/.nvm/.../@aholbreich/agent-skills/skills/jira-browser-fetch/SKILL.md
95
112
  ```
96
113
 
114
+ That is useful for intentional repo-specific overrides, but it also means package updates may not affect the active skill in that repository. If you see a collision warning, choose one source of truth:
115
+
116
+ - Keep the project-local skill if the repository intentionally customizes it.
117
+ - Remove the project-local copy if you want the package/global install to be active.
118
+ - Re-run the installer/update command for the install method that owns the active copy.
119
+
97
120
  ## Discoverability
98
121
 
99
122
  The package is tagged for discovery with npm keywords including:
@@ -106,7 +129,11 @@ The package is tagged for discovery with npm keywords including:
106
129
  - `claude-code`
107
130
  - `codex`
108
131
 
109
- After publishing to npm, tools and indexes that crawl npm packages for Agent Skills-compatible packages, such as skills registries, should be able to discover the package from its package metadata and conventional `skills/` directory.
132
+ The repository is also compatible with the Skills CLI GitHub shorthand:
133
+
134
+ ```bash
135
+ npx skills add aholbreich/agent-skills --list
136
+ ```
110
137
 
111
138
  If a registry requires manual submission, use:
112
139
 
@@ -114,6 +141,7 @@ If a registry requires manual submission, use:
114
141
  Package: @aholbreich/agent-skills
115
142
  Repository: https://github.com/aholbreich/agent-skills
116
143
  Skills directory: skills/
144
+ Install command: npx skills add aholbreich/agent-skills -g
117
145
  ```
118
146
 
119
147
  ## Compliance checks in this repo
@@ -129,6 +157,6 @@ That includes:
129
157
  - JavaScript syntax checks.
130
158
  - Unit tests.
131
159
  - Skill frontmatter compliance checks.
132
- - `pnpm pack --dry-run` package content check.
160
+ - `npm pack --dry-run` package content check.
133
161
 
134
162
  The compliance tests are intentionally local and dependency-free; they validate the parts of the Agent Skills structure that matter for broad tool compatibility.
package/README.md CHANGED
@@ -15,49 +15,80 @@ This repository is a pure skills package. It currently contains browser-authenti
15
15
 
16
16
  This repository follows the Agent Skills directory convention: each skill lives under `skills/<skill-name>/SKILL.md` with matching frontmatter.
17
17
 
18
- Supported install targets:
18
+ Recommended install paths:
19
19
 
20
- | Target | Command |
20
+ | Use case | Command |
21
21
  |---|---|
22
- | Pi | `pi install npm:@aholbreich/agent-skills` |
23
- | Pi via npx | `npx @aholbreich/agent-skills --target pi` |
24
- | Claude Code-style global skills | `npx @aholbreich/agent-skills --target claude` |
25
- | Codex-style global skills | `npx @aholbreich/agent-skills --target codex` |
26
- | OpenClaw / generic `.agents/skills` | `npx @aholbreich/agent-skills --target agents` |
27
- | Project-local generic skills | `npx @aholbreich/agent-skills --target project-agents` |
22
+ | Cross-agent wizard (recommended) | `npx skills add aholbreich/agent-skills -g` |
23
+ | Pi package-managed install | `pi install npm:@aholbreich/agent-skills` |
24
+ | Project-local/team skills | `npx skills add aholbreich/agent-skills` |
25
+ | Package fallback without the aggregator | `npx @aholbreich/agent-skills` |
28
26
 
29
- See [`COMPATIBILITY.md`](COMPATIBILITY.md) for details.
27
+ See [`COMPATIBILITY.md`](COMPATIBILITY.md) for details, including collision behavior.
30
28
 
31
29
  ## Requirements
32
30
 
33
31
  - Node.js `>=22`.
34
- - Google Chrome or Chromium.
32
+ - A Chromium-compatible browser: Chrome, Chromium, Brave, Edge, or Vivaldi.
35
33
  - Access to the Jira/Confluence site in the browser account you use.
36
34
  - Pi, or any Agent Skills-compatible harness, if you want skill discovery.
37
35
 
38
36
  No npm runtime dependencies are required.
39
37
 
40
- ## Install with Pi
38
+ ## Recommended install with the Skills CLI
41
39
 
42
- From GitHub:
40
+ For most users, use the open `skills` installer. It discovers the skills in this repository, prompts for compatible agents, and symlinks agent-specific installs to a single managed source by default.
41
+
42
+ Global/user install:
43
43
 
44
44
  ```bash
45
- pi install git:github.com/aholbreich/agent-skills
45
+ npx skills add aholbreich/agent-skills -g
46
46
  ```
47
47
 
48
48
  Project-local install, useful for teams:
49
49
 
50
50
  ```bash
51
- pi install -l git:github.com/aholbreich/agent-skills
51
+ npx skills add aholbreich/agent-skills
52
+ ```
53
+
54
+ List available skills without installing:
55
+
56
+ ```bash
57
+ npx skills add aholbreich/agent-skills --list
58
+ ```
59
+
60
+ Non-interactive examples:
61
+
62
+ ```bash
63
+ npx skills add aholbreich/agent-skills -g --skill '*' -y
64
+ npx skills add aholbreich/agent-skills -g --agent claude-code --agent codex --skill jira-browser-fetch -y
65
+ ```
66
+
67
+ Use `--copy` only when symlinks are not supported in your environment.
68
+
69
+ ## Pi-native install
70
+
71
+ If you only use Pi and want Pi to manage package updates, install the npm package directly:
72
+
73
+ ```bash
74
+ pi install npm:@aholbreich/agent-skills
75
+ ```
76
+
77
+ Project-local Pi package install, useful for teams that already standardize on Pi packages:
78
+
79
+ ```bash
80
+ pi install -l npm:@aholbreich/agent-skills
52
81
  ```
53
82
 
54
83
  Try without installing:
55
84
 
56
85
  ```bash
57
- pi -e git:github.com/aholbreich/agent-skills
86
+ pi -e npm:@aholbreich/agent-skills
58
87
  ```
59
88
 
60
- ## One-shot install with npx
89
+ ## Package fallback with npx
90
+
91
+ If you cannot use the `skills` aggregator, this package also ships a small installer. It copies bundled skills into a selected skills directory.
61
92
 
62
93
  Install bundled skills into the generic Agent Skills directory `~/.agents/skills`:
63
94
 
@@ -65,24 +96,27 @@ Install bundled skills into the generic Agent Skills directory `~/.agents/skills
65
96
  npx @aholbreich/agent-skills
66
97
  ```
67
98
 
68
- This is equivalent to:
99
+ Install for a specific target:
69
100
 
70
101
  ```bash
71
- npx @aholbreich/agent-skills --target agents
102
+ npx @aholbreich/agent-skills install --target agents
103
+ npx @aholbreich/agent-skills install --target claude
104
+ npx @aholbreich/agent-skills install --target codex
105
+ npx @aholbreich/agent-skills install --target project
72
106
  ```
73
107
 
74
- Install into a project-local `.pi/skills` directory:
108
+ Install only selected skills:
75
109
 
76
110
  ```bash
77
- npx @aholbreich/agent-skills install --target project
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
78
114
  ```
79
115
 
80
- Install for Claude Code, Codex, or generic Agent Skills harnesses:
116
+ Or use the dependency-free interactive picker:
81
117
 
82
118
  ```bash
83
- npx @aholbreich/agent-skills install --target claude
84
- npx @aholbreich/agent-skills install --target codex
85
- npx @aholbreich/agent-skills install --target agents
119
+ npx @aholbreich/agent-skills install --pick
86
120
  ```
87
121
 
88
122
  Overwrite existing installed skill directories:
@@ -97,6 +131,18 @@ List bundled skills:
97
131
  npx @aholbreich/agent-skills list
98
132
  ```
99
133
 
134
+ ## Collision and update notes
135
+
136
+ Avoid installing the same skill into multiple locations for the same agent unless you intentionally want one copy to shadow another. Most agents give project-local skills priority over user/global skills.
137
+
138
+ For example, in Pi a project skill at `.pi/skills/jira-browser-fetch/SKILL.md` shadows the same skill installed from `npm:@aholbreich/agent-skills`. In that case `pi update` updates the package, but the active project-local copy remains unchanged.
139
+
140
+ Recommended rule of thumb:
141
+
142
+ - Cross-agent users: prefer `npx skills add aholbreich/agent-skills -g`.
143
+ - Pi-only users: prefer `pi install npm:@aholbreich/agent-skills`.
144
+ - Team/repo-specific overrides: commit project-local skills intentionally and update them intentionally.
145
+
100
146
  ## Manual install
101
147
 
102
148
  ```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');
@@ -27,6 +28,11 @@ function usage() {
27
28
 
28
29
  Install this package's Agent Skills into a local skills directory.
29
30
 
31
+ Recommended cross-agent installer:
32
+ npx skills add aholbreich/agent-skills -g
33
+
34
+ This fallback installer copies files for environments where the Skills CLI is unavailable.
35
+
30
36
  Commands:
31
37
  install Install skills (default command)
32
38
  list List bundled skills
@@ -36,11 +42,16 @@ Commands:
36
42
  Options for install:
37
43
  --target NAME pi | agents | claude | codex | openclaw | project | project-agents | project-claude | project-codex (default: agents)
38
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
39
47
  --force Overwrite existing skill directories
40
48
  --dry-run Show what would be copied without writing
41
49
 
42
50
  Examples:
51
+ npx skills add aholbreich/agent-skills -g
43
52
  npx @aholbreich/agent-skills
53
+ npx @aholbreich/agent-skills install --skill jira-browser-fetch
54
+ npx @aholbreich/agent-skills install --pick
44
55
  npx @aholbreich/agent-skills install --target agents --force
45
56
  npx @aholbreich/agent-skills install --target pi --force
46
57
  npx @aholbreich/agent-skills install --target project
@@ -74,16 +85,72 @@ async function copyDir(src, dest) {
74
85
  await fsp.cp(src, dest, { recursive: true, force: true, errorOnExist: false });
75
86
  }
76
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
+
77
140
  async function install(args) {
78
141
  let target = 'agents';
79
142
  let customDir = '';
80
143
  let force = false;
81
144
  let dryRun = false;
145
+ let pick = false;
146
+ const skillFilters = [];
82
147
 
83
148
  for (let i = 0; i < args.length; i++) {
84
149
  const a = args[i];
85
150
  if (a === '--target') target = args[++i];
86
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;
87
154
  else if (a === '--force') force = true;
88
155
  else if (a === '--dry-run') dryRun = true;
89
156
  else if (a === '-h' || a === '--help') { usage(); return; }
@@ -93,11 +160,15 @@ async function install(args) {
93
160
  if (!customDir && !TARGETS[target]) {
94
161
  throw new Error(`Unknown target '${target}'. Valid targets: ${Object.keys(TARGETS).join(', ')}`);
95
162
  }
163
+ if (pick && skillFilters.length) {
164
+ throw new Error('Use either --pick or --skill, not both');
165
+ }
96
166
 
97
167
  const destRoot = path.resolve(expandHome(customDir || TARGETS[target]));
98
- const skills = await listSkills();
168
+ const allSkills = await listSkills();
169
+ const skills = pick ? await pickSkills(allSkills) : selectSkills(allSkills, skillFilters);
99
170
 
100
- console.log(`Installing ${skills.length} skill(s) to ${destRoot}`);
171
+ console.log(`Installing ${skills.length} of ${allSkills.length} skill(s) to ${destRoot}`);
101
172
  if (dryRun) console.log('Dry run: no files will be written.');
102
173
 
103
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.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",
@@ -42,10 +42,10 @@
42
42
  "scripts": {
43
43
  "check": "node --check bin/agent-skills.js && node --check skills/jira-browser-fetch/scripts/jira-browser-fetch.js && node --check skills/jira-browser-fetch/scripts/lib.js && node --check skills/confluence-browser-fetch/scripts/confluence-browser-fetch.js && node --check skills/confluence-browser-fetch/scripts/lib.js",
44
44
  "test": "node --test",
45
- "ci": "pnpm run check && pnpm test && pnpm pack --dry-run",
46
- "pack:dry": "pnpm pack --dry-run",
47
- "prepack": "pnpm run check",
48
- "prepublishOnly": "pnpm run check && pnpm test"
45
+ "ci": "npm run check && npm test && npm pack --dry-run",
46
+ "pack:dry": "npm pack --dry-run",
47
+ "prepack": "npm run check",
48
+ "prepublishOnly": "npm run check && npm test"
49
49
  },
50
50
  "pi": {
51
51
  "skills": [
@@ -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);