@kontextmind/kxm 0.6.0 → 0.7.10

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 (175) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/agents/coordinator.yaml +9 -0
  3. package/.kxm/agents/critic-arch.yaml +13 -0
  4. package/.kxm/agents/critic-cli.yaml +13 -0
  5. package/.kxm/agents/implementer.yaml +13 -0
  6. package/.kxm/gates.yaml +8 -0
  7. package/.kxm/producers.yaml +22 -0
  8. package/.kxm/project.yaml +15 -0
  9. package/.kxm/roles/writer.yaml +7 -0
  10. package/.kxm/workflows/default.yaml +47 -0
  11. package/CHANGELOG.md +39 -7
  12. package/README.md +1 -0
  13. package/docs/README.md +5 -0
  14. package/docs/adr/ADR-0002-browser-automation-steel-doks.md +103 -0
  15. package/docs/agent-skills.md +135 -0
  16. package/docs/architecture.md +1 -1
  17. package/docs/assignment-runner.md +21 -8
  18. package/docs/browser-automation.md +116 -0
  19. package/docs/configuration.md +11 -2
  20. package/docs/getting-started.md +21 -0
  21. package/docs/kb/how-credentials-retrieved-safely.md +31 -0
  22. package/docs/kb/how-to-capture-and-annotate-section.md +60 -0
  23. package/docs/kb/how-to-connect-playwright-to-steel.md +54 -0
  24. package/docs/kb/how-to-recover-expired-session-or-orphan.md +54 -0
  25. package/docs/kb/how-to-resume-after-mfa.md +28 -0
  26. package/docs/kb/how-to-take-over-session.md +32 -0
  27. package/docs/kb/why-authentication-disappeared.md +32 -0
  28. package/docs/kb/why-automation-opened-different-browser.md +32 -0
  29. package/docs/kb/why-session-viewer-cannot-control.md +31 -0
  30. package/docs/kxm-handbook.md +3 -3
  31. package/docs/operations.md +24 -0
  32. package/docs/operator-pi-packages.md +63 -0
  33. package/docs/prompts/browser-annotate-feedback.md +41 -0
  34. package/docs/prompts/browser-diagnose-recover.md +38 -0
  35. package/docs/prompts/browser-explore.md +42 -0
  36. package/docs/prompts/browser-repro-fix.md +48 -0
  37. package/docs/prompts/browser-start.md +41 -0
  38. package/docs/prompts/browser-takeover.md +50 -0
  39. package/docs/skills/repo-work-delivery.md +107 -0
  40. package/docs/skills.md +2 -0
  41. package/docs/test-matrix.md +4 -3
  42. package/docs/troubleshooting.md +41 -1
  43. package/docs/vnext/validation.md +9 -0
  44. package/docs/webhook-workflows.md +2 -2
  45. package/examples/README.md +1 -1
  46. package/package.json +16 -17
  47. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  48. package/plugins/kxm/README.md +1 -1
  49. package/plugins/kxm/dist/cli.js +41620 -35578
  50. package/plugins/kxm/dist/core.js +271 -34
  51. package/plugins/kxm/dist/extension.js +7759 -86
  52. package/plugins/kxm/dist/mcp-server.js +75 -21
  53. package/plugins/kxm/dist/runtime.js +8218 -2328
  54. package/plugins/kxm/dist/server.js +3125 -2260
  55. package/plugins/kxm/dist/vnext-runtime-supervisor.js +5961 -661
  56. package/plugins/kxm/package.json +1 -1
  57. package/plugins/kxm/skills/SUITE.md +5 -0
  58. package/plugins/kxm/skills/hints.json +103 -0
  59. package/plugins/kxm/skills/kxm/SKILL.md +30 -83
  60. package/plugins/kxm/skills/kxm-browser-annotate/SKILL.md +90 -0
  61. package/plugins/kxm/skills/kxm-browser-auth/SKILL.md +47 -0
  62. package/plugins/kxm/skills/kxm-browser-diagnostics/SKILL.md +48 -0
  63. package/plugins/kxm/skills/kxm-browser-explore/SKILL.md +48 -0
  64. package/plugins/kxm/skills/kxm-browser-session/SKILL.md +94 -0
  65. package/plugins/kxm/skills/kxm-browser-takeover/SKILL.md +87 -0
  66. package/plugins/kxm/skills/kxm-browser-verify/SKILL.md +71 -0
  67. package/plugins/kxm/skills/kxm-context-memory/SKILL.md +69 -0
  68. package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
  69. package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
  70. package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
  71. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +43 -0
  72. package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
  73. package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
  74. package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
  75. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +42 -0
  76. package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
  77. package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
  78. package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
  79. package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
  80. package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
  81. package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
  82. package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
  83. package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
  84. package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
  85. package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
  86. package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
  87. package/plugins/kxm/src/autocomplete.ts +9 -3
  88. package/plugins/kxm/src/browser.ts +603 -0
  89. package/plugins/kxm/src/cli/context-skills.ts +373 -0
  90. package/plugins/kxm/src/cli/hub.ts +614 -0
  91. package/plugins/kxm/src/cli/roles.ts +615 -0
  92. package/plugins/kxm/src/cli/system.ts +906 -0
  93. package/plugins/kxm/src/cli/tasks.ts +364 -0
  94. package/plugins/kxm/src/cli/types.ts +270 -0
  95. package/plugins/kxm/src/cli/vnext.ts +698 -0
  96. package/plugins/kxm/src/cli/workflows.ts +699 -0
  97. package/plugins/kxm/src/cli.ts +362 -2849
  98. package/plugins/kxm/src/commands.ts +150 -8
  99. package/plugins/kxm/src/completion-install.ts +223 -0
  100. package/plugins/kxm/src/config.ts +7 -4
  101. package/plugins/kxm/src/context-packet.ts +172 -0
  102. package/plugins/kxm/src/database.ts +1 -1
  103. package/plugins/kxm/src/extension.ts +36 -1
  104. package/plugins/kxm/src/external-effects.ts +357 -8
  105. package/plugins/kxm/src/hub-env.ts +193 -0
  106. package/plugins/kxm/src/hub.ts +2 -4
  107. package/plugins/kxm/src/improve.ts +72 -0
  108. package/plugins/kxm/src/init-guide-setup.ts +547 -0
  109. package/plugins/kxm/src/local-snapshot.ts +1 -1
  110. package/plugins/kxm/src/mcp-server.ts +1 -1
  111. package/plugins/kxm/src/model-inventory.ts +127 -0
  112. package/plugins/kxm/src/modes.ts +348 -0
  113. package/plugins/kxm/src/policy-draft.d.mts +55 -0
  114. package/plugins/kxm/src/policy-draft.mjs +565 -0
  115. package/plugins/kxm/src/price-calc.ts +17 -18
  116. package/plugins/kxm/src/prices.ts +32 -16
  117. package/plugins/kxm/src/producers.ts +71 -0
  118. package/plugins/kxm/src/protocol.ts +111 -0
  119. package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
  120. package/plugins/kxm/src/restricted-yaml.mjs +145 -0
  121. package/plugins/kxm/src/role.ts +710 -0
  122. package/plugins/kxm/src/routing.ts +99 -1
  123. package/plugins/kxm/src/runtime.ts +4 -0
  124. package/plugins/kxm/src/safety-integrity.ts +76 -0
  125. package/plugins/kxm/src/session-work.ts +9 -2
  126. package/plugins/kxm/src/sqlite.ts +76 -0
  127. package/plugins/kxm/src/ssh-remote.ts +560 -0
  128. package/plugins/kxm/src/store.ts +1 -1
  129. package/plugins/kxm/src/studio-layout.ts +660 -17
  130. package/plugins/kxm/src/subagent-control.ts +312 -0
  131. package/plugins/kxm/src/suggest.ts +7 -13
  132. package/plugins/kxm/src/telemetry.ts +82 -0
  133. package/plugins/kxm/src/tui.ts +140 -0
  134. package/plugins/kxm/src/vnext-bindings.ts +1 -1
  135. package/plugins/kxm/src/vnext-config.ts +53 -111
  136. package/plugins/kxm/src/vnext-engine-command.ts +2 -0
  137. package/plugins/kxm/src/vnext-engine.ts +214 -62
  138. package/plugins/kxm/src/vnext-harness.ts +336 -84
  139. package/plugins/kxm/src/vnext-oneshot-evidence.ts +117 -0
  140. package/plugins/kxm/src/vnext-oneshot-process.ts +187 -0
  141. package/plugins/kxm/src/vnext-oneshot-producer.ts +182 -224
  142. package/plugins/kxm/src/vnext-pi-producer.ts +11 -7
  143. package/plugins/kxm/src/vnext-runtime-store.ts +36 -2
  144. package/plugins/kxm/src/vnext-runtime-supervisor.ts +122 -5
  145. package/plugins/kxm/src/vnext-runtime.ts +14 -0
  146. package/plugins/kxm/src/workflow-manager.ts +392 -0
  147. package/plugins/kxm/src/workflow-tui.ts +255 -0
  148. package/plugins/kxm/src/workflow.ts +144 -0
  149. package/schemas/policy-draft/README.md +17 -0
  150. package/schemas/policy-draft/model.v2.schema.json +140 -0
  151. package/schemas/policy-draft/role.v2.schema.json +91 -0
  152. package/schemas/vnext/modes.schema.json +56 -0
  153. package/schemas/vnext/role.schema.json +76 -0
  154. package/schemas/vnext/run-event.schema.json +1 -0
  155. package/scripts/assignment-run.d.mts +1 -1
  156. package/scripts/assignment-run.mjs +44 -35
  157. package/scripts/check-generated.mjs +33 -9
  158. package/scripts/emit-codex-artifacts.mjs +255 -11
  159. package/scripts/harness-run.d.mts +12 -4
  160. package/scripts/harness-run.mjs +65 -17
  161. package/scripts/kxm-bump-version.mjs +146 -0
  162. package/scripts/kxm-hub.mjs +150 -2
  163. package/scripts/kxm-publish-npm.mjs +3 -1
  164. package/scripts/kxm-release-github.mjs +3 -1
  165. package/scripts/kxm.mjs +0 -0
  166. package/scripts/native-critic.d.mts +5 -0
  167. package/scripts/native-critic.mjs +60 -0
  168. package/.kxm/config/README.md +0 -5
  169. package/.kxm/config/agents.json +0 -43
  170. package/.kxm/config/env.example +0 -56
  171. package/.kxm/config/update.example.yaml +0 -9
  172. package/.kxm/config/workflows/fix.json +0 -160
  173. package/.kxm/config/workflows/jira-development.json +0 -116
  174. package/.kxm/config/workflows/provenance-quorum.json +0 -150
  175. package/.kxm/config/workflows/v04-dogfood.json +0 -72
@@ -0,0 +1,603 @@
1
+ /**
2
+ * KXM Browser Automation & Steel Session Client
3
+ *
4
+ * Lightweight, harness-agnostic client for self-hosted Steel on DOKS.
5
+ * Manages remote browser sessions, CDP endpoints, human takeover handoffs,
6
+ * pass-cli credential references, and automated cleanup without leaking secrets.
7
+ */
8
+
9
+ import { execSync } from "node:child_process";
10
+
11
+ export type SessionState =
12
+ | "AGENT_CONTROL"
13
+ | "AUTH_REQUIRED"
14
+ | "HUMAN_CONTROL"
15
+ | "VERIFY_AUTHENTICATION"
16
+ | "RELEASED"
17
+ | "EXPIRED"
18
+ | "FAILED";
19
+
20
+ export interface SteelSession {
21
+ id: string;
22
+ createdAt: string;
23
+ status: "idle" | "live" | "released" | "failed";
24
+ state: SessionState;
25
+ websocketUrl: string;
26
+ debugUrl: string;
27
+ debuggerUrl: string;
28
+ sessionViewerUrl: string;
29
+ timeoutMs: number;
30
+ lastActiveAt: number;
31
+ activeController: "agent" | "human" | "none";
32
+ takeoverReason?: string | undefined;
33
+ userAgent?: string | undefined;
34
+ }
35
+
36
+ export interface ViewportPreset {
37
+ name: string;
38
+ category: "mobile" | "tablet" | "desktop" | "laptop";
39
+ width: number;
40
+ height: number;
41
+ deviceScaleFactor?: number | undefined;
42
+ isMobile?: boolean | undefined;
43
+ hasTouch?: boolean | undefined;
44
+ }
45
+
46
+ export const VIEWPORT_PRESETS: Record<string, ViewportPreset> = Object.freeze({
47
+ "mobile-sm": { name: "Mobile Small (SE)", category: "mobile", width: 375, height: 667, isMobile: true, hasTouch: true },
48
+ "mobile": { name: "Mobile (iPhone 16 / 15 Pro)", category: "mobile", width: 393, height: 852, isMobile: true, hasTouch: true },
49
+ "mobile-lg": { name: "Mobile Large (Pro Max)", category: "mobile", width: 430, height: 932, isMobile: true, hasTouch: true },
50
+ "pixel": { name: "Google Pixel 8/9", category: "mobile", width: 412, height: 924, isMobile: true, hasTouch: true },
51
+ "galaxy": { name: "Samsung Galaxy S24", category: "mobile", width: 360, height: 780, isMobile: true, hasTouch: true },
52
+ "tablet": { name: "Tablet (iPad Air / Mini)", category: "tablet", width: 820, height: 1180, isMobile: true, hasTouch: true },
53
+ "tablet-lg": { name: "Tablet Large (iPad Pro 12.9)", category: "tablet", width: 1024, height: 1366, isMobile: true, hasTouch: true },
54
+ "laptop": { name: "Standard Laptop", category: "laptop", width: 1366, height: 768 },
55
+ "macbook-13": { name: "MacBook Air 13", category: "laptop", width: 1440, height: 900 },
56
+ "macbook-16": { name: "MacBook Pro 16", category: "laptop", width: 1728, height: 1117 },
57
+ "desktop": { name: "Desktop FHD (1080p)", category: "desktop", width: 1920, height: 1080 },
58
+ "desktop-2k": { name: "Desktop QHD (1440p)", category: "desktop", width: 2560, height: 1440 },
59
+ "desktop-4k": { name: "Desktop 4K UHD", category: "desktop", width: 3840, height: 2160 },
60
+ });
61
+
62
+ export function resolveViewportDimensions(
63
+ presetOrDims?: string | { width: number; height: number } | undefined
64
+ ): { width: number; height: number } {
65
+ if (!presetOrDims) {
66
+ return { width: 1920, height: 1080 };
67
+ }
68
+ if (typeof presetOrDims === "string") {
69
+ const matched = VIEWPORT_PRESETS[presetOrDims.toLowerCase()];
70
+ if (matched) {
71
+ return { width: matched.width, height: matched.height };
72
+ }
73
+ return { width: 1920, height: 1080 };
74
+ }
75
+ return presetOrDims;
76
+ }
77
+
78
+ export interface CreateSessionOptions {
79
+ timeoutMs?: number | undefined;
80
+ dimensions?: { width: number; height: number } | undefined;
81
+ viewportPreset?: string | undefined;
82
+ userAgent?: string | undefined;
83
+ proxy?: string | undefined;
84
+ }
85
+
86
+ export interface ScrapeResult {
87
+ content: {
88
+ html: string;
89
+ };
90
+ metadata: {
91
+ statusCode: number;
92
+ title: string;
93
+ description?: string | undefined;
94
+ language?: string | undefined;
95
+ urlSource: string;
96
+ timestamp: string;
97
+ };
98
+ links: Array<{ url: string; text: string }>;
99
+ }
100
+
101
+ export interface ScreenshotResult {
102
+ path?: string | undefined;
103
+ base64?: string | undefined;
104
+ url: string;
105
+ }
106
+
107
+ export interface BoundingBox {
108
+ x: number;
109
+ y: number;
110
+ width: number;
111
+ height: number;
112
+ }
113
+
114
+ export interface AnnotationItem {
115
+ id?: string | undefined;
116
+ label: string;
117
+ note: string;
118
+ selector?: string | undefined;
119
+ boundingBox?: BoundingBox | undefined;
120
+ severity?: "suggestion" | "fix" | "blocker" | undefined;
121
+ }
122
+
123
+ export interface AnnotationFeedback {
124
+ sessionId?: string | undefined;
125
+ url: string;
126
+ sectionSelector?: string | undefined;
127
+ screenshotBase64?: string | undefined;
128
+ screenshotPath?: string | undefined;
129
+ annotations: AnnotationItem[];
130
+ overallSummary: string;
131
+ requestedChanges: string[];
132
+ capturedAt: string;
133
+ }
134
+
135
+ export interface SteelConfig {
136
+ apiUrl: string;
137
+ apiKey?: string | undefined;
138
+ uiUrl?: string | undefined;
139
+ timeoutMs?: number | undefined;
140
+ }
141
+
142
+ export function resolvePassCliApiKey(
143
+ execFn: (cmd: string) => string = (cmd) =>
144
+ execSync(cmd, { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"], timeout: 5000 }),
145
+ ): string | undefined {
146
+ if (typeof process === "undefined" || process.env.USE_PASS_CLI === "false") {
147
+ return undefined;
148
+ }
149
+ if (process.env.PASS_CLI_OUTPUT_MOCK) {
150
+ try {
151
+ const parsed = JSON.parse(process.env.PASS_CLI_OUTPUT_MOCK);
152
+ const extraFields = parsed?.item?.content?.extra_fields || [];
153
+ const customSections = parsed?.item?.content?.content?.Custom?.sections || [];
154
+ const sectionFields = customSections.flatMap((s: any) => s.section_fields || []);
155
+ const allFields = [...extraFields, ...sectionFields];
156
+ return allFields.find((f: any) => f.name === "STEEL_API_KEY")?.content?.Hidden;
157
+ } catch {
158
+ return undefined;
159
+ }
160
+ }
161
+ try {
162
+ const output = execFn(
163
+ 'pass-cli item view --vault-name "AI Provider Keys" --item-title "Steel Browser (KontextMind DOKS)" --output json',
164
+ );
165
+ const parsed = JSON.parse(output);
166
+ const extraFields = parsed?.item?.content?.extra_fields || [];
167
+ const customSections = parsed?.item?.content?.content?.Custom?.sections || [];
168
+ const sectionFields = customSections.flatMap((s: any) => s.section_fields || []);
169
+ const allFields = [...extraFields, ...sectionFields];
170
+ return allFields.find((f: any) => f.name === "STEEL_API_KEY")?.content?.Hidden;
171
+ } catch {
172
+ return undefined;
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Resolve Steel configuration from environment or pass-cli.
178
+ * Does not write secrets to disk or logs.
179
+ */
180
+ export function resolveSteelConfig(overrides?: Partial<SteelConfig>): SteelConfig {
181
+ const apiUrl =
182
+ overrides?.apiUrl ||
183
+ process.env.STEEL_API_URL ||
184
+ "https://steel.kontextmind.com";
185
+
186
+ const apiKey = overrides?.apiKey || process.env.STEEL_API_KEY || resolvePassCliApiKey();
187
+
188
+ const uiUrl =
189
+ overrides?.uiUrl ||
190
+ (overrides?.apiUrl ? `${overrides.apiUrl.replace(/\/$/, "")}/ui` : undefined) ||
191
+ process.env.STEEL_UI_URL ||
192
+ `${apiUrl.replace(/\/$/, "")}/ui`;
193
+
194
+ return {
195
+ apiUrl: apiUrl.replace(/\/$/, ""),
196
+ apiKey,
197
+ uiUrl,
198
+ timeoutMs: overrides?.timeoutMs || 300000, // 5 minutes default
199
+ };
200
+ }
201
+
202
+ /**
203
+ * Format a remote CDP connection URL for Playwright or agent-browser.
204
+ */
205
+ export function formatCDPEndpoint(session: Pick<SteelSession, "id" | "websocketUrl">, config: SteelConfig): string {
206
+ const baseApi = config.apiUrl;
207
+ const urlObj = new URL(baseApi);
208
+ const isSecure = urlObj.protocol === "https:";
209
+ const wsProtocol = isSecure ? "wss:" : "ws:";
210
+ const host = urlObj.host;
211
+
212
+ const searchParams = new URLSearchParams();
213
+ searchParams.set("sessionId", session.id);
214
+ if (config.apiKey) {
215
+ searchParams.set("apiKey", config.apiKey);
216
+ }
217
+
218
+ return `${wsProtocol}//${host}/v1/devtools?${searchParams.toString()}`;
219
+ }
220
+
221
+ /**
222
+ * Redact sensitive API keys and tokens from URLs and objects for logging.
223
+ */
224
+ export function sanitizeLogOutput<T>(input: T): T {
225
+ if (typeof input === "string") {
226
+ return input.replace(/apiKey=[^&]+/g, "apiKey=[REDACTED]").replace(/steel_[a-f0-9]+/g, "steel_[REDACTED]") as unknown as T;
227
+ }
228
+ if (Array.isArray(input)) {
229
+ return input.map(sanitizeLogOutput) as unknown as T;
230
+ }
231
+ if (input !== null && typeof input === "object") {
232
+ const copy: Record<string, any> = {};
233
+ for (const [k, v] of Object.entries(input)) {
234
+ if (/key|secret|token|auth|password/i.test(k) && typeof v === "string") {
235
+ copy[k] = "[REDACTED]";
236
+ } else {
237
+ copy[k] = sanitizeLogOutput(v);
238
+ }
239
+ }
240
+ return copy as T;
241
+ }
242
+ return input;
243
+ }
244
+
245
+ /**
246
+ * Create a structured annotation feedback package from captured section data.
247
+ */
248
+ export function createAnnotationFeedback(
249
+ feedback: Omit<AnnotationFeedback, "capturedAt"> & { capturedAt?: string }
250
+ ): AnnotationFeedback {
251
+ return {
252
+ ...feedback,
253
+ capturedAt: feedback.capturedAt || new Date().toISOString(),
254
+ };
255
+ }
256
+
257
+ /**
258
+ * Format structured visual annotations and requested UI changes into a
259
+ * clear, actionable prompt block that an agent can parse and execute.
260
+ */
261
+ export function formatAnnotationFeedbackPrompt(feedback: AnnotationFeedback): string {
262
+ let output = `## Visual Feedback & Section Annotation\n\n`;
263
+ output += `- **Target URL**: ${feedback.url}\n`;
264
+ if (feedback.sessionId) {
265
+ output += `- **Session ID**: ${feedback.sessionId}\n`;
266
+ }
267
+ if (feedback.sectionSelector) {
268
+ output += `- **Section Target Selector**: \`${feedback.sectionSelector}\`\n`;
269
+ }
270
+ if (feedback.screenshotPath) {
271
+ output += `- **Screenshot Artifact**: \`${feedback.screenshotPath}\`\n`;
272
+ }
273
+ output += `- **Captured At**: ${feedback.capturedAt}\n\n`;
274
+
275
+ output += `### Summary\n${feedback.overallSummary}\n\n`;
276
+
277
+ if (feedback.annotations.length > 0) {
278
+ output += `### Annotated Elements & Notes\n\n`;
279
+ feedback.annotations.forEach((item, idx) => {
280
+ output += `${idx + 1}. **${item.label}**`;
281
+ if (item.severity) {
282
+ output += ` [${item.severity.toUpperCase()}]`;
283
+ }
284
+ output += `\n - **Note**: ${item.note}\n`;
285
+ if (item.selector) {
286
+ output += ` - **Selector**: \`${item.selector}\`\n`;
287
+ }
288
+ if (item.boundingBox) {
289
+ output += ` - **Region (Box)**: x=${item.boundingBox.x}, y=${item.boundingBox.y}, w=${item.boundingBox.width}, h=${item.boundingBox.height}\n`;
290
+ }
291
+ });
292
+ output += `\n`;
293
+ }
294
+
295
+ if (feedback.requestedChanges.length > 0) {
296
+ output += `### Actionable Change List\n\n`;
297
+ feedback.requestedChanges.forEach((change, idx) => {
298
+ output += `- [ ] ${change}\n`;
299
+ });
300
+ }
301
+
302
+ return output;
303
+ }
304
+
305
+ /**
306
+ * Steel Browser Session Manager & Handoff Orchestrator.
307
+ */
308
+ export class SteelClient {
309
+ private config: SteelConfig;
310
+ private activeSessions = new Map<string, SteelSession>();
311
+
312
+ constructor(config?: Partial<SteelConfig>) {
313
+ this.config = resolveSteelConfig(config);
314
+ }
315
+
316
+ getConfig(): Readonly<SteelConfig> {
317
+ return { ...this.config };
318
+ }
319
+
320
+ private headers(): Record<string, string> {
321
+ const h: Record<string, string> = {
322
+ "Content-Type": "application/json",
323
+ };
324
+ if (this.config.apiKey) {
325
+ h["x-steel-api-key"] = this.config.apiKey;
326
+ }
327
+ return h;
328
+ }
329
+
330
+ /**
331
+ * Launch a new Steel browser session on DOKS.
332
+ */
333
+ async createSession(options?: CreateSessionOptions): Promise<SteelSession> {
334
+ const timeoutMs = options?.timeoutMs ?? this.config.timeoutMs ?? 300000;
335
+ const body: Record<string, any> = {
336
+ timeout: timeoutMs,
337
+ };
338
+ const dimensions = options?.dimensions || (options?.viewportPreset ? resolveViewportDimensions(options.viewportPreset) : undefined);
339
+ if (dimensions) {
340
+ body.dimensions = dimensions;
341
+ }
342
+ if (options?.userAgent) {
343
+ body.userAgent = options.userAgent;
344
+ }
345
+ if (options?.proxy) {
346
+ body.proxy = options.proxy;
347
+ }
348
+
349
+ const res = await fetch(`${this.config.apiUrl}/v1/sessions`, {
350
+ method: "POST",
351
+ headers: this.headers(),
352
+ body: JSON.stringify(body),
353
+ });
354
+
355
+ if (!res.ok) {
356
+ const errText = await res.text();
357
+ throw new Error(`Failed to create Steel session (${res.status}): ${errText}`);
358
+ }
359
+
360
+ const data = await res.json();
361
+ const session: SteelSession = {
362
+ id: data.id,
363
+ createdAt: data.createdAt || new Date().toISOString(),
364
+ status: "live",
365
+ state: "AGENT_CONTROL",
366
+ websocketUrl: data.websocketUrl || "",
367
+ debugUrl: data.debugUrl || "",
368
+ debuggerUrl: data.debuggerUrl || "",
369
+ sessionViewerUrl: data.sessionViewerUrl || `${this.config.uiUrl}?sessionId=${data.id}`,
370
+ timeoutMs,
371
+ lastActiveAt: Date.now(),
372
+ activeController: "agent",
373
+ userAgent: data.userAgent,
374
+ };
375
+
376
+ this.activeSessions.set(session.id, session);
377
+ return session;
378
+ }
379
+
380
+ /**
381
+ * Get details of an existing session.
382
+ */
383
+ async getSession(sessionId: string): Promise<SteelSession | null> {
384
+ const res = await fetch(`${this.config.apiUrl}/v1/sessions/${encodeURIComponent(sessionId)}`, {
385
+ method: "GET",
386
+ headers: this.headers(),
387
+ });
388
+
389
+ if (res.status === 404) {
390
+ const cached = this.activeSessions.get(sessionId);
391
+ if (cached) {
392
+ cached.state = "EXPIRED";
393
+ cached.status = "released";
394
+ cached.activeController = "none";
395
+ }
396
+ return null;
397
+ }
398
+
399
+ if (!res.ok) {
400
+ throw new Error(`Failed to fetch Steel session (${res.status})`);
401
+ }
402
+
403
+ const data = await res.json();
404
+ const existing = this.activeSessions.get(sessionId);
405
+ const session: SteelSession = {
406
+ id: data.id,
407
+ createdAt: data.createdAt,
408
+ status: data.status,
409
+ state: existing?.state ?? (data.status === "live" ? "AGENT_CONTROL" : "RELEASED"),
410
+ websocketUrl: data.websocketUrl || existing?.websocketUrl || "",
411
+ debugUrl: data.debugUrl || existing?.debugUrl || "",
412
+ debuggerUrl: data.debuggerUrl || existing?.debuggerUrl || "",
413
+ sessionViewerUrl: existing?.sessionViewerUrl || `${this.config.uiUrl}?sessionId=${data.id}`,
414
+ timeoutMs: data.timeout || existing?.timeoutMs || 300000,
415
+ lastActiveAt: Date.now(),
416
+ activeController: existing?.activeController ?? (data.status === "live" ? "agent" : "none"),
417
+ userAgent: data.userAgent,
418
+ };
419
+
420
+ this.activeSessions.set(session.id, session);
421
+ return session;
422
+ }
423
+
424
+ /**
425
+ * Request human takeover for MFA, login, or consent.
426
+ * Pauses agent automation and sets state to HUMAN_CONTROL.
427
+ */
428
+ requestHumanTakeover(sessionId: string, reason: string): {
429
+ session: SteelSession;
430
+ takeoverUrl: string;
431
+ instructions: string;
432
+ } {
433
+ const session = this.activeSessions.get(sessionId);
434
+ if (!session) {
435
+ throw new Error(`Session ${sessionId} not tracked or already released`);
436
+ }
437
+ if (session.state === "RELEASED" || session.state === "EXPIRED" || session.state === "FAILED") {
438
+ throw new Error(`Cannot initiate takeover on session in state ${session.state}`);
439
+ }
440
+
441
+ session.state = "HUMAN_CONTROL";
442
+ session.activeController = "human";
443
+ session.takeoverReason = reason;
444
+ session.lastActiveAt = Date.now();
445
+
446
+ const takeoverUrl = `${this.config.uiUrl}?sessionId=${encodeURIComponent(sessionId)}`;
447
+ const instructions =
448
+ `[HUMAN TAKEOVER REQUIRED]\n` +
449
+ `Reason: ${reason}\n` +
450
+ `Session ID: ${session.id}\n` +
451
+ `Takeover URL: ${takeoverUrl}\n\n` +
452
+ `Instructions for Operator:\n` +
453
+ `1. Open the URL above to access the session UI.\n` +
454
+ `2. Perform the required authentication / MFA / consent action.\n` +
455
+ `3. Return to the terminal and signal completion. Automation is paused until you confirm.`;
456
+
457
+ return {
458
+ session,
459
+ takeoverUrl,
460
+ instructions,
461
+ };
462
+ }
463
+
464
+ /**
465
+ * Signal that human takeover is complete.
466
+ * Moves state to VERIFY_AUTHENTICATION before transitioning back to AGENT_CONTROL.
467
+ */
468
+ signalHumanComplete(sessionId: string): {
469
+ session: SteelSession;
470
+ state: SessionState;
471
+ } {
472
+ const session = this.activeSessions.get(sessionId);
473
+ if (!session) {
474
+ throw new Error(`Session ${sessionId} not tracked or already released`);
475
+ }
476
+ if (session.state !== "HUMAN_CONTROL") {
477
+ throw new Error(`Session ${sessionId} is not in HUMAN_CONTROL state (currently ${session.state})`);
478
+ }
479
+
480
+ session.state = "VERIFY_AUTHENTICATION";
481
+ session.activeController = "agent";
482
+ session.lastActiveAt = Date.now();
483
+
484
+ return {
485
+ session,
486
+ state: session.state,
487
+ };
488
+ }
489
+
490
+ /**
491
+ * Confirm authentication verification passed and restore AGENT_CONTROL.
492
+ */
493
+ confirmAuthenticationVerified(sessionId: string): SteelSession {
494
+ const session = this.activeSessions.get(sessionId);
495
+ if (!session) {
496
+ throw new Error(`Session ${sessionId} not tracked or already released`);
497
+ }
498
+ if (session.state !== "VERIFY_AUTHENTICATION") {
499
+ throw new Error(`Session ${sessionId} is not in VERIFY_AUTHENTICATION state (currently ${session.state})`);
500
+ }
501
+
502
+ session.state = "AGENT_CONTROL";
503
+ session.activeController = "agent";
504
+ session.takeoverReason = undefined;
505
+ session.lastActiveAt = Date.now();
506
+
507
+ return session;
508
+ }
509
+
510
+ /**
511
+ * Gracefully release a session.
512
+ */
513
+ async releaseSession(sessionId: string): Promise<boolean> {
514
+ try {
515
+ const res = await fetch(`${this.config.apiUrl}/v1/sessions/${encodeURIComponent(sessionId)}/release`, {
516
+ method: "POST",
517
+ headers: this.headers(),
518
+ });
519
+
520
+ const session = this.activeSessions.get(sessionId);
521
+ if (session) {
522
+ session.status = "released";
523
+ session.state = "RELEASED";
524
+ session.activeController = "none";
525
+ }
526
+ this.activeSessions.delete(sessionId);
527
+ return res.ok;
528
+ } catch {
529
+ this.activeSessions.delete(sessionId);
530
+ return false;
531
+ }
532
+ }
533
+
534
+ /**
535
+ * Perform a direct stateless scrape without manual session management.
536
+ */
537
+ async scrape(url: string): Promise<ScrapeResult> {
538
+ const res = await fetch(`${this.config.apiUrl}/v1/scrape`, {
539
+ method: "POST",
540
+ headers: this.headers(),
541
+ body: JSON.stringify({ url }),
542
+ });
543
+
544
+ if (!res.ok) {
545
+ const err = await res.text();
546
+ throw new Error(`Scrape failed (${res.status}): ${err}`);
547
+ }
548
+
549
+ return res.json();
550
+ }
551
+
552
+ /**
553
+ * Perform a direct screenshot action.
554
+ */
555
+ async screenshot(url: string, fullPage = false): Promise<ScreenshotResult> {
556
+ const res = await fetch(`${this.config.apiUrl}/v1/screenshot`, {
557
+ method: "POST",
558
+ headers: this.headers(),
559
+ body: JSON.stringify({ url, fullPage }),
560
+ });
561
+
562
+ if (!res.ok) {
563
+ const err = await res.text();
564
+ throw new Error(`Screenshot failed (${res.status}): ${err}`);
565
+ }
566
+
567
+ return res.json();
568
+ }
569
+
570
+ /**
571
+ * Detect and list orphaned or timed-out active sessions.
572
+ */
573
+ async checkOrphanedSessions(maxIdleMs = 600000): Promise<string[]> {
574
+ const res = await fetch(`${this.config.apiUrl}/v1/sessions`, {
575
+ method: "GET",
576
+ headers: this.headers(),
577
+ });
578
+
579
+ if (!res.ok) {
580
+ return [];
581
+ }
582
+
583
+ const data = await res.json();
584
+ const remoteSessions: Array<{ id: string; status: string; createdAt: string; duration: number }> =
585
+ data.sessions || [];
586
+
587
+ const now = Date.now();
588
+ const orphaned: string[] = [];
589
+
590
+ for (const rs of remoteSessions) {
591
+ if (rs.status === "live" || rs.status === "idle") {
592
+ const tracked = this.activeSessions.get(rs.id);
593
+ if (!tracked && rs.duration > maxIdleMs) {
594
+ orphaned.push(rs.id);
595
+ } else if (tracked && now - tracked.lastActiveAt > maxIdleMs && tracked.state !== "HUMAN_CONTROL") {
596
+ orphaned.push(rs.id);
597
+ }
598
+ }
599
+ }
600
+
601
+ return orphaned;
602
+ }
603
+ }