threadnote 1.6.0 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp_server.cjs +233 -84
- package/dist/threadnote.cjs +252 -145
- package/docs/index.html +37 -8
- 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,7 @@ function parseJsonConfigObject(content) {
|
|
|
35514
35590
|
}
|
|
35515
35591
|
}
|
|
35516
35592
|
function redactText(content) {
|
|
35517
|
-
return content
|
|
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);
|
|
35521
35594
|
}
|
|
35522
35595
|
function escapeRegExp(value) {
|
|
35523
35596
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -36778,53 +36851,6 @@ var PACK_FILES_DIR = "files";
|
|
|
36778
36851
|
var PACK_ROOT_TOKEN = "${THREADNOTE_PACK_ROOT}";
|
|
36779
36852
|
var AUTO_SHARE_FETCH_INTERVAL_MS = 5 * 60 * 1e3;
|
|
36780
36853
|
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
36854
|
var autoShareStates = /* @__PURE__ */ new Map();
|
|
36829
36855
|
function startShareBackgroundFetch(config2) {
|
|
36830
36856
|
const state = autoShareState(config2);
|
|
@@ -37053,7 +37079,7 @@ async function publishShareGitChange(worktree, relativePath, commitMessage, opti
|
|
|
37053
37079
|
const git = await requiredExecutable("git");
|
|
37054
37080
|
const messages = [];
|
|
37055
37081
|
const paths = typeof relativePath === "string" ? [relativePath] : [...relativePath];
|
|
37056
|
-
const stageArgs = verb === "rm" ? ["-C", worktree, "rm", ...paths] : ["-C", worktree, "add", "--", ...paths];
|
|
37082
|
+
const stageArgs = verb === "rm" ? ["-C", worktree, "rm", "--", ...paths] : ["-C", worktree, "add", "--", ...paths];
|
|
37057
37083
|
const stageResult = await runGitCommand(dryRun, git, stageArgs, `git ${verb} failed`);
|
|
37058
37084
|
if (stageResult) {
|
|
37059
37085
|
messages.push(`git ${verb}: ${stageResult.stdout.trim() || "ok"}`);
|
|
@@ -37396,13 +37422,7 @@ function isProbablyBinary(buffer) {
|
|
|
37396
37422
|
}
|
|
37397
37423
|
}
|
|
37398
37424
|
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;
|
|
37425
|
+
return credentialScrubberBlocker(buffer.toString("latin1"));
|
|
37406
37426
|
}
|
|
37407
37427
|
function detectBinaryLocalPath(buffer, rewriteRoots) {
|
|
37408
37428
|
const latin1 = buffer.toString("latin1");
|
|
@@ -38653,6 +38673,18 @@ function stripPersonalProvenance(content) {
|
|
|
38653
38673
|
}
|
|
38654
38674
|
return cleaned.join("\n");
|
|
38655
38675
|
}
|
|
38676
|
+
async function readMemoryContent(config2, ov, uri, dryRun) {
|
|
38677
|
+
const args = withIdentity(config2, ["read", uri]);
|
|
38678
|
+
if (dryRun) {
|
|
38679
|
+
console.log(`Would run: ${formatShellCommand(ov, args)}`);
|
|
38680
|
+
return "<dry-run memory body>";
|
|
38681
|
+
}
|
|
38682
|
+
const result = await runCommand(ov, args);
|
|
38683
|
+
if (!result.stdout.trim()) {
|
|
38684
|
+
throw new Error(`Refusing to publish empty memory at ${uri}`);
|
|
38685
|
+
}
|
|
38686
|
+
return result.stdout;
|
|
38687
|
+
}
|
|
38656
38688
|
async function ensureSharedDirectoryChain(config2, ov, memoryUri, dryRun, options = {}) {
|
|
38657
38689
|
const directoryUri = parentUri(memoryUri);
|
|
38658
38690
|
for (const uri of sharedDirectoryChain(config2, directoryUri)) {
|
|
@@ -38810,9 +38842,19 @@ function isResourceBusyFailure(stderr, stdout2) {
|
|
|
38810
38842
|
${stdout2}`.toLowerCase();
|
|
38811
38843
|
return output.includes("resource is busy") || output.includes("resource is being processed");
|
|
38812
38844
|
}
|
|
38813
|
-
async function
|
|
38814
|
-
|
|
38815
|
-
|
|
38845
|
+
async function readSharedInboundFileContent(uri, filePath) {
|
|
38846
|
+
if (!await isRegularFileNoSymlink(filePath)) {
|
|
38847
|
+
throw new Error(`Refusing to ingest non-regular shared file: ${filePath}`);
|
|
38848
|
+
}
|
|
38849
|
+
return prepareSharedInboundContent(uri, await (0, import_promises4.readFile)(filePath, "utf8"));
|
|
38850
|
+
}
|
|
38851
|
+
function prepareSharedInboundContent(uri, rawContent) {
|
|
38852
|
+
const stripped = stripPersonalProvenance(rawContent);
|
|
38853
|
+
const scrub = applyScrubber(stripped, { redact: false });
|
|
38854
|
+
if (scrub.blocker) {
|
|
38855
|
+
throw new Error(`Refusing to ingest ${uri}: possible ${scrub.blocker}. Strip the sensitive value upstream first.`);
|
|
38856
|
+
}
|
|
38857
|
+
return scrub.cleaned;
|
|
38816
38858
|
}
|
|
38817
38859
|
async function removeMemoryUri(config2, ov, uri, dryRun, options = {}) {
|
|
38818
38860
|
const args = withIdentity(config2, ["rm", uri]);
|
|
@@ -38860,8 +38902,10 @@ async function gitOutput(worktree, args, dryRun) {
|
|
|
38860
38902
|
}
|
|
38861
38903
|
return result.stdout.trim();
|
|
38862
38904
|
}
|
|
38905
|
+
var GIT_MODE_ABSENT = "000000";
|
|
38906
|
+
var GIT_MODE_SYMLINK = "120000";
|
|
38863
38907
|
async function listChangedFiles(worktree, beforeRev, afterRev) {
|
|
38864
|
-
const result = await runCommand("git", ["-C", worktree, "diff", "--
|
|
38908
|
+
const result = await runCommand("git", ["-C", worktree, "diff", "--raw", "-z", `${beforeRev}..${afterRev}`], {
|
|
38865
38909
|
allowFailure: true
|
|
38866
38910
|
});
|
|
38867
38911
|
if (result.exitCode !== 0) {
|
|
@@ -38870,27 +38914,65 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
|
|
|
38870
38914
|
const entries = result.stdout.split("\0").filter((part) => part.length > 0);
|
|
38871
38915
|
const changes = [];
|
|
38872
38916
|
for (let index = 0; index < entries.length; ) {
|
|
38873
|
-
const raw = entries[index];
|
|
38874
|
-
const
|
|
38917
|
+
const raw = entries[index++];
|
|
38918
|
+
const match = raw.match(/^:(\d{6}) (\d{6}) [0-9a-f]+ [0-9a-f]+ ([A-Z])\d*/);
|
|
38919
|
+
if (!match) {
|
|
38920
|
+
continue;
|
|
38921
|
+
}
|
|
38922
|
+
const [, oldMode, newMode, head] = match;
|
|
38875
38923
|
if (head === "R" || head === "C") {
|
|
38876
|
-
const oldRel = entries[index
|
|
38877
|
-
const newRel = entries[index +
|
|
38878
|
-
if (oldRel
|
|
38879
|
-
changes.push({
|
|
38924
|
+
const oldRel = entries[index];
|
|
38925
|
+
const newRel = entries[index + 1];
|
|
38926
|
+
if (oldRel) {
|
|
38927
|
+
changes.push({
|
|
38928
|
+
path: (0, import_node_path3.join)(worktree, oldRel),
|
|
38929
|
+
previousContent: oldMode === GIT_MODE_SYMLINK ? void 0 : await gitFileContent(worktree, beforeRev, oldRel),
|
|
38930
|
+
relativePath: oldRel,
|
|
38931
|
+
status: "removed"
|
|
38932
|
+
});
|
|
38933
|
+
}
|
|
38934
|
+
if (newRel && newMode !== GIT_MODE_SYMLINK) {
|
|
38880
38935
|
changes.push({ path: (0, import_node_path3.join)(worktree, newRel), relativePath: newRel, status: "added" });
|
|
38881
38936
|
}
|
|
38882
|
-
index +=
|
|
38937
|
+
index += 2;
|
|
38883
38938
|
continue;
|
|
38884
38939
|
}
|
|
38885
|
-
const rel = entries[index
|
|
38940
|
+
const rel = entries[index];
|
|
38886
38941
|
if (rel) {
|
|
38887
|
-
|
|
38888
|
-
|
|
38942
|
+
if (head === "D") {
|
|
38943
|
+
changes.push({
|
|
38944
|
+
path: (0, import_node_path3.join)(worktree, rel),
|
|
38945
|
+
previousContent: oldMode === GIT_MODE_SYMLINK ? void 0 : await gitFileContent(worktree, beforeRev, rel),
|
|
38946
|
+
relativePath: rel,
|
|
38947
|
+
status: "removed"
|
|
38948
|
+
});
|
|
38949
|
+
} else if (newMode === GIT_MODE_SYMLINK) {
|
|
38950
|
+
if (oldMode !== GIT_MODE_ABSENT) {
|
|
38951
|
+
changes.push({
|
|
38952
|
+
path: (0, import_node_path3.join)(worktree, rel),
|
|
38953
|
+
previousContent: oldMode === GIT_MODE_SYMLINK ? void 0 : await gitFileContent(worktree, beforeRev, rel),
|
|
38954
|
+
relativePath: rel,
|
|
38955
|
+
status: "removed"
|
|
38956
|
+
});
|
|
38957
|
+
}
|
|
38958
|
+
} else {
|
|
38959
|
+
const status = head === "A" ? "added" : "modified";
|
|
38960
|
+
changes.push({
|
|
38961
|
+
path: (0, import_node_path3.join)(worktree, rel),
|
|
38962
|
+
previousContent: oldMode === GIT_MODE_ABSENT || oldMode === GIT_MODE_SYMLINK ? void 0 : await gitFileContent(worktree, beforeRev, rel),
|
|
38963
|
+
relativePath: rel,
|
|
38964
|
+
status
|
|
38965
|
+
});
|
|
38966
|
+
}
|
|
38889
38967
|
}
|
|
38890
|
-
index +=
|
|
38968
|
+
index += 1;
|
|
38891
38969
|
}
|
|
38892
38970
|
return changes;
|
|
38893
38971
|
}
|
|
38972
|
+
async function gitFileContent(worktree, rev, relativePath) {
|
|
38973
|
+
const result = await runCommand("git", ["-C", worktree, "show", `${rev}:${relativePath}`], { allowFailure: true });
|
|
38974
|
+
return result.exitCode === 0 ? result.stdout : void 0;
|
|
38975
|
+
}
|
|
38894
38976
|
async function applyChangesToOpenViking(config2, team, changes, options = {}) {
|
|
38895
38977
|
const ov = await openVikingCliForMode(false);
|
|
38896
38978
|
const failed = [];
|
|
@@ -38905,22 +38987,37 @@ async function applyChangesToOpenViking(config2, team, changes, options = {}) {
|
|
|
38905
38987
|
const uri = workfileToVikingUri(config2, team, change.path);
|
|
38906
38988
|
try {
|
|
38907
38989
|
if (change.status === "removed") {
|
|
38990
|
+
const currentContent2 = await readExistingMemoryContent(config2, ov, uri);
|
|
38991
|
+
if (currentContent2 === void 0) {
|
|
38992
|
+
continue;
|
|
38993
|
+
}
|
|
38994
|
+
assertInboundPreviousContentMatches(change, uri, currentContent2);
|
|
38908
38995
|
await removeMemoryUri(config2, ov, uri, false, options);
|
|
38909
38996
|
continue;
|
|
38910
38997
|
}
|
|
38911
|
-
if (!await
|
|
38998
|
+
if (!await isRegularFileNoSymlink(change.path)) {
|
|
38912
38999
|
continue;
|
|
38913
39000
|
}
|
|
38914
|
-
const
|
|
38915
|
-
|
|
38916
|
-
|
|
39001
|
+
const content = await readSharedInboundFileContent(uri, change.path);
|
|
39002
|
+
const currentContent = await readExistingMemoryContent(config2, ov, uri);
|
|
39003
|
+
if (currentContent !== void 0) {
|
|
39004
|
+
if (change.status === "added") {
|
|
39005
|
+
if (currentContent === content) {
|
|
39006
|
+
continue;
|
|
39007
|
+
}
|
|
39008
|
+
throw new Error(
|
|
39009
|
+
`Refusing to ingest newly added shared file over existing local OpenViking resource ${uri}; inspect and resolve the local edit first.`
|
|
39010
|
+
);
|
|
39011
|
+
}
|
|
39012
|
+
assertInboundPreviousContentMatches(change, uri, currentContent);
|
|
39013
|
+
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
39014
|
if (options.quiet !== true) {
|
|
38918
39015
|
console.warn(`share sync: ${uri}: ${reason}.`);
|
|
38919
39016
|
}
|
|
38920
39017
|
}
|
|
38921
39018
|
await ensureSharedDirectoryChain(config2, ov, uri, false, options);
|
|
38922
|
-
const writeMode =
|
|
38923
|
-
await
|
|
39019
|
+
const writeMode = currentContent !== void 0 ? "replace" : "create";
|
|
39020
|
+
await writeMemoryFile(config2, ov, uri, content, writeMode, false, options);
|
|
38924
39021
|
} catch (err) {
|
|
38925
39022
|
const message = err instanceof Error ? err.message : String(err);
|
|
38926
39023
|
if (options.quiet !== true) {
|
|
@@ -38931,6 +39028,25 @@ async function applyChangesToOpenViking(config2, team, changes, options = {}) {
|
|
|
38931
39028
|
}
|
|
38932
39029
|
return { failed };
|
|
38933
39030
|
}
|
|
39031
|
+
async function readExistingMemoryContent(config2, ov, uri) {
|
|
39032
|
+
if (!await vikingResourceExists(ov, config2, uri)) {
|
|
39033
|
+
return void 0;
|
|
39034
|
+
}
|
|
39035
|
+
return readMemoryContent(config2, ov, uri, false);
|
|
39036
|
+
}
|
|
39037
|
+
function assertInboundPreviousContentMatches(change, uri, currentContent) {
|
|
39038
|
+
if (change.previousContent === void 0) {
|
|
39039
|
+
throw new Error(
|
|
39040
|
+
`Refusing to apply inbound shared change for ${uri}: previous shared content is unavailable, so local edits cannot be distinguished from upstream edits.`
|
|
39041
|
+
);
|
|
39042
|
+
}
|
|
39043
|
+
const expectedContent = prepareSharedInboundContent(uri, change.previousContent);
|
|
39044
|
+
if (currentContent !== expectedContent) {
|
|
39045
|
+
throw new Error(
|
|
39046
|
+
`Refusing to apply inbound shared change for ${uri}: local OpenViking content differs from the previous shared version. Inspect and resolve the local edit first.`
|
|
39047
|
+
);
|
|
39048
|
+
}
|
|
39049
|
+
}
|
|
38934
39050
|
function mergeChanges(...lists) {
|
|
38935
39051
|
const map2 = /* @__PURE__ */ new Map();
|
|
38936
39052
|
for (const list of lists) {
|
|
@@ -39659,6 +39775,10 @@ function registerTools(server, config2) {
|
|
|
39659
39775
|
if (!checkedPattern.ok) {
|
|
39660
39776
|
return checkedPattern.error;
|
|
39661
39777
|
}
|
|
39778
|
+
const checkedLiteralPattern = rejectLeadingDash(checkedPattern.value, "grep", "pattern");
|
|
39779
|
+
if (!checkedLiteralPattern.ok) {
|
|
39780
|
+
return checkedLiteralPattern.error;
|
|
39781
|
+
}
|
|
39662
39782
|
const checkedUri = optionalVikingUri(uri, "grep");
|
|
39663
39783
|
if (!checkedUri.ok) {
|
|
39664
39784
|
return checkedUri.error;
|
|
@@ -39666,7 +39786,7 @@ function registerTools(server, config2) {
|
|
|
39666
39786
|
return runOpenVikingMcpTool(config2, "grep", {
|
|
39667
39787
|
case_insensitive: caseInsensitive ?? case_insensitive,
|
|
39668
39788
|
node_limit: nodeLimit ?? node_limit,
|
|
39669
|
-
pattern:
|
|
39789
|
+
pattern: checkedLiteralPattern.value,
|
|
39670
39790
|
uri: checkedUri.value ?? `viking://user/${uriSegment2(config2.user)}/memories`
|
|
39671
39791
|
});
|
|
39672
39792
|
}
|
|
@@ -39688,13 +39808,17 @@ function registerTools(server, config2) {
|
|
|
39688
39808
|
if (!checkedPattern.ok) {
|
|
39689
39809
|
return checkedPattern.error;
|
|
39690
39810
|
}
|
|
39811
|
+
const checkedLiteralPattern = rejectLeadingDash(checkedPattern.value, "glob", "pattern");
|
|
39812
|
+
if (!checkedLiteralPattern.ok) {
|
|
39813
|
+
return checkedLiteralPattern.error;
|
|
39814
|
+
}
|
|
39691
39815
|
const checkedUri = optionalVikingUri(uri, "glob");
|
|
39692
39816
|
if (!checkedUri.ok) {
|
|
39693
39817
|
return checkedUri.error;
|
|
39694
39818
|
}
|
|
39695
39819
|
return runOpenVikingMcpTool(config2, "glob", {
|
|
39696
39820
|
node_limit: nodeLimit ?? node_limit,
|
|
39697
|
-
pattern:
|
|
39821
|
+
pattern: checkedLiteralPattern.value,
|
|
39698
39822
|
uri: checkedUri.value
|
|
39699
39823
|
});
|
|
39700
39824
|
}
|
|
@@ -40007,6 +40131,10 @@ function registerOpenVikingParityTools(server, config2) {
|
|
|
40007
40131
|
if (!checkedPattern.ok) {
|
|
40008
40132
|
return checkedPattern.error;
|
|
40009
40133
|
}
|
|
40134
|
+
const checkedLiteralPattern = rejectLeadingDash(checkedPattern.value, "ov_grep", "pattern");
|
|
40135
|
+
if (!checkedLiteralPattern.ok) {
|
|
40136
|
+
return checkedLiteralPattern.error;
|
|
40137
|
+
}
|
|
40010
40138
|
const checkedUri = optionalVikingUri(uri, "ov_grep");
|
|
40011
40139
|
if (!checkedUri.ok) {
|
|
40012
40140
|
return checkedUri.error;
|
|
@@ -40014,7 +40142,7 @@ function registerOpenVikingParityTools(server, config2) {
|
|
|
40014
40142
|
return runOpenVikingMcpTool(config2, "grep", {
|
|
40015
40143
|
case_insensitive: caseInsensitive ?? case_insensitive,
|
|
40016
40144
|
node_limit: nodeLimit ?? node_limit,
|
|
40017
|
-
pattern:
|
|
40145
|
+
pattern: checkedLiteralPattern.value,
|
|
40018
40146
|
uri: checkedUri.value
|
|
40019
40147
|
});
|
|
40020
40148
|
}
|
|
@@ -40036,13 +40164,17 @@ function registerOpenVikingParityTools(server, config2) {
|
|
|
40036
40164
|
if (!checkedPattern.ok) {
|
|
40037
40165
|
return checkedPattern.error;
|
|
40038
40166
|
}
|
|
40167
|
+
const checkedLiteralPattern = rejectLeadingDash(checkedPattern.value, "ov_glob", "pattern");
|
|
40168
|
+
if (!checkedLiteralPattern.ok) {
|
|
40169
|
+
return checkedLiteralPattern.error;
|
|
40170
|
+
}
|
|
40039
40171
|
const checkedUri = optionalVikingUri(uri, "ov_glob");
|
|
40040
40172
|
if (!checkedUri.ok) {
|
|
40041
40173
|
return checkedUri.error;
|
|
40042
40174
|
}
|
|
40043
40175
|
return runOpenVikingMcpTool(config2, "glob", {
|
|
40044
40176
|
node_limit: nodeLimit ?? node_limit,
|
|
40045
|
-
pattern:
|
|
40177
|
+
pattern: checkedLiteralPattern.value,
|
|
40046
40178
|
uri: checkedUri.value
|
|
40047
40179
|
});
|
|
40048
40180
|
}
|
|
@@ -40994,6 +41126,17 @@ function requiredText(value, toolName, fieldName, example) {
|
|
|
40994
41126
|
ok: false
|
|
40995
41127
|
};
|
|
40996
41128
|
}
|
|
41129
|
+
function rejectLeadingDash(value, toolName, fieldName) {
|
|
41130
|
+
if (!value.startsWith("-")) {
|
|
41131
|
+
return { ok: true, value };
|
|
41132
|
+
}
|
|
41133
|
+
return {
|
|
41134
|
+
error: argumentError(
|
|
41135
|
+
`Threadnote MCP tool "${toolName}" rejects "${fieldName}" values that start with "-". Prefix relative file paths with "./" or use an absolute path.`
|
|
41136
|
+
),
|
|
41137
|
+
ok: false
|
|
41138
|
+
};
|
|
41139
|
+
}
|
|
40997
41140
|
function requiredVikingUri(value, toolName, exampleUri) {
|
|
40998
41141
|
const checked = requiredText(value, toolName, "uri", { uri: exampleUri });
|
|
40999
41142
|
if (!checked.ok) {
|
|
@@ -41078,6 +41221,12 @@ async function runOpenVikingAddResourceTool(config2, toolName, params) {
|
|
|
41078
41221
|
].join("\n")
|
|
41079
41222
|
);
|
|
41080
41223
|
}
|
|
41224
|
+
if (source) {
|
|
41225
|
+
const checkedSource = rejectLeadingDash(source, toolName, "path");
|
|
41226
|
+
if (!checkedSource.ok) {
|
|
41227
|
+
return checkedSource.error;
|
|
41228
|
+
}
|
|
41229
|
+
}
|
|
41081
41230
|
if (tempFileId) {
|
|
41082
41231
|
const checkedTo2 = optionalVikingUri(params.to, toolName);
|
|
41083
41232
|
if (!checkedTo2.ok) {
|
package/dist/threadnote.cjs
CHANGED
|
@@ -3394,7 +3394,7 @@ var program = new Command();
|
|
|
3394
3394
|
// src/constants.ts
|
|
3395
3395
|
var DEFAULT_HOST = "127.0.0.1";
|
|
3396
3396
|
var DEFAULT_PORT = 1933;
|
|
3397
|
-
var DEFAULT_OPENVIKING_VERSION = "0.4.
|
|
3397
|
+
var DEFAULT_OPENVIKING_VERSION = "0.4.7";
|
|
3398
3398
|
var OPENVIKING_TOOL_PYTHON = "3.12";
|
|
3399
3399
|
var DEFAULT_ACCOUNT = "local";
|
|
3400
3400
|
var DEFAULT_AGENT_ID = "threadnote";
|
|
@@ -3576,6 +3576,91 @@ function startProgress(message) {
|
|
|
3576
3576
|
};
|
|
3577
3577
|
}
|
|
3578
3578
|
|
|
3579
|
+
// src/scrubber.ts
|
|
3580
|
+
var SCRUBBER_PATTERNS = [
|
|
3581
|
+
{ name: "private key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
3582
|
+
{ name: "API key (sk-...)", regex: /\bsk-[A-Za-z0-9_-]{16,}/ },
|
|
3583
|
+
{ name: "GitHub token", regex: /\bgh[pousr]_[A-Za-z0-9_]{16,}/ },
|
|
3584
|
+
{ name: "GitHub fine-grained PAT", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}/ },
|
|
3585
|
+
{ name: "GitLab PAT", regex: /\bglpat-[A-Za-z0-9_-]{20,}/ },
|
|
3586
|
+
{ name: "bearer token", regex: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/i },
|
|
3587
|
+
{ name: "basic auth header", regex: /\bBasic\s+[A-Za-z0-9+/]{16,}={0,2}\b/i },
|
|
3588
|
+
{ name: "JWT", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
|
|
3589
|
+
{ name: "AWS access key", regex: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/ },
|
|
3590
|
+
{ name: "AWS secret access key", regex: /\baws_secret_access_key\s*[:=]\s*["']?[A-Za-z0-9/+=]{35,}["']?/i },
|
|
3591
|
+
{ name: "AWS session token", regex: /\baws_session_token\s*[:=]\s*["']?[A-Za-z0-9/+=]{40,}["']?/i },
|
|
3592
|
+
{ name: "Google API key", regex: /\bAIza[0-9A-Za-z_-]{35}\b/ },
|
|
3593
|
+
{ name: "Google OAuth token", regex: /\bya29\.[0-9A-Za-z_-]{20,}/ },
|
|
3594
|
+
{ name: "Stripe key", regex: /\b(?:sk|rk)_(?:live|test)_[0-9A-Za-z]{16,}\b/ },
|
|
3595
|
+
{ name: "Stripe webhook secret", regex: /\bwhsec_[0-9A-Za-z]{16,}\b/ },
|
|
3596
|
+
{
|
|
3597
|
+
name: "Discord token",
|
|
3598
|
+
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/
|
|
3599
|
+
},
|
|
3600
|
+
{
|
|
3601
|
+
name: "Discord webhook",
|
|
3602
|
+
regex: /https:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[A-Za-z0-9_-]+/i
|
|
3603
|
+
},
|
|
3604
|
+
{ name: "Slack token", regex: /\bx(?:app|ox[abcdeprs])(?:-\d-)?[A-Za-z0-9._-]{10,}/i },
|
|
3605
|
+
{ name: "Slack webhook", regex: /https:\/\/hooks\.slack\.com\/services\/[A-Z0-9]+\/[A-Z0-9]+\/[A-Za-z0-9]+/i },
|
|
3606
|
+
{
|
|
3607
|
+
name: "database URI",
|
|
3608
|
+
regex: /\b(?:postgres(?:ql)?|mysql|mariadb|mongodb(?:\+srv)?|redis):\/\/[^:\s/@]+:[^@\s]+@[^\s)>"'`,]+/i
|
|
3609
|
+
},
|
|
3610
|
+
{ name: "URL basic auth", regex: /\bhttps?:\/\/[^:\s/@]+:[^@\s]+@[^\s)>"'`,]+/i },
|
|
3611
|
+
{ name: "macOS home path", placeholder: "<local-path>", regex: /\/Users\/[^\s)>"'`,]+/ },
|
|
3612
|
+
{ name: "linux home path", placeholder: "<local-path>", regex: /\/home\/[^\s)>"'`,]+/ }
|
|
3613
|
+
];
|
|
3614
|
+
function applyScrubber(content, { redact }) {
|
|
3615
|
+
let cleaned = content;
|
|
3616
|
+
const redactions = [];
|
|
3617
|
+
for (const pattern of SCRUBBER_PATTERNS) {
|
|
3618
|
+
if (!pattern.regex.test(cleaned)) {
|
|
3619
|
+
continue;
|
|
3620
|
+
}
|
|
3621
|
+
if (!pattern.placeholder || !redact) {
|
|
3622
|
+
return { blocker: pattern.name, cleaned: content, redactions: [] };
|
|
3623
|
+
}
|
|
3624
|
+
const globalRegex = globalize(pattern.regex);
|
|
3625
|
+
const matches = cleaned.match(globalRegex) ?? [];
|
|
3626
|
+
cleaned = cleaned.replace(globalRegex, pattern.placeholder);
|
|
3627
|
+
redactions.push({ count: matches.length, name: pattern.name });
|
|
3628
|
+
}
|
|
3629
|
+
return { cleaned, redactions };
|
|
3630
|
+
}
|
|
3631
|
+
function detectSecretMatches(content) {
|
|
3632
|
+
const matches = [];
|
|
3633
|
+
for (const pattern of SCRUBBER_PATTERNS) {
|
|
3634
|
+
if (pattern.regex.test(content)) {
|
|
3635
|
+
matches.push(pattern.name);
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
return matches;
|
|
3639
|
+
}
|
|
3640
|
+
function credentialScrubberBlocker(content) {
|
|
3641
|
+
for (const pattern of SCRUBBER_PATTERNS) {
|
|
3642
|
+
if (pattern.placeholder === void 0 && pattern.regex.test(content)) {
|
|
3643
|
+
return pattern.name;
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
return void 0;
|
|
3647
|
+
}
|
|
3648
|
+
function redactSensitiveText(content) {
|
|
3649
|
+
let redacted = content.replace(
|
|
3650
|
+
/([A-Za-z0-9_.-]*(?:token|secret|password|api[_-]?key|authorization|credential|session)[A-Za-z0-9_.-]*\s*[:=]\s*)("[^"]+"|'[^']+'|Bearer\s+[^'"\s]+|\S+)/gi,
|
|
3651
|
+
"$1[REDACTED]"
|
|
3652
|
+
);
|
|
3653
|
+
for (const pattern of SCRUBBER_PATTERNS) {
|
|
3654
|
+
const placeholder = pattern.placeholder ?? "<secret>";
|
|
3655
|
+
redacted = redacted.replace(globalize(pattern.regex), placeholder);
|
|
3656
|
+
}
|
|
3657
|
+
return redacted;
|
|
3658
|
+
}
|
|
3659
|
+
function globalize(regex) {
|
|
3660
|
+
const flags = regex.flags.includes("g") ? regex.flags : `${regex.flags}g`;
|
|
3661
|
+
return new RegExp(regex.source, flags);
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3579
3664
|
// src/utils.ts
|
|
3580
3665
|
function isJsonObject(value) {
|
|
3581
3666
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -3589,10 +3674,7 @@ function parseJsonConfigObject(content) {
|
|
|
3589
3674
|
}
|
|
3590
3675
|
}
|
|
3591
3676
|
function redactText(content) {
|
|
3592
|
-
return content
|
|
3593
|
-
/([A-Za-z0-9_.-]*(?:token|secret|password|api[_-]?key|authorization)[A-Za-z0-9_.-]*\s*[:=]\s*)("[^"]+"|'[^']+'|Bearer\s+[^'"\s]+|\S+)/gi,
|
|
3594
|
-
"$1[REDACTED]"
|
|
3595
|
-
).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]");
|
|
3677
|
+
return redactSensitiveText(content);
|
|
3596
3678
|
}
|
|
3597
3679
|
async function walkFiles(root) {
|
|
3598
3680
|
const files = [];
|
|
@@ -8469,53 +8551,6 @@ var PACK_FILES_DIR = "files";
|
|
|
8469
8551
|
var PACK_ROOT_TOKEN = "${THREADNOTE_PACK_ROOT}";
|
|
8470
8552
|
var AUTO_SHARE_FETCH_INTERVAL_MS = 5 * 60 * 1e3;
|
|
8471
8553
|
var DEFAULT_GIT_REMOTE_NAME = "origin";
|
|
8472
|
-
var SCRUBBER_PATTERNS = [
|
|
8473
|
-
// Credentials: never redactable. Blocking is the only safe response —
|
|
8474
|
-
// automated redaction risks false negatives that leave material in git.
|
|
8475
|
-
{ name: "private key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
8476
|
-
{ name: "API key (sk-...)", regex: /\bsk-[A-Za-z0-9_-]{16,}/ },
|
|
8477
|
-
{ name: "GitHub token", regex: /\bgh[pousr]_[A-Za-z0-9_]{16,}/ },
|
|
8478
|
-
{ name: "GitHub fine-grained PAT", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}/ },
|
|
8479
|
-
{ name: "GitLab PAT", regex: /\bglpat-[A-Za-z0-9_-]{20,}/ },
|
|
8480
|
-
{ name: "bearer token", regex: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/i },
|
|
8481
|
-
// Matches bare JWTs (three base64url segments). May surface a JWE token in
|
|
8482
|
-
// legitimate docs; if that becomes noisy we can switch to warn-only.
|
|
8483
|
-
{ name: "JWT", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
|
|
8484
|
-
{ name: "AWS access key", regex: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
8485
|
-
// Slack tokens: xoxa/xoxb/xoxc (configuration)/xoxd (legacy user cookie)/
|
|
8486
|
-
// xoxe (refresh)/xoxp/xoxr/xoxs, with optional -N- segment for the workspace tier.
|
|
8487
|
-
{ name: "Slack token", regex: /\bxox[abcdeprs](?:-\d-)?[A-Za-z0-9._-]{10,}/i },
|
|
8488
|
-
// Soft leaks: block by default (so the agent sees them and decides), but
|
|
8489
|
-
// allow opt-in redaction so curated memories with incidental matches can
|
|
8490
|
-
// ship without a manual rewrite. Local home paths are the recurring
|
|
8491
|
-
// real-world leak; the regexes greedily consume the whole path segment
|
|
8492
|
-
// (including subdirectories) up to whitespace or common closing punctuation
|
|
8493
|
-
// so redaction collapses an entire path to a single placeholder rather than
|
|
8494
|
-
// leaving the subpath visible.
|
|
8495
|
-
{ name: "macOS home path", placeholder: "<local-path>", regex: /\/Users\/[^\s)>"'`,]+/ },
|
|
8496
|
-
// No leading \b: it only matches when a word char precedes the slash, which
|
|
8497
|
-
// misses the common cases (after =, :, whitespace, or line start). Mirror the
|
|
8498
|
-
// macOS pattern so /home/... is caught in every position.
|
|
8499
|
-
{ name: "linux home path", placeholder: "<local-path>", regex: /\/home\/[^\s)>"'`,]+/ }
|
|
8500
|
-
];
|
|
8501
|
-
function applyScrubber(content, { redact }) {
|
|
8502
|
-
let cleaned = content;
|
|
8503
|
-
const redactions = [];
|
|
8504
|
-
for (const pattern of SCRUBBER_PATTERNS) {
|
|
8505
|
-
if (!pattern.regex.test(cleaned)) {
|
|
8506
|
-
continue;
|
|
8507
|
-
}
|
|
8508
|
-
if (!pattern.placeholder || !redact) {
|
|
8509
|
-
return { blocker: pattern.name, cleaned: content, redactions: [] };
|
|
8510
|
-
}
|
|
8511
|
-
const flags = pattern.regex.flags.includes("g") ? pattern.regex.flags : `${pattern.regex.flags}g`;
|
|
8512
|
-
const globalRegex = new RegExp(pattern.regex.source, flags);
|
|
8513
|
-
const matches = cleaned.match(globalRegex) ?? [];
|
|
8514
|
-
cleaned = cleaned.replace(globalRegex, pattern.placeholder);
|
|
8515
|
-
redactions.push({ count: matches.length, name: pattern.name });
|
|
8516
|
-
}
|
|
8517
|
-
return { cleaned, redactions };
|
|
8518
|
-
}
|
|
8519
8554
|
var autoShareStates = /* @__PURE__ */ new Map();
|
|
8520
8555
|
async function runShareInit(config, remoteUrl, options) {
|
|
8521
8556
|
if (!remoteUrl.trim()) {
|
|
@@ -8538,7 +8573,15 @@ async function runShareInit(config, remoteUrl, options) {
|
|
|
8538
8573
|
await ensureDirectory((0, import_node_path7.dirname)(worktree), dryRun);
|
|
8539
8574
|
await ensureDirectory((0, import_node_path7.dirname)(gitdir), dryRun);
|
|
8540
8575
|
const git = await requiredExecutable("git");
|
|
8541
|
-
await maybeRun(dryRun, git, [
|
|
8576
|
+
await maybeRun(dryRun, git, [
|
|
8577
|
+
"clone",
|
|
8578
|
+
"-c",
|
|
8579
|
+
"core.symlinks=false",
|
|
8580
|
+
`--separate-git-dir=${gitdir}`,
|
|
8581
|
+
"--",
|
|
8582
|
+
remoteUrl,
|
|
8583
|
+
worktree
|
|
8584
|
+
]);
|
|
8542
8585
|
const newConfig = {
|
|
8543
8586
|
addedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8544
8587
|
gitdir,
|
|
@@ -8972,7 +9015,7 @@ async function publishShareGitChange(worktree, relativePath, commitMessage, opti
|
|
|
8972
9015
|
const git = await requiredExecutable("git");
|
|
8973
9016
|
const messages = [];
|
|
8974
9017
|
const paths = typeof relativePath === "string" ? [relativePath] : [...relativePath];
|
|
8975
|
-
const stageArgs = verb === "rm" ? ["-C", worktree, "rm", ...paths] : ["-C", worktree, "add", "--", ...paths];
|
|
9018
|
+
const stageArgs = verb === "rm" ? ["-C", worktree, "rm", "--", ...paths] : ["-C", worktree, "add", "--", ...paths];
|
|
8976
9019
|
const stageResult = await runGitCommand(dryRun, git, stageArgs, `git ${verb} failed`);
|
|
8977
9020
|
if (stageResult) {
|
|
8978
9021
|
messages.push(`git ${verb}: ${stageResult.stdout.trim() || "ok"}`);
|
|
@@ -9386,13 +9429,7 @@ function isProbablyBinary(buffer) {
|
|
|
9386
9429
|
}
|
|
9387
9430
|
}
|
|
9388
9431
|
function detectBinaryCredential(buffer) {
|
|
9389
|
-
|
|
9390
|
-
for (const pattern of SCRUBBER_PATTERNS) {
|
|
9391
|
-
if (pattern.placeholder === void 0 && pattern.regex.test(latin1)) {
|
|
9392
|
-
return pattern.name;
|
|
9393
|
-
}
|
|
9394
|
-
}
|
|
9395
|
-
return void 0;
|
|
9432
|
+
return credentialScrubberBlocker(buffer.toString("latin1"));
|
|
9396
9433
|
}
|
|
9397
9434
|
function detectBinaryLocalPath(buffer, rewriteRoots) {
|
|
9398
9435
|
const latin1 = buffer.toString("latin1");
|
|
@@ -10067,7 +10104,7 @@ async function runShareSetUrl(config, remoteUrl, options) {
|
|
|
10067
10104
|
const git = await requiredExecutable("git");
|
|
10068
10105
|
if (dryRun) {
|
|
10069
10106
|
console.log(
|
|
10070
|
-
`Would run: ${formatShellCommand(git, ["-C", team.config.worktree, "remote", "set-url", DEFAULT_GIT_REMOTE_NAME, remoteUrl])}`
|
|
10107
|
+
`Would run: ${formatShellCommand(git, ["-C", team.config.worktree, "remote", "set-url", DEFAULT_GIT_REMOTE_NAME, "--", remoteUrl])}`
|
|
10071
10108
|
);
|
|
10072
10109
|
console.log(
|
|
10073
10110
|
`Would run: ${formatShellCommand(git, ["-C", team.config.worktree, "fetch", DEFAULT_GIT_REMOTE_NAME])}`
|
|
@@ -10075,7 +10112,7 @@ async function runShareSetUrl(config, remoteUrl, options) {
|
|
|
10075
10112
|
console.log(`Would write teams file: ${teamsFilePath(config)}`);
|
|
10076
10113
|
return;
|
|
10077
10114
|
}
|
|
10078
|
-
await runCommand(git, ["-C", team.config.worktree, "remote", "set-url", DEFAULT_GIT_REMOTE_NAME, remoteUrl]);
|
|
10115
|
+
await runCommand(git, ["-C", team.config.worktree, "remote", "set-url", DEFAULT_GIT_REMOTE_NAME, "--", remoteUrl]);
|
|
10079
10116
|
await runCommand(git, ["-C", team.config.worktree, "fetch", DEFAULT_GIT_REMOTE_NAME]);
|
|
10080
10117
|
const teamsFile = await readTeamsFile(config);
|
|
10081
10118
|
const updatedTeam = { ...team.config, remote: remoteUrl };
|
|
@@ -10383,8 +10420,8 @@ function vikingUriToWorktreeRelative(config, uri, team) {
|
|
|
10383
10420
|
}
|
|
10384
10421
|
async function isRegularFileNoSymlink(path2) {
|
|
10385
10422
|
try {
|
|
10386
|
-
const
|
|
10387
|
-
return
|
|
10423
|
+
const stat5 = await (0, import_promises5.lstat)(path2);
|
|
10424
|
+
return stat5.isFile();
|
|
10388
10425
|
} catch (_err) {
|
|
10389
10426
|
return false;
|
|
10390
10427
|
}
|
|
@@ -11123,9 +11160,23 @@ ${stdout2}`.toLowerCase();
|
|
|
11123
11160
|
return output2.includes("resource is busy") || output2.includes("resource is being processed");
|
|
11124
11161
|
}
|
|
11125
11162
|
async function ingestSingleFile(ov, config, uri, filePath, initialMode, options = {}) {
|
|
11126
|
-
const content = await (
|
|
11163
|
+
const content = await readSharedInboundFileContent(uri, filePath);
|
|
11127
11164
|
await writeMemoryFile(config, ov, uri, content, initialMode, false, options);
|
|
11128
11165
|
}
|
|
11166
|
+
async function readSharedInboundFileContent(uri, filePath) {
|
|
11167
|
+
if (!await isRegularFileNoSymlink(filePath)) {
|
|
11168
|
+
throw new Error(`Refusing to ingest non-regular shared file: ${filePath}`);
|
|
11169
|
+
}
|
|
11170
|
+
return prepareSharedInboundContent(uri, await (0, import_promises5.readFile)(filePath, "utf8"));
|
|
11171
|
+
}
|
|
11172
|
+
function prepareSharedInboundContent(uri, rawContent) {
|
|
11173
|
+
const stripped = stripPersonalProvenance(rawContent);
|
|
11174
|
+
const scrub = applyScrubber(stripped, { redact: false });
|
|
11175
|
+
if (scrub.blocker) {
|
|
11176
|
+
throw new Error(`Refusing to ingest ${uri}: possible ${scrub.blocker}. Strip the sensitive value upstream first.`);
|
|
11177
|
+
}
|
|
11178
|
+
return scrub.cleaned;
|
|
11179
|
+
}
|
|
11129
11180
|
async function removeMemoryUri(config, ov, uri, dryRun, options = {}) {
|
|
11130
11181
|
const args = withIdentity(config, ["rm", uri]);
|
|
11131
11182
|
if (dryRun) {
|
|
@@ -11172,8 +11223,10 @@ async function gitOutput(worktree, args, dryRun) {
|
|
|
11172
11223
|
}
|
|
11173
11224
|
return result.stdout.trim();
|
|
11174
11225
|
}
|
|
11226
|
+
var GIT_MODE_ABSENT = "000000";
|
|
11227
|
+
var GIT_MODE_SYMLINK = "120000";
|
|
11175
11228
|
async function listChangedFiles(worktree, beforeRev, afterRev) {
|
|
11176
|
-
const result = await runCommand("git", ["-C", worktree, "diff", "--
|
|
11229
|
+
const result = await runCommand("git", ["-C", worktree, "diff", "--raw", "-z", `${beforeRev}..${afterRev}`], {
|
|
11177
11230
|
allowFailure: true
|
|
11178
11231
|
});
|
|
11179
11232
|
if (result.exitCode !== 0) {
|
|
@@ -11182,27 +11235,65 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
|
|
|
11182
11235
|
const entries = result.stdout.split("\0").filter((part) => part.length > 0);
|
|
11183
11236
|
const changes = [];
|
|
11184
11237
|
for (let index = 0; index < entries.length; ) {
|
|
11185
|
-
const raw = entries[index];
|
|
11186
|
-
const
|
|
11238
|
+
const raw = entries[index++];
|
|
11239
|
+
const match = raw.match(/^:(\d{6}) (\d{6}) [0-9a-f]+ [0-9a-f]+ ([A-Z])\d*/);
|
|
11240
|
+
if (!match) {
|
|
11241
|
+
continue;
|
|
11242
|
+
}
|
|
11243
|
+
const [, oldMode, newMode, head] = match;
|
|
11187
11244
|
if (head === "R" || head === "C") {
|
|
11188
|
-
const oldRel = entries[index
|
|
11189
|
-
const newRel = entries[index +
|
|
11190
|
-
if (oldRel
|
|
11191
|
-
changes.push({
|
|
11245
|
+
const oldRel = entries[index];
|
|
11246
|
+
const newRel = entries[index + 1];
|
|
11247
|
+
if (oldRel) {
|
|
11248
|
+
changes.push({
|
|
11249
|
+
path: (0, import_node_path7.join)(worktree, oldRel),
|
|
11250
|
+
previousContent: oldMode === GIT_MODE_SYMLINK ? void 0 : await gitFileContent(worktree, beforeRev, oldRel),
|
|
11251
|
+
relativePath: oldRel,
|
|
11252
|
+
status: "removed"
|
|
11253
|
+
});
|
|
11254
|
+
}
|
|
11255
|
+
if (newRel && newMode !== GIT_MODE_SYMLINK) {
|
|
11192
11256
|
changes.push({ path: (0, import_node_path7.join)(worktree, newRel), relativePath: newRel, status: "added" });
|
|
11193
11257
|
}
|
|
11194
|
-
index +=
|
|
11258
|
+
index += 2;
|
|
11195
11259
|
continue;
|
|
11196
11260
|
}
|
|
11197
|
-
const rel = entries[index
|
|
11261
|
+
const rel = entries[index];
|
|
11198
11262
|
if (rel) {
|
|
11199
|
-
|
|
11200
|
-
|
|
11263
|
+
if (head === "D") {
|
|
11264
|
+
changes.push({
|
|
11265
|
+
path: (0, import_node_path7.join)(worktree, rel),
|
|
11266
|
+
previousContent: oldMode === GIT_MODE_SYMLINK ? void 0 : await gitFileContent(worktree, beforeRev, rel),
|
|
11267
|
+
relativePath: rel,
|
|
11268
|
+
status: "removed"
|
|
11269
|
+
});
|
|
11270
|
+
} else if (newMode === GIT_MODE_SYMLINK) {
|
|
11271
|
+
if (oldMode !== GIT_MODE_ABSENT) {
|
|
11272
|
+
changes.push({
|
|
11273
|
+
path: (0, import_node_path7.join)(worktree, rel),
|
|
11274
|
+
previousContent: oldMode === GIT_MODE_SYMLINK ? void 0 : await gitFileContent(worktree, beforeRev, rel),
|
|
11275
|
+
relativePath: rel,
|
|
11276
|
+
status: "removed"
|
|
11277
|
+
});
|
|
11278
|
+
}
|
|
11279
|
+
} else {
|
|
11280
|
+
const status = head === "A" ? "added" : "modified";
|
|
11281
|
+
changes.push({
|
|
11282
|
+
path: (0, import_node_path7.join)(worktree, rel),
|
|
11283
|
+
previousContent: oldMode === GIT_MODE_ABSENT || oldMode === GIT_MODE_SYMLINK ? void 0 : await gitFileContent(worktree, beforeRev, rel),
|
|
11284
|
+
relativePath: rel,
|
|
11285
|
+
status
|
|
11286
|
+
});
|
|
11287
|
+
}
|
|
11201
11288
|
}
|
|
11202
|
-
index +=
|
|
11289
|
+
index += 1;
|
|
11203
11290
|
}
|
|
11204
11291
|
return changes;
|
|
11205
11292
|
}
|
|
11293
|
+
async function gitFileContent(worktree, rev, relativePath) {
|
|
11294
|
+
const result = await runCommand("git", ["-C", worktree, "show", `${rev}:${relativePath}`], { allowFailure: true });
|
|
11295
|
+
return result.exitCode === 0 ? result.stdout : void 0;
|
|
11296
|
+
}
|
|
11206
11297
|
async function applyChangesToOpenViking(config, team, changes, options = {}) {
|
|
11207
11298
|
const ov = await openVikingCliForMode(false);
|
|
11208
11299
|
const failed = [];
|
|
@@ -11217,22 +11308,37 @@ async function applyChangesToOpenViking(config, team, changes, options = {}) {
|
|
|
11217
11308
|
const uri = workfileToVikingUri(config, team, change.path);
|
|
11218
11309
|
try {
|
|
11219
11310
|
if (change.status === "removed") {
|
|
11311
|
+
const currentContent2 = await readExistingMemoryContent(config, ov, uri);
|
|
11312
|
+
if (currentContent2 === void 0) {
|
|
11313
|
+
continue;
|
|
11314
|
+
}
|
|
11315
|
+
assertInboundPreviousContentMatches(change, uri, currentContent2);
|
|
11220
11316
|
await removeMemoryUri(config, ov, uri, false, options);
|
|
11221
11317
|
continue;
|
|
11222
11318
|
}
|
|
11223
|
-
if (!await
|
|
11319
|
+
if (!await isRegularFileNoSymlink(change.path)) {
|
|
11224
11320
|
continue;
|
|
11225
11321
|
}
|
|
11226
|
-
const
|
|
11227
|
-
|
|
11228
|
-
|
|
11322
|
+
const content = await readSharedInboundFileContent(uri, change.path);
|
|
11323
|
+
const currentContent = await readExistingMemoryContent(config, ov, uri);
|
|
11324
|
+
if (currentContent !== void 0) {
|
|
11325
|
+
if (change.status === "added") {
|
|
11326
|
+
if (currentContent === content) {
|
|
11327
|
+
continue;
|
|
11328
|
+
}
|
|
11329
|
+
throw new Error(
|
|
11330
|
+
`Refusing to ingest newly added shared file over existing local OpenViking resource ${uri}; inspect and resolve the local edit first.`
|
|
11331
|
+
);
|
|
11332
|
+
}
|
|
11333
|
+
assertInboundPreviousContentMatches(change, uri, currentContent);
|
|
11334
|
+
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)";
|
|
11229
11335
|
if (options.quiet !== true) {
|
|
11230
11336
|
console.warn(`share sync: ${uri}: ${reason}.`);
|
|
11231
11337
|
}
|
|
11232
11338
|
}
|
|
11233
11339
|
await ensureSharedDirectoryChain(config, ov, uri, false, options);
|
|
11234
|
-
const writeMode =
|
|
11235
|
-
await
|
|
11340
|
+
const writeMode = currentContent !== void 0 ? "replace" : "create";
|
|
11341
|
+
await writeMemoryFile(config, ov, uri, content, writeMode, false, options);
|
|
11236
11342
|
} catch (err) {
|
|
11237
11343
|
const message = err instanceof Error ? err.message : String(err);
|
|
11238
11344
|
if (options.quiet !== true) {
|
|
@@ -11243,6 +11349,25 @@ async function applyChangesToOpenViking(config, team, changes, options = {}) {
|
|
|
11243
11349
|
}
|
|
11244
11350
|
return { failed };
|
|
11245
11351
|
}
|
|
11352
|
+
async function readExistingMemoryContent(config, ov, uri) {
|
|
11353
|
+
if (!await vikingResourceExists(ov, config, uri)) {
|
|
11354
|
+
return void 0;
|
|
11355
|
+
}
|
|
11356
|
+
return readMemoryContent(config, ov, uri, false);
|
|
11357
|
+
}
|
|
11358
|
+
function assertInboundPreviousContentMatches(change, uri, currentContent) {
|
|
11359
|
+
if (change.previousContent === void 0) {
|
|
11360
|
+
throw new Error(
|
|
11361
|
+
`Refusing to apply inbound shared change for ${uri}: previous shared content is unavailable, so local edits cannot be distinguished from upstream edits.`
|
|
11362
|
+
);
|
|
11363
|
+
}
|
|
11364
|
+
const expectedContent = prepareSharedInboundContent(uri, change.previousContent);
|
|
11365
|
+
if (currentContent !== expectedContent) {
|
|
11366
|
+
throw new Error(
|
|
11367
|
+
`Refusing to apply inbound shared change for ${uri}: local OpenViking content differs from the previous shared version. Inspect and resolve the local edit first.`
|
|
11368
|
+
);
|
|
11369
|
+
}
|
|
11370
|
+
}
|
|
11246
11371
|
function mergeChanges(...lists) {
|
|
11247
11372
|
const map = /* @__PURE__ */ new Map();
|
|
11248
11373
|
for (const list of lists) {
|
|
@@ -12059,8 +12184,8 @@ async function removeVikingResourceWithRetry(ov, config, uri) {
|
|
|
12059
12184
|
return false;
|
|
12060
12185
|
}
|
|
12061
12186
|
async function vikingResourceExists2(ov, config, uri) {
|
|
12062
|
-
const
|
|
12063
|
-
return
|
|
12187
|
+
const stat5 = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
|
|
12188
|
+
return stat5.exitCode === 0;
|
|
12064
12189
|
}
|
|
12065
12190
|
async function ensureDurableMemoryDirectory(ov, config) {
|
|
12066
12191
|
await ensureMemoryDirectory(ov, config, durableMemoryDirectoryUri(config));
|
|
@@ -13238,8 +13363,6 @@ async function runSeed(config, options) {
|
|
|
13238
13363
|
importPath,
|
|
13239
13364
|
"--to",
|
|
13240
13365
|
candidate.destinationUri,
|
|
13241
|
-
"--reason",
|
|
13242
|
-
seedResourceReason(candidate),
|
|
13243
13366
|
...seedWatchArgs({
|
|
13244
13367
|
watchIntervalMinutes,
|
|
13245
13368
|
importedOriginal: importPath === candidate.filePath,
|
|
@@ -13306,19 +13429,7 @@ async function seedDependencyGraphs(config, ov, manifest, targetProjects, dryRun
|
|
|
13306
13429
|
await ensureDirectory((0, import_node_path13.dirname)(graphPath), false);
|
|
13307
13430
|
await (0, import_promises11.writeFile)(graphPath, document, { encoding: "utf8", mode: 384 });
|
|
13308
13431
|
await (0, import_promises11.chmod)(graphPath, 384);
|
|
13309
|
-
await maybeRun(
|
|
13310
|
-
false,
|
|
13311
|
-
ov,
|
|
13312
|
-
withIdentity(config, [
|
|
13313
|
-
"add-resource",
|
|
13314
|
-
graphPath,
|
|
13315
|
-
"--to",
|
|
13316
|
-
destinationUri,
|
|
13317
|
-
"--reason",
|
|
13318
|
-
`Dependency facts for ${project.name}`,
|
|
13319
|
-
"--wait"
|
|
13320
|
-
])
|
|
13321
|
-
);
|
|
13432
|
+
await maybeRun(false, ov, withIdentity(config, ["add-resource", graphPath, "--to", destinationUri, "--wait"]));
|
|
13322
13433
|
written += 1;
|
|
13323
13434
|
}
|
|
13324
13435
|
console.log(
|
|
@@ -13475,15 +13586,7 @@ async function runSeedSkills(config, options) {
|
|
|
13475
13586
|
console.log(`SKIP command in native skill mode: ${skill.filePath}`);
|
|
13476
13587
|
continue;
|
|
13477
13588
|
}
|
|
13478
|
-
const args = nativeMode ? ["add-skill", skill.filePath, "--wait"] : [
|
|
13479
|
-
"add-resource",
|
|
13480
|
-
skill.filePath,
|
|
13481
|
-
"--to",
|
|
13482
|
-
skillResourceUri(skill),
|
|
13483
|
-
"--reason",
|
|
13484
|
-
skillResourceReason(skill),
|
|
13485
|
-
"--wait"
|
|
13486
|
-
];
|
|
13589
|
+
const args = nativeMode ? ["add-skill", skill.filePath, "--wait"] : ["add-resource", skill.filePath, "--to", skillResourceUri(skill), "--wait"];
|
|
13487
13590
|
await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
|
|
13488
13591
|
}
|
|
13489
13592
|
console.log(
|
|
@@ -13581,17 +13684,9 @@ async function prepareSeedFile(config, candidate, dryRun) {
|
|
|
13581
13684
|
await (0, import_promises11.chmod)(redactedPath, 384);
|
|
13582
13685
|
return redactedPath;
|
|
13583
13686
|
}
|
|
13584
|
-
function seedResourceReason(candidate) {
|
|
13585
|
-
return `Project guidance for ${candidate.projectName}: ${candidate.relativePath}`;
|
|
13586
|
-
}
|
|
13587
13687
|
function graphCacheFileName(projectName) {
|
|
13588
13688
|
return `${uriSegment(projectName)}-${sha256(projectName).slice(0, 8)}.graph.md`;
|
|
13589
13689
|
}
|
|
13590
|
-
function skillResourceReason(skill) {
|
|
13591
|
-
return `${skill.kind === "command" ? "Agent command" : "Agent skill"} catalog item from ${skill.source}: ${(0, import_node_path13.basename)(
|
|
13592
|
-
skill.filePath
|
|
13593
|
-
)}`;
|
|
13594
|
-
}
|
|
13595
13690
|
async function collectSkillCandidates(config) {
|
|
13596
13691
|
const sources = [
|
|
13597
13692
|
{ kind: "skill", pattern: "~/.codex/skills/**/SKILL.md", source: "codex-global" },
|
|
@@ -13716,23 +13811,6 @@ function redactJsonValue(value) {
|
|
|
13716
13811
|
function isSensitiveKey(key) {
|
|
13717
13812
|
return /token|secret|password|credential|authorization|api[_-]?key|client[_-]?secret|bearer/i.test(key);
|
|
13718
13813
|
}
|
|
13719
|
-
function detectSecretMatches(content) {
|
|
13720
|
-
const detectors = [
|
|
13721
|
-
{ label: "private-key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
13722
|
-
{ label: "openai-key", regex: /sk-[A-Za-z0-9_-]{24,}/ },
|
|
13723
|
-
{ label: "github-token", regex: /gh[pousr]_[A-Za-z0-9_]{24,}/ },
|
|
13724
|
-
{ label: "slack-token", regex: /xox[abprs]-[A-Za-z0-9-]{24,}/ },
|
|
13725
|
-
{ label: "bearer-token", regex: /Bearer\s+[A-Za-z0-9._~+/=-]{24,}/i },
|
|
13726
|
-
{ label: "aws-access-key", regex: /AKIA[0-9A-Z]{16}/ }
|
|
13727
|
-
];
|
|
13728
|
-
const matches = [];
|
|
13729
|
-
for (const detector of detectors) {
|
|
13730
|
-
if (detector.regex.test(content)) {
|
|
13731
|
-
matches.push(detector.label);
|
|
13732
|
-
}
|
|
13733
|
-
}
|
|
13734
|
-
return matches;
|
|
13735
|
-
}
|
|
13736
13814
|
|
|
13737
13815
|
// src/lifecycle.ts
|
|
13738
13816
|
var import_node_child_process4 = require("node:child_process");
|
|
@@ -13851,6 +13929,7 @@ function escapeRegExp3(value) {
|
|
|
13851
13929
|
// src/update.ts
|
|
13852
13930
|
var NPM_PACKAGE_NAME = "threadnote";
|
|
13853
13931
|
var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
13932
|
+
var ALLOW_UNTRUSTED_REGISTRY_ENV = "THREADNOTE_ALLOW_UNTRUSTED_NPM_REGISTRY";
|
|
13854
13933
|
var UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
13855
13934
|
var POST_UPDATE_MIGRATIONS_FILE = "post-update-migrations.json";
|
|
13856
13935
|
var POST_UPDATE_STATE_FILE = "post-update-state.json";
|
|
@@ -13881,7 +13960,7 @@ async function maybeNotifyUpdate(config, options = {}) {
|
|
|
13881
13960
|
}
|
|
13882
13961
|
}
|
|
13883
13962
|
async function runUpdate(config, options) {
|
|
13884
|
-
const registry =
|
|
13963
|
+
const registry = resolveUpdateRegistry(options.registry, options.allowUntrustedRegistry);
|
|
13885
13964
|
const info2 = await withSpinner(
|
|
13886
13965
|
"Checking npm for latest threadnote version",
|
|
13887
13966
|
() => getUpdateInfo(config, {
|
|
@@ -14392,10 +14471,31 @@ function updatePackageCommand(runtime, registry) {
|
|
|
14392
14471
|
};
|
|
14393
14472
|
}
|
|
14394
14473
|
function normalizeRegistry(registry) {
|
|
14395
|
-
|
|
14474
|
+
const normalized = registry.endsWith("/") ? registry : `${registry}/`;
|
|
14475
|
+
const url = new URL(normalized);
|
|
14476
|
+
if (url.protocol !== "https:") {
|
|
14477
|
+
throw new Error(`npm registry must use https: ${normalized}`);
|
|
14478
|
+
}
|
|
14479
|
+
return url.toString();
|
|
14396
14480
|
}
|
|
14397
14481
|
function updateRegistry() {
|
|
14398
|
-
return
|
|
14482
|
+
return resolveUpdateRegistry(void 0, false);
|
|
14483
|
+
}
|
|
14484
|
+
function resolveUpdateRegistry(registry, allowUntrustedRegistry) {
|
|
14485
|
+
const normalized = normalizeRegistry(registry ?? process.env.THREADNOTE_NPM_REGISTRY ?? DEFAULT_NPM_REGISTRY);
|
|
14486
|
+
if (normalized !== DEFAULT_NPM_REGISTRY && !allowsUntrustedRegistry(allowUntrustedRegistry)) {
|
|
14487
|
+
throw new Error(
|
|
14488
|
+
`Refusing custom npm registry ${normalized}: threadnote update does not verify package signatures from alternate registries. Use the default registry, pass --allow-untrusted-registry, or set ${ALLOW_UNTRUSTED_REGISTRY_ENV}=1 only for an approved mirror.`
|
|
14489
|
+
);
|
|
14490
|
+
}
|
|
14491
|
+
return normalized;
|
|
14492
|
+
}
|
|
14493
|
+
function allowsUntrustedRegistry(option) {
|
|
14494
|
+
if (option === true) {
|
|
14495
|
+
return true;
|
|
14496
|
+
}
|
|
14497
|
+
const envValue = process.env[ALLOW_UNTRUSTED_REGISTRY_ENV]?.trim().toLowerCase();
|
|
14498
|
+
return envValue === "1" || envValue === "true" || envValue === "yes";
|
|
14399
14499
|
}
|
|
14400
14500
|
function isUpdateNotificationDisabled() {
|
|
14401
14501
|
return process.env.CI !== void 0 || process.env.NO_UPDATE_NOTIFIER !== void 0 || process.env.THREADNOTE_NO_UPDATE_CHECK !== void 0;
|
|
@@ -15905,7 +16005,7 @@ function parsePackageManager(value) {
|
|
|
15905
16005
|
// src/version_command.ts
|
|
15906
16006
|
async function runVersion(config, options) {
|
|
15907
16007
|
const currentVersion = await currentPackageVersion();
|
|
15908
|
-
const registry =
|
|
16008
|
+
const registry = resolveUpdateRegistry(options.registry, options.allowUntrustedRegistry);
|
|
15909
16009
|
let latestVersion;
|
|
15910
16010
|
let latestWarning;
|
|
15911
16011
|
try {
|
|
@@ -16018,7 +16118,11 @@ async function readManagedMemory(config, uri) {
|
|
|
16018
16118
|
if (!path2) {
|
|
16019
16119
|
throw new Error(`Manager can only read current-user memory URIs: ${uri}`);
|
|
16020
16120
|
}
|
|
16021
|
-
const
|
|
16121
|
+
const pathStat = await (0, import_promises16.lstat)(path2);
|
|
16122
|
+
if (!pathStat.isFile()) {
|
|
16123
|
+
throw new Error(`Manager can only read regular memory files: ${uri}`);
|
|
16124
|
+
}
|
|
16125
|
+
const content = await (0, import_promises16.readFile)(path2, "utf8");
|
|
16022
16126
|
const relativePath = (0, import_node_path16.relative)(localMemoriesRoot(config), path2).split(import_node_path16.sep).join("/");
|
|
16023
16127
|
const record = parseMemoryDocument(uri, content);
|
|
16024
16128
|
return {
|
|
@@ -16326,10 +16430,13 @@ async function serveStatic(context, url, response) {
|
|
|
16326
16430
|
response.end(content);
|
|
16327
16431
|
}
|
|
16328
16432
|
async function readTree(config, path2, uri, relativePath, options = {}) {
|
|
16329
|
-
const pathStat = await (0, import_promises16.
|
|
16433
|
+
const pathStat = await (0, import_promises16.lstat)(path2);
|
|
16330
16434
|
const name = relativePath ? relativePath.split("/").at(-1) ?? relativePath : options.rootName ?? "memories";
|
|
16331
16435
|
const isDir = pathStat.isDirectory();
|
|
16332
16436
|
if (!isDir) {
|
|
16437
|
+
if (!pathStat.isFile()) {
|
|
16438
|
+
throw new Error(`Manager can only read regular files or directories: ${uri}`);
|
|
16439
|
+
}
|
|
16333
16440
|
const record = options.parseMemoryDocuments === false ? void 0 : parseMemoryDocument(uri, await (0, import_promises16.readFile)(path2, "utf8").catch(() => ""));
|
|
16334
16441
|
return {
|
|
16335
16442
|
isDir: false,
|
|
@@ -16346,7 +16453,7 @@ async function readTree(config, path2, uri, relativePath, options = {}) {
|
|
|
16346
16453
|
}
|
|
16347
16454
|
const entries = await (0, import_promises16.readdir)(path2, { withFileTypes: true });
|
|
16348
16455
|
const children = await Promise.all(
|
|
16349
|
-
entries.sort(
|
|
16456
|
+
entries.filter((entry) => entry.isDirectory() || entry.isFile()).sort(
|
|
16350
16457
|
(left, right) => Number(right.isDirectory()) - Number(left.isDirectory()) || left.name.localeCompare(right.name)
|
|
16351
16458
|
).map((entry) => {
|
|
16352
16459
|
const childRelative = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
@@ -16524,7 +16631,7 @@ async function removeManagedFolder(config, uri) {
|
|
|
16524
16631
|
if (!path2) {
|
|
16525
16632
|
throw new Error(`Manager can only remove current-user memory folders: ${uri}`);
|
|
16526
16633
|
}
|
|
16527
|
-
const pathStat = await (0, import_promises16.
|
|
16634
|
+
const pathStat = await (0, import_promises16.lstat)(path2);
|
|
16528
16635
|
if (!pathStat.isDirectory()) {
|
|
16529
16636
|
throw new Error(`Not a folder: ${uri}`);
|
|
16530
16637
|
}
|
|
@@ -16986,10 +17093,10 @@ async function main() {
|
|
|
16986
17093
|
}
|
|
16987
17094
|
await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
|
|
16988
17095
|
});
|
|
16989
|
-
program2.command("version").description("Print the installed Threadnote version, latest npm version, and release notes").option("--registry <url>", "npm registry URL",
|
|
17096
|
+
program2.command("version").description("Print the installed Threadnote version, latest npm version, and release notes").option("--registry <url>", "npm registry URL").option("--allow-untrusted-registry", "Allow a non-default npm registry without package signature verification").action(async (options) => {
|
|
16990
17097
|
await runVersion(getRuntimeConfig(program2), options);
|
|
16991
17098
|
});
|
|
16992
|
-
program2.command("update").description("Update the published Threadnote package, then repair local shims and MCP config").option("--check", "Only check whether a newer version is available").option("--dry-run", "Print update and repair commands without running them").option("--force", "Run package-manager update even if this version is already current").option("--registry <url>", "npm registry URL",
|
|
17099
|
+
program2.command("update").description("Update the published Threadnote package, then repair local shims and MCP config").option("--check", "Only check whether a newer version is available").option("--dry-run", "Print update and repair commands without running them").option("--force", "Run package-manager update even if this version is already current").option("--registry <url>", "npm registry URL").option("--allow-untrusted-registry", "Allow a non-default npm registry without package signature verification").option("--runtime <runtime>", "auto, npm, bun, or deno", parseUpdateRuntime, "auto").option("--no-repair", "Skip threadnote repair after updating the package").option("--no-post-update", "Skip post-update migration prompts").option("--yes", "Accept applicable post-update migrations without prompting").action(async (options) => {
|
|
16993
17100
|
await runUpdate(getRuntimeConfig(program2), options);
|
|
16994
17101
|
});
|
|
16995
17102
|
program2.command("post-update", { hidden: true }).description("Run packaged post-update migration prompts").requiredOption("--from-version <version>", "Version before update").requiredOption("--to-version <version>", "Version after update").option("--dry-run", "Print post-update actions without running them").option("--yes", "Accept applicable post-update migrations without prompting").action(async (options) => {
|
package/docs/index.html
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
26
26
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
27
27
|
<link
|
|
28
|
-
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap"
|
|
28
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap"
|
|
29
29
|
rel="stylesheet"
|
|
30
30
|
/>
|
|
31
31
|
|
|
@@ -783,7 +783,7 @@
|
|
|
783
783
|
handoffs, seeded repo docs, reusable skills, and team knowledge.
|
|
784
784
|
</p>
|
|
785
785
|
<div class="cta-row">
|
|
786
|
-
<span class="pill"><span id="version-tag">v1.
|
|
786
|
+
<span class="pill"><span id="version-tag">v1.6.0</span> · local-first · AGPL-3.0-or-later</span>
|
|
787
787
|
</div>
|
|
788
788
|
</div>
|
|
789
789
|
<div class="scroll-cue">scroll ↓ · <span class="kbd">↓</span> / <span class="kbd">j</span></div>
|
|
@@ -1147,7 +1147,7 @@
|
|
|
1147
1147
|
<span class="out">--user`, which fails on PEP 668 setups.</span>
|
|
1148
1148
|
<span class="out">Install uv now? [Y/n] </span><span class="out-strong">Y</span>
|
|
1149
1149
|
<span class="out">Installing uv via Homebrew…</span>
|
|
1150
|
-
<span class="out-strong">OK openviking 0.4.
|
|
1150
|
+
<span class="out-strong">OK openviking 0.4.7 ready · server healthy</span></pre>
|
|
1151
1151
|
</div>
|
|
1152
1152
|
<p class="muted" style="margin-top: 0.75rem; font-size: 0.9rem">
|
|
1153
1153
|
Or manually: <code>npm install -g threadnote && threadnote install</code>. On a fresh macOS / modern
|
|
@@ -1203,7 +1203,7 @@
|
|
|
1203
1203
|
--repo ~/src/mobile-app</span>
|
|
1204
1204
|
<span class="out">Wrote ~/.openviking/seed-manifest.yaml (2 project(s))</span>
|
|
1205
1205
|
|
|
1206
|
-
<span class="cmd">$ threadnote seed</span>
|
|
1206
|
+
<span class="cmd">$ threadnote seed --graph</span>
|
|
1207
1207
|
<span class="out-strong">Imported 47 curated path(s); skipped 6 secret-flagged.</span>
|
|
1208
1208
|
|
|
1209
1209
|
<span class="cmd">$ threadnote seed-skills</span>
|
|
@@ -1228,7 +1228,11 @@ projects:
|
|
|
1228
1228
|
uri: viking://resources/repos/mobile-app
|
|
1229
1229
|
seed:
|
|
1230
1230
|
- AGENTS.md
|
|
1231
|
-
- docs/**/*.md
|
|
1231
|
+
- docs/**/*.md
|
|
1232
|
+
worksets:
|
|
1233
|
+
- name: mobile_web
|
|
1234
|
+
description: Mobile and web app context
|
|
1235
|
+
projects: [web-app, mobile-app]</code></pre>
|
|
1232
1236
|
</div>
|
|
1233
1237
|
</div>
|
|
1234
1238
|
|
|
@@ -1241,6 +1245,14 @@ projects:
|
|
|
1241
1245
|
<strong>Seeding is opt-in and re-runnable.</strong> <code>threadnote seed</code> never deletes; it
|
|
1242
1246
|
upserts.
|
|
1243
1247
|
</li>
|
|
1248
|
+
<li>
|
|
1249
|
+
<strong>Worksets recall related repos together.</strong> Ask for a workset when a task crosses mobile,
|
|
1250
|
+
web, API, or shared-library boundaries.
|
|
1251
|
+
</li>
|
|
1252
|
+
<li>
|
|
1253
|
+
<strong>Dependency facts are searchable.</strong> <code>threadnote seed --graph</code> adds compact
|
|
1254
|
+
package and module facts without indexing source files.
|
|
1255
|
+
</li>
|
|
1244
1256
|
<li>
|
|
1245
1257
|
<strong>Skills are a first-class catalog.</strong> <code>seed-skills</code> makes reusable workflows
|
|
1246
1258
|
discoverable: testing, release, on-call, debugging, plugin guidance.
|
|
@@ -1339,6 +1351,9 @@ threadnote remember \
|
|
|
1339
1351
|
threadnote handoff \
|
|
1340
1352
|
--project web-app --topic auth-token-refresh \
|
|
1341
1353
|
--task "Address reviewer comments on token refresh PR" \
|
|
1354
|
+
--pr "https://github.com/acme/web-app/pull/184" \
|
|
1355
|
+
--ci "lint and auth tests passing" \
|
|
1356
|
+
--reference "viking://user/<you>/memories/durable/projects/web-app/token-client.md" \
|
|
1342
1357
|
--tests "make lint-lite; mocha auth_client_test 77 passing" \
|
|
1343
1358
|
--next-step "Push and request re-review"
|
|
1344
1359
|
|
|
@@ -1553,6 +1568,19 @@ threadnote remember \
|
|
|
1553
1568
|
</div>
|
|
1554
1569
|
</div>
|
|
1555
1570
|
|
|
1571
|
+
<div class="vignette reveal">
|
|
1572
|
+
<div class="v-say">Find the latest mobile and web context for sign-in.</div>
|
|
1573
|
+
<pre class="v-call">recall_context({query: "latest sign-in handoff", workset: "mobile_web"})</pre>
|
|
1574
|
+
<pre class="v-response">
|
|
1575
|
+
→ mobile-app guidance, handoffs, and durable memories
|
|
1576
|
+
→ web-app guidance, handoffs, and durable memories</pre
|
|
1577
|
+
>
|
|
1578
|
+
<div class="v-answer">
|
|
1579
|
+
Worksets let a manifest group related repos once, then recall them together for cross-platform work. A
|
|
1580
|
+
typo fails early instead of silently falling back to broad search.
|
|
1581
|
+
</div>
|
|
1582
|
+
</div>
|
|
1583
|
+
|
|
1556
1584
|
<div class="vignette reveal">
|
|
1557
1585
|
<div class="v-say">Save where we are — I'm switching to another agent.</div>
|
|
1558
1586
|
<pre class="v-call">
|
|
@@ -1591,8 +1619,9 @@ remember_context({kind: "handoff", project: "<repo>", topic: "auto-precomp
|
|
|
1591
1619
|
Stored: viking://user/you/memories/handoffs/active/<repo>/auto-precompact.md</pre
|
|
1592
1620
|
>
|
|
1593
1621
|
<div class="v-answer">
|
|
1594
|
-
Compaction summarizes the conversation arc but loses
|
|
1595
|
-
first; the post-compaction turn recalls them and resumes
|
|
1622
|
+
Compaction summarizes the conversation arc but loses concrete working state. Threadnote's hook captures
|
|
1623
|
+
a scrubbed trace summary and current handoff first; the post-compaction turn recalls them and resumes
|
|
1624
|
+
without amnesia.
|
|
1596
1625
|
</div>
|
|
1597
1626
|
</div>
|
|
1598
1627
|
|
|
@@ -1651,7 +1680,7 @@ threadnote doctor --dry-run</code></pre>
|
|
|
1651
1680
|
</div>
|
|
1652
1681
|
|
|
1653
1682
|
<p class="colophon">
|
|
1654
|
-
threadnote · AGPL-3.0-or-later · built on OpenViking 0.4.
|
|
1683
|
+
threadnote · AGPL-3.0-or-later · built on OpenViking 0.4.7 · use <span class="kbd">↑</span>
|
|
1655
1684
|
<span class="kbd">↓</span> / <span class="kbd">j</span> <span class="kbd">k</span> to navigate
|
|
1656
1685
|
</p>
|
|
1657
1686
|
</div>
|