@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/vite.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,21 +776,363 @@ 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()
779
+ function readTicket(ticketId) {
780
+ return payloadTickets.get(ticketId);
781
+ }
782
+ function launchURI(uri) {
783
+ try {
784
+ if (process.platform === "darwin") {
785
+ (0, import_node_child_process.execFileSync)("open", [uri]);
786
+ } else if (process.platform === "win32") {
787
+ (0, import_node_child_process.execFileSync)("cmd", ["/c", "start", '""', uri]);
788
+ } else {
789
+ (0, import_node_child_process.execFileSync)("xdg-open", [uri]);
790
+ }
791
+ } catch (e) {
792
+ serverLogger.error("Failed to launch URI via execFileSync, falling back to launchIDE:", e);
793
+ (0, import_launch_ide.launchIDE)({ file: uri });
794
+ }
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
+ }
790
873
  };
791
- var serverInstance = null;
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() });
792
1129
  function resolveProjectRoot() {
793
1130
  const cwd = process.cwd();
794
1131
  let gitRoot;
795
1132
  try {
796
- gitRoot = (0, import_node_child_process.execSync)("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
1133
+ gitRoot = (0, import_node_child_process3.execSync)("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
797
1134
  } catch (e) {
798
- serverLogger.warn("Failed to resolve git root via git rev-parse:", e);
1135
+ serverLogger3.warn("Failed to resolve git root via git rev-parse:", e);
799
1136
  gitRoot = cwd;
800
1137
  }
801
1138
  const visited = /* @__PURE__ */ new Set();
@@ -803,34 +1140,31 @@ function resolveProjectRoot() {
803
1140
  let current = start;
804
1141
  while (!visited.has(current)) {
805
1142
  visited.add(current);
806
- if (import_node_fs2.default.existsSync(import_node_path5.default.join(current, ".inspecto"))) return current;
1143
+ if (import_node_fs2.default.existsSync(import_node_path6.default.join(current, ".inspecto"))) return current;
807
1144
  if (current === stop) break;
808
- const parent = import_node_path5.default.dirname(current);
1145
+ const parent = import_node_path6.default.dirname(current);
809
1146
  if (parent === current) break;
810
1147
  current = parent;
811
1148
  }
812
1149
  return null;
813
1150
  };
814
- const cwdMatch = search(cwd, import_node_path5.default.parse(cwd).root);
1151
+ const cwdMatch = search(cwd, import_node_path6.default.parse(cwd).root);
815
1152
  if (cwdMatch) return cwdMatch;
816
- const repoMatch = search(gitRoot, import_node_path5.default.parse(gitRoot).root);
1153
+ const repoMatch = search(gitRoot, import_node_path6.default.parse(gitRoot).root);
817
1154
  if (repoMatch) return repoMatch;
818
1155
  return gitRoot;
819
1156
  }
820
- function launchURI(uri) {
821
- try {
822
- if (process.platform === "darwin") {
823
- (0, import_node_child_process.execFileSync)("open", [uri]);
824
- } else if (process.platform === "win32") {
825
- (0, import_node_child_process.execFileSync)("cmd", ["/c", "start", '""', uri]);
826
- } else {
827
- (0, import_node_child_process.execFileSync)("xdg-open", [uri]);
828
- }
829
- } catch (e) {
830
- serverLogger.error("Failed to launch URI via execFileSync, falling back to launchIDE:", e);
831
- (0, import_launch_ide.launchIDE)({ file: uri });
832
- }
833
- }
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;
834
1168
  async function startServer() {
835
1169
  if (serverState.running && serverState.port !== null) {
836
1170
  return serverState.port;
@@ -842,7 +1176,7 @@ async function startServer() {
842
1176
  const port = await import_portfinder.default.getPortPromise();
843
1177
  watchConfig(
844
1178
  () => {
845
- serverLogger.info("user config reloaded.");
1179
+ serverLogger4.info("user config reloaded.");
846
1180
  },
847
1181
  serverState.cwd,
848
1182
  serverState.configRoot
@@ -858,7 +1192,7 @@ async function startServer() {
858
1192
  }
859
1193
  const url = new URL(req.url ?? "/", `http://localhost:${port}`);
860
1194
  handleRequest(url, req, res).catch((err) => {
861
- serverLogger.error("server error:", err);
1195
+ serverLogger4.error("server error:", err);
862
1196
  res.writeHead(500, { "Content-Type": "application/json" });
863
1197
  res.end(JSON.stringify({ success: false, error: String(err) }));
864
1198
  });
@@ -871,41 +1205,41 @@ async function startServer() {
871
1205
  serverInstance.once("error", reject);
872
1206
  });
873
1207
  serverInstance.on("error", (err) => {
874
- serverLogger.error("persistent server error:", err);
1208
+ serverLogger4.error("persistent server error:", err);
875
1209
  });
876
1210
  serverState.port = port;
877
1211
  serverState.running = true;
878
- 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");
879
1213
  try {
880
1214
  let portData = {};
881
- if (import_node_fs2.default.existsSync(portFile)) {
1215
+ if (import_node_fs3.default.existsSync(portFile)) {
882
1216
  try {
883
- portData = JSON.parse(import_node_fs2.default.readFileSync(portFile, "utf-8"));
1217
+ portData = JSON.parse(import_node_fs3.default.readFileSync(portFile, "utf-8"));
884
1218
  } catch (e) {
885
1219
  }
886
1220
  }
887
- 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");
888
1222
  portData[rootHash] = port;
889
- 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");
890
1224
  } catch (e) {
891
- serverLogger.warn("Failed to write port file:", e);
1225
+ serverLogger4.warn("Failed to write port file:", e);
892
1226
  }
893
1227
  process.once("exit", () => {
894
1228
  try {
895
- if (import_node_fs2.default.existsSync(portFile)) {
896
- const portData = JSON.parse(import_node_fs2.default.readFileSync(portFile, "utf-8"));
897
- 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");
898
1232
  delete portData[rootHash];
899
1233
  if (Object.keys(portData).length === 0) {
900
- import_node_fs2.default.unlinkSync(portFile);
1234
+ import_node_fs3.default.unlinkSync(portFile);
901
1235
  } else {
902
- 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");
903
1237
  }
904
1238
  }
905
1239
  } catch {
906
1240
  }
907
1241
  });
908
- serverLogger.info(`server running at http://127.0.0.1:${port}`);
1242
+ serverLogger4.info(`server running at http://127.0.0.1:${port}`);
909
1243
  return port;
910
1244
  }
911
1245
  async function readBody(req) {
@@ -924,26 +1258,7 @@ async function handleRequest(url, req, res) {
924
1258
  return;
925
1259
  }
926
1260
  if (pathname === import_types2.INSPECTO_API_PATHS.CLIENT_CONFIG && req.method === "GET") {
927
- const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
928
- const promptsConfig = await loadPromptsConfig(false, serverState.cwd, serverState.configRoot);
929
- const effectiveIde = userConfig.ide ?? "vscode";
930
- let info;
931
- if (!serverState.ideInfo) {
932
- info = {
933
- ide: effectiveIde
934
- };
935
- } else {
936
- const { scheme: _scheme, ...rest } = serverState.ideInfo;
937
- info = rest;
938
- }
939
- const config = {
940
- ...info,
941
- prompts: resolveIntents(promptsConfig),
942
- hotKeys: userConfig["inspector.hotKey"] ?? "alt",
943
- theme: userConfig["inspector.theme"] ?? "auto",
944
- includeSnippet: userConfig["prompt.includeSnippet"] ?? false,
945
- autoSend: userConfig["prompt.autoSend"] ?? false
946
- };
1261
+ const config = await buildClientConfig(serverState);
947
1262
  delete config.providers;
948
1263
  res.writeHead(200, { "Content-Type": "application/json" });
949
1264
  res.end(JSON.stringify(config));
@@ -954,23 +1269,23 @@ async function handleRequest(url, req, res) {
954
1269
  const body = JSON.parse(await readBody(req));
955
1270
  const ideWorkspace = body.workspaceRoot || "";
956
1271
  const serverProjectRoot = serverState.projectRoot || "";
957
- const normalizedIdeRoot = ideWorkspace ? import_node_path5.default.resolve(ideWorkspace) : "";
958
- const normalizedServerRoot = serverProjectRoot ? import_node_path5.default.resolve(serverProjectRoot) : "";
959
- const isSameProject = !normalizedIdeRoot || !normalizedServerRoot || normalizedIdeRoot === normalizedServerRoot || normalizedServerRoot.startsWith(normalizedIdeRoot + import_node_path5.default.sep) || normalizedIdeRoot.startsWith(normalizedServerRoot + import_node_path5.default.sep);
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);
960
1275
  if (isSameProject) {
961
1276
  serverState.ideInfo = body;
962
- serverLogger.debug(
1277
+ serverLogger4.debug(
963
1278
  `Accepted IDE info from matched workspace (ide-${body.ide} / schema-${body.scheme})`
964
1279
  );
965
1280
  } else {
966
- serverLogger.debug(
1281
+ serverLogger4.debug(
967
1282
  `Ignored IDE info from unrelated workspace (IDE Workspace: ${ideWorkspace}, Server: ${serverProjectRoot}, Scheme: ${body.scheme}, IDE: ${body.ide})`
968
1283
  );
969
1284
  }
970
1285
  res.writeHead(200, { "Content-Type": "application/json" });
971
1286
  res.end(JSON.stringify({ success: true }));
972
1287
  } catch (e) {
973
- 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);
974
1289
  res.writeHead(400, { "Content-Type": "application/json" });
975
1290
  res.end(JSON.stringify({ error: "Invalid JSON body" }));
976
1291
  }
@@ -985,79 +1300,14 @@ async function handleRequest(url, req, res) {
985
1300
  res.end(JSON.stringify({ error: "Invalid JSON body" }));
986
1301
  return;
987
1302
  }
988
- 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);
989
- const relativeToRoot = import_node_path5.default.relative(serverState.projectRoot, absolutePath);
990
- if (relativeToRoot.startsWith("..") || import_node_path5.default.isAbsolute(relativeToRoot)) {
991
- 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}`);
992
1307
  res.writeHead(403, { "Content-Type": "application/json" });
993
1308
  res.end(JSON.stringify({ error: "Access denied: File is outside of project workspace" }));
994
1309
  return;
995
1310
  }
996
- const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
997
- const configuredIde = userConfig.ide;
998
- const activeIde = serverState.ideInfo?.ide;
999
- const activeIdeScheme = serverState.ideInfo?.scheme;
1000
- const rawEditorHint = configuredIde || activeIde || activeIdeScheme || "code";
1001
- if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
1002
- serverLogger.warn(
1003
- `Active IDE is ${activeIdeScheme}, but config forces ${configuredIde}. Using configured IDE.`
1004
- );
1005
- }
1006
- let editorHint = rawEditorHint;
1007
- if (rawEditorHint === "vscode") editorHint = "code";
1008
- else if (rawEditorHint === "vscode-insiders") editorHint = "code-insiders";
1009
- else if (rawEditorHint === "vscodium") editorHint = "codium";
1010
- else if (rawEditorHint === "trae-cn" || rawEditorHint === "trae") editorHint = "trae";
1011
- serverLogger.debug(
1012
- `IDE_OPEN: activeIde=${activeIde}, activeIdeScheme=${activeIdeScheme}, configuredIde=${configuredIde} -> rawEditorHint=${rawEditorHint}, finalEditorHint=${editorHint}`
1013
- );
1014
- const VSCODE_FAMILY_SCHEMES = [
1015
- "vscode",
1016
- "vscode-insiders",
1017
- "cursor",
1018
- "windsurf",
1019
- "trae",
1020
- "trae-cn",
1021
- "vscodium",
1022
- "codebuddy",
1023
- "codebuddy-cn",
1024
- "antigravity"
1025
- ];
1026
- if (VSCODE_FAMILY_SCHEMES.includes(rawEditorHint)) {
1027
- let normalizedPath = absolutePath.replace(/\\/g, "/");
1028
- if (!normalizedPath.startsWith("/")) {
1029
- normalizedPath = "/" + normalizedPath;
1030
- }
1031
- const encodedPath = encodeURI(normalizedPath);
1032
- const uri = `${rawEditorHint}://file${encodedPath}:${body.line}:${body.column}`;
1033
- serverLogger.debug(`IDE_OPEN: Bypassing launchIDE, using URI scheme directly: ${uri}`);
1034
- try {
1035
- if (process.platform === "darwin") {
1036
- (0, import_node_child_process.execFileSync)("open", [uri]);
1037
- } else if (process.platform === "win32") {
1038
- (0, import_node_child_process.execFileSync)("cmd", ["/c", "start", '""', uri]);
1039
- } else {
1040
- (0, import_node_child_process.execFileSync)("xdg-open", [uri]);
1041
- }
1042
- } catch (e) {
1043
- serverLogger.error(`Failed to launch URI for IDE_OPEN (${uri}):`, e);
1044
- (0, import_launch_ide.launchIDE)({
1045
- file: absolutePath,
1046
- line: body.line,
1047
- column: body.column,
1048
- editor: editorHint,
1049
- type: process.platform === "darwin" ? "open" : "exec"
1050
- });
1051
- }
1052
- } else {
1053
- (0, import_launch_ide.launchIDE)({
1054
- file: absolutePath,
1055
- line: body.line,
1056
- column: body.column,
1057
- editor: editorHint,
1058
- type: process.platform === "darwin" ? "open" : "exec"
1059
- });
1060
- }
1061
1311
  res.writeHead(200, { "Content-Type": "application/json" });
1062
1312
  res.end(JSON.stringify({ success: true }));
1063
1313
  return;
@@ -1068,10 +1318,11 @@ async function handleRequest(url, req, res) {
1068
1318
  const column = parseInt(url.searchParams.get("column") ?? "1", 10);
1069
1319
  const maxLines = parseInt(url.searchParams.get("maxLines") ?? "100", 10);
1070
1320
  try {
1071
- const absolutePath = import_node_path5.default.isAbsolute(file) ? import_node_path5.default.resolve(file) : import_node_path5.default.resolve(serverState.cwd, file);
1072
- const relativeToRoot = import_node_path5.default.relative(serverState.projectRoot, absolutePath);
1073
- if (relativeToRoot.startsWith("..") || import_node_path5.default.isAbsolute(relativeToRoot)) {
1074
- 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}`);
1075
1326
  res.writeHead(403, { "Content-Type": "application/json" });
1076
1327
  res.end(
1077
1328
  JSON.stringify({
@@ -1101,7 +1352,23 @@ async function handleRequest(url, req, res) {
1101
1352
  res.writeHead(result.success ? 200 : 500, { "Content-Type": "application/json" });
1102
1353
  res.end(JSON.stringify(result));
1103
1354
  } catch (e) {
1104
- 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);
1105
1372
  res.writeHead(500, { "Content-Type": "application/json" });
1106
1373
  res.end(JSON.stringify({ success: false, error: String(e), errorCode: "INTERNAL_ERROR" }));
1107
1374
  }
@@ -1109,7 +1376,7 @@ async function handleRequest(url, req, res) {
1109
1376
  }
1110
1377
  if (pathname.startsWith(`${import_types2.INSPECTO_API_PATHS.AI_TICKET}/`) && req.method === "GET") {
1111
1378
  const ticketId = pathname.substring(import_types2.INSPECTO_API_PATHS.AI_TICKET.length + 1);
1112
- const payloadStr = payloadTickets.get(ticketId);
1379
+ const payloadStr = readTicket(ticketId);
1113
1380
  if (!payloadStr) {
1114
1381
  res.writeHead(404, { "Content-Type": "application/json" });
1115
1382
  res.end(JSON.stringify({ success: false, error: "Ticket not found or expired" }));
@@ -1123,54 +1390,28 @@ async function handleRequest(url, req, res) {
1123
1390
  res.end(JSON.stringify({ error: "not found" }));
1124
1391
  }
1125
1392
  async function dispatchToAi(req) {
1126
- const { location, snippet, prompt } = req;
1127
- const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
1128
- const resolvedTarget = resolveTargetTool(userConfig);
1393
+ const { location, snippet, prompt, screenshotContext } = req;
1129
1394
  const formattedPrompt = prompt ?? `Please help me with this code from \`${location.file}\` (line ${location.line}):
1130
1395
 
1131
1396
  \`\`\`
1132
1397
  ${snippet}
1133
1398
  \`\`\`
1134
1399
  `;
1135
- const ideReportedMode = serverState.ideInfo?.providers[resolvedTarget]?.mode;
1136
- const configuredIde = userConfig.ide;
1137
- const activeIde = serverState.ideInfo?.ide;
1138
- const activeIdeScheme = serverState.ideInfo?.scheme;
1139
- const finalIde = configuredIde || activeIdeScheme || activeIde || "vscode";
1140
- if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
1141
- serverLogger.warn(
1142
- `dispatchToAi: Active IDE is ${activeIdeScheme}, but config forces ${configuredIde}. Using configured IDE.`
1143
- );
1144
- }
1145
- const mode = resolveProviderMode(resolvedTarget, finalIde, userConfig);
1146
- const overrides = extractToolOverrides(finalIde, userConfig)[resolvedTarget] || {};
1147
- overrides.type = mode;
1148
- const fullPayload = {
1149
- ide: finalIde,
1150
- target: resolvedTarget,
1151
- targetType: mode,
1400
+ const runtime = resolvePromptDispatchRuntime(serverState);
1401
+ return dispatchPromptThroughIde(runtime, {
1152
1402
  prompt: formattedPrompt,
1153
1403
  filePath: location.file,
1154
1404
  line: location.line,
1155
1405
  column: location.column,
1156
1406
  snippet,
1157
- overrides: Object.keys(overrides).length > 0 ? overrides : void 0,
1158
- autoSend: userConfig["prompt.autoSend"] !== void 0 ? Boolean(userConfig["prompt.autoSend"]) : void 0
1159
- };
1160
- const ticketId = createTicket(fullPayload);
1161
- const params = new URLSearchParams();
1162
- params.set("ticket", ticketId);
1163
- params.set("target", resolvedTarget);
1164
- const uri = `${finalIde}://inspecto.inspecto/send?${params.toString()}`;
1165
- serverLogger.debug(`dispatchToAi: Generated URI: ${uri}`);
1166
- launchURI(uri);
1167
- return {
1168
- success: true,
1169
- fallbackPayload: {
1170
- prompt: formattedPrompt,
1171
- file: location.file
1172
- }
1173
- };
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;
1174
1415
  }
1175
1416
 
1176
1417
  // src/injectors/utils.ts