threadnote 1.6.2 → 1.7.1
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 +21 -0
- package/dist/mcp_server.cjs +126 -2
- package/dist/threadnote.cjs +572 -10
- package/docs/index.html +1 -1
- package/package.json +1 -1
|
@@ -18,6 +18,27 @@
|
|
|
18
18
|
"If Threadnote reported any original files were still being processed, rerun the printed threadnote forget <uri> command later."
|
|
19
19
|
],
|
|
20
20
|
"requiresLegacyHandoffs": true
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "remote-project-name-consolidation-v1",
|
|
24
|
+
"introducedIn": "1.6.3",
|
|
25
|
+
"title": "Consolidate memories under the git remote project name",
|
|
26
|
+
"description": [
|
|
27
|
+
"Threadnote now uses the git remote repository name as the default project identifier instead of the clone folder name.",
|
|
28
|
+
"This migration scans personal lifecycle-aware memory project folders and uses repo_path evidence in memories, plus matching seed manifest paths when present, to resolve git remote names.",
|
|
29
|
+
"It covers every detected personal memory project, not just the workspace where threadnote update was run.",
|
|
30
|
+
"It also renames matching seed manifest projects and workset references so future seeded guidance uses the remote-derived project name.",
|
|
31
|
+
"Matching memories are copied under the remote-derived project name with updated project metadata, then the old copy is removed.",
|
|
32
|
+
"If a destination file already exists with different content, Threadnote keeps both by writing the migrated copy with a -from-<old-project> suffix.",
|
|
33
|
+
"Shared team memories and projects without repo path evidence are left untouched."
|
|
34
|
+
],
|
|
35
|
+
"commandArgs": ["migrate-project-names", "--apply"],
|
|
36
|
+
"instructions": [
|
|
37
|
+
"Post-update project-name migration finished. Future default handoffs and memories for migrated repos will use the git remote repository name as their project.",
|
|
38
|
+
"If the migration updated your seed manifest, run threadnote seed --only <new-project-name> to re-ingest seeded resources under the new project URI.",
|
|
39
|
+
"If Threadnote reported any original files were still being processed, rerun the printed threadnote forget <uri> command later."
|
|
40
|
+
],
|
|
41
|
+
"requiresProjectNameConsolidation": true
|
|
21
42
|
}
|
|
22
43
|
]
|
|
23
44
|
}
|
package/dist/mcp_server.cjs
CHANGED
|
@@ -35592,6 +35592,23 @@ function parseJsonConfigObject(content) {
|
|
|
35592
35592
|
function redactText(content) {
|
|
35593
35593
|
return redactSensitiveText(content);
|
|
35594
35594
|
}
|
|
35595
|
+
var GIT_ENVIRONMENT_KEYS = [
|
|
35596
|
+
"GIT_DIR",
|
|
35597
|
+
"GIT_WORK_TREE",
|
|
35598
|
+
"GIT_INDEX_FILE",
|
|
35599
|
+
"GIT_PREFIX",
|
|
35600
|
+
"GIT_COMMON_DIR",
|
|
35601
|
+
"GIT_OBJECT_DIRECTORY",
|
|
35602
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
35603
|
+
"GIT_QUARANTINE_PATH"
|
|
35604
|
+
];
|
|
35605
|
+
function withoutGitEnvironment(env = process.env) {
|
|
35606
|
+
const next = { ...env };
|
|
35607
|
+
for (const key of GIT_ENVIRONMENT_KEYS) {
|
|
35608
|
+
delete next[key];
|
|
35609
|
+
}
|
|
35610
|
+
return next;
|
|
35611
|
+
}
|
|
35595
35612
|
function escapeRegExp(value) {
|
|
35596
35613
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
35597
35614
|
}
|
|
@@ -35711,6 +35728,7 @@ async function runCommand(executable, args, options = {}) {
|
|
|
35711
35728
|
{
|
|
35712
35729
|
cwd: options.cwd,
|
|
35713
35730
|
encoding: "utf8",
|
|
35731
|
+
env: commandEnvironment(executable, options.env),
|
|
35714
35732
|
maxBuffer: maxOutputBytes
|
|
35715
35733
|
},
|
|
35716
35734
|
(err, stdout2, stderr) => {
|
|
@@ -35750,6 +35768,12 @@ async function runCommand(executable, args, options = {}) {
|
|
|
35750
35768
|
}
|
|
35751
35769
|
});
|
|
35752
35770
|
}
|
|
35771
|
+
function commandEnvironment(executable, env) {
|
|
35772
|
+
if ((0, import_node_path.basename)(executable) !== "git") {
|
|
35773
|
+
return env;
|
|
35774
|
+
}
|
|
35775
|
+
return withoutGitEnvironment(env ?? process.env);
|
|
35776
|
+
}
|
|
35753
35777
|
function commandResultFromExecFileCallback(params) {
|
|
35754
35778
|
if (params.failureMessage) {
|
|
35755
35779
|
return { exitCode: 124, stderr: params.failureMessage, stdout: params.stdout };
|
|
@@ -35786,6 +35810,64 @@ async function gitValue(args, cwd = getInvocationCwd()) {
|
|
|
35786
35810
|
}
|
|
35787
35811
|
return result.stdout.trim();
|
|
35788
35812
|
}
|
|
35813
|
+
async function resolveRepoName(cwd = getInvocationCwd()) {
|
|
35814
|
+
const repoRoot = await gitValue(["rev-parse", "--show-toplevel"], cwd);
|
|
35815
|
+
if (!repoRoot) {
|
|
35816
|
+
return void 0;
|
|
35817
|
+
}
|
|
35818
|
+
const remoteName = await resolveGitRemoteRepoName(repoRoot);
|
|
35819
|
+
if (remoteName) {
|
|
35820
|
+
return remoteName;
|
|
35821
|
+
}
|
|
35822
|
+
return resolveRepoFolderName(repoRoot);
|
|
35823
|
+
}
|
|
35824
|
+
async function resolveRepoFolderName(cwd = getInvocationCwd()) {
|
|
35825
|
+
const repoRoot = await gitValue(["rev-parse", "--show-toplevel"], cwd);
|
|
35826
|
+
if (!repoRoot) {
|
|
35827
|
+
return void 0;
|
|
35828
|
+
}
|
|
35829
|
+
const commonDir = await gitValue(["rev-parse", "--git-common-dir"], repoRoot);
|
|
35830
|
+
if (commonDir) {
|
|
35831
|
+
const absoluteCommonDir = (0, import_node_path.isAbsolute)(commonDir) ? commonDir : (0, import_node_path.resolve)(repoRoot, commonDir);
|
|
35832
|
+
const primaryRoot = (0, import_node_path.basename)(absoluteCommonDir) === ".git" ? (0, import_node_path.dirname)(absoluteCommonDir) : absoluteCommonDir;
|
|
35833
|
+
const name = (0, import_node_path.basename)(primaryRoot).replace(/\.git$/, "");
|
|
35834
|
+
if (name && name !== ".") {
|
|
35835
|
+
return name;
|
|
35836
|
+
}
|
|
35837
|
+
}
|
|
35838
|
+
return (0, import_node_path.basename)(repoRoot);
|
|
35839
|
+
}
|
|
35840
|
+
async function resolveGitRemoteRepoName(repoRoot) {
|
|
35841
|
+
const originUrl = await gitValue(["remote", "get-url", "origin"], repoRoot);
|
|
35842
|
+
const originName = originUrl ? gitRemoteRepoName(originUrl) : void 0;
|
|
35843
|
+
if (originName) {
|
|
35844
|
+
return originName;
|
|
35845
|
+
}
|
|
35846
|
+
const remotes = await gitValue(["remote"], repoRoot);
|
|
35847
|
+
const remote = remotes?.split(/\r?\n/).map((name) => name.trim()).find((name) => name.length > 0);
|
|
35848
|
+
if (!remote) {
|
|
35849
|
+
return void 0;
|
|
35850
|
+
}
|
|
35851
|
+
const remoteUrl = await gitValue(["remote", "get-url", remote], repoRoot);
|
|
35852
|
+
return remoteUrl ? gitRemoteRepoName(remoteUrl) : void 0;
|
|
35853
|
+
}
|
|
35854
|
+
function gitRemoteRepoName(remoteUrl) {
|
|
35855
|
+
const trimmed = remoteUrl.trim();
|
|
35856
|
+
if (!trimmed) {
|
|
35857
|
+
return void 0;
|
|
35858
|
+
}
|
|
35859
|
+
let remotePath = trimmed.replace(/[?#].*$/, "");
|
|
35860
|
+
try {
|
|
35861
|
+
remotePath = new URL(trimmed).pathname;
|
|
35862
|
+
} catch (_err) {
|
|
35863
|
+
const scpLike = trimmed.match(/^[^@\s/]+@[^:\s]+:(.+)$/);
|
|
35864
|
+
if (scpLike?.[1]) {
|
|
35865
|
+
remotePath = scpLike[1];
|
|
35866
|
+
}
|
|
35867
|
+
}
|
|
35868
|
+
const name = remotePath.replace(/[\\/]+$/, "").split(/[\\/:]/).filter(Boolean).pop()?.replace(/\.git$/i, "");
|
|
35869
|
+
return name && name !== "." && name !== ".." ? name : void 0;
|
|
35870
|
+
}
|
|
35789
35871
|
async function sleep(ms) {
|
|
35790
35872
|
return new Promise((resolvePromise) => {
|
|
35791
35873
|
setTimeout(resolvePromise, ms);
|
|
@@ -35926,6 +36008,13 @@ async function enrichRecallQueryWithWorkspaceContext(query, options = {}) {
|
|
|
35926
36008
|
async function enrichRecallQueryWithWorkspaceProjectContext(query, options = {}) {
|
|
35927
36009
|
return enrichRecallQueryWithWorkspaceTerms(query, options, false);
|
|
35928
36010
|
}
|
|
36011
|
+
async function resolveWorkspaceRepoName(options = {}) {
|
|
36012
|
+
const cwd = options.cwd ?? (options.includeProcessCwd === false ? void 0 : getInvocationCwd());
|
|
36013
|
+
if (!cwd || !(0, import_node_path.isAbsolute)(cwd)) {
|
|
36014
|
+
return void 0;
|
|
36015
|
+
}
|
|
36016
|
+
return resolveRepoName(cwd);
|
|
36017
|
+
}
|
|
35929
36018
|
async function enrichRecallQueryWithWorkspaceTerms(query, options, includeBranch) {
|
|
35930
36019
|
if (!recallQueryRequestsWorkspaceContext(query)) {
|
|
35931
36020
|
return query;
|
|
@@ -35944,10 +36033,11 @@ async function currentWorkspaceRecallTerms(options, includeBranch) {
|
|
|
35944
36033
|
return [];
|
|
35945
36034
|
}
|
|
35946
36035
|
const branch = await gitValue(["branch", "--show-current"], repoRoot);
|
|
36036
|
+
const repoName = await resolveWorkspaceRepoName({ cwd, includeProcessCwd: false });
|
|
35947
36037
|
const parent = (0, import_node_path.dirname)(repoRoot);
|
|
35948
36038
|
return uniqueUsefulWorkspaceTerms([
|
|
35949
36039
|
{ source: "branch", value: includeBranch ? branch : void 0 },
|
|
35950
|
-
{ source: "path", value:
|
|
36040
|
+
{ source: "path", value: repoName },
|
|
35951
36041
|
{ source: "path", value: parent === (0, import_node_os.homedir)() ? void 0 : (0, import_node_path.basename)(parent) }
|
|
35952
36042
|
]);
|
|
35953
36043
|
}
|
|
@@ -40375,6 +40465,7 @@ async function runRecallTool(config2, params) {
|
|
|
40375
40465
|
indexRepairMessages = [`Auto-index repair warning: ${errorMessage(err)}`];
|
|
40376
40466
|
}
|
|
40377
40467
|
const project = params.pinnedUri ? void 0 : await inferProjectFromQuery(config2.manifestPath, projectQuery);
|
|
40468
|
+
const projectMemoryName = params.pinnedUri ? void 0 : await resolveWorkspaceRepoName({ cwd: params.callerCwd, includeProcessCwd: false });
|
|
40378
40469
|
const limitArgs = params.nodeLimit ? ["--node-limit", String(params.nodeLimit)] : [];
|
|
40379
40470
|
const threshold = params.threshold ?? RECALL_SCORE_THRESHOLD;
|
|
40380
40471
|
const explicitWorkset = params.workset ? await requireWorkset(config2.manifestPath, params.workset) : void 0;
|
|
@@ -40386,6 +40477,19 @@ async function runRecallTool(config2, params) {
|
|
|
40386
40477
|
params.includeArchived
|
|
40387
40478
|
);
|
|
40388
40479
|
const passes = [base.hits];
|
|
40480
|
+
const scopedRecallUris = new Set([params.pinnedUri].filter((uri) => uri !== void 0));
|
|
40481
|
+
for (const scope of projectMemoryScopeUris(config2, projectMemoryName, params.includeArchived)) {
|
|
40482
|
+
if (!scopedRecallUris.has(scope)) {
|
|
40483
|
+
scopedRecallUris.add(scope);
|
|
40484
|
+
const projectMemoryPass = await recallSearchHits(
|
|
40485
|
+
config2,
|
|
40486
|
+
["search", query, "--uri", scope, ...limitArgs],
|
|
40487
|
+
threshold,
|
|
40488
|
+
params.includeArchived
|
|
40489
|
+
);
|
|
40490
|
+
passes.push(projectMemoryPass.hits);
|
|
40491
|
+
}
|
|
40492
|
+
}
|
|
40389
40493
|
const seededUri = project ? trimTrailingSlash(project.uri) : void 0;
|
|
40390
40494
|
if (seededUri?.startsWith("viking://") && seededUri !== params.pinnedUri) {
|
|
40391
40495
|
const seeded = await recallSearchHits(
|
|
@@ -40400,7 +40504,9 @@ async function runRecallTool(config2, params) {
|
|
|
40400
40504
|
const workset = params.pinnedUri ? void 0 : explicitWorkset ? explicitWorkset : await inferWorksetFromQuery(config2.manifestPath, projectQuery);
|
|
40401
40505
|
if (workset && workset.projects.length > 0) {
|
|
40402
40506
|
sections.push(`Workset scope: ${workset.name} (${workset.projects.map((member) => member.name).join(", ")})`);
|
|
40403
|
-
const alreadyScoped = new Set(
|
|
40507
|
+
const alreadyScoped = new Set(
|
|
40508
|
+
[params.pinnedUri, seededUri, ...scopedRecallUris].filter((uri) => uri !== void 0)
|
|
40509
|
+
);
|
|
40404
40510
|
const worksetScopes = worksetScopeUris(config2, workset).filter((uri) => !alreadyScoped.has(uri)).slice(0, MAX_WORKSET_PASSES);
|
|
40405
40511
|
for (const scope of worksetScopes) {
|
|
40406
40512
|
const worksetPass = await recallSearchHits(
|
|
@@ -41087,6 +41193,24 @@ function worksetScopeUris(config2, workset) {
|
|
|
41087
41193
|
}
|
|
41088
41194
|
return [...new Set(scopes)];
|
|
41089
41195
|
}
|
|
41196
|
+
function projectMemoryScopeUris(config2, projectName, includeArchived) {
|
|
41197
|
+
if (!projectName) {
|
|
41198
|
+
return [];
|
|
41199
|
+
}
|
|
41200
|
+
const base = `viking://user/${uriSegment2(config2.user)}/memories`;
|
|
41201
|
+
const projectSegment = uriSegment2(projectName);
|
|
41202
|
+
const scopes = [
|
|
41203
|
+
`${base}/durable/projects/${projectSegment}`,
|
|
41204
|
+
`${base}/handoffs/active/${projectSegment}`,
|
|
41205
|
+
`${base}/incidents/active/${projectSegment}`
|
|
41206
|
+
];
|
|
41207
|
+
return includeArchived ? [
|
|
41208
|
+
...scopes,
|
|
41209
|
+
`${base}/durable/archived/${projectSegment}`,
|
|
41210
|
+
`${base}/handoffs/archived/${projectSegment}`,
|
|
41211
|
+
`${base}/incidents/archived/${projectSegment}`
|
|
41212
|
+
] : scopes;
|
|
41213
|
+
}
|
|
41090
41214
|
function formatMemoryDocument2(title, metadata, body) {
|
|
41091
41215
|
const header = [
|
|
41092
41216
|
title,
|
package/dist/threadnote.cjs
CHANGED
|
@@ -3676,6 +3676,23 @@ function parseJsonConfigObject(content) {
|
|
|
3676
3676
|
function redactText(content) {
|
|
3677
3677
|
return redactSensitiveText(content);
|
|
3678
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;
|
|
3695
|
+
}
|
|
3679
3696
|
async function walkFiles(root) {
|
|
3680
3697
|
const files = [];
|
|
3681
3698
|
async function visit(path2) {
|
|
@@ -3863,6 +3880,7 @@ async function runCommand(executable, args, options = {}) {
|
|
|
3863
3880
|
{
|
|
3864
3881
|
cwd: options.cwd,
|
|
3865
3882
|
encoding: "utf8",
|
|
3883
|
+
env: commandEnvironment(executable, options.env),
|
|
3866
3884
|
maxBuffer: maxOutputBytes
|
|
3867
3885
|
},
|
|
3868
3886
|
(err, stdout2, stderr) => {
|
|
@@ -3902,6 +3920,12 @@ async function runCommand(executable, args, options = {}) {
|
|
|
3902
3920
|
}
|
|
3903
3921
|
});
|
|
3904
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
|
+
}
|
|
3905
3929
|
function commandResultFromExecFileCallback(params) {
|
|
3906
3930
|
if (params.failureMessage) {
|
|
3907
3931
|
return { exitCode: 124, stderr: params.failureMessage, stdout: params.stdout };
|
|
@@ -3939,6 +3963,17 @@ async function gitValue(args, cwd = getInvocationCwd()) {
|
|
|
3939
3963
|
return result.stdout.trim();
|
|
3940
3964
|
}
|
|
3941
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()) {
|
|
3942
3977
|
const repoRoot = await gitValue(["rev-parse", "--show-toplevel"], cwd);
|
|
3943
3978
|
if (!repoRoot) {
|
|
3944
3979
|
return void 0;
|
|
@@ -3954,6 +3989,37 @@ async function resolveRepoName(cwd = getInvocationCwd()) {
|
|
|
3954
3989
|
}
|
|
3955
3990
|
return (0, import_node_path2.basename)(repoRoot);
|
|
3956
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
|
+
}
|
|
3957
4023
|
async function runInteractive(executable, args) {
|
|
3958
4024
|
return new Promise((resolvePromise) => {
|
|
3959
4025
|
const child = (0, import_node_child_process2.spawn)(executable, args, { stdio: "inherit" });
|
|
@@ -4223,6 +4289,13 @@ async function enrichRecallQueryWithWorkspaceContext(query, options = {}) {
|
|
|
4223
4289
|
async function enrichRecallQueryWithWorkspaceProjectContext(query, options = {}) {
|
|
4224
4290
|
return enrichRecallQueryWithWorkspaceTerms(query, options, false);
|
|
4225
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
|
+
}
|
|
4226
4299
|
async function enrichRecallQueryWithWorkspaceTerms(query, options, includeBranch) {
|
|
4227
4300
|
if (!recallQueryRequestsWorkspaceContext(query)) {
|
|
4228
4301
|
return query;
|
|
@@ -4241,10 +4314,11 @@ async function currentWorkspaceRecallTerms(options, includeBranch) {
|
|
|
4241
4314
|
return [];
|
|
4242
4315
|
}
|
|
4243
4316
|
const branch = await gitValue(["branch", "--show-current"], repoRoot);
|
|
4317
|
+
const repoName = await resolveWorkspaceRepoName({ cwd, includeProcessCwd: false });
|
|
4244
4318
|
const parent = (0, import_node_path2.dirname)(repoRoot);
|
|
4245
4319
|
return uniqueUsefulWorkspaceTerms([
|
|
4246
4320
|
{ source: "branch", value: includeBranch ? branch : void 0 },
|
|
4247
|
-
{ source: "path", value:
|
|
4321
|
+
{ source: "path", value: repoName },
|
|
4248
4322
|
{ source: "path", value: parent === (0, import_node_os.homedir)() ? void 0 : (0, import_node_path2.basename)(parent) }
|
|
4249
4323
|
]);
|
|
4250
4324
|
}
|
|
@@ -5251,9 +5325,6 @@ function existsSyncDirectory(path2) {
|
|
|
5251
5325
|
var import_promises6 = require("node:fs/promises");
|
|
5252
5326
|
var import_node_path8 = require("node:path");
|
|
5253
5327
|
|
|
5254
|
-
// src/manifest.ts
|
|
5255
|
-
var import_promises3 = require("node:fs/promises");
|
|
5256
|
-
|
|
5257
5328
|
// node_modules/js-yaml/dist/js-yaml.mjs
|
|
5258
5329
|
var __create2 = Object.create;
|
|
5259
5330
|
var __defProp2 = Object.defineProperty;
|
|
@@ -7617,6 +7688,7 @@ var { Type, Schema, FAILSAFE_SCHEMA, JSON_SCHEMA, CORE_SCHEMA, DEFAULT_SCHEMA, l
|
|
|
7617
7688
|
var index_vite_proxy_tmp_default = import_js_yaml.default;
|
|
7618
7689
|
|
|
7619
7690
|
// src/manifest.ts
|
|
7691
|
+
var import_promises3 = require("node:fs/promises");
|
|
7620
7692
|
function uriSegment(value) {
|
|
7621
7693
|
const normalized = value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
7622
7694
|
return normalized.length > 0 ? normalized : "unknown";
|
|
@@ -11558,6 +11630,453 @@ async function runMigrateLifecycle(config, options) {
|
|
|
11558
11630
|
].filter((part) => part !== void 0).join("; ")
|
|
11559
11631
|
);
|
|
11560
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 contexts = await projectNameMigrationContexts(config);
|
|
11637
|
+
if (contexts.length === 0) {
|
|
11638
|
+
console.log("No git remote project-name changes apply across configured projects.");
|
|
11639
|
+
return;
|
|
11640
|
+
}
|
|
11641
|
+
const plans = [];
|
|
11642
|
+
let remaining = limit;
|
|
11643
|
+
for (const context of contexts) {
|
|
11644
|
+
const candidates2 = remaining === 0 ? [] : await projectNameMigrationCandidates(config, context, remaining);
|
|
11645
|
+
plans.push({ candidates: candidates2, context });
|
|
11646
|
+
if (remaining !== void 0) {
|
|
11647
|
+
remaining = Math.max(0, remaining - candidates2.length);
|
|
11648
|
+
}
|
|
11649
|
+
}
|
|
11650
|
+
const seedManifestMigration = await seedManifestProjectNameMigration(config, contexts);
|
|
11651
|
+
if (!plans.some((plan) => plan.candidates.length > 0) && !seedManifestMigration) {
|
|
11652
|
+
console.log("No project-name migration candidates found across configured projects.");
|
|
11653
|
+
return;
|
|
11654
|
+
}
|
|
11655
|
+
const seedManifestUpdated = await migrateSeedManifestProjectNames(config, seedManifestMigration, dryRun);
|
|
11656
|
+
let existingCount = 0;
|
|
11657
|
+
let migratedCount = 0;
|
|
11658
|
+
let skippedCount = 0;
|
|
11659
|
+
const candidates = plans.flatMap((plan) => [...plan.candidates]);
|
|
11660
|
+
if (candidates.length > 0) {
|
|
11661
|
+
const ov = await openVikingCliForMode(dryRun);
|
|
11662
|
+
for (const candidate of candidates) {
|
|
11663
|
+
const action = candidate.destinationExistsWithSameContent ? dryRun ? "Would consolidate duplicate" : "Consolidating duplicate" : dryRun ? "Would migrate" : "Migrating";
|
|
11664
|
+
console.log(`${action} ${candidate.sourceUri} -> ${candidate.destinationUri}`);
|
|
11665
|
+
if (!dryRun) {
|
|
11666
|
+
if (candidate.destinationExistsWithSameContent) {
|
|
11667
|
+
existingCount += 1;
|
|
11668
|
+
} else {
|
|
11669
|
+
await ensureMemoryDirectory(ov, config, parentVikingUri(candidate.destinationUri));
|
|
11670
|
+
await writeMemoryFile(config, ov, candidate.destinationUri, candidate.destinationContent, "create", false);
|
|
11671
|
+
}
|
|
11672
|
+
const removedOriginal = await removeVikingResourceWithRetry(ov, config, candidate.sourceUri);
|
|
11673
|
+
if (!removedOriginal) {
|
|
11674
|
+
console.error(
|
|
11675
|
+
`Migrated copy stored, but original is still processing. Retry later: threadnote forget ${candidate.sourceUri}`
|
|
11676
|
+
);
|
|
11677
|
+
skippedCount += 1;
|
|
11678
|
+
}
|
|
11679
|
+
}
|
|
11680
|
+
migratedCount += 1;
|
|
11681
|
+
}
|
|
11682
|
+
}
|
|
11683
|
+
const activeContexts = projectNameMigrationActiveContexts(plans, seedManifestMigration);
|
|
11684
|
+
const newProjectsToSeed = [...new Set(seedManifestMigration?.newProjects ?? [])];
|
|
11685
|
+
console.log(
|
|
11686
|
+
[
|
|
11687
|
+
projectNameMigrationSummary(migratedCount, dryRun, activeContexts),
|
|
11688
|
+
seedManifestUpdated ? `seed manifest ${dryRun ? "would be updated" : "updated"}` : "seed manifest unchanged",
|
|
11689
|
+
`${existingCount} duplicate destination(s) reused`,
|
|
11690
|
+
`${skippedCount} source(s) still processing`,
|
|
11691
|
+
dryRun ? "Run with --apply to perform this migration." : void 0,
|
|
11692
|
+
...newProjectsToSeed.map(
|
|
11693
|
+
(project) => `Run threadnote seed --only ${project} to re-ingest seeded resources under the new project URI.`
|
|
11694
|
+
)
|
|
11695
|
+
].filter((part) => part !== void 0).join("; ")
|
|
11696
|
+
);
|
|
11697
|
+
}
|
|
11698
|
+
async function hasProjectNameMigrationCandidates(config) {
|
|
11699
|
+
const contexts = await projectNameMigrationContexts(config);
|
|
11700
|
+
if (contexts.length === 0) {
|
|
11701
|
+
return false;
|
|
11702
|
+
}
|
|
11703
|
+
for (const context of contexts) {
|
|
11704
|
+
if ((await projectNameMigrationCandidates(config, context, 1)).length > 0) {
|
|
11705
|
+
return true;
|
|
11706
|
+
}
|
|
11707
|
+
}
|
|
11708
|
+
return await seedManifestProjectNameMigration(config, contexts) !== void 0;
|
|
11709
|
+
}
|
|
11710
|
+
async function projectNameMigrationContexts(config) {
|
|
11711
|
+
const evidence = await projectNameMigrationMemoryEvidence(config);
|
|
11712
|
+
const contexts = [];
|
|
11713
|
+
let manifest;
|
|
11714
|
+
try {
|
|
11715
|
+
manifest = await readSeedManifest(config.manifestPath);
|
|
11716
|
+
} catch (_err) {
|
|
11717
|
+
manifest = void 0;
|
|
11718
|
+
}
|
|
11719
|
+
if (manifest) {
|
|
11720
|
+
for (const project of manifest.projects) {
|
|
11721
|
+
const projectEvidence = evidence.get(uriSegment(project.name));
|
|
11722
|
+
if (projectEvidence) {
|
|
11723
|
+
projectEvidence.repoPaths.add(expandPath(project.path));
|
|
11724
|
+
}
|
|
11725
|
+
}
|
|
11726
|
+
}
|
|
11727
|
+
for (const projectEvidence of evidence.values()) {
|
|
11728
|
+
for (const repoPath of projectEvidence.repoPaths) {
|
|
11729
|
+
const context = await projectNameMigrationContextForRepoPath(projectEvidence.oldProject, repoPath);
|
|
11730
|
+
if (context) {
|
|
11731
|
+
contexts.push(context);
|
|
11732
|
+
}
|
|
11733
|
+
}
|
|
11734
|
+
}
|
|
11735
|
+
const currentContext = await currentWorkspaceProjectNameMigrationContext(evidence);
|
|
11736
|
+
if (currentContext) {
|
|
11737
|
+
contexts.push(currentContext);
|
|
11738
|
+
}
|
|
11739
|
+
return dedupeProjectNameMigrationContexts(contexts);
|
|
11740
|
+
}
|
|
11741
|
+
async function projectNameMigrationMemoryEvidence(config) {
|
|
11742
|
+
const evidence = /* @__PURE__ */ new Map();
|
|
11743
|
+
for (const location of projectMemoryLocations()) {
|
|
11744
|
+
const locationRoot = (0, import_node_path8.join)(localUserMemoriesRoot(config), ...location.relativePath);
|
|
11745
|
+
let projectEntries;
|
|
11746
|
+
try {
|
|
11747
|
+
projectEntries = await (0, import_promises6.readdir)(locationRoot, { withFileTypes: true });
|
|
11748
|
+
} catch (_err) {
|
|
11749
|
+
continue;
|
|
11750
|
+
}
|
|
11751
|
+
for (const projectEntry of projectEntries) {
|
|
11752
|
+
if (!projectEntry.isDirectory() || projectEntry.name.startsWith(".")) {
|
|
11753
|
+
continue;
|
|
11754
|
+
}
|
|
11755
|
+
const oldSegment = projectEntry.name;
|
|
11756
|
+
const projectEvidence = ensureProjectNameMigrationEvidence(evidence, oldSegment);
|
|
11757
|
+
const projectDirectory = (0, import_node_path8.join)(locationRoot, projectEntry.name);
|
|
11758
|
+
let memoryEntries;
|
|
11759
|
+
try {
|
|
11760
|
+
memoryEntries = await (0, import_promises6.readdir)(projectDirectory, { withFileTypes: true });
|
|
11761
|
+
} catch (_err) {
|
|
11762
|
+
continue;
|
|
11763
|
+
}
|
|
11764
|
+
for (const memoryEntry of memoryEntries) {
|
|
11765
|
+
if (!memoryEntry.isFile() || memoryEntry.name.startsWith(".") || !memoryEntry.name.endsWith(".md")) {
|
|
11766
|
+
continue;
|
|
11767
|
+
}
|
|
11768
|
+
const content = await readTextIfExists((0, import_node_path8.join)(projectDirectory, memoryEntry.name));
|
|
11769
|
+
if (!content) {
|
|
11770
|
+
continue;
|
|
11771
|
+
}
|
|
11772
|
+
const sourceUri = `viking://user/${uriSegment(config.user)}/memories/${location.uriPath}/${oldSegment}/${memoryEntry.name}`;
|
|
11773
|
+
const record = parseMemoryDocument(sourceUri, content);
|
|
11774
|
+
if (record?.metadata.project && uriSegment(record.metadata.project) === oldSegment) {
|
|
11775
|
+
projectEvidence.oldProject = record.metadata.project;
|
|
11776
|
+
}
|
|
11777
|
+
const repoPath = repoPathEvidenceFromMemory(content);
|
|
11778
|
+
if (repoPath) {
|
|
11779
|
+
projectEvidence.repoPaths.add(repoPath);
|
|
11780
|
+
}
|
|
11781
|
+
}
|
|
11782
|
+
}
|
|
11783
|
+
}
|
|
11784
|
+
return evidence;
|
|
11785
|
+
}
|
|
11786
|
+
function ensureProjectNameMigrationEvidence(evidence, oldSegment) {
|
|
11787
|
+
const existing = evidence.get(oldSegment);
|
|
11788
|
+
if (existing) {
|
|
11789
|
+
return existing;
|
|
11790
|
+
}
|
|
11791
|
+
const created = { oldProject: oldSegment, oldSegment, repoPaths: /* @__PURE__ */ new Set() };
|
|
11792
|
+
evidence.set(oldSegment, created);
|
|
11793
|
+
return created;
|
|
11794
|
+
}
|
|
11795
|
+
function repoPathEvidenceFromMemory(content) {
|
|
11796
|
+
const match = /^repo_path:\s*(.+)$/m.exec(content);
|
|
11797
|
+
if (!match?.[1]) {
|
|
11798
|
+
return void 0;
|
|
11799
|
+
}
|
|
11800
|
+
const cleaned = match[1].trim().replace(/^['"`]+|['"`]+$/g, "").replace(/[.,;]+$/g, "");
|
|
11801
|
+
if (!cleaned.startsWith("/") && !cleaned.startsWith("~/")) {
|
|
11802
|
+
return void 0;
|
|
11803
|
+
}
|
|
11804
|
+
return expandPath(cleaned);
|
|
11805
|
+
}
|
|
11806
|
+
async function projectNameMigrationContextForRepoPath(oldProject, repoPath) {
|
|
11807
|
+
const repoRoot = await gitValue(["rev-parse", "--show-toplevel"], repoPath);
|
|
11808
|
+
if (!repoRoot) {
|
|
11809
|
+
return void 0;
|
|
11810
|
+
}
|
|
11811
|
+
const newProject = await resolveGitRemoteRepoName(repoRoot);
|
|
11812
|
+
if (!newProject) {
|
|
11813
|
+
return void 0;
|
|
11814
|
+
}
|
|
11815
|
+
return projectNameMigrationContextFromParts({
|
|
11816
|
+
newProject,
|
|
11817
|
+
oldProject,
|
|
11818
|
+
repoRoot
|
|
11819
|
+
});
|
|
11820
|
+
}
|
|
11821
|
+
async function currentWorkspaceProjectNameMigrationContext(evidence) {
|
|
11822
|
+
const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]);
|
|
11823
|
+
if (!repoRoot) {
|
|
11824
|
+
return void 0;
|
|
11825
|
+
}
|
|
11826
|
+
const newProject = await resolveGitRemoteRepoName(repoRoot);
|
|
11827
|
+
const oldProject = await resolveRepoFolderName(repoRoot);
|
|
11828
|
+
if (!newProject || !oldProject) {
|
|
11829
|
+
return void 0;
|
|
11830
|
+
}
|
|
11831
|
+
const oldSegment = uriSegment(oldProject);
|
|
11832
|
+
if (!evidence.has(oldSegment)) {
|
|
11833
|
+
return void 0;
|
|
11834
|
+
}
|
|
11835
|
+
return projectNameMigrationContextFromParts({ newProject, oldProject, repoRoot });
|
|
11836
|
+
}
|
|
11837
|
+
function projectNameMigrationContextFromParts(params) {
|
|
11838
|
+
const newSegment = uriSegment(params.newProject);
|
|
11839
|
+
const oldSegment = uriSegment(params.oldProject);
|
|
11840
|
+
if (newSegment === oldSegment) {
|
|
11841
|
+
return void 0;
|
|
11842
|
+
}
|
|
11843
|
+
return {
|
|
11844
|
+
newProject: params.newProject,
|
|
11845
|
+
newSegment,
|
|
11846
|
+
oldProject: params.oldProject,
|
|
11847
|
+
oldSegment,
|
|
11848
|
+
repoRoot: params.repoRoot
|
|
11849
|
+
};
|
|
11850
|
+
}
|
|
11851
|
+
function dedupeProjectNameMigrationContexts(contexts) {
|
|
11852
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11853
|
+
const out = [];
|
|
11854
|
+
for (const context of contexts) {
|
|
11855
|
+
const key = `${context.oldSegment}\0${context.newSegment}\0${context.repoRoot}`;
|
|
11856
|
+
if (seen.has(key)) {
|
|
11857
|
+
continue;
|
|
11858
|
+
}
|
|
11859
|
+
seen.add(key);
|
|
11860
|
+
out.push(context);
|
|
11861
|
+
}
|
|
11862
|
+
return out;
|
|
11863
|
+
}
|
|
11864
|
+
async function projectNameMigrationCandidates(config, context, limit) {
|
|
11865
|
+
const candidates = [];
|
|
11866
|
+
for (const location of projectMemoryLocations()) {
|
|
11867
|
+
const sourceDirectory = (0, import_node_path8.join)(localUserMemoriesRoot(config), ...location.relativePath, context.oldSegment);
|
|
11868
|
+
const sourceDirectoryUri = `viking://user/${uriSegment(config.user)}/memories/${location.uriPath}/${context.oldSegment}`;
|
|
11869
|
+
let entries;
|
|
11870
|
+
try {
|
|
11871
|
+
entries = await (0, import_promises6.readdir)(sourceDirectory, { withFileTypes: true });
|
|
11872
|
+
} catch (_err) {
|
|
11873
|
+
continue;
|
|
11874
|
+
}
|
|
11875
|
+
for (const entry of entries) {
|
|
11876
|
+
if (!entry.isFile() || entry.name.startsWith(".") || !entry.name.endsWith(".md")) {
|
|
11877
|
+
continue;
|
|
11878
|
+
}
|
|
11879
|
+
const sourceUri = `${sourceDirectoryUri}/${entry.name}`;
|
|
11880
|
+
const content = await readTextIfExists((0, import_node_path8.join)(sourceDirectory, entry.name));
|
|
11881
|
+
if (!content) {
|
|
11882
|
+
continue;
|
|
11883
|
+
}
|
|
11884
|
+
const record = parseMemoryDocument(sourceUri, content);
|
|
11885
|
+
if (!record || !canMigrateProjectName(record, context)) {
|
|
11886
|
+
continue;
|
|
11887
|
+
}
|
|
11888
|
+
const metadata = { ...record.metadata, project: context.newProject };
|
|
11889
|
+
const destinationDirectoryUri = memoryDirectoryUri(config, metadata);
|
|
11890
|
+
const destinationDirectory = localMemoryPathForUri(config, destinationDirectoryUri);
|
|
11891
|
+
if (!destinationDirectory) {
|
|
11892
|
+
continue;
|
|
11893
|
+
}
|
|
11894
|
+
const destinationContent = formatMemoryDocument2(record.headerTitle, metadata, record.body);
|
|
11895
|
+
const destination = await projectNameMigrationDestination(
|
|
11896
|
+
destinationDirectory,
|
|
11897
|
+
entry.name,
|
|
11898
|
+
destinationContent,
|
|
11899
|
+
context.oldSegment
|
|
11900
|
+
);
|
|
11901
|
+
candidates.push({
|
|
11902
|
+
destinationContent,
|
|
11903
|
+
destinationExistsWithSameContent: destination.existsWithSameContent,
|
|
11904
|
+
destinationUri: `${destinationDirectoryUri}/${destination.filename}`,
|
|
11905
|
+
sourceUri
|
|
11906
|
+
});
|
|
11907
|
+
if (limit !== void 0 && candidates.length >= limit) {
|
|
11908
|
+
return candidates;
|
|
11909
|
+
}
|
|
11910
|
+
}
|
|
11911
|
+
}
|
|
11912
|
+
return candidates;
|
|
11913
|
+
}
|
|
11914
|
+
function projectNameMigrationActiveContexts(plans, seedManifestMigration) {
|
|
11915
|
+
return dedupeProjectNameMigrationContexts([
|
|
11916
|
+
...plans.filter((plan) => plan.candidates.length > 0).map((plan) => plan.context),
|
|
11917
|
+
...seedManifestMigration?.contexts ?? []
|
|
11918
|
+
]);
|
|
11919
|
+
}
|
|
11920
|
+
function projectNameMigrationSummary(migratedCount, dryRun, contexts) {
|
|
11921
|
+
const memoryWord = migratedCount === 1 ? "memory" : "memories";
|
|
11922
|
+
const verb = dryRun ? "would be migrated" : "migrated";
|
|
11923
|
+
if (contexts.length === 1) {
|
|
11924
|
+
const [context] = contexts;
|
|
11925
|
+
return `Project-name migration summary: ${migratedCount} ${memoryWord} ${verb} from ${context.oldProject} to ${context.newProject}`;
|
|
11926
|
+
}
|
|
11927
|
+
const renameSummary = contexts.map((context) => `${context.oldProject} -> ${context.newProject}`).join(", ");
|
|
11928
|
+
return `Project-name migration summary: ${migratedCount} ${memoryWord} ${verb} across ${contexts.length} project rename(s)${renameSummary ? `: ${renameSummary}` : ""}`;
|
|
11929
|
+
}
|
|
11930
|
+
async function migrateSeedManifestProjectNames(config, migration, dryRun) {
|
|
11931
|
+
if (!migration) {
|
|
11932
|
+
return false;
|
|
11933
|
+
}
|
|
11934
|
+
if (dryRun) {
|
|
11935
|
+
console.log(`Would update seed manifest: ${config.manifestPath}`);
|
|
11936
|
+
console.log(migration.output.trimEnd());
|
|
11937
|
+
return true;
|
|
11938
|
+
}
|
|
11939
|
+
await ensureDirectory((0, import_node_path8.dirname)(config.manifestPath), false);
|
|
11940
|
+
const currentContent = await readFileIfExists(config.manifestPath);
|
|
11941
|
+
if (currentContent !== void 0) {
|
|
11942
|
+
const backupPath = `${config.manifestPath}.project-name-${safeTimestamp()}`;
|
|
11943
|
+
await (0, import_promises6.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
|
|
11944
|
+
await (0, import_promises6.chmod)(backupPath, 384);
|
|
11945
|
+
console.log(`Backup: ${backupPath}`);
|
|
11946
|
+
}
|
|
11947
|
+
await (0, import_promises6.writeFile)(config.manifestPath, migration.output, { encoding: "utf8", mode: 384 });
|
|
11948
|
+
await (0, import_promises6.chmod)(config.manifestPath, 384);
|
|
11949
|
+
console.log(`Updated seed manifest: ${config.manifestPath}`);
|
|
11950
|
+
return true;
|
|
11951
|
+
}
|
|
11952
|
+
async function seedManifestProjectNameMigration(config, contexts) {
|
|
11953
|
+
let manifest;
|
|
11954
|
+
try {
|
|
11955
|
+
manifest = await readSeedManifest(config.manifestPath);
|
|
11956
|
+
} catch (_err) {
|
|
11957
|
+
return void 0;
|
|
11958
|
+
}
|
|
11959
|
+
const renamed = /* @__PURE__ */ new Map();
|
|
11960
|
+
let changed = false;
|
|
11961
|
+
const projects = manifest.projects.map((project) => {
|
|
11962
|
+
const context = contexts.find(
|
|
11963
|
+
(candidate) => isSeedManifestProjectNameCandidate(
|
|
11964
|
+
project,
|
|
11965
|
+
candidate,
|
|
11966
|
+
`viking://resources/repos/${candidate.oldSegment}`,
|
|
11967
|
+
`viking://resources/repos/${candidate.newSegment}`
|
|
11968
|
+
)
|
|
11969
|
+
);
|
|
11970
|
+
if (!context) {
|
|
11971
|
+
return project;
|
|
11972
|
+
}
|
|
11973
|
+
const newNameExists = manifest.projects.some(
|
|
11974
|
+
(other) => other !== project && uriSegment(other.name) === context.newSegment
|
|
11975
|
+
);
|
|
11976
|
+
if (newNameExists || [...renamed.values()].some((existing) => existing.newSegment === context.newSegment)) {
|
|
11977
|
+
return project;
|
|
11978
|
+
}
|
|
11979
|
+
changed = true;
|
|
11980
|
+
renamed.set(context.oldSegment, context);
|
|
11981
|
+
return {
|
|
11982
|
+
...project,
|
|
11983
|
+
name: context.newProject,
|
|
11984
|
+
uri: trimTrailingSlash(project.uri) === `viking://resources/repos/${context.oldSegment}` ? `viking://resources/repos/${context.newSegment}` : project.uri
|
|
11985
|
+
};
|
|
11986
|
+
});
|
|
11987
|
+
const worksets = renamed.size > 0 ? manifest.worksets?.map((workset) => {
|
|
11988
|
+
const members = workset.projects.map((projectName) => {
|
|
11989
|
+
const context = renamed.get(uriSegment(projectName));
|
|
11990
|
+
if (!context) {
|
|
11991
|
+
return projectName;
|
|
11992
|
+
}
|
|
11993
|
+
changed = true;
|
|
11994
|
+
return context.newProject;
|
|
11995
|
+
});
|
|
11996
|
+
return { ...workset, projects: members };
|
|
11997
|
+
}) : manifest.worksets;
|
|
11998
|
+
if (!changed) {
|
|
11999
|
+
return void 0;
|
|
12000
|
+
}
|
|
12001
|
+
return {
|
|
12002
|
+
contexts: [...renamed.values()],
|
|
12003
|
+
newProjects: [...new Set([...renamed.values()].map((context) => context.newProject))],
|
|
12004
|
+
output: `${index_vite_proxy_tmp_default.dump(
|
|
12005
|
+
{
|
|
12006
|
+
version: manifest.version,
|
|
12007
|
+
projects: projects.map((project) => ({
|
|
12008
|
+
name: project.name,
|
|
12009
|
+
path: project.path,
|
|
12010
|
+
uri: project.uri,
|
|
12011
|
+
seed: [...project.seed]
|
|
12012
|
+
})),
|
|
12013
|
+
...worksets ? {
|
|
12014
|
+
worksets: worksets.map((workset) => ({
|
|
12015
|
+
name: workset.name,
|
|
12016
|
+
...workset.description ? { description: workset.description } : {},
|
|
12017
|
+
projects: [...workset.projects]
|
|
12018
|
+
}))
|
|
12019
|
+
} : {},
|
|
12020
|
+
...manifest.futureMonorepo ? {
|
|
12021
|
+
future_monorepo: {
|
|
12022
|
+
path_candidates: [...manifest.futureMonorepo.pathCandidates],
|
|
12023
|
+
uri: manifest.futureMonorepo.uri
|
|
12024
|
+
}
|
|
12025
|
+
} : {}
|
|
12026
|
+
},
|
|
12027
|
+
{ lineWidth: 120, noRefs: true }
|
|
12028
|
+
)}`
|
|
12029
|
+
};
|
|
12030
|
+
}
|
|
12031
|
+
function isSeedManifestProjectNameCandidate(project, context, oldDefaultUri, newDefaultUri) {
|
|
12032
|
+
const nameSegment = uriSegment(project.name);
|
|
12033
|
+
const uriMatchesOld = trimTrailingSlash(project.uri) === oldDefaultUri;
|
|
12034
|
+
const pathMatchesRepo = expandPath(project.path) === context.repoRoot;
|
|
12035
|
+
if (nameSegment === context.newSegment && !uriMatchesOld) {
|
|
12036
|
+
return false;
|
|
12037
|
+
}
|
|
12038
|
+
if (nameSegment !== context.oldSegment && !uriMatchesOld && !pathMatchesRepo) {
|
|
12039
|
+
return false;
|
|
12040
|
+
}
|
|
12041
|
+
return nameSegment !== context.newSegment || uriMatchesOld || trimTrailingSlash(project.uri) !== newDefaultUri;
|
|
12042
|
+
}
|
|
12043
|
+
function canMigrateProjectName(record, context) {
|
|
12044
|
+
const projectSegment = record.metadata.project ? uriSegment(record.metadata.project) : context.oldSegment;
|
|
12045
|
+
return projectSegment === context.oldSegment || projectSegment === context.newSegment;
|
|
12046
|
+
}
|
|
12047
|
+
async function projectNameMigrationDestination(destinationDirectory, filename, content, oldProjectSegment) {
|
|
12048
|
+
const direct = await projectNameMigrationDestinationState(destinationDirectory, filename, content);
|
|
12049
|
+
if (!direct.exists || direct.sameContent) {
|
|
12050
|
+
return { existsWithSameContent: direct.sameContent, filename };
|
|
12051
|
+
}
|
|
12052
|
+
const stem = filename.replace(/\.md$/i, "");
|
|
12053
|
+
const fromOldProject = `${stem}-from-${oldProjectSegment}.md`;
|
|
12054
|
+
const renamed = await projectNameMigrationDestinationState(destinationDirectory, fromOldProject, content);
|
|
12055
|
+
if (!renamed.exists || renamed.sameContent) {
|
|
12056
|
+
return { existsWithSameContent: renamed.sameContent, filename: fromOldProject };
|
|
12057
|
+
}
|
|
12058
|
+
return {
|
|
12059
|
+
existsWithSameContent: false,
|
|
12060
|
+
filename: `${stem}-from-${oldProjectSegment}-${sha256(content).slice(0, 12)}.md`
|
|
12061
|
+
};
|
|
12062
|
+
}
|
|
12063
|
+
async function projectNameMigrationDestinationState(destinationDirectory, filename, content) {
|
|
12064
|
+
const existing = await readTextIfExists((0, import_node_path8.join)(destinationDirectory, filename));
|
|
12065
|
+
return { exists: existing !== void 0, sameContent: existing?.trim() === content.trim() };
|
|
12066
|
+
}
|
|
12067
|
+
function projectMemoryLocations() {
|
|
12068
|
+
return [
|
|
12069
|
+
{ relativePath: ["durable", "projects"], uriPath: "durable/projects" },
|
|
12070
|
+
{ relativePath: ["durable", "archived"], uriPath: "durable/archived" },
|
|
12071
|
+
{ relativePath: ["durable", "superseded"], uriPath: "durable/superseded" },
|
|
12072
|
+
{ relativePath: ["handoffs", "active"], uriPath: "handoffs/active" },
|
|
12073
|
+
{ relativePath: ["handoffs", "archived"], uriPath: "handoffs/archived" },
|
|
12074
|
+
{ relativePath: ["handoffs", "superseded"], uriPath: "handoffs/superseded" },
|
|
12075
|
+
{ relativePath: ["incidents", "active"], uriPath: "incidents/active" },
|
|
12076
|
+
{ relativePath: ["incidents", "archived"], uriPath: "incidents/archived" },
|
|
12077
|
+
{ relativePath: ["incidents", "superseded"], uriPath: "incidents/superseded" }
|
|
12078
|
+
];
|
|
12079
|
+
}
|
|
11561
12080
|
async function runRecall(config, options) {
|
|
11562
12081
|
if (options.dryRun !== true) {
|
|
11563
12082
|
await syncSharedReposAndLog(config);
|
|
@@ -11581,6 +12100,9 @@ async function runRecall(config, options) {
|
|
|
11581
12100
|
const dryRun = options.dryRun === true;
|
|
11582
12101
|
const inferredUri = options.uri ?? (options.inferScope === false ? void 0 : await inferRecallUri(config, projectQuery));
|
|
11583
12102
|
const project = await inferProjectFromQuery(config.manifestPath, options.project ?? projectQuery);
|
|
12103
|
+
const projectMemoryName = await recallProjectMemoryName(options.project, {
|
|
12104
|
+
includeProcessCwd: true
|
|
12105
|
+
});
|
|
11584
12106
|
const nodeLimit = options.nodeLimit ? parsePositiveInteger(options.nodeLimit, "node limit") : void 0;
|
|
11585
12107
|
const explicitWorkset = options.workset ? await requireWorkset(config.manifestPath, options.workset) : void 0;
|
|
11586
12108
|
const searchArgs = (scopeUri) => [
|
|
@@ -11600,9 +12122,19 @@ async function runRecall(config, options) {
|
|
|
11600
12122
|
const passes = [
|
|
11601
12123
|
await recallSearchHits(config, ov, searchArgs(inferredUri), { dryRun, includeArchived })
|
|
11602
12124
|
];
|
|
12125
|
+
const scopedRecallUris = new Set([inferredUri].filter((uri) => uri !== void 0));
|
|
11603
12126
|
if (options.project && project) {
|
|
11604
12127
|
const projectMemoryUri = `viking://user/${uriSegment(config.user)}/memories/durable/projects/${uriSegment(project.name)}`;
|
|
11605
|
-
|
|
12128
|
+
if (!scopedRecallUris.has(projectMemoryUri)) {
|
|
12129
|
+
scopedRecallUris.add(projectMemoryUri);
|
|
12130
|
+
passes.push(await recallSearchHits(config, ov, searchArgs(projectMemoryUri), { dryRun, includeArchived }));
|
|
12131
|
+
}
|
|
12132
|
+
}
|
|
12133
|
+
for (const scope of projectMemoryScopeUris(config, projectMemoryName, includeArchived)) {
|
|
12134
|
+
if (!scopedRecallUris.has(scope)) {
|
|
12135
|
+
scopedRecallUris.add(scope);
|
|
12136
|
+
passes.push(await recallSearchHits(config, ov, searchArgs(scope), { dryRun, includeArchived }));
|
|
12137
|
+
}
|
|
11606
12138
|
}
|
|
11607
12139
|
const seededUri = project ? trimTrailingSlash(project.uri) : void 0;
|
|
11608
12140
|
if (seededUri?.startsWith("viking://") && seededUri !== inferredUri && !options.uri && options.inferScope !== false) {
|
|
@@ -11611,7 +12143,9 @@ async function runRecall(config, options) {
|
|
|
11611
12143
|
const workset = !options.uri && explicitWorkset ? explicitWorkset : !options.uri && options.inferScope !== false ? await inferWorksetFromQuery(config.manifestPath, projectQuery) : void 0;
|
|
11612
12144
|
if (workset && workset.projects.length > 0) {
|
|
11613
12145
|
console.log(`Workset scope: ${workset.name} (${workset.projects.map((member) => member.name).join(", ")})`);
|
|
11614
|
-
const alreadyScoped = new Set(
|
|
12146
|
+
const alreadyScoped = new Set(
|
|
12147
|
+
[inferredUri, seededUri, ...scopedRecallUris].filter((uri) => uri !== void 0)
|
|
12148
|
+
);
|
|
11615
12149
|
const worksetScopes = worksetScopeUris(config, workset).filter((uri) => !alreadyScoped.has(uri)).slice(0, MAX_WORKSET_PASSES);
|
|
11616
12150
|
for (const scope of worksetScopes) {
|
|
11617
12151
|
passes.push(await recallSearchHits(config, ov, searchArgs(scope), { dryRun, includeArchived }));
|
|
@@ -12264,6 +12798,27 @@ function worksetScopeUris(config, workset) {
|
|
|
12264
12798
|
}
|
|
12265
12799
|
return [...new Set(scopes)];
|
|
12266
12800
|
}
|
|
12801
|
+
async function recallProjectMemoryName(explicitProject, options) {
|
|
12802
|
+
return normalizeOptionalMetadata2(explicitProject) ?? await resolveWorkspaceRepoName(options);
|
|
12803
|
+
}
|
|
12804
|
+
function projectMemoryScopeUris(config, projectName, includeArchived) {
|
|
12805
|
+
if (!projectName) {
|
|
12806
|
+
return [];
|
|
12807
|
+
}
|
|
12808
|
+
const base = `viking://user/${uriSegment(config.user)}/memories`;
|
|
12809
|
+
const projectSegment = uriSegment(projectName);
|
|
12810
|
+
const scopes = [
|
|
12811
|
+
`${base}/durable/projects/${projectSegment}`,
|
|
12812
|
+
`${base}/handoffs/active/${projectSegment}`,
|
|
12813
|
+
`${base}/incidents/active/${projectSegment}`
|
|
12814
|
+
];
|
|
12815
|
+
return includeArchived ? [
|
|
12816
|
+
...scopes,
|
|
12817
|
+
`${base}/durable/archived/${projectSegment}`,
|
|
12818
|
+
`${base}/handoffs/archived/${projectSegment}`,
|
|
12819
|
+
`${base}/incidents/archived/${projectSegment}`
|
|
12820
|
+
] : scopes;
|
|
12821
|
+
}
|
|
12267
12822
|
function exactMemoryScopes(config, includeArchived, query, project) {
|
|
12268
12823
|
return exactMemoryScopeUris({
|
|
12269
12824
|
agentMemoriesUri: `viking://agent/${uriSegment(config.agentId)}/memories`,
|
|
@@ -13500,7 +14055,7 @@ async function runInitManifest(config, options) {
|
|
|
13500
14055
|
continue;
|
|
13501
14056
|
}
|
|
13502
14057
|
seen.add(identity);
|
|
13503
|
-
projects.push(projectManifestForRepo(repoRoot, projects));
|
|
14058
|
+
projects.push(await projectManifestForRepo(repoRoot, projects));
|
|
13504
14059
|
}
|
|
13505
14060
|
const outputManifest = {
|
|
13506
14061
|
version: 1,
|
|
@@ -13608,8 +14163,8 @@ async function projectIdentity(path2) {
|
|
|
13608
14163
|
return expanded;
|
|
13609
14164
|
}
|
|
13610
14165
|
}
|
|
13611
|
-
function projectManifestForRepo(repoRoot, existingProjects) {
|
|
13612
|
-
const baseName = uriSegment((0, import_node_path13.basename)(repoRoot));
|
|
14166
|
+
async function projectManifestForRepo(repoRoot, existingProjects) {
|
|
14167
|
+
const baseName = uriSegment(await resolveRepoName(repoRoot) ?? (0, import_node_path13.basename)(repoRoot));
|
|
13613
14168
|
const usedNames = new Set(existingProjects.map((project) => project.name));
|
|
13614
14169
|
const usedUris = new Set(existingProjects.map((project) => project.uri));
|
|
13615
14170
|
let name = baseName;
|
|
@@ -14322,6 +14877,9 @@ async function applicablePostUpdateMigrations(config, options) {
|
|
|
14322
14877
|
if (migration.requiresLegacyHandoffs === true && !await hasLegacyLifecycleHandoffCandidates(config)) {
|
|
14323
14878
|
continue;
|
|
14324
14879
|
}
|
|
14880
|
+
if (migration.requiresProjectNameConsolidation === true && !await hasProjectNameMigrationCandidates(config)) {
|
|
14881
|
+
continue;
|
|
14882
|
+
}
|
|
14325
14883
|
applicable.push(migration);
|
|
14326
14884
|
}
|
|
14327
14885
|
return applicable;
|
|
@@ -14348,6 +14906,7 @@ function parsePostUpdateMigration(value) {
|
|
|
14348
14906
|
instructions: stringArray(value, "instructions"),
|
|
14349
14907
|
introducedIn: value.introducedIn,
|
|
14350
14908
|
requiresLegacyHandoffs: value.requiresLegacyHandoffs === true,
|
|
14909
|
+
requiresProjectNameConsolidation: value.requiresProjectNameConsolidation === true,
|
|
14351
14910
|
title: value.title
|
|
14352
14911
|
};
|
|
14353
14912
|
}
|
|
@@ -14697,7 +15256,7 @@ async function repairManifest(config, dryRun) {
|
|
|
14697
15256
|
console.log(`WARN cannot create replacement manifest from current directory: ${errorMessage(err)}`);
|
|
14698
15257
|
return;
|
|
14699
15258
|
}
|
|
14700
|
-
const project = projectManifestForRepo(repoRoot, []);
|
|
15259
|
+
const project = await projectManifestForRepo(repoRoot, []);
|
|
14701
15260
|
const output2 = index_vite_proxy_tmp_default.dump(
|
|
14702
15261
|
{
|
|
14703
15262
|
version: 1,
|
|
@@ -17175,6 +17734,9 @@ async function main() {
|
|
|
17175
17734
|
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) => {
|
|
17176
17735
|
await runMigrateLifecycle(getRuntimeConfig(program2), options);
|
|
17177
17736
|
});
|
|
17737
|
+
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) => {
|
|
17738
|
+
await runMigrateProjectNames(getRuntimeConfig(program2), options);
|
|
17739
|
+
});
|
|
17178
17740
|
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(
|
|
17179
17741
|
"--workset <name>",
|
|
17180
17742
|
"Recall across a named seed-manifest workset (a set of related repos) as one working set"
|
package/docs/index.html
CHANGED
|
@@ -1006,7 +1006,7 @@
|
|
|
1006
1006
|
|
|
1007
1007
|
<!-- threadnote MCP -->
|
|
1008
1008
|
<g font-family="JetBrains Mono, monospace" font-size="13" fill="#ececf1">
|
|
1009
|
-
<rect x="
|
|
1009
|
+
<rect x="300" y="150" width="280" height="60" rx="10" fill="#5ee0c5" />
|
|
1010
1010
|
<text x="440" y="178" text-anchor="middle" fill="#0a0a0d" font-weight="600">threadnote-mcp-server</text>
|
|
1011
1011
|
<text x="440" y="196" text-anchor="middle" fill="#0a0a0d" opacity="0.7" font-size="11">
|
|
1012
1012
|
recall · read · list · remember · share
|