@codacy/verity-cli 0.24.0-experimental.ae62726 → 0.24.0-experimental.b35bbc7

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/bin/verity.js CHANGED
@@ -10326,8 +10326,8 @@ var {
10326
10326
 
10327
10327
  // src/commands/auth.ts
10328
10328
  var import_promises3 = require("node:fs/promises");
10329
- var import_node_child_process3 = require("node:child_process");
10330
- var import_node_path2 = require("node:path");
10329
+ var import_node_child_process4 = require("node:child_process");
10330
+ var import_node_path3 = require("node:path");
10331
10331
 
10332
10332
  // src/lib/auth.ts
10333
10333
  var import_promises = require("node:fs/promises");
@@ -10475,7 +10475,11 @@ var SECURITY_PATTERNS = [
10475
10475
  /Dockerfile/
10476
10476
  ];
10477
10477
  var PROD_SERVICE_URL = "https://ofcamwrjwrkazqvdchko.supabase.co/functions/v1";
10478
- var DEFAULT_SERVICE_URL = "https://yykkfdexzljmmkklpgyz.supabase.co/functions/v1".length > 0 ? "https://yykkfdexzljmmkklpgyz.supabase.co/functions/v1" : PROD_SERVICE_URL;
10478
+ var DEFAULT_SERVICE_URL = "https://yyfaqvcgslcrzvrbvqik.supabase.co/functions/v1".length > 0 ? "https://yyfaqvcgslcrzvrbvqik.supabase.co/functions/v1" : PROD_SERVICE_URL;
10479
+ var GITHUB_CLIENT_ID = "Ov23liBpj42KMTtUtN10";
10480
+ var GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
10481
+ var GITHUB_ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
10482
+ var GITHUB_OAUTH_SCOPES = "repo user:email";
10479
10483
 
10480
10484
  // src/lib/auth.ts
10481
10485
  async function resolveToken(flagToken) {
@@ -10641,7 +10645,8 @@ async function apiRequest(options) {
10641
10645
  timeout = 9e4,
10642
10646
  cmd = "unknown",
10643
10647
  retry = false,
10644
- encodeBody = false
10648
+ encodeBody = false,
10649
+ extraHeaders
10645
10650
  } = options;
10646
10651
  const url = `${serviceUrl}${path}`;
10647
10652
  const headers = {
@@ -10650,6 +10655,9 @@ async function apiRequest(options) {
10650
10655
  if (token) {
10651
10656
  headers["Authorization"] = `Bearer ${token}`;
10652
10657
  }
10658
+ if (extraHeaders) {
10659
+ Object.assign(headers, extraHeaders);
10660
+ }
10653
10661
  const testMockScenario = process.env.VERITY_TEST_MOCK_SCENARIO;
10654
10662
  if (testMockScenario) {
10655
10663
  headers["X-Verity-Mock-Scenario"] = testMockScenario;
@@ -10740,6 +10748,324 @@ function analyzeRequest(options) {
10740
10748
  });
10741
10749
  }
10742
10750
 
10751
+ // src/lib/provider-auth.ts
10752
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
10753
+ var form = (fields) => new URLSearchParams(fields).toString();
10754
+ async function githubDeviceFlow() {
10755
+ const override = process.env.VERITY_PROVIDER_TOKEN;
10756
+ if (override) return { ok: true, data: override };
10757
+ let dc;
10758
+ try {
10759
+ const res = await fetch(GITHUB_DEVICE_CODE_URL, {
10760
+ method: "POST",
10761
+ headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" },
10762
+ body: form({ client_id: GITHUB_CLIENT_ID, scope: GITHUB_OAUTH_SCOPES })
10763
+ });
10764
+ if (!res.ok) {
10765
+ return { ok: false, error: `GitHub device-code request failed (HTTP ${res.status})` };
10766
+ }
10767
+ dc = await res.json();
10768
+ } catch (err) {
10769
+ return { ok: false, error: `Network error contacting GitHub: ${err.message}` };
10770
+ }
10771
+ if (!dc.device_code || !dc.user_code) {
10772
+ return {
10773
+ ok: false,
10774
+ error: "GitHub did not return a device code (is Device Flow enabled on the OAuth app?)"
10775
+ };
10776
+ }
10777
+ printInfo("");
10778
+ printInfo(`To authorize Verity, open: ${dc.verification_uri}`);
10779
+ printInfo(`And enter the code: ${dc.user_code}`);
10780
+ printInfo("Waiting for authorization\u2026");
10781
+ const deadline = Date.now() + (dc.expires_in || 900) * 1e3;
10782
+ let interval = dc.interval || 5;
10783
+ while (Date.now() < deadline) {
10784
+ await sleep(interval * 1e3);
10785
+ let data;
10786
+ try {
10787
+ const res = await fetch(GITHUB_ACCESS_TOKEN_URL, {
10788
+ method: "POST",
10789
+ headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" },
10790
+ body: form({
10791
+ client_id: GITHUB_CLIENT_ID,
10792
+ device_code: dc.device_code,
10793
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
10794
+ })
10795
+ });
10796
+ data = await res.json().catch(() => ({}));
10797
+ } catch {
10798
+ continue;
10799
+ }
10800
+ if (data.access_token) return { ok: true, data: data.access_token };
10801
+ switch (data.error) {
10802
+ case "authorization_pending":
10803
+ break;
10804
+ case "slow_down":
10805
+ interval += 5;
10806
+ break;
10807
+ case "access_denied":
10808
+ return { ok: false, error: "Authorization was denied on GitHub." };
10809
+ case "expired_token":
10810
+ return { ok: false, error: "The authorization code expired. Re-run register." };
10811
+ default:
10812
+ if (data.error) return { ok: false, error: `GitHub auth error: ${data.error}` };
10813
+ }
10814
+ }
10815
+ return { ok: false, error: "Timed out waiting for GitHub authorization." };
10816
+ }
10817
+
10818
+ // src/lib/git.ts
10819
+ var import_node_child_process3 = require("node:child_process");
10820
+ var import_node_fs2 = require("node:fs");
10821
+ var import_node_path2 = require("node:path");
10822
+ function resolveFile(relpath) {
10823
+ if ((0, import_node_fs2.existsSync)(relpath)) return relpath;
10824
+ if ((0, import_node_fs2.existsSync)(".claude/worktrees")) {
10825
+ try {
10826
+ const entries = (0, import_node_fs2.readdirSync)(".claude/worktrees", { withFileTypes: true });
10827
+ for (const entry of entries) {
10828
+ if (!entry.isDirectory()) continue;
10829
+ const candidate = (0, import_node_path2.join)(".claude/worktrees", entry.name, relpath);
10830
+ if ((0, import_node_fs2.existsSync)(candidate)) return candidate;
10831
+ }
10832
+ } catch {
10833
+ }
10834
+ }
10835
+ return null;
10836
+ }
10837
+ function execGit(cmd) {
10838
+ try {
10839
+ return (0, import_node_child_process3.execSync)(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
10840
+ } catch {
10841
+ return "";
10842
+ }
10843
+ }
10844
+ function splitLines(s) {
10845
+ return s.split("\n").filter((l) => l.length > 0);
10846
+ }
10847
+ var SHA_RE = /^[0-9a-f]{40}$/;
10848
+ function readBaselineSha() {
10849
+ if (!(0, import_node_fs2.existsSync)(BASELINE_SHA_FILE)) return null;
10850
+ let sha;
10851
+ try {
10852
+ sha = (0, import_node_fs2.readFileSync)(BASELINE_SHA_FILE, "utf-8").trim();
10853
+ } catch {
10854
+ return null;
10855
+ }
10856
+ if (!SHA_RE.test(sha)) return null;
10857
+ const reachable = execGit(`git cat-file -e ${sha}^{commit} 2>/dev/null && echo ok`) === "ok";
10858
+ if (!reachable) {
10859
+ try {
10860
+ (0, import_node_fs2.unlinkSync)(BASELINE_SHA_FILE);
10861
+ } catch {
10862
+ }
10863
+ return null;
10864
+ }
10865
+ return sha;
10866
+ }
10867
+ function writeBaselineSha(sha) {
10868
+ if (!SHA_RE.test(sha)) return;
10869
+ try {
10870
+ (0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(BASELINE_SHA_FILE), { recursive: true });
10871
+ (0, import_node_fs2.writeFileSync)(BASELINE_SHA_FILE, sha);
10872
+ } catch {
10873
+ }
10874
+ }
10875
+ function getChangedFiles() {
10876
+ const sets = /* @__PURE__ */ new Set();
10877
+ let hasRecentCommitFiles = false;
10878
+ for (const f of splitLines(execGit("git diff --name-only HEAD"))) sets.add(f);
10879
+ for (const f of splitLines(execGit("git diff --name-only --cached"))) sets.add(f);
10880
+ for (const f of splitLines(execGit("git ls-files --others --exclude-standard"))) sets.add(f);
10881
+ const baseline = readBaselineSha();
10882
+ if (baseline) {
10883
+ const committed = splitLines(execGit(`git diff --name-only ${baseline}..HEAD`));
10884
+ if (committed.length > 0) {
10885
+ hasRecentCommitFiles = true;
10886
+ for (const f of committed) sets.add(f);
10887
+ }
10888
+ } else {
10889
+ const headTimestamp = parseInt(execGit("git log -1 --format=%ct HEAD"), 10) || 0;
10890
+ const commitAge = Math.floor(Date.now() / 1e3) - headTimestamp;
10891
+ const hasUnstaged = splitLines(execGit("git diff --name-only HEAD")).length > 0;
10892
+ const hasStaged = splitLines(execGit("git diff --name-only --cached")).length > 0;
10893
+ if (commitAge < 120 && !hasUnstaged && !hasStaged) {
10894
+ const recentFiles = splitLines(execGit("git diff --name-only HEAD~1..HEAD"));
10895
+ if (recentFiles.length > 0) {
10896
+ hasRecentCommitFiles = true;
10897
+ for (const f of recentFiles) sets.add(f);
10898
+ }
10899
+ }
10900
+ }
10901
+ for (const f of getWorktreeFiles()) sets.add(f);
10902
+ const filtered = Array.from(sets).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
10903
+ return { files: filtered, hasRecentCommitFiles };
10904
+ }
10905
+ function getStagedFiles() {
10906
+ return splitLines(execGit("git diff --cached --name-only")).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
10907
+ }
10908
+ function getDirtyFiles() {
10909
+ const set = /* @__PURE__ */ new Set();
10910
+ for (const f of splitLines(execGit("git diff --name-only HEAD"))) set.add(f);
10911
+ for (const f of splitLines(execGit("git diff --name-only --cached"))) set.add(f);
10912
+ for (const f of splitLines(execGit("git ls-files --others --exclude-standard"))) set.add(f);
10913
+ return Array.from(set).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
10914
+ }
10915
+ function showContentAtRef(ref, repoRelPath) {
10916
+ if (!ref || ref === "no-git") return null;
10917
+ const normalizedPath = repoRelPath.replace(/\\/g, "/");
10918
+ try {
10919
+ return (0, import_node_child_process3.execFileSync)("git", ["show", `${ref}:${normalizedPath}`], {
10920
+ encoding: "utf-8",
10921
+ maxBuffer: 64 * 1024 * 1024,
10922
+ stdio: ["pipe", "pipe", "pipe"]
10923
+ });
10924
+ } catch {
10925
+ return null;
10926
+ }
10927
+ }
10928
+ function getPushRangeFiles() {
10929
+ const diff = (range) => splitLines(execGit(`git diff --name-only ${range}`)).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
10930
+ const resolvers = [
10931
+ () => execGit("git rev-parse --abbrev-ref --symbolic-full-name @{push}") ? "@{push}..HEAD" : null,
10932
+ () => execGit("git rev-parse --abbrev-ref --symbolic-full-name @{upstream}") ? "@{upstream}..HEAD" : null,
10933
+ () => {
10934
+ const branch = execGit("git rev-parse --abbrev-ref HEAD");
10935
+ return branch && branch !== "HEAD" && execGit(`git rev-parse --verify -q origin/${branch}`) ? `origin/${branch}..HEAD` : null;
10936
+ }
10937
+ ];
10938
+ for (const resolve of resolvers) {
10939
+ const range = resolve();
10940
+ if (range) return { files: diff(range), range };
10941
+ }
10942
+ const baseline = readBaselineSha();
10943
+ if (baseline) {
10944
+ const files = diff(`${baseline}..HEAD`);
10945
+ if (files.length > 0) return { files, range: `${baseline}..HEAD` };
10946
+ }
10947
+ const last = diff("HEAD~1..HEAD");
10948
+ return { files: last, range: last.length > 0 ? "HEAD~1..HEAD" : null };
10949
+ }
10950
+ function getPushRangeMessages() {
10951
+ const { range } = getPushRangeFiles();
10952
+ if (!range) return "";
10953
+ return execGit(`git log ${range} --format=%B%x00`).split("\0").map((s) => s.trim()).filter(Boolean).join("\n\n");
10954
+ }
10955
+ function getWorktreeFiles() {
10956
+ const result = [];
10957
+ const worktreeDir = ".claude/worktrees";
10958
+ if (!(0, import_node_fs2.existsSync)(worktreeDir)) return result;
10959
+ try {
10960
+ const fiveMinAgo = Date.now() - 5 * 60 * 1e3;
10961
+ const entries = (0, import_node_fs2.readdirSync)(worktreeDir, { withFileTypes: true });
10962
+ for (const entry of entries) {
10963
+ if (!entry.isDirectory()) continue;
10964
+ const wtDir = (0, import_node_path2.join)(worktreeDir, entry.name);
10965
+ scanDir(wtDir, wtDir, fiveMinAgo, result);
10966
+ }
10967
+ } catch {
10968
+ }
10969
+ return result;
10970
+ }
10971
+ function scanDir(baseDir, dir, minMtime, result) {
10972
+ try {
10973
+ const entries = (0, import_node_fs2.readdirSync)(dir, { withFileTypes: true });
10974
+ for (const entry of entries) {
10975
+ const fullPath = (0, import_node_path2.join)(dir, entry.name);
10976
+ if (entry.isDirectory()) {
10977
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
10978
+ scanDir(baseDir, fullPath, minMtime, result);
10979
+ } else if (entry.isFile()) {
10980
+ const ext = (0, import_node_path2.extname)(entry.name).slice(1);
10981
+ if (!ANALYZABLE_EXTENSIONS.has(ext)) continue;
10982
+ try {
10983
+ const stat3 = (0, import_node_fs2.statSync)(fullPath);
10984
+ if (stat3.mtimeMs >= minMtime) {
10985
+ const relPath = fullPath.slice(baseDir.length + 1);
10986
+ result.push(relPath);
10987
+ }
10988
+ } catch {
10989
+ }
10990
+ }
10991
+ }
10992
+ } catch {
10993
+ }
10994
+ }
10995
+ function filterAnalyzable(files) {
10996
+ return files.filter((f) => {
10997
+ const ext = (0, import_node_path2.extname)(f).slice(1);
10998
+ return ANALYZABLE_EXTENSIONS.has(ext);
10999
+ });
11000
+ }
11001
+ function filterReviewable(files) {
11002
+ return files.filter((f) => {
11003
+ const ext = (0, import_node_path2.extname)(f).slice(1);
11004
+ if (ANALYZABLE_EXTENSIONS.has(ext)) return false;
11005
+ if (REVIEWABLE_EXTENSIONS.has(ext)) return true;
11006
+ const basename2 = f.split("/").pop() ?? "";
11007
+ if (REVIEWABLE_FILENAMES.has(basename2)) return true;
11008
+ if (REVIEWABLE_PATH_PATTERNS.some((p) => p.test(f))) return true;
11009
+ return false;
11010
+ });
11011
+ }
11012
+ function filterSecurity(files) {
11013
+ return files.filter(
11014
+ (f) => SECURITY_PATTERNS.some((p) => p.test(f))
11015
+ );
11016
+ }
11017
+ function getCurrentCommit() {
11018
+ return execGit("git rev-parse HEAD") || "no-git";
11019
+ }
11020
+ function detectProvider(host) {
11021
+ const h = host.toLowerCase();
11022
+ if (h.includes("github")) return "github";
11023
+ if (h.includes("gitlab")) return "gitlab";
11024
+ if (h.includes("bitbucket")) return "bitbucket";
11025
+ return "unknown";
11026
+ }
11027
+ function parseRemote(raw) {
11028
+ if (!raw || typeof raw !== "string") return null;
11029
+ let s = raw.trim();
11030
+ if (!s) return null;
11031
+ let host = "";
11032
+ let path = "";
11033
+ const scp = s.match(/^[^/@]+@([^:/]+):(.+)$/);
11034
+ if (scp) {
11035
+ host = scp[1];
11036
+ path = scp[2];
11037
+ } else {
11038
+ s = s.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//, "");
11039
+ s = s.replace(/^[^/@]+@/, "");
11040
+ const slash = s.indexOf("/");
11041
+ if (slash === -1) return null;
11042
+ host = s.slice(0, slash);
11043
+ path = s.slice(slash + 1);
11044
+ }
11045
+ host = host.toLowerCase().trim();
11046
+ path = path.replace(/\/+$/, "").replace(/\.git$/, "");
11047
+ if (!host || !path) return null;
11048
+ const segments = path.split("/").filter(Boolean);
11049
+ if (segments.length < 2) return null;
11050
+ const owner = segments[0];
11051
+ const repo = segments[segments.length - 1];
11052
+ if (!owner || !repo) return null;
11053
+ return {
11054
+ host,
11055
+ owner,
11056
+ repo,
11057
+ provider: detectProvider(host),
11058
+ orgUrl: `https://${host}/${owner}`,
11059
+ orgName: owner
11060
+ };
11061
+ }
11062
+ function listTrackedFiles() {
11063
+ const set = /* @__PURE__ */ new Set();
11064
+ for (const f of splitLines(execGit("git ls-files"))) set.add(f);
11065
+ for (const f of splitLines(execGit("git ls-files --others --exclude-standard"))) set.add(f);
11066
+ return Array.from(set);
11067
+ }
11068
+
10743
11069
  // src/commands/auth.ts
10744
11070
  function registerAuthCommands(program2) {
10745
11071
  const auth = program2.command("auth").description("Manage project authentication");
@@ -10749,35 +11075,65 @@ function registerAuthCommands(program2) {
10749
11075
  let remote = opts.remote;
10750
11076
  if (!remote) {
10751
11077
  try {
10752
- remote = (0, import_node_child_process3.execSync)("git remote get-url origin", { encoding: "utf-8" }).trim();
11078
+ remote = (0, import_node_child_process4.execSync)("git remote get-url origin", { encoding: "utf-8" }).trim();
10753
11079
  } catch {
10754
11080
  printError("No git remote found. Use --remote to specify one.");
10755
11081
  process.exit(1);
10756
11082
  }
10757
11083
  }
11084
+ const parsed = parseRemote(remote);
11085
+ if (!parsed) {
11086
+ printError(`Could not parse git remote: ${remote}`);
11087
+ process.exit(1);
11088
+ }
11089
+ if (parsed.provider !== "github") {
11090
+ printError(`Provider '${parsed.provider}' is not supported yet \u2014 GitHub only for now.`);
11091
+ process.exit(1);
11092
+ }
11093
+ const providerAuth = await githubDeviceFlow();
11094
+ if (!providerAuth.ok) {
11095
+ printError(providerAuth.error);
11096
+ process.exit(1);
11097
+ }
11098
+ const providerToken = providerAuth.data;
10758
11099
  const result = await apiRequest({
10759
11100
  method: "POST",
10760
11101
  path: "/auth/register",
10761
11102
  serviceUrl,
10762
11103
  body: { project_name: opts.project, git_remote_url: remote },
11104
+ extraHeaders: { "X-Provider-Token": providerToken },
10763
11105
  verbose: globals.verbose
10764
11106
  });
10765
11107
  if (!result.ok) {
10766
11108
  printError(result.error);
10767
11109
  process.exit(1);
10768
11110
  }
10769
- const { project_id, token, service_url } = result.data;
11111
+ const { project_id, token, service_url, user } = result.data;
10770
11112
  await (0, import_promises3.mkdir)(VERITY_DIR, { recursive: true });
10771
- await (0, import_promises3.writeFile)(CREDENTIALS_FILE, `token: ${token}
11113
+ await (0, import_promises3.writeFile)(
11114
+ CREDENTIALS_FILE,
11115
+ `token: ${token}
10772
11116
  service_url: ${service_url}
10773
- `);
11117
+ provider_token: ${providerToken}
11118
+ `,
11119
+ { mode: 384 }
11120
+ );
11121
+ await (0, import_promises3.chmod)(CREDENTIALS_FILE, 384).catch(() => {
11122
+ });
10774
11123
  try {
10775
- await (0, import_promises3.mkdir)((0, import_node_path2.dirname)(GLOBAL_CREDENTIALS_FILE), { recursive: true });
10776
- await (0, import_promises3.appendFile)(GLOBAL_CREDENTIALS_FILE, `${remote} token: ${token}
10777
- `);
11124
+ await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(GLOBAL_CREDENTIALS_FILE), { recursive: true });
11125
+ await (0, import_promises3.appendFile)(
11126
+ GLOBAL_CREDENTIALS_FILE,
11127
+ `${remote} token: ${token}
11128
+ ${remote} provider_token: ${providerToken}
11129
+ `
11130
+ );
11131
+ await (0, import_promises3.chmod)(GLOBAL_CREDENTIALS_FILE, 384).catch(() => {
11132
+ });
10778
11133
  } catch {
10779
11134
  }
10780
11135
  printInfo(`Project registered: ${project_id}`);
11136
+ if (user?.email) printInfo(`Authenticated as: ${user.email}`);
10781
11137
  printJson({ project_id, service_url });
10782
11138
  });
10783
11139
  auth.command("verify").description("Verify the current token is valid").action(async () => {
@@ -10811,7 +11167,7 @@ service_url: ${service_url}
10811
11167
  let remote = opts.remote;
10812
11168
  if (!remote) {
10813
11169
  try {
10814
- remote = (0, import_node_child_process3.execSync)("git remote get-url origin", { encoding: "utf-8" }).trim();
11170
+ remote = (0, import_node_child_process4.execSync)("git remote get-url origin", { encoding: "utf-8" }).trim();
10815
11171
  } catch {
10816
11172
  printError("No git remote found. Use --remote to specify one.");
10817
11173
  process.exit(1);
@@ -10839,7 +11195,7 @@ service_url: ${service_url}
10839
11195
 
10840
11196
  // src/lib/hooks.ts
10841
11197
  var import_promises4 = require("node:fs/promises");
10842
- var import_node_path3 = require("node:path");
11198
+ var import_node_path4 = require("node:path");
10843
11199
  var VERITY_STOP_HOOK = {
10844
11200
  type: "command",
10845
11201
  command: "verity analyze",
@@ -10991,19 +11347,19 @@ async function checkAllVerityHooksDetailed() {
10991
11347
  return { stop, intent, baseline, current: hasCurrent, legacyOnly: hasLegacy && !hasCurrent };
10992
11348
  }
10993
11349
  async function writeSettings(settings) {
10994
- await (0, import_promises4.mkdir)((0, import_node_path3.dirname)(CLAUDE_SETTINGS_FILE), { recursive: true });
11350
+ await (0, import_promises4.mkdir)((0, import_node_path4.dirname)(CLAUDE_SETTINGS_FILE), { recursive: true });
10995
11351
  await (0, import_promises4.writeFile)(CLAUDE_SETTINGS_FILE, JSON.stringify(settings, null, 2) + "\n");
10996
11352
  }
10997
11353
  async function readSettingsAt(root) {
10998
11354
  try {
10999
- return JSON.parse(await (0, import_promises4.readFile)((0, import_node_path3.join)(root, CLAUDE_SETTINGS_FILE), "utf-8"));
11355
+ return JSON.parse(await (0, import_promises4.readFile)((0, import_node_path4.join)(root, CLAUDE_SETTINGS_FILE), "utf-8"));
11000
11356
  } catch {
11001
11357
  return {};
11002
11358
  }
11003
11359
  }
11004
11360
  async function writeSettingsAt(root, settings) {
11005
- const file = (0, import_node_path3.join)(root, CLAUDE_SETTINGS_FILE);
11006
- await (0, import_promises4.mkdir)((0, import_node_path3.dirname)(file), { recursive: true });
11361
+ const file = (0, import_node_path4.join)(root, CLAUDE_SETTINGS_FILE);
11362
+ await (0, import_promises4.mkdir)((0, import_node_path4.dirname)(file), { recursive: true });
11007
11363
  await (0, import_promises4.writeFile)(file, JSON.stringify(settings, null, 2) + "\n");
11008
11364
  }
11009
11365
  async function hasLegacyHooksAt(root) {
@@ -11244,8 +11600,8 @@ var import_node_crypto2 = require("node:crypto");
11244
11600
 
11245
11601
  // src/lib/conversation-buffer.ts
11246
11602
  var import_promises5 = require("node:fs/promises");
11247
- var import_node_fs2 = require("node:fs");
11248
- var import_node_child_process4 = require("node:child_process");
11603
+ var import_node_fs3 = require("node:fs");
11604
+ var import_node_child_process5 = require("node:child_process");
11249
11605
  function stripImageReferences(text) {
11250
11606
  return text.replace(/\[Image #\d+\]/g, "[screenshot \u2014 not available for review]");
11251
11607
  }
@@ -11276,7 +11632,7 @@ async function appendToConversationBuffer(prompt, sessionId) {
11276
11632
  }
11277
11633
  async function readAndClearConversationBuffer(currentSessionId) {
11278
11634
  try {
11279
- if ((0, import_node_fs2.existsSync)(CONVERSATION_BUFFER_FILE)) {
11635
+ if ((0, import_node_fs3.existsSync)(CONVERSATION_BUFFER_FILE)) {
11280
11636
  const entries = await readBufferEntries();
11281
11637
  let mine = entries;
11282
11638
  let others = [];
@@ -11300,7 +11656,7 @@ async function readAndClearConversationBuffer(currentSessionId) {
11300
11656
  };
11301
11657
  }
11302
11658
  }
11303
- if ((0, import_node_fs2.existsSync)(INTENT_FILE)) {
11659
+ if ((0, import_node_fs3.existsSync)(INTENT_FILE)) {
11304
11660
  try {
11305
11661
  const content = await (0, import_promises5.readFile)(INTENT_FILE, "utf-8");
11306
11662
  await (0, import_promises5.unlink)(INTENT_FILE).catch(() => {
@@ -11344,7 +11700,7 @@ async function readBufferEntries() {
11344
11700
  }
11345
11701
  function getRecentCommitMessages() {
11346
11702
  try {
11347
- const output = (0, import_node_child_process4.execSync)(
11703
+ const output = (0, import_node_child_process5.execSync)(
11348
11704
  'git log --since="30 minutes ago" --format="%s" -5',
11349
11705
  { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
11350
11706
  ).trim();
@@ -11357,8 +11713,8 @@ function getRecentCommitMessages() {
11357
11713
 
11358
11714
  // src/lib/task-context-buffer.ts
11359
11715
  var import_promises6 = require("node:fs/promises");
11360
- var import_node_fs3 = require("node:fs");
11361
- var import_node_path4 = require("node:path");
11716
+ var import_node_fs4 = require("node:fs");
11717
+ var import_node_path5 = require("node:path");
11362
11718
  var TASK_CONTEXT_DIR = `${VERITY_DIR}/.task-context`;
11363
11719
  var MAX_BUFFER_BYTES = 500 * 1024;
11364
11720
  var MAX_PROMPT_CHARS = 2e3;
@@ -11397,7 +11753,7 @@ async function appendResponseToTaskBuffer(taskId, assistantResponse, actionSumma
11397
11753
  }
11398
11754
  async function readTaskContextBuffer(taskId) {
11399
11755
  const filePath = bufferPath(taskId);
11400
- if (!(0, import_node_fs3.existsSync)(filePath)) return null;
11756
+ if (!(0, import_node_fs4.existsSync)(filePath)) return null;
11401
11757
  try {
11402
11758
  const content = await (0, import_promises6.readFile)(filePath, "utf-8");
11403
11759
  if (!content.trim()) return null;
@@ -11431,12 +11787,12 @@ async function readTaskContextBuffer(taskId) {
11431
11787
  }
11432
11788
  async function cleanupTaskContextBuffers() {
11433
11789
  try {
11434
- if (!(0, import_node_fs3.existsSync)(TASK_CONTEXT_DIR)) return;
11790
+ if (!(0, import_node_fs4.existsSync)(TASK_CONTEXT_DIR)) return;
11435
11791
  const files = await (0, import_promises6.readdir)(TASK_CONTEXT_DIR);
11436
11792
  const cutoffMs = Date.now() - RETENTION_DAYS * 24 * 60 * 60 * 1e3;
11437
11793
  for (const file of files) {
11438
11794
  if (!file.endsWith(".jsonl")) continue;
11439
- const filePath = (0, import_node_path4.join)(TASK_CONTEXT_DIR, file);
11795
+ const filePath = (0, import_node_path5.join)(TASK_CONTEXT_DIR, file);
11440
11796
  try {
11441
11797
  const stats = await (0, import_promises6.stat)(filePath);
11442
11798
  if (stats.mtimeMs < cutoffMs) {
@@ -11450,13 +11806,13 @@ async function cleanupTaskContextBuffers() {
11450
11806
  }
11451
11807
  function bufferPath(taskId) {
11452
11808
  const safe = taskId.replace(/[^a-zA-Z0-9_-]/g, "");
11453
- return (0, import_node_path4.join)(TASK_CONTEXT_DIR, `${safe}.jsonl`);
11809
+ return (0, import_node_path5.join)(TASK_CONTEXT_DIR, `${safe}.jsonl`);
11454
11810
  }
11455
11811
  async function appendEntry(taskId, entry) {
11456
11812
  try {
11457
11813
  await (0, import_promises6.mkdir)(TASK_CONTEXT_DIR, { recursive: true });
11458
11814
  const filePath = bufferPath(taskId);
11459
- if ((0, import_node_fs3.existsSync)(filePath)) {
11815
+ if ((0, import_node_fs4.existsSync)(filePath)) {
11460
11816
  const stats = await (0, import_promises6.stat)(filePath);
11461
11817
  if (stats.size >= MAX_BUFFER_BYTES) {
11462
11818
  const content = await (0, import_promises6.readFile)(filePath, "utf-8");
@@ -11467,7 +11823,7 @@ async function appendEntry(taskId, entry) {
11467
11823
  }
11468
11824
  }
11469
11825
  const line = JSON.stringify(entry) + "\n";
11470
- const existing = (0, import_node_fs3.existsSync)(filePath) ? await (0, import_promises6.readFile)(filePath, "utf-8") : "";
11826
+ const existing = (0, import_node_fs4.existsSync)(filePath) ? await (0, import_promises6.readFile)(filePath, "utf-8") : "";
11471
11827
  await (0, import_promises6.writeFile)(filePath, existing + line);
11472
11828
  } catch {
11473
11829
  }
@@ -11475,8 +11831,8 @@ async function appendEntry(taskId, entry) {
11475
11831
 
11476
11832
  // src/lib/memory-retrieval.ts
11477
11833
  var import_promises7 = require("node:fs/promises");
11478
- var import_node_fs4 = require("node:fs");
11479
- var import_node_path5 = require("node:path");
11834
+ var import_node_fs5 = require("node:fs");
11835
+ var import_node_path6 = require("node:path");
11480
11836
  var memoryDir = () => projectPath(`${VERITY_DIR}/memory`);
11481
11837
  var DOMAINS = ["decisions", "quality", "security", "intent", "gotchas", "patterns", "domain", "integrations"];
11482
11838
  var DEFAULT_BUDGET_TOKENS = 2e3;
@@ -11569,19 +11925,19 @@ function parseFrontmatter(content) {
11569
11925
  return { fm, body: match[2].trim() };
11570
11926
  }
11571
11927
  async function retrieveForInjection(promptText, taskFiles = [], budgetTokens = DEFAULT_BUDGET_TOKENS) {
11572
- if (!(0, import_node_fs4.existsSync)(memoryDir())) return null;
11928
+ if (!(0, import_node_fs5.existsSync)(memoryDir())) return null;
11573
11929
  const budget = Math.min(budgetTokens, MAX_BUDGET_TOKENS);
11574
11930
  const promptTokens = tokenize(promptText);
11575
11931
  const nodes = [];
11576
11932
  for (const domain of DOMAINS) {
11577
- const domainDir = (0, import_node_path5.join)(memoryDir(), domain);
11578
- if (!(0, import_node_fs4.existsSync)(domainDir)) continue;
11933
+ const domainDir = (0, import_node_path6.join)(memoryDir(), domain);
11934
+ if (!(0, import_node_fs5.existsSync)(domainDir)) continue;
11579
11935
  try {
11580
11936
  const files = await (0, import_promises7.readdir)(domainDir);
11581
11937
  for (const file of files) {
11582
11938
  if (!file.endsWith(".md")) continue;
11583
11939
  try {
11584
- const content = await (0, import_promises7.readFile)((0, import_node_path5.join)(domainDir, file), "utf-8");
11940
+ const content = await (0, import_promises7.readFile)((0, import_node_path6.join)(domainDir, file), "utf-8");
11585
11941
  const { fm, body } = parseFrontmatter(content);
11586
11942
  if (fm.status && fm.status !== "active") continue;
11587
11943
  nodes.push({
@@ -11640,8 +11996,8 @@ async function retrieveForInjection(promptText, taskFiles = [], budgetTokens = D
11640
11996
 
11641
11997
  // src/lib/memory-sync.ts
11642
11998
  var import_promises8 = require("node:fs/promises");
11643
- var import_node_fs5 = require("node:fs");
11644
- var import_node_path6 = require("node:path");
11999
+ var import_node_fs6 = require("node:fs");
12000
+ var import_node_path7 = require("node:path");
11645
12001
  var import_node_crypto = require("node:crypto");
11646
12002
 
11647
12003
  // src/lib/glob-match.ts
@@ -11712,32 +12068,32 @@ var syncStateFile = () => projectPath(`${VERITY_DIR}/.memory-sync-state.json`);
11712
12068
  async function ensureMemoryDir() {
11713
12069
  await (0, import_promises8.mkdir)(memoryDir2(), { recursive: true });
11714
12070
  for (const domain of DOMAINS2) {
11715
- await (0, import_promises8.mkdir)((0, import_node_path6.join)(memoryDir2(), domain), { recursive: true });
12071
+ await (0, import_promises8.mkdir)((0, import_node_path7.join)(memoryDir2(), domain), { recursive: true });
11716
12072
  }
11717
- if (!(0, import_node_fs5.existsSync)((0, import_node_path6.join)(memoryDir2(), "SCHEMA.md"))) {
11718
- await (0, import_promises8.writeFile)((0, import_node_path6.join)(memoryDir2(), "SCHEMA.md"), SCHEMA_TEMPLATE);
12073
+ if (!(0, import_node_fs6.existsSync)((0, import_node_path7.join)(memoryDir2(), "SCHEMA.md"))) {
12074
+ await (0, import_promises8.writeFile)((0, import_node_path7.join)(memoryDir2(), "SCHEMA.md"), SCHEMA_TEMPLATE);
11719
12075
  }
11720
- if (!(0, import_node_fs5.existsSync)((0, import_node_path6.join)(memoryDir2(), "index.md"))) {
11721
- await (0, import_promises8.writeFile)((0, import_node_path6.join)(memoryDir2(), "index.md"), "# Project Memory Index\n\nNo nodes yet. Run an analysis to start building the knowledge graph.\n");
12076
+ if (!(0, import_node_fs6.existsSync)((0, import_node_path7.join)(memoryDir2(), "index.md"))) {
12077
+ await (0, import_promises8.writeFile)((0, import_node_path7.join)(memoryDir2(), "index.md"), "# Project Memory Index\n\nNo nodes yet. Run an analysis to start building the knowledge graph.\n");
11722
12078
  }
11723
- if (!(0, import_node_fs5.existsSync)((0, import_node_path6.join)(memoryDir2(), "log.md"))) {
11724
- await (0, import_promises8.writeFile)((0, import_node_path6.join)(memoryDir2(), "log.md"), "# Memory Log\n\n");
12079
+ if (!(0, import_node_fs6.existsSync)((0, import_node_path7.join)(memoryDir2(), "log.md"))) {
12080
+ await (0, import_promises8.writeFile)((0, import_node_path7.join)(memoryDir2(), "log.md"), "# Memory Log\n\n");
11725
12081
  }
11726
12082
  }
11727
12083
  async function buildManifest() {
11728
- if (!(0, import_node_fs5.existsSync)(memoryDir2())) {
12084
+ if (!(0, import_node_fs6.existsSync)(memoryDir2())) {
11729
12085
  return { schema_version: 1, nodes: [], index_hash: null, log_length: 0 };
11730
12086
  }
11731
12087
  const nodes = [];
11732
12088
  for (const domain of DOMAINS2) {
11733
- const domainDir = (0, import_node_path6.join)(memoryDir2(), domain);
11734
- if (!(0, import_node_fs5.existsSync)(domainDir)) continue;
12089
+ const domainDir = (0, import_node_path7.join)(memoryDir2(), domain);
12090
+ if (!(0, import_node_fs6.existsSync)(domainDir)) continue;
11735
12091
  try {
11736
12092
  const files = await (0, import_promises8.readdir)(domainDir);
11737
12093
  for (const file of files) {
11738
12094
  if (!file.endsWith(".md")) continue;
11739
12095
  const filePath = `${domain}/${file}`;
11740
- const fullPath = (0, import_node_path6.join)(memoryDir2(), filePath);
12096
+ const fullPath = (0, import_node_path7.join)(memoryDir2(), filePath);
11741
12097
  try {
11742
12098
  const content = await (0, import_promises8.readFile)(fullPath, "utf-8");
11743
12099
  const hash = (0, import_node_crypto.createHash)("sha256").update(content).digest("hex").slice(0, 16);
@@ -11750,13 +12106,13 @@ async function buildManifest() {
11750
12106
  }
11751
12107
  let indexHash = null;
11752
12108
  try {
11753
- const indexContent = await (0, import_promises8.readFile)((0, import_node_path6.join)(memoryDir2(), "index.md"), "utf-8");
12109
+ const indexContent = await (0, import_promises8.readFile)((0, import_node_path7.join)(memoryDir2(), "index.md"), "utf-8");
11754
12110
  indexHash = `sha256:${(0, import_node_crypto.createHash)("sha256").update(indexContent).digest("hex").slice(0, 16)}`;
11755
12111
  } catch {
11756
12112
  }
11757
12113
  let logLength = 0;
11758
12114
  try {
11759
- const logContent = await (0, import_promises8.readFile)((0, import_node_path6.join)(memoryDir2(), "log.md"), "utf-8");
12115
+ const logContent = await (0, import_promises8.readFile)((0, import_node_path7.join)(memoryDir2(), "log.md"), "utf-8");
11760
12116
  logLength = logContent.split("\n").length;
11761
12117
  } catch {
11762
12118
  }
@@ -11767,15 +12123,15 @@ function hashContent(content) {
11767
12123
  }
11768
12124
  async function readOnDiskNodes() {
11769
12125
  const out = /* @__PURE__ */ new Map();
11770
- if (!(0, import_node_fs5.existsSync)(memoryDir2())) return out;
12126
+ if (!(0, import_node_fs6.existsSync)(memoryDir2())) return out;
11771
12127
  for (const domain of DOMAINS2) {
11772
- const domainDir = (0, import_node_path6.join)(memoryDir2(), domain);
11773
- if (!(0, import_node_fs5.existsSync)(domainDir)) continue;
12128
+ const domainDir = (0, import_node_path7.join)(memoryDir2(), domain);
12129
+ if (!(0, import_node_fs6.existsSync)(domainDir)) continue;
11774
12130
  try {
11775
12131
  for (const file of await (0, import_promises8.readdir)(domainDir)) {
11776
12132
  if (!file.endsWith(".md")) continue;
11777
12133
  try {
11778
- out.set(`${domain}/${file}`, hashContent(await (0, import_promises8.readFile)((0, import_node_path6.join)(domainDir, file), "utf-8")));
12134
+ out.set(`${domain}/${file}`, hashContent(await (0, import_promises8.readFile)((0, import_node_path7.join)(domainDir, file), "utf-8")));
11779
12135
  } catch {
11780
12136
  }
11781
12137
  }
@@ -11821,8 +12177,8 @@ async function computeEditedNodeUploads() {
11821
12177
  const uploads = [];
11822
12178
  for (const [path, prevHash] of prev) {
11823
12179
  if (prevHash == null) continue;
11824
- const full = (0, import_node_path6.join)(memoryDir2(), path);
11825
- if (!(0, import_node_fs5.existsSync)(full)) continue;
12180
+ const full = (0, import_node_path7.join)(memoryDir2(), path);
12181
+ if (!(0, import_node_fs6.existsSync)(full)) continue;
11826
12182
  let content;
11827
12183
  try {
11828
12184
  content = await (0, import_promises8.readFile)(full, "utf-8");
@@ -11858,15 +12214,15 @@ async function applyMemoryWrites(writes, opts = {}) {
11858
12214
  const logLines = [`- ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)} \u2014 Applied ${count} write(s) from server`];
11859
12215
  for (const n of notes) logLines.push(` - ${n}`);
11860
12216
  try {
11861
- const existing = (0, import_node_fs5.existsSync)((0, import_node_path6.join)(memoryDir2(), "log.md")) ? await (0, import_promises8.readFile)((0, import_node_path6.join)(memoryDir2(), "log.md"), "utf-8") : "# Memory Log\n\n";
11862
- await (0, import_promises8.writeFile)((0, import_node_path6.join)(memoryDir2(), "log.md"), existing + logLines.join("\n") + "\n");
12217
+ const existing = (0, import_node_fs6.existsSync)((0, import_node_path7.join)(memoryDir2(), "log.md")) ? await (0, import_promises8.readFile)((0, import_node_path7.join)(memoryDir2(), "log.md"), "utf-8") : "# Memory Log\n\n";
12218
+ await (0, import_promises8.writeFile)((0, import_node_path7.join)(memoryDir2(), "log.md"), existing + logLines.join("\n") + "\n");
11863
12219
  } catch {
11864
12220
  }
11865
12221
  await recordSyncedNodePaths();
11866
12222
  return count;
11867
12223
  }
11868
12224
  async function applyOneWrite(write, treePaths) {
11869
- const fullPath = (0, import_node_path6.join)(memoryDir2(), write.path);
12225
+ const fullPath = (0, import_node_path7.join)(memoryDir2(), write.path);
11870
12226
  const notes = [];
11871
12227
  let content = write.content;
11872
12228
  if (treePaths && treePaths.length > 0) {
@@ -11876,7 +12232,7 @@ async function applyOneWrite(write, treePaths) {
11876
12232
  notes.push(`${write.path}: dropped unmatched file_globs [${grounded.dropped.join(", ")}]`);
11877
12233
  }
11878
12234
  }
11879
- if ((0, import_node_fs5.existsSync)(fullPath)) {
12235
+ if ((0, import_node_fs6.existsSync)(fullPath)) {
11880
12236
  let existing = "";
11881
12237
  try {
11882
12238
  existing = await (0, import_promises8.readFile)(fullPath, "utf-8");
@@ -11888,7 +12244,7 @@ async function applyOneWrite(write, treePaths) {
11888
12244
  return { written: false, notes };
11889
12245
  }
11890
12246
  }
11891
- await (0, import_promises8.mkdir)((0, import_node_path6.dirname)(fullPath), { recursive: true });
12247
+ await (0, import_promises8.mkdir)((0, import_node_path7.dirname)(fullPath), { recursive: true });
11892
12248
  await (0, import_promises8.writeFile)(fullPath, content);
11893
12249
  return { written: true, notes };
11894
12250
  }
@@ -11929,8 +12285,8 @@ async function regenerateIndex() {
11929
12285
  ];
11930
12286
  let totalNodes = 0;
11931
12287
  for (const domain of DOMAINS2.filter((d) => d !== "_archive")) {
11932
- const domainDir = (0, import_node_path6.join)(memoryDir2(), domain);
11933
- if (!(0, import_node_fs5.existsSync)(domainDir)) continue;
12288
+ const domainDir = (0, import_node_path7.join)(memoryDir2(), domain);
12289
+ if (!(0, import_node_fs6.existsSync)(domainDir)) continue;
11934
12290
  try {
11935
12291
  const files = await (0, import_promises8.readdir)(domainDir);
11936
12292
  const mdFiles = files.filter((f) => f.endsWith(".md"));
@@ -11940,7 +12296,7 @@ async function regenerateIndex() {
11940
12296
  for (const file of mdFiles.sort()) {
11941
12297
  const slug = file.replace(/\.md$/, "");
11942
12298
  try {
11943
- const content = await (0, import_promises8.readFile)((0, import_node_path6.join)(domainDir, file), "utf-8");
12299
+ const content = await (0, import_promises8.readFile)((0, import_node_path7.join)(domainDir, file), "utf-8");
11944
12300
  const title = pickFrontmatter(content, "title") ?? slug;
11945
12301
  const kind = pickFrontmatter(content, "kind") ?? "-";
11946
12302
  const confidence = pickFrontmatter(content, "confidence");
@@ -11964,7 +12320,7 @@ async function regenerateIndex() {
11964
12320
  lines.push("No nodes yet. Run an analysis to start building the knowledge graph.");
11965
12321
  }
11966
12322
  const next = lines.join("\n") + "\n";
11967
- const indexPath = (0, import_node_path6.join)(memoryDir2(), "index.md");
12323
+ const indexPath = (0, import_node_path7.join)(memoryDir2(), "index.md");
11968
12324
  let existing = null;
11969
12325
  try {
11970
12326
  existing = await (0, import_promises8.readFile)(indexPath, "utf-8");
@@ -12046,9 +12402,9 @@ function hasLegacyMemoryBlock(text) {
12046
12402
  return findMarker(text, LEGACY_MD_START) !== -1;
12047
12403
  }
12048
12404
  async function ensureClaudeMdPointer(cwd = repoRoot()) {
12049
- const claudeMdPath = (0, import_node_path6.join)(cwd, "CLAUDE.md");
12405
+ const claudeMdPath = (0, import_node_path7.join)(cwd, "CLAUDE.md");
12050
12406
  let existing = "";
12051
- if ((0, import_node_fs5.existsSync)(claudeMdPath)) {
12407
+ if ((0, import_node_fs6.existsSync)(claudeMdPath)) {
12052
12408
  existing = await (0, import_promises8.readFile)(claudeMdPath, "utf-8");
12053
12409
  }
12054
12410
  let startTag = CLAUDE_MD_START;
@@ -12178,7 +12534,7 @@ Body content (\u22648KB). Use [[node-id]] wikilinks for cross-references.
12178
12534
  `;
12179
12535
 
12180
12536
  // src/commands/intent.ts
12181
- var import_node_fs6 = require("node:fs");
12537
+ var import_node_fs7 = require("node:fs");
12182
12538
  function registerIntentCommands(program2) {
12183
12539
  const intent = program2.command("intent").description("Manage intent capture");
12184
12540
  intent.command("capture").description("Capture user intent from stdin (used by UserPromptSubmit hook)").action(async () => {
@@ -12187,7 +12543,7 @@ function registerIntentCommands(program2) {
12187
12543
  process.chdir(repoRoot());
12188
12544
  } catch {
12189
12545
  }
12190
- if (!(0, import_node_fs6.existsSync)(VERITY_DIR)) {
12546
+ if (!(0, import_node_fs7.existsSync)(VERITY_DIR)) {
12191
12547
  process.exit(0);
12192
12548
  }
12193
12549
  const chunks = [];
@@ -12766,215 +13122,6 @@ async function sendGeneralFeedback(message, opts, globals) {
12766
13122
  var import_node_fs19 = require("node:fs");
12767
13123
  var import_node_path14 = require("node:path");
12768
13124
 
12769
- // src/lib/git.ts
12770
- var import_node_child_process5 = require("node:child_process");
12771
- var import_node_fs7 = require("node:fs");
12772
- var import_node_path7 = require("node:path");
12773
- function resolveFile(relpath) {
12774
- if ((0, import_node_fs7.existsSync)(relpath)) return relpath;
12775
- if ((0, import_node_fs7.existsSync)(".claude/worktrees")) {
12776
- try {
12777
- const entries = (0, import_node_fs7.readdirSync)(".claude/worktrees", { withFileTypes: true });
12778
- for (const entry of entries) {
12779
- if (!entry.isDirectory()) continue;
12780
- const candidate = (0, import_node_path7.join)(".claude/worktrees", entry.name, relpath);
12781
- if ((0, import_node_fs7.existsSync)(candidate)) return candidate;
12782
- }
12783
- } catch {
12784
- }
12785
- }
12786
- return null;
12787
- }
12788
- function execGit(cmd) {
12789
- try {
12790
- return (0, import_node_child_process5.execSync)(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
12791
- } catch {
12792
- return "";
12793
- }
12794
- }
12795
- function splitLines(s) {
12796
- return s.split("\n").filter((l) => l.length > 0);
12797
- }
12798
- var SHA_RE = /^[0-9a-f]{40}$/;
12799
- function readBaselineSha() {
12800
- if (!(0, import_node_fs7.existsSync)(BASELINE_SHA_FILE)) return null;
12801
- let sha;
12802
- try {
12803
- sha = (0, import_node_fs7.readFileSync)(BASELINE_SHA_FILE, "utf-8").trim();
12804
- } catch {
12805
- return null;
12806
- }
12807
- if (!SHA_RE.test(sha)) return null;
12808
- const reachable = execGit(`git cat-file -e ${sha}^{commit} 2>/dev/null && echo ok`) === "ok";
12809
- if (!reachable) {
12810
- try {
12811
- (0, import_node_fs7.unlinkSync)(BASELINE_SHA_FILE);
12812
- } catch {
12813
- }
12814
- return null;
12815
- }
12816
- return sha;
12817
- }
12818
- function writeBaselineSha(sha) {
12819
- if (!SHA_RE.test(sha)) return;
12820
- try {
12821
- (0, import_node_fs7.mkdirSync)((0, import_node_path7.dirname)(BASELINE_SHA_FILE), { recursive: true });
12822
- (0, import_node_fs7.writeFileSync)(BASELINE_SHA_FILE, sha);
12823
- } catch {
12824
- }
12825
- }
12826
- function getChangedFiles() {
12827
- const sets = /* @__PURE__ */ new Set();
12828
- let hasRecentCommitFiles = false;
12829
- for (const f of splitLines(execGit("git diff --name-only HEAD"))) sets.add(f);
12830
- for (const f of splitLines(execGit("git diff --name-only --cached"))) sets.add(f);
12831
- for (const f of splitLines(execGit("git ls-files --others --exclude-standard"))) sets.add(f);
12832
- const baseline = readBaselineSha();
12833
- if (baseline) {
12834
- const committed = splitLines(execGit(`git diff --name-only ${baseline}..HEAD`));
12835
- if (committed.length > 0) {
12836
- hasRecentCommitFiles = true;
12837
- for (const f of committed) sets.add(f);
12838
- }
12839
- } else {
12840
- const headTimestamp = parseInt(execGit("git log -1 --format=%ct HEAD"), 10) || 0;
12841
- const commitAge = Math.floor(Date.now() / 1e3) - headTimestamp;
12842
- const hasUnstaged = splitLines(execGit("git diff --name-only HEAD")).length > 0;
12843
- const hasStaged = splitLines(execGit("git diff --name-only --cached")).length > 0;
12844
- if (commitAge < 120 && !hasUnstaged && !hasStaged) {
12845
- const recentFiles = splitLines(execGit("git diff --name-only HEAD~1..HEAD"));
12846
- if (recentFiles.length > 0) {
12847
- hasRecentCommitFiles = true;
12848
- for (const f of recentFiles) sets.add(f);
12849
- }
12850
- }
12851
- }
12852
- for (const f of getWorktreeFiles()) sets.add(f);
12853
- const filtered = Array.from(sets).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
12854
- return { files: filtered, hasRecentCommitFiles };
12855
- }
12856
- function getStagedFiles() {
12857
- return splitLines(execGit("git diff --cached --name-only")).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
12858
- }
12859
- function getDirtyFiles() {
12860
- const set = /* @__PURE__ */ new Set();
12861
- for (const f of splitLines(execGit("git diff --name-only HEAD"))) set.add(f);
12862
- for (const f of splitLines(execGit("git diff --name-only --cached"))) set.add(f);
12863
- for (const f of splitLines(execGit("git ls-files --others --exclude-standard"))) set.add(f);
12864
- return Array.from(set).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
12865
- }
12866
- function showContentAtRef(ref, repoRelPath) {
12867
- if (!ref || ref === "no-git") return null;
12868
- const normalizedPath = repoRelPath.replace(/\\/g, "/");
12869
- try {
12870
- return (0, import_node_child_process5.execFileSync)("git", ["show", `${ref}:${normalizedPath}`], {
12871
- encoding: "utf-8",
12872
- maxBuffer: 64 * 1024 * 1024,
12873
- stdio: ["pipe", "pipe", "pipe"]
12874
- });
12875
- } catch {
12876
- return null;
12877
- }
12878
- }
12879
- function getPushRangeFiles() {
12880
- const diff = (range) => splitLines(execGit(`git diff --name-only ${range}`)).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
12881
- const resolvers = [
12882
- () => execGit("git rev-parse --abbrev-ref --symbolic-full-name @{push}") ? "@{push}..HEAD" : null,
12883
- () => execGit("git rev-parse --abbrev-ref --symbolic-full-name @{upstream}") ? "@{upstream}..HEAD" : null,
12884
- () => {
12885
- const branch = execGit("git rev-parse --abbrev-ref HEAD");
12886
- return branch && branch !== "HEAD" && execGit(`git rev-parse --verify -q origin/${branch}`) ? `origin/${branch}..HEAD` : null;
12887
- }
12888
- ];
12889
- for (const resolve of resolvers) {
12890
- const range = resolve();
12891
- if (range) return { files: diff(range), range };
12892
- }
12893
- const baseline = readBaselineSha();
12894
- if (baseline) {
12895
- const files = diff(`${baseline}..HEAD`);
12896
- if (files.length > 0) return { files, range: `${baseline}..HEAD` };
12897
- }
12898
- const last = diff("HEAD~1..HEAD");
12899
- return { files: last, range: last.length > 0 ? "HEAD~1..HEAD" : null };
12900
- }
12901
- function getPushRangeMessages() {
12902
- const { range } = getPushRangeFiles();
12903
- if (!range) return "";
12904
- return execGit(`git log ${range} --format=%B%x00`).split("\0").map((s) => s.trim()).filter(Boolean).join("\n\n");
12905
- }
12906
- function getWorktreeFiles() {
12907
- const result = [];
12908
- const worktreeDir = ".claude/worktrees";
12909
- if (!(0, import_node_fs7.existsSync)(worktreeDir)) return result;
12910
- try {
12911
- const fiveMinAgo = Date.now() - 5 * 60 * 1e3;
12912
- const entries = (0, import_node_fs7.readdirSync)(worktreeDir, { withFileTypes: true });
12913
- for (const entry of entries) {
12914
- if (!entry.isDirectory()) continue;
12915
- const wtDir = (0, import_node_path7.join)(worktreeDir, entry.name);
12916
- scanDir(wtDir, wtDir, fiveMinAgo, result);
12917
- }
12918
- } catch {
12919
- }
12920
- return result;
12921
- }
12922
- function scanDir(baseDir, dir, minMtime, result) {
12923
- try {
12924
- const entries = (0, import_node_fs7.readdirSync)(dir, { withFileTypes: true });
12925
- for (const entry of entries) {
12926
- const fullPath = (0, import_node_path7.join)(dir, entry.name);
12927
- if (entry.isDirectory()) {
12928
- if (entry.name === "node_modules" || entry.name === ".git") continue;
12929
- scanDir(baseDir, fullPath, minMtime, result);
12930
- } else if (entry.isFile()) {
12931
- const ext = (0, import_node_path7.extname)(entry.name).slice(1);
12932
- if (!ANALYZABLE_EXTENSIONS.has(ext)) continue;
12933
- try {
12934
- const stat3 = (0, import_node_fs7.statSync)(fullPath);
12935
- if (stat3.mtimeMs >= minMtime) {
12936
- const relPath = fullPath.slice(baseDir.length + 1);
12937
- result.push(relPath);
12938
- }
12939
- } catch {
12940
- }
12941
- }
12942
- }
12943
- } catch {
12944
- }
12945
- }
12946
- function filterAnalyzable(files) {
12947
- return files.filter((f) => {
12948
- const ext = (0, import_node_path7.extname)(f).slice(1);
12949
- return ANALYZABLE_EXTENSIONS.has(ext);
12950
- });
12951
- }
12952
- function filterReviewable(files) {
12953
- return files.filter((f) => {
12954
- const ext = (0, import_node_path7.extname)(f).slice(1);
12955
- if (ANALYZABLE_EXTENSIONS.has(ext)) return false;
12956
- if (REVIEWABLE_EXTENSIONS.has(ext)) return true;
12957
- const basename2 = f.split("/").pop() ?? "";
12958
- if (REVIEWABLE_FILENAMES.has(basename2)) return true;
12959
- if (REVIEWABLE_PATH_PATTERNS.some((p) => p.test(f))) return true;
12960
- return false;
12961
- });
12962
- }
12963
- function filterSecurity(files) {
12964
- return files.filter(
12965
- (f) => SECURITY_PATTERNS.some((p) => p.test(f))
12966
- );
12967
- }
12968
- function getCurrentCommit() {
12969
- return execGit("git rev-parse HEAD") || "no-git";
12970
- }
12971
- function listTrackedFiles() {
12972
- const set = /* @__PURE__ */ new Set();
12973
- for (const f of splitLines(execGit("git ls-files"))) set.add(f);
12974
- for (const f of splitLines(execGit("git ls-files --others --exclude-standard"))) set.add(f);
12975
- return Array.from(set);
12976
- }
12977
-
12978
13125
  // src/lib/files.ts
12979
13126
  var import_node_fs8 = require("node:fs");
12980
13127
  var import_node_path8 = require("node:path");
@@ -13108,10 +13255,15 @@ function collectCodeDelta(files, opts) {
13108
13255
  // src/lib/debounce.ts
13109
13256
  var import_node_fs9 = require("node:fs");
13110
13257
  var import_node_crypto3 = require("node:crypto");
13111
- function checkDebounce(debounceSeconds = DEBOUNCE_SECONDS) {
13112
- if (!(0, import_node_fs9.existsSync)(DEBOUNCE_FILE)) return null;
13258
+ function scopedFile(base, sessionId) {
13259
+ if (!sessionId) return base;
13260
+ return `${base}.${(0, import_node_crypto3.createHash)("sha1").update(sessionId).digest("hex").slice(0, 12)}`;
13261
+ }
13262
+ function checkDebounce(debounceSeconds = DEBOUNCE_SECONDS, sessionId) {
13263
+ const file = scopedFile(DEBOUNCE_FILE, sessionId);
13264
+ if (!(0, import_node_fs9.existsSync)(file)) return null;
13113
13265
  try {
13114
- const lastTs = parseInt((0, import_node_fs9.readFileSync)(DEBOUNCE_FILE, "utf-8").trim(), 10);
13266
+ const lastTs = parseInt((0, import_node_fs9.readFileSync)(file, "utf-8").trim(), 10);
13115
13267
  const nowTs = Math.floor(Date.now() / 1e3);
13116
13268
  const elapsed = nowTs - lastTs;
13117
13269
  if (elapsed < debounceSeconds) {
@@ -13121,12 +13273,13 @@ function checkDebounce(debounceSeconds = DEBOUNCE_SECONDS) {
13121
13273
  }
13122
13274
  return null;
13123
13275
  }
13124
- function checkMtime(files, bypassForRecentCommits) {
13276
+ function checkMtime(files, bypassForRecentCommits, sessionId) {
13125
13277
  if (bypassForRecentCommits) return null;
13126
- if (!(0, import_node_fs9.existsSync)(DEBOUNCE_FILE)) return null;
13278
+ const file = scopedFile(DEBOUNCE_FILE, sessionId);
13279
+ if (!(0, import_node_fs9.existsSync)(file)) return null;
13127
13280
  let debounceTime;
13128
13281
  try {
13129
- debounceTime = (0, import_node_fs9.statSync)(DEBOUNCE_FILE).mtimeMs;
13282
+ debounceTime = (0, import_node_fs9.statSync)(file).mtimeMs;
13130
13283
  } catch {
13131
13284
  return null;
13132
13285
  }
@@ -13158,11 +13311,12 @@ function computeContentHash(files) {
13158
13311
  }
13159
13312
  return hash.digest("hex");
13160
13313
  }
13161
- function checkContentHash(files) {
13314
+ function checkContentHash(files, sessionId) {
13162
13315
  const hash = computeContentHash(files);
13163
- if ((0, import_node_fs9.existsSync)(HASH_FILE)) {
13316
+ const file = scopedFile(HASH_FILE, sessionId);
13317
+ if ((0, import_node_fs9.existsSync)(file)) {
13164
13318
  try {
13165
- const storedHash = (0, import_node_fs9.readFileSync)(HASH_FILE, "utf-8").trim();
13319
+ const storedHash = (0, import_node_fs9.readFileSync)(file, "utf-8").trim();
13166
13320
  if (hash === storedHash) {
13167
13321
  return { skip: "No source changes since last analysis", hash };
13168
13322
  }
@@ -13171,18 +13325,19 @@ function checkContentHash(files) {
13171
13325
  }
13172
13326
  return { skip: null, hash };
13173
13327
  }
13174
- function recordAnalysisStart() {
13328
+ function recordAnalysisStart(sessionId) {
13175
13329
  (0, import_node_fs9.mkdirSync)(VERITY_DIR, { recursive: true });
13176
- (0, import_node_fs9.writeFileSync)(DEBOUNCE_FILE, String(Math.floor(Date.now() / 1e3)));
13330
+ (0, import_node_fs9.writeFileSync)(scopedFile(DEBOUNCE_FILE, sessionId), String(Math.floor(Date.now() / 1e3)));
13177
13331
  }
13178
- function recordPassHash(hash) {
13179
- (0, import_node_fs9.writeFileSync)(HASH_FILE, hash);
13332
+ function recordPassHash(hash, sessionId) {
13333
+ (0, import_node_fs9.writeFileSync)(scopedFile(HASH_FILE, sessionId), hash);
13180
13334
  }
13181
- function narrowToRecent(files) {
13182
- if (!(0, import_node_fs9.existsSync)(DEBOUNCE_FILE)) return files;
13335
+ function narrowToRecent(files, sessionId) {
13336
+ const file = scopedFile(DEBOUNCE_FILE, sessionId);
13337
+ if (!(0, import_node_fs9.existsSync)(file)) return files;
13183
13338
  let debounceTime;
13184
13339
  try {
13185
- debounceTime = (0, import_node_fs9.statSync)(DEBOUNCE_FILE).mtimeMs;
13340
+ debounceTime = (0, import_node_fs9.statSync)(file).mtimeMs;
13186
13341
  } catch {
13187
13342
  return files;
13188
13343
  }
@@ -14688,7 +14843,7 @@ async function runAnalyze(opts, globals) {
14688
14843
  let currentCommit = "";
14689
14844
  if (analysisMode !== "plan") {
14690
14845
  const debounceSeconds = parseInt(opts.debounce, 10);
14691
- const debounceSkip = checkDebounce(debounceSeconds);
14846
+ const debounceSkip = checkDebounce(debounceSeconds, baselineSessionId);
14692
14847
  if (debounceSkip) {
14693
14848
  if (assistantResponse) {
14694
14849
  analysisMode = "plan";
@@ -14698,7 +14853,7 @@ async function runAnalyze(opts, globals) {
14698
14853
  }
14699
14854
  if (analysisMode !== "plan") {
14700
14855
  const allCheckable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...reviewable, ...securityFiles]));
14701
- const mtimeSkip = checkMtime(allCheckable, hasRecentCommitFiles);
14856
+ const mtimeSkip = checkMtime(allCheckable, hasRecentCommitFiles, baselineSessionId);
14702
14857
  if (mtimeSkip) {
14703
14858
  if (assistantResponse) {
14704
14859
  analysisMode = "plan";
@@ -14708,9 +14863,9 @@ async function runAnalyze(opts, globals) {
14708
14863
  }
14709
14864
  }
14710
14865
  if (analysisMode !== "plan") {
14711
- recordAnalysisStart();
14866
+ recordAnalysisStart(baselineSessionId);
14712
14867
  const allCheckable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...reviewable, ...securityFiles]));
14713
- const hashResult = checkContentHash(allCheckable);
14868
+ const hashResult = checkContentHash(allCheckable, baselineSessionId);
14714
14869
  if (hashResult.skip) {
14715
14870
  if (assistantResponse) {
14716
14871
  analysisMode = "plan";
@@ -14722,7 +14877,7 @@ async function runAnalyze(opts, globals) {
14722
14877
  if (analysisMode !== "plan") {
14723
14878
  const agentNarrowed = narrowToAgentAuthored(allForReview, actionSummary);
14724
14879
  const baseForReview = agentNarrowed.length > 0 ? agentNarrowed : allForReview;
14725
- const recentForReview = narrowToRecent(baseForReview);
14880
+ const recentForReview = narrowToRecent(baseForReview, baselineSessionId);
14726
14881
  if (!opts.skipStatic && isCodacyAvailable()) {
14727
14882
  let allScannable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...securityFiles]));
14728
14883
  if (baseline) {
@@ -15097,7 +15252,7 @@ async function runAnalyze(opts, globals) {
15097
15252
  }
15098
15253
  case "PASS": {
15099
15254
  writeIteration(1, currentCommit, contentHash ?? void 0);
15100
- if (contentHash) recordPassHash(contentHash);
15255
+ if (contentHash) recordPassHash(contentHash, baselineSessionId);
15101
15256
  if (currentCommit && currentCommit !== "no-git") writeBaselineSha(currentCommit);
15102
15257
  let userSummary = response.user_summary ?? "Verity: PASS";
15103
15258
  const viewUrl = response.view_url ?? "";
@@ -15108,7 +15263,7 @@ async function runAnalyze(opts, globals) {
15108
15263
  break;
15109
15264
  }
15110
15265
  case "WARN": {
15111
- if (contentHash) recordPassHash(contentHash);
15266
+ if (contentHash) recordPassHash(contentHash, baselineSessionId);
15112
15267
  if (currentCommit && currentCommit !== "no-git") writeBaselineSha(currentCommit);
15113
15268
  let userSummary = response.user_summary ?? "Verity: WARN";
15114
15269
  const viewUrl = response.view_url ?? "";
@@ -16766,7 +16921,7 @@ function registerTelemetryCommands(program2) {
16766
16921
  }
16767
16922
 
16768
16923
  // src/cli.ts
16769
- program.name("verity").description("CLI for Verity quality gate service").version("0.24.0-experimental.ae62726").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr");
16924
+ program.name("verity").description("CLI for Verity quality gate service").version("0.24.0-experimental.b35bbc7").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr");
16770
16925
  registerAuthCommands(program);
16771
16926
  registerHooksCommands(program);
16772
16927
  registerIntentCommands(program);
@@ -312,18 +312,25 @@ Expected: single-digit findings per file, not hundreds. If you see 50+ issues fr
312
312
 
313
313
  ## Step 6: Register with Verity service
314
314
 
315
- Use the `verity` CLI to register. It handles credential storage and service URL automatically.
315
+ Use the `verity` CLI to register. It handles provider auth, credential storage, and the service URL automatically.
316
316
 
317
317
  ```bash
318
318
  verity auth register --project "PROJECT_NAME" --remote "GIT_REMOTE_URL"
319
319
  ```
320
320
 
321
+ Registration is **provider-gated** (GitHub today): the CLI runs a GitHub OAuth
322
+ **device flow** and prints something like *"open https://github.com/login/device
323
+ and enter code WXYZ-1234"*. The user approves in the browser; the CLI then proves
324
+ the user has **write access** to the repo before the service issues a token. Tell
325
+ the user to expect this prompt and to complete it in their browser.
326
+
321
327
  This command:
322
- - **New project**: Registers, stores token + service URL in `.verity/credentials`, prints `project_id`. Continue.
323
- - **Already registered**: Automatically discovers the project, ensures `.verity/credentials` has the service URL. If a token already exists in credentials, it updates the file and succeeds. If no token exists, it asks you to paste one.
328
+ - **Write access confirmed**: Registers, stores the verity token + service URL + provider token in `.verity/credentials` (perms 600), prints `project_id` and the authenticated email. Continue.
329
+ - **No write access** (`403 NO_WRITE_ACCESS`): The user lacks push rights on the repo they can't register it. Verity still runs locally as an anonymous gate, but no history/org/repo data is stored. Stop.
330
+ - **Non-GitHub remote**: Only GitHub is supported for now; show the error and stop.
324
331
  - **Other errors**: Show the error and stop.
325
332
 
326
- After this step, `.verity/credentials` will contain both `token` and `service_url` — all subsequent `verity` commands will work.
333
+ After this step, `.verity/credentials` will contain `token`, `service_url`, and `provider_token` — all subsequent `verity` commands will work.
327
334
 
328
335
  ---
329
336
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codacy/verity-cli",
3
- "version": "0.24.0-experimental.ae62726",
3
+ "version": "0.24.0-experimental.b35bbc7",
4
4
  "description": "CLI for Verity quality gate service",
5
5
  "homepage": "https://verity.md",
6
6
  "repository": {