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.
- package/config/post-update-migrations.json +20 -0
- package/dist/mcp_server.cjs +359 -86
- package/dist/threadnote.cjs +631 -122
- package/docs/index.html +1 -1
- package/package.json +1 -1
package/dist/mcp_server.cjs
CHANGED
|
@@ -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
|
|
35518
|
-
|
|
35519
|
-
|
|
35520
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
38814
|
-
|
|
38815
|
-
|
|
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", "--
|
|
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
|
|
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
|
|
38877
|
-
const newRel = entries[index +
|
|
38878
|
-
if (oldRel
|
|
38879
|
-
changes.push({
|
|
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 +=
|
|
39027
|
+
index += 2;
|
|
38883
39028
|
continue;
|
|
38884
39029
|
}
|
|
38885
|
-
const rel = entries[index
|
|
39030
|
+
const rel = entries[index];
|
|
38886
39031
|
if (rel) {
|
|
38887
|
-
|
|
38888
|
-
|
|
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 +=
|
|
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
|
|
39088
|
+
if (!await isRegularFileNoSymlink(change.path)) {
|
|
38912
39089
|
continue;
|
|
38913
39090
|
}
|
|
38914
|
-
const
|
|
38915
|
-
|
|
38916
|
-
|
|
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 =
|
|
38923
|
-
await
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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) {
|