mcp-memory-bucket 0.10.2 → 0.10.3
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.
|
@@ -23,10 +23,34 @@ export class AttachmentRepository {
|
|
|
23
23
|
await this.skillRepo.update(docIdOrName, { attachments });
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Pushes an attachment's binary content to folderfoo if the doc lives in a remote folder —
|
|
28
|
+
* no-op for a local folder (both repos' pushAttachmentIfNeeded already guard on that). Callers
|
|
29
|
+
* must roll back their own local file write if this throws, same as skill/memory create() rolls
|
|
30
|
+
* back its own file on a failed remote push.
|
|
31
|
+
*/
|
|
32
|
+
async pushAttachmentToRemoteIfNeeded(kind, folder, filePath, mimeType) {
|
|
33
|
+
if (kind === 'memory') {
|
|
34
|
+
await this.memoryRepo.pushAttachmentIfNeeded(folder, filePath, mimeType);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
await this.skillRepo.pushAttachmentIfNeeded(folder, filePath, mimeType);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
26
40
|
async add(kind, docIdOrName, filename, data) {
|
|
27
41
|
const doc = await this.getDoc(kind, docIdOrName);
|
|
28
42
|
const dir = attachmentsDirFor(doc.source_path, kind);
|
|
29
43
|
const entry = writeAttachmentFile(dir, filename, data);
|
|
44
|
+
try {
|
|
45
|
+
await this.pushAttachmentToRemoteIfNeeded(kind, doc.folder, path.join(dir, entry.filename), entry.mime_type);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
// Mirrors create()'s rollback: remote push failed after the local mirror file was already
|
|
49
|
+
// written — remove the orphaned local file rather than leaving it registered as if it were
|
|
50
|
+
// already synced remotely.
|
|
51
|
+
fs.rmSync(path.join(dir, entry.filename), { force: true });
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
30
54
|
const existing = doc.attachments ?? [];
|
|
31
55
|
await this.saveAttachmentsList(kind, docIdOrName, [...existing, entry]);
|
|
32
56
|
return entry;
|
|
@@ -44,6 +68,7 @@ export class AttachmentRepository {
|
|
|
44
68
|
const safePath = resolveWithinBase(dir, undefined, filename);
|
|
45
69
|
fs.rmSync(safePath, { force: true });
|
|
46
70
|
const written = writeAttachmentFile(dir, filename, data);
|
|
71
|
+
await this.pushAttachmentToRemoteIfNeeded(kind, doc.folder, path.join(dir, written.filename), written.mime_type);
|
|
47
72
|
// Full replace: preserve position, overwrite metadata for this filename entry.
|
|
48
73
|
const next = (doc.attachments ?? []).map((a) => (a.filename === filename ? written : a));
|
|
49
74
|
await this.saveAttachmentsList(kind, docIdOrName, next);
|
|
@@ -54,6 +79,10 @@ export class AttachmentRepository {
|
|
|
54
79
|
const dir = attachmentsDirFor(doc.source_path, kind);
|
|
55
80
|
const safePath = resolveWithinBase(dir, undefined, filename);
|
|
56
81
|
fs.rmSync(safePath, { force: true });
|
|
82
|
+
// Does NOT delete the corresponding file on folderfoo — there is no delete endpoint in
|
|
83
|
+
// folderfoo-client.ts, matching the same already-flagged gap on skill/memory doc rename/delete
|
|
84
|
+
// (see SkillRepository.rename()/remove() doc comments). A removed attachment leaves a stale
|
|
85
|
+
// copy on the remote source until folderfoo itself gains a delete API.
|
|
57
86
|
const next = (doc.attachments ?? []).filter((a) => a.filename !== filename);
|
|
58
87
|
await this.saveAttachmentsList(kind, docIdOrName, next);
|
|
59
88
|
if (listAttachmentFiles(dir).length === 0) {
|
|
@@ -11,7 +11,7 @@ import { applyBodyEdits } from '../shared/body-edits.js';
|
|
|
11
11
|
import { attachmentsDirFor } from '../attachments/storage.js';
|
|
12
12
|
import { rebaseFolderPath } from '../config.js';
|
|
13
13
|
import { normalizeKey } from '../types.js';
|
|
14
|
-
import { readFile as readRemoteFile, writeFile as writeRemoteFile, joinRemoteFolderPath, assertRemoteFolderExists } from '../remote/folderfoo-client.js';
|
|
14
|
+
import { readFile as readRemoteFile, writeFile as writeRemoteFile, writeBinaryFile as writeRemoteBinaryFile, joinRemoteFolderPath, assertRemoteFolderExists } from '../remote/folderfoo-client.js';
|
|
15
15
|
import { isFolderVisible } from '../remote/identity.js';
|
|
16
16
|
/** Uppercases and strips everything but letters/digits — used to compare keys that differ only in
|
|
17
17
|
* punctuation/whitespace formatting (e.g. `RMXS-15` and `RMXS15` strip to the same `RMXS15`). */
|
|
@@ -73,6 +73,24 @@ export class MemoryRepository {
|
|
|
73
73
|
const identity = this.identity.current();
|
|
74
74
|
return this.remoteFolders.filter((f) => !isFolderVisible(f, identity)).map((f) => f.name);
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Pushes one attachment file's raw binary content to folderfoo, if `folderName` resolves to a
|
|
78
|
+
* remote source — the attachment counterpart to create()/update()'s inline remote-write blocks.
|
|
79
|
+
* Pushed under the attachment's own actual filename (unlike the doc's own .md file, which is
|
|
80
|
+
* pushed under its id with the .md suffix stripped). No-op for a local folder. Called by
|
|
81
|
+
* AttachmentRepository after it writes an attachment to the local mirror — callers must roll
|
|
82
|
+
* back the local write on a thrown error here, exactly like create() already rolls back its own
|
|
83
|
+
* .md file on a failed push.
|
|
84
|
+
*/
|
|
85
|
+
async pushAttachmentIfNeeded(folderName, attachmentFilePath, mimeType) {
|
|
86
|
+
const remote = this.remoteFor(folderName);
|
|
87
|
+
if (!remote || !this.credentialsBaseDir)
|
|
88
|
+
return;
|
|
89
|
+
await assertRemoteFolderExists(remote.server, this.credentialsBaseDir, remote.tenantId, remote.folderPath, folderName);
|
|
90
|
+
const dirRelPath = joinRemoteFolderPath(remote.folderPath, path.relative(remote.mirrorDir, path.dirname(attachmentFilePath)));
|
|
91
|
+
const data = fs.readFileSync(attachmentFilePath);
|
|
92
|
+
await writeRemoteBinaryFile(remote.server, this.credentialsBaseDir, remote.tenantId, dirRelPath, path.basename(attachmentFilePath), data, mimeType);
|
|
93
|
+
}
|
|
76
94
|
/** Attaches the live chokidar watcher so addFolder/removeFolder can mutate it without a restart. */
|
|
77
95
|
setWatcher(watcher) {
|
|
78
96
|
this.watcher = watcher;
|
|
@@ -172,3 +172,25 @@ export async function writeFile(server, baseDir, tenantId, folderPath, name, con
|
|
|
172
172
|
body: content,
|
|
173
173
|
}), async () => undefined);
|
|
174
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Writes one file's raw BINARY content via POST /save/:filename — the attachment-file counterpart
|
|
177
|
+
* to writeFile's markdown-string upload. Same endpoint, same auth/retry wrapper, just a Buffer body
|
|
178
|
+
* and the attachment's own mime type instead of a fixed text/markdown content-type.
|
|
179
|
+
*
|
|
180
|
+
* NOT YET LIVE-VERIFIED against folderfoo: memory doc ids are deliberately stripped of hyphens
|
|
181
|
+
* before ever reaching writeFile's `name` param, per a confirmed-in-production finding that
|
|
182
|
+
* folderfoo's save endpoint silently drops non-[0-9a-zA-Z_] characters from the final filename
|
|
183
|
+
* segment. Attachment filenames routinely contain dots and hyphens (e.g. "entity.java.hbs") that
|
|
184
|
+
* this same stripping would mangle if it applies uniformly here too. This function intentionally
|
|
185
|
+
* does NOT pre-sanitize the name (unlike memory ids) because that behavior hasn't been confirmed
|
|
186
|
+
* for this endpoint/content-type combination from this client alone — verify with a real round-trip
|
|
187
|
+
* (attach a dotted/hyphenated filename to a doc in a remote folder, then check the folderfoo UI
|
|
188
|
+
* shows it unmangled) before relying on this for anything beyond best-effort.
|
|
189
|
+
*/
|
|
190
|
+
export async function writeBinaryFile(server, baseDir, tenantId, folderPath, name, data, mimeType) {
|
|
191
|
+
await withAuth(server, baseDir, (jwt) => fetch(`${server}/save/${filenameParam(folderPath, name)}`, {
|
|
192
|
+
method: 'POST',
|
|
193
|
+
headers: { authorization: `Bearer ${jwt}`, 'x-tenant-id': tenantId, 'content-type': mimeType },
|
|
194
|
+
body: data,
|
|
195
|
+
}), async () => undefined);
|
|
196
|
+
}
|
|
@@ -8,7 +8,7 @@ import { upsertFile, removeFile, scanSingleFolder, unregisterFolder, skillSyncSp
|
|
|
8
8
|
import { SearchQueryError, sanitizeFtsQuery } from '../store/search.js';
|
|
9
9
|
import { applyBodyEdits } from '../shared/body-edits.js';
|
|
10
10
|
import { rebaseFolderPath } from '../config.js';
|
|
11
|
-
import { readFile as readRemoteFile, writeFile as writeRemoteFile, joinRemoteFolderPath, assertRemoteFolderExists } from '../remote/folderfoo-client.js';
|
|
11
|
+
import { readFile as readRemoteFile, writeFile as writeRemoteFile, writeBinaryFile as writeRemoteBinaryFile, joinRemoteFolderPath, assertRemoteFolderExists } from '../remote/folderfoo-client.js';
|
|
12
12
|
import { isFolderVisible } from '../remote/identity.js';
|
|
13
13
|
function rowToDoc(row) {
|
|
14
14
|
return {
|
|
@@ -81,6 +81,24 @@ export class SkillRepository {
|
|
|
81
81
|
const fileContents = fs.readFileSync(filePath, 'utf-8');
|
|
82
82
|
await writeRemoteFile(remote.server, this.credentialsBaseDir, remote.tenantId, skillDirRelPath, 'SKILL', fileContents);
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Pushes one attachment file's raw binary content to folderfoo, if `folderName` resolves to a
|
|
86
|
+
* remote source — the attachment counterpart to pushToRemoteIfNeeded. Unlike SKILL.md (always
|
|
87
|
+
* pushed under the fixed name 'SKILL'), an attachment is pushed under its own actual filename,
|
|
88
|
+
* since folderfoo needs to store each attachment as a distinct file. No-op for a local folder,
|
|
89
|
+
* same as pushToRemoteIfNeeded. Called by AttachmentRepository after it writes an attachment to
|
|
90
|
+
* the local mirror — callers must roll back the local write on a thrown error here, exactly like
|
|
91
|
+
* create()/update() already roll back SKILL.md on a failed push.
|
|
92
|
+
*/
|
|
93
|
+
async pushAttachmentIfNeeded(folderName, attachmentFilePath, mimeType) {
|
|
94
|
+
const remote = this.remoteFor(folderName);
|
|
95
|
+
if (!remote || !this.credentialsBaseDir)
|
|
96
|
+
return;
|
|
97
|
+
await assertRemoteFolderExists(remote.server, this.credentialsBaseDir, remote.tenantId, remote.folderPath, folderName);
|
|
98
|
+
const dirRelPath = joinRemoteFolderPath(remote.folderPath, path.relative(remote.mirrorDir, path.dirname(attachmentFilePath)));
|
|
99
|
+
const data = fs.readFileSync(attachmentFilePath);
|
|
100
|
+
await writeRemoteBinaryFile(remote.server, this.credentialsBaseDir, remote.tenantId, dirRelPath, path.basename(attachmentFilePath), data, mimeType);
|
|
101
|
+
}
|
|
84
102
|
/** Attaches the live chokidar watcher so addFolder/removeFolder can mutate it without a restart. */
|
|
85
103
|
setWatcher(watcher) {
|
|
86
104
|
this.watcher = watcher;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-memory-bucket",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP server exposing skill_* (reusable coding patterns) and memory_* (point-in-time working context) tools over a markdown+frontmatter source, cached into SQLite at runtime.",
|
|
6
6
|
"repository": {
|