hyper-agent-browser 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.
- package/LICENSE +21 -0
- package/README.md +196 -0
- package/package.json +63 -0
- package/src/browser/context.ts +66 -0
- package/src/browser/manager.ts +414 -0
- package/src/browser/sync-chrome-data.ts +53 -0
- package/src/cli.ts +628 -0
- package/src/cli.ts.backup +529 -0
- package/src/commands/actions.ts +232 -0
- package/src/commands/advanced.ts +252 -0
- package/src/commands/config.ts +44 -0
- package/src/commands/getters.ts +110 -0
- package/src/commands/info.ts +195 -0
- package/src/commands/navigation.ts +50 -0
- package/src/commands/session.ts +83 -0
- package/src/daemon/browser-pool.ts +65 -0
- package/src/daemon/client.ts +128 -0
- package/src/daemon/main.ts +200 -0
- package/src/daemon/server.ts +562 -0
- package/src/session/manager.ts +110 -0
- package/src/session/store.ts +172 -0
- package/src/snapshot/accessibility.ts +182 -0
- package/src/snapshot/dom-extractor.ts +220 -0
- package/src/snapshot/formatter.ts +115 -0
- package/src/snapshot/reference-store.ts +97 -0
- package/src/utils/config.ts +183 -0
- package/src/utils/errors.ts +121 -0
- package/src/utils/logger.ts +12 -0
- package/src/utils/selector.ts +23 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type { Page } from "patchright";
|
|
2
|
+
import { AccessibilityExtractor } from "../snapshot/accessibility";
|
|
3
|
+
import { DomSnapshotExtractor } from "../snapshot/dom-extractor";
|
|
4
|
+
import { SnapshotFormatter } from "../snapshot/formatter";
|
|
5
|
+
|
|
6
|
+
export interface SnapshotOptions {
|
|
7
|
+
interactive?: boolean;
|
|
8
|
+
full?: boolean;
|
|
9
|
+
raw?: boolean;
|
|
10
|
+
maxElements?: number;
|
|
11
|
+
referenceStore?: any; // Will be ReferenceStore instance
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function snapshot(page: Page, options: SnapshotOptions = {}): Promise<string> {
|
|
15
|
+
const interactiveOnly = options.interactive ?? true;
|
|
16
|
+
|
|
17
|
+
// Try accessibility API first, fallback to DOM extraction
|
|
18
|
+
let snapshot;
|
|
19
|
+
let domExtractor: DomSnapshotExtractor | null = null;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const accessibilityExtractor = new AccessibilityExtractor();
|
|
23
|
+
snapshot = await accessibilityExtractor.extract(page, interactiveOnly);
|
|
24
|
+
|
|
25
|
+
// If snapshot is empty, try DOM extraction
|
|
26
|
+
if (snapshot.elements.length === 0) {
|
|
27
|
+
throw new Error("Accessibility API returned no elements");
|
|
28
|
+
}
|
|
29
|
+
} catch (error) {
|
|
30
|
+
// Fallback to DOM extraction
|
|
31
|
+
domExtractor = new DomSnapshotExtractor();
|
|
32
|
+
const elements = await domExtractor.extract(page, interactiveOnly);
|
|
33
|
+
|
|
34
|
+
snapshot = {
|
|
35
|
+
url: await page.url(),
|
|
36
|
+
title: await page.title(),
|
|
37
|
+
timestamp: Date.now(),
|
|
38
|
+
elements,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Store element-to-selector mappings if referenceStore is provided
|
|
42
|
+
if (options.referenceStore && domExtractor) {
|
|
43
|
+
const mappings = domExtractor.getAllMappings();
|
|
44
|
+
options.referenceStore.setAll(mappings);
|
|
45
|
+
await options.referenceStore.save();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (options.raw) {
|
|
50
|
+
const formatter = new SnapshotFormatter();
|
|
51
|
+
return formatter.formatJson(snapshot, true);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const formatter = new SnapshotFormatter();
|
|
55
|
+
return formatter.format(snapshot, {
|
|
56
|
+
maxElements: options.maxElements,
|
|
57
|
+
includeDisabled: options.full,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ScreenshotOptions {
|
|
62
|
+
output?: string;
|
|
63
|
+
fullPage?: boolean;
|
|
64
|
+
selector?: string;
|
|
65
|
+
quality?: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function screenshot(page: Page, options: ScreenshotOptions = {}): Promise<string> {
|
|
69
|
+
const output = options.output || "screenshot.png";
|
|
70
|
+
|
|
71
|
+
if (options.selector) {
|
|
72
|
+
const { parseSelector } = await import("../utils/selector");
|
|
73
|
+
const parsed = parseSelector(options.selector);
|
|
74
|
+
|
|
75
|
+
let locator;
|
|
76
|
+
if (parsed.type === "css") {
|
|
77
|
+
locator = page.locator(parsed.value);
|
|
78
|
+
} else if (parsed.type === "text") {
|
|
79
|
+
locator = page.getByText(parsed.value);
|
|
80
|
+
} else if (parsed.type === "xpath") {
|
|
81
|
+
locator = page.locator(`xpath=${parsed.value}`);
|
|
82
|
+
} else {
|
|
83
|
+
throw new Error(`Unsupported selector type for screenshot: ${parsed.type}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await locator.screenshot({ path: output });
|
|
87
|
+
} else {
|
|
88
|
+
await page.screenshot({
|
|
89
|
+
path: output,
|
|
90
|
+
fullPage: options.fullPage,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return output;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Whitelist of safe operations
|
|
98
|
+
const SAFE_OPERATIONS = [
|
|
99
|
+
/^document\.title$/,
|
|
100
|
+
/^document\.URL$/,
|
|
101
|
+
/^window\.location\.href$/,
|
|
102
|
+
/^window\.innerWidth$/,
|
|
103
|
+
/^window\.innerHeight$/,
|
|
104
|
+
/^document\.body\.scrollHeight$/,
|
|
105
|
+
/^document\.body\.scrollWidth$/,
|
|
106
|
+
/^document\.readyState$/,
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
// Enhanced blacklist for dangerous patterns
|
|
110
|
+
const FORBIDDEN_PATTERNS = [
|
|
111
|
+
/\b(eval|Function|constructor)\b/i,
|
|
112
|
+
/require\s*\(/,
|
|
113
|
+
/import\s*\(/,
|
|
114
|
+
/process\./,
|
|
115
|
+
/child_process/,
|
|
116
|
+
/fs\./,
|
|
117
|
+
/__dirname/,
|
|
118
|
+
/__filename/,
|
|
119
|
+
/globalThis/,
|
|
120
|
+
/window\s*\[/,
|
|
121
|
+
/\[(['"`])[a-z_]+\1\]/i, // Bracket notation: window['process']
|
|
122
|
+
/Object\.(assign|defineProperty|create)/,
|
|
123
|
+
/\.constructor/,
|
|
124
|
+
/prototype/,
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
export async function evaluate(page: Page, script: string): Promise<any> {
|
|
128
|
+
const trimmedScript = script.trim();
|
|
129
|
+
|
|
130
|
+
// 1. Whitelist mode - only allow safe operations
|
|
131
|
+
const isSafe = SAFE_OPERATIONS.some((pattern) => pattern.test(trimmedScript));
|
|
132
|
+
|
|
133
|
+
if (!isSafe) {
|
|
134
|
+
// 2. Enhanced blacklist check
|
|
135
|
+
for (const pattern of FORBIDDEN_PATTERNS) {
|
|
136
|
+
if (pattern.test(trimmedScript)) {
|
|
137
|
+
throw new Error(`Unsafe operation blocked: ${pattern.source}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 3. Execute and limit result size (防止数据窃取)
|
|
143
|
+
const result = await page.evaluate(trimmedScript);
|
|
144
|
+
const serialized = JSON.stringify(result);
|
|
145
|
+
|
|
146
|
+
if (serialized.length > 100000) {
|
|
147
|
+
throw new Error("Result too large (max 100KB). Use snapshot command instead.");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function url(page: Page): Promise<string> {
|
|
154
|
+
return page.url();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function title(page: Page): Promise<string> {
|
|
158
|
+
return page.title();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface ContentOptions {
|
|
162
|
+
selector?: string;
|
|
163
|
+
maxLength?: number;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function content(page: Page, options: ContentOptions = {}): Promise<string> {
|
|
167
|
+
const maxLength = options.maxLength || 10000;
|
|
168
|
+
|
|
169
|
+
let text: string;
|
|
170
|
+
if (options.selector) {
|
|
171
|
+
const { parseSelector } = await import("../utils/selector");
|
|
172
|
+
const parsed = parseSelector(options.selector);
|
|
173
|
+
|
|
174
|
+
let locator;
|
|
175
|
+
if (parsed.type === "css") {
|
|
176
|
+
locator = page.locator(parsed.value);
|
|
177
|
+
} else if (parsed.type === "text") {
|
|
178
|
+
locator = page.getByText(parsed.value);
|
|
179
|
+
} else if (parsed.type === "xpath") {
|
|
180
|
+
locator = page.locator(`xpath=${parsed.value}`);
|
|
181
|
+
} else {
|
|
182
|
+
throw new Error(`Unsupported selector type: ${parsed.type}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
text = (await locator.textContent()) || "";
|
|
186
|
+
} else {
|
|
187
|
+
text = (await page.textContent("body")) || "";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (text.length > maxLength) {
|
|
191
|
+
text = `${text.slice(0, maxLength)}... (truncated at ${maxLength} chars)`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return text;
|
|
195
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Page } from "patchright";
|
|
2
|
+
|
|
3
|
+
export interface NavigationOptions {
|
|
4
|
+
waitUntil?: "load" | "domcontentloaded" | "networkidle";
|
|
5
|
+
timeout?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function open(
|
|
9
|
+
page: Page,
|
|
10
|
+
url: string,
|
|
11
|
+
options: NavigationOptions = {},
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
const waitUntil = options.waitUntil || "load";
|
|
14
|
+
const timeout = options.timeout || 30000;
|
|
15
|
+
|
|
16
|
+
await page.goto(url, {
|
|
17
|
+
waitUntil,
|
|
18
|
+
timeout,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function reload(page: Page, options: NavigationOptions = {}): Promise<void> {
|
|
23
|
+
const waitUntil = options.waitUntil || "load";
|
|
24
|
+
const timeout = options.timeout || 30000;
|
|
25
|
+
|
|
26
|
+
await page.reload({
|
|
27
|
+
waitUntil,
|
|
28
|
+
timeout,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function back(page: Page, options: NavigationOptions = {}): Promise<void> {
|
|
33
|
+
const waitUntil = options.waitUntil || "load";
|
|
34
|
+
const timeout = options.timeout || 30000;
|
|
35
|
+
|
|
36
|
+
await page.goBack({
|
|
37
|
+
waitUntil,
|
|
38
|
+
timeout,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function forward(page: Page, options: NavigationOptions = {}): Promise<void> {
|
|
43
|
+
const waitUntil = options.waitUntil || "load";
|
|
44
|
+
const timeout = options.timeout || 30000;
|
|
45
|
+
|
|
46
|
+
await page.goForward({
|
|
47
|
+
waitUntil,
|
|
48
|
+
timeout,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { SessionManager } from "../session/manager";
|
|
2
|
+
|
|
3
|
+
export interface SessionListOptions {
|
|
4
|
+
json?: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function listSessions(
|
|
8
|
+
sessionManager: SessionManager,
|
|
9
|
+
options: SessionListOptions = {},
|
|
10
|
+
): Promise<string> {
|
|
11
|
+
const sessions = await sessionManager.list();
|
|
12
|
+
|
|
13
|
+
if (options.json) {
|
|
14
|
+
return JSON.stringify(sessions, null, 2);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (sessions.length === 0) {
|
|
18
|
+
return "No sessions found.";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const lines: string[] = [];
|
|
22
|
+
lines.push(
|
|
23
|
+
`${
|
|
24
|
+
"SESSION".padEnd(15) + "STATUS".padEnd(10) + "BROWSER".padEnd(12) + "URL".padEnd(40)
|
|
25
|
+
}LAST ACTIVE`,
|
|
26
|
+
);
|
|
27
|
+
lines.push("-".repeat(100));
|
|
28
|
+
|
|
29
|
+
for (const session of sessions) {
|
|
30
|
+
const name = session.name.padEnd(15);
|
|
31
|
+
const status = session.status.padEnd(10);
|
|
32
|
+
const browser = (session.channel || "-").padEnd(12);
|
|
33
|
+
const url = (session.url || "-").slice(0, 38).padEnd(40);
|
|
34
|
+
const lastActive = formatLastActive(session.lastActiveAt);
|
|
35
|
+
|
|
36
|
+
lines.push(`${name}${status}${browser}${url}${lastActive}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return lines.join("\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function formatLastActive(timestamp: number): string {
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
const diff = now - timestamp;
|
|
45
|
+
|
|
46
|
+
const minutes = Math.floor(diff / 60000);
|
|
47
|
+
const hours = Math.floor(diff / 3600000);
|
|
48
|
+
const days = Math.floor(diff / 86400000);
|
|
49
|
+
|
|
50
|
+
if (minutes < 1) {
|
|
51
|
+
return "just now";
|
|
52
|
+
}
|
|
53
|
+
if (minutes < 60) {
|
|
54
|
+
return `${minutes} min ago`;
|
|
55
|
+
}
|
|
56
|
+
if (hours < 24) {
|
|
57
|
+
return `${hours} hour${hours > 1 ? "s" : ""} ago`;
|
|
58
|
+
}
|
|
59
|
+
return `${days} day${days > 1 ? "s" : ""} ago`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function closeSession(
|
|
63
|
+
sessionManager: SessionManager,
|
|
64
|
+
sessionName?: string,
|
|
65
|
+
closeAll = false,
|
|
66
|
+
): Promise<string> {
|
|
67
|
+
if (closeAll) {
|
|
68
|
+
await sessionManager.closeAll();
|
|
69
|
+
return "All sessions closed.";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!sessionName) {
|
|
73
|
+
throw new Error("Session name required");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const session = await sessionManager.get(sessionName);
|
|
77
|
+
if (!session) {
|
|
78
|
+
throw new Error(`Session not found: ${sessionName}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await sessionManager.close(sessionName);
|
|
82
|
+
return `Session closed: ${sessionName}`;
|
|
83
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { BrowserManager } from "../browser/manager";
|
|
2
|
+
import type { BrowserManagerOptions } from "../browser/manager";
|
|
3
|
+
import type { Session } from "../session/store";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* BrowserPool 管理多个 Session 的浏览器实例
|
|
7
|
+
*/
|
|
8
|
+
export class BrowserPool {
|
|
9
|
+
private browsers: Map<string, BrowserManager> = new Map();
|
|
10
|
+
|
|
11
|
+
async get(session: Session, options: BrowserManagerOptions = {}): Promise<BrowserManager> {
|
|
12
|
+
const key = session.name;
|
|
13
|
+
|
|
14
|
+
if (this.browsers.has(key)) {
|
|
15
|
+
const browser = this.browsers.get(key)!;
|
|
16
|
+
|
|
17
|
+
// 检查浏览器是否还连接
|
|
18
|
+
if (browser.isConnected()) {
|
|
19
|
+
return browser;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 浏览器断开连接,尝试重连
|
|
23
|
+
try {
|
|
24
|
+
await browser.connect();
|
|
25
|
+
return browser;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error(`Failed to reconnect browser for session ${key}:`, error);
|
|
28
|
+
// 重连失败,移除旧实例
|
|
29
|
+
this.browsers.delete(key);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 创建新的浏览器实例
|
|
34
|
+
const browser = new BrowserManager(session, options);
|
|
35
|
+
await browser.connect();
|
|
36
|
+
this.browsers.set(key, browser);
|
|
37
|
+
|
|
38
|
+
return browser;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async close(sessionName: string): Promise<boolean> {
|
|
42
|
+
const browser = this.browsers.get(sessionName);
|
|
43
|
+
if (!browser) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await browser.close();
|
|
48
|
+
this.browsers.delete(sessionName);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async closeAll(): Promise<void> {
|
|
53
|
+
const closePromises = Array.from(this.browsers.values()).map((b) => b.close());
|
|
54
|
+
await Promise.all(closePromises);
|
|
55
|
+
this.browsers.clear();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
getActiveSessions(): string[] {
|
|
59
|
+
return Array.from(this.browsers.keys());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
size(): number {
|
|
63
|
+
return this.browsers.size;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { CommandRequest, CommandResponse } from "./server";
|
|
6
|
+
|
|
7
|
+
const CONFIG_FILE = join(homedir(), ".hab", "daemon.json");
|
|
8
|
+
|
|
9
|
+
export class DaemonClient {
|
|
10
|
+
private host = "127.0.0.1";
|
|
11
|
+
private port = 9527;
|
|
12
|
+
|
|
13
|
+
async ensureDaemonRunning(): Promise<void> {
|
|
14
|
+
// Load daemon config
|
|
15
|
+
if (existsSync(CONFIG_FILE)) {
|
|
16
|
+
try {
|
|
17
|
+
const content = await readFile(CONFIG_FILE, "utf-8");
|
|
18
|
+
const config = JSON.parse(content);
|
|
19
|
+
this.host = config.host;
|
|
20
|
+
this.port = config.port;
|
|
21
|
+
} catch {
|
|
22
|
+
// Use defaults
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check if daemon is running
|
|
27
|
+
const healthy = await this.checkHealth();
|
|
28
|
+
if (healthy) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Auto-start daemon
|
|
33
|
+
console.log("Starting daemon...");
|
|
34
|
+
await this.startDaemon();
|
|
35
|
+
|
|
36
|
+
// Wait for daemon to be ready
|
|
37
|
+
for (let i = 0; i < 30; i++) {
|
|
38
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
39
|
+
if (await this.checkHealth()) {
|
|
40
|
+
console.log("Daemon started successfully");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
throw new Error("Failed to start daemon");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private async checkHealth(): Promise<boolean> {
|
|
49
|
+
try {
|
|
50
|
+
const response = await fetch(`http://${this.host}:${this.port}/health`, {
|
|
51
|
+
signal: AbortSignal.timeout(1000),
|
|
52
|
+
});
|
|
53
|
+
return response.ok;
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private async startDaemon(): Promise<void> {
|
|
60
|
+
// Start daemon in background
|
|
61
|
+
const projectRoot = join(import.meta.dir, "../..");
|
|
62
|
+
const daemonScript = join(projectRoot, "src/daemon/main.ts");
|
|
63
|
+
|
|
64
|
+
const proc = Bun.spawn({
|
|
65
|
+
cmd: ["bun", daemonScript, "start"],
|
|
66
|
+
cwd: projectRoot,
|
|
67
|
+
stdout: "ignore",
|
|
68
|
+
stderr: "ignore",
|
|
69
|
+
stdin: "ignore",
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
proc.unref();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async execute(request: CommandRequest): Promise<CommandResponse> {
|
|
76
|
+
await this.ensureDaemonRunning();
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetch(`http://${this.host}:${this.port}/execute`, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: { "Content-Type": "application/json" },
|
|
82
|
+
body: JSON.stringify(request),
|
|
83
|
+
signal: AbortSignal.timeout(60000), // 60s timeout
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
const error = await response.text();
|
|
88
|
+
throw new Error(`Daemon error: ${error}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return await response.json();
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (error instanceof Error && error.name === "TimeoutError") {
|
|
94
|
+
throw new Error("Command timed out");
|
|
95
|
+
}
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async closeSession(session: string): Promise<boolean> {
|
|
101
|
+
await this.ensureDaemonRunning();
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const response = await fetch(`http://${this.host}:${this.port}/close`, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
headers: { "Content-Type": "application/json" },
|
|
107
|
+
body: JSON.stringify({ session }),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const result = await response.json();
|
|
111
|
+
return result.closed;
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async listSessions(): Promise<any[]> {
|
|
118
|
+
await this.ensureDaemonRunning();
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const response = await fetch(`http://${this.host}:${this.port}/sessions`);
|
|
122
|
+
const result = await response.json();
|
|
123
|
+
return result.sessions || [];
|
|
124
|
+
} catch {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|