@lightcone-ai/daemon 0.9.61 → 0.9.63

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.9.61",
3
+ "version": "0.9.63",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -5,7 +5,7 @@ import path from 'path';
5
5
  import { buildSystemPrompt } from './drivers/claude.js';
6
6
  import { buildCodexSpawn, buildCodexSystemPrompt, parseCodexLine } from './drivers/codex.js';
7
7
  import { buildKimiSpawn, buildKimiInitMessages, parseKimiLine, encodeKimiStdin } from './drivers/kimi.js';
8
- import { startSession, stopSession } from './browser-login.js';
8
+ import { startSession, stopSession, stopAllSessions } from './browser-login.js';
9
9
  import { buildSkillMcpServers } from './mcp-config.js';
10
10
 
11
11
  export class AgentManager {
@@ -35,11 +35,12 @@ export class AgentManager {
35
35
  }
36
36
  }
37
37
 
38
- stopAll() {
38
+ async stopAll() {
39
39
  for (const [, agent] of this.agents) {
40
40
  if (agent.proc) agent.proc.kill();
41
41
  }
42
42
  this.agents.clear();
43
+ await stopAllSessions();
43
44
  }
44
45
 
45
46
  // ── private ───────────────────────────────────────────────────────────────
@@ -355,7 +356,7 @@ export class AgentManager {
355
356
  _stopBrowserLogin(msg) {
356
357
  const platform = msg.platform ?? 'xhs';
357
358
  console.log(`[AgentManager] Stopping browser login for platform=${platform}`);
358
- stopSession(platform);
359
+ return stopSession(platform);
359
360
  }
360
361
 
361
362
  _stopAgent(agentId, teamId, connection) {
@@ -23,13 +23,29 @@ export const PLATFORM_CONFIGS = {
23
23
  qrTabSelector: ['.qrcode-box', '[class*="qrcode"]', '[class*="scan-login"]', '[class*="qr-login"]'],
24
24
  qrFallbackScript: `
25
25
  const loginBox = document.querySelector('.login-box-container') || document.body;
26
- const candidates = [...loginBox.querySelectorAll('img, canvas, svg, [role="img"]')];
27
- const el = candidates.find(e => {
26
+ const boxRect = loginBox.getBoundingClientRect();
27
+ const visible = (e) => {
28
28
  const r = e.getBoundingClientRect();
29
29
  const cs = getComputedStyle(e);
30
- return r.width >= 40 && r.width <= 100 && r.height >= 40 && r.height <= 100 && cs.cursor === 'pointer';
31
- });
32
- if (el) { el.click(); return 'corner-qr-icon'; }
30
+ return r.width >= 35 && r.width <= 110
31
+ && r.height >= 35 && r.height <= 110
32
+ && r.left >= boxRect.left
33
+ && r.top >= boxRect.top
34
+ && r.right <= boxRect.right + 4
35
+ && r.bottom <= boxRect.bottom + 4
36
+ && cs.display !== 'none'
37
+ && cs.visibility !== 'hidden';
38
+ };
39
+ const elements = [...loginBox.querySelectorAll('img, canvas, svg, [role="img"], div, span, button')].filter(visible);
40
+ const corner = elements
41
+ .map(e => ({ e, r: e.getBoundingClientRect() }))
42
+ .filter(({ r }) => r.top <= boxRect.top + 120 && r.right >= boxRect.right - 140)
43
+ .sort((a, b) => (b.r.right + boxRect.top - b.r.top) - (a.r.right + boxRect.top - a.r.top))[0]?.e;
44
+ if (corner) {
45
+ const target = corner.closest('button, [role="button"], a, div') || corner;
46
+ target.click();
47
+ return 'corner-qr-icon';
48
+ }
33
49
  return null;
34
50
  `,
35
51
  getSessionValue: (cookies) =>
@@ -119,6 +135,7 @@ export class BrowserLoginSession {
119
135
  this._loginCheckTimer = null;
120
136
  this._profileDir = null;
121
137
  this._profileLock = null;
138
+ this._closing = null;
122
139
  }
123
140
 
124
141
  async start(connection, userId) {
@@ -212,7 +229,7 @@ export class BrowserLoginSession {
212
229
  if (this._config.qrTabSelector) {
213
230
  try {
214
231
  const selectors = this._config.qrTabSelector;
215
- await this.send('Runtime.evaluate', {
232
+ const qrResult = await this.send('Runtime.evaluate', {
216
233
  expression: `(function() {
217
234
  const sels = ${JSON.stringify(selectors)};
218
235
  for (const s of sels) {
@@ -228,8 +245,11 @@ export class BrowserLoginSession {
228
245
  })()`,
229
246
  returnByValue: true,
230
247
  });
248
+ console.log(`[BrowserLogin][${this._platform}] QR switch result: ${qrResult.result?.value ?? 'not-found'}`);
231
249
  await sleep(1000);
232
- } catch {}
250
+ } catch (err) {
251
+ console.error(`[BrowserLogin][${this._platform}] QR switch failed: ${err.message}`);
252
+ }
233
253
  }
234
254
 
235
255
  // Record baseline session cookie value.
@@ -325,7 +345,7 @@ export class BrowserLoginSession {
325
345
  console.error(`[BrowserLogin][${this._platform}] Failed to save cookies: ${err.message}`);
326
346
  }
327
347
  connection.send({ type: 'browser:login_complete', platform: this._platform, profileDir: this._profileDir });
328
- this.close();
348
+ await this.close();
329
349
  }
330
350
  } catch (err) {
331
351
  console.error(`[BrowserLogin][${this._platform}] Login check error:`, err.message);
@@ -348,13 +368,17 @@ export class BrowserLoginSession {
348
368
  }
349
369
 
350
370
  async close() {
351
- this._stopTimers();
352
- // Use CDP Browser.close for graceful shutdown so Chrome flushes cookies to disk
353
- try { await this.send('Browser.close', {}, 3000); } catch {}
354
- await sleep(1000);
355
- if (this._ws) { try { this._ws.close(); } catch {} this._ws = null; }
356
- if (this._proc) { try { this._proc.kill('SIGKILL'); } catch {} this._proc = null; }
357
- if (this._profileLock) { this._profileLock.release(); this._profileLock = null; }
371
+ if (this._closing) return this._closing;
372
+ this._closing = (async () => {
373
+ this._stopTimers();
374
+ // Use CDP Browser.close for graceful shutdown so Chrome flushes cookies to disk
375
+ try { await this.send('Browser.close', {}, 3000); } catch {}
376
+ await sleep(1000);
377
+ if (this._ws) { try { this._ws.close(); } catch {} this._ws = null; }
378
+ if (this._proc) { try { this._proc.kill('SIGKILL'); } catch {} this._proc = null; }
379
+ if (this._profileLock) { this._profileLock.release(); this._profileLock = null; }
380
+ })();
381
+ return this._closing;
358
382
  }
359
383
  }
360
384
 
@@ -366,14 +390,26 @@ export function getSession(platform) { return _sessions.get(platform) ?? null; }
366
390
 
367
391
  export async function startSession(platform, connection, userId) {
368
392
  const existing = _sessions.get(platform);
369
- if (existing) { existing.close(); _sessions.delete(platform); }
393
+ if (existing) { await existing.close(); _sessions.delete(platform); }
370
394
  const session = new BrowserLoginSession(platform);
371
395
  _sessions.set(platform, session);
372
- await session.start(connection, userId);
373
- return session;
396
+ try {
397
+ await session.start(connection, userId);
398
+ return session;
399
+ } catch (err) {
400
+ _sessions.delete(platform);
401
+ await session.close();
402
+ throw err;
403
+ }
374
404
  }
375
405
 
376
- export function stopSession(platform) {
406
+ export async function stopSession(platform) {
377
407
  const session = _sessions.get(platform);
378
- if (session) { session.close(); _sessions.delete(platform); }
408
+ if (session) { await session.close(); _sessions.delete(platform); }
409
+ }
410
+
411
+ export async function stopAllSessions() {
412
+ const sessions = [..._sessions.values()];
413
+ _sessions.clear();
414
+ await Promise.allSettled(sessions.map(session => session.close()));
379
415
  }
package/src/index.js CHANGED
@@ -3,6 +3,7 @@ import 'dotenv/config';
3
3
  import { createRequire } from 'module';
4
4
  import { DaemonConnection } from './connection.js';
5
5
  import { AgentManager } from './agent-manager.js';
6
+ import { releaseProfileLocksForProcess } from './profile-lock.js';
6
7
 
7
8
  const { version } = createRequire(import.meta.url)('../package.json');
8
9
 
@@ -42,5 +43,18 @@ const connection = new DaemonConnection({
42
43
 
43
44
  connection.connect();
44
45
 
45
- process.on('SIGINT', () => { connection.stop(); agentManager.stopAll(); process.exit(0); });
46
- process.on('SIGTERM', () => { connection.stop(); agentManager.stopAll(); process.exit(0); });
46
+ let shuttingDown = false;
47
+ async function shutdown(signal) {
48
+ if (shuttingDown) return;
49
+ shuttingDown = true;
50
+ console.log(`[Daemon] Shutting down (${signal})`);
51
+ connection.stop();
52
+ try { await agentManager.stopAll(); } catch (err) { console.error('[Daemon] Shutdown error:', err.message); }
53
+ releaseProfileLocksForProcess();
54
+ process.exit(0);
55
+ }
56
+
57
+ process.on('SIGINT', () => { shutdown('SIGINT'); });
58
+ process.on('SIGTERM', () => { shutdown('SIGTERM'); });
59
+ process.on('SIGHUP', () => { shutdown('SIGHUP'); });
60
+ process.on('exit', () => { releaseProfileLocksForProcess(); });
@@ -1,6 +1,7 @@
1
1
  import { mkdirSync, rmSync, writeFileSync, readFileSync, existsSync, statSync } from 'fs';
2
2
  import { homedir } from 'os';
3
3
  import path from 'path';
4
+ import { randomUUID } from 'crypto';
4
5
 
5
6
  const LOCK_ROOT = path.join(homedir(), '.lightcone', 'profile-locks');
6
7
  const DEFAULT_STALE_MS = 20 * 60 * 1000;
@@ -37,6 +38,28 @@ function isStale(dir, staleMs) {
37
38
  }
38
39
  }
39
40
 
41
+ function pidIsAlive(pid) {
42
+ if (!Number.isInteger(pid) || pid <= 0) return false;
43
+ try {
44
+ process.kill(pid, 0);
45
+ return true;
46
+ } catch (err) {
47
+ return err.code === 'EPERM';
48
+ }
49
+ }
50
+
51
+ function isAbandoned(dir) {
52
+ const info = lockInfo(dir);
53
+ if (!info?.pid) return false;
54
+ return !pidIsAlive(info.pid);
55
+ }
56
+
57
+ function removeLockDir(dir) {
58
+ try { rmSync(dir, { recursive: true, force: true }); } catch {}
59
+ }
60
+
61
+ const heldLocks = new Map();
62
+
40
63
  export async function acquireProfileLock(platform, profileDir, {
41
64
  owner = 'unknown',
42
65
  timeoutMs = DEFAULT_TIMEOUT_MS,
@@ -50,21 +73,26 @@ export async function acquireProfileLock(platform, profileDir, {
50
73
  while (true) {
51
74
  try {
52
75
  mkdirSync(dir);
76
+ const token = randomUUID();
53
77
  const release = () => {
54
- try { rmSync(dir, { recursive: true, force: true }); } catch {}
78
+ const info = lockInfo(dir);
79
+ heldLocks.delete(dir);
80
+ if (!info || info.token === token || info.pid === process.pid) removeLockDir(dir);
55
81
  };
56
82
  writeFileSync(path.join(dir, 'owner.json'), JSON.stringify({
57
83
  platform,
58
84
  profileDir,
59
85
  owner,
60
86
  pid: process.pid,
87
+ token,
61
88
  createdAt: new Date().toISOString(),
62
89
  }, null, 2));
90
+ heldLocks.set(dir, token);
63
91
  return { release, dir };
64
92
  } catch (err) {
65
93
  if (err.code !== 'EEXIST') throw err;
66
- if (existsSync(dir) && isStale(dir, staleMs)) {
67
- try { rmSync(dir, { recursive: true, force: true }); } catch {}
94
+ if (existsSync(dir) && (isAbandoned(dir) || isStale(dir, staleMs))) {
95
+ removeLockDir(dir);
68
96
  continue;
69
97
  }
70
98
  if (Date.now() >= deadline) {
@@ -76,6 +104,14 @@ export async function acquireProfileLock(platform, profileDir, {
76
104
  }
77
105
  }
78
106
 
107
+ export function releaseProfileLocksForProcess() {
108
+ for (const [dir, token] of heldLocks) {
109
+ const info = lockInfo(dir);
110
+ if (!info || info.token === token || info.pid === process.pid) removeLockDir(dir);
111
+ heldLocks.delete(dir);
112
+ }
113
+ }
114
+
79
115
  export async function withProfileLock(platform, profileDir, options, fn) {
80
116
  const lock = await acquireProfileLock(platform, profileDir, options);
81
117
  try {