@coffer-org/server 2.6.1 → 3.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 +17 -3
- package/dist/auth-store.js +7 -1
- package/dist/background-scheduler.js +20 -6
- package/dist/collection-io.js +5 -2
- package/dist/embed-openai.js +2 -1
- package/dist/embeddings.js +11 -3
- package/dist/entity-schema.d.ts +1 -0
- package/dist/entity-schema.js +28 -4
- package/dist/extend-io.js +6 -1
- package/dist/extend-table.js +9 -4
- package/dist/file-fields.d.ts +2 -0
- package/dist/file-fields.js +25 -6
- package/dist/index.js +4 -1
- package/dist/mcp-http.test-helpers.js +4 -1
- package/dist/mcp-tools.js +3 -5
- package/dist/msg-log.d.ts +2 -0
- package/dist/msg-log.js +21 -3
- package/dist/mutate.d.ts +3 -1
- package/dist/mutate.js +17 -4
- package/dist/oauth-api.js +25 -10
- package/dist/plugin-i18n.d.ts +6 -0
- package/dist/plugin-i18n.js +38 -0
- package/dist/rag-search.js +1 -2
- package/dist/recompute-derived.d.ts +6 -0
- package/dist/recompute-derived.js +40 -0
- package/dist/records-api.js +3 -1
- package/dist/search-index.js +2 -8
- package/dist/temporal.d.ts +5 -2
- package/dist/temporal.js +8 -6
- package/dist/thread-state.d.ts +8 -0
- package/dist/thread-state.js +39 -0
- package/dist/thread-store.js +3 -1
- package/dist/uploads.js +1 -1
- package/package.json +3 -3
package/dist/auth-api.js
CHANGED
|
@@ -75,9 +75,18 @@ export async function registerAuthApi(app) {
|
|
|
75
75
|
return reply.code(409).send({ error: 'already_setup' });
|
|
76
76
|
const body = (req.body ?? {});
|
|
77
77
|
if (!body.login || !body.password || !body.displayName) {
|
|
78
|
-
return reply
|
|
78
|
+
return reply
|
|
79
|
+
.code(422)
|
|
80
|
+
.send({
|
|
81
|
+
issues: [{ field: !body.login ? 'login' : !body.password ? 'password' : 'displayName', code: 'required' }],
|
|
82
|
+
});
|
|
79
83
|
}
|
|
80
|
-
const user = await createUser({
|
|
84
|
+
const user = await createUser({
|
|
85
|
+
login: body.login,
|
|
86
|
+
password: body.password,
|
|
87
|
+
displayName: body.displayName,
|
|
88
|
+
role: 'admin',
|
|
89
|
+
});
|
|
81
90
|
const { raw } = await createSession(user.id, SESSION_TTL_MS);
|
|
82
91
|
setCookie(reply, raw, SESSION_TTL_MS / 1000);
|
|
83
92
|
return publicUser(user);
|
|
@@ -129,7 +138,12 @@ export async function registerAuthApi(app) {
|
|
|
129
138
|
if (!body.login || !body.password || !body.displayName || (body.role !== 'admin' && body.role !== 'member')) {
|
|
130
139
|
return reply.code(422).send({ issues: [{ field: 'login', code: 'required' }] });
|
|
131
140
|
}
|
|
132
|
-
const user = await createUser({
|
|
141
|
+
const user = await createUser({
|
|
142
|
+
login: body.login,
|
|
143
|
+
password: body.password,
|
|
144
|
+
displayName: body.displayName,
|
|
145
|
+
role: body.role,
|
|
146
|
+
});
|
|
133
147
|
return publicUser(user);
|
|
134
148
|
});
|
|
135
149
|
app.patch('/api/auth/users/:id', async (req, reply) => {
|
package/dist/auth-store.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { getEm } from "./db.js";
|
|
2
2
|
import { hashPassword, generateToken, hashToken } from "./auth-crypto.js";
|
|
3
3
|
function toAuthUser(row) {
|
|
4
|
-
return {
|
|
4
|
+
return {
|
|
5
|
+
id: row.id,
|
|
6
|
+
login: row.login,
|
|
7
|
+
displayName: row.display_name,
|
|
8
|
+
role: row.role,
|
|
9
|
+
disabled: Boolean(row.disabled),
|
|
10
|
+
};
|
|
5
11
|
}
|
|
6
12
|
export async function countUsers() {
|
|
7
13
|
const em = getEm().fork();
|
|
@@ -22,7 +22,10 @@ export function makeScheduler(opts = {}) {
|
|
|
22
22
|
function arm(cb, ms) {
|
|
23
23
|
if (stopped)
|
|
24
24
|
return;
|
|
25
|
-
const h = setTimer(() => {
|
|
25
|
+
const h = setTimer(() => {
|
|
26
|
+
timers.delete(h);
|
|
27
|
+
cb();
|
|
28
|
+
}, ms);
|
|
26
29
|
timers.add(h);
|
|
27
30
|
}
|
|
28
31
|
function enqueue(task) {
|
|
@@ -51,7 +54,11 @@ export function makeScheduler(opts = {}) {
|
|
|
51
54
|
const timeoutMs = task.timeoutMs ?? defaultTimeoutMs;
|
|
52
55
|
let to;
|
|
53
56
|
const timeout = new Promise((resolve) => {
|
|
54
|
-
to = setTimer(() => {
|
|
57
|
+
to = setTimer(() => {
|
|
58
|
+
timers.delete(to);
|
|
59
|
+
log.warn(`${task.name}: timed out after ${timeoutMs}ms`);
|
|
60
|
+
resolve();
|
|
61
|
+
}, timeoutMs);
|
|
55
62
|
timers.add(to);
|
|
56
63
|
});
|
|
57
64
|
try {
|
|
@@ -62,7 +69,10 @@ export function makeScheduler(opts = {}) {
|
|
|
62
69
|
catch (e) {
|
|
63
70
|
runPromise = Promise.reject(e);
|
|
64
71
|
}
|
|
65
|
-
await Promise.race([
|
|
72
|
+
await Promise.race([
|
|
73
|
+
runPromise.catch((e) => log.warn(`${task.name}: ${e.message}`)),
|
|
74
|
+
timeout,
|
|
75
|
+
]);
|
|
66
76
|
}
|
|
67
77
|
finally {
|
|
68
78
|
clearTimer(to);
|
|
@@ -70,13 +80,17 @@ export function makeScheduler(opts = {}) {
|
|
|
70
80
|
}
|
|
71
81
|
}
|
|
72
82
|
return {
|
|
73
|
-
register(task) {
|
|
83
|
+
register(task) {
|
|
84
|
+
tasks.push(task);
|
|
85
|
+
},
|
|
74
86
|
start() {
|
|
75
87
|
if (started || stopped)
|
|
76
88
|
return;
|
|
77
89
|
started = true;
|
|
78
|
-
arm(() => {
|
|
79
|
-
|
|
90
|
+
arm(() => {
|
|
91
|
+
for (const t of tasks)
|
|
92
|
+
enqueue(t);
|
|
93
|
+
}, startupDelayMs);
|
|
80
94
|
},
|
|
81
95
|
stop() {
|
|
82
96
|
stopped = true;
|
package/dist/collection-io.js
CHANGED
|
@@ -47,14 +47,17 @@ export async function writeAt(tx, prefix, fields, parentId, collections) {
|
|
|
47
47
|
await deleteAt(tx, table, childFields, ex.id);
|
|
48
48
|
await tx.nativeDelete(table, { parent_id: parentId });
|
|
49
49
|
const sKeys = scalarKeys(childFields);
|
|
50
|
-
const jsonKeys = new Set(fieldEntries(childFields)
|
|
50
|
+
const jsonKeys = new Set(fieldEntries(childFields)
|
|
51
|
+
.filter(([, fm]) => isJsonStored(fm))
|
|
52
|
+
.map(([k]) => k));
|
|
51
53
|
let position = 0;
|
|
52
54
|
for (const r of rows) {
|
|
53
55
|
const flatR = flattenEmbeddedAt(childFields, r);
|
|
54
56
|
const picked = {};
|
|
55
57
|
for (const k of sKeys)
|
|
56
58
|
if (flatR[k] !== undefined)
|
|
57
|
-
picked[k] =
|
|
59
|
+
picked[k] =
|
|
60
|
+
jsonKeys.has(k) && flatR[k] !== null && typeof flatR[k] === 'object' ? JSON.stringify(flatR[k]) : flatR[k];
|
|
58
61
|
const child = tx.create(table, { parent_id: parentId, position, ...picked });
|
|
59
62
|
await tx.flush();
|
|
60
63
|
const childId = child.id;
|
package/dist/embed-openai.js
CHANGED
|
@@ -29,7 +29,8 @@ function parseErrorBody(body) {
|
|
|
29
29
|
}
|
|
30
30
|
function classifyEmbeddingFailure(status, code, detail) {
|
|
31
31
|
const marker = `${code ?? ''} ${detail ?? ''}`.toLowerCase();
|
|
32
|
-
if (status === 402 ||
|
|
32
|
+
if (status === 402 ||
|
|
33
|
+
/insufficient_quota|billing|payment_required|payment required|hard.?limit|quota exceeded/.test(marker))
|
|
33
34
|
return 'billing';
|
|
34
35
|
if (status === 401 || status === 403)
|
|
35
36
|
return 'auth';
|
package/dist/embeddings.js
CHANGED
|
@@ -12,7 +12,10 @@ async function loadCache() {
|
|
|
12
12
|
const em = getEm().fork();
|
|
13
13
|
const rows = (await em.find('_Embedding', {}));
|
|
14
14
|
cache = rows.map((r) => ({
|
|
15
|
-
shelfKey: r.shelf_key,
|
|
15
|
+
shelfKey: r.shelf_key,
|
|
16
|
+
recordId: r.record_id,
|
|
17
|
+
snippet: r.snippet,
|
|
18
|
+
full: decodeVector(r.vector),
|
|
16
19
|
}));
|
|
17
20
|
return cache;
|
|
18
21
|
}
|
|
@@ -30,7 +33,9 @@ export async function migrateEmbeddingVectorsToBlob() {
|
|
|
30
33
|
const em = getEm().fork();
|
|
31
34
|
if (dialectOf(em) !== 'sqlite')
|
|
32
35
|
return 0;
|
|
33
|
-
const [{ n }] = (await em
|
|
36
|
+
const [{ n }] = (await em
|
|
37
|
+
.getConnection()
|
|
38
|
+
.execute("SELECT count(*) AS n FROM _embeddings WHERE typeof(vector) = 'text'"));
|
|
34
39
|
if (n === 0)
|
|
35
40
|
return 0;
|
|
36
41
|
const rows = (await em.find('_Embedding', {}));
|
|
@@ -105,6 +110,9 @@ export async function searchEmbeddings(queryVec, k, opts = {}) {
|
|
|
105
110
|
}
|
|
106
111
|
export async function listEventsSince(lastId, limit) {
|
|
107
112
|
const em = getEm().fork();
|
|
108
|
-
const rows = (await em.find('_Event', { id: { $gt: lastId } }, {
|
|
113
|
+
const rows = (await em.find('_Event', { id: { $gt: lastId } }, {
|
|
114
|
+
orderBy: { id: 'ASC' },
|
|
115
|
+
limit,
|
|
116
|
+
}));
|
|
109
117
|
return rows;
|
|
110
118
|
}
|
package/dist/entity-schema.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export declare const EmbeddingSchema: EntitySchema<any, never, import("@mikro-or
|
|
|
18
18
|
export declare const PluginStateSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
|
|
19
19
|
export declare const RecordActivitySchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
|
|
20
20
|
export declare const ThreadMessageSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
|
|
21
|
+
export declare const ThreadStateSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
|
|
21
22
|
export declare function buildPluginEntities(plugins: PluginManifest[]): EntitySchema[];
|
|
22
23
|
export declare const MsgLogSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
|
|
23
24
|
export declare const UserSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
|
package/dist/entity-schema.js
CHANGED
|
@@ -180,6 +180,17 @@ export const ThreadMessageSchema = new EntitySchema({
|
|
|
180
180
|
reply_to_id: { type: 'text', nullable: true },
|
|
181
181
|
},
|
|
182
182
|
});
|
|
183
|
+
export const ThreadStateSchema = new EntitySchema({
|
|
184
|
+
name: '_ThreadState',
|
|
185
|
+
tableName: '_thread_state',
|
|
186
|
+
properties: {
|
|
187
|
+
connector: { type: 'text', primary: true },
|
|
188
|
+
chat_id: { type: 'text', primary: true },
|
|
189
|
+
agent_id: { type: 'text', nullable: true },
|
|
190
|
+
preset_id: { type: 'text', nullable: true },
|
|
191
|
+
updated_at: { type: 'text' },
|
|
192
|
+
},
|
|
193
|
+
});
|
|
183
194
|
export function buildPluginEntities(plugins) {
|
|
184
195
|
return plugins.flatMap((p) => [
|
|
185
196
|
...(p.libraries ?? []).flatMap((v) => v.shelves.flatMap((m) => [buildEntitySchema(m), ...buildCollectionEntities(m)])),
|
|
@@ -202,6 +213,8 @@ export const MsgLogSchema = new EntitySchema({
|
|
|
202
213
|
tokens_in: { type: 'integer', nullable: true },
|
|
203
214
|
tokens_out: { type: 'integer', nullable: true },
|
|
204
215
|
ms: { type: 'integer', nullable: true },
|
|
216
|
+
agent_id: { type: 'text', nullable: true },
|
|
217
|
+
preset_id: { type: 'text', nullable: true },
|
|
205
218
|
},
|
|
206
219
|
});
|
|
207
220
|
export const UserSchema = new EntitySchema({
|
|
@@ -280,9 +293,20 @@ export const OAuthTokenSchema = new EntitySchema({
|
|
|
280
293
|
},
|
|
281
294
|
});
|
|
282
295
|
export const systemEntities = [
|
|
283
|
-
EventSchema,
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
296
|
+
EventSchema,
|
|
297
|
+
PluginRowSchema,
|
|
298
|
+
MigrationRowSchema,
|
|
299
|
+
SeedRowSchema,
|
|
300
|
+
EmbeddingSchema,
|
|
301
|
+
PluginStateSchema,
|
|
302
|
+
RecordActivitySchema,
|
|
303
|
+
MsgLogSchema,
|
|
304
|
+
UserSchema,
|
|
305
|
+
SessionSchema,
|
|
306
|
+
ApiTokenSchema,
|
|
307
|
+
OAuthClientSchema,
|
|
308
|
+
OAuthCodeSchema,
|
|
309
|
+
OAuthTokenSchema,
|
|
287
310
|
ThreadMessageSchema,
|
|
311
|
+
ThreadStateSchema,
|
|
288
312
|
];
|
package/dist/extend-io.js
CHANGED
|
@@ -2,6 +2,7 @@ import { getExtendsFor } from "./registry-context.js";
|
|
|
2
2
|
import { buildExtendZodPartial } from '@coffer-org/sdk/extend';
|
|
3
3
|
import { upsertExtendRecord, deleteExtendRecord, getExtendRecord } from "./extend-table.js";
|
|
4
4
|
import { toIssue, ValidationError } from "./mutate.js";
|
|
5
|
+
import { normalizeFileFieldsAt } from "./file-fields.js";
|
|
5
6
|
export function splitBody(body) {
|
|
6
7
|
const raw = (body ?? {});
|
|
7
8
|
const base = {};
|
|
@@ -48,7 +49,11 @@ export async function saveExtends(em, library, shelf, baseId, extData) {
|
|
|
48
49
|
continue;
|
|
49
50
|
const parsed = buildExtendZodPartial(e).safeParse(data);
|
|
50
51
|
if (parsed.success && Object.keys(parsed.data).length > 0) {
|
|
51
|
-
|
|
52
|
+
const row = parsed.data;
|
|
53
|
+
const fileIssues = normalizeFileFieldsAt(e.fields, row);
|
|
54
|
+
if (fileIssues.length)
|
|
55
|
+
throw new ValidationError(fileIssues);
|
|
56
|
+
await upsertExtendRecord(em, e, baseId, row);
|
|
52
57
|
}
|
|
53
58
|
}
|
|
54
59
|
}
|
package/dist/extend-table.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { getEm } from "./db.js";
|
|
2
|
-
import { splitAt, writeAt, readAt, readAtMany, deleteAt } from "./collection-io.js";
|
|
2
|
+
import { splitAt, writeAt, readAt, readAtMany, deleteAt, flattenEmbeddedAt, nestEmbeddedAt } from "./collection-io.js";
|
|
3
|
+
import { encodeJsonAt } from "./mutate.js";
|
|
4
|
+
import { encodeTemporalAt, decodeTemporalAt } from "./temporal.js";
|
|
3
5
|
import { chunk } from "./batch.js";
|
|
4
6
|
import { selectRows } from "./read-rows.js";
|
|
5
7
|
export function extendEntityName(e) {
|
|
@@ -11,8 +13,9 @@ export async function getExtendRecord(e, baseId, em) {
|
|
|
11
13
|
const [flat] = (await selectRows(fork, name, { base_id: baseId }, { limit: 1 }));
|
|
12
14
|
if (!flat)
|
|
13
15
|
return undefined;
|
|
16
|
+
const decoded = nestEmbeddedAt(e.fields, decodeTemporalAt(e.fields, flat));
|
|
14
17
|
const collections = await readAt(fork, name, e.fields, baseId);
|
|
15
|
-
return { ...
|
|
18
|
+
return { ...decoded, ...collections };
|
|
16
19
|
}
|
|
17
20
|
export async function getExtendRecords(e, baseIds, em) {
|
|
18
21
|
const out = new Map();
|
|
@@ -30,14 +33,16 @@ export async function getExtendRecords(e, baseIds, em) {
|
|
|
30
33
|
const collectionsById = await readAtMany(fork, name, e.fields, presentIds);
|
|
31
34
|
for (const flat of flats) {
|
|
32
35
|
const id = Number(flat.base_id);
|
|
33
|
-
|
|
36
|
+
const decoded = nestEmbeddedAt(e.fields, decodeTemporalAt(e.fields, flat));
|
|
37
|
+
out.set(id, { ...decoded, ...(collectionsById.get(id) ?? {}) });
|
|
34
38
|
}
|
|
35
39
|
return out;
|
|
36
40
|
}
|
|
37
41
|
export async function upsertExtendRecord(em, e, baseId, data) {
|
|
38
42
|
const name = extendEntityName(e);
|
|
39
43
|
const { base, collections } = splitAt(e.fields, data);
|
|
40
|
-
|
|
44
|
+
const flat = encodeJsonAt(e.fields, encodeTemporalAt(e.fields, flattenEmbeddedAt(e.fields, base)));
|
|
45
|
+
await em.upsert(name, { base_id: baseId, ...flat });
|
|
41
46
|
await writeAt(em, name, e.fields, baseId, collections);
|
|
42
47
|
}
|
|
43
48
|
export async function deleteExtendRecord(em, e, baseId) {
|
package/dist/file-fields.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ShelfDef } from '@coffer-org/sdk/shelf';
|
|
2
|
+
import type { LayoutEl } from '@coffer-org/sdk/fields';
|
|
2
3
|
import type { ValidationIssue } from './mutate.ts';
|
|
3
4
|
export declare function mimeForName(name: string): string | undefined;
|
|
4
5
|
export declare function touchesFileFields(m: ShelfDef, input: unknown): boolean;
|
|
5
6
|
export declare function dropUnchangedFileFields(m: ShelfDef, input: Record<string, unknown>, stored: Record<string, unknown>): Record<string, unknown>;
|
|
7
|
+
export declare function normalizeFileFieldsAt(fields: LayoutEl[], data: Record<string, unknown>): ValidationIssue[];
|
|
6
8
|
export declare function normalizeFileFields(m: ShelfDef, data: Record<string, unknown>): ValidationIssue[];
|
package/dist/file-fields.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { fieldEntries } from '@coffer-org/sdk/shelf';
|
|
3
|
+
import { fieldEntries, collectionGroups } from '@coffer-org/sdk/shelf';
|
|
4
4
|
import { jsonValue } from '@coffer-org/sdk/fields';
|
|
5
5
|
import { uploadsDir } from "./uploads.js";
|
|
6
6
|
const MIME_BY_EXT = {
|
|
@@ -87,9 +87,9 @@ export function dropUnchangedFileFields(m, input, stored) {
|
|
|
87
87
|
}
|
|
88
88
|
return out ?? input;
|
|
89
89
|
}
|
|
90
|
-
|
|
90
|
+
function normalizeLevel(fields, data, path) {
|
|
91
91
|
const issues = [];
|
|
92
|
-
for (const [key, f] of fieldEntries(
|
|
92
|
+
for (const [key, f] of fieldEntries(fields)) {
|
|
93
93
|
if (f.prim !== 'file')
|
|
94
94
|
continue;
|
|
95
95
|
if (!(key in data))
|
|
@@ -101,21 +101,40 @@ export function normalizeFileFields(m, data) {
|
|
|
101
101
|
const isArray = Array.isArray(parsed);
|
|
102
102
|
const items = (isArray ? parsed : [parsed]);
|
|
103
103
|
const out = [];
|
|
104
|
+
let failed = false;
|
|
104
105
|
for (const it of items) {
|
|
105
106
|
if (typeof it !== 'object' || it === null || typeof it.name !== 'string') {
|
|
106
|
-
issues.push({ field: key, code: 'file_not_uploaded', path: [key] });
|
|
107
|
+
issues.push({ field: key, code: 'file_not_uploaded', path: [...path, key] });
|
|
108
|
+
failed = true;
|
|
107
109
|
continue;
|
|
108
110
|
}
|
|
109
111
|
const resolved = resolveEntry(it);
|
|
110
112
|
if (!resolved) {
|
|
111
|
-
issues.push({ field: key, code: 'file_not_uploaded', params: { name: it.name }, path: [key] });
|
|
113
|
+
issues.push({ field: key, code: 'file_not_uploaded', params: { name: it.name }, path: [...path, key] });
|
|
114
|
+
failed = true;
|
|
112
115
|
continue;
|
|
113
116
|
}
|
|
114
117
|
out.push(resolved);
|
|
115
118
|
}
|
|
116
|
-
if (
|
|
119
|
+
if (failed)
|
|
117
120
|
continue;
|
|
118
121
|
data[key] = isArray ? out : out[0];
|
|
119
122
|
}
|
|
123
|
+
for (const c of collectionGroups(fields)) {
|
|
124
|
+
const rows = data[c.key];
|
|
125
|
+
if (!Array.isArray(rows))
|
|
126
|
+
continue;
|
|
127
|
+
rows.forEach((row, i) => {
|
|
128
|
+
if (typeof row !== 'object' || row === null)
|
|
129
|
+
return;
|
|
130
|
+
issues.push(...normalizeLevel(c.group.fields, row, [...path, c.key, i]));
|
|
131
|
+
});
|
|
132
|
+
}
|
|
120
133
|
return issues;
|
|
121
134
|
}
|
|
135
|
+
export function normalizeFileFieldsAt(fields, data) {
|
|
136
|
+
return normalizeLevel(fields, data, []);
|
|
137
|
+
}
|
|
138
|
+
export function normalizeFileFields(m, data) {
|
|
139
|
+
return normalizeFileFieldsAt(m.fields, data);
|
|
140
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -546,7 +546,10 @@ app.patch('/api/:library/:shelf/:id', (req, reply) => guard(reply, async () => {
|
|
|
546
546
|
const bk = `_extend_${def.id}`;
|
|
547
547
|
const sub = body[bk];
|
|
548
548
|
if (sub && typeof sub === 'object') {
|
|
549
|
-
body = {
|
|
549
|
+
body = {
|
|
550
|
+
...body,
|
|
551
|
+
[bk]: preserveTree(def.fields, sub, (exRecs[def.id] ?? {})),
|
|
552
|
+
};
|
|
550
553
|
}
|
|
551
554
|
}
|
|
552
555
|
}
|
|
@@ -13,7 +13,10 @@ export async function freshMcpApp(opts = {}) {
|
|
|
13
13
|
await orm.schema.update({ safe: false, dropTables: false });
|
|
14
14
|
const app = Fastify();
|
|
15
15
|
app.addHook('onRequest', async (req, reply) => {
|
|
16
|
-
const gated = req.url.startsWith('/api/') ||
|
|
16
|
+
const gated = req.url.startsWith('/api/') ||
|
|
17
|
+
req.url.startsWith('/uploads/') ||
|
|
18
|
+
req.url === '/mcp' ||
|
|
19
|
+
req.url.startsWith('/mcp?');
|
|
17
20
|
if (!gated)
|
|
18
21
|
return;
|
|
19
22
|
if (PUBLIC_API_PATHS.some((p) => req.url.startsWith(p)))
|
package/dist/mcp-tools.js
CHANGED
|
@@ -39,7 +39,7 @@ const fail = (text) => ({ content: [{ type: 'text', text }], isError: true });
|
|
|
39
39
|
export async function resolveRagDeps() {
|
|
40
40
|
const db = (await getPluginSettings('claude-agent'));
|
|
41
41
|
const enabled = db['rag_enabled'] !== false;
|
|
42
|
-
const embeddingApiKey =
|
|
42
|
+
const embeddingApiKey = process.env.OPENAI_API_KEY ?? db['openai_api_key'] ?? '';
|
|
43
43
|
if (!enabled || !embeddingApiKey)
|
|
44
44
|
return null;
|
|
45
45
|
return { embeddingApiKey };
|
|
@@ -81,7 +81,7 @@ export async function collectMcpTools(opts = {}) {
|
|
|
81
81
|
how_to: `curl -H "Authorization: Bearer <token>" -F "file=@<path>" ${uploadUrl} → {"name":"<name>"}. Then set a file field to {"name":"<name>"}. mime/size are filled in by the server — do not send your own.` +
|
|
82
82
|
(base
|
|
83
83
|
? ''
|
|
84
|
-
:
|
|
84
|
+
: " The server has no public URL configured (core settings publicUrl / PUBLIC_URL env), so the path is relative — resolve it against this MCP server's own origin."),
|
|
85
85
|
});
|
|
86
86
|
},
|
|
87
87
|
});
|
|
@@ -328,9 +328,7 @@ export async function buildDomainSections() {
|
|
|
328
328
|
const singles = collectSingleShelves();
|
|
329
329
|
const singleSection = singles.length
|
|
330
330
|
? '## Single-record shelves (one document each — read the record, do not search the shelf)\n\n' +
|
|
331
|
-
singles
|
|
332
|
-
.map((s) => `- ${s.library}/${s.shelf}: ${s.claude.replace(/\s*\n\s*/g, ' ')}`)
|
|
333
|
-
.join('\n')
|
|
331
|
+
singles.map((s) => `- ${s.library}/${s.shelf}: ${s.claude.replace(/\s*\n\s*/g, ' ')}`).join('\n')
|
|
334
332
|
: null;
|
|
335
333
|
const dataModel = '## Data model\n' +
|
|
336
334
|
'Library (top-level area) → shelf (a kind of record, e.g. things/item) → record (addressed library/shelf/id) → fields. ' +
|
package/dist/msg-log.d.ts
CHANGED
package/dist/msg-log.js
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
import { getEm } from "./db.js";
|
|
2
2
|
export async function logMessage(row) {
|
|
3
|
-
await getEm()
|
|
4
|
-
|
|
3
|
+
await getEm()
|
|
4
|
+
.fork()
|
|
5
|
+
.getConnection()
|
|
6
|
+
.execute(`INSERT INTO _orch_msg_log (ts, connector, chat_id, user_id, role, text, tokens_in, tokens_out, ms, agent_id, preset_id)
|
|
7
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
8
|
+
new Date().toISOString(),
|
|
9
|
+
row.connector,
|
|
10
|
+
row.chatId,
|
|
11
|
+
row.userId,
|
|
12
|
+
row.role,
|
|
13
|
+
row.text,
|
|
14
|
+
row.tokensIn,
|
|
15
|
+
row.tokensOut,
|
|
16
|
+
row.ms,
|
|
17
|
+
row.agentId,
|
|
18
|
+
row.presetId,
|
|
19
|
+
]);
|
|
5
20
|
}
|
|
6
21
|
export async function listRecentMessageMetrics(limit = 25) {
|
|
7
22
|
const bounded = Math.max(1, Math.min(100, Math.trunc(limit)));
|
|
8
|
-
const rows = (await getEm()
|
|
23
|
+
const rows = (await getEm()
|
|
24
|
+
.fork()
|
|
25
|
+
.getConnection()
|
|
26
|
+
.execute(`SELECT id, ts, connector, role, tokens_in, tokens_out, ms
|
|
9
27
|
FROM _orch_msg_log
|
|
10
28
|
ORDER BY id DESC
|
|
11
29
|
LIMIT ?`, [bounded]));
|
package/dist/mutate.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/core';
|
|
2
2
|
import type { ShelfDef } from '@coffer-org/sdk/shelf';
|
|
3
|
+
import type { LayoutEl } from '@coffer-org/sdk/fields';
|
|
3
4
|
import { z } from 'zod';
|
|
4
|
-
export declare function
|
|
5
|
+
export declare function encodeJsonAt(fields: LayoutEl[], data: Record<string, unknown>): Record<string, unknown>;
|
|
6
|
+
export declare const encodeJson: (m: ShelfDef, data: Record<string, unknown>) => Record<string, unknown>;
|
|
5
7
|
export interface MutateCtx {
|
|
6
8
|
actor: string;
|
|
7
9
|
}
|
package/dist/mutate.js
CHANGED
|
@@ -2,6 +2,7 @@ import { serialize } from '@mikro-orm/core';
|
|
|
2
2
|
import { buildZodObject, buildZodObjectPartial, fieldEntries } 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';
|
|
5
6
|
import { getEm } from "./db.js";
|
|
6
7
|
import { selectRows } from "./read-rows.js";
|
|
7
8
|
import { normalizeFileFields, dropUnchangedFileFields, touchesFileFields } from "./file-fields.js";
|
|
@@ -11,15 +12,16 @@ import { splitCollections, writeCollections, deleteCollections, flattenEmbedded,
|
|
|
11
12
|
function nowIso() {
|
|
12
13
|
return new Date().toISOString();
|
|
13
14
|
}
|
|
14
|
-
export function
|
|
15
|
+
export function encodeJsonAt(fields, data) {
|
|
15
16
|
const out = { ...data };
|
|
16
|
-
for (const [k, f] of fieldEntries(
|
|
17
|
+
for (const [k, f] of fieldEntries(fields)) {
|
|
17
18
|
if (isJsonStored(f) && out[k] !== null && out[k] !== undefined && typeof out[k] === 'object') {
|
|
18
19
|
out[k] = JSON.stringify(out[k]);
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
return out;
|
|
22
23
|
}
|
|
24
|
+
export const encodeJson = (m, data) => encodeJsonAt(m.fields, data);
|
|
23
25
|
export class ValidationError extends Error {
|
|
24
26
|
issues;
|
|
25
27
|
constructor(issues) {
|
|
@@ -86,6 +88,11 @@ export async function createRecord(m, entityName, input, ctx, afterBase) {
|
|
|
86
88
|
await tx.flush();
|
|
87
89
|
id = entity.id;
|
|
88
90
|
await writeCollections(tx, m, id, collections);
|
|
91
|
+
const derived = applyDerived(m.fields, { ...parsedData, id });
|
|
92
|
+
if (Object.keys(derived).length) {
|
|
93
|
+
await tx.nativeUpdate(entityName, { id }, encodeJson(m, encodeTemporal(m, flattenEmbedded(m, derived))));
|
|
94
|
+
Object.assign(parsedData, derived);
|
|
95
|
+
}
|
|
89
96
|
const _extends = afterBase ? await afterBase(tx, id) : undefined;
|
|
90
97
|
writeEvent(tx, ctx.actor, 'create', `${m.library}/${m.shelf}`, id, null, {
|
|
91
98
|
...parsedData,
|
|
@@ -119,10 +126,11 @@ export async function updateRecord(m, entityName, id, input, ctx, afterBase) {
|
|
|
119
126
|
const existingFlat = decodeTemporal(m, serialize(found));
|
|
120
127
|
const existing = nestEmbedded(m, existingFlat);
|
|
121
128
|
const ts = nowIso();
|
|
122
|
-
const
|
|
123
|
-
const fileIssues = normalizeFileFields(m,
|
|
129
|
+
const patchData = parsed.data;
|
|
130
|
+
const fileIssues = normalizeFileFields(m, patchData);
|
|
124
131
|
if (fileIssues.length)
|
|
125
132
|
throw new ValidationError(fileIssues);
|
|
133
|
+
const { base, collections } = splitCollections(m, { ...patchData });
|
|
126
134
|
const merged = { ...existing, ...base };
|
|
127
135
|
const reqIssues = [];
|
|
128
136
|
for (const [key, f] of fieldEntries(m.fields)) {
|
|
@@ -142,6 +150,11 @@ export async function updateRecord(m, entityName, id, input, ctx, afterBase) {
|
|
|
142
150
|
await tx.upsert(entityName, dbRow);
|
|
143
151
|
await writeCollections(tx, m, id, collections);
|
|
144
152
|
const allCollections = await readCollections(tx, m, id);
|
|
153
|
+
const derived = applyDerived(m.fields, { ...merged, ...allCollections });
|
|
154
|
+
if (Object.keys(derived).length) {
|
|
155
|
+
await tx.nativeUpdate(entityName, { id }, encodeJson(m, encodeTemporal(m, flattenEmbedded(m, derived))));
|
|
156
|
+
Object.assign(merged, derived);
|
|
157
|
+
}
|
|
145
158
|
const _extends = afterBase ? await afterBase(tx, id) : undefined;
|
|
146
159
|
const after = { ...merged, ...allCollections, updated_at: ts, ...(_extends ? { _extends } : {}) };
|
|
147
160
|
writeEvent(tx, ctx.actor, 'update', `${m.library}/${m.shelf}`, id, existing, after);
|
package/dist/oauth-api.js
CHANGED
|
@@ -3,10 +3,7 @@ import { resolveRequestUser, startPasswordSession } from "./auth-api.js";
|
|
|
3
3
|
import { baseUrl, mcpResource } from "./public-url.js";
|
|
4
4
|
import { getLogger } from "./log.js";
|
|
5
5
|
const log = getLogger('oauth');
|
|
6
|
-
const DEFAULT_REDIRECT_ALLOW = [
|
|
7
|
-
'https://claude.ai/api/mcp/auth_callback',
|
|
8
|
-
'https://claude.com/api/mcp/auth_callback',
|
|
9
|
-
];
|
|
6
|
+
const DEFAULT_REDIRECT_ALLOW = ['https://claude.ai/api/mcp/auth_callback', 'https://claude.com/api/mcp/auth_callback'];
|
|
10
7
|
function redirectAllowlist() {
|
|
11
8
|
const extra = (process.env['OAUTH_REDIRECT_ALLOW'] ?? '')
|
|
12
9
|
.split(',')
|
|
@@ -147,7 +144,10 @@ export function registerOAuthApi(app) {
|
|
|
147
144
|
.code(400)
|
|
148
145
|
.send({ error: 'invalid_redirect_uri', error_description: `redirect_uri not allowed: ${bad}` });
|
|
149
146
|
}
|
|
150
|
-
const client = await registerClient({
|
|
147
|
+
const client = await registerClient({
|
|
148
|
+
clientName: body.client_name?.slice(0, 200) || 'MCP client',
|
|
149
|
+
redirectUris: uris,
|
|
150
|
+
});
|
|
151
151
|
log.info(`registered client ${client.clientId} (${client.clientName})`);
|
|
152
152
|
return reply.code(201).send({
|
|
153
153
|
client_id: client.clientId,
|
|
@@ -160,14 +160,23 @@ export function registerOAuthApi(app) {
|
|
|
160
160
|
});
|
|
161
161
|
async function validateAuthz(p) {
|
|
162
162
|
if (!p.client_id || !p.redirect_uri) {
|
|
163
|
-
return {
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
html: page('Invalid request', '<h1>Invalid request</h1><p>Missing client_id or redirect_uri.</p>'),
|
|
166
|
+
};
|
|
164
167
|
}
|
|
165
168
|
const client = await findClient(p.client_id);
|
|
166
169
|
if (!client) {
|
|
167
|
-
return {
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
html: page('Unknown client', '<h1>Unknown client</h1><p>This application is not registered.</p>'),
|
|
173
|
+
};
|
|
168
174
|
}
|
|
169
175
|
if (!client.redirectUris.includes(p.redirect_uri)) {
|
|
170
|
-
return {
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
html: page('Invalid redirect', '<h1>Invalid redirect</h1><p>redirect_uri does not match this client.</p>'),
|
|
179
|
+
};
|
|
171
180
|
}
|
|
172
181
|
return { ok: true, clientName: client.clientName };
|
|
173
182
|
}
|
|
@@ -197,12 +206,18 @@ export function registerOAuthApi(app) {
|
|
|
197
206
|
return reply.code(429).type('text/html').send(page('Slow down', '<h1>Too many attempts</h1>'));
|
|
198
207
|
const user = await startPasswordSession(reply, String(body['login'] ?? ''), String(body['password'] ?? ''));
|
|
199
208
|
if (!user)
|
|
200
|
-
return reply
|
|
209
|
+
return reply
|
|
210
|
+
.code(401)
|
|
211
|
+
.type('text/html')
|
|
212
|
+
.send(loginForm(p, check.clientName, 'Wrong login or password.'));
|
|
201
213
|
return reply.type('text/html').send(consentForm(p, check.clientName, user));
|
|
202
214
|
}
|
|
203
215
|
const user = await resolveRequestUser(req);
|
|
204
216
|
if (!user)
|
|
205
|
-
return reply
|
|
217
|
+
return reply
|
|
218
|
+
.code(401)
|
|
219
|
+
.type('text/html')
|
|
220
|
+
.send(loginForm(p, check.clientName, 'Your session expired. Sign in again.'));
|
|
206
221
|
if (body['action'] !== 'approve')
|
|
207
222
|
return redirectWithError(reply, p, 'access_denied');
|
|
208
223
|
if (!p.code_challenge)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
const cache = new Map();
|
|
3
|
+
async function systemLanguage(fallback) {
|
|
4
|
+
try {
|
|
5
|
+
const { getPluginSettings } = await import("./plugin-runtime.js");
|
|
6
|
+
return (await getPluginSettings('core'))['language'] || fallback;
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return fallback;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export async function loadPluginI18n(localesDir, fallbackLang = 'uk') {
|
|
13
|
+
const lang = await systemLanguage(fallbackLang);
|
|
14
|
+
const url = new URL(`${lang}.json`, localesDir);
|
|
15
|
+
let dict = cache.get(url.href);
|
|
16
|
+
if (!dict) {
|
|
17
|
+
try {
|
|
18
|
+
dict = JSON.parse(await readFile(url, 'utf8'));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
dict = {};
|
|
22
|
+
}
|
|
23
|
+
cache.set(url.href, dict);
|
|
24
|
+
}
|
|
25
|
+
const loaded = dict;
|
|
26
|
+
return {
|
|
27
|
+
lang,
|
|
28
|
+
t(key, fallback) {
|
|
29
|
+
const value = key
|
|
30
|
+
.split('.')
|
|
31
|
+
.reduce((node, part) => (node == null ? undefined : node[part]), loaded);
|
|
32
|
+
return typeof value === 'string' ? value : (fallback ?? key);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function clearPluginI18nCache() {
|
|
37
|
+
cache.clear();
|
|
38
|
+
}
|
package/dist/rag-search.js
CHANGED
|
@@ -89,7 +89,6 @@ export async function hybridSearch(opts, deps = {}) {
|
|
|
89
89
|
score: entry.score,
|
|
90
90
|
};
|
|
91
91
|
})
|
|
92
|
-
.sort((a, b) => b.score - a.score ||
|
|
93
|
-
hitKey(a.shelfKey, a.recordId).localeCompare(hitKey(b.shelfKey, b.recordId)))
|
|
92
|
+
.sort((a, b) => b.score - a.score || hitKey(a.shelfKey, a.recordId).localeCompare(hitKey(b.shelfKey, b.recordId)))
|
|
94
93
|
.slice(0, opts.k);
|
|
95
94
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { derivedEntries, applyDerived } from '@coffer-org/sdk/derive';
|
|
2
|
+
import { getEm } from "./db.js";
|
|
3
|
+
import { selectRows } from "./read-rows.js";
|
|
4
|
+
import { encodeJson } from "./mutate.js";
|
|
5
|
+
import { encodeTemporal, decodeTemporal } from "./temporal.js";
|
|
6
|
+
import { flattenEmbedded, nestEmbedded, readCollections } from "./collection-io.js";
|
|
7
|
+
function sameValue(a, b) {
|
|
8
|
+
if (a instanceof Date && b instanceof Date)
|
|
9
|
+
return a.getTime() === b.getTime();
|
|
10
|
+
if (a instanceof Date || b instanceof Date)
|
|
11
|
+
return false;
|
|
12
|
+
return a === b;
|
|
13
|
+
}
|
|
14
|
+
export async function recomputeDerivedFields(m, entityName) {
|
|
15
|
+
if (derivedEntries(m.fields).length === 0)
|
|
16
|
+
return { scanned: 0, changed: 0 };
|
|
17
|
+
const fork = getEm().fork();
|
|
18
|
+
const rows = await selectRows(fork, entityName, {}, { orderBy: { id: 'asc' } });
|
|
19
|
+
let changed = 0;
|
|
20
|
+
for (const row of rows) {
|
|
21
|
+
const id = row['id'];
|
|
22
|
+
const flat = decodeTemporal(m, row);
|
|
23
|
+
const nested = nestEmbedded(m, flat);
|
|
24
|
+
const collections = await readCollections(fork, m, id);
|
|
25
|
+
const derived = applyDerived(m.fields, { ...nested, ...collections });
|
|
26
|
+
if (Object.keys(derived).length === 0)
|
|
27
|
+
continue;
|
|
28
|
+
const encoded = encodeJson(m, encodeTemporal(m, flattenEmbedded(m, derived)));
|
|
29
|
+
const dirty = {};
|
|
30
|
+
for (const [k, v] of Object.entries(encoded)) {
|
|
31
|
+
if (!sameValue(row[k], v))
|
|
32
|
+
dirty[k] = v;
|
|
33
|
+
}
|
|
34
|
+
if (Object.keys(dirty).length === 0)
|
|
35
|
+
continue;
|
|
36
|
+
await fork.nativeUpdate(entityName, { id }, dirty);
|
|
37
|
+
changed += 1;
|
|
38
|
+
}
|
|
39
|
+
return { scanned: rows.length, changed };
|
|
40
|
+
}
|
package/dist/records-api.js
CHANGED
|
@@ -116,7 +116,9 @@ export async function recordCount(library, shelf, query = {}, opts = {}) {
|
|
|
116
116
|
where.deleted_at = { $ne: null };
|
|
117
117
|
if (m.standalone === false && Object.keys(filterParams).length === 0)
|
|
118
118
|
return 0;
|
|
119
|
-
return getEm()
|
|
119
|
+
return getEm()
|
|
120
|
+
.fork()
|
|
121
|
+
.count(ename, where);
|
|
120
122
|
}
|
|
121
123
|
async function listRecords(library, shelf, query = {}, opts = {}) {
|
|
122
124
|
const { view = 'full', extends: withExt = true, deleted = 'active' } = opts;
|
package/dist/search-index.js
CHANGED
|
@@ -57,11 +57,7 @@ export async function upsertSearchRow(shelf, recordId, folded) {
|
|
|
57
57
|
await conn.execute('DELETE FROM "_search" WHERE shelf = ? AND record_id = ?', [shelf, recordId]);
|
|
58
58
|
if (!folded)
|
|
59
59
|
return;
|
|
60
|
-
await conn.execute('INSERT INTO "_search" (shelf, record_id, folded) VALUES (?, ?, ?)', [
|
|
61
|
-
shelf,
|
|
62
|
-
recordId,
|
|
63
|
-
folded,
|
|
64
|
-
]);
|
|
60
|
+
await conn.execute('INSERT INTO "_search" (shelf, record_id, folded) VALUES (?, ?, ?)', [shelf, recordId, folded]);
|
|
65
61
|
}
|
|
66
62
|
export async function deleteSearchRow(shelf, recordId) {
|
|
67
63
|
if (!ftsAvailable())
|
|
@@ -81,9 +77,7 @@ export async function ftsCandidates(tokens) {
|
|
|
81
77
|
const rows = (await getEm()
|
|
82
78
|
.fork()
|
|
83
79
|
.getConnection()
|
|
84
|
-
.execute('SELECT shelf, record_id FROM "_search" WHERE "_search" MATCH ?', [
|
|
85
|
-
buildFtsMatchQuery(tokens),
|
|
86
|
-
]));
|
|
80
|
+
.execute('SELECT shelf, record_id FROM "_search" WHERE "_search" MATCH ?', [buildFtsMatchQuery(tokens)]));
|
|
87
81
|
for (const r of rows) {
|
|
88
82
|
const list = out.get(r.shelf);
|
|
89
83
|
if (list)
|
package/dist/temporal.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { ShelfDef } from '@coffer-org/sdk/shelf';
|
|
2
|
+
import type { LayoutEl } from '@coffer-org/sdk/fields';
|
|
2
3
|
export declare function dtStringToDate(s: string): Date;
|
|
3
4
|
export declare function dateToDtString(d: Date): string;
|
|
4
|
-
export declare function
|
|
5
|
-
export declare function
|
|
5
|
+
export declare function encodeTemporalAt(fields: LayoutEl[], row: Record<string, unknown>): Record<string, unknown>;
|
|
6
|
+
export declare function decodeTemporalAt(fields: LayoutEl[], row: Record<string, unknown>): Record<string, unknown>;
|
|
7
|
+
export declare const encodeTemporal: (m: ShelfDef, row: Record<string, unknown>) => Record<string, unknown>;
|
|
8
|
+
export declare const decodeTemporal: (m: ShelfDef, row: Record<string, unknown>) => Record<string, unknown>;
|
package/dist/temporal.js
CHANGED
|
@@ -7,22 +7,22 @@ export function dateToDtString(d) {
|
|
|
7
7
|
return (`${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())}` +
|
|
8
8
|
`T${p(d.getUTCHours())}:${p(d.getUTCMinutes())}`);
|
|
9
9
|
}
|
|
10
|
-
function
|
|
11
|
-
const fm = fieldMap(
|
|
10
|
+
function datetimeKeysAt(fields) {
|
|
11
|
+
const fm = fieldMap(fields);
|
|
12
12
|
return Object.keys(fm).filter((k) => fm[k]?.column === 'datetime');
|
|
13
13
|
}
|
|
14
|
-
export function
|
|
14
|
+
export function encodeTemporalAt(fields, row) {
|
|
15
15
|
const out = { ...row };
|
|
16
|
-
for (const k of
|
|
16
|
+
for (const k of datetimeKeysAt(fields)) {
|
|
17
17
|
const v = out[k];
|
|
18
18
|
if (typeof v === 'string' && v)
|
|
19
19
|
out[k] = dtStringToDate(v);
|
|
20
20
|
}
|
|
21
21
|
return out;
|
|
22
22
|
}
|
|
23
|
-
export function
|
|
23
|
+
export function decodeTemporalAt(fields, row) {
|
|
24
24
|
const out = { ...row };
|
|
25
|
-
for (const k of
|
|
25
|
+
for (const k of datetimeKeysAt(fields)) {
|
|
26
26
|
const v = out[k];
|
|
27
27
|
if (v instanceof Date)
|
|
28
28
|
out[k] = dateToDtString(v);
|
|
@@ -31,3 +31,5 @@ export function decodeTemporal(m, row) {
|
|
|
31
31
|
}
|
|
32
32
|
return out;
|
|
33
33
|
}
|
|
34
|
+
export const encodeTemporal = (m, row) => encodeTemporalAt(m.fields, row);
|
|
35
|
+
export const decodeTemporal = (m, row) => decodeTemporalAt(m.fields, row);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ThreadSelection {
|
|
2
|
+
agentId: string | null;
|
|
3
|
+
presetId: string | null;
|
|
4
|
+
}
|
|
5
|
+
export declare function getThreadState(connector: string, chatId: string): Promise<ThreadSelection>;
|
|
6
|
+
export declare function setThreadState(connector: string, chatId: string, patch: Partial<ThreadSelection>): Promise<void>;
|
|
7
|
+
export declare function readAndTouchThreadState(connector: string, chatId: string): Promise<ThreadSelection>;
|
|
8
|
+
export declare function pruneThreadState(connector: string, cutoffIso: string): Promise<void>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getEm } from "./db.js";
|
|
2
|
+
const EMPTY = { agentId: null, presetId: null };
|
|
3
|
+
export async function getThreadState(connector, chatId) {
|
|
4
|
+
const em = getEm().fork();
|
|
5
|
+
const row = (await em.findOne('_ThreadState', { connector, chat_id: chatId }));
|
|
6
|
+
if (!row)
|
|
7
|
+
return { ...EMPTY };
|
|
8
|
+
return { agentId: row.agent_id, presetId: row.preset_id };
|
|
9
|
+
}
|
|
10
|
+
export async function setThreadState(connector, chatId, patch) {
|
|
11
|
+
const em = getEm().fork();
|
|
12
|
+
const current = await getThreadState(connector, chatId);
|
|
13
|
+
const next = {
|
|
14
|
+
agentId: 'agentId' in patch ? (patch.agentId ?? null) : current.agentId,
|
|
15
|
+
presetId: 'presetId' in patch ? (patch.presetId ?? null) : current.presetId,
|
|
16
|
+
};
|
|
17
|
+
const existing = await em.findOne('_ThreadState', { connector, chat_id: chatId });
|
|
18
|
+
const data = { agent_id: next.agentId, preset_id: next.presetId, updated_at: new Date().toISOString() };
|
|
19
|
+
if (existing) {
|
|
20
|
+
em.assign(existing, data);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
em.persist(em.create('_ThreadState', { connector, chat_id: chatId, ...data }));
|
|
24
|
+
}
|
|
25
|
+
await em.flush();
|
|
26
|
+
}
|
|
27
|
+
export async function readAndTouchThreadState(connector, chatId) {
|
|
28
|
+
const em = getEm().fork();
|
|
29
|
+
const row = (await em.findOne('_ThreadState', { connector, chat_id: chatId }));
|
|
30
|
+
if (!row)
|
|
31
|
+
return { ...EMPTY };
|
|
32
|
+
em.assign(row, { updated_at: new Date().toISOString() });
|
|
33
|
+
await em.flush();
|
|
34
|
+
return { agentId: row.agent_id, presetId: row.preset_id };
|
|
35
|
+
}
|
|
36
|
+
export async function pruneThreadState(connector, cutoffIso) {
|
|
37
|
+
const em = getEm().fork();
|
|
38
|
+
await em.nativeDelete('_ThreadState', { connector, updated_at: { $lt: cutoffIso } });
|
|
39
|
+
}
|
package/dist/thread-store.js
CHANGED
|
@@ -51,7 +51,9 @@ export async function listThreadMessages(connector, chatId, limit = 200) {
|
|
|
51
51
|
const em = getEm().fork();
|
|
52
52
|
const visible = (await em.find('_ThreadMessage', { connector, chat_id: chatId, role: { $ne: 'reasoning' } }, { orderBy: { ts: 'desc', msg_id: 'desc' }, limit }));
|
|
53
53
|
const oldest = visible.at(-1)?.ts;
|
|
54
|
-
const reasoning = oldest === undefined
|
|
54
|
+
const reasoning = oldest === undefined
|
|
55
|
+
? []
|
|
56
|
+
: (await em.find('_ThreadMessage', { connector, chat_id: chatId, role: 'reasoning', ts: { $gte: oldest - 1 } }, { orderBy: { ts: 'desc', msg_id: 'desc' } }));
|
|
55
57
|
return [...visible, ...reasoning]
|
|
56
58
|
.sort((a, b) => b.ts - a.ts || (a.msg_id < b.msg_id ? 1 : a.msg_id > b.msg_id ? -1 : 0))
|
|
57
59
|
.reverse()
|
package/dist/uploads.js
CHANGED
|
@@ -28,7 +28,7 @@ export async function saveUploadBytes(bytes, opts = {}) {
|
|
|
28
28
|
if (bytes.byteLength > maxBytes)
|
|
29
29
|
throw new Error(`upload exceeds ${maxBytes} bytes`);
|
|
30
30
|
const originalExt = opts.originalName ? extname(basename(opts.originalName)).toLowerCase() : '';
|
|
31
|
-
const ext = originalExt && /^[.][a-z0-9]{1,10}$/.test(originalExt) ? originalExt : MIME_EXT[opts.mime ?? ''] ?? '';
|
|
31
|
+
const ext = originalExt && /^[.][a-z0-9]{1,10}$/.test(originalExt) ? originalExt : (MIME_EXT[opts.mime ?? ''] ?? '');
|
|
32
32
|
const name = `${randomUUID()}${ext}`;
|
|
33
33
|
await writeFile(join(uploadsDir(), name), bytes);
|
|
34
34
|
const mime = opts.mime === 'application/octet-stream' ? undefined : opts.mime;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffer-org/server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24"
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"postpack": "node ../../scripts/swap-exports.mjs src"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@coffer-org/core": "^
|
|
28
|
-
"@coffer-org/sdk": "^
|
|
27
|
+
"@coffer-org/core": "^3.0.0",
|
|
28
|
+
"@coffer-org/sdk": "^3.0.0",
|
|
29
29
|
"@extractus/oembed-extractor": "^4.1.0",
|
|
30
30
|
"@fastify/cors": "^11.2.0",
|
|
31
31
|
"@fastify/multipart": "^10.0.0",
|