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 +7 -3
- package/dist/mcp_server.cjs +424 -27
- package/dist/threadnote.cjs +2387 -2078
- package/docs/agent-instructions.md +3 -2
- package/docs/index.html +10 -8
- package/docs/share.md +15 -2
- package/package.json +1 -1
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
|
|
354
|
-
|
|
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.
|
|
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.
|
package/dist/mcp_server.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34099
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.");
|