@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 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.code(422).send({ issues: [{ field: !body.login ? 'login' : !body.password ? 'password' : 'displayName', code: 'required' }] });
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({ login: body.login, password: body.password, displayName: body.displayName, role: 'admin' });
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({ login: body.login, password: body.password, displayName: body.displayName, role: body.role });
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) => {
@@ -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 { id: row.id, login: row.login, displayName: row.display_name, role: row.role, disabled: Boolean(row.disabled) };
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(() => { timers.delete(h); cb(); }, ms);
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(() => { timers.delete(to); log.warn(`${task.name}: timed out after ${timeoutMs}ms`); resolve(); }, timeoutMs);
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([runPromise.catch((e) => log.warn(`${task.name}: ${e.message}`)), timeout]);
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) { tasks.push(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(() => { for (const t of tasks)
79
- enqueue(t); }, startupDelayMs);
90
+ arm(() => {
91
+ for (const t of tasks)
92
+ enqueue(t);
93
+ }, startupDelayMs);
80
94
  },
81
95
  stop() {
82
96
  stopped = true;
@@ -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).filter(([, fm]) => isJsonStored(fm)).map(([k]) => k));
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] = jsonKeys.has(k) && flatR[k] !== null && typeof flatR[k] === 'object' ? JSON.stringify(flatR[k]) : flatR[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;
@@ -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 || /insufficient_quota|billing|payment_required|payment required|hard.?limit|quota exceeded/.test(marker))
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';
@@ -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): Promise<EmbeddingHit[]>;
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[]>;
@@ -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, recordId: r.record_id, snippet: r.snippet, full: decodeVector(r.vector),
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.getConnection().execute("SELECT count(*) AS n FROM _embeddings WHERE typeof(vector) = 'text'"));
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 N = rows.length;
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 = rows
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 = rows[i];
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 } }, { orderBy: { id: 'ASC' }, limit }));
113
+ const rows = (await em.find('_Event', { id: { $gt: lastId } }, {
114
+ orderBy: { id: 'ASC' },
115
+ limit,
116
+ }));
108
117
  return rows;
109
118
  }
@@ -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>>;
@@ -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, PluginRowSchema, MigrationRowSchema, SeedRowSchema,
284
- EmbeddingSchema, PluginStateSchema, RecordActivitySchema, MsgLogSchema,
285
- UserSchema, SessionSchema, ApiTokenSchema,
286
- OAuthClientSchema, OAuthCodeSchema, OAuthTokenSchema,
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
- await upsertExtendRecord(em, e, baseId, parsed.data);
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
  }
@@ -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 { ...flat, ...collections };
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
- out.set(id, { ...flat, ...(collectionsById.get(id) ?? {}) });
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
- await em.upsert(name, { base_id: baseId, ...base });
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) {
@@ -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[];
@@ -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
- export function normalizeFileFields(m, data) {
90
+ function normalizeLevel(fields, data, path) {
91
91
  const issues = [];
92
- for (const [key, f] of fieldEntries(m.fields)) {
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 (issues.length)
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 = { ...body, [bk]: preserveTree(def.fields, sub, (exRecs[def.id] ?? {})) };
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/') || req.url.startsWith('/uploads/') || req.url === '/mcp' || req.url.startsWith('/mcp?');
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) {
@@ -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 EmbeddingHit } from './embeddings.ts';
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: EmbeddingHit[]): string;
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;