@aholbreich/agent-skills 0.2.3 → 0.3.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,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ Added:
6
+
7
+ - `jira-browser-fetch --backlog URL|BOARD_ID` to fetch all issues from a Jira Software board backlog through the authenticated browser session.
8
+ - Backlog manifests at `raw/jira-board-<board-id>-backlog.json` and a `backlogs` section in `raw/jira-browser-fetch-run.json`.
9
+ - Documentation examples for natural-language user requests that should invoke the skills.
10
+
3
11
  ## 0.1.0 - 2026-05-06
4
12
 
5
13
  Initial public package structure.
package/README.md CHANGED
@@ -8,7 +8,7 @@ This repository is a pure skills package. It currently contains browser-authenti
8
8
 
9
9
  | Skill | Purpose |
10
10
  |---|---|
11
- | [`jira-browser-fetch`](skills/jira-browser-fetch/) | Fetch Jira issue JSON, rendered HTML/XML, linked/referenced issues, JQL result sets, and attachments through an authenticated Chrome session. |
11
+ | [`jira-browser-fetch`](skills/jira-browser-fetch/) | Fetch Jira issue JSON, rendered HTML/XML, linked/referenced issues, Jira Software board backlogs, JQL result sets, and attachments through an authenticated Chrome session. |
12
12
  | [`confluence-browser-fetch`](skills/confluence-browser-fetch/) | Fetch Confluence page JSON, storage/view HTML, browser HTML, descendants, CQL result sets, and attachments through an authenticated Chrome session. |
13
13
 
14
14
  ## Compatibility
@@ -170,6 +170,23 @@ jira-browser-fetch \
170
170
  --jql 'assignee = currentUser() ORDER BY updated DESC'
171
171
  ```
172
172
 
173
+ Fetch a Jira Software board backlog:
174
+
175
+ ```bash
176
+ jira-browser-fetch \
177
+ --server https://example.atlassian.net \
178
+ --raw-dir ./raw \
179
+ --backlog 'https://example.atlassian.net/jira/software/c/projects/ABC/boards/42/backlog?epics=visible'
180
+ ```
181
+
182
+ Example user requests that should invoke this skill:
183
+
184
+ - "Fetch all Jira issues from this backlog URL into `raw/`."
185
+ - "Archive board 42's Jira backlog for my LLM wiki."
186
+ - "Fetch `PROJ-123` through my browser session and include linked issues."
187
+ - "Pull my assigned Jira issues without asking me for an API token."
188
+ - "Use this JQL and store the raw Jira evidence under the wiki raw folder."
189
+
173
190
  ## Confluence examples
174
191
 
175
192
  Fetch one page by URL:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aholbreich/agent-skills",
3
- "version": "0.2.3",
3
+ "version": "0.3.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",
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: jira-browser-fetch
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, linked tickets, rendered HTML/XML, remote links, and attachments into a raw wiki folder.
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
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.
6
6
  ---
7
7
 
8
8
  # Jira Browser Fetch
9
9
 
10
- Use this skill when Jira API-token authentication fails or the organization uses Microsoft/SSO and the user wants Jira issues archived into a local raw/wiki folder.
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
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.
13
13
 
@@ -33,12 +33,23 @@ Important options:
33
33
  --depth N recursion depth for connected tickets
34
34
  --scan-text find issue keys in JSON/XML/HTML text, not only formal Jira links
35
35
  --jql JQL search Jira with JQL and fetch all matching issues
36
+ --backlog URL|ID fetch all issues from a Jira Software board backlog URL or board id
36
37
  --assignee-me fetch all issues assigned to current Jira user
37
38
  --max-attachment-size S skip attachment files larger than S (default 5mb; use unlimited to disable)
38
39
  --prefix A,B,C only follow keys with these project prefixes
39
40
  --wait SEC SSO/session wait timeout per issue
40
41
  ```
41
42
 
43
+ ## Example User Requests
44
+
45
+ Use this skill for user requests like:
46
+
47
+ - "Fetch Jira issue `PROJ-123` into `raw/` through my browser session."
48
+ - "Archive this Jira backlog for my LLM wiki: `https://example.atlassian.net/jira/software/c/projects/ABC/boards/42/backlog?epics=visible`."
49
+ - "Fetch all Jira issues matching this JQL into the wiki raw folder."
50
+ - "Pull my assigned Jira issues without asking me for an API token."
51
+ - "Fetch this ticket and all linked tickets, including attachments under the default size limit."
52
+
42
53
  ## Typical Workflow
43
54
 
44
55
  1. Identify raw directory.
@@ -64,6 +75,12 @@ scripts/jira-browser-fetch.js \
64
75
  --server https://example.atlassian.net \
65
76
  --raw-dir ./raw \
66
77
  --assignee-me
78
+
79
+ # Fetch every issue currently visible in a Jira Software board backlog:
80
+ scripts/jira-browser-fetch.js \
81
+ --server https://example.atlassian.net \
82
+ --raw-dir ./raw \
83
+ --backlog 'https://example.atlassian.net/jira/software/c/projects/ABC/boards/42/backlog?epics=visible'
67
84
  ```
68
85
 
69
86
  ## Output Layout
@@ -88,6 +105,12 @@ A run manifest is written to:
88
105
  raw/jira-browser-fetch-run.json
89
106
  ```
90
107
 
108
+ Backlog fetches also write:
109
+
110
+ ```text
111
+ raw/jira-board-<board-id>-backlog.json
112
+ ```
113
+
91
114
  ## Installation / PATH
92
115
 
93
116
  The skill can be used directly by path. Optionally install a convenience symlink:
@@ -80,6 +80,26 @@ scripts/jira-browser-fetch.js \
80
80
  --jql "assignee = currentUser() AND statusCategory != Done ORDER BY updated DESC"
81
81
  ```
82
82
 
83
+ Fetch every issue currently visible in a Jira Software board backlog:
84
+
85
+ ```bash
86
+ scripts/jira-browser-fetch.js \
87
+ --server https://example.atlassian.net \
88
+ --raw-dir /path/to/wiki/raw \
89
+ --backlog 'https://example.atlassian.net/jira/software/c/projects/ABC/boards/42/backlog?epics=visible'
90
+ ```
91
+
92
+ If you already know the board id, this is equivalent:
93
+
94
+ ```bash
95
+ scripts/jira-browser-fetch.js \
96
+ --server https://example.atlassian.net \
97
+ --raw-dir /path/to/wiki/raw \
98
+ --backlog 42
99
+ ```
100
+
101
+ A backlog fetch writes `raw/jira-board-<board-id>-backlog.json` with the ordered backlog issue keys and adds a `backlogs` section to `raw/jira-browser-fetch-run.json`.
102
+
83
103
  Use a shorter wait when the browser session is already logged in:
84
104
 
85
105
  ```bash
@@ -102,11 +122,21 @@ Default max attachment download size is `5mb`. Use `--max-attachment-size unlimi
102
122
  | `JIRA_RAW_DIR` | Default output raw directory |
103
123
  | `JIRA_CHROME_DEBUG_PORT` | Chrome DevTools port, default `9223` |
104
124
  | `JIRA_FETCH_WAIT_SEC` | Wait timeout per issue, default `900` |
105
- | `JIRA_MAX_SEARCH_RESULTS` | Max issues added per JQL search, default `1000` |
125
+ | `JIRA_MAX_SEARCH_RESULTS` | Max issues added per JQL or backlog search, default `1000` |
106
126
  | `JIRA_MAX_ATTACHMENT_SIZE` / `JIRA_MAX_ATTACHMENT_BYTES` | Max attachment download size, default `5mb`; skipped files are listed in `attachments.json` |
107
127
  | `JIRA_CHROME_PROFILE` | Dedicated Chrome profile dir |
108
128
  | `CHROME` | Chrome executable path |
109
129
 
130
+ ## Example user requests
131
+
132
+ Agents should invoke this skill for requests such as:
133
+
134
+ - "Fetch all Jira issues from this backlog URL into `/raw`."
135
+ - "Archive board 42's Jira backlog for my LLM wiki."
136
+ - "Fetch my assigned Jira issues through the browser because API tokens do not work."
137
+ - "Fetch `PROJ-123` and all connected tickets with attachments."
138
+ - "Use this JQL and store the raw Jira evidence under the wiki raw folder."
139
+
110
140
  ## Troubleshooting
111
141
 
112
142
  ### `no Jira cookies yet`
@@ -6,7 +6,15 @@ const fsp = require('fs/promises');
6
6
  const os = require('os');
7
7
  const path = require('path');
8
8
  const { spawn } = require('child_process');
9
- const { parseSize, formatBytes, safeName, issueKeysFromText } = require('./lib');
9
+ const {
10
+ parseSize,
11
+ formatBytes,
12
+ safeName,
13
+ issueKeysFromText,
14
+ parseBacklogInput,
15
+ backlogApiUrl,
16
+ issueKeysFromAgilePage,
17
+ } = require('./lib');
10
18
 
11
19
  function usage() {
12
20
  console.log(`Usage: jira-browser-fetch [ISSUE-KEY ...] [options]
@@ -21,8 +29,9 @@ Options:
21
29
  --depth N Connected fetch depth (default: 1 with --connected, otherwise 0)
22
30
  --scan-text Include issue keys found anywhere in issue JSON/XML/HTML text
23
31
  --jql JQL Search Jira with JQL and fetch all matching issues
32
+ --backlog URL|BOARD_ID Fetch all issues from a Jira Software board backlog URL or board id
24
33
  --assignee-me Fetch all issues assigned to current Jira user
25
- --max-search-results N Max issues to add per JQL search (default: 1000)
34
+ --max-search-results N Max issues to add per JQL/backlog search (default: 1000)
26
35
  --max-attachment-size S Skip attachment downloads larger than S (default: 5mb; use unlimited to disable)
27
36
  --prefix A,B,C Only fetch referenced keys with these project prefixes
28
37
  --wait SEC Wait time for SSO/session per issue (default: 900)
@@ -37,7 +46,8 @@ Examples:
37
46
  jira-browser-fetch SWING-4770 --raw-dir /path/wiki/raw
38
47
  jira-browser-fetch SWING-4770 --connected --prefix SWING,SSD,EC --raw-dir ./raw
39
48
  jira-browser-fetch --assignee-me --raw-dir ./raw
40
- JIRA_SERVER=https://example.atlassian.net jira-browser-fetch PROJ-123 --connected
49
+ jira-browser-fetch --backlog 'https://example.atlassian.net/jira/software/c/projects/ABC/boards/42/backlog?epics=visible' --raw-dir ./raw
50
+ JIRA_SERVER=https://example.atlassian.net jira-browser-fetch --backlog 42 --connected
41
51
  `);
42
52
  }
43
53
 
@@ -51,6 +61,7 @@ const opts = {
51
61
  depth: undefined,
52
62
  scanText: false,
53
63
  jqls: [],
64
+ backlogs: [],
54
65
  assigneeMe: false,
55
66
  maxSearchResults: Number(process.env.JIRA_MAX_SEARCH_RESULTS || 1000),
56
67
  maxAttachmentBytes: parseSize(process.env.JIRA_MAX_ATTACHMENT_SIZE || process.env.JIRA_MAX_ATTACHMENT_BYTES || '5mb'),
@@ -70,6 +81,7 @@ for (let i = 2; i < process.argv.length; i++) {
70
81
  else if (a === '--depth') opts.depth = Number(process.argv[++i]);
71
82
  else if (a === '--scan-text') opts.scanText = true;
72
83
  else if (a === '--jql') opts.jqls.push(process.argv[++i]);
84
+ else if (a === '--backlog') opts.backlogs.push(process.argv[++i]);
73
85
  else if (a === '--assignee-me') opts.assigneeMe = true;
74
86
  else if (a === '--max-search-results') opts.maxSearchResults = Number(process.argv[++i]);
75
87
  else if (a === '--max-attachment-size') opts.maxAttachmentBytes = parseSize(process.argv[++i]);
@@ -84,7 +96,7 @@ for (let i = 2; i < process.argv.length; i++) {
84
96
  else { console.error(`Unknown argument: ${a}`); process.exit(2); }
85
97
  }
86
98
 
87
- if (!issues.length && !opts.jqls.length && !opts.assigneeMe) { usage(); process.exit(2); }
99
+ if (!issues.length && !opts.jqls.length && !opts.backlogs.length && !opts.assigneeMe) { usage(); process.exit(2); }
88
100
  if (opts.assigneeMe) opts.jqls.push('assignee = currentUser() ORDER BY updated DESC');
89
101
  if (opts.depth === undefined) opts.depth = opts.connected ? 1 : 0;
90
102
  if (opts.depth > 0) opts.connected = true;
@@ -259,6 +271,65 @@ async function searchJql(jql) {
259
271
  return [...new Set(found)];
260
272
  }
261
273
 
274
+ async function fetchBacklogPageWithWait(url) {
275
+ const deadline = Date.now() + opts.waitSec * 1000;
276
+ let last = '';
277
+ while (Date.now() < deadline) {
278
+ 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
+ }
287
+ } 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)}`);
289
+ await sleep(3000);
290
+ }
291
+ process.stdout.write('\n');
292
+ throw new Error(`Could not fetch Jira backlog. Last result: ${last}`);
293
+ }
294
+
295
+ async function searchBacklog(input) {
296
+ const backlog = parseBacklogInput(input, opts.server);
297
+ await ensureBrowser(backlog.browseUrl);
298
+ console.log(`If prompted in Chrome, complete SSO for: ${backlog.browseUrl}`);
299
+ console.log(`Waiting up to ${opts.waitSec}s for Jira backlog access...`);
300
+
301
+ const found = [];
302
+ let startAt = 0;
303
+ const pageSize = Math.min(100, Math.max(1, opts.maxSearchResults || 1000));
304
+
305
+ while (found.length < opts.maxSearchResults) {
306
+ const limit = Math.min(pageSize, opts.maxSearchResults - found.length);
307
+ const url = backlogApiUrl(opts.server, backlog.boardId, startAt, limit);
308
+ const page = await fetchBacklogPageWithWait(url);
309
+ const keys = issueKeysFromAgilePage(page);
310
+ for (const key of keys) found.push(key);
311
+ console.log(`Fetched backlog page board=${backlog.boardId} startAt=${startAt}, issues=${keys.length}${typeof page.total === 'number' ? `, total=${page.total}` : ''}`);
312
+ if (page.isLast === true) break;
313
+ if (typeof page.total === 'number' && startAt + keys.length >= page.total) break;
314
+ if (!keys.length) break;
315
+ startAt += keys.length;
316
+ }
317
+
318
+ const keys = [...new Set(found)];
319
+ const manifest = {
320
+ fetchedAt: new Date().toISOString(),
321
+ server: opts.server,
322
+ boardId: backlog.boardId,
323
+ source: backlog.source,
324
+ browseUrl: backlog.browseUrl,
325
+ endpoint: `/rest/agile/1.0/board/${backlog.boardId}/backlog`,
326
+ issueCount: keys.length,
327
+ issues: keys,
328
+ };
329
+ await fsp.writeFile(path.join(opts.rawDir, `jira-board-${backlog.boardId}-backlog.json`), JSON.stringify(manifest, null, 2));
330
+ return manifest;
331
+ }
332
+
262
333
  function addKey(set, key) {
263
334
  if (!key) return;
264
335
  key = String(key).toUpperCase();
@@ -455,6 +526,22 @@ async function main() {
455
526
  const seen = new Set();
456
527
  const failed = [];
457
528
  const searches = [];
529
+ const backlogs = [];
530
+
531
+ for (const backlogInput of opts.backlogs) {
532
+ console.log(`\n===== Fetching Jira backlog: ${backlogInput} =====`);
533
+ try {
534
+ const backlog = await searchBacklog(backlogInput);
535
+ backlogs.push(backlog);
536
+ console.log(`Backlog board ${backlog.boardId} matched ${backlog.issueCount} issue(s): ${backlog.issues.join(' ') || '(none)'}`);
537
+ for (const key of backlog.issues) {
538
+ if (!queue.some(q => q.key === key)) queue.push({ key, depth: 0, from: `Backlog board ${backlog.boardId}` });
539
+ }
540
+ } catch (e) {
541
+ failed.push({ key: `BACKLOG: ${backlogInput}`, error: e.message });
542
+ console.error(`BACKLOG FAILED: ${e.message}`);
543
+ }
544
+ }
458
545
 
459
546
  for (const jql of opts.jqls) {
460
547
  console.log(`\n===== Searching JQL: ${jql} =====`);
@@ -489,7 +576,7 @@ async function main() {
489
576
  }
490
577
  }
491
578
 
492
- const runMeta = { fetchedAt: new Date().toISOString(), server: opts.server, rawDir: opts.rawDir, requested: issues, searches, fetched: [...seen].filter(k => !failed.some(f => f.key === k)), failed };
579
+ const runMeta = { fetchedAt: new Date().toISOString(), server: opts.server, rawDir: opts.rawDir, requested: issues, searches, backlogs, fetched: [...seen].filter(k => !failed.some(f => f.key === k)), failed };
493
580
  await fsp.writeFile(path.join(opts.rawDir, 'jira-browser-fetch-run.json'), JSON.stringify(runMeta, null, 2));
494
581
 
495
582
  if (failed.length) {
@@ -40,6 +40,46 @@ function shouldSkipAttachment(size, maxAttachmentBytes) {
40
40
  return Number.isFinite(n) && n > maxAttachmentBytes;
41
41
  }
42
42
 
43
+ function parseBacklogInput(input, server = '') {
44
+ if (!input) throw new Error('Missing Jira backlog URL or board id');
45
+ const value = String(input).trim();
46
+ const numeric = value.match(/^\d+$/);
47
+ if (numeric) {
48
+ return {
49
+ boardId: Number(value),
50
+ source: value,
51
+ browseUrl: server ? `${String(server).replace(/\/$/, '')}/jira/software/c/boards/${value}/backlog` : value,
52
+ };
53
+ }
54
+
55
+ let url;
56
+ try {
57
+ url = new URL(value);
58
+ } catch {
59
+ throw new Error(`Invalid Jira backlog URL or board id: ${input}`);
60
+ }
61
+
62
+ const m = url.pathname.match(/\/boards\/(\d+)(?:\/backlog)?\b/);
63
+ if (!m) throw new Error(`Could not parse Jira board id from backlog URL: ${input}`);
64
+
65
+ return {
66
+ boardId: Number(m[1]),
67
+ source: value,
68
+ browseUrl: value,
69
+ };
70
+ }
71
+
72
+ function backlogApiUrl(server, boardId, startAt, maxResults) {
73
+ const base = String(server || '').replace(/\/$/, '');
74
+ const params = new URLSearchParams({ startAt: String(startAt), maxResults: String(maxResults) });
75
+ return `${base}/rest/agile/1.0/board/${boardId}/backlog?${params}`;
76
+ }
77
+
78
+ function issueKeysFromAgilePage(page) {
79
+ const issues = page && Array.isArray(page.issues) ? page.issues : [];
80
+ return issues.map(issue => issue && issue.key).filter(Boolean);
81
+ }
82
+
43
83
  module.exports = {
44
84
  DEFAULT_MAX_ATTACHMENT_BYTES,
45
85
  parseSize,
@@ -47,4 +87,7 @@ module.exports = {
47
87
  safeName,
48
88
  issueKeysFromText,
49
89
  shouldSkipAttachment,
90
+ parseBacklogInput,
91
+ backlogApiUrl,
92
+ issueKeysFromAgilePage,
50
93
  };