kroki 1.0.5 → 1.0.7
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/bin/kroki.js +1311 -185
- package/package.json +1 -1
package/bin/kroki.js
CHANGED
|
@@ -10,8 +10,12 @@
|
|
|
10
10
|
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'node:fs';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { homedir } from 'node:os';
|
|
13
|
+
import { createServer } from 'node:http';
|
|
14
|
+
import readline, { createInterface } from 'node:readline';
|
|
15
|
+
import { exec } from 'node:child_process';
|
|
13
16
|
|
|
14
17
|
const CONFIG_DIR = join(homedir(), '.kroki');
|
|
18
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
15
19
|
const SESSION_FILE = join(CONFIG_DIR, 'session.json');
|
|
16
20
|
|
|
17
21
|
const DEFAULT_SUPABASE_URL = 'https://vjkzlvtvnmorokocnbhp.supabase.co';
|
|
@@ -24,43 +28,336 @@ function ensureConfigDir() {
|
|
|
24
28
|
}
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
function openBrowser(url) {
|
|
32
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
33
|
+
exec(`${cmd} "${url}"`, () => {});
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
function decodePairingCode(code) {
|
|
28
|
-
|
|
37
|
+
if (typeof code === 'object' && code !== null) {
|
|
38
|
+
if (code.token) {
|
|
39
|
+
if (!code.googleId) {
|
|
40
|
+
try {
|
|
41
|
+
const parts = code.token.split('.');
|
|
42
|
+
if (parts.length >= 2) {
|
|
43
|
+
const jwtPayload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
44
|
+
code.googleId = jwtPayload.google_id || jwtPayload.sub;
|
|
45
|
+
}
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
return code;
|
|
49
|
+
}
|
|
50
|
+
throw new Error('Missing token in authorization payload');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const trimmed = String(code).trim();
|
|
29
54
|
const raw = trimmed.startsWith('kroki_') ? trimmed.slice(6) : trimmed;
|
|
30
55
|
try {
|
|
31
56
|
const jsonStr = Buffer.from(raw, 'base64url').toString('utf8');
|
|
32
57
|
const parsed = JSON.parse(jsonStr);
|
|
33
|
-
if (!parsed.
|
|
34
|
-
throw new Error('Missing
|
|
58
|
+
if (!parsed.token) {
|
|
59
|
+
throw new Error('Missing token in code');
|
|
60
|
+
}
|
|
61
|
+
if (!parsed.googleId) {
|
|
62
|
+
try {
|
|
63
|
+
const parts = parsed.token.split('.');
|
|
64
|
+
if (parts.length >= 2) {
|
|
65
|
+
const jwtPayload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
66
|
+
parsed.googleId = jwtPayload.google_id || jwtPayload.sub;
|
|
67
|
+
}
|
|
68
|
+
} catch {}
|
|
35
69
|
}
|
|
36
70
|
return parsed;
|
|
37
71
|
} catch {
|
|
38
72
|
throw new Error(
|
|
39
|
-
'Invalid Kroki
|
|
73
|
+
'Invalid Kroki code.\nPlease get a valid code from your Kroki Devices panel: https://kroki.ai/#/devices',
|
|
40
74
|
);
|
|
41
75
|
}
|
|
42
76
|
}
|
|
43
77
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
78
|
+
// ── Config & Session Management ─────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
function getConfig() {
|
|
81
|
+
if (existsSync(CONFIG_FILE)) {
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
|
|
84
|
+
} catch {}
|
|
85
|
+
}
|
|
86
|
+
// Fallback / migration from legacy session.json
|
|
87
|
+
if (existsSync(SESSION_FILE)) {
|
|
88
|
+
try {
|
|
89
|
+
const legacy = JSON.parse(readFileSync(SESSION_FILE, 'utf8'));
|
|
90
|
+
if (legacy && legacy.token) {
|
|
91
|
+
const config = {
|
|
92
|
+
account: {
|
|
93
|
+
googleId: legacy.googleId,
|
|
94
|
+
token: legacy.token,
|
|
95
|
+
supabaseUrl: legacy.supabaseUrl || DEFAULT_SUPABASE_URL,
|
|
96
|
+
anonKey: legacy.anonKey || DEFAULT_ANON_KEY,
|
|
97
|
+
},
|
|
98
|
+
selectedDeviceIds: legacy.deviceId ? [legacy.deviceId] : [],
|
|
99
|
+
defaultDeviceId: legacy.deviceId || null,
|
|
100
|
+
devices: {},
|
|
101
|
+
};
|
|
102
|
+
if (legacy.deviceId) {
|
|
103
|
+
config.devices[legacy.deviceId] = {
|
|
104
|
+
deviceId: legacy.deviceId,
|
|
105
|
+
deviceName: legacy.deviceName || legacy.deviceId,
|
|
106
|
+
platform: legacy.platform || 'mac',
|
|
107
|
+
lastSeenAt: legacy.pairedAt || Date.now(),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
saveConfig(config);
|
|
111
|
+
return config;
|
|
112
|
+
}
|
|
113
|
+
} catch {}
|
|
50
114
|
}
|
|
115
|
+
return null;
|
|
51
116
|
}
|
|
52
117
|
|
|
53
|
-
function
|
|
118
|
+
function saveConfig(config) {
|
|
54
119
|
ensureConfigDir();
|
|
55
|
-
writeFileSync(
|
|
120
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
121
|
+
// Also keep session.json in sync for legacy compatibility
|
|
122
|
+
if (config.account && config.account.token) {
|
|
123
|
+
const activeDevice = (config.devices && config.defaultDeviceId && config.devices[config.defaultDeviceId]) || null;
|
|
124
|
+
const legacySession = {
|
|
125
|
+
v: 1,
|
|
126
|
+
token: config.account.token,
|
|
127
|
+
googleId: config.account.googleId,
|
|
128
|
+
deviceId: activeDevice?.deviceId || '',
|
|
129
|
+
deviceName: activeDevice?.deviceName || '',
|
|
130
|
+
platform: activeDevice?.platform || 'mac',
|
|
131
|
+
supabaseUrl: config.account.supabaseUrl || DEFAULT_SUPABASE_URL,
|
|
132
|
+
pairedAt: Date.now(),
|
|
133
|
+
};
|
|
134
|
+
writeFileSync(SESSION_FILE, JSON.stringify(legacySession, null, 2), { mode: 0o600 });
|
|
135
|
+
}
|
|
56
136
|
}
|
|
57
137
|
|
|
58
|
-
function
|
|
59
|
-
if (existsSync(
|
|
60
|
-
|
|
138
|
+
function clearAllSessions() {
|
|
139
|
+
if (existsSync(CONFIG_FILE)) unlinkSync(CONFIG_FILE);
|
|
140
|
+
if (existsSync(SESSION_FILE)) unlinkSync(SESSION_FILE);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function saveFromCode(code) {
|
|
144
|
+
const payload = decodePairingCode(code);
|
|
145
|
+
const config = getConfig() || {
|
|
146
|
+
account: {},
|
|
147
|
+
selectedDeviceIds: [],
|
|
148
|
+
defaultDeviceId: null,
|
|
149
|
+
devices: {},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
config.account = {
|
|
153
|
+
googleId: payload.googleId,
|
|
154
|
+
email: payload.email || config.account?.email,
|
|
155
|
+
token: payload.token,
|
|
156
|
+
supabaseUrl: payload.supabaseUrl || DEFAULT_SUPABASE_URL,
|
|
157
|
+
anonKey: payload.anonKey || DEFAULT_ANON_KEY,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
config.devices = config.devices || {};
|
|
161
|
+
config.selectedDeviceIds = config.selectedDeviceIds || [];
|
|
162
|
+
|
|
163
|
+
if (Array.isArray(payload.accounts)) {
|
|
164
|
+
for (const acc of payload.accounts) {
|
|
165
|
+
if (Array.isArray(acc.devices)) {
|
|
166
|
+
for (const d of acc.devices) {
|
|
167
|
+
const resolvedEmail =
|
|
168
|
+
acc.email ||
|
|
169
|
+
(acc.google_id === payload.googleId ? payload.email : null) ||
|
|
170
|
+
config.account?.email ||
|
|
171
|
+
acc.label ||
|
|
172
|
+
acc.google_id;
|
|
173
|
+
config.devices[d.device_id] = {
|
|
174
|
+
deviceId: d.device_id,
|
|
175
|
+
deviceName: d.device_name || d.device_id,
|
|
176
|
+
platform: d.platform || 'mac',
|
|
177
|
+
account: resolvedEmail,
|
|
178
|
+
googleId: acc.google_id,
|
|
179
|
+
lastSeenAt: d.last_seen_at ? Date.parse(d.last_seen_at) : Date.now(),
|
|
180
|
+
};
|
|
181
|
+
if (!config.defaultDeviceId) {
|
|
182
|
+
config.defaultDeviceId = d.device_id;
|
|
183
|
+
}
|
|
184
|
+
if (!config.selectedDeviceIds.includes(d.device_id)) {
|
|
185
|
+
config.selectedDeviceIds.push(d.device_id);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (payload.deviceId) {
|
|
193
|
+
config.devices[payload.deviceId] = {
|
|
194
|
+
deviceId: payload.deviceId,
|
|
195
|
+
deviceName: payload.deviceName || payload.deviceId,
|
|
196
|
+
platform: payload.platform || 'mac',
|
|
197
|
+
lastSeenAt: Date.now(),
|
|
198
|
+
};
|
|
199
|
+
if (!config.selectedDeviceIds.includes(payload.deviceId)) {
|
|
200
|
+
config.selectedDeviceIds.push(payload.deviceId);
|
|
201
|
+
}
|
|
202
|
+
config.defaultDeviceId = payload.deviceId;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
saveConfig(config);
|
|
206
|
+
return config;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function resolveTargetDevice(config, targetQuery) {
|
|
210
|
+
const devices = config.devices || {};
|
|
211
|
+
const allDevices = Object.values(devices);
|
|
212
|
+
|
|
213
|
+
const query =
|
|
214
|
+
targetQuery !== null && targetQuery !== undefined && String(targetQuery).trim() !== ''
|
|
215
|
+
? targetQuery
|
|
216
|
+
: process.env.KROKI_DEVICE && String(process.env.KROKI_DEVICE).trim() !== ''
|
|
217
|
+
? process.env.KROKI_DEVICE
|
|
218
|
+
: null;
|
|
219
|
+
|
|
220
|
+
if (query !== null && query !== undefined && String(query).trim() !== '') {
|
|
221
|
+
const q = String(query).toLowerCase().trim();
|
|
222
|
+
// 1. Index number (1-based, e.g. "1", "2")
|
|
223
|
+
const num = parseInt(q, 10);
|
|
224
|
+
if (!isNaN(num) && String(num) === q && num >= 1 && num <= allDevices.length) {
|
|
225
|
+
return allDevices[num - 1];
|
|
226
|
+
}
|
|
227
|
+
// 2. Exact ID
|
|
228
|
+
let match = allDevices.find(d => d.deviceId.toLowerCase() === q);
|
|
229
|
+
// 3. Exact name
|
|
230
|
+
if (!match) match = allDevices.find(d => (d.deviceName || '').toLowerCase() === q);
|
|
231
|
+
// 4. Exact account / email
|
|
232
|
+
if (!match) match = allDevices.find(d => (d.account || '').toLowerCase() === q);
|
|
233
|
+
// 5. Substring name or account
|
|
234
|
+
if (!match) {
|
|
235
|
+
match = allDevices.find(
|
|
236
|
+
d => (d.deviceName || '').toLowerCase().includes(q) || (d.account || '').toLowerCase().includes(q),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
if (!match) {
|
|
240
|
+
const names = allDevices
|
|
241
|
+
.map((d, i) => `[${i + 1}] "${d.deviceName || d.deviceId}"${d.account ? ` (${d.account})` : ''}`)
|
|
242
|
+
.join('\n ');
|
|
243
|
+
throw new Error(`Device "${targetQuery}" not found.\nAvailable browsers:\n ${names || 'none'}`);
|
|
244
|
+
}
|
|
245
|
+
return match;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Default: defaultDeviceId or first device
|
|
249
|
+
const defId = config.defaultDeviceId;
|
|
250
|
+
if (defId && devices[defId]) {
|
|
251
|
+
return devices[defId];
|
|
252
|
+
}
|
|
253
|
+
if (allDevices.length > 0) {
|
|
254
|
+
return allDevices[0];
|
|
61
255
|
}
|
|
256
|
+
return null;
|
|
62
257
|
}
|
|
63
258
|
|
|
259
|
+
function isTokenExpired(token) {
|
|
260
|
+
if (!token) return true;
|
|
261
|
+
try {
|
|
262
|
+
const parts = token.split('.');
|
|
263
|
+
if (parts.length >= 2) {
|
|
264
|
+
const jwt = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
265
|
+
if (jwt.exp) {
|
|
266
|
+
return Date.now() / 1000 >= jwt.exp;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
} catch {}
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function fetchOnlineDeviceIds(config, timeoutMs = 1500) {
|
|
274
|
+
if (!config?.account?.token || !config?.account?.supabaseUrl) {
|
|
275
|
+
return { onlineIds: new Set(), expired: false };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (isTokenExpired(config.account.token)) {
|
|
279
|
+
return { onlineIds: new Set(), expired: true };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const onlineIds = new Set();
|
|
283
|
+
const googleIds = new Set();
|
|
284
|
+
if (config.account.googleId) googleIds.add(config.account.googleId);
|
|
285
|
+
for (const d of Object.values(config.devices || {})) {
|
|
286
|
+
if (d.googleId) googleIds.add(d.googleId);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const anonKey = config.account.anonKey || DEFAULT_ANON_KEY;
|
|
290
|
+
const wsBase = (config.account.supabaseUrl || DEFAULT_SUPABASE_URL).replace(/^http/, 'ws');
|
|
291
|
+
const wsUrl = `${wsBase}/realtime/v1/websocket?apikey=${anonKey}&vsn=1.0.0`;
|
|
292
|
+
|
|
293
|
+
await Promise.all(
|
|
294
|
+
Array.from(googleIds).map(gId => {
|
|
295
|
+
return new Promise(resolve => {
|
|
296
|
+
let ws = null;
|
|
297
|
+
let settled = false;
|
|
298
|
+
const done = () => {
|
|
299
|
+
if (!settled) {
|
|
300
|
+
settled = true;
|
|
301
|
+
if (ws) {
|
|
302
|
+
try {
|
|
303
|
+
ws.close();
|
|
304
|
+
} catch {}
|
|
305
|
+
}
|
|
306
|
+
resolve();
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const timer = setTimeout(done, timeoutMs);
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
ws = new globalThis.WebSocket(wsUrl);
|
|
314
|
+
ws.onopen = () => {
|
|
315
|
+
const payload = {
|
|
316
|
+
topic: `realtime:control:${gId}`,
|
|
317
|
+
event: 'phx_join',
|
|
318
|
+
payload: {
|
|
319
|
+
config: { private: true, broadcast: { self: false }, presence: { key: '' } },
|
|
320
|
+
access_token: config.account.token,
|
|
321
|
+
},
|
|
322
|
+
ref: 'ping_' + gId,
|
|
323
|
+
};
|
|
324
|
+
ws.send(JSON.stringify(payload));
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
ws.onmessage = event => {
|
|
328
|
+
try {
|
|
329
|
+
const msg = JSON.parse(event.data);
|
|
330
|
+
if (msg.event === 'presence_state' && msg.payload) {
|
|
331
|
+
for (const [k, v] of Object.entries(msg.payload)) {
|
|
332
|
+
if (k && k.startsWith('device-')) onlineIds.add(k);
|
|
333
|
+
const metas = Array.isArray(v) ? v : Array.isArray(v?.metas) ? v.metas : [];
|
|
334
|
+
for (const m of metas) {
|
|
335
|
+
if (m && m.deviceId) onlineIds.add(m.deviceId);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
clearTimeout(timer);
|
|
339
|
+
done();
|
|
340
|
+
}
|
|
341
|
+
} catch {}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
ws.onerror = () => {
|
|
345
|
+
clearTimeout(timer);
|
|
346
|
+
done();
|
|
347
|
+
};
|
|
348
|
+
} catch {
|
|
349
|
+
clearTimeout(timer);
|
|
350
|
+
done();
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}),
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
return { onlineIds, expired: false };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ── Realtime WebSocket Connection ───────────────────────────────────────────
|
|
360
|
+
|
|
64
361
|
class RealtimeConnection {
|
|
65
362
|
constructor(config) {
|
|
66
363
|
this.url = config.supabaseUrl || DEFAULT_SUPABASE_URL;
|
|
@@ -73,12 +370,17 @@ class RealtimeConnection {
|
|
|
73
370
|
this.joinRef = null;
|
|
74
371
|
this.ws = null;
|
|
75
372
|
this.heartbeatTimer = null;
|
|
373
|
+
this.onlineDeviceIds = new Set();
|
|
76
374
|
}
|
|
77
375
|
|
|
78
376
|
nextRef() {
|
|
79
377
|
return String(++this.ref);
|
|
80
378
|
}
|
|
81
379
|
|
|
380
|
+
isDeviceOnline(deviceId) {
|
|
381
|
+
return this.onlineDeviceIds.has(deviceId);
|
|
382
|
+
}
|
|
383
|
+
|
|
82
384
|
async connect(timeoutMs = 15000) {
|
|
83
385
|
return new Promise((resolve, reject) => {
|
|
84
386
|
const wsUrl = `${this.url.replace(/^http/, 'ws')}/realtime/v1/websocket?apikey=${this.anonKey}&vsn=1.0.0`;
|
|
@@ -100,7 +402,7 @@ class RealtimeConnection {
|
|
|
100
402
|
);
|
|
101
403
|
}
|
|
102
404
|
}, 25000);
|
|
103
|
-
resolve(this);
|
|
405
|
+
setTimeout(() => resolve(this), 250);
|
|
104
406
|
})
|
|
105
407
|
.catch(reject);
|
|
106
408
|
};
|
|
@@ -119,12 +421,47 @@ class RealtimeConnection {
|
|
|
119
421
|
topic: this.topic,
|
|
120
422
|
event: 'phx_join',
|
|
121
423
|
payload: {
|
|
122
|
-
config: { private: true, broadcast: { self: false } },
|
|
424
|
+
config: { private: true, broadcast: { self: false }, presence: { key: '' } },
|
|
123
425
|
access_token: this.token,
|
|
124
426
|
},
|
|
125
427
|
ref: this.joinRef,
|
|
126
428
|
};
|
|
127
429
|
|
|
430
|
+
const presenceHandler = event => {
|
|
431
|
+
try {
|
|
432
|
+
const msg = JSON.parse(event.data);
|
|
433
|
+
if (msg.event === 'presence_state' && msg.payload) {
|
|
434
|
+
for (const [k, v] of Object.entries(msg.payload)) {
|
|
435
|
+
if (k && k.startsWith('device-')) this.onlineDeviceIds.add(k);
|
|
436
|
+
const metas = Array.isArray(v) ? v : Array.isArray(v?.metas) ? v.metas : [];
|
|
437
|
+
for (const m of metas) {
|
|
438
|
+
if (m && m.deviceId) this.onlineDeviceIds.add(m.deviceId);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
} else if (msg.event === 'presence_diff' && msg.payload) {
|
|
442
|
+
if (msg.payload.joins) {
|
|
443
|
+
for (const [k, v] of Object.entries(msg.payload.joins)) {
|
|
444
|
+
if (k && k.startsWith('device-')) this.onlineDeviceIds.add(k);
|
|
445
|
+
const metas = Array.isArray(v) ? v : Array.isArray(v?.metas) ? v.metas : [];
|
|
446
|
+
for (const m of metas) {
|
|
447
|
+
if (m && m.deviceId) this.onlineDeviceIds.add(m.deviceId);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (msg.payload.leaves) {
|
|
452
|
+
for (const [k, v] of Object.entries(msg.payload.leaves)) {
|
|
453
|
+
if (k && k.startsWith('device-')) this.onlineDeviceIds.delete(k);
|
|
454
|
+
const metas = Array.isArray(v) ? v : Array.isArray(v?.metas) ? v.metas : [];
|
|
455
|
+
for (const m of metas) {
|
|
456
|
+
if (m && m.deviceId) this.onlineDeviceIds.delete(m.deviceId);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
} catch {}
|
|
462
|
+
};
|
|
463
|
+
this.ws.addEventListener('message', presenceHandler);
|
|
464
|
+
|
|
128
465
|
const handler = event => {
|
|
129
466
|
try {
|
|
130
467
|
const msg = JSON.parse(event.data);
|
|
@@ -133,7 +470,12 @@ class RealtimeConnection {
|
|
|
133
470
|
if (msg.payload?.status === 'ok') {
|
|
134
471
|
resolve();
|
|
135
472
|
} else {
|
|
136
|
-
|
|
473
|
+
const reason = msg.payload?.response?.reason || JSON.stringify(msg.payload || {});
|
|
474
|
+
if (typeof reason === 'string' && reason.includes('Token has expired')) {
|
|
475
|
+
reject(new Error('Session token has expired. Run "kroki login" to reconnect.'));
|
|
476
|
+
} else {
|
|
477
|
+
reject(new Error(`Channel join rejected: ${reason}`));
|
|
478
|
+
}
|
|
137
479
|
}
|
|
138
480
|
}
|
|
139
481
|
} catch {}
|
|
@@ -144,38 +486,70 @@ class RealtimeConnection {
|
|
|
144
486
|
});
|
|
145
487
|
}
|
|
146
488
|
|
|
147
|
-
sendCommand(command, onEvent, timeoutMs =
|
|
489
|
+
async sendCommand(command, onEvent, timeoutMs = 600000) {
|
|
148
490
|
return new Promise((resolve, reject) => {
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
491
|
+
let activityTimeout = null;
|
|
492
|
+
const resetActivityTimeout = () => {
|
|
493
|
+
if (activityTimeout) clearTimeout(activityTimeout);
|
|
494
|
+
// 2-minute silence limit between steps
|
|
495
|
+
activityTimeout = setTimeout(() => {
|
|
496
|
+
cleanup();
|
|
497
|
+
reject(new Error('Task timed out: no activity received for 2 minutes'));
|
|
498
|
+
}, 120000);
|
|
499
|
+
};
|
|
500
|
+
resetActivityTimeout();
|
|
501
|
+
|
|
502
|
+
const totalTimeout = setTimeout(() => {
|
|
503
|
+
cleanup();
|
|
504
|
+
reject(new Error(`Task reached maximum time limit (${Math.round(timeoutMs / 60000)}m)`));
|
|
152
505
|
}, timeoutMs);
|
|
153
506
|
|
|
507
|
+
const cleanup = () => {
|
|
508
|
+
if (activityTimeout) clearTimeout(activityTimeout);
|
|
509
|
+
clearTimeout(totalTimeout);
|
|
510
|
+
this.ws.removeEventListener('message', handler);
|
|
511
|
+
};
|
|
512
|
+
|
|
154
513
|
const handler = event => {
|
|
155
514
|
try {
|
|
156
515
|
const msg = JSON.parse(event.data);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
516
|
+
let envelope = null;
|
|
517
|
+
if (msg.event === 'broadcast' && msg.payload) {
|
|
518
|
+
envelope = msg.payload.payload || msg.payload;
|
|
519
|
+
} else if (msg.event === 'event' && msg.payload) {
|
|
520
|
+
envelope = msg.payload;
|
|
521
|
+
} else if (msg.payload && (msg.payload.kind || msg.payload.taskId)) {
|
|
522
|
+
envelope = msg.payload;
|
|
523
|
+
}
|
|
160
524
|
|
|
525
|
+
if (envelope && (envelope.taskId === command.taskId || !envelope.taskId)) {
|
|
526
|
+
resetActivityTimeout();
|
|
161
527
|
if (onEvent) onEvent(envelope);
|
|
162
528
|
|
|
163
529
|
if (
|
|
530
|
+
envelope.kind === 'task_complete' ||
|
|
164
531
|
envelope.kind === 'chat_task_complete' ||
|
|
165
532
|
envelope.kind === 'workflow_complete' ||
|
|
166
|
-
envelope.
|
|
533
|
+
envelope.payload?.state === 'task.ok' ||
|
|
534
|
+
envelope.payload?.state === 'task.cancel'
|
|
167
535
|
) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
536
|
+
cleanup();
|
|
537
|
+
if (envelope.kind === 'workflow_complete' && envelope.payload?.success === false) {
|
|
538
|
+
const errMsg =
|
|
539
|
+
envelope.payload?.error ||
|
|
540
|
+
(envelope.payload?.cancelled ? 'Workflow was cancelled' : 'Workflow execution failed');
|
|
541
|
+
const err = new Error(errMsg);
|
|
542
|
+
err.payload = envelope.payload;
|
|
543
|
+
reject(err);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
resolve(envelope.payload || { success: true });
|
|
547
|
+
} else if (envelope.kind === 'error' || envelope.payload?.state === 'task.fail') {
|
|
548
|
+
cleanup();
|
|
549
|
+
const errMsg = envelope.payload?.message || envelope.payload?.error || 'Task failed';
|
|
550
|
+
const err = new Error(errMsg);
|
|
551
|
+
err.payload = envelope.payload;
|
|
552
|
+
reject(err);
|
|
179
553
|
}
|
|
180
554
|
}
|
|
181
555
|
} catch {}
|
|
@@ -183,7 +557,7 @@ class RealtimeConnection {
|
|
|
183
557
|
|
|
184
558
|
this.ws.addEventListener('message', handler);
|
|
185
559
|
|
|
186
|
-
const
|
|
560
|
+
const broadcastMsg = {
|
|
187
561
|
topic: this.topic,
|
|
188
562
|
event: 'broadcast',
|
|
189
563
|
payload: {
|
|
@@ -191,84 +565,24 @@ class RealtimeConnection {
|
|
|
191
565
|
event: 'command',
|
|
192
566
|
payload: command,
|
|
193
567
|
},
|
|
194
|
-
ref:
|
|
195
|
-
join_ref: this.joinRef,
|
|
568
|
+
ref: this.nextRef(),
|
|
196
569
|
};
|
|
197
570
|
|
|
198
|
-
this.ws.send(JSON.stringify(
|
|
571
|
+
this.ws.send(JSON.stringify(broadcastMsg));
|
|
199
572
|
});
|
|
200
573
|
}
|
|
201
574
|
|
|
202
575
|
close() {
|
|
203
|
-
if (this.heartbeatTimer)
|
|
204
|
-
clearInterval(this.heartbeatTimer);
|
|
205
|
-
this.heartbeatTimer = null;
|
|
206
|
-
}
|
|
576
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
207
577
|
if (this.ws) {
|
|
208
578
|
try {
|
|
209
579
|
this.ws.close();
|
|
210
580
|
} catch {}
|
|
211
|
-
this.ws = null;
|
|
212
581
|
}
|
|
213
582
|
}
|
|
214
583
|
}
|
|
215
584
|
|
|
216
|
-
|
|
217
|
-
if (!code) {
|
|
218
|
-
console.log(`
|
|
219
|
-
\x1b[1mKroki Pairing\x1b[0m
|
|
220
|
-
Requires an active \x1b[33mUltra\x1b[0m plan.
|
|
221
|
-
|
|
222
|
-
1. Log in at \x1b[36mhttps://kroki.ai/#/devices\x1b[0m
|
|
223
|
-
2. Find your active browser profile
|
|
224
|
-
3. Click \x1b[32m"Скопировать код для AI"\x1b[0m
|
|
225
|
-
4. Run:
|
|
226
|
-
\x1b[33mnpx kroki pair <YOUR_CODE>\x1b[0m
|
|
227
|
-
`);
|
|
228
|
-
process.exit(0);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const payload = decodePairingCode(code);
|
|
232
|
-
const name = payload.deviceName || payload.deviceId;
|
|
233
|
-
console.log(`\x1b[36mConnecting to profile [${name}]...\x1b[0m`);
|
|
234
|
-
|
|
235
|
-
const conn = new RealtimeConnection(payload);
|
|
236
|
-
await conn.connect();
|
|
237
|
-
|
|
238
|
-
console.log(`\x1b[33mWaiting for confirmation in Kroki browser (click "Разрешить")...\x1b[0m`);
|
|
239
|
-
|
|
240
|
-
const pairCmd = {
|
|
241
|
-
v: 1,
|
|
242
|
-
id: `pair-${Date.now()}`,
|
|
243
|
-
kind: 'pair_request',
|
|
244
|
-
targetDeviceId: payload.deviceId,
|
|
245
|
-
taskId: `pair-${Date.now()}`,
|
|
246
|
-
issuedAt: Date.now(),
|
|
247
|
-
payload: {
|
|
248
|
-
clientName: 'Antigravity CLI',
|
|
249
|
-
},
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
try {
|
|
253
|
-
await conn.sendCommand(pairCmd, undefined, 60000);
|
|
254
|
-
} catch (err) {
|
|
255
|
-
conn.close();
|
|
256
|
-
console.error(`\x1b[31m✖ ${err.message}\x1b[0m`);
|
|
257
|
-
process.exit(1);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
saveActiveSession({
|
|
261
|
-
...payload,
|
|
262
|
-
pairedAt: Date.now(),
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
conn.close();
|
|
266
|
-
|
|
267
|
-
console.log(`\x1b[32m✔ Successfully paired with "${name}".\x1b[0m`);
|
|
268
|
-
console.log('You can now run tasks without entering the code:');
|
|
269
|
-
console.log(' \x1b[33mnpx kroki "open example.com and extract titles"\x1b[0m');
|
|
270
|
-
console.log(' \x1b[33mnpx kroki run "My Workflow Name"\x1b[0m\n');
|
|
271
|
-
}
|
|
585
|
+
// ── UI Helpers ──────────────────────────────────────────────────────────────
|
|
272
586
|
|
|
273
587
|
function createProgressTracker({ verbose = false, isJson = false, initialText = '' }) {
|
|
274
588
|
const steps = [];
|
|
@@ -302,6 +616,16 @@ function createProgressTracker({ verbose = false, isJson = false, initialText =
|
|
|
302
616
|
currentStatus = firstLine;
|
|
303
617
|
}
|
|
304
618
|
},
|
|
619
|
+
onStatus(statusText) {
|
|
620
|
+
if (!statusText || typeof statusText !== 'string') return;
|
|
621
|
+
const firstLine = statusText.split('\n')[0].trim();
|
|
622
|
+
if (!firstLine) return;
|
|
623
|
+
if (verbose) {
|
|
624
|
+
console.log(` \x1b[90mℹ ${firstLine.slice(0, 100)}\x1b[0m`);
|
|
625
|
+
} else {
|
|
626
|
+
currentStatus = firstLine;
|
|
627
|
+
}
|
|
628
|
+
},
|
|
305
629
|
stop() {
|
|
306
630
|
if (timer) {
|
|
307
631
|
clearInterval(timer);
|
|
@@ -317,81 +641,651 @@ function createProgressTracker({ verbose = false, isJson = false, initialText =
|
|
|
317
641
|
};
|
|
318
642
|
}
|
|
319
643
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
console.
|
|
644
|
+
// ── Commands ────────────────────────────────────────────────────────────────
|
|
645
|
+
|
|
646
|
+
async function loginCommand(explicitCode) {
|
|
647
|
+
if (explicitCode) {
|
|
648
|
+
saveFromCode(explicitCode);
|
|
649
|
+
console.log('\x1b[32m✔ Successfully logged in!\x1b[0m\n');
|
|
650
|
+
await devicesCommand();
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const port = 54321;
|
|
655
|
+
const authUrl = `https://kroki.ai/#/devices?cli_port=${port}`;
|
|
656
|
+
|
|
657
|
+
console.log(`\x1b[36mLogging in to Kroki...\x1b[0m`);
|
|
658
|
+
console.log(`Opening browser at: \x1b[33m${authUrl}\x1b[0m\n`);
|
|
659
|
+
|
|
660
|
+
openBrowser(authUrl);
|
|
661
|
+
|
|
662
|
+
console.log('Waiting for browser authentication (or paste your code from https://kroki.ai/#/devices below):');
|
|
663
|
+
|
|
664
|
+
let resolved = false;
|
|
665
|
+
let server = null;
|
|
666
|
+
let rl = null;
|
|
667
|
+
|
|
668
|
+
const finish = async code => {
|
|
669
|
+
if (resolved) return;
|
|
670
|
+
resolved = true;
|
|
671
|
+
if (server) {
|
|
672
|
+
try {
|
|
673
|
+
server.close();
|
|
674
|
+
} catch {}
|
|
675
|
+
}
|
|
676
|
+
if (rl) {
|
|
677
|
+
try {
|
|
678
|
+
rl.close();
|
|
679
|
+
} catch {}
|
|
680
|
+
}
|
|
681
|
+
try {
|
|
682
|
+
saveFromCode(code);
|
|
683
|
+
console.log('\n\x1b[32m✔ Successfully logged in!\x1b[0m\n');
|
|
684
|
+
await devicesCommand();
|
|
685
|
+
} catch (e) {
|
|
686
|
+
console.error(`\x1b[31m✖ Login failed:\x1b[0m ${e.message}`);
|
|
687
|
+
process.exit(1);
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
try {
|
|
692
|
+
server = createServer((req, res) => {
|
|
693
|
+
const parsedUrl = new URL(req.url, `http://localhost:${port}`);
|
|
694
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
695
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
696
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
697
|
+
|
|
698
|
+
if (req.method === 'OPTIONS') {
|
|
699
|
+
res.writeHead(204);
|
|
700
|
+
res.end();
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (req.method === 'POST') {
|
|
705
|
+
let body = '';
|
|
706
|
+
req.on('data', chunk => {
|
|
707
|
+
body += chunk;
|
|
708
|
+
});
|
|
709
|
+
req.on('end', () => {
|
|
710
|
+
try {
|
|
711
|
+
const data = JSON.parse(body);
|
|
712
|
+
if (data.token || data.code) {
|
|
713
|
+
const cfg = saveFromCode(data.code ? data.code : data);
|
|
714
|
+
const activeIds =
|
|
715
|
+
cfg.selectedDeviceIds && cfg.selectedDeviceIds.length > 0
|
|
716
|
+
? cfg.selectedDeviceIds
|
|
717
|
+
: Object.keys(cfg.devices || {});
|
|
718
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
719
|
+
res.end(JSON.stringify({ ok: true, selectedDeviceIds: activeIds }));
|
|
720
|
+
finish(data.code ? data.code : data);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
} catch {}
|
|
724
|
+
res.writeHead(400);
|
|
725
|
+
res.end(JSON.stringify({ error: 'invalid payload' }));
|
|
726
|
+
});
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const codeParam = parsedUrl.searchParams.get('code') || parsedUrl.searchParams.get('token');
|
|
731
|
+
if (codeParam) {
|
|
732
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
733
|
+
res.end(
|
|
734
|
+
`<html><body style="font-family:system-ui;text-align:center;padding:50px;background:#18181b;color:#f4f4f5"><h2>✔ Kroki CLI Authorized!</h2><p>You can return to your terminal.</p></body></html>`,
|
|
735
|
+
);
|
|
736
|
+
finish(codeParam);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
res.writeHead(404);
|
|
741
|
+
res.end();
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
server.listen(port, '127.0.0.1');
|
|
745
|
+
server.on('error', () => {
|
|
746
|
+
// Port in use, proceed with stdin prompt
|
|
747
|
+
});
|
|
748
|
+
} catch {}
|
|
749
|
+
|
|
750
|
+
rl = createInterface({
|
|
751
|
+
input: process.stdin,
|
|
752
|
+
output: process.stdout,
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
rl.question('> ', answer => {
|
|
756
|
+
if (answer && answer.trim()) {
|
|
757
|
+
finish(answer.trim());
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async function devicesCommand({ json = false, all = false, target = null, args = [] } = {}) {
|
|
763
|
+
const config = getConfig();
|
|
764
|
+
if (!config || !config.account || !config.account.token) {
|
|
765
|
+
console.error('\x1b[31mError: Not logged in.\x1b[0m');
|
|
766
|
+
console.error('Run \x1b[33mkroki login\x1b[0m to authenticate first.');
|
|
326
767
|
process.exit(1);
|
|
327
768
|
}
|
|
328
769
|
|
|
329
|
-
const
|
|
770
|
+
const devices = { ...(config.devices || {}) };
|
|
771
|
+
|
|
772
|
+
// Fetch accounts & devices from backend using control token
|
|
773
|
+
try {
|
|
774
|
+
const res = await fetch(`${config.account.supabaseUrl || DEFAULT_SUPABASE_URL}/functions/v1/get-control-token`, {
|
|
775
|
+
method: 'POST',
|
|
776
|
+
headers: {
|
|
777
|
+
Origin: 'https://kroki.ai',
|
|
778
|
+
'Content-Type': 'application/json',
|
|
779
|
+
apikey: config.account.anonKey || DEFAULT_ANON_KEY,
|
|
780
|
+
Authorization: `Bearer ${config.account.token}`,
|
|
781
|
+
},
|
|
782
|
+
body: JSON.stringify({ action: 'list_devices' }),
|
|
783
|
+
});
|
|
784
|
+
if (res.ok) {
|
|
785
|
+
const data = await res.json();
|
|
786
|
+
if (Array.isArray(data.accounts)) {
|
|
787
|
+
for (const acc of data.accounts) {
|
|
788
|
+
if (Array.isArray(acc.devices)) {
|
|
789
|
+
for (const d of acc.devices) {
|
|
790
|
+
const resolvedEmail =
|
|
791
|
+
acc.email ||
|
|
792
|
+
(acc.google_id === config.account.googleId ? config.account.email : null) ||
|
|
793
|
+
config.account.email ||
|
|
794
|
+
acc.label ||
|
|
795
|
+
acc.google_id;
|
|
796
|
+
devices[d.device_id] = {
|
|
797
|
+
deviceId: d.device_id,
|
|
798
|
+
deviceName: d.device_name || d.device_id,
|
|
799
|
+
platform: d.platform || 'mac',
|
|
800
|
+
account: resolvedEmail,
|
|
801
|
+
googleId: acc.google_id,
|
|
802
|
+
lastSeenAt: d.last_seen_at ? Date.parse(d.last_seen_at) : Date.now(),
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
config.devices = devices;
|
|
808
|
+
saveConfig(config);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
} catch {}
|
|
812
|
+
|
|
813
|
+
const deviceList = Object.values(devices);
|
|
814
|
+
|
|
815
|
+
if (deviceList.length === 0) {
|
|
816
|
+
console.log('\n\x1b[33mNo connected browsers found in this account.\x1b[0m');
|
|
817
|
+
console.log('Connect Chrome from the Kroki extension or visit https://kroki.ai/#/devices\n');
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Ensure a default device exists
|
|
822
|
+
if (!config.defaultDeviceId || !devices[config.defaultDeviceId]) {
|
|
823
|
+
config.defaultDeviceId = deviceList[0].deviceId;
|
|
824
|
+
saveConfig(config);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const rawInput = (args.length > 0 ? args.join(' ') : target || '').trim();
|
|
828
|
+
|
|
829
|
+
// Query live presence
|
|
830
|
+
const { onlineIds, expired } = await fetchOnlineDeviceIds(config);
|
|
831
|
+
|
|
832
|
+
// 1. If arguments provided to select browser(s): e.g. "1", "1, 2", "all"
|
|
833
|
+
if (rawInput) {
|
|
834
|
+
if (rawInput.toLowerCase() === 'all') {
|
|
835
|
+
config.activeDeviceIds = [];
|
|
836
|
+
saveConfig(config);
|
|
837
|
+
console.log('\x1b[32m✔ All browsers are now active.\x1b[0m\n');
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const parts = rawInput
|
|
842
|
+
.split(/[,\s]+/)
|
|
843
|
+
.map(s => s.trim())
|
|
844
|
+
.filter(Boolean);
|
|
845
|
+
const chosen = [];
|
|
846
|
+
for (const p of parts) {
|
|
847
|
+
try {
|
|
848
|
+
const dev = resolveTargetDevice(config, p);
|
|
849
|
+
if (dev && !chosen.some(c => c.deviceId === dev.deviceId)) {
|
|
850
|
+
chosen.push(dev);
|
|
851
|
+
}
|
|
852
|
+
} catch (err) {
|
|
853
|
+
console.error(`\x1b[31m✖ Error resolving "${p}":\x1b[0m ${err.message}`);
|
|
854
|
+
process.exit(1);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (chosen.length > 0) {
|
|
859
|
+
config.activeDeviceIds = chosen.map(d => d.deviceId);
|
|
860
|
+
config.defaultDeviceId = chosen[0].deviceId;
|
|
861
|
+
saveConfig(config);
|
|
862
|
+
if (chosen.length === 1) {
|
|
863
|
+
const idx = deviceList.findIndex(d => d.deviceId === chosen[0].deviceId);
|
|
864
|
+
const isOnline = onlineIds.has(chosen[0].deviceId);
|
|
865
|
+
const statusTag = isOnline ? ' \x1b[32m● online\x1b[0m' : ' \x1b[90m○ offline\x1b[0m';
|
|
866
|
+
console.log(
|
|
867
|
+
`\x1b[32m✔ Active browser set to:\x1b[0m \x1b[36m[${idx + 1}] ${chosen[0].deviceName}\x1b[0m${statusTag} (${chosen[0].account || 'browser'})\n`,
|
|
868
|
+
);
|
|
869
|
+
if (!isOnline && !expired) {
|
|
870
|
+
console.log(
|
|
871
|
+
`\x1b[33m⚠ Note: This browser is currently offline. Open Chrome on this profile before running tasks.\x1b[0m\n`,
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
} else {
|
|
875
|
+
console.log(`\x1b[32m✔ Active browsers set (${chosen.length}):\x1b[0m`);
|
|
876
|
+
let hasOffline = false;
|
|
877
|
+
chosen.forEach(c => {
|
|
878
|
+
const idx = deviceList.findIndex(d => d.deviceId === c.deviceId);
|
|
879
|
+
const isOnline = onlineIds.has(c.deviceId);
|
|
880
|
+
if (!isOnline) hasOffline = true;
|
|
881
|
+
const statusTag = isOnline ? ' \x1b[32m● online\x1b[0m' : ' \x1b[90m○ offline\x1b[0m';
|
|
882
|
+
console.log(` \x1b[36m[${idx + 1}]\x1b[0m ${c.deviceName}${statusTag} (${c.account || 'browser'})`);
|
|
883
|
+
});
|
|
884
|
+
console.log();
|
|
885
|
+
if (hasOffline && !expired) {
|
|
886
|
+
console.log(
|
|
887
|
+
`\x1b[33m⚠ Note: Some chosen browsers are offline. Tasks will only execute on online browsers.\x1b[0m\n`,
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// 2. JSON output
|
|
896
|
+
if (json) {
|
|
897
|
+
const activeIds = config.activeDeviceIds && config.activeDeviceIds.length > 0 ? config.activeDeviceIds : null;
|
|
898
|
+
const list = activeIds && !all ? deviceList.filter(d => activeIds.includes(d.deviceId)) : deviceList;
|
|
899
|
+
const output = list.map(d => {
|
|
900
|
+
const idx = deviceList.findIndex(item => item.deviceId === d.deviceId);
|
|
901
|
+
const isOnline = onlineIds.has(d.deviceId);
|
|
902
|
+
return {
|
|
903
|
+
index: idx + 1,
|
|
904
|
+
deviceId: d.deviceId,
|
|
905
|
+
deviceName: d.deviceName,
|
|
906
|
+
account: d.account || null,
|
|
907
|
+
platform: d.platform || 'mac',
|
|
908
|
+
status: isOnline ? 'online' : 'offline',
|
|
909
|
+
isOnline,
|
|
910
|
+
isActive: !activeIds || activeIds.includes(d.deviceId),
|
|
911
|
+
isDefault: config.defaultDeviceId === d.deviceId,
|
|
912
|
+
lastSeenAt: d.lastSeenAt || null,
|
|
913
|
+
};
|
|
914
|
+
});
|
|
915
|
+
console.log(JSON.stringify(output, null, 2));
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// 3. Display list of browsers
|
|
920
|
+
const activeIds = config.activeDeviceIds && config.activeDeviceIds.length > 0 ? config.activeDeviceIds : null;
|
|
921
|
+
console.log(`\n\x1b[1mAvailable Kroki Browsers (${deviceList.length}):\x1b[0m`);
|
|
922
|
+
deviceList.forEach((d, idx) => {
|
|
923
|
+
const isAct = !activeIds || activeIds.includes(d.deviceId);
|
|
924
|
+
const isDef = config.defaultDeviceId === d.deviceId;
|
|
925
|
+
const isOnline = onlineIds.has(d.deviceId);
|
|
926
|
+
const statusTag = isOnline ? ' \x1b[32m● online\x1b[0m' : ' \x1b[90m○ offline\x1b[0m';
|
|
927
|
+
const mark = isAct ? '\x1b[32m✔\x1b[0m ' : ' ';
|
|
928
|
+
const defTag = isDef ? ' \x1b[33m[default]\x1b[0m' : '';
|
|
929
|
+
const accTag = d.account && !/^\d{15,}$/.test(d.account) ? ` \x1b[90m(${d.account})\x1b[0m` : '';
|
|
930
|
+
console.log(` ${mark}\x1b[36m[${idx + 1}]\x1b[0m ${d.deviceName || d.deviceId}${statusTag}${accTag}${defTag}`);
|
|
931
|
+
});
|
|
932
|
+
console.log();
|
|
933
|
+
|
|
934
|
+
if (activeIds && activeIds.length < deviceList.length) {
|
|
935
|
+
console.log(`\x1b[32mActive:\x1b[0m ${activeIds.length} of ${deviceList.length} browsers\n`);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (expired) {
|
|
939
|
+
console.log(
|
|
940
|
+
`\x1b[33m⚠ Note: Session token has expired. Run "kroki login" to re-authenticate and refresh live presence.\x1b[0m\n`,
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Interactive selection if in terminal (TTY)
|
|
945
|
+
if (process.stdin.isTTY) {
|
|
946
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
947
|
+
rl.question('Select browser(s) [1..10, or "all"] (Enter to keep current): ', ans => {
|
|
948
|
+
rl.close();
|
|
949
|
+
const val = ans.trim();
|
|
950
|
+
if (!val) return;
|
|
951
|
+
if (val.toLowerCase() === 'all') {
|
|
952
|
+
config.activeDeviceIds = [];
|
|
953
|
+
saveConfig(config);
|
|
954
|
+
console.log('\n\x1b[32m✔ All browsers are now active.\x1b[0m\n');
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const parts = val
|
|
959
|
+
.split(/[,\s]+/)
|
|
960
|
+
.map(s => s.trim())
|
|
961
|
+
.filter(Boolean);
|
|
962
|
+
const chosen = [];
|
|
963
|
+
for (const p of parts) {
|
|
964
|
+
try {
|
|
965
|
+
const dev = resolveTargetDevice(config, p);
|
|
966
|
+
if (dev && !chosen.some(c => c.deviceId === dev.deviceId)) {
|
|
967
|
+
chosen.push(dev);
|
|
968
|
+
}
|
|
969
|
+
} catch (err) {
|
|
970
|
+
console.warn(`\x1b[33mWarning: ${err.message}\x1b[0m`);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (chosen.length > 0) {
|
|
975
|
+
config.activeDeviceIds = chosen.map(d => d.deviceId);
|
|
976
|
+
config.defaultDeviceId = chosen[0].deviceId;
|
|
977
|
+
saveConfig(config);
|
|
978
|
+
console.log(`\n\x1b[32m✔ Active browser(s) set (${chosen.length}):\x1b[0m`);
|
|
979
|
+
chosen.forEach(c => {
|
|
980
|
+
const idx = deviceList.findIndex(d => d.deviceId === c.deviceId);
|
|
981
|
+
const isOnline = onlineIds.has(c.deviceId);
|
|
982
|
+
const statusTag = isOnline ? ' \x1b[32m● online\x1b[0m' : ' \x1b[90m○ offline\x1b[0m';
|
|
983
|
+
console.log(` \x1b[36m[${idx + 1}]\x1b[0m ${c.deviceName}${statusTag} (${c.account || 'browser'})`);
|
|
984
|
+
});
|
|
985
|
+
console.log();
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
} else {
|
|
989
|
+
console.log('\x1b[90mSelect browser: kroki devices <number> (e.g. kroki devices 1, kroki devices all)\x1b[0m\n');
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
async function pairCommand(code) {
|
|
994
|
+
if (!code) {
|
|
995
|
+
console.log(`
|
|
996
|
+
Pair Kroki CLI with a live browser profile.
|
|
997
|
+
|
|
998
|
+
1. Log in at \x1b[36mhttps://kroki.ai/#/devices\x1b[0m
|
|
999
|
+
2. Find your active browser profile
|
|
1000
|
+
3. Click \x1b[32m"Скопировать код для AI"\x1b[0m
|
|
1001
|
+
4. Run:
|
|
1002
|
+
\x1b[33mkroki pair <YOUR_CODE>\x1b[0m
|
|
1003
|
+
`);
|
|
1004
|
+
process.exit(0);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
const payload = decodePairingCode(code);
|
|
1008
|
+
const name = payload.deviceName || payload.deviceId;
|
|
1009
|
+
console.log(`\x1b[36mConnecting to profile [${name}]...\x1b[0m`);
|
|
1010
|
+
|
|
1011
|
+
const conn = new RealtimeConnection(payload);
|
|
330
1012
|
await conn.connect();
|
|
331
1013
|
|
|
1014
|
+
console.log(`\x1b[33mWaiting for confirmation in Kroki browser (click "Разрешить")...\x1b[0m`);
|
|
1015
|
+
|
|
1016
|
+
const pairCmd = {
|
|
1017
|
+
v: 1,
|
|
1018
|
+
id: `pair-${Date.now()}`,
|
|
1019
|
+
kind: 'pair_request',
|
|
1020
|
+
targetDeviceId: payload.deviceId,
|
|
1021
|
+
taskId: `pair-${Date.now()}`,
|
|
1022
|
+
issuedAt: Date.now(),
|
|
1023
|
+
payload: {
|
|
1024
|
+
clientName: 'Antigravity CLI',
|
|
1025
|
+
},
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
try {
|
|
1029
|
+
await conn.sendCommand(pairCmd, undefined, 60000);
|
|
1030
|
+
} catch (err) {
|
|
1031
|
+
conn.close();
|
|
1032
|
+
console.error(`\x1b[31m✖ ${err.message}\x1b[0m`);
|
|
1033
|
+
process.exit(1);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
saveFromCode(code);
|
|
1037
|
+
conn.close();
|
|
1038
|
+
|
|
1039
|
+
console.log(`\x1b[32m✔ Successfully paired with "${name}".\x1b[0m`);
|
|
1040
|
+
console.log('You can now run tasks without entering the code:');
|
|
1041
|
+
console.log(' \x1b[33mkroki "open example.com and extract titles"\x1b[0m');
|
|
1042
|
+
console.log(' \x1b[33mkroki run "My Workflow Name"\x1b[0m\n');
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
async function runSingleDeviceTask(config, device, taskText, { verbose = false, json = false } = {}) {
|
|
1046
|
+
const devList = Object.values(config.devices || {});
|
|
1047
|
+
const devIdx = devList.findIndex(d => d.deviceId === device.deviceId) + 1;
|
|
1048
|
+
const idxStr = devIdx > 0 ? `[${devIdx}] ` : '';
|
|
1049
|
+
const devLabel = `${idxStr}${device.deviceName || device.deviceId}`;
|
|
1050
|
+
|
|
1051
|
+
if (isTokenExpired(config.account?.token)) {
|
|
1052
|
+
const errMsg = 'Session token has expired. Run "kroki login" to re-authenticate.';
|
|
1053
|
+
if (!json) {
|
|
1054
|
+
console.error('\x1b[31m✖ Error: Session token has expired.\x1b[0m');
|
|
1055
|
+
console.error('Run \x1b[33mkroki login\x1b[0m to re-authenticate.\n');
|
|
1056
|
+
}
|
|
1057
|
+
return { device, ok: false, error: errMsg, steps: [] };
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
const targetGoogleId = device.googleId || config.account.googleId;
|
|
1061
|
+
const conn = new RealtimeConnection({
|
|
1062
|
+
...config.account,
|
|
1063
|
+
googleId: targetGoogleId,
|
|
1064
|
+
deviceId: device.deviceId,
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
try {
|
|
1068
|
+
await conn.connect();
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
return { device, ok: false, error: err.message, steps: [] };
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
if (!conn.isDeviceOnline(device.deviceId)) {
|
|
1074
|
+
conn.close();
|
|
1075
|
+
const errMsg = `Browser ${devLabel} is offline. Make sure Google Chrome is open with the Kroki extension active on this profile.`;
|
|
1076
|
+
if (!json) {
|
|
1077
|
+
console.error(`\x1b[31m✖ Cannot run task: Browser ${devLabel} is offline.\x1b[0m`);
|
|
1078
|
+
console.error(` Make sure Google Chrome is open with the Kroki extension active on this profile.\n`);
|
|
1079
|
+
}
|
|
1080
|
+
return { device, ok: false, error: errMsg, steps: [] };
|
|
1081
|
+
}
|
|
1082
|
+
|
|
332
1083
|
const command = {
|
|
333
1084
|
v: 1,
|
|
334
|
-
id: `task-${Date.now()}`,
|
|
1085
|
+
id: `task-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
335
1086
|
kind: 'chat_task',
|
|
336
|
-
targetDeviceId:
|
|
337
|
-
taskId: `task-${Date.now()}`,
|
|
1087
|
+
targetDeviceId: device.deviceId,
|
|
1088
|
+
taskId: `task-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
338
1089
|
issuedAt: Date.now(),
|
|
339
1090
|
payload: {
|
|
340
1091
|
task: taskText,
|
|
341
1092
|
},
|
|
342
1093
|
};
|
|
343
1094
|
|
|
344
|
-
const initialMsg = `\x1b[36m[Kroki: ${
|
|
1095
|
+
const initialMsg = `\x1b[36m[Kroki: ${devLabel}]\x1b[0m Running: "${taskText}"...`;
|
|
345
1096
|
const tracker = createProgressTracker({ verbose, isJson: json, initialText: initialMsg });
|
|
346
1097
|
|
|
347
1098
|
try {
|
|
348
1099
|
const result = await conn.sendCommand(command, ev => {
|
|
349
1100
|
if (ev.kind === 'execution') {
|
|
350
1101
|
const details = ev.payload?.data?.details || ev.payload?.title || ev.payload?.type;
|
|
351
|
-
if (details) tracker.onStep(details);
|
|
1102
|
+
if (details) tracker.onStep(`[${devLabel}] ${details}`);
|
|
352
1103
|
}
|
|
353
1104
|
});
|
|
354
1105
|
|
|
355
1106
|
tracker.stop();
|
|
1107
|
+
return { device, ok: true, result, steps: tracker.getSteps() };
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
tracker.stop();
|
|
1110
|
+
return { device, ok: false, error: err.message, steps: tracker.getSteps() };
|
|
1111
|
+
} finally {
|
|
1112
|
+
conn.close();
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
356
1115
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
1116
|
+
async function taskCommand(taskText, { verbose = false, json = false, device: targetQuery = null, all = false } = {}) {
|
|
1117
|
+
const config = getConfig();
|
|
1118
|
+
if (!config || !config.account || !config.account.token) {
|
|
1119
|
+
console.error('\x1b[31mError: Not logged in or paired.\x1b[0m');
|
|
1120
|
+
console.error('Run \x1b[33mkroki login\x1b[0m or \x1b[33mkroki pair <CODE>\x1b[0m first.\n');
|
|
1121
|
+
process.exit(1);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Auto-detect natural language requests to run a workflow (e.g. 'запусти Order Ali', 'run workflow Ali')
|
|
1125
|
+
const wfMatch = taskText.match(
|
|
1126
|
+
/^(?:запусти(?:ть)?|стартуй|run|launch|execute)\s+(?:воркфлоу|workflow\s+)?["']?([^"']+)["']?$/i,
|
|
1127
|
+
);
|
|
1128
|
+
if (wfMatch) {
|
|
1129
|
+
const candidate = wfMatch[1].trim();
|
|
1130
|
+
const resolvedDev = resolveTargetDevice(config, targetQuery);
|
|
1131
|
+
const targetGoogleId = resolvedDev?.googleId || config.account.googleId;
|
|
1132
|
+
try {
|
|
1133
|
+
const res = await fetch(`${config.account.supabaseUrl || DEFAULT_SUPABASE_URL}/functions/v1/get-control-token`, {
|
|
1134
|
+
method: 'POST',
|
|
1135
|
+
headers: {
|
|
1136
|
+
Origin: 'https://kroki.ai',
|
|
1137
|
+
'Content-Type': 'application/json',
|
|
1138
|
+
apikey: config.account.anonKey || DEFAULT_ANON_KEY,
|
|
1139
|
+
Authorization: `Bearer ${config.account.token}`,
|
|
1140
|
+
},
|
|
1141
|
+
body: JSON.stringify({ action: 'list_workflows', owner_google_id: targetGoogleId }),
|
|
1142
|
+
});
|
|
1143
|
+
if (res.ok) {
|
|
1144
|
+
const data = await res.json();
|
|
1145
|
+
if (Array.isArray(data.workflows)) {
|
|
1146
|
+
const q = candidate.toLowerCase();
|
|
1147
|
+
const match =
|
|
1148
|
+
data.workflows.find(w => w.id === candidate) ||
|
|
1149
|
+
data.workflows.find(w => (w.name || '').toLowerCase() === q) ||
|
|
1150
|
+
data.workflows.find(w => (w.name || '').toLowerCase().includes(q));
|
|
1151
|
+
if (match) {
|
|
1152
|
+
console.log(
|
|
1153
|
+
`\x1b[36m[Kroki]\x1b[0m Detected workflow "${match.name || match.id}". Launching workflow runner...\n`,
|
|
1154
|
+
);
|
|
1155
|
+
return workflowCommand(match.id, null, { verbose, json, device: targetQuery, taskText });
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
362
1158
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
1159
|
+
} catch {}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const isAll =
|
|
1163
|
+
all ||
|
|
1164
|
+
targetQuery === 'all' ||
|
|
1165
|
+
targetQuery === 'both' ||
|
|
1166
|
+
/обоих браузер|всех браузер|на всех|на обоих|все браузеры|оба браузера/i.test(taskText);
|
|
1167
|
+
|
|
1168
|
+
let targetDevices = [];
|
|
1169
|
+
if (isAll) {
|
|
1170
|
+
targetDevices = Object.values(config.devices || {});
|
|
1171
|
+
} else {
|
|
1172
|
+
const device = resolveTargetDevice(config, targetQuery);
|
|
1173
|
+
if (device) targetDevices = [device];
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
if (targetDevices.length === 0) {
|
|
1177
|
+
console.error('\x1b[31mError: No target browser device found.\x1b[0m');
|
|
1178
|
+
console.error('Run \x1b[33mkroki devices\x1b[0m to select active browsers.\n');
|
|
1179
|
+
process.exit(1);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
if (targetDevices.length === 1) {
|
|
1183
|
+
const r = await runSingleDeviceTask(config, targetDevices[0], taskText, { verbose, json });
|
|
1184
|
+
if (!r.ok) {
|
|
1185
|
+
if (!verbose && r.steps && r.steps.length > 0) {
|
|
1186
|
+
console.error('\x1b[90mRecent steps before failure:\x1b[0m');
|
|
1187
|
+
for (const step of r.steps.slice(-10)) {
|
|
1188
|
+
console.error(` \x1b[90m→ ${step}\x1b[0m`);
|
|
1189
|
+
}
|
|
1190
|
+
console.error('');
|
|
367
1191
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
console.error('\x1b[90mRecent steps before failure:\x1b[0m');
|
|
373
|
-
for (const step of tracker.getSteps().slice(-10)) {
|
|
374
|
-
console.error(` \x1b[90m→ ${step}\x1b[0m`);
|
|
1192
|
+
if (json) {
|
|
1193
|
+
console.error(JSON.stringify({ success: false, error: r.error, steps: r.steps }, null, 2));
|
|
1194
|
+
} else {
|
|
1195
|
+
console.error(`\x1b[31m✖ Error: ${r.error}\x1b[0m`);
|
|
375
1196
|
}
|
|
376
|
-
|
|
1197
|
+
process.exit(1);
|
|
377
1198
|
}
|
|
1199
|
+
|
|
378
1200
|
if (json) {
|
|
379
|
-
console.
|
|
1201
|
+
console.log(JSON.stringify(r.result, null, 2));
|
|
380
1202
|
} else {
|
|
381
|
-
console.
|
|
1203
|
+
if (verbose) console.log('\x1b[32m✔ Task completed.\x1b[0m\n');
|
|
1204
|
+
if (r.result && typeof r.result === 'object' && r.result.message) {
|
|
1205
|
+
console.log(r.result.message);
|
|
1206
|
+
} else if (r.result && typeof r.result === 'object') {
|
|
1207
|
+
console.log(JSON.stringify(r.result, null, 2));
|
|
1208
|
+
}
|
|
382
1209
|
}
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// Multi-device parallel dispatch
|
|
1214
|
+
console.log(`\x1b[36mDispatching task to ${targetDevices.length} browsers in parallel...\x1b[0m\n`);
|
|
1215
|
+
const results = await Promise.all(
|
|
1216
|
+
targetDevices.map(d => runSingleDeviceTask(config, d, taskText, { verbose, json })),
|
|
1217
|
+
);
|
|
1218
|
+
|
|
1219
|
+
if (json) {
|
|
1220
|
+
console.log(JSON.stringify(results, null, 2));
|
|
1221
|
+
} else {
|
|
1222
|
+
console.log('\n\x1b[32m✔ Completed on all target browsers:\x1b[0m');
|
|
1223
|
+
for (const r of results) {
|
|
1224
|
+
const devName = r.device.deviceName || r.device.deviceId;
|
|
1225
|
+
if (r.ok) {
|
|
1226
|
+
const msg = r.result && typeof r.result === 'object' && r.result.message ? r.result.message : 'Success';
|
|
1227
|
+
console.log(` \x1b[32m✔\x1b[0m \x1b[1m${devName}\x1b[0m: ${msg}`);
|
|
1228
|
+
} else {
|
|
1229
|
+
console.log(` \x1b[31m✖\x1b[0m \x1b[1m${devName}\x1b[0m: ${r.error}`);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
async function workflowsCommand({ json = false, device: targetQuery = null } = {}) {
|
|
1236
|
+
const config = getConfig();
|
|
1237
|
+
if (!config || !config.account || !config.account.token) {
|
|
1238
|
+
console.error('\x1b[31mError: Not logged in.\x1b[0m');
|
|
1239
|
+
console.error('Run \x1b[33mkroki login\x1b[0m first.\n');
|
|
383
1240
|
process.exit(1);
|
|
384
|
-
} finally {
|
|
385
|
-
conn.close();
|
|
386
1241
|
}
|
|
1242
|
+
|
|
1243
|
+
const device = resolveTargetDevice(config, targetQuery);
|
|
1244
|
+
const targetGoogleId = device?.googleId || config.account.googleId;
|
|
1245
|
+
|
|
1246
|
+
const res = await fetch(`${config.account.supabaseUrl || DEFAULT_SUPABASE_URL}/functions/v1/get-control-token`, {
|
|
1247
|
+
method: 'POST',
|
|
1248
|
+
headers: {
|
|
1249
|
+
Origin: 'https://kroki.ai',
|
|
1250
|
+
'Content-Type': 'application/json',
|
|
1251
|
+
apikey: config.account.anonKey || DEFAULT_ANON_KEY,
|
|
1252
|
+
Authorization: `Bearer ${config.account.token}`,
|
|
1253
|
+
},
|
|
1254
|
+
body: JSON.stringify({ action: 'list_workflows', owner_google_id: targetGoogleId }),
|
|
1255
|
+
});
|
|
1256
|
+
if (!res.ok) {
|
|
1257
|
+
console.error('Failed to load workflows.');
|
|
1258
|
+
process.exit(1);
|
|
1259
|
+
}
|
|
1260
|
+
const data = await res.json();
|
|
1261
|
+
const list = data.workflows || [];
|
|
1262
|
+
if (json) {
|
|
1263
|
+
console.log(JSON.stringify(list, null, 2));
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
console.log(`\n\x1b[1mAvailable workflows for ${device?.deviceName || 'browser'}:\x1b[0m`);
|
|
1267
|
+
for (const wf of list) {
|
|
1268
|
+
console.log(` • \x1b[36m${wf.name}\x1b[0m \x1b[90m(${wf.id})\x1b[0m`);
|
|
1269
|
+
}
|
|
1270
|
+
console.log('\nRun workflow with:\n \x1b[33mkroki run "<name>"\x1b[0m\n');
|
|
387
1271
|
}
|
|
388
1272
|
|
|
389
|
-
async function workflowCommand(
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
1273
|
+
async function workflowCommand(
|
|
1274
|
+
nameOrId,
|
|
1275
|
+
varsJson,
|
|
1276
|
+
{ verbose = false, json = false, device: targetQuery = null, taskText = null } = {},
|
|
1277
|
+
) {
|
|
1278
|
+
const config = getConfig();
|
|
1279
|
+
if (!config || !config.account || !config.account.token) {
|
|
1280
|
+
console.error('\x1b[31mError: Not logged in or paired.\x1b[0m');
|
|
1281
|
+
console.error('Run \x1b[33mkroki login\x1b[0m or \x1b[33mkroki pair <CODE>\x1b[0m first.\n');
|
|
1282
|
+
process.exit(1);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
const device = resolveTargetDevice(config, targetQuery);
|
|
1286
|
+
if (!device) {
|
|
1287
|
+
console.error('\x1b[31mError: No target browser device found.\x1b[0m');
|
|
1288
|
+
console.error('Run \x1b[33mkroki devices\x1b[0m to select active browsers.\n');
|
|
395
1289
|
process.exit(1);
|
|
396
1290
|
}
|
|
397
1291
|
|
|
@@ -404,56 +1298,219 @@ async function workflowCommand(nameOrId, varsJson, { verbose = false, json = fal
|
|
|
404
1298
|
}
|
|
405
1299
|
}
|
|
406
1300
|
|
|
407
|
-
const
|
|
408
|
-
|
|
1301
|
+
const targetGoogleId = device.googleId || config.account.googleId;
|
|
1302
|
+
|
|
1303
|
+
// Resolve human workflow name to actual workflowId
|
|
1304
|
+
let targetWorkflowId = nameOrId;
|
|
1305
|
+
let resolvedName = nameOrId;
|
|
1306
|
+
|
|
1307
|
+
try {
|
|
1308
|
+
const res = await fetch(`${config.account.supabaseUrl || DEFAULT_SUPABASE_URL}/functions/v1/get-control-token`, {
|
|
1309
|
+
method: 'POST',
|
|
1310
|
+
headers: {
|
|
1311
|
+
Origin: 'https://kroki.ai',
|
|
1312
|
+
'Content-Type': 'application/json',
|
|
1313
|
+
apikey: config.account.anonKey || DEFAULT_ANON_KEY,
|
|
1314
|
+
Authorization: `Bearer ${config.account.token}`,
|
|
1315
|
+
},
|
|
1316
|
+
body: JSON.stringify({ action: 'list_workflows', owner_google_id: targetGoogleId }),
|
|
1317
|
+
});
|
|
1318
|
+
if (res.ok) {
|
|
1319
|
+
const data = await res.json();
|
|
1320
|
+
if (Array.isArray(data.workflows)) {
|
|
1321
|
+
const q = nameOrId.toLowerCase().trim();
|
|
1322
|
+
const match =
|
|
1323
|
+
data.workflows.find(w => w.id === nameOrId) ||
|
|
1324
|
+
data.workflows.find(w => (w.name || '').toLowerCase() === q) ||
|
|
1325
|
+
data.workflows.find(w => (w.name || '').toLowerCase().includes(q));
|
|
1326
|
+
if (match) {
|
|
1327
|
+
targetWorkflowId = match.id;
|
|
1328
|
+
resolvedName = match.name || match.id;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
} catch {}
|
|
1333
|
+
|
|
1334
|
+
if (isTokenExpired(config.account?.token)) {
|
|
1335
|
+
console.error('\x1b[31m✖ Error: Session token has expired.\x1b[0m');
|
|
1336
|
+
console.error('Run \x1b[33mkroki login\x1b[0m to re-authenticate.\n');
|
|
1337
|
+
process.exit(1);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
const conn = new RealtimeConnection({
|
|
1341
|
+
...config.account,
|
|
1342
|
+
googleId: targetGoogleId,
|
|
1343
|
+
deviceId: device.deviceId,
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
try {
|
|
1347
|
+
await conn.connect();
|
|
1348
|
+
} catch (err) {
|
|
1349
|
+
console.error(`\x1b[31m✖ Error: ${err.message}\x1b[0m\n`);
|
|
1350
|
+
process.exit(1);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
const devList = Object.values(config.devices || {});
|
|
1354
|
+
const devIdx = devList.findIndex(d => d.deviceId === device.deviceId) + 1;
|
|
1355
|
+
const idxStr = devIdx > 0 ? `[${devIdx}] ` : '';
|
|
1356
|
+
const devLabel = `${idxStr}${device.deviceName || device.deviceId}`;
|
|
1357
|
+
|
|
1358
|
+
if (!conn.isDeviceOnline(device.deviceId)) {
|
|
1359
|
+
conn.close();
|
|
1360
|
+
console.error(`\x1b[31m✖ Cannot run workflow: Browser ${devLabel} is offline.\x1b[0m`);
|
|
1361
|
+
console.error(` Make sure Google Chrome is open with the Kroki extension active on this profile.\n`);
|
|
1362
|
+
process.exit(1);
|
|
1363
|
+
}
|
|
409
1364
|
|
|
410
1365
|
const command = {
|
|
411
1366
|
v: 1,
|
|
412
1367
|
id: `wf-${Date.now()}`,
|
|
413
1368
|
kind: 'workflow_execute',
|
|
414
|
-
targetDeviceId:
|
|
1369
|
+
targetDeviceId: device.deviceId,
|
|
415
1370
|
taskId: `wf-${Date.now()}`,
|
|
416
1371
|
issuedAt: Date.now(),
|
|
417
1372
|
payload: {
|
|
418
|
-
workflowId:
|
|
1373
|
+
workflowId: targetWorkflowId,
|
|
1374
|
+
workflowName: resolvedName,
|
|
1375
|
+
task: taskText || `Run workflow "${resolvedName}"`,
|
|
419
1376
|
variables,
|
|
420
1377
|
},
|
|
421
1378
|
};
|
|
422
1379
|
|
|
423
|
-
const
|
|
424
|
-
const
|
|
1380
|
+
const devName = device.deviceName || device.deviceId || 'Browser';
|
|
1381
|
+
const initialMsg = `\x1b[36m[Kroki: ${devName}]\x1b[0m Started workflow "${resolvedName}" (ID: ${targetWorkflowId})`;
|
|
1382
|
+
if (!json) {
|
|
1383
|
+
console.log(initialMsg);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
const tracker = createProgressTracker({ verbose, isJson: json, initialText: 'Running workflow...' });
|
|
1387
|
+
const startTime = Date.now();
|
|
1388
|
+
let stepCount = 0;
|
|
1389
|
+
const stepHistory = [];
|
|
1390
|
+
let currentStepStartTime = Date.now();
|
|
425
1391
|
|
|
426
1392
|
try {
|
|
427
1393
|
const result = await conn.sendCommand(command, ev => {
|
|
428
|
-
if (ev.kind === '
|
|
429
|
-
const
|
|
430
|
-
tracker.
|
|
1394
|
+
if (ev.kind === 'workflow_queued') {
|
|
1395
|
+
const queuePos = ev.payload?.position;
|
|
1396
|
+
tracker.onStatus(`Queued (position ${queuePos || 1}). Waiting for runner...`);
|
|
1397
|
+
} else if (ev.kind === 'workflow_step_start') {
|
|
1398
|
+
stepCount++;
|
|
1399
|
+
currentStepStartTime = Date.now();
|
|
1400
|
+
const type = ev.payload?.stepType || 'ACTION';
|
|
1401
|
+
const comment = ev.payload?.comment ? `: ${ev.payload.comment}` : '';
|
|
1402
|
+
const stepDesc = `[step ${stepCount}] ${type}${comment}`;
|
|
1403
|
+
stepHistory.push({
|
|
1404
|
+
index: stepCount,
|
|
1405
|
+
id: ev.payload?.stepId,
|
|
1406
|
+
type,
|
|
1407
|
+
comment: ev.payload?.comment || '',
|
|
1408
|
+
startedAt: currentStepStartTime,
|
|
1409
|
+
status: 'running',
|
|
1410
|
+
});
|
|
1411
|
+
if (verbose) {
|
|
1412
|
+
tracker.onStep(stepDesc);
|
|
1413
|
+
} else {
|
|
1414
|
+
tracker.onStatus(`Running "${resolvedName}" (step ${stepCount})...`);
|
|
1415
|
+
}
|
|
1416
|
+
} else if (ev.kind === 'workflow_step_result') {
|
|
1417
|
+
const lastStep = stepHistory[stepHistory.length - 1];
|
|
1418
|
+
const duration = ((Date.now() - currentStepStartTime) / 1000).toFixed(1);
|
|
1419
|
+
if (lastStep) {
|
|
1420
|
+
lastStep.duration = `${duration}s`;
|
|
1421
|
+
lastStep.status = ev.payload?.success ? 'success' : 'failed';
|
|
1422
|
+
lastStep.error = ev.payload?.error;
|
|
1423
|
+
}
|
|
1424
|
+
if (verbose) {
|
|
1425
|
+
const stepLabel = lastStep ? `[step ${lastStep.index}] ${lastStep.type}` : 'step';
|
|
1426
|
+
if (ev.payload?.success) {
|
|
1427
|
+
console.log(` \x1b[32m✔\x1b[0m ${stepLabel} completed in ${duration}s`);
|
|
1428
|
+
} else {
|
|
1429
|
+
console.log(` \x1b[31m✖\x1b[0m ${stepLabel} failed: ${ev.payload?.error || 'Unknown error'}`);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
431
1432
|
}
|
|
432
1433
|
});
|
|
433
1434
|
|
|
434
1435
|
tracker.stop();
|
|
1436
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1) + 's';
|
|
435
1437
|
|
|
436
1438
|
if (json) {
|
|
437
|
-
console.log(
|
|
1439
|
+
console.log(
|
|
1440
|
+
JSON.stringify(
|
|
1441
|
+
{
|
|
1442
|
+
success: true,
|
|
1443
|
+
workflowId: targetWorkflowId,
|
|
1444
|
+
workflowName: resolvedName,
|
|
1445
|
+
device: devName,
|
|
1446
|
+
elapsed,
|
|
1447
|
+
totalSteps: stepCount,
|
|
1448
|
+
steps: stepHistory,
|
|
1449
|
+
result,
|
|
1450
|
+
},
|
|
1451
|
+
null,
|
|
1452
|
+
2,
|
|
1453
|
+
),
|
|
1454
|
+
);
|
|
438
1455
|
} else {
|
|
1456
|
+
console.log(
|
|
1457
|
+
`\x1b[32m✔ Workflow "${resolvedName}" completed successfully on ${devName} in ${elapsed} (${stepCount} steps executed).\x1b[0m`,
|
|
1458
|
+
);
|
|
439
1459
|
if (verbose) {
|
|
440
|
-
|
|
1460
|
+
if (stepHistory.length > 0) {
|
|
1461
|
+
console.log('\n\x1b[1mExecution Details:\x1b[0m');
|
|
1462
|
+
for (const s of stepHistory) {
|
|
1463
|
+
const icon = s.status === 'success' ? '\x1b[32m✔\x1b[0m' : '\x1b[31m✖\x1b[0m';
|
|
1464
|
+
const comment = s.comment ? ` - ${s.comment}` : '';
|
|
1465
|
+
const dur = s.duration ? ` (${s.duration})` : '';
|
|
1466
|
+
console.log(` ${icon} [step ${s.index}] \x1b[1m${s.type}\x1b[0m${comment}${dur}`);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
if (result && typeof result === 'object' && Object.keys(result).length > 0) {
|
|
1470
|
+
console.log('\n\x1b[1mResult Payload:\x1b[0m');
|
|
1471
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1472
|
+
}
|
|
1473
|
+
console.log('');
|
|
441
1474
|
}
|
|
442
|
-
if (result) console.log(JSON.stringify(result, null, 2));
|
|
443
1475
|
}
|
|
444
1476
|
} catch (err) {
|
|
445
1477
|
tracker.stop();
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
451
|
-
console.error('');
|
|
452
|
-
}
|
|
1478
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1) + 's';
|
|
1479
|
+
const isCancelled = err.payload?.cancelled || /cancelled/i.test(err.message);
|
|
1480
|
+
const failStatus = isCancelled ? 'was cancelled' : 'failed';
|
|
1481
|
+
|
|
453
1482
|
if (json) {
|
|
454
|
-
console.error(
|
|
1483
|
+
console.error(
|
|
1484
|
+
JSON.stringify(
|
|
1485
|
+
{
|
|
1486
|
+
success: false,
|
|
1487
|
+
cancelled: isCancelled,
|
|
1488
|
+
workflowId: targetWorkflowId,
|
|
1489
|
+
workflowName: resolvedName,
|
|
1490
|
+
device: devName,
|
|
1491
|
+
elapsed,
|
|
1492
|
+
error: err.message,
|
|
1493
|
+
totalSteps: stepCount,
|
|
1494
|
+
steps: stepHistory,
|
|
1495
|
+
},
|
|
1496
|
+
null,
|
|
1497
|
+
2,
|
|
1498
|
+
),
|
|
1499
|
+
);
|
|
455
1500
|
} else {
|
|
456
|
-
console.error(
|
|
1501
|
+
console.error(
|
|
1502
|
+
`\x1b[31m✖ Workflow "${resolvedName}" ${failStatus} on ${devName} in ${elapsed}: ${err.message}\x1b[0m`,
|
|
1503
|
+
);
|
|
1504
|
+
if (stepHistory.length > 0) {
|
|
1505
|
+
console.error('\n\x1b[90mRecent steps before failure:\x1b[0m');
|
|
1506
|
+
for (const s of stepHistory.slice(-5)) {
|
|
1507
|
+
const icon = s.status === 'failed' ? '\x1b[31m✖\x1b[0m' : '\x1b[90m→\x1b[0m';
|
|
1508
|
+
const comment = s.comment ? ` - ${s.comment}` : '';
|
|
1509
|
+
const dur = s.duration ? ` (${s.duration})` : '';
|
|
1510
|
+
console.error(` ${icon} [step ${s.index}] ${s.type}${comment}${dur}`);
|
|
1511
|
+
}
|
|
1512
|
+
console.error('');
|
|
1513
|
+
}
|
|
457
1514
|
}
|
|
458
1515
|
process.exit(1);
|
|
459
1516
|
} finally {
|
|
@@ -462,26 +1519,36 @@ async function workflowCommand(nameOrId, varsJson, { verbose = false, json = fal
|
|
|
462
1519
|
}
|
|
463
1520
|
|
|
464
1521
|
function statusCommand({ json = false } = {}) {
|
|
465
|
-
const
|
|
466
|
-
if (!
|
|
1522
|
+
const config = getConfig();
|
|
1523
|
+
if (!config || !config.account || !config.account.token) {
|
|
467
1524
|
if (json) {
|
|
468
|
-
console.log(JSON.stringify({ paired: false }, null, 2));
|
|
1525
|
+
console.log(JSON.stringify({ authenticated: false, paired: false }, null, 2));
|
|
469
1526
|
return;
|
|
470
1527
|
}
|
|
471
|
-
console.log('
|
|
472
|
-
console.log('
|
|
473
|
-
console.log(' npx kroki pair <CODE>');
|
|
1528
|
+
console.log('Not logged in.');
|
|
1529
|
+
console.log('Run \x1b[33mkroki login\x1b[0m or \x1b[33mkroki pair <CODE>\x1b[0m to connect.');
|
|
474
1530
|
return;
|
|
475
1531
|
}
|
|
1532
|
+
|
|
1533
|
+
const defaultDevice = (config.devices && config.defaultDeviceId && config.devices[config.defaultDeviceId]) || null;
|
|
1534
|
+
const selectedCount = (config.selectedDeviceIds || []).length;
|
|
1535
|
+
const totalCount = Object.keys(config.devices || {}).length;
|
|
1536
|
+
|
|
476
1537
|
if (json) {
|
|
477
1538
|
console.log(
|
|
478
1539
|
JSON.stringify(
|
|
479
1540
|
{
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
1541
|
+
authenticated: true,
|
|
1542
|
+
googleId: config.account.googleId,
|
|
1543
|
+
defaultDevice: defaultDevice
|
|
1544
|
+
? {
|
|
1545
|
+
deviceId: defaultDevice.deviceId,
|
|
1546
|
+
deviceName: defaultDevice.deviceName,
|
|
1547
|
+
platform: defaultDevice.platform,
|
|
1548
|
+
}
|
|
1549
|
+
: null,
|
|
1550
|
+
selectedDevicesCount: selectedCount,
|
|
1551
|
+
totalDevicesCount: totalCount,
|
|
485
1552
|
},
|
|
486
1553
|
null,
|
|
487
1554
|
2,
|
|
@@ -489,10 +1556,17 @@ function statusCommand({ json = false } = {}) {
|
|
|
489
1556
|
);
|
|
490
1557
|
return;
|
|
491
1558
|
}
|
|
1559
|
+
|
|
1560
|
+
console.log(`\x1b[32m✔ Authenticated\x1b[0m (Account ID: ${config.account.googleId})`);
|
|
1561
|
+
if (defaultDevice) {
|
|
1562
|
+
console.log(
|
|
1563
|
+
` Default browser: \x1b[36m${defaultDevice.deviceName || defaultDevice.deviceId}\x1b[0m (${defaultDevice.account || defaultDevice.platform || 'browser'})`,
|
|
1564
|
+
);
|
|
1565
|
+
console.log(` Device ID: ${defaultDevice.deviceId}`);
|
|
1566
|
+
}
|
|
492
1567
|
console.log(
|
|
493
|
-
|
|
1568
|
+
` All ${totalCount} browsers are available (use -d <number|name> to target, or 'kroki devices' to list)`,
|
|
494
1569
|
);
|
|
495
|
-
console.log(` Device ID: ${session.deviceId}`);
|
|
496
1570
|
}
|
|
497
1571
|
|
|
498
1572
|
function helpCommand() {
|
|
@@ -500,17 +1574,30 @@ function helpCommand() {
|
|
|
500
1574
|
\x1b[1mKroki CLI\x1b[0m — Browser Automation & AI Agent Bridge for Google Chrome
|
|
501
1575
|
|
|
502
1576
|
\x1b[1mUsage:\x1b[0m
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
1577
|
+
kroki login [code] Log in to your Kroki account
|
|
1578
|
+
kroki devices [1..N | all] View or select working browser(s)
|
|
1579
|
+
kroki "<task>" [options] Send an AI task to the live browser
|
|
1580
|
+
kroki run "<workflow>" [json] [opts] Execute a saved workflow
|
|
1581
|
+
kroki list / workflows [options] List available workflows
|
|
1582
|
+
kroki status [options] View active account and browsers
|
|
1583
|
+
kroki clear / logout Clear local credentials and logout
|
|
508
1584
|
|
|
509
1585
|
\x1b[1mOptions:\x1b[0m
|
|
1586
|
+
-d, --device <number|name|id> Target specific browser on-demand
|
|
510
1587
|
-v, --verbose Show real-time execution steps and logs
|
|
511
1588
|
-j, --json Output result as JSON
|
|
512
1589
|
-h, --help Show help
|
|
513
1590
|
|
|
1591
|
+
\x1b[1mExamples:\x1b[0m
|
|
1592
|
+
kroki devices
|
|
1593
|
+
kroki devices 1
|
|
1594
|
+
kroki devices 1, 2
|
|
1595
|
+
kroki devices all
|
|
1596
|
+
kroki "open apple.com"
|
|
1597
|
+
kroki "check orders" -d 2
|
|
1598
|
+
kroki run "Order Ali"
|
|
1599
|
+
kroki "open google.com" --all
|
|
1600
|
+
|
|
514
1601
|
\x1b[1mDashboard:\x1b[0m
|
|
515
1602
|
https://kroki.ai/#/devices
|
|
516
1603
|
`);
|
|
@@ -521,13 +1608,24 @@ function helpCommand() {
|
|
|
521
1608
|
const rawArgs = process.argv.slice(2);
|
|
522
1609
|
let verbose = false;
|
|
523
1610
|
let json = false;
|
|
1611
|
+
let targetDevice = process.env.KROKI_DEVICE || null;
|
|
1612
|
+
let runOnAll = false;
|
|
524
1613
|
const filteredArgs = [];
|
|
525
1614
|
|
|
526
|
-
for (
|
|
1615
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
1616
|
+
const arg = rawArgs[i];
|
|
527
1617
|
if (arg === '--verbose' || arg === '-v') {
|
|
528
1618
|
verbose = true;
|
|
529
1619
|
} else if (arg === '--json' || arg === '-j') {
|
|
530
1620
|
json = true;
|
|
1621
|
+
} else if (arg === '--all' || arg === '-a') {
|
|
1622
|
+
runOnAll = true;
|
|
1623
|
+
} else if (arg === '-d' || arg === '--device') {
|
|
1624
|
+
targetDevice = rawArgs[++i] || null;
|
|
1625
|
+
} else if (arg.startsWith('--device=')) {
|
|
1626
|
+
targetDevice = arg.slice(9);
|
|
1627
|
+
} else if (arg.startsWith('-d=')) {
|
|
1628
|
+
targetDevice = arg.slice(3);
|
|
531
1629
|
} else {
|
|
532
1630
|
filteredArgs.push(arg);
|
|
533
1631
|
}
|
|
@@ -536,6 +1634,25 @@ for (const arg of rawArgs) {
|
|
|
536
1634
|
const first = filteredArgs[0] || 'help';
|
|
537
1635
|
|
|
538
1636
|
switch (first) {
|
|
1637
|
+
case 'login':
|
|
1638
|
+
case 'auth':
|
|
1639
|
+
loginCommand(filteredArgs[1]).catch(e => {
|
|
1640
|
+
console.error(`\x1b[31mError:\x1b[0m ${e.message}`);
|
|
1641
|
+
process.exit(1);
|
|
1642
|
+
});
|
|
1643
|
+
break;
|
|
1644
|
+
|
|
1645
|
+
case 'device':
|
|
1646
|
+
case 'devices':
|
|
1647
|
+
case 'use':
|
|
1648
|
+
case 'select':
|
|
1649
|
+
case 'default':
|
|
1650
|
+
devicesCommand({ json, all: runOnAll, target: targetDevice, args: filteredArgs.slice(1) }).catch(e => {
|
|
1651
|
+
console.error(`\x1b[31mError:\x1b[0m ${e.message}`);
|
|
1652
|
+
process.exit(1);
|
|
1653
|
+
});
|
|
1654
|
+
break;
|
|
1655
|
+
|
|
539
1656
|
case 'pair':
|
|
540
1657
|
case 'connect':
|
|
541
1658
|
pairCommand(filteredArgs[1]).catch(e => {
|
|
@@ -550,16 +1667,25 @@ switch (first) {
|
|
|
550
1667
|
|
|
551
1668
|
case 'run':
|
|
552
1669
|
case 'workflow':
|
|
553
|
-
workflowCommand(filteredArgs[1], filteredArgs[2], { verbose, json }).catch(e => {
|
|
1670
|
+
workflowCommand(filteredArgs[1], filteredArgs[2], { verbose, json, device: targetDevice }).catch(e => {
|
|
1671
|
+
console.error(`\x1b[31mError:\x1b[0m ${e.message}`);
|
|
1672
|
+
process.exit(1);
|
|
1673
|
+
});
|
|
1674
|
+
break;
|
|
1675
|
+
|
|
1676
|
+
case 'list':
|
|
1677
|
+
case 'workflows':
|
|
1678
|
+
workflowsCommand({ json, device: targetDevice }).catch(e => {
|
|
554
1679
|
console.error(`\x1b[31mError:\x1b[0m ${e.message}`);
|
|
555
1680
|
process.exit(1);
|
|
556
1681
|
});
|
|
557
1682
|
break;
|
|
558
1683
|
|
|
559
1684
|
case 'clear':
|
|
1685
|
+
case 'logout':
|
|
560
1686
|
case 'disconnect':
|
|
561
|
-
|
|
562
|
-
console.log('\x1b[32m✔
|
|
1687
|
+
clearAllSessions();
|
|
1688
|
+
console.log('\x1b[32m✔ Logged out and sessions cleared.\x1b[0m');
|
|
563
1689
|
break;
|
|
564
1690
|
|
|
565
1691
|
case 'help':
|
|
@@ -574,7 +1700,7 @@ switch (first) {
|
|
|
574
1700
|
helpCommand();
|
|
575
1701
|
break;
|
|
576
1702
|
}
|
|
577
|
-
taskCommand(taskText, { verbose, json }).catch(e => {
|
|
1703
|
+
taskCommand(taskText, { verbose, json, device: targetDevice, all: runOnAll }).catch(e => {
|
|
578
1704
|
console.error(`\x1b[31mError:\x1b[0m ${e.message}`);
|
|
579
1705
|
process.exit(1);
|
|
580
1706
|
});
|