@aholbreich/agent-skills 0.9.0 → 1.0.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.
@@ -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 { spawn } = require('child_process');
7
+ const { createBrowserSession } = require('./atlassian-browser');
9
8
  const lib = require('./lib');
10
9
  const {
11
10
  safeName,
@@ -133,7 +132,6 @@ for (let i = 0; i < args.length; i++) {
133
132
  opts.site = opts.site.replace(/\/$/, '');
134
133
  opts.rawDir = path.resolve(opts.rawDir);
135
134
  const wikiBase = opts.site ? `${opts.site}/wiki` : '';
136
- const sleep = ms => new Promise(r => setTimeout(r, ms));
137
135
 
138
136
  function failUsage(message) {
139
137
  console.error(message);
@@ -153,210 +151,42 @@ if (opts.command === 'create') {
153
151
  }
154
152
  if (opts.expectedVersion !== null && opts.expectedVersion !== 'auto' && (!Number.isInteger(opts.expectedVersion) || opts.expectedVersion < 1)) failUsage('--expected-version must be "auto" or a positive integer.');
155
153
 
156
- async function endpoint(pathname) {
157
- const res = await fetch(`http://127.0.0.1:${opts.port}${pathname}`);
158
- if (!res.ok) throw new Error(`DevTools HTTP ${res.status} for ${pathname}`);
159
- return res.json();
160
- }
161
-
162
- async function devtoolsReady() {
163
- try { await endpoint('/json/version'); return true; } catch { return false; }
164
- }
165
-
166
- async function waitDevtools() {
167
- for (let i = 0; i < 80; i++) {
168
- if (await devtoolsReady()) return;
169
- await sleep(250);
170
- }
171
- throw new Error('Chrome DevTools endpoint did not start');
172
- }
173
-
174
- async function openDevtoolsTab(url) {
175
- if (!url) return false;
176
- const endpointUrl = `http://127.0.0.1:${opts.port}/json/new?${encodeURIComponent(url)}`;
177
- for (const init of [{ method: 'PUT' }, {}]) {
178
- try {
179
- const res = await fetch(endpointUrl, init);
180
- if (res.ok) { await sleep(500); return true; }
181
- } catch {}
182
- }
183
- return false;
184
- }
185
-
186
- async function hasDevtoolsTabForWiki(url) {
187
- if (!url) return false;
188
- const host = new URL(url).host;
189
- const list = await endpoint('/json/list');
190
- return list.some(t => t.type === 'page' && t.url && (() => {
191
- try {
192
- const tabUrl = new URL(t.url);
193
- return tabUrl.host === host && tabUrl.pathname.startsWith('/wiki');
194
- } catch { return false; }
195
- })());
196
- }
197
-
198
- function isExecutable(file) {
199
- try { fs.accessSync(file, fs.constants.X_OK); return true; } catch { return false; }
200
- }
201
-
202
- function resolveBrowserCandidate(candidate) {
203
- if (!candidate) return null;
204
- if (candidate.includes(path.sep)) return isExecutable(candidate) ? candidate : null;
205
- for (const dir of String(process.env.PATH || '').split(path.delimiter)) {
206
- if (!dir) continue;
207
- const full = path.join(dir, candidate);
208
- if (isExecutable(full)) return full;
209
- }
210
- return null;
211
- }
212
-
213
- function findBrowserExecutable() {
214
- const candidates = [
215
- process.env.CHROME,
216
- process.env.CHROMIUM,
217
- 'google-chrome',
218
- 'google-chrome-stable',
219
- 'chromium',
220
- 'chromium-browser',
221
- 'brave-browser',
222
- 'brave',
223
- 'microsoft-edge',
224
- 'microsoft-edge-stable',
225
- 'vivaldi',
226
- 'vivaldi-stable',
227
- '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
228
- '/Applications/Chromium.app/Contents/MacOS/Chromium',
229
- '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
230
- '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
231
- '/Applications/Vivaldi.app/Contents/MacOS/Vivaldi',
232
- ];
233
- for (const candidate of candidates) {
234
- const resolved = resolveBrowserCandidate(candidate);
235
- if (resolved) return resolved;
236
- }
237
- throw new Error('Could not find a Chromium-compatible browser. Install Chrome/Chromium/Brave/Edge/Vivaldi or set CHROME=/path/to/browser.');
238
- }
239
-
240
- function launchChrome(url) {
241
- const browser = findBrowserExecutable();
242
- const args = [
243
- `--remote-debugging-port=${opts.port}`,
244
- '--remote-debugging-address=127.0.0.1',
245
- '--remote-allow-origins=*',
246
- `--user-data-dir=${opts.profileDir}`,
247
- '--no-first-run',
248
- '--no-default-browser-check',
249
- url,
250
- ];
251
- console.log(`Launching browser: ${browser}`);
252
- const child = spawn(browser, args, { detached: true, stdio: 'ignore' });
253
- child.on('error', err => console.error(`Failed to launch browser ${browser}: ${err.message}`));
254
- child.unref();
255
- }
256
-
257
- async function ensureBrowser(openUrl) {
258
- if (!(await devtoolsReady())) {
259
- console.log(`Opening Chromium-compatible browser with reusable profile: ${opts.profileDir}`);
260
- launchChrome(openUrl || wikiBase);
261
- } else {
262
- console.log(`Reusing Chrome DevTools on port ${opts.port}`);
263
- const targetUrl = openUrl || wikiBase;
264
- const hasTab = await hasDevtoolsTabForWiki(targetUrl);
265
- if (hasTab) {
266
- console.log(`Found existing Confluence tab for ${new URL(targetUrl).host}; not opening another tab.`);
267
- } else {
268
- const opened = await openDevtoolsTab(targetUrl);
269
- if (opened) console.log(`Opened target URL in reused browser: ${targetUrl}`);
270
- else console.warn('Could not open target URL through DevTools; continuing with existing tabs.');
271
- }
272
- }
273
- await waitDevtools();
274
- }
275
-
276
- async function getPageWsUrl() {
277
- const list = await endpoint('/json/list');
278
- const pages = list.filter(t => t.type === 'page' && t.webSocketDebuggerUrl);
279
- const host = new URL(opts.site).host;
280
- const preferred = pages.find(t => (t.url || '').includes(host)) || pages[0];
281
- return preferred && preferred.webSocketDebuggerUrl;
282
- }
283
-
284
- function connectCdp(wsUrl) {
285
- return new Promise((resolve, reject) => {
286
- const ws = new WebSocket(wsUrl);
287
- let id = 0;
288
- const pending = new Map();
289
- const failTimer = setTimeout(() => reject(new Error('CDP websocket timeout')), 10000);
290
-
291
- ws.addEventListener('open', () => {
292
- clearTimeout(failTimer);
293
- resolve({
294
- send(method, params = {}) {
295
- return new Promise((res, rej) => {
296
- const msgId = ++id;
297
- pending.set(msgId, { res, rej });
298
- ws.send(JSON.stringify({ id: msgId, method, params }));
299
- });
300
- },
301
- close() { try { ws.close(); } catch {} },
302
- });
303
- });
304
-
305
- ws.addEventListener('message', ev => {
306
- let data = ev.data;
307
- if (typeof data !== 'string') data = Buffer.from(data).toString('utf8');
308
- const msg = JSON.parse(data);
309
- if (!msg.id || !pending.has(msg.id)) return;
310
- const { res, rej } = pending.get(msg.id);
311
- pending.delete(msg.id);
312
- if (msg.error) rej(new Error(`${msg.error.message || 'CDP error'} ${JSON.stringify(msg.error)}`));
313
- else res(msg.result);
314
- });
315
-
316
- ws.addEventListener('error', err => reject(err));
154
+ let session = null;
155
+ function getSession() {
156
+ if (session) return session;
157
+ session = createBrowserSession({
158
+ port: opts.port,
159
+ profileDir: opts.profileDir,
160
+ waitSec: opts.waitSec,
161
+ serverHost: new URL(opts.site).host,
162
+ cookieUrls: [`${opts.site}/`, wikiBase],
163
+ userAgent: 'confluence-update/1.0',
164
+ verifySession: cookie => verifyConfluenceSession(cookie),
317
165
  });
166
+ return session;
318
167
  }
319
168
 
320
- async function getCookieHeader() {
321
- const wsUrl = await getPageWsUrl();
322
- if (!wsUrl) return '';
323
- const cdp = await connectCdp(wsUrl);
324
- try {
325
- await cdp.send('Network.enable');
326
- const host = new URL(opts.site).host;
327
- const result = await cdp.send('Network.getCookies', { urls: [`${opts.site}/`, wikiBase] });
328
- const cookies = (result.cookies || [])
329
- .filter(c => c.domain && (c.domain === host || c.domain.endsWith(`.${host}`)))
330
- .map(c => `${c.name}=${c.value}`);
331
- return cookies.join('; ');
332
- } finally {
333
- cdp.close();
334
- }
335
- }
336
-
337
- async function fetchText(url, cookie, method = 'GET', body = null) {
338
- const headers = {
339
- Cookie: cookie,
340
- Accept: 'application/json',
341
- 'User-Agent': 'confluence-update/1.0',
342
- };
343
- if (body !== null) headers['Content-Type'] = 'application/json';
344
- const res = await fetch(url, { method, redirect: 'follow', headers, body });
345
- return { status: res.status, contentType: res.headers.get('content-type') || '', text: await res.text() };
169
+ async function fetchTextAdapter(url, cookie, method = 'GET', body = null) {
170
+ return getSession().fetchText(url, cookie, {
171
+ method,
172
+ body,
173
+ accept: 'application/json',
174
+ });
346
175
  }
347
176
 
348
- async function fetchJson(url, cookie, method = 'GET', json = null) {
349
- const result = await fetchText(url, cookie, method, json === null ? null : JSON.stringify(json));
350
- let parsed = null;
351
- try { parsed = JSON.parse(result.text); } catch {}
352
- return { ...result, json: parsed };
177
+ async function fetchJsonAdapter(url, cookie, method = 'GET', json = null) {
178
+ return getSession().fetchJson(url, cookie, {
179
+ method,
180
+ body: json === null ? null : JSON.stringify(json),
181
+ accept: 'application/json',
182
+ });
353
183
  }
354
184
 
355
185
  async function verifyConfluenceSession(cookie) {
356
186
  if (!cookie) return { ok: false, message: 'no Atlassian cookies yet' };
357
187
  const probes = [`${wikiBase}/rest/api/user/current`, `${wikiBase}/rest/api/space?limit=1`];
358
188
  for (const url of probes) {
359
- const result = await fetchJson(url, cookie);
189
+ const result = await fetchJsonAdapter(url, cookie);
360
190
  if (result.status === 200 && result.json) return { ok: true, url };
361
191
  if (result.status === 401 || result.status === 403) return { ok: false, message: `not authenticated yet (${result.status} from ${url})` };
362
192
  if (result.status === 302 || result.status === 303) return { ok: false, message: `still redirected to login (${result.status} from ${url})` };
@@ -366,31 +196,8 @@ async function verifyConfluenceSession(cookie) {
366
196
  return { ok: false, message: 'could not verify Confluence session' };
367
197
  }
368
198
 
369
- async function getCookieWithWait(openUrl) {
370
- await ensureBrowser(openUrl || wikiBase);
371
- console.log(`If prompted in Chrome, complete SSO for: ${openUrl || wikiBase}`);
372
- const deadline = Date.now() + opts.waitSec * 1000;
373
- let last = '';
374
- while (Date.now() < deadline) {
375
- try {
376
- const cookie = await getCookieHeader();
377
- const session = await verifyConfluenceSession(cookie);
378
- if (session.ok) {
379
- if (process.stdout.isTTY) process.stdout.write('\n');
380
- console.log(`Authenticated Confluence session verified via ${session.url}`);
381
- return cookie;
382
- }
383
- last = session.message;
384
- } catch (e) { last = e.message; }
385
- if (process.stdout.isTTY) {
386
- process.stdout.write(`\r${new Date().toLocaleTimeString()} ${last.padEnd(120).slice(0, 120)}`);
387
- } else if (Date.now() - deadline + opts.waitSec * 1000 < 4000) {
388
- console.log(`Waiting up to ${opts.waitSec}s for Confluence session...`);
389
- }
390
- await sleep(3000);
391
- }
392
- if (process.stdout.isTTY) process.stdout.write('\n');
393
- throw new Error(`Could not verify authenticated Confluence session. Last result: ${last}`);
199
+ function getCookieWithWait(openUrl) {
200
+ return getSession().getCookieWithWait(openUrl || wikiBase, { tabPathPrefix: '/wiki' });
394
201
  }
395
202
 
396
203
  function pageUrl(pageId) {
@@ -399,7 +206,7 @@ function pageUrl(pageId) {
399
206
 
400
207
  async function getPage(pageId, cookie) {
401
208
  const url = `${wikiBase}/rest/api/content/${pageId}?expand=body.storage,version,space,ancestors`;
402
- const result = await fetchJson(url, cookie);
209
+ const result = await fetchJsonAdapter(url, cookie);
403
210
  if (result.status !== 200 || !result.json || !result.json.id) {
404
211
  throw new Error(`Could not read page ${pageId}. HTTP ${result.status}: ${(result.text || '').slice(0, 300).replace(/\s+/g, ' ')}`);
405
212
  }
@@ -503,7 +310,7 @@ async function runUpdate(cookie, pageId, inputStorage) {
503
310
  console.log('Dry-run only. Re-run with --apply to write to Confluence.');
504
311
  return;
505
312
  }
506
- const result = await fetchJson(`${wikiBase}/rest/api/content/${pageId}`, cookie, 'PUT', payload);
313
+ const result = await fetchJsonAdapter(`${wikiBase}/rest/api/content/${pageId}`, cookie, 'PUT', payload);
507
314
  if (result.status !== 200 || !result.json || !result.json.id) {
508
315
  throw new Error(`Update failed HTTP ${result.status}: ${(result.text || '').slice(0, 500).replace(/\s+/g, ' ')}`);
509
316
  }
@@ -534,7 +341,7 @@ async function runCreate(cookie, inputStorage) {
534
341
  console.log('Dry-run only. Re-run with --apply to write to Confluence.');
535
342
  return;
536
343
  }
537
- const result = await fetchJson(`${wikiBase}/rest/api/content`, cookie, 'POST', payload);
344
+ const result = await fetchJsonAdapter(`${wikiBase}/rest/api/content`, cookie, 'POST', payload);
538
345
  if ((result.status !== 200 && result.status !== 201) || !result.json || !result.json.id) {
539
346
  throw new Error(`Create failed HTTP ${result.status}: ${(result.text || '').slice(0, 500).replace(/\s+/g, ' ')}`);
540
347
  }