@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.
package/dist/rspack.cjs CHANGED
@@ -309,13 +309,11 @@ function transformRouter(options) {
309
309
 
310
310
  // src/server/index.ts
311
311
  var import_node_http = __toESM(require("http"), 1);
312
- var import_node_fs2 = __toESM(require("fs"), 1);
313
- var import_node_path5 = __toESM(require("path"), 1);
312
+ var import_node_fs3 = __toESM(require("fs"), 1);
313
+ var import_node_path7 = __toESM(require("path"), 1);
314
314
  var import_node_os2 = __toESM(require("os"), 1);
315
- var import_node_crypto = __toESM(require("crypto"), 1);
316
- var import_node_child_process = require("child_process");
315
+ var import_node_crypto2 = __toESM(require("crypto"), 1);
317
316
  var import_portfinder = __toESM(require("portfinder"), 1);
318
- var import_launch_ide = require("launch-ide");
319
317
  var import_types2 = require("@inspecto-dev/types");
320
318
 
321
319
  // src/server/snippet.ts
@@ -649,9 +647,9 @@ function extractToolOverrides(ide, config) {
649
647
  function resolveIntents(serverPrompts) {
650
648
  const baseMap = /* @__PURE__ */ new Map();
651
649
  for (const intent of import_types.DEFAULT_INTENTS) {
652
- if (intent.id) baseMap.set(intent.id, { ...intent });
650
+ baseMap.set(intent.id, { ...intent });
653
651
  }
654
- const defaults = () => ensureOpenInEditorLast(Array.from(baseMap.values()));
652
+ const defaults = () => Array.from(baseMap.values());
655
653
  if (!serverPrompts) return defaults();
656
654
  const isReplace = !Array.isArray(serverPrompts) && typeof serverPrompts === "object" && serverPrompts.$replace === true;
657
655
  const promptsArray = Array.isArray(serverPrompts) ? serverPrompts : isReplace ? serverPrompts.items : [];
@@ -678,16 +676,18 @@ function resolveIntents(serverPrompts) {
678
676
  );
679
677
  continue;
680
678
  }
681
- if (item.isAction && item.id !== "open-in-editor") {
679
+ if (!item.aiIntent) {
682
680
  configLogger.warn(
683
- `isAction is reserved for built-in actions. Ignoring intent "${item.id}".`
681
+ `Intent "${item.id}" is missing required "aiIntent".`
684
682
  );
685
683
  continue;
686
684
  }
687
- result.push(baseMap.has(item.id) ? { ...baseMap.get(item.id), ...item } : item);
685
+ result.push(
686
+ baseMap.has(item.id) ? { ...baseMap.get(item.id), ...item } : item
687
+ );
688
688
  }
689
689
  }
690
- return ensureOpenInEditorLast(result);
690
+ return result;
691
691
  }
692
692
  const merged = Array.from(baseMap.values());
693
693
  for (const item of promptsArray) {
@@ -704,9 +704,9 @@ function resolveIntents(serverPrompts) {
704
704
  configLogger.warn('Intent object missing required "id" field, skipping.');
705
705
  continue;
706
706
  }
707
- if (item.isAction && item.id !== "open-in-editor") {
707
+ if (!item.aiIntent) {
708
708
  configLogger.warn(
709
- `isAction is reserved for built-in actions. Ignoring intent "${item.id}".`
709
+ `Intent "${item.id}" is missing required "aiIntent".`
710
710
  );
711
711
  continue;
712
712
  }
@@ -724,15 +724,7 @@ function resolveIntents(serverPrompts) {
724
724
  }
725
725
  }
726
726
  }
727
- return ensureOpenInEditorLast(merged);
728
- }
729
- function ensureOpenInEditorLast(intents) {
730
- const idx = intents.findIndex((i) => i.id === "open-in-editor");
731
- if (idx === -1 || idx === intents.length - 1) return intents;
732
- const result = [...intents];
733
- const item = result.splice(idx, 1)[0];
734
- result.push(item);
735
- return result;
727
+ return merged;
736
728
  }
737
729
  var watchers = [];
738
730
  function watchConfig(onReload, cwd = process.cwd(), gitRoot) {
@@ -767,7 +759,10 @@ function watchConfig(onReload, cwd = process.cwd(), gitRoot) {
767
759
  }
768
760
  }
769
761
 
770
- // src/server/index.ts
762
+ // src/server/dispatch-transport.ts
763
+ var import_node_crypto = __toESM(require("crypto"), 1);
764
+ var import_node_child_process = require("child_process");
765
+ var import_launch_ide = require("launch-ide");
771
766
  var serverLogger = createLogger("inspecto:server", { logLevel: getGlobalLogLevel() });
772
767
  var payloadTickets = /* @__PURE__ */ new Map();
773
768
  function createTicket(payload) {
@@ -781,32 +776,8 @@ function createTicket(payload) {
781
776
  );
782
777
  return ticketId;
783
778
  }
784
- var serverState = {
785
- port: null,
786
- running: false,
787
- projectRoot: "",
788
- configRoot: "",
789
- cwd: process.cwd()
790
- };
791
- var serverInstance = null;
792
- function resolveProjectRoot() {
793
- let gitRoot;
794
- try {
795
- serverLogger.info("Resolving project root...");
796
- gitRoot = (0, import_node_child_process.execSync)("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
797
- serverLogger.info("Resolved project root: " + gitRoot);
798
- } catch (e) {
799
- serverLogger.error("Failed to resolve project root:", e);
800
- gitRoot = process.cwd();
801
- }
802
- let current = gitRoot;
803
- while (true) {
804
- if (import_node_fs2.default.existsSync(import_node_path5.default.join(current, ".inspecto"))) return current;
805
- const parent = import_node_path5.default.dirname(current);
806
- if (parent === current) break;
807
- current = parent;
808
- }
809
- return gitRoot;
779
+ function readTicket(ticketId) {
780
+ return payloadTickets.get(ticketId);
810
781
  }
811
782
  function launchURI(uri) {
812
783
  try {
@@ -822,6 +793,378 @@ function launchURI(uri) {
822
793
  (0, import_launch_ide.launchIDE)({ file: uri });
823
794
  }
824
795
  }
796
+
797
+ // src/server/dispatch-runtime.ts
798
+ function resolvePromptDispatchRuntime(state) {
799
+ const userConfig = loadUserConfigSync(false, state.cwd, state.projectRoot);
800
+ const resolvedTarget = resolveTargetTool(userConfig);
801
+ const finalIde = resolveFinalIde(userConfig.ide, state.ideInfo?.ide, state.ideInfo?.scheme);
802
+ const mode = resolveProviderMode(resolvedTarget, finalIde, userConfig);
803
+ const overrides = extractToolOverrides(finalIde, userConfig)[resolvedTarget] || void 0;
804
+ return {
805
+ resolvedTarget,
806
+ finalIde,
807
+ mode,
808
+ ...hasOverrides(overrides) ? { overrides } : {},
809
+ ...userConfig["prompt.autoSend"] !== void 0 ? { autoSend: Boolean(userConfig["prompt.autoSend"]) } : {}
810
+ };
811
+ }
812
+ function dispatchPromptThroughIde(runtime, payload) {
813
+ const ticketId = createTicket({
814
+ ide: runtime.finalIde,
815
+ target: runtime.resolvedTarget,
816
+ targetType: runtime.mode,
817
+ prompt: payload.prompt,
818
+ filePath: payload.filePath,
819
+ line: payload.line,
820
+ column: payload.column,
821
+ snippet: payload.snippet,
822
+ ...payload.screenshotContext ? { screenshotContext: payload.screenshotContext } : {},
823
+ overrides: runtime.overrides,
824
+ autoSend: runtime.autoSend
825
+ });
826
+ const params = new URLSearchParams();
827
+ params.set("ticket", ticketId);
828
+ params.set("target", runtime.resolvedTarget);
829
+ launchURI(`${runtime.finalIde}://inspecto.inspecto/send?${params.toString()}`);
830
+ return {
831
+ success: true,
832
+ fallbackPayload: {
833
+ prompt: payload.prompt,
834
+ ...payload.filePath ? { file: payload.filePath } : {}
835
+ }
836
+ };
837
+ }
838
+ function resolveFinalIde(configuredIde, activeIde, activeIdeScheme) {
839
+ if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
840
+ return configuredIde;
841
+ }
842
+ return configuredIde || activeIdeScheme || activeIde || "vscode";
843
+ }
844
+ function hasOverrides(overrides) {
845
+ return Boolean(overrides && Object.keys(overrides).length > 0);
846
+ }
847
+
848
+ // src/server/path-guards.ts
849
+ var import_node_path5 = __toESM(require("path"), 1);
850
+ function isWindowsAbsolutePath(file) {
851
+ return /^[a-zA-Z]:[\\/]/.test(file) || /^\\\\[^\\]+\\[^\\]+/.test(file);
852
+ }
853
+ function resolveWorkspacePath(file, cwd) {
854
+ if (isWindowsAbsolutePath(file)) {
855
+ return import_node_path5.default.win32.normalize(file);
856
+ }
857
+ return import_node_path5.default.isAbsolute(file) ? import_node_path5.default.resolve(file) : import_node_path5.default.resolve(cwd, file);
858
+ }
859
+ function assertPathWithinProject(file, projectRoot) {
860
+ const relativeToRoot = isWindowsAbsolutePath(file) || isWindowsAbsolutePath(projectRoot) ? import_node_path5.default.win32.relative(import_node_path5.default.win32.normalize(projectRoot), import_node_path5.default.win32.normalize(file)) : import_node_path5.default.relative(projectRoot, file);
861
+ if (relativeToRoot.startsWith("..") || import_node_path5.default.isAbsolute(relativeToRoot)) {
862
+ throw new Error("Access denied: File is outside of project workspace");
863
+ }
864
+ }
865
+
866
+ // src/server/annotation-dispatch.ts
867
+ var AnnotationDispatchError = class extends Error {
868
+ constructor(message, errorCode) {
869
+ super(message);
870
+ this.name = "AnnotationDispatchError";
871
+ this.errorCode = errorCode;
872
+ }
873
+ };
874
+ async function dispatchAnnotationsToAi(req, state) {
875
+ try {
876
+ validateAnnotationDispatchRequest(req, state);
877
+ const batch = normalizeAnnotationBatch(req);
878
+ const prompt = buildAnnotationBatchPrompt(batch);
879
+ const representativeTarget = batch.annotations[0]?.targets[0];
880
+ const runtime = resolvePromptDispatchRuntime(state);
881
+ return dispatchPromptThroughIde(runtime, {
882
+ prompt,
883
+ ...representativeTarget?.file ? { filePath: representativeTarget.file } : {},
884
+ ...representativeTarget?.line ? { line: representativeTarget.line } : {},
885
+ ...representativeTarget?.column ? { column: representativeTarget.column } : {},
886
+ ...batch.screenshotContext ? { screenshotContext: batch.screenshotContext } : {}
887
+ });
888
+ } catch (error) {
889
+ return {
890
+ success: false,
891
+ error: error instanceof Error ? error.message : String(error),
892
+ errorCode: getAnnotationDispatchErrorCode(error)
893
+ };
894
+ }
895
+ }
896
+ function validateAnnotationDispatchRequest(req, state) {
897
+ if (!req.annotations.length) {
898
+ throw new AnnotationDispatchError("At least one annotation is required.", "INVALID_REQUEST");
899
+ }
900
+ for (const annotation of req.annotations) {
901
+ if (!annotation.targets.length) {
902
+ throw new AnnotationDispatchError(
903
+ "Each annotation must include at least one target.",
904
+ "INVALID_REQUEST"
905
+ );
906
+ }
907
+ for (const target of annotation.targets) {
908
+ const absolutePath = resolveWorkspacePath(target.location.file, state.cwd);
909
+ assertPathWithinProject(absolutePath, state.projectRoot);
910
+ }
911
+ }
912
+ }
913
+ function normalizeAnnotationBatch(req) {
914
+ return {
915
+ instruction: req.instruction?.trim() ?? "",
916
+ responseMode: req.responseMode ?? "unified",
917
+ ...req.runtimeContext ? { runtimeContext: req.runtimeContext } : {},
918
+ ...req.screenshotContext ? { screenshotContext: req.screenshotContext } : {},
919
+ ...req.cssContextPrompt?.trim() ? { cssContextPrompt: req.cssContextPrompt.trim() } : {},
920
+ annotations: req.annotations.map((annotation, index) => ({
921
+ index: index + 1,
922
+ note: annotation.note.trim(),
923
+ intent: annotation.intent,
924
+ targets: annotation.targets.map((target) => ({
925
+ file: target.location.file,
926
+ line: target.location.line,
927
+ column: target.location.column,
928
+ ...target.label ? { label: target.label } : {},
929
+ ...target.selector ? { selector: target.selector } : {},
930
+ ...target.snippet ? { snippet: target.snippet } : {}
931
+ }))
932
+ }))
933
+ };
934
+ }
935
+ function buildAnnotationBatchPrompt(batch) {
936
+ const body = buildSelectedElementsPrompt(batch.annotations);
937
+ const prompt = batch.instruction ? `${batch.instruction}
938
+
939
+ ${body}` : body;
940
+ return appendScreenshotContextSection(
941
+ appendCssContextSection(
942
+ appendRuntimeContextSection(prompt, batch.runtimeContext),
943
+ batch.cssContextPrompt
944
+ ),
945
+ batch.screenshotContext
946
+ );
947
+ }
948
+ function appendCssContextSection(prompt, cssContextPrompt) {
949
+ if (!cssContextPrompt) return prompt;
950
+ return `${prompt}
951
+
952
+ ${cssContextPrompt}`;
953
+ }
954
+ function buildSelectedElementsPrompt(annotations) {
955
+ const lines = ["Selected elements:"];
956
+ for (const annotation of annotations) {
957
+ const trimmedNote = annotation.note.trim();
958
+ for (const target of annotation.targets) {
959
+ const targetLabel = (target.label || "Unknown target").trim() || "Unknown target";
960
+ lines.push(`- ${targetLabel}`);
961
+ lines.push(`file=${target.file}:${target.line}:${target.column}`);
962
+ if (trimmedNote) {
963
+ lines.push(`note=${trimmedNote}`);
964
+ }
965
+ }
966
+ }
967
+ if (lines.length === 1) {
968
+ lines.push("- None");
969
+ }
970
+ return lines.join("\n");
971
+ }
972
+ function appendScreenshotContextSection(prompt, screenshotContext) {
973
+ if (!screenshotContext || !screenshotContext.imageDataUrl && !screenshotContext.imageAssetId) {
974
+ return prompt;
975
+ }
976
+ const lines = [
977
+ "Visual screenshot context attached:",
978
+ `- capturedAt=${screenshotContext.capturedAt}`,
979
+ `- mimeType=${screenshotContext.mimeType}`,
980
+ ...screenshotContext.imageAssetId ? [`- imageAssetId=${screenshotContext.imageAssetId}`] : []
981
+ ];
982
+ return `${prompt}
983
+
984
+ ${lines.join("\n")}`;
985
+ }
986
+ function appendRuntimeContextSection(prompt, runtimeContext) {
987
+ if (!runtimeContext?.records.length) {
988
+ return prompt;
989
+ }
990
+ return `${prompt}
991
+
992
+ ${buildRuntimeContextSection(runtimeContext.records)}`;
993
+ }
994
+ function buildRuntimeContextSection(records) {
995
+ return ["Relevant runtime context:", ...records.map(formatRuntimeRecord)].join("\n");
996
+ }
997
+ function formatRuntimeRecord(record) {
998
+ 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}`;
999
+ const reasonSummary = record.relevanceReasons.length ? record.relevanceReasons.join("; ") : "timing-based";
1000
+ const stackSummary = record.stack ? `
1001
+ stack=${record.stack.split("\n").slice(0, 5).join(" | ")}` : "";
1002
+ return [
1003
+ `- [${record.kind}] ${record.message}`,
1004
+ ` relevance=${record.relevanceLevel} (${reasonSummary})`,
1005
+ ` ${requestSummary}`,
1006
+ stackSummary
1007
+ ].filter(Boolean).join("\n");
1008
+ }
1009
+ function getAnnotationDispatchErrorCode(error) {
1010
+ if (error instanceof AnnotationDispatchError) return error.errorCode;
1011
+ if (error instanceof Error && error.message.includes("outside of project workspace")) {
1012
+ return "FORBIDDEN_PATH";
1013
+ }
1014
+ return "UNKNOWN";
1015
+ }
1016
+
1017
+ // src/server/client-config.ts
1018
+ async function buildClientConfig(serverState2) {
1019
+ const userConfig = loadUserConfigSync(false, serverState2.cwd, serverState2.configRoot);
1020
+ const promptsConfig = await loadPromptsConfig(false, serverState2.cwd, serverState2.configRoot);
1021
+ const effectiveIde = userConfig.ide ?? "vscode";
1022
+ let info;
1023
+ if (!serverState2.ideInfo) {
1024
+ info = { ide: effectiveIde };
1025
+ } else {
1026
+ const { scheme: _scheme, ...rest } = serverState2.ideInfo;
1027
+ info = rest;
1028
+ }
1029
+ return {
1030
+ ...info,
1031
+ prompts: resolveIntents(promptsConfig),
1032
+ hotKeys: userConfig["inspector.hotKey"] ?? "alt",
1033
+ theme: userConfig["inspector.theme"] ?? "auto",
1034
+ includeSnippet: userConfig["prompt.includeSnippet"] ?? false,
1035
+ runtimeContext: {
1036
+ enabled: true,
1037
+ preview: true,
1038
+ maxRuntimeErrors: 3,
1039
+ maxFailedRequests: 2
1040
+ },
1041
+ screenshotContext: {
1042
+ enabled: false
1043
+ },
1044
+ annotationResponseMode: userConfig["prompt.annotationResponseMode"] ?? "unified",
1045
+ autoSend: userConfig["prompt.autoSend"] ?? false
1046
+ };
1047
+ }
1048
+
1049
+ // src/server/open-file.ts
1050
+ var import_node_child_process2 = require("child_process");
1051
+ var import_launch_ide2 = require("launch-ide");
1052
+ var serverLogger2 = createLogger("inspecto:server", { logLevel: getGlobalLogLevel() });
1053
+ var VSCODE_FAMILY_SCHEMES = [
1054
+ "vscode",
1055
+ "vscode-insiders",
1056
+ "cursor",
1057
+ "windsurf",
1058
+ "trae",
1059
+ "trae-cn",
1060
+ "vscodium",
1061
+ "codebuddy",
1062
+ "codebuddy-cn",
1063
+ "antigravity"
1064
+ ];
1065
+ function handleOpenFileRequest(body, serverState2) {
1066
+ const absolutePath = resolveWorkspacePath(body.file, serverState2.cwd);
1067
+ assertPathWithinProject(absolutePath, serverState2.projectRoot);
1068
+ const userConfig = loadUserConfigSync(false, serverState2.cwd, serverState2.configRoot);
1069
+ const configuredIde = userConfig.ide;
1070
+ const activeIde = serverState2.ideInfo?.ide;
1071
+ const activeIdeScheme = serverState2.ideInfo?.scheme;
1072
+ const rawEditorHint = configuredIde || activeIde || activeIdeScheme || "code";
1073
+ if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
1074
+ serverLogger2.warn(
1075
+ `Active IDE is ${activeIdeScheme}, but config forces ${configuredIde}. Using configured IDE.`
1076
+ );
1077
+ }
1078
+ let editorHint = rawEditorHint;
1079
+ if (rawEditorHint === "vscode") editorHint = "code";
1080
+ else if (rawEditorHint === "vscode-insiders") editorHint = "code-insiders";
1081
+ else if (rawEditorHint === "vscodium") editorHint = "codium";
1082
+ else if (rawEditorHint === "trae-cn" || rawEditorHint === "trae") editorHint = "trae";
1083
+ serverLogger2.debug(
1084
+ `IDE_OPEN: activeIde=${activeIde}, activeIdeScheme=${activeIdeScheme}, configuredIde=${configuredIde} -> rawEditorHint=${rawEditorHint}, finalEditorHint=${editorHint}`
1085
+ );
1086
+ if (VSCODE_FAMILY_SCHEMES.includes(rawEditorHint)) {
1087
+ let normalizedPath = absolutePath.replace(/\\/g, "/");
1088
+ if (!normalizedPath.startsWith("/")) {
1089
+ normalizedPath = "/" + normalizedPath;
1090
+ }
1091
+ const encodedPath = encodeURI(normalizedPath);
1092
+ const uri = `${rawEditorHint}://file${encodedPath}:${body.line}:${body.column}`;
1093
+ serverLogger2.debug(`IDE_OPEN: Bypassing launchIDE, using URI scheme directly: ${uri}`);
1094
+ try {
1095
+ if (process.platform === "darwin") {
1096
+ (0, import_node_child_process2.execFileSync)("open", [uri]);
1097
+ } else if (process.platform === "win32") {
1098
+ (0, import_node_child_process2.execFileSync)("cmd", ["/c", "start", '""', uri]);
1099
+ } else {
1100
+ (0, import_node_child_process2.execFileSync)("xdg-open", [uri]);
1101
+ }
1102
+ } catch (e) {
1103
+ serverLogger2.error(`Failed to launch URI for IDE_OPEN (${uri}):`, e);
1104
+ (0, import_launch_ide2.launchIDE)({
1105
+ file: absolutePath,
1106
+ line: body.line,
1107
+ column: body.column,
1108
+ editor: editorHint,
1109
+ type: process.platform === "darwin" ? "open" : "exec"
1110
+ });
1111
+ }
1112
+ } else {
1113
+ (0, import_launch_ide2.launchIDE)({
1114
+ file: absolutePath,
1115
+ line: body.line,
1116
+ column: body.column,
1117
+ editor: editorHint,
1118
+ type: process.platform === "darwin" ? "open" : "exec"
1119
+ });
1120
+ }
1121
+ return { success: true };
1122
+ }
1123
+
1124
+ // src/server/project-root.ts
1125
+ var import_node_fs2 = __toESM(require("fs"), 1);
1126
+ var import_node_path6 = __toESM(require("path"), 1);
1127
+ var import_node_child_process3 = require("child_process");
1128
+ var serverLogger3 = createLogger("inspecto:server", { logLevel: getGlobalLogLevel() });
1129
+ function resolveProjectRoot() {
1130
+ const cwd = process.cwd();
1131
+ let gitRoot;
1132
+ try {
1133
+ gitRoot = (0, import_node_child_process3.execSync)("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
1134
+ } catch (e) {
1135
+ serverLogger3.warn("Failed to resolve git root via git rev-parse:", e);
1136
+ gitRoot = cwd;
1137
+ }
1138
+ const visited = /* @__PURE__ */ new Set();
1139
+ const search = (start, stop) => {
1140
+ let current = start;
1141
+ while (!visited.has(current)) {
1142
+ visited.add(current);
1143
+ if (import_node_fs2.default.existsSync(import_node_path6.default.join(current, ".inspecto"))) return current;
1144
+ if (current === stop) break;
1145
+ const parent = import_node_path6.default.dirname(current);
1146
+ if (parent === current) break;
1147
+ current = parent;
1148
+ }
1149
+ return null;
1150
+ };
1151
+ const cwdMatch = search(cwd, import_node_path6.default.parse(cwd).root);
1152
+ if (cwdMatch) return cwdMatch;
1153
+ const repoMatch = search(gitRoot, import_node_path6.default.parse(gitRoot).root);
1154
+ if (repoMatch) return repoMatch;
1155
+ return gitRoot;
1156
+ }
1157
+
1158
+ // src/server/index.ts
1159
+ var serverLogger4 = createLogger("inspecto:server", { logLevel: getGlobalLogLevel() });
1160
+ var serverState = {
1161
+ port: null,
1162
+ running: false,
1163
+ projectRoot: "",
1164
+ configRoot: "",
1165
+ cwd: process.cwd()
1166
+ };
1167
+ var serverInstance = null;
825
1168
  async function startServer() {
826
1169
  if (serverState.running && serverState.port !== null) {
827
1170
  return serverState.port;
@@ -833,7 +1176,7 @@ async function startServer() {
833
1176
  const port = await import_portfinder.default.getPortPromise();
834
1177
  watchConfig(
835
1178
  () => {
836
- serverLogger.info("user config reloaded.");
1179
+ serverLogger4.info("user config reloaded.");
837
1180
  },
838
1181
  serverState.cwd,
839
1182
  serverState.configRoot
@@ -849,7 +1192,7 @@ async function startServer() {
849
1192
  }
850
1193
  const url = new URL(req.url ?? "/", `http://localhost:${port}`);
851
1194
  handleRequest(url, req, res).catch((err) => {
852
- serverLogger.error("server error:", err);
1195
+ serverLogger4.error("server error:", err);
853
1196
  res.writeHead(500, { "Content-Type": "application/json" });
854
1197
  res.end(JSON.stringify({ success: false, error: String(err) }));
855
1198
  });
@@ -862,41 +1205,41 @@ async function startServer() {
862
1205
  serverInstance.once("error", reject);
863
1206
  });
864
1207
  serverInstance.on("error", (err) => {
865
- serverLogger.error("persistent server error:", err);
1208
+ serverLogger4.error("persistent server error:", err);
866
1209
  });
867
1210
  serverState.port = port;
868
1211
  serverState.running = true;
869
- const portFile = import_node_path5.default.join(import_node_os2.default.tmpdir(), "inspecto.port.json");
1212
+ const portFile = import_node_path7.default.join(import_node_os2.default.tmpdir(), "inspecto.port.json");
870
1213
  try {
871
1214
  let portData = {};
872
- if (import_node_fs2.default.existsSync(portFile)) {
1215
+ if (import_node_fs3.default.existsSync(portFile)) {
873
1216
  try {
874
- portData = JSON.parse(import_node_fs2.default.readFileSync(portFile, "utf-8"));
1217
+ portData = JSON.parse(import_node_fs3.default.readFileSync(portFile, "utf-8"));
875
1218
  } catch (e) {
876
1219
  }
877
1220
  }
878
- const rootHash = import_node_crypto.default.createHash("md5").update(serverState.projectRoot).digest("hex");
1221
+ const rootHash = import_node_crypto2.default.createHash("md5").update(serverState.projectRoot).digest("hex");
879
1222
  portData[rootHash] = port;
880
- import_node_fs2.default.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
1223
+ import_node_fs3.default.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
881
1224
  } catch (e) {
882
- serverLogger.warn("Failed to write port file:", e);
1225
+ serverLogger4.warn("Failed to write port file:", e);
883
1226
  }
884
1227
  process.once("exit", () => {
885
1228
  try {
886
- if (import_node_fs2.default.existsSync(portFile)) {
887
- const portData = JSON.parse(import_node_fs2.default.readFileSync(portFile, "utf-8"));
888
- const rootHash = import_node_crypto.default.createHash("md5").update(serverState.projectRoot).digest("hex");
1229
+ if (import_node_fs3.default.existsSync(portFile)) {
1230
+ const portData = JSON.parse(import_node_fs3.default.readFileSync(portFile, "utf-8"));
1231
+ const rootHash = import_node_crypto2.default.createHash("md5").update(serverState.projectRoot).digest("hex");
889
1232
  delete portData[rootHash];
890
1233
  if (Object.keys(portData).length === 0) {
891
- import_node_fs2.default.unlinkSync(portFile);
1234
+ import_node_fs3.default.unlinkSync(portFile);
892
1235
  } else {
893
- import_node_fs2.default.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
1236
+ import_node_fs3.default.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
894
1237
  }
895
1238
  }
896
1239
  } catch {
897
1240
  }
898
1241
  });
899
- serverLogger.info(`server running at http://127.0.0.1:${port}`);
1242
+ serverLogger4.info(`server running at http://127.0.0.1:${port}`);
900
1243
  return port;
901
1244
  }
902
1245
  async function readBody(req) {
@@ -915,26 +1258,7 @@ async function handleRequest(url, req, res) {
915
1258
  return;
916
1259
  }
917
1260
  if (pathname === import_types2.INSPECTO_API_PATHS.CLIENT_CONFIG && req.method === "GET") {
918
- const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
919
- const promptsConfig = await loadPromptsConfig(false, serverState.cwd, serverState.configRoot);
920
- const effectiveIde = userConfig.ide ?? "vscode";
921
- let info;
922
- if (!serverState.ideInfo) {
923
- info = {
924
- ide: effectiveIde
925
- };
926
- } else {
927
- const { scheme: _scheme, ...rest } = serverState.ideInfo;
928
- info = rest;
929
- }
930
- const config = {
931
- ...info,
932
- prompts: resolveIntents(promptsConfig),
933
- hotKeys: userConfig["inspector.hotKey"] ?? "alt",
934
- theme: userConfig["inspector.theme"] ?? "auto",
935
- includeSnippet: userConfig["prompt.includeSnippet"] ?? false,
936
- autoSend: userConfig["prompt.autoSend"] ?? false
937
- };
1261
+ const config = await buildClientConfig(serverState);
938
1262
  delete config.providers;
939
1263
  res.writeHead(200, { "Content-Type": "application/json" });
940
1264
  res.end(JSON.stringify(config));
@@ -945,21 +1269,23 @@ async function handleRequest(url, req, res) {
945
1269
  const body = JSON.parse(await readBody(req));
946
1270
  const ideWorkspace = body.workspaceRoot || "";
947
1271
  const serverProjectRoot = serverState.projectRoot || "";
948
- const isSameProject = !ideWorkspace || !serverProjectRoot || ideWorkspace === serverProjectRoot || serverProjectRoot.startsWith(ideWorkspace);
1272
+ const normalizedIdeRoot = ideWorkspace ? import_node_path7.default.resolve(ideWorkspace) : "";
1273
+ const normalizedServerRoot = serverProjectRoot ? import_node_path7.default.resolve(serverProjectRoot) : "";
1274
+ const isSameProject = !normalizedIdeRoot || !normalizedServerRoot || normalizedIdeRoot === normalizedServerRoot || normalizedServerRoot.startsWith(normalizedIdeRoot + import_node_path7.default.sep) || normalizedIdeRoot.startsWith(normalizedServerRoot + import_node_path7.default.sep);
949
1275
  if (isSameProject) {
950
1276
  serverState.ideInfo = body;
951
- serverLogger.debug(
1277
+ serverLogger4.debug(
952
1278
  `Accepted IDE info from matched workspace (ide-${body.ide} / schema-${body.scheme})`
953
1279
  );
954
1280
  } else {
955
- serverLogger.debug(
1281
+ serverLogger4.debug(
956
1282
  `Ignored IDE info from unrelated workspace (IDE Workspace: ${ideWorkspace}, Server: ${serverProjectRoot}, Scheme: ${body.scheme}, IDE: ${body.ide})`
957
1283
  );
958
1284
  }
959
1285
  res.writeHead(200, { "Content-Type": "application/json" });
960
1286
  res.end(JSON.stringify({ success: true }));
961
1287
  } catch (e) {
962
- serverLogger.error(`Error parsing ${import_types2.INSPECTO_API_PATHS.IDE_INFO} POST request:`, e);
1288
+ serverLogger4.error(`Error parsing ${import_types2.INSPECTO_API_PATHS.IDE_INFO} POST request:`, e);
963
1289
  res.writeHead(400, { "Content-Type": "application/json" });
964
1290
  res.end(JSON.stringify({ error: "Invalid JSON body" }));
965
1291
  }
@@ -974,74 +1300,14 @@ async function handleRequest(url, req, res) {
974
1300
  res.end(JSON.stringify({ error: "Invalid JSON body" }));
975
1301
  return;
976
1302
  }
977
- const absolutePath = import_node_path5.default.isAbsolute(body.file) ? import_node_path5.default.resolve(body.file) : import_node_path5.default.resolve(serverState.cwd, body.file);
978
- const relativeToRoot = import_node_path5.default.relative(serverState.projectRoot, absolutePath);
979
- if (relativeToRoot.startsWith("..") || import_node_path5.default.isAbsolute(relativeToRoot)) {
980
- serverLogger.warn(`Security: Blocked path traversal attempt in IDE_OPEN: ${body.file}`);
1303
+ try {
1304
+ handleOpenFileRequest(body, serverState);
1305
+ } catch {
1306
+ serverLogger4.warn(`Security: Blocked path traversal attempt in IDE_OPEN: ${body.file}`);
981
1307
  res.writeHead(403, { "Content-Type": "application/json" });
982
1308
  res.end(JSON.stringify({ error: "Access denied: File is outside of project workspace" }));
983
1309
  return;
984
1310
  }
985
- const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
986
- const configuredIde = userConfig.ide;
987
- const activeIde = serverState.ideInfo?.ide;
988
- const activeIdeScheme = serverState.ideInfo?.scheme;
989
- const rawEditorHint = configuredIde || activeIde || activeIdeScheme || "code";
990
- if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
991
- serverLogger.warn(
992
- `Active IDE is ${activeIdeScheme}, but config forces ${configuredIde}. Using configured IDE.`
993
- );
994
- }
995
- let editorHint = rawEditorHint;
996
- if (rawEditorHint === "vscode") editorHint = "code";
997
- else if (rawEditorHint === "vscode-insiders") editorHint = "code-insiders";
998
- else if (rawEditorHint === "vscodium") editorHint = "codium";
999
- else if (rawEditorHint === "trae-cn" || rawEditorHint === "trae") editorHint = "trae";
1000
- serverLogger.debug(
1001
- `IDE_OPEN: activeIde=${activeIde}, activeIdeScheme=${activeIdeScheme}, configuredIde=${configuredIde} -> rawEditorHint=${rawEditorHint}, finalEditorHint=${editorHint}`
1002
- );
1003
- const VSCODE_FAMILY_SCHEMES = [
1004
- "vscode",
1005
- "vscode-insiders",
1006
- "cursor",
1007
- "windsurf",
1008
- "trae",
1009
- "trae-cn",
1010
- "vscodium",
1011
- "codebuddy",
1012
- "codebuddy-cn",
1013
- "antigravity"
1014
- ];
1015
- if (VSCODE_FAMILY_SCHEMES.includes(rawEditorHint)) {
1016
- const uri = `${rawEditorHint}://file${absolutePath}:${body.line}:${body.column}`;
1017
- serverLogger.debug(`IDE_OPEN: Bypassing launchIDE, using URI scheme directly: ${uri}`);
1018
- try {
1019
- if (process.platform === "darwin") {
1020
- (0, import_node_child_process.execFileSync)("open", [uri]);
1021
- } else if (process.platform === "win32") {
1022
- (0, import_node_child_process.execFileSync)("cmd", ["/c", "start", '""', uri]);
1023
- } else {
1024
- (0, import_node_child_process.execFileSync)("xdg-open", [uri]);
1025
- }
1026
- } catch (e) {
1027
- serverLogger.error(`Failed to launch URI for IDE_OPEN (${uri}):`, e);
1028
- (0, import_launch_ide.launchIDE)({
1029
- file: absolutePath,
1030
- line: body.line,
1031
- column: body.column,
1032
- editor: editorHint,
1033
- type: process.platform === "darwin" ? "open" : "exec"
1034
- });
1035
- }
1036
- } else {
1037
- (0, import_launch_ide.launchIDE)({
1038
- file: absolutePath,
1039
- line: body.line,
1040
- column: body.column,
1041
- editor: editorHint,
1042
- type: process.platform === "darwin" ? "open" : "exec"
1043
- });
1044
- }
1045
1311
  res.writeHead(200, { "Content-Type": "application/json" });
1046
1312
  res.end(JSON.stringify({ success: true }));
1047
1313
  return;
@@ -1052,10 +1318,11 @@ async function handleRequest(url, req, res) {
1052
1318
  const column = parseInt(url.searchParams.get("column") ?? "1", 10);
1053
1319
  const maxLines = parseInt(url.searchParams.get("maxLines") ?? "100", 10);
1054
1320
  try {
1055
- const absolutePath = import_node_path5.default.isAbsolute(file) ? import_node_path5.default.resolve(file) : import_node_path5.default.resolve(serverState.cwd, file);
1056
- const relativeToRoot = import_node_path5.default.relative(serverState.projectRoot, absolutePath);
1057
- if (relativeToRoot.startsWith("..") || import_node_path5.default.isAbsolute(relativeToRoot)) {
1058
- serverLogger.warn(`Security: Blocked path traversal attempt in PROJECT_SNIPPET: ${file}`);
1321
+ const absolutePath = resolveWorkspacePath(file, serverState.cwd);
1322
+ try {
1323
+ assertPathWithinProject(absolutePath, serverState.projectRoot);
1324
+ } catch {
1325
+ serverLogger4.warn(`Security: Blocked path traversal attempt in PROJECT_SNIPPET: ${file}`);
1059
1326
  res.writeHead(403, { "Content-Type": "application/json" });
1060
1327
  res.end(
1061
1328
  JSON.stringify({
@@ -1085,7 +1352,23 @@ async function handleRequest(url, req, res) {
1085
1352
  res.writeHead(result.success ? 200 : 500, { "Content-Type": "application/json" });
1086
1353
  res.end(JSON.stringify(result));
1087
1354
  } catch (e) {
1088
- serverLogger.error(`Error parsing ${import_types2.INSPECTO_API_PATHS.AI_DISPATCH} request:`, e);
1355
+ serverLogger4.error(`Error parsing ${import_types2.INSPECTO_API_PATHS.AI_DISPATCH} request:`, e);
1356
+ res.writeHead(500, { "Content-Type": "application/json" });
1357
+ res.end(JSON.stringify({ success: false, error: String(e), errorCode: "INTERNAL_ERROR" }));
1358
+ }
1359
+ return;
1360
+ }
1361
+ if (pathname === import_types2.INSPECTO_API_PATHS.AI_BATCH_DISPATCH && req.method === "POST") {
1362
+ try {
1363
+ const rawBody = await readBody(req);
1364
+ const body = JSON.parse(rawBody);
1365
+ const result = await dispatchAnnotationsToAi(body, serverState);
1366
+ res.writeHead(getBatchDispatchStatusCode(result.errorCode, result.success), {
1367
+ "Content-Type": "application/json"
1368
+ });
1369
+ res.end(JSON.stringify(result));
1370
+ } catch (e) {
1371
+ serverLogger4.error(`Error parsing ${import_types2.INSPECTO_API_PATHS.AI_BATCH_DISPATCH} request:`, e);
1089
1372
  res.writeHead(500, { "Content-Type": "application/json" });
1090
1373
  res.end(JSON.stringify({ success: false, error: String(e), errorCode: "INTERNAL_ERROR" }));
1091
1374
  }
@@ -1093,7 +1376,7 @@ async function handleRequest(url, req, res) {
1093
1376
  }
1094
1377
  if (pathname.startsWith(`${import_types2.INSPECTO_API_PATHS.AI_TICKET}/`) && req.method === "GET") {
1095
1378
  const ticketId = pathname.substring(import_types2.INSPECTO_API_PATHS.AI_TICKET.length + 1);
1096
- const payloadStr = payloadTickets.get(ticketId);
1379
+ const payloadStr = readTicket(ticketId);
1097
1380
  if (!payloadStr) {
1098
1381
  res.writeHead(404, { "Content-Type": "application/json" });
1099
1382
  res.end(JSON.stringify({ success: false, error: "Ticket not found or expired" }));
@@ -1107,54 +1390,28 @@ async function handleRequest(url, req, res) {
1107
1390
  res.end(JSON.stringify({ error: "not found" }));
1108
1391
  }
1109
1392
  async function dispatchToAi(req) {
1110
- const { location, snippet, prompt } = req;
1111
- const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
1112
- const resolvedTarget = resolveTargetTool(userConfig);
1393
+ const { location, snippet, prompt, screenshotContext } = req;
1113
1394
  const formattedPrompt = prompt ?? `Please help me with this code from \`${location.file}\` (line ${location.line}):
1114
1395
 
1115
1396
  \`\`\`
1116
1397
  ${snippet}
1117
1398
  \`\`\`
1118
1399
  `;
1119
- const ideReportedMode = serverState.ideInfo?.providers[resolvedTarget]?.mode;
1120
- const configuredIde = userConfig.ide;
1121
- const activeIde = serverState.ideInfo?.ide;
1122
- const activeIdeScheme = serverState.ideInfo?.scheme;
1123
- const finalIde = configuredIde || activeIdeScheme || activeIde || "vscode";
1124
- if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
1125
- serverLogger.warn(
1126
- `dispatchToAi: Active IDE is ${activeIdeScheme}, but config forces ${configuredIde}. Using configured IDE.`
1127
- );
1128
- }
1129
- const mode = resolveProviderMode(resolvedTarget, finalIde, userConfig);
1130
- const overrides = extractToolOverrides(finalIde, userConfig)[resolvedTarget] || {};
1131
- overrides.type = mode;
1132
- const fullPayload = {
1133
- ide: finalIde,
1134
- target: resolvedTarget,
1135
- targetType: mode,
1400
+ const runtime = resolvePromptDispatchRuntime(serverState);
1401
+ return dispatchPromptThroughIde(runtime, {
1136
1402
  prompt: formattedPrompt,
1137
1403
  filePath: location.file,
1138
1404
  line: location.line,
1139
1405
  column: location.column,
1140
1406
  snippet,
1141
- overrides: Object.keys(overrides).length > 0 ? overrides : void 0,
1142
- autoSend: userConfig["prompt.autoSend"] !== void 0 ? Boolean(userConfig["prompt.autoSend"]) : void 0
1143
- };
1144
- const ticketId = createTicket(fullPayload);
1145
- const params = new URLSearchParams();
1146
- params.set("ticket", ticketId);
1147
- params.set("target", resolvedTarget);
1148
- const uri = `${finalIde}://inspecto.inspecto/send?${params.toString()}`;
1149
- serverLogger.debug(`dispatchToAi: Generated URI: ${uri}`);
1150
- launchURI(uri);
1151
- return {
1152
- success: true,
1153
- fallbackPayload: {
1154
- prompt: formattedPrompt,
1155
- file: location.file
1156
- }
1157
- };
1407
+ ...screenshotContext ? { screenshotContext } : {}
1408
+ });
1409
+ }
1410
+ function getBatchDispatchStatusCode(errorCode, success) {
1411
+ if (success) return 200;
1412
+ if (errorCode === "INVALID_REQUEST") return 400;
1413
+ if (errorCode === "FORBIDDEN_PATH") return 403;
1414
+ return 500;
1158
1415
  }
1159
1416
 
1160
1417
  // src/injectors/utils.ts