codetraxis 1.0.0 → 1.0.1

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.
@@ -82,10 +82,11 @@ async function detectProjectKind(targetDir) {
82
82
  return "expo";
83
83
  if (deps["react-native"] && !deps["expo"])
84
84
  return "react-native";
85
- if (deps["vite"] || deps["@vitejs/plugin-react"] || deps["@vitejs/plugin-react-swc"])
86
- return "vite-react";
85
+ // react-scripts takes priority over vite (project may have both)
87
86
  if (deps["react-scripts"])
88
87
  return "cra-react";
88
+ if (deps["vite"] || deps["@vitejs/plugin-react"] || deps["@vitejs/plugin-react-swc"])
89
+ return "vite-react";
89
90
  if (deps["webpack"] || deps["@webpack-cli/generators"])
90
91
  return "webpack-react";
91
92
  if (await fileExists(node_path_1.default.join(targetDir, "index.html")) && !deps["react"])
@@ -148,6 +149,7 @@ async function scanDirForPattern(dir, patterns, maxDepth = 3, _depth = 0) {
148
149
  // Step 3 — per-framework adapters
149
150
  // ─────────────────────────────────────────────────────────────────────────────
150
151
  async function resolveVitePlan(targetDir) {
152
+ // 1. Try to find <script type="module" src="..."> in index.html
151
153
  for (const htmlPath of ["index.html", "public/index.html"]) {
152
154
  const htmlFile = node_path_1.default.join(targetDir, htmlPath);
153
155
  if (!(await fileExists(htmlFile)))
@@ -162,15 +164,24 @@ async function resolveVitePlan(targetDir) {
162
164
  reason: `Vite: index.html <script type="module" src="${match[1]}">` };
163
165
  }
164
166
  }
165
- // index.html exists but no module script → HTML patch
166
- return { strategy: "patch-html", htmlFile, scriptSrc: "/codetraxisAgent.js",
167
- confidence: 0.6, reason: "Vite: index.html found, no module entry detected" };
168
167
  }
169
- // Fallback: semantic scan
168
+ // 2. Semantic scan — look for createRoot / ReactDOM.render in JS/JSX/TS/TSX
170
169
  const found = await scanDirForPattern(targetDir, WEB_BOOTSTRAP_PATTERNS);
171
170
  if (found)
172
171
  return { strategy: "inject-import", targetFile: found.file, confidence: found.confidence,
173
- reason: `Vite fallback: ${found.reason}` };
172
+ reason: `Vite semantic: ${found.reason}` };
173
+ // 3. Well-known candidate filenames
174
+ const found2 = await resolveCandidateFallback(targetDir);
175
+ if (found2.strategy === "inject-import")
176
+ return { ...found2, reason: `Vite candidate: ${found2.reason}` };
177
+ // 4. Last resort — patch index.html if it exists
178
+ for (const htmlPath of ["index.html", "public/index.html"]) {
179
+ const htmlFile = node_path_1.default.join(targetDir, htmlPath);
180
+ if (await fileExists(htmlFile)) {
181
+ return { strategy: "patch-html", htmlFile, scriptSrc: "/codetraxisAgent.js",
182
+ confidence: 0.5, reason: "Vite: index.html found, no JS entry detected" };
183
+ }
184
+ }
174
185
  return { strategy: "manual", reason: "Vite project: no entry file found" };
175
186
  }
176
187
  async function resolveNextjsPlan(targetDir) {
@@ -414,9 +425,25 @@ async function installAgent(targetDir) {
414
425
  return { success: false, alreadyInstalled: false, projectKind: kind, strategy: "manual", error: plan.reason };
415
426
  }
416
427
  if (plan.strategy === "patch-html") {
428
+ const publicDir = node_path_1.default.dirname(plan.htmlFile);
429
+ const bundleFile = node_path_1.default.join(publicDir, "codetraxisAgent.js");
430
+ const port = process.env.PORT ?? "3333";
431
+ // Check if already installed
432
+ const htmlSource = await promises_1.default.readFile(plan.htmlFile, "utf-8");
433
+ const alreadyInstalled = htmlSource.includes("codetraxisAgent.js");
434
+ // Always overwrite the bundle so it stays up-to-date
435
+ await promises_1.default.writeFile(bundleFile, buildHtmlAgentBundle(port), "utf-8");
436
+ // Patch index.html only if not already patched
437
+ if (!alreadyInstalled) {
438
+ await promises_1.default.writeFile(plan.htmlFile, patchHtmlFile(htmlSource, plan.scriptSrc), "utf-8");
439
+ }
417
440
  return {
418
- success: false, alreadyInstalled: false, projectKind: kind, strategy: "patch-html",
419
- error: `HTML patching not yet supported. Add <script src="${plan.scriptSrc}"> manually to ${plan.htmlFile}`,
441
+ success: true,
442
+ alreadyInstalled,
443
+ entryFile: plan.htmlFile,
444
+ agentFile: bundleFile,
445
+ projectKind: kind,
446
+ strategy: "patch-html",
420
447
  };
421
448
  }
422
449
  // strategy === "inject-import"
@@ -428,7 +455,12 @@ async function installAgent(targetDir) {
428
455
  // expo-router: entry is in app/ — put agent at project root instead
429
456
  ? targetDir
430
457
  : node_path_1.default.dirname(targetFile);
431
- const agentFile = node_path_1.default.join(agentDir, "codetraxisAgent", "index.ts");
458
+ // Use .js extension for plain JS projects (webpack / CRA without TS)
459
+ // so webpack can resolve ./codetraxisAgent without a TypeScript loader.
460
+ const entryExt = node_path_1.default.extname(targetFile); // .js | .jsx | .ts | .tsx
461
+ const useJs = entryExt === ".js" || entryExt === ".jsx";
462
+ const agentIndexExt = useJs ? "js" : "ts";
463
+ const agentFile = node_path_1.default.join(agentDir, "codetraxisAgent", `index.${agentIndexExt}`);
432
464
  // Import path from the entry file to the agent folder's index
433
465
  let agentImportPath = AGENT_IMPORT_MARKER; // default: "./codetraxisAgent"
434
466
  if (agentDir !== node_path_1.default.dirname(targetFile)) {
@@ -439,7 +471,7 @@ async function installAgent(targetDir) {
439
471
  const entrySource = await promises_1.default.readFile(targetFile, "utf-8");
440
472
  const alreadyInstalled = entrySource.includes("codetraxisAgent");
441
473
  // Always overwrite the agent folder so the latest version is always present.
442
- await writeAgentFiles(agentDir, process.env.PORT ?? "3333", kind);
474
+ await writeAgentFiles(agentDir, process.env.PORT ?? "3333", kind, useJs);
443
475
  // Only inject the import if it's not there yet.
444
476
  if (!alreadyInstalled) {
445
477
  await promises_1.default.writeFile(targetFile, injectImportAst(entrySource, agentImportPath), "utf-8");
@@ -455,11 +487,36 @@ async function installAgent(targetDir) {
455
487
  // ─────────────────────────────────────────────────────────────────────────────
456
488
  async function findEntryFile(targetDir) {
457
489
  const { plan } = await buildInstallPlan(targetDir);
458
- return plan.strategy === "inject-import" ? plan.targetFile : null;
490
+ if (plan.strategy === "inject-import")
491
+ return plan.targetFile;
492
+ if (plan.strategy === "patch-html")
493
+ return plan.htmlFile;
494
+ return null;
459
495
  }
460
496
  async function removeAgent(targetDir) {
461
497
  try {
462
- const entryFile = await findEntryFile(targetDir);
498
+ const { plan } = await buildInstallPlan(targetDir);
499
+ // ── patch-html case ───────────────────────────────────────────────────────
500
+ if (plan.strategy === "patch-html") {
501
+ const publicDir = node_path_1.default.dirname(plan.htmlFile);
502
+ const bundleFile = node_path_1.default.join(publicDir, "codetraxisAgent.js");
503
+ // Remove <script> tag from index.html
504
+ const htmlSource = await promises_1.default.readFile(plan.htmlFile, "utf-8");
505
+ if (htmlSource.includes("codetraxisAgent.js")) {
506
+ const cleaned = htmlSource
507
+ .split("\n")
508
+ .filter(line => !line.includes("codetraxisAgent.js"))
509
+ .join("\n");
510
+ await promises_1.default.writeFile(plan.htmlFile, cleaned, "utf-8");
511
+ }
512
+ // Remove the bundle file
513
+ if (await fileExists(bundleFile)) {
514
+ await promises_1.default.unlink(bundleFile);
515
+ }
516
+ return { success: true, entryFile: plan.htmlFile, agentFile: bundleFile };
517
+ }
518
+ // ── inject-import case ────────────────────────────────────────────────────
519
+ const entryFile = plan.strategy === "inject-import" ? plan.targetFile : null;
463
520
  if (!entryFile) {
464
521
  return { success: false, error: "Entry file not found" };
465
522
  }
@@ -507,6 +564,217 @@ async function removeAgent(targetDir) {
507
564
  }
508
565
  }
509
566
  // ─────────────────────────────────────────────────────────────────────────────
567
+ // HTML patching helpers (for CRA / plain-web projects with public/index.html)
568
+ // ─────────────────────────────────────────────────────────────────────────────
569
+ /** Insert <script src="..."> just before </body> (or </head> as fallback). */
570
+ function patchHtmlFile(html, scriptSrc) {
571
+ const tag = `<script src="${scriptSrc}"></script>`;
572
+ if (html.includes(tag))
573
+ return html; // idempotent
574
+ if (/<\/body>/i.test(html)) {
575
+ return html.replace(/<\/body>/i, ` ${tag}\n</body>`);
576
+ }
577
+ if (/<\/head>/i.test(html)) {
578
+ return html.replace(/<\/head>/i, ` ${tag}\n</head>`);
579
+ }
580
+ // No recognisable tags — just append
581
+ return html + `\n${tag}\n`;
582
+ }
583
+ /** Build a self-contained IIFE bundle (no import/export) for injection via <script>. */
584
+ function buildHtmlAgentBundle(serverPort) {
585
+ // We inline shared + interceptors as one IIFE so it works in any plain HTML page
586
+ // without a bundler or module system.
587
+ return `/**
588
+ * codetraxis debug agent — standalone bundle (auto-generated, do not edit).
589
+ * Injected into public/index.html via <script src="/codetraxisAgent.js">.
590
+ */
591
+ (function () {
592
+ 'use strict';
593
+
594
+ // ─── bridge ────────────────────────────────────────────────────────────────
595
+ var PORT = '${serverPort}';
596
+ var WS_URL = 'ws://localhost:' + PORT + '/agent';
597
+ var MAX_LEN = 20000;
598
+ var _ws = null;
599
+ var _queue = [];
600
+ var _reconnectTimer = null;
601
+
602
+ function connect() {
603
+ try {
604
+ _ws = new WebSocket(WS_URL);
605
+ _ws.onopen = function () {
606
+ var q = _queue.splice(0);
607
+ for (var i = 0; i < q.length; i++) _ws.send(q[i]);
608
+ };
609
+ _ws.onclose = function () {
610
+ _ws = null;
611
+ if (!_reconnectTimer) {
612
+ _reconnectTimer = setTimeout(function () { _reconnectTimer = null; connect(); }, 3000);
613
+ }
614
+ };
615
+ _ws.onerror = function () { /* suppress */ };
616
+ } catch (e) { /* WebSocket not available */ }
617
+ }
618
+
619
+ connect();
620
+
621
+ var _uid = 0;
622
+ function uid() { return 'h' + (++_uid) + '_' + Date.now(); }
623
+
624
+ function truncate(s) {
625
+ if (typeof s !== 'string') return s;
626
+ return s.length > MAX_LEN ? s.slice(0, MAX_LEN) + '…[truncated]' : s;
627
+ }
628
+
629
+ function safeSerialize(v) {
630
+ if (v === undefined || v === null) return undefined;
631
+ if (typeof v === 'string') return truncate(v);
632
+ try { return truncate(JSON.stringify(v)); } catch { return '[unserializable]'; }
633
+ }
634
+
635
+ function send(msg) {
636
+ try {
637
+ var s = JSON.stringify(msg);
638
+ if (_ws && _ws.readyState === 1 /* OPEN */) {
639
+ _ws.send(s);
640
+ } else {
641
+ _queue.push(s);
642
+ }
643
+ } catch { /* ignore */ }
644
+ }
645
+
646
+ var bridge = { uid: uid, truncate: truncate, safeSerialize: safeSerialize, send: send };
647
+
648
+ // ─── console interceptor ───────────────────────────────────────────────────
649
+ var LEVELS = ['log', 'warn', 'error', 'info', 'debug'];
650
+ LEVELS.forEach(function (level) {
651
+ var orig = console[level].bind(console);
652
+ console[level] = function () {
653
+ orig.apply(console, arguments);
654
+ try {
655
+ var args = Array.prototype.slice.call(arguments);
656
+ bridge.send({
657
+ id: bridge.uid(), type: 'console', level: level, timestamp: Date.now(),
658
+ args: args.map(function (a) { return bridge.safeSerialize(a); }),
659
+ });
660
+ } catch { /* ignore */ }
661
+ };
662
+ });
663
+
664
+ // ─── fetch interceptor ─────────────────────────────────────────────────────
665
+ if (typeof fetch !== 'undefined') {
666
+ var _origFetch = fetch;
667
+ fetch = function (input, init) {
668
+ var id = bridge.uid();
669
+ var start = Date.now();
670
+ var method = (init && init.method ? init.method : 'GET').toUpperCase();
671
+ var url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : (input.url || String(input));
672
+ var reqBody = init && init.body != null ? bridge.truncate(String(init.body)) : undefined;
673
+ var reqHeaders;
674
+ try {
675
+ if (init && init.headers) {
676
+ reqHeaders = {};
677
+ var h = new Headers(init.headers);
678
+ h.forEach(function (v, k) { reqHeaders[k] = v; });
679
+ }
680
+ } catch { /* ignore */ }
681
+
682
+ bridge.send({ id: id, type: 'network', transport: 'fetch', method: method, url: url,
683
+ requestBody: reqBody, requestHeaders: reqHeaders, state: 'pending', timestamp: start });
684
+
685
+ return _origFetch.call(this, input, init).then(function (response) {
686
+ var cloned = response.clone();
687
+ var resHeaders = {};
688
+ try { response.headers.forEach(function (v, k) { resHeaders[k] = v; }); } catch { /* ignore */ }
689
+ cloned.text().then(function (body) {
690
+ bridge.send({ id: id, type: 'network', transport: 'fetch', method: method, url: url,
691
+ status: response.status, requestBody: reqBody, requestHeaders: reqHeaders,
692
+ responseBody: bridge.truncate(body), responseHeaders: resHeaders,
693
+ state: response.ok ? 'success' : 'error',
694
+ duration: Date.now() - start, timestamp: start });
695
+ }).catch(function () {
696
+ bridge.send({ id: id, type: 'network', transport: 'fetch', method: method, url: url,
697
+ status: response.status, requestBody: reqBody, requestHeaders: reqHeaders,
698
+ responseHeaders: resHeaders,
699
+ state: response.ok ? 'success' : 'error',
700
+ duration: Date.now() - start, timestamp: start });
701
+ });
702
+ return response;
703
+ }).catch(function (err) {
704
+ bridge.send({ id: id, type: 'network', transport: 'fetch', method: method, url: url,
705
+ requestBody: reqBody, requestHeaders: reqHeaders,
706
+ state: 'error', duration: Date.now() - start, timestamp: start });
707
+ throw err;
708
+ });
709
+ };
710
+ }
711
+
712
+ // ─── XHR interceptor ──────────────────────────────────────────────────────
713
+ try {
714
+ var proto = XMLHttpRequest.prototype;
715
+ var _origOpen = proto.open;
716
+ proto.open = function (method, url) {
717
+ this.__tv_method = method.toUpperCase();
718
+ this.__tv_url = String(url);
719
+ return _origOpen.apply(this, arguments);
720
+ };
721
+ var _origSetHeader = proto.setRequestHeader;
722
+ proto.setRequestHeader = function (name, value) {
723
+ if (!this.__tv_reqHeaders) this.__tv_reqHeaders = {};
724
+ this.__tv_reqHeaders[name] = value;
725
+ return _origSetHeader.apply(this, arguments);
726
+ };
727
+ var _origSend = proto.send;
728
+ proto.send = function (body) {
729
+ var self = this;
730
+ var id = bridge.uid();
731
+ var method = this.__tv_method || 'GET';
732
+ var url = this.__tv_url || '';
733
+ var reqHeaders = this.__tv_reqHeaders;
734
+ var start = Date.now();
735
+ var reqBody;
736
+ if (body != null) {
737
+ try {
738
+ reqBody = typeof body === 'string' ? bridge.truncate(body)
739
+ : body instanceof URLSearchParams ? bridge.truncate(body.toString())
740
+ : '[binary]';
741
+ } catch { /* ignore */ }
742
+ }
743
+ bridge.send({ id: id, type: 'network', transport: 'xhr', method: method, url: url,
744
+ requestBody: reqBody, requestHeaders: reqHeaders, state: 'pending', timestamp: start });
745
+
746
+ this.addEventListener('load', function () {
747
+ var resBody, resHeaders;
748
+ try { resBody = bridge.truncate(self.responseText); } catch { /* binary */ }
749
+ try {
750
+ var raw = self.getAllResponseHeaders();
751
+ if (raw) {
752
+ resHeaders = {};
753
+ raw.trim().split(/\r?\n/).forEach(function (line) {
754
+ var idx = line.indexOf(': ');
755
+ if (idx > 0) resHeaders[line.slice(0, idx).toLowerCase()] = line.slice(idx + 2);
756
+ });
757
+ }
758
+ } catch { /* ignore */ }
759
+ bridge.send({ id: id, type: 'network', transport: 'xhr', method: method, url: url,
760
+ status: self.status, requestBody: reqBody, requestHeaders: reqHeaders,
761
+ responseBody: resBody, responseHeaders: resHeaders,
762
+ state: self.status >= 200 && self.status < 400 ? 'success' : 'error',
763
+ duration: Date.now() - start, timestamp: start });
764
+ });
765
+ this.addEventListener('error', function () {
766
+ bridge.send({ id: id, type: 'network', transport: 'xhr', method: method, url: url,
767
+ requestBody: reqBody, requestHeaders: reqHeaders,
768
+ state: 'error', duration: Date.now() - start, timestamp: start });
769
+ });
770
+ return _origSend.apply(this, arguments);
771
+ };
772
+ } catch (e) { /* XHR not available */ }
773
+
774
+ })();
775
+ `;
776
+ }
777
+ // ─────────────────────────────────────────────────────────────────────────────
510
778
  // Agent source — modular architecture
511
779
  //
512
780
  // Generates a codetraxisAgent/ folder with:
@@ -516,18 +784,131 @@ async function removeAgent(targetDir) {
516
784
  // interceptors/xhrInterceptor.ts — for React Native & raw XHR users
517
785
  // index.ts — wires everything together
518
786
  // ─────────────────────────────────────────────────────────────────────────────
519
- async function writeAgentFiles(agentParentDir, serverPort, kind) {
787
+ async function writeAgentFiles(agentParentDir, serverPort, kind, useJs = false) {
520
788
  const agentDir = node_path_1.default.join(agentParentDir, "codetraxisAgent");
521
789
  const interceptorsDir = node_path_1.default.join(agentDir, "interceptors");
790
+ const ext = useJs ? "js" : "ts";
791
+ // Always wipe the old agent folder first so stale .ts/.js files don't coexist
792
+ await promises_1.default.rm(agentDir, { recursive: true, force: true });
522
793
  await promises_1.default.mkdir(interceptorsDir, { recursive: true });
523
- await promises_1.default.writeFile(node_path_1.default.join(agentDir, "shared.ts"), buildSharedSource(serverPort), "utf-8");
524
- await promises_1.default.writeFile(node_path_1.default.join(interceptorsDir, "consoleInterceptor.ts"), buildConsoleInterceptorSource(), "utf-8");
525
- await promises_1.default.writeFile(node_path_1.default.join(interceptorsDir, "fetchInterceptor.ts"), buildFetchInterceptorSource(), "utf-8");
526
- await promises_1.default.writeFile(node_path_1.default.join(interceptorsDir, "xhrInterceptor.ts"), buildXhrInterceptorSource(), "utf-8");
527
- await promises_1.default.writeFile(node_path_1.default.join(agentDir, "index.ts"), buildAgentIndexSource(kind), "utf-8");
794
+ await promises_1.default.writeFile(node_path_1.default.join(agentDir, `shared.${ext}`), buildSharedSource(serverPort, useJs), "utf-8");
795
+ await promises_1.default.writeFile(node_path_1.default.join(interceptorsDir, `consoleInterceptor.${ext}`), buildConsoleInterceptorSource(useJs), "utf-8");
796
+ await promises_1.default.writeFile(node_path_1.default.join(interceptorsDir, `fetchInterceptor.${ext}`), buildFetchInterceptorSource(useJs), "utf-8");
797
+ await promises_1.default.writeFile(node_path_1.default.join(interceptorsDir, `xhrInterceptor.${ext}`), buildXhrInterceptorSource(useJs), "utf-8");
798
+ await promises_1.default.writeFile(node_path_1.default.join(agentDir, `index.${ext}`), buildAgentIndexSource(kind, serverPort, useJs), "utf-8");
799
+ }
800
+ // ─── Strip TypeScript annotations for plain JS projects ──────────────────────
801
+ // Removes: import type ..., : Type, <Type>, as Type, interface ..., export type ...
802
+ // Keeps the runtime logic intact so webpack/babel can process it without ts-loader.
803
+ function stripTypescript(src) {
804
+ return src
805
+ // remove `import type { ... } from "..."` lines
806
+ .replace(/^import type\s+\{[^}]*\}\s+from\s+['"][^'"]+['"];?\s*$/gm, "")
807
+ // remove `export type ...` and `export interface ...` blocks (single or multi-line)
808
+ .replace(/^export\s+(?:type|interface)\s+\w+[^{]*\{[^}]*\};?\s*$/gm, "")
809
+ .replace(/^export\s+(?:type|interface)\s+[^=\n]+=[^;\n]+;?\s*$/gm, "")
810
+ // remove `interface Foo { ... }` blocks
811
+ .replace(/^interface\s+\w+[^{]*\{[\s\S]*?\}\s*$/gm, "")
812
+ // remove inline type annotations: `: string`, `: boolean`, `: number`, `: void`, `: unknown`, `: any`, `: never`
813
+ .replace(/:\s*(string|number|boolean|void|unknown|any|never|null|undefined)\b/g, "")
814
+ // remove `: SomeInterface` / `: Record<...>` / `: Array<...>` etc. (complex types)
815
+ .replace(/:\s*[A-Z][A-Za-z0-9_]*(?:<[^>]*>)?/g, "")
816
+ // remove `as SomeType` casts
817
+ .replace(/\bas\s+[A-Za-z][A-Za-z0-9_<>, |&[\]]*(?=\s*[),;\]}])/g, "")
818
+ // remove `<Type>` generic parameters in function calls / declarations (simple)
819
+ .replace(/<[A-Z][A-Za-z0-9_, ]*>/g, "")
820
+ // clean up resulting double spaces / empty lines
821
+ .replace(/\n{3,}/g, "\n\n")
822
+ .trimEnd() + "\n";
823
+ }
824
+ // ─── shared.ts / shared.js ───────────────────────────────────────────────────
825
+ function buildSharedSource(serverPort, useJs = false) {
826
+ if (useJs) {
827
+ return `/**
828
+ * codetraxis agent — shared bridge (auto-generated, do not edit).
829
+ */
830
+
831
+ export function createTreeViewerBridge(serverPort) {
832
+ const g = globalThis;
833
+
834
+ const isWeb =
835
+ typeof g.document !== "undefined" &&
836
+ typeof g.addEventListener === "function";
837
+
838
+ let ws = null;
839
+ let wsReady = false;
840
+ const queue = [];
841
+
842
+ const getHost = () => {
843
+ if (typeof g.__TREE_VIEWER_HOST__ === "string" && g.__TREE_VIEWER_HOST__) {
844
+ return g.__TREE_VIEWER_HOST__;
845
+ }
846
+ if (isWeb && g.location?.hostname) return g.location.hostname;
847
+ return "localhost";
848
+ };
849
+
850
+ const connect = () => {
851
+ try {
852
+ const WS = typeof WebSocket !== "undefined" ? WebSocket : g.WebSocket;
853
+ if (!WS) return;
854
+
855
+ ws = new WS(\`ws://\${getHost()}:${serverPort}/agent\`);
856
+ ws.onopen = () => {
857
+ wsReady = true;
858
+ while (queue.length > 0) {
859
+ const item = queue.shift();
860
+ if (item && ws) ws.send(item);
861
+ }
862
+ };
863
+ ws.onclose = () => { wsReady = false; setTimeout(connect, 3000); };
864
+ ws.onerror = () => { wsReady = false; };
865
+ } catch { /* unavailable */ }
866
+ };
867
+ connect();
868
+
869
+ const truncate = (value, max = 500000) =>
870
+ value.length > max ? \`\${value.slice(0, max)}…[truncated]\` : value;
871
+
872
+ const safeSerialize = (value, depth = 0) => {
873
+ if (depth > 4) return "[depth limit]";
874
+ if (value == null) return value;
875
+ if (typeof value === "function") return \`[Function: \${value.name || "anonymous"}]\`;
876
+ if (typeof value === "symbol") return value.toString();
877
+ if (typeof value !== "object") return value;
878
+ if (value instanceof Error) return { __error: true, name: value.name, message: value.message, stack: value.stack };
879
+ if (Array.isArray(value)) return value.slice(0, 1000).map(i => safeSerialize(i, depth + 1));
880
+
881
+ const seen = new WeakSet();
882
+ const walk = (obj, d) => {
883
+ if (seen.has(obj)) return "[circular]";
884
+ seen.add(obj);
885
+ const r = {};
886
+ let n = 0;
887
+ for (const k in obj) {
888
+ if (n++ > 500) { r["..."] = "[truncated]"; break; }
889
+ try { r[k] = safeSerialize(obj[k], d + 1); }
890
+ catch { r[k] = "[unserializable]"; }
891
+ }
892
+ return r;
893
+ };
894
+ return walk(value, depth);
895
+ };
896
+
897
+ const uid = () =>
898
+ \`\${Math.random().toString(36).slice(2)}\${Date.now().toString(36)}\`;
899
+
900
+ const send = (event) => {
901
+ try {
902
+ const payload = JSON.stringify(event);
903
+ if (wsReady && ws) { ws.send(payload); }
904
+ else { queue.push(payload); if (queue.length > 200) queue.shift(); }
905
+ } catch { /* ignore */ }
906
+ };
907
+
908
+ return { send, uid, safeSerialize, truncate, isWeb };
528
909
  }
529
- // ─── shared.ts ────────────────────────────────────────────────────────────────
530
- function buildSharedSource(serverPort) {
910
+ `;
911
+ }
531
912
  return `/**
532
913
  * codetraxis agent — shared bridge (auto-generated, do not edit).
533
914
  */
@@ -626,8 +1007,42 @@ export function createTreeViewerBridge(serverPort: string): TreeViewerBridge {
626
1007
  }
627
1008
  `;
628
1009
  }
629
- // ─── consoleInterceptor.ts ────────────────────────────────────────────────────
630
- function buildConsoleInterceptorSource() {
1010
+ // ─── consoleInterceptor.ts / .js ─────────────────────────────────────────────
1011
+ function buildConsoleInterceptorSource(useJs = false) {
1012
+ if (useJs) {
1013
+ return `/**
1014
+ * codetraxis agent — console interceptor (auto-generated, do not edit).
1015
+ */
1016
+
1017
+ const INSTALLED_KEY = "__tv_console_installed__";
1018
+
1019
+ export function setupConsoleInterceptor(bridge) {
1020
+ const g = globalThis;
1021
+ if (g[INSTALLED_KEY]) return;
1022
+ g[INSTALLED_KEY] = true;
1023
+
1024
+ const original = {
1025
+ log: console.log.bind(console),
1026
+ info: console.info.bind(console),
1027
+ warn: console.warn.bind(console),
1028
+ error: console.error.bind(console),
1029
+ };
1030
+
1031
+ ["log", "info", "warn", "error"].forEach(level => {
1032
+ console[level] = (...args) => {
1033
+ original[level](...args);
1034
+ bridge.send({
1035
+ id: bridge.uid(),
1036
+ type: "console",
1037
+ level,
1038
+ args: args.map(a => bridge.safeSerialize(a)),
1039
+ timestamp: Date.now(),
1040
+ });
1041
+ };
1042
+ });
1043
+ }
1044
+ `;
1045
+ }
631
1046
  return `/**
632
1047
  * codetraxis agent — console interceptor (auto-generated, do not edit).
633
1048
  */
@@ -663,8 +1078,100 @@ export function setupConsoleInterceptor(bridge: TreeViewerBridge): void {
663
1078
  }
664
1079
  `;
665
1080
  }
666
- // ─── fetchInterceptor.ts ──────────────────────────────────────────────────────
667
- function buildFetchInterceptorSource() {
1081
+ // ─── fetchInterceptor.ts / .js ───────────────────────────────────────────────
1082
+ function buildFetchInterceptorSource(useJs = false) {
1083
+ if (useJs) {
1084
+ return `/**
1085
+ * codetraxis agent — fetch interceptor (auto-generated, do not edit).
1086
+ */
1087
+
1088
+ const INSTALLED_KEY = "__tv_fetch_installed__";
1089
+
1090
+ function normalizeHeaders(headers) {
1091
+ if (!headers) return undefined;
1092
+ const result = {};
1093
+ try {
1094
+ if (headers instanceof Headers) {
1095
+ headers.forEach((v, k) => { result[k] = v; });
1096
+ } else if (Array.isArray(headers)) {
1097
+ headers.forEach(([k, v]) => { result[k] = v; });
1098
+ } else {
1099
+ Object.assign(result, headers);
1100
+ }
1101
+ return result;
1102
+ } catch { return undefined; }
1103
+ }
1104
+
1105
+ export function setupFetchInterceptor(bridge) {
1106
+ const g = globalThis;
1107
+ if (g[INSTALLED_KEY]) return;
1108
+ g[INSTALLED_KEY] = true;
1109
+
1110
+ const originalFetch =
1111
+ typeof globalThis.fetch === "function"
1112
+ ? globalThis.fetch.bind(globalThis)
1113
+ : null;
1114
+ if (!originalFetch) return;
1115
+
1116
+ globalThis.fetch = async function tvFetch(input, init) {
1117
+ const url =
1118
+ typeof input === "string" ? input
1119
+ : input instanceof URL ? input.href
1120
+ : input.url;
1121
+
1122
+ const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
1123
+ const id = bridge.uid();
1124
+ const start = Date.now();
1125
+
1126
+ const requestHeaders = normalizeHeaders(
1127
+ init?.headers ?? (input instanceof Request ? input.headers : undefined),
1128
+ );
1129
+
1130
+ let requestBody;
1131
+ const rawBody = init?.body;
1132
+ if (rawBody != null) {
1133
+ try {
1134
+ requestBody =
1135
+ typeof rawBody === "string" ? bridge.truncate(rawBody)
1136
+ : rawBody instanceof URLSearchParams ? bridge.truncate(rawBody.toString())
1137
+ : "[binary]";
1138
+ } catch { requestBody = "[unserializable-body]"; }
1139
+ }
1140
+
1141
+ bridge.send({ id, type: "network", transport: "fetch", method, url,
1142
+ requestHeaders, requestBody, state: "pending", timestamp: start });
1143
+
1144
+ try {
1145
+ const response = await originalFetch(input, init);
1146
+
1147
+ let responseHeaders;
1148
+ try {
1149
+ responseHeaders = {};
1150
+ response.headers.forEach((v, k) => { responseHeaders[k] = v; });
1151
+ } catch { /* ignore */ }
1152
+
1153
+ let responseBody;
1154
+ try { responseBody = bridge.truncate(await response.clone().text()); }
1155
+ catch { responseBody = "[binary]"; }
1156
+
1157
+ bridge.send({ id, type: "network", transport: "fetch", method, url,
1158
+ status: response.status, requestHeaders, requestBody,
1159
+ responseHeaders, responseBody,
1160
+ state: response.ok ? "success" : "error",
1161
+ duration: Date.now() - start, timestamp: start });
1162
+
1163
+ return response;
1164
+ } catch (error) {
1165
+ bridge.send({ id, type: "network", transport: "fetch", method, url,
1166
+ requestHeaders, requestBody, state: "error",
1167
+ duration: Date.now() - start, timestamp: start,
1168
+ error: bridge.safeSerialize(error) });
1169
+ throw error;
1170
+ }
1171
+ };
1172
+ }
1173
+ `;
1174
+ }
668
1175
  return `/**
669
1176
  * codetraxis agent — fetch interceptor (auto-generated, do not edit).
670
1177
  */
@@ -760,8 +1267,95 @@ export function setupFetchInterceptor(bridge: TreeViewerBridge): void {
760
1267
  }
761
1268
  `;
762
1269
  }
763
- // ─── xhrInterceptor.ts ────────────────────────────────────────────────────────
764
- function buildXhrInterceptorSource() {
1270
+ // ─── xhrInterceptor.ts / .js ─────────────────────────────────────────────────
1271
+ function buildXhrInterceptorSource(useJs = false) {
1272
+ if (useJs) {
1273
+ return `/**
1274
+ * codetraxis agent — XHR interceptor (auto-generated, do not edit).
1275
+ */
1276
+
1277
+ const INSTALLED_KEY = "__tv_xhr_installed__";
1278
+
1279
+ export function setupXhrInterceptor(bridge) {
1280
+ if (typeof XMLHttpRequest === "undefined") return;
1281
+ const g = globalThis;
1282
+ if (g[INSTALLED_KEY]) return;
1283
+ g[INSTALLED_KEY] = true;
1284
+
1285
+ try {
1286
+ const proto = XMLHttpRequest.prototype;
1287
+
1288
+ const _origOpen = proto.open;
1289
+ proto.open = function(method, url, ...rest) {
1290
+ this.__tv_method = method.toUpperCase();
1291
+ this.__tv_url = String(url);
1292
+ this.__tv_reqHeaders = undefined;
1293
+ return _origOpen.apply(this, [method, url, ...rest]);
1294
+ };
1295
+
1296
+ const _origSetHeader = proto.setRequestHeader;
1297
+ proto.setRequestHeader = function(name, value) {
1298
+ if (!this.__tv_reqHeaders) this.__tv_reqHeaders = {};
1299
+ this.__tv_reqHeaders[name] = value;
1300
+ return _origSetHeader.apply(this, [name, value]);
1301
+ };
1302
+
1303
+ const _origSend = proto.send;
1304
+ proto.send = function(body) {
1305
+ const id = bridge.uid();
1306
+ const method = this.__tv_method ?? "GET";
1307
+ const url = this.__tv_url ?? "";
1308
+ const reqHeaders = this.__tv_reqHeaders;
1309
+ const start = Date.now();
1310
+
1311
+ let requestBody;
1312
+ if (body != null) {
1313
+ try {
1314
+ requestBody =
1315
+ typeof body === "string" ? bridge.truncate(body)
1316
+ : body instanceof URLSearchParams ? bridge.truncate(body.toString())
1317
+ : "[binary]";
1318
+ } catch { /* ignore */ }
1319
+ }
1320
+
1321
+ bridge.send({ id, type: "network", transport: "xhr", method, url,
1322
+ requestBody, requestHeaders: reqHeaders, state: "pending", timestamp: start });
1323
+
1324
+ this.addEventListener("load", () => {
1325
+ let responseBody;
1326
+ try { responseBody = bridge.truncate(this.responseText); } catch { /* binary */ }
1327
+
1328
+ let responseHeaders;
1329
+ try {
1330
+ const raw = this.getAllResponseHeaders();
1331
+ if (raw) {
1332
+ responseHeaders = {};
1333
+ raw.trim().split(/\\r?\\n/).forEach(line => {
1334
+ const idx = line.indexOf(": ");
1335
+ if (idx > 0) responseHeaders[line.slice(0, idx).toLowerCase()] = line.slice(idx + 2);
1336
+ });
1337
+ }
1338
+ } catch { /* ignore */ }
1339
+
1340
+ bridge.send({ id, type: "network", transport: "xhr", method, url,
1341
+ status: this.status, requestBody, requestHeaders: reqHeaders,
1342
+ responseBody, responseHeaders,
1343
+ state: this.status >= 200 && this.status < 400 ? "success" : "error",
1344
+ duration: Date.now() - start, timestamp: start });
1345
+ });
1346
+
1347
+ this.addEventListener("error", () => {
1348
+ bridge.send({ id, type: "network", transport: "xhr", method, url,
1349
+ requestBody, requestHeaders: reqHeaders,
1350
+ state: "error", duration: Date.now() - start, timestamp: start });
1351
+ });
1352
+
1353
+ return _origSend.apply(this, [body]);
1354
+ };
1355
+ } catch { /* not available */ }
1356
+ }
1357
+ `;
1358
+ }
765
1359
  return `/**
766
1360
  * codetraxis agent — XHR interceptor (auto-generated, do not edit).
767
1361
  * Patches XMLHttpRequest.prototype so existing references (e.g. axios
@@ -853,8 +1447,171 @@ export function setupXhrInterceptor(bridge: TreeViewerBridge): void {
853
1447
  `;
854
1448
  }
855
1449
  // ─── index.ts ─────────────────────────────────────────────────────────────────
856
- function buildAgentIndexSource(kind) {
1450
+ function buildAgentIndexSource(kind, serverPort, useJs = false) {
857
1451
  const isRn = kind === "expo" || kind === "react-native";
1452
+ if (useJs) {
1453
+ return `/**
1454
+ * codetraxis debug agent — entry point (auto-generated, do not edit).
1455
+ */
1456
+ import { createTreeViewerBridge } from "./shared";
1457
+ import { setupConsoleInterceptor } from "./interceptors/consoleInterceptor";
1458
+ import { setupFetchInterceptor } from "./interceptors/fetchInterceptor";
1459
+ import { setupXhrInterceptor } from "./interceptors/xhrInterceptor";
1460
+
1461
+ export const treeViewerBridge = createTreeViewerBridge("__PORT__");
1462
+
1463
+ setupConsoleInterceptor(treeViewerBridge);
1464
+ setupFetchInterceptor(treeViewerBridge);
1465
+ ${isRn ? "setupXhrInterceptor(treeViewerBridge); // React Native uses XHR under the hood" : "setupXhrInterceptor(treeViewerBridge);"}
1466
+
1467
+ // ─── Auto-attach default axios instance ──────────────────────────────────────
1468
+ try {
1469
+ const axiosModule = require("axios");
1470
+ const axiosInstance = axiosModule?.default ?? axiosModule;
1471
+ if (axiosInstance?.interceptors) {
1472
+ attachAxios(axiosInstance);
1473
+ }
1474
+ } catch { /* axios not installed — skip */ }
1475
+
1476
+ // ─── attachAxios — for axios.create() instances ───────────────────────────────
1477
+ // Usage: import { attachAxios } from "./codetraxisAgent";
1478
+ // attachAxios(myAxiosInstance);
1479
+ export function attachAxios(instance) {
1480
+ if (!instance?.interceptors) return;
1481
+
1482
+ const INSTALLED_KEY = "__tv_axios_installed__";
1483
+ if (instance[INSTALLED_KEY]) return;
1484
+ instance[INSTALLED_KEY] = true;
1485
+
1486
+ const REQ_ID_KEY = "__tv_req_id__";
1487
+ const REQ_START_KEY = "__tv_req_start__";
1488
+
1489
+ const joinUrl = (base, url) => {
1490
+ if (!base) return url || "";
1491
+ if (!url) return base;
1492
+ try { return new URL(url, base).toString(); } catch { return \`\${base}\${url}\`; }
1493
+ };
1494
+
1495
+ const normalizeHeaders = (h) => {
1496
+ if (!h) return undefined;
1497
+ try {
1498
+ if (typeof h.toJSON === "function") return h.toJSON();
1499
+ return { ...h };
1500
+ } catch { return undefined; }
1501
+ };
1502
+
1503
+ instance.interceptors.request.use(
1504
+ (config) => {
1505
+ const id = treeViewerBridge.uid();
1506
+ const start = Date.now();
1507
+ config[REQ_ID_KEY] = id;
1508
+ config[REQ_START_KEY] = start;
1509
+ treeViewerBridge.send({
1510
+ id, type: "network", transport: "axios",
1511
+ method: (config.method || "get").toUpperCase(),
1512
+ url: joinUrl(config.baseURL, config.url),
1513
+ requestHeaders: normalizeHeaders(config.headers),
1514
+ requestBody: treeViewerBridge.safeSerialize(config.data),
1515
+ state: "pending", timestamp: start,
1516
+ });
1517
+ return config;
1518
+ },
1519
+ (error) => {
1520
+ treeViewerBridge.send({
1521
+ id: treeViewerBridge.uid(), type: "network", transport: "axios",
1522
+ state: "error", timestamp: Date.now(),
1523
+ error: treeViewerBridge.safeSerialize(error),
1524
+ });
1525
+ return Promise.reject(error);
1526
+ },
1527
+ );
1528
+
1529
+ instance.interceptors.response.use(
1530
+ (response) => {
1531
+ const id = response.config[REQ_ID_KEY] || treeViewerBridge.uid();
1532
+ const start = response.config[REQ_START_KEY] || Date.now();
1533
+ treeViewerBridge.send({
1534
+ id, type: "network", transport: "axios",
1535
+ method: (response.config.method || "get").toUpperCase(),
1536
+ url: joinUrl(response.config.baseURL, response.config.url),
1537
+ status: response.status,
1538
+ requestHeaders: normalizeHeaders(response.config.headers),
1539
+ requestBody: treeViewerBridge.safeSerialize(response.config.data),
1540
+ responseHeaders: normalizeHeaders(response.headers),
1541
+ responseBody: treeViewerBridge.safeSerialize(response.data),
1542
+ state: "success", duration: Date.now() - start, timestamp: start,
1543
+ });
1544
+ return response;
1545
+ },
1546
+ (error) => {
1547
+ const cfg = error?.config || {};
1548
+ const id = cfg[REQ_ID_KEY] || treeViewerBridge.uid();
1549
+ const start = cfg[REQ_START_KEY] || Date.now();
1550
+ treeViewerBridge.send({
1551
+ id, type: "network", transport: "axios",
1552
+ method: (cfg.method || "get").toUpperCase(),
1553
+ url: joinUrl(cfg.baseURL, cfg.url),
1554
+ status: error?.response?.status,
1555
+ requestHeaders: normalizeHeaders(cfg.headers),
1556
+ requestBody: treeViewerBridge.safeSerialize(cfg.data),
1557
+ responseHeaders: normalizeHeaders(error?.response?.headers),
1558
+ responseBody: treeViewerBridge.safeSerialize(error?.response?.data),
1559
+ state: "error", duration: Date.now() - start, timestamp: start,
1560
+ error: treeViewerBridge.safeSerialize({ message: error?.message, code: error?.code, name: error?.name }),
1561
+ });
1562
+ return Promise.reject(error);
1563
+ },
1564
+ );
1565
+ }
1566
+
1567
+ // ─── attachSocketIO — for socket.io-client instances ─────────────────────────
1568
+ // Usage: import { attachSocketIO } from "./codetraxisAgent";
1569
+ // attachSocketIO(socket);
1570
+ export function attachSocketIO(socket) {
1571
+ if (!socket) return;
1572
+
1573
+ const INSTALLED_KEY = "__tv_sio_installed__";
1574
+ if (socket[INSTALLED_KEY]) return;
1575
+ socket[INSTALLED_KEY] = true;
1576
+
1577
+ const url = socket.io?.uri ?? socket.nsp ?? "socket.io";
1578
+
1579
+ socket.onAny((event, ...args) => {
1580
+ treeViewerBridge.send({
1581
+ id: treeViewerBridge.uid(),
1582
+ type: "network",
1583
+ transport: "websocket",
1584
+ url,
1585
+ method: "MESSAGE",
1586
+ responseBody: treeViewerBridge.truncate(
1587
+ JSON.stringify({ event, data: args.length === 1 ? args[0] : args }),
1588
+ ),
1589
+ state: "success",
1590
+ timestamp: Date.now(),
1591
+ });
1592
+ });
1593
+
1594
+ const origEmit = socket.emit.bind(socket);
1595
+ socket.emit = (event, ...args) => {
1596
+ if (!["ping", "pong"].includes(event)) {
1597
+ treeViewerBridge.send({
1598
+ id: treeViewerBridge.uid(),
1599
+ type: "network",
1600
+ transport: "websocket",
1601
+ url,
1602
+ method: "SEND",
1603
+ requestBody: treeViewerBridge.truncate(
1604
+ JSON.stringify({ event, data: args[0] }),
1605
+ ),
1606
+ state: "success",
1607
+ timestamp: Date.now(),
1608
+ });
1609
+ }
1610
+ return origEmit(event, ...args);
1611
+ };
1612
+ }
1613
+ `.replace("__PORT__", serverPort);
1614
+ }
858
1615
  return `/**
859
1616
  * codetraxis debug agent — entry point (auto-generated, do not edit).
860
1617
  */
@@ -1023,5 +1780,5 @@ export function attachSocketIO(socket: any): void {
1023
1780
  }
1024
1781
 
1025
1782
  export {};
1026
- `.replace("__PORT__", "${process.env.PORT ?? '3333'}");
1783
+ `.replace("__PORT__", serverPort);
1027
1784
  }