hanzi-browse 2.2.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/README.md +182 -0
- package/dist/agent/loop.d.ts +63 -0
- package/dist/agent/loop.js +186 -0
- package/dist/agent/system-prompt.d.ts +7 -0
- package/dist/agent/system-prompt.js +41 -0
- package/dist/agent/tools.d.ts +9 -0
- package/dist/agent/tools.js +154 -0
- package/dist/cli/detect-credentials.d.ts +31 -0
- package/dist/cli/detect-credentials.js +44 -0
- package/dist/cli/import-credentials-handler.d.ts +14 -0
- package/dist/cli/import-credentials-handler.js +22 -0
- package/dist/cli/session-files.d.ts +28 -0
- package/dist/cli/session-files.js +118 -0
- package/dist/cli/setup.d.ts +10 -0
- package/dist/cli/setup.js +915 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +506 -0
- package/dist/dashboard/assets/index-CEFyesbT.js +46 -0
- package/dist/dashboard/assets/index-Dnht2kLU.css +1 -0
- package/dist/dashboard/index.html +13 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1116 -0
- package/dist/ipc/index.d.ts +8 -0
- package/dist/ipc/index.js +8 -0
- package/dist/ipc/native-host.d.ts +96 -0
- package/dist/ipc/native-host.js +223 -0
- package/dist/ipc/websocket-client.d.ts +73 -0
- package/dist/ipc/websocket-client.js +199 -0
- package/dist/license/manager.d.ts +20 -0
- package/dist/license/manager.js +15 -0
- package/dist/llm/client.d.ts +72 -0
- package/dist/llm/client.js +227 -0
- package/dist/llm/credentials.d.ts +61 -0
- package/dist/llm/credentials.js +200 -0
- package/dist/llm/vertex.d.ts +22 -0
- package/dist/llm/vertex.js +335 -0
- package/dist/managed/api-http.test.d.ts +7 -0
- package/dist/managed/api-http.test.js +623 -0
- package/dist/managed/api.d.ts +51 -0
- package/dist/managed/api.js +1448 -0
- package/dist/managed/api.test.d.ts +10 -0
- package/dist/managed/api.test.js +146 -0
- package/dist/managed/auth.d.ts +38 -0
- package/dist/managed/auth.js +192 -0
- package/dist/managed/billing.d.ts +70 -0
- package/dist/managed/billing.js +227 -0
- package/dist/managed/deploy.d.ts +17 -0
- package/dist/managed/deploy.js +385 -0
- package/dist/managed/e2e.test.d.ts +15 -0
- package/dist/managed/e2e.test.js +151 -0
- package/dist/managed/hardening.test.d.ts +14 -0
- package/dist/managed/hardening.test.js +346 -0
- package/dist/managed/integration.test.d.ts +8 -0
- package/dist/managed/integration.test.js +274 -0
- package/dist/managed/log.d.ts +18 -0
- package/dist/managed/log.js +31 -0
- package/dist/managed/server.d.ts +12 -0
- package/dist/managed/server.js +69 -0
- package/dist/managed/store-pg.d.ts +191 -0
- package/dist/managed/store-pg.js +479 -0
- package/dist/managed/store.d.ts +188 -0
- package/dist/managed/store.js +379 -0
- package/dist/relay/auto-start.d.ts +19 -0
- package/dist/relay/auto-start.js +71 -0
- package/dist/relay/server.d.ts +17 -0
- package/dist/relay/server.js +403 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.js +4 -0
- package/dist/types/session.d.ts +134 -0
- package/dist/types/session.js +16 -0
- package/package.json +61 -0
- package/skills/README.md +48 -0
- package/skills/a11y-auditor/SKILL.md +42 -0
- package/skills/e2e-tester/SKILL.md +154 -0
- package/skills/hanzi-browse/SKILL.md +182 -0
- package/skills/linkedin-prospector/SKILL.md +149 -0
- package/skills/social-poster/SKILL.md +146 -0
- package/skills/x-marketer/SKILL.md +479 -0
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* LLM Browser CLI
|
|
4
|
+
*
|
|
5
|
+
* Command-line interface for browser automation.
|
|
6
|
+
* Sends tasks to the Chrome extension via WebSocket relay.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* hanzi-browser start "task" --url https://example.com
|
|
10
|
+
* hanzi-browser status [session_id]
|
|
11
|
+
* hanzi-browser message <session_id> "message"
|
|
12
|
+
* hanzi-browser logs <session_id> [--follow]
|
|
13
|
+
* hanzi-browser stop <session_id> [--remove]
|
|
14
|
+
* hanzi-browser screenshot <session_id>
|
|
15
|
+
*/
|
|
16
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* LLM Browser CLI
|
|
4
|
+
*
|
|
5
|
+
* Command-line interface for browser automation.
|
|
6
|
+
* Sends tasks to the Chrome extension via WebSocket relay.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* hanzi-browser start "task" --url https://example.com
|
|
10
|
+
* hanzi-browser status [session_id]
|
|
11
|
+
* hanzi-browser message <session_id> "message"
|
|
12
|
+
* hanzi-browser logs <session_id> [--follow]
|
|
13
|
+
* hanzi-browser stop <session_id> [--remove]
|
|
14
|
+
* hanzi-browser screenshot <session_id>
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readFileSync, mkdirSync, watch, writeFileSync } from 'fs';
|
|
17
|
+
import { randomUUID } from 'crypto';
|
|
18
|
+
import { join } from 'path';
|
|
19
|
+
import { fileURLToPath } from 'url';
|
|
20
|
+
import { dirname } from 'path';
|
|
21
|
+
import { WebSocketClient } from './ipc/websocket-client.js';
|
|
22
|
+
import { writeSessionStatus, readSessionStatus, appendSessionLog, listSessions, deleteSessionFiles, getSessionLogPath, getSessionScreenshotPath, } from './cli/session-files.js';
|
|
23
|
+
// Parse command line arguments
|
|
24
|
+
const args = process.argv.slice(2);
|
|
25
|
+
const command = args[0];
|
|
26
|
+
const jsonOutput = args.includes('--json');
|
|
27
|
+
let connection;
|
|
28
|
+
// Track completion for blocking start
|
|
29
|
+
let pendingResolve = null;
|
|
30
|
+
let activeSessionId = null;
|
|
31
|
+
let pendingScreenshotResolve = null;
|
|
32
|
+
async function initConnection() {
|
|
33
|
+
if (connection?.isConnected())
|
|
34
|
+
return;
|
|
35
|
+
connection = new WebSocketClient({
|
|
36
|
+
role: 'cli',
|
|
37
|
+
autoStartRelay: true,
|
|
38
|
+
onDisconnect: () => console.error('[CLI] Relay connection lost, will reconnect'),
|
|
39
|
+
});
|
|
40
|
+
connection.onMessage(handleMessage);
|
|
41
|
+
await connection.connect();
|
|
42
|
+
console.error('[CLI] Connected to WebSocket relay');
|
|
43
|
+
}
|
|
44
|
+
function handleMessage(message) {
|
|
45
|
+
const { type, sessionId, ...data } = message;
|
|
46
|
+
if (!sessionId)
|
|
47
|
+
return;
|
|
48
|
+
// Only process events for the session this CLI instance started.
|
|
49
|
+
// Without this, all relay-connected CLI processes would write
|
|
50
|
+
// logs/status for every session, causing duplicates.
|
|
51
|
+
if (!activeSessionId || sessionId !== activeSessionId)
|
|
52
|
+
return;
|
|
53
|
+
const step = data.step || data.status || data.message;
|
|
54
|
+
switch (type) {
|
|
55
|
+
case 'task_update':
|
|
56
|
+
if (step && step !== 'thinking' && !step.startsWith('[thinking]')) {
|
|
57
|
+
appendSessionLog(sessionId, step);
|
|
58
|
+
writeSessionStatus(sessionId, { status: 'running' });
|
|
59
|
+
if (!jsonOutput)
|
|
60
|
+
console.log(` ${step.slice(0, 100)}`);
|
|
61
|
+
}
|
|
62
|
+
break;
|
|
63
|
+
case 'task_complete': {
|
|
64
|
+
const raw = step || data.result || 'Task completed';
|
|
65
|
+
const result = typeof raw === 'object' ? raw : String(raw);
|
|
66
|
+
const answer = typeof result === 'object' ? JSON.stringify(result, null, 2) : result;
|
|
67
|
+
appendSessionLog(sessionId, `[COMPLETE] ${answer}`);
|
|
68
|
+
writeSessionStatus(sessionId, { status: 'complete', result: answer });
|
|
69
|
+
if (jsonOutput) {
|
|
70
|
+
console.log(JSON.stringify({ session_id: sessionId, status: 'completed', result }));
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
console.log(`\n[CLI] Task completed: ${sessionId}`);
|
|
74
|
+
console.log(answer);
|
|
75
|
+
}
|
|
76
|
+
pendingResolve?.();
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case 'task_error':
|
|
80
|
+
appendSessionLog(sessionId, `[ERROR] ${data.error}`);
|
|
81
|
+
writeSessionStatus(sessionId, { status: 'error', error: data.error });
|
|
82
|
+
if (jsonOutput) {
|
|
83
|
+
console.log(JSON.stringify({ session_id: sessionId, status: 'error', error: data.error }));
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
console.error(`\n[CLI] Task error: ${data.error}`);
|
|
87
|
+
}
|
|
88
|
+
pendingResolve?.();
|
|
89
|
+
break;
|
|
90
|
+
case 'screenshot':
|
|
91
|
+
if (data.data && pendingScreenshotResolve) {
|
|
92
|
+
pendingScreenshotResolve(data.data);
|
|
93
|
+
pendingScreenshotResolve = null;
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function waitForTaskCompletion(timeoutMs = 5 * 60 * 1000) {
|
|
99
|
+
await new Promise((resolve) => {
|
|
100
|
+
pendingResolve = resolve;
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
console.error(`\n[CLI] Task timed out after ${Math.round(timeoutMs / 60000)} minutes`);
|
|
103
|
+
resolve();
|
|
104
|
+
}, timeoutMs);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function disconnectAndExit(code = 0) {
|
|
108
|
+
connection?.disconnect();
|
|
109
|
+
setTimeout(() => process.exit(code), 100);
|
|
110
|
+
}
|
|
111
|
+
// --- Commands ---
|
|
112
|
+
function loadSkillPrompt(skillName) {
|
|
113
|
+
// Resolve relative to package root: dist/cli.js → ../skills/<name>/SKILL.md
|
|
114
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
115
|
+
const __dirname = dirname(__filename);
|
|
116
|
+
const skillPath = join(__dirname, '..', 'skills', skillName, 'SKILL.md');
|
|
117
|
+
if (!existsSync(skillPath))
|
|
118
|
+
return null;
|
|
119
|
+
const content = readFileSync(skillPath, 'utf-8');
|
|
120
|
+
// Strip frontmatter
|
|
121
|
+
return content.replace(/^---[\s\S]*?---\n*/m, '').trim();
|
|
122
|
+
}
|
|
123
|
+
async function cmdStart() {
|
|
124
|
+
const task = args[1];
|
|
125
|
+
if (!task) {
|
|
126
|
+
console.error('Usage: hanzi-browser start "task description" [--url URL] [--context TEXT] [--skill NAME]');
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
let url;
|
|
130
|
+
let context;
|
|
131
|
+
let skill;
|
|
132
|
+
for (let i = 2; i < args.length; i++) {
|
|
133
|
+
if (args[i] === '--url' || args[i] === '-u')
|
|
134
|
+
url = args[++i];
|
|
135
|
+
else if (args[i] === '--context' || args[i] === '-c')
|
|
136
|
+
context = args[++i];
|
|
137
|
+
else if (args[i] === '--skill' || args[i] === '-s')
|
|
138
|
+
skill = args[++i];
|
|
139
|
+
}
|
|
140
|
+
// Inject skill prompt as context
|
|
141
|
+
if (skill) {
|
|
142
|
+
const skillPrompt = loadSkillPrompt(skill);
|
|
143
|
+
if (!skillPrompt) {
|
|
144
|
+
console.error(`Unknown skill: ${skill}`);
|
|
145
|
+
console.error(`Available: ${SKILL_REGISTRY.map(s => s.name).join(', ')}`);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
context = context
|
|
149
|
+
? `${skillPrompt}\n\n---\n\nAdditional context: ${context}`
|
|
150
|
+
: skillPrompt;
|
|
151
|
+
}
|
|
152
|
+
if (!jsonOutput) {
|
|
153
|
+
console.log('[CLI] Starting browser task...');
|
|
154
|
+
console.log(` Task: ${task}`);
|
|
155
|
+
if (url)
|
|
156
|
+
console.log(` URL: ${url}`);
|
|
157
|
+
if (context)
|
|
158
|
+
console.log(` Context: ${context.substring(0, 50)}...`);
|
|
159
|
+
}
|
|
160
|
+
await initConnection();
|
|
161
|
+
const sessionId = randomUUID().slice(0, 8);
|
|
162
|
+
activeSessionId = sessionId;
|
|
163
|
+
writeSessionStatus(sessionId, {
|
|
164
|
+
session_id: sessionId,
|
|
165
|
+
status: 'running',
|
|
166
|
+
task,
|
|
167
|
+
url,
|
|
168
|
+
context,
|
|
169
|
+
});
|
|
170
|
+
await connection.send({
|
|
171
|
+
type: 'mcp_start_task',
|
|
172
|
+
sessionId,
|
|
173
|
+
task,
|
|
174
|
+
url,
|
|
175
|
+
context,
|
|
176
|
+
});
|
|
177
|
+
if (!jsonOutput) {
|
|
178
|
+
console.log(`\n[CLI] Session: ${sessionId}`);
|
|
179
|
+
console.log(` Status: ~/.hanzi-browse/sessions/${sessionId}.json`);
|
|
180
|
+
console.log(` Logs: ~/.hanzi-browse/sessions/${sessionId}.log`);
|
|
181
|
+
console.log(` Skills: run \`hanzi-browser skills\` for optimized workflows (e.g. LinkedIn prospecting)`);
|
|
182
|
+
console.log('\nWaiting for completion...\n');
|
|
183
|
+
}
|
|
184
|
+
// Block until task completes
|
|
185
|
+
await waitForTaskCompletion();
|
|
186
|
+
disconnectAndExit(0);
|
|
187
|
+
}
|
|
188
|
+
function cmdStatus() {
|
|
189
|
+
const sessionId = args[1]?.startsWith('--') ? undefined : args[1];
|
|
190
|
+
if (sessionId) {
|
|
191
|
+
const status = readSessionStatus(sessionId);
|
|
192
|
+
if (!status) {
|
|
193
|
+
console.error(`Session not found: ${sessionId}`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
console.log(JSON.stringify(status, jsonOutput ? undefined : null, jsonOutput ? undefined : 2));
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
const allSessions = listSessions();
|
|
200
|
+
if (jsonOutput) {
|
|
201
|
+
console.log(JSON.stringify(allSessions));
|
|
202
|
+
}
|
|
203
|
+
else if (allSessions.length === 0) {
|
|
204
|
+
console.log('No sessions found.');
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
console.log(`Found ${allSessions.length} session(s):\n`);
|
|
208
|
+
for (const s of allSessions) {
|
|
209
|
+
const taskPreview = s.task ? s.task.substring(0, 55) : '(no task)';
|
|
210
|
+
console.log(` ${s.session_id.padEnd(10)} ${s.status.padEnd(10)} ${taskPreview}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function cmdMessage() {
|
|
216
|
+
const sessionId = args[1];
|
|
217
|
+
const message = args[2];
|
|
218
|
+
if (!sessionId || !message) {
|
|
219
|
+
console.error('Usage: hanzi-browser message <session_id> "message"');
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
activeSessionId = sessionId;
|
|
223
|
+
await initConnection();
|
|
224
|
+
await connection.send({ type: 'mcp_send_message', sessionId, message });
|
|
225
|
+
appendSessionLog(sessionId, `[USER] ${message}`);
|
|
226
|
+
console.log(`Message sent to session ${sessionId}`);
|
|
227
|
+
console.log('Waiting for completion...\n');
|
|
228
|
+
await waitForTaskCompletion();
|
|
229
|
+
disconnectAndExit(0);
|
|
230
|
+
}
|
|
231
|
+
function cmdLogs() {
|
|
232
|
+
const sessionId = args[1];
|
|
233
|
+
const follow = args.includes('--follow') || args.includes('-f');
|
|
234
|
+
if (!sessionId) {
|
|
235
|
+
console.error('Usage: hanzi-browser logs <session_id> [--follow]');
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
const logPath = getSessionLogPath(sessionId);
|
|
239
|
+
if (!existsSync(logPath)) {
|
|
240
|
+
console.error(`Log file not found: ${logPath}`);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
const content = readFileSync(logPath, 'utf-8');
|
|
244
|
+
console.log(content.split('\n').slice(-50).join('\n'));
|
|
245
|
+
if (follow) {
|
|
246
|
+
console.log('\n--- Watching for new logs (Ctrl+C to stop) ---\n');
|
|
247
|
+
let lastSize = content.length;
|
|
248
|
+
const watcher = watch(logPath, () => {
|
|
249
|
+
const newContent = readFileSync(logPath, 'utf-8');
|
|
250
|
+
if (newContent.length > lastSize) {
|
|
251
|
+
process.stdout.write(newContent.slice(lastSize));
|
|
252
|
+
lastSize = newContent.length;
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
process.on('SIGINT', () => { watcher.close(); process.exit(0); });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async function cmdStop() {
|
|
259
|
+
const sessionId = args[1];
|
|
260
|
+
const remove = args.includes('--remove') || args.includes('-r');
|
|
261
|
+
if (!sessionId) {
|
|
262
|
+
console.error('Usage: hanzi-browser stop <session_id> [--remove]');
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
activeSessionId = sessionId;
|
|
266
|
+
await initConnection();
|
|
267
|
+
await connection.send({ type: 'mcp_stop_task', sessionId, remove });
|
|
268
|
+
if (remove) {
|
|
269
|
+
deleteSessionFiles(sessionId);
|
|
270
|
+
console.log(`Session ${sessionId} stopped and removed.`);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
writeSessionStatus(sessionId, { status: 'stopped' });
|
|
274
|
+
console.log(`Session ${sessionId} stopped.`);
|
|
275
|
+
}
|
|
276
|
+
disconnectAndExit(0);
|
|
277
|
+
}
|
|
278
|
+
async function cmdScreenshot() {
|
|
279
|
+
const sessionId = args[1];
|
|
280
|
+
const requestId = sessionId || `screenshot-${Date.now()}`;
|
|
281
|
+
activeSessionId = requestId;
|
|
282
|
+
await initConnection();
|
|
283
|
+
await connection.send({ type: 'mcp_screenshot', sessionId: requestId });
|
|
284
|
+
console.log(`Screenshot requested for ${requestId}. Waiting for image...\n`);
|
|
285
|
+
const data = await new Promise((resolve) => {
|
|
286
|
+
pendingScreenshotResolve = resolve;
|
|
287
|
+
setTimeout(() => {
|
|
288
|
+
pendingScreenshotResolve = null;
|
|
289
|
+
resolve(null);
|
|
290
|
+
}, 10000);
|
|
291
|
+
});
|
|
292
|
+
if (!data) {
|
|
293
|
+
console.error('[CLI] Screenshot timed out');
|
|
294
|
+
disconnectAndExit(1);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const screenshotPath = getSessionScreenshotPath(requestId);
|
|
298
|
+
writeFileSync(screenshotPath, Buffer.from(data, 'base64'));
|
|
299
|
+
console.log(`[CLI] Screenshot saved: ${screenshotPath}`);
|
|
300
|
+
disconnectAndExit(0);
|
|
301
|
+
}
|
|
302
|
+
// --- Skills ---
|
|
303
|
+
const SKILLS_BASE_URL = 'https://raw.githubusercontent.com/hanzili/hanzi-browse/main/server/skills';
|
|
304
|
+
const SKILL_REGISTRY = [
|
|
305
|
+
{
|
|
306
|
+
name: 'linkedin-prospector',
|
|
307
|
+
description: 'Find people on LinkedIn and send personalized connection requests',
|
|
308
|
+
files: ['SKILL.md'],
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
name: 'e2e-tester',
|
|
312
|
+
description: 'Test your web app in a real browser — reports bugs with code references',
|
|
313
|
+
files: ['SKILL.md'],
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: 'social-poster',
|
|
317
|
+
description: 'Post across LinkedIn, Twitter, Reddit, HN — drafts per-platform, posts from your browser',
|
|
318
|
+
files: ['SKILL.md'],
|
|
319
|
+
},
|
|
320
|
+
];
|
|
321
|
+
async function cmdSkills() {
|
|
322
|
+
const subcommand = args[1];
|
|
323
|
+
if (subcommand === 'install') {
|
|
324
|
+
const skillName = args[2];
|
|
325
|
+
if (!skillName) {
|
|
326
|
+
console.error('Usage: hanzi-browser skills install <name>');
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
const skill = SKILL_REGISTRY.find(s => s.name === skillName);
|
|
330
|
+
if (!skill) {
|
|
331
|
+
console.error(`Unknown skill: ${skillName}`);
|
|
332
|
+
console.error(`Available: ${SKILL_REGISTRY.map(s => s.name).join(', ')}`);
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
// Detect the right directory
|
|
336
|
+
const targetDir = detectSkillsDir(skillName);
|
|
337
|
+
mkdirSync(targetDir, { recursive: true });
|
|
338
|
+
console.log(`Installing ${skillName}...`);
|
|
339
|
+
for (const file of skill.files) {
|
|
340
|
+
const url = `${SKILLS_BASE_URL}/${skillName}/${file}`;
|
|
341
|
+
try {
|
|
342
|
+
const response = await fetch(url);
|
|
343
|
+
if (!response.ok)
|
|
344
|
+
throw new Error(`HTTP ${response.status}`);
|
|
345
|
+
const content = await response.text();
|
|
346
|
+
const filePath = join(targetDir, file);
|
|
347
|
+
writeFileSync(filePath, content);
|
|
348
|
+
console.log(` → ${filePath}`);
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
console.error(` Failed to download ${file}: ${err.message}`);
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
console.log(`\nDone! "${skillName}" is ready to use.`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
// Default: list available skills
|
|
359
|
+
console.log('\nAvailable skills:\n');
|
|
360
|
+
for (const skill of SKILL_REGISTRY) {
|
|
361
|
+
console.log(` ${skill.name.padEnd(24)} ${skill.description}`);
|
|
362
|
+
}
|
|
363
|
+
console.log(`\nInstall: hanzi-browser skills install <name>`);
|
|
364
|
+
console.log(`Browse: https://browse.hanzilla.co/skills\n`);
|
|
365
|
+
}
|
|
366
|
+
function detectSkillsDir(skillName) {
|
|
367
|
+
// Check for common agent skill directories in the current project
|
|
368
|
+
// Priority: .agents/skills (universal) > .claude/skills (Claude Code) > .cursor/rules (Cursor)
|
|
369
|
+
if (existsSync('.agents/skills') || existsSync('.agents')) {
|
|
370
|
+
return join('.agents', 'skills', skillName);
|
|
371
|
+
}
|
|
372
|
+
if (existsSync('.claude/skills') || existsSync('.claude')) {
|
|
373
|
+
return join('.claude', 'skills', skillName);
|
|
374
|
+
}
|
|
375
|
+
// Default to .agents/skills (most portable)
|
|
376
|
+
return join('.agents', 'skills', skillName);
|
|
377
|
+
}
|
|
378
|
+
async function cmdSetup() {
|
|
379
|
+
const { runSetup } = await import('./cli/setup.js');
|
|
380
|
+
let only;
|
|
381
|
+
let yes = false;
|
|
382
|
+
for (let i = 1; i < args.length; i++) {
|
|
383
|
+
if (args[i] === '--only' && args[i + 1])
|
|
384
|
+
only = args[++i];
|
|
385
|
+
if (args[i] === '--yes' || args[i] === '-y')
|
|
386
|
+
yes = true;
|
|
387
|
+
}
|
|
388
|
+
await runSetup({ only, yes });
|
|
389
|
+
}
|
|
390
|
+
function cmdHelp() {
|
|
391
|
+
console.log(`
|
|
392
|
+
Hanzi Browser CLI - Browser automation from the command line
|
|
393
|
+
|
|
394
|
+
Controls your real Chrome browser with your existing logins, cookies, and
|
|
395
|
+
sessions. Good for authenticated sites, dynamic pages, and multi-step tasks
|
|
396
|
+
that need a real browser.
|
|
397
|
+
|
|
398
|
+
Usage:
|
|
399
|
+
hanzi-browser <command> [options]
|
|
400
|
+
|
|
401
|
+
Commands:
|
|
402
|
+
start <task> Start a browser automation task
|
|
403
|
+
--url, -u <url> Starting URL
|
|
404
|
+
--context, -c <text> Context information for the task
|
|
405
|
+
--skill, -s <name> Use a bundled skill (e.g. linkedin-prospector)
|
|
406
|
+
Blocks until complete or timeout.
|
|
407
|
+
You can run multiple start commands in parallel.
|
|
408
|
+
Each session gets its own browser window.
|
|
409
|
+
|
|
410
|
+
status [session_id] Show status of session(s)
|
|
411
|
+
|
|
412
|
+
message <session_id> <msg> Send follow-up instructions to a session
|
|
413
|
+
Reuses the same browser window and page state.
|
|
414
|
+
|
|
415
|
+
logs <session_id> Show logs for a session
|
|
416
|
+
--follow, -f Watch logs in real-time
|
|
417
|
+
|
|
418
|
+
stop <session_id> Stop a session
|
|
419
|
+
--remove, -r Also delete session files
|
|
420
|
+
|
|
421
|
+
screenshot [session_id] Take a screenshot
|
|
422
|
+
|
|
423
|
+
setup Auto-detect AI agents and configure MCP
|
|
424
|
+
--only <agent> Only configure one agent (claude-code, cursor, windsurf, claude-desktop)
|
|
425
|
+
|
|
426
|
+
skills List available agent skills
|
|
427
|
+
skills install <name> Download a skill into your project
|
|
428
|
+
|
|
429
|
+
help Show this help message
|
|
430
|
+
|
|
431
|
+
Typical workflow:
|
|
432
|
+
1. Run \`hanzi-browser start "task"\`
|
|
433
|
+
2. If needed, inspect progress with \`status\`, \`logs\`, or \`screenshot\`
|
|
434
|
+
3. Continue the same session with \`message <session_id> "next step"\`
|
|
435
|
+
4. Stop it with \`stop <session_id>\`
|
|
436
|
+
|
|
437
|
+
Use Hanzi when the task needs a real browser:
|
|
438
|
+
- Logged-in sites: Jira, LinkedIn, Slack, GitHub, dashboards
|
|
439
|
+
- UI testing and visual verification
|
|
440
|
+
- Form filling in third-party web apps
|
|
441
|
+
- Dynamic pages and infinite scroll
|
|
442
|
+
|
|
443
|
+
Prefer other tools first for:
|
|
444
|
+
- Code inspection, git history, logs
|
|
445
|
+
- APIs, SDKs, CLI commands, or other MCPs
|
|
446
|
+
- Public/static pages you can fetch directly
|
|
447
|
+
- Local files, env vars, structured data
|
|
448
|
+
|
|
449
|
+
Examples:
|
|
450
|
+
hanzi-browser start "Search LinkedIn for immigration consultants in Toronto and collect 10 names" --url https://www.linkedin.com
|
|
451
|
+
hanzi-browser start "Check flight prices to Tokyo" --url https://flights.google.com
|
|
452
|
+
hanzi-browser status abc123
|
|
453
|
+
hanzi-browser logs abc123 --follow
|
|
454
|
+
hanzi-browser message abc123 "Click the first result and summarize the page"
|
|
455
|
+
hanzi-browser screenshot abc123
|
|
456
|
+
hanzi-browser stop abc123 --remove
|
|
457
|
+
|
|
458
|
+
Skills:
|
|
459
|
+
Pre-built workflows for common tasks (LinkedIn prospecting, etc.).
|
|
460
|
+
Run \`hanzi-browser skills\` to see what's available, or install one:
|
|
461
|
+
\`hanzi-browser skills install linkedin-prospector\`
|
|
462
|
+
`);
|
|
463
|
+
}
|
|
464
|
+
// --- Main ---
|
|
465
|
+
async function main() {
|
|
466
|
+
switch (command) {
|
|
467
|
+
case 'start':
|
|
468
|
+
await cmdStart();
|
|
469
|
+
break;
|
|
470
|
+
case 'status':
|
|
471
|
+
cmdStatus();
|
|
472
|
+
break;
|
|
473
|
+
case 'message':
|
|
474
|
+
await cmdMessage();
|
|
475
|
+
break;
|
|
476
|
+
case 'logs':
|
|
477
|
+
cmdLogs();
|
|
478
|
+
break;
|
|
479
|
+
case 'stop':
|
|
480
|
+
await cmdStop();
|
|
481
|
+
break;
|
|
482
|
+
case 'screenshot':
|
|
483
|
+
await cmdScreenshot();
|
|
484
|
+
break;
|
|
485
|
+
case 'skills':
|
|
486
|
+
await cmdSkills();
|
|
487
|
+
break;
|
|
488
|
+
case 'setup':
|
|
489
|
+
await cmdSetup();
|
|
490
|
+
break;
|
|
491
|
+
case 'help':
|
|
492
|
+
case '--help':
|
|
493
|
+
case '-h':
|
|
494
|
+
case undefined:
|
|
495
|
+
cmdHelp();
|
|
496
|
+
break;
|
|
497
|
+
default:
|
|
498
|
+
console.error(`Unknown command: ${command}`);
|
|
499
|
+
cmdHelp();
|
|
500
|
+
process.exit(1);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
main().catch((err) => {
|
|
504
|
+
console.error('[CLI] Error:', err);
|
|
505
|
+
process.exit(1);
|
|
506
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))a(i);new MutationObserver(i=>{for(const o of i)if(o.type==="childList")for(const l of o.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&a(l)}).observe(document,{childList:!0,subtree:!0});function n(i){const o={};return i.integrity&&(o.integrity=i.integrity),i.referrerPolicy&&(o.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?o.credentials="include":i.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function a(i){if(i.ep)return;i.ep=!0;const o=n(i);fetch(i.href,o)}})();var le,w,Le,U,Se,Oe,Me,Be,ye,de,pe,se={},oe=[],Qe=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,ce=Array.isArray;function D(t,e){for(var n in e)t[n]=e[n];return t}function ve(t){t&&t.parentNode&&t.parentNode.removeChild(t)}function s(t,e,n){var a,i,o,l={};for(o in e)o=="key"?a=e[o]:o=="ref"?i=e[o]:l[o]=e[o];if(arguments.length>2&&(l.children=arguments.length>3?le.call(arguments,2):n),typeof t=="function"&&t.defaultProps!=null)for(o in t.defaultProps)l[o]===void 0&&(l[o]=t.defaultProps[o]);return ee(t,l,a,i,null)}function ee(t,e,n,a,i){var o={type:t,props:e,key:n,ref:a,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:i??++Le,__i:-1,__u:0};return i==null&&w.vnode!=null&&w.vnode(o),o}function G(t){return t.children}function te(t,e){this.props=t,this.context=e}function B(t,e){if(e==null)return t.__?B(t.__,t.__i+1):null;for(var n;e<t.__k.length;e++)if((n=t.__k[e])!=null&&n.__e!=null)return n.__e;return typeof t.type=="function"?B(t):null}function et(t){if(t.__P&&t.__d){var e=t.__v,n=e.__e,a=[],i=[],o=D({},e);o.__v=e.__v+1,w.vnode&&w.vnode(o),ge(t.__P,o,e,t.__n,t.__P.namespaceURI,32&e.__u?[n]:null,a,n??B(e),!!(32&e.__u),i),o.__v=e.__v,o.__.__k[o.__i]=o,We(a,o,i),e.__e=e.__=null,o.__e!=n&&Ge(o)}}function Ge(t){if((t=t.__)!=null&&t.__c!=null)return t.__e=t.__c.base=null,t.__k.some(function(e){if(e!=null&&e.__e!=null)return t.__e=t.__c.base=e.__e}),Ge(t)}function Ce(t){(!t.__d&&(t.__d=!0)&&U.push(t)&&!ie.__r++||Se!=w.debounceRendering)&&((Se=w.debounceRendering)||Oe)(ie)}function ie(){try{for(var t,e=1;U.length;)U.length>e&&U.sort(Me),t=U.shift(),e=U.length,et(t)}finally{U.length=ie.__r=0}}function Fe(t,e,n,a,i,o,l,u,_,c,f){var r,d,p,y,S,b,g,h=a&&a.__k||oe,N=e.length;for(_=tt(n,e,h,_,N),r=0;r<N;r++)(p=n.__k[r])!=null&&(d=p.__i!=-1&&h[p.__i]||se,p.__i=r,b=ge(t,p,d,i,o,l,u,_,c,f),y=p.__e,p.ref&&d.ref!=p.ref&&(d.ref&&be(d.ref,null,p),f.push(p.ref,p.__c||y,p)),S==null&&y!=null&&(S=y),(g=!!(4&p.__u))||d.__k===p.__k?_=Ke(p,_,t,g):typeof p.type=="function"&&b!==void 0?_=b:y&&(_=y.nextSibling),p.__u&=-7);return n.__e=S,_}function tt(t,e,n,a,i){var o,l,u,_,c,f=n.length,r=f,d=0;for(t.__k=new Array(i),o=0;o<i;o++)(l=e[o])!=null&&typeof l!="boolean"&&typeof l!="function"?(typeof l=="string"||typeof l=="number"||typeof l=="bigint"||l.constructor==String?l=t.__k[o]=ee(null,l,null,null,null):ce(l)?l=t.__k[o]=ee(G,{children:l},null,null,null):l.constructor===void 0&&l.__b>0?l=t.__k[o]=ee(l.type,l.props,l.key,l.ref?l.ref:null,l.__v):t.__k[o]=l,_=o+d,l.__=t,l.__b=t.__b+1,u=null,(c=l.__i=nt(l,n,_,r))!=-1&&(r--,(u=n[c])&&(u.__u|=2)),u==null||u.__v==null?(c==-1&&(i>f?d--:i<f&&d++),typeof l.type!="function"&&(l.__u|=4)):c!=_&&(c==_-1?d--:c==_+1?d++:(c>_?d--:d++,l.__u|=4))):t.__k[o]=null;if(r)for(o=0;o<f;o++)(u=n[o])!=null&&!(2&u.__u)&&(u.__e==a&&(a=B(u)),qe(u,u));return a}function Ke(t,e,n,a){var i,o;if(typeof t.type=="function"){for(i=t.__k,o=0;i&&o<i.length;o++)i[o]&&(i[o].__=t,e=Ke(i[o],e,n,a));return e}t.__e!=e&&(a&&(e&&t.type&&!e.parentNode&&(e=B(t)),n.insertBefore(t.__e,e||null)),e=t.__e);do e=e&&e.nextSibling;while(e!=null&&e.nodeType==8);return e}function nt(t,e,n,a){var i,o,l,u=t.key,_=t.type,c=e[n],f=c!=null&&(2&c.__u)==0;if(c===null&&u==null||f&&u==c.key&&_==c.type)return n;if(a>(f?1:0)){for(i=n-1,o=n+1;i>=0||o<e.length;)if((c=e[l=i>=0?i--:o++])!=null&&!(2&c.__u)&&u==c.key&&_==c.type)return l}return-1}function Ae(t,e,n){e[0]=="-"?t.setProperty(e,n??""):t[e]=n==null?"":typeof n!="number"||Qe.test(e)?n:n+"px"}function Q(t,e,n,a,i){var o,l;e:if(e=="style")if(typeof n=="string")t.style.cssText=n;else{if(typeof a=="string"&&(t.style.cssText=a=""),a)for(e in a)n&&e in n||Ae(t.style,e,"");if(n)for(e in n)a&&n[e]==a[e]||Ae(t.style,e,n[e])}else if(e[0]=="o"&&e[1]=="n")o=e!=(e=e.replace(Be,"$1")),l=e.toLowerCase(),e=l in t||e=="onFocusOut"||e=="onFocusIn"?l.slice(2):e.slice(2),t.l||(t.l={}),t.l[e+o]=n,n?a?n.u=a.u:(n.u=ye,t.addEventListener(e,o?pe:de,o)):t.removeEventListener(e,o?pe:de,o);else{if(i=="http://www.w3.org/2000/svg")e=e.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(e!="width"&&e!="height"&&e!="href"&&e!="list"&&e!="form"&&e!="tabIndex"&&e!="download"&&e!="rowSpan"&&e!="colSpan"&&e!="role"&&e!="popover"&&e in t)try{t[e]=n??"";break e}catch{}typeof n=="function"||(n==null||n===!1&&e[4]!="-"?t.removeAttribute(e):t.setAttribute(e,e=="popover"&&n==1?"":n))}}function Pe(t){return function(e){if(this.l){var n=this.l[e.type+t];if(e.t==null)e.t=ye++;else if(e.t<n.u)return;return n(w.event?w.event(e):e)}}}function ge(t,e,n,a,i,o,l,u,_,c){var f,r,d,p,y,S,b,g,h,N,C,I,M,H,R,A=e.type;if(e.constructor!==void 0)return null;128&n.__u&&(_=!!(32&n.__u),o=[u=e.__e=n.__e]),(f=w.__b)&&f(e);e:if(typeof A=="function")try{if(g=e.props,h=A.prototype&&A.prototype.render,N=(f=A.contextType)&&a[f.__c],C=f?N?N.props.value:f.__:a,n.__c?b=(r=e.__c=n.__c).__=r.__E:(h?e.__c=r=new A(g,C):(e.__c=r=new te(g,C),r.constructor=A,r.render=ot),N&&N.sub(r),r.state||(r.state={}),r.__n=a,d=r.__d=!0,r.__h=[],r._sb=[]),h&&r.__s==null&&(r.__s=r.state),h&&A.getDerivedStateFromProps!=null&&(r.__s==r.state&&(r.__s=D({},r.__s)),D(r.__s,A.getDerivedStateFromProps(g,r.__s))),p=r.props,y=r.state,r.__v=e,d)h&&A.getDerivedStateFromProps==null&&r.componentWillMount!=null&&r.componentWillMount(),h&&r.componentDidMount!=null&&r.__h.push(r.componentDidMount);else{if(h&&A.getDerivedStateFromProps==null&&g!==p&&r.componentWillReceiveProps!=null&&r.componentWillReceiveProps(g,C),e.__v==n.__v||!r.__e&&r.shouldComponentUpdate!=null&&r.shouldComponentUpdate(g,r.__s,C)===!1){e.__v!=n.__v&&(r.props=g,r.state=r.__s,r.__d=!1),e.__e=n.__e,e.__k=n.__k,e.__k.some(function(P){P&&(P.__=e)}),oe.push.apply(r.__h,r._sb),r._sb=[],r.__h.length&&l.push(r);break e}r.componentWillUpdate!=null&&r.componentWillUpdate(g,r.__s,C),h&&r.componentDidUpdate!=null&&r.__h.push(function(){r.componentDidUpdate(p,y,S)})}if(r.context=C,r.props=g,r.__P=t,r.__e=!1,I=w.__r,M=0,h)r.state=r.__s,r.__d=!1,I&&I(e),f=r.render(r.props,r.state,r.context),oe.push.apply(r.__h,r._sb),r._sb=[];else do r.__d=!1,I&&I(e),f=r.render(r.props,r.state,r.context),r.state=r.__s;while(r.__d&&++M<25);r.state=r.__s,r.getChildContext!=null&&(a=D(D({},a),r.getChildContext())),h&&!d&&r.getSnapshotBeforeUpdate!=null&&(S=r.getSnapshotBeforeUpdate(p,y)),H=f!=null&&f.type===G&&f.key==null?je(f.props.children):f,u=Fe(t,ce(H)?H:[H],e,n,a,i,o,l,u,_,c),r.base=e.__e,e.__u&=-161,r.__h.length&&l.push(r),b&&(r.__E=r.__=null)}catch(P){if(e.__v=null,_||o!=null)if(P.then){for(e.__u|=_?160:128;u&&u.nodeType==8&&u.nextSibling;)u=u.nextSibling;o[o.indexOf(u)]=null,e.__e=u}else{for(R=o.length;R--;)ve(o[R]);fe(e)}else e.__e=n.__e,e.__k=n.__k,P.then||fe(e);w.__e(P,e,n)}else o==null&&e.__v==n.__v?(e.__k=n.__k,e.__e=n.__e):u=e.__e=st(n.__e,e,n,a,i,o,l,_,c);return(f=w.diffed)&&f(e),128&e.__u?void 0:u}function fe(t){t&&(t.__c&&(t.__c.__e=!0),t.__k&&t.__k.some(fe))}function We(t,e,n){for(var a=0;a<n.length;a++)be(n[a],n[++a],n[++a]);w.__c&&w.__c(e,t),t.some(function(i){try{t=i.__h,i.__h=[],t.some(function(o){o.call(i)})}catch(o){w.__e(o,i.__v)}})}function je(t){return typeof t!="object"||t==null||t.__b>0?t:ce(t)?t.map(je):D({},t)}function st(t,e,n,a,i,o,l,u,_){var c,f,r,d,p,y,S,b=n.props||se,g=e.props,h=e.type;if(h=="svg"?i="http://www.w3.org/2000/svg":h=="math"?i="http://www.w3.org/1998/Math/MathML":i||(i="http://www.w3.org/1999/xhtml"),o!=null){for(c=0;c<o.length;c++)if((p=o[c])&&"setAttribute"in p==!!h&&(h?p.localName==h:p.nodeType==3)){t=p,o[c]=null;break}}if(t==null){if(h==null)return document.createTextNode(g);t=document.createElementNS(i,h,g.is&&g),u&&(w.__m&&w.__m(e,o),u=!1),o=null}if(h==null)b===g||u&&t.data==g||(t.data=g);else{if(o=o&&le.call(t.childNodes),!u&&o!=null)for(b={},c=0;c<t.attributes.length;c++)b[(p=t.attributes[c]).name]=p.value;for(c in b)p=b[c],c=="dangerouslySetInnerHTML"?r=p:c=="children"||c in g||c=="value"&&"defaultValue"in g||c=="checked"&&"defaultChecked"in g||Q(t,c,null,p,i);for(c in g)p=g[c],c=="children"?d=p:c=="dangerouslySetInnerHTML"?f=p:c=="value"?y=p:c=="checked"?S=p:u&&typeof p!="function"||b[c]===p||Q(t,c,p,b[c],i);if(f)u||r&&(f.__html==r.__html||f.__html==t.innerHTML)||(t.innerHTML=f.__html),e.__k=[];else if(r&&(t.innerHTML=""),Fe(e.type=="template"?t.content:t,ce(d)?d:[d],e,n,a,h=="foreignObject"?"http://www.w3.org/1999/xhtml":i,o,l,o?o[0]:n.__k&&B(n,0),u,_),o!=null)for(c=o.length;c--;)ve(o[c]);u||(c="value",h=="progress"&&y==null?t.removeAttribute("value"):y!=null&&(y!==t[c]||h=="progress"&&!y||h=="option"&&y!=b[c])&&Q(t,c,y,b[c],i),c="checked",S!=null&&S!=t[c]&&Q(t,c,S,b[c],i))}return t}function be(t,e,n){try{if(typeof t=="function"){var a=typeof t.__u=="function";a&&t.__u(),a&&e==null||(t.__u=t(e))}else t.current=e}catch(i){w.__e(i,n)}}function qe(t,e,n){var a,i;if(w.unmount&&w.unmount(t),(a=t.ref)&&(a.current&&a.current!=t.__e||be(a,null,e)),(a=t.__c)!=null){if(a.componentWillUnmount)try{a.componentWillUnmount()}catch(o){w.__e(o,e)}a.base=a.__P=null}if(a=t.__k)for(i=0;i<a.length;i++)a[i]&&qe(a[i],e,n||typeof t.type!="function");n||ve(t.__e),t.__c=t.__=t.__e=void 0}function ot(t,e,n){return this.constructor(t,n)}function it(t,e,n){var a,i,o,l;e==document&&(e=document.documentElement),w.__&&w.__(t,e),i=(a=!1)?null:e.__k,o=[],l=[],ge(e,t=e.__k=s(G,null,[t]),i||se,se,e.namespaceURI,i?null:e.firstChild?le.call(e.childNodes):null,o,i?i.__e:e.firstChild,a,l),We(o,t,l)}le=oe.slice,w={__e:function(t,e,n,a){for(var i,o,l;e=e.__;)if((i=e.__c)&&!i.__)try{if((o=i.constructor)&&o.getDerivedStateFromError!=null&&(i.setState(o.getDerivedStateFromError(t)),l=i.__d),i.componentDidCatch!=null&&(i.componentDidCatch(t,a||{}),l=i.__d),l)return i.__E=i}catch(u){t=u}throw t}},Le=0,te.prototype.setState=function(t,e){var n;n=this.__s!=null&&this.__s!=this.state?this.__s:this.__s=D({},this.state),typeof t=="function"&&(t=t(D({},n),this.props)),t&&D(n,t),t!=null&&this.__v&&(e&&this._sb.push(e),Ce(this))},te.prototype.forceUpdate=function(t){this.__v&&(this.__e=!0,t&&this.__h.push(t),Ce(this))},te.prototype.render=G,U=[],Oe=typeof Promise=="function"?Promise.prototype.then.bind(Promise.resolve()):setTimeout,Me=function(t,e){return t.__v.__b-e.__v.__b},ie.__r=0,Be=/(PointerCapture)$|Capture$/i,ye=0,de=Pe(!1),pe=Pe(!0);var Z,x,ue,$e,ae=0,Ye=[],T=w,ze=T.__b,Ie=T.__r,Ne=T.diffed,Ee=T.__c,He=T.unmount,Re=T.__;function ke(t,e){T.__h&&T.__h(x,t,ae||e),ae=0;var n=x.__H||(x.__H={__:[],__h:[]});return t>=n.__.length&&n.__.push({}),n.__[t]}function k(t){return ae=1,at(Je,t)}function at(t,e,n){var a=ke(Z++,2);if(a.t=t,!a.__c&&(a.__=[Je(void 0,e),function(u){var _=a.__N?a.__N[0]:a.__[0],c=a.t(_,u);_!==c&&(a.__N=[c,a.__[1]],a.__c.setState({}))}],a.__c=x,!x.__f)){var i=function(u,_,c){if(!a.__c.__H)return!0;var f=a.__c.__H.__.filter(function(d){return d.__c});if(f.every(function(d){return!d.__N}))return!o||o.call(this,u,_,c);var r=a.__c.props!==u;return f.some(function(d){if(d.__N){var p=d.__[0];d.__=d.__N,d.__N=void 0,p!==d.__[0]&&(r=!0)}}),o&&o.call(this,u,_,c)||r};x.__f=!0;var o=x.shouldComponentUpdate,l=x.componentWillUpdate;x.componentWillUpdate=function(u,_,c){if(this.__e){var f=o;o=void 0,i(u,_,c),o=f}l&&l.call(this,u,_,c)},x.shouldComponentUpdate=i}return a.__N||a.__}function he(t,e){var n=ke(Z++,3);!T.__s&&Ze(n.__H,e)&&(n.__=t,n.u=e,x.__H.__h.push(n))}function rt(t,e){var n=ke(Z++,7);return Ze(n.__H,e)&&(n.__=t(),n.__H=e,n.__h=t),n.__}function Y(t,e){return ae=8,rt(function(){return t},e)}function lt(){for(var t;t=Ye.shift();){var e=t.__H;if(t.__P&&e)try{e.__h.some(ne),e.__h.some(me),e.__h=[]}catch(n){e.__h=[],T.__e(n,t.__v)}}}T.__b=function(t){x=null,ze&&ze(t)},T.__=function(t,e){t&&e.__k&&e.__k.__m&&(t.__m=e.__k.__m),Re&&Re(t,e)},T.__r=function(t){Ie&&Ie(t),Z=0;var e=(x=t.__c).__H;e&&(ue===x?(e.__h=[],x.__h=[],e.__.some(function(n){n.__N&&(n.__=n.__N),n.u=n.__N=void 0})):(e.__h.some(ne),e.__h.some(me),e.__h=[],Z=0)),ue=x},T.diffed=function(t){Ne&&Ne(t);var e=t.__c;e&&e.__H&&(e.__H.__h.length&&(Ye.push(e)!==1&&$e===T.requestAnimationFrame||(($e=T.requestAnimationFrame)||ct)(lt)),e.__H.__.some(function(n){n.u&&(n.__H=n.u),n.u=void 0})),ue=x=null},T.__c=function(t,e){e.some(function(n){try{n.__h.some(ne),n.__h=n.__h.filter(function(a){return!a.__||me(a)})}catch(a){e.some(function(i){i.__h&&(i.__h=[])}),e=[],T.__e(a,n.__v)}}),Ee&&Ee(t,e)},T.unmount=function(t){He&&He(t);var e,n=t.__c;n&&n.__H&&(n.__H.__.some(function(a){try{ne(a)}catch(i){e=i}}),n.__H=void 0,e&&T.__e(e,n.__v))};var De=typeof requestAnimationFrame=="function";function ct(t){var e,n=function(){clearTimeout(a),De&&cancelAnimationFrame(e),setTimeout(t)},a=setTimeout(n,35);De&&(e=requestAnimationFrame(n))}function ne(t){var e=x,n=t.__c;typeof n=="function"&&(t.__c=void 0,n()),x=e}function me(t){var e=x;t.__c=t.__(),x=e}function Ze(t,e){return!t||t.length!==e.length||e.some(function(n,a){return n!==t[a]})}function Je(t,e){return typeof e=="function"?e(t):e}const _t="";async function z(t,e,n){const a={method:t,headers:{"Content-Type":"application/json"},credentials:"include"};n&&(a.body=JSON.stringify(n));const i=await fetch(_t+e,a);if(i.status===401)return{status:401,data:null,unauthorized:!0};const o=await i.json().catch(()=>null);return{status:i.status,data:o}}async function ut(){try{const e=await(await fetch("/api/auth/sign-in/social",{method:"POST",headers:{"Content-Type":"application/json"},credentials:"include",body:JSON.stringify({provider:"google",callbackURL:"/dashboard"})})).json().catch(()=>null);if(e!=null&&e.url){window.location.href=e.url;return}}catch{}window.location.href="/api/auth/sign-in/social?provider=google&callbackURL=/dashboard"}function re({text:t,label:e="Copy"}){const[n,a]=k(!1);return s("button",{class:"btn-copy",onClick:()=>{navigator.clipboard.writeText(t),a(!0),setTimeout(()=>a(!1),2e3)}},n?"✓ Copied!":e)}function Ve(t){if(!t)return"never";const e=Math.floor((Date.now()-t)/1e3);return e<60?`${e}s ago`:e<3600?`${Math.floor(e/60)}m ago`:e<86400?`${Math.floor(e/3600)}h ago`:`${Math.floor(e/86400)}d ago`}function dt(){var m,$,L;const[t,e]=k(!0),[n,a]=k(null),[i,o]=k([]),[l,u]=k([]),[_,c]=k(null),[f,r]=k(null),[d,p]=k(null),[y,S]=k("start"),[b,g]=k(!1),[h,N]=k(!1),[C,I]=k(!1),[M,H]=k(!1),R=Y(async()=>{const v=await z("GET","/v1/me");if(v!=null&&v.unauthorized){H(!0);return}v!=null&&v.data&&a(v.data)},[]),A=Y(async()=>{var E;const v=await z("GET","/v1/api-keys");v&&o(((E=v.data)==null?void 0:E.api_keys)||[])},[]),P=Y(async()=>{var E;const v=await z("GET","/v1/browser-sessions");v&&u(((E=v.data)==null?void 0:E.sessions)||[])},[]),F=Y(async()=>{const v=await z("GET","/v1/usage");v&&c(v.data)},[]),J=Y(async()=>{const v=await z("GET","/v1/billing/credits");v&&r(v.data)},[]);if(he(()=>{Promise.all([R(),A(),P(),F(),J()]).then(()=>e(!1));const v=setInterval(P,5e3);return()=>clearInterval(v)},[]),he(()=>{const v=E=>{var j,q;((j=E.data)==null?void 0:j.type)==="HANZI_EXTENSION_READY"&&g(!0),((q=E.data)==null?void 0:q.type)==="HANZI_PAIR_RESULT"&&(N(!1),E.data.success?(I(!0),P()):p("Pairing failed: "+(E.data.error||"unknown")))};return window.addEventListener("message",v),window.postMessage({type:"HANZI_PING"},"*"),()=>window.removeEventListener("message",v)},[]),t)return s(vt,null);if(M)return s("div",{class:"page",style:{textAlign:"center",paddingTop:80}},s("h1",{style:{fontSize:24,marginBottom:8}},"Hanzi Dashboard"),s("p",{style:{color:"var(--muted)",marginBottom:24}},"Sign in to manage your workspace."),s("button",{class:"btn-primary",onClick:ut,style:{fontSize:15,padding:"12px 28px"}},"Sign in with Google"));const V=(($=(m=n==null?void 0:n.user)==null?void 0:m.name)==null?void 0:$.split(" ")[0])||"there",K=(L=n==null?void 0:n.user)!=null&&L.name?`${n.user.name}'s workspace`:"Your workspace",_e=i.length>0,W=l.find(v=>v.status==="connected"),X=!!W||C;return s("div",{class:"page"},s("div",{class:"header"},s("div",null,s("h1",null,K),s("div",{class:"subtitle"},"Hi, ",V)),s("div",{style:{display:"flex",alignItems:"center",gap:12}},f&&s("div",{style:{textAlign:"right",fontSize:13,color:"var(--muted)"}},s("div",null,s("strong",{style:{color:"var(--ink)",fontSize:16}},(f.free_remaining||0)+(f.credit_balance||0))," tasks left"),s("div",null,f.free_remaining||0," free + ",f.credit_balance||0," credits")),s("button",{class:"signout",onClick:gt},"Sign out"))),s("div",{class:"tabs"},s("button",{class:`tab ${y==="start"?"active":""}`,onClick:()=>S("start")},"Getting Started"),s("button",{class:`tab ${y==="sessions"?"active":""}`,onClick:()=>S("sessions")},"Sessions",l.length>0&&s("span",{class:"tab-count"},l.filter(v=>v.status==="connected").length)),s("button",{class:`tab ${y==="settings"?"active":""}`,onClick:()=>S("settings")},"Settings")),y==="start"&&s(pt,{keys:i,loadKeys:A,setError:p,extensionReady:b,pairing:h,paired:C,setPairing:N,setPaired:I,hasKeys:_e,hasConnected:X,connectedSession:W,sessions:l,loadSessions:P,loadUsage:F}),y==="sessions"&&s(ht,{sessions:l,onRefresh:P,usage:_}),y==="settings"&&s(mt,{keys:i,loadKeys:A,setError:p,profile:n,credits:f,loadCredits:J}),d&&s("div",{class:"error-toast",onClick:()=>p(null)},d))}function pt({keys:t,loadKeys:e,setError:n,extensionReady:a,pairing:i,paired:o,setPairing:l,setPaired:u,hasKeys:_,hasConnected:c,connectedSession:f,sessions:r,loadSessions:d,loadUsage:p}){var X;const[y,S]=k(""),[b,g]=k(null),[h,N]=k("Go to example.com and tell me the page title"),[C,I]=k(null),[M,H]=k(""),[R,A]=k(0),P=C==="complete",F=async()=>{var $;if(!y.trim())return;const m=await z("POST","/v1/api-keys",{name:y.trim()});(m==null?void 0:m.status)===201?(g(m.data.key),S(""),await e()):n((($=m==null?void 0:m.data)==null?void 0:$.error)||"Failed")},J=async()=>{var $;l(!0);const m=await z("POST","/v1/browser-sessions/pair",{label:"Developer testing"});if(!m||m.status!==201){l(!1),n((($=m==null?void 0:m.data)==null?void 0:$.error)||"Failed");return}window.postMessage({type:"HANZI_PAIR",token:m.data.pairing_token,apiUrl:location.origin},"*"),setTimeout(()=>l(L=>L&&(n("Extension did not respond."),!1)),5e3)},V=async()=>{var E,j,q,we,xe,Te;const m=(f==null?void 0:f.id)||((E=r.find(O=>O.status==="connected"))==null?void 0:E.id);if(!h.trim()||!m)return;I("running"),H(""),A(0);const $=await z("POST","/v1/tasks",{task:h.trim(),browser_session_id:m});if(!$||$.status!==201){I("error"),H(((j=$==null?void 0:$.data)==null?void 0:j.error)||"Failed");return}const L=$.data.id,v=Date.now()+3*60*1e3;for(;Date.now()<v;){await new Promise(Xe=>setTimeout(Xe,2e3));const O=await z("GET",`/v1/tasks/${L}`);if(!O)break;if(A(((q=O.data)==null?void 0:q.steps)||0),((we=O.data)==null?void 0:we.status)!=="running"){I(((xe=O.data)==null?void 0:xe.status)||"error"),H(((Te=O.data)==null?void 0:Te.answer)||"No answer."),p();return}}I("error"),H("Timed out after 3 minutes.")},[K,_e]=k(!1),W=`Add browser automation to this project using the Hanzi API. Read the codebase first, then ask me:
|
|
2
|
+
|
|
3
|
+
1. What browser task should Hanzi automate? (e.g. "read patient chart", "fill out a form", "extract data from a web portal")
|
|
4
|
+
2. Where in the UI should the browser pairing flow go? (e.g. settings page, onboarding, a dedicated page)
|
|
5
|
+
3. Where should task results appear? (e.g. inline in the app, a chat interface, a dashboard)
|
|
6
|
+
|
|
7
|
+
Then build the integration using this API reference:
|
|
8
|
+
|
|
9
|
+
## Hanzi API (base URL: https://api.hanzilla.co)
|
|
10
|
+
|
|
11
|
+
Auth: \`Authorization: Bearer ${b||((X=t[0])==null?void 0:X.key_prefix)||"hic_live_..."}\` header on all requests.
|
|
12
|
+
|
|
13
|
+
### Core flow
|
|
14
|
+
1. Create pairing token → show user a link → they connect their browser
|
|
15
|
+
2. Run tasks against their connected browser → poll for results
|
|
16
|
+
3. Show the answer in your app
|
|
17
|
+
|
|
18
|
+
### Endpoints
|
|
19
|
+
|
|
20
|
+
POST /v1/browser-sessions/pair
|
|
21
|
+
Body: {"label": "User Name", "external_user_id": "your_user_id"}
|
|
22
|
+
Returns: {"pairing_token": "hic_pair_...", "expires_in_seconds": 300}
|
|
23
|
+
→ Build link: https://api.hanzilla.co/pair/{pairing_token}
|
|
24
|
+
|
|
25
|
+
GET /v1/browser-sessions
|
|
26
|
+
Returns: {"sessions": [{"id": "...", "status": "connected", "label": "..."}]}
|
|
27
|
+
|
|
28
|
+
POST /v1/tasks
|
|
29
|
+
Body: {"task": "description", "browser_session_id": "...", "url": "optional", "context": "optional"}
|
|
30
|
+
Returns: {"id": "task_id", "status": "running"}
|
|
31
|
+
→ Poll GET /v1/tasks/:id every 2s until status != "running". Typical: 10-60s.
|
|
32
|
+
|
|
33
|
+
GET /v1/tasks/:id
|
|
34
|
+
Returns: {"status": "running|complete|error", "answer": "...", "steps": 4}
|
|
35
|
+
|
|
36
|
+
POST /v1/tasks/:id/cancel
|
|
37
|
+
|
|
38
|
+
GET /v1/tasks/:id/steps
|
|
39
|
+
Returns: {"steps": [{"step": 1, "status": "tool_use", "toolName": "navigate", ...}]}
|
|
40
|
+
|
|
41
|
+
### Key details
|
|
42
|
+
- 20 free tasks/month, then $0.05/completed task. Errors are free.
|
|
43
|
+
- User needs the Hanzi Chrome extension: https://chromewebstore.google.com/detail/iklpkemlmbhemkiojndpbhoakgikpmcd
|
|
44
|
+
- Sample app: https://github.com/hanzili/hanzi-browse/tree/main/examples/partner-quickstart
|
|
45
|
+
|
|
46
|
+
Read the codebase to understand the stack and project structure, then ask me the 3 questions above. After I answer, build the full integration.`;return s("div",null,!_&&s("div",{class:"card"},s("h3",null,"Create your API key"),s("p",{class:"step-explain"},"You need this to call the Hanzi API from your backend."),s("div",{class:"inline-form"},s("input",{value:y,onInput:m=>S(m.target.value),placeholder:"Key name (e.g. dev)",maxLength:100,onKeyDown:m=>m.key==="Enter"&&F()}),s("button",{class:"btn-primary",onClick:F,disabled:!y.trim()},"Create key"))),b&&s("div",{class:"card"},s("h3",null,"Your API key"),s("div",{class:"key-created"},s("div",{class:"mono-with-copy"},s("div",{class:"mono"},b),s(re,{text:b,label:"Copy key"})),s("div",{class:"warning"},"Save this key — it won't be shown again.")),s("p",{class:"step-explain",style:{marginTop:12}},"Verify it works:"),s("div",{class:"mono-with-copy",style:{marginTop:4}},s("div",{class:"mono",style:{fontSize:11}},`curl ${location.origin}/v1/billing/credits -H "Authorization: Bearer ${b}"`),s(re,{text:`curl ${location.origin}/v1/billing/credits -H "Authorization: Bearer ${b}"`,label:"Copy"}))),_&&s("div",{class:"card",style:{background:"#f5f1e8"}},s("h3",null,"Build the integration"),s("p",{class:"step-explain"},"Copy this prompt into Claude Code, Cursor, or any AI coding agent. It has the full API reference and will ask you 3 questions before building."),s("div",{style:{display:"flex",gap:8,marginTop:10}},s("button",{class:"btn-primary",onClick:()=>{navigator.clipboard.writeText(W)},style:{fontSize:13},ref:m=>{m&&(m.onclick=()=>{navigator.clipboard.writeText(W),m.textContent="Copied!",setTimeout(()=>m.textContent="Copy integration prompt",1500)})}},"Copy integration prompt"),s("a",{href:"/docs.html#build-with-hanzi",class:"btn-secondary",style:{textDecoration:"none",padding:"6px 14px",borderRadius:8,fontSize:13}},"Read the docs"))),_&&s(ft,null),_&&s(G,null,s("button",{class:"btn-secondary",style:{marginTop:28,fontSize:13,width:"100%",textAlign:"left",padding:"10px 14px",borderRadius:10},onClick:()=>_e(!K)},K?"▾":"▸"," Test it manually — pair your browser and run a task"),K&&s("div",null,s("p",{class:"section-desc"},"Pair your browser and run a task to see it work."),s("div",{class:"card"},s("div",{class:"step-row"},s("span",{class:`step-badge ${c?"done":"active"}`},c?"✓":"1"),s("div",{class:"step-content"},s("h3",null,c?"Browser connected":"Connect your browser"),s("p",{class:"step-explain"},c?"Your Chrome is paired for testing.":"Pair your own Chrome to test tasks in it."),!c&&a&&s("button",{class:"btn-primary",onClick:J,disabled:i},i?"Connecting...":"Connect this browser"),!c&&!a&&s("p",{class:"step-explain"},s("a",{href:"https://chromewebstore.google.com/detail/hanzi-browse/iklpkemlmbhemkiojndpbhoakgikpmcd",target:"_blank"},"Install the Hanzi extension"),", then reload this page.")))),c&&s("div",{class:"card"},s("div",{class:"step-row"},s("span",{class:`step-badge ${P?"done":"active"}`},P?"✓":"2"),s("div",{class:"step-content"},s("h3",null,"Run a test task"),s("p",{class:"step-explain"},"Tell Hanzi what to do in your connected browser."),C?C==="running"?s("div",{class:"task-running"},s("div",{class:"task-spinner"}),s("span",null,"Running... (",R," step",R!==1?"s":"",")")):s("div",{class:"task-result"},s("div",{class:`task-status-label ${C}`},C==="complete"?"✓ Complete":"✗ "+C,R>0&&` · ${R} steps`),s("div",{class:"task-answer"},M),s("button",{class:"btn-secondary",onClick:()=>{I(null),H("")},style:{marginTop:8}},"Run another")):s("div",{class:"inline-form"},s("input",{value:h,onInput:m=>N(m.target.value),placeholder:"What should Hanzi do?",onKeyDown:m=>m.key==="Enter"&&V()}),s("button",{class:"btn-primary",onClick:V,disabled:!h.trim()},"Run"))))))))}function ft(){const[t,e]=k(null),[n,a]=k(!1);return s(G,null,s("div",{class:"section-label",style:{marginTop:28}},"Pair your users"),s("p",{class:"section-desc"},"Generate a link. Your user clicks it → extension auto-pairs → done."),s("div",{class:"card"},t?s("div",null,s("div",{class:"mono-with-copy"},s("div",{class:"mono",style:{fontSize:12}},t),s(re,{text:t,label:"Copy link"})),s("div",{style:{display:"flex",gap:8,marginTop:8}},s("a",{href:t,target:"_blank",rel:"noreferrer",class:"btn-primary",style:{display:"inline-block",textDecoration:"none",color:"white",padding:"6px 14px",borderRadius:8,fontSize:13}},"Open it"),s("button",{class:"btn-secondary",onClick:()=>e(null),style:{fontSize:12}},"New link")),s("p",{class:"step-explain",style:{marginTop:8}},"Expires in 5 minutes. User needs the ",s("a",{href:"https://chromewebstore.google.com/detail/hanzi-browse/iklpkemlmbhemkiojndpbhoakgikpmcd",target:"_blank"},"Hanzi extension")," installed.")):s("div",null,s("p",{class:"step-explain"},"In production, your backend calls ",s("code",null,"POST /v1/browser-sessions/pair")," to generate these. Try one now:"),s("button",{class:"btn-primary",onClick:async()=>{a(!0);const o=await z("POST","/v1/browser-sessions/pair",{label:"User pairing link"});a(!1),(o==null?void 0:o.status)===201&&e(`${location.origin}/pair/${o.data.pairing_token}`)},disabled:n,style:{marginTop:8}},n?"Generating...":"Generate a test pairing link"))))}function ht({sessions:t,onRefresh:e,usage:n}){const a=t.filter(_=>_.status==="connected"),i=t.filter(_=>_.status==="disconnected"),o=async _=>{await z("DELETE",`/v1/browser-sessions/${_}`),e()},l=async()=>{for(const _ of i)await z("DELETE",`/v1/browser-sessions/${_.id}`);e()},u=_=>_>999999?(_/1e6).toFixed(1)+"M":_>999?(_/1e3).toFixed(1)+"K":String(_||0);return s("div",null,s("div",{class:"summary-bar"},s("span",{class:"summary-stat"},s("strong",null,a.length)," connected"),s("span",{class:"summary-stat"},s("strong",null,i.length)," disconnected"),s("span",{class:"summary-stat"},s("strong",null,(n==null?void 0:n.taskCount)||0)," tasks run")),a.length>0&&s("div",{class:"card"},s("h3",{style:{color:"var(--green)"}},"Connected"),a.map(_=>s(Ue,{key:_.id,session:_}))),i.length>0&&s("div",{class:"card"},s("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center"}},s("h3",{style:{color:"var(--muted)"}},"Disconnected"),s("button",{class:"btn-secondary",onClick:l,style:{fontSize:11,padding:"3px 10px"}},"Remove all")),i.map(_=>s(Ue,{key:_.id,session:_,onRemove:()=>o(_.id)})),s("p",{class:"step-explain",style:{marginTop:6}},"Sessions reconnect automatically when the browser reopens.")),t.length===0&&s("div",{class:"card"},s("p",{class:"step-explain"},"No sessions yet. Go to Getting Started to pair a browser.")),s("div",{class:"card"},s("h3",null,"Usage"),s("div",{class:"usage-grid"},s("div",{class:"usage-stat"},s("div",{class:"num"},(n==null?void 0:n.taskCount)||0),s("div",{class:"label"},"Tasks")),s("div",{class:"usage-stat"},s("div",{class:"num"},u(n==null?void 0:n.totalApiCalls)),s("div",{class:"label"},"API calls")),s("div",{class:"usage-stat"},s("div",{class:"num"},u(((n==null?void 0:n.totalInputTokens)||0)+((n==null?void 0:n.totalOutputTokens)||0))),s("div",{class:"label"},"Tokens")))),s("button",{class:"btn-secondary",onClick:e,style:{marginTop:8,fontSize:12}},"Refresh"))}function Ue({session:t,onRemove:e}){const n=t.label||t.external_user_id||"Unnamed";return s("div",{class:"session-row"},s("span",{class:"session-info"},s("span",{class:`status-dot ${t.status}`}),s("span",{class:"session-label"},n),t.external_user_id&&t.label&&s("span",{class:"session-meta"},t.external_user_id)),s("span",{class:"session-id-group"},s("span",{class:"session-time"},Ve(t.last_heartbeat)),s("code",null,t.id.slice(0,8),"..."),e&&s("button",{class:"btn-danger",onClick:e,style:{padding:"2px 8px",fontSize:11}},"Remove")))}function mt({keys:t,loadKeys:e,setError:n,profile:a,credits:i,loadCredits:o}){const[l,u]=k(""),[_,c]=k(null),f=async()=>{var p;if(!l.trim())return;const d=await z("POST","/v1/api-keys",{name:l.trim()});(d==null?void 0:d.status)===201?(c(d.data.key),u(""),await e()):n(((p=d==null?void 0:d.data)==null?void 0:p.error)||"Failed")},r=async d=>{confirm("Delete this API key?")&&(await z("DELETE",`/v1/api-keys/${d}`),c(null),await e())};return s("div",null,s("div",{class:"card"},s("h3",null,"API Keys"),t.map(d=>s("div",{class:"key-row",key:d.id},s("span",null,s("strong",null,d.name)," ",s("code",{class:"key-prefix"},d.key_prefix),d.last_used_at&&s("span",{class:"session-meta"}," · used ",Ve(d.last_used_at))),s("button",{class:"btn-danger",onClick:()=>r(d.id)},"Delete"))),_&&s("div",{class:"key-created"},s("div",{class:"mono-with-copy"},s("div",{class:"mono"},_),s(re,{text:_,label:"Copy key"})),s("div",{class:"warning"},"Save this key — it won't be shown again.")),s("div",{class:"inline-form",style:{marginTop:8}},s("input",{value:l,onInput:d=>u(d.target.value),placeholder:"Key name",maxLength:100,onKeyDown:d=>d.key==="Enter"&&f()}),s("button",{class:"btn-primary",onClick:f,disabled:!l.trim()},"Create key"))),s("div",{class:"card"},s("h3",null,"Credits & Usage"),i?s("div",null,s("div",{style:{display:"grid",gridTemplateColumns:"1fr 1fr",gap:12,margin:"8px 0 12px"}},s("div",{style:{padding:12,background:"#f5f1e8",borderRadius:8,textAlign:"center"}},s("div",{style:{fontSize:28,fontWeight:700}},i.free_remaining||0),s("div",{style:{fontSize:12,color:"var(--muted)"}},"free tasks left"),s("div",{style:{fontSize:11,color:"var(--muted)"}},"of ",i.free_tasks_per_month,"/month")),s("div",{style:{padding:12,background:"#f5f1e8",borderRadius:8,textAlign:"center"}},s("div",{style:{fontSize:28,fontWeight:700}},i.credit_balance||0),s("div",{style:{fontSize:12,color:"var(--muted)"}},"purchased credits"),s("div",{style:{fontSize:11,color:"var(--muted)"}},"$0.05/task"))),s("p",{class:"step-explain"},"You only pay for completed tasks. Errors and timeouts are free."),s(yt,{loadCredits:o,setError:n})):s("p",{class:"step-explain"},"Loading...")),s("div",{class:"card",style:{background:"#f5f1e8"}},s("h3",null,"Building a product with Hanzi?"),s("p",{class:"step-explain"},"Need volume pricing, custom SLAs, or dedicated support? We offer wholesale rates starting at $0.02/task for partners."),s("a",{href:"mailto:hanzili0217@gmail.com?subject=Partner%20pricing&body=Hi%20Hanzi%20team%2C%0A%0AI%27m%20building%20a%20product%20that%20uses%20browser%20automation.%0A%0AExpected%20volume%3A%20%0AUse%20case%3A%20%0A",class:"btn-primary",style:{display:"inline-block",textDecoration:"none",color:"white",padding:"8px 16px",borderRadius:8,fontSize:13,marginTop:8}},"Contact us for partner pricing")),s("div",{class:"card"},s("h3",null,"Resources"),s("div",{style:{display:"flex",flexDirection:"column",gap:6}},s("a",{href:"/docs.html#build-with-hanzi"},"API Documentation"),s("a",{href:"https://github.com/hanzili/hanzi-browse/tree/main/examples/partner-quickstart",target:"_blank"},"Sample App (GitHub)"),s("a",{href:"https://github.com/hanzili/hanzi-browse/tree/main/sdk",target:"_blank"},"SDK Source"),s("a",{href:"https://discord.gg/hahgu5hcA5",target:"_blank"},"Discord Community"))))}function yt({loadCredits:t,setError:e}){const[n,a]=k(!1),i=async o=>{var u,_;a(!0);const l=await z("POST","/v1/billing/checkout",{credits:o,success_url:location.origin+"/dashboard?checkout=success",cancel_url:location.origin+"/dashboard"});a(!1),(u=l==null?void 0:l.data)!=null&&u.url?window.location.href=l.data.url:e(((_=l==null?void 0:l.data)==null?void 0:_.error)||"Billing not available yet")};return he(()=>{location.search.includes("checkout=success")&&(t(),history.replaceState(null,"","/dashboard"))},[]),s("div",{style:{display:"flex",gap:8,marginTop:8}},s("button",{class:"btn-primary",onClick:()=>i(100),disabled:n,style:{fontSize:13}},"100 credits — $5"),s("button",{class:"btn-secondary",onClick:()=>i(500),disabled:n,style:{fontSize:13}},"500 — $20"),s("button",{class:"btn-secondary",onClick:()=>i(1500),disabled:n,style:{fontSize:13}},"1500 — $50"))}function vt(){return s("div",{class:"page"},s("div",{class:"skeleton skeleton-header"}),s("div",{class:"skeleton skeleton-subtitle"}),s("div",{class:"skeleton skeleton-card"}),s("div",{class:"skeleton skeleton-card"}))}async function gt(){await fetch("/api/auth/sign-out",{method:"POST",credentials:"include"}),window.location.href="https://browse.hanzilla.co"}it(s(dt,null),document.getElementById("app"));
|