threadnote 1.6.2 → 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.
@@ -18,6 +18,26 @@
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 folders for the old folder-derived project name from the current workspace.",
29
+ "It also renames the matching seed manifest project and workset references so future seeded guidance uses the remote-derived project name.",
30
+ "Matching memories are copied under the remote-derived project name with updated project metadata, then the old copy is removed.",
31
+ "If a destination file already exists with different content, Threadnote keeps both by writing the migrated copy with a -from-<old-project> suffix.",
32
+ "Shared team memories and unrelated project folders are left untouched."
33
+ ],
34
+ "commandArgs": ["migrate-project-names", "--apply"],
35
+ "instructions": [
36
+ "Post-update project-name migration finished. Future default handoffs and memories for this repo will use the git remote repository name as their project.",
37
+ "If the migration updated your seed manifest, run threadnote seed --only <new-project-name> to re-ingest seeded resources under the new project URI.",
38
+ "If Threadnote reported any original files were still being processed, rerun the printed threadnote forget <uri> command later."
39
+ ],
40
+ "requiresProjectNameConsolidation": true
21
41
  }
22
42
  ]
23
43
  }
@@ -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: (0, import_node_path.basename)(repoRoot) },
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([params.pinnedUri, seededUri].filter((uri) => uri !== void 0));
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,
@@ -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: (0, import_node_path2.basename)(repoRoot) },
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,263 @@ 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 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
+ }
11561
11890
  async function runRecall(config, options) {
11562
11891
  if (options.dryRun !== true) {
11563
11892
  await syncSharedReposAndLog(config);
@@ -11581,6 +11910,9 @@ async function runRecall(config, options) {
11581
11910
  const dryRun = options.dryRun === true;
11582
11911
  const inferredUri = options.uri ?? (options.inferScope === false ? void 0 : await inferRecallUri(config, projectQuery));
11583
11912
  const project = await inferProjectFromQuery(config.manifestPath, options.project ?? projectQuery);
11913
+ const projectMemoryName = await recallProjectMemoryName(options.project, {
11914
+ includeProcessCwd: true
11915
+ });
11584
11916
  const nodeLimit = options.nodeLimit ? parsePositiveInteger(options.nodeLimit, "node limit") : void 0;
11585
11917
  const explicitWorkset = options.workset ? await requireWorkset(config.manifestPath, options.workset) : void 0;
11586
11918
  const searchArgs = (scopeUri) => [
@@ -11600,9 +11932,19 @@ async function runRecall(config, options) {
11600
11932
  const passes = [
11601
11933
  await recallSearchHits(config, ov, searchArgs(inferredUri), { dryRun, includeArchived })
11602
11934
  ];
11935
+ const scopedRecallUris = new Set([inferredUri].filter((uri) => uri !== void 0));
11603
11936
  if (options.project && project) {
11604
11937
  const projectMemoryUri = `viking://user/${uriSegment(config.user)}/memories/durable/projects/${uriSegment(project.name)}`;
11605
- passes.push(await recallSearchHits(config, ov, searchArgs(projectMemoryUri), { dryRun, includeArchived }));
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
+ }
11606
11948
  }
11607
11949
  const seededUri = project ? trimTrailingSlash(project.uri) : void 0;
11608
11950
  if (seededUri?.startsWith("viking://") && seededUri !== inferredUri && !options.uri && options.inferScope !== false) {
@@ -11611,7 +11953,9 @@ async function runRecall(config, options) {
11611
11953
  const workset = !options.uri && explicitWorkset ? explicitWorkset : !options.uri && options.inferScope !== false ? await inferWorksetFromQuery(config.manifestPath, projectQuery) : void 0;
11612
11954
  if (workset && workset.projects.length > 0) {
11613
11955
  console.log(`Workset scope: ${workset.name} (${workset.projects.map((member) => member.name).join(", ")})`);
11614
- const alreadyScoped = new Set([inferredUri, seededUri].filter((uri) => uri !== void 0));
11956
+ const alreadyScoped = new Set(
11957
+ [inferredUri, seededUri, ...scopedRecallUris].filter((uri) => uri !== void 0)
11958
+ );
11615
11959
  const worksetScopes = worksetScopeUris(config, workset).filter((uri) => !alreadyScoped.has(uri)).slice(0, MAX_WORKSET_PASSES);
11616
11960
  for (const scope of worksetScopes) {
11617
11961
  passes.push(await recallSearchHits(config, ov, searchArgs(scope), { dryRun, includeArchived }));
@@ -12264,6 +12608,27 @@ function worksetScopeUris(config, workset) {
12264
12608
  }
12265
12609
  return [...new Set(scopes)];
12266
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
+ }
12267
12632
  function exactMemoryScopes(config, includeArchived, query, project) {
12268
12633
  return exactMemoryScopeUris({
12269
12634
  agentMemoriesUri: `viking://agent/${uriSegment(config.agentId)}/memories`,
@@ -13500,7 +13865,7 @@ async function runInitManifest(config, options) {
13500
13865
  continue;
13501
13866
  }
13502
13867
  seen.add(identity);
13503
- projects.push(projectManifestForRepo(repoRoot, projects));
13868
+ projects.push(await projectManifestForRepo(repoRoot, projects));
13504
13869
  }
13505
13870
  const outputManifest = {
13506
13871
  version: 1,
@@ -13608,8 +13973,8 @@ async function projectIdentity(path2) {
13608
13973
  return expanded;
13609
13974
  }
13610
13975
  }
13611
- function projectManifestForRepo(repoRoot, existingProjects) {
13612
- 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));
13613
13978
  const usedNames = new Set(existingProjects.map((project) => project.name));
13614
13979
  const usedUris = new Set(existingProjects.map((project) => project.uri));
13615
13980
  let name = baseName;
@@ -14322,6 +14687,9 @@ async function applicablePostUpdateMigrations(config, options) {
14322
14687
  if (migration.requiresLegacyHandoffs === true && !await hasLegacyLifecycleHandoffCandidates(config)) {
14323
14688
  continue;
14324
14689
  }
14690
+ if (migration.requiresProjectNameConsolidation === true && !await hasProjectNameMigrationCandidates(config)) {
14691
+ continue;
14692
+ }
14325
14693
  applicable.push(migration);
14326
14694
  }
14327
14695
  return applicable;
@@ -14348,6 +14716,7 @@ function parsePostUpdateMigration(value) {
14348
14716
  instructions: stringArray(value, "instructions"),
14349
14717
  introducedIn: value.introducedIn,
14350
14718
  requiresLegacyHandoffs: value.requiresLegacyHandoffs === true,
14719
+ requiresProjectNameConsolidation: value.requiresProjectNameConsolidation === true,
14351
14720
  title: value.title
14352
14721
  };
14353
14722
  }
@@ -14697,7 +15066,7 @@ async function repairManifest(config, dryRun) {
14697
15066
  console.log(`WARN cannot create replacement manifest from current directory: ${errorMessage(err)}`);
14698
15067
  return;
14699
15068
  }
14700
- const project = projectManifestForRepo(repoRoot, []);
15069
+ const project = await projectManifestForRepo(repoRoot, []);
14701
15070
  const output2 = index_vite_proxy_tmp_default.dump(
14702
15071
  {
14703
15072
  version: 1,
@@ -17175,6 +17544,9 @@ async function main() {
17175
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) => {
17176
17545
  await runMigrateLifecycle(getRuntimeConfig(program2), options);
17177
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
+ });
17178
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(
17179
17551
  "--workset <name>",
17180
17552
  "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="340" y="150" width="200" height="60" rx="10" fill="#5ee0c5" />
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadnote",
3
- "version": "1.6.2",
3
+ "version": "1.7.0",
4
4
  "type": "commonjs",
5
5
  "main": "dist/threadnote.cjs",
6
6
  "description": "Shared local context and handoffs for development agents",