threadnote 0.7.4 → 0.7.6
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 +33 -10
- package/config/launchd/io.threadnote.openviking.plist.template +1 -1
- package/dist/mcp_server.cjs +776 -36
- package/dist/threadnote.cjs +938 -158
- package/docs/agent-instructions.md +24 -5
- package/docs/migration.md +4 -2
- package/package.json +10 -5
package/dist/mcp_server.cjs
CHANGED
|
@@ -30952,7 +30952,7 @@ var StdioServerTransport = class {
|
|
|
30952
30952
|
// src/mcp_server.ts
|
|
30953
30953
|
var import_promises4 = require("node:fs/promises");
|
|
30954
30954
|
var import_node_os3 = require("node:os");
|
|
30955
|
-
var
|
|
30955
|
+
var import_node_path4 = require("node:path");
|
|
30956
30956
|
|
|
30957
30957
|
// src/constants.ts
|
|
30958
30958
|
var DEFAULT_ACCOUNT = "local";
|
|
@@ -33684,6 +33684,13 @@ async function runCommand(executable, args, options = {}) {
|
|
|
33684
33684
|
});
|
|
33685
33685
|
});
|
|
33686
33686
|
}
|
|
33687
|
+
async function gitValue(args, cwd = getInvocationCwd()) {
|
|
33688
|
+
const result = await runCommand("git", args, { allowFailure: true, cwd });
|
|
33689
|
+
if (result.exitCode !== 0) {
|
|
33690
|
+
return void 0;
|
|
33691
|
+
}
|
|
33692
|
+
return result.stdout.trim();
|
|
33693
|
+
}
|
|
33687
33694
|
async function sleep(ms) {
|
|
33688
33695
|
return new Promise((resolvePromise) => {
|
|
33689
33696
|
setTimeout(resolvePromise, ms);
|
|
@@ -33723,6 +33730,57 @@ function expandPath(path) {
|
|
|
33723
33730
|
function getInvocationCwd() {
|
|
33724
33731
|
return process.env.THREADNOTE_CALLER_CWD ?? process.cwd();
|
|
33725
33732
|
}
|
|
33733
|
+
function recallQueryRequestsWorkspaceContext(query) {
|
|
33734
|
+
const normalized = query.toLowerCase();
|
|
33735
|
+
return /\b(?:this|current)\s+(?:branch|repo|repository|workspace|worktree)\b/.test(normalized);
|
|
33736
|
+
}
|
|
33737
|
+
async function enrichRecallQueryWithWorkspaceContext(query, options = {}) {
|
|
33738
|
+
return enrichRecallQueryWithWorkspaceTerms(query, options, true);
|
|
33739
|
+
}
|
|
33740
|
+
async function enrichRecallQueryWithWorkspaceProjectContext(query, options = {}) {
|
|
33741
|
+
return enrichRecallQueryWithWorkspaceTerms(query, options, false);
|
|
33742
|
+
}
|
|
33743
|
+
async function enrichRecallQueryWithWorkspaceTerms(query, options, includeBranch) {
|
|
33744
|
+
if (!recallQueryRequestsWorkspaceContext(query)) {
|
|
33745
|
+
return query;
|
|
33746
|
+
}
|
|
33747
|
+
const terms = await currentWorkspaceRecallTerms(options, includeBranch);
|
|
33748
|
+
const additions = terms.filter((term) => !query.toLowerCase().includes(term.toLowerCase()));
|
|
33749
|
+
return additions.length > 0 ? `${query} ${additions.join(" ")}` : query;
|
|
33750
|
+
}
|
|
33751
|
+
async function currentWorkspaceRecallTerms(options, includeBranch) {
|
|
33752
|
+
const cwd = options.cwd ?? (options.includeProcessCwd === false ? void 0 : getInvocationCwd());
|
|
33753
|
+
if (!cwd || !(0, import_node_path.isAbsolute)(cwd)) {
|
|
33754
|
+
return [];
|
|
33755
|
+
}
|
|
33756
|
+
const repoRoot = await gitValue(["rev-parse", "--show-toplevel"], cwd);
|
|
33757
|
+
if (!repoRoot) {
|
|
33758
|
+
return [];
|
|
33759
|
+
}
|
|
33760
|
+
const branch = await gitValue(["branch", "--show-current"], repoRoot);
|
|
33761
|
+
const parent = (0, import_node_path.dirname)(repoRoot);
|
|
33762
|
+
return uniqueUsefulWorkspaceTerms([
|
|
33763
|
+
{ source: "branch", value: includeBranch ? branch : void 0 },
|
|
33764
|
+
{ source: "path", value: (0, import_node_path.basename)(repoRoot) },
|
|
33765
|
+
{ source: "path", value: parent === (0, import_node_os.homedir)() ? void 0 : (0, import_node_path.basename)(parent) }
|
|
33766
|
+
]);
|
|
33767
|
+
}
|
|
33768
|
+
function uniqueUsefulWorkspaceTerms(values) {
|
|
33769
|
+
const ignored = /* @__PURE__ */ new Set(["repos", "repositories", "workspaces", "worktrees"]);
|
|
33770
|
+
const seen = /* @__PURE__ */ new Set();
|
|
33771
|
+
const terms = [];
|
|
33772
|
+
for (const { source, value } of values) {
|
|
33773
|
+
const term = value?.trim();
|
|
33774
|
+
const normalized = term?.toLowerCase();
|
|
33775
|
+
const tooShort = source === "branch" ? false : (term?.length ?? 0) < 4;
|
|
33776
|
+
if (!term || !normalized || tooShort || ignored.has(normalized) || seen.has(normalized)) {
|
|
33777
|
+
continue;
|
|
33778
|
+
}
|
|
33779
|
+
seen.add(normalized);
|
|
33780
|
+
terms.push(term);
|
|
33781
|
+
}
|
|
33782
|
+
return terms;
|
|
33783
|
+
}
|
|
33726
33784
|
function trimTrailingSlash(value) {
|
|
33727
33785
|
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
33728
33786
|
}
|
|
@@ -33738,19 +33796,29 @@ function exactRecallTerms(query) {
|
|
|
33738
33796
|
"branch",
|
|
33739
33797
|
"case",
|
|
33740
33798
|
"current",
|
|
33799
|
+
"durable",
|
|
33741
33800
|
"find",
|
|
33801
|
+
"feature",
|
|
33802
|
+
"features",
|
|
33742
33803
|
"handoff",
|
|
33743
33804
|
"issue",
|
|
33744
33805
|
"issues",
|
|
33806
|
+
"knowledge",
|
|
33745
33807
|
"latest",
|
|
33746
33808
|
"memory",
|
|
33747
33809
|
"memories",
|
|
33810
|
+
"project",
|
|
33748
33811
|
"recall",
|
|
33812
|
+
"repo",
|
|
33813
|
+
"repository",
|
|
33749
33814
|
"related",
|
|
33750
33815
|
"search",
|
|
33751
|
-
"
|
|
33816
|
+
"stored",
|
|
33752
33817
|
"this",
|
|
33753
|
-
"
|
|
33818
|
+
"the",
|
|
33819
|
+
"with",
|
|
33820
|
+
"workspace",
|
|
33821
|
+
"worktree"
|
|
33754
33822
|
]);
|
|
33755
33823
|
const seen = /* @__PURE__ */ new Set();
|
|
33756
33824
|
const terms = [];
|
|
@@ -33863,10 +33931,435 @@ function readStringArray(object3, key) {
|
|
|
33863
33931
|
return value;
|
|
33864
33932
|
}
|
|
33865
33933
|
|
|
33934
|
+
// src/memory_hygiene.ts
|
|
33935
|
+
var import_node_path2 = require("node:path");
|
|
33936
|
+
var HYGIENE_SOURCES_HEADING = "## Threadnote Hygiene Sources";
|
|
33937
|
+
var STALE_HANDOFF_AGE_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
33938
|
+
function parseMemoryDocument(uri, content) {
|
|
33939
|
+
const trimmed = content.trim();
|
|
33940
|
+
if (!trimmed) {
|
|
33941
|
+
return void 0;
|
|
33942
|
+
}
|
|
33943
|
+
const separatorIndex = trimmed.indexOf("\n\n");
|
|
33944
|
+
const header = separatorIndex === -1 ? trimmed : trimmed.slice(0, separatorIndex);
|
|
33945
|
+
const body = separatorIndex === -1 ? "" : trimmed.slice(separatorIndex + 2).trim();
|
|
33946
|
+
const firstLine = header.split("\n")[0]?.trim();
|
|
33947
|
+
if (firstLine !== "MEMORY" && firstLine !== "HANDOFF") {
|
|
33948
|
+
return void 0;
|
|
33949
|
+
}
|
|
33950
|
+
const kind = parseOptionalMemoryKind(headerValue(header, "kind")) ?? (firstLine === "HANDOFF" ? "handoff" : void 0);
|
|
33951
|
+
const status = parseOptionalMemoryStatus(headerValue(header, "status")) ?? "active";
|
|
33952
|
+
if (!kind) {
|
|
33953
|
+
return void 0;
|
|
33954
|
+
}
|
|
33955
|
+
return {
|
|
33956
|
+
body,
|
|
33957
|
+
content: trimmed,
|
|
33958
|
+
headerTitle: firstLine,
|
|
33959
|
+
metadata: {
|
|
33960
|
+
archivedFrom: headerValue(header, "archived_from"),
|
|
33961
|
+
kind,
|
|
33962
|
+
project: normalizeOptionalMetadata(headerValue(header, "project") ?? headerValue(header, "repo")),
|
|
33963
|
+
sourceAgentClient: headerValue(header, "source_agent_client") ?? "unknown",
|
|
33964
|
+
status,
|
|
33965
|
+
supersedes: headerValue(header, "supersedes"),
|
|
33966
|
+
timestamp: headerValue(header, "timestamp") ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
33967
|
+
topic: normalizeOptionalMetadata(headerValue(header, "topic"))
|
|
33968
|
+
},
|
|
33969
|
+
uri
|
|
33970
|
+
};
|
|
33971
|
+
}
|
|
33972
|
+
function buildCompactPlan(records, options) {
|
|
33973
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
33974
|
+
const groupedRecords = records.map((record2) => groupableRecord(record2)).filter((item) => item !== void 0).filter((item) => item.project === options.project).filter((item) => options.topic === void 0 || item.topic === options.topic).filter((item) => options.kind === void 0 || item.record.metadata.kind === options.kind);
|
|
33975
|
+
const groups = /* @__PURE__ */ new Map();
|
|
33976
|
+
for (const item of groupedRecords) {
|
|
33977
|
+
groups.set(item.groupKey, [...groups.get(item.groupKey) ?? [], item]);
|
|
33978
|
+
}
|
|
33979
|
+
const keepUpdates = [];
|
|
33980
|
+
const archives = [];
|
|
33981
|
+
const forgets = [];
|
|
33982
|
+
const manualReview = [];
|
|
33983
|
+
for (const group of [...groups.values()].sort(compareGroupedRecordLists)) {
|
|
33984
|
+
const recordsInGroup = group.map((item) => item.record);
|
|
33985
|
+
const kind = recordsInGroup[0]?.metadata.kind;
|
|
33986
|
+
const topic = group[0]?.topic;
|
|
33987
|
+
const project = group[0]?.project;
|
|
33988
|
+
if (!kind || !project || !isCompactableKind(kind)) {
|
|
33989
|
+
continue;
|
|
33990
|
+
}
|
|
33991
|
+
const duplicateGroups = groupBy(recordsInGroup, (record2) => comparableMemoryBody(record2.body));
|
|
33992
|
+
const duplicateForgetUris = /* @__PURE__ */ new Set();
|
|
33993
|
+
const distinctBodyCount = duplicateGroups.size;
|
|
33994
|
+
for (const duplicateGroup of duplicateGroups.values()) {
|
|
33995
|
+
if (duplicateGroup.length < 2) {
|
|
33996
|
+
continue;
|
|
33997
|
+
}
|
|
33998
|
+
const duplicateKeep = preferredKeepRecord(duplicateGroup, topic);
|
|
33999
|
+
for (const duplicate of sortedNewestFirst(duplicateGroup).filter((record2) => record2.uri !== duplicateKeep.uri)) {
|
|
34000
|
+
duplicateForgetUris.add(duplicate.uri);
|
|
34001
|
+
forgets.push({ reason: `exact duplicate of ${duplicateKeep.uri}`, uri: duplicate.uri });
|
|
34002
|
+
}
|
|
34003
|
+
if (distinctBodyCount === 1 || kind !== "handoff") {
|
|
34004
|
+
keepUpdates.push({
|
|
34005
|
+
content: memoryContentWithHygieneSources(
|
|
34006
|
+
duplicateKeep,
|
|
34007
|
+
duplicateGroup.map((record2) => record2.uri)
|
|
34008
|
+
),
|
|
34009
|
+
reason: "keep exact duplicate group with source URIs",
|
|
34010
|
+
sourceUris: duplicateGroup.map((record2) => record2.uri),
|
|
34011
|
+
uri: duplicateKeep.uri
|
|
34012
|
+
});
|
|
34013
|
+
}
|
|
34014
|
+
}
|
|
34015
|
+
const remainingRecords = recordsInGroup.filter((record2) => !duplicateForgetUris.has(record2.uri));
|
|
34016
|
+
if (recordsInGroup.length > 1 && distinctBodyCount === 1) {
|
|
34017
|
+
continue;
|
|
34018
|
+
}
|
|
34019
|
+
if (remainingRecords.length === 0) {
|
|
34020
|
+
continue;
|
|
34021
|
+
}
|
|
34022
|
+
if (remainingRecords.length === 1) {
|
|
34023
|
+
const [record2] = remainingRecords;
|
|
34024
|
+
if (!record2) {
|
|
34025
|
+
continue;
|
|
34026
|
+
}
|
|
34027
|
+
if (record2.metadata.supersedes === record2.uri) {
|
|
34028
|
+
keepUpdates.push({
|
|
34029
|
+
content: memoryContentWithHygieneSources(record2, [record2.uri]),
|
|
34030
|
+
reason: "strip self-supersedes header",
|
|
34031
|
+
sourceUris: [record2.uri],
|
|
34032
|
+
uri: record2.uri
|
|
34033
|
+
});
|
|
34034
|
+
}
|
|
34035
|
+
if (isStaleLookingHandoff(record2, now)) {
|
|
34036
|
+
manualReview.push({ reason: "stale-looking active handoff", uri: record2.uri });
|
|
34037
|
+
}
|
|
34038
|
+
continue;
|
|
34039
|
+
}
|
|
34040
|
+
if (kind === "handoff") {
|
|
34041
|
+
const keep = preferredKeepRecord(remainingRecords, topic);
|
|
34042
|
+
const sourceUris = recordsInGroup.map((record2) => record2.uri);
|
|
34043
|
+
keepUpdates.push({
|
|
34044
|
+
content: memoryContentWithHygieneSources(keep, sourceUris),
|
|
34045
|
+
reason: "keep latest handoff and preserve source URIs",
|
|
34046
|
+
sourceUris,
|
|
34047
|
+
uri: keep.uri
|
|
34048
|
+
});
|
|
34049
|
+
for (const record2 of sortedNewestFirst(remainingRecords).filter((item) => item.uri !== keep.uri)) {
|
|
34050
|
+
archives.push({
|
|
34051
|
+
kind,
|
|
34052
|
+
project,
|
|
34053
|
+
reason: `older handoff for ${project}/${topic ?? "unknown"}`,
|
|
34054
|
+
topic,
|
|
34055
|
+
uri: record2.uri
|
|
34056
|
+
});
|
|
34057
|
+
}
|
|
34058
|
+
continue;
|
|
34059
|
+
}
|
|
34060
|
+
for (const record2 of sortedNewestFirst(remainingRecords)) {
|
|
34061
|
+
manualReview.push({ reason: `non-exact ${kind} memory in overlapping group`, uri: record2.uri });
|
|
34062
|
+
}
|
|
34063
|
+
}
|
|
34064
|
+
return {
|
|
34065
|
+
archives: dedupeByUri(archives),
|
|
34066
|
+
forgets: dedupeByUri(forgets),
|
|
34067
|
+
keepUpdates: dedupeByUri(keepUpdates),
|
|
34068
|
+
manualReview: dedupeByUri(manualReview),
|
|
34069
|
+
options,
|
|
34070
|
+
recordsScanned: groupedRecords.length
|
|
34071
|
+
};
|
|
34072
|
+
}
|
|
34073
|
+
function memoryContentWithHygieneSources(record2, sourceUris) {
|
|
34074
|
+
const body = stripHygieneSources(record2.body);
|
|
34075
|
+
const uniqueSourceUris = [...new Set(sourceUris)].sort();
|
|
34076
|
+
const metadata = {
|
|
34077
|
+
...record2.metadata,
|
|
34078
|
+
supersedes: record2.metadata.supersedes === record2.uri ? void 0 : record2.metadata.supersedes
|
|
34079
|
+
};
|
|
34080
|
+
return formatMemoryDocument(
|
|
34081
|
+
record2.headerTitle,
|
|
34082
|
+
metadata,
|
|
34083
|
+
[body, "", HYGIENE_SOURCES_HEADING, "", ...uniqueSourceUris.map((uri) => `- ${uri}`)].join("\n")
|
|
34084
|
+
);
|
|
34085
|
+
}
|
|
34086
|
+
function formatCompactPlan(plan, options) {
|
|
34087
|
+
const scope = [
|
|
34088
|
+
`project ${plan.options.project}`,
|
|
34089
|
+
plan.options.topic ? `topic ${plan.options.topic}` : void 0,
|
|
34090
|
+
plan.options.kind ? `kind ${plan.options.kind}` : void 0
|
|
34091
|
+
].filter((item) => item !== void 0).join(", ");
|
|
34092
|
+
const lines = [
|
|
34093
|
+
`${options.apply ? "Applying" : "Dry-run"} memory hygiene plan for ${scope}`,
|
|
34094
|
+
`Records scanned: ${plan.recordsScanned}`,
|
|
34095
|
+
"",
|
|
34096
|
+
formatPlanSection(
|
|
34097
|
+
"Keep/update",
|
|
34098
|
+
plan.keepUpdates.map((action) => `${action.uri} (${action.reason}; sources: ${action.sourceUris.length})`)
|
|
34099
|
+
),
|
|
34100
|
+
formatPlanSection(
|
|
34101
|
+
"Archive old handoffs",
|
|
34102
|
+
plan.archives.map((action) => `${action.uri} (${action.reason})`)
|
|
34103
|
+
),
|
|
34104
|
+
formatPlanSection(
|
|
34105
|
+
"Forget exact duplicates",
|
|
34106
|
+
plan.forgets.map((action) => `${action.uri} (${action.reason})`)
|
|
34107
|
+
),
|
|
34108
|
+
formatPlanSection(
|
|
34109
|
+
"Manual review",
|
|
34110
|
+
plan.manualReview.map((item) => `${item.uri} (${item.reason})`)
|
|
34111
|
+
)
|
|
34112
|
+
];
|
|
34113
|
+
if (!options.apply) {
|
|
34114
|
+
lines.push("", "No changes made. Re-run with --apply to execute this plan.");
|
|
34115
|
+
}
|
|
34116
|
+
return lines.join("\n");
|
|
34117
|
+
}
|
|
34118
|
+
function recallHygieneNudges(text, options) {
|
|
34119
|
+
const activeUris = activePersonalMemoryUrisFromText(text, options.user);
|
|
34120
|
+
const nudges = [];
|
|
34121
|
+
const returnedUriSet = new Set(activeUris);
|
|
34122
|
+
const returnedRecords = options.records?.filter((record2) => returnedUriSet.has(record2.uri)).map((record2) => groupableRecord(record2)) ?? [];
|
|
34123
|
+
const groups = /* @__PURE__ */ new Map();
|
|
34124
|
+
for (const item of returnedRecords) {
|
|
34125
|
+
if (!item) {
|
|
34126
|
+
continue;
|
|
34127
|
+
}
|
|
34128
|
+
groups.set(item.groupKey, [...groups.get(item.groupKey) ?? [], item]);
|
|
34129
|
+
}
|
|
34130
|
+
for (const group of [...groups.values()].sort(compareGroupedRecordLists)) {
|
|
34131
|
+
if (group.length < 3) {
|
|
34132
|
+
continue;
|
|
34133
|
+
}
|
|
34134
|
+
const first = group[0];
|
|
34135
|
+
if (!first || !isCompactableKind(first.record.metadata.kind)) {
|
|
34136
|
+
continue;
|
|
34137
|
+
}
|
|
34138
|
+
const topic = first.topic;
|
|
34139
|
+
if (!topic) {
|
|
34140
|
+
continue;
|
|
34141
|
+
}
|
|
34142
|
+
nudges.push(
|
|
34143
|
+
`${group.length} active ${memoryKindPlural(first.record.metadata.kind)} look overlapping for ${first.project}/${topic}; run compact_context({"project":"${first.project}","topic":"${topic}","dryRun":true}).`
|
|
34144
|
+
);
|
|
34145
|
+
}
|
|
34146
|
+
const projectCounts = /* @__PURE__ */ new Map();
|
|
34147
|
+
for (const uri of activeUris) {
|
|
34148
|
+
const parsed = parsePersonalMemoryUri(uri, options.user);
|
|
34149
|
+
if (!parsed || parsed.kind !== "handoff" || parsed.status !== "active") {
|
|
34150
|
+
continue;
|
|
34151
|
+
}
|
|
34152
|
+
projectCounts.set(parsed.project, (projectCounts.get(parsed.project) ?? 0) + 1);
|
|
34153
|
+
}
|
|
34154
|
+
for (const [project, count] of [...projectCounts.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
34155
|
+
if (count < 10) {
|
|
34156
|
+
continue;
|
|
34157
|
+
}
|
|
34158
|
+
nudges.push(
|
|
34159
|
+
`Many active handoffs surfaced for ${project}; run compact_context({"project":"${project}","dryRun":true}).`
|
|
34160
|
+
);
|
|
34161
|
+
}
|
|
34162
|
+
return [...new Set(nudges)];
|
|
34163
|
+
}
|
|
34164
|
+
function activePersonalMemoryUrisFromText(text, user) {
|
|
34165
|
+
const userSegment = uriSegment(user);
|
|
34166
|
+
const matches = text.matchAll(/viking:\/\/[^\s)]+/g);
|
|
34167
|
+
const uris = [];
|
|
34168
|
+
for (const match of matches) {
|
|
34169
|
+
const uri = match[0]?.replace(/[.,;:]+$/g, "");
|
|
34170
|
+
if (!uri || !parsePersonalMemoryUri(uri, userSegment)) {
|
|
34171
|
+
continue;
|
|
34172
|
+
}
|
|
34173
|
+
uris.push(uri);
|
|
34174
|
+
}
|
|
34175
|
+
return [...new Set(uris)];
|
|
34176
|
+
}
|
|
34177
|
+
function parsePersonalMemoryUri(uri, user) {
|
|
34178
|
+
const prefix = `viking://user/${uriSegment(user)}/memories/`;
|
|
34179
|
+
if (!uri.startsWith(prefix) || uri.includes("/shared/")) {
|
|
34180
|
+
return void 0;
|
|
34181
|
+
}
|
|
34182
|
+
const rest = uri.slice(prefix.length);
|
|
34183
|
+
const parts = rest.split("/").filter(Boolean);
|
|
34184
|
+
if (parts.length < 4) {
|
|
34185
|
+
return void 0;
|
|
34186
|
+
}
|
|
34187
|
+
if (parts[0] === "handoffs" && parts[1] === "active" && parts[2] && parts[3]?.endsWith(".md")) {
|
|
34188
|
+
return { kind: "handoff", project: parts[2], status: "active", topic: parts[3].replace(/\.md$/, "") };
|
|
34189
|
+
}
|
|
34190
|
+
if (parts[0] === "durable" && parts[1] === "projects" && parts[2] && parts[3]?.endsWith(".md")) {
|
|
34191
|
+
return { kind: "durable", project: parts[2], status: "active", topic: parts[3].replace(/\.md$/, "") };
|
|
34192
|
+
}
|
|
34193
|
+
if (parts[0] === "incidents" && parts[1] === "active" && parts[2] && parts[3]?.endsWith(".md")) {
|
|
34194
|
+
return { kind: "incident", project: parts[2], status: "active", topic: parts[3].replace(/\.md$/, "") };
|
|
34195
|
+
}
|
|
34196
|
+
return void 0;
|
|
34197
|
+
}
|
|
34198
|
+
function groupableRecord(record2) {
|
|
34199
|
+
if (record2.metadata.status !== "active" || !isCompactableKind(record2.metadata.kind)) {
|
|
34200
|
+
return void 0;
|
|
34201
|
+
}
|
|
34202
|
+
const project = normalizeOptionalMetadata(record2.metadata.project) ?? parseProjectFromUri(record2.uri);
|
|
34203
|
+
if (!project) {
|
|
34204
|
+
return void 0;
|
|
34205
|
+
}
|
|
34206
|
+
const topic = topicForRecord(record2);
|
|
34207
|
+
const groupKey = [record2.metadata.kind, project, topic ?? record2.uri].join("\0");
|
|
34208
|
+
return { groupKey, project, record: record2, topic };
|
|
34209
|
+
}
|
|
34210
|
+
function topicForRecord(record2) {
|
|
34211
|
+
return normalizeOptionalMetadata(record2.metadata.topic) ?? normalizeOptionalMetadata(branchFromBody(record2.body)) ?? topicFromUri(record2.uri);
|
|
34212
|
+
}
|
|
34213
|
+
function branchFromBody(body) {
|
|
34214
|
+
const branch = /^branch:\s*(.+)$/m.exec(body)?.[1]?.trim();
|
|
34215
|
+
return branch?.split(/\s+/)[0]?.replace(/[.,;:]+$/g, "");
|
|
34216
|
+
}
|
|
34217
|
+
function topicFromUri(uri) {
|
|
34218
|
+
const name = (0, import_node_path2.basename)(uri).replace(/\.md$/, "");
|
|
34219
|
+
return name.startsWith("threadnote-") ? void 0 : name;
|
|
34220
|
+
}
|
|
34221
|
+
function parseProjectFromUri(uri) {
|
|
34222
|
+
const parts = uri.split("/memories/")[1]?.split("/").filter(Boolean) ?? [];
|
|
34223
|
+
if ((parts[0] === "handoffs" || parts[0] === "incidents") && parts[1] === "active") {
|
|
34224
|
+
return parts[2];
|
|
34225
|
+
}
|
|
34226
|
+
if (parts[0] === "durable" && parts[1] === "projects") {
|
|
34227
|
+
return parts[2];
|
|
34228
|
+
}
|
|
34229
|
+
return void 0;
|
|
34230
|
+
}
|
|
34231
|
+
function comparableMemoryBody(body) {
|
|
34232
|
+
return stripHygieneSources(body).trim().replace(/\s+/g, " ");
|
|
34233
|
+
}
|
|
34234
|
+
function stripHygieneSources(body) {
|
|
34235
|
+
const index = body.indexOf(`
|
|
34236
|
+
${HYGIENE_SOURCES_HEADING}`);
|
|
34237
|
+
if (index !== -1) {
|
|
34238
|
+
return body.slice(0, index).trim();
|
|
34239
|
+
}
|
|
34240
|
+
return body.startsWith(HYGIENE_SOURCES_HEADING) ? "" : body.trim();
|
|
34241
|
+
}
|
|
34242
|
+
function preferredKeepRecord(records, topic) {
|
|
34243
|
+
const stableRecords = records.filter((record2) => isStableRecord(record2, topic));
|
|
34244
|
+
return sortedNewestFirst(stableRecords.length > 0 ? stableRecords : records)[0] ?? records[0];
|
|
34245
|
+
}
|
|
34246
|
+
function isStableRecord(record2, topic) {
|
|
34247
|
+
const recordTopic = topic ?? topicForRecord(record2);
|
|
34248
|
+
return recordTopic !== void 0 && (0, import_node_path2.basename)(record2.uri) === `${uriSegment(recordTopic)}.md`;
|
|
34249
|
+
}
|
|
34250
|
+
function sortedNewestFirst(records) {
|
|
34251
|
+
return [...records].sort((left, right) => {
|
|
34252
|
+
const timestampDiff = timestampMs(right) - timestampMs(left);
|
|
34253
|
+
return timestampDiff === 0 ? left.uri.localeCompare(right.uri) : timestampDiff;
|
|
34254
|
+
});
|
|
34255
|
+
}
|
|
34256
|
+
function timestampMs(record2) {
|
|
34257
|
+
const parsed = Date.parse(record2.metadata.timestamp);
|
|
34258
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
34259
|
+
}
|
|
34260
|
+
function isStaleLookingHandoff(record2, now) {
|
|
34261
|
+
if (record2.metadata.kind !== "handoff") {
|
|
34262
|
+
return false;
|
|
34263
|
+
}
|
|
34264
|
+
if (now.getTime() - timestampMs(record2) < STALE_HANDOFF_AGE_MS) {
|
|
34265
|
+
return false;
|
|
34266
|
+
}
|
|
34267
|
+
return /\b(?:PR|pull request)\s+(?:OPEN|open|is open)|awaiting review|waiting for review|next steps?:\s*address PR review/i.test(
|
|
34268
|
+
record2.body
|
|
34269
|
+
);
|
|
34270
|
+
}
|
|
34271
|
+
function groupBy(values, keyForValue) {
|
|
34272
|
+
const groups = /* @__PURE__ */ new Map();
|
|
34273
|
+
for (const value of values) {
|
|
34274
|
+
const key = keyForValue(value);
|
|
34275
|
+
groups.set(key, [...groups.get(key) ?? [], value]);
|
|
34276
|
+
}
|
|
34277
|
+
return groups;
|
|
34278
|
+
}
|
|
34279
|
+
function compareGroupedRecordLists(left, right) {
|
|
34280
|
+
return (left[0]?.groupKey ?? "").localeCompare(right[0]?.groupKey ?? "");
|
|
34281
|
+
}
|
|
34282
|
+
function dedupeByUri(items) {
|
|
34283
|
+
const seen = /* @__PURE__ */ new Set();
|
|
34284
|
+
const result = [];
|
|
34285
|
+
for (const item of items) {
|
|
34286
|
+
if (seen.has(item.uri)) {
|
|
34287
|
+
continue;
|
|
34288
|
+
}
|
|
34289
|
+
seen.add(item.uri);
|
|
34290
|
+
result.push(item);
|
|
34291
|
+
}
|
|
34292
|
+
return result;
|
|
34293
|
+
}
|
|
34294
|
+
function formatMemoryDocument(title, metadata, body) {
|
|
34295
|
+
const header = [
|
|
34296
|
+
title,
|
|
34297
|
+
`kind: ${metadata.kind}`,
|
|
34298
|
+
`status: ${metadata.status}`,
|
|
34299
|
+
metadata.project ? `project: ${metadata.project}` : void 0,
|
|
34300
|
+
metadata.topic ? `topic: ${metadata.topic}` : void 0,
|
|
34301
|
+
`source_agent_client: ${metadata.sourceAgentClient}`,
|
|
34302
|
+
`timestamp: ${metadata.timestamp}`,
|
|
34303
|
+
metadata.supersedes ? `supersedes: ${metadata.supersedes}` : void 0,
|
|
34304
|
+
metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0
|
|
34305
|
+
].filter((line) => line !== void 0);
|
|
34306
|
+
return [...header, "", body.trim()].join("\n");
|
|
34307
|
+
}
|
|
34308
|
+
function headerValue(header, key) {
|
|
34309
|
+
const prefix = `${key}:`;
|
|
34310
|
+
return header.split("\n").find((line) => line.startsWith(prefix))?.slice(prefix.length).trim();
|
|
34311
|
+
}
|
|
34312
|
+
function parseOptionalMemoryKind(value) {
|
|
34313
|
+
if (!value) {
|
|
34314
|
+
return void 0;
|
|
34315
|
+
}
|
|
34316
|
+
if (["durable", "handoff", "incident", "preference", "smoke"].includes(value)) {
|
|
34317
|
+
return value;
|
|
34318
|
+
}
|
|
34319
|
+
return void 0;
|
|
34320
|
+
}
|
|
34321
|
+
function parseOptionalMemoryStatus(value) {
|
|
34322
|
+
if (!value) {
|
|
34323
|
+
return void 0;
|
|
34324
|
+
}
|
|
34325
|
+
if (["active", "archived", "superseded"].includes(value)) {
|
|
34326
|
+
return value;
|
|
34327
|
+
}
|
|
34328
|
+
return void 0;
|
|
34329
|
+
}
|
|
34330
|
+
function normalizeOptionalMetadata(value) {
|
|
34331
|
+
const trimmed = value?.trim();
|
|
34332
|
+
return trimmed ? trimmed : void 0;
|
|
34333
|
+
}
|
|
34334
|
+
function isCompactableKind(kind) {
|
|
34335
|
+
return kind === "durable" || kind === "handoff" || kind === "incident";
|
|
34336
|
+
}
|
|
34337
|
+
function memoryKindPlural(kind) {
|
|
34338
|
+
switch (kind) {
|
|
34339
|
+
case "handoff":
|
|
34340
|
+
return "handoffs";
|
|
34341
|
+
case "incident":
|
|
34342
|
+
return "incidents";
|
|
34343
|
+
case "durable":
|
|
34344
|
+
return "durable memories";
|
|
34345
|
+
case "preference":
|
|
34346
|
+
return "preferences";
|
|
34347
|
+
case "smoke":
|
|
34348
|
+
return "smoke memories";
|
|
34349
|
+
}
|
|
34350
|
+
}
|
|
34351
|
+
function formatPlanSection(title, lines) {
|
|
34352
|
+
if (lines.length === 0) {
|
|
34353
|
+
return `${title}:
|
|
34354
|
+
- none`;
|
|
34355
|
+
}
|
|
34356
|
+
return [`${title}:`, ...lines.map((line) => `- ${line}`)].join("\n");
|
|
34357
|
+
}
|
|
34358
|
+
|
|
33866
34359
|
// src/share.ts
|
|
33867
34360
|
var import_promises3 = require("node:fs/promises");
|
|
33868
34361
|
var import_node_os2 = require("node:os");
|
|
33869
|
-
var
|
|
34362
|
+
var import_node_path3 = require("node:path");
|
|
33870
34363
|
|
|
33871
34364
|
// src/runtime.ts
|
|
33872
34365
|
function withIdentity(config2, args) {
|
|
@@ -33980,7 +34473,7 @@ function autoShareState(config2) {
|
|
|
33980
34473
|
return state;
|
|
33981
34474
|
}
|
|
33982
34475
|
function pendingReindexesPath(config2) {
|
|
33983
|
-
return (0,
|
|
34476
|
+
return (0, import_node_path3.join)(config2.agentContextHome, "share", "auto-sync-pending-reindexes.json");
|
|
33984
34477
|
}
|
|
33985
34478
|
async function loadPendingReindexes(config2, state) {
|
|
33986
34479
|
const raw = await readFileIfExists(pendingReindexesPath(config2));
|
|
@@ -34024,7 +34517,7 @@ async function writePendingReindexes(config2, state) {
|
|
|
34024
34517
|
teams: Object.fromEntries(state.pendingReindexes),
|
|
34025
34518
|
version: 1
|
|
34026
34519
|
};
|
|
34027
|
-
await (0, import_promises3.mkdir)((0,
|
|
34520
|
+
await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(path), { recursive: true });
|
|
34028
34521
|
const tempPath = `${path}.${process.pid}.tmp`;
|
|
34029
34522
|
await (0, import_promises3.writeFile)(tempPath, `${JSON.stringify(contents, void 0, 2)}
|
|
34030
34523
|
`, { encoding: "utf8", mode: 384 });
|
|
@@ -34124,7 +34617,7 @@ async function runShareSyncQuiet(config2, state, options) {
|
|
|
34124
34617
|
const beforeRev = await gitOutput(worktree, ["rev-parse", "HEAD"], false);
|
|
34125
34618
|
const pullResult = await runCommand(git, ["-C", worktree, "rebase", "@{u}"], { allowFailure: true });
|
|
34126
34619
|
if (pullResult.exitCode !== 0) {
|
|
34127
|
-
if (await exists((0,
|
|
34620
|
+
if (await exists((0, import_node_path3.join)(team.config.gitdir, "rebase-merge")) || await exists((0, import_node_path3.join)(team.config.gitdir, "rebase-apply"))) {
|
|
34128
34621
|
throw new Error(
|
|
34129
34622
|
`Automatic share sync hit git conflicts in ${worktree}. Resolve them in-place, run \`git -C ${worktree} rebase --continue\` (or --abort), then rerun recall/read.`
|
|
34130
34623
|
);
|
|
@@ -34157,10 +34650,10 @@ function normalizeTeamName(input) {
|
|
|
34157
34650
|
return candidate;
|
|
34158
34651
|
}
|
|
34159
34652
|
function teamsFilePath(config2) {
|
|
34160
|
-
return (0,
|
|
34653
|
+
return (0, import_node_path3.join)(config2.agentContextHome, "share", "teams.json");
|
|
34161
34654
|
}
|
|
34162
34655
|
function teamWorktreePath(config2, team) {
|
|
34163
|
-
return (0,
|
|
34656
|
+
return (0, import_node_path3.join)(
|
|
34164
34657
|
config2.agentContextHome,
|
|
34165
34658
|
"data",
|
|
34166
34659
|
"viking",
|
|
@@ -34173,7 +34666,7 @@ function teamWorktreePath(config2, team) {
|
|
|
34173
34666
|
);
|
|
34174
34667
|
}
|
|
34175
34668
|
function teamGitdirPath(config2, team) {
|
|
34176
|
-
return (0,
|
|
34669
|
+
return (0, import_node_path3.join)(config2.agentContextHome, "share", "teams", `${team}.gitdir`);
|
|
34177
34670
|
}
|
|
34178
34671
|
async function readTeamsFile(config2) {
|
|
34179
34672
|
const path = teamsFilePath(config2);
|
|
@@ -34229,7 +34722,7 @@ async function resolveTeam(config2, requested) {
|
|
|
34229
34722
|
return { config: found, name: wantName };
|
|
34230
34723
|
}
|
|
34231
34724
|
function workfileToVikingUri(config2, team, filePath) {
|
|
34232
|
-
const rel = (0,
|
|
34725
|
+
const rel = (0, import_node_path3.relative)(team.worktree, filePath).split(import_node_path3.sep).join("/");
|
|
34233
34726
|
return `viking://user/${uriSegment(config2.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
|
|
34234
34727
|
}
|
|
34235
34728
|
function isInSharedNamespace(config2, uri) {
|
|
@@ -34326,8 +34819,8 @@ async function writeMemoryFile(config2, ov, uri, content, initialMode, dryRun, o
|
|
|
34326
34819
|
}
|
|
34327
34820
|
return;
|
|
34328
34821
|
}
|
|
34329
|
-
const stagingDir = await (0, import_promises3.mkdtemp)((0,
|
|
34330
|
-
const tempPath = (0,
|
|
34822
|
+
const stagingDir = await (0, import_promises3.mkdtemp)((0, import_node_path3.join)((0, import_node_os2.tmpdir)(), "threadnote-share-"));
|
|
34823
|
+
const tempPath = (0, import_node_path3.join)(stagingDir, "body.txt");
|
|
34331
34824
|
try {
|
|
34332
34825
|
await (0, import_promises3.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
|
|
34333
34826
|
await writeOvFileWithRetry(config2, ov, uri, tempPath, initialMode, options);
|
|
@@ -34449,7 +34942,7 @@ async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
|
|
|
34449
34942
|
if (!relative2) {
|
|
34450
34943
|
return;
|
|
34451
34944
|
}
|
|
34452
|
-
await (0, import_promises3.rm)((0,
|
|
34945
|
+
await (0, import_promises3.rm)((0, import_node_path3.join)(worktree, relative2), { force: true });
|
|
34453
34946
|
}
|
|
34454
34947
|
async function removeMemoryUri(config2, ov, uri, dryRun, options = {}) {
|
|
34455
34948
|
const args = withIdentity(config2, ["rm", uri]);
|
|
@@ -34513,8 +35006,8 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
|
|
|
34513
35006
|
const oldRel = entries[index + 1];
|
|
34514
35007
|
const newRel = entries[index + 2];
|
|
34515
35008
|
if (oldRel && newRel) {
|
|
34516
|
-
changes.push({ path: (0,
|
|
34517
|
-
changes.push({ path: (0,
|
|
35009
|
+
changes.push({ path: (0, import_node_path3.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
|
|
35010
|
+
changes.push({ path: (0, import_node_path3.join)(worktree, newRel), relativePath: newRel, status: "added" });
|
|
34518
35011
|
}
|
|
34519
35012
|
index += 3;
|
|
34520
35013
|
continue;
|
|
@@ -34522,7 +35015,7 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
|
|
|
34522
35015
|
const rel = entries[index + 1];
|
|
34523
35016
|
if (rel) {
|
|
34524
35017
|
const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
|
|
34525
|
-
changes.push({ path: (0,
|
|
35018
|
+
changes.push({ path: (0, import_node_path3.join)(worktree, rel), relativePath: rel, status });
|
|
34526
35019
|
}
|
|
34527
35020
|
index += 2;
|
|
34528
35021
|
}
|
|
@@ -34604,13 +35097,14 @@ async function main() {
|
|
|
34604
35097
|
"",
|
|
34605
35098
|
"Stdio MCP adapter for Threadnote shared local context.",
|
|
34606
35099
|
"Prefer `recall_context` to find candidate viking:// URIs, then `read_context` files or `list_context` directories.",
|
|
34607
|
-
'Always pass JSON arguments. Example: recall_context({"query":"current repo latest handoff"}).',
|
|
35100
|
+
'Always pass JSON arguments. Example: recall_context({"query":"current repo latest handoff","callerCwd":"/absolute/workspace/path"}).',
|
|
34608
35101
|
"recall_context also surfaces seeded project resources under viking://resources/repos/<project> when the query mentions a project from the seed manifest. See its tool description for the query convention.",
|
|
34609
35102
|
"Older clients may use the compatibility aliases `search`, `read`, and `list`.",
|
|
34610
35103
|
'For durable facts, store kind="durable"; for current work logs, store kind="handoff" with project/topic so Threadnote keeps one active memory updated.',
|
|
34611
35104
|
"When a handoff describes an active branch or feature, recall durable feature memories for the same branch/topic before coding.",
|
|
34612
35105
|
"During feature work, update durable feature knowledge when valuable implementation details, decisions, interfaces, or gotchas change.",
|
|
34613
35106
|
"When updating the same active issue, pass project/topic or replaceUri to remember_context so duplicate durable memories or handoffs do not accumulate.",
|
|
35107
|
+
"Use compact_context with dryRun=true for scoped memory hygiene when recall surfaces overlapping active memories.",
|
|
34614
35108
|
"To share a durable memory with teammates, call `share_publish` with its viking:// URI. share_publish is destructive: it scrubs for secrets, moves the memory into the shared subtree, removes the personal copy, and pushes a git commit. Do not publish handoffs, preferences, or anything carrying machine-local paths or in-flight task context.",
|
|
34615
35109
|
"Do not store secrets, customer data, raw production logs, or credentials."
|
|
34616
35110
|
].join("\n")
|
|
@@ -34635,7 +35129,7 @@ function registerTools(server, config2) {
|
|
|
34635
35129
|
server,
|
|
34636
35130
|
config2,
|
|
34637
35131
|
"recall_context",
|
|
34638
|
-
`Search Threadnote context across personal memories and seeded project resources. Returns semantic hits from indexed Threadnote context (handoffs, durable feature memories, preferences, shared team memories) \u2014 and, when the query mentions a project name from the seed manifest, also from that project's seeded guidance (README, AGENTS.md, CLAUDE.md, SKILL.md, docs/**) under viking://resources/repos/<project>. Include the repo or project name in the query to make the project-guidance pass fire. Required: pass JSON arguments with a non-empty query, for example {"query":"unity-ui-ccc latest handoff"}.`
|
|
35132
|
+
`Search Threadnote context across personal memories and seeded project resources. Returns semantic hits from indexed Threadnote context (handoffs, durable feature memories, preferences, shared team memories) \u2014 and, when the query mentions a project name from the seed manifest, also from that project's seeded guidance (README, AGENTS.md, CLAUDE.md, SKILL.md, docs/**) under viking://resources/repos/<project>. Queries that mention this/current branch are enriched with local git/workspace terms when callerCwd is provided. Include the repo or project name in the query to make the project-guidance pass fire. Required: pass JSON arguments with a non-empty query, for example {"query":"unity-ui-ccc latest handoff"}.`
|
|
34639
35133
|
);
|
|
34640
35134
|
registerSearchTool(
|
|
34641
35135
|
server,
|
|
@@ -34666,6 +35160,7 @@ function registerTools(server, config2) {
|
|
|
34666
35160
|
"Archive a memory so it remains readable as provenance but is no longer current working context."
|
|
34667
35161
|
);
|
|
34668
35162
|
registerArchiveTool(server, config2, "archive", "Compatibility alias for archive_context.");
|
|
35163
|
+
registerCompactTool(server, config2);
|
|
34669
35164
|
server.registerTool(
|
|
34670
35165
|
"forget",
|
|
34671
35166
|
{
|
|
@@ -34828,11 +35323,12 @@ function registerSearchTool(server, config2, name, description) {
|
|
|
34828
35323
|
inputSchema: {
|
|
34829
35324
|
query: external_exports.string().optional().describe('Required search query, for example "unity-ui-ccc latest handoff"'),
|
|
34830
35325
|
uri: external_exports.string().optional().describe("Optional viking:// subtree to search"),
|
|
35326
|
+
callerCwd: external_exports.string().optional().describe("Optional absolute caller workspace path used to resolve this/current branch queries"),
|
|
34831
35327
|
nodeLimit: external_exports.number().int().positive().max(100).optional().describe("Maximum result count"),
|
|
34832
|
-
includeArchived: external_exports.boolean().optional().describe("Include archived memories in exact
|
|
35328
|
+
includeArchived: external_exports.boolean().optional().describe("Include archived memories in exact memory/resource matches")
|
|
34833
35329
|
}
|
|
34834
35330
|
},
|
|
34835
|
-
async ({ includeArchived, nodeLimit, query, uri }) => {
|
|
35331
|
+
async ({ callerCwd, includeArchived, nodeLimit, query, uri }) => {
|
|
34836
35332
|
const checkedQuery = requiredText(query, name, "query", { query: "unity-ui-ccc latest handoff" });
|
|
34837
35333
|
if (!checkedQuery.ok) {
|
|
34838
35334
|
return checkedQuery.error;
|
|
@@ -34842,6 +35338,7 @@ function registerSearchTool(server, config2, name, description) {
|
|
|
34842
35338
|
return checkedUri.error;
|
|
34843
35339
|
}
|
|
34844
35340
|
return runRecallTool(config2, {
|
|
35341
|
+
callerCwd,
|
|
34845
35342
|
query: checkedQuery.value,
|
|
34846
35343
|
pinnedUri: checkedUri.value,
|
|
34847
35344
|
nodeLimit,
|
|
@@ -34860,9 +35357,18 @@ async function runRecallTool(config2, params) {
|
|
|
34860
35357
|
} catch (err) {
|
|
34861
35358
|
syncWarnings.push(errorMessage(err));
|
|
34862
35359
|
}
|
|
35360
|
+
const query = await enrichRecallQueryWithWorkspaceContext(params.query, {
|
|
35361
|
+
cwd: params.callerCwd,
|
|
35362
|
+
includeProcessCwd: false
|
|
35363
|
+
});
|
|
35364
|
+
const projectQuery = await enrichRecallQueryWithWorkspaceProjectContext(params.query, {
|
|
35365
|
+
cwd: params.callerCwd,
|
|
35366
|
+
includeProcessCwd: false
|
|
35367
|
+
});
|
|
35368
|
+
const contextualParams = { ...params, query };
|
|
34863
35369
|
const baseArgs = [
|
|
34864
35370
|
"search",
|
|
34865
|
-
|
|
35371
|
+
query,
|
|
34866
35372
|
...params.pinnedUri ? ["--uri", params.pinnedUri] : [],
|
|
34867
35373
|
...params.nodeLimit ? ["--node-limit", String(params.nodeLimit)] : []
|
|
34868
35374
|
];
|
|
@@ -34878,15 +35384,19 @@ async function runRecallTool(config2, params) {
|
|
|
34878
35384
|
if (firstContent.text) {
|
|
34879
35385
|
sections.push(firstContent.text);
|
|
34880
35386
|
}
|
|
34881
|
-
const seededSection = await seededResourcesSection(config2,
|
|
35387
|
+
const seededSection = await seededResourcesSection(config2, contextualParams, projectQuery);
|
|
34882
35388
|
if (seededSection) {
|
|
34883
35389
|
sections.push(seededSection);
|
|
34884
35390
|
}
|
|
34885
|
-
const exactMatches = await exactMemoryMatchesText(config2,
|
|
35391
|
+
const exactMatches = await exactMemoryMatchesText(config2, query, params.includeArchived);
|
|
34886
35392
|
if (exactMatches) {
|
|
34887
|
-
sections.push(`Exact
|
|
35393
|
+
sections.push(`Exact memory/resource matches:
|
|
34888
35394
|
${exactMatches}`);
|
|
34889
35395
|
}
|
|
35396
|
+
const hygieneHints = await recallHygieneHintsSection(config2, sections.join("\n\n"));
|
|
35397
|
+
if (hygieneHints) {
|
|
35398
|
+
sections.push(hygieneHints);
|
|
35399
|
+
}
|
|
34890
35400
|
if (syncedTeams.length > 0) {
|
|
34891
35401
|
sections.push(`Auto-synced shared memories: ${syncedTeams.join(", ")}`);
|
|
34892
35402
|
}
|
|
@@ -34898,11 +35408,20 @@ ${exactMatches}`);
|
|
|
34898
35408
|
}
|
|
34899
35409
|
return { ...semanticResult, content: [{ type: "text", text: sections.join("\n\n") }] };
|
|
34900
35410
|
}
|
|
34901
|
-
async function
|
|
35411
|
+
async function recallHygieneHintsSection(config2, recallText) {
|
|
35412
|
+
const uris = activePersonalMemoryUrisFromText(recallText, config2.user);
|
|
35413
|
+
if (uris.length === 0) {
|
|
35414
|
+
return void 0;
|
|
35415
|
+
}
|
|
35416
|
+
const records = await readMemoryRecordsByUri(config2, uris);
|
|
35417
|
+
const nudges = recallHygieneNudges(recallText, { records, user: config2.user });
|
|
35418
|
+
return nudges.length > 0 ? ["Memory hygiene hints:", ...nudges.map((nudge) => `- ${nudge}`)].join("\n") : void 0;
|
|
35419
|
+
}
|
|
35420
|
+
async function seededResourcesSection(config2, params, projectQuery) {
|
|
34902
35421
|
if (params.pinnedUri) {
|
|
34903
35422
|
return void 0;
|
|
34904
35423
|
}
|
|
34905
|
-
const project = await inferProjectFromQuery(config2.manifestPath,
|
|
35424
|
+
const project = await inferProjectFromQuery(config2.manifestPath, projectQuery);
|
|
34906
35425
|
if (!project) {
|
|
34907
35426
|
return void 0;
|
|
34908
35427
|
}
|
|
@@ -35046,11 +35565,11 @@ function registerStoreTool(server, config2, name, description) {
|
|
|
35046
35565
|
}
|
|
35047
35566
|
const metadata = {
|
|
35048
35567
|
kind: kind ?? "durable",
|
|
35049
|
-
project:
|
|
35568
|
+
project: normalizeOptionalMetadata2(project),
|
|
35050
35569
|
sourceAgentClient: sourceAgentClient ?? "mcp",
|
|
35051
35570
|
status: status ?? "active",
|
|
35052
35571
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
35053
|
-
topic:
|
|
35572
|
+
topic: normalizeOptionalMetadata2(topic)
|
|
35054
35573
|
};
|
|
35055
35574
|
return writeDurableMemory(config2, {
|
|
35056
35575
|
bodyText: checkedText.value,
|
|
@@ -35091,11 +35610,11 @@ function registerArchiveTool(server, config2, name, description) {
|
|
|
35091
35610
|
const metadata = {
|
|
35092
35611
|
archivedFrom: checkedUri.value,
|
|
35093
35612
|
kind: kind ?? "handoff",
|
|
35094
|
-
project:
|
|
35613
|
+
project: normalizeOptionalMetadata2(project),
|
|
35095
35614
|
sourceAgentClient: "mcp",
|
|
35096
35615
|
status: "archived",
|
|
35097
35616
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
35098
|
-
topic:
|
|
35617
|
+
topic: normalizeOptionalMetadata2(topic)
|
|
35099
35618
|
};
|
|
35100
35619
|
const archiveResult = await writeDurableMemory(config2, {
|
|
35101
35620
|
bodyText: ["Archived original Threadnote memory.", "", original].join("\n"),
|
|
@@ -35123,17 +35642,238 @@ Archive stored, but original memory is still processing. Retry later with forget
|
|
|
35123
35642
|
}
|
|
35124
35643
|
);
|
|
35125
35644
|
}
|
|
35645
|
+
function registerCompactTool(server, config2) {
|
|
35646
|
+
server.registerTool(
|
|
35647
|
+
"compact_context",
|
|
35648
|
+
{
|
|
35649
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
35650
|
+
description: "Plan or apply scoped Threadnote memory hygiene. Defaults to dry-run; pass apply=true to archive stale handoffs and forget exact duplicates.",
|
|
35651
|
+
inputSchema: {
|
|
35652
|
+
apply: external_exports.boolean().optional().describe("Apply the compact plan; defaults to false"),
|
|
35653
|
+
dryRun: external_exports.boolean().optional().describe("Keep the call read-only; defaults to true unless apply=true"),
|
|
35654
|
+
kind: external_exports.enum(["durable", "handoff", "incident"]).optional().describe("Optional memory kind filter"),
|
|
35655
|
+
project: external_exports.string().optional().describe("Required project/repo namespace, for example threadnote"),
|
|
35656
|
+
topic: external_exports.string().optional().describe("Optional stable topic name")
|
|
35657
|
+
}
|
|
35658
|
+
},
|
|
35659
|
+
async ({ apply, dryRun, kind, project, topic }) => {
|
|
35660
|
+
const checkedProject = requiredText(project, "compact_context", "project", { project: "threadnote" });
|
|
35661
|
+
if (!checkedProject.ok) {
|
|
35662
|
+
return checkedProject.error;
|
|
35663
|
+
}
|
|
35664
|
+
if (apply === true && dryRun === true) {
|
|
35665
|
+
return {
|
|
35666
|
+
content: [{ type: "text", text: "compact_context cannot combine apply=true with dryRun=true." }],
|
|
35667
|
+
isError: true
|
|
35668
|
+
};
|
|
35669
|
+
}
|
|
35670
|
+
try {
|
|
35671
|
+
const records = await scopedCompactRecords(config2, {
|
|
35672
|
+
kind,
|
|
35673
|
+
project: checkedProject.value
|
|
35674
|
+
});
|
|
35675
|
+
const plan = buildCompactPlan(records, {
|
|
35676
|
+
kind,
|
|
35677
|
+
project: checkedProject.value,
|
|
35678
|
+
topic: normalizeOptionalMetadata2(topic)
|
|
35679
|
+
});
|
|
35680
|
+
const shouldApply = apply === true;
|
|
35681
|
+
const planText = formatCompactPlan(plan, { apply: shouldApply });
|
|
35682
|
+
if (!shouldApply) {
|
|
35683
|
+
return { content: [{ type: "text", text: planText }] };
|
|
35684
|
+
}
|
|
35685
|
+
const ov = await requiredOpenVikingCli2();
|
|
35686
|
+
const appliedMessages = [];
|
|
35687
|
+
for (const action of plan.keepUpdates) {
|
|
35688
|
+
const result = await runOpenVikingWriteWithRetry(
|
|
35689
|
+
ov,
|
|
35690
|
+
config2,
|
|
35691
|
+
action.uri,
|
|
35692
|
+
withIdentity2(config2, [
|
|
35693
|
+
"write",
|
|
35694
|
+
action.uri,
|
|
35695
|
+
"--content",
|
|
35696
|
+
action.content,
|
|
35697
|
+
"--mode",
|
|
35698
|
+
"replace",
|
|
35699
|
+
"--wait",
|
|
35700
|
+
"--timeout",
|
|
35701
|
+
"120"
|
|
35702
|
+
])
|
|
35703
|
+
);
|
|
35704
|
+
appliedMessages.push(`Updated kept memory: ${action.uri}`);
|
|
35705
|
+
const output = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
|
|
35706
|
+
if (output) {
|
|
35707
|
+
appliedMessages.push(output);
|
|
35708
|
+
}
|
|
35709
|
+
}
|
|
35710
|
+
for (const action of plan.archives) {
|
|
35711
|
+
const archiveResult = await archiveMemoryForCompact(config2, ov, action);
|
|
35712
|
+
if (archiveResult.isError === true) {
|
|
35713
|
+
return archiveResult;
|
|
35714
|
+
}
|
|
35715
|
+
const [content] = archiveResult.content;
|
|
35716
|
+
if (content?.type === "text") {
|
|
35717
|
+
appliedMessages.push(content.text);
|
|
35718
|
+
}
|
|
35719
|
+
}
|
|
35720
|
+
for (const action of plan.forgets) {
|
|
35721
|
+
const removed = await removeVikingResourceWithRetry(ov, config2, action.uri);
|
|
35722
|
+
appliedMessages.push(
|
|
35723
|
+
removed ? `Forgot exact duplicate: ${action.uri}` : `Exact duplicate is still processing; retry later with forget: ${action.uri}`
|
|
35724
|
+
);
|
|
35725
|
+
}
|
|
35726
|
+
return {
|
|
35727
|
+
content: [
|
|
35728
|
+
{
|
|
35729
|
+
type: "text",
|
|
35730
|
+
text: [planText, "", "Applied actions:", ...appliedMessages.map((message) => `- ${message}`)].join("\n")
|
|
35731
|
+
}
|
|
35732
|
+
]
|
|
35733
|
+
};
|
|
35734
|
+
} catch (err) {
|
|
35735
|
+
return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
|
|
35736
|
+
}
|
|
35737
|
+
}
|
|
35738
|
+
);
|
|
35739
|
+
}
|
|
35740
|
+
async function archiveMemoryForCompact(config2, ov, action) {
|
|
35741
|
+
const readResult = await runCommand(ov, withIdentity2(config2, ["read", action.uri]));
|
|
35742
|
+
const original = readResult.stdout.trim();
|
|
35743
|
+
if (!original) {
|
|
35744
|
+
return { content: [{ type: "text", text: `Could not read ${action.uri} before archiving.` }], isError: true };
|
|
35745
|
+
}
|
|
35746
|
+
const archiveResult = await writeDurableMemory(config2, {
|
|
35747
|
+
bodyText: ["Archived original Threadnote memory.", "", original].join("\n"),
|
|
35748
|
+
metadata: {
|
|
35749
|
+
archivedFrom: action.uri,
|
|
35750
|
+
kind: action.kind,
|
|
35751
|
+
project: action.project,
|
|
35752
|
+
sourceAgentClient: "mcp",
|
|
35753
|
+
status: "archived",
|
|
35754
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
35755
|
+
topic: action.topic
|
|
35756
|
+
}
|
|
35757
|
+
});
|
|
35758
|
+
if (archiveResult.isError === true) {
|
|
35759
|
+
return archiveResult;
|
|
35760
|
+
}
|
|
35761
|
+
const removedOriginal = await removeVikingResourceWithRetry(ov, config2, action.uri);
|
|
35762
|
+
const [content] = archiveResult.content;
|
|
35763
|
+
const text = content?.type === "text" ? content.text : "Archived memory stored.";
|
|
35764
|
+
return {
|
|
35765
|
+
content: [
|
|
35766
|
+
{
|
|
35767
|
+
type: "text",
|
|
35768
|
+
text: removedOriginal ? `${text}
|
|
35769
|
+
Archived original memory: ${action.uri}` : `${text}
|
|
35770
|
+
Archive stored, but original memory is still processing. Retry later with forget: ${action.uri}`
|
|
35771
|
+
}
|
|
35772
|
+
]
|
|
35773
|
+
};
|
|
35774
|
+
}
|
|
35775
|
+
async function scopedCompactRecords(config2, options) {
|
|
35776
|
+
const kinds = options.kind ? [options.kind] : ["handoff", "durable", "incident"];
|
|
35777
|
+
const records = [];
|
|
35778
|
+
for (const kind of kinds) {
|
|
35779
|
+
const directory = localMemoryDirectoryForCompact(config2, kind, options.project);
|
|
35780
|
+
const uriDirectory = memoryUriDirectoryForCompact(config2, kind, options.project);
|
|
35781
|
+
let entries;
|
|
35782
|
+
try {
|
|
35783
|
+
entries = await (0, import_promises4.readdir)(directory, { withFileTypes: true });
|
|
35784
|
+
} catch (_err) {
|
|
35785
|
+
continue;
|
|
35786
|
+
}
|
|
35787
|
+
for (const entry of entries) {
|
|
35788
|
+
if (!entry.isFile() || entry.name.startsWith(".") || !entry.name.endsWith(".md")) {
|
|
35789
|
+
continue;
|
|
35790
|
+
}
|
|
35791
|
+
const content = await readTextIfExists((0, import_node_path4.join)(directory, entry.name));
|
|
35792
|
+
if (!content) {
|
|
35793
|
+
continue;
|
|
35794
|
+
}
|
|
35795
|
+
const record2 = parseMemoryDocument(`${uriDirectory}/${entry.name}`, content);
|
|
35796
|
+
if (record2) {
|
|
35797
|
+
records.push(record2);
|
|
35798
|
+
}
|
|
35799
|
+
}
|
|
35800
|
+
}
|
|
35801
|
+
return records;
|
|
35802
|
+
}
|
|
35803
|
+
async function readMemoryRecordsByUri(config2, uris) {
|
|
35804
|
+
const records = [];
|
|
35805
|
+
for (const uri of uris) {
|
|
35806
|
+
const localPath = localMemoryPathForUri(config2, uri);
|
|
35807
|
+
if (!localPath) {
|
|
35808
|
+
continue;
|
|
35809
|
+
}
|
|
35810
|
+
const content = await readTextIfExists(localPath);
|
|
35811
|
+
if (!content) {
|
|
35812
|
+
continue;
|
|
35813
|
+
}
|
|
35814
|
+
const record2 = parseMemoryDocument(uri, content);
|
|
35815
|
+
if (record2) {
|
|
35816
|
+
records.push(record2);
|
|
35817
|
+
}
|
|
35818
|
+
}
|
|
35819
|
+
return records;
|
|
35820
|
+
}
|
|
35821
|
+
function localMemoryDirectoryForCompact(config2, kind, project) {
|
|
35822
|
+
const root = localUserMemoriesRoot(config2);
|
|
35823
|
+
const projectSegment = uriSegment2(project);
|
|
35824
|
+
switch (kind) {
|
|
35825
|
+
case "durable":
|
|
35826
|
+
return (0, import_node_path4.join)(root, "durable", "projects", projectSegment);
|
|
35827
|
+
case "handoff":
|
|
35828
|
+
return (0, import_node_path4.join)(root, "handoffs", "active", projectSegment);
|
|
35829
|
+
case "incident":
|
|
35830
|
+
return (0, import_node_path4.join)(root, "incidents", "active", projectSegment);
|
|
35831
|
+
}
|
|
35832
|
+
}
|
|
35833
|
+
function memoryUriDirectoryForCompact(config2, kind, project) {
|
|
35834
|
+
const base = `viking://user/${uriSegment2(config2.user)}/memories`;
|
|
35835
|
+
const projectSegment = uriSegment2(project);
|
|
35836
|
+
switch (kind) {
|
|
35837
|
+
case "durable":
|
|
35838
|
+
return `${base}/durable/projects/${projectSegment}`;
|
|
35839
|
+
case "handoff":
|
|
35840
|
+
return `${base}/handoffs/active/${projectSegment}`;
|
|
35841
|
+
case "incident":
|
|
35842
|
+
return `${base}/incidents/active/${projectSegment}`;
|
|
35843
|
+
}
|
|
35844
|
+
}
|
|
35845
|
+
function localMemoryPathForUri(config2, uri) {
|
|
35846
|
+
const prefix = `viking://user/${uriSegment2(config2.user)}/memories/`;
|
|
35847
|
+
if (!uri.startsWith(prefix) || uri.includes("/shared/")) {
|
|
35848
|
+
return void 0;
|
|
35849
|
+
}
|
|
35850
|
+
const relative2 = uri.slice(prefix.length);
|
|
35851
|
+
if (relative2.includes("..") || relative2.startsWith("/")) {
|
|
35852
|
+
return void 0;
|
|
35853
|
+
}
|
|
35854
|
+
return (0, import_node_path4.join)(localUserMemoriesRoot(config2), ...relative2.split("/"));
|
|
35855
|
+
}
|
|
35856
|
+
function localUserMemoriesRoot(config2) {
|
|
35857
|
+
return (0, import_node_path4.join)(config2.agentContextHome, "data", "viking", config2.account, "user", uriSegment2(config2.user), "memories");
|
|
35858
|
+
}
|
|
35859
|
+
async function readTextIfExists(path) {
|
|
35860
|
+
try {
|
|
35861
|
+
return await (0, import_promises4.readFile)(path, "utf8");
|
|
35862
|
+
} catch (_err) {
|
|
35863
|
+
return void 0;
|
|
35864
|
+
}
|
|
35865
|
+
}
|
|
35126
35866
|
async function writeDurableMemory(config2, params) {
|
|
35127
35867
|
try {
|
|
35128
35868
|
const ov = await requiredOpenVikingCli2();
|
|
35129
35869
|
const directoryUri = memoryDirectoryUri(config2, params.metadata);
|
|
35130
35870
|
await ensureMemoryDirectory(ov, config2, directoryUri);
|
|
35131
35871
|
const candidateMetadata = { ...params.metadata, supersedes: params.replaceUri };
|
|
35132
|
-
const candidateMemory =
|
|
35872
|
+
const candidateMemory = formatMemoryDocument2("MEMORY", candidateMetadata, params.bodyText);
|
|
35133
35873
|
const memoryUri = memoryUriFor(config2, candidateMemory, candidateMetadata);
|
|
35134
35874
|
const isInPlaceUpdate = params.replaceUri !== void 0 && params.replaceUri === memoryUri;
|
|
35135
35875
|
const finalMetadata = isInPlaceUpdate ? { ...params.metadata, supersedes: void 0 } : candidateMetadata;
|
|
35136
|
-
const memory = isInPlaceUpdate ?
|
|
35876
|
+
const memory = isInPlaceUpdate ? formatMemoryDocument2("MEMORY", finalMetadata, params.bodyText) : candidateMemory;
|
|
35137
35877
|
const writeMode = await memoryWriteMode(ov, config2, memoryUri, finalMetadata);
|
|
35138
35878
|
const result = await runOpenVikingWriteWithRetry(
|
|
35139
35879
|
ov,
|
|
@@ -35280,7 +36020,7 @@ function exactMemoryScopes(config2, includeArchived) {
|
|
|
35280
36020
|
];
|
|
35281
36021
|
return includeArchived ? [...scopes, `${userBase}/durable/archived`, `${userBase}/handoffs/archived`, `${userBase}/incidents/archived`] : scopes;
|
|
35282
36022
|
}
|
|
35283
|
-
function
|
|
36023
|
+
function formatMemoryDocument2(title, metadata, body) {
|
|
35284
36024
|
const header = [
|
|
35285
36025
|
title,
|
|
35286
36026
|
`kind: ${metadata.kind}`,
|
|
@@ -35294,7 +36034,7 @@ function formatMemoryDocument(title, metadata, body) {
|
|
|
35294
36034
|
].filter((line) => line !== void 0);
|
|
35295
36035
|
return [...header, "", body.trim()].join("\n");
|
|
35296
36036
|
}
|
|
35297
|
-
function
|
|
36037
|
+
function normalizeOptionalMetadata2(value) {
|
|
35298
36038
|
const trimmed = value?.trim();
|
|
35299
36039
|
return trimmed ? trimmed : void 0;
|
|
35300
36040
|
}
|
|
@@ -35484,7 +36224,7 @@ function withIdentity2(config2, args) {
|
|
|
35484
36224
|
return [...args, "--account", config2.account, "--user", config2.user, "--agent-id", config2.agentId];
|
|
35485
36225
|
}
|
|
35486
36226
|
async function requiredOpenVikingCli2() {
|
|
35487
|
-
const command = process.env.THREADNOTE_OV ?? await findExecutable(["ov", "openviking"]) ?? await firstExistingPath([(0,
|
|
36227
|
+
const command = process.env.THREADNOTE_OV ?? await findExecutable(["ov", "openviking"]) ?? await firstExistingPath([(0, import_node_path4.join)((0, import_node_os3.homedir)(), ".local", "bin", "ov"), (0, import_node_path4.join)((0, import_node_os3.homedir)(), ".local", "bin", "openviking")]);
|
|
35488
36228
|
if (!command) {
|
|
35489
36229
|
throw new Error("Neither ov nor openviking was found. Run threadnote install first.");
|
|
35490
36230
|
}
|