@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,33 @@
|
|
|
1
|
+
const MAX_SUGGESTIONS = 4;
|
|
2
|
+
export function makeSuggestionCapabilities() {
|
|
3
|
+
let captured = null;
|
|
4
|
+
const tool = {
|
|
5
|
+
name: 'suggest_replies',
|
|
6
|
+
description: 'You have already given your final answer, shown above. Propose 2-4 short, concrete follow-up replies in ' +
|
|
7
|
+
"the user's own language that a user might plausibly send next — grounded in what you ACTUALLY wrote, not " +
|
|
8
|
+
'generic filler. If your answer ended with a question, one suggestion should directly answer that exact ' +
|
|
9
|
+
'question. If your answer named specific items (a record, a recipe, a place), prefer a suggestion that ' +
|
|
10
|
+
'references one of them by name over a vague "tell me more". Call this once; skip it entirely if nothing ' +
|
|
11
|
+
'concrete fits — it does not change your answer, it only offers the user clickable shortcuts.',
|
|
12
|
+
inputSchema: {
|
|
13
|
+
suggestions: {
|
|
14
|
+
type: 'array',
|
|
15
|
+
items: { type: 'string' },
|
|
16
|
+
description: '2-4 short reply options, grounded in the answer above, in the language of the conversation.',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
forcedAfterAnswer: true,
|
|
20
|
+
handler: async (args) => {
|
|
21
|
+
const raw = args['suggestions'];
|
|
22
|
+
const list = Array.isArray(raw)
|
|
23
|
+
? raw
|
|
24
|
+
.map((v) => String(v).trim())
|
|
25
|
+
.filter((v) => v.length > 0)
|
|
26
|
+
.slice(0, MAX_SUGGESTIONS)
|
|
27
|
+
: [];
|
|
28
|
+
captured = list.length ? list : null;
|
|
29
|
+
return { ok: true };
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
return { tools: [tool], getCaptured: () => captured };
|
|
33
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { currentEnvironment } from "./environment.js";
|
|
2
|
+
export function buildSystem(input) {
|
|
3
|
+
const text = [input.base, input.channelSystem].filter(Boolean).join('\n\n');
|
|
4
|
+
const layers = [{ text, stable: true }];
|
|
5
|
+
const environment = input.environment ?? currentEnvironment();
|
|
6
|
+
const volatile = [environment, input.volatileSystem]
|
|
7
|
+
.map((part) => part?.trim())
|
|
8
|
+
.filter(Boolean)
|
|
9
|
+
.join('\n\n');
|
|
10
|
+
if (volatile)
|
|
11
|
+
layers.push({ text: volatile, stable: false });
|
|
12
|
+
return layers;
|
|
13
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { stat, statfs } from 'node:fs/promises';
|
|
2
|
+
import { arch, freemem, platform, totalmem } from 'node:os';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { discoverRuntime } from "../plugin-discovery.js";
|
|
5
|
+
import { systemTimeZone } from "./environment.js";
|
|
6
|
+
const mb = (bytes) => Math.round(bytes / 1024 / 1024);
|
|
7
|
+
function databasePath() {
|
|
8
|
+
return resolve(process.env['DB_PATH'] ?? 'data/app.db');
|
|
9
|
+
}
|
|
10
|
+
async function databaseState() {
|
|
11
|
+
const path = databasePath();
|
|
12
|
+
const out = { path };
|
|
13
|
+
try {
|
|
14
|
+
out['size_mb'] = mb((await stat(path)).size);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
out['size_mb'] = null;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const fs = await statfs(dirname(path));
|
|
21
|
+
out['disk_free_mb'] = mb(Number(fs.bavail) * Number(fs.bsize));
|
|
22
|
+
out['disk_total_mb'] = mb(Number(fs.blocks) * Number(fs.bsize));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
out['disk_free_mb'] = null;
|
|
26
|
+
out['disk_total_mb'] = null;
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
async function runtimeVersion() {
|
|
31
|
+
try {
|
|
32
|
+
return (await discoverRuntime())?.installedVersion ?? null;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function makeSystemCapabilities() {
|
|
39
|
+
const serverState = {
|
|
40
|
+
name: 'mcp__coffer__server_state',
|
|
41
|
+
description: 'Health and identity of the Coffer server this assistant runs on: installed version, uptime, Node version, ' +
|
|
42
|
+
'platform, memory, and the database file path with its size and free disk space. ' +
|
|
43
|
+
'Use it for questions about the instance itself ("which version am I running", "is the disk filling up", ' +
|
|
44
|
+
'"how long has the server been up"). It returns nothing about the user\'s records — use count_records for those. ' +
|
|
45
|
+
'The current date and time are already given in the system prompt; do not call this tool to find them.',
|
|
46
|
+
inputSchema: {},
|
|
47
|
+
handler: async () => ({
|
|
48
|
+
coffer_version: await runtimeVersion(),
|
|
49
|
+
uptime_seconds: Math.round(process.uptime()),
|
|
50
|
+
node_version: process.version,
|
|
51
|
+
platform: platform(),
|
|
52
|
+
arch: arch(),
|
|
53
|
+
timezone: systemTimeZone(),
|
|
54
|
+
memory: {
|
|
55
|
+
total_mb: mb(totalmem()),
|
|
56
|
+
free_mb: mb(freemem()),
|
|
57
|
+
process_rss_mb: mb(process.memoryUsage().rss),
|
|
58
|
+
},
|
|
59
|
+
database: await databaseState(),
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
return { tools: [serverState] };
|
|
63
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
export interface AttachmentRef {
|
|
2
|
+
name: string;
|
|
3
|
+
mime?: string;
|
|
4
|
+
size?: number;
|
|
5
|
+
label?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface AgentToolDefinition {
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
inputSchema: Record<string, unknown>;
|
|
11
|
+
handler: (args: Record<string, unknown>) => Promise<unknown>;
|
|
12
|
+
forcedAfterAnswer?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface AgentToolProvider {
|
|
15
|
+
tools: AgentToolDefinition[];
|
|
16
|
+
}
|
|
17
|
+
export type AgentToolContentBlock = {
|
|
18
|
+
type: 'text';
|
|
19
|
+
text: string;
|
|
20
|
+
} | {
|
|
21
|
+
type: 'image';
|
|
22
|
+
mime: string;
|
|
23
|
+
data: string;
|
|
24
|
+
} | {
|
|
25
|
+
type: 'document';
|
|
26
|
+
mime: 'application/pdf';
|
|
27
|
+
data: string;
|
|
28
|
+
};
|
|
29
|
+
export interface AgentToolContentResult {
|
|
30
|
+
__cofferToolContent: true;
|
|
31
|
+
content: AgentToolContentBlock[];
|
|
32
|
+
isError?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface ConvMessage {
|
|
35
|
+
role: 'user' | 'assistant';
|
|
36
|
+
content: string;
|
|
37
|
+
attachments?: AttachmentRef[];
|
|
38
|
+
sender?: string | null;
|
|
39
|
+
msgId: string;
|
|
40
|
+
ts: number;
|
|
41
|
+
}
|
|
42
|
+
export interface SystemLayer {
|
|
43
|
+
text: string;
|
|
44
|
+
stable: boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface AgentCapabilities {
|
|
47
|
+
vision?: boolean;
|
|
48
|
+
documents?: boolean;
|
|
49
|
+
tools?: boolean;
|
|
50
|
+
reasoning?: boolean;
|
|
51
|
+
}
|
|
52
|
+
export interface AgentPreset {
|
|
53
|
+
id: string;
|
|
54
|
+
title: string;
|
|
55
|
+
hint?: string;
|
|
56
|
+
quality?: number;
|
|
57
|
+
time?: number;
|
|
58
|
+
default?: boolean;
|
|
59
|
+
capabilities?: AgentCapabilities;
|
|
60
|
+
}
|
|
61
|
+
export interface AgentDescriptor {
|
|
62
|
+
id: string;
|
|
63
|
+
title: string;
|
|
64
|
+
presets: AgentPreset[];
|
|
65
|
+
}
|
|
66
|
+
export interface AgentRequest {
|
|
67
|
+
system: SystemLayer[];
|
|
68
|
+
messages: ConvMessage[];
|
|
69
|
+
toolProvider?: AgentToolProvider;
|
|
70
|
+
presetId?: string;
|
|
71
|
+
onDelta?: (accumulated: string) => void;
|
|
72
|
+
onReasoning?: (accumulated: string) => void;
|
|
73
|
+
onSegment?: () => void;
|
|
74
|
+
}
|
|
75
|
+
export interface AgentResult {
|
|
76
|
+
text: string | null;
|
|
77
|
+
reasoning: string | null;
|
|
78
|
+
tokensIn: number | null;
|
|
79
|
+
tokensOut: number | null;
|
|
80
|
+
stopReason: string | null;
|
|
81
|
+
presetId: string | null;
|
|
82
|
+
suggestions?: string[] | null;
|
|
83
|
+
}
|
|
84
|
+
export interface AgentRuntime {
|
|
85
|
+
id: string;
|
|
86
|
+
run(request: AgentRequest): Promise<AgentResult>;
|
|
87
|
+
systemBase(): Promise<string>;
|
|
88
|
+
describe(): Promise<AgentDescriptor>;
|
|
89
|
+
starters?(): Promise<string[]>;
|
|
90
|
+
}
|
|
91
|
+
export interface ConnectorRegistration {
|
|
92
|
+
id: string;
|
|
93
|
+
}
|
|
94
|
+
export interface AttachmentMaterializer {
|
|
95
|
+
store(bytes: Uint8Array, opts?: {
|
|
96
|
+
originalName?: string;
|
|
97
|
+
mime?: string;
|
|
98
|
+
}): Promise<AttachmentRef>;
|
|
99
|
+
}
|
|
100
|
+
export interface IncomingConversation {
|
|
101
|
+
connectorId: string;
|
|
102
|
+
agentId?: string;
|
|
103
|
+
presetId?: string;
|
|
104
|
+
chatId: string;
|
|
105
|
+
channelSystem?: string;
|
|
106
|
+
volatileSystem?: string;
|
|
107
|
+
sender: {
|
|
108
|
+
id: string;
|
|
109
|
+
displayName?: string;
|
|
110
|
+
};
|
|
111
|
+
messages: ConvMessage[];
|
|
112
|
+
prepareAttachments?: (materializer: AttachmentMaterializer) => Promise<ConvMessage[]>;
|
|
113
|
+
supportsSuggestions?: boolean;
|
|
114
|
+
}
|
|
115
|
+
export interface ReplyContext {
|
|
116
|
+
parentMsgId: string | null;
|
|
117
|
+
}
|
|
118
|
+
export interface ReplyPayload {
|
|
119
|
+
text: string | null;
|
|
120
|
+
reasoning: string | null;
|
|
121
|
+
suggestions: string[] | null;
|
|
122
|
+
}
|
|
123
|
+
export interface ReplyChannel {
|
|
124
|
+
update(text: string): void;
|
|
125
|
+
updateReasoning?(acc: string): void;
|
|
126
|
+
segment(): void;
|
|
127
|
+
finish(r: ReplyPayload): Promise<string | null>;
|
|
128
|
+
}
|
|
129
|
+
export interface Connector {
|
|
130
|
+
id: string;
|
|
131
|
+
reply(chatId: string, ctx: ReplyContext): ReplyChannel;
|
|
132
|
+
recordAssistant(m: {
|
|
133
|
+
parentMsgId: string | null;
|
|
134
|
+
botMsgId: string | null;
|
|
135
|
+
text: string;
|
|
136
|
+
reasoning: string | null;
|
|
137
|
+
}): Promise<void>;
|
|
138
|
+
}
|
|
139
|
+
export interface GatePolicy {
|
|
140
|
+
agentId?: string;
|
|
141
|
+
accessPassword: string;
|
|
142
|
+
triggerPrefix: string;
|
|
143
|
+
replyWindow: number;
|
|
144
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/part-injection.d.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/core';
|
|
1
2
|
import { type LayoutEl } from '@coffer-org/sdk/fields';
|
|
3
|
+
import type { RefState } from './part-ref.ts';
|
|
4
|
+
import { type Row } from './compute-unit.ts';
|
|
5
|
+
export type PartMemo = Map<string, unknown>;
|
|
6
|
+
export declare const newPartMemo: () => PartMemo;
|
|
2
7
|
export interface PartCtx {
|
|
3
|
-
record: Record<string, unknown>;
|
|
4
8
|
settings: Record<string, unknown>;
|
|
5
9
|
global: Record<string, unknown>;
|
|
6
10
|
meta: {
|
|
7
11
|
library: string;
|
|
8
12
|
shelf: string;
|
|
9
13
|
};
|
|
14
|
+
em?: EntityManager;
|
|
15
|
+
memo?: PartMemo;
|
|
16
|
+
refs?: RefState;
|
|
17
|
+
now?: string;
|
|
10
18
|
}
|
|
11
|
-
export declare function
|
|
12
|
-
export declare function applyStoredParts(fields: LayoutEl[], row:
|
|
19
|
+
export declare function hasStoredPartWork(fields: LayoutEl[]): boolean;
|
|
20
|
+
export declare function applyStoredParts(fields: LayoutEl[], row: Row, ctx: PartCtx): Promise<Row>;
|
package/dist/part-injection.js
CHANGED
|
@@ -1,125 +1,105 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { hasChildren, isNamedField, isCollectionGroup, isStorageGroup } from '@coffer-org/sdk/fields';
|
|
2
|
+
import { runFieldUnit, isComputeUnit, childScope, newScope, isPlainObject, } from "./compute-unit.js";
|
|
3
|
+
export const newPartMemo = () => new Map();
|
|
4
|
+
async function completeUnit(scope, key) {
|
|
5
|
+
if (scope.status.get(key) === 'done')
|
|
6
|
+
return;
|
|
7
|
+
const fm = scope.units.get(key);
|
|
8
|
+
if (!fm)
|
|
9
|
+
return;
|
|
10
|
+
await runFieldUnit(scope, key, fm);
|
|
11
|
+
scope.status.set(key, 'done');
|
|
4
12
|
}
|
|
5
|
-
async function
|
|
6
|
-
if (value == null || typeof value !== 'object' || Array.isArray(value))
|
|
7
|
-
return value;
|
|
8
|
-
const self = { ...value };
|
|
9
|
-
for (const p of fm.parts) {
|
|
10
|
-
if (p.mode !== 'pinned' && p.mode !== 'computed')
|
|
11
|
-
continue;
|
|
12
|
-
try {
|
|
13
|
-
self[p.role] = await partValue(p, ctx, self);
|
|
14
|
-
}
|
|
15
|
-
catch (e) {
|
|
16
|
-
console.warn(`[part-injection] '${key}.${p.role}' threw`, e);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return self;
|
|
20
|
-
}
|
|
21
|
-
async function computeOne(key, fm, value, ctx) {
|
|
22
|
-
if (value == null || typeof value !== 'object' || Array.isArray(value))
|
|
23
|
-
return value;
|
|
24
|
-
const self = { ...value };
|
|
25
|
-
const contextOnly = [];
|
|
26
|
-
for (const p of fm.parts) {
|
|
27
|
-
if (p.mode !== 'pinned' && p.mode !== 'computed')
|
|
28
|
-
continue;
|
|
29
|
-
contextOnly.push(p.role);
|
|
30
|
-
try {
|
|
31
|
-
self[p.role] = await partValue(p, ctx, self);
|
|
32
|
-
}
|
|
33
|
-
catch (e) {
|
|
34
|
-
console.warn(`[part-compute] '${key}.${p.role}' threw`, e);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
for (const p of fm.parts) {
|
|
38
|
-
if (p.mode !== 'computedStored')
|
|
39
|
-
continue;
|
|
40
|
-
try {
|
|
41
|
-
self[p.key] = await partValue(p, ctx, self);
|
|
42
|
-
}
|
|
43
|
-
catch (e) {
|
|
44
|
-
console.warn(`[part-compute] '${key}.${p.role}' threw`, e);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
for (const role of contextOnly)
|
|
48
|
-
delete self[role];
|
|
49
|
-
return self;
|
|
50
|
-
}
|
|
51
|
-
async function walkFields(fields, obj, ctx, complete) {
|
|
52
|
-
const out = { ...obj };
|
|
13
|
+
async function driveScope(scope, fields) {
|
|
53
14
|
for (const it of fields) {
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
if (!fm.parts?.length)
|
|
57
|
-
continue;
|
|
58
|
-
const value = out[it.key];
|
|
59
|
-
if (value == null)
|
|
15
|
+
if (isNamedField(it)) {
|
|
16
|
+
if (!isComputeUnit(it.type))
|
|
60
17
|
continue;
|
|
61
|
-
|
|
62
|
-
if (!Array.isArray(value))
|
|
63
|
-
continue;
|
|
64
|
-
out[it.key] = await Promise.all(value.map((v) => complete(it.key, fm, v, ctx)));
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
out[it.key] = await complete(it.key, fm, value, ctx);
|
|
68
|
-
}
|
|
18
|
+
await completeUnit(scope, it.key);
|
|
69
19
|
}
|
|
70
|
-
else if (
|
|
71
|
-
|
|
72
|
-
|
|
20
|
+
else if (hasChildren(it)) {
|
|
21
|
+
const sub = it.fields;
|
|
22
|
+
if (!isStorageGroup(it)) {
|
|
23
|
+
await driveScope(scope, sub);
|
|
73
24
|
}
|
|
74
25
|
else if (isCollectionGroup(it)) {
|
|
75
|
-
const rows =
|
|
76
|
-
if (Array.isArray(rows))
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
26
|
+
const rows = scope.obj[it.key];
|
|
27
|
+
if (!Array.isArray(rows))
|
|
28
|
+
continue;
|
|
29
|
+
const copies = rows.map((row) => (isPlainObject(row) ? { ...row } : row));
|
|
30
|
+
scope.obj[it.key] = copies;
|
|
31
|
+
await Promise.all(copies.map((row, i) => isPlainObject(row)
|
|
32
|
+
? runScope(scope.st, row, sub, [...scope.path, it.key, i], null, `collection row '${it.key}'`)
|
|
33
|
+
: Promise.resolve(row)));
|
|
81
34
|
}
|
|
82
35
|
else {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
36
|
+
const cur = scope.obj[it.key];
|
|
37
|
+
const child = childScope(scope, it.key);
|
|
38
|
+
if (child) {
|
|
39
|
+
await driveScope(child, sub);
|
|
40
|
+
}
|
|
41
|
+
else if (cur == null && scope.st.mode === 'write' && partWork(sub).computedStored) {
|
|
42
|
+
const made = newScope(scope.st, {}, sub, [...scope.path, it.key], scope.level, scope.level.label);
|
|
43
|
+
await driveScope(made, sub);
|
|
44
|
+
if (Object.keys(made.obj).length) {
|
|
45
|
+
scope.obj[it.key] = made.obj;
|
|
46
|
+
scope.groups.set(it.key, made);
|
|
47
|
+
}
|
|
86
48
|
}
|
|
87
49
|
}
|
|
88
50
|
}
|
|
89
51
|
}
|
|
90
|
-
|
|
52
|
+
}
|
|
53
|
+
async function runScope(st, obj, fields, path, level, label) {
|
|
54
|
+
const scope = newScope(st, obj, fields, path, level, label);
|
|
55
|
+
await driveScope(scope, fields);
|
|
56
|
+
return scope.obj;
|
|
91
57
|
}
|
|
92
58
|
const partWorkCache = new WeakMap();
|
|
93
59
|
function partWork(fields) {
|
|
94
60
|
const cached = partWorkCache.get(fields);
|
|
95
61
|
if (cached !== undefined)
|
|
96
62
|
return cached;
|
|
97
|
-
const found = {
|
|
63
|
+
const found = { computedStored: false };
|
|
98
64
|
for (const it of fields) {
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
65
|
+
if (isNamedField(it)) {
|
|
66
|
+
if (it.type.compute !== undefined)
|
|
67
|
+
found.computedStored = true;
|
|
68
|
+
for (const p of it.type.parts ?? [])
|
|
69
|
+
if (p.mode === 'computedStored')
|
|
104
70
|
found.computedStored = true;
|
|
105
|
-
}
|
|
106
71
|
}
|
|
107
|
-
else if (
|
|
72
|
+
else if (hasChildren(it)) {
|
|
73
|
+
if (isStorageGroup(it) && it.compute !== undefined)
|
|
74
|
+
found.computedStored = true;
|
|
108
75
|
const sub = partWork(it.fields);
|
|
109
|
-
found.unstored ||= sub.unstored;
|
|
110
76
|
found.computedStored ||= sub.computedStored;
|
|
111
77
|
}
|
|
112
78
|
}
|
|
113
79
|
partWorkCache.set(fields, found);
|
|
114
80
|
return found;
|
|
115
81
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
82
|
+
async function runWalk(fields, row, ctx) {
|
|
83
|
+
const id = row['id'] ?? row['base_id'];
|
|
84
|
+
const st = {
|
|
85
|
+
ctx,
|
|
86
|
+
mode: 'write',
|
|
87
|
+
memo: ctx.memo ?? newPartMemo(),
|
|
88
|
+
refs: ctx.refs ?? null,
|
|
89
|
+
selfKey: typeof id === 'number' ? `${ctx.meta.library}/${ctx.meta.shelf}#${id}` : null,
|
|
90
|
+
recordLevel: null,
|
|
91
|
+
now: ctx.now ?? new Date().toISOString(),
|
|
92
|
+
};
|
|
93
|
+
const scope = newScope(st, { ...row }, fields, [], null, `${ctx.meta.library}/${ctx.meta.shelf}`);
|
|
94
|
+
st.recordLevel = scope.level;
|
|
95
|
+
await driveScope(scope, fields);
|
|
96
|
+
return scope.obj;
|
|
97
|
+
}
|
|
98
|
+
export function hasStoredPartWork(fields) {
|
|
99
|
+
return partWork(fields).computedStored;
|
|
120
100
|
}
|
|
121
101
|
export async function applyStoredParts(fields, row, ctx) {
|
|
122
102
|
if (!partWork(fields).computedStored)
|
|
123
103
|
return row;
|
|
124
|
-
return
|
|
104
|
+
return runWalk(fields, row, ctx);
|
|
125
105
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/core';
|
|
2
|
+
import type { ShelfDef } from '@coffer-org/sdk/shelf';
|
|
3
|
+
type Row = Record<string, unknown>;
|
|
4
|
+
export type CompleteRef = (m: ShelfDef, record: Row, refs: RefState) => Promise<Row>;
|
|
5
|
+
export interface RefLoader {
|
|
6
|
+
load(library: string, shelf: string, id: number, chain: readonly string[]): Promise<Row | null>;
|
|
7
|
+
}
|
|
8
|
+
export interface RefState {
|
|
9
|
+
loader: RefLoader;
|
|
10
|
+
chain: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
export declare function createRefLoader(complete: CompleteRef, em?: EntityManager): RefLoader;
|
|
13
|
+
export {};
|
package/dist/part-ref.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { getEm } from "./db.js";
|
|
2
|
+
import { selectRows } from "./read-rows.js";
|
|
3
|
+
import { nestEmbedded, readAtMany } from "./collection-io.js";
|
|
4
|
+
import { decodeTemporal } from "./temporal.js";
|
|
5
|
+
import { shelfTableName } from "./entity-schema.js";
|
|
6
|
+
import { getShelf } from "./registry-context.js";
|
|
7
|
+
import { chunk } from "./batch.js";
|
|
8
|
+
import { PartContractError } from "./part-errors.js";
|
|
9
|
+
const MAX_REF_DEPTH = 8;
|
|
10
|
+
export function createRefLoader(complete, em) {
|
|
11
|
+
const cache = new Map();
|
|
12
|
+
const waiters = new Map();
|
|
13
|
+
const pending = new Map();
|
|
14
|
+
let scheduled = false;
|
|
15
|
+
const loader = { load };
|
|
16
|
+
function load(library, shelf, id, chain) {
|
|
17
|
+
const key = `${library}/${shelf}#${id}`;
|
|
18
|
+
if (chain.includes(key)) {
|
|
19
|
+
console.warn(`[part-ref] ref loop ${[...chain, key].join(' → ')} — resolved as null. Two records reach each other ` +
|
|
20
|
+
`through ctx.ref; the value at the far end of the loop cannot be computed.`);
|
|
21
|
+
return Promise.resolve(null);
|
|
22
|
+
}
|
|
23
|
+
if (chain.length >= MAX_REF_DEPTH) {
|
|
24
|
+
console.warn(`[part-ref] ref chain deeper than ${MAX_REF_DEPTH} hops (${[...chain, key].join(' → ')}) — resolved as null.`);
|
|
25
|
+
return Promise.resolve(null);
|
|
26
|
+
}
|
|
27
|
+
const hit = cache.get(key);
|
|
28
|
+
if (hit)
|
|
29
|
+
return hit;
|
|
30
|
+
const p = new Promise((resolve, reject) => {
|
|
31
|
+
waiters.set(key, { resolve, reject, chain: [...chain, key] });
|
|
32
|
+
});
|
|
33
|
+
p.catch(() => { });
|
|
34
|
+
cache.set(key, p);
|
|
35
|
+
const shelfKey = `${library}/${shelf}`;
|
|
36
|
+
const ids = pending.get(shelfKey);
|
|
37
|
+
if (ids)
|
|
38
|
+
ids.push(id);
|
|
39
|
+
else
|
|
40
|
+
pending.set(shelfKey, [id]);
|
|
41
|
+
if (!scheduled) {
|
|
42
|
+
scheduled = true;
|
|
43
|
+
queueMicrotask(flush);
|
|
44
|
+
}
|
|
45
|
+
return p;
|
|
46
|
+
}
|
|
47
|
+
function flush() {
|
|
48
|
+
scheduled = false;
|
|
49
|
+
const batches = [...pending];
|
|
50
|
+
pending.clear();
|
|
51
|
+
for (const [shelfKey, ids] of batches)
|
|
52
|
+
void runBatch(shelfKey, ids);
|
|
53
|
+
}
|
|
54
|
+
function settle(key, fn) {
|
|
55
|
+
const w = waiters.get(key);
|
|
56
|
+
if (!w)
|
|
57
|
+
return;
|
|
58
|
+
waiters.delete(key);
|
|
59
|
+
fn(w);
|
|
60
|
+
}
|
|
61
|
+
async function runBatch(shelfKey, ids) {
|
|
62
|
+
const slash = shelfKey.indexOf('/');
|
|
63
|
+
const library = shelfKey.slice(0, slash);
|
|
64
|
+
const shelf = shelfKey.slice(slash + 1);
|
|
65
|
+
const keyOf = (id) => `${shelfKey}#${id}`;
|
|
66
|
+
const unique = [...new Set(ids)];
|
|
67
|
+
try {
|
|
68
|
+
const m = getShelf(library, shelf);
|
|
69
|
+
if (!m)
|
|
70
|
+
throw new PartContractError(`ctx.ref: no shelf '${shelfKey}' in the registry`);
|
|
71
|
+
const fork = em ?? getEm().fork();
|
|
72
|
+
const table = shelfTableName(library, shelf);
|
|
73
|
+
const flats = [];
|
|
74
|
+
for (const part of chunk(unique)) {
|
|
75
|
+
flats.push(...(await selectRows(fork, table, { id: { $in: part }, deleted_at: null })));
|
|
76
|
+
}
|
|
77
|
+
const foundIds = flats.map((r) => Number(r['id']));
|
|
78
|
+
const collectionsById = await readAtMany(fork, table, m.fields, foundIds);
|
|
79
|
+
const seen = new Set();
|
|
80
|
+
await Promise.all(flats.map(async (flat) => {
|
|
81
|
+
const id = Number(flat['id']);
|
|
82
|
+
seen.add(id);
|
|
83
|
+
const key = keyOf(id);
|
|
84
|
+
const w = waiters.get(key);
|
|
85
|
+
if (!w)
|
|
86
|
+
return;
|
|
87
|
+
const record = {
|
|
88
|
+
...nestEmbedded(m, decodeTemporal(m, flat)),
|
|
89
|
+
...(collectionsById.get(id) ?? {}),
|
|
90
|
+
};
|
|
91
|
+
try {
|
|
92
|
+
const done = await complete(m, record, { loader, chain: w.chain });
|
|
93
|
+
settle(key, (x) => x.resolve(done));
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
settle(key, (x) => x.reject(e));
|
|
97
|
+
}
|
|
98
|
+
}));
|
|
99
|
+
for (const id of unique)
|
|
100
|
+
if (!seen.has(id))
|
|
101
|
+
settle(keyOf(id), (w) => w.resolve(null));
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
for (const id of unique)
|
|
105
|
+
settle(keyOf(id), (w) => w.reject(e));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return loader;
|
|
109
|
+
}
|
package/dist/plugin-hooks.d.ts
CHANGED
|
@@ -42,6 +42,7 @@ export interface AgentTool {
|
|
|
42
42
|
export interface AgentContribution {
|
|
43
43
|
instructions?: string | ((ctx: PluginCtx) => string | Promise<string>);
|
|
44
44
|
tools?: AgentTool[];
|
|
45
|
+
starterHint?: string | ((ctx: PluginCtx) => string | Promise<string>);
|
|
45
46
|
}
|
|
46
47
|
export type PluginAction = (body: Record<string, unknown>) => Promise<unknown>;
|
|
47
48
|
export declare class HttpError extends Error {
|
package/dist/plugin-i18n.js
CHANGED
|
@@ -3,7 +3,8 @@ const cache = new Map();
|
|
|
3
3
|
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");
|
|
7
|
+
return (await getPluginSettings(SYSTEM_SETTINGS_ID))['language'] || fallback;
|
|
7
8
|
}
|
|
8
9
|
catch {
|
|
9
10
|
return fallback;
|
package/dist/plugin-runtime.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { type Registry } from '@coffer-org/
|
|
1
|
+
import { type Registry } from '@coffer-org/sdk/compose';
|
|
2
2
|
import type { PluginManifest } from '@coffer-org/sdk/plugin';
|
|
3
3
|
export declare function getPlugins(): Promise<PluginManifest[]>;
|
|
4
4
|
export declare function readDisabled(): Promise<Set<string>>;
|
|
5
5
|
export declare function getDisabled(): Promise<Set<string>>;
|
|
6
|
-
export declare function getPluginSettings(
|
|
6
|
+
export declare function getPluginSettings(groupId: string): Promise<Record<string, unknown>>;
|
|
7
7
|
export declare function requireSettings<K extends string>(pluginId: string, keys: readonly K[]): Promise<Record<K, string>>;
|
|
8
8
|
export declare function initStorage(): Promise<Set<string>>;
|
|
9
9
|
export declare function initPlugins(): Promise<Registry>;
|