@inspecto-dev/plugin 0.2.0-alpha.4 → 0.3.0

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/dist/webpack.js CHANGED
@@ -269,13 +269,11 @@ function transformRouter(options) {
269
269
 
270
270
  // src/server/index.ts
271
271
  import http from "http";
272
- import fs3 from "fs";
273
- import path6 from "path";
272
+ import fs4 from "fs";
273
+ import path8 from "path";
274
274
  import os2 from "os";
275
- import crypto from "crypto";
276
- import { execSync, execFileSync } from "child_process";
275
+ import crypto2 from "crypto";
277
276
  import portfinder from "portfinder";
278
- import { launchIDE } from "launch-ide";
279
277
  import { INSPECTO_API_PATHS } from "@inspecto-dev/types";
280
278
 
281
279
  // src/server/snippet.ts
@@ -613,9 +611,9 @@ function extractToolOverrides(ide, config) {
613
611
  function resolveIntents(serverPrompts) {
614
612
  const baseMap = /* @__PURE__ */ new Map();
615
613
  for (const intent of DEFAULT_INTENTS) {
616
- if (intent.id) baseMap.set(intent.id, { ...intent });
614
+ baseMap.set(intent.id, { ...intent });
617
615
  }
618
- const defaults = () => ensureOpenInEditorLast(Array.from(baseMap.values()));
616
+ const defaults = () => Array.from(baseMap.values());
619
617
  if (!serverPrompts) return defaults();
620
618
  const isReplace = !Array.isArray(serverPrompts) && typeof serverPrompts === "object" && serverPrompts.$replace === true;
621
619
  const promptsArray = Array.isArray(serverPrompts) ? serverPrompts : isReplace ? serverPrompts.items : [];
@@ -642,16 +640,18 @@ function resolveIntents(serverPrompts) {
642
640
  );
643
641
  continue;
644
642
  }
645
- if (item.isAction && item.id !== "open-in-editor") {
643
+ if (!item.aiIntent) {
646
644
  configLogger.warn(
647
- `isAction is reserved for built-in actions. Ignoring intent "${item.id}".`
645
+ `Intent "${item.id}" is missing required "aiIntent".`
648
646
  );
649
647
  continue;
650
648
  }
651
- result.push(baseMap.has(item.id) ? { ...baseMap.get(item.id), ...item } : item);
649
+ result.push(
650
+ baseMap.has(item.id) ? { ...baseMap.get(item.id), ...item } : item
651
+ );
652
652
  }
653
653
  }
654
- return ensureOpenInEditorLast(result);
654
+ return result;
655
655
  }
656
656
  const merged = Array.from(baseMap.values());
657
657
  for (const item of promptsArray) {
@@ -668,9 +668,9 @@ function resolveIntents(serverPrompts) {
668
668
  configLogger.warn('Intent object missing required "id" field, skipping.');
669
669
  continue;
670
670
  }
671
- if (item.isAction && item.id !== "open-in-editor") {
671
+ if (!item.aiIntent) {
672
672
  configLogger.warn(
673
- `isAction is reserved for built-in actions. Ignoring intent "${item.id}".`
673
+ `Intent "${item.id}" is missing required "aiIntent".`
674
674
  );
675
675
  continue;
676
676
  }
@@ -688,15 +688,7 @@ function resolveIntents(serverPrompts) {
688
688
  }
689
689
  }
690
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;
691
+ return merged;
700
692
  }
701
693
  var watchers = [];
702
694
  function watchConfig(onReload, cwd = process.cwd(), gitRoot) {
@@ -731,7 +723,10 @@ function watchConfig(onReload, cwd = process.cwd(), gitRoot) {
731
723
  }
732
724
  }
733
725
 
734
- // src/server/index.ts
726
+ // src/server/dispatch-transport.ts
727
+ import crypto from "crypto";
728
+ import { execFileSync } from "child_process";
729
+ import { launchIDE } from "launch-ide";
735
730
  var serverLogger = createLogger("inspecto:server", { logLevel: getGlobalLogLevel() });
736
731
  var payloadTickets = /* @__PURE__ */ new Map();
737
732
  function createTicket(payload) {
@@ -745,21 +740,363 @@ function createTicket(payload) {
745
740
  );
746
741
  return ticketId;
747
742
  }
748
- var serverState = {
749
- port: null,
750
- running: false,
751
- projectRoot: "",
752
- configRoot: "",
753
- cwd: process.cwd()
743
+ function readTicket(ticketId) {
744
+ return payloadTickets.get(ticketId);
745
+ }
746
+ function launchURI(uri) {
747
+ try {
748
+ if (process.platform === "darwin") {
749
+ execFileSync("open", [uri]);
750
+ } else if (process.platform === "win32") {
751
+ execFileSync("cmd", ["/c", "start", '""', uri]);
752
+ } else {
753
+ execFileSync("xdg-open", [uri]);
754
+ }
755
+ } catch (e) {
756
+ serverLogger.error("Failed to launch URI via execFileSync, falling back to launchIDE:", e);
757
+ launchIDE({ file: uri });
758
+ }
759
+ }
760
+
761
+ // src/server/dispatch-runtime.ts
762
+ function resolvePromptDispatchRuntime(state) {
763
+ const userConfig = loadUserConfigSync(false, state.cwd, state.projectRoot);
764
+ const resolvedTarget = resolveTargetTool(userConfig);
765
+ const finalIde = resolveFinalIde(userConfig.ide, state.ideInfo?.ide, state.ideInfo?.scheme);
766
+ const mode = resolveProviderMode(resolvedTarget, finalIde, userConfig);
767
+ const overrides = extractToolOverrides(finalIde, userConfig)[resolvedTarget] || void 0;
768
+ return {
769
+ resolvedTarget,
770
+ finalIde,
771
+ mode,
772
+ ...hasOverrides(overrides) ? { overrides } : {},
773
+ ...userConfig["prompt.autoSend"] !== void 0 ? { autoSend: Boolean(userConfig["prompt.autoSend"]) } : {}
774
+ };
775
+ }
776
+ function dispatchPromptThroughIde(runtime, payload) {
777
+ const ticketId = createTicket({
778
+ ide: runtime.finalIde,
779
+ target: runtime.resolvedTarget,
780
+ targetType: runtime.mode,
781
+ prompt: payload.prompt,
782
+ filePath: payload.filePath,
783
+ line: payload.line,
784
+ column: payload.column,
785
+ snippet: payload.snippet,
786
+ ...payload.screenshotContext ? { screenshotContext: payload.screenshotContext } : {},
787
+ overrides: runtime.overrides,
788
+ autoSend: runtime.autoSend
789
+ });
790
+ const params = new URLSearchParams();
791
+ params.set("ticket", ticketId);
792
+ params.set("target", runtime.resolvedTarget);
793
+ launchURI(`${runtime.finalIde}://inspecto.inspecto/send?${params.toString()}`);
794
+ return {
795
+ success: true,
796
+ fallbackPayload: {
797
+ prompt: payload.prompt,
798
+ ...payload.filePath ? { file: payload.filePath } : {}
799
+ }
800
+ };
801
+ }
802
+ function resolveFinalIde(configuredIde, activeIde, activeIdeScheme) {
803
+ if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
804
+ return configuredIde;
805
+ }
806
+ return configuredIde || activeIdeScheme || activeIde || "vscode";
807
+ }
808
+ function hasOverrides(overrides) {
809
+ return Boolean(overrides && Object.keys(overrides).length > 0);
810
+ }
811
+
812
+ // src/server/path-guards.ts
813
+ import path6 from "path";
814
+ function isWindowsAbsolutePath(file) {
815
+ return /^[a-zA-Z]:[\\/]/.test(file) || /^\\\\[^\\]+\\[^\\]+/.test(file);
816
+ }
817
+ function resolveWorkspacePath(file, cwd) {
818
+ if (isWindowsAbsolutePath(file)) {
819
+ return path6.win32.normalize(file);
820
+ }
821
+ return path6.isAbsolute(file) ? path6.resolve(file) : path6.resolve(cwd, file);
822
+ }
823
+ function assertPathWithinProject(file, projectRoot) {
824
+ const relativeToRoot = isWindowsAbsolutePath(file) || isWindowsAbsolutePath(projectRoot) ? path6.win32.relative(path6.win32.normalize(projectRoot), path6.win32.normalize(file)) : path6.relative(projectRoot, file);
825
+ if (relativeToRoot.startsWith("..") || path6.isAbsolute(relativeToRoot)) {
826
+ throw new Error("Access denied: File is outside of project workspace");
827
+ }
828
+ }
829
+
830
+ // src/server/annotation-dispatch.ts
831
+ var AnnotationDispatchError = class extends Error {
832
+ constructor(message, errorCode) {
833
+ super(message);
834
+ this.name = "AnnotationDispatchError";
835
+ this.errorCode = errorCode;
836
+ }
754
837
  };
755
- var serverInstance = null;
838
+ async function dispatchAnnotationsToAi(req, state) {
839
+ try {
840
+ validateAnnotationDispatchRequest(req, state);
841
+ const batch = normalizeAnnotationBatch(req);
842
+ const prompt = buildAnnotationBatchPrompt(batch);
843
+ const representativeTarget = batch.annotations[0]?.targets[0];
844
+ const runtime = resolvePromptDispatchRuntime(state);
845
+ return dispatchPromptThroughIde(runtime, {
846
+ prompt,
847
+ ...representativeTarget?.file ? { filePath: representativeTarget.file } : {},
848
+ ...representativeTarget?.line ? { line: representativeTarget.line } : {},
849
+ ...representativeTarget?.column ? { column: representativeTarget.column } : {},
850
+ ...batch.screenshotContext ? { screenshotContext: batch.screenshotContext } : {}
851
+ });
852
+ } catch (error) {
853
+ return {
854
+ success: false,
855
+ error: error instanceof Error ? error.message : String(error),
856
+ errorCode: getAnnotationDispatchErrorCode(error)
857
+ };
858
+ }
859
+ }
860
+ function validateAnnotationDispatchRequest(req, state) {
861
+ if (!req.annotations.length) {
862
+ throw new AnnotationDispatchError("At least one annotation is required.", "INVALID_REQUEST");
863
+ }
864
+ for (const annotation of req.annotations) {
865
+ if (!annotation.targets.length) {
866
+ throw new AnnotationDispatchError(
867
+ "Each annotation must include at least one target.",
868
+ "INVALID_REQUEST"
869
+ );
870
+ }
871
+ for (const target of annotation.targets) {
872
+ const absolutePath = resolveWorkspacePath(target.location.file, state.cwd);
873
+ assertPathWithinProject(absolutePath, state.projectRoot);
874
+ }
875
+ }
876
+ }
877
+ function normalizeAnnotationBatch(req) {
878
+ return {
879
+ instruction: req.instruction?.trim() ?? "",
880
+ responseMode: req.responseMode ?? "unified",
881
+ ...req.runtimeContext ? { runtimeContext: req.runtimeContext } : {},
882
+ ...req.screenshotContext ? { screenshotContext: req.screenshotContext } : {},
883
+ ...req.cssContextPrompt?.trim() ? { cssContextPrompt: req.cssContextPrompt.trim() } : {},
884
+ annotations: req.annotations.map((annotation, index) => ({
885
+ index: index + 1,
886
+ note: annotation.note.trim(),
887
+ intent: annotation.intent,
888
+ targets: annotation.targets.map((target) => ({
889
+ file: target.location.file,
890
+ line: target.location.line,
891
+ column: target.location.column,
892
+ ...target.label ? { label: target.label } : {},
893
+ ...target.selector ? { selector: target.selector } : {},
894
+ ...target.snippet ? { snippet: target.snippet } : {}
895
+ }))
896
+ }))
897
+ };
898
+ }
899
+ function buildAnnotationBatchPrompt(batch) {
900
+ const body = buildSelectedElementsPrompt(batch.annotations);
901
+ const prompt = batch.instruction ? `${batch.instruction}
902
+
903
+ ${body}` : body;
904
+ return appendScreenshotContextSection(
905
+ appendCssContextSection(
906
+ appendRuntimeContextSection(prompt, batch.runtimeContext),
907
+ batch.cssContextPrompt
908
+ ),
909
+ batch.screenshotContext
910
+ );
911
+ }
912
+ function appendCssContextSection(prompt, cssContextPrompt) {
913
+ if (!cssContextPrompt) return prompt;
914
+ return `${prompt}
915
+
916
+ ${cssContextPrompt}`;
917
+ }
918
+ function buildSelectedElementsPrompt(annotations) {
919
+ const lines = ["Selected elements:"];
920
+ for (const annotation of annotations) {
921
+ const trimmedNote = annotation.note.trim();
922
+ for (const target of annotation.targets) {
923
+ const targetLabel = (target.label || "Unknown target").trim() || "Unknown target";
924
+ lines.push(`- ${targetLabel}`);
925
+ lines.push(`file=${target.file}:${target.line}:${target.column}`);
926
+ if (trimmedNote) {
927
+ lines.push(`note=${trimmedNote}`);
928
+ }
929
+ }
930
+ }
931
+ if (lines.length === 1) {
932
+ lines.push("- None");
933
+ }
934
+ return lines.join("\n");
935
+ }
936
+ function appendScreenshotContextSection(prompt, screenshotContext) {
937
+ if (!screenshotContext || !screenshotContext.imageDataUrl && !screenshotContext.imageAssetId) {
938
+ return prompt;
939
+ }
940
+ const lines = [
941
+ "Visual screenshot context attached:",
942
+ `- capturedAt=${screenshotContext.capturedAt}`,
943
+ `- mimeType=${screenshotContext.mimeType}`,
944
+ ...screenshotContext.imageAssetId ? [`- imageAssetId=${screenshotContext.imageAssetId}`] : []
945
+ ];
946
+ return `${prompt}
947
+
948
+ ${lines.join("\n")}`;
949
+ }
950
+ function appendRuntimeContextSection(prompt, runtimeContext) {
951
+ if (!runtimeContext?.records.length) {
952
+ return prompt;
953
+ }
954
+ return `${prompt}
955
+
956
+ ${buildRuntimeContextSection(runtimeContext.records)}`;
957
+ }
958
+ function buildRuntimeContextSection(records) {
959
+ return ["Relevant runtime context:", ...records.map(formatRuntimeRecord)].join("\n");
960
+ }
961
+ function formatRuntimeRecord(record) {
962
+ 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}`;
963
+ const reasonSummary = record.relevanceReasons.length ? record.relevanceReasons.join("; ") : "timing-based";
964
+ const stackSummary = record.stack ? `
965
+ stack=${record.stack.split("\n").slice(0, 5).join(" | ")}` : "";
966
+ return [
967
+ `- [${record.kind}] ${record.message}`,
968
+ ` relevance=${record.relevanceLevel} (${reasonSummary})`,
969
+ ` ${requestSummary}`,
970
+ stackSummary
971
+ ].filter(Boolean).join("\n");
972
+ }
973
+ function getAnnotationDispatchErrorCode(error) {
974
+ if (error instanceof AnnotationDispatchError) return error.errorCode;
975
+ if (error instanceof Error && error.message.includes("outside of project workspace")) {
976
+ return "FORBIDDEN_PATH";
977
+ }
978
+ return "UNKNOWN";
979
+ }
980
+
981
+ // src/server/client-config.ts
982
+ async function buildClientConfig(serverState2) {
983
+ const userConfig = loadUserConfigSync(false, serverState2.cwd, serverState2.configRoot);
984
+ const promptsConfig = await loadPromptsConfig(false, serverState2.cwd, serverState2.configRoot);
985
+ const effectiveIde = userConfig.ide ?? "vscode";
986
+ let info;
987
+ if (!serverState2.ideInfo) {
988
+ info = { ide: effectiveIde };
989
+ } else {
990
+ const { scheme: _scheme, ...rest } = serverState2.ideInfo;
991
+ info = rest;
992
+ }
993
+ return {
994
+ ...info,
995
+ prompts: resolveIntents(promptsConfig),
996
+ hotKeys: userConfig["inspector.hotKey"] ?? "alt",
997
+ theme: userConfig["inspector.theme"] ?? "auto",
998
+ includeSnippet: userConfig["prompt.includeSnippet"] ?? false,
999
+ runtimeContext: {
1000
+ enabled: true,
1001
+ preview: true,
1002
+ maxRuntimeErrors: 3,
1003
+ maxFailedRequests: 2
1004
+ },
1005
+ screenshotContext: {
1006
+ enabled: false
1007
+ },
1008
+ annotationResponseMode: userConfig["prompt.annotationResponseMode"] ?? "unified",
1009
+ autoSend: userConfig["prompt.autoSend"] ?? false
1010
+ };
1011
+ }
1012
+
1013
+ // src/server/open-file.ts
1014
+ import { execFileSync as execFileSync2 } from "child_process";
1015
+ import { launchIDE as launchIDE2 } from "launch-ide";
1016
+ var serverLogger2 = createLogger("inspecto:server", { logLevel: getGlobalLogLevel() });
1017
+ var VSCODE_FAMILY_SCHEMES = [
1018
+ "vscode",
1019
+ "vscode-insiders",
1020
+ "cursor",
1021
+ "windsurf",
1022
+ "trae",
1023
+ "trae-cn",
1024
+ "vscodium",
1025
+ "codebuddy",
1026
+ "codebuddy-cn",
1027
+ "antigravity"
1028
+ ];
1029
+ function handleOpenFileRequest(body, serverState2) {
1030
+ const absolutePath = resolveWorkspacePath(body.file, serverState2.cwd);
1031
+ assertPathWithinProject(absolutePath, serverState2.projectRoot);
1032
+ const userConfig = loadUserConfigSync(false, serverState2.cwd, serverState2.configRoot);
1033
+ const configuredIde = userConfig.ide;
1034
+ const activeIde = serverState2.ideInfo?.ide;
1035
+ const activeIdeScheme = serverState2.ideInfo?.scheme;
1036
+ const rawEditorHint = configuredIde || activeIde || activeIdeScheme || "code";
1037
+ if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
1038
+ serverLogger2.warn(
1039
+ `Active IDE is ${activeIdeScheme}, but config forces ${configuredIde}. Using configured IDE.`
1040
+ );
1041
+ }
1042
+ let editorHint = rawEditorHint;
1043
+ if (rawEditorHint === "vscode") editorHint = "code";
1044
+ else if (rawEditorHint === "vscode-insiders") editorHint = "code-insiders";
1045
+ else if (rawEditorHint === "vscodium") editorHint = "codium";
1046
+ else if (rawEditorHint === "trae-cn" || rawEditorHint === "trae") editorHint = "trae";
1047
+ serverLogger2.debug(
1048
+ `IDE_OPEN: activeIde=${activeIde}, activeIdeScheme=${activeIdeScheme}, configuredIde=${configuredIde} -> rawEditorHint=${rawEditorHint}, finalEditorHint=${editorHint}`
1049
+ );
1050
+ if (VSCODE_FAMILY_SCHEMES.includes(rawEditorHint)) {
1051
+ let normalizedPath = absolutePath.replace(/\\/g, "/");
1052
+ if (!normalizedPath.startsWith("/")) {
1053
+ normalizedPath = "/" + normalizedPath;
1054
+ }
1055
+ const encodedPath = encodeURI(normalizedPath);
1056
+ const uri = `${rawEditorHint}://file${encodedPath}:${body.line}:${body.column}`;
1057
+ serverLogger2.debug(`IDE_OPEN: Bypassing launchIDE, using URI scheme directly: ${uri}`);
1058
+ try {
1059
+ if (process.platform === "darwin") {
1060
+ execFileSync2("open", [uri]);
1061
+ } else if (process.platform === "win32") {
1062
+ execFileSync2("cmd", ["/c", "start", '""', uri]);
1063
+ } else {
1064
+ execFileSync2("xdg-open", [uri]);
1065
+ }
1066
+ } catch (e) {
1067
+ serverLogger2.error(`Failed to launch URI for IDE_OPEN (${uri}):`, e);
1068
+ launchIDE2({
1069
+ file: absolutePath,
1070
+ line: body.line,
1071
+ column: body.column,
1072
+ editor: editorHint,
1073
+ type: process.platform === "darwin" ? "open" : "exec"
1074
+ });
1075
+ }
1076
+ } else {
1077
+ launchIDE2({
1078
+ file: absolutePath,
1079
+ line: body.line,
1080
+ column: body.column,
1081
+ editor: editorHint,
1082
+ type: process.platform === "darwin" ? "open" : "exec"
1083
+ });
1084
+ }
1085
+ return { success: true };
1086
+ }
1087
+
1088
+ // src/server/project-root.ts
1089
+ import fs3 from "fs";
1090
+ import path7 from "path";
1091
+ import { execSync } from "child_process";
1092
+ var serverLogger3 = createLogger("inspecto:server", { logLevel: getGlobalLogLevel() });
756
1093
  function resolveProjectRoot() {
757
1094
  const cwd = process.cwd();
758
1095
  let gitRoot;
759
1096
  try {
760
1097
  gitRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
761
1098
  } catch (e) {
762
- serverLogger.warn("Failed to resolve git root via git rev-parse:", e);
1099
+ serverLogger3.warn("Failed to resolve git root via git rev-parse:", e);
763
1100
  gitRoot = cwd;
764
1101
  }
765
1102
  const visited = /* @__PURE__ */ new Set();
@@ -767,34 +1104,31 @@ function resolveProjectRoot() {
767
1104
  let current = start;
768
1105
  while (!visited.has(current)) {
769
1106
  visited.add(current);
770
- if (fs3.existsSync(path6.join(current, ".inspecto"))) return current;
1107
+ if (fs3.existsSync(path7.join(current, ".inspecto"))) return current;
771
1108
  if (current === stop) break;
772
- const parent = path6.dirname(current);
1109
+ const parent = path7.dirname(current);
773
1110
  if (parent === current) break;
774
1111
  current = parent;
775
1112
  }
776
1113
  return null;
777
1114
  };
778
- const cwdMatch = search(cwd, path6.parse(cwd).root);
1115
+ const cwdMatch = search(cwd, path7.parse(cwd).root);
779
1116
  if (cwdMatch) return cwdMatch;
780
- const repoMatch = search(gitRoot, path6.parse(gitRoot).root);
1117
+ const repoMatch = search(gitRoot, path7.parse(gitRoot).root);
781
1118
  if (repoMatch) return repoMatch;
782
1119
  return gitRoot;
783
1120
  }
784
- function launchURI(uri) {
785
- try {
786
- if (process.platform === "darwin") {
787
- execFileSync("open", [uri]);
788
- } else if (process.platform === "win32") {
789
- execFileSync("cmd", ["/c", "start", '""', uri]);
790
- } else {
791
- execFileSync("xdg-open", [uri]);
792
- }
793
- } catch (e) {
794
- serverLogger.error("Failed to launch URI via execFileSync, falling back to launchIDE:", e);
795
- launchIDE({ file: uri });
796
- }
797
- }
1121
+
1122
+ // src/server/index.ts
1123
+ var serverLogger4 = createLogger("inspecto:server", { logLevel: getGlobalLogLevel() });
1124
+ var serverState = {
1125
+ port: null,
1126
+ running: false,
1127
+ projectRoot: "",
1128
+ configRoot: "",
1129
+ cwd: process.cwd()
1130
+ };
1131
+ var serverInstance = null;
798
1132
  async function startServer() {
799
1133
  if (serverState.running && serverState.port !== null) {
800
1134
  return serverState.port;
@@ -806,7 +1140,7 @@ async function startServer() {
806
1140
  const port = await portfinder.getPortPromise();
807
1141
  watchConfig(
808
1142
  () => {
809
- serverLogger.info("user config reloaded.");
1143
+ serverLogger4.info("user config reloaded.");
810
1144
  },
811
1145
  serverState.cwd,
812
1146
  serverState.configRoot
@@ -822,7 +1156,7 @@ async function startServer() {
822
1156
  }
823
1157
  const url = new URL(req.url ?? "/", `http://localhost:${port}`);
824
1158
  handleRequest(url, req, res).catch((err) => {
825
- serverLogger.error("server error:", err);
1159
+ serverLogger4.error("server error:", err);
826
1160
  res.writeHead(500, { "Content-Type": "application/json" });
827
1161
  res.end(JSON.stringify({ success: false, error: String(err) }));
828
1162
  });
@@ -835,41 +1169,41 @@ async function startServer() {
835
1169
  serverInstance.once("error", reject);
836
1170
  });
837
1171
  serverInstance.on("error", (err) => {
838
- serverLogger.error("persistent server error:", err);
1172
+ serverLogger4.error("persistent server error:", err);
839
1173
  });
840
1174
  serverState.port = port;
841
1175
  serverState.running = true;
842
- const portFile = path6.join(os2.tmpdir(), "inspecto.port.json");
1176
+ const portFile = path8.join(os2.tmpdir(), "inspecto.port.json");
843
1177
  try {
844
1178
  let portData = {};
845
- if (fs3.existsSync(portFile)) {
1179
+ if (fs4.existsSync(portFile)) {
846
1180
  try {
847
- portData = JSON.parse(fs3.readFileSync(portFile, "utf-8"));
1181
+ portData = JSON.parse(fs4.readFileSync(portFile, "utf-8"));
848
1182
  } catch (e) {
849
1183
  }
850
1184
  }
851
- const rootHash = crypto.createHash("md5").update(serverState.projectRoot).digest("hex");
1185
+ const rootHash = crypto2.createHash("md5").update(serverState.projectRoot).digest("hex");
852
1186
  portData[rootHash] = port;
853
- fs3.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
1187
+ fs4.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
854
1188
  } catch (e) {
855
- serverLogger.warn("Failed to write port file:", e);
1189
+ serverLogger4.warn("Failed to write port file:", e);
856
1190
  }
857
1191
  process.once("exit", () => {
858
1192
  try {
859
- if (fs3.existsSync(portFile)) {
860
- const portData = JSON.parse(fs3.readFileSync(portFile, "utf-8"));
861
- const rootHash = crypto.createHash("md5").update(serverState.projectRoot).digest("hex");
1193
+ if (fs4.existsSync(portFile)) {
1194
+ const portData = JSON.parse(fs4.readFileSync(portFile, "utf-8"));
1195
+ const rootHash = crypto2.createHash("md5").update(serverState.projectRoot).digest("hex");
862
1196
  delete portData[rootHash];
863
1197
  if (Object.keys(portData).length === 0) {
864
- fs3.unlinkSync(portFile);
1198
+ fs4.unlinkSync(portFile);
865
1199
  } else {
866
- fs3.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
1200
+ fs4.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
867
1201
  }
868
1202
  }
869
1203
  } catch {
870
1204
  }
871
1205
  });
872
- serverLogger.info(`server running at http://127.0.0.1:${port}`);
1206
+ serverLogger4.info(`server running at http://127.0.0.1:${port}`);
873
1207
  return port;
874
1208
  }
875
1209
  async function readBody(req) {
@@ -888,26 +1222,7 @@ async function handleRequest(url, req, res) {
888
1222
  return;
889
1223
  }
890
1224
  if (pathname === INSPECTO_API_PATHS.CLIENT_CONFIG && req.method === "GET") {
891
- const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
892
- const promptsConfig = await loadPromptsConfig(false, serverState.cwd, serverState.configRoot);
893
- const effectiveIde = userConfig.ide ?? "vscode";
894
- let info;
895
- if (!serverState.ideInfo) {
896
- info = {
897
- ide: effectiveIde
898
- };
899
- } else {
900
- const { scheme: _scheme, ...rest } = serverState.ideInfo;
901
- info = rest;
902
- }
903
- const config = {
904
- ...info,
905
- prompts: resolveIntents(promptsConfig),
906
- hotKeys: userConfig["inspector.hotKey"] ?? "alt",
907
- theme: userConfig["inspector.theme"] ?? "auto",
908
- includeSnippet: userConfig["prompt.includeSnippet"] ?? false,
909
- autoSend: userConfig["prompt.autoSend"] ?? false
910
- };
1225
+ const config = await buildClientConfig(serverState);
911
1226
  delete config.providers;
912
1227
  res.writeHead(200, { "Content-Type": "application/json" });
913
1228
  res.end(JSON.stringify(config));
@@ -918,23 +1233,23 @@ async function handleRequest(url, req, res) {
918
1233
  const body = JSON.parse(await readBody(req));
919
1234
  const ideWorkspace = body.workspaceRoot || "";
920
1235
  const serverProjectRoot = serverState.projectRoot || "";
921
- const normalizedIdeRoot = ideWorkspace ? path6.resolve(ideWorkspace) : "";
922
- const normalizedServerRoot = serverProjectRoot ? path6.resolve(serverProjectRoot) : "";
923
- const isSameProject = !normalizedIdeRoot || !normalizedServerRoot || normalizedIdeRoot === normalizedServerRoot || normalizedServerRoot.startsWith(normalizedIdeRoot + path6.sep) || normalizedIdeRoot.startsWith(normalizedServerRoot + path6.sep);
1236
+ const normalizedIdeRoot = ideWorkspace ? path8.resolve(ideWorkspace) : "";
1237
+ const normalizedServerRoot = serverProjectRoot ? path8.resolve(serverProjectRoot) : "";
1238
+ const isSameProject = !normalizedIdeRoot || !normalizedServerRoot || normalizedIdeRoot === normalizedServerRoot || normalizedServerRoot.startsWith(normalizedIdeRoot + path8.sep) || normalizedIdeRoot.startsWith(normalizedServerRoot + path8.sep);
924
1239
  if (isSameProject) {
925
1240
  serverState.ideInfo = body;
926
- serverLogger.debug(
1241
+ serverLogger4.debug(
927
1242
  `Accepted IDE info from matched workspace (ide-${body.ide} / schema-${body.scheme})`
928
1243
  );
929
1244
  } else {
930
- serverLogger.debug(
1245
+ serverLogger4.debug(
931
1246
  `Ignored IDE info from unrelated workspace (IDE Workspace: ${ideWorkspace}, Server: ${serverProjectRoot}, Scheme: ${body.scheme}, IDE: ${body.ide})`
932
1247
  );
933
1248
  }
934
1249
  res.writeHead(200, { "Content-Type": "application/json" });
935
1250
  res.end(JSON.stringify({ success: true }));
936
1251
  } catch (e) {
937
- serverLogger.error(`Error parsing ${INSPECTO_API_PATHS.IDE_INFO} POST request:`, e);
1252
+ serverLogger4.error(`Error parsing ${INSPECTO_API_PATHS.IDE_INFO} POST request:`, e);
938
1253
  res.writeHead(400, { "Content-Type": "application/json" });
939
1254
  res.end(JSON.stringify({ error: "Invalid JSON body" }));
940
1255
  }
@@ -949,79 +1264,14 @@ async function handleRequest(url, req, res) {
949
1264
  res.end(JSON.stringify({ error: "Invalid JSON body" }));
950
1265
  return;
951
1266
  }
952
- const absolutePath = path6.isAbsolute(body.file) ? path6.resolve(body.file) : path6.resolve(serverState.cwd, body.file);
953
- const relativeToRoot = path6.relative(serverState.projectRoot, absolutePath);
954
- if (relativeToRoot.startsWith("..") || path6.isAbsolute(relativeToRoot)) {
955
- serverLogger.warn(`Security: Blocked path traversal attempt in IDE_OPEN: ${body.file}`);
1267
+ try {
1268
+ handleOpenFileRequest(body, serverState);
1269
+ } catch {
1270
+ serverLogger4.warn(`Security: Blocked path traversal attempt in IDE_OPEN: ${body.file}`);
956
1271
  res.writeHead(403, { "Content-Type": "application/json" });
957
1272
  res.end(JSON.stringify({ error: "Access denied: File is outside of project workspace" }));
958
1273
  return;
959
1274
  }
960
- const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
961
- const configuredIde = userConfig.ide;
962
- const activeIde = serverState.ideInfo?.ide;
963
- const activeIdeScheme = serverState.ideInfo?.scheme;
964
- const rawEditorHint = configuredIde || activeIde || activeIdeScheme || "code";
965
- if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
966
- serverLogger.warn(
967
- `Active IDE is ${activeIdeScheme}, but config forces ${configuredIde}. Using configured IDE.`
968
- );
969
- }
970
- let editorHint = rawEditorHint;
971
- if (rawEditorHint === "vscode") editorHint = "code";
972
- else if (rawEditorHint === "vscode-insiders") editorHint = "code-insiders";
973
- else if (rawEditorHint === "vscodium") editorHint = "codium";
974
- else if (rawEditorHint === "trae-cn" || rawEditorHint === "trae") editorHint = "trae";
975
- serverLogger.debug(
976
- `IDE_OPEN: activeIde=${activeIde}, activeIdeScheme=${activeIdeScheme}, configuredIde=${configuredIde} -> rawEditorHint=${rawEditorHint}, finalEditorHint=${editorHint}`
977
- );
978
- const VSCODE_FAMILY_SCHEMES = [
979
- "vscode",
980
- "vscode-insiders",
981
- "cursor",
982
- "windsurf",
983
- "trae",
984
- "trae-cn",
985
- "vscodium",
986
- "codebuddy",
987
- "codebuddy-cn",
988
- "antigravity"
989
- ];
990
- if (VSCODE_FAMILY_SCHEMES.includes(rawEditorHint)) {
991
- let normalizedPath = absolutePath.replace(/\\/g, "/");
992
- if (!normalizedPath.startsWith("/")) {
993
- normalizedPath = "/" + normalizedPath;
994
- }
995
- const encodedPath = encodeURI(normalizedPath);
996
- const uri = `${rawEditorHint}://file${encodedPath}:${body.line}:${body.column}`;
997
- serverLogger.debug(`IDE_OPEN: Bypassing launchIDE, using URI scheme directly: ${uri}`);
998
- try {
999
- if (process.platform === "darwin") {
1000
- execFileSync("open", [uri]);
1001
- } else if (process.platform === "win32") {
1002
- execFileSync("cmd", ["/c", "start", '""', uri]);
1003
- } else {
1004
- execFileSync("xdg-open", [uri]);
1005
- }
1006
- } catch (e) {
1007
- serverLogger.error(`Failed to launch URI for IDE_OPEN (${uri}):`, e);
1008
- launchIDE({
1009
- file: absolutePath,
1010
- line: body.line,
1011
- column: body.column,
1012
- editor: editorHint,
1013
- type: process.platform === "darwin" ? "open" : "exec"
1014
- });
1015
- }
1016
- } else {
1017
- launchIDE({
1018
- file: absolutePath,
1019
- line: body.line,
1020
- column: body.column,
1021
- editor: editorHint,
1022
- type: process.platform === "darwin" ? "open" : "exec"
1023
- });
1024
- }
1025
1275
  res.writeHead(200, { "Content-Type": "application/json" });
1026
1276
  res.end(JSON.stringify({ success: true }));
1027
1277
  return;
@@ -1032,10 +1282,11 @@ async function handleRequest(url, req, res) {
1032
1282
  const column = parseInt(url.searchParams.get("column") ?? "1", 10);
1033
1283
  const maxLines = parseInt(url.searchParams.get("maxLines") ?? "100", 10);
1034
1284
  try {
1035
- const absolutePath = path6.isAbsolute(file) ? path6.resolve(file) : path6.resolve(serverState.cwd, file);
1036
- const relativeToRoot = path6.relative(serverState.projectRoot, absolutePath);
1037
- if (relativeToRoot.startsWith("..") || path6.isAbsolute(relativeToRoot)) {
1038
- serverLogger.warn(`Security: Blocked path traversal attempt in PROJECT_SNIPPET: ${file}`);
1285
+ const absolutePath = resolveWorkspacePath(file, serverState.cwd);
1286
+ try {
1287
+ assertPathWithinProject(absolutePath, serverState.projectRoot);
1288
+ } catch {
1289
+ serverLogger4.warn(`Security: Blocked path traversal attempt in PROJECT_SNIPPET: ${file}`);
1039
1290
  res.writeHead(403, { "Content-Type": "application/json" });
1040
1291
  res.end(
1041
1292
  JSON.stringify({
@@ -1065,7 +1316,23 @@ async function handleRequest(url, req, res) {
1065
1316
  res.writeHead(result.success ? 200 : 500, { "Content-Type": "application/json" });
1066
1317
  res.end(JSON.stringify(result));
1067
1318
  } catch (e) {
1068
- serverLogger.error(`Error parsing ${INSPECTO_API_PATHS.AI_DISPATCH} request:`, e);
1319
+ serverLogger4.error(`Error parsing ${INSPECTO_API_PATHS.AI_DISPATCH} request:`, e);
1320
+ res.writeHead(500, { "Content-Type": "application/json" });
1321
+ res.end(JSON.stringify({ success: false, error: String(e), errorCode: "INTERNAL_ERROR" }));
1322
+ }
1323
+ return;
1324
+ }
1325
+ if (pathname === INSPECTO_API_PATHS.AI_BATCH_DISPATCH && req.method === "POST") {
1326
+ try {
1327
+ const rawBody = await readBody(req);
1328
+ const body = JSON.parse(rawBody);
1329
+ const result = await dispatchAnnotationsToAi(body, serverState);
1330
+ res.writeHead(getBatchDispatchStatusCode(result.errorCode, result.success), {
1331
+ "Content-Type": "application/json"
1332
+ });
1333
+ res.end(JSON.stringify(result));
1334
+ } catch (e) {
1335
+ serverLogger4.error(`Error parsing ${INSPECTO_API_PATHS.AI_BATCH_DISPATCH} request:`, e);
1069
1336
  res.writeHead(500, { "Content-Type": "application/json" });
1070
1337
  res.end(JSON.stringify({ success: false, error: String(e), errorCode: "INTERNAL_ERROR" }));
1071
1338
  }
@@ -1073,7 +1340,7 @@ async function handleRequest(url, req, res) {
1073
1340
  }
1074
1341
  if (pathname.startsWith(`${INSPECTO_API_PATHS.AI_TICKET}/`) && req.method === "GET") {
1075
1342
  const ticketId = pathname.substring(INSPECTO_API_PATHS.AI_TICKET.length + 1);
1076
- const payloadStr = payloadTickets.get(ticketId);
1343
+ const payloadStr = readTicket(ticketId);
1077
1344
  if (!payloadStr) {
1078
1345
  res.writeHead(404, { "Content-Type": "application/json" });
1079
1346
  res.end(JSON.stringify({ success: false, error: "Ticket not found or expired" }));
@@ -1087,54 +1354,28 @@ async function handleRequest(url, req, res) {
1087
1354
  res.end(JSON.stringify({ error: "not found" }));
1088
1355
  }
1089
1356
  async function dispatchToAi(req) {
1090
- const { location, snippet, prompt } = req;
1091
- const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
1092
- const resolvedTarget = resolveTargetTool(userConfig);
1357
+ const { location, snippet, prompt, screenshotContext } = req;
1093
1358
  const formattedPrompt = prompt ?? `Please help me with this code from \`${location.file}\` (line ${location.line}):
1094
1359
 
1095
1360
  \`\`\`
1096
1361
  ${snippet}
1097
1362
  \`\`\`
1098
1363
  `;
1099
- const ideReportedMode = serverState.ideInfo?.providers[resolvedTarget]?.mode;
1100
- const configuredIde = userConfig.ide;
1101
- const activeIde = serverState.ideInfo?.ide;
1102
- const activeIdeScheme = serverState.ideInfo?.scheme;
1103
- const finalIde = configuredIde || activeIdeScheme || activeIde || "vscode";
1104
- if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
1105
- serverLogger.warn(
1106
- `dispatchToAi: Active IDE is ${activeIdeScheme}, but config forces ${configuredIde}. Using configured IDE.`
1107
- );
1108
- }
1109
- const mode = resolveProviderMode(resolvedTarget, finalIde, userConfig);
1110
- const overrides = extractToolOverrides(finalIde, userConfig)[resolvedTarget] || {};
1111
- overrides.type = mode;
1112
- const fullPayload = {
1113
- ide: finalIde,
1114
- target: resolvedTarget,
1115
- targetType: mode,
1364
+ const runtime = resolvePromptDispatchRuntime(serverState);
1365
+ return dispatchPromptThroughIde(runtime, {
1116
1366
  prompt: formattedPrompt,
1117
1367
  filePath: location.file,
1118
1368
  line: location.line,
1119
1369
  column: location.column,
1120
1370
  snippet,
1121
- overrides: Object.keys(overrides).length > 0 ? overrides : void 0,
1122
- autoSend: userConfig["prompt.autoSend"] !== void 0 ? Boolean(userConfig["prompt.autoSend"]) : void 0
1123
- };
1124
- const ticketId = createTicket(fullPayload);
1125
- const params = new URLSearchParams();
1126
- params.set("ticket", ticketId);
1127
- params.set("target", resolvedTarget);
1128
- const uri = `${finalIde}://inspecto.inspecto/send?${params.toString()}`;
1129
- serverLogger.debug(`dispatchToAi: Generated URI: ${uri}`);
1130
- launchURI(uri);
1131
- return {
1132
- success: true,
1133
- fallbackPayload: {
1134
- prompt: formattedPrompt,
1135
- file: location.file
1136
- }
1137
- };
1371
+ ...screenshotContext ? { screenshotContext } : {}
1372
+ });
1373
+ }
1374
+ function getBatchDispatchStatusCode(errorCode, success) {
1375
+ if (success) return 200;
1376
+ if (errorCode === "INVALID_REQUEST") return 400;
1377
+ if (errorCode === "FORBIDDEN_PATH") return 403;
1378
+ return 500;
1138
1379
  }
1139
1380
 
1140
1381
  // src/injectors/utils.ts