threadnote 1.7.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp_server.cjs +513 -5
- package/dist/threadnote.cjs +472 -7
- package/docs/share.md +46 -4
- package/package.json +1 -1
package/dist/mcp_server.cjs
CHANGED
|
@@ -37023,7 +37023,12 @@ async function loadPendingReindexes(config2, state) {
|
|
|
37023
37023
|
}
|
|
37024
37024
|
const entry = item;
|
|
37025
37025
|
if (typeof entry.path === "string" && typeof entry.relativePath === "string" && (entry.status === "added" || entry.status === "removed" || entry.status === "modified")) {
|
|
37026
|
-
changes.push({
|
|
37026
|
+
changes.push({
|
|
37027
|
+
path: entry.path,
|
|
37028
|
+
previousContent: typeof entry.previousContent === "string" ? entry.previousContent : void 0,
|
|
37029
|
+
relativePath: entry.relativePath,
|
|
37030
|
+
status: entry.status
|
|
37031
|
+
});
|
|
37027
37032
|
}
|
|
37028
37033
|
}
|
|
37029
37034
|
if (changes.length > 0) {
|
|
@@ -37121,13 +37126,105 @@ async function fetchShareUpdateStatuses(config2) {
|
|
|
37121
37126
|
}
|
|
37122
37127
|
return statuses;
|
|
37123
37128
|
}
|
|
37129
|
+
async function listShareConflicts(config2, options = {}) {
|
|
37130
|
+
const teams = await teamsForShareQuery(config2, options.team);
|
|
37131
|
+
const state = autoShareState(config2);
|
|
37132
|
+
await loadPendingReindexes(config2, state);
|
|
37133
|
+
const summaries = [];
|
|
37134
|
+
for (const team of teams) {
|
|
37135
|
+
const pending = state.pendingReindexes.get(team.name) ?? [];
|
|
37136
|
+
for (const change of pending) {
|
|
37137
|
+
if (!isShareableMemoryChange(change)) {
|
|
37138
|
+
continue;
|
|
37139
|
+
}
|
|
37140
|
+
summaries.push(await buildShareConflictSummary(config2, team, normalizePendingChange(team, change)));
|
|
37141
|
+
}
|
|
37142
|
+
}
|
|
37143
|
+
return summaries;
|
|
37144
|
+
}
|
|
37145
|
+
async function showShareConflict(config2, reference, options = {}) {
|
|
37146
|
+
const conflict = await readPendingShareConflict(config2, reference, options.team);
|
|
37147
|
+
const inspected = await inspectShareConflict(config2, conflict.team, conflict.change);
|
|
37148
|
+
return {
|
|
37149
|
+
...inspected,
|
|
37150
|
+
diff: formatShareConflictDiff(inspected),
|
|
37151
|
+
resolutionGuidance: shareConflictResolutionGuidance(inspected.id)
|
|
37152
|
+
};
|
|
37153
|
+
}
|
|
37154
|
+
async function resolveShareConflict(config2, reference, options) {
|
|
37155
|
+
const fromFile = options.fromFile?.trim();
|
|
37156
|
+
const mergedContent = options.mergedContent;
|
|
37157
|
+
const rawTake = options.take;
|
|
37158
|
+
if (rawTake !== void 0 && rawTake !== "shared" && rawTake !== "local") {
|
|
37159
|
+
throw new Error(`Unsupported --take value "${rawTake}". Expected "shared" or "local".`);
|
|
37160
|
+
}
|
|
37161
|
+
const take = rawTake;
|
|
37162
|
+
if ((take ? 1 : 0) + (fromFile ? 1 : 0) + (mergedContent !== void 0 ? 1 : 0) !== 1) {
|
|
37163
|
+
throw new Error(
|
|
37164
|
+
"Choose exactly one resolution: --take shared, --take local, --from-file <path>, or mergedContent via MCP."
|
|
37165
|
+
);
|
|
37166
|
+
}
|
|
37167
|
+
const conflict = await readPendingShareConflict(config2, reference, options.team);
|
|
37168
|
+
const inspected = await inspectShareConflict(config2, conflict.team, conflict.change);
|
|
37169
|
+
const dryRun = options.dryRun === true;
|
|
37170
|
+
const ov = await openVikingCliForMode(dryRun);
|
|
37171
|
+
const messages = [];
|
|
37172
|
+
const gitMessages = [];
|
|
37173
|
+
const backupPath = dryRun ? void 0 : await backupShareConflict(config2, inspected);
|
|
37174
|
+
if (take === "shared") {
|
|
37175
|
+
if (inspected.status === "removed") {
|
|
37176
|
+
if (inspected.hasLocalContent) {
|
|
37177
|
+
await removeMemoryUri(config2, ov, inspected.uri, dryRun);
|
|
37178
|
+
messages.push(`Accepted shared deletion for ${inspected.uri}.`);
|
|
37179
|
+
} else {
|
|
37180
|
+
messages.push(`Shared deletion was already reflected in OpenViking for ${inspected.uri}.`);
|
|
37181
|
+
}
|
|
37182
|
+
} else {
|
|
37183
|
+
if (inspected.sharedContent === void 0) {
|
|
37184
|
+
throw new Error(`Cannot take shared for ${inspected.id}: shared file is missing or not readable.`);
|
|
37185
|
+
}
|
|
37186
|
+
await ensureSharedDirectoryChain(config2, ov, inspected.uri, dryRun);
|
|
37187
|
+
await writeMemoryFile(
|
|
37188
|
+
config2,
|
|
37189
|
+
ov,
|
|
37190
|
+
inspected.uri,
|
|
37191
|
+
inspected.sharedContent,
|
|
37192
|
+
inspected.hasLocalContent ? "replace" : "create",
|
|
37193
|
+
dryRun
|
|
37194
|
+
);
|
|
37195
|
+
messages.push(`Accepted shared file content for ${inspected.uri}.`);
|
|
37196
|
+
}
|
|
37197
|
+
} else {
|
|
37198
|
+
const content = await conflictResolutionContent(inspected, take, fromFile, mergedContent);
|
|
37199
|
+
await writeSharedConflictFile(conflict.team, inspected, content, dryRun);
|
|
37200
|
+
await ensureSharedDirectoryChain(config2, ov, inspected.uri, dryRun);
|
|
37201
|
+
await writeMemoryFile(config2, ov, inspected.uri, content, inspected.hasLocalContent ? "replace" : "create", dryRun);
|
|
37202
|
+
const message = options.message ?? `share: resolve ${inspected.relativePath}`;
|
|
37203
|
+
gitMessages.push(
|
|
37204
|
+
...await publishShareGitChange(conflict.team.config.worktree, inspected.relativePath, message, {
|
|
37205
|
+
dryRun,
|
|
37206
|
+
push: options.push
|
|
37207
|
+
})
|
|
37208
|
+
);
|
|
37209
|
+
messages.push(
|
|
37210
|
+
take === "local" ? `Published local OpenViking content for ${inspected.uri}.` : `Applied merged content for ${inspected.uri}.`
|
|
37211
|
+
);
|
|
37212
|
+
}
|
|
37213
|
+
if (!dryRun) {
|
|
37214
|
+
await clearPendingShareConflict(config2, conflict.team.name, inspected.relativePath);
|
|
37215
|
+
}
|
|
37216
|
+
return { backupPath, gitMessages, id: inspected.id, messages, team: inspected.team, uri: inspected.uri };
|
|
37217
|
+
}
|
|
37124
37218
|
async function runShareSyncQuiet(config2, state, options) {
|
|
37125
37219
|
const team = await resolveTeam(config2, options.team);
|
|
37126
37220
|
const git = await requiredExecutable("git");
|
|
37127
37221
|
const worktree = team.config.worktree;
|
|
37128
37222
|
const pendingChanges = state.pendingReindexes.get(team.name);
|
|
37129
37223
|
if (pendingChanges && pendingChanges.length > 0) {
|
|
37130
|
-
await applyAndPersistChanges(config2, team.config, state, pendingChanges, { quiet: true });
|
|
37224
|
+
const result = await applyAndPersistChanges(config2, team.config, state, pendingChanges, { quiet: true });
|
|
37225
|
+
if (result.failed.length > 0) {
|
|
37226
|
+
return `Shared team "${team.name}" has ${result.failed.length} pending shared memory conflict(s). Run \`threadnote share conflicts --team ${team.name}\` to inspect, then \`threadnote share conflict resolve <id> --take shared|local\` or \`--from-file <path>\`.`;
|
|
37227
|
+
}
|
|
37131
37228
|
}
|
|
37132
37229
|
if (await hasUncommittedChanges(worktree)) {
|
|
37133
37230
|
return `Shared team "${team.name}" has uncommitted changes; skipped automatic sync. Run \`threadnote share sync --team ${team.name}\` to publish or resolve them.`;
|
|
@@ -37157,11 +37254,271 @@ async function runShareSyncQuiet(config2, state, options) {
|
|
|
37157
37254
|
if (changes.length > 0) {
|
|
37158
37255
|
const stillPending = state.pendingReindexes.get(team.name) ?? [];
|
|
37159
37256
|
const combined = mergeChanges(stillPending, changes);
|
|
37160
|
-
await applyAndPersistChanges(config2, team.config, state, combined, { quiet: true });
|
|
37257
|
+
const result = await applyAndPersistChanges(config2, team.config, state, combined, { quiet: true });
|
|
37258
|
+
if (result.failed.length > 0) {
|
|
37259
|
+
return `Shared team "${team.name}" has ${result.failed.length} pending shared memory conflict(s). Run \`threadnote share conflicts --team ${team.name}\` to inspect, then \`threadnote share conflict resolve <id> --take shared|local\` or \`--from-file <path>\`.`;
|
|
37260
|
+
}
|
|
37161
37261
|
}
|
|
37162
37262
|
}
|
|
37163
37263
|
return void 0;
|
|
37164
37264
|
}
|
|
37265
|
+
async function teamsForShareQuery(config2, teamName) {
|
|
37266
|
+
if (teamName) {
|
|
37267
|
+
return [await resolveTeam(config2, teamName)];
|
|
37268
|
+
}
|
|
37269
|
+
const teams = await readTeamsFile(config2);
|
|
37270
|
+
const entries = Object.entries(teams.teams);
|
|
37271
|
+
if (entries.length === 0) {
|
|
37272
|
+
throw new Error("No shared teams configured. Run: threadnote share init <remote-url>");
|
|
37273
|
+
}
|
|
37274
|
+
return entries.map(([name, team]) => ({ config: team, name }));
|
|
37275
|
+
}
|
|
37276
|
+
async function readPendingShareConflict(config2, reference, optionTeam) {
|
|
37277
|
+
const target = await parseShareConflictReference(config2, reference, optionTeam);
|
|
37278
|
+
const state = autoShareState(config2);
|
|
37279
|
+
await loadPendingReindexes(config2, state);
|
|
37280
|
+
const pending = state.pendingReindexes.get(target.team.name) ?? [];
|
|
37281
|
+
const change = pending.find((candidate) => candidate.relativePath === target.relativePath);
|
|
37282
|
+
if (!change) {
|
|
37283
|
+
const available = pending.filter(isShareableMemoryChange).map((candidate) => conflictId(target.team.name, candidate.relativePath));
|
|
37284
|
+
throw new Error(
|
|
37285
|
+
[
|
|
37286
|
+
`No pending shared memory conflict found for ${conflictId(target.team.name, target.relativePath)}.`,
|
|
37287
|
+
available.length > 0 ? `Pending conflicts for this team:
|
|
37288
|
+
${available.map((id) => `- ${id}`).join("\n")}` : `No pending conflicts for team "${target.team.name}".`
|
|
37289
|
+
].join("\n")
|
|
37290
|
+
);
|
|
37291
|
+
}
|
|
37292
|
+
return { change: normalizePendingChange(target.team, change), team: target.team };
|
|
37293
|
+
}
|
|
37294
|
+
async function parseShareConflictReference(config2, reference, optionTeam) {
|
|
37295
|
+
const trimmed = reference.trim();
|
|
37296
|
+
if (!trimmed) {
|
|
37297
|
+
throw new Error("Provide a conflict id, relative path, or viking:// shared memory URI.");
|
|
37298
|
+
}
|
|
37299
|
+
if (trimmed.startsWith("viking://")) {
|
|
37300
|
+
const teamName = sharedTeamNameForUri(config2, trimmed);
|
|
37301
|
+
if (!teamName) {
|
|
37302
|
+
throw new Error(`Shared memory URI does not include a configured team: ${trimmed}`);
|
|
37303
|
+
}
|
|
37304
|
+
const team2 = await resolveTeam(config2, optionTeam ?? teamName);
|
|
37305
|
+
return { relativePath: assertSafeShareRelativePath(vikingUriToWorktreeRelative(config2, trimmed, team2.name)), team: team2 };
|
|
37306
|
+
}
|
|
37307
|
+
const colon = trimmed.indexOf(":");
|
|
37308
|
+
if (colon > 0 && !trimmed.slice(0, colon).includes("/")) {
|
|
37309
|
+
const team2 = await resolveTeam(config2, optionTeam ?? trimmed.slice(0, colon));
|
|
37310
|
+
return { relativePath: assertSafeShareRelativePath(trimmed.slice(colon + 1)), team: team2 };
|
|
37311
|
+
}
|
|
37312
|
+
const team = await resolveTeam(config2, optionTeam);
|
|
37313
|
+
return { relativePath: assertSafeShareRelativePath(trimmed), team };
|
|
37314
|
+
}
|
|
37315
|
+
function assertSafeShareRelativePath(relativePath) {
|
|
37316
|
+
if (!relativePath || relativePath.startsWith("/") || relativePath.split("/").some((segment) => segment === ".." || segment.length === 0)) {
|
|
37317
|
+
throw new Error(`Invalid shared relative path: ${relativePath}`);
|
|
37318
|
+
}
|
|
37319
|
+
return relativePath;
|
|
37320
|
+
}
|
|
37321
|
+
function normalizePendingChange(team, change) {
|
|
37322
|
+
return { ...change, path: (0, import_node_path3.join)(team.config.worktree, change.relativePath) };
|
|
37323
|
+
}
|
|
37324
|
+
function isShareableMemoryChange(change) {
|
|
37325
|
+
const firstSegment = change.relativePath.split("/")[0];
|
|
37326
|
+
return change.relativePath.endsWith(".md") && SHAREABLE_MEMORY_KIND_DIRS.includes(firstSegment);
|
|
37327
|
+
}
|
|
37328
|
+
async function buildShareConflictSummary(config2, team, change) {
|
|
37329
|
+
const inspected = await inspectShareConflict(config2, team, change);
|
|
37330
|
+
return {
|
|
37331
|
+
hasLocalContent: inspected.hasLocalContent,
|
|
37332
|
+
hasPreviousContent: inspected.hasPreviousContent,
|
|
37333
|
+
hasSharedContent: inspected.hasSharedContent,
|
|
37334
|
+
id: inspected.id,
|
|
37335
|
+
reason: inspected.reason,
|
|
37336
|
+
relativePath: inspected.relativePath,
|
|
37337
|
+
status: inspected.status,
|
|
37338
|
+
team: inspected.team,
|
|
37339
|
+
uri: inspected.uri
|
|
37340
|
+
};
|
|
37341
|
+
}
|
|
37342
|
+
async function inspectShareConflict(config2, team, change) {
|
|
37343
|
+
const ov = await openVikingCliForMode(false);
|
|
37344
|
+
const uri = workfileToVikingUri(config2, team.config, change.path);
|
|
37345
|
+
const localContent = await readOptionalMemoryContent(config2, ov, uri);
|
|
37346
|
+
const shared = await readOptionalSharedConflictContent(uri, change);
|
|
37347
|
+
const previousContent = change.previousContent === void 0 ? void 0 : prepareSharedInboundContent(uri, change.previousContent);
|
|
37348
|
+
return {
|
|
37349
|
+
hasLocalContent: localContent !== void 0,
|
|
37350
|
+
hasPreviousContent: previousContent !== void 0,
|
|
37351
|
+
hasSharedContent: shared.content !== void 0,
|
|
37352
|
+
id: conflictId(team.name, change.relativePath),
|
|
37353
|
+
localContent,
|
|
37354
|
+
previousContent,
|
|
37355
|
+
reason: shareConflictReason(change, localContent, shared.content, previousContent, shared.error),
|
|
37356
|
+
relativePath: change.relativePath,
|
|
37357
|
+
sharedContent: shared.content,
|
|
37358
|
+
status: change.status,
|
|
37359
|
+
team: team.name,
|
|
37360
|
+
uri
|
|
37361
|
+
};
|
|
37362
|
+
}
|
|
37363
|
+
async function readOptionalSharedConflictContent(uri, change) {
|
|
37364
|
+
try {
|
|
37365
|
+
if (change.status === "removed" || !await isRegularFileNoSymlink(change.path)) {
|
|
37366
|
+
return {};
|
|
37367
|
+
}
|
|
37368
|
+
return { content: await readSharedInboundFileContent(uri, change.path) };
|
|
37369
|
+
} catch (err) {
|
|
37370
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
37371
|
+
}
|
|
37372
|
+
}
|
|
37373
|
+
async function readOptionalMemoryContent(config2, ov, uri) {
|
|
37374
|
+
if (!await vikingResourceExists(ov, config2, uri)) {
|
|
37375
|
+
return void 0;
|
|
37376
|
+
}
|
|
37377
|
+
return readMemoryContent(config2, ov, uri, false);
|
|
37378
|
+
}
|
|
37379
|
+
function shareConflictReason(change, localContent, sharedContent, previousContent, sharedError) {
|
|
37380
|
+
if (sharedError) {
|
|
37381
|
+
return `shared file is not readable: ${sharedError}`;
|
|
37382
|
+
}
|
|
37383
|
+
if (change.status === "added") {
|
|
37384
|
+
if (localContent === void 0) {
|
|
37385
|
+
return "shared file is pending ingestion into OpenViking";
|
|
37386
|
+
}
|
|
37387
|
+
if (sharedContent === void 0) {
|
|
37388
|
+
return "shared file is missing or not readable";
|
|
37389
|
+
}
|
|
37390
|
+
return sharedMemoryContentsEquivalent(localContent, sharedContent) ? "pending replay is already reflected in OpenViking" : "local OpenViking content differs from the newly added shared file";
|
|
37391
|
+
}
|
|
37392
|
+
if (change.status === "modified") {
|
|
37393
|
+
if (localContent === void 0) {
|
|
37394
|
+
return "OpenViking resource is missing while a shared update is pending";
|
|
37395
|
+
}
|
|
37396
|
+
if (previousContent === void 0) {
|
|
37397
|
+
return "previous shared content is unavailable, so local edits cannot be distinguished from upstream edits";
|
|
37398
|
+
}
|
|
37399
|
+
return sharedMemoryContentsEquivalent(localContent, previousContent) ? "shared update is pending ingestion into OpenViking" : "local OpenViking content differs from the previous shared version";
|
|
37400
|
+
}
|
|
37401
|
+
if (localContent === void 0) {
|
|
37402
|
+
return "shared deletion is already reflected in OpenViking";
|
|
37403
|
+
}
|
|
37404
|
+
if (previousContent === void 0) {
|
|
37405
|
+
return "previous shared content is unavailable, so local deletion cannot be verified safely";
|
|
37406
|
+
}
|
|
37407
|
+
return sharedMemoryContentsEquivalent(localContent, previousContent) ? "shared deletion is pending removal from OpenViking" : "local OpenViking content differs from the deleted shared version";
|
|
37408
|
+
}
|
|
37409
|
+
async function conflictResolutionContent(conflict, take, fromFile, mergedContent) {
|
|
37410
|
+
const raw = fromFile !== void 0 ? await (0, import_promises4.readFile)(expandPath(fromFile), "utf8") : mergedContent !== void 0 ? mergedContent : take === "local" ? conflict.localContent : void 0;
|
|
37411
|
+
if (raw === void 0) {
|
|
37412
|
+
throw new Error(`Cannot resolve ${conflict.id}: local OpenViking content is unavailable.`);
|
|
37413
|
+
}
|
|
37414
|
+
const scrub = applyScrubber(stripPersonalProvenance(raw), { redact: false });
|
|
37415
|
+
if (scrub.blocker) {
|
|
37416
|
+
throw new Error(
|
|
37417
|
+
`Refusing to resolve ${conflict.id}: possible ${scrub.blocker}. Strip the sensitive value before writing it to shared memory.`
|
|
37418
|
+
);
|
|
37419
|
+
}
|
|
37420
|
+
return scrub.cleaned;
|
|
37421
|
+
}
|
|
37422
|
+
async function writeSharedConflictFile(team, conflict, content, dryRun) {
|
|
37423
|
+
const filePath = (0, import_node_path3.join)(team.config.worktree, conflict.relativePath);
|
|
37424
|
+
if (dryRun) {
|
|
37425
|
+
console.log(`Would write shared file: ${portablePath(filePath)}`);
|
|
37426
|
+
return;
|
|
37427
|
+
}
|
|
37428
|
+
await (0, import_promises4.mkdir)((0, import_node_path3.dirname)(filePath), { recursive: true });
|
|
37429
|
+
await (0, import_promises4.writeFile)(filePath, content, "utf8");
|
|
37430
|
+
}
|
|
37431
|
+
async function backupShareConflict(config2, conflict) {
|
|
37432
|
+
const backupDir = (0, import_node_path3.join)(
|
|
37433
|
+
config2.agentContextHome,
|
|
37434
|
+
"share",
|
|
37435
|
+
"conflict-backups",
|
|
37436
|
+
safeTimestamp(),
|
|
37437
|
+
conflict.team,
|
|
37438
|
+
...conflict.relativePath.split("/")
|
|
37439
|
+
);
|
|
37440
|
+
await (0, import_promises4.mkdir)(backupDir, { recursive: true });
|
|
37441
|
+
const metadata = {
|
|
37442
|
+
id: conflict.id,
|
|
37443
|
+
reason: conflict.reason,
|
|
37444
|
+
relativePath: conflict.relativePath,
|
|
37445
|
+
status: conflict.status,
|
|
37446
|
+
team: conflict.team,
|
|
37447
|
+
uri: conflict.uri
|
|
37448
|
+
};
|
|
37449
|
+
await (0, import_promises4.writeFile)((0, import_node_path3.join)(backupDir, "metadata.json"), `${JSON.stringify(metadata, void 0, 2)}
|
|
37450
|
+
`, "utf8");
|
|
37451
|
+
if (conflict.localContent !== void 0) {
|
|
37452
|
+
await (0, import_promises4.writeFile)((0, import_node_path3.join)(backupDir, "local.md"), conflict.localContent, "utf8");
|
|
37453
|
+
}
|
|
37454
|
+
if (conflict.sharedContent !== void 0) {
|
|
37455
|
+
await (0, import_promises4.writeFile)((0, import_node_path3.join)(backupDir, "shared.md"), conflict.sharedContent, "utf8");
|
|
37456
|
+
}
|
|
37457
|
+
if (conflict.previousContent !== void 0) {
|
|
37458
|
+
await (0, import_promises4.writeFile)((0, import_node_path3.join)(backupDir, "previous.md"), conflict.previousContent, "utf8");
|
|
37459
|
+
}
|
|
37460
|
+
return backupDir;
|
|
37461
|
+
}
|
|
37462
|
+
async function clearPendingShareConflict(config2, teamName, relativePath) {
|
|
37463
|
+
const state = autoShareState(config2);
|
|
37464
|
+
await loadPendingReindexes(config2, state);
|
|
37465
|
+
const pending = state.pendingReindexes.get(teamName) ?? [];
|
|
37466
|
+
const remaining = pending.filter((change) => change.relativePath !== relativePath);
|
|
37467
|
+
if (remaining.length > 0) {
|
|
37468
|
+
state.pendingReindexes.set(teamName, remaining);
|
|
37469
|
+
} else {
|
|
37470
|
+
state.pendingReindexes.delete(teamName);
|
|
37471
|
+
}
|
|
37472
|
+
await writePendingReindexes(config2, state);
|
|
37473
|
+
}
|
|
37474
|
+
function conflictId(team, relativePath) {
|
|
37475
|
+
return `${team}:${relativePath}`;
|
|
37476
|
+
}
|
|
37477
|
+
function shareConflictResolutionGuidance(id) {
|
|
37478
|
+
return [
|
|
37479
|
+
`threadnote share conflict resolve ${id} --take shared`,
|
|
37480
|
+
`threadnote share conflict resolve ${id} --take local`,
|
|
37481
|
+
`threadnote share conflict resolve ${id} --from-file merged.md`
|
|
37482
|
+
];
|
|
37483
|
+
}
|
|
37484
|
+
function formatShareConflictDiff(conflict) {
|
|
37485
|
+
const parts = [];
|
|
37486
|
+
if (conflict.previousContent !== void 0) {
|
|
37487
|
+
parts.push(
|
|
37488
|
+
formatTwoWayDiff("previous shared", conflict.previousContent, "local OpenViking", conflict.localContent)
|
|
37489
|
+
);
|
|
37490
|
+
}
|
|
37491
|
+
parts.push(formatTwoWayDiff("local OpenViking", conflict.localContent, "shared file", conflict.sharedContent));
|
|
37492
|
+
return parts.join("\n\n");
|
|
37493
|
+
}
|
|
37494
|
+
function formatTwoWayDiff(leftLabel, leftContent, rightLabel, rightContent) {
|
|
37495
|
+
if (leftContent === void 0 && rightContent === void 0) {
|
|
37496
|
+
return `${leftLabel} and ${rightLabel} are both unavailable.`;
|
|
37497
|
+
}
|
|
37498
|
+
if (leftContent === rightContent) {
|
|
37499
|
+
return `${leftLabel} and ${rightLabel} are identical.`;
|
|
37500
|
+
}
|
|
37501
|
+
const leftLines = splitDiffLines(leftContent);
|
|
37502
|
+
const rightLines = splitDiffLines(rightContent);
|
|
37503
|
+
const lines = [`--- ${leftLabel}`, `+++ ${rightLabel}`];
|
|
37504
|
+
for (const line of leftLines) {
|
|
37505
|
+
lines.push(`-${line}`);
|
|
37506
|
+
}
|
|
37507
|
+
for (const line of rightLines) {
|
|
37508
|
+
lines.push(`+${line}`);
|
|
37509
|
+
}
|
|
37510
|
+
return lines.join("\n");
|
|
37511
|
+
}
|
|
37512
|
+
function splitDiffLines(content) {
|
|
37513
|
+
if (content === void 0) {
|
|
37514
|
+
return ["<missing>"];
|
|
37515
|
+
}
|
|
37516
|
+
const lines = content.split(/\n/);
|
|
37517
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
37518
|
+
return lines.slice(0, -1);
|
|
37519
|
+
}
|
|
37520
|
+
return lines;
|
|
37521
|
+
}
|
|
37165
37522
|
async function publishShareGitChange(worktree, relativePath, commitMessage, options = {}) {
|
|
37166
37523
|
const dryRun = options.dryRun === true;
|
|
37167
37524
|
const push = options.push !== false;
|
|
@@ -39092,7 +39449,7 @@ async function applyChangesToOpenViking(config2, team, changes, options = {}) {
|
|
|
39092
39449
|
const currentContent = await readExistingMemoryContent(config2, ov, uri);
|
|
39093
39450
|
if (currentContent !== void 0) {
|
|
39094
39451
|
if (change.status === "added") {
|
|
39095
|
-
if (currentContent
|
|
39452
|
+
if (sharedMemoryContentsEquivalent(currentContent, content)) {
|
|
39096
39453
|
continue;
|
|
39097
39454
|
}
|
|
39098
39455
|
throw new Error(
|
|
@@ -39131,12 +39488,18 @@ function assertInboundPreviousContentMatches(change, uri, currentContent) {
|
|
|
39131
39488
|
);
|
|
39132
39489
|
}
|
|
39133
39490
|
const expectedContent = prepareSharedInboundContent(uri, change.previousContent);
|
|
39134
|
-
if (currentContent
|
|
39491
|
+
if (!sharedMemoryContentsEquivalent(currentContent, expectedContent)) {
|
|
39135
39492
|
throw new Error(
|
|
39136
39493
|
`Refusing to apply inbound shared change for ${uri}: local OpenViking content differs from the previous shared version. Inspect and resolve the local edit first.`
|
|
39137
39494
|
);
|
|
39138
39495
|
}
|
|
39139
39496
|
}
|
|
39497
|
+
function sharedMemoryContentsEquivalent(left, right) {
|
|
39498
|
+
return normalizeSharedMemoryComparisonContent(left) === normalizeSharedMemoryComparisonContent(right);
|
|
39499
|
+
}
|
|
39500
|
+
function normalizeSharedMemoryComparisonContent(content) {
|
|
39501
|
+
return content.replace(/\r\n?/g, "\n").replace(/\n$/, "");
|
|
39502
|
+
}
|
|
39140
39503
|
function mergeChanges(...lists) {
|
|
39141
39504
|
const map2 = /* @__PURE__ */ new Map();
|
|
39142
39505
|
for (const list of lists) {
|
|
@@ -39732,6 +40095,7 @@ async function main() {
|
|
|
39732
40095
|
"When updating the same active issue, pass project/topic or replaceUri to remember_context so duplicate durable memories or handoffs do not accumulate.",
|
|
39733
40096
|
"Use compact_context with dryRun=true for scoped memory hygiene when recall surfaces overlapping active memories.",
|
|
39734
40097
|
"To share a durable memory with teammates, call `share_publish` with its viking:// URI. share_publish scrubs for secrets, writes and pushes the shared copy first, then removes the personal copy after the push succeeds. Do not publish handoffs, preferences, or anything carrying machine-local paths or in-flight task context.",
|
|
40098
|
+
'When recall/read reports pending shared memory conflicts, call `share_conflicts`, inspect one with `share_conflict_show`, then resolve it only after user direction with `share_conflict_resolve` using take="shared", take="local", or mergedContent.',
|
|
39735
40099
|
"To share a local Codex/Claude skill or Claude command with teammates, call `share_skill` with the local file path. It publishes into the shared artifact catalog after the same scrubber checks.",
|
|
39736
40100
|
"To use a team shared skill as a native local skill, call `list_shared_skills` first, then `install_shared_skill` for the selected name/agent/kind.",
|
|
39737
40101
|
"Do not store secrets, customer data, raw production logs, or credentials."
|
|
@@ -39951,6 +40315,71 @@ function registerTools(server, config2) {
|
|
|
39951
40315
|
return runSharePublishTool(config2, checkedUri.value, { message, preview, push, redact, team });
|
|
39952
40316
|
}
|
|
39953
40317
|
);
|
|
40318
|
+
server.registerTool(
|
|
40319
|
+
"share_conflicts",
|
|
40320
|
+
{
|
|
40321
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
40322
|
+
description: "List pending shared memory conflicts left by share sync reindex failures. Use this when recall/read/sync reports pending shared memory conflicts. Each result includes a stable id plus exact next-step guidance for show/resolve.",
|
|
40323
|
+
inputSchema: {
|
|
40324
|
+
team: external_exports.string().optional().describe("Team name; omit to inspect all configured teams")
|
|
40325
|
+
}
|
|
40326
|
+
},
|
|
40327
|
+
async ({ team }) => runShareConflictsTool(config2, { team })
|
|
40328
|
+
);
|
|
40329
|
+
server.registerTool(
|
|
40330
|
+
"share_conflict_show",
|
|
40331
|
+
{
|
|
40332
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
40333
|
+
description: "Show one pending shared memory conflict, including local OpenViking content vs shared file diff and safe resolution options. The id comes from share_conflicts and has the form team:durable/projects/.../topic.md; a shared viking:// URI also works.",
|
|
40334
|
+
inputSchema: {
|
|
40335
|
+
id: external_exports.string().optional().describe("Required conflict id from share_conflicts, relative path plus team, or shared viking:// URI"),
|
|
40336
|
+
team: external_exports.string().optional().describe("Team name when id is only a relative path")
|
|
40337
|
+
}
|
|
40338
|
+
},
|
|
40339
|
+
async ({ id, team }) => {
|
|
40340
|
+
const checkedId = requiredText(id, "share_conflict_show", "id", {
|
|
40341
|
+
id: "default:durable/projects/foo/bar.md"
|
|
40342
|
+
});
|
|
40343
|
+
if (!checkedId.ok) {
|
|
40344
|
+
return checkedId.error;
|
|
40345
|
+
}
|
|
40346
|
+
return runShareConflictShowTool(config2, checkedId.value, { team });
|
|
40347
|
+
}
|
|
40348
|
+
);
|
|
40349
|
+
server.registerTool(
|
|
40350
|
+
"share_conflict_resolve",
|
|
40351
|
+
{
|
|
40352
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
40353
|
+
description: 'Resolve one pending shared memory conflict on the user\u2019s behalf after they choose a winner. Use take="shared" to accept the shared git file into OpenViking, take="local" to publish local OpenViking content back to the shared repo, or mergedContent to write explicit merged markdown to both places. Creates a local backup before mutation and clears only the resolved pending entry.',
|
|
40354
|
+
inputSchema: {
|
|
40355
|
+
dryRun: external_exports.boolean().optional().describe("Preview without writing OpenViking, shared files, git commits, or pending state"),
|
|
40356
|
+
id: external_exports.string().optional().describe("Required conflict id from share_conflicts, relative path plus team, or shared viking:// URI"),
|
|
40357
|
+
mergedContent: external_exports.string().optional().describe(
|
|
40358
|
+
"Explicit merged memory markdown. Mutually exclusive with take. MCP equivalent of CLI --from-file."
|
|
40359
|
+
),
|
|
40360
|
+
message: external_exports.string().optional().describe("Commit message when writing local or merged content to the shared repo"),
|
|
40361
|
+
push: external_exports.boolean().optional().describe("Push local/merged resolution commit to remote; defaults to true"),
|
|
40362
|
+
take: external_exports.enum(["shared", "local"]).optional().describe("Resolution side. Mutually exclusive with mergedContent."),
|
|
40363
|
+
team: external_exports.string().optional().describe("Team name when id is only a relative path")
|
|
40364
|
+
}
|
|
40365
|
+
},
|
|
40366
|
+
async ({ dryRun, id, mergedContent, message, push, take, team }) => {
|
|
40367
|
+
const checkedId = requiredText(id, "share_conflict_resolve", "id", {
|
|
40368
|
+
id: "default:durable/projects/foo/bar.md"
|
|
40369
|
+
});
|
|
40370
|
+
if (!checkedId.ok) {
|
|
40371
|
+
return checkedId.error;
|
|
40372
|
+
}
|
|
40373
|
+
return runShareConflictResolveTool(config2, checkedId.value, {
|
|
40374
|
+
dryRun,
|
|
40375
|
+
mergedContent,
|
|
40376
|
+
message,
|
|
40377
|
+
push,
|
|
40378
|
+
take,
|
|
40379
|
+
team
|
|
40380
|
+
});
|
|
40381
|
+
}
|
|
40382
|
+
);
|
|
39954
40383
|
server.registerTool(
|
|
39955
40384
|
"share_skill",
|
|
39956
40385
|
{
|
|
@@ -41478,6 +41907,85 @@ async function forgetVikingResourceWithRetry(config2, uri) {
|
|
|
41478
41907
|
const ov = await requiredOpenVikingCli2();
|
|
41479
41908
|
return removeVikingResourceWithRetry(ov, config2, uri);
|
|
41480
41909
|
}
|
|
41910
|
+
async function runShareConflictsTool(config2, options) {
|
|
41911
|
+
try {
|
|
41912
|
+
const conflicts = await listShareConflicts(config2, options);
|
|
41913
|
+
if (conflicts.length === 0) {
|
|
41914
|
+
return {
|
|
41915
|
+
content: [
|
|
41916
|
+
{
|
|
41917
|
+
type: "text",
|
|
41918
|
+
text: options.team ? `No pending shared memory conflicts for team "${options.team}".` : "No pending shared memory conflicts."
|
|
41919
|
+
}
|
|
41920
|
+
]
|
|
41921
|
+
};
|
|
41922
|
+
}
|
|
41923
|
+
const lines = [`Pending shared memory conflicts: ${conflicts.length}`];
|
|
41924
|
+
for (const conflict of conflicts) {
|
|
41925
|
+
lines.push(
|
|
41926
|
+
"",
|
|
41927
|
+
conflict.id,
|
|
41928
|
+
`uri: ${conflict.uri}`,
|
|
41929
|
+
`status: ${conflict.status}`,
|
|
41930
|
+
`reason: ${conflict.reason}`,
|
|
41931
|
+
`show: share_conflict_show({"id":${JSON.stringify(conflict.id)}})`,
|
|
41932
|
+
`take shared: share_conflict_resolve({"id":${JSON.stringify(conflict.id)},"take":"shared"})`,
|
|
41933
|
+
`take local: share_conflict_resolve({"id":${JSON.stringify(conflict.id)},"take":"local"})`,
|
|
41934
|
+
`merged: share_conflict_resolve({"id":${JSON.stringify(conflict.id)},"mergedContent":"<merged MEMORY markdown>"})`
|
|
41935
|
+
);
|
|
41936
|
+
}
|
|
41937
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
41938
|
+
} catch (err) {
|
|
41939
|
+
return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
|
|
41940
|
+
}
|
|
41941
|
+
}
|
|
41942
|
+
async function runShareConflictShowTool(config2, id, options) {
|
|
41943
|
+
try {
|
|
41944
|
+
const detail = await showShareConflict(config2, id, options);
|
|
41945
|
+
return {
|
|
41946
|
+
content: [
|
|
41947
|
+
{
|
|
41948
|
+
type: "text",
|
|
41949
|
+
text: [
|
|
41950
|
+
`Conflict: ${detail.id}`,
|
|
41951
|
+
`URI: ${detail.uri}`,
|
|
41952
|
+
`Status: ${detail.status}`,
|
|
41953
|
+
`Reason: ${detail.reason}`,
|
|
41954
|
+
"",
|
|
41955
|
+
detail.diff,
|
|
41956
|
+
"",
|
|
41957
|
+
"Resolve:",
|
|
41958
|
+
`share_conflict_resolve({"id":${JSON.stringify(detail.id)},"take":"shared"})`,
|
|
41959
|
+
`share_conflict_resolve({"id":${JSON.stringify(detail.id)},"take":"local"})`,
|
|
41960
|
+
`share_conflict_resolve({"id":${JSON.stringify(detail.id)},"mergedContent":"<merged MEMORY markdown>"})`
|
|
41961
|
+
].join("\n")
|
|
41962
|
+
}
|
|
41963
|
+
]
|
|
41964
|
+
};
|
|
41965
|
+
} catch (err) {
|
|
41966
|
+
return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
|
|
41967
|
+
}
|
|
41968
|
+
}
|
|
41969
|
+
async function runShareConflictResolveTool(config2, id, options) {
|
|
41970
|
+
try {
|
|
41971
|
+
const result = await resolveShareConflict(config2, id, {
|
|
41972
|
+
dryRun: options.dryRun,
|
|
41973
|
+
mergedContent: options.mergedContent,
|
|
41974
|
+
message: options.message,
|
|
41975
|
+
push: options.push,
|
|
41976
|
+
take: options.take,
|
|
41977
|
+
team: options.team
|
|
41978
|
+
});
|
|
41979
|
+
const lines = [...result.messages];
|
|
41980
|
+
if (result.backupPath) {
|
|
41981
|
+
lines.push(`Backup: ${result.backupPath}`);
|
|
41982
|
+
}
|
|
41983
|
+
lines.push(...result.gitMessages, `Resolved shared memory conflict: ${result.id}`);
|
|
41984
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
41985
|
+
} catch (err) {
|
|
41986
|
+
return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
|
|
41987
|
+
}
|
|
41988
|
+
}
|
|
41481
41989
|
async function runSharePublishTool(config2, sourceUri, options) {
|
|
41482
41990
|
try {
|
|
41483
41991
|
if (isInSharedNamespace(config2, sourceUri)) {
|
package/dist/threadnote.cjs
CHANGED
|
@@ -8808,7 +8808,12 @@ async function loadPendingReindexes(config, state) {
|
|
|
8808
8808
|
}
|
|
8809
8809
|
const entry = item;
|
|
8810
8810
|
if (typeof entry.path === "string" && typeof entry.relativePath === "string" && (entry.status === "added" || entry.status === "removed" || entry.status === "modified")) {
|
|
8811
|
-
changes.push({
|
|
8811
|
+
changes.push({
|
|
8812
|
+
path: entry.path,
|
|
8813
|
+
previousContent: typeof entry.previousContent === "string" ? entry.previousContent : void 0,
|
|
8814
|
+
relativePath: entry.relativePath,
|
|
8815
|
+
status: entry.status
|
|
8816
|
+
});
|
|
8812
8817
|
}
|
|
8813
8818
|
}
|
|
8814
8819
|
if (changes.length > 0) {
|
|
@@ -8897,7 +8902,35 @@ async function fetchShareUpdateStatuses(config) {
|
|
|
8897
8902
|
return statuses;
|
|
8898
8903
|
}
|
|
8899
8904
|
async function runShareSync(config, options) {
|
|
8900
|
-
const
|
|
8905
|
+
const teams = await teamsForShareQuery(config, options.team);
|
|
8906
|
+
if (options.team) {
|
|
8907
|
+
const team = teams[0];
|
|
8908
|
+
if (!team) {
|
|
8909
|
+
throw new Error("No shared teams configured. Run: threadnote share init <remote-url>");
|
|
8910
|
+
}
|
|
8911
|
+
await runShareSyncForTeam(config, team, options);
|
|
8912
|
+
return;
|
|
8913
|
+
}
|
|
8914
|
+
const failures = [];
|
|
8915
|
+
for (const [index, team] of teams.entries()) {
|
|
8916
|
+
if (teams.length > 1) {
|
|
8917
|
+
console.log(`Syncing shared team "${team.name}" (${index + 1}/${teams.length})...`);
|
|
8918
|
+
}
|
|
8919
|
+
try {
|
|
8920
|
+
await runShareSyncForTeam(config, team, options);
|
|
8921
|
+
} catch (error) {
|
|
8922
|
+
failures.push(`${team.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
8923
|
+
}
|
|
8924
|
+
}
|
|
8925
|
+
if (failures.length > 0) {
|
|
8926
|
+
throw new Error(
|
|
8927
|
+
[`share sync failed for ${failures.length} shared team(s):`, ...failures.map((failure2) => `- ${failure2}`)].join(
|
|
8928
|
+
"\n"
|
|
8929
|
+
)
|
|
8930
|
+
);
|
|
8931
|
+
}
|
|
8932
|
+
}
|
|
8933
|
+
async function runShareSyncForTeam(config, team, options) {
|
|
8901
8934
|
const dryRun = options.dryRun === true;
|
|
8902
8935
|
const git = await requiredExecutable("git");
|
|
8903
8936
|
const worktree = team.config.worktree;
|
|
@@ -8963,6 +8996,7 @@ Resolve the conflicts in-place, run \`git -C ${worktree} rebase --continue\` (or
|
|
|
8963
8996
|
console.warn(
|
|
8964
8997
|
`share sync: ${result.failed.length} file(s) could not be ingested on this run; they are persisted and will be retried on the next sync or agent recall/read.`
|
|
8965
8998
|
);
|
|
8999
|
+
console.warn(formatShareConflictNextSteps(team.name, result.failed));
|
|
8966
9000
|
}
|
|
8967
9001
|
}
|
|
8968
9002
|
}
|
|
@@ -8977,13 +9011,152 @@ Resolve the conflicts in-place, run \`git -C ${worktree} rebase --continue\` (or
|
|
|
8977
9011
|
}
|
|
8978
9012
|
}
|
|
8979
9013
|
}
|
|
9014
|
+
async function runShareConflicts(config, options) {
|
|
9015
|
+
const conflicts = await listShareConflicts(config, options);
|
|
9016
|
+
if (conflicts.length === 0) {
|
|
9017
|
+
const team = options.team ? ` for team "${options.team}"` : "";
|
|
9018
|
+
console.log(`No pending shared memory conflicts${team}.`);
|
|
9019
|
+
return;
|
|
9020
|
+
}
|
|
9021
|
+
console.log(`Pending shared memory conflicts: ${conflicts.length}`);
|
|
9022
|
+
for (const conflict of conflicts) {
|
|
9023
|
+
console.log("");
|
|
9024
|
+
console.log(`${conflict.id}`);
|
|
9025
|
+
console.log(` uri: ${conflict.uri}`);
|
|
9026
|
+
console.log(` status: ${conflict.status}`);
|
|
9027
|
+
console.log(` reason: ${conflict.reason}`);
|
|
9028
|
+
console.log(` show: threadnote share conflict show ${conflict.id}`);
|
|
9029
|
+
console.log(` take shared: threadnote share conflict resolve ${conflict.id} --take shared`);
|
|
9030
|
+
console.log(` take local: threadnote share conflict resolve ${conflict.id} --take local`);
|
|
9031
|
+
console.log(` merged file: threadnote share conflict resolve ${conflict.id} --from-file merged.md`);
|
|
9032
|
+
}
|
|
9033
|
+
}
|
|
9034
|
+
async function runShareConflictShow(config, reference, options) {
|
|
9035
|
+
const detail = await showShareConflict(config, reference, options);
|
|
9036
|
+
console.log(`Conflict: ${detail.id}`);
|
|
9037
|
+
console.log(`URI: ${detail.uri}`);
|
|
9038
|
+
console.log(`Status: ${detail.status}`);
|
|
9039
|
+
console.log(`Reason: ${detail.reason}`);
|
|
9040
|
+
console.log("");
|
|
9041
|
+
console.log(detail.diff);
|
|
9042
|
+
console.log("");
|
|
9043
|
+
console.log("Resolve:");
|
|
9044
|
+
for (const line of detail.resolutionGuidance) {
|
|
9045
|
+
console.log(` ${line}`);
|
|
9046
|
+
}
|
|
9047
|
+
}
|
|
9048
|
+
async function runShareConflictResolve(config, reference, options) {
|
|
9049
|
+
const result = await resolveShareConflict(config, reference, options);
|
|
9050
|
+
for (const message of result.messages) {
|
|
9051
|
+
console.log(message);
|
|
9052
|
+
}
|
|
9053
|
+
if (result.backupPath) {
|
|
9054
|
+
console.log(`Backup: ${portablePath(result.backupPath)}`);
|
|
9055
|
+
}
|
|
9056
|
+
for (const message of result.gitMessages) {
|
|
9057
|
+
console.log(message);
|
|
9058
|
+
}
|
|
9059
|
+
console.log(`Resolved shared memory conflict: ${result.id}`);
|
|
9060
|
+
}
|
|
9061
|
+
async function listShareConflicts(config, options = {}) {
|
|
9062
|
+
const teams = await teamsForShareQuery(config, options.team);
|
|
9063
|
+
const state = autoShareState(config);
|
|
9064
|
+
await loadPendingReindexes(config, state);
|
|
9065
|
+
const summaries = [];
|
|
9066
|
+
for (const team of teams) {
|
|
9067
|
+
const pending = state.pendingReindexes.get(team.name) ?? [];
|
|
9068
|
+
for (const change of pending) {
|
|
9069
|
+
if (!isShareableMemoryChange(change)) {
|
|
9070
|
+
continue;
|
|
9071
|
+
}
|
|
9072
|
+
summaries.push(await buildShareConflictSummary(config, team, normalizePendingChange(team, change)));
|
|
9073
|
+
}
|
|
9074
|
+
}
|
|
9075
|
+
return summaries;
|
|
9076
|
+
}
|
|
9077
|
+
async function showShareConflict(config, reference, options = {}) {
|
|
9078
|
+
const conflict = await readPendingShareConflict(config, reference, options.team);
|
|
9079
|
+
const inspected = await inspectShareConflict(config, conflict.team, conflict.change);
|
|
9080
|
+
return {
|
|
9081
|
+
...inspected,
|
|
9082
|
+
diff: formatShareConflictDiff(inspected),
|
|
9083
|
+
resolutionGuidance: shareConflictResolutionGuidance(inspected.id)
|
|
9084
|
+
};
|
|
9085
|
+
}
|
|
9086
|
+
async function resolveShareConflict(config, reference, options) {
|
|
9087
|
+
const fromFile = options.fromFile?.trim();
|
|
9088
|
+
const mergedContent = options.mergedContent;
|
|
9089
|
+
const rawTake = options.take;
|
|
9090
|
+
if (rawTake !== void 0 && rawTake !== "shared" && rawTake !== "local") {
|
|
9091
|
+
throw new Error(`Unsupported --take value "${rawTake}". Expected "shared" or "local".`);
|
|
9092
|
+
}
|
|
9093
|
+
const take = rawTake;
|
|
9094
|
+
if ((take ? 1 : 0) + (fromFile ? 1 : 0) + (mergedContent !== void 0 ? 1 : 0) !== 1) {
|
|
9095
|
+
throw new Error(
|
|
9096
|
+
"Choose exactly one resolution: --take shared, --take local, --from-file <path>, or mergedContent via MCP."
|
|
9097
|
+
);
|
|
9098
|
+
}
|
|
9099
|
+
const conflict = await readPendingShareConflict(config, reference, options.team);
|
|
9100
|
+
const inspected = await inspectShareConflict(config, conflict.team, conflict.change);
|
|
9101
|
+
const dryRun = options.dryRun === true;
|
|
9102
|
+
const ov = await openVikingCliForMode(dryRun);
|
|
9103
|
+
const messages = [];
|
|
9104
|
+
const gitMessages = [];
|
|
9105
|
+
const backupPath = dryRun ? void 0 : await backupShareConflict(config, inspected);
|
|
9106
|
+
if (take === "shared") {
|
|
9107
|
+
if (inspected.status === "removed") {
|
|
9108
|
+
if (inspected.hasLocalContent) {
|
|
9109
|
+
await removeMemoryUri(config, ov, inspected.uri, dryRun);
|
|
9110
|
+
messages.push(`Accepted shared deletion for ${inspected.uri}.`);
|
|
9111
|
+
} else {
|
|
9112
|
+
messages.push(`Shared deletion was already reflected in OpenViking for ${inspected.uri}.`);
|
|
9113
|
+
}
|
|
9114
|
+
} else {
|
|
9115
|
+
if (inspected.sharedContent === void 0) {
|
|
9116
|
+
throw new Error(`Cannot take shared for ${inspected.id}: shared file is missing or not readable.`);
|
|
9117
|
+
}
|
|
9118
|
+
await ensureSharedDirectoryChain(config, ov, inspected.uri, dryRun);
|
|
9119
|
+
await writeMemoryFile(
|
|
9120
|
+
config,
|
|
9121
|
+
ov,
|
|
9122
|
+
inspected.uri,
|
|
9123
|
+
inspected.sharedContent,
|
|
9124
|
+
inspected.hasLocalContent ? "replace" : "create",
|
|
9125
|
+
dryRun
|
|
9126
|
+
);
|
|
9127
|
+
messages.push(`Accepted shared file content for ${inspected.uri}.`);
|
|
9128
|
+
}
|
|
9129
|
+
} else {
|
|
9130
|
+
const content = await conflictResolutionContent(inspected, take, fromFile, mergedContent);
|
|
9131
|
+
await writeSharedConflictFile(conflict.team, inspected, content, dryRun);
|
|
9132
|
+
await ensureSharedDirectoryChain(config, ov, inspected.uri, dryRun);
|
|
9133
|
+
await writeMemoryFile(config, ov, inspected.uri, content, inspected.hasLocalContent ? "replace" : "create", dryRun);
|
|
9134
|
+
const message = options.message ?? `share: resolve ${inspected.relativePath}`;
|
|
9135
|
+
gitMessages.push(
|
|
9136
|
+
...await publishShareGitChange(conflict.team.config.worktree, inspected.relativePath, message, {
|
|
9137
|
+
dryRun,
|
|
9138
|
+
push: options.push
|
|
9139
|
+
})
|
|
9140
|
+
);
|
|
9141
|
+
messages.push(
|
|
9142
|
+
take === "local" ? `Published local OpenViking content for ${inspected.uri}.` : `Applied merged content for ${inspected.uri}.`
|
|
9143
|
+
);
|
|
9144
|
+
}
|
|
9145
|
+
if (!dryRun) {
|
|
9146
|
+
await clearPendingShareConflict(config, conflict.team.name, inspected.relativePath);
|
|
9147
|
+
}
|
|
9148
|
+
return { backupPath, gitMessages, id: inspected.id, messages, team: inspected.team, uri: inspected.uri };
|
|
9149
|
+
}
|
|
8980
9150
|
async function runShareSyncQuiet(config, state, options) {
|
|
8981
9151
|
const team = await resolveTeam(config, options.team);
|
|
8982
9152
|
const git = await requiredExecutable("git");
|
|
8983
9153
|
const worktree = team.config.worktree;
|
|
8984
9154
|
const pendingChanges = state.pendingReindexes.get(team.name);
|
|
8985
9155
|
if (pendingChanges && pendingChanges.length > 0) {
|
|
8986
|
-
await applyAndPersistChanges(config, team.config, state, pendingChanges, { quiet: true });
|
|
9156
|
+
const result = await applyAndPersistChanges(config, team.config, state, pendingChanges, { quiet: true });
|
|
9157
|
+
if (result.failed.length > 0) {
|
|
9158
|
+
return `Shared team "${team.name}" has ${result.failed.length} pending shared memory conflict(s). Run \`threadnote share conflicts --team ${team.name}\` to inspect, then \`threadnote share conflict resolve <id> --take shared|local\` or \`--from-file <path>\`.`;
|
|
9159
|
+
}
|
|
8987
9160
|
}
|
|
8988
9161
|
if (await hasUncommittedChanges(worktree)) {
|
|
8989
9162
|
return `Shared team "${team.name}" has uncommitted changes; skipped automatic sync. Run \`threadnote share sync --team ${team.name}\` to publish or resolve them.`;
|
|
@@ -9013,11 +9186,287 @@ async function runShareSyncQuiet(config, state, options) {
|
|
|
9013
9186
|
if (changes.length > 0) {
|
|
9014
9187
|
const stillPending = state.pendingReindexes.get(team.name) ?? [];
|
|
9015
9188
|
const combined = mergeChanges(stillPending, changes);
|
|
9016
|
-
await applyAndPersistChanges(config, team.config, state, combined, { quiet: true });
|
|
9189
|
+
const result = await applyAndPersistChanges(config, team.config, state, combined, { quiet: true });
|
|
9190
|
+
if (result.failed.length > 0) {
|
|
9191
|
+
return `Shared team "${team.name}" has ${result.failed.length} pending shared memory conflict(s). Run \`threadnote share conflicts --team ${team.name}\` to inspect, then \`threadnote share conflict resolve <id> --take shared|local\` or \`--from-file <path>\`.`;
|
|
9192
|
+
}
|
|
9017
9193
|
}
|
|
9018
9194
|
}
|
|
9019
9195
|
return void 0;
|
|
9020
9196
|
}
|
|
9197
|
+
async function teamsForShareQuery(config, teamName) {
|
|
9198
|
+
if (teamName) {
|
|
9199
|
+
return [await resolveTeam(config, teamName)];
|
|
9200
|
+
}
|
|
9201
|
+
const teams = await readTeamsFile(config);
|
|
9202
|
+
const entries = Object.entries(teams.teams);
|
|
9203
|
+
if (entries.length === 0) {
|
|
9204
|
+
throw new Error("No shared teams configured. Run: threadnote share init <remote-url>");
|
|
9205
|
+
}
|
|
9206
|
+
return entries.map(([name, team]) => ({ config: team, name }));
|
|
9207
|
+
}
|
|
9208
|
+
async function readPendingShareConflict(config, reference, optionTeam) {
|
|
9209
|
+
const target = await parseShareConflictReference(config, reference, optionTeam);
|
|
9210
|
+
const state = autoShareState(config);
|
|
9211
|
+
await loadPendingReindexes(config, state);
|
|
9212
|
+
const pending = state.pendingReindexes.get(target.team.name) ?? [];
|
|
9213
|
+
const change = pending.find((candidate) => candidate.relativePath === target.relativePath);
|
|
9214
|
+
if (!change) {
|
|
9215
|
+
const available = pending.filter(isShareableMemoryChange).map((candidate) => conflictId(target.team.name, candidate.relativePath));
|
|
9216
|
+
throw new Error(
|
|
9217
|
+
[
|
|
9218
|
+
`No pending shared memory conflict found for ${conflictId(target.team.name, target.relativePath)}.`,
|
|
9219
|
+
available.length > 0 ? `Pending conflicts for this team:
|
|
9220
|
+
${available.map((id) => `- ${id}`).join("\n")}` : `No pending conflicts for team "${target.team.name}".`
|
|
9221
|
+
].join("\n")
|
|
9222
|
+
);
|
|
9223
|
+
}
|
|
9224
|
+
return { change: normalizePendingChange(target.team, change), team: target.team };
|
|
9225
|
+
}
|
|
9226
|
+
async function parseShareConflictReference(config, reference, optionTeam) {
|
|
9227
|
+
const trimmed = reference.trim();
|
|
9228
|
+
if (!trimmed) {
|
|
9229
|
+
throw new Error("Provide a conflict id, relative path, or viking:// shared memory URI.");
|
|
9230
|
+
}
|
|
9231
|
+
if (trimmed.startsWith("viking://")) {
|
|
9232
|
+
const teamName = sharedTeamNameForUri(config, trimmed);
|
|
9233
|
+
if (!teamName) {
|
|
9234
|
+
throw new Error(`Shared memory URI does not include a configured team: ${trimmed}`);
|
|
9235
|
+
}
|
|
9236
|
+
const team2 = await resolveTeam(config, optionTeam ?? teamName);
|
|
9237
|
+
return { relativePath: assertSafeShareRelativePath(vikingUriToWorktreeRelative(config, trimmed, team2.name)), team: team2 };
|
|
9238
|
+
}
|
|
9239
|
+
const colon = trimmed.indexOf(":");
|
|
9240
|
+
if (colon > 0 && !trimmed.slice(0, colon).includes("/")) {
|
|
9241
|
+
const team2 = await resolveTeam(config, optionTeam ?? trimmed.slice(0, colon));
|
|
9242
|
+
return { relativePath: assertSafeShareRelativePath(trimmed.slice(colon + 1)), team: team2 };
|
|
9243
|
+
}
|
|
9244
|
+
const team = await resolveTeam(config, optionTeam);
|
|
9245
|
+
return { relativePath: assertSafeShareRelativePath(trimmed), team };
|
|
9246
|
+
}
|
|
9247
|
+
function assertSafeShareRelativePath(relativePath) {
|
|
9248
|
+
if (!relativePath || relativePath.startsWith("/") || relativePath.split("/").some((segment) => segment === ".." || segment.length === 0)) {
|
|
9249
|
+
throw new Error(`Invalid shared relative path: ${relativePath}`);
|
|
9250
|
+
}
|
|
9251
|
+
return relativePath;
|
|
9252
|
+
}
|
|
9253
|
+
function normalizePendingChange(team, change) {
|
|
9254
|
+
return { ...change, path: (0, import_node_path7.join)(team.config.worktree, change.relativePath) };
|
|
9255
|
+
}
|
|
9256
|
+
function isShareableMemoryChange(change) {
|
|
9257
|
+
const firstSegment = change.relativePath.split("/")[0];
|
|
9258
|
+
return change.relativePath.endsWith(".md") && SHAREABLE_MEMORY_KIND_DIRS.includes(firstSegment);
|
|
9259
|
+
}
|
|
9260
|
+
async function buildShareConflictSummary(config, team, change) {
|
|
9261
|
+
const inspected = await inspectShareConflict(config, team, change);
|
|
9262
|
+
return {
|
|
9263
|
+
hasLocalContent: inspected.hasLocalContent,
|
|
9264
|
+
hasPreviousContent: inspected.hasPreviousContent,
|
|
9265
|
+
hasSharedContent: inspected.hasSharedContent,
|
|
9266
|
+
id: inspected.id,
|
|
9267
|
+
reason: inspected.reason,
|
|
9268
|
+
relativePath: inspected.relativePath,
|
|
9269
|
+
status: inspected.status,
|
|
9270
|
+
team: inspected.team,
|
|
9271
|
+
uri: inspected.uri
|
|
9272
|
+
};
|
|
9273
|
+
}
|
|
9274
|
+
async function inspectShareConflict(config, team, change) {
|
|
9275
|
+
const ov = await openVikingCliForMode(false);
|
|
9276
|
+
const uri = workfileToVikingUri(config, team.config, change.path);
|
|
9277
|
+
const localContent = await readOptionalMemoryContent(config, ov, uri);
|
|
9278
|
+
const shared = await readOptionalSharedConflictContent(uri, change);
|
|
9279
|
+
const previousContent = change.previousContent === void 0 ? void 0 : prepareSharedInboundContent(uri, change.previousContent);
|
|
9280
|
+
return {
|
|
9281
|
+
hasLocalContent: localContent !== void 0,
|
|
9282
|
+
hasPreviousContent: previousContent !== void 0,
|
|
9283
|
+
hasSharedContent: shared.content !== void 0,
|
|
9284
|
+
id: conflictId(team.name, change.relativePath),
|
|
9285
|
+
localContent,
|
|
9286
|
+
previousContent,
|
|
9287
|
+
reason: shareConflictReason(change, localContent, shared.content, previousContent, shared.error),
|
|
9288
|
+
relativePath: change.relativePath,
|
|
9289
|
+
sharedContent: shared.content,
|
|
9290
|
+
status: change.status,
|
|
9291
|
+
team: team.name,
|
|
9292
|
+
uri
|
|
9293
|
+
};
|
|
9294
|
+
}
|
|
9295
|
+
async function readOptionalSharedConflictContent(uri, change) {
|
|
9296
|
+
try {
|
|
9297
|
+
if (change.status === "removed" || !await isRegularFileNoSymlink(change.path)) {
|
|
9298
|
+
return {};
|
|
9299
|
+
}
|
|
9300
|
+
return { content: await readSharedInboundFileContent(uri, change.path) };
|
|
9301
|
+
} catch (err) {
|
|
9302
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
9303
|
+
}
|
|
9304
|
+
}
|
|
9305
|
+
async function readOptionalMemoryContent(config, ov, uri) {
|
|
9306
|
+
if (!await vikingResourceExists(ov, config, uri)) {
|
|
9307
|
+
return void 0;
|
|
9308
|
+
}
|
|
9309
|
+
return readMemoryContent(config, ov, uri, false);
|
|
9310
|
+
}
|
|
9311
|
+
function shareConflictReason(change, localContent, sharedContent, previousContent, sharedError) {
|
|
9312
|
+
if (sharedError) {
|
|
9313
|
+
return `shared file is not readable: ${sharedError}`;
|
|
9314
|
+
}
|
|
9315
|
+
if (change.status === "added") {
|
|
9316
|
+
if (localContent === void 0) {
|
|
9317
|
+
return "shared file is pending ingestion into OpenViking";
|
|
9318
|
+
}
|
|
9319
|
+
if (sharedContent === void 0) {
|
|
9320
|
+
return "shared file is missing or not readable";
|
|
9321
|
+
}
|
|
9322
|
+
return sharedMemoryContentsEquivalent(localContent, sharedContent) ? "pending replay is already reflected in OpenViking" : "local OpenViking content differs from the newly added shared file";
|
|
9323
|
+
}
|
|
9324
|
+
if (change.status === "modified") {
|
|
9325
|
+
if (localContent === void 0) {
|
|
9326
|
+
return "OpenViking resource is missing while a shared update is pending";
|
|
9327
|
+
}
|
|
9328
|
+
if (previousContent === void 0) {
|
|
9329
|
+
return "previous shared content is unavailable, so local edits cannot be distinguished from upstream edits";
|
|
9330
|
+
}
|
|
9331
|
+
return sharedMemoryContentsEquivalent(localContent, previousContent) ? "shared update is pending ingestion into OpenViking" : "local OpenViking content differs from the previous shared version";
|
|
9332
|
+
}
|
|
9333
|
+
if (localContent === void 0) {
|
|
9334
|
+
return "shared deletion is already reflected in OpenViking";
|
|
9335
|
+
}
|
|
9336
|
+
if (previousContent === void 0) {
|
|
9337
|
+
return "previous shared content is unavailable, so local deletion cannot be verified safely";
|
|
9338
|
+
}
|
|
9339
|
+
return sharedMemoryContentsEquivalent(localContent, previousContent) ? "shared deletion is pending removal from OpenViking" : "local OpenViking content differs from the deleted shared version";
|
|
9340
|
+
}
|
|
9341
|
+
async function conflictResolutionContent(conflict, take, fromFile, mergedContent) {
|
|
9342
|
+
const raw = fromFile !== void 0 ? await (0, import_promises5.readFile)(expandPath(fromFile), "utf8") : mergedContent !== void 0 ? mergedContent : take === "local" ? conflict.localContent : void 0;
|
|
9343
|
+
if (raw === void 0) {
|
|
9344
|
+
throw new Error(`Cannot resolve ${conflict.id}: local OpenViking content is unavailable.`);
|
|
9345
|
+
}
|
|
9346
|
+
const scrub = applyScrubber(stripPersonalProvenance(raw), { redact: false });
|
|
9347
|
+
if (scrub.blocker) {
|
|
9348
|
+
throw new Error(
|
|
9349
|
+
`Refusing to resolve ${conflict.id}: possible ${scrub.blocker}. Strip the sensitive value before writing it to shared memory.`
|
|
9350
|
+
);
|
|
9351
|
+
}
|
|
9352
|
+
return scrub.cleaned;
|
|
9353
|
+
}
|
|
9354
|
+
async function writeSharedConflictFile(team, conflict, content, dryRun) {
|
|
9355
|
+
const filePath = (0, import_node_path7.join)(team.config.worktree, conflict.relativePath);
|
|
9356
|
+
if (dryRun) {
|
|
9357
|
+
console.log(`Would write shared file: ${portablePath(filePath)}`);
|
|
9358
|
+
return;
|
|
9359
|
+
}
|
|
9360
|
+
await (0, import_promises5.mkdir)((0, import_node_path7.dirname)(filePath), { recursive: true });
|
|
9361
|
+
await (0, import_promises5.writeFile)(filePath, content, "utf8");
|
|
9362
|
+
}
|
|
9363
|
+
async function backupShareConflict(config, conflict) {
|
|
9364
|
+
const backupDir = (0, import_node_path7.join)(
|
|
9365
|
+
config.agentContextHome,
|
|
9366
|
+
"share",
|
|
9367
|
+
"conflict-backups",
|
|
9368
|
+
safeTimestamp(),
|
|
9369
|
+
conflict.team,
|
|
9370
|
+
...conflict.relativePath.split("/")
|
|
9371
|
+
);
|
|
9372
|
+
await (0, import_promises5.mkdir)(backupDir, { recursive: true });
|
|
9373
|
+
const metadata = {
|
|
9374
|
+
id: conflict.id,
|
|
9375
|
+
reason: conflict.reason,
|
|
9376
|
+
relativePath: conflict.relativePath,
|
|
9377
|
+
status: conflict.status,
|
|
9378
|
+
team: conflict.team,
|
|
9379
|
+
uri: conflict.uri
|
|
9380
|
+
};
|
|
9381
|
+
await (0, import_promises5.writeFile)((0, import_node_path7.join)(backupDir, "metadata.json"), `${JSON.stringify(metadata, void 0, 2)}
|
|
9382
|
+
`, "utf8");
|
|
9383
|
+
if (conflict.localContent !== void 0) {
|
|
9384
|
+
await (0, import_promises5.writeFile)((0, import_node_path7.join)(backupDir, "local.md"), conflict.localContent, "utf8");
|
|
9385
|
+
}
|
|
9386
|
+
if (conflict.sharedContent !== void 0) {
|
|
9387
|
+
await (0, import_promises5.writeFile)((0, import_node_path7.join)(backupDir, "shared.md"), conflict.sharedContent, "utf8");
|
|
9388
|
+
}
|
|
9389
|
+
if (conflict.previousContent !== void 0) {
|
|
9390
|
+
await (0, import_promises5.writeFile)((0, import_node_path7.join)(backupDir, "previous.md"), conflict.previousContent, "utf8");
|
|
9391
|
+
}
|
|
9392
|
+
return backupDir;
|
|
9393
|
+
}
|
|
9394
|
+
async function clearPendingShareConflict(config, teamName, relativePath) {
|
|
9395
|
+
const state = autoShareState(config);
|
|
9396
|
+
await loadPendingReindexes(config, state);
|
|
9397
|
+
const pending = state.pendingReindexes.get(teamName) ?? [];
|
|
9398
|
+
const remaining = pending.filter((change) => change.relativePath !== relativePath);
|
|
9399
|
+
if (remaining.length > 0) {
|
|
9400
|
+
state.pendingReindexes.set(teamName, remaining);
|
|
9401
|
+
} else {
|
|
9402
|
+
state.pendingReindexes.delete(teamName);
|
|
9403
|
+
}
|
|
9404
|
+
await writePendingReindexes(config, state);
|
|
9405
|
+
}
|
|
9406
|
+
function conflictId(team, relativePath) {
|
|
9407
|
+
return `${team}:${relativePath}`;
|
|
9408
|
+
}
|
|
9409
|
+
function shareConflictResolutionGuidance(id) {
|
|
9410
|
+
return [
|
|
9411
|
+
`threadnote share conflict resolve ${id} --take shared`,
|
|
9412
|
+
`threadnote share conflict resolve ${id} --take local`,
|
|
9413
|
+
`threadnote share conflict resolve ${id} --from-file merged.md`
|
|
9414
|
+
];
|
|
9415
|
+
}
|
|
9416
|
+
function formatShareConflictNextSteps(teamName, changes) {
|
|
9417
|
+
const ids = changes.filter(isShareableMemoryChange).map((change) => conflictId(teamName, change.relativePath));
|
|
9418
|
+
if (ids.length === 0) {
|
|
9419
|
+
return `Run \`threadnote share conflicts --team ${teamName}\` to inspect pending reindexes.`;
|
|
9420
|
+
}
|
|
9421
|
+
return [
|
|
9422
|
+
`Resolve pending shared memory conflicts with:`,
|
|
9423
|
+
` threadnote share conflicts --team ${teamName}`,
|
|
9424
|
+
...ids.flatMap((id) => [
|
|
9425
|
+
` threadnote share conflict show ${id}`,
|
|
9426
|
+
` threadnote share conflict resolve ${id} --take shared`,
|
|
9427
|
+
` threadnote share conflict resolve ${id} --take local`,
|
|
9428
|
+
` threadnote share conflict resolve ${id} --from-file merged.md`
|
|
9429
|
+
])
|
|
9430
|
+
].join("\n");
|
|
9431
|
+
}
|
|
9432
|
+
function formatShareConflictDiff(conflict) {
|
|
9433
|
+
const parts = [];
|
|
9434
|
+
if (conflict.previousContent !== void 0) {
|
|
9435
|
+
parts.push(
|
|
9436
|
+
formatTwoWayDiff("previous shared", conflict.previousContent, "local OpenViking", conflict.localContent)
|
|
9437
|
+
);
|
|
9438
|
+
}
|
|
9439
|
+
parts.push(formatTwoWayDiff("local OpenViking", conflict.localContent, "shared file", conflict.sharedContent));
|
|
9440
|
+
return parts.join("\n\n");
|
|
9441
|
+
}
|
|
9442
|
+
function formatTwoWayDiff(leftLabel, leftContent, rightLabel, rightContent) {
|
|
9443
|
+
if (leftContent === void 0 && rightContent === void 0) {
|
|
9444
|
+
return `${leftLabel} and ${rightLabel} are both unavailable.`;
|
|
9445
|
+
}
|
|
9446
|
+
if (leftContent === rightContent) {
|
|
9447
|
+
return `${leftLabel} and ${rightLabel} are identical.`;
|
|
9448
|
+
}
|
|
9449
|
+
const leftLines = splitDiffLines(leftContent);
|
|
9450
|
+
const rightLines = splitDiffLines(rightContent);
|
|
9451
|
+
const lines = [`--- ${leftLabel}`, `+++ ${rightLabel}`];
|
|
9452
|
+
for (const line of leftLines) {
|
|
9453
|
+
lines.push(`-${line}`);
|
|
9454
|
+
}
|
|
9455
|
+
for (const line of rightLines) {
|
|
9456
|
+
lines.push(`+${line}`);
|
|
9457
|
+
}
|
|
9458
|
+
return lines.join("\n");
|
|
9459
|
+
}
|
|
9460
|
+
function splitDiffLines(content) {
|
|
9461
|
+
if (content === void 0) {
|
|
9462
|
+
return ["<missing>"];
|
|
9463
|
+
}
|
|
9464
|
+
const lines = content.split(/\n/);
|
|
9465
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
9466
|
+
return lines.slice(0, -1);
|
|
9467
|
+
}
|
|
9468
|
+
return lines;
|
|
9469
|
+
}
|
|
9021
9470
|
async function stageShareableChanges(dryRun, git, worktree) {
|
|
9022
9471
|
await removeOrphanPackIndexes(dryRun, git, worktree);
|
|
9023
9472
|
const pathspecs = await existingShareablePathspecs(git, worktree);
|
|
@@ -11395,7 +11844,7 @@ async function applyChangesToOpenViking(config, team, changes, options = {}) {
|
|
|
11395
11844
|
const currentContent = await readExistingMemoryContent(config, ov, uri);
|
|
11396
11845
|
if (currentContent !== void 0) {
|
|
11397
11846
|
if (change.status === "added") {
|
|
11398
|
-
if (currentContent
|
|
11847
|
+
if (sharedMemoryContentsEquivalent(currentContent, content)) {
|
|
11399
11848
|
continue;
|
|
11400
11849
|
}
|
|
11401
11850
|
throw new Error(
|
|
@@ -11434,12 +11883,18 @@ function assertInboundPreviousContentMatches(change, uri, currentContent) {
|
|
|
11434
11883
|
);
|
|
11435
11884
|
}
|
|
11436
11885
|
const expectedContent = prepareSharedInboundContent(uri, change.previousContent);
|
|
11437
|
-
if (currentContent
|
|
11886
|
+
if (!sharedMemoryContentsEquivalent(currentContent, expectedContent)) {
|
|
11438
11887
|
throw new Error(
|
|
11439
11888
|
`Refusing to apply inbound shared change for ${uri}: local OpenViking content differs from the previous shared version. Inspect and resolve the local edit first.`
|
|
11440
11889
|
);
|
|
11441
11890
|
}
|
|
11442
11891
|
}
|
|
11892
|
+
function sharedMemoryContentsEquivalent(left, right) {
|
|
11893
|
+
return normalizeSharedMemoryComparisonContent(left) === normalizeSharedMemoryComparisonContent(right);
|
|
11894
|
+
}
|
|
11895
|
+
function normalizeSharedMemoryComparisonContent(content) {
|
|
11896
|
+
return content.replace(/\r\n?/g, "\n").replace(/\n$/, "");
|
|
11897
|
+
}
|
|
11443
11898
|
function mergeChanges(...lists) {
|
|
11444
11899
|
const map = /* @__PURE__ */ new Map();
|
|
11445
11900
|
for (const list of lists) {
|
|
@@ -17783,9 +18238,19 @@ async function main() {
|
|
|
17783
18238
|
share.command("status").description("Show git status and ahead/behind counts for a shared team").option("--team <name>", "Team name; defaults to the configured default team").option("--dry-run", "Print git commands without running them").action(async (options) => {
|
|
17784
18239
|
await runShareStatus(getRuntimeConfig(program2), options);
|
|
17785
18240
|
});
|
|
17786
|
-
share.command("sync").description("Pull, reindex, and push
|
|
18241
|
+
share.command("sync").description("Pull, reindex, and push shared memories repos").option("--team <name>", "Team name; when omitted, syncs all configured teams").option("--message <text>", "Commit message when auto-committing local edits").option("--no-auto-commit", "Refuse to sync if there are uncommitted local changes").option("--no-push", "Skip the push step after pulling and reindexing").option("--dry-run", "Print actions without running them").action(async (options) => {
|
|
17787
18242
|
await runShareSync(getRuntimeConfig(program2), options);
|
|
17788
18243
|
});
|
|
18244
|
+
share.command("conflicts").description("List pending shared memory conflicts from failed share sync reindexes").option("--team <name>", "Team name; defaults to all configured teams").action(async (options) => {
|
|
18245
|
+
await runShareConflicts(getRuntimeConfig(program2), options);
|
|
18246
|
+
});
|
|
18247
|
+
const shareConflict = share.command("conflict").description("Inspect or resolve one pending shared memory conflict");
|
|
18248
|
+
shareConflict.command("show").description("Show local/shared content and resolution commands for one pending shared memory conflict").argument("<conflict-id>", "Conflict id from `threadnote share conflicts`, relative path, or shared viking:// URI").option("--team <name>", "Team name when conflict-id is a relative path").action(async (conflictId2, options) => {
|
|
18249
|
+
await runShareConflictShow(getRuntimeConfig(program2), conflictId2, options);
|
|
18250
|
+
});
|
|
18251
|
+
shareConflict.command("resolve").description("Resolve one pending shared memory conflict with shared, local, or merged content").argument("<conflict-id>", "Conflict id from `threadnote share conflicts`, relative path, or shared viking:// URI").option("--team <name>", "Team name when conflict-id is a relative path").option("--take <side>", "Resolution side: shared or local").option("--from-file <path>", "Merged memory markdown to write to both OpenViking and the shared repo").option("--message <text>", "Commit message when writing local or merged content to the shared repo").option("--no-push", "Skip pushing a local/merged resolution commit").option("--dry-run", "Print actions without writing OpenViking, shared files, git commits, or pending state").action(async (conflictId2, options) => {
|
|
18252
|
+
await runShareConflictResolve(getRuntimeConfig(program2), conflictId2, options);
|
|
18253
|
+
});
|
|
17789
18254
|
share.command("publish").description("Move a personal memory into the shared team namespace, commit and push").argument("<viking-uri>", "viking:// memory URI to publish").option("--team <name>", "Team name; defaults to the configured default team").option("--message <text>", "Commit message override").option("--no-push", "Skip the push step").option("--dry-run", "Print actions without running them").option(
|
|
17790
18255
|
"--preview",
|
|
17791
18256
|
"Print the exact bytes that would land in the shared git repo (after frontmatter strip and scrubber redaction) without writing, committing, or pushing"
|
package/docs/share.md
CHANGED
|
@@ -291,12 +291,13 @@ Manual sync remains useful when you want to publish local edits, clear a dirty
|
|
|
291
291
|
shared worktree, resolve git conflicts, or force a sync immediately:
|
|
292
292
|
|
|
293
293
|
```bash
|
|
294
|
-
threadnote share sync #
|
|
295
|
-
threadnote share sync --team friends #
|
|
296
|
-
threadnote share sync --no-push # pull only
|
|
294
|
+
threadnote share sync # all configured teams
|
|
295
|
+
threadnote share sync --team friends # one team
|
|
296
|
+
threadnote share sync --no-push # pull only, all configured teams
|
|
297
297
|
```
|
|
298
298
|
|
|
299
|
-
`share sync`
|
|
299
|
+
`share sync` without `--team` runs this flow for every configured team. For each
|
|
300
|
+
team it will auto-commit edits in Threadnote-managed share paths, fetch and
|
|
300
301
|
rebase onto the configured upstream, reindex pulled markdown files into
|
|
301
302
|
OpenViking (so `recall` finds them immediately), and push. Managed share paths
|
|
302
303
|
are root guidance/metadata files (`README.md`, `AGENTS.md`, `CLAUDE.md`,
|
|
@@ -397,6 +398,47 @@ upstream. When git can't merge cleanly:
|
|
|
397
398
|
Two publishes touching the same `<topic>.md` from different machines will
|
|
398
399
|
collide; coordinate ownership per-topic, or use distinct topics.
|
|
399
400
|
|
|
401
|
+
### Resolve pending shared memory conflicts
|
|
402
|
+
|
|
403
|
+
If `share sync` cannot safely reindex a shared memory into OpenViking, it keeps
|
|
404
|
+
that file in the pending reindex queue and prints resolver commands. This is
|
|
405
|
+
different from a git rebase conflict: the shared git worktree is clean, but the
|
|
406
|
+
local OpenViking resource differs from the shared file or from the previous
|
|
407
|
+
shared version.
|
|
408
|
+
|
|
409
|
+
Inspect pending conflicts:
|
|
410
|
+
|
|
411
|
+
```bash
|
|
412
|
+
threadnote share conflicts
|
|
413
|
+
threadnote share conflicts --team default
|
|
414
|
+
threadnote share conflict show default:durable/projects/foo/bar.md
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Resolve exactly one pending entry:
|
|
418
|
+
|
|
419
|
+
```bash
|
|
420
|
+
# Accept the shared git file into OpenViking. No git commit is made.
|
|
421
|
+
threadnote share conflict resolve default:durable/projects/foo/bar.md --take shared
|
|
422
|
+
|
|
423
|
+
# Publish the local OpenViking content back to the shared repo, then push.
|
|
424
|
+
threadnote share conflict resolve default:durable/projects/foo/bar.md --take local
|
|
425
|
+
|
|
426
|
+
# Write an explicit merge result to both OpenViking and the shared repo.
|
|
427
|
+
threadnote share conflict resolve default:durable/projects/foo/bar.md --from-file merged.md
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Each resolver writes a backup under
|
|
431
|
+
`~/.openviking/share/conflict-backups/<timestamp>/...` before mutating local
|
|
432
|
+
state. `--take shared` clears the pending item after updating or deleting the
|
|
433
|
+
OpenViking resource. `--take local` and `--from-file` run the same scrubber as
|
|
434
|
+
publish, commit the shared file, push by default, update OpenViking, and then
|
|
435
|
+
clear only the resolved pending item. Pass `--no-push` to leave the resolution
|
|
436
|
+
commit local.
|
|
437
|
+
|
|
438
|
+
MCP agents have equivalent tools: `share_conflicts`, `share_conflict_show`, and
|
|
439
|
+
`share_conflict_resolve`. `share_conflict_resolve` accepts `take:"shared"`,
|
|
440
|
+
`take:"local"`, or `mergedContent:"..."`.
|
|
441
|
+
|
|
400
442
|
## Cross-machine identity notes
|
|
401
443
|
|
|
402
444
|
Each user clones into their own user-namespaced path. A memory authored on
|