@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
@@ -17,9 +17,11 @@ import http from "http";
17
17
  import fs3 from "fs";
18
18
  import path4 from "path";
19
19
  import os2 from "os";
20
+ import crypto from "crypto";
20
21
  import { execSync, execFileSync } from "child_process";
21
22
  import portfinder from "portfinder";
22
23
  import { launchIDE } from "launch-ide";
24
+ import { INSPECTO_API_PATHS } from "@inspecto-dev/types";
23
25
 
24
26
  // src/server/snippet.ts
25
27
  import * as fs from "fs";
@@ -134,9 +136,76 @@ import fs2 from "fs";
134
136
  import path3 from "path";
135
137
  import os from "os";
136
138
  import { createDefu } from "defu";
137
- import { DEFAULT_TOOL_MODE, VALID_MODES } from "@inspecto-dev/types";
139
+ import {
140
+ DEFAULT_PROVIDER_MODE,
141
+ VALID_MODES,
142
+ DEFAULT_INTENTS
143
+ } from "@inspecto-dev/types";
144
+
145
+ // src/utils/logger.ts
146
+ var LOG_LEVELS = {
147
+ silent: 0,
148
+ error: 1,
149
+ warn: 2,
150
+ info: 3
151
+ };
152
+ function isDebugEnabled(namespace) {
153
+ if (typeof process === "undefined" || !process.env) return false;
154
+ const debugEnv = process.env.DEBUG;
155
+ if (!debugEnv) return false;
156
+ const namespaces = debugEnv.split(",").map((s) => s.trim());
157
+ for (const ns of namespaces) {
158
+ if (ns === "*") return true;
159
+ if (ns.endsWith("*")) {
160
+ const prefix = ns.slice(0, -1);
161
+ if (namespace.startsWith(prefix)) return true;
162
+ } else if (ns === namespace) {
163
+ return true;
164
+ }
165
+ }
166
+ return false;
167
+ }
168
+ var globalLevel = "warn";
169
+ var registeredLoggers = /* @__PURE__ */ new Set();
170
+ function createLogger(namespace, options) {
171
+ let currentLevel = options?.logLevel ?? globalLevel;
172
+ let numericLevel = LOG_LEVELS[currentLevel] ?? 2;
173
+ const debugEnabled = isDebugEnabled(namespace);
174
+ const logger = {
175
+ setLevel(level) {
176
+ currentLevel = level;
177
+ numericLevel = LOG_LEVELS[level] ?? 2;
178
+ },
179
+ info(msg, ...args) {
180
+ if (numericLevel >= LOG_LEVELS.info) {
181
+ console.log(`\x1B[36m[inspecto]\x1B[0m ${msg}`, ...args);
182
+ }
183
+ },
184
+ warn(msg, ...args) {
185
+ if (numericLevel >= LOG_LEVELS.warn) {
186
+ console.warn(`\x1B[33m[inspecto] WARN:\x1B[0m ${msg}`, ...args);
187
+ }
188
+ },
189
+ error(msg, ...args) {
190
+ if (numericLevel >= LOG_LEVELS.error) {
191
+ console.error(`\x1B[31m[inspecto] ERROR:\x1B[0m ${msg}`, ...args);
192
+ }
193
+ },
194
+ debug(msg, ...args) {
195
+ if (debugEnabled) {
196
+ console.log(`\x1B[90m[${namespace}]\x1B[0m ${msg}`, ...args);
197
+ }
198
+ }
199
+ };
200
+ registeredLoggers.add(logger);
201
+ return logger;
202
+ }
203
+
204
+ // src/config.ts
205
+ var configLogger = createLogger("inspecto:config");
138
206
  var loadedConfig = null;
139
207
  var loadedPrompts = null;
208
+ var globalLogLevel = "warn";
140
209
  var isWatching = false;
141
210
  var arrayReplaceMerge = createDefu((obj, key, val) => {
142
211
  if (Array.isArray(val)) {
@@ -144,6 +213,9 @@ var arrayReplaceMerge = createDefu((obj, key, val) => {
144
213
  return true;
145
214
  }
146
215
  });
216
+ function getGlobalLogLevel() {
217
+ return globalLogLevel;
218
+ }
147
219
  function resolveConfigRoots(cwd, gitRoot) {
148
220
  const roots = [];
149
221
  let current = cwd;
@@ -173,7 +245,7 @@ function loadUserConfigSync(force = false, cwd = process.cwd(), gitRoot) {
173
245
  layers.push(readJsonSafely(path3.join(root, ".inspecto", "settings.json")));
174
246
  }
175
247
  layers.push(readJsonSafely(path3.join(os.homedir(), ".inspecto", "settings.json")));
176
- layers.push({ providers: {} });
248
+ layers.push({});
177
249
  const validLayers = layers.filter((l) => l !== null);
178
250
  loadedConfig = arrayReplaceMerge(...validLayers);
179
251
  return loadedConfig;
@@ -216,48 +288,145 @@ function readJsonSafely(filePath) {
216
288
  }
217
289
  } catch (e) {
218
290
  if (e instanceof SyntaxError) {
219
- console.warn(`[inspecto] Failed to parse config at ${filePath}: Invalid JSON`);
291
+ configLogger.warn(`Failed to parse config at ${filePath}: Invalid JSON`);
220
292
  } else {
221
- console.warn(`[inspecto] Failed to read config at ${filePath}:`, e);
293
+ configLogger.warn(`Failed to read config at ${filePath}:`, e);
222
294
  }
223
295
  }
224
296
  return null;
225
297
  }
226
- function resolveTargetTool(config) {
227
- if (config.prefer) {
228
- return config.prefer;
229
- }
230
- if (config.providers && Object.keys(config.providers).length > 0) {
231
- return Object.keys(config.providers)[0];
298
+ function resolveTargetTool(config, ide = "vscode") {
299
+ const defaultProvider = config["provider.default"];
300
+ if (defaultProvider) {
301
+ const tool = defaultProvider.split(".")[0];
302
+ return tool;
232
303
  }
233
- return "github-copilot";
304
+ return "copilot";
234
305
  }
235
- function resolveToolMode(tool, ide, config) {
306
+ function resolveProviderMode(tool, ide, config) {
236
307
  let requestedType = void 0;
237
- if (config.providers && config.providers[tool] && config.providers[tool].type) {
238
- const type = config.providers[tool].type;
239
- if (type === "plugin" || type === "cli") {
240
- requestedType = type;
241
- }
242
- }
243
- requestedType = requestedType ?? DEFAULT_TOOL_MODE[tool];
244
- const valid = VALID_MODES[tool] || [DEFAULT_TOOL_MODE[tool]];
308
+ const defaultProvider = config["provider.default"];
309
+ if (defaultProvider && defaultProvider.startsWith(`${tool}.`)) {
310
+ const mode = defaultProvider.split(".")[1];
311
+ if (mode === "extension") requestedType = "extension";
312
+ if (mode === "cli") requestedType = "cli";
313
+ }
314
+ requestedType = requestedType ?? DEFAULT_PROVIDER_MODE[tool];
315
+ const valid = VALID_MODES[tool] || [DEFAULT_PROVIDER_MODE[tool]];
245
316
  return requestedType && valid.includes(requestedType) ? requestedType : valid[0];
246
317
  }
247
318
  function extractToolOverrides(ide, config) {
248
319
  const result = {};
249
- if (!config.providers) return result;
250
- for (const [tool, cfg] of Object.entries(config.providers)) {
251
- if (!cfg) continue;
252
- const overrides = {
253
- type: cfg.type || DEFAULT_TOOL_MODE[tool] || "plugin"
254
- };
255
- if (cfg.bin) overrides.binaryPath = cfg.bin;
256
- if (cfg.args) overrides.args = cfg.args;
257
- if (cfg.cwd) overrides.cwd = cfg.cwd;
258
- if (cfg.autoSend !== void 0) overrides.autoSend = cfg.autoSend;
259
- result[tool] = overrides;
320
+ if (!config) return result;
321
+ for (const [key, value] of Object.entries(config)) {
322
+ if (!key.startsWith("provider.")) continue;
323
+ if (key === "provider.default") continue;
324
+ const toolIndex = 1;
325
+ const modeIndex = 2;
326
+ const propIndex = 3;
327
+ const parts = key.split(".");
328
+ if (parts.length >= propIndex + 1) {
329
+ const tool = parts[toolIndex];
330
+ const mode = parts[modeIndex];
331
+ const prop = parts[propIndex];
332
+ if (!result[tool]) {
333
+ result[tool] = { type: mode };
334
+ }
335
+ const overrides = result[tool];
336
+ if (prop === "bin") overrides.binaryPath = value;
337
+ if (prop === "args") overrides.args = value;
338
+ if (prop === "cwd") overrides.cwd = value;
339
+ if (prop === "coldStartDelay") overrides.coldStartDelay = value;
340
+ }
341
+ }
342
+ return result;
343
+ }
344
+ function resolveIntents(serverPrompts) {
345
+ const baseMap = /* @__PURE__ */ new Map();
346
+ for (const intent of DEFAULT_INTENTS) {
347
+ if (intent.id) baseMap.set(intent.id, { ...intent });
348
+ }
349
+ const defaults = () => ensureOpenInEditorLast(Array.from(baseMap.values()));
350
+ if (!serverPrompts) return defaults();
351
+ const isReplace = !Array.isArray(serverPrompts) && typeof serverPrompts === "object" && serverPrompts.$replace === true;
352
+ const promptsArray = Array.isArray(serverPrompts) ? serverPrompts : isReplace ? serverPrompts.items : [];
353
+ if (!promptsArray || promptsArray.length === 0) return defaults();
354
+ if (isReplace) {
355
+ const result = [];
356
+ for (const item of promptsArray) {
357
+ if (typeof item === "string") {
358
+ if (baseMap.has(item)) {
359
+ result.push(baseMap.get(item));
360
+ } else {
361
+ configLogger.warn(
362
+ `Unknown built-in intent id: "${item}". Available: ${[...baseMap.keys()].join(", ")}`
363
+ );
364
+ }
365
+ } else if (typeof item === "object") {
366
+ if (!item.id) {
367
+ configLogger.warn('Intent object missing required "id" field, skipping.');
368
+ continue;
369
+ }
370
+ if (item.enabled === false) {
371
+ configLogger.warn(
372
+ `Intent "${item.id}" is listed in $replace but has enabled:false \u2014 it will be excluded.`
373
+ );
374
+ continue;
375
+ }
376
+ if (item.isAction && item.id !== "open-in-editor") {
377
+ configLogger.warn(
378
+ `isAction is reserved for built-in actions. Ignoring intent "${item.id}".`
379
+ );
380
+ continue;
381
+ }
382
+ result.push(baseMap.has(item.id) ? { ...baseMap.get(item.id), ...item } : item);
383
+ }
384
+ }
385
+ return ensureOpenInEditorLast(result);
386
+ }
387
+ const merged = Array.from(baseMap.values());
388
+ for (const item of promptsArray) {
389
+ if (typeof item === "string") {
390
+ if (!baseMap.has(item)) {
391
+ configLogger.warn(
392
+ `Unknown built-in intent id: "${item}". In append mode, strings have no effect on ordering \u2014 use $replace to control order.`
393
+ );
394
+ }
395
+ continue;
396
+ }
397
+ if (typeof item === "object") {
398
+ if (!item.id) {
399
+ configLogger.warn('Intent object missing required "id" field, skipping.');
400
+ continue;
401
+ }
402
+ if (item.isAction && item.id !== "open-in-editor") {
403
+ configLogger.warn(
404
+ `isAction is reserved for built-in actions. Ignoring intent "${item.id}".`
405
+ );
406
+ continue;
407
+ }
408
+ const existingIdx = merged.findIndex((i) => i.id === item.id);
409
+ if (existingIdx !== -1) {
410
+ if (item.enabled === false) {
411
+ merged.splice(existingIdx, 1);
412
+ } else {
413
+ merged[existingIdx] = { ...merged[existingIdx], ...item };
414
+ }
415
+ } else {
416
+ if (item.enabled !== false) {
417
+ merged.push(item);
418
+ }
419
+ }
420
+ }
260
421
  }
422
+ return ensureOpenInEditorLast(merged);
423
+ }
424
+ function ensureOpenInEditorLast(intents) {
425
+ const idx = intents.findIndex((i) => i.id === "open-in-editor");
426
+ if (idx === -1 || idx === intents.length - 1) return intents;
427
+ const result = [...intents];
428
+ const item = result.splice(idx, 1)[0];
429
+ result.push(item);
261
430
  return result;
262
431
  }
263
432
  var watchers = [];
@@ -294,6 +463,19 @@ function watchConfig(onReload, cwd = process.cwd(), gitRoot) {
294
463
  }
295
464
 
296
465
  // src/server/index.ts
466
+ var serverLogger = createLogger("inspecto:server", { logLevel: getGlobalLogLevel() });
467
+ var payloadTickets = /* @__PURE__ */ new Map();
468
+ function createTicket(payload) {
469
+ const ticketId = crypto.randomUUID();
470
+ payloadTickets.set(ticketId, JSON.stringify(payload));
471
+ setTimeout(
472
+ () => {
473
+ payloadTickets.delete(ticketId);
474
+ },
475
+ 5 * 60 * 1e3
476
+ );
477
+ return ticketId;
478
+ }
297
479
  var serverState = {
298
480
  port: null,
299
481
  running: false,
@@ -305,8 +487,11 @@ var serverInstance = null;
305
487
  function resolveProjectRoot() {
306
488
  let gitRoot;
307
489
  try {
490
+ serverLogger.info("Resolving project root...");
308
491
  gitRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
309
- } catch {
492
+ serverLogger.info("Resolved project root: " + gitRoot);
493
+ } catch (e) {
494
+ serverLogger.error("Failed to resolve project root:", e);
310
495
  gitRoot = process.cwd();
311
496
  }
312
497
  let current = gitRoot;
@@ -328,7 +513,7 @@ function launchURI(uri) {
328
513
  execFileSync("xdg-open", [uri]);
329
514
  }
330
515
  } catch (e) {
331
- console.error("[inspecto] Failed to launch URI via execFileSync, falling back to launchIDE:", e);
516
+ serverLogger.error("Failed to launch URI via execFileSync, falling back to launchIDE:", e);
332
517
  launchIDE({ file: uri });
333
518
  }
334
519
  }
@@ -343,7 +528,7 @@ async function startServer() {
343
528
  const port = await portfinder.getPortPromise();
344
529
  watchConfig(
345
530
  () => {
346
- console.log("[inspecto] user config reloaded.");
531
+ serverLogger.info("user config reloaded.");
347
532
  },
348
533
  serverState.cwd,
349
534
  serverState.configRoot
@@ -359,7 +544,7 @@ async function startServer() {
359
544
  }
360
545
  const url = new URL(req.url ?? "/", `http://localhost:${port}`);
361
546
  handleRequest(url, req, res).catch((err) => {
362
- console.error("[inspecto] server error:", err);
547
+ serverLogger.error("server error:", err);
363
548
  res.writeHead(500, { "Content-Type": "application/json" });
364
549
  res.end(JSON.stringify({ success: false, error: String(err) }));
365
550
  });
@@ -372,22 +557,41 @@ async function startServer() {
372
557
  serverInstance.once("error", reject);
373
558
  });
374
559
  serverInstance.on("error", (err) => {
375
- console.error("[inspecto] persistent server error:", err);
560
+ serverLogger.error("persistent server error:", err);
376
561
  });
377
562
  serverState.port = port;
378
563
  serverState.running = true;
379
- const portFile = path4.join(os2.tmpdir(), "inspecto.port");
564
+ const portFile = path4.join(os2.tmpdir(), "inspecto.port.json");
380
565
  try {
381
- fs3.writeFileSync(portFile, String(port), "utf-8");
382
- } catch {
566
+ let portData = {};
567
+ if (fs3.existsSync(portFile)) {
568
+ try {
569
+ portData = JSON.parse(fs3.readFileSync(portFile, "utf-8"));
570
+ } catch (e) {
571
+ }
572
+ }
573
+ const rootHash = crypto.createHash("md5").update(serverState.projectRoot).digest("hex");
574
+ portData[rootHash] = port;
575
+ fs3.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
576
+ } catch (e) {
577
+ serverLogger.warn("Failed to write port file:", e);
383
578
  }
384
579
  process.once("exit", () => {
385
580
  try {
386
- fs3.unlinkSync(portFile);
581
+ if (fs3.existsSync(portFile)) {
582
+ const portData = JSON.parse(fs3.readFileSync(portFile, "utf-8"));
583
+ const rootHash = crypto.createHash("md5").update(serverState.projectRoot).digest("hex");
584
+ delete portData[rootHash];
585
+ if (Object.keys(portData).length === 0) {
586
+ fs3.unlinkSync(portFile);
587
+ } else {
588
+ fs3.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
589
+ }
590
+ }
387
591
  } catch {
388
592
  }
389
593
  });
390
- console.log(`[inspecto] server running at http://127.0.0.1:${port}`);
594
+ serverLogger.info(`server running at http://127.0.0.1:${port}`);
391
595
  return port;
392
596
  }
393
597
  async function readBody(req) {
@@ -400,66 +604,63 @@ async function readBody(req) {
400
604
  }
401
605
  async function handleRequest(url, req, res) {
402
606
  const pathname = url.pathname;
403
- if (pathname === "/health" && req.method === "GET") {
607
+ if ((pathname === "/health" || pathname === INSPECTO_API_PATHS.HEALTH) && req.method === "GET") {
404
608
  res.writeHead(200, { "Content-Type": "application/json" });
405
609
  res.end(JSON.stringify({ ok: true, port: serverState.port }));
406
610
  return;
407
611
  }
408
- if (pathname === "/config" && req.method === "GET") {
612
+ if (pathname === INSPECTO_API_PATHS.CLIENT_CONFIG && req.method === "GET") {
409
613
  const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
410
614
  const promptsConfig = await loadPromptsConfig(false, serverState.cwd, serverState.configRoot);
411
615
  const effectiveIde = userConfig.ide ?? "vscode";
412
616
  let info;
413
617
  if (!serverState.ideInfo) {
414
- const fallbackTargets = userConfig.providers ? Object.keys(userConfig.providers) : ["claude-code", "gemini", "coco", "codex"];
415
618
  info = {
416
- ide: effectiveIde,
417
- providers: fallbackTargets.reduce(
418
- (acc, target) => {
419
- acc[target] = {
420
- mode: resolveToolMode(target, effectiveIde, userConfig),
421
- installed: false
422
- };
423
- return acc;
424
- },
425
- {}
426
- )
619
+ ide: effectiveIde
427
620
  };
428
621
  } else {
429
622
  const { scheme: _scheme, ...rest } = serverState.ideInfo;
430
623
  info = rest;
431
624
  }
432
- const resolvedProviders = { ...info.providers };
433
- for (const tool in resolvedProviders) {
434
- resolvedProviders[tool].mode = resolveToolMode(tool, info.ide, userConfig);
435
- }
436
625
  const config = {
437
626
  ...info,
438
- providers: resolvedProviders,
439
- providerOverrides: extractToolOverrides(info.ide, userConfig),
440
- prompts: promptsConfig,
441
- hotKeys: userConfig.hotKeys,
442
- includeSnippet: userConfig.includeSnippet
627
+ prompts: resolveIntents(promptsConfig),
628
+ hotKeys: userConfig["inspector.hotKey"] ?? "alt",
629
+ theme: userConfig["inspector.theme"] ?? "auto",
630
+ includeSnippet: userConfig["prompt.includeSnippet"] ?? false,
631
+ autoSend: userConfig["prompt.autoSend"] ?? false
443
632
  };
633
+ delete config.providers;
444
634
  res.writeHead(200, { "Content-Type": "application/json" });
445
635
  res.end(JSON.stringify(config));
446
636
  return;
447
637
  }
448
- if (pathname === "/config" && req.method === "POST") {
638
+ if (pathname === INSPECTO_API_PATHS.IDE_INFO && req.method === "POST") {
449
639
  try {
450
640
  const body = JSON.parse(await readBody(req));
451
- serverState.ideInfo = body;
452
- console.log(`[inspecto] Received IDE info from extension:`, body);
641
+ const ideWorkspace = body.workspaceRoot || "";
642
+ const serverProjectRoot = serverState.projectRoot || "";
643
+ const isSameProject = !ideWorkspace || !serverProjectRoot || ideWorkspace === serverProjectRoot || serverProjectRoot.startsWith(ideWorkspace);
644
+ if (isSameProject) {
645
+ serverState.ideInfo = body;
646
+ serverLogger.debug(
647
+ `Accepted IDE info from matched workspace (ide-${body.ide} / schema-${body.scheme})`
648
+ );
649
+ } else {
650
+ serverLogger.debug(
651
+ `Ignored IDE info from unrelated workspace (IDE Workspace: ${ideWorkspace}, Server: ${serverProjectRoot}, Scheme: ${body.scheme}, IDE: ${body.ide})`
652
+ );
653
+ }
453
654
  res.writeHead(200, { "Content-Type": "application/json" });
454
655
  res.end(JSON.stringify({ success: true }));
455
656
  } catch (e) {
456
- console.error("[inspecto] Error parsing /config POST request:", e);
657
+ serverLogger.error(`Error parsing ${INSPECTO_API_PATHS.IDE_INFO} POST request:`, e);
457
658
  res.writeHead(400, { "Content-Type": "application/json" });
458
659
  res.end(JSON.stringify({ error: "Invalid JSON body" }));
459
660
  }
460
661
  return;
461
662
  }
462
- if (pathname === "/open" && req.method === "POST") {
663
+ if (pathname === INSPECTO_API_PATHS.IDE_OPEN && req.method === "POST") {
463
664
  let body;
464
665
  try {
465
666
  body = JSON.parse(await readBody(req));
@@ -468,28 +669,98 @@ async function handleRequest(url, req, res) {
468
669
  res.end(JSON.stringify({ error: "Invalid JSON body" }));
469
670
  return;
470
671
  }
471
- const absolutePath = path4.isAbsolute(body.file) ? body.file : path4.resolve(serverState.cwd, body.file);
672
+ const absolutePath = path4.isAbsolute(body.file) ? path4.resolve(body.file) : path4.resolve(serverState.cwd, body.file);
673
+ const relativeToRoot = path4.relative(serverState.projectRoot, absolutePath);
674
+ if (relativeToRoot.startsWith("..") || path4.isAbsolute(relativeToRoot)) {
675
+ serverLogger.warn(`Security: Blocked path traversal attempt in IDE_OPEN: ${body.file}`);
676
+ res.writeHead(403, { "Content-Type": "application/json" });
677
+ res.end(JSON.stringify({ error: "Access denied: File is outside of project workspace" }));
678
+ return;
679
+ }
472
680
  const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
473
- const ide = userConfig.ide ?? "vscode";
474
- const editorHint = "code";
475
- launchIDE({
476
- file: absolutePath,
477
- line: body.line,
478
- column: body.column,
479
- editor: editorHint,
480
- type: process.platform === "darwin" ? "open" : "exec"
481
- });
681
+ const configuredIde = userConfig.ide;
682
+ const activeIde = serverState.ideInfo?.ide;
683
+ const activeIdeScheme = serverState.ideInfo?.scheme;
684
+ const rawEditorHint = configuredIde || activeIde || activeIdeScheme || "code";
685
+ if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
686
+ serverLogger.warn(
687
+ `Active IDE is ${activeIdeScheme}, but config forces ${configuredIde}. Using configured IDE.`
688
+ );
689
+ }
690
+ let editorHint = rawEditorHint;
691
+ if (rawEditorHint === "vscode") editorHint = "code";
692
+ else if (rawEditorHint === "vscode-insiders") editorHint = "code-insiders";
693
+ else if (rawEditorHint === "vscodium") editorHint = "codium";
694
+ else if (rawEditorHint === "trae-cn" || rawEditorHint === "trae") editorHint = "trae";
695
+ serverLogger.debug(
696
+ `IDE_OPEN: activeIde=${activeIde}, activeIdeScheme=${activeIdeScheme}, configuredIde=${configuredIde} -> rawEditorHint=${rawEditorHint}, finalEditorHint=${editorHint}`
697
+ );
698
+ const VSCODE_FAMILY_SCHEMES = [
699
+ "vscode",
700
+ "vscode-insiders",
701
+ "cursor",
702
+ "windsurf",
703
+ "trae",
704
+ "trae-cn",
705
+ "vscodium",
706
+ "codebuddy",
707
+ "codebuddy-cn",
708
+ "antigravity"
709
+ ];
710
+ if (VSCODE_FAMILY_SCHEMES.includes(rawEditorHint)) {
711
+ const uri = `${rawEditorHint}://file${absolutePath}:${body.line}:${body.column}`;
712
+ serverLogger.debug(`IDE_OPEN: Bypassing launchIDE, using URI scheme directly: ${uri}`);
713
+ try {
714
+ if (process.platform === "darwin") {
715
+ execFileSync("open", [uri]);
716
+ } else if (process.platform === "win32") {
717
+ execFileSync("cmd", ["/c", "start", '""', uri]);
718
+ } else {
719
+ execFileSync("xdg-open", [uri]);
720
+ }
721
+ } catch (e) {
722
+ serverLogger.error(`Failed to launch URI for IDE_OPEN (${uri}):`, e);
723
+ launchIDE({
724
+ file: absolutePath,
725
+ line: body.line,
726
+ column: body.column,
727
+ editor: editorHint,
728
+ type: process.platform === "darwin" ? "open" : "exec"
729
+ });
730
+ }
731
+ } else {
732
+ launchIDE({
733
+ file: absolutePath,
734
+ line: body.line,
735
+ column: body.column,
736
+ editor: editorHint,
737
+ type: process.platform === "darwin" ? "open" : "exec"
738
+ });
739
+ }
482
740
  res.writeHead(200, { "Content-Type": "application/json" });
483
741
  res.end(JSON.stringify({ success: true }));
484
742
  return;
485
743
  }
486
- if (pathname === "/snippet" && req.method === "GET") {
744
+ if (pathname === INSPECTO_API_PATHS.PROJECT_SNIPPET && req.method === "GET") {
487
745
  const file = url.searchParams.get("file") ?? "";
488
746
  const line = parseInt(url.searchParams.get("line") ?? "1", 10);
489
747
  const column = parseInt(url.searchParams.get("column") ?? "1", 10);
490
748
  const maxLines = parseInt(url.searchParams.get("maxLines") ?? "100", 10);
491
749
  try {
492
- const absolutePath = path4.isAbsolute(file) ? file : path4.resolve(serverState.cwd, file);
750
+ const absolutePath = path4.isAbsolute(file) ? path4.resolve(file) : path4.resolve(serverState.cwd, file);
751
+ const relativeToRoot = path4.relative(serverState.projectRoot, absolutePath);
752
+ if (relativeToRoot.startsWith("..") || path4.isAbsolute(relativeToRoot)) {
753
+ serverLogger.warn(`Security: Blocked path traversal attempt in PROJECT_SNIPPET: ${file}`);
754
+ res.writeHead(403, { "Content-Type": "application/json" });
755
+ res.end(
756
+ JSON.stringify({
757
+ success: false,
758
+ error: "Access denied: File is outside of project workspace",
759
+ errorCode: "FORBIDDEN"
760
+ })
761
+ );
762
+ return;
763
+ }
493
764
  const result = await extractSnippet({ file: absolutePath, line, column, maxLines });
494
765
  res.writeHead(200, { "Content-Type": "application/json" });
495
766
  res.end(JSON.stringify(result));
@@ -501,7 +772,7 @@ async function handleRequest(url, req, res) {
501
772
  }
502
773
  return;
503
774
  }
504
- if (pathname === "/send-to-ai" && req.method === "POST") {
775
+ if (pathname === INSPECTO_API_PATHS.AI_DISPATCH && req.method === "POST") {
505
776
  try {
506
777
  const rawBody = await readBody(req);
507
778
  const body = JSON.parse(rawBody);
@@ -509,19 +780,30 @@ async function handleRequest(url, req, res) {
509
780
  res.writeHead(result.success ? 200 : 500, { "Content-Type": "application/json" });
510
781
  res.end(JSON.stringify(result));
511
782
  } catch (e) {
512
- console.error("[inspecto] Error parsing /send-to-ai request:", e);
783
+ serverLogger.error(`Error parsing ${INSPECTO_API_PATHS.AI_DISPATCH} request:`, e);
513
784
  res.writeHead(500, { "Content-Type": "application/json" });
514
785
  res.end(JSON.stringify({ success: false, error: String(e), errorCode: "INTERNAL_ERROR" }));
515
786
  }
516
787
  return;
517
788
  }
789
+ if (pathname.startsWith(`${INSPECTO_API_PATHS.AI_TICKET}/`) && req.method === "GET") {
790
+ const ticketId = pathname.substring(INSPECTO_API_PATHS.AI_TICKET.length + 1);
791
+ const payloadStr = payloadTickets.get(ticketId);
792
+ if (!payloadStr) {
793
+ res.writeHead(404, { "Content-Type": "application/json" });
794
+ res.end(JSON.stringify({ success: false, error: "Ticket not found or expired" }));
795
+ return;
796
+ }
797
+ res.writeHead(200, { "Content-Type": "application/json" });
798
+ res.end(payloadStr);
799
+ return;
800
+ }
518
801
  res.writeHead(404, { "Content-Type": "application/json" });
519
802
  res.end(JSON.stringify({ error: "not found" }));
520
803
  }
521
804
  async function dispatchToAi(req) {
522
805
  const { location, snippet, prompt } = req;
523
806
  const userConfig = loadUserConfigSync(false, serverState.cwd, serverState.configRoot);
524
- const ide = userConfig.ide ?? "vscode";
525
807
  const resolvedTarget = resolveTargetTool(userConfig);
526
808
  const formattedPrompt = prompt ?? `Please help me with this code from \`${location.file}\` (line ${location.line}):
527
809
 
@@ -529,22 +811,45 @@ async function dispatchToAi(req) {
529
811
  ${snippet}
530
812
  \`\`\`
531
813
  `;
814
+ const ideReportedMode = serverState.ideInfo?.providers[resolvedTarget]?.mode;
815
+ const configuredIde = userConfig.ide;
816
+ const activeIde = serverState.ideInfo?.ide;
817
+ const activeIdeScheme = serverState.ideInfo?.scheme;
818
+ const finalIde = configuredIde || activeIdeScheme || activeIde || "vscode";
819
+ if (configuredIde && activeIdeScheme && !activeIdeScheme.includes(configuredIde)) {
820
+ serverLogger.warn(
821
+ `dispatchToAi: Active IDE is ${activeIdeScheme}, but config forces ${configuredIde}. Using configured IDE.`
822
+ );
823
+ }
824
+ const mode = resolveProviderMode(resolvedTarget, finalIde, userConfig);
825
+ const overrides = extractToolOverrides(finalIde, userConfig)[resolvedTarget] || {};
826
+ overrides.type = mode;
827
+ const fullPayload = {
828
+ ide: finalIde,
829
+ target: resolvedTarget,
830
+ targetType: mode,
831
+ prompt: formattedPrompt,
832
+ filePath: location.file,
833
+ line: location.line,
834
+ column: location.column,
835
+ snippet,
836
+ overrides: Object.keys(overrides).length > 0 ? overrides : void 0,
837
+ autoSend: userConfig["prompt.autoSend"] !== void 0 ? Boolean(userConfig["prompt.autoSend"]) : void 0
838
+ };
839
+ const ticketId = createTicket(fullPayload);
532
840
  const params = new URLSearchParams();
841
+ params.set("ticket", ticketId);
533
842
  params.set("target", resolvedTarget);
534
- const overrides = extractToolOverrides(ide, userConfig)[resolvedTarget];
535
- if (overrides) {
536
- params.set("overrides", JSON.stringify(overrides));
537
- }
538
- params.set("prompt", formattedPrompt);
539
- params.set("file", location.file);
540
- params.set("line", String(location.line));
541
- params.set("col", String(location.column));
542
- params.set("snippet", snippet);
543
- const scheme = serverState.ideInfo?.scheme || "vscode";
544
- const uri = `${scheme}://inspecto.inspecto/send?${params.toString()}`;
545
- console.log(`[inspecto] dispatchToAi: Generated URI: ${uri}`);
843
+ const uri = `${finalIde}://inspecto.inspecto/send?${params.toString()}`;
844
+ serverLogger.debug(`dispatchToAi: Generated URI: ${uri}`);
546
845
  launchURI(uri);
547
- return { success: true };
846
+ return {
847
+ success: true,
848
+ fallbackPayload: {
849
+ prompt: formattedPrompt,
850
+ file: location.file
851
+ }
852
+ };
548
853
  }
549
854
 
550
855
  // src/injectors/utils.ts
@@ -556,7 +861,9 @@ var resolveClientModule = () => {
556
861
  try {
557
862
  return __require.resolve("@inspecto-dev/core");
558
863
  } catch {
559
- console.warn("[inspecto] Could not resolve @inspecto-dev/core \u2014 falling back to bare specifier");
864
+ console.warn(
865
+ "[inspecto] Could not resolve @inspecto-dev/core \u2014 falling back to bare specifier"
866
+ );
560
867
  return "@inspecto-dev/core";
561
868
  }
562
869
  }
@@ -584,7 +891,8 @@ var DEFAULT_OPTIONS = {
584
891
  exclude: [],
585
892
  escapeTags: [],
586
893
  pathType: "absolute",
587
- attributeName: "data-inspecto"
894
+ attributeName: "data-inspecto",
895
+ logLevel: "warn"
588
896
  };
589
897
  var serverPort = null;
590
898
  var ensureServer = async () => {