datanotes 2.0.0 → 3.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/NOTICE +9 -0
- package/README.md +207 -28
- package/SECURITY.md +92 -45
- package/dist/THIRD-PARTY-NOTICES.txt +682 -0
- package/dist/api/catalog.d.cts +69 -0
- package/dist/api/catalog.d.ts +50 -2
- package/dist/api/cli-options.d.cts +39 -0
- package/dist/api/cli-options.d.ts +39 -0
- package/dist/api/client.d.cts +89 -0
- package/dist/api/client.d.ts +19 -30
- package/dist/api/docs.d.cts +211 -0
- package/dist/api/docs.d.ts +211 -0
- package/dist/api/guidance.d.cts +81 -0
- package/dist/api/guidance.d.ts +81 -0
- package/dist/api/index.cjs +6610 -2247
- package/dist/api/index.d.cts +13 -0
- package/dist/api/index.d.ts +10 -7
- package/dist/api/index.js +6610 -2247
- package/dist/api/local.cjs +118 -6
- package/dist/api/local.d.cts +95 -0
- package/dist/api/local.d.ts +70 -6
- package/dist/api/local.js +104 -6
- package/dist/api/mcp.cjs +17384 -0
- package/dist/api/mcp.d.cts +108 -0
- package/dist/api/mcp.d.ts +108 -0
- package/dist/api/mcp.js +17334 -0
- package/dist/api/otlp.d.cts +65 -0
- package/dist/api/redact.d.cts +17 -0
- package/dist/api/redact.d.ts +17 -0
- package/dist/api/requests.d.cts +26 -0
- package/dist/api/requests.d.ts +14 -16
- package/dist/api/results.d.cts +16 -0
- package/dist/api/results.d.ts +2 -1
- package/dist/api/tokens.d.cts +7 -0
- package/dist/datanotes.cjs +62911 -28409
- package/dist/docs/administration/configuration.md +59 -0
- package/dist/docs/administration/docker.md +80 -0
- package/dist/docs/administration/git-sync.md +71 -0
- package/dist/docs/administration/limits.md +69 -0
- package/dist/docs/administration/run-the-service.md +219 -0
- package/dist/docs/administration/status.md +61 -0
- package/dist/docs/administration/use-with-obsidian.md +75 -0
- package/dist/docs/api/operations/add_column.md +49 -0
- package/dist/docs/api/operations/add_constraint.md +55 -0
- package/dist/docs/api/operations/adopt_rows.md +50 -0
- package/dist/docs/api/operations/align_relations.md +41 -0
- package/dist/docs/api/operations/alter_column.md +46 -0
- package/dist/docs/api/operations/alter_table.md +67 -0
- package/dist/docs/api/operations/batch.md +48 -0
- package/dist/docs/api/operations/changes_stream.md +28 -0
- package/dist/docs/api/operations/check_table.md +29 -0
- package/dist/docs/api/operations/clone_table.md +48 -0
- package/dist/docs/api/operations/comment_on.md +42 -0
- package/dist/docs/api/operations/configure_database.md +52 -0
- package/dist/docs/api/operations/create_table.md +78 -0
- package/dist/docs/api/operations/create_view.md +54 -0
- package/dist/docs/api/operations/create_webhook.md +37 -0
- package/dist/docs/api/operations/define_base.md +52 -0
- package/dist/docs/api/operations/delete_rows.md +43 -0
- package/dist/docs/api/operations/delete_webhook.md +31 -0
- package/dist/docs/api/operations/describe_database.md +30 -0
- package/dist/docs/api/operations/describe_history.md +40 -0
- package/dist/docs/api/operations/describe_semantic_index.md +24 -0
- package/dist/docs/api/operations/describe_table.md +38 -0
- package/dist/docs/api/operations/docs.md +13 -0
- package/dist/docs/api/operations/drop_column.md +41 -0
- package/dist/docs/api/operations/drop_constraint.md +39 -0
- package/dist/docs/api/operations/drop_table.md +43 -0
- package/dist/docs/api/operations/drop_view.md +38 -0
- package/dist/docs/api/operations/engine_status.md +23 -0
- package/dist/docs/api/operations/graph_neighbors.md +32 -0
- package/dist/docs/api/operations/graph_path.md +32 -0
- package/dist/docs/api/operations/graph_traverse.md +43 -0
- package/dist/docs/api/operations/identify_session.md +24 -0
- package/dist/docs/api/operations/infer_table.md +43 -0
- package/dist/docs/api/operations/insert_row.md +48 -0
- package/dist/docs/api/operations/list_changes.md +35 -0
- package/dist/docs/api/operations/list_folder.md +28 -0
- package/dist/docs/api/operations/list_link_targets.md +44 -0
- package/dist/docs/api/operations/list_migrations.md +35 -0
- package/dist/docs/api/operations/list_tables.md +32 -0
- package/dist/docs/api/operations/list_webhooks.md +23 -0
- package/dist/docs/api/operations/manual_index.md +14 -0
- package/dist/docs/api/operations/manual_page.md +22 -0
- package/dist/docs/api/operations/merge_rows.md +44 -0
- package/dist/docs/api/operations/move_rows.md +52 -0
- package/dist/docs/api/operations/openapi.md +13 -0
- package/dist/docs/api/operations/openapi_post.md +20 -0
- package/dist/docs/api/operations/read_note.md +29 -0
- package/dist/docs/api/operations/recompute_columns.md +44 -0
- package/dist/docs/api/operations/record_tool_call.md +36 -0
- package/dist/docs/api/operations/rename_column.md +44 -0
- package/dist/docs/api/operations/rename_value.md +45 -0
- package/dist/docs/api/operations/restore_database.md +51 -0
- package/dist/docs/api/operations/restore_table.md +46 -0
- package/dist/docs/api/operations/search_notes.md +28 -0
- package/dist/docs/api/operations/select_rows.md +61 -0
- package/dist/docs/api/operations/semantic_search.md +34 -0
- package/dist/docs/api/operations/semantic_search_docs.md +26 -0
- package/dist/docs/api/operations/set_table_bases.md +44 -0
- package/dist/docs/api/operations/status.md +13 -0
- package/dist/docs/api/operations/stop_engine.md +23 -0
- package/dist/docs/api/operations/sync_semantic_index.md +33 -0
- package/dist/docs/api/operations/table_changes.md +31 -0
- package/dist/docs/api/operations/tags/auxiliary.md +14 -0
- package/dist/docs/api/operations/tags/change-feed.md +8 -0
- package/dist/docs/api/operations/tags/history.md +17 -0
- package/dist/docs/api/operations/tags/queries.md +15 -0
- package/dist/docs/api/operations/tags/rows-dml.md +16 -0
- package/dist/docs/api/operations/tags/schema-ddl.md +22 -0
- package/dist/docs/api/operations/tags/service.md +15 -0
- package/dist/docs/api/operations/tags/tokens.md +13 -0
- package/dist/docs/api/operations/tokens_create.md +34 -0
- package/dist/docs/api/operations/tokens_delete.md +29 -0
- package/dist/docs/api/operations/tokens_list.md +21 -0
- package/dist/docs/api/operations/tokens_me.md +20 -0
- package/dist/docs/api/operations/tokens_rotate.md +29 -0
- package/dist/docs/api/operations/tokens_update.md +34 -0
- package/dist/docs/api/operations/undo_migration.md +46 -0
- package/dist/docs/api/operations/update_row.md +60 -0
- package/dist/docs/api/operations/update_rows.md +46 -0
- package/dist/docs/api/operations/vacuum_table.md +37 -0
- package/dist/docs/api.md +135 -0
- package/dist/docs/develop/build-an-app.md +320 -0
- package/dist/docs/develop/build-on-datanotes.md +138 -0
- package/dist/docs/develop/change-feed-and-webhooks.md +70 -0
- package/dist/docs/develop/client.md +116 -0
- package/dist/docs/develop/concurrency.md +153 -0
- package/dist/docs/develop/http-api.md +79 -0
- package/dist/docs/develop/telemetry.md +86 -0
- package/dist/docs/guides/agents/docs-offline.md +99 -0
- package/dist/docs/guides/agents/how-agents-use-datanotes.md +124 -0
- package/dist/docs/guides/agents/prompts.md +495 -0
- package/dist/docs/guides/agents/reading-errors.md +112 -0
- package/dist/docs/guides/data-quality/check-constraints.md +379 -0
- package/dist/docs/guides/data-quality/check-table.md +63 -0
- package/dist/docs/guides/data-quality/hand-edits.md +507 -0
- package/dist/docs/guides/data-quality/sync-between-devices.md +51 -0
- package/dist/docs/guides/data-quality/validation.md +90 -0
- package/dist/docs/guides/database-objects/bases.md +480 -0
- package/dist/docs/guides/database-objects/catalogs-and-schemas.md +897 -0
- package/dist/docs/guides/database-objects/columns.md +1332 -0
- package/dist/docs/guides/database-objects/relations.md +1454 -0
- package/dist/docs/guides/database-objects/tables.md +1404 -0
- package/dist/docs/guides/database-objects/views.md +1003 -0
- package/dist/docs/guides/history/back-up.md +195 -0
- package/dist/docs/guides/history/change-data-feed.md +247 -0
- package/dist/docs/guides/history/journal-and-undo.md +262 -0
- package/dist/docs/guides/history/restore.md +341 -0
- package/dist/docs/guides/history/table-history.md +657 -0
- package/dist/docs/guides/history/vacuum.md +35 -0
- package/dist/docs/guides/read-and-write/automatic-links.md +768 -0
- package/dist/docs/guides/read-and-write/batch.md +139 -0
- package/dist/docs/guides/read-and-write/import-notes.md +896 -0
- package/dist/docs/guides/read-and-write/joins-and-aggregates.md +251 -0
- package/dist/docs/guides/read-and-write/read-and-filter.md +536 -0
- package/dist/docs/guides/read-and-write/write-rows.md +573 -0
- package/dist/docs/guides/search/link-graph.md +146 -0
- package/dist/docs/guides/search/semantic.md +133 -0
- package/dist/docs/guides/search/text.md +142 -0
- package/dist/docs/guides/security/security-model.md +110 -0
- package/dist/docs/guides/security/tokens.md +60 -0
- package/dist/docs/guides/security/who-did-what.md +42 -0
- package/dist/docs/index.md +113 -0
- package/dist/docs/llms.txt +33 -0
- package/dist/docs/reference/cli.md +136 -0
- package/dist/docs/reference/compatibility.md +63 -0
- package/dist/docs/reference/errors.md +72 -0
- package/dist/docs/reference/glossary.md +161 -0
- package/dist/docs/reference/operations/auxiliary.md +116 -0
- package/dist/docs/reference/operations/history.md +168 -0
- package/dist/docs/reference/operations/queries.md +169 -0
- package/dist/docs/reference/operations/rows.md +185 -0
- package/dist/docs/reference/operations/schema.md +316 -0
- package/dist/docs/reference/operations.md +96 -0
- package/dist/docs/reference/settings.md +31 -0
- package/dist/docs/release-notes.md +67 -0
- package/dist/docs/start/connect-agents.md +204 -0
- package/dist/docs/start/how-it-works.md +317 -0
- package/dist/docs/start/quick-start.md +126 -0
- package/package.json +59 -14
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Follow the change feed and webhooks
|
|
2
|
+
|
|
3
|
+
> React to every change in the vault from a program: poll or long-poll the change feed, stream it, or have the engine post it to a webhook.
|
|
4
|
+
|
|
5
|
+
The change feed numbers every change in the vault: writes through the engine, hand edits with the rules they break, schema changes and, when asked for, the tool calls of agents. A program follows it to react when a note changes, without reading the tables again. [Change data feed](https://datanotes.dev/guides/history/change-data-feed/) shows its events on a real engine.
|
|
6
|
+
|
|
7
|
+
## Choose how to follow the feed
|
|
8
|
+
|
|
9
|
+
A program learns about changes in three ways:
|
|
10
|
+
|
|
11
|
+
* **Polling**: `POST /datanotes/operations/list_changes/` returns the changes since a point in the feed
|
|
12
|
+
* **Streaming**: `GET /datanotes/changes/stream` sends each change as a Server-Sent Events (SSE) message
|
|
13
|
+
* **Webhooks**: the owner creates them, and the engine posts change events to their URL
|
|
14
|
+
|
|
15
|
+
Each event has its number, `seq`, the table, its `kind` (`insert`, `update`, `delete`, `move` or `metastore`), the operation that made it in `op`, the note’s path, the changed columns with their values before and after, and the actor. A hand edit has `source: "external"`, and its last event in a batch lists the rules it breaks in `violations`. When you rename a note by hand, the engine repairs the links to it. The events of that repair come after the move and name it in `cause`. For a row, `cause` also gives the `seq` of its `move` event.
|
|
16
|
+
|
|
17
|
+
## Long-poll the feed from code
|
|
18
|
+
|
|
19
|
+
`list_changes` returns the changes after `since`, oldest first. Pass the answer’s `last` as `since` on the next call, and `wait_ms` (at most 60,000) to wait for the next change instead of polling. `head` is the feed’s newest number, so a program that starts from `head` sees only what happens after it starts:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
type Feed = {
|
|
23
|
+
head: number;
|
|
24
|
+
last: number;
|
|
25
|
+
events: { kind: string; path: string }[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
const first = await db.call("list_changes", { limit: 1 });
|
|
30
|
+
if (!first.ok) throw new Error(String(first.value));
|
|
31
|
+
let since = (first.value as Feed).head;
|
|
32
|
+
for (;;) {
|
|
33
|
+
const r = await db.call("list_changes", {
|
|
34
|
+
since,
|
|
35
|
+
wait_ms: 20_000,
|
|
36
|
+
tables: ["film"],
|
|
37
|
+
});
|
|
38
|
+
if (!r.ok) throw new Error(String(r.value));
|
|
39
|
+
const feed = r.value as Feed;
|
|
40
|
+
for (const event of feed.events) console.log(event.kind, event.path);
|
|
41
|
+
since = feed.last;
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The filters narrow the feed before it reaches you:
|
|
46
|
+
|
|
47
|
+
* **`tables`**, **`kinds`** and **`columns`**: the tables, kinds of change and columns to keep
|
|
48
|
+
* **`where`**: rows that match a filter, as in `select_rows`, on the values after the change
|
|
49
|
+
* **`exclude_actor`**: leaves out the changes of one harness, such as the program’s own writes when it sends its name as `harness`
|
|
50
|
+
|
|
51
|
+
An answer with `gap: true` means the engine dropped older changes that you hadn’t read: read the tables again. To pick up where the program stopped after a restart, save `last` and start from it.
|
|
52
|
+
|
|
53
|
+
## Stream the feed
|
|
54
|
+
|
|
55
|
+
`GET /datanotes/changes/stream` sends each change as a Server-Sent Events (SSE) message, with its number as the event’s `id`. It takes the same filters as query parameters, and resumes from `Last-Event-ID` or `since`. A comment line every 25s keeps proxies from closing the connection, and an `event: gap` message tells the client that events were dropped.
|
|
56
|
+
|
|
57
|
+
## Receive changes with a webhook
|
|
58
|
+
|
|
59
|
+
A webhook has the engine post the feed to a web address, for a service that would rather be called than poll. Only the owner token creates webhooks, with [`create_webhook`](https://datanotes.dev/reference/operations/history/#create_webhook), and the answer shows the webhook’s `secret` once: give it to the receiver. An agent connected with `datanotes mcp` gets `create_webhook` only with `--owner-tools` or `--profile all` ([Choose the tools an agent sees](https://datanotes.dev/start/connect-agents/#choose-the-tools-an-agent-sees)).
|
|
60
|
+
|
|
61
|
+
The engine posts batches of up to 100 events, in order, as `{"webhook": "…", "events": [ … ], "last": 42}`. Two headers come with each one:
|
|
62
|
+
|
|
63
|
+
* **`X-Datanotes-Signature`**: `sha256=` and the hex HMAC-SHA256 of the body, keyed with the secret; check it before you trust the body
|
|
64
|
+
* **`X-Datanotes-Delivery`**: the number of the batch’s last event
|
|
65
|
+
|
|
66
|
+
A 2xx answer moves the webhook on. Any other answer is retried with backoff, from 5s up to 10 minutes, so a receiver that was down gets every event when it comes back. `datanotes status` lists a failing webhook with its last error.
|
|
67
|
+
|
|
68
|
+
## Choose the feed or a webhook
|
|
69
|
+
|
|
70
|
+
A named token can read the feed, while only the owner creates webhooks. So a program with its own token long-polls or streams the feed, as [Build an app on Datanotes](https://datanotes.dev/develop/build-an-app/) does, and a service the owner sets up receives a webhook.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Call the engine from JavaScript
|
|
2
|
+
|
|
3
|
+
> Use datanotes/client to run operations from Node, browsers and other applications.
|
|
4
|
+
|
|
5
|
+
The `datanotes` package exports `datanotes/client`, a client for the [HTTP API](https://datanotes.dev/develop/http-api/) with no dependencies. It runs in Node, in browsers and inside other applications that bring their own transport. The Model Context Protocol (MCP) server for agents uses it too.
|
|
6
|
+
|
|
7
|
+
## Install the client
|
|
8
|
+
|
|
9
|
+
The client ships in the `datanotes` package:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install datanotes
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Connect and run operations
|
|
16
|
+
|
|
17
|
+
Create one client per engine, with its address and a token. The token is the owner token or a [named token](https://datanotes.dev/guides/security/tokens/):
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { DatanotesClient } from "datanotes/client";
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
const db = new DatanotesClient({
|
|
24
|
+
url: "http://127.0.0.1:27150",
|
|
25
|
+
token: process.env.DATANOTES_TOKEN ?? "",
|
|
26
|
+
actor: { harness: "my-script" },
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The engine records `actor` with every write the client makes, unless a call names another actor.
|
|
31
|
+
|
|
32
|
+
Run an [operation](https://datanotes.dev/reference/operations/) by name, with the same arguments agents use. Each call answers `{ ok, value }`:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
const films = await db.call("select_rows", {
|
|
36
|
+
table: "film",
|
|
37
|
+
order_by: "-minutes",
|
|
38
|
+
});
|
|
39
|
+
if (films.ok) console.log((films.value as { rows: unknown[] }).rows);
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
const r = await db.call("insert_row", {
|
|
43
|
+
table: "film",
|
|
44
|
+
values: { title: "Metropolis", year: 1927, minutes: 153 },
|
|
45
|
+
});
|
|
46
|
+
if (!r.ok) console.log(r.value);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The first call lists films, longest first. The second adds a film. When the engine refuses a write, `ok` is `false`, `value` gives a readable reason, and the engine writes nothing. To write only when a row didn’t change since you read it, and to retry safely, see [Concurrent writes](https://datanotes.dev/develop/concurrency/).
|
|
50
|
+
|
|
51
|
+
For a complete program built on the client, see [Build an app on Datanotes](https://datanotes.dev/develop/build-an-app/).
|
|
52
|
+
|
|
53
|
+
## Call operations and routes
|
|
54
|
+
|
|
55
|
+
The client has three methods for the engine. Each one that takes `actor` uses it instead of the client’s default:
|
|
56
|
+
|
|
57
|
+
* **`call(name, args, actor?)`**: runs an engine operation and answers `{ ok, value }`; a name that isn’t an operation answers `ok: false` without a request
|
|
58
|
+
* **`post(route, body, actor?)`**: sends `POST /datanotes` followed by `route`, for example `post("/tokens/me/")`, and throws on an error status
|
|
59
|
+
* **`ping()`**: tells whether the engine answers
|
|
60
|
+
|
|
61
|
+
The package also exports the catalog of operations:
|
|
62
|
+
|
|
63
|
+
* **`OPERATIONS`**: each operation’s name, description and input schema
|
|
64
|
+
* **`operationRequest(name, args)`**: the HTTP request behind an operation
|
|
65
|
+
|
|
66
|
+
To offer the operations as tools of your own Model Context Protocol (MCP) server, use `datanotes/mcp`, as [Build on Datanotes](https://datanotes.dev/develop/build-on-datanotes/#build-your-own-mcp-server) shows.
|
|
67
|
+
|
|
68
|
+
## Handle errors
|
|
69
|
+
|
|
70
|
+
Failures that aren’t answers from an operation throw a `DatanotesError`. Its `kind` tells you what went wrong:
|
|
71
|
+
|
|
72
|
+
* **`unreachable`**: the engine didn’t answer
|
|
73
|
+
* **`auth`**: the engine refused the token
|
|
74
|
+
* **`http`**: `post` got an error status, given in `status`
|
|
75
|
+
|
|
76
|
+
Its `message` ends with a `Hint:` line, how to fix the call, and a `Docs:` line, the page that shows the fix; the error also has them as `hint` and `docs`. A failed `call` returns the same text as its `value`. To check an agent’s arguments before you send them, `checkArguments(name, args)` names the unknown, missing and wrongly typed ones. [Read the engine’s errors](https://datanotes.dev/guides/agents/reading-errors/) shows the shape of every error.
|
|
77
|
+
|
|
78
|
+
## Find the local engine
|
|
79
|
+
|
|
80
|
+
`datanotes/local` finds the engine that runs on a vault of this machine. It works in Node only:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { localEngine } from "datanotes/local";
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
const engine = localEngine("your_vault_folder");
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`localEngine` returns `{ url, token, lock }` while an engine runs on the vault, and `null` when none does. It throws when the engine that holds the vault runs on another machine.
|
|
90
|
+
|
|
91
|
+
It finds the engine as the engine finds its store: by the database’s id, in the data home. It honors the same variables, `DATANOTES_DATA_HOME` and `DATANOTES_STORE`, and takes them as options too:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
const engine = localEngine("your_vault_folder", {
|
|
95
|
+
store: "your_store_folder",
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`storeLocation("your_vault_folder")` returns the store’s folder, the database id and the database folder, without an engine running.
|
|
100
|
+
|
|
101
|
+
## Use your own transport
|
|
102
|
+
|
|
103
|
+
An application with its own way to make HTTP requests passes it as the transport. An Obsidian plugin, for example, passes `requestUrl`, so its requests aren’t subject to Cross-Origin Resource Sharing (CORS) rules:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const db = new DatanotesClient({
|
|
107
|
+
url,
|
|
108
|
+
token,
|
|
109
|
+
transport: async (req) => {
|
|
110
|
+
const r = await requestUrl({ ...req, throw: false });
|
|
111
|
+
return { status: r.status, text: r.text };
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The transport receives each request and returns its status and body text.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Concurrent writes
|
|
2
|
+
|
|
3
|
+
> What happens when several agents, programs and devices write to one engine at once, and how to write safely with expect, batch and retries.
|
|
4
|
+
|
|
5
|
+
Several clients often write to one database at once. Think of an agent, a script and the app on your phone. This page explains what the engine does with those writes, and how your program keeps its own writes correct.
|
|
6
|
+
|
|
7
|
+
## One engine per database
|
|
8
|
+
|
|
9
|
+
Run exactly one engine per database folder. While it runs, the engine holds a lock file, `engine.lock`, in its store. A second engine with the same store refuses to start.
|
|
10
|
+
|
|
11
|
+
The lock lives in the store, not in the database folder. So an engine with a store of its own isn’t refused, and two engines on one folder lose each other’s writes. Never run these on the same folder:
|
|
12
|
+
|
|
13
|
+
* the engine on the host and the engine in Docker
|
|
14
|
+
* engines on two devices that sync the vault
|
|
15
|
+
* two engines started with different `--store` folders
|
|
16
|
+
|
|
17
|
+
[Run the engine as a service](https://datanotes.dev/administration/run-the-service/#run-one-engine-per-database) shows the lock and what to do when it’s stale. [Sync the vault between devices](https://datanotes.dev/guides/data-quality/sync-between-devices/) shows how to run one engine on a synced vault.
|
|
18
|
+
|
|
19
|
+
Every client goes through that engine:
|
|
20
|
+
|
|
21
|
+
* **Programs**: call its [HTTP API](https://datanotes.dev/develop/http-api/), directly or with the [JavaScript client](https://datanotes.dev/develop/client/)
|
|
22
|
+
* **Agents**: call the Model Context Protocol (MCP) servers, which call the same HTTP API
|
|
23
|
+
* **People**: edit notes by hand, in Obsidian or another editor, which the engine records but doesn’t check first
|
|
24
|
+
|
|
25
|
+
## Writes run one at a time
|
|
26
|
+
|
|
27
|
+
The engine applies writes one at a time, under the lock of its transaction log. Reads don’t wait for that lock: they run in parallel, with each other and with writes.
|
|
28
|
+
|
|
29
|
+
The engine checks each write against the database as the writes before it left it. A write is checked first, then applied. If another write is applied while the engine checks yours, the engine checks yours again on the new state. It tries up to 5 times before it gives up.
|
|
30
|
+
|
|
31
|
+
So the rules of your tables hold even when clients race:
|
|
32
|
+
|
|
33
|
+
* **Unique columns**: two clients insert the same value of a `unique` column at once. One write succeeds; the other is refused with a violation of the unique column.
|
|
34
|
+
* **CHECK constraints that follow links**: a client adds a task to a project while another client closes that project. If the CHECK refuses tasks of closed projects, the task is refused.
|
|
35
|
+
* **A database that keeps changing**: when the state changes on every try, the engine refuses the write with a conflict.
|
|
36
|
+
|
|
37
|
+
That last conflict reads “the database changed while this write was being checked: send it again”. The answer has status 422 and `ok: false`, and the engine writes nothing. Send the write again.
|
|
38
|
+
|
|
39
|
+
## Two writes to the same row
|
|
40
|
+
|
|
41
|
+
Each edit sets only the columns it names. So two writes to one row behave this way:
|
|
42
|
+
|
|
43
|
+
* **Different columns**: both writes apply, and the row keeps both changes.
|
|
44
|
+
* **The same column, without `expect`**: both writes apply, and the last one stays. The last writer wins.
|
|
45
|
+
* **The same column, with `expect`**: the second write is refused, because the row no longer has the value it expects.
|
|
46
|
+
|
|
47
|
+
## Write only if the row didn’t change
|
|
48
|
+
|
|
49
|
+
`expect` on `update_row` makes the write a compare-and-set. You pass the values you read, and the engine writes only if the row still has them. Otherwise it refuses the write with a conflict and writes nothing.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { DatanotesClient } from "datanotes/client";
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
const db = new DatanotesClient({
|
|
56
|
+
url: "http://127.0.0.1:27150",
|
|
57
|
+
token: process.env.DATANOTES_TOKEN ?? "",
|
|
58
|
+
actor: { harness: "film-night" },
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
const r = await db.call("update_row", {
|
|
63
|
+
table: "film",
|
|
64
|
+
path: "main/public/film/Metropolis.md",
|
|
65
|
+
expect: { status: "to watch" },
|
|
66
|
+
set: { status: "watched" },
|
|
67
|
+
});
|
|
68
|
+
if (!r.ok) console.log(r.value);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
If someone marked the film `watched` after your read, the engine refuses the write. `r.value` names the column, the value you expected and the value it found.
|
|
72
|
+
|
|
73
|
+
`update_rows` has a guard of its own. `expect_count` refuses the write unless exactly that many rows match `where`. A filter that matches more rows than you meant then changes nothing.
|
|
74
|
+
|
|
75
|
+
## Retry after you read again
|
|
76
|
+
|
|
77
|
+
A refused write is safe to retry only after you read the current state. The row changed for a reason: compute your write again from what it holds now.
|
|
78
|
+
|
|
79
|
+
This loop adds one to a film’s `times_seen`. It reads the row, writes with `expect`, and reads again when the write is refused:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const path = "main/public/film/Metropolis.md";
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async function addViewing(attempts = 5): Promise<boolean> {
|
|
86
|
+
for (let i = 0; i < attempts; i++) {
|
|
87
|
+
const read = await db.call("select_rows", {
|
|
88
|
+
table: "film",
|
|
89
|
+
where: { path },
|
|
90
|
+
columns: ["times_seen"],
|
|
91
|
+
});
|
|
92
|
+
if (!read.ok) throw new Error(String(read.value));
|
|
93
|
+
const [film] = (read.value as {
|
|
94
|
+
rows: { times_seen: number }[];
|
|
95
|
+
}).rows;
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
const write = await db.call("update_row", {
|
|
99
|
+
table: "film",
|
|
100
|
+
path,
|
|
101
|
+
expect: { times_seen: film.times_seen },
|
|
102
|
+
set: { times_seen: film.times_seen + 1 },
|
|
103
|
+
});
|
|
104
|
+
if (write.ok) return true;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The loop stops after a few attempts. A write that keeps failing usually breaks a rule, and no retry fixes that. Show its reason to a person instead.
|
|
111
|
+
|
|
112
|
+
Decide on a refusal by its status, never by the text of its message:
|
|
113
|
+
|
|
114
|
+
* **422**: the engine refused the write, for validation or a conflict, and wrote nothing
|
|
115
|
+
* **409**: the engine refused an undo, or a table name is ambiguous
|
|
116
|
+
* **503**: a file failed mid-write, and the engine put back what it wrote (`rolled_back: true`). Nothing changed: send the write again later
|
|
117
|
+
* **500**: a file failed mid-write, and so did the rollback. `failed` lists what stayed: run `undo_migration` first
|
|
118
|
+
|
|
119
|
+
`call` answers `ok: false` for any of these. To read the status, use `post`: it throws a `DatanotesError` whose `status` holds it. Over plain HTTP, read the status of the response. [Call the engine over HTTP](https://datanotes.dev/develop/http-api/#read-errors) lists every status.
|
|
120
|
+
|
|
121
|
+
## Group writes that belong together
|
|
122
|
+
|
|
123
|
+
A `batch` is all or nothing while the engine keeps running. Its operations run in order, each one seeing the ones before it. If the engine refuses one, it undoes the ones already applied, and the answer has `rolled_back: true`.
|
|
124
|
+
|
|
125
|
+
A crash or `kill -9` in the middle of a batch is different. It may leave part of the batch applied:
|
|
126
|
+
|
|
127
|
+
* **Steps already done**: each keeps a journal entry of its own, which `undo_migration` reverts
|
|
128
|
+
* **The step under way**: its notes are written before its journal entry, so part of it may stay outside the journal
|
|
129
|
+
* **The batch entry**: the engine writes it last, so it’s missing
|
|
130
|
+
|
|
131
|
+
Run `check_table` after the next start to find the rows such a stop left. Stop the engine with `datanotes stop` instead, which lets the batch finish.
|
|
132
|
+
|
|
133
|
+
Use a batch when several writes must agree, such as a screening and the film it marks as watched. [Batch writes](https://datanotes.dev/guides/read-and-write/batch/) shows a batch with its answer.
|
|
134
|
+
|
|
135
|
+
A batch isn’t isolated from other clients, though. The engine applies its operations one by one, like separate writes. So other clients can act between two of them:
|
|
136
|
+
|
|
137
|
+
* **Reads**: a client may see the first operations of a refused batch before the engine undoes them.
|
|
138
|
+
* **Writes**: another client’s write can land between two operations of the batch. The engine then undoes the batch and runs it again on the new state.
|
|
139
|
+
* **Undo**: if another client changed a row the batch wrote, the undo may fail. The answer then has `rolled_back: false` and status 500, and `rollback` lists what stayed.
|
|
140
|
+
|
|
141
|
+
When `rolled_back` is `false`, undo the listed writes with `undo_migration`, or repair the rows, before you retry.
|
|
142
|
+
|
|
143
|
+
## Hand edits skip the checks
|
|
144
|
+
|
|
145
|
+
A person who edits a note in Obsidian writes straight to the file. The engine doesn’t validate that edit, because it can’t refuse it.
|
|
146
|
+
|
|
147
|
+
The engine records the edit instead, so you find what it broke:
|
|
148
|
+
|
|
149
|
+
* **Transaction log**: a table with history records the edit as `EXTERNAL_EDIT`
|
|
150
|
+
* **Change feed**: the edit’s event has `source: "external"`, and its last event lists the rules it breaks in `violations`
|
|
151
|
+
* **`check_table`**: lists every row of a table that breaks the table’s rules
|
|
152
|
+
|
|
153
|
+
A program that follows the [change feed](https://datanotes.dev/develop/change-feed-and-webhooks/) sees hand edits as they happen. [Hand edits](https://datanotes.dev/guides/data-quality/hand-edits/) shows how to find and repair the rows they break.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Call the engine over HTTP
|
|
2
|
+
|
|
3
|
+
> Authenticate, call the engine’s routes, check its health and read its errors.
|
|
4
|
+
|
|
5
|
+
`datanotes serve` starts the engine’s HTTP server on `http://127.0.0.1:27150`. Every operation of the engine is a route, so any language that sends HTTP requests can read and write your notes. This page covers tokens, the routes, the engine’s health report and errors; the [HTTP API reference](https://datanotes.dev/api/) documents each route.
|
|
6
|
+
|
|
7
|
+
## Authenticate your requests
|
|
8
|
+
|
|
9
|
+
Every request needs an `Authorization: Bearer your_token` header, with one of two tokens:
|
|
10
|
+
|
|
11
|
+
* **Owner token**: `httpServerToken` in the settings file, which the engine generates when it first starts; it can do everything
|
|
12
|
+
* **Named token**: the engine creates it when you [create a token](https://datanotes.dev/guides/security/tokens/#create-a-token) for a program or an agent, with only the permissions you give it
|
|
13
|
+
|
|
14
|
+
Three routes answer without a token: `GET /`, `GET /datanotes/openapi.json` and the `/datanotes/docs` console. They hold no data from your notes.
|
|
15
|
+
|
|
16
|
+
To use another port, set `httpServerPort` in the settings, or start the engine with `--port`. To listen on another address, start the engine with `--host`. [Choose the port](https://datanotes.dev/administration/run-the-service/#choose-the-port) explains both.
|
|
17
|
+
|
|
18
|
+
## Call the engine’s routes
|
|
19
|
+
|
|
20
|
+
The engine serves its operations and a description of itself:
|
|
21
|
+
|
|
22
|
+
* **`POST /datanotes/operations/<operation>/`**: one route per operation, with the operation’s arguments as the JSON body, for example `POST /datanotes/operations/insert_row/` with `{"table": "film", "values": {…}}`
|
|
23
|
+
* **`GET /`**: whether the engine runs, and its version
|
|
24
|
+
* **`GET /datanotes/openapi.json`**: the OpenAPI 3.1 description of every route, generated from the catalog of operations, to generate clients in any language
|
|
25
|
+
* **`GET /datanotes/docs`**: the same description in an interactive console
|
|
26
|
+
|
|
27
|
+
The console is Swagger UI, which the engine serves itself, with nothing from other hosts. Click **Authorize**, paste the owner token or a named token, then click **Execute** on any route to call this engine. The browser keeps the token in its local storage until you log out in the **Authorize** dialog.
|
|
28
|
+
|
|
29
|
+
An operation’s body takes the same arguments as the agent’s tool of the same name. You read notes, rows or not, with three operations:
|
|
30
|
+
|
|
31
|
+
* **[`read_note`](https://datanotes.dev/reference/operations/queries/#read_note)**: one note, by its `path`
|
|
32
|
+
* **[`list_folder`](https://datanotes.dev/reference/operations/queries/#list_folder)**: the notes and folders in a `folder`
|
|
33
|
+
* **[`search_notes`](https://datanotes.dev/reference/operations/queries/#search_notes)**: the notes that contain a `query`
|
|
34
|
+
|
|
35
|
+
Every route and field keeps working in every 3.x version: see [Compatibility](https://datanotes.dev/reference/compatibility/).
|
|
36
|
+
|
|
37
|
+
## Record who makes each write
|
|
38
|
+
|
|
39
|
+
The `X-Datanotes-Actor` header tells the engine on whose behalf you write. Its value is JSON, with any of `session`, `harness`, `model` and `tool`:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
curl -X POST http://127.0.0.1:27150/datanotes/operations/insert_row/ \
|
|
43
|
+
-H "Authorization: Bearer your_owner_token" \
|
|
44
|
+
-H "Content-Type: application/json" \
|
|
45
|
+
-H 'X-Datanotes-Actor: {"harness": "my-script"}' \
|
|
46
|
+
-d '{"table": "film", "values": {"title": "His Girl Friday", "year": 1940}}'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The engine stores the actor with the write in the journal, and in the table’s history when the table keeps one. It adds the credential as `auth`: `owner`, or the named token’s name, which the header can’t claim.
|
|
50
|
+
|
|
51
|
+
## Check the engine’s health
|
|
52
|
+
|
|
53
|
+
`POST /datanotes/status/` reports the engine’s health, and `datanotes status` prints the same report from a shell. [Check the engine’s status](https://datanotes.dev/administration/status/) shows both.
|
|
54
|
+
|
|
55
|
+
## Read errors
|
|
56
|
+
|
|
57
|
+
Errors come back as JSON `{"error": "…", "hint": "…", "docs": "…"}`: what is wrong, how to fix it, and the page of this site, in Markdown, that shows the fix ([read the engine’s errors](https://datanotes.dev/guides/agents/reading-errors/)). The status is one of these:
|
|
58
|
+
|
|
59
|
+
* **400**: bad input
|
|
60
|
+
* **401**: missing or wrong token
|
|
61
|
+
* **403**: a named token beyond its permissions
|
|
62
|
+
* **404**: unknown table, view, migration or note
|
|
63
|
+
* **409**: ambiguous table name
|
|
64
|
+
* **500**: an unexpected error, or a write that failed partway and couldn’t be [rolled back](https://datanotes.dev/guides/history/journal-and-undo/#the-engine-rolls-back-a-write-it-cannot-finish)
|
|
65
|
+
* **503**: a write that failed partway, which the engine rolled back, so nothing changed: send it again later
|
|
66
|
+
|
|
67
|
+
The engine answers a refused write, from validation or a conflict, with 422 (409 for an undo). The body is the operation’s result, with `ok: false`, the reasons, `hint` and `docs`, and the engine writes nothing. An unexpected error (500) has no hint: it isn’t a mistake in the request.
|
|
68
|
+
|
|
69
|
+
Several clients can write at the same time. [Concurrent writes](https://datanotes.dev/develop/concurrency/) explains what the engine does then, and when to retry a refused write.
|
|
70
|
+
|
|
71
|
+
## Keep the engine local
|
|
72
|
+
|
|
73
|
+
The engine binds to `127.0.0.1` unless you start it with `--host`. It answers no Cross-Origin Resource Sharing (CORS) preflight, so web pages can’t call it. To reach it from another device, follow the [security model](https://datanotes.dev/guides/security/security-model/).
|
|
74
|
+
|
|
75
|
+
## Generate a client or explore the routes
|
|
76
|
+
|
|
77
|
+
The [HTTP API reference](https://datanotes.dev/api/) documents every route, generated from the same OpenAPI description, [openapi.json](https://datanotes.dev/openapi.json). From JavaScript, use the [JavaScript client](https://datanotes.dev/develop/client/). Agents use the [Model Context Protocol (MCP) server](https://datanotes.dev/start/connect-agents/).
|
|
78
|
+
|
|
79
|
+
To see the routes at work in one program, follow [Build an app on Datanotes](https://datanotes.dev/develop/build-an-app/).
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Send logs to your observability backend
|
|
2
|
+
|
|
3
|
+
> Send the logs of the engine, the MCP server and the apps to any OpenTelemetry backend.
|
|
4
|
+
|
|
5
|
+
Every part of Datanotes sends its logs with OpenTelemetry, the vendor-neutral standard, so any backend with an OpenTelemetry Protocol (OTLP) endpoint can receive them. This page shows how to point the engine, the Model Context Protocol (MCP) server and the apps at your backend. It also shows how to follow one agent call across all three. Datanotes has no vendor-specific code.
|
|
6
|
+
|
|
7
|
+
## What Datanotes sends and where
|
|
8
|
+
|
|
9
|
+
Datanotes sends logs only, over HTTP, to `your_endpoint/v1/logs`. It encodes them as `http/protobuf`, the OpenTelemetry default, or as `http/json`. Any OpenTelemetry Collector receives them, and so do these backends, among others:
|
|
10
|
+
|
|
11
|
+
* SigNoz
|
|
12
|
+
* Grafana: Alloy, Loki or Grafana Cloud
|
|
13
|
+
* Elastic, Datadog, Honeycomb, New Relic and Dynatrace
|
|
14
|
+
|
|
15
|
+
Datanotes sends no traces or metrics. For a backend that accepts only gRPC, put an OpenTelemetry Collector in front of it. When the collector doesn’t answer, Datanotes counts the records as lost and keeps working.
|
|
16
|
+
|
|
17
|
+
## Configure the export with environment variables
|
|
18
|
+
|
|
19
|
+
The engine, the MCP server and the apps all read the standard OpenTelemetry variables:
|
|
20
|
+
|
|
21
|
+
| Variable | Meaning |
|
|
22
|
+
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
23
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | The collector’s base address, for example `http://127.0.0.1:4318` |
|
|
24
|
+
| `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | Or the full `…/v1/logs` address |
|
|
25
|
+
| `OTEL_EXPORTER_OTLP_HEADERS` | `key=value,key2=value2`, with URL-encoded values: the API key or `Authorization` your backend asks for |
|
|
26
|
+
| `OTEL_EXPORTER_OTLP_PROTOCOL` | `http/protobuf` (default) or `http/json` |
|
|
27
|
+
| `OTEL_SERVICE_NAME` | The service name; defaults to `datanotes`, `datanotes-mcp` (for `datanotes mcp`) or the app’s name |
|
|
28
|
+
| `OTEL_RESOURCE_ATTRIBUTES` | Extra resource attributes, for example `deployment.environment=home` |
|
|
29
|
+
| `OTEL_SDK_DISABLED=true` or `OTEL_LOGS_EXPORTER=none` | Turns the export off |
|
|
30
|
+
|
|
31
|
+
Datanotes also reads the `…_LOGS_…` variants of the headers and protocol variables. Without an endpoint, it sends nothing.
|
|
32
|
+
|
|
33
|
+
## Configure the engine in its settings file
|
|
34
|
+
|
|
35
|
+
Instead of the variables, you can configure the engine in its settings file, `settings.json` in its store (`datanotes store path` shows where). A variable you set overrides the matching setting:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"telemetryEnabled": true,
|
|
40
|
+
"telemetryEndpoint": "http://127.0.0.1:4318",
|
|
41
|
+
"telemetryServiceName": "datanotes",
|
|
42
|
+
"telemetryProtocol": "http/protobuf",
|
|
43
|
+
"telemetryHeaders": {}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`telemetryHeaders` holds what your backend asks for, such as an API key or `Authorization: Bearer …`. The engine keeps it in the settings file. To keep a key out of that file, set `OTEL_EXPORTER_OTLP_HEADERS` instead.
|
|
48
|
+
|
|
49
|
+
## What the engine logs
|
|
50
|
+
|
|
51
|
+
The engine logs these events:
|
|
52
|
+
|
|
53
|
+
* Every HTTP request and operation, with the route, duration, outcome and actor
|
|
54
|
+
* The tool calls MCP servers report, with the tool, duration, result size in characters and tokens, client, model and session
|
|
55
|
+
* Refused requests
|
|
56
|
+
* The semantic index and git sync
|
|
57
|
+
* Errors
|
|
58
|
+
|
|
59
|
+
Whatever the collector does, the engine also writes its records to files in `logs/`, in its store outside the vault. The `fileLogEnabled` setting turns these files on or off, and `logRetentionDays` sets how long the engine keeps them.
|
|
60
|
+
|
|
61
|
+
## Configure the MCP server and the apps
|
|
62
|
+
|
|
63
|
+
The MCP server and the apps read the same variables, and each has its own alternative:
|
|
64
|
+
|
|
65
|
+
* **`datanotes mcp`**: the `--otlp-endpoint your_endpoint` option
|
|
66
|
+
* **Apps**: an `otlp` block in the app’s configuration, `{"endpoint": "http://127.0.0.1:4318", "headers": {}, "serviceName": "…"}`
|
|
67
|
+
|
|
68
|
+
## Follow one agent call across services
|
|
69
|
+
|
|
70
|
+
The engine, the MCP server and the apps log the same `session` and `tool` for each agent call. `session` is the first 16 characters of the MCP session ID. Filter by `session` in your backend to see the call in the MCP server, in the app whose tool it reached, and the writes it made in the engine.
|
|
71
|
+
|
|
72
|
+
## Send logs from your own program
|
|
73
|
+
|
|
74
|
+
`datanotes/client` exports the exporter that Datanotes uses, so your own programs can log the same way:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { OtlpLogExporter, otlpFromEnv } from "datanotes/client";
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
const otlp = otlpFromEnv(process.env, { serviceName: "my-app" });
|
|
81
|
+
const log = otlp ? new OtlpLogExporter(otlp) : null;
|
|
82
|
+
log?.log("INFO", "imported films", { count: 8 });
|
|
83
|
+
await log?.close();
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`otlpFromEnv` reads the variables above and returns `null` when you set no endpoint or turn the export off; the program then logs nothing. Call `close()` before the program exits to send the remaining records.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Read the docs offline
|
|
2
|
+
|
|
3
|
+
> The datanotes package ships these docs as Markdown for its version: agents read and search them with list_docs, read_docs and search_docs, programs through the engine, people from the package.
|
|
4
|
+
|
|
5
|
+
The `datanotes` package ships this documentation as Markdown: one file per page of the site in `dist/docs/`, with `llms.txt`. The pages match the installed version, and reading them needs no internet access.
|
|
6
|
+
|
|
7
|
+
## The agent reads the docs of its version
|
|
8
|
+
|
|
9
|
+
`datanotes mcp` serves these pages to agents. The agent looks up an operation or an example without internet access, and the pages always match the version it talks to. Three tools read them:
|
|
10
|
+
|
|
11
|
+
* **`list_docs`**: lists the pages, each with its path, title and description. `prefix` keeps one folder, such as `guides/`.
|
|
12
|
+
* **`read_docs`**: returns a page as Markdown, or one section of it by the anchor of its heading, such as `guides/read-and-write/read-and-filter#compare-values-with-operators`. It returns at most 20,000 characters at a time, and a longer text ends with how to read the rest.
|
|
13
|
+
* **`search_docs`**: finds the sections that match a few words, best first. Each hit has the path to pass to `read_docs` and a short excerpt.
|
|
14
|
+
|
|
15
|
+
The `Docs:` line of an error names the `read_docs` call for its section, so the agent reads it offline at once:
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
Docs: read_docs("guides/read-and-write/read-and-filter#compare-values-with-operators")
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The server leaves out the web address, since its agents always have the docs tools. A program that uses `DatanotesClient` gets both: the address, then the `read_docs` call in parentheses. Your own MCP server with the docs tools drops the address with `offlineDocs` from `datanotes/mcp`.
|
|
22
|
+
|
|
23
|
+
An agent that asks for a page or section that doesn’t exist gets the closest ones in the hint. Reading a section instead of a whole page keeps the agent’s context small: `search_docs` finds the section, and `read_docs` returns only that.
|
|
24
|
+
|
|
25
|
+
## Search with required and excluded words
|
|
26
|
+
|
|
27
|
+
`search_docs` ranks the sections by the words of the query. A word matches in any form, so `rows` finds `row`. It also matches its synonyms, so `revert` finds `undo`. Words in a heading count most.
|
|
28
|
+
|
|
29
|
+
Three marks narrow the search:
|
|
30
|
+
|
|
31
|
+
* **`"a phrase"`**: the section must hold these words in a row, in any form, such as `"time travel"`
|
|
32
|
+
* **`+word`**: the section must hold the word, such as `+view`
|
|
33
|
+
* **`-word`**: the section must not hold the word, such as `-sql`; `-"a phrase"` leaves out a phrase
|
|
34
|
+
|
|
35
|
+
The other words only rank the sections. This query finds the sections on time travel that mention views and never SQL. Here, `db` is a [client](https://datanotes.dev/develop/client/) connected to the engine, as in [Build your own MCP server](https://datanotes.dev/develop/build-on-datanotes/#build-your-own-mcp-server):
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { callDocsTool } from "datanotes/mcp";
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
const hits = await callDocsTool(
|
|
42
|
+
"search_docs",
|
|
43
|
+
{ query: '"time travel" +view -sql' },
|
|
44
|
+
{ client: db },
|
|
45
|
+
);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The marks filter the results, and never rank them. A section with every required word still ranks by all the words of the query. A word that starts with two dashes, such as `--vault`, is a plain word.
|
|
49
|
+
|
|
50
|
+
## Search by words and by meaning
|
|
51
|
+
|
|
52
|
+
When the engine has [semantic search](https://datanotes.dev/guides/search/semantic/) on, `search_docs` becomes a hybrid search. It ranks the sections twice:
|
|
53
|
+
|
|
54
|
+
* **By words**: the ranking above, made by the MCP server from its own copy of the docs
|
|
55
|
+
* **By meaning**: the engine embeds the query and ranks the sections by the similarity of their vectors
|
|
56
|
+
|
|
57
|
+
Reciprocal Rank Fusion merges the two rankings. Each section scores the sum of 1 / (60 + its rank) over the rankings that hold it. Only the first 50 sections of each ranking count. A section that both rankings place high comes first, ahead of one that a single ranking finds. The required and excluded words filter both rankings.
|
|
58
|
+
|
|
59
|
+
Each hit then ends with the rankings that found it: `(keyword, semantic)`, `(keyword)` or `(semantic)`. The last line of the answer says the search was hybrid.
|
|
60
|
+
|
|
61
|
+
This is the hybrid search of vector databases such as ChromaDB, with no service to add. The engine already has the embedding model of semantic search. It keeps the vectors of the docs in its store, in `semantic/docs.json`, apart from the index of your notes.
|
|
62
|
+
|
|
63
|
+
The engine builds that index in the background:
|
|
64
|
+
|
|
65
|
+
* **When it starts**: after it updates the index of your notes
|
|
66
|
+
* **When the docs change**: after an upgrade or a new docs build, the engine embeds again only the sections whose text changed
|
|
67
|
+
* **When the model changes**: another provider, address or model starts a new index
|
|
68
|
+
|
|
69
|
+
The version of the docs is a hash of their sections, so any change to a section counts. The docs are public: embedding them sends nothing from your vault. With a paid provider, the docs cost about as much to index as 700 short notes.
|
|
70
|
+
|
|
71
|
+
`search_docs` ranks by words alone, as without semantic search, in these cases:
|
|
72
|
+
|
|
73
|
+
* **No semantic search**: it’s off, or the engine is older than hybrid search
|
|
74
|
+
* **The index is being built**: the first search after a change starts it
|
|
75
|
+
* **The engine runs another version**: its docs differ from those of the MCP server
|
|
76
|
+
* **The engine doesn’t answer**: `search_docs` asks it again a minute later
|
|
77
|
+
|
|
78
|
+
## Read the docs from programs and editors
|
|
79
|
+
|
|
80
|
+
Programs and people read the same pages:
|
|
81
|
+
|
|
82
|
+
* **Programs**: the engine serves them, with a token like its other routes. `GET /datanotes/manual/` lists them, and `GET /datanotes/manual/guides/read-and-write/read-and-filter.md` returns one page. Add `?section=compare-values-with-operators` for one section.
|
|
83
|
+
* **Search by meaning**: `POST /datanotes/manual/search/` takes a `query` and returns the sections closest in meaning, with `ready: false` while the engine builds the index. Programs that use `datanotes/mcp` pass a `client` to `callDocsTool` to search like `datanotes mcp`.
|
|
84
|
+
* **People and editors**: the files themselves, in `node_modules/datanotes/dist/docs/`.
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
curl -H "Authorization: Bearer your_token" \
|
|
88
|
+
http://127.0.0.1:27150/datanotes/manual/start/quick-start.md
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
A program ranks the sections by meaning with the same [client](https://datanotes.dev/develop/client/):
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
const ranked = await db.post("/manual/search/", {
|
|
95
|
+
query: "undo what an agent did",
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
In a checkout of the repository, build the docs first with `cd docs && npm ci && npm run build`, which copies the pages to `dist/docs/`. Until then, the tools answer with that command.
|