@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 +7 -1
- package/COMPATIBILITY.md +71 -43
- package/README.md +70 -24
- package/SECURITY.md +3 -2
- package/bin/agent-skills.js +73 -2
- package/package.json +5 -5
- package/skills/confluence-browser-fetch/SKILL.md +2 -2
- package/skills/confluence-browser-fetch/references/usage.md +13 -8
- package/skills/confluence-browser-fetch/scripts/confluence-browser-fetch.js +80 -6
- package/skills/jira-browser-fetch/SKILL.md +2 -2
- package/skills/jira-browser-fetch/references/usage.md +14 -9
- package/skills/jira-browser-fetch/scripts/jira-browser-fetch.js +97 -44
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
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
|
-
##
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
### Generic Agent Skills default
|
|
9
|
+
For cross-agent installation, prefer the open Skills CLI:
|
|
20
10
|
|
|
21
11
|
```bash
|
|
22
|
-
npx
|
|
12
|
+
npx skills add aholbreich/agent-skills -g
|
|
23
13
|
```
|
|
24
14
|
|
|
25
|
-
|
|
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
|
|
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
|
-
|
|
27
|
+
## Compatibility matrix
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
39
|
+
Pi can install this repository as a Pi package directly from npm:
|
|
38
40
|
|
|
39
41
|
```bash
|
|
40
|
-
|
|
42
|
+
pi install npm:@aholbreich/agent-skills
|
|
41
43
|
```
|
|
42
44
|
|
|
43
|
-
|
|
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
|
-
|
|
51
|
+
Temporary Pi run without installing:
|
|
50
52
|
|
|
51
53
|
```bash
|
|
52
|
-
|
|
54
|
+
pi -e npm:@aholbreich/agent-skills
|
|
53
55
|
```
|
|
54
56
|
|
|
55
|
-
|
|
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
|
|
62
|
+
npx @aholbreich/agent-skills
|
|
59
63
|
```
|
|
60
64
|
|
|
61
|
-
|
|
65
|
+
This installs to `~/.agents/skills` and is equivalent to:
|
|
62
66
|
|
|
63
67
|
```bash
|
|
64
|
-
npx @aholbreich/agent-skills --target
|
|
68
|
+
npx @aholbreich/agent-skills --target agents
|
|
65
69
|
```
|
|
66
70
|
|
|
67
|
-
|
|
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
|
-
|
|
82
|
+
Select one or more skills with `--skill`, or use the interactive picker:
|
|
74
83
|
|
|
75
84
|
```bash
|
|
76
|
-
npx @aholbreich/agent-skills --
|
|
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
|
-
|
|
90
|
+
This fallback copies files. For symlinked, multi-agent installs, prefer `npx skills add aholbreich/agent-skills`.
|
|
80
91
|
|
|
81
|
-
|
|
82
|
-
npx @aholbreich/agent-skills --target agents
|
|
83
|
-
```
|
|
92
|
+
## Collision behavior
|
|
84
93
|
|
|
85
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
108
|
+
shadows:
|
|
92
109
|
|
|
93
|
-
```
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
- `
|
|
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
|
-
|
|
18
|
+
Recommended install paths:
|
|
19
19
|
|
|
20
|
-
|
|
|
20
|
+
| Use case | Command |
|
|
21
21
|
|---|---|
|
|
22
|
-
|
|
|
23
|
-
| Pi
|
|
24
|
-
|
|
|
25
|
-
|
|
|
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
|
-
-
|
|
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
|
-
##
|
|
38
|
+
## Recommended install with the Skills CLI
|
|
41
39
|
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
86
|
+
pi -e npm:@aholbreich/agent-skills
|
|
58
87
|
```
|
|
59
88
|
|
|
60
|
-
##
|
|
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
|
-
|
|
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
|
|
108
|
+
Install only selected skills:
|
|
75
109
|
|
|
76
110
|
```bash
|
|
77
|
-
npx @aholbreich/agent-skills install --
|
|
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
|
-
|
|
116
|
+
Or use the dependency-free interactive picker:
|
|
81
117
|
|
|
82
118
|
```bash
|
|
83
|
-
npx @aholbreich/agent-skills install --
|
|
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
|
|
11
|
+
1. launch or reuse a Chromium-compatible browser with a dedicated local profile,
|
|
12
12
|
2. let you complete normal Atlassian SSO in the browser,
|
|
13
13
|
3. read Atlassian cookies through the local Chrome DevTools protocol,
|
|
14
|
-
4.
|
|
14
|
+
4. verify those cookies represent an authenticated Jira/Confluence REST session,
|
|
15
|
+
5. call Atlassian REST endpoints with those cookies.
|
|
15
16
|
|
|
16
17
|
They do **not** require you to paste API tokens or cookies into chat.
|
|
17
18
|
|
package/bin/agent-skills.js
CHANGED
|
@@ -5,6 +5,7 @@ const fs = require('fs');
|
|
|
5
5
|
const fsp = require('fs/promises');
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const path = require('path');
|
|
8
|
+
const readline = require('readline/promises');
|
|
8
9
|
|
|
9
10
|
const packageRoot = path.resolve(__dirname, '..');
|
|
10
11
|
const sourceSkillsDir = path.join(packageRoot, 'skills');
|
|
@@ -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
|
|
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
|
+
"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": "
|
|
46
|
-
"pack:dry": "
|
|
47
|
-
"prepack": "
|
|
48
|
-
"prepublishOnly": "
|
|
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
|
|
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.
|
|
10
|
+
4. Verifying those cookies represent an authenticated Confluence REST session.
|
|
11
|
+
5. Calling Confluence REST endpoints with those cookies.
|
|
11
12
|
|
|
12
13
|
No cookie or API token needs to be pasted into chat.
|
|
13
14
|
|
|
14
15
|
## Requirements
|
|
15
16
|
|
|
16
17
|
- Node.js 22+.
|
|
17
|
-
-
|
|
18
|
+
- A Chromium-compatible browser: Chrome, Chromium, Brave, Edge, or Vivaldi.
|
|
18
19
|
- Access to the Confluence page with the logged-in account.
|
|
19
20
|
|
|
20
21
|
Check:
|
|
21
22
|
|
|
22
23
|
```bash
|
|
23
24
|
node --version
|
|
24
|
-
which google-chrome || which chromium || which chromium-browser
|
|
25
|
+
which google-chrome || which chromium || which chromium-browser || which brave-browser || which microsoft-edge
|
|
25
26
|
```
|
|
26
27
|
|
|
27
|
-
If
|
|
28
|
+
The script auto-detects common Chromium-compatible browsers. If yours has a different path:
|
|
28
29
|
|
|
29
30
|
```bash
|
|
30
31
|
CHROME=/path/to/chrome scripts/confluence-browser-fetch.js 123456
|
|
@@ -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` |
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
258
|
-
|
|
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
|
|
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
|
|
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.
|
|
10
|
+
4. Verifying those cookies represent an authenticated Jira REST session.
|
|
11
|
+
5. Calling Jira REST endpoints with those cookies.
|
|
11
12
|
|
|
12
13
|
No token or cookie needs to be pasted into chat.
|
|
13
14
|
|
|
14
15
|
## Requirements
|
|
15
16
|
|
|
16
|
-
- Linux/macOS with Chrome or
|
|
17
|
+
- Linux/macOS with a Chromium-compatible browser: Chrome, Chromium, Brave, Edge, or Vivaldi.
|
|
17
18
|
- Node.js 22+.
|
|
18
19
|
- Network access to the Jira site.
|
|
19
20
|
|
|
@@ -21,10 +22,10 @@ Check:
|
|
|
21
22
|
|
|
22
23
|
```bash
|
|
23
24
|
node --version
|
|
24
|
-
which google-chrome || which chromium || which chromium-browser
|
|
25
|
+
which google-chrome || which chromium || which chromium-browser || which brave-browser || which microsoft-edge
|
|
25
26
|
```
|
|
26
27
|
|
|
27
|
-
If
|
|
28
|
+
The script auto-detects common Chromium-compatible browsers. If yours has a different path:
|
|
28
29
|
|
|
29
30
|
```bash
|
|
30
31
|
CHROME=/path/to/chrome scripts/jira-browser-fetch.js PROJ-123
|
|
@@ -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` |
|
|
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
|
|
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
|
-
###
|
|
155
|
+
### Browser does not open
|
|
151
156
|
|
|
152
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
234
|
-
|
|
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
|
|
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
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
} else {
|
|
283
|
-
const result = await fetchJson(url, cookie, 'application/json');
|
|
284
|
-
if (result.status === 200 && result.json && Array.isArray(result.json.issues)) return result.json;
|
|
285
|
-
last = `HTTP ${result.status} ${(result.text || '').slice(0, 180).replace(/\s+/g, ' ')}`;
|
|
286
|
-
}
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
471
|
-
|
|
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);
|