codetraxis 1.0.0 → 1.0.2

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, await 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,521 +564,122 @@ async function removeAgent(targetDir) {
507
564
  }
508
565
  }
509
566
  // ─────────────────────────────────────────────────────────────────────────────
567
+ // Agent source location
568
+ //
569
+ // In production: __dirname = server/dist/utils/agent/
570
+ // pkg root = ../../../../ → codetraxisAgent/ at root
571
+ // In dev (ts-node): __dirname = server/src/utils/agent/
572
+ // pkg root = ../../../../ → codetraxisAgent/ at root
573
+ // ─────────────────────────────────────────────────────────────────────────────
574
+ const AGENT_SRC_DIR = node_path_1.default.resolve(__dirname, "../../../../codetraxisAgent");
575
+ // ─── Strip TypeScript annotations for plain JS projects ──────────────────────
576
+ // Removes: import type ..., : Type, <Type>, as Type, interface ..., export type ...
577
+ // Keeps the runtime logic intact so webpack/babel can process it without ts-loader.
578
+ function stripTypescript(src) {
579
+ return src
580
+ // remove `import type { ... } from "..."` lines
581
+ .replace(/^import type\s+\{[^}]*\}\s+from\s+['"][^'"]+['"];?\s*$/gm, "")
582
+ // remove `export type ...` and `export interface ...` blocks (single or multi-line)
583
+ .replace(/^export\s+(?:type|interface)\s+\w+[^{]*\{[^}]*\};?\s*$/gm, "")
584
+ .replace(/^export\s+(?:type|interface)\s+[^=\n]+=[^;\n]+;?\s*$/gm, "")
585
+ // remove `interface Foo { ... }` blocks
586
+ .replace(/^interface\s+\w+[^{]*\{[\s\S]*?\}\s*$/gm, "")
587
+ // remove inline type annotations: `: string`, `: boolean`, etc.
588
+ .replace(/:\s*(string|number|boolean|void|unknown|any|never|null|undefined)\b/g, "")
589
+ // remove `: SomeInterface` / `: Record<...>` etc.
590
+ .replace(/:\s*[A-Z][A-Za-z0-9_]*(?:<[^>]*>)?/g, "")
591
+ // remove `as SomeType` casts
592
+ .replace(/\bas\s+[A-Za-z][A-Za-z0-9_<>, |&[\]]*(?=\s*[),;\]}])/g, "")
593
+ // remove `<Type>` generic parameters
594
+ .replace(/<[A-Z][A-Za-z0-9_, ]*>/g, "")
595
+ // clean up resulting blank lines
596
+ .replace(/\n{3,}/g, "\n\n")
597
+ .trimEnd() + "\n";
598
+ }
599
+ // ─── Read agent source file and apply substitutions ──────────────────────────
600
+ async function readAgentFile(relativePath, port, useJs) {
601
+ const fullPath = node_path_1.default.join(AGENT_SRC_DIR, relativePath);
602
+ let src = await promises_1.default.readFile(fullPath, "utf-8");
603
+ src = src.split("__PORT__").join(port);
604
+ if (useJs)
605
+ src = stripTypescript(src);
606
+ return src;
607
+ }
608
+ // ─────────────────────────────────────────────────────────────────────────────
510
609
  // Agent source — modular architecture
511
610
  //
512
- // Generates a codetraxisAgent/ folder with:
513
- // shared.ts — bridge (WS transport + helpers)
514
- // interceptors/consoleInterceptor.ts
515
- // interceptors/fetchInterceptor.ts
516
- // interceptors/xhrInterceptor.ts — for React Native & raw XHR users
517
- // index.ts wires everything together
611
+ // Reads from codetraxisAgent/ at package root and writes to target project:
612
+ // shared.ts/js
613
+ // interceptors/consoleInterceptor.ts/js
614
+ // interceptors/fetchInterceptor.ts/js
615
+ // interceptors/xhrInterceptor.ts/js
616
+ // index.ts/js (with __PORT__ and __XHR_LINE__ substituted)
518
617
  // ─────────────────────────────────────────────────────────────────────────────
519
- async function writeAgentFiles(agentParentDir, serverPort, kind) {
618
+ async function writeAgentFiles(agentParentDir, serverPort, kind, useJs = false) {
520
619
  const agentDir = node_path_1.default.join(agentParentDir, "codetraxisAgent");
521
620
  const interceptorsDir = node_path_1.default.join(agentDir, "interceptors");
621
+ const ext = useJs ? "js" : "ts";
622
+ const isRn = kind === "expo" || kind === "react-native";
623
+ // Always wipe the old agent folder first so stale .ts/.js files don't coexist
624
+ await promises_1.default.rm(agentDir, { recursive: true, force: true });
522
625
  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");
626
+ // shared
627
+ await promises_1.default.writeFile(node_path_1.default.join(agentDir, `shared.${ext}`), await readAgentFile("shared.ts", serverPort, useJs), "utf-8");
628
+ // interceptors
629
+ await promises_1.default.writeFile(node_path_1.default.join(interceptorsDir, `consoleInterceptor.${ext}`), await readAgentFile("interceptors/consoleInterceptor.ts", serverPort, useJs), "utf-8");
630
+ await promises_1.default.writeFile(node_path_1.default.join(interceptorsDir, `fetchInterceptor.${ext}`), await readAgentFile("interceptors/fetchInterceptor.ts", serverPort, useJs), "utf-8");
631
+ await promises_1.default.writeFile(node_path_1.default.join(interceptorsDir, `xhrInterceptor.${ext}`), await readAgentFile("interceptors/xhrInterceptor.ts", serverPort, useJs), "utf-8");
632
+ // index — substitute __XHR_LINE__ based on project kind
633
+ const xhrLine = isRn
634
+ ? "setupXhrInterceptor(treeViewerBridge); // React Native uses XHR under the hood"
635
+ : "setupXhrInterceptor(treeViewerBridge);";
636
+ let indexSrc = await readAgentFile("index.ts", serverPort, useJs);
637
+ indexSrc = indexSrc.split("__XHR_LINE__").join(xhrLine);
638
+ await promises_1.default.writeFile(node_path_1.default.join(agentDir, `index.${ext}`), indexSrc, "utf-8");
528
639
  }
529
- // ─── shared.ts ────────────────────────────────────────────────────────────────
530
- function buildSharedSource(serverPort) {
531
- return `/**
532
- * codetraxis agent — shared bridge (auto-generated, do not edit).
533
- */
534
-
535
- export type TreeViewerEvent = Record<string, unknown>;
536
-
537
- export interface TreeViewerBridge {
538
- send: (event: TreeViewerEvent) => void;
539
- uid: () => string;
540
- safeSerialize: (value: unknown, depth?: number) => unknown;
541
- truncate: (value: string, max?: number) => string;
542
- isWeb: boolean;
640
+ /** Insert <script src="..."> just before </body> (or </head> as fallback). */
641
+ function patchHtmlFile(html, scriptSrc) {
642
+ const tag = `<script src="${scriptSrc}"></script>`;
643
+ if (html.includes(tag))
644
+ return html; // idempotent
645
+ if (/<\/body>/i.test(html)) {
646
+ return html.replace(/<\/body>/i, ` ${tag}\n</body>`);
647
+ }
648
+ if (/<\/head>/i.test(html)) {
649
+ return html.replace(/<\/head>/i, ` ${tag}\n</head>`);
650
+ }
651
+ // No recognisable tags just append
652
+ return html + `\n${tag}\n`;
543
653
  }
544
-
545
- export function createTreeViewerBridge(serverPort: string): TreeViewerBridge {
546
- const g = globalThis as Record<string, any>;
547
-
548
- const isWeb: boolean =
549
- typeof g.document !== "undefined" &&
550
- typeof g.addEventListener === "function";
551
-
552
- let ws: WebSocket | null = null;
553
- let wsReady = false;
554
- const queue: string[] = [];
555
-
556
- const getHost = (): string => {
557
- if (typeof g.__TREE_VIEWER_HOST__ === "string" && g.__TREE_VIEWER_HOST__) {
558
- return g.__TREE_VIEWER_HOST__ as string;
559
- }
560
- if (isWeb && g.location?.hostname) return g.location.hostname as string;
561
- return "localhost";
562
- };
563
-
564
- const connect = () => {
565
- try {
566
- const WS =
567
- typeof WebSocket !== "undefined"
568
- ? WebSocket
569
- : (g.WebSocket as typeof WebSocket | undefined);
570
- if (!WS) return;
571
-
572
- ws = new WS(\`ws://\${getHost()}:${serverPort}/agent\`);
573
- ws.onopen = () => {
574
- wsReady = true;
575
- while (queue.length > 0) {
576
- const item = queue.shift();
577
- if (item && ws) ws.send(item);
578
- }
579
- };
580
- ws.onclose = () => { wsReady = false; setTimeout(connect, 3000); };
581
- ws.onerror = () => { wsReady = false; };
582
- } catch { /* unavailable */ }
583
- };
584
- connect();
585
-
586
- const truncate = (value: string, max = 500000): string =>
587
- value.length > max ? \`\${value.slice(0, max)}…[truncated]\` : value;
588
-
589
- const safeSerialize = (value: unknown, depth = 0): unknown => {
590
- if (depth > 4) return "[depth limit]";
591
- if (value == null) return value;
592
- if (typeof value === "function") return \`[Function: \${(value as Function).name || "anonymous"}]\`;
593
- if (typeof value === "symbol") return value.toString();
594
- if (typeof value !== "object") return value;
595
- if (value instanceof Error) return { __error: true, name: value.name, message: value.message, stack: value.stack };
596
- if (Array.isArray(value)) return value.slice(0, 1000).map(i => safeSerialize(i, depth + 1));
597
-
598
- const seen = new WeakSet<object>();
599
- const walk = (obj: object, d: number): Record<string, unknown> | string => {
600
- if (seen.has(obj)) return "[circular]";
601
- seen.add(obj);
602
- const r: Record<string, unknown> = {};
603
- let n = 0;
604
- for (const k in obj as Record<string, unknown>) {
605
- if (n++ > 500) { r["..."] = "[truncated]"; break; }
606
- try { r[k] = safeSerialize((obj as Record<string, unknown>)[k], d + 1); }
607
- catch { r[k] = "[unserializable]"; }
608
- }
609
- return r;
610
- };
611
- return walk(value as object, depth);
612
- };
613
-
614
- const uid = () =>
615
- \`\${Math.random().toString(36).slice(2)}\${Date.now().toString(36)}\`;
616
-
617
- const send = (event: TreeViewerEvent) => {
618
- try {
619
- const payload = JSON.stringify(event);
620
- if (wsReady && ws) { ws.send(payload); }
621
- else { queue.push(payload); if (queue.length > 200) queue.shift(); }
622
- } catch { /* ignore */ }
623
- };
624
-
625
- return { send, uid, safeSerialize, truncate, isWeb };
626
- }
627
- `;
628
- }
629
- // ─── consoleInterceptor.ts ────────────────────────────────────────────────────
630
- function buildConsoleInterceptorSource() {
631
- return `/**
632
- * codetraxis agent — console interceptor (auto-generated, do not edit).
633
- */
634
- import type { TreeViewerBridge } from "../shared";
635
-
636
- const INSTALLED_KEY = "__tv_console_installed__";
637
-
638
- export function setupConsoleInterceptor(bridge: TreeViewerBridge): void {
639
- const g = globalThis as Record<string, any>;
640
- if (g[INSTALLED_KEY]) return;
641
- g[INSTALLED_KEY] = true;
642
-
643
- const original = {
644
- log: console.log.bind(console),
645
- info: console.info.bind(console),
646
- warn: console.warn.bind(console),
647
- error: console.error.bind(console),
648
- };
649
-
650
- (["log", "info", "warn", "error"] as const).forEach(level => {
651
- // @ts-ignore
652
- console[level] = (...args: unknown[]) => {
653
- original[level](...args);
654
- bridge.send({
655
- id: bridge.uid(),
656
- type: "console",
657
- level,
658
- args: args.map(a => bridge.safeSerialize(a)),
659
- timestamp: Date.now(),
660
- });
661
- };
662
- });
663
- }
664
- `;
665
- }
666
- // ─── fetchInterceptor.ts ──────────────────────────────────────────────────────
667
- function buildFetchInterceptorSource() {
654
+ /** Build a self-contained IIFE bundle for plain-HTML projects by reading agent sources from disk. */
655
+ async function buildHtmlAgentBundle(serverPort) {
656
+ const read = (rel) => readAgentFile(rel, serverPort, true /* JS */);
657
+ const shared = await read("shared.ts");
658
+ const console_ = await read("interceptors/consoleInterceptor.ts");
659
+ const fetch_ = await read("interceptors/fetchInterceptor.ts");
660
+ const xhr_ = await read("interceptors/xhrInterceptor.ts");
661
+ // Convert ESM exports to local variable assignments for IIFE context
662
+ const strip = (src) => src
663
+ .replace(/^export\s+function\s+/gm, "function ")
664
+ .replace(/^export\s+(const|let|var)\s+/gm, "$1 ")
665
+ .replace(/^import[^\n]+\n/gm, "")
666
+ .replace(/^export\s+\{\s*\};\s*$/gm, "");
668
667
  return `/**
669
- * codetraxis agent — fetch interceptor (auto-generated, do not edit).
668
+ * codetraxis debug agent — standalone bundle (auto-generated, do not edit).
669
+ * Injected into public/index.html via <script src="/codetraxisAgent.js">.
670
670
  */
671
- import type { TreeViewerBridge } from "../shared";
672
-
673
- const INSTALLED_KEY = "__tv_fetch_installed__";
674
-
675
- function normalizeHeaders(headers: HeadersInit | undefined): Record<string, string> | undefined {
676
- if (!headers) return undefined;
677
- const result: Record<string, string> = {};
678
- try {
679
- if (headers instanceof Headers) {
680
- headers.forEach((v, k) => { result[k] = v; });
681
- } else if (Array.isArray(headers)) {
682
- (headers as [string, string][]).forEach(([k, v]) => { result[k] = v; });
683
- } else {
684
- Object.assign(result, headers as Record<string, string>);
685
- }
686
- return result;
687
- } catch { return undefined; }
688
- }
689
-
690
- export function setupFetchInterceptor(bridge: TreeViewerBridge): void {
691
- const g = globalThis as Record<string, any>;
692
- if (g[INSTALLED_KEY]) return;
693
- g[INSTALLED_KEY] = true;
694
-
695
- const originalFetch =
696
- typeof globalThis.fetch === "function"
697
- ? globalThis.fetch.bind(globalThis)
698
- : null;
699
- if (!originalFetch) return;
700
-
701
- globalThis.fetch = async function tvFetch(
702
- input: RequestInfo | URL,
703
- init?: RequestInit,
704
- ): Promise<Response> {
705
- const url =
706
- typeof input === "string" ? input
707
- : input instanceof URL ? input.href
708
- : (input as Request).url;
709
-
710
- const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
711
- const id = bridge.uid();
712
- const start = Date.now();
713
-
714
- const requestHeaders = normalizeHeaders(
715
- init?.headers ?? (input instanceof Request ? input.headers : undefined),
716
- );
717
-
718
- let requestBody: string | undefined;
719
- const rawBody = init?.body;
720
- if (rawBody != null) {
721
- try {
722
- requestBody =
723
- typeof rawBody === "string" ? bridge.truncate(rawBody)
724
- : rawBody instanceof URLSearchParams ? bridge.truncate(rawBody.toString())
725
- : "[binary]";
726
- } catch { requestBody = "[unserializable-body]"; }
727
- }
728
-
729
- bridge.send({ id, type: "network", transport: "fetch", method, url,
730
- requestHeaders, requestBody, state: "pending", timestamp: start });
731
-
732
- try {
733
- const response = await originalFetch(input, init);
734
-
735
- let responseHeaders: Record<string, string> | undefined;
736
- try {
737
- responseHeaders = {};
738
- response.headers.forEach((v, k) => { responseHeaders![k] = v; });
739
- } catch { /* ignore */ }
740
-
741
- let responseBody: string | undefined;
742
- try { responseBody = bridge.truncate(await response.clone().text()); }
743
- catch { responseBody = "[binary]"; }
744
-
745
- bridge.send({ id, type: "network", transport: "fetch", method, url,
746
- status: response.status, requestHeaders, requestBody,
747
- responseHeaders, responseBody,
748
- state: response.ok ? "success" : "error",
749
- duration: Date.now() - start, timestamp: start });
750
-
751
- return response;
752
- } catch (error) {
753
- bridge.send({ id, type: "network", transport: "fetch", method, url,
754
- requestHeaders, requestBody, state: "error",
755
- duration: Date.now() - start, timestamp: start,
756
- error: bridge.safeSerialize(error) });
757
- throw error;
758
- }
759
- };
760
- }
761
- `;
762
- }
763
- // ─── xhrInterceptor.ts ────────────────────────────────────────────────────────
764
- function buildXhrInterceptorSource() {
765
- return `/**
766
- * codetraxis agent — XHR interceptor (auto-generated, do not edit).
767
- * Patches XMLHttpRequest.prototype so existing references (e.g. axios
768
- * internals captured at import time) are also intercepted.
769
- * Works in browser and React Native (which ships its own XHR polyfill).
770
- */
771
- import type { TreeViewerBridge } from "../shared";
772
-
773
- const INSTALLED_KEY = "__tv_xhr_installed__";
774
-
775
- export function setupXhrInterceptor(bridge: TreeViewerBridge): void {
776
- if (typeof XMLHttpRequest === "undefined") return;
777
- const g = globalThis as Record<string, any>;
778
- if (g[INSTALLED_KEY]) return;
779
- g[INSTALLED_KEY] = true;
780
-
781
- try {
782
- const proto = XMLHttpRequest.prototype;
783
-
784
- const _origOpen = proto.open;
785
- proto.open = function(this: XMLHttpRequest, method: string, url: string, ...rest: unknown[]) {
786
- (this as any).__tv_method = method.toUpperCase();
787
- (this as any).__tv_url = String(url);
788
- (this as any).__tv_reqHeaders = undefined;
789
- return (_origOpen as Function).apply(this, [method, url, ...rest]);
790
- };
791
-
792
- const _origSetHeader = proto.setRequestHeader;
793
- proto.setRequestHeader = function(this: XMLHttpRequest, name: string, value: string) {
794
- if (!(this as any).__tv_reqHeaders) (this as any).__tv_reqHeaders = {} as Record<string, string>;
795
- (this as any).__tv_reqHeaders[name] = value;
796
- return _origSetHeader.apply(this, [name, value]);
797
- };
798
-
799
- const _origSend = proto.send;
800
- proto.send = function(this: XMLHttpRequest, body?: Document | XMLHttpRequestBodyInit | null) {
801
- const id = bridge.uid();
802
- const method = (this as any).__tv_method ?? "GET";
803
- const url = (this as any).__tv_url ?? "";
804
- const reqHeaders = (this as any).__tv_reqHeaders as Record<string, string> | undefined;
805
- const start = Date.now();
806
-
807
- let requestBody: string | undefined;
808
- if (body != null) {
809
- try {
810
- requestBody =
811
- typeof body === "string" ? bridge.truncate(body)
812
- : body instanceof URLSearchParams ? bridge.truncate(body.toString())
813
- : "[binary]";
814
- } catch { /* ignore */ }
815
- }
816
-
817
- bridge.send({ id, type: "network", transport: "xhr", method, url,
818
- requestBody, requestHeaders: reqHeaders, state: "pending", timestamp: start });
819
-
820
- this.addEventListener("load", () => {
821
- let responseBody: string | undefined;
822
- try { responseBody = bridge.truncate(this.responseText); } catch { /* binary */ }
823
-
824
- let responseHeaders: Record<string, string> | undefined;
825
- try {
826
- const raw = this.getAllResponseHeaders();
827
- if (raw) {
828
- responseHeaders = {};
829
- raw.trim().split(/\\r?\\n/).forEach(line => {
830
- const idx = line.indexOf(": ");
831
- if (idx > 0) responseHeaders![line.slice(0, idx).toLowerCase()] = line.slice(idx + 2);
832
- });
833
- }
834
- } catch { /* ignore */ }
835
-
836
- bridge.send({ id, type: "network", transport: "xhr", method, url,
837
- status: this.status, requestBody, requestHeaders: reqHeaders,
838
- responseBody, responseHeaders,
839
- state: this.status >= 200 && this.status < 400 ? "success" : "error",
840
- duration: Date.now() - start, timestamp: start });
841
- });
842
-
843
- this.addEventListener("error", () => {
844
- bridge.send({ id, type: "network", transport: "xhr", method, url,
845
- requestBody, requestHeaders: reqHeaders,
846
- state: "error", duration: Date.now() - start, timestamp: start });
847
- });
848
-
849
- return _origSend.apply(this, [body]);
850
- };
851
- } catch { /* not available */ }
852
- }
671
+ (function () {
672
+ 'use strict';
673
+
674
+ ${strip(shared)}
675
+ ${strip(console_)}
676
+ ${strip(fetch_)}
677
+ ${strip(xhr_)}
678
+
679
+ const _bridge = createTreeViewerBridge("${serverPort}");
680
+ setupConsoleInterceptor(_bridge);
681
+ setupFetchInterceptor(_bridge);
682
+ setupXhrInterceptor(_bridge);
683
+ })();
853
684
  `;
854
685
  }
855
- // ─── index.ts ─────────────────────────────────────────────────────────────────
856
- function buildAgentIndexSource(kind) {
857
- const isRn = kind === "expo" || kind === "react-native";
858
- return `/**
859
- * codetraxis debug agent — entry point (auto-generated, do not edit).
860
- */
861
- import { createTreeViewerBridge } from "./shared";
862
- import { setupConsoleInterceptor } from "./interceptors/consoleInterceptor";
863
- import { setupFetchInterceptor } from "./interceptors/fetchInterceptor";
864
- import { setupXhrInterceptor } from "./interceptors/xhrInterceptor";
865
-
866
- export const treeViewerBridge = createTreeViewerBridge("__PORT__");
867
-
868
- setupConsoleInterceptor(treeViewerBridge);
869
- setupFetchInterceptor(treeViewerBridge);
870
- ${isRn ? "setupXhrInterceptor(treeViewerBridge); // React Native uses XHR under the hood" : "setupXhrInterceptor(treeViewerBridge);"}
871
-
872
- // ─── Auto-attach default axios instance ──────────────────────────────────────
873
- // If the project uses axios, we attach interceptors to the default instance.
874
- // For axios.create() instances, call attachAxios(instance) manually.
875
- try {
876
- // eslint-disable-next-line @typescript-eslint/no-require-imports
877
- const axiosModule = require("axios");
878
- const axiosInstance = axiosModule?.default ?? axiosModule;
879
- if (axiosInstance?.interceptors) {
880
- attachAxios(axiosInstance);
881
- }
882
- } catch { /* axios not installed — skip */ }
883
-
884
- // ─── attachAxios — for axios.create() instances ───────────────────────────────
885
- // Usage: import { attachAxios } from "./codetraxisAgent";
886
- // attachAxios(myAxiosInstance);
887
- export function attachAxios(instance: any): void {
888
- if (!instance?.interceptors) return;
889
-
890
- const INSTALLED_KEY = "__tv_axios_installed__";
891
- if (instance[INSTALLED_KEY]) return;
892
- instance[INSTALLED_KEY] = true;
893
-
894
- const REQ_ID_KEY = "__tv_req_id__";
895
- const REQ_START_KEY = "__tv_req_start__";
896
-
897
- const joinUrl = (base?: string, url?: string) => {
898
- if (!base) return url || "";
899
- if (!url) return base;
900
- try { return new URL(url, base).toString(); } catch { return \`\${base}\${url}\`; }
901
- };
902
-
903
- const normalizeHeaders = (h: unknown): Record<string, string> | undefined => {
904
- if (!h) return undefined;
905
- try {
906
- if (typeof (h as any).toJSON === "function") return (h as any).toJSON() as Record<string, string>;
907
- return { ...(h as Record<string, string>) };
908
- } catch { return undefined; }
909
- };
910
-
911
- instance.interceptors.request.use(
912
- (config: any) => {
913
- const id = treeViewerBridge.uid();
914
- const start = Date.now();
915
- config[REQ_ID_KEY] = id;
916
- config[REQ_START_KEY] = start;
917
- treeViewerBridge.send({
918
- id, type: "network", transport: "axios",
919
- method: (config.method || "get").toUpperCase(),
920
- url: joinUrl(config.baseURL, config.url),
921
- requestHeaders: normalizeHeaders(config.headers),
922
- requestBody: treeViewerBridge.safeSerialize(config.data),
923
- state: "pending", timestamp: start,
924
- });
925
- return config;
926
- },
927
- (error: any) => {
928
- treeViewerBridge.send({
929
- id: treeViewerBridge.uid(), type: "network", transport: "axios",
930
- state: "error", timestamp: Date.now(),
931
- error: treeViewerBridge.safeSerialize(error),
932
- });
933
- return Promise.reject(error);
934
- },
935
- );
936
-
937
- instance.interceptors.response.use(
938
- (response: any) => {
939
- const id = response.config[REQ_ID_KEY] || treeViewerBridge.uid();
940
- const start = response.config[REQ_START_KEY] || Date.now();
941
- treeViewerBridge.send({
942
- id, type: "network", transport: "axios",
943
- method: (response.config.method || "get").toUpperCase(),
944
- url: joinUrl(response.config.baseURL, response.config.url),
945
- status: response.status,
946
- requestHeaders: normalizeHeaders(response.config.headers),
947
- requestBody: treeViewerBridge.safeSerialize(response.config.data),
948
- responseHeaders: normalizeHeaders(response.headers),
949
- responseBody: treeViewerBridge.safeSerialize(response.data),
950
- state: "success", duration: Date.now() - start, timestamp: start,
951
- });
952
- return response;
953
- },
954
- (error: any) => {
955
- const cfg = error?.config || {};
956
- const id = cfg[REQ_ID_KEY] || treeViewerBridge.uid();
957
- const start = cfg[REQ_START_KEY] || Date.now();
958
- treeViewerBridge.send({
959
- id, type: "network", transport: "axios",
960
- method: (cfg.method || "get").toUpperCase(),
961
- url: joinUrl(cfg.baseURL, cfg.url),
962
- status: error?.response?.status,
963
- requestHeaders: normalizeHeaders(cfg.headers),
964
- requestBody: treeViewerBridge.safeSerialize(cfg.data),
965
- responseHeaders: normalizeHeaders(error?.response?.headers),
966
- responseBody: treeViewerBridge.safeSerialize(error?.response?.data),
967
- state: "error", duration: Date.now() - start, timestamp: start,
968
- error: treeViewerBridge.safeSerialize({ message: error?.message, code: error?.code, name: error?.name }),
969
- });
970
- return Promise.reject(error);
971
- },
972
- );
973
- }
974
-
975
- // ─── attachSocketIO — for socket.io-client instances ─────────────────────────
976
- // React Native uses socket.io which doesn't go through globalThis.WebSocket.
977
- // Usage: import { attachSocketIO } from "./codetraxisAgent";
978
- // attachSocketIO(socket); // call after getChatSocket() or io()
979
- export function attachSocketIO(socket: any): void {
980
- if (!socket) return;
981
-
982
- const INSTALLED_KEY = "__tv_sio_installed__";
983
- if (socket[INSTALLED_KEY]) return;
984
- socket[INSTALLED_KEY] = true;
985
-
986
- const url: string = socket.io?.uri ?? socket.nsp ?? "socket.io";
987
-
988
- // ── Incoming events ──────────────────────────────────────────────────────
989
- socket.onAny((event: string, ...args: unknown[]) => {
990
- treeViewerBridge.send({
991
- id: treeViewerBridge.uid(),
992
- type: "network",
993
- transport: "websocket",
994
- url,
995
- method: "MESSAGE",
996
- responseBody: treeViewerBridge.truncate(
997
- JSON.stringify({ event, data: args.length === 1 ? args[0] : args }),
998
- ),
999
- state: "success",
1000
- timestamp: Date.now(),
1001
- });
1002
- });
1003
-
1004
- // ── Outgoing events ──────────────────────────────────────────────────────
1005
- const origEmit = socket.emit.bind(socket);
1006
- socket.emit = (event: string, ...args: unknown[]) => {
1007
- if (!["ping", "pong"].includes(event)) {
1008
- treeViewerBridge.send({
1009
- id: treeViewerBridge.uid(),
1010
- type: "network",
1011
- transport: "websocket",
1012
- url,
1013
- method: "SEND",
1014
- requestBody: treeViewerBridge.truncate(
1015
- JSON.stringify({ event, data: args[0] }),
1016
- ),
1017
- state: "success",
1018
- timestamp: Date.now(),
1019
- });
1020
- }
1021
- return origEmit(event, ...args);
1022
- };
1023
- }
1024
-
1025
- export {};
1026
- `.replace("__PORT__", "${process.env.PORT ?? '3333'}");
1027
- }