@optima-chat/browser-cli 0.1.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.
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,480 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/config.ts
7
+ var BROWSER_API_URL = process.env.BROWSER_API_URL || "http://localhost:7292";
8
+
9
+ // src/api.ts
10
+ var BrowserAPIError = class extends Error {
11
+ constructor(statusCode, errorCode, message) {
12
+ super(message);
13
+ this.statusCode = statusCode;
14
+ this.errorCode = errorCode;
15
+ this.name = "BrowserAPIError";
16
+ }
17
+ };
18
+ async function request(path, options = {}) {
19
+ const url = `${BROWSER_API_URL}${path}`;
20
+ let response;
21
+ try {
22
+ response = await fetch(url, {
23
+ ...options,
24
+ headers: {
25
+ "Content-Type": "application/json",
26
+ ...options.headers
27
+ }
28
+ });
29
+ } catch (err) {
30
+ throw new Error(
31
+ `Cannot connect to browser backend at ${BROWSER_API_URL}. Is the service running?`
32
+ );
33
+ }
34
+ if (!response.ok) {
35
+ let body;
36
+ try {
37
+ body = await response.json();
38
+ } catch {
39
+ throw new BrowserAPIError(
40
+ response.status,
41
+ "unknown",
42
+ `HTTP ${response.status}: ${response.statusText}`
43
+ );
44
+ }
45
+ throw new BrowserAPIError(response.status, body.error, body.message);
46
+ }
47
+ return await response.json();
48
+ }
49
+ async function requestBinary(path) {
50
+ const url = `${BROWSER_API_URL}${path}`;
51
+ let response;
52
+ try {
53
+ response = await fetch(url);
54
+ } catch {
55
+ throw new Error(
56
+ `Cannot connect to browser backend at ${BROWSER_API_URL}. Is the service running?`
57
+ );
58
+ }
59
+ if (!response.ok) {
60
+ let body;
61
+ try {
62
+ body = await response.json();
63
+ } catch {
64
+ throw new BrowserAPIError(
65
+ response.status,
66
+ "unknown",
67
+ `HTTP ${response.status}: ${response.statusText}`
68
+ );
69
+ }
70
+ throw new BrowserAPIError(response.status, body.error, body.message);
71
+ }
72
+ const arrayBuffer = await response.arrayBuffer();
73
+ return Buffer.from(arrayBuffer);
74
+ }
75
+ var BrowserAPI = class {
76
+ async createSession(url) {
77
+ const body = url ? { url } : {};
78
+ return request("/api/sessions", {
79
+ method: "POST",
80
+ body: JSON.stringify(body)
81
+ });
82
+ }
83
+ async closeSession(sessionId) {
84
+ await request(`/api/sessions/${sessionId}`, { method: "DELETE" });
85
+ }
86
+ async getStatus(sessionId) {
87
+ return request(`/api/sessions/${sessionId}/status`);
88
+ }
89
+ async navigate(sessionId, url) {
90
+ return request(`/api/sessions/${sessionId}/navigate`, {
91
+ method: "POST",
92
+ body: JSON.stringify({ url })
93
+ });
94
+ }
95
+ async back(sessionId) {
96
+ return request(`/api/sessions/${sessionId}/back`, {
97
+ method: "POST",
98
+ body: JSON.stringify({})
99
+ });
100
+ }
101
+ async refresh(sessionId) {
102
+ return request(`/api/sessions/${sessionId}/refresh`, {
103
+ method: "POST",
104
+ body: JSON.stringify({})
105
+ });
106
+ }
107
+ async click(sessionId, index) {
108
+ return request(`/api/sessions/${sessionId}/click`, {
109
+ method: "POST",
110
+ body: JSON.stringify({ index })
111
+ });
112
+ }
113
+ async input(sessionId, index, text, clear = true) {
114
+ return request(`/api/sessions/${sessionId}/input`, {
115
+ method: "POST",
116
+ body: JSON.stringify({ index, text, clear })
117
+ });
118
+ }
119
+ async scroll(sessionId, down, pages = 1) {
120
+ return request(`/api/sessions/${sessionId}/scroll`, {
121
+ method: "POST",
122
+ body: JSON.stringify({ down, pages })
123
+ });
124
+ }
125
+ async keys(sessionId, keys) {
126
+ return request(`/api/sessions/${sessionId}/keys`, {
127
+ method: "POST",
128
+ body: JSON.stringify({ keys })
129
+ });
130
+ }
131
+ async select(sessionId, index, text) {
132
+ return request(`/api/sessions/${sessionId}/select`, {
133
+ method: "POST",
134
+ body: JSON.stringify({ index, text })
135
+ });
136
+ }
137
+ async getState(sessionId) {
138
+ return request(`/api/sessions/${sessionId}/state`);
139
+ }
140
+ async getScreenshot(sessionId) {
141
+ return requestBinary(`/api/sessions/${sessionId}/screenshot`);
142
+ }
143
+ async search(sessionId, pattern) {
144
+ return request(`/api/sessions/${sessionId}/search`, {
145
+ method: "POST",
146
+ body: JSON.stringify({ pattern })
147
+ });
148
+ }
149
+ async find(sessionId, selector) {
150
+ return request(`/api/sessions/${sessionId}/find`, {
151
+ method: "POST",
152
+ body: JSON.stringify({ selector })
153
+ });
154
+ }
155
+ async getTabs(sessionId) {
156
+ return request(`/api/sessions/${sessionId}/tabs`);
157
+ }
158
+ async switchTab(sessionId, tabId) {
159
+ return request(`/api/sessions/${sessionId}/switch`, {
160
+ method: "POST",
161
+ body: JSON.stringify({ tab_id: tabId })
162
+ });
163
+ }
164
+ async closeTab(sessionId, tabId) {
165
+ return request(
166
+ `/api/sessions/${sessionId}/tabs/${tabId}`,
167
+ { method: "DELETE" }
168
+ );
169
+ }
170
+ };
171
+ var api = new BrowserAPI();
172
+
173
+ // src/session.ts
174
+ import { readFileSync, writeFileSync, unlinkSync, existsSync } from "fs";
175
+ var SESSION_FILE = "/tmp/browser-session-id";
176
+ function saveSessionId(id) {
177
+ writeFileSync(SESSION_FILE, id, "utf-8");
178
+ }
179
+ function loadSessionId() {
180
+ if (!existsSync(SESSION_FILE)) {
181
+ throw new Error(
182
+ "No active browser session. Run 'browser-cli launch' first."
183
+ );
184
+ }
185
+ return readFileSync(SESSION_FILE, "utf-8").trim();
186
+ }
187
+ function clearSessionId() {
188
+ if (existsSync(SESSION_FILE)) {
189
+ unlinkSync(SESSION_FILE);
190
+ }
191
+ }
192
+
193
+ // src/formatter.ts
194
+ function formatPageState(state) {
195
+ const lines = [];
196
+ lines.push(`Page: ${state.title} (${state.url})`);
197
+ lines.push("");
198
+ lines.push("DOM:");
199
+ lines.push(state.dom);
200
+ lines.push("");
201
+ lines.push(`Tabs: ${formatTabsInline(state.tabs)}`);
202
+ return lines.join("\n");
203
+ }
204
+ function formatLaunch(state) {
205
+ const lines = [];
206
+ lines.push(`\u2713 Browser launched (session: ${state.session_id})`);
207
+ lines.push(formatPageState(state));
208
+ return lines.join("\n");
209
+ }
210
+ function formatClose(sessionId) {
211
+ return `\u2713 Browser closed (session: ${sessionId})`;
212
+ }
213
+ function formatStatus(status) {
214
+ const lines = [];
215
+ lines.push(`Session: ${status.session_id}`);
216
+ lines.push(`Status: ${status.status}`);
217
+ lines.push(`Created: ${status.created_at}`);
218
+ lines.push(`Idle: ${status.idle_seconds}s`);
219
+ return lines.join("\n");
220
+ }
221
+ function formatScreenshot(filePath) {
222
+ return `\u2713 Screenshot saved to ${filePath}`;
223
+ }
224
+ function formatTabs(tabList) {
225
+ const lines = ["Tabs:"];
226
+ for (const tab of tabList.tabs) {
227
+ lines.push(` [${tab.tab_id}] ${tab.title} (${tab.url})`);
228
+ }
229
+ return lines.join("\n");
230
+ }
231
+ function formatSearch(result) {
232
+ if (result.total === 0) {
233
+ return "No matches found.";
234
+ }
235
+ const lines = [`Found ${result.total} match(es):`, ""];
236
+ for (const match of result.matches) {
237
+ lines.push(match.text);
238
+ }
239
+ return lines.join("\n");
240
+ }
241
+ function formatFind(result) {
242
+ if (result.total === 0) {
243
+ return "No elements found.";
244
+ }
245
+ const lines = [`Found ${result.total} element(s):`, ""];
246
+ for (const match of result.matches) {
247
+ const attrs = Object.entries(match.attributes).map(([k, v]) => `${k}="${v}"`).join(" ");
248
+ lines.push(` [${match.index}] <${match.tag} ${attrs}>`);
249
+ }
250
+ return lines.join("\n");
251
+ }
252
+ function formatError(err) {
253
+ if (err instanceof Error) {
254
+ return `\u2717 Error: ${err.message}`;
255
+ }
256
+ return `\u2717 Error: ${String(err)}`;
257
+ }
258
+ function formatTabsInline(tabs) {
259
+ return tabs.map((t) => `[${t.tab_id}] ${t.title}`).join(", ");
260
+ }
261
+
262
+ // src/commands/lifecycle.ts
263
+ function registerLifecycleCommands(program2) {
264
+ program2.command("launch").description("Launch a browser session").option("--url <url>", "Navigate to URL after launch").action(async (opts) => {
265
+ try {
266
+ const state = await api.createSession(opts.url);
267
+ saveSessionId(state.session_id);
268
+ console.log(formatLaunch(state));
269
+ } catch (err) {
270
+ console.error(formatError(err));
271
+ process.exit(1);
272
+ }
273
+ });
274
+ program2.command("close").description("Close the active browser session").action(async () => {
275
+ try {
276
+ const sessionId = loadSessionId();
277
+ await api.closeSession(sessionId);
278
+ clearSessionId();
279
+ console.log(formatClose(sessionId));
280
+ } catch (err) {
281
+ console.error(formatError(err));
282
+ process.exit(1);
283
+ }
284
+ });
285
+ program2.command("status").description("Show session status").action(async () => {
286
+ try {
287
+ const sessionId = loadSessionId();
288
+ const status = await api.getStatus(sessionId);
289
+ console.log(formatStatus(status));
290
+ } catch (err) {
291
+ console.error(formatError(err));
292
+ process.exit(1);
293
+ }
294
+ });
295
+ }
296
+
297
+ // src/commands/navigation.ts
298
+ function registerNavigationCommands(program2) {
299
+ program2.command("navigate").description("Navigate to a URL").argument("<url>", "URL to navigate to").action(async (url) => {
300
+ try {
301
+ const sessionId = loadSessionId();
302
+ const state = await api.navigate(sessionId, url);
303
+ console.log(formatPageState(state));
304
+ } catch (err) {
305
+ console.error(formatError(err));
306
+ process.exit(1);
307
+ }
308
+ });
309
+ program2.command("back").description("Go back to the previous page").action(async () => {
310
+ try {
311
+ const sessionId = loadSessionId();
312
+ const state = await api.back(sessionId);
313
+ console.log(formatPageState(state));
314
+ } catch (err) {
315
+ console.error(formatError(err));
316
+ process.exit(1);
317
+ }
318
+ });
319
+ program2.command("refresh").description("Refresh the current page").action(async () => {
320
+ try {
321
+ const sessionId = loadSessionId();
322
+ const state = await api.refresh(sessionId);
323
+ console.log(formatPageState(state));
324
+ } catch (err) {
325
+ console.error(formatError(err));
326
+ process.exit(1);
327
+ }
328
+ });
329
+ }
330
+
331
+ // src/commands/interaction.ts
332
+ function registerInteractionCommands(program2) {
333
+ program2.command("click").description("Click an element by index").argument("<index>", "Element index from DOM").action(async (index) => {
334
+ try {
335
+ const sessionId = loadSessionId();
336
+ const state = await api.click(sessionId, parseInt(index, 10));
337
+ console.log(formatPageState(state));
338
+ } catch (err) {
339
+ console.error(formatError(err));
340
+ process.exit(1);
341
+ }
342
+ });
343
+ program2.command("input").description("Input text into an element").argument("<index>", "Element index from DOM").argument("<text>", "Text to input").option("--no-clear", "Do not clear existing content before input").action(async (index, text, opts) => {
344
+ try {
345
+ const sessionId = loadSessionId();
346
+ const state = await api.input(
347
+ sessionId,
348
+ parseInt(index, 10),
349
+ text,
350
+ opts.clear
351
+ );
352
+ console.log(formatPageState(state));
353
+ } catch (err) {
354
+ console.error(formatError(err));
355
+ process.exit(1);
356
+ }
357
+ });
358
+ program2.command("scroll").description("Scroll the page up or down").argument("<direction>", "Scroll direction: up or down").option("--pages <n>", "Number of pages to scroll", "1").action(async (direction, opts) => {
359
+ try {
360
+ const sessionId = loadSessionId();
361
+ const down = direction.toLowerCase() === "down";
362
+ const pages = parseInt(opts.pages, 10);
363
+ const state = await api.scroll(sessionId, down, pages);
364
+ console.log(formatPageState(state));
365
+ } catch (err) {
366
+ console.error(formatError(err));
367
+ process.exit(1);
368
+ }
369
+ });
370
+ program2.command("keys").description("Send keyboard keys").argument("<keys>", "Key name (e.g. Enter, Tab, Escape, ArrowDown)").action(async (keys) => {
371
+ try {
372
+ const sessionId = loadSessionId();
373
+ const state = await api.keys(sessionId, keys);
374
+ console.log(formatPageState(state));
375
+ } catch (err) {
376
+ console.error(formatError(err));
377
+ process.exit(1);
378
+ }
379
+ });
380
+ program2.command("select").description("Select an option from a dropdown").argument("<index>", "Dropdown element index from DOM").argument("<option>", "Option text to select").action(async (index, option) => {
381
+ try {
382
+ const sessionId = loadSessionId();
383
+ const state = await api.select(sessionId, parseInt(index, 10), option);
384
+ console.log(formatPageState(state));
385
+ } catch (err) {
386
+ console.error(formatError(err));
387
+ process.exit(1);
388
+ }
389
+ });
390
+ }
391
+
392
+ // src/commands/info.ts
393
+ import { writeFileSync as writeFileSync2 } from "fs";
394
+ function registerInfoCommands(program2) {
395
+ program2.command("dom").description("Get the current page DOM").action(async () => {
396
+ try {
397
+ const sessionId = loadSessionId();
398
+ const state = await api.getState(sessionId);
399
+ console.log(formatPageState(state));
400
+ } catch (err) {
401
+ console.error(formatError(err));
402
+ process.exit(1);
403
+ }
404
+ });
405
+ program2.command("screenshot").description("Take a screenshot of the current page").argument("[file]", "File path to save screenshot", "/tmp/screenshot.png").action(async (file) => {
406
+ try {
407
+ const sessionId = loadSessionId();
408
+ const buffer = await api.getScreenshot(sessionId);
409
+ writeFileSync2(file, buffer);
410
+ console.log(formatScreenshot(file));
411
+ } catch (err) {
412
+ console.error(formatError(err));
413
+ process.exit(1);
414
+ }
415
+ });
416
+ program2.command("search").description("Search for text in the page DOM").argument("<pattern>", "Text pattern to search for").action(async (pattern) => {
417
+ try {
418
+ const sessionId = loadSessionId();
419
+ const result = await api.search(sessionId, pattern);
420
+ console.log(formatSearch(result));
421
+ } catch (err) {
422
+ console.error(formatError(err));
423
+ process.exit(1);
424
+ }
425
+ });
426
+ program2.command("find").description("Find elements by CSS selector").argument("<selector>", "CSS selector").action(async (selector) => {
427
+ try {
428
+ const sessionId = loadSessionId();
429
+ const result = await api.find(sessionId, selector);
430
+ console.log(formatFind(result));
431
+ } catch (err) {
432
+ console.error(formatError(err));
433
+ process.exit(1);
434
+ }
435
+ });
436
+ }
437
+
438
+ // src/commands/tabs.ts
439
+ function registerTabCommands(program2) {
440
+ program2.command("tabs").description("List all open tabs").action(async () => {
441
+ try {
442
+ const sessionId = loadSessionId();
443
+ const tabList = await api.getTabs(sessionId);
444
+ console.log(formatTabs(tabList));
445
+ } catch (err) {
446
+ console.error(formatError(err));
447
+ process.exit(1);
448
+ }
449
+ });
450
+ program2.command("switch").description("Switch to a tab").argument("<tab_id>", "Tab ID to switch to").action(async (tabId) => {
451
+ try {
452
+ const sessionId = loadSessionId();
453
+ const state = await api.switchTab(sessionId, tabId);
454
+ console.log(formatPageState(state));
455
+ } catch (err) {
456
+ console.error(formatError(err));
457
+ process.exit(1);
458
+ }
459
+ });
460
+ program2.command("close-tab").description("Close a tab").argument("<tab_id>", "Tab ID to close").action(async (tabId) => {
461
+ try {
462
+ const sessionId = loadSessionId();
463
+ const state = await api.closeTab(sessionId, tabId);
464
+ console.log(formatPageState(state));
465
+ } catch (err) {
466
+ console.error(formatError(err));
467
+ process.exit(1);
468
+ }
469
+ });
470
+ }
471
+
472
+ // src/index.ts
473
+ var program = new Command();
474
+ program.name("browser-cli").description("CLI client for browser-backend").version("0.1.0");
475
+ registerLifecycleCommands(program);
476
+ registerNavigationCommands(program);
477
+ registerInteractionCommands(program);
478
+ registerInfoCommands(program);
479
+ registerTabCommands(program);
480
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@optima-chat/browser-cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "browser-cli": "./dist/index.js"
7
+ },
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsup",
13
+ "dev": "tsup --watch",
14
+ "test": "vitest run"
15
+ },
16
+ "dependencies": {
17
+ "commander": "^12.1.0"
18
+ },
19
+ "devDependencies": {
20
+ "tsup": "^8.0.0",
21
+ "typescript": "^5.4.0",
22
+ "@types/node": "^20.11.0",
23
+ "vitest": "^3.0.0"
24
+ }
25
+ }