mindoodb-app-sdk 0.0.65 → 0.0.67
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/README.md +124 -0
- package/TESTING.md +39 -1
- package/dist/{chunk-BE7LLFFE.js → chunk-376SABPG.js} +208 -2
- package/dist/index.d.ts +89 -3
- package/dist/index.js +89 -1
- package/dist/testing/index.d.ts +111 -2
- package/dist/testing/index.js +937 -192
- package/dist/{types-BeRUxGKN.d.ts → types-BGLXh4JQ.d.ts} +164 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -327,10 +327,79 @@ if (session.agent) {
|
|
|
327
327
|
- **`annotations`**: `readOnlyHint` for reads; `consequentialHint` for anything the user should confirm (sending, sharing, deleting) — Haven shows its own confirmation dialog and a refusal reaches the agent as `NOT_ALLOWED`; `untrustedContentHint` when the result contains text other people wrote.
|
|
328
328
|
- **Errors**: throw `MindooDBAppAgentToolError(code, message, requiredAction?)` with `NOT_FOUND`, `INVALID_INPUT`, `INVALID_STATE`, `STATE_CHANGED`, `NOT_ALLOWED` or `FAILED`; `requiredAction` tells the agent what to do next ("call todo_tasks_search"). Any other exception becomes `FAILED`. Haven passes the agent one JSON error.
|
|
329
329
|
- **Results** must be JSON. Return what changed (ids, new values), not just `ok`.
|
|
330
|
+
- **`scope`**: `"app"` (default) for the app's own operations ("list documents", "create a map"), `"document"` for operations on the document it shows ("write this paragraph", "set these cells"). See *Agent tools of embedded components* below.
|
|
330
331
|
- **`setContext(context)`** shows the app's state to agents in `haven_get_context` under the focused app.
|
|
331
332
|
- **Files** never travel through the agent. `agent.provideFile(blob, { name, mimeType })` hands Haven a file the app produced and returns a `fileRef` (valid ten minutes) for the agent to pass to `haven_files_export`, which saves it into the user's exchange folder. `agent.takeFile(fileRef)` returns a `File` the agent imported for this app with `haven_files_import`.
|
|
332
333
|
- A call that runs longer than two minutes fails with `FAILED`; files are limited to 50 MB.
|
|
333
334
|
|
|
335
|
+
#### An op guide for edit tools
|
|
336
|
+
|
|
337
|
+
Apps with an edit language (`apply_ops` batches, formatting changes) do better with short tool descriptions plus a reference the agent loads when it needs it. `createAgentGuideTool` builds that reference as a read-only tool (`ops_guide` by default): without arguments it returns the group list and one signature line per op; with `groups` it returns field tables, units, JSON examples and common mistakes of those groups.
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
import { agentGuideExamples, createAgentGuideTool, type MindooDBAppAgentGuideGroup } from "mindoodb-app-sdk";
|
|
341
|
+
|
|
342
|
+
const GUIDE: MindooDBAppAgentGuideGroup[] = [
|
|
343
|
+
{
|
|
344
|
+
name: "blocks",
|
|
345
|
+
summary: "insert, replace, move and delete blocks",
|
|
346
|
+
intro: "Block indexes come from todo_document_read and refer to the document before the call.",
|
|
347
|
+
ops: [
|
|
348
|
+
{
|
|
349
|
+
name: "insertContent",
|
|
350
|
+
signature: "{after, markdown}",
|
|
351
|
+
fields: [
|
|
352
|
+
{ name: "after", type: "integer | {heading}", required: true, description: "-1 = document start" },
|
|
353
|
+
{ name: "markdown", type: "string", required: true, description: "One or more blocks." },
|
|
354
|
+
],
|
|
355
|
+
examples: [{ op: "insertContent", after: -1, markdown: "# Agenda" }],
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
mistakes: ["Sending indexes from an old read: pass the revision, read again when refused."],
|
|
359
|
+
},
|
|
360
|
+
];
|
|
361
|
+
|
|
362
|
+
const tools = [
|
|
363
|
+
createAgentGuideTool({ documents: "todo_apply_ops", groups: GUIDE }),
|
|
364
|
+
// apply_ops' description: "... Signatures and examples: todo_ops_guide."
|
|
365
|
+
];
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
A group can also bring its reference as ready Markdown in `content` (TeamSlides passes GenOffice's op docs). Test the examples: `agentGuideExamples(GUIDE)` lists them, so a unit test can dry-run each one through the edit tool.
|
|
369
|
+
|
|
370
|
+
#### Agent tools of embedded components
|
|
371
|
+
|
|
372
|
+
Agents work on three levels: Haven's own tools (`haven_*`), the tools of the app the
|
|
373
|
+
user has open, and the tools of the components that app embeds. A host such as Mindoo
|
|
374
|
+
CRM offers the *workbench*: finding, creating, filing, opening and closing documents.
|
|
375
|
+
The embedded editors offer the *content* of the one document each shows.
|
|
376
|
+
|
|
377
|
+
- **Embedded, an app offers only its `scope: "document"` tools.** It sees just its
|
|
378
|
+
sandbox, so "list documents" or "create a document" would mislead; the host has its
|
|
379
|
+
own tools for those. Register the same tool list either way; Haven drops the rest.
|
|
380
|
+
- **One component per app is active.** When a host shows two documents of the same
|
|
381
|
+
app (two text documents side by side), the app's tool names belong to the active one:
|
|
382
|
+
the newest, until the host calls `embed.setActive()`, e.g. when a tab comes to the
|
|
383
|
+
front. Components of different apps (a spreadsheet next to a text) all keep theirs.
|
|
384
|
+
Haven prefers the launch in the focused app, so a component in the CRM wins over the
|
|
385
|
+
same app running in a background tab.
|
|
386
|
+
- **The host learns about its components' tools**: `embed.agentTools` is
|
|
387
|
+
`{ prefix, names, active, enabled }` or `null`, `embed.onAgentToolsChange` follows it,
|
|
388
|
+
and `embed.waitForAgentTools(timeoutMs)` resolves once the component offers tools.
|
|
389
|
+
A host's "open document" tool can wait for it and tell the agent which tools to use
|
|
390
|
+
next:
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
const embed = await session.embeds.open({ componentKey, databaseId, docId, container });
|
|
394
|
+
await embed.setActive();
|
|
395
|
+
const tools = await embed.waitForAgentTools(5000);
|
|
396
|
+
return { docKey, editorTools: tools ? { prefix: tools.prefix, names: tools.names } : null };
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
In tests, `host.setEmbedAgentTools(embedId, ["text_insert"])` acts as if the component
|
|
400
|
+
registered tools, and an app under test launched with `launchContext.embed` offers only
|
|
401
|
+
its document tools, as in Haven.
|
|
402
|
+
|
|
334
403
|
`session.agent` is absent on hosts without agent tools, so check for it. Test tools by calling `execute` directly in unit tests; the reference implementation is `src/features/agent/` in [mindoodb-app-vega](https://github.com/klehmann/mindoodb-app-vega). How to design tools that agents use well is in the [best practices guide](https://github.com/klehmann/MindooDB/blob/main/docs/best-practices.md) (section "Offer operations to AI agents").
|
|
335
404
|
|
|
336
405
|
### Host-owned drag
|
|
@@ -426,6 +495,39 @@ await session.embedding.complete({ title }); // or session.embedding.cancel()
|
|
|
426
495
|
Haven writes every change back to the host's database as it happens and only lets the
|
|
427
496
|
component create documents that are children of the root.
|
|
428
497
|
|
|
498
|
+
**Kinds, contracts and references.** Three optional fields let hosts pick components by
|
|
499
|
+
what they do instead of by app:
|
|
500
|
+
|
|
501
|
+
```json
|
|
502
|
+
{
|
|
503
|
+
"id": "person",
|
|
504
|
+
"label": { "en": "Person", "de": "Person" },
|
|
505
|
+
"intents": ["create", "edit", "view"],
|
|
506
|
+
"match": { "form": "contactperson" },
|
|
507
|
+
"category": "form",
|
|
508
|
+
"provides": [{ "contract": "mindoo.contacts.person@1", "fields": { "parents": "companyIds" } }],
|
|
509
|
+
"references": [
|
|
510
|
+
{ "field": "companyIds", "to": "mindoo.contacts.company@1", "match": { "form": "contactcompany" }, "show": ["subject"] }
|
|
511
|
+
]
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
- `category`: `editor` (content: a document, a spreadsheet, a map) or `form` (a record: a
|
|
516
|
+
company, a person); hosts may treat a component without one as an `editor`. A CRM
|
|
517
|
+
offers its editors under "New" and uses its forms elsewhere.
|
|
518
|
+
- `provides`: contracts the root documents fulfil (`<name>@<major version>`), so a host
|
|
519
|
+
can rely on their shape; `fields` maps contract fields to this app's field names.
|
|
520
|
+
Another app offering the same contract can take this one's place.
|
|
521
|
+
- `references`: fields that point at other root documents (not parts, unlike
|
|
522
|
+
`children`), e.g. a person's company. Embedded, the component cannot see the host's
|
|
523
|
+
database, so it asks Haven:
|
|
524
|
+
|
|
525
|
+
```ts
|
|
526
|
+
const companies = await session.embedding.lookup("companyIds", { query: "Mül" });
|
|
527
|
+
// [{ docId, fields: { subject } }]: only documents matching the reference's `match`,
|
|
528
|
+
// only its `show` fields. `docIds: [...]` fetches given ones (to show the current value).
|
|
529
|
+
```
|
|
530
|
+
|
|
429
531
|
**Embedding a component** (host app):
|
|
430
532
|
|
|
431
533
|
```ts
|
|
@@ -447,6 +549,25 @@ hands its connection to Haven, so the host cannot reach into the component or it
|
|
|
447
549
|
channel. Haven tells the SDK where the container is (for the component's own menus);
|
|
448
550
|
the SDK follows the container's size and position by itself.
|
|
449
551
|
|
|
552
|
+
**Asking once.** The first time a host shows a component in one of its databases,
|
|
553
|
+
Haven asks the user whether that app may see the document. A host that shows several
|
|
554
|
+
components asks for all of them at once instead, e.g. right after `components.list`:
|
|
555
|
+
|
|
556
|
+
```ts
|
|
557
|
+
const { granted, pending } = await session.embeds.requestAccess({
|
|
558
|
+
components: [
|
|
559
|
+
{ componentKey: sheet.key, databaseIds: ["crm"] },
|
|
560
|
+
{ componentKey: person.key, databaseIds: ["contacts"] }, // default: all its databases
|
|
561
|
+
],
|
|
562
|
+
});
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
Haven lists what is not allowed yet in one dialog; the user may untick entries. What the
|
|
566
|
+
user allowed or put off before is not asked again, so call it on every start. `open`
|
|
567
|
+
still asks for a pending pair when it is used, and asks again when a newer version of
|
|
568
|
+
the component wants more access (e.g. new `references`). In tests, `embedAccess` on the
|
|
569
|
+
mock plays the user (default: allow all).
|
|
570
|
+
|
|
450
571
|
**Unsaved changes.** A component reports whether it has changes it has not saved yet,
|
|
451
572
|
and saves when its host asks:
|
|
452
573
|
|
|
@@ -485,6 +606,9 @@ answers like a current Haven, so the SDK frames that URL in the container. Both
|
|
|
485
606
|
controllers simulate unsaved changes: `setEmbedDirty`, `failNextEmbedSave`, and for an
|
|
486
607
|
app under test that is a component, `getEmbeddingDirty` and `requestEmbeddingSave`.
|
|
487
608
|
|
|
609
|
+
A host with agent tools marks the component in the front tab with `embed.setActive()`
|
|
610
|
+
and reads `embed.agentTools`; see *Agent tools of embedded components*.
|
|
611
|
+
|
|
488
612
|
### Databases and capabilities
|
|
489
613
|
|
|
490
614
|
Each database mapped to your app carries a set of **capabilities** that Haven controls. Your app should check capabilities before attempting operations and adapt its UI accordingly.
|
package/TESTING.md
CHANGED
|
@@ -104,11 +104,12 @@ Each database entry can provide:
|
|
|
104
104
|
- `info`
|
|
105
105
|
- `documents` — seed documents into the mock store (used by `list`/`query` and by evaluating VirtualViews). A seed's `decryptionKeyId` is reported back by `documents.get`, like a real host; documents created through the mock report `"default"` unless `create` named a key, and person-encrypted documents (`recipients`) report none.
|
|
106
106
|
- `methods.documents`
|
|
107
|
+
- Document history: every write (create, update, delete, a remote update) is kept as a revision, so `listHistory` (newest first, `isCurrent` on the latest), `getAtRevision` (with `phase: "before"`) and `getAtTimestamp` return the states the document went through. Revision ids are `<docId>@<n>`; timestamps are the write's `updatedAt`, strictly increasing per document.
|
|
107
108
|
- `methods.views` for session-level `createView()` and `openView()` calls (overrides the default evaluating navigator)
|
|
108
109
|
- `methods.attachments`
|
|
109
110
|
- `fulltextSetup` — the initial config returned by `db.getFulltextSetup()`; `db.setFulltextSetup()` overwrites it for the lifetime of the handle. Use this to test your app's full-text bootstrap logic. Note the mock evaluates `text` query clauses regardless of this config.
|
|
110
111
|
- `summarySetup` — the initial config returned by `db.getSummarySetup()`; `db.setSummarySetup()` overwrites it for the lifetime of the handle. The mock's `query()` reads seeded documents directly and ignores this config; use it to test your app's summary bootstrap logic.
|
|
111
|
-
- `automerge: true` — back every document with a real Automerge document, as Haven does (off by default: plain JSON with `mock-head-N` heads). `create`/`update` then return real Automerge heads, `update` applies `set`/`unset`, `json` (with `Automerge.changeAt(json.baseHeads)` when given) and `text` patches as Automerge changes with Haven's rules (missing parents and lists are created, `unset` is idempotent, `counterIncrement` needs a counter, an empty `json` patch is rejected), and `get`/`list`/`query` return the plain projection (atomic strings as strings, counters as numbers, timestamps as ISO strings). `richText`
|
|
112
|
+
- `automerge: true` — back every document with a real Automerge document, as Haven does (off by default: plain JSON with `mock-head-N` heads). `create`/`update` then return real Automerge heads, `update` applies `set`/`unset`, `json` (with `Automerge.changeAt(json.baseHeads)` when given) and `text` patches as Automerge changes with Haven's rules (missing parents and lists are created, `unset` is idempotent, `counterIncrement` needs a counter, an empty `json` patch is rejected), and `get`/`list`/`query` return the plain projection (atomic strings as strings, counters as numbers, timestamps as ISO strings). `richText` patches run Automerge's `updateSpans` at their `baseHeads` and `richTextSteps` splice and mark at theirs, both validated and with MindooDB's recovery, and `getRichText` returns `Automerge.spans` — so values Automerge rejects (an `immutableString` as a mark value) fail here as they do in Haven. A `richText` snapshot written at older `baseHeads` is applied on a fork at those heads and merged back, as MindooDB does (Automerge 3.5's `updateSpans` inside `changeAt` indexes the current text), so a concurrent edit of the same text survives. As in MindooDB's `applyDocumentUpdate`, all parts of one `update` whose `baseHeads` agree are applied as one Automerge change (one history entry), all or nothing. Seeds are built with a fixed actor, so two mocks from the same seed have identical heads. `mockDatabasesFromDefinition(definition, seed, { automerge: true })` and the test host URL setting `automerge=1` turn it on, too.
|
|
112
113
|
|
|
113
114
|
#### Concurrent edits from another device
|
|
114
115
|
|
|
@@ -179,6 +180,24 @@ const page = await navigator.entriesForward({ limit: 100 });
|
|
|
179
180
|
Without `mindoodb`, the testing entrypoint falls back to an empty navigator and
|
|
180
181
|
logs a warning — install the peer for realistic view tests.
|
|
181
182
|
|
|
183
|
+
Views configured for the app in Haven arrive in `launchContext.views`. Pass
|
|
184
|
+
them there and `session.openViewNavigator(viewId, options)` evaluates them
|
|
185
|
+
over their source databases' seeded documents, compiled the way Haven does
|
|
186
|
+
(rule or formula filter, field or formula columns). Each call opens a
|
|
187
|
+
navigator of its own with its options applied (`rootCategoryPath`,
|
|
188
|
+
`includeCategories`, `includeDocuments`, …), as in Haven; disposing one does
|
|
189
|
+
not affect the others. A view created with `createViewNavigator` can be
|
|
190
|
+
opened again by its definition id the same way.
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
const mock = createMockMindooDBAppBridge({
|
|
194
|
+
launchContext: { views: [contactsByCompany] }, // a MindooDBAppResolvedViewDefinition
|
|
195
|
+
databases: [{ info: { id: "main", title: "Main", capabilities: ["read", "views"] }, documents: contacts }],
|
|
196
|
+
});
|
|
197
|
+
const session = await mock.bridge.connect();
|
|
198
|
+
const acme = await session.openViewNavigator("contacts_by_company", { rootCategoryPath: ["ACME"] });
|
|
199
|
+
```
|
|
200
|
+
|
|
182
201
|
The default in-memory document store also implements `documents.query()` and `documents.liveQuery()`: expression and formula-string filters are evaluated against the seeded documents, `sortBy`/`limit`/`offset` work as documented, and live query callbacks fire automatically after `create`/`update`/`delete`/`undelete` mutations. Full-text `text` clauses are supported with a deterministic mock implementation (token matching with prefix/AND semantics and an occurrence-count `textScore`) — assert on membership and relative ordering, not absolute scores, since real hosts use a BM25-style engine. That means app code built on queries and live queries is testable at Level 1 without any extra setup:
|
|
183
202
|
|
|
184
203
|
```ts
|
|
@@ -422,6 +441,25 @@ Lower-level pieces, if you build your own page: `createBrowserTestHost({ frame,
|
|
|
422
441
|
|
|
423
442
|
Keep the test page out of production builds. The starter template builds it only for `vite dev` and when `HAVEN_TEST_HOST=1` is set (for preview deployments), so the app's public URL keeps showing its landing page.
|
|
424
443
|
|
|
444
|
+
### Two users on two replicas
|
|
445
|
+
|
|
446
|
+
The panel's **Mode** section switches between *Single user* and *Two users* (keeping the other URL settings). `/__haven-test/?twoUsers=1` (or `mountHavenTestHost({ twoUsers: true })`) shows the app twice, side by side, as two people on two devices: each frame is launched by its own mock Haven with its own replica of every database (`automerge: true`, distinct Automerge actors), so their edits really are concurrent. The bar on top:
|
|
447
|
+
|
|
448
|
+
- **Auto-sync every 2 s** (on by default) merges the replicas with `syncMockReplicas`, like Haven's sync. Turn it off to keep the two apart — two devices offline — edit on both sides, then press **Sync** and watch the merge.
|
|
449
|
+
- The status says "✓ in sync" or how many documents still differ.
|
|
450
|
+
|
|
451
|
+
Each app sees the other's changes the way it would in Haven (changefeed, live queries, or its own polling). Use it to try "one changes the text, the other the color", "both add a slide", "both format the same word" by hand. Playwright reaches both frames as `[data-testid="haven-two-users-frame-1"]` / `-2`, and `window.__havenTwoUsers` scripts the mode (`sync()`, `setAutoSync(on)`, `differences()`, `hosts`).
|
|
452
|
+
|
|
453
|
+
In Vitest, two mocks with distinct `automergeActor`s plus `syncMockReplicas(a, b)` do the same without a browser:
|
|
454
|
+
|
|
455
|
+
```ts
|
|
456
|
+
const a = createMockMindooDBAppSession({ databases: [def()], automergeActor: "a1".repeat(16) });
|
|
457
|
+
const b = createMockMindooDBAppSession({ databases: [def()], automergeActor: "b2".repeat(16) });
|
|
458
|
+
// … edit through each session, then:
|
|
459
|
+
await syncMockReplicas(a, b); // documents, deletions and attachments, both ways
|
|
460
|
+
expect(mockReplicaDifferences(a, b)).toBe(0);
|
|
461
|
+
```
|
|
462
|
+
|
|
425
463
|
## When to use which level
|
|
426
464
|
|
|
427
465
|
Choose Level 1 when:
|
|
@@ -363,6 +363,89 @@ function readComponentFieldMatch(value, label, errors) {
|
|
|
363
363
|
}
|
|
364
364
|
return result;
|
|
365
365
|
}
|
|
366
|
+
var COMPONENT_CATEGORY_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
|
|
367
|
+
var MINDOODB_APP_COMPONENT_CONTRACT_PATTERN = /^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+@[1-9][0-9]{0,3}$/;
|
|
368
|
+
var COMPONENT_CONTRACTS_MAX = 8;
|
|
369
|
+
var COMPONENT_REFERENCES_MAX = 8;
|
|
370
|
+
var COMPONENT_REFERENCE_SHOW_MAX = 8;
|
|
371
|
+
function readComponentContracts(value, label, errors) {
|
|
372
|
+
if (value === void 0) {
|
|
373
|
+
return void 0;
|
|
374
|
+
}
|
|
375
|
+
if (!Array.isArray(value) || value.length > COMPONENT_CONTRACTS_MAX) {
|
|
376
|
+
errors.push(`${label} must be an array of at most ${COMPONENT_CONTRACTS_MAX} contracts.`);
|
|
377
|
+
return void 0;
|
|
378
|
+
}
|
|
379
|
+
const result = [];
|
|
380
|
+
for (const [index, entry] of value.entries()) {
|
|
381
|
+
const contract = isPlainObject(entry) && typeof entry.contract === "string" ? entry.contract.trim() : "";
|
|
382
|
+
if (!MINDOODB_APP_COMPONENT_CONTRACT_PATTERN.test(contract)) {
|
|
383
|
+
errors.push(`${label}[${index}].contract must look like "mindoo.contacts.company@1".`);
|
|
384
|
+
return void 0;
|
|
385
|
+
}
|
|
386
|
+
let fields;
|
|
387
|
+
if (isPlainObject(entry) && entry.fields !== void 0) {
|
|
388
|
+
if (!isPlainObject(entry.fields)) {
|
|
389
|
+
errors.push(`${label}[${index}].fields must map contract fields to document fields.`);
|
|
390
|
+
return void 0;
|
|
391
|
+
}
|
|
392
|
+
fields = {};
|
|
393
|
+
for (const [from, to] of Object.entries(entry.fields)) {
|
|
394
|
+
if (!COMPONENT_FIELD_PATTERN.test(from) || typeof to !== "string" || !COMPONENT_FIELD_PATTERN.test(to)) {
|
|
395
|
+
errors.push(`${label}[${index}].fields must map field names to top-level field names.`);
|
|
396
|
+
return void 0;
|
|
397
|
+
}
|
|
398
|
+
fields[from] = to;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
result.push({ contract, ...fields && Object.keys(fields).length ? { fields } : {} });
|
|
402
|
+
}
|
|
403
|
+
return result.length ? result : void 0;
|
|
404
|
+
}
|
|
405
|
+
function readComponentReferences(value, label, rootMatch, errors) {
|
|
406
|
+
if (value === void 0) {
|
|
407
|
+
return void 0;
|
|
408
|
+
}
|
|
409
|
+
if (!Array.isArray(value) || value.length > COMPONENT_REFERENCES_MAX) {
|
|
410
|
+
errors.push(`${label} must be an array of at most ${COMPONENT_REFERENCES_MAX} references.`);
|
|
411
|
+
return void 0;
|
|
412
|
+
}
|
|
413
|
+
const result = [];
|
|
414
|
+
const fieldsSeen = /* @__PURE__ */ new Set();
|
|
415
|
+
for (const [index, entry] of value.entries()) {
|
|
416
|
+
const at = `${label}[${index}]`;
|
|
417
|
+
if (!isPlainObject(entry)) {
|
|
418
|
+
errors.push(`${at} must be an object.`);
|
|
419
|
+
return void 0;
|
|
420
|
+
}
|
|
421
|
+
const field = typeof entry.field === "string" ? entry.field.trim() : "";
|
|
422
|
+
if (!COMPONENT_FIELD_PATTERN.test(field) || field in rootMatch || fieldsSeen.has(field)) {
|
|
423
|
+
errors.push(`${at}.field must be a top-level field name, not a match field and not used twice.`);
|
|
424
|
+
return void 0;
|
|
425
|
+
}
|
|
426
|
+
fieldsSeen.add(field);
|
|
427
|
+
const to = typeof entry.to === "string" ? entry.to.trim() : void 0;
|
|
428
|
+
if (entry.to !== void 0 && !MINDOODB_APP_COMPONENT_CONTRACT_PATTERN.test(to ?? "")) {
|
|
429
|
+
errors.push(`${at}.to must look like "mindoo.contacts.company@1".`);
|
|
430
|
+
return void 0;
|
|
431
|
+
}
|
|
432
|
+
const match = readComponentFieldMatch(entry.match, `${at}.match`, errors);
|
|
433
|
+
if (!match) {
|
|
434
|
+
return void 0;
|
|
435
|
+
}
|
|
436
|
+
if (!Object.keys(match).length) {
|
|
437
|
+
errors.push(`${at}.match must name at least one field, or every document could be referenced.`);
|
|
438
|
+
return void 0;
|
|
439
|
+
}
|
|
440
|
+
const show = Array.isArray(entry.show) ? entry.show.filter((name) => typeof name === "string" && COMPONENT_FIELD_PATTERN.test(name)) : [];
|
|
441
|
+
if (!show.length || show.length !== entry.show.length || show.length > COMPONENT_REFERENCE_SHOW_MAX) {
|
|
442
|
+
errors.push(`${at}.show must list 1-${COMPONENT_REFERENCE_SHOW_MAX} top-level field names.`);
|
|
443
|
+
return void 0;
|
|
444
|
+
}
|
|
445
|
+
result.push({ field, ...to ? { to } : {}, match, show: [...new Set(show)] });
|
|
446
|
+
}
|
|
447
|
+
return result.length ? result : void 0;
|
|
448
|
+
}
|
|
366
449
|
function readComponents(value, errors) {
|
|
367
450
|
if (value === void 0 || value === null) {
|
|
368
451
|
return void 0;
|
|
@@ -475,6 +558,16 @@ function readComponents(value, errors) {
|
|
|
475
558
|
}
|
|
476
559
|
children = { linkField, ...childMatch && Object.keys(childMatch).length ? { match: childMatch } : {} };
|
|
477
560
|
}
|
|
561
|
+
let category;
|
|
562
|
+
if (entry.category !== void 0) {
|
|
563
|
+
category = typeof entry.category === "string" ? entry.category.trim() : "";
|
|
564
|
+
if (!COMPONENT_CATEGORY_PATTERN.test(category)) {
|
|
565
|
+
errors.push(`${label}.category must be 1-32 lowercase letters, digits or "-", e.g. "editor" or "form".`);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const provides = readComponentContracts(entry.provides, `${label}.provides`, errors);
|
|
570
|
+
const references = readComponentReferences(entry.references, `${label}.references`, match, errors);
|
|
478
571
|
if (errors.length !== errorCount) {
|
|
479
572
|
return;
|
|
480
573
|
}
|
|
@@ -486,7 +579,10 @@ function readComponents(value, errors) {
|
|
|
486
579
|
intents,
|
|
487
580
|
match,
|
|
488
581
|
...create ? { create } : {},
|
|
489
|
-
...children ? { children } : {}
|
|
582
|
+
...children ? { children } : {},
|
|
583
|
+
...category ? { category } : {},
|
|
584
|
+
...provides ? { provides } : {},
|
|
585
|
+
...references ? { references } : {}
|
|
490
586
|
});
|
|
491
587
|
});
|
|
492
588
|
return result.length ? result : void 0;
|
|
@@ -1174,7 +1270,8 @@ var MindooDBAppAgentApiImpl = class {
|
|
|
1174
1270
|
name: tool.name,
|
|
1175
1271
|
description: tool.description,
|
|
1176
1272
|
inputSchema: toJson(tool.inputSchema),
|
|
1177
|
-
...tool.annotations ? { annotations: { ...tool.annotations } } : {}
|
|
1273
|
+
...tool.annotations ? { annotations: { ...tool.annotations } } : {},
|
|
1274
|
+
...tool.scope ? { scope: tool.scope } : {}
|
|
1178
1275
|
}))
|
|
1179
1276
|
});
|
|
1180
1277
|
}
|
|
@@ -1438,6 +1535,8 @@ var MindooDBAppEmbedImpl = class {
|
|
|
1438
1535
|
dirtyListeners = /* @__PURE__ */ new Set();
|
|
1439
1536
|
dirtyState = void 0;
|
|
1440
1537
|
tracker = null;
|
|
1538
|
+
toolsState = null;
|
|
1539
|
+
toolsListeners = /* @__PURE__ */ new Set();
|
|
1441
1540
|
get placement() {
|
|
1442
1541
|
return this.frame ? "frame" : "overlay";
|
|
1443
1542
|
}
|
|
@@ -1470,6 +1569,56 @@ var MindooDBAppEmbedImpl = class {
|
|
|
1470
1569
|
}
|
|
1471
1570
|
await this.rpc.call("embeds.save", { embedId: this.embedId });
|
|
1472
1571
|
}
|
|
1572
|
+
async setActive() {
|
|
1573
|
+
if (this.closedEvent) {
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
await this.rpc.call("embeds.setActive", { embedId: this.embedId });
|
|
1577
|
+
}
|
|
1578
|
+
get agentTools() {
|
|
1579
|
+
return this.toolsState;
|
|
1580
|
+
}
|
|
1581
|
+
onAgentToolsChange(listener) {
|
|
1582
|
+
this.toolsListeners.add(listener);
|
|
1583
|
+
return () => {
|
|
1584
|
+
this.toolsListeners.delete(listener);
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
/** Haven reported the component's agent tools. */
|
|
1588
|
+
setAgentToolsState(tools) {
|
|
1589
|
+
if (this.closedEvent) {
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
this.toolsState = tools ? { ...tools, names: [...tools.names] } : null;
|
|
1593
|
+
this.toolsListeners.forEach((listener) => {
|
|
1594
|
+
try {
|
|
1595
|
+
listener(this.toolsState);
|
|
1596
|
+
} catch (error) {
|
|
1597
|
+
console.error("MindooDB embed agent tools listener failed.", error);
|
|
1598
|
+
}
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
waitForAgentTools(timeoutMs = 1e4) {
|
|
1602
|
+
if (this.toolsState?.names.length) {
|
|
1603
|
+
return Promise.resolve(this.toolsState);
|
|
1604
|
+
}
|
|
1605
|
+
if (this.closedEvent) {
|
|
1606
|
+
return Promise.resolve(null);
|
|
1607
|
+
}
|
|
1608
|
+
return new Promise((resolve) => {
|
|
1609
|
+
const finish = (tools) => {
|
|
1610
|
+
clearTimeout(timer);
|
|
1611
|
+
stopTools();
|
|
1612
|
+
stopClosed();
|
|
1613
|
+
resolve(tools);
|
|
1614
|
+
};
|
|
1615
|
+
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
1616
|
+
const stopTools = this.onAgentToolsChange((tools) => {
|
|
1617
|
+
if (tools?.names.length) finish(tools);
|
|
1618
|
+
});
|
|
1619
|
+
const stopClosed = this.onClosed(() => finish(null));
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1473
1622
|
async setRect(rect) {
|
|
1474
1623
|
if (this.closedEvent) {
|
|
1475
1624
|
return;
|
|
@@ -1524,6 +1673,15 @@ var MindooDBAppEmbedImpl = class {
|
|
|
1524
1673
|
});
|
|
1525
1674
|
this.listeners.clear();
|
|
1526
1675
|
this.dirtyListeners.clear();
|
|
1676
|
+
this.toolsState = null;
|
|
1677
|
+
this.toolsListeners.forEach((listener) => {
|
|
1678
|
+
try {
|
|
1679
|
+
listener(null);
|
|
1680
|
+
} catch (error) {
|
|
1681
|
+
console.error("MindooDB embed agent tools listener failed.", error);
|
|
1682
|
+
}
|
|
1683
|
+
});
|
|
1684
|
+
this.toolsListeners.clear();
|
|
1527
1685
|
}
|
|
1528
1686
|
};
|
|
1529
1687
|
var MindooDBAppEmbedsClient = class {
|
|
@@ -1547,6 +1705,16 @@ var MindooDBAppEmbedsClient = class {
|
|
|
1547
1705
|
}
|
|
1548
1706
|
return;
|
|
1549
1707
|
}
|
|
1708
|
+
if (message.event?.type === "agentTools") {
|
|
1709
|
+
const { embedId, agentTools } = message.event;
|
|
1710
|
+
const embed2 = this.open.get(embedId);
|
|
1711
|
+
if (embed2) {
|
|
1712
|
+
embed2.setAgentToolsState(agentTools ?? null);
|
|
1713
|
+
} else {
|
|
1714
|
+
this.earlyTools.set(embedId, agentTools ?? null);
|
|
1715
|
+
}
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1550
1718
|
if (message.event?.type !== "closed") {
|
|
1551
1719
|
return;
|
|
1552
1720
|
}
|
|
@@ -1591,6 +1759,10 @@ var MindooDBAppEmbedsClient = class {
|
|
|
1591
1759
|
this.earlyDirty.delete(embed.embedId);
|
|
1592
1760
|
embed.setDirtyState(earlyDirty);
|
|
1593
1761
|
}
|
|
1762
|
+
if (this.earlyTools.has(embed.embedId)) {
|
|
1763
|
+
embed.setAgentToolsState(this.earlyTools.get(embed.embedId) ?? null);
|
|
1764
|
+
this.earlyTools.delete(embed.embedId);
|
|
1765
|
+
}
|
|
1594
1766
|
const early = this.early.get(embed.embedId);
|
|
1595
1767
|
if (early) {
|
|
1596
1768
|
this.early.delete(embed.embedId);
|
|
@@ -1599,6 +1771,29 @@ var MindooDBAppEmbedsClient = class {
|
|
|
1599
1771
|
this.open.set(embed.embedId, embed);
|
|
1600
1772
|
}
|
|
1601
1773
|
return embed;
|
|
1774
|
+
},
|
|
1775
|
+
requestAccess: async (input) => {
|
|
1776
|
+
const components = (input?.components ?? []).map((entry) => ({
|
|
1777
|
+
componentKey: String(entry.componentKey),
|
|
1778
|
+
...entry.databaseIds ? { databaseIds: entry.databaseIds.map(String) } : {},
|
|
1779
|
+
...entry.intent === "view" ? { intent: "view" } : {}
|
|
1780
|
+
}));
|
|
1781
|
+
try {
|
|
1782
|
+
const response = await this.rpc.call("embeds.requestAccess", {
|
|
1783
|
+
input: { components }
|
|
1784
|
+
});
|
|
1785
|
+
return { granted: response?.granted ?? [], pending: response?.pending ?? [] };
|
|
1786
|
+
} catch (error) {
|
|
1787
|
+
if (!(error instanceof Error && error.name === "method-not-found")) {
|
|
1788
|
+
throw error;
|
|
1789
|
+
}
|
|
1790
|
+
return {
|
|
1791
|
+
granted: [],
|
|
1792
|
+
pending: components.flatMap(
|
|
1793
|
+
(entry) => (entry.databaseIds ?? []).map((databaseId) => ({ componentKey: entry.componentKey, databaseId }))
|
|
1794
|
+
)
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1602
1797
|
}
|
|
1603
1798
|
};
|
|
1604
1799
|
this.embedding = {
|
|
@@ -1617,6 +1812,15 @@ var MindooDBAppEmbedsClient = class {
|
|
|
1617
1812
|
setDirty: async (dirty) => {
|
|
1618
1813
|
await this.rpc.call("embedding.setDirty", { dirty: dirty === true });
|
|
1619
1814
|
},
|
|
1815
|
+
lookup: async (field, options) => {
|
|
1816
|
+
const result = await this.rpc.call("embedding.lookup", {
|
|
1817
|
+
field: String(field),
|
|
1818
|
+
...options?.query ? { query: String(options.query) } : {},
|
|
1819
|
+
...options?.docIds ? { docIds: options.docIds.map(String) } : {},
|
|
1820
|
+
...options?.limit ? { limit: Number(options.limit) } : {}
|
|
1821
|
+
});
|
|
1822
|
+
return Array.isArray(result) ? result : [];
|
|
1823
|
+
},
|
|
1620
1824
|
onSaveRequest: (handler) => {
|
|
1621
1825
|
this.saveHandler = handler;
|
|
1622
1826
|
return () => {
|
|
@@ -1635,6 +1839,7 @@ var MindooDBAppEmbedsClient = class {
|
|
|
1635
1839
|
/** Events that arrived before `embeds.open` resolved with their id. */
|
|
1636
1840
|
early = /* @__PURE__ */ new Map();
|
|
1637
1841
|
earlyDirty = /* @__PURE__ */ new Map();
|
|
1842
|
+
earlyTools = /* @__PURE__ */ new Map();
|
|
1638
1843
|
stopListening;
|
|
1639
1844
|
/** This app as a component: what to do when its host asks it to save. */
|
|
1640
1845
|
saveHandler = null;
|
|
@@ -3370,6 +3575,7 @@ export {
|
|
|
3370
3575
|
validateMindooDBAppAccepts,
|
|
3371
3576
|
mindooDBAppAcceptMatchesType,
|
|
3372
3577
|
MINDOODB_APP_COMPONENTS_MAX,
|
|
3578
|
+
MINDOODB_APP_COMPONENT_CONTRACT_PATTERN,
|
|
3373
3579
|
validateMindooDBAppComponents,
|
|
3374
3580
|
mindooDBAppComponentFieldsMatch,
|
|
3375
3581
|
resolveMindooDBAppLocalizedText,
|