gologin-web-access 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.
Files changed (141) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +21 -0
  3. package/README.md +344 -0
  4. package/dist/cli.js +173 -0
  5. package/dist/commands/back.js +13 -0
  6. package/dist/commands/batch.js +81 -0
  7. package/dist/commands/batchChangeTrack.js +99 -0
  8. package/dist/commands/batchExtract.js +97 -0
  9. package/dist/commands/batchScrape.js +140 -0
  10. package/dist/commands/changeTrack.js +65 -0
  11. package/dist/commands/check.js +14 -0
  12. package/dist/commands/click.js +14 -0
  13. package/dist/commands/close.js +19 -0
  14. package/dist/commands/configInit.js +77 -0
  15. package/dist/commands/configShow.js +23 -0
  16. package/dist/commands/cookies.js +22 -0
  17. package/dist/commands/cookiesClear.js +13 -0
  18. package/dist/commands/cookiesImport.js +14 -0
  19. package/dist/commands/crawl.js +71 -0
  20. package/dist/commands/crawlErrors.js +20 -0
  21. package/dist/commands/crawlResult.js +27 -0
  22. package/dist/commands/crawlStart.js +56 -0
  23. package/dist/commands/crawlStatus.js +25 -0
  24. package/dist/commands/current.js +14 -0
  25. package/dist/commands/dblclick.js +14 -0
  26. package/dist/commands/eval.js +20 -0
  27. package/dist/commands/extract.js +44 -0
  28. package/dist/commands/fill.js +15 -0
  29. package/dist/commands/find.js +16 -0
  30. package/dist/commands/focus.js +14 -0
  31. package/dist/commands/forward.js +13 -0
  32. package/dist/commands/get.js +15 -0
  33. package/dist/commands/hover.js +14 -0
  34. package/dist/commands/jobs.js +47 -0
  35. package/dist/commands/map.js +61 -0
  36. package/dist/commands/open.js +22 -0
  37. package/dist/commands/parseDocument.js +34 -0
  38. package/dist/commands/pdf.js +14 -0
  39. package/dist/commands/press.js +15 -0
  40. package/dist/commands/read.js +51 -0
  41. package/dist/commands/reload.js +13 -0
  42. package/dist/commands/run.js +76 -0
  43. package/dist/commands/scrape.js +19 -0
  44. package/dist/commands/scrapeJson.js +24 -0
  45. package/dist/commands/scrapeMarkdown.js +37 -0
  46. package/dist/commands/scrapeScreenshot.js +65 -0
  47. package/dist/commands/scrapeText.js +37 -0
  48. package/dist/commands/screenshot.js +23 -0
  49. package/dist/commands/scroll.js +23 -0
  50. package/dist/commands/scrollIntoView.js +14 -0
  51. package/dist/commands/search.js +39 -0
  52. package/dist/commands/searchBrowser.js +28 -0
  53. package/dist/commands/select.js +15 -0
  54. package/dist/commands/sessions.js +14 -0
  55. package/dist/commands/shared.js +102 -0
  56. package/dist/commands/snapshot.js +18 -0
  57. package/dist/commands/storageClear.js +18 -0
  58. package/dist/commands/storageExport.js +26 -0
  59. package/dist/commands/storageImport.js +23 -0
  60. package/dist/commands/tabClose.js +18 -0
  61. package/dist/commands/tabFocus.js +15 -0
  62. package/dist/commands/tabOpen.js +19 -0
  63. package/dist/commands/tabs.js +13 -0
  64. package/dist/commands/type.js +15 -0
  65. package/dist/commands/uncheck.js +14 -0
  66. package/dist/commands/upload.js +15 -0
  67. package/dist/commands/wait.js +27 -0
  68. package/dist/config.js +260 -0
  69. package/dist/doctor.js +86 -0
  70. package/dist/internal-agent/cli.js +336 -0
  71. package/dist/internal-agent/commands/back.js +12 -0
  72. package/dist/internal-agent/commands/check.js +17 -0
  73. package/dist/internal-agent/commands/click.js +17 -0
  74. package/dist/internal-agent/commands/close.js +12 -0
  75. package/dist/internal-agent/commands/cookies.js +23 -0
  76. package/dist/internal-agent/commands/cookiesClear.js +12 -0
  77. package/dist/internal-agent/commands/cookiesImport.js +18 -0
  78. package/dist/internal-agent/commands/current.js +9 -0
  79. package/dist/internal-agent/commands/dblclick.js +17 -0
  80. package/dist/internal-agent/commands/doctor.js +53 -0
  81. package/dist/internal-agent/commands/eval.js +30 -0
  82. package/dist/internal-agent/commands/fill.js +18 -0
  83. package/dist/internal-agent/commands/find.js +86 -0
  84. package/dist/internal-agent/commands/focus.js +17 -0
  85. package/dist/internal-agent/commands/forward.js +12 -0
  86. package/dist/internal-agent/commands/get.js +19 -0
  87. package/dist/internal-agent/commands/hover.js +17 -0
  88. package/dist/internal-agent/commands/open.js +67 -0
  89. package/dist/internal-agent/commands/pdf.js +18 -0
  90. package/dist/internal-agent/commands/press.js +19 -0
  91. package/dist/internal-agent/commands/reload.js +12 -0
  92. package/dist/internal-agent/commands/screenshot.js +22 -0
  93. package/dist/internal-agent/commands/scroll.js +25 -0
  94. package/dist/internal-agent/commands/scrollIntoView.js +17 -0
  95. package/dist/internal-agent/commands/select.js +18 -0
  96. package/dist/internal-agent/commands/sessions.js +15 -0
  97. package/dist/internal-agent/commands/shared.js +51 -0
  98. package/dist/internal-agent/commands/snapshot.js +16 -0
  99. package/dist/internal-agent/commands/storageClear.js +13 -0
  100. package/dist/internal-agent/commands/storageExport.js +24 -0
  101. package/dist/internal-agent/commands/storageImport.js +20 -0
  102. package/dist/internal-agent/commands/tabClose.js +21 -0
  103. package/dist/internal-agent/commands/tabFocus.js +21 -0
  104. package/dist/internal-agent/commands/tabOpen.js +13 -0
  105. package/dist/internal-agent/commands/tabs.js +17 -0
  106. package/dist/internal-agent/commands/type.js +18 -0
  107. package/dist/internal-agent/commands/uncheck.js +17 -0
  108. package/dist/internal-agent/commands/upload.js +18 -0
  109. package/dist/internal-agent/commands/wait.js +41 -0
  110. package/dist/internal-agent/daemon/browser.js +818 -0
  111. package/dist/internal-agent/daemon/refStore.js +26 -0
  112. package/dist/internal-agent/daemon/server.js +330 -0
  113. package/dist/internal-agent/daemon/sessionManager.js +684 -0
  114. package/dist/internal-agent/daemon/snapshot.js +285 -0
  115. package/dist/internal-agent/lib/config.js +59 -0
  116. package/dist/internal-agent/lib/daemon.js +300 -0
  117. package/dist/internal-agent/lib/errors.js +63 -0
  118. package/dist/internal-agent/lib/types.js +2 -0
  119. package/dist/internal-agent/lib/utils.js +165 -0
  120. package/dist/jobRunner.js +56 -0
  121. package/dist/lib/agentCli.js +158 -0
  122. package/dist/lib/browserRead.js +125 -0
  123. package/dist/lib/browserStructured.js +77 -0
  124. package/dist/lib/changeTracking.js +117 -0
  125. package/dist/lib/cloudApi.js +41 -0
  126. package/dist/lib/concurrency.js +15 -0
  127. package/dist/lib/crawl.js +313 -0
  128. package/dist/lib/document.js +170 -0
  129. package/dist/lib/errors.js +55 -0
  130. package/dist/lib/extract.js +65 -0
  131. package/dist/lib/extractRunner.js +22 -0
  132. package/dist/lib/jobRegistry.js +164 -0
  133. package/dist/lib/output.js +122 -0
  134. package/dist/lib/readSource.js +297 -0
  135. package/dist/lib/runbooks.js +193 -0
  136. package/dist/lib/search.js +727 -0
  137. package/dist/lib/selfCli.js +136 -0
  138. package/dist/lib/structuredScrape.js +83 -0
  139. package/dist/lib/types.js +2 -0
  140. package/dist/lib/unlocker.js +383 -0
  141. package/package.json +67 -0
@@ -0,0 +1,297 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeReadSourceMode = normalizeReadSourceMode;
4
+ exports.readMarkdownContent = readMarkdownContent;
5
+ exports.readHtmlContent = readHtmlContent;
6
+ exports.readTextContent = readTextContent;
7
+ exports.readRenderedHtmlContent = readRenderedHtmlContent;
8
+ exports.assessReadableContent = assessReadableContent;
9
+ exports.extractReadableSegmentFromHtml = extractReadableSegmentFromHtml;
10
+ const cheerio_1 = require("cheerio");
11
+ const config_1 = require("../config");
12
+ const browserRead_1 = require("./browserRead");
13
+ const unlocker_1 = require("./unlocker");
14
+ function normalizeReadSourceMode(value, defaultMode = "auto") {
15
+ if (!value) {
16
+ return defaultMode;
17
+ }
18
+ if (value === "auto" || value === "unlocker" || value === "browser") {
19
+ return value;
20
+ }
21
+ throw new Error(`Unsupported source mode: ${value}`);
22
+ }
23
+ async function readMarkdownContent(url, config, apiKey, options = {}) {
24
+ return readReadableContent(url, config, apiKey, {
25
+ ...options,
26
+ format: "markdown",
27
+ });
28
+ }
29
+ async function readHtmlContent(url, config, apiKey, options = {}) {
30
+ return readReadableContent(url, config, apiKey, {
31
+ ...options,
32
+ format: "html",
33
+ });
34
+ }
35
+ async function readTextContent(url, config, apiKey, options = {}) {
36
+ return readReadableContent(url, config, apiKey, {
37
+ ...options,
38
+ format: "text",
39
+ });
40
+ }
41
+ async function readRenderedHtmlContent(url, config, apiKey, options = {}) {
42
+ const source = options.source ?? "auto";
43
+ if (source === "browser") {
44
+ (0, config_1.requireCloudToken)(config);
45
+ const browser = await (0, browserRead_1.scrapeRenderedHtmlViaBrowser)(url, config, {
46
+ profile: options.profile,
47
+ });
48
+ return {
49
+ html: browser.html,
50
+ renderSource: "browser",
51
+ fallbackAttempted: false,
52
+ fallbackUsed: false,
53
+ };
54
+ }
55
+ const unlocker = await (0, unlocker_1.scrapeRenderedHtml)(url, apiKey, options.request);
56
+ if (source === "unlocker") {
57
+ return {
58
+ html: unlocker.content,
59
+ renderSource: "unlocker",
60
+ fallbackAttempted: false,
61
+ fallbackUsed: false,
62
+ request: unlocker.request,
63
+ };
64
+ }
65
+ const unlockerText = (0, unlocker_1.htmlToText)(unlocker.content);
66
+ const assessment = assessReadableContent(unlocker.content, unlockerText);
67
+ if (!assessment.shouldFallback) {
68
+ return {
69
+ html: unlocker.content,
70
+ renderSource: "unlocker",
71
+ fallbackAttempted: false,
72
+ fallbackUsed: false,
73
+ request: unlocker.request,
74
+ };
75
+ }
76
+ if (!config.cloudToken) {
77
+ return {
78
+ html: unlocker.content,
79
+ renderSource: "unlocker",
80
+ fallbackAttempted: true,
81
+ fallbackUsed: false,
82
+ fallbackReason: `${assessment.reason}; GOLOGIN_CLOUD_TOKEN is not configured`,
83
+ request: unlocker.request,
84
+ };
85
+ }
86
+ const browser = await (0, browserRead_1.scrapeRenderedHtmlViaBrowser)(url, config, {
87
+ profile: options.profile,
88
+ });
89
+ return {
90
+ html: browser.html,
91
+ renderSource: "browser",
92
+ fallbackAttempted: true,
93
+ fallbackUsed: true,
94
+ fallbackReason: assessment.reason,
95
+ request: unlocker.request,
96
+ };
97
+ }
98
+ function assessReadableContent(html, content) {
99
+ const $ = (0, cheerio_1.load)(html);
100
+ const normalizedContentLength = meaningfulTextLength(content);
101
+ const mainLength = meaningfulTextLength($("main").first().text());
102
+ const articleLength = meaningfulTextLength($("article").first().text());
103
+ const paragraphCount = $("p").length;
104
+ const headingCount = $("h1, h2, h3").length;
105
+ const linkCount = $("a[href]").length;
106
+ const scriptCount = $("script").length;
107
+ const shellMarkers = /__next_data__|__nuxt__|window\.__|astro-|mintlify|vitepress|docusaurus|hydration|application\/json/i.test(html);
108
+ const docsUiChromeMarkers = /open in chatgpt|copyask ai|copy ask ai|on this page/i.test(content);
109
+ if (mainLength < 200 && articleLength < 200 && linkCount > 40 && normalizedContentLength < 5000) {
110
+ return {
111
+ shouldFallback: true,
112
+ reason: "Unlocker returned navigation-heavy shell with weak main/article content",
113
+ };
114
+ }
115
+ if (normalizedContentLength < 600 && (shellMarkers || linkCount > 30 || scriptCount > 10)) {
116
+ return {
117
+ shouldFallback: true,
118
+ reason: "Unlocker returned very little readable text from a likely JS-rendered page",
119
+ };
120
+ }
121
+ if (shellMarkers && mainLength < 300 && paragraphCount < 3) {
122
+ return {
123
+ shouldFallback: true,
124
+ reason: "Unlocker output looks like a JS docs shell without rendered article content",
125
+ };
126
+ }
127
+ if (paragraphCount < 3 && headingCount <= 1 && linkCount > 60 && scriptCount > 15) {
128
+ return {
129
+ shouldFallback: true,
130
+ reason: "Unlocker output is link-heavy and content-light",
131
+ };
132
+ }
133
+ if (docsUiChromeMarkers) {
134
+ return {
135
+ shouldFallback: true,
136
+ reason: "Unlocker output still contains docs UI chrome and action controls",
137
+ };
138
+ }
139
+ return { shouldFallback: false };
140
+ }
141
+ async function readReadableContent(url, config, apiKey, options) {
142
+ const source = options.source ?? "auto";
143
+ if (source === "browser") {
144
+ (0, config_1.requireCloudToken)(config);
145
+ const browser = await (0, browserRead_1.scrapeReadableContentViaBrowser)(url, config, {
146
+ profile: options.profile,
147
+ });
148
+ return {
149
+ content: formatReadableContent(options.format, browser.html, browser.text),
150
+ renderSource: "browser",
151
+ fallbackAttempted: false,
152
+ fallbackUsed: false,
153
+ };
154
+ }
155
+ const unlocker = await (0, unlocker_1.scrapeRenderedHtml)(url, apiKey, options.request);
156
+ const readable = extractReadableSegmentFromHtml(unlocker.content);
157
+ const unlockerContent = formatReadableContent(options.format, readable.html, readable.text);
158
+ if (source === "unlocker") {
159
+ return {
160
+ content: unlockerContent,
161
+ renderSource: "unlocker",
162
+ fallbackAttempted: false,
163
+ fallbackUsed: false,
164
+ request: unlocker.request,
165
+ };
166
+ }
167
+ const assessment = assessReadableContent(unlocker.content, unlockerContent);
168
+ if (!assessment.shouldFallback) {
169
+ return {
170
+ content: unlockerContent,
171
+ renderSource: "unlocker",
172
+ fallbackAttempted: false,
173
+ fallbackUsed: false,
174
+ request: unlocker.request,
175
+ };
176
+ }
177
+ if (!config.cloudToken) {
178
+ return {
179
+ content: unlockerContent,
180
+ renderSource: "unlocker",
181
+ fallbackAttempted: true,
182
+ fallbackUsed: false,
183
+ fallbackReason: `${assessment.reason}; GOLOGIN_CLOUD_TOKEN is not configured`,
184
+ request: unlocker.request,
185
+ };
186
+ }
187
+ const browser = await (0, browserRead_1.scrapeReadableContentViaBrowser)(url, config, {
188
+ profile: options.profile,
189
+ });
190
+ const browserContent = formatReadableContent(options.format, browser.html, browser.text);
191
+ if (meaningfulTextLength(browserContent) < Math.max(300, meaningfulTextLength(unlockerContent))) {
192
+ return {
193
+ content: unlockerContent,
194
+ renderSource: "unlocker",
195
+ fallbackAttempted: true,
196
+ fallbackUsed: false,
197
+ fallbackReason: "Browser fallback did not improve readable output",
198
+ request: unlocker.request,
199
+ };
200
+ }
201
+ return {
202
+ content: browserContent,
203
+ renderSource: "browser",
204
+ fallbackAttempted: true,
205
+ fallbackUsed: true,
206
+ fallbackReason: assessment.reason,
207
+ request: unlocker.request,
208
+ };
209
+ }
210
+ function formatReadableContent(format, html, text) {
211
+ if (format === "html") {
212
+ return html;
213
+ }
214
+ return format === "markdown" ? (0, unlocker_1.htmlToMarkdown)(html) : text.trim();
215
+ }
216
+ function meaningfulTextLength(value) {
217
+ return value.replace(/\s+/g, " ").trim().length;
218
+ }
219
+ function extractReadableSegmentFromHtml(html) {
220
+ const $ = (0, cheerio_1.load)(html);
221
+ const candidates = [
222
+ { selector: "#content-area", element: $("#content-area").first() },
223
+ { selector: "main article", element: $("main article").first() },
224
+ { selector: "article", element: $("article").first() },
225
+ { selector: "main .prose", element: $("main .prose").first() },
226
+ { selector: "main", element: $("main").first() },
227
+ { selector: "[role='main']", element: $("[role='main']").first() },
228
+ { selector: ".mintlify-content", element: $(".mintlify-content").first() },
229
+ { selector: ".docs-content", element: $(".docs-content").first() },
230
+ { selector: ".content", element: $(".content").first() },
231
+ { selector: ".prose", element: $(".prose").first() },
232
+ { selector: "body", element: $("body").first() },
233
+ ];
234
+ for (const candidate of candidates.filter((item) => item.selector !== "body")) {
235
+ if (candidate.element.length === 0) {
236
+ continue;
237
+ }
238
+ const text = candidate.element.text();
239
+ const normalizedLength = meaningfulTextLength(text);
240
+ const headings = candidate.element.find("h1, h2, h3").length;
241
+ const paragraphs = candidate.element.find("p, li").length;
242
+ if (normalizedLength >= 600 && (headings >= 1 || paragraphs >= 3)) {
243
+ const cleaned = sanitizeReadableFragment($.html(candidate.element));
244
+ return {
245
+ selector: candidate.selector,
246
+ html: cleaned.html,
247
+ text: cleaned.text,
248
+ };
249
+ }
250
+ }
251
+ let best = {
252
+ selector: "body",
253
+ html: $("body").html() ?? html,
254
+ text: $("body").text(),
255
+ score: Number.NEGATIVE_INFINITY,
256
+ };
257
+ for (const candidate of candidates) {
258
+ if (candidate.element.length === 0) {
259
+ continue;
260
+ }
261
+ const text = candidate.element.text();
262
+ const normalizedLength = meaningfulTextLength(text);
263
+ if (normalizedLength === 0) {
264
+ continue;
265
+ }
266
+ const headings = candidate.element.find("h1, h2, h3").length;
267
+ const paragraphs = candidate.element.find("p, li").length;
268
+ const codeBlocks = candidate.element.find("pre, code").length;
269
+ const links = candidate.element.find("a[href]").length;
270
+ let score = Math.min(normalizedLength, 12_000) + headings * 180 + paragraphs * 120 + codeBlocks * 80 - links * 8;
271
+ if (/^(#content-area|article|main|\[role='main'\])/.test(candidate.selector)) {
272
+ score += 400;
273
+ }
274
+ if (score > best.score) {
275
+ const cleaned = sanitizeReadableFragment($.html(candidate.element));
276
+ best = {
277
+ selector: candidate.selector,
278
+ html: cleaned.html,
279
+ text: cleaned.text,
280
+ score,
281
+ };
282
+ }
283
+ }
284
+ return {
285
+ selector: best.selector,
286
+ html: best.html,
287
+ text: best.text.replace(/\s+/g, " ").trim(),
288
+ };
289
+ }
290
+ function sanitizeReadableFragment(fragmentHtml) {
291
+ const $ = (0, cheerio_1.load)(fragmentHtml);
292
+ $("script, style, nav, aside, form, button, svg, dialog, [role='button'], [aria-label='More actions'], .sr-only").remove();
293
+ return {
294
+ html: $.root().html() ?? fragmentHtml,
295
+ text: $.root().text().replace(/\s+/g, " ").trim(),
296
+ };
297
+ }
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loadRunbookDefinition = loadRunbookDefinition;
7
+ exports.loadBatchDefinition = loadBatchDefinition;
8
+ exports.loadVariablesFile = loadVariablesFile;
9
+ exports.executeRunbook = executeRunbook;
10
+ exports.executeBatch = executeBatch;
11
+ const fs_1 = require("fs");
12
+ const path_1 = __importDefault(require("path"));
13
+ const selfCli_1 = require("./selfCli");
14
+ const SESSION_SCOPED_COMMANDS = new Set([
15
+ "open",
16
+ "search-browser",
17
+ "tabs",
18
+ "tabopen",
19
+ "tabfocus",
20
+ "tabclose",
21
+ "snapshot",
22
+ "click",
23
+ "dblclick",
24
+ "focus",
25
+ "type",
26
+ "fill",
27
+ "hover",
28
+ "select",
29
+ "check",
30
+ "uncheck",
31
+ "press",
32
+ "scroll",
33
+ "scrollintoview",
34
+ "wait",
35
+ "get",
36
+ "back",
37
+ "forward",
38
+ "reload",
39
+ "find",
40
+ "cookies",
41
+ "cookies-import",
42
+ "cookies-clear",
43
+ "storage-export",
44
+ "storage-import",
45
+ "storage-clear",
46
+ "eval",
47
+ "upload",
48
+ "pdf",
49
+ "screenshot",
50
+ "close"
51
+ ]);
52
+ const PROFILE_SCOPED_COMMANDS = new Set(["open", "search-browser", "scrape-screenshot"]);
53
+ const DISALLOWED_COMMANDS = new Set(["run", "batch", "__job-runner"]);
54
+ function loadRunbookDefinition(baseDir, filePath) {
55
+ const absolutePath = ensureAbsolutePath(baseDir, filePath);
56
+ return JSON.parse((0, fs_1.readFileSync)(absolutePath, "utf8"));
57
+ }
58
+ function loadBatchDefinition(baseDir, filePath) {
59
+ const absolutePath = ensureAbsolutePath(baseDir, filePath);
60
+ const parsed = JSON.parse((0, fs_1.readFileSync)(absolutePath, "utf8"));
61
+ if (Array.isArray(parsed)) {
62
+ return { targets: parsed };
63
+ }
64
+ return parsed;
65
+ }
66
+ function loadVariablesFile(baseDir, filePath) {
67
+ const absolutePath = ensureAbsolutePath(baseDir, filePath);
68
+ return JSON.parse((0, fs_1.readFileSync)(absolutePath, "utf8"));
69
+ }
70
+ async function executeRunbook(runbook, options) {
71
+ const mergedVariables = {
72
+ ...(runbook.variables ?? {}),
73
+ ...(options.variables ?? {})
74
+ };
75
+ const continueOnError = options.continueOnError === true || runbook.continueOnError === true;
76
+ const results = [];
77
+ for (const step of runbook.steps) {
78
+ if (DISALLOWED_COMMANDS.has(step.command)) {
79
+ throw new Error(`Runbook command ${step.command} is not allowed`);
80
+ }
81
+ const invocation = buildStepInvocation(step, mergedVariables, {
82
+ sessionId: options.sessionId,
83
+ profileId: options.profileId
84
+ });
85
+ const startedAt = Date.now();
86
+ const result = await (0, selfCli_1.runSelfCommandCapture)(invocation, { cwd: options.cwd });
87
+ const durationMs = Date.now() - startedAt;
88
+ const stepResult = {
89
+ command: step.command,
90
+ label: step.label,
91
+ status: result.exitCode === 0 ? "ok" : "failed",
92
+ durationMs,
93
+ stdout: result.stdout,
94
+ stderr: result.stderr,
95
+ error: result.exitCode === 0 ? undefined : result.stderr.trim() || result.stdout.trim() || `exit ${result.exitCode}`
96
+ };
97
+ results.push(stepResult);
98
+ if (stepResult.status === "failed" && !(continueOnError || step.continueOnError)) {
99
+ throw Object.assign(new Error(stepResult.error ?? `Runbook step failed: ${step.command}`), { steps: results });
100
+ }
101
+ }
102
+ return { steps: results };
103
+ }
104
+ async function executeBatch(runbook, batch, options) {
105
+ const concurrency = Math.max(1, options.concurrency ?? batch.concurrency ?? 2);
106
+ const sharedVariables = {
107
+ ...(batch.variables ?? {}),
108
+ ...(options.variables ?? {})
109
+ };
110
+ const queue = [...batch.targets];
111
+ const results = [];
112
+ await Promise.all(Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
113
+ while (queue.length > 0) {
114
+ const target = queue.shift();
115
+ if (!target) {
116
+ return;
117
+ }
118
+ const name = target.name ?? target.sessionId ?? target.profileId ?? `target-${results.length + 1}`;
119
+ const startedAt = Date.now();
120
+ try {
121
+ const execution = await executeRunbook(runbook, {
122
+ cwd: options.cwd,
123
+ sessionId: target.sessionId,
124
+ profileId: target.profileId,
125
+ variables: {
126
+ ...sharedVariables,
127
+ ...(target.variables ?? {})
128
+ },
129
+ continueOnError: options.continueOnError === true || target.continueOnError === true
130
+ });
131
+ results.push({
132
+ name,
133
+ status: execution.steps.some((step) => step.status === "failed") ? "failed" : "ok",
134
+ durationMs: Date.now() - startedAt,
135
+ sessionId: target.sessionId,
136
+ profileId: target.profileId,
137
+ steps: execution.steps
138
+ });
139
+ }
140
+ catch (error) {
141
+ const steps = error && typeof error === "object" && "steps" in error && Array.isArray(error.steps)
142
+ ? (error.steps ?? [])
143
+ : [];
144
+ results.push({
145
+ name,
146
+ status: "failed",
147
+ durationMs: Date.now() - startedAt,
148
+ sessionId: target.sessionId,
149
+ profileId: target.profileId,
150
+ steps
151
+ });
152
+ }
153
+ }
154
+ }));
155
+ return results.sort((left, right) => left.name.localeCompare(right.name));
156
+ }
157
+ function buildStepInvocation(step, variables, options) {
158
+ const args = [resolveTemplate(step.command, variables)];
159
+ const positionals = (step.args ?? []).map((value) => String(resolvePrimitive(value, variables)));
160
+ const flags = Object.entries(step.flags ?? {}).flatMap(([name, value]) => {
161
+ const resolved = resolvePrimitive(value, variables);
162
+ if (typeof resolved === "boolean") {
163
+ return resolved ? [`--${name}`] : [];
164
+ }
165
+ return [`--${name}`, String(resolved)];
166
+ });
167
+ args.push(...positionals, ...flags);
168
+ if (options.sessionId && SESSION_SCOPED_COMMANDS.has(step.command) && !flagPresent(args, "session")) {
169
+ args.push("--session", options.sessionId);
170
+ }
171
+ if (options.profileId && PROFILE_SCOPED_COMMANDS.has(step.command) && !flagPresent(args, "profile")) {
172
+ args.push("--profile", options.profileId);
173
+ }
174
+ return args;
175
+ }
176
+ function resolvePrimitive(value, variables) {
177
+ return typeof value === "string" ? resolveTemplate(value, variables) : value;
178
+ }
179
+ function resolveTemplate(value, variables) {
180
+ return value.replace(/\$\{([^}]+)\}/g, (_, rawKey) => {
181
+ const key = rawKey.trim();
182
+ if (!(key in variables)) {
183
+ throw new Error(`Missing runbook variable: ${key}`);
184
+ }
185
+ return String(variables[key]);
186
+ });
187
+ }
188
+ function flagPresent(argv, flagName) {
189
+ return argv.includes(`--${flagName}`);
190
+ }
191
+ function ensureAbsolutePath(baseDir, targetPath) {
192
+ return path_1.default.isAbsolute(targetPath) ? targetPath : path_1.default.resolve(baseDir, targetPath);
193
+ }