threadnote 0.7.5 → 0.7.7
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 +38 -13
- package/dist/mcp_server.cjs +845 -38
- package/dist/threadnote.cjs +891 -146
- package/docs/agent-instructions.md +29 -7
- package/docs/index.html +1 -1
- package/docs/migration.md +4 -2
- package/docs/share.md +18 -1
- package/package.json +1 -1
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,11 +34722,39 @@ 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) {
|
|
34236
|
-
return
|
|
34729
|
+
return sharedTeamNameForUri(config2, uri) !== void 0;
|
|
34730
|
+
}
|
|
34731
|
+
function sharedTeamNameForUri(config2, uri) {
|
|
34732
|
+
const prefix = `viking://user/${uriSegment(config2.user)}/memories/${SHARED_SEGMENT}/`;
|
|
34733
|
+
if (!uri.startsWith(prefix)) {
|
|
34734
|
+
return void 0;
|
|
34735
|
+
}
|
|
34736
|
+
const [team] = uri.slice(prefix.length).split("/");
|
|
34737
|
+
return team || void 0;
|
|
34738
|
+
}
|
|
34739
|
+
function sharedMemoryUriParts(config2, uri) {
|
|
34740
|
+
const prefix = `viking://user/${uriSegment(config2.user)}/memories/${SHARED_SEGMENT}/`;
|
|
34741
|
+
if (!uri.startsWith(prefix)) {
|
|
34742
|
+
return void 0;
|
|
34743
|
+
}
|
|
34744
|
+
const [team, kind, scope, project, ...topicParts] = uri.slice(prefix.length).split("/");
|
|
34745
|
+
if (!team) {
|
|
34746
|
+
return void 0;
|
|
34747
|
+
}
|
|
34748
|
+
if (kind !== "durable" || scope !== "projects" || !project || topicParts.length === 0) {
|
|
34749
|
+
return { team };
|
|
34750
|
+
}
|
|
34751
|
+
const topicPath = topicParts.join("/");
|
|
34752
|
+
return {
|
|
34753
|
+
kind,
|
|
34754
|
+
project,
|
|
34755
|
+
team,
|
|
34756
|
+
topic: topicPath.endsWith(".md") ? topicPath.slice(0, -".md".length) : topicPath
|
|
34757
|
+
};
|
|
34237
34758
|
}
|
|
34238
34759
|
function sharedUriFor(config2, personalUri, team) {
|
|
34239
34760
|
const prefix = `viking://user/${uriSegment(config2.user)}/memories/`;
|
|
@@ -34326,8 +34847,8 @@ async function writeMemoryFile(config2, ov, uri, content, initialMode, dryRun, o
|
|
|
34326
34847
|
}
|
|
34327
34848
|
return;
|
|
34328
34849
|
}
|
|
34329
|
-
const stagingDir = await (0, import_promises3.mkdtemp)((0,
|
|
34330
|
-
const tempPath = (0,
|
|
34850
|
+
const stagingDir = await (0, import_promises3.mkdtemp)((0, import_node_path3.join)((0, import_node_os2.tmpdir)(), "threadnote-share-"));
|
|
34851
|
+
const tempPath = (0, import_node_path3.join)(stagingDir, "body.txt");
|
|
34331
34852
|
try {
|
|
34332
34853
|
await (0, import_promises3.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
|
|
34333
34854
|
await writeOvFileWithRetry(config2, ov, uri, tempPath, initialMode, options);
|
|
@@ -34449,7 +34970,7 @@ async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
|
|
|
34449
34970
|
if (!relative2) {
|
|
34450
34971
|
return;
|
|
34451
34972
|
}
|
|
34452
|
-
await (0, import_promises3.rm)((0,
|
|
34973
|
+
await (0, import_promises3.rm)((0, import_node_path3.join)(worktree, relative2), { force: true });
|
|
34453
34974
|
}
|
|
34454
34975
|
async function removeMemoryUri(config2, ov, uri, dryRun, options = {}) {
|
|
34455
34976
|
const args = withIdentity(config2, ["rm", uri]);
|
|
@@ -34513,8 +35034,8 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
|
|
|
34513
35034
|
const oldRel = entries[index + 1];
|
|
34514
35035
|
const newRel = entries[index + 2];
|
|
34515
35036
|
if (oldRel && newRel) {
|
|
34516
|
-
changes.push({ path: (0,
|
|
34517
|
-
changes.push({ path: (0,
|
|
35037
|
+
changes.push({ path: (0, import_node_path3.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
|
|
35038
|
+
changes.push({ path: (0, import_node_path3.join)(worktree, newRel), relativePath: newRel, status: "added" });
|
|
34518
35039
|
}
|
|
34519
35040
|
index += 3;
|
|
34520
35041
|
continue;
|
|
@@ -34522,7 +35043,7 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
|
|
|
34522
35043
|
const rel = entries[index + 1];
|
|
34523
35044
|
if (rel) {
|
|
34524
35045
|
const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
|
|
34525
|
-
changes.push({ path: (0,
|
|
35046
|
+
changes.push({ path: (0, import_node_path3.join)(worktree, rel), relativePath: rel, status });
|
|
34526
35047
|
}
|
|
34527
35048
|
index += 2;
|
|
34528
35049
|
}
|
|
@@ -34604,13 +35125,14 @@ async function main() {
|
|
|
34604
35125
|
"",
|
|
34605
35126
|
"Stdio MCP adapter for Threadnote shared local context.",
|
|
34606
35127
|
"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"}).',
|
|
35128
|
+
'Always pass JSON arguments. Example: recall_context({"query":"current repo latest handoff","callerCwd":"/absolute/workspace/path"}).',
|
|
34608
35129
|
"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
35130
|
"Older clients may use the compatibility aliases `search`, `read`, and `list`.",
|
|
34610
35131
|
'For durable facts, store kind="durable"; for current work logs, store kind="handoff" with project/topic so Threadnote keeps one active memory updated.',
|
|
34611
35132
|
"When a handoff describes an active branch or feature, recall durable feature memories for the same branch/topic before coding.",
|
|
34612
35133
|
"During feature work, update durable feature knowledge when valuable implementation details, decisions, interfaces, or gotchas change.",
|
|
34613
35134
|
"When updating the same active issue, pass project/topic or replaceUri to remember_context so duplicate durable memories or handoffs do not accumulate.",
|
|
35135
|
+
"Use compact_context with dryRun=true for scoped memory hygiene when recall surfaces overlapping active memories.",
|
|
34614
35136
|
"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
35137
|
"Do not store secrets, customer data, raw production logs, or credentials."
|
|
34616
35138
|
].join("\n")
|
|
@@ -34635,7 +35157,7 @@ function registerTools(server, config2) {
|
|
|
34635
35157
|
server,
|
|
34636
35158
|
config2,
|
|
34637
35159
|
"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"}.`
|
|
35160
|
+
`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
35161
|
);
|
|
34640
35162
|
registerSearchTool(
|
|
34641
35163
|
server,
|
|
@@ -34666,6 +35188,7 @@ function registerTools(server, config2) {
|
|
|
34666
35188
|
"Archive a memory so it remains readable as provenance but is no longer current working context."
|
|
34667
35189
|
);
|
|
34668
35190
|
registerArchiveTool(server, config2, "archive", "Compatibility alias for archive_context.");
|
|
35191
|
+
registerCompactTool(server, config2);
|
|
34669
35192
|
server.registerTool(
|
|
34670
35193
|
"forget",
|
|
34671
35194
|
{
|
|
@@ -34828,11 +35351,12 @@ function registerSearchTool(server, config2, name, description) {
|
|
|
34828
35351
|
inputSchema: {
|
|
34829
35352
|
query: external_exports.string().optional().describe('Required search query, for example "unity-ui-ccc latest handoff"'),
|
|
34830
35353
|
uri: external_exports.string().optional().describe("Optional viking:// subtree to search"),
|
|
35354
|
+
callerCwd: external_exports.string().optional().describe("Optional absolute caller workspace path used to resolve this/current branch queries"),
|
|
34831
35355
|
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
|
|
35356
|
+
includeArchived: external_exports.boolean().optional().describe("Include archived memories in exact memory/resource matches")
|
|
34833
35357
|
}
|
|
34834
35358
|
},
|
|
34835
|
-
async ({ includeArchived, nodeLimit, query, uri }) => {
|
|
35359
|
+
async ({ callerCwd, includeArchived, nodeLimit, query, uri }) => {
|
|
34836
35360
|
const checkedQuery = requiredText(query, name, "query", { query: "unity-ui-ccc latest handoff" });
|
|
34837
35361
|
if (!checkedQuery.ok) {
|
|
34838
35362
|
return checkedQuery.error;
|
|
@@ -34842,6 +35366,7 @@ function registerSearchTool(server, config2, name, description) {
|
|
|
34842
35366
|
return checkedUri.error;
|
|
34843
35367
|
}
|
|
34844
35368
|
return runRecallTool(config2, {
|
|
35369
|
+
callerCwd,
|
|
34845
35370
|
query: checkedQuery.value,
|
|
34846
35371
|
pinnedUri: checkedUri.value,
|
|
34847
35372
|
nodeLimit,
|
|
@@ -34860,9 +35385,18 @@ async function runRecallTool(config2, params) {
|
|
|
34860
35385
|
} catch (err) {
|
|
34861
35386
|
syncWarnings.push(errorMessage(err));
|
|
34862
35387
|
}
|
|
35388
|
+
const query = await enrichRecallQueryWithWorkspaceContext(params.query, {
|
|
35389
|
+
cwd: params.callerCwd,
|
|
35390
|
+
includeProcessCwd: false
|
|
35391
|
+
});
|
|
35392
|
+
const projectQuery = await enrichRecallQueryWithWorkspaceProjectContext(params.query, {
|
|
35393
|
+
cwd: params.callerCwd,
|
|
35394
|
+
includeProcessCwd: false
|
|
35395
|
+
});
|
|
35396
|
+
const contextualParams = { ...params, query };
|
|
34863
35397
|
const baseArgs = [
|
|
34864
35398
|
"search",
|
|
34865
|
-
|
|
35399
|
+
query,
|
|
34866
35400
|
...params.pinnedUri ? ["--uri", params.pinnedUri] : [],
|
|
34867
35401
|
...params.nodeLimit ? ["--node-limit", String(params.nodeLimit)] : []
|
|
34868
35402
|
];
|
|
@@ -34878,15 +35412,19 @@ async function runRecallTool(config2, params) {
|
|
|
34878
35412
|
if (firstContent.text) {
|
|
34879
35413
|
sections.push(firstContent.text);
|
|
34880
35414
|
}
|
|
34881
|
-
const seededSection = await seededResourcesSection(config2,
|
|
35415
|
+
const seededSection = await seededResourcesSection(config2, contextualParams, projectQuery);
|
|
34882
35416
|
if (seededSection) {
|
|
34883
35417
|
sections.push(seededSection);
|
|
34884
35418
|
}
|
|
34885
|
-
const exactMatches = await exactMemoryMatchesText(config2,
|
|
35419
|
+
const exactMatches = await exactMemoryMatchesText(config2, query, params.includeArchived);
|
|
34886
35420
|
if (exactMatches) {
|
|
34887
|
-
sections.push(`Exact
|
|
35421
|
+
sections.push(`Exact memory/resource matches:
|
|
34888
35422
|
${exactMatches}`);
|
|
34889
35423
|
}
|
|
35424
|
+
const hygieneHints = await recallHygieneHintsSection(config2, sections.join("\n\n"));
|
|
35425
|
+
if (hygieneHints) {
|
|
35426
|
+
sections.push(hygieneHints);
|
|
35427
|
+
}
|
|
34890
35428
|
if (syncedTeams.length > 0) {
|
|
34891
35429
|
sections.push(`Auto-synced shared memories: ${syncedTeams.join(", ")}`);
|
|
34892
35430
|
}
|
|
@@ -34898,11 +35436,20 @@ ${exactMatches}`);
|
|
|
34898
35436
|
}
|
|
34899
35437
|
return { ...semanticResult, content: [{ type: "text", text: sections.join("\n\n") }] };
|
|
34900
35438
|
}
|
|
34901
|
-
async function
|
|
35439
|
+
async function recallHygieneHintsSection(config2, recallText) {
|
|
35440
|
+
const uris = activePersonalMemoryUrisFromText(recallText, config2.user);
|
|
35441
|
+
if (uris.length === 0) {
|
|
35442
|
+
return void 0;
|
|
35443
|
+
}
|
|
35444
|
+
const records = await readMemoryRecordsByUri(config2, uris);
|
|
35445
|
+
const nudges = recallHygieneNudges(recallText, { records, user: config2.user });
|
|
35446
|
+
return nudges.length > 0 ? ["Memory hygiene hints:", ...nudges.map((nudge) => `- ${nudge}`)].join("\n") : void 0;
|
|
35447
|
+
}
|
|
35448
|
+
async function seededResourcesSection(config2, params, projectQuery) {
|
|
34902
35449
|
if (params.pinnedUri) {
|
|
34903
35450
|
return void 0;
|
|
34904
35451
|
}
|
|
34905
|
-
const project = await inferProjectFromQuery(config2.manifestPath,
|
|
35452
|
+
const project = await inferProjectFromQuery(config2.manifestPath, projectQuery);
|
|
34906
35453
|
if (!project) {
|
|
34907
35454
|
return void 0;
|
|
34908
35455
|
}
|
|
@@ -35028,7 +35575,9 @@ function registerStoreTool(server, config2, name, description) {
|
|
|
35028
35575
|
inputSchema: {
|
|
35029
35576
|
kind: external_exports.enum(["durable", "handoff", "incident", "preference", "smoke"]).optional().describe("Memory lifecycle kind; durable facts and handoffs are most common"),
|
|
35030
35577
|
project: external_exports.string().optional().describe("Project/repo namespace, for example threadnote or mobile-native"),
|
|
35031
|
-
replaceUri: external_exports.string().optional().describe(
|
|
35578
|
+
replaceUri: external_exports.string().optional().describe(
|
|
35579
|
+
"Optional viking:// memory URI to replace. Shared URIs are updated in place and pushed; personal URIs are forgotten after the replacement is safely stored."
|
|
35580
|
+
),
|
|
35032
35581
|
text: external_exports.string().optional().describe("Required memory text to store"),
|
|
35033
35582
|
sourceAgentClient: external_exports.string().optional().describe("Originating client, for example cursor, copilot, codex, or claude"),
|
|
35034
35583
|
status: external_exports.enum(["active", "archived", "superseded"]).optional().describe("Memory lifecycle status"),
|
|
@@ -35046,11 +35595,11 @@ function registerStoreTool(server, config2, name, description) {
|
|
|
35046
35595
|
}
|
|
35047
35596
|
const metadata = {
|
|
35048
35597
|
kind: kind ?? "durable",
|
|
35049
|
-
project:
|
|
35598
|
+
project: normalizeOptionalMetadata2(project),
|
|
35050
35599
|
sourceAgentClient: sourceAgentClient ?? "mcp",
|
|
35051
35600
|
status: status ?? "active",
|
|
35052
35601
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
35053
|
-
topic:
|
|
35602
|
+
topic: normalizeOptionalMetadata2(topic)
|
|
35054
35603
|
};
|
|
35055
35604
|
return writeDurableMemory(config2, {
|
|
35056
35605
|
bodyText: checkedText.value,
|
|
@@ -35091,11 +35640,11 @@ function registerArchiveTool(server, config2, name, description) {
|
|
|
35091
35640
|
const metadata = {
|
|
35092
35641
|
archivedFrom: checkedUri.value,
|
|
35093
35642
|
kind: kind ?? "handoff",
|
|
35094
|
-
project:
|
|
35643
|
+
project: normalizeOptionalMetadata2(project),
|
|
35095
35644
|
sourceAgentClient: "mcp",
|
|
35096
35645
|
status: "archived",
|
|
35097
35646
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
35098
|
-
topic:
|
|
35647
|
+
topic: normalizeOptionalMetadata2(topic)
|
|
35099
35648
|
};
|
|
35100
35649
|
const archiveResult = await writeDurableMemory(config2, {
|
|
35101
35650
|
bodyText: ["Archived original Threadnote memory.", "", original].join("\n"),
|
|
@@ -35123,17 +35672,241 @@ Archive stored, but original memory is still processing. Retry later with forget
|
|
|
35123
35672
|
}
|
|
35124
35673
|
);
|
|
35125
35674
|
}
|
|
35675
|
+
function registerCompactTool(server, config2) {
|
|
35676
|
+
server.registerTool(
|
|
35677
|
+
"compact_context",
|
|
35678
|
+
{
|
|
35679
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
35680
|
+
description: "Plan or apply scoped Threadnote memory hygiene. Defaults to dry-run; pass apply=true to archive stale handoffs and forget exact duplicates.",
|
|
35681
|
+
inputSchema: {
|
|
35682
|
+
apply: external_exports.boolean().optional().describe("Apply the compact plan; defaults to false"),
|
|
35683
|
+
dryRun: external_exports.boolean().optional().describe("Keep the call read-only; defaults to true unless apply=true"),
|
|
35684
|
+
kind: external_exports.enum(["durable", "handoff", "incident"]).optional().describe("Optional memory kind filter"),
|
|
35685
|
+
project: external_exports.string().optional().describe("Required project/repo namespace, for example threadnote"),
|
|
35686
|
+
topic: external_exports.string().optional().describe("Optional stable topic name")
|
|
35687
|
+
}
|
|
35688
|
+
},
|
|
35689
|
+
async ({ apply, dryRun, kind, project, topic }) => {
|
|
35690
|
+
const checkedProject = requiredText(project, "compact_context", "project", { project: "threadnote" });
|
|
35691
|
+
if (!checkedProject.ok) {
|
|
35692
|
+
return checkedProject.error;
|
|
35693
|
+
}
|
|
35694
|
+
if (apply === true && dryRun === true) {
|
|
35695
|
+
return {
|
|
35696
|
+
content: [{ type: "text", text: "compact_context cannot combine apply=true with dryRun=true." }],
|
|
35697
|
+
isError: true
|
|
35698
|
+
};
|
|
35699
|
+
}
|
|
35700
|
+
try {
|
|
35701
|
+
const records = await scopedCompactRecords(config2, {
|
|
35702
|
+
kind,
|
|
35703
|
+
project: checkedProject.value
|
|
35704
|
+
});
|
|
35705
|
+
const plan = buildCompactPlan(records, {
|
|
35706
|
+
kind,
|
|
35707
|
+
project: checkedProject.value,
|
|
35708
|
+
topic: normalizeOptionalMetadata2(topic)
|
|
35709
|
+
});
|
|
35710
|
+
const shouldApply = apply === true;
|
|
35711
|
+
const planText = formatCompactPlan(plan, { apply: shouldApply });
|
|
35712
|
+
if (!shouldApply) {
|
|
35713
|
+
return { content: [{ type: "text", text: planText }] };
|
|
35714
|
+
}
|
|
35715
|
+
const ov = await requiredOpenVikingCli2();
|
|
35716
|
+
const appliedMessages = [];
|
|
35717
|
+
for (const action of plan.keepUpdates) {
|
|
35718
|
+
const result = await runOpenVikingWriteWithRetry(
|
|
35719
|
+
ov,
|
|
35720
|
+
config2,
|
|
35721
|
+
action.uri,
|
|
35722
|
+
withIdentity2(config2, [
|
|
35723
|
+
"write",
|
|
35724
|
+
action.uri,
|
|
35725
|
+
"--content",
|
|
35726
|
+
action.content,
|
|
35727
|
+
"--mode",
|
|
35728
|
+
"replace",
|
|
35729
|
+
"--wait",
|
|
35730
|
+
"--timeout",
|
|
35731
|
+
"120"
|
|
35732
|
+
])
|
|
35733
|
+
);
|
|
35734
|
+
appliedMessages.push(`Updated kept memory: ${action.uri}`);
|
|
35735
|
+
const output = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
|
|
35736
|
+
if (output) {
|
|
35737
|
+
appliedMessages.push(output);
|
|
35738
|
+
}
|
|
35739
|
+
}
|
|
35740
|
+
for (const action of plan.archives) {
|
|
35741
|
+
const archiveResult = await archiveMemoryForCompact(config2, ov, action);
|
|
35742
|
+
if (archiveResult.isError === true) {
|
|
35743
|
+
return archiveResult;
|
|
35744
|
+
}
|
|
35745
|
+
const [content] = archiveResult.content;
|
|
35746
|
+
if (content?.type === "text") {
|
|
35747
|
+
appliedMessages.push(content.text);
|
|
35748
|
+
}
|
|
35749
|
+
}
|
|
35750
|
+
for (const action of plan.forgets) {
|
|
35751
|
+
const removed = await removeVikingResourceWithRetry(ov, config2, action.uri);
|
|
35752
|
+
appliedMessages.push(
|
|
35753
|
+
removed ? `Forgot exact duplicate: ${action.uri}` : `Exact duplicate is still processing; retry later with forget: ${action.uri}`
|
|
35754
|
+
);
|
|
35755
|
+
}
|
|
35756
|
+
return {
|
|
35757
|
+
content: [
|
|
35758
|
+
{
|
|
35759
|
+
type: "text",
|
|
35760
|
+
text: [planText, "", "Applied actions:", ...appliedMessages.map((message) => `- ${message}`)].join("\n")
|
|
35761
|
+
}
|
|
35762
|
+
]
|
|
35763
|
+
};
|
|
35764
|
+
} catch (err) {
|
|
35765
|
+
return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
|
|
35766
|
+
}
|
|
35767
|
+
}
|
|
35768
|
+
);
|
|
35769
|
+
}
|
|
35770
|
+
async function archiveMemoryForCompact(config2, ov, action) {
|
|
35771
|
+
const readResult = await runCommand(ov, withIdentity2(config2, ["read", action.uri]));
|
|
35772
|
+
const original = readResult.stdout.trim();
|
|
35773
|
+
if (!original) {
|
|
35774
|
+
return { content: [{ type: "text", text: `Could not read ${action.uri} before archiving.` }], isError: true };
|
|
35775
|
+
}
|
|
35776
|
+
const archiveResult = await writeDurableMemory(config2, {
|
|
35777
|
+
bodyText: ["Archived original Threadnote memory.", "", original].join("\n"),
|
|
35778
|
+
metadata: {
|
|
35779
|
+
archivedFrom: action.uri,
|
|
35780
|
+
kind: action.kind,
|
|
35781
|
+
project: action.project,
|
|
35782
|
+
sourceAgentClient: "mcp",
|
|
35783
|
+
status: "archived",
|
|
35784
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
35785
|
+
topic: action.topic
|
|
35786
|
+
}
|
|
35787
|
+
});
|
|
35788
|
+
if (archiveResult.isError === true) {
|
|
35789
|
+
return archiveResult;
|
|
35790
|
+
}
|
|
35791
|
+
const removedOriginal = await removeVikingResourceWithRetry(ov, config2, action.uri);
|
|
35792
|
+
const [content] = archiveResult.content;
|
|
35793
|
+
const text = content?.type === "text" ? content.text : "Archived memory stored.";
|
|
35794
|
+
return {
|
|
35795
|
+
content: [
|
|
35796
|
+
{
|
|
35797
|
+
type: "text",
|
|
35798
|
+
text: removedOriginal ? `${text}
|
|
35799
|
+
Archived original memory: ${action.uri}` : `${text}
|
|
35800
|
+
Archive stored, but original memory is still processing. Retry later with forget: ${action.uri}`
|
|
35801
|
+
}
|
|
35802
|
+
]
|
|
35803
|
+
};
|
|
35804
|
+
}
|
|
35805
|
+
async function scopedCompactRecords(config2, options) {
|
|
35806
|
+
const kinds = options.kind ? [options.kind] : ["handoff", "durable", "incident"];
|
|
35807
|
+
const records = [];
|
|
35808
|
+
for (const kind of kinds) {
|
|
35809
|
+
const directory = localMemoryDirectoryForCompact(config2, kind, options.project);
|
|
35810
|
+
const uriDirectory = memoryUriDirectoryForCompact(config2, kind, options.project);
|
|
35811
|
+
let entries;
|
|
35812
|
+
try {
|
|
35813
|
+
entries = await (0, import_promises4.readdir)(directory, { withFileTypes: true });
|
|
35814
|
+
} catch (_err) {
|
|
35815
|
+
continue;
|
|
35816
|
+
}
|
|
35817
|
+
for (const entry of entries) {
|
|
35818
|
+
if (!entry.isFile() || entry.name.startsWith(".") || !entry.name.endsWith(".md")) {
|
|
35819
|
+
continue;
|
|
35820
|
+
}
|
|
35821
|
+
const content = await readTextIfExists((0, import_node_path4.join)(directory, entry.name));
|
|
35822
|
+
if (!content) {
|
|
35823
|
+
continue;
|
|
35824
|
+
}
|
|
35825
|
+
const record2 = parseMemoryDocument(`${uriDirectory}/${entry.name}`, content);
|
|
35826
|
+
if (record2) {
|
|
35827
|
+
records.push(record2);
|
|
35828
|
+
}
|
|
35829
|
+
}
|
|
35830
|
+
}
|
|
35831
|
+
return records;
|
|
35832
|
+
}
|
|
35833
|
+
async function readMemoryRecordsByUri(config2, uris) {
|
|
35834
|
+
const records = [];
|
|
35835
|
+
for (const uri of uris) {
|
|
35836
|
+
const localPath = localMemoryPathForUri(config2, uri);
|
|
35837
|
+
if (!localPath) {
|
|
35838
|
+
continue;
|
|
35839
|
+
}
|
|
35840
|
+
const content = await readTextIfExists(localPath);
|
|
35841
|
+
if (!content) {
|
|
35842
|
+
continue;
|
|
35843
|
+
}
|
|
35844
|
+
const record2 = parseMemoryDocument(uri, content);
|
|
35845
|
+
if (record2) {
|
|
35846
|
+
records.push(record2);
|
|
35847
|
+
}
|
|
35848
|
+
}
|
|
35849
|
+
return records;
|
|
35850
|
+
}
|
|
35851
|
+
function localMemoryDirectoryForCompact(config2, kind, project) {
|
|
35852
|
+
const root = localUserMemoriesRoot(config2);
|
|
35853
|
+
const projectSegment = uriSegment2(project);
|
|
35854
|
+
switch (kind) {
|
|
35855
|
+
case "durable":
|
|
35856
|
+
return (0, import_node_path4.join)(root, "durable", "projects", projectSegment);
|
|
35857
|
+
case "handoff":
|
|
35858
|
+
return (0, import_node_path4.join)(root, "handoffs", "active", projectSegment);
|
|
35859
|
+
case "incident":
|
|
35860
|
+
return (0, import_node_path4.join)(root, "incidents", "active", projectSegment);
|
|
35861
|
+
}
|
|
35862
|
+
}
|
|
35863
|
+
function memoryUriDirectoryForCompact(config2, kind, project) {
|
|
35864
|
+
const base = `viking://user/${uriSegment2(config2.user)}/memories`;
|
|
35865
|
+
const projectSegment = uriSegment2(project);
|
|
35866
|
+
switch (kind) {
|
|
35867
|
+
case "durable":
|
|
35868
|
+
return `${base}/durable/projects/${projectSegment}`;
|
|
35869
|
+
case "handoff":
|
|
35870
|
+
return `${base}/handoffs/active/${projectSegment}`;
|
|
35871
|
+
case "incident":
|
|
35872
|
+
return `${base}/incidents/active/${projectSegment}`;
|
|
35873
|
+
}
|
|
35874
|
+
}
|
|
35875
|
+
function localMemoryPathForUri(config2, uri) {
|
|
35876
|
+
const prefix = `viking://user/${uriSegment2(config2.user)}/memories/`;
|
|
35877
|
+
if (!uri.startsWith(prefix) || uri.includes("/shared/")) {
|
|
35878
|
+
return void 0;
|
|
35879
|
+
}
|
|
35880
|
+
const relative2 = uri.slice(prefix.length);
|
|
35881
|
+
if (relative2.includes("..") || relative2.startsWith("/")) {
|
|
35882
|
+
return void 0;
|
|
35883
|
+
}
|
|
35884
|
+
return (0, import_node_path4.join)(localUserMemoriesRoot(config2), ...relative2.split("/"));
|
|
35885
|
+
}
|
|
35886
|
+
function localUserMemoriesRoot(config2) {
|
|
35887
|
+
return (0, import_node_path4.join)(config2.agentContextHome, "data", "viking", config2.account, "user", uriSegment2(config2.user), "memories");
|
|
35888
|
+
}
|
|
35889
|
+
async function readTextIfExists(path) {
|
|
35890
|
+
try {
|
|
35891
|
+
return await (0, import_promises4.readFile)(path, "utf8");
|
|
35892
|
+
} catch (_err) {
|
|
35893
|
+
return void 0;
|
|
35894
|
+
}
|
|
35895
|
+
}
|
|
35126
35896
|
async function writeDurableMemory(config2, params) {
|
|
35127
35897
|
try {
|
|
35128
35898
|
const ov = await requiredOpenVikingCli2();
|
|
35899
|
+
if (params.replaceUri && isInSharedNamespace(config2, params.replaceUri)) {
|
|
35900
|
+
return await writeSharedMemoryReplacement(config2, ov, params, params.replaceUri);
|
|
35901
|
+
}
|
|
35129
35902
|
const directoryUri = memoryDirectoryUri(config2, params.metadata);
|
|
35130
35903
|
await ensureMemoryDirectory(ov, config2, directoryUri);
|
|
35131
35904
|
const candidateMetadata = { ...params.metadata, supersedes: params.replaceUri };
|
|
35132
|
-
const candidateMemory =
|
|
35905
|
+
const candidateMemory = formatMemoryDocument2("MEMORY", candidateMetadata, params.bodyText);
|
|
35133
35906
|
const memoryUri = memoryUriFor(config2, candidateMemory, candidateMetadata);
|
|
35134
35907
|
const isInPlaceUpdate = params.replaceUri !== void 0 && params.replaceUri === memoryUri;
|
|
35135
35908
|
const finalMetadata = isInPlaceUpdate ? { ...params.metadata, supersedes: void 0 } : candidateMetadata;
|
|
35136
|
-
const memory = isInPlaceUpdate ?
|
|
35909
|
+
const memory = isInPlaceUpdate ? formatMemoryDocument2("MEMORY", finalMetadata, params.bodyText) : candidateMemory;
|
|
35137
35910
|
const writeMode = await memoryWriteMode(ov, config2, memoryUri, finalMetadata);
|
|
35138
35911
|
const result = await runOpenVikingWriteWithRetry(
|
|
35139
35912
|
ov,
|
|
@@ -35166,6 +35939,40 @@ async function writeDurableMemory(config2, params) {
|
|
|
35166
35939
|
return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
|
|
35167
35940
|
}
|
|
35168
35941
|
}
|
|
35942
|
+
async function writeSharedMemoryReplacement(config2, ov, params, targetUri) {
|
|
35943
|
+
if (params.metadata.kind !== "durable") {
|
|
35944
|
+
return argumentError("Shared memory replacement only supports durable memories.");
|
|
35945
|
+
}
|
|
35946
|
+
const teamName = sharedTeamNameForUri(config2, targetUri);
|
|
35947
|
+
if (!teamName) {
|
|
35948
|
+
return argumentError(`Memory ${targetUri} is not in the shared namespace.`);
|
|
35949
|
+
}
|
|
35950
|
+
const resolved = await resolveTeam(config2, teamName);
|
|
35951
|
+
const inferred = sharedMemoryUriParts(config2, targetUri);
|
|
35952
|
+
const metadata = {
|
|
35953
|
+
...params.metadata,
|
|
35954
|
+
project: params.metadata.project ?? inferred?.project,
|
|
35955
|
+
topic: params.metadata.topic ?? inferred?.topic
|
|
35956
|
+
};
|
|
35957
|
+
const rawMemory = formatMemoryDocument2("MEMORY", metadata, params.bodyText);
|
|
35958
|
+
const scrub = applyScrubber(stripPersonalProvenance(rawMemory), { redact: false });
|
|
35959
|
+
if (scrub.blocker) {
|
|
35960
|
+
return argumentError(
|
|
35961
|
+
`Refusing to update shared memory ${targetUri}: possible ${scrub.blocker}. Strip the sensitive value first.`
|
|
35962
|
+
);
|
|
35963
|
+
}
|
|
35964
|
+
await ensureSharedDirectoryChain(config2, ov, targetUri, false, { quiet: true });
|
|
35965
|
+
await writeMemoryFile(config2, ov, targetUri, scrub.cleaned, "replace", false, { quiet: true });
|
|
35966
|
+
const relativePath = vikingUriToWorktreeRelative(config2, targetUri, resolved.name);
|
|
35967
|
+
const messages = [`Updated shared memory: ${targetUri}`];
|
|
35968
|
+
for (const redaction of scrub.redactions) {
|
|
35969
|
+
messages.push(`Redacted ${redaction.count}\xD7 ${redaction.name} before shared update.`);
|
|
35970
|
+
}
|
|
35971
|
+
messages.push(
|
|
35972
|
+
...await gitPublishWorkflow(resolved.config.worktree, relativePath, `share: update ${relativePath}`, true)
|
|
35973
|
+
);
|
|
35974
|
+
return { content: [{ type: "text", text: messages.join("\n") }] };
|
|
35975
|
+
}
|
|
35169
35976
|
async function runOpenVikingWriteWithRetry(ov, config2, memoryUri, args) {
|
|
35170
35977
|
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
35171
35978
|
const result = await runCommand(ov, args, { allowFailure: true });
|
|
@@ -35280,7 +36087,7 @@ function exactMemoryScopes(config2, includeArchived) {
|
|
|
35280
36087
|
];
|
|
35281
36088
|
return includeArchived ? [...scopes, `${userBase}/durable/archived`, `${userBase}/handoffs/archived`, `${userBase}/incidents/archived`] : scopes;
|
|
35282
36089
|
}
|
|
35283
|
-
function
|
|
36090
|
+
function formatMemoryDocument2(title, metadata, body) {
|
|
35284
36091
|
const header = [
|
|
35285
36092
|
title,
|
|
35286
36093
|
`kind: ${metadata.kind}`,
|
|
@@ -35294,7 +36101,7 @@ function formatMemoryDocument(title, metadata, body) {
|
|
|
35294
36101
|
].filter((line) => line !== void 0);
|
|
35295
36102
|
return [...header, "", body.trim()].join("\n");
|
|
35296
36103
|
}
|
|
35297
|
-
function
|
|
36104
|
+
function normalizeOptionalMetadata2(value) {
|
|
35298
36105
|
const trimmed = value?.trim();
|
|
35299
36106
|
return trimmed ? trimmed : void 0;
|
|
35300
36107
|
}
|
|
@@ -35484,7 +36291,7 @@ function withIdentity2(config2, args) {
|
|
|
35484
36291
|
return [...args, "--account", config2.account, "--user", config2.user, "--agent-id", config2.agentId];
|
|
35485
36292
|
}
|
|
35486
36293
|
async function requiredOpenVikingCli2() {
|
|
35487
|
-
const command = process.env.THREADNOTE_OV ?? await findExecutable(["ov", "openviking"]) ?? await firstExistingPath([(0,
|
|
36294
|
+
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
36295
|
if (!command) {
|
|
35489
36296
|
throw new Error("Neither ov nor openviking was found. Run threadnote install first.");
|
|
35490
36297
|
}
|