@coffer-org/server 3.4.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.
Files changed (98) hide show
  1. package/dist/auth-api.js +4 -5
  2. package/dist/cache-clock.d.ts +8 -0
  3. package/dist/cache-clock.js +103 -0
  4. package/dist/cache-refresh.d.ts +7 -0
  5. package/dist/cache-refresh.js +322 -0
  6. package/dist/collection-io.d.ts +13 -0
  7. package/dist/collection-io.js +40 -29
  8. package/dist/compute-unit.d.ts +66 -0
  9. package/dist/compute-unit.js +369 -0
  10. package/dist/condition-where.js +1 -3
  11. package/dist/data-dir.d.ts +3 -0
  12. package/dist/data-dir.js +39 -0
  13. package/dist/entity-schema.js +5 -2
  14. package/dist/extend-table.d.ts +1 -1
  15. package/dist/extend-table.js +22 -8
  16. package/dist/file-fields.js +50 -23
  17. package/dist/global-search.js +1 -1
  18. package/dist/index.js +20 -26
  19. package/dist/mcp-contract/client.d.ts +24 -0
  20. package/dist/mcp-contract/client.js +12 -0
  21. package/dist/mcp-contract/schema.d.ts +46 -0
  22. package/dist/mcp-contract/schema.js +90 -0
  23. package/dist/mcp-contract/tools.d.ts +19 -0
  24. package/dist/mcp-contract/tools.js +160 -0
  25. package/dist/mcp-local.d.ts +1 -1
  26. package/dist/mcp-local.js +1 -1
  27. package/dist/mcp-tools.d.ts +12 -1
  28. package/dist/mcp-tools.js +41 -8
  29. package/dist/migrations.d.ts +5 -0
  30. package/dist/migrations.js +122 -0
  31. package/dist/mutate.js +77 -36
  32. package/dist/orchestrator/agent-capabilities.d.ts +2 -0
  33. package/dist/orchestrator/agent-capabilities.js +201 -0
  34. package/dist/orchestrator/allow.d.ts +16 -0
  35. package/dist/orchestrator/allow.js +65 -0
  36. package/dist/orchestrator/attachments.d.ts +2 -0
  37. package/dist/orchestrator/attachments.js +11 -0
  38. package/dist/orchestrator/config.d.ts +4 -0
  39. package/dist/orchestrator/config.js +19 -0
  40. package/dist/orchestrator/db.d.ts +17 -0
  41. package/dist/orchestrator/db.js +12 -0
  42. package/dist/orchestrator/diagnostics.d.ts +9 -0
  43. package/dist/orchestrator/diagnostics.js +13 -0
  44. package/dist/orchestrator/environment.d.ts +3 -0
  45. package/dist/orchestrator/environment.js +30 -0
  46. package/dist/orchestrator/file-inspection.d.ts +3 -0
  47. package/dist/orchestrator/file-inspection.js +61 -0
  48. package/dist/orchestrator/format.d.ts +1 -0
  49. package/dist/orchestrator/format.js +6 -0
  50. package/dist/orchestrator/index.d.ts +30 -0
  51. package/dist/orchestrator/index.js +56 -0
  52. package/dist/orchestrator/live-message.d.ts +14 -0
  53. package/dist/orchestrator/live-message.js +100 -0
  54. package/dist/orchestrator/pipeline.d.ts +11 -0
  55. package/dist/orchestrator/pipeline.js +198 -0
  56. package/dist/orchestrator/registry.d.ts +13 -0
  57. package/dist/orchestrator/registry.js +74 -0
  58. package/dist/orchestrator/starters.d.ts +19 -0
  59. package/dist/orchestrator/starters.js +81 -0
  60. package/dist/orchestrator/suggestion-capabilities.d.ts +5 -0
  61. package/dist/orchestrator/suggestion-capabilities.js +33 -0
  62. package/dist/orchestrator/system-assembly.d.ts +7 -0
  63. package/dist/orchestrator/system-assembly.js +13 -0
  64. package/dist/orchestrator/system-capabilities.d.ts +2 -0
  65. package/dist/orchestrator/system-capabilities.js +63 -0
  66. package/dist/orchestrator/types.d.ts +144 -0
  67. package/dist/orchestrator/types.js +1 -0
  68. package/dist/part-injection.d.ts +3 -4
  69. package/dist/part-injection.js +25 -368
  70. package/dist/plugin-hooks.d.ts +1 -0
  71. package/dist/plugin-i18n.js +2 -1
  72. package/dist/plugin-runtime.d.ts +2 -2
  73. package/dist/plugin-runtime.js +20 -11
  74. package/dist/plugin-updates.d.ts +2 -2
  75. package/dist/plugin-updates.js +6 -12
  76. package/dist/public-url.js +2 -1
  77. package/dist/recompute-derived.d.ts +2 -1
  78. package/dist/recompute-derived.js +111 -34
  79. package/dist/records-api.js +14 -16
  80. package/dist/registry-context.d.ts +1 -1
  81. package/dist/registry-context.js +3 -0
  82. package/dist/search-index.js +1 -1
  83. package/dist/search-indexer.d.ts +1 -1
  84. package/dist/search-indexer.js +3 -3
  85. package/dist/settings-groups.d.ts +14 -0
  86. package/dist/settings-groups.js +25 -0
  87. package/dist/settings-write.d.ts +4 -4
  88. package/dist/settings-write.js +23 -26
  89. package/dist/system-settings.d.ts +3 -0
  90. package/dist/system-settings.js +27 -0
  91. package/dist/thread-store.d.ts +3 -2
  92. package/dist/thread-store.js +6 -5
  93. package/dist/trigger-queue.d.ts +33 -0
  94. package/dist/trigger-queue.js +432 -0
  95. package/dist/uploads.js +6 -3
  96. package/dist/value-compare.d.ts +1 -0
  97. package/dist/value-compare.js +7 -0
  98. package/package.json +17 -3
@@ -1,60 +1,137 @@
1
1
  import { collectionGroups } from '@coffer-org/sdk/shelf';
2
- import { derivedEntries, applyDerived } from '@coffer-org/sdk/derive';
3
2
  import { applyStoredParts, hasStoredPartWork } from "./part-injection.js";
3
+ import { extendEntityName } from "./extend-table.js";
4
4
  import { getEm } from "./db.js";
5
5
  import { selectRows } from "./read-rows.js";
6
- import { encodeJson } from "./mutate.js";
7
- import { encodeTemporal, decodeTemporal } from "./temporal.js";
8
- import { flattenEmbedded, nestEmbedded, readCollections } from "./collection-io.js";
9
- function sameValue(a, b) {
10
- if (a instanceof Date && b instanceof Date)
11
- return a.getTime() === b.getTime();
12
- if (a instanceof Date || b instanceof Date)
13
- return false;
14
- return a === b;
15
- }
6
+ import { encodeJson, encodeJsonAt } from "./mutate.js";
7
+ import { encodeTemporal, decodeTemporal, encodeTemporalAt, decodeTemporalAt } from "./temporal.js";
8
+ import { flattenEmbedded, nestEmbedded, readCollectionsWithMeta, flattenEmbeddedAt, nestEmbeddedAt, readAtWithMeta, } from "./collection-io.js";
9
+ import { levelClockColumnsByOwner } from "./cache-clock.js";
10
+ import { sameValue } from "./value-compare.js";
16
11
  function partCtx(m) {
17
12
  return { settings: {}, global: {}, meta: { library: m.library, shelf: m.shelf } };
18
13
  }
19
- function changedTopLevel(m, before, after) {
20
- const collKeys = new Set(collectionGroups(m.fields).map((c) => c.key));
14
+ function extendCtx(e) {
15
+ return { settings: {}, global: {}, meta: { library: 'extend', shelf: e.id } };
16
+ }
17
+ function changedKeys(fields, before, after) {
18
+ const collKeys = new Set(collectionGroups(fields).map((c) => c.key));
19
+ const clocksByOwner = levelClockColumnsByOwner(fields);
20
+ const clockKeys = new Set([...clocksByOwner.values()].flat());
21
21
  const out = {};
22
22
  for (const [k, v] of Object.entries(after)) {
23
- if (collKeys.has(k))
23
+ if (collKeys.has(k) || clockKeys.has(k))
24
24
  continue;
25
25
  if (JSON.stringify(before[k] ?? null) !== JSON.stringify(v ?? null))
26
26
  out[k] = v;
27
27
  }
28
+ for (const [owner, cols] of clocksByOwner) {
29
+ if (!(owner in out))
30
+ continue;
31
+ for (const col of cols) {
32
+ if (after[col] !== undefined)
33
+ out[col] = after[col];
34
+ }
35
+ }
28
36
  return out;
29
37
  }
30
- export async function recomputeDerivedFields(m, entityName) {
31
- const storedParts = hasStoredPartWork(m.fields);
32
- if (derivedEntries(m.fields).length === 0 && !storedParts)
38
+ async function recomputeCollectionRows(em, tablePrefix, fields, before, after, meta) {
39
+ let anyChanged = false;
40
+ for (const c of collectionGroups(fields)) {
41
+ const table = `${tablePrefix}__${c.key}`;
42
+ const childFields = c.group.fields;
43
+ const beforeRows = before[c.key] ?? [];
44
+ const afterRows = after[c.key] ?? [];
45
+ const metas = meta[c.key] ?? [];
46
+ for (let i = 0; i < beforeRows.length; i++) {
47
+ const rm = metas[i];
48
+ const b = beforeRows[i];
49
+ const a = afterRows[i];
50
+ if (!rm || !b || !a)
51
+ continue;
52
+ const patch = changedKeys(childFields, b, a);
53
+ if (Object.keys(patch).length > 0) {
54
+ const encodedPatch = encodeJsonAt(childFields, flattenEmbeddedAt(childFields, patch));
55
+ const encodedBefore = encodeJsonAt(childFields, flattenEmbeddedAt(childFields, b));
56
+ const dirty = {};
57
+ for (const [k, v] of Object.entries(encodedPatch)) {
58
+ if (!sameValue(encodedBefore[k], v))
59
+ dirty[k] = v;
60
+ }
61
+ if (Object.keys(dirty).length > 0) {
62
+ await em.nativeUpdate(table, { id: rm.id }, dirty);
63
+ anyChanged = true;
64
+ }
65
+ }
66
+ if (await recomputeCollectionRows(em, table, childFields, b, a, rm.nested))
67
+ anyChanged = true;
68
+ }
69
+ }
70
+ return anyChanged;
71
+ }
72
+ async function recomputeExtendRow(em, e, baseId) {
73
+ const table = extendEntityName(e);
74
+ const [flat] = (await selectRows(em, table, { base_id: baseId }, { limit: 1 }));
75
+ if (!flat)
76
+ return false;
77
+ const decoded = nestEmbeddedAt(e.fields, decodeTemporalAt(e.fields, flat));
78
+ const { collections, meta } = await readAtWithMeta(em, table, e.fields, baseId);
79
+ const hydrated = { ...decoded, ...collections };
80
+ const withParts = await applyStoredParts(e.fields, hydrated, extendCtx(e));
81
+ let changed = false;
82
+ const patch = changedKeys(e.fields, hydrated, withParts);
83
+ if (Object.keys(patch).length > 0) {
84
+ const encoded = encodeJsonAt(e.fields, encodeTemporalAt(e.fields, flattenEmbeddedAt(e.fields, patch)));
85
+ const dirty = {};
86
+ for (const [k, v] of Object.entries(encoded)) {
87
+ if (!sameValue(flat[k], v))
88
+ dirty[k] = v;
89
+ }
90
+ if (Object.keys(dirty).length > 0) {
91
+ await em.nativeUpdate(table, { base_id: baseId }, dirty);
92
+ changed = true;
93
+ }
94
+ }
95
+ if (await recomputeCollectionRows(em, table, e.fields, collections, withParts, meta))
96
+ changed = true;
97
+ return changed;
98
+ }
99
+ export async function recomputeDerivedFields(m, entityName, extends_ = []) {
100
+ const hasExtendWork = extends_.some((e) => hasStoredPartWork(e.fields));
101
+ if (!hasStoredPartWork(m.fields) && !hasExtendWork)
33
102
  return { scanned: 0, changed: 0 };
34
103
  const fork = getEm().fork();
35
104
  const rows = await selectRows(fork, entityName, {}, { orderBy: { id: 'asc' } });
36
- let changed = 0;
105
+ const changedIds = new Set();
37
106
  for (const row of rows) {
38
107
  const id = row['id'];
39
108
  const flat = decodeTemporal(m, row);
40
109
  const nested = nestEmbedded(m, flat);
41
- const collections = await readCollections(fork, m, id);
110
+ const { collections, meta } = await readCollectionsWithMeta(fork, m, id);
42
111
  const hydrated = { ...nested, ...collections };
43
- const withParts = storedParts ? await applyStoredParts(m.fields, hydrated, partCtx(m)) : hydrated;
44
- const partPatch = storedParts ? changedTopLevel(m, hydrated, withParts) : {};
45
- const derived = await applyDerived(m.fields, withParts);
46
- if (Object.keys(derived).length === 0 && Object.keys(partPatch).length === 0)
47
- continue;
48
- const encoded = encodeJson(m, encodeTemporal(m, flattenEmbedded(m, { ...partPatch, ...derived })));
49
- const dirty = {};
50
- for (const [k, v] of Object.entries(encoded)) {
51
- if (!sameValue(row[k], v))
52
- dirty[k] = v;
112
+ const withParts = await applyStoredParts(m.fields, hydrated, partCtx(m));
113
+ const partPatch = changedKeys(m.fields, hydrated, withParts);
114
+ if (Object.keys(partPatch).length > 0) {
115
+ const encoded = encodeJson(m, encodeTemporal(m, flattenEmbedded(m, partPatch)));
116
+ const dirty = {};
117
+ for (const [k, v] of Object.entries(encoded)) {
118
+ if (!sameValue(row[k], v))
119
+ dirty[k] = v;
120
+ }
121
+ if (Object.keys(dirty).length > 0) {
122
+ await fork.nativeUpdate(entityName, { id }, dirty);
123
+ changedIds.add(id);
124
+ }
125
+ }
126
+ if (await recomputeCollectionRows(fork, entityName, m.fields, collections, withParts, meta)) {
127
+ changedIds.add(id);
128
+ }
129
+ for (const e of extends_) {
130
+ if (!hasStoredPartWork(e.fields))
131
+ continue;
132
+ if (await recomputeExtendRow(fork, e, id))
133
+ changedIds.add(id);
53
134
  }
54
- if (Object.keys(dirty).length === 0)
55
- continue;
56
- await fork.nativeUpdate(entityName, { id }, dirty);
57
- changed += 1;
58
135
  }
59
- return { scanned: rows.length, changed };
136
+ return { scanned: rows.length, changed: changedIds.size };
60
137
  }
@@ -1,5 +1,5 @@
1
- import { fieldMap, textSearchKeys, titleKey, recordTitle, listKeys, storageColumnsFor, magnitudeSub, } from '@coffer-org/sdk/shelf';
2
- import { tokenize, matchScoreFolded, foldText } from '@coffer-org/core/search';
1
+ import { fieldMap, textSearchKeys, titleKey, recordTitle, listKeys, storageColumnsFor, magnitudeSub, resolveColumnKey, } from '@coffer-org/sdk/shelf';
2
+ import { tokenize, matchScoreFolded, foldText } from '@coffer-org/sdk/search';
3
3
  import { getActiveRegistry, getShelf, getExtendsFor } from "./registry-context.js";
4
4
  import { selectRows } from "./read-rows.js";
5
5
  import { shelfTableName } from "./entity-schema.js";
@@ -10,6 +10,7 @@ import { splitBody, saveExtends, deleteExtends, validateExtends, readExtends } f
10
10
  import { createRecord, updateRecord, getRecord, deleteRecord, restoreRecord, purgeRecord } from "./mutate.js";
11
11
  import { newPartMemo } from "./part-injection.js";
12
12
  import { deleteRecordActivity } from "./record-activity.js";
13
+ import { stripClockKeys } from "./cache-clock.js";
13
14
  export class UnknownShelfError extends Error {
14
15
  }
15
16
  export class SingleShelfError extends Error {
@@ -95,26 +96,19 @@ export class FilterError extends Error {
95
96
  function resolveFilterKey(m, fm, k) {
96
97
  const direct = fm[k];
97
98
  if (direct) {
98
- if (direct.virtual)
99
- return undefined;
100
99
  if (!direct.columns)
101
100
  return { column: k, type: direct.column };
102
101
  const sub = magnitudeSub(direct);
103
102
  if (sub === undefined)
104
- throw new FilterError(`${m.library}/${m.shelf}: '${k}' is a composite whose parts are all computed or pinned — it owns no column to filter on`);
103
+ throw new FilterError(`${m.library}/${m.shelf}: '${k}' is a composite with no stored part — it owns no column to filter on`);
105
104
  return { column: `${k}__${sub}`, type: direct.columns[sub] };
106
105
  }
107
- const cut = k.lastIndexOf('__');
108
- if (cut <= 0)
106
+ const owner = resolveColumnKey(fm, k);
107
+ if (!owner)
109
108
  return undefined;
110
- const base = fm[k.slice(0, cut)];
111
- if (!base?.columns)
112
- return undefined;
113
- const part = k.slice(cut + 2);
114
- const type = base.columns[part];
115
- if (type === undefined)
116
- throw new FilterError(`${m.library}/${m.shelf}: '${k}' names no part of composite '${k.slice(0, cut)}' (parts: ${Object.keys(base.columns).join(', ')})`);
117
- return { column: k, type };
109
+ if (owner.kind === 'unknown-sub')
110
+ throw new FilterError(`${m.library}/${m.shelf}: '${k}' names no part of composite '${owner.key}' (parts: ${Object.keys(owner.field.columns).join(', ')})`);
111
+ return { column: k, type: owner.field.columns[owner.sub] };
118
112
  }
119
113
  function buildWhere(m, filterParams) {
120
114
  const where = {};
@@ -179,8 +173,12 @@ async function listRecords(library, shelf, query = {}, opts = {}) {
179
173
  let rows = await selectRows(fork, ename, where, findOpts);
180
174
  if (tokens.length > 0)
181
175
  rows = rows.filter((row) => rowMatch(m, row, tokens) !== null);
182
- if (projected)
176
+ if (projected) {
183
177
  rows = rows.map((row) => pickCols(row, projected));
178
+ }
179
+ else {
180
+ rows = rows.map((row) => stripClockKeys(row));
181
+ }
184
182
  const decoded = rows.map((r) => decodeTemporal(m, r));
185
183
  if (!withExt)
186
184
  return decoded;
@@ -1,4 +1,4 @@
1
- import type { Registry } from '@coffer-org/core/compose';
1
+ import type { Registry } from '@coffer-org/sdk/compose';
2
2
  export declare function setActiveRegistry(reg: Registry): void;
3
3
  export declare function getActiveRegistry(): Registry;
4
4
  export declare const getShelf: Registry['getShelf'];
@@ -1,5 +1,8 @@
1
+ import { buildMutateGraph } from '@coffer-org/sdk/mutate-graph';
1
2
  let active = null;
2
3
  export function setActiveRegistry(reg) {
4
+ if (!reg.mutateGraph)
5
+ reg.mutateGraph = buildMutateGraph(reg);
3
6
  active = reg;
4
7
  }
5
8
  function current() {
@@ -1,5 +1,5 @@
1
1
  import { textSearchKeys, titleKey } from '@coffer-org/sdk/shelf';
2
- import { foldText } from '@coffer-org/core/search';
2
+ import { foldText } from '@coffer-org/sdk/search';
3
3
  import { getLogger } from '@coffer-org/sdk/logger';
4
4
  import { getEm } from "./db.js";
5
5
  import { getDialect } from "./dialect.js";
@@ -1,4 +1,4 @@
1
- export declare const SEARCH_STATE_PLUGIN = "core";
1
+ export declare const SEARCH_STATE_NAMESPACE = "system";
2
2
  export declare const SEARCH_CURSOR_KEY = "fts_last_event_id";
3
3
  export declare function indexSearchOnce(batch?: number): Promise<number>;
4
4
  export declare function startSearchIndexer(batch?: number): () => void;
@@ -5,13 +5,13 @@ import { getShelf } from "./registry-context.js";
5
5
  import { onRecordsChanged } from "./index-signal.js";
6
6
  import { ftsAvailable, foldedTextFor, upsertSearchRow, deleteSearchRow } from "./search-index.js";
7
7
  const log = getLogger('search-indexer');
8
- export const SEARCH_STATE_PLUGIN = 'core';
8
+ export const SEARCH_STATE_NAMESPACE = 'system';
9
9
  export const SEARCH_CURSOR_KEY = 'fts_last_event_id';
10
10
  const DEFAULT_BATCH = 5000;
11
11
  export async function indexSearchOnce(batch = DEFAULT_BATCH) {
12
12
  if (!ftsAvailable())
13
13
  return 0;
14
- const last = Number((await getPluginState(SEARCH_STATE_PLUGIN, SEARCH_CURSOR_KEY)) ?? '0');
14
+ const last = Number((await getPluginState(SEARCH_STATE_NAMESPACE, SEARCH_CURSOR_KEY)) ?? '0');
15
15
  const rows = await listEventsSince(last, batch);
16
16
  if (rows.length === 0)
17
17
  return 0;
@@ -52,7 +52,7 @@ export async function indexSearchOnce(batch = DEFAULT_BATCH) {
52
52
  await upsertSearchRow(p.shelf, p.recordId, p.folded);
53
53
  }
54
54
  const lastId = rows[rows.length - 1].id;
55
- await setPluginState(SEARCH_STATE_PLUGIN, SEARCH_CURSOR_KEY, String(lastId));
55
+ await setPluginState(SEARCH_STATE_NAMESPACE, SEARCH_CURSOR_KEY, String(lastId));
56
56
  log.debug(`indexed ${byRef.size} record(s) from ${rows.length} event(s), cursor → ${lastId}`);
57
57
  return rows.length;
58
58
  }
@@ -0,0 +1,14 @@
1
+ import type { LayoutEl } from '@coffer-org/sdk/fields';
2
+ import type { PluginManifest } from '@coffer-org/sdk/plugin';
3
+ import { SYSTEM_SETTINGS_ID } from './system-settings.ts';
4
+ export interface SettingsGroup {
5
+ id: string;
6
+ label: string;
7
+ fields: LayoutEl[];
8
+ }
9
+ export { SYSTEM_SETTINGS_ID };
10
+ export declare function groupsFromPlugins(plugins: PluginManifest[]): SettingsGroup[];
11
+ export declare function systemSettingsGroup(): SettingsGroup;
12
+ export declare function groupsWithSystem(plugins: PluginManifest[]): SettingsGroup[];
13
+ export declare function settingsGroups(): Promise<SettingsGroup[]>;
14
+ export declare function findSettingsGroup(id: string): Promise<SettingsGroup | undefined>;
@@ -0,0 +1,25 @@
1
+ import { getPlugins } from "./plugin-runtime.js";
2
+ import { SYSTEM_SETTINGS_ID, SYSTEM_SETTINGS } from "./system-settings.js";
3
+ export { SYSTEM_SETTINGS_ID };
4
+ export function groupsFromPlugins(plugins) {
5
+ return plugins
6
+ .filter((p) => p.settings && p.settings.fields.length > 0)
7
+ .map((p) => ({ id: p.id, label: p.settings.label, fields: p.settings.fields }));
8
+ }
9
+ export function systemSettingsGroup() {
10
+ return { id: SYSTEM_SETTINGS_ID, label: SYSTEM_SETTINGS.label, fields: SYSTEM_SETTINGS.fields };
11
+ }
12
+ export function groupsWithSystem(plugins) {
13
+ const fromPlugins = groupsFromPlugins(plugins);
14
+ const clash = fromPlugins.find((g) => g.id === SYSTEM_SETTINGS_ID);
15
+ if (clash) {
16
+ throw new Error(`[settings] a plugin declares settings under the reserved group id '${SYSTEM_SETTINGS_ID}'`);
17
+ }
18
+ return [systemSettingsGroup(), ...fromPlugins];
19
+ }
20
+ export async function settingsGroups() {
21
+ return groupsWithSystem(await getPlugins());
22
+ }
23
+ export async function findSettingsGroup(id) {
24
+ return (await settingsGroups()).find((g) => g.id === id);
25
+ }
@@ -9,10 +9,10 @@ export interface SettingsFieldInfo {
9
9
  options?: string[];
10
10
  }
11
11
  export declare function describeSettingsFields(fields: LayoutEl[]): SettingsFieldInfo[];
12
- export declare function buildSettingsBody(pluginId: string, fields: LayoutEl[], incoming: Record<string, unknown>, existing: Record<string, unknown>): Record<string, unknown>;
13
- export declare function writePluginSettings(em: EntityManager, pluginId: string, incoming: Record<string, unknown>, actor: string, plugins?: PluginManifest[]): Promise<Record<string, unknown>>;
14
- export declare function listSettings(plugins?: PluginManifest[]): Promise<{
15
- plugin: string;
12
+ export declare function buildSettingsBody(groupId: string, fields: LayoutEl[], incoming: Record<string, unknown>, existing: Record<string, unknown>, label?: string): Record<string, unknown>;
13
+ export declare function writePluginSettings(em: EntityManager, groupId: string, incoming: Record<string, unknown>, actor: string, pluginsOverride?: PluginManifest[]): Promise<Record<string, unknown>>;
14
+ export declare function listSettings(pluginsOverride?: PluginManifest[]): Promise<{
15
+ group: string;
16
16
  label: string;
17
17
  fields: SettingsFieldInfo[];
18
18
  values: Record<string, unknown>;
@@ -1,8 +1,9 @@
1
1
  import { fieldEntries, buildZodObject } from '@coffer-org/sdk/shelf';
2
2
  import { preserveTree, maskTree } from "./field-masking.js";
3
3
  import { ValidationError, NotFoundError, toIssue } from "./mutate.js";
4
- import { getPlugins, getPluginSettings } from "./plugin-runtime.js";
4
+ import { getPluginSettings } from "./plugin-runtime.js";
5
5
  import { invalidatePublicUrlCache } from "./public-url.js";
6
+ import { groupsFromPlugins, settingsGroups, findSettingsGroup, SYSTEM_SETTINGS_ID } from "./settings-groups.js";
6
7
  export function describeSettingsFields(fields) {
7
8
  return fieldEntries(fields).map(([key, f]) => ({
8
9
  key,
@@ -12,38 +13,35 @@ export function describeSettingsFields(fields) {
12
13
  ...(f.options ? { options: f.options.map((o) => o.value) } : {}),
13
14
  }));
14
15
  }
15
- function settingsShelf(pluginId, fields) {
16
- return { library: '_settings', shelf: pluginId, label: `${pluginId}.plugin.label`, fields };
16
+ function settingsShelf(groupId, fields, label = `${groupId}.plugin.label`) {
17
+ return { library: '_settings', shelf: groupId, label, fields };
17
18
  }
18
- export function buildSettingsBody(pluginId, fields, incoming, existing) {
19
+ export function buildSettingsBody(groupId, fields, incoming, existing, label) {
19
20
  const body = preserveTree(fields, incoming, existing);
20
- const parsed = buildZodObject(settingsShelf(pluginId, fields)).safeParse(body);
21
+ const parsed = buildZodObject(settingsShelf(groupId, fields, label)).safeParse(body);
21
22
  if (!parsed.success)
22
23
  throw new ValidationError(parsed.error.issues.map(toIssue));
23
24
  return parsed.data;
24
25
  }
25
- function withSettings(plugins) {
26
- return plugins.filter((p) => p.settings && p.settings.fields.length > 0);
27
- }
28
- export async function writePluginSettings(em, pluginId, incoming, actor, plugins) {
29
- const all = plugins ?? (await getPlugins());
30
- const p = all.find((x) => x.id === pluginId);
31
- if (!p || !p.settings || p.settings.fields.length === 0) {
26
+ export async function writePluginSettings(em, groupId, incoming, actor, pluginsOverride) {
27
+ const group = pluginsOverride
28
+ ? groupsFromPlugins(pluginsOverride).find((g) => g.id === groupId)
29
+ : await findSettingsGroup(groupId);
30
+ if (!group)
32
31
  throw new NotFoundError();
33
- }
34
- const fields = p.settings.fields;
35
- const existing = await getPluginSettings(pluginId);
36
- const data = buildSettingsBody(pluginId, fields, incoming, existing);
37
- const row = { plugin_id: pluginId, ...data };
38
- await em.upsert(`_settings__${pluginId}`, row);
39
- if (pluginId === 'core')
32
+ const fields = group.fields;
33
+ const existing = await getPluginSettings(groupId);
34
+ const data = buildSettingsBody(groupId, fields, incoming, existing, group.label);
35
+ const row = { plugin_id: groupId, ...data };
36
+ await em.upsert(`_settings__${groupId}`, row);
37
+ if (groupId === SYSTEM_SETTINGS_ID)
40
38
  invalidatePublicUrlCache();
41
39
  const masked = maskTree(fields, row);
42
40
  em.create('_Event', {
43
41
  ts: new Date().toISOString(),
44
42
  actor,
45
43
  op: 'settings',
46
- type: pluginId,
44
+ type: groupId,
47
45
  record_id: null,
48
46
  before: JSON.stringify(maskTree(fields, existing)),
49
47
  after: JSON.stringify(masked),
@@ -51,13 +49,12 @@ export async function writePluginSettings(em, pluginId, incoming, actor, plugins
51
49
  await em.flush();
52
50
  return masked;
53
51
  }
54
- export async function listSettings(plugins) {
55
- const all = withSettings(plugins ?? (await getPlugins()));
52
+ export async function listSettings(pluginsOverride) {
53
+ const groups = pluginsOverride ? groupsFromPlugins(pluginsOverride) : await settingsGroups();
56
54
  const out = [];
57
- for (const p of all) {
58
- const fields = p.settings.fields;
59
- const values = maskTree(fields, await getPluginSettings(p.id));
60
- out.push({ plugin: p.id, label: p.settings.label, fields: describeSettingsFields(fields), values });
55
+ for (const g of groups) {
56
+ const values = maskTree(g.fields, await getPluginSettings(g.id));
57
+ out.push({ group: g.id, label: g.label, fields: describeSettingsFields(g.fields), values });
61
58
  }
62
59
  return out;
63
60
  }
@@ -0,0 +1,3 @@
1
+ import { type SettingsDef } from '@coffer-org/sdk/settings';
2
+ export declare const SYSTEM_SETTINGS_ID = "system";
3
+ export declare const SYSTEM_SETTINGS: SettingsDef;
@@ -0,0 +1,27 @@
1
+ import { field } from '@coffer-org/sdk/fields';
2
+ import { defineSettings } from '@coffer-org/sdk/settings';
3
+ export const SYSTEM_SETTINGS_ID = 'system';
4
+ export const SYSTEM_SETTINGS = defineSettings({
5
+ label: 'core.settings.label',
6
+ fields: {
7
+ publicUrl: field.string({
8
+ label: 'core.settings.publicUrl',
9
+ rules: { pattern: '^https?://[^/\\s]+/?$', messageKey: 'core.settings.publicUrlHint' },
10
+ }),
11
+ language: field.string({
12
+ label: 'core.settings.language',
13
+ default: 'uk',
14
+ strict: true,
15
+ view: { noSearch: true },
16
+ options: [
17
+ { value: 'uk', title: 'Українська' },
18
+ { value: 'ru', title: 'Русский' },
19
+ { value: 'en', title: 'English' },
20
+ ],
21
+ }),
22
+ agent_id: field.string({ label: 'core.settings.agent_id', strict: true, view: { noSearch: true } }),
23
+ access_password: field.password({ label: 'core.settings.access_password' }),
24
+ trigger_prefix: field.string({ label: 'core.settings.trigger_prefix' }),
25
+ reply_window: field.int({ label: 'core.settings.reply_window', default: 1800 }),
26
+ },
27
+ });
@@ -1,6 +1,7 @@
1
+ export declare const SIDECAR_ROLES: readonly ["reasoning", "suggestions"];
1
2
  export interface StoredMsg {
2
3
  msgId: string;
3
- role: 'user' | 'assistant' | 'reasoning';
4
+ role: 'user' | 'assistant' | 'reasoning' | 'suggestions';
4
5
  sender: string | null;
5
6
  text: string;
6
7
  attachments?: StoredAttachment[];
@@ -24,7 +25,7 @@ export declare function putThreadMessage(m: {
24
25
  connector: string;
25
26
  chatId: string;
26
27
  msgId: string;
27
- role: 'user' | 'assistant' | 'reasoning';
28
+ role: 'user' | 'assistant' | 'reasoning' | 'suggestions';
28
29
  sender?: string | null;
29
30
  attachments?: StoredAttachment[];
30
31
  text: string;
@@ -1,4 +1,5 @@
1
1
  import { getEm } from "./db.js";
2
+ export const SIDECAR_ROLES = ['reasoning', 'suggestions'];
2
3
  function toStored(r) {
3
4
  let attachments;
4
5
  if (r.attachments) {
@@ -49,12 +50,12 @@ export async function pruneThreadMessages(connector, cutoffTs) {
49
50
  }
50
51
  export async function listThreadMessages(connector, chatId, limit = 200) {
51
52
  const em = getEm().fork();
52
- const visible = (await em.find('_ThreadMessage', { connector, chat_id: chatId, role: { $ne: 'reasoning' } }, { orderBy: { ts: 'desc', msg_id: 'desc' }, limit }));
53
+ const visible = (await em.find('_ThreadMessage', { connector, chat_id: chatId, role: { $nin: SIDECAR_ROLES } }, { orderBy: { ts: 'desc', msg_id: 'desc' }, limit }));
53
54
  const oldest = visible.at(-1)?.ts;
54
- const reasoning = oldest === undefined
55
+ const sidecars = oldest === undefined
55
56
  ? []
56
- : (await em.find('_ThreadMessage', { connector, chat_id: chatId, role: 'reasoning', ts: { $gte: oldest - 1 } }, { orderBy: { ts: 'desc', msg_id: 'desc' } }));
57
- return [...visible, ...reasoning]
57
+ : (await em.find('_ThreadMessage', { connector, chat_id: chatId, role: { $in: SIDECAR_ROLES }, ts: { $gte: oldest - 1 } }, { orderBy: { ts: 'desc', msg_id: 'desc' } }));
58
+ return [...visible, ...sidecars]
58
59
  .sort((a, b) => b.ts - a.ts || (a.msg_id < b.msg_id ? 1 : a.msg_id > b.msg_id ? -1 : 0))
59
60
  .reverse()
60
61
  .map(toStored);
@@ -69,7 +70,7 @@ export async function listThreadChats(connector, chatIdPrefix) {
69
70
  const rows = (await em.find('_ThreadMessage', { connector, chat_id: { $gte: chatIdPrefix, $lt: upper } }, { orderBy: { ts: 'asc' } }));
70
71
  const byChat = new Map();
71
72
  for (const r of rows) {
72
- if (r.role === 'reasoning')
73
+ if (SIDECAR_ROLES.includes(r.role))
73
74
  continue;
74
75
  const cur = byChat.get(r.chat_id);
75
76
  if (!cur) {
@@ -0,0 +1,33 @@
1
+ import { type LayoutEl } from '@coffer-org/sdk/fields';
2
+ import type { MutateTarget } from '@coffer-org/sdk/mutate-path';
3
+ import { type MutateGraph } from '@coffer-org/sdk/mutate-graph';
4
+ import { type Row } from './compute-unit.ts';
5
+ import { type PartCtx } from './part-injection.ts';
6
+ export declare class TriggerQueueOverflowError extends Error {
7
+ }
8
+ export declare class TriggerRecordCapError extends Error {
9
+ }
10
+ export interface TouchedRecord {
11
+ library: string;
12
+ shelf: string;
13
+ id: number;
14
+ changed: string[];
15
+ }
16
+ interface CrossRecordState {
17
+ visited: Set<string>;
18
+ cap: number;
19
+ touched?: TouchedRecord[];
20
+ }
21
+ export interface ChangedField {
22
+ path: string[];
23
+ }
24
+ export declare function changedFields(before: Row, after: Row, fields: LayoutEl[]): ChangedField[];
25
+ export declare function allUnitPaths(fields: LayoutEl[], scope?: string[]): string[][];
26
+ export declare function runTriggers(fields: LayoutEl[], row: Row, changed: ChangedField[], graph: MutateGraph, shelfKey: string, ctx: PartCtx, opts?: {
27
+ seedAll?: boolean;
28
+ seedTargets?: MutateTarget[];
29
+ crossRecordState?: CrossRecordState;
30
+ recordCap?: number;
31
+ touched?: TouchedRecord[];
32
+ }): Promise<Row>;
33
+ export {};