@inspecto-dev/plugin 0.2.0-alpha.0 → 0.2.0-alpha.2

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.
Files changed (44) hide show
  1. package/README.md +19 -0
  2. package/dist/index.cjs +431 -102
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.js +436 -103
  7. package/dist/index.js.map +1 -1
  8. package/dist/legacy/rspack/index.cjs +402 -98
  9. package/dist/legacy/rspack/index.cjs.map +1 -1
  10. package/dist/legacy/rspack/index.js +407 -99
  11. package/dist/legacy/rspack/index.js.map +1 -1
  12. package/dist/legacy/rspack/loader.cjs +8 -1
  13. package/dist/legacy/rspack/loader.cjs.map +1 -1
  14. package/dist/legacy/rspack/loader.js +8 -1
  15. package/dist/legacy/rspack/loader.js.map +1 -1
  16. package/dist/legacy/webpack4/index.cjs +977 -0
  17. package/dist/legacy/webpack4/index.cjs.map +1 -0
  18. package/dist/legacy/webpack4/index.d.cts +8 -0
  19. package/dist/legacy/webpack4/index.d.ts +8 -0
  20. package/dist/legacy/webpack4/index.js +953 -0
  21. package/dist/legacy/webpack4/index.js.map +1 -0
  22. package/dist/legacy/webpack4/loader.cjs +347 -0
  23. package/dist/legacy/webpack4/loader.cjs.map +1 -0
  24. package/dist/legacy/webpack4/loader.d.cts +3 -0
  25. package/dist/legacy/webpack4/loader.d.ts +3 -0
  26. package/dist/legacy/webpack4/loader.js +314 -0
  27. package/dist/legacy/webpack4/loader.js.map +1 -0
  28. package/dist/rollup.cjs +431 -102
  29. package/dist/rollup.cjs.map +1 -1
  30. package/dist/rollup.js +436 -103
  31. package/dist/rollup.js.map +1 -1
  32. package/dist/rspack.cjs +431 -102
  33. package/dist/rspack.cjs.map +1 -1
  34. package/dist/rspack.js +436 -103
  35. package/dist/rspack.js.map +1 -1
  36. package/dist/vite.cjs +431 -102
  37. package/dist/vite.cjs.map +1 -1
  38. package/dist/vite.js +436 -103
  39. package/dist/vite.js.map +1 -1
  40. package/dist/webpack.cjs +431 -102
  41. package/dist/webpack.cjs.map +1 -1
  42. package/dist/webpack.js +436 -103
  43. package/dist/webpack.js.map +1 -1
  44. package/package.json +19 -3
@@ -45,9 +45,11 @@ var import_node_http = __toESM(require("http"), 1);
45
45
  var import_node_fs2 = __toESM(require("fs"), 1);
46
46
  var import_node_path2 = __toESM(require("path"), 1);
47
47
  var import_node_os2 = __toESM(require("os"), 1);
48
+ var import_node_crypto = __toESM(require("crypto"), 1);
48
49
  var import_node_child_process = require("child_process");
49
50
  var import_portfinder = __toESM(require("portfinder"), 1);
50
51
  var import_launch_ide = require("launch-ide");
52
+ var import_types2 = require("@inspecto-dev/types");
51
53
 
52
54
  // src/server/snippet.ts
53
55
  var fs = __toESM(require("fs"), 1);
@@ -163,8 +165,71 @@ var import_node_path = __toESM(require("path"), 1);
163
165
  var import_node_os = __toESM(require("os"), 1);
164
166
  var import_defu = require("defu");
165
167
  var import_types = require("@inspecto-dev/types");
168
+
169
+ // src/utils/logger.ts
170
+ var LOG_LEVELS = {
171
+ silent: 0,
172
+ error: 1,
173
+ warn: 2,
174
+ info: 3
175
+ };
176
+ function isDebugEnabled(namespace) {
177
+ if (typeof process === "undefined" || !process.env) return false;
178
+ const debugEnv = process.env.DEBUG;
179
+ if (!debugEnv) return false;
180
+ const namespaces = debugEnv.split(",").map((s) => s.trim());
181
+ for (const ns of namespaces) {
182
+ if (ns === "*") return true;
183
+ if (ns.endsWith("*")) {
184
+ const prefix = ns.slice(0, -1);
185
+ if (namespace.startsWith(prefix)) return true;
186
+ } else if (ns === namespace) {
187
+ return true;
188
+ }
189
+ }
190
+ return false;
191
+ }
192
+ var globalLevel = "warn";
193
+ var registeredLoggers = /* @__PURE__ */ new Set();
194
+ function createLogger(namespace, options) {
195
+ let currentLevel = options?.logLevel ?? globalLevel;
196
+ let numericLevel = LOG_LEVELS[currentLevel] ?? 2;
197
+ const debugEnabled = isDebugEnabled(namespace);
198
+ const logger = {
199
+ setLevel(level) {
200
+ currentLevel = level;
201
+ numericLevel = LOG_LEVELS[level] ?? 2;
202
+ },
203
+ info(msg, ...args) {
204
+ if (numericLevel >= LOG_LEVELS.info) {
205
+ console.log(`\x1B[36m[inspecto]\x1B[0m ${msg}`, ...args);
206
+ }
207
+ },
208
+ warn(msg, ...args) {
209
+ if (numericLevel >= LOG_LEVELS.warn) {
210
+ console.warn(`\x1B[33m[inspecto] WARN:\x1B[0m ${msg}`, ...args);
211
+ }
212
+ },
213
+ error(msg, ...args) {
214
+ if (numericLevel >= LOG_LEVELS.error) {
215
+ console.error(`\x1B[31m[inspecto] ERROR:\x1B[0m ${msg}`, ...args);
216
+ }
217
+ },
218
+ debug(msg, ...args) {
219
+ if (debugEnabled) {
220
+ console.log(`\x1B[90m[${namespace}]\x1B[0m ${msg}`, ...args);
221
+ }
222
+ }
223
+ };
224
+ registeredLoggers.add(logger);
225
+ return logger;
226
+ }
227
+
228
+ // src/config.ts
229
+ var configLogger = createLogger("inspecto:config");
166
230
  var loadedConfig = null;
167
231
  var loadedPrompts = null;
232
+ var globalLogLevel = "warn";
168
233
  var isWatching = false;
169
234
  var arrayReplaceMerge = (0, import_defu.createDefu)((obj, key, val) => {
170
235
  if (Array.isArray(val)) {
@@ -172,6 +237,9 @@ var arrayReplaceMerge = (0, import_defu.createDefu)((obj, key, val) => {
172
237
  return true;
173
238
  }
174
239
  });
240
+ function getGlobalLogLevel() {
241
+ return globalLogLevel;
242
+ }
175
243
  function resolveConfigRoots(cwd, gitRoot) {
176
244
  const roots = [];
177
245
  let current = cwd;
@@ -201,7 +269,7 @@ function loadUserConfigSync(force = false, cwd = process.cwd(), gitRoot) {
201
269
  layers.push(readJsonSafely(import_node_path.default.join(root, ".inspecto", "settings.json")));
202
270
  }
203
271
  layers.push(readJsonSafely(import_node_path.default.join(import_node_os.default.homedir(), ".inspecto", "settings.json")));
204
- layers.push({ providers: {} });
272
+ layers.push({});
205
273
  const validLayers = layers.filter((l) => l !== null);
206
274
  loadedConfig = arrayReplaceMerge(...validLayers);
207
275
  return loadedConfig;
@@ -244,48 +312,145 @@ function readJsonSafely(filePath) {
244
312
  }
245
313
  } catch (e) {
246
314
  if (e instanceof SyntaxError) {
247
- console.warn(`[inspecto] Failed to parse config at ${filePath}: Invalid JSON`);
315
+ configLogger.warn(`Failed to parse config at ${filePath}: Invalid JSON`);
248
316
  } else {
249
- console.warn(`[inspecto] Failed to read config at ${filePath}:`, e);
317
+ configLogger.warn(`Failed to read config at ${filePath}:`, e);
250
318
  }
251
319
  }
252
320
  return null;
253
321
  }
254
- function resolveTargetTool(config) {
255
- if (config.prefer) {
256
- return config.prefer;
257
- }
258
- if (config.providers && Object.keys(config.providers).length > 0) {
259
- return Object.keys(config.providers)[0];
322
+ function resolveTargetTool(config, ide = "vscode") {
323
+ const defaultProvider = config["provider.default"];
324
+ if (defaultProvider) {
325
+ const tool = defaultProvider.split(".")[0];
326
+ return tool;
260
327
  }
261
- return "github-copilot";
328
+ return "copilot";
262
329
  }
263
- function resolveToolMode(tool, ide, config) {
330
+ function resolveProviderMode(tool, ide, config) {
264
331
  let requestedType = void 0;
265
- if (config.providers && config.providers[tool] && config.providers[tool].type) {
266
- const type = config.providers[tool].type;
267
- if (type === "plugin" || type === "cli") {
268
- requestedType = type;
269
- }
270
- }
271
- requestedType = requestedType ?? import_types.DEFAULT_TOOL_MODE[tool];
272
- const valid = import_types.VALID_MODES[tool] || [import_types.DEFAULT_TOOL_MODE[tool]];
332
+ const defaultProvider = config["provider.default"];
333
+ if (defaultProvider && defaultProvider.startsWith(`${tool}.`)) {
334
+ const mode = defaultProvider.split(".")[1];
335
+ if (mode === "extension") requestedType = "extension";
336
+ if (mode === "cli") requestedType = "cli";
337
+ }
338
+ requestedType = requestedType ?? import_types.DEFAULT_PROVIDER_MODE[tool];
339
+ const valid = import_types.VALID_MODES[tool] || [import_types.DEFAULT_PROVIDER_MODE[tool]];
273
340
  return requestedType && valid.includes(requestedType) ? requestedType : valid[0];
274
341
  }
275
342
  function extractToolOverrides(ide, config) {
276
343
  const result = {};
277
- if (!config.providers) return result;
278
- for (const [tool, cfg] of Object.entries(config.providers)) {
279
- if (!cfg) continue;
280
- const overrides = {
281
- type: cfg.type || import_types.DEFAULT_TOOL_MODE[tool] || "plugin"
282
- };
283
- if (cfg.bin) overrides.binaryPath = cfg.bin;
284
- if (cfg.args) overrides.args = cfg.args;
285
- if (cfg.cwd) overrides.cwd = cfg.cwd;
286
- if (cfg.autoSend !== void 0) overrides.autoSend = cfg.autoSend;
287
- result[tool] = overrides;
344
+ if (!config) return result;
345
+ for (const [key, value] of Object.entries(config)) {
346
+ if (!key.startsWith("provider.")) continue;
347
+ if (key === "provider.default") continue;
348
+ const toolIndex = 1;
349
+ const modeIndex = 2;
350
+ const propIndex = 3;
351
+ const parts = key.split(".");
352
+ if (parts.length >= propIndex + 1) {
353
+ const tool = parts[toolIndex];
354
+ const mode = parts[modeIndex];
355
+ const prop = parts[propIndex];
356
+ if (!result[tool]) {
357
+ result[tool] = { type: mode };
358
+ }
359
+ const overrides = result[tool];
360
+ if (prop === "bin") overrides.binaryPath = value;
361
+ if (prop === "args") overrides.args = value;
362
+ if (prop === "cwd") overrides.cwd = value;
363
+ if (prop === "coldStartDelay") overrides.coldStartDelay = value;
364
+ }
365
+ }
366
+ return result;
367
+ }
368
+ function resolveIntents(serverPrompts) {
369
+ const baseMap = /* @__PURE__ */ new Map();
370
+ for (const intent of import_types.DEFAULT_INTENTS) {
371
+ if (intent.id) baseMap.set(intent.id, { ...intent });
372
+ }
373
+ const defaults = () => ensureOpenInEditorLast(Array.from(baseMap.values()));
374
+ if (!serverPrompts) return defaults();
375
+ const isReplace = !Array.isArray(serverPrompts) && typeof serverPrompts === "object" && serverPrompts.$replace === true;
376
+ const promptsArray = Array.isArray(serverPrompts) ? serverPrompts : isReplace ? serverPrompts.items : [];
377
+ if (!promptsArray || promptsArray.length === 0) return defaults();
378
+ if (isReplace) {
379
+ const result = [];
380
+ for (const item of promptsArray) {
381
+ if (typeof item === "string") {
382
+ if (baseMap.has(item)) {
383
+ result.push(baseMap.get(item));
384
+ } else {
385
+ configLogger.warn(
386
+ `Unknown built-in intent id: "${item}". Available: ${[...baseMap.keys()].join(", ")}`
387
+ );
388
+ }
389
+ } else if (typeof item === "object") {
390
+ if (!item.id) {
391
+ configLogger.warn('Intent object missing required "id" field, skipping.');
392
+ continue;
393
+ }
394
+ if (item.enabled === false) {
395
+ configLogger.warn(
396
+ `Intent "${item.id}" is listed in $replace but has enabled:false \u2014 it will be excluded.`
397
+ );
398
+ continue;
399
+ }
400
+ if (item.isAction && item.id !== "open-in-editor") {
401
+ configLogger.warn(
402
+ `isAction is reserved for built-in actions. Ignoring intent "${item.id}".`
403
+ );
404
+ continue;
405
+ }
406
+ result.push(baseMap.has(item.id) ? { ...baseMap.get(item.id), ...item } : item);
407
+ }
408
+ }
409
+ return ensureOpenInEditorLast(result);
410
+ }
411
+ const merged = Array.from(baseMap.values());
412
+ for (const item of promptsArray) {
413
+ if (typeof item === "string") {
414
+ if (!baseMap.has(item)) {
415
+ configLogger.warn(
416
+ `Unknown built-in intent id: "${item}". In append mode, strings have no effect on ordering \u2014 use $replace to control order.`
417
+ );
418
+ }
419
+ continue;
420
+ }
421
+ if (typeof item === "object") {
422
+ if (!item.id) {
423
+ configLogger.warn('Intent object missing required "id" field, skipping.');
424
+ continue;
425
+ }
426
+ if (item.isAction && item.id !== "open-in-editor") {
427
+ configLogger.warn(
428
+ `isAction is reserved for built-in actions. Ignoring intent "${item.id}".`
429
+ );
430
+ continue;
431
+ }
432
+ const existingIdx = merged.findIndex((i) => i.id === item.id);
433
+ if (existingIdx !== -1) {
434
+ if (item.enabled === false) {
435
+ merged.splice(existingIdx, 1);
436
+ } else {
437
+ merged[existingIdx] = { ...merged[existingIdx], ...item };
438
+ }
439
+ } else {
440
+ if (item.enabled !== false) {
441
+ merged.push(item);
442
+ }
443
+ }
444
+ }
288
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);
289
454
  return result;
290
455
  }
291
456
  var watchers = [];
@@ -322,6 +487,19 @@ function watchConfig(onReload, cwd = process.cwd(), gitRoot) {
322
487
  }
323
488
 
324
489
  // src/server/index.ts
490
+ var serverLogger = createLogger("inspecto:server", { logLevel: getGlobalLogLevel() });
491
+ var payloadTickets = /* @__PURE__ */ new Map();
492
+ function createTicket(payload) {
493
+ const ticketId = import_node_crypto.default.randomUUID();
494
+ payloadTickets.set(ticketId, JSON.stringify(payload));
495
+ setTimeout(
496
+ () => {
497
+ payloadTickets.delete(ticketId);
498
+ },
499
+ 5 * 60 * 1e3
500
+ );
501
+ return ticketId;
502
+ }
325
503
  var serverState = {
326
504
  port: null,
327
505
  running: false,
@@ -333,8 +511,11 @@ var serverInstance = null;
333
511
  function resolveProjectRoot() {
334
512
  let gitRoot;
335
513
  try {
514
+ serverLogger.info("Resolving project root...");
336
515
  gitRoot = (0, import_node_child_process.execSync)("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
337
- } catch {
516
+ serverLogger.info("Resolved project root: " + gitRoot);
517
+ } catch (e) {
518
+ serverLogger.error("Failed to resolve project root:", e);
338
519
  gitRoot = process.cwd();
339
520
  }
340
521
  let current = gitRoot;
@@ -356,7 +537,7 @@ function launchURI(uri) {
356
537
  (0, import_node_child_process.execFileSync)("xdg-open", [uri]);
357
538
  }
358
539
  } catch (e) {
359
- console.error("[inspecto] Failed to launch URI via execFileSync, falling back to launchIDE:", e);
540
+ serverLogger.error("Failed to launch URI via execFileSync, falling back to launchIDE:", e);
360
541
  (0, import_launch_ide.launchIDE)({ file: uri });
361
542
  }
362
543
  }
@@ -371,7 +552,7 @@ async function startServer() {
371
552
  const port = await import_portfinder.default.getPortPromise();
372
553
  watchConfig(
373
554
  () => {
374
- console.log("[inspecto] user config reloaded.");
555
+ serverLogger.info("user config reloaded.");
375
556
  },
376
557
  serverState.cwd,
377
558
  serverState.configRoot
@@ -387,7 +568,7 @@ async function startServer() {
387
568
  }
388
569
  const url = new URL(req.url ?? "/", `http://localhost:${port}`);
389
570
  handleRequest(url, req, res).catch((err) => {
390
- console.error("[inspecto] server error:", err);
571
+ serverLogger.error("server error:", err);
391
572
  res.writeHead(500, { "Content-Type": "application/json" });
392
573
  res.end(JSON.stringify({ success: false, error: String(err) }));
393
574
  });
@@ -400,22 +581,41 @@ async function startServer() {
400
581
  serverInstance.once("error", reject);
401
582
  });
402
583
  serverInstance.on("error", (err) => {
403
- console.error("[inspecto] persistent server error:", err);
584
+ serverLogger.error("persistent server error:", err);
404
585
  });
405
586
  serverState.port = port;
406
587
  serverState.running = true;
407
- const portFile = import_node_path2.default.join(import_node_os2.default.tmpdir(), "inspecto.port");
588
+ const portFile = import_node_path2.default.join(import_node_os2.default.tmpdir(), "inspecto.port.json");
408
589
  try {
409
- import_node_fs2.default.writeFileSync(portFile, String(port), "utf-8");
410
- } catch {
590
+ let portData = {};
591
+ if (import_node_fs2.default.existsSync(portFile)) {
592
+ try {
593
+ portData = JSON.parse(import_node_fs2.default.readFileSync(portFile, "utf-8"));
594
+ } catch (e) {
595
+ }
596
+ }
597
+ const rootHash = import_node_crypto.default.createHash("md5").update(serverState.projectRoot).digest("hex");
598
+ portData[rootHash] = port;
599
+ import_node_fs2.default.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
600
+ } catch (e) {
601
+ serverLogger.warn("Failed to write port file:", e);
411
602
  }
412
603
  process.once("exit", () => {
413
604
  try {
414
- import_node_fs2.default.unlinkSync(portFile);
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");
608
+ delete portData[rootHash];
609
+ if (Object.keys(portData).length === 0) {
610
+ import_node_fs2.default.unlinkSync(portFile);
611
+ } else {
612
+ import_node_fs2.default.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
613
+ }
614
+ }
415
615
  } catch {
416
616
  }
417
617
  });
418
- console.log(`[inspecto] server running at http://127.0.0.1:${port}`);
618
+ serverLogger.info(`server running at http://127.0.0.1:${port}`);
419
619
  return port;
420
620
  }
421
621
  async function readBody(req) {
@@ -428,66 +628,63 @@ async function readBody(req) {
428
628
  }
429
629
  async function handleRequest(url, req, res) {
430
630
  const pathname = url.pathname;
431
- if (pathname === "/health" && req.method === "GET") {
631
+ if ((pathname === "/health" || pathname === import_types2.INSPECTO_API_PATHS.HEALTH) && req.method === "GET") {
432
632
  res.writeHead(200, { "Content-Type": "application/json" });
433
633
  res.end(JSON.stringify({ ok: true, port: serverState.port }));
434
634
  return;
435
635
  }
436
- if (pathname === "/config" && req.method === "GET") {
636
+ if (pathname === import_types2.INSPECTO_API_PATHS.CLIENT_CONFIG && req.method === "GET") {
437
637
  const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
438
638
  const promptsConfig = await loadPromptsConfig(false, serverState.cwd, serverState.configRoot);
439
639
  const effectiveIde = userConfig.ide ?? "vscode";
440
640
  let info;
441
641
  if (!serverState.ideInfo) {
442
- const fallbackTargets = userConfig.providers ? Object.keys(userConfig.providers) : ["claude-code", "gemini", "coco", "codex"];
443
642
  info = {
444
- ide: effectiveIde,
445
- providers: fallbackTargets.reduce(
446
- (acc, target) => {
447
- acc[target] = {
448
- mode: resolveToolMode(target, effectiveIde, userConfig),
449
- installed: false
450
- };
451
- return acc;
452
- },
453
- {}
454
- )
643
+ ide: effectiveIde
455
644
  };
456
645
  } else {
457
646
  const { scheme: _scheme, ...rest } = serverState.ideInfo;
458
647
  info = rest;
459
648
  }
460
- const resolvedProviders = { ...info.providers };
461
- for (const tool in resolvedProviders) {
462
- resolvedProviders[tool].mode = resolveToolMode(tool, info.ide, userConfig);
463
- }
464
649
  const config = {
465
650
  ...info,
466
- providers: resolvedProviders,
467
- providerOverrides: extractToolOverrides(info.ide, userConfig),
468
- prompts: promptsConfig,
469
- hotKeys: userConfig.hotKeys,
470
- includeSnippet: userConfig.includeSnippet
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
471
656
  };
657
+ delete config.providers;
472
658
  res.writeHead(200, { "Content-Type": "application/json" });
473
659
  res.end(JSON.stringify(config));
474
660
  return;
475
661
  }
476
- if (pathname === "/config" && req.method === "POST") {
662
+ if (pathname === import_types2.INSPECTO_API_PATHS.IDE_INFO && req.method === "POST") {
477
663
  try {
478
664
  const body = JSON.parse(await readBody(req));
479
- serverState.ideInfo = body;
480
- console.log(`[inspecto] Received IDE info from extension:`, body);
665
+ const ideWorkspace = body.workspaceRoot || "";
666
+ const serverProjectRoot = serverState.projectRoot || "";
667
+ const isSameProject = !ideWorkspace || !serverProjectRoot || ideWorkspace === serverProjectRoot || serverProjectRoot.startsWith(ideWorkspace);
668
+ if (isSameProject) {
669
+ serverState.ideInfo = body;
670
+ serverLogger.debug(
671
+ `Accepted IDE info from matched workspace (ide-${body.ide} / schema-${body.scheme})`
672
+ );
673
+ } else {
674
+ serverLogger.debug(
675
+ `Ignored IDE info from unrelated workspace (IDE Workspace: ${ideWorkspace}, Server: ${serverProjectRoot}, Scheme: ${body.scheme}, IDE: ${body.ide})`
676
+ );
677
+ }
481
678
  res.writeHead(200, { "Content-Type": "application/json" });
482
679
  res.end(JSON.stringify({ success: true }));
483
680
  } catch (e) {
484
- console.error("[inspecto] Error parsing /config POST request:", e);
681
+ serverLogger.error(`Error parsing ${import_types2.INSPECTO_API_PATHS.IDE_INFO} POST request:`, e);
485
682
  res.writeHead(400, { "Content-Type": "application/json" });
486
683
  res.end(JSON.stringify({ error: "Invalid JSON body" }));
487
684
  }
488
685
  return;
489
686
  }
490
- if (pathname === "/open" && req.method === "POST") {
687
+ if (pathname === import_types2.INSPECTO_API_PATHS.IDE_OPEN && req.method === "POST") {
491
688
  let body;
492
689
  try {
493
690
  body = JSON.parse(await readBody(req));
@@ -496,28 +693,98 @@ async function handleRequest(url, req, res) {
496
693
  res.end(JSON.stringify({ error: "Invalid JSON body" }));
497
694
  return;
498
695
  }
499
- const absolutePath = import_node_path2.default.isAbsolute(body.file) ? body.file : import_node_path2.default.resolve(serverState.cwd, body.file);
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}`);
700
+ res.writeHead(403, { "Content-Type": "application/json" });
701
+ res.end(JSON.stringify({ error: "Access denied: File is outside of project workspace" }));
702
+ return;
703
+ }
500
704
  const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
501
- const ide = userConfig.ide ?? "vscode";
502
- const editorHint = "code";
503
- (0, import_launch_ide.launchIDE)({
504
- file: absolutePath,
505
- line: body.line,
506
- column: body.column,
507
- editor: editorHint,
508
- type: process.platform === "darwin" ? "open" : "exec"
509
- });
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
+ }
510
764
  res.writeHead(200, { "Content-Type": "application/json" });
511
765
  res.end(JSON.stringify({ success: true }));
512
766
  return;
513
767
  }
514
- if (pathname === "/snippet" && req.method === "GET") {
768
+ if (pathname === import_types2.INSPECTO_API_PATHS.PROJECT_SNIPPET && req.method === "GET") {
515
769
  const file = url.searchParams.get("file") ?? "";
516
770
  const line = parseInt(url.searchParams.get("line") ?? "1", 10);
517
771
  const column = parseInt(url.searchParams.get("column") ?? "1", 10);
518
772
  const maxLines = parseInt(url.searchParams.get("maxLines") ?? "100", 10);
519
773
  try {
520
- const absolutePath = import_node_path2.default.isAbsolute(file) ? file : import_node_path2.default.resolve(serverState.cwd, file);
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}`);
778
+ res.writeHead(403, { "Content-Type": "application/json" });
779
+ res.end(
780
+ JSON.stringify({
781
+ success: false,
782
+ error: "Access denied: File is outside of project workspace",
783
+ errorCode: "FORBIDDEN"
784
+ })
785
+ );
786
+ return;
787
+ }
521
788
  const result = await extractSnippet({ file: absolutePath, line, column, maxLines });
522
789
  res.writeHead(200, { "Content-Type": "application/json" });
523
790
  res.end(JSON.stringify(result));
@@ -529,7 +796,7 @@ async function handleRequest(url, req, res) {
529
796
  }
530
797
  return;
531
798
  }
532
- if (pathname === "/send-to-ai" && req.method === "POST") {
799
+ if (pathname === import_types2.INSPECTO_API_PATHS.AI_DISPATCH && req.method === "POST") {
533
800
  try {
534
801
  const rawBody = await readBody(req);
535
802
  const body = JSON.parse(rawBody);
@@ -537,19 +804,30 @@ async function handleRequest(url, req, res) {
537
804
  res.writeHead(result.success ? 200 : 500, { "Content-Type": "application/json" });
538
805
  res.end(JSON.stringify(result));
539
806
  } catch (e) {
540
- console.error("[inspecto] Error parsing /send-to-ai request:", e);
807
+ serverLogger.error(`Error parsing ${import_types2.INSPECTO_API_PATHS.AI_DISPATCH} request:`, e);
541
808
  res.writeHead(500, { "Content-Type": "application/json" });
542
809
  res.end(JSON.stringify({ success: false, error: String(e), errorCode: "INTERNAL_ERROR" }));
543
810
  }
544
811
  return;
545
812
  }
813
+ if (pathname.startsWith(`${import_types2.INSPECTO_API_PATHS.AI_TICKET}/`) && req.method === "GET") {
814
+ const ticketId = pathname.substring(import_types2.INSPECTO_API_PATHS.AI_TICKET.length + 1);
815
+ const payloadStr = payloadTickets.get(ticketId);
816
+ if (!payloadStr) {
817
+ res.writeHead(404, { "Content-Type": "application/json" });
818
+ res.end(JSON.stringify({ success: false, error: "Ticket not found or expired" }));
819
+ return;
820
+ }
821
+ res.writeHead(200, { "Content-Type": "application/json" });
822
+ res.end(payloadStr);
823
+ return;
824
+ }
546
825
  res.writeHead(404, { "Content-Type": "application/json" });
547
826
  res.end(JSON.stringify({ error: "not found" }));
548
827
  }
549
828
  async function dispatchToAi(req) {
550
829
  const { location, snippet, prompt } = req;
551
830
  const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
552
- const ide = userConfig.ide ?? "vscode";
553
831
  const resolvedTarget = resolveTargetTool(userConfig);
554
832
  const formattedPrompt = prompt ?? `Please help me with this code from \`${location.file}\` (line ${location.line}):
555
833
 
@@ -557,22 +835,45 @@ async function dispatchToAi(req) {
557
835
  ${snippet}
558
836
  \`\`\`
559
837
  `;
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,
855
+ prompt: formattedPrompt,
856
+ filePath: location.file,
857
+ line: location.line,
858
+ column: location.column,
859
+ 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);
560
864
  const params = new URLSearchParams();
865
+ params.set("ticket", ticketId);
561
866
  params.set("target", resolvedTarget);
562
- const overrides = extractToolOverrides(ide, userConfig)[resolvedTarget];
563
- if (overrides) {
564
- params.set("overrides", JSON.stringify(overrides));
565
- }
566
- params.set("prompt", formattedPrompt);
567
- params.set("file", location.file);
568
- params.set("line", String(location.line));
569
- params.set("col", String(location.column));
570
- params.set("snippet", snippet);
571
- const scheme = serverState.ideInfo?.scheme || "vscode";
572
- const uri = `${scheme}://inspecto.inspecto/send?${params.toString()}`;
573
- console.log(`[inspecto] dispatchToAi: Generated URI: ${uri}`);
867
+ const uri = `${finalIde}://inspecto.inspecto/send?${params.toString()}`;
868
+ serverLogger.debug(`dispatchToAi: Generated URI: ${uri}`);
574
869
  launchURI(uri);
575
- return { success: true };
870
+ return {
871
+ success: true,
872
+ fallbackPayload: {
873
+ prompt: formattedPrompt,
874
+ file: location.file
875
+ }
876
+ };
576
877
  }
577
878
 
578
879
  // src/injectors/utils.ts
@@ -584,7 +885,9 @@ var resolveClientModule = () => {
584
885
  try {
585
886
  return require.resolve("@inspecto-dev/core");
586
887
  } catch {
587
- console.warn("[inspecto] Could not resolve @inspecto-dev/core \u2014 falling back to bare specifier");
888
+ console.warn(
889
+ "[inspecto] Could not resolve @inspecto-dev/core \u2014 falling back to bare specifier"
890
+ );
588
891
  return "@inspecto-dev/core";
589
892
  }
590
893
  }
@@ -612,7 +915,8 @@ var DEFAULT_OPTIONS = {
612
915
  exclude: [],
613
916
  escapeTags: [],
614
917
  pathType: "absolute",
615
- attributeName: "data-inspecto"
918
+ attributeName: "data-inspecto",
919
+ logLevel: "warn"
616
920
  };
617
921
  var serverPort = null;
618
922
  var ensureServer = async () => {