@coffer-org/server 2.6.0 → 2.7.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/auth-api.js +17 -3
- package/dist/auth-store.js +7 -1
- package/dist/background-scheduler.js +20 -6
- package/dist/collection-io.js +5 -2
- package/dist/embed-openai.js +2 -1
- package/dist/embeddings.d.ts +4 -1
- package/dist/embeddings.js +17 -8
- package/dist/entity-schema.d.ts +1 -0
- package/dist/entity-schema.js +28 -4
- package/dist/extend-io.js +6 -1
- package/dist/extend-table.js +9 -4
- package/dist/file-fields.d.ts +2 -0
- package/dist/file-fields.js +25 -6
- package/dist/index.js +16 -2
- package/dist/mcp-http.test-helpers.js +4 -1
- package/dist/mcp-local.js +3 -1
- package/dist/mcp-tools.d.ts +15 -2
- package/dist/mcp-tools.js +83 -11
- package/dist/msg-log.d.ts +2 -0
- package/dist/msg-log.js +21 -3
- package/dist/mutate.d.ts +3 -1
- package/dist/mutate.js +6 -4
- package/dist/oauth-api.js +25 -10
- package/dist/plugin-i18n.d.ts +6 -0
- package/dist/plugin-i18n.js +38 -0
- package/dist/rag-search.d.ts +40 -0
- package/dist/rag-search.js +94 -0
- package/dist/records-api.d.ts +4 -0
- package/dist/records-api.js +53 -4
- package/dist/search-index.js +2 -8
- package/dist/temporal.d.ts +5 -2
- package/dist/temporal.js +8 -6
- package/dist/thread-state.d.ts +8 -0
- package/dist/thread-state.js +39 -0
- package/dist/thread-store.js +3 -1
- package/dist/uploads.js +1 -1
- package/package.json +2 -2
package/dist/auth-api.js
CHANGED
|
@@ -75,9 +75,18 @@ export async function registerAuthApi(app) {
|
|
|
75
75
|
return reply.code(409).send({ error: 'already_setup' });
|
|
76
76
|
const body = (req.body ?? {});
|
|
77
77
|
if (!body.login || !body.password || !body.displayName) {
|
|
78
|
-
return reply
|
|
78
|
+
return reply
|
|
79
|
+
.code(422)
|
|
80
|
+
.send({
|
|
81
|
+
issues: [{ field: !body.login ? 'login' : !body.password ? 'password' : 'displayName', code: 'required' }],
|
|
82
|
+
});
|
|
79
83
|
}
|
|
80
|
-
const user = await createUser({
|
|
84
|
+
const user = await createUser({
|
|
85
|
+
login: body.login,
|
|
86
|
+
password: body.password,
|
|
87
|
+
displayName: body.displayName,
|
|
88
|
+
role: 'admin',
|
|
89
|
+
});
|
|
81
90
|
const { raw } = await createSession(user.id, SESSION_TTL_MS);
|
|
82
91
|
setCookie(reply, raw, SESSION_TTL_MS / 1000);
|
|
83
92
|
return publicUser(user);
|
|
@@ -129,7 +138,12 @@ export async function registerAuthApi(app) {
|
|
|
129
138
|
if (!body.login || !body.password || !body.displayName || (body.role !== 'admin' && body.role !== 'member')) {
|
|
130
139
|
return reply.code(422).send({ issues: [{ field: 'login', code: 'required' }] });
|
|
131
140
|
}
|
|
132
|
-
const user = await createUser({
|
|
141
|
+
const user = await createUser({
|
|
142
|
+
login: body.login,
|
|
143
|
+
password: body.password,
|
|
144
|
+
displayName: body.displayName,
|
|
145
|
+
role: body.role,
|
|
146
|
+
});
|
|
133
147
|
return publicUser(user);
|
|
134
148
|
});
|
|
135
149
|
app.patch('/api/auth/users/:id', async (req, reply) => {
|
package/dist/auth-store.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { getEm } from "./db.js";
|
|
2
2
|
import { hashPassword, generateToken, hashToken } from "./auth-crypto.js";
|
|
3
3
|
function toAuthUser(row) {
|
|
4
|
-
return {
|
|
4
|
+
return {
|
|
5
|
+
id: row.id,
|
|
6
|
+
login: row.login,
|
|
7
|
+
displayName: row.display_name,
|
|
8
|
+
role: row.role,
|
|
9
|
+
disabled: Boolean(row.disabled),
|
|
10
|
+
};
|
|
5
11
|
}
|
|
6
12
|
export async function countUsers() {
|
|
7
13
|
const em = getEm().fork();
|
|
@@ -22,7 +22,10 @@ export function makeScheduler(opts = {}) {
|
|
|
22
22
|
function arm(cb, ms) {
|
|
23
23
|
if (stopped)
|
|
24
24
|
return;
|
|
25
|
-
const h = setTimer(() => {
|
|
25
|
+
const h = setTimer(() => {
|
|
26
|
+
timers.delete(h);
|
|
27
|
+
cb();
|
|
28
|
+
}, ms);
|
|
26
29
|
timers.add(h);
|
|
27
30
|
}
|
|
28
31
|
function enqueue(task) {
|
|
@@ -51,7 +54,11 @@ export function makeScheduler(opts = {}) {
|
|
|
51
54
|
const timeoutMs = task.timeoutMs ?? defaultTimeoutMs;
|
|
52
55
|
let to;
|
|
53
56
|
const timeout = new Promise((resolve) => {
|
|
54
|
-
to = setTimer(() => {
|
|
57
|
+
to = setTimer(() => {
|
|
58
|
+
timers.delete(to);
|
|
59
|
+
log.warn(`${task.name}: timed out after ${timeoutMs}ms`);
|
|
60
|
+
resolve();
|
|
61
|
+
}, timeoutMs);
|
|
55
62
|
timers.add(to);
|
|
56
63
|
});
|
|
57
64
|
try {
|
|
@@ -62,7 +69,10 @@ export function makeScheduler(opts = {}) {
|
|
|
62
69
|
catch (e) {
|
|
63
70
|
runPromise = Promise.reject(e);
|
|
64
71
|
}
|
|
65
|
-
await Promise.race([
|
|
72
|
+
await Promise.race([
|
|
73
|
+
runPromise.catch((e) => log.warn(`${task.name}: ${e.message}`)),
|
|
74
|
+
timeout,
|
|
75
|
+
]);
|
|
66
76
|
}
|
|
67
77
|
finally {
|
|
68
78
|
clearTimer(to);
|
|
@@ -70,13 +80,17 @@ export function makeScheduler(opts = {}) {
|
|
|
70
80
|
}
|
|
71
81
|
}
|
|
72
82
|
return {
|
|
73
|
-
register(task) {
|
|
83
|
+
register(task) {
|
|
84
|
+
tasks.push(task);
|
|
85
|
+
},
|
|
74
86
|
start() {
|
|
75
87
|
if (started || stopped)
|
|
76
88
|
return;
|
|
77
89
|
started = true;
|
|
78
|
-
arm(() => {
|
|
79
|
-
|
|
90
|
+
arm(() => {
|
|
91
|
+
for (const t of tasks)
|
|
92
|
+
enqueue(t);
|
|
93
|
+
}, startupDelayMs);
|
|
80
94
|
},
|
|
81
95
|
stop() {
|
|
82
96
|
stopped = true;
|
package/dist/collection-io.js
CHANGED
|
@@ -47,14 +47,17 @@ export async function writeAt(tx, prefix, fields, parentId, collections) {
|
|
|
47
47
|
await deleteAt(tx, table, childFields, ex.id);
|
|
48
48
|
await tx.nativeDelete(table, { parent_id: parentId });
|
|
49
49
|
const sKeys = scalarKeys(childFields);
|
|
50
|
-
const jsonKeys = new Set(fieldEntries(childFields)
|
|
50
|
+
const jsonKeys = new Set(fieldEntries(childFields)
|
|
51
|
+
.filter(([, fm]) => isJsonStored(fm))
|
|
52
|
+
.map(([k]) => k));
|
|
51
53
|
let position = 0;
|
|
52
54
|
for (const r of rows) {
|
|
53
55
|
const flatR = flattenEmbeddedAt(childFields, r);
|
|
54
56
|
const picked = {};
|
|
55
57
|
for (const k of sKeys)
|
|
56
58
|
if (flatR[k] !== undefined)
|
|
57
|
-
picked[k] =
|
|
59
|
+
picked[k] =
|
|
60
|
+
jsonKeys.has(k) && flatR[k] !== null && typeof flatR[k] === 'object' ? JSON.stringify(flatR[k]) : flatR[k];
|
|
58
61
|
const child = tx.create(table, { parent_id: parentId, position, ...picked });
|
|
59
62
|
await tx.flush();
|
|
60
63
|
const childId = child.id;
|
package/dist/embed-openai.js
CHANGED
|
@@ -29,7 +29,8 @@ function parseErrorBody(body) {
|
|
|
29
29
|
}
|
|
30
30
|
function classifyEmbeddingFailure(status, code, detail) {
|
|
31
31
|
const marker = `${code ?? ''} ${detail ?? ''}`.toLowerCase();
|
|
32
|
-
if (status === 402 ||
|
|
32
|
+
if (status === 402 ||
|
|
33
|
+
/insufficient_quota|billing|payment_required|payment required|hard.?limit|quota exceeded/.test(marker))
|
|
33
34
|
return 'billing';
|
|
34
35
|
if (status === 401 || status === 403)
|
|
35
36
|
return 'auth';
|
package/dist/embeddings.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export declare function cosine(a: ArrayLike<number>, b: ArrayLike<number>): numb
|
|
|
25
25
|
export declare function twoPassSearch(rows: EmbeddingRow[], queryVec: ArrayLike<number>, k: number, opts?: {
|
|
26
26
|
coarseDims?: number;
|
|
27
27
|
poolMinFactor?: number;
|
|
28
|
+
shelfKeys?: Set<string>;
|
|
28
29
|
}): EmbeddingHit[];
|
|
29
30
|
export declare function upsertEmbedding(args: {
|
|
30
31
|
shelfKey: string;
|
|
@@ -34,5 +35,7 @@ export declare function upsertEmbedding(args: {
|
|
|
34
35
|
model: string;
|
|
35
36
|
}): Promise<void>;
|
|
36
37
|
export declare function deleteEmbedding(shelfKey: string, recordId: number): Promise<void>;
|
|
37
|
-
export declare function searchEmbeddings(queryVec: number[], k: number
|
|
38
|
+
export declare function searchEmbeddings(queryVec: number[], k: number, opts?: {
|
|
39
|
+
shelfKeys?: Set<string>;
|
|
40
|
+
}): Promise<EmbeddingHit[]>;
|
|
38
41
|
export declare function listEventsSince(lastId: number, limit: number): Promise<EventRow[]>;
|
package/dist/embeddings.js
CHANGED
|
@@ -12,7 +12,10 @@ async function loadCache() {
|
|
|
12
12
|
const em = getEm().fork();
|
|
13
13
|
const rows = (await em.find('_Embedding', {}));
|
|
14
14
|
cache = rows.map((r) => ({
|
|
15
|
-
shelfKey: r.shelf_key,
|
|
15
|
+
shelfKey: r.shelf_key,
|
|
16
|
+
recordId: r.record_id,
|
|
17
|
+
snippet: r.snippet,
|
|
18
|
+
full: decodeVector(r.vector),
|
|
16
19
|
}));
|
|
17
20
|
return cache;
|
|
18
21
|
}
|
|
@@ -30,7 +33,9 @@ export async function migrateEmbeddingVectorsToBlob() {
|
|
|
30
33
|
const em = getEm().fork();
|
|
31
34
|
if (dialectOf(em) !== 'sqlite')
|
|
32
35
|
return 0;
|
|
33
|
-
const [{ n }] = (await em
|
|
36
|
+
const [{ n }] = (await em
|
|
37
|
+
.getConnection()
|
|
38
|
+
.execute("SELECT count(*) AS n FROM _embeddings WHERE typeof(vector) = 'text'"));
|
|
34
39
|
if (n === 0)
|
|
35
40
|
return 0;
|
|
36
41
|
const rows = (await em.find('_Embedding', {}));
|
|
@@ -59,7 +64,8 @@ export function cosine(a, b) {
|
|
|
59
64
|
return denom ? dot / denom : 0;
|
|
60
65
|
}
|
|
61
66
|
export function twoPassSearch(rows, queryVec, k, opts = {}) {
|
|
62
|
-
const
|
|
67
|
+
const scope = opts.shelfKeys ? rows.filter((r) => opts.shelfKeys.has(r.shelfKey)) : rows;
|
|
68
|
+
const N = scope.length;
|
|
63
69
|
if (N === 0)
|
|
64
70
|
return [];
|
|
65
71
|
const coarseDims = opts.coarseDims ?? COARSE_DIMS;
|
|
@@ -68,13 +74,13 @@ export function twoPassSearch(rows, queryVec, k, opts = {}) {
|
|
|
68
74
|
const m = Math.min(coarseDims, qFull.length);
|
|
69
75
|
const qSmall = qFull.subarray(0, m);
|
|
70
76
|
const poolSize = Math.min(N, Math.max(k * poolMinFactor, Math.ceil(N * 0.1)));
|
|
71
|
-
const pool =
|
|
77
|
+
const pool = scope
|
|
72
78
|
.map((r, i) => ({ i, d: 1 - cosine(r.full.subarray(0, m), qSmall) }))
|
|
73
79
|
.sort((a, b) => a.d - b.d)
|
|
74
80
|
.slice(0, poolSize);
|
|
75
81
|
return pool
|
|
76
82
|
.map(({ i }) => {
|
|
77
|
-
const r =
|
|
83
|
+
const r = scope[i];
|
|
78
84
|
return { shelfKey: r.shelfKey, recordId: r.recordId, snippet: r.snippet, distance: 1 - cosine(r.full, qFull) };
|
|
79
85
|
})
|
|
80
86
|
.sort((a, b) => a.distance - b.distance)
|
|
@@ -99,11 +105,14 @@ export async function deleteEmbedding(shelfKey, recordId) {
|
|
|
99
105
|
await em.nativeDelete('_Embedding', { shelf_key: shelfKey, record_id: recordId });
|
|
100
106
|
invalidateEmbeddingCache();
|
|
101
107
|
}
|
|
102
|
-
export async function searchEmbeddings(queryVec, k) {
|
|
103
|
-
return twoPassSearch(await loadCache(), queryVec, k);
|
|
108
|
+
export async function searchEmbeddings(queryVec, k, opts = {}) {
|
|
109
|
+
return twoPassSearch(await loadCache(), queryVec, k, opts);
|
|
104
110
|
}
|
|
105
111
|
export async function listEventsSince(lastId, limit) {
|
|
106
112
|
const em = getEm().fork();
|
|
107
|
-
const rows = (await em.find('_Event', { id: { $gt: lastId } }, {
|
|
113
|
+
const rows = (await em.find('_Event', { id: { $gt: lastId } }, {
|
|
114
|
+
orderBy: { id: 'ASC' },
|
|
115
|
+
limit,
|
|
116
|
+
}));
|
|
108
117
|
return rows;
|
|
109
118
|
}
|
package/dist/entity-schema.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export declare const EmbeddingSchema: EntitySchema<any, never, import("@mikro-or
|
|
|
18
18
|
export declare const PluginStateSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
|
|
19
19
|
export declare const RecordActivitySchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
|
|
20
20
|
export declare const ThreadMessageSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
|
|
21
|
+
export declare const ThreadStateSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
|
|
21
22
|
export declare function buildPluginEntities(plugins: PluginManifest[]): EntitySchema[];
|
|
22
23
|
export declare const MsgLogSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
|
|
23
24
|
export declare const UserSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
|
package/dist/entity-schema.js
CHANGED
|
@@ -180,6 +180,17 @@ export const ThreadMessageSchema = new EntitySchema({
|
|
|
180
180
|
reply_to_id: { type: 'text', nullable: true },
|
|
181
181
|
},
|
|
182
182
|
});
|
|
183
|
+
export const ThreadStateSchema = new EntitySchema({
|
|
184
|
+
name: '_ThreadState',
|
|
185
|
+
tableName: '_thread_state',
|
|
186
|
+
properties: {
|
|
187
|
+
connector: { type: 'text', primary: true },
|
|
188
|
+
chat_id: { type: 'text', primary: true },
|
|
189
|
+
agent_id: { type: 'text', nullable: true },
|
|
190
|
+
preset_id: { type: 'text', nullable: true },
|
|
191
|
+
updated_at: { type: 'text' },
|
|
192
|
+
},
|
|
193
|
+
});
|
|
183
194
|
export function buildPluginEntities(plugins) {
|
|
184
195
|
return plugins.flatMap((p) => [
|
|
185
196
|
...(p.libraries ?? []).flatMap((v) => v.shelves.flatMap((m) => [buildEntitySchema(m), ...buildCollectionEntities(m)])),
|
|
@@ -202,6 +213,8 @@ export const MsgLogSchema = new EntitySchema({
|
|
|
202
213
|
tokens_in: { type: 'integer', nullable: true },
|
|
203
214
|
tokens_out: { type: 'integer', nullable: true },
|
|
204
215
|
ms: { type: 'integer', nullable: true },
|
|
216
|
+
agent_id: { type: 'text', nullable: true },
|
|
217
|
+
preset_id: { type: 'text', nullable: true },
|
|
205
218
|
},
|
|
206
219
|
});
|
|
207
220
|
export const UserSchema = new EntitySchema({
|
|
@@ -280,9 +293,20 @@ export const OAuthTokenSchema = new EntitySchema({
|
|
|
280
293
|
},
|
|
281
294
|
});
|
|
282
295
|
export const systemEntities = [
|
|
283
|
-
EventSchema,
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
296
|
+
EventSchema,
|
|
297
|
+
PluginRowSchema,
|
|
298
|
+
MigrationRowSchema,
|
|
299
|
+
SeedRowSchema,
|
|
300
|
+
EmbeddingSchema,
|
|
301
|
+
PluginStateSchema,
|
|
302
|
+
RecordActivitySchema,
|
|
303
|
+
MsgLogSchema,
|
|
304
|
+
UserSchema,
|
|
305
|
+
SessionSchema,
|
|
306
|
+
ApiTokenSchema,
|
|
307
|
+
OAuthClientSchema,
|
|
308
|
+
OAuthCodeSchema,
|
|
309
|
+
OAuthTokenSchema,
|
|
287
310
|
ThreadMessageSchema,
|
|
311
|
+
ThreadStateSchema,
|
|
288
312
|
];
|
package/dist/extend-io.js
CHANGED
|
@@ -2,6 +2,7 @@ import { getExtendsFor } from "./registry-context.js";
|
|
|
2
2
|
import { buildExtendZodPartial } from '@coffer-org/sdk/extend';
|
|
3
3
|
import { upsertExtendRecord, deleteExtendRecord, getExtendRecord } from "./extend-table.js";
|
|
4
4
|
import { toIssue, ValidationError } from "./mutate.js";
|
|
5
|
+
import { normalizeFileFieldsAt } from "./file-fields.js";
|
|
5
6
|
export function splitBody(body) {
|
|
6
7
|
const raw = (body ?? {});
|
|
7
8
|
const base = {};
|
|
@@ -48,7 +49,11 @@ export async function saveExtends(em, library, shelf, baseId, extData) {
|
|
|
48
49
|
continue;
|
|
49
50
|
const parsed = buildExtendZodPartial(e).safeParse(data);
|
|
50
51
|
if (parsed.success && Object.keys(parsed.data).length > 0) {
|
|
51
|
-
|
|
52
|
+
const row = parsed.data;
|
|
53
|
+
const fileIssues = normalizeFileFieldsAt(e.fields, row);
|
|
54
|
+
if (fileIssues.length)
|
|
55
|
+
throw new ValidationError(fileIssues);
|
|
56
|
+
await upsertExtendRecord(em, e, baseId, row);
|
|
52
57
|
}
|
|
53
58
|
}
|
|
54
59
|
}
|
package/dist/extend-table.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { getEm } from "./db.js";
|
|
2
|
-
import { splitAt, writeAt, readAt, readAtMany, deleteAt } from "./collection-io.js";
|
|
2
|
+
import { splitAt, writeAt, readAt, readAtMany, deleteAt, flattenEmbeddedAt, nestEmbeddedAt } from "./collection-io.js";
|
|
3
|
+
import { encodeJsonAt } from "./mutate.js";
|
|
4
|
+
import { encodeTemporalAt, decodeTemporalAt } from "./temporal.js";
|
|
3
5
|
import { chunk } from "./batch.js";
|
|
4
6
|
import { selectRows } from "./read-rows.js";
|
|
5
7
|
export function extendEntityName(e) {
|
|
@@ -11,8 +13,9 @@ export async function getExtendRecord(e, baseId, em) {
|
|
|
11
13
|
const [flat] = (await selectRows(fork, name, { base_id: baseId }, { limit: 1 }));
|
|
12
14
|
if (!flat)
|
|
13
15
|
return undefined;
|
|
16
|
+
const decoded = nestEmbeddedAt(e.fields, decodeTemporalAt(e.fields, flat));
|
|
14
17
|
const collections = await readAt(fork, name, e.fields, baseId);
|
|
15
|
-
return { ...
|
|
18
|
+
return { ...decoded, ...collections };
|
|
16
19
|
}
|
|
17
20
|
export async function getExtendRecords(e, baseIds, em) {
|
|
18
21
|
const out = new Map();
|
|
@@ -30,14 +33,16 @@ export async function getExtendRecords(e, baseIds, em) {
|
|
|
30
33
|
const collectionsById = await readAtMany(fork, name, e.fields, presentIds);
|
|
31
34
|
for (const flat of flats) {
|
|
32
35
|
const id = Number(flat.base_id);
|
|
33
|
-
|
|
36
|
+
const decoded = nestEmbeddedAt(e.fields, decodeTemporalAt(e.fields, flat));
|
|
37
|
+
out.set(id, { ...decoded, ...(collectionsById.get(id) ?? {}) });
|
|
34
38
|
}
|
|
35
39
|
return out;
|
|
36
40
|
}
|
|
37
41
|
export async function upsertExtendRecord(em, e, baseId, data) {
|
|
38
42
|
const name = extendEntityName(e);
|
|
39
43
|
const { base, collections } = splitAt(e.fields, data);
|
|
40
|
-
|
|
44
|
+
const flat = encodeJsonAt(e.fields, encodeTemporalAt(e.fields, flattenEmbeddedAt(e.fields, base)));
|
|
45
|
+
await em.upsert(name, { base_id: baseId, ...flat });
|
|
41
46
|
await writeAt(em, name, e.fields, baseId, collections);
|
|
42
47
|
}
|
|
43
48
|
export async function deleteExtendRecord(em, e, baseId) {
|
package/dist/file-fields.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ShelfDef } from '@coffer-org/sdk/shelf';
|
|
2
|
+
import type { LayoutEl } from '@coffer-org/sdk/fields';
|
|
2
3
|
import type { ValidationIssue } from './mutate.ts';
|
|
3
4
|
export declare function mimeForName(name: string): string | undefined;
|
|
4
5
|
export declare function touchesFileFields(m: ShelfDef, input: unknown): boolean;
|
|
5
6
|
export declare function dropUnchangedFileFields(m: ShelfDef, input: Record<string, unknown>, stored: Record<string, unknown>): Record<string, unknown>;
|
|
7
|
+
export declare function normalizeFileFieldsAt(fields: LayoutEl[], data: Record<string, unknown>): ValidationIssue[];
|
|
6
8
|
export declare function normalizeFileFields(m: ShelfDef, data: Record<string, unknown>): ValidationIssue[];
|
package/dist/file-fields.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { fieldEntries } from '@coffer-org/sdk/shelf';
|
|
3
|
+
import { fieldEntries, collectionGroups } from '@coffer-org/sdk/shelf';
|
|
4
4
|
import { jsonValue } from '@coffer-org/sdk/fields';
|
|
5
5
|
import { uploadsDir } from "./uploads.js";
|
|
6
6
|
const MIME_BY_EXT = {
|
|
@@ -87,9 +87,9 @@ export function dropUnchangedFileFields(m, input, stored) {
|
|
|
87
87
|
}
|
|
88
88
|
return out ?? input;
|
|
89
89
|
}
|
|
90
|
-
|
|
90
|
+
function normalizeLevel(fields, data, path) {
|
|
91
91
|
const issues = [];
|
|
92
|
-
for (const [key, f] of fieldEntries(
|
|
92
|
+
for (const [key, f] of fieldEntries(fields)) {
|
|
93
93
|
if (f.prim !== 'file')
|
|
94
94
|
continue;
|
|
95
95
|
if (!(key in data))
|
|
@@ -101,21 +101,40 @@ export function normalizeFileFields(m, data) {
|
|
|
101
101
|
const isArray = Array.isArray(parsed);
|
|
102
102
|
const items = (isArray ? parsed : [parsed]);
|
|
103
103
|
const out = [];
|
|
104
|
+
let failed = false;
|
|
104
105
|
for (const it of items) {
|
|
105
106
|
if (typeof it !== 'object' || it === null || typeof it.name !== 'string') {
|
|
106
|
-
issues.push({ field: key, code: 'file_not_uploaded', path: [key] });
|
|
107
|
+
issues.push({ field: key, code: 'file_not_uploaded', path: [...path, key] });
|
|
108
|
+
failed = true;
|
|
107
109
|
continue;
|
|
108
110
|
}
|
|
109
111
|
const resolved = resolveEntry(it);
|
|
110
112
|
if (!resolved) {
|
|
111
|
-
issues.push({ field: key, code: 'file_not_uploaded', params: { name: it.name }, path: [key] });
|
|
113
|
+
issues.push({ field: key, code: 'file_not_uploaded', params: { name: it.name }, path: [...path, key] });
|
|
114
|
+
failed = true;
|
|
112
115
|
continue;
|
|
113
116
|
}
|
|
114
117
|
out.push(resolved);
|
|
115
118
|
}
|
|
116
|
-
if (
|
|
119
|
+
if (failed)
|
|
117
120
|
continue;
|
|
118
121
|
data[key] = isArray ? out : out[0];
|
|
119
122
|
}
|
|
123
|
+
for (const c of collectionGroups(fields)) {
|
|
124
|
+
const rows = data[c.key];
|
|
125
|
+
if (!Array.isArray(rows))
|
|
126
|
+
continue;
|
|
127
|
+
rows.forEach((row, i) => {
|
|
128
|
+
if (typeof row !== 'object' || row === null)
|
|
129
|
+
return;
|
|
130
|
+
issues.push(...normalizeLevel(c.group.fields, row, [...path, c.key, i]));
|
|
131
|
+
});
|
|
132
|
+
}
|
|
120
133
|
return issues;
|
|
121
134
|
}
|
|
135
|
+
export function normalizeFileFieldsAt(fields, data) {
|
|
136
|
+
return normalizeLevel(fields, data, []);
|
|
137
|
+
}
|
|
138
|
+
export function normalizeFileFields(m, data) {
|
|
139
|
+
return normalizeFileFieldsAt(m.fields, data);
|
|
140
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -24,7 +24,7 @@ import { baseUrl } from "./public-url.js";
|
|
|
24
24
|
import { discoverPluginAssets, discoverRuntime } from "./plugin-discovery.js";
|
|
25
25
|
import { checkLatestVersion, resolveUpdateTarget, resolveAllUpdateTargets, resolveBaseTargets, resolveRuntimeTarget, runNpmInstall, } from "./plugin-updates.js";
|
|
26
26
|
import { buildClientSchema } from "./schema-api.js";
|
|
27
|
-
import { recordList, recordListPage, isPagedRequest, recordGet, recordCreate, recordUpdate, recordDelete, recordRestore, recordPurge, recordTrashList, MAX_PAGE, UnknownShelfError, } from "./records-api.js";
|
|
27
|
+
import { recordList, recordListPage, isPagedRequest, recordGet, recordCreate, recordUpdate, recordDelete, recordRestore, recordPurge, recordTrashList, MAX_PAGE, UnknownShelfError, ensureSingle, SingleShelfError, } from "./records-api.js";
|
|
28
28
|
import { maskTree, preserveTree } from "./field-masking.js";
|
|
29
29
|
import { writePluginSettings } from "./settings-write.js";
|
|
30
30
|
import { rootLogger, getLogger } from "./log.js";
|
|
@@ -125,6 +125,8 @@ async function guard(reply, fn) {
|
|
|
125
125
|
catch (e) {
|
|
126
126
|
if (e instanceof ValidationError)
|
|
127
127
|
return reply.code(422).send({ error: 'validation', issues: e.issues });
|
|
128
|
+
if (e instanceof SingleShelfError)
|
|
129
|
+
return reply.code(409).send({ error: 'single_shelf', message: e.message });
|
|
128
130
|
if (e instanceof NotFoundError)
|
|
129
131
|
return reply.code(404).send({ error: 'not_found' });
|
|
130
132
|
app.log.error(e);
|
|
@@ -481,6 +483,15 @@ app.patch('/api/:library/:shelf/:id/activity', async (req, reply) => {
|
|
|
481
483
|
throw e;
|
|
482
484
|
}
|
|
483
485
|
});
|
|
486
|
+
app.get('/api/:library/:shelf/single', async (req, reply) => {
|
|
487
|
+
const { library, shelf } = req.params;
|
|
488
|
+
const m = getShelf(library, shelf);
|
|
489
|
+
if (!m)
|
|
490
|
+
return reply.code(404).send({ error: 'unknown_shelf' });
|
|
491
|
+
if (!m.single)
|
|
492
|
+
return reply.code(404).send({ error: 'not_single_shelf' });
|
|
493
|
+
return maskRecordRow(library, shelf, await ensureSingle(library, shelf));
|
|
494
|
+
});
|
|
484
495
|
app.get('/api/:library/:shelf/:id', async (req, reply) => {
|
|
485
496
|
const { library, shelf, id } = req.params;
|
|
486
497
|
const rid = Number(id);
|
|
@@ -535,7 +546,10 @@ app.patch('/api/:library/:shelf/:id', (req, reply) => guard(reply, async () => {
|
|
|
535
546
|
const bk = `_extend_${def.id}`;
|
|
536
547
|
const sub = body[bk];
|
|
537
548
|
if (sub && typeof sub === 'object') {
|
|
538
|
-
body = {
|
|
549
|
+
body = {
|
|
550
|
+
...body,
|
|
551
|
+
[bk]: preserveTree(def.fields, sub, (exRecs[def.id] ?? {})),
|
|
552
|
+
};
|
|
539
553
|
}
|
|
540
554
|
}
|
|
541
555
|
}
|
|
@@ -13,7 +13,10 @@ export async function freshMcpApp(opts = {}) {
|
|
|
13
13
|
await orm.schema.update({ safe: false, dropTables: false });
|
|
14
14
|
const app = Fastify();
|
|
15
15
|
app.addHook('onRequest', async (req, reply) => {
|
|
16
|
-
const gated = req.url.startsWith('/api/') ||
|
|
16
|
+
const gated = req.url.startsWith('/api/') ||
|
|
17
|
+
req.url.startsWith('/uploads/') ||
|
|
18
|
+
req.url === '/mcp' ||
|
|
19
|
+
req.url.startsWith('/mcp?');
|
|
17
20
|
if (!gated)
|
|
18
21
|
return;
|
|
19
22
|
if (PUBLIC_API_PATHS.some((p) => req.url.startsWith(p)))
|
package/dist/mcp-local.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ValidationError, NotFoundError } from '@coffer-org/mcp/client';
|
|
2
|
-
import { recordList, recordListPage, recordGet, recordCreate, recordUpdate, recordDelete, UnknownShelfError, recordTrashList, recordRestore, recordPurge, } from "./records-api.js";
|
|
2
|
+
import { recordList, recordListPage, recordGet, recordCreate, recordUpdate, recordDelete, UnknownShelfError, recordTrashList, recordRestore, recordPurge, SingleShelfError, } 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) {
|
|
@@ -7,6 +7,8 @@ export function mapError(e) {
|
|
|
7
7
|
throw new ValidationError(e.issues ?? []);
|
|
8
8
|
if (e instanceof UnknownShelfError)
|
|
9
9
|
throw new NotFoundError('not_found');
|
|
10
|
+
if (e instanceof SingleShelfError)
|
|
11
|
+
throw new ValidationError([{ field: 'shelf', code: 'single_shelf', message: e.message }]);
|
|
10
12
|
throw e;
|
|
11
13
|
}
|
|
12
14
|
export function splitListQuery(raw) {
|
package/dist/mcp-tools.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import { type ToolResult } from '@coffer-org/mcp';
|
|
4
4
|
import { type AuthRole, type PluginHooks } from './plugin-hooks.ts';
|
|
5
5
|
import { type Condition } from '@coffer-org/sdk/condition';
|
|
6
|
-
import { type
|
|
6
|
+
import { type RagHit } from './rag-search.ts';
|
|
7
7
|
export interface McpToolDef {
|
|
8
8
|
server: string;
|
|
9
9
|
bareName: string;
|
|
@@ -17,7 +17,7 @@ export interface McpToolDef {
|
|
|
17
17
|
export interface RagDeps {
|
|
18
18
|
embeddingApiKey: string;
|
|
19
19
|
}
|
|
20
|
-
export declare function formatHits(hits:
|
|
20
|
+
export declare function formatHits(hits: RagHit[]): string;
|
|
21
21
|
export declare function resolveRagDeps(): Promise<RagDeps | null>;
|
|
22
22
|
export declare function collectMcpTools(opts?: {
|
|
23
23
|
rag?: RagDeps | null;
|
|
@@ -28,6 +28,19 @@ export declare function collectPluginInstructions(hooks?: Record<string, PluginH
|
|
|
28
28
|
id: string;
|
|
29
29
|
instructions: string;
|
|
30
30
|
}[]>;
|
|
31
|
+
type SingleShelf = {
|
|
32
|
+
library: string;
|
|
33
|
+
shelf: string;
|
|
34
|
+
claude: string;
|
|
35
|
+
};
|
|
36
|
+
export declare function collectSingleShelves(reg?: {
|
|
37
|
+
shelves: {
|
|
38
|
+
library: string;
|
|
39
|
+
shelf: string;
|
|
40
|
+
single?: boolean;
|
|
41
|
+
claude?: string;
|
|
42
|
+
}[];
|
|
43
|
+
}): SingleShelf[];
|
|
31
44
|
type LibraryPurpose = {
|
|
32
45
|
id: string;
|
|
33
46
|
agent: string;
|