vg-coder-cli 2.0.66 → 2.0.68

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/INTEGRATION.md CHANGED
@@ -217,6 +217,8 @@ Server có thể chủ động list / đóng / mở tab AI Studio trong từng p
217
217
  | `GET` | `/api/launcher/tabs` | `?chromeId=<uuid>` \| `?label=<email>` (optional) | List tabs trong profile (hoặc all profiles nếu bỏ pin) |
218
218
  | `POST` | `/api/launcher/close-tab` | `{ chromeId? \| workerLabel?, tabId?, all? }` | Đóng tab cụ thể, hoặc tất cả tab AI Studio nếu bỏ `tabId` |
219
219
  | `POST` | `/api/launcher/open-tab` | `{ chromeId? \| workerLabel?, model?, url?, active? }` | Mở tab mới. `model` mặc định `gemini-3-flash-preview`. Response v2.0.52+ kèm `requested_model` / `actual_model` / `fallback_occurred` (URL-based, **không reliable** với AI Studio versions mới — xem note) |
220
+ | `GET` | `/api/launcher/debug` | `?chromeId=<uuid>` \| `?workerLabel=<email>` (optional) | Dump launcher SW state: `swChromeId`, `syncStorage`, `localStorage`, AI Studio tabs, windows count, runtime info. Bỏ pin → broadcast all launchers. (v2.0.65+) |
221
+ | `POST` | `/api/launcher/exec` | `{ chromeId? \| workerLabel? \| all?, cmd, args?, timeoutMs? }` | Chạy 1 lệnh `chrome.*` đã định nghĩa sẵn trong launcher SW. (v2.0.66+, thay cho `eval` bị Manifest V3 CSP chặn) |
220
222
 
221
223
  **Pin precedence**: `chromeId` > `workerLabel` > default. `chromeId` chỉ exact-match một
222
224
  launcher cụ thể — dùng để address **profile chưa bind email** (mới cài extension,
@@ -243,6 +245,98 @@ curl -X POST -d '{"workerLabel":"alice@gmail.com","model":"gemini-3-flash-previe
243
245
  -H 'Content-Type: application/json' http://127.0.0.1:6868/api/launcher/open-tab
244
246
  ```
245
247
 
248
+ ### Remote launcher debug (v2.0.65+)
249
+
250
+ Hai endpoint dùng để inspect / điều khiển launcher SW từ xa thay vì phải mở
251
+ DevTools trong từng Chrome profile:
252
+
253
+ **`GET /api/launcher/debug`** — dump state cố định:
254
+
255
+ ```bash
256
+ # Một launcher cụ thể
257
+ curl -s "http://127.0.0.1:6868/api/launcher/debug?chromeId=6b420bac-..." | jq
258
+
259
+ # Broadcast 5 launchers (bỏ pin)
260
+ curl -s http://127.0.0.1:6868/api/launcher/debug | jq
261
+ ```
262
+
263
+ Response per launcher:
264
+
265
+ ```json
266
+ {
267
+ "ok": true,
268
+ "data": {
269
+ "swChromeId": "6b420bac-...",
270
+ "runtimeId": "comfeilpfnlaoijgndaikpniioglmonf",
271
+ "extVersion": "1.0.0",
272
+ "extName": "VetGo Pro",
273
+ "syncStorage": { "id": "6b420bac-..." },
274
+ "localStorage": {},
275
+ "profileInfo": null,
276
+ "socketId": "KFh5YmiCbYTGBmAzAAAB",
277
+ "serverUrl": "http://127.0.0.1:6868",
278
+ "tabs": {
279
+ "aistudio": [{ "id": 437926431, "url": "...", "status": "complete", "active": true }],
280
+ "totalCount": 12
281
+ },
282
+ "windows": 2,
283
+ "userAgent": "Mozilla/5.0 ..."
284
+ }
285
+ }
286
+ ```
287
+
288
+ Dùng để verify chromeId pipeline (launcher SW chromeId == `chrome.storage.sync.id`),
289
+ phát hiện profile có nhiều cửa sổ / quá nhiều tab, debug AI Studio tab missing.
290
+
291
+ **`POST /api/launcher/exec`** — chạy 1 lệnh đã định nghĩa sẵn trong launcher SW.
292
+ Manifest V3 cấm `new Function()` nên không có eval tự do — đây là tập 10 lệnh
293
+ chrome.* được whitelist, mỗi lệnh có schema args cố định:
294
+
295
+ | `cmd` | `args` | Trả về |
296
+ |---|---|---|
297
+ | `tabs.query` | `chrome.tabs.QueryInfo` (`{ url?, active?, ... }`) | Mảng tab info `{ id, windowId, url, title, status, active, ... }` |
298
+ | `tabs.get` | `{ tabId: number }` | 1 tab info |
299
+ | `tabs.update` | `{ tabId: number, ...props }` (`url`, `active`, `pinned`, ...) | `{ id, url, active }` |
300
+ | `tabs.reload` | `{ tabId: number, bypassCache?: bool }` | `{ ok: true, tabId }` |
301
+ | `storage.sync.get` | `{ keys?: string \| string[] \| null }` (null = all) | `Record<string, unknown>` |
302
+ | `storage.sync.set` | `{ items: Record<string, unknown> }` | `{ ok: true }` |
303
+ | `storage.local.get` | `{ keys?: ... }` | `Record<string, unknown>` |
304
+ | `runtime.reload` | — | `{ ok: true, scheduled: true }` (extension reload sau ~50ms; ack có thể không tới do SW chết) |
305
+ | `windows.list` | — | Mảng window info `{ id, focused, type, state, incognito }` |
306
+ | `cookies.get` | `{ url: string, name?: string }` | 1 cookie nếu `name`, mảng cookies nếu chỉ `url`. Mỗi cookie: `{ name, value, domain, path, expirationDate, secure, httpOnly }` |
307
+
308
+ Unknown `cmd` → `{ ok: false, error: "unknown_cmd", available: [...] }` (discovery).
309
+
310
+ Ví dụ:
311
+
312
+ ```bash
313
+ # Phải có chromeId nào sync với storage UUID không?
314
+ curl -s -X POST -H 'Content-Type: application/json' \
315
+ -d '{"chromeId":"6b420bac-...","cmd":"storage.sync.get"}' \
316
+ http://127.0.0.1:6868/api/launcher/exec | jq '.value'
317
+ # → { "id": "6b420bac-..." }
318
+
319
+ # Profile login Google chưa?
320
+ curl -s -X POST -H 'Content-Type: application/json' \
321
+ -d '{"chromeId":"6b420bac-...","cmd":"cookies.get","args":{"url":"https://accounts.google.com","name":"SID"}}' \
322
+ http://127.0.0.1:6868/api/launcher/exec | jq '.value'
323
+ # → { "name": "SID", "value": "...", ... } nếu đã login, null nếu chưa
324
+
325
+ # Reload extension (debug last resort)
326
+ curl -s -X POST -H 'Content-Type: application/json' \
327
+ -d '{"chromeId":"6b420bac-...","cmd":"runtime.reload"}' \
328
+ http://127.0.0.1:6868/api/launcher/exec
329
+
330
+ # Broadcast: tab AI Studio đang mở ở profile nào?
331
+ curl -s -X POST -H 'Content-Type: application/json' \
332
+ -d '{"all":true,"cmd":"tabs.query","args":{"url":"*://aistudio.google.com/*"}}' \
333
+ http://127.0.0.1:6868/api/launcher/exec \
334
+ | jq '.[] | { chromeId: .launcher.chromeId, tabs: (.value | length) }'
335
+ ```
336
+
337
+ **Thêm command mới** → sửa `execCommands` map trong [vetgo-auto/chrome/src/launcher.ts](vetgo-auto/chrome/src/launcher.ts),
338
+ rebuild + bump version + reload extension trong từng Chrome profile.
339
+
246
340
  `open-tab` response (v2.0.52+):
247
341
 
248
342
  ```json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vg-coder-cli",
3
- "version": "2.0.66",
3
+ "version": "2.0.68",
4
4
  "description": "🚀 CLI tool to analyze projects, concatenate source files, count tokens, and export HTML with syntax highlighting and copy functionality",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -99,7 +99,12 @@ class ApiServer {
99
99
 
100
100
  // Task worker registration & lifecycle
101
101
  socket.on('worker:register', (meta) => { taskQueue.setWorker(socket, meta || {}); });
102
- socket.on('worker:heartbeat', () => { /* keep-alive only */ });
102
+ socket.on('worker:heartbeat', () => {
103
+ // Bump lastSeen so the stale-worker reaper doesn't drop a live
104
+ // socket that's just been quiet (no task in flight).
105
+ const w = taskQueue.workers.get(socket.id);
106
+ if (w) w.lastSeen = Date.now();
107
+ });
103
108
  socket.on('task:complete', (payload) => {
104
109
  if (payload?.taskId) taskQueue.onWorkerComplete(payload.taskId, payload, socket.id);
105
110
  });
@@ -799,6 +804,31 @@ class ApiServer {
799
804
  } catch (e) { res.status(launcherErr(e)).json({ error: e.message }); }
800
805
  });
801
806
 
807
+ // Block until the pinned launcher is ready for tasks. Replaces the common
808
+ // open-tab → poll-launchers → poll-workers pattern with a single sync
809
+ // call. Body: { chromeId, timeoutMs?: 15000, requireEmail?: true,
810
+ // requireWorker?: true }
811
+ // - 200 { ok: true, launcher, worker, elapsedMs } when ready
812
+ // - 404 launcher_not_found if chromeId never matched a launcher
813
+ // - 504 wait_ready_timeout if the readiness condition didn't hold in time
814
+ this.app.post('/api/launcher/wait-ready', async (req, res) => {
815
+ try {
816
+ const body = req.body || {};
817
+ const chromeId = (body.chromeId || '').toString().trim();
818
+ if (!chromeId) return res.status(400).json({ error: 'chromeId required' });
819
+ const result = await taskQueue.waitReady(chromeId, {
820
+ timeoutMs: body.timeoutMs,
821
+ requireEmail: body.requireEmail,
822
+ requireWorker: body.requireWorker,
823
+ });
824
+ res.json(result);
825
+ } catch (e) {
826
+ if (e.code === 'launcher_not_found') return res.status(404).json({ error: 'launcher_not_found' });
827
+ if (e.code === 'wait_ready_timeout') return res.status(504).json({ error: 'wait_ready_timeout', details: e.details });
828
+ res.status(500).json({ error: e.message });
829
+ }
830
+ });
831
+
802
832
  // Run a predefined chrome.* command inside the launcher SW. Manifest V3
803
833
  // CSP forbids new Function() in service workers, so eval was replaced by
804
834
  // a fixed command map. Body:
@@ -9,6 +9,13 @@ const AUTO_LAUNCH_DEBOUNCE_MS = 30_000;
9
9
  const INTER_TASK_GAP_MS = 2_500;
10
10
  const RATE_LIMIT_COOLDOWN_MS = 30 * 60_000;
11
11
  const COOLDOWN_TICK_MS = 60_000;
12
+ // Reap workers whose socket went silent: no heartbeat AND no task activity
13
+ // past this window. Heartbeat fires every 5s from task-worker.js, so 120s
14
+ // is ~24 missed beats — comfortably past any legitimate network hiccup but
15
+ // short enough that zombie sockets (Chrome tab closed without disconnect
16
+ // event reaching server, content-script paused by power-saver, etc.) don't
17
+ // linger forever and bloat _drain's idleWorkers snapshot.
18
+ const STALE_WORKER_TTL_MS = 120_000;
12
19
  const MAX_TASK_ATTEMPTS = 3;
13
20
  const RL_CODES = new Set(['rate_limit_exceeded', 'quota_exceeded', 'generation_failed']);
14
21
 
@@ -269,6 +276,62 @@ class TaskQueue {
269
276
  }
270
277
  }
271
278
 
279
+ /**
280
+ * Poll until a launcher's profile is "ready" for tasks: launcher connected,
281
+ * email scraped (if requireEmail), and ≥1 idle worker registered for the
282
+ * chromeId (if requireWorker). Saves callers from running their own
283
+ * GET /api/launchers + /api/workers poll loop after open-tab.
284
+ *
285
+ * Returns a snapshot on success; throws on timeout or immediate-miss.
286
+ */
287
+ async waitReady(chromeId, opts = {}) {
288
+ const timeoutMs = Math.min(Math.max(opts.timeoutMs | 0 || 15_000, 1_000), 60_000);
289
+ const requireEmail = opts.requireEmail !== false;
290
+ const requireWorker = opts.requireWorker !== false;
291
+ const pollMs = 250;
292
+ const deadline = Date.now() + timeoutMs;
293
+
294
+ // Immediate-miss: chromeId never matched a launcher → 404 contract
295
+ // (vs. timeout). Caller can map differently.
296
+ const hasLauncher = () => [...this.launchers.values()].some(l => l.chromeId === chromeId);
297
+ if (!hasLauncher()) {
298
+ const err = new Error('launcher_not_found');
299
+ err.code = 'launcher_not_found';
300
+ throw err;
301
+ }
302
+
303
+ while (Date.now() < deadline) {
304
+ const launcher = [...this.launchers.values()].find(l => l.chromeId === chromeId);
305
+ if (launcher) {
306
+ const emailOk = !requireEmail || (launcher.email && !launcher.email.startsWith('unknown:'));
307
+ const worker = !requireWorker ? null : [...this.workers.values()].find(w =>
308
+ w.chromeId === chromeId
309
+ && w.status === 'idle'
310
+ && w.email && !w.email.startsWith('unknown:')
311
+ );
312
+ const workerOk = !requireWorker || !!worker;
313
+ if (emailOk && workerOk) {
314
+ return {
315
+ ok: true,
316
+ launcher: { id: launcher.id, chromeId: launcher.chromeId, email: launcher.email },
317
+ worker: worker ? { id: worker.id, email: worker.email, status: worker.status } : null,
318
+ elapsedMs: timeoutMs - (deadline - Date.now()),
319
+ };
320
+ }
321
+ } else {
322
+ // Launcher disconnected during the wait — surface as not_found, not timeout.
323
+ const err = new Error('launcher_not_found');
324
+ err.code = 'launcher_not_found';
325
+ throw err;
326
+ }
327
+ await new Promise(r => setTimeout(r, pollMs));
328
+ }
329
+ const err = new Error('wait_ready_timeout');
330
+ err.code = 'wait_ready_timeout';
331
+ err.details = { chromeId, requireEmail, requireWorker, timeoutMs };
332
+ throw err;
333
+ }
334
+
272
335
  // Reverse lookup: find chromeId bound to email. Returns the first match —
273
336
  // if two launchers login the same email (test vs prod), this is ambiguous
274
337
  // and the caller should prefer explicit chromeId routing instead.
@@ -818,13 +881,23 @@ class TaskQueue {
818
881
  }
819
882
 
820
883
  async _dispatch(task, worker) {
884
+ // Mark worker busy synchronously BEFORE any await so that concurrent
885
+ // _drain() ticks (each enqueue schedules its own setImmediate(_drain))
886
+ // don't both observe the same worker as idle and dispatch two tasks to
887
+ // it. Previously `worker.status = 'busy'` happened after `await
888
+ // store.saveTask` — the await yielded, a second drain saw worker idle,
889
+ // and a second task was dispatched onto the same worker. The worker can
890
+ // only run one task at a time, so the second one would hang in cache
891
+ // status='running' forever (worker.currentTaskId was overwritten and
892
+ // onWorkerComplete's `worker.currentTaskId !== taskId` guard rejected
893
+ // the late completion as stale).
821
894
  task.status = 'running';
822
895
  task.timing.startedAt = Date.now();
823
896
  task.worker = { socketId: worker.id, email: worker.email, ...(worker.meta || {}) };
824
- await store.saveTask(task);
825
897
  this.cache.set(task.id, task);
826
898
  worker.currentTaskId = task.id;
827
899
  worker.status = 'busy';
900
+ await store.saveTask(task);
828
901
 
829
902
  const port = this.io?.httpServer?.address?.()?.port || 6868;
830
903
  const baseUrl = `http://127.0.0.1:${port}`;
@@ -863,9 +936,33 @@ class TaskQueue {
863
936
  console.log(chalk.green(`[TaskQueue] Worker ${w.email} cooldown ended`));
864
937
  }
865
938
  }
939
+ this._reapStaleWorkers(now);
866
940
  if (revived) setImmediate(() => this._drain());
867
941
  }
868
942
 
943
+ // Drop workers whose socket has been silent past STALE_WORKER_TTL_MS. Skips
944
+ // busy/recycling workers — those are mid-task or mid-recycle and may
945
+ // legitimately not heartbeat until they emit task:complete. Reaping a
946
+ // zombie worker also triggers _drain because the removal frees a slot
947
+ // that the legitimate launcher can refill when it reconnects.
948
+ _reapStaleWorkers(now = Date.now()) {
949
+ let reaped = 0;
950
+ for (const w of [...this.workers.values()]) {
951
+ if (w.status === 'busy' || w.status === 'recycling') continue;
952
+ const idleFor = now - (w.lastSeen || 0);
953
+ if (idleFor <= STALE_WORKER_TTL_MS) continue;
954
+ console.log(chalk.yellow(
955
+ `[TaskQueue] Reaping stale worker ${w.id} (${w.email || 'unknown'}, idle ${Math.round(idleFor / 1000)}s)`
956
+ ));
957
+ // Drop from registry. Don't try to disconnect the socket — if it's
958
+ // genuinely alive, _drain will skip dispatching to it, and the next
959
+ // worker:heartbeat or worker:register from that socket re-adds it.
960
+ this.workers.delete(w.id);
961
+ reaped++;
962
+ }
963
+ if (reaped) setImmediate(() => this._drain());
964
+ }
965
+
869
966
  _autoLaunchBrowser(task = null) {
870
967
  const now = Date.now();
871
968
  if (now - this.launchedAt < AUTO_LAUNCH_DEBOUNCE_MS) return;