@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.
- package/dist/auth-api.js +4 -5
- package/dist/cache-clock.d.ts +8 -0
- package/dist/cache-clock.js +103 -0
- package/dist/cache-refresh.d.ts +7 -0
- package/dist/cache-refresh.js +322 -0
- package/dist/collection-io.d.ts +13 -0
- package/dist/collection-io.js +42 -28
- package/dist/compute-unit.d.ts +66 -0
- package/dist/compute-unit.js +369 -0
- package/dist/condition-where.js +1 -3
- package/dist/data-dir.d.ts +3 -0
- package/dist/data-dir.js +39 -0
- package/dist/db.js +2 -0
- package/dist/entity-schema.d.ts +1 -0
- package/dist/entity-schema.js +25 -2
- package/dist/extend-io.d.ts +3 -2
- package/dist/extend-io.js +4 -4
- package/dist/extend-table.d.ts +3 -2
- package/dist/extend-table.js +48 -11
- package/dist/file-fields.js +50 -23
- package/dist/global-search.js +1 -1
- package/dist/http-errors.d.ts +7 -0
- package/dist/http-errors.js +24 -0
- package/dist/index.js +31 -35
- package/dist/mcp-contract/client.d.ts +24 -0
- package/dist/mcp-contract/client.js +12 -0
- package/dist/mcp-contract/schema.d.ts +46 -0
- package/dist/mcp-contract/schema.js +90 -0
- package/dist/mcp-contract/tools.d.ts +19 -0
- package/dist/mcp-contract/tools.js +160 -0
- package/dist/mcp-local.d.ts +1 -1
- package/dist/mcp-local.js +4 -2
- package/dist/mcp-tools.d.ts +12 -1
- package/dist/mcp-tools.js +41 -8
- package/dist/migrations.d.ts +30 -2
- package/dist/migrations.js +368 -19
- package/dist/mutate.js +86 -57
- package/dist/orchestrator/agent-capabilities.d.ts +2 -0
- package/dist/orchestrator/agent-capabilities.js +201 -0
- package/dist/orchestrator/allow.d.ts +16 -0
- package/dist/orchestrator/allow.js +65 -0
- package/dist/orchestrator/attachments.d.ts +2 -0
- package/dist/orchestrator/attachments.js +11 -0
- package/dist/orchestrator/config.d.ts +4 -0
- package/dist/orchestrator/config.js +19 -0
- package/dist/orchestrator/db.d.ts +17 -0
- package/dist/orchestrator/db.js +12 -0
- package/dist/orchestrator/diagnostics.d.ts +9 -0
- package/dist/orchestrator/diagnostics.js +13 -0
- package/dist/orchestrator/environment.d.ts +3 -0
- package/dist/orchestrator/environment.js +30 -0
- package/dist/orchestrator/file-inspection.d.ts +3 -0
- package/dist/orchestrator/file-inspection.js +61 -0
- package/dist/orchestrator/format.d.ts +1 -0
- package/dist/orchestrator/format.js +6 -0
- package/dist/orchestrator/index.d.ts +30 -0
- package/dist/orchestrator/index.js +56 -0
- package/dist/orchestrator/live-message.d.ts +14 -0
- package/dist/orchestrator/live-message.js +100 -0
- package/dist/orchestrator/pipeline.d.ts +11 -0
- package/dist/orchestrator/pipeline.js +198 -0
- package/dist/orchestrator/registry.d.ts +13 -0
- package/dist/orchestrator/registry.js +74 -0
- package/dist/orchestrator/starters.d.ts +19 -0
- package/dist/orchestrator/starters.js +81 -0
- package/dist/orchestrator/suggestion-capabilities.d.ts +5 -0
- package/dist/orchestrator/suggestion-capabilities.js +33 -0
- package/dist/orchestrator/system-assembly.d.ts +7 -0
- package/dist/orchestrator/system-assembly.js +13 -0
- package/dist/orchestrator/system-capabilities.d.ts +2 -0
- package/dist/orchestrator/system-capabilities.js +63 -0
- package/dist/orchestrator/types.d.ts +144 -0
- package/dist/orchestrator/types.js +1 -0
- package/dist/part-errors.d.ts +3 -0
- package/dist/part-errors.js +6 -0
- package/dist/part-injection.d.ts +11 -3
- package/dist/part-injection.js +72 -92
- package/dist/part-ref.d.ts +13 -0
- package/dist/part-ref.js +109 -0
- package/dist/plugin-hooks.d.ts +1 -0
- package/dist/plugin-i18n.js +2 -1
- package/dist/plugin-runtime.d.ts +2 -2
- package/dist/plugin-runtime.js +21 -11
- package/dist/plugin-updates.d.ts +2 -2
- package/dist/plugin-updates.js +6 -12
- package/dist/public-url.js +2 -1
- package/dist/recompute-derived.d.ts +2 -1
- package/dist/recompute-derived.js +123 -26
- package/dist/records-api.d.ts +5 -1
- package/dist/records-api.js +47 -15
- package/dist/registry-context.d.ts +1 -1
- package/dist/registry-context.js +3 -0
- package/dist/rename-guard.d.ts +1 -0
- package/dist/rename-guard.js +9 -0
- package/dist/schema-sync.js +38 -1
- package/dist/search-index.js +1 -1
- package/dist/search-indexer.d.ts +1 -1
- package/dist/search-indexer.js +3 -3
- package/dist/settings-groups.d.ts +14 -0
- package/dist/settings-groups.js +25 -0
- package/dist/settings-write.d.ts +4 -4
- package/dist/settings-write.js +23 -26
- package/dist/system-settings.d.ts +3 -0
- package/dist/system-settings.js +27 -0
- package/dist/thread-store.d.ts +3 -2
- package/dist/thread-store.js +6 -5
- package/dist/trigger-queue.d.ts +33 -0
- package/dist/trigger-queue.js +432 -0
- package/dist/uploads.js +6 -3
- package/dist/value-compare.d.ts +1 -0
- package/dist/value-compare.js +7 -0
- package/package.json +17 -3
package/dist/extend-table.js
CHANGED
|
@@ -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 {
|
|
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,
|
|
12
|
-
return {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
+
async function storedExtendState(em, e, baseId) {
|
|
48
54
|
const name = extendEntityName(e);
|
|
49
|
-
const
|
|
50
|
-
|
|
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);
|
package/dist/file-fields.js
CHANGED
|
@@ -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
|
-
|
|
98
|
-
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
let
|
|
105
|
-
for (const
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
+
row[p.role] = r.value;
|
|
145
|
+
touched = true;
|
|
118
146
|
}
|
|
119
|
-
if (
|
|
120
|
-
|
|
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];
|
package/dist/global-search.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { textSearchKeys, titleKey, recordTitle, storageColumnsFor } from '@coffer-org/sdk/shelf';
|
|
2
|
-
import { tokenize } from '@coffer-org/
|
|
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 {
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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(
|
|
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
|
|
292
|
-
const
|
|
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.
|
|
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
|
|
311
|
-
|
|
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/
|
|
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
|
|
324
|
-
|
|
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[];
|