@coffer-org/server 7.2.0 → 7.4.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 +52 -0
- package/dist/auth-store.d.ts +2 -0
- package/dist/auth-store.js +1 -0
- package/dist/compute-unit.js +0 -3
- package/dist/conversation-store.d.ts +61 -0
- package/dist/conversation-store.js +223 -0
- package/dist/entity-schema.d.ts +5 -2
- package/dist/entity-schema.js +35 -18
- package/dist/identity-link.d.ts +15 -0
- package/dist/identity-link.js +76 -0
- package/dist/identity-providers.d.ts +17 -0
- package/dist/identity-providers.js +14 -0
- package/dist/index.js +8 -1
- package/dist/mcp-contract/schema.d.ts +0 -1
- package/dist/mcp-contract/schema.js +0 -2
- 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/mutate.js +0 -15
- 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/draft-message.d.ts +18 -0
- package/dist/orchestrator/draft-message.js +85 -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 +14 -9
- package/dist/orchestrator/index.js +6 -7
- package/dist/orchestrator/live-message.d.ts +7 -4
- package/dist/orchestrator/live-message.js +52 -32
- package/dist/orchestrator/pipeline.d.ts +22 -4
- package/dist/orchestrator/pipeline.js +316 -115
- package/dist/orchestrator/registry.d.ts +2 -2
- package/dist/orchestrator/registry.js +1 -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 +100 -49
- package/dist/plugin-hooks.d.ts +38 -1
- package/dist/plugin-hooks.js +4 -0
- package/dist/plugin-http-mounts.d.ts +18 -0
- package/dist/plugin-http-mounts.js +94 -0
- package/dist/plugin-runtime.d.ts +4 -0
- package/dist/plugin-runtime.js +9 -5
- package/dist/plugins-api.d.ts +8 -2
- package/dist/plugins-api.js +4 -2
- package/dist/records-api.js +15 -3
- package/dist/settings-write.d.ts +1 -2
- package/dist/settings-write.js +1 -2
- package/dist/system-settings.js +0 -1
- package/dist/temporal.js +8 -3
- 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/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;
|
|
@@ -52,7 +52,6 @@ export function flattenFields(items) {
|
|
|
52
52
|
key,
|
|
53
53
|
kind: t.kind,
|
|
54
54
|
prim: t.prim,
|
|
55
|
-
required: t.required,
|
|
56
55
|
...(t.agent ? { agent: t.agent } : {}),
|
|
57
56
|
...(t.relation ? { relation: t.relation } : {}),
|
|
58
57
|
...(t.options ? { options: t.options } : {}),
|
|
@@ -69,7 +68,6 @@ export function flattenFields(items) {
|
|
|
69
68
|
key: it.key,
|
|
70
69
|
kind: 'group',
|
|
71
70
|
prim: 'group',
|
|
72
|
-
required: it.required === true,
|
|
73
71
|
...(it.agent ? { agent: it.agent } : {}),
|
|
74
72
|
...(it.multiple ? { multiple: true } : {}),
|
|
75
73
|
fields: flattenFields(it.fields),
|
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>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
export class ImageNormalizeError extends Error {
|
|
3
|
+
}
|
|
4
|
+
const MIME_BY_FORMAT = {
|
|
5
|
+
jpeg: 'image/jpeg',
|
|
6
|
+
jpg: 'image/jpeg',
|
|
7
|
+
png: 'image/png',
|
|
8
|
+
webp: 'image/webp',
|
|
9
|
+
gif: 'image/gif',
|
|
10
|
+
avif: 'image/avif',
|
|
11
|
+
tiff: 'image/tiff',
|
|
12
|
+
heif: 'image/heif',
|
|
13
|
+
};
|
|
14
|
+
const QUALITY_LADDER = [85, 72, 58, 44];
|
|
15
|
+
const MAX_ATTEMPTS = 6;
|
|
16
|
+
const QUALITY_CAN_CLOSE = 2;
|
|
17
|
+
function fit(width, height, maxEdge, maxPixels) {
|
|
18
|
+
let scale = Math.min(1, maxEdge / Math.max(width, height));
|
|
19
|
+
if (maxPixels)
|
|
20
|
+
scale = Math.min(scale, Math.sqrt(maxPixels / (width * height)));
|
|
21
|
+
return scaleTo(width, height, scale);
|
|
22
|
+
}
|
|
23
|
+
function scaleTo(width, height, scale) {
|
|
24
|
+
return {
|
|
25
|
+
width: Math.max(1, Math.floor(width * scale)),
|
|
26
|
+
height: Math.max(1, Math.floor(height * scale)),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function pickEncoding(target, hasAlpha) {
|
|
30
|
+
const accepts = (mime) => target.encode.includes(mime);
|
|
31
|
+
const order = hasAlpha ? ['image/webp', 'image/png', 'image/jpeg'] : ['image/jpeg', 'image/webp', 'image/png'];
|
|
32
|
+
for (const mime of order)
|
|
33
|
+
if (accepts(mime))
|
|
34
|
+
return mime;
|
|
35
|
+
throw new ImageNormalizeError(`the agent takes back none of the encodings this image could be produced in: ${target.encode.join(', ') || '(none declared)'}`);
|
|
36
|
+
}
|
|
37
|
+
function encode(pipeline, mime, quality) {
|
|
38
|
+
switch (mime) {
|
|
39
|
+
case 'image/jpeg':
|
|
40
|
+
return pipeline.jpeg({ quality, mozjpeg: true }).toBuffer();
|
|
41
|
+
case 'image/webp':
|
|
42
|
+
return pipeline.webp({ quality }).toBuffer();
|
|
43
|
+
case 'image/png':
|
|
44
|
+
return pipeline.png({ compressionLevel: 9, palette: true, quality }).toBuffer();
|
|
45
|
+
default:
|
|
46
|
+
throw new ImageNormalizeError(`Unsupported target encoding: ${mime}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function normalizeImage(bytes, target) {
|
|
50
|
+
let meta;
|
|
51
|
+
try {
|
|
52
|
+
meta = await sharp(bytes).metadata();
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
throw new ImageNormalizeError(`Not a decodable image: ${err instanceof Error ? err.message : String(err)}`);
|
|
56
|
+
}
|
|
57
|
+
const format = meta.format ? MIME_BY_FORMAT[meta.format] : undefined;
|
|
58
|
+
const swap = (meta.orientation ?? 0) >= 5;
|
|
59
|
+
const width = swap ? meta.height : meta.width;
|
|
60
|
+
const height = swap ? meta.width : meta.height;
|
|
61
|
+
if (!format || !width || !height)
|
|
62
|
+
throw new ImageNormalizeError('Image has no readable format or dimensions.');
|
|
63
|
+
const source = { mime: format, width, height, bytes: bytes.length };
|
|
64
|
+
const target1 = fit(width, height, target.maxEdge, target.maxPixels);
|
|
65
|
+
const fitsNow = target.encode.includes(format) &&
|
|
66
|
+
bytes.length <= target.maxBytes &&
|
|
67
|
+
target1.width === width &&
|
|
68
|
+
target1.height === height;
|
|
69
|
+
if (fitsNow)
|
|
70
|
+
return { bytes, mime: format, width, height, source, changed: false };
|
|
71
|
+
const mime = pickEncoding(target, meta.hasAlpha === true);
|
|
72
|
+
const flatten = meta.hasAlpha === true && mime === 'image/jpeg';
|
|
73
|
+
let dims = target1;
|
|
74
|
+
let qualityStep = 0;
|
|
75
|
+
let last;
|
|
76
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
77
|
+
let pipeline = sharp(bytes).rotate();
|
|
78
|
+
if (flatten)
|
|
79
|
+
pipeline = pipeline.flatten({ background: '#ffffff' });
|
|
80
|
+
pipeline = pipeline.resize({ ...dims, fit: 'inside', withoutEnlargement: true });
|
|
81
|
+
const quality = QUALITY_LADDER[Math.min(qualityStep, QUALITY_LADDER.length - 1)];
|
|
82
|
+
last = await encode(pipeline, mime, quality);
|
|
83
|
+
if (last.length <= target.maxBytes) {
|
|
84
|
+
const out = await sharp(last).metadata();
|
|
85
|
+
return {
|
|
86
|
+
bytes: last,
|
|
87
|
+
mime,
|
|
88
|
+
width: out.width ?? dims.width,
|
|
89
|
+
height: out.height ?? dims.height,
|
|
90
|
+
source,
|
|
91
|
+
changed: true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (last.length <= target.maxBytes * QUALITY_CAN_CLOSE && qualityStep < QUALITY_LADDER.length - 1) {
|
|
95
|
+
qualityStep += 1;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
const shrink = Math.sqrt(target.maxBytes / last.length) * 0.9;
|
|
99
|
+
dims = scaleTo(dims.width, dims.height, Math.min(0.9, shrink));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
throw new ImageNormalizeError(`Could not bring the image within ${target.maxBytes} bytes: ${last?.length ?? source.bytes} bytes after ${MAX_ATTEMPTS} attempts.`);
|
|
103
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { normalizeImage, ImageNormalizeError, type ImageTarget, type ImageFacts, type NormalizedImage, } from './image.ts';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { normalizeImage, ImageNormalizeError, } from "./image.js";
|
package/dist/migrations.js
CHANGED
|
@@ -394,7 +394,7 @@ export function makeTable(em, table, record) {
|
|
|
394
394
|
export async function renameSystemShelfKey(em) {
|
|
395
395
|
await makeTable(em, '_embeddings').renameColumn('type', 'shelf_key');
|
|
396
396
|
}
|
|
397
|
-
const LEGACY_ORCHESTRATOR_FIELDS = ['agent_id', '
|
|
397
|
+
const LEGACY_ORCHESTRATOR_FIELDS = ['agent_id', 'trigger_prefix', 'reply_window'];
|
|
398
398
|
export const SYSTEM_MIGRATIONS = [
|
|
399
399
|
{
|
|
400
400
|
name: 'embeddings-shelf-key',
|
package/dist/mutate.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { serialize } from '@mikro-orm/core';
|
|
2
2
|
import { buildZodObject, buildZodObjectPartial, fieldEntries, collectionGroups } from '@coffer-org/sdk/shelf';
|
|
3
3
|
import { isJsonStored, decodeVmsg } from '@coffer-org/sdk/fields';
|
|
4
|
-
import { resolveFlag } from '@coffer-org/sdk/condition';
|
|
5
4
|
import { getEm } from "./db.js";
|
|
6
5
|
import { selectRows } from "./read-rows.js";
|
|
7
6
|
import { normalizeFileFields, dropUnchangedFileFields, touchesFileFields } from "./file-fields.js";
|
|
@@ -190,20 +189,6 @@ export async function updateRecord(m, entityName, id, input, ctx, afterBase) {
|
|
|
190
189
|
throw new ValidationError(fileIssues);
|
|
191
190
|
const { base, collections } = splitCollections(m, { ...patchData });
|
|
192
191
|
const merged = { ...existing, ...base };
|
|
193
|
-
const reqIssues = [];
|
|
194
|
-
for (const [key, f] of fieldEntries(m.fields)) {
|
|
195
|
-
if (f.required === true)
|
|
196
|
-
continue;
|
|
197
|
-
if (key.includes('__'))
|
|
198
|
-
continue;
|
|
199
|
-
if (!resolveFlag(f.required, merged))
|
|
200
|
-
continue;
|
|
201
|
-
const v = merged[key];
|
|
202
|
-
if (v == null || v === '' || (Array.isArray(v) && v.length === 0))
|
|
203
|
-
reqIssues.push({ field: key, code: 'required', path: [key] });
|
|
204
|
-
}
|
|
205
|
-
if (reqIssues.length)
|
|
206
|
-
throw new ValidationError(reqIssues);
|
|
207
192
|
const storedWithMeta = hasStoredPartWork(m.fields)
|
|
208
193
|
? await readCollectionsWithMeta(tx, m, id)
|
|
209
194
|
: { collections: {}, meta: {} };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { AgentToolProvider, AttachmentRef } from './types.ts';
|
|
2
|
-
export declare function makeAttachmentCapabilities(attachments: AttachmentRef[]): Promise<AgentToolProvider>;
|
|
1
|
+
import type { AgentMediaLimits, AgentToolProvider, AttachmentRef } from './types.ts';
|
|
2
|
+
export declare function makeAttachmentCapabilities(attachments: AttachmentRef[], limits: AgentMediaLimits): Promise<AgentToolProvider>;
|
|
@@ -57,8 +57,8 @@ function fileItems(value) {
|
|
|
57
57
|
function findTool(defs, name) {
|
|
58
58
|
return defs.find((def) => def.server === 'coffer' && def.bareName === name);
|
|
59
59
|
}
|
|
60
|
-
export async function makeAttachmentCapabilities(attachments) {
|
|
61
|
-
const defs = await collectMcpTools();
|
|
60
|
+
export async function makeAttachmentCapabilities(attachments, limits) {
|
|
61
|
+
const defs = await collectMcpTools({ role: 'member' });
|
|
62
62
|
const getRecord = findTool(defs, 'get_record');
|
|
63
63
|
const updateRecord = findTool(defs, 'update_record');
|
|
64
64
|
const tools = [];
|
|
@@ -115,7 +115,7 @@ export async function makeAttachmentCapabilities(attachments) {
|
|
|
115
115
|
return inspectUpload(item['name'], {
|
|
116
116
|
...(typeof item['mime'] === 'string' ? { mime: item['mime'] } : {}),
|
|
117
117
|
...(typeof item['size'] === 'number' ? { size: item['size'] } : {}),
|
|
118
|
-
});
|
|
118
|
+
}, limits);
|
|
119
119
|
},
|
|
120
120
|
});
|
|
121
121
|
}
|
|
@@ -1,16 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const stateDir: () => string;
|
|
3
|
-
export declare function carryLegacyState(): void;
|
|
4
|
-
export declare function allowFileFor(connectorId: string): string;
|
|
5
|
-
export interface AllowState {
|
|
6
|
-
ids: Record<string, number>;
|
|
7
|
-
}
|
|
8
|
-
export declare function emptyAllowed(): AllowState;
|
|
9
|
-
export declare function loadAllowed(file: string): AllowState;
|
|
10
|
-
export declare function saveAllowed(file: string, state: AllowState): void;
|
|
11
|
-
export declare function isAllowed(state: AllowState, id: string | number | null | undefined): boolean;
|
|
12
|
-
export declare function addAllowed(state: AllowState, id: string | number, now: number): AllowState;
|
|
13
|
-
export declare function isSenderAllowed(connectorId: string, senderId: string, deps?: {
|
|
14
|
-
policy?: GatePolicy;
|
|
15
|
-
}): Promise<boolean>;
|
|
16
|
-
export declare function makeThrottle(maxAttempts?: number, windowMs?: number): (id: string | number, now?: number) => boolean;
|
|
1
|
+
export declare function makeThrottle(maxNotices?: number, windowMs?: number): (id: string | number, now?: number) => boolean;
|
|
@@ -1,63 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { loadGatePolicy } from "./config.js";
|
|
4
|
-
import { carryInto, dataDir } from "../data-dir.js";
|
|
5
|
-
export const stateDir = () => process.env['ORCHESTRATOR_STATE_DIR'] ?? path.join(dataDir(), 'state');
|
|
6
|
-
function legacyStateDirs() {
|
|
7
|
-
return [
|
|
8
|
-
path.join(process.cwd(), 'packages', 'plugin-orchestrator', 'runtime', 'state'),
|
|
9
|
-
path.join(process.cwd(), 'node_modules', '@coffer-org', 'plugin-orchestrator', 'runtime', 'state'),
|
|
10
|
-
];
|
|
11
|
-
}
|
|
12
|
-
export function carryLegacyState() {
|
|
13
|
-
carryInto(stateDir(), ...legacyStateDirs());
|
|
14
|
-
}
|
|
15
|
-
export function allowFileFor(connectorId) {
|
|
16
|
-
return path.join(stateDir(), `${connectorId}.allowed.json`);
|
|
17
|
-
}
|
|
18
|
-
export function emptyAllowed() {
|
|
19
|
-
return { ids: {} };
|
|
20
|
-
}
|
|
21
|
-
export function loadAllowed(file) {
|
|
22
|
-
try {
|
|
23
|
-
const obj = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
24
|
-
if (obj && typeof obj === 'object' && 'ids' in obj && typeof obj.ids === 'object') {
|
|
25
|
-
return obj;
|
|
26
|
-
}
|
|
27
|
-
return emptyAllowed();
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return emptyAllowed();
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
export function saveAllowed(file, state) {
|
|
34
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
35
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
36
|
-
fs.writeFileSync(tmp, JSON.stringify(state), 'utf-8');
|
|
37
|
-
fs.renameSync(tmp, file);
|
|
38
|
-
}
|
|
39
|
-
export function isAllowed(state, id) {
|
|
40
|
-
return id != null && Object.prototype.hasOwnProperty.call(state.ids, String(id));
|
|
41
|
-
}
|
|
42
|
-
export function addAllowed(state, id, now) {
|
|
43
|
-
return { ids: { ...state.ids, [String(id)]: now } };
|
|
44
|
-
}
|
|
45
|
-
export async function isSenderAllowed(connectorId, senderId, deps) {
|
|
46
|
-
const policy = deps?.policy ?? (await loadGatePolicy());
|
|
47
|
-
if (!policy.accessPassword)
|
|
48
|
-
return true;
|
|
49
|
-
return isAllowed(loadAllowed(allowFileFor(connectorId)), senderId);
|
|
50
|
-
}
|
|
51
|
-
export function makeThrottle(maxAttempts = 5, windowMs = 60_000) {
|
|
1
|
+
export function makeThrottle(maxNotices = 5, windowMs = 60_000) {
|
|
52
2
|
const hits = new Map();
|
|
53
|
-
return function
|
|
3
|
+
return function allowNotice(id, now = Date.now()) {
|
|
54
4
|
const key = String(id);
|
|
55
5
|
const rec = hits.get(key);
|
|
56
6
|
if (!rec || now - rec.start >= windowMs) {
|
|
57
7
|
hits.set(key, { start: now, count: 1 });
|
|
58
8
|
return true;
|
|
59
9
|
}
|
|
60
|
-
if (rec.count >=
|
|
10
|
+
if (rec.count >= maxNotices)
|
|
61
11
|
return false;
|
|
62
12
|
rec.count += 1;
|
|
63
13
|
return true;
|
|
@@ -3,7 +3,6 @@ export function buildPolicy(dbSettings = {}) {
|
|
|
3
3
|
const db = dbSettings;
|
|
4
4
|
return {
|
|
5
5
|
...(typeof db.agent_id === 'string' && db.agent_id ? { agentId: db.agent_id } : {}),
|
|
6
|
-
accessPassword: db.access_password ?? '',
|
|
7
6
|
triggerPrefix: db.trigger_prefix ?? '',
|
|
8
7
|
replyWindow: Number(db.reply_window ?? 1800) || 1800,
|
|
9
8
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AuthRole } from '../plugin-hooks.ts';
|
|
2
|
+
export interface ContextFact {
|
|
3
|
+
name: string;
|
|
4
|
+
value: string;
|
|
5
|
+
attrs?: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
export declare const CONNECTOR_FACT_NAME: RegExp;
|
|
8
|
+
export declare const CONTEXT_GAP_MS: number;
|
|
9
|
+
export declare const MAX_SPEAKER_NAME = 80;
|
|
10
|
+
export declare const MAX_FACT_VALUE = 200;
|
|
11
|
+
export declare const MAX_CONNECTOR_FACTS = 12;
|
|
12
|
+
export declare function oneLine(value: string, maxLen?: number): string;
|
|
13
|
+
export interface ContextSpeaker {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
role: AuthRole;
|
|
17
|
+
}
|
|
18
|
+
export interface ContextInput {
|
|
19
|
+
connectorFacts: readonly ContextFact[];
|
|
20
|
+
speaker: ContextSpeaker;
|
|
21
|
+
now: Date;
|
|
22
|
+
timeZone: string;
|
|
23
|
+
previous: readonly ContextFact[] | null;
|
|
24
|
+
previousAt: Date | null;
|
|
25
|
+
}
|
|
26
|
+
export declare function humanizeGap(ms: number): string;
|
|
27
|
+
export declare function buildContextFacts(input: ContextInput): ContextFact[];
|