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
@@ -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 hosted HTML page. Agents can publish it with page.publish.",
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 hosted HTML page's slug, title, visibility, or HTML.",
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 Shift Labs hosted HTML page.",
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({ pageId: idSchema("Page ID") }, STRICT_OBJECT),
148
+ inputSchema: t.Object({ id: idSchema("Page ID") }, STRICT_OBJECT),
117
149
  async execute(input, ctx) {
118
- const { pageId } = input;
119
- const body = await request(ctx, `/v1/pages/${pathSegment(pageId)}/shares`);
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
- pageId: idSchema("Page ID"),
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 { pageId, ...fields } = input;
137
- const body = await request(ctx, `/v1/pages/${pathSegment(String(pageId))}/shares`, {
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({ pageId: idSchema("Page ID") }, STRICT_OBJECT),
191
+ inputSchema: t.Object({ id: idSchema("Page ID") }, STRICT_OBJECT),
160
192
  async execute(input, ctx) {
161
- const { pageId } = input;
162
- const body = await request(ctx, `/v1/pages/${pathSegment(pageId)}`);
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.1.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({ jobId: Id }, STRICT_OBJECT),
112
+ inputSchema: t.Object({ id: Id }, STRICT_OBJECT),
97
113
  async execute(input, ctx) {
98
- const { jobId } = input;
99
- const body = await request(ctx, `/v1/services/transcription/jobs/${pathSegment(jobId)}/artifacts`);
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
- jobId: Id,
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.jobId)}/artifacts/${pathSegment(format)}/download`, { method: "POST" });
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
- if (fields.parentIssueId !== undefined &&
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
- issueId: idSchema("Issue ID"),
255
+ id: idSchema("Issue ID"),
177
256
  dependencyId: idSchema("Dependency ID"),
178
257
  }, STRICT_OBJECT),
179
258
  async execute(input, ctx) {
180
- const { issueId, dependencyId } = input;
181
- await request(ctx, `/v1/issues/${pathSegment(issueId)}/dependencies/${pathSegment(dependencyId)}`, { method: "DELETE" });
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, apiKey, compactRecord } from "./shared.js";
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, session }. Requires the host app to have playwright installed.",
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 requires the host project to install playwright. Install playwright or use session.create + session.cdpUrl instead.");
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 cdpUrl = `wss://connect.steel.dev?apiKey=${encodeURIComponent(apiKey(ctx))}&sessionId=${encodeURIComponent(String(session.id))}`;
93
+ const endpoint = cdpUrl(ctx, String(session.id));
154
94
  let browser;
155
95
  try {
156
- let context;
157
- let page;
158
- try {
159
- const playwrightBrowser = await playwright.chromium.connectOverCDP(cdpUrl, { timeout: 30000 });
160
- browser = playwrightBrowser;
161
- context = playwrightBrowser.contexts()[0] ?? await playwrightBrowser.newContext();
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);