@babylen/legion 0.1.3 → 0.1.5

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 +457 -51
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
  var import_child_process = require("child_process");
28
28
 
29
29
  // package.json
30
- var version = "0.1.3";
30
+ var version = "0.1.5";
31
31
 
32
32
  // src/index.ts
33
33
  var import_socket = require("socket.io-client");
@@ -43,7 +43,9 @@ var ConfigSchema = import_zod.z.object({
43
43
  // Token ID from database (for faster lookup)
44
44
  token: import_zod.z.string(),
45
45
  // Secret token (lg_xxx format)
46
- serverUrl: import_zod.z.string().url()
46
+ serverUrl: import_zod.z.string().url(),
47
+ allowedPaths: import_zod.z.array(import_zod.z.string()).optional()
48
+ // Whitelist paths for file system access
47
49
  });
48
50
  var HOME_DIR = import_os.default.homedir();
49
51
  var CONFIG_DIR = import_path.default.join(HOME_DIR, ".tanuki");
@@ -58,9 +60,14 @@ async function ensureConfigDir() {
58
60
  }
59
61
  async function loadConfig() {
60
62
  try {
61
- const content = await import_promises.default.readFile(CONFIG_FILE, "utf-8");
63
+ let content = await import_promises.default.readFile(CONFIG_FILE, "utf-8");
64
+ content = content.replace(/^\uFEFF/, "");
62
65
  const data2 = JSON.parse(content);
63
- return ConfigSchema.parse(data2);
66
+ const result = ConfigSchema.safeParse(data2);
67
+ if (!result.success) {
68
+ return null;
69
+ }
70
+ return result.data;
64
71
  } catch (error) {
65
72
  return null;
66
73
  }
@@ -75,6 +82,7 @@ async function saveConfig(config2) {
75
82
  }
76
83
  async function getConfig() {
77
84
  const fileConfig = await loadConfig();
85
+ console.log("[DEBUG] Loaded config object:", fileConfig ? "FOUND" : "NULL");
78
86
  const id = fileConfig?.id || process.env.LEGION_TOKEN_ID || void 0;
79
87
  const token = fileConfig?.token || process.env.LEGION_TOKEN_SECRET || process.env.LEGION_TOKEN;
80
88
  const serverUrl = fileConfig?.serverUrl || process.env.TANUKI_SERVER_URL || "wss://tanuki.sabw.ru";
@@ -90,8 +98,17 @@ async function getConfig() {
90
98
  if (id) {
91
99
  config2.id = id;
92
100
  }
101
+ if (fileConfig?.allowedPaths) {
102
+ config2.allowedPaths = fileConfig.allowedPaths;
103
+ }
93
104
  return config2;
94
105
  }
106
+ function getAllowedPaths(config2) {
107
+ if (config2.allowedPaths && config2.allowedPaths.length > 0) {
108
+ return config2.allowedPaths.map((p) => import_path.default.resolve(p));
109
+ }
110
+ return [HOME_DIR];
111
+ }
95
112
  async function updateConfig(updates) {
96
113
  const currentConfig = await loadConfig();
97
114
  if (!currentConfig) {
@@ -320,6 +337,389 @@ async function getSystemFingerprint() {
320
337
  return cachedFingerprint;
321
338
  }
322
339
 
340
+ // src/file/legion-bridge.ts
341
+ var import_promises4 = __toESM(require("fs/promises"));
342
+ var import_path4 = __toESM(require("path"));
343
+ var log = Log.create({ service: "file.legion-bridge" });
344
+ async function listFiles(targetPath, depth = 1) {
345
+ const resolved = import_path4.default.resolve(targetPath);
346
+ const exclude = [".git", ".DS_Store", ".tanuki"];
347
+ const nodes = [];
348
+ try {
349
+ const entries = await import_promises4.default.readdir(resolved, { withFileTypes: true });
350
+ for (const entry of entries) {
351
+ if (exclude.includes(entry.name)) continue;
352
+ const fullPath = import_path4.default.join(resolved, entry.name);
353
+ const isDirectory = entry.isDirectory();
354
+ const node = {
355
+ name: entry.name,
356
+ path: fullPath,
357
+ type: isDirectory ? "directory" : "file"
358
+ };
359
+ if (!isDirectory) {
360
+ try {
361
+ const stats = await import_promises4.default.stat(fullPath);
362
+ node.size = stats.size;
363
+ } catch {
364
+ }
365
+ }
366
+ nodes.push(node);
367
+ }
368
+ } catch (error) {
369
+ log.error("Failed to list directory", { error, targetPath });
370
+ throw error;
371
+ }
372
+ return nodes.sort((a, b) => {
373
+ if (a.type !== b.type) {
374
+ return a.type === "directory" ? -1 : 1;
375
+ }
376
+ return a.name.localeCompare(b.name);
377
+ });
378
+ }
379
+ async function shouldEncode(file) {
380
+ const type = file.type?.toLowerCase();
381
+ if (!type) return false;
382
+ if (type.startsWith("text/")) return false;
383
+ if (type.includes("charset=")) return false;
384
+ const parts = type.split("/", 2);
385
+ const top = parts[0];
386
+ const rest = parts[1] ?? "";
387
+ const sub = rest.split(";", 1)[0];
388
+ const tops = ["image", "audio", "video", "font", "model", "multipart"];
389
+ if (tops.includes(top)) return true;
390
+ const bins = [
391
+ "zip",
392
+ "gzip",
393
+ "bzip",
394
+ "compressed",
395
+ "binary",
396
+ "pdf",
397
+ "msword",
398
+ "vnd.ms",
399
+ "octet-stream"
400
+ ];
401
+ if (bins.includes(sub)) return true;
402
+ try {
403
+ const buffer = await file.arrayBuffer();
404
+ if (buffer.byteLength === 0) return false;
405
+ const view = new Uint8Array(buffer.slice(0, 512));
406
+ return view.some((byte) => byte === 0);
407
+ } catch {
408
+ return false;
409
+ }
410
+ }
411
+ async function readFile(filePath, maxSize = 1024 * 1024) {
412
+ const resolved = import_path4.default.resolve(filePath);
413
+ const bunFile = Bun.file(resolved);
414
+ if (!await bunFile.exists()) {
415
+ throw new Error(`File not found: ${filePath}`);
416
+ }
417
+ let stats;
418
+ try {
419
+ stats = await import_promises4.default.stat(resolved);
420
+ } catch (error) {
421
+ throw new Error(`Failed to stat file: ${filePath}`);
422
+ }
423
+ if (stats.size > maxSize) {
424
+ return {
425
+ type: "blob",
426
+ size: stats.size,
427
+ error: "too_large"
428
+ };
429
+ }
430
+ const encode = await shouldEncode(bunFile);
431
+ if (encode) {
432
+ const buffer = await bunFile.arrayBuffer();
433
+ const content2 = Buffer.from(buffer).toString("base64");
434
+ const mimeType = bunFile.type || "application/octet-stream";
435
+ return {
436
+ type: "blob",
437
+ content: content2,
438
+ encoding: "base64",
439
+ mimeType,
440
+ size: stats.size
441
+ };
442
+ }
443
+ const content = await bunFile.text().catch(() => "");
444
+ return {
445
+ type: "text",
446
+ content,
447
+ encoding: "utf-8",
448
+ size: stats.size
449
+ };
450
+ }
451
+
452
+ // src/core/path-validator.ts
453
+ var import_path6 = __toESM(require("path"));
454
+ var import_fs3 = require("fs");
455
+
456
+ // src/util/filesystem.ts
457
+ var import_fs2 = require("fs");
458
+ var import_path5 = require("path");
459
+ var Filesystem;
460
+ ((Filesystem2) => {
461
+ Filesystem2.exists = (p) => Bun.file(p).stat().then(() => true).catch(() => false);
462
+ Filesystem2.isDir = (p) => Bun.file(p).stat().then((s) => s.isDirectory()).catch(() => false);
463
+ function normalizePath(p) {
464
+ if (process.platform !== "win32") return p;
465
+ try {
466
+ return import_fs2.realpathSync.native(p);
467
+ } catch {
468
+ return p;
469
+ }
470
+ }
471
+ Filesystem2.normalizePath = normalizePath;
472
+ function overlaps(a, b) {
473
+ const relA = (0, import_path5.relative)(a, b);
474
+ const relB = (0, import_path5.relative)(b, a);
475
+ return !relA || !relA.startsWith("..") || !relB || !relB.startsWith("..");
476
+ }
477
+ Filesystem2.overlaps = overlaps;
478
+ function contains(parent, child) {
479
+ return !(0, import_path5.relative)(parent, child).startsWith("..");
480
+ }
481
+ Filesystem2.contains = contains;
482
+ async function findUp(target, start, stop) {
483
+ let current = start;
484
+ const result = [];
485
+ while (true) {
486
+ const search = (0, import_path5.join)(current, target);
487
+ if (await (0, Filesystem2.exists)(search)) result.push(search);
488
+ if (stop === current) break;
489
+ const parent = (0, import_path5.dirname)(current);
490
+ if (parent === current) break;
491
+ current = parent;
492
+ }
493
+ return result;
494
+ }
495
+ Filesystem2.findUp = findUp;
496
+ async function* up(options) {
497
+ const { targets, start, stop } = options;
498
+ let current = start;
499
+ while (true) {
500
+ for (const target of targets) {
501
+ const search = (0, import_path5.join)(current, target);
502
+ if (await (0, Filesystem2.exists)(search)) yield search;
503
+ }
504
+ if (stop === current) break;
505
+ const parent = (0, import_path5.dirname)(current);
506
+ if (parent === current) break;
507
+ current = parent;
508
+ }
509
+ }
510
+ Filesystem2.up = up;
511
+ async function globUp(pattern, start, stop) {
512
+ let current = start;
513
+ const result = [];
514
+ while (true) {
515
+ try {
516
+ const glob = new Bun.Glob(pattern);
517
+ for await (const match of glob.scan({
518
+ cwd: current,
519
+ absolute: true,
520
+ onlyFiles: true,
521
+ followSymlinks: true,
522
+ dot: true
523
+ })) {
524
+ result.push(match);
525
+ }
526
+ } catch {
527
+ }
528
+ if (stop === current) break;
529
+ const parent = (0, import_path5.dirname)(current);
530
+ if (parent === current) break;
531
+ current = parent;
532
+ }
533
+ return result;
534
+ }
535
+ Filesystem2.globUp = globUp;
536
+ })(Filesystem || (Filesystem = {}));
537
+
538
+ // src/core/path-validator.ts
539
+ var log2 = Log.create({ service: "path-validator" });
540
+ function isPathAllowed(requestedPath, allowedPaths) {
541
+ try {
542
+ const normalized = import_path6.default.resolve(requestedPath);
543
+ let realPath;
544
+ try {
545
+ realPath = (0, import_fs3.realpathSync)(normalized);
546
+ } catch {
547
+ realPath = normalized;
548
+ }
549
+ for (const allowed of allowedPaths) {
550
+ const allowedResolved = import_path6.default.resolve(allowed);
551
+ if (Filesystem.contains(allowedResolved, realPath)) {
552
+ return true;
553
+ }
554
+ }
555
+ log2.warn("Path access denied", { requestedPath, realPath, allowedPaths });
556
+ return false;
557
+ } catch (error) {
558
+ log2.error("Path validation error", { error, requestedPath });
559
+ return false;
560
+ }
561
+ }
562
+
563
+ // src/socket/handlers/fs-list.ts
564
+ async function handleFsList(req, socket) {
565
+ const { path: requestedPath, depth = 1 } = req;
566
+ const targetPath = requestedPath || process.cwd();
567
+ const config2 = socket.legionConfig;
568
+ const allowedPaths = getAllowedPaths(config2);
569
+ if (!isPathAllowed(targetPath, allowedPaths)) {
570
+ return {
571
+ id: req.id,
572
+ status: "error",
573
+ error: "Access denied: path not in whitelist"
574
+ };
575
+ }
576
+ try {
577
+ const files = await listFiles(targetPath, depth);
578
+ return {
579
+ id: req.id,
580
+ status: "ok",
581
+ data: files
582
+ };
583
+ } catch (error) {
584
+ return {
585
+ id: req.id,
586
+ status: "error",
587
+ error: error instanceof Error ? error.message : "Unknown error"
588
+ };
589
+ }
590
+ }
591
+
592
+ // src/socket/handlers/fs-read.ts
593
+ async function handleFsRead(req, socket) {
594
+ const { path: requestedPath, maxSize } = req;
595
+ if (!requestedPath) {
596
+ return {
597
+ id: req.id,
598
+ status: "error",
599
+ error: "Path is required"
600
+ };
601
+ }
602
+ const config2 = socket.legionConfig;
603
+ const allowedPaths = getAllowedPaths(config2);
604
+ if (!isPathAllowed(requestedPath, allowedPaths)) {
605
+ return {
606
+ id: req.id,
607
+ status: "error",
608
+ error: "Access denied: path not in whitelist"
609
+ };
610
+ }
611
+ try {
612
+ const content = await readFile(requestedPath, maxSize || 1024 * 1024);
613
+ return {
614
+ id: req.id,
615
+ status: "ok",
616
+ data: content
617
+ };
618
+ } catch (error) {
619
+ return {
620
+ id: req.id,
621
+ status: "error",
622
+ error: error instanceof Error ? error.message : "Unknown error"
623
+ };
624
+ }
625
+ }
626
+
627
+ // src/project/binding.ts
628
+ var import_promises5 = __toESM(require("fs/promises"));
629
+ var import_path7 = __toESM(require("path"));
630
+ var log3 = Log.create({ service: "project.binding" });
631
+ async function bindProject(targetPath, projectId, projectName) {
632
+ const resolved = import_path7.default.resolve(targetPath);
633
+ const tanukiDir = import_path7.default.join(resolved, ".tanuki");
634
+ const projectFile = import_path7.default.join(tanukiDir, "project.json");
635
+ await import_promises5.default.mkdir(tanukiDir, { recursive: true });
636
+ const config2 = {
637
+ id: projectId,
638
+ name: projectName || import_path7.default.basename(resolved)
639
+ };
640
+ await import_promises5.default.writeFile(projectFile, JSON.stringify(config2, null, 2), "utf-8");
641
+ const gitignorePath = import_path7.default.join(resolved, ".gitignore");
642
+ try {
643
+ let gitignoreContent = await import_promises5.default.readFile(gitignorePath, "utf-8");
644
+ if (!gitignoreContent.includes(".tanuki")) {
645
+ gitignoreContent += "\n.tanuki\n";
646
+ await import_promises5.default.writeFile(gitignorePath, gitignoreContent, "utf-8");
647
+ }
648
+ } catch {
649
+ await import_promises5.default.writeFile(gitignorePath, ".tanuki\n", "utf-8");
650
+ }
651
+ log3.info("Project bound", { path: resolved, projectId });
652
+ return projectFile;
653
+ }
654
+
655
+ // src/socket/handlers/project-bind.ts
656
+ async function handleProjectBind(req, socket) {
657
+ const { path: requestedPath, projectId, projectName } = req;
658
+ if (!requestedPath) {
659
+ return {
660
+ id: req.id,
661
+ status: "error",
662
+ error: "Path is required"
663
+ };
664
+ }
665
+ if (!projectId) {
666
+ return {
667
+ id: req.id,
668
+ status: "error",
669
+ error: "projectId is required"
670
+ };
671
+ }
672
+ const config2 = socket.legionConfig;
673
+ const allowedPaths = getAllowedPaths(config2);
674
+ if (!isPathAllowed(requestedPath, allowedPaths)) {
675
+ return {
676
+ id: req.id,
677
+ status: "error",
678
+ error: "Access denied: path not in whitelist"
679
+ };
680
+ }
681
+ try {
682
+ const configPath = await bindProject(requestedPath, projectId, projectName);
683
+ return {
684
+ id: req.id,
685
+ status: "ok",
686
+ data: { configPath }
687
+ };
688
+ } catch (error) {
689
+ return {
690
+ id: req.id,
691
+ status: "error",
692
+ error: error instanceof Error ? error.message : "Unknown error"
693
+ };
694
+ }
695
+ }
696
+
697
+ // src/socket/dispatcher.ts
698
+ var handlers = {
699
+ "legion:fs:list": handleFsList,
700
+ "legion:fs:read": handleFsRead,
701
+ "legion:project:bind": handleProjectBind
702
+ };
703
+ function setupDispatcher(socket, log4, config2) {
704
+ socket.legionConfig = config2;
705
+ for (const [event, handler] of Object.entries(handlers)) {
706
+ socket.on(event, async (request) => {
707
+ try {
708
+ const response = await handler(request, socket);
709
+ socket.emit(`${event}:response`, response);
710
+ } catch (error) {
711
+ log4.error(`Handler error for ${event}`, { error, request });
712
+ socket.emit(`${event}:response`, {
713
+ id: request.id,
714
+ status: "error",
715
+ error: error instanceof Error ? error.message : "Unknown error"
716
+ });
717
+ }
718
+ });
719
+ }
720
+ log4.info("Socket dispatcher initialized", { events: Object.keys(handlers) });
721
+ }
722
+
323
723
  // src/index.ts
324
724
  if (process.argv.includes("--version") || process.argv.includes("-v")) {
325
725
  console.log(version);
@@ -344,7 +744,7 @@ function parseArgs() {
344
744
  }
345
745
  return parsed;
346
746
  }
347
- async function handleDeviceLogin(log, serverUrl = "https://tanuki.sabw.ru") {
747
+ async function handleDeviceLogin(log4, serverUrl = "https://tanuki.sabw.ru") {
348
748
  const codeEndpoint = `${serverUrl}/api/v1/legion/device/code`;
349
749
  const tokenEndpoint = `${serverUrl}/api/v1/legion/device/token`;
350
750
  const codeResponse = await fetch(codeEndpoint, {
@@ -374,7 +774,7 @@ async function handleDeviceLogin(log, serverUrl = "https://tanuki.sabw.ru") {
374
774
  id: token_id,
375
775
  serverUrl: existingConfig?.serverUrl || "wss://tanuki.sabw.ru"
376
776
  });
377
- log.info("\u2705 Device activated successfully");
777
+ log4.info("\u2705 Device activated successfully");
378
778
  return;
379
779
  } else if (tokenResponse.status === 202) {
380
780
  continue;
@@ -405,20 +805,20 @@ function checkForUpdates() {
405
805
  }
406
806
  }, 100);
407
807
  }
408
- async function enterHibernationMode(log, configFile, onConfigChanged) {
409
- log.warn("\u26D4 Auth failed. Entering hibernation mode. Waiting for config update...");
410
- const fs4 = await import("fs");
808
+ async function enterHibernationMode(log4, configFile, onConfigChanged) {
809
+ log4.warn("\u26D4 Auth failed. Entering hibernation mode. Waiting for config update...");
810
+ const fs6 = await import("fs");
411
811
  return new Promise((resolve) => {
412
- const watchHandle = fs4.watch(configFile, async (eventType) => {
812
+ const watchHandle = fs6.watch(configFile, async (eventType) => {
413
813
  if (eventType === "change") {
414
- log.info("\u{1F4DD} Config file changed. Attempting reconnection...");
814
+ log4.info("\u{1F4DD} Config file changed. Attempting reconnection...");
415
815
  try {
416
816
  await new Promise((resolve2) => setTimeout(resolve2, 100));
417
817
  await onConfigChanged();
418
818
  watchHandle.close();
419
819
  resolve();
420
820
  } catch (error) {
421
- log.error("\u274C Reconnection failed", { error });
821
+ log4.error("\u274C Reconnection failed", { error });
422
822
  }
423
823
  }
424
824
  });
@@ -440,9 +840,9 @@ function createSocket(config2) {
440
840
  reconnectionAttempts: Infinity
441
841
  });
442
842
  }
443
- function setupSocketHandlers(socket, log, fingerprint, version2, onTokenRotation, onReconnect) {
843
+ function setupSocketHandlers(socket, log4, fingerprint, version2, onTokenRotation, onReconnect) {
444
844
  socket.on("connect", () => {
445
- log.info("\u2705 Connected to server", { socketId: socket.id });
845
+ log4.info("\u2705 Connected to server", { socketId: socket.id });
446
846
  const handshakeData = {
447
847
  fingerprint,
448
848
  version: version2,
@@ -452,98 +852,102 @@ function setupSocketHandlers(socket, log, fingerprint, version2, onTokenRotation
452
852
  cwd: process.cwd()
453
853
  };
454
854
  socket.emit("legion:handshake", handshakeData);
455
- log.debug("\u{1F4E4} Sent handshake", handshakeData);
855
+ log4.debug("\u{1F4E4} Sent handshake", handshakeData);
856
+ const socketConfig = socket.legionConfig;
857
+ if (socketConfig) {
858
+ setupDispatcher(socket, log4, socketConfig);
859
+ }
456
860
  });
457
861
  socket.on("connect_error", (err) => {
458
- log.error("\u274C Connection error", {
862
+ log4.error("\u274C Connection error", {
459
863
  message: err.message,
460
864
  type: err.type
461
865
  });
462
866
  if (err.message.includes("auth") || err.message.includes("token")) {
463
- log.error("\u{1F510} Authentication failed. Please check your token.");
464
- log.error("\u{1F4A1} Re-authenticate by updating ~/.tanuki/config.json or LEGION_TOKEN env var");
465
- enterHibernationMode(log, CONFIG_FILE, onReconnect).catch((error) => {
466
- log.error("\u274C Hibernation mode failed", { error });
867
+ log4.error("\u{1F510} Authentication failed. Please check your token.");
868
+ log4.error("\u{1F4A1} Re-authenticate by updating ~/.tanuki/config.json or LEGION_TOKEN env var");
869
+ enterHibernationMode(log4, CONFIG_FILE, onReconnect).catch((error) => {
870
+ log4.error("\u274C Hibernation mode failed", { error });
467
871
  process.exit(1);
468
872
  });
469
873
  }
470
874
  });
471
875
  socket.on("disconnect", (reason) => {
472
- log.warn("\u26A0\uFE0F Disconnected from server", { reason });
876
+ log4.warn("\u26A0\uFE0F Disconnected from server", { reason });
473
877
  if (reason === "io server disconnect") {
474
- log.error("\u{1F6AB} Server disconnected this client. Please check your token.");
475
- enterHibernationMode(log, CONFIG_FILE, onReconnect).catch((error) => {
476
- log.error("\u274C Hibernation mode failed", { error });
878
+ log4.error("\u{1F6AB} Server disconnected this client. Please check your token.");
879
+ enterHibernationMode(log4, CONFIG_FILE, onReconnect).catch((error) => {
880
+ log4.error("\u274C Hibernation mode failed", { error });
477
881
  process.exit(1);
478
882
  });
479
883
  }
480
884
  });
481
885
  socket.on("legion:update_token", async (data2) => {
482
886
  try {
483
- log.info("\u{1F504} Received permanent credentials. Persisting...");
887
+ log4.info("\u{1F504} Received permanent credentials. Persisting...");
484
888
  await updateConfig({
485
889
  token: data2.secret,
486
890
  // secret -> token
487
891
  id: data2.token_id
488
892
  // token_id -> id
489
893
  });
490
- log.info("\u2705 Config updated successfully. Reconnecting with new token...");
894
+ log4.info("\u2705 Config updated successfully. Reconnecting with new token...");
491
895
  await onTokenRotation();
492
896
  } catch (error) {
493
- log.error("\u274C Failed to update config", { error });
897
+ log4.error("\u274C Failed to update config", { error });
494
898
  }
495
899
  });
496
900
  socket.on("server:ping", (data2) => {
497
- log.debug("\u{1F4E9} Ping from server", data2);
901
+ log4.debug("\u{1F4E9} Ping from server", data2);
498
902
  socket.emit("legion:pong", { ts: Date.now() });
499
903
  });
500
904
  socket.on("reconnect", (attemptNumber) => {
501
- log.info("\u{1F504} Reconnected to server", { attempt: attemptNumber });
905
+ log4.info("\u{1F504} Reconnected to server", { attempt: attemptNumber });
502
906
  });
503
907
  socket.on("reconnect_attempt", (attemptNumber) => {
504
- log.debug("\u{1F504} Reconnection attempt", { attempt: attemptNumber });
908
+ log4.debug("\u{1F504} Reconnection attempt", { attempt: attemptNumber });
505
909
  });
506
910
  socket.on("reconnect_error", (error) => {
507
- log.error("\u{1F504} Reconnection error", { message: error.message });
911
+ log4.error("\u{1F504} Reconnection error", { message: error.message });
508
912
  });
509
913
  socket.on("reconnect_failed", () => {
510
- log.error("\u{1F504} Reconnection failed after all attempts");
914
+ log4.error("\u{1F504} Reconnection failed after all attempts");
511
915
  process.exit(1);
512
916
  });
513
917
  }
514
918
  async function main() {
515
- let log = null;
919
+ let log4 = null;
516
920
  const safeLog = {
517
921
  info: (message, extra) => {
518
- if (log) log.info(message, extra);
922
+ if (log4) log4.info(message, extra);
519
923
  else console.log(message, extra);
520
924
  },
521
925
  error: (message, extra) => {
522
- if (log) log.error(message, extra);
926
+ if (log4) log4.error(message, extra);
523
927
  else console.error(message, extra);
524
928
  },
525
929
  warn: (message, extra) => {
526
- if (log) log.warn(message, extra);
930
+ if (log4) log4.warn(message, extra);
527
931
  else console.warn(message, extra);
528
932
  },
529
933
  debug: (message, extra) => {
530
- if (log) log.debug(message, extra);
934
+ if (log4) log4.debug(message, extra);
531
935
  }
532
936
  };
533
937
  try {
534
- log = Log.create({ service: "legion" });
938
+ log4 = Log.create({ service: "legion" });
535
939
  const args = parseArgs();
536
940
  if (args.command === "login") {
537
941
  try {
538
- await handleDeviceLogin(log);
942
+ await handleDeviceLogin(log4);
539
943
  process.exit(0);
540
944
  } catch (error) {
541
- log.error("\u274C Device login failed", { error });
945
+ log4.error("\u274C Device login failed", { error });
542
946
  process.exit(1);
543
947
  }
544
948
  }
545
949
  if (args.token) {
546
- log.info("\u{1F511} Saving token from command line...");
950
+ log4.info("\u{1F511} Saving token from command line...");
547
951
  try {
548
952
  const defaultServerUrl = process.env.TANUKI_SERVER_URL || "wss://tanuki.sabw.ru";
549
953
  const existingConfig = await loadConfig();
@@ -552,18 +956,18 @@ async function main() {
552
956
  serverUrl: existingConfig?.serverUrl || defaultServerUrl,
553
957
  ...existingConfig?.id ? { id: existingConfig.id } : {}
554
958
  });
555
- log.info("\u2705 Token saved successfully");
959
+ log4.info("\u2705 Token saved successfully");
556
960
  } catch (error) {
557
- log.error("\u274C Failed to save token", { error });
961
+ log4.error("\u274C Failed to save token", { error });
558
962
  process.exit(1);
559
963
  }
560
964
  }
561
965
  checkForUpdates();
562
966
  await Global.init();
563
967
  const fingerprint = await getSystemFingerprint();
564
- log.info(`\u{1F6E1}\uFE0F Legion v${version} starting...`);
968
+ log4.info(`\u{1F6E1}\uFE0F Legion v${version} starting...`);
565
969
  let config2 = await getConfig();
566
- log.info("\u{1F517} Connecting to server", { serverUrl: config2.serverUrl });
970
+ log4.info("\u{1F517} Connecting to server", { serverUrl: config2.serverUrl });
567
971
  let currentSocket;
568
972
  let isTokenRotation = false;
569
973
  let handleTokenRotation;
@@ -576,9 +980,10 @@ async function main() {
576
980
  await new Promise((resolve) => setTimeout(resolve, 500));
577
981
  }
578
982
  const newConfig = await getConfig();
579
- log.info("\u{1F504} Reconnecting...");
983
+ log4.info("\u{1F504} Reconnecting...");
580
984
  currentSocket = createSocket(newConfig);
581
- setupSocketHandlers(currentSocket, log, fingerprint, version, handleTokenRotation, handleReconnect);
985
+ currentSocket.legionConfig = newConfig;
986
+ setupSocketHandlers(currentSocket, log4, fingerprint, version, handleTokenRotation, handleReconnect);
582
987
  config2 = newConfig;
583
988
  };
584
989
  handleTokenRotation = async () => {
@@ -587,20 +992,21 @@ async function main() {
587
992
  isTokenRotation = false;
588
993
  };
589
994
  currentSocket = createSocket(config2);
590
- setupSocketHandlers(currentSocket, log, fingerprint, version, handleTokenRotation, handleReconnect);
995
+ currentSocket.legionConfig = config2;
996
+ setupSocketHandlers(currentSocket, log4, fingerprint, version, handleTokenRotation, handleReconnect);
591
997
  process.stdin.resume();
592
998
  const shutdown = () => {
593
- log.info("\u{1F6D1} Shutting down...");
999
+ log4.info("\u{1F6D1} Shutting down...");
594
1000
  currentSocket.disconnect();
595
1001
  process.exit(0);
596
1002
  };
597
1003
  process.on("SIGINT", shutdown);
598
1004
  process.on("SIGTERM", shutdown);
599
1005
  process.on("unhandledRejection", (reason) => {
600
- log.error("Unhandled rejection", { reason });
1006
+ log4.error("Unhandled rejection", { reason });
601
1007
  });
602
1008
  process.on("uncaughtException", (error) => {
603
- log.error("Uncaught exception", { error: error.message, stack: error.stack });
1009
+ log4.error("Uncaught exception", { error: error.message, stack: error.stack });
604
1010
  shutdown();
605
1011
  });
606
1012
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@babylen/legion",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Legion agent for connecting devices to Tanuki Cloud",
5
5
  "main": "./dist/index.js",
6
6
  "keywords": [