@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/vite.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,32 +740,8 @@ 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()
754
- };
755
- var serverInstance = null;
756
- function resolveProjectRoot() {
757
- let gitRoot;
758
- try {
759
- serverLogger.info("Resolving project root...");
760
- gitRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
761
- serverLogger.info("Resolved project root: " + gitRoot);
762
- } catch (e) {
763
- serverLogger.error("Failed to resolve project root:", e);
764
- gitRoot = process.cwd();
765
- }
766
- let current = gitRoot;
767
- while (true) {
768
- if (fs3.existsSync(path6.join(current, ".inspecto"))) return current;
769
- const parent = path6.dirname(current);
770
- if (parent === current) break;
771
- current = parent;
772
- }
773
- return gitRoot;
743
+ function readTicket(ticketId) {
744
+ return payloadTickets.get(ticketId);
774
745
  }
775
746
  function launchURI(uri) {
776
747
  try {
@@ -786,6 +757,378 @@ function launchURI(uri) {
786
757
  launchIDE({ file: uri });
787
758
  }
788
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
+ }
837
+ };
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() });
1093
+ function resolveProjectRoot() {
1094
+ const cwd = process.cwd();
1095
+ let gitRoot;
1096
+ try {
1097
+ gitRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
1098
+ } catch (e) {
1099
+ serverLogger3.warn("Failed to resolve git root via git rev-parse:", e);
1100
+ gitRoot = cwd;
1101
+ }
1102
+ const visited = /* @__PURE__ */ new Set();
1103
+ const search = (start, stop) => {
1104
+ let current = start;
1105
+ while (!visited.has(current)) {
1106
+ visited.add(current);
1107
+ if (fs3.existsSync(path7.join(current, ".inspecto"))) return current;
1108
+ if (current === stop) break;
1109
+ const parent = path7.dirname(current);
1110
+ if (parent === current) break;
1111
+ current = parent;
1112
+ }
1113
+ return null;
1114
+ };
1115
+ const cwdMatch = search(cwd, path7.parse(cwd).root);
1116
+ if (cwdMatch) return cwdMatch;
1117
+ const repoMatch = search(gitRoot, path7.parse(gitRoot).root);
1118
+ if (repoMatch) return repoMatch;
1119
+ return gitRoot;
1120
+ }
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;
789
1132
  async function startServer() {
790
1133
  if (serverState.running && serverState.port !== null) {
791
1134
  return serverState.port;
@@ -797,7 +1140,7 @@ async function startServer() {
797
1140
  const port = await portfinder.getPortPromise();
798
1141
  watchConfig(
799
1142
  () => {
800
- serverLogger.info("user config reloaded.");
1143
+ serverLogger4.info("user config reloaded.");
801
1144
  },
802
1145
  serverState.cwd,
803
1146
  serverState.configRoot
@@ -813,7 +1156,7 @@ async function startServer() {
813
1156
  }
814
1157
  const url = new URL(req.url ?? "/", `http://localhost:${port}`);
815
1158
  handleRequest(url, req, res).catch((err) => {
816
- serverLogger.error("server error:", err);
1159
+ serverLogger4.error("server error:", err);
817
1160
  res.writeHead(500, { "Content-Type": "application/json" });
818
1161
  res.end(JSON.stringify({ success: false, error: String(err) }));
819
1162
  });
@@ -826,41 +1169,41 @@ async function startServer() {
826
1169
  serverInstance.once("error", reject);
827
1170
  });
828
1171
  serverInstance.on("error", (err) => {
829
- serverLogger.error("persistent server error:", err);
1172
+ serverLogger4.error("persistent server error:", err);
830
1173
  });
831
1174
  serverState.port = port;
832
1175
  serverState.running = true;
833
- const portFile = path6.join(os2.tmpdir(), "inspecto.port.json");
1176
+ const portFile = path8.join(os2.tmpdir(), "inspecto.port.json");
834
1177
  try {
835
1178
  let portData = {};
836
- if (fs3.existsSync(portFile)) {
1179
+ if (fs4.existsSync(portFile)) {
837
1180
  try {
838
- portData = JSON.parse(fs3.readFileSync(portFile, "utf-8"));
1181
+ portData = JSON.parse(fs4.readFileSync(portFile, "utf-8"));
839
1182
  } catch (e) {
840
1183
  }
841
1184
  }
842
- const rootHash = crypto.createHash("md5").update(serverState.projectRoot).digest("hex");
1185
+ const rootHash = crypto2.createHash("md5").update(serverState.projectRoot).digest("hex");
843
1186
  portData[rootHash] = port;
844
- fs3.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
1187
+ fs4.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
845
1188
  } catch (e) {
846
- serverLogger.warn("Failed to write port file:", e);
1189
+ serverLogger4.warn("Failed to write port file:", e);
847
1190
  }
848
1191
  process.once("exit", () => {
849
1192
  try {
850
- if (fs3.existsSync(portFile)) {
851
- const portData = JSON.parse(fs3.readFileSync(portFile, "utf-8"));
852
- const rootHash = crypto.createHash("md5").update(serverState.projectRoot).digest("hex");
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");
853
1196
  delete portData[rootHash];
854
1197
  if (Object.keys(portData).length === 0) {
855
- fs3.unlinkSync(portFile);
1198
+ fs4.unlinkSync(portFile);
856
1199
  } else {
857
- fs3.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
1200
+ fs4.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
858
1201
  }
859
1202
  }
860
1203
  } catch {
861
1204
  }
862
1205
  });
863
- serverLogger.info(`server running at http://127.0.0.1:${port}`);
1206
+ serverLogger4.info(`server running at http://127.0.0.1:${port}`);
864
1207
  return port;
865
1208
  }
866
1209
  async function readBody(req) {
@@ -879,26 +1222,7 @@ async function handleRequest(url, req, res) {
879
1222
  return;
880
1223
  }
881
1224
  if (pathname === INSPECTO_API_PATHS.CLIENT_CONFIG && req.method === "GET") {
882
- const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
883
- const promptsConfig = await loadPromptsConfig(false, serverState.cwd, serverState.configRoot);
884
- const effectiveIde = userConfig.ide ?? "vscode";
885
- let info;
886
- if (!serverState.ideInfo) {
887
- info = {
888
- ide: effectiveIde
889
- };
890
- } else {
891
- const { scheme: _scheme, ...rest } = serverState.ideInfo;
892
- info = rest;
893
- }
894
- const config = {
895
- ...info,
896
- prompts: resolveIntents(promptsConfig),
897
- hotKeys: userConfig["inspector.hotKey"] ?? "alt",
898
- theme: userConfig["inspector.theme"] ?? "auto",
899
- includeSnippet: userConfig["prompt.includeSnippet"] ?? false,
900
- autoSend: userConfig["prompt.autoSend"] ?? false
901
- };
1225
+ const config = await buildClientConfig(serverState);
902
1226
  delete config.providers;
903
1227
  res.writeHead(200, { "Content-Type": "application/json" });
904
1228
  res.end(JSON.stringify(config));
@@ -909,21 +1233,23 @@ async function handleRequest(url, req, res) {
909
1233
  const body = JSON.parse(await readBody(req));
910
1234
  const ideWorkspace = body.workspaceRoot || "";
911
1235
  const serverProjectRoot = serverState.projectRoot || "";
912
- const isSameProject = !ideWorkspace || !serverProjectRoot || ideWorkspace === serverProjectRoot || serverProjectRoot.startsWith(ideWorkspace);
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);
913
1239
  if (isSameProject) {
914
1240
  serverState.ideInfo = body;
915
- serverLogger.debug(
1241
+ serverLogger4.debug(
916
1242
  `Accepted IDE info from matched workspace (ide-${body.ide} / schema-${body.scheme})`
917
1243
  );
918
1244
  } else {
919
- serverLogger.debug(
1245
+ serverLogger4.debug(
920
1246
  `Ignored IDE info from unrelated workspace (IDE Workspace: ${ideWorkspace}, Server: ${serverProjectRoot}, Scheme: ${body.scheme}, IDE: ${body.ide})`
921
1247
  );
922
1248
  }
923
1249
  res.writeHead(200, { "Content-Type": "application/json" });
924
1250
  res.end(JSON.stringify({ success: true }));
925
1251
  } catch (e) {
926
- 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);
927
1253
  res.writeHead(400, { "Content-Type": "application/json" });
928
1254
  res.end(JSON.stringify({ error: "Invalid JSON body" }));
929
1255
  }
@@ -938,74 +1264,14 @@ async function handleRequest(url, req, res) {
938
1264
  res.end(JSON.stringify({ error: "Invalid JSON body" }));
939
1265
  return;
940
1266
  }
941
- const absolutePath = path6.isAbsolute(body.file) ? path6.resolve(body.file) : path6.resolve(serverState.cwd, body.file);
942
- const relativeToRoot = path6.relative(serverState.projectRoot, absolutePath);
943
- if (relativeToRoot.startsWith("..") || path6.isAbsolute(relativeToRoot)) {
944
- serverLogger.warn(`Security: Blocked path traversal attempt in IDE_OPEN: ${body.file}`);
1267
+ try {
1268
+ handleOpenFileRequest(body, serverState);
1269
+ } catch {
1270
+ serverLogger4.warn(`Security: Blocked path traversal attempt in IDE_OPEN: ${body.file}`);
945
1271
  res.writeHead(403, { "Content-Type": "application/json" });
946
1272
  res.end(JSON.stringify({ error: "Access denied: File is outside of project workspace" }));
947
1273
  return;
948
1274
  }
949
- const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
950
- const configuredIde = userConfig.ide;
951
- const activeIde = serverState.ideInfo?.ide;
952
- const activeIdeScheme = serverState.ideInfo?.scheme;
953
- const rawEditorHint = configuredIde || activeIde || activeIdeScheme || "code";
954
- if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
955
- serverLogger.warn(
956
- `Active IDE is ${activeIdeScheme}, but config forces ${configuredIde}. Using configured IDE.`
957
- );
958
- }
959
- let editorHint = rawEditorHint;
960
- if (rawEditorHint === "vscode") editorHint = "code";
961
- else if (rawEditorHint === "vscode-insiders") editorHint = "code-insiders";
962
- else if (rawEditorHint === "vscodium") editorHint = "codium";
963
- else if (rawEditorHint === "trae-cn" || rawEditorHint === "trae") editorHint = "trae";
964
- serverLogger.debug(
965
- `IDE_OPEN: activeIde=${activeIde}, activeIdeScheme=${activeIdeScheme}, configuredIde=${configuredIde} -> rawEditorHint=${rawEditorHint}, finalEditorHint=${editorHint}`
966
- );
967
- const VSCODE_FAMILY_SCHEMES = [
968
- "vscode",
969
- "vscode-insiders",
970
- "cursor",
971
- "windsurf",
972
- "trae",
973
- "trae-cn",
974
- "vscodium",
975
- "codebuddy",
976
- "codebuddy-cn",
977
- "antigravity"
978
- ];
979
- if (VSCODE_FAMILY_SCHEMES.includes(rawEditorHint)) {
980
- const uri = `${rawEditorHint}://file${absolutePath}:${body.line}:${body.column}`;
981
- serverLogger.debug(`IDE_OPEN: Bypassing launchIDE, using URI scheme directly: ${uri}`);
982
- try {
983
- if (process.platform === "darwin") {
984
- execFileSync("open", [uri]);
985
- } else if (process.platform === "win32") {
986
- execFileSync("cmd", ["/c", "start", '""', uri]);
987
- } else {
988
- execFileSync("xdg-open", [uri]);
989
- }
990
- } catch (e) {
991
- serverLogger.error(`Failed to launch URI for IDE_OPEN (${uri}):`, e);
992
- launchIDE({
993
- file: absolutePath,
994
- line: body.line,
995
- column: body.column,
996
- editor: editorHint,
997
- type: process.platform === "darwin" ? "open" : "exec"
998
- });
999
- }
1000
- } else {
1001
- launchIDE({
1002
- file: absolutePath,
1003
- line: body.line,
1004
- column: body.column,
1005
- editor: editorHint,
1006
- type: process.platform === "darwin" ? "open" : "exec"
1007
- });
1008
- }
1009
1275
  res.writeHead(200, { "Content-Type": "application/json" });
1010
1276
  res.end(JSON.stringify({ success: true }));
1011
1277
  return;
@@ -1016,10 +1282,11 @@ async function handleRequest(url, req, res) {
1016
1282
  const column = parseInt(url.searchParams.get("column") ?? "1", 10);
1017
1283
  const maxLines = parseInt(url.searchParams.get("maxLines") ?? "100", 10);
1018
1284
  try {
1019
- const absolutePath = path6.isAbsolute(file) ? path6.resolve(file) : path6.resolve(serverState.cwd, file);
1020
- const relativeToRoot = path6.relative(serverState.projectRoot, absolutePath);
1021
- if (relativeToRoot.startsWith("..") || path6.isAbsolute(relativeToRoot)) {
1022
- serverLogger.warn(`Security: Blocked path traversal attempt in PROJECT_SNIPPET: ${file}`);
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}`);
1023
1290
  res.writeHead(403, { "Content-Type": "application/json" });
1024
1291
  res.end(
1025
1292
  JSON.stringify({
@@ -1049,7 +1316,23 @@ async function handleRequest(url, req, res) {
1049
1316
  res.writeHead(result.success ? 200 : 500, { "Content-Type": "application/json" });
1050
1317
  res.end(JSON.stringify(result));
1051
1318
  } catch (e) {
1052
- 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);
1053
1336
  res.writeHead(500, { "Content-Type": "application/json" });
1054
1337
  res.end(JSON.stringify({ success: false, error: String(e), errorCode: "INTERNAL_ERROR" }));
1055
1338
  }
@@ -1057,7 +1340,7 @@ async function handleRequest(url, req, res) {
1057
1340
  }
1058
1341
  if (pathname.startsWith(`${INSPECTO_API_PATHS.AI_TICKET}/`) && req.method === "GET") {
1059
1342
  const ticketId = pathname.substring(INSPECTO_API_PATHS.AI_TICKET.length + 1);
1060
- const payloadStr = payloadTickets.get(ticketId);
1343
+ const payloadStr = readTicket(ticketId);
1061
1344
  if (!payloadStr) {
1062
1345
  res.writeHead(404, { "Content-Type": "application/json" });
1063
1346
  res.end(JSON.stringify({ success: false, error: "Ticket not found or expired" }));
@@ -1071,54 +1354,28 @@ async function handleRequest(url, req, res) {
1071
1354
  res.end(JSON.stringify({ error: "not found" }));
1072
1355
  }
1073
1356
  async function dispatchToAi(req) {
1074
- const { location, snippet, prompt } = req;
1075
- const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
1076
- const resolvedTarget = resolveTargetTool(userConfig);
1357
+ const { location, snippet, prompt, screenshotContext } = req;
1077
1358
  const formattedPrompt = prompt ?? `Please help me with this code from \`${location.file}\` (line ${location.line}):
1078
1359
 
1079
1360
  \`\`\`
1080
1361
  ${snippet}
1081
1362
  \`\`\`
1082
1363
  `;
1083
- const ideReportedMode = serverState.ideInfo?.providers[resolvedTarget]?.mode;
1084
- const configuredIde = userConfig.ide;
1085
- const activeIde = serverState.ideInfo?.ide;
1086
- const activeIdeScheme = serverState.ideInfo?.scheme;
1087
- const finalIde = configuredIde || activeIdeScheme || activeIde || "vscode";
1088
- if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
1089
- serverLogger.warn(
1090
- `dispatchToAi: Active IDE is ${activeIdeScheme}, but config forces ${configuredIde}. Using configured IDE.`
1091
- );
1092
- }
1093
- const mode = resolveProviderMode(resolvedTarget, finalIde, userConfig);
1094
- const overrides = extractToolOverrides(finalIde, userConfig)[resolvedTarget] || {};
1095
- overrides.type = mode;
1096
- const fullPayload = {
1097
- ide: finalIde,
1098
- target: resolvedTarget,
1099
- targetType: mode,
1364
+ const runtime = resolvePromptDispatchRuntime(serverState);
1365
+ return dispatchPromptThroughIde(runtime, {
1100
1366
  prompt: formattedPrompt,
1101
1367
  filePath: location.file,
1102
1368
  line: location.line,
1103
1369
  column: location.column,
1104
1370
  snippet,
1105
- overrides: Object.keys(overrides).length > 0 ? overrides : void 0,
1106
- autoSend: userConfig["prompt.autoSend"] !== void 0 ? Boolean(userConfig["prompt.autoSend"]) : void 0
1107
- };
1108
- const ticketId = createTicket(fullPayload);
1109
- const params = new URLSearchParams();
1110
- params.set("ticket", ticketId);
1111
- params.set("target", resolvedTarget);
1112
- const uri = `${finalIde}://inspecto.inspecto/send?${params.toString()}`;
1113
- serverLogger.debug(`dispatchToAi: Generated URI: ${uri}`);
1114
- launchURI(uri);
1115
- return {
1116
- success: true,
1117
- fallbackPayload: {
1118
- prompt: formattedPrompt,
1119
- file: location.file
1120
- }
1121
- };
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;
1122
1379
  }
1123
1380
 
1124
1381
  // src/injectors/utils.ts