From 99a2ea3ec57677cd90d61bc1cfa30b5da2e04c13 Mon Sep 17 00:00:00 2001 From: August Cayzer Date: Sun, 1 Nov 2026 01:07:44 +0000 Subject: [PATCH 1/4] docs(content): apply the final read's fixes --- STYLE.md | 3 +- content/AGENTS.md | 2 +- content/docs/(overview)/access.mdx | 2 +- .../docs/(overview)/agents-cli-and-mcp.mdx | 2 +- content/docs/(overview)/files.mdx | 2 +- content/docs/(overview)/items-and-types.mdx | 2 +- content/docs/(reference)/marfa-mcp-server.mdx | 6 +- .../swift-sdk/views-and-folders.mdx | 24 +++++- content/docs/build/change-a-type.mdx | 6 +- .../docs/build/connect-claude-and-chatgpt.mdx | 2 +- .../docs/build/move-items-to-another-type.mdx | 2 +- content/docs/build/read-a-view.mdx | 25 ++---- content/docs/build/save-a-view.mdx | 2 +- .../docs/build/write-in-a-working-copy.mdx | 2 +- content/docs/catalog/connectors/github.mdx | 22 ++--- .../catalog/connectors/google-calendar.mdx | 24 +++--- content/docs/catalog/connectors/index.mdx | 2 +- .../catalog/connectors/readwise-reader.mdx | 16 ++-- content/docs/catalog/connectors/readwise.mdx | 14 ++-- content/docs/catalog/connectors/rss.mdx | 20 ++--- content/docs/catalog/connectors/todoist.mdx | 20 ++--- content/docs/run/export-and-restore.mdx | 2 +- content/docs/run/folder-from-a-mac-app.mdx | 26 ------ content/docs/run/folder-safeguards.mdx | 6 +- content/docs/run/housekeeping.mdx | 83 ++----------------- content/docs/run/index.mdx | 1 + content/docs/run/keep-folders-running.mdx | 6 +- content/docs/run/meta.json | 1 - content/docs/run/run-a-connector.mdx | 10 +-- content/docs/run/settings.mdx | 2 +- content/docs/run/sign-in-as-the-owner.mdx | 8 +- content/docs/run/sync-a-folder.mdx | 2 +- .../run/troubleshooting/google-connectors.mdx | 2 +- 33 files changed, 131 insertions(+), 218 deletions(-) delete mode 100644 content/docs/run/folder-from-a-mac-app.mdx diff --git a/STYLE.md b/STYLE.md index b2087f69..e876b07d 100644 --- a/STYLE.md +++ b/STYLE.md @@ -63,7 +63,7 @@ A page that needs a list the Catalog holds, such as the types, edge types, permi - **A list carries items that are parallel**: steps, options, things to check. A page never opens with a list, except the three opening lines of a tutorial, and a list never stands in for an explanation of why. - **A table compares things or holds what a reader looks up**: fields, settings, commands, options, a choice between two ways. Keep cells short. **Tables stay tables**: never turn one into a list to fit a phone. At phone width every table stacks each row, and a table of three or more columns puts each cell after the first under its column's name, so give each column a header that reads as a label. - **Explain, then point.** A concept page explains the idea and the rules a reader meets in normal use, with one small example. Rare cases, limits, exact numbers and error codes go in a guide, the API reference or the contract, with a link. -- **Less is clearer.** If a page is hard going, cut before you add. A page past its limit is usually two pages, or holds detail that belongs in the reference: a guide that covers two jobs done at different times is two guides. `pnpm test` enforces the limits, counting the words after the frontmatter, code included: **900 for a concept page, 1,500 for a guide, a tutorial or a Catalog connector page.** The folder decides the kind. A lookup page (Settings, the CLI and SDK pages, the Glossary, the API reference, the Catalog type pages and the Changelog) has a limit of its own, and so does a tutorial or guide whose kind differs from its folder's. Those exceptions, each with its reason, are in `src/lib/page-limits.json`, and no page is exempt. When a page is over, the failure names it and the way out: split it, move detail to the reference, or cut. A page that is over today is listed there as rewrite pending, with its current count as a ceiling that it must not pass. +- **Less is clearer.** If a page is hard going, cut before you add. A page past its limit is usually two pages, or holds detail that belongs in the reference: a guide that covers two jobs done at different times is two guides. `pnpm test` enforces the limits, counting the words after the frontmatter, code included: **900 for a concept page, 1,500 for a guide, a tutorial or a Catalog connector page.** The folder decides the kind. A lookup page (Settings, the CLI and SDK pages, the Glossary, the API reference, the Catalog type pages and the Changelog) has a limit of its own, and so does a tutorial or guide whose kind differs from its folder's. Those exceptions, each with its reason, are in `src/lib/page-limits.json`, and no page is exempt. When a page is over, the failure names it and the way out: split it, move detail to the reference, or cut. A page over its limit while it waits for a rewrite can be listed there under `pending`, with its current count as a ceiling; the test fails once the page is back within its limit. - **Describe the present.** No history, no "now", no "new", nothing about what changed. - **Replace, don't append.** When something changes, rewrite the sentence or section it changes. Never add a paragraph that qualifies an older one. @@ -73,6 +73,7 @@ Marfa's [`GLOSSARY.md`](https://github.com/withmarfa/marfa/blob/main/GLOSSARY.md - The `marfa` program is **the CLI**. One of its commands, such as `marfa items list`, is **a command**. - One thing the API does is **an operation**, never "a door". +- Write **the bin** for where a deleted item goes, and **move to the bin** for the act. Use `trash` only in a literal name: the owner's **Trash** page, `trashed`, `trash-purge`, `TRASH_RETENTION_DAYS` and API operation titles. If the glossary doesn't settle a term, ask the owners of the contract before a page uses it. diff --git a/content/AGENTS.md b/content/AGENTS.md index 9fc1853c..0849dc93 100644 --- a/content/AGENTS.md +++ b/content/AGENTS.md @@ -8,7 +8,7 @@ This file sits above `content/docs/` on purpose: the docs collection publishes e - Each folder directly under `content/docs/` is a tab. `(overview)/` and `(reference)/` are folder groups, so their pages sit at the site root (`/files`, `/cli`). `build/`, `run/` and `catalog/` keep their names in the URL, and so does `(reference)/api-reference/`. The Catalog sidebar has three labeled sections: Data (Items, Types, Edges), Events and access, and Integrations (Connectors). The Catalog tab's Types pages sit in a folder group, `catalog/(types)/`, which the sidebar shows as a closed folder named Types. Its own page is `types.mdx` (`pagesIndex` in its `meta.json`, so the folder opens on `/catalog/types`), and it holds Type rules and Field kinds and two more folder groups, `(core)/` and `(system)/`, shown as closed folders, "Core types" and "System types". No group adds to an address: a type page stays at `/catalog/note`. - A new page needs its file and an entry in its folder's `meta.json`. `content/docs/meta.json` orders the tabs. Build, Run and the Catalog each open on an `index.mdx` that their `meta.json` lists first; a page that belongs in Build (code against an instance) or Run (an instance and its care, with no code) goes in that folder, under the group a reader would name. -- A slug in `(overview)/` or `(reference)/` must not repeat a slug in either group. It must not be a name the Worker answers at the root (`api`, `assets`, `mcp`, `llms`, `llms-full`, `docs-map`, `version`) or a tab folder (`build`, `run`, `catalog`), or `docs`, which the docs tools strip as a prefix. +- A slug in `(overview)/` or `(reference)/` must not repeat a slug in either group. It must not be a name the Worker answers at the root (`api`, `assets`, `mcp`, `llms`, `llms-full`, `docs-map`, `version`) or a tab folder (`build`, `run`, `catalog`), or `docs`, which `marfa docs` strips as a prefix. - The Catalog tab lists names and shapes and explains nothing. Each top-level type, core or system, has a page (`src/lib/catalog.test.ts` holds them to `src/generated/catalog/builtin-catalog.json`) that takes its title and description from the type and includes the type's generated file. A core type's page ends with one `## Example` item that the test checks; Marfa writes system items itself, so their pages have none. Add a type's page, in the folder of its family and in that folder's `meta.json`, when marfa ships a type, and leave explanation to the Overview, Build and Run. Include `catalog/edge-types.md`, `permissions.md` and `event-types.md` on Catalog pages only; every other page links to the Catalog page. - The header tabs come from `SECTIONS` in `src/components/section-nav.tsx`, not from `meta.json`. A new tab or a renamed landing page needs both. The Reference tab opens on `(reference)/reference.mdx` (`/reference`), the first entry in `(reference)/meta.json`, with a card for each destination below it. A new Reference page needs its entry in `(reference)/meta.json`, which the header reads, and a card there when it is a new kind of destination. - The header holds no text links. The Changelog and the contract sit in the Reference tab's Resources group, so a new destination goes in a sidebar group, not in `links` in `src/lib/layout.shared.tsx`. diff --git a/content/docs/(overview)/access.mdx b/content/docs/(overview)/access.mdx index 9c90456e..6fafdd11 100644 --- a/content/docs/(overview)/access.mdx +++ b/content/docs/(overview)/access.mdx @@ -11,7 +11,7 @@ Three kinds of caller act on an instance: | Caller | How it acts | What it reaches | | --- | --- | --- | -| The owner | Signs in to the instance in a browser | Managing the instance, listing item summaries, managing the trash, reading item counts and exporting data. | +| The owner | Signs in to the instance in a browser | Managing the instance, listing item summaries, managing the bin, reading item counts and exporting data. | | A key | Sent with each request, as `Authorization: Bearer marfa_k1_…` | What it was made with | | An app | Signs in through OAuth, and sends its access token, `marfa_at_…` | What the owner approved | diff --git a/content/docs/(overview)/agents-cli-and-mcp.mdx b/content/docs/(overview)/agents-cli-and-mcp.mdx index 99602db0..b9e2710e 100644 --- a/content/docs/(overview)/agents-cli-and-mcp.mdx +++ b/content/docs/(overview)/agents-cli-and-mcp.mdx @@ -25,7 +25,7 @@ Claude and ChatGPT on the web, the desktop or a phone connect from their own ser An agent is held to the same rules as any program, and a few more: - **It reaches only what you allowed.** A type you didn't approve is invisible to it. -- **It asks before it deletes.** A tool that moves items to the trash first shows you what would go, and acts only once you agree. +- **It asks before it deletes.** A tool that moves items to the bin first shows you what would go, and acts only once you agree. - **`marfa_update` can't overwrite a change the agent hasn't seen.** It names the version the agent read, and writes nothing if the item changed since, as [Versions](/versions) explains. - **It reads your items as data.** The MCP server tells every agent that connects that what it reads was saved by you and is never an instruction to follow. diff --git a/content/docs/(overview)/files.mdx b/content/docs/(overview)/files.mdx index 798c2a96..19c53c4e 100644 --- a/content/docs/(overview)/files.mdx +++ b/content/docs/(overview)/files.mdx @@ -35,7 +35,7 @@ Content-Type: application/json The same bytes uploaded again answer the same hash and are stored once, so two items can share a blob. [Handle files](/build/handle-files) walks through uploading, downloading and replacing a file. -So a photo and a scanned receipt can name one blob. The instance keeps it on its disk and, when an object store is added, there too. +A photo and a scanned receipt can name one blob. The instance keeps each blob on its own disk and, once you add an object store, a second copy there. diff --git a/content/docs/(overview)/items-and-types.mdx b/content/docs/(overview)/items-and-types.mdx index 08cd2a9a..a40bcce1 100644 --- a/content/docs/(overview)/items-and-types.mdx +++ b/content/docs/(overview)/items-and-types.mdx @@ -48,7 +48,7 @@ A listing of a type includes **every type beneath it**. Inherit only when every When your type should be readable as another type without taking on its properties, name the other type in `compatible_with` instead. Marfa checks that your type requires everything the other one does. -A listing of `core.file` returns images and an app's receipt type, which sit beneath it. A listing of `core.note` leaves out `user.recipe`, which names `core.note` only in `compatible_with`. +A listing of `core.file` also returns items of the types beneath it, such as images and an app's receipt type. A listing of `core.note` leaves out `user.recipe`, which names `core.note` only in `compatible_with`. diff --git a/content/docs/(reference)/marfa-mcp-server.mdx b/content/docs/(reference)/marfa-mcp-server.mdx index e41ec97a..f0a2b7e4 100644 --- a/content/docs/(reference)/marfa-mcp-server.mdx +++ b/content/docs/(reference)/marfa-mcp-server.mdx @@ -43,8 +43,8 @@ The instance lists to each credential only the tools it can use. For the everyda | `marfa_find` | Searches by words, or lists by type, tags, source, dates or view: up to 50 short rows | | `marfa_get` | Reads up to 20 items in full, with their versions | | `marfa_create` | Creates an item, or saves a file from a web address or a chat | -| `marfa_update` | Sets or clears fields, or changes tags, edges or state, including a restore from the trash | -| `marfa_delete` | Moves up to 20 items to the trash, after a preview | +| `marfa_update` | Sets or clears fields, or changes tags, edges or state, including a restore from the bin | +| `marfa_delete` | Moves up to 20 items to the bin, after a preview | ### Schema tools @@ -78,7 +78,7 @@ Four tools act in two calls. **The first call changes nothing** and answers a pr | Tool | What the preview names | | --- | --- | -| `marfa_delete` | Every item the trash would take, including the items that go with the named ones, and their `total` | +| `marfa_delete` | Every item the move to the bin would take, including the items that go with the named ones, and their `total` | | `marfa_move_items` | The items the move would take | | `marfa_revoke_key` | The key | | `marfa_manage_webhook` | The webhook's address, events, type filter and what each delivery sends. Only a call that makes a webhook, or changes its `url`, `events` or `type_filter`, is previewed | diff --git a/content/docs/(reference)/swift-sdk/views-and-folders.mdx b/content/docs/(reference)/swift-sdk/views-and-folders.mdx index 72b00eb1..5eb6ab94 100644 --- a/content/docs/(reference)/swift-sdk/views-and-folders.mdx +++ b/content/docs/(reference)/swift-sdk/views-and-folders.mdx @@ -101,7 +101,7 @@ The rules are in the contract's [views written through a copy](https://github.co ## Folders on a Mac -On macOS, `Folders` keeps a directory of files in step with a folder on the instance, and shares the Mac's list of folders with `marfa folders`. **Nothing is written into the directory or sent until the first sync is confirmed.** [Sync a folder from a Mac app](/run/folder-from-a-mac-app) sets one up. +On macOS, `Folders` keeps a directory of files in step with a folder on the instance, and shares the Mac's list of folders with `marfa folders`. **Nothing is written into the directory or sent until the first sync is confirmed.** [Sync a folder](/run/sync-a-folder) explains what a folder is and does the same with the CLI. `copy.createFolder(_:idempotencyKey:)`, `changeFolder(_:_:baseVersion:idempotencyKey:)` and `revokeFolder(_:idempotencyKey:)` write a folder's settings, and `copy.items.folder(_:)` reads them. `Folders(server:)` works the directories on this Mac: @@ -117,6 +117,28 @@ On macOS, `Folders` keeps a directory of files in step with a folder on the inst A `FileState` is `.inStep`, `.waiting` for a queued write, `.held` (the file's status gives the `reason`), `.unmatched`, `.unreached` (held rather than deleted) or `.outside`. +### Set up a folder + +This code creates a folder, binds a directory to it and confirms the first sync, with `serverURL`, `key` and `storeURL` as in [Open a working copy](/swift-sdk/working-copy#open-a-copy). Until the first sync is confirmed, `sync(_:)` returns `.awaitingConfirmation` with the plan: + +```swift +import Marfa + +let server = Server(url: serverURL, key: key) +let copy = try await WorkingCopy.open(store: storeURL, server: server) +let folder = try await copy.createFolder( + FolderSettings(title: "Notes", query: FolderQuery(types: ["core.note"]))) + +let folders = Folders(server: server) +let directory = URL(filePath: "/Users/sam/Notes") +_ = try await folders.add(directory, following: folder.id) +_ = try await folders.sync(directory) // .awaitingConfirmation(plan) +_ = try await folders.confirmFirstSync(in: directory) +_ = try await folders.sync(directory) +``` + +A client without the SDK calls [Create a folder](/api-reference/folders/createFolder) and the other folder operations. + ### Watch a folder A `FolderWatch` is an `AsyncSequence` of `FolderEvent`. It throws a `MarfaError` when the key is refused, the instance's changes stop reaching the folder, or the directory can't be watched. **Leaving a loop early doesn't stop it**: call `await watch.stop()` when its owner finishes. Its threads run at utility quality of service, so they give way to the app's own work. diff --git a/content/docs/build/change-a-type.mdx b/content/docs/build/change-a-type.mdx index ae7b8ca7..ba3e2f04 100644 --- a/content/docs/build/change-a-type.mdx +++ b/content/docs/build/change-a-type.mdx @@ -39,7 +39,7 @@ The answer's `type` is your definition with its defaults made explicit, and `reg **A replacement keeps the icon the owner chose** for the type (see [the owner's icon](/catalog/type-rules#the-owners-icon)), and `registered_type` shows the icon you registered, not the owner's. -**A `GET` of a type, put back as a replacement, stores only what you wrote.** A hint the type doesn't set itself (`title_field`, `body_field` or `icon`) and that you send back with the value the `GET` answered is the type's inheritance read back, not your choice. Marfa stores no value for it, so the type goes on following its parent and the owner's icon. A hint the type sets stays, including one set to a different value than its parent's. For an icon the type sets, a value equal to the owner's icon keeps the icon you registered. A hint you newly set to the value the type already inherits is stored as none, and the type reads the same. A change to `title_field` or `body_field` changes how the server and every device [name and preview](/catalog/type-rules#names-and-previews) the type's items. +**Sending back what a `GET` answered changes only what you changed.** A `GET` answers the display hints a type inherits as well as its own. A hint you send with the value the type inherits isn't stored, so the type keeps following its parent and the owner's icon. A hint the type sets itself stays as it is. A change to `title_field` or `body_field` changes how the server and every device [name and preview](/catalog/type-rules#names-and-previews) the type's items. Then replace the type: @@ -76,7 +76,7 @@ Send the `version` every time: a replacement without one sets it back to 0. You ## Delete the type -Marfa deletes a type that **no item holds**, in any state and the trash included, that no folder, view or webhook names, and that no other type names as its parent or in `compatible_with`: +Marfa deletes a type that **no item holds**, in any state, the bin included, that no folder, view or webhook names, and that no other type names as its parent or in `compatible_with`: ```bash tab="CLI" tab-group="client" marfa types delete app.example.receipt @@ -102,7 +102,7 @@ Otherwise it refuses with `409 type_in_use` when items hold the type or a settin A `type_in_use` refusal counts the items in `details.item_count` and names each folder, view and webhook setting in `details.settings`, without the `id` of a setting your key can't read. **[Move the items to another type](/build/move-items-to-another-type) first:** the move changes those settings too. `details.grants` lists the keys and apps whose type map names the type, if your key may manage them. They don't stop a delete, so change them through [Manage keys](/run/manage-keys) when you're ready. -A key can send `force=true` (the CLI's `--force`) to delete a type that items still hold or a setting names. A subtype or a `compatible_with` claim still blocks it. The items and settings stay, but Marfa refuses writes to the items' properties, tier or time until the type is registered again. You can still tag, archive or trash them. +A key can send `force=true` (the CLI's `--force`) to delete a type that items still hold or a setting names. A subtype or a `compatible_with` claim still blocks it. The items and settings stay, but Marfa refuses writes to the items' properties, tier or time until the type is registered again. You can still tag them, archive them or move them to the bin. The owner can remove an unused type without a key: with a [sign-in from the last five minutes](/run/sign-in-as-the-owner#confirm-its-you), select **Remove type** on the type's page under **Items** in the owner's console, or send the deletion through the private socket. Marfa makes the same checks, and refuses `force=true` from the owner with `403 forbidden`. diff --git a/content/docs/build/connect-claude-and-chatgpt.mdx b/content/docs/build/connect-claude-and-chatgpt.mdx index a63355f9..6265e757 100644 --- a/content/docs/build/connect-claude-and-chatgpt.mdx +++ b/content/docs/build/connect-claude-and-chatgpt.mdx @@ -87,7 +87,7 @@ In either chat, you can also ask it to save a file from a public web address. Th ## What the chat can do -The chat reaches only the types and permissions you approved. **It asks before it deletes**: the first delete call only previews what would go, and the chat shows you before anything moves to the trash. It asks the same way before it makes a webhook, which sends events outside your instance. No tool purges, exports or changes the instance's settings. +The chat reaches only the types and permissions you approved. **It asks before it deletes**: the first delete call only previews what would go, and the chat shows you before anything moves to the bin. It asks the same way before it makes a webhook, which sends events outside your instance. No tool purges, exports or changes the instance's settings. ## Disconnect it diff --git a/content/docs/build/move-items-to-another-type.mdx b/content/docs/build/move-items-to-another-type.mdx index e3d01e0c..77de7d9b 100644 --- a/content/docs/build/move-items-to-another-type.mdx +++ b/content/docs/build/move-items-to-another-type.mdx @@ -49,7 +49,7 @@ Marfa answers `200` with the items it would move, the settings it would change a } ``` -**A move takes every item of exactly that type that your key can see, in any state.** An archived item stays archived, and an item in the trash moves and stays in the trash. A type that names `acme.deal` as its `parent` keeps its own items. An item your key's source filter hides isn't moved or counted in `matched`. One move takes up to 50,000 items. +**A move takes every item of exactly that type that your key can see, in any state.** An archived item stays archived, and an item in the bin moves and stays in the bin. A type that names `acme.deal` as its `parent` keeps its own items. An item your key's source filter hides isn't moved or counted in `matched`. One move takes up to 50,000 items. ## Move the items diff --git a/content/docs/build/read-a-view.mdx b/content/docs/build/read-a-view.mdx index 0fb54d13..388dc47d 100644 --- a/content/docs/build/read-a-view.mdx +++ b/content/docs/build/read-a-view.mdx @@ -101,29 +101,20 @@ marfa device --db ./copy.sqlite views groups --time-zone Europe/London ### Read a view in an app -Use the core's view reads on the same handle as other working-copy reads. Rust exposes `list_views`, `view`, `list_in_view`, `search_in_view` and `view_groups`; the core's Node binding exposes the corresponding camel-case methods, and the [Swift SDK](/swift-sdk/views-and-folders#read-a-view) wraps them in its own types. - -For example, with an open Rust `Core` handle, read a page and the view's groups: - -```rust -let page = core.list_in_view(&view_id, Some(50), None)?; -let groups = core.view_groups(&view_id, None)?; -``` - -With a Swift `WorkingCopy`, the same two reads are: +An app on the Swift SDK reads a view from its working copy with the same calls as any other read: ```swift let page = try await copy.items.list(inView: viewID, limit: 50) let groups = try await copy.items.groups(inView: viewID) ``` -| Core read | Swift SDK | Answer | -| --- | --- | --- | -| `list_views` | `items.views()` | The views held in the copy. | -| `view` | `items.view(_:)` | One view's settings and version. | -| `list_in_view` | `items.list(inView:limit:offset:)` | A page of matching items, in the view's sort order. | -| `search_in_view` | `search(_:inView:limit:)` | Search matches within the view's query. | -| `view_groups` | `items.groups(inView:timeZone:)` | The view's group keys and counts. | +| Swift SDK | Answer | +| --- | --- | +| `items.views()` | The views held in the copy. | +| `items.view(_:)` | One view's settings and version. | +| `items.list(inView:limit:offset:)` | A page of matching items, in the view's sort order. | +| `copy.search(_:inView:limit:)` | Search matches within the view's query. | +| `items.groups(inView:timeZone:)` | The view's group keys and counts. | Pass the device's time zone when asking for date groups. The Swift SDK also writes views and follows one with a live query: [Views and folders](/swift-sdk/views-and-folders#saved-views) shows both. diff --git a/content/docs/build/save-a-view.mdx b/content/docs/build/save-a-view.mdx index dacb369f..5c4612f1 100644 --- a/content/docs/build/save-a-view.mdx +++ b/content/docs/build/save-a-view.mdx @@ -49,7 +49,7 @@ A view's `filter` takes any expression that [List, filter and search](/build/lis ## Change, delete or read a view - **To change a view,** [update it](/api-reference/views/updateView) with the `version` you read and the settings you change. Each setting you name is replaced whole. A change based on an older version still applies if nobody has changed the same setting since. To stop grouping, send `group: null`, or run `marfa views change --version --group null`. -- **To delete a view,** [delete it](/api-reference/views/deleteView). It's gone at once, with no trash. +- **To delete a view,** [delete it](/api-reference/views/deleteView). It's gone at once; it doesn't go to the bin. - **To read a view's settings,** [get it](/api-reference/items/getItem) by its `id`, or list the type `system.view`, like any other item. To read the items it lists, run `marfa views items `, as [Read a view](/build/read-a-view#read-a-views-items-from-the-server) shows. ## What your key needs diff --git a/content/docs/build/write-in-a-working-copy.mdx b/content/docs/build/write-in-a-working-copy.mdx index c06b7297..228e05fe 100644 --- a/content/docs/build/write-in-a-working-copy.mdx +++ b/content/docs/build/write-in-a-working-copy.mdx @@ -46,7 +46,7 @@ An app can show `synced_at` as "Updated 5 minutes ago" or "Offline since 10:42". ### Read the bin -**The copy keeps the items in its slice that go to the bin**, whether you binned them here or another app did. `marfa device items list --state trashed` lists them with no connection, most recently binned first, and `marfa device items restore ` takes one out. +**The copy keeps the items in its slice that go to the bin**, whether you moved them there or another app did. `marfa device items list --state trashed` lists them with no connection, the most recent first, and `marfa device items restore ` takes one out. `marfa device items bin` reads the instance's whole bin instead, a page at a time. It needs the server, and keeps nothing it reads in the copy. diff --git a/content/docs/catalog/connectors/github.mdx b/content/docs/catalog/connectors/github.mdx index fbaba5d1..dff738ba 100644 --- a/content/docs/catalog/connectors/github.mdx +++ b/content/docs/catalog/connectors/github.mdx @@ -14,7 +14,7 @@ You choose how it signs in, and the choice decides whether anything can go back: It reads: -- **Repositories.** Every repository the token or App can reach, or only those named in `GITHUB_REPOSITORIES`. A repository left out of that setting is paused, not archived: its rows stay as they are. With a personal token, or a notifications token and no personal one, it also reads the repositories you have starred. +- **Repositories.** Every repository the token or App can reach, or only those named in `GITHUB_REPOSITORIES`. A repository left out of that setting is paused, not archived: its items stay as they are. With a personal token, or a notifications token and no personal one, it also reads the repositories you have starred. - **Issues and pull requests.** Everything open, and what closed or changed in the last 90 days. An older issue comes in once anything happens to it. An issue's assignees are core `assignees` entries, each with the login as `name`. A pull request carries its branches and check results. A fine-grained personal token has no Checks permission, so with one a pull request keeps the check summary it had, a new one has none, and the run names `checks-unreadable`. - **Discussions and comments.** The discussions of each repository that has Discussions turned on, with their comments and replies, and the comments on issues. - **Relations.** Sub-issues, blockers, related issues, duplicates, and which pull request closed which issue. @@ -36,18 +36,18 @@ It reads: | `github.repository_file` | [Note](/catalog/note) | A Markdown file from a repository, with its text as the body, read only | | `github.directory` | [Collection](/catalog/collection) | A directory that holds synced Markdown files, read only | -Every row of the other types holds `github_id`, which is how the connector finds the same thing on GitHub again; a repository file or directory is known by its repository and path. The connector registers these types on its first start. +Every item of the other types holds `github_id`, which is how the connector finds the same thing on GitHub again; a repository file or directory is known by its repository and path. The connector registers these types on its first start. -It also writes the connections `github.in-repository`, `github.sub-issue-of`, `github.blocked-by`, `github.relates-to`, `github.duplicate-of`, `github.closes` and `github.closed-by`, and uses Marfa's own `in-thread`, `references`, `parent-of`, `attached-to` and `in-collection`. A repository file joins its directory through `in-collection`, and a directory joins its parent directory through `parent-of`. +It also writes the edges `github.in-repository`, `github.sub-issue-of`, `github.blocked-by`, `github.relates-to`, `github.duplicate-of`, `github.closes` and `github.closed-by`, and uses Marfa's own `in-thread`, `references`, `parent-of`, `attached-to` and `in-collection`. A repository file joins its directory through `in-collection`, and a directory joins its parent directory through `parent-of`. ## What goes back Only with a GitHub App, and only for issues, discussions, comments and the relations between issues: - **Edits.** An issue's title, body, labels, assignees and status; a discussion's title, body and whether it is closed; a comment's text. A status other than `completed`, `canceled` or `duplicate` reopens a closed issue, and one GitHub has no word for, such as `in_progress`, stays in Marfa. An assignee with no `name` isn't sent. -- **New rows.** An issue or discussion made in Marfa is created in the repository its `github.in-repository` connection names, and a comment in its thread is posted. A discussion also needs a `category` that the repository has. -- **Relations.** A `github.sub-issue-of`, `github.blocked-by` or `github.relates-to` connection drawn or removed in Marfa is made or removed on GitHub. -- **Removal.** Trashing an open issue closes it as not planned, and a discussion is left as it is. Trashing a comment the App wrote deletes it. +- **New items.** An issue or discussion made in Marfa is created in the repository its `github.in-repository` edge names, and a comment in its thread is posted. A discussion also needs a `category` that the repository has. +- **Relations.** A `github.sub-issue-of`, `github.blocked-by` or `github.relates-to` edge made or removed in Marfa is made or removed on GitHub. +- **Removal.** Moving an open issue to the bin closes it as not planned, and a discussion is left as it is. Moving a comment the App wrote to the bin deletes it. Repositories, pull requests, notifications, files, repository files, directories, duplicates and closing links are always read only, whatever `GITHUB_READ_ONLY` says. A change to one in Marfa is put back from GitHub. Edits to someone else's comment are put back too. @@ -71,15 +71,15 @@ Repositories, pull requests, notifications, files, repository files, directories ## What to know - **Pick one way to sign in.** The connector refuses a personal token set beside the App settings. -- **Every issue, discussion and comment syncs.** With an App, each one on the instance is sent to GitHub, whoever made it. A row meant to stay in Marfa is a core task, note or message instead. +- **Every issue, discussion and comment syncs.** With an App, each one on the instance is sent to GitHub, whoever made it. An item meant to stay in Marfa is a core task, note or message instead. - **Changes arrive on a schedule, unless the webhook is on.** With the App, setting `GITHUB_PUBLIC_URL` turns on GitHub's webhook, and changes arrive within seconds. A personal token has no webhook. -- **`private` is a marker, not a lock.** Each row says whether its repository is private. Marfa gives a key access by type, not by that field, so read it before you copy a row somewhere public. -- **GitHub can refuse a change,** such as one in an archived repository. The run names it and does not send it again until the row changes. -- **Stars are read on scheduled runs only.** A repository loses its star mark only after two scheduled runs in a row list your stars and lack it. A repository that a process syncs, or once synced, only loses its mark. A repository kept for its star alone is archived, unless an earlier version of the connector wrote its row: that row only loses its mark, so archive it in Marfa yourself if you no longer want it. +- **`private` is a marker, not a lock.** Each item says whether its repository is private. Marfa gives a key access by type, not by that field, so read it before you copy an item somewhere public. +- **GitHub can refuse a change,** such as one in an archived repository. The run names it and does not send it again until the item changes. +- **Stars are read on scheduled runs only.** A repository loses its star mark only after two scheduled runs in a row list your stars and lack it. A repository that a process syncs, or once synced, only loses its mark. A repository kept for its star alone is archived, unless an earlier version of the connector wrote its item: that item only loses its mark, so archive it in Marfa yourself if you no longer want it. - **Two processes on one instance need one star reader.** To read repositories under two owners, run the connector twice, each with its own token, key and `GITHUB_REPOSITORIES`, and no repository in both. Every key after the first passes `--claim github`. Set `GITHUB_STARS=false` on every process but one, or each unmarks the stars the other marked. Give each repository to one process for good: one that moves is archived by the first process, and you restore it in Marfa. Run the same version in both. - **It never marks a notification read.** - **Repository files need Contents read access.** A personal token needs it on the repositories `GITHUB_FILES` names, and the App asks for it. A repository the run doesn't sync, a path that isn't on the default branch, a tree GitHub cuts short or refuses, and a file over 512 KiB or not in UTF-8 are named in the run, and nothing is archived for them. -- **Trashing a directory in Marfa trashes the directories below it,** since `parent-of` cascades, but not the files in them. The directories stay in the bin until you restore them; a later run doesn't bring them back. A trashed file stays in the bin the same way. +- **Moving a directory to the bin in Marfa takes the directories below it too,** since `parent-of` cascades, but not the files in them. The directories stay in the bin until you restore them; a later run doesn't bring them back. A file in the bin stays there the same way. ## Run it diff --git a/content/docs/catalog/connectors/google-calendar.mdx b/content/docs/catalog/connectors/google-calendar.mdx index 7148e6ac..7d09e85c 100644 --- a/content/docs/catalog/connectors/google-calendar.mdx +++ b/content/docs/catalog/connectors/google-calendar.mdx @@ -8,7 +8,7 @@ The Google Calendar connector keeps the calendars and events of one Google accou ## What it reads - **Calendars.** Every calendar in the account's calendar list that the account owns or can write to, including hidden ones. It skips the Birthdays calendar, and skips a calendar the account can only read unless you name it in `GOOGLE_CALENDAR_READ_ONLY_CALENDARS`. -- **Events.** Every event of those calendars, past and canceled included, because Google reports a deletion as a canceled event. A canceled event archives its row. It skips working-location events, which are not appointments. +- **Events.** Every event of those calendars, past and canceled included, because Google reports a deletion as a canceled event. A canceled event archives its item. It skips working-location events, which are not appointments. After the first read, the connector asks Google only for what changed since the last run. @@ -17,24 +17,24 @@ After the first read, the connector asks Google only for what changed since the | Type | Parent type | Holds | | --- | --- | --- | | `google.calendar.calendar` | None | A calendar, read only. Its `access_role` says what the account may do on it. | -| `google.calendar.event` | [Event](/catalog/event) | An event, linked to its calendar by the `google.calendar.in-calendar` connection. | +| `google.calendar.event` | [Event](/catalog/event) | An event, linked to its calendar by the `google.calendar.in-calendar` edge. | -A recurring event is one row whose `recurrence` holds Google's rules, and Marfa expands the occurrences. A moved or edited occurrence is its own row, with `original_starts_at` set to the time it replaces, and sits beneath its series through `parent-of`. A canceled occurrence is added to the series as an `EXDATE` line. +A recurring event is one item whose `recurrence` holds Google's rules, and Marfa expands the occurrences. A moved or edited occurrence is its own item, with `original_starts_at` set to the time it replaces, and sits beneath its series through `parent-of`. A canceled occurrence is added to the series as an `EXDATE` line. -An event row holds its guests twice, both read only: +An event item holds its guests twice, both read only: - **`attendees`**, the [core event's](/catalog/event) field, has each guest's `name` and `email` where Google gives them, and a `response` where the guest replied. A guest with neither a name nor a usable address is left out. - **`guests`** keeps Google's own list, with the `optional`, `self`, `organizer` and `resource` flags that `attendees` has no place for. -An event row also holds `calendar_id`, `organizer`, and `editable`, which says whether Marfa may change the event, with `uneditable_reason` when it may not. A change in Marfa to a field Google holds, such as `organizer`, `attendees` or `guests`, is put back. +An event item also holds `calendar_id`, `organizer`, and `editable`, which says whether Marfa may change the event, with `uneditable_reason` when it may not. A change in Marfa to a field Google holds, such as `organizer`, `attendees` or `guests`, is put back. ## What goes back -- **A new event row.** It becomes an event on the calendar its `google.calendar.in-calendar` connection names, or on the primary calendar if it has none. The connector refuses to make an event on a calendar the account can only read. -- **Edits.** The `title`, `description`, `place`, `status` (`confirmed` or `tentative`) and times, and a series' `recurrence`. Connecting the row to another calendar moves the event. -- **Removal.** Trashing a row deletes its event, and restoring the row brings the event back. +- **A new event item.** It becomes an event on the calendar its `google.calendar.in-calendar` edge names, or on the primary calendar if it has none. The connector refuses to make an event on a calendar the account can only read. +- **Edits.** The `title`, `description`, `place`, `status` (`confirmed` or `tentative`) and times, and a series' `recurrence`. Pointing the item's `google.calendar.in-calendar` edge at another calendar moves the event. +- **Removal.** Moving an event to the bin deletes it in Google Calendar, and restoring it brings it back. -Google holds a description as HTML, so the connector reads it as Markdown and sends it back as HTML. It sends bold, italic, links, lists, quotes, code and line breaks. Google has no headings, so a heading becomes bold text, and a table becomes a line of text for each row. The row shows the Google form after the next read. Text that looks like a tag, such as `
`, stays text, and text Markdown would show differently, such as `\*not emphasis\*`, is sent so that Google shows the characters and the read gives the escapes back. A description Google holds that is longer than 600,000 characters, has more than 25,000 elements, or nests more than 200 levels deep is left out of the row, and an edit to such an event's description is still sent. A description in Marfa is refused when it is longer than 100,000 characters, nests deeper than the converter reads, or has a paragraph with so many `*` and `_` marks that converting it would take seconds. Nothing is sent for it, the run names the event, and the run carries on. +Google holds a description as HTML, so the connector reads it as Markdown and sends it back as HTML. It sends bold, italic, links, lists, quotes, code and line breaks. Google has no headings, so a heading becomes bold text, and a table becomes a line of text for each row. The item shows the Google form after the next read. Text that looks like a tag, such as `
`, stays text, and text Markdown would show differently, such as `\*not emphasis\*`, is sent so that Google shows the characters and the read gives the escapes back. A description Google holds that is longer than 600,000 characters, has more than 25,000 elements, or nests more than 200 levels deep is left out of the item, and an edit to such an event's description is still sent. A description in Marfa is refused when it is longer than 100,000 characters, nests deeper than the converter reads, or has a paragraph with so many `*` and `_` marks that converting it would take seconds. Nothing is sent for it, the run names the event, and the run carries on. Every write sets `sendUpdates=none`, and none names guests. @@ -52,8 +52,8 @@ The Google settings are those of every Google connector, and [Create a Google Cl ## What to know -- **Marfa does not change an event with other guests.** `editable` is `false` when the account can only read the calendar, someone else organizes the event, Google reserves its kind, or it has guests besides the account, since a change would reach their calendars and Marfa sends no invitation. A change to such a row is put back, and the run raises `declined:`. -- **A safety stop holds a large removal.** If one run would remove more than 10 events and more than a quarter of a calendar, nothing is sent and the run fails. Restore the rows in Marfa to keep the events, or set `GOOGLE_CALENDAR_CONFIRM_DELETES` to the value the condition names for one run. +- **Marfa does not change an event with other guests.** `editable` is `false` when the account can only read the calendar, someone else organizes the event, Google reserves its kind, or it has guests besides the account, since a change would reach their calendars and Marfa sends no invitation. A change to such an item is put back, and the run raises `declined:`. +- **A safety stop holds a large removal.** If one run would remove more than 10 events and more than a quarter of a calendar, nothing is sent and the run fails. Restore the items in Marfa to keep the events, or set `GOOGLE_CALENDAR_CONFIRM_DELETES` to the value the condition names for one run. - **Times keep their zone.** A timed event stores UTC instants with its `timezone`. An all-day event stores local midnights in the event's zone, or else the calendar's. - **A series cannot stop repeating from Marfa.** A `recurrence` with no rules left is refused. - **One Google account for each instance.** A grant for another account fails the run. @@ -61,4 +61,4 @@ The Google settings are those of every Google connector, and [Create a Google Cl ## Run it -[Run a connector](/run/run-a-connector) runs any connector. This one's key also holds write on the `google.calendar.in-calendar` and `parent-of` connections, and its [README](https://github.com/withmarfa/marfa-connectors/blob/main/connectors/google-calendar/README.md) has the `marfa keys create` command. +[Run a connector](/run/run-a-connector) runs any connector. This one's key also holds write on the `google.calendar.in-calendar` and `parent-of` edges, and its [README](https://github.com/withmarfa/marfa-connectors/blob/main/connectors/google-calendar/README.md) has the `marfa keys create` command. diff --git a/content/docs/catalog/connectors/index.mdx b/content/docs/catalog/connectors/index.mdx index efd23a9e..97c02724 100644 --- a/content/docs/catalog/connectors/index.mdx +++ b/content/docs/catalog/connectors/index.mdx @@ -19,7 +19,7 @@ Marfa ships six, in the [withmarfa/marfa-connectors](https://github.com/withmarf ## What every connector shares - **Its own key.** Each connector refuses a key that holds more, or less, than it needs. -- **The feed.** Rows land in the [feed](/items-and-types#library-and-feed). Moving a row to the library is a person's or an app's act. A connector names the library only for a file that belongs to something already there, such as a picture the RSS or Readwise Reader connector saves for something you kept. +- **The feed.** Items land in the [feed](/items-and-types#library-and-feed). Moving an item to the library is a person's or an app's act. A connector names the library only for a file that belongs to something already there, such as a picture the RSS or Readwise Reader connector saves for something you kept. - **Nothing kept on your machine.** A connector keeps its progress on the instance, and takes its secrets from its environment. - **Two settings every connector reads.** `MARFA_API_URL` is the address of your instance and `MARFA_API_KEY` is the connector's key. The pages below list only the settings that are specific to a connector. diff --git a/content/docs/catalog/connectors/readwise-reader.mdx b/content/docs/catalog/connectors/readwise-reader.mdx index 9f6af3e8..737f63fe 100644 --- a/content/docs/catalog/connectors/readwise-reader.mdx +++ b/content/docs/catalog/connectors/readwise-reader.mdx @@ -22,15 +22,15 @@ Each run reads what changed since the last, with a five-minute overlap, and a fu | `readwise.highlight` | [Highlight](/catalog/highlight) | A highlight with its annotation and tags | | `readwise.file` | [File](/catalog/file) | Kept content, attached to its document or highlight | -The Readwise connector writes the same types, so one row can hold what both connectors know. A row Reader manages carries a Reader link, `:reader-document:` or `:reader-highlight:`, in `canonical_key`. A highlight connects to its document through `references`, and a file connects to what it belongs to through `attached-to`. +The Readwise connector writes the same types, so one item can hold what both connectors know. An item Reader manages carries a Reader link, `:reader-document:` or `:reader-highlight:`, in `canonical_key`. A highlight connects to its document through `references`, and a file connects to what it belongs to through `attached-to`. ## What goes back - **Document edits.** The title, author, summary (`description`), notes, tags, reading location (`reader_location`) and supported categories. - **Highlight edits.** The annotation (`note`) and tags. An empty note or tag list clears them. -- **A new row.** Set `reader_create: true` and your `account` label on a `readwise.source` that has a stable HTTP URL, or on a `readwise.highlight` that has `reader_parent_id` and passage text. A row without that flag is never created in Reader. +- **A new item.** Set `reader_create: true` and your `account` label on a `readwise.source` that has a stable HTTP URL, or on a `readwise.highlight` that has `reader_parent_id` and passage text. An item without that flag is never created in Reader. -Only changed supported fields are sent. The body, a passage, a highlight's `title` and `body`, reading progress and an original file stay read only. Moving a row to the bin changes nothing in Reader. +Only changed supported fields are sent. The body, a passage, a highlight's `title` and `body`, reading progress and an original file stay read only. Moving an item to the bin changes nothing in Reader. ## Archive and restore @@ -49,17 +49,17 @@ A document's HTML often shows pictures from other websites. `READWISE_READER_PIC - **`addresses`:** nothing is fetched, and the manifest lists each such picture as not kept, with the reason `third-party`. - **`save`:** the connector saves the pictures of every document. -- **`save-in-library`:** it saves them only for documents whose `readwise.source` row is in the [library](/items-and-types#library-and-feed). +- **`save-in-library`:** it saves them only for documents whose `readwise.source` item is in the [library](/items-and-types#library-and-feed). **The HTML isn't changed.** A picture is the address in an `img` element's `src`, or its `data-src` when it has no `src`, or the document's cover `image_url`. Each whole address is one picture, so addresses that differ only in the query are two. Other references, such as `srcset` candidates and relative or `data:` addresses, are never fetched. -A saved picture is a `readwise.file` row with `resource_kind` `picture`, its bytes, `mime_type`, and `source_url`, the address it came from. An `attached-to` connection runs from it to its document, and it takes the document's tier when it's created. +A saved picture is a `readwise.file` item with `resource_kind` `picture`, its bytes, `mime_type`, and `source_url`, the address it came from. An `attached-to` edge runs from it to its document, and it takes the document's tier when it's created. **Pictures never hold up a document's content.** Picture work runs after the listing and the content in each scheduled run, with its own queue and limits, and reads each document from Reader once. A run that a webhook started, or that Reader asks to slow down, leaves pictures to the next run. Turning the setting on reaches documents already kept over later runs, without walking the Reader listing again. The manifest lists each picture as: -- **Kept:** Marfa holds the picture and its `attached-to` connection. This holds whichever connector wrote the manifest, so one the Readwise connector writes lists the pictures this one saved. +- **Kept:** Marfa holds the picture and its `attached-to` edge. This holds whichever connector wrote the manifest, so one the Readwise connector writes lists the pictures this one saved. - **Not kept,** with a reason: `third-party` (not saved yet, or not under this setting), `not_https`, `over-document-limit` (past the first 50 of a document), or `removed` (a person moved it to the bin, archived or purged it, so it's never fetched again). - **Unavailable,** with the reason the fetch gave and any HTTP status. The fetch refuses a private address, a known tracker's counting pixel, an SVG, a one-pixel image, and a response that isn't a picture or is too large. A timeout, a failed connection or upload, or an HTTP 408, 425, 429 or 5xx is tried in three runs in all. @@ -82,10 +82,10 @@ Each run fetches at most 100 pictures and 200 MiB, from at most 25 documents, an - **Give Reader its own key source.** The key's own source is `readwise-reader` and it also claims `readwise`, which you set through the instance's private socket with `--claim readwise`. The Readwise connector's key uses the source `readwise`, so the two keep separate progress. - **Use one account label in both connectors.** Never change it against saved progress. - **Purges are capped.** With `READWISE_READER_PROPAGATE_PURGE` on, a run that would delete more than 20 Reader records fails before it reads or sends anything. It keeps failing until the count is 20 or fewer or you turn propagation off. -- **A disappearance needs proof.** A failed request or an incomplete listing never archives a row. More than 20 confirmed disappearances archive none. +- **A disappearance needs proof.** A failed request or an incomplete listing never archives an item. More than 20 confirmed disappearances archive none. - **Over-limit content stays pending.** A resource larger than `READWISE_CONTENT_MAX_BYTES` keeps its last good copy and reports the limit. - **An existing Readwise install needs a schema step first.** Replace the two type definitions as the [Readwise README](https://github.com/withmarfa/marfa-connectors/blob/main/connectors/readwise/README.md#replace-existing-schemas) describes, then restart both connectors. A change to a field's kind needs the same step, as [Upgrade to highlight titles and field kinds](https://github.com/withmarfa/marfa-connectors/blob/main/connectors/readwise/README.md#upgrade-to-highlight-titles-and-field-kinds) describes. ## Run it -[Run a connector](/run/run-a-connector) runs any connector. This one's key holds write on `readwise.source`, `readwise.highlight` and `readwise.file`, and on the `references` and `attached-to` connections, and its [README](https://github.com/withmarfa/marfa-connectors/blob/main/connectors/readwise-reader/README.md) lists what the key needs. Saving pictures needs no other permission. +[Run a connector](/run/run-a-connector) runs any connector. This one's key holds write on `readwise.source`, `readwise.highlight` and `readwise.file`, and on the `references` and `attached-to` edges, and its [README](https://github.com/withmarfa/marfa-connectors/blob/main/connectors/readwise-reader/README.md) lists what the key needs. Saving pictures needs no other permission. diff --git a/content/docs/catalog/connectors/readwise.mdx b/content/docs/catalog/connectors/readwise.mdx index 99fddb9c..d56e447e 100644 --- a/content/docs/catalog/connectors/readwise.mdx +++ b/content/docs/catalog/connectors/readwise.mdx @@ -19,15 +19,15 @@ The first run exports everything. Later runs ask for what changed since the last | `readwise.highlight` | [Highlight](/catalog/highlight) | A highlight with its annotation and tags | | `readwise.file` | [File](/catalog/file) | Content kept for a document that Readwise Reader manages | -Each highlight connects to its source through `references`. A row's `source_id` is `:source:` or `:highlight:`, where the account is your `READWISE_ACCOUNT`. Readwise's original timestamps, location kinds and colors are kept as properties. +Each highlight connects to its source through `references`. An item's `source_id` is `:source:` or `:highlight:`, where the account is your `READWISE_ACCOUNT`. Readwise's original timestamps, location kinds and colors are kept as properties. Each highlight has a `title` and a `body`, which apps show as its title and text. The title is the passage's first paragraph as plain text, cut at a word boundary to at most 80 characters. A blank line ends a paragraph and a heading is a paragraph of its own, so lines with a single line break between them are joined. The title leaves out list and quotation markers at its start, a heading's permalink mark, and closes up the gap before a closing punctuation mark. A passage that holds only a picture is titled after its source, as in "Picture from Deep Work". The body is the whole passage as Markdown, pictures included. -Dates and times are `datetime` [fields](/catalog/field-kinds), `reader_published_date` is a `date`, and addresses are `url` fields. **A value Readwise sends blank is left out.** A value its field's kind refuses is kept as sent under `original_values`, by field name, so one odd value never stops a row being written. +Dates and times are `datetime` [fields](/catalog/field-kinds), `reader_published_date` is a `date`, and addresses are `url` fields. **A value Readwise sends blank is left out.** A value its field's kind refuses is kept as sent under `original_values`, by field name, so one odd value never stops an item being written. ## What goes back -Nothing. The connector sends no write to Readwise, and trashing or purging a row in Marfa deletes nothing there. An export tombstone archives only the row it names. A missing record, an empty page or a 404 never archives anything. +Nothing. The connector sends no write to Readwise, and moving an item to the bin or purging it in Marfa deletes nothing there. An export tombstone archives only the item it names. A missing record, an empty page or a 404 never archives anything. ## Settings @@ -39,13 +39,13 @@ Nothing. The connector sends no write to Readwise, and trashing or purging a row ## What to know -- **Keep the account label.** It is part of every row's identity, so keep it the same across token changes and reinstallations. Changing it against saved progress is refused. +- **Keep the account label.** It is part of every item's identity, so keep it the same across token changes and reinstallations. Changing it against saved progress is refused. - **One Readwise account for each connector.** The label is never worked out from the token. -- **Readwise Reader shares these rows.** If you also run [Readwise Reader](/catalog/connectors/readwise-reader), give both the same label. Reader supplies the canonical passage, and this connector keeps a differing export passage in `readwise_text`. -- **An existing install needs a schema step.** The field kinds change existing fields, so replace both type definitions as the [README](https://github.com/withmarfa/marfa-connectors/blob/main/connectors/readwise/README.md#upgrade-to-highlight-titles-and-field-kinds) describes, then run the connector straight away. Its first run walks everything once and rewrites each row in place, adding titles and dropping blank values. Until then, an edit in Marfa to a row that holds a blank address or date is refused. +- **Readwise Reader shares these items.** If you also run [Readwise Reader](/catalog/connectors/readwise-reader), give both the same label. Reader supplies the canonical passage, and this connector keeps a differing export passage in `readwise_text`. +- **An existing install needs a schema step.** The field kinds change existing fields, so replace both type definitions as the [README](https://github.com/withmarfa/marfa-connectors/blob/main/connectors/readwise/README.md#upgrade-to-highlight-titles-and-field-kinds) describes, then run the connector straight away. Its first run walks everything once and rewrites each item in place, adding titles and dropping blank values. Until then, an edit in Marfa to an item that holds a blank address or date is refused. - **Rate limits are honored.** A 429 saves its `Retry-After` deadline across restarts. A failed request or malformed answer fails the run and never becomes an empty import. - **Requests time out after 30 seconds**, refuse redirects, and read at most 32 MiB of an answer. ## Run it -[Run a connector](/run/run-a-connector) runs any connector. This one's key holds write on `readwise.source`, `readwise.highlight` and `readwise.file`, and on the `references` and `attached-to` connections, and its [README](https://github.com/withmarfa/marfa-connectors/blob/main/connectors/readwise/README.md) has the `marfa keys create` command. +[Run a connector](/run/run-a-connector) runs any connector. This one's key holds write on `readwise.source`, `readwise.highlight` and `readwise.file`, and on the `references` and `attached-to` edges, and its [README](https://github.com/withmarfa/marfa-connectors/blob/main/connectors/readwise/README.md) has the `marfa keys create` command. diff --git a/content/docs/catalog/connectors/rss.mdx b/content/docs/catalog/connectors/rss.mdx index 5ae1e2b3..8e323ea0 100644 --- a/content/docs/catalog/connectors/rss.mdx +++ b/content/docs/catalog/connectors/rss.mdx @@ -3,7 +3,7 @@ title: RSS description: The entries of the Atom, RSS and JSON feeds you list, read only, and on request the pictures they point to, saved as files. --- -The RSS connector reads the feeds you list and writes one row for each entry into the feed. Nothing goes back to a feed. With `RSS_PICTURES` set, it also saves an entry's pictures as files attached to the entry. +The RSS connector reads the feeds you list and writes one item for each entry into the feed. Nothing goes back to a feed. With `RSS_PICTURES` set, it also saves an entry's pictures as files attached to the entry. ## What it reads @@ -16,7 +16,7 @@ Atom, RSS 2.0, RSS 1.0 (RDF) and JSON Feed, judged by the document rather than i | `rss.entry` (RSS Entry) | [Bookmark](/catalog/bookmark) | One entry of a feed | | `rss.picture` (RSS Picture) | [File image](/catalog/file#core-file-image) | One picture saved from an entry. Declared only when `RSS_PICTURES` is `save` or `save-in-library` | -An entry row holds: +An entry item holds: - **`url`, `title` and `description`:** the entry's link, and its title and summary as plain text. - **`body`:** the entry's content as Markdown. HTML is converted, keeping its structure, links and images and dropping scripts, styles, frames and tracking pixels. Plain text is kept as it is. @@ -26,7 +26,7 @@ An entry row holds: - **`feed_hash`:** the same for every entry of one feed. - **`rss_pictures`:** what became of the entry's pictures ([What an entry records](#what-an-entry-records)). -A row's `source_id` is the feed's hash, a colon and the entry's ID. An entry with no ID and no link is left out. A field longer than 100,000 characters is left out of its entry, and the run names it. +An item's `source_id` is the feed's hash, a colon and the entry's ID. An entry with no ID and no link is left out. A field longer than 100,000 characters is left out of its entry, and the run names it. ## Saving pictures @@ -41,9 +41,9 @@ The connector never moves an entry to the library. The connector reads an entry's `image_url`, then the Markdown images in its `body`, each address once, up to 20 for each entry. It fetches only `https` addresses, never a private host unless `RSS_PRIVATE_HOSTS` allows it, and keeps only PNG, JPEG, GIF, WebP, AVIF and HEIC files, judged by their bytes. An `http` address is recorded as not kept, and an SVG file is refused. The [README](https://github.com/withmarfa/marfa-connectors/blob/main/connectors/rss/README.md#what-is-saved) has every fetch rule. -### The picture rows +### The picture items -Each saved picture is an `rss.picture` row with the source `rss`, and an `attached-to` connection runs from it to its entry. +Each saved picture is an `rss.picture` item with the source `rss`, and an `attached-to` edge runs from it to its entry. | Field | What it holds | | --- | --- | @@ -51,7 +51,7 @@ Each saved picture is an `rss.picture` row with the source `rss`, and an `attach | `title` | `p-`, 12 characters of a hash of that address and the extension for its type, such as `p-0123456789ab.png` | | `blob_ref`, `mime_type` | The file and its type, as for every file | -To find an entry's pictures, read the `attached-to` connections that end at the entry, and match each to an address in the entry's text by `source_url`, never by title. +To find an entry's pictures, read the `attached-to` edges that end at the entry, and match each to an address in the entry's text by `source_url`, never by title. ### What an entry records @@ -82,7 +82,7 @@ A picture a person moved to the bin or purged is never saved again for that entr ## What goes back -Nothing. The connector never writes to a feed, and never archives an entry, moves it to the bin or removes it. An entry a feed drops stays as last written, and so do the rows of a feed you take out of `RSS_FEEDS`. A row you move to the bin or purge isn't written back. The only rows it archives are pictures, as above. +Nothing. The connector never writes to a feed, and never archives an entry, moves it to the bin or removes it. An entry a feed drops stays as last written, and so do the items of a feed you take out of `RSS_FEEDS`. An item you move to the bin or purge isn't written back. The only items it archives are pictures, as above. ## Settings @@ -92,11 +92,11 @@ Nothing. The connector never writes to a feed, and never archives an entry, move | `RSS_PRIVATE_HOSTS` | Host names that may resolve to this machine or a private network, separated by whitespace. Optional. | | `RSS_PICTURES` | `addresses` (the default), which fetches nothing, `save` or `save-in-library`. Any other value stops the start. Optional. | -A feed in `RSS_FEEDS` is an `http` or `https` address, or `name=address`. **Name a feed whose address or token may change:** the name then identifies the feed, and its rows carry on. Without a name, a changed address or query starts new rows. Credentials in an address go as Basic authorization to that address's origin only. +A feed in `RSS_FEEDS` is an `http` or `https` address, or `name=address`. **Name a feed whose address or token may change:** the name then identifies the feed, and its items carry on. Without a name, a changed address or query starts new items. Credentials in an address go as Basic authorization to that address's origin only. ## What to know -- **A feed's address is a secret,** since a private feed's address carries its token. Logs, reports and rows carry only the feed's origin and hash. +- **A feed's address is a secret,** since a private feed's address carries its token. Logs, reports and items carry only the feed's origin and hash. - **It never fetches from a private network** unless `RSS_PRIVATE_HOSTS` names the host, whether a feed lists it or redirects to it. A picture may use a listed host only when it's the host of the entry's own feed. - **A feed is skipped, and named in the run,** when it's too large, too slow to read or answers with a status other than 200 or 304. A 410 stops it until you change its address. A 301 or 308 is followed, and each run says the feed moved. The [README](https://github.com/withmarfa/marfa-connectors/blob/main/connectors/rss/README.md#limits-and-conditions) has the limits. - **An entry without a date** is dated by when the connector first stored it, and keeps that date. @@ -106,7 +106,7 @@ A feed in `RSS_FEEDS` is an `http` or `https` address, or `name=address`. **Name ## Run it -[Run a connector](/run/run-a-connector) uses this connector as its example, with `RSS_PICTURES` left at `addresses`. To save pictures, the key also needs write on `rss.picture` and on the `attached-to` connection: +[Run a connector](/run/run-a-connector) uses this connector as its example, with `RSS_PICTURES` left at `addresses`. To save pictures, the key also needs write on `rss.picture` and on the `attached-to` edge: ```bash marfa keys create --label rss --source rss --type-permission rss.entry=write \ diff --git a/content/docs/catalog/connectors/todoist.mdx b/content/docs/catalog/connectors/todoist.mdx index 06ae21ad..92d71ba9 100644 --- a/content/docs/catalog/connectors/todoist.mdx +++ b/content/docs/catalog/connectors/todoist.mdx @@ -7,9 +7,9 @@ The Todoist connector keeps the tasks of one Todoist account in the feed, and ca ## What it reads -Every task in the account, through Todoist's Sync API. The first run reads all open tasks, then the tasks completed in the last 12 weeks, and asks about each row still open in Marfa that neither list names. A task deleted in Todoist is archived, and one completed earlier is read as completed. +Every task in the account, through Todoist's Sync API. The first run reads all open tasks, then the tasks completed in the last 12 weeks, and asks about each item still open in Marfa that neither list names. A task deleted in Todoist is archived, and one completed earlier is read as completed. -It also follows the account's projects. When a project is archived or deleted, or the account leaves a shared project, the next run archives the rows of its tasks, completed tasks and subtasks included. An older completed task in a project that is unarchived stays archived. +It also follows the account's projects. When a project is archived or deleted, or the account leaves a shared project, the next run archives the items of its tasks, completed tasks and subtasks included. An older completed task in a project that is unarchived stays archived. ## Types it writes @@ -21,7 +21,7 @@ Beside the core task fields, a `todoist.task` has these: | Field | What it holds | | --- | --- | -| `todoist_id` | The Todoist task this row is. A row without one has not been sent to Todoist yet. | +| `todoist_id` | The Todoist task this item is. An item without one has not been sent to Todoist yet. | | `project_id` | The project the task sits in. | | `section_id` | The section of that project, if it has one. | | `parent_id` | The task this one is a subtask of. Read only. | @@ -37,11 +37,11 @@ A repeating task also holds the [core task's](/catalog/task) `recurrence`, as RF Unless the connector is read only, these changes reach Todoist: -- **A new row.** A `todoist.task` made in Marfa becomes a task, in the project and section it names with its labels, or in the Inbox if it names no project. +- **A new item.** A `todoist.task` made in Marfa becomes a task, in the project and section it names with its labels, or in the Inbox if it names no project. - **Edits.** The title, description, priority, due date, `recurrence_text`, labels, project and section, and completing or reopening the task. Only fields that changed are sent, so an edit never undoes a move made in Todoist. -- **Trash.** Trashing a row deletes the task, and its subtasks, in Todoist. A restore makes the task again if Todoist no longer has it. +- **The bin.** Moving an item to the bin deletes the task, and its subtasks, in Todoist. A restore makes the task again if Todoist no longer has it. -The core `recurrence`, the parent and the order among siblings do not travel, and archiving a row sends nothing. +The core `recurrence`, the parent and the order among siblings do not travel, and archiving an item sends nothing. ## Settings @@ -52,11 +52,11 @@ The core `recurrence`, the parent and the order among siblings do not travel, an ## What to know -- **Every `todoist.task` syncs.** Whoever made a row and under whatever source, it is sent to Todoist. A task meant to stay in Marfa is a `core.task`. -- **Use one Todoist account for each instance.** A connector for a second account would create, in that account, every row of the type that has no link yet. +- **Every `todoist.task` syncs.** Whoever made an item and under whatever source, it is sent to Todoist. A task meant to stay in Marfa is a `core.task`. +- **Use one Todoist account for each instance.** A connector for a second account would create, in that account, every item of the type that has no link yet. - **A Todoist token cannot be narrowed to reading.** `TODOIST_READ_ONLY` is the only guard when you try the connector on a real account. -- **A repeat needs a date.** Clearing only the date of a row that still has a `recurrence_text` is put back, and the run names the row. Clear `recurrence_text` first, which leaves the task due once on its date. -- **Todoist can refuse a change**, such as a label or a move to a project that no longer exists. The run names the row. +- **A repeat needs a date.** Clearing only the date of an item that still has a `recurrence_text` is put back, and the run names the item. Clear `recurrence_text` first, which leaves the task due once on its date. +- **Todoist can refuse a change**, such as a label or a move to a project that no longer exists. The run names the item. ## Run it diff --git a/content/docs/run/export-and-restore.mdx b/content/docs/run/export-and-restore.mdx index 5ae73974..e30ae5fb 100644 --- a/content/docs/run/export-and-restore.mdx +++ b/content/docs/run/export-and-restore.mdx @@ -181,6 +181,6 @@ A restore refuses the whole archive when the file isn't a valid archive, lacks o - **Keys, webhooks and configuration.** Make keys, register webhooks and set the configuration again. - **Connected apps.** A restored `system.connection` item has `status: revoked`: it records past access rather than reconnecting the app, so sign the app in again. An item an app wrote gets `restored:` in front of its source, so a later sign-in can't overwrite it. -- **Rows as archived.** A restore takes an item exactly as archived, even when its type is no longer registered or has changed its fields. An item of a `core` or `system` type that the build ships is still held to its shape. +- **Items as archived.** A restore takes an item exactly as archived, even when its type is no longer registered or has changed its fields. An item of a `core` or `system` type that the build ships is still held to its shape. The rules behind this page are in the [export](https://github.com/withmarfa/marfa/blob/main/conformance/spec/search-and-filters.md#export) and [restore](https://github.com/withmarfa/marfa/blob/main/conformance/spec/search-and-filters.md#restore) sections of the search and filters chapter of the contract, with [`search-and-filters/archive-missing-unreadable`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/search-and-filters.md#search-and-filtersarchive-missing-unreadable) for a blob the key may not read. diff --git a/content/docs/run/folder-from-a-mac-app.mdx b/content/docs/run/folder-from-a-mac-app.mdx deleted file mode 100644 index 8c8edb8e..00000000 --- a/content/docs/run/folder-from-a-mac-app.mdx +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: Use a folder from a Mac app -description: Create a folder, bind a directory to it and confirm its first sync from a Mac app, with the Swift SDK's Folders. ---- - -A Mac app can do what `marfa folders` does. **The Swift SDK's `Folders` shares the Mac's list of folders with `marfa folders`.** [Sync a folder](/run/sync-a-folder) explains what a folder is and the CLI's steps. - -This code creates the folder, binds a directory and confirms the first sync, with `serverURL`, `key` and `storeURL` as in [Open a working copy](/swift-sdk/working-copy#open-a-copy). Until the first sync is confirmed, `sync(_:)` returns `.awaitingConfirmation` with the plan: - -```swift -import Marfa - -let server = Server(url: serverURL, key: key) -let copy = try await WorkingCopy.open(store: storeURL, server: server) -let folder = try await copy.createFolder( - FolderSettings(title: "Notes", query: FolderQuery(types: ["core.note"]))) - -let folders = Folders(server: server) -let directory = URL(filePath: "/Users/sam/Notes") -_ = try await folders.add(directory, following: folder.id) -_ = try await folders.sync(directory) // .awaitingConfirmation(plan) -_ = try await folders.confirmFirstSync(in: directory) -_ = try await folders.sync(directory) -``` - -[Folders on a Mac](/swift-sdk/views-and-folders#folders-on-a-mac) lists every call, including `changeFolder(_:_:baseVersion:)`. A client without the SDK calls [Create a folder](/api-reference/folders/createFolder) and the other folder operations. diff --git a/content/docs/run/folder-safeguards.mdx b/content/docs/run/folder-safeguards.mdx index 26001075..17e603ae 100644 --- a/content/docs/run/folder-safeguards.mdx +++ b/content/docs/run/folder-safeguards.mdx @@ -49,7 +49,7 @@ the server answered 0 write(s) A removal pauses when it takes **more than 10 files and more than a quarter of the folder's files**, which the `removal_threshold` setting changes. It also holds files whose items left the folder elsewhere, such as notes another app moved to the bin. -Cancel the removal to write the files back, or confirm it to move unchanged items to the bin at the next sync. Files attached to a note come back with the note, whether or not the query holds them. Confirmation checks for moves and remote edits too: a missing file found nowhere comes back if another device changed content it showed while the query still holds it. In Swift, `folders` and `directory` are those of [Use a folder from a Mac app](/run/folder-from-a-mac-app): +Cancel the removal to write the files back, or confirm it to move unchanged items to the bin at the next sync. Files attached to a note come back with the note, whether or not the query holds them. Confirmation checks for moves and remote edits too: a missing file found nowhere comes back if another device changed content it showed while the query still holds it. In Swift, `folders` and `directory` are those of [Set up a folder](/swift-sdk/views-and-folders#set-up-a-folder): ```bash tab="CLI" tab-group="client" marfa folders restore Notes @@ -69,8 +69,8 @@ if status.paused.isPaused { ## A missing or unreadable directory -- **A missing directory,** such as on a disk that isn't connected: Marfa trashes none of the folder's items, and `marfa folders status`, `confirm` and `restore` say the directory can't be found. -- **A scan that can't read or identify an entry:** Marfa trashes nothing for files it failed to see, and holds deletions already waiting until a complete scan succeeds. +- **A missing directory,** such as on a disk that isn't connected: Marfa moves none of the folder's items to the bin, and `marfa folders status`, `confirm` and `restore` say the directory can't be found. +- **A scan that can't read or identify an entry:** Marfa moves nothing to the bin for files it failed to see, and holds deletions already waiting until a complete scan succeeds. - **A different directory at the folder's path,** such as one a restore or a sync tool put there: Marfa keeps the folder listed but doesn't read the new directory as the folder. When the original returns, a watch of every folder carries on with it. ## What a folder never takes in diff --git a/content/docs/run/housekeeping.mdx b/content/docs/run/housekeeping.mdx index dbc6e27c..d6e8d57b 100644 --- a/content/docs/run/housekeeping.mdx +++ b/content/docs/run/housekeeping.mdx @@ -55,6 +55,8 @@ A job whose `last_outcome` stays `error`, or whose `next_run_at` is long past, i `feed-expiry` acts only on a type with a `feed_days` window in its [retention policy](/catalog/type-rules#retention-policy), set on the type or taken from a parent. Once more than that many days have passed since an active feed item arrived, the job moves it to the bin, as a delete does. A type with no window keeps its items, and so does the job for **library items, archived items and items brought back from the bin**. A moved item can be restored until the bin's retention ends, and then `trash-purge` deletes it. The interval is `FEED_EXPIRY_INTERVAL_MS`. +To watch `feed-expiry` act without waiting, a test server can move its clock forward, as the contract's [instance](https://github.com/withmarfa/marfa/blob/main/conformance/spec/instance.md#test-hooks) chapter describes. + Each run reports `moved` and `held` in `last_result`. `moved` includes items a cascade took with them. `held` counts expired items the job left alone because: - Deleting one would also delete an item that hasn't expired, isn't a feed item or was brought back from the bin. @@ -103,9 +105,9 @@ The server refuses `409 housekeeping_job_running` while a run of the same housek An item stays in the bin until it is purged. Left alone, `trash-purge` purges each item once it has been in the bin longer than `TRASH_RETENTION_DAYS`, which is 60 days unless you change it. `trash_retention_days` in the [configuration](/run/settings#change-settings-while-it-runs) overrides that while the server runs, and a retention of `0` keeps every item. A run removes a limited number of items. While items past the retention remain and the last run removed some, the job runs again at once. -It deletes the items and their history. Emptying the trash has no dry run, so read the trash first. +It deletes the items and their history. Emptying the bin has no dry run, so read the bin first. -To empty the trash through the private socket, use the owner's summary operation. **Each call attempts up to 200 selected items** and returns the committed purge count, per-item failures and a cursor for the next batch. Send `--cursor` or the HTTP `cursor` query parameter with the returned value until `next_cursor` is `null`. If a write outcome is unknown, read the trash again before retrying. The owner can also empty it from a recent browser session. +**The owner empties the bin** with `marfa items trash empty` over the private socket, or from a browser signed in within the last five minutes. Each call purges up to 200 items and answers how many it purged, any it couldn't, and a `next_cursor`. Send the cursor back, with `--cursor` or `?cursor=`, until `next_cursor` is `null`. If you don't know whether a call finished, read the bin before you send it again. ```bash tab="CLI" tab-group="client" env -u MARFA_API_URL -u MARFA_API_KEY -u MARFA_PROFILE marfa --socket /srv/marfa/data/control/marfa.sock items trash empty --confirm PURGE @@ -117,8 +119,6 @@ Origin: https://marfa.example.com Cookie: ``` -Continue a batch with `POST /trash/empty?cursor=`. The response reports `purged`, per-item `failures` and `next_cursor`. - A key holding `items.purge` can instead queue a bulk action for every item whose state is `trashed`. This dry run counts what a purge would take: @@ -152,77 +152,4 @@ Content-Type: application/json `marfa items bulk-action job ` shows the job again, and its `status` is `completed` once the purge has finished. A bin of more than 10,000 items is refused `400 bulk_cap_exceeded` unless you raise `--max-items`, which Marfa caps at 50,000: purge what's left with a second run. To purge one item, run `marfa items purge `. -## Try feed expiry with a moved clock - -A test server can move its clock forward, so you can watch `feed-expiry` act without waiting a day. Start it with `MARFA_TEST_HOOKS=true`, and **never** on an instance that holds real data: the server refuses it when `NODE_ENV` is `production`, which the container sets. - -The hooks sit at `/_control/test` on the private socket only, so run these steps on the instance's machine. `GET /_control/test/clock` answers `offset_ms` and the server's `now`. `POST /_control/test/clock` with `{ "advance_ms": 172800000 }` moves the clock forward by that many milliseconds, never back, and everything that reads the time moves with it. `MARFA_TEST_CLOCK_OFFSET_MS` starts the clock that far ahead and needs `MARFA_TEST_HOOKS`. - -**Two authorities are in play**: a key runs `marfa types register`, `marfa items create` and the content listing below, and the private socket reaches the clock and the owner summary operations but no item content. - - - - -### Put an expiring item in the feed - -With a key in `MARFA_API_KEY`, register a type whose feed items expire after one day, and create one item in the feed: - -```bash -marfa types register --body '{"id":"user.headline","fields":{"title":{"type":"string","required":true}},"retention_policy":{"feed_days":1}}' -marfa items create --type user.headline --prop title="Today" --tier feed -``` - - - - -### Move the clock two days forward - -Through the socket: - -```bash -curl --unix-socket /srv/marfa/data/control/marfa.sock \ - -X POST http://localhost/_control/test/clock \ - -H 'Content-Type: application/json' -d '{"advance_ms": 172800000}' -``` - - - - -### Run the job and read the bin - -Run `feed-expiry` now. `moved` is `1` when the job found the item: - - - -```bash -marfa housekeeping run feed-expiry -``` - -```json -{ - "name": "feed-expiry", - "started_at": "2026-10-23T15:45:53.273Z", - "finished_at": "2026-10-23T15:45:53.282Z", - "outcome": "ok", - "result": { - "moved": 1, - "held": 0, - "types": 1, - "unresolvable": 0 - }, - "error": null -} -``` - - - -Then list the bin, which now holds the item: - -```bash -marfa items list --state trashed --type user.headline -``` - - - - -The rules behind this page are in the [housekeeping](https://github.com/withmarfa/marfa/blob/main/conformance/spec/housekeeping.md) chapter of the contract, and the test hooks in the [instance](https://github.com/withmarfa/marfa/blob/main/conformance/spec/instance.md) chapter. +The rules behind this page are in the [housekeeping](https://github.com/withmarfa/marfa/blob/main/conformance/spec/housekeeping.md) chapter of the contract. diff --git a/content/docs/run/index.mdx b/content/docs/run/index.mdx index b83c6667..c48818cf 100644 --- a/content/docs/run/index.mdx +++ b/content/docs/run/index.mdx @@ -14,6 +14,7 @@ An instance is yours to run, so installing, upgrading and backing it up fall to Copy the data directory and secrets, and bring them back. Move to a new build, which updates the database after copying it. Keep a directory on your computer in step with your items, as Markdown files. + Build a connector, give it a key of its own, and run it once or on a schedule. The cause and the fix for a problem, found by the message Marfa shows. diff --git a/content/docs/run/keep-folders-running.mdx b/content/docs/run/keep-folders-running.mdx index 76baa9c4..1e7aa455 100644 --- a/content/docs/run/keep-folders-running.mdx +++ b/content/docs/run/keep-folders-running.mdx @@ -47,11 +47,9 @@ To watch one folder, name its directory: `marfa folders watch Notes`. ## What the watch does while it runs -A watch avoids rendering files whose contents are already in step. +A watch rewrites only the files a change affects: the changed item's file, files that show a linked item, files whose links change when a name changes, and any file the last pull couldn't write. **It rewrites every file** on its first pull, after a type or the folder's settings change, and when more than 4,096 items changed since the last pull. With nothing to do, it rewrites nothing. -- **When nothing changes and no work remains,** the watch can reuse the result of its last pull and render no file, until the hourly check is due. -- **When one file changes,** the watch sends the edit and chooses which files to render from what the change affects. These include the changed item's file, files showing linked items, and files whose links need disambiguating after a name changes. It renders every file on its first pull, when the working copy's generation or the folder's settings change, when more than 4,096 items changed since the last pull, or when a type changed. It also retries files the previous pull could not bring into step. -- **Once an hour** it renders every file, to check that choice. If it rewrites a file that the choice would have skipped, it reports `warning: the hourly check rewrote` followed by the file's path. A watch of every folder prefixes the warning with the directory. A file changed since the last pull can be rewritten during this check without a warning. +**Once an hour it rewrites every file** to check that choice. If that rewrites a file the choice skipped, it reports `warning: the hourly check rewrote `. The contract states these as [`folders/watch-quiet-pass-unrendered`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/folders.md#folderswatch-quiet-pass-unrendered), [`folders/watch-pull-renders-reached`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/folders.md#folderswatch-pull-renders-reached) and [`folders/watch-hourly-full-render`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/folders.md#folderswatch-hourly-full-render). diff --git a/content/docs/run/meta.json b/content/docs/run/meta.json index 37fa29fa..cb23191a 100644 --- a/content/docs/run/meta.json +++ b/content/docs/run/meta.json @@ -16,7 +16,6 @@ "---Folders---", "sync-a-folder", "folder-files", - "folder-from-a-mac-app", "keep-folders-running", "sync-from-login", "folder-safeguards", diff --git a/content/docs/run/run-a-connector.mdx b/content/docs/run/run-a-connector.mdx index cd11e41c..1a9198b6 100644 --- a/content/docs/run/run-a-connector.mdx +++ b/content/docs/run/run-a-connector.mdx @@ -95,14 +95,14 @@ marfa connectors list ## Check and manage it in the browser -The owner's **Connectors** page at `/auth/owner/connectors` lists registrations and their reported state. Open a connector to see its details, current problems and recent runs. **Failing** means its latest run failed; **Problems** means it reported an issue; **Not heard from** means it has not reported a heartbeat, run or registration for more than a day. Check the process where it runs for the cause. +The owner's **Connectors** page, at `/auth/owner/connectors`, lists each connector with its state. **Failing** means its last run failed, **Problems** that it reported a problem, and **Not heard from** that it hasn't reported for more than a day. Open one to see its problems and recent runs, then check the process where it runs. -The detail page offers two actions that cannot be undone: +A connector's page has two actions that can't be undone, and **neither stops the process or revokes its key**: -- **Clear saved state** removes the state and agreements for that connector's source. If a replacement key has registered under the same source, this also clears its saved state. The source's next run starts from the beginning. -- **Remove connector** deletes the selected registration and its runs. It also deletes the source's inbound endpoints and retained deliveries if this is its current registration. Removing an older registration leaves the successor's endpoints and deliveries in place. +- **Clear saved state** deletes what the connector keeps between runs, for every key under its source, so its next run starts from the beginning. +- **Remove connector** deletes the registration and its runs. If it's the current registration, it also deletes the source's [inbound endpoints](/build/connector-webhooks) and the deliveries they hold. -Neither action stops the external process or revokes its key. Stop the process where it runs and revoke the key separately if you no longer need it. The [connector contract](https://github.com/withmarfa/marfa/blob/main/conformance/spec/connectors.md) and [inbound webhook contract](https://github.com/withmarfa/marfa/blob/main/conformance/spec/inbound-webhooks.md#inbound-webhookssource-old-delete-keeps) define the effects. +The [connectors](https://github.com/withmarfa/marfa/blob/main/conformance/spec/connectors.md) and [inbound webhooks](https://github.com/withmarfa/marfa/blob/main/conformance/spec/inbound-webhooks.md#inbound-webhookssource-old-delete-keeps) chapters of the contract define both. ## Exit codes diff --git a/content/docs/run/settings.mdx b/content/docs/run/settings.mdx index fa5916ba..16e90b0e 100644 --- a/content/docs/run/settings.mdx +++ b/content/docs/run/settings.mdx @@ -46,7 +46,7 @@ The container's entrypoint reads one more name, `MARFA_REPLACE_NEWER_REPLICA`, w ## Every environment setting -The list includes the intervals and retention windows of the [housekeeping jobs](/run/housekeeping). The `MARFA_TEST_` settings are for the conformance harness. **Never set them on an instance that holds real data**: the server refuses `MARFA_TEST_HOOKS` when `NODE_ENV` is `production`, which the container sets. [Housekeeping](/run/housekeeping#try-feed-expiry-with-a-moved-clock) shows one use. +The list includes the intervals and retention windows of the [housekeeping jobs](/run/housekeeping). The `MARFA_TEST_` settings are for the conformance harness. **Never set them on an instance that holds real data**: the server refuses `MARFA_TEST_HOOKS` when `NODE_ENV` is `production`, which the container sets. The owner reads each setting's current value on the **All settings** page at `/auth/owner/settings`. A secret shows only whether it is set. diff --git a/content/docs/run/sign-in-as-the-owner.mdx b/content/docs/run/sign-in-as-the-owner.mdx index 62466f3d..a3efc930 100644 --- a/content/docs/run/sign-in-as-the-owner.mdx +++ b/content/docs/run/sign-in-as-the-owner.mdx @@ -43,7 +43,7 @@ When your sign-in is older than five minutes, these changes show a **Confirm it' Confirming replaces your browser session with a new one. A wrong password answers `401 unauthorized`, and after too many attempts `429 rate_limited` with `Retry-After`. A passkey still confirms. A refused confirmation leaves your session as it was. -The rule binds only a browser session. A key or the private socket is never asked to confirm. A program that holds an older browser session has no dialog, so it gets `403 forbidden` with `Sign in again to perform this operation`, and Marfa changes nothing. The dialog calls `POST /auth/confirm`, which takes the signed-in owner's password or a passkey assertion and answers `401 unauthorized` to a key or a request with no owner session. +**Only a browser asks you to confirm.** A key and the private socket never need to. A program holding a browser session older than five minutes gets `403 forbidden` with `Sign in again to perform this operation`, and Marfa changes nothing. ## Start from Home @@ -57,8 +57,8 @@ Open the instance's address in a browser signed in as the owner, and Home lists | Your data | [Items](#what-the-owners-browser-reaches) | `/auth/owner/items` | | Your data | [Trash](/run/housekeeping#empty-the-bin) | `/auth/owner/trash` | | Your data | [Export and restore](/run/export-and-restore) | `/auth/owner/export` | -| Connections | [Connectors](/connectors) | `/auth/owner/connectors` | -| Connections | [Folders](/devices#folders) | `/auth/owner/folders` | +| Connections | [Connectors](/run/run-a-connector#check-and-manage-it-in-the-browser) | `/auth/owner/connectors` | +| Connections | [Folders](/run/sync-a-folder) | `/auth/owner/folders` | | This Marfa | [General](/run/settings#change-settings-while-it-runs) | `/auth/owner/general` | | This Marfa | [Health](/run/housekeeping) | `/auth/owner/health` | | This Marfa | [Activity](/run/audit-log) | `/auth/owner/activity` | @@ -68,7 +68,7 @@ Open the instance's address in a browser signed in as the owner, and Home lists **Needs attention** on Home shows known health, job, connector and folder problems and keys nearing expiry. If a check fails, it is shown as unavailable rather than as healthy. The **Folders** pages show reported working copies and historical sign-ins; a recent report does not prove that a computer is online. -Opening an owner page without a session answers `302` to `/auth/sign-in` with the page as `return_to`, or to `/setup` when there is no owner yet. A program, one that sends `Authorization` or doesn't ask for HTML, gets no redirect. +An owner page opened without a sign-in sends the browser to the sign-in page, or to `/setup` before the instance has an owner, and back to the page afterwards. A request that sends `Authorization`, or doesn't ask for HTML, gets no redirect. ## What the owner's browser reaches diff --git a/content/docs/run/sync-a-folder.mdx b/content/docs/run/sync-a-folder.mdx index e21d54c4..f857425d 100644 --- a/content/docs/run/sync-a-folder.mdx +++ b/content/docs/run/sync-a-folder.mdx @@ -9,7 +9,7 @@ description: Keep a directory on your computer and a set of items in step, so ea A folder is a [device](/devices) whose working copy is a directory. **Its settings live on the instance** as a `system.folder` item, so every computer that follows the folder shares them, and each keeps its own copy and queue of writes. [Folder safeguards](/run/folder-safeguards) says when a folder removes a file or an item. -The steps use the CLI. A Mac app does the same with the Swift SDK, as [Use a folder from a Mac app](/run/folder-from-a-mac-app) shows. +The steps use the CLI. A Mac app does the same with the Swift SDK, as [Set up a folder](/swift-sdk/views-and-folders#set-up-a-folder) shows. diff --git a/content/docs/run/troubleshooting/google-connectors.mdx b/content/docs/run/troubleshooting/google-connectors.mdx index 67d5cd12..627a24d1 100644 --- a/content/docs/run/troubleshooting/google-connectors.mdx +++ b/content/docs/run/troubleshooting/google-connectors.mdx @@ -31,7 +31,7 @@ Check that `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` belong to the **Desktop ## A condition says the account is different (google:account) -The grant belongs to another Google account than the one the connector keeps, which is the account it first ran with, or the one its rows came from. +The grant belongs to another Google account than the one the connector keeps, which is the account it first ran with, or the one its items came from. Run `--setup ` again and sign in with the original account, or use another instance. From 402786830ef0fc27f3ec79f1e7233238c7ac145f Mon Sep 17 00:00:00 2001 From: August Cayzer Date: Sun, 1 Nov 2026 01:07:44 +0000 Subject: [PATCH 2/4] fix(llms): unpadded tables, sidebar order and no /docs alias --- package.json | 1 + pnpm-lock.yaml | 3 +++ source.config.ts | 5 +++++ src/lib/docs-map.test.ts | 8 ++++++++ src/lib/docs-map.ts | 2 +- src/lib/docs-tools.test.ts | 4 ++-- src/lib/docs-tools.ts | 1 - src/lib/mcp/tools.ts | 6 +++--- src/lib/source.ts | 29 ++++++++++++++++++++++++++++- 9 files changed, 51 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 53ce8ab1..10ecbe98 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@types/react-dom": "^19.3.0", "@vitejs/plugin-react": "^6.1.1", "jsdom": "^30.1.3", + "mdast-util-gfm-table": "^2.0.0", "tailwindcss": "^4.3.3", "typescript": "^7.0.2", "vitest": "^5.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 38704227..73a27eff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,6 +84,9 @@ importers: jsdom: specifier: ^30.1.3 version: 30.1.3 + mdast-util-gfm-table: + specifier: ^2.0.0 + version: 2.0.0 tailwindcss: specifier: ^4.3.3 version: 4.3.3 diff --git a/source.config.ts b/source.config.ts index 0d5de109..ddba1f1c 100644 --- a/source.config.ts +++ b/source.config.ts @@ -4,6 +4,7 @@ import { pageSchema } from 'fumadocs-core/source/schema'; import { readFileSync } from 'node:fs'; import { z } from 'zod'; import type { LLMsOptions } from 'fumadocs-core/mdx-plugins/remark-llms'; +import { gfmTableToMarkdown } from 'mdast-util-gfm-table'; import { changelogMarkdown, remarkChangelogIndex, type Release } from './src/lib/changelog'; import { statusLabel } from './src/lib/http-status'; import { sseLanguage } from './src/lib/sse-language'; @@ -92,6 +93,10 @@ function fileTree(node: JsxElement, depth = 0): string[] { * prose carries every fact a visual shows. */ const llms: LLMsOptions = { + // Tables without column alignment: padding each cell to its column's width + // would make the copies, and so llms-full.txt, about a fifth spaces and + // dashes. A later extension's handlers replace remark-gfm's. + extensions: [gfmTableToMarkdown({ tablePipeAlign: false })], filterElement(node) { if (!isJsx(node) || !node.name) return true; if (WRAPPERS.has(node.name)) return 'children-only'; diff --git a/src/lib/docs-map.test.ts b/src/lib/docs-map.test.ts index 1f4577e0..869e5da5 100644 --- a/src/lib/docs-map.test.ts +++ b/src/lib/docs-map.test.ts @@ -157,4 +157,12 @@ describe('getLlmsFullText', () => { expect(full).toContain('# Errors (/api-reference/errors)'); expect(full).not.toContain('# Create an item (/api-reference/items/createItem)'); }); + + it('holds the pages in sidebar order', async () => { + const full = await getLlmsFullText(); + const at = (url: string) => full.indexOf(`(${url})\n`); + const urls = ['/overview', '/introduction', '/quickstart', '/how-marfa-works', '/access', '/build', '/run', '/catalog']; + for (const url of urls) expect(at(url), url).toBeGreaterThanOrEqual(0); + for (let i = 1; i < urls.length; i += 1) expect(at(urls[i - 1]), urls[i]).toBeLessThan(at(urls[i])); + }); }); diff --git a/src/lib/docs-map.ts b/src/lib/docs-map.ts index ea6f93a2..3eb246e6 100644 --- a/src/lib/docs-map.ts +++ b/src/lib/docs-map.ts @@ -14,7 +14,7 @@ export function buildDocsMap(): string { [ '# Marfa documentation map', '', - 'Hierarchical index of every page on docs.marfa.so. Each link resolves to the human-readable HTML page; append `.md` for the markdown twin (e.g. `/items-and-types.md`).', + 'Every page on docs.marfa.so, as the sidebar groups them. Each link goes to the HTML page; add `.md` to a path for its Markdown copy, such as `/items-and-types.md`.', '', ], (url) => url, diff --git a/src/lib/docs-tools.test.ts b/src/lib/docs-tools.test.ts index 046cba9a..07a894fc 100644 --- a/src/lib/docs-tools.test.ts +++ b/src/lib/docs-tools.test.ts @@ -74,7 +74,7 @@ describe('pathToSlugs', () => { ['https://docs.marfa.so/x/y.md?z=1#top', ['x', 'y']], ['/x/y.md', ['x', 'y']], ['/x/y/', ['x', 'y']], - ['/docs/x/y', ['x', 'y']], + ['/docs/x/y', ['docs', 'x', 'y']], ['/docs-x/y', ['docs-x', 'y']], [' /x/y ', ['x', 'y']], ['/', []], @@ -91,7 +91,7 @@ describe('fetchDocsPage', () => { expect(page).toMatch(/^# Housekeeping \(\/run\/housekeeping\)\n/); expect(page).toContain('housekeeping jobs'); - for (const form of ['run/housekeeping', '/run/housekeeping.md', '/run/housekeeping/', '/docs/run/housekeeping']) { + for (const form of ['run/housekeeping', '/run/housekeeping.md', '/run/housekeeping/']) { expect(await fetchDocsPage(form), form).toBe(page); } }); diff --git a/src/lib/docs-tools.ts b/src/lib/docs-tools.ts index 74c1aea3..fdf4edae 100644 --- a/src/lib/docs-tools.ts +++ b/src/lib/docs-tools.ts @@ -143,7 +143,6 @@ export function pathToSlugs(input: string): string[] { } } p = p.replace(/^\/+/, ''); - p = p.replace(/^docs(?:\/|$)/, ''); p = p.replace(/\.md$/i, ''); p = p.replace(/\/+$/, ''); if (p === '') return []; diff --git a/src/lib/mcp/tools.ts b/src/lib/mcp/tools.ts index b7e96c3f..e3e62123 100644 --- a/src/lib/mcp/tools.ts +++ b/src/lib/mcp/tools.ts @@ -4,7 +4,7 @@ * Three read-only tools, each an adapter over `src/lib/docs-tools.ts`: * * - docs_search(query, limit?) full-text BM25 against the docs corpus - * - docs_fetch(path, section?) the markdown body for a docs URL, or one section of it + * - docs_fetch(path, section?) the Markdown body of a docs page, or one section of it * - docs_list_topics() every page as a flat list * * They are registered in that order, which is the order `tools/list` returns @@ -119,12 +119,12 @@ export function createDocsServer(): McpServer { DOCS_TOOL_NAMES[1], { description: - 'Fetch the markdown body of a single docs page by its URL path (e.g. "/build/write-safely"). Returns the same markdown the `.md` copy returns: a heading with the page title and path, then the full prose with code blocks preserved. Pass `section`, a heading from the page, to get only that heading and what is under it; an unknown heading is an error that lists the page\'s headings.', + 'Fetch the Markdown body of one docs page by its path, such as "/build/write-safely". Returns the same Markdown as the `.md` copy: a heading with the page title and path, then the full prose with code blocks preserved. Pass `section`, a heading from the page, to get only that heading and what is under it; an unknown heading is an error that lists the page\'s headings.', annotations: { readOnlyHint: true, openWorldHint: false }, inputSchema: z.object({ path: z .string() - .describe('Docs URL path. Accepts "/x/y", "x/y", or a legacy "/docs/x/y".'), + .describe('Docs page path. Accepts "/x/y", "x/y", a full docs.marfa.so address, or any of them ending in ".md".'), section: z .string() .optional() diff --git a/src/lib/source.ts b/src/lib/source.ts index 375caae5..a3845656 100644 --- a/src/lib/source.ts +++ b/src/lib/source.ts @@ -1,4 +1,5 @@ import { loader } from 'fumadocs-core/source'; +import type * as PageTree from 'fumadocs-core/page-tree'; import { docs } from 'collections/server'; import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons'; import { docsRoute } from './shared'; @@ -82,6 +83,32 @@ export function inLlmsFull(page: (typeof source)['$inferPage']) { return page.type !== 'openapi' && page.url !== '/changelog'; } +/** Page addresses in sidebar order: each tab, then its folders' landing pages and pages as they appear. */ +function sidebarOrder(): Map { + const order = new Map(); + const add = (url: string) => { + if (!order.has(url)) order.set(url, order.size); + }; + const walk = (nodes: PageTree.Node[]) => { + for (const node of nodes) { + if (node.type === 'page') add(node.url); + else if (node.type === 'folder') { + if (node.index) add(node.index.url); + walk(node.children); + } + } + }; + walk(source.getPageTree().children); + return order; +} + +/** The written pages in sidebar order, which `llms.txt` promises; a page outside the tree goes last. */ export async function getLlmsFullText() { - return (await Promise.all(source.getPages().filter(inLlmsFull).map(getLLMText))).join('\n\n'); + const order = sidebarOrder(); + const rank = (page: (typeof source)['$inferPage']) => order.get(page.url) ?? Number.MAX_SAFE_INTEGER; + const pages = source + .getPages() + .filter(inLlmsFull) + .sort((a, b) => rank(a) - rank(b)); + return (await Promise.all(pages.map(getLLMText))).join('\n\n'); } From ad16a3cc062c0a96277c8cd2f61a793abb5c3272 Mon Sep 17 00:00:00 2001 From: August Cayzer Date: Sun, 1 Nov 2026 01:12:07 +0000 Subject: [PATCH 3/4] docs(run): keep folder safeguards within its limit --- content/docs/(overview)/items-and-types.mdx | 2 +- content/docs/run/folder-safeguards.mdx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/content/docs/(overview)/items-and-types.mdx b/content/docs/(overview)/items-and-types.mdx index a40bcce1..0abf5867 100644 --- a/content/docs/(overview)/items-and-types.mdx +++ b/content/docs/(overview)/items-and-types.mdx @@ -15,7 +15,7 @@ Every item carries three timestamps, all in UTC: - **`created_at`** is when Marfa created the item. - **`updated_at`** is when Marfa last wrote it. A change to a tag or an extension moves it too, but not `version`. -An item in the bin also carries `trashed_at`, when it went there, and a listing of the bin comes most recently binned first. +An item in the bin also carries `trashed_at`, when it went there, and a listing of the bin comes the most recent first. ## What a type does diff --git a/content/docs/run/folder-safeguards.mdx b/content/docs/run/folder-safeguards.mdx index 17e603ae..d49d3ca1 100644 --- a/content/docs/run/folder-safeguards.mdx +++ b/content/docs/run/folder-safeguards.mdx @@ -26,7 +26,7 @@ Remove a folder and add it again, such as after a restore, and **the first sync - **Changed bytes:** a file whose bytes changed meanwhile is sent as a new item under a numbered name, and its item's bytes are written back at its path. - **A read that fails:** a file a note embeds waits while one of the note's own files can't be read, since it may be that one. -**A file taken back is the folder's own:** delete it and its item goes to the bin, and bin its item elsewhere and the file goes, unless you added a YAML comment or an empty property to it. +**A file taken back is the folder's own:** delete it and its item goes to the bin; move the item to the bin elsewhere and the file goes, unless you added a YAML comment or an empty property to it. ## Confirm or undo a large removal @@ -69,7 +69,7 @@ if status.paused.isPaused { ## A missing or unreadable directory -- **A missing directory,** such as on a disk that isn't connected: Marfa moves none of the folder's items to the bin, and `marfa folders status`, `confirm` and `restore` say the directory can't be found. +- **A missing directory,** such as on a disconnected disk: no item goes to the bin, and `marfa folders status`, `confirm` and `restore` say the directory can't be found. - **A scan that can't read or identify an entry:** Marfa moves nothing to the bin for files it failed to see, and holds deletions already waiting until a complete scan succeeds. - **A different directory at the folder's path,** such as one a restore or a sync tool put there: Marfa keeps the folder listed but doesn't read the new directory as the folder. When the original returns, a watch of every folder carries on with it. From 8ac457c084ea8aa1d54f97c73402aac23966605e Mon Sep 17 00:00:00 2001 From: August Cayzer Date: Sun, 1 Nov 2026 01:18:41 +0000 Subject: [PATCH 4/4] test(mcp): cover docs_fetch path forms and the removed /docs/ prefix --- .../docs/(reference)/api-reference/index.mdx | 2 +- src/lib/mcp/server.test.ts | 27 +++++++++++++++++++ src/lib/source.ts | 2 +- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/content/docs/(reference)/api-reference/index.mdx b/content/docs/(reference)/api-reference/index.mdx index 4f9608a5..4ae11b51 100644 --- a/content/docs/(reference)/api-reference/index.mdx +++ b/content/docs/(reference)/api-reference/index.mdx @@ -51,7 +51,7 @@ The reference groups operations by what they act on. The groups and the operatio | Group | What it's for | Start with | | --- | --- | --- | | Items | The records Marfa stores, each of a type, and the ways to read and change them | [Create an item](/api-reference/items/createItem) | -| Trash | Items in the trash: list them, move an item in, and restore, purge or empty them | [List the trash](/api-reference/trash/listTrash) | +| Trash | Items in the bin: list them, move an item in, and restore, purge or empty them | [List the trash](/api-reference/trash/listTrash) | | Bulk actions | One change applied to every item a filter matches, run as a job you can follow and cancel | [Apply a bulk action](/api-reference/bulk-actions/applyBulkAction) | | Metadata | An item's tags and extensions together, and the tags in use | [Get item metadata](/api-reference/metadata/getItemMetadata) | | Extensions | JSON objects attached to an item, one per namespace | [Get an extension namespace](/api-reference/extensions/getItemExtension) | diff --git a/src/lib/mcp/server.test.ts b/src/lib/mcp/server.test.ts index c4ca840d..1ef61b53 100644 --- a/src/lib/mcp/server.test.ts +++ b/src/lib/mcp/server.test.ts @@ -425,6 +425,33 @@ describe('structured results', () => { }); }); +describe('docs_fetch takes the path forms its description lists', () => { + const PAGE = '/files'; + + it('describes the forms it accepts', async () => { + const tools = await listTools(legacyPost({ jsonrpc: '2.0', id: 2, method: 'tools/list' })); + const description = tools.find((t) => t.name === 'docs_fetch')!.inputSchema.properties?.['path']?.description ?? ''; + expect(description).toContain('docs.marfa.so'); + expect(description).toContain('.md'); + expect(description).not.toContain('/docs/'); + }); + + it('answers one page for each form', async () => { + const whole = (await callTool('docs_fetch', { path: PAGE })).content![0]!.text; + for (const path of ['files', '/files.md', 'files.md', '/files/', 'https://docs.marfa.so/files', 'https://docs.marfa.so/files.md']) { + expect((await callTool('docs_fetch', { path })).content![0]!.text, path).toBe(whole); + } + }); + + it('treats a /docs/ prefix as part of the path, so it is a miss', async () => { + const result = await callTool('docs_fetch', { path: '/docs/files' }); + expect(result.isError).toBe(true); + const payload = JSON.parse(result.content![0]!.text) as { error: string; slugs: string[] }; + expect(payload.error).toBe('not_found'); + expect(payload.slugs).toEqual(['docs', 'files']); + }); +}); + describe('docs_fetch takes an optional section', () => { const PAGE = '/files'; const HEADING = 'Who may read a blob'; diff --git a/src/lib/source.ts b/src/lib/source.ts index a3845656..1578ca4f 100644 --- a/src/lib/source.ts +++ b/src/lib/source.ts @@ -102,7 +102,7 @@ function sidebarOrder(): Map { return order; } -/** The written pages in sidebar order, which `llms.txt` promises; a page outside the tree goes last. */ +/** The written pages in the sidebar's order, as `llms.txt` lists them; a page outside the tree goes last. */ export async function getLlmsFullText() { const order = sidebarOrder(); const rank = (page: (typeof source)['$inferPage']) => order.get(page.url) ?? Number.MAX_SAFE_INTEGER;