@inspecto-dev/plugin 0.2.0-alpha.3 → 0.3.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -72,13 +72,11 @@ var resolveClientModule = () => {
72
72
 
73
73
  // src/server/index.ts
74
74
  var import_node_http = __toESM(require("http"), 1);
75
- var import_node_fs2 = __toESM(require("fs"), 1);
76
- var import_node_path2 = __toESM(require("path"), 1);
75
+ var import_node_fs3 = __toESM(require("fs"), 1);
76
+ var import_node_path4 = __toESM(require("path"), 1);
77
77
  var import_node_os2 = __toESM(require("os"), 1);
78
- var import_node_crypto = __toESM(require("crypto"), 1);
79
- var import_node_child_process = require("child_process");
78
+ var import_node_crypto2 = __toESM(require("crypto"), 1);
80
79
  var import_portfinder = __toESM(require("portfinder"), 1);
81
- var import_launch_ide = require("launch-ide");
82
80
  var import_types2 = require("@inspecto-dev/types");
83
81
 
84
82
  // src/server/snippet.ts
@@ -398,9 +396,9 @@ function extractToolOverrides(ide, config) {
398
396
  function resolveIntents(serverPrompts) {
399
397
  const baseMap = /* @__PURE__ */ new Map();
400
398
  for (const intent of import_types.DEFAULT_INTENTS) {
401
- if (intent.id) baseMap.set(intent.id, { ...intent });
399
+ baseMap.set(intent.id, { ...intent });
402
400
  }
403
- const defaults = () => ensureOpenInEditorLast(Array.from(baseMap.values()));
401
+ const defaults = () => Array.from(baseMap.values());
404
402
  if (!serverPrompts) return defaults();
405
403
  const isReplace = !Array.isArray(serverPrompts) && typeof serverPrompts === "object" && serverPrompts.$replace === true;
406
404
  const promptsArray = Array.isArray(serverPrompts) ? serverPrompts : isReplace ? serverPrompts.items : [];
@@ -427,16 +425,18 @@ function resolveIntents(serverPrompts) {
427
425
  );
428
426
  continue;
429
427
  }
430
- if (item.isAction && item.id !== "open-in-editor") {
428
+ if (!item.aiIntent) {
431
429
  configLogger.warn(
432
- `isAction is reserved for built-in actions. Ignoring intent "${item.id}".`
430
+ `Intent "${item.id}" is missing required "aiIntent".`
433
431
  );
434
432
  continue;
435
433
  }
436
- result.push(baseMap.has(item.id) ? { ...baseMap.get(item.id), ...item } : item);
434
+ result.push(
435
+ baseMap.has(item.id) ? { ...baseMap.get(item.id), ...item } : item
436
+ );
437
437
  }
438
438
  }
439
- return ensureOpenInEditorLast(result);
439
+ return result;
440
440
  }
441
441
  const merged = Array.from(baseMap.values());
442
442
  for (const item of promptsArray) {
@@ -453,9 +453,9 @@ function resolveIntents(serverPrompts) {
453
453
  configLogger.warn('Intent object missing required "id" field, skipping.');
454
454
  continue;
455
455
  }
456
- if (item.isAction && item.id !== "open-in-editor") {
456
+ if (!item.aiIntent) {
457
457
  configLogger.warn(
458
- `isAction is reserved for built-in actions. Ignoring intent "${item.id}".`
458
+ `Intent "${item.id}" is missing required "aiIntent".`
459
459
  );
460
460
  continue;
461
461
  }
@@ -473,15 +473,7 @@ function resolveIntents(serverPrompts) {
473
473
  }
474
474
  }
475
475
  }
476
- return ensureOpenInEditorLast(merged);
477
- }
478
- function ensureOpenInEditorLast(intents) {
479
- const idx = intents.findIndex((i) => i.id === "open-in-editor");
480
- if (idx === -1 || idx === intents.length - 1) return intents;
481
- const result = [...intents];
482
- const item = result.splice(idx, 1)[0];
483
- result.push(item);
484
- return result;
476
+ return merged;
485
477
  }
486
478
  var watchers = [];
487
479
  function watchConfig(onReload, cwd = process.cwd(), gitRoot) {
@@ -516,7 +508,10 @@ function watchConfig(onReload, cwd = process.cwd(), gitRoot) {
516
508
  }
517
509
  }
518
510
 
519
- // src/server/index.ts
511
+ // src/server/dispatch-transport.ts
512
+ var import_node_crypto = __toESM(require("crypto"), 1);
513
+ var import_node_child_process = require("child_process");
514
+ var import_launch_ide = require("launch-ide");
520
515
  var serverLogger = createLogger("inspecto:server", { logLevel: getGlobalLogLevel() });
521
516
  var payloadTickets = /* @__PURE__ */ new Map();
522
517
  function createTicket(payload) {
@@ -530,32 +525,8 @@ function createTicket(payload) {
530
525
  );
531
526
  return ticketId;
532
527
  }
533
- var serverState = {
534
- port: null,
535
- running: false,
536
- projectRoot: "",
537
- configRoot: "",
538
- cwd: process.cwd()
539
- };
540
- var serverInstance = null;
541
- function resolveProjectRoot() {
542
- let gitRoot;
543
- try {
544
- serverLogger.info("Resolving project root...");
545
- gitRoot = (0, import_node_child_process.execSync)("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
546
- serverLogger.info("Resolved project root: " + gitRoot);
547
- } catch (e) {
548
- serverLogger.error("Failed to resolve project root:", e);
549
- gitRoot = process.cwd();
550
- }
551
- let current = gitRoot;
552
- while (true) {
553
- if (import_node_fs2.default.existsSync(import_node_path2.default.join(current, ".inspecto"))) return current;
554
- const parent = import_node_path2.default.dirname(current);
555
- if (parent === current) break;
556
- current = parent;
557
- }
558
- return gitRoot;
528
+ function readTicket(ticketId) {
529
+ return payloadTickets.get(ticketId);
559
530
  }
560
531
  function launchURI(uri) {
561
532
  try {
@@ -571,6 +542,378 @@ function launchURI(uri) {
571
542
  (0, import_launch_ide.launchIDE)({ file: uri });
572
543
  }
573
544
  }
545
+
546
+ // src/server/dispatch-runtime.ts
547
+ function resolvePromptDispatchRuntime(state) {
548
+ const userConfig = loadUserConfigSync(false, state.cwd, state.projectRoot);
549
+ const resolvedTarget = resolveTargetTool(userConfig);
550
+ const finalIde = resolveFinalIde(userConfig.ide, state.ideInfo?.ide, state.ideInfo?.scheme);
551
+ const mode = resolveProviderMode(resolvedTarget, finalIde, userConfig);
552
+ const overrides = extractToolOverrides(finalIde, userConfig)[resolvedTarget] || void 0;
553
+ return {
554
+ resolvedTarget,
555
+ finalIde,
556
+ mode,
557
+ ...hasOverrides(overrides) ? { overrides } : {},
558
+ ...userConfig["prompt.autoSend"] !== void 0 ? { autoSend: Boolean(userConfig["prompt.autoSend"]) } : {}
559
+ };
560
+ }
561
+ function dispatchPromptThroughIde(runtime, payload) {
562
+ const ticketId = createTicket({
563
+ ide: runtime.finalIde,
564
+ target: runtime.resolvedTarget,
565
+ targetType: runtime.mode,
566
+ prompt: payload.prompt,
567
+ filePath: payload.filePath,
568
+ line: payload.line,
569
+ column: payload.column,
570
+ snippet: payload.snippet,
571
+ ...payload.screenshotContext ? { screenshotContext: payload.screenshotContext } : {},
572
+ overrides: runtime.overrides,
573
+ autoSend: runtime.autoSend
574
+ });
575
+ const params = new URLSearchParams();
576
+ params.set("ticket", ticketId);
577
+ params.set("target", runtime.resolvedTarget);
578
+ launchURI(`${runtime.finalIde}://inspecto.inspecto/send?${params.toString()}`);
579
+ return {
580
+ success: true,
581
+ fallbackPayload: {
582
+ prompt: payload.prompt,
583
+ ...payload.filePath ? { file: payload.filePath } : {}
584
+ }
585
+ };
586
+ }
587
+ function resolveFinalIde(configuredIde, activeIde, activeIdeScheme) {
588
+ if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
589
+ return configuredIde;
590
+ }
591
+ return configuredIde || activeIdeScheme || activeIde || "vscode";
592
+ }
593
+ function hasOverrides(overrides) {
594
+ return Boolean(overrides && Object.keys(overrides).length > 0);
595
+ }
596
+
597
+ // src/server/path-guards.ts
598
+ var import_node_path2 = __toESM(require("path"), 1);
599
+ function isWindowsAbsolutePath(file) {
600
+ return /^[a-zA-Z]:[\\/]/.test(file) || /^\\\\[^\\]+\\[^\\]+/.test(file);
601
+ }
602
+ function resolveWorkspacePath(file, cwd) {
603
+ if (isWindowsAbsolutePath(file)) {
604
+ return import_node_path2.default.win32.normalize(file);
605
+ }
606
+ return import_node_path2.default.isAbsolute(file) ? import_node_path2.default.resolve(file) : import_node_path2.default.resolve(cwd, file);
607
+ }
608
+ function assertPathWithinProject(file, projectRoot) {
609
+ const relativeToRoot = isWindowsAbsolutePath(file) || isWindowsAbsolutePath(projectRoot) ? import_node_path2.default.win32.relative(import_node_path2.default.win32.normalize(projectRoot), import_node_path2.default.win32.normalize(file)) : import_node_path2.default.relative(projectRoot, file);
610
+ if (relativeToRoot.startsWith("..") || import_node_path2.default.isAbsolute(relativeToRoot)) {
611
+ throw new Error("Access denied: File is outside of project workspace");
612
+ }
613
+ }
614
+
615
+ // src/server/annotation-dispatch.ts
616
+ var AnnotationDispatchError = class extends Error {
617
+ constructor(message, errorCode) {
618
+ super(message);
619
+ this.name = "AnnotationDispatchError";
620
+ this.errorCode = errorCode;
621
+ }
622
+ };
623
+ async function dispatchAnnotationsToAi(req, state) {
624
+ try {
625
+ validateAnnotationDispatchRequest(req, state);
626
+ const batch = normalizeAnnotationBatch(req);
627
+ const prompt = buildAnnotationBatchPrompt(batch);
628
+ const representativeTarget = batch.annotations[0]?.targets[0];
629
+ const runtime = resolvePromptDispatchRuntime(state);
630
+ return dispatchPromptThroughIde(runtime, {
631
+ prompt,
632
+ ...representativeTarget?.file ? { filePath: representativeTarget.file } : {},
633
+ ...representativeTarget?.line ? { line: representativeTarget.line } : {},
634
+ ...representativeTarget?.column ? { column: representativeTarget.column } : {},
635
+ ...batch.screenshotContext ? { screenshotContext: batch.screenshotContext } : {}
636
+ });
637
+ } catch (error) {
638
+ return {
639
+ success: false,
640
+ error: error instanceof Error ? error.message : String(error),
641
+ errorCode: getAnnotationDispatchErrorCode(error)
642
+ };
643
+ }
644
+ }
645
+ function validateAnnotationDispatchRequest(req, state) {
646
+ if (!req.annotations.length) {
647
+ throw new AnnotationDispatchError("At least one annotation is required.", "INVALID_REQUEST");
648
+ }
649
+ for (const annotation of req.annotations) {
650
+ if (!annotation.targets.length) {
651
+ throw new AnnotationDispatchError(
652
+ "Each annotation must include at least one target.",
653
+ "INVALID_REQUEST"
654
+ );
655
+ }
656
+ for (const target of annotation.targets) {
657
+ const absolutePath = resolveWorkspacePath(target.location.file, state.cwd);
658
+ assertPathWithinProject(absolutePath, state.projectRoot);
659
+ }
660
+ }
661
+ }
662
+ function normalizeAnnotationBatch(req) {
663
+ return {
664
+ instruction: req.instruction?.trim() ?? "",
665
+ responseMode: req.responseMode ?? "unified",
666
+ ...req.runtimeContext ? { runtimeContext: req.runtimeContext } : {},
667
+ ...req.screenshotContext ? { screenshotContext: req.screenshotContext } : {},
668
+ ...req.cssContextPrompt?.trim() ? { cssContextPrompt: req.cssContextPrompt.trim() } : {},
669
+ annotations: req.annotations.map((annotation, index) => ({
670
+ index: index + 1,
671
+ note: annotation.note.trim(),
672
+ intent: annotation.intent,
673
+ targets: annotation.targets.map((target) => ({
674
+ file: target.location.file,
675
+ line: target.location.line,
676
+ column: target.location.column,
677
+ ...target.label ? { label: target.label } : {},
678
+ ...target.selector ? { selector: target.selector } : {},
679
+ ...target.snippet ? { snippet: target.snippet } : {}
680
+ }))
681
+ }))
682
+ };
683
+ }
684
+ function buildAnnotationBatchPrompt(batch) {
685
+ const body = buildSelectedElementsPrompt(batch.annotations);
686
+ const prompt = batch.instruction ? `${batch.instruction}
687
+
688
+ ${body}` : body;
689
+ return appendScreenshotContextSection(
690
+ appendCssContextSection(
691
+ appendRuntimeContextSection(prompt, batch.runtimeContext),
692
+ batch.cssContextPrompt
693
+ ),
694
+ batch.screenshotContext
695
+ );
696
+ }
697
+ function appendCssContextSection(prompt, cssContextPrompt) {
698
+ if (!cssContextPrompt) return prompt;
699
+ return `${prompt}
700
+
701
+ ${cssContextPrompt}`;
702
+ }
703
+ function buildSelectedElementsPrompt(annotations) {
704
+ const lines = ["Selected elements:"];
705
+ for (const annotation of annotations) {
706
+ const trimmedNote = annotation.note.trim();
707
+ for (const target of annotation.targets) {
708
+ const targetLabel = (target.label || "Unknown target").trim() || "Unknown target";
709
+ lines.push(`- ${targetLabel}`);
710
+ lines.push(`file=${target.file}:${target.line}:${target.column}`);
711
+ if (trimmedNote) {
712
+ lines.push(`note=${trimmedNote}`);
713
+ }
714
+ }
715
+ }
716
+ if (lines.length === 1) {
717
+ lines.push("- None");
718
+ }
719
+ return lines.join("\n");
720
+ }
721
+ function appendScreenshotContextSection(prompt, screenshotContext) {
722
+ if (!screenshotContext || !screenshotContext.imageDataUrl && !screenshotContext.imageAssetId) {
723
+ return prompt;
724
+ }
725
+ const lines = [
726
+ "Visual screenshot context attached:",
727
+ `- capturedAt=${screenshotContext.capturedAt}`,
728
+ `- mimeType=${screenshotContext.mimeType}`,
729
+ ...screenshotContext.imageAssetId ? [`- imageAssetId=${screenshotContext.imageAssetId}`] : []
730
+ ];
731
+ return `${prompt}
732
+
733
+ ${lines.join("\n")}`;
734
+ }
735
+ function appendRuntimeContextSection(prompt, runtimeContext) {
736
+ if (!runtimeContext?.records.length) {
737
+ return prompt;
738
+ }
739
+ return `${prompt}
740
+
741
+ ${buildRuntimeContextSection(runtimeContext.records)}`;
742
+ }
743
+ function buildRuntimeContextSection(records) {
744
+ return ["Relevant runtime context:", ...records.map(formatRuntimeRecord)].join("\n");
745
+ }
746
+ function formatRuntimeRecord(record) {
747
+ const requestSummary = record.kind === "failed-request" ? `request=${record.request?.method ?? "GET"} ${record.request?.pathname ?? record.request?.url ?? "unknown"} status=${record.request?.status ?? "unknown"}` : `occurrences=${record.occurrenceCount}`;
748
+ const reasonSummary = record.relevanceReasons.length ? record.relevanceReasons.join("; ") : "timing-based";
749
+ const stackSummary = record.stack ? `
750
+ stack=${record.stack.split("\n").slice(0, 5).join(" | ")}` : "";
751
+ return [
752
+ `- [${record.kind}] ${record.message}`,
753
+ ` relevance=${record.relevanceLevel} (${reasonSummary})`,
754
+ ` ${requestSummary}`,
755
+ stackSummary
756
+ ].filter(Boolean).join("\n");
757
+ }
758
+ function getAnnotationDispatchErrorCode(error) {
759
+ if (error instanceof AnnotationDispatchError) return error.errorCode;
760
+ if (error instanceof Error && error.message.includes("outside of project workspace")) {
761
+ return "FORBIDDEN_PATH";
762
+ }
763
+ return "UNKNOWN";
764
+ }
765
+
766
+ // src/server/client-config.ts
767
+ async function buildClientConfig(serverState2) {
768
+ const userConfig = loadUserConfigSync(false, serverState2.cwd, serverState2.configRoot);
769
+ const promptsConfig = await loadPromptsConfig(false, serverState2.cwd, serverState2.configRoot);
770
+ const effectiveIde = userConfig.ide ?? "vscode";
771
+ let info;
772
+ if (!serverState2.ideInfo) {
773
+ info = { ide: effectiveIde };
774
+ } else {
775
+ const { scheme: _scheme, ...rest } = serverState2.ideInfo;
776
+ info = rest;
777
+ }
778
+ return {
779
+ ...info,
780
+ prompts: resolveIntents(promptsConfig),
781
+ hotKeys: userConfig["inspector.hotKey"] ?? "alt",
782
+ theme: userConfig["inspector.theme"] ?? "auto",
783
+ includeSnippet: userConfig["prompt.includeSnippet"] ?? false,
784
+ runtimeContext: {
785
+ enabled: true,
786
+ preview: true,
787
+ maxRuntimeErrors: 3,
788
+ maxFailedRequests: 2
789
+ },
790
+ screenshotContext: {
791
+ enabled: false
792
+ },
793
+ annotationResponseMode: userConfig["prompt.annotationResponseMode"] ?? "unified",
794
+ autoSend: userConfig["prompt.autoSend"] ?? false
795
+ };
796
+ }
797
+
798
+ // src/server/open-file.ts
799
+ var import_node_child_process2 = require("child_process");
800
+ var import_launch_ide2 = require("launch-ide");
801
+ var serverLogger2 = createLogger("inspecto:server", { logLevel: getGlobalLogLevel() });
802
+ var VSCODE_FAMILY_SCHEMES = [
803
+ "vscode",
804
+ "vscode-insiders",
805
+ "cursor",
806
+ "windsurf",
807
+ "trae",
808
+ "trae-cn",
809
+ "vscodium",
810
+ "codebuddy",
811
+ "codebuddy-cn",
812
+ "antigravity"
813
+ ];
814
+ function handleOpenFileRequest(body, serverState2) {
815
+ const absolutePath = resolveWorkspacePath(body.file, serverState2.cwd);
816
+ assertPathWithinProject(absolutePath, serverState2.projectRoot);
817
+ const userConfig = loadUserConfigSync(false, serverState2.cwd, serverState2.configRoot);
818
+ const configuredIde = userConfig.ide;
819
+ const activeIde = serverState2.ideInfo?.ide;
820
+ const activeIdeScheme = serverState2.ideInfo?.scheme;
821
+ const rawEditorHint = configuredIde || activeIde || activeIdeScheme || "code";
822
+ if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
823
+ serverLogger2.warn(
824
+ `Active IDE is ${activeIdeScheme}, but config forces ${configuredIde}. Using configured IDE.`
825
+ );
826
+ }
827
+ let editorHint = rawEditorHint;
828
+ if (rawEditorHint === "vscode") editorHint = "code";
829
+ else if (rawEditorHint === "vscode-insiders") editorHint = "code-insiders";
830
+ else if (rawEditorHint === "vscodium") editorHint = "codium";
831
+ else if (rawEditorHint === "trae-cn" || rawEditorHint === "trae") editorHint = "trae";
832
+ serverLogger2.debug(
833
+ `IDE_OPEN: activeIde=${activeIde}, activeIdeScheme=${activeIdeScheme}, configuredIde=${configuredIde} -> rawEditorHint=${rawEditorHint}, finalEditorHint=${editorHint}`
834
+ );
835
+ if (VSCODE_FAMILY_SCHEMES.includes(rawEditorHint)) {
836
+ let normalizedPath = absolutePath.replace(/\\/g, "/");
837
+ if (!normalizedPath.startsWith("/")) {
838
+ normalizedPath = "/" + normalizedPath;
839
+ }
840
+ const encodedPath = encodeURI(normalizedPath);
841
+ const uri = `${rawEditorHint}://file${encodedPath}:${body.line}:${body.column}`;
842
+ serverLogger2.debug(`IDE_OPEN: Bypassing launchIDE, using URI scheme directly: ${uri}`);
843
+ try {
844
+ if (process.platform === "darwin") {
845
+ (0, import_node_child_process2.execFileSync)("open", [uri]);
846
+ } else if (process.platform === "win32") {
847
+ (0, import_node_child_process2.execFileSync)("cmd", ["/c", "start", '""', uri]);
848
+ } else {
849
+ (0, import_node_child_process2.execFileSync)("xdg-open", [uri]);
850
+ }
851
+ } catch (e) {
852
+ serverLogger2.error(`Failed to launch URI for IDE_OPEN (${uri}):`, e);
853
+ (0, import_launch_ide2.launchIDE)({
854
+ file: absolutePath,
855
+ line: body.line,
856
+ column: body.column,
857
+ editor: editorHint,
858
+ type: process.platform === "darwin" ? "open" : "exec"
859
+ });
860
+ }
861
+ } else {
862
+ (0, import_launch_ide2.launchIDE)({
863
+ file: absolutePath,
864
+ line: body.line,
865
+ column: body.column,
866
+ editor: editorHint,
867
+ type: process.platform === "darwin" ? "open" : "exec"
868
+ });
869
+ }
870
+ return { success: true };
871
+ }
872
+
873
+ // src/server/project-root.ts
874
+ var import_node_fs2 = __toESM(require("fs"), 1);
875
+ var import_node_path3 = __toESM(require("path"), 1);
876
+ var import_node_child_process3 = require("child_process");
877
+ var serverLogger3 = createLogger("inspecto:server", { logLevel: getGlobalLogLevel() });
878
+ function resolveProjectRoot() {
879
+ const cwd = process.cwd();
880
+ let gitRoot;
881
+ try {
882
+ gitRoot = (0, import_node_child_process3.execSync)("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
883
+ } catch (e) {
884
+ serverLogger3.warn("Failed to resolve git root via git rev-parse:", e);
885
+ gitRoot = cwd;
886
+ }
887
+ const visited = /* @__PURE__ */ new Set();
888
+ const search = (start, stop) => {
889
+ let current = start;
890
+ while (!visited.has(current)) {
891
+ visited.add(current);
892
+ if (import_node_fs2.default.existsSync(import_node_path3.default.join(current, ".inspecto"))) return current;
893
+ if (current === stop) break;
894
+ const parent = import_node_path3.default.dirname(current);
895
+ if (parent === current) break;
896
+ current = parent;
897
+ }
898
+ return null;
899
+ };
900
+ const cwdMatch = search(cwd, import_node_path3.default.parse(cwd).root);
901
+ if (cwdMatch) return cwdMatch;
902
+ const repoMatch = search(gitRoot, import_node_path3.default.parse(gitRoot).root);
903
+ if (repoMatch) return repoMatch;
904
+ return gitRoot;
905
+ }
906
+
907
+ // src/server/index.ts
908
+ var serverLogger4 = createLogger("inspecto:server", { logLevel: getGlobalLogLevel() });
909
+ var serverState = {
910
+ port: null,
911
+ running: false,
912
+ projectRoot: "",
913
+ configRoot: "",
914
+ cwd: process.cwd()
915
+ };
916
+ var serverInstance = null;
574
917
  async function startServer() {
575
918
  if (serverState.running && serverState.port !== null) {
576
919
  return serverState.port;
@@ -582,7 +925,7 @@ async function startServer() {
582
925
  const port = await import_portfinder.default.getPortPromise();
583
926
  watchConfig(
584
927
  () => {
585
- serverLogger.info("user config reloaded.");
928
+ serverLogger4.info("user config reloaded.");
586
929
  },
587
930
  serverState.cwd,
588
931
  serverState.configRoot
@@ -598,7 +941,7 @@ async function startServer() {
598
941
  }
599
942
  const url = new URL(req.url ?? "/", `http://localhost:${port}`);
600
943
  handleRequest(url, req, res).catch((err) => {
601
- serverLogger.error("server error:", err);
944
+ serverLogger4.error("server error:", err);
602
945
  res.writeHead(500, { "Content-Type": "application/json" });
603
946
  res.end(JSON.stringify({ success: false, error: String(err) }));
604
947
  });
@@ -611,41 +954,41 @@ async function startServer() {
611
954
  serverInstance.once("error", reject);
612
955
  });
613
956
  serverInstance.on("error", (err) => {
614
- serverLogger.error("persistent server error:", err);
957
+ serverLogger4.error("persistent server error:", err);
615
958
  });
616
959
  serverState.port = port;
617
960
  serverState.running = true;
618
- const portFile = import_node_path2.default.join(import_node_os2.default.tmpdir(), "inspecto.port.json");
961
+ const portFile = import_node_path4.default.join(import_node_os2.default.tmpdir(), "inspecto.port.json");
619
962
  try {
620
963
  let portData = {};
621
- if (import_node_fs2.default.existsSync(portFile)) {
964
+ if (import_node_fs3.default.existsSync(portFile)) {
622
965
  try {
623
- portData = JSON.parse(import_node_fs2.default.readFileSync(portFile, "utf-8"));
966
+ portData = JSON.parse(import_node_fs3.default.readFileSync(portFile, "utf-8"));
624
967
  } catch (e) {
625
968
  }
626
969
  }
627
- const rootHash = import_node_crypto.default.createHash("md5").update(serverState.projectRoot).digest("hex");
970
+ const rootHash = import_node_crypto2.default.createHash("md5").update(serverState.projectRoot).digest("hex");
628
971
  portData[rootHash] = port;
629
- import_node_fs2.default.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
972
+ import_node_fs3.default.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
630
973
  } catch (e) {
631
- serverLogger.warn("Failed to write port file:", e);
974
+ serverLogger4.warn("Failed to write port file:", e);
632
975
  }
633
976
  process.once("exit", () => {
634
977
  try {
635
- if (import_node_fs2.default.existsSync(portFile)) {
636
- const portData = JSON.parse(import_node_fs2.default.readFileSync(portFile, "utf-8"));
637
- const rootHash = import_node_crypto.default.createHash("md5").update(serverState.projectRoot).digest("hex");
978
+ if (import_node_fs3.default.existsSync(portFile)) {
979
+ const portData = JSON.parse(import_node_fs3.default.readFileSync(portFile, "utf-8"));
980
+ const rootHash = import_node_crypto2.default.createHash("md5").update(serverState.projectRoot).digest("hex");
638
981
  delete portData[rootHash];
639
982
  if (Object.keys(portData).length === 0) {
640
- import_node_fs2.default.unlinkSync(portFile);
983
+ import_node_fs3.default.unlinkSync(portFile);
641
984
  } else {
642
- import_node_fs2.default.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
985
+ import_node_fs3.default.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
643
986
  }
644
987
  }
645
988
  } catch {
646
989
  }
647
990
  });
648
- serverLogger.info(`server running at http://127.0.0.1:${port}`);
991
+ serverLogger4.info(`server running at http://127.0.0.1:${port}`);
649
992
  return port;
650
993
  }
651
994
  async function readBody(req) {
@@ -664,26 +1007,7 @@ async function handleRequest(url, req, res) {
664
1007
  return;
665
1008
  }
666
1009
  if (pathname === import_types2.INSPECTO_API_PATHS.CLIENT_CONFIG && req.method === "GET") {
667
- const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
668
- const promptsConfig = await loadPromptsConfig(false, serverState.cwd, serverState.configRoot);
669
- const effectiveIde = userConfig.ide ?? "vscode";
670
- let info;
671
- if (!serverState.ideInfo) {
672
- info = {
673
- ide: effectiveIde
674
- };
675
- } else {
676
- const { scheme: _scheme, ...rest } = serverState.ideInfo;
677
- info = rest;
678
- }
679
- const config = {
680
- ...info,
681
- prompts: resolveIntents(promptsConfig),
682
- hotKeys: userConfig["inspector.hotKey"] ?? "alt",
683
- theme: userConfig["inspector.theme"] ?? "auto",
684
- includeSnippet: userConfig["prompt.includeSnippet"] ?? false,
685
- autoSend: userConfig["prompt.autoSend"] ?? false
686
- };
1010
+ const config = await buildClientConfig(serverState);
687
1011
  delete config.providers;
688
1012
  res.writeHead(200, { "Content-Type": "application/json" });
689
1013
  res.end(JSON.stringify(config));
@@ -694,21 +1018,23 @@ async function handleRequest(url, req, res) {
694
1018
  const body = JSON.parse(await readBody(req));
695
1019
  const ideWorkspace = body.workspaceRoot || "";
696
1020
  const serverProjectRoot = serverState.projectRoot || "";
697
- const isSameProject = !ideWorkspace || !serverProjectRoot || ideWorkspace === serverProjectRoot || serverProjectRoot.startsWith(ideWorkspace);
1021
+ const normalizedIdeRoot = ideWorkspace ? import_node_path4.default.resolve(ideWorkspace) : "";
1022
+ const normalizedServerRoot = serverProjectRoot ? import_node_path4.default.resolve(serverProjectRoot) : "";
1023
+ const isSameProject = !normalizedIdeRoot || !normalizedServerRoot || normalizedIdeRoot === normalizedServerRoot || normalizedServerRoot.startsWith(normalizedIdeRoot + import_node_path4.default.sep) || normalizedIdeRoot.startsWith(normalizedServerRoot + import_node_path4.default.sep);
698
1024
  if (isSameProject) {
699
1025
  serverState.ideInfo = body;
700
- serverLogger.debug(
1026
+ serverLogger4.debug(
701
1027
  `Accepted IDE info from matched workspace (ide-${body.ide} / schema-${body.scheme})`
702
1028
  );
703
1029
  } else {
704
- serverLogger.debug(
1030
+ serverLogger4.debug(
705
1031
  `Ignored IDE info from unrelated workspace (IDE Workspace: ${ideWorkspace}, Server: ${serverProjectRoot}, Scheme: ${body.scheme}, IDE: ${body.ide})`
706
1032
  );
707
1033
  }
708
1034
  res.writeHead(200, { "Content-Type": "application/json" });
709
1035
  res.end(JSON.stringify({ success: true }));
710
1036
  } catch (e) {
711
- serverLogger.error(`Error parsing ${import_types2.INSPECTO_API_PATHS.IDE_INFO} POST request:`, e);
1037
+ serverLogger4.error(`Error parsing ${import_types2.INSPECTO_API_PATHS.IDE_INFO} POST request:`, e);
712
1038
  res.writeHead(400, { "Content-Type": "application/json" });
713
1039
  res.end(JSON.stringify({ error: "Invalid JSON body" }));
714
1040
  }
@@ -723,74 +1049,14 @@ async function handleRequest(url, req, res) {
723
1049
  res.end(JSON.stringify({ error: "Invalid JSON body" }));
724
1050
  return;
725
1051
  }
726
- const absolutePath = import_node_path2.default.isAbsolute(body.file) ? import_node_path2.default.resolve(body.file) : import_node_path2.default.resolve(serverState.cwd, body.file);
727
- const relativeToRoot = import_node_path2.default.relative(serverState.projectRoot, absolutePath);
728
- if (relativeToRoot.startsWith("..") || import_node_path2.default.isAbsolute(relativeToRoot)) {
729
- serverLogger.warn(`Security: Blocked path traversal attempt in IDE_OPEN: ${body.file}`);
1052
+ try {
1053
+ handleOpenFileRequest(body, serverState);
1054
+ } catch {
1055
+ serverLogger4.warn(`Security: Blocked path traversal attempt in IDE_OPEN: ${body.file}`);
730
1056
  res.writeHead(403, { "Content-Type": "application/json" });
731
1057
  res.end(JSON.stringify({ error: "Access denied: File is outside of project workspace" }));
732
1058
  return;
733
1059
  }
734
- const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
735
- const configuredIde = userConfig.ide;
736
- const activeIde = serverState.ideInfo?.ide;
737
- const activeIdeScheme = serverState.ideInfo?.scheme;
738
- const rawEditorHint = configuredIde || activeIde || activeIdeScheme || "code";
739
- if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
740
- serverLogger.warn(
741
- `Active IDE is ${activeIdeScheme}, but config forces ${configuredIde}. Using configured IDE.`
742
- );
743
- }
744
- let editorHint = rawEditorHint;
745
- if (rawEditorHint === "vscode") editorHint = "code";
746
- else if (rawEditorHint === "vscode-insiders") editorHint = "code-insiders";
747
- else if (rawEditorHint === "vscodium") editorHint = "codium";
748
- else if (rawEditorHint === "trae-cn" || rawEditorHint === "trae") editorHint = "trae";
749
- serverLogger.debug(
750
- `IDE_OPEN: activeIde=${activeIde}, activeIdeScheme=${activeIdeScheme}, configuredIde=${configuredIde} -> rawEditorHint=${rawEditorHint}, finalEditorHint=${editorHint}`
751
- );
752
- const VSCODE_FAMILY_SCHEMES = [
753
- "vscode",
754
- "vscode-insiders",
755
- "cursor",
756
- "windsurf",
757
- "trae",
758
- "trae-cn",
759
- "vscodium",
760
- "codebuddy",
761
- "codebuddy-cn",
762
- "antigravity"
763
- ];
764
- if (VSCODE_FAMILY_SCHEMES.includes(rawEditorHint)) {
765
- const uri = `${rawEditorHint}://file${absolutePath}:${body.line}:${body.column}`;
766
- serverLogger.debug(`IDE_OPEN: Bypassing launchIDE, using URI scheme directly: ${uri}`);
767
- try {
768
- if (process.platform === "darwin") {
769
- (0, import_node_child_process.execFileSync)("open", [uri]);
770
- } else if (process.platform === "win32") {
771
- (0, import_node_child_process.execFileSync)("cmd", ["/c", "start", '""', uri]);
772
- } else {
773
- (0, import_node_child_process.execFileSync)("xdg-open", [uri]);
774
- }
775
- } catch (e) {
776
- serverLogger.error(`Failed to launch URI for IDE_OPEN (${uri}):`, e);
777
- (0, import_launch_ide.launchIDE)({
778
- file: absolutePath,
779
- line: body.line,
780
- column: body.column,
781
- editor: editorHint,
782
- type: process.platform === "darwin" ? "open" : "exec"
783
- });
784
- }
785
- } else {
786
- (0, import_launch_ide.launchIDE)({
787
- file: absolutePath,
788
- line: body.line,
789
- column: body.column,
790
- editor: editorHint,
791
- type: process.platform === "darwin" ? "open" : "exec"
792
- });
793
- }
794
1060
  res.writeHead(200, { "Content-Type": "application/json" });
795
1061
  res.end(JSON.stringify({ success: true }));
796
1062
  return;
@@ -801,10 +1067,11 @@ async function handleRequest(url, req, res) {
801
1067
  const column = parseInt(url.searchParams.get("column") ?? "1", 10);
802
1068
  const maxLines = parseInt(url.searchParams.get("maxLines") ?? "100", 10);
803
1069
  try {
804
- const absolutePath = import_node_path2.default.isAbsolute(file) ? import_node_path2.default.resolve(file) : import_node_path2.default.resolve(serverState.cwd, file);
805
- const relativeToRoot = import_node_path2.default.relative(serverState.projectRoot, absolutePath);
806
- if (relativeToRoot.startsWith("..") || import_node_path2.default.isAbsolute(relativeToRoot)) {
807
- serverLogger.warn(`Security: Blocked path traversal attempt in PROJECT_SNIPPET: ${file}`);
1070
+ const absolutePath = resolveWorkspacePath(file, serverState.cwd);
1071
+ try {
1072
+ assertPathWithinProject(absolutePath, serverState.projectRoot);
1073
+ } catch {
1074
+ serverLogger4.warn(`Security: Blocked path traversal attempt in PROJECT_SNIPPET: ${file}`);
808
1075
  res.writeHead(403, { "Content-Type": "application/json" });
809
1076
  res.end(
810
1077
  JSON.stringify({
@@ -834,7 +1101,23 @@ async function handleRequest(url, req, res) {
834
1101
  res.writeHead(result.success ? 200 : 500, { "Content-Type": "application/json" });
835
1102
  res.end(JSON.stringify(result));
836
1103
  } catch (e) {
837
- serverLogger.error(`Error parsing ${import_types2.INSPECTO_API_PATHS.AI_DISPATCH} request:`, e);
1104
+ serverLogger4.error(`Error parsing ${import_types2.INSPECTO_API_PATHS.AI_DISPATCH} request:`, e);
1105
+ res.writeHead(500, { "Content-Type": "application/json" });
1106
+ res.end(JSON.stringify({ success: false, error: String(e), errorCode: "INTERNAL_ERROR" }));
1107
+ }
1108
+ return;
1109
+ }
1110
+ if (pathname === import_types2.INSPECTO_API_PATHS.AI_BATCH_DISPATCH && req.method === "POST") {
1111
+ try {
1112
+ const rawBody = await readBody(req);
1113
+ const body = JSON.parse(rawBody);
1114
+ const result = await dispatchAnnotationsToAi(body, serverState);
1115
+ res.writeHead(getBatchDispatchStatusCode(result.errorCode, result.success), {
1116
+ "Content-Type": "application/json"
1117
+ });
1118
+ res.end(JSON.stringify(result));
1119
+ } catch (e) {
1120
+ serverLogger4.error(`Error parsing ${import_types2.INSPECTO_API_PATHS.AI_BATCH_DISPATCH} request:`, e);
838
1121
  res.writeHead(500, { "Content-Type": "application/json" });
839
1122
  res.end(JSON.stringify({ success: false, error: String(e), errorCode: "INTERNAL_ERROR" }));
840
1123
  }
@@ -842,7 +1125,7 @@ async function handleRequest(url, req, res) {
842
1125
  }
843
1126
  if (pathname.startsWith(`${import_types2.INSPECTO_API_PATHS.AI_TICKET}/`) && req.method === "GET") {
844
1127
  const ticketId = pathname.substring(import_types2.INSPECTO_API_PATHS.AI_TICKET.length + 1);
845
- const payloadStr = payloadTickets.get(ticketId);
1128
+ const payloadStr = readTicket(ticketId);
846
1129
  if (!payloadStr) {
847
1130
  res.writeHead(404, { "Content-Type": "application/json" });
848
1131
  res.end(JSON.stringify({ success: false, error: "Ticket not found or expired" }));
@@ -856,58 +1139,32 @@ async function handleRequest(url, req, res) {
856
1139
  res.end(JSON.stringify({ error: "not found" }));
857
1140
  }
858
1141
  async function dispatchToAi(req) {
859
- const { location, snippet, prompt } = req;
860
- const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
861
- const resolvedTarget = resolveTargetTool(userConfig);
1142
+ const { location, snippet, prompt, screenshotContext } = req;
862
1143
  const formattedPrompt = prompt ?? `Please help me with this code from \`${location.file}\` (line ${location.line}):
863
1144
 
864
1145
  \`\`\`
865
1146
  ${snippet}
866
1147
  \`\`\`
867
1148
  `;
868
- const ideReportedMode = serverState.ideInfo?.providers[resolvedTarget]?.mode;
869
- const configuredIde = userConfig.ide;
870
- const activeIde = serverState.ideInfo?.ide;
871
- const activeIdeScheme = serverState.ideInfo?.scheme;
872
- const finalIde = configuredIde || activeIdeScheme || activeIde || "vscode";
873
- if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
874
- serverLogger.warn(
875
- `dispatchToAi: Active IDE is ${activeIdeScheme}, but config forces ${configuredIde}. Using configured IDE.`
876
- );
877
- }
878
- const mode = resolveProviderMode(resolvedTarget, finalIde, userConfig);
879
- const overrides = extractToolOverrides(finalIde, userConfig)[resolvedTarget] || {};
880
- overrides.type = mode;
881
- const fullPayload = {
882
- ide: finalIde,
883
- target: resolvedTarget,
884
- targetType: mode,
1149
+ const runtime = resolvePromptDispatchRuntime(serverState);
1150
+ return dispatchPromptThroughIde(runtime, {
885
1151
  prompt: formattedPrompt,
886
1152
  filePath: location.file,
887
1153
  line: location.line,
888
1154
  column: location.column,
889
1155
  snippet,
890
- overrides: Object.keys(overrides).length > 0 ? overrides : void 0,
891
- autoSend: userConfig["prompt.autoSend"] !== void 0 ? Boolean(userConfig["prompt.autoSend"]) : void 0
892
- };
893
- const ticketId = createTicket(fullPayload);
894
- const params = new URLSearchParams();
895
- params.set("ticket", ticketId);
896
- params.set("target", resolvedTarget);
897
- const uri = `${finalIde}://inspecto.inspecto/send?${params.toString()}`;
898
- serverLogger.debug(`dispatchToAi: Generated URI: ${uri}`);
899
- launchURI(uri);
900
- return {
901
- success: true,
902
- fallbackPayload: {
903
- prompt: formattedPrompt,
904
- file: location.file
905
- }
906
- };
1156
+ ...screenshotContext ? { screenshotContext } : {}
1157
+ });
1158
+ }
1159
+ function getBatchDispatchStatusCode(errorCode, success) {
1160
+ if (success) return 200;
1161
+ if (errorCode === "INVALID_REQUEST") return 400;
1162
+ if (errorCode === "FORBIDDEN_PATH") return 403;
1163
+ return 500;
907
1164
  }
908
1165
 
909
1166
  // src/legacy/webpack4/index.ts
910
- var import_node_path3 = __toESM(require("path"), 1);
1167
+ var import_node_path5 = __toESM(require("path"), 1);
911
1168
  var InspectoWebpack4Plugin = class {
912
1169
  constructor(options = {}) {
913
1170
  this.options = options;
@@ -915,7 +1172,7 @@ var InspectoWebpack4Plugin = class {
915
1172
  apply(compiler) {
916
1173
  const clientPath = resolveClientModule();
917
1174
  compiler.hooks.afterEnvironment.tap("InspectoWebpack4Plugin", () => {
918
- const inspectoLoader = import_node_path3.default.resolve(__dirname, "loader.cjs");
1175
+ const inspectoLoader = import_node_path5.default.resolve(__dirname, "loader.cjs");
919
1176
  compiler.options.module.rules.push({
920
1177
  test: /\.[jt]sx?$/,
921
1178
  enforce: "pre",