@wispbit/local 1.0.28 → 1.0.29

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.
package/dist/cli.js CHANGED
@@ -245,7 +245,8 @@ var GetRulesResponseSchema = z2.object({
245
245
  message: z2.string(),
246
246
  prompt: z2.string(),
247
247
  severity: z2.enum(["suggestion", "violation"]),
248
- schema: z2.any()
248
+ schema: z2.any(),
249
+ execution: z2.enum(["llm", "deterministic"]).optional()
249
250
  })
250
251
  )
251
252
  });
@@ -1280,6 +1281,7 @@ var WispbitRuleProvider = class {
1280
1281
  config: {
1281
1282
  message: rule.message,
1282
1283
  severity: rule.severity,
1284
+ execution: rule.execution || "llm",
1283
1285
  steps: rule.schema
1284
1286
  },
1285
1287
  prompt: rule.prompt,
@@ -1414,6 +1416,59 @@ import * as fs2 from "fs";
1414
1416
  import * as path3 from "path";
1415
1417
  import { glob } from "glob";
1416
1418
 
1419
+ // src/utils/diffValidation.ts
1420
+ function isFileValidInDiff(filePath, options) {
1421
+ const { changedFiles } = options;
1422
+ return changedFiles.some((changedFile) => {
1423
+ return filePath === changedFile || filePath.endsWith(changedFile) || changedFile.endsWith(filePath);
1424
+ });
1425
+ }
1426
+ function isLineRangeValidInDiff(filePath, startLine, endLine, options) {
1427
+ const { fileChangeMap } = options;
1428
+ const fileChange = fileChangeMap.get(filePath);
1429
+ if (!fileChange) {
1430
+ return false;
1431
+ }
1432
+ if (fileChange.status === "added") {
1433
+ return true;
1434
+ }
1435
+ if (fileChange.status === "removed") {
1436
+ return false;
1437
+ }
1438
+ return isLineRangeInPatch(fileChange.patch, startLine, endLine);
1439
+ }
1440
+ function isMatchValidInDiff(match, options) {
1441
+ return isFileValidInDiff(match.filePath, options) && isLineRangeValidInDiff(match.filePath, match.range.start.line, match.range.end.line, options);
1442
+ }
1443
+ function isLineRangeInPatch(patch, startLine, endLine) {
1444
+ const lines = patch.split("\n");
1445
+ let currentNewLine = 0;
1446
+ let inHunk = false;
1447
+ for (const line of lines) {
1448
+ const hunkMatch = line.match(/^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);
1449
+ if (hunkMatch) {
1450
+ currentNewLine = parseInt(hunkMatch[1], 10);
1451
+ inHunk = true;
1452
+ continue;
1453
+ }
1454
+ if (!inHunk) continue;
1455
+ if (line.startsWith("+")) {
1456
+ if (currentNewLine >= startLine && currentNewLine <= endLine) {
1457
+ return true;
1458
+ }
1459
+ currentNewLine++;
1460
+ } else if (line.startsWith("-")) {
1461
+ continue;
1462
+ } else if (!line.startsWith("\\")) {
1463
+ if (currentNewLine >= startLine && currentNewLine <= endLine) {
1464
+ return true;
1465
+ }
1466
+ currentNewLine++;
1467
+ }
1468
+ }
1469
+ return false;
1470
+ }
1471
+
1417
1472
  // src/utils/patternMatching.ts
1418
1473
  import ignore from "ignore";
1419
1474
  function matchesAnyPattern(filePath, patterns) {
@@ -1703,30 +1758,11 @@ var FileExecutionContext = class _FileExecutionContext {
1703
1758
  return true;
1704
1759
  }
1705
1760
  const { filePath } = options;
1706
- return this.diffMode.changedFiles.some((changedFile) => {
1707
- return filePath === changedFile || filePath.endsWith(changedFile) || changedFile.endsWith(filePath);
1761
+ return isFileValidInDiff(filePath, {
1762
+ changedFiles: this.diffMode.changedFiles,
1763
+ fileChangeMap: this.diffMode.fileChangeMap
1708
1764
  });
1709
1765
  }
1710
- /**
1711
- * Check if a specific line range should be processed
1712
- */
1713
- isLineRangeValid(options) {
1714
- if (this.mode === "check") {
1715
- return true;
1716
- }
1717
- const { filePath, startLine, endLine } = options;
1718
- const fileChange = this.diffMode.fileChangeMap.get(filePath);
1719
- if (!fileChange) {
1720
- return false;
1721
- }
1722
- if (fileChange.status === "added") {
1723
- return true;
1724
- }
1725
- if (fileChange.status === "removed") {
1726
- return false;
1727
- }
1728
- return this.isLineRangeInPatch({ patch: fileChange.patch, startLine, endLine });
1729
- }
1730
1766
  /**
1731
1767
  * Check if a match should be processed
1732
1768
  */
@@ -1735,46 +1771,11 @@ var FileExecutionContext = class _FileExecutionContext {
1735
1771
  return true;
1736
1772
  }
1737
1773
  const { match } = options;
1738
- return this.isFileValid({ filePath: match.filePath }) && this.isLineRangeValid({
1739
- filePath: match.filePath,
1740
- startLine: match.range.start.line,
1741
- endLine: match.range.end.line
1774
+ return isMatchValidInDiff(match, {
1775
+ changedFiles: this.diffMode.changedFiles,
1776
+ fileChangeMap: this.diffMode.fileChangeMap
1742
1777
  });
1743
1778
  }
1744
- // ===== Helper Methods =====
1745
- /**
1746
- * Parse a git patch to determine if a line range intersects with changed lines
1747
- * Adapted from patchParser.ts logic
1748
- */
1749
- isLineRangeInPatch(options) {
1750
- const { patch, startLine, endLine } = options;
1751
- const lines = patch.split("\n");
1752
- let currentNewLine = 0;
1753
- let inHunk = false;
1754
- for (const line of lines) {
1755
- const hunkMatch = line.match(/^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);
1756
- if (hunkMatch) {
1757
- currentNewLine = parseInt(hunkMatch[1], 10);
1758
- inHunk = true;
1759
- continue;
1760
- }
1761
- if (!inHunk) continue;
1762
- if (line.startsWith("+")) {
1763
- if (currentNewLine >= startLine && currentNewLine <= endLine) {
1764
- return true;
1765
- }
1766
- currentNewLine++;
1767
- } else if (line.startsWith("-")) {
1768
- continue;
1769
- } else if (!line.startsWith("\\")) {
1770
- if (currentNewLine >= startLine && currentNewLine <= endLine) {
1771
- return true;
1772
- }
1773
- currentNewLine++;
1774
- }
1775
- }
1776
- return false;
1777
- }
1778
1779
  };
1779
1780
 
1780
1781
  // src/steps/FileFilterStep.ts
@@ -1876,6 +1877,40 @@ import path8 from "path";
1876
1877
  import { readFile as readFile2 } from "fs/promises";
1877
1878
  import path5 from "path";
1878
1879
  import { parse as parse2, parseAsync } from "@ast-grep/napi";
1880
+
1881
+ // src/utils/coordinates.ts
1882
+ function matchTo0Indexed(match) {
1883
+ return {
1884
+ ...match,
1885
+ range: {
1886
+ start: {
1887
+ line: match.range.start.line - 1,
1888
+ column: match.range.start.column - 1
1889
+ },
1890
+ end: {
1891
+ line: match.range.end.line - 1,
1892
+ column: match.range.end.column - 1
1893
+ }
1894
+ }
1895
+ };
1896
+ }
1897
+ function matchTo1Indexed(match) {
1898
+ return {
1899
+ ...match,
1900
+ range: {
1901
+ start: {
1902
+ line: match.range.start.line + 1,
1903
+ column: match.range.start.column + 1
1904
+ },
1905
+ end: {
1906
+ line: match.range.end.line + 1,
1907
+ column: match.range.end.column + 1
1908
+ }
1909
+ }
1910
+ };
1911
+ }
1912
+
1913
+ // src/providers/AstGrepAstProvider.ts
1879
1914
  var AstGrepAstProvider = class {
1880
1915
  environment;
1881
1916
  language;
@@ -1932,7 +1967,7 @@ var AstGrepAstProvider = class {
1932
1967
  const matches = [];
1933
1968
  for (const { filePath, sgNode } of sgNodes) {
1934
1969
  const range = sgNode.range();
1935
- matches.push({
1970
+ const match0Indexed = {
1936
1971
  filePath,
1937
1972
  text: sgNode.text(),
1938
1973
  range: {
@@ -1948,7 +1983,8 @@ var AstGrepAstProvider = class {
1948
1983
  // Try to extract symbol if possible
1949
1984
  symbol: this.extractSymbol(sgNode),
1950
1985
  language: this.language
1951
- });
1986
+ };
1987
+ matches.push(matchTo1Indexed(match0Indexed));
1952
1988
  }
1953
1989
  return matches;
1954
1990
  }
@@ -1991,10 +2027,11 @@ var AstGrepAstProvider = class {
1991
2027
  const absolutePath = path5.resolve(this.environment.getWorkspaceRoot(), match.filePath);
1992
2028
  const content = await readFile2(absolutePath, "utf-8");
1993
2029
  const root = parse2(this.language, content).root();
2030
+ const match0Indexed = matchTo0Indexed(match);
1994
2031
  const nodeAtPosition = this.findNodeAtPosition(
1995
2032
  root,
1996
- match.range.start.line,
1997
- match.range.start.column
2033
+ match0Indexed.range.start.line,
2034
+ match0Indexed.range.start.column
1998
2035
  );
1999
2036
  if (!nodeAtPosition) {
2000
2037
  return null;
@@ -2005,7 +2042,7 @@ var AstGrepAstProvider = class {
2005
2042
  const kindStr = typeof kindValue === "string" ? kindValue : String(kindValue);
2006
2043
  if (targetNodeKinds.has(kindStr)) {
2007
2044
  const range = current.range();
2008
- return {
2045
+ const expandedMatch0Indexed = {
2009
2046
  filePath: match.filePath,
2010
2047
  text: current.text(),
2011
2048
  range: {
@@ -2015,6 +2052,7 @@ var AstGrepAstProvider = class {
2015
2052
  symbol: match.symbol,
2016
2053
  language: this.language
2017
2054
  };
2055
+ return matchTo1Indexed(expandedMatch0Indexed);
2018
2056
  }
2019
2057
  current = current.parent();
2020
2058
  }
@@ -2215,13 +2253,14 @@ var ScipIntelligenceProvider = class {
2215
2253
  async findDefinitions(match) {
2216
2254
  await this.ensureIndexReady();
2217
2255
  if (!this.scipIndex) throw new Error("SCIP index not ready");
2218
- this.eventEmitter.startScipMatchLookup(this.language, match);
2256
+ const match0Indexed = matchTo0Indexed(match);
2257
+ this.eventEmitter.startScipMatchLookup(this.language, match0Indexed);
2219
2258
  const definitions = [];
2220
2259
  const documents = this.scipIndex.documents;
2221
2260
  for (const document of documents) {
2222
- if (document.relative_path !== match.filePath) continue;
2261
+ if (document.relative_path !== match0Indexed.filePath) continue;
2223
2262
  this.eventEmitter.scipMatchLookupProgress(this.language, document);
2224
- const scipOcc = this.findBestOverlappingOccurrence(document.occurrences, match);
2263
+ const scipOcc = this.findBestOverlappingOccurrence(document.occurrences, match0Indexed);
2225
2264
  if (scipOcc) {
2226
2265
  if (this.isExternalSymbol(scipOcc.symbol)) {
2227
2266
  continue;
@@ -2229,7 +2268,7 @@ var ScipIntelligenceProvider = class {
2229
2268
  this.eventEmitter.scipMatchLookupProgress(this.language, scipOcc);
2230
2269
  const def = await this.findDefinitionForSymbol(scipOcc.symbol, documents);
2231
2270
  if (def) {
2232
- definitions.push(def);
2271
+ definitions.push(matchTo1Indexed(def));
2233
2272
  }
2234
2273
  }
2235
2274
  }
@@ -2244,17 +2283,18 @@ var ScipIntelligenceProvider = class {
2244
2283
  if (!this.scipIndex) {
2245
2284
  return [];
2246
2285
  }
2286
+ const match0Indexed = matchTo0Indexed(match);
2247
2287
  const references = [];
2248
2288
  const documents = this.scipIndex.documents;
2249
2289
  for (const document of documents) {
2250
- if (document.relative_path !== match.filePath) continue;
2251
- const scipOcc = this.findBestOverlappingOccurrence(document.occurrences, match);
2290
+ if (document.relative_path !== match0Indexed.filePath) continue;
2291
+ const scipOcc = this.findBestOverlappingOccurrence(document.occurrences, match0Indexed);
2252
2292
  if (scipOcc) {
2253
2293
  if (this.isExternalSymbol(scipOcc.symbol)) {
2254
2294
  break;
2255
2295
  }
2256
2296
  const refs = await this.findReferencesForSymbol(scipOcc.symbol, documents);
2257
- references.push(...refs);
2297
+ references.push(...refs.map((ref) => matchTo1Indexed(ref)));
2258
2298
  break;
2259
2299
  }
2260
2300
  }
@@ -3197,14 +3237,21 @@ var VIOLATION_COLOR = "#f87171";
3197
3237
  var SUGGESTION_COLOR = "#60a5fa";
3198
3238
  var SKIPPED_COLOR = "#9b59b6";
3199
3239
  var BRAND_COLOR = "#fbbf24";
3240
+ var DETERMINISTIC_COLOR = "#9ca3af";
3241
+ var LLM_COLOR = "#9b59b6";
3200
3242
  function formatClickableRuleId(ruleId, internalId) {
3201
3243
  if (internalId) {
3202
3244
  const url = `https://app.wispbit.com/rules/${internalId}`;
3203
- const link = `\x1B]8;;${url}\x07${chalk.dim.underline(ruleId)}\x1B]8;;\x07`;
3204
- return link;
3245
+ return `${chalk.dim.underline(ruleId)}${chalk.dim("|")}${chalk.dim.underline(url)}`;
3205
3246
  }
3206
3247
  return chalk.dim(ruleId);
3207
3248
  }
3249
+ function truncateMessage(message, maxWidth) {
3250
+ if (message.length <= maxWidth) {
3251
+ return message;
3252
+ }
3253
+ return message.substring(0, maxWidth - 3) + "...";
3254
+ }
3208
3255
  var LINE_NUMBER_BG_COLOR = "#f8f8f8";
3209
3256
  var LINE_NUMBER_TEXT_COLOR = "#888888";
3210
3257
  var loadingFrames = [
@@ -3283,9 +3330,9 @@ function printSummary(results, summary) {
3283
3330
  var _a;
3284
3331
  ruleData.matches.push({
3285
3332
  filePath: match.filePath,
3286
- line: match.range.start.line + 1,
3287
- column: match.range.start.column + 1,
3288
- endLine: match.range.end.line !== match.range.start.line ? match.range.end.line + 1 : void 0,
3333
+ line: match.range.start.line,
3334
+ column: match.range.start.column,
3335
+ endLine: match.range.end.line !== match.range.start.line ? match.range.end.line : void 0,
3289
3336
  text: match.text
3290
3337
  });
3291
3338
  if ((_a = match.metadata) == null ? void 0 : _a.llmValidation) {
@@ -3388,11 +3435,14 @@ function printRulesList(rules) {
3388
3435
  const tableData = [];
3389
3436
  tableData.push([
3390
3437
  "",
3391
- `${"id".padEnd(12)} ${chalk.dim("\u2502")} ${"severity".padEnd(16)} ${chalk.dim("\u2502")} message`
3438
+ `${"id".padEnd(12)} ${chalk.dim("\u2502")} ${"severity".padEnd(16)} ${chalk.dim("\u2502")} ${"execution".padEnd(18)} ${chalk.dim("\u2502")} message`
3392
3439
  ]);
3440
+ const terminalWidth = process.stdout.columns || 120;
3441
+ const fixedColumnsWidth = 12 + 3 + 16 + 3 + 18 + 3;
3442
+ const messageMaxWidth = Math.max(20, terminalWidth - fixedColumnsWidth - 10);
3393
3443
  tableData.push([
3394
3444
  "",
3395
- `${chalk.dim("\u2500".repeat(12))} ${chalk.dim("\u253C")} ${chalk.dim("\u2500".repeat(16))} ${chalk.dim("\u253C")} ${chalk.dim("\u2500".repeat(80))}`
3445
+ `${chalk.dim("\u2500".repeat(12))} ${chalk.dim("\u253C")} ${chalk.dim("\u2500".repeat(16))} ${chalk.dim("\u253C")} ${chalk.dim("\u2500".repeat(18))} ${chalk.dim("\u253C")} ${chalk.dim("\u2500".repeat(messageMaxWidth))}`
3396
3446
  ]);
3397
3447
  for (const rule of rules) {
3398
3448
  const severityIcon = rule.config.severity === "violation" ? "\u25A0" : "\u25CF";
@@ -3400,11 +3450,14 @@ function printRulesList(rules) {
3400
3450
  const severityIconColor = rule.config.severity === "violation" ? chalk.hex(VIOLATION_COLOR) : chalk.hex(SUGGESTION_COLOR);
3401
3451
  const severityText = severityIconColor(severityIcon) + " " + severityColor(rule.config.severity);
3402
3452
  const ruleId = formatClickableRuleId(rule.id, rule.internalId);
3453
+ const executionText = rule.config.execution === "llm" ? chalk.hex(LLM_COLOR)("\u25C6") + " " + chalk.hex(LLM_COLOR).bold("llm") : chalk.hex(DETERMINISTIC_COLOR)("\u25C6") + " " + chalk.hex(DETERMINISTIC_COLOR).bold("deterministic");
3454
+ const truncatedMessage = truncateMessage(rule.config.message, messageMaxWidth);
3403
3455
  const idPadding = Math.max(0, 12 - rule.id.length);
3404
3456
  const severityPadding = Math.max(0, 16 - (rule.config.severity.length + 2));
3457
+ const executionPadding = Math.max(0, 18 - (rule.config.execution.length + 2));
3405
3458
  tableData.push([
3406
3459
  "",
3407
- `${ruleId}${" ".repeat(idPadding)} ${chalk.dim("\u2502")} ${severityText}${" ".repeat(severityPadding)} ${chalk.dim("\u2502")} ${rule.config.message}`
3460
+ `${ruleId}${" ".repeat(idPadding)} ${chalk.dim("\u2502")} ${severityText}${" ".repeat(severityPadding)} ${chalk.dim("\u2502")} ${executionText}${" ".repeat(executionPadding)} ${chalk.dim("\u2502")} ${truncatedMessage}`
3408
3461
  ]);
3409
3462
  }
3410
3463
  const table = textTable(tableData, {
@@ -3819,8 +3872,7 @@ async function loadApiKey() {
3819
3872
  return null;
3820
3873
  }
3821
3874
  }
3822
- async function ensureConfigured() {
3823
- const environment = new Environment();
3875
+ async function ensureConfigured(environment) {
3824
3876
  let apiKey = process.env.WISPBIT_API_KEY || null;
3825
3877
  if (!apiKey) {
3826
3878
  apiKey = await loadApiKey();
@@ -3851,7 +3903,7 @@ async function ensureConfigured() {
3851
3903
  const repositoryUrl = await environment.getRepositoryUrl();
3852
3904
  console.log(
3853
3905
  chalk4.red(
3854
- `No repository in wispbit found for url: ${repositoryUrl}. If your git remote URL was recently modified, use "git remote set-url origin <new-url>" to update the remote URL.`
3906
+ `No repository in wispbit found for url: ${repositoryUrl}. If you are passing the repository URL as a flag, make sure it is correct. If your git remote URL was recently modified, use "git remote set-url origin <new-url>" to update the remote URL.`
3855
3907
  )
3856
3908
  );
3857
3909
  process.exit(1);
@@ -3867,7 +3919,7 @@ async function checkForUpdates() {
3867
3919
  chalk4.bgHex("#b2f5ea").black.bold(` NEW VERSION AVAILABLE: ${latestCliVersion} (current: ${currentVersion}) `)
3868
3920
  );
3869
3921
  console.log(
3870
- chalk4.bgHex("#b2f5ea").black.bold(` Run 'pnpm install -g @wispbit/cli' to update
3922
+ chalk4.bgHex("#b2f5ea").black.bold(` Run 'pnpm install -g @wispbit/local' to update
3871
3923
  `)
3872
3924
  );
3873
3925
  }
@@ -3915,6 +3967,7 @@ Diff examples:
3915
3967
  $ wispbit abc123 --include committed Compare against specific commit SHA, show only committed
3916
3968
 
3917
3969
  Global options:
3970
+ --repo-url <url> Override repository URL (default: from git remote)
3918
3971
  -v, --version Show version number
3919
3972
  -h, --help Show help
3920
3973
  `,
@@ -3932,6 +3985,10 @@ Global options:
3932
3985
  include: {
3933
3986
  type: "string"
3934
3987
  },
3988
+ // Repository URL override
3989
+ repoUrl: {
3990
+ type: "string"
3991
+ },
3935
3992
  // Debug option
3936
3993
  debug: {
3937
3994
  type: "boolean",
@@ -3952,8 +4009,8 @@ Global options:
3952
4009
  }
3953
4010
  );
3954
4011
  async function executeCommand(options) {
3955
- const environment = new Environment();
3956
- const config = await ensureConfigured();
4012
+ const environment = new Environment({ repositoryUrl: options.repoUrl });
4013
+ const config = await ensureConfigured(environment);
3957
4014
  const { ruleId, json: json2, mode, filePath } = options;
3958
4015
  let { commitSelector, diffInclude } = options;
3959
4016
  if (mode === "diff") {
@@ -4119,9 +4176,9 @@ async function executeCommand(options) {
4119
4176
  }
4120
4177
  return results;
4121
4178
  }
4122
- async function listRules() {
4123
- const config = await ensureConfigured();
4124
- const environment = new Environment();
4179
+ async function listRules(repoUrl) {
4180
+ const environment = new Environment({ repositoryUrl: repoUrl });
4181
+ const config = await ensureConfigured(environment);
4125
4182
  const ruleProvider = new WispbitRuleProvider(config, environment);
4126
4183
  const rules = await ruleProvider.loadAllRules();
4127
4184
  printRulesList(rules);
@@ -4137,7 +4194,8 @@ async function main() {
4137
4194
  json: cli.flags.json,
4138
4195
  debug: cli.flags.debug,
4139
4196
  mode: "check",
4140
- filePath
4197
+ filePath,
4198
+ repoUrl: cli.flags.repoUrl
4141
4199
  });
4142
4200
  break;
4143
4201
  }
@@ -4165,17 +4223,18 @@ async function main() {
4165
4223
  debug: cli.flags.debug,
4166
4224
  mode: "diff",
4167
4225
  diffInclude,
4168
- commitSelector
4226
+ commitSelector,
4227
+ repoUrl: cli.flags.repoUrl
4169
4228
  });
4170
4229
  break;
4171
4230
  }
4172
4231
  case "list": {
4173
- await listRules();
4232
+ await listRules(cli.flags.repoUrl);
4174
4233
  break;
4175
4234
  }
4176
4235
  case "cache": {
4177
4236
  if (subcommand === "purge") {
4178
- const environment = new Environment();
4237
+ const environment = new Environment({ repositoryUrl: cli.flags.repoUrl });
4179
4238
  const storage = new Storage(environment);
4180
4239
  const result = await storage.purgeCache();
4181
4240
  console.log(
@@ -4213,7 +4272,8 @@ ${chalk4.green("Cache purged successfully.")} Removed ${result.deletedCount} ite
4213
4272
  debug: cli.flags.debug,
4214
4273
  mode: "diff",
4215
4274
  diffInclude,
4216
- commitSelector
4275
+ commitSelector,
4276
+ repoUrl: cli.flags.repoUrl
4217
4277
  });
4218
4278
  break;
4219
4279
  }