@letta-ai/letta-code 0.14.3 → 0.14.4
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/letta.js +2227 -1842
- package/package.json +2 -2
- package/skills/syncing-memory-filesystem/scripts/lib/frontmatter.ts +47 -0
- package/skills/syncing-memory-filesystem/scripts/memfs-diff.ts +176 -107
- package/skills/syncing-memory-filesystem/scripts/memfs-resolve.ts +209 -115
- package/skills/syncing-memory-filesystem/scripts/memfs-status.ts +164 -127
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@letta-ai/letta-code",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.4",
|
|
4
4
|
"description": "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@letta-ai/letta-client": "^1.7.
|
|
33
|
+
"@letta-ai/letta-client": "^1.7.7",
|
|
34
34
|
"glob": "^13.0.0",
|
|
35
35
|
"ink-link": "^5.0.0",
|
|
36
36
|
"open": "^10.2.0",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { READ_ONLY_BLOCK_LABELS } from "../../../../../agent/memoryConstants";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Read-only block labels. These are API-authoritative.
|
|
6
|
+
*/
|
|
7
|
+
export const READ_ONLY_LABELS = new Set(
|
|
8
|
+
READ_ONLY_BLOCK_LABELS as readonly string[],
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse MDX-style frontmatter from content.
|
|
13
|
+
* This is a copy of parseMdxFrontmatter from src/agent/memory.ts.
|
|
14
|
+
* The test ensures this stays in sync with the original.
|
|
15
|
+
*/
|
|
16
|
+
export function parseFrontmatter(content: string): {
|
|
17
|
+
frontmatter: Record<string, string>;
|
|
18
|
+
body: string;
|
|
19
|
+
} {
|
|
20
|
+
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
|
|
21
|
+
const match = content.match(frontmatterRegex);
|
|
22
|
+
|
|
23
|
+
if (!match || !match[1] || !match[2]) {
|
|
24
|
+
return { frontmatter: {}, body: content };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const frontmatterText = match[1];
|
|
28
|
+
const body = match[2];
|
|
29
|
+
const frontmatter: Record<string, string> = {};
|
|
30
|
+
|
|
31
|
+
// Parse YAML-like frontmatter (simple key: value pairs)
|
|
32
|
+
for (const line of frontmatterText.split("\n")) {
|
|
33
|
+
const colonIndex = line.indexOf(":");
|
|
34
|
+
if (colonIndex > 0) {
|
|
35
|
+
const key = line.slice(0, colonIndex).trim();
|
|
36
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
37
|
+
frontmatter[key] = value;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { frontmatter, body: body.trim() };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function hashFileBody(content: string): string {
|
|
45
|
+
const { body } = parseFrontmatter(content);
|
|
46
|
+
return createHash("sha256").update(body).digest("hex");
|
|
47
|
+
}
|
|
@@ -18,6 +18,7 @@ import { readdir, readFile } from "node:fs/promises";
|
|
|
18
18
|
import { createRequire } from "node:module";
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
20
|
import { join, normalize, relative } from "node:path";
|
|
21
|
+
import { hashFileBody, READ_ONLY_LABELS } from "./lib/frontmatter";
|
|
21
22
|
|
|
22
23
|
const require = createRequire(import.meta.url);
|
|
23
24
|
const Letta = require("@letta-ai/letta-client")
|
|
@@ -45,12 +46,11 @@ function getApiKey(): string {
|
|
|
45
46
|
|
|
46
47
|
const MEMORY_FS_STATE_FILE = ".sync-state.json";
|
|
47
48
|
|
|
49
|
+
// Unified sync state format (matches main memoryFilesystem.ts)
|
|
48
50
|
type SyncState = {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
detachedFiles: Record<string, string>;
|
|
53
|
-
detachedBlockIds: Record<string, string>;
|
|
51
|
+
blockHashes: Record<string, string>;
|
|
52
|
+
fileHashes: Record<string, string>;
|
|
53
|
+
blockIds: Record<string, string>;
|
|
54
54
|
lastSync: string | null;
|
|
55
55
|
};
|
|
56
56
|
|
|
@@ -58,6 +58,8 @@ function hashContent(content: string): string {
|
|
|
58
58
|
return createHash("sha256").update(content).digest("hex");
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// parseFrontmatter/hashFileBody provided by shared helper
|
|
62
|
+
|
|
61
63
|
function getMemoryRoot(agentId: string): string {
|
|
62
64
|
return join(homedir(), ".letta", "agents", agentId, "memory");
|
|
63
65
|
}
|
|
@@ -66,49 +68,45 @@ function loadSyncState(agentId: string): SyncState {
|
|
|
66
68
|
const statePath = join(getMemoryRoot(agentId), MEMORY_FS_STATE_FILE);
|
|
67
69
|
if (!existsSync(statePath)) {
|
|
68
70
|
return {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
detachedFiles: {},
|
|
73
|
-
detachedBlockIds: {},
|
|
71
|
+
blockHashes: {},
|
|
72
|
+
fileHashes: {},
|
|
73
|
+
blockIds: {},
|
|
74
74
|
lastSync: null,
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
try {
|
|
79
79
|
const raw = readFileSync(statePath, "utf-8");
|
|
80
|
-
const parsed = JSON.parse(raw)
|
|
81
|
-
blocks?: Record<string, string>;
|
|
82
|
-
files?: Record<string, string>;
|
|
83
|
-
};
|
|
80
|
+
const parsed = JSON.parse(raw);
|
|
84
81
|
return {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
detachedFiles: parsed.detachedFiles || {},
|
|
89
|
-
detachedBlockIds: parsed.detachedBlockIds || {},
|
|
82
|
+
blockHashes: parsed.blockHashes || {},
|
|
83
|
+
fileHashes: parsed.fileHashes || {},
|
|
84
|
+
blockIds: parsed.blockIds || {},
|
|
90
85
|
lastSync: parsed.lastSync || null,
|
|
91
86
|
};
|
|
92
87
|
} catch {
|
|
93
88
|
return {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
detachedFiles: {},
|
|
98
|
-
detachedBlockIds: {},
|
|
89
|
+
blockHashes: {},
|
|
90
|
+
fileHashes: {},
|
|
91
|
+
blockIds: {},
|
|
99
92
|
lastSync: null,
|
|
100
93
|
};
|
|
101
94
|
}
|
|
102
95
|
}
|
|
103
96
|
|
|
104
|
-
async function scanMdFiles(
|
|
97
|
+
async function scanMdFiles(
|
|
98
|
+
dir: string,
|
|
99
|
+
baseDir = dir,
|
|
100
|
+
excludeDirs: string[] = [],
|
|
101
|
+
): Promise<string[]> {
|
|
105
102
|
if (!existsSync(dir)) return [];
|
|
106
103
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
107
104
|
const results: string[] = [];
|
|
108
105
|
for (const entry of entries) {
|
|
109
106
|
const fullPath = join(dir, entry.name);
|
|
110
107
|
if (entry.isDirectory()) {
|
|
111
|
-
|
|
108
|
+
if (excludeDirs.includes(entry.name)) continue;
|
|
109
|
+
results.push(...(await scanMdFiles(fullPath, baseDir, excludeDirs)));
|
|
112
110
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
113
111
|
results.push(relative(baseDir, fullPath));
|
|
114
112
|
}
|
|
@@ -122,8 +120,9 @@ function labelFromPath(relativePath: string): string {
|
|
|
122
120
|
|
|
123
121
|
async function readMemoryFiles(
|
|
124
122
|
dir: string,
|
|
123
|
+
excludeDirs: string[] = [],
|
|
125
124
|
): Promise<Map<string, { content: string }>> {
|
|
126
|
-
const files = await scanMdFiles(dir);
|
|
125
|
+
const files = await scanMdFiles(dir, dir, excludeDirs);
|
|
127
126
|
const entries = new Map<string, { content: string }>();
|
|
128
127
|
for (const rel of files) {
|
|
129
128
|
const label = labelFromPath(rel);
|
|
@@ -133,11 +132,8 @@ async function readMemoryFiles(
|
|
|
133
132
|
return entries;
|
|
134
133
|
}
|
|
135
134
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
"skills",
|
|
139
|
-
"loaded_skills",
|
|
140
|
-
]);
|
|
135
|
+
// Only memory_filesystem is managed by memfs itself
|
|
136
|
+
const MEMFS_MANAGED_LABELS = new Set(["memory_filesystem"]);
|
|
141
137
|
|
|
142
138
|
interface Conflict {
|
|
143
139
|
label: string;
|
|
@@ -145,6 +141,12 @@ interface Conflict {
|
|
|
145
141
|
blockContent: string;
|
|
146
142
|
}
|
|
147
143
|
|
|
144
|
+
interface MetadataChange {
|
|
145
|
+
label: string;
|
|
146
|
+
fileContent: string;
|
|
147
|
+
blockContent: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
148
150
|
/**
|
|
149
151
|
* Get the overflow directory following the same pattern as tool output overflow.
|
|
150
152
|
* Pattern: ~/.letta/projects/<project-path>/agent-tools/
|
|
@@ -160,122 +162,163 @@ function getOverflowDirectory(): string {
|
|
|
160
162
|
return join(homedir(), ".letta", "projects", sanitizedPath, "agent-tools");
|
|
161
163
|
}
|
|
162
164
|
|
|
163
|
-
async function findConflicts(agentId: string): Promise<
|
|
165
|
+
async function findConflicts(agentId: string): Promise<{
|
|
166
|
+
conflicts: Conflict[];
|
|
167
|
+
metadataOnly: MetadataChange[];
|
|
168
|
+
}> {
|
|
164
169
|
const baseUrl = process.env.LETTA_BASE_URL || "https://api.letta.com";
|
|
165
170
|
const client = new Letta({ apiKey: getApiKey(), baseUrl });
|
|
166
171
|
|
|
167
172
|
const root = getMemoryRoot(agentId);
|
|
168
173
|
const systemDir = join(root, "system");
|
|
169
|
-
// Detached files go at root level (flat structure)
|
|
170
174
|
const detachedDir = root;
|
|
171
175
|
|
|
172
176
|
for (const dir of [root, systemDir]) {
|
|
173
177
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
174
178
|
}
|
|
175
179
|
|
|
180
|
+
// Read files from both locations
|
|
176
181
|
const systemFiles = await readMemoryFiles(systemDir);
|
|
177
|
-
const detachedFiles = await readMemoryFiles(detachedDir);
|
|
178
|
-
systemFiles.delete("memory_filesystem");
|
|
182
|
+
const detachedFiles = await readMemoryFiles(detachedDir, ["system", "user"]);
|
|
179
183
|
|
|
184
|
+
// Fetch attached blocks
|
|
180
185
|
const blocksResponse = await client.agents.blocks.list(agentId, {
|
|
181
186
|
limit: 1000,
|
|
182
187
|
});
|
|
183
|
-
const
|
|
188
|
+
const attachedBlocks = Array.isArray(blocksResponse)
|
|
184
189
|
? blocksResponse
|
|
185
190
|
: ((blocksResponse as { items?: unknown[] }).items as Array<{
|
|
191
|
+
id?: string;
|
|
186
192
|
label?: string;
|
|
187
193
|
value?: string;
|
|
194
|
+
read_only?: boolean;
|
|
188
195
|
}>) || [];
|
|
189
196
|
|
|
190
|
-
const systemBlockMap = new Map
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const detachedBlockMap = new Map<string, string>();
|
|
203
|
-
for (const [label, blockId] of Object.entries(lastState.detachedBlockIds)) {
|
|
204
|
-
try {
|
|
205
|
-
const block = await client.blocks.retrieve(blockId);
|
|
206
|
-
detachedBlockMap.set(label, block.value || "");
|
|
207
|
-
} catch {
|
|
208
|
-
// Block no longer exists
|
|
197
|
+
const systemBlockMap = new Map<
|
|
198
|
+
string,
|
|
199
|
+
{ value: string; id: string; read_only?: boolean }
|
|
200
|
+
>();
|
|
201
|
+
for (const block of attachedBlocks) {
|
|
202
|
+
if (block.label && block.id) {
|
|
203
|
+
systemBlockMap.set(block.label, {
|
|
204
|
+
value: block.value || "",
|
|
205
|
+
id: block.id,
|
|
206
|
+
read_only: block.read_only,
|
|
207
|
+
});
|
|
209
208
|
}
|
|
210
209
|
}
|
|
211
210
|
|
|
212
|
-
|
|
211
|
+
// Fetch detached blocks via owner tag
|
|
212
|
+
const ownedBlocksResponse = await client.blocks.list({
|
|
213
|
+
tags: [`owner:${agentId}`],
|
|
214
|
+
limit: 1000,
|
|
215
|
+
});
|
|
216
|
+
const ownedBlocks = Array.isArray(ownedBlocksResponse)
|
|
217
|
+
? ownedBlocksResponse
|
|
218
|
+
: ((ownedBlocksResponse as { items?: unknown[] }).items as Array<{
|
|
219
|
+
id?: string;
|
|
220
|
+
label?: string;
|
|
221
|
+
value?: string;
|
|
222
|
+
read_only?: boolean;
|
|
223
|
+
}>) || [];
|
|
213
224
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
conflicts.push({ label, fileContent, blockContent: blockValue });
|
|
225
|
+
const attachedIds = new Set(attachedBlocks.map((b) => b.id));
|
|
226
|
+
const detachedBlockMap = new Map<
|
|
227
|
+
string,
|
|
228
|
+
{ value: string; id: string; read_only?: boolean }
|
|
229
|
+
>();
|
|
230
|
+
for (const block of ownedBlocks) {
|
|
231
|
+
if (block.label && block.id && !attachedIds.has(block.id)) {
|
|
232
|
+
if (!systemBlockMap.has(block.label)) {
|
|
233
|
+
detachedBlockMap.set(block.label, {
|
|
234
|
+
value: block.value || "",
|
|
235
|
+
id: block.id,
|
|
236
|
+
read_only: block.read_only,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
229
239
|
}
|
|
230
240
|
}
|
|
231
241
|
|
|
232
|
-
|
|
233
|
-
const
|
|
242
|
+
const lastState = loadSyncState(agentId);
|
|
243
|
+
const conflicts: Conflict[] = [];
|
|
244
|
+
const metadataOnly: MetadataChange[] = [];
|
|
245
|
+
|
|
246
|
+
// Collect all labels
|
|
247
|
+
const allLabels = new Set<string>([
|
|
234
248
|
...systemFiles.keys(),
|
|
249
|
+
...detachedFiles.keys(),
|
|
235
250
|
...systemBlockMap.keys(),
|
|
236
|
-
...
|
|
237
|
-
...Object.keys(lastState.
|
|
251
|
+
...detachedBlockMap.keys(),
|
|
252
|
+
...Object.keys(lastState.blockHashes),
|
|
253
|
+
...Object.keys(lastState.fileHashes),
|
|
238
254
|
]);
|
|
239
255
|
|
|
240
|
-
for (const label of [...
|
|
241
|
-
if (
|
|
242
|
-
checkConflict(
|
|
243
|
-
label,
|
|
244
|
-
systemFiles.get(label)?.content ?? null,
|
|
245
|
-
systemBlockMap.get(label) ?? null,
|
|
246
|
-
lastState.systemFiles[label] ?? null,
|
|
247
|
-
lastState.systemBlocks[label] ?? null,
|
|
248
|
-
);
|
|
249
|
-
}
|
|
256
|
+
for (const label of [...allLabels].sort()) {
|
|
257
|
+
if (MEMFS_MANAGED_LABELS.has(label)) continue;
|
|
250
258
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
...Object.keys(lastState.detachedBlocks),
|
|
256
|
-
...Object.keys(lastState.detachedFiles),
|
|
257
|
-
]);
|
|
259
|
+
const systemFile = systemFiles.get(label);
|
|
260
|
+
const detachedFile = detachedFiles.get(label);
|
|
261
|
+
const attachedBlock = systemBlockMap.get(label);
|
|
262
|
+
const detachedBlock = detachedBlockMap.get(label);
|
|
258
263
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
264
|
+
const fileEntry = systemFile || detachedFile;
|
|
265
|
+
const blockEntry = attachedBlock || detachedBlock;
|
|
266
|
+
|
|
267
|
+
if (!fileEntry || !blockEntry) continue;
|
|
268
|
+
|
|
269
|
+
// read_only blocks are API-authoritative; no conflicts possible
|
|
270
|
+
const effectiveReadOnly =
|
|
271
|
+
!!blockEntry.read_only || READ_ONLY_LABELS.has(label);
|
|
272
|
+
if (effectiveReadOnly) continue;
|
|
273
|
+
|
|
274
|
+
// Full file hash for "file changed" check
|
|
275
|
+
const fileHash = hashContent(fileEntry.content);
|
|
276
|
+
// Body hash for "content matches" check
|
|
277
|
+
const fileBodyHash = hashFileBody(fileEntry.content);
|
|
278
|
+
const blockHash = hashContent(blockEntry.value);
|
|
279
|
+
|
|
280
|
+
const lastFileHash = lastState.fileHashes[label] ?? null;
|
|
281
|
+
const lastBlockHash = lastState.blockHashes[label] ?? null;
|
|
282
|
+
const fileChanged = fileHash !== lastFileHash;
|
|
283
|
+
const blockChanged = blockHash !== lastBlockHash;
|
|
284
|
+
|
|
285
|
+
// Content matches - check for frontmatter-only changes
|
|
286
|
+
if (fileBodyHash === blockHash) {
|
|
287
|
+
if (fileChanged) {
|
|
288
|
+
metadataOnly.push({
|
|
289
|
+
label,
|
|
290
|
+
fileContent: fileEntry.content,
|
|
291
|
+
blockContent: blockEntry.value,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Conflict only if both changed
|
|
298
|
+
if (fileChanged && blockChanged) {
|
|
299
|
+
conflicts.push({
|
|
300
|
+
label,
|
|
301
|
+
fileContent: fileEntry.content,
|
|
302
|
+
blockContent: blockEntry.value,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
267
305
|
}
|
|
268
306
|
|
|
269
|
-
return conflicts;
|
|
307
|
+
return { conflicts, metadataOnly };
|
|
270
308
|
}
|
|
271
309
|
|
|
272
|
-
function formatDiffFile(
|
|
310
|
+
function formatDiffFile(
|
|
311
|
+
conflicts: Conflict[],
|
|
312
|
+
metadataOnly: MetadataChange[],
|
|
313
|
+
agentId: string,
|
|
314
|
+
): string {
|
|
273
315
|
const lines: string[] = [
|
|
274
316
|
`# Memory Filesystem Diff`,
|
|
275
317
|
``,
|
|
276
318
|
`Agent: ${agentId}`,
|
|
277
319
|
`Generated: ${new Date().toISOString()}`,
|
|
278
320
|
`Conflicts: ${conflicts.length}`,
|
|
321
|
+
`Metadata-only changes: ${metadataOnly.length}`,
|
|
279
322
|
``,
|
|
280
323
|
`---`,
|
|
281
324
|
``,
|
|
@@ -298,6 +341,32 @@ function formatDiffFile(conflicts: Conflict[], agentId: string): string {
|
|
|
298
341
|
lines.push(``);
|
|
299
342
|
}
|
|
300
343
|
|
|
344
|
+
if (metadataOnly.length > 0) {
|
|
345
|
+
lines.push(`## Metadata-only Changes`);
|
|
346
|
+
lines.push(``);
|
|
347
|
+
lines.push(
|
|
348
|
+
`Frontmatter changed while body content stayed the same (file wins).`,
|
|
349
|
+
);
|
|
350
|
+
lines.push(``);
|
|
351
|
+
|
|
352
|
+
for (const change of metadataOnly) {
|
|
353
|
+
lines.push(`### ${change.label}`);
|
|
354
|
+
lines.push(``);
|
|
355
|
+
lines.push(`#### File Version (with frontmatter)`);
|
|
356
|
+
lines.push(`\`\`\``);
|
|
357
|
+
lines.push(change.fileContent);
|
|
358
|
+
lines.push(`\`\`\``);
|
|
359
|
+
lines.push(``);
|
|
360
|
+
lines.push(`#### Block Version (body only)`);
|
|
361
|
+
lines.push(`\`\`\``);
|
|
362
|
+
lines.push(change.blockContent);
|
|
363
|
+
lines.push(`\`\`\``);
|
|
364
|
+
lines.push(``);
|
|
365
|
+
lines.push(`---`);
|
|
366
|
+
lines.push(``);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
301
370
|
return lines.join("\n");
|
|
302
371
|
}
|
|
303
372
|
|
|
@@ -329,13 +398,13 @@ Output: Path to the diff file, or a message if no conflicts exist.
|
|
|
329
398
|
}
|
|
330
399
|
|
|
331
400
|
findConflicts(agentId)
|
|
332
|
-
.then((conflicts) => {
|
|
333
|
-
if (conflicts.length === 0) {
|
|
401
|
+
.then(({ conflicts, metadataOnly }) => {
|
|
402
|
+
if (conflicts.length === 0 && metadataOnly.length === 0) {
|
|
334
403
|
console.log("No conflicts found. Memory filesystem is clean.");
|
|
335
404
|
return;
|
|
336
405
|
}
|
|
337
406
|
|
|
338
|
-
const diffContent = formatDiffFile(conflicts, agentId);
|
|
407
|
+
const diffContent = formatDiffFile(conflicts, metadataOnly, agentId);
|
|
339
408
|
|
|
340
409
|
// Write to overflow directory (same pattern as tool output overflow)
|
|
341
410
|
const overflowDir = getOverflowDirectory();
|
|
@@ -348,7 +417,7 @@ Output: Path to the diff file, or a message if no conflicts exist.
|
|
|
348
417
|
writeFileSync(diffPath, diffContent, "utf-8");
|
|
349
418
|
|
|
350
419
|
console.log(
|
|
351
|
-
`Diff (${conflicts.length} conflict${conflicts.length === 1 ? "" : "s"}) written to: ${diffPath}`,
|
|
420
|
+
`Diff (${conflicts.length} conflict${conflicts.length === 1 ? "" : "s"}, ${metadataOnly.length} metadata-only change${metadataOnly.length === 1 ? "" : "s"}) written to: ${diffPath}`,
|
|
352
421
|
);
|
|
353
422
|
})
|
|
354
423
|
.catch((error) => {
|