@coffer-org/server 1.7.1 → 1.9.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.d.ts +1 -0
- package/dist/auth-api.js +20 -10
- 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 +9 -5
- package/dist/entity-schema.js +64 -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 +68 -160
- 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/oauth-api.d.ts +2 -0
- package/dist/oauth-api.js +281 -0
- package/dist/oauth-store.d.ts +56 -0
- package/dist/oauth-store.js +159 -0
- 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/public-url.d.ts +4 -0
- package/dist/public-url.js +35 -0
- 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 +63 -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,117 @@
|
|
|
1
|
+
import Fastify from 'fastify';
|
|
2
|
+
import { initDb, closeDb } from "./db.js";
|
|
3
|
+
import { systemEntities } from "./entity-schema.js";
|
|
4
|
+
import { registerAuthApi, resolveRequestUser, PUBLIC_API_PATHS } from "./auth-api.js";
|
|
5
|
+
import { registerMcpHttp } from "./mcp-http.js";
|
|
6
|
+
import { createUser, createApiToken } from "./auth-store.js";
|
|
7
|
+
import { pluginHooks } from "./plugin-hooks.js";
|
|
8
|
+
let currentAdminToken;
|
|
9
|
+
let currentMemberToken;
|
|
10
|
+
export async function freshMcpApp(opts = {}) {
|
|
11
|
+
process.env['DB_PATH'] = ':memory:';
|
|
12
|
+
const orm = await initDb(systemEntities);
|
|
13
|
+
await orm.schema.update({ safe: false, dropTables: false });
|
|
14
|
+
const app = Fastify();
|
|
15
|
+
app.addHook('onRequest', async (req, reply) => {
|
|
16
|
+
const gated = req.url.startsWith('/api/') || req.url.startsWith('/uploads/') || req.url === '/mcp' || req.url.startsWith('/mcp?');
|
|
17
|
+
if (!gated)
|
|
18
|
+
return;
|
|
19
|
+
if (PUBLIC_API_PATHS.some((p) => req.url.startsWith(p)))
|
|
20
|
+
return;
|
|
21
|
+
const user = await resolveRequestUser(req);
|
|
22
|
+
if (!user)
|
|
23
|
+
return reply.code(401).send({ error: 'unauthorized' });
|
|
24
|
+
req.user = user;
|
|
25
|
+
});
|
|
26
|
+
await registerAuthApi(app);
|
|
27
|
+
await registerMcpHttp(app);
|
|
28
|
+
const admin = await createUser({ login: 'admin', password: 'hunter2', displayName: 'Admin', role: 'admin' });
|
|
29
|
+
const member = await createUser({ login: 'member', password: 'hunter2', displayName: 'Member', role: 'member' });
|
|
30
|
+
currentAdminToken = `admin-raw-token-${Math.random().toString(36).slice(2)}`;
|
|
31
|
+
currentMemberToken = `member-raw-token-${Math.random().toString(36).slice(2)}`;
|
|
32
|
+
await createApiToken(admin.id, 'admin-token', currentAdminToken);
|
|
33
|
+
await createApiToken(member.id, 'member-token', currentMemberToken);
|
|
34
|
+
if (opts.withAdminTool || opts.withInstructions) {
|
|
35
|
+
pluginHooks['demo'] = {
|
|
36
|
+
agent: {
|
|
37
|
+
...(opts.withInstructions ? { instructions: 'Demo library — widgets.' } : {}),
|
|
38
|
+
...(opts.withAdminTool
|
|
39
|
+
? { tools: [{ name: 'danger', description: 'd', inputSchema: {}, role: 'admin', handler: () => 1 }] }
|
|
40
|
+
: {}),
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const origClose = app.close.bind(app);
|
|
45
|
+
app.close = ((...args) => {
|
|
46
|
+
delete pluginHooks['demo'];
|
|
47
|
+
return origClose(...args);
|
|
48
|
+
});
|
|
49
|
+
return app;
|
|
50
|
+
}
|
|
51
|
+
export async function adminToken() {
|
|
52
|
+
if (!currentAdminToken)
|
|
53
|
+
throw new Error('adminToken() called before freshMcpApp()');
|
|
54
|
+
return currentAdminToken;
|
|
55
|
+
}
|
|
56
|
+
export async function memberToken() {
|
|
57
|
+
if (!currentMemberToken)
|
|
58
|
+
throw new Error('memberToken() called before freshMcpApp()');
|
|
59
|
+
return currentMemberToken;
|
|
60
|
+
}
|
|
61
|
+
export function parseRpcResult(res) {
|
|
62
|
+
const contentType = String(res.headers['content-type'] ?? '');
|
|
63
|
+
const raw = [];
|
|
64
|
+
if (contentType.includes('text/event-stream')) {
|
|
65
|
+
for (const line of res.body.split('\n')) {
|
|
66
|
+
const trimmed = line.trim();
|
|
67
|
+
if (trimmed.startsWith('data:'))
|
|
68
|
+
raw.push(trimmed.slice('data:'.length).trim());
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
raw.push(res.body);
|
|
73
|
+
}
|
|
74
|
+
for (const s of raw) {
|
|
75
|
+
if (!s)
|
|
76
|
+
continue;
|
|
77
|
+
try {
|
|
78
|
+
const parsed = JSON.parse(s);
|
|
79
|
+
const msg = (Array.isArray(parsed) ? parsed : [parsed]).find((m) => m.result);
|
|
80
|
+
if (msg?.result)
|
|
81
|
+
return msg.result;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
export function parseToolNames(res) {
|
|
89
|
+
const contentType = String(res.headers['content-type'] ?? '');
|
|
90
|
+
const messages = [];
|
|
91
|
+
if (contentType.includes('text/event-stream')) {
|
|
92
|
+
for (const line of res.body.split('\n')) {
|
|
93
|
+
const trimmed = line.trim();
|
|
94
|
+
if (!trimmed.startsWith('data:'))
|
|
95
|
+
continue;
|
|
96
|
+
const jsonStr = trimmed.slice('data:'.length).trim();
|
|
97
|
+
if (!jsonStr)
|
|
98
|
+
continue;
|
|
99
|
+
try {
|
|
100
|
+
messages.push(JSON.parse(jsonStr));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(res.body);
|
|
109
|
+
messages.push(...(Array.isArray(parsed) ? parsed : [parsed]));
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const withTools = messages.find((m) => m.result?.tools);
|
|
115
|
+
return (withTools?.result?.tools ?? []).map((t) => t.name);
|
|
116
|
+
}
|
|
117
|
+
export { closeDb };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CofferClientApi } from '@coffer-org/mcp/client';
|
|
2
|
+
export declare function mapError(e: unknown): never;
|
|
3
|
+
export declare class LocalClient implements CofferClientApi {
|
|
4
|
+
getSchema(): Promise<unknown>;
|
|
5
|
+
listRecords(library: string, type: string, query?: Record<string, unknown>): Promise<unknown>;
|
|
6
|
+
getRecord(library: string, type: string, id: number): Promise<unknown>;
|
|
7
|
+
createRecord(library: string, type: string, fields: Record<string, unknown>): Promise<unknown>;
|
|
8
|
+
updateRecord(library: string, type: string, id: number, fields: Record<string, unknown>): Promise<unknown>;
|
|
9
|
+
deleteRecord(library: string, type: string, id: number): Promise<unknown>;
|
|
10
|
+
}
|
|
@@ -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
|
}
|