@inspecto-dev/plugin 0.2.0-alpha.1 → 0.2.0-alpha.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.
Files changed (44) hide show
  1. package/README.md +19 -0
  2. package/dist/index.cjs +428 -101
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.js +433 -102
  7. package/dist/index.js.map +1 -1
  8. package/dist/legacy/rspack/index.cjs +399 -97
  9. package/dist/legacy/rspack/index.cjs.map +1 -1
  10. package/dist/legacy/rspack/index.js +404 -98
  11. package/dist/legacy/rspack/index.js.map +1 -1
  12. package/dist/legacy/rspack/loader.cjs +8 -1
  13. package/dist/legacy/rspack/loader.cjs.map +1 -1
  14. package/dist/legacy/rspack/loader.js +8 -1
  15. package/dist/legacy/rspack/loader.js.map +1 -1
  16. package/dist/legacy/webpack4/index.cjs +977 -0
  17. package/dist/legacy/webpack4/index.cjs.map +1 -0
  18. package/dist/legacy/webpack4/index.d.cts +8 -0
  19. package/dist/legacy/webpack4/index.d.ts +8 -0
  20. package/dist/legacy/webpack4/index.js +953 -0
  21. package/dist/legacy/webpack4/index.js.map +1 -0
  22. package/dist/legacy/webpack4/loader.cjs +347 -0
  23. package/dist/legacy/webpack4/loader.cjs.map +1 -0
  24. package/dist/legacy/webpack4/loader.d.cts +3 -0
  25. package/dist/legacy/webpack4/loader.d.ts +3 -0
  26. package/dist/legacy/webpack4/loader.js +314 -0
  27. package/dist/legacy/webpack4/loader.js.map +1 -0
  28. package/dist/rollup.cjs +428 -101
  29. package/dist/rollup.cjs.map +1 -1
  30. package/dist/rollup.js +433 -102
  31. package/dist/rollup.js.map +1 -1
  32. package/dist/rspack.cjs +428 -101
  33. package/dist/rspack.cjs.map +1 -1
  34. package/dist/rspack.js +433 -102
  35. package/dist/rspack.js.map +1 -1
  36. package/dist/vite.cjs +428 -101
  37. package/dist/vite.cjs.map +1 -1
  38. package/dist/vite.js +433 -102
  39. package/dist/vite.js.map +1 -1
  40. package/dist/webpack.cjs +428 -101
  41. package/dist/webpack.cjs.map +1 -1
  42. package/dist/webpack.js +433 -102
  43. package/dist/webpack.js.map +1 -1
  44. package/package.json +19 -3
package/dist/rspack.js CHANGED
@@ -41,6 +41,10 @@ function shouldTransform(filePath, options) {
41
41
  if (filePath.includes("node_modules")) return false;
42
42
  if (filePath.startsWith("\0")) return false;
43
43
  if (/[/\\](dist|build|\.next|\.nuxt)[/\\]/.test(filePath)) return false;
44
+ const ext = filePath.split(".").pop()?.toLowerCase();
45
+ if (ext && !["js", "jsx", "ts", "tsx", "mjs", "mts", "vue"].includes(ext)) {
46
+ return false;
47
+ }
44
48
  return true;
45
49
  }
46
50
  function buildEscapeTagsSet(escapeTags) {
@@ -205,7 +209,10 @@ function transformVue(options) {
205
209
  const loc = node.loc;
206
210
  if (!loc) return;
207
211
  const { line, column } = loc.start;
208
- const attrValue = formatAttrValue(normalizedPath, line, column + 1);
212
+ const templateStartLoc = descriptor.template.loc.start;
213
+ const absoluteLine = templateStartLoc.line + line - 1;
214
+ const absoluteColumn = line === 1 ? templateStartLoc.column + column - 1 : column;
215
+ const attrValue = formatAttrValue(normalizedPath, absoluteLine, absoluteColumn);
209
216
  const tagNameEnd = loc.start.offset + tagName.length + 1;
210
217
  const absoluteOffset = templateBlockStart + tagNameEnd;
211
218
  ms.appendLeft(absoluteOffset, ` ${attributeName}="${attrValue}"`);
@@ -265,9 +272,11 @@ import http from "http";
265
272
  import fs3 from "fs";
266
273
  import path6 from "path";
267
274
  import os2 from "os";
275
+ import crypto from "crypto";
268
276
  import { execSync, execFileSync } from "child_process";
269
277
  import portfinder from "portfinder";
270
278
  import { launchIDE } from "launch-ide";
279
+ import { INSPECTO_API_PATHS } from "@inspecto-dev/types";
271
280
 
272
281
  // src/server/snippet.ts
273
282
  import * as fs from "fs";
@@ -382,9 +391,84 @@ import fs2 from "fs";
382
391
  import path5 from "path";
383
392
  import os from "os";
384
393
  import { createDefu } from "defu";
385
- import { DEFAULT_TOOL_MODE, VALID_MODES } from "@inspecto-dev/types";
394
+ import {
395
+ DEFAULT_PROVIDER_MODE,
396
+ VALID_MODES,
397
+ DEFAULT_INTENTS
398
+ } from "@inspecto-dev/types";
399
+
400
+ // src/utils/logger.ts
401
+ var LOG_LEVELS = {
402
+ silent: 0,
403
+ error: 1,
404
+ warn: 2,
405
+ info: 3
406
+ };
407
+ function isDebugEnabled(namespace) {
408
+ if (typeof process === "undefined" || !process.env) return false;
409
+ const debugEnv = process.env.DEBUG;
410
+ if (!debugEnv) return false;
411
+ const namespaces = debugEnv.split(",").map((s) => s.trim());
412
+ for (const ns of namespaces) {
413
+ if (ns === "*") return true;
414
+ if (ns.endsWith("*")) {
415
+ const prefix = ns.slice(0, -1);
416
+ if (namespace.startsWith(prefix)) return true;
417
+ } else if (ns === namespace) {
418
+ return true;
419
+ }
420
+ }
421
+ return false;
422
+ }
423
+ var globalLevel = "warn";
424
+ var registeredLoggers = /* @__PURE__ */ new Set();
425
+ function setLoggerGlobalLevel(level) {
426
+ globalLevel = level;
427
+ for (const logger of registeredLoggers) {
428
+ if (logger.setLevel) {
429
+ logger.setLevel(level);
430
+ }
431
+ }
432
+ }
433
+ function createLogger(namespace, options) {
434
+ let currentLevel = options?.logLevel ?? globalLevel;
435
+ let numericLevel = LOG_LEVELS[currentLevel] ?? 2;
436
+ const debugEnabled = isDebugEnabled(namespace);
437
+ const logger = {
438
+ setLevel(level) {
439
+ currentLevel = level;
440
+ numericLevel = LOG_LEVELS[level] ?? 2;
441
+ },
442
+ info(msg, ...args) {
443
+ if (numericLevel >= LOG_LEVELS.info) {
444
+ console.log(`\x1B[36m[inspecto]\x1B[0m ${msg}`, ...args);
445
+ }
446
+ },
447
+ warn(msg, ...args) {
448
+ if (numericLevel >= LOG_LEVELS.warn) {
449
+ console.warn(`\x1B[33m[inspecto] WARN:\x1B[0m ${msg}`, ...args);
450
+ }
451
+ },
452
+ error(msg, ...args) {
453
+ if (numericLevel >= LOG_LEVELS.error) {
454
+ console.error(`\x1B[31m[inspecto] ERROR:\x1B[0m ${msg}`, ...args);
455
+ }
456
+ },
457
+ debug(msg, ...args) {
458
+ if (debugEnabled) {
459
+ console.log(`\x1B[90m[${namespace}]\x1B[0m ${msg}`, ...args);
460
+ }
461
+ }
462
+ };
463
+ registeredLoggers.add(logger);
464
+ return logger;
465
+ }
466
+
467
+ // src/config.ts
468
+ var configLogger = createLogger("inspecto:config");
386
469
  var loadedConfig = null;
387
470
  var loadedPrompts = null;
471
+ var globalLogLevel = "warn";
388
472
  var isWatching = false;
389
473
  var arrayReplaceMerge = createDefu((obj, key, val) => {
390
474
  if (Array.isArray(val)) {
@@ -392,6 +476,15 @@ var arrayReplaceMerge = createDefu((obj, key, val) => {
392
476
  return true;
393
477
  }
394
478
  });
479
+ function setGlobalLogLevel(level) {
480
+ if (level) {
481
+ globalLogLevel = level;
482
+ setLoggerGlobalLevel(level);
483
+ }
484
+ }
485
+ function getGlobalLogLevel() {
486
+ return globalLogLevel;
487
+ }
395
488
  function resolveConfigRoots(cwd, gitRoot) {
396
489
  const roots = [];
397
490
  let current = cwd;
@@ -421,7 +514,7 @@ function loadUserConfigSync(force = false, cwd = process.cwd(), gitRoot) {
421
514
  layers.push(readJsonSafely(path5.join(root, ".inspecto", "settings.json")));
422
515
  }
423
516
  layers.push(readJsonSafely(path5.join(os.homedir(), ".inspecto", "settings.json")));
424
- layers.push({ providers: {} });
517
+ layers.push({});
425
518
  const validLayers = layers.filter((l) => l !== null);
426
519
  loadedConfig = arrayReplaceMerge(...validLayers);
427
520
  return loadedConfig;
@@ -464,50 +557,147 @@ function readJsonSafely(filePath) {
464
557
  }
465
558
  } catch (e) {
466
559
  if (e instanceof SyntaxError) {
467
- console.warn(`[inspecto] Failed to parse config at ${filePath}: Invalid JSON`);
560
+ configLogger.warn(`Failed to parse config at ${filePath}: Invalid JSON`);
468
561
  } else {
469
- console.warn(`[inspecto] Failed to read config at ${filePath}:`, e);
562
+ configLogger.warn(`Failed to read config at ${filePath}:`, e);
470
563
  }
471
564
  }
472
565
  return null;
473
566
  }
474
- function resolveTargetTool(config) {
475
- if (config.prefer) {
476
- return config.prefer;
567
+ function resolveTargetTool(config, ide = "vscode") {
568
+ const defaultProvider = config["provider.default"];
569
+ if (defaultProvider) {
570
+ const tool = defaultProvider.split(".")[0];
571
+ return tool;
477
572
  }
478
- if (config.providers && Object.keys(config.providers).length > 0) {
479
- return Object.keys(config.providers)[0];
480
- }
481
- return "github-copilot";
573
+ return "copilot";
482
574
  }
483
- function resolveToolMode(tool, ide, config) {
575
+ function resolveProviderMode(tool, ide, config) {
484
576
  let requestedType = void 0;
485
- if (config.providers && config.providers[tool] && config.providers[tool].type) {
486
- const type = config.providers[tool].type;
487
- if (type === "plugin" || type === "cli") {
488
- requestedType = type;
489
- }
577
+ const defaultProvider = config["provider.default"];
578
+ if (defaultProvider && defaultProvider.startsWith(`${tool}.`)) {
579
+ const mode = defaultProvider.split(".")[1];
580
+ if (mode === "extension") requestedType = "extension";
581
+ if (mode === "cli") requestedType = "cli";
490
582
  }
491
- requestedType = requestedType ?? DEFAULT_TOOL_MODE[tool];
492
- const valid = VALID_MODES[tool] || [DEFAULT_TOOL_MODE[tool]];
583
+ requestedType = requestedType ?? DEFAULT_PROVIDER_MODE[tool];
584
+ const valid = VALID_MODES[tool] || [DEFAULT_PROVIDER_MODE[tool]];
493
585
  return requestedType && valid.includes(requestedType) ? requestedType : valid[0];
494
586
  }
495
587
  function extractToolOverrides(ide, config) {
496
588
  const result = {};
497
- if (!config.providers) return result;
498
- for (const [tool, cfg] of Object.entries(config.providers)) {
499
- if (!cfg) continue;
500
- const overrides = {
501
- type: cfg.type || DEFAULT_TOOL_MODE[tool] || "plugin"
502
- };
503
- if (cfg.bin) overrides.binaryPath = cfg.bin;
504
- if (cfg.args) overrides.args = cfg.args;
505
- if (cfg.cwd) overrides.cwd = cfg.cwd;
506
- if (cfg.autoSend !== void 0) overrides.autoSend = cfg.autoSend;
507
- result[tool] = overrides;
589
+ if (!config) return result;
590
+ for (const [key, value] of Object.entries(config)) {
591
+ if (!key.startsWith("provider.")) continue;
592
+ if (key === "provider.default") continue;
593
+ const toolIndex = 1;
594
+ const modeIndex = 2;
595
+ const propIndex = 3;
596
+ const parts = key.split(".");
597
+ if (parts.length >= propIndex + 1) {
598
+ const tool = parts[toolIndex];
599
+ const mode = parts[modeIndex];
600
+ const prop = parts[propIndex];
601
+ if (!result[tool]) {
602
+ result[tool] = { type: mode };
603
+ }
604
+ const overrides = result[tool];
605
+ if (prop === "bin") overrides.binaryPath = value;
606
+ if (prop === "args") overrides.args = value;
607
+ if (prop === "cwd") overrides.cwd = value;
608
+ if (prop === "coldStartDelay") overrides.coldStartDelay = value;
609
+ }
508
610
  }
509
611
  return result;
510
612
  }
613
+ function resolveIntents(serverPrompts) {
614
+ const baseMap = /* @__PURE__ */ new Map();
615
+ for (const intent of DEFAULT_INTENTS) {
616
+ if (intent.id) baseMap.set(intent.id, { ...intent });
617
+ }
618
+ const defaults = () => ensureOpenInEditorLast(Array.from(baseMap.values()));
619
+ if (!serverPrompts) return defaults();
620
+ const isReplace = !Array.isArray(serverPrompts) && typeof serverPrompts === "object" && serverPrompts.$replace === true;
621
+ const promptsArray = Array.isArray(serverPrompts) ? serverPrompts : isReplace ? serverPrompts.items : [];
622
+ if (!promptsArray || promptsArray.length === 0) return defaults();
623
+ if (isReplace) {
624
+ const result = [];
625
+ for (const item of promptsArray) {
626
+ if (typeof item === "string") {
627
+ if (baseMap.has(item)) {
628
+ result.push(baseMap.get(item));
629
+ } else {
630
+ configLogger.warn(
631
+ `Unknown built-in intent id: "${item}". Available: ${[...baseMap.keys()].join(", ")}`
632
+ );
633
+ }
634
+ } else if (typeof item === "object") {
635
+ if (!item.id) {
636
+ configLogger.warn('Intent object missing required "id" field, skipping.');
637
+ continue;
638
+ }
639
+ if (item.enabled === false) {
640
+ configLogger.warn(
641
+ `Intent "${item.id}" is listed in $replace but has enabled:false \u2014 it will be excluded.`
642
+ );
643
+ continue;
644
+ }
645
+ if (item.isAction && item.id !== "open-in-editor") {
646
+ configLogger.warn(
647
+ `isAction is reserved for built-in actions. Ignoring intent "${item.id}".`
648
+ );
649
+ continue;
650
+ }
651
+ result.push(baseMap.has(item.id) ? { ...baseMap.get(item.id), ...item } : item);
652
+ }
653
+ }
654
+ return ensureOpenInEditorLast(result);
655
+ }
656
+ const merged = Array.from(baseMap.values());
657
+ for (const item of promptsArray) {
658
+ if (typeof item === "string") {
659
+ if (!baseMap.has(item)) {
660
+ configLogger.warn(
661
+ `Unknown built-in intent id: "${item}". In append mode, strings have no effect on ordering \u2014 use $replace to control order.`
662
+ );
663
+ }
664
+ continue;
665
+ }
666
+ if (typeof item === "object") {
667
+ if (!item.id) {
668
+ configLogger.warn('Intent object missing required "id" field, skipping.');
669
+ continue;
670
+ }
671
+ if (item.isAction && item.id !== "open-in-editor") {
672
+ configLogger.warn(
673
+ `isAction is reserved for built-in actions. Ignoring intent "${item.id}".`
674
+ );
675
+ continue;
676
+ }
677
+ const existingIdx = merged.findIndex((i) => i.id === item.id);
678
+ if (existingIdx !== -1) {
679
+ if (item.enabled === false) {
680
+ merged.splice(existingIdx, 1);
681
+ } else {
682
+ merged[existingIdx] = { ...merged[existingIdx], ...item };
683
+ }
684
+ } else {
685
+ if (item.enabled !== false) {
686
+ merged.push(item);
687
+ }
688
+ }
689
+ }
690
+ }
691
+ return ensureOpenInEditorLast(merged);
692
+ }
693
+ function ensureOpenInEditorLast(intents) {
694
+ const idx = intents.findIndex((i) => i.id === "open-in-editor");
695
+ if (idx === -1 || idx === intents.length - 1) return intents;
696
+ const result = [...intents];
697
+ const item = result.splice(idx, 1)[0];
698
+ result.push(item);
699
+ return result;
700
+ }
511
701
  var watchers = [];
512
702
  function watchConfig(onReload, cwd = process.cwd(), gitRoot) {
513
703
  if (isWatching) return;
@@ -542,6 +732,19 @@ function watchConfig(onReload, cwd = process.cwd(), gitRoot) {
542
732
  }
543
733
 
544
734
  // src/server/index.ts
735
+ var serverLogger = createLogger("inspecto:server", { logLevel: getGlobalLogLevel() });
736
+ var payloadTickets = /* @__PURE__ */ new Map();
737
+ function createTicket(payload) {
738
+ const ticketId = crypto.randomUUID();
739
+ payloadTickets.set(ticketId, JSON.stringify(payload));
740
+ setTimeout(
741
+ () => {
742
+ payloadTickets.delete(ticketId);
743
+ },
744
+ 5 * 60 * 1e3
745
+ );
746
+ return ticketId;
747
+ }
545
748
  var serverState = {
546
749
  port: null,
547
750
  running: false,
@@ -553,8 +756,11 @@ var serverInstance = null;
553
756
  function resolveProjectRoot() {
554
757
  let gitRoot;
555
758
  try {
759
+ serverLogger.info("Resolving project root...");
556
760
  gitRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
557
- } catch {
761
+ serverLogger.info("Resolved project root: " + gitRoot);
762
+ } catch (e) {
763
+ serverLogger.error("Failed to resolve project root:", e);
558
764
  gitRoot = process.cwd();
559
765
  }
560
766
  let current = gitRoot;
@@ -576,7 +782,7 @@ function launchURI(uri) {
576
782
  execFileSync("xdg-open", [uri]);
577
783
  }
578
784
  } catch (e) {
579
- console.error("[inspecto] Failed to launch URI via execFileSync, falling back to launchIDE:", e);
785
+ serverLogger.error("Failed to launch URI via execFileSync, falling back to launchIDE:", e);
580
786
  launchIDE({ file: uri });
581
787
  }
582
788
  }
@@ -591,7 +797,7 @@ async function startServer() {
591
797
  const port = await portfinder.getPortPromise();
592
798
  watchConfig(
593
799
  () => {
594
- console.log("[inspecto] user config reloaded.");
800
+ serverLogger.info("user config reloaded.");
595
801
  },
596
802
  serverState.cwd,
597
803
  serverState.configRoot
@@ -607,7 +813,7 @@ async function startServer() {
607
813
  }
608
814
  const url = new URL(req.url ?? "/", `http://localhost:${port}`);
609
815
  handleRequest(url, req, res).catch((err) => {
610
- console.error("[inspecto] server error:", err);
816
+ serverLogger.error("server error:", err);
611
817
  res.writeHead(500, { "Content-Type": "application/json" });
612
818
  res.end(JSON.stringify({ success: false, error: String(err) }));
613
819
  });
@@ -620,22 +826,41 @@ async function startServer() {
620
826
  serverInstance.once("error", reject);
621
827
  });
622
828
  serverInstance.on("error", (err) => {
623
- console.error("[inspecto] persistent server error:", err);
829
+ serverLogger.error("persistent server error:", err);
624
830
  });
625
831
  serverState.port = port;
626
832
  serverState.running = true;
627
- const portFile = path6.join(os2.tmpdir(), "inspecto.port");
833
+ const portFile = path6.join(os2.tmpdir(), "inspecto.port.json");
628
834
  try {
629
- fs3.writeFileSync(portFile, String(port), "utf-8");
630
- } catch {
835
+ let portData = {};
836
+ if (fs3.existsSync(portFile)) {
837
+ try {
838
+ portData = JSON.parse(fs3.readFileSync(portFile, "utf-8"));
839
+ } catch (e) {
840
+ }
841
+ }
842
+ const rootHash = crypto.createHash("md5").update(serverState.projectRoot).digest("hex");
843
+ portData[rootHash] = port;
844
+ fs3.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
845
+ } catch (e) {
846
+ serverLogger.warn("Failed to write port file:", e);
631
847
  }
632
848
  process.once("exit", () => {
633
849
  try {
634
- fs3.unlinkSync(portFile);
850
+ if (fs3.existsSync(portFile)) {
851
+ const portData = JSON.parse(fs3.readFileSync(portFile, "utf-8"));
852
+ const rootHash = crypto.createHash("md5").update(serverState.projectRoot).digest("hex");
853
+ delete portData[rootHash];
854
+ if (Object.keys(portData).length === 0) {
855
+ fs3.unlinkSync(portFile);
856
+ } else {
857
+ fs3.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
858
+ }
859
+ }
635
860
  } catch {
636
861
  }
637
862
  });
638
- console.log(`[inspecto] server running at http://127.0.0.1:${port}`);
863
+ serverLogger.info(`server running at http://127.0.0.1:${port}`);
639
864
  return port;
640
865
  }
641
866
  async function readBody(req) {
@@ -648,66 +873,63 @@ async function readBody(req) {
648
873
  }
649
874
  async function handleRequest(url, req, res) {
650
875
  const pathname = url.pathname;
651
- if (pathname === "/health" && req.method === "GET") {
876
+ if ((pathname === "/health" || pathname === INSPECTO_API_PATHS.HEALTH) && req.method === "GET") {
652
877
  res.writeHead(200, { "Content-Type": "application/json" });
653
878
  res.end(JSON.stringify({ ok: true, port: serverState.port }));
654
879
  return;
655
880
  }
656
- if (pathname === "/config" && req.method === "GET") {
881
+ if (pathname === INSPECTO_API_PATHS.CLIENT_CONFIG && req.method === "GET") {
657
882
  const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
658
883
  const promptsConfig = await loadPromptsConfig(false, serverState.cwd, serverState.configRoot);
659
884
  const effectiveIde = userConfig.ide ?? "vscode";
660
885
  let info;
661
886
  if (!serverState.ideInfo) {
662
- const fallbackTargets = userConfig.providers ? Object.keys(userConfig.providers) : ["claude-code", "gemini", "coco", "codex"];
663
887
  info = {
664
- ide: effectiveIde,
665
- providers: fallbackTargets.reduce(
666
- (acc, target) => {
667
- acc[target] = {
668
- mode: resolveToolMode(target, effectiveIde, userConfig),
669
- installed: false
670
- };
671
- return acc;
672
- },
673
- {}
674
- )
888
+ ide: effectiveIde
675
889
  };
676
890
  } else {
677
891
  const { scheme: _scheme, ...rest } = serverState.ideInfo;
678
892
  info = rest;
679
893
  }
680
- const resolvedProviders = { ...info.providers };
681
- for (const tool in resolvedProviders) {
682
- resolvedProviders[tool].mode = resolveToolMode(tool, info.ide, userConfig);
683
- }
684
894
  const config = {
685
895
  ...info,
686
- providers: resolvedProviders,
687
- providerOverrides: extractToolOverrides(info.ide, userConfig),
688
- prompts: promptsConfig,
689
- hotKeys: userConfig.hotKeys,
690
- includeSnippet: userConfig.includeSnippet
896
+ prompts: resolveIntents(promptsConfig),
897
+ hotKeys: userConfig["inspector.hotKey"] ?? "alt",
898
+ theme: userConfig["inspector.theme"] ?? "auto",
899
+ includeSnippet: userConfig["prompt.includeSnippet"] ?? false,
900
+ autoSend: userConfig["prompt.autoSend"] ?? false
691
901
  };
902
+ delete config.providers;
692
903
  res.writeHead(200, { "Content-Type": "application/json" });
693
904
  res.end(JSON.stringify(config));
694
905
  return;
695
906
  }
696
- if (pathname === "/config" && req.method === "POST") {
907
+ if (pathname === INSPECTO_API_PATHS.IDE_INFO && req.method === "POST") {
697
908
  try {
698
909
  const body = JSON.parse(await readBody(req));
699
- serverState.ideInfo = body;
700
- console.log(`[inspecto] Received IDE info from extension:`, body);
910
+ const ideWorkspace = body.workspaceRoot || "";
911
+ const serverProjectRoot = serverState.projectRoot || "";
912
+ const isSameProject = !ideWorkspace || !serverProjectRoot || ideWorkspace === serverProjectRoot || serverProjectRoot.startsWith(ideWorkspace);
913
+ if (isSameProject) {
914
+ serverState.ideInfo = body;
915
+ serverLogger.debug(
916
+ `Accepted IDE info from matched workspace (ide-${body.ide} / schema-${body.scheme})`
917
+ );
918
+ } else {
919
+ serverLogger.debug(
920
+ `Ignored IDE info from unrelated workspace (IDE Workspace: ${ideWorkspace}, Server: ${serverProjectRoot}, Scheme: ${body.scheme}, IDE: ${body.ide})`
921
+ );
922
+ }
701
923
  res.writeHead(200, { "Content-Type": "application/json" });
702
924
  res.end(JSON.stringify({ success: true }));
703
925
  } catch (e) {
704
- console.error("[inspecto] Error parsing /config POST request:", e);
926
+ serverLogger.error(`Error parsing ${INSPECTO_API_PATHS.IDE_INFO} POST request:`, e);
705
927
  res.writeHead(400, { "Content-Type": "application/json" });
706
928
  res.end(JSON.stringify({ error: "Invalid JSON body" }));
707
929
  }
708
930
  return;
709
931
  }
710
- if (pathname === "/open" && req.method === "POST") {
932
+ if (pathname === INSPECTO_API_PATHS.IDE_OPEN && req.method === "POST") {
711
933
  let body;
712
934
  try {
713
935
  body = JSON.parse(await readBody(req));
@@ -716,28 +938,98 @@ async function handleRequest(url, req, res) {
716
938
  res.end(JSON.stringify({ error: "Invalid JSON body" }));
717
939
  return;
718
940
  }
719
- const absolutePath = path6.isAbsolute(body.file) ? body.file : path6.resolve(serverState.cwd, body.file);
941
+ const absolutePath = path6.isAbsolute(body.file) ? path6.resolve(body.file) : path6.resolve(serverState.cwd, body.file);
942
+ const relativeToRoot = path6.relative(serverState.projectRoot, absolutePath);
943
+ if (relativeToRoot.startsWith("..") || path6.isAbsolute(relativeToRoot)) {
944
+ serverLogger.warn(`Security: Blocked path traversal attempt in IDE_OPEN: ${body.file}`);
945
+ res.writeHead(403, { "Content-Type": "application/json" });
946
+ res.end(JSON.stringify({ error: "Access denied: File is outside of project workspace" }));
947
+ return;
948
+ }
720
949
  const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
721
- const ide = userConfig.ide ?? "vscode";
722
- const editorHint = "code";
723
- launchIDE({
724
- file: absolutePath,
725
- line: body.line,
726
- column: body.column,
727
- editor: editorHint,
728
- type: process.platform === "darwin" ? "open" : "exec"
729
- });
950
+ const configuredIde = userConfig.ide;
951
+ const activeIde = serverState.ideInfo?.ide;
952
+ const activeIdeScheme = serverState.ideInfo?.scheme;
953
+ const rawEditorHint = configuredIde || activeIde || activeIdeScheme || "code";
954
+ if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
955
+ serverLogger.warn(
956
+ `Active IDE is ${activeIdeScheme}, but config forces ${configuredIde}. Using configured IDE.`
957
+ );
958
+ }
959
+ let editorHint = rawEditorHint;
960
+ if (rawEditorHint === "vscode") editorHint = "code";
961
+ else if (rawEditorHint === "vscode-insiders") editorHint = "code-insiders";
962
+ else if (rawEditorHint === "vscodium") editorHint = "codium";
963
+ else if (rawEditorHint === "trae-cn" || rawEditorHint === "trae") editorHint = "trae";
964
+ serverLogger.debug(
965
+ `IDE_OPEN: activeIde=${activeIde}, activeIdeScheme=${activeIdeScheme}, configuredIde=${configuredIde} -> rawEditorHint=${rawEditorHint}, finalEditorHint=${editorHint}`
966
+ );
967
+ const VSCODE_FAMILY_SCHEMES = [
968
+ "vscode",
969
+ "vscode-insiders",
970
+ "cursor",
971
+ "windsurf",
972
+ "trae",
973
+ "trae-cn",
974
+ "vscodium",
975
+ "codebuddy",
976
+ "codebuddy-cn",
977
+ "antigravity"
978
+ ];
979
+ if (VSCODE_FAMILY_SCHEMES.includes(rawEditorHint)) {
980
+ const uri = `${rawEditorHint}://file${absolutePath}:${body.line}:${body.column}`;
981
+ serverLogger.debug(`IDE_OPEN: Bypassing launchIDE, using URI scheme directly: ${uri}`);
982
+ try {
983
+ if (process.platform === "darwin") {
984
+ execFileSync("open", [uri]);
985
+ } else if (process.platform === "win32") {
986
+ execFileSync("cmd", ["/c", "start", '""', uri]);
987
+ } else {
988
+ execFileSync("xdg-open", [uri]);
989
+ }
990
+ } catch (e) {
991
+ serverLogger.error(`Failed to launch URI for IDE_OPEN (${uri}):`, e);
992
+ launchIDE({
993
+ file: absolutePath,
994
+ line: body.line,
995
+ column: body.column,
996
+ editor: editorHint,
997
+ type: process.platform === "darwin" ? "open" : "exec"
998
+ });
999
+ }
1000
+ } else {
1001
+ launchIDE({
1002
+ file: absolutePath,
1003
+ line: body.line,
1004
+ column: body.column,
1005
+ editor: editorHint,
1006
+ type: process.platform === "darwin" ? "open" : "exec"
1007
+ });
1008
+ }
730
1009
  res.writeHead(200, { "Content-Type": "application/json" });
731
1010
  res.end(JSON.stringify({ success: true }));
732
1011
  return;
733
1012
  }
734
- if (pathname === "/snippet" && req.method === "GET") {
1013
+ if (pathname === INSPECTO_API_PATHS.PROJECT_SNIPPET && req.method === "GET") {
735
1014
  const file = url.searchParams.get("file") ?? "";
736
1015
  const line = parseInt(url.searchParams.get("line") ?? "1", 10);
737
1016
  const column = parseInt(url.searchParams.get("column") ?? "1", 10);
738
1017
  const maxLines = parseInt(url.searchParams.get("maxLines") ?? "100", 10);
739
1018
  try {
740
- const absolutePath = path6.isAbsolute(file) ? file : path6.resolve(serverState.cwd, file);
1019
+ const absolutePath = path6.isAbsolute(file) ? path6.resolve(file) : path6.resolve(serverState.cwd, file);
1020
+ const relativeToRoot = path6.relative(serverState.projectRoot, absolutePath);
1021
+ if (relativeToRoot.startsWith("..") || path6.isAbsolute(relativeToRoot)) {
1022
+ serverLogger.warn(`Security: Blocked path traversal attempt in PROJECT_SNIPPET: ${file}`);
1023
+ res.writeHead(403, { "Content-Type": "application/json" });
1024
+ res.end(
1025
+ JSON.stringify({
1026
+ success: false,
1027
+ error: "Access denied: File is outside of project workspace",
1028
+ errorCode: "FORBIDDEN"
1029
+ })
1030
+ );
1031
+ return;
1032
+ }
741
1033
  const result = await extractSnippet({ file: absolutePath, line, column, maxLines });
742
1034
  res.writeHead(200, { "Content-Type": "application/json" });
743
1035
  res.end(JSON.stringify(result));
@@ -749,7 +1041,7 @@ async function handleRequest(url, req, res) {
749
1041
  }
750
1042
  return;
751
1043
  }
752
- if (pathname === "/send-to-ai" && req.method === "POST") {
1044
+ if (pathname === INSPECTO_API_PATHS.AI_DISPATCH && req.method === "POST") {
753
1045
  try {
754
1046
  const rawBody = await readBody(req);
755
1047
  const body = JSON.parse(rawBody);
@@ -757,19 +1049,30 @@ async function handleRequest(url, req, res) {
757
1049
  res.writeHead(result.success ? 200 : 500, { "Content-Type": "application/json" });
758
1050
  res.end(JSON.stringify(result));
759
1051
  } catch (e) {
760
- console.error("[inspecto] Error parsing /send-to-ai request:", e);
1052
+ serverLogger.error(`Error parsing ${INSPECTO_API_PATHS.AI_DISPATCH} request:`, e);
761
1053
  res.writeHead(500, { "Content-Type": "application/json" });
762
1054
  res.end(JSON.stringify({ success: false, error: String(e), errorCode: "INTERNAL_ERROR" }));
763
1055
  }
764
1056
  return;
765
1057
  }
1058
+ if (pathname.startsWith(`${INSPECTO_API_PATHS.AI_TICKET}/`) && req.method === "GET") {
1059
+ const ticketId = pathname.substring(INSPECTO_API_PATHS.AI_TICKET.length + 1);
1060
+ const payloadStr = payloadTickets.get(ticketId);
1061
+ if (!payloadStr) {
1062
+ res.writeHead(404, { "Content-Type": "application/json" });
1063
+ res.end(JSON.stringify({ success: false, error: "Ticket not found or expired" }));
1064
+ return;
1065
+ }
1066
+ res.writeHead(200, { "Content-Type": "application/json" });
1067
+ res.end(payloadStr);
1068
+ return;
1069
+ }
766
1070
  res.writeHead(404, { "Content-Type": "application/json" });
767
1071
  res.end(JSON.stringify({ error: "not found" }));
768
1072
  }
769
1073
  async function dispatchToAi(req) {
770
1074
  const { location, snippet, prompt } = req;
771
1075
  const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
772
- const ide = userConfig.ide ?? "vscode";
773
1076
  const resolvedTarget = resolveTargetTool(userConfig);
774
1077
  const formattedPrompt = prompt ?? `Please help me with this code from \`${location.file}\` (line ${location.line}):
775
1078
 
@@ -777,22 +1080,45 @@ async function dispatchToAi(req) {
777
1080
  ${snippet}
778
1081
  \`\`\`
779
1082
  `;
1083
+ const ideReportedMode = serverState.ideInfo?.providers[resolvedTarget]?.mode;
1084
+ const configuredIde = userConfig.ide;
1085
+ const activeIde = serverState.ideInfo?.ide;
1086
+ const activeIdeScheme = serverState.ideInfo?.scheme;
1087
+ const finalIde = configuredIde || activeIdeScheme || activeIde || "vscode";
1088
+ if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
1089
+ serverLogger.warn(
1090
+ `dispatchToAi: Active IDE is ${activeIdeScheme}, but config forces ${configuredIde}. Using configured IDE.`
1091
+ );
1092
+ }
1093
+ const mode = resolveProviderMode(resolvedTarget, finalIde, userConfig);
1094
+ const overrides = extractToolOverrides(finalIde, userConfig)[resolvedTarget] || {};
1095
+ overrides.type = mode;
1096
+ const fullPayload = {
1097
+ ide: finalIde,
1098
+ target: resolvedTarget,
1099
+ targetType: mode,
1100
+ prompt: formattedPrompt,
1101
+ filePath: location.file,
1102
+ line: location.line,
1103
+ column: location.column,
1104
+ snippet,
1105
+ overrides: Object.keys(overrides).length > 0 ? overrides : void 0,
1106
+ autoSend: userConfig["prompt.autoSend"] !== void 0 ? Boolean(userConfig["prompt.autoSend"]) : void 0
1107
+ };
1108
+ const ticketId = createTicket(fullPayload);
780
1109
  const params = new URLSearchParams();
1110
+ params.set("ticket", ticketId);
781
1111
  params.set("target", resolvedTarget);
782
- const overrides = extractToolOverrides(ide, userConfig)[resolvedTarget];
783
- if (overrides) {
784
- params.set("overrides", JSON.stringify(overrides));
785
- }
786
- params.set("prompt", formattedPrompt);
787
- params.set("file", location.file);
788
- params.set("line", String(location.line));
789
- params.set("col", String(location.column));
790
- params.set("snippet", snippet);
791
- const scheme = serverState.ideInfo?.scheme || "vscode";
792
- const uri = `${scheme}://inspecto.inspecto/send?${params.toString()}`;
793
- console.log(`[inspecto] dispatchToAi: Generated URI: ${uri}`);
1112
+ const uri = `${finalIde}://inspecto.inspecto/send?${params.toString()}`;
1113
+ serverLogger.debug(`dispatchToAi: Generated URI: ${uri}`);
794
1114
  launchURI(uri);
795
- return { success: true };
1115
+ return {
1116
+ success: true,
1117
+ fallbackPayload: {
1118
+ prompt: formattedPrompt,
1119
+ file: location.file
1120
+ }
1121
+ };
796
1122
  }
797
1123
 
798
1124
  // src/injectors/utils.ts
@@ -848,9 +1174,11 @@ if (typeof window !== 'undefined') {
848
1174
  }
849
1175
  function injectWebpack(compiler, serverPortFn, resolveClientModule2) {
850
1176
  const inspectoClientPath = resolveClientModule2();
851
- new compiler.webpack.EntryPlugin(compiler.context, inspectoClientPath, { name: void 0 }).apply(
852
- compiler
853
- );
1177
+ if (compiler.webpack && compiler.webpack.EntryPlugin) {
1178
+ new compiler.webpack.EntryPlugin(compiler.context, inspectoClientPath, {
1179
+ name: void 0
1180
+ }).apply(compiler);
1181
+ }
854
1182
  compiler.hooks.compilation.tap("inspecto-overlay", (compilation) => {
855
1183
  const HtmlWebpackPlugin = compiler.options.plugins.find(
856
1184
  (p) => p && p.constructor && p.constructor.name === "HtmlWebpackPlugin"
@@ -932,7 +1260,8 @@ var DEFAULT_OPTIONS = {
932
1260
  exclude: [],
933
1261
  escapeTags: [],
934
1262
  pathType: "absolute",
935
- attributeName: "data-inspecto"
1263
+ attributeName: "data-inspecto",
1264
+ logLevel: "warn"
936
1265
  };
937
1266
  var DEFAULT_PORT = 5678;
938
1267
  var getCleanId = (id) => id.split("?")[0];
@@ -941,6 +1270,8 @@ var InspectoPlugin = createUnplugin((userOptions = {}) => {
941
1270
  ...DEFAULT_OPTIONS,
942
1271
  ...userOptions
943
1272
  };
1273
+ setGlobalLogLevel(options.logLevel);
1274
+ const pluginLogger = createLogger("inspecto:plugin", { logLevel: options.logLevel });
944
1275
  const isProduction = process.env["NODE_ENV"] === "production";
945
1276
  let projectRoot = process.cwd();
946
1277
  let serverPort = null;
@@ -956,7 +1287,7 @@ var InspectoPlugin = createUnplugin((userOptions = {}) => {
956
1287
  buildStart() {
957
1288
  if (isProduction) return;
958
1289
  projectRoot = serverState.cwd || process.cwd();
959
- ensureServer().catch(console.error);
1290
+ ensureServer().catch((err) => pluginLogger.error("Failed to start server:", err));
960
1291
  },
961
1292
  buildEnd() {
962
1293
  },