lunel-cli 0.1.118 → 0.1.119

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +226 -4
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -90,10 +90,13 @@ let aiManagerInitPromise = null;
90
90
  // Proxy tunnel management
91
91
  let currentSessionCode = null;
92
92
  let currentSessionPassword = null;
93
+ let currentManagerSessionId = null;
93
94
  let currentPrimaryGateway = DEFAULT_PROXY_URL;
94
95
  let activeGatewayUrl = DEFAULT_PROXY_URL;
96
+ let appPeerConnected = false;
95
97
  let shuttingDown = false;
96
98
  let activeV2Transport = null;
99
+ const runningAiSessions = new Set();
97
100
  const trackedEditorFiles = new Map();
98
101
  const trackedEditorDirectories = new Map();
99
102
  const pendingTrackedFileChecks = new Set();
@@ -302,7 +305,21 @@ async function readCliConfig() {
302
305
  rootDir: entry.rootDir,
303
306
  sessionCode: typeof entry.sessionCode === "string" ? entry.sessionCode : null,
304
307
  sessionPassword: entry.sessionPassword,
308
+ managerSessionId: typeof entry.managerSessionId === "string" ? entry.managerSessionId : null,
305
309
  savedAt: entry.savedAt,
310
+ pushDevices: Array.isArray(entry.pushDevices)
311
+ ? entry.pushDevices.filter((device) => (!!device
312
+ && typeof device.phoneId === "string"
313
+ && typeof device.notificationsEnabled === "boolean"
314
+ && typeof device.updatedAt === "number"
315
+ && (typeof device.expoPushToken === "string" || device.expoPushToken === null))).map((device) => ({
316
+ phoneId: device.phoneId,
317
+ expoPushToken: device.expoPushToken,
318
+ notificationsEnabled: device.notificationsEnabled,
319
+ platform: typeof device.platform === "string" ? device.platform : null,
320
+ updatedAt: device.updatedAt,
321
+ }))
322
+ : [],
306
323
  }))
307
324
  : [],
308
325
  };
@@ -317,7 +334,9 @@ async function readCliConfig() {
317
334
  }
318
335
  async function writeCliConfig(config) {
319
336
  await fs.mkdir(path.dirname(CLI_CONFIG_PATH), { recursive: true });
320
- await fs.writeFile(CLI_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
337
+ const tmpPath = `${CLI_CONFIG_PATH}.${process.pid}.${Date.now()}.tmp`;
338
+ await fs.writeFile(tmpPath, JSON.stringify(config, null, 2), "utf-8");
339
+ await fs.rename(tmpPath, CLI_CONFIG_PATH);
321
340
  cliConfigPromise = Promise.resolve(config);
322
341
  }
323
342
  let cliConfigPromise = null;
@@ -331,14 +350,17 @@ function getSavedSessionForRoot(config, rootDir) {
331
350
  const sessions = Array.isArray(config.sessions) ? config.sessions : [];
332
351
  return sessions.find((entry) => entry.rootDir === rootDir) || null;
333
352
  }
334
- async function saveSessionForRoot(sessionCode, sessionPassword) {
353
+ async function saveSessionForRoot(sessionCode, sessionPassword, managerSessionId) {
335
354
  const config = await getCliConfig();
336
355
  const sessions = Array.isArray(config.sessions) ? [...config.sessions] : [];
356
+ const previous = sessions.find((entry) => entry.rootDir === ROOT_DIR);
337
357
  const nextEntry = {
338
358
  rootDir: ROOT_DIR,
339
359
  sessionCode,
340
360
  sessionPassword,
361
+ managerSessionId,
341
362
  savedAt: Date.now(),
363
+ pushDevices: previous?.pushDevices ?? [],
342
364
  };
343
365
  const deduped = sessions.filter((entry) => entry.rootDir !== ROOT_DIR);
344
366
  deduped.unshift(nextEntry);
@@ -347,6 +369,192 @@ async function saveSessionForRoot(sessionCode, sessionPassword) {
347
369
  sessions: deduped.slice(0, 100),
348
370
  });
349
371
  }
372
+ async function savePushDeviceForCurrentSession(payload) {
373
+ if (!currentSessionPassword) {
374
+ throw Object.assign(new Error("No active session"), { code: "EUNAVAILABLE" });
375
+ }
376
+ const phoneId = typeof payload.phoneId === "string" ? payload.phoneId.trim() : "";
377
+ if (!phoneId) {
378
+ throw Object.assign(new Error("phoneId is required"), { code: "EINVAL" });
379
+ }
380
+ const expoPushToken = typeof payload.expoPushToken === "string" && payload.expoPushToken.trim()
381
+ ? payload.expoPushToken.trim()
382
+ : null;
383
+ const notificationsEnabled = payload.notificationsEnabled === true && Boolean(expoPushToken);
384
+ const platform = typeof payload.platform === "string" && payload.platform.trim()
385
+ ? payload.platform.trim()
386
+ : null;
387
+ const config = await getCliConfig();
388
+ const sessions = Array.isArray(config.sessions) ? [...config.sessions] : [];
389
+ const now = Date.now();
390
+ const currentEntry = sessions.find((entry) => entry.rootDir === ROOT_DIR) ?? {
391
+ rootDir: ROOT_DIR,
392
+ sessionCode: currentSessionCode,
393
+ sessionPassword: currentSessionPassword,
394
+ managerSessionId: currentManagerSessionId,
395
+ savedAt: now,
396
+ pushDevices: [],
397
+ };
398
+ const nextDevices = (currentEntry.pushDevices ?? []).filter((device) => device.phoneId !== phoneId);
399
+ nextDevices.unshift({
400
+ phoneId,
401
+ expoPushToken,
402
+ notificationsEnabled,
403
+ platform,
404
+ updatedAt: now,
405
+ });
406
+ const nextEntry = {
407
+ ...currentEntry,
408
+ sessionCode: currentSessionCode,
409
+ sessionPassword: currentSessionPassword,
410
+ managerSessionId: currentManagerSessionId,
411
+ savedAt: now,
412
+ pushDevices: nextDevices.slice(0, 10),
413
+ };
414
+ const deduped = sessions.filter((entry) => entry.rootDir !== ROOT_DIR);
415
+ deduped.unshift(nextEntry);
416
+ await writeCliConfig({
417
+ ...config,
418
+ sessions: deduped.slice(0, 100),
419
+ });
420
+ return { ok: true, notificationsEnabled, tokenStored: Boolean(expoPushToken) };
421
+ }
422
+ function readAiEventSessionId(event) {
423
+ const properties = event.properties || {};
424
+ const info = properties.info && typeof properties.info === "object" ? properties.info : {};
425
+ return ((typeof properties.sessionID === "string" && properties.sessionID)
426
+ || (typeof properties.sessionId === "string" && properties.sessionId)
427
+ || (typeof info.sessionID === "string" && info.sessionID)
428
+ || (typeof info.sessionId === "string" && info.sessionId)
429
+ || (typeof info.id === "string" && info.id)
430
+ || null);
431
+ }
432
+ function readAiStatusType(status) {
433
+ if (typeof status === "string")
434
+ return status.toLowerCase();
435
+ if (status && typeof status === "object") {
436
+ const value = status.type;
437
+ if (typeof value === "string")
438
+ return value.toLowerCase();
439
+ }
440
+ return "";
441
+ }
442
+ function isRunningAiStatus(status) {
443
+ const value = readAiStatusType(status);
444
+ return value === "running" || value === "busy" || value === "working" || value === "processing" || value === "retry";
445
+ }
446
+ function isTerminalAiStatus(status) {
447
+ const value = readAiStatusType(status);
448
+ return (value === "idle"
449
+ || value === "complete"
450
+ || value === "completed"
451
+ || value === "done"
452
+ || value === "finished"
453
+ || value === "error"
454
+ || value === "failed"
455
+ || value === "cancelled"
456
+ || value === "canceled");
457
+ }
458
+ async function getCurrentNotificationDevices() {
459
+ const config = await getCliConfig();
460
+ const saved = getSavedSessionForRoot(config, ROOT_DIR);
461
+ if (!saved || saved.sessionPassword !== currentSessionPassword)
462
+ return [];
463
+ return (saved.pushDevices ?? []).filter((device) => (device.notificationsEnabled
464
+ && typeof device.expoPushToken === "string"
465
+ && device.expoPushToken.length > 0));
466
+ }
467
+ async function clearInvalidNotificationDevices(phoneIds) {
468
+ if (phoneIds.length === 0)
469
+ return;
470
+ const invalidPhoneIds = new Set(phoneIds);
471
+ const config = await getCliConfig();
472
+ const sessions = Array.isArray(config.sessions) ? [...config.sessions] : [];
473
+ const saved = getSavedSessionForRoot(config, ROOT_DIR);
474
+ if (!saved?.pushDevices?.length)
475
+ return;
476
+ const nextEntry = {
477
+ ...saved,
478
+ pushDevices: saved.pushDevices.map((device) => (invalidPhoneIds.has(device.phoneId)
479
+ ? { ...device, expoPushToken: null, notificationsEnabled: false, updatedAt: Date.now() }
480
+ : device)),
481
+ };
482
+ const deduped = sessions.filter((entry) => entry.rootDir !== ROOT_DIR);
483
+ deduped.unshift(nextEntry);
484
+ await writeCliConfig({
485
+ ...config,
486
+ sessions: deduped.slice(0, 100),
487
+ });
488
+ }
489
+ async function notifyManagerAiCompletion(backend, aiSessionId) {
490
+ if (appPeerConnected)
491
+ return;
492
+ if (!currentManagerSessionId || !currentSessionPassword)
493
+ return;
494
+ const devices = await getCurrentNotificationDevices();
495
+ if (devices.length === 0)
496
+ return;
497
+ const response = await fetch(new URL("/v1/notifications/ai-complete", MANAGER_URL), {
498
+ method: "POST",
499
+ headers: { "Content-Type": "application/json" },
500
+ signal: AbortSignal.timeout(10_000),
501
+ body: JSON.stringify({
502
+ sessionId: currentManagerSessionId,
503
+ resumeToken: currentSessionPassword,
504
+ backend,
505
+ aiSessionId,
506
+ devices: devices.map((device) => ({
507
+ phoneId: device.phoneId,
508
+ expoPushToken: device.expoPushToken,
509
+ platform: device.platform,
510
+ })),
511
+ }),
512
+ });
513
+ if (!response.ok) {
514
+ if (DEBUG_MODE) {
515
+ console.warn(`[notifications] manager notify failed: ${response.status}`);
516
+ }
517
+ return;
518
+ }
519
+ const body = await response.json().catch(() => ({}));
520
+ const invalidPhoneIds = Array.isArray(body.invalidPhoneIds)
521
+ ? body.invalidPhoneIds.filter((value) => typeof value === "string")
522
+ : [];
523
+ if (invalidPhoneIds.length > 0) {
524
+ await clearInvalidNotificationDevices(invalidPhoneIds);
525
+ }
526
+ }
527
+ function handleAiNotificationEvent(backend, event) {
528
+ const sessionId = readAiEventSessionId(event);
529
+ if (!sessionId)
530
+ return;
531
+ const key = `${backend}:${sessionId}`;
532
+ if (event.type === "session.status") {
533
+ if (isRunningAiStatus(event.properties.status)) {
534
+ runningAiSessions.add(key);
535
+ return;
536
+ }
537
+ if (!isTerminalAiStatus(event.properties.status))
538
+ return;
539
+ if (!runningAiSessions.delete(key))
540
+ return;
541
+ void notifyManagerAiCompletion(backend, sessionId).catch((error) => {
542
+ if (DEBUG_MODE) {
543
+ console.warn("[notifications] failed to notify manager:", error instanceof Error ? error.message : String(error));
544
+ }
545
+ });
546
+ return;
547
+ }
548
+ if (event.type !== "session.idle")
549
+ return;
550
+ if (!runningAiSessions.delete(key))
551
+ return;
552
+ void notifyManagerAiCompletion(backend, sessionId).catch((error) => {
553
+ if (DEBUG_MODE) {
554
+ console.warn("[notifications] failed to notify manager:", error instanceof Error ? error.message : String(error));
555
+ }
556
+ });
557
+ }
350
558
  async function clearSavedSessionForRoot() {
351
559
  const config = await getCliConfig();
352
560
  const sessions = Array.isArray(config.sessions) ? config.sessions : [];
@@ -1533,6 +1741,7 @@ function handleSystemCapabilities() {
1533
1741
  platform: os.platform(),
1534
1742
  rootDir: ROOT_DIR,
1535
1743
  hostname: os.hostname(),
1744
+ managerSessionId: currentManagerSessionId,
1536
1745
  };
1537
1746
  }
1538
1747
  function handleSystemPing() {
@@ -2506,6 +2715,9 @@ async function processMessage(message) {
2506
2715
  case "ping":
2507
2716
  result = handleSystemPing();
2508
2717
  break;
2718
+ case "setPushToken":
2719
+ result = await savePushDeviceForCurrentSession(payload);
2720
+ break;
2509
2721
  case "pairDevice": {
2510
2722
  throw Object.assign(new Error("pairDevice is no longer supported"), { code: "EINVAL" });
2511
2723
  }
@@ -2895,7 +3107,11 @@ async function assembleWithCode(code) {
2895
3107
  return;
2896
3108
  settled = true;
2897
3109
  ws.send(JSON.stringify({ type: "ack" }));
2898
- resolve({ code: parsed.code, password: parsed.password });
3110
+ resolve({
3111
+ code: parsed.code,
3112
+ password: parsed.password,
3113
+ sessionId: typeof parsed.sessionId === "string" ? parsed.sessionId : null,
3114
+ });
2899
3115
  }
2900
3116
  catch (error) {
2901
3117
  fail(error instanceof Error ? error : new Error(String(error)));
@@ -3157,6 +3373,7 @@ function startAiManagerInBackground() {
3157
3373
  }
3158
3374
  aiManager = manager;
3159
3375
  aiManager.subscribe((backend, event) => {
3376
+ handleAiNotificationEvent(backend, event);
3160
3377
  emitAppEvent({
3161
3378
  v: 1,
3162
3379
  id: `evt-${Date.now()}`,
@@ -3196,16 +3413,19 @@ async function connectWebSocketV2() {
3196
3413
  if (message.type === "connected")
3197
3414
  return;
3198
3415
  if (message.type === "peer_connected") {
3416
+ appPeerConnected = true;
3199
3417
  console.log("App connected!\n");
3200
3418
  void publishDiscoveredPorts(true);
3201
3419
  return;
3202
3420
  }
3203
3421
  if (message.type === "peer_disconnected") {
3422
+ appPeerConnected = false;
3204
3423
  console.log("App disconnected. Waiting for reconnect window.\n");
3205
3424
  stopPortSync();
3206
3425
  return;
3207
3426
  }
3208
3427
  if (message.type === "app_disconnected") {
3428
+ appPeerConnected = false;
3209
3429
  if (message.reconnectDeadline) {
3210
3430
  console.log(`[session] app disconnected, waiting until ${new Date(message.reconnectDeadline).toISOString()}`);
3211
3431
  }
@@ -3310,6 +3530,7 @@ async function main() {
3310
3530
  displaySavedSessionNotice();
3311
3531
  sessionCodeToUse = savedSession.sessionCode;
3312
3532
  sessionPasswordToUse = savedSession.sessionPassword;
3533
+ currentManagerSessionId = savedSession.managerSessionId ?? null;
3313
3534
  usedSavedSession = true;
3314
3535
  }
3315
3536
  else {
@@ -3323,7 +3544,8 @@ async function main() {
3323
3544
  const assembled = await assembleWithCode(qr.code);
3324
3545
  sessionCodeToUse = assembled.code;
3325
3546
  sessionPasswordToUse = assembled.password;
3326
- await saveSessionForRoot(sessionCodeToUse, sessionPasswordToUse);
3547
+ currentManagerSessionId = assembled.sessionId;
3548
+ await saveSessionForRoot(sessionCodeToUse, sessionPasswordToUse, currentManagerSessionId);
3327
3549
  }
3328
3550
  currentSessionCode = sessionCodeToUse;
3329
3551
  currentSessionPassword = sessionPasswordToUse;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lunel-cli",
3
- "version": "0.1.118",
3
+ "version": "0.1.119",
4
4
  "author": [
5
5
  {
6
6
  "name": "Soham Bharambe",