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