@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.
@@ -45,13 +45,11 @@ var resolveClientModule = () => {
45
45
 
46
46
  // src/server/index.ts
47
47
  import http from "http";
48
- import fs3 from "fs";
49
- import path4 from "path";
48
+ import fs4 from "fs";
49
+ import path6 from "path";
50
50
  import os2 from "os";
51
- import crypto from "crypto";
52
- import { execSync, execFileSync } from "child_process";
51
+ import crypto2 from "crypto";
53
52
  import portfinder from "portfinder";
54
- import { launchIDE } from "launch-ide";
55
53
  import { INSPECTO_API_PATHS } from "@inspecto-dev/types";
56
54
 
57
55
  // src/server/snippet.ts
@@ -375,9 +373,9 @@ function extractToolOverrides(ide, config) {
375
373
  function resolveIntents(serverPrompts) {
376
374
  const baseMap = /* @__PURE__ */ new Map();
377
375
  for (const intent of DEFAULT_INTENTS) {
378
- if (intent.id) baseMap.set(intent.id, { ...intent });
376
+ baseMap.set(intent.id, { ...intent });
379
377
  }
380
- const defaults = () => ensureOpenInEditorLast(Array.from(baseMap.values()));
378
+ const defaults = () => Array.from(baseMap.values());
381
379
  if (!serverPrompts) return defaults();
382
380
  const isReplace = !Array.isArray(serverPrompts) && typeof serverPrompts === "object" && serverPrompts.$replace === true;
383
381
  const promptsArray = Array.isArray(serverPrompts) ? serverPrompts : isReplace ? serverPrompts.items : [];
@@ -404,16 +402,18 @@ function resolveIntents(serverPrompts) {
404
402
  );
405
403
  continue;
406
404
  }
407
- if (item.isAction && item.id !== "open-in-editor") {
405
+ if (!item.aiIntent) {
408
406
  configLogger.warn(
409
- `isAction is reserved for built-in actions. Ignoring intent "${item.id}".`
407
+ `Intent "${item.id}" is missing required "aiIntent".`
410
408
  );
411
409
  continue;
412
410
  }
413
- result.push(baseMap.has(item.id) ? { ...baseMap.get(item.id), ...item } : item);
411
+ result.push(
412
+ baseMap.has(item.id) ? { ...baseMap.get(item.id), ...item } : item
413
+ );
414
414
  }
415
415
  }
416
- return ensureOpenInEditorLast(result);
416
+ return result;
417
417
  }
418
418
  const merged = Array.from(baseMap.values());
419
419
  for (const item of promptsArray) {
@@ -430,9 +430,9 @@ function resolveIntents(serverPrompts) {
430
430
  configLogger.warn('Intent object missing required "id" field, skipping.');
431
431
  continue;
432
432
  }
433
- if (item.isAction && item.id !== "open-in-editor") {
433
+ if (!item.aiIntent) {
434
434
  configLogger.warn(
435
- `isAction is reserved for built-in actions. Ignoring intent "${item.id}".`
435
+ `Intent "${item.id}" is missing required "aiIntent".`
436
436
  );
437
437
  continue;
438
438
  }
@@ -450,15 +450,7 @@ function resolveIntents(serverPrompts) {
450
450
  }
451
451
  }
452
452
  }
453
- return ensureOpenInEditorLast(merged);
454
- }
455
- function ensureOpenInEditorLast(intents) {
456
- const idx = intents.findIndex((i) => i.id === "open-in-editor");
457
- if (idx === -1 || idx === intents.length - 1) return intents;
458
- const result = [...intents];
459
- const item = result.splice(idx, 1)[0];
460
- result.push(item);
461
- return result;
453
+ return merged;
462
454
  }
463
455
  var watchers = [];
464
456
  function watchConfig(onReload, cwd = process.cwd(), gitRoot) {
@@ -493,7 +485,10 @@ function watchConfig(onReload, cwd = process.cwd(), gitRoot) {
493
485
  }
494
486
  }
495
487
 
496
- // src/server/index.ts
488
+ // src/server/dispatch-transport.ts
489
+ import crypto from "crypto";
490
+ import { execFileSync } from "child_process";
491
+ import { launchIDE } from "launch-ide";
497
492
  var serverLogger = createLogger("inspecto:server", { logLevel: getGlobalLogLevel() });
498
493
  var payloadTickets = /* @__PURE__ */ new Map();
499
494
  function createTicket(payload) {
@@ -507,21 +502,363 @@ function createTicket(payload) {
507
502
  );
508
503
  return ticketId;
509
504
  }
510
- var serverState = {
511
- port: null,
512
- running: false,
513
- projectRoot: "",
514
- configRoot: "",
515
- cwd: process.cwd()
505
+ function readTicket(ticketId) {
506
+ return payloadTickets.get(ticketId);
507
+ }
508
+ function launchURI(uri) {
509
+ try {
510
+ if (process.platform === "darwin") {
511
+ execFileSync("open", [uri]);
512
+ } else if (process.platform === "win32") {
513
+ execFileSync("cmd", ["/c", "start", '""', uri]);
514
+ } else {
515
+ execFileSync("xdg-open", [uri]);
516
+ }
517
+ } catch (e) {
518
+ serverLogger.error("Failed to launch URI via execFileSync, falling back to launchIDE:", e);
519
+ launchIDE({ file: uri });
520
+ }
521
+ }
522
+
523
+ // src/server/dispatch-runtime.ts
524
+ function resolvePromptDispatchRuntime(state) {
525
+ const userConfig = loadUserConfigSync(false, state.cwd, state.projectRoot);
526
+ const resolvedTarget = resolveTargetTool(userConfig);
527
+ const finalIde = resolveFinalIde(userConfig.ide, state.ideInfo?.ide, state.ideInfo?.scheme);
528
+ const mode = resolveProviderMode(resolvedTarget, finalIde, userConfig);
529
+ const overrides = extractToolOverrides(finalIde, userConfig)[resolvedTarget] || void 0;
530
+ return {
531
+ resolvedTarget,
532
+ finalIde,
533
+ mode,
534
+ ...hasOverrides(overrides) ? { overrides } : {},
535
+ ...userConfig["prompt.autoSend"] !== void 0 ? { autoSend: Boolean(userConfig["prompt.autoSend"]) } : {}
536
+ };
537
+ }
538
+ function dispatchPromptThroughIde(runtime, payload) {
539
+ const ticketId = createTicket({
540
+ ide: runtime.finalIde,
541
+ target: runtime.resolvedTarget,
542
+ targetType: runtime.mode,
543
+ prompt: payload.prompt,
544
+ filePath: payload.filePath,
545
+ line: payload.line,
546
+ column: payload.column,
547
+ snippet: payload.snippet,
548
+ ...payload.screenshotContext ? { screenshotContext: payload.screenshotContext } : {},
549
+ overrides: runtime.overrides,
550
+ autoSend: runtime.autoSend
551
+ });
552
+ const params = new URLSearchParams();
553
+ params.set("ticket", ticketId);
554
+ params.set("target", runtime.resolvedTarget);
555
+ launchURI(`${runtime.finalIde}://inspecto.inspecto/send?${params.toString()}`);
556
+ return {
557
+ success: true,
558
+ fallbackPayload: {
559
+ prompt: payload.prompt,
560
+ ...payload.filePath ? { file: payload.filePath } : {}
561
+ }
562
+ };
563
+ }
564
+ function resolveFinalIde(configuredIde, activeIde, activeIdeScheme) {
565
+ if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
566
+ return configuredIde;
567
+ }
568
+ return configuredIde || activeIdeScheme || activeIde || "vscode";
569
+ }
570
+ function hasOverrides(overrides) {
571
+ return Boolean(overrides && Object.keys(overrides).length > 0);
572
+ }
573
+
574
+ // src/server/path-guards.ts
575
+ import path4 from "path";
576
+ function isWindowsAbsolutePath(file) {
577
+ return /^[a-zA-Z]:[\\/]/.test(file) || /^\\\\[^\\]+\\[^\\]+/.test(file);
578
+ }
579
+ function resolveWorkspacePath(file, cwd) {
580
+ if (isWindowsAbsolutePath(file)) {
581
+ return path4.win32.normalize(file);
582
+ }
583
+ return path4.isAbsolute(file) ? path4.resolve(file) : path4.resolve(cwd, file);
584
+ }
585
+ function assertPathWithinProject(file, projectRoot) {
586
+ const relativeToRoot = isWindowsAbsolutePath(file) || isWindowsAbsolutePath(projectRoot) ? path4.win32.relative(path4.win32.normalize(projectRoot), path4.win32.normalize(file)) : path4.relative(projectRoot, file);
587
+ if (relativeToRoot.startsWith("..") || path4.isAbsolute(relativeToRoot)) {
588
+ throw new Error("Access denied: File is outside of project workspace");
589
+ }
590
+ }
591
+
592
+ // src/server/annotation-dispatch.ts
593
+ var AnnotationDispatchError = class extends Error {
594
+ constructor(message, errorCode) {
595
+ super(message);
596
+ this.name = "AnnotationDispatchError";
597
+ this.errorCode = errorCode;
598
+ }
516
599
  };
517
- var serverInstance = null;
600
+ async function dispatchAnnotationsToAi(req, state) {
601
+ try {
602
+ validateAnnotationDispatchRequest(req, state);
603
+ const batch = normalizeAnnotationBatch(req);
604
+ const prompt = buildAnnotationBatchPrompt(batch);
605
+ const representativeTarget = batch.annotations[0]?.targets[0];
606
+ const runtime = resolvePromptDispatchRuntime(state);
607
+ return dispatchPromptThroughIde(runtime, {
608
+ prompt,
609
+ ...representativeTarget?.file ? { filePath: representativeTarget.file } : {},
610
+ ...representativeTarget?.line ? { line: representativeTarget.line } : {},
611
+ ...representativeTarget?.column ? { column: representativeTarget.column } : {},
612
+ ...batch.screenshotContext ? { screenshotContext: batch.screenshotContext } : {}
613
+ });
614
+ } catch (error) {
615
+ return {
616
+ success: false,
617
+ error: error instanceof Error ? error.message : String(error),
618
+ errorCode: getAnnotationDispatchErrorCode(error)
619
+ };
620
+ }
621
+ }
622
+ function validateAnnotationDispatchRequest(req, state) {
623
+ if (!req.annotations.length) {
624
+ throw new AnnotationDispatchError("At least one annotation is required.", "INVALID_REQUEST");
625
+ }
626
+ for (const annotation of req.annotations) {
627
+ if (!annotation.targets.length) {
628
+ throw new AnnotationDispatchError(
629
+ "Each annotation must include at least one target.",
630
+ "INVALID_REQUEST"
631
+ );
632
+ }
633
+ for (const target of annotation.targets) {
634
+ const absolutePath = resolveWorkspacePath(target.location.file, state.cwd);
635
+ assertPathWithinProject(absolutePath, state.projectRoot);
636
+ }
637
+ }
638
+ }
639
+ function normalizeAnnotationBatch(req) {
640
+ return {
641
+ instruction: req.instruction?.trim() ?? "",
642
+ responseMode: req.responseMode ?? "unified",
643
+ ...req.runtimeContext ? { runtimeContext: req.runtimeContext } : {},
644
+ ...req.screenshotContext ? { screenshotContext: req.screenshotContext } : {},
645
+ ...req.cssContextPrompt?.trim() ? { cssContextPrompt: req.cssContextPrompt.trim() } : {},
646
+ annotations: req.annotations.map((annotation, index) => ({
647
+ index: index + 1,
648
+ note: annotation.note.trim(),
649
+ intent: annotation.intent,
650
+ targets: annotation.targets.map((target) => ({
651
+ file: target.location.file,
652
+ line: target.location.line,
653
+ column: target.location.column,
654
+ ...target.label ? { label: target.label } : {},
655
+ ...target.selector ? { selector: target.selector } : {},
656
+ ...target.snippet ? { snippet: target.snippet } : {}
657
+ }))
658
+ }))
659
+ };
660
+ }
661
+ function buildAnnotationBatchPrompt(batch) {
662
+ const body = buildSelectedElementsPrompt(batch.annotations);
663
+ const prompt = batch.instruction ? `${batch.instruction}
664
+
665
+ ${body}` : body;
666
+ return appendScreenshotContextSection(
667
+ appendCssContextSection(
668
+ appendRuntimeContextSection(prompt, batch.runtimeContext),
669
+ batch.cssContextPrompt
670
+ ),
671
+ batch.screenshotContext
672
+ );
673
+ }
674
+ function appendCssContextSection(prompt, cssContextPrompt) {
675
+ if (!cssContextPrompt) return prompt;
676
+ return `${prompt}
677
+
678
+ ${cssContextPrompt}`;
679
+ }
680
+ function buildSelectedElementsPrompt(annotations) {
681
+ const lines = ["Selected elements:"];
682
+ for (const annotation of annotations) {
683
+ const trimmedNote = annotation.note.trim();
684
+ for (const target of annotation.targets) {
685
+ const targetLabel = (target.label || "Unknown target").trim() || "Unknown target";
686
+ lines.push(`- ${targetLabel}`);
687
+ lines.push(`file=${target.file}:${target.line}:${target.column}`);
688
+ if (trimmedNote) {
689
+ lines.push(`note=${trimmedNote}`);
690
+ }
691
+ }
692
+ }
693
+ if (lines.length === 1) {
694
+ lines.push("- None");
695
+ }
696
+ return lines.join("\n");
697
+ }
698
+ function appendScreenshotContextSection(prompt, screenshotContext) {
699
+ if (!screenshotContext || !screenshotContext.imageDataUrl && !screenshotContext.imageAssetId) {
700
+ return prompt;
701
+ }
702
+ const lines = [
703
+ "Visual screenshot context attached:",
704
+ `- capturedAt=${screenshotContext.capturedAt}`,
705
+ `- mimeType=${screenshotContext.mimeType}`,
706
+ ...screenshotContext.imageAssetId ? [`- imageAssetId=${screenshotContext.imageAssetId}`] : []
707
+ ];
708
+ return `${prompt}
709
+
710
+ ${lines.join("\n")}`;
711
+ }
712
+ function appendRuntimeContextSection(prompt, runtimeContext) {
713
+ if (!runtimeContext?.records.length) {
714
+ return prompt;
715
+ }
716
+ return `${prompt}
717
+
718
+ ${buildRuntimeContextSection(runtimeContext.records)}`;
719
+ }
720
+ function buildRuntimeContextSection(records) {
721
+ return ["Relevant runtime context:", ...records.map(formatRuntimeRecord)].join("\n");
722
+ }
723
+ function formatRuntimeRecord(record) {
724
+ 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}`;
725
+ const reasonSummary = record.relevanceReasons.length ? record.relevanceReasons.join("; ") : "timing-based";
726
+ const stackSummary = record.stack ? `
727
+ stack=${record.stack.split("\n").slice(0, 5).join(" | ")}` : "";
728
+ return [
729
+ `- [${record.kind}] ${record.message}`,
730
+ ` relevance=${record.relevanceLevel} (${reasonSummary})`,
731
+ ` ${requestSummary}`,
732
+ stackSummary
733
+ ].filter(Boolean).join("\n");
734
+ }
735
+ function getAnnotationDispatchErrorCode(error) {
736
+ if (error instanceof AnnotationDispatchError) return error.errorCode;
737
+ if (error instanceof Error && error.message.includes("outside of project workspace")) {
738
+ return "FORBIDDEN_PATH";
739
+ }
740
+ return "UNKNOWN";
741
+ }
742
+
743
+ // src/server/client-config.ts
744
+ async function buildClientConfig(serverState2) {
745
+ const userConfig = loadUserConfigSync(false, serverState2.cwd, serverState2.configRoot);
746
+ const promptsConfig = await loadPromptsConfig(false, serverState2.cwd, serverState2.configRoot);
747
+ const effectiveIde = userConfig.ide ?? "vscode";
748
+ let info;
749
+ if (!serverState2.ideInfo) {
750
+ info = { ide: effectiveIde };
751
+ } else {
752
+ const { scheme: _scheme, ...rest } = serverState2.ideInfo;
753
+ info = rest;
754
+ }
755
+ return {
756
+ ...info,
757
+ prompts: resolveIntents(promptsConfig),
758
+ hotKeys: userConfig["inspector.hotKey"] ?? "alt",
759
+ theme: userConfig["inspector.theme"] ?? "auto",
760
+ includeSnippet: userConfig["prompt.includeSnippet"] ?? false,
761
+ runtimeContext: {
762
+ enabled: true,
763
+ preview: true,
764
+ maxRuntimeErrors: 3,
765
+ maxFailedRequests: 2
766
+ },
767
+ screenshotContext: {
768
+ enabled: false
769
+ },
770
+ annotationResponseMode: userConfig["prompt.annotationResponseMode"] ?? "unified",
771
+ autoSend: userConfig["prompt.autoSend"] ?? false
772
+ };
773
+ }
774
+
775
+ // src/server/open-file.ts
776
+ import { execFileSync as execFileSync2 } from "child_process";
777
+ import { launchIDE as launchIDE2 } from "launch-ide";
778
+ var serverLogger2 = createLogger("inspecto:server", { logLevel: getGlobalLogLevel() });
779
+ var VSCODE_FAMILY_SCHEMES = [
780
+ "vscode",
781
+ "vscode-insiders",
782
+ "cursor",
783
+ "windsurf",
784
+ "trae",
785
+ "trae-cn",
786
+ "vscodium",
787
+ "codebuddy",
788
+ "codebuddy-cn",
789
+ "antigravity"
790
+ ];
791
+ function handleOpenFileRequest(body, serverState2) {
792
+ const absolutePath = resolveWorkspacePath(body.file, serverState2.cwd);
793
+ assertPathWithinProject(absolutePath, serverState2.projectRoot);
794
+ const userConfig = loadUserConfigSync(false, serverState2.cwd, serverState2.configRoot);
795
+ const configuredIde = userConfig.ide;
796
+ const activeIde = serverState2.ideInfo?.ide;
797
+ const activeIdeScheme = serverState2.ideInfo?.scheme;
798
+ const rawEditorHint = configuredIde || activeIde || activeIdeScheme || "code";
799
+ if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
800
+ serverLogger2.warn(
801
+ `Active IDE is ${activeIdeScheme}, but config forces ${configuredIde}. Using configured IDE.`
802
+ );
803
+ }
804
+ let editorHint = rawEditorHint;
805
+ if (rawEditorHint === "vscode") editorHint = "code";
806
+ else if (rawEditorHint === "vscode-insiders") editorHint = "code-insiders";
807
+ else if (rawEditorHint === "vscodium") editorHint = "codium";
808
+ else if (rawEditorHint === "trae-cn" || rawEditorHint === "trae") editorHint = "trae";
809
+ serverLogger2.debug(
810
+ `IDE_OPEN: activeIde=${activeIde}, activeIdeScheme=${activeIdeScheme}, configuredIde=${configuredIde} -> rawEditorHint=${rawEditorHint}, finalEditorHint=${editorHint}`
811
+ );
812
+ if (VSCODE_FAMILY_SCHEMES.includes(rawEditorHint)) {
813
+ let normalizedPath = absolutePath.replace(/\\/g, "/");
814
+ if (!normalizedPath.startsWith("/")) {
815
+ normalizedPath = "/" + normalizedPath;
816
+ }
817
+ const encodedPath = encodeURI(normalizedPath);
818
+ const uri = `${rawEditorHint}://file${encodedPath}:${body.line}:${body.column}`;
819
+ serverLogger2.debug(`IDE_OPEN: Bypassing launchIDE, using URI scheme directly: ${uri}`);
820
+ try {
821
+ if (process.platform === "darwin") {
822
+ execFileSync2("open", [uri]);
823
+ } else if (process.platform === "win32") {
824
+ execFileSync2("cmd", ["/c", "start", '""', uri]);
825
+ } else {
826
+ execFileSync2("xdg-open", [uri]);
827
+ }
828
+ } catch (e) {
829
+ serverLogger2.error(`Failed to launch URI for IDE_OPEN (${uri}):`, e);
830
+ launchIDE2({
831
+ file: absolutePath,
832
+ line: body.line,
833
+ column: body.column,
834
+ editor: editorHint,
835
+ type: process.platform === "darwin" ? "open" : "exec"
836
+ });
837
+ }
838
+ } else {
839
+ launchIDE2({
840
+ file: absolutePath,
841
+ line: body.line,
842
+ column: body.column,
843
+ editor: editorHint,
844
+ type: process.platform === "darwin" ? "open" : "exec"
845
+ });
846
+ }
847
+ return { success: true };
848
+ }
849
+
850
+ // src/server/project-root.ts
851
+ import fs3 from "fs";
852
+ import path5 from "path";
853
+ import { execSync } from "child_process";
854
+ var serverLogger3 = createLogger("inspecto:server", { logLevel: getGlobalLogLevel() });
518
855
  function resolveProjectRoot() {
519
856
  const cwd = process.cwd();
520
857
  let gitRoot;
521
858
  try {
522
859
  gitRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
523
860
  } catch (e) {
524
- serverLogger.warn("Failed to resolve git root via git rev-parse:", e);
861
+ serverLogger3.warn("Failed to resolve git root via git rev-parse:", e);
525
862
  gitRoot = cwd;
526
863
  }
527
864
  const visited = /* @__PURE__ */ new Set();
@@ -529,34 +866,31 @@ function resolveProjectRoot() {
529
866
  let current = start;
530
867
  while (!visited.has(current)) {
531
868
  visited.add(current);
532
- if (fs3.existsSync(path4.join(current, ".inspecto"))) return current;
869
+ if (fs3.existsSync(path5.join(current, ".inspecto"))) return current;
533
870
  if (current === stop) break;
534
- const parent = path4.dirname(current);
871
+ const parent = path5.dirname(current);
535
872
  if (parent === current) break;
536
873
  current = parent;
537
874
  }
538
875
  return null;
539
876
  };
540
- const cwdMatch = search(cwd, path4.parse(cwd).root);
877
+ const cwdMatch = search(cwd, path5.parse(cwd).root);
541
878
  if (cwdMatch) return cwdMatch;
542
- const repoMatch = search(gitRoot, path4.parse(gitRoot).root);
879
+ const repoMatch = search(gitRoot, path5.parse(gitRoot).root);
543
880
  if (repoMatch) return repoMatch;
544
881
  return gitRoot;
545
882
  }
546
- function launchURI(uri) {
547
- try {
548
- if (process.platform === "darwin") {
549
- execFileSync("open", [uri]);
550
- } else if (process.platform === "win32") {
551
- execFileSync("cmd", ["/c", "start", '""', uri]);
552
- } else {
553
- execFileSync("xdg-open", [uri]);
554
- }
555
- } catch (e) {
556
- serverLogger.error("Failed to launch URI via execFileSync, falling back to launchIDE:", e);
557
- launchIDE({ file: uri });
558
- }
559
- }
883
+
884
+ // src/server/index.ts
885
+ var serverLogger4 = createLogger("inspecto:server", { logLevel: getGlobalLogLevel() });
886
+ var serverState = {
887
+ port: null,
888
+ running: false,
889
+ projectRoot: "",
890
+ configRoot: "",
891
+ cwd: process.cwd()
892
+ };
893
+ var serverInstance = null;
560
894
  async function startServer() {
561
895
  if (serverState.running && serverState.port !== null) {
562
896
  return serverState.port;
@@ -568,7 +902,7 @@ async function startServer() {
568
902
  const port = await portfinder.getPortPromise();
569
903
  watchConfig(
570
904
  () => {
571
- serverLogger.info("user config reloaded.");
905
+ serverLogger4.info("user config reloaded.");
572
906
  },
573
907
  serverState.cwd,
574
908
  serverState.configRoot
@@ -584,7 +918,7 @@ async function startServer() {
584
918
  }
585
919
  const url = new URL(req.url ?? "/", `http://localhost:${port}`);
586
920
  handleRequest(url, req, res).catch((err) => {
587
- serverLogger.error("server error:", err);
921
+ serverLogger4.error("server error:", err);
588
922
  res.writeHead(500, { "Content-Type": "application/json" });
589
923
  res.end(JSON.stringify({ success: false, error: String(err) }));
590
924
  });
@@ -597,41 +931,41 @@ async function startServer() {
597
931
  serverInstance.once("error", reject);
598
932
  });
599
933
  serverInstance.on("error", (err) => {
600
- serverLogger.error("persistent server error:", err);
934
+ serverLogger4.error("persistent server error:", err);
601
935
  });
602
936
  serverState.port = port;
603
937
  serverState.running = true;
604
- const portFile = path4.join(os2.tmpdir(), "inspecto.port.json");
938
+ const portFile = path6.join(os2.tmpdir(), "inspecto.port.json");
605
939
  try {
606
940
  let portData = {};
607
- if (fs3.existsSync(portFile)) {
941
+ if (fs4.existsSync(portFile)) {
608
942
  try {
609
- portData = JSON.parse(fs3.readFileSync(portFile, "utf-8"));
943
+ portData = JSON.parse(fs4.readFileSync(portFile, "utf-8"));
610
944
  } catch (e) {
611
945
  }
612
946
  }
613
- const rootHash = crypto.createHash("md5").update(serverState.projectRoot).digest("hex");
947
+ const rootHash = crypto2.createHash("md5").update(serverState.projectRoot).digest("hex");
614
948
  portData[rootHash] = port;
615
- fs3.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
949
+ fs4.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
616
950
  } catch (e) {
617
- serverLogger.warn("Failed to write port file:", e);
951
+ serverLogger4.warn("Failed to write port file:", e);
618
952
  }
619
953
  process.once("exit", () => {
620
954
  try {
621
- if (fs3.existsSync(portFile)) {
622
- const portData = JSON.parse(fs3.readFileSync(portFile, "utf-8"));
623
- const rootHash = crypto.createHash("md5").update(serverState.projectRoot).digest("hex");
955
+ if (fs4.existsSync(portFile)) {
956
+ const portData = JSON.parse(fs4.readFileSync(portFile, "utf-8"));
957
+ const rootHash = crypto2.createHash("md5").update(serverState.projectRoot).digest("hex");
624
958
  delete portData[rootHash];
625
959
  if (Object.keys(portData).length === 0) {
626
- fs3.unlinkSync(portFile);
960
+ fs4.unlinkSync(portFile);
627
961
  } else {
628
- fs3.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
962
+ fs4.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
629
963
  }
630
964
  }
631
965
  } catch {
632
966
  }
633
967
  });
634
- serverLogger.info(`server running at http://127.0.0.1:${port}`);
968
+ serverLogger4.info(`server running at http://127.0.0.1:${port}`);
635
969
  return port;
636
970
  }
637
971
  async function readBody(req) {
@@ -650,26 +984,7 @@ async function handleRequest(url, req, res) {
650
984
  return;
651
985
  }
652
986
  if (pathname === INSPECTO_API_PATHS.CLIENT_CONFIG && req.method === "GET") {
653
- const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
654
- const promptsConfig = await loadPromptsConfig(false, serverState.cwd, serverState.configRoot);
655
- const effectiveIde = userConfig.ide ?? "vscode";
656
- let info;
657
- if (!serverState.ideInfo) {
658
- info = {
659
- ide: effectiveIde
660
- };
661
- } else {
662
- const { scheme: _scheme, ...rest } = serverState.ideInfo;
663
- info = rest;
664
- }
665
- const config = {
666
- ...info,
667
- prompts: resolveIntents(promptsConfig),
668
- hotKeys: userConfig["inspector.hotKey"] ?? "alt",
669
- theme: userConfig["inspector.theme"] ?? "auto",
670
- includeSnippet: userConfig["prompt.includeSnippet"] ?? false,
671
- autoSend: userConfig["prompt.autoSend"] ?? false
672
- };
987
+ const config = await buildClientConfig(serverState);
673
988
  delete config.providers;
674
989
  res.writeHead(200, { "Content-Type": "application/json" });
675
990
  res.end(JSON.stringify(config));
@@ -680,23 +995,23 @@ async function handleRequest(url, req, res) {
680
995
  const body = JSON.parse(await readBody(req));
681
996
  const ideWorkspace = body.workspaceRoot || "";
682
997
  const serverProjectRoot = serverState.projectRoot || "";
683
- const normalizedIdeRoot = ideWorkspace ? path4.resolve(ideWorkspace) : "";
684
- const normalizedServerRoot = serverProjectRoot ? path4.resolve(serverProjectRoot) : "";
685
- const isSameProject = !normalizedIdeRoot || !normalizedServerRoot || normalizedIdeRoot === normalizedServerRoot || normalizedServerRoot.startsWith(normalizedIdeRoot + path4.sep) || normalizedIdeRoot.startsWith(normalizedServerRoot + path4.sep);
998
+ const normalizedIdeRoot = ideWorkspace ? path6.resolve(ideWorkspace) : "";
999
+ const normalizedServerRoot = serverProjectRoot ? path6.resolve(serverProjectRoot) : "";
1000
+ const isSameProject = !normalizedIdeRoot || !normalizedServerRoot || normalizedIdeRoot === normalizedServerRoot || normalizedServerRoot.startsWith(normalizedIdeRoot + path6.sep) || normalizedIdeRoot.startsWith(normalizedServerRoot + path6.sep);
686
1001
  if (isSameProject) {
687
1002
  serverState.ideInfo = body;
688
- serverLogger.debug(
1003
+ serverLogger4.debug(
689
1004
  `Accepted IDE info from matched workspace (ide-${body.ide} / schema-${body.scheme})`
690
1005
  );
691
1006
  } else {
692
- serverLogger.debug(
1007
+ serverLogger4.debug(
693
1008
  `Ignored IDE info from unrelated workspace (IDE Workspace: ${ideWorkspace}, Server: ${serverProjectRoot}, Scheme: ${body.scheme}, IDE: ${body.ide})`
694
1009
  );
695
1010
  }
696
1011
  res.writeHead(200, { "Content-Type": "application/json" });
697
1012
  res.end(JSON.stringify({ success: true }));
698
1013
  } catch (e) {
699
- serverLogger.error(`Error parsing ${INSPECTO_API_PATHS.IDE_INFO} POST request:`, e);
1014
+ serverLogger4.error(`Error parsing ${INSPECTO_API_PATHS.IDE_INFO} POST request:`, e);
700
1015
  res.writeHead(400, { "Content-Type": "application/json" });
701
1016
  res.end(JSON.stringify({ error: "Invalid JSON body" }));
702
1017
  }
@@ -711,79 +1026,14 @@ async function handleRequest(url, req, res) {
711
1026
  res.end(JSON.stringify({ error: "Invalid JSON body" }));
712
1027
  return;
713
1028
  }
714
- const absolutePath = path4.isAbsolute(body.file) ? path4.resolve(body.file) : path4.resolve(serverState.cwd, body.file);
715
- const relativeToRoot = path4.relative(serverState.projectRoot, absolutePath);
716
- if (relativeToRoot.startsWith("..") || path4.isAbsolute(relativeToRoot)) {
717
- serverLogger.warn(`Security: Blocked path traversal attempt in IDE_OPEN: ${body.file}`);
1029
+ try {
1030
+ handleOpenFileRequest(body, serverState);
1031
+ } catch {
1032
+ serverLogger4.warn(`Security: Blocked path traversal attempt in IDE_OPEN: ${body.file}`);
718
1033
  res.writeHead(403, { "Content-Type": "application/json" });
719
1034
  res.end(JSON.stringify({ error: "Access denied: File is outside of project workspace" }));
720
1035
  return;
721
1036
  }
722
- const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
723
- const configuredIde = userConfig.ide;
724
- const activeIde = serverState.ideInfo?.ide;
725
- const activeIdeScheme = serverState.ideInfo?.scheme;
726
- const rawEditorHint = configuredIde || activeIde || activeIdeScheme || "code";
727
- if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
728
- serverLogger.warn(
729
- `Active IDE is ${activeIdeScheme}, but config forces ${configuredIde}. Using configured IDE.`
730
- );
731
- }
732
- let editorHint = rawEditorHint;
733
- if (rawEditorHint === "vscode") editorHint = "code";
734
- else if (rawEditorHint === "vscode-insiders") editorHint = "code-insiders";
735
- else if (rawEditorHint === "vscodium") editorHint = "codium";
736
- else if (rawEditorHint === "trae-cn" || rawEditorHint === "trae") editorHint = "trae";
737
- serverLogger.debug(
738
- `IDE_OPEN: activeIde=${activeIde}, activeIdeScheme=${activeIdeScheme}, configuredIde=${configuredIde} -> rawEditorHint=${rawEditorHint}, finalEditorHint=${editorHint}`
739
- );
740
- const VSCODE_FAMILY_SCHEMES = [
741
- "vscode",
742
- "vscode-insiders",
743
- "cursor",
744
- "windsurf",
745
- "trae",
746
- "trae-cn",
747
- "vscodium",
748
- "codebuddy",
749
- "codebuddy-cn",
750
- "antigravity"
751
- ];
752
- if (VSCODE_FAMILY_SCHEMES.includes(rawEditorHint)) {
753
- let normalizedPath = absolutePath.replace(/\\/g, "/");
754
- if (!normalizedPath.startsWith("/")) {
755
- normalizedPath = "/" + normalizedPath;
756
- }
757
- const encodedPath = encodeURI(normalizedPath);
758
- const uri = `${rawEditorHint}://file${encodedPath}:${body.line}:${body.column}`;
759
- serverLogger.debug(`IDE_OPEN: Bypassing launchIDE, using URI scheme directly: ${uri}`);
760
- try {
761
- if (process.platform === "darwin") {
762
- execFileSync("open", [uri]);
763
- } else if (process.platform === "win32") {
764
- execFileSync("cmd", ["/c", "start", '""', uri]);
765
- } else {
766
- execFileSync("xdg-open", [uri]);
767
- }
768
- } catch (e) {
769
- serverLogger.error(`Failed to launch URI for IDE_OPEN (${uri}):`, e);
770
- launchIDE({
771
- file: absolutePath,
772
- line: body.line,
773
- column: body.column,
774
- editor: editorHint,
775
- type: process.platform === "darwin" ? "open" : "exec"
776
- });
777
- }
778
- } else {
779
- launchIDE({
780
- file: absolutePath,
781
- line: body.line,
782
- column: body.column,
783
- editor: editorHint,
784
- type: process.platform === "darwin" ? "open" : "exec"
785
- });
786
- }
787
1037
  res.writeHead(200, { "Content-Type": "application/json" });
788
1038
  res.end(JSON.stringify({ success: true }));
789
1039
  return;
@@ -794,10 +1044,11 @@ async function handleRequest(url, req, res) {
794
1044
  const column = parseInt(url.searchParams.get("column") ?? "1", 10);
795
1045
  const maxLines = parseInt(url.searchParams.get("maxLines") ?? "100", 10);
796
1046
  try {
797
- const absolutePath = path4.isAbsolute(file) ? path4.resolve(file) : path4.resolve(serverState.cwd, file);
798
- const relativeToRoot = path4.relative(serverState.projectRoot, absolutePath);
799
- if (relativeToRoot.startsWith("..") || path4.isAbsolute(relativeToRoot)) {
800
- serverLogger.warn(`Security: Blocked path traversal attempt in PROJECT_SNIPPET: ${file}`);
1047
+ const absolutePath = resolveWorkspacePath(file, serverState.cwd);
1048
+ try {
1049
+ assertPathWithinProject(absolutePath, serverState.projectRoot);
1050
+ } catch {
1051
+ serverLogger4.warn(`Security: Blocked path traversal attempt in PROJECT_SNIPPET: ${file}`);
801
1052
  res.writeHead(403, { "Content-Type": "application/json" });
802
1053
  res.end(
803
1054
  JSON.stringify({
@@ -827,7 +1078,23 @@ async function handleRequest(url, req, res) {
827
1078
  res.writeHead(result.success ? 200 : 500, { "Content-Type": "application/json" });
828
1079
  res.end(JSON.stringify(result));
829
1080
  } catch (e) {
830
- serverLogger.error(`Error parsing ${INSPECTO_API_PATHS.AI_DISPATCH} request:`, e);
1081
+ serverLogger4.error(`Error parsing ${INSPECTO_API_PATHS.AI_DISPATCH} request:`, e);
1082
+ res.writeHead(500, { "Content-Type": "application/json" });
1083
+ res.end(JSON.stringify({ success: false, error: String(e), errorCode: "INTERNAL_ERROR" }));
1084
+ }
1085
+ return;
1086
+ }
1087
+ if (pathname === INSPECTO_API_PATHS.AI_BATCH_DISPATCH && req.method === "POST") {
1088
+ try {
1089
+ const rawBody = await readBody(req);
1090
+ const body = JSON.parse(rawBody);
1091
+ const result = await dispatchAnnotationsToAi(body, serverState);
1092
+ res.writeHead(getBatchDispatchStatusCode(result.errorCode, result.success), {
1093
+ "Content-Type": "application/json"
1094
+ });
1095
+ res.end(JSON.stringify(result));
1096
+ } catch (e) {
1097
+ serverLogger4.error(`Error parsing ${INSPECTO_API_PATHS.AI_BATCH_DISPATCH} request:`, e);
831
1098
  res.writeHead(500, { "Content-Type": "application/json" });
832
1099
  res.end(JSON.stringify({ success: false, error: String(e), errorCode: "INTERNAL_ERROR" }));
833
1100
  }
@@ -835,7 +1102,7 @@ async function handleRequest(url, req, res) {
835
1102
  }
836
1103
  if (pathname.startsWith(`${INSPECTO_API_PATHS.AI_TICKET}/`) && req.method === "GET") {
837
1104
  const ticketId = pathname.substring(INSPECTO_API_PATHS.AI_TICKET.length + 1);
838
- const payloadStr = payloadTickets.get(ticketId);
1105
+ const payloadStr = readTicket(ticketId);
839
1106
  if (!payloadStr) {
840
1107
  res.writeHead(404, { "Content-Type": "application/json" });
841
1108
  res.end(JSON.stringify({ success: false, error: "Ticket not found or expired" }));
@@ -849,58 +1116,32 @@ async function handleRequest(url, req, res) {
849
1116
  res.end(JSON.stringify({ error: "not found" }));
850
1117
  }
851
1118
  async function dispatchToAi(req) {
852
- const { location, snippet, prompt } = req;
853
- const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
854
- const resolvedTarget = resolveTargetTool(userConfig);
1119
+ const { location, snippet, prompt, screenshotContext } = req;
855
1120
  const formattedPrompt = prompt ?? `Please help me with this code from \`${location.file}\` (line ${location.line}):
856
1121
 
857
1122
  \`\`\`
858
1123
  ${snippet}
859
1124
  \`\`\`
860
1125
  `;
861
- const ideReportedMode = serverState.ideInfo?.providers[resolvedTarget]?.mode;
862
- const configuredIde = userConfig.ide;
863
- const activeIde = serverState.ideInfo?.ide;
864
- const activeIdeScheme = serverState.ideInfo?.scheme;
865
- const finalIde = configuredIde || activeIdeScheme || activeIde || "vscode";
866
- if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
867
- serverLogger.warn(
868
- `dispatchToAi: Active IDE is ${activeIdeScheme}, but config forces ${configuredIde}. Using configured IDE.`
869
- );
870
- }
871
- const mode = resolveProviderMode(resolvedTarget, finalIde, userConfig);
872
- const overrides = extractToolOverrides(finalIde, userConfig)[resolvedTarget] || {};
873
- overrides.type = mode;
874
- const fullPayload = {
875
- ide: finalIde,
876
- target: resolvedTarget,
877
- targetType: mode,
1126
+ const runtime = resolvePromptDispatchRuntime(serverState);
1127
+ return dispatchPromptThroughIde(runtime, {
878
1128
  prompt: formattedPrompt,
879
1129
  filePath: location.file,
880
1130
  line: location.line,
881
1131
  column: location.column,
882
1132
  snippet,
883
- overrides: Object.keys(overrides).length > 0 ? overrides : void 0,
884
- autoSend: userConfig["prompt.autoSend"] !== void 0 ? Boolean(userConfig["prompt.autoSend"]) : void 0
885
- };
886
- const ticketId = createTicket(fullPayload);
887
- const params = new URLSearchParams();
888
- params.set("ticket", ticketId);
889
- params.set("target", resolvedTarget);
890
- const uri = `${finalIde}://inspecto.inspecto/send?${params.toString()}`;
891
- serverLogger.debug(`dispatchToAi: Generated URI: ${uri}`);
892
- launchURI(uri);
893
- return {
894
- success: true,
895
- fallbackPayload: {
896
- prompt: formattedPrompt,
897
- file: location.file
898
- }
899
- };
1133
+ ...screenshotContext ? { screenshotContext } : {}
1134
+ });
1135
+ }
1136
+ function getBatchDispatchStatusCode(errorCode, success) {
1137
+ if (success) return 200;
1138
+ if (errorCode === "INVALID_REQUEST") return 400;
1139
+ if (errorCode === "FORBIDDEN_PATH") return 403;
1140
+ return 500;
900
1141
  }
901
1142
 
902
1143
  // src/legacy/webpack4/index.ts
903
- import path5 from "path";
1144
+ import path7 from "path";
904
1145
  var InspectoWebpack4Plugin = class {
905
1146
  constructor(options = {}) {
906
1147
  this.options = options;
@@ -908,7 +1149,7 @@ var InspectoWebpack4Plugin = class {
908
1149
  apply(compiler) {
909
1150
  const clientPath = resolveClientModule();
910
1151
  compiler.hooks.afterEnvironment.tap("InspectoWebpack4Plugin", () => {
911
- const inspectoLoader = path5.resolve(__dirname, "loader.cjs");
1152
+ const inspectoLoader = path7.resolve(__dirname, "loader.cjs");
912
1153
  compiler.options.module.rules.push({
913
1154
  test: /\.[jt]sx?$/,
914
1155
  enforce: "pre",