mcp-memory-bucket 0.7.4 → 0.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/README.md +20 -0
- package/dist/client/assets/{index-Cr5jNKWL.js → index-BfU_uQyK.js} +397 -262
- package/dist/client/index.html +7 -1
- package/dist/src/attachments/repository.js +22 -22
- package/dist/src/attachments/tools.js +12 -12
- package/dist/src/config.js +70 -3
- package/dist/src/memory/repository.js +142 -39
- package/dist/src/memory/tools.js +32 -13
- package/dist/src/remote/credentials.js +31 -0
- package/dist/src/remote/folderfoo-client.js +149 -0
- package/dist/src/remote/remote-sync.js +176 -0
- package/dist/src/server.js +18 -3
- package/dist/src/shared/body-edits.js +87 -0
- package/dist/src/shared/relocate-tool.js +2 -2
- package/dist/src/shared/relocate.js +9 -5
- package/dist/src/skills/repository.js +141 -47
- package/dist/src/skills/tools.js +34 -15
- package/dist/src/store/sync.js +19 -2
- package/dist/src/web/routes.js +220 -25
- package/package.json +1 -1
package/dist/src/web/routes.js
CHANGED
|
@@ -3,12 +3,15 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import express from 'express';
|
|
5
5
|
import matter from 'gray-matter';
|
|
6
|
-
import { saveFolder, removeFolder as removeFolderFromConfig, sanitizeFolderName } from '../config.js';
|
|
6
|
+
import { saveFolder, saveRemoteFolder, removeFolder as removeFolderFromConfig, sanitizeFolderName, mirrorDirFor } from '../config.js';
|
|
7
7
|
import { stripKey } from '../memory/repository.js';
|
|
8
8
|
import { initialScan } from '../store/sync.js';
|
|
9
9
|
import { sanitizeFtsQuery } from '../store/search.js';
|
|
10
10
|
import { resolveWithinBase } from '../store/safe-path.js';
|
|
11
11
|
import { attachmentsDirFor, guessMimeType } from '../attachments/storage.js';
|
|
12
|
+
import { listFolders as listFolderfooFolders } from '../remote/folderfoo-client.js';
|
|
13
|
+
import { setCredential } from '../remote/credentials.js';
|
|
14
|
+
import { pollOne } from '../remote/remote-sync.js';
|
|
12
15
|
function asArray(v) {
|
|
13
16
|
if (v === undefined)
|
|
14
17
|
return [];
|
|
@@ -268,10 +271,17 @@ function buildHealth(db) {
|
|
|
268
271
|
.map((m) => m.id);
|
|
269
272
|
return { danglingExtends, danglingRelatedTo, emptyTriggerPhrases, staleActiveMemoryDocs };
|
|
270
273
|
}
|
|
271
|
-
export function buildWebRouter(db, config, skillRepo, memoryRepo, skillSpec, memorySpec) {
|
|
274
|
+
export function buildWebRouter(db, config, skillRepo, memoryRepo, skillSpec, memorySpec, remotePollers) {
|
|
272
275
|
const router = express.Router();
|
|
273
|
-
|
|
276
|
+
// Resyncs every remote source FIRST (force: true - always does real work,
|
|
277
|
+
// including reconcileDeletions, regardless of the watermark check), so a
|
|
278
|
+
// deletion made on folderfoo shows up here even if the poller's next
|
|
279
|
+
// regular tick hasn't fired yet. Without this, the wipe-and-rescan below
|
|
280
|
+
// is LOCAL-ONLY - it would silently resurrect a file that was deleted on
|
|
281
|
+
// folderfoo but whose stale mirror copy hadn't been reconciled away yet.
|
|
282
|
+
router.post('/api/rebuild-cache', async (_req, res) => {
|
|
274
283
|
try {
|
|
284
|
+
await Promise.all([remotePollers?.skill?.resyncAll(), remotePollers?.memory?.resyncAll()]);
|
|
275
285
|
db.exec(`DELETE FROM skills; DELETE FROM memory_docs; DELETE FROM search_index; DELETE FROM doc_dates;`);
|
|
276
286
|
initialScan(db, skillSpec);
|
|
277
287
|
initialScan(db, memorySpec);
|
|
@@ -399,7 +409,7 @@ export function buildWebRouter(db, config, skillRepo, memoryRepo, skillSpec, mem
|
|
|
399
409
|
}
|
|
400
410
|
});
|
|
401
411
|
});
|
|
402
|
-
router.patch('/api/entries/:table/:id/deprecated', (req, res) => {
|
|
412
|
+
router.patch('/api/entries/:table/:id/deprecated', async (req, res) => {
|
|
403
413
|
const { table, id } = req.params;
|
|
404
414
|
const { deprecated } = req.body;
|
|
405
415
|
if (table !== 'skills' && table !== 'memory_docs') {
|
|
@@ -416,16 +426,16 @@ export function buildWebRouter(db, config, skillRepo, memoryRepo, skillSpec, mem
|
|
|
416
426
|
}
|
|
417
427
|
try {
|
|
418
428
|
if (table === 'skills')
|
|
419
|
-
skillRepo.update(id, { deprecated });
|
|
429
|
+
await skillRepo.update(id, { deprecated });
|
|
420
430
|
else
|
|
421
|
-
memoryRepo.update(id, { deprecated });
|
|
431
|
+
await memoryRepo.update(id, { deprecated });
|
|
422
432
|
res.json({ id, deprecated });
|
|
423
433
|
}
|
|
424
434
|
catch (err) {
|
|
425
435
|
res.status(404).json({ error: err.message });
|
|
426
436
|
}
|
|
427
437
|
});
|
|
428
|
-
router.post('/api/entries/:table/bulk/deprecated', (req, res) => {
|
|
438
|
+
router.post('/api/entries/:table/bulk/deprecated', async (req, res) => {
|
|
429
439
|
const { table } = req.params;
|
|
430
440
|
const { ids, deprecated } = req.body;
|
|
431
441
|
if (table !== 'skills' && table !== 'memory_docs') {
|
|
@@ -436,7 +446,7 @@ export function buildWebRouter(db, config, skillRepo, memoryRepo, skillSpec, mem
|
|
|
436
446
|
res.status(400).json({ error: 'body must be { ids: string[], deprecated: boolean }' });
|
|
437
447
|
return;
|
|
438
448
|
}
|
|
439
|
-
const results = table === 'skills' ? skillRepo.bulkUpdate(ids, { deprecated }) : memoryRepo.bulkUpdate(ids, { deprecated });
|
|
449
|
+
const results = table === 'skills' ? await skillRepo.bulkUpdate(ids, { deprecated }) : await memoryRepo.bulkUpdate(ids, { deprecated });
|
|
440
450
|
res.json({ results });
|
|
441
451
|
});
|
|
442
452
|
// General frontmatter edit — covers both "edit an existing doc's fields" and "add frontmatter
|
|
@@ -444,7 +454,7 @@ export function buildWebRouter(db, config, skillRepo, memoryRepo, skillSpec, mem
|
|
|
444
454
|
// "add" is just this same call supplying real values). Memory `key` is editable here (update()
|
|
445
455
|
// normalizes it). Skill `name` is not — SkillRepository.update() rejects it outright since
|
|
446
456
|
// renaming requires a real folder move (see the rename route below).
|
|
447
|
-
router.patch('/api/entries/:table/:id', (req, res) => {
|
|
457
|
+
router.patch('/api/entries/:table/:id', async (req, res) => {
|
|
448
458
|
const { table, id } = req.params;
|
|
449
459
|
const { frontmatter } = req.body;
|
|
450
460
|
if (table !== 'skills' && table !== 'memory_docs') {
|
|
@@ -464,14 +474,14 @@ export function buildWebRouter(db, config, skillRepo, memoryRepo, skillSpec, mem
|
|
|
464
474
|
return;
|
|
465
475
|
}
|
|
466
476
|
try {
|
|
467
|
-
const updated = table === 'skills' ? skillRepo.update(id, frontmatter) : memoryRepo.update(id, frontmatter);
|
|
477
|
+
const updated = table === 'skills' ? await skillRepo.update(id, frontmatter) : await memoryRepo.update(id, frontmatter);
|
|
468
478
|
res.json(updated);
|
|
469
479
|
}
|
|
470
480
|
catch (err) {
|
|
471
481
|
res.status(404).json({ error: err.message });
|
|
472
482
|
}
|
|
473
483
|
});
|
|
474
|
-
router.post('/api/entries/skills/:name/rename', (req, res) => {
|
|
484
|
+
router.post('/api/entries/skills/:name/rename', async (req, res) => {
|
|
475
485
|
const { name } = req.params;
|
|
476
486
|
const { new_name } = req.body;
|
|
477
487
|
if (!name) {
|
|
@@ -483,7 +493,7 @@ export function buildWebRouter(db, config, skillRepo, memoryRepo, skillSpec, mem
|
|
|
483
493
|
return;
|
|
484
494
|
}
|
|
485
495
|
try {
|
|
486
|
-
const renamed = skillRepo.rename(name, new_name);
|
|
496
|
+
const renamed = await skillRepo.rename(name, new_name);
|
|
487
497
|
res.json(renamed);
|
|
488
498
|
}
|
|
489
499
|
catch (err) {
|
|
@@ -492,14 +502,14 @@ export function buildWebRouter(db, config, skillRepo, memoryRepo, skillSpec, mem
|
|
|
492
502
|
});
|
|
493
503
|
// Memory docs only — skill frontmatter (name/description) is required by the agentskills.io
|
|
494
504
|
// spec, so stripping it would produce a non-conformant SKILL.md no compliant agent can load.
|
|
495
|
-
router.delete('/api/entries/memory_docs/:id/frontmatter', (req, res) => {
|
|
505
|
+
router.delete('/api/entries/memory_docs/:id/frontmatter', async (req, res) => {
|
|
496
506
|
const { id } = req.params;
|
|
497
507
|
if (!id) {
|
|
498
508
|
res.status(400).json({ error: 'id is required' });
|
|
499
509
|
return;
|
|
500
510
|
}
|
|
501
511
|
try {
|
|
502
|
-
memoryRepo.stripFrontmatter(id);
|
|
512
|
+
await memoryRepo.stripFrontmatter(id);
|
|
503
513
|
res.json({ id, frontmatterRemoved: true });
|
|
504
514
|
}
|
|
505
515
|
catch (err) {
|
|
@@ -508,7 +518,7 @@ export function buildWebRouter(db, config, skillRepo, memoryRepo, skillSpec, mem
|
|
|
508
518
|
});
|
|
509
519
|
// `paused` is a local-only cache toggle (see SkillRepository/MemoryRepository#setPaused) — it
|
|
510
520
|
// never touches the source file, so this goes through setPaused, not update()/bulkUpdate().
|
|
511
|
-
router.patch('/api/entries/:table/:id/paused', (req, res) => {
|
|
521
|
+
router.patch('/api/entries/:table/:id/paused', async (req, res) => {
|
|
512
522
|
const { table, id } = req.params;
|
|
513
523
|
const { paused } = req.body;
|
|
514
524
|
if (table !== 'skills' && table !== 'memory_docs') {
|
|
@@ -523,14 +533,14 @@ export function buildWebRouter(db, config, skillRepo, memoryRepo, skillSpec, mem
|
|
|
523
533
|
res.status(400).json({ error: 'body must be { paused: boolean }' });
|
|
524
534
|
return;
|
|
525
535
|
}
|
|
526
|
-
const [result] = table === 'skills' ? skillRepo.setPaused([id], paused) : memoryRepo.setPaused([id], paused);
|
|
536
|
+
const [result] = table === 'skills' ? await skillRepo.setPaused([id], paused) : await memoryRepo.setPaused([id], paused);
|
|
527
537
|
if (!result?.ok) {
|
|
528
538
|
res.status(404).json({ error: result?.error ?? 'not found' });
|
|
529
539
|
return;
|
|
530
540
|
}
|
|
531
541
|
res.json({ id, paused });
|
|
532
542
|
});
|
|
533
|
-
router.post('/api/entries/:table/bulk/paused', (req, res) => {
|
|
543
|
+
router.post('/api/entries/:table/bulk/paused', async (req, res) => {
|
|
534
544
|
const { table } = req.params;
|
|
535
545
|
const { ids, paused } = req.body;
|
|
536
546
|
if (table !== 'skills' && table !== 'memory_docs') {
|
|
@@ -541,10 +551,10 @@ export function buildWebRouter(db, config, skillRepo, memoryRepo, skillSpec, mem
|
|
|
541
551
|
res.status(400).json({ error: 'body must be { ids: string[], paused: boolean }' });
|
|
542
552
|
return;
|
|
543
553
|
}
|
|
544
|
-
const results = table === 'skills' ? skillRepo.setPaused(ids, paused) : memoryRepo.setPaused(ids, paused);
|
|
554
|
+
const results = table === 'skills' ? await skillRepo.setPaused(ids, paused) : await memoryRepo.setPaused(ids, paused);
|
|
545
555
|
res.json({ results });
|
|
546
556
|
});
|
|
547
|
-
router.delete('/api/entries/:table/:id', (req, res) => {
|
|
557
|
+
router.delete('/api/entries/:table/:id', async (req, res) => {
|
|
548
558
|
const { table, id } = req.params;
|
|
549
559
|
if (table !== 'skills' && table !== 'memory_docs') {
|
|
550
560
|
res.status(400).json({ error: 'table must be "skills" or "memory_docs"' });
|
|
@@ -556,16 +566,16 @@ export function buildWebRouter(db, config, skillRepo, memoryRepo, skillSpec, mem
|
|
|
556
566
|
}
|
|
557
567
|
try {
|
|
558
568
|
if (table === 'skills')
|
|
559
|
-
skillRepo.delete(id);
|
|
569
|
+
await skillRepo.delete(id);
|
|
560
570
|
else
|
|
561
|
-
memoryRepo.delete(id);
|
|
571
|
+
await memoryRepo.delete(id);
|
|
562
572
|
res.json({ deleted: id });
|
|
563
573
|
}
|
|
564
574
|
catch (err) {
|
|
565
575
|
res.status(404).json({ error: err.message });
|
|
566
576
|
}
|
|
567
577
|
});
|
|
568
|
-
router.post('/api/entries/:table/bulk/delete', (req, res) => {
|
|
578
|
+
router.post('/api/entries/:table/bulk/delete', async (req, res) => {
|
|
569
579
|
const { table } = req.params;
|
|
570
580
|
const { ids } = req.body;
|
|
571
581
|
if (table !== 'skills' && table !== 'memory_docs') {
|
|
@@ -576,7 +586,7 @@ export function buildWebRouter(db, config, skillRepo, memoryRepo, skillSpec, mem
|
|
|
576
586
|
res.status(400).json({ error: 'body must be { ids: string[] }' });
|
|
577
587
|
return;
|
|
578
588
|
}
|
|
579
|
-
const results = table === 'skills' ? skillRepo.bulkDelete(ids) : memoryRepo.bulkDelete(ids);
|
|
589
|
+
const results = table === 'skills' ? await skillRepo.bulkDelete(ids) : await memoryRepo.bulkDelete(ids);
|
|
580
590
|
res.json({ results });
|
|
581
591
|
});
|
|
582
592
|
router.get('/api/facets', (req, res) => {
|
|
@@ -594,10 +604,80 @@ export function buildWebRouter(db, config, skillRepo, memoryRepo, skillSpec, mem
|
|
|
594
604
|
router.get('/api/health', (_req, res) => {
|
|
595
605
|
res.json(buildHealth(db));
|
|
596
606
|
});
|
|
607
|
+
// Tells the client which folderfoo deployment (if any) to point the
|
|
608
|
+
// "Connect folderfoo" flow and the page-level login widget at — see
|
|
609
|
+
// config.ts's FolderfooMode doc comment for why this can't be inferred
|
|
610
|
+
// from window.location.hostname the way mindfoo/bulletino/avotuner do.
|
|
611
|
+
router.get('/api/config', (_req, res) => {
|
|
612
|
+
res.json({ folderfooMode: config.folderfooMode, folderfooHost: config.folderfooHost });
|
|
613
|
+
});
|
|
597
614
|
router.get('/api/folders', (_req, res) => {
|
|
598
615
|
res.json({
|
|
599
|
-
skill: skillRepo.
|
|
600
|
-
memory: memoryRepo.
|
|
616
|
+
skill: skillRepo.listFoldersWithRemoteInfo().map((f) => ({ ...f, kind: 'skill' })),
|
|
617
|
+
memory: memoryRepo.listFoldersWithRemoteInfo().map((f) => ({ ...f, kind: 'memory' })),
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
// Resolves a file opened via folderfoo's own File Open dialog
|
|
621
|
+
// (<folderfoo-profile-circle>'s "folderfoo-file-open" event) back to a
|
|
622
|
+
// memory-bucket doc, so the web UI can select it in the right panel —
|
|
623
|
+
// and, since that panel's edits already push through to folderfoo for
|
|
624
|
+
// any doc whose folder resolves to a remote source (see PATCH
|
|
625
|
+
// /api/entries/:table/:id), frontmatter changes made there persist to
|
|
626
|
+
// the cloud automatically once the right doc is open, no separate wiring
|
|
627
|
+
// needed for that half.
|
|
628
|
+
//
|
|
629
|
+
// Only works when the opened file's (server, tenantId, folderPath) falls
|
|
630
|
+
// under a folder ALREADY connected as a remote source here — folderfoo's
|
|
631
|
+
// File Open dialog browses the user's whole folderfoo tree, not just
|
|
632
|
+
// connected folders, so a miss (file from an unconnected folder) is a
|
|
633
|
+
// real, expected case the client surfaces as a clear message, not
|
|
634
|
+
// silently swallowed.
|
|
635
|
+
router.post('/api/folderfoo/resolve-open', (req, res) => {
|
|
636
|
+
const { server, tenantId, folderPath, name } = req.body;
|
|
637
|
+
if (!server || !tenantId || !name) {
|
|
638
|
+
res.status(400).json({ error: 'server, tenantId, and name are required' });
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const openedFolderPath = folderPath || '';
|
|
642
|
+
for (const [table, repo] of [
|
|
643
|
+
['skills', skillRepo],
|
|
644
|
+
['memory_docs', memoryRepo],
|
|
645
|
+
]) {
|
|
646
|
+
for (const remote of repo.listRemoteFolders()) {
|
|
647
|
+
if (remote.server !== server || remote.tenantId !== tenantId)
|
|
648
|
+
continue;
|
|
649
|
+
// The opened file's folder must be remote.folderPath itself, or nested inside it.
|
|
650
|
+
const sourceFolder = remote.folderPath;
|
|
651
|
+
let withinFolder;
|
|
652
|
+
if (openedFolderPath === sourceFolder) {
|
|
653
|
+
withinFolder = '';
|
|
654
|
+
}
|
|
655
|
+
else if (sourceFolder === '') {
|
|
656
|
+
withinFolder = openedFolderPath;
|
|
657
|
+
}
|
|
658
|
+
else if (openedFolderPath.startsWith(`${sourceFolder}/`)) {
|
|
659
|
+
withinFolder = openedFolderPath.slice(sourceFolder.length + 1);
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
continue; // not under this source
|
|
663
|
+
}
|
|
664
|
+
const sourcePath = table === 'skills'
|
|
665
|
+
? path.join(remote.mirrorDir, withinFolder, name, 'SKILL.md')
|
|
666
|
+
: path.join(remote.mirrorDir, withinFolder, `${name}.md`);
|
|
667
|
+
const row = db.prepare(`SELECT id FROM ${table} WHERE source_path = ?`).get(sourcePath);
|
|
668
|
+
if (row) {
|
|
669
|
+
res.json({ table, id: row.id });
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
// Matched the connected source but no cache row yet (e.g. the
|
|
673
|
+
// poller hasn't picked it up since it was just created elsewhere) —
|
|
674
|
+
// a different, more specific miss than "not connected at all".
|
|
675
|
+
res.status(404).json({ error: `matched remote source "${remote.name}", but no cached doc found yet for this file — try Resync` });
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
res.status(404).json({
|
|
680
|
+
error: `this file's folder isn't connected in memory-bucket yet — add it as a remote source first`,
|
|
601
681
|
});
|
|
602
682
|
});
|
|
603
683
|
router.post('/api/folders', (req, res) => {
|
|
@@ -629,6 +709,121 @@ export function buildWebRouter(db, config, skillRepo, memoryRepo, skillSpec, mem
|
|
|
629
709
|
res.status(409).json({ error: err.message });
|
|
630
710
|
}
|
|
631
711
|
});
|
|
712
|
+
// Persists a folderfoo JWT the browser already obtained (see
|
|
713
|
+
// credentials.ts — keyed by server URL, so every remote source pointing
|
|
714
|
+
// at the same folderfoo deployment shares this one login). The actual
|
|
715
|
+
// login UI is folderfoo's own <folderfoo-profile-circle> widget,
|
|
716
|
+
// dynamically imported into the client page exactly like every other
|
|
717
|
+
// folderfoo-consuming app (bulletino, mindfoo) — that widget stores its
|
|
718
|
+
// token in THIS page's localStorage (browser-only), so the client reads
|
|
719
|
+
// it back out and POSTs it here to also persist it server-side, where
|
|
720
|
+
// the Node process (poller, live reads/writes) actually needs it. This
|
|
721
|
+
// avoids a second username/password prompt: one login, reused for both
|
|
722
|
+
// the browser tab and the backend.
|
|
723
|
+
router.post('/api/folderfoo/login', async (req, res) => {
|
|
724
|
+
const { server, token } = req.body;
|
|
725
|
+
if (!server || !token) {
|
|
726
|
+
res.status(400).json({ error: 'server and token are required' });
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
setCredential(config.baseDir, server, token);
|
|
730
|
+
res.json({ connected: true, server });
|
|
731
|
+
});
|
|
732
|
+
// Lists the caller's own folderfoo folders for the "connect a folder"
|
|
733
|
+
// picker, once already logged in to `server` (via the route above).
|
|
734
|
+
router.get('/api/folderfoo/folders', async (req, res) => {
|
|
735
|
+
const server = req.query.server;
|
|
736
|
+
const tenantId = req.query.tenantId;
|
|
737
|
+
if (!server || !tenantId) {
|
|
738
|
+
res.status(400).json({ error: 'server and tenantId query params are required' });
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
try {
|
|
742
|
+
const folders = await listFolderfooFolders(server, config.baseDir, tenantId);
|
|
743
|
+
res.json(folders);
|
|
744
|
+
}
|
|
745
|
+
catch (err) {
|
|
746
|
+
res.status(401).json({ error: err.message });
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
// Registers a REMOTE (folderfoo) source — the connect-a-folder counterpart
|
|
750
|
+
// to POST /api/folders above, which only ever handles local filesystem
|
|
751
|
+
// paths. Creates the local mirror directory, registers it with the
|
|
752
|
+
// repository (so it starts watching/scanning like any local folder), then
|
|
753
|
+
// immediately triggers one poll so content shows up without waiting for
|
|
754
|
+
// the first interval tick.
|
|
755
|
+
router.post('/api/remote-folders', async (req, res) => {
|
|
756
|
+
const { kind, name, server, tenantId, folderPath } = req.body;
|
|
757
|
+
if (kind !== 'skill' && kind !== 'memory') {
|
|
758
|
+
res.status(400).json({ error: 'kind must be "skill" or "memory"' });
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (!server || !tenantId || folderPath === undefined) {
|
|
762
|
+
res.status(400).json({ error: 'server, tenantId, and folderPath are required' });
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
const folderName = sanitizeFolderName(name || folderPath.split('/').filter(Boolean).pop() || tenantId);
|
|
766
|
+
if (!folderName) {
|
|
767
|
+
res.status(400).json({ error: 'could not derive a valid folder name — provide one explicitly' });
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
const repo = kind === 'skill' ? skillRepo : memoryRepo;
|
|
771
|
+
const spec = kind === 'skill' ? skillSpec : memorySpec;
|
|
772
|
+
const mirrorDir = mirrorDirFor(config.baseDir, folderName);
|
|
773
|
+
const remote = { name: folderName, server, tenantId, folderPath, mirrorDir };
|
|
774
|
+
try {
|
|
775
|
+
repo.registerRemoteFolder(remote);
|
|
776
|
+
saveRemoteFolder(config, kind, { name: folderName, server, tenantId, folderPath });
|
|
777
|
+
await pollOne(db, spec, remote, config.baseDir);
|
|
778
|
+
res.json({ name: folderName, server, tenantId, folderPath, kind });
|
|
779
|
+
}
|
|
780
|
+
catch (err) {
|
|
781
|
+
res.status(409).json({ error: err.message });
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
// Triggers one immediate out-of-cycle poll for a single remote source —
|
|
785
|
+
// backs the web UI's per-source "resync now" action, so a user who just
|
|
786
|
+
// saved something remotely doesn't have to wait out the fixed poll
|
|
787
|
+
// interval to see it reflected locally.
|
|
788
|
+
router.post('/api/remote-folders/:kind/:name/resync', async (req, res) => {
|
|
789
|
+
const { kind, name } = req.params;
|
|
790
|
+
if (kind !== 'skill' && kind !== 'memory') {
|
|
791
|
+
res.status(400).json({ error: 'kind must be "skill" or "memory"' });
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
if (!name) {
|
|
795
|
+
res.status(400).json({ error: 'name is required' });
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
const handle = kind === 'skill' ? remotePollers?.skill : remotePollers?.memory;
|
|
799
|
+
if (!handle) {
|
|
800
|
+
res.status(404).json({ error: `no remote ${kind} sources configured` });
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
try {
|
|
804
|
+
await handle.resyncNow(name);
|
|
805
|
+
res.json({ resynced: name, kind });
|
|
806
|
+
}
|
|
807
|
+
catch (err) {
|
|
808
|
+
res.status(404).json({ error: err.message });
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
// Lightweight "check every remote source for changes right now" — unlike
|
|
812
|
+
// rebuild-cache (which wipes the WHOLE local cache and re-scans
|
|
813
|
+
// everything, including local-only folders), this only force-resyncs the
|
|
814
|
+
// remote sources' own incremental pull+reconcile, so it's cheap enough to
|
|
815
|
+
// call on every tab focus without the user needing to hit rebuild-cache
|
|
816
|
+
// by hand just to see a remote change (e.g. a deletion) sooner than the
|
|
817
|
+
// fixed poll interval.
|
|
818
|
+
router.post('/api/remote-folders/resync-all', async (_req, res) => {
|
|
819
|
+
try {
|
|
820
|
+
await Promise.all([remotePollers?.skill?.resyncAll(), remotePollers?.memory?.resyncAll()]);
|
|
821
|
+
res.json({ resynced: true });
|
|
822
|
+
}
|
|
823
|
+
catch (err) {
|
|
824
|
+
res.status(500).json({ error: err.message });
|
|
825
|
+
}
|
|
826
|
+
});
|
|
632
827
|
router.delete('/api/folders/:kind/:name', (req, res) => {
|
|
633
828
|
const { kind, name } = req.params;
|
|
634
829
|
if (kind !== 'skill' && kind !== 'memory') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-memory-bucket",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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": {
|