threadnote 1.6.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -35501,6 +35501,82 @@ function shouldUseColor() {
35501
35501
  return import_node_process2.stdout.isTTY === true && process.env.NO_COLOR === void 0 && process.env.CI === void 0 && process.env.TERM !== "dumb";
35502
35502
  }
35503
35503
 
35504
+ // src/scrubber.ts
35505
+ var SCRUBBER_PATTERNS = [
35506
+ { name: "private key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
35507
+ { name: "API key (sk-...)", regex: /\bsk-[A-Za-z0-9_-]{16,}/ },
35508
+ { name: "GitHub token", regex: /\bgh[pousr]_[A-Za-z0-9_]{16,}/ },
35509
+ { name: "GitHub fine-grained PAT", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}/ },
35510
+ { name: "GitLab PAT", regex: /\bglpat-[A-Za-z0-9_-]{20,}/ },
35511
+ { name: "bearer token", regex: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/i },
35512
+ { name: "basic auth header", regex: /\bBasic\s+[A-Za-z0-9+/]{16,}={0,2}\b/i },
35513
+ { name: "JWT", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
35514
+ { name: "AWS access key", regex: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/ },
35515
+ { name: "AWS secret access key", regex: /\baws_secret_access_key\s*[:=]\s*["']?[A-Za-z0-9/+=]{35,}["']?/i },
35516
+ { name: "AWS session token", regex: /\baws_session_token\s*[:=]\s*["']?[A-Za-z0-9/+=]{40,}["']?/i },
35517
+ { name: "Google API key", regex: /\bAIza[0-9A-Za-z_-]{35}\b/ },
35518
+ { name: "Google OAuth token", regex: /\bya29\.[0-9A-Za-z_-]{20,}/ },
35519
+ { name: "Stripe key", regex: /\b(?:sk|rk)_(?:live|test)_[0-9A-Za-z]{16,}\b/ },
35520
+ { name: "Stripe webhook secret", regex: /\bwhsec_[0-9A-Za-z]{16,}\b/ },
35521
+ {
35522
+ name: "Discord token",
35523
+ regex: /\b(?:mfa\.[A-Za-z0-9_-]{20,}|[MN][A-Za-z0-9_-]{23}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27,})\b/
35524
+ },
35525
+ {
35526
+ name: "Discord webhook",
35527
+ regex: /https:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[A-Za-z0-9_-]+/i
35528
+ },
35529
+ { name: "Slack token", regex: /\bx(?:app|ox[abcdeprs])(?:-\d-)?[A-Za-z0-9._-]{10,}/i },
35530
+ { name: "Slack webhook", regex: /https:\/\/hooks\.slack\.com\/services\/[A-Z0-9]+\/[A-Z0-9]+\/[A-Za-z0-9]+/i },
35531
+ {
35532
+ name: "database URI",
35533
+ regex: /\b(?:postgres(?:ql)?|mysql|mariadb|mongodb(?:\+srv)?|redis):\/\/[^:\s/@]+:[^@\s]+@[^\s)>"'`,]+/i
35534
+ },
35535
+ { name: "URL basic auth", regex: /\bhttps?:\/\/[^:\s/@]+:[^@\s]+@[^\s)>"'`,]+/i },
35536
+ { name: "macOS home path", placeholder: "<local-path>", regex: /\/Users\/[^\s)>"'`,]+/ },
35537
+ { name: "linux home path", placeholder: "<local-path>", regex: /\/home\/[^\s)>"'`,]+/ }
35538
+ ];
35539
+ function applyScrubber(content, { redact }) {
35540
+ let cleaned = content;
35541
+ const redactions = [];
35542
+ for (const pattern of SCRUBBER_PATTERNS) {
35543
+ if (!pattern.regex.test(cleaned)) {
35544
+ continue;
35545
+ }
35546
+ if (!pattern.placeholder || !redact) {
35547
+ return { blocker: pattern.name, cleaned: content, redactions: [] };
35548
+ }
35549
+ const globalRegex = globalize(pattern.regex);
35550
+ const matches = cleaned.match(globalRegex) ?? [];
35551
+ cleaned = cleaned.replace(globalRegex, pattern.placeholder);
35552
+ redactions.push({ count: matches.length, name: pattern.name });
35553
+ }
35554
+ return { cleaned, redactions };
35555
+ }
35556
+ function credentialScrubberBlocker(content) {
35557
+ for (const pattern of SCRUBBER_PATTERNS) {
35558
+ if (pattern.placeholder === void 0 && pattern.regex.test(content)) {
35559
+ return pattern.name;
35560
+ }
35561
+ }
35562
+ return void 0;
35563
+ }
35564
+ function redactSensitiveText(content) {
35565
+ let redacted = content.replace(
35566
+ /([A-Za-z0-9_.-]*(?:token|secret|password|api[_-]?key|authorization|credential|session)[A-Za-z0-9_.-]*\s*[:=]\s*)("[^"]+"|'[^']+'|Bearer\s+[^'"\s]+|\S+)/gi,
35567
+ "$1[REDACTED]"
35568
+ );
35569
+ for (const pattern of SCRUBBER_PATTERNS) {
35570
+ const placeholder = pattern.placeholder ?? "<secret>";
35571
+ redacted = redacted.replace(globalize(pattern.regex), placeholder);
35572
+ }
35573
+ return redacted;
35574
+ }
35575
+ function globalize(regex) {
35576
+ const flags = regex.flags.includes("g") ? regex.flags : `${regex.flags}g`;
35577
+ return new RegExp(regex.source, flags);
35578
+ }
35579
+
35504
35580
  // src/utils.ts
35505
35581
  function isJsonObject(value) {
35506
35582
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -35514,10 +35590,24 @@ function parseJsonConfigObject(content) {
35514
35590
  }
35515
35591
  }
35516
35592
  function redactText(content) {
35517
- return content.replace(
35518
- /([A-Za-z0-9_.-]*(?:token|secret|password|api[_-]?key|authorization)[A-Za-z0-9_.-]*\s*[:=]\s*)("[^"]+"|'[^']+'|Bearer\s+[^'"\s]+|\S+)/gi,
35519
- "$1[REDACTED]"
35520
- ).replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]").replace(/sk-[A-Za-z0-9_-]{16,}/g, "sk-[REDACTED]").replace(/gh[pousr]_[A-Za-z0-9_]{16,}/g, "gh_[REDACTED]");
35593
+ return redactSensitiveText(content);
35594
+ }
35595
+ var GIT_ENVIRONMENT_KEYS = [
35596
+ "GIT_DIR",
35597
+ "GIT_WORK_TREE",
35598
+ "GIT_INDEX_FILE",
35599
+ "GIT_PREFIX",
35600
+ "GIT_COMMON_DIR",
35601
+ "GIT_OBJECT_DIRECTORY",
35602
+ "GIT_ALTERNATE_OBJECT_DIRECTORIES",
35603
+ "GIT_QUARANTINE_PATH"
35604
+ ];
35605
+ function withoutGitEnvironment(env = process.env) {
35606
+ const next = { ...env };
35607
+ for (const key of GIT_ENVIRONMENT_KEYS) {
35608
+ delete next[key];
35609
+ }
35610
+ return next;
35521
35611
  }
35522
35612
  function escapeRegExp(value) {
35523
35613
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -35638,6 +35728,7 @@ async function runCommand(executable, args, options = {}) {
35638
35728
  {
35639
35729
  cwd: options.cwd,
35640
35730
  encoding: "utf8",
35731
+ env: commandEnvironment(executable, options.env),
35641
35732
  maxBuffer: maxOutputBytes
35642
35733
  },
35643
35734
  (err, stdout2, stderr) => {
@@ -35677,6 +35768,12 @@ async function runCommand(executable, args, options = {}) {
35677
35768
  }
35678
35769
  });
35679
35770
  }
35771
+ function commandEnvironment(executable, env) {
35772
+ if ((0, import_node_path.basename)(executable) !== "git") {
35773
+ return env;
35774
+ }
35775
+ return withoutGitEnvironment(env ?? process.env);
35776
+ }
35680
35777
  function commandResultFromExecFileCallback(params) {
35681
35778
  if (params.failureMessage) {
35682
35779
  return { exitCode: 124, stderr: params.failureMessage, stdout: params.stdout };
@@ -35713,6 +35810,64 @@ async function gitValue(args, cwd = getInvocationCwd()) {
35713
35810
  }
35714
35811
  return result.stdout.trim();
35715
35812
  }
35813
+ async function resolveRepoName(cwd = getInvocationCwd()) {
35814
+ const repoRoot = await gitValue(["rev-parse", "--show-toplevel"], cwd);
35815
+ if (!repoRoot) {
35816
+ return void 0;
35817
+ }
35818
+ const remoteName = await resolveGitRemoteRepoName(repoRoot);
35819
+ if (remoteName) {
35820
+ return remoteName;
35821
+ }
35822
+ return resolveRepoFolderName(repoRoot);
35823
+ }
35824
+ async function resolveRepoFolderName(cwd = getInvocationCwd()) {
35825
+ const repoRoot = await gitValue(["rev-parse", "--show-toplevel"], cwd);
35826
+ if (!repoRoot) {
35827
+ return void 0;
35828
+ }
35829
+ const commonDir = await gitValue(["rev-parse", "--git-common-dir"], repoRoot);
35830
+ if (commonDir) {
35831
+ const absoluteCommonDir = (0, import_node_path.isAbsolute)(commonDir) ? commonDir : (0, import_node_path.resolve)(repoRoot, commonDir);
35832
+ const primaryRoot = (0, import_node_path.basename)(absoluteCommonDir) === ".git" ? (0, import_node_path.dirname)(absoluteCommonDir) : absoluteCommonDir;
35833
+ const name = (0, import_node_path.basename)(primaryRoot).replace(/\.git$/, "");
35834
+ if (name && name !== ".") {
35835
+ return name;
35836
+ }
35837
+ }
35838
+ return (0, import_node_path.basename)(repoRoot);
35839
+ }
35840
+ async function resolveGitRemoteRepoName(repoRoot) {
35841
+ const originUrl = await gitValue(["remote", "get-url", "origin"], repoRoot);
35842
+ const originName = originUrl ? gitRemoteRepoName(originUrl) : void 0;
35843
+ if (originName) {
35844
+ return originName;
35845
+ }
35846
+ const remotes = await gitValue(["remote"], repoRoot);
35847
+ const remote = remotes?.split(/\r?\n/).map((name) => name.trim()).find((name) => name.length > 0);
35848
+ if (!remote) {
35849
+ return void 0;
35850
+ }
35851
+ const remoteUrl = await gitValue(["remote", "get-url", remote], repoRoot);
35852
+ return remoteUrl ? gitRemoteRepoName(remoteUrl) : void 0;
35853
+ }
35854
+ function gitRemoteRepoName(remoteUrl) {
35855
+ const trimmed = remoteUrl.trim();
35856
+ if (!trimmed) {
35857
+ return void 0;
35858
+ }
35859
+ let remotePath = trimmed.replace(/[?#].*$/, "");
35860
+ try {
35861
+ remotePath = new URL(trimmed).pathname;
35862
+ } catch (_err) {
35863
+ const scpLike = trimmed.match(/^[^@\s/]+@[^:\s]+:(.+)$/);
35864
+ if (scpLike?.[1]) {
35865
+ remotePath = scpLike[1];
35866
+ }
35867
+ }
35868
+ const name = remotePath.replace(/[\\/]+$/, "").split(/[\\/:]/).filter(Boolean).pop()?.replace(/\.git$/i, "");
35869
+ return name && name !== "." && name !== ".." ? name : void 0;
35870
+ }
35716
35871
  async function sleep(ms) {
35717
35872
  return new Promise((resolvePromise) => {
35718
35873
  setTimeout(resolvePromise, ms);
@@ -35853,6 +36008,13 @@ async function enrichRecallQueryWithWorkspaceContext(query, options = {}) {
35853
36008
  async function enrichRecallQueryWithWorkspaceProjectContext(query, options = {}) {
35854
36009
  return enrichRecallQueryWithWorkspaceTerms(query, options, false);
35855
36010
  }
36011
+ async function resolveWorkspaceRepoName(options = {}) {
36012
+ const cwd = options.cwd ?? (options.includeProcessCwd === false ? void 0 : getInvocationCwd());
36013
+ if (!cwd || !(0, import_node_path.isAbsolute)(cwd)) {
36014
+ return void 0;
36015
+ }
36016
+ return resolveRepoName(cwd);
36017
+ }
35856
36018
  async function enrichRecallQueryWithWorkspaceTerms(query, options, includeBranch) {
35857
36019
  if (!recallQueryRequestsWorkspaceContext(query)) {
35858
36020
  return query;
@@ -35871,10 +36033,11 @@ async function currentWorkspaceRecallTerms(options, includeBranch) {
35871
36033
  return [];
35872
36034
  }
35873
36035
  const branch = await gitValue(["branch", "--show-current"], repoRoot);
36036
+ const repoName = await resolveWorkspaceRepoName({ cwd, includeProcessCwd: false });
35874
36037
  const parent = (0, import_node_path.dirname)(repoRoot);
35875
36038
  return uniqueUsefulWorkspaceTerms([
35876
36039
  { source: "branch", value: includeBranch ? branch : void 0 },
35877
- { source: "path", value: (0, import_node_path.basename)(repoRoot) },
36040
+ { source: "path", value: repoName },
35878
36041
  { source: "path", value: parent === (0, import_node_os.homedir)() ? void 0 : (0, import_node_path.basename)(parent) }
35879
36042
  ]);
35880
36043
  }
@@ -36778,53 +36941,6 @@ var PACK_FILES_DIR = "files";
36778
36941
  var PACK_ROOT_TOKEN = "${THREADNOTE_PACK_ROOT}";
36779
36942
  var AUTO_SHARE_FETCH_INTERVAL_MS = 5 * 60 * 1e3;
36780
36943
  var DEFAULT_GIT_REMOTE_NAME = "origin";
36781
- var SCRUBBER_PATTERNS = [
36782
- // Credentials: never redactable. Blocking is the only safe response —
36783
- // automated redaction risks false negatives that leave material in git.
36784
- { name: "private key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
36785
- { name: "API key (sk-...)", regex: /\bsk-[A-Za-z0-9_-]{16,}/ },
36786
- { name: "GitHub token", regex: /\bgh[pousr]_[A-Za-z0-9_]{16,}/ },
36787
- { name: "GitHub fine-grained PAT", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}/ },
36788
- { name: "GitLab PAT", regex: /\bglpat-[A-Za-z0-9_-]{20,}/ },
36789
- { name: "bearer token", regex: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/i },
36790
- // Matches bare JWTs (three base64url segments). May surface a JWE token in
36791
- // legitimate docs; if that becomes noisy we can switch to warn-only.
36792
- { name: "JWT", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
36793
- { name: "AWS access key", regex: /\bAKIA[0-9A-Z]{16}\b/ },
36794
- // Slack tokens: xoxa/xoxb/xoxc (configuration)/xoxd (legacy user cookie)/
36795
- // xoxe (refresh)/xoxp/xoxr/xoxs, with optional -N- segment for the workspace tier.
36796
- { name: "Slack token", regex: /\bxox[abcdeprs](?:-\d-)?[A-Za-z0-9._-]{10,}/i },
36797
- // Soft leaks: block by default (so the agent sees them and decides), but
36798
- // allow opt-in redaction so curated memories with incidental matches can
36799
- // ship without a manual rewrite. Local home paths are the recurring
36800
- // real-world leak; the regexes greedily consume the whole path segment
36801
- // (including subdirectories) up to whitespace or common closing punctuation
36802
- // so redaction collapses an entire path to a single placeholder rather than
36803
- // leaving the subpath visible.
36804
- { name: "macOS home path", placeholder: "<local-path>", regex: /\/Users\/[^\s)>"'`,]+/ },
36805
- // No leading \b: it only matches when a word char precedes the slash, which
36806
- // misses the common cases (after =, :, whitespace, or line start). Mirror the
36807
- // macOS pattern so /home/... is caught in every position.
36808
- { name: "linux home path", placeholder: "<local-path>", regex: /\/home\/[^\s)>"'`,]+/ }
36809
- ];
36810
- function applyScrubber(content, { redact }) {
36811
- let cleaned = content;
36812
- const redactions = [];
36813
- for (const pattern of SCRUBBER_PATTERNS) {
36814
- if (!pattern.regex.test(cleaned)) {
36815
- continue;
36816
- }
36817
- if (!pattern.placeholder || !redact) {
36818
- return { blocker: pattern.name, cleaned: content, redactions: [] };
36819
- }
36820
- const flags = pattern.regex.flags.includes("g") ? pattern.regex.flags : `${pattern.regex.flags}g`;
36821
- const globalRegex = new RegExp(pattern.regex.source, flags);
36822
- const matches = cleaned.match(globalRegex) ?? [];
36823
- cleaned = cleaned.replace(globalRegex, pattern.placeholder);
36824
- redactions.push({ count: matches.length, name: pattern.name });
36825
- }
36826
- return { cleaned, redactions };
36827
- }
36828
36944
  var autoShareStates = /* @__PURE__ */ new Map();
36829
36945
  function startShareBackgroundFetch(config2) {
36830
36946
  const state = autoShareState(config2);
@@ -37053,7 +37169,7 @@ async function publishShareGitChange(worktree, relativePath, commitMessage, opti
37053
37169
  const git = await requiredExecutable("git");
37054
37170
  const messages = [];
37055
37171
  const paths = typeof relativePath === "string" ? [relativePath] : [...relativePath];
37056
- const stageArgs = verb === "rm" ? ["-C", worktree, "rm", ...paths] : ["-C", worktree, "add", "--", ...paths];
37172
+ const stageArgs = verb === "rm" ? ["-C", worktree, "rm", "--", ...paths] : ["-C", worktree, "add", "--", ...paths];
37057
37173
  const stageResult = await runGitCommand(dryRun, git, stageArgs, `git ${verb} failed`);
37058
37174
  if (stageResult) {
37059
37175
  messages.push(`git ${verb}: ${stageResult.stdout.trim() || "ok"}`);
@@ -37396,13 +37512,7 @@ function isProbablyBinary(buffer) {
37396
37512
  }
37397
37513
  }
37398
37514
  function detectBinaryCredential(buffer) {
37399
- const latin1 = buffer.toString("latin1");
37400
- for (const pattern of SCRUBBER_PATTERNS) {
37401
- if (pattern.placeholder === void 0 && pattern.regex.test(latin1)) {
37402
- return pattern.name;
37403
- }
37404
- }
37405
- return void 0;
37515
+ return credentialScrubberBlocker(buffer.toString("latin1"));
37406
37516
  }
37407
37517
  function detectBinaryLocalPath(buffer, rewriteRoots) {
37408
37518
  const latin1 = buffer.toString("latin1");
@@ -38653,6 +38763,18 @@ function stripPersonalProvenance(content) {
38653
38763
  }
38654
38764
  return cleaned.join("\n");
38655
38765
  }
38766
+ async function readMemoryContent(config2, ov, uri, dryRun) {
38767
+ const args = withIdentity(config2, ["read", uri]);
38768
+ if (dryRun) {
38769
+ console.log(`Would run: ${formatShellCommand(ov, args)}`);
38770
+ return "<dry-run memory body>";
38771
+ }
38772
+ const result = await runCommand(ov, args);
38773
+ if (!result.stdout.trim()) {
38774
+ throw new Error(`Refusing to publish empty memory at ${uri}`);
38775
+ }
38776
+ return result.stdout;
38777
+ }
38656
38778
  async function ensureSharedDirectoryChain(config2, ov, memoryUri, dryRun, options = {}) {
38657
38779
  const directoryUri = parentUri(memoryUri);
38658
38780
  for (const uri of sharedDirectoryChain(config2, directoryUri)) {
@@ -38810,9 +38932,19 @@ function isResourceBusyFailure(stderr, stdout2) {
38810
38932
  ${stdout2}`.toLowerCase();
38811
38933
  return output.includes("resource is busy") || output.includes("resource is being processed");
38812
38934
  }
38813
- async function ingestSingleFile(ov, config2, uri, filePath, initialMode, options = {}) {
38814
- const content = await (0, import_promises4.readFile)(filePath, "utf8");
38815
- await writeMemoryFile(config2, ov, uri, content, initialMode, false, options);
38935
+ async function readSharedInboundFileContent(uri, filePath) {
38936
+ if (!await isRegularFileNoSymlink(filePath)) {
38937
+ throw new Error(`Refusing to ingest non-regular shared file: ${filePath}`);
38938
+ }
38939
+ return prepareSharedInboundContent(uri, await (0, import_promises4.readFile)(filePath, "utf8"));
38940
+ }
38941
+ function prepareSharedInboundContent(uri, rawContent) {
38942
+ const stripped = stripPersonalProvenance(rawContent);
38943
+ const scrub = applyScrubber(stripped, { redact: false });
38944
+ if (scrub.blocker) {
38945
+ throw new Error(`Refusing to ingest ${uri}: possible ${scrub.blocker}. Strip the sensitive value upstream first.`);
38946
+ }
38947
+ return scrub.cleaned;
38816
38948
  }
38817
38949
  async function removeMemoryUri(config2, ov, uri, dryRun, options = {}) {
38818
38950
  const args = withIdentity(config2, ["rm", uri]);
@@ -38860,8 +38992,10 @@ async function gitOutput(worktree, args, dryRun) {
38860
38992
  }
38861
38993
  return result.stdout.trim();
38862
38994
  }
38995
+ var GIT_MODE_ABSENT = "000000";
38996
+ var GIT_MODE_SYMLINK = "120000";
38863
38997
  async function listChangedFiles(worktree, beforeRev, afterRev) {
38864
- const result = await runCommand("git", ["-C", worktree, "diff", "--name-status", "-z", `${beforeRev}..${afterRev}`], {
38998
+ const result = await runCommand("git", ["-C", worktree, "diff", "--raw", "-z", `${beforeRev}..${afterRev}`], {
38865
38999
  allowFailure: true
38866
39000
  });
38867
39001
  if (result.exitCode !== 0) {
@@ -38870,27 +39004,65 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
38870
39004
  const entries = result.stdout.split("\0").filter((part) => part.length > 0);
38871
39005
  const changes = [];
38872
39006
  for (let index = 0; index < entries.length; ) {
38873
- const raw = entries[index];
38874
- const head = raw.slice(0, 1);
39007
+ const raw = entries[index++];
39008
+ const match = raw.match(/^:(\d{6}) (\d{6}) [0-9a-f]+ [0-9a-f]+ ([A-Z])\d*/);
39009
+ if (!match) {
39010
+ continue;
39011
+ }
39012
+ const [, oldMode, newMode, head] = match;
38875
39013
  if (head === "R" || head === "C") {
38876
- const oldRel = entries[index + 1];
38877
- const newRel = entries[index + 2];
38878
- if (oldRel && newRel) {
38879
- changes.push({ path: (0, import_node_path3.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
39014
+ const oldRel = entries[index];
39015
+ const newRel = entries[index + 1];
39016
+ if (oldRel) {
39017
+ changes.push({
39018
+ path: (0, import_node_path3.join)(worktree, oldRel),
39019
+ previousContent: oldMode === GIT_MODE_SYMLINK ? void 0 : await gitFileContent(worktree, beforeRev, oldRel),
39020
+ relativePath: oldRel,
39021
+ status: "removed"
39022
+ });
39023
+ }
39024
+ if (newRel && newMode !== GIT_MODE_SYMLINK) {
38880
39025
  changes.push({ path: (0, import_node_path3.join)(worktree, newRel), relativePath: newRel, status: "added" });
38881
39026
  }
38882
- index += 3;
39027
+ index += 2;
38883
39028
  continue;
38884
39029
  }
38885
- const rel = entries[index + 1];
39030
+ const rel = entries[index];
38886
39031
  if (rel) {
38887
- const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
38888
- changes.push({ path: (0, import_node_path3.join)(worktree, rel), relativePath: rel, status });
39032
+ if (head === "D") {
39033
+ changes.push({
39034
+ path: (0, import_node_path3.join)(worktree, rel),
39035
+ previousContent: oldMode === GIT_MODE_SYMLINK ? void 0 : await gitFileContent(worktree, beforeRev, rel),
39036
+ relativePath: rel,
39037
+ status: "removed"
39038
+ });
39039
+ } else if (newMode === GIT_MODE_SYMLINK) {
39040
+ if (oldMode !== GIT_MODE_ABSENT) {
39041
+ changes.push({
39042
+ path: (0, import_node_path3.join)(worktree, rel),
39043
+ previousContent: oldMode === GIT_MODE_SYMLINK ? void 0 : await gitFileContent(worktree, beforeRev, rel),
39044
+ relativePath: rel,
39045
+ status: "removed"
39046
+ });
39047
+ }
39048
+ } else {
39049
+ const status = head === "A" ? "added" : "modified";
39050
+ changes.push({
39051
+ path: (0, import_node_path3.join)(worktree, rel),
39052
+ previousContent: oldMode === GIT_MODE_ABSENT || oldMode === GIT_MODE_SYMLINK ? void 0 : await gitFileContent(worktree, beforeRev, rel),
39053
+ relativePath: rel,
39054
+ status
39055
+ });
39056
+ }
38889
39057
  }
38890
- index += 2;
39058
+ index += 1;
38891
39059
  }
38892
39060
  return changes;
38893
39061
  }
39062
+ async function gitFileContent(worktree, rev, relativePath) {
39063
+ const result = await runCommand("git", ["-C", worktree, "show", `${rev}:${relativePath}`], { allowFailure: true });
39064
+ return result.exitCode === 0 ? result.stdout : void 0;
39065
+ }
38894
39066
  async function applyChangesToOpenViking(config2, team, changes, options = {}) {
38895
39067
  const ov = await openVikingCliForMode(false);
38896
39068
  const failed = [];
@@ -38905,22 +39077,37 @@ async function applyChangesToOpenViking(config2, team, changes, options = {}) {
38905
39077
  const uri = workfileToVikingUri(config2, team, change.path);
38906
39078
  try {
38907
39079
  if (change.status === "removed") {
39080
+ const currentContent2 = await readExistingMemoryContent(config2, ov, uri);
39081
+ if (currentContent2 === void 0) {
39082
+ continue;
39083
+ }
39084
+ assertInboundPreviousContentMatches(change, uri, currentContent2);
38908
39085
  await removeMemoryUri(config2, ov, uri, false, options);
38909
39086
  continue;
38910
39087
  }
38911
- if (!await isFile(change.path)) {
39088
+ if (!await isRegularFileNoSymlink(change.path)) {
38912
39089
  continue;
38913
39090
  }
38914
- const ovHasResource = await vikingResourceExists(ov, config2, uri);
38915
- if (ovHasResource) {
38916
- const reason = change.status === "modified" ? "overwriting local with upstream (local edits to the shared subtree are not preserved across sync)" : "aligning OV to upstream (resource pre-existed in OV, likely from an earlier local publish or sync)";
39091
+ const content = await readSharedInboundFileContent(uri, change.path);
39092
+ const currentContent = await readExistingMemoryContent(config2, ov, uri);
39093
+ if (currentContent !== void 0) {
39094
+ if (change.status === "added") {
39095
+ if (currentContent === content) {
39096
+ continue;
39097
+ }
39098
+ throw new Error(
39099
+ `Refusing to ingest newly added shared file over existing local OpenViking resource ${uri}; inspect and resolve the local edit first.`
39100
+ );
39101
+ }
39102
+ assertInboundPreviousContentMatches(change, uri, currentContent);
39103
+ const reason = change.status === "modified" ? "updating from upstream after verifying local content matches the previous shared version" : "aligning OV to upstream (resource pre-existed in OV, likely from an earlier local publish or sync)";
38917
39104
  if (options.quiet !== true) {
38918
39105
  console.warn(`share sync: ${uri}: ${reason}.`);
38919
39106
  }
38920
39107
  }
38921
39108
  await ensureSharedDirectoryChain(config2, ov, uri, false, options);
38922
- const writeMode = ovHasResource ? "replace" : "create";
38923
- await ingestSingleFile(ov, config2, uri, change.path, writeMode, options);
39109
+ const writeMode = currentContent !== void 0 ? "replace" : "create";
39110
+ await writeMemoryFile(config2, ov, uri, content, writeMode, false, options);
38924
39111
  } catch (err) {
38925
39112
  const message = err instanceof Error ? err.message : String(err);
38926
39113
  if (options.quiet !== true) {
@@ -38931,6 +39118,25 @@ async function applyChangesToOpenViking(config2, team, changes, options = {}) {
38931
39118
  }
38932
39119
  return { failed };
38933
39120
  }
39121
+ async function readExistingMemoryContent(config2, ov, uri) {
39122
+ if (!await vikingResourceExists(ov, config2, uri)) {
39123
+ return void 0;
39124
+ }
39125
+ return readMemoryContent(config2, ov, uri, false);
39126
+ }
39127
+ function assertInboundPreviousContentMatches(change, uri, currentContent) {
39128
+ if (change.previousContent === void 0) {
39129
+ throw new Error(
39130
+ `Refusing to apply inbound shared change for ${uri}: previous shared content is unavailable, so local edits cannot be distinguished from upstream edits.`
39131
+ );
39132
+ }
39133
+ const expectedContent = prepareSharedInboundContent(uri, change.previousContent);
39134
+ if (currentContent !== expectedContent) {
39135
+ throw new Error(
39136
+ `Refusing to apply inbound shared change for ${uri}: local OpenViking content differs from the previous shared version. Inspect and resolve the local edit first.`
39137
+ );
39138
+ }
39139
+ }
38934
39140
  function mergeChanges(...lists) {
38935
39141
  const map2 = /* @__PURE__ */ new Map();
38936
39142
  for (const list of lists) {
@@ -39659,6 +39865,10 @@ function registerTools(server, config2) {
39659
39865
  if (!checkedPattern.ok) {
39660
39866
  return checkedPattern.error;
39661
39867
  }
39868
+ const checkedLiteralPattern = rejectLeadingDash(checkedPattern.value, "grep", "pattern");
39869
+ if (!checkedLiteralPattern.ok) {
39870
+ return checkedLiteralPattern.error;
39871
+ }
39662
39872
  const checkedUri = optionalVikingUri(uri, "grep");
39663
39873
  if (!checkedUri.ok) {
39664
39874
  return checkedUri.error;
@@ -39666,7 +39876,7 @@ function registerTools(server, config2) {
39666
39876
  return runOpenVikingMcpTool(config2, "grep", {
39667
39877
  case_insensitive: caseInsensitive ?? case_insensitive,
39668
39878
  node_limit: nodeLimit ?? node_limit,
39669
- pattern: checkedPattern.value,
39879
+ pattern: checkedLiteralPattern.value,
39670
39880
  uri: checkedUri.value ?? `viking://user/${uriSegment2(config2.user)}/memories`
39671
39881
  });
39672
39882
  }
@@ -39688,13 +39898,17 @@ function registerTools(server, config2) {
39688
39898
  if (!checkedPattern.ok) {
39689
39899
  return checkedPattern.error;
39690
39900
  }
39901
+ const checkedLiteralPattern = rejectLeadingDash(checkedPattern.value, "glob", "pattern");
39902
+ if (!checkedLiteralPattern.ok) {
39903
+ return checkedLiteralPattern.error;
39904
+ }
39691
39905
  const checkedUri = optionalVikingUri(uri, "glob");
39692
39906
  if (!checkedUri.ok) {
39693
39907
  return checkedUri.error;
39694
39908
  }
39695
39909
  return runOpenVikingMcpTool(config2, "glob", {
39696
39910
  node_limit: nodeLimit ?? node_limit,
39697
- pattern: checkedPattern.value,
39911
+ pattern: checkedLiteralPattern.value,
39698
39912
  uri: checkedUri.value
39699
39913
  });
39700
39914
  }
@@ -40007,6 +40221,10 @@ function registerOpenVikingParityTools(server, config2) {
40007
40221
  if (!checkedPattern.ok) {
40008
40222
  return checkedPattern.error;
40009
40223
  }
40224
+ const checkedLiteralPattern = rejectLeadingDash(checkedPattern.value, "ov_grep", "pattern");
40225
+ if (!checkedLiteralPattern.ok) {
40226
+ return checkedLiteralPattern.error;
40227
+ }
40010
40228
  const checkedUri = optionalVikingUri(uri, "ov_grep");
40011
40229
  if (!checkedUri.ok) {
40012
40230
  return checkedUri.error;
@@ -40014,7 +40232,7 @@ function registerOpenVikingParityTools(server, config2) {
40014
40232
  return runOpenVikingMcpTool(config2, "grep", {
40015
40233
  case_insensitive: caseInsensitive ?? case_insensitive,
40016
40234
  node_limit: nodeLimit ?? node_limit,
40017
- pattern: checkedPattern.value,
40235
+ pattern: checkedLiteralPattern.value,
40018
40236
  uri: checkedUri.value
40019
40237
  });
40020
40238
  }
@@ -40036,13 +40254,17 @@ function registerOpenVikingParityTools(server, config2) {
40036
40254
  if (!checkedPattern.ok) {
40037
40255
  return checkedPattern.error;
40038
40256
  }
40257
+ const checkedLiteralPattern = rejectLeadingDash(checkedPattern.value, "ov_glob", "pattern");
40258
+ if (!checkedLiteralPattern.ok) {
40259
+ return checkedLiteralPattern.error;
40260
+ }
40039
40261
  const checkedUri = optionalVikingUri(uri, "ov_glob");
40040
40262
  if (!checkedUri.ok) {
40041
40263
  return checkedUri.error;
40042
40264
  }
40043
40265
  return runOpenVikingMcpTool(config2, "glob", {
40044
40266
  node_limit: nodeLimit ?? node_limit,
40045
- pattern: checkedPattern.value,
40267
+ pattern: checkedLiteralPattern.value,
40046
40268
  uri: checkedUri.value
40047
40269
  });
40048
40270
  }
@@ -40243,6 +40465,7 @@ async function runRecallTool(config2, params) {
40243
40465
  indexRepairMessages = [`Auto-index repair warning: ${errorMessage(err)}`];
40244
40466
  }
40245
40467
  const project = params.pinnedUri ? void 0 : await inferProjectFromQuery(config2.manifestPath, projectQuery);
40468
+ const projectMemoryName = params.pinnedUri ? void 0 : await resolveWorkspaceRepoName({ cwd: params.callerCwd, includeProcessCwd: false });
40246
40469
  const limitArgs = params.nodeLimit ? ["--node-limit", String(params.nodeLimit)] : [];
40247
40470
  const threshold = params.threshold ?? RECALL_SCORE_THRESHOLD;
40248
40471
  const explicitWorkset = params.workset ? await requireWorkset(config2.manifestPath, params.workset) : void 0;
@@ -40254,6 +40477,19 @@ async function runRecallTool(config2, params) {
40254
40477
  params.includeArchived
40255
40478
  );
40256
40479
  const passes = [base.hits];
40480
+ const scopedRecallUris = new Set([params.pinnedUri].filter((uri) => uri !== void 0));
40481
+ for (const scope of projectMemoryScopeUris(config2, projectMemoryName, params.includeArchived)) {
40482
+ if (!scopedRecallUris.has(scope)) {
40483
+ scopedRecallUris.add(scope);
40484
+ const projectMemoryPass = await recallSearchHits(
40485
+ config2,
40486
+ ["search", query, "--uri", scope, ...limitArgs],
40487
+ threshold,
40488
+ params.includeArchived
40489
+ );
40490
+ passes.push(projectMemoryPass.hits);
40491
+ }
40492
+ }
40257
40493
  const seededUri = project ? trimTrailingSlash(project.uri) : void 0;
40258
40494
  if (seededUri?.startsWith("viking://") && seededUri !== params.pinnedUri) {
40259
40495
  const seeded = await recallSearchHits(
@@ -40268,7 +40504,9 @@ async function runRecallTool(config2, params) {
40268
40504
  const workset = params.pinnedUri ? void 0 : explicitWorkset ? explicitWorkset : await inferWorksetFromQuery(config2.manifestPath, projectQuery);
40269
40505
  if (workset && workset.projects.length > 0) {
40270
40506
  sections.push(`Workset scope: ${workset.name} (${workset.projects.map((member) => member.name).join(", ")})`);
40271
- const alreadyScoped = new Set([params.pinnedUri, seededUri].filter((uri) => uri !== void 0));
40507
+ const alreadyScoped = new Set(
40508
+ [params.pinnedUri, seededUri, ...scopedRecallUris].filter((uri) => uri !== void 0)
40509
+ );
40272
40510
  const worksetScopes = worksetScopeUris(config2, workset).filter((uri) => !alreadyScoped.has(uri)).slice(0, MAX_WORKSET_PASSES);
40273
40511
  for (const scope of worksetScopes) {
40274
40512
  const worksetPass = await recallSearchHits(
@@ -40955,6 +41193,24 @@ function worksetScopeUris(config2, workset) {
40955
41193
  }
40956
41194
  return [...new Set(scopes)];
40957
41195
  }
41196
+ function projectMemoryScopeUris(config2, projectName, includeArchived) {
41197
+ if (!projectName) {
41198
+ return [];
41199
+ }
41200
+ const base = `viking://user/${uriSegment2(config2.user)}/memories`;
41201
+ const projectSegment = uriSegment2(projectName);
41202
+ const scopes = [
41203
+ `${base}/durable/projects/${projectSegment}`,
41204
+ `${base}/handoffs/active/${projectSegment}`,
41205
+ `${base}/incidents/active/${projectSegment}`
41206
+ ];
41207
+ return includeArchived ? [
41208
+ ...scopes,
41209
+ `${base}/durable/archived/${projectSegment}`,
41210
+ `${base}/handoffs/archived/${projectSegment}`,
41211
+ `${base}/incidents/archived/${projectSegment}`
41212
+ ] : scopes;
41213
+ }
40958
41214
  function formatMemoryDocument2(title, metadata, body) {
40959
41215
  const header = [
40960
41216
  title,
@@ -40994,6 +41250,17 @@ function requiredText(value, toolName, fieldName, example) {
40994
41250
  ok: false
40995
41251
  };
40996
41252
  }
41253
+ function rejectLeadingDash(value, toolName, fieldName) {
41254
+ if (!value.startsWith("-")) {
41255
+ return { ok: true, value };
41256
+ }
41257
+ return {
41258
+ error: argumentError(
41259
+ `Threadnote MCP tool "${toolName}" rejects "${fieldName}" values that start with "-". Prefix relative file paths with "./" or use an absolute path.`
41260
+ ),
41261
+ ok: false
41262
+ };
41263
+ }
40997
41264
  function requiredVikingUri(value, toolName, exampleUri) {
40998
41265
  const checked = requiredText(value, toolName, "uri", { uri: exampleUri });
40999
41266
  if (!checked.ok) {
@@ -41078,6 +41345,12 @@ async function runOpenVikingAddResourceTool(config2, toolName, params) {
41078
41345
  ].join("\n")
41079
41346
  );
41080
41347
  }
41348
+ if (source) {
41349
+ const checkedSource = rejectLeadingDash(source, toolName, "path");
41350
+ if (!checkedSource.ok) {
41351
+ return checkedSource.error;
41352
+ }
41353
+ }
41081
41354
  if (tempFileId) {
41082
41355
  const checkedTo2 = optionalVikingUri(params.to, toolName);
41083
41356
  if (!checkedTo2.ok) {