@coffer-org/server 3.3.0 → 4.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/auth-api.js +4 -5
- package/dist/cache-clock.d.ts +8 -0
- package/dist/cache-clock.js +103 -0
- package/dist/cache-refresh.d.ts +7 -0
- package/dist/cache-refresh.js +322 -0
- package/dist/collection-io.d.ts +13 -0
- package/dist/collection-io.js +42 -28
- package/dist/compute-unit.d.ts +66 -0
- package/dist/compute-unit.js +369 -0
- package/dist/condition-where.js +1 -3
- package/dist/data-dir.d.ts +3 -0
- package/dist/data-dir.js +39 -0
- package/dist/db.js +2 -0
- package/dist/entity-schema.d.ts +1 -0
- package/dist/entity-schema.js +25 -2
- package/dist/extend-io.d.ts +3 -2
- package/dist/extend-io.js +4 -4
- package/dist/extend-table.d.ts +3 -2
- package/dist/extend-table.js +48 -11
- package/dist/file-fields.js +50 -23
- package/dist/global-search.js +1 -1
- package/dist/http-errors.d.ts +7 -0
- package/dist/http-errors.js +24 -0
- package/dist/index.js +31 -35
- package/dist/mcp-contract/client.d.ts +24 -0
- package/dist/mcp-contract/client.js +12 -0
- package/dist/mcp-contract/schema.d.ts +46 -0
- package/dist/mcp-contract/schema.js +90 -0
- package/dist/mcp-contract/tools.d.ts +19 -0
- package/dist/mcp-contract/tools.js +160 -0
- package/dist/mcp-local.d.ts +1 -1
- package/dist/mcp-local.js +4 -2
- package/dist/mcp-tools.d.ts +12 -1
- package/dist/mcp-tools.js +41 -8
- package/dist/migrations.d.ts +30 -2
- package/dist/migrations.js +368 -19
- package/dist/mutate.js +86 -57
- package/dist/orchestrator/agent-capabilities.d.ts +2 -0
- package/dist/orchestrator/agent-capabilities.js +201 -0
- package/dist/orchestrator/allow.d.ts +16 -0
- package/dist/orchestrator/allow.js +65 -0
- package/dist/orchestrator/attachments.d.ts +2 -0
- package/dist/orchestrator/attachments.js +11 -0
- package/dist/orchestrator/config.d.ts +4 -0
- package/dist/orchestrator/config.js +19 -0
- package/dist/orchestrator/db.d.ts +17 -0
- package/dist/orchestrator/db.js +12 -0
- package/dist/orchestrator/diagnostics.d.ts +9 -0
- package/dist/orchestrator/diagnostics.js +13 -0
- package/dist/orchestrator/environment.d.ts +3 -0
- package/dist/orchestrator/environment.js +30 -0
- package/dist/orchestrator/file-inspection.d.ts +3 -0
- package/dist/orchestrator/file-inspection.js +61 -0
- package/dist/orchestrator/format.d.ts +1 -0
- package/dist/orchestrator/format.js +6 -0
- package/dist/orchestrator/index.d.ts +30 -0
- package/dist/orchestrator/index.js +56 -0
- package/dist/orchestrator/live-message.d.ts +14 -0
- package/dist/orchestrator/live-message.js +100 -0
- package/dist/orchestrator/pipeline.d.ts +11 -0
- package/dist/orchestrator/pipeline.js +198 -0
- package/dist/orchestrator/registry.d.ts +13 -0
- package/dist/orchestrator/registry.js +74 -0
- package/dist/orchestrator/starters.d.ts +19 -0
- package/dist/orchestrator/starters.js +81 -0
- package/dist/orchestrator/suggestion-capabilities.d.ts +5 -0
- package/dist/orchestrator/suggestion-capabilities.js +33 -0
- package/dist/orchestrator/system-assembly.d.ts +7 -0
- package/dist/orchestrator/system-assembly.js +13 -0
- package/dist/orchestrator/system-capabilities.d.ts +2 -0
- package/dist/orchestrator/system-capabilities.js +63 -0
- package/dist/orchestrator/types.d.ts +144 -0
- package/dist/orchestrator/types.js +1 -0
- package/dist/part-errors.d.ts +3 -0
- package/dist/part-errors.js +6 -0
- package/dist/part-injection.d.ts +11 -3
- package/dist/part-injection.js +72 -92
- package/dist/part-ref.d.ts +13 -0
- package/dist/part-ref.js +109 -0
- package/dist/plugin-hooks.d.ts +1 -0
- package/dist/plugin-i18n.js +2 -1
- package/dist/plugin-runtime.d.ts +2 -2
- package/dist/plugin-runtime.js +21 -11
- package/dist/plugin-updates.d.ts +2 -2
- package/dist/plugin-updates.js +6 -12
- package/dist/public-url.js +2 -1
- package/dist/recompute-derived.d.ts +2 -1
- package/dist/recompute-derived.js +123 -26
- package/dist/records-api.d.ts +5 -1
- package/dist/records-api.js +47 -15
- package/dist/registry-context.d.ts +1 -1
- package/dist/registry-context.js +3 -0
- package/dist/rename-guard.d.ts +1 -0
- package/dist/rename-guard.js +9 -0
- package/dist/schema-sync.js +38 -1
- package/dist/search-index.js +1 -1
- package/dist/search-indexer.d.ts +1 -1
- package/dist/search-indexer.js +3 -3
- package/dist/settings-groups.d.ts +14 -0
- package/dist/settings-groups.js +25 -0
- package/dist/settings-write.d.ts +4 -4
- package/dist/settings-write.js +23 -26
- package/dist/system-settings.d.ts +3 -0
- package/dist/system-settings.js +27 -0
- package/dist/thread-store.d.ts +3 -2
- package/dist/thread-store.js +6 -5
- package/dist/trigger-queue.d.ts +33 -0
- package/dist/trigger-queue.js +432 -0
- package/dist/uploads.js +6 -3
- package/dist/value-compare.d.ts +1 -0
- package/dist/value-compare.js +7 -0
- package/package.json +17 -3
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ValidationError, NotFoundError, ConnectionError } from "./client.js";
|
|
3
|
+
const ok = (data) => ({
|
|
4
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
5
|
+
});
|
|
6
|
+
const fail = (text) => ({ content: [{ type: 'text', text }], isError: true });
|
|
7
|
+
const ISSUE_HINTS = {
|
|
8
|
+
file_remote_url: 'a file field stores an uploaded file, not a remote URL',
|
|
9
|
+
file_unknown_key: 'a file entry may only carry name, mime and size',
|
|
10
|
+
file_name: 'name must be a bare filename, without a path',
|
|
11
|
+
file_not_uploaded: 'no file by that name has been uploaded',
|
|
12
|
+
};
|
|
13
|
+
const FILE_RECIPE = '\n\nTo put a file in a file/image/media field:\n' +
|
|
14
|
+
'1. create_upload_ticket → {token, upload_url} — upload_url is the absolute address to post to\n' +
|
|
15
|
+
'2. curl -H "Authorization: Bearer <token>" -F "file=@<local path>" <upload_url> → {"name":"<filename>"}\n' +
|
|
16
|
+
'3. write the field as {"name":"<filename>"} (or an array of those).\n' +
|
|
17
|
+
'Downloading a remote image is your side of the job: fetch it to a local file first, then upload that file. ' +
|
|
18
|
+
'mime and size are set by the server. To remove a file, drop its entry — entries cannot be edited in place.';
|
|
19
|
+
async function guard(fn) {
|
|
20
|
+
try {
|
|
21
|
+
return ok(await fn());
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
if (e instanceof ValidationError) {
|
|
25
|
+
const lines = e.issues.map((i) => {
|
|
26
|
+
const o = i;
|
|
27
|
+
const code = o.code ?? 'invalid';
|
|
28
|
+
const hint = ISSUE_HINTS[code];
|
|
29
|
+
return `- ${o.field ?? '?'}: ${code}${hint ? ` — ${hint}` : ''}`;
|
|
30
|
+
});
|
|
31
|
+
const fileIssue = e.issues.some((i) => (i.code ?? '').startsWith('file_'));
|
|
32
|
+
return fail(`Validation error:\n${lines.join('\n')}${fileIssue ? FILE_RECIPE : ''}`);
|
|
33
|
+
}
|
|
34
|
+
if (e instanceof NotFoundError)
|
|
35
|
+
return fail('Record or shelf not found.');
|
|
36
|
+
if (e instanceof ConnectionError)
|
|
37
|
+
return fail(e.message);
|
|
38
|
+
return fail(`Unexpected error: ${e.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export const LIST_DEFAULT_LIMIT = 25;
|
|
42
|
+
export const LIST_MAX_LIMIT = 200;
|
|
43
|
+
export function buildTools(client, cache) {
|
|
44
|
+
const library = z.string().describe('Library identifier, e.g. "things"');
|
|
45
|
+
const shelf = z.string().describe('Shelf identifier, e.g. "item"');
|
|
46
|
+
const id = z.coerce.number().int().describe('record id');
|
|
47
|
+
return [
|
|
48
|
+
{
|
|
49
|
+
name: 'list_libraries',
|
|
50
|
+
description: 'Lightweight index of all libraries and shelves (library, shelf, label). Starting point for exploring content.',
|
|
51
|
+
inputSchema: {},
|
|
52
|
+
handler: () => guard(() => cache.index()),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'describe_shelf',
|
|
56
|
+
description: 'Detailed shelf schema: fields with kind/prim/required and relation/options. A collection is one entry with ' +
|
|
57
|
+
'multiple:true and its row shape in fields[]. A field with derived:true is computed by the server and rejects ' +
|
|
58
|
+
'writes — omit it when writing. Call BEFORE create_record/update_record.',
|
|
59
|
+
inputSchema: { library, shelf },
|
|
60
|
+
handler: (a) => guard(() => cache.describeShelf(a.library, a.shelf)),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'refresh_schema',
|
|
64
|
+
description: 'Re-read the schema from the server. Call after enabling/installing a plugin (server restart required).',
|
|
65
|
+
inputSchema: {},
|
|
66
|
+
handler: async () => {
|
|
67
|
+
cache.refresh();
|
|
68
|
+
return ok({ refreshed: true });
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'list_records',
|
|
73
|
+
description: `List records of a shelf, one page at a time. q — free-text search over the shelf's text fields ` +
|
|
74
|
+
`(this is how you FIND a record by part of its name: q "minisforum" matches "Minisforum UM790"); ` +
|
|
75
|
+
`filters — exact field match; folderId — folder filter; ` +
|
|
76
|
+
`limit — page size (default ${LIST_DEFAULT_LIMIT}, max ${LIST_MAX_LIMIT}); offset — rows to skip. ` +
|
|
77
|
+
`Returns {rows, total, limit, offset}: "total" is how many records match IN FULL, so answer "how many do I have" ` +
|
|
78
|
+
`from it instead of listing everything, and never conclude a record is missing from one unfiltered page — ` +
|
|
79
|
+
`search with q first.`,
|
|
80
|
+
inputSchema: {
|
|
81
|
+
library,
|
|
82
|
+
shelf,
|
|
83
|
+
q: z.string().optional().describe('Free-text search over text fields; matches partial words'),
|
|
84
|
+
filters: z
|
|
85
|
+
.record(z.string(), z.union([z.string(), z.number()]))
|
|
86
|
+
.optional()
|
|
87
|
+
.describe('Exact field=value match'),
|
|
88
|
+
folderId: z.string().optional(),
|
|
89
|
+
limit: z.number().int().positive().optional(),
|
|
90
|
+
offset: z.number().int().nonnegative().optional(),
|
|
91
|
+
},
|
|
92
|
+
handler: async (a) => guard(async () => {
|
|
93
|
+
const q = { ...a.filters };
|
|
94
|
+
if (a.folderId !== undefined)
|
|
95
|
+
q.folderId = a.folderId;
|
|
96
|
+
if (typeof a.q === 'string' && a.q.trim())
|
|
97
|
+
q.q = a.q.trim();
|
|
98
|
+
const limit = Math.min(typeof a.limit === 'number' ? a.limit : LIST_DEFAULT_LIMIT, LIST_MAX_LIMIT);
|
|
99
|
+
q.limit = limit;
|
|
100
|
+
const offset = typeof a.offset === 'number' ? a.offset : 0;
|
|
101
|
+
if (offset > 0)
|
|
102
|
+
q.offset = offset;
|
|
103
|
+
const res = await client.listRecords(a.library, a.shelf, q);
|
|
104
|
+
const paged = res;
|
|
105
|
+
const rows = Array.isArray(res) ? res : Array.isArray(paged.rows) ? paged.rows : [];
|
|
106
|
+
const total = typeof paged.total === 'number' ? paged.total : rows.length;
|
|
107
|
+
return { rows, total, limit, offset };
|
|
108
|
+
}),
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'get_record',
|
|
112
|
+
description: 'A single record by id (with _extends, if any).',
|
|
113
|
+
inputSchema: { library, shelf, id },
|
|
114
|
+
handler: (a) => guard(() => client.getRecord(a.library, a.shelf, a.id)),
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'create_record',
|
|
118
|
+
description: 'Create a record. fields — an object of field values (see describe_shelf). name is required.',
|
|
119
|
+
inputSchema: { library, shelf, fields: z.record(z.string(), z.unknown()).describe('Record field values') },
|
|
120
|
+
handler: (a) => guard(() => client.createRecord(a.library, a.shelf, a.fields)),
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'update_record',
|
|
124
|
+
description: 'Partial record update. fields — only the fields to change. Read the record first, call describe_shelf first, and preserve every unmentioned field; never overwrite conflicts silently.',
|
|
125
|
+
inputSchema: { library, shelf, id, fields: z.record(z.string(), z.unknown()) },
|
|
126
|
+
handler: (a) => guard(() => client.updateRecord(a.library, a.shelf, a.id, a.fields)),
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'delete_record',
|
|
130
|
+
description: 'Move a record to the trash by id (reversible soft-delete; data remains in the database). Read the complete record first and get explicit user confirmation for a non-obvious deletion. Never blank fields to simulate deletion.',
|
|
131
|
+
inputSchema: { library, shelf, id },
|
|
132
|
+
handler: (a) => guard(() => client.deleteRecord(a.library, a.shelf, a.id)),
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'list_trash',
|
|
136
|
+
description: 'List every record currently in the trash, including its full data. Use this before restoring or permanently purging a record; normal list_records/get_record calls hide trashed records.',
|
|
137
|
+
inputSchema: {},
|
|
138
|
+
handler: () => guard(() => client.listTrash()),
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'restore_record',
|
|
142
|
+
description: 'Restore a record from the trash. Inspect it with list_trash first and confirm the target if the user did not name the exact record.',
|
|
143
|
+
inputSchema: { library, shelf, id },
|
|
144
|
+
handler: (a) => guard(() => client.restoreRecord(a.library, a.shelf, a.id)),
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'purge_record',
|
|
148
|
+
description: 'Permanently delete a record that is already in the trash. This is irreversible: inspect list_trash and require explicit user confirmation immediately before calling it.',
|
|
149
|
+
inputSchema: {
|
|
150
|
+
library,
|
|
151
|
+
shelf,
|
|
152
|
+
id,
|
|
153
|
+
confirmed: z.literal(true).describe('Must be true only after the user explicitly confirmed permanent deletion'),
|
|
154
|
+
},
|
|
155
|
+
handler: async (a) => a.confirmed === true
|
|
156
|
+
? guard(() => client.purgeRecord(a.library, a.shelf, a.id))
|
|
157
|
+
: fail('Permanent deletion requires explicit user confirmation.'),
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
}
|
package/dist/mcp-local.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CofferClientApi } from '
|
|
1
|
+
import type { CofferClientApi } from './mcp-contract/client.ts';
|
|
2
2
|
export declare function mapError(e: unknown): never;
|
|
3
3
|
export declare function splitListQuery(raw: Record<string, unknown>): {
|
|
4
4
|
query: Record<string, unknown>;
|
package/dist/mcp-local.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ValidationError, NotFoundError } from
|
|
2
|
-
import { recordList, recordListPage, recordGet, recordCreate, recordUpdate, recordDelete, UnknownShelfError, recordTrashList, recordRestore, recordPurge, SingleShelfError, } from "./records-api.js";
|
|
1
|
+
import { ValidationError, NotFoundError } from "./mcp-contract/client.js";
|
|
2
|
+
import { recordList, recordListPage, recordGet, recordCreate, recordUpdate, recordDelete, UnknownShelfError, recordTrashList, recordRestore, recordPurge, SingleShelfError, FilterError, } from "./records-api.js";
|
|
3
3
|
import { ValidationError as ServerValidationError } from "./mutate.js";
|
|
4
4
|
import { buildClientSchema } from "./schema-api.js";
|
|
5
5
|
export function mapError(e) {
|
|
@@ -9,6 +9,8 @@ export function mapError(e) {
|
|
|
9
9
|
throw new NotFoundError('not_found');
|
|
10
10
|
if (e instanceof SingleShelfError)
|
|
11
11
|
throw new ValidationError([{ field: 'shelf', code: 'single_shelf', message: e.message }]);
|
|
12
|
+
if (e instanceof FilterError)
|
|
13
|
+
throw new ValidationError([{ field: 'query', code: 'bad_filter', message: e.message }]);
|
|
12
14
|
throw e;
|
|
13
15
|
}
|
|
14
16
|
export function splitListQuery(raw) {
|
package/dist/mcp-tools.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/core';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { type ToolResult } from '
|
|
3
|
+
import { type ToolResult } from './mcp-contract/tools.ts';
|
|
4
4
|
import { type AuthRole, type PluginHooks } from './plugin-hooks.ts';
|
|
5
5
|
import { type Condition } from '@coffer-org/sdk/condition';
|
|
6
6
|
import { type RagHit } from './rag-search.ts';
|
|
@@ -69,5 +69,16 @@ export declare function collectLibraryPurposes(reg?: {
|
|
|
69
69
|
}[];
|
|
70
70
|
}): LibraryPurpose[];
|
|
71
71
|
export declare function buildDomainSections(): Promise<string[]>;
|
|
72
|
+
export declare function collectStarterHints(hooks?: Record<string, PluginHooks>, emFactory?: () => EntityManager, reg?: {
|
|
73
|
+
libraries: {
|
|
74
|
+
meta: {
|
|
75
|
+
id: string;
|
|
76
|
+
starterHint?: string;
|
|
77
|
+
};
|
|
78
|
+
}[];
|
|
79
|
+
}): Promise<{
|
|
80
|
+
combined: string;
|
|
81
|
+
hash: string;
|
|
82
|
+
}>;
|
|
72
83
|
export declare function buildMcpInstructions(sections: string[]): string;
|
|
73
84
|
export {};
|
package/dist/mcp-tools.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { z } from 'zod';
|
|
2
|
-
import { buildTools } from
|
|
3
|
-
import { SchemaCache } from
|
|
3
|
+
import { buildTools } from "./mcp-contract/tools.js";
|
|
4
|
+
import { SchemaCache } from "./mcp-contract/schema.js";
|
|
4
5
|
import { LocalClient } from "./mcp-local.js";
|
|
5
6
|
import { frontendInstructions } from "./frontend-agent.js";
|
|
6
7
|
import { mintUploadTicket } from "./upload-ticket.js";
|
|
@@ -81,7 +82,7 @@ export async function collectMcpTools(opts = {}) {
|
|
|
81
82
|
how_to: `curl -H "Authorization: Bearer <token>" -F "file=@<path>" ${uploadUrl} → {"name":"<name>"}. Then set a file field to {"name":"<name>"}. mime/size are filled in by the server — do not send your own.` +
|
|
82
83
|
(base
|
|
83
84
|
? ''
|
|
84
|
-
: " The server has no public URL configured (
|
|
85
|
+
: " The server has no public URL configured (system settings publicUrl / PUBLIC_URL env), so the path is relative — resolve it against this MCP server's own origin."),
|
|
85
86
|
});
|
|
86
87
|
},
|
|
87
88
|
});
|
|
@@ -204,7 +205,7 @@ export async function collectMcpTools(opts = {}) {
|
|
|
204
205
|
server: 'coffer',
|
|
205
206
|
bareName: 'list_settings',
|
|
206
207
|
httpName: 'list_settings',
|
|
207
|
-
description:
|
|
208
|
+
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.",
|
|
208
209
|
inputSchema: {},
|
|
209
210
|
scope: 'settings',
|
|
210
211
|
role: 'admin',
|
|
@@ -221,13 +222,13 @@ export async function collectMcpTools(opts = {}) {
|
|
|
221
222
|
server: 'coffer',
|
|
222
223
|
bareName: 'update_settings',
|
|
223
224
|
httpName: 'update_settings',
|
|
224
|
-
description: 'Update a plugin or system settings group.
|
|
225
|
-
inputSchema: {
|
|
225
|
+
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.',
|
|
226
|
+
inputSchema: { group: z.string(), fields: z.record(z.string(), z.unknown()) },
|
|
226
227
|
scope: 'settings',
|
|
227
228
|
role: 'admin',
|
|
228
229
|
handler: async (args) => {
|
|
229
230
|
try {
|
|
230
|
-
const row = await writePluginSettings(getEm().fork(), args.
|
|
231
|
+
const row = await writePluginSettings(getEm().fork(), args.group, args.fields, actor);
|
|
231
232
|
return ok(row);
|
|
232
233
|
}
|
|
233
234
|
catch (e) {
|
|
@@ -239,7 +240,7 @@ export async function collectMcpTools(opts = {}) {
|
|
|
239
240
|
return fail(`Validation error:\n${lines.join('\n')}`);
|
|
240
241
|
}
|
|
241
242
|
if (e instanceof NotFoundError)
|
|
242
|
-
return fail(`
|
|
243
|
+
return fail(`No settings group '${String(args.group)}'.`);
|
|
243
244
|
return fail(`Error: ${e.message}`);
|
|
244
245
|
}
|
|
245
246
|
},
|
|
@@ -349,6 +350,38 @@ export async function buildDomainSections() {
|
|
|
349
350
|
...rules,
|
|
350
351
|
];
|
|
351
352
|
}
|
|
353
|
+
export async function collectStarterHints(hooks = pluginHooks, emFactory = () => getEm().fork(), reg) {
|
|
354
|
+
const parts = [];
|
|
355
|
+
for (const [id, h] of Object.entries(hooks)) {
|
|
356
|
+
const hint = h.agent?.starterHint;
|
|
357
|
+
if (hint == null)
|
|
358
|
+
continue;
|
|
359
|
+
try {
|
|
360
|
+
const text = typeof hint === 'function' ? await hint(pluginCtx(id, emFactory())) : hint;
|
|
361
|
+
if (text)
|
|
362
|
+
parts.push(`- ${id}: ${text}`);
|
|
363
|
+
}
|
|
364
|
+
catch (e) {
|
|
365
|
+
log.warn(`plugin ${id}: starterHint skipped — ${e.message}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
let registry = reg;
|
|
369
|
+
if (!registry) {
|
|
370
|
+
try {
|
|
371
|
+
registry = getActiveRegistry();
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
registry = undefined;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
for (const v of registry?.libraries ?? []) {
|
|
378
|
+
if (v.meta.starterHint)
|
|
379
|
+
parts.push(`- ${v.meta.id}: ${v.meta.starterHint}`);
|
|
380
|
+
}
|
|
381
|
+
const combined = parts.join('\n');
|
|
382
|
+
const hash = createHash('sha1').update(combined).digest('hex');
|
|
383
|
+
return { combined, hash };
|
|
384
|
+
}
|
|
352
385
|
export function buildMcpInstructions(sections) {
|
|
353
386
|
const base = [
|
|
354
387
|
"Coffer is the user's personal database, organized into libraries (kitchen, people, finance, health, devices, home, garden, documents, travel, and more), each holding typed records.",
|
package/dist/migrations.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { EntityManager, EntitySchema } from '@mikro-orm/core';
|
|
2
2
|
import type { Migration, PluginHooks, TableOps } from './plugin-hooks.ts';
|
|
3
|
+
export declare function markColumnHandedOver(table: string, column: string): void;
|
|
4
|
+
export declare function isColumnHandedOver(table: string, column: string): boolean;
|
|
3
5
|
export declare function introspectTable(em: EntityManager, table: string): Promise<{
|
|
4
6
|
exists: boolean;
|
|
5
7
|
columns: Set<string>;
|
|
@@ -9,8 +11,29 @@ export declare function introspectTable(em: EntityManager, table: string): Promi
|
|
|
9
11
|
primary: boolean;
|
|
10
12
|
}[];
|
|
11
13
|
}>;
|
|
12
|
-
export declare function
|
|
14
|
+
export declare function introspectAllColumns(em: EntityManager, tables: string[]): Promise<Map<string, Set<string>>>;
|
|
15
|
+
export type MigrationOpCode = 'no-table' | 'no-rows' | 'no-change' | 'no-source-column' | 'target-column-exists' | 'converted-nothing';
|
|
16
|
+
export declare const CONCERNING_CODES: ReadonlySet<MigrationOpCode>;
|
|
17
|
+
export interface MigrationOp {
|
|
18
|
+
op: 'renameColumn' | 'fill' | 'changeType' | 'convert' | 'dropColumn';
|
|
19
|
+
table: string;
|
|
20
|
+
performed: boolean;
|
|
21
|
+
rows?: number;
|
|
22
|
+
scanned?: number;
|
|
23
|
+
emptyTargets?: string[];
|
|
24
|
+
strandedSources?: string[];
|
|
25
|
+
code?: MigrationOpCode;
|
|
26
|
+
reason?: string;
|
|
27
|
+
}
|
|
28
|
+
export type OpRecorder = (op: MigrationOp) => void;
|
|
29
|
+
export declare function describeMigrationConcerns(plugin: string, version: number, ops: MigrationOp[]): string | null;
|
|
30
|
+
export declare function makeTable(em: EntityManager, table: string, record?: OpRecorder): TableOps;
|
|
13
31
|
export declare function renameSystemShelfKey(em: EntityManager): Promise<void>;
|
|
32
|
+
export declare const SYSTEM_MIGRATIONS: {
|
|
33
|
+
name: string;
|
|
34
|
+
up(em: EntityManager): Promise<void>;
|
|
35
|
+
}[];
|
|
36
|
+
export declare function runSystemMigrations(em: EntityManager): Promise<void>;
|
|
14
37
|
export declare function validateMigrations(pluginId: string, list: Migration[]): void;
|
|
15
38
|
interface RunArgs {
|
|
16
39
|
em: EntityManager;
|
|
@@ -19,7 +42,12 @@ interface RunArgs {
|
|
|
19
42
|
tables?: string[];
|
|
20
43
|
}[];
|
|
21
44
|
hooks: Record<string, PluginHooks>;
|
|
45
|
+
onApplied?: (r: {
|
|
46
|
+
plugin: string;
|
|
47
|
+
version: number;
|
|
48
|
+
ops: MigrationOp[];
|
|
49
|
+
}) => void;
|
|
22
50
|
}
|
|
23
|
-
export declare function runMigrations({ em, plugins, hooks }: RunArgs): Promise<void>;
|
|
51
|
+
export declare function runMigrations({ em, plugins, hooks, onApplied }: RunArgs): Promise<void>;
|
|
24
52
|
export declare function assertSafeRequired(em: EntityManager, entities: EntitySchema[]): Promise<void>;
|
|
25
53
|
export {};
|