runline 0.22.0 → 0.24.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 (27) hide show
  1. package/dist/plugins/shiftOcr/src/index.js +13 -0
  2. package/dist/plugins/shiftPages/src/pages.js +72 -15
  3. package/dist/plugins/shiftPages/src/shared.js +5 -0
  4. package/dist/plugins/shiftTranscription/src/index.js +1 -1
  5. package/dist/plugins/shiftTranscription/src/transcription.js +21 -5
  6. package/dist/plugins/shiftWork/src/issues.js +119 -33
  7. package/dist/plugins/steel/src/browser.js +11 -79
  8. package/dist/plugins/steel/src/index.js +2 -0
  9. package/dist/plugins/steel/src/page/bundle.generated.js +11 -0
  10. package/dist/plugins/steel/src/page/cdp.js +232 -0
  11. package/dist/plugins/steel/src/page/driver.js +286 -0
  12. package/dist/plugins/steel/src/page/entry.js +91 -0
  13. package/dist/plugins/steel/src/page/install.js +3 -0
  14. package/dist/plugins/steel/src/page/page-snapshot.js +330 -0
  15. package/dist/plugins/steel/src/page/tools.js +649 -0
  16. package/dist/plugins/steel/src/page/vendor/aria-snapshot/ariaSnapshot.js +411 -0
  17. package/dist/plugins/steel/src/page/vendor/aria-snapshot/ariaSnapshotDistiller.js +207 -0
  18. package/dist/plugins/steel/src/page/vendor/aria-snapshot/ariaTypes.js +62 -0
  19. package/dist/plugins/steel/src/page/vendor/aria-snapshot/cssTokenizer.js +949 -0
  20. package/dist/plugins/steel/src/page/vendor/aria-snapshot/domUtils.js +157 -0
  21. package/dist/plugins/steel/src/page/vendor/aria-snapshot/roleUtils.js +1193 -0
  22. package/dist/plugins/steel/src/page/vendor/aria-snapshot/stringUtils.js +194 -0
  23. package/dist/plugins/steel/src/page/vendor/aria-snapshot/yaml.js +80 -0
  24. package/dist/plugins/steel/src/page/wait-for.js +30 -0
  25. package/dist/plugins/steel/src/sessions.js +2 -7
  26. package/dist/plugins/steel/src/shared.js +12 -0
  27. package/package.json +1 -1
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ // NOTE: this function should not be used to escape any selectors.
17
+ export function escapeWithQuotes(text, char = '\'') {
18
+ const stringified = JSON.stringify(text);
19
+ const escapedText = stringified.substring(1, stringified.length - 1).replace(/\\"/g, '"');
20
+ if (char === '\'')
21
+ return char + escapedText.replace(/[']/g, '\\\'') + char;
22
+ if (char === '"')
23
+ return char + escapedText.replace(/["]/g, '\\"') + char;
24
+ if (char === '`')
25
+ return char + escapedText.replace(/[`]/g, '\\`') + char;
26
+ throw new Error('Invalid escape char');
27
+ }
28
+ export function escapeTemplateString(text) {
29
+ return text
30
+ .replace(/\\/g, '\\\\')
31
+ .replace(/`/g, '\\`')
32
+ .replace(/\$\{/g, '\\${');
33
+ }
34
+ export function isString(obj) {
35
+ return typeof obj === 'string' || obj instanceof String;
36
+ }
37
+ export function toTitleCase(name) {
38
+ return name.charAt(0).toUpperCase() + name.substring(1);
39
+ }
40
+ export function toSnakeCase(name) {
41
+ // E.g. ignoreHTTPSErrors => ignore_https_errors.
42
+ return name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/([A-Z])([A-Z][a-z])/g, '$1_$2').toLowerCase();
43
+ }
44
+ export function formatObject(value, indent = ' ', mode = 'multiline') {
45
+ if (typeof value === 'string')
46
+ return escapeWithQuotes(value, '\'');
47
+ if (Array.isArray(value))
48
+ return `[${value.map(o => formatObject(o)).join(', ')}]`;
49
+ if (typeof value === 'object') {
50
+ const keys = Object.keys(value).filter(key => key !== 'timeout' && value[key] !== undefined).sort();
51
+ if (!keys.length)
52
+ return '{}';
53
+ const tokens = [];
54
+ for (const key of keys)
55
+ tokens.push(`${key}: ${formatObject(value[key])}`);
56
+ if (mode === 'multiline')
57
+ return `{\n${tokens.map(t => indent + t).join(`,\n`)}\n}`;
58
+ return `{ ${tokens.join(', ')} }`;
59
+ }
60
+ return String(value);
61
+ }
62
+ export function formatObjectOrVoid(value, indent = ' ') {
63
+ const result = formatObject(value, indent);
64
+ return result === '{}' ? '' : result;
65
+ }
66
+ export function quoteCSSAttributeValue(text) {
67
+ return `"${text.replace(/["\\]/g, char => '\\' + char)}"`;
68
+ }
69
+ let normalizedWhitespaceCache;
70
+ export function cacheNormalizedWhitespaces() {
71
+ normalizedWhitespaceCache = new Map();
72
+ }
73
+ export function normalizeWhiteSpace(text) {
74
+ let result = normalizedWhitespaceCache?.get(text);
75
+ if (result === undefined) {
76
+ result = text.replace(/[\u200b\u00ad]/g, '').trim().replace(/\s+/g, ' ');
77
+ normalizedWhitespaceCache?.set(text, result);
78
+ }
79
+ return result;
80
+ }
81
+ export function normalizeEscapedRegexQuotes(source) {
82
+ // This function reverses the effect of escapeRegexForSelector below.
83
+ // Odd number of backslashes followed by the quote -> remove unneeded backslash.
84
+ return source.replace(/(^|[^\\])(\\\\)*\\(['"`])/g, '$1$2$3');
85
+ }
86
+ function escapeRegexForSelector(re) {
87
+ // Unicode mode does not allow "identity character escapes", so we do not escape and
88
+ // hope that it does not contain quotes and/or >> signs.
89
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Character_escape
90
+ // TODO: rework RE usages in internal selectors away from literal representation to json, e.g. {source,flags}.
91
+ if (re.unicode || re.unicodeSets)
92
+ return String(re);
93
+ // Even number of backslashes followed by the quote -> insert a backslash.
94
+ return String(re).replace(/(^|[^\\])(\\\\)*(["'`])/g, '$1$2\\$3').replace(/>>/g, '\\>\\>');
95
+ }
96
+ export function escapeForTextSelector(text, exact) {
97
+ if (typeof text !== 'string')
98
+ return escapeRegexForSelector(text);
99
+ return `${JSON.stringify(text)}${exact ? 's' : 'i'}`;
100
+ }
101
+ export function escapeForAttributeSelector(value, exact) {
102
+ if (typeof value !== 'string')
103
+ return escapeRegexForSelector(value);
104
+ // TODO: this should actually be
105
+ // cssEscape(value).replace(/\\ /g, ' ')
106
+ // However, our attribute selectors do not conform to CSS parsing spec,
107
+ // so we escape them differently.
108
+ return `"${value.replace(/\\/g, '\\\\').replace(/["]/g, '\\"')}"${exact ? 's' : 'i'}`;
109
+ }
110
+ export function trimString(input, cap, suffix = '') {
111
+ if (input.length <= cap)
112
+ return input;
113
+ const chars = [...input];
114
+ if (chars.length > cap)
115
+ return chars.slice(0, cap - suffix.length).join('') + suffix;
116
+ return chars.join('');
117
+ }
118
+ export function trimStringWithEllipsis(input, cap) {
119
+ return trimString(input, cap, '\u2026');
120
+ }
121
+ export function truncateDataUrl(url) {
122
+ // Data URLs can carry megabytes of base64 payload, which is never useful in
123
+ // human/AI-facing output. Keep the media type prefix for context, drop the data.
124
+ if (!url.startsWith('data:'))
125
+ return url;
126
+ const comma = url.indexOf(',');
127
+ if (comma === -1)
128
+ return url;
129
+ return url.slice(0, comma + 1) + '\u2026';
130
+ }
131
+ export function escapeRegExp(s) {
132
+ // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
133
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
134
+ }
135
+ const escaped = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', '\'': '&#39;' };
136
+ export function escapeHTMLAttribute(s) {
137
+ return s.replace(/[&<>"']/ug, char => escaped[char]);
138
+ }
139
+ export function escapeHTML(s) {
140
+ return s.replace(/[&<]/ug, char => escaped[char]);
141
+ }
142
+ export function longestCommonSubstring(s1, s2) {
143
+ const n = s1.length;
144
+ const m = s2.length;
145
+ let maxLen = 0;
146
+ let endingIndex = 0;
147
+ // Initialize a 2D array with zeros
148
+ const dp = Array(n + 1)
149
+ .fill(null)
150
+ .map(() => Array(m + 1).fill(0));
151
+ // Build the dp table
152
+ for (let i = 1; i <= n; i++) {
153
+ for (let j = 1; j <= m; j++) {
154
+ if (s1[i - 1] === s2[j - 1]) {
155
+ dp[i][j] = dp[i - 1][j - 1] + 1;
156
+ if (dp[i][j] > maxLen) {
157
+ maxLen = dp[i][j];
158
+ endingIndex = i;
159
+ }
160
+ }
161
+ }
162
+ }
163
+ // Extract the longest common substring
164
+ return s1.slice(endingIndex - maxLen, endingIndex);
165
+ }
166
+ export function parseRegex(regex) {
167
+ if (regex[0] !== '/')
168
+ throw new Error(`Invalid regex, must start with '/': ${regex}`);
169
+ const lastSlash = regex.lastIndexOf('/');
170
+ if (lastSlash <= 0)
171
+ throw new Error(`Invalid regex, must end with '/' followed by optional flags: ${regex}`);
172
+ const source = regex.slice(1, lastSlash);
173
+ const flags = regex.slice(lastSlash + 1);
174
+ return new RegExp(source, flags);
175
+ }
176
+ export function tomlBasicString(value) {
177
+ // JSON.stringify produces a valid TOML basic string: escapes \", \\, \n, \r, \t and uses \uXXXX for control chars.
178
+ return JSON.stringify(value);
179
+ }
180
+ export function tomlArray(values) {
181
+ return `[${values.map(value => tomlBasicString(value)).join(', ')}]`;
182
+ }
183
+ export function tomlMultilineBasicString(value) {
184
+ // Triple-quoted basic string: escape backslashes first, then any literal """ sequences.
185
+ const escaped = value.replace(/\\/g, '\\\\').replace(/"""/g, '\\"\\"\\"');
186
+ return `"""\n${escaped}\n"""`;
187
+ }
188
+ // Semicolons removed from [[\]()#;?] to avoid polynomial backtracking
189
+ // when both that group and (?:;...)* can match runs of semicolons.
190
+ // \d{1,4} relaxed to \d{0,4} so empty params (e.g. ESC[;H) still match.
191
+ export const ansiRegex = new RegExp('([\\u001B\\u009B][[\\]()#?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{0,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~])))', 'g');
192
+ export function stripAnsiEscapes(str) {
193
+ return str.replace(ansiRegex, '');
194
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ export function yamlEscapeKeyIfNeeded(str) {
17
+ if (!yamlStringNeedsQuotes(str))
18
+ return str;
19
+ return `'` + str.replace(/'/g, `''`) + `'`;
20
+ }
21
+ export function yamlEscapeValueIfNeeded(str) {
22
+ if (!yamlStringNeedsQuotes(str))
23
+ return str;
24
+ return '"' + str.replace(/[\\"\x00-\x1f\x7f-\x9f]/g, c => {
25
+ switch (c) {
26
+ case '\\':
27
+ return '\\\\';
28
+ case '"':
29
+ return '\\"';
30
+ case '\b':
31
+ return '\\b';
32
+ case '\f':
33
+ return '\\f';
34
+ case '\n':
35
+ return '\\n';
36
+ case '\r':
37
+ return '\\r';
38
+ case '\t':
39
+ return '\\t';
40
+ default:
41
+ const code = c.charCodeAt(0);
42
+ return '\\x' + code.toString(16).padStart(2, '0');
43
+ }
44
+ }) + '"';
45
+ }
46
+ function yamlStringNeedsQuotes(str) {
47
+ if (str.length === 0)
48
+ return true;
49
+ // Strings with leading or trailing whitespace need quotes
50
+ if (/^\s|\s$/.test(str))
51
+ return true;
52
+ // Strings containing control characters need quotes
53
+ if (/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/.test(str))
54
+ return true;
55
+ // Strings starting with '-' need quotes
56
+ if (/^-/.test(str))
57
+ return true;
58
+ // Strings containing ':' or '\n' followed by a space or at the end need quotes
59
+ if (/[\n:](\s|$)/.test(str))
60
+ return true;
61
+ // Strings containing '#' preceded by a space need quotes (comment indicator)
62
+ if (/\s#/.test(str))
63
+ return true;
64
+ // Strings that contain line breaks need quotes
65
+ if (/[\n\r]/.test(str))
66
+ return true;
67
+ // Strings starting with indicator characters or quotes need quotes
68
+ if (/^[&*\],?!>|@"'#%]/.test(str))
69
+ return true;
70
+ // Strings containing special characters that could cause ambiguity
71
+ if (/[{}`]/.test(str))
72
+ return true;
73
+ // YAML array starts with [
74
+ if (/^\[/.test(str))
75
+ return true;
76
+ // Non-string types recognized by YAML
77
+ if (!isNaN(Number(str)) || ['y', 'n', 'yes', 'no', 'true', 'false', 'on', 'off', 'null'].includes(str.toLowerCase()))
78
+ return true;
79
+ return false;
80
+ }
@@ -0,0 +1,30 @@
1
+ export async function waitForPage(document, request, options = {}) {
2
+ const now = options.now ?? Date.now;
3
+ const sleep = options.sleep ??
4
+ (async (ms) => {
5
+ await new Promise((resolve) => setTimeout(resolve, ms));
6
+ });
7
+ const timeoutMs = Math.min(30, Math.max(0, request.time ?? 5)) * 1_000;
8
+ const started = now();
9
+ if (!request.text && !request.textGone) {
10
+ await sleep(timeoutMs);
11
+ return {
12
+ matched: true,
13
+ timedOut: false,
14
+ elapsedMs: Math.max(0, now() - started),
15
+ };
16
+ }
17
+ while (true) {
18
+ const text = document.body?.innerText ?? document.body?.textContent ?? "";
19
+ const found = !request.text || text.includes(request.text);
20
+ const gone = !request.textGone || !text.includes(request.textGone);
21
+ const elapsedMs = Math.max(0, now() - started);
22
+ if (found && gone) {
23
+ return { matched: true, timedOut: false, elapsedMs };
24
+ }
25
+ if (elapsedMs >= timeoutMs) {
26
+ return { matched: false, timedOut: true, elapsedMs };
27
+ }
28
+ await sleep(Math.min(100, timeoutMs - elapsedMs));
29
+ }
30
+ }
@@ -1,5 +1,5 @@
1
1
  import * as t from "typebox";
2
- import { LIST_INPUT_SCHEMA, SESSION_OPTIONS_SCHEMA, api, apiKey, compactRecord } from "./shared.js";
2
+ import { LIST_INPUT_SCHEMA, SESSION_OPTIONS_SCHEMA, api, cdpUrl, compactRecord } from "./shared.js";
3
3
  export function registerSessionActions(rl) {
4
4
  rl.registerAction("session.create", {
5
5
  access: "write",
@@ -119,12 +119,7 @@ export function registerSessionActions(rl) {
119
119
  inputSchema: t.Object({ id: t.String({ description: "Session ID" }), websocketUrl: t.Optional(t.String({ description: "Optional websocketUrl returned by session.create. If omitted, uses wss://connect.steel.dev with sessionId." })) }),
120
120
  async execute(input, ctx) {
121
121
  const { id, websocketUrl } = input;
122
- const key = encodeURIComponent(apiKey(ctx));
123
- if (websocketUrl) {
124
- const sep = websocketUrl.includes("?") ? "&" : "?";
125
- return { cdpUrl: `${websocketUrl}${sep}apiKey=${key}` };
126
- }
127
- return { cdpUrl: `wss://connect.steel.dev?apiKey=${key}&sessionId=${encodeURIComponent(id)}` };
122
+ return { cdpUrl: cdpUrl(ctx, id, websocketUrl) };
128
123
  },
129
124
  });
130
125
  }
@@ -45,6 +45,18 @@ export async function api(ctx, path, options = {}) {
45
45
  return JSON.parse(text);
46
46
  return text;
47
47
  }
48
+ /**
49
+ * The CDP endpoint for a session. One definition: this URL carries the
50
+ * API key, so a second hand-written copy is a second place to leak it or
51
+ * to forget encoding.
52
+ */
53
+ export function cdpUrl(ctx, sessionId, websocketUrl) {
54
+ const key = encodeURIComponent(apiKey(ctx));
55
+ if (websocketUrl) {
56
+ return `${websocketUrl}${websocketUrl.includes("?") ? "&" : "?"}apiKey=${key}`;
57
+ }
58
+ return `wss://connect.steel.dev?apiKey=${key}&sessionId=${encodeURIComponent(sessionId)}`;
59
+ }
48
60
  export function compactRecord(input) {
49
61
  return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined && value !== null));
50
62
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runline",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "Code mode for agents \u2014 turn any API or command into a callable action",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",