@coffer-org/server 5.0.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2 -0
- package/dist/instance-api.d.ts +5 -0
- package/dist/instance-api.js +4 -0
- package/dist/locale-registry.d.ts +9 -0
- package/dist/locale-registry.js +51 -0
- package/dist/mcp-contract/schema.d.ts +6 -1
- package/dist/mcp-contract/schema.js +21 -3
- package/dist/mcp-contract/tools.d.ts +2 -1
- package/dist/mcp-contract/tools.js +30 -4
- package/dist/mcp-tools.d.ts +18 -11
- package/dist/mcp-tools.js +35 -22
- package/dist/orchestrator/starters.d.ts +3 -3
- package/dist/orchestrator/starters.js +105 -38
- package/dist/orchestrator/types.d.ts +2 -2
- package/dist/plugin-discovery.d.ts +1 -0
- package/dist/plugin-discovery.js +4 -4
- package/dist/plugin-i18n.d.ts +1 -0
- package/dist/plugin-i18n.js +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import { registerPluginUserApi, registerPluginAdminApi } from "./plugin-user-api
|
|
|
22
22
|
import { registerAuthApi, resolveRequestUser, requireAdmin, PUBLIC_API_PATHS } from "./auth-api.js";
|
|
23
23
|
import { registerMcpHttp } from "./mcp-http.js";
|
|
24
24
|
import { registerOAuthApi } from "./oauth-api.js";
|
|
25
|
+
import { registerInstanceApi } from "./instance-api.js";
|
|
25
26
|
import { baseUrl } from "./public-url.js";
|
|
26
27
|
import { discoverPluginAssets, discoverRuntime } from "./plugin-discovery.js";
|
|
27
28
|
import { checkLatestVersion, resolveUpdateTarget, resolveAllUpdateTargets, resolveBaseTargets, resolveRuntimeTarget, runNpmInstall, } from "./plugin-updates.js";
|
|
@@ -146,6 +147,7 @@ app.get('/health', async (_req, reply) => {
|
|
|
146
147
|
return reply.code(503).send({ status: 'db_unavailable' });
|
|
147
148
|
}
|
|
148
149
|
});
|
|
150
|
+
registerInstanceApi(app);
|
|
149
151
|
let librariesPayload = null;
|
|
150
152
|
app.get('/api/libraries', async (req, reply) => {
|
|
151
153
|
if (!librariesPayload) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface LocaleResolver {
|
|
2
|
+
lang: string;
|
|
3
|
+
resolve(key: string): string | undefined;
|
|
4
|
+
}
|
|
5
|
+
export declare function loadComposedLocales(opts?: {
|
|
6
|
+
dirs?: string[];
|
|
7
|
+
lang?: string;
|
|
8
|
+
}): Promise<LocaleResolver>;
|
|
9
|
+
export declare function clearComposedLocaleCache(): void;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { pluginPackageNames } from "./plugin-discovery.js";
|
|
4
|
+
import { systemLanguage } from "./plugin-i18n.js";
|
|
5
|
+
function deepMerge(into, from) {
|
|
6
|
+
for (const [k, v] of Object.entries(from)) {
|
|
7
|
+
const prev = into[k];
|
|
8
|
+
if (v && typeof v === 'object' && !Array.isArray(v) && prev && typeof prev === 'object' && !Array.isArray(prev)) {
|
|
9
|
+
deepMerge(prev, v);
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
into[k] = v;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function pluginLocaleDirs() {
|
|
17
|
+
const nm = join(process.cwd(), 'node_modules');
|
|
18
|
+
return (await pluginPackageNames(nm)).map((name) => join(nm, name, 'locales'));
|
|
19
|
+
}
|
|
20
|
+
function makeResolver(lang, dict) {
|
|
21
|
+
return {
|
|
22
|
+
lang,
|
|
23
|
+
resolve(key) {
|
|
24
|
+
const value = key
|
|
25
|
+
.split('.')
|
|
26
|
+
.reduce((node, part) => (node == null ? undefined : node[part]), dict);
|
|
27
|
+
return typeof value === 'string' ? value : undefined;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
let cached = null;
|
|
32
|
+
export async function loadComposedLocales(opts = {}) {
|
|
33
|
+
const lang = opts.lang ?? (await systemLanguage('uk'));
|
|
34
|
+
if (!opts.dirs && cached?.lang === lang)
|
|
35
|
+
return makeResolver(lang, cached.dict);
|
|
36
|
+
const dirs = opts.dirs ?? (await pluginLocaleDirs());
|
|
37
|
+
const dict = {};
|
|
38
|
+
for (const dir of dirs) {
|
|
39
|
+
try {
|
|
40
|
+
deepMerge(dict, JSON.parse(await readFile(join(dir, `${lang}.json`), 'utf8')));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!opts.dirs)
|
|
46
|
+
cached = { lang, dict };
|
|
47
|
+
return makeResolver(lang, dict);
|
|
48
|
+
}
|
|
49
|
+
export function clearComposedLocaleCache() {
|
|
50
|
+
cached = null;
|
|
51
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LocaleResolver } from '../locale-registry.ts';
|
|
1
2
|
export interface ShelfIndexEntry {
|
|
2
3
|
library: string;
|
|
3
4
|
libraryLabel: string;
|
|
@@ -9,6 +10,7 @@ export interface FieldInfo {
|
|
|
9
10
|
kind: string;
|
|
10
11
|
prim: string;
|
|
11
12
|
required: boolean;
|
|
13
|
+
agent?: string;
|
|
12
14
|
relation?: {
|
|
13
15
|
library: string;
|
|
14
16
|
shelf: string;
|
|
@@ -27,6 +29,7 @@ export interface ShelfDescription {
|
|
|
27
29
|
library: string;
|
|
28
30
|
shelf: string;
|
|
29
31
|
label: string;
|
|
32
|
+
agent?: string;
|
|
30
33
|
fields: FieldInfo[];
|
|
31
34
|
}
|
|
32
35
|
interface SchemaClient {
|
|
@@ -35,8 +38,10 @@ interface SchemaClient {
|
|
|
35
38
|
export declare function flattenFields(items: unknown[]): FieldInfo[];
|
|
36
39
|
export declare class SchemaCache {
|
|
37
40
|
private client;
|
|
41
|
+
private locales?;
|
|
38
42
|
private cached?;
|
|
39
|
-
constructor(client: SchemaClient);
|
|
43
|
+
constructor(client: SchemaClient, locales?: LocaleResolver | undefined);
|
|
44
|
+
private display;
|
|
40
45
|
refresh(): void;
|
|
41
46
|
private load;
|
|
42
47
|
index(): Promise<ShelfIndexEntry[]>;
|
|
@@ -34,6 +34,7 @@ export function flattenFields(items) {
|
|
|
34
34
|
kind: t.kind,
|
|
35
35
|
prim: t.prim,
|
|
36
36
|
required: t.required,
|
|
37
|
+
...(t.agent ? { agent: t.agent } : {}),
|
|
37
38
|
...(t.relation ? { relation: t.relation } : {}),
|
|
38
39
|
...(t.options ? { options: t.options } : {}),
|
|
39
40
|
...(t.prim === 'file' ? { write: FILE_WRITE_HINT } : {}),
|
|
@@ -50,6 +51,7 @@ export function flattenFields(items) {
|
|
|
50
51
|
kind: 'group',
|
|
51
52
|
prim: 'group',
|
|
52
53
|
required: it.required === true,
|
|
54
|
+
...(it.agent ? { agent: it.agent } : {}),
|
|
53
55
|
...(it.multiple ? { multiple: true } : {}),
|
|
54
56
|
fields: flattenFields(it.fields),
|
|
55
57
|
});
|
|
@@ -60,9 +62,14 @@ export function flattenFields(items) {
|
|
|
60
62
|
}
|
|
61
63
|
export class SchemaCache {
|
|
62
64
|
client;
|
|
65
|
+
locales;
|
|
63
66
|
cached;
|
|
64
|
-
constructor(client) {
|
|
67
|
+
constructor(client, locales) {
|
|
65
68
|
this.client = client;
|
|
69
|
+
this.locales = locales;
|
|
70
|
+
}
|
|
71
|
+
display(labelKey, id) {
|
|
72
|
+
return (labelKey ? this.locales?.resolve(labelKey) : undefined) ?? id;
|
|
66
73
|
}
|
|
67
74
|
refresh() {
|
|
68
75
|
this.cached = undefined;
|
|
@@ -74,7 +81,12 @@ export class SchemaCache {
|
|
|
74
81
|
}
|
|
75
82
|
async index() {
|
|
76
83
|
const libraries = await this.load();
|
|
77
|
-
return libraries.flatMap((v) => v.shelves.map((m) => ({
|
|
84
|
+
return libraries.flatMap((v) => v.shelves.map((m) => ({
|
|
85
|
+
library: v.id,
|
|
86
|
+
libraryLabel: this.display(v.label, v.id),
|
|
87
|
+
shelf: m.shelf,
|
|
88
|
+
label: this.display(m.label, m.shelf),
|
|
89
|
+
})));
|
|
78
90
|
}
|
|
79
91
|
async describeShelf(library, shelf) {
|
|
80
92
|
let m = this.find(await this.load(), library, shelf);
|
|
@@ -82,7 +94,13 @@ export class SchemaCache {
|
|
|
82
94
|
m = this.find(await this.load(true), library, shelf);
|
|
83
95
|
if (!m)
|
|
84
96
|
throw new Error(`unknown_shelf ${library}/${shelf}`);
|
|
85
|
-
return {
|
|
97
|
+
return {
|
|
98
|
+
library,
|
|
99
|
+
shelf,
|
|
100
|
+
label: this.display(m.label, m.shelf),
|
|
101
|
+
...(m.agent ? { agent: m.agent } : {}),
|
|
102
|
+
fields: flattenFields(m.fields),
|
|
103
|
+
};
|
|
86
104
|
}
|
|
87
105
|
find(libraries, library, shelf) {
|
|
88
106
|
return libraries.find((v) => v.id === library)?.shelves.find((m) => m.shelf === shelf);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { type CofferClientApi } from './client.ts';
|
|
3
3
|
import { SchemaCache } from './schema.ts';
|
|
4
|
+
import type { LocaleResolver } from '../locale-registry.ts';
|
|
4
5
|
export interface ToolResult {
|
|
5
6
|
content: {
|
|
6
7
|
type: 'text';
|
|
@@ -16,4 +17,4 @@ export interface ToolDef {
|
|
|
16
17
|
}
|
|
17
18
|
export declare const LIST_DEFAULT_LIMIT = 25;
|
|
18
19
|
export declare const LIST_MAX_LIMIT = 200;
|
|
19
|
-
export declare function buildTools(client: CofferClientApi, cache: SchemaCache): ToolDef[];
|
|
20
|
+
export declare function buildTools(client: CofferClientApi, cache: SchemaCache, locales?: LocaleResolver): ToolDef[];
|
|
@@ -40,7 +40,7 @@ async function guard(fn) {
|
|
|
40
40
|
}
|
|
41
41
|
export const LIST_DEFAULT_LIMIT = 25;
|
|
42
42
|
export const LIST_MAX_LIMIT = 200;
|
|
43
|
-
export function buildTools(client, cache) {
|
|
43
|
+
export function buildTools(client, cache, locales) {
|
|
44
44
|
const library = z.string().describe('Library identifier, e.g. "things"');
|
|
45
45
|
const shelf = z.string().describe('Shelf identifier, e.g. "item"');
|
|
46
46
|
const id = z.coerce.number().int().describe('record id');
|
|
@@ -53,12 +53,38 @@ export function buildTools(client, cache) {
|
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
name: 'describe_shelf',
|
|
56
|
-
description: 'Detailed shelf schema: fields with kind/prim/required
|
|
57
|
-
'
|
|
58
|
-
'writes — omit it when
|
|
56
|
+
description: 'Detailed shelf schema: the shelf note (agent), then fields with kind/prim/required, relation/options, and ' +
|
|
57
|
+
'a per-field note (agent) saying what that field means. A collection is one entry with multiple:true and its ' +
|
|
58
|
+
'row shape in fields[]. A field with derived:true is computed by the server and rejects writes — omit it when ' +
|
|
59
|
+
'writing. Call BEFORE create_record/update_record.',
|
|
59
60
|
inputSchema: { library, shelf },
|
|
60
61
|
handler: (a) => guard(() => cache.describeShelf(a.library, a.shelf)),
|
|
61
62
|
},
|
|
63
|
+
{
|
|
64
|
+
name: 'translate',
|
|
65
|
+
description: "Resolve interface text keys (dotted, e.g. 'things.item.options.condition.good') to their words in this " +
|
|
66
|
+
"instance's language. Use it when a payload hands you a key instead of a word — an option's title, an " +
|
|
67
|
+
'extra field-set name — and you need to say that thing to the user. It is for NAMING things in your answer: ' +
|
|
68
|
+
'tool arguments still take machine ids, so never send a translated word back as a library, shelf, field or ' +
|
|
69
|
+
'value. A key this instance cannot resolve comes back under "missing" — say you do not have a name for it ' +
|
|
70
|
+
'rather than inventing one.',
|
|
71
|
+
inputSchema: {
|
|
72
|
+
keys: z.array(z.string()).describe('Dotted interface text keys to resolve'),
|
|
73
|
+
},
|
|
74
|
+
handler: async (a) => {
|
|
75
|
+
const keys = Array.isArray(a.keys) ? a.keys.map((k) => String(k)) : [];
|
|
76
|
+
const resolved = {};
|
|
77
|
+
const missing = [];
|
|
78
|
+
for (const k of keys) {
|
|
79
|
+
const v = locales?.resolve(k);
|
|
80
|
+
if (v === undefined)
|
|
81
|
+
missing.push(k);
|
|
82
|
+
else
|
|
83
|
+
resolved[k] = v;
|
|
84
|
+
}
|
|
85
|
+
return ok({ resolved, missing });
|
|
86
|
+
},
|
|
87
|
+
},
|
|
62
88
|
{
|
|
63
89
|
name: 'refresh_schema',
|
|
64
90
|
description: 'Re-read the schema from the server. Call after enabling/installing a plugin (server restart required).',
|
package/dist/mcp-tools.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/core';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { type ToolResult } from './mcp-contract/tools.ts';
|
|
4
|
+
import { type LocaleResolver } from './locale-registry.ts';
|
|
4
5
|
import { type AuthRole, type PluginHooks } from './plugin-hooks.ts';
|
|
5
6
|
import { type Condition } from '@coffer-org/sdk/condition';
|
|
6
7
|
import { type RagHit } from './rag-search.ts';
|
|
@@ -12,7 +13,7 @@ export interface McpToolDef {
|
|
|
12
13
|
inputSchema: z.ZodRawShape;
|
|
13
14
|
scope: 'crud' | 'plugin' | 'rag' | 'settings' | 'upload';
|
|
14
15
|
role: AuthRole;
|
|
15
|
-
handler: (args: Record<string, unknown
|
|
16
|
+
handler: (args: Record<string, unknown>, signal?: AbortSignal) => Promise<ToolResult>;
|
|
16
17
|
}
|
|
17
18
|
export interface RagDeps {
|
|
18
19
|
embeddingApiKey: string;
|
|
@@ -31,23 +32,24 @@ export declare function collectPluginInstructions(hooks?: Record<string, PluginH
|
|
|
31
32
|
type SingleShelf = {
|
|
32
33
|
library: string;
|
|
33
34
|
shelf: string;
|
|
34
|
-
|
|
35
|
+
agent: string;
|
|
35
36
|
};
|
|
36
37
|
export declare function collectSingleShelves(reg?: {
|
|
37
38
|
shelves: {
|
|
38
39
|
library: string;
|
|
39
40
|
shelf: string;
|
|
40
41
|
single?: boolean;
|
|
41
|
-
|
|
42
|
+
agent?: string;
|
|
42
43
|
}[];
|
|
43
44
|
}): SingleShelf[];
|
|
44
45
|
type LibraryPurpose = {
|
|
45
46
|
id: string;
|
|
47
|
+
name: string;
|
|
46
48
|
agent: string;
|
|
47
49
|
extends: {
|
|
48
50
|
id: string;
|
|
49
51
|
shelf: string;
|
|
50
|
-
|
|
52
|
+
agent: string;
|
|
51
53
|
showWhen?: Condition;
|
|
52
54
|
}[];
|
|
53
55
|
};
|
|
@@ -55,31 +57,36 @@ export declare function collectLibraryPurposes(reg?: {
|
|
|
55
57
|
libraries: {
|
|
56
58
|
meta: {
|
|
57
59
|
id: string;
|
|
60
|
+
label?: string;
|
|
58
61
|
agent?: string;
|
|
59
62
|
};
|
|
60
63
|
}[];
|
|
61
64
|
extends_: {
|
|
62
65
|
id: string;
|
|
63
|
-
|
|
66
|
+
agent?: string;
|
|
64
67
|
showWhen?: Condition;
|
|
65
68
|
attachTo: {
|
|
66
69
|
library: string;
|
|
67
70
|
shelf: string;
|
|
68
71
|
}[];
|
|
69
72
|
}[];
|
|
70
|
-
}): LibraryPurpose[];
|
|
73
|
+
}, locales?: LocaleResolver): LibraryPurpose[];
|
|
71
74
|
export declare function siteSection(siteUrl: string): Promise<string | null>;
|
|
72
|
-
export declare function buildDomainSections(): Promise<string[]>;
|
|
75
|
+
export declare function buildDomainSections(locales?: LocaleResolver): Promise<string[]>;
|
|
76
|
+
export interface StarterHint {
|
|
77
|
+
id: string;
|
|
78
|
+
text: string;
|
|
79
|
+
hash: string;
|
|
80
|
+
}
|
|
81
|
+
export declare const CORE_STARTER_HINT: string;
|
|
73
82
|
export declare function collectStarterHints(hooks?: Record<string, PluginHooks>, emFactory?: () => EntityManager, reg?: {
|
|
74
83
|
libraries: {
|
|
75
84
|
meta: {
|
|
76
85
|
id: string;
|
|
86
|
+
label?: string;
|
|
77
87
|
starterHint?: string;
|
|
78
88
|
};
|
|
79
89
|
}[];
|
|
80
|
-
}): Promise<
|
|
81
|
-
combined: string;
|
|
82
|
-
hash: string;
|
|
83
|
-
}>;
|
|
90
|
+
}, locales?: LocaleResolver): Promise<StarterHint[]>;
|
|
84
91
|
export declare function buildMcpInstructions(sections: string[]): string;
|
|
85
92
|
export {};
|
package/dist/mcp-tools.js
CHANGED
|
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import { buildTools } from "./mcp-contract/tools.js";
|
|
4
4
|
import { SchemaCache } from "./mcp-contract/schema.js";
|
|
5
5
|
import { LocalClient } from "./mcp-local.js";
|
|
6
|
+
import { loadComposedLocales } from "./locale-registry.js";
|
|
6
7
|
import { frontendInstructions } from "./frontend-agent.js";
|
|
7
8
|
import { mintUploadTicket } from "./upload-ticket.js";
|
|
8
9
|
import { pluginHooks, pluginCtx } from "./plugin-hooks.js";
|
|
@@ -48,8 +49,9 @@ export async function resolveRagDeps() {
|
|
|
48
49
|
export async function collectMcpTools(opts = {}) {
|
|
49
50
|
const out = [];
|
|
50
51
|
const client = new LocalClient();
|
|
51
|
-
const
|
|
52
|
-
|
|
52
|
+
const locales = await loadComposedLocales();
|
|
53
|
+
const cache = new SchemaCache(client, locales);
|
|
54
|
+
for (const t of buildTools(client, cache, locales)) {
|
|
53
55
|
out.push({
|
|
54
56
|
server: 'coffer',
|
|
55
57
|
bareName: t.name,
|
|
@@ -278,10 +280,10 @@ export function collectSingleShelves(reg) {
|
|
|
278
280
|
if (!registry)
|
|
279
281
|
return [];
|
|
280
282
|
return registry.shelves
|
|
281
|
-
.filter((s) => s.single && s.
|
|
282
|
-
.map((s) => ({ library: s.library, shelf: s.shelf,
|
|
283
|
+
.filter((s) => s.single && s.agent)
|
|
284
|
+
.map((s) => ({ library: s.library, shelf: s.shelf, agent: s.agent }));
|
|
283
285
|
}
|
|
284
|
-
export function collectLibraryPurposes(reg) {
|
|
286
|
+
export function collectLibraryPurposes(reg, locales) {
|
|
285
287
|
let registry = reg;
|
|
286
288
|
if (!registry) {
|
|
287
289
|
try {
|
|
@@ -297,11 +299,12 @@ export function collectLibraryPurposes(reg) {
|
|
|
297
299
|
.filter((v) => v.meta.agent)
|
|
298
300
|
.map((v) => ({
|
|
299
301
|
id: v.meta.id,
|
|
302
|
+
name: (v.meta.label ? locales?.resolve(v.meta.label) : undefined) ?? v.meta.id,
|
|
300
303
|
agent: v.meta.agent,
|
|
301
|
-
extends: registry.extends_.flatMap((e) => e.
|
|
304
|
+
extends: registry.extends_.flatMap((e) => e.agent
|
|
302
305
|
? e.attachTo
|
|
303
306
|
.filter((a) => a.library === v.meta.id)
|
|
304
|
-
.map((a) => ({ id: e.id, shelf: a.shelf,
|
|
307
|
+
.map((a) => ({ id: e.id, shelf: a.shelf, agent: e.agent, showWhen: e.showWhen }))
|
|
305
308
|
: []),
|
|
306
309
|
}));
|
|
307
310
|
}
|
|
@@ -309,19 +312,20 @@ export async function siteSection(siteUrl) {
|
|
|
309
312
|
const site = await frontendInstructions(siteUrl);
|
|
310
313
|
return site ? `## web\n${site}` : null;
|
|
311
314
|
}
|
|
312
|
-
export async function buildDomainSections() {
|
|
313
|
-
const
|
|
315
|
+
export async function buildDomainSections(locales) {
|
|
316
|
+
const i18n = locales ?? (await loadComposedLocales());
|
|
317
|
+
const libraries = collectLibraryPurposes(undefined, i18n);
|
|
314
318
|
let overview = null;
|
|
315
319
|
if (libraries.length) {
|
|
316
320
|
const blocks = libraries.map((v) => {
|
|
317
|
-
let s = `### ${v.id} — ${v.agent}`;
|
|
321
|
+
let s = v.name === v.id ? `### ${v.id} — ${v.agent}` : `### ${v.id} ("${v.name}") — ${v.agent}`;
|
|
318
322
|
if (v.extends.length) {
|
|
319
323
|
s +=
|
|
320
324
|
'\nExtra field-sets some records carry (which one depends on the record):\n' +
|
|
321
325
|
v.extends
|
|
322
326
|
.map((e) => {
|
|
323
327
|
const when = describeCondition(e.showWhen, (f) => f).join(' and ');
|
|
324
|
-
return `- ${e.id} (on ${e.shelf}${when ? `, when ${when}` : ''}): ${e.
|
|
328
|
+
return `- ${e.id} (on ${e.shelf}${when ? `, when ${when}` : ''}): ${e.agent.replace(/\s*\n\s*/g, ' ')}`;
|
|
325
329
|
})
|
|
326
330
|
.join('\n');
|
|
327
331
|
}
|
|
@@ -333,7 +337,7 @@ export async function buildDomainSections() {
|
|
|
333
337
|
const singles = collectSingleShelves();
|
|
334
338
|
const singleSection = singles.length
|
|
335
339
|
? '## Single-record shelves (one document each — read the record, do not search the shelf)\n\n' +
|
|
336
|
-
singles.map((s) => `- ${s.library}/${s.shelf}: ${s.
|
|
340
|
+
singles.map((s) => `- ${s.library}/${s.shelf}: ${s.agent.replace(/\s*\n\s*/g, ' ')}`).join('\n')
|
|
337
341
|
: null;
|
|
338
342
|
const dataModel = '## Data model\n' +
|
|
339
343
|
'Library (top-level area) → shelf (a kind of record, e.g. things/item) → record (addressed library/shelf/id) → fields. ' +
|
|
@@ -347,16 +351,23 @@ export async function buildDomainSections() {
|
|
|
347
351
|
const rules = (await collectPluginInstructions()).map(({ id, instructions }) => `## ${id}\n${instructions}`);
|
|
348
352
|
return [dataModel, ...(overview ? [overview] : []), ...(singleSection ? [singleSection] : []), ...rules];
|
|
349
353
|
}
|
|
350
|
-
export
|
|
351
|
-
|
|
354
|
+
export const CORE_STARTER_HINT = 'what this Coffer instance itself holds — which libraries are present and what kinds of ' +
|
|
355
|
+
'questions the stored data can answer';
|
|
356
|
+
function hintHash(text) {
|
|
357
|
+
return createHash('sha1').update(text).digest('hex');
|
|
358
|
+
}
|
|
359
|
+
export async function collectStarterHints(hooks = pluginHooks, emFactory = () => getEm().fork(), reg, locales) {
|
|
360
|
+
const out = [{ id: 'system', text: CORE_STARTER_HINT, hash: hintHash(CORE_STARTER_HINT) }];
|
|
352
361
|
for (const [id, h] of Object.entries(hooks)) {
|
|
353
362
|
const hint = h.agent?.starterHint;
|
|
354
363
|
if (hint == null)
|
|
355
364
|
continue;
|
|
356
365
|
try {
|
|
357
|
-
const
|
|
358
|
-
if (
|
|
359
|
-
|
|
366
|
+
const hintText = typeof hint === 'function' ? await hint(pluginCtx(id, emFactory())) : hint;
|
|
367
|
+
if (hintText) {
|
|
368
|
+
const text = `${id}: ${hintText}`;
|
|
369
|
+
out.push({ id: `plugin:${id}`, text, hash: hintHash(text) });
|
|
370
|
+
}
|
|
360
371
|
}
|
|
361
372
|
catch (e) {
|
|
362
373
|
log.warn(`plugin ${id}: starterHint skipped — ${e.message}`);
|
|
@@ -371,13 +382,15 @@ export async function collectStarterHints(hooks = pluginHooks, emFactory = () =>
|
|
|
371
382
|
registry = undefined;
|
|
372
383
|
}
|
|
373
384
|
}
|
|
385
|
+
const i18n = locales ?? (await loadComposedLocales());
|
|
374
386
|
for (const v of registry?.libraries ?? []) {
|
|
375
|
-
if (v.meta.starterHint)
|
|
376
|
-
|
|
387
|
+
if (!v.meta.starterHint)
|
|
388
|
+
continue;
|
|
389
|
+
const name = v.meta.label ? i18n.resolve(v.meta.label) : undefined;
|
|
390
|
+
const text = `${v.meta.id}${name ? ` ("${name}")` : ''}: ${v.meta.starterHint}`;
|
|
391
|
+
out.push({ id: `library:${v.meta.id}`, text, hash: hintHash(text) });
|
|
377
392
|
}
|
|
378
|
-
|
|
379
|
-
const hash = createHash('sha1').update(combined).digest('hex');
|
|
380
|
-
return { combined, hash };
|
|
393
|
+
return out;
|
|
381
394
|
}
|
|
382
395
|
export function buildMcpInstructions(sections) {
|
|
383
396
|
const base = [
|
|
@@ -12,8 +12,8 @@ export interface StartersDeps {
|
|
|
12
12
|
loadAgentId: typeof loadAgentId;
|
|
13
13
|
resolveAgent: (id?: string) => AgentRuntime;
|
|
14
14
|
getDefaultAgentId: typeof getDefaultAgentId;
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
now: () => number;
|
|
16
|
+
shuffle: <T>(xs: T[]) => T[];
|
|
17
17
|
}
|
|
18
|
-
export declare function getConversationStarters(deps?: StartersDeps): Promise<string[]>;
|
|
19
18
|
export declare function refreshSystemStarters(deps?: StartersDeps): Promise<void>;
|
|
19
|
+
export declare function getConversationStarters(deps?: StartersDeps): Promise<string[]>;
|
|
@@ -3,12 +3,24 @@ import { SYSTEM_SETTINGS_ID } from "../system-settings.js";
|
|
|
3
3
|
import { getPluginState, setPluginState } from "../plugin-state.js";
|
|
4
4
|
import { collectStarterHints } from "../mcp-tools.js";
|
|
5
5
|
import { getLogger } from '@coffer-org/sdk/logger';
|
|
6
|
-
import { todayDateString } from "./environment.js";
|
|
7
6
|
import { loadAgentId } from "./config.js";
|
|
8
7
|
import { resolveAgent, getDefaultAgentId } from "./registry.js";
|
|
9
8
|
const log = getLogger('orchestrator');
|
|
10
9
|
const PLUGIN = 'orchestrator';
|
|
11
10
|
const STATE_KEY = 'system_starters';
|
|
11
|
+
const POOL_VERSION = 2;
|
|
12
|
+
const ENTRY_TTL_MS = 24 * 60 * 60_000;
|
|
13
|
+
const SAMPLE_MIN = 4;
|
|
14
|
+
const SAMPLE_MAX = 5;
|
|
15
|
+
const PER_SOURCE_SOFT_CAP = 2;
|
|
16
|
+
function shuffled(xs) {
|
|
17
|
+
const out = [...xs];
|
|
18
|
+
for (let i = out.length - 1; i > 0; i--) {
|
|
19
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
20
|
+
[out[i], out[j]] = [out[j], out[i]];
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
12
24
|
const defaultDeps = {
|
|
13
25
|
getPluginSettings,
|
|
14
26
|
getPluginState,
|
|
@@ -17,65 +29,120 @@ const defaultDeps = {
|
|
|
17
29
|
loadAgentId,
|
|
18
30
|
resolveAgent,
|
|
19
31
|
getDefaultAgentId,
|
|
20
|
-
|
|
21
|
-
|
|
32
|
+
now: () => Date.now(),
|
|
33
|
+
shuffle: shuffled,
|
|
22
34
|
};
|
|
23
35
|
async function currentLanguage(deps) {
|
|
24
36
|
const system = await deps.getPluginSettings(SYSTEM_SETTINGS_ID);
|
|
25
37
|
const v = system['language'];
|
|
26
38
|
return typeof v === 'string' && v ? v : 'en';
|
|
27
39
|
}
|
|
28
|
-
|
|
40
|
+
function isPool(v) {
|
|
41
|
+
if (typeof v !== 'object' || v === null)
|
|
42
|
+
return false;
|
|
43
|
+
const p = v;
|
|
44
|
+
return p.version === POOL_VERSION && typeof p.language === 'string' && typeof p.sources === 'object' && !!p.sources;
|
|
45
|
+
}
|
|
46
|
+
async function readPool(deps) {
|
|
29
47
|
const raw = await deps.getPluginState(PLUGIN, STATE_KEY);
|
|
30
48
|
if (!raw)
|
|
31
49
|
return null;
|
|
32
50
|
try {
|
|
33
|
-
|
|
51
|
+
const parsed = JSON.parse(raw);
|
|
52
|
+
return isPool(parsed) ? parsed : null;
|
|
34
53
|
}
|
|
35
54
|
catch {
|
|
36
55
|
return null;
|
|
37
56
|
}
|
|
38
57
|
}
|
|
39
|
-
|
|
40
|
-
return
|
|
58
|
+
function fresh(entry, nowMs) {
|
|
59
|
+
return nowMs - Date.parse(entry.generatedAt) < ENTRY_TTL_MS;
|
|
60
|
+
}
|
|
61
|
+
function tryResolve(deps, agentId) {
|
|
62
|
+
try {
|
|
63
|
+
return deps.resolveAgent(agentId);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
41
68
|
}
|
|
42
69
|
export async function refreshSystemStarters(deps = defaultDeps) {
|
|
43
|
-
const date = deps.today();
|
|
44
70
|
const [language, hints] = await Promise.all([currentLanguage(deps), deps.collectStarterHints()]);
|
|
45
71
|
const agentId = (await deps.loadAgentId()) ?? deps.getDefaultAgentId();
|
|
46
72
|
if (!agentId)
|
|
47
73
|
return;
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
74
|
+
const stored = await readPool(deps);
|
|
75
|
+
const pool = stored && stored.language === language ? stored : { version: POOL_VERSION, language, sources: {} };
|
|
76
|
+
let dirty = pool !== stored;
|
|
77
|
+
const declared = new Set(hints.map((h) => h.id));
|
|
78
|
+
for (const id of Object.keys(pool.sources)) {
|
|
79
|
+
if (!declared.has(id)) {
|
|
80
|
+
delete pool.sources[id];
|
|
81
|
+
dirty = true;
|
|
82
|
+
}
|
|
55
83
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
84
|
+
const nowMs = deps.now();
|
|
85
|
+
const stale = hints.find((h) => {
|
|
86
|
+
const e = pool.sources[h.id];
|
|
87
|
+
if (!e)
|
|
88
|
+
return true;
|
|
89
|
+
if (e.hintHash !== h.hash || e.agentId !== agentId)
|
|
90
|
+
return true;
|
|
91
|
+
return !fresh(e, nowMs);
|
|
92
|
+
});
|
|
93
|
+
if (stale) {
|
|
94
|
+
const runtime = tryResolve(deps, agentId);
|
|
95
|
+
if (runtime?.starters) {
|
|
96
|
+
try {
|
|
97
|
+
const messages = await runtime.starters(stale.text);
|
|
98
|
+
pool.sources[stale.id] = {
|
|
99
|
+
messages,
|
|
100
|
+
generatedAt: new Date(nowMs).toISOString(),
|
|
101
|
+
hintHash: stale.hash,
|
|
102
|
+
agentId,
|
|
103
|
+
};
|
|
104
|
+
dirty = true;
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
log.warn(`starters for ${stale.id} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
59
110
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return;
|
|
111
|
+
if (dirty)
|
|
112
|
+
await setPool(deps, pool);
|
|
113
|
+
}
|
|
114
|
+
async function setPool(deps, pool) {
|
|
115
|
+
await deps.setPluginState(PLUGIN, STATE_KEY, JSON.stringify(pool));
|
|
116
|
+
}
|
|
117
|
+
export async function getConversationStarters(deps = defaultDeps) {
|
|
118
|
+
const [pool, language] = await Promise.all([readPool(deps), currentLanguage(deps)]);
|
|
119
|
+
if (!pool)
|
|
120
|
+
return [];
|
|
121
|
+
if (pool.language !== language)
|
|
122
|
+
return [];
|
|
123
|
+
const nowMs = deps.now();
|
|
124
|
+
const seen = new Set();
|
|
125
|
+
const primary = [];
|
|
126
|
+
const spare = [];
|
|
127
|
+
for (const entry of deps.shuffle(Object.values(pool.sources))) {
|
|
128
|
+
if (!fresh(entry, nowMs))
|
|
129
|
+
continue;
|
|
130
|
+
let taken = 0;
|
|
131
|
+
for (const message of deps.shuffle(entry.messages)) {
|
|
132
|
+
if (seen.has(message))
|
|
133
|
+
continue;
|
|
134
|
+
seen.add(message);
|
|
135
|
+
if (taken < PER_SOURCE_SOFT_CAP) {
|
|
136
|
+
primary.push(message);
|
|
137
|
+
taken++;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
spare.push(message);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
72
143
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
agentId,
|
|
78
|
-
starters,
|
|
79
|
-
generatedAt: deps.now(),
|
|
80
|
-
}));
|
|
144
|
+
const out = deps.shuffle(primary).slice(0, SAMPLE_MAX);
|
|
145
|
+
if (out.length < SAMPLE_MIN)
|
|
146
|
+
out.push(...deps.shuffle(spare).slice(0, SAMPLE_MAX - out.length));
|
|
147
|
+
return out;
|
|
81
148
|
}
|
|
@@ -8,7 +8,7 @@ export interface AgentToolDefinition {
|
|
|
8
8
|
name: string;
|
|
9
9
|
description: string;
|
|
10
10
|
inputSchema: Record<string, unknown>;
|
|
11
|
-
handler: (args: Record<string, unknown
|
|
11
|
+
handler: (args: Record<string, unknown>, signal?: AbortSignal) => Promise<unknown>;
|
|
12
12
|
forcedAfterAnswer?: boolean;
|
|
13
13
|
}
|
|
14
14
|
export interface AgentToolProvider {
|
|
@@ -83,7 +83,7 @@ export interface AgentRuntime {
|
|
|
83
83
|
run(request: AgentRequest): Promise<AgentResult>;
|
|
84
84
|
systemBase(): Promise<string>;
|
|
85
85
|
describe(): Promise<AgentDescriptor>;
|
|
86
|
-
starters?(): Promise<string[]>;
|
|
86
|
+
starters?(hint: string): Promise<string[]>;
|
|
87
87
|
}
|
|
88
88
|
export interface ConnectorRegistration {
|
|
89
89
|
id: string;
|
package/dist/plugin-discovery.js
CHANGED
|
@@ -11,7 +11,7 @@ import { join, sep } from 'node:path';
|
|
|
11
11
|
import { pathToFileURL } from 'node:url';
|
|
12
12
|
import { getLogger } from '@coffer-org/sdk/logger';
|
|
13
13
|
const log = getLogger('discovery');
|
|
14
|
-
async function
|
|
14
|
+
export async function pluginPackageNames(nm) {
|
|
15
15
|
const names = [];
|
|
16
16
|
try {
|
|
17
17
|
for (const d of await readdir(join(nm, '@coffer-org'))) {
|
|
@@ -58,7 +58,7 @@ export async function discoverPluginAssets() {
|
|
|
58
58
|
if (assetsCache)
|
|
59
59
|
return assetsCache;
|
|
60
60
|
const nm = join(process.cwd(), 'node_modules');
|
|
61
|
-
const names = await
|
|
61
|
+
const names = await pluginPackageNames(nm);
|
|
62
62
|
const out = [];
|
|
63
63
|
for (const name of names) {
|
|
64
64
|
try {
|
|
@@ -121,7 +121,7 @@ async function loadManifest(nm, name) {
|
|
|
121
121
|
}
|
|
122
122
|
export async function discoverPlugins() {
|
|
123
123
|
const nm = join(process.cwd(), 'node_modules');
|
|
124
|
-
const names = await
|
|
124
|
+
const names = await pluginPackageNames(nm);
|
|
125
125
|
const manifests = [];
|
|
126
126
|
for (const name of names) {
|
|
127
127
|
const m = await loadManifest(nm, name);
|
|
@@ -132,7 +132,7 @@ export async function discoverPlugins() {
|
|
|
132
132
|
}
|
|
133
133
|
export async function loadServerHooks() {
|
|
134
134
|
const nm = join(process.cwd(), 'node_modules');
|
|
135
|
-
const names = await
|
|
135
|
+
const names = await pluginPackageNames(nm);
|
|
136
136
|
const hooks = {};
|
|
137
137
|
for (const name of names) {
|
|
138
138
|
try {
|
package/dist/plugin-i18n.d.ts
CHANGED
package/dist/plugin-i18n.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
const cache = new Map();
|
|
3
|
-
async function systemLanguage(fallback) {
|
|
3
|
+
export async function systemLanguage(fallback) {
|
|
4
4
|
try {
|
|
5
5
|
const { getPluginSettings } = await import("./plugin-runtime.js");
|
|
6
6
|
const { SYSTEM_SETTINGS_ID } = await import("./system-settings.js");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffer-org/server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24"
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"postpack": "node ../../scripts/swap-exports.mjs src"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@coffer-org/sdk": "^
|
|
39
|
+
"@coffer-org/sdk": "^7.0.0",
|
|
40
40
|
"@extractus/oembed-extractor": "^4.1.0",
|
|
41
41
|
"@fastify/cors": "^11.2.0",
|
|
42
42
|
"@fastify/multipart": "^10.0.0",
|