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.
Files changed (78) hide show
  1. package/README.md +182 -0
  2. package/dist/agent/loop.d.ts +63 -0
  3. package/dist/agent/loop.js +186 -0
  4. package/dist/agent/system-prompt.d.ts +7 -0
  5. package/dist/agent/system-prompt.js +41 -0
  6. package/dist/agent/tools.d.ts +9 -0
  7. package/dist/agent/tools.js +154 -0
  8. package/dist/cli/detect-credentials.d.ts +31 -0
  9. package/dist/cli/detect-credentials.js +44 -0
  10. package/dist/cli/import-credentials-handler.d.ts +14 -0
  11. package/dist/cli/import-credentials-handler.js +22 -0
  12. package/dist/cli/session-files.d.ts +28 -0
  13. package/dist/cli/session-files.js +118 -0
  14. package/dist/cli/setup.d.ts +10 -0
  15. package/dist/cli/setup.js +915 -0
  16. package/dist/cli.d.ts +16 -0
  17. package/dist/cli.js +506 -0
  18. package/dist/dashboard/assets/index-CEFyesbT.js +46 -0
  19. package/dist/dashboard/assets/index-Dnht2kLU.css +1 -0
  20. package/dist/dashboard/index.html +13 -0
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.js +1116 -0
  23. package/dist/ipc/index.d.ts +8 -0
  24. package/dist/ipc/index.js +8 -0
  25. package/dist/ipc/native-host.d.ts +96 -0
  26. package/dist/ipc/native-host.js +223 -0
  27. package/dist/ipc/websocket-client.d.ts +73 -0
  28. package/dist/ipc/websocket-client.js +199 -0
  29. package/dist/license/manager.d.ts +20 -0
  30. package/dist/license/manager.js +15 -0
  31. package/dist/llm/client.d.ts +72 -0
  32. package/dist/llm/client.js +227 -0
  33. package/dist/llm/credentials.d.ts +61 -0
  34. package/dist/llm/credentials.js +200 -0
  35. package/dist/llm/vertex.d.ts +22 -0
  36. package/dist/llm/vertex.js +335 -0
  37. package/dist/managed/api-http.test.d.ts +7 -0
  38. package/dist/managed/api-http.test.js +623 -0
  39. package/dist/managed/api.d.ts +51 -0
  40. package/dist/managed/api.js +1448 -0
  41. package/dist/managed/api.test.d.ts +10 -0
  42. package/dist/managed/api.test.js +146 -0
  43. package/dist/managed/auth.d.ts +38 -0
  44. package/dist/managed/auth.js +192 -0
  45. package/dist/managed/billing.d.ts +70 -0
  46. package/dist/managed/billing.js +227 -0
  47. package/dist/managed/deploy.d.ts +17 -0
  48. package/dist/managed/deploy.js +385 -0
  49. package/dist/managed/e2e.test.d.ts +15 -0
  50. package/dist/managed/e2e.test.js +151 -0
  51. package/dist/managed/hardening.test.d.ts +14 -0
  52. package/dist/managed/hardening.test.js +346 -0
  53. package/dist/managed/integration.test.d.ts +8 -0
  54. package/dist/managed/integration.test.js +274 -0
  55. package/dist/managed/log.d.ts +18 -0
  56. package/dist/managed/log.js +31 -0
  57. package/dist/managed/server.d.ts +12 -0
  58. package/dist/managed/server.js +69 -0
  59. package/dist/managed/store-pg.d.ts +191 -0
  60. package/dist/managed/store-pg.js +479 -0
  61. package/dist/managed/store.d.ts +188 -0
  62. package/dist/managed/store.js +379 -0
  63. package/dist/relay/auto-start.d.ts +19 -0
  64. package/dist/relay/auto-start.js +71 -0
  65. package/dist/relay/server.d.ts +17 -0
  66. package/dist/relay/server.js +403 -0
  67. package/dist/types/index.d.ts +5 -0
  68. package/dist/types/index.js +4 -0
  69. package/dist/types/session.d.ts +134 -0
  70. package/dist/types/session.js +16 -0
  71. package/package.json +61 -0
  72. package/skills/README.md +48 -0
  73. package/skills/a11y-auditor/SKILL.md +42 -0
  74. package/skills/e2e-tester/SKILL.md +154 -0
  75. package/skills/hanzi-browse/SKILL.md +182 -0
  76. package/skills/linkedin-prospector/SKILL.md +149 -0
  77. package/skills/social-poster/SKILL.md +146 -0
  78. 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"));