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/threadnote.cjs
CHANGED
|
@@ -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,24 @@ function parseJsonConfigObject(content) {
|
|
|
3589
3674
|
}
|
|
3590
3675
|
}
|
|
3591
3676
|
function redactText(content) {
|
|
3592
|
-
return content
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3677
|
+
return redactSensitiveText(content);
|
|
3678
|
+
}
|
|
3679
|
+
var GIT_ENVIRONMENT_KEYS = [
|
|
3680
|
+
"GIT_DIR",
|
|
3681
|
+
"GIT_WORK_TREE",
|
|
3682
|
+
"GIT_INDEX_FILE",
|
|
3683
|
+
"GIT_PREFIX",
|
|
3684
|
+
"GIT_COMMON_DIR",
|
|
3685
|
+
"GIT_OBJECT_DIRECTORY",
|
|
3686
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
3687
|
+
"GIT_QUARANTINE_PATH"
|
|
3688
|
+
];
|
|
3689
|
+
function withoutGitEnvironment(env = process.env) {
|
|
3690
|
+
const next = { ...env };
|
|
3691
|
+
for (const key of GIT_ENVIRONMENT_KEYS) {
|
|
3692
|
+
delete next[key];
|
|
3693
|
+
}
|
|
3694
|
+
return next;
|
|
3596
3695
|
}
|
|
3597
3696
|
async function walkFiles(root) {
|
|
3598
3697
|
const files = [];
|
|
@@ -3781,6 +3880,7 @@ async function runCommand(executable, args, options = {}) {
|
|
|
3781
3880
|
{
|
|
3782
3881
|
cwd: options.cwd,
|
|
3783
3882
|
encoding: "utf8",
|
|
3883
|
+
env: commandEnvironment(executable, options.env),
|
|
3784
3884
|
maxBuffer: maxOutputBytes
|
|
3785
3885
|
},
|
|
3786
3886
|
(err, stdout2, stderr) => {
|
|
@@ -3820,6 +3920,12 @@ async function runCommand(executable, args, options = {}) {
|
|
|
3820
3920
|
}
|
|
3821
3921
|
});
|
|
3822
3922
|
}
|
|
3923
|
+
function commandEnvironment(executable, env) {
|
|
3924
|
+
if ((0, import_node_path2.basename)(executable) !== "git") {
|
|
3925
|
+
return env;
|
|
3926
|
+
}
|
|
3927
|
+
return withoutGitEnvironment(env ?? process.env);
|
|
3928
|
+
}
|
|
3823
3929
|
function commandResultFromExecFileCallback(params) {
|
|
3824
3930
|
if (params.failureMessage) {
|
|
3825
3931
|
return { exitCode: 124, stderr: params.failureMessage, stdout: params.stdout };
|
|
@@ -3857,6 +3963,17 @@ async function gitValue(args, cwd = getInvocationCwd()) {
|
|
|
3857
3963
|
return result.stdout.trim();
|
|
3858
3964
|
}
|
|
3859
3965
|
async function resolveRepoName(cwd = getInvocationCwd()) {
|
|
3966
|
+
const repoRoot = await gitValue(["rev-parse", "--show-toplevel"], cwd);
|
|
3967
|
+
if (!repoRoot) {
|
|
3968
|
+
return void 0;
|
|
3969
|
+
}
|
|
3970
|
+
const remoteName = await resolveGitRemoteRepoName(repoRoot);
|
|
3971
|
+
if (remoteName) {
|
|
3972
|
+
return remoteName;
|
|
3973
|
+
}
|
|
3974
|
+
return resolveRepoFolderName(repoRoot);
|
|
3975
|
+
}
|
|
3976
|
+
async function resolveRepoFolderName(cwd = getInvocationCwd()) {
|
|
3860
3977
|
const repoRoot = await gitValue(["rev-parse", "--show-toplevel"], cwd);
|
|
3861
3978
|
if (!repoRoot) {
|
|
3862
3979
|
return void 0;
|
|
@@ -3872,6 +3989,37 @@ async function resolveRepoName(cwd = getInvocationCwd()) {
|
|
|
3872
3989
|
}
|
|
3873
3990
|
return (0, import_node_path2.basename)(repoRoot);
|
|
3874
3991
|
}
|
|
3992
|
+
async function resolveGitRemoteRepoName(repoRoot) {
|
|
3993
|
+
const originUrl = await gitValue(["remote", "get-url", "origin"], repoRoot);
|
|
3994
|
+
const originName = originUrl ? gitRemoteRepoName(originUrl) : void 0;
|
|
3995
|
+
if (originName) {
|
|
3996
|
+
return originName;
|
|
3997
|
+
}
|
|
3998
|
+
const remotes = await gitValue(["remote"], repoRoot);
|
|
3999
|
+
const remote = remotes?.split(/\r?\n/).map((name) => name.trim()).find((name) => name.length > 0);
|
|
4000
|
+
if (!remote) {
|
|
4001
|
+
return void 0;
|
|
4002
|
+
}
|
|
4003
|
+
const remoteUrl = await gitValue(["remote", "get-url", remote], repoRoot);
|
|
4004
|
+
return remoteUrl ? gitRemoteRepoName(remoteUrl) : void 0;
|
|
4005
|
+
}
|
|
4006
|
+
function gitRemoteRepoName(remoteUrl) {
|
|
4007
|
+
const trimmed = remoteUrl.trim();
|
|
4008
|
+
if (!trimmed) {
|
|
4009
|
+
return void 0;
|
|
4010
|
+
}
|
|
4011
|
+
let remotePath = trimmed.replace(/[?#].*$/, "");
|
|
4012
|
+
try {
|
|
4013
|
+
remotePath = new URL(trimmed).pathname;
|
|
4014
|
+
} catch (_err) {
|
|
4015
|
+
const scpLike = trimmed.match(/^[^@\s/]+@[^:\s]+:(.+)$/);
|
|
4016
|
+
if (scpLike?.[1]) {
|
|
4017
|
+
remotePath = scpLike[1];
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
const name = remotePath.replace(/[\\/]+$/, "").split(/[\\/:]/).filter(Boolean).pop()?.replace(/\.git$/i, "");
|
|
4021
|
+
return name && name !== "." && name !== ".." ? name : void 0;
|
|
4022
|
+
}
|
|
3875
4023
|
async function runInteractive(executable, args) {
|
|
3876
4024
|
return new Promise((resolvePromise) => {
|
|
3877
4025
|
const child = (0, import_node_child_process2.spawn)(executable, args, { stdio: "inherit" });
|
|
@@ -4141,6 +4289,13 @@ async function enrichRecallQueryWithWorkspaceContext(query, options = {}) {
|
|
|
4141
4289
|
async function enrichRecallQueryWithWorkspaceProjectContext(query, options = {}) {
|
|
4142
4290
|
return enrichRecallQueryWithWorkspaceTerms(query, options, false);
|
|
4143
4291
|
}
|
|
4292
|
+
async function resolveWorkspaceRepoName(options = {}) {
|
|
4293
|
+
const cwd = options.cwd ?? (options.includeProcessCwd === false ? void 0 : getInvocationCwd());
|
|
4294
|
+
if (!cwd || !(0, import_node_path2.isAbsolute)(cwd)) {
|
|
4295
|
+
return void 0;
|
|
4296
|
+
}
|
|
4297
|
+
return resolveRepoName(cwd);
|
|
4298
|
+
}
|
|
4144
4299
|
async function enrichRecallQueryWithWorkspaceTerms(query, options, includeBranch) {
|
|
4145
4300
|
if (!recallQueryRequestsWorkspaceContext(query)) {
|
|
4146
4301
|
return query;
|
|
@@ -4159,10 +4314,11 @@ async function currentWorkspaceRecallTerms(options, includeBranch) {
|
|
|
4159
4314
|
return [];
|
|
4160
4315
|
}
|
|
4161
4316
|
const branch = await gitValue(["branch", "--show-current"], repoRoot);
|
|
4317
|
+
const repoName = await resolveWorkspaceRepoName({ cwd, includeProcessCwd: false });
|
|
4162
4318
|
const parent = (0, import_node_path2.dirname)(repoRoot);
|
|
4163
4319
|
return uniqueUsefulWorkspaceTerms([
|
|
4164
4320
|
{ source: "branch", value: includeBranch ? branch : void 0 },
|
|
4165
|
-
{ source: "path", value:
|
|
4321
|
+
{ source: "path", value: repoName },
|
|
4166
4322
|
{ source: "path", value: parent === (0, import_node_os.homedir)() ? void 0 : (0, import_node_path2.basename)(parent) }
|
|
4167
4323
|
]);
|
|
4168
4324
|
}
|
|
@@ -5169,9 +5325,6 @@ function existsSyncDirectory(path2) {
|
|
|
5169
5325
|
var import_promises6 = require("node:fs/promises");
|
|
5170
5326
|
var import_node_path8 = require("node:path");
|
|
5171
5327
|
|
|
5172
|
-
// src/manifest.ts
|
|
5173
|
-
var import_promises3 = require("node:fs/promises");
|
|
5174
|
-
|
|
5175
5328
|
// node_modules/js-yaml/dist/js-yaml.mjs
|
|
5176
5329
|
var __create2 = Object.create;
|
|
5177
5330
|
var __defProp2 = Object.defineProperty;
|
|
@@ -7535,6 +7688,7 @@ var { Type, Schema, FAILSAFE_SCHEMA, JSON_SCHEMA, CORE_SCHEMA, DEFAULT_SCHEMA, l
|
|
|
7535
7688
|
var index_vite_proxy_tmp_default = import_js_yaml.default;
|
|
7536
7689
|
|
|
7537
7690
|
// src/manifest.ts
|
|
7691
|
+
var import_promises3 = require("node:fs/promises");
|
|
7538
7692
|
function uriSegment(value) {
|
|
7539
7693
|
const normalized = value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
7540
7694
|
return normalized.length > 0 ? normalized : "unknown";
|
|
@@ -8469,53 +8623,6 @@ var PACK_FILES_DIR = "files";
|
|
|
8469
8623
|
var PACK_ROOT_TOKEN = "${THREADNOTE_PACK_ROOT}";
|
|
8470
8624
|
var AUTO_SHARE_FETCH_INTERVAL_MS = 5 * 60 * 1e3;
|
|
8471
8625
|
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
8626
|
var autoShareStates = /* @__PURE__ */ new Map();
|
|
8520
8627
|
async function runShareInit(config, remoteUrl, options) {
|
|
8521
8628
|
if (!remoteUrl.trim()) {
|
|
@@ -8538,7 +8645,15 @@ async function runShareInit(config, remoteUrl, options) {
|
|
|
8538
8645
|
await ensureDirectory((0, import_node_path7.dirname)(worktree), dryRun);
|
|
8539
8646
|
await ensureDirectory((0, import_node_path7.dirname)(gitdir), dryRun);
|
|
8540
8647
|
const git = await requiredExecutable("git");
|
|
8541
|
-
await maybeRun(dryRun, git, [
|
|
8648
|
+
await maybeRun(dryRun, git, [
|
|
8649
|
+
"clone",
|
|
8650
|
+
"-c",
|
|
8651
|
+
"core.symlinks=false",
|
|
8652
|
+
`--separate-git-dir=${gitdir}`,
|
|
8653
|
+
"--",
|
|
8654
|
+
remoteUrl,
|
|
8655
|
+
worktree
|
|
8656
|
+
]);
|
|
8542
8657
|
const newConfig = {
|
|
8543
8658
|
addedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8544
8659
|
gitdir,
|
|
@@ -8972,7 +9087,7 @@ async function publishShareGitChange(worktree, relativePath, commitMessage, opti
|
|
|
8972
9087
|
const git = await requiredExecutable("git");
|
|
8973
9088
|
const messages = [];
|
|
8974
9089
|
const paths = typeof relativePath === "string" ? [relativePath] : [...relativePath];
|
|
8975
|
-
const stageArgs = verb === "rm" ? ["-C", worktree, "rm", ...paths] : ["-C", worktree, "add", "--", ...paths];
|
|
9090
|
+
const stageArgs = verb === "rm" ? ["-C", worktree, "rm", "--", ...paths] : ["-C", worktree, "add", "--", ...paths];
|
|
8976
9091
|
const stageResult = await runGitCommand(dryRun, git, stageArgs, `git ${verb} failed`);
|
|
8977
9092
|
if (stageResult) {
|
|
8978
9093
|
messages.push(`git ${verb}: ${stageResult.stdout.trim() || "ok"}`);
|
|
@@ -9386,13 +9501,7 @@ function isProbablyBinary(buffer) {
|
|
|
9386
9501
|
}
|
|
9387
9502
|
}
|
|
9388
9503
|
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;
|
|
9504
|
+
return credentialScrubberBlocker(buffer.toString("latin1"));
|
|
9396
9505
|
}
|
|
9397
9506
|
function detectBinaryLocalPath(buffer, rewriteRoots) {
|
|
9398
9507
|
const latin1 = buffer.toString("latin1");
|
|
@@ -10067,7 +10176,7 @@ async function runShareSetUrl(config, remoteUrl, options) {
|
|
|
10067
10176
|
const git = await requiredExecutable("git");
|
|
10068
10177
|
if (dryRun) {
|
|
10069
10178
|
console.log(
|
|
10070
|
-
`Would run: ${formatShellCommand(git, ["-C", team.config.worktree, "remote", "set-url", DEFAULT_GIT_REMOTE_NAME, remoteUrl])}`
|
|
10179
|
+
`Would run: ${formatShellCommand(git, ["-C", team.config.worktree, "remote", "set-url", DEFAULT_GIT_REMOTE_NAME, "--", remoteUrl])}`
|
|
10071
10180
|
);
|
|
10072
10181
|
console.log(
|
|
10073
10182
|
`Would run: ${formatShellCommand(git, ["-C", team.config.worktree, "fetch", DEFAULT_GIT_REMOTE_NAME])}`
|
|
@@ -10075,7 +10184,7 @@ async function runShareSetUrl(config, remoteUrl, options) {
|
|
|
10075
10184
|
console.log(`Would write teams file: ${teamsFilePath(config)}`);
|
|
10076
10185
|
return;
|
|
10077
10186
|
}
|
|
10078
|
-
await runCommand(git, ["-C", team.config.worktree, "remote", "set-url", DEFAULT_GIT_REMOTE_NAME, remoteUrl]);
|
|
10187
|
+
await runCommand(git, ["-C", team.config.worktree, "remote", "set-url", DEFAULT_GIT_REMOTE_NAME, "--", remoteUrl]);
|
|
10079
10188
|
await runCommand(git, ["-C", team.config.worktree, "fetch", DEFAULT_GIT_REMOTE_NAME]);
|
|
10080
10189
|
const teamsFile = await readTeamsFile(config);
|
|
10081
10190
|
const updatedTeam = { ...team.config, remote: remoteUrl };
|
|
@@ -10383,8 +10492,8 @@ function vikingUriToWorktreeRelative(config, uri, team) {
|
|
|
10383
10492
|
}
|
|
10384
10493
|
async function isRegularFileNoSymlink(path2) {
|
|
10385
10494
|
try {
|
|
10386
|
-
const
|
|
10387
|
-
return
|
|
10495
|
+
const stat5 = await (0, import_promises5.lstat)(path2);
|
|
10496
|
+
return stat5.isFile();
|
|
10388
10497
|
} catch (_err) {
|
|
10389
10498
|
return false;
|
|
10390
10499
|
}
|
|
@@ -11123,9 +11232,23 @@ ${stdout2}`.toLowerCase();
|
|
|
11123
11232
|
return output2.includes("resource is busy") || output2.includes("resource is being processed");
|
|
11124
11233
|
}
|
|
11125
11234
|
async function ingestSingleFile(ov, config, uri, filePath, initialMode, options = {}) {
|
|
11126
|
-
const content = await (
|
|
11235
|
+
const content = await readSharedInboundFileContent(uri, filePath);
|
|
11127
11236
|
await writeMemoryFile(config, ov, uri, content, initialMode, false, options);
|
|
11128
11237
|
}
|
|
11238
|
+
async function readSharedInboundFileContent(uri, filePath) {
|
|
11239
|
+
if (!await isRegularFileNoSymlink(filePath)) {
|
|
11240
|
+
throw new Error(`Refusing to ingest non-regular shared file: ${filePath}`);
|
|
11241
|
+
}
|
|
11242
|
+
return prepareSharedInboundContent(uri, await (0, import_promises5.readFile)(filePath, "utf8"));
|
|
11243
|
+
}
|
|
11244
|
+
function prepareSharedInboundContent(uri, rawContent) {
|
|
11245
|
+
const stripped = stripPersonalProvenance(rawContent);
|
|
11246
|
+
const scrub = applyScrubber(stripped, { redact: false });
|
|
11247
|
+
if (scrub.blocker) {
|
|
11248
|
+
throw new Error(`Refusing to ingest ${uri}: possible ${scrub.blocker}. Strip the sensitive value upstream first.`);
|
|
11249
|
+
}
|
|
11250
|
+
return scrub.cleaned;
|
|
11251
|
+
}
|
|
11129
11252
|
async function removeMemoryUri(config, ov, uri, dryRun, options = {}) {
|
|
11130
11253
|
const args = withIdentity(config, ["rm", uri]);
|
|
11131
11254
|
if (dryRun) {
|
|
@@ -11172,8 +11295,10 @@ async function gitOutput(worktree, args, dryRun) {
|
|
|
11172
11295
|
}
|
|
11173
11296
|
return result.stdout.trim();
|
|
11174
11297
|
}
|
|
11298
|
+
var GIT_MODE_ABSENT = "000000";
|
|
11299
|
+
var GIT_MODE_SYMLINK = "120000";
|
|
11175
11300
|
async function listChangedFiles(worktree, beforeRev, afterRev) {
|
|
11176
|
-
const result = await runCommand("git", ["-C", worktree, "diff", "--
|
|
11301
|
+
const result = await runCommand("git", ["-C", worktree, "diff", "--raw", "-z", `${beforeRev}..${afterRev}`], {
|
|
11177
11302
|
allowFailure: true
|
|
11178
11303
|
});
|
|
11179
11304
|
if (result.exitCode !== 0) {
|
|
@@ -11182,27 +11307,65 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
|
|
|
11182
11307
|
const entries = result.stdout.split("\0").filter((part) => part.length > 0);
|
|
11183
11308
|
const changes = [];
|
|
11184
11309
|
for (let index = 0; index < entries.length; ) {
|
|
11185
|
-
const raw = entries[index];
|
|
11186
|
-
const
|
|
11310
|
+
const raw = entries[index++];
|
|
11311
|
+
const match = raw.match(/^:(\d{6}) (\d{6}) [0-9a-f]+ [0-9a-f]+ ([A-Z])\d*/);
|
|
11312
|
+
if (!match) {
|
|
11313
|
+
continue;
|
|
11314
|
+
}
|
|
11315
|
+
const [, oldMode, newMode, head] = match;
|
|
11187
11316
|
if (head === "R" || head === "C") {
|
|
11188
|
-
const oldRel = entries[index
|
|
11189
|
-
const newRel = entries[index +
|
|
11190
|
-
if (oldRel
|
|
11191
|
-
changes.push({
|
|
11317
|
+
const oldRel = entries[index];
|
|
11318
|
+
const newRel = entries[index + 1];
|
|
11319
|
+
if (oldRel) {
|
|
11320
|
+
changes.push({
|
|
11321
|
+
path: (0, import_node_path7.join)(worktree, oldRel),
|
|
11322
|
+
previousContent: oldMode === GIT_MODE_SYMLINK ? void 0 : await gitFileContent(worktree, beforeRev, oldRel),
|
|
11323
|
+
relativePath: oldRel,
|
|
11324
|
+
status: "removed"
|
|
11325
|
+
});
|
|
11326
|
+
}
|
|
11327
|
+
if (newRel && newMode !== GIT_MODE_SYMLINK) {
|
|
11192
11328
|
changes.push({ path: (0, import_node_path7.join)(worktree, newRel), relativePath: newRel, status: "added" });
|
|
11193
11329
|
}
|
|
11194
|
-
index +=
|
|
11330
|
+
index += 2;
|
|
11195
11331
|
continue;
|
|
11196
11332
|
}
|
|
11197
|
-
const rel = entries[index
|
|
11333
|
+
const rel = entries[index];
|
|
11198
11334
|
if (rel) {
|
|
11199
|
-
|
|
11200
|
-
|
|
11335
|
+
if (head === "D") {
|
|
11336
|
+
changes.push({
|
|
11337
|
+
path: (0, import_node_path7.join)(worktree, rel),
|
|
11338
|
+
previousContent: oldMode === GIT_MODE_SYMLINK ? void 0 : await gitFileContent(worktree, beforeRev, rel),
|
|
11339
|
+
relativePath: rel,
|
|
11340
|
+
status: "removed"
|
|
11341
|
+
});
|
|
11342
|
+
} else if (newMode === GIT_MODE_SYMLINK) {
|
|
11343
|
+
if (oldMode !== GIT_MODE_ABSENT) {
|
|
11344
|
+
changes.push({
|
|
11345
|
+
path: (0, import_node_path7.join)(worktree, rel),
|
|
11346
|
+
previousContent: oldMode === GIT_MODE_SYMLINK ? void 0 : await gitFileContent(worktree, beforeRev, rel),
|
|
11347
|
+
relativePath: rel,
|
|
11348
|
+
status: "removed"
|
|
11349
|
+
});
|
|
11350
|
+
}
|
|
11351
|
+
} else {
|
|
11352
|
+
const status = head === "A" ? "added" : "modified";
|
|
11353
|
+
changes.push({
|
|
11354
|
+
path: (0, import_node_path7.join)(worktree, rel),
|
|
11355
|
+
previousContent: oldMode === GIT_MODE_ABSENT || oldMode === GIT_MODE_SYMLINK ? void 0 : await gitFileContent(worktree, beforeRev, rel),
|
|
11356
|
+
relativePath: rel,
|
|
11357
|
+
status
|
|
11358
|
+
});
|
|
11359
|
+
}
|
|
11201
11360
|
}
|
|
11202
|
-
index +=
|
|
11361
|
+
index += 1;
|
|
11203
11362
|
}
|
|
11204
11363
|
return changes;
|
|
11205
11364
|
}
|
|
11365
|
+
async function gitFileContent(worktree, rev, relativePath) {
|
|
11366
|
+
const result = await runCommand("git", ["-C", worktree, "show", `${rev}:${relativePath}`], { allowFailure: true });
|
|
11367
|
+
return result.exitCode === 0 ? result.stdout : void 0;
|
|
11368
|
+
}
|
|
11206
11369
|
async function applyChangesToOpenViking(config, team, changes, options = {}) {
|
|
11207
11370
|
const ov = await openVikingCliForMode(false);
|
|
11208
11371
|
const failed = [];
|
|
@@ -11217,22 +11380,37 @@ async function applyChangesToOpenViking(config, team, changes, options = {}) {
|
|
|
11217
11380
|
const uri = workfileToVikingUri(config, team, change.path);
|
|
11218
11381
|
try {
|
|
11219
11382
|
if (change.status === "removed") {
|
|
11383
|
+
const currentContent2 = await readExistingMemoryContent(config, ov, uri);
|
|
11384
|
+
if (currentContent2 === void 0) {
|
|
11385
|
+
continue;
|
|
11386
|
+
}
|
|
11387
|
+
assertInboundPreviousContentMatches(change, uri, currentContent2);
|
|
11220
11388
|
await removeMemoryUri(config, ov, uri, false, options);
|
|
11221
11389
|
continue;
|
|
11222
11390
|
}
|
|
11223
|
-
if (!await
|
|
11391
|
+
if (!await isRegularFileNoSymlink(change.path)) {
|
|
11224
11392
|
continue;
|
|
11225
11393
|
}
|
|
11226
|
-
const
|
|
11227
|
-
|
|
11228
|
-
|
|
11394
|
+
const content = await readSharedInboundFileContent(uri, change.path);
|
|
11395
|
+
const currentContent = await readExistingMemoryContent(config, ov, uri);
|
|
11396
|
+
if (currentContent !== void 0) {
|
|
11397
|
+
if (change.status === "added") {
|
|
11398
|
+
if (currentContent === content) {
|
|
11399
|
+
continue;
|
|
11400
|
+
}
|
|
11401
|
+
throw new Error(
|
|
11402
|
+
`Refusing to ingest newly added shared file over existing local OpenViking resource ${uri}; inspect and resolve the local edit first.`
|
|
11403
|
+
);
|
|
11404
|
+
}
|
|
11405
|
+
assertInboundPreviousContentMatches(change, uri, currentContent);
|
|
11406
|
+
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
11407
|
if (options.quiet !== true) {
|
|
11230
11408
|
console.warn(`share sync: ${uri}: ${reason}.`);
|
|
11231
11409
|
}
|
|
11232
11410
|
}
|
|
11233
11411
|
await ensureSharedDirectoryChain(config, ov, uri, false, options);
|
|
11234
|
-
const writeMode =
|
|
11235
|
-
await
|
|
11412
|
+
const writeMode = currentContent !== void 0 ? "replace" : "create";
|
|
11413
|
+
await writeMemoryFile(config, ov, uri, content, writeMode, false, options);
|
|
11236
11414
|
} catch (err) {
|
|
11237
11415
|
const message = err instanceof Error ? err.message : String(err);
|
|
11238
11416
|
if (options.quiet !== true) {
|
|
@@ -11243,6 +11421,25 @@ async function applyChangesToOpenViking(config, team, changes, options = {}) {
|
|
|
11243
11421
|
}
|
|
11244
11422
|
return { failed };
|
|
11245
11423
|
}
|
|
11424
|
+
async function readExistingMemoryContent(config, ov, uri) {
|
|
11425
|
+
if (!await vikingResourceExists(ov, config, uri)) {
|
|
11426
|
+
return void 0;
|
|
11427
|
+
}
|
|
11428
|
+
return readMemoryContent(config, ov, uri, false);
|
|
11429
|
+
}
|
|
11430
|
+
function assertInboundPreviousContentMatches(change, uri, currentContent) {
|
|
11431
|
+
if (change.previousContent === void 0) {
|
|
11432
|
+
throw new Error(
|
|
11433
|
+
`Refusing to apply inbound shared change for ${uri}: previous shared content is unavailable, so local edits cannot be distinguished from upstream edits.`
|
|
11434
|
+
);
|
|
11435
|
+
}
|
|
11436
|
+
const expectedContent = prepareSharedInboundContent(uri, change.previousContent);
|
|
11437
|
+
if (currentContent !== expectedContent) {
|
|
11438
|
+
throw new Error(
|
|
11439
|
+
`Refusing to apply inbound shared change for ${uri}: local OpenViking content differs from the previous shared version. Inspect and resolve the local edit first.`
|
|
11440
|
+
);
|
|
11441
|
+
}
|
|
11442
|
+
}
|
|
11246
11443
|
function mergeChanges(...lists) {
|
|
11247
11444
|
const map = /* @__PURE__ */ new Map();
|
|
11248
11445
|
for (const list of lists) {
|
|
@@ -11433,6 +11630,263 @@ async function runMigrateLifecycle(config, options) {
|
|
|
11433
11630
|
].filter((part) => part !== void 0).join("; ")
|
|
11434
11631
|
);
|
|
11435
11632
|
}
|
|
11633
|
+
async function runMigrateProjectNames(config, options) {
|
|
11634
|
+
const dryRun = options.dryRun === true || options.apply !== true;
|
|
11635
|
+
const limit = options.limit ? parsePositiveInteger(options.limit, "project-name migration limit") : void 0;
|
|
11636
|
+
const context = await projectNameMigrationContext();
|
|
11637
|
+
if (!context) {
|
|
11638
|
+
console.log("No git remote project-name change applies in the current workspace.");
|
|
11639
|
+
return;
|
|
11640
|
+
}
|
|
11641
|
+
const candidates = await projectNameMigrationCandidates(config, context, limit);
|
|
11642
|
+
const seedManifestCandidate = await hasSeedManifestProjectNameMigrationCandidate(config, context);
|
|
11643
|
+
if (candidates.length === 0 && !seedManifestCandidate) {
|
|
11644
|
+
console.log(`No project-name migration candidates found for ${context.oldProject} -> ${context.newProject}.`);
|
|
11645
|
+
return;
|
|
11646
|
+
}
|
|
11647
|
+
const seedManifestUpdated = await migrateSeedManifestProjectName(config, context, dryRun);
|
|
11648
|
+
let existingCount = 0;
|
|
11649
|
+
let migratedCount = 0;
|
|
11650
|
+
let skippedCount = 0;
|
|
11651
|
+
if (candidates.length > 0) {
|
|
11652
|
+
const ov = await openVikingCliForMode(dryRun);
|
|
11653
|
+
for (const candidate of candidates) {
|
|
11654
|
+
const action = candidate.destinationExistsWithSameContent ? dryRun ? "Would consolidate duplicate" : "Consolidating duplicate" : dryRun ? "Would migrate" : "Migrating";
|
|
11655
|
+
console.log(`${action} ${candidate.sourceUri} -> ${candidate.destinationUri}`);
|
|
11656
|
+
if (!dryRun) {
|
|
11657
|
+
if (candidate.destinationExistsWithSameContent) {
|
|
11658
|
+
existingCount += 1;
|
|
11659
|
+
} else {
|
|
11660
|
+
await ensureMemoryDirectory(ov, config, parentVikingUri(candidate.destinationUri));
|
|
11661
|
+
await writeMemoryFile(config, ov, candidate.destinationUri, candidate.destinationContent, "create", false);
|
|
11662
|
+
}
|
|
11663
|
+
const removedOriginal = await removeVikingResourceWithRetry(ov, config, candidate.sourceUri);
|
|
11664
|
+
if (!removedOriginal) {
|
|
11665
|
+
console.error(
|
|
11666
|
+
`Migrated copy stored, but original is still processing. Retry later: threadnote forget ${candidate.sourceUri}`
|
|
11667
|
+
);
|
|
11668
|
+
skippedCount += 1;
|
|
11669
|
+
}
|
|
11670
|
+
}
|
|
11671
|
+
migratedCount += 1;
|
|
11672
|
+
}
|
|
11673
|
+
}
|
|
11674
|
+
console.log(
|
|
11675
|
+
[
|
|
11676
|
+
`Project-name migration summary: ${migratedCount} memor${migratedCount === 1 ? "y" : "ies"} ${dryRun ? "would be migrated" : "migrated"} from ${context.oldProject} to ${context.newProject}`,
|
|
11677
|
+
seedManifestUpdated ? `seed manifest ${dryRun ? "would be updated" : "updated"}` : "seed manifest unchanged",
|
|
11678
|
+
`${existingCount} duplicate destination(s) reused`,
|
|
11679
|
+
`${skippedCount} source(s) still processing`,
|
|
11680
|
+
dryRun ? "Run with --apply to perform this migration." : void 0,
|
|
11681
|
+
seedManifestUpdated ? `Run threadnote seed --only ${context.newProject} to re-ingest seeded resources under the new project URI.` : void 0
|
|
11682
|
+
].filter((part) => part !== void 0).join("; ")
|
|
11683
|
+
);
|
|
11684
|
+
}
|
|
11685
|
+
async function hasProjectNameMigrationCandidates(config) {
|
|
11686
|
+
const context = await projectNameMigrationContext();
|
|
11687
|
+
return context ? (await projectNameMigrationCandidates(config, context, 1)).length > 0 || await hasSeedManifestProjectNameMigrationCandidate(config, context) : false;
|
|
11688
|
+
}
|
|
11689
|
+
async function projectNameMigrationContext() {
|
|
11690
|
+
const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]);
|
|
11691
|
+
if (!repoRoot) {
|
|
11692
|
+
return void 0;
|
|
11693
|
+
}
|
|
11694
|
+
const newProject = await resolveRepoName(repoRoot);
|
|
11695
|
+
const oldProject = await resolveRepoFolderName(repoRoot);
|
|
11696
|
+
if (!newProject || !oldProject) {
|
|
11697
|
+
return void 0;
|
|
11698
|
+
}
|
|
11699
|
+
const newSegment = uriSegment(newProject);
|
|
11700
|
+
const oldSegment = uriSegment(oldProject);
|
|
11701
|
+
if (newSegment === oldSegment) {
|
|
11702
|
+
return void 0;
|
|
11703
|
+
}
|
|
11704
|
+
return { newProject, newSegment, oldProject, oldSegment, repoRoot };
|
|
11705
|
+
}
|
|
11706
|
+
async function projectNameMigrationCandidates(config, context, limit) {
|
|
11707
|
+
const candidates = [];
|
|
11708
|
+
for (const location of projectMemoryLocations()) {
|
|
11709
|
+
const sourceDirectory = (0, import_node_path8.join)(localUserMemoriesRoot(config), ...location.relativePath, context.oldSegment);
|
|
11710
|
+
const sourceDirectoryUri = `viking://user/${uriSegment(config.user)}/memories/${location.uriPath}/${context.oldSegment}`;
|
|
11711
|
+
let entries;
|
|
11712
|
+
try {
|
|
11713
|
+
entries = await (0, import_promises6.readdir)(sourceDirectory, { withFileTypes: true });
|
|
11714
|
+
} catch (_err) {
|
|
11715
|
+
continue;
|
|
11716
|
+
}
|
|
11717
|
+
for (const entry of entries) {
|
|
11718
|
+
if (!entry.isFile() || entry.name.startsWith(".") || !entry.name.endsWith(".md")) {
|
|
11719
|
+
continue;
|
|
11720
|
+
}
|
|
11721
|
+
const sourceUri = `${sourceDirectoryUri}/${entry.name}`;
|
|
11722
|
+
const content = await readTextIfExists((0, import_node_path8.join)(sourceDirectory, entry.name));
|
|
11723
|
+
if (!content) {
|
|
11724
|
+
continue;
|
|
11725
|
+
}
|
|
11726
|
+
const record = parseMemoryDocument(sourceUri, content);
|
|
11727
|
+
if (!record || !canMigrateProjectName(record, context)) {
|
|
11728
|
+
continue;
|
|
11729
|
+
}
|
|
11730
|
+
const metadata = { ...record.metadata, project: context.newProject };
|
|
11731
|
+
const destinationDirectoryUri = memoryDirectoryUri(config, metadata);
|
|
11732
|
+
const destinationDirectory = localMemoryPathForUri(config, destinationDirectoryUri);
|
|
11733
|
+
if (!destinationDirectory) {
|
|
11734
|
+
continue;
|
|
11735
|
+
}
|
|
11736
|
+
const destinationContent = formatMemoryDocument2(record.headerTitle, metadata, record.body);
|
|
11737
|
+
const destination = await projectNameMigrationDestination(
|
|
11738
|
+
destinationDirectory,
|
|
11739
|
+
entry.name,
|
|
11740
|
+
destinationContent,
|
|
11741
|
+
context.oldSegment
|
|
11742
|
+
);
|
|
11743
|
+
candidates.push({
|
|
11744
|
+
destinationContent,
|
|
11745
|
+
destinationExistsWithSameContent: destination.existsWithSameContent,
|
|
11746
|
+
destinationUri: `${destinationDirectoryUri}/${destination.filename}`,
|
|
11747
|
+
sourceUri
|
|
11748
|
+
});
|
|
11749
|
+
if (limit !== void 0 && candidates.length >= limit) {
|
|
11750
|
+
return candidates;
|
|
11751
|
+
}
|
|
11752
|
+
}
|
|
11753
|
+
}
|
|
11754
|
+
return candidates;
|
|
11755
|
+
}
|
|
11756
|
+
async function hasSeedManifestProjectNameMigrationCandidate(config, context) {
|
|
11757
|
+
return await seedManifestProjectNameMigration(config, context) !== void 0;
|
|
11758
|
+
}
|
|
11759
|
+
async function migrateSeedManifestProjectName(config, context, dryRun) {
|
|
11760
|
+
const migration = await seedManifestProjectNameMigration(config, context);
|
|
11761
|
+
if (!migration) {
|
|
11762
|
+
return false;
|
|
11763
|
+
}
|
|
11764
|
+
if (dryRun) {
|
|
11765
|
+
console.log(`Would update seed manifest: ${config.manifestPath}`);
|
|
11766
|
+
console.log(migration.output.trimEnd());
|
|
11767
|
+
return true;
|
|
11768
|
+
}
|
|
11769
|
+
await ensureDirectory((0, import_node_path8.dirname)(config.manifestPath), false);
|
|
11770
|
+
const currentContent = await readFileIfExists(config.manifestPath);
|
|
11771
|
+
if (currentContent !== void 0) {
|
|
11772
|
+
const backupPath = `${config.manifestPath}.project-name-${safeTimestamp()}`;
|
|
11773
|
+
await (0, import_promises6.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
|
|
11774
|
+
await (0, import_promises6.chmod)(backupPath, 384);
|
|
11775
|
+
console.log(`Backup: ${backupPath}`);
|
|
11776
|
+
}
|
|
11777
|
+
await (0, import_promises6.writeFile)(config.manifestPath, migration.output, { encoding: "utf8", mode: 384 });
|
|
11778
|
+
await (0, import_promises6.chmod)(config.manifestPath, 384);
|
|
11779
|
+
console.log(`Updated seed manifest: ${config.manifestPath}`);
|
|
11780
|
+
return true;
|
|
11781
|
+
}
|
|
11782
|
+
async function seedManifestProjectNameMigration(config, context) {
|
|
11783
|
+
let manifest;
|
|
11784
|
+
try {
|
|
11785
|
+
manifest = await readSeedManifest(config.manifestPath);
|
|
11786
|
+
} catch (_err) {
|
|
11787
|
+
return void 0;
|
|
11788
|
+
}
|
|
11789
|
+
const oldDefaultUri = `viking://resources/repos/${context.oldSegment}`;
|
|
11790
|
+
const newDefaultUri = `viking://resources/repos/${context.newSegment}`;
|
|
11791
|
+
const newNameExists = manifest.projects.some((project) => uriSegment(project.name) === context.newSegment);
|
|
11792
|
+
let changed = false;
|
|
11793
|
+
let renamedProject = false;
|
|
11794
|
+
const projects = manifest.projects.map((project) => {
|
|
11795
|
+
if (!isSeedManifestProjectNameCandidate(project, context, oldDefaultUri) || newNameExists) {
|
|
11796
|
+
return project;
|
|
11797
|
+
}
|
|
11798
|
+
changed = true;
|
|
11799
|
+
renamedProject = true;
|
|
11800
|
+
return {
|
|
11801
|
+
...project,
|
|
11802
|
+
name: context.newProject,
|
|
11803
|
+
uri: trimTrailingSlash(project.uri) === oldDefaultUri ? newDefaultUri : project.uri
|
|
11804
|
+
};
|
|
11805
|
+
});
|
|
11806
|
+
const worksets = renamedProject ? manifest.worksets?.map((workset) => {
|
|
11807
|
+
const members = workset.projects.map((projectName) => {
|
|
11808
|
+
if (uriSegment(projectName) !== context.oldSegment) {
|
|
11809
|
+
return projectName;
|
|
11810
|
+
}
|
|
11811
|
+
changed = true;
|
|
11812
|
+
return context.newProject;
|
|
11813
|
+
});
|
|
11814
|
+
return { ...workset, projects: members };
|
|
11815
|
+
}) : manifest.worksets;
|
|
11816
|
+
if (!changed) {
|
|
11817
|
+
return void 0;
|
|
11818
|
+
}
|
|
11819
|
+
return {
|
|
11820
|
+
output: `${index_vite_proxy_tmp_default.dump(
|
|
11821
|
+
{
|
|
11822
|
+
version: manifest.version,
|
|
11823
|
+
projects: projects.map((project) => ({
|
|
11824
|
+
name: project.name,
|
|
11825
|
+
path: project.path,
|
|
11826
|
+
uri: project.uri,
|
|
11827
|
+
seed: [...project.seed]
|
|
11828
|
+
})),
|
|
11829
|
+
...worksets ? {
|
|
11830
|
+
worksets: worksets.map((workset) => ({
|
|
11831
|
+
name: workset.name,
|
|
11832
|
+
...workset.description ? { description: workset.description } : {},
|
|
11833
|
+
projects: [...workset.projects]
|
|
11834
|
+
}))
|
|
11835
|
+
} : {},
|
|
11836
|
+
...manifest.futureMonorepo ? {
|
|
11837
|
+
future_monorepo: {
|
|
11838
|
+
path_candidates: [...manifest.futureMonorepo.pathCandidates],
|
|
11839
|
+
uri: manifest.futureMonorepo.uri
|
|
11840
|
+
}
|
|
11841
|
+
} : {}
|
|
11842
|
+
},
|
|
11843
|
+
{ lineWidth: 120, noRefs: true }
|
|
11844
|
+
)}`
|
|
11845
|
+
};
|
|
11846
|
+
}
|
|
11847
|
+
function isSeedManifestProjectNameCandidate(project, context, oldDefaultUri) {
|
|
11848
|
+
if (uriSegment(project.name) !== context.oldSegment) {
|
|
11849
|
+
return false;
|
|
11850
|
+
}
|
|
11851
|
+
return trimTrailingSlash(project.uri) === oldDefaultUri || expandPath(project.path) === context.repoRoot;
|
|
11852
|
+
}
|
|
11853
|
+
function canMigrateProjectName(record, context) {
|
|
11854
|
+
const projectSegment = record.metadata.project ? uriSegment(record.metadata.project) : context.oldSegment;
|
|
11855
|
+
return projectSegment === context.oldSegment || projectSegment === context.newSegment;
|
|
11856
|
+
}
|
|
11857
|
+
async function projectNameMigrationDestination(destinationDirectory, filename, content, oldProjectSegment) {
|
|
11858
|
+
const direct = await projectNameMigrationDestinationState(destinationDirectory, filename, content);
|
|
11859
|
+
if (!direct.exists || direct.sameContent) {
|
|
11860
|
+
return { existsWithSameContent: direct.sameContent, filename };
|
|
11861
|
+
}
|
|
11862
|
+
const stem = filename.replace(/\.md$/i, "");
|
|
11863
|
+
const fromOldProject = `${stem}-from-${oldProjectSegment}.md`;
|
|
11864
|
+
const renamed = await projectNameMigrationDestinationState(destinationDirectory, fromOldProject, content);
|
|
11865
|
+
if (!renamed.exists || renamed.sameContent) {
|
|
11866
|
+
return { existsWithSameContent: renamed.sameContent, filename: fromOldProject };
|
|
11867
|
+
}
|
|
11868
|
+
return {
|
|
11869
|
+
existsWithSameContent: false,
|
|
11870
|
+
filename: `${stem}-from-${oldProjectSegment}-${sha256(content).slice(0, 12)}.md`
|
|
11871
|
+
};
|
|
11872
|
+
}
|
|
11873
|
+
async function projectNameMigrationDestinationState(destinationDirectory, filename, content) {
|
|
11874
|
+
const existing = await readTextIfExists((0, import_node_path8.join)(destinationDirectory, filename));
|
|
11875
|
+
return { exists: existing !== void 0, sameContent: existing?.trim() === content.trim() };
|
|
11876
|
+
}
|
|
11877
|
+
function projectMemoryLocations() {
|
|
11878
|
+
return [
|
|
11879
|
+
{ relativePath: ["durable", "projects"], uriPath: "durable/projects" },
|
|
11880
|
+
{ relativePath: ["durable", "archived"], uriPath: "durable/archived" },
|
|
11881
|
+
{ relativePath: ["durable", "superseded"], uriPath: "durable/superseded" },
|
|
11882
|
+
{ relativePath: ["handoffs", "active"], uriPath: "handoffs/active" },
|
|
11883
|
+
{ relativePath: ["handoffs", "archived"], uriPath: "handoffs/archived" },
|
|
11884
|
+
{ relativePath: ["handoffs", "superseded"], uriPath: "handoffs/superseded" },
|
|
11885
|
+
{ relativePath: ["incidents", "active"], uriPath: "incidents/active" },
|
|
11886
|
+
{ relativePath: ["incidents", "archived"], uriPath: "incidents/archived" },
|
|
11887
|
+
{ relativePath: ["incidents", "superseded"], uriPath: "incidents/superseded" }
|
|
11888
|
+
];
|
|
11889
|
+
}
|
|
11436
11890
|
async function runRecall(config, options) {
|
|
11437
11891
|
if (options.dryRun !== true) {
|
|
11438
11892
|
await syncSharedReposAndLog(config);
|
|
@@ -11456,6 +11910,9 @@ async function runRecall(config, options) {
|
|
|
11456
11910
|
const dryRun = options.dryRun === true;
|
|
11457
11911
|
const inferredUri = options.uri ?? (options.inferScope === false ? void 0 : await inferRecallUri(config, projectQuery));
|
|
11458
11912
|
const project = await inferProjectFromQuery(config.manifestPath, options.project ?? projectQuery);
|
|
11913
|
+
const projectMemoryName = await recallProjectMemoryName(options.project, {
|
|
11914
|
+
includeProcessCwd: true
|
|
11915
|
+
});
|
|
11459
11916
|
const nodeLimit = options.nodeLimit ? parsePositiveInteger(options.nodeLimit, "node limit") : void 0;
|
|
11460
11917
|
const explicitWorkset = options.workset ? await requireWorkset(config.manifestPath, options.workset) : void 0;
|
|
11461
11918
|
const searchArgs = (scopeUri) => [
|
|
@@ -11475,9 +11932,19 @@ async function runRecall(config, options) {
|
|
|
11475
11932
|
const passes = [
|
|
11476
11933
|
await recallSearchHits(config, ov, searchArgs(inferredUri), { dryRun, includeArchived })
|
|
11477
11934
|
];
|
|
11935
|
+
const scopedRecallUris = new Set([inferredUri].filter((uri) => uri !== void 0));
|
|
11478
11936
|
if (options.project && project) {
|
|
11479
11937
|
const projectMemoryUri = `viking://user/${uriSegment(config.user)}/memories/durable/projects/${uriSegment(project.name)}`;
|
|
11480
|
-
|
|
11938
|
+
if (!scopedRecallUris.has(projectMemoryUri)) {
|
|
11939
|
+
scopedRecallUris.add(projectMemoryUri);
|
|
11940
|
+
passes.push(await recallSearchHits(config, ov, searchArgs(projectMemoryUri), { dryRun, includeArchived }));
|
|
11941
|
+
}
|
|
11942
|
+
}
|
|
11943
|
+
for (const scope of projectMemoryScopeUris(config, projectMemoryName, includeArchived)) {
|
|
11944
|
+
if (!scopedRecallUris.has(scope)) {
|
|
11945
|
+
scopedRecallUris.add(scope);
|
|
11946
|
+
passes.push(await recallSearchHits(config, ov, searchArgs(scope), { dryRun, includeArchived }));
|
|
11947
|
+
}
|
|
11481
11948
|
}
|
|
11482
11949
|
const seededUri = project ? trimTrailingSlash(project.uri) : void 0;
|
|
11483
11950
|
if (seededUri?.startsWith("viking://") && seededUri !== inferredUri && !options.uri && options.inferScope !== false) {
|
|
@@ -11486,7 +11953,9 @@ async function runRecall(config, options) {
|
|
|
11486
11953
|
const workset = !options.uri && explicitWorkset ? explicitWorkset : !options.uri && options.inferScope !== false ? await inferWorksetFromQuery(config.manifestPath, projectQuery) : void 0;
|
|
11487
11954
|
if (workset && workset.projects.length > 0) {
|
|
11488
11955
|
console.log(`Workset scope: ${workset.name} (${workset.projects.map((member) => member.name).join(", ")})`);
|
|
11489
|
-
const alreadyScoped = new Set(
|
|
11956
|
+
const alreadyScoped = new Set(
|
|
11957
|
+
[inferredUri, seededUri, ...scopedRecallUris].filter((uri) => uri !== void 0)
|
|
11958
|
+
);
|
|
11490
11959
|
const worksetScopes = worksetScopeUris(config, workset).filter((uri) => !alreadyScoped.has(uri)).slice(0, MAX_WORKSET_PASSES);
|
|
11491
11960
|
for (const scope of worksetScopes) {
|
|
11492
11961
|
passes.push(await recallSearchHits(config, ov, searchArgs(scope), { dryRun, includeArchived }));
|
|
@@ -12059,8 +12528,8 @@ async function removeVikingResourceWithRetry(ov, config, uri) {
|
|
|
12059
12528
|
return false;
|
|
12060
12529
|
}
|
|
12061
12530
|
async function vikingResourceExists2(ov, config, uri) {
|
|
12062
|
-
const
|
|
12063
|
-
return
|
|
12531
|
+
const stat5 = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
|
|
12532
|
+
return stat5.exitCode === 0;
|
|
12064
12533
|
}
|
|
12065
12534
|
async function ensureDurableMemoryDirectory(ov, config) {
|
|
12066
12535
|
await ensureMemoryDirectory(ov, config, durableMemoryDirectoryUri(config));
|
|
@@ -12139,6 +12608,27 @@ function worksetScopeUris(config, workset) {
|
|
|
12139
12608
|
}
|
|
12140
12609
|
return [...new Set(scopes)];
|
|
12141
12610
|
}
|
|
12611
|
+
async function recallProjectMemoryName(explicitProject, options) {
|
|
12612
|
+
return normalizeOptionalMetadata2(explicitProject) ?? await resolveWorkspaceRepoName(options);
|
|
12613
|
+
}
|
|
12614
|
+
function projectMemoryScopeUris(config, projectName, includeArchived) {
|
|
12615
|
+
if (!projectName) {
|
|
12616
|
+
return [];
|
|
12617
|
+
}
|
|
12618
|
+
const base = `viking://user/${uriSegment(config.user)}/memories`;
|
|
12619
|
+
const projectSegment = uriSegment(projectName);
|
|
12620
|
+
const scopes = [
|
|
12621
|
+
`${base}/durable/projects/${projectSegment}`,
|
|
12622
|
+
`${base}/handoffs/active/${projectSegment}`,
|
|
12623
|
+
`${base}/incidents/active/${projectSegment}`
|
|
12624
|
+
];
|
|
12625
|
+
return includeArchived ? [
|
|
12626
|
+
...scopes,
|
|
12627
|
+
`${base}/durable/archived/${projectSegment}`,
|
|
12628
|
+
`${base}/handoffs/archived/${projectSegment}`,
|
|
12629
|
+
`${base}/incidents/archived/${projectSegment}`
|
|
12630
|
+
] : scopes;
|
|
12631
|
+
}
|
|
12142
12632
|
function exactMemoryScopes(config, includeArchived, query, project) {
|
|
12143
12633
|
return exactMemoryScopeUris({
|
|
12144
12634
|
agentMemoriesUri: `viking://agent/${uriSegment(config.agentId)}/memories`,
|
|
@@ -13375,7 +13865,7 @@ async function runInitManifest(config, options) {
|
|
|
13375
13865
|
continue;
|
|
13376
13866
|
}
|
|
13377
13867
|
seen.add(identity);
|
|
13378
|
-
projects.push(projectManifestForRepo(repoRoot, projects));
|
|
13868
|
+
projects.push(await projectManifestForRepo(repoRoot, projects));
|
|
13379
13869
|
}
|
|
13380
13870
|
const outputManifest = {
|
|
13381
13871
|
version: 1,
|
|
@@ -13483,8 +13973,8 @@ async function projectIdentity(path2) {
|
|
|
13483
13973
|
return expanded;
|
|
13484
13974
|
}
|
|
13485
13975
|
}
|
|
13486
|
-
function projectManifestForRepo(repoRoot, existingProjects) {
|
|
13487
|
-
const baseName = uriSegment((0, import_node_path13.basename)(repoRoot));
|
|
13976
|
+
async function projectManifestForRepo(repoRoot, existingProjects) {
|
|
13977
|
+
const baseName = uriSegment(await resolveRepoName(repoRoot) ?? (0, import_node_path13.basename)(repoRoot));
|
|
13488
13978
|
const usedNames = new Set(existingProjects.map((project) => project.name));
|
|
13489
13979
|
const usedUris = new Set(existingProjects.map((project) => project.uri));
|
|
13490
13980
|
let name = baseName;
|
|
@@ -13686,23 +14176,6 @@ function redactJsonValue(value) {
|
|
|
13686
14176
|
function isSensitiveKey(key) {
|
|
13687
14177
|
return /token|secret|password|credential|authorization|api[_-]?key|client[_-]?secret|bearer/i.test(key);
|
|
13688
14178
|
}
|
|
13689
|
-
function detectSecretMatches(content) {
|
|
13690
|
-
const detectors = [
|
|
13691
|
-
{ label: "private-key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
13692
|
-
{ label: "openai-key", regex: /sk-[A-Za-z0-9_-]{24,}/ },
|
|
13693
|
-
{ label: "github-token", regex: /gh[pousr]_[A-Za-z0-9_]{24,}/ },
|
|
13694
|
-
{ label: "slack-token", regex: /xox[abprs]-[A-Za-z0-9-]{24,}/ },
|
|
13695
|
-
{ label: "bearer-token", regex: /Bearer\s+[A-Za-z0-9._~+/=-]{24,}/i },
|
|
13696
|
-
{ label: "aws-access-key", regex: /AKIA[0-9A-Z]{16}/ }
|
|
13697
|
-
];
|
|
13698
|
-
const matches = [];
|
|
13699
|
-
for (const detector of detectors) {
|
|
13700
|
-
if (detector.regex.test(content)) {
|
|
13701
|
-
matches.push(detector.label);
|
|
13702
|
-
}
|
|
13703
|
-
}
|
|
13704
|
-
return matches;
|
|
13705
|
-
}
|
|
13706
14179
|
|
|
13707
14180
|
// src/lifecycle.ts
|
|
13708
14181
|
var import_node_child_process4 = require("node:child_process");
|
|
@@ -13821,6 +14294,7 @@ function escapeRegExp3(value) {
|
|
|
13821
14294
|
// src/update.ts
|
|
13822
14295
|
var NPM_PACKAGE_NAME = "threadnote";
|
|
13823
14296
|
var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
14297
|
+
var ALLOW_UNTRUSTED_REGISTRY_ENV = "THREADNOTE_ALLOW_UNTRUSTED_NPM_REGISTRY";
|
|
13824
14298
|
var UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
13825
14299
|
var POST_UPDATE_MIGRATIONS_FILE = "post-update-migrations.json";
|
|
13826
14300
|
var POST_UPDATE_STATE_FILE = "post-update-state.json";
|
|
@@ -13851,7 +14325,7 @@ async function maybeNotifyUpdate(config, options = {}) {
|
|
|
13851
14325
|
}
|
|
13852
14326
|
}
|
|
13853
14327
|
async function runUpdate(config, options) {
|
|
13854
|
-
const registry =
|
|
14328
|
+
const registry = resolveUpdateRegistry(options.registry, options.allowUntrustedRegistry);
|
|
13855
14329
|
const info2 = await withSpinner(
|
|
13856
14330
|
"Checking npm for latest threadnote version",
|
|
13857
14331
|
() => getUpdateInfo(config, {
|
|
@@ -14213,6 +14687,9 @@ async function applicablePostUpdateMigrations(config, options) {
|
|
|
14213
14687
|
if (migration.requiresLegacyHandoffs === true && !await hasLegacyLifecycleHandoffCandidates(config)) {
|
|
14214
14688
|
continue;
|
|
14215
14689
|
}
|
|
14690
|
+
if (migration.requiresProjectNameConsolidation === true && !await hasProjectNameMigrationCandidates(config)) {
|
|
14691
|
+
continue;
|
|
14692
|
+
}
|
|
14216
14693
|
applicable.push(migration);
|
|
14217
14694
|
}
|
|
14218
14695
|
return applicable;
|
|
@@ -14239,6 +14716,7 @@ function parsePostUpdateMigration(value) {
|
|
|
14239
14716
|
instructions: stringArray(value, "instructions"),
|
|
14240
14717
|
introducedIn: value.introducedIn,
|
|
14241
14718
|
requiresLegacyHandoffs: value.requiresLegacyHandoffs === true,
|
|
14719
|
+
requiresProjectNameConsolidation: value.requiresProjectNameConsolidation === true,
|
|
14242
14720
|
title: value.title
|
|
14243
14721
|
};
|
|
14244
14722
|
}
|
|
@@ -14362,10 +14840,31 @@ function updatePackageCommand(runtime, registry) {
|
|
|
14362
14840
|
};
|
|
14363
14841
|
}
|
|
14364
14842
|
function normalizeRegistry(registry) {
|
|
14365
|
-
|
|
14843
|
+
const normalized = registry.endsWith("/") ? registry : `${registry}/`;
|
|
14844
|
+
const url = new URL(normalized);
|
|
14845
|
+
if (url.protocol !== "https:") {
|
|
14846
|
+
throw new Error(`npm registry must use https: ${normalized}`);
|
|
14847
|
+
}
|
|
14848
|
+
return url.toString();
|
|
14366
14849
|
}
|
|
14367
14850
|
function updateRegistry() {
|
|
14368
|
-
return
|
|
14851
|
+
return resolveUpdateRegistry(void 0, false);
|
|
14852
|
+
}
|
|
14853
|
+
function resolveUpdateRegistry(registry, allowUntrustedRegistry) {
|
|
14854
|
+
const normalized = normalizeRegistry(registry ?? process.env.THREADNOTE_NPM_REGISTRY ?? DEFAULT_NPM_REGISTRY);
|
|
14855
|
+
if (normalized !== DEFAULT_NPM_REGISTRY && !allowsUntrustedRegistry(allowUntrustedRegistry)) {
|
|
14856
|
+
throw new Error(
|
|
14857
|
+
`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.`
|
|
14858
|
+
);
|
|
14859
|
+
}
|
|
14860
|
+
return normalized;
|
|
14861
|
+
}
|
|
14862
|
+
function allowsUntrustedRegistry(option) {
|
|
14863
|
+
if (option === true) {
|
|
14864
|
+
return true;
|
|
14865
|
+
}
|
|
14866
|
+
const envValue = process.env[ALLOW_UNTRUSTED_REGISTRY_ENV]?.trim().toLowerCase();
|
|
14867
|
+
return envValue === "1" || envValue === "true" || envValue === "yes";
|
|
14369
14868
|
}
|
|
14370
14869
|
function isUpdateNotificationDisabled() {
|
|
14371
14870
|
return process.env.CI !== void 0 || process.env.NO_UPDATE_NOTIFIER !== void 0 || process.env.THREADNOTE_NO_UPDATE_CHECK !== void 0;
|
|
@@ -14567,7 +15066,7 @@ async function repairManifest(config, dryRun) {
|
|
|
14567
15066
|
console.log(`WARN cannot create replacement manifest from current directory: ${errorMessage(err)}`);
|
|
14568
15067
|
return;
|
|
14569
15068
|
}
|
|
14570
|
-
const project = projectManifestForRepo(repoRoot, []);
|
|
15069
|
+
const project = await projectManifestForRepo(repoRoot, []);
|
|
14571
15070
|
const output2 = index_vite_proxy_tmp_default.dump(
|
|
14572
15071
|
{
|
|
14573
15072
|
version: 1,
|
|
@@ -15875,7 +16374,7 @@ function parsePackageManager(value) {
|
|
|
15875
16374
|
// src/version_command.ts
|
|
15876
16375
|
async function runVersion(config, options) {
|
|
15877
16376
|
const currentVersion = await currentPackageVersion();
|
|
15878
|
-
const registry =
|
|
16377
|
+
const registry = resolveUpdateRegistry(options.registry, options.allowUntrustedRegistry);
|
|
15879
16378
|
let latestVersion;
|
|
15880
16379
|
let latestWarning;
|
|
15881
16380
|
try {
|
|
@@ -15988,7 +16487,11 @@ async function readManagedMemory(config, uri) {
|
|
|
15988
16487
|
if (!path2) {
|
|
15989
16488
|
throw new Error(`Manager can only read current-user memory URIs: ${uri}`);
|
|
15990
16489
|
}
|
|
15991
|
-
const
|
|
16490
|
+
const pathStat = await (0, import_promises16.lstat)(path2);
|
|
16491
|
+
if (!pathStat.isFile()) {
|
|
16492
|
+
throw new Error(`Manager can only read regular memory files: ${uri}`);
|
|
16493
|
+
}
|
|
16494
|
+
const content = await (0, import_promises16.readFile)(path2, "utf8");
|
|
15992
16495
|
const relativePath = (0, import_node_path16.relative)(localMemoriesRoot(config), path2).split(import_node_path16.sep).join("/");
|
|
15993
16496
|
const record = parseMemoryDocument(uri, content);
|
|
15994
16497
|
return {
|
|
@@ -16296,10 +16799,13 @@ async function serveStatic(context, url, response) {
|
|
|
16296
16799
|
response.end(content);
|
|
16297
16800
|
}
|
|
16298
16801
|
async function readTree(config, path2, uri, relativePath, options = {}) {
|
|
16299
|
-
const pathStat = await (0, import_promises16.
|
|
16802
|
+
const pathStat = await (0, import_promises16.lstat)(path2);
|
|
16300
16803
|
const name = relativePath ? relativePath.split("/").at(-1) ?? relativePath : options.rootName ?? "memories";
|
|
16301
16804
|
const isDir = pathStat.isDirectory();
|
|
16302
16805
|
if (!isDir) {
|
|
16806
|
+
if (!pathStat.isFile()) {
|
|
16807
|
+
throw new Error(`Manager can only read regular files or directories: ${uri}`);
|
|
16808
|
+
}
|
|
16303
16809
|
const record = options.parseMemoryDocuments === false ? void 0 : parseMemoryDocument(uri, await (0, import_promises16.readFile)(path2, "utf8").catch(() => ""));
|
|
16304
16810
|
return {
|
|
16305
16811
|
isDir: false,
|
|
@@ -16316,7 +16822,7 @@ async function readTree(config, path2, uri, relativePath, options = {}) {
|
|
|
16316
16822
|
}
|
|
16317
16823
|
const entries = await (0, import_promises16.readdir)(path2, { withFileTypes: true });
|
|
16318
16824
|
const children = await Promise.all(
|
|
16319
|
-
entries.sort(
|
|
16825
|
+
entries.filter((entry) => entry.isDirectory() || entry.isFile()).sort(
|
|
16320
16826
|
(left, right) => Number(right.isDirectory()) - Number(left.isDirectory()) || left.name.localeCompare(right.name)
|
|
16321
16827
|
).map((entry) => {
|
|
16322
16828
|
const childRelative = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
@@ -16494,7 +17000,7 @@ async function removeManagedFolder(config, uri) {
|
|
|
16494
17000
|
if (!path2) {
|
|
16495
17001
|
throw new Error(`Manager can only remove current-user memory folders: ${uri}`);
|
|
16496
17002
|
}
|
|
16497
|
-
const pathStat = await (0, import_promises16.
|
|
17003
|
+
const pathStat = await (0, import_promises16.lstat)(path2);
|
|
16498
17004
|
if (!pathStat.isDirectory()) {
|
|
16499
17005
|
throw new Error(`Not a folder: ${uri}`);
|
|
16500
17006
|
}
|
|
@@ -16956,10 +17462,10 @@ async function main() {
|
|
|
16956
17462
|
}
|
|
16957
17463
|
await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
|
|
16958
17464
|
});
|
|
16959
|
-
program2.command("version").description("Print the installed Threadnote version, latest npm version, and release notes").option("--registry <url>", "npm registry URL",
|
|
17465
|
+
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) => {
|
|
16960
17466
|
await runVersion(getRuntimeConfig(program2), options);
|
|
16961
17467
|
});
|
|
16962
|
-
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",
|
|
17468
|
+
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) => {
|
|
16963
17469
|
await runUpdate(getRuntimeConfig(program2), options);
|
|
16964
17470
|
});
|
|
16965
17471
|
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) => {
|
|
@@ -17038,6 +17544,9 @@ async function main() {
|
|
|
17038
17544
|
program2.command("migrate-lifecycle").description("Move clear legacy handoff memories into lifecycle-aware archive paths").option("--apply", "Perform the migration; without this, prints a dry run").option("--dry-run", "Print migration actions without writing or removing memories").option("--limit <count>", "Maximum number of legacy handoffs to migrate").action(async (options) => {
|
|
17039
17545
|
await runMigrateLifecycle(getRuntimeConfig(program2), options);
|
|
17040
17546
|
});
|
|
17547
|
+
program2.command("migrate-project-names").description("Move memories from old clone-folder project names to the git remote repo name").option("--apply", "Perform the migration; without this, prints a dry run").option("--dry-run", "Print migration actions without writing or removing memories").option("--limit <count>", "Maximum number of memories to migrate").action(async (options) => {
|
|
17548
|
+
await runMigrateProjectNames(getRuntimeConfig(program2), options);
|
|
17549
|
+
});
|
|
17041
17550
|
program2.command("recall").description("Search shared OpenViking context").requiredOption("--query <query>", "Search query").option("--dry-run", "Print ov command without searching").option("--include-archived", "Include archived memories in recall results").option("-n, --node-limit <count>", "Maximum number of search results").option("--no-infer-scope", "Disable query-based scope inference").option("--project <name>", "Prioritize a project: add a scoped pass over its memories alongside the global search").option("--threshold <score>", "Minimum relevance score 0-1 (default 0.45); lower to broaden when recall is empty").option("--uri <uri>", "Restrict search to a viking:// URI").option(
|
|
17042
17551
|
"--workset <name>",
|
|
17043
17552
|
"Recall across a named seed-manifest workset (a set of related repos) as one working set"
|