@coffer-org/server 1.13.0 → 2.0.1
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/counts.d.ts +6 -0
- package/dist/counts.js +31 -0
- package/dist/db.js +2 -0
- package/dist/embeddings.d.ts +4 -4
- package/dist/embeddings.js +6 -6
- package/dist/entity-schema.js +1 -1
- package/dist/file-fields.d.ts +6 -0
- package/dist/file-fields.js +121 -0
- package/dist/frontend-agent.d.ts +1 -0
- package/dist/frontend-agent.js +37 -0
- package/dist/global-search.d.ts +16 -0
- package/dist/global-search.js +87 -0
- package/dist/index-signal.d.ts +3 -2
- package/dist/index-signal.js +22 -8
- package/dist/index.js +78 -103
- package/dist/local-api.d.ts +5 -5
- package/dist/local-api.js +9 -9
- package/dist/mcp-http.js +16 -1
- package/dist/mcp-local.d.ts +5 -5
- package/dist/mcp-local.js +12 -12
- package/dist/mcp-tools.js +21 -10
- package/dist/migrations.d.ts +1 -0
- package/dist/migrations.js +72 -2
- package/dist/mutate.d.ts +1 -0
- package/dist/mutate.js +18 -2
- package/dist/plugin-hooks.d.ts +15 -1
- package/dist/plugin-runtime.d.ts +1 -0
- package/dist/plugin-runtime.js +24 -1
- package/dist/plugin-user-api.d.ts +4 -0
- package/dist/plugin-user-api.js +92 -0
- package/dist/public-url.d.ts +1 -0
- package/dist/public-url.js +4 -1
- package/dist/records-api.d.ts +16 -6
- package/dist/records-api.js +76 -32
- package/dist/search-index.d.ts +10 -0
- package/dist/search-index.js +100 -0
- package/dist/search-indexer.d.ts +4 -0
- package/dist/search-indexer.js +88 -0
- package/dist/thread-store.d.ts +10 -2
- package/dist/thread-store.js +40 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import { createWriteStream, existsSync } from 'node:fs';
|
|
2
|
+
import { createWriteStream, existsSync, statSync } from 'node:fs';
|
|
3
3
|
import { realpath } from 'node:fs/promises';
|
|
4
4
|
import { pipeline } from 'node:stream/promises';
|
|
5
5
|
import { extname, join, sep } from 'node:path';
|
|
@@ -8,17 +8,15 @@ import Fastify from 'fastify';
|
|
|
8
8
|
import cors from '@fastify/cors';
|
|
9
9
|
import multipart from '@fastify/multipart';
|
|
10
10
|
import fastifyStatic from '@fastify/static';
|
|
11
|
-
import { serialize } from '@mikro-orm/core';
|
|
12
|
-
import { textSearchKeys, recordTitle, titleKey, storageColumnsFor } from '@coffer-org/sdk/shelf';
|
|
13
|
-
import { tokenize } from '@coffer-org/core/search';
|
|
14
11
|
import { shelfTableName } from "./entity-schema.js";
|
|
15
12
|
import { getEm, closeDb } from "./db.js";
|
|
13
|
+
import { recordCounts } from "./counts.js";
|
|
16
14
|
import { ValidationError, NotFoundError } from "./mutate.js";
|
|
17
15
|
import { resolveEmbed } from "./embed.js";
|
|
18
16
|
import { resolveWithinHome, filterSort, listDir } from "./fs-list.js";
|
|
19
17
|
import { initPlugins, teardownPlugins, readDisabled, purgePluginData, getPluginSettings, getPlugins, } from "./plugin-runtime.js";
|
|
20
18
|
import { registerPluginsApi } from "./plugins-api.js";
|
|
21
|
-
import {
|
|
19
|
+
import { registerPluginUserApi, registerPluginAdminApi } from "./plugin-user-api.js";
|
|
22
20
|
import { registerAuthApi, resolveRequestUser, requireAdmin, PUBLIC_API_PATHS } from "./auth-api.js";
|
|
23
21
|
import { registerMcpHttp } from "./mcp-http.js";
|
|
24
22
|
import { registerOAuthApi } from "./oauth-api.js";
|
|
@@ -26,12 +24,16 @@ import { baseUrl } from "./public-url.js";
|
|
|
26
24
|
import { discoverPluginAssets, discoverRuntime } from "./plugin-discovery.js";
|
|
27
25
|
import { checkLatestVersion, resolveUpdateTarget, resolveAllUpdateTargets, resolveBaseTargets, resolveRuntimeTarget, runNpmInstall, } from "./plugin-updates.js";
|
|
28
26
|
import { buildClientSchema } from "./schema-api.js";
|
|
29
|
-
import { recordList, recordGet, recordCreate, recordUpdate, recordDelete,
|
|
27
|
+
import { recordList, recordListPage, isPagedRequest, recordGet, recordCreate, recordUpdate, recordDelete, UnknownShelfError, } from "./records-api.js";
|
|
30
28
|
import { maskTree, preserveTree } from "./field-masking.js";
|
|
31
29
|
import { writePluginSettings } from "./settings-write.js";
|
|
32
30
|
import { rootLogger, getLogger } from "./log.js";
|
|
33
31
|
import { uploadsDir } from "./uploads.js";
|
|
32
|
+
import { mimeForName } from "./file-fields.js";
|
|
34
33
|
import { verifyUploadTicket } from "./upload-ticket.js";
|
|
34
|
+
import { setPluginState } from "./plugin-state.js";
|
|
35
|
+
import { SEARCH_STATE_PLUGIN, SEARCH_CURSOR_KEY } from "./search-indexer.js";
|
|
36
|
+
import { globalSearch } from "./global-search.js";
|
|
35
37
|
const ENV_FILE = join(process.cwd(), '.env');
|
|
36
38
|
if (existsSync(ENV_FILE))
|
|
37
39
|
process.loadEnvFile(ENV_FILE);
|
|
@@ -96,8 +98,11 @@ app.post('/api/upload', async (req, reply) => {
|
|
|
96
98
|
return reply.code(400).send({ error: 'no_file' });
|
|
97
99
|
const ext = extname(data.filename).toLowerCase();
|
|
98
100
|
const name = `${randomUUID()}${ext}`;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
const dest = join(UPLOADS, name);
|
|
102
|
+
await pipeline(data.file, createWriteStream(dest));
|
|
103
|
+
const size = statSync(dest).size;
|
|
104
|
+
const mime = mimeForName(name) ?? (data.mimetype || undefined);
|
|
105
|
+
return reply.send({ filename: name, name, ...(mime ? { mime } : {}), size });
|
|
101
106
|
});
|
|
102
107
|
app.post('/api/embed', async (req, reply) => {
|
|
103
108
|
const url = req.body?.url;
|
|
@@ -108,8 +113,8 @@ app.post('/api/embed', async (req, reply) => {
|
|
|
108
113
|
return reply.code(422).send({ error: 'unresolved' });
|
|
109
114
|
return reply.send(rec);
|
|
110
115
|
});
|
|
111
|
-
function key(library,
|
|
112
|
-
return `${library}/${
|
|
116
|
+
function key(library, shelf) {
|
|
117
|
+
return `${library}/${shelf}`;
|
|
113
118
|
}
|
|
114
119
|
async function guard(reply, fn) {
|
|
115
120
|
try {
|
|
@@ -146,55 +151,30 @@ app.get('/api/libraries', async (req, reply) => {
|
|
|
146
151
|
return reply.type('application/json').send(librariesPayload.body);
|
|
147
152
|
});
|
|
148
153
|
app.get('/api/counts', async () => {
|
|
149
|
-
const
|
|
150
|
-
const out = {};
|
|
154
|
+
const targets = [];
|
|
151
155
|
for (const [k, mdef] of shelfDefs) {
|
|
152
156
|
if (mdef.standalone === false)
|
|
153
157
|
continue;
|
|
154
158
|
const ename = entityNames.get(k);
|
|
155
159
|
if (!ename)
|
|
156
160
|
continue;
|
|
157
|
-
|
|
161
|
+
targets.push({ key: k, table: ename });
|
|
158
162
|
}
|
|
159
|
-
return
|
|
163
|
+
return recordCounts(targets);
|
|
160
164
|
});
|
|
161
|
-
const SEARCH_SCAN_LIMIT = 1000;
|
|
162
|
-
const searchLog = getLogger('search');
|
|
163
165
|
app.get('/api/search', async (req) => {
|
|
164
166
|
const { q = '', limit = '20' } = req.query;
|
|
165
|
-
|
|
166
|
-
if (tokens.length === 0 || q.trim().length < 2)
|
|
167
|
+
if (q.trim().length < 2)
|
|
167
168
|
return [];
|
|
168
169
|
const lim = Math.min(Number(limit) || 20, 100);
|
|
169
|
-
const
|
|
170
|
-
const scored = [];
|
|
170
|
+
const shelves = [];
|
|
171
171
|
for (const [k, mdef] of shelfDefs) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const ename = entityNames.get(k);
|
|
175
|
-
if (!ename)
|
|
176
|
-
continue;
|
|
177
|
-
if (!textSearchKeys(mdef).length)
|
|
172
|
+
const table = entityNames.get(k);
|
|
173
|
+
if (!table)
|
|
178
174
|
continue;
|
|
179
|
-
|
|
180
|
-
const cols = storageColumnsFor(mdef, [...textSearchKeys(mdef), titleKey(mdef)]);
|
|
181
|
-
const rows = (await fork.find(ename, {}, { limit: SEARCH_SCAN_LIMIT, fields: cols })).map((r) => serialize(r));
|
|
182
|
-
if (rows.length === SEARCH_SCAN_LIMIT) {
|
|
183
|
-
searchLog.warn('scan cap reached', { shelf: k, limit: SEARCH_SCAN_LIMIT });
|
|
184
|
-
}
|
|
185
|
-
for (const row of rows) {
|
|
186
|
-
const m = rowMatch(mdef, row, tokens);
|
|
187
|
-
if (!m)
|
|
188
|
-
continue;
|
|
189
|
-
const label = recordTitle(mdef, row);
|
|
190
|
-
scored.push({
|
|
191
|
-
score: m.score,
|
|
192
|
-
result: { library, type, id: String(row.id), label, snippet: m.snippet },
|
|
193
|
-
});
|
|
194
|
-
}
|
|
175
|
+
shelves.push({ key: k, table, def: mdef });
|
|
195
176
|
}
|
|
196
|
-
|
|
197
|
-
return scored.slice(0, lim).map((s) => s.result);
|
|
177
|
+
return globalSearch(shelves, q, lim);
|
|
198
178
|
});
|
|
199
179
|
app.patch('/api/plugins/:id', async (req, reply) => {
|
|
200
180
|
if (!requireAdmin(req, reply))
|
|
@@ -228,6 +208,9 @@ app.patch('/api/plugins/:id', async (req, reply) => {
|
|
|
228
208
|
fork.create('_Plugin', { id, enabled: enabled ? 1 : 0, updated_at: new Date().toISOString() });
|
|
229
209
|
}
|
|
230
210
|
await fork.flush();
|
|
211
|
+
if (enabled) {
|
|
212
|
+
await setPluginState(SEARCH_STATE_PLUGIN, SEARCH_CURSOR_KEY, '0');
|
|
213
|
+
}
|
|
231
214
|
return { ok: true, restartRequired: true };
|
|
232
215
|
});
|
|
233
216
|
app.delete('/api/plugins/:id/data', async (req, reply) => {
|
|
@@ -340,26 +323,8 @@ app.put('/api/plugins/:id/settings', (req, reply) => {
|
|
|
340
323
|
return { ok: true, row };
|
|
341
324
|
});
|
|
342
325
|
});
|
|
343
|
-
app
|
|
344
|
-
|
|
345
|
-
return;
|
|
346
|
-
const { id, action } = req.params;
|
|
347
|
-
const fn = pluginHooks[id]?.actions?.[action];
|
|
348
|
-
if (!fn)
|
|
349
|
-
return reply.code(404).send({ error: `unknown action ${id}/${action}` });
|
|
350
|
-
try {
|
|
351
|
-
return await fn((req.body ?? {}));
|
|
352
|
-
}
|
|
353
|
-
catch (e) {
|
|
354
|
-
if (e instanceof ValidationError) {
|
|
355
|
-
const detail = e.issues.map((i) => `${i.path.join('.') || i.field}: ${i.code}`).join('; ');
|
|
356
|
-
return reply.code(422).send({ error: `Validation error — ${detail}`, issues: e.issues });
|
|
357
|
-
}
|
|
358
|
-
if (e instanceof HttpError)
|
|
359
|
-
return reply.code(e.status).send({ error: e.message });
|
|
360
|
-
return reply.code(400).send({ error: e.message });
|
|
361
|
-
}
|
|
362
|
-
});
|
|
326
|
+
registerPluginAdminApi(app, requireAdmin);
|
|
327
|
+
registerPluginUserApi(app);
|
|
363
328
|
app.get('/api/fs/list', async (req, reply) => {
|
|
364
329
|
const home = homedir();
|
|
365
330
|
const q = req.query;
|
|
@@ -383,12 +348,12 @@ app.get('/api/fs/list', async (req, reply) => {
|
|
|
383
348
|
return { dir: abs, entries: [] };
|
|
384
349
|
}
|
|
385
350
|
});
|
|
386
|
-
function maskRecordRow(library,
|
|
387
|
-
const m = getShelf(library,
|
|
351
|
+
function maskRecordRow(library, shelf, row) {
|
|
352
|
+
const m = getShelf(library, shelf);
|
|
388
353
|
let out = m ? maskTree(m.fields, row) : row;
|
|
389
354
|
const ext = out._extends;
|
|
390
355
|
if (ext) {
|
|
391
|
-
const defs = getExtendsFor(library,
|
|
356
|
+
const defs = getExtendsFor(library, shelf);
|
|
392
357
|
const maskedExt = {};
|
|
393
358
|
for (const [id, sub] of Object.entries(ext)) {
|
|
394
359
|
const def = defs.find((e) => e.id === id);
|
|
@@ -398,67 +363,77 @@ function maskRecordRow(library, type, row) {
|
|
|
398
363
|
}
|
|
399
364
|
return out;
|
|
400
365
|
}
|
|
401
|
-
app.get('/api/:library/:
|
|
402
|
-
const { library,
|
|
403
|
-
const { _fields, _extends, ...query } = req.query;
|
|
366
|
+
app.get('/api/:library/:shelf', async (req, reply) => {
|
|
367
|
+
const { library, shelf } = req.params;
|
|
368
|
+
const { _fields, _extends, limit, offset, ...query } = req.query;
|
|
369
|
+
const paged = isPagedRequest(limit, offset);
|
|
370
|
+
const opts = {
|
|
371
|
+
view: _fields === 'full' ? 'full' : 'list',
|
|
372
|
+
extends: _extends === '1',
|
|
373
|
+
};
|
|
404
374
|
try {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
375
|
+
if (!paged) {
|
|
376
|
+
const rows = await recordList(library, shelf, query, opts);
|
|
377
|
+
return rows.map((r) => maskRecordRow(library, shelf, r));
|
|
378
|
+
}
|
|
379
|
+
const page = await recordListPage(library, shelf, query, {
|
|
380
|
+
...opts,
|
|
381
|
+
limit: limit === undefined ? undefined : Number(limit),
|
|
382
|
+
offset: offset === undefined ? undefined : Number(offset),
|
|
408
383
|
});
|
|
409
|
-
return rows.map((r) => maskRecordRow(library,
|
|
384
|
+
return { rows: page.rows.map((r) => maskRecordRow(library, shelf, r)), total: page.total };
|
|
410
385
|
}
|
|
411
386
|
catch (e) {
|
|
412
|
-
if (e instanceof
|
|
413
|
-
return reply.code(404).send({ error: '
|
|
387
|
+
if (e instanceof UnknownShelfError)
|
|
388
|
+
return reply.code(404).send({ error: 'unknown_shelf' });
|
|
414
389
|
throw e;
|
|
415
390
|
}
|
|
416
391
|
});
|
|
417
|
-
app.get('/api/:library/:
|
|
418
|
-
const { library,
|
|
392
|
+
app.get('/api/:library/:shelf/:id', async (req, reply) => {
|
|
393
|
+
const { library, shelf, id } = req.params;
|
|
419
394
|
const rid = Number(id);
|
|
420
395
|
if (isNaN(rid))
|
|
421
396
|
return reply.code(400).send({ error: 'invalid_id' });
|
|
422
397
|
try {
|
|
423
|
-
const row = await recordGet(library,
|
|
398
|
+
const row = await recordGet(library, shelf, rid);
|
|
424
399
|
if (!row)
|
|
425
400
|
return reply.code(404).send({ error: 'not_found' });
|
|
426
|
-
return maskRecordRow(library,
|
|
401
|
+
return maskRecordRow(library, shelf, row);
|
|
427
402
|
}
|
|
428
403
|
catch (e) {
|
|
429
|
-
if (e instanceof
|
|
430
|
-
return reply.code(404).send({ error: '
|
|
404
|
+
if (e instanceof UnknownShelfError)
|
|
405
|
+
return reply.code(404).send({ error: 'unknown_shelf' });
|
|
431
406
|
throw e;
|
|
432
407
|
}
|
|
433
408
|
});
|
|
434
|
-
app.post('/api/:library/:
|
|
435
|
-
const { library,
|
|
409
|
+
app.post('/api/:library/:shelf', (req, reply) => guard(reply, async () => {
|
|
410
|
+
const { library, shelf } = req.params;
|
|
436
411
|
try {
|
|
437
|
-
const m = getShelf(library,
|
|
412
|
+
const m = getShelf(library, shelf);
|
|
438
413
|
let body = (req.body ?? {});
|
|
439
414
|
if (m)
|
|
440
415
|
body = preserveTree(m.fields, body, {});
|
|
441
|
-
const row = await recordCreate(library,
|
|
442
|
-
return reply.code(201).send(maskRecordRow(library,
|
|
416
|
+
const row = await recordCreate(library, shelf, body);
|
|
417
|
+
return reply.code(201).send(maskRecordRow(library, shelf, row));
|
|
443
418
|
}
|
|
444
419
|
catch (e) {
|
|
445
|
-
if (e instanceof
|
|
446
|
-
return reply.code(404).send({ error: '
|
|
420
|
+
if (e instanceof UnknownShelfError)
|
|
421
|
+
return reply.code(404).send({ error: 'unknown_shelf' });
|
|
447
422
|
throw e;
|
|
448
423
|
}
|
|
449
424
|
}));
|
|
450
|
-
app.patch('/api/:library/:
|
|
451
|
-
const { library,
|
|
425
|
+
app.patch('/api/:library/:shelf/:id', (req, reply) => guard(reply, async () => {
|
|
426
|
+
const { library, shelf, id } = req.params;
|
|
452
427
|
const rid = Number(id);
|
|
453
428
|
if (isNaN(rid))
|
|
454
429
|
return reply.code(400).send({ error: 'invalid_id' });
|
|
455
430
|
try {
|
|
456
|
-
const m = getShelf(library,
|
|
431
|
+
const m = getShelf(library, shelf);
|
|
457
432
|
let body = (req.body ?? {});
|
|
458
433
|
if (m) {
|
|
459
|
-
const existing = (await recordGet(library,
|
|
434
|
+
const existing = (await recordGet(library, shelf, rid)) ?? {};
|
|
460
435
|
body = preserveTree(m.fields, body, existing);
|
|
461
|
-
const exDefs = getExtendsFor(library,
|
|
436
|
+
const exDefs = getExtendsFor(library, shelf);
|
|
462
437
|
const exRecs = (existing._extends ?? {});
|
|
463
438
|
for (const def of exDefs) {
|
|
464
439
|
const bk = `_extend_${def.id}`;
|
|
@@ -468,27 +443,27 @@ app.patch('/api/:library/:type/:id', (req, reply) => guard(reply, async () => {
|
|
|
468
443
|
}
|
|
469
444
|
}
|
|
470
445
|
}
|
|
471
|
-
const row = await recordUpdate(library,
|
|
472
|
-
return maskRecordRow(library,
|
|
446
|
+
const row = await recordUpdate(library, shelf, rid, body);
|
|
447
|
+
return maskRecordRow(library, shelf, row);
|
|
473
448
|
}
|
|
474
449
|
catch (e) {
|
|
475
|
-
if (e instanceof
|
|
476
|
-
return reply.code(404).send({ error: '
|
|
450
|
+
if (e instanceof UnknownShelfError)
|
|
451
|
+
return reply.code(404).send({ error: 'unknown_shelf' });
|
|
477
452
|
throw e;
|
|
478
453
|
}
|
|
479
454
|
}));
|
|
480
|
-
app.delete('/api/:library/:
|
|
481
|
-
const { library,
|
|
455
|
+
app.delete('/api/:library/:shelf/:id', (req, reply) => guard(reply, async () => {
|
|
456
|
+
const { library, shelf, id } = req.params;
|
|
482
457
|
const rid = Number(id);
|
|
483
458
|
if (isNaN(rid))
|
|
484
459
|
return reply.code(400).send({ error: 'invalid_id' });
|
|
485
460
|
try {
|
|
486
|
-
await recordDelete(library,
|
|
461
|
+
await recordDelete(library, shelf, rid);
|
|
487
462
|
return reply.code(204).send();
|
|
488
463
|
}
|
|
489
464
|
catch (e) {
|
|
490
|
-
if (e instanceof
|
|
491
|
-
return reply.code(404).send({ error: '
|
|
465
|
+
if (e instanceof UnknownShelfError)
|
|
466
|
+
return reply.code(404).send({ error: 'unknown_shelf' });
|
|
492
467
|
throw e;
|
|
493
468
|
}
|
|
494
469
|
}));
|
package/dist/local-api.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
3
|
-
export declare function localExists(library: string,
|
|
4
|
-
export declare function localPost(library: string,
|
|
5
|
-
export declare function localPatch(library: string,
|
|
1
|
+
import { UnknownShelfError } from './records-api.ts';
|
|
2
|
+
export { UnknownShelfError };
|
|
3
|
+
export declare function localExists(library: string, shelf: string, id: number): Promise<boolean>;
|
|
4
|
+
export declare function localPost(library: string, shelf: string, body: unknown): Promise<Record<string, unknown>>;
|
|
5
|
+
export declare function localPatch(library: string, shelf: string, id: number, body: unknown): Promise<Record<string, unknown>>;
|
package/dist/local-api.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import { recordCreate, recordUpdate, recordGet,
|
|
2
|
-
export {
|
|
3
|
-
export async function localExists(library,
|
|
1
|
+
import { recordCreate, recordUpdate, recordGet, UnknownShelfError } from "./records-api.js";
|
|
2
|
+
export { UnknownShelfError };
|
|
3
|
+
export async function localExists(library, shelf, id) {
|
|
4
4
|
try {
|
|
5
|
-
return Boolean(await recordGet(library,
|
|
5
|
+
return Boolean(await recordGet(library, shelf, id));
|
|
6
6
|
}
|
|
7
7
|
catch (e) {
|
|
8
|
-
if (e instanceof
|
|
8
|
+
if (e instanceof UnknownShelfError)
|
|
9
9
|
return false;
|
|
10
10
|
throw e;
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
-
export function localPost(library,
|
|
14
|
-
return recordCreate(library,
|
|
13
|
+
export function localPost(library, shelf, body) {
|
|
14
|
+
return recordCreate(library, shelf, body, 'import');
|
|
15
15
|
}
|
|
16
|
-
export function localPatch(library,
|
|
17
|
-
return recordUpdate(library,
|
|
16
|
+
export function localPatch(library, shelf, id, body) {
|
|
17
|
+
return recordUpdate(library, shelf, id, body, 'import');
|
|
18
18
|
}
|
package/dist/mcp-http.js
CHANGED
|
@@ -20,7 +20,19 @@ export async function registerMcpHttp(app) {
|
|
|
20
20
|
const actor = req.user.login;
|
|
21
21
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
22
22
|
reply.hijack();
|
|
23
|
-
|
|
23
|
+
let closed = false;
|
|
24
|
+
const closeTransport = async () => {
|
|
25
|
+
if (closed)
|
|
26
|
+
return;
|
|
27
|
+
closed = true;
|
|
28
|
+
try {
|
|
29
|
+
await transport.close();
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
log.debug(`/mcp transport close: ${e.message}`);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
reply.raw.on('close', () => void closeTransport());
|
|
24
36
|
try {
|
|
25
37
|
const server = await buildMcpServer(role, actor);
|
|
26
38
|
await server.connect(transport);
|
|
@@ -33,5 +45,8 @@ export async function registerMcpHttp(app) {
|
|
|
33
45
|
reply.raw.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32603, message: 'internal' }, id: null }));
|
|
34
46
|
}
|
|
35
47
|
}
|
|
48
|
+
finally {
|
|
49
|
+
await closeTransport();
|
|
50
|
+
}
|
|
36
51
|
});
|
|
37
52
|
}
|
package/dist/mcp-local.d.ts
CHANGED
|
@@ -2,9 +2,9 @@ import type { CofferClientApi } from '@coffer-org/mcp/client';
|
|
|
2
2
|
export declare function mapError(e: unknown): never;
|
|
3
3
|
export declare class LocalClient implements CofferClientApi {
|
|
4
4
|
getSchema(): Promise<unknown>;
|
|
5
|
-
listRecords(library: string,
|
|
6
|
-
getRecord(library: string,
|
|
7
|
-
createRecord(library: string,
|
|
8
|
-
updateRecord(library: string,
|
|
9
|
-
deleteRecord(library: string,
|
|
5
|
+
listRecords(library: string, shelf: string, query?: Record<string, unknown>): Promise<unknown>;
|
|
6
|
+
getRecord(library: string, shelf: string, id: number): Promise<unknown>;
|
|
7
|
+
createRecord(library: string, shelf: string, fields: Record<string, unknown>): Promise<unknown>;
|
|
8
|
+
updateRecord(library: string, shelf: string, id: number, fields: Record<string, unknown>): Promise<unknown>;
|
|
9
|
+
deleteRecord(library: string, shelf: string, id: number): Promise<unknown>;
|
|
10
10
|
}
|
package/dist/mcp-local.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { ValidationError, NotFoundError } from '@coffer-org/mcp/client';
|
|
2
|
-
import { recordList, recordGet, recordCreate, recordUpdate, recordDelete,
|
|
2
|
+
import { recordList, recordGet, recordCreate, recordUpdate, recordDelete, UnknownShelfError, } from "./records-api.js";
|
|
3
3
|
import { ValidationError as ServerValidationError } from "./mutate.js";
|
|
4
4
|
import { buildClientSchema } from "./schema-api.js";
|
|
5
5
|
export function mapError(e) {
|
|
6
6
|
if (e instanceof ServerValidationError)
|
|
7
7
|
throw new ValidationError(e.issues ?? []);
|
|
8
|
-
if (e instanceof
|
|
8
|
+
if (e instanceof UnknownShelfError)
|
|
9
9
|
throw new NotFoundError('not_found');
|
|
10
10
|
throw e;
|
|
11
11
|
}
|
|
@@ -13,41 +13,41 @@ export class LocalClient {
|
|
|
13
13
|
async getSchema() {
|
|
14
14
|
return buildClientSchema();
|
|
15
15
|
}
|
|
16
|
-
async listRecords(library,
|
|
16
|
+
async listRecords(library, shelf, query = {}) {
|
|
17
17
|
try {
|
|
18
|
-
return await recordList(library,
|
|
18
|
+
return await recordList(library, shelf, query);
|
|
19
19
|
}
|
|
20
20
|
catch (e) {
|
|
21
21
|
mapError(e);
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
-
async getRecord(library,
|
|
24
|
+
async getRecord(library, shelf, id) {
|
|
25
25
|
try {
|
|
26
|
-
return await recordGet(library,
|
|
26
|
+
return await recordGet(library, shelf, id);
|
|
27
27
|
}
|
|
28
28
|
catch (e) {
|
|
29
29
|
mapError(e);
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
-
async createRecord(library,
|
|
32
|
+
async createRecord(library, shelf, fields) {
|
|
33
33
|
try {
|
|
34
|
-
return await recordCreate(library,
|
|
34
|
+
return await recordCreate(library, shelf, fields);
|
|
35
35
|
}
|
|
36
36
|
catch (e) {
|
|
37
37
|
mapError(e);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
async updateRecord(library,
|
|
40
|
+
async updateRecord(library, shelf, id, fields) {
|
|
41
41
|
try {
|
|
42
|
-
return await recordUpdate(library,
|
|
42
|
+
return await recordUpdate(library, shelf, id, fields);
|
|
43
43
|
}
|
|
44
44
|
catch (e) {
|
|
45
45
|
mapError(e);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
async deleteRecord(library,
|
|
48
|
+
async deleteRecord(library, shelf, id) {
|
|
49
49
|
try {
|
|
50
|
-
await recordDelete(library,
|
|
50
|
+
await recordDelete(library, shelf, id);
|
|
51
51
|
return null;
|
|
52
52
|
}
|
|
53
53
|
catch (e) {
|
package/dist/mcp-tools.js
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { buildTools } from '@coffer-org/mcp';
|
|
3
3
|
import { SchemaCache } from '@coffer-org/mcp/schema';
|
|
4
4
|
import { LocalClient } from "./mcp-local.js";
|
|
5
|
+
import { frontendInstructions } from "./frontend-agent.js";
|
|
5
6
|
import { mintUploadTicket } from "./upload-ticket.js";
|
|
6
7
|
import { pluginHooks, pluginCtx } from "./plugin-hooks.js";
|
|
7
8
|
import { getActiveRegistry } from "./registry-context.js";
|
|
@@ -18,7 +19,7 @@ export function formatHits(hits) {
|
|
|
18
19
|
if (hits.length === 0)
|
|
19
20
|
return 'No matching records.';
|
|
20
21
|
return hits
|
|
21
|
-
.map((h) => `[${h.
|
|
22
|
+
.map((h) => `[${h.shelfKey}/${h.recordId}] (dist ${h.distance.toFixed(3)})\n${h.snippet}`)
|
|
22
23
|
.join('\n\n');
|
|
23
24
|
}
|
|
24
25
|
const ok = (data) => ({
|
|
@@ -56,7 +57,7 @@ export async function collectMcpTools(opts = {}) {
|
|
|
56
57
|
server: 'coffer',
|
|
57
58
|
bareName: 'create_upload_ticket',
|
|
58
59
|
httpName: 'create_upload_ticket',
|
|
59
|
-
description: 'Mint a short-lived (60 min) upload-only token for POST /api/upload. Use it as a Bearer header to stream local files to the server without sending their bytes through this conversation. The token cannot read or modify records.
|
|
60
|
+
description: 'Mint a short-lived (60 min) upload-only token for POST /api/upload. Use it as a Bearer header to stream local files to the server without sending their bytes through this conversation. The token cannot read or modify records. Each upload responds {"name":"<name>","mime":...,"size":...} — store it in a file/image/media field as {"name":"<name>"}. This is the ONLY way to fill such a field: a remote URL written straight into the field is rejected, so download the file locally first and upload that.',
|
|
60
61
|
inputSchema: {},
|
|
61
62
|
scope: 'upload',
|
|
62
63
|
role: 'member',
|
|
@@ -66,7 +67,7 @@ export async function collectMcpTools(opts = {}) {
|
|
|
66
67
|
token,
|
|
67
68
|
upload_url: '/api/upload',
|
|
68
69
|
expires_in: expiresInSec,
|
|
69
|
-
how_to: 'curl -H "Authorization: Bearer <token>" -F "file=@<path>" <base-url>/api/upload → {"
|
|
70
|
+
how_to: 'curl -H "Authorization: Bearer <token>" -F "file=@<path>" <base-url>/api/upload → {"name":"<name>"}. Then set a file field to {"name":"<name>"}. mime/size are filled in by the server — do not send your own.',
|
|
70
71
|
});
|
|
71
72
|
},
|
|
72
73
|
});
|
|
@@ -99,7 +100,7 @@ export async function collectMcpTools(opts = {}) {
|
|
|
99
100
|
server: 'rag',
|
|
100
101
|
bareName: 'search_records',
|
|
101
102
|
httpName: 'search_records',
|
|
102
|
-
description: "Semantic search over the user's coffer records. Returns the most relevant records as
|
|
103
|
+
description: "Semantic search over the user's coffer records. Returns the most relevant records as library/shelf/id refs with a text snippet.",
|
|
103
104
|
inputSchema: { query: z.string(), k: z.number().int().positive().optional() },
|
|
104
105
|
scope: 'rag',
|
|
105
106
|
role: 'member',
|
|
@@ -227,21 +228,31 @@ export async function buildDomainSections() {
|
|
|
227
228
|
'## Libraries (what each holds / when to use it — pick the right one before searching)\n\n' + blocks.join('\n\n');
|
|
228
229
|
}
|
|
229
230
|
const dataModel = '## Data model\n' +
|
|
230
|
-
'Library (top-level area) →
|
|
231
|
+
'Library (top-level area) → shelf (a kind of record, e.g. things/item) → record (addressed library/shelf/id) → fields. ' +
|
|
231
232
|
'Some field values are JSON (e.g. quantity {"value":2000,"unit":"ml"}); some are relations (hold another record\'s id); ' +
|
|
232
|
-
|
|
233
|
+
"some are collections (nested rows — an array). Extends add extra field-sets to a shelf's records, shown only when a " +
|
|
233
234
|
'condition holds (the "when …" notes below); in a fetched record they sit under `_extends`. ' +
|
|
234
|
-
'Read: list_libraries →
|
|
235
|
+
'Read: list_libraries → describe_shelf → list_records/get_record. Write: create_record/update_record (call describe_shelf first); ' +
|
|
235
236
|
'to remove a record use delete_record — do not blank its fields.';
|
|
236
237
|
const rules = (await collectPluginInstructions()).map(({ id, instructions }) => `## ${id}\n${instructions}`);
|
|
237
|
-
|
|
238
|
+
const site = await frontendInstructions();
|
|
239
|
+
return [
|
|
240
|
+
dataModel,
|
|
241
|
+
...(overview ? [overview] : []),
|
|
242
|
+
...(site ? [`## web\n${site}`] : []),
|
|
243
|
+
...rules,
|
|
244
|
+
];
|
|
238
245
|
}
|
|
239
246
|
export function buildMcpInstructions(sections) {
|
|
240
247
|
const base = [
|
|
241
248
|
"Coffer is the user's personal database, organized into libraries (kitchen, people, finance, health, devices, home, garden, documents, travel, and more), each holding typed records.",
|
|
242
|
-
'Use the tools to read and write this data: call list_libraries to see what exists,
|
|
243
|
-
'The per-library notes below name the shelves and the rules — not every field. For exact field names, types, and which are required, call
|
|
249
|
+
'Use the tools to read and write this data: call list_libraries to see what exists, describe_shelf before create_record/update_record, then list_records / get_record to read. When a search_records tool is available, use it for semantic lookup.',
|
|
250
|
+
'The per-library notes below name the shelves and the rules — not every field. For exact field names, types, and which are required, call describe_shelf(library, shelf) rather than guessing from the notes.',
|
|
244
251
|
'Record field values may be JSON-encoded (e.g. quantity {"value":2000,"unit":"ml"}) — parse them.',
|
|
252
|
+
'File fields (file/document/audio/video/image/media/avatar/cover/poster) hold a file uploaded to this server, ' +
|
|
253
|
+
'never a remote URL: {"name":"<filename from POST /api/upload>"}. Writing a URL is rejected. ' +
|
|
254
|
+
'To store a picture found on the web, download it locally first, then create_upload_ticket → POST /api/upload → write the returned name. ' +
|
|
255
|
+
'See the field\'s "write" note in describe_shelf.',
|
|
245
256
|
].join('\n');
|
|
246
257
|
return sections.length ? `${base}\n\n${sections.join('\n\n')}` : base;
|
|
247
258
|
}
|
package/dist/migrations.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export declare function introspectTable(em: EntityManager, table: string): Promi
|
|
|
10
10
|
}[];
|
|
11
11
|
}>;
|
|
12
12
|
export declare function makeTable(em: EntityManager, table: string): TableOps;
|
|
13
|
+
export declare function renameSystemShelfKey(em: EntityManager): Promise<void>;
|
|
13
14
|
export declare function validateMigrations(pluginId: string, list: Migration[]): void;
|
|
14
15
|
interface RunArgs {
|
|
15
16
|
em: EntityManager;
|