threadnote 0.6.3 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/mcp_server.cjs +439 -29
- package/dist/threadnote.cjs +2362 -2111
- 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/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,34 +34357,44 @@ 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) {
|
|
34103
34383
|
throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
|
|
34104
34384
|
}
|
|
34105
|
-
|
|
34385
|
+
if (isResourceBusyFailure(result.stderr, result.stdout)) {
|
|
34386
|
+
await waitForOvQueue(ov, config2, options);
|
|
34387
|
+
} else {
|
|
34388
|
+
await sleep(1e3 * (attempt + 1));
|
|
34389
|
+
}
|
|
34106
34390
|
}
|
|
34107
34391
|
}
|
|
34108
|
-
async function waitForOvQueue(ov, config2) {
|
|
34392
|
+
async function waitForOvQueue(ov, config2, options = {}) {
|
|
34109
34393
|
const result = await runCommand(ov, withIdentity(config2, ["wait", "--timeout", "120"]), { allowFailure: true });
|
|
34110
|
-
if (result.stdout.trim()) {
|
|
34394
|
+
if (options.quiet !== true && result.stdout.trim()) {
|
|
34111
34395
|
console.log(result.stdout.trim());
|
|
34112
34396
|
}
|
|
34113
|
-
if (result.stderr.trim()) {
|
|
34397
|
+
if (options.quiet !== true && result.stderr.trim()) {
|
|
34114
34398
|
console.error(result.stderr.trim());
|
|
34115
34399
|
}
|
|
34116
34400
|
}
|
|
@@ -34119,6 +34403,15 @@ function isTransientOvFailure(stderr, stdout) {
|
|
|
34119
34403
|
${stdout}`.toLowerCase();
|
|
34120
34404
|
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
34405
|
}
|
|
34406
|
+
function isResourceBusyFailure(stderr, stdout) {
|
|
34407
|
+
const output = `${stderr}
|
|
34408
|
+
${stdout}`.toLowerCase();
|
|
34409
|
+
return output.includes("resource is busy") || output.includes("resource is being processed");
|
|
34410
|
+
}
|
|
34411
|
+
async function ingestSingleFile(ov, config2, uri, filePath, initialMode, options = {}) {
|
|
34412
|
+
const content = await (0, import_promises3.readFile)(filePath, "utf8");
|
|
34413
|
+
await writeMemoryFile(config2, ov, uri, content, initialMode, false, options);
|
|
34414
|
+
}
|
|
34122
34415
|
async function removeWithRollback(config2, ov, sourceUri, rollbackUri, worktree, dryRun, label) {
|
|
34123
34416
|
try {
|
|
34124
34417
|
await removeMemoryUri(config2, ov, sourceUri, dryRun);
|
|
@@ -34160,17 +34453,19 @@ async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
|
|
|
34160
34453
|
}
|
|
34161
34454
|
await (0, import_promises3.rm)((0, import_node_path2.join)(worktree, relative2), { force: true });
|
|
34162
34455
|
}
|
|
34163
|
-
async function removeMemoryUri(config2, ov, uri, dryRun) {
|
|
34456
|
+
async function removeMemoryUri(config2, ov, uri, dryRun, options = {}) {
|
|
34164
34457
|
const args = withIdentity(config2, ["rm", uri]);
|
|
34165
34458
|
if (dryRun) {
|
|
34166
|
-
|
|
34459
|
+
if (options.quiet !== true) {
|
|
34460
|
+
console.log(`Would run: ${formatShellCommand(ov, args)}`);
|
|
34461
|
+
}
|
|
34167
34462
|
return;
|
|
34168
34463
|
}
|
|
34169
34464
|
const maxAttempts = 4;
|
|
34170
34465
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
34171
34466
|
const result = await runCommand(ov, args, { allowFailure: true });
|
|
34172
34467
|
if (result.exitCode === 0) {
|
|
34173
|
-
if (result.stdout.trim()) {
|
|
34468
|
+
if (options.quiet !== true && result.stdout.trim()) {
|
|
34174
34469
|
console.log(result.stdout.trim());
|
|
34175
34470
|
}
|
|
34176
34471
|
return;
|
|
@@ -34178,13 +34473,92 @@ async function removeMemoryUri(config2, ov, uri, dryRun) {
|
|
|
34178
34473
|
if (!isTransientOvFailure(result.stderr, result.stdout) || attempt === maxAttempts - 1) {
|
|
34179
34474
|
throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
|
|
34180
34475
|
}
|
|
34181
|
-
|
|
34476
|
+
if (isResourceBusyFailure(result.stderr, result.stdout)) {
|
|
34477
|
+
await waitForOvQueue(ov, config2, options);
|
|
34478
|
+
} else {
|
|
34479
|
+
await sleep(1e3 * (attempt + 1));
|
|
34480
|
+
}
|
|
34182
34481
|
}
|
|
34183
34482
|
}
|
|
34184
34483
|
async function vikingResourceExists(ov, config2, uri) {
|
|
34185
34484
|
const result = await runCommand(ov, withIdentity(config2, ["stat", uri]), { allowFailure: true });
|
|
34186
34485
|
return result.exitCode === 0;
|
|
34187
34486
|
}
|
|
34487
|
+
async function hasUncommittedChanges(worktree) {
|
|
34488
|
+
const result = await runCommand("git", ["-C", worktree, "status", "--porcelain"], { allowFailure: true });
|
|
34489
|
+
return result.stdout.trim().length > 0;
|
|
34490
|
+
}
|
|
34491
|
+
async function gitOutput(worktree, args, dryRun) {
|
|
34492
|
+
if (dryRun) {
|
|
34493
|
+
return void 0;
|
|
34494
|
+
}
|
|
34495
|
+
const result = await runCommand("git", ["-C", worktree, ...args], { allowFailure: true });
|
|
34496
|
+
if (result.exitCode !== 0) {
|
|
34497
|
+
return void 0;
|
|
34498
|
+
}
|
|
34499
|
+
return result.stdout.trim();
|
|
34500
|
+
}
|
|
34501
|
+
async function listChangedFiles(worktree, beforeRev, afterRev) {
|
|
34502
|
+
const result = await runCommand("git", ["-C", worktree, "diff", "--name-status", "-z", `${beforeRev}..${afterRev}`], {
|
|
34503
|
+
allowFailure: true
|
|
34504
|
+
});
|
|
34505
|
+
if (result.exitCode !== 0) {
|
|
34506
|
+
return [];
|
|
34507
|
+
}
|
|
34508
|
+
const entries = result.stdout.split("\0").filter((part) => part.length > 0);
|
|
34509
|
+
const changes = [];
|
|
34510
|
+
for (let index = 0; index < entries.length; ) {
|
|
34511
|
+
const raw = entries[index];
|
|
34512
|
+
const head = raw.slice(0, 1);
|
|
34513
|
+
if (head === "R" || head === "C") {
|
|
34514
|
+
const oldRel = entries[index + 1];
|
|
34515
|
+
const newRel = entries[index + 2];
|
|
34516
|
+
if (oldRel && newRel) {
|
|
34517
|
+
changes.push({ path: (0, import_node_path2.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
|
|
34518
|
+
changes.push({ path: (0, import_node_path2.join)(worktree, newRel), relativePath: newRel, status: "added" });
|
|
34519
|
+
}
|
|
34520
|
+
index += 3;
|
|
34521
|
+
continue;
|
|
34522
|
+
}
|
|
34523
|
+
const rel = entries[index + 1];
|
|
34524
|
+
if (rel) {
|
|
34525
|
+
const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
|
|
34526
|
+
changes.push({ path: (0, import_node_path2.join)(worktree, rel), relativePath: rel, status });
|
|
34527
|
+
}
|
|
34528
|
+
index += 2;
|
|
34529
|
+
}
|
|
34530
|
+
return changes;
|
|
34531
|
+
}
|
|
34532
|
+
async function applyChangesToOpenViking(config2, team, changes, options = {}) {
|
|
34533
|
+
const ov = await openVikingCliForMode(false);
|
|
34534
|
+
for (const change of changes) {
|
|
34535
|
+
if (!change.relativePath.endsWith(".md")) {
|
|
34536
|
+
continue;
|
|
34537
|
+
}
|
|
34538
|
+
const firstSegment = change.relativePath.split("/")[0];
|
|
34539
|
+
if (!SHAREABLE_MEMORY_KIND_DIRS.includes(firstSegment)) {
|
|
34540
|
+
continue;
|
|
34541
|
+
}
|
|
34542
|
+
const uri = workfileToVikingUri(config2, team, change.path);
|
|
34543
|
+
if (change.status === "removed") {
|
|
34544
|
+
await removeMemoryUri(config2, ov, uri, false, options);
|
|
34545
|
+
continue;
|
|
34546
|
+
}
|
|
34547
|
+
if (!await isFile(change.path)) {
|
|
34548
|
+
continue;
|
|
34549
|
+
}
|
|
34550
|
+
const ovHasResource = await vikingResourceExists(ov, config2, uri);
|
|
34551
|
+
if (ovHasResource) {
|
|
34552
|
+
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)";
|
|
34553
|
+
if (options.quiet !== true) {
|
|
34554
|
+
console.warn(`share sync: ${uri}: ${reason}.`);
|
|
34555
|
+
}
|
|
34556
|
+
}
|
|
34557
|
+
await ensureSharedDirectoryChain(config2, ov, uri, false, options);
|
|
34558
|
+
const writeMode = ovHasResource ? "replace" : "create";
|
|
34559
|
+
await ingestSingleFile(ov, config2, uri, change.path, writeMode, options);
|
|
34560
|
+
}
|
|
34561
|
+
}
|
|
34188
34562
|
|
|
34189
34563
|
// src/mcp_server.ts
|
|
34190
34564
|
async function main() {
|
|
@@ -34210,6 +34584,7 @@ async function main() {
|
|
|
34210
34584
|
}
|
|
34211
34585
|
);
|
|
34212
34586
|
registerTools(server, config2);
|
|
34587
|
+
startShareBackgroundFetch(config2);
|
|
34213
34588
|
await server.connect(new StdioServerTransport());
|
|
34214
34589
|
process.stderr.write("Threadnote local MCP adapter running\n");
|
|
34215
34590
|
}
|
|
@@ -34273,7 +34648,7 @@ function registerTools(server, config2) {
|
|
|
34273
34648
|
return checkedUri.error;
|
|
34274
34649
|
}
|
|
34275
34650
|
try {
|
|
34276
|
-
const ov = await
|
|
34651
|
+
const ov = await requiredOpenVikingCli2();
|
|
34277
34652
|
const removed = await removeVikingResourceWithRetry(ov, config2, checkedUri.value);
|
|
34278
34653
|
return {
|
|
34279
34654
|
content: [
|
|
@@ -34443,6 +34818,15 @@ function registerSearchTool(server, config2, name, description) {
|
|
|
34443
34818
|
);
|
|
34444
34819
|
}
|
|
34445
34820
|
async function runRecallTool(config2, params) {
|
|
34821
|
+
let syncedTeams = [];
|
|
34822
|
+
const syncWarnings = [];
|
|
34823
|
+
try {
|
|
34824
|
+
const syncResult = await syncSharedReposBeforeAgentRead(config2);
|
|
34825
|
+
syncedTeams = syncResult.syncedTeams;
|
|
34826
|
+
syncWarnings.push(...syncResult.warnings);
|
|
34827
|
+
} catch (err) {
|
|
34828
|
+
syncWarnings.push(errorMessage(err));
|
|
34829
|
+
}
|
|
34446
34830
|
const baseArgs = [
|
|
34447
34831
|
"search",
|
|
34448
34832
|
params.query,
|
|
@@ -34470,6 +34854,12 @@ async function runRecallTool(config2, params) {
|
|
|
34470
34854
|
sections.push(`Exact durable memory matches:
|
|
34471
34855
|
${exactMatches}`);
|
|
34472
34856
|
}
|
|
34857
|
+
if (syncedTeams.length > 0) {
|
|
34858
|
+
sections.push(`Auto-synced shared memories: ${syncedTeams.join(", ")}`);
|
|
34859
|
+
}
|
|
34860
|
+
for (const warning of syncWarnings) {
|
|
34861
|
+
sections.push(`Auto-sync warning: ${warning}`);
|
|
34862
|
+
}
|
|
34473
34863
|
if (sections.length <= 1) {
|
|
34474
34864
|
return semanticResult;
|
|
34475
34865
|
}
|
|
@@ -34487,7 +34877,7 @@ async function seededResourcesSection(config2, params) {
|
|
|
34487
34877
|
if (!projectResourceUri.startsWith("viking://")) {
|
|
34488
34878
|
return void 0;
|
|
34489
34879
|
}
|
|
34490
|
-
const ov = await
|
|
34880
|
+
const ov = await requiredOpenVikingCli2();
|
|
34491
34881
|
const args = [
|
|
34492
34882
|
"search",
|
|
34493
34883
|
params.query,
|
|
@@ -34511,7 +34901,7 @@ async function exactMemoryMatchesText(config2, query, includeArchived) {
|
|
|
34511
34901
|
if (terms.length === 0) {
|
|
34512
34902
|
return void 0;
|
|
34513
34903
|
}
|
|
34514
|
-
const ov = await
|
|
34904
|
+
const ov = await requiredOpenVikingCli2();
|
|
34515
34905
|
const scopes = exactMemoryScopes(config2, includeArchived);
|
|
34516
34906
|
const outputs = [];
|
|
34517
34907
|
for (const term of terms) {
|
|
@@ -34542,7 +34932,27 @@ function registerReadTool(server, config2, name, description) {
|
|
|
34542
34932
|
if (!checkedUri.ok) {
|
|
34543
34933
|
return checkedUri.error;
|
|
34544
34934
|
}
|
|
34545
|
-
|
|
34935
|
+
let syncedTeams = [];
|
|
34936
|
+
const syncWarnings = [];
|
|
34937
|
+
try {
|
|
34938
|
+
const syncResult = await syncSharedReposBeforeAgentRead(config2);
|
|
34939
|
+
syncedTeams = syncResult.syncedTeams;
|
|
34940
|
+
syncWarnings.push(...syncResult.warnings);
|
|
34941
|
+
} catch (err) {
|
|
34942
|
+
syncWarnings.push(errorMessage(err));
|
|
34943
|
+
}
|
|
34944
|
+
const result = await runOpenVikingTool(config2, ["read", checkedUri.value]);
|
|
34945
|
+
if (result.isError === true || syncedTeams.length === 0 && syncWarnings.length === 0) {
|
|
34946
|
+
return result;
|
|
34947
|
+
}
|
|
34948
|
+
const syncMessages = [
|
|
34949
|
+
syncedTeams.length > 0 ? `Auto-synced shared memories: ${syncedTeams.join(", ")}` : void 0,
|
|
34950
|
+
...syncWarnings.map((warning) => `Auto-sync warning: ${warning}`)
|
|
34951
|
+
].filter((part) => part !== void 0);
|
|
34952
|
+
return {
|
|
34953
|
+
...result,
|
|
34954
|
+
content: [...result.content, { type: "text", text: syncMessages.join("\n") }]
|
|
34955
|
+
};
|
|
34546
34956
|
}
|
|
34547
34957
|
);
|
|
34548
34958
|
}
|
|
@@ -34636,7 +35046,7 @@ function registerArchiveTool(server, config2, name, description) {
|
|
|
34636
35046
|
return checkedUri.error;
|
|
34637
35047
|
}
|
|
34638
35048
|
try {
|
|
34639
|
-
const ov = await
|
|
35049
|
+
const ov = await requiredOpenVikingCli2();
|
|
34640
35050
|
const readResult = await runCommand(ov, withIdentity2(config2, ["read", checkedUri.value]));
|
|
34641
35051
|
const original = readResult.stdout.trim();
|
|
34642
35052
|
if (!original) {
|
|
@@ -34682,7 +35092,7 @@ Archive stored, but original memory is still processing. Retry later with forget
|
|
|
34682
35092
|
}
|
|
34683
35093
|
async function writeDurableMemory(config2, params) {
|
|
34684
35094
|
try {
|
|
34685
|
-
const ov = await
|
|
35095
|
+
const ov = await requiredOpenVikingCli2();
|
|
34686
35096
|
const directoryUri = memoryDirectoryUri(config2, params.metadata);
|
|
34687
35097
|
await ensureMemoryDirectory(ov, config2, directoryUri);
|
|
34688
35098
|
const candidateMetadata = { ...params.metadata, supersedes: params.replaceUri };
|
|
@@ -34908,7 +35318,7 @@ function argumentError(text) {
|
|
|
34908
35318
|
}
|
|
34909
35319
|
async function runOpenVikingTool(config2, args) {
|
|
34910
35320
|
try {
|
|
34911
|
-
const ov = await
|
|
35321
|
+
const ov = await requiredOpenVikingCli2();
|
|
34912
35322
|
const result = await runCommand(ov, withIdentity2(config2, args));
|
|
34913
35323
|
const text = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
|
|
34914
35324
|
return { content: [{ type: "text", text: text || "OK" }] };
|
|
@@ -34922,7 +35332,7 @@ async function runSharePublishTool(config2, sourceUri, options) {
|
|
|
34922
35332
|
return argumentError(`Memory ${sourceUri} is already in the shared namespace.`);
|
|
34923
35333
|
}
|
|
34924
35334
|
const resolved = await resolveTeam(config2, options.team);
|
|
34925
|
-
const ov = await
|
|
35335
|
+
const ov = await requiredOpenVikingCli2();
|
|
34926
35336
|
const readResult = await runCommand(ov, withIdentity2(config2, ["read", sourceUri]), { allowFailure: true });
|
|
34927
35337
|
if (readResult.exitCode !== 0 || !readResult.stdout.trim()) {
|
|
34928
35338
|
return {
|
|
@@ -35040,7 +35450,7 @@ async function gitPublishWorkflow(worktree, relativePath, commitMessage, push) {
|
|
|
35040
35450
|
function withIdentity2(config2, args) {
|
|
35041
35451
|
return [...args, "--account", config2.account, "--user", config2.user, "--agent-id", config2.agentId];
|
|
35042
35452
|
}
|
|
35043
|
-
async function
|
|
35453
|
+
async function requiredOpenVikingCli2() {
|
|
35044
35454
|
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
35455
|
if (!command) {
|
|
35046
35456
|
throw new Error("Neither ov nor openviking was found. Run threadnote install first.");
|