diff --git a/skills/sanity-kb-setup/SKILL.md b/skills/sanity-kb-setup/SKILL.md new file mode 100644 index 0000000..6f26d4f --- /dev/null +++ b/skills/sanity-kb-setup/SKILL.md @@ -0,0 +1,67 @@ +--- +name: sanity-kb-setup +description: Set up, check and fix a Sanity Context Knowledge Base, then connect coding agents to it over MCP. Use when the user wants a Knowledge Base or "KB" planned, created or built, its conflicts or issues reviewed or resolved, its content corrected or audited, or an agent such as Claude Code, Cursor or Codex connected to one. Also use when they say "what did the build flag" or "pick the winners", or when setup is blocked by a missing Sanity project, login, organisation token or Knowledge Base slot. +compatibility: Needs a Node version supported by the project's installed Sanity packages, a Sanity project with the `sanity` package installed, and `npx sanity login`. Tested with Sanity 6.14.0, which requires Node >=22.12. +metadata: + version: "1.2.0" +--- + +# Sanity Knowledge Base setup + +Take a Sanity project to a Knowledge Base that agents can read and trust. You do the work with the Sanity CLI, the project's own files and the examples in `references/api.md`. + +## Stages + +Read only the file for the stage you are in. + +| Stage | When | Read | +|---|---|---| +| 1. Plan | The project has no `kb-setup.md` | `references/plan.md`, then `references/type-roles-and-queries.md`. For a `turbo-start-sanity` repo, also `references/turbo-start-sanity.md` | +| 2. Create | The user said yes to the plan | `references/create.md` and `references/cli.md` | +| 3. Check | A build or rebuild just finished | `references/check.md` and `references/api.md` | +| 4. Resolve | The user replied with picks such as `1A 2B` | `references/resolve.md` and `references/api.md` | +| 5. Fix content | Issues are resolved and the documents still hold the losing claims | `references/fix-content.md` | +| 6. Connect | The user wants an agent to read the Knowledge Base | `references/connect-agents.md` | +| Blocked | A command fails, a result looks wrong, or something is missing | `references/blocked.md` | + +Stages 2 to 5 are one flow. A finished build sends you straight to stage 3. The stops are the ones marked in the stage files, where a person has to say yes or pick. + +## Where things stand + +Check what exists before any stage. + +- `kb-setup.md` in the project is the plan, called the sheet in these files. It records the Knowledge Base id once one exists. +- `npx sanity context list --organization ` shows the organisation's Knowledge Bases. `references/create.md` says when to reuse one. +- `npx sanity context imports list ` shows which sources already imported. Add each source once. + +Then name the state in these words. + +| State | Means | +|---|---| +| Planned | `kb-setup.md` exists, nothing is created | +| Built | Sources imported and a build finished. Nobody has checked what it says | +| Reviewed | You compared the entries with the source claims, and the user has seen every disagreement, raised or silent | +| Clean | Reviewed, the picks are resolved, the content is corrected, and a rebuild raised nothing new | +| Connected | An agent reads it, and you confirmed the endpoint serves this Knowledge Base | + +Connected doesn't require Clean. Some users keep conflicts on purpose, for a demo or to test a checking tool. If the project looks like that, for example a README that lists planted conflicts, ask before stage 4 or 5. If they keep the conflicts, stop at Reviewed and go to stage 6. + +## How a Knowledge Base works + +Sanity reads the sources, sorts the facts into topics, and writes one cited entry per topic. Where sources disagree it keeps one claim and may file an issue. Agents read the entries over MCP with `initial_context` and `knowledge_base_read`. + +- **The purpose steers the build.** It decides which sources the build keeps, which subjects lead and how deep each entry goes. +- **A dataset source is one GROQ query over one dataset.** It starts with `*[`, reads published documents only, and matches between 1 and 5,000 documents. +- **File sources never re-sync.** To update one, delete the import and add the new file. +- **A silent settlement is a disagreement the build settled without filing an issue.** The issues list misses these, and results vary between builds of the same sources. Only reading the entries finds them. +- **Resolving changes the Knowledge Base, not the website.** The losing claim stays in the document until someone corrects it. + +## Ground rules + +- Name only types, fields and files you have read. A query that names a missing field fails silently and indexes nulls. +- A `0` or `[]` is a suspect zero. Run the checks in `references/blocked.md` before you act on it. +- Content comes from the user. Never seed, import, generate or reset content in a real project, and never offer to. Seed only a project the user calls a demo. +- Never write a token, key or password into a file or a chat message. They live in environment variables, and you never see one. +- A person decides every delete, resolution and dataset write. Delete only the Knowledge Base or import the user names. Resolve only the issues they picked. Write only the exact change list they approved, with every edit guarded by the revision you reviewed, and as drafts unless they ask to publish. +- The CLI's `context` commands and the `client.context` API are missing from Sanity's public docs. If one behaves differently from these files, say so and use the dashboard. +- If your own permission settings refuse a command, give the user the exact command to run. Leave other routes to the same write alone. diff --git a/skills/sanity-kb-setup/assets/kb-setup-template.md b/skills/sanity-kb-setup/assets/kb-setup-template.md new file mode 100644 index 0000000..72f5589 --- /dev/null +++ b/skills/sanity-kb-setup/assets/kb-setup-template.md @@ -0,0 +1,105 @@ +# Knowledge Base setup: {client} + +Generated from {repo} on {date}. Sections 1 to 3 can be created with the Sanity CLI (see the skill's `references/cli.md`) or copied into the Context dashboard. Section 6 is dashboard-only. + +- **Organisation:** {org id} +- **Knowledge Base id:** {filled in after creation} + +## 1. New knowledge base + +- **Title:** {short title. Letters, numbers and spaces only; some tools reject `&`} +- **Purpose:** + ``` + Answer questions from {who asks} about {client}. + Lead with: {three or four subjects}. + Leave out: {what should not be indexed at all. Never list content whose claims need checking}. + ``` + +## 2. Sources + +### Dataset + +- **Project:** {project id} +- **Dataset:** {dataset name} +- **Query:** + ```groq + {query} + ``` +- **Documents matched:** {count, or "unknown" and why} +- **Fallback query** if the dashboard rejects `pt::text()` or conditional projections: + ```groq + {same filter, raw fields only} + ``` + +### Files + +| File | Path | Authority for | +|---|---|---| + +Files never re-sync. Delete and re-upload on every change. + +### Website + +- **URL:** {public URL, or "none" and why} + +## 3. Build + +Build with no instructions, so they can't hide a conflict. A build still misses some conflicts, so read the entries afterwards: `npx sanity context build --watch`, or **Build entries** in the dashboard. + +## 4. Expected outline + +- {topic} [core | peripheral] + +## 5. After the first build + +- Compare the real outline with section 4. A missing topic means a missing source or a purpose that doesn't reach it. +- Read the entries. For each disagreement in section 10, note which claim the entry kept and whether the build raised an issue. +- A person picks the winner of each conflict. Resolve those picks, which creates the instructions. Don't write instructions by hand first. +- Correct the losing claims in the source documents, publish, refresh, then rebuild. + +## 6. MCP endpoint + +- **Title:** {title} +- **Name:** {lowercase-with-hyphens. Cannot be changed later} +- **Instructions:** leave empty +- **Content source:** this Knowledge Base only. Do not add the dataset. +- **URL once created:** `https://api.sanity.io/v1/context/organizations/{org-id}/mcp/{name}` + +## 7. Type roles + +| Type | Role | Included | Reason | +|---|---|---|---| + +## 8. Fields left out + +| Type | Field | Reason | Could hide a conflict? | +|---|---|---|---| + +## 9. Decisions for a person + +- **Homepage:** {included or excluded, and why} +- {other judgement calls} + +## 10. Where conflicts are likely + +- {facts stated in more than one place, with each location} + +## 11. Build results + +- **Built:** {lastChangedAt} +- **Open issues:** {count} +- **Outline compared with section 4:** {missing or unexpected topics} + +| Fact | Knowledge Base says | Other claim | Entries | Severity | +|---|---|---|---|---| + +- **Expected in section 10 but not raised:** {conflicts the build missed, to check on the live page} + +## 12. Resolutions + +- **Chosen by:** {person} on {date} + +| Fact | Winner | Kept or accepted | Source documents corrected | +|---|---|---|---| + +- **After rebuild:** {open conflicts, suggestions and gaps left} diff --git a/skills/sanity-kb-setup/references/api.md b/skills/sanity-kb-setup/references/api.md new file mode 100644 index 0000000..c369cd1 --- /dev/null +++ b/skills/sanity-kb-setup/references/api.md @@ -0,0 +1,117 @@ +# API examples + +`@sanity/client` reads entries, lists issues and resolves them through `client.context`, which the CLI can't. These methods are missing from Sanity's public docs, so expect them to change. Every call below ran against a real Knowledge Base on 2026-09-17. + +Write a short throwaway script from these examples, save it in the user's Sanity project folder, run it with `node`, and delete it. + +## Get a client + +This borrows the project's Sanity CLI, so it uses the `npx sanity login` session and needs no token. + +```js +import { createRequire } from 'node:module' +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' + +const KB_ID = 'kb...' +const API_VERSION = 'v2026-08-25' + +const fromProject = createRequire(join(process.cwd(), 'package.json')) +let cliCore +for (const pkg of ['@sanity/cli/package.json', 'sanity/package.json']) { + try { + const fromCli = createRequire(fromProject.resolve(pkg)) + cliCore = await import(pathToFileURL(fromCli.resolve('@sanity/cli-core')).href) + break + } catch {} +} +const { getGlobalCliClient, getProjectCliClient } = cliCore + +const kb = await (await getGlobalCliClient({ apiVersion: API_VERSION, requireUser: true })) + .context.knowledgeBases.get(KB_ID) + +const client = await getGlobalCliClient({ + apiVersion: API_VERSION, + requireUser: true, + resource: { id: kb.publicId, type: 'knowledge-base' }, + context: { organizationId: kb.organizationId }, +}) +``` + +End the script by setting `process.exitCode`. On Windows, `process.exit()` after network calls can crash Node while sockets close. + +## Read the entries + +```js +const outline = await client.context.entries.list() +// [{ _id, path, title, tldr, status }], status is 'filled' once written + +const entry = await client.context.entries.get({ path: 'delivery' }) +// entry.body is Markdown, entry.citations lists its sources +``` + +This works before any MCP endpoint exists. + +## List issues + +`scripts/kb-issues.mjs --status open` makes this call and formats the result. + +```js +const issues = await client.context.issues.list({ status: 'open' }) // 'open' | 'accepted' | 'rejected' +// issue._id, issue._createdAt, issue.status, and issue.content with: +// kind ('conflict' | 'update_required' | 'gap'), severity, claimKey, +// currentClaim, alternativeClaim, involvedScopes, scopePath, issue, suggestedFix +``` + +`accepted` means resolved. `rejected` means dismissed. + +## Resolve, dismiss, reopen + +```js +const issue = await client.context.issues.get({ issueId }) +if (issue.content.kind !== 'conflict') throw new Error('only conflicts can be resolved') + +// Print the winner and compare it with the user's pick before the call. +await client.context.issues.resolve({ issueId, resolution: 'keep_existing' }) // currentClaim wins, pick A +await client.context.issues.resolve({ issueId, resolution: 'accept_new' }) // alternativeClaim wins, pick B + +await client.context.issues.dismiss({ issueId }) // close a stale issue, a suggestion or a gap +await client.context.issues.reopen({ issueId }) // undo a resolve, deletes the instruction it created +``` + +## Guarded edits + +A guarded edit carries the revision you reviewed, so it can't overwrite a newer change. The example targets a published document. Run it for real only when the user approved a direct publish of that exact change. + +```js +const data = await getProjectCliClient({ + apiVersion: 'v2025-08-15', + projectId: '', + dataset: '', + requireUser: true, + useCdn: false, + perspective: 'raw', // so drafts. is visible +}) + +const doc = await data.getDocument('faq-main') // note doc._rev and the field's current value +const draft = await data.getDocument('drafts.faq-main') // someone's unpublished edits, if not null + +await data + .transaction() + .patch('faq-main', (p) => + p.ifRevisionId(doc._rev).set({ 'items[_key=="ret"].answer': 'You have 14 days from delivery...' }), + ) + .commit({ dryRun: true }) // drop dryRun once the user has approved the change +``` + +- `ifRevisionId` fails the commit with a 409 if anyone edited the document after you read it. Nothing is written. Read it again and show the user the new value. +- `commit({ dryRun: true })` validates the whole transaction without writing. +- Address array items by `_key`, never by index. +- In draft mode, edit only the draft. Never patch the published document. + +| Mode | Do | +|---|---| +| Draft, and no draft exists | Create `drafts.` from the published document with `transaction.create(...)`, then patch the draft | +| Draft, and one exists | Patch the draft with its own `_rev`. Tell the user it holds other unpublished edits, and keep them | +| Approved direct publish, and a draft holds the losing claim | Put the draft's correction in the approved change list, and patch both documents in one transaction, each with its own reviewed revision. Keep the draft's unrelated edits | +| Direct publish where the draft's correction isn't approved | The user reconciles the draft in Studio first. Publishing that draft later would restore the losing claim | diff --git a/skills/sanity-kb-setup/references/blocked.md b/skills/sanity-kb-setup/references/blocked.md new file mode 100644 index 0000000..e818124 --- /dev/null +++ b/skills/sanity-kb-setup/references/blocked.md @@ -0,0 +1,83 @@ +# When something blocks you + +Find the symptom, tell the user what it means in one sentence, and give them the exact step. Steps that need a browser login, a token or a plan belong to the user. Don't work around them. + +## Setup + +| Symptom | Means | Step | +|---|---|---| +| No `sanity.config.ts` or `sanity.cli.ts`, in the root or under `apps/studio` | No Sanity project here | If they have one elsewhere, work in that folder. If not, they run `npm create sanity@latest`, or `npx sanity@latest init` inside a Next.js app for an embedded Studio. They finish the schema and publish content before stage 1 | +| "You must be logged in", or a command asks for a login | No CLI session | The user runs `npx sanity login` and picks their provider in the browser. You can't do this for them. Afterwards `npx sanity projects list` prints their projects. Stages 1 to 5 use that login and need no token | +| `npx sanity context` doesn't exist | The `sanity` package is too old | Update it with the project's package manager, then check `npx sanity context --help`. If the commands are still missing, do stage 2 in the dashboard at `sanity.io/@/context` from `kb-setup.md` | +| No Context app in the dashboard, or create returns "not enabled" | Knowledge Bases are an opt-in beta | In `sanity.io/manage`, an organisation admin turns on Context from the organisation's **Labs** page and Knowledge Bases from its **Apps** page. A user who isn't an admin asks the organisation's owner | +| No organisation id | It is in `npx sanity projects list` and in the dashboard URL `sanity.io/@/...` | A project outside any organisation can't have a Knowledge Base. The user moves it into one from the project's settings in Manage | + +## A suspect zero + +A query returned `0` or `[]`. In testing, the same query returned nothing through one shell and four products through another. Check in this order. + +1. **Quoting.** A shim may have stripped the quotes. Use the tested form or the direct Node fallback in `cli.md`. +2. **The target.** Confirm the project id and dataset against `sanity.cli.ts` and the Studio the user edits. Pass `--dataset ` if the project has several. +3. **The CLI's wording.** Wrap counts in an object, as `cli.md` shows. +4. **The type name.** If `count(*)` is above 0, the dataset has content and your filter is wrong. List the types with `"array::unique(*[]._type)"`. +5. **Drafts.** A Knowledge Base can't see content that exists only as drafts. The CLI query may hide drafts, so look with the `perspective: 'raw'` client from `api.md`, using `*[_id in path('drafts.**')]._id`. + +The dataset is empty only after all five pass. + +| Project | Step | +|---|---| +| Real | Stop. The Knowledge Base needs published content, and it comes from the user, through Studio or their own migration. Never seed, import or generate content here, and don't offer to | +| Demo, meaning the user calls it a demo or a test, usually with fictional content and a seed script in the repo | A seed script is fine. Ask before running it, because seed scripts often replace documents by id and wipe Studio edits. It usually needs a write token in `.env.local`, which the user creates under the **project's** API settings in Manage with Editor permission and pastes in themselves. When the script reads a token, give the user the command and let them run it. Never open `.env.local` | + +## Creating + +**"The organization's plan limit for knowledge bases is reached (2 of 2 used)", or "Organization is at its limit".** The cap counts every Knowledge Base in the organisation, across all its projects. List them with `npx sanity context list --organization ` and show the table. First check whether one matches the plan's audience, purpose and sources, because reuse needs no slot. Belonging to the same project alone is not a match. Otherwise the user chooses. + +- **Delete one they name.** Deletion is permanent and removes its entries, issues and instructions. Before they choose, check which Knowledge Base each MCP endpoint reads, so a working connection survives. Never delete one on your own judgement, even an obvious test. +- **Upgrade the plan.** Sanity doesn't publish the numbers, so they check billing in Manage. +- **Use another organisation**, which has its own allowance. Client work usually belongs in the client's organisation. + +| Symptom | Means | Step | +|---|---|---| +| 403 on create or on a dataset import | A missing role | `cli.md` lists the roles. Say which one is missing, and skip the retry | +| "Invalid GROQ filter ... Unexpected end of query" | A shim cut the query at a newline | Pass it on one line, as `cli.md` shows | +| The import fails on the query, or matches 0 documents | Usually quoting | Check the quoting, then that the query starts with `*[` and every field exists in the schema. Then try the sheet's fallback query and say which one worked. Above 5,000 matches needs a narrower filter | +| "fetch failed" on an import or a refresh | A network error | Run `imports list` or `context get` so a retry doesn't repeat work that went through, then retry once | + +## Building and resolving + +| Symptom | Means | Step | +|---|---|---| +| A rebuild raises conflicts the user already resolved | The documents still hold the losing claims, or the rebuild ran without a refresh | Do stage 5 in order, which is fix, publish, refresh, build. Leave the repeats unresolved | +| `pendingChanges` is all zeros after a refresh, though documents changed | The edits are drafts, or sit in documents the query doesn't match | Query the documents to check | +| Issues quote text that no longer exists | A build leaves earlier builds' issues open | Confirm the current value, then dismiss them once the user agrees | +| "Could not find the Sanity CLI in this folder", or `@sanity/cli-core` won't resolve | Wrong folder, or no install | Run from the Sanity project folder, where `node_modules/sanity` exists. Install first if `node_modules` is missing | +| `client.context` is undefined, or a method is missing | The Sanity packages are too old, or the API changed | Update `sanity`. Failing that, the dashboard has Entries for reading, and Issues for picking a claim and pressing **Resolve** | +| The build sits in `review` | Normal while issues are open | It reaches `ready` when none are left. Agents can read entries in both states | + +## Fixing content + +| Symptom | Means | Step | +|---|---|---| +| 409 on commit | Someone edited the document after you read it. The guard held and nothing was written | Read it again, show the user the new value, and redo that document's change list | +| 401 or 403 on commit | The logged-in user can't write to the dataset | They need Editor or higher on the project. Otherwise give them the change list to apply in Studio | +| The edits don't show on the website | They are drafts, or the site is cached | Publish. A cached site may then need a redeploy or a revalidation | +| You can't find the losing claim | It is worded differently, split across spans, carried by a boolean or number, or lives in a file or the code | `fix-content.md` step 1 lists where to look | + +## Connecting + +| Symptom | Means | Step | +|---|---|---| +| 401 | The token is missing, expired or mistyped, or the app can't see the variable | On Windows, `setx` reaches only apps started afterwards, so restart the app fully. For Cursor, see the `${env:...}` bug in `connect-agents.md` | +| 401 with "Not a member of this organization" | The token belongs to a different organisation, often left in the environment by an earlier project, or it isn't a valid token at all. A made-up token gets the same message | The user creates one in this organisation and stores it under its own variable name. `connect-agents.md` step 2 has the link | +| 403 with `contextGrantRequired`, or "No access to knowledge base ''. Requires one of: sanity.knowledge-base.read" | A project token, or no Context Viewer. The second message names the Knowledge Base the endpoint serves, so the endpoint itself is right | The user creates a new token at organisation level, from the link in `connect-agents.md` step 2 | +| 404 | The organisation id or endpoint name in the URL is wrong | The name is the one chosen at creation, not the title | +| 400 or 406 | A header is missing | Send `Content-Type: application/json` and `Accept: application/json, text/event-stream` | +| Error -32005, or a 200 with `"isError": true` | The endpoint has no readable Knowledge Base. None is attached, or the first build hasn't finished | Read the body on every call. A 200 doesn't mean the tool call worked | +| Four tools instead of two | A dataset source is attached, so the endpoint runs in GROQ mode | The user removes the dataset source, or creates a new endpoint with the Knowledge Base only | +| Answers come from a different project | The URL points at another endpoint, often from an old `SANITY_CONTEXT_MCP_URL` | Write the URL out in full, and check the Knowledge Base id in `initial_context` against `kb-setup.md` | +| The agent answers without calling the tools | It isn't using the server | Name the server in the question, such as "use the `` tools". Check the agent lists it as connected. Some agents load MCP settings only at startup | + +## Your own permissions refuse a command + +Tell the user what you tried and why, and give them the exact command in a code block so they can run it or allow it. Leave other routes to the same write alone. diff --git a/skills/sanity-kb-setup/references/check.md b/skills/sanity-kb-setup/references/check.md new file mode 100644 index 0000000..6bbb365 --- /dev/null +++ b/skills/sanity-kb-setup/references/check.md @@ -0,0 +1,67 @@ +# Stage 3. Check + +Run this after every build. It moves the Knowledge Base from Built to Reviewed. That takes three kinds of evidence, because each one misleads when read alone. + +| Evidence | Tells you | Misses | +|---|---|---| +| The source claims, from section 10 of the sheet and your own reading | Disagreements in the material you inspected | Claims you didn't inspect, and which claim is correct | +| The entries the build wrote | Which claim the Knowledge Base kept | Whether anyone was told | +| The issues list | Which disagreements the build raised | Every silent settlement | + +In testing, a build kept "we ship to the US" from an FAQ against a delivery policy that said UK and Ireland only, and listed a wrong £30 offer as a live promotion. It raised no issue for either. Only the entries showed it. + +## Steps + +1. **Read the state** with `npx sanity context get --json`. Note `state`, `instructionCount`, `lastChangedAt` and `pendingChanges`. Ignore `openIssueCount`. +2. **Read every entry.** `api.md` shows `entries.list()` and `entries.get({ path })`, which work before any MCP endpoint exists. If an endpoint is already connected to you, `initial_context` and `knowledge_base_read` return the same text. +3. **List the open issues** with `node /scripts/kb-issues.mjs --status open --json`, run from the project folder. Always run it, whatever `openIssueCount` says. The counter is wrong in both directions. In testing it read 2 with 1 issue open, 0 with 7 open, and 10 with 13 open. The list is the only count to report or act on. In the `--json` output it is `knowledgeBase.openInList`. +4. **Build the comparison**, one row per disagreement from section 10 plus any you spot in the entries. + + | Fact | Source claims | The entry says | Issue raised? | + |---|---|---|---| + | Countries delivered to | Policy: UK and Ireland. FAQ: also the US | "UK, Ireland and the US" | No | + + A "No" in the last column is a silent settlement. Those matter most. If the entry kept the wrong claim, agents are giving wrong answers now. +5. **Check each open issue against the current documents.** A build leaves issues from earlier builds open, so an issue can quote text that has since changed. Mark those stale. +6. **Compare the outline with the plan's** and note missing topics. Note any claim in an entry that no source makes. In testing, a build added a Friday dispatch rule that appeared nowhere in the content. +7. **Append the table and the counts** to `kb-setup.md` under "Build results". +8. **Print the choices**, then stop and wait. + +Done when every row of section 10 has an entry value and a yes or no for the issue. + +## The choice format + +Use plain text, never an interactive widget. Plain text works in every agent, and the user can answer in one line. + +``` +Raised by the build + +1. Free UK delivery threshold + A. Over £50 ← the delivery policy says this + B. Over £75 (from the FAQ) + +Settled silently by the build, no issue raised + +2. Countries delivered to. The entry currently says "UK, Ireland and the US". + A. UK and Ireland only ← the delivery policy says this + B. Also the US (from the FAQ) + +Reply with your picks, for example: 1A 2A. Skip any you're unsure of. +``` + +- For a raised conflict, **A** is the issue's `currentClaim`, which is what the Knowledge Base says now. **B** is its `alternativeClaim`. +- A silent settlement has no issue to resolve. Its pick decides which documents stage 5 corrects. Tell the user that. +- Mark the side the authoritative source supports, such as a policy, the legal terms or a spec sheet. Don't pick for the user. +- Shorten long claims to the fact that differs. Keep the numbers exact. +- Two issues can state the same fact, sometimes with the sides swapped. Say so, so the answers agree. +- List suggestions, gaps and stale issues last, under their own headings. Those get applied or dismissed, not resolved. + +## After the choices + +| The user | Do | +|---|---| +| Picks winners | Go to stage 4 | +| Wants the conflicts kept, for a demo or a test | Record that in `kb-setup.md`. The state is Reviewed. Go to stage 6 if they want an agent connected, and resolve or fix nothing | +| Has nothing to pick, because the build and your comparison both came back empty | Say both checks were empty. A build can still hide something neither caught | + +Report every known disagreement before you move on, even when the issues list is empty. diff --git a/skills/sanity-kb-setup/references/cli.md b/skills/sanity-kb-setup/references/cli.md new file mode 100644 index 0000000..79f3c66 --- /dev/null +++ b/skills/sanity-kb-setup/references/cli.md @@ -0,0 +1,78 @@ +# Sanity CLI for Knowledge Bases + +Found in `@sanity/cli` 8.11.0 under `sanity context`. `npx sanity context --help` is the source of truth if a command here fails. + +## Commands + +| Task | Command | +|---|---| +| List Knowledge Bases | `npx sanity context list --organization --json` | +| Create | `npx sanity context create --organization --title "" --description "<purpose>"` | +| Read state | `npx sanity context get <kb-id> --json` | +| Add dataset source | `npx sanity context imports create <kb-id> --sanity-project <project-id> --sanity-dataset <dataset> --query "<groq>"` | +| Add file | `npx sanity context imports create <kb-id> --file <path>` | +| Add website | `npx sanity context imports create <kb-id> --url <url>` | +| Add inline text | `npx sanity context imports create <kb-id> --text "<text>" --title "<title>"` | +| List sources | `npx sanity context imports list <kb-id> --json` | +| Remove a source | `npx sanity context imports delete <kb-id> <import-id> --yes` | +| Build | `npx sanity context build <kb-id> --watch` | +| Refresh | `npx sanity context refresh <kb-id>` | +| Refresh schedule | `npx sanity context update <kb-id> --refresh-enabled --refresh-frequency weekly` | +| Job status | `npx sanity context jobs get <kb-id> <job-id> --watch` | +| Delete | `npx sanity context delete <kb-id> --yes` | + +Both delete commands refuse without `--yes` when no terminal is attached, with "Deletion requires confirmation". `--yes` skips the CLI's own prompt, so the user naming that Knowledge Base or import is the only confirmation there is. Get it before you run either one. + +Bash expands backticks and `$(...)` inside double quotes. Before you run a command, check that every value you put in it, whether a title, purpose, query, inline text, path or URL, holds neither, and no unescaped `"`. If one does, escape it for the user's shell or have the user enter the value in the dashboard. + +`context get --json` returns `state`, `openIssueCount`, `instructionCount`, `lastChangedAt` for the last build, `pendingChanges`, `sourceUsage` and the refresh schedule. Ignore `openIssueCount`. `check.md` says why. + +The dataset import accepts `pt::text()` and conditional projections such as `_type == 'x' => { }`. + +## Quoting on Windows + +Three traps produce a misleading empty result or error. + +| Trap | Symptom | Do | +|---|---|---| +| A command shim strips double quotes, so `_type == "product"` arrives as `_type == product` | `[]` or `0` | Use single quotes inside the query and wrap the argument in double quotes | +| The `.cmd` shims behind `npx` and `pnpm exec` cut an argument at the first newline | `Invalid GROQ filter ... Unexpected end of query` | Pass the query on one line | +| The CLI prints "Query returned no results" for a bare `0` | A true zero looks like a failure | Wrap counts in an object | + +This form passed in Windows PowerShell 5.1, through `npx` and direct Node, and in bash: + +``` +npx sanity documents query "{'n': count(*[_type == 'product'])}" +``` + +To pass `kb-query.groq` on one line: + +- bash: `--query "$(tr '\n' ' ' < kb-query.groq | tr -s ' ')"` +- PowerShell: `--query ((Get-Content -Raw kb-query.groq) -replace '\s+', ' ')` + +If a command still misbehaves through a shim, call the CLI directly with `node node_modules/sanity/bin/sanity <command>`. That skips the shim, but Windows PowerShell 5.1 may still lose double quotes, so keep the single quotes. In the Lark and Kettle test the same double-quoted query returned `[]` through `pnpm exec` and four products through direct Node. + +## Transient errors + +`fetch failed` on an import or a refresh is a network error. Run `imports list` or `context get` first so a retry doesn't repeat work that went through, then retry once. + +## What the CLI can't do + +| Task | Use | +|---|---| +| Create an MCP endpoint | The Context dashboard. New endpoint, this Knowledge Base as the only source. Endpoints are organisation documents the client can read but not create | +| Read entries, list issues, resolve issues | `api.md`, or `scripts/kb-issues.mjs` to list issues | +| Add an instruction with custom wording | The dashboard. The client only lists them | + +## Issue states + +`scripts/kb-issues.mjs --status` takes `open`, `accepted` for resolved, or `rejected` for dismissed. Only `conflict` issues resolve. `update_required` suggestions and `gap` issues get applied in the dashboard or dismissed. + +## Permissions + +The CLI acts as the logged-in user and ignores tokens. A 403 usually means one of these is missing. Say which one, and skip the retry. + +| Action | Needs | +|---|---| +| Create a Knowledge Base | Administrator or Developer on the organisation | +| Add a dataset source | Administrator or Developer on the project, plus unrestricted read on the dataset | diff --git a/skills/sanity-kb-setup/references/connect-agents.md b/skills/sanity-kb-setup/references/connect-agents.md new file mode 100644 index 0000000..885c730 --- /dev/null +++ b/skills/sanity-kb-setup/references/connect-agents.md @@ -0,0 +1,146 @@ +# Stage 6. Connect coding agents + +If the user only says "connect an agent", ask which kind they mean. + +| Kind | Do | +|---|---| +| A coding agent, such as Claude Code, Cursor or Codex, reading the Knowledge Base while they work | This file | +| An agent inside their application, such as a support chatbot | Steps 1 to 3 so the endpoint works, then Sanity's guide at `https://www.sanity.io/docs/ai/sanity-context`. It needs an LLM provider and application code, which this skill doesn't build | + +## The two values every agent needs + +``` +URL: https://api.sanity.io/v1/context/organizations/<org-id>/mcp/<endpoint-name> +Header: Authorization: Bearer <organisation token with Context Viewer> +``` + +In Knowledge Base mode the endpoint serves two tools, `initial_context` and `knowledge_base_read`. Agents call `initial_context` first, as the server's own instructions say. The dashboard's ready-made setup prompt lists four, `initial_context`, `schema_explorer`, `groq_query` and `array_field_reader`, which are the GROQ mode tools. Trust what the endpoint returns in step 3, not that list. + +## 1. The user creates the endpoint + +The CLI can't. Walk the user through it. + +1. Open `sanity.io/@<org-id>/context`. +2. Next to **MCP endpoints**, press **New**. +3. Name it with lowercase letters, numbers and hyphens, up to 64 characters. Sanity rejects `by-name` and any name made of `mcp` plus eight characters. The name goes in the URL and can't change later. +4. Pick this Knowledge Base as the **only** source. With a dataset source attached too, the endpoint runs in GROQ mode and ignores the Knowledge Base. +5. Leave the instructions empty. + +## 2. The user creates the token + +You can't create it and must never see it. Give the user these steps. + +1. Open `https://www.sanity.io/organizations/<org-id>/api/tokens`. Give the user this link with the org id filled in. From `sanity.io/manage` it is the **organisation**, not a project, then **API**, then **Tokens**. A URL containing `/project/<id>/api` is the project's token page, which offers only Developer, Editor, Contributor and Viewer. +2. Add a token with **Context Viewer** permission only. A project token fails with a 403, however broad its permissions, because Editor and the other project roles say nothing about the organisation's Knowledge Bases. +3. Create one token per tool, named after it, such as `kb-cursor`. A leaked token then breaks one tool. +4. Store it in an environment variable, never in a committed file. Pick the variable name first. It is `<TOKEN_VAR>` in every command and config below. + - Default to `SANITY_ORGANIZATION_TOKEN`. + - Check whether that name is already set for another organisation. If it is, leave it alone, because overwriting it breaks that organisation's agents with a 401. Use a name of its own, such as `SANITY_<PROJECT>_CONTEXT_TOKEN`. + + Then the user stores the token under that name. + - Windows: `setx <TOKEN_VAR> "<token>"` in PowerShell or cmd, then fully restart the agent app. An app that stays in the system tray has to be quit from there, because closing its window leaves it running. + - macOS and Linux: add `export <TOKEN_VAR>="<token>"` to the shell profile, then restart the terminal. + +A Context Viewer token reads every endpoint in the organisation, and hosted tools such as v0, Lovable and Replit store it on their servers. For client work, use the client's own organisation. + +## 3. Confirm the endpoint serves this Knowledge Base + +A working connection may still be the wrong Knowledge Base. In testing, an inherited `SANITY_CONTEXT_MCP_URL` pointed at another project's endpoint, and every check passed against the wrong content. Always write the URL out in full. Don't read it from an environment variable someone set earlier. + +If the endpoint is already connected to you as MCP tools, call `initial_context` yourself. Otherwise use this one-off diagnostic request, tested on 2026-09-17. Use the form for the shell you are in. PowerShell reads an environment variable as `$env:NAME`. A bash-style `$NAME` expands to nothing there, so the header goes out empty and the endpoint answers 401. + +```bash +curl -s -X POST "https://api.sanity.io/v1/context/organizations/<org-id>/mcp/<endpoint-name>" \ + -H "Authorization: Bearer $<TOKEN_VAR>" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"initial_context","arguments":{}}}' +``` + +```powershell +$headers = @{ Authorization = "Bearer $env:<TOKEN_VAR>"; Accept = "application/json, text/event-stream" } +$body = '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"initial_context","arguments":{}}}' +$uri = "https://api.sanity.io/v1/context/organizations/<org-id>/mcp/<endpoint-name>" +try { (Invoke-WebRequest -UseBasicParsing -Method Post -Uri $uri -Headers $headers -ContentType "application/json" -Body $body).Content } +catch { $r = $_.Exception.Response; if ($r) { "HTTP " + [int]$r.StatusCode; $s = $r.GetResponseStream(); $s.Position = 0; (New-Object IO.StreamReader($s)).ReadToEnd() } else { $_.Exception.Message } } +``` + +Windows PowerShell 5.1 throws on a 401, 403 or 404, so the `catch` prints the status and the body. `blocked.md` matches on that body text. In 5.1, `curl` is an alias for `Invoke-WebRequest`, so the bash form fails there. `setx` reaches only shells started afterwards, so open a new one first. + +Done when all four hold. `blocked.md` covers the HTTP errors. + +1. The text contains ``Knowledge base id: `<kb-id>` `` with the id from `kb-setup.md`. A different id, or several, means the endpoint has the wrong sources for this setup. Sanity allows several Knowledge Bases on one endpoint, but then every question has to name the right one, so this skill uses one. +2. The response has no `"isError": true`. MCP reports a failed tool inside a successful response, so an HTTP 200 proves nothing. +3. With the body `{"jsonrpc":"2.0","id":1,"method":"tools/list"}`, the endpoint lists two tools. Four means GROQ mode, so go back to step 1.4. +4. `knowledge_base_read` with `{"knowledgeBase":"<kb-id>","paths":["<entry-path>"]}` returns a body that matches what stage 3 read. + +## 4. Configure the agent + +Claude Code, Cursor and Codex were checked against their docs on 2026-09-16, and the hosted tools on 2026-09-17. MCP settings change often, so the tool's own docs win where they differ. + +### Claude Code + +`.mcp.json` in the project. Claude Code keeps a project server inactive until the user trusts the workspace and approves that server, which it asks for when they next run `claude` there. + +```json +{ + "mcpServers": { + "<kb-name>": { + "type": "http", + "url": "https://api.sanity.io/v1/context/organizations/<org-id>/mcp/<endpoint-name>", + "headers": { "Authorization": "Bearer ${<TOKEN_VAR>}" } + } + } +} +``` + +### Cursor + +`.cursor/mcp.json` in the project, or `~/.cursor/mcp.json` for all projects. + +```json +{ + "mcpServers": { + "<kb-name>": { + "url": "https://api.sanity.io/v1/context/organizations/<org-id>/mcp/<endpoint-name>", + "headers": { "Authorization": "Bearer ${env:<TOKEN_VAR>}" } + } + } +} +``` + +Cursor's syntax is `${env:NAME}`, not `${NAME}`. A reported bug makes remote servers send the literal `${env:...}` string. On a 401, the user puts the token in the user-level `~/.cursor/mcp.json`, never in a project file that gets committed. Third-party pricing guides say MCP needs Cursor Pro or higher. + +### Codex + +`~/.codex/config.toml`, or `.codex/config.toml` in the repo. Codex reads the repo file only in a project the user has trusted. + +```toml +[mcp_servers.<kb-name>] +url = "https://api.sanity.io/v1/context/organizations/<org-id>/mcp/<endpoint-name>" +bearer_token_env_var = "<TOKEN_VAR>" +``` + +### Hosted tools and any other agent + +Every tool needs the same two values. Enter the endpoint URL as a direct remote server. Where the tool offers a choice of authentication, pick the bearer token option instead of OAuth, because Sanity Context has no OAuth flow. If the tool only offers custom headers, add one named `Authorization` with the value `Bearer <token>`. The token must travel in that header, never in a query string or a differently named header. The user pastes the token themselves. + +| Tool | Steps | +|---|---| +| v0 | Open **Settings**, then **Integrations**, then **MCP server**, or open the **+** menu in the prompt form and choose **MCPs**. Add a custom server with the URL, choose **Bearer Token** and paste the token. v0 warns that remote MCP can raise the cost per message | +| Lovable | Open **Connectors**, press **+**, choose **MCP server**. Keep the **Direct** connection type and enter the URL. Choose **Bearer token or API key** instead of OAuth and paste the token | +| Replit | In the **Integrations** pane, add a custom MCP server with the URL, and add a custom header named `Authorization` with the value `Bearer <token>` | +| Any other agent | It needs support for a remote HTTP MCP server with a custom header. Give it the URL and the `Authorization` header, following the tool's own MCP docs | + +## 5. Test inside the agent + +Ask two or three questions whose answers you know from the content. A good answer calls `initial_context`, then `knowledge_base_read`, and cites sources. An answer with no tool calls came from the model's own knowledge. + +Include a question that names something only this project has, such as a product name. A right answer confirms the agent reads this Knowledge Base. + +To check a specific claim, ask "Is this text accurate: '<claim>'?". A Knowledge Base that is only Built or Reviewed gives unreliable verdicts. In testing it accepted a wrong promotion and doubted a correct cut-off time until the conflicts were resolved and the content fixed. + +## Known limits of the answers + +- A citation inside an entry can point at the wrong source document, so build no Studio field links from them. +- A build can add a claim no source makes. Stage 3 looks for those. diff --git a/skills/sanity-kb-setup/references/create.md b/skills/sanity-kb-setup/references/create.md new file mode 100644 index 0000000..3941bb6 --- /dev/null +++ b/skills/sanity-kb-setup/references/create.md @@ -0,0 +1,30 @@ +# Stage 2. Create + +This stage adds a Knowledge Base to a real organisation. Plans cap how many an organisation can hold. The commands are in `cli.md`. + +Show the user the organisation, the title and the sources, and wait for a clear yes before the first command. + +## Steps + +1. **List the organisation's Knowledge Bases.** This proves the login works. Reuse one only when its audience, purpose and sources match the plan, and follow "Resuming" below. A different audience or purpose justifies a new one over the same content. If the organisation is at its limit, open `blocked.md`. Never delete one to make room unless the user names it. +2. **Create it** with the title, and the purpose as its description. Write the returned id, which starts with `kb`, into `kb-setup.md`. +3. **Add the dataset source** from `kb-query.groq`, passed on one line as `cli.md` shows. +4. **Add each file source**, then the website source if there is one. +5. **Check the imports.** Done when every import is complete and the document count matches the plan. +6. **Build** with `npx sanity context build <kb-id> --watch`. A first build takes a few minutes. +7. **Tell the user the step the CLI can't do.** They create the MCP endpoint in the Context dashboard with this Knowledge Base as its only source. `connect-agents.md` has the steps, and it can wait until stage 6. + +The state is now Built, which says nothing about whether the content is right. Go straight to stage 3. + +Add no instructions by hand. An instruction overrides the sources, so one added now hides the conflicts the first build exists to find. Stage 4 creates them from the user's picks. + +## Resuming an existing Knowledge Base + +An existing import proves the content was imported once, not that it is current. Read the description and the imports, and compare them with `kb-setup.md` by project, dataset, query, website URLs and file versions. + +- Add only the missing sources. Show any change to the purpose or the sources before you make it. +- Replace a changed file's import only after the user names that import for deletion. +- Refresh the dataset and website sources with `npx sanity context refresh <kb-id>`, then wait on the job with `npx sanity context jobs get <kb-id> <job-id> --watch`. +- Done when the imports and the refresh job succeeded and `pendingChanges` matches what changed. If content changed and it shows zeros, check publication and the query. + +Then continue from step 5. Keep the existing instructions and record them in `kb-setup.md` when you review the result. diff --git a/skills/sanity-kb-setup/references/fix-content.md b/skills/sanity-kb-setup/references/fix-content.md new file mode 100644 index 0000000..d089f0e --- /dev/null +++ b/skills/sanity-kb-setup/references/fix-content.md @@ -0,0 +1,40 @@ +# Stage 5. Fix content, refresh, rebuild + +The documents still hold the losing claims, so the website shows them and the next build raises them again. This stage corrects the documents and brings the Knowledge Base up to date. + +You know this project's schema, so prepare small, specific edits for it. + +## Steps + +1. **Find where each losing claim lives**, using the schema and `npx sanity documents query`, not a blind text search. + - Start from the documents the issue cites, then ask which other types could state the same fact. A return window can sit in a policy, an FAQ answer, a homepage banner and a product field at once. + - Project Portable Text with `pt::text(body)`. Formatting can split "28 " and "days" into separate spans that a string match misses. + - Check other wordings, such as "28-day", "twenty-eight" or "four weeks". + - Check booleans and numbers that carry the same fact, such as `dishwasherSafe: true` beside "dishwasher safe" in a description. + - Check the file sources, and search the site's code for hardcoded copies. + + Done when every location is listed. Say where you looked, because finding nothing doesn't prove the claim is gone. +2. **Read each document before you plan its edit.** Note its `_rev` and the field's current value. Check whether `drafts.<id>` exists, which means someone has unpublished edits. +3. **Show the change list and get a yes.** Use one row per field, with the document, the field, the old value and the new value. If a document has a draft, show the draft's current value separately from the published value. Keep each edit as small as the fact allows. Changing `28` to `14` is safe. Rewriting a paragraph needs the user to read it. Ask whether they want drafts to review in Studio, the default and the right choice for a live site, or a direct publish. +4. **Write guarded edits**, each tied to the revision you reviewed. In order of preference: + - a Sanity MCP server or other write tool you already have, if it supports revision checks, + - the example in `api.md`, run with `dryRun: true` first, + - the user edits in Studio from your change list. + + Whichever path you use, open `api.md` first and follow its draft rules. In draft mode, edit only the draft. For a direct publish, a draft that holds the losing claim needs its own approved correction. +5. **Handle what isn't a dataset field.** For a file source, put the file's correction, the import to delete and the new upload in the change list. Once the user approves them and names that import, correct the file, delete the old import and add the new file. For a claim hardcoded in the site's code, report the file and line. +6. **Verify.** The Knowledge Base reads published documents only, so wait until the user has published any drafts. Done when a query of the changed fields returns the new values. +7. **Refresh, then rebuild**, in that order. A build reuses the content from the last import, so a rebuild straight after an edit still sees the old text. + ``` + npx sanity context refresh <kb-id> + npx sanity context jobs get <kb-id> <job-id> --watch + npx sanity context get <kb-id> --json + npx sanity context build <kb-id> --watch + ``` + After the refresh, `pendingChanges.changed` counts the documents you edited. A `0` means the edits aren't published or the query doesn't match those documents. +8. **Run stage 3 again**, entries included. Confirm each corrected fact in its entry. Dismiss stale issues that quote the old text. Print any new conflict as a choice. +9. **Record it** under "Resolutions" in `kb-setup.md`, with the picks, the documents corrected and the final counts. + +## Clean + +`state` is `ready`, the issues list shows 0 open conflicts, `pendingChanges` is all zeros, and every row of the stage 3 table shows the winning claim in its entry. diff --git a/skills/sanity-kb-setup/references/plan.md b/skills/sanity-kb-setup/references/plan.md new file mode 100644 index 0000000..5b0d4fb --- /dev/null +++ b/skills/sanity-kb-setup/references/plan.md @@ -0,0 +1,35 @@ +# Stage 1. Plan + +Read the repo and write `kb-setup.md` and `kb-query.groq`. This stage changes nothing in Sanity. + +## Preconditions + +`blocked.md` has the fix for each one that fails. + +1. `sanity.config.ts` or `sanity.cli.ts` exists, in the root or under `apps/studio`. +2. `npx sanity context --help` lists the `context` commands. +3. `npx sanity projects list` prints the user's projects. +4. No `kb-setup.md` exists. If one does, read it and continue from the state it records. + +## Steps + +1. **Gather inputs by reading them.** + - Project id and dataset, from `sanity.cli.ts`, `sanity.config.ts` or the env example file. + - Organisation id, from `npx sanity projects list` or the dashboard URL `sanity.io/@<org-id>/...`. + - The schema. Prefer `schema.json` from `npx sanity schema extract`, otherwise the schema type files. + - Published document counts per type, with `npx sanity documents query "{'n': count(*[_type == '<type>'])}"`. `cli.md` explains the quoting. + - Front-end routes, which show the types that render as pages. + - Repo files that state facts, such as terms, policies, pricing, care guides and specs. These become file sources. + - The public site URL, if the site is deployed and open. + + Done when every input has a value, or "not found" plus where you looked. +2. **Verify the counts.** Each `0` is a suspect zero until the checks in `blocked.md` pass. If a real project's dataset is empty, stop and tell the user the content has to come from them. +3. **Give every document type one role** and decide what to include, using `type-roles-and-queries.md`. +4. **Write one GROQ query** with a projection per included type. Keep every field that states a fact, booleans and numbers included. A `turbo-start-sanity` repo has `apps/studio`, `packages/sanity-blocks`, or a `pageBuilder` with `hero`, `cta` and `faqAccordion` blocks. For those, start from `turbo-start-sanity.md`. Done when the query runs and matches the documents you mean to include. +5. **Write the purpose** in three lines that say who asks, what leads, and what is left out. Two audiences means two Knowledge Bases. "Leave out" makes the build drop that material, so list only what should never be indexed. Content you include so its claims get checked stays off that line. In testing, "Leave out: homepage promotions" stopped the build from checking a wrong promo claim. +6. **Predict the outline**, marking each topic core or peripheral. If the sources can only produce one or two topics, say so. +7. **List every disagreement you saw** while reading, in section 10 of the sheet, with both claims and where each lives. Stage 3 checks each one against the entries. +8. **Write `kb-setup.md`** from `assets/kb-setup-template.md`. Save the query alone to `kb-query.groq` with single-quoted strings. Where you couldn't find something, write "not found" and where you looked. +9. **Show the plan** in a few lines. Give the title, purpose, included types, document count, files, and the decisions a person should make. + +Stop here. The state is Planned. Stage 2 starts when the user says to create the Knowledge Base. diff --git a/skills/sanity-kb-setup/references/resolve.md b/skills/sanity-kb-setup/references/resolve.md new file mode 100644 index 0000000..7968c41 --- /dev/null +++ b/skills/sanity-kb-setup/references/resolve.md @@ -0,0 +1,27 @@ +# Stage 4. Resolve + +Resolving creates a standing instruction, the same as the dashboard's Resolve button. The instruction overrides the sources from then on, so resolve only what the user picked. + +The CLI has no command for this. Write a throwaway script from the calls in `api.md`, run it from the project folder, and delete it. + +## Steps + +1. **Split the picks.** Picks on raised conflicts get resolved here. Picks on silent settlements have no issue, so they go straight to stage 5. +2. **Map each raised pick.** **A** is `keep_existing`, so the current claim wins. **B** is `accept_new`, so the other source's claim wins. +3. **Read each issue back and print the claim that will win** before the call. It must match the user's pick. Issue ids and their order change between builds, so list the issues fresh and never resolve by list position from an earlier run. +4. **Resolve**, then read the issue again. Done when its `status` is `accepted`. +5. **Dismiss** the stale issues and obsolete suggestions the user agreed to close. +6. **Show a table** of fact, winner, and keep or accept. +7. **Go to stage 5.** Until the content is fixed, the website still shows the losing claims. + +If the user would rather click, the dashboard does the same under **Issues**. They pick the claim and press **Resolve**. Continue at stage 5. + +## Special cases + +| Case | Do | +|---|---| +| Neither claim is right | Leave the issue open. Correct the documents in stage 5 and rebuild. An instruction in the user's own words is written in the dashboard under Instructions | +| A wrong pick | `issues.reopen` clears the resolution and deletes its instruction | +| A suggestion or a gap | Only `conflict` issues resolve. `update_required` and `gap` issues get applied in the dashboard or dismissed | +| Two open issues state the same fact | Resolve the one with the later `_createdAt`. Tell the user about the other and dismiss it once they agree. Resolving both creates duplicate instructions | +| A rebuild raises a conflict the user already resolved | The source still holds the losing claim, or the rebuild ran without a refresh. Don't resolve it a second time, because that adds a duplicate instruction. Go to stage 5 | diff --git a/skills/sanity-kb-setup/references/turbo-start-sanity.md b/skills/sanity-kb-setup/references/turbo-start-sanity.md new file mode 100644 index 0000000..969bd35 --- /dev/null +++ b/skills/sanity-kb-setup/references/turbo-start-sanity.md @@ -0,0 +1,80 @@ +# turbo-start-sanity baseline + +Taken from `robotostudio/turbo-start-sanity` `apps/studio/schema.json`. Clients customise the starter, so check every type and field against the project's own schema before using this. + +## Query + +```groq +*[_type in ['faq', 'page', 'homePage', 'blogIndex', 'settings']]{ + _type, + _type == 'faq' => { + 'question': title, + 'answer': pt::text(richText) + }, + _type in ['page', 'homePage', 'blogIndex'] => { + title, + description, + 'blocks': pageBuilder[]{ + _type, + badge, + eyebrow, + title, + subtitle, + 'body': pt::text(richText), + 'cards': cards[]{ title, 'body': pt::text(richText) }, + caption + } + }, + _type == 'settings' => { + siteTitle, + siteDescription, + contactEmail + } +} +``` + +Add the client's own fact types, such as `product` or `policy`, as further branches. + +## Types + +| Type | Role | Kept | Left out | +|---|---|---|---| +| `faq` | Fact | `title` as question, `richText` as answer | none | +| `page` | Page content | `title`, `description`, block text | `slug`, `image`, SEO and OG fields | +| `homePage` | Page content | as `page` | as `page` | +| `blogIndex` | Page content | as `page` | as `page` | +| `settings` | Fact | `siteTitle`, `siteDescription`, `contactEmail` | logos, favicon, OG image, social links | +| `blog`, `author` | Editorial | excluded | | +| `navbar`, `footer`, `redirect` | Structural | excluded | | +| `assist.instruction.context`, `mux.videoAsset`, `media.folder`, `media.tag`, `sanity.fileAsset`, `sanity.imageAsset` | System | excluded | | + +## Page-builder blocks + +| Block | Text kept | Dropped | +|---|---|---| +| `hero` | `badge`, `title`, `richText` | `video`, `buttons` | +| `heroSplit` | `title`, `subtitle` | `buttons`, `image` | +| `cta` | `eyebrow`, `title`, `richText` | `buttons`, `usedByTeams` | +| `featureCardsIcon` | `eyebrow`, `title`, `richText`, cards' `title` and `richText` | icons | +| `richTextBlock` | `eyebrow`, `title`, `richText` | none | +| `videoFeature` | `eyebrow`, `title`, `richText`, `caption` | `video` | +| `faqAccordion` | `eyebrow`, `title`, `subtitle` | `categories[].faqs`, which reference `faq` documents already read directly | +| `socialGrid` | `eyebrow`, `title`, `subtitle` | `socials` | +| `showcaseGrid` | `title` | `description`, `items` | +| `logoCloud` | nothing | `logos` | +| `subscribeNewsletter` | `title` | `subTitle`, `helperText`, `testimonial` | + +`showcaseGrid.description` is dropped because the projection has no `description` key at block level. Add it if a client uses the block for facts. + +## Routes + +| Route | Type | +|---|---| +| `/` | `homePage` | +| `/[...slug]` | `page` | +| `/blog` | `blogIndex` | +| `/blog/[slug]` | `blog` | + +## Expected outline + +A bare starter holds no policy or product types, so the outline will be thin, often just an FAQ topic and a homepage topic. Say so in the sheet. Most useful topics (services, how to buy, pricing, support, policies) need client-specific types, files, or written knowledge documents. diff --git a/skills/sanity-kb-setup/references/type-roles-and-queries.md b/skills/sanity-kb-setup/references/type-roles-and-queries.md new file mode 100644 index 0000000..a26462c --- /dev/null +++ b/skills/sanity-kb-setup/references/type-roles-and-queries.md @@ -0,0 +1,62 @@ +# Type roles and query rules + +## Roles + +Give every document type exactly one role, with a one-line reason. If a type fits none, say so. + +| Role | What it holds | Default | +|---|---|---| +| Fact | Policies, product specs, FAQs, help articles, knowledge documents | Include, projected to the fields that state facts | +| Page content | Pages built from page-builder blocks, including the homepage | Include text fields only, and record the homepage decision | +| Editorial | Blog posts, news, case studies, author bios | Exclude from a support Knowledge Base. They date quickly and argue positions. A separate Knowledge Base if needed | +| Structural | Navigation, footer, settings, redirects | Exclude. Keep only settings fields that state a fact, such as a contact email | +| Live data | Prices, stock, availability | Exclude. A compiled index goes stale on anything that changes daily | +| System | `sanity.*`, `media.*`, `mux.*`, `assist.*` | Exclude | + +## Projection rules + +- **Keep every field that states a fact, whatever its type.** That covers booleans and numbers such as `dishwasherSafe`, a capacity, a weight or a warranty length. In testing, a `dishwasherSafe: true` flag contradicted the care text, and the build saw it only because the query kept the flag. Drop slugs, images, video, buttons, links, icons, SEO and Open Graph fields. +- **Convert Portable Text** with `pt::text(field)` so the build reads prose. +- **Page-builder arrays.** Project each block's text fields and keep `_type`, so an entry can say which kind of block a claim came from. A field a block lacks comes back null, which is harmless. +- **Don't follow references to documents the query already reads.** If FAQ blocks reference FAQ documents and the query reads those directly, following the reference indexes each answer twice and muddies citations. +- **List every field left out of a fact type** in the sheet. In testing, a product `description` said a skillet was dishwasher safe while the care guide said hand wash only. Dropping `description` would have hidden that conflict. + +## Query shape + +One query, one projection branch per type. + +```groq +*[_type in ['typeA', 'typeB']]{ + _type, + _type == 'typeA' => { title, 'body': pt::text(body) }, + _type == 'typeB' => { name, spec, warranty } +} +``` + +The query is ready when all four hold. + +- It starts with `*[`. The import rejects a bare filter. +- Every type in the filter exists in the schema and has published documents. The import rejects a query that matches nothing. +- It matches at most 5,000 documents. Narrow the filter for a larger catalogue, and note the split. +- Document ids contain no dots. Sanity reads the part before a dot as a version namespace, so `product.skillet` never reaches the published perspective or the index. Flag any such ids. + +## The homepage decision + +Hero badges, promo banners and call-to-action blocks are where unchecked claims usually live, and the build may give them little weight. + +| Choice | Cost | +|---|---| +| Include | The build may ignore it without raising a conflict | +| Exclude | Only a check against the live page will catch a wrong claim | + +Record the choice in the sheet's decisions section. If you include it, keep it off the purpose's "Leave out" line, for the reason in `plan.md` step 5. + +## Files and instructions + +- Each file is the authority for the facts it holds. Name those facts in the sheet. +- An instruction corrects one fact and is tied to the documents that state it. Stage 4 creates them after the first build. +- An instruction is archived when every source it is tied to is dropped, and re-uploading a file drops it. Tie any file-based instruction to a dataset document as well. + +## Endpoints + +An endpoint with both a dataset and a Knowledge Base attached serves the dataset and ignores the Knowledge Base. Give the Knowledge Base endpoint the Knowledge Base only. diff --git a/skills/sanity-kb-setup/scripts/kb-issues.mjs b/skills/sanity-kb-setup/scripts/kb-issues.mjs new file mode 100644 index 0000000..1e092bb --- /dev/null +++ b/skills/sanity-kb-setup/scripts/kb-issues.mjs @@ -0,0 +1,126 @@ +#!/usr/bin/env node +/** + * List the issues on a Sanity Context Knowledge Base. + * + * node kb-issues.mjs <knowledge-base-id> [--status open|accepted|rejected] [--json] + * + * Run it from a Sanity project folder. It borrows the Sanity CLI installed in + * that project and the CLI's login, so run `npx sanity login` first. Read-only. + * + * Issues are not in Sanity's public docs. They are `sanity.context.issue` + * documents that `@sanity/client` reads through `client.context.issues`. + */ +import { createRequire } from 'node:module' +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' + +const API_VERSION = 'v2026-08-25' + +const args = process.argv.slice(2) +const flag = (name) => { + const index = args.indexOf(name) + return index === -1 ? undefined : args[index + 1] +} +const knowledgeBaseId = args.find((arg, index) => !arg.startsWith('--') && !args[index - 1]?.startsWith('--status')) +const status = flag('--status') +const asJson = args.includes('--json') + +if (!knowledgeBaseId) { + console.error('Usage: node kb-issues.mjs <knowledge-base-id> [--status open|accepted|rejected] [--json]') + console.error('Find the id with: npx sanity context list') + process.exit(1) +} + +async function loadCliCore() { + const fromProject = createRequire(join(process.cwd(), 'package.json')) + for (const entry of ['@sanity/cli/package.json', 'sanity/package.json']) { + try { + const fromCli = createRequire(fromProject.resolve(entry)) + return await import(pathToFileURL(fromCli.resolve('@sanity/cli-core')).href) + } catch { + // try the next entry + } + } + console.error('Could not find the Sanity CLI in this folder. Run this from a project with `sanity` installed.') + process.exit(1) +} + +const { getGlobalCliClient } = await loadCliCore() + +// After a network call, set process.exitCode and let Node finish. On Windows, +// process.exit() can crash Node while sockets close, and it can cut off piped output. +const knowledgeBase = await readKnowledgeBase() +if (knowledgeBase) await listIssues(knowledgeBase) + +async function readKnowledgeBase() { + try { + const client = await getGlobalCliClient({ apiVersion: API_VERSION, requireUser: true }) + return await client.context.knowledgeBases.get(knowledgeBaseId) + } catch (error) { + console.error(`Could not read knowledge base "${knowledgeBaseId}": ${error.message}`) + console.error('Check the id with `npx sanity context list`, and that `npx sanity login` is done.') + process.exitCode = 1 + return null + } +} + +async function listIssues(knowledgeBase) { + let issues + try { + const client = await getGlobalCliClient({ + apiVersion: API_VERSION, + requireUser: true, + resource: { id: knowledgeBase.publicId, type: 'knowledge-base' }, + context: { organizationId: knowledgeBase.organizationId }, + }) + issues = await client.context.issues.list(status ? { status } : undefined) + } catch (error) { + console.error(`Could not list issues for "${knowledgeBaseId}": ${error.message}`) + console.error('A 403 means the logged-in user lacks access to this Knowledge Base. A missing `context.issues` means the project\'s `sanity` package is too old.') + process.exitCode = 1 + return + } + + // Sanity's openIssueCount is often wrong. When the list includes open issues, count them here + // so both outputs carry a number that can be trusted next to the counter. + const listCoversOpen = !status || status === 'open' + const openInList = listCoversOpen ? issues.filter((issue) => issue.status === 'open').length : null + + if (asJson) { + console.log(JSON.stringify({ knowledgeBase: { ...summary(knowledgeBase), openInList }, issues }, null, 2)) + return + } + + console.log(`${knowledgeBase.title} (${knowledgeBase.publicId})`) + console.log(`State: ${knowledgeBase.state}. Last built: ${knowledgeBase.lastChangedAt ?? 'never'}.`) + console.log(`Showing ${issues.length} ${status ?? 'issues of any status'}.`) + if (listCoversOpen) { + console.log(`${openInList} open in this list, which is the count to trust. Sanity's openIssueCount reads ${knowledgeBase.openIssueCount} and is often wrong.`) + } + console.log('') + + issues.forEach((issue, index) => { + const content = issue.content ?? {} + console.log(`${index + 1}. [${content.severity ?? 'unknown'}] ${content.kind ?? 'issue'}, ${issue.status}`) + if (content.claimKey) console.log(` Fact: ${content.claimKey}`) + if (content.currentClaim) console.log(` KB says: ${content.currentClaim}`) + if (content.alternativeClaim) console.log(` Other: ${content.alternativeClaim}`) + if (content.involvedScopes?.length) console.log(` Entries: ${content.involvedScopes.join(', ')}`) + else if (content.scopePath) console.log(` Entry: ${content.scopePath}`) + if (content.issue) console.log(` Detail: ${content.issue}`) + if (content.suggestedFix) console.log(` Next step: ${content.suggestedFix}`) + console.log(` Id: ${issue._id}\n`) + }) +} + +function summary(kb) { + return { + id: kb.publicId, + title: kb.title, + organizationId: kb.organizationId, + state: kb.state, + lastChangedAt: kb.lastChangedAt, + openIssueCount: kb.openIssueCount, + instructionCount: kb.instructionCount, + } +}