@aholbreich/agent-skills 0.9.0 → 1.1.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 +44 -1
- package/README.md +85 -13
- package/bin/agent-skills.js +11 -5
- package/bin/check.js +80 -0
- package/bin/vendor.js +24 -0
- package/package.json +6 -3
- package/skills/bitbucket-browser-fetch/SKILL.md +14 -0
- package/skills/bitbucket-browser-fetch/scripts/atlassian-browser.js +261 -0
- package/skills/bitbucket-browser-fetch/scripts/bitbucket-browser-fetch.js +20 -201
- package/skills/confluence-browser-fetch/SKILL.md +15 -4
- package/skills/confluence-browser-fetch/references/distribution.md +5 -1
- package/skills/confluence-browser-fetch/references/usage.md +2 -2
- package/skills/confluence-browser-fetch/scripts/atlassian-browser.js +261 -0
- package/skills/confluence-browser-fetch/scripts/confluence-browser-fetch.js +32 -215
- package/skills/confluence-update/SKILL.md +13 -2
- package/skills/confluence-update/references/usage.md +2 -2
- package/skills/confluence-update/scripts/atlassian-browser.js +261 -0
- package/skills/confluence-update/scripts/confluence-update.js +144 -257
- package/skills/jira-browser-fetch/SKILL.md +15 -3
- package/skills/jira-browser-fetch/references/distribution.md +5 -1
- package/skills/jira-browser-fetch/scripts/atlassian-browser.js +261 -0
- package/skills/jira-browser-fetch/scripts/jira-browser-fetch.js +58 -232
- package/skills/jira-browser-fetch/scripts/lib.js +26 -0
- package/skills/jira-update/SKILL.md +132 -0
- package/skills/jira-update/references/distribution.md +5 -0
- package/skills/jira-update/references/usage.md +75 -0
- package/skills/jira-update/scripts/atlassian-browser.js +261 -0
- package/skills/jira-update/scripts/jira-update.js +505 -0
- package/skills/jira-update/scripts/lib.js +291 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { spawn } = require('node:child_process');
|
|
6
|
+
|
|
7
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
8
|
+
|
|
9
|
+
function isExecutable(file) {
|
|
10
|
+
try { fs.accessSync(file, fs.constants.X_OK); return true; } catch { return false; }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function resolveBrowserCandidate(candidate) {
|
|
14
|
+
if (!candidate) return null;
|
|
15
|
+
if (candidate.includes(path.sep)) return isExecutable(candidate) ? candidate : null;
|
|
16
|
+
for (const dir of String(process.env.PATH || '').split(path.delimiter)) {
|
|
17
|
+
if (!dir) continue;
|
|
18
|
+
const full = path.join(dir, candidate);
|
|
19
|
+
if (isExecutable(full)) return full;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function findBrowserExecutable() {
|
|
25
|
+
const candidates = [
|
|
26
|
+
process.env.CHROME,
|
|
27
|
+
process.env.CHROMIUM,
|
|
28
|
+
'google-chrome',
|
|
29
|
+
'google-chrome-stable',
|
|
30
|
+
'chromium',
|
|
31
|
+
'chromium-browser',
|
|
32
|
+
'brave-browser',
|
|
33
|
+
'brave',
|
|
34
|
+
'microsoft-edge',
|
|
35
|
+
'microsoft-edge-stable',
|
|
36
|
+
'vivaldi',
|
|
37
|
+
'vivaldi-stable',
|
|
38
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
39
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
40
|
+
'/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
|
|
41
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
42
|
+
'/Applications/Vivaldi.app/Contents/MacOS/Vivaldi',
|
|
43
|
+
];
|
|
44
|
+
for (const candidate of candidates) {
|
|
45
|
+
const resolved = resolveBrowserCandidate(candidate);
|
|
46
|
+
if (resolved) return resolved;
|
|
47
|
+
}
|
|
48
|
+
throw new Error('Could not find a Chromium-compatible browser. Install Chrome/Chromium/Brave/Edge/Vivaldi or set CHROME=/path/to/browser.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function connectCdp(wsUrl) {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const ws = new WebSocket(wsUrl);
|
|
54
|
+
let id = 0;
|
|
55
|
+
const pending = new Map();
|
|
56
|
+
const failTimer = setTimeout(() => reject(new Error('CDP websocket timeout')), 10000);
|
|
57
|
+
|
|
58
|
+
ws.addEventListener('open', () => {
|
|
59
|
+
clearTimeout(failTimer);
|
|
60
|
+
resolve({
|
|
61
|
+
send(method, params = {}) {
|
|
62
|
+
return new Promise((res, rej) => {
|
|
63
|
+
const msgId = ++id;
|
|
64
|
+
pending.set(msgId, { res, rej });
|
|
65
|
+
ws.send(JSON.stringify({ id: msgId, method, params }));
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
close() { try { ws.close(); } catch {} },
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
ws.addEventListener('message', ev => {
|
|
73
|
+
let data = ev.data;
|
|
74
|
+
if (typeof data !== 'string') data = Buffer.from(data).toString('utf8');
|
|
75
|
+
const msg = JSON.parse(data);
|
|
76
|
+
if (!msg.id || !pending.has(msg.id)) return;
|
|
77
|
+
const { res, rej } = pending.get(msg.id);
|
|
78
|
+
pending.delete(msg.id);
|
|
79
|
+
if (msg.error) rej(new Error(`${msg.error.message || 'CDP error'} ${JSON.stringify(msg.error)}`));
|
|
80
|
+
else res(msg.result);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
ws.addEventListener('error', err => reject(err));
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function createBrowserSession({ port, profileDir, waitSec, serverHost, verifySession, cookieUrls, userAgent }) {
|
|
88
|
+
if (!serverHost) throw new Error('createBrowserSession requires serverHost');
|
|
89
|
+
if (typeof verifySession !== 'function') throw new Error('createBrowserSession requires verifySession callback');
|
|
90
|
+
const ua = userAgent || 'agent-skills/1.0';
|
|
91
|
+
|
|
92
|
+
async function endpoint(pathname) {
|
|
93
|
+
const res = await fetch(`http://127.0.0.1:${port}${pathname}`);
|
|
94
|
+
if (!res.ok) throw new Error(`DevTools HTTP ${res.status} for ${pathname}`);
|
|
95
|
+
return res.json();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function devtoolsReady() {
|
|
99
|
+
try { await endpoint('/json/version'); return true; } catch { return false; }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function waitDevtools() {
|
|
103
|
+
for (let i = 0; i < 80; i++) {
|
|
104
|
+
if (await devtoolsReady()) return;
|
|
105
|
+
await sleep(250);
|
|
106
|
+
}
|
|
107
|
+
throw new Error('Chrome DevTools endpoint did not start');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function openDevtoolsTab(url) {
|
|
111
|
+
if (!url) return false;
|
|
112
|
+
const endpointUrl = `http://127.0.0.1:${port}/json/new?${encodeURIComponent(url)}`;
|
|
113
|
+
for (const init of [{ method: 'PUT' }, {}]) {
|
|
114
|
+
try {
|
|
115
|
+
const res = await fetch(endpointUrl, init);
|
|
116
|
+
if (res.ok) { await sleep(500); return true; }
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function hasDevtoolsTabForHost(url, pathPrefix) {
|
|
123
|
+
if (!url) return false;
|
|
124
|
+
const host = new URL(url).host;
|
|
125
|
+
const list = await endpoint('/json/list');
|
|
126
|
+
return list.some(t => t.type === 'page' && t.url && (() => {
|
|
127
|
+
try {
|
|
128
|
+
const tabUrl = new URL(t.url);
|
|
129
|
+
if (tabUrl.host !== host) return false;
|
|
130
|
+
if (pathPrefix && !tabUrl.pathname.startsWith(pathPrefix)) return false;
|
|
131
|
+
return true;
|
|
132
|
+
} catch { return false; }
|
|
133
|
+
})());
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function launchChrome(url) {
|
|
137
|
+
const browser = findBrowserExecutable();
|
|
138
|
+
const args = [
|
|
139
|
+
`--remote-debugging-port=${port}`,
|
|
140
|
+
'--remote-debugging-address=127.0.0.1',
|
|
141
|
+
'--remote-allow-origins=*',
|
|
142
|
+
`--user-data-dir=${profileDir}`,
|
|
143
|
+
'--no-first-run',
|
|
144
|
+
'--no-default-browser-check',
|
|
145
|
+
url,
|
|
146
|
+
];
|
|
147
|
+
console.log(`Launching browser: ${browser}`);
|
|
148
|
+
const child = spawn(browser, args, { detached: true, stdio: 'ignore' });
|
|
149
|
+
child.on('error', err => console.error(`Failed to launch browser ${browser}: ${err.message}`));
|
|
150
|
+
child.unref();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function ensureBrowser(openUrl, { tabPathPrefix } = {}) {
|
|
154
|
+
if (!(await devtoolsReady())) {
|
|
155
|
+
console.log(`Opening Chromium-compatible browser with reusable profile: ${profileDir}`);
|
|
156
|
+
launchChrome(openUrl);
|
|
157
|
+
} else {
|
|
158
|
+
console.log(`Reusing Chrome DevTools on port ${port}`);
|
|
159
|
+
if (openUrl) {
|
|
160
|
+
const hasTab = await hasDevtoolsTabForHost(openUrl, tabPathPrefix);
|
|
161
|
+
if (hasTab) {
|
|
162
|
+
console.log(`Found existing tab for ${new URL(openUrl).host}; not opening another tab.`);
|
|
163
|
+
} else {
|
|
164
|
+
const opened = await openDevtoolsTab(openUrl);
|
|
165
|
+
if (opened) console.log(`Opened target URL in reused browser: ${openUrl}`);
|
|
166
|
+
else console.warn('Could not open target URL through DevTools; continuing with existing tabs.');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
await waitDevtools();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function getPageWsUrl() {
|
|
174
|
+
const list = await endpoint('/json/list');
|
|
175
|
+
const pages = list.filter(t => t.type === 'page' && t.webSocketDebuggerUrl);
|
|
176
|
+
const preferred = pages.find(t => (t.url || '').includes(serverHost)) || pages[0];
|
|
177
|
+
return preferred && preferred.webSocketDebuggerUrl;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function getCookieHeader() {
|
|
181
|
+
const wsUrl = await getPageWsUrl();
|
|
182
|
+
if (!wsUrl) return '';
|
|
183
|
+
const cdp = await connectCdp(wsUrl);
|
|
184
|
+
try {
|
|
185
|
+
await cdp.send('Network.enable');
|
|
186
|
+
const urls = (cookieUrls && cookieUrls.length) ? cookieUrls : [`https://${serverHost}/`];
|
|
187
|
+
const result = await cdp.send('Network.getCookies', { urls });
|
|
188
|
+
const cookies = (result.cookies || [])
|
|
189
|
+
.filter(c => c.domain && (c.domain === serverHost || c.domain.endsWith(`.${serverHost}`)))
|
|
190
|
+
.map(c => `${c.name}=${c.value}`);
|
|
191
|
+
return cookies.join('; ');
|
|
192
|
+
} finally {
|
|
193
|
+
cdp.close();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function fetchText(url, cookie, options = {}) {
|
|
198
|
+
const method = options.method || 'GET';
|
|
199
|
+
const headers = {
|
|
200
|
+
Cookie: cookie,
|
|
201
|
+
Accept: options.accept || '*/*',
|
|
202
|
+
'User-Agent': ua,
|
|
203
|
+
};
|
|
204
|
+
if (options.body !== undefined && options.body !== null) headers['Content-Type'] = options.contentType || 'application/json';
|
|
205
|
+
const res = await fetch(url, { method, redirect: 'follow', headers, body: options.body ?? null });
|
|
206
|
+
return { status: res.status, contentType: res.headers.get('content-type') || '', text: await res.text() };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function fetchJson(url, cookie, options = {}) {
|
|
210
|
+
const result = await fetchText(url, cookie, { ...options, accept: options.accept || 'application/json' });
|
|
211
|
+
let json = null;
|
|
212
|
+
try { json = JSON.parse(result.text); } catch {}
|
|
213
|
+
return { ...result, json };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function getCookieWithWait(openUrl, { tabPathPrefix } = {}) {
|
|
217
|
+
await ensureBrowser(openUrl, { tabPathPrefix });
|
|
218
|
+
console.log(`If prompted in Chrome, complete SSO for: ${openUrl}`);
|
|
219
|
+
const deadline = Date.now() + waitSec * 1000;
|
|
220
|
+
let last = '';
|
|
221
|
+
while (Date.now() < deadline) {
|
|
222
|
+
try {
|
|
223
|
+
const cookie = await getCookieHeader();
|
|
224
|
+
const result = await verifySession(cookie);
|
|
225
|
+
if (result && result.ok) {
|
|
226
|
+
if (process.stdout.isTTY) process.stdout.write('\n');
|
|
227
|
+
console.log(`Authenticated session verified${result.url ? ` via ${result.url}` : ''}`);
|
|
228
|
+
return cookie;
|
|
229
|
+
}
|
|
230
|
+
last = (result && result.message) || 'session not yet verified';
|
|
231
|
+
} catch (e) { last = e.message; }
|
|
232
|
+
if (process.stdout.isTTY) {
|
|
233
|
+
process.stdout.write(`\r${new Date().toLocaleTimeString()} ${last.padEnd(120).slice(0, 120)}`);
|
|
234
|
+
}
|
|
235
|
+
await sleep(3000);
|
|
236
|
+
}
|
|
237
|
+
if (process.stdout.isTTY) process.stdout.write('\n');
|
|
238
|
+
throw new Error(`Could not verify authenticated session. Last result: ${last}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
devtoolsReady,
|
|
243
|
+
waitDevtools,
|
|
244
|
+
openDevtoolsTab,
|
|
245
|
+
hasDevtoolsTabForHost,
|
|
246
|
+
launchChrome,
|
|
247
|
+
ensureBrowser,
|
|
248
|
+
getPageWsUrl,
|
|
249
|
+
getCookieHeader,
|
|
250
|
+
getCookieWithWait,
|
|
251
|
+
fetchText,
|
|
252
|
+
fetchJson,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
module.exports = {
|
|
257
|
+
createBrowserSession,
|
|
258
|
+
findBrowserExecutable,
|
|
259
|
+
resolveBrowserCandidate,
|
|
260
|
+
connectCdp,
|
|
261
|
+
};
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const fs = require('fs');
|
|
5
4
|
const fsp = require('fs/promises');
|
|
6
5
|
const os = require('os');
|
|
7
6
|
const path = require('path');
|
|
8
|
-
const {
|
|
7
|
+
const { createBrowserSession } = require('./atlassian-browser');
|
|
9
8
|
const {
|
|
10
9
|
parseProjectInput,
|
|
11
10
|
repositoriesApiUrl,
|
|
@@ -26,8 +25,8 @@ Options:
|
|
|
26
25
|
--raw-dir DIR Output raw directory (default: BITBUCKET_RAW_DIR or ./raw)
|
|
27
26
|
--pagelen N Internal API page size (default: 100)
|
|
28
27
|
--wait SEC Wait time for login/session (default: 900)
|
|
29
|
-
--port PORT Chrome DevTools port (default: BITBUCKET_CHROME_DEBUG_PORT, ATLASSIAN_CHROME_DEBUG_PORT, or
|
|
30
|
-
--profile-dir DIR Chrome profile dir (default: BITBUCKET_CHROME_PROFILE, ATLASSIAN_CHROME_PROFILE, or ~/.local/share/
|
|
28
|
+
--port PORT Chrome DevTools port (default: BITBUCKET_CHROME_DEBUG_PORT, ATLASSIAN_CHROME_DEBUG_PORT, or 9223)
|
|
29
|
+
--profile-dir DIR Chrome profile dir (default: BITBUCKET_CHROME_PROFILE, ATLASSIAN_CHROME_PROFILE, or ~/.local/share/atlassian-browser-chrome)
|
|
31
30
|
--help Show this help
|
|
32
31
|
|
|
33
32
|
Examples:
|
|
@@ -40,9 +39,9 @@ const opts = {
|
|
|
40
39
|
workspace: '',
|
|
41
40
|
projectKey: '',
|
|
42
41
|
rawDir: process.env.BITBUCKET_RAW_DIR || path.resolve(process.cwd(), 'raw'),
|
|
43
|
-
port: Number(process.env.BITBUCKET_CHROME_DEBUG_PORT || process.env.ATLASSIAN_CHROME_DEBUG_PORT ||
|
|
42
|
+
port: Number(process.env.BITBUCKET_CHROME_DEBUG_PORT || process.env.ATLASSIAN_CHROME_DEBUG_PORT || 9223),
|
|
44
43
|
waitSec: Number(process.env.BITBUCKET_FETCH_WAIT_SEC || 900),
|
|
45
|
-
profileDir: process.env.BITBUCKET_CHROME_PROFILE || process.env.ATLASSIAN_CHROME_PROFILE || path.join(os.homedir(), '.local/share/
|
|
44
|
+
profileDir: process.env.BITBUCKET_CHROME_PROFILE || process.env.ATLASSIAN_CHROME_PROFILE || path.join(os.homedir(), '.local/share/atlassian-browser-chrome'),
|
|
46
45
|
pagelen: Number(process.env.BITBUCKET_PAGELEN || 100),
|
|
47
46
|
};
|
|
48
47
|
|
|
@@ -71,182 +70,23 @@ project.browseUrl = `https://bitbucket.org/${project.workspace}/workspace/projec
|
|
|
71
70
|
opts.rawDir = path.resolve(opts.rawDir);
|
|
72
71
|
opts.pagelen = Math.min(100, Math.max(1, opts.pagelen || 100));
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
async function waitDevtools() {
|
|
87
|
-
for (let i = 0; i < 80; i++) {
|
|
88
|
-
if (await devtoolsReady()) return;
|
|
89
|
-
await sleep(250);
|
|
90
|
-
}
|
|
91
|
-
throw new Error('Chrome DevTools endpoint did not start');
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function openDevtoolsTab(url) {
|
|
95
|
-
const endpointUrl = `http://127.0.0.1:${opts.port}/json/new?${encodeURIComponent(url)}`;
|
|
96
|
-
for (const init of [{ method: 'PUT' }, {}]) {
|
|
97
|
-
try {
|
|
98
|
-
const res = await fetch(endpointUrl, init);
|
|
99
|
-
if (res.ok) { await sleep(1000); return true; }
|
|
100
|
-
} catch {}
|
|
101
|
-
}
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function hasBitbucketTab(url) {
|
|
106
|
-
const host = new URL(url).host;
|
|
107
|
-
const list = await endpoint('/json/list');
|
|
108
|
-
return list.some(t => t.type === 'page' && t.url && (() => {
|
|
109
|
-
try { return new URL(t.url).host === host; } catch { return false; }
|
|
110
|
-
})());
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function isExecutable(file) {
|
|
114
|
-
try { fs.accessSync(file, fs.constants.X_OK); return true; } catch { return false; }
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function resolveBrowserCandidate(candidate) {
|
|
118
|
-
if (!candidate) return null;
|
|
119
|
-
if (candidate.includes(path.sep)) return isExecutable(candidate) ? candidate : null;
|
|
120
|
-
for (const dir of String(process.env.PATH || '').split(path.delimiter)) {
|
|
121
|
-
if (!dir) continue;
|
|
122
|
-
const full = path.join(dir, candidate);
|
|
123
|
-
if (isExecutable(full)) return full;
|
|
124
|
-
}
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function findBrowserExecutable() {
|
|
129
|
-
const candidates = [
|
|
130
|
-
process.env.CHROME,
|
|
131
|
-
process.env.CHROMIUM,
|
|
132
|
-
'google-chrome',
|
|
133
|
-
'google-chrome-stable',
|
|
134
|
-
'chromium',
|
|
135
|
-
'chromium-browser',
|
|
136
|
-
'brave-browser',
|
|
137
|
-
'brave',
|
|
138
|
-
'microsoft-edge',
|
|
139
|
-
'microsoft-edge-stable',
|
|
140
|
-
'vivaldi',
|
|
141
|
-
'vivaldi-stable',
|
|
142
|
-
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
143
|
-
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
144
|
-
'/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
|
|
145
|
-
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
146
|
-
'/Applications/Vivaldi.app/Contents/MacOS/Vivaldi',
|
|
147
|
-
];
|
|
148
|
-
for (const candidate of candidates) {
|
|
149
|
-
const resolved = resolveBrowserCandidate(candidate);
|
|
150
|
-
if (resolved) return resolved;
|
|
151
|
-
}
|
|
152
|
-
throw new Error('Could not find a Chromium-compatible browser. Install Chrome/Chromium/Brave/Edge/Vivaldi or set CHROME=/path/to/browser.');
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function launchChrome(url) {
|
|
156
|
-
const browser = findBrowserExecutable();
|
|
157
|
-
const args = [
|
|
158
|
-
`--remote-debugging-port=${opts.port}`,
|
|
159
|
-
'--remote-debugging-address=127.0.0.1',
|
|
160
|
-
'--remote-allow-origins=*',
|
|
161
|
-
`--user-data-dir=${opts.profileDir}`,
|
|
162
|
-
'--no-first-run',
|
|
163
|
-
'--no-default-browser-check',
|
|
164
|
-
url,
|
|
165
|
-
];
|
|
166
|
-
console.log(`Launching browser: ${browser}`);
|
|
167
|
-
const child = spawn(browser, args, { detached: true, stdio: 'ignore' });
|
|
168
|
-
child.on('error', err => console.error(`Failed to launch browser ${browser}: ${err.message}`));
|
|
169
|
-
child.unref();
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async function ensureBrowser(openUrl) {
|
|
173
|
-
if (!(await devtoolsReady())) {
|
|
174
|
-
console.log(`Opening Chromium-compatible browser with reusable profile: ${opts.profileDir}`);
|
|
175
|
-
launchChrome(openUrl);
|
|
176
|
-
} else {
|
|
177
|
-
console.log(`Reusing Chrome DevTools on port ${opts.port}`);
|
|
178
|
-
if (await hasBitbucketTab(openUrl)) console.log(`Found existing Bitbucket tab for ${new URL(openUrl).host}; not opening another tab.`);
|
|
179
|
-
else if (await openDevtoolsTab(openUrl)) console.log(`Opened target URL in reused browser: ${openUrl}`);
|
|
180
|
-
else console.warn('Could not open target URL through DevTools; continuing with existing tabs.');
|
|
181
|
-
}
|
|
182
|
-
await waitDevtools();
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
async function getPageWsUrl() {
|
|
186
|
-
const list = await endpoint('/json/list');
|
|
187
|
-
const pages = list.filter(t => t.type === 'page' && t.webSocketDebuggerUrl);
|
|
188
|
-
const preferred = pages.find(t => (t.url || '').includes('bitbucket.org')) || pages[0];
|
|
189
|
-
return preferred && preferred.webSocketDebuggerUrl;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function connectCdp(wsUrl) {
|
|
193
|
-
return new Promise((resolve, reject) => {
|
|
194
|
-
const ws = new WebSocket(wsUrl);
|
|
195
|
-
let id = 0;
|
|
196
|
-
const pending = new Map();
|
|
197
|
-
const failTimer = setTimeout(() => reject(new Error('CDP websocket timeout')), 10000);
|
|
198
|
-
ws.addEventListener('open', () => {
|
|
199
|
-
clearTimeout(failTimer);
|
|
200
|
-
resolve({
|
|
201
|
-
send(method, params = {}) {
|
|
202
|
-
return new Promise((res, rej) => {
|
|
203
|
-
const msgId = ++id;
|
|
204
|
-
pending.set(msgId, { res, rej });
|
|
205
|
-
ws.send(JSON.stringify({ id: msgId, method, params }));
|
|
206
|
-
});
|
|
207
|
-
},
|
|
208
|
-
close() { try { ws.close(); } catch {} },
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
ws.addEventListener('message', ev => {
|
|
212
|
-
let data = ev.data;
|
|
213
|
-
if (typeof data !== 'string') data = Buffer.from(data).toString('utf8');
|
|
214
|
-
const msg = JSON.parse(data);
|
|
215
|
-
if (!msg.id || !pending.has(msg.id)) return;
|
|
216
|
-
const { res, rej } = pending.get(msg.id);
|
|
217
|
-
pending.delete(msg.id);
|
|
218
|
-
if (msg.error) rej(new Error(`${msg.error.message || 'CDP error'} ${JSON.stringify(msg.error)}`));
|
|
219
|
-
else res(msg.result);
|
|
220
|
-
});
|
|
221
|
-
ws.addEventListener('error', err => reject(err));
|
|
73
|
+
let session = null;
|
|
74
|
+
function getSession() {
|
|
75
|
+
if (session) return session;
|
|
76
|
+
session = createBrowserSession({
|
|
77
|
+
port: opts.port,
|
|
78
|
+
profileDir: opts.profileDir,
|
|
79
|
+
waitSec: opts.waitSec,
|
|
80
|
+
serverHost: 'bitbucket.org',
|
|
81
|
+
cookieUrls: ['https://bitbucket.org/'],
|
|
82
|
+
userAgent: 'bitbucket-browser-fetch/1.0',
|
|
83
|
+
verifySession: cookie => verifyBitbucketSession(cookie),
|
|
222
84
|
});
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
async function getCookieHeader() {
|
|
226
|
-
const wsUrl = await getPageWsUrl();
|
|
227
|
-
if (!wsUrl) return '';
|
|
228
|
-
const cdp = await connectCdp(wsUrl);
|
|
229
|
-
try {
|
|
230
|
-
await cdp.send('Network.enable');
|
|
231
|
-
const result = await cdp.send('Network.getCookies', { urls: ['https://bitbucket.org/'] });
|
|
232
|
-
return (result.cookies || [])
|
|
233
|
-
.filter(c => c.domain && (c.domain === 'bitbucket.org' || c.domain.endsWith('.bitbucket.org')))
|
|
234
|
-
.map(c => `${c.name}=${c.value}`)
|
|
235
|
-
.join('; ');
|
|
236
|
-
} finally {
|
|
237
|
-
cdp.close();
|
|
238
|
-
}
|
|
85
|
+
return session;
|
|
239
86
|
}
|
|
240
87
|
|
|
241
88
|
async function fetchJson(url, cookie) {
|
|
242
|
-
|
|
243
|
-
headers: { Cookie: cookie, Accept: 'application/json', 'User-Agent': 'bitbucket-browser-fetch/1.0' },
|
|
244
|
-
redirect: 'follow',
|
|
245
|
-
});
|
|
246
|
-
const text = await res.text();
|
|
247
|
-
let json = null;
|
|
248
|
-
try { json = JSON.parse(text); } catch {}
|
|
249
|
-
return { status: res.status, contentType: res.headers.get('content-type') || '', text, json };
|
|
89
|
+
return getSession().fetchJson(url, cookie, { accept: 'application/json' });
|
|
250
90
|
}
|
|
251
91
|
|
|
252
92
|
async function verifyBitbucketSession(cookie) {
|
|
@@ -260,29 +100,8 @@ async function verifyBitbucketSession(cookie) {
|
|
|
260
100
|
return { ok: false, message: `session probe HTTP ${result.status} from ${url}` };
|
|
261
101
|
}
|
|
262
102
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
console.log(`If prompted in Chrome, complete Bitbucket/Atlassian login for: ${project.browseUrl}`);
|
|
266
|
-
const deadline = Date.now() + opts.waitSec * 1000;
|
|
267
|
-
let last = '';
|
|
268
|
-
let printedWait = false;
|
|
269
|
-
while (Date.now() < deadline) {
|
|
270
|
-
try {
|
|
271
|
-
const cookie = await getCookieHeader();
|
|
272
|
-
const session = await verifyBitbucketSession(cookie);
|
|
273
|
-
if (session.ok) {
|
|
274
|
-
if (process.stdout.isTTY) process.stdout.write('\n');
|
|
275
|
-
console.log(`Authenticated Bitbucket session verified via ${session.url}`);
|
|
276
|
-
return cookie;
|
|
277
|
-
}
|
|
278
|
-
last = session.message;
|
|
279
|
-
} catch (e) { last = e.message; }
|
|
280
|
-
if (process.stdout.isTTY) process.stdout.write(`\r${new Date().toLocaleTimeString()} ${last.padEnd(120).slice(0, 120)}`);
|
|
281
|
-
else if (!printedWait) { console.log(`Waiting up to ${opts.waitSec}s for Bitbucket session...`); printedWait = true; }
|
|
282
|
-
await sleep(3000);
|
|
283
|
-
}
|
|
284
|
-
if (process.stdout.isTTY) process.stdout.write('\n');
|
|
285
|
-
throw new Error(`Could not verify authenticated Bitbucket session. Last result: ${last}`);
|
|
103
|
+
function getCookieWithWait() {
|
|
104
|
+
return getSession().getCookieWithWait(project.browseUrl);
|
|
286
105
|
}
|
|
287
106
|
|
|
288
107
|
async function fetchRepositories(cookie) {
|
|
@@ -43,15 +43,26 @@ Important options:
|
|
|
43
43
|
--no-browser-html skip rendered browser HTML
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
## Shared Atlassian SSO Session
|
|
47
|
+
|
|
48
|
+
All five Atlassian skills (`jira-browser-fetch`, `jira-update`, `confluence-browser-fetch`, `confluence-update`, `bitbucket-browser-fetch`) default to the same Chrome profile (`~/.local/share/atlassian-browser-chrome`) and DevTools port (`9223`). Log in once via any skill and the others reuse that session automatically — no env vars needed.
|
|
49
|
+
|
|
50
|
+
**This is a separate Chrome window from any browser the user already has open.** The script always launches its own profile with remote-debugging enabled; cookies from the user's regular Chrome are not read. The user logs in inside the window the script opens; that session is then reused by every Atlassian skill until it expires.
|
|
51
|
+
|
|
52
|
+
**Reuse signal.** When attaching to an existing session, the script prints `Reusing Chrome DevTools on port 9223` and (if the target tab is open) `Found existing tab for <host>`. When you see those lines, do not ask the user to re-SSO — the session is already valid.
|
|
53
|
+
|
|
54
|
+
If Chrome/Chromium is installed via Flatpak, Snap, or another non-PATH location, set `CHROME=/path/to/launcher` (or a wrapper script) so the script can find the binary.
|
|
55
|
+
|
|
56
|
+
Override with `ATLASSIAN_CHROME_PROFILE` and/or `ATLASSIAN_CHROME_DEBUG_PORT` to relocate the shared profile/port, or use skill-specific `*_CHROME_PROFILE` / `*_CHROME_DEBUG_PORT` env vars for isolation.
|
|
57
|
+
|
|
46
58
|
## Typical Workflow
|
|
47
59
|
|
|
48
60
|
1. If the user gives a Confluence URL, run the script directly with that URL.
|
|
49
61
|
2. If the user gives a title, ask for the space key or use `--cql`.
|
|
50
62
|
3. Show the command before running it.
|
|
51
|
-
4. If Chrome opens, ask the user to complete SSO in that
|
|
52
|
-
5.
|
|
53
|
-
6.
|
|
54
|
-
7. If this is an LLM wiki ingest, process the saved `raw/confluence/...` material into `wiki/` per the project `AGENTS.md`.
|
|
63
|
+
4. If Chrome opens (first run or expired session), ask the user to complete SSO in that window. On subsequent invocations the script reuses the session silently — see the Reuse signal above.
|
|
64
|
+
5. Verify saved files.
|
|
65
|
+
6. If this is an LLM wiki ingest, process the saved `raw/confluence/...` material into `wiki/` per the project `AGENTS.md`.
|
|
55
66
|
|
|
56
67
|
Example:
|
|
57
68
|
|
|
@@ -6,12 +6,16 @@ This skill follows Pi / Agent Skills layout:
|
|
|
6
6
|
confluence-browser-fetch/
|
|
7
7
|
├── SKILL.md
|
|
8
8
|
├── scripts/
|
|
9
|
-
│
|
|
9
|
+
│ ├── atlassian-browser.js # vendored from lib/atlassian-browser.js
|
|
10
|
+
│ ├── confluence-browser-fetch.js
|
|
11
|
+
│ └── lib.js
|
|
10
12
|
└── references/
|
|
11
13
|
├── usage.md
|
|
12
14
|
└── distribution.md
|
|
13
15
|
```
|
|
14
16
|
|
|
17
|
+
`scripts/atlassian-browser.js` is vendored from `lib/atlassian-browser.js` at the repo root by `bin/vendor.js` (run via `npm run vendor`, and automatically before `npm test` and `npm pack`). The skill folder is self-contained, so copying just the `confluence-browser-fetch/` directory works once vendoring has happened. If you hand-copy from a fresh source checkout, run `npm run vendor` first or include all three files in `scripts/`.
|
|
18
|
+
|
|
15
19
|
## Install for Current User
|
|
16
20
|
|
|
17
21
|
```bash
|
|
@@ -106,8 +106,8 @@ By default, pages with matching local `metadata.json` Confluence `version.number
|
|
|
106
106
|
|---|---|
|
|
107
107
|
| `CONFLUENCE_SITE` | Default Atlassian site, e.g. `https://example.atlassian.net` |
|
|
108
108
|
| `CONFLUENCE_RAW_DIR` | Default output raw directory |
|
|
109
|
-
| `CONFLUENCE_CHROME_DEBUG_PORT` | Chrome DevTools port, default `
|
|
110
|
-
| `ATLASSIAN_CHROME_DEBUG_PORT` | Shared Chrome DevTools port for
|
|
109
|
+
| `CONFLUENCE_CHROME_DEBUG_PORT` | Chrome DevTools port, default `9223`; overrides `ATLASSIAN_CHROME_DEBUG_PORT` |
|
|
110
|
+
| `ATLASSIAN_CHROME_DEBUG_PORT` | Shared Chrome DevTools port for all Atlassian browser skills (Jira/Confluence/Bitbucket). Default `9223`. |
|
|
111
111
|
| `CONFLUENCE_FETCH_WAIT_SEC` | Wait timeout, default `900` |
|
|
112
112
|
| `CONFLUENCE_MAX_SEARCH_RESULTS` | Max CQL pages, default `200` |
|
|
113
113
|
| `CONFLUENCE_MAX_ATTACHMENT_SIZE` / `CONFLUENCE_MAX_ATTACHMENT_BYTES` | Max attachment download size, default `5mb`; skipped files are listed in `attachments.json` |
|