kroki 1.0.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 (3) hide show
  1. package/README.md +114 -0
  2. package/bin/kroki.js +390 -0
  3. package/package.json +37 -0
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # Kroki CLI
2
+
3
+ > **Browser Automation & AI Agent Bridge for Google Chrome & Dolphin{anty}**
4
+ > Control live browser tabs, run deterministic workflows, and extract web data from your terminal or any AI Coding Agent (Antigravity, Cursor, Claude Code, Windsurf).
5
+
6
+ [![npm version](https://img.shields.io/npm/v/kroki.svg)](https://www.npmjs.com/package/kroki)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
8
+ [![Node: >=18](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)](https://nodejs.org/)
9
+
10
+ ---
11
+
12
+ ## ⚡ Quick Start
13
+
14
+ No installation required. You can run it directly with `npx`:
15
+
16
+ ### 1. Pair with your browser profile
17
+
18
+ Go to your [Kroki Devices Dashboard](https://kroki.one/devices) and click **"Скопировать код для AI"** next to the active Chrome or Dolphin{anty} profile you want to control.
19
+
20
+ ```bash
21
+ npx kroki pair <YOUR_PAIRING_CODE>
22
+ ```
23
+
24
+ ```text
25
+ Connecting to profile [Dolphin WB-Bot]...
26
+ ✔ Successfully paired with "Dolphin WB-Bot".
27
+ ```
28
+
29
+ ### 2. Send an AI task to the live browser
30
+
31
+ ```bash
32
+ npx kroki "open wildberries.ru, search for 'iPhone 16 cases' and list the top 5 prices"
33
+ ```
34
+
35
+ Kroki's built-in Planner & Navigator subagent will navigate to the page, interact with elements, extract the requested information, and stream the result back to your terminal.
36
+
37
+ ### 3. Run a saved workflow
38
+
39
+ ```bash
40
+ npx kroki run "Export Leads" '{"query": "CTO", "limit": 20}'
41
+ ```
42
+
43
+ ---
44
+
45
+ ## 📦 Global Installation (Optional)
46
+
47
+ If you prefer using `kroki` without typing `npx` every time:
48
+
49
+ ```bash
50
+ npm install -g kroki
51
+ ```
52
+
53
+ Now you can just run:
54
+
55
+ ```bash
56
+ kroki pair <CODE>
57
+ kroki "scrape the current page"
58
+ kroki run "My Workflow"
59
+ ```
60
+
61
+ ---
62
+
63
+ ## 🤖 Using with AI Agents (Antigravity, Claude Code, Cursor)
64
+
65
+ Kroki CLI works out-of-the-box with AI pair-programming agents.
66
+
67
+ ### In Antigravity CLI
68
+
69
+ Whenever you mention **kroki** in the chat, the agent automatically knows how to use this CLI:
70
+
71
+ > **You:** `kroki pair kroki_eyJh...`
72
+ > **Agent:** Pairs with the specified profile and confirms readiness.
73
+ >
74
+ > **You:** `kroki найди на Avito Mac mini M4 и выпиши самые дешевые цены`
75
+ > **Agent:** Executes `kroki "<task>"`, waits for Kroki in the browser to finish, and summarizes the findings.
76
+
77
+ ### In Cursor / Claude Code
78
+
79
+ Add the following to your agent prompt or rule file (`.cursorrules` or `CLAUDE.md`):
80
+
81
+ ```markdown
82
+ When the user requests live browser interaction or running Kroki workflows:
83
+ 1. Ensure the profile is paired: run `npx kroki status`. If not paired, ask the user for their pairing code from https://kroki.one/devices.
84
+ 2. Pair via `npx kroki pair <CODE>`.
85
+ 3. Dispatch tasks via `npx kroki "<task>"` or workflows via `npx kroki run "<workflow>" '[vars]'`.
86
+ ```
87
+
88
+ ---
89
+
90
+ ## 🛠 Commands Reference
91
+
92
+ | Command | Description |
93
+ | :--- | :--- |
94
+ | `npx kroki pair <code_from_devices>` | Connect to a specific browser profile (Chrome or Dolphin{anty}) |
95
+ | `npx kroki "<browser_task>"` | Send an autonomous agent task to the active browser profile |
96
+ | `npx kroki run "<workflow_name>" [json]` | Execute a saved Kroki deterministic workflow |
97
+ | `npx kroki status` | View the currently paired profile and device info |
98
+ | `npx kroki clear` | Clear active pairing session |
99
+ | `npx kroki help` | Show usage options and help |
100
+
101
+ ---
102
+
103
+ ## 🔒 Security Architecture
104
+
105
+ - **Zero Global Credentials**: Pairing codes are session-bound and scoped strictly to your selected browser window (`targetDeviceId`).
106
+ - **Confirmation Gate**: The extension prompts you for confirmation on your computer when a connection attempt is made.
107
+ - **Ultra Plan Scoped**: Cloud control channels require an active Ultra entitlement.
108
+ - **Pure Native Node.js**: Zero third-party npm dependencies. Clean, audited, and lightweight.
109
+
110
+ ---
111
+
112
+ ## 📄 License
113
+
114
+ MIT © [Kroki Team](https://kroki.one)
package/bin/kroki.js ADDED
@@ -0,0 +1,390 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Kroki CLI — Control Kroki browser automation from terminal & AI agents.
5
+ * Zero external dependencies. Uses native Node.js 18+ WebSocket & Fetch.
6
+ *
7
+ * https://kroki.one
8
+ */
9
+
10
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'node:fs';
11
+ import { resolve, join } from 'node:path';
12
+ import { homedir } from 'node:os';
13
+
14
+ const CONFIG_DIR = join(homedir(), '.kroki');
15
+ const SESSION_FILE = join(CONFIG_DIR, 'session.json');
16
+
17
+ const DEFAULT_SUPABASE_URL = 'https://vjkzlvtvnmorokocnbhp.supabase.co';
18
+ const DEFAULT_ANON_KEY =
19
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZqa3psdnR2bm1vcm9rb2NuYmhwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTY4NTI3NTcsImV4cCI6MjA3MjQyODc1N30.NZ4nMq2z8rFQZ2oMlYH0XJX3FrWI2T1j1vmF6vu1-HE';
20
+
21
+ function ensureConfigDir() {
22
+ if (!existsSync(CONFIG_DIR)) {
23
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
24
+ }
25
+ }
26
+
27
+ function decodePairingCode(code) {
28
+ const trimmed = code.trim();
29
+ const raw = trimmed.startsWith('kroki_') ? trimmed.slice(6) : trimmed;
30
+ try {
31
+ const jsonStr = Buffer.from(raw, 'base64url').toString('utf8');
32
+ const parsed = JSON.parse(jsonStr);
33
+ if (!parsed.googleId || !parsed.token || !parsed.deviceId) {
34
+ throw new Error('Missing required fields in pairing code');
35
+ }
36
+ return parsed;
37
+ } catch {
38
+ throw new Error(
39
+ 'Invalid Kroki pairing code.\nPlease copy a valid code from your Kroki Devices panel (https://kroki.one/devices).',
40
+ );
41
+ }
42
+ }
43
+
44
+ function getActiveSession() {
45
+ if (!existsSync(SESSION_FILE)) return null;
46
+ try {
47
+ return JSON.parse(readFileSync(SESSION_FILE, 'utf8'));
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ function saveActiveSession(session) {
54
+ ensureConfigDir();
55
+ writeFileSync(SESSION_FILE, JSON.stringify(session, null, 2), { mode: 0o600 });
56
+ }
57
+
58
+ function clearActiveSession() {
59
+ if (existsSync(SESSION_FILE)) {
60
+ unlinkSync(SESSION_FILE);
61
+ }
62
+ }
63
+
64
+ class RealtimeConnection {
65
+ constructor(config) {
66
+ this.url = config.supabaseUrl || DEFAULT_SUPABASE_URL;
67
+ this.anonKey = config.anonKey || DEFAULT_ANON_KEY;
68
+ this.token = config.token;
69
+ this.googleId = config.googleId;
70
+ this.deviceId = config.deviceId;
71
+ this.topic = `realtime:control:${this.googleId}`;
72
+ this.ref = 0;
73
+ this.ws = null;
74
+ }
75
+
76
+ nextRef() {
77
+ return String(++this.ref);
78
+ }
79
+
80
+ async connect(timeoutMs = 15000) {
81
+ return new Promise((resolve, reject) => {
82
+ const wsUrl = `${this.url.replace(/^http/, 'ws')}/realtime/v1/websocket?apikey=${this.anonKey}&vsn=1.0.0`;
83
+ this.ws = new globalThis.WebSocket(wsUrl);
84
+
85
+ const timer = setTimeout(() => {
86
+ if (this.ws) this.ws.close();
87
+ reject(new Error('Connection to Kroki Realtime timed out. Check your internet connection.'));
88
+ }, timeoutMs);
89
+
90
+ this.ws.onopen = () => {
91
+ clearTimeout(timer);
92
+ this.joinChannel()
93
+ .then(() => resolve(this))
94
+ .catch(reject);
95
+ };
96
+
97
+ this.ws.onerror = err => {
98
+ clearTimeout(timer);
99
+ reject(new Error(`WebSocket connection failed: ${err.message || 'network error'}`));
100
+ };
101
+ });
102
+ }
103
+
104
+ joinChannel() {
105
+ return new Promise((resolve, reject) => {
106
+ const joinRef = this.nextRef();
107
+ const payload = {
108
+ topic: this.topic,
109
+ event: 'phx_join',
110
+ payload: {
111
+ config: { broadcast: { self: false } },
112
+ access_token: this.token,
113
+ },
114
+ ref: joinRef,
115
+ };
116
+
117
+ const handler = event => {
118
+ try {
119
+ const msg = JSON.parse(event.data);
120
+ if (msg.ref === joinRef) {
121
+ this.ws.removeEventListener('message', handler);
122
+ if (msg.payload?.status === 'ok') {
123
+ resolve();
124
+ } else {
125
+ reject(new Error(`Channel join rejected: ${JSON.stringify(msg.payload || {})}`));
126
+ }
127
+ }
128
+ } catch {}
129
+ };
130
+
131
+ this.ws.addEventListener('message', handler);
132
+ this.ws.send(JSON.stringify(payload));
133
+ });
134
+ }
135
+
136
+ sendCommand(command, onEvent, timeoutMs = 240000) {
137
+ return new Promise((resolve, reject) => {
138
+ const msgRef = this.nextRef();
139
+ const timer = setTimeout(() => {
140
+ reject(new Error('Command timed out while waiting for browser response (4 min limit).'));
141
+ }, timeoutMs);
142
+
143
+ const handler = event => {
144
+ try {
145
+ const msg = JSON.parse(event.data);
146
+ if (msg.topic === this.topic && msg.event === 'broadcast') {
147
+ const envelope = msg.payload?.payload;
148
+ if (!envelope || envelope.deviceId !== this.deviceId) return;
149
+
150
+ if (onEvent) onEvent(envelope);
151
+
152
+ if (envelope.kind === 'chat_task_complete' || envelope.kind === 'workflow_complete') {
153
+ clearTimeout(timer);
154
+ this.ws.removeEventListener('message', handler);
155
+ resolve(envelope.payload);
156
+ } else if (envelope.kind === 'error') {
157
+ clearTimeout(timer);
158
+ this.ws.removeEventListener('message', handler);
159
+ reject(new Error(envelope.payload?.message || 'Remote error from Kroki browser'));
160
+ }
161
+ }
162
+ } catch {}
163
+ };
164
+
165
+ this.ws.addEventListener('message', handler);
166
+
167
+ const packet = {
168
+ topic: this.topic,
169
+ event: 'broadcast',
170
+ payload: {
171
+ type: 'broadcast',
172
+ event: 'command',
173
+ payload: command,
174
+ },
175
+ ref: msgRef,
176
+ };
177
+
178
+ this.ws.send(JSON.stringify(packet));
179
+ });
180
+ }
181
+
182
+ close() {
183
+ if (this.ws) {
184
+ try {
185
+ this.ws.close();
186
+ } catch {}
187
+ this.ws = null;
188
+ }
189
+ }
190
+ }
191
+
192
+ async function pairCommand(code) {
193
+ if (!code) {
194
+ console.error('Error: Pairing code required.\nUsage: npx kroki pair <kroki_code>\n');
195
+ console.error('Get your code from: https://kroki.one/devices');
196
+ process.exit(1);
197
+ }
198
+
199
+ const payload = decodePairingCode(code);
200
+ const name = payload.deviceName || payload.deviceId;
201
+ console.log(`\x1b[36mConnecting to profile [${name}]...\x1b[0m`);
202
+
203
+ const conn = new RealtimeConnection(payload);
204
+ await conn.connect();
205
+
206
+ saveActiveSession({
207
+ ...payload,
208
+ pairedAt: Date.now(),
209
+ });
210
+
211
+ conn.close();
212
+
213
+ console.log(`\x1b[32m✔ Successfully paired with "${name}".\x1b[0m`);
214
+ console.log('You can now run tasks without entering the code:');
215
+ console.log(' \x1b[33mnpx kroki "open example.com and extract titles"\x1b[0m');
216
+ console.log(' \x1b[33mnpx kroki run "My Workflow Name"\x1b[0m\n');
217
+ }
218
+
219
+ async function taskCommand(taskText) {
220
+ const session = getActiveSession();
221
+ if (!session) {
222
+ console.error('\x1b[31mError: Not paired.\x1b[0m');
223
+ console.error('First pair with your browser profile:\n \x1b[33mnpx kroki pair <CODE_FROM_DEVICES>\x1b[0m\n');
224
+ console.error('Get your code from: https://kroki.one/devices');
225
+ process.exit(1);
226
+ }
227
+
228
+ const conn = new RealtimeConnection(session);
229
+ await conn.connect();
230
+
231
+ const command = {
232
+ v: 1,
233
+ id: `task-${Date.now()}`,
234
+ kind: 'chat_task',
235
+ targetDeviceId: session.deviceId,
236
+ taskId: `task-${Date.now()}`,
237
+ issuedAt: Date.now(),
238
+ payload: {
239
+ task: taskText,
240
+ },
241
+ };
242
+
243
+ try {
244
+ console.log(`\x1b[36m[Kroki: ${session.deviceName || 'Browser'}]\x1b[0m Running: "${taskText}"...`);
245
+ const result = await conn.sendCommand(command, ev => {
246
+ if (ev.kind === 'execution') {
247
+ const title = ev.payload?.title || ev.payload?.type;
248
+ if (title) console.log(` \x1b[90m→ ${title}\x1b[0m`);
249
+ }
250
+ });
251
+
252
+ console.log('\x1b[32m✔ Task completed.\x1b[0m');
253
+ if (result && typeof result === 'object') {
254
+ console.log(JSON.stringify(result, null, 2));
255
+ }
256
+ } finally {
257
+ conn.close();
258
+ }
259
+ }
260
+
261
+ async function workflowCommand(nameOrId, varsJson) {
262
+ const session = getActiveSession();
263
+ if (!session) {
264
+ console.error('\x1b[31mError: Not paired.\x1b[0m');
265
+ console.error('Pair first with:\n \x1b[33mnpx kroki pair <CODE>\x1b[0m\n');
266
+ process.exit(1);
267
+ }
268
+
269
+ let variables = {};
270
+ if (varsJson) {
271
+ try {
272
+ variables = JSON.parse(varsJson);
273
+ } catch {
274
+ console.error('Warning: Could not parse variables JSON, passing empty object.');
275
+ }
276
+ }
277
+
278
+ const conn = new RealtimeConnection(session);
279
+ await conn.connect();
280
+
281
+ const command = {
282
+ v: 1,
283
+ id: `wf-${Date.now()}`,
284
+ kind: 'workflow_execute',
285
+ targetDeviceId: session.deviceId,
286
+ taskId: `wf-${Date.now()}`,
287
+ issuedAt: Date.now(),
288
+ payload: {
289
+ workflowId: nameOrId,
290
+ variables,
291
+ },
292
+ };
293
+
294
+ try {
295
+ console.log(`\x1b[36m[Kroki: ${session.deviceName || 'Browser'}]\x1b[0m Executing workflow "${nameOrId}"...`);
296
+ const result = await conn.sendCommand(command, ev => {
297
+ if (ev.kind === 'workflow_step_start') {
298
+ console.log(` \x1b[90m[step] ${ev.payload?.stepType || 'action'}: ${ev.payload?.comment || ''}\x1b[0m`);
299
+ }
300
+ });
301
+
302
+ console.log('\x1b[32m✔ Workflow finished.\x1b[0m');
303
+ if (result) console.log(JSON.stringify(result, null, 2));
304
+ } finally {
305
+ conn.close();
306
+ }
307
+ }
308
+
309
+ function statusCommand() {
310
+ const session = getActiveSession();
311
+ if (!session) {
312
+ console.log('\x1b[33mStatus: Not paired with any browser profile.\x1b[0m');
313
+ console.log('Pair with:\n npx kroki pair <CODE_FROM_DEVICES>');
314
+ } else {
315
+ console.log('\x1b[32mStatus: Paired and Active\x1b[0m');
316
+ console.log(`Profile: ${session.deviceName || session.deviceId}`);
317
+ console.log(`Device ID: ${session.deviceId}`);
318
+ console.log(`Platform: ${session.platform || 'unknown'}`);
319
+ console.log(`Paired At: ${new Date(session.pairedAt).toLocaleString()}`);
320
+ }
321
+ }
322
+
323
+ function helpCommand() {
324
+ console.log(`
325
+ \x1b[1mKroki CLI\x1b[0m — Control Kroki browser automation from terminal & AI agents
326
+
327
+ \x1b[1mUsage:\x1b[0m
328
+ npx kroki pair <code_from_devices> Pair with a browser profile (Chrome / Dolphin{anty})
329
+ npx kroki "<browser_task>" Send an AI task to the paired browser
330
+ npx kroki run "<workflow_name>" [json] Execute a deterministic workflow by name
331
+ npx kroki status Show currently paired profile
332
+ npx kroki clear Clear active pairing session
333
+ npx kroki help Show this help message
334
+
335
+ \x1b[1mExamples:\x1b[0m
336
+ npx kroki pair kroki_eyJh...
337
+ npx kroki "find top 5 mechanical keyboards on Amazon and return prices"
338
+ npx kroki run "Export Leads" '{"limit": 50}'
339
+
340
+ \x1b[1mDocs & Dashboard:\x1b[0m
341
+ https://kroki.one/devices
342
+ `);
343
+ }
344
+
345
+ // ── Entry Point ─────────────────────────────────────────────────────────────
346
+
347
+ const rawArgs = process.argv.slice(2);
348
+ const first = rawArgs[0] || 'help';
349
+
350
+ switch (first) {
351
+ case 'pair':
352
+ case 'connect':
353
+ pairCommand(rawArgs[1]).catch(e => {
354
+ console.error(`\x1b[31mError:\x1b[0m ${e.message}`);
355
+ process.exit(1);
356
+ });
357
+ break;
358
+
359
+ case 'run':
360
+ case 'workflow':
361
+ workflowCommand(rawArgs[1], rawArgs[2]).catch(e => {
362
+ console.error(`\x1b[31mError:\x1b[0m ${e.message}`);
363
+ process.exit(1);
364
+ });
365
+ break;
366
+
367
+ case 'status':
368
+ statusCommand();
369
+ break;
370
+
371
+ case 'clear':
372
+ case 'disconnect':
373
+ clearActiveSession();
374
+ console.log('\x1b[32m✔ Session cleared.\x1b[0m');
375
+ break;
376
+
377
+ case 'help':
378
+ case '--help':
379
+ case '-h':
380
+ helpCommand();
381
+ break;
382
+
383
+ default:
384
+ // If user ran `npx kroki "do something..."` directly
385
+ taskCommand(rawArgs.join(' ')).catch(e => {
386
+ console.error(`\x1b[31mError:\x1b[0m ${e.message}`);
387
+ process.exit(1);
388
+ });
389
+ break;
390
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "kroki",
3
+ "version": "1.0.0",
4
+ "description": "Kroki Browser Automation CLI — Control Chrome & Dolphin{anty} from terminal and AI agents",
5
+ "keywords": [
6
+ "kroki",
7
+ "browser-automation",
8
+ "ai-agent",
9
+ "antigravity",
10
+ "dolphin-anty",
11
+ "chrome",
12
+ "mcp",
13
+ "cli"
14
+ ],
15
+ "author": "Kroki Team",
16
+ "license": "MIT",
17
+ "type": "module",
18
+ "bin": {
19
+ "kroki": "./bin/kroki.js"
20
+ },
21
+ "files": [
22
+ "bin",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "engines": {
27
+ "node": ">=18.0.0"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/one-focus/kroki-cli.git"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/one-focus/kroki-cli/issues"
35
+ },
36
+ "homepage": "https://kroki.one"
37
+ }