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.
- package/dist/plugins/shiftOcr/src/index.js +13 -0
- package/dist/plugins/shiftPages/src/pages.js +72 -15
- package/dist/plugins/shiftPages/src/shared.js +5 -0
- package/dist/plugins/shiftTranscription/src/index.js +1 -1
- package/dist/plugins/shiftTranscription/src/transcription.js +21 -5
- package/dist/plugins/shiftWork/src/issues.js +119 -33
- package/dist/plugins/steel/src/browser.js +11 -79
- package/dist/plugins/steel/src/index.js +2 -0
- package/dist/plugins/steel/src/page/bundle.generated.js +11 -0
- package/dist/plugins/steel/src/page/cdp.js +232 -0
- package/dist/plugins/steel/src/page/driver.js +286 -0
- package/dist/plugins/steel/src/page/entry.js +91 -0
- package/dist/plugins/steel/src/page/install.js +3 -0
- package/dist/plugins/steel/src/page/page-snapshot.js +330 -0
- package/dist/plugins/steel/src/page/tools.js +649 -0
- package/dist/plugins/steel/src/page/vendor/aria-snapshot/ariaSnapshot.js +411 -0
- package/dist/plugins/steel/src/page/vendor/aria-snapshot/ariaSnapshotDistiller.js +207 -0
- package/dist/plugins/steel/src/page/vendor/aria-snapshot/ariaTypes.js +62 -0
- package/dist/plugins/steel/src/page/vendor/aria-snapshot/cssTokenizer.js +949 -0
- package/dist/plugins/steel/src/page/vendor/aria-snapshot/domUtils.js +157 -0
- package/dist/plugins/steel/src/page/vendor/aria-snapshot/roleUtils.js +1193 -0
- package/dist/plugins/steel/src/page/vendor/aria-snapshot/stringUtils.js +194 -0
- package/dist/plugins/steel/src/page/vendor/aria-snapshot/yaml.js +80 -0
- package/dist/plugins/steel/src/page/wait-for.js +30 -0
- package/dist/plugins/steel/src/sessions.js +2 -7
- package/dist/plugins/steel/src/shared.js +12 -0
- package/package.json +1 -1
|
@@ -114,6 +114,17 @@ export default function shiftOcr(rl) {
|
|
|
114
114
|
description: "Override for URLs whose extension does not reveal the type. " +
|
|
115
115
|
"Inferred from the extension otherwise.",
|
|
116
116
|
})),
|
|
117
|
+
provider: t.Optional(t.String({
|
|
118
|
+
minLength: 1,
|
|
119
|
+
pattern: "\\S",
|
|
120
|
+
description: "Provider ID from ocr.providers. Defaults to the first " +
|
|
121
|
+
"catalog provider.",
|
|
122
|
+
})),
|
|
123
|
+
model: t.Optional(t.String({
|
|
124
|
+
minLength: 1,
|
|
125
|
+
pattern: "\\S",
|
|
126
|
+
description: "Model override. Defaults to the provider's defaultModel.",
|
|
127
|
+
})),
|
|
117
128
|
pages: t.Optional(t.String({
|
|
118
129
|
minLength: 1,
|
|
119
130
|
pattern: "^\\d+(-\\d+)?(,\\d+(-\\d+)?)*$",
|
|
@@ -151,6 +162,8 @@ export default function shiftOcr(rl) {
|
|
|
151
162
|
method: "POST",
|
|
152
163
|
body: JSON.stringify({
|
|
153
164
|
document,
|
|
165
|
+
...(fields.provider ? { provider: fields.provider } : {}),
|
|
166
|
+
...(fields.model ? { model: fields.model } : {}),
|
|
154
167
|
...(fields.pages ? { pages: fields.pages } : {}),
|
|
155
168
|
...(fields.schema
|
|
156
169
|
? {
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
-
import { enumSchema, idSchema, listParams, PAGE_STATUS, PAGE_VISIBILITY, pageRenderUrl, pathSegment, request, STRICT_OBJECT, timestampSchema, withQuery, } from "./shared.js";
|
|
2
|
+
import { enumSchema, idSchema, listParams, PAGE_STATUS, PAGE_TYPE, PAGE_VISIBILITY, pageRenderUrl, pathSegment, request, STRICT_OBJECT, timestampSchema, withQuery, } from "./shared.js";
|
|
3
3
|
const PageSlug = t.String({
|
|
4
4
|
minLength: 1,
|
|
5
5
|
maxLength: 120,
|
|
6
6
|
pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
7
7
|
description: "Lowercase kebab-case page slug",
|
|
8
8
|
});
|
|
9
|
+
const OriginUrl = t.String({
|
|
10
|
+
minLength: 1,
|
|
11
|
+
format: "uri",
|
|
12
|
+
pattern: "^https://",
|
|
13
|
+
description: "HTTPS origin to proxy behind SSO (protected_origin pages only). " +
|
|
14
|
+
"Localhost and private IP hosts are rejected.",
|
|
15
|
+
});
|
|
9
16
|
export function registerPageActions(rl) {
|
|
10
17
|
rl.registerAction("page.list", {
|
|
11
18
|
access: "read",
|
|
@@ -33,9 +40,23 @@ export function registerPageActions(rl) {
|
|
|
33
40
|
return body.page;
|
|
34
41
|
},
|
|
35
42
|
});
|
|
43
|
+
rl.registerAction("page.vexArtifacts", {
|
|
44
|
+
access: "read",
|
|
45
|
+
description: "List the shareable Vex artifacts on a managed deployment, sorted by " +
|
|
46
|
+
"title, each carrying the workspace it belongs to. Pass an artifact's " +
|
|
47
|
+
"ID to page.create with type vex_artifact to publish it. The " +
|
|
48
|
+
"deployment must belong to the API key's organization.",
|
|
49
|
+
inputSchema: t.Object({ deploymentId: idSchema("Managed Vex deployment ID") }, STRICT_OBJECT),
|
|
50
|
+
async execute(input, ctx) {
|
|
51
|
+
const body = await request(ctx, withQuery("/v1/pages/vex-artifacts", listParams(input)));
|
|
52
|
+
return body.artifacts;
|
|
53
|
+
},
|
|
54
|
+
});
|
|
36
55
|
rl.registerAction("page.create", {
|
|
37
56
|
access: "write",
|
|
38
|
-
description: "Create a draft
|
|
57
|
+
description: "Create a draft page and publish it with page.publish. hosted_html " +
|
|
58
|
+
"pages carry their own HTML; vex_artifact pages render a live Vex " +
|
|
59
|
+
"artifact (pass deploymentId and artifactId from page.vexArtifacts).",
|
|
39
60
|
inputSchema: t.Object({
|
|
40
61
|
slug: PageSlug,
|
|
41
62
|
title: t.String({
|
|
@@ -44,14 +65,19 @@ export function registerPageActions(rl) {
|
|
|
44
65
|
pattern: "\\S",
|
|
45
66
|
description: "Page title",
|
|
46
67
|
}),
|
|
68
|
+
type: t.Optional(enumSchema("Page type", PAGE_TYPE)),
|
|
47
69
|
html: t.Optional(t.String({
|
|
48
70
|
maxLength: 1_000_000,
|
|
49
|
-
description: "Hosted HTML content",
|
|
71
|
+
description: "Hosted HTML content (hosted_html pages only)",
|
|
50
72
|
})),
|
|
73
|
+
originUrl: t.Optional(OriginUrl),
|
|
74
|
+
deploymentId: t.Optional(idSchema("Managed Vex deployment ID (vex_artifact pages only)")),
|
|
75
|
+
artifactId: t.Optional(idSchema("Vex artifact ID (vex_artifact pages only)")),
|
|
51
76
|
visibility: t.Optional(enumSchema("Page visibility", PAGE_VISIBILITY)),
|
|
52
77
|
}, STRICT_OBJECT),
|
|
53
78
|
async execute(input, ctx) {
|
|
54
79
|
const fields = input;
|
|
80
|
+
assertPageContent(fields);
|
|
55
81
|
const body = await request(ctx, "/v1/pages", {
|
|
56
82
|
method: "POST",
|
|
57
83
|
body: JSON.stringify({
|
|
@@ -65,7 +91,9 @@ export function registerPageActions(rl) {
|
|
|
65
91
|
});
|
|
66
92
|
rl.registerAction("page.update", {
|
|
67
93
|
access: "write",
|
|
68
|
-
description: "Update a
|
|
94
|
+
description: "Update a page's slug, title, visibility, HTML, or — for vex_artifact " +
|
|
95
|
+
"pages — which deployment and artifact it renders. A page's type is " +
|
|
96
|
+
"fixed at creation.",
|
|
69
97
|
inputSchema: t.Object({
|
|
70
98
|
id: idSchema("Page ID"),
|
|
71
99
|
slug: t.Optional(PageSlug),
|
|
@@ -78,8 +106,11 @@ export function registerPageActions(rl) {
|
|
|
78
106
|
visibility: t.Optional(enumSchema("Page visibility", PAGE_VISIBILITY)),
|
|
79
107
|
html: t.Optional(t.String({
|
|
80
108
|
maxLength: 1_000_000,
|
|
81
|
-
description: "Hosted HTML content",
|
|
109
|
+
description: "Hosted HTML content (hosted_html pages only)",
|
|
82
110
|
})),
|
|
111
|
+
originUrl: t.Optional(OriginUrl),
|
|
112
|
+
deploymentId: t.Optional(idSchema("Managed Vex deployment ID (vex_artifact pages only)")),
|
|
113
|
+
artifactId: t.Optional(idSchema("Vex artifact ID (vex_artifact pages only)")),
|
|
83
114
|
}, STRICT_OBJECT),
|
|
84
115
|
async execute(input, ctx) {
|
|
85
116
|
const { id, ...fields } = input;
|
|
@@ -92,7 +123,8 @@ export function registerPageActions(rl) {
|
|
|
92
123
|
});
|
|
93
124
|
rl.registerAction("page.publish", {
|
|
94
125
|
access: "write",
|
|
95
|
-
description: "Publish a
|
|
126
|
+
description: "Publish a draft page, making it reachable at its render URL by the " +
|
|
127
|
+
"organization (or by invited viewers when visibility is invited).",
|
|
96
128
|
inputSchema: t.Object({ id: idSchema("Page ID") }, STRICT_OBJECT),
|
|
97
129
|
async execute(input, ctx) {
|
|
98
130
|
const { id } = input;
|
|
@@ -113,10 +145,10 @@ export function registerPageActions(rl) {
|
|
|
113
145
|
rl.registerAction("page.shares", {
|
|
114
146
|
access: "read",
|
|
115
147
|
description: "List shares for a Shift Labs page.",
|
|
116
|
-
inputSchema: t.Object({
|
|
148
|
+
inputSchema: t.Object({ id: idSchema("Page ID") }, STRICT_OBJECT),
|
|
117
149
|
async execute(input, ctx) {
|
|
118
|
-
const {
|
|
119
|
-
const body = await request(ctx, `/v1/pages/${pathSegment(
|
|
150
|
+
const { id } = input;
|
|
151
|
+
const body = await request(ctx, `/v1/pages/${pathSegment(id)}/shares`);
|
|
120
152
|
return body.shares;
|
|
121
153
|
},
|
|
122
154
|
});
|
|
@@ -124,7 +156,7 @@ export function registerPageActions(rl) {
|
|
|
124
156
|
access: "write",
|
|
125
157
|
description: "Create a viewer share for a Shift Labs page.",
|
|
126
158
|
inputSchema: t.Object({
|
|
127
|
-
|
|
159
|
+
id: idSchema("Page ID"),
|
|
128
160
|
email: t.String({
|
|
129
161
|
minLength: 1,
|
|
130
162
|
format: "email",
|
|
@@ -133,8 +165,8 @@ export function registerPageActions(rl) {
|
|
|
133
165
|
expiresAt: t.Optional(timestampSchema("Optional ISO-8601 expiration")),
|
|
134
166
|
}, STRICT_OBJECT),
|
|
135
167
|
async execute(input, ctx) {
|
|
136
|
-
const {
|
|
137
|
-
const body = await request(ctx, `/v1/pages/${pathSegment(String(
|
|
168
|
+
const { id, ...fields } = input;
|
|
169
|
+
const body = await request(ctx, `/v1/pages/${pathSegment(String(id))}/shares`, {
|
|
138
170
|
method: "POST",
|
|
139
171
|
body: JSON.stringify(fields),
|
|
140
172
|
});
|
|
@@ -156,13 +188,38 @@ export function registerPageActions(rl) {
|
|
|
156
188
|
rl.registerAction("page.renderUrl", {
|
|
157
189
|
access: "read",
|
|
158
190
|
description: "Return the authenticated render URL for a page.",
|
|
159
|
-
inputSchema: t.Object({
|
|
191
|
+
inputSchema: t.Object({ id: idSchema("Page ID") }, STRICT_OBJECT),
|
|
160
192
|
async execute(input, ctx) {
|
|
161
|
-
const {
|
|
162
|
-
const body = await request(ctx, `/v1/pages/${pathSegment(
|
|
193
|
+
const { id } = input;
|
|
194
|
+
const body = await request(ctx, `/v1/pages/${pathSegment(id)}`);
|
|
163
195
|
return {
|
|
164
196
|
url: pageRenderUrl(body.page.organizationId, body.page.slug),
|
|
165
197
|
};
|
|
166
198
|
},
|
|
167
199
|
});
|
|
168
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* The content rules the API enforces per page type, checked before the
|
|
203
|
+
* round-trip so a caller gets the reason rather than a bare 400. Each
|
|
204
|
+
* type owns exactly one content field.
|
|
205
|
+
*/
|
|
206
|
+
function assertPageContent(fields) {
|
|
207
|
+
const type = fields.type ?? "hosted_html";
|
|
208
|
+
const referencesArtifact = fields.deploymentId !== undefined || fields.artifactId !== undefined;
|
|
209
|
+
if (type !== "vex_artifact" && referencesArtifact) {
|
|
210
|
+
throw new Error("Only vex_artifact pages may reference a deployment or artifact");
|
|
211
|
+
}
|
|
212
|
+
if (type !== "protected_origin" && fields.originUrl !== undefined) {
|
|
213
|
+
throw new Error("Only protected_origin pages may set an originUrl");
|
|
214
|
+
}
|
|
215
|
+
if (type !== "hosted_html" && fields.html !== undefined) {
|
|
216
|
+
throw new Error("Only hosted_html pages may carry html");
|
|
217
|
+
}
|
|
218
|
+
if (type === "vex_artifact" &&
|
|
219
|
+
(fields.deploymentId === undefined || fields.artifactId === undefined)) {
|
|
220
|
+
throw new Error("vex_artifact pages require both deploymentId and artifactId");
|
|
221
|
+
}
|
|
222
|
+
if (type === "protected_origin" && fields.originUrl === undefined) {
|
|
223
|
+
throw new Error("protected_origin pages require an originUrl");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { baseUrl, pathSegment } from "../../_shared/shiftCloud.js";
|
|
2
2
|
export { enumSchema, idSchema, listParams, pathSegment, request, STRICT_OBJECT, timestampSchema, withQuery, } from "../../_shared/shiftCloud.js";
|
|
3
3
|
export const PAGE_STATUS = ["draft", "published", "archived"];
|
|
4
|
+
export const PAGE_TYPE = [
|
|
5
|
+
"hosted_html",
|
|
6
|
+
"protected_origin",
|
|
7
|
+
"vex_artifact",
|
|
8
|
+
];
|
|
4
9
|
export const PAGE_VISIBILITY = ["org", "invited"];
|
|
5
10
|
export function pageRenderUrl(organizationId, slug) {
|
|
6
11
|
return new URL(`/pages/${pathSegment(organizationId)}/${pathSegment(slug)}`, baseUrl()).toString();
|
|
@@ -9,7 +9,7 @@ import { registerTranscriptionActions } from "./transcription.js";
|
|
|
9
9
|
*/
|
|
10
10
|
export default function shiftTranscription(rl) {
|
|
11
11
|
rl.setName("shiftTranscription");
|
|
12
|
-
rl.setVersion("0.
|
|
12
|
+
rl.setVersion("0.2.0");
|
|
13
13
|
rl.setConnectionSchema(t.Object({
|
|
14
14
|
apiKey: t.String({
|
|
15
15
|
description: "Shift Labs API key",
|
|
@@ -28,6 +28,9 @@ export function registerTranscriptionActions(rl) {
|
|
|
28
28
|
pattern: "\\S",
|
|
29
29
|
description: "Label shown in Shift Cloud",
|
|
30
30
|
})),
|
|
31
|
+
metadata: t.Optional(t.Record(t.String(), t.Unknown(), {
|
|
32
|
+
description: "Small caller-defined metadata object stored on the job",
|
|
33
|
+
})),
|
|
31
34
|
waitSeconds: t.Optional(t.Number({
|
|
32
35
|
minimum: 0,
|
|
33
36
|
maximum: 120,
|
|
@@ -55,6 +58,7 @@ export function registerTranscriptionActions(rl) {
|
|
|
55
58
|
language: fields.language,
|
|
56
59
|
diarize: fields.diarize,
|
|
57
60
|
name: fields.name,
|
|
61
|
+
metadata: fields.metadata,
|
|
58
62
|
}),
|
|
59
63
|
});
|
|
60
64
|
if (!fields.waitSeconds)
|
|
@@ -90,13 +94,25 @@ export function registerTranscriptionActions(rl) {
|
|
|
90
94
|
return request(ctx, `/v1/services/transcription/jobs/${pathSegment(id)}/cancel`, { method: "POST" });
|
|
91
95
|
},
|
|
92
96
|
});
|
|
97
|
+
rl.registerAction("transcription.job.delete", {
|
|
98
|
+
access: "write",
|
|
99
|
+
description: "Permanently delete a finished (succeeded, failed, or canceled) " +
|
|
100
|
+
"transcription job, its transcript artifacts, and its uploaded " +
|
|
101
|
+
"source media when no other job shares it. Cancel live jobs first. " +
|
|
102
|
+
"This cannot be undone.",
|
|
103
|
+
inputSchema: t.Object({ id: Id }, STRICT_OBJECT),
|
|
104
|
+
async execute(input, ctx) {
|
|
105
|
+
const { id } = input;
|
|
106
|
+
return request(ctx, `/v1/services/transcription/jobs/${pathSegment(id)}`, { method: "DELETE" });
|
|
107
|
+
},
|
|
108
|
+
});
|
|
93
109
|
rl.registerAction("transcription.artifact.list", {
|
|
94
110
|
access: "read",
|
|
95
111
|
description: "List the available transcript artifacts for a job.",
|
|
96
|
-
inputSchema: t.Object({
|
|
112
|
+
inputSchema: t.Object({ id: Id }, STRICT_OBJECT),
|
|
97
113
|
async execute(input, ctx) {
|
|
98
|
-
const {
|
|
99
|
-
const body = await request(ctx, `/v1/services/transcription/jobs/${pathSegment(
|
|
114
|
+
const { id } = input;
|
|
115
|
+
const body = await request(ctx, `/v1/services/transcription/jobs/${pathSegment(id)}/artifacts`);
|
|
100
116
|
return body.artifacts;
|
|
101
117
|
},
|
|
102
118
|
});
|
|
@@ -106,14 +122,14 @@ export function registerTranscriptionActions(rl) {
|
|
|
106
122
|
"return subtitles; json returns the full timestamped document. Set " +
|
|
107
123
|
"downloadOnly=true to return the short-lived signed download grant.",
|
|
108
124
|
inputSchema: t.Object({
|
|
109
|
-
|
|
125
|
+
id: Id,
|
|
110
126
|
format: t.Optional(enumSchema("Transcript format", [...TRANSCRIPT_FORMAT])),
|
|
111
127
|
downloadOnly: t.Optional(t.Boolean({ description: "Return a signed download grant only" })),
|
|
112
128
|
}, STRICT_OBJECT),
|
|
113
129
|
async execute(input, ctx) {
|
|
114
130
|
const fields = input;
|
|
115
131
|
const format = fields.format ?? "txt";
|
|
116
|
-
const grant = await request(ctx, `/v1/services/transcription/jobs/${pathSegment(fields.
|
|
132
|
+
const grant = await request(ctx, `/v1/services/transcription/jobs/${pathSegment(fields.id)}/artifacts/${pathSegment(format)}/download`, { method: "POST" });
|
|
117
133
|
if (fields.downloadOnly ||
|
|
118
134
|
grant.artifact.sizeBytes > MAX_TRANSCRIPT_BYTES) {
|
|
119
135
|
return { format, ...grant };
|
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
2
|
import { cursorSchema, enumSchema, ISSUE_PRIORITY, ISSUE_SOURCE, ISSUE_STATUS, idSchema, listParams, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, timestampSchema, withQuery, } from "./shared.js";
|
|
3
|
+
const issueFields = {
|
|
4
|
+
title: t.String({
|
|
5
|
+
minLength: 1,
|
|
6
|
+
maxLength: 160,
|
|
7
|
+
pattern: "\\S",
|
|
8
|
+
description: "Issue title",
|
|
9
|
+
}),
|
|
10
|
+
description: t.Optional(t.String({ maxLength: 20_000, description: "Issue description" })),
|
|
11
|
+
projectId: t.Optional(idSchema("Project ID")),
|
|
12
|
+
parentIssueId: t.Optional(idSchema("Parent Issue ID")),
|
|
13
|
+
sortOrder: t.Optional(t.Integer({ minimum: 0 })),
|
|
14
|
+
status: t.Optional(enumSchema("Issue status", ISSUE_STATUS)),
|
|
15
|
+
priority: t.Optional(enumSchema("Issue priority", ISSUE_PRIORITY)),
|
|
16
|
+
assigneeUserId: t.Optional(idSchema("Assignee user ID")),
|
|
17
|
+
startAt: t.Optional(timestampSchema("ISO-8601 start timestamp")),
|
|
18
|
+
dueAt: t.Optional(timestampSchema("ISO-8601 due timestamp")),
|
|
19
|
+
deploymentId: t.Optional(idSchema("Deployment ID")),
|
|
20
|
+
workspaceId: t.Optional(idSchema("Workspace ID")),
|
|
21
|
+
sessionId: t.Optional(idSchema("Session ID")),
|
|
22
|
+
traceId: t.Optional(idSchema("Trace ID")),
|
|
23
|
+
fingerprint: t.Optional(t.String({ minLength: 1, maxLength: 512, pattern: "\\S" })),
|
|
24
|
+
labels: t.Optional(t.Array(t.String({ minLength: 1, maxLength: 64, pattern: "\\S" }))),
|
|
25
|
+
metadata: t.Optional(t.Record(t.String(), t.Unknown(), { description: "Issue metadata" })),
|
|
26
|
+
source: t.Optional(enumSchema("Issue source", ISSUE_SOURCE)),
|
|
27
|
+
};
|
|
3
28
|
const issueListFields = {
|
|
4
29
|
status: t.Optional(enumSchema("Issue status", ISSUE_STATUS)),
|
|
5
30
|
projectId: t.Optional(idSchema("Project ID")),
|
|
@@ -14,6 +39,15 @@ const issueListFields = {
|
|
|
14
39
|
description: "Max results, default 50",
|
|
15
40
|
})),
|
|
16
41
|
};
|
|
42
|
+
const eventListFields = {
|
|
43
|
+
id: idSchema("Issue ID"),
|
|
44
|
+
cursor: t.Optional(cursorSchema()),
|
|
45
|
+
limit: t.Optional(t.Integer({
|
|
46
|
+
minimum: 1,
|
|
47
|
+
maximum: 100,
|
|
48
|
+
description: "Max results, default 50",
|
|
49
|
+
})),
|
|
50
|
+
};
|
|
17
51
|
export function registerIssueActions(rl) {
|
|
18
52
|
rl.registerAction("issue.list", {
|
|
19
53
|
access: "read",
|
|
@@ -45,38 +79,10 @@ export function registerIssueActions(rl) {
|
|
|
45
79
|
rl.registerAction("issue.create", {
|
|
46
80
|
access: "write",
|
|
47
81
|
description: "Create an Issue. Leave projectId empty for inbox work; parentIssueId requires a Project.",
|
|
48
|
-
inputSchema: t.Object(
|
|
49
|
-
title: t.String({
|
|
50
|
-
minLength: 1,
|
|
51
|
-
maxLength: 160,
|
|
52
|
-
pattern: "\\S",
|
|
53
|
-
description: "Issue title",
|
|
54
|
-
}),
|
|
55
|
-
description: t.Optional(t.String({ maxLength: 20_000, description: "Issue description" })),
|
|
56
|
-
projectId: t.Optional(idSchema("Project ID")),
|
|
57
|
-
parentIssueId: t.Optional(idSchema("Parent Issue ID")),
|
|
58
|
-
sortOrder: t.Optional(t.Integer({ minimum: 0 })),
|
|
59
|
-
status: t.Optional(enumSchema("Issue status", ISSUE_STATUS)),
|
|
60
|
-
priority: t.Optional(enumSchema("Issue priority", ISSUE_PRIORITY)),
|
|
61
|
-
source: t.Optional(enumSchema("Issue source", ISSUE_SOURCE)),
|
|
62
|
-
assigneeUserId: t.Optional(idSchema("Assignee user ID")),
|
|
63
|
-
startAt: t.Optional(timestampSchema("ISO-8601 start timestamp")),
|
|
64
|
-
dueAt: t.Optional(timestampSchema("ISO-8601 due timestamp")),
|
|
65
|
-
deploymentId: t.Optional(idSchema("Deployment ID")),
|
|
66
|
-
workspaceId: t.Optional(idSchema("Workspace ID")),
|
|
67
|
-
sessionId: t.Optional(idSchema("Session ID")),
|
|
68
|
-
traceId: t.Optional(idSchema("Trace ID")),
|
|
69
|
-
fingerprint: t.Optional(t.String({ minLength: 1, maxLength: 512, pattern: "\\S" })),
|
|
70
|
-
labels: t.Optional(t.Array(t.String({ minLength: 1, maxLength: 64, pattern: "\\S" }))),
|
|
71
|
-
metadata: t.Optional(t.Record(t.String(), t.Unknown(), { description: "Issue metadata" })),
|
|
72
|
-
}, STRICT_OBJECT),
|
|
82
|
+
inputSchema: t.Object(issueFields, STRICT_OBJECT),
|
|
73
83
|
async execute(input, ctx) {
|
|
74
84
|
const fields = input;
|
|
75
|
-
|
|
76
|
-
fields.projectId === undefined) {
|
|
77
|
-
throw new Error("projectId is required when parentIssueId is set");
|
|
78
|
-
}
|
|
79
|
-
assertDateOrder(fields.startAt, fields.dueAt, "Issue dueAt must not precede startAt");
|
|
85
|
+
assertIssueShape(fields);
|
|
80
86
|
const body = await request(ctx, "/v1/issues", {
|
|
81
87
|
method: "POST",
|
|
82
88
|
body: JSON.stringify(fields),
|
|
@@ -84,6 +90,19 @@ export function registerIssueActions(rl) {
|
|
|
84
90
|
return body.issue;
|
|
85
91
|
},
|
|
86
92
|
});
|
|
93
|
+
rl.registerAction("issue.report", {
|
|
94
|
+
access: "write",
|
|
95
|
+
description: "Report an Issue the way an agent should: source defaults to agent, " +
|
|
96
|
+
"and a repeated fingerprint folds into the existing Issue as a " +
|
|
97
|
+
"reported_again event instead of creating a duplicate.",
|
|
98
|
+
inputSchema: t.Object(issueFields, STRICT_OBJECT),
|
|
99
|
+
async execute(input, ctx) {
|
|
100
|
+
const fields = input;
|
|
101
|
+
assertIssueShape(fields);
|
|
102
|
+
const body = await request(ctx, "/v1/issues/report", { method: "POST", body: JSON.stringify(fields) });
|
|
103
|
+
return body.issue;
|
|
104
|
+
},
|
|
105
|
+
});
|
|
87
106
|
rl.registerAction("issue.update", {
|
|
88
107
|
access: "write",
|
|
89
108
|
description: "Update an Issue, including Project assignment, one-level parentage, ordering, status, dates, or assignee.",
|
|
@@ -127,6 +146,66 @@ export function registerIssueActions(rl) {
|
|
|
127
146
|
return response.event;
|
|
128
147
|
},
|
|
129
148
|
});
|
|
149
|
+
rl.registerAction("issue.resolve", {
|
|
150
|
+
access: "write",
|
|
151
|
+
description: "Resolve an Issue, optionally with a closing comment recorded on the " +
|
|
152
|
+
"event timeline.",
|
|
153
|
+
inputSchema: t.Object({
|
|
154
|
+
id: idSchema("Issue ID"),
|
|
155
|
+
comment: t.Optional(t.String({
|
|
156
|
+
minLength: 1,
|
|
157
|
+
maxLength: 20_000,
|
|
158
|
+
pattern: "\\S",
|
|
159
|
+
description: "Resolution comment",
|
|
160
|
+
})),
|
|
161
|
+
}, STRICT_OBJECT),
|
|
162
|
+
async execute(input, ctx) {
|
|
163
|
+
const { id, ...fields } = input;
|
|
164
|
+
const body = await request(ctx, `/v1/issues/${pathSegment(id)}/resolve`, { method: "POST", body: JSON.stringify(fields) });
|
|
165
|
+
return body.issue;
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
rl.registerAction("issue.reopen", {
|
|
169
|
+
access: "write",
|
|
170
|
+
description: "Reopen a resolved or closed Issue.",
|
|
171
|
+
inputSchema: t.Object({ id: idSchema("Issue ID") }, STRICT_OBJECT),
|
|
172
|
+
async execute(input, ctx) {
|
|
173
|
+
const { id } = input;
|
|
174
|
+
const body = await request(ctx, `/v1/issues/${pathSegment(id)}/reopen`, { method: "POST" });
|
|
175
|
+
return body.issue;
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
rl.registerAction("issue.close", {
|
|
179
|
+
access: "write",
|
|
180
|
+
description: "Close an Issue without resolving it — the outcome for work that is " +
|
|
181
|
+
"abandoned rather than finished.",
|
|
182
|
+
inputSchema: t.Object({ id: idSchema("Issue ID") }, STRICT_OBJECT),
|
|
183
|
+
async execute(input, ctx) {
|
|
184
|
+
const { id } = input;
|
|
185
|
+
const body = await request(ctx, `/v1/issues/${pathSegment(id)}/close`, { method: "POST" });
|
|
186
|
+
return body.issue;
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
rl.registerAction("issue.event.list", {
|
|
190
|
+
access: "read",
|
|
191
|
+
description: "List the first page of an Issue's event timeline: status changes, " +
|
|
192
|
+
"comments, assignment and dependency history, newest first.",
|
|
193
|
+
inputSchema: t.Object(eventListFields, STRICT_OBJECT),
|
|
194
|
+
async execute(input, ctx) {
|
|
195
|
+
const { id, ...pagination } = input;
|
|
196
|
+
const body = await request(ctx, withQuery(`/v1/issues/${pathSegment(id)}/events`, listParams(pagination)));
|
|
197
|
+
return body.events;
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
rl.registerAction("issue.event.listPage", {
|
|
201
|
+
access: "read",
|
|
202
|
+
description: "List a cursor-paginated page of an Issue's event timeline.",
|
|
203
|
+
inputSchema: t.Object(eventListFields, STRICT_OBJECT),
|
|
204
|
+
async execute(input, ctx) {
|
|
205
|
+
const { id, ...pagination } = input;
|
|
206
|
+
return request(ctx, withQuery(`/v1/issues/${pathSegment(id)}/events`, listParams(pagination)));
|
|
207
|
+
},
|
|
208
|
+
});
|
|
130
209
|
rl.registerAction("issue.dependency.list", {
|
|
131
210
|
access: "read",
|
|
132
211
|
description: "List the first page of blocking and blocked-by links.",
|
|
@@ -173,16 +252,23 @@ export function registerIssueActions(rl) {
|
|
|
173
252
|
access: "write",
|
|
174
253
|
description: "Remove an Issue dependency link.",
|
|
175
254
|
inputSchema: t.Object({
|
|
176
|
-
|
|
255
|
+
id: idSchema("Issue ID"),
|
|
177
256
|
dependencyId: idSchema("Dependency ID"),
|
|
178
257
|
}, STRICT_OBJECT),
|
|
179
258
|
async execute(input, ctx) {
|
|
180
|
-
const {
|
|
181
|
-
await request(ctx, `/v1/issues/${pathSegment(
|
|
259
|
+
const { id, dependencyId } = input;
|
|
260
|
+
await request(ctx, `/v1/issues/${pathSegment(id)}/dependencies/${pathSegment(dependencyId)}`, { method: "DELETE" });
|
|
182
261
|
return { removed: true };
|
|
183
262
|
},
|
|
184
263
|
});
|
|
185
264
|
}
|
|
265
|
+
/** Shape rules the API enforces server-side, caught before the round-trip. */
|
|
266
|
+
function assertIssueShape(fields) {
|
|
267
|
+
if (fields.parentIssueId !== undefined && fields.projectId === undefined) {
|
|
268
|
+
throw new Error("projectId is required when parentIssueId is set");
|
|
269
|
+
}
|
|
270
|
+
assertDateOrder(fields.startAt, fields.dueAt, "Issue dueAt must not precede startAt");
|
|
271
|
+
}
|
|
186
272
|
function assertDateOrder(start, end, message) {
|
|
187
273
|
if (typeof start === "string" && typeof end === "string" && start > end) {
|
|
188
274
|
throw new Error(message);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
-
import { SESSION_OPTIONS_SCHEMA, api,
|
|
2
|
+
import { SESSION_OPTIONS_SCHEMA, api, cdpUrl, compactRecord } from "./shared.js";
|
|
3
3
|
const SCRAPE_SCHEMA = {
|
|
4
4
|
url: t.String({ description: "URL to scrape" }),
|
|
5
5
|
format: t.Optional(t.Array(t.String(), { description: "Formats: html, cleaned_html, markdown, readability" })),
|
|
@@ -20,67 +20,6 @@ async function scrape(input, ctx) {
|
|
|
20
20
|
async function screenshot(input, ctx) {
|
|
21
21
|
return api(ctx, "/v1/screenshot", { method: "POST", body: compactRecord(input) });
|
|
22
22
|
}
|
|
23
|
-
async function connectMiniCdp(cdpUrl) {
|
|
24
|
-
const ws = new WebSocket(cdpUrl);
|
|
25
|
-
let nextId = 0;
|
|
26
|
-
const pending = new Map();
|
|
27
|
-
await new Promise((resolve, reject) => {
|
|
28
|
-
const timer = setTimeout(() => reject(new Error("CDP websocket connection timed out")), 30000);
|
|
29
|
-
ws.addEventListener("open", () => { clearTimeout(timer); resolve(); }, { once: true });
|
|
30
|
-
ws.addEventListener("error", () => { clearTimeout(timer); reject(new Error("CDP websocket connection failed")); }, { once: true });
|
|
31
|
-
});
|
|
32
|
-
ws.addEventListener("message", (event) => {
|
|
33
|
-
let message;
|
|
34
|
-
try {
|
|
35
|
-
message = JSON.parse(String(event.data));
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
if (typeof message.id !== "number")
|
|
41
|
-
return;
|
|
42
|
-
const wait = pending.get(message.id);
|
|
43
|
-
if (!wait)
|
|
44
|
-
return;
|
|
45
|
-
pending.delete(message.id);
|
|
46
|
-
if (message.error)
|
|
47
|
-
wait.reject(new Error(JSON.stringify(message.error)));
|
|
48
|
-
else
|
|
49
|
-
wait.resolve(message.result);
|
|
50
|
-
});
|
|
51
|
-
const send = (method, params = {}, sessionId) => new Promise((resolve, reject) => {
|
|
52
|
-
const id = ++nextId;
|
|
53
|
-
pending.set(id, { resolve, reject });
|
|
54
|
-
ws.send(JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) }));
|
|
55
|
-
setTimeout(() => {
|
|
56
|
-
if (pending.delete(id))
|
|
57
|
-
reject(new Error(`CDP ${method} timed out`));
|
|
58
|
-
}, 30000);
|
|
59
|
-
});
|
|
60
|
-
const targets = await send("Target.getTargets");
|
|
61
|
-
const target = targets.targetInfos?.find((info) => info.type === "page") ?? targets.targetInfos?.[0];
|
|
62
|
-
if (!target)
|
|
63
|
-
throw new Error("Steel CDP session has no browser target");
|
|
64
|
-
const attached = await send("Target.attachToTarget", { targetId: target.targetId, flatten: true });
|
|
65
|
-
const sid = attached.sessionId;
|
|
66
|
-
const evaluate = async (expression) => {
|
|
67
|
-
const result = await send("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true }, sid);
|
|
68
|
-
return result.result?.value;
|
|
69
|
-
};
|
|
70
|
-
const page = {
|
|
71
|
-
async goto(url, _options) {
|
|
72
|
-
await send("Page.navigate", { url }, sid);
|
|
73
|
-
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
74
|
-
return null;
|
|
75
|
-
},
|
|
76
|
-
title: () => evaluate("document.title"),
|
|
77
|
-
url: () => evaluate("location.href"),
|
|
78
|
-
text: () => evaluate("document.body?.innerText ?? ''"),
|
|
79
|
-
html: () => evaluate("document.documentElement?.outerHTML ?? ''"),
|
|
80
|
-
evaluate: (expression) => evaluate(`(${expression})()`),
|
|
81
|
-
};
|
|
82
|
-
return { page, browser: { close: () => ws.close() }, context: {}, close: () => ws.close() };
|
|
83
|
-
}
|
|
84
23
|
export function registerBrowserActions(rl) {
|
|
85
24
|
rl.registerAction("scrape", {
|
|
86
25
|
access: "write",
|
|
@@ -134,7 +73,7 @@ export function registerBrowserActions(rl) {
|
|
|
134
73
|
});
|
|
135
74
|
rl.registerAction("browser.run", {
|
|
136
75
|
access: "write",
|
|
137
|
-
description: "Create a Steel session, connect with Playwright over CDP, run an async JavaScript script, then release by default. The script receives { page, browser, context
|
|
76
|
+
description: "Create a Steel session, connect with real Playwright over CDP, run an async JavaScript script, then release by default. The script receives the genuine Playwright { page, browser, context } plus session. Requires the host app to have playwright installed — this action fails rather than degrading if it is missing. For automation that works without a Playwright install, prefer the page.* actions, which drive the page semantically over CDP.",
|
|
138
77
|
inputSchema: t.Object({
|
|
139
78
|
script: t.String({ description: "Async JavaScript body. Example: await page.goto('https://example.com'); return { title: await page.title() };" }),
|
|
140
79
|
release: t.Optional(t.Boolean({ description: "Release the Steel session after the script finishes (default true)" })),
|
|
@@ -147,26 +86,19 @@ export function registerBrowserActions(rl) {
|
|
|
147
86
|
playwright = await import("playwright");
|
|
148
87
|
}
|
|
149
88
|
catch (_error) {
|
|
150
|
-
throw new Error("steel.browser.run
|
|
89
|
+
throw new Error("steel.browser.run needs playwright installed in the host project. " +
|
|
90
|
+
"Install it, or use the steel.page.* actions, which need nothing installed.");
|
|
151
91
|
}
|
|
152
92
|
const session = await api(ctx, "/v1/sessions", { method: "POST", body: compactRecord(sessionOptions) });
|
|
153
|
-
const
|
|
93
|
+
const endpoint = cdpUrl(ctx, String(session.id));
|
|
154
94
|
let browser;
|
|
155
95
|
try {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
page = context.pages()[0] ?? await context.newPage();
|
|
163
|
-
}
|
|
164
|
-
catch {
|
|
165
|
-
const mini = await connectMiniCdp(cdpUrl);
|
|
166
|
-
browser = mini.browser;
|
|
167
|
-
context = mini.context;
|
|
168
|
-
page = mini.page;
|
|
169
|
-
}
|
|
96
|
+
// No fallback shim on purpose. A hand-rolled lookalike that
|
|
97
|
+
// answers to the same names but implements a fraction of the API
|
|
98
|
+
// does not fail — it returns wrong results, which is worse.
|
|
99
|
+
browser = await playwright.chromium.connectOverCDP(endpoint, { timeout: 30000 });
|
|
100
|
+
const context = browser.contexts()[0] ?? await browser.newContext();
|
|
101
|
+
const page = context.pages()[0] ?? await context.newPage();
|
|
170
102
|
const fn = new Function("page", "browser", "context", "session", `return (async () => {\n${script}\n})();`);
|
|
171
103
|
const result = await fn(page, browser, context, session);
|
|
172
104
|
return { session, result };
|
|
@@ -4,6 +4,7 @@ import { registerCaptchaActions } from "./captchas.js";
|
|
|
4
4
|
import { registerCredentialActions } from "./credentials.js";
|
|
5
5
|
import { registerExtensionActions } from "./extensions.js";
|
|
6
6
|
import { registerFileActions } from "./files.js";
|
|
7
|
+
import { registerPageActions } from "./page/tools.js";
|
|
7
8
|
import { registerProfileActions } from "./profiles.js";
|
|
8
9
|
import { registerSessionActions } from "./sessions.js";
|
|
9
10
|
export default function steel(rl) {
|
|
@@ -17,6 +18,7 @@ export default function steel(rl) {
|
|
|
17
18
|
}));
|
|
18
19
|
registerSessionActions(rl);
|
|
19
20
|
registerBrowserActions(rl);
|
|
21
|
+
registerPageActions(rl);
|
|
20
22
|
registerFileActions(rl);
|
|
21
23
|
registerCredentialActions(rl);
|
|
22
24
|
registerProfileActions(rl);
|