threadnote 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -350,8 +350,10 @@ This is it! Start working with your agents as usual. The agent will automaticall
350
350
  - `forget`: removes a `viking://` URI.
351
351
  - `export-pack` / `import-pack`: moves local context through `.ovpack` files.
352
352
  - `share init|status|sync|publish|unpublish|list|remove`: opts a curated subset of durable memories into a team git
353
- repo so teammates can pull them. Personal handoffs and preferences stay local. See `docs/share.md` for the full
354
- workflow and the publish-time scrubber rules.
353
+ repo. Threadnote periodically fetches configured share repos and automatically syncs clean incoming changes before
354
+ agent recall/read; use `share sync` for dirty worktrees, conflicts, explicit pushes, or immediate manual sync.
355
+ Personal handoffs and preferences stay local. See `docs/share.md` for the full workflow and the publish-time scrubber
356
+ rules.
355
357
 
356
358
  ## Source Checkout
357
359
 
@@ -427,4 +429,6 @@ threadnote list viking://agent/threadnote/memories --all --recursive
427
429
 
428
430
  When MCP is installed, the agent should use Threadnote MCP `recall_context`, then `read_context` or `list_context`.
429
431
  Agents must pass JSON arguments, for example `recall_context({"query":"agent context"})`. Older adapters expose
430
- `search`, `read`, and `list` aliases. The CLI commands are the fallback path.
432
+ `search`, `read`, and `list` aliases. Before recall/read returns, Threadnote automatically syncs clean incoming shared
433
+ memory updates when a configured share repo is behind; failures are reported as warnings and the local read still
434
+ continues. The CLI commands are the fallback path.
@@ -33608,6 +33608,26 @@ function redactText(content) {
33608
33608
  "$1[REDACTED]"
33609
33609
  ).replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]").replace(/sk-[A-Za-z0-9_-]{16,}/g, "sk-[REDACTED]").replace(/gh[pousr]_[A-Za-z0-9_]{16,}/g, "gh_[REDACTED]");
33610
33610
  }
33611
+ async function requiredOpenVikingCli() {
33612
+ const command = await findExecutable(["ov", "openviking"]);
33613
+ if (!command) {
33614
+ throw new Error("Neither ov nor openviking was found in PATH. Run threadnote install first.");
33615
+ }
33616
+ return command;
33617
+ }
33618
+ async function openVikingCliForMode(dryRun) {
33619
+ if (dryRun) {
33620
+ return await findExecutable(["ov", "openviking"]) ?? "ov";
33621
+ }
33622
+ return requiredOpenVikingCli();
33623
+ }
33624
+ async function requiredExecutable(command) {
33625
+ const executable = await findExecutable([command]);
33626
+ if (!executable) {
33627
+ throw new Error(`${command} was not found in PATH.`);
33628
+ }
33629
+ return executable;
33630
+ }
33611
33631
  async function findExecutable(commands) {
33612
33632
  for (const command of commands) {
33613
33633
  const result = await runCommand("which", [command], { allowFailure: true });
@@ -33669,6 +33689,21 @@ async function sleep(ms) {
33669
33689
  setTimeout(resolvePromise, ms);
33670
33690
  });
33671
33691
  }
33692
+ async function exists(path) {
33693
+ try {
33694
+ await (0, import_promises.access)(path);
33695
+ return true;
33696
+ } catch (_err) {
33697
+ return false;
33698
+ }
33699
+ }
33700
+ async function isFile(path) {
33701
+ try {
33702
+ return (await (0, import_promises.stat)(path)).isFile();
33703
+ } catch (_err) {
33704
+ return false;
33705
+ }
33706
+ }
33672
33707
  async function readFileIfExists(path) {
33673
33708
  try {
33674
33709
  return await (0, import_promises.readFile)(path, "utf8");
@@ -33841,6 +33876,8 @@ function withIdentity(config2, args) {
33841
33876
  // src/share.ts
33842
33877
  var TEAMS_FILE_VERSION = 1;
33843
33878
  var SHARED_SEGMENT = "shared";
33879
+ var SHAREABLE_MEMORY_KIND_DIRS = ["durable"];
33880
+ var AUTO_SHARE_FETCH_INTERVAL_MS = 5 * 60 * 1e3;
33844
33881
  var DEFAULT_GIT_REMOTE_NAME = "origin";
33845
33882
  var SCRUBBER_PATTERNS = [
33846
33883
  // Credentials: never redactable. Blocking is the only safe response —
@@ -33886,6 +33923,231 @@ function applyScrubber(content, { redact }) {
33886
33923
  }
33887
33924
  return { cleaned, redactions };
33888
33925
  }
33926
+ var autoShareStates = /* @__PURE__ */ new Map();
33927
+ function startShareBackgroundFetch(config2) {
33928
+ const state = autoShareState(config2);
33929
+ if (state.timer) {
33930
+ return;
33931
+ }
33932
+ void refreshShareUpdateState(config2, { force: true }).catch((err) => {
33933
+ console.error(`share auto-fetch failed: ${err instanceof Error ? err.message : String(err)}`);
33934
+ });
33935
+ state.timer = setInterval(() => {
33936
+ void refreshShareUpdateState(config2, { force: false }).catch((err) => {
33937
+ console.error(`share auto-fetch failed: ${err instanceof Error ? err.message : String(err)}`);
33938
+ });
33939
+ }, AUTO_SHARE_FETCH_INTERVAL_MS);
33940
+ state.timer.unref?.();
33941
+ }
33942
+ async function syncSharedReposBeforeAgentRead(config2) {
33943
+ const state = autoShareState(config2);
33944
+ return enqueueShareOperation(state, async () => {
33945
+ await loadPendingReindexes(config2, state);
33946
+ const warnings = await refreshShareUpdateStateLocked(config2, state, { force: false });
33947
+ const syncTeams = /* @__PURE__ */ new Set([...state.behindTeams, ...state.pendingReindexes.keys()]);
33948
+ if (syncTeams.size === 0) {
33949
+ return { syncedTeams: [], warnings };
33950
+ }
33951
+ const syncedTeams = [];
33952
+ const remainingBehind = new Set(state.behindTeams);
33953
+ for (const team of syncTeams) {
33954
+ try {
33955
+ const warning = await runShareSyncQuiet(config2, state, { team });
33956
+ if (warning) {
33957
+ warnings.push(warning);
33958
+ } else {
33959
+ remainingBehind.delete(team);
33960
+ syncedTeams.push(team);
33961
+ }
33962
+ } catch (err) {
33963
+ warnings.push(
33964
+ `Auto-sync for shared team "${team}" failed: ${err instanceof Error ? err.message : String(err)}`
33965
+ );
33966
+ }
33967
+ }
33968
+ state.behindTeams = remainingBehind;
33969
+ state.lastCheckedAt = Date.now();
33970
+ return { syncedTeams, warnings };
33971
+ });
33972
+ }
33973
+ function autoShareState(config2) {
33974
+ const key = `${config2.agentContextHome}:${config2.account}:${config2.user}`;
33975
+ let state = autoShareStates.get(key);
33976
+ if (!state) {
33977
+ state = { behindTeams: /* @__PURE__ */ new Set(), lastCheckedAt: 0, pendingReindexes: /* @__PURE__ */ new Map() };
33978
+ autoShareStates.set(key, state);
33979
+ }
33980
+ return state;
33981
+ }
33982
+ function pendingReindexesPath(config2) {
33983
+ return (0, import_node_path2.join)(config2.agentContextHome, "share", "auto-sync-pending-reindexes.json");
33984
+ }
33985
+ async function loadPendingReindexes(config2, state) {
33986
+ const raw = await readFileIfExists(pendingReindexesPath(config2));
33987
+ if (!raw) {
33988
+ state.pendingReindexes = /* @__PURE__ */ new Map();
33989
+ return;
33990
+ }
33991
+ const parsed = parseJsonConfigObject(raw);
33992
+ if (!parsed || typeof parsed.teams !== "object" || parsed.teams === null || Array.isArray(parsed.teams)) {
33993
+ state.pendingReindexes = /* @__PURE__ */ new Map();
33994
+ return;
33995
+ }
33996
+ const pending = /* @__PURE__ */ new Map();
33997
+ for (const [team, value] of Object.entries(parsed.teams)) {
33998
+ if (!Array.isArray(value)) {
33999
+ continue;
34000
+ }
34001
+ const changes = [];
34002
+ for (const item of value) {
34003
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
34004
+ continue;
34005
+ }
34006
+ const entry = item;
34007
+ if (typeof entry.path === "string" && typeof entry.relativePath === "string" && (entry.status === "added" || entry.status === "removed" || entry.status === "modified")) {
34008
+ changes.push({ path: entry.path, relativePath: entry.relativePath, status: entry.status });
34009
+ }
34010
+ }
34011
+ if (changes.length > 0) {
34012
+ pending.set(team, changes);
34013
+ }
34014
+ }
34015
+ state.pendingReindexes = pending;
34016
+ }
34017
+ async function writePendingReindexes(config2, state) {
34018
+ const path = pendingReindexesPath(config2);
34019
+ if (state.pendingReindexes.size === 0) {
34020
+ await (0, import_promises3.rm)(path, { force: true });
34021
+ return;
34022
+ }
34023
+ const contents = {
34024
+ teams: Object.fromEntries(state.pendingReindexes),
34025
+ version: 1
34026
+ };
34027
+ await (0, import_promises3.mkdir)((0, import_node_path2.dirname)(path), { recursive: true });
34028
+ const tempPath = `${path}.${process.pid}.tmp`;
34029
+ await (0, import_promises3.writeFile)(tempPath, `${JSON.stringify(contents, void 0, 2)}
34030
+ `, { encoding: "utf8", mode: 384 });
34031
+ await (0, import_promises3.rename)(tempPath, path);
34032
+ }
34033
+ async function refreshShareUpdateState(config2, options) {
34034
+ const state = autoShareState(config2);
34035
+ const warnings = await enqueueShareOperation(
34036
+ state,
34037
+ async () => refreshShareUpdateStateLocked(config2, state, options)
34038
+ );
34039
+ for (const warning of warnings) {
34040
+ console.error(warning);
34041
+ }
34042
+ }
34043
+ async function refreshShareUpdateStateLocked(config2, state, options) {
34044
+ const now = Date.now();
34045
+ if (!options.force && state.lastCheckedAt > 0 && now - state.lastCheckedAt < AUTO_SHARE_FETCH_INTERVAL_MS) {
34046
+ return [];
34047
+ }
34048
+ try {
34049
+ const statuses = await fetchShareUpdateStatuses(config2);
34050
+ const nextBehindTeams = new Set(state.behindTeams);
34051
+ for (const status of statuses) {
34052
+ if (status.warning) {
34053
+ continue;
34054
+ }
34055
+ if (status.behind > 0) {
34056
+ nextBehindTeams.add(status.team);
34057
+ } else {
34058
+ nextBehindTeams.delete(status.team);
34059
+ }
34060
+ }
34061
+ state.behindTeams = nextBehindTeams;
34062
+ return statuses.flatMap((status) => status.warning ? [status.warning] : []);
34063
+ } finally {
34064
+ state.lastCheckedAt = Date.now();
34065
+ }
34066
+ }
34067
+ function enqueueShareOperation(state, action) {
34068
+ const previous = state.operationPromise ?? Promise.resolve();
34069
+ const current = previous.catch(() => void 0).then(action);
34070
+ state.operationPromise = current.catch(() => void 0);
34071
+ return current;
34072
+ }
34073
+ async function fetchShareUpdateStatuses(config2) {
34074
+ const teamsFile = await readTeamsFile(config2);
34075
+ const teams = Object.entries(teamsFile.teams);
34076
+ if (teams.length === 0) {
34077
+ return [];
34078
+ }
34079
+ const git = await requiredExecutable("git");
34080
+ const statuses = [];
34081
+ for (const [name, team] of teams) {
34082
+ const fetchResult = await runCommand(git, ["-C", team.worktree, "fetch", DEFAULT_GIT_REMOTE_NAME], {
34083
+ allowFailure: true
34084
+ });
34085
+ if (fetchResult.exitCode !== 0) {
34086
+ statuses.push({
34087
+ behind: 0,
34088
+ team: name,
34089
+ warning: `Auto-sync check for shared team "${name}" failed: ${fetchResult.stderr.trim() || fetchResult.stdout.trim() || "unknown git fetch error"}`
34090
+ });
34091
+ continue;
34092
+ }
34093
+ const behind = await gitOutput(team.worktree, ["rev-list", "--count", "HEAD..@{u}"], false);
34094
+ if (behind === void 0) {
34095
+ statuses.push({
34096
+ behind: 0,
34097
+ team: name,
34098
+ warning: `Auto-sync check for shared team "${name}" failed: could not read upstream behind count.`
34099
+ });
34100
+ continue;
34101
+ }
34102
+ statuses.push({ behind: Number.parseInt(behind, 10) || 0, team: name });
34103
+ }
34104
+ return statuses;
34105
+ }
34106
+ async function runShareSyncQuiet(config2, state, options) {
34107
+ const team = await resolveTeam(config2, options.team);
34108
+ const git = await requiredExecutable("git");
34109
+ const worktree = team.config.worktree;
34110
+ const pendingChanges = state.pendingReindexes.get(team.name);
34111
+ if (pendingChanges) {
34112
+ await applyChangesToOpenViking(config2, team.config, pendingChanges, { quiet: true });
34113
+ state.pendingReindexes.delete(team.name);
34114
+ await writePendingReindexes(config2, state);
34115
+ }
34116
+ if (await hasUncommittedChanges(worktree)) {
34117
+ return `Shared team "${team.name}" has uncommitted changes; skipped automatic sync. Run \`threadnote share sync --team ${team.name}\` to publish or resolve them.`;
34118
+ }
34119
+ const ahead = await gitOutput(worktree, ["rev-list", "--count", "@{u}..HEAD"], false);
34120
+ if (ahead === void 0) {
34121
+ return `Shared team "${team.name}" upstream status is unknown; skipped automatic sync. Run \`threadnote share sync --team ${team.name}\` to inspect and resolve it.`;
34122
+ }
34123
+ if ((Number.parseInt(ahead, 10) || 0) > 0) {
34124
+ return `Shared team "${team.name}" has local commits ahead of upstream; skipped automatic sync. Run \`threadnote share sync --team ${team.name}\` to publish or reconcile them.`;
34125
+ }
34126
+ const beforeRev = await gitOutput(worktree, ["rev-parse", "HEAD"], false);
34127
+ const pullResult = await runCommand(git, ["-C", worktree, "rebase", "@{u}"], { allowFailure: true });
34128
+ if (pullResult.exitCode !== 0) {
34129
+ if (await exists((0, import_node_path2.join)(team.config.gitdir, "rebase-merge")) || await exists((0, import_node_path2.join)(team.config.gitdir, "rebase-apply"))) {
34130
+ throw new Error(
34131
+ `Automatic share sync hit git conflicts in ${worktree}. Resolve them in-place, run \`git -C ${worktree} rebase --continue\` (or --abort), then rerun recall/read.`
34132
+ );
34133
+ }
34134
+ throw new Error(
34135
+ `Automatic share sync failed in ${worktree}: ${pullResult.stderr.trim() || pullResult.stdout.trim() || "unknown error"}`
34136
+ );
34137
+ }
34138
+ const afterRev = await gitOutput(worktree, ["rev-parse", "HEAD"], false);
34139
+ if (beforeRev && afterRev && beforeRev !== afterRev) {
34140
+ const changes = await listChangedFiles(worktree, beforeRev, afterRev);
34141
+ if (changes.length > 0) {
34142
+ state.pendingReindexes.set(team.name, changes);
34143
+ await writePendingReindexes(config2, state);
34144
+ await applyChangesToOpenViking(config2, team.config, changes, { quiet: true });
34145
+ state.pendingReindexes.delete(team.name);
34146
+ await writePendingReindexes(config2, state);
34147
+ }
34148
+ }
34149
+ return void 0;
34150
+ }
33889
34151
  function normalizeTeamName(input) {
33890
34152
  const candidate = (input ?? "default").trim();
33891
34153
  if (!candidate) {
@@ -33970,6 +34232,10 @@ async function resolveTeam(config2, requested) {
33970
34232
  }
33971
34233
  return { config: found, name: wantName };
33972
34234
  }
34235
+ function workfileToVikingUri(config2, team, filePath) {
34236
+ const rel = (0, import_node_path2.relative)(team.worktree, filePath).split(import_node_path2.sep).join("/");
34237
+ return `viking://user/${uriSegment(config2.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
34238
+ }
33973
34239
  function isInSharedNamespace(config2, uri) {
33974
34240
  return uri.startsWith(`viking://user/${uriSegment(config2.user)}/memories/${SHARED_SEGMENT}/`);
33975
34241
  }
@@ -34009,19 +34275,25 @@ function stripPersonalProvenance(content) {
34009
34275
  }
34010
34276
  return cleaned.join("\n");
34011
34277
  }
34012
- async function ensureSharedDirectoryChain(config2, ov, memoryUri, dryRun) {
34278
+ async function ensureSharedDirectoryChain(config2, ov, memoryUri, dryRun, options = {}) {
34013
34279
  const directoryUri = parentUri(memoryUri);
34014
34280
  for (const uri of sharedDirectoryChain(config2, directoryUri)) {
34015
34281
  const args = withIdentity(config2, ["stat", uri]);
34016
34282
  if (dryRun) {
34017
- console.log(`Would run: ${formatShellCommand(ov, args)}`);
34283
+ if (options.quiet !== true) {
34284
+ console.log(`Would run: ${formatShellCommand(ov, args)}`);
34285
+ }
34018
34286
  continue;
34019
34287
  }
34020
34288
  const statResult = await runCommand(ov, args, { allowFailure: true });
34021
34289
  if (statResult.exitCode === 0) {
34022
34290
  continue;
34023
34291
  }
34024
- await maybeRun(false, ov, withIdentity(config2, ["mkdir", uri, "--description", "Threadnote shared memories."]));
34292
+ if (options.quiet === true) {
34293
+ await runCommand(ov, withIdentity(config2, ["mkdir", uri, "--description", "Threadnote shared memories."]));
34294
+ } else {
34295
+ await maybeRun(false, ov, withIdentity(config2, ["mkdir", uri, "--description", "Threadnote shared memories."]));
34296
+ }
34025
34297
  }
34026
34298
  }
34027
34299
  function parentUri(uri) {
@@ -34040,7 +34312,7 @@ function sharedDirectoryChain(config2, directoryUri) {
34040
34312
  }
34041
34313
  return chain;
34042
34314
  }
34043
- async function writeMemoryFile(config2, ov, uri, content, initialMode, dryRun) {
34315
+ async function writeMemoryFile(config2, ov, uri, content, initialMode, dryRun, options = {}) {
34044
34316
  if (dryRun) {
34045
34317
  const args = withIdentity(config2, [
34046
34318
  "write",
@@ -34053,19 +34325,21 @@ async function writeMemoryFile(config2, ov, uri, content, initialMode, dryRun) {
34053
34325
  "--timeout",
34054
34326
  "120"
34055
34327
  ]);
34056
- console.log(`Would run: ${formatShellCommand(ov, args)}`);
34328
+ if (options.quiet !== true) {
34329
+ console.log(`Would run: ${formatShellCommand(ov, args)}`);
34330
+ }
34057
34331
  return;
34058
34332
  }
34059
34333
  const stagingDir = await (0, import_promises3.mkdtemp)((0, import_node_path2.join)((0, import_node_os2.tmpdir)(), "threadnote-share-"));
34060
34334
  const tempPath = (0, import_node_path2.join)(stagingDir, "body.txt");
34061
34335
  try {
34062
34336
  await (0, import_promises3.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
34063
- await writeOvFileWithRetry(config2, ov, uri, tempPath, initialMode);
34337
+ await writeOvFileWithRetry(config2, ov, uri, tempPath, initialMode, options);
34064
34338
  } finally {
34065
34339
  await (0, import_promises3.rm)(stagingDir, { force: true, recursive: true });
34066
34340
  }
34067
34341
  }
34068
- async function writeOvFileWithRetry(config2, ov, uri, fromFile, initialMode) {
34342
+ async function writeOvFileWithRetry(config2, ov, uri, fromFile, initialMode, options = {}) {
34069
34343
  const maxAttempts = 4;
34070
34344
  const existedBeforeWrite = await vikingResourceExists(ov, config2, uri);
34071
34345
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
@@ -34083,20 +34357,26 @@ async function writeOvFileWithRetry(config2, ov, uri, fromFile, initialMode) {
34083
34357
  "--timeout",
34084
34358
  "120"
34085
34359
  ]);
34086
- console.log(`${attempt === 0 ? "Running" : "Retrying"}: ${formatShellCommand(ov, args)}`);
34360
+ if (options.quiet !== true) {
34361
+ console.log(`${attempt === 0 ? "Running" : "Retrying"}: ${formatShellCommand(ov, args)}`);
34362
+ }
34087
34363
  const result = await runCommand(ov, args, { allowFailure: true });
34088
34364
  if (result.exitCode === 0) {
34089
- if (result.stdout.trim()) {
34365
+ if (options.quiet !== true && result.stdout.trim()) {
34090
34366
  console.log(result.stdout.trim());
34091
34367
  }
34092
- if (result.stderr.trim()) {
34368
+ if (options.quiet !== true && result.stderr.trim()) {
34093
34369
  console.error(result.stderr.trim());
34094
34370
  }
34095
34371
  return;
34096
34372
  }
34097
34373
  if (isTransientOvFailure(result.stderr, result.stdout) && await vikingResourceExists(ov, config2, uri) && !existedBeforeWrite) {
34098
- console.log("OpenViking accepted the write but returned an error before the wait completed; draining the queue.");
34099
- await waitForOvQueue(ov, config2);
34374
+ if (options.quiet !== true) {
34375
+ console.log(
34376
+ "OpenViking accepted the write but returned an error before the wait completed; draining the queue."
34377
+ );
34378
+ }
34379
+ await waitForOvQueue(ov, config2, options);
34100
34380
  return;
34101
34381
  }
34102
34382
  if (!isTransientOvFailure(result.stderr, result.stdout) || attempt === maxAttempts - 1) {
@@ -34105,12 +34385,12 @@ async function writeOvFileWithRetry(config2, ov, uri, fromFile, initialMode) {
34105
34385
  await sleep(1e3 * (attempt + 1));
34106
34386
  }
34107
34387
  }
34108
- async function waitForOvQueue(ov, config2) {
34388
+ async function waitForOvQueue(ov, config2, options = {}) {
34109
34389
  const result = await runCommand(ov, withIdentity(config2, ["wait", "--timeout", "120"]), { allowFailure: true });
34110
- if (result.stdout.trim()) {
34390
+ if (options.quiet !== true && result.stdout.trim()) {
34111
34391
  console.log(result.stdout.trim());
34112
34392
  }
34113
- if (result.stderr.trim()) {
34393
+ if (options.quiet !== true && result.stderr.trim()) {
34114
34394
  console.error(result.stderr.trim());
34115
34395
  }
34116
34396
  }
@@ -34119,6 +34399,10 @@ function isTransientOvFailure(stderr, stdout) {
34119
34399
  ${stdout}`.toLowerCase();
34120
34400
  return output.includes("resource is busy") || output.includes("resource is being processed") || output.includes("network error") || output.includes("error sending request") || output.includes("http request failed") || output.includes("connection refused") || output.includes("connection reset") || output.includes("timed out");
34121
34401
  }
34402
+ async function ingestSingleFile(ov, config2, uri, filePath, initialMode, options = {}) {
34403
+ const content = await (0, import_promises3.readFile)(filePath, "utf8");
34404
+ await writeMemoryFile(config2, ov, uri, content, initialMode, false, options);
34405
+ }
34122
34406
  async function removeWithRollback(config2, ov, sourceUri, rollbackUri, worktree, dryRun, label) {
34123
34407
  try {
34124
34408
  await removeMemoryUri(config2, ov, sourceUri, dryRun);
@@ -34160,17 +34444,19 @@ async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
34160
34444
  }
34161
34445
  await (0, import_promises3.rm)((0, import_node_path2.join)(worktree, relative2), { force: true });
34162
34446
  }
34163
- async function removeMemoryUri(config2, ov, uri, dryRun) {
34447
+ async function removeMemoryUri(config2, ov, uri, dryRun, options = {}) {
34164
34448
  const args = withIdentity(config2, ["rm", uri]);
34165
34449
  if (dryRun) {
34166
- console.log(`Would run: ${formatShellCommand(ov, args)}`);
34450
+ if (options.quiet !== true) {
34451
+ console.log(`Would run: ${formatShellCommand(ov, args)}`);
34452
+ }
34167
34453
  return;
34168
34454
  }
34169
34455
  const maxAttempts = 4;
34170
34456
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
34171
34457
  const result = await runCommand(ov, args, { allowFailure: true });
34172
34458
  if (result.exitCode === 0) {
34173
- if (result.stdout.trim()) {
34459
+ if (options.quiet !== true && result.stdout.trim()) {
34174
34460
  console.log(result.stdout.trim());
34175
34461
  }
34176
34462
  return;
@@ -34185,6 +34471,81 @@ async function vikingResourceExists(ov, config2, uri) {
34185
34471
  const result = await runCommand(ov, withIdentity(config2, ["stat", uri]), { allowFailure: true });
34186
34472
  return result.exitCode === 0;
34187
34473
  }
34474
+ async function hasUncommittedChanges(worktree) {
34475
+ const result = await runCommand("git", ["-C", worktree, "status", "--porcelain"], { allowFailure: true });
34476
+ return result.stdout.trim().length > 0;
34477
+ }
34478
+ async function gitOutput(worktree, args, dryRun) {
34479
+ if (dryRun) {
34480
+ return void 0;
34481
+ }
34482
+ const result = await runCommand("git", ["-C", worktree, ...args], { allowFailure: true });
34483
+ if (result.exitCode !== 0) {
34484
+ return void 0;
34485
+ }
34486
+ return result.stdout.trim();
34487
+ }
34488
+ async function listChangedFiles(worktree, beforeRev, afterRev) {
34489
+ const result = await runCommand("git", ["-C", worktree, "diff", "--name-status", "-z", `${beforeRev}..${afterRev}`], {
34490
+ allowFailure: true
34491
+ });
34492
+ if (result.exitCode !== 0) {
34493
+ return [];
34494
+ }
34495
+ const entries = result.stdout.split("\0").filter((part) => part.length > 0);
34496
+ const changes = [];
34497
+ for (let index = 0; index < entries.length; ) {
34498
+ const raw = entries[index];
34499
+ const head = raw.slice(0, 1);
34500
+ if (head === "R" || head === "C") {
34501
+ const oldRel = entries[index + 1];
34502
+ const newRel = entries[index + 2];
34503
+ if (oldRel && newRel) {
34504
+ changes.push({ path: (0, import_node_path2.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
34505
+ changes.push({ path: (0, import_node_path2.join)(worktree, newRel), relativePath: newRel, status: "added" });
34506
+ }
34507
+ index += 3;
34508
+ continue;
34509
+ }
34510
+ const rel = entries[index + 1];
34511
+ if (rel) {
34512
+ const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
34513
+ changes.push({ path: (0, import_node_path2.join)(worktree, rel), relativePath: rel, status });
34514
+ }
34515
+ index += 2;
34516
+ }
34517
+ return changes;
34518
+ }
34519
+ async function applyChangesToOpenViking(config2, team, changes, options = {}) {
34520
+ const ov = await openVikingCliForMode(false);
34521
+ for (const change of changes) {
34522
+ if (!change.relativePath.endsWith(".md")) {
34523
+ continue;
34524
+ }
34525
+ const firstSegment = change.relativePath.split("/")[0];
34526
+ if (!SHAREABLE_MEMORY_KIND_DIRS.includes(firstSegment)) {
34527
+ continue;
34528
+ }
34529
+ const uri = workfileToVikingUri(config2, team, change.path);
34530
+ if (change.status === "removed") {
34531
+ await removeMemoryUri(config2, ov, uri, false, options);
34532
+ continue;
34533
+ }
34534
+ if (!await isFile(change.path)) {
34535
+ continue;
34536
+ }
34537
+ const ovHasResource = await vikingResourceExists(ov, config2, uri);
34538
+ if (ovHasResource) {
34539
+ const reason = change.status === "modified" ? "overwriting local with upstream (local edits to the shared subtree are not preserved across sync)" : "aligning OV to upstream (resource pre-existed in OV, likely from an earlier local publish or sync)";
34540
+ if (options.quiet !== true) {
34541
+ console.warn(`share sync: ${uri}: ${reason}.`);
34542
+ }
34543
+ }
34544
+ await ensureSharedDirectoryChain(config2, ov, uri, false, options);
34545
+ const writeMode = ovHasResource ? "replace" : "create";
34546
+ await ingestSingleFile(ov, config2, uri, change.path, writeMode, options);
34547
+ }
34548
+ }
34188
34549
 
34189
34550
  // src/mcp_server.ts
34190
34551
  async function main() {
@@ -34210,6 +34571,7 @@ async function main() {
34210
34571
  }
34211
34572
  );
34212
34573
  registerTools(server, config2);
34574
+ startShareBackgroundFetch(config2);
34213
34575
  await server.connect(new StdioServerTransport());
34214
34576
  process.stderr.write("Threadnote local MCP adapter running\n");
34215
34577
  }
@@ -34273,7 +34635,7 @@ function registerTools(server, config2) {
34273
34635
  return checkedUri.error;
34274
34636
  }
34275
34637
  try {
34276
- const ov = await requiredOpenVikingCli();
34638
+ const ov = await requiredOpenVikingCli2();
34277
34639
  const removed = await removeVikingResourceWithRetry(ov, config2, checkedUri.value);
34278
34640
  return {
34279
34641
  content: [
@@ -34443,6 +34805,15 @@ function registerSearchTool(server, config2, name, description) {
34443
34805
  );
34444
34806
  }
34445
34807
  async function runRecallTool(config2, params) {
34808
+ let syncedTeams = [];
34809
+ const syncWarnings = [];
34810
+ try {
34811
+ const syncResult = await syncSharedReposBeforeAgentRead(config2);
34812
+ syncedTeams = syncResult.syncedTeams;
34813
+ syncWarnings.push(...syncResult.warnings);
34814
+ } catch (err) {
34815
+ syncWarnings.push(errorMessage(err));
34816
+ }
34446
34817
  const baseArgs = [
34447
34818
  "search",
34448
34819
  params.query,
@@ -34470,6 +34841,12 @@ async function runRecallTool(config2, params) {
34470
34841
  sections.push(`Exact durable memory matches:
34471
34842
  ${exactMatches}`);
34472
34843
  }
34844
+ if (syncedTeams.length > 0) {
34845
+ sections.push(`Auto-synced shared memories: ${syncedTeams.join(", ")}`);
34846
+ }
34847
+ for (const warning of syncWarnings) {
34848
+ sections.push(`Auto-sync warning: ${warning}`);
34849
+ }
34473
34850
  if (sections.length <= 1) {
34474
34851
  return semanticResult;
34475
34852
  }
@@ -34487,7 +34864,7 @@ async function seededResourcesSection(config2, params) {
34487
34864
  if (!projectResourceUri.startsWith("viking://")) {
34488
34865
  return void 0;
34489
34866
  }
34490
- const ov = await requiredOpenVikingCli();
34867
+ const ov = await requiredOpenVikingCli2();
34491
34868
  const args = [
34492
34869
  "search",
34493
34870
  params.query,
@@ -34511,7 +34888,7 @@ async function exactMemoryMatchesText(config2, query, includeArchived) {
34511
34888
  if (terms.length === 0) {
34512
34889
  return void 0;
34513
34890
  }
34514
- const ov = await requiredOpenVikingCli();
34891
+ const ov = await requiredOpenVikingCli2();
34515
34892
  const scopes = exactMemoryScopes(config2, includeArchived);
34516
34893
  const outputs = [];
34517
34894
  for (const term of terms) {
@@ -34542,7 +34919,27 @@ function registerReadTool(server, config2, name, description) {
34542
34919
  if (!checkedUri.ok) {
34543
34920
  return checkedUri.error;
34544
34921
  }
34545
- return runOpenVikingTool(config2, ["read", checkedUri.value]);
34922
+ let syncedTeams = [];
34923
+ const syncWarnings = [];
34924
+ try {
34925
+ const syncResult = await syncSharedReposBeforeAgentRead(config2);
34926
+ syncedTeams = syncResult.syncedTeams;
34927
+ syncWarnings.push(...syncResult.warnings);
34928
+ } catch (err) {
34929
+ syncWarnings.push(errorMessage(err));
34930
+ }
34931
+ const result = await runOpenVikingTool(config2, ["read", checkedUri.value]);
34932
+ if (result.isError === true || syncedTeams.length === 0 && syncWarnings.length === 0) {
34933
+ return result;
34934
+ }
34935
+ const syncMessages = [
34936
+ syncedTeams.length > 0 ? `Auto-synced shared memories: ${syncedTeams.join(", ")}` : void 0,
34937
+ ...syncWarnings.map((warning) => `Auto-sync warning: ${warning}`)
34938
+ ].filter((part) => part !== void 0);
34939
+ return {
34940
+ ...result,
34941
+ content: [...result.content, { type: "text", text: syncMessages.join("\n") }]
34942
+ };
34546
34943
  }
34547
34944
  );
34548
34945
  }
@@ -34636,7 +35033,7 @@ function registerArchiveTool(server, config2, name, description) {
34636
35033
  return checkedUri.error;
34637
35034
  }
34638
35035
  try {
34639
- const ov = await requiredOpenVikingCli();
35036
+ const ov = await requiredOpenVikingCli2();
34640
35037
  const readResult = await runCommand(ov, withIdentity2(config2, ["read", checkedUri.value]));
34641
35038
  const original = readResult.stdout.trim();
34642
35039
  if (!original) {
@@ -34682,7 +35079,7 @@ Archive stored, but original memory is still processing. Retry later with forget
34682
35079
  }
34683
35080
  async function writeDurableMemory(config2, params) {
34684
35081
  try {
34685
- const ov = await requiredOpenVikingCli();
35082
+ const ov = await requiredOpenVikingCli2();
34686
35083
  const directoryUri = memoryDirectoryUri(config2, params.metadata);
34687
35084
  await ensureMemoryDirectory(ov, config2, directoryUri);
34688
35085
  const candidateMetadata = { ...params.metadata, supersedes: params.replaceUri };
@@ -34908,7 +35305,7 @@ function argumentError(text) {
34908
35305
  }
34909
35306
  async function runOpenVikingTool(config2, args) {
34910
35307
  try {
34911
- const ov = await requiredOpenVikingCli();
35308
+ const ov = await requiredOpenVikingCli2();
34912
35309
  const result = await runCommand(ov, withIdentity2(config2, args));
34913
35310
  const text = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
34914
35311
  return { content: [{ type: "text", text: text || "OK" }] };
@@ -34922,7 +35319,7 @@ async function runSharePublishTool(config2, sourceUri, options) {
34922
35319
  return argumentError(`Memory ${sourceUri} is already in the shared namespace.`);
34923
35320
  }
34924
35321
  const resolved = await resolveTeam(config2, options.team);
34925
- const ov = await requiredOpenVikingCli();
35322
+ const ov = await requiredOpenVikingCli2();
34926
35323
  const readResult = await runCommand(ov, withIdentity2(config2, ["read", sourceUri]), { allowFailure: true });
34927
35324
  if (readResult.exitCode !== 0 || !readResult.stdout.trim()) {
34928
35325
  return {
@@ -35040,7 +35437,7 @@ async function gitPublishWorkflow(worktree, relativePath, commitMessage, push) {
35040
35437
  function withIdentity2(config2, args) {
35041
35438
  return [...args, "--account", config2.account, "--user", config2.user, "--agent-id", config2.agentId];
35042
35439
  }
35043
- async function requiredOpenVikingCli() {
35440
+ async function requiredOpenVikingCli2() {
35044
35441
  const command = process.env.THREADNOTE_OV ?? await findExecutable(["ov", "openviking"]) ?? await firstExistingPath([(0, import_node_path3.join)((0, import_node_os3.homedir)(), ".local", "bin", "ov"), (0, import_node_path3.join)((0, import_node_os3.homedir)(), ".local", "bin", "openviking")]);
35045
35442
  if (!command) {
35046
35443
  throw new Error("Neither ov nor openviking was found. Run threadnote install first.");