@coffer-org/server 1.7.1 → 1.8.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 +3 -3
- package/dist/background-scheduler.d.ts +22 -0
- package/dist/background-scheduler.js +101 -0
- package/dist/collection-io.d.ts +7 -7
- package/dist/collection-io.js +2 -2
- package/dist/connector-identity.d.ts +5 -0
- package/dist/connector-identity.js +32 -0
- package/dist/embed-openai.d.ts +10 -0
- package/dist/embed-openai.js +28 -0
- package/dist/entity-schema.d.ts +6 -5
- package/dist/entity-schema.js +24 -10
- package/dist/extend-io.d.ts +5 -3
- package/dist/extend-io.js +17 -10
- package/dist/extend-table.d.ts +4 -3
- package/dist/extend-table.js +10 -18
- package/dist/field-masking.d.ts +7 -0
- package/dist/field-masking.js +60 -0
- package/dist/index-signal.d.ts +3 -0
- package/dist/index-signal.js +14 -0
- package/dist/index.js +59 -159
- package/dist/local-api.d.ts +3 -3
- package/dist/local-api.js +6 -6
- package/dist/mcp-http.d.ts +5 -0
- package/dist/mcp-http.js +37 -0
- package/dist/mcp-http.test-helpers.d.ts +17 -0
- package/dist/mcp-http.test-helpers.js +117 -0
- package/dist/mcp-local.d.ts +10 -0
- package/dist/mcp-local.js +57 -0
- package/dist/mcp-tools.d.ts +61 -0
- package/dist/mcp-tools.js +225 -0
- package/dist/msg-log.d.ts +0 -1
- package/dist/msg-log.js +2 -2
- package/dist/mutate.d.ts +7 -5
- package/dist/mutate.js +20 -8
- package/dist/plugin-hooks.d.ts +10 -0
- package/dist/plugin-hooks.js +8 -0
- package/dist/plugin-runtime.d.ts +1 -0
- package/dist/plugin-runtime.js +30 -6
- package/dist/plugins-api.d.ts +1 -1
- package/dist/plugins-api.js +21 -11
- package/dist/records-api.d.ts +8 -8
- package/dist/records-api.js +35 -32
- package/dist/registry-context.d.ts +1 -1
- package/dist/registry-context.js +2 -2
- package/dist/schema-api.js +5 -5
- package/dist/settings-write.d.ts +19 -0
- package/dist/settings-write.js +60 -0
- package/dist/temporal.d.ts +3 -3
- package/dist/temporal.js +1 -1
- package/dist/thread-store.d.ts +20 -0
- package/dist/thread-store.js +27 -0
- package/dist/uploads.d.ts +1 -0
- package/dist/uploads.js +4 -0
- package/package.json +6 -6
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ValidationError, NotFoundError } from '@coffer-org/mcp/client';
|
|
2
|
+
import { recordList, recordGet, recordCreate, recordUpdate, recordDelete, UnknownTypeError, } from "./records-api.js";
|
|
3
|
+
import { ValidationError as ServerValidationError } from "./mutate.js";
|
|
4
|
+
import { buildClientSchema } from "./schema-api.js";
|
|
5
|
+
export function mapError(e) {
|
|
6
|
+
if (e instanceof ServerValidationError)
|
|
7
|
+
throw new ValidationError(e.issues ?? []);
|
|
8
|
+
if (e instanceof UnknownTypeError)
|
|
9
|
+
throw new NotFoundError('not_found');
|
|
10
|
+
throw e;
|
|
11
|
+
}
|
|
12
|
+
export class LocalClient {
|
|
13
|
+
async getSchema() {
|
|
14
|
+
return buildClientSchema();
|
|
15
|
+
}
|
|
16
|
+
async listRecords(library, type, query = {}) {
|
|
17
|
+
try {
|
|
18
|
+
return await recordList(library, type, query);
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
mapError(e);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async getRecord(library, type, id) {
|
|
25
|
+
try {
|
|
26
|
+
return await recordGet(library, type, id);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
mapError(e);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async createRecord(library, type, fields) {
|
|
33
|
+
try {
|
|
34
|
+
return await recordCreate(library, type, fields);
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
mapError(e);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async updateRecord(library, type, id, fields) {
|
|
41
|
+
try {
|
|
42
|
+
return await recordUpdate(library, type, id, fields);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
mapError(e);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async deleteRecord(library, type, id) {
|
|
49
|
+
try {
|
|
50
|
+
await recordDelete(library, type, id);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
mapError(e);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { type ToolResult } from '@coffer-org/mcp';
|
|
4
|
+
import { type AuthRole, type PluginHooks } from './plugin-hooks.ts';
|
|
5
|
+
import { type Condition } from '@coffer-org/sdk/condition';
|
|
6
|
+
import { type EmbeddingHit } from './embeddings.ts';
|
|
7
|
+
export interface McpToolDef {
|
|
8
|
+
server: string;
|
|
9
|
+
bareName: string;
|
|
10
|
+
httpName: string;
|
|
11
|
+
description: string;
|
|
12
|
+
inputSchema: z.ZodRawShape;
|
|
13
|
+
scope: 'crud' | 'plugin' | 'rag' | 'settings';
|
|
14
|
+
role: AuthRole;
|
|
15
|
+
handler: (args: Record<string, unknown>) => Promise<ToolResult>;
|
|
16
|
+
}
|
|
17
|
+
export interface RagDeps {
|
|
18
|
+
embeddingApiKey: string;
|
|
19
|
+
topK: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function formatHits(hits: EmbeddingHit[]): string;
|
|
22
|
+
export declare function resolveRagDeps(): Promise<RagDeps | null>;
|
|
23
|
+
export declare function collectMcpTools(opts?: {
|
|
24
|
+
rag?: RagDeps | null;
|
|
25
|
+
includeAdmin?: boolean;
|
|
26
|
+
actor?: string;
|
|
27
|
+
}): Promise<McpToolDef[]>;
|
|
28
|
+
export declare function collectPluginInstructions(hooks?: Record<string, PluginHooks>, emFactory?: () => EntityManager): Promise<{
|
|
29
|
+
id: string;
|
|
30
|
+
instructions: string;
|
|
31
|
+
}[]>;
|
|
32
|
+
type LibraryPurpose = {
|
|
33
|
+
id: string;
|
|
34
|
+
agent: string;
|
|
35
|
+
extends: {
|
|
36
|
+
id: string;
|
|
37
|
+
shelf: string;
|
|
38
|
+
claude: string;
|
|
39
|
+
showWhen?: Condition;
|
|
40
|
+
}[];
|
|
41
|
+
};
|
|
42
|
+
export declare function collectLibraryPurposes(reg?: {
|
|
43
|
+
libraries: {
|
|
44
|
+
meta: {
|
|
45
|
+
id: string;
|
|
46
|
+
agent?: string;
|
|
47
|
+
};
|
|
48
|
+
}[];
|
|
49
|
+
extends_: {
|
|
50
|
+
id: string;
|
|
51
|
+
claude?: string;
|
|
52
|
+
showWhen?: Condition;
|
|
53
|
+
attachTo: {
|
|
54
|
+
library: string;
|
|
55
|
+
shelf: string;
|
|
56
|
+
}[];
|
|
57
|
+
}[];
|
|
58
|
+
}): LibraryPurpose[];
|
|
59
|
+
export declare function buildDomainSections(): Promise<string[]>;
|
|
60
|
+
export declare function buildMcpInstructions(sections: string[]): string;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { buildTools } from '@coffer-org/mcp';
|
|
3
|
+
import { SchemaCache } from '@coffer-org/mcp/schema';
|
|
4
|
+
import { LocalClient } from "./mcp-local.js";
|
|
5
|
+
import { pluginHooks, pluginCtx } from "./plugin-hooks.js";
|
|
6
|
+
import { getActiveRegistry } from "./registry-context.js";
|
|
7
|
+
import { describeCondition } from '@coffer-org/sdk/condition';
|
|
8
|
+
import { getEm } from "./db.js";
|
|
9
|
+
import { getPluginSettings } from "./plugin-runtime.js";
|
|
10
|
+
import { searchEmbeddings } from "./embeddings.js";
|
|
11
|
+
import { embedOne } from "./embed-openai.js";
|
|
12
|
+
import { getLogger } from "./log.js";
|
|
13
|
+
import { writePluginSettings, listSettings } from "./settings-write.js";
|
|
14
|
+
import { ValidationError, NotFoundError } from "./mutate.js";
|
|
15
|
+
const log = getLogger('mcp-tools');
|
|
16
|
+
export function formatHits(hits) {
|
|
17
|
+
if (hits.length === 0)
|
|
18
|
+
return 'No matching records.';
|
|
19
|
+
return hits
|
|
20
|
+
.map((h) => `[${h.type}/${h.recordId}] (dist ${h.distance.toFixed(3)})\n${h.snippet}`)
|
|
21
|
+
.join('\n\n');
|
|
22
|
+
}
|
|
23
|
+
const ok = (data) => ({
|
|
24
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
25
|
+
});
|
|
26
|
+
const fail = (text) => ({ content: [{ type: 'text', text }], isError: true });
|
|
27
|
+
export async function resolveRagDeps() {
|
|
28
|
+
const db = (await getPluginSettings('claude-agent'));
|
|
29
|
+
const enabled = db['rag_enabled'] !== false;
|
|
30
|
+
const embeddingApiKey = (process.env.OPENAI_API_KEY ?? db['openai_api_key'] ?? '');
|
|
31
|
+
const topK = Number(process.env.AGENT_RAG_TOP_K ?? db['rag_top_k'] ?? 5) || 5;
|
|
32
|
+
if (!enabled || !embeddingApiKey)
|
|
33
|
+
return null;
|
|
34
|
+
return { embeddingApiKey, topK };
|
|
35
|
+
}
|
|
36
|
+
export async function collectMcpTools(opts = {}) {
|
|
37
|
+
const out = [];
|
|
38
|
+
const client = new LocalClient();
|
|
39
|
+
const cache = new SchemaCache(client);
|
|
40
|
+
for (const t of buildTools(client, cache)) {
|
|
41
|
+
out.push({
|
|
42
|
+
server: 'coffer',
|
|
43
|
+
bareName: t.name,
|
|
44
|
+
httpName: t.name,
|
|
45
|
+
description: t.description,
|
|
46
|
+
inputSchema: t.inputSchema,
|
|
47
|
+
scope: 'crud',
|
|
48
|
+
role: 'member',
|
|
49
|
+
handler: t.handler,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
for (const [id, h] of Object.entries(pluginHooks)) {
|
|
53
|
+
for (const t of h.agent?.tools ?? []) {
|
|
54
|
+
out.push({
|
|
55
|
+
server: id,
|
|
56
|
+
bareName: t.name,
|
|
57
|
+
httpName: `${id}_${t.name}`,
|
|
58
|
+
description: t.description,
|
|
59
|
+
inputSchema: t.inputSchema,
|
|
60
|
+
scope: 'plugin',
|
|
61
|
+
role: t.role ?? 'member',
|
|
62
|
+
handler: async (args) => {
|
|
63
|
+
try {
|
|
64
|
+
const data = await t.handler(args, pluginCtx(id, getEm().fork()));
|
|
65
|
+
return ok(data);
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
return fail(`Error: ${e.message}`);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (opts.rag) {
|
|
75
|
+
const { embeddingApiKey, topK } = opts.rag;
|
|
76
|
+
out.push({
|
|
77
|
+
server: 'rag',
|
|
78
|
+
bareName: 'search_records',
|
|
79
|
+
httpName: 'search_records',
|
|
80
|
+
description: "Semantic search over the user's coffer records. Returns the most relevant records as type/id refs with a text snippet.",
|
|
81
|
+
inputSchema: { query: z.string(), k: z.number().int().positive().optional() },
|
|
82
|
+
scope: 'rag',
|
|
83
|
+
role: 'member',
|
|
84
|
+
handler: async (args) => {
|
|
85
|
+
try {
|
|
86
|
+
const { vector } = await embedOne(args.query, embeddingApiKey);
|
|
87
|
+
const hits = await searchEmbeddings(vector, args.k ?? topK);
|
|
88
|
+
return { content: [{ type: 'text', text: formatHits(hits) }] };
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
return fail(`Error: ${e.message}`);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (opts.includeAdmin) {
|
|
97
|
+
const actor = opts.actor ?? 'mcp';
|
|
98
|
+
out.push({
|
|
99
|
+
server: 'coffer',
|
|
100
|
+
bareName: 'list_settings',
|
|
101
|
+
httpName: 'list_settings',
|
|
102
|
+
description: 'List every plugin and system settings group (including "core"): each field\'s key/kind/required and the current values (secrets masked). Call before update_settings.',
|
|
103
|
+
inputSchema: {},
|
|
104
|
+
scope: 'settings',
|
|
105
|
+
role: 'admin',
|
|
106
|
+
handler: async () => {
|
|
107
|
+
try {
|
|
108
|
+
return ok(await listSettings());
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
return fail(`Error: ${e.message}`);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
out.push({
|
|
116
|
+
server: 'coffer',
|
|
117
|
+
bareName: 'update_settings',
|
|
118
|
+
httpName: 'update_settings',
|
|
119
|
+
description: 'Update a plugin or system settings group. plugin — the plugin id (e.g. "claude-agent", "core"); fields — only the settings to change (see list_settings). Send real secret values; the masked placeholder (********) is treated as unchanged.',
|
|
120
|
+
inputSchema: { plugin: z.string(), fields: z.record(z.string(), z.unknown()) },
|
|
121
|
+
scope: 'settings',
|
|
122
|
+
role: 'admin',
|
|
123
|
+
handler: async (args) => {
|
|
124
|
+
try {
|
|
125
|
+
const row = await writePluginSettings(getEm().fork(), args.plugin, args.fields, actor);
|
|
126
|
+
return ok(row);
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
if (e instanceof ValidationError) {
|
|
130
|
+
const lines = e.issues.map((i) => {
|
|
131
|
+
const o = i;
|
|
132
|
+
return `- ${o.field ?? '?'}: ${o.code ?? 'invalid'}`;
|
|
133
|
+
});
|
|
134
|
+
return fail(`Validation error:\n${lines.join('\n')}`);
|
|
135
|
+
}
|
|
136
|
+
if (e instanceof NotFoundError)
|
|
137
|
+
return fail(`Plugin '${String(args.plugin)}' has no settings.`);
|
|
138
|
+
return fail(`Error: ${e.message}`);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
export async function collectPluginInstructions(hooks = pluginHooks, emFactory = () => getEm().fork()) {
|
|
146
|
+
const out = [];
|
|
147
|
+
for (const [id, h] of Object.entries(hooks)) {
|
|
148
|
+
const a = h.agent;
|
|
149
|
+
if (a?.instructions == null)
|
|
150
|
+
continue;
|
|
151
|
+
try {
|
|
152
|
+
const instructions = typeof a.instructions === 'function' ? await a.instructions(pluginCtx(id, emFactory())) : a.instructions;
|
|
153
|
+
if (instructions)
|
|
154
|
+
out.push({ id, instructions });
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
log.warn(`plugin ${id}: instructions skipped — ${e.message}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
export function collectLibraryPurposes(reg) {
|
|
163
|
+
let registry = reg;
|
|
164
|
+
if (!registry) {
|
|
165
|
+
try {
|
|
166
|
+
registry = getActiveRegistry();
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (!registry)
|
|
173
|
+
return [];
|
|
174
|
+
return registry.libraries
|
|
175
|
+
.filter((v) => v.meta.agent)
|
|
176
|
+
.map((v) => ({
|
|
177
|
+
id: v.meta.id,
|
|
178
|
+
agent: v.meta.agent,
|
|
179
|
+
extends: registry.extends_.flatMap((e) => e.claude
|
|
180
|
+
? e.attachTo
|
|
181
|
+
.filter((a) => a.library === v.meta.id)
|
|
182
|
+
.map((a) => ({ id: e.id, shelf: a.shelf, claude: e.claude, showWhen: e.showWhen }))
|
|
183
|
+
: []),
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
export async function buildDomainSections() {
|
|
187
|
+
const libraries = collectLibraryPurposes();
|
|
188
|
+
let overview = null;
|
|
189
|
+
if (libraries.length) {
|
|
190
|
+
const blocks = libraries.map((v) => {
|
|
191
|
+
let s = `### ${v.id} — ${v.agent}`;
|
|
192
|
+
if (v.extends.length) {
|
|
193
|
+
s +=
|
|
194
|
+
'\nExtra field-sets some records carry (which one depends on the record):\n' +
|
|
195
|
+
v.extends
|
|
196
|
+
.map((e) => {
|
|
197
|
+
const when = describeCondition(e.showWhen, (f) => f).join(' and ');
|
|
198
|
+
return `- ${e.id} (on ${e.shelf}${when ? `, when ${when}` : ''}): ${e.claude.replace(/\s*\n\s*/g, ' ')}`;
|
|
199
|
+
})
|
|
200
|
+
.join('\n');
|
|
201
|
+
}
|
|
202
|
+
return s;
|
|
203
|
+
});
|
|
204
|
+
overview =
|
|
205
|
+
'## Libraries (what each holds / when to use it — pick the right one before searching)\n\n' + blocks.join('\n\n');
|
|
206
|
+
}
|
|
207
|
+
const dataModel = '## Data model\n' +
|
|
208
|
+
'Library (top-level area) → type/shelf (a kind of record, e.g. things/item) → record (addressed library/type/id) → fields. ' +
|
|
209
|
+
'Some field values are JSON (e.g. quantity {"value":2000,"unit":"ml"}); some are relations (hold another record\'s id); ' +
|
|
210
|
+
'some are collections (nested rows — an array). Extends add extra field-sets to a type\'s records, shown only when a ' +
|
|
211
|
+
'condition holds (the "when …" notes below); in a fetched record they sit under `_extends`. ' +
|
|
212
|
+
'Read: list_libraries → describe_type → list_records/get_record. Write: create_record/update_record (call describe_type first); ' +
|
|
213
|
+
'to remove a record use delete_record — do not blank its fields.';
|
|
214
|
+
const rules = (await collectPluginInstructions()).map(({ id, instructions }) => `## ${id}\n${instructions}`);
|
|
215
|
+
return overview ? [dataModel, overview, ...rules] : [dataModel, ...rules];
|
|
216
|
+
}
|
|
217
|
+
export function buildMcpInstructions(sections) {
|
|
218
|
+
const base = [
|
|
219
|
+
"Coffer is the user's personal database, organized into libraries (kitchen, people, finance, health, devices, home, garden, documents, travel, and more), each holding typed records.",
|
|
220
|
+
'Use the tools to read and write this data: call list_libraries to see what exists, describe_type before create_record/update_record, then list_records / get_record to read. When a search_records tool is available, use it for semantic lookup.',
|
|
221
|
+
'The per-library notes below name the shelves and the rules — not every field. For exact field names, types, and which are required, call describe_type(library, type) rather than guessing from the notes.',
|
|
222
|
+
'Record field values may be JSON-encoded (e.g. quantity {"value":2000,"unit":"ml"}) — parse them.',
|
|
223
|
+
].join('\n');
|
|
224
|
+
return sections.length ? `${base}\n\n${sections.join('\n\n')}` : base;
|
|
225
|
+
}
|
package/dist/msg-log.d.ts
CHANGED
package/dist/msg-log.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getEm } from "./db.js";
|
|
2
2
|
export async function logMessage(row) {
|
|
3
|
-
await getEm().fork().getConnection().execute(`INSERT INTO _orch_msg_log (ts, connector, chat_id, user_id, role, text,
|
|
4
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?,
|
|
3
|
+
await getEm().fork().getConnection().execute(`INSERT INTO _orch_msg_log (ts, connector, chat_id, user_id, role, text, tokens_in, tokens_out, ms)
|
|
4
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [new Date().toISOString(), row.connector, row.chatId, row.userId, row.role, row.text, row.tokensIn, row.tokensOut, row.ms]);
|
|
5
5
|
}
|
package/dist/mutate.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/core';
|
|
2
|
+
import type { ShelfDef } from '@coffer-org/sdk/shelf';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
export interface MutateCtx {
|
|
4
5
|
actor: string;
|
|
5
6
|
}
|
|
7
|
+
export type AfterBaseHook = (tx: EntityManager, id: number) => Promise<Record<string, unknown> | void>;
|
|
6
8
|
export interface ValidationIssue {
|
|
7
9
|
field: string;
|
|
8
10
|
code: string;
|
|
@@ -18,7 +20,7 @@ export declare class NotFoundError extends Error {
|
|
|
18
20
|
}
|
|
19
21
|
export declare function toIssue(i: z.ZodIssue): ValidationIssue;
|
|
20
22
|
export declare function listRecords(entityName: string): Promise<Record<string, unknown>[]>;
|
|
21
|
-
export declare function getRecord(m:
|
|
22
|
-
export declare function createRecord(m:
|
|
23
|
-
export declare function updateRecord(m:
|
|
24
|
-
export declare function deleteRecord(m:
|
|
23
|
+
export declare function getRecord(m: ShelfDef, entityName: string, id: number): Promise<Record<string, unknown> | undefined>;
|
|
24
|
+
export declare function createRecord(m: ShelfDef, entityName: string, input: unknown, ctx: MutateCtx, afterBase?: AfterBaseHook): Promise<Record<string, unknown>>;
|
|
25
|
+
export declare function updateRecord(m: ShelfDef, entityName: string, id: number, input: unknown, ctx: MutateCtx, afterBase?: AfterBaseHook): Promise<Record<string, unknown>>;
|
|
26
|
+
export declare function deleteRecord(m: ShelfDef, entityName: string, id: number, ctx: MutateCtx, afterBase?: AfterBaseHook): Promise<void>;
|
package/dist/mutate.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { serialize } from '@mikro-orm/core';
|
|
2
|
-
import { buildZodObject, buildZodObjectPartial, fieldEntries } from '@coffer-org/sdk/
|
|
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
5
|
import { getEm } from "./db.js";
|
|
6
|
+
import { notifyRecordsChanged } from "./index-signal.js";
|
|
6
7
|
import { encodeTemporal, decodeTemporal } from "./temporal.js";
|
|
7
8
|
import { splitCollections, writeCollections, deleteCollections, flattenEmbedded, nestEmbedded, readCollections, } from "./collection-io.js";
|
|
8
9
|
function nowIso() {
|
|
@@ -63,7 +64,7 @@ export async function getRecord(m, entityName, id) {
|
|
|
63
64
|
const collections = await readCollections(fork, m, id);
|
|
64
65
|
return { ...nested, ...collections };
|
|
65
66
|
}
|
|
66
|
-
export async function createRecord(m, entityName, input, ctx) {
|
|
67
|
+
export async function createRecord(m, entityName, input, ctx, afterBase) {
|
|
67
68
|
const parsed = buildZodObject(m).safeParse(input);
|
|
68
69
|
if (!parsed.success)
|
|
69
70
|
throw new ValidationError(parsed.error.issues.map(toIssue));
|
|
@@ -79,11 +80,17 @@ export async function createRecord(m, entityName, input, ctx) {
|
|
|
79
80
|
await tx.flush();
|
|
80
81
|
id = entity.id;
|
|
81
82
|
await writeCollections(tx, m, id, collections);
|
|
82
|
-
|
|
83
|
+
const _extends = afterBase ? await afterBase(tx, id) : undefined;
|
|
84
|
+
writeEvent(tx, ctx.actor, 'create', `${m.library}/${m.shelf}`, id, null, {
|
|
85
|
+
...parsedData,
|
|
86
|
+
id,
|
|
87
|
+
...(_extends ? { _extends } : {}),
|
|
88
|
+
});
|
|
83
89
|
});
|
|
90
|
+
notifyRecordsChanged();
|
|
84
91
|
return { ...parsedData, id, created_at: ts, updated_at: ts };
|
|
85
92
|
}
|
|
86
|
-
export async function updateRecord(m, entityName, id, input, ctx) {
|
|
93
|
+
export async function updateRecord(m, entityName, id, input, ctx, afterBase) {
|
|
87
94
|
const parsed = buildZodObjectPartial(m).safeParse(input);
|
|
88
95
|
if (!parsed.success)
|
|
89
96
|
throw new ValidationError(parsed.error.issues.map(toIssue));
|
|
@@ -117,13 +124,15 @@ export async function updateRecord(m, entityName, id, input, ctx) {
|
|
|
117
124
|
await tx.upsert(entityName, dbRow);
|
|
118
125
|
await writeCollections(tx, m, id, collections);
|
|
119
126
|
const allCollections = await readCollections(tx, m, id);
|
|
120
|
-
const
|
|
121
|
-
|
|
127
|
+
const _extends = afterBase ? await afterBase(tx, id) : undefined;
|
|
128
|
+
const after = { ...merged, ...allCollections, updated_at: ts, ...(_extends ? { _extends } : {}) };
|
|
129
|
+
writeEvent(tx, ctx.actor, 'update', `${m.library}/${m.shelf}`, id, existing, after);
|
|
122
130
|
result = after;
|
|
123
131
|
});
|
|
132
|
+
notifyRecordsChanged();
|
|
124
133
|
return result;
|
|
125
134
|
}
|
|
126
|
-
export async function deleteRecord(m, entityName, id, ctx) {
|
|
135
|
+
export async function deleteRecord(m, entityName, id, ctx, afterBase) {
|
|
127
136
|
await getEm()
|
|
128
137
|
.fork()
|
|
129
138
|
.transactional(async (tx) => {
|
|
@@ -132,7 +141,10 @@ export async function deleteRecord(m, entityName, id, ctx) {
|
|
|
132
141
|
throw new NotFoundError();
|
|
133
142
|
const existing = serialize(found);
|
|
134
143
|
await deleteCollections(tx, m, id);
|
|
144
|
+
if (afterBase)
|
|
145
|
+
await afterBase(tx, id);
|
|
135
146
|
tx.remove(found);
|
|
136
|
-
writeEvent(tx, ctx.actor, 'delete', `${m.
|
|
147
|
+
writeEvent(tx, ctx.actor, 'delete', `${m.library}/${m.shelf}`, id, existing, null);
|
|
137
148
|
});
|
|
149
|
+
notifyRecordsChanged();
|
|
138
150
|
}
|
package/dist/plugin-hooks.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { EntityManager } from '@mikro-orm/core';
|
|
|
2
2
|
import type { z } from 'zod';
|
|
3
3
|
import type { ColumnType } from '@coffer-org/sdk/fields';
|
|
4
4
|
import { type Logger } from '@coffer-org/sdk/logger';
|
|
5
|
+
export type { BackgroundTask } from './background-scheduler.ts';
|
|
5
6
|
export interface TableOps {
|
|
6
7
|
renameColumn(from: string, to: string): Promise<void>;
|
|
7
8
|
fill(column: string, value: string | number | boolean | null, opts?: {
|
|
@@ -28,21 +29,30 @@ export interface Seed {
|
|
|
28
29
|
name: string;
|
|
29
30
|
run(ctx: PluginCtx): Promise<void> | void;
|
|
30
31
|
}
|
|
32
|
+
export type AuthRole = 'admin' | 'member';
|
|
31
33
|
export interface AgentTool {
|
|
32
34
|
name: string;
|
|
33
35
|
description: string;
|
|
34
36
|
inputSchema: z.ZodRawShape;
|
|
35
37
|
handler: (args: Record<string, unknown>, ctx: PluginCtx) => Promise<unknown> | unknown;
|
|
38
|
+
role?: AuthRole;
|
|
36
39
|
}
|
|
37
40
|
export interface AgentContribution {
|
|
38
41
|
instructions?: string | ((ctx: PluginCtx) => string | Promise<string>);
|
|
39
42
|
tools?: AgentTool[];
|
|
40
43
|
}
|
|
44
|
+
export type PluginAction = (body: Record<string, unknown>) => Promise<unknown>;
|
|
45
|
+
export declare class HttpError extends Error {
|
|
46
|
+
status: number;
|
|
47
|
+
constructor(status: number, message: string);
|
|
48
|
+
}
|
|
41
49
|
export interface PluginHooks {
|
|
42
50
|
migrations?: Migration[];
|
|
43
51
|
seed?: Seed[];
|
|
44
52
|
init?(ctx: PluginCtx): Promise<void> | void;
|
|
45
53
|
teardown?(ctx: PluginCtx): Promise<void> | void;
|
|
46
54
|
agent?: AgentContribution;
|
|
55
|
+
backgroundTasks?: import('./background-scheduler.ts').BackgroundTask[];
|
|
56
|
+
actions?: Record<string, PluginAction>;
|
|
47
57
|
}
|
|
48
58
|
export declare const pluginHooks: Record<string, PluginHooks>;
|
package/dist/plugin-hooks.js
CHANGED
|
@@ -2,4 +2,12 @@ import { getLogger } from '@coffer-org/sdk/logger';
|
|
|
2
2
|
export function pluginCtx(id, em) {
|
|
3
3
|
return { em, log: getLogger(id) };
|
|
4
4
|
}
|
|
5
|
+
export class HttpError extends Error {
|
|
6
|
+
status;
|
|
7
|
+
constructor(status, message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.name = 'HttpError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
5
13
|
export const pluginHooks = {};
|
package/dist/plugin-runtime.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { PluginManifest } from '@coffer-org/sdk/plugin';
|
|
|
3
3
|
export declare function getPlugins(): Promise<PluginManifest[]>;
|
|
4
4
|
export declare function readDisabled(): Promise<Set<string>>;
|
|
5
5
|
export declare function getPluginSettings(pluginId: string): Promise<Record<string, unknown>>;
|
|
6
|
+
export declare function requireSettings<K extends string>(pluginId: string, keys: readonly K[]): Promise<Record<K, string>>;
|
|
6
7
|
export declare function initStorage(): Promise<Set<string>>;
|
|
7
8
|
export declare function initPlugins(): Promise<Registry>;
|
|
8
9
|
export declare function teardownPlugins(): Promise<void>;
|
package/dist/plugin-runtime.js
CHANGED
|
@@ -3,13 +3,14 @@ import { composeRegistry } from '@coffer-org/core/compose';
|
|
|
3
3
|
import { getLogger } from '@coffer-org/sdk/logger';
|
|
4
4
|
import { initDb, getOrm, getEm } from "./db.js";
|
|
5
5
|
import { syncSchema } from "./schema-sync.js";
|
|
6
|
-
import { systemEntities, buildPluginEntities,
|
|
7
|
-
import { pluginHooks, pluginCtx } from "./plugin-hooks.js";
|
|
6
|
+
import { systemEntities, buildPluginEntities, shelfTableName } from "./entity-schema.js";
|
|
7
|
+
import { pluginHooks, pluginCtx, HttpError } from "./plugin-hooks.js";
|
|
8
8
|
import { discoverPlugins, loadServerHooks } from "./plugin-discovery.js";
|
|
9
9
|
import { runMigrations, assertSafeRequired } from "./migrations.js";
|
|
10
10
|
import { runSeeds } from "./seeds.js";
|
|
11
11
|
import { setActiveRegistry } from "./registry-context.js";
|
|
12
12
|
import { migrateEmbeddingVectorsToBlob } from "./embeddings.js";
|
|
13
|
+
import { startScheduler, stopScheduler } from "./background-scheduler.js";
|
|
13
14
|
const log = getLogger('plugins');
|
|
14
15
|
let _plugins = null;
|
|
15
16
|
export async function getPlugins() {
|
|
@@ -36,6 +37,22 @@ export async function getPluginSettings(pluginId) {
|
|
|
36
37
|
return {};
|
|
37
38
|
}
|
|
38
39
|
}
|
|
40
|
+
export async function requireSettings(pluginId, keys) {
|
|
41
|
+
const s = await getPluginSettings(pluginId);
|
|
42
|
+
const out = {};
|
|
43
|
+
const missing = [];
|
|
44
|
+
for (const k of keys) {
|
|
45
|
+
const v = s[k];
|
|
46
|
+
if (v == null || v === '')
|
|
47
|
+
missing.push(k);
|
|
48
|
+
else
|
|
49
|
+
out[k] = String(v);
|
|
50
|
+
}
|
|
51
|
+
if (missing.length) {
|
|
52
|
+
throw new HttpError(400, `${pluginId} settings not configured: ${missing.join(', ')}. Save them first.`);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
39
56
|
async function seedPluginRows() {
|
|
40
57
|
const fork = getEm().fork();
|
|
41
58
|
const existing = new Set((await fork.find('_Plugin', {})).map((r) => r.id));
|
|
@@ -74,6 +91,7 @@ export async function initPlugins() {
|
|
|
74
91
|
setActiveRegistry(reg);
|
|
75
92
|
Object.assign(pluginHooks, await loadServerHooks());
|
|
76
93
|
await runSeeds({ em: getEm().fork(), plugins: reg.order, hooks: pluginHooks });
|
|
94
|
+
const bgTasks = [];
|
|
77
95
|
for (const p of reg.order) {
|
|
78
96
|
const h = pluginHooks[p.id];
|
|
79
97
|
try {
|
|
@@ -81,17 +99,23 @@ export async function initPlugins() {
|
|
|
81
99
|
await h.init(pluginCtx(p.id, getEm().fork()));
|
|
82
100
|
log.debug(`${p.id}: init ✓`);
|
|
83
101
|
}
|
|
102
|
+
for (const task of h?.backgroundTasks ?? [])
|
|
103
|
+
bgTasks.push(task);
|
|
84
104
|
}
|
|
85
105
|
catch (err) {
|
|
86
106
|
log.error(`${p.id}: initialization failure`, err);
|
|
87
107
|
throw err;
|
|
88
108
|
}
|
|
89
109
|
}
|
|
110
|
+
startScheduler(bgTasks);
|
|
111
|
+
if (bgTasks.length)
|
|
112
|
+
log.debug(`scheduler: ${bgTasks.length} background task(s): ${bgTasks.map((t) => t.name).join(' ')}`);
|
|
90
113
|
log.info(`active: ${reg.order.map((p) => p.id).join(' ')}` +
|
|
91
114
|
(disabled.size ? ` | disabled: ${[...disabled].join(' ')}` : ''));
|
|
92
115
|
return reg;
|
|
93
116
|
}
|
|
94
117
|
export async function teardownPlugins() {
|
|
118
|
+
stopScheduler();
|
|
95
119
|
Object.assign(pluginHooks, await loadServerHooks());
|
|
96
120
|
const disabled = await readDisabled();
|
|
97
121
|
const reg = composeRegistry(await getPlugins(), { disabled });
|
|
@@ -115,15 +139,15 @@ export async function teardownPlugin(id) {
|
|
|
115
139
|
log.debug(`${id}: teardown ✓`);
|
|
116
140
|
}
|
|
117
141
|
}
|
|
118
|
-
function
|
|
142
|
+
function pluginShelves(p) {
|
|
119
143
|
return [
|
|
120
|
-
...(p.
|
|
121
|
-
...(p.
|
|
144
|
+
...(p.libraries ?? []).flatMap((v) => v.shelves.map((m) => ({ library: m.library, shelf: m.shelf }))),
|
|
145
|
+
...(p.libraryShelves ?? []).map((m) => ({ library: m.library, shelf: m.shelf })),
|
|
122
146
|
];
|
|
123
147
|
}
|
|
124
148
|
function pluginTables(p) {
|
|
125
149
|
return [
|
|
126
|
-
...
|
|
150
|
+
...pluginShelves(p).map(({ library, shelf }) => shelfTableName(library, shelf)),
|
|
127
151
|
...(p.extends_ ?? []).map((e) => `extend__${e.id}`),
|
|
128
152
|
...(p.settings && p.settings.fields.length > 0 ? [`_settings__${p.id}`] : []),
|
|
129
153
|
];
|