@lightcone-ai/daemon 0.15.59 → 0.15.61

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.15.59",
3
+ "version": "0.15.61",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -2,6 +2,7 @@ import { spawn } from 'child_process';
2
2
  import { createHash } from 'crypto';
3
3
  import {
4
4
  existsSync,
5
+ cpSync,
5
6
  mkdirSync,
6
7
  readFileSync,
7
8
  readdirSync,
@@ -179,6 +180,48 @@ export class AgentManager {
179
180
  return `${workspaceId ?? ''}:${agentId}`;
180
181
  }
181
182
 
183
+ /**
184
+ * Snapshot of agents this daemon process is currently managing. Sent to the
185
+ * server alongside 'ready' so the server can reconcile lifecycle state
186
+ * after a reconnect — agents previously on this machine but absent from
187
+ * the inventory get reset from stale `crashed unreachable` back to a clean
188
+ * `standby reachable available` baseline (machine is healthy, agent will
189
+ * be spawned on demand).
190
+ *
191
+ * Each entry mirrors the daemon-internal agent state machine:
192
+ * - 'starting': spawn in progress (entry exists in this.starting)
193
+ * - 'running': child process is alive (this.agents has it)
194
+ * The server treats whatever is NOT in this list as 'standby' (spawnable
195
+ * but not currently running on this machine).
196
+ */
197
+ getAgentInventory() {
198
+ const inventory = [];
199
+ for (const key of this.starting) {
200
+ const colonIdx = key.indexOf(':');
201
+ const workspaceId = colonIdx === -1 ? null : (key.slice(0, colonIdx) || null);
202
+ const agentId = colonIdx === -1 ? key : key.slice(colonIdx + 1);
203
+ inventory.push({
204
+ agentId,
205
+ workspaceId,
206
+ runtimeState: 'starting',
207
+ });
208
+ }
209
+ for (const [key, agent] of this.agents.entries()) {
210
+ const colonIdx = key.indexOf(':');
211
+ const workspaceId = agent.workspaceId
212
+ ?? (colonIdx === -1 ? null : (key.slice(0, colonIdx) || null));
213
+ const agentId = agent.agentId
214
+ ?? (colonIdx === -1 ? key : key.slice(colonIdx + 1));
215
+ inventory.push({
216
+ agentId,
217
+ workspaceId,
218
+ runtimeState: 'running',
219
+ sessionId: agent.sessionId ?? null,
220
+ });
221
+ }
222
+ return inventory;
223
+ }
224
+
182
225
  handle(msg, connection) {
183
226
  switch (msg.type) {
184
227
  case 'agent:start': return this._startAgent(msg, connection);
@@ -188,6 +231,7 @@ export class AgentManager {
188
231
  case 'agent:reprobe': return this._reprobeCapability(msg, connection);
189
232
  case 'publish:job': return this._handlePublishJob(msg, connection);
190
233
  case 'browser:start_login': return this._startBrowserLogin(msg, connection);
234
+ case 'browser:bind_profile': return this._bindBrowserProfile(msg, connection);
191
235
  case 'browser:stop_login': return this._stopBrowserLogin(msg);
192
236
  case 'policy_invalidate': return this._handlePolicyInvalidate(msg, connection);
193
237
  case 'credential_revoked': return this._handleCredentialRevoked(msg, connection);
@@ -1464,6 +1508,34 @@ export class AgentManager {
1464
1508
  }
1465
1509
  }
1466
1510
 
1511
+ _bindBrowserProfile(msg, connection) {
1512
+ const platform = String(msg.platform ?? 'xhs').trim() || 'xhs';
1513
+ const credentialId = String(msg.credentialId ?? '').trim();
1514
+ const sourceProfileDir = String(msg.sourceProfileDir ?? '').trim();
1515
+ if (!credentialId || !sourceProfileDir) {
1516
+ connection.send({
1517
+ type: 'browser:login_error',
1518
+ platform,
1519
+ error: 'browser profile bind missing credentialId/sourceProfileDir',
1520
+ });
1521
+ return;
1522
+ }
1523
+ const targetProfileDir = path.join(homedir(), '.lightcone', 'chrome-profiles', `cred-${credentialId}`);
1524
+ try {
1525
+ if (!existsSync(sourceProfileDir)) {
1526
+ throw new Error(`source profile does not exist: ${sourceProfileDir}`);
1527
+ }
1528
+ rmSync(targetProfileDir, { recursive: true, force: true });
1529
+ mkdirSync(path.dirname(targetProfileDir), { recursive: true });
1530
+ cpSync(sourceProfileDir, targetProfileDir, { recursive: true });
1531
+ console.log(`[AgentManager] Bound browser profile platform=${platform} credential=${credentialId}`);
1532
+ connection.send({ type: 'browser:profile_bound', platform, credentialId });
1533
+ } catch (err) {
1534
+ console.error(`[AgentManager] Failed to bind browser profile (${platform}):`, err.message);
1535
+ connection.send({ type: 'browser:login_error', platform, error: `profile_bind_failed:${err.message}` });
1536
+ }
1537
+ }
1538
+
1467
1539
  _stopBrowserLogin(msg) {
1468
1540
  const platform = msg.platform ?? 'xhs';
1469
1541
  console.log(`[AgentManager] Stopping browser login for platform=${platform}`);
@@ -461,6 +461,77 @@ export class BrowserLoginSession {
461
461
  return true;
462
462
  }
463
463
 
464
+ async _extractAccountIdentity(cookies = []) {
465
+ if (this._platform !== 'xhs') return null;
466
+ const result = await this.send('Runtime.evaluate', {
467
+ expression: `(() => {
468
+ const out = { externalAccountId: '', externalDisplayName: '', externalAvatarUrl: '' };
469
+ const seen = new Set();
470
+ const candidates = [];
471
+ const push = (value, source) => {
472
+ if (value == null) return;
473
+ const text = String(value).trim();
474
+ if (!text || text.length < 3 || text.length > 160 || seen.has(text)) return;
475
+ seen.add(text);
476
+ candidates.push({ value: text, source });
477
+ };
478
+ const visit = (obj, source, depth = 0) => {
479
+ if (!obj || depth > 5) return;
480
+ if (typeof obj === 'string') {
481
+ const trimmed = obj.trim();
482
+ if (!trimmed) return;
483
+ if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
484
+ try { visit(JSON.parse(trimmed), source, depth + 1); } catch {}
485
+ }
486
+ return;
487
+ }
488
+ if (typeof obj !== 'object') return;
489
+ for (const [key, value] of Object.entries(obj)) {
490
+ const normalizedKey = String(key).toLowerCase();
491
+ if (['userid', 'user_id', 'user id', 'creatorid', 'creator_id', 'redid', 'red_id', 'authorid', 'author_id', 'accountid', 'account_id'].includes(normalizedKey)) {
492
+ push(value, source + ':' + key);
493
+ }
494
+ if (!out.externalDisplayName && ['nickname', 'nick_name', 'name', 'username', 'user_name'].includes(normalizedKey) && typeof value === 'string') {
495
+ out.externalDisplayName = value.trim().slice(0, 80);
496
+ }
497
+ if (!out.externalAvatarUrl && ['avatar', 'avatarurl', 'avatar_url', 'image'].includes(normalizedKey) && typeof value === 'string' && /^https?:\\/\\//.test(value)) {
498
+ out.externalAvatarUrl = value.trim();
499
+ }
500
+ if (value && typeof value === 'object') visit(value, source + ':' + key, depth + 1);
501
+ else if (typeof value === 'string' && value.length < 5000) visit(value, source + ':' + key, depth + 1);
502
+ }
503
+ };
504
+ for (const storage of [localStorage, sessionStorage]) {
505
+ for (let i = 0; i < storage.length; i += 1) {
506
+ const key = storage.key(i);
507
+ const value = storage.getItem(key);
508
+ const lk = String(key || '').toLowerCase();
509
+ if (/(user|account|creator|red)/.test(lk)) {
510
+ push(value, 'storage:' + key);
511
+ visit(value, 'storage:' + key);
512
+ }
513
+ }
514
+ }
515
+ for (const key of ['__INITIAL_STATE__', '__INITIAL_DATA__', '__REDUX_STATE__']) {
516
+ if (window[key]) visit(window[key], 'window:' + key);
517
+ }
518
+ out.externalAccountId = candidates[0]?.value || '';
519
+ out.identitySource = candidates[0]?.source || '';
520
+ return out;
521
+ })()`,
522
+ returnByValue: true,
523
+ }, 5000);
524
+ const value = result?.result?.value ?? {};
525
+ const externalAccountId = String(value.externalAccountId ?? '').trim();
526
+ if (!externalAccountId) return null;
527
+ return {
528
+ externalAccountId,
529
+ externalDisplayName: String(value.externalDisplayName ?? '').trim(),
530
+ externalAvatarUrl: String(value.externalAvatarUrl ?? '').trim(),
531
+ identitySource: String(value.identitySource ?? (value.externalAccountId ? 'page' : 'session_cookie')).trim(),
532
+ };
533
+ }
534
+
464
535
  _startPolling(connection, baselineSession) {
465
536
  let _screenshotInProgress = false;
466
537
  this._screenshotTimer = setInterval(async () => {
@@ -485,10 +556,11 @@ export class BrowserLoginSession {
485
556
  try {
486
557
  const cookieResult = await this.send('Network.getAllCookies', {});
487
558
  const baseDomain = new URL(this._config.loginUrl).hostname.split('.').slice(-2).join('.');
488
- const cookies = (cookieResult.cookies ?? []).filter(c =>
489
- c.domain.includes(baseDomain)
490
- );
491
- writeFileSync(path.join(this._profileDir, 'cookies.json'), JSON.stringify(cookies));
559
+ const cookies = (cookieResult.cookies ?? []).filter(c =>
560
+ c.domain.includes(baseDomain)
561
+ );
562
+ var accountIdentity = await this._extractAccountIdentity(cookies);
563
+ writeFileSync(path.join(this._profileDir, 'cookies.json'), JSON.stringify(cookies));
492
564
  console.log(`[BrowserLogin][${this._platform}] Saved ${cookies.length} cookies to cookies.json`);
493
565
  if (this._platform === 'wechat_mp') {
494
566
  const hrefResult = await this.send('Runtime.evaluate', {
@@ -515,9 +587,14 @@ export class BrowserLoginSession {
515
587
  closePublisherSession(this._platform);
516
588
  console.log(`[BrowserLogin][${this._platform}] Closed stale publisher Chrome session after re-login`);
517
589
  } catch {}
518
- connection.send({ type: 'browser:login_complete', platform: this._platform, profileDir: this._profileDir });
519
- await this.close();
520
- }
590
+ await this.close();
591
+ connection.send({
592
+ type: 'browser:login_complete',
593
+ platform: this._platform,
594
+ profileDir: this._profileDir,
595
+ accountIdentity,
596
+ });
597
+ }
521
598
  } catch (err) {
522
599
  console.error(`[BrowserLogin][${this._platform}] Login check error:`, err.message);
523
600
  }
package/src/connection.js CHANGED
@@ -45,10 +45,18 @@ function parseOptionalDeviceHints() {
45
45
  }
46
46
 
47
47
  export class DaemonConnection {
48
- constructor({ serverUrl, machineApiKey, onMessage }) {
48
+ constructor({ serverUrl, machineApiKey, onMessage, getAgentInventory }) {
49
+ if (typeof getAgentInventory !== 'function') {
50
+ throw new TypeError('DaemonConnection: getAgentInventory callback is required');
51
+ }
49
52
  this.serverUrl = serverUrl.replace(/^http/, 'ws');
50
53
  this.machineApiKey = machineApiKey;
51
54
  this.onMessage = onMessage;
55
+ // Snapshot of agents this daemon is currently managing, sent at 'ready'
56
+ // time. The server uses it to reconcile stale lifecycle state after
57
+ // reconnects (see src/daemon/index.js 'ready' handler — agents on this
58
+ // machine but missing from inventory get reset from stale crashed→standby).
59
+ this.getAgentInventory = getAgentInventory;
52
60
  this.ws = null;
53
61
  this.reconnectDelay = RECONNECT_INITIAL;
54
62
  this.stopped = false;
@@ -111,7 +119,11 @@ export class DaemonConnection {
111
119
  ...parseOptionalDeviceHints(),
112
120
  };
113
121
 
114
- console.log(`[Connection] Ready host=${hostname} runtimes=[${runtimes.join(',')}] v${DAEMON_VERSION}`);
122
+ const agentInventory = this.getAgentInventory();
123
+ console.log(
124
+ `[Connection] Ready — host=${hostname} runtimes=[${runtimes.join(',')}] v${DAEMON_VERSION}`
125
+ + ` inventory=${agentInventory.length}`
126
+ );
115
127
  this.send({
116
128
  type: 'ready',
117
129
  hostname,
@@ -119,6 +131,7 @@ export class DaemonConnection {
119
131
  runtimes,
120
132
  daemonVersion: DAEMON_VERSION,
121
133
  deviceHints,
134
+ agentInventory,
122
135
  });
123
136
  }
124
137
 
package/src/index.js CHANGED
@@ -58,6 +58,7 @@ const connection = new DaemonConnection({
58
58
  serverUrl: SERVER_URL,
59
59
  machineApiKey: MACHINE_API_KEY,
60
60
  onMessage: (msg) => agentManager.handle(msg, connection),
61
+ getAgentInventory: () => agentManager.getAgentInventory(),
61
62
  });
62
63
 
63
64
  connection.connect();