datanotes 1.1.1 → 1.2.1

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 CHANGED
@@ -3,9 +3,9 @@
3
3
  The engine of your second brain: Markdown notes as database tables, for you and your AI agents.
4
4
 
5
5
  A row is any note whose `table` property names a table; tables are defined in
6
- `Database/database.yaml`. Every write is checked against its table, journaled and undoable. Agents
7
- programs use its HTTP API or the `datanotes/client` library, agents use the separate MCP server
8
- `datanotes-mcp`, and it also runs inside Obsidian as a plugin.
6
+ `Database/database.yaml`. Every write is checked against its table, journaled and undoable.
7
+ Programs use its HTTP API or the `datanotes/client` library; agents use the separate MCP server
8
+ `datanotes-mcp`.
9
9
 
10
10
  **Documentation: [datanotes.dev](https://datanotes.dev)**
11
11
 
@@ -17,7 +17,8 @@ Requires Node.js 20 or newer. Start the engine on a vault:
17
17
  npx -y datanotes serve --vault /path/to/vault
18
18
  ```
19
19
 
20
- It serves the HTTP API on `http://127.0.0.1:27150/datanotes/*`, with the owner's token in
20
+ It serves the HTTP API on `http://127.0.0.1:27150` (routes under `/datanotes/*`, plus the
21
+ Local-REST-API-style `/vault/*` and `/search/simple/`), with the owner's token in
21
22
  `<vault>/.datanotes/service.json`. From code:
22
23
 
23
24
  ```js
@@ -37,7 +38,7 @@ server of Datanotes, a separate package built on this API.
37
38
  - [Connect agents (datanotes-mcp)](https://datanotes.dev/start/connect-agents/)
38
39
  - [JavaScript client](https://datanotes.dev/guides/client/)
39
40
  - [Apps](https://datanotes.dev/guides/apps/)
40
- - [Obsidian plugin](https://datanotes.dev/guides/obsidian/)
41
+ - [Obsidian client](https://datanotes.dev/guides/obsidian/)
41
42
 
42
43
  ## Security
43
44
 
package/SECURITY.md CHANGED
@@ -25,7 +25,7 @@ run on your own machine, for you and the agents you choose.
25
25
  | Credential | Where | What it can do |
26
26
  | --- | --- | --- |
27
27
  | Owner token | `httpServerToken` in the settings | Everything: data, schema, apps, webhooks, undo. Keep it on the machine. |
28
- | App token | created with `POST /datanotes/apps/create/` (owner only), stored hashed | Read everything; write only the tables in its `write` patterns; change the schema only with `ddl`. It cannot create webhooks, manage apps or undo; with `maintain` it may also undo, restore, vacuum and configure (an agent's own token). |
28
+ | App token | created with `POST /datanotes/apps/create/` (owner only), stored hashed | Read everything; write only the tables in its `write` patterns; change the schema only with `ddl`. It cannot create webhooks, manage apps or undo; with `maintain` it may also undo, restore, vacuum, configure and rebuild the semantic index (an agent's own token). |
29
29
 
30
30
  Prefer an **app token per device or agent**: it has only the permissions it needs and the journal
31
31
  names it. MCP clients use the same tokens: the MCP server passes them to the engine.
@@ -44,8 +44,11 @@ followed: do not link to folders you would not give the engine.
44
44
 
45
45
  Nothing leaves the machine unless you turn it on:
46
46
 
47
- - **Semantic search** sends note text to the embedding provider you configure (`semanticProvider`,
48
- off by default). A local provider (Ollama) keeps it on the machine.
47
+ - **Semantic search** sends note text to the embedding provider you configure (`semanticEnabled`,
48
+ off by default; provider `semanticProvider` / `semanticBaseUrl`). A local provider (Ollama,
49
+ LM Studio) keeps it on the machine.
49
50
  - **Telemetry** sends logs to the OTLP endpoint you configure (`telemetryEnabled`, off by default).
50
51
  - **Webhooks** (created by the owner) post change events to the URLs they name.
52
+ - **Apps' MCP endpoints** (registered by the owner): the engine sends each app's `mcp_token`, and
53
+ the arguments and caller of every call of its tools, to the app's `mcp_url`, which may be remote.
51
54
  - **Git sync** (`--git-sync`) pushes the vault to its git remote.
@@ -41,7 +41,7 @@ module.exports = __toCommonJS(index_exports);
41
41
  var OPERATIONS = [
42
42
  {
43
43
  "name": "read_note",
44
- "description": "Read a vault file (``GET /vault/{filename}``).\n\nDefault response is raw markdown. With ``as_json`` true, requests ``Accept: application/vnd.olrapi.note+json`` and returns ``NoteJson``: ``content``, ``frontmatter``, ``tags``, ``path``, and ``stat`` (file times and size), per the plugin OpenAPI schema.\n\nInputs:\n- ``path``: Vault-relative path to the file (forward slashes).\n- ``as_json``: If false (default), return markdown. If true, return ``NoteJson``.\n\nReturns:\n- ``str`` \u2014 markdown body, or\n- ``dict[str, Any]`` \u2014 parsed ``NoteJson`` when ``as_json`` is true.",
44
+ "description": "Read a vault file (`GET /vault/{path}`). Default: the raw markdown. With `as_json`:\n`{content, frontmatter, tags, path, stat}` (`tags` without `#`, from the text and the\nfrontmatter; `stat`: `{ctime, mtime, size}` or null). A missing file is an error.\n`path`: vault-relative, forward slashes.",
45
45
  "inputSchema": {
46
46
  "additionalProperties": false,
47
47
  "properties": {
@@ -68,7 +68,7 @@ var OPERATIONS = [
68
68
  },
69
69
  {
70
70
  "name": "list_directory",
71
- "description": 'List files under a vault directory (``GET /vault/`` or ``GET /vault/{pathToDirectory}/``).\n\nResponse JSON includes a ``files`` array. Names ending with ``/`` are subdirectories. Empty directories may not appear in listings (per API). Use ``""`` for the vault root.\n\nInputs:\n- ``path``: Vault-relative directory path; use ``""`` for the vault root.\n\nReturns:\n- ``dict[str, Any]`` \u2014 parsed JSON listing from the API.',
71
+ "description": 'Direct children of a vault folder (`GET /vault/{folder}/`): `{files: [...]}`. Folders end with\n`/`, names are sorted, dot-files are hidden. `""` is the vault root. A missing folder is an\nerror.',
72
72
  "inputSchema": {
73
73
  "additionalProperties": false,
74
74
  "properties": {
@@ -89,7 +89,7 @@ var OPERATIONS = [
89
89
  },
90
90
  {
91
91
  "name": "search_simple",
92
- "description": "Full-text search (``POST /search/simple/``).\n\nUses Obsidian\u2019s native search syntax (keywords, ``path:``, ``tag:``, ``file:``, etc.). Results include filenames, match positions, context snippets, and scores per the plugin OpenAPI.\n\nInputs:\n- ``query``: Obsidian search expression.\n- ``context_length``: Max characters of context around each match (default 100).\n\nReturns:\n- Parsed JSON from the API (matches per file, context, score).",
92
+ "description": "Plain-text search (`POST /search/simple/`): every word of `query` must occur, case- and\naccent-insensitive, in the note's path or text; there are no operators. Returns up to 200\nfiles, best first: `[{filename, score, matches: [{match: {start, end}, context}]}]`, at most 20\nmatches per file. `context_length`: characters of context on each side (default 100, max 1000).",
93
93
  "inputSchema": {
94
94
  "additionalProperties": false,
95
95
  "properties": {
@@ -116,7 +116,7 @@ var OPERATIONS = [
116
116
  },
117
117
  {
118
118
  "name": "list_tables",
119
- "description": "List every table: `name`, `catalog`, `schema`, `qualified` (`personal.metrics.mood` \u2014 use it\nwherever a table is expected; `metrics.mood` or `mood` also work when unambiguous), `path` and,\nwhen declared, `description` and `aliases` (other words for what it records). Tables are\ndefined in `database.yaml` (catalog \u2192 schema \u2192 table); legacy schema-note tables have no\n`catalog` and a two-part `qualified` name. `views` lists the stored views (same fields, no\npath): select them like tables. `system.information_schema` is read with `select_rows`.\n\nCall this first to find the table for what the user reports (match their words against\nname, description and aliases), and before creating tables or columns so you reuse existing\nones instead of creating duplicates.",
119
+ "description": "List every table: `name`, `catalog`, `schema`, `qualified` (`personal.metrics.mood` \u2014 use it\nwherever a table is expected; `metrics.mood` or `mood` also work when unambiguous), `path` and,\nwhen declared, `description` and `aliases` (other words for what it records). Tables are\ndefined in `database.yaml` (catalog \u2192 schema \u2192 table). Returns `{tables, views}`: `views` lists the stored views (same fields, no\npath): select them like tables. `system.information_schema` is read with `select_rows`.\n\nCall this first to find the table for what the user reports (match their words against\nname, description and aliases), and before creating tables or columns so you reuse existing\nones instead of creating duplicates.",
120
120
  "inputSchema": {
121
121
  "additionalProperties": false,
122
122
  "properties": {},
@@ -236,7 +236,8 @@ var OPERATIONS = [
236
236
  "type": "null"
237
237
  }
238
238
  ],
239
- "default": null
239
+ "default": null,
240
+ "description": "Untyped fields of the table (name \u2192 default), kept on every row; a field cannot share a name with a column."
240
241
  },
241
242
  "body": {
242
243
  "anyOf": [
@@ -344,7 +345,7 @@ var OPERATIONS = [
344
345
  },
345
346
  {
346
347
  "name": "rename_column",
347
- "description": "Rename a column on the schema and every row, and rewrite the references to it in notes.\nAlso the way to merge a duplicate column into an existing compatible one. Refused (nothing\nwritten) if any row has both columns with different values.",
348
+ "description": "Rename a column in database.yaml and on every row. Refused if `to_column` is already a column\nof the table; a row that has a stray `to_column` key with a different value is a conflict, and\nnothing is written. Views and note text that mention the old name are not rewritten.",
348
349
  "inputSchema": {
349
350
  "additionalProperties": false,
350
351
  "properties": {
@@ -715,7 +716,7 @@ var OPERATIONS = [
715
716
  },
716
717
  {
717
718
  "name": "describe_database",
718
- "description": "Describe the database: its folder (`root`), `metastore` (`database.yaml`), configuration\n(default catalog and schema, row partition and file-name formats, archive folder, time zone),\njournal folder, `bases` (shared column sets and the tables using them), `catalogs` \u2192 schemas \u2192 tables (row counts) and views, legacy schema-note\ntables (`schemas`) and tables outside the database (`outside`).",
719
+ "description": "Describe the database: its folder (`root`), `metastore` (`database.yaml`), configuration\n(default catalog and schema, row partition and file-name formats, archive folder, time zone),\njournal folder, `bases` (shared column sets and the tables using them), `catalogs` \u2192 `schemas` \u2192 `tables` (with `rows` counts) and `views`.",
719
720
  "inputSchema": {
720
721
  "additionalProperties": false,
721
722
  "properties": {},
@@ -731,7 +732,7 @@ var OPERATIONS = [
731
732
  },
732
733
  {
733
734
  "name": "configure_database",
734
- "description": 'Configure the database (persisted in database.yaml `config:`): `root` (rename the database\nfolder; links follow; revert by renaming back), `default_catalog`, `default_schema`,\n`default_partition` ("" = flat) and `filename` (date formats for new time-based tables),\n`archive_folder`, `timezone` (IANA; completes date-times written without offset), `history`\n(transaction log for every table \u2014 only on the user\'s request). Returns describe_database;\nafter a root rename also `rewritten` and `mentions` (show them to the user).',
735
+ "description": 'Configure the database (persisted in database.yaml `config:`): `root` (rename the database\nfolder; links follow; revert by renaming back), `default_catalog`, `default_schema`,\n`default_partition` ("" = flat) and `filename` (date formats for new time-based tables),\n`archive_folder`, `timezone` (IANA; completes date-times written without offset), `history`\n(transaction log for every table \u2014 only on the user\'s request). Returns describe_database.\nRenaming `root` moves the folder (links to its notes are rewritten); it is not journaled: to\nrevert, call it again with the old value.',
735
736
  "inputSchema": {
736
737
  "additionalProperties": false,
737
738
  "properties": {
@@ -906,7 +907,8 @@ var OPERATIONS = [
906
907
  "type": "null"
907
908
  }
908
909
  ],
909
- "default": null
910
+ "default": null,
911
+ "description": "Optional note path (vault-relative), instead of the table's file-name pattern; `.md` is added if missing; it must stay inside the vault."
910
912
  },
911
913
  "dry_run": {
912
914
  "default": false,
@@ -1144,7 +1146,7 @@ var OPERATIONS = [
1144
1146
  "title": "Update Row",
1145
1147
  "readOnlyHint": false,
1146
1148
  "destructiveHint": false,
1147
- "idempotentHint": true,
1149
+ "idempotentHint": false,
1148
1150
  "openWorldHint": false
1149
1151
  }
1150
1152
  },
@@ -1298,7 +1300,7 @@ var OPERATIONS = [
1298
1300
  },
1299
1301
  {
1300
1302
  "name": "adopt_rows",
1301
- "description": 'Turn existing notes (not rows of any table yet) into rows of `table`, keeping their body and\nlinks: `rename` keys (`{"data": "occurred_at"}`), `set_values` for all, `per_row` values by\nnote path (`{"Home/x.md": {"zone": "[[Kitchen]]"}}`), `drop` keys the table does not have.\nEvery note gets `table` and a system `id`; with `relocate` (default) it moves to the table\'s\nfolder, named by its filename pattern (Obsidian updates links). All notes are validated first:\nunknown keys and bad values are listed and nothing is written. Run with `dry_run` first; undo\nwith `undo_migration`.',
1303
+ "description": 'Turn existing notes (not rows of any table yet) into rows of `table`, keeping their body and\nlinks: `rename` keys (`{"data": "occurred_at"}`), `set_values` for all, `per_row` values by\nnote path (`{"Home/x.md": {"zone": "[[Kitchen]]"}}`), `drop` keys the table does not have.\nEvery note gets `table` and a system `id`; with `relocate` (default) it moves to the table\'s\nfolder, named by its filename pattern (links to it are rewritten). All notes are validated first:\nunknown keys and bad values are listed and nothing is written. Run with `dry_run` first; undo\nwith `undo_migration`.',
1302
1304
  "inputSchema": {
1303
1305
  "additionalProperties": false,
1304
1306
  "properties": {
@@ -1601,7 +1603,7 @@ var OPERATIONS = [
1601
1603
  "annotations": {
1602
1604
  "title": "Undo Migration",
1603
1605
  "readOnlyHint": false,
1604
- "destructiveHint": false,
1606
+ "destructiveHint": true,
1605
1607
  "idempotentHint": false,
1606
1608
  "openWorldHint": false
1607
1609
  }
@@ -1766,7 +1768,7 @@ var OPERATIONS = [
1766
1768
  },
1767
1769
  {
1768
1770
  "name": "semantic_search",
1769
- "description": 'Find notes by meaning, not exact words: `{query, results: [{path, score, heading, snippet,\ntable?}]}`, one result per note (its best passage), best first. Use it for vague questions\n("the dinner where we talked about moving", "what did I decide about the car") and to find the\nrows to read or update; then read_note / select_rows for the details. Filters: `tables`\n(qualified or short names), `folder`, `min_score` (cosine 0\u20131; ~0.5+ is usually relevant).\nFails with a clear message when semantic search is off in the plugin settings.',
1771
+ "description": 'Find notes by meaning, not exact words: `{query, results: [{path, score, heading, snippet,\ntable?}]}`, one result per note (its best passage), best first. Use it for vague questions\n("the dinner where we talked about moving", "what did I decide about the car") and to find the\nrows to read or update; then read_note / select_rows for the details. Filters: `tables`\n(qualified or short names), `folder`, `min_score` (cosine 0\u20131; ~0.5+ is usually relevant).\n`limit`: default 10, max 100. Fails with a clear message when semantic search is not enabled in\nthe engine settings.',
1770
1772
  "inputSchema": {
1771
1773
  "additionalProperties": false,
1772
1774
  "properties": {
@@ -1829,7 +1831,7 @@ var OPERATIONS = [
1829
1831
  },
1830
1832
  {
1831
1833
  "name": "semantic_index",
1832
- "description": "Semantic search index: `status` (provider, model, notes, passages, last error), `update`\n(embed notes changed since the last run; the plugin also does it on its own a few seconds after\nedits) or `rebuild` (re-embed everything: slow, and costs money with a paid provider \u2014 ask\nfirst).",
1834
+ "description": "Semantic search index: `status` (`enabled`, `provider`, `model`, `files`, `passages`, `pending`,\n`running`, `lastSync`, `lastError`), `update` (embed notes changed since the last run; the\nengine also does it on its own about ten seconds after edits) or `rebuild` (re-embed\neverything: slow, and costs money with a paid provider \u2014 ask first).",
1833
1835
  "inputSchema": {
1834
1836
  "additionalProperties": false,
1835
1837
  "properties": {
@@ -1975,7 +1977,7 @@ var OPERATIONS = [
1975
1977
  },
1976
1978
  {
1977
1979
  "name": "describe_history",
1978
- "description": "DESCRIBE HISTORY: the table's versions, newest first \u2014 `version`, `timestamp`, `operation`\n(CREATE TABLE, WRITE, UPDATE, DELETE, MERGE, ALTER TABLE, ADD COLUMNS, RESTORE, UNDO, \u2026;\n`EXTERNAL_EDIT` = changed by hand in Obsidian), `operationMetrics` and `migrationId` (undo).\nWorks for dropped tables too (full `catalog.schema.table`). Use a version with `select_rows`\n(time travel), `table_changes` or `restore_table`.",
1980
+ "description": "DESCRIBE HISTORY: the table's versions, newest first \u2014 `version`, `timestamp`, `operation`\n(CREATE TABLE, WRITE, UPDATE, DELETE, MERGE, ALTER TABLE, ADD COLUMNS, RESTORE, UNDO, \u2026;\n`EXTERNAL_EDIT` = changed outside the engine: an editor, git, sync), `operationMetrics` and `migrationId` (undo).\nWorks for dropped tables too (full `catalog.schema.table`). Use a version with `select_rows`\n(time travel), `table_changes` or `restore_table`.",
1979
1981
  "inputSchema": {
1980
1982
  "additionalProperties": false,
1981
1983
  "properties": {
@@ -2047,7 +2049,7 @@ var OPERATIONS = [
2047
2049
  },
2048
2050
  {
2049
2051
  "name": "list_changes",
2050
- "description": "Change feed across all tables: numbered row changes (`seq`), oldest first, from engine writes\nand from hand edits (`source: external`). Each has `table`, `op` (insert, update, delete, move,\nschema), `path`, `changes` ({column: {before, after}}), `actor`, `migration`. MCP tool calls are `op: tool_call` events (`call`: tool, args, ok, value, servedBy), returned only when `ops` includes tool_call. Pass the returned\n`last` as `since` next time (`head` is the newest number); `wait_ms` waits for a new change (long poll). `gap: true` means\nolder changes were dropped: re-read the tables. `where` filters rows as in select_rows (on the values after the change). Per-table versions: `table_changes`.",
2052
+ "description": "Change feed across all tables: numbered row changes (`seq`), oldest first, from engine writes\nand from hand edits (`source: external`). Each has `table`, `op` (insert, update, delete, move,\nschema), `path`, `changes` ({column: {before, after}}), `actor`, `migration`. Agents' tool calls (reported by MCP servers) are `op: tool_call` events (`call`: tool, args, ok, value, servedBy), returned only when `ops` includes tool_call. Pass the returned\n`last` as `since` next time (`head` is the newest number); `wait_ms` (max 60000) waits for a new change (long poll); `limit` max 5000. `gap: true` means\nolder changes were dropped: re-read the tables. `where` filters rows as in select_rows (on the values after the change). Per-table versions: `table_changes`.",
2051
2053
  "inputSchema": {
2052
2054
  "additionalProperties": false,
2053
2055
  "properties": {
@@ -2573,7 +2575,7 @@ var OPERATIONS = [
2573
2575
  },
2574
2576
  {
2575
2577
  "name": "clone_table",
2576
- "description": "CREATE TABLE `target` CLONE `source`: same definition plus independent copies of its rows\n(deep clone), optionally as of a `version` / `timestamp`. `shallow=true` copies the\ndefinition only (CREATE TABLE LIKE). `target`: `catalog.schema.table`, `schema.table` (default\ncatalog) or a name (source's catalog and schema). Useful for experiments, e.g. a sandbox copy\nin the `agent` catalog.",
2578
+ "description": "CREATE TABLE `target` CLONE `source`: same definition plus independent copies of its rows\n(deep clone), optionally as of a `version` / `timestamp`. `shallow=true` copies the\ndefinition only (CREATE TABLE LIKE). `target`: `catalog.schema.table`, `schema.table` (in the\nsource's catalog) or a name (source's catalog and schema). Useful for experiments, e.g. a sandbox copy\nin the `agent` catalog.",
2577
2579
  "inputSchema": {
2578
2580
  "additionalProperties": false,
2579
2581
  "properties": {
@@ -2630,7 +2632,7 @@ var OPERATIONS = [
2630
2632
  },
2631
2633
  {
2632
2634
  "name": "recompute_columns",
2633
- "description": "Bring generated and identity columns up to date (all rows, or only `paths`), e.g. after\nrows were edited outside the plugin or a generated expression changed with `alter_column`.\nAlso gives rows without an `id` a new one, and a duplicated note (same `id` as an older row)\na new id. `new_ids=true` regenerates every id in scope: only on the user's explicit request.\n`check_table` reports stale values with code `generated`. No-op when nothing is stale;\notherwise journaled (undo_migration) and a RECOMPUTE version in the table's history.",
2635
+ "description": "Bring generated and identity columns up to date (all rows, or only `paths`), e.g. after\nrows were edited by hand (an editor, git, sync) or a generated expression changed with `alter_column`.\nAlso gives rows without an `id` a new one, and a duplicated note (same `id` as an older row)\na new id. `new_ids=true` regenerates every id in scope (new ones on every call): only on the user's\nexplicit request.\n`check_table` reports stale values with code `generated`. No-op when nothing is stale;\notherwise journaled (undo_migration) and a RECOMPUTE version in the table's history.",
2634
2636
  "inputSchema": {
2635
2637
  "additionalProperties": false,
2636
2638
  "properties": {
@@ -2669,7 +2671,7 @@ var OPERATIONS = [
2669
2671
  "title": "Recompute Columns",
2670
2672
  "readOnlyHint": false,
2671
2673
  "destructiveHint": false,
2672
- "idempotentHint": true,
2674
+ "idempotentHint": false,
2673
2675
  "openWorldHint": false
2674
2676
  }
2675
2677
  },
@@ -2834,7 +2836,7 @@ var OPERATIONS = [
2834
2836
  },
2835
2837
  {
2836
2838
  "name": "graph_path",
2837
- "description": 'Shortest chain of links between two sets of notes (each `{"paths": [...]}` and/or\n`{"table", "where"}`): `found`, `length` and the steps (`from`, `label`, `direction`, `to`).\n`direction` both by default; `max_depth` default 6 (max 10).',
2839
+ "description": 'Shortest chain of links between two sets of notes (each `{"paths": [...]}` and/or\n`{"table", "where"}`): `found`, `length` and `path`: the steps (`from`, `label`, `direction`, `to`).\n`direction` both by default; `max_depth` default 6 (max 10).',
2838
2840
  "inputSchema": {
2839
2841
  "additionalProperties": false,
2840
2842
  "properties": {
@@ -2972,7 +2974,8 @@ var ROUTES = {
2972
2974
  create_webhook: (a) => ["/webhooks/create/", pick(a, ["url", "tables", "ops", "columns", "exclude_actor", "where", "since"])],
2973
2975
  delete_webhook: (a) => ["/webhooks/delete/", pick(a, ["id"])],
2974
2976
  restore_table: (a) => ["/history/restore/", { ...pick(a, ["table", "version", "timestamp"]), ...dry(a) }],
2975
- vacuum_table: (a) => ["/history/vacuum/", { ...pick(a, ["table", "retain_from_version"]), ...dry(a) }],
2977
+ // vacuum deletes history for good: a dry run unless the caller says `dry_run: false`
2978
+ vacuum_table: (a) => ["/history/vacuum/", { ...pick(a, ["table", "retain_from_version"]), dry_run: a.dry_run !== false }],
2976
2979
  graph_neighbors: (a) => ["/graph/neighbors/", compact({ from: a.from_nodes, direction: a.direction, edges: a.edges })],
2977
2980
  graph_traverse: (a) => [
2978
2981
  "/graph/traverse/",
package/dist/api/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  var OPERATIONS = [
3
3
  {
4
4
  "name": "read_note",
5
- "description": "Read a vault file (``GET /vault/{filename}``).\n\nDefault response is raw markdown. With ``as_json`` true, requests ``Accept: application/vnd.olrapi.note+json`` and returns ``NoteJson``: ``content``, ``frontmatter``, ``tags``, ``path``, and ``stat`` (file times and size), per the plugin OpenAPI schema.\n\nInputs:\n- ``path``: Vault-relative path to the file (forward slashes).\n- ``as_json``: If false (default), return markdown. If true, return ``NoteJson``.\n\nReturns:\n- ``str`` \u2014 markdown body, or\n- ``dict[str, Any]`` \u2014 parsed ``NoteJson`` when ``as_json`` is true.",
5
+ "description": "Read a vault file (`GET /vault/{path}`). Default: the raw markdown. With `as_json`:\n`{content, frontmatter, tags, path, stat}` (`tags` without `#`, from the text and the\nfrontmatter; `stat`: `{ctime, mtime, size}` or null). A missing file is an error.\n`path`: vault-relative, forward slashes.",
6
6
  "inputSchema": {
7
7
  "additionalProperties": false,
8
8
  "properties": {
@@ -29,7 +29,7 @@ var OPERATIONS = [
29
29
  },
30
30
  {
31
31
  "name": "list_directory",
32
- "description": 'List files under a vault directory (``GET /vault/`` or ``GET /vault/{pathToDirectory}/``).\n\nResponse JSON includes a ``files`` array. Names ending with ``/`` are subdirectories. Empty directories may not appear in listings (per API). Use ``""`` for the vault root.\n\nInputs:\n- ``path``: Vault-relative directory path; use ``""`` for the vault root.\n\nReturns:\n- ``dict[str, Any]`` \u2014 parsed JSON listing from the API.',
32
+ "description": 'Direct children of a vault folder (`GET /vault/{folder}/`): `{files: [...]}`. Folders end with\n`/`, names are sorted, dot-files are hidden. `""` is the vault root. A missing folder is an\nerror.',
33
33
  "inputSchema": {
34
34
  "additionalProperties": false,
35
35
  "properties": {
@@ -50,7 +50,7 @@ var OPERATIONS = [
50
50
  },
51
51
  {
52
52
  "name": "search_simple",
53
- "description": "Full-text search (``POST /search/simple/``).\n\nUses Obsidian\u2019s native search syntax (keywords, ``path:``, ``tag:``, ``file:``, etc.). Results include filenames, match positions, context snippets, and scores per the plugin OpenAPI.\n\nInputs:\n- ``query``: Obsidian search expression.\n- ``context_length``: Max characters of context around each match (default 100).\n\nReturns:\n- Parsed JSON from the API (matches per file, context, score).",
53
+ "description": "Plain-text search (`POST /search/simple/`): every word of `query` must occur, case- and\naccent-insensitive, in the note's path or text; there are no operators. Returns up to 200\nfiles, best first: `[{filename, score, matches: [{match: {start, end}, context}]}]`, at most 20\nmatches per file. `context_length`: characters of context on each side (default 100, max 1000).",
54
54
  "inputSchema": {
55
55
  "additionalProperties": false,
56
56
  "properties": {
@@ -77,7 +77,7 @@ var OPERATIONS = [
77
77
  },
78
78
  {
79
79
  "name": "list_tables",
80
- "description": "List every table: `name`, `catalog`, `schema`, `qualified` (`personal.metrics.mood` \u2014 use it\nwherever a table is expected; `metrics.mood` or `mood` also work when unambiguous), `path` and,\nwhen declared, `description` and `aliases` (other words for what it records). Tables are\ndefined in `database.yaml` (catalog \u2192 schema \u2192 table); legacy schema-note tables have no\n`catalog` and a two-part `qualified` name. `views` lists the stored views (same fields, no\npath): select them like tables. `system.information_schema` is read with `select_rows`.\n\nCall this first to find the table for what the user reports (match their words against\nname, description and aliases), and before creating tables or columns so you reuse existing\nones instead of creating duplicates.",
80
+ "description": "List every table: `name`, `catalog`, `schema`, `qualified` (`personal.metrics.mood` \u2014 use it\nwherever a table is expected; `metrics.mood` or `mood` also work when unambiguous), `path` and,\nwhen declared, `description` and `aliases` (other words for what it records). Tables are\ndefined in `database.yaml` (catalog \u2192 schema \u2192 table). Returns `{tables, views}`: `views` lists the stored views (same fields, no\npath): select them like tables. `system.information_schema` is read with `select_rows`.\n\nCall this first to find the table for what the user reports (match their words against\nname, description and aliases), and before creating tables or columns so you reuse existing\nones instead of creating duplicates.",
81
81
  "inputSchema": {
82
82
  "additionalProperties": false,
83
83
  "properties": {},
@@ -197,7 +197,8 @@ var OPERATIONS = [
197
197
  "type": "null"
198
198
  }
199
199
  ],
200
- "default": null
200
+ "default": null,
201
+ "description": "Untyped fields of the table (name \u2192 default), kept on every row; a field cannot share a name with a column."
201
202
  },
202
203
  "body": {
203
204
  "anyOf": [
@@ -305,7 +306,7 @@ var OPERATIONS = [
305
306
  },
306
307
  {
307
308
  "name": "rename_column",
308
- "description": "Rename a column on the schema and every row, and rewrite the references to it in notes.\nAlso the way to merge a duplicate column into an existing compatible one. Refused (nothing\nwritten) if any row has both columns with different values.",
309
+ "description": "Rename a column in database.yaml and on every row. Refused if `to_column` is already a column\nof the table; a row that has a stray `to_column` key with a different value is a conflict, and\nnothing is written. Views and note text that mention the old name are not rewritten.",
309
310
  "inputSchema": {
310
311
  "additionalProperties": false,
311
312
  "properties": {
@@ -676,7 +677,7 @@ var OPERATIONS = [
676
677
  },
677
678
  {
678
679
  "name": "describe_database",
679
- "description": "Describe the database: its folder (`root`), `metastore` (`database.yaml`), configuration\n(default catalog and schema, row partition and file-name formats, archive folder, time zone),\njournal folder, `bases` (shared column sets and the tables using them), `catalogs` \u2192 schemas \u2192 tables (row counts) and views, legacy schema-note\ntables (`schemas`) and tables outside the database (`outside`).",
680
+ "description": "Describe the database: its folder (`root`), `metastore` (`database.yaml`), configuration\n(default catalog and schema, row partition and file-name formats, archive folder, time zone),\njournal folder, `bases` (shared column sets and the tables using them), `catalogs` \u2192 `schemas` \u2192 `tables` (with `rows` counts) and `views`.",
680
681
  "inputSchema": {
681
682
  "additionalProperties": false,
682
683
  "properties": {},
@@ -692,7 +693,7 @@ var OPERATIONS = [
692
693
  },
693
694
  {
694
695
  "name": "configure_database",
695
- "description": 'Configure the database (persisted in database.yaml `config:`): `root` (rename the database\nfolder; links follow; revert by renaming back), `default_catalog`, `default_schema`,\n`default_partition` ("" = flat) and `filename` (date formats for new time-based tables),\n`archive_folder`, `timezone` (IANA; completes date-times written without offset), `history`\n(transaction log for every table \u2014 only on the user\'s request). Returns describe_database;\nafter a root rename also `rewritten` and `mentions` (show them to the user).',
696
+ "description": 'Configure the database (persisted in database.yaml `config:`): `root` (rename the database\nfolder; links follow; revert by renaming back), `default_catalog`, `default_schema`,\n`default_partition` ("" = flat) and `filename` (date formats for new time-based tables),\n`archive_folder`, `timezone` (IANA; completes date-times written without offset), `history`\n(transaction log for every table \u2014 only on the user\'s request). Returns describe_database.\nRenaming `root` moves the folder (links to its notes are rewritten); it is not journaled: to\nrevert, call it again with the old value.',
696
697
  "inputSchema": {
697
698
  "additionalProperties": false,
698
699
  "properties": {
@@ -867,7 +868,8 @@ var OPERATIONS = [
867
868
  "type": "null"
868
869
  }
869
870
  ],
870
- "default": null
871
+ "default": null,
872
+ "description": "Optional note path (vault-relative), instead of the table's file-name pattern; `.md` is added if missing; it must stay inside the vault."
871
873
  },
872
874
  "dry_run": {
873
875
  "default": false,
@@ -1105,7 +1107,7 @@ var OPERATIONS = [
1105
1107
  "title": "Update Row",
1106
1108
  "readOnlyHint": false,
1107
1109
  "destructiveHint": false,
1108
- "idempotentHint": true,
1110
+ "idempotentHint": false,
1109
1111
  "openWorldHint": false
1110
1112
  }
1111
1113
  },
@@ -1259,7 +1261,7 @@ var OPERATIONS = [
1259
1261
  },
1260
1262
  {
1261
1263
  "name": "adopt_rows",
1262
- "description": 'Turn existing notes (not rows of any table yet) into rows of `table`, keeping their body and\nlinks: `rename` keys (`{"data": "occurred_at"}`), `set_values` for all, `per_row` values by\nnote path (`{"Home/x.md": {"zone": "[[Kitchen]]"}}`), `drop` keys the table does not have.\nEvery note gets `table` and a system `id`; with `relocate` (default) it moves to the table\'s\nfolder, named by its filename pattern (Obsidian updates links). All notes are validated first:\nunknown keys and bad values are listed and nothing is written. Run with `dry_run` first; undo\nwith `undo_migration`.',
1264
+ "description": 'Turn existing notes (not rows of any table yet) into rows of `table`, keeping their body and\nlinks: `rename` keys (`{"data": "occurred_at"}`), `set_values` for all, `per_row` values by\nnote path (`{"Home/x.md": {"zone": "[[Kitchen]]"}}`), `drop` keys the table does not have.\nEvery note gets `table` and a system `id`; with `relocate` (default) it moves to the table\'s\nfolder, named by its filename pattern (links to it are rewritten). All notes are validated first:\nunknown keys and bad values are listed and nothing is written. Run with `dry_run` first; undo\nwith `undo_migration`.',
1263
1265
  "inputSchema": {
1264
1266
  "additionalProperties": false,
1265
1267
  "properties": {
@@ -1562,7 +1564,7 @@ var OPERATIONS = [
1562
1564
  "annotations": {
1563
1565
  "title": "Undo Migration",
1564
1566
  "readOnlyHint": false,
1565
- "destructiveHint": false,
1567
+ "destructiveHint": true,
1566
1568
  "idempotentHint": false,
1567
1569
  "openWorldHint": false
1568
1570
  }
@@ -1727,7 +1729,7 @@ var OPERATIONS = [
1727
1729
  },
1728
1730
  {
1729
1731
  "name": "semantic_search",
1730
- "description": 'Find notes by meaning, not exact words: `{query, results: [{path, score, heading, snippet,\ntable?}]}`, one result per note (its best passage), best first. Use it for vague questions\n("the dinner where we talked about moving", "what did I decide about the car") and to find the\nrows to read or update; then read_note / select_rows for the details. Filters: `tables`\n(qualified or short names), `folder`, `min_score` (cosine 0\u20131; ~0.5+ is usually relevant).\nFails with a clear message when semantic search is off in the plugin settings.',
1732
+ "description": 'Find notes by meaning, not exact words: `{query, results: [{path, score, heading, snippet,\ntable?}]}`, one result per note (its best passage), best first. Use it for vague questions\n("the dinner where we talked about moving", "what did I decide about the car") and to find the\nrows to read or update; then read_note / select_rows for the details. Filters: `tables`\n(qualified or short names), `folder`, `min_score` (cosine 0\u20131; ~0.5+ is usually relevant).\n`limit`: default 10, max 100. Fails with a clear message when semantic search is not enabled in\nthe engine settings.',
1731
1733
  "inputSchema": {
1732
1734
  "additionalProperties": false,
1733
1735
  "properties": {
@@ -1790,7 +1792,7 @@ var OPERATIONS = [
1790
1792
  },
1791
1793
  {
1792
1794
  "name": "semantic_index",
1793
- "description": "Semantic search index: `status` (provider, model, notes, passages, last error), `update`\n(embed notes changed since the last run; the plugin also does it on its own a few seconds after\nedits) or `rebuild` (re-embed everything: slow, and costs money with a paid provider \u2014 ask\nfirst).",
1795
+ "description": "Semantic search index: `status` (`enabled`, `provider`, `model`, `files`, `passages`, `pending`,\n`running`, `lastSync`, `lastError`), `update` (embed notes changed since the last run; the\nengine also does it on its own about ten seconds after edits) or `rebuild` (re-embed\neverything: slow, and costs money with a paid provider \u2014 ask first).",
1794
1796
  "inputSchema": {
1795
1797
  "additionalProperties": false,
1796
1798
  "properties": {
@@ -1936,7 +1938,7 @@ var OPERATIONS = [
1936
1938
  },
1937
1939
  {
1938
1940
  "name": "describe_history",
1939
- "description": "DESCRIBE HISTORY: the table's versions, newest first \u2014 `version`, `timestamp`, `operation`\n(CREATE TABLE, WRITE, UPDATE, DELETE, MERGE, ALTER TABLE, ADD COLUMNS, RESTORE, UNDO, \u2026;\n`EXTERNAL_EDIT` = changed by hand in Obsidian), `operationMetrics` and `migrationId` (undo).\nWorks for dropped tables too (full `catalog.schema.table`). Use a version with `select_rows`\n(time travel), `table_changes` or `restore_table`.",
1941
+ "description": "DESCRIBE HISTORY: the table's versions, newest first \u2014 `version`, `timestamp`, `operation`\n(CREATE TABLE, WRITE, UPDATE, DELETE, MERGE, ALTER TABLE, ADD COLUMNS, RESTORE, UNDO, \u2026;\n`EXTERNAL_EDIT` = changed outside the engine: an editor, git, sync), `operationMetrics` and `migrationId` (undo).\nWorks for dropped tables too (full `catalog.schema.table`). Use a version with `select_rows`\n(time travel), `table_changes` or `restore_table`.",
1940
1942
  "inputSchema": {
1941
1943
  "additionalProperties": false,
1942
1944
  "properties": {
@@ -2008,7 +2010,7 @@ var OPERATIONS = [
2008
2010
  },
2009
2011
  {
2010
2012
  "name": "list_changes",
2011
- "description": "Change feed across all tables: numbered row changes (`seq`), oldest first, from engine writes\nand from hand edits (`source: external`). Each has `table`, `op` (insert, update, delete, move,\nschema), `path`, `changes` ({column: {before, after}}), `actor`, `migration`. MCP tool calls are `op: tool_call` events (`call`: tool, args, ok, value, servedBy), returned only when `ops` includes tool_call. Pass the returned\n`last` as `since` next time (`head` is the newest number); `wait_ms` waits for a new change (long poll). `gap: true` means\nolder changes were dropped: re-read the tables. `where` filters rows as in select_rows (on the values after the change). Per-table versions: `table_changes`.",
2013
+ "description": "Change feed across all tables: numbered row changes (`seq`), oldest first, from engine writes\nand from hand edits (`source: external`). Each has `table`, `op` (insert, update, delete, move,\nschema), `path`, `changes` ({column: {before, after}}), `actor`, `migration`. Agents' tool calls (reported by MCP servers) are `op: tool_call` events (`call`: tool, args, ok, value, servedBy), returned only when `ops` includes tool_call. Pass the returned\n`last` as `since` next time (`head` is the newest number); `wait_ms` (max 60000) waits for a new change (long poll); `limit` max 5000. `gap: true` means\nolder changes were dropped: re-read the tables. `where` filters rows as in select_rows (on the values after the change). Per-table versions: `table_changes`.",
2012
2014
  "inputSchema": {
2013
2015
  "additionalProperties": false,
2014
2016
  "properties": {
@@ -2534,7 +2536,7 @@ var OPERATIONS = [
2534
2536
  },
2535
2537
  {
2536
2538
  "name": "clone_table",
2537
- "description": "CREATE TABLE `target` CLONE `source`: same definition plus independent copies of its rows\n(deep clone), optionally as of a `version` / `timestamp`. `shallow=true` copies the\ndefinition only (CREATE TABLE LIKE). `target`: `catalog.schema.table`, `schema.table` (default\ncatalog) or a name (source's catalog and schema). Useful for experiments, e.g. a sandbox copy\nin the `agent` catalog.",
2539
+ "description": "CREATE TABLE `target` CLONE `source`: same definition plus independent copies of its rows\n(deep clone), optionally as of a `version` / `timestamp`. `shallow=true` copies the\ndefinition only (CREATE TABLE LIKE). `target`: `catalog.schema.table`, `schema.table` (in the\nsource's catalog) or a name (source's catalog and schema). Useful for experiments, e.g. a sandbox copy\nin the `agent` catalog.",
2538
2540
  "inputSchema": {
2539
2541
  "additionalProperties": false,
2540
2542
  "properties": {
@@ -2591,7 +2593,7 @@ var OPERATIONS = [
2591
2593
  },
2592
2594
  {
2593
2595
  "name": "recompute_columns",
2594
- "description": "Bring generated and identity columns up to date (all rows, or only `paths`), e.g. after\nrows were edited outside the plugin or a generated expression changed with `alter_column`.\nAlso gives rows without an `id` a new one, and a duplicated note (same `id` as an older row)\na new id. `new_ids=true` regenerates every id in scope: only on the user's explicit request.\n`check_table` reports stale values with code `generated`. No-op when nothing is stale;\notherwise journaled (undo_migration) and a RECOMPUTE version in the table's history.",
2596
+ "description": "Bring generated and identity columns up to date (all rows, or only `paths`), e.g. after\nrows were edited by hand (an editor, git, sync) or a generated expression changed with `alter_column`.\nAlso gives rows without an `id` a new one, and a duplicated note (same `id` as an older row)\na new id. `new_ids=true` regenerates every id in scope (new ones on every call): only on the user's\nexplicit request.\n`check_table` reports stale values with code `generated`. No-op when nothing is stale;\notherwise journaled (undo_migration) and a RECOMPUTE version in the table's history.",
2595
2597
  "inputSchema": {
2596
2598
  "additionalProperties": false,
2597
2599
  "properties": {
@@ -2630,7 +2632,7 @@ var OPERATIONS = [
2630
2632
  "title": "Recompute Columns",
2631
2633
  "readOnlyHint": false,
2632
2634
  "destructiveHint": false,
2633
- "idempotentHint": true,
2635
+ "idempotentHint": false,
2634
2636
  "openWorldHint": false
2635
2637
  }
2636
2638
  },
@@ -2795,7 +2797,7 @@ var OPERATIONS = [
2795
2797
  },
2796
2798
  {
2797
2799
  "name": "graph_path",
2798
- "description": 'Shortest chain of links between two sets of notes (each `{"paths": [...]}` and/or\n`{"table", "where"}`): `found`, `length` and the steps (`from`, `label`, `direction`, `to`).\n`direction` both by default; `max_depth` default 6 (max 10).',
2800
+ "description": 'Shortest chain of links between two sets of notes (each `{"paths": [...]}` and/or\n`{"table", "where"}`): `found`, `length` and `path`: the steps (`from`, `label`, `direction`, `to`).\n`direction` both by default; `max_depth` default 6 (max 10).',
2799
2801
  "inputSchema": {
2800
2802
  "additionalProperties": false,
2801
2803
  "properties": {
@@ -2933,7 +2935,8 @@ var ROUTES = {
2933
2935
  create_webhook: (a) => ["/webhooks/create/", pick(a, ["url", "tables", "ops", "columns", "exclude_actor", "where", "since"])],
2934
2936
  delete_webhook: (a) => ["/webhooks/delete/", pick(a, ["id"])],
2935
2937
  restore_table: (a) => ["/history/restore/", { ...pick(a, ["table", "version", "timestamp"]), ...dry(a) }],
2936
- vacuum_table: (a) => ["/history/vacuum/", { ...pick(a, ["table", "retain_from_version"]), ...dry(a) }],
2938
+ // vacuum deletes history for good: a dry run unless the caller says `dry_run: false`
2939
+ vacuum_table: (a) => ["/history/vacuum/", { ...pick(a, ["table", "retain_from_version"]), dry_run: a.dry_run !== false }],
2937
2940
  graph_neighbors: (a) => ["/graph/neighbors/", compact({ from: a.from_nodes, direction: a.direction, edges: a.edges })],
2938
2941
  graph_traverse: (a) => [
2939
2942
  "/graph/traverse/",
@@ -40,8 +40,7 @@ var import_node_os = __toESM(require("node:os"), 1);
40
40
  var import_node_path = __toESM(require("node:path"), 1);
41
41
  var LOCK_STALE_MS = 3e4;
42
42
  function vaultSettingsPath(vault) {
43
- const plugin = import_node_path.default.join(vault, ".obsidian", "plugins", "datanotes", "data.json");
44
- return import_node_fs.default.existsSync(plugin) ? plugin : import_node_path.default.join(vault, ".datanotes", "service.json");
43
+ return import_node_path.default.join(vault, ".datanotes", "service.json");
45
44
  }
46
45
  function readJson(file) {
47
46
  try {
@@ -4,7 +4,7 @@
4
4
  */
5
5
  /** The lock an engine writes in its store folder (`<vault>/.datanotes/engine.lock`). */
6
6
  export interface EngineLock {
7
- kind: "service" | "plugin";
7
+ kind: "service";
8
8
  pid: number;
9
9
  host: string;
10
10
  port: number;
@@ -18,7 +18,7 @@ export interface LocalEngine {
18
18
  token: string;
19
19
  lock: EngineLock;
20
20
  }
21
- /** The settings file of a vault: the Obsidian plugin's when present, else the service's. */
21
+ /** The settings file of a vault's engine. */
22
22
  export declare function vaultSettingsPath(vault: string): string;
23
23
  /**
24
24
  * The engine holding `vault`, or null when none runs. Throws when the lock is fresh but held on
package/dist/api/local.js CHANGED
@@ -4,8 +4,7 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  var LOCK_STALE_MS = 3e4;
6
6
  function vaultSettingsPath(vault) {
7
- const plugin = path.join(vault, ".obsidian", "plugins", "datanotes", "data.json");
8
- return fs.existsSync(plugin) ? plugin : path.join(vault, ".datanotes", "service.json");
7
+ return path.join(vault, ".datanotes", "service.json");
9
8
  }
10
9
  function readJson(file) {
11
10
  try {
@@ -7424,19 +7424,16 @@ var DEFAULT_SETTINGS = {
7424
7424
  tableProperty: "table",
7425
7425
  telemetryEnabled: false,
7426
7426
  telemetryEndpoint: "http://127.0.0.1:4318",
7427
- telemetryServiceName: "obsidian-datanotes",
7428
- restApiEnabled: false,
7427
+ telemetryServiceName: "datanotes",
7429
7428
  autoRecompute: true,
7430
7429
  viewNotes: true,
7431
7430
  semanticEnabled: false,
7432
7431
  semanticProvider: "ollama",
7433
7432
  semanticBaseUrl: "",
7434
7433
  semanticModel: "",
7435
- semanticApiKeySecret: "",
7436
7434
  semanticScope: "vault",
7437
7435
  semanticExclude: [],
7438
7436
  semanticBatchSize: 32,
7439
- httpServerEnabled: false,
7440
7437
  httpServerPort: 27150,
7441
7438
  httpServerToken: ""
7442
7439
  };
@@ -7453,18 +7450,15 @@ var KEYS = [
7453
7450
  "telemetryEnabled",
7454
7451
  "telemetryEndpoint",
7455
7452
  "telemetryServiceName",
7456
- "restApiEnabled",
7457
7453
  "autoRecompute",
7458
7454
  "viewNotes",
7459
7455
  "semanticEnabled",
7460
7456
  "semanticProvider",
7461
7457
  "semanticBaseUrl",
7462
7458
  "semanticModel",
7463
- "semanticApiKeySecret",
7464
7459
  "semanticScope",
7465
7460
  "semanticExclude",
7466
7461
  "semanticBatchSize",
7467
- "httpServerEnabled",
7468
7462
  "httpServerPort",
7469
7463
  "httpServerToken"
7470
7464
  ];
@@ -7548,9 +7542,8 @@ function mergeWithDefaults(loaded) {
7548
7542
  if (typeof raw === "string" && (raw.trim() === "" || /^https?:\/\/\S+$/.test(raw.trim()))) out.semanticBaseUrl = raw.trim().replace(/\/+$/, "");
7549
7543
  break;
7550
7544
  }
7551
- case "semanticModel":
7552
- case "semanticApiKeySecret": {
7553
- if (typeof raw === "string") out[key] = raw.trim();
7545
+ case "semanticModel": {
7546
+ if (typeof raw === "string") out.semanticModel = raw.trim();
7554
7547
  break;
7555
7548
  }
7556
7549
  case "semanticScope": {
@@ -7574,9 +7567,7 @@ function mergeWithDefaults(loaded) {
7574
7567
  break;
7575
7568
  }
7576
7569
  case "telemetryEnabled":
7577
- case "httpServerEnabled":
7578
7570
  case "fileLogEnabled":
7579
- case "restApiEnabled":
7580
7571
  case "semanticEnabled":
7581
7572
  case "autoRecompute":
7582
7573
  case "viewNotes": {
@@ -7833,10 +7824,6 @@ var LockHolder = class {
7833
7824
  async write() {
7834
7825
  this.info.heartbeat = (/* @__PURE__ */ new Date()).toISOString();
7835
7826
  try {
7836
- if (this.info.kind === "plugin") {
7837
- const current2 = await readLock(this.adapter, this.storeFolder);
7838
- if (current2 && current2.kind === "service" && isFresh(current2)) return;
7839
- }
7840
7827
  await this.adapter.mkdir(this.storeFolder);
7841
7828
  await this.adapter.write(lockPath(this.storeFolder), JSON.stringify(this.info));
7842
7829
  } catch {
@@ -10104,7 +10091,7 @@ async function tableStateAt(app, settings, full, asOf2 = {}) {
10104
10091
  if (versions.length === 0) {
10105
10092
  if (asOf2.version !== void 0 || asOf2.timestamp !== void 0) {
10106
10093
  throw new HistoryError(
10107
- `${full} has no history: time travel is off for it (turn it on with configure_database(history=true) or alter_table(history=true))`
10094
+ `${full} has no history: time travel is off for it (turn it on with configure_database(history=true) or alter_table(history="on"))`
10108
10095
  );
10109
10096
  }
10110
10097
  return emptyState(full);
@@ -15023,7 +15010,7 @@ var SemanticIndex = class {
15023
15010
  }
15024
15011
  requireEnabled() {
15025
15012
  if (!this.getSettings().semanticEnabled) {
15026
- throw new OperationInputError("semantic search is off: enable it in the plugin settings and choose an embedding provider");
15013
+ throw new OperationInputError("semantic search is off: set semanticEnabled, semanticProvider and semanticModel in the engine settings");
15027
15014
  }
15028
15015
  }
15029
15016
  async load() {
@@ -15071,7 +15058,7 @@ var SemanticIndex = class {
15071
15058
  /** Notes in scope: every markdown note, or only table rows; minus excluded folders and archives. */
15072
15059
  inScope(f, table) {
15073
15060
  const s = this.getSettings();
15074
- if (f.path.includes("/_archive/")) return false;
15061
+ if (f.path.startsWith("_archive/") || f.path.includes("/_archive/")) return false;
15075
15062
  if (s.semanticExclude.some((dir) => f.path === dir || f.path.startsWith(`${dir.replace(/\/+$/, "")}/`))) return false;
15076
15063
  if (s.semanticScope === "tables" && !table) return false;
15077
15064
  return true;
@@ -15650,13 +15637,13 @@ function createHandlers(app, getSettings, saveSettings = async () => {
15650
15637
  },
15651
15638
  "/tables/describe/": async (body) => ({ status: 200, json: await describeTable(app, s(), str(body, "table")) }),
15652
15639
  "/ddl/": ddl,
15653
- // The plugin API itself, method by method (camelCase inputs as in public-api.ts): what the
15654
- // Obsidian plugin uses in client mode, when the headless service owns the vault.
15640
+ // The engine's API itself, method by method (camelCase inputs as in public-api.ts), for the
15641
+ // owner: `remote-api.ts` is its client.
15655
15642
  "/api/": async (body) => {
15656
15643
  const method = str(body, "method");
15657
15644
  const args = list(body, "args") ?? [];
15658
15645
  const fn = pub[method];
15659
- if (typeof fn !== "function" || API_EXCLUDED.has(method)) throw new BadRequest(`"method" must be a method of the plugin API`);
15646
+ if (typeof fn !== "function" || API_EXCLUDED.has(method)) throw new BadRequest(`"method" must be a method of the engine API`);
15660
15647
  const result = await fn.apply(pub, args);
15661
15648
  return { status: 200, json: { result: result === void 0 ? null : result } };
15662
15649
  },
@@ -16214,7 +16201,7 @@ async function simpleSearch(app, query, contextLength = 100) {
16214
16201
  }
16215
16202
  return out.sort((a, b) => b.score - a.score || a.filename.localeCompare(b.filename)).slice(0, 200);
16216
16203
  }
16217
- var PluginHttpServer = class {
16204
+ var HttpApiServer = class {
16218
16205
  constructor(opts) {
16219
16206
  this.opts = opts;
16220
16207
  this.server = null;
@@ -16223,7 +16210,7 @@ var PluginHttpServer = class {
16223
16210
  }
16224
16211
  /**
16225
16212
  * Resolves with the bound port once listening. A busy port is retried a few times: when the
16226
- * plugin reloads, the previous instance may still be closing.
16213
+ * engine restarts, the previous instance may still be closing.
16227
16214
  */
16228
16215
  async start(retries = 6, delayMs = 500) {
16229
16216
  for (let i = 0; ; i++) {
@@ -17550,7 +17537,7 @@ function checkPatterns(patterns) {
17550
17537
  var OPERATIONS = [
17551
17538
  {
17552
17539
  "name": "read_note",
17553
- "description": "Read a vault file (``GET /vault/{filename}``).\n\nDefault response is raw markdown. With ``as_json`` true, requests ``Accept: application/vnd.olrapi.note+json`` and returns ``NoteJson``: ``content``, ``frontmatter``, ``tags``, ``path``, and ``stat`` (file times and size), per the plugin OpenAPI schema.\n\nInputs:\n- ``path``: Vault-relative path to the file (forward slashes).\n- ``as_json``: If false (default), return markdown. If true, return ``NoteJson``.\n\nReturns:\n- ``str`` \u2014 markdown body, or\n- ``dict[str, Any]`` \u2014 parsed ``NoteJson`` when ``as_json`` is true.",
17540
+ "description": "Read a vault file (`GET /vault/{path}`). Default: the raw markdown. With `as_json`:\n`{content, frontmatter, tags, path, stat}` (`tags` without `#`, from the text and the\nfrontmatter; `stat`: `{ctime, mtime, size}` or null). A missing file is an error.\n`path`: vault-relative, forward slashes.",
17554
17541
  "inputSchema": {
17555
17542
  "additionalProperties": false,
17556
17543
  "properties": {
@@ -17577,7 +17564,7 @@ var OPERATIONS = [
17577
17564
  },
17578
17565
  {
17579
17566
  "name": "list_directory",
17580
- "description": 'List files under a vault directory (``GET /vault/`` or ``GET /vault/{pathToDirectory}/``).\n\nResponse JSON includes a ``files`` array. Names ending with ``/`` are subdirectories. Empty directories may not appear in listings (per API). Use ``""`` for the vault root.\n\nInputs:\n- ``path``: Vault-relative directory path; use ``""`` for the vault root.\n\nReturns:\n- ``dict[str, Any]`` \u2014 parsed JSON listing from the API.',
17567
+ "description": 'Direct children of a vault folder (`GET /vault/{folder}/`): `{files: [...]}`. Folders end with\n`/`, names are sorted, dot-files are hidden. `""` is the vault root. A missing folder is an\nerror.',
17581
17568
  "inputSchema": {
17582
17569
  "additionalProperties": false,
17583
17570
  "properties": {
@@ -17598,7 +17585,7 @@ var OPERATIONS = [
17598
17585
  },
17599
17586
  {
17600
17587
  "name": "search_simple",
17601
- "description": "Full-text search (``POST /search/simple/``).\n\nUses Obsidian\u2019s native search syntax (keywords, ``path:``, ``tag:``, ``file:``, etc.). Results include filenames, match positions, context snippets, and scores per the plugin OpenAPI.\n\nInputs:\n- ``query``: Obsidian search expression.\n- ``context_length``: Max characters of context around each match (default 100).\n\nReturns:\n- Parsed JSON from the API (matches per file, context, score).",
17588
+ "description": "Plain-text search (`POST /search/simple/`): every word of `query` must occur, case- and\naccent-insensitive, in the note's path or text; there are no operators. Returns up to 200\nfiles, best first: `[{filename, score, matches: [{match: {start, end}, context}]}]`, at most 20\nmatches per file. `context_length`: characters of context on each side (default 100, max 1000).",
17602
17589
  "inputSchema": {
17603
17590
  "additionalProperties": false,
17604
17591
  "properties": {
@@ -17625,7 +17612,7 @@ var OPERATIONS = [
17625
17612
  },
17626
17613
  {
17627
17614
  "name": "list_tables",
17628
- "description": "List every table: `name`, `catalog`, `schema`, `qualified` (`personal.metrics.mood` \u2014 use it\nwherever a table is expected; `metrics.mood` or `mood` also work when unambiguous), `path` and,\nwhen declared, `description` and `aliases` (other words for what it records). Tables are\ndefined in `database.yaml` (catalog \u2192 schema \u2192 table); legacy schema-note tables have no\n`catalog` and a two-part `qualified` name. `views` lists the stored views (same fields, no\npath): select them like tables. `system.information_schema` is read with `select_rows`.\n\nCall this first to find the table for what the user reports (match their words against\nname, description and aliases), and before creating tables or columns so you reuse existing\nones instead of creating duplicates.",
17615
+ "description": "List every table: `name`, `catalog`, `schema`, `qualified` (`personal.metrics.mood` \u2014 use it\nwherever a table is expected; `metrics.mood` or `mood` also work when unambiguous), `path` and,\nwhen declared, `description` and `aliases` (other words for what it records). Tables are\ndefined in `database.yaml` (catalog \u2192 schema \u2192 table). Returns `{tables, views}`: `views` lists the stored views (same fields, no\npath): select them like tables. `system.information_schema` is read with `select_rows`.\n\nCall this first to find the table for what the user reports (match their words against\nname, description and aliases), and before creating tables or columns so you reuse existing\nones instead of creating duplicates.",
17629
17616
  "inputSchema": {
17630
17617
  "additionalProperties": false,
17631
17618
  "properties": {},
@@ -17745,7 +17732,8 @@ var OPERATIONS = [
17745
17732
  "type": "null"
17746
17733
  }
17747
17734
  ],
17748
- "default": null
17735
+ "default": null,
17736
+ "description": "Untyped fields of the table (name \u2192 default), kept on every row; a field cannot share a name with a column."
17749
17737
  },
17750
17738
  "body": {
17751
17739
  "anyOf": [
@@ -17853,7 +17841,7 @@ var OPERATIONS = [
17853
17841
  },
17854
17842
  {
17855
17843
  "name": "rename_column",
17856
- "description": "Rename a column on the schema and every row, and rewrite the references to it in notes.\nAlso the way to merge a duplicate column into an existing compatible one. Refused (nothing\nwritten) if any row has both columns with different values.",
17844
+ "description": "Rename a column in database.yaml and on every row. Refused if `to_column` is already a column\nof the table; a row that has a stray `to_column` key with a different value is a conflict, and\nnothing is written. Views and note text that mention the old name are not rewritten.",
17857
17845
  "inputSchema": {
17858
17846
  "additionalProperties": false,
17859
17847
  "properties": {
@@ -18224,7 +18212,7 @@ var OPERATIONS = [
18224
18212
  },
18225
18213
  {
18226
18214
  "name": "describe_database",
18227
- "description": "Describe the database: its folder (`root`), `metastore` (`database.yaml`), configuration\n(default catalog and schema, row partition and file-name formats, archive folder, time zone),\njournal folder, `bases` (shared column sets and the tables using them), `catalogs` \u2192 schemas \u2192 tables (row counts) and views, legacy schema-note\ntables (`schemas`) and tables outside the database (`outside`).",
18215
+ "description": "Describe the database: its folder (`root`), `metastore` (`database.yaml`), configuration\n(default catalog and schema, row partition and file-name formats, archive folder, time zone),\njournal folder, `bases` (shared column sets and the tables using them), `catalogs` \u2192 `schemas` \u2192 `tables` (with `rows` counts) and `views`.",
18228
18216
  "inputSchema": {
18229
18217
  "additionalProperties": false,
18230
18218
  "properties": {},
@@ -18240,7 +18228,7 @@ var OPERATIONS = [
18240
18228
  },
18241
18229
  {
18242
18230
  "name": "configure_database",
18243
- "description": 'Configure the database (persisted in database.yaml `config:`): `root` (rename the database\nfolder; links follow; revert by renaming back), `default_catalog`, `default_schema`,\n`default_partition` ("" = flat) and `filename` (date formats for new time-based tables),\n`archive_folder`, `timezone` (IANA; completes date-times written without offset), `history`\n(transaction log for every table \u2014 only on the user\'s request). Returns describe_database;\nafter a root rename also `rewritten` and `mentions` (show them to the user).',
18231
+ "description": 'Configure the database (persisted in database.yaml `config:`): `root` (rename the database\nfolder; links follow; revert by renaming back), `default_catalog`, `default_schema`,\n`default_partition` ("" = flat) and `filename` (date formats for new time-based tables),\n`archive_folder`, `timezone` (IANA; completes date-times written without offset), `history`\n(transaction log for every table \u2014 only on the user\'s request). Returns describe_database.\nRenaming `root` moves the folder (links to its notes are rewritten); it is not journaled: to\nrevert, call it again with the old value.',
18244
18232
  "inputSchema": {
18245
18233
  "additionalProperties": false,
18246
18234
  "properties": {
@@ -18415,7 +18403,8 @@ var OPERATIONS = [
18415
18403
  "type": "null"
18416
18404
  }
18417
18405
  ],
18418
- "default": null
18406
+ "default": null,
18407
+ "description": "Optional note path (vault-relative), instead of the table's file-name pattern; `.md` is added if missing; it must stay inside the vault."
18419
18408
  },
18420
18409
  "dry_run": {
18421
18410
  "default": false,
@@ -18653,7 +18642,7 @@ var OPERATIONS = [
18653
18642
  "title": "Update Row",
18654
18643
  "readOnlyHint": false,
18655
18644
  "destructiveHint": false,
18656
- "idempotentHint": true,
18645
+ "idempotentHint": false,
18657
18646
  "openWorldHint": false
18658
18647
  }
18659
18648
  },
@@ -18807,7 +18796,7 @@ var OPERATIONS = [
18807
18796
  },
18808
18797
  {
18809
18798
  "name": "adopt_rows",
18810
- "description": 'Turn existing notes (not rows of any table yet) into rows of `table`, keeping their body and\nlinks: `rename` keys (`{"data": "occurred_at"}`), `set_values` for all, `per_row` values by\nnote path (`{"Home/x.md": {"zone": "[[Kitchen]]"}}`), `drop` keys the table does not have.\nEvery note gets `table` and a system `id`; with `relocate` (default) it moves to the table\'s\nfolder, named by its filename pattern (Obsidian updates links). All notes are validated first:\nunknown keys and bad values are listed and nothing is written. Run with `dry_run` first; undo\nwith `undo_migration`.',
18799
+ "description": 'Turn existing notes (not rows of any table yet) into rows of `table`, keeping their body and\nlinks: `rename` keys (`{"data": "occurred_at"}`), `set_values` for all, `per_row` values by\nnote path (`{"Home/x.md": {"zone": "[[Kitchen]]"}}`), `drop` keys the table does not have.\nEvery note gets `table` and a system `id`; with `relocate` (default) it moves to the table\'s\nfolder, named by its filename pattern (links to it are rewritten). All notes are validated first:\nunknown keys and bad values are listed and nothing is written. Run with `dry_run` first; undo\nwith `undo_migration`.',
18811
18800
  "inputSchema": {
18812
18801
  "additionalProperties": false,
18813
18802
  "properties": {
@@ -19110,7 +19099,7 @@ var OPERATIONS = [
19110
19099
  "annotations": {
19111
19100
  "title": "Undo Migration",
19112
19101
  "readOnlyHint": false,
19113
- "destructiveHint": false,
19102
+ "destructiveHint": true,
19114
19103
  "idempotentHint": false,
19115
19104
  "openWorldHint": false
19116
19105
  }
@@ -19275,7 +19264,7 @@ var OPERATIONS = [
19275
19264
  },
19276
19265
  {
19277
19266
  "name": "semantic_search",
19278
- "description": 'Find notes by meaning, not exact words: `{query, results: [{path, score, heading, snippet,\ntable?}]}`, one result per note (its best passage), best first. Use it for vague questions\n("the dinner where we talked about moving", "what did I decide about the car") and to find the\nrows to read or update; then read_note / select_rows for the details. Filters: `tables`\n(qualified or short names), `folder`, `min_score` (cosine 0\u20131; ~0.5+ is usually relevant).\nFails with a clear message when semantic search is off in the plugin settings.',
19267
+ "description": 'Find notes by meaning, not exact words: `{query, results: [{path, score, heading, snippet,\ntable?}]}`, one result per note (its best passage), best first. Use it for vague questions\n("the dinner where we talked about moving", "what did I decide about the car") and to find the\nrows to read or update; then read_note / select_rows for the details. Filters: `tables`\n(qualified or short names), `folder`, `min_score` (cosine 0\u20131; ~0.5+ is usually relevant).\n`limit`: default 10, max 100. Fails with a clear message when semantic search is not enabled in\nthe engine settings.',
19279
19268
  "inputSchema": {
19280
19269
  "additionalProperties": false,
19281
19270
  "properties": {
@@ -19338,7 +19327,7 @@ var OPERATIONS = [
19338
19327
  },
19339
19328
  {
19340
19329
  "name": "semantic_index",
19341
- "description": "Semantic search index: `status` (provider, model, notes, passages, last error), `update`\n(embed notes changed since the last run; the plugin also does it on its own a few seconds after\nedits) or `rebuild` (re-embed everything: slow, and costs money with a paid provider \u2014 ask\nfirst).",
19330
+ "description": "Semantic search index: `status` (`enabled`, `provider`, `model`, `files`, `passages`, `pending`,\n`running`, `lastSync`, `lastError`), `update` (embed notes changed since the last run; the\nengine also does it on its own about ten seconds after edits) or `rebuild` (re-embed\neverything: slow, and costs money with a paid provider \u2014 ask first).",
19342
19331
  "inputSchema": {
19343
19332
  "additionalProperties": false,
19344
19333
  "properties": {
@@ -19484,7 +19473,7 @@ var OPERATIONS = [
19484
19473
  },
19485
19474
  {
19486
19475
  "name": "describe_history",
19487
- "description": "DESCRIBE HISTORY: the table's versions, newest first \u2014 `version`, `timestamp`, `operation`\n(CREATE TABLE, WRITE, UPDATE, DELETE, MERGE, ALTER TABLE, ADD COLUMNS, RESTORE, UNDO, \u2026;\n`EXTERNAL_EDIT` = changed by hand in Obsidian), `operationMetrics` and `migrationId` (undo).\nWorks for dropped tables too (full `catalog.schema.table`). Use a version with `select_rows`\n(time travel), `table_changes` or `restore_table`.",
19476
+ "description": "DESCRIBE HISTORY: the table's versions, newest first \u2014 `version`, `timestamp`, `operation`\n(CREATE TABLE, WRITE, UPDATE, DELETE, MERGE, ALTER TABLE, ADD COLUMNS, RESTORE, UNDO, \u2026;\n`EXTERNAL_EDIT` = changed outside the engine: an editor, git, sync), `operationMetrics` and `migrationId` (undo).\nWorks for dropped tables too (full `catalog.schema.table`). Use a version with `select_rows`\n(time travel), `table_changes` or `restore_table`.",
19488
19477
  "inputSchema": {
19489
19478
  "additionalProperties": false,
19490
19479
  "properties": {
@@ -19556,7 +19545,7 @@ var OPERATIONS = [
19556
19545
  },
19557
19546
  {
19558
19547
  "name": "list_changes",
19559
- "description": "Change feed across all tables: numbered row changes (`seq`), oldest first, from engine writes\nand from hand edits (`source: external`). Each has `table`, `op` (insert, update, delete, move,\nschema), `path`, `changes` ({column: {before, after}}), `actor`, `migration`. MCP tool calls are `op: tool_call` events (`call`: tool, args, ok, value, servedBy), returned only when `ops` includes tool_call. Pass the returned\n`last` as `since` next time (`head` is the newest number); `wait_ms` waits for a new change (long poll). `gap: true` means\nolder changes were dropped: re-read the tables. `where` filters rows as in select_rows (on the values after the change). Per-table versions: `table_changes`.",
19548
+ "description": "Change feed across all tables: numbered row changes (`seq`), oldest first, from engine writes\nand from hand edits (`source: external`). Each has `table`, `op` (insert, update, delete, move,\nschema), `path`, `changes` ({column: {before, after}}), `actor`, `migration`. Agents' tool calls (reported by MCP servers) are `op: tool_call` events (`call`: tool, args, ok, value, servedBy), returned only when `ops` includes tool_call. Pass the returned\n`last` as `since` next time (`head` is the newest number); `wait_ms` (max 60000) waits for a new change (long poll); `limit` max 5000. `gap: true` means\nolder changes were dropped: re-read the tables. `where` filters rows as in select_rows (on the values after the change). Per-table versions: `table_changes`.",
19560
19549
  "inputSchema": {
19561
19550
  "additionalProperties": false,
19562
19551
  "properties": {
@@ -20082,7 +20071,7 @@ var OPERATIONS = [
20082
20071
  },
20083
20072
  {
20084
20073
  "name": "clone_table",
20085
- "description": "CREATE TABLE `target` CLONE `source`: same definition plus independent copies of its rows\n(deep clone), optionally as of a `version` / `timestamp`. `shallow=true` copies the\ndefinition only (CREATE TABLE LIKE). `target`: `catalog.schema.table`, `schema.table` (default\ncatalog) or a name (source's catalog and schema). Useful for experiments, e.g. a sandbox copy\nin the `agent` catalog.",
20074
+ "description": "CREATE TABLE `target` CLONE `source`: same definition plus independent copies of its rows\n(deep clone), optionally as of a `version` / `timestamp`. `shallow=true` copies the\ndefinition only (CREATE TABLE LIKE). `target`: `catalog.schema.table`, `schema.table` (in the\nsource's catalog) or a name (source's catalog and schema). Useful for experiments, e.g. a sandbox copy\nin the `agent` catalog.",
20086
20075
  "inputSchema": {
20087
20076
  "additionalProperties": false,
20088
20077
  "properties": {
@@ -20139,7 +20128,7 @@ var OPERATIONS = [
20139
20128
  },
20140
20129
  {
20141
20130
  "name": "recompute_columns",
20142
- "description": "Bring generated and identity columns up to date (all rows, or only `paths`), e.g. after\nrows were edited outside the plugin or a generated expression changed with `alter_column`.\nAlso gives rows without an `id` a new one, and a duplicated note (same `id` as an older row)\na new id. `new_ids=true` regenerates every id in scope: only on the user's explicit request.\n`check_table` reports stale values with code `generated`. No-op when nothing is stale;\notherwise journaled (undo_migration) and a RECOMPUTE version in the table's history.",
20131
+ "description": "Bring generated and identity columns up to date (all rows, or only `paths`), e.g. after\nrows were edited by hand (an editor, git, sync) or a generated expression changed with `alter_column`.\nAlso gives rows without an `id` a new one, and a duplicated note (same `id` as an older row)\na new id. `new_ids=true` regenerates every id in scope (new ones on every call): only on the user's\nexplicit request.\n`check_table` reports stale values with code `generated`. No-op when nothing is stale;\notherwise journaled (undo_migration) and a RECOMPUTE version in the table's history.",
20143
20132
  "inputSchema": {
20144
20133
  "additionalProperties": false,
20145
20134
  "properties": {
@@ -20178,7 +20167,7 @@ var OPERATIONS = [
20178
20167
  "title": "Recompute Columns",
20179
20168
  "readOnlyHint": false,
20180
20169
  "destructiveHint": false,
20181
- "idempotentHint": true,
20170
+ "idempotentHint": false,
20182
20171
  "openWorldHint": false
20183
20172
  }
20184
20173
  },
@@ -20343,7 +20332,7 @@ var OPERATIONS = [
20343
20332
  },
20344
20333
  {
20345
20334
  "name": "graph_path",
20346
- "description": 'Shortest chain of links between two sets of notes (each `{"paths": [...]}` and/or\n`{"table", "where"}`): `found`, `length` and the steps (`from`, `label`, `direction`, `to`).\n`direction` both by default; `max_depth` default 6 (max 10).',
20335
+ "description": 'Shortest chain of links between two sets of notes (each `{"paths": [...]}` and/or\n`{"table", "where"}`): `found`, `length` and `path`: the steps (`from`, `label`, `direction`, `to`).\n`direction` both by default; `max_depth` default 6 (max 10).',
20347
20336
  "inputSchema": {
20348
20337
  "additionalProperties": false,
20349
20338
  "properties": {
@@ -20481,7 +20470,8 @@ var ROUTES = {
20481
20470
  create_webhook: (a) => ["/webhooks/create/", pick(a, ["url", "tables", "ops", "columns", "exclude_actor", "where", "since"])],
20482
20471
  delete_webhook: (a) => ["/webhooks/delete/", pick(a, ["id"])],
20483
20472
  restore_table: (a) => ["/history/restore/", { ...pick(a, ["table", "version", "timestamp"]), ...dry(a) }],
20484
- vacuum_table: (a) => ["/history/vacuum/", { ...pick(a, ["table", "retain_from_version"]), ...dry(a) }],
20473
+ // vacuum deletes history for good: a dry run unless the caller says `dry_run: false`
20474
+ vacuum_table: (a) => ["/history/vacuum/", { ...pick(a, ["table", "retain_from_version"]), dry_run: a.dry_run !== false }],
20485
20475
  graph_neighbors: (a) => ["/graph/neighbors/", compact({ from: a.from_nodes, direction: a.direction, edges: a.edges })],
20486
20476
  graph_traverse: (a) => [
20487
20477
  "/graph/traverse/",
@@ -20775,7 +20765,7 @@ var EngineRuntime = class {
20775
20765
  transport: async (url, body) => this.o.postLogs(url, body),
20776
20766
  resource: { "service.version": this.o.version, "host.name": this.o.hostName }
20777
20767
  });
20778
- const source = this.o.hostName === "obsidian" ? void 0 : this.o.hostName;
20768
+ const source = this.o.hostName;
20779
20769
  const fileLog = new FileLogger({ enabled: () => s().fileLogEnabled, write: (day, lines) => appendLog(this.o.app, s(), day, lines, source) });
20780
20770
  const tee = new TeeTelemetry([logger, fileLog]);
20781
20771
  setTelemetry(tee);
@@ -20813,7 +20803,6 @@ var EngineRuntime = class {
20813
20803
  this.httpServer = null;
20814
20804
  this.httpServerError = null;
20815
20805
  const s = this.o.getSettings();
20816
- if (!s.httpServerEnabled) return;
20817
20806
  if (!this.o.canServeHttp) {
20818
20807
  this.httpServerError = "the HTTP server runs on desktop only";
20819
20808
  return;
@@ -20824,7 +20813,7 @@ var EngineRuntime = class {
20824
20813
  }
20825
20814
  const handlers = this.handlers();
20826
20815
  const guarded = this.apps.guard(dispatch);
20827
- const server = new PluginHttpServer({
20816
+ const server = new HttpApiServer({
20828
20817
  app: this.o.app,
20829
20818
  handlers,
20830
20819
  token: s.httpServerToken,
@@ -22080,7 +22069,7 @@ var NodeWatcher = class {
22080
22069
  };
22081
22070
 
22082
22071
  // src/node/service.ts
22083
- var VERSION = true ? "1.1.1" : "dev";
22072
+ var VERSION = true ? "1.2.1" : "dev";
22084
22073
  function parseArgs(argv, env = process.env) {
22085
22074
  const get = (name) => {
22086
22075
  const i = argv.indexOf(`--${name}`);
@@ -22103,8 +22092,6 @@ function parseArgs(argv, env = process.env) {
22103
22092
  }
22104
22093
  function settingsPath(o) {
22105
22094
  if (o.settingsFile) return import_node_path5.default.resolve(o.settingsFile);
22106
- const plugin = import_node_path5.default.join(o.vault, ".obsidian", "plugins", "datanotes", "data.json");
22107
- if (import_node_fs5.default.existsSync(plugin)) return plugin;
22108
22095
  return import_node_path5.default.join(o.vault, ".datanotes", "service.json");
22109
22096
  }
22110
22097
  async function startService(o, env = process.env) {
@@ -22123,14 +22110,15 @@ async function startService(o, env = process.env) {
22123
22110
  } catch {
22124
22111
  current2 = {};
22125
22112
  }
22126
- const { httpServerEnabled: _forced, ...mine } = settings;
22127
22113
  import_node_fs5.default.mkdirSync(import_node_path5.default.dirname(file), { recursive: true });
22128
- import_node_fs5.default.writeFileSync(file, JSON.stringify({ ...current2, ...mine }, null, 2));
22114
+ import_node_fs5.default.writeFileSync(file, JSON.stringify({ ...current2, ...settings }, null, 2));
22129
22115
  };
22130
- const view = new Proxy(settings, {
22131
- get: (t, k) => k === "httpServerEnabled" ? true : t[k]
22132
- });
22133
22116
  const host = openNodeVault(o.vault);
22117
+ const held = await readLock(host.vault.adapter, storeRoot(settings));
22118
+ if (isFresh(held) && held.pid !== process.pid && (held.host !== import_node_os.default.hostname() || processAlive(held.pid))) {
22119
+ host.metadataCache.dispose();
22120
+ throw new Error(`another engine holds the vault (pid ${held.pid} on ${held.host}, port ${held.port}): stop it first`);
22121
+ }
22134
22122
  const watcher = new NodeWatcher(host.vault, { mode: o.watch, intervalMs: o.pollMs });
22135
22123
  const keyFromFile = () => {
22136
22124
  if (!o.embeddingKeyFile) return void 0;
@@ -22143,7 +22131,7 @@ async function startService(o, env = process.env) {
22143
22131
  const runtime = new EngineRuntime({
22144
22132
  ...o.host ? { httpHost: o.host } : {},
22145
22133
  app: host.app,
22146
- getSettings: () => view,
22134
+ getSettings: () => settings,
22147
22135
  saveSettings: save,
22148
22136
  transport: async (req) => {
22149
22137
  const res = await fetch(req.url, { method: req.method, headers: req.headers, body: req.body });
@@ -22208,6 +22196,14 @@ async function startService(o, env = process.env) {
22208
22196
  }
22209
22197
  };
22210
22198
  }
22199
+ function processAlive(pid) {
22200
+ try {
22201
+ process.kill(pid, 0);
22202
+ return true;
22203
+ } catch (e) {
22204
+ return e.code === "EPERM";
22205
+ }
22206
+ }
22211
22207
  async function main(argv = process.argv.slice(2)) {
22212
22208
  const li = argv.indexOf("--log");
22213
22209
  if (li >= 0 && argv[li + 1]) {
@@ -22251,7 +22247,7 @@ Docs: https://datanotes.dev`;
22251
22247
  }
22252
22248
 
22253
22249
  // src/node/cli.ts
22254
- var VERSION2 = true ? "1.1.1" : "dev";
22250
+ var VERSION2 = true ? "1.2.1" : "dev";
22255
22251
  async function main2(argv = process.argv.slice(2)) {
22256
22252
  const [command, ...rest] = argv;
22257
22253
  if (command === "--version" || command === "-v" || command === "version") return void process.stdout.write(`${VERSION2}
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "datanotes",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "description": "The engine of your second brain: Markdown notes as database tables, for you and your agents.",
5
5
  "keywords": [
6
- "obsidian",
7
6
  "markdown",
8
7
  "database",
9
8
  "mcp",
10
9
  "model-context-protocol",
11
10
  "second-brain",
12
11
  "agents",
13
- "frontmatter"
12
+ "frontmatter",
13
+ "obsidian-vault"
14
14
  ],
15
15
  "homepage": "https://datanotes.dev",
16
16
  "bugs": {
@@ -56,8 +56,6 @@
56
56
  "node": ">=20"
57
57
  },
58
58
  "scripts": {
59
- "dev": "node esbuild.config.mjs",
60
- "build": "node scripts/check-layers.mjs && tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
61
59
  "test": "vitest run",
62
60
  "test:coverage": "vitest run --coverage",
63
61
  "test:watch": "vitest",
@@ -65,16 +63,14 @@
65
63
  "check:layers": "node scripts/check-layers.mjs",
66
64
  "test:node": "vitest run --config vitest.node.config.ts",
67
65
  "build:service": "node scripts/check-layers.mjs && tsc -noEmit -skipLibCheck && node esbuild.service.mjs && npm run build:api",
68
- "build:client": "node scripts/check-layers.mjs && tsc -noEmit -skipLibCheck && node esbuild.client.mjs",
69
66
  "prepack": "npm run build:service",
70
- "build:api": "tsc -p tsconfig.api.json && node esbuild.api.mjs"
67
+ "build:api": "tsc -p tsconfig.api.json && node esbuild.api.mjs",
68
+ "build": "npm run build:service"
71
69
  },
72
70
  "devDependencies": {
73
71
  "@types/node": "^22.10.0",
74
72
  "@vitest/coverage-v8": "^2.1.9",
75
73
  "esbuild": "0.25.5",
76
- "happy-dom": "^20.9.0",
77
- "obsidian": "1.7.2",
78
74
  "tslib": "2.8.1",
79
75
  "typescript": "^5.8.3",
80
76
  "vitest": "^2.1.9",