@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/mutate.js
CHANGED
|
@@ -1,34 +1,70 @@
|
|
|
1
1
|
import { serialize } from '@mikro-orm/core';
|
|
2
|
-
import { buildZodObject, buildZodObjectPartial, fieldEntries } from '@coffer-org/sdk/shelf';
|
|
2
|
+
import { buildZodObject, buildZodObjectPartial, fieldEntries, collectionGroups } from '@coffer-org/sdk/shelf';
|
|
3
3
|
import { isJsonStored, decodeVmsg } from '@coffer-org/sdk/fields';
|
|
4
4
|
import { resolveFlag } from '@coffer-org/sdk/condition';
|
|
5
|
-
import { applyDerived } from '@coffer-org/sdk/derive';
|
|
6
5
|
import { getEm } from "./db.js";
|
|
7
6
|
import { selectRows } from "./read-rows.js";
|
|
8
7
|
import { normalizeFileFields, dropUnchangedFileFields, touchesFileFields } from "./file-fields.js";
|
|
9
8
|
import { notifyRecordsChanged } from "./index-signal.js";
|
|
10
9
|
import { encodeTemporal, decodeTemporal } from "./temporal.js";
|
|
11
|
-
import { splitCollections, writeCollections, deleteCollections, flattenEmbedded, nestEmbedded, readCollections, } from "./collection-io.js";
|
|
12
|
-
import {
|
|
10
|
+
import { splitCollections, writeCollections, deleteCollections, flattenEmbedded, flattenEmbeddedAt, nestEmbedded, readCollections, readCollectionsWithMeta, } from "./collection-io.js";
|
|
11
|
+
import { stripClockKeysDeep, clockValuesOf } from "./cache-clock.js";
|
|
12
|
+
import { refreshCachedUnits } from "./cache-refresh.js";
|
|
13
|
+
import { hasStoredPartWork, newPartMemo } from "./part-injection.js";
|
|
14
|
+
import { changedFields, runTriggers } from "./trigger-queue.js";
|
|
15
|
+
import { getActiveRegistry } from "./registry-context.js";
|
|
16
|
+
import { sameValue } from "./value-compare.js";
|
|
13
17
|
const EMPTY_SETTINGS = {};
|
|
14
18
|
const EMPTY_GLOBAL = {};
|
|
15
19
|
function nowIso() {
|
|
16
20
|
return new Date().toISOString();
|
|
17
21
|
}
|
|
18
|
-
function partCtx(m,
|
|
22
|
+
function partCtx(m, memo, now, em) {
|
|
19
23
|
return {
|
|
20
|
-
record,
|
|
21
24
|
settings: EMPTY_SETTINGS,
|
|
22
25
|
global: EMPTY_GLOBAL,
|
|
23
26
|
meta: { library: m.library, shelf: m.shelf },
|
|
27
|
+
...(memo ? { memo } : {}),
|
|
28
|
+
...(em ? { em } : {}),
|
|
29
|
+
...(now !== undefined ? { now } : {}),
|
|
24
30
|
};
|
|
25
31
|
}
|
|
26
|
-
async function
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
async function driveTriggers(m, before, after, memo, now, em, opts = {}) {
|
|
33
|
+
const changed = changedFields(before, after, m.fields);
|
|
34
|
+
const graph = getActiveRegistry().mutateGraph;
|
|
35
|
+
const shelfKey = `${m.library}/${m.shelf}`;
|
|
36
|
+
const preTrigger = { ...after };
|
|
37
|
+
const touched = [];
|
|
38
|
+
const triggered = await runTriggers(m.fields, after, changed, graph, shelfKey, partCtx(m, memo, now, em), {
|
|
39
|
+
...opts,
|
|
40
|
+
touched,
|
|
41
|
+
});
|
|
42
|
+
Object.assign(after, triggered);
|
|
43
|
+
return { changed: changedFields(preTrigger, after, m.fields).map((c) => c.path.join('.')), touched };
|
|
44
|
+
}
|
|
45
|
+
async function persistUntouchedCollectionDiffs(tx, m, id, patchedKeys, before, beforeMeta, after) {
|
|
46
|
+
for (const c of collectionGroups(m.fields)) {
|
|
47
|
+
if (patchedKeys.has(c.key))
|
|
48
|
+
continue;
|
|
49
|
+
const beforeRows = before[c.key] ?? [];
|
|
50
|
+
const afterRows = after[c.key] ?? [];
|
|
51
|
+
const metas = beforeMeta[c.key] ?? [];
|
|
52
|
+
const childFields = c.group.fields;
|
|
53
|
+
const table = `${m.library}__${m.shelf}__${c.key}`;
|
|
54
|
+
const n = Math.min(beforeRows.length, afterRows.length, metas.length);
|
|
55
|
+
for (let i = 0; i < n; i++) {
|
|
56
|
+
const rowId = metas[i].id;
|
|
57
|
+
const flatBefore = encodeJsonAt(childFields, flattenEmbeddedAt(childFields, beforeRows[i]));
|
|
58
|
+
const flatAfter = encodeJsonAt(childFields, flattenEmbeddedAt(childFields, afterRows[i]));
|
|
59
|
+
const rowPatch = {};
|
|
60
|
+
for (const [k, v] of Object.entries(flatAfter)) {
|
|
61
|
+
if (!sameValue(flatBefore[k] ?? null, v ?? null))
|
|
62
|
+
rowPatch[k] = v ?? null;
|
|
63
|
+
}
|
|
64
|
+
if (Object.keys(rowPatch).length > 0)
|
|
65
|
+
await tx.nativeUpdate(table, { id: rowId }, rowPatch);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
32
68
|
}
|
|
33
69
|
export function encodeJsonAt(fields, data) {
|
|
34
70
|
const out = { ...data };
|
|
@@ -83,15 +119,11 @@ export async function getRecord(m, entityName, id, opts = {}) {
|
|
|
83
119
|
if (!row)
|
|
84
120
|
return undefined;
|
|
85
121
|
const flat = decodeTemporal(m, row);
|
|
122
|
+
const clocks = clockValuesOf(flat);
|
|
86
123
|
const nested = nestEmbedded(m, flat);
|
|
87
|
-
const collections = await
|
|
124
|
+
const { collections, meta } = await readCollectionsWithMeta(fork, m, id);
|
|
88
125
|
const record = { ...nested, ...collections };
|
|
89
|
-
return
|
|
90
|
-
record,
|
|
91
|
-
settings: EMPTY_SETTINGS,
|
|
92
|
-
global: EMPTY_GLOBAL,
|
|
93
|
-
meta: { library: m.library, shelf: m.shelf },
|
|
94
|
-
});
|
|
126
|
+
return refreshCachedUnits(m, entityName, record, clocks, meta, partCtx(m));
|
|
95
127
|
}
|
|
96
128
|
export async function createRecord(m, entityName, input, ctx, afterBase) {
|
|
97
129
|
const parsed = buildZodObject(m).safeParse(input);
|
|
@@ -102,7 +134,10 @@ export async function createRecord(m, entityName, input, ctx, afterBase) {
|
|
|
102
134
|
const fileIssues = normalizeFileFields(m, parsedData);
|
|
103
135
|
if (fileIssues.length)
|
|
104
136
|
throw new ValidationError(fileIssues);
|
|
105
|
-
|
|
137
|
+
const memo = newPartMemo();
|
|
138
|
+
const triggerResult = hasStoredPartWork(m.fields)
|
|
139
|
+
? await driveTriggers(m, {}, parsedData, memo, ts, undefined, { seedAll: true })
|
|
140
|
+
: { changed: [], touched: [] };
|
|
106
141
|
const { base, collections } = splitCollections(m, { ...parsedData });
|
|
107
142
|
const data = encodeJson(m, encodeTemporal(m, flattenEmbedded(m, { ...base, created_at: ts, updated_at: ts })));
|
|
108
143
|
let id;
|
|
@@ -113,26 +148,16 @@ export async function createRecord(m, entityName, input, ctx, afterBase) {
|
|
|
113
148
|
await tx.flush();
|
|
114
149
|
id = entity.id;
|
|
115
150
|
await writeCollections(tx, m, id, collections);
|
|
116
|
-
const derived = await applyDerived(m.fields, { ...parsedData, id });
|
|
117
|
-
if (Object.keys(derived).length) {
|
|
118
|
-
await tx.nativeUpdate(entityName, { id }, encodeJson(m, encodeTemporal(m, flattenEmbedded(m, derived))));
|
|
119
|
-
Object.assign(parsedData, derived);
|
|
120
|
-
}
|
|
121
151
|
const _extends = afterBase ? await afterBase(tx, id) : undefined;
|
|
122
|
-
writeEvent(tx, ctx.actor, 'create', `${m.library}/${m.shelf}`, id, null, {
|
|
123
|
-
...parsedData,
|
|
124
|
-
id,
|
|
125
|
-
...(_extends ? { _extends } : {}),
|
|
126
|
-
});
|
|
152
|
+
writeEvent(tx, ctx.actor, 'create', `${m.library}/${m.shelf}`, id, null, stripClockKeysDeep(m.fields, { ...parsedData, id, ...(_extends ? { _extends } : {}) }));
|
|
127
153
|
});
|
|
128
154
|
notifyRecordsChanged();
|
|
129
155
|
const record = { ...parsedData, id, created_at: ts, updated_at: ts };
|
|
130
|
-
return
|
|
131
|
-
record,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
});
|
|
156
|
+
return {
|
|
157
|
+
...stripClockKeysDeep(m.fields, record),
|
|
158
|
+
changed: triggerResult.changed,
|
|
159
|
+
...(triggerResult.touched.length ? { changedRecords: triggerResult.touched } : {}),
|
|
160
|
+
};
|
|
136
161
|
}
|
|
137
162
|
export async function updateRecord(m, entityName, id, input, ctx, afterBase) {
|
|
138
163
|
let patch = input;
|
|
@@ -147,7 +172,9 @@ export async function updateRecord(m, entityName, id, input, ctx, afterBase) {
|
|
|
147
172
|
const parsed = buildZodObjectPartial(m).safeParse(patch);
|
|
148
173
|
if (!parsed.success)
|
|
149
174
|
throw new ValidationError(parsed.error.issues.map(toIssue));
|
|
175
|
+
const memo = newPartMemo();
|
|
150
176
|
let result;
|
|
177
|
+
let triggerResult = { changed: [], touched: [] };
|
|
151
178
|
await getEm()
|
|
152
179
|
.fork()
|
|
153
180
|
.transactional(async (tx) => {
|
|
@@ -165,7 +192,7 @@ export async function updateRecord(m, entityName, id, input, ctx, afterBase) {
|
|
|
165
192
|
const merged = { ...existing, ...base };
|
|
166
193
|
const reqIssues = [];
|
|
167
194
|
for (const [key, f] of fieldEntries(m.fields)) {
|
|
168
|
-
if (f.
|
|
195
|
+
if (f.required === true)
|
|
169
196
|
continue;
|
|
170
197
|
if (key.includes('__'))
|
|
171
198
|
continue;
|
|
@@ -177,30 +204,37 @@ export async function updateRecord(m, entityName, id, input, ctx, afterBase) {
|
|
|
177
204
|
}
|
|
178
205
|
if (reqIssues.length)
|
|
179
206
|
throw new ValidationError(reqIssues);
|
|
180
|
-
const
|
|
181
|
-
|
|
207
|
+
const storedWithMeta = hasStoredPartWork(m.fields)
|
|
208
|
+
? await readCollectionsWithMeta(tx, m, id)
|
|
209
|
+
: { collections: {}, meta: {} };
|
|
210
|
+
const storedCollections = storedWithMeta.collections;
|
|
211
|
+
const target = { ...merged, ...storedCollections, ...collections };
|
|
212
|
+
if (hasStoredPartWork(m.fields)) {
|
|
213
|
+
const before = { ...existing, ...storedCollections };
|
|
214
|
+
triggerResult = await driveTriggers(m, before, target, memo, ts, tx);
|
|
182
215
|
const resplit = splitCollections(m, target);
|
|
183
216
|
Object.assign(merged, resplit.base);
|
|
184
|
-
Object.
|
|
217
|
+
const patchedKeys = new Set(Object.keys(collections));
|
|
218
|
+
for (const k of patchedKeys) {
|
|
219
|
+
const rows = resplit.collections[k];
|
|
220
|
+
if (rows)
|
|
221
|
+
collections[k] = rows;
|
|
222
|
+
}
|
|
223
|
+
await persistUntouchedCollectionDiffs(tx, m, id, patchedKeys, storedCollections, storedWithMeta.meta, resplit.collections);
|
|
185
224
|
}
|
|
186
225
|
const dbRow = encodeJson(m, encodeTemporal(m, flattenEmbedded(m, { ...merged, updated_at: ts })));
|
|
187
226
|
await tx.upsert(entityName, dbRow);
|
|
188
227
|
await writeCollections(tx, m, id, collections);
|
|
189
228
|
const allCollections = await readCollections(tx, m, id);
|
|
190
|
-
const derived = await applyDerived(m.fields, { ...merged, ...allCollections });
|
|
191
|
-
if (Object.keys(derived).length) {
|
|
192
|
-
await tx.nativeUpdate(entityName, { id }, encodeJson(m, encodeTemporal(m, flattenEmbedded(m, derived))));
|
|
193
|
-
Object.assign(merged, derived);
|
|
194
|
-
}
|
|
195
229
|
const _extends = afterBase ? await afterBase(tx, id) : undefined;
|
|
196
230
|
const after = { ...merged, ...allCollections, updated_at: ts, ...(_extends ? { _extends } : {}) };
|
|
231
|
+
stripClockKeysDeep(m.fields, after);
|
|
197
232
|
writeEvent(tx, ctx.actor, 'update', `${m.library}/${m.shelf}`, id, existing, after);
|
|
198
|
-
result =
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
});
|
|
233
|
+
result = {
|
|
234
|
+
...after,
|
|
235
|
+
changed: triggerResult.changed,
|
|
236
|
+
...(triggerResult.touched.length ? { changedRecords: triggerResult.touched } : {}),
|
|
237
|
+
};
|
|
204
238
|
});
|
|
205
239
|
notifyRecordsChanged();
|
|
206
240
|
return result;
|
|
@@ -235,12 +269,7 @@ export async function restoreRecord(m, entityName, id, ctx, afterBase) {
|
|
|
235
269
|
const _extends = afterBase ? await afterBase(tx, id) : undefined;
|
|
236
270
|
const restored = { ...nested, ...collections, ...(_extends ? { _extends } : {}) };
|
|
237
271
|
writeEvent(tx, ctx.actor, 'restore', `${m.library}/${m.shelf}`, id, null, restored);
|
|
238
|
-
result =
|
|
239
|
-
record: restored,
|
|
240
|
-
settings: EMPTY_SETTINGS,
|
|
241
|
-
global: EMPTY_GLOBAL,
|
|
242
|
-
meta: { library: m.library, shelf: m.shelf },
|
|
243
|
-
});
|
|
272
|
+
result = restored;
|
|
244
273
|
});
|
|
245
274
|
notifyRecordsChanged();
|
|
246
275
|
return result;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { collectMcpTools } from "../mcp-tools.js";
|
|
2
|
+
import { LocalClient } from "../mcp-local.js";
|
|
3
|
+
import { inspectUpload } from "./file-inspection.js";
|
|
4
|
+
const ok = (data) => ({
|
|
5
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
6
|
+
});
|
|
7
|
+
const fail = (text) => ({ content: [{ type: 'text', text }], isError: true });
|
|
8
|
+
function textOf(result) {
|
|
9
|
+
const block = result.content.find((b) => b.type === 'text');
|
|
10
|
+
return block?.type === 'text' ? block.text : '';
|
|
11
|
+
}
|
|
12
|
+
function resultData(result) {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(textOf(result));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function fieldType(items, key) {
|
|
21
|
+
for (const item of items) {
|
|
22
|
+
if (!item || typeof item !== 'object')
|
|
23
|
+
continue;
|
|
24
|
+
const raw = item;
|
|
25
|
+
if (raw.key === key && raw.type)
|
|
26
|
+
return raw.type;
|
|
27
|
+
if (raw.kind === 'group' && Array.isArray(raw.fields)) {
|
|
28
|
+
const nested = fieldType(raw.fields, key);
|
|
29
|
+
if (nested)
|
|
30
|
+
return nested;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
async function findFileField(library, shelf, key) {
|
|
36
|
+
const schema = (await new LocalClient().getSchema());
|
|
37
|
+
const lib = schema.find((item) => item.id === library);
|
|
38
|
+
const sh = lib?.shelves?.find((item) => item.shelf === shelf);
|
|
39
|
+
return sh?.fields ? fieldType(sh.fields, key) : undefined;
|
|
40
|
+
}
|
|
41
|
+
function parseStoredValue(value) {
|
|
42
|
+
if (typeof value !== 'string')
|
|
43
|
+
return value;
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(value);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function fileItems(value) {
|
|
52
|
+
const parsed = parseStoredValue(value);
|
|
53
|
+
if (Array.isArray(parsed))
|
|
54
|
+
return parsed.filter((item) => !!item && typeof item === 'object');
|
|
55
|
+
return parsed && typeof parsed === 'object' ? [parsed] : [];
|
|
56
|
+
}
|
|
57
|
+
function findTool(defs, name) {
|
|
58
|
+
return defs.find((def) => def.server === 'coffer' && def.bareName === name);
|
|
59
|
+
}
|
|
60
|
+
export async function makeAttachmentCapabilities(attachments) {
|
|
61
|
+
const defs = await collectMcpTools();
|
|
62
|
+
const getRecord = findTool(defs, 'get_record');
|
|
63
|
+
const updateRecord = findTool(defs, 'update_record');
|
|
64
|
+
const tools = [];
|
|
65
|
+
if (getRecord) {
|
|
66
|
+
tools.push({
|
|
67
|
+
name: 'mcp__coffer__inspect_record_file',
|
|
68
|
+
description: 'Read an existing file/image/PDF/text item stored in a Coffer record. ' +
|
|
69
|
+
'Call get_record first, then pass the exact file name from the field value. ' +
|
|
70
|
+
'Do not use local paths or URLs. If a field contains multiple files, file_name is required.',
|
|
71
|
+
inputSchema: {
|
|
72
|
+
library: { type: 'string', description: 'Library identifier, e.g. things' },
|
|
73
|
+
shelf: { type: 'string', description: 'Shelf identifier, e.g. item' },
|
|
74
|
+
id: { type: 'integer', description: 'Record id' },
|
|
75
|
+
field: { type: 'string', description: 'File/image/document field key' },
|
|
76
|
+
file_name: {
|
|
77
|
+
type: 'string',
|
|
78
|
+
description: 'Exact stored file name from get_record; required for multiple files',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
handler: async (args) => {
|
|
82
|
+
const library = String(args['library'] ?? '');
|
|
83
|
+
const shelf = String(args['shelf'] ?? '');
|
|
84
|
+
const id = Number(args['id']);
|
|
85
|
+
const field = String(args['field'] ?? '');
|
|
86
|
+
if (!library || !shelf || !field || !Number.isInteger(id))
|
|
87
|
+
return fail('library, shelf, field and integer id are required');
|
|
88
|
+
const type = await findFileField(library, shelf, field);
|
|
89
|
+
if (!type || type.prim !== 'file')
|
|
90
|
+
return fail(`Field is not a file field: ${library}/${shelf}.${field}`);
|
|
91
|
+
const currentResult = await getRecord.handler({ library, shelf, id });
|
|
92
|
+
if (currentResult.isError)
|
|
93
|
+
return currentResult;
|
|
94
|
+
const current = resultData(currentResult);
|
|
95
|
+
if (!current || typeof current !== 'object')
|
|
96
|
+
return fail(`Record not found: ${library}/${shelf}/${id}`);
|
|
97
|
+
const existing = fileItems(current[field]);
|
|
98
|
+
if (!existing.length)
|
|
99
|
+
return fail(`Record field is empty: ${library}/${shelf}/${id}.${field}`);
|
|
100
|
+
const requested = typeof args['file_name'] === 'string' ? args['file_name'] : '';
|
|
101
|
+
const item = requested
|
|
102
|
+
? existing.find((entry) => entry['name'] === requested)
|
|
103
|
+
: existing.length === 1
|
|
104
|
+
? existing[0]
|
|
105
|
+
: undefined;
|
|
106
|
+
if (!item || typeof item['name'] !== 'string') {
|
|
107
|
+
const available = existing
|
|
108
|
+
.map((entry) => String(entry['name'] ?? ''))
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.join(', ');
|
|
111
|
+
return fail(existing.length > 1
|
|
112
|
+
? `Multiple files are stored in this field; file_name is required. Available: ${available}`
|
|
113
|
+
: `Stored file was not found in record field: ${requested}`);
|
|
114
|
+
}
|
|
115
|
+
return inspectUpload(item['name'], {
|
|
116
|
+
...(typeof item['mime'] === 'string' ? { mime: item['mime'] } : {}),
|
|
117
|
+
...(typeof item['size'] === 'number' ? { size: item['size'] } : {}),
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (!attachments.length || !getRecord || !updateRecord)
|
|
123
|
+
return { tools };
|
|
124
|
+
const attach = {
|
|
125
|
+
name: 'mcp__coffer__attach_conversation_attachments',
|
|
126
|
+
description: 'Attach files already received in the current conversation to a file/image/document field in a Coffer record. ' +
|
|
127
|
+
'Use attachment_names for selected files, or omit it to attach every current-conversation attachment. ' +
|
|
128
|
+
'The default mode appends to a multiple field; use mode="replace" only when explicitly requested. ' +
|
|
129
|
+
'This is the unified attachment tool for every connector — never use a local path or invent an upload name.',
|
|
130
|
+
inputSchema: {
|
|
131
|
+
library: { type: 'string', description: 'Library identifier, e.g. things' },
|
|
132
|
+
shelf: { type: 'string', description: 'Shelf identifier, e.g. item' },
|
|
133
|
+
id: { type: 'integer', description: 'Record id' },
|
|
134
|
+
field: { type: 'string', description: 'File/image/document field key' },
|
|
135
|
+
attachment_names: {
|
|
136
|
+
type: 'array',
|
|
137
|
+
items: { type: 'string' },
|
|
138
|
+
description: 'Exact server-generated names from the current conversation; omit to use all attachments',
|
|
139
|
+
},
|
|
140
|
+
mode: {
|
|
141
|
+
type: 'string',
|
|
142
|
+
enum: ['append', 'replace'],
|
|
143
|
+
description: 'append by default; replace only on explicit request',
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
handler: async (args) => {
|
|
147
|
+
const library = String(args['library'] ?? '');
|
|
148
|
+
const shelf = String(args['shelf'] ?? '');
|
|
149
|
+
const id = Number(args['id']);
|
|
150
|
+
const field = String(args['field'] ?? '');
|
|
151
|
+
const mode = args['mode'] === 'replace' ? 'replace' : 'append';
|
|
152
|
+
if (!library || !shelf || !field || !Number.isInteger(id))
|
|
153
|
+
return fail('library, shelf, field and integer id are required');
|
|
154
|
+
const names = Array.isArray(args['attachment_names'])
|
|
155
|
+
? args['attachment_names'].filter((name) => typeof name === 'string')
|
|
156
|
+
: attachments.map((item) => item.name);
|
|
157
|
+
const uniqueNames = [...new Set(names)];
|
|
158
|
+
const refs = uniqueNames.map((name) => attachments.find((item) => item.name === name));
|
|
159
|
+
if (refs.some((ref) => !ref)) {
|
|
160
|
+
const missing = uniqueNames.filter((name) => !attachments.some((item) => item.name === name));
|
|
161
|
+
return fail(`Attachment is not available in the current conversation: ${missing.join(', ')}`);
|
|
162
|
+
}
|
|
163
|
+
if (!refs.length)
|
|
164
|
+
return fail('No conversation attachments were selected');
|
|
165
|
+
const type = await findFileField(library, shelf, field);
|
|
166
|
+
if (!type || type.prim !== 'file')
|
|
167
|
+
return fail(`Field is not a file field: ${library}/${shelf}.${field}`);
|
|
168
|
+
const multiple = type.hints?.multiple === true;
|
|
169
|
+
const currentResult = await getRecord.handler({ library, shelf, id });
|
|
170
|
+
if (currentResult.isError)
|
|
171
|
+
return currentResult;
|
|
172
|
+
const current = resultData(currentResult);
|
|
173
|
+
if (!current || typeof current !== 'object')
|
|
174
|
+
return fail(`Record not found: ${library}/${shelf}/${id}`);
|
|
175
|
+
const existing = fileItems(current[field]);
|
|
176
|
+
const incoming = refs.map((ref) => ({ name: ref.name }));
|
|
177
|
+
if (!multiple && mode === 'append' && existing.length)
|
|
178
|
+
return fail(`Field already contains a file; use mode="replace" to replace it`);
|
|
179
|
+
if (!multiple && incoming.length > 1)
|
|
180
|
+
return fail(`Field accepts one file; select one attachment or use a multiple field`);
|
|
181
|
+
const next = mode === 'replace'
|
|
182
|
+
? incoming
|
|
183
|
+
: [...existing, ...incoming.filter((item) => !existing.some((old) => old.name === item.name))];
|
|
184
|
+
const value = multiple ? next : (next[0] ?? null);
|
|
185
|
+
const updated = await updateRecord.handler({ library, shelf, id, fields: { [field]: value } });
|
|
186
|
+
if (updated.isError)
|
|
187
|
+
return updated;
|
|
188
|
+
return ok({
|
|
189
|
+
attached: incoming.map((item) => item.name),
|
|
190
|
+
mode,
|
|
191
|
+
library,
|
|
192
|
+
shelf,
|
|
193
|
+
id,
|
|
194
|
+
field,
|
|
195
|
+
record: resultData(updated),
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
tools.push(attach);
|
|
200
|
+
return { tools };
|
|
201
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { GatePolicy } from './types.ts';
|
|
2
|
+
export declare const stateDir: () => string;
|
|
3
|
+
export declare function carryLegacyState(): void;
|
|
4
|
+
export declare function allowFileFor(connectorId: string): string;
|
|
5
|
+
export interface AllowState {
|
|
6
|
+
ids: Record<string, number>;
|
|
7
|
+
}
|
|
8
|
+
export declare function emptyAllowed(): AllowState;
|
|
9
|
+
export declare function loadAllowed(file: string): AllowState;
|
|
10
|
+
export declare function saveAllowed(file: string, state: AllowState): void;
|
|
11
|
+
export declare function isAllowed(state: AllowState, id: string | number | null | undefined): boolean;
|
|
12
|
+
export declare function addAllowed(state: AllowState, id: string | number, now: number): AllowState;
|
|
13
|
+
export declare function isSenderAllowed(connectorId: string, senderId: string, deps?: {
|
|
14
|
+
policy?: GatePolicy;
|
|
15
|
+
}): Promise<boolean>;
|
|
16
|
+
export declare function makeThrottle(maxAttempts?: number, windowMs?: number): (id: string | number, now?: number) => boolean;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loadGatePolicy } from "./config.js";
|
|
4
|
+
import { carryInto, dataDir } from "../data-dir.js";
|
|
5
|
+
export const stateDir = () => process.env['ORCHESTRATOR_STATE_DIR'] ?? path.join(dataDir(), 'state');
|
|
6
|
+
function legacyStateDirs() {
|
|
7
|
+
return [
|
|
8
|
+
path.join(process.cwd(), 'packages', 'plugin-orchestrator', 'runtime', 'state'),
|
|
9
|
+
path.join(process.cwd(), 'node_modules', '@coffer-org', 'plugin-orchestrator', 'runtime', 'state'),
|
|
10
|
+
];
|
|
11
|
+
}
|
|
12
|
+
export function carryLegacyState() {
|
|
13
|
+
carryInto(stateDir(), ...legacyStateDirs());
|
|
14
|
+
}
|
|
15
|
+
export function allowFileFor(connectorId) {
|
|
16
|
+
return path.join(stateDir(), `${connectorId}.allowed.json`);
|
|
17
|
+
}
|
|
18
|
+
export function emptyAllowed() {
|
|
19
|
+
return { ids: {} };
|
|
20
|
+
}
|
|
21
|
+
export function loadAllowed(file) {
|
|
22
|
+
try {
|
|
23
|
+
const obj = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
24
|
+
if (obj && typeof obj === 'object' && 'ids' in obj && typeof obj.ids === 'object') {
|
|
25
|
+
return obj;
|
|
26
|
+
}
|
|
27
|
+
return emptyAllowed();
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return emptyAllowed();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function saveAllowed(file, state) {
|
|
34
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
35
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
36
|
+
fs.writeFileSync(tmp, JSON.stringify(state), 'utf-8');
|
|
37
|
+
fs.renameSync(tmp, file);
|
|
38
|
+
}
|
|
39
|
+
export function isAllowed(state, id) {
|
|
40
|
+
return id != null && Object.prototype.hasOwnProperty.call(state.ids, String(id));
|
|
41
|
+
}
|
|
42
|
+
export function addAllowed(state, id, now) {
|
|
43
|
+
return { ids: { ...state.ids, [String(id)]: now } };
|
|
44
|
+
}
|
|
45
|
+
export async function isSenderAllowed(connectorId, senderId, deps) {
|
|
46
|
+
const policy = deps?.policy ?? (await loadGatePolicy());
|
|
47
|
+
if (!policy.accessPassword)
|
|
48
|
+
return true;
|
|
49
|
+
return isAllowed(loadAllowed(allowFileFor(connectorId)), senderId);
|
|
50
|
+
}
|
|
51
|
+
export function makeThrottle(maxAttempts = 5, windowMs = 60_000) {
|
|
52
|
+
const hits = new Map();
|
|
53
|
+
return function allowAttempt(id, now = Date.now()) {
|
|
54
|
+
const key = String(id);
|
|
55
|
+
const rec = hits.get(key);
|
|
56
|
+
if (!rec || now - rec.start >= windowMs) {
|
|
57
|
+
hits.set(key, { start: now, count: 1 });
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
if (rec.count >= maxAttempts)
|
|
61
|
+
return false;
|
|
62
|
+
rec.count += 1;
|
|
63
|
+
return true;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { saveUploadBytes } from "../uploads.js";
|
|
2
|
+
export const attachmentMaterializer = {
|
|
3
|
+
async store(bytes, opts = {}) {
|
|
4
|
+
const stored = await saveUploadBytes(bytes, opts);
|
|
5
|
+
return {
|
|
6
|
+
name: stored.name,
|
|
7
|
+
...(stored.mime ? { mime: stored.mime } : {}),
|
|
8
|
+
size: stored.size,
|
|
9
|
+
};
|
|
10
|
+
},
|
|
11
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { SYSTEM_SETTINGS_ID } from "../system-settings.js";
|
|
2
|
+
export function buildPolicy(dbSettings = {}) {
|
|
3
|
+
const db = dbSettings;
|
|
4
|
+
return {
|
|
5
|
+
...(typeof db.agent_id === 'string' && db.agent_id ? { agentId: db.agent_id } : {}),
|
|
6
|
+
accessPassword: db.access_password ?? '',
|
|
7
|
+
triggerPrefix: db.trigger_prefix ?? '',
|
|
8
|
+
replyWindow: Number(db.reply_window ?? 1800) || 1800,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export async function loadAgentId() {
|
|
12
|
+
const { getPluginSettings } = await import("../plugin-runtime.js");
|
|
13
|
+
const value = (await getPluginSettings(SYSTEM_SETTINGS_ID))['agent_id'];
|
|
14
|
+
return typeof value === 'string' && value ? value : undefined;
|
|
15
|
+
}
|
|
16
|
+
export async function loadGatePolicy() {
|
|
17
|
+
const { getPluginSettings } = await import("../plugin-runtime.js");
|
|
18
|
+
return buildPolicy(await getPluginSettings(SYSTEM_SETTINGS_ID));
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type LogTurn = {
|
|
2
|
+
connector: string;
|
|
3
|
+
chatId: string;
|
|
4
|
+
userId: string;
|
|
5
|
+
role: 'user' | 'assistant';
|
|
6
|
+
text: string;
|
|
7
|
+
tokensIn: number | null;
|
|
8
|
+
tokensOut: number | null;
|
|
9
|
+
ms: number | null;
|
|
10
|
+
agentId: string | null;
|
|
11
|
+
presetId: string | null;
|
|
12
|
+
};
|
|
13
|
+
export interface LogDb {
|
|
14
|
+
logTurn(row: LogTurn): void;
|
|
15
|
+
close(): void;
|
|
16
|
+
}
|
|
17
|
+
export declare function openLogDb(): LogDb;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { logMessage } from "../msg-log.js";
|
|
2
|
+
import { getLogger } from '@coffer-org/sdk/logger';
|
|
3
|
+
const log = getLogger('orchestrator');
|
|
4
|
+
export function openLogDb() {
|
|
5
|
+
return {
|
|
6
|
+
logTurn(row) {
|
|
7
|
+
logMessage(row).catch((e) => log.error('logTurn failed', e));
|
|
8
|
+
},
|
|
9
|
+
close() {
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface DiagnosticEvent {
|
|
2
|
+
ts: string;
|
|
3
|
+
level: 'error' | 'warn';
|
|
4
|
+
source: string;
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function recordDiagnostic(level: DiagnosticEvent['level'], source: string, message: string): void;
|
|
8
|
+
export declare function listDiagnostics(limit?: number): DiagnosticEvent[];
|
|
9
|
+
export declare function clearDiagnostics(): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const MAX_EVENTS = 100;
|
|
2
|
+
const events = [];
|
|
3
|
+
export function recordDiagnostic(level, source, message) {
|
|
4
|
+
events.unshift({ ts: new Date().toISOString(), level, source, message: message.slice(0, 500) });
|
|
5
|
+
if (events.length > MAX_EVENTS)
|
|
6
|
+
events.length = MAX_EVENTS;
|
|
7
|
+
}
|
|
8
|
+
export function listDiagnostics(limit = 25) {
|
|
9
|
+
return events.slice(0, Math.max(1, Math.min(100, Math.trunc(limit))));
|
|
10
|
+
}
|
|
11
|
+
export function clearDiagnostics() {
|
|
12
|
+
events.length = 0;
|
|
13
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function systemTimeZone() {
|
|
2
|
+
try {
|
|
3
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
4
|
+
}
|
|
5
|
+
catch {
|
|
6
|
+
return 'UTC';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function part(now, timeZone, locale, options) {
|
|
10
|
+
return new Intl.DateTimeFormat(locale, { ...options, timeZone }).format(now);
|
|
11
|
+
}
|
|
12
|
+
export function todayDateString(now = new Date(), timeZone = systemTimeZone()) {
|
|
13
|
+
return part(now, timeZone, 'en-CA', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
14
|
+
}
|
|
15
|
+
export function currentEnvironment(now = new Date(), timeZone = systemTimeZone()) {
|
|
16
|
+
let zone = timeZone;
|
|
17
|
+
try {
|
|
18
|
+
new Intl.DateTimeFormat('en-CA', { timeZone: zone }).format(now);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
zone = 'UTC';
|
|
22
|
+
}
|
|
23
|
+
const date = part(now, zone, 'en-CA', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
24
|
+
const weekday = part(now, zone, 'en-US', { weekday: 'long' });
|
|
25
|
+
const time = part(now, zone, 'en-GB', { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
26
|
+
return [
|
|
27
|
+
`Current moment, refreshed on every turn: ${date} (${weekday}), ${time}, timezone ${zone}.`,
|
|
28
|
+
'Resolve every relative date against this value — "today", "tomorrow", "this week", ages, and expiry, warranty or due-date checks. Never infer the date from your training data and never tell the user you cannot know it.',
|
|
29
|
+
].join('\n');
|
|
30
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { AgentToolContentResult, AttachmentRef } from './types.ts';
|
|
2
|
+
export declare function inspectUpload(name: string, ref?: Pick<AttachmentRef, 'mime' | 'size' | 'label'>): Promise<AgentToolContentResult>;
|
|
3
|
+
export declare function isAgentToolContentResult(value: unknown): value is AgentToolContentResult;
|