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.
- package/README.md +30 -183
- package/client/dist/assets/{index-BMv7qJps.js → index-DxucfvnK.js} +1 -1
- package/client/dist/index.html +1 -1
- package/codetraxisAgent/index.ts +171 -0
- package/codetraxisAgent/interceptors/consoleInterceptor.ts +33 -0
- package/codetraxisAgent/interceptors/fetchInterceptor.ts +93 -0
- package/codetraxisAgent/interceptors/xhrInterceptor.ts +88 -0
- package/codetraxisAgent/shared.ts +97 -0
- package/package.json +3 -2
- package/server/dist/utils/agent/agentInstaller.js +177 -519
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
|
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:
|
|
419
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
513
|
-
// shared.ts
|
|
514
|
-
// interceptors/consoleInterceptor.ts
|
|
515
|
-
// interceptors/fetchInterceptor.ts
|
|
516
|
-
// interceptors/xhrInterceptor.ts
|
|
517
|
-
// index.ts
|
|
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
|
-
|
|
524
|
-
await promises_1.default.writeFile(node_path_1.default.join(
|
|
525
|
-
|
|
526
|
-
await promises_1.default.writeFile(node_path_1.default.join(interceptorsDir, "
|
|
527
|
-
await promises_1.default.writeFile(node_path_1.default.join(
|
|
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
|
-
|
|
530
|
-
function
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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 —
|
|
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
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
-
}
|