@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.
@@ -42,13 +42,11 @@ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
42
42
 
43
43
  // src/server/index.ts
44
44
  var import_node_http = __toESM(require("http"), 1);
45
- var import_node_fs2 = __toESM(require("fs"), 1);
46
- var import_node_path2 = __toESM(require("path"), 1);
45
+ var import_node_fs3 = __toESM(require("fs"), 1);
46
+ var import_node_path4 = __toESM(require("path"), 1);
47
47
  var import_node_os2 = __toESM(require("os"), 1);
48
- var import_node_crypto = __toESM(require("crypto"), 1);
49
- var import_node_child_process = require("child_process");
48
+ var import_node_crypto2 = __toESM(require("crypto"), 1);
50
49
  var import_portfinder = __toESM(require("portfinder"), 1);
51
- var import_launch_ide = require("launch-ide");
52
50
  var import_types2 = require("@inspecto-dev/types");
53
51
 
54
52
  // src/server/snippet.ts
@@ -368,9 +366,9 @@ function extractToolOverrides(ide, config) {
368
366
  function resolveIntents(serverPrompts) {
369
367
  const baseMap = /* @__PURE__ */ new Map();
370
368
  for (const intent of import_types.DEFAULT_INTENTS) {
371
- if (intent.id) baseMap.set(intent.id, { ...intent });
369
+ baseMap.set(intent.id, { ...intent });
372
370
  }
373
- const defaults = () => ensureOpenInEditorLast(Array.from(baseMap.values()));
371
+ const defaults = () => Array.from(baseMap.values());
374
372
  if (!serverPrompts) return defaults();
375
373
  const isReplace = !Array.isArray(serverPrompts) && typeof serverPrompts === "object" && serverPrompts.$replace === true;
376
374
  const promptsArray = Array.isArray(serverPrompts) ? serverPrompts : isReplace ? serverPrompts.items : [];
@@ -397,16 +395,18 @@ function resolveIntents(serverPrompts) {
397
395
  );
398
396
  continue;
399
397
  }
400
- if (item.isAction && item.id !== "open-in-editor") {
398
+ if (!item.aiIntent) {
401
399
  configLogger.warn(
402
- `isAction is reserved for built-in actions. Ignoring intent "${item.id}".`
400
+ `Intent "${item.id}" is missing required "aiIntent".`
403
401
  );
404
402
  continue;
405
403
  }
406
- result.push(baseMap.has(item.id) ? { ...baseMap.get(item.id), ...item } : item);
404
+ result.push(
405
+ baseMap.has(item.id) ? { ...baseMap.get(item.id), ...item } : item
406
+ );
407
407
  }
408
408
  }
409
- return ensureOpenInEditorLast(result);
409
+ return result;
410
410
  }
411
411
  const merged = Array.from(baseMap.values());
412
412
  for (const item of promptsArray) {
@@ -423,9 +423,9 @@ function resolveIntents(serverPrompts) {
423
423
  configLogger.warn('Intent object missing required "id" field, skipping.');
424
424
  continue;
425
425
  }
426
- if (item.isAction && item.id !== "open-in-editor") {
426
+ if (!item.aiIntent) {
427
427
  configLogger.warn(
428
- `isAction is reserved for built-in actions. Ignoring intent "${item.id}".`
428
+ `Intent "${item.id}" is missing required "aiIntent".`
429
429
  );
430
430
  continue;
431
431
  }
@@ -443,15 +443,7 @@ function resolveIntents(serverPrompts) {
443
443
  }
444
444
  }
445
445
  }
446
- return ensureOpenInEditorLast(merged);
447
- }
448
- function ensureOpenInEditorLast(intents) {
449
- const idx = intents.findIndex((i) => i.id === "open-in-editor");
450
- if (idx === -1 || idx === intents.length - 1) return intents;
451
- const result = [...intents];
452
- const item = result.splice(idx, 1)[0];
453
- result.push(item);
454
- return result;
446
+ return merged;
455
447
  }
456
448
  var watchers = [];
457
449
  function watchConfig(onReload, cwd = process.cwd(), gitRoot) {
@@ -486,7 +478,10 @@ function watchConfig(onReload, cwd = process.cwd(), gitRoot) {
486
478
  }
487
479
  }
488
480
 
489
- // src/server/index.ts
481
+ // src/server/dispatch-transport.ts
482
+ var import_node_crypto = __toESM(require("crypto"), 1);
483
+ var import_node_child_process = require("child_process");
484
+ var import_launch_ide = require("launch-ide");
490
485
  var serverLogger = createLogger("inspecto:server", { logLevel: getGlobalLogLevel() });
491
486
  var payloadTickets = /* @__PURE__ */ new Map();
492
487
  function createTicket(payload) {
@@ -500,32 +495,8 @@ function createTicket(payload) {
500
495
  );
501
496
  return ticketId;
502
497
  }
503
- var serverState = {
504
- port: null,
505
- running: false,
506
- projectRoot: "",
507
- configRoot: "",
508
- cwd: process.cwd()
509
- };
510
- var serverInstance = null;
511
- function resolveProjectRoot() {
512
- let gitRoot;
513
- try {
514
- serverLogger.info("Resolving project root...");
515
- gitRoot = (0, import_node_child_process.execSync)("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
516
- serverLogger.info("Resolved project root: " + gitRoot);
517
- } catch (e) {
518
- serverLogger.error("Failed to resolve project root:", e);
519
- gitRoot = process.cwd();
520
- }
521
- let current = gitRoot;
522
- while (true) {
523
- if (import_node_fs2.default.existsSync(import_node_path2.default.join(current, ".inspecto"))) return current;
524
- const parent = import_node_path2.default.dirname(current);
525
- if (parent === current) break;
526
- current = parent;
527
- }
528
- return gitRoot;
498
+ function readTicket(ticketId) {
499
+ return payloadTickets.get(ticketId);
529
500
  }
530
501
  function launchURI(uri) {
531
502
  try {
@@ -541,6 +512,378 @@ function launchURI(uri) {
541
512
  (0, import_launch_ide.launchIDE)({ file: uri });
542
513
  }
543
514
  }
515
+
516
+ // src/server/dispatch-runtime.ts
517
+ function resolvePromptDispatchRuntime(state) {
518
+ const userConfig = loadUserConfigSync(false, state.cwd, state.projectRoot);
519
+ const resolvedTarget = resolveTargetTool(userConfig);
520
+ const finalIde = resolveFinalIde(userConfig.ide, state.ideInfo?.ide, state.ideInfo?.scheme);
521
+ const mode = resolveProviderMode(resolvedTarget, finalIde, userConfig);
522
+ const overrides = extractToolOverrides(finalIde, userConfig)[resolvedTarget] || void 0;
523
+ return {
524
+ resolvedTarget,
525
+ finalIde,
526
+ mode,
527
+ ...hasOverrides(overrides) ? { overrides } : {},
528
+ ...userConfig["prompt.autoSend"] !== void 0 ? { autoSend: Boolean(userConfig["prompt.autoSend"]) } : {}
529
+ };
530
+ }
531
+ function dispatchPromptThroughIde(runtime, payload) {
532
+ const ticketId = createTicket({
533
+ ide: runtime.finalIde,
534
+ target: runtime.resolvedTarget,
535
+ targetType: runtime.mode,
536
+ prompt: payload.prompt,
537
+ filePath: payload.filePath,
538
+ line: payload.line,
539
+ column: payload.column,
540
+ snippet: payload.snippet,
541
+ ...payload.screenshotContext ? { screenshotContext: payload.screenshotContext } : {},
542
+ overrides: runtime.overrides,
543
+ autoSend: runtime.autoSend
544
+ });
545
+ const params = new URLSearchParams();
546
+ params.set("ticket", ticketId);
547
+ params.set("target", runtime.resolvedTarget);
548
+ launchURI(`${runtime.finalIde}://inspecto.inspecto/send?${params.toString()}`);
549
+ return {
550
+ success: true,
551
+ fallbackPayload: {
552
+ prompt: payload.prompt,
553
+ ...payload.filePath ? { file: payload.filePath } : {}
554
+ }
555
+ };
556
+ }
557
+ function resolveFinalIde(configuredIde, activeIde, activeIdeScheme) {
558
+ if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
559
+ return configuredIde;
560
+ }
561
+ return configuredIde || activeIdeScheme || activeIde || "vscode";
562
+ }
563
+ function hasOverrides(overrides) {
564
+ return Boolean(overrides && Object.keys(overrides).length > 0);
565
+ }
566
+
567
+ // src/server/path-guards.ts
568
+ var import_node_path2 = __toESM(require("path"), 1);
569
+ function isWindowsAbsolutePath(file) {
570
+ return /^[a-zA-Z]:[\\/]/.test(file) || /^\\\\[^\\]+\\[^\\]+/.test(file);
571
+ }
572
+ function resolveWorkspacePath(file, cwd) {
573
+ if (isWindowsAbsolutePath(file)) {
574
+ return import_node_path2.default.win32.normalize(file);
575
+ }
576
+ return import_node_path2.default.isAbsolute(file) ? import_node_path2.default.resolve(file) : import_node_path2.default.resolve(cwd, file);
577
+ }
578
+ function assertPathWithinProject(file, projectRoot) {
579
+ 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);
580
+ if (relativeToRoot.startsWith("..") || import_node_path2.default.isAbsolute(relativeToRoot)) {
581
+ throw new Error("Access denied: File is outside of project workspace");
582
+ }
583
+ }
584
+
585
+ // src/server/annotation-dispatch.ts
586
+ var AnnotationDispatchError = class extends Error {
587
+ constructor(message, errorCode) {
588
+ super(message);
589
+ this.name = "AnnotationDispatchError";
590
+ this.errorCode = errorCode;
591
+ }
592
+ };
593
+ async function dispatchAnnotationsToAi(req, state) {
594
+ try {
595
+ validateAnnotationDispatchRequest(req, state);
596
+ const batch = normalizeAnnotationBatch(req);
597
+ const prompt = buildAnnotationBatchPrompt(batch);
598
+ const representativeTarget = batch.annotations[0]?.targets[0];
599
+ const runtime = resolvePromptDispatchRuntime(state);
600
+ return dispatchPromptThroughIde(runtime, {
601
+ prompt,
602
+ ...representativeTarget?.file ? { filePath: representativeTarget.file } : {},
603
+ ...representativeTarget?.line ? { line: representativeTarget.line } : {},
604
+ ...representativeTarget?.column ? { column: representativeTarget.column } : {},
605
+ ...batch.screenshotContext ? { screenshotContext: batch.screenshotContext } : {}
606
+ });
607
+ } catch (error) {
608
+ return {
609
+ success: false,
610
+ error: error instanceof Error ? error.message : String(error),
611
+ errorCode: getAnnotationDispatchErrorCode(error)
612
+ };
613
+ }
614
+ }
615
+ function validateAnnotationDispatchRequest(req, state) {
616
+ if (!req.annotations.length) {
617
+ throw new AnnotationDispatchError("At least one annotation is required.", "INVALID_REQUEST");
618
+ }
619
+ for (const annotation of req.annotations) {
620
+ if (!annotation.targets.length) {
621
+ throw new AnnotationDispatchError(
622
+ "Each annotation must include at least one target.",
623
+ "INVALID_REQUEST"
624
+ );
625
+ }
626
+ for (const target of annotation.targets) {
627
+ const absolutePath = resolveWorkspacePath(target.location.file, state.cwd);
628
+ assertPathWithinProject(absolutePath, state.projectRoot);
629
+ }
630
+ }
631
+ }
632
+ function normalizeAnnotationBatch(req) {
633
+ return {
634
+ instruction: req.instruction?.trim() ?? "",
635
+ responseMode: req.responseMode ?? "unified",
636
+ ...req.runtimeContext ? { runtimeContext: req.runtimeContext } : {},
637
+ ...req.screenshotContext ? { screenshotContext: req.screenshotContext } : {},
638
+ ...req.cssContextPrompt?.trim() ? { cssContextPrompt: req.cssContextPrompt.trim() } : {},
639
+ annotations: req.annotations.map((annotation, index) => ({
640
+ index: index + 1,
641
+ note: annotation.note.trim(),
642
+ intent: annotation.intent,
643
+ targets: annotation.targets.map((target) => ({
644
+ file: target.location.file,
645
+ line: target.location.line,
646
+ column: target.location.column,
647
+ ...target.label ? { label: target.label } : {},
648
+ ...target.selector ? { selector: target.selector } : {},
649
+ ...target.snippet ? { snippet: target.snippet } : {}
650
+ }))
651
+ }))
652
+ };
653
+ }
654
+ function buildAnnotationBatchPrompt(batch) {
655
+ const body = buildSelectedElementsPrompt(batch.annotations);
656
+ const prompt = batch.instruction ? `${batch.instruction}
657
+
658
+ ${body}` : body;
659
+ return appendScreenshotContextSection(
660
+ appendCssContextSection(
661
+ appendRuntimeContextSection(prompt, batch.runtimeContext),
662
+ batch.cssContextPrompt
663
+ ),
664
+ batch.screenshotContext
665
+ );
666
+ }
667
+ function appendCssContextSection(prompt, cssContextPrompt) {
668
+ if (!cssContextPrompt) return prompt;
669
+ return `${prompt}
670
+
671
+ ${cssContextPrompt}`;
672
+ }
673
+ function buildSelectedElementsPrompt(annotations) {
674
+ const lines = ["Selected elements:"];
675
+ for (const annotation of annotations) {
676
+ const trimmedNote = annotation.note.trim();
677
+ for (const target of annotation.targets) {
678
+ const targetLabel = (target.label || "Unknown target").trim() || "Unknown target";
679
+ lines.push(`- ${targetLabel}`);
680
+ lines.push(`file=${target.file}:${target.line}:${target.column}`);
681
+ if (trimmedNote) {
682
+ lines.push(`note=${trimmedNote}`);
683
+ }
684
+ }
685
+ }
686
+ if (lines.length === 1) {
687
+ lines.push("- None");
688
+ }
689
+ return lines.join("\n");
690
+ }
691
+ function appendScreenshotContextSection(prompt, screenshotContext) {
692
+ if (!screenshotContext || !screenshotContext.imageDataUrl && !screenshotContext.imageAssetId) {
693
+ return prompt;
694
+ }
695
+ const lines = [
696
+ "Visual screenshot context attached:",
697
+ `- capturedAt=${screenshotContext.capturedAt}`,
698
+ `- mimeType=${screenshotContext.mimeType}`,
699
+ ...screenshotContext.imageAssetId ? [`- imageAssetId=${screenshotContext.imageAssetId}`] : []
700
+ ];
701
+ return `${prompt}
702
+
703
+ ${lines.join("\n")}`;
704
+ }
705
+ function appendRuntimeContextSection(prompt, runtimeContext) {
706
+ if (!runtimeContext?.records.length) {
707
+ return prompt;
708
+ }
709
+ return `${prompt}
710
+
711
+ ${buildRuntimeContextSection(runtimeContext.records)}`;
712
+ }
713
+ function buildRuntimeContextSection(records) {
714
+ return ["Relevant runtime context:", ...records.map(formatRuntimeRecord)].join("\n");
715
+ }
716
+ function formatRuntimeRecord(record) {
717
+ 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}`;
718
+ const reasonSummary = record.relevanceReasons.length ? record.relevanceReasons.join("; ") : "timing-based";
719
+ const stackSummary = record.stack ? `
720
+ stack=${record.stack.split("\n").slice(0, 5).join(" | ")}` : "";
721
+ return [
722
+ `- [${record.kind}] ${record.message}`,
723
+ ` relevance=${record.relevanceLevel} (${reasonSummary})`,
724
+ ` ${requestSummary}`,
725
+ stackSummary
726
+ ].filter(Boolean).join("\n");
727
+ }
728
+ function getAnnotationDispatchErrorCode(error) {
729
+ if (error instanceof AnnotationDispatchError) return error.errorCode;
730
+ if (error instanceof Error && error.message.includes("outside of project workspace")) {
731
+ return "FORBIDDEN_PATH";
732
+ }
733
+ return "UNKNOWN";
734
+ }
735
+
736
+ // src/server/client-config.ts
737
+ async function buildClientConfig(serverState3) {
738
+ const userConfig = loadUserConfigSync(false, serverState3.cwd, serverState3.configRoot);
739
+ const promptsConfig = await loadPromptsConfig(false, serverState3.cwd, serverState3.configRoot);
740
+ const effectiveIde = userConfig.ide ?? "vscode";
741
+ let info;
742
+ if (!serverState3.ideInfo) {
743
+ info = { ide: effectiveIde };
744
+ } else {
745
+ const { scheme: _scheme, ...rest } = serverState3.ideInfo;
746
+ info = rest;
747
+ }
748
+ return {
749
+ ...info,
750
+ prompts: resolveIntents(promptsConfig),
751
+ hotKeys: userConfig["inspector.hotKey"] ?? "alt",
752
+ theme: userConfig["inspector.theme"] ?? "auto",
753
+ includeSnippet: userConfig["prompt.includeSnippet"] ?? false,
754
+ runtimeContext: {
755
+ enabled: true,
756
+ preview: true,
757
+ maxRuntimeErrors: 3,
758
+ maxFailedRequests: 2
759
+ },
760
+ screenshotContext: {
761
+ enabled: false
762
+ },
763
+ annotationResponseMode: userConfig["prompt.annotationResponseMode"] ?? "unified",
764
+ autoSend: userConfig["prompt.autoSend"] ?? false
765
+ };
766
+ }
767
+
768
+ // src/server/open-file.ts
769
+ var import_node_child_process2 = require("child_process");
770
+ var import_launch_ide2 = require("launch-ide");
771
+ var serverLogger2 = createLogger("inspecto:server", { logLevel: getGlobalLogLevel() });
772
+ var VSCODE_FAMILY_SCHEMES = [
773
+ "vscode",
774
+ "vscode-insiders",
775
+ "cursor",
776
+ "windsurf",
777
+ "trae",
778
+ "trae-cn",
779
+ "vscodium",
780
+ "codebuddy",
781
+ "codebuddy-cn",
782
+ "antigravity"
783
+ ];
784
+ function handleOpenFileRequest(body, serverState3) {
785
+ const absolutePath = resolveWorkspacePath(body.file, serverState3.cwd);
786
+ assertPathWithinProject(absolutePath, serverState3.projectRoot);
787
+ const userConfig = loadUserConfigSync(false, serverState3.cwd, serverState3.configRoot);
788
+ const configuredIde = userConfig.ide;
789
+ const activeIde = serverState3.ideInfo?.ide;
790
+ const activeIdeScheme = serverState3.ideInfo?.scheme;
791
+ const rawEditorHint = configuredIde || activeIde || activeIdeScheme || "code";
792
+ if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
793
+ serverLogger2.warn(
794
+ `Active IDE is ${activeIdeScheme}, but config forces ${configuredIde}. Using configured IDE.`
795
+ );
796
+ }
797
+ let editorHint = rawEditorHint;
798
+ if (rawEditorHint === "vscode") editorHint = "code";
799
+ else if (rawEditorHint === "vscode-insiders") editorHint = "code-insiders";
800
+ else if (rawEditorHint === "vscodium") editorHint = "codium";
801
+ else if (rawEditorHint === "trae-cn" || rawEditorHint === "trae") editorHint = "trae";
802
+ serverLogger2.debug(
803
+ `IDE_OPEN: activeIde=${activeIde}, activeIdeScheme=${activeIdeScheme}, configuredIde=${configuredIde} -> rawEditorHint=${rawEditorHint}, finalEditorHint=${editorHint}`
804
+ );
805
+ if (VSCODE_FAMILY_SCHEMES.includes(rawEditorHint)) {
806
+ let normalizedPath = absolutePath.replace(/\\/g, "/");
807
+ if (!normalizedPath.startsWith("/")) {
808
+ normalizedPath = "/" + normalizedPath;
809
+ }
810
+ const encodedPath = encodeURI(normalizedPath);
811
+ const uri = `${rawEditorHint}://file${encodedPath}:${body.line}:${body.column}`;
812
+ serverLogger2.debug(`IDE_OPEN: Bypassing launchIDE, using URI scheme directly: ${uri}`);
813
+ try {
814
+ if (process.platform === "darwin") {
815
+ (0, import_node_child_process2.execFileSync)("open", [uri]);
816
+ } else if (process.platform === "win32") {
817
+ (0, import_node_child_process2.execFileSync)("cmd", ["/c", "start", '""', uri]);
818
+ } else {
819
+ (0, import_node_child_process2.execFileSync)("xdg-open", [uri]);
820
+ }
821
+ } catch (e) {
822
+ serverLogger2.error(`Failed to launch URI for IDE_OPEN (${uri}):`, e);
823
+ (0, import_launch_ide2.launchIDE)({
824
+ file: absolutePath,
825
+ line: body.line,
826
+ column: body.column,
827
+ editor: editorHint,
828
+ type: process.platform === "darwin" ? "open" : "exec"
829
+ });
830
+ }
831
+ } else {
832
+ (0, import_launch_ide2.launchIDE)({
833
+ file: absolutePath,
834
+ line: body.line,
835
+ column: body.column,
836
+ editor: editorHint,
837
+ type: process.platform === "darwin" ? "open" : "exec"
838
+ });
839
+ }
840
+ return { success: true };
841
+ }
842
+
843
+ // src/server/project-root.ts
844
+ var import_node_fs2 = __toESM(require("fs"), 1);
845
+ var import_node_path3 = __toESM(require("path"), 1);
846
+ var import_node_child_process3 = require("child_process");
847
+ var serverLogger3 = createLogger("inspecto:server", { logLevel: getGlobalLogLevel() });
848
+ function resolveProjectRoot() {
849
+ const cwd = process.cwd();
850
+ let gitRoot;
851
+ try {
852
+ gitRoot = (0, import_node_child_process3.execSync)("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
853
+ } catch (e) {
854
+ serverLogger3.warn("Failed to resolve git root via git rev-parse:", e);
855
+ gitRoot = cwd;
856
+ }
857
+ const visited = /* @__PURE__ */ new Set();
858
+ const search = (start, stop) => {
859
+ let current = start;
860
+ while (!visited.has(current)) {
861
+ visited.add(current);
862
+ if (import_node_fs2.default.existsSync(import_node_path3.default.join(current, ".inspecto"))) return current;
863
+ if (current === stop) break;
864
+ const parent = import_node_path3.default.dirname(current);
865
+ if (parent === current) break;
866
+ current = parent;
867
+ }
868
+ return null;
869
+ };
870
+ const cwdMatch = search(cwd, import_node_path3.default.parse(cwd).root);
871
+ if (cwdMatch) return cwdMatch;
872
+ const repoMatch = search(gitRoot, import_node_path3.default.parse(gitRoot).root);
873
+ if (repoMatch) return repoMatch;
874
+ return gitRoot;
875
+ }
876
+
877
+ // src/server/index.ts
878
+ var serverLogger4 = createLogger("inspecto:server", { logLevel: getGlobalLogLevel() });
879
+ var serverState = {
880
+ port: null,
881
+ running: false,
882
+ projectRoot: "",
883
+ configRoot: "",
884
+ cwd: process.cwd()
885
+ };
886
+ var serverInstance = null;
544
887
  async function startServer() {
545
888
  if (serverState.running && serverState.port !== null) {
546
889
  return serverState.port;
@@ -552,7 +895,7 @@ async function startServer() {
552
895
  const port = await import_portfinder.default.getPortPromise();
553
896
  watchConfig(
554
897
  () => {
555
- serverLogger.info("user config reloaded.");
898
+ serverLogger4.info("user config reloaded.");
556
899
  },
557
900
  serverState.cwd,
558
901
  serverState.configRoot
@@ -568,7 +911,7 @@ async function startServer() {
568
911
  }
569
912
  const url = new URL(req.url ?? "/", `http://localhost:${port}`);
570
913
  handleRequest(url, req, res).catch((err) => {
571
- serverLogger.error("server error:", err);
914
+ serverLogger4.error("server error:", err);
572
915
  res.writeHead(500, { "Content-Type": "application/json" });
573
916
  res.end(JSON.stringify({ success: false, error: String(err) }));
574
917
  });
@@ -581,41 +924,41 @@ async function startServer() {
581
924
  serverInstance.once("error", reject);
582
925
  });
583
926
  serverInstance.on("error", (err) => {
584
- serverLogger.error("persistent server error:", err);
927
+ serverLogger4.error("persistent server error:", err);
585
928
  });
586
929
  serverState.port = port;
587
930
  serverState.running = true;
588
- const portFile = import_node_path2.default.join(import_node_os2.default.tmpdir(), "inspecto.port.json");
931
+ const portFile = import_node_path4.default.join(import_node_os2.default.tmpdir(), "inspecto.port.json");
589
932
  try {
590
933
  let portData = {};
591
- if (import_node_fs2.default.existsSync(portFile)) {
934
+ if (import_node_fs3.default.existsSync(portFile)) {
592
935
  try {
593
- portData = JSON.parse(import_node_fs2.default.readFileSync(portFile, "utf-8"));
936
+ portData = JSON.parse(import_node_fs3.default.readFileSync(portFile, "utf-8"));
594
937
  } catch (e) {
595
938
  }
596
939
  }
597
- const rootHash = import_node_crypto.default.createHash("md5").update(serverState.projectRoot).digest("hex");
940
+ const rootHash = import_node_crypto2.default.createHash("md5").update(serverState.projectRoot).digest("hex");
598
941
  portData[rootHash] = port;
599
- import_node_fs2.default.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
942
+ import_node_fs3.default.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
600
943
  } catch (e) {
601
- serverLogger.warn("Failed to write port file:", e);
944
+ serverLogger4.warn("Failed to write port file:", e);
602
945
  }
603
946
  process.once("exit", () => {
604
947
  try {
605
- if (import_node_fs2.default.existsSync(portFile)) {
606
- const portData = JSON.parse(import_node_fs2.default.readFileSync(portFile, "utf-8"));
607
- const rootHash = import_node_crypto.default.createHash("md5").update(serverState.projectRoot).digest("hex");
948
+ if (import_node_fs3.default.existsSync(portFile)) {
949
+ const portData = JSON.parse(import_node_fs3.default.readFileSync(portFile, "utf-8"));
950
+ const rootHash = import_node_crypto2.default.createHash("md5").update(serverState.projectRoot).digest("hex");
608
951
  delete portData[rootHash];
609
952
  if (Object.keys(portData).length === 0) {
610
- import_node_fs2.default.unlinkSync(portFile);
953
+ import_node_fs3.default.unlinkSync(portFile);
611
954
  } else {
612
- import_node_fs2.default.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
955
+ import_node_fs3.default.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
613
956
  }
614
957
  }
615
958
  } catch {
616
959
  }
617
960
  });
618
- serverLogger.info(`server running at http://127.0.0.1:${port}`);
961
+ serverLogger4.info(`server running at http://127.0.0.1:${port}`);
619
962
  return port;
620
963
  }
621
964
  async function readBody(req) {
@@ -634,26 +977,7 @@ async function handleRequest(url, req, res) {
634
977
  return;
635
978
  }
636
979
  if (pathname === import_types2.INSPECTO_API_PATHS.CLIENT_CONFIG && req.method === "GET") {
637
- const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
638
- const promptsConfig = await loadPromptsConfig(false, serverState.cwd, serverState.configRoot);
639
- const effectiveIde = userConfig.ide ?? "vscode";
640
- let info;
641
- if (!serverState.ideInfo) {
642
- info = {
643
- ide: effectiveIde
644
- };
645
- } else {
646
- const { scheme: _scheme, ...rest } = serverState.ideInfo;
647
- info = rest;
648
- }
649
- const config = {
650
- ...info,
651
- prompts: resolveIntents(promptsConfig),
652
- hotKeys: userConfig["inspector.hotKey"] ?? "alt",
653
- theme: userConfig["inspector.theme"] ?? "auto",
654
- includeSnippet: userConfig["prompt.includeSnippet"] ?? false,
655
- autoSend: userConfig["prompt.autoSend"] ?? false
656
- };
980
+ const config = await buildClientConfig(serverState);
657
981
  delete config.providers;
658
982
  res.writeHead(200, { "Content-Type": "application/json" });
659
983
  res.end(JSON.stringify(config));
@@ -664,21 +988,23 @@ async function handleRequest(url, req, res) {
664
988
  const body = JSON.parse(await readBody(req));
665
989
  const ideWorkspace = body.workspaceRoot || "";
666
990
  const serverProjectRoot = serverState.projectRoot || "";
667
- const isSameProject = !ideWorkspace || !serverProjectRoot || ideWorkspace === serverProjectRoot || serverProjectRoot.startsWith(ideWorkspace);
991
+ const normalizedIdeRoot = ideWorkspace ? import_node_path4.default.resolve(ideWorkspace) : "";
992
+ const normalizedServerRoot = serverProjectRoot ? import_node_path4.default.resolve(serverProjectRoot) : "";
993
+ const isSameProject = !normalizedIdeRoot || !normalizedServerRoot || normalizedIdeRoot === normalizedServerRoot || normalizedServerRoot.startsWith(normalizedIdeRoot + import_node_path4.default.sep) || normalizedIdeRoot.startsWith(normalizedServerRoot + import_node_path4.default.sep);
668
994
  if (isSameProject) {
669
995
  serverState.ideInfo = body;
670
- serverLogger.debug(
996
+ serverLogger4.debug(
671
997
  `Accepted IDE info from matched workspace (ide-${body.ide} / schema-${body.scheme})`
672
998
  );
673
999
  } else {
674
- serverLogger.debug(
1000
+ serverLogger4.debug(
675
1001
  `Ignored IDE info from unrelated workspace (IDE Workspace: ${ideWorkspace}, Server: ${serverProjectRoot}, Scheme: ${body.scheme}, IDE: ${body.ide})`
676
1002
  );
677
1003
  }
678
1004
  res.writeHead(200, { "Content-Type": "application/json" });
679
1005
  res.end(JSON.stringify({ success: true }));
680
1006
  } catch (e) {
681
- serverLogger.error(`Error parsing ${import_types2.INSPECTO_API_PATHS.IDE_INFO} POST request:`, e);
1007
+ serverLogger4.error(`Error parsing ${import_types2.INSPECTO_API_PATHS.IDE_INFO} POST request:`, e);
682
1008
  res.writeHead(400, { "Content-Type": "application/json" });
683
1009
  res.end(JSON.stringify({ error: "Invalid JSON body" }));
684
1010
  }
@@ -693,74 +1019,14 @@ async function handleRequest(url, req, res) {
693
1019
  res.end(JSON.stringify({ error: "Invalid JSON body" }));
694
1020
  return;
695
1021
  }
696
- 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);
697
- const relativeToRoot = import_node_path2.default.relative(serverState.projectRoot, absolutePath);
698
- if (relativeToRoot.startsWith("..") || import_node_path2.default.isAbsolute(relativeToRoot)) {
699
- serverLogger.warn(`Security: Blocked path traversal attempt in IDE_OPEN: ${body.file}`);
1022
+ try {
1023
+ handleOpenFileRequest(body, serverState);
1024
+ } catch {
1025
+ serverLogger4.warn(`Security: Blocked path traversal attempt in IDE_OPEN: ${body.file}`);
700
1026
  res.writeHead(403, { "Content-Type": "application/json" });
701
1027
  res.end(JSON.stringify({ error: "Access denied: File is outside of project workspace" }));
702
1028
  return;
703
1029
  }
704
- const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
705
- const configuredIde = userConfig.ide;
706
- const activeIde = serverState.ideInfo?.ide;
707
- const activeIdeScheme = serverState.ideInfo?.scheme;
708
- const rawEditorHint = configuredIde || activeIde || activeIdeScheme || "code";
709
- if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
710
- serverLogger.warn(
711
- `Active IDE is ${activeIdeScheme}, but config forces ${configuredIde}. Using configured IDE.`
712
- );
713
- }
714
- let editorHint = rawEditorHint;
715
- if (rawEditorHint === "vscode") editorHint = "code";
716
- else if (rawEditorHint === "vscode-insiders") editorHint = "code-insiders";
717
- else if (rawEditorHint === "vscodium") editorHint = "codium";
718
- else if (rawEditorHint === "trae-cn" || rawEditorHint === "trae") editorHint = "trae";
719
- serverLogger.debug(
720
- `IDE_OPEN: activeIde=${activeIde}, activeIdeScheme=${activeIdeScheme}, configuredIde=${configuredIde} -> rawEditorHint=${rawEditorHint}, finalEditorHint=${editorHint}`
721
- );
722
- const VSCODE_FAMILY_SCHEMES = [
723
- "vscode",
724
- "vscode-insiders",
725
- "cursor",
726
- "windsurf",
727
- "trae",
728
- "trae-cn",
729
- "vscodium",
730
- "codebuddy",
731
- "codebuddy-cn",
732
- "antigravity"
733
- ];
734
- if (VSCODE_FAMILY_SCHEMES.includes(rawEditorHint)) {
735
- const uri = `${rawEditorHint}://file${absolutePath}:${body.line}:${body.column}`;
736
- serverLogger.debug(`IDE_OPEN: Bypassing launchIDE, using URI scheme directly: ${uri}`);
737
- try {
738
- if (process.platform === "darwin") {
739
- (0, import_node_child_process.execFileSync)("open", [uri]);
740
- } else if (process.platform === "win32") {
741
- (0, import_node_child_process.execFileSync)("cmd", ["/c", "start", '""', uri]);
742
- } else {
743
- (0, import_node_child_process.execFileSync)("xdg-open", [uri]);
744
- }
745
- } catch (e) {
746
- serverLogger.error(`Failed to launch URI for IDE_OPEN (${uri}):`, e);
747
- (0, import_launch_ide.launchIDE)({
748
- file: absolutePath,
749
- line: body.line,
750
- column: body.column,
751
- editor: editorHint,
752
- type: process.platform === "darwin" ? "open" : "exec"
753
- });
754
- }
755
- } else {
756
- (0, import_launch_ide.launchIDE)({
757
- file: absolutePath,
758
- line: body.line,
759
- column: body.column,
760
- editor: editorHint,
761
- type: process.platform === "darwin" ? "open" : "exec"
762
- });
763
- }
764
1030
  res.writeHead(200, { "Content-Type": "application/json" });
765
1031
  res.end(JSON.stringify({ success: true }));
766
1032
  return;
@@ -771,10 +1037,11 @@ async function handleRequest(url, req, res) {
771
1037
  const column = parseInt(url.searchParams.get("column") ?? "1", 10);
772
1038
  const maxLines = parseInt(url.searchParams.get("maxLines") ?? "100", 10);
773
1039
  try {
774
- const absolutePath = import_node_path2.default.isAbsolute(file) ? import_node_path2.default.resolve(file) : import_node_path2.default.resolve(serverState.cwd, file);
775
- const relativeToRoot = import_node_path2.default.relative(serverState.projectRoot, absolutePath);
776
- if (relativeToRoot.startsWith("..") || import_node_path2.default.isAbsolute(relativeToRoot)) {
777
- serverLogger.warn(`Security: Blocked path traversal attempt in PROJECT_SNIPPET: ${file}`);
1040
+ const absolutePath = resolveWorkspacePath(file, serverState.cwd);
1041
+ try {
1042
+ assertPathWithinProject(absolutePath, serverState.projectRoot);
1043
+ } catch {
1044
+ serverLogger4.warn(`Security: Blocked path traversal attempt in PROJECT_SNIPPET: ${file}`);
778
1045
  res.writeHead(403, { "Content-Type": "application/json" });
779
1046
  res.end(
780
1047
  JSON.stringify({
@@ -804,7 +1071,23 @@ async function handleRequest(url, req, res) {
804
1071
  res.writeHead(result.success ? 200 : 500, { "Content-Type": "application/json" });
805
1072
  res.end(JSON.stringify(result));
806
1073
  } catch (e) {
807
- serverLogger.error(`Error parsing ${import_types2.INSPECTO_API_PATHS.AI_DISPATCH} request:`, e);
1074
+ serverLogger4.error(`Error parsing ${import_types2.INSPECTO_API_PATHS.AI_DISPATCH} request:`, e);
1075
+ res.writeHead(500, { "Content-Type": "application/json" });
1076
+ res.end(JSON.stringify({ success: false, error: String(e), errorCode: "INTERNAL_ERROR" }));
1077
+ }
1078
+ return;
1079
+ }
1080
+ if (pathname === import_types2.INSPECTO_API_PATHS.AI_BATCH_DISPATCH && req.method === "POST") {
1081
+ try {
1082
+ const rawBody = await readBody(req);
1083
+ const body = JSON.parse(rawBody);
1084
+ const result = await dispatchAnnotationsToAi(body, serverState);
1085
+ res.writeHead(getBatchDispatchStatusCode(result.errorCode, result.success), {
1086
+ "Content-Type": "application/json"
1087
+ });
1088
+ res.end(JSON.stringify(result));
1089
+ } catch (e) {
1090
+ serverLogger4.error(`Error parsing ${import_types2.INSPECTO_API_PATHS.AI_BATCH_DISPATCH} request:`, e);
808
1091
  res.writeHead(500, { "Content-Type": "application/json" });
809
1092
  res.end(JSON.stringify({ success: false, error: String(e), errorCode: "INTERNAL_ERROR" }));
810
1093
  }
@@ -812,7 +1095,7 @@ async function handleRequest(url, req, res) {
812
1095
  }
813
1096
  if (pathname.startsWith(`${import_types2.INSPECTO_API_PATHS.AI_TICKET}/`) && req.method === "GET") {
814
1097
  const ticketId = pathname.substring(import_types2.INSPECTO_API_PATHS.AI_TICKET.length + 1);
815
- const payloadStr = payloadTickets.get(ticketId);
1098
+ const payloadStr = readTicket(ticketId);
816
1099
  if (!payloadStr) {
817
1100
  res.writeHead(404, { "Content-Type": "application/json" });
818
1101
  res.end(JSON.stringify({ success: false, error: "Ticket not found or expired" }));
@@ -826,54 +1109,28 @@ async function handleRequest(url, req, res) {
826
1109
  res.end(JSON.stringify({ error: "not found" }));
827
1110
  }
828
1111
  async function dispatchToAi(req) {
829
- const { location, snippet, prompt } = req;
830
- const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
831
- const resolvedTarget = resolveTargetTool(userConfig);
1112
+ const { location, snippet, prompt, screenshotContext } = req;
832
1113
  const formattedPrompt = prompt ?? `Please help me with this code from \`${location.file}\` (line ${location.line}):
833
1114
 
834
1115
  \`\`\`
835
1116
  ${snippet}
836
1117
  \`\`\`
837
1118
  `;
838
- const ideReportedMode = serverState.ideInfo?.providers[resolvedTarget]?.mode;
839
- const configuredIde = userConfig.ide;
840
- const activeIde = serverState.ideInfo?.ide;
841
- const activeIdeScheme = serverState.ideInfo?.scheme;
842
- const finalIde = configuredIde || activeIdeScheme || activeIde || "vscode";
843
- if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
844
- serverLogger.warn(
845
- `dispatchToAi: Active IDE is ${activeIdeScheme}, but config forces ${configuredIde}. Using configured IDE.`
846
- );
847
- }
848
- const mode = resolveProviderMode(resolvedTarget, finalIde, userConfig);
849
- const overrides = extractToolOverrides(finalIde, userConfig)[resolvedTarget] || {};
850
- overrides.type = mode;
851
- const fullPayload = {
852
- ide: finalIde,
853
- target: resolvedTarget,
854
- targetType: mode,
1119
+ const runtime = resolvePromptDispatchRuntime(serverState);
1120
+ return dispatchPromptThroughIde(runtime, {
855
1121
  prompt: formattedPrompt,
856
1122
  filePath: location.file,
857
1123
  line: location.line,
858
1124
  column: location.column,
859
1125
  snippet,
860
- overrides: Object.keys(overrides).length > 0 ? overrides : void 0,
861
- autoSend: userConfig["prompt.autoSend"] !== void 0 ? Boolean(userConfig["prompt.autoSend"]) : void 0
862
- };
863
- const ticketId = createTicket(fullPayload);
864
- const params = new URLSearchParams();
865
- params.set("ticket", ticketId);
866
- params.set("target", resolvedTarget);
867
- const uri = `${finalIde}://inspecto.inspecto/send?${params.toString()}`;
868
- serverLogger.debug(`dispatchToAi: Generated URI: ${uri}`);
869
- launchURI(uri);
870
- return {
871
- success: true,
872
- fallbackPayload: {
873
- prompt: formattedPrompt,
874
- file: location.file
875
- }
876
- };
1126
+ ...screenshotContext ? { screenshotContext } : {}
1127
+ });
1128
+ }
1129
+ function getBatchDispatchStatusCode(errorCode, success) {
1130
+ if (success) return 200;
1131
+ if (errorCode === "INVALID_REQUEST") return 400;
1132
+ if (errorCode === "FORBIDDEN_PATH") return 403;
1133
+ return 500;
877
1134
  }
878
1135
 
879
1136
  // src/injectors/utils.ts
@@ -908,7 +1165,7 @@ window.addEventListener('load', () => {
908
1165
  }
909
1166
 
910
1167
  // src/legacy/rspack/index.ts
911
- var import_node_path3 = __toESM(require("path"), 1);
1168
+ var import_node_path5 = __toESM(require("path"), 1);
912
1169
  var _dirname = typeof __dirname !== "undefined" ? __dirname : __dirname;
913
1170
  var DEFAULT_OPTIONS = {
914
1171
  include: [],
@@ -978,7 +1235,7 @@ var InspectoRspackLegacyPlugin = class {
978
1235
  );
979
1236
  }
980
1237
  });
981
- const loaderPath = import_node_path3.default.resolve(_dirname, "./loader.cjs");
1238
+ const loaderPath = import_node_path5.default.resolve(_dirname, "./loader.cjs");
982
1239
  compiler.options.module.rules.push({
983
1240
  test: /\.[jt]sx?$/,
984
1241
  use: [