@coffer-org/server 7.2.0 → 7.3.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 +4 -0
- package/dist/auth-api.js +53 -0
- package/dist/auth-store.d.ts +2 -0
- package/dist/auth-store.js +1 -0
- package/dist/entity-schema.d.ts +2 -0
- package/dist/entity-schema.js +26 -0
- package/dist/identity-link.d.ts +15 -0
- package/dist/identity-link.js +76 -0
- package/dist/index.js +8 -1
- package/dist/mcp-http.js +7 -3
- package/dist/mcp-tools.d.ts +4 -3
- package/dist/mcp-tools.js +84 -84
- package/dist/media/image.d.ts +23 -0
- package/dist/media/image.js +103 -0
- package/dist/media/index.d.ts +1 -0
- package/dist/media/index.js +1 -0
- package/dist/migrations.js +1 -1
- package/dist/orchestrator/agent-capabilities.d.ts +2 -2
- package/dist/orchestrator/agent-capabilities.js +3 -3
- package/dist/orchestrator/allow.d.ts +1 -16
- package/dist/orchestrator/allow.js +3 -53
- package/dist/orchestrator/config.js +0 -1
- package/dist/orchestrator/context-facts.d.ts +27 -0
- package/dist/orchestrator/context-facts.js +89 -0
- package/dist/orchestrator/conversation-access.d.ts +9 -0
- package/dist/orchestrator/conversation-access.js +12 -0
- package/dist/orchestrator/environment.d.ts +1 -0
- package/dist/orchestrator/environment.js +10 -0
- package/dist/orchestrator/file-inspection.d.ts +2 -2
- package/dist/orchestrator/file-inspection.js +41 -19
- package/dist/orchestrator/index.d.ts +15 -9
- package/dist/orchestrator/index.js +13 -7
- package/dist/orchestrator/live-message.d.ts +7 -4
- package/dist/orchestrator/live-message.js +48 -31
- package/dist/orchestrator/pipeline.d.ts +25 -4
- package/dist/orchestrator/pipeline.js +214 -94
- package/dist/orchestrator/registry.d.ts +4 -2
- package/dist/orchestrator/registry.js +10 -1
- package/dist/orchestrator/system-areas.d.ts +12 -0
- package/dist/orchestrator/system-areas.js +63 -0
- package/dist/orchestrator/system-capabilities.js +1 -1
- package/dist/orchestrator/turn-context.d.ts +18 -0
- package/dist/orchestrator/turn-context.js +39 -0
- package/dist/orchestrator/types.d.ts +101 -44
- package/dist/plugin-hooks.d.ts +26 -0
- package/dist/plugin-http-mounts.d.ts +18 -0
- package/dist/plugin-http-mounts.js +94 -0
- package/dist/plugin-runtime.js +2 -2
- package/dist/records-api.js +15 -3
- package/dist/system-settings.js +0 -1
- package/dist/thread-state.d.ts +14 -0
- package/dist/thread-state.js +71 -11
- package/dist/thread-store.d.ts +5 -3
- package/dist/thread-store.js +12 -9
- package/dist/turn-gate.d.ts +8 -0
- package/dist/turn-gate.js +39 -0
- package/package.json +7 -2
package/dist/auth-api.d.ts
CHANGED
|
@@ -7,6 +7,10 @@ declare module 'fastify' {
|
|
|
7
7
|
}
|
|
8
8
|
export declare const PUBLIC_API_PATHS: string[];
|
|
9
9
|
export declare function startPasswordSession(reply: FastifyReply, login: string, password: string): Promise<AuthUser | null>;
|
|
10
|
+
export declare function parseBasicCredential(header: string | undefined): {
|
|
11
|
+
login: string;
|
|
12
|
+
secret: string;
|
|
13
|
+
} | null;
|
|
10
14
|
export declare function resolveRequestUser(req: FastifyRequest): Promise<AuthUser | null>;
|
|
11
15
|
export declare function requireAdmin(req: FastifyRequest, reply: FastifyReply): boolean;
|
|
12
16
|
export declare function registerAuthApi(app: FastifyInstance): Promise<void>;
|
package/dist/auth-api.js
CHANGED
|
@@ -2,6 +2,8 @@ import { field } from '@coffer-org/sdk/fields';
|
|
|
2
2
|
import { materializeTree } from '@coffer-org/sdk/materialize/pipeline';
|
|
3
3
|
import { countUsers, createUser, findUserByLogin, findUserById, getPasswordHash, listUsers, updateUser, deleteUser, countAdmins, createSession, resolveSession, deleteSession, createApiToken, resolveApiToken, listApiTokens, revokeApiToken, } from "./auth-store.js";
|
|
4
4
|
import { resolveAccessToken } from "./oauth-store.js";
|
|
5
|
+
import { listLinks, unlink, mintLinkCode } from "./identity-link.js";
|
|
6
|
+
import { listLinkableConnectors, connectorLinkUrl } from "./orchestrator/index.js";
|
|
5
7
|
import { mcpResource } from "./public-url.js";
|
|
6
8
|
import { verifyPassword } from "./auth-crypto.js";
|
|
7
9
|
import { maskSecrets, preserveSecrets } from "./field-masking.js";
|
|
@@ -11,6 +13,7 @@ import { USERS_SHELF } from '@coffer-org/sdk/users-shelf';
|
|
|
11
13
|
const PASSWORD_FIELDS = materializeTree({ password: field.password({}) });
|
|
12
14
|
const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
13
15
|
const COOKIE_NAME = 'sid';
|
|
16
|
+
const LINK_CODE_TTL_MS = 10 * 60 * 1000;
|
|
14
17
|
export const PUBLIC_API_PATHS = ['/api/auth/status', '/api/auth/login', '/api/auth/setup'];
|
|
15
18
|
function readCookie(req, name) {
|
|
16
19
|
const header = req.headers.cookie;
|
|
@@ -37,12 +40,32 @@ export async function startPasswordSession(reply, login, password) {
|
|
|
37
40
|
setCookie(reply, raw, SESSION_TTL_MS / 1000);
|
|
38
41
|
return row;
|
|
39
42
|
}
|
|
43
|
+
export function parseBasicCredential(header) {
|
|
44
|
+
if (!header?.startsWith('Basic '))
|
|
45
|
+
return null;
|
|
46
|
+
let decoded;
|
|
47
|
+
try {
|
|
48
|
+
decoded = Buffer.from(header.slice('Basic '.length).trim(), 'base64').toString('utf8');
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const at = decoded.indexOf(':');
|
|
54
|
+
if (at <= 0)
|
|
55
|
+
return null;
|
|
56
|
+
return { login: decoded.slice(0, at), secret: decoded.slice(at + 1) };
|
|
57
|
+
}
|
|
40
58
|
export async function resolveRequestUser(req) {
|
|
41
59
|
const auth = req.headers.authorization;
|
|
42
60
|
if (auth?.startsWith('Bearer ')) {
|
|
43
61
|
const raw = auth.slice('Bearer '.length);
|
|
44
62
|
return (await resolveApiToken(raw)) ?? (await resolveAccessToken(raw, await mcpResource(req)));
|
|
45
63
|
}
|
|
64
|
+
const basic = parseBasicCredential(auth);
|
|
65
|
+
if (basic) {
|
|
66
|
+
const user = await resolveApiToken(basic.secret);
|
|
67
|
+
return user && user.login === basic.login ? user : null;
|
|
68
|
+
}
|
|
46
69
|
const sid = readCookie(req, COOKIE_NAME);
|
|
47
70
|
if (!sid)
|
|
48
71
|
return null;
|
|
@@ -208,4 +231,34 @@ export async function registerAuthApi(app) {
|
|
|
208
231
|
return reply.code(404).send({ error: 'not_found' });
|
|
209
232
|
return { ok: true };
|
|
210
233
|
});
|
|
234
|
+
app.get('/api/auth/links', async (req, reply) => {
|
|
235
|
+
if (!req.user)
|
|
236
|
+
return reply.code(401).send({ error: 'unauthorized' });
|
|
237
|
+
return { links: await listLinks(req.user.id), linkable: listLinkableConnectors() };
|
|
238
|
+
});
|
|
239
|
+
app.post('/api/auth/links', async (req, reply) => {
|
|
240
|
+
if (!req.user)
|
|
241
|
+
return reply.code(401).send({ error: 'unauthorized' });
|
|
242
|
+
const minted = await mintLinkCode(req.user.id, LINK_CODE_TTL_MS);
|
|
243
|
+
const links = {};
|
|
244
|
+
for (const connectorId of listLinkableConnectors()) {
|
|
245
|
+
const url = connectorLinkUrl(connectorId, minted.raw);
|
|
246
|
+
if (url)
|
|
247
|
+
links[connectorId] = url;
|
|
248
|
+
}
|
|
249
|
+
return { ...minted, links };
|
|
250
|
+
});
|
|
251
|
+
app.delete('/api/auth/links/:connector/:externalId', async (req, reply) => {
|
|
252
|
+
if (!req.user)
|
|
253
|
+
return reply.code(401).send({ error: 'unauthorized' });
|
|
254
|
+
const { connector, externalId } = req.params;
|
|
255
|
+
const own = await listLinks(req.user.id);
|
|
256
|
+
const owns = own.some((l) => l.connector === connector && l.externalId === externalId);
|
|
257
|
+
if (!owns)
|
|
258
|
+
return reply.code(404).send({ error: 'not_found' });
|
|
259
|
+
const ok = await unlink(connector, externalId);
|
|
260
|
+
if (!ok)
|
|
261
|
+
return reply.code(404).send({ error: 'not_found' });
|
|
262
|
+
return { ok: true };
|
|
263
|
+
});
|
|
211
264
|
}
|
package/dist/auth-store.d.ts
CHANGED
package/dist/auth-store.js
CHANGED
package/dist/entity-schema.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export declare const MsgLogSchema: EntitySchema<any, never, import("@mikro-orm/c
|
|
|
25
25
|
export declare const UserSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
|
|
26
26
|
export declare const SessionSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
|
|
27
27
|
export declare const ApiTokenSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
|
|
28
|
+
export declare const IdentityLinkSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
|
|
29
|
+
export declare const LinkCodeSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
|
|
28
30
|
export declare const OAuthClientSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
|
|
29
31
|
export declare const OAuthCodeSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
|
|
30
32
|
export declare const OAuthTokenSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
|
package/dist/entity-schema.js
CHANGED
|
@@ -209,6 +209,9 @@ export const ThreadStateSchema = new EntitySchema({
|
|
|
209
209
|
chat_id: { type: 'text', primary: true },
|
|
210
210
|
agent_id: { type: 'text', nullable: true },
|
|
211
211
|
preset_id: { type: 'text', nullable: true },
|
|
212
|
+
title: { type: 'text', nullable: true },
|
|
213
|
+
owner: { type: 'text', nullable: true },
|
|
214
|
+
visibility: { type: 'text', nullable: true },
|
|
212
215
|
updated_at: { type: 'text' },
|
|
213
216
|
},
|
|
214
217
|
});
|
|
@@ -274,6 +277,27 @@ export const ApiTokenSchema = new EntitySchema({
|
|
|
274
277
|
revoked: { type: 'integer' },
|
|
275
278
|
},
|
|
276
279
|
});
|
|
280
|
+
export const IdentityLinkSchema = new EntitySchema({
|
|
281
|
+
name: '_IdentityLink',
|
|
282
|
+
tableName: '_identity_links',
|
|
283
|
+
properties: {
|
|
284
|
+
id: { type: 'integer', primary: true, autoincrement: true },
|
|
285
|
+
connector: { type: 'text' },
|
|
286
|
+
external_id: { type: 'text' },
|
|
287
|
+
user_id: { type: 'integer' },
|
|
288
|
+
linked_at: { type: 'text' },
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
export const LinkCodeSchema = new EntitySchema({
|
|
292
|
+
name: '_LinkCode',
|
|
293
|
+
tableName: '_link_codes',
|
|
294
|
+
properties: {
|
|
295
|
+
code_hash: { type: 'text', primary: true },
|
|
296
|
+
user_id: { type: 'integer' },
|
|
297
|
+
expires_at: { type: 'text' },
|
|
298
|
+
created_at: { type: 'text' },
|
|
299
|
+
},
|
|
300
|
+
});
|
|
277
301
|
export const OAuthClientSchema = new EntitySchema({
|
|
278
302
|
name: '_OAuthClient',
|
|
279
303
|
tableName: '_oauth_clients',
|
|
@@ -331,5 +355,7 @@ export const systemEntities = [
|
|
|
331
355
|
OAuthTokenSchema,
|
|
332
356
|
ThreadMessageSchema,
|
|
333
357
|
ThreadStateSchema,
|
|
358
|
+
IdentityLinkSchema,
|
|
359
|
+
LinkCodeSchema,
|
|
334
360
|
buildSettingsEntitySchema(SYSTEM_SETTINGS_ID, SYSTEM_SETTINGS),
|
|
335
361
|
];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface IdentityLink {
|
|
2
|
+
connector: string;
|
|
3
|
+
externalId: string;
|
|
4
|
+
userId: number;
|
|
5
|
+
linkedAt: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function findLinkedUser(connector: string, externalId: string): Promise<number | null>;
|
|
8
|
+
export declare function listLinks(userId: number): Promise<IdentityLink[]>;
|
|
9
|
+
export declare function unlink(connector: string, externalId: string): Promise<boolean>;
|
|
10
|
+
export declare function mintLinkCode(userId: number, ttlMs: number): Promise<{
|
|
11
|
+
raw: string;
|
|
12
|
+
expiresAt: string;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function redeemLinkCode(raw: string, connector: string, externalId: string): Promise<number | null>;
|
|
15
|
+
export declare function pruneLinkCodes(): Promise<void>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { getEm } from "./db.js";
|
|
2
|
+
import { generateToken, hashToken, findUserById } from "./auth-store.js";
|
|
3
|
+
export async function findLinkedUser(connector, externalId) {
|
|
4
|
+
const em = getEm().fork();
|
|
5
|
+
const row = (await em.findOne('_IdentityLink', {
|
|
6
|
+
connector,
|
|
7
|
+
external_id: externalId,
|
|
8
|
+
}));
|
|
9
|
+
if (!row)
|
|
10
|
+
return null;
|
|
11
|
+
const user = await findUserById(row.user_id);
|
|
12
|
+
return user && !user.disabled ? row.user_id : null;
|
|
13
|
+
}
|
|
14
|
+
export async function listLinks(userId) {
|
|
15
|
+
const em = getEm().fork();
|
|
16
|
+
const rows = (await em.find('_IdentityLink', { user_id: userId }));
|
|
17
|
+
return rows.map((r) => ({
|
|
18
|
+
connector: r.connector,
|
|
19
|
+
externalId: r.external_id,
|
|
20
|
+
userId: r.user_id,
|
|
21
|
+
linkedAt: r.linked_at,
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
export async function unlink(connector, externalId) {
|
|
25
|
+
const em = getEm().fork();
|
|
26
|
+
const deleted = await em.nativeDelete('_IdentityLink', { connector, external_id: externalId });
|
|
27
|
+
return deleted > 0;
|
|
28
|
+
}
|
|
29
|
+
export async function mintLinkCode(userId, ttlMs) {
|
|
30
|
+
const em = getEm().fork();
|
|
31
|
+
const raw = generateToken();
|
|
32
|
+
const expiresAt = new Date(Date.now() + ttlMs).toISOString();
|
|
33
|
+
em.create('_LinkCode', {
|
|
34
|
+
code_hash: hashToken(raw),
|
|
35
|
+
user_id: userId,
|
|
36
|
+
expires_at: expiresAt,
|
|
37
|
+
created_at: new Date().toISOString(),
|
|
38
|
+
});
|
|
39
|
+
await em.flush();
|
|
40
|
+
return { raw, expiresAt };
|
|
41
|
+
}
|
|
42
|
+
async function setLink(em, connector, externalId, userId) {
|
|
43
|
+
const existing = (await em.findOne('_IdentityLink', {
|
|
44
|
+
connector,
|
|
45
|
+
external_id: externalId,
|
|
46
|
+
}));
|
|
47
|
+
const linkedAt = new Date().toISOString();
|
|
48
|
+
if (existing) {
|
|
49
|
+
em.assign(existing, { user_id: userId, linked_at: linkedAt });
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
em.create('_IdentityLink', { connector, external_id: externalId, user_id: userId, linked_at: linkedAt });
|
|
53
|
+
}
|
|
54
|
+
await em.flush();
|
|
55
|
+
}
|
|
56
|
+
export async function redeemLinkCode(raw, connector, externalId) {
|
|
57
|
+
const em = getEm().fork();
|
|
58
|
+
const hash = hashToken(raw);
|
|
59
|
+
const row = (await em.findOne('_LinkCode', { code_hash: hash }));
|
|
60
|
+
if (!row)
|
|
61
|
+
return null;
|
|
62
|
+
const deleted = await em.nativeDelete('_LinkCode', { code_hash: hash });
|
|
63
|
+
if (deleted === 0)
|
|
64
|
+
return null;
|
|
65
|
+
if (row.expires_at < new Date().toISOString())
|
|
66
|
+
return null;
|
|
67
|
+
const owner = await findUserById(row.user_id);
|
|
68
|
+
if (!owner || owner.disabled)
|
|
69
|
+
return null;
|
|
70
|
+
await setLink(em, connector, externalId, row.user_id);
|
|
71
|
+
return row.user_id;
|
|
72
|
+
}
|
|
73
|
+
export async function pruneLinkCodes() {
|
|
74
|
+
const em = getEm().fork();
|
|
75
|
+
await em.nativeDelete('_LinkCode', { expires_at: { $lt: new Date().toISOString() } });
|
|
76
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -15,10 +15,12 @@ import { NotFoundError } from "./mutate.js";
|
|
|
15
15
|
import { httpErrorFor, registerErrorHandler } from "./http-errors.js";
|
|
16
16
|
import { resolveEmbed } from "./embed.js";
|
|
17
17
|
import { resolveWithinHome, filterSort, listDir } from "./fs-list.js";
|
|
18
|
-
import { initPlugins, teardownPlugins, readDisabled, purgePluginData, getPluginSettings, getPlugins, } from "./plugin-runtime.js";
|
|
18
|
+
import { initPlugins, teardownPlugins, readDisabled, getDisabled, purgePluginData, getPluginSettings, getPlugins, } from "./plugin-runtime.js";
|
|
19
19
|
import { registerPluginsApi } from "./plugins-api.js";
|
|
20
20
|
import { orchestratorDiagnostics } from "./orchestrator/index.js";
|
|
21
|
+
import { pluginHooks } from "./plugin-hooks.js";
|
|
21
22
|
import { registerPluginUserApi, registerPluginAdminApi } from "./plugin-user-api.js";
|
|
23
|
+
import { collectHttpMounts, registerPluginHttpMounts, registerTextBodyParsers } from "./plugin-http-mounts.js";
|
|
22
24
|
import { registerAuthApi, resolveRequestUser, requireAdmin, PUBLIC_API_PATHS } from "./auth-api.js";
|
|
23
25
|
import { registerMcpHttp } from "./mcp-http.js";
|
|
24
26
|
import { registerOAuthApi } from "./oauth-api.js";
|
|
@@ -71,6 +73,7 @@ app.addHook('onResponse', async (req, reply) => {
|
|
|
71
73
|
});
|
|
72
74
|
await app.register(cors, { origin: true });
|
|
73
75
|
await app.register(multipart, { limits: { fileSize: 25 * 1024 * 1024 } });
|
|
76
|
+
registerTextBodyParsers(app);
|
|
74
77
|
await app.register(fastifyStatic, { root: UPLOADS, prefix: '/uploads/' });
|
|
75
78
|
app.addHook('onSend', async (req, reply, payload) => {
|
|
76
79
|
if (req.url.startsWith('/uploads/')) {
|
|
@@ -327,6 +330,10 @@ app.put('/api/settings/:id', (req, reply) => {
|
|
|
327
330
|
});
|
|
328
331
|
registerPluginAdminApi(app, requireAdmin);
|
|
329
332
|
registerPluginUserApi(app);
|
|
333
|
+
const httpMounts = collectHttpMounts(pluginHooks);
|
|
334
|
+
registerPluginHttpMounts(app, httpMounts, { disabledSet: getDisabled, resolveUser: resolveRequestUser });
|
|
335
|
+
if (httpMounts.length)
|
|
336
|
+
app.log.info(`plugin http mounts: ${httpMounts.map((m) => `${m.pluginId}${m.mount.prefix}`).join(', ')}`);
|
|
330
337
|
app.get('/api/fs/list', async (req, reply) => {
|
|
331
338
|
const home = homedir();
|
|
332
339
|
const q = req.query;
|
package/dist/mcp-http.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
|
-
import { collectMcpTools, resolveRagDeps,
|
|
3
|
+
import { collectMcpTools, resolveRagDeps, buildDomainAreas, buildMcpInstructions, siteSection } from "./mcp-tools.js";
|
|
4
|
+
import { assembleSystem, ALL_PRESENT } from "./orchestrator/system-areas.js";
|
|
4
5
|
import { configuredPublicUrl } from "./public-url.js";
|
|
5
6
|
import { getLogger } from "./log.js";
|
|
6
7
|
const log = getLogger('mcp-http');
|
|
7
8
|
export async function buildMcpServer(role, actor) {
|
|
8
9
|
const rag = await resolveRagDeps();
|
|
9
|
-
const tools =
|
|
10
|
+
const tools = await collectMcpTools({ rag, role, actor });
|
|
10
11
|
const site = await siteSection(await configuredPublicUrl());
|
|
11
|
-
const
|
|
12
|
+
const areas = await buildDomainAreas();
|
|
13
|
+
if (site)
|
|
14
|
+
areas['channel'] = [site];
|
|
15
|
+
const sections = assembleSystem(areas, ALL_PRESENT);
|
|
12
16
|
const server = new McpServer({ name: 'coffer', version: '1.0.0' }, { instructions: buildMcpInstructions(sections) });
|
|
13
17
|
const registerTool = server.registerTool.bind(server);
|
|
14
18
|
for (const t of tools) {
|
package/dist/mcp-tools.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { type AuthRole, type PluginHooks } from './plugin-hooks.ts';
|
|
|
6
6
|
import { type Condition } from '@coffer-org/sdk/condition';
|
|
7
7
|
import { type AgentMeta } from '@coffer-org/sdk/library';
|
|
8
8
|
import { type RagHit } from './rag-search.ts';
|
|
9
|
+
import type { SystemAreas } from './orchestrator/system-areas.ts';
|
|
9
10
|
export interface McpToolDef {
|
|
10
11
|
server: string;
|
|
11
12
|
bareName: string;
|
|
@@ -21,9 +22,9 @@ export interface RagDeps {
|
|
|
21
22
|
}
|
|
22
23
|
export declare function formatHits(hits: RagHit[]): string;
|
|
23
24
|
export declare function resolveRagDeps(): Promise<RagDeps | null>;
|
|
24
|
-
export declare function collectMcpTools(opts
|
|
25
|
+
export declare function collectMcpTools(opts: {
|
|
26
|
+
role: AuthRole;
|
|
25
27
|
rag?: RagDeps | null;
|
|
26
|
-
includeAdmin?: boolean;
|
|
27
28
|
actor?: string;
|
|
28
29
|
}): Promise<McpToolDef[]>;
|
|
29
30
|
export declare function collectPluginInstructions(hooks?: Record<string, PluginHooks>, emFactory?: () => EntityManager): Promise<{
|
|
@@ -73,7 +74,7 @@ export declare function collectLibraryPurposes(reg?: {
|
|
|
73
74
|
}[];
|
|
74
75
|
}, locales?: LocaleResolver): LibraryPurpose[];
|
|
75
76
|
export declare function siteSection(siteUrl: string): Promise<string | null>;
|
|
76
|
-
export declare function
|
|
77
|
+
export declare function buildDomainAreas(locales?: LocaleResolver): Promise<SystemAreas>;
|
|
77
78
|
export interface StarterHint {
|
|
78
79
|
id: string;
|
|
79
80
|
text: string;
|
package/dist/mcp-tools.js
CHANGED
|
@@ -47,7 +47,7 @@ export async function resolveRagDeps() {
|
|
|
47
47
|
return null;
|
|
48
48
|
return { embeddingApiKey };
|
|
49
49
|
}
|
|
50
|
-
export async function collectMcpTools(opts
|
|
50
|
+
export async function collectMcpTools(opts) {
|
|
51
51
|
const out = [];
|
|
52
52
|
const client = new LocalClient();
|
|
53
53
|
const locales = await loadComposedLocales();
|
|
@@ -202,54 +202,52 @@ export async function collectMcpTools(opts = {}) {
|
|
|
202
202
|
},
|
|
203
203
|
});
|
|
204
204
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
return fail(`Validation error:\n${lines.join('\n')}`);
|
|
244
|
-
}
|
|
245
|
-
if (e instanceof NotFoundError)
|
|
246
|
-
return fail(`No settings group '${String(args.group)}'.`);
|
|
247
|
-
return fail(`Error: ${e.message}`);
|
|
205
|
+
const actor = opts.actor ?? 'mcp';
|
|
206
|
+
out.push({
|
|
207
|
+
server: 'coffer',
|
|
208
|
+
bareName: 'list_settings',
|
|
209
|
+
httpName: 'list_settings',
|
|
210
|
+
description: "List every settings group (the instance's own `system` group and each plugin's): each field's key/kind/required and the current values (secrets masked). Call before update_settings.",
|
|
211
|
+
inputSchema: {},
|
|
212
|
+
scope: 'settings',
|
|
213
|
+
role: 'admin',
|
|
214
|
+
handler: async () => {
|
|
215
|
+
try {
|
|
216
|
+
return ok(await listSettings());
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
return fail(`Error: ${e.message}`);
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
out.push({
|
|
224
|
+
server: 'coffer',
|
|
225
|
+
bareName: 'update_settings',
|
|
226
|
+
httpName: 'update_settings',
|
|
227
|
+
description: 'Update a plugin or system settings group. group — the settings group id (e.g. "system", "claude-agent"); fields — only the settings to change (see list_settings). Send real secret values; the masked placeholder (********) is treated as unchanged.',
|
|
228
|
+
inputSchema: { group: z.string(), fields: z.record(z.string(), z.unknown()) },
|
|
229
|
+
scope: 'settings',
|
|
230
|
+
role: 'admin',
|
|
231
|
+
handler: async (args) => {
|
|
232
|
+
try {
|
|
233
|
+
const row = await writePluginSettings(getEm().fork(), args.group, args.fields, actor);
|
|
234
|
+
return ok(row);
|
|
235
|
+
}
|
|
236
|
+
catch (e) {
|
|
237
|
+
if (e instanceof ValidationError) {
|
|
238
|
+
const lines = e.issues.map((i) => {
|
|
239
|
+
const o = i;
|
|
240
|
+
return `- ${o.field ?? '?'}: ${o.code ?? 'invalid'}`;
|
|
241
|
+
});
|
|
242
|
+
return fail(`Validation error:\n${lines.join('\n')}`);
|
|
248
243
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
244
|
+
if (e instanceof NotFoundError)
|
|
245
|
+
return fail(`No settings group '${String(args.group)}'.`);
|
|
246
|
+
return fail(`Error: ${e.message}`);
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
return opts.role === 'admin' ? out : out.filter((t) => t.role === 'member');
|
|
253
251
|
}
|
|
254
252
|
export async function collectPluginInstructions(hooks = pluginHooks, emFactory = () => getEm().fork()) {
|
|
255
253
|
const out = [];
|
|
@@ -316,44 +314,46 @@ export async function siteSection(siteUrl) {
|
|
|
316
314
|
const site = await frontendInstructions(siteUrl);
|
|
317
315
|
return site ? `## web\n${site}` : null;
|
|
318
316
|
}
|
|
319
|
-
|
|
317
|
+
const DATA_MODEL = '## Data model\n' +
|
|
318
|
+
'Library (top-level area) → shelf (a kind of record, e.g. things/item) → record (addressed library/shelf/id) → fields. ' +
|
|
319
|
+
'Some field values are JSON (e.g. quantity {"value":2000,"unit":"ml"}); some are relations (hold another record\'s id); ' +
|
|
320
|
+
"some are collections (nested rows — an array). Extends add extra field-sets to a shelf's records, shown only when a " +
|
|
321
|
+
'condition holds (the "when …" notes below); in a fetched record they sit under `_extends`. ' +
|
|
322
|
+
'Read: list_libraries → describe_shelf → list_records/get_record. Write: create_record/update_record (call describe_shelf first); ' +
|
|
323
|
+
'delete_record moves a record to reversible trash — do not blank fields. Before editing, read the complete record and patch only requested fields. ' +
|
|
324
|
+
'For duplicates, read both records completely, compare fields/collections/extends/attachments, recommend the less complete record for trash, and offer a field-by-field merge first. ' +
|
|
325
|
+
'Use list_trash and restore_record for recovery; purge_record is irreversible and requires explicit confirmation. ' +
|
|
326
|
+
'Each library below names what it holds and when to use it — pick the right one before searching. ' +
|
|
327
|
+
'A single-record shelf holds exactly one document — read it directly, do not search the shelf.';
|
|
328
|
+
function renderLibrary(v) {
|
|
329
|
+
let s = v.name === v.id ? `### ${v.id} — ${v.agent}` : `### ${v.id} ("${v.name}") — ${v.agent}`;
|
|
330
|
+
if (v.extends.length) {
|
|
331
|
+
s +=
|
|
332
|
+
'\nExtra field-sets some records carry (which one depends on the record):\n' +
|
|
333
|
+
v.extends
|
|
334
|
+
.map((e) => {
|
|
335
|
+
const when = describeCondition(e.showWhen, (f) => f).join(' and ');
|
|
336
|
+
return `- ${e.id} (on ${e.shelf}${when ? `, when ${when}` : ''}): ${e.agent.replace(/\s*\n\s*/g, ' ')}`;
|
|
337
|
+
})
|
|
338
|
+
.join('\n');
|
|
339
|
+
}
|
|
340
|
+
return s;
|
|
341
|
+
}
|
|
342
|
+
export async function buildDomainAreas(locales) {
|
|
320
343
|
const i18n = locales ?? (await loadComposedLocales());
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
return `- ${e.id} (on ${e.shelf}${when ? `, when ${when}` : ''}): ${e.agent.replace(/\s*\n\s*/g, ' ')}`;
|
|
333
|
-
})
|
|
334
|
-
.join('\n');
|
|
335
|
-
}
|
|
336
|
-
return s;
|
|
337
|
-
});
|
|
338
|
-
overview =
|
|
339
|
-
'## Libraries (what each holds / when to use it — pick the right one before searching)\n\n' + blocks.join('\n\n');
|
|
344
|
+
const areas = { root: [DATA_MODEL] };
|
|
345
|
+
for (const v of collectLibraryPurposes(undefined, i18n)) {
|
|
346
|
+
areas[`library:${v.id}`] = [renderLibrary(v)];
|
|
347
|
+
}
|
|
348
|
+
for (const s of collectSingleShelves()) {
|
|
349
|
+
areas[`shelf:${s.library}/${s.shelf}`] = [
|
|
350
|
+
`### ${s.library}/${s.shelf} — single record: read it directly, do not search the shelf. ${s.agent.replace(/\s*\n\s*/g, ' ')}`,
|
|
351
|
+
];
|
|
352
|
+
}
|
|
353
|
+
for (const { id, instructions } of await collectPluginInstructions()) {
|
|
354
|
+
areas[`plugin:${id}`] = [`## ${id}\n${instructions}`];
|
|
340
355
|
}
|
|
341
|
-
|
|
342
|
-
const singleSection = singles.length
|
|
343
|
-
? '## Single-record shelves (one document each — read the record, do not search the shelf)\n\n' +
|
|
344
|
-
singles.map((s) => `- ${s.library}/${s.shelf}: ${s.agent.replace(/\s*\n\s*/g, ' ')}`).join('\n')
|
|
345
|
-
: null;
|
|
346
|
-
const dataModel = '## Data model\n' +
|
|
347
|
-
'Library (top-level area) → shelf (a kind of record, e.g. things/item) → record (addressed library/shelf/id) → fields. ' +
|
|
348
|
-
'Some field values are JSON (e.g. quantity {"value":2000,"unit":"ml"}); some are relations (hold another record\'s id); ' +
|
|
349
|
-
"some are collections (nested rows — an array). Extends add extra field-sets to a shelf's records, shown only when a " +
|
|
350
|
-
'condition holds (the "when …" notes below); in a fetched record they sit under `_extends`. ' +
|
|
351
|
-
'Read: list_libraries → describe_shelf → list_records/get_record. Write: create_record/update_record (call describe_shelf first); ' +
|
|
352
|
-
'delete_record moves a record to reversible trash — do not blank fields. Before editing, read the complete record and patch only requested fields. ' +
|
|
353
|
-
'For duplicates, read both records completely, compare fields/collections/extends/attachments, recommend the less complete record for trash, and offer a field-by-field merge first. ' +
|
|
354
|
-
'Use list_trash and restore_record for recovery; purge_record is irreversible and requires explicit confirmation.';
|
|
355
|
-
const rules = (await collectPluginInstructions()).map(({ id, instructions }) => `## ${id}\n${instructions}`);
|
|
356
|
-
return [dataModel, ...(overview ? [overview] : []), ...(singleSection ? [singleSection] : []), ...rules];
|
|
356
|
+
return areas;
|
|
357
357
|
}
|
|
358
358
|
export const CORE_STARTER_HINT = 'what this Coffer instance itself holds — which libraries are present and what kinds of ' +
|
|
359
359
|
'questions the stored data can answer';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface ImageTarget {
|
|
2
|
+
maxEdge: number;
|
|
3
|
+
maxPixels?: number;
|
|
4
|
+
maxBytes: number;
|
|
5
|
+
encode: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
export interface ImageFacts {
|
|
8
|
+
mime: string;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
bytes: number;
|
|
12
|
+
}
|
|
13
|
+
export interface NormalizedImage {
|
|
14
|
+
bytes: Buffer;
|
|
15
|
+
mime: string;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
source: ImageFacts;
|
|
19
|
+
changed: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare class ImageNormalizeError extends Error {
|
|
22
|
+
}
|
|
23
|
+
export declare function normalizeImage(bytes: Buffer, target: ImageTarget): Promise<NormalizedImage>;
|