@coffer-org/server 3.3.0 → 4.0.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.
Files changed (112) hide show
  1. package/dist/auth-api.js +4 -5
  2. package/dist/cache-clock.d.ts +8 -0
  3. package/dist/cache-clock.js +103 -0
  4. package/dist/cache-refresh.d.ts +7 -0
  5. package/dist/cache-refresh.js +322 -0
  6. package/dist/collection-io.d.ts +13 -0
  7. package/dist/collection-io.js +42 -28
  8. package/dist/compute-unit.d.ts +66 -0
  9. package/dist/compute-unit.js +369 -0
  10. package/dist/condition-where.js +1 -3
  11. package/dist/data-dir.d.ts +3 -0
  12. package/dist/data-dir.js +39 -0
  13. package/dist/db.js +2 -0
  14. package/dist/entity-schema.d.ts +1 -0
  15. package/dist/entity-schema.js +25 -2
  16. package/dist/extend-io.d.ts +3 -2
  17. package/dist/extend-io.js +4 -4
  18. package/dist/extend-table.d.ts +3 -2
  19. package/dist/extend-table.js +48 -11
  20. package/dist/file-fields.js +50 -23
  21. package/dist/global-search.js +1 -1
  22. package/dist/http-errors.d.ts +7 -0
  23. package/dist/http-errors.js +24 -0
  24. package/dist/index.js +31 -35
  25. package/dist/mcp-contract/client.d.ts +24 -0
  26. package/dist/mcp-contract/client.js +12 -0
  27. package/dist/mcp-contract/schema.d.ts +46 -0
  28. package/dist/mcp-contract/schema.js +90 -0
  29. package/dist/mcp-contract/tools.d.ts +19 -0
  30. package/dist/mcp-contract/tools.js +160 -0
  31. package/dist/mcp-local.d.ts +1 -1
  32. package/dist/mcp-local.js +4 -2
  33. package/dist/mcp-tools.d.ts +12 -1
  34. package/dist/mcp-tools.js +41 -8
  35. package/dist/migrations.d.ts +30 -2
  36. package/dist/migrations.js +368 -19
  37. package/dist/mutate.js +86 -57
  38. package/dist/orchestrator/agent-capabilities.d.ts +2 -0
  39. package/dist/orchestrator/agent-capabilities.js +201 -0
  40. package/dist/orchestrator/allow.d.ts +16 -0
  41. package/dist/orchestrator/allow.js +65 -0
  42. package/dist/orchestrator/attachments.d.ts +2 -0
  43. package/dist/orchestrator/attachments.js +11 -0
  44. package/dist/orchestrator/config.d.ts +4 -0
  45. package/dist/orchestrator/config.js +19 -0
  46. package/dist/orchestrator/db.d.ts +17 -0
  47. package/dist/orchestrator/db.js +12 -0
  48. package/dist/orchestrator/diagnostics.d.ts +9 -0
  49. package/dist/orchestrator/diagnostics.js +13 -0
  50. package/dist/orchestrator/environment.d.ts +3 -0
  51. package/dist/orchestrator/environment.js +30 -0
  52. package/dist/orchestrator/file-inspection.d.ts +3 -0
  53. package/dist/orchestrator/file-inspection.js +61 -0
  54. package/dist/orchestrator/format.d.ts +1 -0
  55. package/dist/orchestrator/format.js +6 -0
  56. package/dist/orchestrator/index.d.ts +30 -0
  57. package/dist/orchestrator/index.js +56 -0
  58. package/dist/orchestrator/live-message.d.ts +14 -0
  59. package/dist/orchestrator/live-message.js +100 -0
  60. package/dist/orchestrator/pipeline.d.ts +11 -0
  61. package/dist/orchestrator/pipeline.js +198 -0
  62. package/dist/orchestrator/registry.d.ts +13 -0
  63. package/dist/orchestrator/registry.js +74 -0
  64. package/dist/orchestrator/starters.d.ts +19 -0
  65. package/dist/orchestrator/starters.js +81 -0
  66. package/dist/orchestrator/suggestion-capabilities.d.ts +5 -0
  67. package/dist/orchestrator/suggestion-capabilities.js +33 -0
  68. package/dist/orchestrator/system-assembly.d.ts +7 -0
  69. package/dist/orchestrator/system-assembly.js +13 -0
  70. package/dist/orchestrator/system-capabilities.d.ts +2 -0
  71. package/dist/orchestrator/system-capabilities.js +63 -0
  72. package/dist/orchestrator/types.d.ts +144 -0
  73. package/dist/orchestrator/types.js +1 -0
  74. package/dist/part-errors.d.ts +3 -0
  75. package/dist/part-errors.js +6 -0
  76. package/dist/part-injection.d.ts +11 -3
  77. package/dist/part-injection.js +72 -92
  78. package/dist/part-ref.d.ts +13 -0
  79. package/dist/part-ref.js +109 -0
  80. package/dist/plugin-hooks.d.ts +1 -0
  81. package/dist/plugin-i18n.js +2 -1
  82. package/dist/plugin-runtime.d.ts +2 -2
  83. package/dist/plugin-runtime.js +21 -11
  84. package/dist/plugin-updates.d.ts +2 -2
  85. package/dist/plugin-updates.js +6 -12
  86. package/dist/public-url.js +2 -1
  87. package/dist/recompute-derived.d.ts +2 -1
  88. package/dist/recompute-derived.js +123 -26
  89. package/dist/records-api.d.ts +5 -1
  90. package/dist/records-api.js +47 -15
  91. package/dist/registry-context.d.ts +1 -1
  92. package/dist/registry-context.js +3 -0
  93. package/dist/rename-guard.d.ts +1 -0
  94. package/dist/rename-guard.js +9 -0
  95. package/dist/schema-sync.js +38 -1
  96. package/dist/search-index.js +1 -1
  97. package/dist/search-indexer.d.ts +1 -1
  98. package/dist/search-indexer.js +3 -3
  99. package/dist/settings-groups.d.ts +14 -0
  100. package/dist/settings-groups.js +25 -0
  101. package/dist/settings-write.d.ts +4 -4
  102. package/dist/settings-write.js +23 -26
  103. package/dist/system-settings.d.ts +3 -0
  104. package/dist/system-settings.js +27 -0
  105. package/dist/thread-store.d.ts +3 -2
  106. package/dist/thread-store.js +6 -5
  107. package/dist/trigger-queue.d.ts +33 -0
  108. package/dist/trigger-queue.js +432 -0
  109. package/dist/uploads.js +6 -3
  110. package/dist/value-compare.d.ts +1 -0
  111. package/dist/value-compare.js +7 -0
  112. package/package.json +17 -3
@@ -4,14 +4,22 @@ import { encodeJsonAt } from "./mutate.js";
4
4
  import { encodeTemporalAt, decodeTemporalAt } from "./temporal.js";
5
5
  import { chunk } from "./batch.js";
6
6
  import { selectRows } from "./read-rows.js";
7
- import { injectParts, applyStoredParts } from "./part-injection.js";
7
+ import { hasStoredPartWork } from "./part-injection.js";
8
+ import { changedFields, runTriggers } from "./trigger-queue.js";
9
+ import { getActiveRegistry } from "./registry-context.js";
8
10
  export function extendEntityName(e) {
9
11
  return `extend__${e.id}`;
10
12
  }
11
- function extendPartCtx(e, record) {
12
- return { record, settings: {}, global: {}, meta: { library: 'extend', shelf: e.id } };
13
+ function extendPartCtx(e, em, memo) {
14
+ return {
15
+ settings: {},
16
+ global: {},
17
+ meta: { library: 'extend', shelf: e.id },
18
+ ...(em ? { em } : {}),
19
+ ...(memo ? { memo } : {}),
20
+ };
13
21
  }
14
- export async function getExtendRecord(e, baseId, em) {
22
+ export async function getExtendRecord(e, baseId, em, _memo) {
15
23
  const fork = em ?? getEm().fork();
16
24
  const name = extendEntityName(e);
17
25
  const [flat] = (await selectRows(fork, name, { base_id: baseId }, { limit: 1 }));
@@ -19,8 +27,7 @@ export async function getExtendRecord(e, baseId, em) {
19
27
  return undefined;
20
28
  const decoded = nestEmbeddedAt(e.fields, decodeTemporalAt(e.fields, flat));
21
29
  const collections = await readAt(fork, name, e.fields, baseId);
22
- const record = { ...decoded, ...collections };
23
- return injectParts(e.fields, record, extendPartCtx(e, record));
30
+ return { ...decoded, ...collections };
24
31
  }
25
32
  export async function getExtendRecords(e, baseIds, em) {
26
33
  const out = new Map();
@@ -39,15 +46,45 @@ export async function getExtendRecords(e, baseIds, em) {
39
46
  for (const flat of flats) {
40
47
  const id = Number(flat.base_id);
41
48
  const decoded = nestEmbeddedAt(e.fields, decodeTemporalAt(e.fields, flat));
42
- const record = { ...decoded, ...(collectionsById.get(id) ?? {}) };
43
- out.set(id, await injectParts(e.fields, record, extendPartCtx(e, record)));
49
+ out.set(id, { ...decoded, ...(collectionsById.get(id) ?? {}) });
44
50
  }
45
51
  return out;
46
52
  }
47
- export async function upsertExtendRecord(em, e, baseId, data) {
53
+ async function storedExtendState(em, e, baseId) {
48
54
  const name = extendEntityName(e);
49
- const withParts = await applyStoredParts(e.fields, data, extendPartCtx(e, data));
50
- const { base, collections } = splitAt(e.fields, withParts);
55
+ const [flat] = (await selectRows(em, name, { base_id: baseId }, { limit: 1 }));
56
+ if (!flat)
57
+ return {};
58
+ const decoded = nestEmbeddedAt(e.fields, decodeTemporalAt(e.fields, flat));
59
+ return { ...decoded, ...(await readAt(em, name, e.fields, baseId)) };
60
+ }
61
+ export async function upsertExtendRecord(em, e, baseId, data, memo) {
62
+ const name = extendEntityName(e);
63
+ const needParts = hasStoredPartWork(e.fields);
64
+ let target;
65
+ let withParts;
66
+ if (needParts) {
67
+ const stored = await storedExtendState(em, e, baseId);
68
+ target = { ...stored, ...data };
69
+ const changed = changedFields(stored, target, e.fields);
70
+ const graph = getActiveRegistry().mutateGraph;
71
+ const seedAll = Object.keys(stored).length === 0;
72
+ withParts = await runTriggers(e.fields, target, changed, graph, `extend:${e.id}`, extendPartCtx(e, em, memo), {
73
+ seedAll,
74
+ });
75
+ }
76
+ else {
77
+ target = data;
78
+ withParts = data;
79
+ }
80
+ const write = {};
81
+ for (const [k, v] of Object.entries(withParts))
82
+ if (k in data || v !== target[k])
83
+ write[k] = v;
84
+ const { base, collections } = splitAt(e.fields, write);
85
+ for (const k of Object.keys(collections))
86
+ if (!(k in data))
87
+ delete collections[k];
51
88
  const flat = encodeJsonAt(e.fields, encodeTemporalAt(e.fields, flattenEmbeddedAt(e.fields, base)));
52
89
  await em.upsert(name, { base_id: baseId, ...flat });
53
90
  await writeAt(em, name, e.fields, baseId, collections);
@@ -87,38 +87,65 @@ export function dropUnchangedFileFields(m, input, stored) {
87
87
  }
88
88
  return out ?? input;
89
89
  }
90
+ function normalizeFileValue(raw, field, path, issues) {
91
+ const parsed = jsonValue(raw);
92
+ const isArray = Array.isArray(parsed);
93
+ const items = (isArray ? parsed : [parsed]);
94
+ const out = [];
95
+ let failed = false;
96
+ for (const it of items) {
97
+ if (typeof it !== 'object' || it === null || typeof it.name !== 'string') {
98
+ issues.push({ field, code: 'file_not_uploaded', path });
99
+ failed = true;
100
+ continue;
101
+ }
102
+ const resolved = resolveEntry(it);
103
+ if (!resolved) {
104
+ issues.push({ field, code: 'file_not_uploaded', params: { name: it.name }, path });
105
+ failed = true;
106
+ continue;
107
+ }
108
+ out.push(resolved);
109
+ }
110
+ if (failed)
111
+ return null;
112
+ return { value: isArray ? out : out[0] };
113
+ }
90
114
  function normalizeLevel(fields, data, path) {
91
115
  const issues = [];
92
116
  for (const [key, f] of fieldEntries(fields)) {
93
- if (f.prim !== 'file')
94
- continue;
95
117
  if (!(key in data))
96
118
  continue;
97
- const raw = data[key];
98
- if (raw == null || raw === '')
119
+ if (f.prim === 'file') {
120
+ const raw = data[key];
121
+ if (raw == null || raw === '')
122
+ continue;
123
+ const r = normalizeFileValue(raw, key, [...path, key], issues);
124
+ if (r)
125
+ data[key] = r.value;
126
+ continue;
127
+ }
128
+ if (!f.parts?.some((p) => p.meta.prim === 'file'))
99
129
  continue;
100
- const parsed = jsonValue(raw);
101
- const isArray = Array.isArray(parsed);
102
- const items = (isArray ? parsed : [parsed]);
103
- const out = [];
104
- let failed = false;
105
- for (const it of items) {
106
- if (typeof it !== 'object' || it === null || typeof it.name !== 'string') {
107
- issues.push({ field: key, code: 'file_not_uploaded', path: [...path, key] });
108
- failed = true;
130
+ const obj = jsonValue(data[key]);
131
+ if (obj == null || typeof obj !== 'object' || Array.isArray(obj))
132
+ continue;
133
+ const row = obj;
134
+ let touched = false;
135
+ for (const p of f.parts) {
136
+ if (p.meta.prim !== 'file')
137
+ continue;
138
+ const raw = row[p.role];
139
+ if (raw == null || raw === '')
109
140
  continue;
110
- }
111
- const resolved = resolveEntry(it);
112
- if (!resolved) {
113
- issues.push({ field: key, code: 'file_not_uploaded', params: { name: it.name }, path: [...path, key] });
114
- failed = true;
141
+ const r = normalizeFileValue(raw, key, [...path, key, p.role], issues);
142
+ if (!r)
115
143
  continue;
116
- }
117
- out.push(resolved);
144
+ row[p.role] = r.value;
145
+ touched = true;
118
146
  }
119
- if (failed)
120
- continue;
121
- data[key] = isArray ? out : out[0];
147
+ if (touched)
148
+ data[key] = row;
122
149
  }
123
150
  for (const c of collectionGroups(fields)) {
124
151
  const rows = data[c.key];
@@ -1,5 +1,5 @@
1
1
  import { textSearchKeys, titleKey, recordTitle, storageColumnsFor } from '@coffer-org/sdk/shelf';
2
- import { tokenize } from '@coffer-org/core/search';
2
+ import { tokenize } from '@coffer-org/sdk/search';
3
3
  import { getLogger } from '@coffer-org/sdk/logger';
4
4
  import { getEm } from "./db.js";
5
5
  import { rowMatch } from "./records-api.js";
@@ -0,0 +1,7 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ export interface HttpError {
3
+ code: number;
4
+ body: Record<string, unknown>;
5
+ }
6
+ export declare function httpErrorFor(e: unknown): HttpError | null;
7
+ export declare function registerErrorHandler(app: FastifyInstance): void;
@@ -0,0 +1,24 @@
1
+ import { ValidationError, NotFoundError } from "./mutate.js";
2
+ import { SingleShelfError } from "./records-api.js";
3
+ import { PartContractError } from "./part-errors.js";
4
+ export function httpErrorFor(e) {
5
+ if (e instanceof ValidationError)
6
+ return { code: 422, body: { error: 'validation', issues: e.issues } };
7
+ if (e instanceof SingleShelfError)
8
+ return { code: 409, body: { error: 'single_shelf', message: e.message } };
9
+ if (e instanceof NotFoundError)
10
+ return { code: 404, body: { error: 'not_found' } };
11
+ if (e instanceof PartContractError)
12
+ return { code: 500, body: { error: 'part_contract', message: e.message } };
13
+ return null;
14
+ }
15
+ export function registerErrorHandler(app) {
16
+ app.setErrorHandler((error, req, reply) => {
17
+ const mapped = httpErrorFor(error);
18
+ if (!mapped)
19
+ return reply.send(error);
20
+ if (mapped.code >= 500)
21
+ req.log.error(error);
22
+ return reply.code(mapped.code).send(mapped.body);
23
+ });
24
+ }
package/dist/index.js CHANGED
@@ -11,11 +11,13 @@ import fastifyStatic from '@fastify/static';
11
11
  import { shelfTableName } from "./entity-schema.js";
12
12
  import { getEm, closeDb } from "./db.js";
13
13
  import { recordCounts } from "./counts.js";
14
- import { ValidationError, NotFoundError } from "./mutate.js";
14
+ import { NotFoundError } from "./mutate.js";
15
+ import { httpErrorFor, registerErrorHandler } from "./http-errors.js";
15
16
  import { resolveEmbed } from "./embed.js";
16
17
  import { resolveWithinHome, filterSort, listDir } from "./fs-list.js";
17
18
  import { initPlugins, teardownPlugins, readDisabled, purgePluginData, getPluginSettings, getPlugins, } from "./plugin-runtime.js";
18
19
  import { registerPluginsApi } from "./plugins-api.js";
20
+ import { orchestratorDiagnostics } from "./orchestrator/index.js";
19
21
  import { registerPluginUserApi, registerPluginAdminApi } from "./plugin-user-api.js";
20
22
  import { registerAuthApi, resolveRequestUser, requireAdmin, PUBLIC_API_PATHS } from "./auth-api.js";
21
23
  import { registerMcpHttp } from "./mcp-http.js";
@@ -24,17 +26,18 @@ import { baseUrl } from "./public-url.js";
24
26
  import { discoverPluginAssets, discoverRuntime } from "./plugin-discovery.js";
25
27
  import { checkLatestVersion, resolveUpdateTarget, resolveAllUpdateTargets, resolveBaseTargets, resolveRuntimeTarget, runNpmInstall, } from "./plugin-updates.js";
26
28
  import { buildClientSchema } from "./schema-api.js";
27
- import { recordList, recordListPage, isPagedRequest, recordGet, recordCreate, recordUpdate, recordDelete, recordRestore, recordPurge, recordTrashList, MAX_PAGE, UnknownShelfError, ensureSingle, SingleShelfError, } from "./records-api.js";
29
+ import { recordList, recordListPage, isPagedRequest, recordGet, recordCreate, recordUpdate, recordDelete, recordRestore, recordPurge, recordTrashList, MAX_PAGE, UnknownShelfError, FilterError, ensureSingle, } from "./records-api.js";
28
30
  import { showcaseList, showcaseListAll, showcaseCount, UnknownShowcaseError } from "./showcase-api.js";
29
31
  import { getActiveRegistry } from "./registry-context.js";
30
32
  import { maskTree, preserveTree } from "./field-masking.js";
31
33
  import { writePluginSettings } from "./settings-write.js";
34
+ import { findSettingsGroup, settingsGroups } from "./settings-groups.js";
32
35
  import { rootLogger, getLogger } from "./log.js";
33
36
  import { uploadsDir } from "./uploads.js";
34
37
  import { mimeForName } from "./file-fields.js";
35
38
  import { verifyUploadTicket } from "./upload-ticket.js";
36
39
  import { setPluginState } from "./plugin-state.js";
37
- import { SEARCH_STATE_PLUGIN, SEARCH_CURSOR_KEY } from "./search-indexer.js";
40
+ import { SEARCH_STATE_NAMESPACE, SEARCH_CURSOR_KEY } from "./search-indexer.js";
38
41
  import { globalSearch } from "./global-search.js";
39
42
  import { applySmartRanking, getRecordActivity, markRecordViewed, setRecordFavorite } from "./record-activity.js";
40
43
  import { tracksRecordActivity } from "./record-activity-policy.js";
@@ -117,6 +120,7 @@ app.post('/api/embed', async (req, reply) => {
117
120
  return reply.code(422).send({ error: 'unresolved' });
118
121
  return reply.send(rec);
119
122
  });
123
+ registerErrorHandler(app);
120
124
  function key(library, shelf) {
121
125
  return `${library}/${shelf}`;
122
126
  }
@@ -125,13 +129,11 @@ async function guard(reply, fn) {
125
129
  return await fn();
126
130
  }
127
131
  catch (e) {
128
- if (e instanceof ValidationError)
129
- return reply.code(422).send({ error: 'validation', issues: e.issues });
130
- if (e instanceof SingleShelfError)
131
- return reply.code(409).send({ error: 'single_shelf', message: e.message });
132
- if (e instanceof NotFoundError)
133
- return reply.code(404).send({ error: 'not_found' });
134
- app.log.error(e);
132
+ const mapped = httpErrorFor(e);
133
+ if (!mapped || mapped.code >= 500)
134
+ app.log.error(e);
135
+ if (mapped)
136
+ return reply.code(mapped.code).send(mapped.body);
135
137
  return reply.code(500).send({ error: 'internal' });
136
138
  }
137
139
  }
@@ -191,8 +193,6 @@ app.patch('/api/plugins/:id', async (req, reply) => {
191
193
  const p = plugins.find((x) => x.id === id);
192
194
  if (!p)
193
195
  return reply.code(404).send({ error: 'unknown_plugin' });
194
- if (id === 'core')
195
- return reply.code(400).send({ error: 'core_required' });
196
196
  const disabled = await readDisabled();
197
197
  if (!enabled) {
198
198
  const dependents = plugins.filter((x) => !disabled.has(x.id) && x.id !== id && x.dependsOn.includes(id));
@@ -215,7 +215,7 @@ app.patch('/api/plugins/:id', async (req, reply) => {
215
215
  }
216
216
  await fork.flush();
217
217
  if (enabled) {
218
- await setPluginState(SEARCH_STATE_PLUGIN, SEARCH_CURSOR_KEY, '0');
218
+ await setPluginState(SEARCH_STATE_NAMESPACE, SEARCH_CURSOR_KEY, '0');
219
219
  }
220
220
  return { ok: true, restartRequired: true };
221
221
  });
@@ -227,8 +227,6 @@ app.delete('/api/plugins/:id/data', async (req, reply) => {
227
227
  const p = plugins.find((x) => x.id === id);
228
228
  if (!p)
229
229
  return reply.code(404).send({ error: 'unknown_plugin' });
230
- if (id === 'core')
231
- return reply.code(400).send({ error: 'core_required' });
232
230
  const disabled = await readDisabled();
233
231
  const dependents = plugins.filter((x) => !disabled.has(x.id) && x.id !== id && x.dependsOn.includes(id));
234
232
  if (dependents.length) {
@@ -286,14 +284,9 @@ app.post('/api/plugins/update-all', async (req, reply) => {
286
284
  app.post('/api/system/update', async (req, reply) => {
287
285
  if (!requireAdmin(req, reply))
288
286
  return;
289
- const assets = await discoverPluginAssets();
290
287
  const runtime = await discoverRuntime();
291
- const core = assets.find((a) => a.id === 'core');
292
- const [coreLatest, runtimeLatest] = await Promise.all([
293
- core && !core.local ? checkLatestVersion(core.packageName) : Promise.resolve(null),
294
- runtime && !runtime.local ? checkLatestVersion(runtime.packageName) : Promise.resolve(null),
295
- ]);
296
- const targets = resolveBaseTargets(assets, runtime, coreLatest, runtimeLatest);
288
+ const runtimeLatest = runtime && !runtime.local ? await checkLatestVersion(runtime.packageName) : null;
289
+ const targets = resolveBaseTargets(runtime, runtimeLatest);
297
290
  if (targets.length === 0)
298
291
  return reply.code(400).send({ error: 'no_update_available' });
299
292
  const result = await runNpmInstall(targets.map((t) => `${t.packageName}@${t.to}`), process.cwd());
@@ -303,29 +296,30 @@ app.post('/api/system/update', async (req, reply) => {
303
296
  reply.send({ ok: true, updated: targets.map(({ id, from, to }) => ({ id, from, to })) });
304
297
  setTimeout(() => process.exit(0), 500);
305
298
  });
306
- app.get('/api/plugins/:id/settings', async (req, reply) => {
299
+ app.post('/api/system/orchestrator-diagnostics', async (req, reply) => {
300
+ if (!requireAdmin(req, reply))
301
+ return;
302
+ reply.send(await orchestratorDiagnostics());
303
+ });
304
+ app.get('/api/settings', async () => (await settingsGroups()).map((g) => ({ id: g.id, label: g.label, fields: g.fields })));
305
+ app.get('/api/settings/:id', async (req, reply) => {
307
306
  if (!requireAdmin(req, reply))
308
307
  return;
309
308
  const { id } = req.params;
310
- const plugins = await getPlugins();
311
- const p = plugins.find((x) => x.id === id);
312
- if (!p || !p.settings || p.settings.fields.length === 0) {
309
+ const group = await findSettingsGroup(id);
310
+ if (!group)
313
311
  return reply.code(404).send({ error: 'no_settings' });
314
- }
315
- const row = await getPluginSettings(id);
316
- return maskTree(p.settings.fields, row);
312
+ return maskTree(group.fields, await getPluginSettings(id));
317
313
  });
318
- app.put('/api/plugins/:id/settings', (req, reply) => {
314
+ app.put('/api/settings/:id', (req, reply) => {
319
315
  if (!requireAdmin(req, reply))
320
316
  return;
321
317
  return guard(reply, async () => {
322
318
  const { id } = req.params;
323
- const plugins = await getPlugins();
324
- const p = plugins.find((x) => x.id === id);
325
- if (!p || !p.settings || p.settings.fields.length === 0) {
319
+ const group = await findSettingsGroup(id);
320
+ if (!group)
326
321
  return reply.code(404).send({ error: 'no_settings' });
327
- }
328
- const row = await writePluginSettings(getEm().fork(), id, (req.body ?? {}), req.user.login, plugins);
322
+ const row = await writePluginSettings(getEm().fork(), id, (req.body ?? {}), req.user.login);
329
323
  return { ok: true, row };
330
324
  });
331
325
  });
@@ -445,6 +439,8 @@ app.get('/api/:library/:shelf', async (req, reply) => {
445
439
  catch (e) {
446
440
  if (e instanceof UnknownShelfError)
447
441
  return reply.code(404).send({ error: 'unknown_shelf' });
442
+ if (e instanceof FilterError)
443
+ return reply.code(400).send({ error: 'bad_filter', message: e.message });
448
444
  throw e;
449
445
  }
450
446
  });
@@ -0,0 +1,24 @@
1
+ export declare class ConnectionError extends Error {
2
+ }
3
+ export declare class NotFoundError extends Error {
4
+ }
5
+ export declare class ValidationError extends Error {
6
+ issues: unknown[];
7
+ constructor(issues: unknown[]);
8
+ }
9
+ export interface ListQuery {
10
+ folderId?: string;
11
+ limit?: number;
12
+ [field: string]: string | number | undefined;
13
+ }
14
+ export interface CofferClientApi {
15
+ getSchema(): Promise<unknown>;
16
+ listRecords(library: string, shelf: string, query?: ListQuery): Promise<unknown>;
17
+ getRecord(library: string, shelf: string, id: number): Promise<unknown>;
18
+ createRecord(library: string, shelf: string, fields: Record<string, unknown>): Promise<unknown>;
19
+ updateRecord(library: string, shelf: string, id: number, fields: Record<string, unknown>): Promise<unknown>;
20
+ deleteRecord(library: string, shelf: string, id: number): Promise<unknown>;
21
+ listTrash(): Promise<unknown>;
22
+ restoreRecord(library: string, shelf: string, id: number): Promise<unknown>;
23
+ purgeRecord(library: string, shelf: string, id: number): Promise<unknown>;
24
+ }
@@ -0,0 +1,12 @@
1
+ export class ConnectionError extends Error {
2
+ }
3
+ export class NotFoundError extends Error {
4
+ }
5
+ export class ValidationError extends Error {
6
+ issues;
7
+ constructor(issues) {
8
+ super('validation');
9
+ this.issues = issues;
10
+ this.name = 'ValidationError';
11
+ }
12
+ }
@@ -0,0 +1,46 @@
1
+ export interface ShelfIndexEntry {
2
+ library: string;
3
+ libraryLabel: string;
4
+ shelf: string;
5
+ label: string;
6
+ }
7
+ export interface FieldInfo {
8
+ key: string;
9
+ kind: string;
10
+ prim: string;
11
+ required: boolean;
12
+ relation?: {
13
+ library: string;
14
+ shelf: string;
15
+ };
16
+ options?: unknown[];
17
+ fields?: FieldInfo[];
18
+ multiple?: true;
19
+ derived?: true;
20
+ write?: string;
21
+ }
22
+ export declare const FILE_WRITE_HINT: string;
23
+ export declare const SOURCE_WRITE_HINT: string;
24
+ export declare const DERIVED_WRITE_HINT: string;
25
+ export declare const serverOwnedPartsHint: (roles: string[]) => string;
26
+ export interface ShelfDescription {
27
+ library: string;
28
+ shelf: string;
29
+ label: string;
30
+ fields: FieldInfo[];
31
+ }
32
+ interface SchemaClient {
33
+ getSchema(): Promise<unknown>;
34
+ }
35
+ export declare function flattenFields(items: unknown[]): FieldInfo[];
36
+ export declare class SchemaCache {
37
+ private client;
38
+ private cached?;
39
+ constructor(client: SchemaClient);
40
+ refresh(): void;
41
+ private load;
42
+ index(): Promise<ShelfIndexEntry[]>;
43
+ describeShelf(library: string, shelf: string): Promise<ShelfDescription>;
44
+ private find;
45
+ }
46
+ export {};
@@ -0,0 +1,90 @@
1
+ export const FILE_WRITE_HINT = 'Uploaded file only: {"name":"<filename returned by POST /api/upload>"} ' +
2
+ '(or an array of those when the field is multiple). ' +
3
+ 'A remote URL is rejected — the server stores files, not links. ' +
4
+ 'To add one: call create_upload_ticket, POST the bytes to the absolute upload_url it returns with that ' +
5
+ 'Bearer token, then write the returned name. mime and size are set by the server; ' +
6
+ 'sending them changes nothing. To remove a file, drop its entry from the array ' +
7
+ '(or clear the field) — an existing entry cannot be edited in place.';
8
+ export const SOURCE_WRITE_HINT = 'plain text — write the full content as a string. Content must be valid UTF-8 text; binary content is rejected. ' +
9
+ 'Capped per field (default 256 KB) — content over the cap fails with source_too_large. ' +
10
+ 'Read-only in the web UI (people can only replace it by uploading a file).';
11
+ export const DERIVED_WRITE_HINT = 'Read-only — the server recomputes this field from other fields of the record on every write ' +
12
+ 'and rejects any value sent for it. To change it, change what it is computed from.';
13
+ export const serverOwnedPartsHint = (roles) => `Partly read-only — the server owns ${roles.map((r) => `\`${r}\``).join(', ')} in this value and ` +
14
+ `computes ${roles.length > 1 ? 'them' : 'it'} on every write. Send the other roles only; anything ` +
15
+ `sent for these is discarded, and a read returns the server's value.`;
16
+ function serverOwnedRoles(t) {
17
+ const parts = t.parts ?? [];
18
+ return parts.filter((p) => p.mode === 'computedStored').map((p) => p.role);
19
+ }
20
+ function hasValue(el) {
21
+ const type = el?.type;
22
+ return type !== undefined && typeof type === 'object';
23
+ }
24
+ function hasChildren(el) {
25
+ return typeof el === 'object' && el !== null && Array.isArray(el.fields);
26
+ }
27
+ function isStorageGroup(el) {
28
+ return el.scope === 'nest';
29
+ }
30
+ export function flattenFields(items) {
31
+ const out = [];
32
+ const push = (key, t) => out.push({
33
+ key,
34
+ kind: t.kind,
35
+ prim: t.prim,
36
+ required: t.required,
37
+ ...(t.relation ? { relation: t.relation } : {}),
38
+ ...(t.options ? { options: t.options } : {}),
39
+ ...(t.prim === 'file' ? { write: FILE_WRITE_HINT } : {}),
40
+ ...(serverOwnedRoles(t).length ? { write: serverOwnedPartsHint(serverOwnedRoles(t)) } : {}),
41
+ ...(t.kind === 'source' ? { write: SOURCE_WRITE_HINT } : {}),
42
+ ...(t.derived ? { derived: true, write: DERIVED_WRITE_HINT } : {}),
43
+ });
44
+ for (const it of items) {
45
+ if (hasValue(it) && 'key' in it)
46
+ push(it.key, it.type);
47
+ else if (hasChildren(it) && isStorageGroup(it))
48
+ out.push({
49
+ key: it.key,
50
+ kind: 'group',
51
+ prim: 'group',
52
+ required: it.required === true,
53
+ ...(it.multiple ? { multiple: true } : {}),
54
+ fields: flattenFields(it.fields),
55
+ });
56
+ else if (hasChildren(it))
57
+ out.push(...flattenFields(it.fields));
58
+ }
59
+ return out;
60
+ }
61
+ export class SchemaCache {
62
+ client;
63
+ cached;
64
+ constructor(client) {
65
+ this.client = client;
66
+ }
67
+ refresh() {
68
+ this.cached = undefined;
69
+ }
70
+ async load(force = false) {
71
+ if (force || !this.cached)
72
+ this.cached = (await this.client.getSchema());
73
+ return this.cached;
74
+ }
75
+ async index() {
76
+ const libraries = await this.load();
77
+ return libraries.flatMap((v) => v.shelves.map((m) => ({ library: v.id, libraryLabel: v.label, shelf: m.shelf, label: m.label })));
78
+ }
79
+ async describeShelf(library, shelf) {
80
+ let m = this.find(await this.load(), library, shelf);
81
+ if (!m)
82
+ m = this.find(await this.load(true), library, shelf);
83
+ if (!m)
84
+ throw new Error(`unknown_shelf ${library}/${shelf}`);
85
+ return { library, shelf, label: m.label, fields: flattenFields(m.fields) };
86
+ }
87
+ find(libraries, library, shelf) {
88
+ return libraries.find((v) => v.id === library)?.shelves.find((m) => m.shelf === shelf);
89
+ }
90
+ }
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod';
2
+ import { type CofferClientApi } from './client.ts';
3
+ import { SchemaCache } from './schema.ts';
4
+ export interface ToolResult {
5
+ content: {
6
+ type: 'text';
7
+ text: string;
8
+ }[];
9
+ isError?: boolean;
10
+ }
11
+ export interface ToolDef {
12
+ name: string;
13
+ description: string;
14
+ inputSchema: z.ZodRawShape;
15
+ handler: (args: Record<string, unknown>) => Promise<ToolResult>;
16
+ }
17
+ export declare const LIST_DEFAULT_LIMIT = 25;
18
+ export declare const LIST_MAX_LIMIT = 200;
19
+ export declare function buildTools(client: CofferClientApi, cache: SchemaCache): ToolDef[];